Skip to content

Instantly share code, notes, and snippets.

@rhcarvalho
Created September 28, 2016 22:16
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 rhcarvalho/5e97f310701528f5a0610415e317b992 to your computer and use it in GitHub Desktop.
Save rhcarvalho/5e97f310701528f5a0610415e317b992 to your computer and use it in GitHub Desktop.
Compare image grayscale transformations in Go
  1. image/draw and GIFT use different formulas when converting images to grayscale:

    $ go test
    --- FAIL: TestToGray (0.00s)
            gray_test.go:25: colors differ: g1.At(96, 0) = color.Gray{Y:0x3c}, g2.At(96, 0) = color.Gray{Y:0x3b}
  2. GIFT performs much better:

    $ go test -bench=. -benchmem -run=-
    BenchmarkToGray/draw.Draw-4                 1000           1572947 ns/op           78256 B/op      30903 allocs/op
    BenchmarkToGray/gift.Draw-4                 5000            303784 ns/op           16881 B/op          9 allocs/op
    PASS
package main
import (
"bufio"
"fmt"
"image"
"image/draw"
_ "image/gif"
_ "image/jpeg"
"image/png"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"sync"
"github.com/disintegration/gift"
)
// out is a temporary directory where showImage saves images.
var out string
// openImage loads an image from disk.
func openImage(path string) (image.Image, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
m, _, err := image.Decode(f)
return m, err
}
// showImage saves the image m with name in a temporary directory and opens it
// with eog (Eye of GNOME). Yes, very system-dependent.
func showImage(name string, m image.Image) error {
f, err := os.Create(filepath.Join(out, name))
if err != nil {
return err
}
fname := f.Name()
log.Printf("created %v", fname)
defer func() {
os.Remove(fname)
log.Printf("removed %v", fname)
}()
w := bufio.NewWriter(f)
if err := png.Encode(w, m); err != nil {
return err
}
w.Flush()
return exec.Command("eog", "-n", fname).Run()
}
// toGray converts m to grayscale using image/draw.
func toGray(m image.Image) image.Image {
gray := image.NewGray(m.Bounds())
draw.Draw(gray, m.Bounds(), m, image.ZP, draw.Src)
return gray
}
// toGrayGift converts m to grayscale using GIFT.
func toGrayGift(m image.Image) image.Image {
gray := image.NewGray(m.Bounds())
gift.Grayscale().Draw(gray, m, nil)
return gray
}
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "usage: %v IMAGE\n", filepath.Base(os.Args[0]))
os.Exit(1)
}
m, err := openImage(os.Args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "could not open image: %v\n", err)
os.Exit(2)
}
out, err = ioutil.TempDir("", "nonogrammer-output-")
if err != nil {
fmt.Fprintf(os.Stderr, "could not create temporary output directory: %v\n", err)
os.Exit(3)
}
defer os.RemoveAll(out)
var wg sync.WaitGroup
show := func(name string, m image.Image) {
wg.Add(1)
go func() {
defer wg.Done()
if err := showImage(name, m); err != nil {
log.Print(err)
}
}()
}
show("original", m)
show("toGray", toGray(m))
show("toGrayGift", toGrayGift(m))
wg.Wait()
}
package main
import (
"encoding/base64"
"image"
"reflect"
"strings"
"testing"
)
func TestToGray(t *testing.T) {
m := loadImage()
g1 := toGray(m)
g2 := toGrayGift(m)
if g1.ColorModel() != g2.ColorModel() {
t.Fatalf("color model differ: g1.ColorModel() = %#v, g2.ColorModel() = %#v", g1.ColorModel(), g2.ColorModel())
}
if g1.Bounds() != g2.Bounds() {
t.Fatalf("bounds differ: g1.Bounds() = %#v, g2.Bounds() = %#v", g1.Bounds(), g2.Bounds())
}
b := g1.Bounds()
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
if !reflect.DeepEqual(g1.At(x, y), g2.At(x, y)) {
t.Fatalf("colors differ: g1.At(%d, %d) = %#v, g2.At(%[1]d, %d) = %#[4]v", x, y, g1.At(x, y), g2.At(x, y))
}
}
}
if !reflect.DeepEqual(g1, g2) {
t.Fatalf("g1 != g2")
}
}
func BenchmarkToGray(b *testing.B) {
m := loadImage()
b.Run("draw.Draw", func(b *testing.B) {
for i := 0; i < b.N; i++ {
toGray(m)
}
})
b.Run("gift.Draw", func(b *testing.B) {
for i := 0; i < b.N; i++ {
toGrayGift(m)
}
})
}
func loadImage() image.Image {
r := base64.NewDecoder(base64.StdEncoding, strings.NewReader(data))
m, _, err := image.Decode(r)
if err != nil {
panic(err)
}
return m
}
const data = `
/9j/4AAQSkZJRgABAQIAHAAcAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdA
SFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2Nj
Y2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCABnAJYDASIAAhEBAxEB/8QA
HwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIh
MUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVW
V1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXG
x8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQF
BgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAV
YnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE
hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq
8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDlwKMD0pwzSiuK57QzGDxS7D6in8Y5ximnAPUfSlcq4m3ilUYp
2OKXHvRcVxnTtS7c07HNFK4DQPakC4PNOA+tOx70XAjK/So5gBGP94fzqfvUVx/qxx/EP51UXqRP4WSE
cmgjilP3jSEZqS0IO/NGDnpUiocDg/McDjvV6HTPOdVWYgsM5KcfzzQ2JySM2jp6VYu7SWzmMUwG4cgj
kMPUVBjjtTGtRu0Zopw+lFFxhinrGzuqqMsxAA9yaXFSRv5cqSEcIwYj6GpuZ30O30fSLKzhUpbpNMv3
5XGTn29BV28jt7pPLuIVljPBBFVreYx+VbqAjycgt3x14zRcNOxGyVFHQkIc/wA61exyKLbuzjdZ046d
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
`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment