Skip to content

Commit

Permalink
image/png: optimise RGBA encoding
Browse files Browse the repository at this point in the history
Optimised RGBA image encoding to PNG. Improved test coverage. Reworked benchmark.

Performance improvement with both old and new versions of the benchmark:

name                           old speed      new speed      delta
EncodeRGBA_OriginalVersion-10   115MB/s ± 1%   308MB/s ± 1%  +166.70%  (p=0.000 n=19+17)
EncodeRGBA_NewVersion-10       40.3MB/s ± 1%  51.1MB/s ± 2%   +26.93%  (p=0.000 n=18+20)

name                           old allocs/op  new allocs/op  delta
EncodeRGBA_OriginalVersion-10      614k ± 0%        0k ± 0%   -99.99%  (p=0.000 n=20+20)
EncodeRGBA_NewVersion-10           614k ± 0%        0k ± 0%   -99.99%  (p=0.000 n=20+20)

Change-Id: I450013909c2410b043cd9c1239facd5bd6e3f3f9
GitHub-Last-Rev: 329d6ac011b08efcba5c1d737ba5395b0a66a6ea
GitHub-Pull-Request: golang/go#55119
Reviewed-on: https://go-review.googlesource.com/c/go/+/431575
Reviewed-by: Dmitri Shuralyov <[email protected]>
Run-TryBot: Nigel Tao <[email protected]>
Reviewed-by: Nigel Tao (INACTIVE; USE @golang.org INSTEAD) <[email protected]>
TryBot-Result: Gopher Robot <[email protected]>
Reviewed-by: Nigel Tao <[email protected]>
  • Loading branch information
alx-ef authored and nigeltao committed Oct 14, 2022
1 parent 7feb687 commit 76e4833
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 3 deletions.
30 changes: 30 additions & 0 deletions src/image/png/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,36 @@ func (e *encoder) writeImage(w io.Writer, m image.Image, cb int, level int) erro
if nrgba != nil {
offset := (y - b.Min.Y) * nrgba.Stride
copy(cr[0][1:], nrgba.Pix[offset:offset+b.Dx()*4])
} else if rgba != nil {
dst := cr[0][1:]
src := rgba.Pix[rgba.PixOffset(b.Min.X, y):rgba.PixOffset(b.Max.X, y)]
for ; len(src) >= 4; dst, src = dst[4:], src[4:] {
d := (*[4]byte)(dst)
s := (*[4]byte)(src)
if s[3] == 0x00 {
d[0] = 0
d[1] = 0
d[2] = 0
d[3] = 0
} else if s[3] == 0xff {
copy(d[:], s[:])
} else {
// This code does the same as color.NRGBAModel.Convert(
// rgba.At(x, y)).(color.NRGBA) but with no extra memory
// allocations or interface/function call overhead.
//
// The multiplier m combines 0x101 (which converts
// 8-bit color to 16-bit color) and 0xffff (which, when
// combined with the division-by-a, converts from
// alpha-premultiplied to non-alpha-premultiplied).
const m = 0x101 * 0xffff
a := uint32(s[3]) * 0x101
d[0] = uint8((uint32(s[0]) * m / a) >> 8)
d[1] = uint8((uint32(s[1]) * m / a) >> 8)
d[2] = uint8((uint32(s[2]) * m / a) >> 8)
d[3] = s[3]
}
}
} else {
// Convert from image.Image (which is alpha-premultiplied) to PNG's non-alpha-premultiplied.
for x := b.Min.X; x < b.Max.X; x++ {
Expand Down
71 changes: 68 additions & 3 deletions src/image/png/writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"fmt"
"image"
"image/color"
"image/draw"
"io"
"testing"
)
Expand All @@ -29,7 +30,7 @@ func diff(m0, m1 image.Image) error {
r0, g0, b0, a0 := c0.RGBA()
r1, g1, b1, a1 := c1.RGBA()
if r0 != r1 || g0 != g1 || b0 != b1 || a0 != a1 {
return fmt.Errorf("colors differ at (%d, %d): %v vs %v", x, y, c0, c1)
return fmt.Errorf("colors differ at (%d, %d): %T%v vs %T%v", x, y, c0, c0, c1, c1)
}
}
}
Expand All @@ -45,6 +46,13 @@ func encodeDecode(m image.Image) (image.Image, error) {
return Decode(&b)
}

func convertToNRGBA(m image.Image) *image.NRGBA {
b := m.Bounds()
ret := image.NewNRGBA(b)
draw.Draw(ret, b, m, b.Min, draw.Src)
return ret
}

func TestWriter(t *testing.T) {
// The filenames variable is declared in reader_test.go.
names := filenames
Expand Down Expand Up @@ -227,6 +235,49 @@ func TestSubImage(t *testing.T) {
}
}

func TestWriteRGBA(t *testing.T) {
const width, height = 640, 480
transparentImg := image.NewRGBA(image.Rect(0, 0, width, height))
opaqueImg := image.NewRGBA(image.Rect(0, 0, width, height))
mixedImg := image.NewRGBA(image.Rect(0, 0, width, height))
translucentImg := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
opaqueColor := color.RGBA{uint8(x), uint8(y), uint8(y + x), 255}
translucentColor := color.RGBA{uint8(x) % 128, uint8(y) % 128, uint8(y+x) % 128, 128}
opaqueImg.Set(x, y, opaqueColor)
translucentImg.Set(x, y, translucentColor)
if y%2 == 0 {
mixedImg.Set(x, y, opaqueColor)
}
}
}

testCases := []struct {
name string
img image.Image
}{
{"Transparent RGBA", transparentImg},
{"Opaque RGBA", opaqueImg},
{"50/50 Transparent/Opaque RGBA", mixedImg},
{"RGBA with variable alpha", translucentImg},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
m0 := tc.img
m1, err := encodeDecode(m0)
if err != nil {
t.Fatal(err)
}
err = diff(convertToNRGBA(m0), m1)
if err != nil {
t.Error(err)
}
})
}
}

func BenchmarkEncodeGray(b *testing.B) {
img := image.NewGray(image.Rect(0, 0, 640, 480))
b.SetBytes(640 * 480 * 1)
Expand Down Expand Up @@ -329,11 +380,25 @@ func BenchmarkEncodeRGBOpaque(b *testing.B) {
}

func BenchmarkEncodeRGBA(b *testing.B) {
img := image.NewRGBA(image.Rect(0, 0, 640, 480))
const width, height = 640, 480
img := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
percent := (x + y) % 100
switch {
case percent < 10: // 10% of pixels are translucent (have alpha >0 and <255)
img.Set(x, y, color.NRGBA{uint8(x), uint8(y), uint8(x * y), uint8(percent)})
case percent < 40: // 30% of pixels are transparent (have alpha == 0)
img.Set(x, y, color.NRGBA{uint8(x), uint8(y), uint8(x * y), 0})
default: // 60% of pixels are opaque (have alpha == 255)
img.Set(x, y, color.NRGBA{uint8(x), uint8(y), uint8(x * y), 255})
}
}
}
if img.Opaque() {
b.Fatal("expected image not to be opaque")
}
b.SetBytes(640 * 480 * 4)
b.SetBytes(width * height * 4)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Expand Down

0 comments on commit 76e4833

Please sign in to comment.