├── README.md ├── example ├── bridge.gif └── bridge.jpg ├── go.mod ├── go.sum └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # voronoi-interp 2 | 3 | If you only know a few random pixels of an image, you can fill in the rest using nearest neighbors. This can result in cool animations as you gradually add more and more pixels at random. 4 | 5 | # Example 6 | 7 | Here is an example output of this program: 8 | 9 | ![Bridge emerging from Voronoi cells](example/bridge.gif) 10 | 11 | # Usage 12 | 13 | Turn any picture into an image like so: 14 | 15 | ```shell 16 | $ go run . -in /path/to/img.jpg -out /path/to/video.mp4 17 | ``` 18 | 19 | There are various options to control the video (see `go run . -help`): 20 | 21 | ``` 22 | -duration float 23 | duration of animation (default 7) 24 | -exponent float 25 | exponent to control rate of added points (default 0.8) 26 | -fps float 27 | frame rate (default 5) 28 | -in string 29 | input file path 30 | -out string 31 | output file path 32 | -pause float 33 | pause at end of animation (default 1) 34 | ``` 35 | -------------------------------------------------------------------------------- /example/bridge.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/voronoi-interp/04ad837b15f7a11f5ff1d6c48707201d8d1bc6d2/example/bridge.gif -------------------------------------------------------------------------------- /example/bridge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixpickle/voronoi-interp/04ad837b15f7a11f5ff1d6c48707201d8d1bc6d2/example/bridge.jpg -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/unixpickle/voronoi-interp 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/unixpickle/essentials v1.3.0 7 | github.com/unixpickle/ffmpego v0.1.3 8 | github.com/unixpickle/model3d v0.2.13 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 2 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 3 | github.com/unixpickle/essentials v1.1.0/go.mod h1:dQ1idvqrgrDgub3mfckQm7osVPzT3u9rB6NK/LEhmtQ= 4 | github.com/unixpickle/essentials v1.3.0 h1:H258Z5Uo1pVzFjxD2rwFWzHPN3s0J0jLs5kuxTRSfCs= 5 | github.com/unixpickle/essentials v1.3.0/go.mod h1:dQ1idvqrgrDgub3mfckQm7osVPzT3u9rB6NK/LEhmtQ= 6 | github.com/unixpickle/ffmpego v0.1.3 h1:hBJOG5bhzks9LdKBredqiaszdR+PI2nXwxIopxNszxw= 7 | github.com/unixpickle/ffmpego v0.1.3/go.mod h1:/FhjiVp7FU7ai+ivSdWDw6KY4FdTYiwS5zp3oiXOrt4= 8 | github.com/unixpickle/model3d v0.2.13 h1:6vRsvBB1tlMGCc813iJXQaUmFEiwOSDDxiwDf9cCAqI= 9 | github.com/unixpickle/model3d v0.2.13/go.mod h1:Xu7k4U/wrdq//+bGAo9QrQ3lrRXA+tiV2FAf4TEf6FE= 10 | github.com/unixpickle/splaytree v0.0.0-20160517015709-ba216b293df0 h1:vf24zG+kzuiwC/Y8I4MeIl0C0StIiBAmFPsnpv/BtuA= 11 | github.com/unixpickle/splaytree v0.0.0-20160517015709-ba216b293df0/go.mod h1:GaKWGsPs4eeIaQbzcYyytkXTrMTczow7bvvBlQSKX1c= 12 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "image" 6 | "image/color" 7 | _ "image/jpeg" 8 | _ "image/png" 9 | "log" 10 | "math" 11 | "math/rand" 12 | "os" 13 | 14 | "github.com/unixpickle/essentials" 15 | "github.com/unixpickle/ffmpego" 16 | "github.com/unixpickle/model3d/model2d" 17 | "github.com/unixpickle/model3d/model3d" 18 | ) 19 | 20 | func main() { 21 | var inPath string 22 | var outPath string 23 | var fps float64 24 | var duration float64 25 | var pauseTime float64 26 | var timeExponent float64 27 | var average bool 28 | flag.StringVar(&inPath, "in", "", "input file path") 29 | flag.StringVar(&outPath, "out", "", "output file path") 30 | flag.Float64Var(&fps, "fps", 5.0, "frame rate") 31 | flag.Float64Var(&duration, "duration", 7.0, "duration of animation") 32 | flag.Float64Var(&pauseTime, "pause", 1.0, "pause at end of animation") 33 | flag.Float64Var(&timeExponent, "exponent", 0.8, "exponent to control rate of added points") 34 | flag.BoolVar(&average, "average", false, "average all colors in each Voronoi cell") 35 | flag.Parse() 36 | 37 | if inPath == "" || outPath == "" { 38 | essentials.Die("missing required flags: -in and -out. See -help.") 39 | } 40 | 41 | f, err := os.Open(inPath) 42 | essentials.Must(err) 43 | img, _, err := image.Decode(f) 44 | f.Close() 45 | essentials.Must(err) 46 | 47 | b := img.Bounds() 48 | var coords []model2d.Coord 49 | for i := 0; i < b.Dx(); i++ { 50 | for j := 0; j < b.Dy(); j++ { 51 | coords = append(coords, model2d.XY(float64(i), float64(j))) 52 | } 53 | } 54 | 55 | for i := 0; i < len(coords)-1; i++ { 56 | j := i + rand.Intn(len(coords)-i) 57 | coords[i], coords[j] = coords[j], coords[i] 58 | } 59 | 60 | writer, err := ffmpego.NewVideoWriter(outPath, b.Dx(), b.Dy(), fps) 61 | essentials.Must(err) 62 | defer writer.Close() 63 | 64 | interpTarget := math.Log(float64(len(coords))) 65 | step := 1 / (fps * duration) 66 | for t := step; true; t += step { 67 | log.Printf("timestep %f", t) 68 | count := int(math.Round(math.Exp(interpTarget * math.Pow(t, timeExponent)))) 69 | count = essentials.MinInt(count, len(coords)) 70 | 71 | var frame image.Image 72 | if average { 73 | frame = RenderFrameAverage(img, coords[:count]) 74 | } else { 75 | frame = RenderFrame(img, coords[:count]) 76 | } 77 | essentials.Must(writer.WriteFrame(frame)) 78 | 79 | if count == len(coords) { 80 | break 81 | } 82 | } 83 | for i := 0.0; i < fps*pauseTime; i++ { 84 | essentials.Must(writer.WriteFrame(img)) 85 | } 86 | } 87 | 88 | func RenderFrame(img image.Image, coords []model2d.Coord) image.Image { 89 | tree := model2d.NewCoordTree(coords) 90 | b := img.Bounds() 91 | res := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy())) 92 | for i := 0; i < b.Dy(); i++ { 93 | for j := 0; j < b.Dx(); j++ { 94 | source := tree.NearestNeighbor(model2d.XY(float64(j), float64(i))) 95 | c := img.At(int(source.X)+b.Min.X, int(source.Y)+b.Min.Y) 96 | res.Set(j, i, c) 97 | } 98 | } 99 | return res 100 | } 101 | 102 | func RenderFrameAverage(img image.Image, coords []model2d.Coord) image.Image { 103 | tree := model2d.NewCoordTree(coords) 104 | sums := map[model2d.Coord]model3d.Coord3D{} 105 | counts := map[model2d.Coord]float64{} 106 | 107 | b := img.Bounds() 108 | for i := 0; i < b.Dy(); i++ { 109 | for j := 0; j < b.Dx(); j++ { 110 | source := tree.NearestNeighbor(model2d.XY(float64(j), float64(i))) 111 | r, g, b, _ := img.At(j+b.Min.X, i+b.Min.Y).RGBA() 112 | rgb := model3d.XYZ(float64(r)/256, float64(g)/256, float64(b)/256) 113 | sums[source] = sums[source].Add(rgb) 114 | counts[source]++ 115 | } 116 | } 117 | 118 | res := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy())) 119 | for i := 0; i < b.Dy(); i++ { 120 | for j := 0; j < b.Dx(); j++ { 121 | source := tree.NearestNeighbor(model2d.XY(float64(j), float64(i))) 122 | avg := sums[source].Scale(1 / counts[source]) 123 | res.Set(j, i, color.RGBA{ 124 | R: uint8(avg.X), 125 | G: uint8(avg.Y), 126 | B: uint8(avg.Z), 127 | A: 0xff, 128 | }) 129 | } 130 | } 131 | return res 132 | } 133 | --------------------------------------------------------------------------------