Commit 28388c4e authored by Nigel Tao's avatar Nigel Tao

image/color: have Palette.Index honor alpha for closest match, not just

red, green and blue.

Fixes #9902

Change-Id: Ibffd0aa2f98996170e39a919296f69e9d5c71545
Reviewed-on: https://go-review.googlesource.com/8907Reviewed-by: default avatarRob Pike <r@golang.org>
parent f68f554d
...@@ -271,32 +271,39 @@ func (p Palette) Convert(c Color) Color { ...@@ -271,32 +271,39 @@ func (p Palette) Convert(c Color) Color {
} }
// Index returns the index of the palette color closest to c in Euclidean // Index returns the index of the palette color closest to c in Euclidean
// R,G,B space. // R,G,B,A space.
func (p Palette) Index(c Color) int { func (p Palette) Index(c Color) int {
// A batch version of this computation is in image/draw/draw.go. // A batch version of this computation is in image/draw/draw.go.
cr, cg, cb, _ := c.RGBA() cr, cg, cb, ca := c.RGBA()
ret, bestSSD := 0, uint32(1<<32-1) ret, bestSum := 0, uint32(1<<32-1)
for i, v := range p { for i, v := range p {
vr, vg, vb, _ := v.RGBA() vr, vg, vb, va := v.RGBA()
// We shift by 1 bit to avoid potential uint32 overflow in sum := sqDiff(cr, vr) + sqDiff(cg, vg) + sqDiff(cb, vb) + sqDiff(ca, va)
// sum-squared-difference. if sum < bestSum {
delta := (int32(cr) - int32(vr)) >> 1 if sum == 0 {
ssd := uint32(delta * delta)
delta = (int32(cg) - int32(vg)) >> 1
ssd += uint32(delta * delta)
delta = (int32(cb) - int32(vb)) >> 1
ssd += uint32(delta * delta)
if ssd < bestSSD {
if ssd == 0 {
return i return i
} }
ret, bestSSD = i, ssd ret, bestSum = i, sum
} }
} }
return ret return ret
} }
// sqDiff returns the squared-difference of x and y, shifted by 2 so that
// adding four of those won't overflow a uint32.
//
// x and y are both assumed to be in the range [0, 0xffff].
func sqDiff(x, y uint32) uint32 {
var d uint32
if x > y {
d = x - y
} else {
d = y - x
}
return (d * d) >> 2
}
// Standard colors. // Standard colors.
var ( var (
Black = Gray16{0} Black = Gray16{0}
......
...@@ -90,3 +90,29 @@ func TestCMYKToRGBConsistency(t *testing.T) { ...@@ -90,3 +90,29 @@ func TestCMYKToRGBConsistency(t *testing.T) {
} }
} }
} }
func TestPalette(t *testing.T) {
p := Palette{
RGBA{0xff, 0xff, 0xff, 0xff},
RGBA{0x80, 0x00, 0x00, 0xff},
RGBA{0x7f, 0x00, 0x00, 0x7f},
RGBA{0x00, 0x00, 0x00, 0x7f},
RGBA{0x00, 0x00, 0x00, 0x00},
RGBA{0x40, 0x40, 0x40, 0x40},
}
// Check that, for a Palette with no repeated colors, the closest color to
// each element is itself.
for i, c := range p {
j := p.Index(c)
if i != j {
t.Errorf("Index(%v): got %d (color = %v), want %d", c, j, p[j], i)
}
}
// Check that finding the closest color considers alpha, not just red,
// green and blue.
got := p.Convert(RGBA{0x80, 0x00, 0x00, 0x80})
want := RGBA{0x7f, 0x00, 0x00, 0x7f}
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
...@@ -554,6 +554,20 @@ func clamp(i int32) int32 { ...@@ -554,6 +554,20 @@ func clamp(i int32) int32 {
return i return i
} }
// sqDiff returns the squared-difference of x and y, shifted by 2 so that
// adding four of those won't overflow a uint32.
//
// x and y are both assumed to be in the range [0, 0xffff].
func sqDiff(x, y int32) uint32 {
var d uint32
if x > y {
d = uint32(x - y)
} else {
d = uint32(y - x)
}
return (d * d) >> 2
}
func drawPaletted(dst Image, r image.Rectangle, src image.Image, sp image.Point, floydSteinberg bool) { func drawPaletted(dst Image, r image.Rectangle, src image.Image, sp image.Point, floydSteinberg bool) {
// TODO(nigeltao): handle the case where the dst and src overlap. // TODO(nigeltao): handle the case where the dst and src overlap.
// Does it even make sense to try and do Floyd-Steinberg whilst // Does it even make sense to try and do Floyd-Steinberg whilst
...@@ -563,14 +577,15 @@ func drawPaletted(dst Image, r image.Rectangle, src image.Image, sp image.Point, ...@@ -563,14 +577,15 @@ func drawPaletted(dst Image, r image.Rectangle, src image.Image, sp image.Point,
// dst.At. The dst.Set equivalent is a batch version of the algorithm // dst.At. The dst.Set equivalent is a batch version of the algorithm
// used by color.Palette's Index method in image/color/color.go, plus // used by color.Palette's Index method in image/color/color.go, plus
// optional Floyd-Steinberg error diffusion. // optional Floyd-Steinberg error diffusion.
palette, pix, stride := [][3]int32(nil), []byte(nil), 0 palette, pix, stride := [][4]int32(nil), []byte(nil), 0
if p, ok := dst.(*image.Paletted); ok { if p, ok := dst.(*image.Paletted); ok {
palette = make([][3]int32, len(p.Palette)) palette = make([][4]int32, len(p.Palette))
for i, col := range p.Palette { for i, col := range p.Palette {
r, g, b, _ := col.RGBA() r, g, b, a := col.RGBA()
palette[i][0] = int32(r) palette[i][0] = int32(r)
palette[i][1] = int32(g) palette[i][1] = int32(g)
palette[i][2] = int32(b) palette[i][2] = int32(b)
palette[i][3] = int32(a)
} }
pix, stride = p.Pix[p.PixOffset(r.Min.X, r.Min.Y):], p.Stride pix, stride = p.Pix[p.PixOffset(r.Min.X, r.Min.Y):], p.Stride
} }
...@@ -578,10 +593,10 @@ func drawPaletted(dst Image, r image.Rectangle, src image.Image, sp image.Point, ...@@ -578,10 +593,10 @@ func drawPaletted(dst Image, r image.Rectangle, src image.Image, sp image.Point,
// quantErrorCurr and quantErrorNext are the Floyd-Steinberg quantization // quantErrorCurr and quantErrorNext are the Floyd-Steinberg quantization
// errors that have been propagated to the pixels in the current and next // errors that have been propagated to the pixels in the current and next
// rows. The +2 simplifies calculation near the edges. // rows. The +2 simplifies calculation near the edges.
var quantErrorCurr, quantErrorNext [][3]int32 var quantErrorCurr, quantErrorNext [][4]int32
if floydSteinberg { if floydSteinberg {
quantErrorCurr = make([][3]int32, r.Dx()+2) quantErrorCurr = make([][4]int32, r.Dx()+2)
quantErrorNext = make([][3]int32, r.Dx()+2) quantErrorNext = make([][4]int32, r.Dx()+2)
} }
// Loop over each source pixel. // Loop over each source pixel.
...@@ -590,30 +605,25 @@ func drawPaletted(dst Image, r image.Rectangle, src image.Image, sp image.Point, ...@@ -590,30 +605,25 @@ func drawPaletted(dst Image, r image.Rectangle, src image.Image, sp image.Point,
for x := 0; x != r.Dx(); x++ { for x := 0; x != r.Dx(); x++ {
// er, eg and eb are the pixel's R,G,B values plus the // er, eg and eb are the pixel's R,G,B values plus the
// optional Floyd-Steinberg error. // optional Floyd-Steinberg error.
sr, sg, sb, _ := src.At(sp.X+x, sp.Y+y).RGBA() sr, sg, sb, sa := src.At(sp.X+x, sp.Y+y).RGBA()
er, eg, eb := int32(sr), int32(sg), int32(sb) er, eg, eb, ea := int32(sr), int32(sg), int32(sb), int32(sa)
if floydSteinberg { if floydSteinberg {
er = clamp(er + quantErrorCurr[x+1][0]/16) er = clamp(er + quantErrorCurr[x+1][0]/16)
eg = clamp(eg + quantErrorCurr[x+1][1]/16) eg = clamp(eg + quantErrorCurr[x+1][1]/16)
eb = clamp(eb + quantErrorCurr[x+1][2]/16) eb = clamp(eb + quantErrorCurr[x+1][2]/16)
ea = clamp(ea + quantErrorCurr[x+1][3]/16)
} }
if palette != nil { if palette != nil {
// Find the closest palette color in Euclidean R,G,B space: the // Find the closest palette color in Euclidean R,G,B,A space:
// one that minimizes sum-squared-difference. We shift by 1 bit // the one that minimizes sum-squared-difference.
// to avoid potential uint32 overflow in sum-squared-difference.
// TODO(nigeltao): consider smarter algorithms. // TODO(nigeltao): consider smarter algorithms.
bestIndex, bestSSD := 0, uint32(1<<32-1) bestIndex, bestSum := 0, uint32(1<<32-1)
for index, p := range palette { for index, p := range palette {
delta := (er - p[0]) >> 1 sum := sqDiff(er, p[0]) + sqDiff(eg, p[1]) + sqDiff(eb, p[2]) + sqDiff(ea, p[3])
ssd := uint32(delta * delta) if sum < bestSum {
delta = (eg - p[1]) >> 1 bestIndex, bestSum = index, sum
ssd += uint32(delta * delta) if sum == 0 {
delta = (eb - p[2]) >> 1
ssd += uint32(delta * delta)
if ssd < bestSSD {
bestIndex, bestSSD = index, ssd
if ssd == 0 {
break break
} }
} }
...@@ -626,11 +636,13 @@ func drawPaletted(dst Image, r image.Rectangle, src image.Image, sp image.Point, ...@@ -626,11 +636,13 @@ func drawPaletted(dst Image, r image.Rectangle, src image.Image, sp image.Point,
er -= int32(palette[bestIndex][0]) er -= int32(palette[bestIndex][0])
eg -= int32(palette[bestIndex][1]) eg -= int32(palette[bestIndex][1])
eb -= int32(palette[bestIndex][2]) eb -= int32(palette[bestIndex][2])
ea -= int32(palette[bestIndex][3])
} else { } else {
out.R = uint16(er) out.R = uint16(er)
out.G = uint16(eg) out.G = uint16(eg)
out.B = uint16(eb) out.B = uint16(eb)
out.A = uint16(ea)
// The third argument is &out instead of out (and out is // The third argument is &out instead of out (and out is
// declared outside of the inner loop) to avoid the implicit // declared outside of the inner loop) to avoid the implicit
// conversion to color.Color here allocating memory in the // conversion to color.Color here allocating memory in the
...@@ -640,32 +652,37 @@ func drawPaletted(dst Image, r image.Rectangle, src image.Image, sp image.Point, ...@@ -640,32 +652,37 @@ func drawPaletted(dst Image, r image.Rectangle, src image.Image, sp image.Point,
if !floydSteinberg { if !floydSteinberg {
continue continue
} }
sr, sg, sb, _ = dst.At(r.Min.X+x, r.Min.Y+y).RGBA() sr, sg, sb, sa = dst.At(r.Min.X+x, r.Min.Y+y).RGBA()
er -= int32(sr) er -= int32(sr)
eg -= int32(sg) eg -= int32(sg)
eb -= int32(sb) eb -= int32(sb)
ea -= int32(sa)
} }
// Propagate the Floyd-Steinberg quantization error. // Propagate the Floyd-Steinberg quantization error.
quantErrorNext[x+0][0] += er * 3 quantErrorNext[x+0][0] += er * 3
quantErrorNext[x+0][1] += eg * 3 quantErrorNext[x+0][1] += eg * 3
quantErrorNext[x+0][2] += eb * 3 quantErrorNext[x+0][2] += eb * 3
quantErrorNext[x+0][3] += ea * 3
quantErrorNext[x+1][0] += er * 5 quantErrorNext[x+1][0] += er * 5
quantErrorNext[x+1][1] += eg * 5 quantErrorNext[x+1][1] += eg * 5
quantErrorNext[x+1][2] += eb * 5 quantErrorNext[x+1][2] += eb * 5
quantErrorNext[x+1][3] += ea * 5
quantErrorNext[x+2][0] += er * 1 quantErrorNext[x+2][0] += er * 1
quantErrorNext[x+2][1] += eg * 1 quantErrorNext[x+2][1] += eg * 1
quantErrorNext[x+2][2] += eb * 1 quantErrorNext[x+2][2] += eb * 1
quantErrorNext[x+2][3] += ea * 1
quantErrorCurr[x+2][0] += er * 7 quantErrorCurr[x+2][0] += er * 7
quantErrorCurr[x+2][1] += eg * 7 quantErrorCurr[x+2][1] += eg * 7
quantErrorCurr[x+2][2] += eb * 7 quantErrorCurr[x+2][2] += eb * 7
quantErrorCurr[x+2][3] += ea * 7
} }
// Recycle the quantization error buffers. // Recycle the quantization error buffers.
if floydSteinberg { if floydSteinberg {
quantErrorCurr, quantErrorNext = quantErrorNext, quantErrorCurr quantErrorCurr, quantErrorNext = quantErrorNext, quantErrorCurr
for i := range quantErrorNext { for i := range quantErrorNext {
quantErrorNext[i] = [3]int32{} quantErrorNext[i] = [4]int32{}
} }
} }
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment