mirror of
https://github.com/esimov/pigo-wasm-demos.git
synced 2025-09-26 20:31:20 +08:00
300 lines
6.8 KiB
Go
300 lines
6.8 KiB
Go
package pixelate
|
|
|
|
import (
|
|
"container/heap"
|
|
"image"
|
|
"image/color"
|
|
"image/draw"
|
|
"math"
|
|
"sort"
|
|
)
|
|
|
|
// SubImager is a wrapper implementing the SubImage method from the image package.
|
|
type SubImager interface {
|
|
SubImage(r image.Rectangle) image.Image
|
|
}
|
|
|
|
// Interface which implements the Quantize method.
|
|
type Quantizer interface {
|
|
Quantize(image.Image, draw.Image, int, bool, bool) image.Image
|
|
}
|
|
|
|
// A workspace with members that can be accessed by methods.
|
|
type Quant struct {
|
|
img image.Image // original image
|
|
cs []cluster // len is the desired number of colors
|
|
px []point // list of all points in the image
|
|
ch chValues // buffer for computing median
|
|
eq []point // additional buffer used when splitting cluster
|
|
}
|
|
|
|
type cluster struct {
|
|
px []point // list of points in the cluster
|
|
widestCh int // rx, gx, bx const for channel with widest value range
|
|
chRange uint32 // value range (vmax-vmin) of widest channel
|
|
}
|
|
|
|
type (
|
|
point struct{ x, y int }
|
|
chValues []uint32
|
|
queue []*cluster
|
|
)
|
|
|
|
const (
|
|
rx = iota
|
|
gx
|
|
bx
|
|
)
|
|
|
|
// NewQuantizer is a constructor method which initializes a new Quantizer.
|
|
func NewQuantizer() *Quant {
|
|
return &Quant{}
|
|
}
|
|
|
|
// Quantize returns a paletted image.
|
|
// We need to use type assertion to match the interface returning type.
|
|
func (q Quant) Quantize(img image.Image, nq int) image.Image {
|
|
qz := newQuantizer(img, nq) // set up a work space
|
|
qz.cluster() // cluster pixels by color
|
|
return qz.Paletted().(image.Image) // generate paletted image from clusters
|
|
}
|
|
|
|
func newQuantizer(img image.Image, nq int) *Quant {
|
|
b := img.Bounds()
|
|
npx := (b.Max.X - b.Min.X) * (b.Max.Y - b.Min.Y)
|
|
// Create work space.
|
|
qz := &Quant{
|
|
img: img,
|
|
ch: make(chValues, npx),
|
|
cs: make([]cluster, nq),
|
|
}
|
|
// Populate initial cluster with all pixels from image.
|
|
c := &qz.cs[0]
|
|
px := make([]point, npx)
|
|
c.px = px
|
|
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 = x
|
|
px[i].y = y
|
|
i++
|
|
}
|
|
}
|
|
return qz
|
|
}
|
|
|
|
func (qz *Quant) cluster() {
|
|
// Cluster by repeatedly splitting clusters.
|
|
// Use a heap as priority queue for picking clusters to split.
|
|
// The rule will be 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.
|
|
pq := new(queue)
|
|
// Initial cluster. populated at this point, but not analyzed.
|
|
c := &qz.cs[0]
|
|
for i := 1; ; {
|
|
qz.setColorRange(c)
|
|
// Cluster cannot be split if all pixels are the same color.
|
|
// Only enqueue clusters that can be split.
|
|
if c.chRange > 0 {
|
|
heap.Push(pq, c) // add new cluster to queue
|
|
}
|
|
// 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
|
|
c = &qz.cs[i] // set c to new cluster
|
|
i++
|
|
m := qz.Median(s)
|
|
qz.Split(s, c, m) // split s into c and s
|
|
// If that was the last cluster, we're done.
|
|
if i == len(qz.cs) {
|
|
break
|
|
}
|
|
qz.setColorRange(s)
|
|
if s.chRange > 0 {
|
|
heap.Push(pq, s) // return to queue
|
|
}
|
|
}
|
|
}
|
|
|
|
func (q *Quant) setColorRange(c *cluster) {
|
|
// Find extents of color values in each channel.
|
|
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.img.At(p.x, p.y).RGBA()
|
|
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 channel had the widest range.
|
|
s := gx
|
|
min := minG
|
|
max := maxG
|
|
if maxR-minR > max-min {
|
|
s = rx
|
|
min = minR
|
|
max = maxR
|
|
}
|
|
if maxB-minB > max-min {
|
|
s = bx
|
|
min = minB
|
|
max = maxB
|
|
}
|
|
c.widestCh = s
|
|
c.chRange = max - min // also store the range of that channel
|
|
}
|
|
|
|
func (q *Quant) Median(c *cluster) uint32 {
|
|
px := c.px
|
|
ch := q.ch[:len(px)]
|
|
// Copy values from appropriate channel to buffer for computing median.
|
|
switch c.widestCh {
|
|
case rx:
|
|
for i, p := range c.px {
|
|
ch[i], _, _, _ = q.img.At(p.x, p.y).RGBA()
|
|
}
|
|
case gx:
|
|
for i, p := range c.px {
|
|
_, ch[i], _, _ = q.img.At(p.x, p.y).RGBA()
|
|
}
|
|
case bx:
|
|
for i, p := range c.px {
|
|
_, _, ch[i], _ = q.img.At(p.x, p.y).RGBA()
|
|
}
|
|
}
|
|
// Median algorithm.
|
|
sort.Sort(ch)
|
|
half := len(ch) / 2
|
|
m := ch[half]
|
|
if len(ch)%2 == 0 {
|
|
m = (m + ch[half-1]) / 2
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (q *Quant) Split(s, c *cluster, m uint32) {
|
|
px := s.px
|
|
var v uint32
|
|
i := 0
|
|
lt := 0
|
|
gt := len(px) - 1
|
|
eq := q.eq[:0] // reuse any existing buffer
|
|
for i <= gt {
|
|
// Get pixel value of appropriate channel.
|
|
r, g, b, _ := q.img.At(px[i].x, px[i].y).RGBA()
|
|
switch s.widestCh {
|
|
case rx:
|
|
v = r
|
|
case gx:
|
|
v = g
|
|
case bx:
|
|
v = b
|
|
}
|
|
// Categorize each pixel as either <, >, or == median.
|
|
switch {
|
|
case v < m:
|
|
px[lt] = px[i]
|
|
lt++
|
|
i++
|
|
case v > m:
|
|
px[gt], px[i] = px[i], px[gt]
|
|
gt--
|
|
default:
|
|
eq = append(eq, px[i])
|
|
i++
|
|
}
|
|
}
|
|
// Handle values equal to the median.
|
|
if len(eq) > 0 {
|
|
copy(px[lt:], eq) // move them back between the lt and gt values.
|
|
// Then, if the number of gt values is < the number of lt values,
|
|
// fix up i so that the split will include the eq values with
|
|
// the gt values.
|
|
if len(px)-i < lt {
|
|
i = lt
|
|
}
|
|
q.eq = eq // squirrel away (possibly expanded) buffer for reuse
|
|
}
|
|
// Split the pixel list.
|
|
s.px = px[:i]
|
|
c.px = px[i:]
|
|
}
|
|
|
|
func (qz *Quant) Paletted() image.PalettedImage {
|
|
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.img.At(p.x, p.y).RGBA()
|
|
rsum += int64(r)
|
|
gsum += int64(g)
|
|
bsum += int64(b)
|
|
}
|
|
n64 := int64(len(px))
|
|
cp[i] = color.NRGBA64{
|
|
uint16(rsum / n64),
|
|
uint16(gsum / n64),
|
|
uint16(bsum / n64),
|
|
0xffff,
|
|
}
|
|
// set image pixels
|
|
for _, p := range px {
|
|
pi.SetColorIndex(p.x, p.y, uint8(i))
|
|
}
|
|
}
|
|
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) }
|
|
|
|
// Less implements rule to select cluster with greatest number of pixels.
|
|
func (q queue) Less(i, j int) bool {
|
|
return len(q[j].px) < len(q[i].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
|
|
}
|