From a71fdfe7d19a3f37eb9909051cfcc7036ee78305 Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Tue, 17 Mar 2015 18:46:52 +1100 Subject: [PATCH] draw: implement Kernel.Transform. Also fix the NN and ABL fast paths to only apply if we can access the Pix elements without src-bounds checking. Change-Id: Ie9fc96b28e0665df49d00c4c53cb81385faee4db Reviewed-on: https://go-review.googlesource.com/7675 Reviewed-by: Rob Pike --- draw/gen.go | 182 ++++++- draw/impl.go | 813 ++++++++++++++++++++++++++-- draw/scale.go | 63 ++- draw/scale_test.go | 130 +++-- testdata/go-turns-two-rotate-bl.png | Bin 0 -> 8764 bytes testdata/go-turns-two-rotate-cr.png | Bin 0 -> 9000 bytes 6 files changed, 1064 insertions(+), 124 deletions(-) create mode 100644 testdata/go-turns-two-rotate-bl.png create mode 100644 testdata/go-turns-two-rotate-cr.png diff --git a/draw/gen.go b/draw/gen.go index ced1c9a..b429465 100644 --- a/draw/gen.go +++ b/draw/gen.go @@ -116,6 +116,12 @@ func genKernel(w *bytes.Buffer) { dType: dType, }) } + for _, t := range dsTypes { + expn(w, codeKernelTransformLeaf, &data{ + dType: t.dType, + sType: t.sType, + }) + } } func expn(w *bytes.Buffer, code string, d *data) { @@ -154,6 +160,9 @@ func expnLine(line string, d *data) string { return line } +// expnDollar expands a "$foo" fragment in a line of generated code. It returns +// the empty string if there was a problem. It returns ";" if the generated +// code is a no-op. func expnDollar(prefix, dollar, suffix string, d *data) string { switch dollar { case "dType": @@ -246,32 +255,39 @@ func expnDollar(prefix, dollar, suffix string, d *data) string { case "outputf": args, _ := splitArgs(suffix) - if len(args) != 4 { + if len(args) != 5 { return "" } + ret := "" switch d.dType { default: log.Fatalf("bad dType %q", d.dType) case "Image": - return fmt.Sprintf(""+ - "dstColorRGBA64.R = ftou(%sr * %s)\n"+ - "dstColorRGBA64.G = ftou(%sg * %s)\n"+ - "dstColorRGBA64.B = ftou(%sb * %s)\n"+ - "dstColorRGBA64.A = ftou(%sa * %s)\n"+ + ret = fmt.Sprintf(""+ + "dstColorRGBA64.R = %s(%sr * %s)\n"+ + "dstColorRGBA64.G = %s(%sg * %s)\n"+ + "dstColorRGBA64.B = %s(%sb * %s)\n"+ + "dstColorRGBA64.A = %s(%sa * %s)\n"+ "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[4], + args[2], args[3], args[4], + args[2], args[3], args[4], + args[2], args[3], args[4], args[0], args[1], ) case "*image.RGBA": - return fmt.Sprintf(""+ - "dst.Pix[d+0] = uint8(ftou(%sr * %s) >> 8)\n"+ - "dst.Pix[d+1] = uint8(ftou(%sg * %s) >> 8)\n"+ - "dst.Pix[d+2] = uint8(ftou(%sb * %s) >> 8)\n"+ - "dst.Pix[d+3] = uint8(ftou(%sa * %s) >> 8)\n"+ - "d += dst.Stride", - args[2], args[3], args[2], args[3], args[2], args[3], args[2], args[3], + ret = fmt.Sprintf(""+ + "dst.Pix[d+0] = uint8(%s(%sr * %s) >> 8)\n"+ + "dst.Pix[d+1] = uint8(%s(%sg * %s) >> 8)\n"+ + "dst.Pix[d+2] = uint8(%s(%sb * %s) >> 8)\n"+ + "dst.Pix[d+3] = uint8(%s(%sa * %s) >> 8)", + args[2], args[3], args[4], + args[2], args[3], args[4], + args[2], args[3], args[4], + args[2], args[3], args[4], ) } + return strings.Replace(ret, " * 1)", ")", -1) case "srcf", "srcu": lhs, eqOp := splitEq(prefix) @@ -329,6 +345,12 @@ func expnDollar(prefix, dollar, suffix string, d *data) string { return strings.TrimSpace(buf.String()) + case "tweakD": + if d.dType == "*image.RGBA" { + return "d += dst.Stride" + } + return ";" + case "tweakDx": if d.dType == "*image.RGBA" { return strings.Replace(suffix, "dx++", "dx, d = dx+1, d+4", 1) @@ -444,7 +466,14 @@ const ( return } d2s := invert(s2d) - $switch z.transform_$dTypeRN_$sTypeRN(dst, dr, adr, &d2s, src, sr) + // sr is the source pixels. If it extends beyond the src bounds, + // we cannot use the type-specific fast paths, as they access + // the Pix fields directly without bounds checking. + if !sr.In(src.Bounds()) { + z.transform_Image_Image(dst, dr, adr, &d2s, src, sr) + } else { + $switch z.transform_$dTypeRN_$sTypeRN(dst, dr, adr, &d2s, src, sr) + } } ` @@ -475,6 +504,7 @@ const ( $preInner $tweakDx for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx++ { dxf := float64(dr.Min.X + int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -549,6 +579,7 @@ const ( $preInner $tweakDx for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx++ { dxf := float64(dr.Min.X + int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -625,8 +656,32 @@ const ( $switchD z.scaleY_$dTypeRN(dst, dr, adr, tmp) } - func (z *Kernel) Transform(dst Image, m *f64.Aff3, src image.Image, sr image.Rectangle, opts *Options) { - panic("unimplemented") + func (q *Kernel) Transform(dst Image, s2d *f64.Aff3, src image.Image, sr image.Rectangle, opts *Options) { + 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) + + xscale := abs(d2s[0]) + if s := abs(d2s[1]); xscale < s { + xscale = s + } + yscale := abs(d2s[3]) + if s := abs(d2s[4]); yscale < s { + yscale = s + } + + // sr is the source pixels. If it extends beyond the src bounds, + // we cannot use the type-specific fast paths, as they access + // the Pix fields directly without bounds checking. + if !sr.In(src.Bounds()) { + q.transform_Image_Image(dst, dr, adr, &d2s, src, sr, xscale, yscale) + } else { + $switch q.transform_$dTypeRN_$sTypeRN(dst, dr, adr, &d2s, src, sr, xscale, yscale) + } } ` @@ -665,7 +720,98 @@ const ( pb += p[2] * c.weight pa += p[3] * c.weight } - $outputf[dr.Min.X + int(dx), dr.Min.Y + int(adr.Min.Y + dy), p, s.invTotalWeight] + $outputf[dr.Min.X + int(dx), dr.Min.Y + int(adr.Min.Y + dy), ftou, p, s.invTotalWeight] + $tweakD + } + } + } + ` + + codeKernelTransformLeaf = ` + func (q *Kernel) transform_$dTypeRN_$sTypeRN(dst $dType, dr, adr image.Rectangle, d2s *f64.Aff3, src $sType, sr image.Rectangle, xscale, yscale float64) { + // When shrinking, broaden the effective kernel support so that we still + // visit every source pixel. + xHalfWidth, xKernelArgScale := q.Support, 1.0 + if xscale > 1 { + xHalfWidth *= xscale + xKernelArgScale = 1 / xscale + } + yHalfWidth, yKernelArgScale := q.Support, 1.0 + if yscale > 1 { + yHalfWidth *= yscale + yKernelArgScale = 1 / yscale + } + + xWeights := make([]float64, 1 + 2*int(math.Ceil(xHalfWidth))) + yWeights := make([]float64, 1 + 2*int(math.Ceil(yHalfWidth))) + + $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 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). + 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 + ix := int(math.Floor(sx - xHalfWidth)) + if ix < sr.Min.X { + ix = sr.Min.X + } + jx := int(math.Ceil(sx + xHalfWidth)) + if jx > sr.Max.X { + jx = sr.Max.X + } + + totalXWeight := 0.0 + for kx := ix; kx < jx; kx++ { + xWeight := 0.0 + if t := abs((sx - float64(kx)) * xKernelArgScale); t < q.Support { + xWeight = q.At(t) + } + xWeights[kx - ix] = xWeight + totalXWeight += xWeight + } + for x := range xWeights[:jx-ix] { + xWeights[x] /= totalXWeight + } + + sy -= 0.5 + iy := int(math.Floor(sy - yHalfWidth)) + if iy < sr.Min.Y { + iy = sr.Min.Y + } + jy := int(math.Ceil(sy + yHalfWidth)) + if jy > sr.Max.Y { + jy = sr.Max.Y + } + + totalYWeight := 0.0 + for ky := iy; ky < jy; ky++ { + yWeight := 0.0 + if t := abs((sy - float64(ky)) * yKernelArgScale); t < q.Support { + yWeight = q.At(t) + } + yWeights[ky - iy] = yWeight + totalYWeight += yWeight + } + for y := range yWeights[:jy-iy] { + yWeights[y] /= totalYWeight + } + + var pr, pg, pb, pa float64 + for ky := iy; ky < jy; ky++ { + yWeight := yWeights[ky - iy] + for kx := ix; kx < jx; kx++ { + p += $srcf[kx, ky] * xWeights[kx - ix] * yWeight + } + } + $outputf[dr.Min.X + int(dx), dr.Min.Y + int(dy), fffftou, p, 1] } } } diff --git a/draw/impl.go b/draw/impl.go index e6f215a..8ebdd6f 100644 --- a/draw/impl.go +++ b/draw/impl.go @@ -55,26 +55,33 @@ func (z nnInterpolator) Transform(dst Image, s2d *f64.Aff3, src image.Image, sr return } d2s := invert(s2d) - switch dst := dst.(type) { - case *image.RGBA: - switch src := src.(type) { - case *image.Gray: - z.transform_RGBA_Gray(dst, dr, adr, &d2s, src, sr) - case *image.NRGBA: - z.transform_RGBA_NRGBA(dst, dr, adr, &d2s, src, sr) + // sr is the source pixels. If it extends beyond the src bounds, + // we cannot use the type-specific fast paths, as they access + // the Pix fields directly without bounds checking. + if !sr.In(src.Bounds()) { + z.transform_Image_Image(dst, dr, adr, &d2s, src, sr) + } else { + switch dst := dst.(type) { case *image.RGBA: - z.transform_RGBA_RGBA(dst, dr, adr, &d2s, src, sr) - case *image.Uniform: - z.transform_RGBA_Uniform(dst, dr, adr, &d2s, src, sr) - case *image.YCbCr: - z.transform_RGBA_YCbCr(dst, dr, adr, &d2s, src, sr) + switch src := src.(type) { + case *image.Gray: + z.transform_RGBA_Gray(dst, dr, adr, &d2s, src, sr) + case *image.NRGBA: + z.transform_RGBA_NRGBA(dst, dr, adr, &d2s, src, sr) + case *image.RGBA: + z.transform_RGBA_RGBA(dst, dr, adr, &d2s, src, sr) + case *image.Uniform: + z.transform_RGBA_Uniform(dst, dr, adr, &d2s, src, sr) + case *image.YCbCr: + z.transform_RGBA_YCbCr(dst, dr, adr, &d2s, src, sr) + default: + z.transform_RGBA_Image(dst, dr, adr, &d2s, src, sr) + } default: - z.transform_RGBA_Image(dst, dr, adr, &d2s, src, sr) - } - default: - switch src := src.(type) { - default: - z.transform_Image_Image(dst, dr, adr, &d2s, src, sr) + switch src := src.(type) { + default: + z.transform_Image_Image(dst, dr, adr, &d2s, src, sr) + } } } } @@ -224,6 +231,7 @@ func (nnInterpolator) transform_RGBA_Gray(dst *image.RGBA, dr, adr image.Rectang d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -244,6 +252,7 @@ func (nnInterpolator) transform_RGBA_NRGBA(dst *image.RGBA, dr, adr image.Rectan d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -264,6 +273,7 @@ func (nnInterpolator) transform_RGBA_RGBA(dst *image.RGBA, dr, adr image.Rectang d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -288,6 +298,7 @@ func (nnInterpolator) transform_RGBA_Uniform(dst *image.RGBA, dr, adr image.Rect d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -308,6 +319,7 @@ func (nnInterpolator) transform_RGBA_YCbCr(dst *image.RGBA, dr, adr image.Rectan d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -328,6 +340,7 @@ func (nnInterpolator) transform_RGBA_Image(dst *image.RGBA, dr, adr image.Rectan d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -349,6 +362,7 @@ func (nnInterpolator) transform_Image_Image(dst Image, dr, adr image.Rectangle, dyf := float64(dr.Min.Y+int(dy)) + 0.5 for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx++ { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -409,26 +423,33 @@ func (z ablInterpolator) Transform(dst Image, s2d *f64.Aff3, src image.Image, sr return } d2s := invert(s2d) - switch dst := dst.(type) { - case *image.RGBA: - switch src := src.(type) { - case *image.Gray: - z.transform_RGBA_Gray(dst, dr, adr, &d2s, src, sr) - case *image.NRGBA: - z.transform_RGBA_NRGBA(dst, dr, adr, &d2s, src, sr) + // sr is the source pixels. If it extends beyond the src bounds, + // we cannot use the type-specific fast paths, as they access + // the Pix fields directly without bounds checking. + if !sr.In(src.Bounds()) { + z.transform_Image_Image(dst, dr, adr, &d2s, src, sr) + } else { + switch dst := dst.(type) { case *image.RGBA: - z.transform_RGBA_RGBA(dst, dr, adr, &d2s, src, sr) - case *image.Uniform: - z.transform_RGBA_Uniform(dst, dr, adr, &d2s, src, sr) - case *image.YCbCr: - z.transform_RGBA_YCbCr(dst, dr, adr, &d2s, src, sr) + switch src := src.(type) { + case *image.Gray: + z.transform_RGBA_Gray(dst, dr, adr, &d2s, src, sr) + case *image.NRGBA: + z.transform_RGBA_NRGBA(dst, dr, adr, &d2s, src, sr) + case *image.RGBA: + z.transform_RGBA_RGBA(dst, dr, adr, &d2s, src, sr) + case *image.Uniform: + z.transform_RGBA_Uniform(dst, dr, adr, &d2s, src, sr) + case *image.YCbCr: + z.transform_RGBA_YCbCr(dst, dr, adr, &d2s, src, sr) + default: + z.transform_RGBA_Image(dst, dr, adr, &d2s, src, sr) + } default: - z.transform_RGBA_Image(dst, dr, adr, &d2s, src, sr) - } - default: - switch src := src.(type) { - default: - z.transform_Image_Image(dst, dr, adr, &d2s, src, sr) + switch src := src.(type) { + default: + z.transform_Image_Image(dst, dr, adr, &d2s, src, sr) + } } } } @@ -1010,6 +1031,7 @@ func (ablInterpolator) transform_RGBA_Gray(dst *image.RGBA, dr, adr image.Rectan d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -1090,6 +1112,7 @@ func (ablInterpolator) transform_RGBA_NRGBA(dst *image.RGBA, dr, adr image.Recta d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -1170,6 +1193,7 @@ func (ablInterpolator) transform_RGBA_RGBA(dst *image.RGBA, dr, adr image.Rectan d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -1266,6 +1290,7 @@ func (ablInterpolator) transform_RGBA_Uniform(dst *image.RGBA, dr, adr image.Rec d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -1346,6 +1371,7 @@ func (ablInterpolator) transform_RGBA_YCbCr(dst *image.RGBA, dr, adr image.Recta d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -1426,6 +1452,7 @@ func (ablInterpolator) transform_RGBA_Image(dst *image.RGBA, dr, adr image.Recta d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -1507,6 +1534,7 @@ func (ablInterpolator) transform_Image_Image(dst Image, dr, adr image.Rectangle, dyf := float64(dr.Min.Y+int(dy)) + 0.5 for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx++ { dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). 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) { @@ -1628,8 +1656,53 @@ func (z *kernelScaler) Scale(dst Image, dr image.Rectangle, src image.Image, sr } } -func (z *Kernel) Transform(dst Image, m *f64.Aff3, src image.Image, sr image.Rectangle, opts *Options) { - panic("unimplemented") +func (q *Kernel) Transform(dst Image, s2d *f64.Aff3, src image.Image, sr image.Rectangle, opts *Options) { + 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) + + xscale := abs(d2s[0]) + if s := abs(d2s[1]); xscale < s { + xscale = s + } + yscale := abs(d2s[3]) + if s := abs(d2s[4]); yscale < s { + yscale = s + } + + // sr is the source pixels. If it extends beyond the src bounds, + // we cannot use the type-specific fast paths, as they access + // the Pix fields directly without bounds checking. + if !sr.In(src.Bounds()) { + q.transform_Image_Image(dst, dr, adr, &d2s, src, sr, xscale, yscale) + } else { + switch dst := dst.(type) { + case *image.RGBA: + switch src := src.(type) { + case *image.Gray: + q.transform_RGBA_Gray(dst, dr, adr, &d2s, src, sr, xscale, yscale) + case *image.NRGBA: + q.transform_RGBA_NRGBA(dst, dr, adr, &d2s, src, sr, xscale, yscale) + case *image.RGBA: + q.transform_RGBA_RGBA(dst, dr, adr, &d2s, src, sr, xscale, yscale) + case *image.Uniform: + q.transform_RGBA_Uniform(dst, dr, adr, &d2s, src, sr, xscale, yscale) + case *image.YCbCr: + q.transform_RGBA_YCbCr(dst, dr, adr, &d2s, src, sr, xscale, yscale) + default: + q.transform_RGBA_Image(dst, dr, adr, &d2s, src, sr, xscale, yscale) + } + default: + switch src := src.(type) { + default: + q.transform_Image_Image(dst, dr, adr, &d2s, src, sr, xscale, yscale) + } + } + } } func (z *kernelScaler) scaleX_Gray(tmp [][4]float64, src *image.Gray, sr image.Rectangle) { @@ -1816,3 +1889,667 @@ func (z *kernelScaler) scaleY_Image(dst Image, dr, adr image.Rectangle, tmp [][4 } } } + +func (q *Kernel) transform_RGBA_Gray(dst *image.RGBA, dr, adr image.Rectangle, d2s *f64.Aff3, src *image.Gray, sr image.Rectangle, xscale, yscale float64) { + // When shrinking, broaden the effective kernel support so that we still + // visit every source pixel. + xHalfWidth, xKernelArgScale := q.Support, 1.0 + if xscale > 1 { + xHalfWidth *= xscale + xKernelArgScale = 1 / xscale + } + yHalfWidth, yKernelArgScale := q.Support, 1.0 + if yscale > 1 { + yHalfWidth *= yscale + yKernelArgScale = 1 / yscale + } + + xWeights := make([]float64, 1+2*int(math.Ceil(xHalfWidth))) + yWeights := make([]float64, 1+2*int(math.Ceil(yHalfWidth))) + + for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ { + dyf := float64(dr.Min.Y+int(dy)) + 0.5 + d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) + for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { + dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). + 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 + ix := int(math.Floor(sx - xHalfWidth)) + if ix < sr.Min.X { + ix = sr.Min.X + } + jx := int(math.Ceil(sx + xHalfWidth)) + if jx > sr.Max.X { + jx = sr.Max.X + } + + totalXWeight := 0.0 + for kx := ix; kx < jx; kx++ { + xWeight := 0.0 + if t := abs((sx - float64(kx)) * xKernelArgScale); t < q.Support { + xWeight = q.At(t) + } + xWeights[kx-ix] = xWeight + totalXWeight += xWeight + } + for x := range xWeights[:jx-ix] { + xWeights[x] /= totalXWeight + } + + sy -= 0.5 + iy := int(math.Floor(sy - yHalfWidth)) + if iy < sr.Min.Y { + iy = sr.Min.Y + } + jy := int(math.Ceil(sy + yHalfWidth)) + if jy > sr.Max.Y { + jy = sr.Max.Y + } + + totalYWeight := 0.0 + for ky := iy; ky < jy; ky++ { + yWeight := 0.0 + if t := abs((sy - float64(ky)) * yKernelArgScale); t < q.Support { + yWeight = q.At(t) + } + yWeights[ky-iy] = yWeight + totalYWeight += yWeight + } + for y := range yWeights[:jy-iy] { + yWeights[y] /= totalYWeight + } + + var pr, pg, pb, pa float64 + for ky := iy; ky < jy; ky++ { + yWeight := yWeights[ky-iy] + for kx := ix; kx < jx; kx++ { + pru, pgu, pbu, pau := src.At(kx, ky).RGBA() + pr += float64(pru) * xWeights[kx-ix] * yWeight + pg += float64(pgu) * xWeights[kx-ix] * yWeight + pb += float64(pbu) * xWeights[kx-ix] * yWeight + pa += float64(pau) * xWeights[kx-ix] * yWeight + } + } + dst.Pix[d+0] = uint8(fffftou(pr) >> 8) + dst.Pix[d+1] = uint8(fffftou(pg) >> 8) + dst.Pix[d+2] = uint8(fffftou(pb) >> 8) + dst.Pix[d+3] = uint8(fffftou(pa) >> 8) + } + } +} + +func (q *Kernel) transform_RGBA_NRGBA(dst *image.RGBA, dr, adr image.Rectangle, d2s *f64.Aff3, src *image.NRGBA, sr image.Rectangle, xscale, yscale float64) { + // When shrinking, broaden the effective kernel support so that we still + // visit every source pixel. + xHalfWidth, xKernelArgScale := q.Support, 1.0 + if xscale > 1 { + xHalfWidth *= xscale + xKernelArgScale = 1 / xscale + } + yHalfWidth, yKernelArgScale := q.Support, 1.0 + if yscale > 1 { + yHalfWidth *= yscale + yKernelArgScale = 1 / yscale + } + + xWeights := make([]float64, 1+2*int(math.Ceil(xHalfWidth))) + yWeights := make([]float64, 1+2*int(math.Ceil(yHalfWidth))) + + for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ { + dyf := float64(dr.Min.Y+int(dy)) + 0.5 + d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) + for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { + dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). + 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 + ix := int(math.Floor(sx - xHalfWidth)) + if ix < sr.Min.X { + ix = sr.Min.X + } + jx := int(math.Ceil(sx + xHalfWidth)) + if jx > sr.Max.X { + jx = sr.Max.X + } + + totalXWeight := 0.0 + for kx := ix; kx < jx; kx++ { + xWeight := 0.0 + if t := abs((sx - float64(kx)) * xKernelArgScale); t < q.Support { + xWeight = q.At(t) + } + xWeights[kx-ix] = xWeight + totalXWeight += xWeight + } + for x := range xWeights[:jx-ix] { + xWeights[x] /= totalXWeight + } + + sy -= 0.5 + iy := int(math.Floor(sy - yHalfWidth)) + if iy < sr.Min.Y { + iy = sr.Min.Y + } + jy := int(math.Ceil(sy + yHalfWidth)) + if jy > sr.Max.Y { + jy = sr.Max.Y + } + + totalYWeight := 0.0 + for ky := iy; ky < jy; ky++ { + yWeight := 0.0 + if t := abs((sy - float64(ky)) * yKernelArgScale); t < q.Support { + yWeight = q.At(t) + } + yWeights[ky-iy] = yWeight + totalYWeight += yWeight + } + for y := range yWeights[:jy-iy] { + yWeights[y] /= totalYWeight + } + + var pr, pg, pb, pa float64 + for ky := iy; ky < jy; ky++ { + yWeight := yWeights[ky-iy] + for kx := ix; kx < jx; kx++ { + pru, pgu, pbu, pau := src.At(kx, ky).RGBA() + pr += float64(pru) * xWeights[kx-ix] * yWeight + pg += float64(pgu) * xWeights[kx-ix] * yWeight + pb += float64(pbu) * xWeights[kx-ix] * yWeight + pa += float64(pau) * xWeights[kx-ix] * yWeight + } + } + dst.Pix[d+0] = uint8(fffftou(pr) >> 8) + dst.Pix[d+1] = uint8(fffftou(pg) >> 8) + dst.Pix[d+2] = uint8(fffftou(pb) >> 8) + dst.Pix[d+3] = uint8(fffftou(pa) >> 8) + } + } +} + +func (q *Kernel) transform_RGBA_RGBA(dst *image.RGBA, dr, adr image.Rectangle, d2s *f64.Aff3, src *image.RGBA, sr image.Rectangle, xscale, yscale float64) { + // When shrinking, broaden the effective kernel support so that we still + // visit every source pixel. + xHalfWidth, xKernelArgScale := q.Support, 1.0 + if xscale > 1 { + xHalfWidth *= xscale + xKernelArgScale = 1 / xscale + } + yHalfWidth, yKernelArgScale := q.Support, 1.0 + if yscale > 1 { + yHalfWidth *= yscale + yKernelArgScale = 1 / yscale + } + + xWeights := make([]float64, 1+2*int(math.Ceil(xHalfWidth))) + yWeights := make([]float64, 1+2*int(math.Ceil(yHalfWidth))) + + for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ { + dyf := float64(dr.Min.Y+int(dy)) + 0.5 + d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) + for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { + dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). + 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 + ix := int(math.Floor(sx - xHalfWidth)) + if ix < sr.Min.X { + ix = sr.Min.X + } + jx := int(math.Ceil(sx + xHalfWidth)) + if jx > sr.Max.X { + jx = sr.Max.X + } + + totalXWeight := 0.0 + for kx := ix; kx < jx; kx++ { + xWeight := 0.0 + if t := abs((sx - float64(kx)) * xKernelArgScale); t < q.Support { + xWeight = q.At(t) + } + xWeights[kx-ix] = xWeight + totalXWeight += xWeight + } + for x := range xWeights[:jx-ix] { + xWeights[x] /= totalXWeight + } + + sy -= 0.5 + iy := int(math.Floor(sy - yHalfWidth)) + if iy < sr.Min.Y { + iy = sr.Min.Y + } + jy := int(math.Ceil(sy + yHalfWidth)) + if jy > sr.Max.Y { + jy = sr.Max.Y + } + + totalYWeight := 0.0 + for ky := iy; ky < jy; ky++ { + yWeight := 0.0 + if t := abs((sy - float64(ky)) * yKernelArgScale); t < q.Support { + yWeight = q.At(t) + } + yWeights[ky-iy] = yWeight + totalYWeight += yWeight + } + for y := range yWeights[:jy-iy] { + yWeights[y] /= totalYWeight + } + + var pr, pg, pb, pa float64 + for ky := iy; ky < jy; ky++ { + yWeight := yWeights[ky-iy] + for kx := ix; kx < jx; kx++ { + pi := src.PixOffset(kx, ky) + pru := uint32(src.Pix[pi+0]) * 0x101 + pgu := uint32(src.Pix[pi+1]) * 0x101 + pbu := uint32(src.Pix[pi+2]) * 0x101 + pau := uint32(src.Pix[pi+3]) * 0x101 + pr += float64(pru) * xWeights[kx-ix] * yWeight + pg += float64(pgu) * xWeights[kx-ix] * yWeight + pb += float64(pbu) * xWeights[kx-ix] * yWeight + pa += float64(pau) * xWeights[kx-ix] * yWeight + } + } + dst.Pix[d+0] = uint8(fffftou(pr) >> 8) + dst.Pix[d+1] = uint8(fffftou(pg) >> 8) + dst.Pix[d+2] = uint8(fffftou(pb) >> 8) + dst.Pix[d+3] = uint8(fffftou(pa) >> 8) + } + } +} + +func (q *Kernel) transform_RGBA_Uniform(dst *image.RGBA, dr, adr image.Rectangle, d2s *f64.Aff3, src *image.Uniform, sr image.Rectangle, xscale, yscale float64) { + // When shrinking, broaden the effective kernel support so that we still + // visit every source pixel. + xHalfWidth, xKernelArgScale := q.Support, 1.0 + if xscale > 1 { + xHalfWidth *= xscale + xKernelArgScale = 1 / xscale + } + yHalfWidth, yKernelArgScale := q.Support, 1.0 + if yscale > 1 { + yHalfWidth *= yscale + yKernelArgScale = 1 / yscale + } + + xWeights := make([]float64, 1+2*int(math.Ceil(xHalfWidth))) + yWeights := make([]float64, 1+2*int(math.Ceil(yHalfWidth))) + + for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ { + dyf := float64(dr.Min.Y+int(dy)) + 0.5 + d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) + for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { + dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). + 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 + ix := int(math.Floor(sx - xHalfWidth)) + if ix < sr.Min.X { + ix = sr.Min.X + } + jx := int(math.Ceil(sx + xHalfWidth)) + if jx > sr.Max.X { + jx = sr.Max.X + } + + totalXWeight := 0.0 + for kx := ix; kx < jx; kx++ { + xWeight := 0.0 + if t := abs((sx - float64(kx)) * xKernelArgScale); t < q.Support { + xWeight = q.At(t) + } + xWeights[kx-ix] = xWeight + totalXWeight += xWeight + } + for x := range xWeights[:jx-ix] { + xWeights[x] /= totalXWeight + } + + sy -= 0.5 + iy := int(math.Floor(sy - yHalfWidth)) + if iy < sr.Min.Y { + iy = sr.Min.Y + } + jy := int(math.Ceil(sy + yHalfWidth)) + if jy > sr.Max.Y { + jy = sr.Max.Y + } + + totalYWeight := 0.0 + for ky := iy; ky < jy; ky++ { + yWeight := 0.0 + if t := abs((sy - float64(ky)) * yKernelArgScale); t < q.Support { + yWeight = q.At(t) + } + yWeights[ky-iy] = yWeight + totalYWeight += yWeight + } + for y := range yWeights[:jy-iy] { + yWeights[y] /= totalYWeight + } + + var pr, pg, pb, pa float64 + for ky := iy; ky < jy; ky++ { + yWeight := yWeights[ky-iy] + for kx := ix; kx < jx; kx++ { + pru, pgu, pbu, pau := src.At(kx, ky).RGBA() + pr += float64(pru) * xWeights[kx-ix] * yWeight + pg += float64(pgu) * xWeights[kx-ix] * yWeight + pb += float64(pbu) * xWeights[kx-ix] * yWeight + pa += float64(pau) * xWeights[kx-ix] * yWeight + } + } + dst.Pix[d+0] = uint8(fffftou(pr) >> 8) + dst.Pix[d+1] = uint8(fffftou(pg) >> 8) + dst.Pix[d+2] = uint8(fffftou(pb) >> 8) + dst.Pix[d+3] = uint8(fffftou(pa) >> 8) + } + } +} + +func (q *Kernel) transform_RGBA_YCbCr(dst *image.RGBA, dr, adr image.Rectangle, d2s *f64.Aff3, src *image.YCbCr, sr image.Rectangle, xscale, yscale float64) { + // When shrinking, broaden the effective kernel support so that we still + // visit every source pixel. + xHalfWidth, xKernelArgScale := q.Support, 1.0 + if xscale > 1 { + xHalfWidth *= xscale + xKernelArgScale = 1 / xscale + } + yHalfWidth, yKernelArgScale := q.Support, 1.0 + if yscale > 1 { + yHalfWidth *= yscale + yKernelArgScale = 1 / yscale + } + + xWeights := make([]float64, 1+2*int(math.Ceil(xHalfWidth))) + yWeights := make([]float64, 1+2*int(math.Ceil(yHalfWidth))) + + for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ { + dyf := float64(dr.Min.Y+int(dy)) + 0.5 + d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) + for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { + dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). + 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 + ix := int(math.Floor(sx - xHalfWidth)) + if ix < sr.Min.X { + ix = sr.Min.X + } + jx := int(math.Ceil(sx + xHalfWidth)) + if jx > sr.Max.X { + jx = sr.Max.X + } + + totalXWeight := 0.0 + for kx := ix; kx < jx; kx++ { + xWeight := 0.0 + if t := abs((sx - float64(kx)) * xKernelArgScale); t < q.Support { + xWeight = q.At(t) + } + xWeights[kx-ix] = xWeight + totalXWeight += xWeight + } + for x := range xWeights[:jx-ix] { + xWeights[x] /= totalXWeight + } + + sy -= 0.5 + iy := int(math.Floor(sy - yHalfWidth)) + if iy < sr.Min.Y { + iy = sr.Min.Y + } + jy := int(math.Ceil(sy + yHalfWidth)) + if jy > sr.Max.Y { + jy = sr.Max.Y + } + + totalYWeight := 0.0 + for ky := iy; ky < jy; ky++ { + yWeight := 0.0 + if t := abs((sy - float64(ky)) * yKernelArgScale); t < q.Support { + yWeight = q.At(t) + } + yWeights[ky-iy] = yWeight + totalYWeight += yWeight + } + for y := range yWeights[:jy-iy] { + yWeights[y] /= totalYWeight + } + + var pr, pg, pb, pa float64 + for ky := iy; ky < jy; ky++ { + yWeight := yWeights[ky-iy] + for kx := ix; kx < jx; kx++ { + pru, pgu, pbu, pau := src.At(kx, ky).RGBA() + pr += float64(pru) * xWeights[kx-ix] * yWeight + pg += float64(pgu) * xWeights[kx-ix] * yWeight + pb += float64(pbu) * xWeights[kx-ix] * yWeight + pa += float64(pau) * xWeights[kx-ix] * yWeight + } + } + dst.Pix[d+0] = uint8(fffftou(pr) >> 8) + dst.Pix[d+1] = uint8(fffftou(pg) >> 8) + dst.Pix[d+2] = uint8(fffftou(pb) >> 8) + dst.Pix[d+3] = uint8(fffftou(pa) >> 8) + } + } +} + +func (q *Kernel) transform_RGBA_Image(dst *image.RGBA, dr, adr image.Rectangle, d2s *f64.Aff3, src image.Image, sr image.Rectangle, xscale, yscale float64) { + // When shrinking, broaden the effective kernel support so that we still + // visit every source pixel. + xHalfWidth, xKernelArgScale := q.Support, 1.0 + if xscale > 1 { + xHalfWidth *= xscale + xKernelArgScale = 1 / xscale + } + yHalfWidth, yKernelArgScale := q.Support, 1.0 + if yscale > 1 { + yHalfWidth *= yscale + yKernelArgScale = 1 / yscale + } + + xWeights := make([]float64, 1+2*int(math.Ceil(xHalfWidth))) + yWeights := make([]float64, 1+2*int(math.Ceil(yHalfWidth))) + + for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ { + dyf := float64(dr.Min.Y+int(dy)) + 0.5 + d := dst.PixOffset(dr.Min.X+adr.Min.X, dr.Min.Y+int(dy)) + for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx, d = dx+1, d+4 { + dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). + 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 + ix := int(math.Floor(sx - xHalfWidth)) + if ix < sr.Min.X { + ix = sr.Min.X + } + jx := int(math.Ceil(sx + xHalfWidth)) + if jx > sr.Max.X { + jx = sr.Max.X + } + + totalXWeight := 0.0 + for kx := ix; kx < jx; kx++ { + xWeight := 0.0 + if t := abs((sx - float64(kx)) * xKernelArgScale); t < q.Support { + xWeight = q.At(t) + } + xWeights[kx-ix] = xWeight + totalXWeight += xWeight + } + for x := range xWeights[:jx-ix] { + xWeights[x] /= totalXWeight + } + + sy -= 0.5 + iy := int(math.Floor(sy - yHalfWidth)) + if iy < sr.Min.Y { + iy = sr.Min.Y + } + jy := int(math.Ceil(sy + yHalfWidth)) + if jy > sr.Max.Y { + jy = sr.Max.Y + } + + totalYWeight := 0.0 + for ky := iy; ky < jy; ky++ { + yWeight := 0.0 + if t := abs((sy - float64(ky)) * yKernelArgScale); t < q.Support { + yWeight = q.At(t) + } + yWeights[ky-iy] = yWeight + totalYWeight += yWeight + } + for y := range yWeights[:jy-iy] { + yWeights[y] /= totalYWeight + } + + var pr, pg, pb, pa float64 + for ky := iy; ky < jy; ky++ { + yWeight := yWeights[ky-iy] + for kx := ix; kx < jx; kx++ { + pru, pgu, pbu, pau := src.At(kx, ky).RGBA() + pr += float64(pru) * xWeights[kx-ix] * yWeight + pg += float64(pgu) * xWeights[kx-ix] * yWeight + pb += float64(pbu) * xWeights[kx-ix] * yWeight + pa += float64(pau) * xWeights[kx-ix] * yWeight + } + } + dst.Pix[d+0] = uint8(fffftou(pr) >> 8) + dst.Pix[d+1] = uint8(fffftou(pg) >> 8) + dst.Pix[d+2] = uint8(fffftou(pb) >> 8) + dst.Pix[d+3] = uint8(fffftou(pa) >> 8) + } + } +} + +func (q *Kernel) transform_Image_Image(dst Image, dr, adr image.Rectangle, d2s *f64.Aff3, src image.Image, sr image.Rectangle, xscale, yscale float64) { + // When shrinking, broaden the effective kernel support so that we still + // visit every source pixel. + xHalfWidth, xKernelArgScale := q.Support, 1.0 + if xscale > 1 { + xHalfWidth *= xscale + xKernelArgScale = 1 / xscale + } + yHalfWidth, yKernelArgScale := q.Support, 1.0 + if yscale > 1 { + yHalfWidth *= yscale + yKernelArgScale = 1 / yscale + } + + xWeights := make([]float64, 1+2*int(math.Ceil(xHalfWidth))) + yWeights := make([]float64, 1+2*int(math.Ceil(yHalfWidth))) + + dstColorRGBA64 := &color.RGBA64{} + dstColor := color.Color(dstColorRGBA64) + for dy := int32(adr.Min.Y); dy < int32(adr.Max.Y); dy++ { + dyf := float64(dr.Min.Y+int(dy)) + 0.5 + for dx := int32(adr.Min.X); dx < int32(adr.Max.X); dx++ { + dxf := float64(dr.Min.X+int(dx)) + 0.5 + // TODO: change the src origin so that we can say int(f) instead of int(math.Floor(f)). + 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 + ix := int(math.Floor(sx - xHalfWidth)) + if ix < sr.Min.X { + ix = sr.Min.X + } + jx := int(math.Ceil(sx + xHalfWidth)) + if jx > sr.Max.X { + jx = sr.Max.X + } + + totalXWeight := 0.0 + for kx := ix; kx < jx; kx++ { + xWeight := 0.0 + if t := abs((sx - float64(kx)) * xKernelArgScale); t < q.Support { + xWeight = q.At(t) + } + xWeights[kx-ix] = xWeight + totalXWeight += xWeight + } + for x := range xWeights[:jx-ix] { + xWeights[x] /= totalXWeight + } + + sy -= 0.5 + iy := int(math.Floor(sy - yHalfWidth)) + if iy < sr.Min.Y { + iy = sr.Min.Y + } + jy := int(math.Ceil(sy + yHalfWidth)) + if jy > sr.Max.Y { + jy = sr.Max.Y + } + + totalYWeight := 0.0 + for ky := iy; ky < jy; ky++ { + yWeight := 0.0 + if t := abs((sy - float64(ky)) * yKernelArgScale); t < q.Support { + yWeight = q.At(t) + } + yWeights[ky-iy] = yWeight + totalYWeight += yWeight + } + for y := range yWeights[:jy-iy] { + yWeights[y] /= totalYWeight + } + + var pr, pg, pb, pa float64 + for ky := iy; ky < jy; ky++ { + yWeight := yWeights[ky-iy] + for kx := ix; kx < jx; kx++ { + pru, pgu, pbu, pau := src.At(kx, ky).RGBA() + pr += float64(pru) * xWeights[kx-ix] * yWeight + pg += float64(pgu) * xWeights[kx-ix] * yWeight + pb += float64(pbu) * xWeights[kx-ix] * yWeight + pa += float64(pau) * xWeights[kx-ix] * yWeight + } + } + dstColorRGBA64.R = fffftou(pr) + dstColorRGBA64.G = fffftou(pg) + dstColorRGBA64.B = fffftou(pb) + dstColorRGBA64.A = fffftou(pa) + dst.Set(dr.Min.X+int(dx), dr.Min.Y+int(dy), dstColor) + } + } +} diff --git a/draw/scale.go b/draw/scale.go index 4f94086..d570f6e 100644 --- a/draw/scale.go +++ b/draw/scale.go @@ -86,21 +86,21 @@ type Kernel struct { } // Scale implements the Scaler interface. -func (k *Kernel) Scale(dst Image, dr image.Rectangle, src image.Image, sr image.Rectangle, opts *Options) { - k.NewScaler(dr.Dx(), dr.Dy(), sr.Dx(), sr.Dy()).Scale(dst, dr, src, sr, opts) +func (q *Kernel) Scale(dst Image, dr image.Rectangle, src image.Image, sr image.Rectangle, opts *Options) { + q.NewScaler(dr.Dx(), dr.Dy(), sr.Dx(), sr.Dy()).Scale(dst, dr, src, sr, opts) } // NewScaler returns a Scaler that is optimized for scaling multiple times with // the same fixed destination and source width and height. -func (k *Kernel) NewScaler(dw, dh, sw, sh int) Scaler { +func (q *Kernel) NewScaler(dw, dh, sw, sh int) Scaler { return &kernelScaler{ - kernel: k, + kernel: q, dw: int32(dw), dh: int32(dh), sw: int32(sw), sh: int32(sh), - horizontal: newDistrib(k, int32(dw), int32(sw)), - vertical: newDistrib(k, int32(dh), int32(sh)), + horizontal: newDistrib(q, int32(dw), int32(sw)), + vertical: newDistrib(q, int32(dh), int32(sh)), } } @@ -181,6 +181,8 @@ type distrib struct { func newDistrib(q *Kernel, dw, sw int32) distrib { scale := float64(sw) / float64(dw) halfWidth, kernelArgScale := q.Support, 1.0 + // When shrinking, broaden the effective kernel support so that we still + // visit every source pixel. if scale > 1 { halfWidth *= scale kernelArgScale = 1 / scale @@ -199,25 +201,22 @@ func newDistrib(q *Kernel, dw, sw int32) distrib { i = 0 } j := int32(math.Ceil(center + halfWidth)) - if j >= sw { - j = sw - 1 + if j > sw { + j = sw if j < i { j = i } } sources[x] = source{i: i, j: j, invTotalWeight: center} - n += j - i + 1 + n += j - i } contribs := make([]contrib, 0, n) for k, b := range sources { totalWeight := 0.0 l := int32(len(contribs)) - for coord := b.i; coord <= b.j; coord++ { - t := (b.invTotalWeight - float64(coord)) * kernelArgScale - if t < 0 { - t = -t - } + for coord := b.i; coord < b.j; coord++ { + t := abs((b.invTotalWeight - float64(coord)) * kernelArgScale) if t >= q.Support { continue } @@ -240,11 +239,34 @@ func newDistrib(q *Kernel, dw, sw int32) distrib { return distrib{sources, contribs} } +// abs is like math.Abs, but it doesn't care about negative zero, infinities or +// NaNs. +func abs(f float64) float64 { + if f < 0 { + f = -f + } + return f +} + +// ftou converts the range [0.0, 1.0] to [0, 0xffff]. func ftou(f float64) uint16 { i := int32(0xffff*f + 0.5) if i > 0xffff { return 0xffff - } else if i > 0 { + } + if i > 0 { + return uint16(i) + } + return 0 +} + +// fffftou converts the range [0.0, 65535.0] to [0, 0xffff]. +func fffftou(f float64) uint16 { + i := int32(f + 0.5) + if i > 0xffff { + return 0xffff + } + if i > 0 { return uint16(i) } return 0 @@ -275,6 +297,17 @@ func invert(m *f64.Aff3) f64.Aff3 { } } +func matMul(p, q *f64.Aff3) f64.Aff3 { + return f64.Aff3{ + p[3*0+0]*q[3*0+0] + p[3*0+1]*q[3*1+0], + p[3*0+0]*q[3*0+1] + p[3*0+1]*q[3*1+1], + p[3*0+0]*q[3*0+2] + p[3*0+1]*q[3*1+2] + p[3*0+2], + p[3*1+0]*q[3*0+0] + p[3*1+1]*q[3*1+0], + p[3*1+0]*q[3*0+1] + p[3*1+1]*q[3*1+1], + p[3*1+0]*q[3*0+2] + p[3*1+1]*q[3*1+2] + p[3*1+2], + } +} + // 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{ diff --git a/draw/scale_test.go b/draw/scale_test.go index c15810e..4ed60d6 100644 --- a/draw/scale_test.go +++ b/draw/scale_test.go @@ -23,13 +23,25 @@ import ( var genGoldenFiles = flag.Bool("gen_golden_files", false, "whether to generate the TestXxx golden files.") -var transformMatrix = func() *f64.Aff3 { +var transformMatrix = func(tx, ty float64) *f64.Aff3 { const scale, cos30, sin30 = 3.75, 0.866025404, 0.5 return &f64.Aff3{ - +scale * cos30, -scale * sin30, 40, - +scale * sin30, +scale * cos30, 10, + +scale * cos30, -scale * sin30, tx, + +scale * sin30, +scale * cos30, ty, } -}() +} + +func encode(filename string, m image.Image) error { + f, err := os.Create(filename) + if err != nil { + return fmt.Errorf("Create: %v", err) + } + defer f.Close() + if err := png.Encode(f, m); err != nil { + return fmt.Errorf("Encode: %v", err) + } + return nil +} // testInterp tests that interpolating the source image gives the exact // destination image. This is to ensure that any refactoring or optimization of @@ -58,25 +70,14 @@ func testInterp(t *testing.T, w int, h int, direction, srcFilename string) { got := image.NewRGBA(image.Rect(0, 0, w, h)) if direction == "rotate" { - if name == "bl" || name == "cr" { - // TODO: implement Kernel.Transform. - continue - } - q.Transform(got, transformMatrix, src, src.Bounds(), nil) + q.Transform(got, transformMatrix(40, 10), src, src.Bounds(), nil) } else { q.Scale(got, got.Bounds(), src, src.Bounds(), nil) } if *genGoldenFiles { - g, err := os.Create(goldenFilename) - if err != nil { - t.Errorf("Create: %v", err) - continue - } - defer g.Close() - if err := png.Encode(g, got); err != nil { - t.Errorf("Encode: %v", err) - continue + if err := encode(goldenFilename, got); err != nil { + t.Error(err) } continue } @@ -132,11 +133,6 @@ func TestInterpClipCommute(t *testing.T) { } for _, transform := range []bool{false, true} { for _, q := range qs { - if transform && q == CatmullRom { - // TODO: implement Kernel.Transform. - continue - } - dst0 := image.NewRGBA(image.Rect(1, 1, 10, 10)) dst1 := image.NewRGBA(image.Rect(1, 1, 10, 10)) for i := range dst0.Pix { @@ -147,7 +143,7 @@ func TestInterpClipCommute(t *testing.T) { var interp func(dst *image.RGBA) if transform { interp = func(dst *image.RGBA) { - q.Transform(dst, transformMatrix, src, src.Bounds(), nil) + q.Transform(dst, transformMatrix(2, 1), src, src.Bounds(), nil) } } else { interp = func(dst *image.RGBA) { @@ -216,20 +212,32 @@ func TestSrcTranslationInvariance(t *testing.T) { {-8, +8}, {-8, -8}, } + m00 := transformMatrix(0, 0) - for _, q := range qs { - want := image.NewRGBA(image.Rect(0, 0, 200, 200)) - q.Scale(want, want.Bounds(), src, src.Bounds(), nil) - for _, delta := range deltas { - tsrc := &translatedImage{src, delta} - - got := image.NewRGBA(image.Rect(0, 0, 200, 200)) - q.Scale(got, got.Bounds(), tsrc, tsrc.Bounds(), nil) - if !bytes.Equal(got.Pix, want.Pix) { - t.Errorf("pix differ for delta=%v, q=%T", delta, q) + for _, transform := range []bool{false, true} { + for _, q := range qs { + want := image.NewRGBA(image.Rect(0, 0, 200, 200)) + if transform { + q.Transform(want, m00, src, src.Bounds(), nil) + } else { + q.Scale(want, want.Bounds(), src, src.Bounds(), nil) + } + for _, delta := range deltas { + tsrc := &translatedImage{src, delta} + got := image.NewRGBA(image.Rect(0, 0, 200, 200)) + if transform { + m := matMul(m00, &f64.Aff3{ + 1, 0, -float64(delta.X), + 0, 1, -float64(delta.Y), + }) + q.Transform(got, &m, tsrc, tsrc.Bounds(), nil) + } else { + q.Scale(got, got.Bounds(), tsrc, tsrc.Bounds(), nil) + } + if !bytes.Equal(got.Pix, want.Pix) { + t.Errorf("pix differ for delta=%v, transform=%t, q=%T", delta, transform, q) + } } - - // TODO: Transform, once Kernel.Transform is implemented. } } } @@ -285,18 +293,27 @@ func TestFastPaths(t *testing.T) { for _, dr := range drs { for _, src := range srcs { for _, sr := range srs { - for _, q := range qs { - dst0 := image.NewRGBA(drs[0]) - dst1 := image.NewRGBA(drs[0]) - Draw(dst0, dst0.Bounds(), blue, image.Point{}, Src) - Draw(dstWrapper{dst1}, dst1.Bounds(), srcWrapper{blue}, image.Point{}, Src) - q.Scale(dst0, dr, src, sr, nil) - q.Scale(dstWrapper{dst1}, dr, srcWrapper{src}, sr, nil) - if !bytes.Equal(dst0.Pix, dst1.Pix) { - t.Errorf("pix differ for dr=%v, src=%T, sr=%v, q=%T", dr, src, sr, q) - } + for _, transform := range []bool{false, true} { + for _, q := range qs { + dst0 := image.NewRGBA(drs[0]) + dst1 := image.NewRGBA(drs[0]) + Draw(dst0, dst0.Bounds(), blue, image.Point{}, Src) + Draw(dstWrapper{dst1}, dst1.Bounds(), srcWrapper{blue}, image.Point{}, Src) - // TODO: Transform, once Kernel.Transform is implemented. + if transform { + m := transformMatrix(2, 1) + q.Transform(dst0, m, src, sr, nil) + q.Transform(dstWrapper{dst1}, m, srcWrapper{src}, sr, nil) + } else { + q.Scale(dst0, dr, src, sr, nil) + q.Scale(dstWrapper{dst1}, dr, srcWrapper{src}, sr, nil) + } + + if !bytes.Equal(dst0.Pix, dst1.Pix) { + t.Errorf("pix differ for dr=%v, src=%T, sr=%v, transform=%t, q=%T", + dr, src, sr, transform, q) + } + } } } } @@ -385,10 +402,11 @@ func benchTform(b *testing.B, srcf func(image.Rectangle) (image.Image, error), w b.Fatal(err) } sr := src.Bounds() + m := transformMatrix(40, 10) b.ResetTimer() for i := 0; i < b.N; i++ { - q.Transform(dst, transformMatrix, src, sr, nil) + q.Transform(dst, m, src, sr, nil) } } @@ -413,8 +431,14 @@ func BenchmarkScaleSrcRGBA(b *testing.B) { benchScale(b, srcRGBA, 200, 150, A func BenchmarkScaleSrcUniform(b *testing.B) { benchScale(b, srcUniform, 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) } +func BenchmarkTformABSrcGray(b *testing.B) { benchTform(b, srcGray, 200, 150, ApproxBiLinear) } +func BenchmarkTformABSrcNRGBA(b *testing.B) { benchTform(b, srcNRGBA, 200, 150, ApproxBiLinear) } +func BenchmarkTformABSrcRGBA(b *testing.B) { benchTform(b, srcRGBA, 200, 150, ApproxBiLinear) } +func BenchmarkTformABSrcUniform(b *testing.B) { benchTform(b, srcUniform, 200, 150, ApproxBiLinear) } +func BenchmarkTformABSrcYCbCr(b *testing.B) { benchTform(b, srcYCbCr, 200, 150, ApproxBiLinear) } + +func BenchmarkTformCRSrcGray(b *testing.B) { benchTform(b, srcGray, 200, 150, CatmullRom) } +func BenchmarkTformCRSrcNRGBA(b *testing.B) { benchTform(b, srcNRGBA, 200, 150, CatmullRom) } +func BenchmarkTformCRSrcRGBA(b *testing.B) { benchTform(b, srcRGBA, 200, 150, CatmullRom) } +func BenchmarkTformCRSrcUniform(b *testing.B) { benchTform(b, srcUniform, 200, 150, CatmullRom) } +func BenchmarkTformCRSrcYCbCr(b *testing.B) { benchTform(b, srcYCbCr, 200, 150, CatmullRom) } diff --git a/testdata/go-turns-two-rotate-bl.png b/testdata/go-turns-two-rotate-bl.png new file mode 100644 index 0000000000000000000000000000000000000000..b4e1279d98f34d9eb6982da994bad974b738d39b GIT binary patch literal 8764 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 O0000