human/src/image/image.ts

179 lines
9.4 KiB
TypeScript
Raw Normal View History

2021-05-25 14:58:20 +02:00
/**
* Image Processing module used by Human
*/
2021-03-17 23:23:19 +01:00
import * as tf from '../../dist/tfjs.esm.js';
2021-02-08 17:39:09 +01:00
import * as fxImage from './imagefx';
2021-05-23 03:54:18 +02:00
import { Tensor } from '../tfjs/types';
import { Config } from '../config';
type Input = Tensor | typeof Image | ImageData | ImageBitmap | HTMLImageElement | HTMLMediaElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas;
2020-11-04 16:18:22 +01:00
2021-03-12 22:43:36 +01:00
const maxSize = 2048;
2020-11-04 16:18:22 +01:00
// internal temp canvases
2021-04-09 16:02:40 +02:00
let inCanvas;
let outCanvas;
2021-09-11 03:21:29 +02:00
// @ts-ignore // imagefx is js module that should be converted to a class
let fx: fxImage.GLImageFilter | null; // instance of imagefx
2020-11-04 16:18:22 +01:00
// process input image and return tensor
// input can be tensor, imagedata, htmlimageelement, htmlvideoelement
// input is resized and run through imagefx filter
export function process(input: Input, config: Config): { tensor: Tensor | null, canvas: OffscreenCanvas | HTMLCanvasElement } {
2020-11-04 16:18:22 +01:00
let tensor;
2021-03-29 21:59:16 +02:00
if (!input) throw new Error('Human: Input is missing');
2021-04-19 22:19:03 +02:00
// sanity checks since different browsers do not implement all dom elements
2021-04-02 14:37:35 +02:00
if (
!(input instanceof tf.Tensor)
&& !(typeof Image !== 'undefined' && input instanceof Image)
&& !(typeof ImageData !== 'undefined' && input instanceof ImageData)
&& !(typeof ImageBitmap !== 'undefined' && input instanceof ImageBitmap)
&& !(typeof HTMLImageElement !== 'undefined' && input instanceof HTMLImageElement)
2021-04-06 17:38:01 +02:00
&& !(typeof HTMLMediaElement !== 'undefined' && input instanceof HTMLMediaElement)
&& !(typeof HTMLVideoElement !== 'undefined' && input instanceof HTMLVideoElement)
&& !(typeof HTMLCanvasElement !== 'undefined' && input instanceof HTMLCanvasElement)
&& !(typeof OffscreenCanvas !== 'undefined' && input instanceof OffscreenCanvas)
2021-04-02 14:37:35 +02:00
) {
2021-03-30 15:03:18 +02:00
throw new Error('Human: Input type is not recognized');
}
2020-11-04 16:18:22 +01:00
if (input instanceof tf.Tensor) {
2021-04-19 22:19:03 +02:00
// if input is tensor, use as-is
2021-09-11 03:21:29 +02:00
if ((input as Tensor).shape && (input as Tensor).shape.length === 4 && (input as Tensor).shape[0] === 1 && (input as Tensor).shape[3] === 3) tensor = tf.clone(input);
else throw new Error(`Human: Input tensor shape must be [1, height, width, 3] and instead was ${(input as Tensor).shape}`);
2020-11-04 16:18:22 +01:00
} else {
2021-04-19 22:19:03 +02:00
// check if resizing will be needed
2021-04-09 16:02:40 +02:00
const originalWidth = input['naturalWidth'] || input['videoWidth'] || input['width'] || (input['shape'] && (input['shape'][1] > 0));
const originalHeight = input['naturalHeight'] || input['videoHeight'] || input['height'] || (input['shape'] && (input['shape'][2] > 0));
2021-05-30 23:56:40 +02:00
if (!originalWidth || !originalHeight) return { tensor: null, canvas: inCanvas }; // video may become temporarily unavailable due to onresize
2020-11-04 16:18:22 +01:00
let targetWidth = originalWidth;
let targetHeight = originalHeight;
2021-03-12 22:43:36 +01:00
if (targetWidth > maxSize) {
targetWidth = maxSize;
targetHeight = targetWidth * originalHeight / originalWidth;
}
if (targetHeight > maxSize) {
targetHeight = maxSize;
targetWidth = targetHeight * originalWidth / originalHeight;
}
2021-04-19 22:19:03 +02:00
// create our canvas and resize it if needed
2020-11-04 16:18:22 +01:00
if (config.filter.width > 0) targetWidth = config.filter.width;
else if (config.filter.height > 0) targetWidth = originalWidth * (config.filter.height / originalHeight);
if (config.filter.height > 0) targetHeight = config.filter.height;
else if (config.filter.width > 0) targetHeight = originalHeight * (config.filter.width / originalWidth);
2021-03-30 15:03:18 +02:00
if (!targetWidth || !targetHeight) throw new Error('Human: Input cannot determine dimension');
2021-04-09 16:02:40 +02:00
if (!inCanvas || (inCanvas?.width !== targetWidth) || (inCanvas?.height !== targetHeight)) {
2020-11-04 16:18:22 +01:00
inCanvas = (typeof OffscreenCanvas !== 'undefined') ? new OffscreenCanvas(targetWidth, targetHeight) : document.createElement('canvas');
2021-04-09 16:02:40 +02:00
if (inCanvas?.width !== targetWidth) inCanvas.width = targetWidth;
if (inCanvas?.height !== targetHeight) inCanvas.height = targetHeight;
2020-11-04 16:18:22 +01:00
}
2021-04-19 22:02:47 +02:00
2021-04-19 22:19:03 +02:00
// draw input to our canvas
2020-11-04 16:18:22 +01:00
const ctx = inCanvas.getContext('2d');
2021-04-19 22:02:47 +02:00
if (input instanceof ImageData) {
ctx.putImageData(input, 0, 0);
} else {
2021-04-19 22:19:03 +02:00
if (config.filter.flip && typeof ctx.translate !== 'undefined') {
2021-04-19 22:02:47 +02:00
ctx.translate(originalWidth, 0);
ctx.scale(-1, 1);
ctx.drawImage(input, 0, 0, originalWidth, originalHeight, 0, 0, inCanvas?.width, inCanvas?.height);
2021-04-19 22:19:03 +02:00
ctx.setTransform(1, 0, 0, 1, 0, 0); // resets transforms to defaults
} else {
ctx.drawImage(input, 0, 0, originalWidth, originalHeight, 0, 0, inCanvas?.width, inCanvas?.height);
2021-04-19 22:02:47 +02:00
}
}
2021-04-19 22:19:03 +02:00
// imagefx transforms using gl
2020-11-04 16:18:22 +01:00
if (config.filter.enabled) {
2021-04-09 16:02:40 +02:00
if (!fx || !outCanvas || (inCanvas.width !== outCanvas.width) || (inCanvas?.height !== outCanvas?.height)) {
outCanvas = (typeof OffscreenCanvas !== 'undefined') ? new OffscreenCanvas(inCanvas?.width, inCanvas?.height) : document.createElement('canvas');
if (outCanvas?.width !== inCanvas?.width) outCanvas.width = inCanvas?.width;
if (outCanvas?.height !== inCanvas?.height) outCanvas.height = inCanvas?.height;
2021-02-22 15:13:11 +01:00
// log('created FX filter');
2021-02-21 13:20:58 +01:00
fx = tf.ENV.flags.IS_BROWSER ? new fxImage.GLImageFilter({ canvas: outCanvas }) : null; // && (typeof document !== 'undefined')
2020-11-04 16:18:22 +01:00
}
if (!fx) return { tensor: null, canvas: inCanvas };
2021-02-21 13:20:58 +01:00
fx.reset();
fx.addFilter('brightness', config.filter.brightness); // must have at least one filter enabled
if (config.filter.contrast !== 0) fx.addFilter('contrast', config.filter.contrast);
if (config.filter.sharpness !== 0) fx.addFilter('sharpen', config.filter.sharpness);
if (config.filter.blur !== 0) fx.addFilter('blur', config.filter.blur);
if (config.filter.saturation !== 0) fx.addFilter('saturation', config.filter.saturation);
if (config.filter.hue !== 0) fx.addFilter('hue', config.filter.hue);
if (config.filter.negative) fx.addFilter('negative');
if (config.filter.sepia) fx.addFilter('sepia');
if (config.filter.vintage) fx.addFilter('brownie');
if (config.filter.sepia) fx.addFilter('sepia');
if (config.filter.kodachrome) fx.addFilter('kodachrome');
if (config.filter.technicolor) fx.addFilter('technicolor');
if (config.filter.polaroid) fx.addFilter('polaroid');
if (config.filter.pixelate !== 0) fx.addFilter('pixelate', config.filter.pixelate);
fx.apply(inCanvas);
2020-11-11 21:02:49 +01:00
// read pixel data
2020-12-27 14:12:22 +01:00
/*
const gl = outCanvas.getContext('webgl');
2020-11-11 21:02:49 +01:00
if (gl) {
const glBuffer = new Uint8Array(outCanvas.width * outCanvas.height * 4);
const pixBuffer = new Uint8Array(outCanvas.width * outCanvas.height * 3);
gl.readPixels(0, 0, outCanvas.width, outCanvas.height, gl.RGBA, gl.UNSIGNED_BYTE, glBuffer);
// gl returns rbga while we only need rgb, so discarding alpha channel
// gl returns starting point as lower left, so need to invert vertical
let i = 0;
for (let y = outCanvas.height - 1; y >= 0; y--) {
for (let x = 0; x < outCanvas.width; x++) {
const index = (x + y * outCanvas.width) * 4;
pixBuffer[i++] = glBuffer[index + 0];
pixBuffer[i++] = glBuffer[index + 1];
pixBuffer[i++] = glBuffer[index + 2];
}
}
outCanvas.data = pixBuffer;
2021-08-05 16:38:04 +02:00
const shape = [outCanvas.height, outCanvas.width, 3];
const pixels = tf.tensor3d(outCanvas.data, shape, 'float32');
tensor = tf.expandDims(pixels, 0);
tf.dispose(pixels);
2020-11-11 21:02:49 +01:00
}
2020-12-27 14:12:22 +01:00
*/
2020-11-09 20:26:10 +01:00
} else {
outCanvas = inCanvas;
2021-02-21 13:20:58 +01:00
if (fx) fx = null;
2020-11-04 16:18:22 +01:00
}
2021-04-19 22:19:03 +02:00
2021-08-05 16:38:04 +02:00
// create tensor from image if tensor is not already defined
if (!tensor) {
let pixels;
if (outCanvas.data) { // if we have data, just convert to tensor
const shape = [outCanvas.height, outCanvas.width, 3];
pixels = tf.tensor3d(outCanvas.data, shape, 'int32');
} else if (outCanvas instanceof ImageData) { // if input is imagedata, just use it
pixels = tf.browser ? tf.browser.fromPixels(outCanvas) : null;
} else if (config.backend === 'webgl' || config.backend === 'humangl') { // tf kernel-optimized method to get imagedata
// we cant use canvas as-is as it already has a context, so we do a silly one more canvas
const tempCanvas = (typeof OffscreenCanvas !== 'undefined') ? new OffscreenCanvas(targetWidth, targetHeight) : document.createElement('canvas');
tempCanvas.width = targetWidth;
tempCanvas.height = targetHeight;
const tempCtx = tempCanvas.getContext('2d');
tempCtx?.drawImage(outCanvas, 0, 0);
pixels = tf.browser ? tf.browser.fromPixels(tempCanvas) : null;
} else { // cpu and wasm kernel does not implement efficient fromPixels method
// we cant use canvas as-is as it already has a context, so we do a silly one more canvas and do fromPixels on ImageData instead
const tempCanvas = (typeof OffscreenCanvas !== 'undefined') ? new OffscreenCanvas(targetWidth, targetHeight) : document.createElement('canvas');
tempCanvas.width = targetWidth;
tempCanvas.height = targetHeight;
const tempCtx = tempCanvas.getContext('2d');
tempCtx?.drawImage(outCanvas, 0, 0);
const data = tempCtx?.getImageData(0, 0, targetWidth, targetHeight);
pixels = tf.browser ? tf.browser.fromPixels(data) : null;
}
if (pixels) {
const casted = tf.cast(pixels, 'float32');
tensor = tf.expandDims(casted, 0);
tf.dispose(pixels);
tf.dispose(casted);
}
2020-11-04 16:18:22 +01:00
}
}
const canvas = config.filter.return ? outCanvas : null;
return { tensor, canvas };
2020-11-04 16:18:22 +01:00
}