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-09-13 19:28:35 +02:00
import type { Tensor } from '../tfjs/types' ;
import type { Config } from '../config' ;
2021-09-12 19:17:33 +02:00
import { env } from '../env' ;
2021-09-15 19:59:18 +02:00
import { log } from '../helpers' ;
2021-06-05 17:54:49 +02:00
2021-09-13 19:28:35 +02:00
type Input = Tensor | ImageData | ImageBitmap | HTMLImageElement | HTMLMediaElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | typeof Image | typeof env . Canvas ;
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
2021-09-17 17:23:00 +02:00
export function canvas ( width , height ) : HTMLCanvasElement | OffscreenCanvas {
2021-09-13 19:28:35 +02:00
let c ;
if ( env . browser ) {
if ( typeof OffscreenCanvas !== 'undefined' ) {
c = new OffscreenCanvas ( width , height ) ;
} else {
c = document . createElement ( 'canvas' ) ;
c . width = width ;
c . height = height ;
}
} else {
// @ts-ignore // env.canvas is an external monkey-patch
c = ( typeof env . Canvas !== 'undefined' ) ? new env . Canvas ( width , height ) : null ;
}
2021-09-14 05:24:04 +02:00
// if (!c) throw new Error('Human: Cannot create canvas');
2021-09-13 19:28:35 +02:00
return c ;
}
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
2021-06-05 17:54:49 +02:00
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 )
2021-04-05 17:48:24 +02:00
&& ! ( typeof Image !== 'undefined' && input instanceof Image )
2021-09-13 19:28:35 +02:00
&& ! ( typeof env . Canvas !== 'undefined' && input instanceof env . Canvas )
2021-04-05 17:48:24 +02:00
&& ! ( 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 )
2021-04-05 17:48:24 +02:00
&& ! ( 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-13 19:28:35 +02:00
if ( ( input as unknown as Tensor ) . shape && ( input as unknown as Tensor ) . shape . length === 4 && ( input as unknown as Tensor ) . shape [ 0 ] === 1 && ( input as unknown 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 unknown 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-09-15 19:59:18 +02:00
if ( typeof input [ 'readyState' ] !== 'undefined' && input [ 'readyState' ] <= 2 ) {
log ( 'input stream is not ready' ) ;
return { tensor : null , canvas : inCanvas } ; // video may become temporarily unavailable due to onresize
}
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-09-15 19:59:18 +02:00
if ( ! originalWidth || ! originalHeight ) {
log ( 'cannot determine input dimensions' ) ;
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
2021-09-12 05:54:35 +02:00
if ( ( config . filter . width || 0 ) > 0 ) targetWidth = config . filter . width ;
else if ( ( config . filter . height || 0 ) > 0 ) targetWidth = originalWidth * ( ( config . filter . height || 0 ) / originalHeight ) ;
if ( ( config . filter . height || 0 ) > 0 ) targetHeight = config . filter . height ;
else if ( ( config . filter . width || 0 ) > 0 ) targetHeight = originalHeight * ( ( config . filter . width || 0 ) / originalWidth ) ;
2021-03-30 15:03:18 +02:00
if ( ! targetWidth || ! targetHeight ) throw new Error ( 'Human: Input cannot determine dimension' ) ;
2021-09-13 19:28:35 +02:00
if ( ! inCanvas || ( inCanvas ? . width !== targetWidth ) || ( inCanvas ? . height !== targetHeight ) ) inCanvas = canvas ( targetWidth , targetHeight ) ;
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-09-13 19:28:35 +02:00
if ( ( typeof ImageData !== 'undefined' ) && ( input instanceof ImageData ) ) {
2021-04-19 22:02:47 +02:00
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
2021-09-14 05:24:04 +02:00
if ( config . filter . enabled && env . webgl . supported ) {
2021-04-09 16:02:40 +02:00
if ( ! fx || ! outCanvas || ( inCanvas . width !== outCanvas . width ) || ( inCanvas ? . height !== outCanvas ? . height ) ) {
2021-09-13 19:28:35 +02:00
outCanvas = canvas ( inCanvas ? . width , inCanvas ? . height ) ;
2021-04-09 16:02:40 +02:00
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-09-12 19:17:33 +02:00
fx = env . browser ? new fxImage . GLImageFilter ( { canvas : outCanvas } ) : null ; // && (typeof document !== 'undefined')
2020-11-04 16:18:22 +01:00
}
2021-03-10 15:44:45 +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-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' ) ;
2021-09-13 19:28:35 +02:00
} else if ( ( typeof ImageData !== 'undefined' ) && ( outCanvas instanceof ImageData ) ) { // if input is imagedata, just use it
2021-08-05 16:38:04 +02:00
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
2021-09-13 19:28:35 +02:00
const tempCanvas = canvas ( targetWidth , targetHeight ) ;
2021-08-05 16:38:04 +02:00
tempCanvas . width = targetWidth ;
tempCanvas . height = targetHeight ;
const tempCtx = tempCanvas . getContext ( '2d' ) ;
tempCtx ? . drawImage ( outCanvas , 0 , 0 ) ;
2021-09-13 19:28:35 +02:00
pixels = ( tf . browser && env . browser ) ? tf . browser . fromPixels ( tempCanvas ) : null ;
2021-08-05 16:38:04 +02:00
} 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
2021-09-13 19:28:35 +02:00
const tempCanvas = canvas ( targetWidth , targetHeight ) ;
2021-09-17 17:23:00 +02:00
if ( ! tempCanvas ) return { tensor : null , canvas : inCanvas } ;
2021-08-05 16:38:04 +02:00
tempCanvas . width = targetWidth ;
tempCanvas . height = targetHeight ;
const tempCtx = tempCanvas . getContext ( '2d' ) ;
2021-09-17 17:23:00 +02:00
if ( ! tempCtx ) return { tensor : null , canvas : inCanvas } ;
2021-09-13 19:28:35 +02:00
tempCtx . drawImage ( outCanvas , 0 , 0 ) ;
const data = tempCtx . getImageData ( 0 , 0 , targetWidth , targetHeight ) ;
if ( tf . browser && env . browser ) {
pixels = tf . browser . fromPixels ( data ) ;
} else {
pixels = tf . tidy ( ( ) = > {
const imageData = tf . tensor ( Array . from ( data . data ) , [ targetWidth , targetHeight , 4 ] ) ;
const channels = tf . split ( imageData , 4 , 2 ) ; // split rgba to channels
const rgb = tf . stack ( [ channels [ 0 ] , channels [ 1 ] , channels [ 2 ] ] , 2 ) ; // stack channels back to rgb and ignore alpha
const expand = tf . reshape ( rgb , [ imageData . shape [ 0 ] , imageData . shape [ 1 ] , 3 ] ) ; // move extra dim from the end of tensor and use it as batch number instead
return expand ;
} ) ;
}
2021-08-05 16:38:04 +02:00
}
if ( pixels ) {
const casted = tf . cast ( pixels , 'float32' ) ;
tensor = tf . expandDims ( casted , 0 ) ;
tf . dispose ( pixels ) ;
tf . dispose ( casted ) ;
2021-09-13 19:28:35 +02:00
} else {
tensor = tf . zeros ( [ 1 , targetWidth , targetHeight , 3 ] ) ;
throw new Error ( 'Human: Cannot create tensor from input' ) ;
2021-08-05 16:38:04 +02:00
}
2020-11-04 16:18:22 +01:00
}
}
2021-09-13 19:28:35 +02:00
return { tensor , canvas : ( config . filter . return ? outCanvas : null ) } ;
2020-11-04 16:18:22 +01:00
}
2021-09-13 00:37:06 +02:00
let lastInputSum = 0 ;
let lastCacheDiff = 1 ;
2021-09-13 19:28:35 +02:00
export async function skip ( config , input : Tensor ) {
if ( config . cacheSensitivity === 0 ) return false ;
2021-09-13 00:37:06 +02:00
const resizeFact = 32 ;
if ( ! input . shape [ 1 ] || ! input . shape [ 2 ] ) return false ;
const reduced : Tensor = tf . image . resizeBilinear ( input , [ Math . trunc ( input . shape [ 1 ] / resizeFact ) , Math . trunc ( input . shape [ 2 ] / resizeFact ) ] ) ;
2021-09-13 19:28:35 +02:00
2021-09-13 00:37:06 +02:00
// use tensor sum
/ *
const sumT = this . tf . sum ( reduced ) ;
const sum = await sumT . data ( ) [ 0 ] as number ;
sumT . dispose ( ) ;
* /
// use js loop sum, faster than uploading tensor to gpu calculating and downloading back
const reducedData = await reduced . data ( ) ; // raw image rgb array
2021-09-13 19:28:35 +02:00
tf . dispose ( reduced ) ;
2021-09-13 00:37:06 +02:00
let sum = 0 ;
for ( let i = 0 ; i < reducedData . length / 3 ; i ++ ) sum += reducedData [ 3 * i + 2 ] ; // look only at green value of each pixel
const diff = 100 * ( Math . max ( sum , lastInputSum ) / Math . min ( sum , lastInputSum ) - 1 ) ;
lastInputSum = sum ;
// if previous frame was skipped, skip this frame if changed more than cacheSensitivity
// if previous frame was not skipped, then look for cacheSensitivity or difference larger than one in previous frame to avoid resetting cache in subsequent frames unnecessarily
2021-09-13 19:28:35 +02:00
const skipFrame = diff < Math . max ( config . cacheSensitivity , lastCacheDiff ) ;
2021-09-13 00:37:06 +02:00
// if difference is above 10x threshold, don't use last value to force reset cache for significant change of scenes or images
2021-09-13 19:28:35 +02:00
lastCacheDiff = diff > 10 * config . cacheSensitivity ? 0 : diff ;
2021-09-13 00:37:06 +02:00
// console.log('skipFrame', skipFrame, this.config.cacheSensitivity, diff);
return skipFrame ;
}