vp8: skip filtering for all-zero-DC macroblock residuals.
This makes the Go code match the libwebp C code's output on blue-purple-pink-large.*-filter.lossy.webp Also make the various WEBP benchmarks all decode a similar image, the image at http://blog.golang.org/gophercon/image01.jpg, to make it more meaningful to e.g. compare the simple filter's numbers with the normal filter's numbers. Also fix a "go vet" warning in webp/decode.go. The test data was generated by: wget http://blog.golang.org/gophercon/image01.jpg -O blue-purple-pink-large.jpeg convert blue-purple-pink-large.jpeg blue-purple-pink-large.png cwebp -lossless blue-purple-pink-large.png -o blue-purple-pink-large.lossless.webp cwebp -q 80 -f 0 blue-purple-pink-large.png -o blue-purple-pink-large.no-filter.lossy.webp cwebp -q 80 -strong blue-purple-pink-large.png -o blue-purple-pink-large.normal-filter.lossy.webp cwebp -q 80 -nostrong blue-purple-pink-large.png -o blue-purple-pink-large.simple-filter.lossy.webp dwebp -pgm blue-purple-pink-large.no-filter.lossy.webp -o tmp.pgm && convert tmp.pgm blue-purple-pink-large.no-filter.lossy.webp.ycbcr.png && rm tmp.pgm dwebp -pgm blue-purple-pink-large.normal-filter.lossy.webp -o tmp.pgm && convert tmp.pgm blue-purple-pink-large.normal-filter.lossy.webp.ycbcr.png && rm tmp.pgm dwebp -pgm blue-purple-pink-large.simple-filter.lossy.webp -o tmp.pgm && convert tmp.pgm blue-purple-pink-large.simple-filter.lossy.webp.ycbcr.png && rm tmp.pgm LGTM=r R=r CC=golang-codereviews https://golang.org/cl/106230044
BIN
testdata/blue-purple-pink-large.lossless.webp
vendored
Normal file
After Width: | Height: | Size: 171 KiB |
BIN
testdata/blue-purple-pink-large.no-filter.lossy.webp
vendored
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
testdata/blue-purple-pink-large.no-filter.lossy.webp.ycbcr.png
vendored
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
testdata/blue-purple-pink-large.normal-filter.lossy.webp
vendored
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
testdata/blue-purple-pink-large.normal-filter.lossy.webp.ycbcr.png
vendored
Normal file
After Width: | Height: | Size: 139 KiB |
BIN
testdata/blue-purple-pink-large.png
vendored
Normal file
After Width: | Height: | Size: 249 KiB |
BIN
testdata/blue-purple-pink-large.simple-filter.lossy.webp
vendored
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
testdata/blue-purple-pink-large.simple-filter.lossy.webp.ycbcr.png
vendored
Normal file
After Width: | Height: | Size: 129 KiB |
|
@ -266,8 +266,9 @@ func (d *Decoder) parseResiduals4(r *partition, plane int, context uint8, quant
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseResiduals parses the residuals.
|
// parseResiduals parses the residuals and returns whether inner loop filtering
|
||||||
func (d *Decoder) parseResiduals(mbx, mby int) {
|
// should be skipped for this macroblock.
|
||||||
|
func (d *Decoder) parseResiduals(mbx, mby int) (skip bool) {
|
||||||
partition := &d.op[mby&(d.nOP-1)]
|
partition := &d.op[mby&(d.nOP-1)]
|
||||||
plane := planeY1SansY2
|
plane := planeY1SansY2
|
||||||
quant := &d.quant[d.segment]
|
quant := &d.quant[d.segment]
|
||||||
|
@ -332,6 +333,11 @@ func (d *Decoder) parseResiduals(mbx, mby int) {
|
||||||
d.upMB[mbx].nzMask = uint8(unzMask)
|
d.upMB[mbx].nzMask = uint8(unzMask)
|
||||||
d.nzDCMask = nzDCMask
|
d.nzDCMask = nzDCMask
|
||||||
d.nzACMask = nzACMask
|
d.nzACMask = nzACMask
|
||||||
|
|
||||||
|
// Section 15.1 of the spec says that "Steps 2 and 4 [of the loop filter]
|
||||||
|
// are skipped... [if] there is no DCT coefficient coded for the whole
|
||||||
|
// macroblock."
|
||||||
|
return nzDCMask == 0 && nzACMask == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// reconstructMacroblock applies the predictor functions and adds the inverse-
|
// reconstructMacroblock applies the predictor functions and adds the inverse-
|
||||||
|
@ -384,7 +390,8 @@ func (d *Decoder) reconstructMacroblock(mbx, mby int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reconstruct reconstructs one macroblock.
|
// reconstruct reconstructs one macroblock and returns whether inner loop
|
||||||
|
// filtering should be skipped for it.
|
||||||
func (d *Decoder) reconstruct(mbx, mby int) (skip bool) {
|
func (d *Decoder) reconstruct(mbx, mby int) (skip bool) {
|
||||||
if d.segmentHeader.updateMap {
|
if d.segmentHeader.updateMap {
|
||||||
if !d.fp.readBit(d.segmentHeader.prob[0]) {
|
if !d.fp.readBit(d.segmentHeader.prob[0]) {
|
||||||
|
@ -411,9 +418,7 @@ func (d *Decoder) reconstruct(mbx, mby int) (skip bool) {
|
||||||
d.parsePredModeC8()
|
d.parsePredModeC8()
|
||||||
// Parse the residuals.
|
// Parse the residuals.
|
||||||
if !skip {
|
if !skip {
|
||||||
// TODO(nigeltao): make d.parseResiduals return a bool, and change this to
|
skip = d.parseResiduals(mbx, mby)
|
||||||
// skip = d.parseResiduals(mbx, mby)
|
|
||||||
d.parseResiduals(mbx, mby)
|
|
||||||
} else {
|
} else {
|
||||||
if d.usePredY16 {
|
if d.usePredY16 {
|
||||||
d.leftMB.nzY16 = 0
|
d.leftMB.nzY16 = 0
|
||||||
|
|
|
@ -65,7 +65,7 @@ func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) {
|
||||||
return m, image.Config{}, nil
|
return m, image.Config{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
r = &io.LimitedReader{r, int64(dataLen)}
|
r = &io.LimitedReader{R: r, N: int64(dataLen)}
|
||||||
if configOnly {
|
if configOnly {
|
||||||
c, err := vp8l.DecodeConfig(r)
|
c, err := vp8l.DecodeConfig(r)
|
||||||
return nil, c, err
|
return nil, c, err
|
||||||
|
|
|
@ -33,6 +33,9 @@ func hex(x []byte) string {
|
||||||
func TestDecodeVP8(t *testing.T) {
|
func TestDecodeVP8(t *testing.T) {
|
||||||
testCases := []string{
|
testCases := []string{
|
||||||
"blue-purple-pink",
|
"blue-purple-pink",
|
||||||
|
"blue-purple-pink-large.no-filter",
|
||||||
|
"blue-purple-pink-large.simple-filter",
|
||||||
|
"blue-purple-pink-large.normal-filter",
|
||||||
"video-001",
|
"video-001",
|
||||||
"yellow_rose",
|
"yellow_rose",
|
||||||
}
|
}
|
||||||
|
@ -40,17 +43,20 @@ func TestDecodeVP8(t *testing.T) {
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
f0, err := os.Open("../testdata/" + tc + ".lossy.webp")
|
f0, err := os.Open("../testdata/" + tc + ".lossy.webp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Errorf("%s: Open WEBP: %v", tc, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
defer f0.Close()
|
defer f0.Close()
|
||||||
img0, err := Decode(f0)
|
img0, err := Decode(f0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Errorf("%s: Decode WEBP: %v", tc, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
m0, ok := img0.(*image.YCbCr)
|
m0, ok := img0.(*image.YCbCr)
|
||||||
if !ok || m0.SubsampleRatio != image.YCbCrSubsampleRatio420 {
|
if !ok || m0.SubsampleRatio != image.YCbCrSubsampleRatio420 {
|
||||||
t.Fatal("decoded WEBP image is not a 4:2:0 YCbCr")
|
t.Errorf("%s: decoded WEBP image is not a 4:2:0 YCbCr", tc)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
// w2 and h2 are the half-width and half-height, rounded up.
|
// w2 and h2 are the half-width and half-height, rounded up.
|
||||||
w, h := m0.Bounds().Dx(), m0.Bounds().Dy()
|
w, h := m0.Bounds().Dx(), m0.Bounds().Dy()
|
||||||
|
@ -58,12 +64,14 @@ func TestDecodeVP8(t *testing.T) {
|
||||||
|
|
||||||
f1, err := os.Open("../testdata/" + tc + ".lossy.webp.ycbcr.png")
|
f1, err := os.Open("../testdata/" + tc + ".lossy.webp.ycbcr.png")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Errorf("%s: Open PNG: %v", tc, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
defer f1.Close()
|
defer f1.Close()
|
||||||
img1, err := png.Decode(f1)
|
img1, err := png.Decode(f1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Errorf("%s: Open PNG: %v", tc, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// The split-into-YCbCr-planes golden image is a 2*w2 wide and h+h2 high
|
// The split-into-YCbCr-planes golden image is a 2*w2 wide and h+h2 high
|
||||||
|
@ -73,11 +81,13 @@ func TestDecodeVP8(t *testing.T) {
|
||||||
// BBRR
|
// BBRR
|
||||||
// See http://www.fourcc.org/yuv.php#IMC4
|
// See http://www.fourcc.org/yuv.php#IMC4
|
||||||
if got, want := img1.Bounds(), image.Rect(0, 0, 2*w2, h+h2); got != want {
|
if got, want := img1.Bounds(), image.Rect(0, 0, 2*w2, h+h2); got != want {
|
||||||
t.Fatalf("bounds0: got %v, want %v", got, want)
|
t.Errorf("%s: bounds0: got %v, want %v", tc, got, want)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
m1, ok := img1.(*image.Gray)
|
m1, ok := img1.(*image.Gray)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("decoded PNG image is not a Gray")
|
t.Errorf("%s: decoded PNG image is not a Gray", tc)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
planes := []struct {
|
planes := []struct {
|
||||||
|
@ -101,14 +111,14 @@ func TestDecodeVP8(t *testing.T) {
|
||||||
}
|
}
|
||||||
nDiff++
|
nDiff++
|
||||||
if nDiff > 10 {
|
if nDiff > 10 {
|
||||||
t.Errorf("%s plane: more rows differ", plane.name)
|
t.Errorf("%s: %s plane: more rows differ", tc, plane.name)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
for i := range got {
|
for i := range got {
|
||||||
diff[i] = got[i] - want[i]
|
diff[i] = got[i] - want[i]
|
||||||
}
|
}
|
||||||
t.Errorf("%s plane: m0 row %d, m1 row %d\ngot %s\nwant%s\ndiff%s",
|
t.Errorf("%s: %s plane: m0 row %d, m1 row %d\ngot %s\nwant%s\ndiff%s",
|
||||||
plane.name, j, y, hex(got), hex(want), hex(diff))
|
tc, plane.name, j, y, hex(got), hex(want), hex(diff))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,6 +127,7 @@ func TestDecodeVP8(t *testing.T) {
|
||||||
func TestDecodeVP8L(t *testing.T) {
|
func TestDecodeVP8L(t *testing.T) {
|
||||||
testCases := []string{
|
testCases := []string{
|
||||||
"blue-purple-pink",
|
"blue-purple-pink",
|
||||||
|
"blue-purple-pink-large",
|
||||||
"gopher-doc.1bpp",
|
"gopher-doc.1bpp",
|
||||||
"gopher-doc.2bpp",
|
"gopher-doc.2bpp",
|
||||||
"gopher-doc.4bpp",
|
"gopher-doc.4bpp",
|
||||||
|
@ -197,7 +208,7 @@ loop:
|
||||||
}
|
}
|
||||||
|
|
||||||
func benchmarkDecode(b *testing.B, filename string) {
|
func benchmarkDecode(b *testing.B, filename string) {
|
||||||
data, err := ioutil.ReadFile("../testdata/" + filename + ".webp")
|
data, err := ioutil.ReadFile("../testdata/blue-purple-pink-large." + filename + ".webp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -213,6 +224,7 @@ func benchmarkDecode(b *testing.B, filename string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkDecodeVP8SimpleFilter(b *testing.B) { benchmarkDecode(b, "blue-purple-pink.lossy") }
|
func BenchmarkDecodeVP8NoFilter(b *testing.B) { benchmarkDecode(b, "no-filter.lossy") }
|
||||||
func BenchmarkDecodeVP8NormalFilter(b *testing.B) { benchmarkDecode(b, "yellow_rose.lossy") }
|
func BenchmarkDecodeVP8SimpleFilter(b *testing.B) { benchmarkDecode(b, "simple-filter.lossy") }
|
||||||
func BenchmarkDecodeVP8L(b *testing.B) { benchmarkDecode(b, "yellow_rose.lossless") }
|
func BenchmarkDecodeVP8NormalFilter(b *testing.B) { benchmarkDecode(b, "normal-filter.lossy") }
|
||||||
|
func BenchmarkDecodeVP8L(b *testing.B) { benchmarkDecode(b, "lossless") }
|
||||||
|
|