From 6409d34970feebb190851cbefeca94e3d7e82203 Mon Sep 17 00:00:00 2001 From: James Pirruccello Date: Sun, 5 Jul 2020 18:20:17 -0400 Subject: [PATCH 1/5] Adds QuantizeMultiple, which ingests a slice of images and produces an optimal palette across the set of images. --- quantize/mediancut.go | 56 ++++++++++++++++++++++++-------------- quantize/mediancut_test.go | 4 +-- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/quantize/mediancut.go b/quantize/mediancut.go index cdf7def..d064f1f 100644 --- a/quantize/mediancut.go +++ b/quantize/mediancut.go @@ -144,34 +144,42 @@ func colorAt(m image.Image, x int, y int) color.RGBA { } } -// buildBucket creates a prioritized color slice with all the colors in the image -func (q MedianCutQuantizer) buildBucket(m image.Image) (bucket colorBucket) { - bounds := m.Bounds() +// buildBucketMultiple creates a prioritized color slice with all the colors in +// the images. +func (q MedianCutQuantizer) buildBucketMultiple(ms []image.Image) (bucket colorBucket) { + if len(ms) < 1 { + return colorBucket{} + } + + bounds := ms[0].Bounds() size := (bounds.Max.X - bounds.Min.X) * (bounds.Max.Y - bounds.Min.Y) * 2 sparseBucket := bpool.getBucket(size) - for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - priority := uint32(1) - if q.Weighting != nil { - priority = q.Weighting(m, x, y) - } - if priority != 0 { - c := colorAt(m, x, y) - index := int(c.R)<<16 | int(c.G)<<8 | int(c.B) - for i := 1; ; i++ { - p := &sparseBucket[index%size] - if p.p == 0 || p.RGBA == c { - *p = colorPriority{p.p + priority, c} - break + for _, m := range ms { + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + priority := uint32(1) + if q.Weighting != nil { + priority = q.Weighting(m, x, y) + } + if priority != 0 { + c := colorAt(m, x, y) + index := int(c.R)<<16 | int(c.G)<<8 | int(c.B) + for i := 1; ; i++ { + p := &sparseBucket[index%size] + if p.p == 0 || p.RGBA == c { + *p = colorPriority{p.p + priority, c} + break + } + index += 1 + i } - index += 1 + i } } } } + bucket = sparseBucket[:0] - switch m.(type) { + switch ms[0].(type) { case *image.YCbCr: for _, p := range sparseBucket { if p.p != 0 { @@ -191,7 +199,15 @@ func (q MedianCutQuantizer) buildBucket(m image.Image) (bucket colorBucket) { // Quantize quantizes an image to a palette and returns the palette func (q MedianCutQuantizer) Quantize(p color.Palette, m image.Image) color.Palette { - bucket := q.buildBucket(m) + bucket := q.buildBucketMultiple([]image.Image{m}) + defer bpool.Put(bucket) + return q.quantizeSlice(p, bucket) +} + +// QuantizeMultiple quantizes several images at once to a palette and returns +// the palette +func (q MedianCutQuantizer) QuantizeMultiple(p color.Palette, m []image.Image) color.Palette { + bucket := q.buildBucketMultiple(m) defer bpool.Put(bucket) return q.quantizeSlice(p, bucket) } diff --git a/quantize/mediancut_test.go b/quantize/mediancut_test.go index 67f834b..a2ed775 100644 --- a/quantize/mediancut_test.go +++ b/quantize/mediancut_test.go @@ -24,7 +24,7 @@ func TestBuildBucket(t *testing.T) { q := MedianCutQuantizer{Mode, nil, false} - colors := q.buildBucket(i) + colors := q.buildBucketMultiple([]image.Image{i}) t.Logf("Naive color map contains %d elements", len(colors)) for _, p := range colors { @@ -40,7 +40,7 @@ func TestBuildBucket(t *testing.T) { return 0 }, false} - colors = q.buildBucket(i) + colors = q.buildBucketMultiple([]image.Image{i}) t.Logf("Color map contains %d elements", len(colors)) } From df1ba62fd42f7bf12703acbda79c5046d0d433b0 Mon Sep 17 00:00:00 2001 From: James Pirruccello Date: Wed, 17 Feb 2021 16:57:36 -0500 Subject: [PATCH 2/5] Fix go mod. --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d3b8136..85f6e9e 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/ericpauley/go-quantize +module github.com/carbocation/go-quantize go 1.12 From 6c4ee5bfa11e992ed7b5c06f7cf2327da199eb1a Mon Sep 17 00:00:00 2001 From: James Pirruccello Date: Wed, 17 Feb 2021 17:06:15 -0500 Subject: [PATCH 3/5] And mod. --- quantize/bench/go.mod | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quantize/bench/go.mod b/quantize/bench/go.mod index 3073729..e4e96c2 100644 --- a/quantize/bench/go.mod +++ b/quantize/bench/go.mod @@ -1,11 +1,11 @@ -module github.com/ericpauley/go-quantize/quantize/bench +module github.com/carbocation/go-quantize/quantize/bench // Note: We use a separate go.mod file here because comparison libraries should not be in top-level dependencies go 1.12 require ( - github.com/ericpauley/go-quantize v0.0.0-20180803033130-bfdbba883ede + github.com/carbocation/go-quantize v0.0.0-20180803033130-bfdbba883ede github.com/esimov/colorquant v1.0.0 github.com/soniakeys/quant v1.0.0 ) -replace github.com/ericpauley/go-quantize => ../.. +replace github.com/carbocation/go-quantize => ../.. From 85417ed22ad1861da3d641c005915d52111f5b35 Mon Sep 17 00:00:00 2001 From: James Pirruccello Date: Fri, 19 Nov 2021 11:22:46 -0500 Subject: [PATCH 4/5] Update buildBucketMultiple to support slices of images which do not have the same size. --- quantize/mediancut.go | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/quantize/mediancut.go b/quantize/mediancut.go index d064f1f..9e0c460 100644 --- a/quantize/mediancut.go +++ b/quantize/mediancut.go @@ -5,6 +5,7 @@ package quantize import ( "image" "image/color" + "math" "sync" ) @@ -151,13 +152,36 @@ func (q MedianCutQuantizer) buildBucketMultiple(ms []image.Image) (bucket colorB return colorBucket{} } - bounds := ms[0].Bounds() - size := (bounds.Max.X - bounds.Min.X) * (bounds.Max.Y - bounds.Min.Y) * 2 + // If all images are not the same size, and if the first image is not the + // largest on both X and Y dimensions, this function will eventually trigger + // a panic unless we've configured the bounds to be based on the greatest x + // and y of all images in the gif, which we do here: + leastX, greatestX, leastY, greatestY := math.MaxInt32, 0, math.MaxInt32, 0 + for _, palettedImage := range ms { + if palettedImage.Bounds().Min.X < leastX { + leastX = palettedImage.Bounds().Min.X + } + if palettedImage.Bounds().Max.X > greatestX { + greatestX = palettedImage.Bounds().Max.X + } + + if palettedImage.Bounds().Min.Y < leastY { + leastY = palettedImage.Bounds().Min.Y + } + if palettedImage.Bounds().Max.Y > greatestY { + greatestY = palettedImage.Bounds().Max.Y + } + } + + size := (greatestX - leastX) * (greatestY - leastY) * 2 sparseBucket := bpool.getBucket(size) for _, m := range ms { - for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { + // Since images may have variable size, don't go beyond each specific + // image's X and Y bounds while we iterate, rather than using the global + // min and max x and y + for y := m.Bounds().Min.Y; y < m.Bounds().Max.Y; y++ { + for x := m.Bounds().Min.X; x < m.Bounds().Max.X; x++ { priority := uint32(1) if q.Weighting != nil { priority = q.Weighting(m, x, y) From 857cc7c8fdfc866cfa80336e150b37811fe347f5 Mon Sep 17 00:00:00 2001 From: James Pirruccello Date: Tue, 8 Mar 2022 14:27:28 -0500 Subject: [PATCH 5/5] Swap the transparent color to be the first color in the palette. This addresses a personal need and is also responsive to https://github.com/ericpauley/go-quantize/issues/3 --- go.mod | 2 +- quantize/mediancut.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 85f6e9e..6f8b434 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/carbocation/go-quantize -go 1.12 +go 1.13 diff --git a/quantize/mediancut.go b/quantize/mediancut.go index 9e0c460..0b6436e 100644 --- a/quantize/mediancut.go +++ b/quantize/mediancut.go @@ -122,6 +122,9 @@ func (q MedianCutQuantizer) quantizeSlice(p color.Palette, colors []colorPriorit p = q.palettize(p, buckets) if addTransparent { p = append(p, color.RGBA{0, 0, 0, 0}) + + // Set our transparent color to be the first color + p[0], p[len(p)-1] = p[len(p)-1], p[0] } return p }