draw: implement NearestNeighbor and ApproxBiLinear Transform.
Change-Id: I70a5e3703dea436354e9591fce7b704ec749c2d1 Reviewed-on: https://go-review.googlesource.com/7541 Reviewed-by: Rob Pike <r@golang.org>
This commit is contained in:
parent
ab1ce1a88c
commit
87013da148
|
@ -9,7 +9,6 @@ import (
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"golang.org/x/image/draw"
|
"golang.org/x/image/draw"
|
||||||
|
@ -34,19 +33,17 @@ func ExampleDraw() {
|
||||||
draw.ApproxBiLinear,
|
draw.ApproxBiLinear,
|
||||||
draw.CatmullRom,
|
draw.CatmullRom,
|
||||||
}
|
}
|
||||||
c, s := math.Cos(math.Pi/3), math.Sin(math.Pi/3)
|
const cos60, sin60 = 0.5, 0.866025404
|
||||||
t := &f64.Aff3{
|
t := &f64.Aff3{
|
||||||
+2 * c, -2 * s, 100,
|
+2 * cos60, -2 * sin60, 100,
|
||||||
+2 * s, +2 * c, 100,
|
+2 * sin60, +2 * cos60, 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
draw.Copy(dst, image.Point{20, 30}, src, sr, nil)
|
draw.Copy(dst, image.Point{20, 30}, src, sr, nil)
|
||||||
for i, q := range qs {
|
for i, q := range qs {
|
||||||
q.Scale(dst, image.Rect(200+10*i, 100*i, 600+10*i, 150+100*i), src, sr, nil)
|
q.Scale(dst, image.Rect(200+10*i, 100*i, 600+10*i, 150+100*i), src, sr, nil)
|
||||||
}
|
}
|
||||||
// TODO: delete the "_ = t" and uncomment this when Transform is implemented.
|
draw.NearestNeighbor.Transform(dst, t, src, sr, nil)
|
||||||
// draw.NearestNeighbor.Transform(dst, t, src, sr, nil)
|
|
||||||
_ = t
|
|
||||||
|
|
||||||
// Change false to true to write the resultant image to disk.
|
// Change false to true to write the resultant image to disk.
|
||||||
if false {
|
if false {
|
||||||
|
|
153
draw/gen.go
153
draw/gen.go
|
@ -27,12 +27,13 @@ func main() {
|
||||||
"package draw\n\nimport (\n" +
|
"package draw\n\nimport (\n" +
|
||||||
"\"image\"\n" +
|
"\"image\"\n" +
|
||||||
"\"image/color\"\n" +
|
"\"image/color\"\n" +
|
||||||
|
"\"math\"\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\"golang.org/x/image/math/f64\"\n" +
|
"\"golang.org/x/image/math/f64\"\n" +
|
||||||
")\n")
|
")\n")
|
||||||
|
|
||||||
gen(w, "nnInterpolator", codeNNScaleLeaf)
|
gen(w, "nnInterpolator", codeNNScaleLeaf, codeNNTransformLeaf)
|
||||||
gen(w, "ablInterpolator", codeABLScaleLeaf)
|
gen(w, "ablInterpolator", codeABLScaleLeaf, codeABLTransformLeaf)
|
||||||
genKernel(w)
|
genKernel(w)
|
||||||
|
|
||||||
if *debug {
|
if *debug {
|
||||||
|
@ -90,14 +91,16 @@ type data struct {
|
||||||
receiver string
|
receiver string
|
||||||
}
|
}
|
||||||
|
|
||||||
func gen(w *bytes.Buffer, receiver string, code string) {
|
func gen(w *bytes.Buffer, receiver string, codes ...string) {
|
||||||
expn(w, codeRoot, &data{receiver: receiver})
|
expn(w, codeRoot, &data{receiver: receiver})
|
||||||
for _, t := range dsTypes {
|
for _, code := range codes {
|
||||||
expn(w, code, &data{
|
for _, t := range dsTypes {
|
||||||
dType: t.dType,
|
expn(w, code, &data{
|
||||||
sType: t.sType,
|
dType: t.dType,
|
||||||
receiver: receiver,
|
sType: t.sType,
|
||||||
})
|
receiver: receiver,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,7 +230,7 @@ func expnDollar(prefix, dollar, suffix string, d *data) string {
|
||||||
"dstColorRGBA64.G = uint16(%sg)\n"+
|
"dstColorRGBA64.G = uint16(%sg)\n"+
|
||||||
"dstColorRGBA64.B = uint16(%sb)\n"+
|
"dstColorRGBA64.B = uint16(%sb)\n"+
|
||||||
"dstColorRGBA64.A = uint16(%sa)\n"+
|
"dstColorRGBA64.A = uint16(%sa)\n"+
|
||||||
"dst.Set(dr.Min.X+int(%s), dr.Min.Y+int(%s), dstColor)",
|
"dst.Set(%s, %s, dstColor)",
|
||||||
args[2], args[2], args[2], args[2],
|
args[2], args[2], args[2], args[2],
|
||||||
args[0], args[1],
|
args[0], args[1],
|
||||||
)
|
)
|
||||||
|
@ -236,8 +239,7 @@ func expnDollar(prefix, dollar, suffix string, d *data) string {
|
||||||
"dst.Pix[d+0] = uint8(uint32(%sr) >> 8)\n"+
|
"dst.Pix[d+0] = uint8(uint32(%sr) >> 8)\n"+
|
||||||
"dst.Pix[d+1] = uint8(uint32(%sg) >> 8)\n"+
|
"dst.Pix[d+1] = uint8(uint32(%sg) >> 8)\n"+
|
||||||
"dst.Pix[d+2] = uint8(uint32(%sb) >> 8)\n"+
|
"dst.Pix[d+2] = uint8(uint32(%sb) >> 8)\n"+
|
||||||
"dst.Pix[d+3] = uint8(uint32(%sa) >> 8)\n"+
|
"dst.Pix[d+3] = uint8(uint32(%sa) >> 8)",
|
||||||
"d += 4",
|
|
||||||
args[2], args[2], args[2], args[2],
|
args[2], args[2], args[2], args[2],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -256,7 +258,7 @@ func expnDollar(prefix, dollar, suffix string, d *data) string {
|
||||||
"dstColorRGBA64.G = ftou(%sg * %s)\n"+
|
"dstColorRGBA64.G = ftou(%sg * %s)\n"+
|
||||||
"dstColorRGBA64.B = ftou(%sb * %s)\n"+
|
"dstColorRGBA64.B = ftou(%sb * %s)\n"+
|
||||||
"dstColorRGBA64.A = ftou(%sa * %s)\n"+
|
"dstColorRGBA64.A = ftou(%sa * %s)\n"+
|
||||||
"dst.Set(dr.Min.X+int(%s), dr.Min.Y+int(%s), dstColor)",
|
"dst.Set(%s, %s, dstColor)",
|
||||||
args[2], args[3], args[2], args[3], args[2], args[3], args[2], args[3],
|
args[2], args[3], args[2], args[3], args[2], args[3], args[2], args[3],
|
||||||
args[0], args[1],
|
args[0], args[1],
|
||||||
)
|
)
|
||||||
|
@ -292,14 +294,14 @@ func expnDollar(prefix, dollar, suffix string, d *data) string {
|
||||||
log.Fatalf("bad sType %q", d.sType)
|
log.Fatalf("bad sType %q", d.sType)
|
||||||
case "image.Image", "*image.Gray", "*image.NRGBA", "*image.Uniform", "*image.YCbCr": // TODO: separate code for concrete types.
|
case "image.Image", "*image.Gray", "*image.NRGBA", "*image.Uniform", "*image.YCbCr": // TODO: separate code for concrete types.
|
||||||
fmt.Fprintf(buf, "%sr%s, %sg%s, %sb%s, %sa%s := "+
|
fmt.Fprintf(buf, "%sr%s, %sg%s, %sb%s, %sa%s := "+
|
||||||
"src.At(sr.Min.X + int(%s), sr.Min.Y+int(%s)).RGBA()\n",
|
"src.At(%s, %s).RGBA()\n",
|
||||||
lhs, tmp, lhs, tmp, lhs, tmp, lhs, tmp,
|
lhs, tmp, lhs, tmp, lhs, tmp, lhs, tmp,
|
||||||
args[0], args[1],
|
args[0], args[1],
|
||||||
)
|
)
|
||||||
case "*image.RGBA":
|
case "*image.RGBA":
|
||||||
// TODO: there's no need to multiply by 0x101 if the next thing
|
// TODO: there's no need to multiply by 0x101 if the next thing
|
||||||
// we're going to do is shift right by 8.
|
// we're going to do is shift right by 8.
|
||||||
fmt.Fprintf(buf, "%si := src.PixOffset(sr.Min.X + int(%s), sr.Min.Y+int(%s))\n"+
|
fmt.Fprintf(buf, "%si := src.PixOffset(%s, %s)\n"+
|
||||||
"%sr%s := uint32(src.Pix[%si+0]) * 0x101\n"+
|
"%sr%s := uint32(src.Pix[%si+0]) * 0x101\n"+
|
||||||
"%sg%s := uint32(src.Pix[%si+1]) * 0x101\n"+
|
"%sg%s := uint32(src.Pix[%si+1]) * 0x101\n"+
|
||||||
"%sb%s := uint32(src.Pix[%si+2]) * 0x101\n"+
|
"%sb%s := uint32(src.Pix[%si+2]) * 0x101\n"+
|
||||||
|
@ -327,6 +329,12 @@ func expnDollar(prefix, dollar, suffix string, d *data) string {
|
||||||
|
|
||||||
return strings.TrimSpace(buf.String())
|
return strings.TrimSpace(buf.String())
|
||||||
|
|
||||||
|
case "tweakDx":
|
||||||
|
if d.dType == "*image.RGBA" {
|
||||||
|
return strings.Replace(suffix, "dx++", "dx, d = dx+1, d+4", 1)
|
||||||
|
}
|
||||||
|
return suffix
|
||||||
|
|
||||||
case "tweakDy":
|
case "tweakDy":
|
||||||
if d.dType == "*image.RGBA" {
|
if d.dType == "*image.RGBA" {
|
||||||
return strings.Replace(suffix, "for dy, s", "for _, s", 1)
|
return strings.Replace(suffix, "for dy, s", "for _, s", 1)
|
||||||
|
@ -428,8 +436,15 @@ const (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z $receiver) Transform(dst Image, m *f64.Aff3, src image.Image, sr image.Rectangle, opts *Options) {
|
func (z $receiver) Transform(dst Image, s2d *f64.Aff3, src image.Image, sr image.Rectangle, opts *Options) {
|
||||||
panic("unimplemented")
|
dr := transformRect(s2d, &sr)
|
||||||
|
// adr is the affected destination pixels, relative to dr.Min.
|
||||||
|
adr := dst.Bounds().Intersect(dr).Sub(dr.Min)
|
||||||
|
if adr.Empty() || sr.Empty() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d2s := invert(s2d)
|
||||||
|
z.transform_Image_Image(dst, dr, adr, &d2s, src, sr)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -443,10 +458,30 @@ const (
|
||||||
for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ {
|
for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ {
|
||||||
sy := (2*uint64(dy) + 1) * sh / dh2
|
sy := (2*uint64(dy) + 1) * sh / dh2
|
||||||
$preInner
|
$preInner
|
||||||
for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx++ {
|
$tweakDx for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx++ {
|
||||||
sx := (2*uint64(dx) + 1) * sw / dw2
|
sx := (2*uint64(dx) + 1) * sw / dw2
|
||||||
p := $srcu[sx, sy]
|
p := $srcu[sr.Min.X + int(sx), sr.Min.Y + int(sy)]
|
||||||
$outputu[dx, dy, p]
|
$outputu[dr.Min.X + int(dx), dr.Min.Y + int(dy), p]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
codeNNTransformLeaf = `
|
||||||
|
func (nnInterpolator) transform_$dTypeRN_$sTypeRN(dst $dType, dr, adr image.Rectangle, d2s *f64.Aff3, src $sType, sr image.Rectangle) {
|
||||||
|
$preOuter
|
||||||
|
for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ {
|
||||||
|
dyf := float64(dr.Min.Y + int(dy)) + 0.5
|
||||||
|
$preInner
|
||||||
|
$tweakDx for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx++ {
|
||||||
|
dxf := float64(dr.Min.X + int(dx)) + 0.5
|
||||||
|
sx0 := int(math.Floor(d2s[0]*dxf + d2s[1]*dyf + d2s[2]))
|
||||||
|
sy0 := int(math.Floor(d2s[3]*dxf + d2s[4]*dyf + d2s[5]))
|
||||||
|
if !(image.Point{sx0, sy0}).In(sr) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p := $srcu[sx0, sy0]
|
||||||
|
$outputu[dr.Min.X + int(dx), dr.Min.Y + int(dy), p]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -458,9 +493,14 @@ const (
|
||||||
sh := int32(sr.Dy())
|
sh := int32(sr.Dy())
|
||||||
yscale := float64(sh) / float64(dr.Dy())
|
yscale := float64(sh) / float64(dr.Dy())
|
||||||
xscale := float64(sw) / float64(dr.Dx())
|
xscale := float64(sw) / float64(dr.Dx())
|
||||||
|
swMinus1, shMinus1 := sw - 1, sh - 1
|
||||||
$preOuter
|
$preOuter
|
||||||
|
|
||||||
for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ {
|
for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ {
|
||||||
sy := (float64(dy)+0.5)*yscale - 0.5
|
sy := (float64(dy)+0.5)*yscale - 0.5
|
||||||
|
// If sy < 0, we will clamp sy0 to 0 anyway, so it doesn't matter if
|
||||||
|
// we say int32(sy) instead of int32(math.Floor(sy)). Similarly for
|
||||||
|
// sx, below.
|
||||||
sy0 := int32(sy)
|
sy0 := int32(sy)
|
||||||
yFrac0 := sy - float64(sy0)
|
yFrac0 := sy - float64(sy0)
|
||||||
yFrac1 := 1 - yFrac0
|
yFrac1 := 1 - yFrac0
|
||||||
|
@ -468,12 +508,13 @@ const (
|
||||||
if sy < 0 {
|
if sy < 0 {
|
||||||
sy0, sy1 = 0, 0
|
sy0, sy1 = 0, 0
|
||||||
yFrac0, yFrac1 = 0, 1
|
yFrac0, yFrac1 = 0, 1
|
||||||
} else if sy1 >= sh {
|
} else if sy1 > shMinus1 {
|
||||||
sy1 = sy0
|
sy0, sy1 = shMinus1, shMinus1
|
||||||
yFrac0, yFrac1 = 1, 0
|
yFrac0, yFrac1 = 1, 0
|
||||||
}
|
}
|
||||||
$preInner
|
$preInner
|
||||||
for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx++ {
|
|
||||||
|
$tweakDx for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx++ {
|
||||||
sx := (float64(dx)+0.5)*xscale - 0.5
|
sx := (float64(dx)+0.5)*xscale - 0.5
|
||||||
sx0 := int32(sx)
|
sx0 := int32(sx)
|
||||||
xFrac0 := sx - float64(sx0)
|
xFrac0 := sx - float64(sx0)
|
||||||
|
@ -482,10 +523,66 @@ const (
|
||||||
if sx < 0 {
|
if sx < 0 {
|
||||||
sx0, sx1 = 0, 0
|
sx0, sx1 = 0, 0
|
||||||
xFrac0, xFrac1 = 0, 1
|
xFrac0, xFrac1 = 0, 1
|
||||||
} else if sx1 >= sw {
|
} else if sx1 > swMinus1 {
|
||||||
sx1 = sx0
|
sx0, sx1 = swMinus1, swMinus1
|
||||||
xFrac0, xFrac1 = 1, 0
|
xFrac0, xFrac1 = 1, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s00 := $srcf[sr.Min.X + int(sx0), sr.Min.Y + int(sy0)]
|
||||||
|
s10 := $srcf[sr.Min.X + int(sx1), sr.Min.Y + int(sy0)]
|
||||||
|
$blend[xFrac1, s00, xFrac0, s10]
|
||||||
|
s01 := $srcf[sr.Min.X + int(sx0), sr.Min.Y + int(sy1)]
|
||||||
|
s11 := $srcf[sr.Min.X + int(sx1), sr.Min.Y + int(sy1)]
|
||||||
|
$blend[xFrac1, s01, xFrac0, s11]
|
||||||
|
$blend[yFrac1, s10, yFrac0, s11]
|
||||||
|
$outputu[dr.Min.X + int(dx), dr.Min.Y + int(dy), s11]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
codeABLTransformLeaf = `
|
||||||
|
func (ablInterpolator) transform_$dTypeRN_$sTypeRN(dst $dType, dr, adr image.Rectangle, d2s *f64.Aff3, src $sType, sr image.Rectangle) {
|
||||||
|
$preOuter
|
||||||
|
for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ {
|
||||||
|
dyf := float64(dr.Min.Y + int(dy)) + 0.5
|
||||||
|
$preInner
|
||||||
|
$tweakDx for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx++ {
|
||||||
|
dxf := float64(dr.Min.X + int(dx)) + 0.5
|
||||||
|
sx := d2s[0]*dxf + d2s[1]*dyf + d2s[2]
|
||||||
|
sy := d2s[3]*dxf + d2s[4]*dyf + d2s[5]
|
||||||
|
if !(image.Point{int(math.Floor(sx)), int(math.Floor(sy))}).In(sr) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sx -= 0.5
|
||||||
|
sxf := math.Floor(sx)
|
||||||
|
xFrac0 := sx - sxf
|
||||||
|
xFrac1 := 1 - xFrac0
|
||||||
|
sx0 := int(sxf)
|
||||||
|
sx1 := sx0 + 1
|
||||||
|
if sx0 < sr.Min.X {
|
||||||
|
sx0, sx1 = sr.Min.X, sr.Min.X
|
||||||
|
xFrac0, xFrac1 = 0, 1
|
||||||
|
} else if sx1 >= sr.Max.X {
|
||||||
|
sx0, sx1 = sr.Max.X-1, sr.Max.X-1
|
||||||
|
xFrac0, xFrac1 = 1, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
sy -= 0.5
|
||||||
|
syf := math.Floor(sy)
|
||||||
|
yFrac0 := sy - syf
|
||||||
|
yFrac1 := 1 - yFrac0
|
||||||
|
sy0 := int(syf)
|
||||||
|
sy1 := sy0 + 1
|
||||||
|
if sy0 < sr.Min.Y {
|
||||||
|
sy0, sy1 = sr.Min.Y, sr.Min.Y
|
||||||
|
yFrac0, yFrac1 = 0, 1
|
||||||
|
} else if sy1 >= sr.Max.Y {
|
||||||
|
sy0, sy1 = sr.Max.Y-1, sr.Max.Y-1
|
||||||
|
yFrac0, yFrac1 = 1, 0
|
||||||
|
}
|
||||||
|
|
||||||
s00 := $srcf[sx0, sy0]
|
s00 := $srcf[sx0, sy0]
|
||||||
s10 := $srcf[sx1, sy0]
|
s10 := $srcf[sx1, sy0]
|
||||||
$blend[xFrac1, s00, xFrac0, s10]
|
$blend[xFrac1, s00, xFrac0, s10]
|
||||||
|
@ -493,7 +590,7 @@ const (
|
||||||
s11 := $srcf[sx1, sy1]
|
s11 := $srcf[sx1, sy1]
|
||||||
$blend[xFrac1, s01, xFrac0, s11]
|
$blend[xFrac1, s01, xFrac0, s11]
|
||||||
$blend[yFrac1, s10, yFrac0, s11]
|
$blend[yFrac1, s10, yFrac0, s11]
|
||||||
$outputu[dx, dy, s11]
|
$outputu[dr.Min.X + int(dx), dr.Min.Y + int(dy), s11]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -540,7 +637,7 @@ const (
|
||||||
for _, s := range z.horizontal.sources {
|
for _, s := range z.horizontal.sources {
|
||||||
var pr, pg, pb, pa float64
|
var pr, pg, pb, pa float64
|
||||||
for _, c := range z.horizontal.contribs[s.i:s.j] {
|
for _, c := range z.horizontal.contribs[s.i:s.j] {
|
||||||
p += $srcf[c.coord, y] * c.weight
|
p += $srcf[sr.Min.X + int(c.coord), sr.Min.Y + int(y)] * c.weight
|
||||||
}
|
}
|
||||||
tmp[t] = [4]float64{
|
tmp[t] = [4]float64{
|
||||||
pr * s.invTotalWeightFFFF,
|
pr * s.invTotalWeightFFFF,
|
||||||
|
@ -568,7 +665,7 @@ const (
|
||||||
pb += p[2] * c.weight
|
pb += p[2] * c.weight
|
||||||
pa += p[3] * c.weight
|
pa += p[3] * c.weight
|
||||||
}
|
}
|
||||||
$outputf[dx, adr.Min.Y+dy, p, s.invTotalWeight]
|
$outputf[dr.Min.X + int(dx), dr.Min.Y + int(adr.Min.Y + dy), p, s.invTotalWeight]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
888
draw/impl.go
888
draw/impl.go
File diff suppressed because it is too large
Load Diff
|
@ -249,3 +249,72 @@ func ftou(f float64) uint16 {
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// invert returns the inverse of m.
|
||||||
|
//
|
||||||
|
// TODO: move this into the f64 package, once we work out the convention for
|
||||||
|
// matrix methods in that package: do they modify the receiver, take a dst
|
||||||
|
// pointer argument, or return a new value?
|
||||||
|
func invert(m *f64.Aff3) f64.Aff3 {
|
||||||
|
m00 := +m[3*1+1]
|
||||||
|
m01 := -m[3*0+1]
|
||||||
|
m02 := +m[3*1+2]*m[3*0+1] - m[3*1+1]*m[3*0+2]
|
||||||
|
m10 := -m[3*1+0]
|
||||||
|
m11 := +m[3*0+0]
|
||||||
|
m12 := +m[3*1+0]*m[3*0+2] - m[3*1+2]*m[3*0+0]
|
||||||
|
|
||||||
|
det := m00*m11 - m10*m01
|
||||||
|
|
||||||
|
return f64.Aff3{
|
||||||
|
m00 / det,
|
||||||
|
m01 / det,
|
||||||
|
m02 / det,
|
||||||
|
m10 / det,
|
||||||
|
m11 / det,
|
||||||
|
m12 / det,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// transformRect returns a rectangle dr that contains sr transformed by s2d.
|
||||||
|
func transformRect(s2d *f64.Aff3, sr *image.Rectangle) (dr image.Rectangle) {
|
||||||
|
ps := [...]image.Point{
|
||||||
|
{sr.Min.X, sr.Min.Y},
|
||||||
|
{sr.Max.X, sr.Min.Y},
|
||||||
|
{sr.Min.X, sr.Max.Y},
|
||||||
|
{sr.Max.X, sr.Max.Y},
|
||||||
|
}
|
||||||
|
for i, p := range ps {
|
||||||
|
sxf := float64(p.X)
|
||||||
|
syf := float64(p.Y)
|
||||||
|
dx := int(math.Floor(s2d[0]*sxf + s2d[1]*syf + s2d[2]))
|
||||||
|
dy := int(math.Floor(s2d[3]*sxf + s2d[4]*syf + s2d[5]))
|
||||||
|
|
||||||
|
// The +1 adjustments below are because an image.Rectangle is inclusive
|
||||||
|
// on the low end but exclusive on the high end.
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
dr = image.Rectangle{
|
||||||
|
Min: image.Point{dx + 0, dy + 0},
|
||||||
|
Max: image.Point{dx + 1, dy + 1},
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if dr.Min.X > dx {
|
||||||
|
dr.Min.X = dx
|
||||||
|
}
|
||||||
|
dx++
|
||||||
|
if dr.Max.X < dx {
|
||||||
|
dr.Max.X = dx
|
||||||
|
}
|
||||||
|
|
||||||
|
if dr.Min.Y > dy {
|
||||||
|
dr.Min.Y = dy
|
||||||
|
}
|
||||||
|
dy++
|
||||||
|
if dr.Max.Y < dy {
|
||||||
|
dr.Max.Y = dy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dr
|
||||||
|
}
|
||||||
|
|
|
@ -16,18 +16,28 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/image/math/f64"
|
||||||
|
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var genScaleFiles = flag.Bool("gen_scale_files", false, "whether to generate the TestScaleXxx golden files.")
|
var genGoldenFiles = flag.Bool("gen_golden_files", false, "whether to generate the TestXxx golden files.")
|
||||||
|
|
||||||
// testScale tests that scaling the source image gives the exact destination
|
var transformMatrix = func() *f64.Aff3 {
|
||||||
// image. This is to ensure that any refactoring or optimization of the scaling
|
const scale, cos30, sin30 = 3.75, 0.866025404, 0.5
|
||||||
// code doesn't change the scaling behavior. Changing the actual algorithm or
|
return &f64.Aff3{
|
||||||
// kernel used by any particular quality setting will obviously change the
|
+scale * cos30, -scale * sin30, 40,
|
||||||
// resultant pixels. In such a case, use the gen_scale_files flag to regenerate
|
+scale * sin30, +scale * cos30, 10,
|
||||||
// the golden files.
|
}
|
||||||
func testScale(t *testing.T, w int, h int, direction, srcFilename string) {
|
}()
|
||||||
|
|
||||||
|
// testInterp tests that interpolating the source image gives the exact
|
||||||
|
// destination image. This is to ensure that any refactoring or optimization of
|
||||||
|
// the interpolation code doesn't change the 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_golden_files flag
|
||||||
|
// to regenerate the golden files.
|
||||||
|
func testInterp(t *testing.T, w int, h int, direction, srcFilename string) {
|
||||||
f, err := os.Open("../testdata/go-turns-two-" + srcFilename)
|
f, err := os.Open("../testdata/go-turns-two-" + srcFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Open: %v", err)
|
t.Fatalf("Open: %v", err)
|
||||||
|
@ -44,12 +54,21 @@ func testScale(t *testing.T, w int, h int, direction, srcFilename string) {
|
||||||
"cr": CatmullRom,
|
"cr": CatmullRom,
|
||||||
}
|
}
|
||||||
for name, q := range testCases {
|
for name, q := range testCases {
|
||||||
gotFilename := fmt.Sprintf("../testdata/go-turns-two-%s-%s.png", direction, name)
|
goldenFilename := fmt.Sprintf("../testdata/go-turns-two-%s-%s.png", direction, name)
|
||||||
|
|
||||||
got := image.NewRGBA(image.Rect(0, 0, w, h))
|
got := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||||
q.Scale(got, got.Bounds(), src, src.Bounds(), nil)
|
if direction == "rotate" {
|
||||||
if *genScaleFiles {
|
if name == "bl" || name == "cr" {
|
||||||
g, err := os.Create(gotFilename)
|
// TODO: implement Kernel.Transform.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
q.Transform(got, transformMatrix, src, src.Bounds(), nil)
|
||||||
|
} else {
|
||||||
|
q.Scale(got, got.Bounds(), src, src.Bounds(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *genGoldenFiles {
|
||||||
|
g, err := os.Create(goldenFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Create: %v", err)
|
t.Errorf("Create: %v", err)
|
||||||
continue
|
continue
|
||||||
|
@ -62,27 +81,35 @@ func testScale(t *testing.T, w int, h int, direction, srcFilename string) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
g, err := os.Open(gotFilename)
|
g, err := os.Open(goldenFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Open: %v", err)
|
t.Errorf("Open: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
defer g.Close()
|
defer g.Close()
|
||||||
want, err := png.Decode(g)
|
wantRaw, err := png.Decode(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Decode: %v", err)
|
t.Errorf("Decode: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// convert wantRaw to RGBA.
|
||||||
|
want, ok := wantRaw.(*image.RGBA)
|
||||||
|
if !ok {
|
||||||
|
b := wantRaw.Bounds()
|
||||||
|
want = image.NewRGBA(b)
|
||||||
|
Draw(want, b, wantRaw, b.Min, Src)
|
||||||
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
if !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("%s: actual image differs from golden image", gotFilename)
|
t.Errorf("%s: actual image differs from golden image", goldenFilename)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestScaleDown(t *testing.T) { testScale(t, 100, 100, "down", "280x360.jpeg") }
|
func TestScaleDown(t *testing.T) { testInterp(t, 100, 100, "down", "280x360.jpeg") }
|
||||||
func TestScaleUp(t *testing.T) { testScale(t, 75, 100, "up", "14x18.png") }
|
func TestScaleUp(t *testing.T) { testInterp(t, 75, 100, "up", "14x18.png") }
|
||||||
|
func TestTransform(t *testing.T) { testInterp(t, 100, 100, "rotate", "14x18.png") }
|
||||||
|
|
||||||
func fillPix(r *rand.Rand, pixs ...[]byte) {
|
func fillPix(r *rand.Rand, pixs ...[]byte) {
|
||||||
for _, pix := range pixs {
|
for _, pix := range pixs {
|
||||||
|
@ -92,7 +119,7 @@ func fillPix(r *rand.Rand, pixs ...[]byte) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestScaleClipCommute(t *testing.T) {
|
func TestInterpClipCommute(t *testing.T) {
|
||||||
src := image.NewNRGBA(image.Rect(0, 0, 20, 20))
|
src := image.NewNRGBA(image.Rect(0, 0, 20, 20))
|
||||||
fillPix(rand.New(rand.NewSource(0)), src.Pix)
|
fillPix(rand.New(rand.NewSource(0)), src.Pix)
|
||||||
|
|
||||||
|
@ -103,28 +130,46 @@ func TestScaleClipCommute(t *testing.T) {
|
||||||
ApproxBiLinear,
|
ApproxBiLinear,
|
||||||
CatmullRom,
|
CatmullRom,
|
||||||
}
|
}
|
||||||
for _, q := range qs {
|
for _, transform := range []bool{false, true} {
|
||||||
dst0 := image.NewRGBA(image.Rect(1, 1, 10, 10))
|
for _, q := range qs {
|
||||||
dst1 := image.NewRGBA(image.Rect(1, 1, 10, 10))
|
if transform && q == CatmullRom {
|
||||||
for i := range dst0.Pix {
|
// TODO: implement Kernel.Transform.
|
||||||
dst0.Pix[i] = uint8(i / 4)
|
continue
|
||||||
dst1.Pix[i] = uint8(i / 4)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Scale then clip.
|
dst0 := image.NewRGBA(image.Rect(1, 1, 10, 10))
|
||||||
q.Scale(dst0, outer, src, src.Bounds(), nil)
|
dst1 := image.NewRGBA(image.Rect(1, 1, 10, 10))
|
||||||
dst0 = dst0.SubImage(inner).(*image.RGBA)
|
for i := range dst0.Pix {
|
||||||
|
dst0.Pix[i] = uint8(i / 4)
|
||||||
|
dst1.Pix[i] = uint8(i / 4)
|
||||||
|
}
|
||||||
|
|
||||||
// Clip then scale.
|
var interp func(dst *image.RGBA)
|
||||||
dst1 = dst1.SubImage(inner).(*image.RGBA)
|
if transform {
|
||||||
q.Scale(dst1, outer, src, src.Bounds(), nil)
|
interp = func(dst *image.RGBA) {
|
||||||
|
q.Transform(dst, transformMatrix, src, src.Bounds(), nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
interp = func(dst *image.RGBA) {
|
||||||
|
q.Scale(dst, outer, src, src.Bounds(), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loop:
|
// Interpolate then clip.
|
||||||
for y := inner.Min.Y; y < inner.Max.Y; y++ {
|
interp(dst0)
|
||||||
for x := inner.Min.X; x < inner.Max.X; x++ {
|
dst0 = dst0.SubImage(inner).(*image.RGBA)
|
||||||
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)
|
// Clip then interpolate.
|
||||||
break loop
|
dst1 = dst1.SubImage(inner).(*image.RGBA)
|
||||||
|
interp(dst1)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,7 +229,7 @@ func TestSrcTranslationInvariance(t *testing.T) {
|
||||||
t.Errorf("pix differ for delta=%v, q=%T", delta, q)
|
t.Errorf("pix differ for delta=%v, q=%T", delta, q)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Transform.
|
// TODO: Transform, once Kernel.Transform is implemented.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,6 +295,8 @@ func TestFastPaths(t *testing.T) {
|
||||||
if !bytes.Equal(dst0.Pix, dst1.Pix) {
|
if !bytes.Equal(dst0.Pix, dst1.Pix) {
|
||||||
t.Errorf("pix differ for dr=%v, src=%T, sr=%v, q=%T", dr, src, sr, q)
|
t.Errorf("pix differ for dr=%v, src=%T, sr=%v, q=%T", dr, src, sr, q)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Transform, once Kernel.Transform is implemented.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -331,6 +378,20 @@ func benchScale(b *testing.B, srcf func(image.Rectangle) (image.Image, error), w
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func benchTform(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)
|
||||||
|
}
|
||||||
|
sr := src.Bounds()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
q.Transform(dst, transformMatrix, src, sr, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkScaleLargeDownNN(b *testing.B) { benchScale(b, srcYCbCrLarge, 200, 150, NearestNeighbor) }
|
func BenchmarkScaleLargeDownNN(b *testing.B) { benchScale(b, srcYCbCrLarge, 200, 150, NearestNeighbor) }
|
||||||
func BenchmarkScaleLargeDownAB(b *testing.B) { benchScale(b, srcYCbCrLarge, 200, 150, ApproxBiLinear) }
|
func BenchmarkScaleLargeDownAB(b *testing.B) { benchScale(b, srcYCbCrLarge, 200, 150, ApproxBiLinear) }
|
||||||
func BenchmarkScaleLargeDownBL(b *testing.B) { benchScale(b, srcYCbCrLarge, 200, 150, BiLinear) }
|
func BenchmarkScaleLargeDownBL(b *testing.B) { benchScale(b, srcYCbCrLarge, 200, 150, BiLinear) }
|
||||||
|
@ -351,3 +412,9 @@ func BenchmarkScaleSrcNRGBA(b *testing.B) { benchScale(b, srcNRGBA, 200, 150,
|
||||||
func BenchmarkScaleSrcRGBA(b *testing.B) { benchScale(b, srcRGBA, 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 BenchmarkScaleSrcUniform(b *testing.B) { benchScale(b, srcUniform, 200, 150, ApproxBiLinear) }
|
||||||
func BenchmarkScaleSrcYCbCr(b *testing.B) { benchScale(b, srcYCbCr, 200, 150, ApproxBiLinear) }
|
func BenchmarkScaleSrcYCbCr(b *testing.B) { benchScale(b, srcYCbCr, 200, 150, ApproxBiLinear) }
|
||||||
|
|
||||||
|
func BenchmarkTformSrcGray(b *testing.B) { benchTform(b, srcGray, 200, 150, ApproxBiLinear) }
|
||||||
|
func BenchmarkTformSrcNRGBA(b *testing.B) { benchTform(b, srcNRGBA, 200, 150, ApproxBiLinear) }
|
||||||
|
func BenchmarkTformSrcRGBA(b *testing.B) { benchTform(b, srcRGBA, 200, 150, ApproxBiLinear) }
|
||||||
|
func BenchmarkTformSrcUniform(b *testing.B) { benchTform(b, srcUniform, 200, 150, ApproxBiLinear) }
|
||||||
|
func BenchmarkTformSrcYCbCr(b *testing.B) { benchTform(b, srcYCbCr, 200, 150, ApproxBiLinear) }
|
||||||
|
|
BIN
testdata/go-turns-two-rotate-ab.png
vendored
Normal file
BIN
testdata/go-turns-two-rotate-ab.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
BIN
testdata/go-turns-two-rotate-nn.png
vendored
Normal file
BIN
testdata/go-turns-two-rotate-nn.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.9 KiB |
Loading…
Reference in New Issue
Block a user