UNPKG

12.3 kBJavaScriptView Raw
1/**
2 * @license
3 * Copyright 2019 Google LLC. All Rights Reserved.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 * =============================================================================
16 */
17import { ENGINE } from '../engine';
18import { env } from '../environment';
19import { FromPixels } from '../kernel_names';
20import { getKernel } from '../kernel_registry';
21import { Tensor } from '../tensor';
22import { convertToTensor } from '../tensor_util_env';
23import { cast } from './cast';
24import { op } from './operation';
25import { tensor3d } from './tensor3d';
26let fromPixels2DContext;
27/**
28 * Creates a `tf.Tensor` from an image.
29 *
30 * ```js
31 * const image = new ImageData(1, 1);
32 * image.data[0] = 100;
33 * image.data[1] = 150;
34 * image.data[2] = 200;
35 * image.data[3] = 255;
36 *
37 * tf.browser.fromPixels(image).print();
38 * ```
39 *
40 * @param pixels The input image to construct the tensor from. The
41 * supported image types are all 4-channel. You can also pass in an image
42 * object with following attributes:
43 * `{data: Uint8Array; width: number; height: number}`
44 * @param numChannels The number of channels of the output tensor. A
45 * numChannels value less than 4 allows you to ignore channels. Defaults to
46 * 3 (ignores alpha channel of input image).
47 *
48 * @returns A Tensor3D with the shape `[height, width, numChannels]`.
49 *
50 * @doc {heading: 'Browser', namespace: 'browser', ignoreCI: true}
51 */
52function fromPixels_(pixels, numChannels = 3) {
53 // Sanity checks.
54 if (numChannels > 4) {
55 throw new Error('Cannot construct Tensor with more than 4 channels from pixels.');
56 }
57 if (pixels == null) {
58 throw new Error('pixels passed to tf.browser.fromPixels() can not be null');
59 }
60 let isPixelData = false;
61 let isImageData = false;
62 let isVideo = false;
63 let isImage = false;
64 let isCanvasLike = false;
65 let isImageBitmap = false;
66 if (pixels.data instanceof Uint8Array) {
67 isPixelData = true;
68 }
69 else if (typeof (ImageData) !== 'undefined' && pixels instanceof ImageData) {
70 isImageData = true;
71 }
72 else if (typeof (HTMLVideoElement) !== 'undefined' &&
73 pixels instanceof HTMLVideoElement) {
74 isVideo = true;
75 }
76 else if (typeof (HTMLImageElement) !== 'undefined' &&
77 pixels instanceof HTMLImageElement) {
78 isImage = true;
79 // tslint:disable-next-line: no-any
80 }
81 else if (pixels.getContext != null) {
82 isCanvasLike = true;
83 }
84 else if (typeof (ImageBitmap) !== 'undefined' && pixels instanceof ImageBitmap) {
85 isImageBitmap = true;
86 }
87 else {
88 throw new Error('pixels passed to tf.browser.fromPixels() must be either an ' +
89 `HTMLVideoElement, HTMLImageElement, HTMLCanvasElement, ImageData ` +
90 `in browser, or OffscreenCanvas, ImageData in webworker` +
91 ` or {data: Uint32Array, width: number, height: number}, ` +
92 `but was ${pixels.constructor.name}`);
93 }
94 if (isVideo) {
95 const HAVE_CURRENT_DATA_READY_STATE = 2;
96 if (isVideo &&
97 pixels.readyState <
98 HAVE_CURRENT_DATA_READY_STATE) {
99 throw new Error('The video element has not loaded data yet. Please wait for ' +
100 '`loadeddata` event on the <video> element.');
101 }
102 }
103 // If the current backend has 'FromPixels' registered, it has a more
104 // efficient way of handling pixel uploads, so we call that.
105 const kernel = getKernel(FromPixels, ENGINE.backendName);
106 if (kernel != null) {
107 const inputs = { pixels };
108 const attrs = { numChannels };
109 return ENGINE.runKernel(FromPixels, inputs, attrs);
110 }
111 const [width, height] = isVideo ?
112 [
113 pixels.videoWidth,
114 pixels.videoHeight
115 ] :
116 [pixels.width, pixels.height];
117 let vals;
118 if (isCanvasLike) {
119 vals =
120 // tslint:disable-next-line:no-any
121 pixels.getContext('2d').getImageData(0, 0, width, height).data;
122 }
123 else if (isImageData || isPixelData) {
124 vals = pixels.data;
125 }
126 else if (isImage || isVideo || isImageBitmap) {
127 if (fromPixels2DContext == null) {
128 fromPixels2DContext = document.createElement('canvas').getContext('2d');
129 }
130 fromPixels2DContext.canvas.width = width;
131 fromPixels2DContext.canvas.height = height;
132 fromPixels2DContext.drawImage(pixels, 0, 0, width, height);
133 vals = fromPixels2DContext.getImageData(0, 0, width, height).data;
134 }
135 let values;
136 if (numChannels === 4) {
137 values = new Int32Array(vals);
138 }
139 else {
140 const numPixels = width * height;
141 values = new Int32Array(numPixels * numChannels);
142 for (let i = 0; i < numPixels; i++) {
143 for (let channel = 0; channel < numChannels; ++channel) {
144 values[i * numChannels + channel] = vals[i * 4 + channel];
145 }
146 }
147 }
148 const outShape = [height, width, numChannels];
149 return tensor3d(values, outShape, 'int32');
150}
151// Helper functions for |fromPixelsAsync| to check whether the input can
152// be wrapped into imageBitmap.
153function isPixelData(pixels) {
154 return (pixels != null) && (pixels.data instanceof Uint8Array);
155}
156function isImageBitmapFullySupported() {
157 return typeof window !== 'undefined' &&
158 typeof (ImageBitmap) !== 'undefined' &&
159 window.hasOwnProperty('createImageBitmap');
160}
161function isNonEmptyPixels(pixels) {
162 return pixels != null && pixels.width !== 0 && pixels.height !== 0;
163}
164function canWrapPixelsToImageBitmap(pixels) {
165 return isImageBitmapFullySupported() && !(pixels instanceof ImageBitmap) &&
166 isNonEmptyPixels(pixels) && !isPixelData(pixels);
167}
168/**
169 * Creates a `tf.Tensor` from an image in async way.
170 *
171 * ```js
172 * const image = new ImageData(1, 1);
173 * image.data[0] = 100;
174 * image.data[1] = 150;
175 * image.data[2] = 200;
176 * image.data[3] = 255;
177 *
178 * (await tf.browser.fromPixelsAsync(image)).print();
179 * ```
180 * This API is the async version of fromPixels. The API will first
181 * check |WRAP_TO_IMAGEBITMAP| flag, and try to wrap the input to
182 * imageBitmap if the flag is set to true.
183 *
184 * @param pixels The input image to construct the tensor from. The
185 * supported image types are all 4-channel. You can also pass in an image
186 * object with following attributes:
187 * `{data: Uint8Array; width: number; height: number}`
188 * @param numChannels The number of channels of the output tensor. A
189 * numChannels value less than 4 allows you to ignore channels. Defaults to
190 * 3 (ignores alpha channel of input image).
191 *
192 * @doc {heading: 'Browser', namespace: 'browser', ignoreCI: true}
193 */
194export async function fromPixelsAsync(pixels, numChannels = 3) {
195 let inputs = null;
196 // Check whether the backend needs to wrap |pixels| to imageBitmap and
197 // whether |pixels| can be wrapped to imageBitmap.
198 if (env().getBool('WRAP_TO_IMAGEBITMAP') &&
199 canWrapPixelsToImageBitmap(pixels)) {
200 // Force the imageBitmap creation to not do any premultiply alpha
201 // ops.
202 let imageBitmap;
203 try {
204 // wrap in try-catch block, because createImageBitmap may not work
205 // properly in some browsers, e.g.
206 // https://bugzilla.mozilla.org/show_bug.cgi?id=1335594
207 // tslint:disable-next-line: no-any
208 imageBitmap = await createImageBitmap(pixels, { premultiplyAlpha: 'none' });
209 }
210 catch (e) {
211 imageBitmap = null;
212 }
213 // createImageBitmap will clip the source size.
214 // In some cases, the input will have larger size than its content.
215 // E.g. new Image(10, 10) but with 1 x 1 content. Using
216 // createImageBitmap will clip the size from 10 x 10 to 1 x 1, which
217 // is not correct. We should avoid wrapping such resouce to
218 // imageBitmap.
219 if (imageBitmap != null && imageBitmap.width === pixels.width &&
220 imageBitmap.height === pixels.height) {
221 inputs = imageBitmap;
222 }
223 else {
224 inputs = pixels;
225 }
226 }
227 else {
228 inputs = pixels;
229 }
230 return fromPixels_(inputs, numChannels);
231}
232/**
233 * Draws a `tf.Tensor` of pixel values to a byte array or optionally a
234 * canvas.
235 *
236 * When the dtype of the input is 'float32', we assume values in the range
237 * [0-1]. Otherwise, when input is 'int32', we assume values in the range
238 * [0-255].
239 *
240 * Returns a promise that resolves when the canvas has been drawn to.
241 *
242 * @param img A rank-2 tensor with shape `[height, width]`, or a rank-3 tensor
243 * of shape `[height, width, numChannels]`. If rank-2, draws grayscale. If
244 * rank-3, must have depth of 1, 3 or 4. When depth of 1, draws
245 * grayscale. When depth of 3, we draw with the first three components of
246 * the depth dimension corresponding to r, g, b and alpha = 1. When depth of
247 * 4, all four components of the depth dimension correspond to r, g, b, a.
248 * @param canvas The canvas to draw to.
249 *
250 * @doc {heading: 'Browser', namespace: 'browser'}
251 */
252export async function toPixels(img, canvas) {
253 let $img = convertToTensor(img, 'img', 'toPixels');
254 if (!(img instanceof Tensor)) {
255 // Assume int32 if user passed a native array.
256 const originalImgTensor = $img;
257 $img = cast(originalImgTensor, 'int32');
258 originalImgTensor.dispose();
259 }
260 if ($img.rank !== 2 && $img.rank !== 3) {
261 throw new Error(`toPixels only supports rank 2 or 3 tensors, got rank ${$img.rank}.`);
262 }
263 const [height, width] = $img.shape.slice(0, 2);
264 const depth = $img.rank === 2 ? 1 : $img.shape[2];
265 if (depth > 4 || depth === 2) {
266 throw new Error(`toPixels only supports depth of size ` +
267 `1, 3 or 4 but got ${depth}`);
268 }
269 if ($img.dtype !== 'float32' && $img.dtype !== 'int32') {
270 throw new Error(`Unsupported type for toPixels: ${$img.dtype}.` +
271 ` Please use float32 or int32 tensors.`);
272 }
273 const data = await $img.data();
274 const multiplier = $img.dtype === 'float32' ? 255 : 1;
275 const bytes = new Uint8ClampedArray(width * height * 4);
276 for (let i = 0; i < height * width; ++i) {
277 const rgba = [0, 0, 0, 255];
278 for (let d = 0; d < depth; d++) {
279 const value = data[i * depth + d];
280 if ($img.dtype === 'float32') {
281 if (value < 0 || value > 1) {
282 throw new Error(`Tensor values for a float32 Tensor must be in the ` +
283 `range [0 - 1] but encountered ${value}.`);
284 }
285 }
286 else if ($img.dtype === 'int32') {
287 if (value < 0 || value > 255) {
288 throw new Error(`Tensor values for a int32 Tensor must be in the ` +
289 `range [0 - 255] but encountered ${value}.`);
290 }
291 }
292 if (depth === 1) {
293 rgba[0] = value * multiplier;
294 rgba[1] = value * multiplier;
295 rgba[2] = value * multiplier;
296 }
297 else {
298 rgba[d] = value * multiplier;
299 }
300 }
301 const j = i * 4;
302 bytes[j + 0] = Math.round(rgba[0]);
303 bytes[j + 1] = Math.round(rgba[1]);
304 bytes[j + 2] = Math.round(rgba[2]);
305 bytes[j + 3] = Math.round(rgba[3]);
306 }
307 if (canvas != null) {
308 canvas.width = width;
309 canvas.height = height;
310 const ctx = canvas.getContext('2d');
311 const imageData = new ImageData(bytes, width, height);
312 ctx.putImageData(imageData, 0, 0);
313 }
314 if ($img !== img) {
315 $img.dispose();
316 }
317 return bytes;
318}
319export const fromPixels = op({ fromPixels_ });
320//# sourceMappingURL=browser.js.map
\No newline at end of file