Skip to content

Commit 7e79fff

Browse files
committed
fix: improve resize for bilinear and bicubic interpolations
Also add more tests. Refs: #452
1 parent 2535f4f commit 7e79fff

File tree

38 files changed

+149
-77
lines changed

38 files changed

+149
-77
lines changed
Loading
Loading

src/geometry/__tests__/resize.test.ts

Lines changed: 106 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
import path from 'node:path';
22

3-
import { encodePng, write } from '../../save';
3+
import { Image } from '../../Image';
4+
import { write } from '../../save';
5+
6+
async function writeDebug(resized: Image, type: string) {
7+
// @ts-expect-error Dynamic string.
8+
const expected = testUtils.load(`opencv/test_resize_${type}.png`);
9+
await write(path.join(__dirname, `resize_${type}_expected.png`), expected);
10+
await write(path.join(__dirname, `resize_${type}_resized.png`), resized);
11+
const subtraction = expected.subtract(resized);
12+
await write(
13+
path.join(__dirname, `resize_${type}_subtraction.png`),
14+
subtraction,
15+
);
16+
}
417

5-
test('compare result of resize with opencv (nearest)', async () => {
18+
test('compare with OpenCV (nearest, larger)', async () => {
619
const img = testUtils.load('opencv/test.png');
720

821
const resized = img.resize({
@@ -11,62 +24,132 @@ test('compare result of resize with opencv (nearest)', async () => {
1124
interpolationType: 'nearest',
1225
});
1326

14-
expect(resized).toMatchImage('opencv/testResizeNearest.png');
27+
expect(resized).toMatchImage('opencv/test_resize_nearest_larger.png');
28+
});
29+
30+
test('compare with OpenCV (nearest, same size)', async () => {
31+
const img = testUtils.load('opencv/test.png');
32+
33+
const resized = img.resize({
34+
xFactor: 1,
35+
interpolationType: 'nearest',
36+
});
37+
38+
expect(resized).toMatchImage('opencv/test_resize_nearest_same.png');
39+
});
40+
41+
test('compare with OpenCV (nearest, smaller)', async () => {
42+
const img = testUtils.load('opencv/test.png');
43+
44+
const resized = img.resize({
45+
width: 5,
46+
height: 6,
47+
interpolationType: 'nearest',
48+
});
49+
50+
expect(resized).toMatchImage('opencv/test_resize_nearest_smaller.png');
1551
});
1652

17-
test.skip('compare result of resize with opencv (bilinear)', async () => {
53+
test.skip('compare with OpenCV (bilinear, larger)', async () => {
1854
const img = testUtils.load('opencv/test.png');
19-
const expectedImg = testUtils.load('opencv/testResizeBilinear.png');
2055

2156
const resized = img.resize({
2257
xFactor: 10,
2358
yFactor: 10,
2459
});
2560

26-
const substraction = expectedImg.clone().subtract(resized);
27-
await write(
28-
path.join(__dirname, 'resize_bilinear_substraction.png'),
29-
substraction,
30-
);
31-
await write(path.join(__dirname, 'resize_bilinear.png'), resized);
61+
await writeDebug(resized, 'bilinear_larger');
3262

33-
expect(resized).toMatchImage('opencv/testResizeBilinear.png');
63+
expect(resized).toMatchImage('opencv/test_resize_bilinear_larger.png');
3464
});
3565

36-
test('result should have correct dimensions', () => {
66+
test.skip('compare with OpenCV (bilinear, same)', async () => {
67+
const img = testUtils.load('opencv/test.png');
68+
69+
const resized = img.resize({
70+
xFactor: 1,
71+
});
72+
73+
await writeDebug(resized, 'bilinear_same');
74+
75+
expect(resized).toMatchImage('opencv/test_resize_bilinear_same.png');
76+
});
77+
78+
test.skip('compare with OpenCV (bilinear, smaller)', async () => {
79+
const img = testUtils.load('opencv/test.png');
80+
81+
const resized = img.resize({
82+
width: 5,
83+
height: 6,
84+
});
85+
86+
await writeDebug(resized, 'bilinear_smaller');
87+
88+
expect(resized).toMatchImage('opencv/test_resize_bilinear_smaller.png');
89+
});
90+
91+
test.skip('compare with OpenCV (bicubic, larger)', async () => {
3792
const img = testUtils.load('opencv/test.png');
3893

3994
const resized = img.resize({
4095
xFactor: 10,
4196
yFactor: 10,
97+
interpolationType: 'bicubic',
4298
});
43-
expect(resized.width).toBe(10 * img.width);
44-
expect(resized.height).toBe(10 * img.height);
99+
100+
await writeDebug(resized, 'bicubic_larger');
101+
102+
expect(resized).toMatchImage('opencv/test_resize_bicubic_larger.png');
45103
});
46104

47-
test('resize to given width and height', () => {
105+
test.skip('compare with OpenCV (bicubic, same)', async () => {
48106
const img = testUtils.load('opencv/test.png');
49107

50108
const resized = img.resize({
51-
width: 300,
52-
height: 100,
109+
xFactor: 1,
110+
interpolationType: 'bicubic',
53111
});
54112

55-
expect(resized.width).toBe(300);
56-
expect(resized.height).toBe(100);
113+
await writeDebug(resized, 'bicubic_same');
114+
115+
expect(resized).toMatchImage('opencv/test_resize_bicubic_same.png');
116+
});
117+
118+
test.skip('compare with OpenCV (bicubic, smaller)', async () => {
119+
const img = testUtils.load('opencv/test.png');
120+
121+
const resized = img.resize({
122+
width: 5,
123+
height: 6,
124+
interpolationType: 'bicubic',
125+
});
126+
127+
await writeDebug(resized, 'bicubic_smaller');
128+
129+
expect(resized).toMatchImage('opencv/test_resize_bicubic_smaller.png');
57130
});
58131

59-
test('has to match snapshot', () => {
132+
test('result should have correct dimensions', () => {
60133
const img = testUtils.load('opencv/test.png');
61134

62135
const resized = img.resize({
63136
xFactor: 10,
64137
yFactor: 10,
65138
});
139+
expect(resized.width).toBe(10 * img.width);
140+
expect(resized.height).toBe(10 * img.height);
141+
});
66142

67-
const png = Buffer.from(encodePng(resized.convertColor('GREY')));
143+
test('resize to given width and height', () => {
144+
const img = testUtils.load('opencv/test.png');
145+
146+
const resized = img.resize({
147+
width: 300,
148+
height: 100,
149+
});
68150

69-
expect(png).toMatchImageSnapshot();
151+
expect(resized.width).toBe(300);
152+
expect(resized.height).toBe(100);
70153
});
71154

72155
test('aspect ratio not preserved', () => {

src/geometry/resize.ts

Lines changed: 17 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { Image } from '../Image';
2-
import { getClamp } from '../utils/clamp';
3-
import { BorderType, getBorderInterpolation } from '../utils/interpolateBorder';
4-
import {
5-
getInterpolationFunction,
6-
InterpolationType,
7-
} from '../utils/interpolatePixel';
2+
import { BorderType } from '../utils/interpolateBorder';
3+
import { InterpolationType } from '../utils/interpolatePixel';
84
import { assert } from '../utils/validators/assert';
95

106
import { transform } from './transform';
@@ -57,52 +53,25 @@ export interface ResizeOptions {
5753
export function resize(image: Image, options: ResizeOptions): Image {
5854
const {
5955
interpolationType = 'bilinear',
60-
borderType = 'constant',
56+
borderType = 'replicate',
6157
borderValue = 0,
6258
} = options;
6359
const { width, height, xFactor, yFactor } = checkOptions(image, options);
6460

65-
if (interpolationType === 'nearest') {
66-
return transform(
67-
image,
68-
[
69-
[xFactor, 0, xFactor / 2],
70-
[0, yFactor, yFactor / 2],
71-
],
72-
{
73-
interpolationType,
74-
borderType,
75-
borderValue,
76-
height,
77-
width,
78-
},
79-
);
80-
}
81-
82-
const newImage = Image.createFrom(image, { width, height });
83-
const interpolate = getInterpolationFunction(interpolationType);
84-
const interpolateBorder = getBorderInterpolation(borderType, borderValue);
85-
const clamp = getClamp(newImage);
86-
const intervalX = (image.width - 1) / (width - 1);
87-
const intervalY = (image.height - 1) / (height - 1);
88-
for (let row = 0; row < newImage.height; row++) {
89-
for (let column = 0; column < newImage.width; column++) {
90-
const nx = column * intervalX;
91-
const ny = row * intervalY;
92-
for (let channel = 0; channel < newImage.channels; channel++) {
93-
const newValue = interpolate(
94-
image,
95-
nx,
96-
ny,
97-
channel,
98-
interpolateBorder,
99-
clamp,
100-
);
101-
newImage.setValue(column, row, channel, newValue);
102-
}
103-
}
104-
}
105-
return newImage;
61+
return transform(
62+
image,
63+
[
64+
[xFactor, 0, xFactor / 2],
65+
[0, yFactor, yFactor / 2],
66+
],
67+
{
68+
interpolationType,
69+
borderType,
70+
borderValue,
71+
height,
72+
width,
73+
},
74+
);
10675
}
10776

10877
/**

test/TestImagePath.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,15 @@ export type TestImagePath =
7272
| 'opencv/testGaussianBlur.png'
7373
| 'opencv/testInterpolate.png'
7474
| 'opencv/testReflect.png'
75-
| 'opencv/testResizeBilinear.png'
76-
| 'opencv/testResizeNearest.png'
75+
| 'opencv/test_resize_bicubic_larger.png'
76+
| 'opencv/test_resize_bicubic_same.png'
77+
| 'opencv/test_resize_bicubic_smaller.png'
78+
| 'opencv/test_resize_bilinear_larger.png'
79+
| 'opencv/test_resize_bilinear_same.png'
80+
| 'opencv/test_resize_bilinear_smaller.png'
81+
| 'opencv/test_resize_nearest_larger.png'
82+
| 'opencv/test_resize_nearest_same.png'
83+
| 'opencv/test_resize_nearest_smaller.png'
7784
| 'opencv/testRotateBicubic.png'
7885
| 'opencv/testRotateBilinear.png'
7986
| 'opencv/testScale.png'

test/img/opencv/generate.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
def writeImg(name, img):
88
cv.imwrite(path.join(dirname, name), img)
99

10+
interpolations = [
11+
[cv.INTER_NEAREST, 'nearest'],
12+
[cv.INTER_LINEAR, 'bilinear'],
13+
[cv.INTER_CUBIC, 'bicubic'],
14+
]
15+
1016
img = cv.imread(path.join(dirname, 'test.png'))
1117
assert img is not None, "file could not be read, check with os.path.exists()"
1218
rows, cols = img.shape[0], img.shape[1]
@@ -19,10 +25,17 @@ def writeImg(name, img):
1925
writeImg('testScale.png', dst)
2026

2127
# Image resizing.
22-
dst = cv.resize(img, (80, 100), interpolation=cv.INTER_NEAREST)
23-
writeImg('testResizeNearest.png', dst)
24-
dst = cv.resize(img, (80, 100), interpolation=cv.INTER_LINEAR)
25-
writeImg('testResizeBilinear.png', dst)
28+
sizes = [
29+
[(80, 100), 'larger'],
30+
[(8, 10), 'same'],
31+
[(5, 6), 'smaller'],
32+
]
33+
for interpolation, interpolationName in interpolations:
34+
for size, sizeName in sizes:
35+
writeImg(
36+
f'test_resize_{interpolationName}_{sizeName}.png',
37+
cv.resize(img, size, interpolation=interpolation)
38+
)
2639

2740
# Image rotate counter-clockwise by 90 degrees
2841
M = np.float32([[0, 1, 0], [-1, 0, cols - 1]])
9.07 KB
Loading
126 Bytes
Loading
162 Bytes
Loading
126 Bytes
Loading
134 Bytes
Loading
126 Bytes
Loading
98 Bytes
Loading

0 commit comments

Comments
 (0)