Skip to content

Commit

Permalink
Add option to maintain aspect ratio on resize (#971)
Browse files Browse the repository at this point in the history
* Add option to resize one dimension and maintain aspect ratio.

* Update src/utils/image.js

Reduce function calls for optimisation.

Co-authored-by: Joshua Lochner <[email protected]>

* Optimise image utils tests.

* nullish dimension may be `null`, `undefined`, or `-1`

---------

Co-authored-by: Joshua Lochner <[email protected]>
  • Loading branch information
BritishWerewolf and xenova authored Nov 25, 2024
1 parent d38134d commit 23647e2
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 21 deletions.
23 changes: 16 additions & 7 deletions src/utils/core.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

/**
* @file Core utility functions/classes for Transformers.js.
*
*
* These are only used internally, meaning an end-user shouldn't
* need to access anything here.
*
*
* @module utils/core
*/

Expand Down Expand Up @@ -65,7 +65,7 @@ export function escapeRegExp(string) {
* Check if a value is a typed array.
* @param {*} val The value to check.
* @returns {boolean} True if the value is a `TypedArray`, false otherwise.
*
*
* Adapted from https://stackoverflow.com/a/71091338/13989043
*/
export function isTypedArray(val) {
Expand All @@ -82,6 +82,15 @@ export function isIntegralNumber(x) {
return Number.isInteger(x) || typeof x === 'bigint'
}

/**
* Determine if a provided width or height is nullish.
* @param {*} x The value to check.
* @returns {boolean} True if the value is `null`, `undefined` or `-1`, false otherwise.
*/
export function isNullishDimension(x) {
return x === null || x === undefined || x === -1;
}

/**
* Calculates the dimensions of a nested array.
*
Expand Down Expand Up @@ -151,9 +160,9 @@ export function calculateReflectOffset(i, w) {
}

/**
*
* @param {Object} o
* @param {string[]} props
*
* @param {Object} o
* @param {string[]} props
* @returns {Object}
*/
export function pick(o, props) {
Expand All @@ -170,7 +179,7 @@ export function pick(o, props) {
/**
* Calculate the length of a string, taking multi-byte characters into account.
* This mimics the behavior of Python's `len` function.
* @param {string} s The string to calculate the length of.
* @param {string} s The string to calculate the length of.
* @returns {number} The length of the string.
*/
export function len(s) {
Expand Down
43 changes: 29 additions & 14 deletions src/utils/image.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@

/**
* @file Helper module for image processing.
*
* These functions and classes are only used internally,
* @file Helper module for image processing.
*
* These functions and classes are only used internally,
* meaning an end-user shouldn't need to access anything here.
*
*
* @module utils/image
*/

import { isNullishDimension } from './core.js';
import { getFile } from './hub.js';
import { env } from '../env.js';
import { Tensor } from './tensor.js';
Expand Down Expand Up @@ -91,7 +92,7 @@ export class RawImage {
this.channels = channels;
}

/**
/**
* Returns the size of the image (width, height).
* @returns {[number, number]} The size of the image (width, height).
*/
Expand All @@ -101,9 +102,9 @@ export class RawImage {

/**
* Helper method for reading an image from a variety of input types.
* @param {RawImage|string|URL} input
* @param {RawImage|string|URL} input
* @returns The image object.
*
*
* **Example:** Read image from a URL.
* ```javascript
* let image = await RawImage.read('https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/football-match.jpg');
Expand Down Expand Up @@ -181,7 +182,7 @@ export class RawImage {

/**
* Helper method to create a new Image from a tensor
* @param {Tensor} tensor
* @param {Tensor} tensor
*/
static fromTensor(tensor, channel_format = 'CHW') {
if (tensor.dims.length !== 3) {
Expand Down Expand Up @@ -306,8 +307,8 @@ export class RawImage {

/**
* Resize the image to the given dimensions. This method uses the canvas API to perform the resizing.
* @param {number} width The width of the new image.
* @param {number} height The height of the new image.
* @param {number} width The width of the new image. `null` or `-1` will preserve the aspect ratio.
* @param {number} height The height of the new image. `null` or `-1` will preserve the aspect ratio.
* @param {Object} options Additional options for resizing.
* @param {0|1|2|3|4|5|string} [options.resample] The resampling method to use.
* @returns {Promise<RawImage>} `this` to support chaining.
Expand All @@ -324,6 +325,20 @@ export class RawImage {
// Ensure resample method is a string
let resampleMethod = RESAMPLING_MAPPING[resample] ?? resample;

// Calculate width / height to maintain aspect ratio, in the event that
// the user passed a null value in.
// This allows users to pass in something like `resize(320, null)` to
// resize to 320 width, but maintain aspect ratio.
const nullish_width = isNullishDimension(width);
const nullish_height = isNullishDimension(height);
if (nullish_width && nullish_height) {
return this;
} else if (nullish_width) {
width = (height / this.height) * this.width;
} else if (nullish_height) {
height = (width / this.width) * this.height;
}

if (BROWSER_ENV) {
// TODO use `resample` in browser environment

Expand Down Expand Up @@ -360,7 +375,7 @@ export class RawImage {
case 'nearest':
case 'bilinear':
case 'bicubic':
// Perform resizing using affine transform.
// Perform resizing using affine transform.
// This matches how the python Pillow library does it.
img = img.affine([width / this.width, 0, 0, height / this.height], {
interpolator: resampleMethod
Expand All @@ -373,7 +388,7 @@ export class RawImage {
img = img.resize({
width, height,
fit: 'fill',
kernel: 'lanczos3', // PIL Lanczos uses a kernel size of 3
kernel: 'lanczos3', // PIL Lanczos uses a kernel size of 3
});
break;

Expand Down Expand Up @@ -452,7 +467,7 @@ export class RawImage {
// Create canvas object for this image
const canvas = this.toCanvas();

// Create a new canvas of the desired size. This is needed since if the
// Create a new canvas of the desired size. This is needed since if the
// image is too small, we need to pad it with black pixels.
const ctx = createCanvasFunction(crop_width, crop_height).getContext('2d');

Expand Down Expand Up @@ -500,7 +515,7 @@ export class RawImage {
// Create canvas object for this image
const canvas = this.toCanvas();

// Create a new canvas of the desired size. This is needed since if the
// Create a new canvas of the desired size. This is needed since if the
// image is too small, we need to pad it with black pixels.
const ctx = createCanvasFunction(crop_width, crop_height).getContext('2d');

Expand Down
32 changes: 32 additions & 0 deletions tests/utils/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AutoProcessor, hamming, hanning, mel_filter_bank } from "../../src/transformers.js";
import { getFile } from "../../src/utils/hub.js";
import { RawImage } from "../../src/utils/image.js";

import { MAX_TEST_EXECUTION_TIME } from "../init.js";
import { compare } from "../test_utils.js";
Expand Down Expand Up @@ -59,4 +60,35 @@ describe("Utilities", () => {
expect(await data.text()).toBe("Hello, world!");
});
});

describe("Image utilities", () => {
let image;
beforeAll(async () => {
image = await RawImage.fromURL("https://picsum.photos/300/200");
});

it("Read image from URL", async () => {
expect(image.width).toBe(300);
expect(image.height).toBe(200);
expect(image.channels).toBe(3);
});

it("Can resize image", async () => {
const resized = await image.resize(150, 100);
expect(resized.width).toBe(150);
expect(resized.height).toBe(100);
});

it("Can resize with aspect ratio", async () => {
const resized = await image.resize(150, null);
expect(resized.width).toBe(150);
expect(resized.height).toBe(100);
});

it("Returns original image if width and height are null", async () => {
const resized = await image.resize(null, null);
expect(resized.width).toBe(300);
expect(resized.height).toBe(200);
});
});
});

0 comments on commit 23647e2

Please sign in to comment.