Skip to content

Commit

Permalink
Dramatically improve "Fast" linear-RGB conversions.
Browse files Browse the repository at this point in the history
This is following the discussion in #18.
Documentation and a notebook for deriving the constants will follow.
  • Loading branch information
lucasb-eyer committed Aug 26, 2017
1 parent df73472 commit 34cdce3
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 5 deletions.
48 changes: 44 additions & 4 deletions colors.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,11 +333,23 @@ func (col Color) LinearRgb() (r, g, b float64) {
return
}

// A much faster and still quite precise linearization using a 6th-order Taylor approximation.
// See the accompanying Jupyter notebook for derivation of the constants.
func linearize_fast(v float64) float64 {
v1 := v - 0.5
v2 := v1*v1
v3 := v2*v1
v4 := v2*v2
//v5 := v3*v2
return -0.248750514614486 + 0.925583310193438*v + 1.16740237321695*v2 + 0.280457026598666*v3 - 0.0757991963780179*v4 //+ 0.0437040411548932*v5
}

// FastLinearRgb is much faster than and almost as accurate as LinearRgb.
// BUT it is important to NOTE that they only produce good results for valid colors r,g,b in [0,1].
func (col Color) FastLinearRgb() (r, g, b float64) {
r = math.Pow(col.R, 2.2)
g = math.Pow(col.G, 2.2)
b = math.Pow(col.B, 2.2)
r = linearize_fast(col.R)
g = linearize_fast(col.G)
b = linearize_fast(col.B)
return
}

Expand All @@ -353,9 +365,37 @@ func LinearRgb(r, g, b float64) Color {
return Color{delinearize(r), delinearize(g), delinearize(b)}
}

func delinearize_fast(v float64) float64 {
// This function (fractional root) is much harder to linearize, so we need to split.
if v > 0.2 {
v1 := v - 0.6
v2 := v1*v1
v3 := v2*v1
v4 := v2*v2
v5 := v3*v2
return 0.442430344268235 + 0.592178981271708*v - 0.287864782562636*v2 + 0.253214392068985*v3 - 0.272557158129811*v4 + 0.325554383321718*v5
} else if v > 0.03 {
v1 := v - 0.115
v2 := v1*v1
v3 := v2*v1
v4 := v2*v2
v5 := v3*v2
return 0.194915592891669 + 1.55227076330229*v - 3.93691860257828*v2 + 18.0679839248761*v3 - 101.468750302746*v4 + 632.341487393927*v5
} else {
v1 := v - 0.015
v2 := v1*v1
v3 := v2*v1
v4 := v2*v2
v5 := v3*v2
// You can clearly see from the involved constants that the low-end is highly nonlinear.
return 0.0519565234928877 + 5.09316778537561*v - 99.0338180489702*v2 + 3484.52322764895*v3 - 150028.083412663*v4 + 7168008.42971613*v5
}
}

// FastLinearRgb is much faster than and almost as accurate as LinearRgb.
// BUT it is important to NOTE that they only produce good results for valid inputs r,g,b in [0,1].
func FastLinearRgb(r, g, b float64) Color {
return Color{math.Pow(r, 1.0/2.2), math.Pow(g, 1.0/2.2), math.Pow(b, 1.0/2.2)}
return Color{delinearize_fast(r), delinearize_fast(g), delinearize_fast(b)}
}

// XyzToLinearRgb converts from CIE XYZ-space to Linear RGB space.
Expand Down
68 changes: 67 additions & 1 deletion colors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package colorful

import (
"math"
"math/rand"
"strings"
"testing"
"image/color"
)

var bench_result float64 // Dummy for benchmarks to avoid optimization

// Checks whether the relative error is below eps
func almosteq_eps(v1, v2, eps float64) bool {
if math.Abs(v1) > delta {
Expand Down Expand Up @@ -197,7 +200,70 @@ func TestHexConversion(t *testing.T) {

/// Linear ///
//////////////
// TODO (implicitly tested by XYZ)

// LinearRgb itself is implicitly tested by XYZ conversions below (they use it).
// So what we do here is just test that the FastLinearRgb approximation is "good enough"
func TestFastLinearRgb(t *testing.T) {
const eps = 6.0/255.0 // We want that "within 6 RGB values total" is "good enough".

for r := 0.0 ; r < 256.0 ; r++ {
for g := 0.0 ; g < 256.0 ; g++ {
for b := 0.0 ; b < 256.0 ; b++ {
c := Color{r/255.0, g/255.0, b/255.0}
r_want, g_want, b_want := c.LinearRgb()
r_appr, g_appr, b_appr := c.FastLinearRgb()
dr, dg, db := math.Abs(r_want-r_appr), math.Abs(g_want-g_appr), math.Abs(b_want-b_appr)
if dr+dg+db > eps {
t.Errorf("FastLinearRgb not precise enough for %v: differences are (%v, %v, %v), allowed total difference is %v", c, dr, dg, db, eps)
return
}

c_want := LinearRgb(r/255.0, g/255.0, b/255.0)
c_appr := FastLinearRgb(r/255.0, g/255.0, b/255.0)
dr, dg, db = math.Abs(c_want.R-c_appr.R), math.Abs(c_want.G-c_appr.G), math.Abs(c_want.B-c_appr.B)
if dr+dg+db > eps {
t.Errorf("FastLinearRgb not precise enough for (%v, %v, %v): differences are (%v, %v, %v), allowed total difference is %v", r, g, b, dr, dg, db, eps)
return
}
}
}
}
}

// Also include some benchmarks to make sure the `Fast` versions are actually significantly faster!
// (Sounds silly, but the original ones weren't!)

func BenchmarkColorToLinear(bench *testing.B) {
var r, g, b float64
for n := 0; n < bench.N; n++ {
r, g, b = Color{rand.Float64(), rand.Float64(), rand.Float64()}.LinearRgb()
}
bench_result = r + g + b
}

func BenchmarkFastColorToLinear(bench *testing.B) {
var r, g, b float64
for n := 0; n < bench.N; n++ {
r, g, b = Color{rand.Float64(), rand.Float64(), rand.Float64()}.FastLinearRgb()
}
bench_result = r + g + b
}

func BenchmarkLinearToColor(bench *testing.B) {
var c Color
for n := 0; n < bench.N; n++ {
c = LinearRgb(rand.Float64(), rand.Float64(), rand.Float64())
}
bench_result = c.R + c.G + c.B
}

func BenchmarkFastLinearToColor(bench *testing.B) {
var c Color
for n := 0; n < bench.N; n++ {
c = FastLinearRgb(rand.Float64(), rand.Float64(), rand.Float64())
}
bench_result = c.R + c.G + c.B
}

/// XYZ ///
///////////
Expand Down

0 comments on commit 34cdce3

Please sign in to comment.