├── LICENSE.md ├── README.md ├── examples ├── computer.png ├── frog.png ├── nyan.png ├── peach.png └── phone.png ├── main.go └── pixsort ├── anneal.go ├── model.go ├── run.go └── util.go /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019 Michael Fogleman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## pixsort 2 | 3 | Applying the traveling salesman problem to pixel art. 4 | 5 | Goal: Find the shortest path to visit all black pixels in an image. 6 | 7 | Algorithm: Simulated annealing. 8 | 9 | ![Frog](http://i.imgur.com/2xiwTVE.gif) 10 | 11 | ![Peach](http://i.imgur.com/sCBhROn.gif) 12 | 13 | ## Usage 14 | 15 | go get github.com/fogleman/pixsort 16 | pixsort image.png 17 | 18 | This will generate a file named `image.png.gif` with the result. 19 | 20 | You can also pass in a `quality` parameter to make it try harder. 21 | 22 | pixsort image.png 28 23 | 24 | The algorithm will run `2 ^ quality` iterations. 25 | -------------------------------------------------------------------------------- /examples/computer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pixsort/29754db2f6df2a39c9db8ae81d1779d2aa3cdecf/examples/computer.png -------------------------------------------------------------------------------- /examples/frog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pixsort/29754db2f6df2a39c9db8ae81d1779d2aa3cdecf/examples/frog.png -------------------------------------------------------------------------------- /examples/nyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pixsort/29754db2f6df2a39c9db8ae81d1779d2aa3cdecf/examples/nyan.png -------------------------------------------------------------------------------- /examples/peach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pixsort/29754db2f6df2a39c9db8ae81d1779d2aa3cdecf/examples/peach.png -------------------------------------------------------------------------------- /examples/phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/pixsort/29754db2f6df2a39c9db8ae81d1779d2aa3cdecf/examples/phone.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/fogleman/pixsort/pixsort" 11 | ) 12 | 13 | func main() { 14 | rand.Seed(time.Now().UTC().UnixNano()) 15 | args := os.Args[1:] 16 | if len(args) < 1 || len(args) > 2 { 17 | fmt.Println("Usage: pixsort image.png [quality]") 18 | return 19 | } 20 | path := args[0] 21 | quality := 24 22 | if len(args) == 2 { 23 | q, err := strconv.ParseInt(args[1], 0, 0) 24 | if err != nil || q < 0 || q > 30 { 25 | fmt.Println("Quality value must be between 0 and 30") 26 | return 27 | } 28 | quality = int(q) 29 | } 30 | pixsort.Run(path, quality) 31 | } 32 | -------------------------------------------------------------------------------- /pixsort/anneal.go: -------------------------------------------------------------------------------- 1 | package pixsort 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | ) 8 | 9 | type Annealable interface { 10 | Energy() float64 11 | DoMove() interface{} 12 | UndoMove(interface{}) 13 | Copy() Annealable 14 | } 15 | 16 | func Anneal(state Annealable, maxTemp, minTemp float64, steps int) Annealable { 17 | factor := -math.Log(maxTemp / minTemp) 18 | state = state.Copy() 19 | bestState := state.Copy() 20 | bestEnergy := state.Energy() 21 | previousEnergy := bestEnergy 22 | for step := 0; step < steps; step++ { 23 | if step%100000 == 0 { 24 | showProgress(step, steps) 25 | } 26 | pct := float64(step) / float64(steps-1) 27 | temp := maxTemp * math.Exp(factor*pct) 28 | undo := state.DoMove() 29 | energy := state.Energy() 30 | change := energy - previousEnergy 31 | if change > 0 && math.Exp(-change/temp) < rand.Float64() { 32 | state.UndoMove(undo) 33 | } else { 34 | previousEnergy = energy 35 | if energy < bestEnergy { 36 | // pct := float64(step*100) / float64(steps) 37 | // fmt.Printf("step: %d of %d (%.1f%%), temp: %.3f, energy: %.3f\n", 38 | // step, steps, pct, temp, energy) 39 | bestEnergy = energy 40 | bestState = state.Copy() 41 | } 42 | } 43 | } 44 | showProgress(steps, steps) 45 | return bestState 46 | } 47 | 48 | func PreAnneal(state Annealable, iterations int) float64 { 49 | state = state.Copy() 50 | previous := state.Energy() 51 | var total float64 52 | for i := 0; i < iterations; i++ { 53 | state.DoMove() 54 | energy := state.Energy() 55 | total += math.Abs(energy - previous) 56 | previous = energy 57 | } 58 | return total / float64(iterations) 59 | } 60 | 61 | func showProgress(i, n int) { 62 | pct := int(100 * float64(i) / float64(n)) 63 | fmt.Printf(" %3d%% [", pct) 64 | for p := 0; p < 100; p += 3 { 65 | if pct > p { 66 | fmt.Print("=") 67 | } else { 68 | fmt.Print(" ") 69 | } 70 | } 71 | fmt.Printf("] \r") 72 | } 73 | -------------------------------------------------------------------------------- /pixsort/model.go: -------------------------------------------------------------------------------- 1 | package pixsort 2 | 3 | import "math/rand" 4 | 5 | type Point struct { 6 | X, Y int 7 | } 8 | 9 | func (a Point) DistanceTo(b Point) int { 10 | dx := a.X - b.X 11 | dy := a.Y - b.Y 12 | return dx*dx + dy*dy 13 | } 14 | 15 | type Undo struct { 16 | I, J, Score int 17 | } 18 | 19 | type Model struct { 20 | Points []Point 21 | Score int 22 | } 23 | 24 | func NewModel(points []Point) *Model { 25 | model := Model{} 26 | model.Points = points 27 | model.Score = 0 28 | for i := 0; i < len(points)-1; i++ { 29 | model.Score += points[i].DistanceTo(points[i+1]) 30 | } 31 | return &model 32 | } 33 | 34 | func (m *Model) Energy() float64 { 35 | return float64(m.Score) 36 | } 37 | 38 | func (m *Model) DoMove() interface{} { 39 | n := len(m.Points) 40 | i := rand.Intn(n) 41 | j := rand.Intn(n) 42 | for m.Closest(i, j) > 50 { 43 | j = rand.Intn(n) 44 | } 45 | s := m.Score 46 | m.Update(i, j, -1) 47 | m.Move(i, j) 48 | m.Update(i, j, 1) 49 | return Undo{i, j, s} 50 | } 51 | 52 | func (m *Model) UndoMove(undo interface{}) { 53 | u := undo.(Undo) 54 | m.Move(u.J, u.I) 55 | m.Score = u.Score 56 | } 57 | 58 | func (m *Model) Copy() Annealable { 59 | points := make([]Point, len(m.Points)) 60 | copy(points, m.Points) 61 | return &Model{points, m.Score} 62 | } 63 | 64 | func (m *Model) Closest(i, j int) int { 65 | p := m.Points 66 | a := p[i].DistanceTo(p[j]) 67 | b := a 68 | if i < j { 69 | if j < len(p)-1 { 70 | b = p[i].DistanceTo(p[j+1]) 71 | } 72 | } else { 73 | if j > 0 { 74 | b = p[i].DistanceTo(p[j-1]) 75 | } 76 | } 77 | if a <= b { 78 | return a 79 | } else { 80 | return b 81 | } 82 | } 83 | 84 | func (m *Model) Move(i, j int) { 85 | p := m.Points 86 | v := p[i] 87 | for i < j { 88 | p[i] = p[i+1] 89 | i++ 90 | } 91 | for i > j { 92 | p[i] = p[i-1] 93 | i-- 94 | } 95 | p[j] = v 96 | } 97 | 98 | func (m *Model) Update(i, j, sign int) { 99 | if i == j { 100 | return 101 | } 102 | var indexes []int 103 | if sign < 0 { 104 | if i < j { 105 | indexes = []int{i - 1, i, j} 106 | } else { 107 | indexes = []int{i - 1, i, j - 1} 108 | } 109 | } else { 110 | if i < j { 111 | indexes = []int{i - 1, j - 1, j} 112 | } else { 113 | indexes = []int{i, j - 1, j} 114 | } 115 | } 116 | n := len(m.Points) - 1 117 | for _, a := range indexes { 118 | if a < 0 || a >= n { 119 | continue 120 | } 121 | m.Score += sign * m.Points[a].DistanceTo(m.Points[a+1]) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /pixsort/run.go: -------------------------------------------------------------------------------- 1 | package pixsort 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | ) 8 | 9 | func Run(path string, quality int) { 10 | iterations := int(math.Pow(2, float64(quality))) 11 | im, err := LoadPNG(path) 12 | if err != nil { 13 | panic(err) 14 | } 15 | w, h, points := GetPoints(im) 16 | fmt.Printf("Sorting %d pixels...\n", len(points)) 17 | fmt.Printf("Quality = %d (%d iterations)\n", quality, iterations) 18 | model := NewModel(points) 19 | fmt.Printf("Initial Score = %d\n", int(model.Energy())) 20 | maxTemp := 10.0 21 | minTemp := 0.1 22 | start := time.Now() 23 | model = Anneal(model, maxTemp, minTemp, iterations).(*Model) 24 | elapsed := time.Since(start).Seconds() 25 | fmt.Printf("%c[2K", 27) 26 | fmt.Printf(" Final Score = %d\n", int(model.Energy())) 27 | fmt.Printf(" Elapsed Time = %.2fs\n", elapsed) 28 | out := fmt.Sprintf("%s.%d.gif", path, int(model.Energy())) 29 | SaveGIF(out, 8, w, h, model.Points) 30 | } 31 | -------------------------------------------------------------------------------- /pixsort/util.go: -------------------------------------------------------------------------------- 1 | package pixsort 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/gif" 7 | "image/png" 8 | "os" 9 | ) 10 | 11 | func LoadPNG(path string) (image.Image, error) { 12 | file, err := os.Open(path) 13 | if err != nil { 14 | return nil, err 15 | } 16 | defer file.Close() 17 | return png.Decode(file) 18 | } 19 | 20 | func GetPoints(im image.Image) (int, int, []Point) { 21 | var result []Point 22 | bounds := im.Bounds() 23 | w := bounds.Size().X 24 | h := bounds.Size().Y 25 | for y := 0; y < h; y++ { 26 | for x := 0; x < w; x++ { 27 | c := im.At(x, y) 28 | r, _, _, a := c.RGBA() 29 | if r < 128 && a > 128 { 30 | result = append(result, Point{x, y}) 31 | } 32 | } 33 | } 34 | return w, h, result 35 | } 36 | 37 | func CreateFrame(m, w, h int, points []Point) *image.Paletted { 38 | var palette []color.Color 39 | palette = append(palette, color.RGBA{255, 255, 255, 255}) 40 | palette = append(palette, color.RGBA{0, 0, 0, 255}) 41 | im := image.NewPaletted(image.Rect(0, 0, w*m, h*m), palette) 42 | for _, point := range points { 43 | for y := 0; y < m; y++ { 44 | for x := 0; x < m; x++ { 45 | im.SetColorIndex(point.X*m+x, point.Y*m+y, 1) 46 | } 47 | } 48 | } 49 | return im 50 | } 51 | 52 | func SaveGIF(path string, m, w, h int, points []Point) error { 53 | g := gif.GIF{} 54 | g.Image = append(g.Image, CreateFrame(m, w, h, nil)) 55 | g.Delay = append(g.Delay, 10) 56 | for i := range points { 57 | g.Image = append(g.Image, CreateFrame(m, w, h, points[:i+1])) 58 | g.Delay = append(g.Delay, 10) 59 | } 60 | g.Image = append(g.Image, CreateFrame(m, w, h, points)) 61 | g.Delay = append(g.Delay, 100) 62 | file, err := os.Create(path) 63 | if err != nil { 64 | return err 65 | } 66 | defer file.Close() 67 | return gif.EncodeAll(file, &g) 68 | } 69 | --------------------------------------------------------------------------------