UNPKG

@tensorflow/tfjs-core

Version:

Hardware-accelerated JavaScript library for machine intelligence

320 lines 12.3 kB
/** * @license * Copyright 2019 Google LLC. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ============================================================================= */ import { ENGINE } from '../engine'; import { env } from '../environment'; import { FromPixels } from '../kernel_names'; import { getKernel } from '../kernel_registry'; import { Tensor } from '../tensor'; import { convertToTensor } from '../tensor_util_env'; import { cast } from './cast'; import { op } from './operation'; import { tensor3d } from './tensor3d'; let fromPixels2DContext; /** * Creates a `tf.Tensor` from an image. * * ```js * const image = new ImageData(1, 1); * image.data[0] = 100; * image.data[1] = 150; * image.data[2] = 200; * image.data[3] = 255; * * tf.browser.fromPixels(image).print(); * ``` * * @param pixels The input image to construct the tensor from. The * supported image types are all 4-channel. You can also pass in an image * object with following attributes: * `{data: Uint8Array; width: number; height: number}` * @param numChannels The number of channels of the output tensor. A * numChannels value less than 4 allows you to ignore channels. Defaults to * 3 (ignores alpha channel of input image). * * @returns A Tensor3D with the shape `[height, width, numChannels]`. * * @doc {heading: 'Browser', namespace: 'browser', ignoreCI: true} */ function fromPixels_(pixels, numChannels = 3) { // Sanity checks. if (numChannels > 4) { throw new Error('Cannot construct Tensor with more than 4 channels from pixels.'); } if (pixels == null) { throw new Error('pixels passed to tf.browser.fromPixels() can not be null'); } let isPixelData = false; let isImageData = false; let isVideo = false; let isImage = false; let isCanvasLike = false; let isImageBitmap = false; if (pixels.data instanceof Uint8Array) { isPixelData = true; } else if (typeof (ImageData) !== 'undefined' && pixels instanceof ImageData) { isImageData = true; } else if (typeof (HTMLVideoElement) !== 'undefined' && pixels instanceof HTMLVideoElement) { isVideo = true; } else if (typeof (HTMLImageElement) !== 'undefined' && pixels instanceof HTMLImageElement) { isImage = true; // tslint:disable-next-line: no-any } else if (pixels.getContext != null) { isCanvasLike = true; } else if (typeof (ImageBitmap) !== 'undefined' && pixels instanceof ImageBitmap) { isImageBitmap = true; } else { throw new Error('pixels passed to tf.browser.fromPixels() must be either an ' + `HTMLVideoElement, HTMLImageElement, HTMLCanvasElement, ImageData ` + `in browser, or OffscreenCanvas, ImageData in webworker` + ` or {data: Uint32Array, width: number, height: number}, ` + `but was ${pixels.constructor.name}`); } if (isVideo) { const HAVE_CURRENT_DATA_READY_STATE = 2; if (isVideo && pixels.readyState < HAVE_CURRENT_DATA_READY_STATE) { throw new Error('The video element has not loaded data yet. Please wait for ' + '`loadeddata` event on the <video> element.'); } } // If the current backend has 'FromPixels' registered, it has a more // efficient way of handling pixel uploads, so we call that. const kernel = getKernel(FromPixels, ENGINE.backendName); if (kernel != null) { const inputs = { pixels }; const attrs = { numChannels }; return ENGINE.runKernel(FromPixels, inputs, attrs); } const [width, height] = isVideo ? [ pixels.videoWidth, pixels.videoHeight ] : [pixels.width, pixels.height]; let vals; if (isCanvasLike) { vals = // tslint:disable-next-line:no-any pixels.getContext('2d').getImageData(0, 0, width, height).data; } else if (isImageData || isPixelData) { vals = pixels.data; } else if (isImage || isVideo || isImageBitmap) { if (fromPixels2DContext == null) { fromPixels2DContext = document.createElement('canvas').getContext('2d'); } fromPixels2DContext.canvas.width = width; fromPixels2DContext.canvas.height = height; fromPixels2DContext.drawImage(pixels, 0, 0, width, height); vals = fromPixels2DContext.getImageData(0, 0, width, height).data; } let values; if (numChannels === 4) { values = new Int32Array(vals); } else { const numPixels = width * height; values = new Int32Array(numPixels * numChannels); for (let i = 0; i < numPixels; i++) { for (let channel = 0; channel < numChannels; ++channel) { values[i * numChannels + channel] = vals[i * 4 + channel]; } } } const outShape = [height, width, numChannels]; return tensor3d(values, outShape, 'int32'); } // Helper functions for |fromPixelsAsync| to check whether the input can // be wrapped into imageBitmap. function isPixelData(pixels) { return (pixels != null) && (pixels.data instanceof Uint8Array); } function isImageBitmapFullySupported() { return typeof window !== 'undefined' && typeof (ImageBitmap) !== 'undefined' && window.hasOwnProperty('createImageBitmap'); } function isNonEmptyPixels(pixels) { return pixels != null && pixels.width !== 0 && pixels.height !== 0; } function canWrapPixelsToImageBitmap(pixels) { return isImageBitmapFullySupported() && !(pixels instanceof ImageBitmap) && isNonEmptyPixels(pixels) && !isPixelData(pixels); } /** * Creates a `tf.Tensor` from an image in async way. * * ```js * const image = new ImageData(1, 1); * image.data[0] = 100; * image.data[1] = 150; * image.data[2] = 200; * image.data[3] = 255; * * (await tf.browser.fromPixelsAsync(image)).print(); * ``` * This API is the async version of fromPixels. The API will first * check |WRAP_TO_IMAGEBITMAP| flag, and try to wrap the input to * imageBitmap if the flag is set to true. * * @param pixels The input image to construct the tensor from. The * supported image types are all 4-channel. You can also pass in an image * object with following attributes: * `{data: Uint8Array; width: number; height: number}` * @param numChannels The number of channels of the output tensor. A * numChannels value less than 4 allows you to ignore channels. Defaults to * 3 (ignores alpha channel of input image). * * @doc {heading: 'Browser', namespace: 'browser', ignoreCI: true} */ export async function fromPixelsAsync(pixels, numChannels = 3) { let inputs = null; // Check whether the backend needs to wrap |pixels| to imageBitmap and // whether |pixels| can be wrapped to imageBitmap. if (env().getBool('WRAP_TO_IMAGEBITMAP') && canWrapPixelsToImageBitmap(pixels)) { // Force the imageBitmap creation to not do any premultiply alpha // ops. let imageBitmap; try { // wrap in try-catch block, because createImageBitmap may not work // properly in some browsers, e.g. // https://bugzilla.mozilla.org/show_bug.cgi?id=1335594 // tslint:disable-next-line: no-any imageBitmap = await createImageBitmap(pixels, { premultiplyAlpha: 'none' }); } catch (e) { imageBitmap = null; } // createImageBitmap will clip the source size. // In some cases, the input will have larger size than its content. // E.g. new Image(10, 10) but with 1 x 1 content. Using // createImageBitmap will clip the size from 10 x 10 to 1 x 1, which // is not correct. We should avoid wrapping such resouce to // imageBitmap. if (imageBitmap != null && imageBitmap.width === pixels.width && imageBitmap.height === pixels.height) { inputs = imageBitmap; } else { inputs = pixels; } } else { inputs = pixels; } return fromPixels_(inputs, numChannels); } /** * Draws a `tf.Tensor` of pixel values to a byte array or optionally a * canvas. * * When the dtype of the input is 'float32', we assume values in the range * [0-1]. Otherwise, when input is 'int32', we assume values in the range * [0-255]. * * Returns a promise that resolves when the canvas has been drawn to. * * @param img A rank-2 tensor with shape `[height, width]`, or a rank-3 tensor * of shape `[height, width, numChannels]`. If rank-2, draws grayscale. If * rank-3, must have depth of 1, 3 or 4. When depth of 1, draws * grayscale. When depth of 3, we draw with the first three components of * the depth dimension corresponding to r, g, b and alpha = 1. When depth of * 4, all four components of the depth dimension correspond to r, g, b, a. * @param canvas The canvas to draw to. * * @doc {heading: 'Browser', namespace: 'browser'} */ export async function toPixels(img, canvas) { let $img = convertToTensor(img, 'img', 'toPixels'); if (!(img instanceof Tensor)) { // Assume int32 if user passed a native array. const originalImgTensor = $img; $img = cast(originalImgTensor, 'int32'); originalImgTensor.dispose(); } if ($img.rank !== 2 && $img.rank !== 3) { throw new Error(`toPixels only supports rank 2 or 3 tensors, got rank ${$img.rank}.`); } const [height, width] = $img.shape.slice(0, 2); const depth = $img.rank === 2 ? 1 : $img.shape[2]; if (depth > 4 || depth === 2) { throw new Error(`toPixels only supports depth of size ` + `1, 3 or 4 but got ${depth}`); } if ($img.dtype !== 'float32' && $img.dtype !== 'int32') { throw new Error(`Unsupported type for toPixels: ${$img.dtype}.` + ` Please use float32 or int32 tensors.`); } const data = await $img.data(); const multiplier = $img.dtype === 'float32' ? 255 : 1; const bytes = new Uint8ClampedArray(width * height * 4); for (let i = 0; i < height * width; ++i) { const rgba = [0, 0, 0, 255]; for (let d = 0; d < depth; d++) { const value = data[i * depth + d]; if ($img.dtype === 'float32') { if (value < 0 || value > 1) { throw new Error(`Tensor values for a float32 Tensor must be in the ` + `range [0 - 1] but encountered ${value}.`); } } else if ($img.dtype === 'int32') { if (value < 0 || value > 255) { throw new Error(`Tensor values for a int32 Tensor must be in the ` + `range [0 - 255] but encountered ${value}.`); } } if (depth === 1) { rgba[0] = value * multiplier; rgba[1] = value * multiplier; rgba[2] = value * multiplier; } else { rgba[d] = value * multiplier; } } const j = i * 4; bytes[j + 0] = Math.round(rgba[0]); bytes[j + 1] = Math.round(rgba[1]); bytes[j + 2] = Math.round(rgba[2]); bytes[j + 3] = Math.round(rgba[3]); } if (canvas != null) { canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); const imageData = new ImageData(bytes, width, height); ctx.putImageData(imageData, 0, 0); } if ($img !== img) { $img.dispose(); } return bytes; } export const fromPixels = op({ fromPixels_ }); //# sourceMappingURL=browser.js.map