golang-image/draw/scale_test.go
Nigel Tao db892dd957 draw: clip scaling to the dst bounds.
This is necessary for the upcoming RGBA dst fast path. The RGBA.Set slow
path will clip automatically. Accessing RGBA.Pix directly will not.

Benchmarks look like noise to me:
benchmark                     old ns/op      new ns/op      delta
BenchmarkScaleLargeDownNN     6212108        6131166        -1.30%
BenchmarkScaleLargeDownAB     15586042       15656681       +0.45%
BenchmarkScaleLargeDownBL     1518783517     1508124217     -0.70%
BenchmarkScaleLargeDownCR     2998969089     2978114154     -0.70%
BenchmarkScaleDownNN          1821187        1809314        -0.65%
BenchmarkScaleDownAB          4286983        4248974        -0.89%
BenchmarkScaleDownBL          29396818       30181926       +2.67%
BenchmarkScaleDownCR          56441945       57952417       +2.68%
BenchmarkScaleUpNN            90325384       89734496       -0.65%
BenchmarkScaleUpAB            211613922      211625435      +0.01%
BenchmarkScaleUpBL            119730880      120817135      +0.91%
BenchmarkScaleUpCR            178592665      182305702      +2.08%
BenchmarkScaleSrcNRGBA        13271034       13210760       -0.45%
BenchmarkScaleSrcRGBA         13082234       12997551       -0.65%
BenchmarkScaleSrcUniform      4003966        3934184        -1.74%
BenchmarkScaleSrcYCbCr        15939182       15900123       -0.25%

Change-Id: Ibf2843bb3c4eb695b58030e7314053c669533016
Reviewed-on: https://go-review.googlesource.com/6073
Reviewed-by: Rob Pike <r@golang.org>
2015-02-26 22:56:31 +00:00

283 lines
8.7 KiB
Go

// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package draw
import (
"bytes"
"flag"
"fmt"
"image"
"image/color"
"image/png"
"math/rand"
"os"
"reflect"
"testing"
_ "image/jpeg"
)
var genScaleFiles = flag.Bool("gen_scale_files", false, "whether to generate the TestScaleXxx golden files.")
// testScale tests that scaling the source image gives the exact destination
// image. This is to ensure that any refactoring or optimization of the scaling
// code doesn't change the scaling behavior. Changing the actual algorithm or
// kernel used by any particular quality setting will obviously change the
// resultant pixels. In such a case, use the gen_scale_files flag to regenerate
// the golden files.
func testScale(t *testing.T, w int, h int, direction, srcFilename string) {
f, err := os.Open("../testdata/go-turns-two-" + srcFilename)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer f.Close()
src, _, err := image.Decode(f)
if err != nil {
t.Fatalf("Decode: %v", err)
}
testCases := map[string]Interpolator{
"nn": NearestNeighbor,
"ab": ApproxBiLinear,
"bl": BiLinear,
"cr": CatmullRom,
}
for name, q := range testCases {
gotFilename := fmt.Sprintf("../testdata/go-turns-two-%s-%s.png", direction, name)
got := image.NewRGBA(image.Rect(0, 0, w, h))
Scale(got, got.Bounds(), src, src.Bounds(), q)
if *genScaleFiles {
g, err := os.Create(gotFilename)
if err != nil {
t.Errorf("Create: %v", err)
continue
}
defer g.Close()
if err := png.Encode(g, got); err != nil {
t.Errorf("Encode: %v", err)
continue
}
continue
}
g, err := os.Open(gotFilename)
if err != nil {
t.Errorf("Open: %v", err)
continue
}
defer g.Close()
want, err := png.Decode(g)
if err != nil {
t.Errorf("Decode: %v", err)
continue
}
if !reflect.DeepEqual(got, want) {
t.Errorf("%s: actual image differs from golden image", gotFilename)
continue
}
}
}
func TestScaleDown(t *testing.T) { testScale(t, 100, 100, "down", "280x360.jpeg") }
func TestScaleUp(t *testing.T) { testScale(t, 75, 100, "up", "14x18.png") }
func fillPix(r *rand.Rand, pixs ...[]byte) {
for _, pix := range pixs {
for i := range pix {
pix[i] = uint8(r.Intn(256))
}
}
}
func TestScaleClipCommute(t *testing.T) {
src := image.NewNRGBA(image.Rect(0, 0, 20, 20))
fillPix(rand.New(rand.NewSource(0)), src.Pix)
outer := image.Rect(1, 1, 8, 5)
inner := image.Rect(2, 3, 6, 5)
qs := []Interpolator{
NearestNeighbor,
ApproxBiLinear,
CatmullRom,
}
for _, q := range qs {
dst0 := image.NewRGBA(image.Rect(1, 1, 10, 10))
dst1 := image.NewRGBA(image.Rect(1, 1, 10, 10))
for i := range dst0.Pix {
dst0.Pix[i] = uint8(i / 4)
dst1.Pix[i] = uint8(i / 4)
}
// Scale then clip.
Scale(dst0, outer, src, src.Bounds(), q)
dst0 = dst0.SubImage(inner).(*image.RGBA)
// Clip then scale.
dst1 = dst1.SubImage(inner).(*image.RGBA)
Scale(dst1, outer, src, src.Bounds(), q)
loop:
for y := inner.Min.Y; y < inner.Max.Y; y++ {
for x := inner.Min.X; x < inner.Max.X; x++ {
if c0, c1 := dst0.RGBAAt(x, y), dst1.RGBAAt(x, y); c0 != c1 {
t.Errorf("q=%T: at (%d, %d): c0=%v, c1=%v", q, x, y, c0, c1)
break loop
}
}
}
}
}
// The fooWrapper types wrap the dst or src image to avoid triggering the
// type-specific fast path implementations.
type (
dstWrapper struct{ Image }
srcWrapper struct{ image.Image }
)
// TestFastPaths tests that the fast path implementations produce identical
// results to the generic implementation.
func TestFastPaths(t *testing.T) {
drs := []image.Rectangle{
image.Rect(0, 0, 10, 10), // The dst bounds.
image.Rect(3, 4, 8, 6), // A strict subset of the dst bounds.
image.Rect(-3, -5, 2, 4), // Partial out-of-bounds #0.
image.Rect(4, -2, 6, 12), // Partial out-of-bounds #1.
image.Rect(12, 14, 23, 45), // Complete out-of-bounds.
image.Rect(5, 5, 5, 5), // Empty.
}
srs := []image.Rectangle{
image.Rect(0, 0, 12, 9), // The src bounds.
image.Rect(2, 2, 10, 8), // A strict subset of the src bounds.
image.Rect(10, 5, 20, 20), // Partial out-of-bounds #0.
image.Rect(-40, 0, 40, 8), // Partial out-of-bounds #1.
image.Rect(-8, -8, -4, -4), // Complete out-of-bounds.
image.Rect(5, 5, 5, 5), // Empty.
}
srcfs := []func(image.Rectangle) (image.Image, error){
srcNRGBA,
srcRGBA,
srcUniform,
srcYCbCr,
}
var srcs []image.Image
for _, srcf := range srcfs {
src, err := srcf(srs[0])
if err != nil {
t.Fatal(err)
}
srcs = append(srcs, src)
}
qs := []Interpolator{
NearestNeighbor,
ApproxBiLinear,
CatmullRom,
}
blue := image.NewUniform(color.RGBA{0x11, 0x22, 0x44, 0x7f})
for _, dr := range drs {
for _, src := range srcs {
for _, sr := range srs {
for _, q := range qs {
dst0 := image.NewRGBA(drs[0])
dst1 := image.NewRGBA(drs[0])
Draw(dst0, dst0.Bounds(), blue, image.Point{}, Src)
Draw(dstWrapper{dst1}, dst1.Bounds(), srcWrapper{blue}, image.Point{}, Src)
Scale(dst0, dr, src, sr, q)
Scale(dstWrapper{dst1}, dr, srcWrapper{src}, sr, q)
if !bytes.Equal(dst0.Pix, dst1.Pix) {
t.Errorf("pix differ for dr=%v, src=%T, sr=%v, q=%T", dr, src, sr, q)
}
}
}
}
}
}
func srcNRGBA(boundsHint image.Rectangle) (image.Image, error) {
m := image.NewNRGBA(boundsHint)
fillPix(rand.New(rand.NewSource(1)), m.Pix)
return m, nil
}
func srcRGBA(boundsHint image.Rectangle) (image.Image, error) {
m := image.NewRGBA(boundsHint)
fillPix(rand.New(rand.NewSource(2)), m.Pix)
// RGBA is alpha-premultiplied, so the R, G and B values should
// be <= the A values.
for i := 0; i < len(m.Pix); i += 4 {
m.Pix[i+0] = uint8(uint32(m.Pix[i+0]) * uint32(m.Pix[i+3]) / 0xff)
m.Pix[i+1] = uint8(uint32(m.Pix[i+1]) * uint32(m.Pix[i+3]) / 0xff)
m.Pix[i+2] = uint8(uint32(m.Pix[i+2]) * uint32(m.Pix[i+3]) / 0xff)
}
return m, nil
}
func srcUniform(boundsHint image.Rectangle) (image.Image, error) {
return image.NewUniform(color.RGBA64{0x1234, 0x5555, 0x9181, 0xbeef}), nil
}
func srcYCbCr(boundsHint image.Rectangle) (image.Image, error) {
m := image.NewYCbCr(boundsHint, image.YCbCrSubsampleRatio420)
fillPix(rand.New(rand.NewSource(3)), m.Y, m.Cb, m.Cr)
return m, nil
}
func srcYCbCrLarge(boundsHint image.Rectangle) (image.Image, error) {
// 3072 x 2304 is over 7 million pixels at 4:3, comparable to a
// 2015 smart-phone camera's output.
return srcYCbCr(image.Rect(0, 0, 3072, 2304))
}
func srcTux(boundsHint image.Rectangle) (image.Image, error) {
// tux.png is a 386 x 395 image.
f, err := os.Open("../testdata/tux.png")
if err != nil {
return nil, fmt.Errorf("Open: %v", err)
}
defer f.Close()
src, err := png.Decode(f)
if err != nil {
return nil, fmt.Errorf("Decode: %v", err)
}
return src, nil
}
func benchScale(b *testing.B, srcf func(image.Rectangle) (image.Image, error), w int, h int, q Interpolator) {
dst := image.NewRGBA(image.Rect(0, 0, w, h))
src, err := srcf(image.Rect(0, 0, 1024, 768))
if err != nil {
b.Fatal(err)
}
dr, sr := dst.Bounds(), src.Bounds()
scaler := q.NewScaler(int32(dr.Dx()), int32(dr.Dy()), int32(sr.Dx()), int32(sr.Dy()))
b.ResetTimer()
for i := 0; i < b.N; i++ {
scaler.Scale(dst, dr.Min, src, sr.Min)
}
}
func BenchmarkScaleLargeDownNN(b *testing.B) { benchScale(b, srcYCbCrLarge, 200, 150, NearestNeighbor) }
func BenchmarkScaleLargeDownAB(b *testing.B) { benchScale(b, srcYCbCrLarge, 200, 150, ApproxBiLinear) }
func BenchmarkScaleLargeDownBL(b *testing.B) { benchScale(b, srcYCbCrLarge, 200, 150, BiLinear) }
func BenchmarkScaleLargeDownCR(b *testing.B) { benchScale(b, srcYCbCrLarge, 200, 150, CatmullRom) }
func BenchmarkScaleDownNN(b *testing.B) { benchScale(b, srcTux, 120, 80, NearestNeighbor) }
func BenchmarkScaleDownAB(b *testing.B) { benchScale(b, srcTux, 120, 80, ApproxBiLinear) }
func BenchmarkScaleDownBL(b *testing.B) { benchScale(b, srcTux, 120, 80, BiLinear) }
func BenchmarkScaleDownCR(b *testing.B) { benchScale(b, srcTux, 120, 80, CatmullRom) }
func BenchmarkScaleUpNN(b *testing.B) { benchScale(b, srcTux, 800, 600, NearestNeighbor) }
func BenchmarkScaleUpAB(b *testing.B) { benchScale(b, srcTux, 800, 600, ApproxBiLinear) }
func BenchmarkScaleUpBL(b *testing.B) { benchScale(b, srcTux, 800, 600, BiLinear) }
func BenchmarkScaleUpCR(b *testing.B) { benchScale(b, srcTux, 800, 600, CatmullRom) }
func BenchmarkScaleSrcNRGBA(b *testing.B) { benchScale(b, srcNRGBA, 200, 150, ApproxBiLinear) }
func BenchmarkScaleSrcRGBA(b *testing.B) { benchScale(b, srcRGBA, 200, 150, ApproxBiLinear) }
func BenchmarkScaleSrcUniform(b *testing.B) { benchScale(b, srcUniform, 200, 150, ApproxBiLinear) }
func BenchmarkScaleSrcYCbCr(b *testing.B) { benchScale(b, srcYCbCr, 200, 150, ApproxBiLinear) }