Skip to content

Commit

Permalink
fix: improve resize for bilinear and bicubic interpolations (#458)
Browse files Browse the repository at this point in the history
Also add more tests.

Refs: #452
  • Loading branch information
targos authored May 6, 2024
1 parent 2535f4f commit 625bc6e
Show file tree
Hide file tree
Showing 38 changed files with 149 additions and 77 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
129 changes: 106 additions & 23 deletions src/geometry/__tests__/resize.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import path from 'node:path';

import { encodePng, write } from '../../save';
import { Image } from '../../Image';
import { write } from '../../save';

async function writeDebug(resized: Image, type: string) {
// @ts-expect-error Dynamic string.
const expected = testUtils.load(`opencv/test_resize_${type}.png`);
await write(path.join(__dirname, `resize_${type}_expected.png`), expected);
await write(path.join(__dirname, `resize_${type}_resized.png`), resized);
const subtraction = expected.subtract(resized);
await write(
path.join(__dirname, `resize_${type}_subtraction.png`),
subtraction,
);
}

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

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

expect(resized).toMatchImage('opencv/testResizeNearest.png');
expect(resized).toMatchImage('opencv/test_resize_nearest_larger.png');
});

test('compare with OpenCV (nearest, same size)', async () => {
const img = testUtils.load('opencv/test.png');

const resized = img.resize({
xFactor: 1,
interpolationType: 'nearest',
});

expect(resized).toMatchImage('opencv/test_resize_nearest_same.png');
});

test('compare with OpenCV (nearest, smaller)', async () => {
const img = testUtils.load('opencv/test.png');

const resized = img.resize({
width: 5,
height: 6,
interpolationType: 'nearest',
});

expect(resized).toMatchImage('opencv/test_resize_nearest_smaller.png');
});

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

const resized = img.resize({
xFactor: 10,
yFactor: 10,
});

const substraction = expectedImg.clone().subtract(resized);
await write(
path.join(__dirname, 'resize_bilinear_substraction.png'),
substraction,
);
await write(path.join(__dirname, 'resize_bilinear.png'), resized);
await writeDebug(resized, 'bilinear_larger');

expect(resized).toMatchImage('opencv/testResizeBilinear.png');
expect(resized).toMatchImage('opencv/test_resize_bilinear_larger.png');
});

test('result should have correct dimensions', () => {
test.skip('compare with OpenCV (bilinear, same)', async () => {
const img = testUtils.load('opencv/test.png');

const resized = img.resize({
xFactor: 1,
});

await writeDebug(resized, 'bilinear_same');

expect(resized).toMatchImage('opencv/test_resize_bilinear_same.png');
});

test.skip('compare with OpenCV (bilinear, smaller)', async () => {
const img = testUtils.load('opencv/test.png');

const resized = img.resize({
width: 5,
height: 6,
});

await writeDebug(resized, 'bilinear_smaller');

expect(resized).toMatchImage('opencv/test_resize_bilinear_smaller.png');
});

test.skip('compare with OpenCV (bicubic, larger)', async () => {
const img = testUtils.load('opencv/test.png');

const resized = img.resize({
xFactor: 10,
yFactor: 10,
interpolationType: 'bicubic',
});
expect(resized.width).toBe(10 * img.width);
expect(resized.height).toBe(10 * img.height);

await writeDebug(resized, 'bicubic_larger');

expect(resized).toMatchImage('opencv/test_resize_bicubic_larger.png');
});

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

const resized = img.resize({
width: 300,
height: 100,
xFactor: 1,
interpolationType: 'bicubic',
});

expect(resized.width).toBe(300);
expect(resized.height).toBe(100);
await writeDebug(resized, 'bicubic_same');

expect(resized).toMatchImage('opencv/test_resize_bicubic_same.png');
});

test.skip('compare with OpenCV (bicubic, smaller)', async () => {
const img = testUtils.load('opencv/test.png');

const resized = img.resize({
width: 5,
height: 6,
interpolationType: 'bicubic',
});

await writeDebug(resized, 'bicubic_smaller');

expect(resized).toMatchImage('opencv/test_resize_bicubic_smaller.png');
});

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

const resized = img.resize({
xFactor: 10,
yFactor: 10,
});
expect(resized.width).toBe(10 * img.width);
expect(resized.height).toBe(10 * img.height);
});

const png = Buffer.from(encodePng(resized.convertColor('GREY')));
test('resize to given width and height', () => {
const img = testUtils.load('opencv/test.png');

const resized = img.resize({
width: 300,
height: 100,
});

expect(png).toMatchImageSnapshot();
expect(resized.width).toBe(300);
expect(resized.height).toBe(100);
});

test('aspect ratio not preserved', () => {
Expand Down
65 changes: 17 additions & 48 deletions src/geometry/resize.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Image } from '../Image';
import { getClamp } from '../utils/clamp';
import { BorderType, getBorderInterpolation } from '../utils/interpolateBorder';
import {
getInterpolationFunction,
InterpolationType,
} from '../utils/interpolatePixel';
import { BorderType } from '../utils/interpolateBorder';
import { InterpolationType } from '../utils/interpolatePixel';
import { assert } from '../utils/validators/assert';

import { transform } from './transform';
Expand Down Expand Up @@ -57,52 +53,25 @@ export interface ResizeOptions {
export function resize(image: Image, options: ResizeOptions): Image {
const {
interpolationType = 'bilinear',
borderType = 'constant',
borderType = 'replicate',
borderValue = 0,
} = options;
const { width, height, xFactor, yFactor } = checkOptions(image, options);

if (interpolationType === 'nearest') {
return transform(
image,
[
[xFactor, 0, xFactor / 2],
[0, yFactor, yFactor / 2],
],
{
interpolationType,
borderType,
borderValue,
height,
width,
},
);
}

const newImage = Image.createFrom(image, { width, height });
const interpolate = getInterpolationFunction(interpolationType);
const interpolateBorder = getBorderInterpolation(borderType, borderValue);
const clamp = getClamp(newImage);
const intervalX = (image.width - 1) / (width - 1);
const intervalY = (image.height - 1) / (height - 1);
for (let row = 0; row < newImage.height; row++) {
for (let column = 0; column < newImage.width; column++) {
const nx = column * intervalX;
const ny = row * intervalY;
for (let channel = 0; channel < newImage.channels; channel++) {
const newValue = interpolate(
image,
nx,
ny,
channel,
interpolateBorder,
clamp,
);
newImage.setValue(column, row, channel, newValue);
}
}
}
return newImage;
return transform(
image,
[
[xFactor, 0, xFactor / 2],
[0, yFactor, yFactor / 2],
],
{
interpolationType,
borderType,
borderValue,
height,
width,
},
);
}

/**
Expand Down
11 changes: 9 additions & 2 deletions test/TestImagePath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,15 @@ export type TestImagePath =
| 'opencv/testGaussianBlur.png'
| 'opencv/testInterpolate.png'
| 'opencv/testReflect.png'
| 'opencv/testResizeBilinear.png'
| 'opencv/testResizeNearest.png'
| 'opencv/test_resize_bicubic_larger.png'
| 'opencv/test_resize_bicubic_same.png'
| 'opencv/test_resize_bicubic_smaller.png'
| 'opencv/test_resize_bilinear_larger.png'
| 'opencv/test_resize_bilinear_same.png'
| 'opencv/test_resize_bilinear_smaller.png'
| 'opencv/test_resize_nearest_larger.png'
| 'opencv/test_resize_nearest_same.png'
| 'opencv/test_resize_nearest_smaller.png'
| 'opencv/testRotateBicubic.png'
| 'opencv/testRotateBilinear.png'
| 'opencv/testScale.png'
Expand Down
21 changes: 17 additions & 4 deletions test/img/opencv/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
def writeImg(name, img):
cv.imwrite(path.join(dirname, name), img)

interpolations = [
[cv.INTER_NEAREST, 'nearest'],
[cv.INTER_LINEAR, 'bilinear'],
[cv.INTER_CUBIC, 'bicubic'],
]

img = cv.imread(path.join(dirname, 'test.png'))
assert img is not None, "file could not be read, check with os.path.exists()"
rows, cols = img.shape[0], img.shape[1]
Expand All @@ -19,10 +25,17 @@ def writeImg(name, img):
writeImg('testScale.png', dst)

# Image resizing.
dst = cv.resize(img, (80, 100), interpolation=cv.INTER_NEAREST)
writeImg('testResizeNearest.png', dst)
dst = cv.resize(img, (80, 100), interpolation=cv.INTER_LINEAR)
writeImg('testResizeBilinear.png', dst)
sizes = [
[(80, 100), 'larger'],
[(8, 10), 'same'],
[(5, 6), 'smaller'],
]
for interpolation, interpolationName in interpolations:
for size, sizeName in sizes:
writeImg(
f'test_resize_{interpolationName}_{sizeName}.png',
cv.resize(img, size, interpolation=interpolation)
)

# Image rotate counter-clockwise by 90 degrees
M = np.float32([[0, 1, 0], [-1, 0, cols - 1]])
Expand Down
Binary file added test/img/opencv/test_resize_bicubic_larger.png
Binary file added test/img/opencv/test_resize_bicubic_same.png
Binary file added test/img/opencv/test_resize_bicubic_smaller.png
File renamed without changes
Binary file added test/img/opencv/test_resize_bilinear_same.png
Binary file added test/img/opencv/test_resize_bilinear_smaller.png
File renamed without changes
Binary file added test/img/opencv/test_resize_nearest_same.png
Binary file added test/img/opencv/test_resize_nearest_smaller.png

0 comments on commit 625bc6e

Please sign in to comment.