Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/ericpauley/go-quantize
module github.com/carbocation/go-quantize

go 1.12
go 1.13
6 changes: 3 additions & 3 deletions quantize/bench/go.mod
Original file line number Diff line number Diff line change
@@ -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 => ../..
85 changes: 64 additions & 21 deletions quantize/mediancut.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package quantize
import (
"image"
"image/color"
"math"
"sync"
)

Expand Down Expand Up @@ -121,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
}
Expand All @@ -144,34 +148,65 @@ 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()
size := (bounds.Max.X - bounds.Min.X) * (bounds.Max.Y - bounds.Min.Y) * 2
// 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{}
}

// 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 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 {
// 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)
}
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 {
Expand All @@ -191,7 +226,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)
}
4 changes: 2 additions & 2 deletions quantize/mediancut_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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))
}

Expand Down