@huggingface/transformers 3.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +376 -0
- package/dist/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/dist/transformers.cjs +30741 -0
- package/dist/transformers.cjs.map +1 -0
- package/dist/transformers.js +33858 -0
- package/dist/transformers.js.map +1 -0
- package/dist/transformers.min.cjs +173 -0
- package/dist/transformers.min.cjs.map +1 -0
- package/dist/transformers.min.js +231 -0
- package/dist/transformers.min.js.map +1 -0
- package/package.json +92 -0
- package/src/backends/onnx.js +151 -0
- package/src/configs.js +360 -0
- package/src/env.js +152 -0
- package/src/generation/configuration_utils.js +381 -0
- package/src/generation/logits_process.js +716 -0
- package/src/generation/logits_sampler.js +204 -0
- package/src/generation/parameters.js +35 -0
- package/src/generation/stopping_criteria.js +156 -0
- package/src/generation/streamers.js +212 -0
- package/src/models/whisper/common_whisper.js +151 -0
- package/src/models/whisper/generation_whisper.js +89 -0
- package/src/models.js +7028 -0
- package/src/ops/registry.js +92 -0
- package/src/pipelines.js +3341 -0
- package/src/processors.js +2614 -0
- package/src/tokenizers.js +4395 -0
- package/src/transformers.js +28 -0
- package/src/utils/audio.js +704 -0
- package/src/utils/constants.js +2 -0
- package/src/utils/core.js +149 -0
- package/src/utils/data-structures.js +445 -0
- package/src/utils/devices.js +11 -0
- package/src/utils/dtypes.js +62 -0
- package/src/utils/generic.js +35 -0
- package/src/utils/hub.js +671 -0
- package/src/utils/image.js +745 -0
- package/src/utils/maths.js +1050 -0
- package/src/utils/tensor.js +1378 -0
- package/types/backends/onnx.d.ts +26 -0
- package/types/backends/onnx.d.ts.map +1 -0
- package/types/configs.d.ts +59 -0
- package/types/configs.d.ts.map +1 -0
- package/types/env.d.ts +106 -0
- package/types/env.d.ts.map +1 -0
- package/types/generation/configuration_utils.d.ts +320 -0
- package/types/generation/configuration_utils.d.ts.map +1 -0
- package/types/generation/logits_process.d.ts +354 -0
- package/types/generation/logits_process.d.ts.map +1 -0
- package/types/generation/logits_sampler.d.ts +51 -0
- package/types/generation/logits_sampler.d.ts.map +1 -0
- package/types/generation/parameters.d.ts +47 -0
- package/types/generation/parameters.d.ts.map +1 -0
- package/types/generation/stopping_criteria.d.ts +81 -0
- package/types/generation/stopping_criteria.d.ts.map +1 -0
- package/types/generation/streamers.d.ts +81 -0
- package/types/generation/streamers.d.ts.map +1 -0
- package/types/models/whisper/common_whisper.d.ts +8 -0
- package/types/models/whisper/common_whisper.d.ts.map +1 -0
- package/types/models/whisper/generation_whisper.d.ts +76 -0
- package/types/models/whisper/generation_whisper.d.ts.map +1 -0
- package/types/models.d.ts +3845 -0
- package/types/models.d.ts.map +1 -0
- package/types/ops/registry.d.ts +11 -0
- package/types/ops/registry.d.ts.map +1 -0
- package/types/pipelines.d.ts +2403 -0
- package/types/pipelines.d.ts.map +1 -0
- package/types/processors.d.ts +917 -0
- package/types/processors.d.ts.map +1 -0
- package/types/tokenizers.d.ts +999 -0
- package/types/tokenizers.d.ts.map +1 -0
- package/types/transformers.d.ts +13 -0
- package/types/transformers.d.ts.map +1 -0
- package/types/utils/audio.d.ts +130 -0
- package/types/utils/audio.d.ts.map +1 -0
- package/types/utils/constants.d.ts +2 -0
- package/types/utils/constants.d.ts.map +1 -0
- package/types/utils/core.d.ts +91 -0
- package/types/utils/core.d.ts.map +1 -0
- package/types/utils/data-structures.d.ts +236 -0
- package/types/utils/data-structures.d.ts.map +1 -0
- package/types/utils/devices.d.ts +8 -0
- package/types/utils/devices.d.ts.map +1 -0
- package/types/utils/dtypes.d.ts +22 -0
- package/types/utils/dtypes.d.ts.map +1 -0
- package/types/utils/generic.d.ts +11 -0
- package/types/utils/generic.d.ts.map +1 -0
- package/types/utils/hub.d.ts +191 -0
- package/types/utils/hub.d.ts.map +1 -0
- package/types/utils/image.d.ts +119 -0
- package/types/utils/image.d.ts.map +1 -0
- package/types/utils/maths.d.ts +280 -0
- package/types/utils/maths.d.ts.map +1 -0
- package/types/utils/tensor.d.ts +392 -0
- package/types/utils/tensor.d.ts.map +1 -0
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @file Helper module for image processing.
|
|
4
|
+
*
|
|
5
|
+
* These functions and classes are only used internally,
|
|
6
|
+
* meaning an end-user shouldn't need to access anything here.
|
|
7
|
+
*
|
|
8
|
+
* @module utils/image
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getFile } from './hub.js';
|
|
12
|
+
import { env } from '../env.js';
|
|
13
|
+
import { Tensor } from './tensor.js';
|
|
14
|
+
|
|
15
|
+
// Will be empty (or not used) if running in browser or web-worker
|
|
16
|
+
import sharp from 'sharp';
|
|
17
|
+
|
|
18
|
+
const BROWSER_ENV = typeof self !== 'undefined';
|
|
19
|
+
const WEBWORKER_ENV = BROWSER_ENV && self.constructor.name === 'DedicatedWorkerGlobalScope';
|
|
20
|
+
|
|
21
|
+
let createCanvasFunction;
|
|
22
|
+
let ImageDataClass;
|
|
23
|
+
let loadImageFunction;
|
|
24
|
+
if (BROWSER_ENV) {
|
|
25
|
+
// Running in browser or web-worker
|
|
26
|
+
createCanvasFunction = (/** @type {number} */ width, /** @type {number} */ height) => {
|
|
27
|
+
if (!self.OffscreenCanvas) {
|
|
28
|
+
throw new Error('OffscreenCanvas not supported by this browser.');
|
|
29
|
+
}
|
|
30
|
+
return new self.OffscreenCanvas(width, height)
|
|
31
|
+
};
|
|
32
|
+
loadImageFunction = self.createImageBitmap;
|
|
33
|
+
ImageDataClass = self.ImageData;
|
|
34
|
+
|
|
35
|
+
} else if (sharp) {
|
|
36
|
+
// Running in Node.js, electron, or other non-browser environment
|
|
37
|
+
|
|
38
|
+
loadImageFunction = async (/**@type {sharp.Sharp}*/img) => {
|
|
39
|
+
const metadata = await img.metadata();
|
|
40
|
+
const rawChannels = metadata.channels;
|
|
41
|
+
|
|
42
|
+
const { data, info } = await img.rotate().raw().toBuffer({ resolveWithObject: true });
|
|
43
|
+
|
|
44
|
+
const newImage = new RawImage(new Uint8ClampedArray(data), info.width, info.height, info.channels);
|
|
45
|
+
if (rawChannels !== undefined && rawChannels !== info.channels) {
|
|
46
|
+
// Make sure the new image has the same number of channels as the input image.
|
|
47
|
+
// This is necessary for grayscale images.
|
|
48
|
+
newImage.convert(rawChannels);
|
|
49
|
+
}
|
|
50
|
+
return newImage;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error('Unable to load image processing library.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// Defined here: https://github.com/python-pillow/Pillow/blob/a405e8406b83f8bfb8916e93971edc7407b8b1ff/src/libImaging/Imaging.h#L262-L268
|
|
59
|
+
const RESAMPLING_MAPPING = {
|
|
60
|
+
0: 'nearest',
|
|
61
|
+
1: 'lanczos',
|
|
62
|
+
2: 'bilinear',
|
|
63
|
+
3: 'bicubic',
|
|
64
|
+
4: 'box',
|
|
65
|
+
5: 'hamming',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Mapping from file extensions to MIME types.
|
|
70
|
+
*/
|
|
71
|
+
const CONTENT_TYPE_MAP = new Map([
|
|
72
|
+
['png', 'image/png'],
|
|
73
|
+
['jpg', 'image/jpeg'],
|
|
74
|
+
['jpeg', 'image/jpeg'],
|
|
75
|
+
['gif', 'image/gif'],
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
export class RawImage {
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a new `RawImage` object.
|
|
82
|
+
* @param {Uint8ClampedArray|Uint8Array} data The pixel data.
|
|
83
|
+
* @param {number} width The width of the image.
|
|
84
|
+
* @param {number} height The height of the image.
|
|
85
|
+
* @param {1|2|3|4} channels The number of channels.
|
|
86
|
+
*/
|
|
87
|
+
constructor(data, width, height, channels) {
|
|
88
|
+
this.data = data;
|
|
89
|
+
this.width = width;
|
|
90
|
+
this.height = height;
|
|
91
|
+
this.channels = channels;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns the size of the image (width, height).
|
|
96
|
+
* @returns {[number, number]} The size of the image (width, height).
|
|
97
|
+
*/
|
|
98
|
+
get size() {
|
|
99
|
+
return [this.width, this.height];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Helper method for reading an image from a variety of input types.
|
|
104
|
+
* @param {RawImage|string|URL} input
|
|
105
|
+
* @returns The image object.
|
|
106
|
+
*
|
|
107
|
+
* **Example:** Read image from a URL.
|
|
108
|
+
* ```javascript
|
|
109
|
+
* let image = await RawImage.read('https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/football-match.jpg');
|
|
110
|
+
* // RawImage {
|
|
111
|
+
* // "data": Uint8ClampedArray [ 25, 25, 25, 19, 19, 19, ... ],
|
|
112
|
+
* // "width": 800,
|
|
113
|
+
* // "height": 533,
|
|
114
|
+
* // "channels": 3
|
|
115
|
+
* // }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
static async read(input) {
|
|
119
|
+
if (input instanceof RawImage) {
|
|
120
|
+
return input;
|
|
121
|
+
} else if (typeof input === 'string' || input instanceof URL) {
|
|
122
|
+
return await this.fromURL(input);
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error(`Unsupported input type: ${typeof input}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Read an image from a canvas.
|
|
130
|
+
* @param {HTMLCanvasElement|OffscreenCanvas} canvas The canvas to read the image from.
|
|
131
|
+
* @returns {RawImage} The image object.
|
|
132
|
+
*/
|
|
133
|
+
static fromCanvas(canvas) {
|
|
134
|
+
if (!BROWSER_ENV) {
|
|
135
|
+
throw new Error('fromCanvas() is only supported in browser environments.')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const ctx = canvas.getContext('2d');
|
|
139
|
+
const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
140
|
+
return new RawImage(data, canvas.width, canvas.height, 4);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Read an image from a URL or file path.
|
|
145
|
+
* @param {string|URL} url The URL or file path to read the image from.
|
|
146
|
+
* @returns {Promise<RawImage>} The image object.
|
|
147
|
+
*/
|
|
148
|
+
static async fromURL(url) {
|
|
149
|
+
const response = await getFile(url);
|
|
150
|
+
if (response.status !== 200) {
|
|
151
|
+
throw new Error(`Unable to read image from "${url}" (${response.status} ${response.statusText})`);
|
|
152
|
+
}
|
|
153
|
+
const blob = await response.blob();
|
|
154
|
+
return this.fromBlob(blob);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Helper method to create a new Image from a blob.
|
|
159
|
+
* @param {Blob} blob The blob to read the image from.
|
|
160
|
+
* @returns {Promise<RawImage>} The image object.
|
|
161
|
+
*/
|
|
162
|
+
static async fromBlob(blob) {
|
|
163
|
+
if (BROWSER_ENV) {
|
|
164
|
+
// Running in environment with canvas
|
|
165
|
+
const img = await loadImageFunction(blob);
|
|
166
|
+
|
|
167
|
+
const ctx = createCanvasFunction(img.width, img.height).getContext('2d');
|
|
168
|
+
|
|
169
|
+
// Draw image to context
|
|
170
|
+
ctx.drawImage(img, 0, 0);
|
|
171
|
+
|
|
172
|
+
return new this(ctx.getImageData(0, 0, img.width, img.height).data, img.width, img.height, 4);
|
|
173
|
+
|
|
174
|
+
} else {
|
|
175
|
+
// Use sharp.js to read (and possible resize) the image.
|
|
176
|
+
const img = sharp(await blob.arrayBuffer());
|
|
177
|
+
|
|
178
|
+
return await loadImageFunction(img);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Helper method to create a new Image from a tensor
|
|
184
|
+
* @param {Tensor} tensor
|
|
185
|
+
*/
|
|
186
|
+
static fromTensor(tensor, channel_format = 'CHW') {
|
|
187
|
+
if (tensor.dims.length !== 3) {
|
|
188
|
+
throw new Error(`Tensor should have 3 dimensions, but has ${tensor.dims.length} dimensions.`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (channel_format === 'CHW') {
|
|
192
|
+
tensor = tensor.transpose(1, 2, 0);
|
|
193
|
+
} else if (channel_format === 'HWC') {
|
|
194
|
+
// Do nothing
|
|
195
|
+
} else {
|
|
196
|
+
throw new Error(`Unsupported channel format: ${channel_format}`);
|
|
197
|
+
}
|
|
198
|
+
if (!(tensor.data instanceof Uint8ClampedArray || tensor.data instanceof Uint8Array)) {
|
|
199
|
+
throw new Error(`Unsupported tensor type: ${tensor.type}`);
|
|
200
|
+
}
|
|
201
|
+
switch (tensor.dims[2]) {
|
|
202
|
+
case 1:
|
|
203
|
+
case 2:
|
|
204
|
+
case 3:
|
|
205
|
+
case 4:
|
|
206
|
+
return new RawImage(tensor.data, tensor.dims[1], tensor.dims[0], tensor.dims[2]);
|
|
207
|
+
default:
|
|
208
|
+
throw new Error(`Unsupported number of channels: ${tensor.dims[2]}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Convert the image to grayscale format.
|
|
214
|
+
* @returns {RawImage} `this` to support chaining.
|
|
215
|
+
*/
|
|
216
|
+
grayscale() {
|
|
217
|
+
if (this.channels === 1) {
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const newData = new Uint8ClampedArray(this.width * this.height * 1);
|
|
222
|
+
switch (this.channels) {
|
|
223
|
+
case 3: // rgb to grayscale
|
|
224
|
+
case 4: // rgba to grayscale
|
|
225
|
+
for (let i = 0, offset = 0; i < this.data.length; i += this.channels) {
|
|
226
|
+
const red = this.data[i];
|
|
227
|
+
const green = this.data[i + 1];
|
|
228
|
+
const blue = this.data[i + 2];
|
|
229
|
+
|
|
230
|
+
newData[offset++] = Math.round(0.2989 * red + 0.5870 * green + 0.1140 * blue);
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
default:
|
|
234
|
+
throw new Error(`Conversion failed due to unsupported number of channels: ${this.channels}`);
|
|
235
|
+
}
|
|
236
|
+
return this._update(newData, this.width, this.height, 1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Convert the image to RGB format.
|
|
241
|
+
* @returns {RawImage} `this` to support chaining.
|
|
242
|
+
*/
|
|
243
|
+
rgb() {
|
|
244
|
+
if (this.channels === 3) {
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const newData = new Uint8ClampedArray(this.width * this.height * 3);
|
|
249
|
+
|
|
250
|
+
switch (this.channels) {
|
|
251
|
+
case 1: // grayscale to rgb
|
|
252
|
+
for (let i = 0, offset = 0; i < this.data.length; ++i) {
|
|
253
|
+
newData[offset++] = this.data[i];
|
|
254
|
+
newData[offset++] = this.data[i];
|
|
255
|
+
newData[offset++] = this.data[i];
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
case 4: // rgba to rgb
|
|
259
|
+
for (let i = 0, offset = 0; i < this.data.length; i += 4) {
|
|
260
|
+
newData[offset++] = this.data[i];
|
|
261
|
+
newData[offset++] = this.data[i + 1];
|
|
262
|
+
newData[offset++] = this.data[i + 2];
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
default:
|
|
266
|
+
throw new Error(`Conversion failed due to unsupported number of channels: ${this.channels}`);
|
|
267
|
+
}
|
|
268
|
+
return this._update(newData, this.width, this.height, 3);
|
|
269
|
+
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Convert the image to RGBA format.
|
|
274
|
+
* @returns {RawImage} `this` to support chaining.
|
|
275
|
+
*/
|
|
276
|
+
rgba() {
|
|
277
|
+
if (this.channels === 4) {
|
|
278
|
+
return this;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const newData = new Uint8ClampedArray(this.width * this.height * 4);
|
|
282
|
+
|
|
283
|
+
switch (this.channels) {
|
|
284
|
+
case 1: // grayscale to rgba
|
|
285
|
+
for (let i = 0, offset = 0; i < this.data.length; ++i) {
|
|
286
|
+
newData[offset++] = this.data[i];
|
|
287
|
+
newData[offset++] = this.data[i];
|
|
288
|
+
newData[offset++] = this.data[i];
|
|
289
|
+
newData[offset++] = 255;
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
case 3: // rgb to rgba
|
|
293
|
+
for (let i = 0, offset = 0; i < this.data.length; i += 3) {
|
|
294
|
+
newData[offset++] = this.data[i];
|
|
295
|
+
newData[offset++] = this.data[i + 1];
|
|
296
|
+
newData[offset++] = this.data[i + 2];
|
|
297
|
+
newData[offset++] = 255;
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
default:
|
|
301
|
+
throw new Error(`Conversion failed due to unsupported number of channels: ${this.channels}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return this._update(newData, this.width, this.height, 4);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Resize the image to the given dimensions. This method uses the canvas API to perform the resizing.
|
|
309
|
+
* @param {number} width The width of the new image.
|
|
310
|
+
* @param {number} height The height of the new image.
|
|
311
|
+
* @param {Object} options Additional options for resizing.
|
|
312
|
+
* @param {0|1|2|3|4|5|string} [options.resample] The resampling method to use.
|
|
313
|
+
* @returns {Promise<RawImage>} `this` to support chaining.
|
|
314
|
+
*/
|
|
315
|
+
async resize(width, height, {
|
|
316
|
+
resample = 2,
|
|
317
|
+
} = {}) {
|
|
318
|
+
|
|
319
|
+
// Ensure resample method is a string
|
|
320
|
+
let resampleMethod = RESAMPLING_MAPPING[resample] ?? resample;
|
|
321
|
+
|
|
322
|
+
if (BROWSER_ENV) {
|
|
323
|
+
// TODO use `resample` in browser environment
|
|
324
|
+
|
|
325
|
+
// Store number of channels before resizing
|
|
326
|
+
const numChannels = this.channels;
|
|
327
|
+
|
|
328
|
+
// Create canvas object for this image
|
|
329
|
+
const canvas = this.toCanvas();
|
|
330
|
+
|
|
331
|
+
// Actually perform resizing using the canvas API
|
|
332
|
+
const ctx = createCanvasFunction(width, height).getContext('2d');
|
|
333
|
+
|
|
334
|
+
// Draw image to context, resizing in the process
|
|
335
|
+
ctx.drawImage(canvas, 0, 0, width, height);
|
|
336
|
+
|
|
337
|
+
// Create image from the resized data
|
|
338
|
+
const resizedImage = new RawImage(ctx.getImageData(0, 0, width, height).data, width, height, 4);
|
|
339
|
+
|
|
340
|
+
// Convert back so that image has the same number of channels as before
|
|
341
|
+
return resizedImage.convert(numChannels);
|
|
342
|
+
|
|
343
|
+
} else {
|
|
344
|
+
// Create sharp image from raw data, and resize
|
|
345
|
+
let img = this.toSharp();
|
|
346
|
+
|
|
347
|
+
switch (resampleMethod) {
|
|
348
|
+
case 'box':
|
|
349
|
+
case 'hamming':
|
|
350
|
+
if (resampleMethod === 'box' || resampleMethod === 'hamming') {
|
|
351
|
+
console.warn(`Resampling method ${resampleMethod} is not yet supported. Using bilinear instead.`);
|
|
352
|
+
resampleMethod = 'bilinear';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
case 'nearest':
|
|
356
|
+
case 'bilinear':
|
|
357
|
+
case 'bicubic':
|
|
358
|
+
// Perform resizing using affine transform.
|
|
359
|
+
// This matches how the python Pillow library does it.
|
|
360
|
+
img = img.affine([width / this.width, 0, 0, height / this.height], {
|
|
361
|
+
interpolator: resampleMethod
|
|
362
|
+
});
|
|
363
|
+
break;
|
|
364
|
+
|
|
365
|
+
case 'lanczos':
|
|
366
|
+
// https://github.com/python-pillow/Pillow/discussions/5519
|
|
367
|
+
// https://github.com/lovell/sharp/blob/main/docs/api-resize.md
|
|
368
|
+
img = img.resize({
|
|
369
|
+
width, height,
|
|
370
|
+
fit: 'fill',
|
|
371
|
+
kernel: 'lanczos3', // PIL Lanczos uses a kernel size of 3
|
|
372
|
+
});
|
|
373
|
+
break;
|
|
374
|
+
|
|
375
|
+
default:
|
|
376
|
+
throw new Error(`Resampling method ${resampleMethod} is not supported.`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return await loadImageFunction(img);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async pad([left, right, top, bottom]) {
|
|
385
|
+
left = Math.max(left, 0);
|
|
386
|
+
right = Math.max(right, 0);
|
|
387
|
+
top = Math.max(top, 0);
|
|
388
|
+
bottom = Math.max(bottom, 0);
|
|
389
|
+
|
|
390
|
+
if (left === 0 && right === 0 && top === 0 && bottom === 0) {
|
|
391
|
+
// No padding needed
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (BROWSER_ENV) {
|
|
396
|
+
// Store number of channels before padding
|
|
397
|
+
const numChannels = this.channels;
|
|
398
|
+
|
|
399
|
+
// Create canvas object for this image
|
|
400
|
+
const canvas = this.toCanvas();
|
|
401
|
+
|
|
402
|
+
const newWidth = this.width + left + right;
|
|
403
|
+
const newHeight = this.height + top + bottom;
|
|
404
|
+
|
|
405
|
+
// Create a new canvas of the desired size.
|
|
406
|
+
const ctx = createCanvasFunction(newWidth, newHeight).getContext('2d');
|
|
407
|
+
|
|
408
|
+
// Draw image to context, padding in the process
|
|
409
|
+
ctx.drawImage(canvas,
|
|
410
|
+
0, 0, this.width, this.height,
|
|
411
|
+
left, top, newWidth, newHeight
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Create image from the padded data
|
|
415
|
+
const paddedImage = new RawImage(
|
|
416
|
+
ctx.getImageData(0, 0, newWidth, newHeight).data,
|
|
417
|
+
newWidth, newHeight, 4);
|
|
418
|
+
|
|
419
|
+
// Convert back so that image has the same number of channels as before
|
|
420
|
+
return paddedImage.convert(numChannels);
|
|
421
|
+
|
|
422
|
+
} else {
|
|
423
|
+
const img = this.toSharp().extend({ left, right, top, bottom });
|
|
424
|
+
return await loadImageFunction(img);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async crop([x_min, y_min, x_max, y_max]) {
|
|
429
|
+
// Ensure crop bounds are within the image
|
|
430
|
+
x_min = Math.max(x_min, 0);
|
|
431
|
+
y_min = Math.max(y_min, 0);
|
|
432
|
+
x_max = Math.min(x_max, this.width - 1);
|
|
433
|
+
y_max = Math.min(y_max, this.height - 1);
|
|
434
|
+
|
|
435
|
+
// Do nothing if the crop is the entire image
|
|
436
|
+
if (x_min === 0 && y_min === 0 && x_max === this.width - 1 && y_max === this.height - 1) {
|
|
437
|
+
return this;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const crop_width = x_max - x_min + 1;
|
|
441
|
+
const crop_height = y_max - y_min + 1;
|
|
442
|
+
|
|
443
|
+
if (BROWSER_ENV) {
|
|
444
|
+
// Store number of channels before resizing
|
|
445
|
+
const numChannels = this.channels;
|
|
446
|
+
|
|
447
|
+
// Create canvas object for this image
|
|
448
|
+
const canvas = this.toCanvas();
|
|
449
|
+
|
|
450
|
+
// Create a new canvas of the desired size. This is needed since if the
|
|
451
|
+
// image is too small, we need to pad it with black pixels.
|
|
452
|
+
const ctx = createCanvasFunction(crop_width, crop_height).getContext('2d');
|
|
453
|
+
|
|
454
|
+
// Draw image to context, cropping in the process
|
|
455
|
+
ctx.drawImage(canvas,
|
|
456
|
+
x_min, y_min, crop_width, crop_height,
|
|
457
|
+
0, 0, crop_width, crop_height
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
// Create image from the resized data
|
|
461
|
+
const resizedImage = new RawImage(ctx.getImageData(0, 0, crop_width, crop_height).data, crop_width, crop_height, 4);
|
|
462
|
+
|
|
463
|
+
// Convert back so that image has the same number of channels as before
|
|
464
|
+
return resizedImage.convert(numChannels);
|
|
465
|
+
|
|
466
|
+
} else {
|
|
467
|
+
// Create sharp image from raw data
|
|
468
|
+
const img = this.toSharp().extract({
|
|
469
|
+
left: x_min,
|
|
470
|
+
top: y_min,
|
|
471
|
+
width: crop_width,
|
|
472
|
+
height: crop_height,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
return await loadImageFunction(img);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async center_crop(crop_width, crop_height) {
|
|
481
|
+
// If the image is already the desired size, return it
|
|
482
|
+
if (this.width === crop_width && this.height === crop_height) {
|
|
483
|
+
return this;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Determine bounds of the image in the new canvas
|
|
487
|
+
const width_offset = (this.width - crop_width) / 2;
|
|
488
|
+
const height_offset = (this.height - crop_height) / 2;
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
if (BROWSER_ENV) {
|
|
492
|
+
// Store number of channels before resizing
|
|
493
|
+
const numChannels = this.channels;
|
|
494
|
+
|
|
495
|
+
// Create canvas object for this image
|
|
496
|
+
const canvas = this.toCanvas();
|
|
497
|
+
|
|
498
|
+
// Create a new canvas of the desired size. This is needed since if the
|
|
499
|
+
// image is too small, we need to pad it with black pixels.
|
|
500
|
+
const ctx = createCanvasFunction(crop_width, crop_height).getContext('2d');
|
|
501
|
+
|
|
502
|
+
let sourceX = 0;
|
|
503
|
+
let sourceY = 0;
|
|
504
|
+
let destX = 0;
|
|
505
|
+
let destY = 0;
|
|
506
|
+
|
|
507
|
+
if (width_offset >= 0) {
|
|
508
|
+
sourceX = width_offset;
|
|
509
|
+
} else {
|
|
510
|
+
destX = -width_offset;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (height_offset >= 0) {
|
|
514
|
+
sourceY = height_offset;
|
|
515
|
+
} else {
|
|
516
|
+
destY = -height_offset;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Draw image to context, cropping in the process
|
|
520
|
+
ctx.drawImage(canvas,
|
|
521
|
+
sourceX, sourceY, crop_width, crop_height,
|
|
522
|
+
destX, destY, crop_width, crop_height
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
// Create image from the resized data
|
|
526
|
+
const resizedImage = new RawImage(ctx.getImageData(0, 0, crop_width, crop_height).data, crop_width, crop_height, 4);
|
|
527
|
+
|
|
528
|
+
// Convert back so that image has the same number of channels as before
|
|
529
|
+
return resizedImage.convert(numChannels);
|
|
530
|
+
|
|
531
|
+
} else {
|
|
532
|
+
// Create sharp image from raw data
|
|
533
|
+
let img = this.toSharp();
|
|
534
|
+
|
|
535
|
+
if (width_offset >= 0 && height_offset >= 0) {
|
|
536
|
+
// Cropped image lies entirely within the original image
|
|
537
|
+
img = img.extract({
|
|
538
|
+
left: Math.floor(width_offset),
|
|
539
|
+
top: Math.floor(height_offset),
|
|
540
|
+
width: crop_width,
|
|
541
|
+
height: crop_height,
|
|
542
|
+
})
|
|
543
|
+
} else if (width_offset <= 0 && height_offset <= 0) {
|
|
544
|
+
// Cropped image lies entirely outside the original image,
|
|
545
|
+
// so we add padding
|
|
546
|
+
const top = Math.floor(-height_offset);
|
|
547
|
+
const left = Math.floor(-width_offset);
|
|
548
|
+
img = img.extend({
|
|
549
|
+
top: top,
|
|
550
|
+
left: left,
|
|
551
|
+
|
|
552
|
+
// Ensures the resulting image has the desired dimensions
|
|
553
|
+
right: crop_width - this.width - left,
|
|
554
|
+
bottom: crop_height - this.height - top,
|
|
555
|
+
});
|
|
556
|
+
} else {
|
|
557
|
+
// Cropped image lies partially outside the original image.
|
|
558
|
+
// We first pad, then crop.
|
|
559
|
+
|
|
560
|
+
let y_padding = [0, 0];
|
|
561
|
+
let y_extract = 0;
|
|
562
|
+
if (height_offset < 0) {
|
|
563
|
+
y_padding[0] = Math.floor(-height_offset);
|
|
564
|
+
y_padding[1] = crop_height - this.height - y_padding[0];
|
|
565
|
+
} else {
|
|
566
|
+
y_extract = Math.floor(height_offset);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
let x_padding = [0, 0];
|
|
570
|
+
let x_extract = 0;
|
|
571
|
+
if (width_offset < 0) {
|
|
572
|
+
x_padding[0] = Math.floor(-width_offset);
|
|
573
|
+
x_padding[1] = crop_width - this.width - x_padding[0];
|
|
574
|
+
} else {
|
|
575
|
+
x_extract = Math.floor(width_offset);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
img = img.extend({
|
|
579
|
+
top: y_padding[0],
|
|
580
|
+
bottom: y_padding[1],
|
|
581
|
+
left: x_padding[0],
|
|
582
|
+
right: x_padding[1],
|
|
583
|
+
}).extract({
|
|
584
|
+
left: x_extract,
|
|
585
|
+
top: y_extract,
|
|
586
|
+
width: crop_width,
|
|
587
|
+
height: crop_height,
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return await loadImageFunction(img);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async toBlob(type = 'image/png', quality = 1) {
|
|
596
|
+
if (!BROWSER_ENV) {
|
|
597
|
+
throw new Error('toBlob() is only supported in browser environments.')
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const canvas = this.toCanvas();
|
|
601
|
+
return await canvas.convertToBlob({ type, quality });
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
toTensor(channel_format = 'CHW') {
|
|
605
|
+
let tensor = new Tensor(
|
|
606
|
+
'uint8',
|
|
607
|
+
new Uint8Array(this.data),
|
|
608
|
+
[this.height, this.width, this.channels]
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
if (channel_format === 'HWC') {
|
|
612
|
+
// Do nothing
|
|
613
|
+
} else if (channel_format === 'CHW') { // hwc -> chw
|
|
614
|
+
tensor = tensor.permute(2, 0, 1);
|
|
615
|
+
} else {
|
|
616
|
+
throw new Error(`Unsupported channel format: ${channel_format}`);
|
|
617
|
+
}
|
|
618
|
+
return tensor;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
toCanvas() {
|
|
622
|
+
if (!BROWSER_ENV) {
|
|
623
|
+
throw new Error('toCanvas() is only supported in browser environments.')
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Clone, and convert data to RGBA before drawing to canvas.
|
|
627
|
+
// This is because the canvas API only supports RGBA
|
|
628
|
+
const cloned = this.clone().rgba();
|
|
629
|
+
|
|
630
|
+
// Create canvas object for the cloned image
|
|
631
|
+
const clonedCanvas = createCanvasFunction(cloned.width, cloned.height);
|
|
632
|
+
|
|
633
|
+
// Draw image to context
|
|
634
|
+
const data = new ImageDataClass(cloned.data, cloned.width, cloned.height);
|
|
635
|
+
clonedCanvas.getContext('2d').putImageData(data, 0, 0);
|
|
636
|
+
|
|
637
|
+
return clonedCanvas;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Helper method to update the image data.
|
|
642
|
+
* @param {Uint8ClampedArray} data The new image data.
|
|
643
|
+
* @param {number} width The new width of the image.
|
|
644
|
+
* @param {number} height The new height of the image.
|
|
645
|
+
* @param {1|2|3|4|null} [channels] The new number of channels of the image.
|
|
646
|
+
* @private
|
|
647
|
+
*/
|
|
648
|
+
_update(data, width, height, channels = null) {
|
|
649
|
+
this.data = data;
|
|
650
|
+
this.width = width;
|
|
651
|
+
this.height = height;
|
|
652
|
+
if (channels !== null) {
|
|
653
|
+
this.channels = channels;
|
|
654
|
+
}
|
|
655
|
+
return this;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Clone the image
|
|
660
|
+
* @returns {RawImage} The cloned image
|
|
661
|
+
*/
|
|
662
|
+
clone() {
|
|
663
|
+
return new RawImage(this.data.slice(), this.width, this.height, this.channels);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Helper method for converting image to have a certain number of channels
|
|
668
|
+
* @param {number} numChannels The number of channels. Must be 1, 3, or 4.
|
|
669
|
+
* @returns {RawImage} `this` to support chaining.
|
|
670
|
+
*/
|
|
671
|
+
convert(numChannels) {
|
|
672
|
+
if (this.channels === numChannels) return this; // Already correct number of channels
|
|
673
|
+
|
|
674
|
+
switch (numChannels) {
|
|
675
|
+
case 1:
|
|
676
|
+
this.grayscale();
|
|
677
|
+
break;
|
|
678
|
+
case 3:
|
|
679
|
+
this.rgb();
|
|
680
|
+
break;
|
|
681
|
+
case 4:
|
|
682
|
+
this.rgba();
|
|
683
|
+
break;
|
|
684
|
+
default:
|
|
685
|
+
throw new Error(`Conversion failed due to unsupported number of channels: ${this.channels}`);
|
|
686
|
+
}
|
|
687
|
+
return this;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Save the image to the given path.
|
|
692
|
+
* @param {string} path The path to save the image to.
|
|
693
|
+
*/
|
|
694
|
+
async save(path) {
|
|
695
|
+
|
|
696
|
+
if (BROWSER_ENV) {
|
|
697
|
+
if (WEBWORKER_ENV) {
|
|
698
|
+
throw new Error('Unable to save an image from a Web Worker.')
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const extension = path.split('.').pop().toLowerCase();
|
|
702
|
+
const mime = CONTENT_TYPE_MAP.get(extension) ?? 'image/png';
|
|
703
|
+
|
|
704
|
+
// Convert image to Blob
|
|
705
|
+
const blob = await this.toBlob(mime);
|
|
706
|
+
|
|
707
|
+
// Convert the canvas content to a data URL
|
|
708
|
+
const dataURL = URL.createObjectURL(blob);
|
|
709
|
+
|
|
710
|
+
// Create an anchor element with the data URL as the href attribute
|
|
711
|
+
const downloadLink = document.createElement('a');
|
|
712
|
+
downloadLink.href = dataURL;
|
|
713
|
+
|
|
714
|
+
// Set the download attribute to specify the desired filename for the downloaded image
|
|
715
|
+
downloadLink.download = path;
|
|
716
|
+
|
|
717
|
+
// Trigger the download
|
|
718
|
+
downloadLink.click();
|
|
719
|
+
|
|
720
|
+
// Clean up: remove the anchor element from the DOM
|
|
721
|
+
downloadLink.remove();
|
|
722
|
+
|
|
723
|
+
} else if (!env.useFS) {
|
|
724
|
+
throw new Error('Unable to save the image because filesystem is disabled in this environment.')
|
|
725
|
+
|
|
726
|
+
} else {
|
|
727
|
+
const img = this.toSharp();
|
|
728
|
+
return await img.toFile(path);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
toSharp() {
|
|
733
|
+
if (BROWSER_ENV) {
|
|
734
|
+
throw new Error('toSharp() is only supported in server-side environments.')
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return sharp(this.data, {
|
|
738
|
+
raw: {
|
|
739
|
+
width: this.width,
|
|
740
|
+
height: this.height,
|
|
741
|
+
channels: this.channels
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|