pax_global_header00006660000000000000000000000064132470523550014520gustar00rootroot0000000000000052 comment=0a3861b3bd925b434c67c93a15633ced8475092d quant-1.0.0/000077500000000000000000000000001324705235500126465ustar00rootroot00000000000000quant-1.0.0/.gitignore000066400000000000000000000000061324705235500146320ustar00rootroot00000000000000*.png quant-1.0.0/changelog.md000066400000000000000000000014711324705235500151220ustar00rootroot00000000000000## v0.2 2013 Nov 14 * Ditherer added. Sierra 24A, a simpler kernel than Floyd-Steinberg. Provided through the new draw.Drawer interface which is new to Go 1.2. * New draw.Quantizer interface supported, which is also new to Go 1.2. * Tests added. These work on whatever .png files are found in the source directory. The tests do nothing if no suitable .png files are present. ## v0.1 2013 Sep 20 This started as a little toy program to implement color quantization. But then I looked to see what else was published in Go along these lines and didn't find much. To add something of interest, I implemented a second algorithm and added an interface. That's about all that is in v0.1 here. It's far from general utility. It needs things like dithering, subsampling, optimization for common image types, and test code. quant-1.0.0/go.mod000066400000000000000000000000441324705235500137520ustar00rootroot00000000000000module "github.com/soniakeys/quant" quant-1.0.0/internal/000077500000000000000000000000001324705235500144625ustar00rootroot00000000000000quant-1.0.0/internal/internal.go000066400000000000000000000016221324705235500166260ustar00rootroot00000000000000// Copyright 2013 Sonia Keys. // Licensed under MIT license. See "license" file in this source tree. package internal import "image" // PxRGBAfunc returns function to get RGBA color values at (x, y) coordinates of // image img. Returned function works the same as img.At(x, y).RGBA() but // implements special cases for certain image types to use type-specific methods // bypassing color.Color interface which escapes to the heap. func PxRGBAfunc(img image.Image) func(x, y int) (r, g, b, a uint32) { switch img0 := img.(type) { case *image.RGBA: return func(x, y int) (r, g, b, a uint32) { return img0.RGBAAt(x, y).RGBA() } case *image.NRGBA: return func(x, y int) (r, g, b, a uint32) { return img0.NRGBAAt(x, y).RGBA() } case *image.YCbCr: return func(x, y int) (r, g, b, a uint32) { return img0.YCbCrAt(x, y).RGBA() } } return func(x, y int) (r, g, b, a uint32) { return img.At(x, y).RGBA() } } quant-1.0.0/license000066400000000000000000000020541324705235500142140ustar00rootroot00000000000000MIT License: Copyright (c) 2013 Sonia Keys Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. quant-1.0.0/mean/000077500000000000000000000000001324705235500135665ustar00rootroot00000000000000quant-1.0.0/mean/mean.go000066400000000000000000000221271324705235500150410ustar00rootroot00000000000000// Copyright 2013 Sonia Keys. // Licensed under MIT license. See "license" file in this source tree. // Mean is a simple color quantizer. The algorithm successively divides the // color space much like a median cut algorithm, but a mean statistic is used // rather than a median. In another simplification, there is no priority // queue to order color blocks; linear search is used instead. // // An added sopphistication though, is that division proceeds in two stages, // with somewhat different criteria used for the earlier cuts than for the // later cuts. // // Motivation for using the mean is the observation that in a two stage // algorithm, cuts are offset from the computed average so having the logically // "correct" value of the median must not be that important. Motivation // for the linear search is that the number of blocks to search is limited // to the target number of colors in the palette, which is small and typically // limited to 256. If n is 256, O(log n) and O(n) both become O(1). package mean import ( "image" "image/color" "image/draw" "math" "github.com/soniakeys/quant" "github.com/soniakeys/quant/internal" ) // Quantizer methods implement mean cut color quantization. // // The value is the target number of colors. // Methods do not require pointer receivers, simply construct Quantizer // objects with a type conversion. // // The type satisfies both quant.Quantizer and draw.Quantizer interfaces. type Quantizer int var _ quant.Quantizer = Quantizer(0) var _ draw.Quantizer = Quantizer(0) // Paletted performs color quantization and returns a paletted image. // // Returned is a new image.Paletted with no more than q colors. Note though // that image.Paletted is limited to 256 colors. func (q Quantizer) Paletted(img image.Image) *image.Paletted { n := int(q) if n > 256 { n = 256 } qz := newQuantizer(img, n) if n > 1 { qz.cluster() // cluster pixels by color } return qz.paletted() // generate paletted image from clusters } // Palette performs color quantization and returns a quant.Palette object. // // Returned is a palette with no more than q colors. Q may be > 256. func (q Quantizer) Palette(img image.Image) quant.Palette { qz := newQuantizer(img, int(q)) if q > 1 { qz.cluster() // cluster pixels by color } return qz.palette() } // Quantize performs color quantization and returns a color.Palette. // // Following the behavior documented with the draw.Quantizer interface, // "Quantize appends up to cap(p) - len(p) colors to p and returns the // updated palette...." This method does not limit the number of colors // to 256. Cap(p) or the quantity cap(p) - len(p) may be > 256. // Also for this method the value of the Quantizer object is ignored. func (Quantizer) Quantize(p color.Palette, m image.Image) color.Palette { n := cap(p) - len(p) qz := newQuantizer(m, n) if n > 1 { qz.cluster() // cluster pixels by color } return p[:len(p)+copy(p[len(p):cap(p)], qz.palette().ColorPalette())] } type quantizer struct { img image.Image // original image cs []cluster // len(cs) is the desired number of colors pxRGBA func(x, y int) (r, g, b, a uint32) // function to get original image RGBA color values } type point struct{ x, y int32 } type cluster struct { px []point // list of points in the cluster // rgb const identifying dimension in color space with widest range widestDim int min, max uint32 // min, max color values in dimension with widest range volume uint64 // color volume priority int // early: population, late: population*volume } // indentifiers for RGB channels, or dimensions or axes of RGB color space const ( rgbR = iota rgbG rgbB ) func newQuantizer(img image.Image, n int) *quantizer { if n < 1 { return &quantizer{img: img, pxRGBA: internal.PxRGBAfunc(img)} } // Make list of all pixels in image. b := img.Bounds() px := make([]point, (b.Max.X-b.Min.X)*(b.Max.Y-b.Min.Y)) i := 0 for y := b.Min.Y; y < b.Max.Y; y++ { for x := b.Min.X; x < b.Max.X; x++ { px[i].x = int32(x) px[i].y = int32(y) i++ } } // Make clusters, populate first cluster with complete pixel list. cs := make([]cluster, n) cs[0].px = px return &quantizer{img: img, cs: cs, pxRGBA: internal.PxRGBAfunc(img)} } // Cluster by repeatedly splitting clusters in two stages. For the first // stage, prioritize by population and split tails off distribution in color // dimension with widest range. For the second stage, prioritize by the // product of population and color volume, and split at the mean of the color // values in the dimension with widest range. Terminate when the desired number // of clusters has been populated or when clusters cannot be further split. func (qz *quantizer) cluster() { cs := qz.cs half := len(cs) / 2 // cx is index of new cluster, populated at start of loop here, but // not yet analyzed. cx := 0 c := &cs[cx] for { qz.setPriority(c, cx < half) // compute statistics for new cluster // determine cluster to split, sx sx := -1 var maxP int for x := 0; x <= cx; x++ { // rule is to consider only clusters with non-zero color volume // and then split cluster with highest priority. if c := &cs[x]; c.max > c.min && c.priority > maxP { maxP = c.priority sx = x } } // If no clusters have any color variation, mark the end of the // cluster list and quit early. if sx < 0 { qz.cs = qz.cs[:cx+1] break } s := &cs[sx] m := qz.cutValue(s, cx < half) // get where to split cluster // point to next cluster to populate cx++ c = &cs[cx] // populate c by splitting s into c and s at value m qz.split(s, c, m) // Normal exit is when all clusters are populated. if cx == len(cs)-1 { break } if cx == half { // change priorities on existing clusters for x := 0; x < cx; x++ { cs[x].priority = int(uint64(cs[x].priority) * (cs[x].volume >> 16) >> 29) } } qz.setPriority(s, cx < half) // set priority for newly split s } } func (q *quantizer) setPriority(c *cluster, early bool) { // Find extents of color values in each dimension. var maxR, maxG, maxB uint32 minR := uint32(math.MaxUint32) minG := uint32(math.MaxUint32) minB := uint32(math.MaxUint32) for _, p := range c.px { r, g, b, _ := q.pxRGBA(int(p.x), int(p.y)) if r < minR { minR = r } if r > maxR { maxR = r } if g < minG { minG = g } if g > maxG { maxG = g } if b < minB { minB = b } if b > maxB { maxB = b } } // See which color dimension had the widest range. w := rgbG min := minG max := maxG if maxR-minR > max-min { w = rgbR min = minR max = maxR } if maxB-minB > max-min { w = rgbB min = minB max = maxB } // store statistics c.widestDim = w c.min = min c.max = max c.volume = uint64(maxR-minR) * uint64(maxG-minG) * uint64(maxB-minB) c.priority = len(c.px) if !early { c.priority = int(uint64(c.priority) * (c.volume >> 16) >> 29) } } func (q *quantizer) cutValue(c *cluster, early bool) uint32 { var sum uint64 switch c.widestDim { case rgbR: for _, p := range c.px { r, _, _, _ := q.pxRGBA(int(p.x), int(p.y)) sum += uint64(r) } case rgbG: for _, p := range c.px { _, g, _, _ := q.pxRGBA(int(p.x), int(p.y)) sum += uint64(g) } case rgbB: for _, p := range c.px { _, _, b, _ := q.pxRGBA(int(p.x), int(p.y)) sum += uint64(b) } } mean := uint32(sum / uint64(len(c.px))) if early { // split in middle of longer tail rather than at mean if c.max-mean > mean-c.min { mean = (mean + c.max) / 2 } else { mean = (mean + c.min) / 2 } } return mean } func (q *quantizer) split(s, c *cluster, m uint32) { px := s.px var v uint32 i := 0 last := len(px) - 1 for i <= last { // Get color value in appropriate dimension. r, g, b, _ := q.pxRGBA(int(px[i].x), int(px[i].y)) switch s.widestDim { case rgbR: v = r case rgbG: v = g case rgbB: v = b } // Split into two non-empty parts at m. if v < m || m == s.min && v == m { i++ } else { px[last], px[i] = px[i], px[last] last-- } } // Split the pixel list. s.px = px[:i] c.px = px[i:] } func (qz *quantizer) paletted() *image.Paletted { cp := make(color.Palette, len(qz.cs)) pi := image.NewPaletted(qz.img.Bounds(), cp) for i := range qz.cs { px := qz.cs[i].px // Average values in cluster to get palette color. var rsum, gsum, bsum int64 for _, p := range px { r, g, b, _ := qz.pxRGBA(int(p.x), int(p.y)) rsum += int64(r) gsum += int64(g) bsum += int64(b) } n64 := int64(len(px) << 8) cp[i] = color.RGBA{ uint8(rsum / n64), uint8(gsum / n64), uint8(bsum / n64), 0xff, } // set image pixels for _, p := range px { pi.SetColorIndex(int(p.x), int(p.y), uint8(i)) } } return pi } func (qz *quantizer) palette() quant.Palette { cp := make(color.Palette, len(qz.cs)) for i := range qz.cs { px := qz.cs[i].px // Average values in cluster to get palette color. var rsum, gsum, bsum int64 for _, p := range px { r, g, b, _ := qz.pxRGBA(int(p.x), int(p.y)) rsum += int64(r) gsum += int64(g) bsum += int64(b) } n64 := int64(len(px) << 8) cp[i] = color.RGBA{ uint8(rsum / n64), uint8(gsum / n64), uint8(bsum / n64), 0xff, } } return quant.LinearPalette{cp} } quant-1.0.0/mean/mean_test.go000066400000000000000000000044171324705235500161020ustar00rootroot00000000000000package mean_test import ( "fmt" "image" "image/png" "os" "path/filepath" "runtime" "testing" "github.com/soniakeys/quant" "github.com/soniakeys/quant/mean" ) // TestMean tests the mean quantizer on png files found in the source // directory. Output files are prefixed with _mean_. Files beginning with // _ are skipped when scanning for input files. Note nothing is tested // with a fresh source tree--drop a png or two in the source directory // before testing to give the test something to work on. Png files in the // parent directory are similarly used for testing. Put files there // to compare results of the different quantizers. func TestMean(t *testing.T) { for _, p := range glob(t) { f, err := os.Open(p) if err != nil { t.Log(err) // skip files that can't be opened continue } img, err := png.Decode(f) f.Close() if err != nil { t.Log(err) // skip files that can't be decoded continue } pDir, pFile := filepath.Split(p) for _, n := range []int{16, 256} { // prefix _ on file name marks this as a result fq, err := os.Create(fmt.Sprintf("%s_mean_%d_%s", pDir, n, pFile)) if err != nil { t.Fatal(err) // probably can't create any others } var q quant.Quantizer = mean.Quantizer(n) if err = png.Encode(fq, q.Paletted(img)); err != nil { t.Fatal(err) // any problem is probably a problem for all } } } } func glob(tb testing.TB) []string { _, file, _, _ := runtime.Caller(0) srcDir, _ := filepath.Split(file) // ignore file names starting with _, those are result files. imgs, err := filepath.Glob(srcDir + "[^_]*.png") if err != nil { tb.Fatal(err) } if srcDir > "" { parentDir, _ := filepath.Split(srcDir[:len(srcDir)-1]) parentImgs, err := filepath.Glob(parentDir + "[^_]*.png") if err != nil { tb.Fatal(err) } imgs = append(parentImgs, imgs...) } return imgs } func BenchmarkPalette(b *testing.B) { var img image.Image for _, p := range glob(b) { f, err := os.Open(p) if err != nil { b.Log(err) // skip files that can't be opened continue } img, err = png.Decode(f) f.Close() if err != nil { b.Log(err) // skip files that can't be decoded continue } break } var q quant.Quantizer = mean.Quantizer(256) b.ResetTimer() for i := 0; i < b.N; i++ { q.Palette(img) } } quant-1.0.0/mean/readme.md000066400000000000000000000016231324705235500153470ustar00rootroot00000000000000Mean ==== A simple color quantizer. Similar to a median cut algorithm execept it uses the mean rather than the median. While the median seems technically correct the mean seems good enough and is easier to compute. This implementation is also simplified over traditional median cut algorithms by replacing the priority queue with a simple linear search. For a typical number of colors (256) a linear search is fast enough and does not represent a significant inefficiency. A bit of sophistication added though is a two stage clustering process. The first stage takes a stab at clipping tails off the distributions of colors by pixel population, with the goal of smoother transitions in larger areas of lower color density. The second stage attempts to allocate remaining palette entries more uniformly. It prioritizes by a combination of pixel population and color range and splits clusters at mean values. quant-1.0.0/median/000077500000000000000000000000001324705235500141035ustar00rootroot00000000000000quant-1.0.0/median/median.go000066400000000000000000000232051324705235500156710ustar00rootroot00000000000000// Copyright 2013 Sonia Keys. // Licensed under MIT license. See "license" file in this source tree. // Median implements basic median cut color quantization. package median import ( "container/heap" "image" "image/color" "image/draw" "math" "sort" "github.com/soniakeys/quant" "github.com/soniakeys/quant/internal" ) // Quantizer methods implement median cut color quantization. // // The value is the target number of colors. // Methods do not require pointer receivers, simply construct Quantizer // objects with a type conversion. // // The type satisfies both quant.Quantizer and draw.Quantizer interfaces. type Quantizer int var _ quant.Quantizer = Quantizer(0) var _ draw.Quantizer = Quantizer(0) // Paletted performs color quantization and returns a paletted image. // // Returned is an image.Paletted with no more than q colors. Note though // that image.Paletted is limited to 256 colors. func (q Quantizer) Paletted(img image.Image) *image.Paletted { n := int(q) if n > 256 { n = 256 } qz := newQuantizer(img, n) if n > 1 { qz.cluster() // cluster pixels by color } return qz.paletted() // generate paletted image from clusters } // Palette performs color quantization and returns a quant.Palette object. // // Returned is a palette with no more than q colors. Q may be > 256. func (q Quantizer) Palette(img image.Image) quant.Palette { qz := newQuantizer(img, int(q)) if q > 1 { qz.cluster() // cluster pixels by color } return qz.t } // Quantize performs color quantization and returns a color.Palette. // // Following the behavior documented with the draw.Quantizer interface, // "Quantize appends up to cap(p) - len(p) colors to p and returns the // updated palette...." This method does not limit the number of colors // to 256. Cap(p) or the quantity cap(p) - len(p) may be > 256. // Also for this method the value of the Quantizer object is ignored. func (Quantizer) Quantize(p color.Palette, m image.Image) color.Palette { n := cap(p) - len(p) qz := newQuantizer(m, n) if n > 1 { qz.cluster() // cluster pixels by color } return p[:len(p)+copy(p[len(p):cap(p)], qz.t.ColorPalette())] } type quantizer struct { img image.Image // original image cs []cluster // len(cs) is the desired number of colors ch chValues // buffer for computing median t quant.TreePalette // root pxRGBA func(x, y int) (r, g, b, a uint32) // function to get original image RGBA color values } type point struct{ x, y int32 } type chValues []uint16 type queue []*cluster type cluster struct { px []point // list of points in the cluster widestCh int // rgb const identifying axis with widest value range // limits of this cluster minR, maxR uint32 minG, maxG uint32 minB, maxB uint32 // true if corresponding value above represents a bound or hull of the // represented color space bMinR, bMaxR bool bMinG, bMaxG bool bMinB, bMaxB bool node *quant.Node // palette node representing this cluster } // indentifiers for RGB channels, or dimensions or axes of RGB color space const ( rgbR = iota rgbG rgbB ) func newQuantizer(img image.Image, nq int) *quantizer { if nq < 1 { return &quantizer{img: img, pxRGBA: internal.PxRGBAfunc(img)} } b := img.Bounds() npx := (b.Max.X - b.Min.X) * (b.Max.Y - b.Min.Y) qz := &quantizer{ img: img, ch: make(chValues, npx), cs: make([]cluster, nq), pxRGBA: internal.PxRGBAfunc(img), } // Populate initial cluster with all pixels from image. c := &qz.cs[0] px := make([]point, npx) c.px = px c.node = &quant.Node{} qz.t.Root = c.node c.minR = math.MaxUint32 c.minG = math.MaxUint32 c.minB = math.MaxUint32 c.bMinR = true c.bMinG = true c.bMinB = true c.bMaxR = true c.bMaxG = true c.bMaxB = true i := 0 for y := b.Min.Y; y < b.Max.Y; y++ { for x := b.Min.X; x < b.Max.X; x++ { px[i].x = int32(x) px[i].y = int32(y) r, g, b, _ := qz.pxRGBA(x, y) if r < c.minR { c.minR = r } if r > c.maxR { c.maxR = r } if g < c.minG { c.minG = g } if g > c.maxG { c.maxG = g } if b < c.minB { c.minB = b } if b > c.maxB { c.maxB = b } i++ } } return qz } // Cluster by repeatedly splitting clusters. // Use a heap as priority queue for picking clusters to split. // The rule is to spilt the cluster with the most pixels. // Terminate when the desired number of clusters has been populated // or when clusters cannot be further split. func (qz *quantizer) cluster() { pq := new(queue) // Initial cluster. populated at this point, but not analyzed. c := &qz.cs[0] var m uint32 i := 1 for { // Only enqueue clusters that can be split. if qz.setWidestChannel(c) { heap.Push(pq, c) } // If no clusters have any color variation, mark the end of the // cluster list and quit early. if len(*pq) == 0 { qz.cs = qz.cs[:i] break } s := heap.Pop(pq).(*cluster) // get cluster to split m = qz.medianCut(s) c = &qz.cs[i] // set c to new cluster i++ qz.split(s, c, m) // split s into c and s at value m // Normal exit is when all clusters are populated. if i == len(qz.cs) { break } if qz.setWidestChannel(s) { heap.Push(pq, s) // return s to queue } } // set TreePalette total and indexes qz.t.Leaves = i qz.t.Walk(func(leaf *quant.Node, i int) { leaf.Index = i }) // compute palette colors for i := range qz.cs { px := qz.cs[i].px // Average values in cluster to get palette color. var rsum, gsum, bsum int64 for _, p := range px { r, g, b, _ := qz.pxRGBA(int(p.x), int(p.y)) rsum += int64(r) gsum += int64(g) bsum += int64(b) } n64 := int64(len(px)) qz.cs[i].node.Color = color.RGBA64{ uint16(rsum / n64), uint16(gsum / n64), uint16(bsum / n64), 0xffff, } } } func (q *quantizer) setWidestChannel(c *cluster) bool { // Find extents of color values in each dimension. // (limits in cluster are not good enough here, we want extents as // represented by pixels.) var maxR, maxG, maxB uint32 minR := uint32(math.MaxUint32) minG := uint32(math.MaxUint32) minB := uint32(math.MaxUint32) for _, p := range c.px { r, g, b, _ := q.pxRGBA(int(p.x), int(p.y)) if r < minR { minR = r } if r > maxR { maxR = r } if g < minG { minG = g } if g > maxG { maxG = g } if b < minB { minB = b } if b > maxB { maxB = b } } // See which color dimension had the widest range. c.widestCh = rgbG min := minG max := maxG if maxR-minR > max-min { c.widestCh = rgbR min = minR max = maxR } if maxB-minB > max-min { c.widestCh = rgbB min = minB max = maxB } return max > min } // Arg c must have value range > 0 in dimension c.widestDim. // return value m is guararanteed to split cluster into two non-empty clusters // by v < m where v is pixel value of dimension c.Widest. func (q *quantizer) medianCut(c *cluster) uint32 { px := c.px ch := q.ch[:len(px)] // Copy values from appropriate color channel to buffer for // computing median. switch c.widestCh { case rgbR: for i, p := range c.px { r, _, _, _ := q.pxRGBA(int(p.x), int(p.y)) ch[i] = uint16(r) } case rgbG: for i, p := range c.px { _, g, _, _ := q.pxRGBA(int(p.x), int(p.y)) ch[i] = uint16(g) } case rgbB: for i, p := range c.px { _, _, b, _ := q.pxRGBA(int(p.x), int(p.y)) ch[i] = uint16(b) } } // Find cut. sort.Sort(ch) m1 := len(ch) / 2 // median if ch[m1] != ch[m1-1] { return uint32(ch[m1]) } m2 := m1 // Dec m1 until element to left is different. for m1--; m1 > 0 && ch[m1] == ch[m1-1]; m1-- { } // Inc m2 until element to left is different. for m2++; m2 < len(ch) && ch[m2] == ch[m2-1]; m2++ { } // Return value that makes more equitable cut. if m1 > len(ch)-m2 { return uint32(ch[m1]) } return uint32(ch[m2]) } // split s into c and s at value m func (q *quantizer) split(s, c *cluster, m uint32) { *c = *s // copy extent data px := s.px var v uint32 i := 0 last := len(px) - 1 for i <= last { // Get color value in appropriate dimension. r, g, b, _ := q.pxRGBA(int(px[i].x), int(px[i].y)) switch s.widestCh { case rgbR: v = r case rgbG: v = g case rgbB: v = b } // Split at m. if v < m { i++ } else { px[last], px[i] = px[i], px[last] last-- } } // Split the pixel list. s keeps smaller values, c gets larger values. s.px = px[:i] c.px = px[i:] // Split color extent n := s.node switch s.widestCh { case rgbR: s.maxR = m c.minR = m s.bMaxR = false c.bMinR = false n.Type = quant.TSplitR case rgbG: s.maxG = m c.minG = m s.bMaxG = false c.bMinG = false n.Type = quant.TSplitG case rgbB: s.maxB = m c.minB = m s.bMaxB = false c.bMinB = false n.Type = quant.TSplitB } // Split node n.Split = m n.Low = &quant.Node{} n.High = &quant.Node{} s.node, c.node = n.Low, n.High } func (qz *quantizer) paletted() *image.Paletted { cp := qz.t.ColorPalette() pi := image.NewPaletted(qz.img.Bounds(), cp) for i := range qz.cs { x := uint8(qz.cs[i].node.Index) for _, p := range qz.cs[i].px { pi.SetColorIndex(int(p.x), int(p.y), x) } } return pi } // Implement sort.Interface for sort in median algorithm. func (c chValues) Len() int { return len(c) } func (c chValues) Less(i, j int) bool { return c[i] < c[j] } func (c chValues) Swap(i, j int) { c[i], c[j] = c[j], c[i] } // Implement heap.Interface for priority queue of clusters. func (q queue) Len() int { return len(q) } // Priority is number of pixels in cluster. func (q queue) Less(i, j int) bool { return len(q[i].px) > len(q[j].px) } func (q queue) Swap(i, j int) { q[i], q[j] = q[j], q[i] } func (pq *queue) Push(x interface{}) { c := x.(*cluster) *pq = append(*pq, c) } func (pq *queue) Pop() interface{} { q := *pq n := len(q) - 1 c := q[n] *pq = q[:n] return c } quant-1.0.0/median/median_test.go000066400000000000000000000044411324705235500167310ustar00rootroot00000000000000package median_test import ( "fmt" "image" "image/png" "os" "path/filepath" "runtime" "testing" "github.com/soniakeys/quant" "github.com/soniakeys/quant/median" ) // TestMedian tests the median quantizer on png files found in the source // directory. Output files are prefixed with _median_. Files beginning with // _ are skipped when scanning for input files. Note nothing is tested // with a fresh source tree--drop a png or two in the source directory // before testing to give the test something to work on. Png files in the // parent directory are similarly used for testing. Put files there // to compare results of the different quantizers. func TestMedian(t *testing.T) { for _, p := range glob(t) { f, err := os.Open(p) if err != nil { t.Log(err) // skip files that can't be opened continue } img, err := png.Decode(f) f.Close() if err != nil { t.Log(err) // skip files that can't be decoded continue } pDir, pFile := filepath.Split(p) for _, n := range []int{16, 256} { // prefix _ on file name marks this as a result fq, err := os.Create(fmt.Sprintf("%s_median_%d_%s", pDir, n, pFile)) if err != nil { t.Fatal(err) // probably can't create any others } var q quant.Quantizer = median.Quantizer(n) if err = png.Encode(fq, q.Paletted(img)); err != nil { t.Fatal(err) // any problem is probably a problem for all } } } } func glob(tb testing.TB) []string { _, file, _, _ := runtime.Caller(0) srcDir, _ := filepath.Split(file) // ignore file names starting with _, those are result files. imgs, err := filepath.Glob(srcDir + "[^_]*.png") if err != nil { tb.Fatal(err) } if srcDir > "" { parentDir, _ := filepath.Split(srcDir[:len(srcDir)-1]) parentImgs, err := filepath.Glob(parentDir + "[^_]*.png") if err != nil { tb.Fatal(err) } imgs = append(parentImgs, imgs...) } return imgs } func BenchmarkPalette(b *testing.B) { var img image.Image for _, p := range glob(b) { f, err := os.Open(p) if err != nil { b.Log(err) // skip files that can't be opened continue } img, err = png.Decode(f) f.Close() if err != nil { b.Log(err) // skip files that can't be decoded continue } break } var q quant.Quantizer = median.Quantizer(256) b.ResetTimer() for i := 0; i < b.N; i++ { q.Palette(img) } } quant-1.0.0/median/readme.md000066400000000000000000000000641324705235500156620ustar00rootroot00000000000000Median ====== Basic median cut color quantization. quant-1.0.0/palette.go000066400000000000000000000074151324705235500146420ustar00rootroot00000000000000// Copyright 2013 Sonia Keys. // Licensed under MIT license. See "license" file in this source tree. package quant import ( "image" "image/color" ) // Palette is a palette of color.Colors, much like color.Palette of the // standard library. // // It is defined as an interface here to allow more general implementations, // presumably ones that maintain some data structure to achieve performance // advantages over linear search. type Palette interface { Len() int IndexNear(color.Color) int ColorNear(color.Color) color.Color ColorPalette() color.Palette } var _ Palette = LinearPalette{} var _ Palette = TreePalette{} // LinearPalette implements the Palette interface with color.Palette // and has no optimizations. type LinearPalette struct { color.Palette } // IndexNear returns the palette index of the nearest palette color. // // It simply wraps color.Palette.Index. func (p LinearPalette) IndexNear(c color.Color) int { return p.Palette.Index(c) } // Color near returns the nearest palette color. // // It simply wraps color.Palette.Convert. func (p LinearPalette) ColorNear(c color.Color) color.Color { return p.Palette.Convert(c) } // ColorPalette satisfies interface Palette. // // It simply returns the internal color.Palette. func (p LinearPalette) ColorPalette() color.Palette { return p.Palette } func (p LinearPalette) Len() int { return len(p.Palette) } // TreePalette implements the Palette interface with a binary tree. // // XNear methods run in O(log n) time for palette size. // // Fields are exported for access by quantizer packages. Typical use of // TreePalette should be through methods. type TreePalette struct { Leaves int Root *Node } func (p TreePalette) Len() int { return p.Leaves } // Node is a TreePalette node. It is exported for access by quantizer // packages and otherwise can be ignored for typical use of this package. type Node struct { Type int // for TLeaf Index int Color color.RGBA64 // for TSplit Split uint32 Low, High *Node } const ( TLeaf = iota TSplitR TSplitG TSplitB ) // IndexNear returns the index of the nearest palette color. func (t TreePalette) IndexNear(c color.Color) (i int) { if t.Root == nil { return -1 } t.Search(c, func(leaf *Node) { i = leaf.Index }) return } // ColorNear returns the nearest palette color. func (t TreePalette) ColorNear(c color.Color) (p color.Color) { if t.Root == nil { return color.RGBA64{0x7fff, 0x7fff, 0x7fff, 0xfff} } t.Search(c, func(leaf *Node) { p = leaf.Color }) return } // Search searches for the given color and calls f for the node representing // the nearest color. func (t TreePalette) Search(c color.Color, f func(leaf *Node)) { r, g, b, _ := c.RGBA() var lt bool var s func(*Node) s = func(n *Node) { switch n.Type { case TLeaf: f(n) return case TSplitR: lt = r < n.Split case TSplitG: lt = g < n.Split case TSplitB: lt = b < n.Split } if lt { s(n.Low) } else { s(n.High) } } s(t.Root) } // ColorPalette returns a color.Palette corresponding to the TreePalette. func (t TreePalette) ColorPalette() color.Palette { if t.Root == nil { return nil } p := make(color.Palette, 0, t.Leaves) t.Walk(func(leaf *Node, i int) { p = append(p, leaf.Color) }) return p } // Walk walks the TreePalette calling f for each color. func (t TreePalette) Walk(f func(leaf *Node, i int)) { i := 0 var w func(*Node) w = func(n *Node) { if n.Type == TLeaf { f(n, i) i++ return } w(n.Low) w(n.High) } w(t.Root) } func Paletted(p Palette, img image.Image) *image.Paletted { if p.Len() > 256 { return nil } b := img.Bounds() pi := image.NewPaletted(b, p.ColorPalette()) for y := b.Min.Y; y < b.Max.Y; y++ { for x := b.Min.X; x < b.Max.X; x++ { pi.SetColorIndex(x, y, uint8(p.IndexNear(img.At(x, y)))) } } return pi } quant-1.0.0/quant.go000066400000000000000000000010441324705235500143240ustar00rootroot00000000000000// Copyright 2013 Sonia Keys. // Licensed under MIT license. See "license" file in this source tree. // Quant provides an interface for image color quantizers. package quant import "image" // Quantizer defines a color quantizer for images. type Quantizer interface { // Paletted quantizes an image and returns a paletted image. Paletted(image.Image) *image.Paletted // Palette quantizes an image and returns a Palette. Note the return // type is the Palette interface of this package and not image.Palette. Palette(image.Image) Palette } quant-1.0.0/quant_test.go000066400000000000000000000062441324705235500153720ustar00rootroot00000000000000package quant_test import ( "fmt" "image" "image/color" "image/draw" "image/png" "os" "path/filepath" "runtime" "testing" "github.com/soniakeys/quant" "github.com/soniakeys/quant/median" ) // TestDither tests Sierra24A on png files found in the source directory. // Output files are prefixed with _dither_256_. Files beginning with _ // are skipped when scanning for input files. Thus nothing is tested // with a fresh source tree--drop a png or two in the source directory // before testing to give the test something to work on. func TestDitherMedianDraw(t *testing.T) { _, file, _, _ := runtime.Caller(0) srcDir, _ := filepath.Split(file) // ignore file names starting with _, those are result files. imgs, err := filepath.Glob(srcDir + "[^_]*.png") if err != nil { t.Fatal(err) } const n = 256 // exercise draw.Quantizer interface var q draw.Quantizer = median.Quantizer(n) // exercise draw.Drawer interface var d draw.Drawer = quant.Sierra24A{} for _, p := range imgs { f, err := os.Open(p) if err != nil { t.Error(err) // skip files that can't be opened continue } img, err := png.Decode(f) f.Close() if err != nil { t.Error(err) // skip files that can't be decoded continue } pDir, pFile := filepath.Split(p) // prefix _ on file name marks this as a result fq, err := os.Create(fmt.Sprintf("%s_dither_median_draw_%d_%s", pDir, n, pFile)) if err != nil { t.Fatal(err) // probably can't create any others } b := img.Bounds() pi := image.NewPaletted(b, q.Quantize(make(color.Palette, 0, n), img)) d.Draw(pi, b, img, b.Min) if err = png.Encode(fq, pi); err != nil { t.Fatal(err) // any problem is probably a problem for all } } } // TestDither tests Sierra24A on png files found in the source directory. // Output files are prefixed with _dither_256_. Files beginning with _ // are skipped when scanning for input files. Thus nothing is tested // with a fresh source tree--drop a png or two in the source directory // before testing to give the test something to work on. func TestDitherMedianPalette(t *testing.T) { _, file, _, _ := runtime.Caller(0) srcDir, _ := filepath.Split(file) // ignore file names starting with _, those are result files. imgs, err := filepath.Glob(srcDir + "[^_]*.png") if err != nil { t.Fatal(err) } const n = 256 // exercise draw.Quantizer interface var q draw.Quantizer = median.Quantizer(n) // exercise draw.Drawer interface var d draw.Drawer = quant.Sierra24A{} for _, p := range imgs { f, err := os.Open(p) if err != nil { t.Error(err) // skip files that can't be opened continue } img, err := png.Decode(f) f.Close() if err != nil { t.Error(err) // skip files that can't be decoded continue } pDir, pFile := filepath.Split(p) // prefix _ on file name marks this as a result fq, err := os.Create(fmt.Sprintf("%s_dither_median_palette_%d_%s", pDir, n, pFile)) if err != nil { t.Fatal(err) // probably can't create any others } b := img.Bounds() pi := image.NewPaletted(b, q.Quantize(make(color.Palette, 0, n), img)) d.Draw(pi, b, img, b.Min) if err = png.Encode(fq, pi); err != nil { t.Fatal(err) // any problem is probably a problem for all } } } quant-1.0.0/readme.md000066400000000000000000000004301324705235500144220ustar00rootroot00000000000000Quant ===== Experiments with color quantizers Implemented here are two rather simple quantizers and an (also simple) ditherer. The quantizers satisfy the draw.Quantizer interface of the standard library. The ditherer satisfies the draw.Drawer interface of the standard library. quant-1.0.0/sierra.go000066400000000000000000000100421324705235500144570ustar00rootroot00000000000000// Copyright 2013 Sonia Keys. // Licensed under MIT license. See "license" file in this source tree. package quant import ( "image" "image/color" "image/draw" "math" ) // Sierra24A satisfies draw.Drawer type Sierra24A struct{} var _ draw.Drawer = Sierra24A{} // Draw performs error diffusion dithering. // // This method satisfies the draw.Drawer interface, implementing a dithering // filter attributed to Frankie Sierra. It uses the kernel // // X 2 // 1 1 func (d Sierra24A) Draw(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point) { pd, ok := dst.(*image.Paletted) if !ok { // dither211 currently requires a palette draw.Draw(dst, r, src, sp, draw.Src) return } // intersect r with both dst and src bounds, fix up sp. ir := r.Intersect(pd.Bounds()). Intersect(src.Bounds().Add(r.Min.Sub(sp))) if ir.Empty() { return // no work to do. } sp = ir.Min.Sub(r.Min) // get subimage of src sr := ir.Add(sp) if !sr.Eq(src.Bounds()) { s, ok := src.(interface { SubImage(image.Rectangle) image.Image }) if !ok { // dither211 currently works on whole images draw.Draw(dst, r, src, sp, draw.Src) return } src = s.SubImage(sr) } // dither211 currently returns a new image, or nil if dithering not // possible. if s := dither211(src, pd.Palette); s != nil { src = s } // this avoids any problem of src dst overlap but it would usually // work to render directly into dst. todo. draw.Draw(dst, r, src, image.Point{}, draw.Src) } // signed color type, no alpha. signed to represent color deltas as well as // color values 0-ffff as with colorRGBA64 type sRGB struct{ r, g, b int32 } type sPalette []sRGB func (p sPalette) index(c sRGB) int { // still the awful linear search i, min := 0, int64(math.MaxInt64) for j, pc := range p { d := int64(c.r) - int64(pc.r) s := d * d d = int64(c.g) - int64(pc.g) s += d * d d = int64(c.b) - int64(pc.b) s += d * d if s < min { min = s i = j } } return i } // currently this is strictly a helper function for Dither211.Draw, so // not generalized to use Palette from this package. func dither211(i0 image.Image, cp color.Palette) *image.Paletted { if len(cp) > 256 { // representation limit of image.Paletted. a little sketchy to return // nil, but unworkable results are always better than wrong results. return nil } b := i0.Bounds() pi := image.NewPaletted(b, cp) if b.Empty() { return pi // no work to do } sp := make(sPalette, len(cp)) for i, c := range cp { r, g, b, _ := c.RGBA() sp[i] = sRGB{int32(r), int32(g), int32(b)} } // afc is adjustd full color. e, rt, dn hold diffused errors. var afc, e, rt sRGB dn := make([]sRGB, b.Dx()+1) for y := b.Min.Y; y < b.Max.Y; y++ { rt = dn[0] dn[0] = sRGB{} for x := b.Min.X; x < b.Max.X; x++ { // full color from original image r0, g0, b0, _ := i0.At(x, y).RGBA() // adjusted full color = original color + diffused error afc.r = int32(r0) + rt.r>>2 afc.g = int32(g0) + rt.g>>2 afc.b = int32(b0) + rt.b>>2 // clipping or clamping is usually explained as necessary // to avoid integer overflow but with palettes that do not // represent the full color space of the image, it is needed // to keep areas of excess color from saturating at palette // limits and bleeding into neighboring areas. if afc.r < 0 { afc.r = 0 } else if afc.r > 0xffff { afc.r = 0xffff } if afc.g < 0 { afc.g = 0 } else if afc.g > 0xffff { afc.g = 0xffff } if afc.b < 0 { afc.b = 0 } else if afc.b > 0xffff { afc.b = 0xffff } // nearest palette entry i := sp.index(afc) // set pixel in destination image pi.SetColorIndex(x, y, uint8(i)) // error to be diffused = full color - palette color. pc := sp[i] e.r = afc.r - pc.r e.g = afc.g - pc.g e.b = afc.b - pc.b // half of error*4 goes right dx := x - b.Min.X + 1 rt.r = dn[dx].r + e.r*2 rt.g = dn[dx].g + e.g*2 rt.b = dn[dx].b + e.b*2 // the other half goes down dn[dx] = e dn[dx-1].r += e.r dn[dx-1].g += e.g dn[dx-1].b += e.b } } return pi }