From e3ccf95732d622a02f75f71e660a343082d0f277 Mon Sep 17 00:00:00 2001 From: BritishWerewolf Date: Sun, 13 Oct 2024 22:38:19 +0100 Subject: [PATCH 01/13] Add tests for original slice method. --- tests/utils/tensor.test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/utils/tensor.test.js b/tests/utils/tensor.test.js index 0d36954e3..fb26fe2a1 100644 --- a/tests/utils/tensor.test.js +++ b/tests/utils/tensor.test.js @@ -51,6 +51,25 @@ describe("Tensor operations", () => { // TODO add tests for errors }); + describe("slice", () => { + it("should return a given row dim", async () => { + const t1 = new Tensor("float32", [1, 2, 3, 4, 5, 6], [3, 2]); + const t2 = t1.slice(1); + const target = new Tensor("float32", [3, 4], [2]); + + compare(t2, target); + }); + + it("should return a range of rows", async () => { + const t1 = new Tensor("float32", [1, 2, 3, 4, 5, 6], [3, 2]); + // The end index is not included. + const t2 = t1.slice([1, 3]); + const target = new Tensor("float32", [3, 4, 5, 6], [2, 2]); + + compare(t2, target); + }); + }); + describe("stack", () => { const t1 = new Tensor("float32", [0, 1, 2, 3, 4, 5], [1, 3, 2]); From 58fbd4602312a39708feeb956254f70bf9918137 Mon Sep 17 00:00:00 2001 From: BritishWerewolf Date: Mon, 14 Oct 2024 22:47:03 +0100 Subject: [PATCH 02/13] Add vslice and tests to retrieve the entire length of a column. --- src/utils/tensor.js | 41 +++++++++++++++++++++++++++++++++++++- tests/utils/tensor.test.js | 17 ++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/utils/tensor.js b/src/utils/tensor.js index 536a8c249..99afb2ec3 100644 --- a/src/utils/tensor.js +++ b/src/utils/tensor.js @@ -344,6 +344,46 @@ export class Tensor { return new Tensor(this.type, this.data.slice(), this.dims.slice()); } + vslice(slices) { + const rowDim = this.dims[0]; + const colDim = this.dims[1]; + + // Handle different slice cases (single column, range, or list of specific columns) + let selectedCols = []; + if (typeof slices === 'number') { + // Single column slice + selectedCols = [slices]; + } else if (Array.isArray(slices) && slices.length === 2 && !Array.isArray(slices[0])) { + // Range slice [start, end] + const [start, end] = slices; + selectedCols = Array.from({ length: end - start }, (_, i) => i + start); + } else if (Array.isArray(slices) && Array.isArray(slices[0])) { + // Specific column list [[col1], [col2]] + selectedCols = slices.flat(); + } else { + throw new Error('Invalid slice input'); + } + + // Determine new dimensions: rows remain the same, columns are based on selection + const newTensorDims = [rowDim, selectedCols.length]; + const newBufferSize = newTensorDims[0] * newTensorDims[1]; + // Allocate memory + // @ts-ignore + const data = new this.data.constructor(newBufferSize); + + // Fill the new data array by selecting the correct columns + for (let row = 0; row < rowDim; ++row) { + for (let i = 0; i < selectedCols.length; ++i) { + const col = selectedCols[i]; + const targetIndex = row * newTensorDims[1] + i; + const originalIndex = row * colDim + col; + data[targetIndex] = this.data[originalIndex]; + } + } + + return new Tensor(this.type, data, newTensorDims); + } + slice(...slices) { // This allows for slicing with ranges and numbers const newTensorDims = []; @@ -413,7 +453,6 @@ export class Tensor { data[i] = this_data[originalIndex]; } return new Tensor(this.type, data, newTensorDims); - } /** diff --git a/tests/utils/tensor.test.js b/tests/utils/tensor.test.js index fb26fe2a1..4ce585e17 100644 --- a/tests/utils/tensor.test.js +++ b/tests/utils/tensor.test.js @@ -68,6 +68,23 @@ describe("Tensor operations", () => { compare(t2, target); }); + + it("should return a given column dim", async () => { + const t1 = new Tensor("float32", [1, 2, 3, 4, 5, 6], [3, 2]); + const t2 = t1.vslice(1); + const target = new Tensor("float32", [2, 4, 6], [3, 1]); + + compare(t2, target); + }); + + it("should return a range of cols", async () => { + const t1 = new Tensor("float32", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], [3, 4]); + // The end index is not included. + const t2 = t1.vslice([1, 3]); + const target = new Tensor("float32", [2, 3, 6, 7, 10, 11], [3, 2]); + + compare(t2, target); + }); }); describe("stack", () => { From de45d7c22fc39ba7cc36b7c4d647d89f25afc006 Mon Sep 17 00:00:00 2001 From: BritishWerewolf Date: Tue, 15 Oct 2024 22:56:32 +0100 Subject: [PATCH 03/13] Add a test for slicing every other column. --- tests/utils/tensor.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/utils/tensor.test.js b/tests/utils/tensor.test.js index 4ce585e17..11011c521 100644 --- a/tests/utils/tensor.test.js +++ b/tests/utils/tensor.test.js @@ -85,6 +85,22 @@ describe("Tensor operations", () => { compare(t2, target); }); + + it("should return a every third row", async () => { + // Create 21 nodes. + const t1 = new Tensor("float32", Array.from({ length: 21 }, (v, i) => v = ++i), [3, 7]); + + // Extract every third column. + const indices = Array.from({ length: t1.dims[1] }, (_, i) => i) + .filter(i => i % 3 === 0) + // Make sure to wrap each in an array since an array creates a new range. + .map(v => [v]); + const t2 = t1.vslice(indices); + + const target = new Tensor("float32", [1, 4, 7, 8, 11, 14, 15, 18, 21], [3, 3]); + + compare(t2, target); + }); }); describe("stack", () => { From 26621d6ef232f1f8b06cb2f1e69e81625d9e80df Mon Sep 17 00:00:00 2001 From: BritishWerewolf Date: Wed, 16 Oct 2024 16:41:45 +0100 Subject: [PATCH 04/13] Add method to return each channel as a separate array. --- src/utils/image.js | 42 +++++++++++++++++++++++++++------------ tests/utils/utils.test.js | 29 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/utils/image.js b/src/utils/image.js index 33bdf11d8..033cd17e5 100644 --- a/src/utils/image.js +++ b/src/utils/image.js @@ -1,10 +1,10 @@ /** - * @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 */ @@ -91,7 +91,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). */ @@ -101,9 +101,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'); @@ -181,7 +181,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) { @@ -355,7 +355,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 @@ -368,7 +368,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; @@ -447,7 +447,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'); @@ -495,7 +495,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'); @@ -637,6 +637,22 @@ export class RawImage { return clonedCanvas; } + /** + * Splits the image data into separate channels. + * The number of elements in the array corresponds to the number of channels in the image. + * @returns {Array} An array containing separate channel data. + */ + toChannels() { + // Split each channel into a separate entry in a `channels` array. + const channels = []; + for (let c = 0; c < this.channels; c++) { + channels.push( + this.data.filter((_, i) => i % this.channels === c) + ); + } + return channels; + } + /** * Helper method to update the image data. * @param {Uint8ClampedArray} data The new image data. @@ -742,4 +758,4 @@ export class RawImage { } }); } -} \ No newline at end of file +} diff --git a/tests/utils/utils.test.js b/tests/utils/utils.test.js index 8a1891f19..96c166ca7 100644 --- a/tests/utils/utils.test.js +++ b/tests/utils/utils.test.js @@ -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"; @@ -59,4 +60,32 @@ describe("Utilities", () => { expect(await data.text()).toBe("Hello, world!"); }); }); + + describe("Image utilities", () => { + it("Can split image into separate channels", async () => { + const url = './examples/demo-site/public/images/cats.jpg'; + const image = await RawImage.fromURL(url); + // Rather than test the entire image, we'll just test the first 3 pixels; + // ensuring that these match. + const image_data = image.toChannels().map(c => c.slice(0, 3)); + + const target = [ + new Uint8Array([140, 144, 145]), // Reds + new Uint8Array([25, 25, 25]), // Greens + new Uint8Array([56, 67, 73]), // Blues + ]; + + compare (image_data, target); + }); + + it("Can splits channels for grayscale", async () => { + const url = './examples/demo-site/public/images/cats.jpg'; + const image = (await RawImage.fromURL(url)).grayscale(); + + const image_data = image.toChannels().map(c => c.slice(0, 3)); + const target = [new Uint8Array([63, 65, 66])]; + + compare (image_data, target); + }); + }); }); From be6a733c1194dcd6a508231171b17124a8dd2450 Mon Sep 17 00:00:00 2001 From: BritishWerewolf Date: Wed, 16 Oct 2024 20:37:26 +0100 Subject: [PATCH 05/13] Add documentation. Fix TypeScript error for unsure type. --- src/utils/tensor.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/utils/tensor.js b/src/utils/tensor.js index 99afb2ec3..d285fa04b 100644 --- a/src/utils/tensor.js +++ b/src/utils/tensor.js @@ -340,10 +340,23 @@ export class Tensor { return this; } + /** + * Creates a deep copy of the current Tensor. + * @returns {Tensor} A new Tensor with the same type, data, and dimensions as the original. + */ clone() { return new Tensor(this.type, this.data.slice(), this.dims.slice()); } + /** + * Performs a vertical slice operation on a 2D Tensor. + * @param {number|number[]|number[][]} slices - The slice specification: + * - If a number is given, then a single column is selected. + * - If an array of two numbers is given, then a range of columns [start, end (exclusive)] is selected. + * - If an array of arrays is given, then those specific columns are selected. + * @returns {Tensor} A new Tensor containing the selected columns. + * @throws {Error} If the slice input is invalid. + */ vslice(slices) { const rowDim = this.dims[0]; const colDim = this.dims[1]; @@ -353,7 +366,7 @@ export class Tensor { if (typeof slices === 'number') { // Single column slice selectedCols = [slices]; - } else if (Array.isArray(slices) && slices.length === 2 && !Array.isArray(slices[0])) { + } else if (Array.isArray(slices) && slices.length === 2 && !Array.isArray(slices[0]) && !Array.isArray(slices[1])) { // Range slice [start, end] const [start, end] = slices; selectedCols = Array.from({ length: end - start }, (_, i) => i + start); @@ -384,6 +397,15 @@ export class Tensor { return new Tensor(this.type, data, newTensorDims); } + /** + * Performs a slice operation on the Tensor along specified dimensions. + * @param {...(number|number[]|null)} slices - The slice specifications for each dimension. + * - If a number is given, then a single column is selected. + * - If an array of two numbers is given, then a range of columns [start, end (exclusive)] is selected. + * - If null is given, then the entire dimension is selected. + * @returns {Tensor} A new Tensor containing the selected elements. + * @throws {Error} If the slice input is invalid. + */ slice(...slices) { // This allows for slicing with ranges and numbers const newTensorDims = []; From 9e74fc3f509f8b2b5ca6e8bf637432a5e9bc3fd5 Mon Sep 17 00:00:00 2001 From: BritishWerewolf Date: Fri, 22 Nov 2024 21:10:39 +0000 Subject: [PATCH 06/13] Remove vslice as it doesn't work as it should. Update documentation. Update tests. --- src/utils/tensor.js | 73 ++++++++++++-------------------------- tests/utils/tensor.test.js | 31 +++------------- 2 files changed, 26 insertions(+), 78 deletions(-) diff --git a/src/utils/tensor.js b/src/utils/tensor.js index d285fa04b..451a505c9 100644 --- a/src/utils/tensor.js +++ b/src/utils/tensor.js @@ -348,60 +348,31 @@ export class Tensor { return new Tensor(this.type, this.data.slice(), this.dims.slice()); } - /** - * Performs a vertical slice operation on a 2D Tensor. - * @param {number|number[]|number[][]} slices - The slice specification: - * - If a number is given, then a single column is selected. - * - If an array of two numbers is given, then a range of columns [start, end (exclusive)] is selected. - * - If an array of arrays is given, then those specific columns are selected. - * @returns {Tensor} A new Tensor containing the selected columns. - * @throws {Error} If the slice input is invalid. - */ - vslice(slices) { - const rowDim = this.dims[0]; - const colDim = this.dims[1]; - - // Handle different slice cases (single column, range, or list of specific columns) - let selectedCols = []; - if (typeof slices === 'number') { - // Single column slice - selectedCols = [slices]; - } else if (Array.isArray(slices) && slices.length === 2 && !Array.isArray(slices[0]) && !Array.isArray(slices[1])) { - // Range slice [start, end] - const [start, end] = slices; - selectedCols = Array.from({ length: end - start }, (_, i) => i + start); - } else if (Array.isArray(slices) && Array.isArray(slices[0])) { - // Specific column list [[col1], [col2]] - selectedCols = slices.flat(); - } else { - throw new Error('Invalid slice input'); - } - - // Determine new dimensions: rows remain the same, columns are based on selection - const newTensorDims = [rowDim, selectedCols.length]; - const newBufferSize = newTensorDims[0] * newTensorDims[1]; - // Allocate memory - // @ts-ignore - const data = new this.data.constructor(newBufferSize); - - // Fill the new data array by selecting the correct columns - for (let row = 0; row < rowDim; ++row) { - for (let i = 0; i < selectedCols.length; ++i) { - const col = selectedCols[i]; - const targetIndex = row * newTensorDims[1] + i; - const originalIndex = row * colDim + col; - data[targetIndex] = this.data[originalIndex]; - } - } - - return new Tensor(this.type, data, newTensorDims); - } - /** * Performs a slice operation on the Tensor along specified dimensions. + * + * Consider a Tensor that has a dimension of [4, 7]: + * ``` + * [ 1, 2, 3, 4, 5, 6, 7] + * [ 8, 9, 10, 11, 12, 13, 14] + * [15, 16, 17, 18, 19, 20, 21] + * [22, 23, 24, 25, 26, 27, 28] + * ``` + * We can slice against the two dims of row and column, for instance in this + * case we can start at the second element, and return to the second last, + * like this: + * ``` + * tensor.slice([1, -1], [1, -1]); + * ``` + * which would return: + * ``` + * [ 9, 10, 11, 12, 13 ] + * [ 16, 17, 18, 19, 20 ] + * ``` + * * @param {...(number|number[]|null)} slices - The slice specifications for each dimension. - * - If a number is given, then a single column is selected. - * - If an array of two numbers is given, then a range of columns [start, end (exclusive)] is selected. + * - If a number is given, then a single element is selected. + * - If an array of two numbers is given, then a range of elements [start, end (exclusive)] is selected. * - If null is given, then the entire dimension is selected. * @returns {Tensor} A new Tensor containing the selected elements. * @throws {Error} If the slice input is invalid. diff --git a/tests/utils/tensor.test.js b/tests/utils/tensor.test.js index 11011c521..b0ea4b277 100644 --- a/tests/utils/tensor.test.js +++ b/tests/utils/tensor.test.js @@ -69,35 +69,12 @@ describe("Tensor operations", () => { compare(t2, target); }); - it("should return a given column dim", async () => { - const t1 = new Tensor("float32", [1, 2, 3, 4, 5, 6], [3, 2]); - const t2 = t1.vslice(1); - const target = new Tensor("float32", [2, 4, 6], [3, 1]); - - compare(t2, target); - }); - - it("should return a range of cols", async () => { - const t1 = new Tensor("float32", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], [3, 4]); - // The end index is not included. - const t2 = t1.vslice([1, 3]); - const target = new Tensor("float32", [2, 3, 6, 7, 10, 11], [3, 2]); - - compare(t2, target); - }); - - it("should return a every third row", async () => { + it("should return a crop", async () => { // Create 21 nodes. - const t1 = new Tensor("float32", Array.from({ length: 21 }, (v, i) => v = ++i), [3, 7]); - - // Extract every third column. - const indices = Array.from({ length: t1.dims[1] }, (_, i) => i) - .filter(i => i % 3 === 0) - // Make sure to wrap each in an array since an array creates a new range. - .map(v => [v]); - const t2 = t1.vslice(indices); + const t1 = new Tensor("float32", Array.from({ length: 28 }, (v, i) => v = ++i), [4, 7]); + const t2 = t1.slice([1, -1], [1, -1]); - const target = new Tensor("float32", [1, 4, 7, 8, 11, 14, 15, 18, 21], [3, 3]); + const target = new Tensor("float32", [9, 10, 11, 12, 13, 16, 17, 18, 19, 20], [2, 5]); compare(t2, target); }); From 64a0510fa6150498fb6a730bd97aec5d81174d36 Mon Sep 17 00:00:00 2001 From: Joshua Lochner Date: Tue, 26 Nov 2024 15:50:35 +0200 Subject: [PATCH 07/13] Optimize `RawImage.split()` function --- src/utils/image.js | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/utils/image.js b/src/utils/image.js index 033cd17e5..5ae1eb5a5 100644 --- a/src/utils/image.js +++ b/src/utils/image.js @@ -638,19 +638,33 @@ export class RawImage { } /** - * Splits the image data into separate channels. - * The number of elements in the array corresponds to the number of channels in the image. - * @returns {Array} An array containing separate channel data. + * Split the image data into individual bands. This method returns an array of individual image bands from an image. + * For example, splitting an "RGB" image creates three new images each containing a copy of one of the original bands (red, green, blue). + * + * Inspired by PIL's `Image.split()` [function](https://pillow.readthedocs.io/en/latest/reference/Image.html#PIL.Image.Image.split). + * @returns {(Uint8Array|Uint8ClampedArray)[]} An array containing bands. */ - toChannels() { - // Split each channel into a separate entry in a `channels` array. - const channels = []; - for (let c = 0; c < this.channels; c++) { - channels.push( - this.data.filter((_, i) => i % this.channels === c) - ); + split() { + const { data, channels } = this; + + /** @type {typeof Uint8Array | typeof Uint8ClampedArray} */ + const data_type = /** @type {any} */(data.constructor); + const per_channel_length = data.length / channels; + + // Pre-allocate buffers for each channel + const split_data = Array.from( + { length: channels }, + () => new data_type(per_channel_length), + ); + + // Write pixel data + for (let i = 0; i < per_channel_length; ++i) { + const data_offset = channels * i; + for (let j = 0; j < channels; ++j) { + split_data[j][i] = data[data_offset + j]; + } } - return channels; + return split_data; } /** From 02b81dd02351e96198f164f24649cc2b2a736498 Mon Sep 17 00:00:00 2001 From: Joshua Lochner Date: Tue, 26 Nov 2024 15:51:17 +0200 Subject: [PATCH 08/13] Use dummy test image --- tests/utils/utils.test.js | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/utils/utils.test.js b/tests/utils/utils.test.js index 96c166ca7..5536cdd45 100644 --- a/tests/utils/utils.test.js +++ b/tests/utils/utils.test.js @@ -62,30 +62,27 @@ describe("Utilities", () => { }); describe("Image utilities", () => { + const [width, height, channels] = [2, 2, 3]; + const data = Uint8Array.from({ length: width * height * channels }, (_, i) => i % 5); + const image = new RawImage(data, width, height, channels); + it("Can split image into separate channels", async () => { - const url = './examples/demo-site/public/images/cats.jpg'; - const image = await RawImage.fromURL(url); - // Rather than test the entire image, we'll just test the first 3 pixels; - // ensuring that these match. - const image_data = image.toChannels().map(c => c.slice(0, 3)); + const image_data = image.split() const target = [ - new Uint8Array([140, 144, 145]), // Reds - new Uint8Array([25, 25, 25]), // Greens - new Uint8Array([56, 67, 73]), // Blues + new Uint8Array([0, 3, 1, 4]), // Reds + new Uint8Array([1, 4, 2, 0]), // Greens + new Uint8Array([2, 0, 3, 1]), // Blues ]; - compare (image_data, target); + compare(image_data, target); }); it("Can splits channels for grayscale", async () => { - const url = './examples/demo-site/public/images/cats.jpg'; - const image = (await RawImage.fromURL(url)).grayscale(); - - const image_data = image.toChannels().map(c => c.slice(0, 3)); - const target = [new Uint8Array([63, 65, 66])]; + const image_data = image.grayscale().split(); + const target = [new Uint8Array([1, 3, 2, 1])]; - compare (image_data, target); + compare(image_data, target); }); }); }); From 4acd791e8b32158f71707bc7a7513d78941a228b Mon Sep 17 00:00:00 2001 From: Joshua Lochner Date: Tue, 26 Nov 2024 15:51:30 +0200 Subject: [PATCH 09/13] Update tensor unit tests --- tests/utils/tensor.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/utils/tensor.test.js b/tests/utils/tensor.test.js index b0ea4b277..1bede1984 100644 --- a/tests/utils/tensor.test.js +++ b/tests/utils/tensor.test.js @@ -62,7 +62,6 @@ describe("Tensor operations", () => { it("should return a range of rows", async () => { const t1 = new Tensor("float32", [1, 2, 3, 4, 5, 6], [3, 2]); - // The end index is not included. const t2 = t1.slice([1, 3]); const target = new Tensor("float32", [3, 4, 5, 6], [2, 2]); @@ -70,8 +69,7 @@ describe("Tensor operations", () => { }); it("should return a crop", async () => { - // Create 21 nodes. - const t1 = new Tensor("float32", Array.from({ length: 28 }, (v, i) => v = ++i), [4, 7]); + const t1 = new Tensor("float32", Array.from({ length: 28 }, (_, i) => i + 1), [4, 7]); const t2 = t1.slice([1, -1], [1, -1]); const target = new Tensor("float32", [9, 10, 11, 12, 13, 16, 17, 18, 19, 20], [2, 5]); From 7c01ef5b33089d39c110de92e4db6283292aadf8 Mon Sep 17 00:00:00 2001 From: Joshua Lochner Date: Tue, 26 Nov 2024 14:05:01 +0000 Subject: [PATCH 10/13] Wrap `.split()` result in `RawImage` --- src/utils/image.js | 6 +++--- tests/utils/utils.test.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/image.js b/src/utils/image.js index 9bb0e50f1..e83f8e85d 100644 --- a/src/utils/image.js +++ b/src/utils/image.js @@ -663,10 +663,10 @@ export class RawImage { * For example, splitting an "RGB" image creates three new images each containing a copy of one of the original bands (red, green, blue). * * Inspired by PIL's `Image.split()` [function](https://pillow.readthedocs.io/en/latest/reference/Image.html#PIL.Image.Image.split). - * @returns {(Uint8Array|Uint8ClampedArray)[]} An array containing bands. + * @returns {RawImage[]} An array containing bands. */ split() { - const { data, channels } = this; + const { data, width, height, channels } = this; /** @type {typeof Uint8Array | typeof Uint8ClampedArray} */ const data_type = /** @type {any} */(data.constructor); @@ -685,7 +685,7 @@ export class RawImage { split_data[j][i] = data[data_offset + j]; } } - return split_data; + return split_data.map((data) => new RawImage(data, width, height, 1)); } /** diff --git a/tests/utils/utils.test.js b/tests/utils/utils.test.js index 6c2d3f56d..d9cc0596a 100644 --- a/tests/utils/utils.test.js +++ b/tests/utils/utils.test.js @@ -72,7 +72,7 @@ describe("Utilities", () => { }); it("Can split image into separate channels", async () => { - const image_data = tiny_image.split() + const image_data = tiny_image.split().map(x => x.data); const target = [ new Uint8Array([0, 3, 1, 4]), // Reds @@ -84,7 +84,7 @@ describe("Utilities", () => { }); it("Can splits channels for grayscale", async () => { - const image_data = tiny_image.grayscale().split(); + const image_data = tiny_image.grayscale().split().map(x => x.data); const target = [new Uint8Array([1, 3, 2, 1])]; compare(image_data, target); From 1765cd2a830080be5ce53521fe3e71a4922c3ec6 Mon Sep 17 00:00:00 2001 From: Joshua Lochner Date: Tue, 26 Nov 2024 14:06:49 +0000 Subject: [PATCH 11/13] Update JSDoc --- src/utils/image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/image.js b/src/utils/image.js index e83f8e85d..04562592a 100644 --- a/src/utils/image.js +++ b/src/utils/image.js @@ -659,7 +659,7 @@ export class RawImage { } /** - * Split the image data into individual bands. This method returns an array of individual image bands from an image. + * Split this image into individual bands. This method returns an array of individual image bands from an image. * For example, splitting an "RGB" image creates three new images each containing a copy of one of the original bands (red, green, blue). * * Inspired by PIL's `Image.split()` [function](https://pillow.readthedocs.io/en/latest/reference/Image.html#PIL.Image.Image.split). From 65e05c562dbd159dae22c059f032e2e153ac2286 Mon Sep 17 00:00:00 2001 From: Joshua Lochner Date: Tue, 26 Nov 2024 14:10:49 +0000 Subject: [PATCH 12/13] Update JSDoc --- src/utils/tensor.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/tensor.js b/src/utils/tensor.js index 451a505c9..dec65d1d7 100644 --- a/src/utils/tensor.js +++ b/src/utils/tensor.js @@ -370,10 +370,10 @@ export class Tensor { * [ 16, 17, 18, 19, 20 ] * ``` * - * @param {...(number|number[]|null)} slices - The slice specifications for each dimension. - * - If a number is given, then a single element is selected. - * - If an array of two numbers is given, then a range of elements [start, end (exclusive)] is selected. - * - If null is given, then the entire dimension is selected. + * @param {...(number|number[]|null)} slices The slice specifications for each dimension. + * - If a number is given, then a single element is selected. + * - If an array of two numbers is given, then a range of elements [start, end (exclusive)] is selected. + * - If null is given, then the entire dimension is selected. * @returns {Tensor} A new Tensor containing the selected elements. * @throws {Error} If the slice input is invalid. */ From 59b04a65f12dffea30a3f0fac0e2116ed1d65ef7 Mon Sep 17 00:00:00 2001 From: Joshua Lochner Date: Tue, 26 Nov 2024 14:13:27 +0000 Subject: [PATCH 13/13] Update comments --- tests/utils/utils.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/utils.test.js b/tests/utils/utils.test.js index d9cc0596a..79c6dcc7f 100644 --- a/tests/utils/utils.test.js +++ b/tests/utils/utils.test.js @@ -75,7 +75,7 @@ describe("Utilities", () => { const image_data = tiny_image.split().map(x => x.data); const target = [ - new Uint8Array([0, 3, 1, 4]), // Reds + new Uint8Array([0, 3, 1, 4]), // Reds new Uint8Array([1, 4, 2, 0]), // Greens new Uint8Array([2, 0, 3, 1]), // Blues ];