├── .travis.yml ├── Godeps ├── Godeps.json ├── Readme └── _workspace │ ├── .gitignore │ └── src │ └── github.com │ └── soniakeys │ └── quant │ ├── changelog.md │ ├── license │ ├── mean │ ├── mean.go │ ├── mean_test.go │ └── readme.md │ ├── median │ ├── median.go │ ├── median_test.go │ └── readme.md │ ├── palette.go │ ├── quant.go │ ├── quant_test.go │ ├── readme.md │ └── sierra.go ├── LICENSE ├── README.md ├── cmd └── colorbot │ └── main.go ├── colorbot.go ├── colorbot_test.go └── test-assets ├── black-and-yellow.gif ├── black-and-yellow.jpg ├── etsy.png ├── hodges-research.png ├── icon-1.png ├── icon-2.png ├── icon-3.png ├── icon-4.png ├── icon-5.png ├── icon-6.png ├── icon-7.png ├── interlaced.png └── low-contrast.png /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | -------------------------------------------------------------------------------- /Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/codahale/colorbot", 3 | "GoVersion": "go1.4.2", 4 | "Packages": [ 5 | "./..." 6 | ], 7 | "Deps": [ 8 | { 9 | "ImportPath": "github.com/soniakeys/quant", 10 | "Comment": "v0.2-5-g67a87a0", 11 | "Rev": "67a87a04e990dc9790189911a1eddd7723da2a50" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Godeps/Readme: -------------------------------------------------------------------------------- 1 | This directory tree is generated automatically by godep. 2 | 3 | Please do not edit. 4 | 5 | See https://github.com/tools/godep for more information. 6 | -------------------------------------------------------------------------------- /Godeps/_workspace/.gitignore: -------------------------------------------------------------------------------- 1 | /pkg 2 | /bin 3 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/changelog.md: -------------------------------------------------------------------------------- 1 | ## v0.2 2013 Nov 14 2 | 3 | * Ditherer added. Sierra 24A, a simpler kernel than Floyd-Steinberg. 4 | Provided through the new draw.Drawer interface which is new to Go 1.2. 5 | * New draw.Quantizer interface supported, which is also new to Go 1.2. 6 | * Tests added. These work on whatever .png files are found in the source 7 | directory. The tests do nothing if no suitable .png files are present. 8 | 9 | ## v0.1 2013 Sep 20 10 | 11 | This started as a little toy program to implement color quantization. But then 12 | I looked to see what else was published in Go along these lines and didn't find 13 | much. To add something of interest, I implemented a second algorithm and added 14 | an interface. That's about all that is in v0.1 here. It's far from general 15 | utility. It needs things like dithering, subsampling, optimization for common 16 | image types, and test code. 17 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/license: -------------------------------------------------------------------------------- 1 | MIT License: 2 | 3 | Copyright (c) 2013 Sonia Keys 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/mean/mean.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Sonia Keys. 2 | // Licensed under MIT license. See "license" file in this source tree. 3 | 4 | // Mean is a simple color quantizer. The algorithm successively divides the 5 | // color space much like a median cut algorithm, but a mean statistic is used 6 | // rather than a median. In another simplification, there is no priority 7 | // queue to order color blocks; linear search is used instead. 8 | // 9 | // An added sopphistication though, is that division proceeds in two stages, 10 | // with somewhat different criteria used for the earlier cuts than for the 11 | // later cuts. 12 | // 13 | // Motivation for using the mean is the observation that in a two stage 14 | // algorithm, cuts are offset from the computed average so having the logically 15 | // "correct" value of the median must not be that important. Motivation 16 | // for the linear search is that the number of blocks to search is limited 17 | // to the target number of colors in the palette, which is small and typically 18 | // limited to 256. If n is 256, O(log n) and O(n) both become O(1). 19 | package mean 20 | 21 | import ( 22 | "image" 23 | "image/color" 24 | "image/draw" 25 | "math" 26 | 27 | "github.com/soniakeys/quant" 28 | ) 29 | 30 | // Quantizer methods implement mean cut color quantization. 31 | // 32 | // The value is the target number of colors. 33 | // Methods do not require pointer receivers, simply construct Quantizer 34 | // objects with a type conversion. 35 | // 36 | // The type satisfies both quant.Quantizer and draw.Quantizer interfaces. 37 | type Quantizer int 38 | 39 | var _ quant.Quantizer = Quantizer(0) 40 | var _ draw.Quantizer = Quantizer(0) 41 | 42 | // Image performs color quantization and returns a paletted image. 43 | // 44 | // Returned is a paletted image with no more than q colors. Note though 45 | // that image.Paletted is limited to 256 colors. 46 | func (q Quantizer) Image(img image.Image) *image.Paletted { 47 | n := int(q) 48 | if n > 256 { 49 | n = 256 50 | } 51 | qz := newQuantizer(img, n) 52 | if n > 1 { 53 | qz.cluster() // cluster pixels by color 54 | } 55 | return qz.paletted() // generate paletted image from clusters 56 | } 57 | 58 | // Palette performs color quantization and returns a quant.Palette object. 59 | // 60 | // Returned is a palette with no more than q colors. Q may be > 256. 61 | func (q Quantizer) Palette(img image.Image) quant.Palette { 62 | qz := newQuantizer(img, int(q)) 63 | if q > 1 { 64 | qz.cluster() // cluster pixels by color 65 | } 66 | return qz.palette() 67 | } 68 | 69 | // Quantize performs color quantization and returns a color.Palette. 70 | // 71 | // Following the behavior documented with the draw.Quantizer interface, 72 | // "Quantize appends up to cap(p) - len(p) colors to p and returns the 73 | // updated palette...." This method does not limit the number of colors 74 | // to 256. Cap(p) or the quantity cap(p) - len(p) may be > 256. 75 | // Also for this method the value of the Quantizer object is ignored. 76 | func (Quantizer) Quantize(p color.Palette, m image.Image) color.Palette { 77 | n := cap(p) - len(p) 78 | qz := newQuantizer(m, n) 79 | if n > 1 { 80 | qz.cluster() // cluster pixels by color 81 | } 82 | return p[:len(p)+copy(p[len(p):cap(p)], qz.palette().ColorPalette())] 83 | } 84 | 85 | type quantizer struct { 86 | img image.Image // original image 87 | cs []cluster // len(cs) is the desired number of colors 88 | } 89 | 90 | type point struct{ x, y int32 } 91 | 92 | type cluster struct { 93 | px []point // list of points in the cluster 94 | // rgb const identifying dimension in color space with widest range 95 | widestDim int 96 | min, max uint32 // min, max color values in dimension with widest range 97 | volume uint64 // color volume 98 | priority int // early: population, late: population*volume 99 | } 100 | 101 | // indentifiers for RGB channels, or dimensions or axes of RGB color space 102 | const ( 103 | rgbR = iota 104 | rgbG 105 | rgbB 106 | ) 107 | 108 | func newQuantizer(img image.Image, n int) *quantizer { 109 | if n < 1 { 110 | return &quantizer{img, nil} 111 | } 112 | // Make list of all pixels in image. 113 | b := img.Bounds() 114 | px := make([]point, (b.Max.X-b.Min.X)*(b.Max.Y-b.Min.Y)) 115 | i := 0 116 | for y := b.Min.Y; y < b.Max.Y; y++ { 117 | for x := b.Min.X; x < b.Max.X; x++ { 118 | px[i].x = int32(x) 119 | px[i].y = int32(y) 120 | i++ 121 | } 122 | } 123 | // Make clusters, populate first cluster with complete pixel list. 124 | cs := make([]cluster, n) 125 | cs[0].px = px 126 | return &quantizer{img, cs} 127 | } 128 | 129 | // Cluster by repeatedly splitting clusters in two stages. For the first 130 | // stage, prioritize by population and split tails off distribution in color 131 | // dimension with widest range. For the second stage, prioritize by the 132 | // product of population and color volume, and split at the mean of the color 133 | // values in the dimension with widest range. Terminate when the desired number 134 | // of clusters has been populated or when clusters cannot be further split. 135 | func (qz *quantizer) cluster() { 136 | cs := qz.cs 137 | half := len(cs) / 2 138 | // cx is index of new cluster, populated at start of loop here, but 139 | // not yet analyzed. 140 | cx := 0 141 | c := &cs[cx] 142 | for { 143 | qz.setPriority(c, cx < half) // compute statistics for new cluster 144 | // determine cluster to split, sx 145 | sx := -1 146 | var maxP int 147 | for x := 0; x <= cx; x++ { 148 | // rule is to consider only clusters with non-zero color volume 149 | // and then split cluster with highest priority. 150 | if c := &cs[x]; c.max > c.min && c.priority > maxP { 151 | maxP = c.priority 152 | sx = x 153 | } 154 | } 155 | // If no clusters have any color variation, mark the end of the 156 | // cluster list and quit early. 157 | if sx < 0 { 158 | qz.cs = qz.cs[:cx+1] 159 | break 160 | } 161 | s := &cs[sx] 162 | m := qz.cutValue(s, cx < half) // get where to split cluster 163 | // point to next cluster to populate 164 | cx++ 165 | c = &cs[cx] 166 | // populate c by splitting s into c and s at value m 167 | qz.split(s, c, m) 168 | // Normal exit is when all clusters are populated. 169 | if cx == len(cs)-1 { 170 | break 171 | } 172 | if cx == half { 173 | // change priorities on existing clusters 174 | for x := 0; x < cx; x++ { 175 | cs[x].priority = 176 | int(uint64(cs[x].priority) * (cs[x].volume >> 16) >> 29) 177 | } 178 | } 179 | qz.setPriority(s, cx < half) // set priority for newly split s 180 | } 181 | } 182 | 183 | func (q *quantizer) setPriority(c *cluster, early bool) { 184 | // Find extents of color values in each dimension. 185 | var maxR, maxG, maxB uint32 186 | minR := uint32(math.MaxUint32) 187 | minG := uint32(math.MaxUint32) 188 | minB := uint32(math.MaxUint32) 189 | for _, p := range c.px { 190 | r, g, b, _ := q.img.At(int(p.x), int(p.y)).RGBA() 191 | if r < minR { 192 | minR = r 193 | } 194 | if r > maxR { 195 | maxR = r 196 | } 197 | if g < minG { 198 | minG = g 199 | } 200 | if g > maxG { 201 | maxG = g 202 | } 203 | if b < minB { 204 | minB = b 205 | } 206 | if b > maxB { 207 | maxB = b 208 | } 209 | } 210 | // See which color dimension had the widest range. 211 | w := rgbG 212 | min := minG 213 | max := maxG 214 | if maxR-minR > max-min { 215 | w = rgbR 216 | min = minR 217 | max = maxR 218 | } 219 | if maxB-minB > max-min { 220 | w = rgbB 221 | min = minB 222 | max = maxB 223 | } 224 | // store statistics 225 | c.widestDim = w 226 | c.min = min 227 | c.max = max 228 | c.volume = uint64(maxR-minR) * uint64(maxG-minG) * uint64(maxB-minB) 229 | c.priority = len(c.px) 230 | if !early { 231 | c.priority = int(uint64(c.priority) * (c.volume >> 16) >> 29) 232 | } 233 | } 234 | 235 | func (q *quantizer) cutValue(c *cluster, early bool) uint32 { 236 | var sum uint64 237 | switch c.widestDim { 238 | case rgbR: 239 | for _, p := range c.px { 240 | r, _, _, _ := q.img.At(int(p.x), int(p.y)).RGBA() 241 | sum += uint64(r) 242 | } 243 | case rgbG: 244 | for _, p := range c.px { 245 | _, g, _, _ := q.img.At(int(p.x), int(p.y)).RGBA() 246 | sum += uint64(g) 247 | } 248 | case rgbB: 249 | for _, p := range c.px { 250 | _, _, b, _ := q.img.At(int(p.x), int(p.y)).RGBA() 251 | sum += uint64(b) 252 | } 253 | } 254 | mean := uint32(sum / uint64(len(c.px))) 255 | if early { 256 | // split in middle of longer tail rather than at mean 257 | if c.max-mean > mean-c.min { 258 | mean = (mean + c.max) / 2 259 | } else { 260 | mean = (mean + c.min) / 2 261 | } 262 | } 263 | return mean 264 | } 265 | 266 | func (q *quantizer) split(s, c *cluster, m uint32) { 267 | px := s.px 268 | var v uint32 269 | i := 0 270 | last := len(px) - 1 271 | for i <= last { 272 | // Get color value in appropriate dimension. 273 | r, g, b, _ := q.img.At(int(px[i].x), int(px[i].y)).RGBA() 274 | switch s.widestDim { 275 | case rgbR: 276 | v = r 277 | case rgbG: 278 | v = g 279 | case rgbB: 280 | v = b 281 | } 282 | // Split into two non-empty parts at m. 283 | if v < m || m == s.min && v == m { 284 | i++ 285 | } else { 286 | px[last], px[i] = px[i], px[last] 287 | last-- 288 | } 289 | } 290 | // Split the pixel list. 291 | s.px = px[:i] 292 | c.px = px[i:] 293 | } 294 | 295 | func (qz *quantizer) paletted() *image.Paletted { 296 | cp := make(color.Palette, len(qz.cs)) 297 | pi := image.NewPaletted(qz.img.Bounds(), cp) 298 | for i := range qz.cs { 299 | px := qz.cs[i].px 300 | // Average values in cluster to get palette color. 301 | var rsum, gsum, bsum int64 302 | for _, p := range px { 303 | r, g, b, _ := qz.img.At(int(p.x), int(p.y)).RGBA() 304 | rsum += int64(r) 305 | gsum += int64(g) 306 | bsum += int64(b) 307 | } 308 | n64 := int64(len(px) << 8) 309 | cp[i] = color.RGBA{ 310 | uint8(rsum / n64), 311 | uint8(gsum / n64), 312 | uint8(bsum / n64), 313 | 0xff, 314 | } 315 | // set image pixels 316 | for _, p := range px { 317 | pi.SetColorIndex(int(p.x), int(p.y), uint8(i)) 318 | } 319 | } 320 | return pi 321 | } 322 | 323 | func (qz *quantizer) palette() quant.Palette { 324 | cp := make(color.Palette, len(qz.cs)) 325 | for i := range qz.cs { 326 | px := qz.cs[i].px 327 | // Average values in cluster to get palette color. 328 | var rsum, gsum, bsum int64 329 | for _, p := range px { 330 | r, g, b, _ := qz.img.At(int(p.x), int(p.y)).RGBA() 331 | rsum += int64(r) 332 | gsum += int64(g) 333 | bsum += int64(b) 334 | } 335 | n64 := int64(len(px) << 8) 336 | cp[i] = color.RGBA{ 337 | uint8(rsum / n64), 338 | uint8(gsum / n64), 339 | uint8(bsum / n64), 340 | 0xff, 341 | } 342 | } 343 | return quant.LinearPalette{cp} 344 | } 345 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/mean/mean_test.go: -------------------------------------------------------------------------------- 1 | package mean_test 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/png" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "testing" 11 | 12 | "github.com/soniakeys/quant" 13 | "github.com/soniakeys/quant/mean" 14 | ) 15 | 16 | // TestMean tests the mean quantizer on png files found in the source 17 | // directory. Output files are prefixed with _mean_. Files begining with 18 | // _ are skipped when scanning for input files. Note nothing is tested 19 | // with a fresh source tree--drop a png or two in the source directory 20 | // before testing to give the test something to work on. Png files in the 21 | // parent directory are similarly used for testing. Put files there 22 | // to compare results of the different quantizers. 23 | func TestMean(t *testing.T) { 24 | for _, p := range glob(t) { 25 | f, err := os.Open(p) 26 | if err != nil { 27 | t.Log(err) // skip files that can't be opened 28 | continue 29 | } 30 | img, err := png.Decode(f) 31 | f.Close() 32 | if err != nil { 33 | t.Log(err) // skip files that can't be decoded 34 | continue 35 | } 36 | pDir, pFile := filepath.Split(p) 37 | for _, n := range []int{16, 256} { 38 | // prefix _ on file name marks this as a result 39 | fq, err := os.Create(fmt.Sprintf("%s_mean_%d_%s", pDir, n, pFile)) 40 | if err != nil { 41 | t.Fatal(err) // probably can't create any others 42 | } 43 | var q quant.Quantizer = mean.Quantizer(n) 44 | if err = png.Encode(fq, q.Image(img)); err != nil { 45 | t.Fatal(err) // any problem is probably a problem for all 46 | } 47 | } 48 | } 49 | } 50 | 51 | func glob(tb testing.TB) []string { 52 | _, file, _, _ := runtime.Caller(0) 53 | srcDir, _ := filepath.Split(file) 54 | // ignore file names starting with _, those are result files. 55 | imgs, err := filepath.Glob(srcDir + "[^_]*.png") 56 | if err != nil { 57 | tb.Fatal(err) 58 | } 59 | if srcDir > "" { 60 | parentDir, _ := filepath.Split(srcDir[:len(srcDir)-1]) 61 | parentImgs, err := filepath.Glob(parentDir + "[^_]*.png") 62 | if err != nil { 63 | tb.Fatal(err) 64 | } 65 | imgs = append(parentImgs, imgs...) 66 | } 67 | return imgs 68 | } 69 | 70 | func BenchmarkPalette(b *testing.B) { 71 | var img image.Image 72 | for _, p := range glob(b) { 73 | f, err := os.Open(p) 74 | if err != nil { 75 | b.Log(err) // skip files that can't be opened 76 | continue 77 | } 78 | img, err = png.Decode(f) 79 | f.Close() 80 | if err != nil { 81 | b.Log(err) // skip files that can't be decoded 82 | continue 83 | } 84 | break 85 | } 86 | var q quant.Quantizer = mean.Quantizer(256) 87 | b.ResetTimer() 88 | for i := 0; i < b.N; i++ { 89 | q.Palette(img) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/mean/readme.md: -------------------------------------------------------------------------------- 1 | Mean 2 | ==== 3 | 4 | A simple color quantizer. Similar to a median cut algorithm execept it uses 5 | the mean rather than the median. While the median seems technically correct 6 | the mean seems good enough and is easier to compute. This implementation is 7 | also simplified over traditional median cut algorithms by replacing the 8 | priority queue with a simple linear search. For a typical number of colors 9 | (256) a linear search is fast enough and does not represent a significant 10 | inefficiency. 11 | 12 | A bit of sophistication added though is a two stage clustering process. 13 | The first stage takes a stab at clipping tails off the distributions of colors 14 | by pixel population, with the goal of smoother transitions in larger areas of 15 | lower color density. The second stage attempts to allocate remaining palette 16 | entries more uniformly. It prioritizes by a combination of pixel population 17 | and color range and splits clusters at mean values. 18 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/median/median.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Sonia Keys. 2 | // Licensed under MIT license. See "license" file in this source tree. 3 | 4 | // Median implements basic median cut color quantization. 5 | package median 6 | 7 | import ( 8 | "container/heap" 9 | "image" 10 | "image/color" 11 | "image/draw" 12 | "math" 13 | "sort" 14 | 15 | "github.com/soniakeys/quant" 16 | ) 17 | 18 | // Quantizer methods implement median cut color quantization. 19 | // 20 | // The value is the target number of colors. 21 | // Methods do not require pointer receivers, simply construct Quantizer 22 | // objects with a type conversion. 23 | // 24 | // The type satisfies both quant.Quantizer and draw.Quantizer interfaces. 25 | type Quantizer int 26 | 27 | var _ quant.Quantizer = Quantizer(0) 28 | var _ draw.Quantizer = Quantizer(0) 29 | 30 | // Image performs color quantization and returns a paletted image. 31 | // 32 | // Returned is a paletted image with no more than q colors. Note though 33 | // that image.Paletted is limited to 256 colors. 34 | func (q Quantizer) Image(img image.Image) *image.Paletted { 35 | n := int(q) 36 | if n > 256 { 37 | n = 256 38 | } 39 | qz := newQuantizer(img, n) 40 | if n > 1 { 41 | qz.cluster() // cluster pixels by color 42 | } 43 | return qz.paletted() // generate paletted image from clusters 44 | } 45 | 46 | // Palette performs color quantization and returns a quant.Palette object. 47 | // 48 | // Returned is a palette with no more than q colors. Q may be > 256. 49 | func (q Quantizer) Palette(img image.Image) quant.Palette { 50 | qz := newQuantizer(img, int(q)) 51 | if q > 1 { 52 | qz.cluster() // cluster pixels by color 53 | } 54 | return qz.palette() 55 | } 56 | 57 | // Quantize performs color quantization and returns a color.Palette. 58 | // 59 | // Following the behavior documented with the draw.Quantizer interface, 60 | // "Quantize appends up to cap(p) - len(p) colors to p and returns the 61 | // updated palette...." This method does not limit the number of colors 62 | // to 256. Cap(p) or the quantity cap(p) - len(p) may be > 256. 63 | // Also for this method the value of the Quantizer object is ignored. 64 | func (Quantizer) Quantize(p color.Palette, m image.Image) color.Palette { 65 | n := cap(p) - len(p) 66 | qz := newQuantizer(m, n) 67 | if n > 1 { 68 | qz.cluster() // cluster pixels by color 69 | } 70 | return p[:len(p)+copy(p[len(p):cap(p)], qz.palette().ColorPalette())] 71 | } 72 | 73 | type quantizer struct { 74 | img image.Image // original image 75 | cs []cluster // len(cs) is the desired number of colors 76 | ch chValues // buffer for computing median 77 | t *quant.TreePalette // root 78 | } 79 | 80 | type point struct{ x, y int32 } 81 | type chValues []uint16 82 | type queue []*cluster 83 | 84 | type cluster struct { 85 | px []point // list of points in the cluster 86 | widestCh int // rgb const identifying axis with widest value range 87 | // limits of this cluster 88 | minR, maxR uint32 89 | minG, maxG uint32 90 | minB, maxB uint32 91 | // true if corresponding value above represents a bound or hull of the 92 | // represented color space 93 | bMinR, bMaxR bool 94 | bMinG, bMaxG bool 95 | bMinB, bMaxB bool 96 | t *quant.TreePalette // leaf node representing this cluster 97 | } 98 | 99 | // indentifiers for RGB channels, or dimensions or axes of RGB color space 100 | const ( 101 | rgbR = iota 102 | rgbG 103 | rgbB 104 | ) 105 | 106 | func newQuantizer(img image.Image, nq int) *quantizer { 107 | if nq < 1 { 108 | return &quantizer{img: img} 109 | } 110 | b := img.Bounds() 111 | npx := (b.Max.X - b.Min.X) * (b.Max.Y - b.Min.Y) 112 | qz := &quantizer{ 113 | img: img, 114 | ch: make(chValues, npx), 115 | cs: make([]cluster, nq), 116 | t: &quant.TreePalette{}, 117 | } 118 | // Populate initial cluster with all pixels from image. 119 | c := &qz.cs[0] 120 | px := make([]point, npx) 121 | c.px = px 122 | c.t = qz.t 123 | c.minR = math.MaxUint32 124 | c.minG = math.MaxUint32 125 | c.minB = math.MaxUint32 126 | c.bMinR = true 127 | c.bMinG = true 128 | c.bMinB = true 129 | c.bMaxR = true 130 | c.bMaxG = true 131 | c.bMaxB = true 132 | i := 0 133 | for y := b.Min.Y; y < b.Max.Y; y++ { 134 | for x := b.Min.X; x < b.Max.X; x++ { 135 | px[i].x = int32(x) 136 | px[i].y = int32(y) 137 | r, g, b, _ := img.At(x, y).RGBA() 138 | if r < c.minR { 139 | c.minR = r 140 | } 141 | if r > c.maxR { 142 | c.maxR = r 143 | } 144 | if g < c.minG { 145 | c.minG = g 146 | } 147 | if g > c.maxG { 148 | c.maxG = g 149 | } 150 | if b < c.minB { 151 | c.minB = b 152 | } 153 | if b > c.maxB { 154 | c.maxB = b 155 | } 156 | i++ 157 | } 158 | } 159 | return qz 160 | } 161 | 162 | // Cluster by repeatedly splitting clusters. 163 | // Use a heap as priority queue for picking clusters to split. 164 | // The rule is to spilt the cluster with the most pixels. 165 | // Terminate when the desired number of clusters has been populated 166 | // or when clusters cannot be further split. 167 | func (qz *quantizer) cluster() { 168 | pq := new(queue) 169 | // Initial cluster. populated at this point, but not analyzed. 170 | c := &qz.cs[0] 171 | var m uint32 172 | for i := 1; ; { 173 | // Only enqueue clusters that can be split. 174 | if qz.setWidestChannel(c) { 175 | heap.Push(pq, c) 176 | } 177 | // If no clusters have any color variation, mark the end of the 178 | // cluster list and quit early. 179 | if len(*pq) == 0 { 180 | qz.cs = qz.cs[:i] 181 | break 182 | } 183 | s := heap.Pop(pq).(*cluster) // get cluster to split 184 | m = qz.medianCut(s) 185 | c = &qz.cs[i] // set c to new cluster 186 | i++ 187 | qz.split(s, c, m) // split s into c and s at value m 188 | // Normal exit is when all clusters are populated. 189 | if i == len(qz.cs) { 190 | break 191 | } 192 | if qz.setWidestChannel(s) { 193 | heap.Push(pq, s) // return s to queue 194 | } 195 | } 196 | } 197 | 198 | func (q *quantizer) setWidestChannel(c *cluster) bool { 199 | // Find extents of color values in each dimension. 200 | // (limits in cluster are not good enough here, we want extents as 201 | // represented by pixels.) 202 | var maxR, maxG, maxB uint32 203 | minR := uint32(math.MaxUint32) 204 | minG := uint32(math.MaxUint32) 205 | minB := uint32(math.MaxUint32) 206 | for _, p := range c.px { 207 | r, g, b, _ := q.img.At(int(p.x), int(p.y)).RGBA() 208 | if r < minR { 209 | minR = r 210 | } 211 | if r > maxR { 212 | maxR = r 213 | } 214 | if g < minG { 215 | minG = g 216 | } 217 | if g > maxG { 218 | maxG = g 219 | } 220 | if b < minB { 221 | minB = b 222 | } 223 | if b > maxB { 224 | maxB = b 225 | } 226 | } 227 | // See which color dimension had the widest range. 228 | c.widestCh = rgbG 229 | min := minG 230 | max := maxG 231 | if maxR-minR > max-min { 232 | c.widestCh = rgbR 233 | min = minR 234 | max = maxR 235 | } 236 | if maxB-minB > max-min { 237 | c.widestCh = rgbB 238 | min = minB 239 | max = maxB 240 | } 241 | return max > min 242 | } 243 | 244 | // Arg c must have value range > 0 in dimension c.widestDim. 245 | // return value m is guararanteed to split cluster into two non-empty clusters 246 | // by v < m where v is pixel value of dimension c.Widest. 247 | func (q *quantizer) medianCut(c *cluster) uint32 { 248 | px := c.px 249 | ch := q.ch[:len(px)] 250 | // Copy values from appropriate color channel to buffer for 251 | // computing median. 252 | switch c.widestCh { 253 | case rgbR: 254 | for i, p := range c.px { 255 | r, _, _, _ := q.img.At(int(p.x), int(p.y)).RGBA() 256 | ch[i] = uint16(r) 257 | } 258 | case rgbG: 259 | for i, p := range c.px { 260 | _, g, _, _ := q.img.At(int(p.x), int(p.y)).RGBA() 261 | ch[i] = uint16(g) 262 | } 263 | case rgbB: 264 | for i, p := range c.px { 265 | _, _, b, _ := q.img.At(int(p.x), int(p.y)).RGBA() 266 | ch[i] = uint16(b) 267 | } 268 | } 269 | // Find cut. 270 | sort.Sort(ch) 271 | m1 := len(ch) / 2 // median 272 | if ch[m1] != ch[m1-1] { 273 | return uint32(ch[m1]) 274 | } 275 | m2 := m1 276 | // Dec m1 until element to left is different. 277 | for m1--; m1 > 0 && ch[m1] == ch[m1-1]; m1-- { 278 | } 279 | // Inc m2 until element to left is different. 280 | for m2++; m2 < len(ch) && ch[m2] == ch[m2-1]; m2++ { 281 | } 282 | // Return value that makes more equitable cut. 283 | if m1 > len(ch)-m2 { 284 | return uint32(ch[m1]) 285 | } 286 | return uint32(ch[m2]) 287 | } 288 | 289 | // split s into c and s at value m 290 | func (q *quantizer) split(s, c *cluster, m uint32) { 291 | *c = *s // copy extent data 292 | px := s.px 293 | var v uint32 294 | i := 0 295 | last := len(px) - 1 296 | for i <= last { 297 | // Get color value in appropriate dimension. 298 | r, g, b, _ := q.img.At(int(px[i].x), int(px[i].y)).RGBA() 299 | switch s.widestCh { 300 | case rgbR: 301 | v = r 302 | case rgbG: 303 | v = g 304 | case rgbB: 305 | v = b 306 | } 307 | // Split at m. 308 | if v < m { 309 | i++ 310 | } else { 311 | px[last], px[i] = px[i], px[last] 312 | last-- 313 | } 314 | } 315 | // Split the pixel list. s keeps smaller values, c gets larger values. 316 | s.px = px[:i] 317 | c.px = px[i:] 318 | // Split color extent 319 | t := s.t 320 | switch s.widestCh { 321 | case rgbR: 322 | s.maxR = m 323 | c.minR = m 324 | s.bMaxR = false 325 | c.bMinR = false 326 | t.Type = quant.TSplitR 327 | case rgbG: 328 | s.maxG = m 329 | c.minG = m 330 | s.bMaxG = false 331 | c.bMinG = false 332 | t.Type = quant.TSplitG 333 | case rgbB: 334 | s.maxB = m 335 | c.minB = m 336 | s.bMaxB = false 337 | c.bMinB = false 338 | t.Type = quant.TSplitB 339 | } 340 | // Split node 341 | t.Split = m 342 | t.Low = &quant.TreePalette{} 343 | t.High = &quant.TreePalette{} 344 | s.t, c.t = t.Low, t.High 345 | } 346 | 347 | func (qz *quantizer) paletted() *image.Paletted { 348 | cp := make(color.Palette, len(qz.cs)) 349 | pi := image.NewPaletted(qz.img.Bounds(), cp) 350 | for i := range qz.cs { 351 | px := qz.cs[i].px 352 | // Average values in cluster to get palette color. 353 | var rsum, gsum, bsum int64 354 | for _, p := range px { 355 | r, g, b, _ := qz.img.At(int(p.x), int(p.y)).RGBA() 356 | rsum += int64(r) 357 | gsum += int64(g) 358 | bsum += int64(b) 359 | } 360 | n64 := int64(len(px) << 8) 361 | cp[i] = color.RGBA{ 362 | uint8(rsum / n64), 363 | uint8(gsum / n64), 364 | uint8(bsum / n64), 365 | 0xff, 366 | } 367 | // Set image pixels. 368 | for _, p := range px { 369 | pi.SetColorIndex(int(p.x), int(p.y), uint8(i)) 370 | } 371 | } 372 | return pi 373 | } 374 | 375 | func (qz *quantizer) palette() quant.Palette { 376 | for i := range qz.cs { 377 | px := qz.cs[i].px 378 | // Average values in cluster to get palette color. 379 | var rsum, gsum, bsum int64 380 | for _, p := range px { 381 | r, g, b, _ := qz.img.At(int(p.x), int(p.y)).RGBA() 382 | rsum += int64(r) 383 | gsum += int64(g) 384 | bsum += int64(b) 385 | } 386 | n64 := int64(len(px)) 387 | qz.cs[i].t.Color = color.RGBA64{ 388 | uint16(rsum / n64), 389 | uint16(gsum / n64), 390 | uint16(bsum / n64), 391 | 0xffff, 392 | } 393 | } 394 | return qz.t 395 | } 396 | 397 | // Implement sort.Interface for sort in median algorithm. 398 | func (c chValues) Len() int { return len(c) } 399 | func (c chValues) Less(i, j int) bool { return c[i] < c[j] } 400 | func (c chValues) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 401 | 402 | // Implement heap.Interface for priority queue of clusters. 403 | func (q queue) Len() int { return len(q) } 404 | 405 | // Priority is number of pixels in cluster. 406 | func (q queue) Less(i, j int) bool { return len(q[i].px) > len(q[j].px) } 407 | func (q queue) Swap(i, j int) { 408 | q[i], q[j] = q[j], q[i] 409 | } 410 | func (pq *queue) Push(x interface{}) { 411 | c := x.(*cluster) 412 | *pq = append(*pq, c) 413 | } 414 | func (pq *queue) Pop() interface{} { 415 | q := *pq 416 | n := len(q) - 1 417 | c := q[n] 418 | *pq = q[:n] 419 | return c 420 | } 421 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/median/median_test.go: -------------------------------------------------------------------------------- 1 | package median_test 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/png" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "testing" 11 | 12 | "github.com/soniakeys/quant" 13 | "github.com/soniakeys/quant/median" 14 | ) 15 | 16 | // TestMedian tests the median quantizer on png files found in the source 17 | // directory. Output files are prefixed with _median_. Files begining with 18 | // _ are skipped when scanning for input files. Note nothing is tested 19 | // with a fresh source tree--drop a png or two in the source directory 20 | // before testing to give the test something to work on. Png files in the 21 | // parent directory are similarly used for testing. Put files there 22 | // to compare results of the different quantizers. 23 | func TestMedian(t *testing.T) { 24 | for _, p := range glob(t) { 25 | f, err := os.Open(p) 26 | if err != nil { 27 | t.Log(err) // skip files that can't be opened 28 | continue 29 | } 30 | img, err := png.Decode(f) 31 | f.Close() 32 | if err != nil { 33 | t.Log(err) // skip files that can't be decoded 34 | continue 35 | } 36 | pDir, pFile := filepath.Split(p) 37 | for _, n := range []int{16, 256} { 38 | // prefix _ on file name marks this as a result 39 | fq, err := os.Create(fmt.Sprintf("%s_median_%d_%s", pDir, n, pFile)) 40 | if err != nil { 41 | t.Fatal(err) // probably can't create any others 42 | } 43 | var q quant.Quantizer = median.Quantizer(n) 44 | if err = png.Encode(fq, q.Image(img)); err != nil { 45 | t.Fatal(err) // any problem is probably a problem for all 46 | } 47 | } 48 | } 49 | } 50 | 51 | func glob(tb testing.TB) []string { 52 | _, file, _, _ := runtime.Caller(0) 53 | srcDir, _ := filepath.Split(file) 54 | // ignore file names starting with _, those are result files. 55 | imgs, err := filepath.Glob(srcDir + "[^_]*.png") 56 | if err != nil { 57 | tb.Fatal(err) 58 | } 59 | if srcDir > "" { 60 | parentDir, _ := filepath.Split(srcDir[:len(srcDir)-1]) 61 | parentImgs, err := filepath.Glob(parentDir + "[^_]*.png") 62 | if err != nil { 63 | tb.Fatal(err) 64 | } 65 | imgs = append(parentImgs, imgs...) 66 | } 67 | return imgs 68 | } 69 | 70 | func BenchmarkPalette(b *testing.B) { 71 | var img image.Image 72 | for _, p := range glob(b) { 73 | f, err := os.Open(p) 74 | if err != nil { 75 | b.Log(err) // skip files that can't be opened 76 | continue 77 | } 78 | img, err = png.Decode(f) 79 | f.Close() 80 | if err != nil { 81 | b.Log(err) // skip files that can't be decoded 82 | continue 83 | } 84 | break 85 | } 86 | var q quant.Quantizer = median.Quantizer(256) 87 | b.ResetTimer() 88 | for i := 0; i < b.N; i++ { 89 | q.Palette(img) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/median/readme.md: -------------------------------------------------------------------------------- 1 | Median 2 | ====== 3 | 4 | Basic median cut color quantization. 5 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/palette.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Sonia Keys. 2 | // Licensed under MIT license. See "license" file in this source tree. 3 | 4 | // Quant provides an interface for image color quantizers. 5 | package quant 6 | 7 | import "image/color" 8 | 9 | // Palette is a palette of color.Colors, much like color.Pallete of the 10 | // standard library. 11 | // 12 | // It is defined as an interface here to allow more general implementations, 13 | // presumably ones that maintain some data structure to achieve performance 14 | // advantages over linear search. 15 | type Palette interface { 16 | IndexNear(color.Color) int 17 | ColorNear(color.Color) color.Color 18 | ColorPalette() color.Palette 19 | } 20 | 21 | var _ Palette = LinearPalette{} 22 | var _ Palette = &TreePalette{} 23 | 24 | // LinearPalette implements the Palette interface with color.Palette 25 | // and has no optimizations. 26 | type LinearPalette struct { 27 | // Convert method of Palette satisfied by method of color.Palette. 28 | color.Palette 29 | } 30 | 31 | func (p LinearPalette) IndexNear(c color.Color) int { 32 | return p.Palette.Index(c) 33 | } 34 | 35 | func (p LinearPalette) ColorNear(c color.Color) color.Color { 36 | return p.Palette.Convert(c) 37 | } 38 | 39 | // ColorPalette satisfies interface Palette. 40 | // 41 | // It simply returns the internal color.Palette. 42 | func (p LinearPalette) ColorPalette() color.Palette { 43 | return p.Palette 44 | } 45 | 46 | type TreePalette struct { 47 | Type int 48 | // for TLeaf 49 | Index int 50 | Color color.RGBA64 51 | // for TSplit 52 | Split uint32 53 | Low, High *TreePalette 54 | } 55 | 56 | const ( 57 | TLeaf = iota 58 | TSplitR 59 | TSplitG 60 | TSplitB 61 | ) 62 | 63 | func (t *TreePalette) IndexNear(c color.Color) (i int) { 64 | if t == nil { 65 | return -1 66 | } 67 | t.search(c, func(leaf *TreePalette) { i = leaf.Index }) 68 | return 69 | } 70 | 71 | func (t *TreePalette) ColorNear(c color.Color) (p color.Color) { 72 | if t == nil { 73 | return color.RGBA64{0x7fff, 0x7fff, 0x7fff, 0xfff} 74 | } 75 | t.search(c, func(leaf *TreePalette) { p = leaf.Color }) 76 | return 77 | } 78 | 79 | func (t *TreePalette) search(c color.Color, f func(leaf *TreePalette)) { 80 | r, g, b, _ := c.RGBA() 81 | var lt bool 82 | var s func(*TreePalette) 83 | s = func(t *TreePalette) { 84 | switch t.Type { 85 | case TLeaf: 86 | f(t) 87 | return 88 | case TSplitR: 89 | lt = r < t.Split 90 | case TSplitG: 91 | lt = g < t.Split 92 | case TSplitB: 93 | lt = b < t.Split 94 | } 95 | if lt { 96 | s(t.Low) 97 | } else { 98 | s(t.High) 99 | } 100 | } 101 | s(t) 102 | return 103 | } 104 | 105 | func (t *TreePalette) ColorPalette() (p color.Palette) { 106 | if t == nil { 107 | return 108 | } 109 | var walk func(*TreePalette) 110 | walk = func(t *TreePalette) { 111 | if t.Type == TLeaf { 112 | p = append(p, color.Color(t.Color)) 113 | return 114 | } 115 | walk(t.Low) 116 | walk(t.High) 117 | } 118 | walk(t) 119 | return 120 | } 121 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/quant.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Sonia Keys. 2 | // Licensed under MIT license. See "license" file in this source tree. 3 | 4 | // Quant provides an interface for image color quantizers. 5 | package quant 6 | 7 | import "image" 8 | 9 | // Quantizer defines a color quantizer for images. 10 | type Quantizer interface { 11 | // Image quantizes an image and returns a paletted image. 12 | Image(image.Image) *image.Paletted 13 | // Palette quantizes an image and returns a Palette. Note the return 14 | // type is the Palette interface of this package and not image.Palette. 15 | Palette(image.Image) Palette 16 | } 17 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/quant_test.go: -------------------------------------------------------------------------------- 1 | package quant_test 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | "image/draw" 8 | "image/png" 9 | "os" 10 | "path/filepath" 11 | "runtime" 12 | "testing" 13 | 14 | "github.com/soniakeys/quant" 15 | "github.com/soniakeys/quant/median" 16 | ) 17 | 18 | // TestDither tests Sierra24A on png files found in the source directory. 19 | // Output files are prefixed with _dither_256_. Files begining with _ 20 | // are skipped when scanning for input files. Thus nothing is tested 21 | // with a fresh source tree--drop a png or two in the source directory 22 | // before testing to give the test something to work on. 23 | func TestDither(t *testing.T) { 24 | _, file, _, ok := runtime.Caller(0) 25 | if !ok { 26 | t.Fatal("runtime.Caller fail") 27 | } 28 | srcDir, _ := filepath.Split(file) 29 | // ignore file names starting with _, those are result files. 30 | imgs, err := filepath.Glob(srcDir + "[^_]*.png") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | const n = 256 35 | // exercise draw.Quantizer interface 36 | var q draw.Quantizer = median.Quantizer(n) 37 | // exercise draw.Drawer interface 38 | var d draw.Drawer = quant.Sierra24A{} 39 | for _, p := range imgs { 40 | f, err := os.Open(p) 41 | if err != nil { 42 | t.Error(err) // skip files that can't be opened 43 | continue 44 | } 45 | img, err := png.Decode(f) 46 | f.Close() 47 | if err != nil { 48 | t.Error(err) // skip files that can't be decoded 49 | continue 50 | } 51 | pDir, pFile := filepath.Split(p) 52 | // prefix _ on file name marks this as a result 53 | fq, err := os.Create(fmt.Sprintf("%s_dither_%d_%s", pDir, n, pFile)) 54 | if err != nil { 55 | t.Fatal(err) // probably can't create any others 56 | } 57 | b := img.Bounds() 58 | pi := image.NewPaletted(b, q.Quantize(make(color.Palette, 0, n), img)) 59 | d.Draw(pi, b, img, b.Min) 60 | if err = png.Encode(fq, pi); err != nil { 61 | t.Fatal(err) // any problem is probably a problem for all 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/readme.md: -------------------------------------------------------------------------------- 1 | Quant 2 | ===== 3 | 4 | Experiments with color quantizers 5 | 6 | Implemented here are two rather simple quantizers and an (also simple) ditherer. 7 | The quantizers satisfy the draw.Quantizer interface of the standard library. 8 | The ditherer satisfies the draw.Drawer interface of the standard library. 9 | -------------------------------------------------------------------------------- /Godeps/_workspace/src/github.com/soniakeys/quant/sierra.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Sonia Keys. 2 | // Licensed under MIT license. See "license" file in this source tree. 3 | 4 | // Quant provides an interface for image color quantizers. 5 | package quant 6 | 7 | import ( 8 | "image" 9 | "image/color" 10 | "image/draw" 11 | "math" 12 | ) 13 | 14 | // Sierra24A satisfies draw.Drawer 15 | type Sierra24A struct{} 16 | 17 | var _ draw.Drawer = Sierra24A{} 18 | 19 | // Draw performs error diffusion dithering. 20 | // 21 | // This method satisfies the draw.Drawer interface, implementing a dithering 22 | // filter attributed to Frankie Sierra. It uses the kernel 23 | // 24 | // X 2 25 | // 1 1 26 | func (d Sierra24A) Draw(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point) { 27 | pd, ok := dst.(*image.Paletted) 28 | if !ok { 29 | // dither211 currently requires a palette 30 | draw.Draw(dst, r, src, sp, draw.Src) 31 | return 32 | } 33 | // intersect r with both dst and src bounds, fix up sp. 34 | ir := r.Intersect(pd.Bounds()). 35 | Intersect(src.Bounds().Add(r.Min.Sub(sp))) 36 | if ir.Empty() { 37 | return // no work to do. 38 | } 39 | sp = ir.Min.Sub(r.Min) 40 | // get subimage of src 41 | sr := ir.Add(sp) 42 | if !sr.Eq(src.Bounds()) { 43 | s, ok := src.(interface { 44 | SubImage(image.Rectangle) image.Image 45 | }) 46 | if !ok { 47 | // dither211 currently works on whole images 48 | draw.Draw(dst, r, src, sp, draw.Src) 49 | return 50 | } 51 | src = s.SubImage(sr) 52 | } 53 | // dither211 currently returns a new image, or nil if dithering not 54 | // possible. 55 | if s := dither211(src, pd.Palette); s != nil { 56 | src = s 57 | } 58 | // this avoids any problem of src dst overlap but it would usually 59 | // work to render directly into dst. todo. 60 | draw.Draw(dst, r, src, image.Point{}, draw.Src) 61 | } 62 | 63 | // signed color type, no alpha. signed to represent color deltas as well as 64 | // color values 0-ffff as with colorRGBA64 65 | type sRGB struct{ r, g, b int32 } 66 | type sPalette []sRGB 67 | 68 | func (p sPalette) index(c sRGB) int { 69 | // still the awful linear search 70 | i, min := 0, int64(math.MaxInt64) 71 | for j, pc := range p { 72 | d := int64(c.r) - int64(pc.r) 73 | s := d * d 74 | d = int64(c.g) - int64(pc.g) 75 | s += d * d 76 | d = int64(c.b) - int64(pc.b) 77 | s += d * d 78 | if s < min { 79 | min = s 80 | i = j 81 | } 82 | } 83 | return i 84 | } 85 | 86 | // currently this is strictly a helper function for Dither211.Draw, so 87 | // not generalized to use Palette from this package. 88 | func dither211(i0 image.Image, cp color.Palette) *image.Paletted { 89 | if len(cp) > 256 { 90 | // representation limit of image.Paletted. a little sketchy to return 91 | // nil, but unworkable results are always better than wrong results. 92 | return nil 93 | } 94 | b := i0.Bounds() 95 | pi := image.NewPaletted(b, cp) 96 | if b.Empty() { 97 | return pi // no work to do 98 | } 99 | sp := make(sPalette, len(cp)) 100 | for i, c := range cp { 101 | r, g, b, _ := c.RGBA() 102 | sp[i] = sRGB{int32(r), int32(g), int32(b)} 103 | } 104 | // afc is adjustd full color. e, rt, dn hold diffused errors. 105 | var afc, e, rt sRGB 106 | dn := make([]sRGB, b.Dx()+1) 107 | for y := b.Min.Y; y < b.Max.Y; y++ { 108 | rt = dn[0] 109 | dn[0] = sRGB{} 110 | for x := b.Min.X; x < b.Max.X; x++ { 111 | // full color from original image 112 | r0, g0, b0, _ := i0.At(x, y).RGBA() 113 | // adjusted full color = original color + diffused error 114 | afc.r = int32(r0) + rt.r>>2 115 | afc.g = int32(g0) + rt.g>>2 116 | afc.b = int32(b0) + rt.b>>2 117 | // clipping or clamping is usually explained as necessary 118 | // to avoid integer overflow but with palettes that do not 119 | // represent the full color space of the image, it is needed 120 | // to keep areas of excess color from saturating at palette 121 | // limits and bleeding into neighboring areas. 122 | if afc.r < 0 { 123 | afc.r = 0 124 | } else if afc.r > 0xffff { 125 | afc.r = 0xffff 126 | } 127 | if afc.g < 0 { 128 | afc.g = 0 129 | } else if afc.g > 0xffff { 130 | afc.g = 0xffff 131 | } 132 | if afc.b < 0 { 133 | afc.b = 0 134 | } else if afc.b > 0xffff { 135 | afc.b = 0xffff 136 | } 137 | // nearest palette entry 138 | i := sp.index(afc) 139 | // set pixel in destination image 140 | pi.SetColorIndex(x, y, uint8(i)) 141 | // error to be diffused = full color - palette color. 142 | pc := sp[i] 143 | e.r = afc.r - pc.r 144 | e.g = afc.g - pc.g 145 | e.b = afc.b - pc.b 146 | // half of error*4 goes right 147 | dx := x - b.Min.X + 1 148 | rt.r = dn[dx].r + e.r*2 149 | rt.g = dn[dx].g + e.g*2 150 | rt.b = dn[dx].b + e.b*2 151 | // the other half goes down 152 | dn[dx] = e 153 | dn[dx-1].r += e.r 154 | dn[dx-1].g += e.g 155 | dn[dx-1].b += e.b 156 | } 157 | } 158 | return pi 159 | } 160 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Coda Hale 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | colorbot 2 | ======== 3 | 4 | [![Build Status](https://travis-ci.org/codahale/colorbot.png?branch=master)](https://travis-ci.org/codahale/colorbot) 5 | 6 | A Go library which determines the dominant colors in an image. 7 | 8 | To install: 9 | 10 | 1. Have Go 1.4+ installed. 11 | 2. Use [godep](https://github.com/tools/godep). 12 | 3. Run `go get -d github.com/codahale/colorbot`. 13 | 4. Run `cd $GOPATH/github.com/codahale/colorbot && godep go install ./...` 14 | 5. Run `colorbot -h`. 15 | 16 | For documentation, check [godoc](http://godoc.org/github.com/codahale/colorbot). 17 | -------------------------------------------------------------------------------- /cmd/colorbot/main.go: -------------------------------------------------------------------------------- 1 | // Command colorbot analyzes a given image and prints out a list of the N most 2 | // dominant colors in the image. 3 | // 4 | // $ colorbot test-assets/hodges-research.png 5 | // #000000 6 | // #af905a 7 | // #f6d185 8 | // #f9e2b3 9 | // #ffffff 10 | // 11 | // $ colorbot https://www.google.com/images/srpr/logo11w.png 12 | // #000000 13 | // #009553 14 | // #0c60a7 15 | // #166bed 16 | // #c56937 17 | // 18 | // Colorbot supports GIF, JPEG, and PNG images. 19 | package main 20 | 21 | import ( 22 | "flag" 23 | "fmt" 24 | _ "image/gif" // support GIF images 25 | _ "image/jpeg" // support JPEG images 26 | _ "image/png" // support PNG images 27 | "io" 28 | "net/http" 29 | "os" 30 | "strings" 31 | 32 | "github.com/codahale/colorbot" 33 | ) 34 | 35 | func main() { 36 | var ( 37 | n = flag.Int("n", 5, "number of colors to return") 38 | maxBytes = flag.Int64("maxbytes", 10*1024*1024, "max image size in bytes") 39 | maxPixels = flag.Int64("maxpixels", 10*1024*1024, "max image size in pixels") 40 | ) 41 | flag.Parse() 42 | 43 | var in io.Reader 44 | if s := flag.Args()[0]; s == "-" { 45 | in = os.Stdin 46 | } else if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") { 47 | resp, err := http.Get(s) 48 | if err != nil { 49 | panic(err) 50 | } 51 | defer resp.Body.Close() 52 | 53 | in = resp.Body 54 | } else { 55 | f, err := os.Open(s) 56 | if err != nil { 57 | panic(err) 58 | } 59 | defer f.Close() 60 | in = f 61 | } 62 | 63 | img, err := colorbot.DecodeImage(in, *maxBytes, *maxPixels) 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | colors := colorbot.DominantColors(img, *n) 69 | for _, color := range colors { 70 | r, g, b, _ := color.RGBA() 71 | fmt.Printf("#%02x%02x%02x\n", r/256, g/256, b/256) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /colorbot.go: -------------------------------------------------------------------------------- 1 | // Package colorbot provides image analysis routines to determine the dominant 2 | // colors in images. 3 | package colorbot 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "image" 9 | "image/color" 10 | "io" 11 | 12 | "github.com/soniakeys/quant/median" 13 | ) 14 | 15 | // DominantColors returns the N most dominant colors in the given image. 16 | // 17 | // This uses the median cut quantization algorithm. 18 | func DominantColors(img image.Image, n int) color.Palette { 19 | return median.Quantizer(n).Palette(img).ColorPalette() 20 | } 21 | 22 | var ( 23 | // ErrImageTooLarge is returned when the image is too large to be processed. 24 | ErrImageTooLarge = errors.New("image is too large") 25 | ) 26 | 27 | // DecodeImage decodes the given reader as either a GIF, JPEG, or PNG image. 28 | // 29 | // If the image is larger than maxBytes or maxPixels, it returns 30 | // ErrImageTooLarge. 31 | func DecodeImage(r io.Reader, maxBytes, maxPixels int64) (image.Image, error) { 32 | // limit images to maxBytes 33 | lr := &io.LimitedReader{ 34 | R: r, 35 | N: maxBytes, 36 | } 37 | 38 | // read the header 39 | header := make([]byte, maxHeaderSize) 40 | if _, err := io.ReadFull(lr, header); err != nil && err != io.ErrUnexpectedEOF { 41 | return nil, err 42 | } 43 | 44 | // parse just the image size 45 | hr := bytes.NewReader(header) 46 | config, _, err := image.DecodeConfig(hr) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // check to see the image isn't too big 52 | pixels := int64(config.Height) * int64(config.Width) 53 | if pixels > maxPixels { 54 | return nil, ErrImageTooLarge 55 | } 56 | 57 | // recombine the image 58 | _, _ = hr.Seek(0, 0) 59 | ir := io.MultiReader(hr, lr) 60 | 61 | // decode the image 62 | img, _, err := image.Decode(ir) 63 | if err == io.ErrUnexpectedEOF { 64 | return nil, ErrImageTooLarge 65 | } 66 | return img, err 67 | } 68 | 69 | const ( 70 | maxHeaderSize = 4096 // bytes 71 | ) 72 | -------------------------------------------------------------------------------- /colorbot_test.go: -------------------------------------------------------------------------------- 1 | package colorbot_test 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | _ "image/gif" // support GIF images 7 | _ "image/jpeg" // support JPEG images 8 | _ "image/png" // support PNG images 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "testing" 13 | 14 | "github.com/codahale/colorbot" 15 | ) 16 | 17 | func TestDominantColors(t *testing.T) { 18 | actual := map[string][]string{} 19 | 20 | filepath.Walk("./test-assets", func(path string, info os.FileInfo, err error) error { 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | if info.IsDir() { 26 | return nil 27 | } 28 | 29 | f, err := os.Open(path) 30 | if err != nil { 31 | return err 32 | } 33 | defer f.Close() 34 | 35 | img, _, err := image.Decode(f) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | p := colorbot.DominantColors(img, 3) 41 | s := []string{} 42 | for _, c := range p { 43 | r, g, b, _ := c.RGBA() 44 | s = append(s, fmt.Sprintf("#%02x%02x%02x", r/256, g/256, b/256)) 45 | } 46 | actual[path] = s 47 | 48 | return nil 49 | }) 50 | 51 | expected := map[string][]string{ 52 | "test-assets/black-and-yellow.gif": {"#000000", "#8e8e00", "#ffff00"}, 53 | "test-assets/black-and-yellow.jpg": {"#000000", "#010008", "#c0c004"}, 54 | "test-assets/etsy.png": {"#000000", "#68310d", "#d5641c"}, 55 | "test-assets/hodges-research.png": {"#000000", "#e0bd78", "#ffffff"}, 56 | "test-assets/icon-1.png": {"#39454f", "#4a525a", "#b6b8ba"}, 57 | "test-assets/icon-2.png": {"#bb1b59", "#d32d6c", "#ed538d"}, 58 | "test-assets/icon-3.png": {"#283c77", "#43588f", "#a9b4ce"}, 59 | "test-assets/icon-4.png": {"#335752", "#6f9c36", "#fbf1d9"}, 60 | "test-assets/icon-5.png": {"#abb4b2", "#dfddd6", "#f4f4ef"}, 61 | "test-assets/icon-6.png": {"#7da03d", "#8ab042", "#c2d899"}, 62 | "test-assets/icon-7.png": {"#4282ea", "#538dec", "#8cb2f2"}, 63 | "test-assets/interlaced.png": {"#000000", "#060606", "#464547"}, 64 | "test-assets/low-contrast.png": {"#ffed50", "#fffbd0", "#ffffff"}, 65 | } 66 | 67 | for file, v := range actual { 68 | if want := expected[file]; !reflect.DeepEqual(v, want) { 69 | t.Errorf("Palette for %s was %v but expected %v", file, v, want) 70 | } 71 | } 72 | } 73 | 74 | func TestDecodeGIF(t *testing.T) { 75 | f, err := os.Open("./test-assets/black-and-yellow.gif") 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | defer f.Close() 80 | 81 | _, err = colorbot.DecodeImage(f, 1024*1024, 1024*1024) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | } 86 | 87 | func TestDecodeJPEG(t *testing.T) { 88 | f, err := os.Open("./test-assets/black-and-yellow.jpg") 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | defer f.Close() 93 | 94 | _, err = colorbot.DecodeImage(f, 1024*1024, 1024*1024) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | } 99 | 100 | func TestDecodePNG(t *testing.T) { 101 | f, err := os.Open("./test-assets/hodges-research.png") 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | defer f.Close() 106 | 107 | _, err = colorbot.DecodeImage(f, 1024*1024, 1024*1024) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | } 112 | 113 | func TestDecodeTooManyBytes(t *testing.T) { 114 | f, err := os.Open("./test-assets/hodges-research.png") 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | defer f.Close() 119 | 120 | _, err = colorbot.DecodeImage(f, 1024, 1024*1024) 121 | if err != colorbot.ErrImageTooLarge { 122 | t.Errorf("Unexpected error: %v", err) 123 | } 124 | } 125 | 126 | func TestDecodeTooManyPixels(t *testing.T) { 127 | f, err := os.Open("./test-assets/hodges-research.png") 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | defer f.Close() 132 | 133 | _, err = colorbot.DecodeImage(f, 1024*1024, 10) 134 | if err != colorbot.ErrImageTooLarge { 135 | t.Errorf("Unexpected error: %v", err) 136 | } 137 | } 138 | 139 | func BenchmarkDominantColors(b *testing.B) { 140 | f, err := os.Open("./test-assets/icon-7.png") 141 | if err != nil { 142 | b.Fatal(err) 143 | } 144 | defer f.Close() 145 | 146 | img, _, err := image.Decode(f) 147 | if err != nil { 148 | b.Fatal(err) 149 | } 150 | 151 | b.ReportAllocs() 152 | b.ResetTimer() 153 | 154 | for i := 0; i < b.N; i++ { 155 | colorbot.DominantColors(img, 4) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /test-assets/black-and-yellow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/black-and-yellow.gif -------------------------------------------------------------------------------- /test-assets/black-and-yellow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/black-and-yellow.jpg -------------------------------------------------------------------------------- /test-assets/etsy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/etsy.png -------------------------------------------------------------------------------- /test-assets/hodges-research.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/hodges-research.png -------------------------------------------------------------------------------- /test-assets/icon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/icon-1.png -------------------------------------------------------------------------------- /test-assets/icon-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/icon-2.png -------------------------------------------------------------------------------- /test-assets/icon-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/icon-3.png -------------------------------------------------------------------------------- /test-assets/icon-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/icon-4.png -------------------------------------------------------------------------------- /test-assets/icon-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/icon-5.png -------------------------------------------------------------------------------- /test-assets/icon-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/icon-6.png -------------------------------------------------------------------------------- /test-assets/icon-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/icon-7.png -------------------------------------------------------------------------------- /test-assets/interlaced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/interlaced.png -------------------------------------------------------------------------------- /test-assets/low-contrast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codahale/colorbot/92869c5ff0514271cead9763e89ee81743eca03b/test-assets/low-contrast.png --------------------------------------------------------------------------------