Skip to content

Commit 766717b

Browse files
author
Artyom Pervukhin
committed
Reduce allocations for special source cases
R,G,B color values of each source image pixel were read with image.Image.At() method returning color.Color interface. This is quite taxing allocation-wise since interface values should be allocated on heap. Introduce special cases for some concrete source types to read color values using type-specific methods. name old time/op new time/op delta pkg:github.com/soniakeys/quant/mean goos:darwin goarch:amd64 Palette-4 1.48s ± 3% 0.62s ± 2% -58.38% (p=0.008 n=5+5) pkg:github.com/soniakeys/quant/median goos:darwin goarch:amd64 Palette-4 284ms ± 2% 133ms ± 2% -53.26% (p=0.008 n=5+5) name old alloc/op new alloc/op delta pkg:github.com/soniakeys/quant/mean goos:darwin goarch:amd64 Palette-4 129MB ± 0% 6MB ± 0% -95.32% (p=0.000 n=4+5) pkg:github.com/soniakeys/quant/median goos:darwin goarch:amd64 Palette-4 29.4MB ± 0% 7.6MB ± 0% -74.20% (p=0.029 n=4+4) name old allocs/op new allocs/op delta pkg:github.com/soniakeys/quant/mean goos:darwin goarch:amd64 Palette-4 30.8M ± 0% 0.0M ± 0% ~ (p=0.079 n=4+5) pkg:github.com/soniakeys/quant/median goos:darwin goarch:amd64 Palette-4 5.46M ± 0% 0.00M ± 0% -99.99% (p=0.008 n=5+5)
1 parent 7a7ffac commit 766717b

File tree

3 files changed

+49
-20
lines changed

3 files changed

+49
-20
lines changed

internal/internal.go

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2013 Sonia Keys.
2+
// Licensed under MIT license. See "license" file in this source tree.
3+
4+
package internal
5+
6+
import "image"
7+
8+
// PxRGBAfunc returns function to get RGBA color values at (x, y) coordinates of
9+
// image img. Returned function works the same as img.At(x, y).RGBA() but
10+
// implements special cases for certain image types to use type-specific methods
11+
// bypassing color.Color interface which escapes to the heap.
12+
func PxRGBAfunc(img image.Image) func(x, y int) (r, g, b, a uint32) {
13+
switch img0 := img.(type) {
14+
case *image.RGBA:
15+
return func(x, y int) (r, g, b, a uint32) { return img0.RGBAAt(x, y).RGBA() }
16+
case *image.NRGBA:
17+
return func(x, y int) (r, g, b, a uint32) { return img0.NRGBAAt(x, y).RGBA() }
18+
case *image.YCbCr:
19+
return func(x, y int) (r, g, b, a uint32) { return img0.YCbCrAt(x, y).RGBA() }
20+
}
21+
return func(x, y int) (r, g, b, a uint32) { return img.At(x, y).RGBA() }
22+
}

mean/mean.go

+12-9
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"math"
2626

2727
"github.com/soniakeys/quant"
28+
"github.com/soniakeys/quant/internal"
2829
)
2930

3031
// Quantizer methods implement mean cut color quantization.
@@ -85,6 +86,8 @@ func (Quantizer) Quantize(p color.Palette, m image.Image) color.Palette {
8586
type quantizer struct {
8687
img image.Image // original image
8788
cs []cluster // len(cs) is the desired number of colors
89+
90+
pxRGBA func(x, y int) (r, g, b, a uint32) // function to get original image RGBA color values
8891
}
8992

9093
type point struct{ x, y int32 }
@@ -107,7 +110,7 @@ const (
107110

108111
func newQuantizer(img image.Image, n int) *quantizer {
109112
if n < 1 {
110-
return &quantizer{img, nil}
113+
return &quantizer{img: img, pxRGBA: internal.PxRGBAfunc(img)}
111114
}
112115
// Make list of all pixels in image.
113116
b := img.Bounds()
@@ -123,7 +126,7 @@ func newQuantizer(img image.Image, n int) *quantizer {
123126
// Make clusters, populate first cluster with complete pixel list.
124127
cs := make([]cluster, n)
125128
cs[0].px = px
126-
return &quantizer{img, cs}
129+
return &quantizer{img: img, cs: cs, pxRGBA: internal.PxRGBAfunc(img)}
127130
}
128131

129132
// Cluster by repeatedly splitting clusters in two stages. For the first
@@ -187,7 +190,7 @@ func (q *quantizer) setPriority(c *cluster, early bool) {
187190
minG := uint32(math.MaxUint32)
188191
minB := uint32(math.MaxUint32)
189192
for _, p := range c.px {
190-
r, g, b, _ := q.img.At(int(p.x), int(p.y)).RGBA()
193+
r, g, b, _ := q.pxRGBA(int(p.x), int(p.y))
191194
if r < minR {
192195
minR = r
193196
}
@@ -237,17 +240,17 @@ func (q *quantizer) cutValue(c *cluster, early bool) uint32 {
237240
switch c.widestDim {
238241
case rgbR:
239242
for _, p := range c.px {
240-
r, _, _, _ := q.img.At(int(p.x), int(p.y)).RGBA()
243+
r, _, _, _ := q.pxRGBA(int(p.x), int(p.y))
241244
sum += uint64(r)
242245
}
243246
case rgbG:
244247
for _, p := range c.px {
245-
_, g, _, _ := q.img.At(int(p.x), int(p.y)).RGBA()
248+
_, g, _, _ := q.pxRGBA(int(p.x), int(p.y))
246249
sum += uint64(g)
247250
}
248251
case rgbB:
249252
for _, p := range c.px {
250-
_, _, b, _ := q.img.At(int(p.x), int(p.y)).RGBA()
253+
_, _, b, _ := q.pxRGBA(int(p.x), int(p.y))
251254
sum += uint64(b)
252255
}
253256
}
@@ -270,7 +273,7 @@ func (q *quantizer) split(s, c *cluster, m uint32) {
270273
last := len(px) - 1
271274
for i <= last {
272275
// Get color value in appropriate dimension.
273-
r, g, b, _ := q.img.At(int(px[i].x), int(px[i].y)).RGBA()
276+
r, g, b, _ := q.pxRGBA(int(px[i].x), int(px[i].y))
274277
switch s.widestDim {
275278
case rgbR:
276279
v = r
@@ -300,7 +303,7 @@ func (qz *quantizer) paletted() *image.Paletted {
300303
// Average values in cluster to get palette color.
301304
var rsum, gsum, bsum int64
302305
for _, p := range px {
303-
r, g, b, _ := qz.img.At(int(p.x), int(p.y)).RGBA()
306+
r, g, b, _ := qz.pxRGBA(int(p.x), int(p.y))
304307
rsum += int64(r)
305308
gsum += int64(g)
306309
bsum += int64(b)
@@ -327,7 +330,7 @@ func (qz *quantizer) palette() quant.Palette {
327330
// Average values in cluster to get palette color.
328331
var rsum, gsum, bsum int64
329332
for _, p := range px {
330-
r, g, b, _ := qz.img.At(int(p.x), int(p.y)).RGBA()
333+
r, g, b, _ := qz.pxRGBA(int(p.x), int(p.y))
331334
rsum += int64(r)
332335
gsum += int64(g)
333336
bsum += int64(b)

median/median.go

+15-11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"sort"
1414

1515
"github.com/soniakeys/quant"
16+
"github.com/soniakeys/quant/internal"
1617
)
1718

1819
// Quantizer methods implement median cut color quantization.
@@ -75,6 +76,8 @@ type quantizer struct {
7576
cs []cluster // len(cs) is the desired number of colors
7677
ch chValues // buffer for computing median
7778
t quant.TreePalette // root
79+
80+
pxRGBA func(x, y int) (r, g, b, a uint32) // function to get original image RGBA color values
7881
}
7982

8083
type point struct{ x, y int32 }
@@ -105,14 +108,15 @@ const (
105108

106109
func newQuantizer(img image.Image, nq int) *quantizer {
107110
if nq < 1 {
108-
return &quantizer{img: img}
111+
return &quantizer{img: img, pxRGBA: internal.PxRGBAfunc(img)}
109112
}
110113
b := img.Bounds()
111114
npx := (b.Max.X - b.Min.X) * (b.Max.Y - b.Min.Y)
112115
qz := &quantizer{
113-
img: img,
114-
ch: make(chValues, npx),
115-
cs: make([]cluster, nq),
116+
img: img,
117+
ch: make(chValues, npx),
118+
cs: make([]cluster, nq),
119+
pxRGBA: internal.PxRGBAfunc(img),
116120
}
117121
// Populate initial cluster with all pixels from image.
118122
c := &qz.cs[0]
@@ -134,7 +138,7 @@ func newQuantizer(img image.Image, nq int) *quantizer {
134138
for x := b.Min.X; x < b.Max.X; x++ {
135139
px[i].x = int32(x)
136140
px[i].y = int32(y)
137-
r, g, b, _ := img.At(x, y).RGBA()
141+
r, g, b, _ := qz.pxRGBA(x, y)
138142
if r < c.minR {
139143
c.minR = r
140144
}
@@ -203,7 +207,7 @@ func (qz *quantizer) cluster() {
203207
// Average values in cluster to get palette color.
204208
var rsum, gsum, bsum int64
205209
for _, p := range px {
206-
r, g, b, _ := qz.img.At(int(p.x), int(p.y)).RGBA()
210+
r, g, b, _ := qz.pxRGBA(int(p.x), int(p.y))
207211
rsum += int64(r)
208212
gsum += int64(g)
209213
bsum += int64(b)
@@ -227,7 +231,7 @@ func (q *quantizer) setWidestChannel(c *cluster) bool {
227231
minG := uint32(math.MaxUint32)
228232
minB := uint32(math.MaxUint32)
229233
for _, p := range c.px {
230-
r, g, b, _ := q.img.At(int(p.x), int(p.y)).RGBA()
234+
r, g, b, _ := q.pxRGBA(int(p.x), int(p.y))
231235
if r < minR {
232236
minR = r
233237
}
@@ -275,17 +279,17 @@ func (q *quantizer) medianCut(c *cluster) uint32 {
275279
switch c.widestCh {
276280
case rgbR:
277281
for i, p := range c.px {
278-
r, _, _, _ := q.img.At(int(p.x), int(p.y)).RGBA()
282+
r, _, _, _ := q.pxRGBA(int(p.x), int(p.y))
279283
ch[i] = uint16(r)
280284
}
281285
case rgbG:
282286
for i, p := range c.px {
283-
_, g, _, _ := q.img.At(int(p.x), int(p.y)).RGBA()
287+
_, g, _, _ := q.pxRGBA(int(p.x), int(p.y))
284288
ch[i] = uint16(g)
285289
}
286290
case rgbB:
287291
for i, p := range c.px {
288-
_, _, b, _ := q.img.At(int(p.x), int(p.y)).RGBA()
292+
_, _, b, _ := q.pxRGBA(int(p.x), int(p.y))
289293
ch[i] = uint16(b)
290294
}
291295
}
@@ -318,7 +322,7 @@ func (q *quantizer) split(s, c *cluster, m uint32) {
318322
last := len(px) - 1
319323
for i <= last {
320324
// Get color value in appropriate dimension.
321-
r, g, b, _ := q.img.At(int(px[i].x), int(px[i].y)).RGBA()
325+
r, g, b, _ := q.pxRGBA(int(px[i].x), int(px[i].y))
322326
switch s.widestCh {
323327
case rgbR:
324328
v = r

0 commit comments

Comments
 (0)