@tensorflow/tfjs-core
Version:
Hardware-accelerated JavaScript library for machine intelligence
320 lines • 12.3 kB
JavaScript
/**
* @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