From 6baa77e7ae721a16c9009c3f8e0529825a1c5617 Mon Sep 17 00:00:00 2001 From: Jacek Olszak Date: Fri, 25 Jul 2025 15:31:34 +0200 Subject: [PATCH] feat: use perceptual color comparison in pi.DecodeCanvas Use perceptual color comparison in pi.DecodeCanvas for more accurate matching. --- internal/color.go | 19 ++++++---- internal/test/decode/indexed-brighter.png | Bin 0 -> 161 bytes internal/test/decode/indexed.png | Bin 0 -> 161 bytes internal/test/decode/rgb-brighter.png | Bin 0 -> 133 bytes internal/test/decode/rgb.png | Bin 0 -> 133 bytes surface_test.go | 41 ++++++++++++++++++++++ 6 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 internal/test/decode/indexed-brighter.png create mode 100644 internal/test/decode/indexed.png create mode 100644 internal/test/decode/rgb-brighter.png create mode 100644 internal/test/decode/rgb.png diff --git a/internal/color.go b/internal/color.go index 6b2dcd5b..c945bdd6 100644 --- a/internal/color.go +++ b/internal/color.go @@ -30,16 +30,16 @@ func (c *ClosestColorPicker[RGB, Color]) IndexInPalette(rgba color.Color) (Color for i, paletteCol := range c.Palette { r, g, b, _ := rgba.RGBA() r, g, b = r&0xff, g&0xff, b&0xff - r2, g2, b2 := paletteCol>>16&0xff, paletteCol>>8&0xff, paletteCol&0xff - if r == uint32(r2) && g == uint32(g2) && b == uint32(b2) { + r2, g2, b2 := uint32(paletteCol>>16&0xff), + uint32(paletteCol>>8&0xff), + uint32(paletteCol&0xff) + if r == r2 && g == g2 && b == b2 { // found perfect match. Short circuit closestColor = Color(i) break } - distance := math.Sqrt( - math.Pow(float64(r2-RGB(r)), 2) + - math.Pow(float64(g2-RGB(g)), 2) + - math.Pow(float64(b2-RGB(b)), 2)) + distance := perceptualColorDistance(r, g, b, r2, g2, b2) + if distance < smallestDistance { smallestDistance = distance closestColor = Color(i) @@ -50,3 +50,10 @@ func (c *ClosestColorPicker[RGB, Color]) IndexInPalette(rgba color.Color) (Color return closestColor, nil } + +func perceptualColorDistance(r1, g1, b1, r2, g2, b2 uint32) float64 { + rd := float64(r1 - r2) + gd := float64(g1 - g2) + bd := float64(b1 - b2) + return math.Sqrt(0.299*rd*rd + 0.587*gd*gd + 0.114*bd*bd) +} diff --git a/internal/test/decode/indexed-brighter.png b/internal/test/decode/indexed-brighter.png new file mode 100644 index 0000000000000000000000000000000000000000..3a6992a5b393978126b0e35e7549d20748fd373e GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^EFjFm3?%32=lK9B#^NA%Cx&(BWL^R}#sNMdu0UE< z+0sYHsld)`Zk*%S8qN0`3jSYP`TxK5ia!x07foyyR|XXuM+K>yJK4Fq)|+JZD;(bE z3)Cj#>Eal|F*7+KAtfm-Au%;M{lSALkJ27IeEOK3fmwl}@<{N80-!nuPgg&ebxsLQ E000*>JOBUy literal 0 HcmV?d00001 diff --git a/internal/test/decode/indexed.png b/internal/test/decode/indexed.png new file mode 100644 index 0000000000000000000000000000000000000000..81f0ad571e8a9de0edca0a8e18236939346d0a0d GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^EFjFm3?%32=lK9B#^NA%Cx&(BWL^R}#sNMdu0UEz z!NgU=Ce6~YKg{}mq1w#_Y3~lt`u)jl=F@mdKI;Vst E0CbKreE&CtCmj literal 0 HcmV?d00001 diff --git a/surface_test.go b/surface_test.go index 07fedeb9..9c7ee382 100644 --- a/surface_test.go +++ b/surface_test.go @@ -4,6 +4,9 @@ package pi_test import ( + _ "embed" + "github.com/elgopher/pi/pitest" + "github.com/stretchr/testify/require" "math/rand" "testing" @@ -12,6 +15,44 @@ import ( "github.com/elgopher/pi" ) +var ( + //go:embed "internal/test/decode/indexed.png" + indexedPNG []byte + //go:embed "internal/test/decode/rgb.png" + rgbPNG []byte + //go:embed "internal/test/decode/indexed-brighter.png" + brighterIndexedPNG []byte + //go:embed "internal/test/decode/rgb-brighter.png" + rgbBrighterPNG []byte +) + +func TestDecodeCanvasOrErr(t *testing.T) { + tests := map[string][]byte{ + "indexed png when palette is the same": indexedPNG, + "RGB png": rgbPNG, + "indexed png when palette is slightly brighter": brighterIndexedPNG, + "RGB png when palette is slightly brighter": rgbBrighterPNG, + } + + for testName, png := range tests { + t.Run(testName, func(t *testing.T) { + pi.Palette = pi.DecodePalette(indexedPNG) + // when + canvas, err := pi.DecodeCanvasOrErr(png) + // then + require.NoError(t, err) + expected := pi.NewCanvas(4, 4) + expected.SetAll( + 0, 1, 2, 3, + 4, 5, 6, 7, + 8, 9, 10, 11, + 12, 13, 14, 15, + ) + pitest.AssertSurfaceEqual(t, expected, canvas) + }) + } +} + func TestSet(t *testing.T) { t.Run("should be noop when outside surface", func(t *testing.T) { width := 2