Skip to content

Instantly share code, notes, and snippets.

@jphastings
Last active February 16, 2019 23:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jphastings/6d664eeea6a5d98b36316bc4cf118b96 to your computer and use it in GitHub Desktop.
Save jphastings/6d664eeea6a5d98b36316bc4cf118b96 to your computer and use it in GitHub Desktop.
Animation gif looper
package main
/*
Some really shonky code that looks for looping moments within a set of frames from animated TV shows.
Find out more here: https://twitter.com/jphastings/status/1096915446702489600
*/
import (
"fmt"
"github.com/Nr90/imgsim"
"github.com/ericpauley/go-quantize/quantize"
"image"
"image/color"
"image/draw"
"image/gif"
"image/png"
_ "image/png"
"log"
"math"
"os"
"path/filepath"
"sort"
)
const (
colours = 256
minFrames = 61
)
func main() {
fps := 23.976
images, err := filepath.Glob("*.png")
if err != nil {
log.Fatal(err)
}
sort.Strings(images)
loop, err := findLoop(images, minFrames)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%d frames; including %s to %s\n", len(loop), loop[0], loop[len(loop) - 1])
if err := createGif(loop, fps); err != nil {
log.Fatal(err)
}
}
func createGif(images []string, fps float64) error {
delay := int(math.Round(100 / fps))
f, err := os.Create("out.gif")
if err != nil {
return err
}
defer f.Close()
// newImage := resize.Resize(160, 0, original_image, resize.Lanczos3)
first, err := openPNG(images[0])
if err != nil {
return err
}
q := quantize.MedianCutQuantizer{
Aggregation: quantize.Mean,
}
pal := q.Quantize(make([]color.Color, 0, colours), first)
g := &gif.GIF{
LoopCount: 0,
}
for _, imagePath := range images {
img, err := openPNG(imagePath)
if err != nil {
return err
}
b := img.Bounds()
pm := image.NewPaletted(b, pal)
draw.FloydSteinberg.Draw(pm, b, img, image.ZP)
g.Image = append(g.Image, pm)
g.Delay = append(g.Delay, delay)
}
return gif.EncodeAll(f, g)
}
func findLoop(images []string, skipFrames int) ([]string, error) {
hashes := make(map[imgsim.Hash]int)
for i, imagePath := range images {
hash, err := hash(imagePath)
if err != nil {
log.Fatal(err)
}
prevI, ok := hashes[hash]
if ok {
if i - prevI >= skipFrames {
return images[prevI:i], nil
}
} else {
hashes[hash] = i
}
}
fmt.Println("no loop :( using all images")
return images, nil
}
func findLoopFromFirst(images []string, skipFrames int) ([]string, error) {
firstHash, err := hash(images[0])
if err != nil {
log.Fatal(err)
}
var diffs []int64
for i, img := range images[skipFrames:] {
h, _ := hash(img)
diff := diff(firstHash, h)
if diff == 0 {
// +1 because it's exclusive, and the range starts at index 1 of images
return images[0 : i+skipFrames], nil
}
diffs = append(diffs, diff)
}
fmt.Println("no exact match :(")
// +1 because it's exclusive, and diffs starts at index 1 of images
return images[0:minIndex(diffs) + minFrames + 1], nil
}
func minIndex(vals []int64) int {
idx := len(vals) - 1
min := vals[idx]
for i, val := range vals {
if val <= min {
idx = i
min = val
}
}
return idx
}
func hash(imagePath string) (imgsim.Hash, error) {
img, err := openPNG(imagePath)
if err != nil {
return 0, err
}
return imgsim.AverageHash(img), nil
}
func diff(master, copy imgsim.Hash) int64 {
diff := int64(master) - int64(copy)
if diff < 0 {
diff *= -1
}
return diff
}
func openPNG(imagePath string) (image.Image, error) {
f, err := os.Open(imagePath)
defer f.Close()
if err != nil {
return nil, err
}
img, err := png.Decode(f)
return img, err
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment