diff options
Diffstat (limited to 'Godeps/_workspace/src/code.google.com/p/graphics-go/graphics')
31 files changed, 2726 insertions, 0 deletions
diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/Makefile b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/Makefile new file mode 100644 index 000000000..28a06f0e8 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/Makefile @@ -0,0 +1,15 @@ +# Copyright 2011 The Graphics-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. + +include $(GOROOT)/src/Make.inc + +TARG=code.google.com/p/graphics-go/graphics +GOFILES=\ + affine.go\ + blur.go\ + rotate.go\ + scale.go\ + thumbnail.go\ + +include $(GOROOT)/src/Make.pkg diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/affine.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/affine.go new file mode 100644 index 000000000..0ac2ec9da --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/affine.go @@ -0,0 +1,174 @@ +// Copyright 2011 The Graphics-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 graphics + +import ( + "code.google.com/p/graphics-go/graphics/interp" + "errors" + "image" + "image/draw" + "math" +) + +// I is the identity Affine transform matrix. +var I = Affine{ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1, +} + +// Affine is a 3x3 2D affine transform matrix. +// M(i,j) is Affine[i*3+j]. +type Affine [9]float64 + +// Mul returns the multiplication of two affine transform matrices. +func (a Affine) Mul(b Affine) Affine { + return Affine{ + a[0]*b[0] + a[1]*b[3] + a[2]*b[6], + a[0]*b[1] + a[1]*b[4] + a[2]*b[7], + a[0]*b[2] + a[1]*b[5] + a[2]*b[8], + a[3]*b[0] + a[4]*b[3] + a[5]*b[6], + a[3]*b[1] + a[4]*b[4] + a[5]*b[7], + a[3]*b[2] + a[4]*b[5] + a[5]*b[8], + a[6]*b[0] + a[7]*b[3] + a[8]*b[6], + a[6]*b[1] + a[7]*b[4] + a[8]*b[7], + a[6]*b[2] + a[7]*b[5] + a[8]*b[8], + } +} + +func (a Affine) transformRGBA(dst *image.RGBA, src *image.RGBA, i interp.RGBA) error { + srcb := src.Bounds() + b := dst.Bounds() + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + sx, sy := a.pt(x, y) + if inBounds(srcb, sx, sy) { + c := i.RGBA(src, sx, sy) + off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4 + dst.Pix[off+0] = c.R + dst.Pix[off+1] = c.G + dst.Pix[off+2] = c.B + dst.Pix[off+3] = c.A + } + } + } + return nil +} + +// Transform applies the affine transform to src and produces dst. +func (a Affine) Transform(dst draw.Image, src image.Image, i interp.Interp) error { + if dst == nil { + return errors.New("graphics: dst is nil") + } + if src == nil { + return errors.New("graphics: src is nil") + } + + // RGBA fast path. + dstRGBA, dstOk := dst.(*image.RGBA) + srcRGBA, srcOk := src.(*image.RGBA) + interpRGBA, interpOk := i.(interp.RGBA) + if dstOk && srcOk && interpOk { + return a.transformRGBA(dstRGBA, srcRGBA, interpRGBA) + } + + srcb := src.Bounds() + b := dst.Bounds() + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + sx, sy := a.pt(x, y) + if inBounds(srcb, sx, sy) { + dst.Set(x, y, i.Interp(src, sx, sy)) + } + } + } + return nil +} + +func inBounds(b image.Rectangle, x, y float64) bool { + if x < float64(b.Min.X) || x >= float64(b.Max.X) { + return false + } + if y < float64(b.Min.Y) || y >= float64(b.Max.Y) { + return false + } + return true +} + +func (a Affine) pt(x0, y0 int) (x1, y1 float64) { + fx := float64(x0) + 0.5 + fy := float64(y0) + 0.5 + x1 = fx*a[0] + fy*a[1] + a[2] + y1 = fx*a[3] + fy*a[4] + a[5] + return x1, y1 +} + +// TransformCenter applies the affine transform to src and produces dst. +// Equivalent to +// a.CenterFit(dst, src).Transform(dst, src, i). +func (a Affine) TransformCenter(dst draw.Image, src image.Image, i interp.Interp) error { + if dst == nil { + return errors.New("graphics: dst is nil") + } + if src == nil { + return errors.New("graphics: src is nil") + } + + return a.CenterFit(dst.Bounds(), src.Bounds()).Transform(dst, src, i) +} + +// Scale produces a scaling transform of factors x and y. +func (a Affine) Scale(x, y float64) Affine { + return a.Mul(Affine{ + 1 / x, 0, 0, + 0, 1 / y, 0, + 0, 0, 1, + }) +} + +// Rotate produces a clockwise rotation transform of angle, in radians. +func (a Affine) Rotate(angle float64) Affine { + s, c := math.Sincos(angle) + return a.Mul(Affine{ + +c, +s, +0, + -s, +c, +0, + +0, +0, +1, + }) +} + +// Shear produces a shear transform by the slopes x and y. +func (a Affine) Shear(x, y float64) Affine { + d := 1 - x*y + return a.Mul(Affine{ + +1 / d, -x / d, 0, + -y / d, +1 / d, 0, + 0, 0, 1, + }) +} + +// Translate produces a translation transform with pixel distances x and y. +func (a Affine) Translate(x, y float64) Affine { + return a.Mul(Affine{ + 1, 0, -x, + 0, 1, -y, + 0, 0, +1, + }) +} + +// Center produces the affine transform, centered around the provided point. +func (a Affine) Center(x, y float64) Affine { + return I.Translate(-x, -y).Mul(a).Translate(x, y) +} + +// CenterFit produces the affine transform, centered around the rectangles. +// It is equivalent to +// I.Translate(-<center of src>).Mul(a).Translate(<center of dst>) +func (a Affine) CenterFit(dst, src image.Rectangle) Affine { + dx := float64(dst.Min.X) + float64(dst.Dx())/2 + dy := float64(dst.Min.Y) + float64(dst.Dy())/2 + sx := float64(src.Min.X) + float64(src.Dx())/2 + sy := float64(src.Min.Y) + float64(src.Dy())/2 + return I.Translate(-sx, -sy).Mul(a).Translate(dx, dy) +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur.go new file mode 100644 index 000000000..9a54d5ad5 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur.go @@ -0,0 +1,68 @@ +// Copyright 2011 The Graphics-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 graphics + +import ( + "code.google.com/p/graphics-go/graphics/convolve" + "errors" + "image" + "image/draw" + "math" +) + +// DefaultStdDev is the default blurring parameter. +var DefaultStdDev = 0.5 + +// BlurOptions are the blurring parameters. +// StdDev is the standard deviation of the normal, higher is blurrier. +// Size is the size of the kernel. If zero, it is set to Ceil(6 * StdDev). +type BlurOptions struct { + StdDev float64 + Size int +} + +// Blur produces a blurred version of the image, using a Gaussian blur. +func Blur(dst draw.Image, src image.Image, opt *BlurOptions) error { + if dst == nil { + return errors.New("graphics: dst is nil") + } + if src == nil { + return errors.New("graphics: src is nil") + } + + sd := DefaultStdDev + size := 0 + + if opt != nil { + sd = opt.StdDev + size = opt.Size + } + + if size < 1 { + size = int(math.Ceil(sd * 6)) + } + + kernel := make([]float64, 2*size+1) + for i := 0; i <= size; i++ { + x := float64(i) / sd + x = math.Pow(1/math.SqrtE, x*x) + kernel[size-i] = x + kernel[size+i] = x + } + + // Normalize the weights to sum to 1.0. + kSum := 0.0 + for _, k := range kernel { + kSum += k + } + for i, k := range kernel { + kernel[i] = k / kSum + } + + return convolve.Convolve(dst, src, &convolve.SeparableKernel{ + X: kernel, + Y: kernel, + }) +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur_test.go new file mode 100644 index 000000000..1d84fa604 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur_test.go @@ -0,0 +1,207 @@ +// Copyright 2011 The Graphics-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 graphics + +import ( + "code.google.com/p/graphics-go/graphics/graphicstest" + "image" + "image/color" + "testing" + + _ "image/png" +) + +var blurOneColorTests = []transformOneColorTest{ + { + "1x1-blank", 1, 1, 1, 1, + &BlurOptions{0.83, 1}, + []uint8{0xff}, + []uint8{0xff}, + }, + { + "1x1-spreadblank", 1, 1, 1, 1, + &BlurOptions{0.83, 2}, + []uint8{0xff}, + []uint8{0xff}, + }, + { + "3x3-blank", 3, 3, 3, 3, + &BlurOptions{0.83, 2}, + []uint8{ + 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, + }, + []uint8{ + 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, + }, + }, + { + "3x3-dot", 3, 3, 3, 3, + &BlurOptions{0.34, 1}, + []uint8{ + 0x00, 0x00, 0x00, + 0x00, 0xff, 0x00, + 0x00, 0x00, 0x00, + }, + []uint8{ + 0x00, 0x03, 0x00, + 0x03, 0xf2, 0x03, + 0x00, 0x03, 0x00, + }, + }, + { + "5x5-dot", 5, 5, 5, 5, + &BlurOptions{0.34, 1}, + []uint8{ + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + }, + []uint8{ + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x03, 0xf2, 0x03, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + }, + }, + { + "5x5-dot-spread", 5, 5, 5, 5, + &BlurOptions{0.85, 1}, + []uint8{ + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + }, + []uint8{ + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x20, 0x10, 0x00, + 0x00, 0x20, 0x40, 0x20, 0x00, + 0x00, 0x10, 0x20, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + }, + }, + { + "4x4-box", 4, 4, 4, 4, + &BlurOptions{0.34, 1}, + []uint8{ + 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0xff, 0x00, + 0x00, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x00, 0x00, + }, + []uint8{ + 0x00, 0x03, 0x03, 0x00, + 0x03, 0xf8, 0xf8, 0x03, + 0x03, 0xf8, 0xf8, 0x03, + 0x00, 0x03, 0x03, 0x00, + }, + }, + { + "5x5-twodots", 5, 5, 5, 5, + &BlurOptions{0.34, 1}, + []uint8{ + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x96, 0x00, 0x96, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + }, + []uint8{ + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x02, 0x00, + 0x02, 0x8e, 0x04, 0x8e, 0x02, + 0x00, 0x02, 0x00, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + }, + }, +} + +func TestBlurOneColor(t *testing.T) { + for _, oc := range blurOneColorTests { + dst := oc.newDst() + src := oc.newSrc() + opt := oc.opt.(*BlurOptions) + if err := Blur(dst, src, opt); err != nil { + t.Fatal(err) + } + + if !checkTransformTest(t, &oc, dst) { + continue + } + } +} + +func TestBlurEmpty(t *testing.T) { + empty := image.NewRGBA(image.Rect(0, 0, 0, 0)) + if err := Blur(empty, empty, nil); err != nil { + t.Fatal(err) + } +} + +func TestBlurGopher(t *testing.T) { + src, err := graphicstest.LoadImage("../testdata/gopher.png") + if err != nil { + t.Fatal(err) + } + + dst := image.NewRGBA(src.Bounds()) + if err = Blur(dst, src, &BlurOptions{StdDev: 1.1}); err != nil { + t.Fatal(err) + } + + cmp, err := graphicstest.LoadImage("../testdata/gopher-blur.png") + if err != nil { + t.Fatal(err) + } + err = graphicstest.ImageWithinTolerance(dst, cmp, 0x101) + if err != nil { + t.Fatal(err) + } +} + +func benchBlur(b *testing.B, bounds image.Rectangle) { + b.StopTimer() + + // Construct a fuzzy image. + src := image.NewRGBA(bounds) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + src.SetRGBA(x, y, color.RGBA{ + uint8(5 * x % 0x100), + uint8(7 * y % 0x100), + uint8((7*x + 5*y) % 0x100), + 0xff, + }) + } + } + dst := image.NewRGBA(bounds) + + b.StartTimer() + for i := 0; i < b.N; i++ { + Blur(dst, src, &BlurOptions{0.84, 3}) + } +} + +func BenchmarkBlur400x400x3(b *testing.B) { + benchBlur(b, image.Rect(0, 0, 400, 400)) +} + +// Exactly twice the pixel count of 400x400. +func BenchmarkBlur400x800x3(b *testing.B) { + benchBlur(b, image.Rect(0, 0, 400, 800)) +} + +// Exactly twice the pixel count of 400x800 +func BenchmarkBlur400x1600x3(b *testing.B) { + benchBlur(b, image.Rect(0, 0, 400, 1600)) +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/Makefile b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/Makefile new file mode 100644 index 000000000..a5691fa30 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/Makefile @@ -0,0 +1,11 @@ +# Copyright 2011 The Graphics-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. + +include $(GOROOT)/src/Make.inc + +TARG=code.google.com/p/graphics-go/graphics/convolve +GOFILES=\ + convolve.go\ + +include $(GOROOT)/src/Make.pkg diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve.go new file mode 100644 index 000000000..da69496d0 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve.go @@ -0,0 +1,274 @@ +// Copyright 2011 The Graphics-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 convolve + +import ( + "errors" + "fmt" + "image" + "image/draw" + "math" +) + +// clamp clamps x to the range [x0, x1]. +func clamp(x, x0, x1 float64) float64 { + if x < x0 { + return x0 + } + if x > x1 { + return x1 + } + return x +} + +// Kernel is a square matrix that defines a convolution. +type Kernel interface { + // Weights returns the square matrix of weights in row major order. + Weights() []float64 +} + +// SeparableKernel is a linearly separable, square convolution kernel. +// X and Y are the per-axis weights. Each slice must be the same length, and +// have an odd length. The middle element of each slice is the weight for the +// central pixel. For example, the horizontal Sobel kernel is: +// sobelX := &SeparableKernel{ +// X: []float64{-1, 0, +1}, +// Y: []float64{1, 2, 1}, +// } +type SeparableKernel struct { + X, Y []float64 +} + +func (k *SeparableKernel) Weights() []float64 { + n := len(k.X) + w := make([]float64, n*n) + for y := 0; y < n; y++ { + for x := 0; x < n; x++ { + w[y*n+x] = k.X[x] * k.Y[y] + } + } + return w +} + +// fullKernel is a square convolution kernel. +type fullKernel []float64 + +func (k fullKernel) Weights() []float64 { return k } + +func kernelSize(w []float64) (size int, err error) { + size = int(math.Sqrt(float64(len(w)))) + if size*size != len(w) { + return 0, errors.New("graphics: kernel is not square") + } + if size%2 != 1 { + return 0, errors.New("graphics: kernel size is not odd") + } + return size, nil +} + +// NewKernel returns a square convolution kernel. +func NewKernel(w []float64) (Kernel, error) { + if _, err := kernelSize(w); err != nil { + return nil, err + } + return fullKernel(w), nil +} + +func convolveRGBASep(dst *image.RGBA, src image.Image, k *SeparableKernel) error { + if len(k.X) != len(k.Y) { + return fmt.Errorf("graphics: kernel not square (x %d, y %d)", len(k.X), len(k.Y)) + } + if len(k.X)%2 != 1 { + return fmt.Errorf("graphics: kernel length (%d) not odd", len(k.X)) + } + radius := (len(k.X) - 1) / 2 + + // buf holds the result of vertically blurring src. + bounds := dst.Bounds() + width, height := bounds.Dx(), bounds.Dy() + buf := make([]float64, width*height*4) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + var r, g, b, a float64 + // k0 is the kernel weight for the center pixel. This may be greater + // than kernel[0], near the boundary of the source image, to avoid + // vignetting. + k0 := k.X[radius] + + // Add the pixels from above. + for i := 1; i <= radius; i++ { + f := k.Y[radius-i] + if y-i < bounds.Min.Y { + k0 += f + } else { + or, og, ob, oa := src.At(x, y-i).RGBA() + r += float64(or>>8) * f + g += float64(og>>8) * f + b += float64(ob>>8) * f + a += float64(oa>>8) * f + } + } + + // Add the pixels from below. + for i := 1; i <= radius; i++ { + f := k.Y[radius+i] + if y+i >= bounds.Max.Y { + k0 += f + } else { + or, og, ob, oa := src.At(x, y+i).RGBA() + r += float64(or>>8) * f + g += float64(og>>8) * f + b += float64(ob>>8) * f + a += float64(oa>>8) * f + } + } + + // Add the central pixel. + or, og, ob, oa := src.At(x, y).RGBA() + r += float64(or>>8) * k0 + g += float64(og>>8) * k0 + b += float64(ob>>8) * k0 + a += float64(oa>>8) * k0 + + // Write to buf. + o := (y-bounds.Min.Y)*width*4 + (x-bounds.Min.X)*4 + buf[o+0] = r + buf[o+1] = g + buf[o+2] = b + buf[o+3] = a + } + } + + // dst holds the result of horizontally blurring buf. + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + var r, g, b, a float64 + k0, off := k.X[radius], y*width*4+x*4 + + // Add the pixels from the left. + for i := 1; i <= radius; i++ { + f := k.X[radius-i] + if x-i < 0 { + k0 += f + } else { + o := off - i*4 + r += buf[o+0] * f + g += buf[o+1] * f + b += buf[o+2] * f + a += buf[o+3] * f + } + } + + // Add the pixels from the right. + for i := 1; i <= radius; i++ { + f := k.X[radius+i] + if x+i >= width { + k0 += f + } else { + o := off + i*4 + r += buf[o+0] * f + g += buf[o+1] * f + b += buf[o+2] * f + a += buf[o+3] * f + } + } + + // Add the central pixel. + r += buf[off+0] * k0 + g += buf[off+1] * k0 + b += buf[off+2] * k0 + a += buf[off+3] * k0 + + // Write to dst, clamping to the range [0, 255]. + dstOff := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4 + dst.Pix[dstOff+0] = uint8(clamp(r+0.5, 0, 255)) + dst.Pix[dstOff+1] = uint8(clamp(g+0.5, 0, 255)) + dst.Pix[dstOff+2] = uint8(clamp(b+0.5, 0, 255)) + dst.Pix[dstOff+3] = uint8(clamp(a+0.5, 0, 255)) + } + } + + return nil +} + +func convolveRGBA(dst *image.RGBA, src image.Image, k Kernel) error { + b := dst.Bounds() + bs := src.Bounds() + w := k.Weights() + size, err := kernelSize(w) + if err != nil { + return err + } + radius := (size - 1) / 2 + + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + if !image.Pt(x, y).In(bs) { + continue + } + + var r, g, b, a, adj float64 + for cy := y - radius; cy <= y+radius; cy++ { + for cx := x - radius; cx <= x+radius; cx++ { + factor := w[(cy-y+radius)*size+cx-x+radius] + if !image.Pt(cx, cy).In(bs) { + adj += factor + } else { + sr, sg, sb, sa := src.At(cx, cy).RGBA() + r += float64(sr>>8) * factor + g += float64(sg>>8) * factor + b += float64(sb>>8) * factor + a += float64(sa>>8) * factor + } + } + } + + if adj != 0 { + sr, sg, sb, sa := src.At(x, y).RGBA() + r += float64(sr>>8) * adj + g += float64(sg>>8) * adj + b += float64(sb>>8) * adj + a += float64(sa>>8) * adj + } + + off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4 + dst.Pix[off+0] = uint8(clamp(r+0.5, 0, 0xff)) + dst.Pix[off+1] = uint8(clamp(g+0.5, 0, 0xff)) + dst.Pix[off+2] = uint8(clamp(b+0.5, 0, 0xff)) + dst.Pix[off+3] = uint8(clamp(a+0.5, 0, 0xff)) + } + } + + return nil +} + +// Convolve produces dst by applying the convolution kernel k to src. +func Convolve(dst draw.Image, src image.Image, k Kernel) (err error) { + if dst == nil || src == nil || k == nil { + return nil + } + + b := dst.Bounds() + dstRgba, ok := dst.(*image.RGBA) + if !ok { + dstRgba = image.NewRGBA(b) + } + + switch k := k.(type) { + case *SeparableKernel: + err = convolveRGBASep(dstRgba, src, k) + default: + err = convolveRGBA(dstRgba, src, k) + } + + if err != nil { + return err + } + + if !ok { + draw.Draw(dst, b, dstRgba, b.Min, draw.Src) + } + return nil +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve_test.go new file mode 100644 index 000000000..f34d7afc8 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve_test.go @@ -0,0 +1,78 @@ +// Copyright 2011 The Graphics-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 convolve + +import ( + "code.google.com/p/graphics-go/graphics/graphicstest" + "image" + "reflect" + "testing" + + _ "image/png" +) + +func TestSeparableWeights(t *testing.T) { + sobelXFull := []float64{ + -1, 0, 1, + -2, 0, 2, + -1, 0, 1, + } + sobelXSep := &SeparableKernel{ + X: []float64{-1, 0, +1}, + Y: []float64{1, 2, 1}, + } + w := sobelXSep.Weights() + if !reflect.DeepEqual(w, sobelXFull) { + t.Errorf("got %v want %v", w, sobelXFull) + } +} + +func TestConvolve(t *testing.T) { + kernFull, err := NewKernel([]float64{ + 0, 0, 0, + 1, 1, 1, + 0, 0, 0, + }) + if err != nil { + t.Fatal(err) + } + + kernSep := &SeparableKernel{ + X: []float64{1, 1, 1}, + Y: []float64{0, 1, 0}, + } + + src, err := graphicstest.LoadImage("../../testdata/gopher.png") + if err != nil { + t.Fatal(err) + } + b := src.Bounds() + + sep := image.NewRGBA(b) + if err = Convolve(sep, src, kernSep); err != nil { + t.Fatal(err) + } + + full := image.NewRGBA(b) + Convolve(full, src, kernFull) + + err = graphicstest.ImageWithinTolerance(sep, full, 0x101) + if err != nil { + t.Fatal(err) + } +} + +func TestConvolveNil(t *testing.T) { + if err := Convolve(nil, nil, nil); err != nil { + t.Fatal(err) + } +} + +func TestConvolveEmpty(t *testing.T) { + empty := image.NewRGBA(image.Rect(0, 0, 0, 0)) + if err := Convolve(empty, empty, nil); err != nil { + t.Fatal(err) + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/Makefile b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/Makefile new file mode 100644 index 000000000..0b1c6cb3e --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/Makefile @@ -0,0 +1,15 @@ +# Copyright 2011 The Graphics-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. + +include $(GOROOT)/src/Make.inc + +TARG=code.google.com/p/graphics-go/graphics +GOFILES=\ + detect.go\ + doc.go\ + integral.go\ + opencv_parser.go\ + projector.go\ + +include $(GOROOT)/src/Make.pkg diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect.go new file mode 100644 index 000000000..dde941cbe --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect.go @@ -0,0 +1,133 @@ +// Copyright 2011 The Graphics-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 detect + +import ( + "image" + "math" +) + +// Feature is a Haar-like feature. +type Feature struct { + Rect image.Rectangle + Weight float64 +} + +// Classifier is a set of features with a threshold. +type Classifier struct { + Feature []Feature + Threshold float64 + Left float64 + Right float64 +} + +// CascadeStage is a cascade of classifiers. +type CascadeStage struct { + Classifier []Classifier + Threshold float64 +} + +// Cascade is a degenerate tree of Haar-like classifiers. +type Cascade struct { + Stage []CascadeStage + Size image.Point +} + +// Match returns true if the full image is classified as an object. +func (c *Cascade) Match(m image.Image) bool { + return c.classify(newWindow(m)) +} + +// Find returns a set of areas of m that match the feature cascade c. +func (c *Cascade) Find(m image.Image) []image.Rectangle { + // TODO(crawshaw): Consider de-duping strategies. + matches := []image.Rectangle{} + w := newWindow(m) + + b := m.Bounds() + origScale := c.Size + for s := origScale; s.X < b.Dx() && s.Y < b.Dy(); s = s.Add(s.Div(10)) { + // translate region and classify + tx := image.Pt(s.X/10, 0) + ty := image.Pt(0, s.Y/10) + for r := image.Rect(0, 0, s.X, s.Y).Add(b.Min); r.In(b); r = r.Add(ty) { + for r1 := r; r1.In(b); r1 = r1.Add(tx) { + if c.classify(w.subWindow(r1)) { + matches = append(matches, r1) + } + } + } + } + return matches +} + +type window struct { + mi *integral + miSq *integral + rect image.Rectangle + invArea float64 + stdDev float64 +} + +func (w *window) init() { + w.invArea = 1 / float64(w.rect.Dx()*w.rect.Dy()) + mean := float64(w.mi.sum(w.rect)) * w.invArea + vr := float64(w.miSq.sum(w.rect))*w.invArea - mean*mean + if vr < 0 { + vr = 1 + } + w.stdDev = math.Sqrt(vr) +} + +func newWindow(m image.Image) *window { + mi, miSq := newIntegrals(m) + res := &window{ + mi: mi, + miSq: miSq, + rect: m.Bounds(), + } + res.init() + return res +} + +func (w *window) subWindow(r image.Rectangle) *window { + res := &window{ + mi: w.mi, + miSq: w.miSq, + rect: r, + } + res.init() + return res +} + +func (c *Classifier) classify(w *window, pr *projector) float64 { + s := 0.0 + for _, f := range c.Feature { + s += float64(w.mi.sum(pr.rect(f.Rect))) * f.Weight + } + s *= w.invArea // normalize to maintain scale invariance + if s < c.Threshold*w.stdDev { + return c.Left + } + return c.Right +} + +func (s *CascadeStage) classify(w *window, pr *projector) bool { + sum := 0.0 + for _, c := range s.Classifier { + sum += c.classify(w, pr) + } + return sum >= s.Threshold +} + +func (c *Cascade) classify(w *window) bool { + pr := newProjector(w.rect, image.Rectangle{image.Pt(0, 0), c.Size}) + for _, s := range c.Stage { + if !s.classify(w, pr) { + return false + } + } + return true +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect_test.go new file mode 100644 index 000000000..8a2df113d --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect_test.go @@ -0,0 +1,77 @@ +// Copyright 2011 The Graphics-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 detect + +import ( + "image" + "image/draw" + "testing" +) + +var ( + c0 = Classifier{ + Feature: []Feature{ + Feature{Rect: image.Rect(0, 0, 3, 4), Weight: 4.0}, + }, + Threshold: 0.2, + Left: 0.8, + Right: 0.2, + } + c1 = Classifier{ + Feature: []Feature{ + Feature{Rect: image.Rect(3, 4, 4, 5), Weight: 4.0}, + }, + Threshold: 0.2, + Left: 0.8, + Right: 0.2, + } + c2 = Classifier{ + Feature: []Feature{ + Feature{Rect: image.Rect(0, 0, 1, 1), Weight: +4.0}, + Feature{Rect: image.Rect(0, 0, 2, 2), Weight: -1.0}, + }, + Threshold: 0.2, + Left: 0.8, + Right: 0.2, + } +) + +func TestClassifier(t *testing.T) { + m := image.NewGray(image.Rect(0, 0, 20, 20)) + b := m.Bounds() + draw.Draw(m, image.Rect(0, 0, 20, 20), image.White, image.ZP, draw.Src) + draw.Draw(m, image.Rect(3, 4, 4, 5), image.Black, image.ZP, draw.Src) + w := newWindow(m) + pr := newProjector(b, b) + + if res := c0.classify(w, pr); res != c0.Right { + t.Errorf("c0 got %f want %f", res, c0.Right) + } + if res := c1.classify(w, pr); res != c1.Left { + t.Errorf("c1 got %f want %f", res, c1.Left) + } + if res := c2.classify(w, pr); res != c1.Left { + t.Errorf("c2 got %f want %f", res, c1.Left) + } +} + +func TestClassifierScale(t *testing.T) { + m := image.NewGray(image.Rect(0, 0, 50, 50)) + b := m.Bounds() + draw.Draw(m, image.Rect(0, 0, 8, 10), image.White, b.Min, draw.Src) + draw.Draw(m, image.Rect(8, 10, 10, 13), image.Black, b.Min, draw.Src) + w := newWindow(m) + pr := newProjector(b, image.Rect(0, 0, 20, 20)) + + if res := c0.classify(w, pr); res != c0.Right { + t.Errorf("scaled c0 got %f want %f", res, c0.Right) + } + if res := c1.classify(w, pr); res != c1.Left { + t.Errorf("scaled c1 got %f want %f", res, c1.Left) + } + if res := c2.classify(w, pr); res != c1.Left { + t.Errorf("scaled c2 got %f want %f", res, c1.Left) + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/doc.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/doc.go new file mode 100644 index 000000000..a0f4e94cd --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/doc.go @@ -0,0 +1,31 @@ +// Copyright 2011 The Graphics-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 detect implements an object detector cascade. + +The technique used is a degenerate tree of Haar-like classifiers, commonly +used for face detection. It is described in + + P. Viola, M. Jones. + Rapid Object Detection using a Boosted Cascade of Simple Features, 2001 + IEEE Conference on Computer Vision and Pattern Recognition + +A Cascade can be constructed manually from a set of Classifiers in stages, +or can be loaded from an XML file in the OpenCV format with + + classifier, _, err := detect.ParseOpenCV(r) + +The classifier can be used to determine if a full image is detected as an +object using Detect + + if classifier.Match(m) { + // m is an image of a face. + } + +It is also possible to search an image for occurrences of an object + + objs := classifier.Find(m) +*/ +package detect diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral.go new file mode 100644 index 000000000..814ced590 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral.go @@ -0,0 +1,93 @@ +// Copyright 2011 The Graphics-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 detect + +import ( + "image" + "image/draw" +) + +// integral is an image.Image-like structure that stores the cumulative +// sum of the preceding pixels. This allows for O(1) summation of any +// rectangular region within the image. +type integral struct { + // pix holds the cumulative sum of the image's pixels. The pixel at + // (x, y) starts at pix[(y-rect.Min.Y)*stride + (x-rect.Min.X)*1]. + pix []uint64 + stride int + rect image.Rectangle +} + +func (p *integral) at(x, y int) uint64 { + return p.pix[(y-p.rect.Min.Y)*p.stride+(x-p.rect.Min.X)] +} + +func (p *integral) sum(b image.Rectangle) uint64 { + c := p.at(b.Max.X-1, b.Max.Y-1) + inY := b.Min.Y > p.rect.Min.Y + inX := b.Min.X > p.rect.Min.X + if inY && inX { + c += p.at(b.Min.X-1, b.Min.Y-1) + } + if inY { + c -= p.at(b.Max.X-1, b.Min.Y-1) + } + if inX { + c -= p.at(b.Min.X-1, b.Max.Y-1) + } + return c +} + +func (m *integral) integrate() { + b := m.rect + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + c := uint64(0) + if y > b.Min.Y && x > b.Min.X { + c += m.at(x-1, y) + c += m.at(x, y-1) + c -= m.at(x-1, y-1) + } else if y > b.Min.Y { + c += m.at(b.Min.X, y-1) + } else if x > b.Min.X { + c += m.at(x-1, b.Min.Y) + } + m.pix[(y-m.rect.Min.Y)*m.stride+(x-m.rect.Min.X)] += c + } + } +} + +// newIntegrals returns the integral and the squared integral. +func newIntegrals(src image.Image) (*integral, *integral) { + b := src.Bounds() + srcg, ok := src.(*image.Gray) + if !ok { + srcg = image.NewGray(b) + draw.Draw(srcg, b, src, b.Min, draw.Src) + } + + m := integral{ + pix: make([]uint64, b.Max.Y*b.Max.X), + stride: b.Max.X, + rect: b, + } + mSq := integral{ + pix: make([]uint64, b.Max.Y*b.Max.X), + stride: b.Max.X, + rect: b, + } + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + os := (y-b.Min.Y)*srcg.Stride + x - b.Min.X + om := (y-b.Min.Y)*m.stride + x - b.Min.X + c := uint64(srcg.Pix[os]) + m.pix[om] = c + mSq.pix[om] = c * c + } + } + m.integrate() + mSq.integrate() + return &m, &mSq +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral_test.go new file mode 100644 index 000000000..0bc321a4d --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral_test.go @@ -0,0 +1,156 @@ +// Copyright 2011 The Graphics-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 detect + +import ( + "bytes" + "fmt" + "image" + "testing" +) + +type integralTest struct { + x int + y int + src []uint8 + res []uint8 +} + +var integralTests = []integralTest{ + { + 1, 1, + []uint8{0x01}, + []uint8{0x01}, + }, + { + 2, 2, + []uint8{ + 0x01, 0x02, + 0x03, 0x04, + }, + []uint8{ + 0x01, 0x03, + 0x04, 0x0a, + }, + }, + { + 4, 4, + []uint8{ + 0x02, 0x03, 0x00, 0x01, + 0x01, 0x02, 0x01, 0x05, + 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, + }, + []uint8{ + 0x02, 0x05, 0x05, 0x06, + 0x03, 0x08, 0x09, 0x0f, + 0x04, 0x0a, 0x0c, 0x13, + 0x05, 0x0c, 0x0f, 0x17, + }, + }, +} + +func sprintBox(box []byte, width, height int) string { + buf := bytes.NewBuffer(nil) + i := 0 + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + fmt.Fprintf(buf, " 0x%02x,", box[i]) + i++ + } + buf.WriteByte('\n') + } + return buf.String() +} + +func TestIntegral(t *testing.T) { + for i, oc := range integralTests { + src := &image.Gray{ + Pix: oc.src, + Stride: oc.x, + Rect: image.Rect(0, 0, oc.x, oc.y), + } + dst, _ := newIntegrals(src) + res := make([]byte, len(dst.pix)) + for i, p := range dst.pix { + res[i] = byte(p) + } + + if !bytes.Equal(res, oc.res) { + got := sprintBox(res, oc.x, oc.y) + want := sprintBox(oc.res, oc.x, oc.y) + t.Errorf("%d: got\n%s\n want\n%s", i, got, want) + } + } +} + +func TestIntegralSum(t *testing.T) { + src := &image.Gray{ + Pix: []uint8{ + 0x02, 0x03, 0x00, 0x01, 0x03, + 0x01, 0x02, 0x01, 0x05, 0x05, + 0x01, 0x01, 0x01, 0x01, 0x02, + 0x01, 0x01, 0x01, 0x01, 0x07, + 0x02, 0x01, 0x00, 0x03, 0x01, + }, + Stride: 5, + Rect: image.Rect(0, 0, 5, 5), + } + img, _ := newIntegrals(src) + + type sumTest struct { + rect image.Rectangle + sum uint64 + } + + var sumTests = []sumTest{ + {image.Rect(0, 0, 1, 1), 2}, + {image.Rect(0, 0, 2, 1), 5}, + {image.Rect(0, 0, 1, 3), 4}, + {image.Rect(1, 1, 3, 3), 5}, + {image.Rect(2, 2, 4, 4), 4}, + {image.Rect(4, 3, 5, 5), 8}, + {image.Rect(2, 4, 3, 5), 0}, + } + + for _, st := range sumTests { + s := img.sum(st.rect) + if s != st.sum { + t.Errorf("%v: got %d want %d", st.rect, s, st.sum) + return + } + } +} + +func TestIntegralSubImage(t *testing.T) { + m0 := &image.Gray{ + Pix: []uint8{ + 0x02, 0x03, 0x00, 0x01, 0x03, + 0x01, 0x02, 0x01, 0x05, 0x05, + 0x01, 0x04, 0x01, 0x01, 0x02, + 0x01, 0x02, 0x01, 0x01, 0x07, + 0x02, 0x01, 0x09, 0x03, 0x01, + }, + Stride: 5, + Rect: image.Rect(0, 0, 5, 5), + } + b := image.Rect(1, 1, 4, 4) + m1 := m0.SubImage(b) + mi0, _ := newIntegrals(m0) + mi1, _ := newIntegrals(m1) + + sum0 := mi0.sum(b) + sum1 := mi1.sum(b) + if sum0 != sum1 { + t.Errorf("b got %d want %d", sum0, sum1) + } + + r0 := image.Rect(2, 2, 4, 4) + sum0 = mi0.sum(r0) + sum1 = mi1.sum(r0) + if sum0 != sum1 { + t.Errorf("r0 got %d want %d", sum1, sum0) + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser.go new file mode 100644 index 000000000..51ded1a1c --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser.go @@ -0,0 +1,125 @@ +// Copyright 2011 The Graphics-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 detect + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "image" + "io" + "io/ioutil" + "strconv" + "strings" +) + +type xmlFeature struct { + Rects []string `xml:"grp>feature>rects>grp"` + Tilted int `xml:"grp>feature>tilted"` + Threshold float64 `xml:"grp>threshold"` + Left float64 `xml:"grp>left_val"` + Right float64 `xml:"grp>right_val"` +} + +type xmlStages struct { + Trees []xmlFeature `xml:"trees>grp"` + Stage_threshold float64 `xml:"stage_threshold"` + Parent int `xml:"parent"` + Next int `xml:"next"` +} + +type opencv_storage struct { + Any struct { + XMLName xml.Name + Type string `xml:"type_id,attr"` + Size string `xml:"size"` + Stages []xmlStages `xml:"stages>grp"` + } `xml:",any"` +} + +func buildFeature(r string) (f Feature, err error) { + var x, y, w, h int + var weight float64 + _, err = fmt.Sscanf(r, "%d %d %d %d %f", &x, &y, &w, &h, &weight) + if err != nil { + return + } + f.Rect = image.Rect(x, y, x+w, y+h) + f.Weight = weight + return +} + +func buildCascade(s *opencv_storage) (c *Cascade, name string, err error) { + if s.Any.Type != "opencv-haar-classifier" { + err = fmt.Errorf("got %s want opencv-haar-classifier", s.Any.Type) + return + } + name = s.Any.XMLName.Local + + c = &Cascade{} + sizes := strings.Split(s.Any.Size, " ") + w, err := strconv.Atoi(sizes[0]) + if err != nil { + return nil, "", err + } + h, err := strconv.Atoi(sizes[1]) + if err != nil { + return nil, "", err + } + c.Size = image.Pt(w, h) + c.Stage = []CascadeStage{} + + for _, stage := range s.Any.Stages { + cs := CascadeStage{ + Classifier: []Classifier{}, + Threshold: stage.Stage_threshold, + } + for _, tree := range stage.Trees { + if tree.Tilted != 0 { + err = errors.New("Cascade does not support tilted features") + return + } + + cls := Classifier{ + Feature: []Feature{}, + Threshold: tree.Threshold, + Left: tree.Left, + Right: tree.Right, + } + + for _, rect := range tree.Rects { + f, err := buildFeature(rect) + if err != nil { + return nil, "", err + } + cls.Feature = append(cls.Feature, f) + } + + cs.Classifier = append(cs.Classifier, cls) + } + c.Stage = append(c.Stage, cs) + } + + return +} + +// ParseOpenCV produces a detection Cascade from an OpenCV XML file. +func ParseOpenCV(r io.Reader) (cascade *Cascade, name string, err error) { + // BUG(crawshaw): tag-based parsing doesn't seem to work with <_> + buf, err := ioutil.ReadAll(r) + if err != nil { + return + } + buf = bytes.Replace(buf, []byte("<_>"), []byte("<grp>"), -1) + buf = bytes.Replace(buf, []byte("</_>"), []byte("</grp>"), -1) + + s := &opencv_storage{} + err = xml.Unmarshal(buf, s) + if err != nil { + return + } + return buildCascade(s) +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser_test.go new file mode 100644 index 000000000..343390499 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser_test.go @@ -0,0 +1,75 @@ +// Copyright 2011 The Graphics-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 detect + +import ( + "image" + "os" + "reflect" + "testing" +) + +var ( + classifier0 = Classifier{ + Feature: []Feature{ + Feature{Rect: image.Rect(0, 0, 3, 4), Weight: -1}, + Feature{Rect: image.Rect(3, 4, 5, 6), Weight: 3.1}, + }, + Threshold: 0.03, + Left: 0.01, + Right: 0.8, + } + classifier1 = Classifier{ + Feature: []Feature{ + Feature{Rect: image.Rect(3, 7, 17, 11), Weight: -3.2}, + Feature{Rect: image.Rect(3, 9, 17, 11), Weight: 2.}, + }, + Threshold: 0.11, + Left: 0.03, + Right: 0.83, + } + classifier2 = Classifier{ + Feature: []Feature{ + Feature{Rect: image.Rect(1, 1, 3, 3), Weight: -1.}, + Feature{Rect: image.Rect(3, 3, 5, 5), Weight: 2.5}, + }, + Threshold: 0.07, + Left: 0.2, + Right: 0.4, + } + cascade = Cascade{ + Stage: []CascadeStage{ + CascadeStage{ + Classifier: []Classifier{classifier0, classifier1}, + Threshold: 0.82, + }, + CascadeStage{ + Classifier: []Classifier{classifier2}, + Threshold: 0.22, + }, + }, + Size: image.Pt(20, 20), + } +) + +func TestParseOpenCV(t *testing.T) { + file, err := os.Open("../../testdata/opencv.xml") + if err != nil { + t.Fatal(err) + } + defer file.Close() + + cascadeFile, name, err := ParseOpenCV(file) + if err != nil { + t.Fatal(err) + } + if name != "name_of_cascade" { + t.Fatalf("name: got %s want name_of_cascade", name) + } + + if !reflect.DeepEqual(cascade, *cascadeFile) { + t.Errorf("got\n %v want\n %v", *cascadeFile, cascade) + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector.go new file mode 100644 index 000000000..1ebd6db59 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector.go @@ -0,0 +1,55 @@ +// Copyright 2011 The Graphics-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 detect + +import ( + "image" +) + +// projector allows projecting from a source Rectangle onto a target Rectangle. +type projector struct { + // rx, ry is the scaling factor. + rx, ry float64 + // dx, dy is the translation factor. + dx, dy float64 + // r is the clipping region of the target. + r image.Rectangle +} + +// newProjector creates a Projector with source src and target dst. +func newProjector(dst image.Rectangle, src image.Rectangle) *projector { + return &projector{ + rx: float64(dst.Dx()) / float64(src.Dx()), + ry: float64(dst.Dy()) / float64(src.Dy()), + dx: float64(dst.Min.X - src.Min.X), + dy: float64(dst.Min.Y - src.Min.Y), + r: dst, + } +} + +// pt projects p from the source rectangle onto the target rectangle. +func (s *projector) pt(p image.Point) image.Point { + return image.Point{ + clamp(s.rx*float64(p.X)+s.dx, s.r.Min.X, s.r.Max.X), + clamp(s.ry*float64(p.Y)+s.dy, s.r.Min.Y, s.r.Max.Y), + } +} + +// rect projects r from the source rectangle onto the target rectangle. +func (s *projector) rect(r image.Rectangle) image.Rectangle { + return image.Rectangle{s.pt(r.Min), s.pt(r.Max)} +} + +// clamp rounds and clamps o to the integer range [x0, x1]. +func clamp(o float64, x0, x1 int) int { + x := int(o + 0.5) + if x < x0 { + return x0 + } + if x > x1 { + return x1 + } + return x +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector_test.go new file mode 100644 index 000000000..c6d0b0cd5 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector_test.go @@ -0,0 +1,49 @@ +// Copyright 2011 The Graphics-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 detect + +import ( + "image" + "reflect" + "testing" +) + +type projectorTest struct { + dst image.Rectangle + src image.Rectangle + pdst image.Rectangle + psrc image.Rectangle +} + +var projectorTests = []projectorTest{ + { + image.Rect(0, 0, 6, 6), + image.Rect(0, 0, 2, 2), + image.Rect(0, 0, 6, 6), + image.Rect(0, 0, 2, 2), + }, + { + image.Rect(0, 0, 6, 6), + image.Rect(0, 0, 2, 2), + image.Rect(3, 3, 6, 6), + image.Rect(1, 1, 2, 2), + }, + { + image.Rect(30, 30, 40, 40), + image.Rect(10, 10, 20, 20), + image.Rect(32, 33, 34, 37), + image.Rect(12, 13, 14, 17), + }, +} + +func TestProjector(t *testing.T) { + for i, tt := range projectorTests { + pr := newProjector(tt.dst, tt.src) + res := pr.rect(tt.psrc) + if !reflect.DeepEqual(res, tt.pdst) { + t.Errorf("%d: got %v want %v", i, res, tt.pdst) + } + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/Makefile b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/Makefile new file mode 100644 index 000000000..7bfdf22d8 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/Makefile @@ -0,0 +1,11 @@ +# Copyright 2011 The Graphics-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. + +include $(GOROOT)/src/Make.inc + +TARG=code.google.com/p/graphics-go/graphics/graphicstest +GOFILES=\ + graphicstest.go\ + +include $(GOROOT)/src/Make.pkg diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/graphicstest.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/graphicstest.go new file mode 100644 index 000000000..ceb3a974d --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/graphicstest.go @@ -0,0 +1,112 @@ +// Copyright 2011 The Graphics-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 graphicstest + +import ( + "bytes" + "errors" + "fmt" + "image" + "image/color" + "os" +) + +// LoadImage decodes an image from a file. +func LoadImage(path string) (img image.Image, err error) { + file, err := os.Open(path) + if err != nil { + return + } + defer file.Close() + img, _, err = image.Decode(file) + return +} + +func delta(u0, u1 uint32) int { + d := int(u0) - int(u1) + if d < 0 { + return -d + } + return d +} + +func withinTolerance(c0, c1 color.Color, tol int) bool { + r0, g0, b0, a0 := c0.RGBA() + r1, g1, b1, a1 := c1.RGBA() + r := delta(r0, r1) + g := delta(g0, g1) + b := delta(b0, b1) + a := delta(a0, a1) + return r <= tol && g <= tol && b <= tol && a <= tol +} + +// ImageWithinTolerance checks that each pixel varies by no more than tol. +func ImageWithinTolerance(m0, m1 image.Image, tol int) error { + b0 := m0.Bounds() + b1 := m1.Bounds() + if !b0.Eq(b1) { + return errors.New(fmt.Sprintf("got bounds %v want %v", b0, b1)) + } + + for y := b0.Min.Y; y < b0.Max.Y; y++ { + for x := b0.Min.X; x < b0.Max.X; x++ { + c0 := m0.At(x, y) + c1 := m1.At(x, y) + if !withinTolerance(c0, c1, tol) { + e := fmt.Sprintf("got %v want %v at (%d, %d)", c0, c1, x, y) + return errors.New(e) + } + } + } + return nil +} + +// SprintBox pretty prints the array as a hexidecimal matrix. +func SprintBox(box []byte, width, height int) string { + buf := bytes.NewBuffer(nil) + i := 0 + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + fmt.Fprintf(buf, " 0x%02x,", box[i]) + i++ + } + buf.WriteByte('\n') + } + return buf.String() +} + +// SprintImageR pretty prints the red channel of src. It looks like SprintBox. +func SprintImageR(src *image.RGBA) string { + w, h := src.Rect.Dx(), src.Rect.Dy() + i := 0 + box := make([]byte, w*h) + for y := src.Rect.Min.Y; y < src.Rect.Max.Y; y++ { + for x := src.Rect.Min.X; x < src.Rect.Max.X; x++ { + off := (y-src.Rect.Min.Y)*src.Stride + (x-src.Rect.Min.X)*4 + box[i] = src.Pix[off] + i++ + } + } + return SprintBox(box, w, h) +} + +// MakeRGBA returns an image with R, G, B taken from src. +func MakeRGBA(src []uint8, width int) *image.RGBA { + b := image.Rect(0, 0, width, len(src)/width) + ret := image.NewRGBA(b) + i := 0 + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + ret.SetRGBA(x, y, color.RGBA{ + R: src[i], + G: src[i], + B: src[i], + A: 0xff, + }) + i++ + } + } + return ret +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/Makefile b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/Makefile new file mode 100644 index 000000000..4d8f524fb --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/Makefile @@ -0,0 +1,13 @@ +# Copyright 2012 The Graphics-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. + +include $(GOROOT)/src/Make.inc + +TARG=code.google.com/p/graphics-go/graphics/interp +GOFILES=\ + bilinear.go\ + doc.go\ + interp.go\ + +include $(GOROOT)/src/Make.pkg diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear.go new file mode 100644 index 000000000..e18321a15 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear.go @@ -0,0 +1,206 @@ +// Copyright 2012 The Graphics-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 interp + +import ( + "image" + "image/color" + "math" +) + +// Bilinear implements bilinear interpolation. +var Bilinear Interp = bilinear{} + +type bilinear struct{} + +func (i bilinear) Interp(src image.Image, x, y float64) color.Color { + if src, ok := src.(*image.RGBA); ok { + return i.RGBA(src, x, y) + } + return bilinearGeneral(src, x, y) +} + +func bilinearGeneral(src image.Image, x, y float64) color.Color { + p := findLinearSrc(src.Bounds(), x, y) + var fr, fg, fb, fa float64 + var r, g, b, a uint32 + + r, g, b, a = src.At(p.low.X, p.low.Y).RGBA() + fr += float64(r) * p.frac00 + fg += float64(g) * p.frac00 + fb += float64(b) * p.frac00 + fa += float64(a) * p.frac00 + + r, g, b, a = src.At(p.high.X, p.low.Y).RGBA() + fr += float64(r) * p.frac01 + fg += float64(g) * p.frac01 + fb += float64(b) * p.frac01 + fa += float64(a) * p.frac01 + + r, g, b, a = src.At(p.low.X, p.high.Y).RGBA() + fr += float64(r) * p.frac10 + fg += float64(g) * p.frac10 + fb += float64(b) * p.frac10 + fa += float64(a) * p.frac10 + + r, g, b, a = src.At(p.high.X, p.high.Y).RGBA() + fr += float64(r) * p.frac11 + fg += float64(g) * p.frac11 + fb += float64(b) * p.frac11 + fa += float64(a) * p.frac11 + + var c color.RGBA64 + c.R = uint16(fr + 0.5) + c.G = uint16(fg + 0.5) + c.B = uint16(fb + 0.5) + c.A = uint16(fa + 0.5) + return c +} + +func (bilinear) RGBA(src *image.RGBA, x, y float64) color.RGBA { + p := findLinearSrc(src.Bounds(), x, y) + + // Array offsets for the surrounding pixels. + off00 := offRGBA(src, p.low.X, p.low.Y) + off01 := offRGBA(src, p.high.X, p.low.Y) + off10 := offRGBA(src, p.low.X, p.high.Y) + off11 := offRGBA(src, p.high.X, p.high.Y) + + var fr, fg, fb, fa float64 + + fr += float64(src.Pix[off00+0]) * p.frac00 + fg += float64(src.Pix[off00+1]) * p.frac00 + fb += float64(src.Pix[off00+2]) * p.frac00 + fa += float64(src.Pix[off00+3]) * p.frac00 + + fr += float64(src.Pix[off01+0]) * p.frac01 + fg += float64(src.Pix[off01+1]) * p.frac01 + fb += float64(src.Pix[off01+2]) * p.frac01 + fa += float64(src.Pix[off01+3]) * p.frac01 + + fr += float64(src.Pix[off10+0]) * p.frac10 + fg += float64(src.Pix[off10+1]) * p.frac10 + fb += float64(src.Pix[off10+2]) * p.frac10 + fa += float64(src.Pix[off10+3]) * p.frac10 + + fr += float64(src.Pix[off11+0]) * p.frac11 + fg += float64(src.Pix[off11+1]) * p.frac11 + fb += float64(src.Pix[off11+2]) * p.frac11 + fa += float64(src.Pix[off11+3]) * p.frac11 + + var c color.RGBA + c.R = uint8(fr + 0.5) + c.G = uint8(fg + 0.5) + c.B = uint8(fb + 0.5) + c.A = uint8(fa + 0.5) + return c +} + +func (bilinear) Gray(src *image.Gray, x, y float64) color.Gray { + p := findLinearSrc(src.Bounds(), x, y) + + // Array offsets for the surrounding pixels. + off00 := offGray(src, p.low.X, p.low.Y) + off01 := offGray(src, p.high.X, p.low.Y) + off10 := offGray(src, p.low.X, p.high.Y) + off11 := offGray(src, p.high.X, p.high.Y) + + var fc float64 + fc += float64(src.Pix[off00]) * p.frac00 + fc += float64(src.Pix[off01]) * p.frac01 + fc += float64(src.Pix[off10]) * p.frac10 + fc += float64(src.Pix[off11]) * p.frac11 + + var c color.Gray + c.Y = uint8(fc + 0.5) + return c +} + +type bilinearSrc struct { + // Top-left and bottom-right interpolation sources + low, high image.Point + // Fraction of each pixel to take. The 0 suffix indicates + // top/left, and the 1 suffix indicates bottom/right. + frac00, frac01, frac10, frac11 float64 +} + +func findLinearSrc(b image.Rectangle, sx, sy float64) bilinearSrc { + maxX := float64(b.Max.X) + maxY := float64(b.Max.Y) + minX := float64(b.Min.X) + minY := float64(b.Min.Y) + lowX := math.Floor(sx - 0.5) + lowY := math.Floor(sy - 0.5) + if lowX < minX { + lowX = minX + } + if lowY < minY { + lowY = minY + } + + highX := math.Ceil(sx - 0.5) + highY := math.Ceil(sy - 0.5) + if highX >= maxX { + highX = maxX - 1 + } + if highY >= maxY { + highY = maxY - 1 + } + + // In the variables below, the 0 suffix indicates top/left, and the + // 1 suffix indicates bottom/right. + + // Center of each surrounding pixel. + x00 := lowX + 0.5 + y00 := lowY + 0.5 + x01 := highX + 0.5 + y01 := lowY + 0.5 + x10 := lowX + 0.5 + y10 := highY + 0.5 + x11 := highX + 0.5 + y11 := highY + 0.5 + + p := bilinearSrc{ + low: image.Pt(int(lowX), int(lowY)), + high: image.Pt(int(highX), int(highY)), + } + + // Literally, edge cases. If we are close enough to the edge of + // the image, curtail the interpolation sources. + if lowX == highX && lowY == highY { + p.frac00 = 1.0 + } else if sy-minY <= 0.5 && sx-minX <= 0.5 { + p.frac00 = 1.0 + } else if maxY-sy <= 0.5 && maxX-sx <= 0.5 { + p.frac11 = 1.0 + } else if sy-minY <= 0.5 || lowY == highY { + p.frac00 = x01 - sx + p.frac01 = sx - x00 + } else if sx-minX <= 0.5 || lowX == highX { + p.frac00 = y10 - sy + p.frac10 = sy - y00 + } else if maxY-sy <= 0.5 { + p.frac10 = x11 - sx + p.frac11 = sx - x10 + } else if maxX-sx <= 0.5 { + p.frac01 = y11 - sy + p.frac11 = sy - y01 + } else { + p.frac00 = (x01 - sx) * (y10 - sy) + p.frac01 = (sx - x00) * (y11 - sy) + p.frac10 = (x11 - sx) * (sy - y00) + p.frac11 = (sx - x10) * (sy - y01) + } + + return p +} + +// TODO(crawshaw): When we have inlining, consider func (p *RGBA) Off(x, y) int +func offRGBA(src *image.RGBA, x, y int) int { + return (y-src.Rect.Min.Y)*src.Stride + (x-src.Rect.Min.X)*4 +} +func offGray(src *image.Gray, x, y int) int { + return (y-src.Rect.Min.Y)*src.Stride + (x - src.Rect.Min.X) +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear_test.go new file mode 100644 index 000000000..242d70546 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear_test.go @@ -0,0 +1,143 @@ +// Copyright 2012 The Graphics-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 interp + +import ( + "image" + "image/color" + "testing" +) + +type interpTest struct { + desc string + src []uint8 + srcWidth int + x, y float64 + expect uint8 +} + +func (p *interpTest) newSrc() *image.RGBA { + b := image.Rect(0, 0, p.srcWidth, len(p.src)/p.srcWidth) + src := image.NewRGBA(b) + i := 0 + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + src.SetRGBA(x, y, color.RGBA{ + R: p.src[i], + G: p.src[i], + B: p.src[i], + A: 0xff, + }) + i++ + } + } + return src +} + +var interpTests = []interpTest{ + { + desc: "center of a single white pixel should match that pixel", + src: []uint8{0x00}, + srcWidth: 1, + x: 0.5, + y: 0.5, + expect: 0x00, + }, + { + desc: "middle of a square is equally weighted", + src: []uint8{ + 0x00, 0xff, + 0xff, 0x00, + }, + srcWidth: 2, + x: 1.0, + y: 1.0, + expect: 0x80, + }, + { + desc: "center of a pixel is just that pixel", + src: []uint8{ + 0x00, 0xff, + 0xff, 0x00, + }, + srcWidth: 2, + x: 1.5, + y: 0.5, + expect: 0xff, + }, + { + desc: "asymmetry abounds", + src: []uint8{ + 0xaa, 0x11, 0x55, + 0xff, 0x95, 0xdd, + }, + srcWidth: 3, + x: 2.0, + y: 1.0, + expect: 0x76, // (0x11 + 0x55 + 0x95 + 0xdd) / 4 + }, +} + +func TestBilinearRGBA(t *testing.T) { + for _, p := range interpTests { + src := p.newSrc() + + // Fast path. + c := Bilinear.(RGBA).RGBA(src, p.x, p.y) + if c.R != c.G || c.R != c.B || c.A != 0xff { + t.Errorf("expect channels to match, got %v", c) + continue + } + if c.R != p.expect { + t.Errorf("%s: got 0x%02x want 0x%02x", p.desc, c.R, p.expect) + continue + } + + // Standard Interp should use the fast path. + cStd := Bilinear.Interp(src, p.x, p.y) + if cStd != c { + t.Errorf("%s: standard mismatch got %v want %v", p.desc, cStd, c) + continue + } + + // General case should match the fast path. + cGen := color.RGBAModel.Convert(bilinearGeneral(src, p.x, p.y)) + r0, g0, b0, a0 := c.RGBA() + r1, g1, b1, a1 := cGen.RGBA() + if r0 != r1 || g0 != g1 || b0 != b1 || a0 != a1 { + t.Errorf("%s: general case mismatch got %v want %v", p.desc, c, cGen) + continue + } + } +} + +func TestBilinearSubImage(t *testing.T) { + b0 := image.Rect(0, 0, 4, 4) + src0 := image.NewRGBA(b0) + b1 := image.Rect(1, 1, 3, 3) + src1 := src0.SubImage(b1).(*image.RGBA) + src1.Set(1, 1, color.RGBA{0x11, 0, 0, 0xff}) + src1.Set(2, 1, color.RGBA{0x22, 0, 0, 0xff}) + src1.Set(1, 2, color.RGBA{0x33, 0, 0, 0xff}) + src1.Set(2, 2, color.RGBA{0x44, 0, 0, 0xff}) + + tests := []struct { + x, y float64 + want uint8 + }{ + {1, 1, 0x11}, + {3, 1, 0x22}, + {1, 3, 0x33}, + {3, 3, 0x44}, + {2, 2, 0x2b}, + } + + for _, p := range tests { + c := Bilinear.(RGBA).RGBA(src1, p.x, p.y) + if c.R != p.want { + t.Errorf("(%.0f, %.0f): got 0x%02x want 0x%02x", p.x, p.y, c.R, p.want) + } + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/doc.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/doc.go new file mode 100644 index 000000000..b115534cc --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/doc.go @@ -0,0 +1,25 @@ +// Copyright 2012 The Graphics-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 interp implements image interpolation. + +An interpolator provides the Interp interface, which can be used +to interpolate a pixel: + + c := interp.Bilinear.Interp(src, 1.2, 1.8) + +To interpolate a large number of RGBA or Gray pixels, an implementation +may provide a fast-path by implementing the RGBA or Gray interfaces. + + i1, ok := i.(interp.RGBA) + if ok { + c := i1.RGBA(src, 1.2, 1.8) + // use c.R, c.G, etc + return + } + c := i.Interp(src, 1.2, 1.8) + // use generic color.Color +*/ +package interp diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/interp.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/interp.go new file mode 100644 index 000000000..560637d4a --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/interp.go @@ -0,0 +1,29 @@ +// Copyright 2012 The Graphics-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 interp + +import ( + "image" + "image/color" +) + +// Interp interpolates an image's color at fractional co-ordinates. +type Interp interface { + // Interp interpolates (x, y). + Interp(src image.Image, x, y float64) color.Color +} + +// RGBA is a fast-path interpolation implementation for image.RGBA. +// It is common for an Interp to also implement RGBA. +type RGBA interface { + // RGBA interpolates (x, y). + RGBA(src *image.RGBA, x, y float64) color.RGBA +} + +// Gray is a fast-path interpolation implementation for image.Gray. +type Gray interface { + // Gray interpolates (x, y). + Gray(src *image.Gray, x, y float64) color.Gray +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate.go new file mode 100644 index 000000000..62bde1a08 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate.go @@ -0,0 +1,35 @@ +// Copyright 2011 The Graphics-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 graphics + +import ( + "code.google.com/p/graphics-go/graphics/interp" + "errors" + "image" + "image/draw" +) + +// RotateOptions are the rotation parameters. +// Angle is the angle, in radians, to rotate the image clockwise. +type RotateOptions struct { + Angle float64 +} + +// Rotate produces a rotated version of src, drawn onto dst. +func Rotate(dst draw.Image, src image.Image, opt *RotateOptions) error { + if dst == nil { + return errors.New("graphics: dst is nil") + } + if src == nil { + return errors.New("graphics: src is nil") + } + + angle := 0.0 + if opt != nil { + angle = opt.Angle + } + + return I.Rotate(angle).TransformCenter(dst, src, interp.Bilinear) +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate_test.go new file mode 100644 index 000000000..bfc532a0a --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate_test.go @@ -0,0 +1,169 @@ +// Copyright 2011 The Graphics-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 graphics + +import ( + "code.google.com/p/graphics-go/graphics/graphicstest" + "image" + "math" + "testing" + + _ "image/png" +) + +var rotateOneColorTests = []transformOneColorTest{ + { + "onepixel-onequarter", 1, 1, 1, 1, + &RotateOptions{math.Pi / 2}, + []uint8{0xff}, + []uint8{0xff}, + }, + { + "onepixel-partial", 1, 1, 1, 1, + &RotateOptions{math.Pi * 2.0 / 3.0}, + []uint8{0xff}, + []uint8{0xff}, + }, + { + "onepixel-complete", 1, 1, 1, 1, + &RotateOptions{2 * math.Pi}, + []uint8{0xff}, + []uint8{0xff}, + }, + { + "even-onequarter", 2, 2, 2, 2, + &RotateOptions{math.Pi / 2.0}, + []uint8{ + 0xff, 0x00, + 0x00, 0xff, + }, + []uint8{ + 0x00, 0xff, + 0xff, 0x00, + }, + }, + { + "even-complete", 2, 2, 2, 2, + &RotateOptions{2.0 * math.Pi}, + []uint8{ + 0xff, 0x00, + 0x00, 0xff, + }, + []uint8{ + 0xff, 0x00, + 0x00, 0xff, + }, + }, + { + "line-partial", 3, 3, 3, 3, + &RotateOptions{math.Pi * 1.0 / 3.0}, + []uint8{ + 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, + }, + []uint8{ + 0xa2, 0x80, 0x00, + 0x22, 0xff, 0x22, + 0x00, 0x80, 0xa2, + }, + }, + { + "line-offset-partial", 3, 3, 3, 3, + &RotateOptions{math.Pi * 3 / 2}, + []uint8{ + 0x00, 0x00, 0x00, + 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, + }, + []uint8{ + 0x00, 0xff, 0x00, + 0x00, 0xff, 0x00, + 0x00, 0x00, 0x00, + }, + }, + { + "dot-partial", 4, 4, 4, 4, + &RotateOptions{math.Pi}, + []uint8{ + 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + }, + []uint8{ + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0x00, + 0x00, 0x00, 0x00, 0x00, + }, + }, +} + +func TestRotateOneColor(t *testing.T) { + for _, oc := range rotateOneColorTests { + src := oc.newSrc() + dst := oc.newDst() + + if err := Rotate(dst, src, oc.opt.(*RotateOptions)); err != nil { + t.Errorf("rotate %s: %v", oc.desc, err) + continue + } + if !checkTransformTest(t, &oc, dst) { + continue + } + } +} + +func TestRotateEmpty(t *testing.T) { + empty := image.NewRGBA(image.Rect(0, 0, 0, 0)) + if err := Rotate(empty, empty, nil); err != nil { + t.Fatal(err) + } +} + +func TestRotateGopherSide(t *testing.T) { + src, err := graphicstest.LoadImage("../testdata/gopher.png") + if err != nil { + t.Fatal(err) + } + + srcb := src.Bounds() + dst := image.NewRGBA(image.Rect(0, 0, srcb.Dy(), srcb.Dx())) + if err := Rotate(dst, src, &RotateOptions{math.Pi / 2.0}); err != nil { + t.Fatal(err) + } + + cmp, err := graphicstest.LoadImage("../testdata/gopher-rotate-side.png") + if err != nil { + t.Fatal(err) + } + err = graphicstest.ImageWithinTolerance(dst, cmp, 0x101) + if err != nil { + t.Fatal(err) + } +} + +func TestRotateGopherPartial(t *testing.T) { + src, err := graphicstest.LoadImage("../testdata/gopher.png") + if err != nil { + t.Fatal(err) + } + + srcb := src.Bounds() + dst := image.NewRGBA(image.Rect(0, 0, srcb.Dx(), srcb.Dy())) + if err := Rotate(dst, src, &RotateOptions{math.Pi / 3.0}); err != nil { + t.Fatal(err) + } + + cmp, err := graphicstest.LoadImage("../testdata/gopher-rotate-partial.png") + if err != nil { + t.Fatal(err) + } + err = graphicstest.ImageWithinTolerance(dst, cmp, 0x101) + if err != nil { + t.Fatal(err) + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale.go new file mode 100644 index 000000000..7a7fe9696 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale.go @@ -0,0 +1,31 @@ +// Copyright 2011 The Graphics-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 graphics + +import ( + "code.google.com/p/graphics-go/graphics/interp" + "errors" + "image" + "image/draw" +) + +// Scale produces a scaled version of the image using bilinear interpolation. +func Scale(dst draw.Image, src image.Image) error { + if dst == nil { + return errors.New("graphics: dst is nil") + } + if src == nil { + return errors.New("graphics: src is nil") + } + + b := dst.Bounds() + srcb := src.Bounds() + if b.Empty() || srcb.Empty() { + return nil + } + sx := float64(b.Dx()) / float64(srcb.Dx()) + sy := float64(b.Dy()) / float64(srcb.Dy()) + return I.Scale(sx, sy).Transform(dst, src, interp.Bilinear) +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale_test.go new file mode 100644 index 000000000..9c2468f11 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale_test.go @@ -0,0 +1,153 @@ +// Copyright 2011 The Graphics-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 graphics + +import ( + "code.google.com/p/graphics-go/graphics/graphicstest" + "image" + "testing" + + _ "image/png" +) + +var scaleOneColorTests = []transformOneColorTest{ + { + "down-half", + 1, 1, + 2, 2, + nil, + []uint8{ + 0x80, 0x00, + 0x00, 0x80, + }, + []uint8{ + 0x40, + }, + }, + { + "up-double", + 4, 4, + 2, 2, + nil, + []uint8{ + 0x80, 0x00, + 0x00, 0x80, + }, + []uint8{ + 0x80, 0x60, 0x20, 0x00, + 0x60, 0x50, 0x30, 0x20, + 0x20, 0x30, 0x50, 0x60, + 0x00, 0x20, 0x60, 0x80, + }, + }, + { + "up-doublewidth", + 4, 2, + 2, 2, + nil, + []uint8{ + 0x80, 0x00, + 0x00, 0x80, + }, + []uint8{ + 0x80, 0x60, 0x20, 0x00, + 0x00, 0x20, 0x60, 0x80, + }, + }, + { + "up-doubleheight", + 2, 4, + 2, 2, + nil, + []uint8{ + 0x80, 0x00, + 0x00, 0x80, + }, + []uint8{ + 0x80, 0x00, + 0x60, 0x20, + 0x20, 0x60, + 0x00, 0x80, + }, + }, + { + "up-partial", + 3, 3, + 2, 2, + nil, + []uint8{ + 0x80, 0x00, + 0x00, 0x80, + }, + []uint8{ + 0x80, 0x40, 0x00, + 0x40, 0x40, 0x40, + 0x00, 0x40, 0x80, + }, + }, +} + +func TestScaleOneColor(t *testing.T) { + for _, oc := range scaleOneColorTests { + dst := oc.newDst() + src := oc.newSrc() + if err := Scale(dst, src); err != nil { + t.Errorf("scale %s: %v", oc.desc, err) + continue + } + + if !checkTransformTest(t, &oc, dst) { + continue + } + } +} + +func TestScaleEmpty(t *testing.T) { + empty := image.NewRGBA(image.Rect(0, 0, 0, 0)) + if err := Scale(empty, empty); err != nil { + t.Fatal(err) + } +} + +func TestScaleGopher(t *testing.T) { + dst := image.NewRGBA(image.Rect(0, 0, 100, 150)) + + src, err := graphicstest.LoadImage("../testdata/gopher.png") + if err != nil { + t.Error(err) + return + } + + // Down-sample. + if err := Scale(dst, src); err != nil { + t.Fatal(err) + } + cmp, err := graphicstest.LoadImage("../testdata/gopher-100x150.png") + if err != nil { + t.Error(err) + return + } + err = graphicstest.ImageWithinTolerance(dst, cmp, 0) + if err != nil { + t.Error(err) + return + } + + // Up-sample. + dst = image.NewRGBA(image.Rect(0, 0, 500, 750)) + if err := Scale(dst, src); err != nil { + t.Fatal(err) + } + cmp, err = graphicstest.LoadImage("../testdata/gopher-500x750.png") + if err != nil { + t.Error(err) + return + } + err = graphicstest.ImageWithinTolerance(dst, cmp, 0) + if err != nil { + t.Error(err) + return + } +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/shared_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/shared_test.go new file mode 100644 index 000000000..e1cd21fb3 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/shared_test.go @@ -0,0 +1,69 @@ +// Copyright 2011 The Graphics-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 graphics + +import ( + "bytes" + "code.google.com/p/graphics-go/graphics/graphicstest" + "image" + "image/color" + "testing" +) + +type transformOneColorTest struct { + desc string + dstWidth int + dstHeight int + srcWidth int + srcHeight int + opt interface{} + src []uint8 + res []uint8 +} + +func (oc *transformOneColorTest) newSrc() *image.RGBA { + b := image.Rect(0, 0, oc.srcWidth, oc.srcHeight) + src := image.NewRGBA(b) + i := 0 + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + src.SetRGBA(x, y, color.RGBA{ + R: oc.src[i], + G: oc.src[i], + B: oc.src[i], + A: oc.src[i], + }) + i++ + } + } + return src +} + +func (oc *transformOneColorTest) newDst() *image.RGBA { + return image.NewRGBA(image.Rect(0, 0, oc.dstWidth, oc.dstHeight)) +} + +func checkTransformTest(t *testing.T, oc *transformOneColorTest, dst *image.RGBA) bool { + for ch := 0; ch < 4; ch++ { + i := 0 + res := make([]byte, len(oc.res)) + for y := 0; y < oc.dstHeight; y++ { + for x := 0; x < oc.dstWidth; x++ { + off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4 + res[i] = dst.Pix[off+ch] + i++ + } + } + + if !bytes.Equal(res, oc.res) { + got := graphicstest.SprintBox(res, oc.dstWidth, oc.dstHeight) + want := graphicstest.SprintBox(oc.res, oc.dstWidth, oc.dstHeight) + t.Errorf("%s: ch=%d\n got\n%s\n want\n%s", oc.desc, ch, got, want) + return false + } + } + + return true +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail.go new file mode 100644 index 000000000..d3ad7e8f7 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail.go @@ -0,0 +1,41 @@ +// Copyright 2011 The Graphics-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 graphics + +import ( + "image" + "image/draw" +) + +// Thumbnail scales and crops src so it fits in dst. +func Thumbnail(dst draw.Image, src image.Image) error { + // Scale down src in the dimension that is closer to dst. + sb := src.Bounds() + db := dst.Bounds() + rx := float64(sb.Dx()) / float64(db.Dx()) + ry := float64(sb.Dy()) / float64(db.Dy()) + var b image.Rectangle + if rx < ry { + b = image.Rect(0, 0, db.Dx(), int(float64(sb.Dy())/rx)) + } else { + b = image.Rect(0, 0, int(float64(sb.Dx())/ry), db.Dy()) + } + + buf := image.NewRGBA(b) + if err := Scale(buf, src); err != nil { + return err + } + + // Crop. + // TODO(crawshaw): improve on center-alignment. + var pt image.Point + if rx < ry { + pt.Y = (b.Dy() - db.Dy()) / 2 + } else { + pt.X = (b.Dx() - db.Dx()) / 2 + } + draw.Draw(dst, db, buf, pt, draw.Src) + return nil +} diff --git a/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail_test.go b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail_test.go new file mode 100644 index 000000000..d12659f17 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail_test.go @@ -0,0 +1,53 @@ +// Copyright 2011 The Graphics-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 graphics + +import ( + "code.google.com/p/graphics-go/graphics/graphicstest" + "image" + "testing" + + _ "image/png" +) + +func TestThumbnailGopher(t *testing.T) { + dst := image.NewRGBA(image.Rect(0, 0, 80, 80)) + + src, err := graphicstest.LoadImage("../testdata/gopher.png") + if err != nil { + t.Fatal(err) + } + if err := Thumbnail(dst, src); err != nil { + t.Fatal(err) + } + cmp, err := graphicstest.LoadImage("../testdata/gopher-thumb-80x80.png") + if err != nil { + t.Fatal(err) + } + err = graphicstest.ImageWithinTolerance(dst, cmp, 0) + if err != nil { + t.Error(err) + } +} + +func TestThumbnailLongGopher(t *testing.T) { + dst := image.NewRGBA(image.Rect(0, 0, 50, 150)) + + src, err := graphicstest.LoadImage("../testdata/gopher.png") + if err != nil { + t.Fatal(err) + } + if err := Thumbnail(dst, src); err != nil { + t.Fatal(err) + } + cmp, err := graphicstest.LoadImage("../testdata/gopher-thumb-50x150.png") + if err != nil { + t.Fatal(err) + } + err = graphicstest.ImageWithinTolerance(dst, cmp, 0) + if err != nil { + t.Error(err) + } +} |