From 036e35a2586f616152eab04fd145b81700f5b82a Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Sat, 7 Dec 2013 12:05:18 -0800 Subject: [PATCH] images: performance and memory improvements. Makes the assumption that our old image logic was 'If the image is huge (>2400px) shrink it to twice the target thumbnail size with resample (which is quick) then box filter the image in half (which is slow)'. There was a bug there that was causing large intermediary images to be allocated when the source image was just the right size and in portrait orientation (mainly trigger by photos from my Nexus 4). Generalizes resize code to always resample to 2 times the thumbnail size and then smooth down. Throws more pixels away faster. Add downsizing routines that operate inplace. This greatly reduces our peak memory usage when generating thumbnails. Add version optimized for our particular resize case (halving); providing a speed boost over the generalized versions. Add tests and benchmarks comparing new resize to old. Test for pkg/misc/resize can be run with --output=output_dir/ to see the images generated by the old and new resize routines along with mask of their differences. Addresses some of the concerns in https://camlistore.org/issue/237 Change-Id: I6464fa637da9db371f15761bb699c045604b6cb8 --- pkg/images/images.go | 14 +- pkg/misc/resize/bench_test.go | 63 ++++ pkg/misc/resize/resize.go | 143 +++++++- pkg/misc/resize/resize_test.go | 337 ++++++++++++++++++ .../testdata/test-resample-128x128-64x64.png | Bin 0 -> 437 bytes .../testdata/test-resample-768x576-128x96.png | Bin 0 -> 1838 bytes pkg/misc/resize/testdata/test.png | Bin 0 -> 34427 bytes pkg/server/image.go | 10 +- 8 files changed, 546 insertions(+), 21 deletions(-) create mode 100644 pkg/misc/resize/bench_test.go create mode 100644 pkg/misc/resize/resize_test.go create mode 100644 pkg/misc/resize/testdata/test-resample-128x128-64x64.png create mode 100644 pkg/misc/resize/testdata/test-resample-768x576-128x96.png create mode 100644 pkg/misc/resize/testdata/test.png diff --git a/pkg/images/images.go b/pkg/images/images.go index 55ae6480a..47871a2d0 100644 --- a/pkg/images/images.go +++ b/pkg/images/images.go @@ -221,19 +221,13 @@ func rescale(im image.Image, opts *DecodeOpts) image.Image { mh = int(mhf * float32(b.Dy())) } - const huge = 2400 // If it's gigantic, it's more efficient to downsample first // and then resize; resizing will smooth out the roughness. // (trusting the moustachio guys on that one). - if b.Dx() > huge || b.Dy() > huge { - w, h := mw*2, mh*2 - if b.Dx() > b.Dy() { - w = b.Dx() * h / b.Dy() - } else { - h = b.Dy() * w / b.Dx() - } - im = resize.Resample(im, b, w, h) - b = im.Bounds() + if b.Dx() > mw*2 || b.Dy() > mh*2 { + w, h := ScaledDimensions(b.Dx(), b.Dy(), mw*2, mh*2) + im = resize.ResampleInplace(im, b, w, h) + return resize.HalveInplace(im) } mw, mh = ScaledDimensions(b.Dx(), b.Dy(), mw, mh) return resize.Resize(im, b, mw, mh) diff --git a/pkg/misc/resize/bench_test.go b/pkg/misc/resize/bench_test.go new file mode 100644 index 000000000..875290f14 --- /dev/null +++ b/pkg/misc/resize/bench_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resize + +import ( + "image" + "testing" +) + +func resize(m image.Image) { + s := m.Bounds().Size().Div(2) + Resize(m, m.Bounds(), s.X, s.Y) +} + +func halve(m image.Image) { + HalveInplace(m) +} + +func BenchmarkResizeRGBA(b *testing.B) { + m := image.NewRGBA(orig) + b.ResetTimer() + for i := 0; i < b.N; i++ { + resize(m) + } +} + +func BenchmarkHalveRGBA(b *testing.B) { + m := image.NewRGBA(orig) + b.ResetTimer() + for i := 0; i < b.N; i++ { + halve(m) + } +} + +func BenchmarkResizeYCrCb(b *testing.B) { + m := image.NewYCbCr(orig, image.YCbCrSubsampleRatio422) + b.ResetTimer() + for i := 0; i < b.N; i++ { + resize(m) + } +} + +func BenchmarkHalveYCrCb(b *testing.B) { + m := image.NewYCbCr(orig, image.YCbCrSubsampleRatio422) + b.ResetTimer() + for i := 0; i < b.N; i++ { + halve(m) + } +} diff --git a/pkg/misc/resize/resize.go b/pkg/misc/resize/resize.go index 07fda28d7..31b23d374 100644 --- a/pkg/misc/resize/resize.go +++ b/pkg/misc/resize/resize.go @@ -8,6 +8,7 @@ package resize import ( "image" "image/color" + "image/draw" ) // Resize returns a scaled copy of the image slice r of m. @@ -219,6 +220,125 @@ func resizeRGBA(m *image.RGBA, r image.Rectangle, w, h int) image.Image { return average(sum, w, h, n) } +// HalveInplace downsamples the image by 50% using averaging interpolation. +func HalveInplace(m image.Image) image.Image { + b := m.Bounds() + switch m := m.(type) { + case *image.YCbCr: + for y := b.Min.Y; y < b.Max.Y/2; y++ { + for x := b.Min.X; x < b.Max.X/2; x++ { + y00 := uint32(m.Y[m.YOffset(2*x, 2*y)]) + y10 := uint32(m.Y[m.YOffset(2*x+1, 2*y)]) + y01 := uint32(m.Y[m.YOffset(2*x, 2*y+1)]) + y11 := uint32(m.Y[m.YOffset(2*x+1, 2*y+1)]) + // Add before divide with uint32 or we get errors in the least + // significant bits. + m.Y[m.YOffset(x, y)] = uint8((y00 + y10 + y01 + y11) >> 2) + + cb00 := uint32(m.Cb[m.COffset(2*x, 2*y)]) + cb10 := uint32(m.Cb[m.COffset(2*x+1, 2*y)]) + cb01 := uint32(m.Cb[m.COffset(2*x, 2*y+1)]) + cb11 := uint32(m.Cb[m.COffset(2*x+1, 2*y+1)]) + m.Cb[m.COffset(x, y)] = uint8((cb00 + cb10 + cb01 + cb11) >> 2) + + cr00 := uint32(m.Cr[m.COffset(2*x, 2*y)]) + cr10 := uint32(m.Cr[m.COffset(2*x+1, 2*y)]) + cr01 := uint32(m.Cr[m.COffset(2*x, 2*y+1)]) + cr11 := uint32(m.Cr[m.COffset(2*x+1, 2*y+1)]) + m.Cr[m.COffset(x, y)] = uint8((cr00 + cr10 + cr01 + cr11) >> 2) + } + } + b.Max = b.Min.Add(b.Size().Div(2)) + return subImage(m, b) + case draw.Image: + for y := b.Min.Y; y < b.Max.Y/2; y++ { + for x := b.Min.X; x < b.Max.X/2; x++ { + r00, g00, b00, a00 := m.At(2*x, 2*y).RGBA() + r10, g10, b10, a10 := m.At(2*x+1, 2*y).RGBA() + r01, g01, b01, a01 := m.At(2*x, 2*y+1).RGBA() + r11, g11, b11, a11 := m.At(2*x+1, 2*y+1).RGBA() + + // Add before divide with uint32 or we get errors in the least + // significant bits. + r := (r00 + r10 + r01 + r11) >> 2 + g := (g00 + g10 + g01 + g11) >> 2 + b := (b00 + b10 + b01 + b11) >> 2 + a := (a00 + a10 + a01 + a11) >> 2 + + m.Set(x, y, color.RGBA{ + R: uint8(r >> 8), + G: uint8(g >> 8), + B: uint8(b >> 8), + A: uint8(a >> 8), + }) + } + } + b.Max = b.Min.Add(b.Size().Div(2)) + return subImage(m, b) + default: + // TODO(wathiede): fallback to generic Resample somehow? + panic("Unhandled image type") + } +} + +// ResampleInplace will resample m inplace, overwritting existing pixel data, +// and return a subimage of m sized to w and h. +func ResampleInplace(m image.Image, r image.Rectangle, w, h int) image.Image { + // We don't support scaling up. + if r.Dx() < w || r.Dy() < h { + return m + } + + switch m := m.(type) { + case *image.YCbCr: + xStep := float64(r.Dx()) / float64(w) + yStep := float64(r.Dy()) / float64(h) + for y := r.Min.Y; y < r.Min.Y+h; y++ { + for x := r.Min.X; x < r.Min.X+w; x++ { + xSrc := int(float64(x) * xStep) + ySrc := int(float64(y) * yStep) + cSrc := m.COffset(xSrc, ySrc) + cDst := m.COffset(x, y) + m.Y[m.YOffset(x, y)] = m.Y[m.YOffset(xSrc, ySrc)] + m.Cb[cDst] = m.Cb[cSrc] + m.Cr[cDst] = m.Cr[cSrc] + } + } + case draw.Image: + xStep := float64(r.Dx()) / float64(w) + yStep := float64(r.Dy()) / float64(h) + for y := r.Min.Y; y < r.Min.Y+h; y++ { + for x := r.Min.X; x < r.Min.X+w; x++ { + xSrc := int(float64(x) * xStep) + ySrc := int(float64(y) * yStep) + r, g, b, a := m.At(xSrc, ySrc).RGBA() + m.Set(x, y, color.RGBA{ + R: uint8(r >> 8), + G: uint8(g >> 8), + B: uint8(b >> 8), + A: uint8(a >> 8), + }) + } + } + default: + // TODO fallback to generic Resample somehow? + panic("Unhandled image type") + } + r.Max.X = r.Min.X + w + r.Max.Y = r.Min.Y + h + return subImage(m, r) +} + +func subImage(m image.Image, r image.Rectangle) image.Image { + type subImager interface { + SubImage(image.Rectangle) image.Image + } + if si, ok := m.(subImager); ok { + return si.SubImage(r) + } + panic("Image type doesn't support SubImage") +} + // Resample returns a resampled copy of the image slice r of m. // The returned image has width w and height h. func Resample(m image.Image, r image.Rectangle, w, h int) image.Image { @@ -228,19 +348,22 @@ func Resample(m image.Image, r image.Rectangle, w, h int) image.Image { if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 { return image.NewRGBA64(image.Rect(0, 0, w, h)) } - curw, curh := r.Dx(), r.Dy() img := image.NewRGBA(image.Rect(0, 0, w, h)) + xStep := float64(r.Dx()) / float64(w) + yStep := float64(r.Dy()) / float64(h) for y := 0; y < h; y++ { for x := 0; x < w; x++ { - // Get a source pixel. - subx := x * curw / w - suby := y * curh / h - r32, g32, b32, a32 := m.At(subx, suby).RGBA() - r := uint8(r32 >> 8) - g := uint8(g32 >> 8) - b := uint8(b32 >> 8) - a := uint8(a32 >> 8) - img.SetRGBA(x, y, color.RGBA{r, g, b, a}) + xSrc := int(float64(r.Min.X) + float64(x)*xStep) + ySrc := int(float64(r.Min.Y) + float64(y)*yStep) + //xSrc = r.Min.X + x*r.Dx()/w + //ySrc = r.Min.Y + y*r.Dy()/h + r, g, b, a := m.At(xSrc, ySrc).RGBA() + img.SetRGBA(x, y, color.RGBA{ + R: uint8(r >> 8), + G: uint8(g >> 8), + B: uint8(b >> 8), + A: uint8(a >> 8), + }) } } return img diff --git a/pkg/misc/resize/resize_test.go b/pkg/misc/resize/resize_test.go new file mode 100644 index 000000000..1ecc8653d --- /dev/null +++ b/pkg/misc/resize/resize_test.go @@ -0,0 +1,337 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resize + +import ( + "flag" + "fmt" + "image" + "image/color" + "image/color/palette" + "image/draw" + "image/png" + "io" + "math" + "os" + "path/filepath" + "strings" + "testing" +) + +const ( + // psnrThreshold is the threshold over which images must match to consider + // HalveInplace equivalent to Resize. It is in terms of dB and 60-80 is + // good for RGB. + psnrThreshold = 50.0 + + maxPixelDiffPercentage = 10 +) + +var ( + output = flag.String("output", "", "If non-empty, the directory to save comparison images.") + + orig = image.Rect(0, 0, 1024, 1024) + thumb = image.Rect(0, 0, 64, 64) +) + +func makeImages(r image.Rectangle) []image.Image { + return []image.Image{ + image.NewGray(r), + image.NewGray16(r), + image.NewNRGBA(r), + image.NewNRGBA64(r), + image.NewPaletted(r, palette.Plan9), + image.NewRGBA(r), + image.NewRGBA64(r), + image.NewYCbCr(r, image.YCbCrSubsampleRatio444), + image.NewYCbCr(r, image.YCbCrSubsampleRatio422), + image.NewYCbCr(r, image.YCbCrSubsampleRatio420), + image.NewYCbCr(r, image.YCbCrSubsampleRatio440), + } +} + +func TestResize(t *testing.T) { + for i, im := range makeImages(orig) { + m := Resize(im, orig, thumb.Dx(), thumb.Dy()) + got, want := m.Bounds(), thumb + if !got.Eq(want) { + t.Error(i, "Want bounds", want, "got", got) + } + } +} + +func TestResampleInplace(t *testing.T) { + for i, im := range makeImages(orig) { + m := ResampleInplace(im, orig, thumb.Dx(), thumb.Dy()) + got, want := m.Bounds(), thumb + if !got.Eq(want) { + t.Error(i, "Want bounds", want, "got", got) + } + } +} + +func TestResample(t *testing.T) { + for i, im := range makeImages(orig) { + m := Resample(im, orig, thumb.Dx(), thumb.Dy()) + got, want := m.Bounds(), thumb + if !got.Eq(want) { + t.Error(i, "Want bounds", want, "got", got) + } + } + + for _, d := range []struct { + wantFn string + r image.Rectangle + w, h int + }{ + { + // Generated with imagemagick: + // $ convert -crop 128x128+320+160 -resize 64x64 -filter point \ + // testdata/test.png testdata/test-resample-128x128-64x64.png + wantFn: "test-resample-128x128-64x64.png", + r: image.Rect(320, 160, 320+128, 160+128), + w: 64, + h: 64, + }, + { + // Generated with imagemagick: + // $ convert -resize 128x128 -filter point testdata/test.png \ + // testdata/test-resample-768x576-128x96.png + wantFn: "test-resample-768x576-128x96.png", + r: image.Rect(0, 0, 768, 576), + w: 128, + h: 96, + }, + } { + m := image.NewRGBA(testIm.Bounds()) + fillTestImage(m) + r, err := os.Open(filepath.Join("testdata", d.wantFn)) + if err != nil { + t.Fatal(err) + } + defer r.Close() + want, err := png.Decode(r) + if err != nil { + t.Fatal(err) + } + got := Resample(m, d.r, d.w, d.h) + res := compareImages(got, want) + t.Logf("PSNR %.4f", res.psnr) + s := got.Bounds().Size() + tot := s.X * s.Y + per := float32(100*res.diffCnt) / float32(tot) + t.Logf("Resample not the same %d pixels different %.2f%%", res.diffCnt, per) + if *output != "" { + err = savePng(t, want, fmt.Sprintf("Resample.%s->%dx%d.want.png", + d.r, d.w, d.h)) + if err != nil { + t.Fatal(err) + } + err = savePng(t, got, fmt.Sprintf("Resample.%s->%dx%d.got.png", + d.r, d.w, d.h)) + if err != nil { + t.Fatal(err) + } + err = savePng(t, res.diffIm, + fmt.Sprintf("Resample.%s->%dx%d.diff.png", d.r, d.w, d.h)) + if err != nil { + t.Fatal(err) + } + } + } +} + +func TestHalveInplace(t *testing.T) { + for i, im := range makeImages(orig) { + m := HalveInplace(im) + b := im.Bounds() + got, want := m.Bounds(), image.Rectangle{ + Min: b.Min, + Max: b.Min.Add(b.Max.Div(2)), + } + if !got.Eq(want) { + t.Error(i, "Want bounds", want, "got", got) + } + } +} + +type results struct { + diffCnt int + psnr float64 + diffIm *image.Gray +} + +func compareImages(m1, m2 image.Image) results { + b := m1.Bounds() + s := b.Size() + res := results{} + mse := uint32(0) + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + r1, g1, b1, a1 := m1.At(x, y).RGBA() + r2, g2, b2, a2 := m2.At(x, y).RGBA() + + mse += ((r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2)) / 3 + if r1 != r2 || g1 != g2 || b1 != b2 || a1 != a2 { + if res.diffIm == nil { + res.diffIm = image.NewGray(m1.Bounds()) + } + res.diffCnt++ + res.diffIm.Set(x, y, color.White) + } + } + } + mse = mse / uint32(s.X*s.Y) + res.psnr = 20*math.Log10(1<<16) - 10*math.Log10(float64(mse)) + return res +} + +var testIm image.Image + +func init() { + r, err := os.Open(filepath.Join("testdata", "test.png")) + if err != nil { + panic(err) + } + defer r.Close() + testIm, err = png.Decode(r) +} + +func fillTestImage(im image.Image) { + b := im.Bounds() + if !b.Eq(testIm.Bounds()) { + panic("Requested target image dimensions not equal reference image.") + } + src := testIm + if dst, ok := im.(*image.YCbCr); ok { + b := testIm.Bounds() + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + r, g, b, _ := src.At(x, y).RGBA() + yp, cb, cr := color.RGBToYCbCr(uint8(r), uint8(g), uint8(b)) + + dst.Y[dst.YOffset(x, y)] = yp + off := dst.COffset(x, y) + dst.Cb[off] = cb + dst.Cr[off] = cr + } + } + return + } + draw.Draw(im.(draw.Image), b, testIm, b.Min, draw.Src) +} + +func savePng(t *testing.T, m image.Image, fn string) error { + fn = filepath.Join(*output, fn) + t.Log("Saving", fn) + f, err := os.Create(fn) + if err != nil { + return err + } + defer f.Close() + + return png.Encode(f, m) +} + +func getFilename(im image.Image, method string) string { + imgType := fmt.Sprintf("%T", im) + imgType = imgType[strings.Index(imgType, ".")+1:] + if m, ok := im.(*image.YCbCr); ok { + imgType += "." + m.SubsampleRatio.String() + } + return fmt.Sprintf("%s.%s.png", imgType, method) +} + +func TestCompareResizeToHavleInplace(t *testing.T) { + if testing.Short() { + t.Skip("Skipping TestCompareResizeToHavleInplace in short mode.") + } + images1, images2 := []image.Image{}, []image.Image{} + for _, im := range makeImages(testIm.Bounds()) { + fillTestImage(im) + images1 = append(images1, HalveInplace(im)) + } + for _, im := range makeImages(testIm.Bounds()) { + fillTestImage(im) + s := im.Bounds().Size() + images2 = append(images2, Resize(im, im.Bounds(), s.X/2, s.Y/2)) + } + + var ( + f io.WriteCloser + err error + ) + if *output != "" { + os.Mkdir(*output, os.FileMode(0777)) + f, err = os.Create(filepath.Join(*output, "index.html")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + fmt.Fprintf(f, ` + + + + Image comparison for TestCompareResizeToHavleInplace + + + +`) + } + for i, im1 := range images1 { + im2 := images2[i] + res := compareImages(im1, im2) + if *output != "" { + fmt.Fprintf(f, "") + fn := getFilename(im1, "halve") + err := savePng(t, im1, fn) + if err != nil { + t.Fatal(err) + } + fmt.Fprintf(f, `

%s`, fn, fn) + + fn = getFilename(im1, "resize") + err = savePng(t, im2, fn) + if err != nil { + t.Fatal(err) + } + fmt.Fprintf(f, `

%s`, fn, fn) + + if res.diffIm != nil { + fn = getFilename(im1, "diff") + err = savePng(t, res.diffIm, fn) + if err != nil { + t.Fatal(err) + } + fmt.Fprintf(f, `

%s`, fn, fn) + } + fmt.Fprintln(f) + } + + if res.psnr < psnrThreshold { + t.Errorf("%T PSNR too low %.4f", im1, res.psnr) + } else { + t.Logf("%T PSNR %.4f", im1, res.psnr) + } + s := im1.Bounds().Size() + tot := s.X * s.Y + if per := float32(100*res.diffCnt) / float32(tot); per > maxPixelDiffPercentage { + t.Errorf("%T not the same %d pixels different %.2f%%", im1, res.diffCnt, per) + } + } + +} diff --git a/pkg/misc/resize/testdata/test-resample-128x128-64x64.png b/pkg/misc/resize/testdata/test-resample-128x128-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..7663ba5f8e89886cb789fd6503816b3075f703e4 GIT binary patch literal 437 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 ? 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 WPaPFfUUP>0tD)X*#seK&zyAU9K`pib literal 0 HcmV?d00001 diff --git a/pkg/server/image.go b/pkg/server/image.go index 2157ab7eb..c49ecd15e 100644 --- a/pkg/server/image.go +++ b/pkg/server/image.go @@ -19,6 +19,7 @@ package server import ( "bytes" "errors" + "expvar" "fmt" "image" "image/jpeg" @@ -42,6 +43,11 @@ import ( const imageDebug = false +var ( + imageBytesServedVar = expvar.NewInt("image-bytes-served") + imageBytesFetchedVar = expvar.NewInt("image-bytes-fetched") +) + type ImageHandler struct { Fetcher blob.StreamingFetcher Cache blobserver.Storage // optional @@ -158,7 +164,8 @@ func (ih *ImageHandler) scaleImage(buf *bytes.Buffer, file blob.Ref) (format str } defer fr.Close() - _, err = io.Copy(buf, fr) + n, err := io.Copy(buf, fr) + imageBytesFetchedVar.Add(int64(n)) if err != nil { return format, fmt.Errorf("image resize: error reading image %s: %v", file, err) } @@ -250,6 +257,7 @@ func (ih *ImageHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, fil h.Set("Last-Modified", time.Now().Format(http.TimeFormat)) h.Set("Content-Type", imageContentTypeOfFormat(format)) size := buf.Len() + imageBytesServedVar.Add(int64(size)) h.Set("Content-Length", fmt.Sprintf("%d", size)) if req.Method == "GET" {