Skip to content

Commit 92e0934

Browse files
rscgopherbot
authored andcommitted
image/jpeg: correct and test reference slowFDCT and slowIDCT
The reference implementations slowFDCT and slowIDCT were not rounding correctly, making the test not as good as it could be. Before, the real implementations were required to always produce values within ±2 of the reference; now, with no changes, the real implementations produce values within ±1 of the (corrected) reference. Also tighten the test to return an error not just on a single value exceeding tolerance but also on too many values at exactly that tolerance. Change-Id: I3dd6ca7582178fef972fb812d848f7a0158a6ed8 Reviewed-on: https://go-review.googlesource.com/c/go/+/705517 Auto-Submit: Russ Cox <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 27c7bbc commit 92e0934

File tree

2 files changed

+218
-50
lines changed

2 files changed

+218
-50
lines changed

src/image/jpeg/dct_test.go

Lines changed: 216 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package jpeg
77
import (
88
"fmt"
99
"math"
10+
"math/big"
1011
"math/rand"
1112
"strings"
1213
"testing"
@@ -29,11 +30,37 @@ func BenchmarkIDCT(b *testing.B) {
2930
benchmarkDCT(b, idct)
3031
}
3132

33+
const testSlowVsBig = true
34+
3235
func TestDCT(t *testing.T) {
3336
blocks := make([]block, len(testBlocks))
3437
copy(blocks, testBlocks[:])
3538

36-
// Append some randomly generated blocks of varying sparseness.
39+
// All zeros
40+
blocks = append(blocks, block{})
41+
42+
// Every possible unit impulse.
43+
for i := range blockSize {
44+
var b block
45+
b[i] = 255
46+
blocks = append(blocks, b)
47+
}
48+
49+
// All ones.
50+
var ones block
51+
for i := range ones {
52+
ones[i] = 255
53+
}
54+
blocks = append(blocks, ones)
55+
56+
// Every possible inverted unit impulse.
57+
for i := range blockSize {
58+
ones[i] = 0
59+
blocks = append(blocks, ones)
60+
ones[i] = 255
61+
}
62+
63+
// Some randomly generated blocks of varying sparseness.
3764
r := rand.New(rand.NewSource(123))
3865
for i := 0; i < 100; i++ {
3966
b := block{}
@@ -44,57 +71,84 @@ func TestDCT(t *testing.T) {
4471
blocks = append(blocks, b)
4572
}
4673

47-
// Check that the FDCT and IDCT functions are inverses, after a scale and
48-
// level shift. Scaling reduces the rounding errors in the conversion from
49-
// floats to ints.
50-
for i, b := range blocks {
51-
got, want := b, b
52-
slowFDCT(&got)
53-
slowIDCT(&got)
54-
for j := range got {
55-
got[j] = got[j]/8 + 128
56-
}
57-
if d := differ(&got, &want, 2); d >= 0 {
58-
t.Errorf("i=%d: IDCT(FDCT) (diff at %d,%d)\nsrc\n%s\ngot\n%s\nwant\n%s\n", i, d/8, d%8, &b, &got, &want)
74+
// Check that the slow FDCT and IDCT functions are inverses,
75+
// after a scale and level shift.
76+
// Scaling reduces the rounding errors in the conversion.
77+
// The “fast” ones are not inverses because the fast IDCT
78+
// is optimized for 8-bit inputs, not full 16-bit ones.
79+
slowRoundTrip := func(b *block) {
80+
slowFDCT(b)
81+
slowIDCT(b)
82+
for j := range b {
83+
b[j] = b[j]/8 + 128
5984
}
6085
}
86+
nop := func(*block) {}
87+
testDCT(t, "IDCT(FDCT)", blocks, slowRoundTrip, nop, 1, 8)
6188

62-
// Check that the optimized and slow FDCT implementations agree.
63-
// The fdct function already does a scale and level shift.
64-
for i, b := range blocks {
65-
got, want := b, b
66-
fdct(&got)
67-
slowFDCT(&want)
68-
if d := differ(&got, &want, 2); d >= 0 {
69-
t.Errorf("i=%d: FDCT (diff at %d,%d)\nsrc\n%s\ngot\n%s\nwant\n%s\n", i, d/8, d%8, &b, &got, &want)
70-
}
89+
if testSlowVsBig {
90+
testDCT(t, "slowFDCT", blocks, slowFDCT, slowerFDCT, 0, 64)
91+
testDCT(t, "slowIDCT", blocks, slowIDCT, slowerIDCT, 0, 64)
7192
}
7293

73-
// Check that the optimized and slow IDCT implementations agree.
74-
for i, b := range blocks {
75-
got, want := b, b
76-
idct(&got)
77-
slowIDCT(&want)
78-
if d := differ(&got, &want, 2); d >= 0 {
79-
t.Errorf("i=%d: IDCT (diff at %d,%d)\nsrc\n%s\ngot\n%s\nwant\n%s\n", i, d/8, d%8, &b, &got, &want)
94+
// Check that the optimized and slow FDCT implementations agree.
95+
testDCT(t, "FDCT", blocks, fdct, slowFDCT, 1, 16)
96+
testDCT(t, "IDCT", blocks, idct, slowIDCT, 1, 8)
97+
}
98+
99+
func testDCT(t *testing.T, name string, blocks []block, fhave, fwant func(*block), tolerance int32, maxCloseCalls int) {
100+
t.Run(name, func(t *testing.T) {
101+
totalClose := 0
102+
for i, b := range blocks {
103+
have, want := b, b
104+
fhave(&have)
105+
fwant(&want)
106+
d, n := differ(&have, &want, tolerance)
107+
if d >= 0 || n > maxCloseCalls {
108+
fail := ""
109+
if d >= 0 {
110+
fail = fmt.Sprintf("diff at %d,%d", d/8, d%8)
111+
}
112+
if n > maxCloseCalls {
113+
if fail != "" {
114+
fail += "; "
115+
}
116+
fail += fmt.Sprintf("%d close calls", n)
117+
}
118+
t.Errorf("i=%d: %s (%s)\nsrc\n%s\nhave\n%s\nwant\n%s\n",
119+
i, name, fail, &b, &have, &want)
120+
}
121+
totalClose += n
80122
}
81-
}
123+
if tolerance > 0 {
124+
t.Logf("%d/%d total close calls", totalClose, len(blocks)*blockSize)
125+
}
126+
})
82127
}
83128

84-
// differ reports whether any pair-wise elements in b0 and b1 differ by more than 'ok'.
85-
// That tolerance is because there isn't a single definitive decoding of
86-
// a given JPEG image, even before the YCbCr to RGB conversion; implementations
129+
// differ returns the index of the first pair-wise elements in b0 and b1
130+
// that differ by more than 'ok', along with the total number of elements
131+
// that differ by at least ok ("close calls").
132+
//
133+
// There isn't a single definitive decoding of a given JPEG image,
134+
// even before the YCbCr to RGB conversion; implementations
87135
// can have different IDCT rounding errors.
88-
// If there is a difference, differ returns the index of the first difference.
89-
// Otherwise it returns -1.
90-
func differ(b0, b1 *block, ok int32) int {
136+
//
137+
// If there are no differences, differ returns -1, 0.
138+
func differ(b0, b1 *block, ok int32) (index, closeCalls int) {
139+
index = -1
91140
for i := range b0 {
92141
delta := b0[i] - b1[i]
93142
if delta < -ok || ok < delta {
94-
return i
143+
if index < 0 {
144+
index = i
145+
}
146+
}
147+
if delta <= -ok || ok <= delta {
148+
closeCalls++
95149
}
96150
}
97-
return -1
151+
return
98152
}
99153

100154
// alpha returns 1 if i is 0 and returns √2 otherwise.
@@ -105,6 +159,14 @@ func alpha(i int) float64 {
105159
return math.Sqrt2
106160
}
107161

162+
// bigAlpha returns 1 if i is 0 and returns √2 otherwise.
163+
func bigAlpha(i int) *big.Float {
164+
if i == 0 {
165+
return bigFloat1
166+
}
167+
return bigFloatSqrt2
168+
}
169+
108170
var cosines = [32]float64{
109171
+1.0000000000000000000000000000000000000000000000000000000000000000, // cos(π/16 * 0)
110172
+0.9807852804032304491261822361342390369739337308933360950029160885, // cos(π/16 * 1)
@@ -143,6 +205,57 @@ var cosines = [32]float64{
143205
+0.9807852804032304491261822361342390369739337308933360950029160885, // cos(π/16 * 31)
144206
}
145207

208+
func bigFloat(s string) *big.Float {
209+
f, ok := new(big.Float).SetString(s)
210+
if !ok {
211+
panic("bad float")
212+
}
213+
return f
214+
}
215+
216+
var (
217+
bigFloat1 = big.NewFloat(1)
218+
bigFloatSqrt2 = bigFloat("1.41421356237309504880168872420969807856967187537694807317667974")
219+
)
220+
221+
var bigCosines = [32]*big.Float{
222+
bigFloat("+1.0000000000000000000000000000000000000000000000000000000000000000"), // cos(π/16 * 0)
223+
bigFloat("+0.9807852804032304491261822361342390369739337308933360950029160885"), // cos(π/16 * 1)
224+
bigFloat("+0.9238795325112867561281831893967882868224166258636424861150977312"), // cos(π/16 * 2)
225+
bigFloat("+0.8314696123025452370787883776179057567385608119872499634461245902"), // cos(π/16 * 3)
226+
bigFloat("+0.7071067811865475244008443621048490392848359376884740365883398689"), // cos(π/16 * 4)
227+
bigFloat("+0.5555702330196022247428308139485328743749371907548040459241535282"), // cos(π/16 * 5)
228+
bigFloat("+0.3826834323650897717284599840303988667613445624856270414338006356"), // cos(π/16 * 6)
229+
bigFloat("+0.1950903220161282678482848684770222409276916177519548077545020894"), // cos(π/16 * 7)
230+
231+
bigFloat("-0.0000000000000000000000000000000000000000000000000000000000000000"), // cos(π/16 * 8)
232+
bigFloat("-0.1950903220161282678482848684770222409276916177519548077545020894"), // cos(π/16 * 9)
233+
bigFloat("-0.3826834323650897717284599840303988667613445624856270414338006356"), // cos(π/16 * 10)
234+
bigFloat("-0.5555702330196022247428308139485328743749371907548040459241535282"), // cos(π/16 * 11)
235+
bigFloat("-0.7071067811865475244008443621048490392848359376884740365883398689"), // cos(π/16 * 12)
236+
bigFloat("-0.8314696123025452370787883776179057567385608119872499634461245902"), // cos(π/16 * 13)
237+
bigFloat("-0.9238795325112867561281831893967882868224166258636424861150977312"), // cos(π/16 * 14)
238+
bigFloat("-0.9807852804032304491261822361342390369739337308933360950029160885"), // cos(π/16 * 15)
239+
240+
bigFloat("-1.0000000000000000000000000000000000000000000000000000000000000000"), // cos(π/16 * 16)
241+
bigFloat("-0.9807852804032304491261822361342390369739337308933360950029160885"), // cos(π/16 * 17)
242+
bigFloat("-0.9238795325112867561281831893967882868224166258636424861150977312"), // cos(π/16 * 18)
243+
bigFloat("-0.8314696123025452370787883776179057567385608119872499634461245902"), // cos(π/16 * 19)
244+
bigFloat("-0.7071067811865475244008443621048490392848359376884740365883398689"), // cos(π/16 * 20)
245+
bigFloat("-0.5555702330196022247428308139485328743749371907548040459241535282"), // cos(π/16 * 21)
246+
bigFloat("-0.3826834323650897717284599840303988667613445624856270414338006356"), // cos(π/16 * 22)
247+
bigFloat("-0.1950903220161282678482848684770222409276916177519548077545020894"), // cos(π/16 * 23)
248+
249+
bigFloat("+0.0000000000000000000000000000000000000000000000000000000000000000"), // cos(π/16 * 24)
250+
bigFloat("+0.1950903220161282678482848684770222409276916177519548077545020894"), // cos(π/16 * 25)
251+
bigFloat("+0.3826834323650897717284599840303988667613445624856270414338006356"), // cos(π/16 * 26)
252+
bigFloat("+0.5555702330196022247428308139485328743749371907548040459241535282"), // cos(π/16 * 27)
253+
bigFloat("+0.7071067811865475244008443621048490392848359376884740365883398689"), // cos(π/16 * 28)
254+
bigFloat("+0.8314696123025452370787883776179057567385608119872499634461245902"), // cos(π/16 * 29)
255+
bigFloat("+0.9238795325112867561281831893967882868224166258636424861150977312"), // cos(π/16 * 30)
256+
bigFloat("+0.9807852804032304491261822361342390369739337308933360950029160885"), // cos(π/16 * 31)
257+
}
258+
146259
// slowFDCT performs the 8*8 2-dimensional forward discrete cosine transform:
147260
//
148261
// dst[u,v] = (1/8) * Σ_x Σ_y alpha(u) * alpha(v) * src[x,y] *
@@ -153,7 +266,7 @@ var cosines = [32]float64{
153266
//
154267
// b acts as both dst and src.
155268
func slowFDCT(b *block) {
156-
var dst [blockSize]float64
269+
var dst block
157270
for v := 0; v < 8; v++ {
158271
for u := 0; u < 8; u++ {
159272
sum := 0.0
@@ -164,13 +277,40 @@ func slowFDCT(b *block) {
164277
cosines[((2*y+1)*v)%32]
165278
}
166279
}
167-
dst[8*v+u] = sum
280+
dst[8*v+u] = int32(math.Round(sum))
168281
}
169282
}
170-
// Convert from float64 to int32.
171-
for i := range dst {
172-
b[i] = int32(dst[i] + 0.5)
283+
*b = dst
284+
}
285+
286+
// slowerFDCT is slowFDCT but using big.Floats to validate slowFDCT.
287+
func slowerFDCT(b *block) {
288+
var dst block
289+
for v := 0; v < 8; v++ {
290+
for u := 0; u < 8; u++ {
291+
sum := big.NewFloat(0)
292+
for y := 0; y < 8; y++ {
293+
for x := 0; x < 8; x++ {
294+
f := big.NewFloat(float64(b[8*y+x] - 128))
295+
f = new(big.Float).Mul(f, bigAlpha(u))
296+
f = new(big.Float).Mul(f, bigAlpha(v))
297+
f = new(big.Float).Mul(f, bigCosines[((2*x+1)*u)%32])
298+
f = new(big.Float).Mul(f, bigCosines[((2*y+1)*v)%32])
299+
sum = new(big.Float).Add(sum, f)
300+
}
301+
}
302+
// Int64 truncates toward zero, so add ±0.5
303+
// as needed to round
304+
if sum.Sign() > 0 {
305+
sum = new(big.Float).Add(sum, big.NewFloat(+0.5))
306+
} else {
307+
sum = new(big.Float).Add(sum, big.NewFloat(-0.5))
308+
}
309+
i, _ := sum.Int64()
310+
dst[8*v+u] = int32(i)
311+
}
173312
}
313+
*b = dst
174314
}
175315

176316
// slowIDCT performs the 8*8 2-dimensional inverse discrete cosine transform:
@@ -183,7 +323,7 @@ func slowFDCT(b *block) {
183323
//
184324
// b acts as both dst and src.
185325
func slowIDCT(b *block) {
186-
var dst [blockSize]float64
326+
var dst block
187327
for y := 0; y < 8; y++ {
188328
for x := 0; x < 8; x++ {
189329
sum := 0.0
@@ -194,13 +334,41 @@ func slowIDCT(b *block) {
194334
cosines[((2*y+1)*v)%32]
195335
}
196336
}
197-
dst[8*y+x] = sum / 8
337+
dst[8*y+x] = int32(math.Round(sum / 8))
198338
}
199339
}
200-
// Convert from float64 to int32.
201-
for i := range dst {
202-
b[i] = int32(dst[i] + 0.5)
340+
*b = dst
341+
}
342+
343+
// slowerIDCT is slowIDCT but using big.Floats to validate slowIDCT.
344+
func slowerIDCT(b *block) {
345+
var dst block
346+
for y := 0; y < 8; y++ {
347+
for x := 0; x < 8; x++ {
348+
sum := big.NewFloat(0)
349+
for v := 0; v < 8; v++ {
350+
for u := 0; u < 8; u++ {
351+
f := big.NewFloat(float64(b[8*v+u]))
352+
f = new(big.Float).Mul(f, bigAlpha(u))
353+
f = new(big.Float).Mul(f, bigAlpha(v))
354+
f = new(big.Float).Mul(f, bigCosines[((2*x+1)*u)%32])
355+
f = new(big.Float).Mul(f, bigCosines[((2*y+1)*v)%32])
356+
f = new(big.Float).Quo(f, big.NewFloat(8))
357+
sum = new(big.Float).Add(sum, f)
358+
}
359+
}
360+
// Int64 truncates toward zero, so add ±0.5
361+
// as needed to round
362+
if sum.Sign() > 0 {
363+
sum = new(big.Float).Add(sum, big.NewFloat(+0.5))
364+
} else {
365+
sum = new(big.Float).Add(sum, big.NewFloat(-0.5))
366+
}
367+
i, _ := sum.Int64()
368+
dst[8*y+x] = int32(i)
369+
}
203370
}
371+
*b = dst
204372
}
205373

206374
func (b *block) String() string {

src/image/jpeg/writer_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ func TestWriter(t *testing.T) {
154154
continue
155155
}
156156
// Compare the average delta to the tolerance level.
157-
if averageDelta(m0, m1) > tc.tolerance {
158-
t.Errorf("%s, quality=%d: average delta is too high", tc.filename, tc.quality)
157+
if d := averageDelta(m0, m1); d > tc.tolerance {
158+
t.Errorf("%s, quality=%d: average delta is too high (%d > %d)", tc.filename, tc.quality, d, tc.tolerance)
159159
continue
160160
}
161161
}

0 commit comments

Comments
 (0)