2021-05-25 14:58:20 +02:00
/ * *
2021-09-25 17:51:15 +02:00
* Image Processing algorithm implementation
2021-05-25 14:58:20 +02:00
* /
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-10-25 19:09:00 +02:00
import type { Input , AnyCanvas , Tensor , Config } from '../exports' ;
2021-09-27 19:58:13 +02:00
import { env } from '../util/env' ;
2021-10-19 15:13:14 +02:00
import { log , now } from '../util/util' ;
2021-11-05 20:09:54 +01:00
import * as enhance from './enhance' ;
2021-06-05 17:54:49 +02:00
2021-03-12 22:43:36 +01:00
const maxSize = 2048 ;
2020-11-04 16:18:22 +01:00
// internal temp canvases
2021-10-21 16:26:44 +02:00
let inCanvas : AnyCanvas | null = null ; // use global variable to avoid recreating canvas on each frame
let outCanvas : AnyCanvas | null = null ; // use global variable to avoid recreating canvas on each frame
let tmpCanvas : AnyCanvas | null = null ; // use global variable to avoid recreating canvas on each frame
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-10-21 16:26:44 +02:00
export function canvas ( width , height ) : AnyCanvas {
2021-09-13 19:28:35 +02:00
let c ;
2021-10-25 15:44:13 +02:00
if ( env . browser ) { // browser defines canvas object
if ( env . worker ) { // if runing in web worker use OffscreenCanvas
2021-09-13 19:28:35 +02:00
c = new OffscreenCanvas ( width , height ) ;
2021-10-25 15:44:13 +02:00
} else { // otherwise use DOM canvas
2021-10-06 17:30:34 +02:00
if ( typeof document === 'undefined' ) throw new Error ( 'attempted to run in web worker but offscreenCanvas is not supported' ) ;
2021-09-13 19:28:35 +02:00
c = document . createElement ( 'canvas' ) ;
c . width = width ;
c . height = height ;
}
2021-10-25 15:44:13 +02:00
} else { // if not running in browser, there is no "default" canvas object, so we need monkey patch or fail
2021-09-13 19:28:35 +02:00
// @ts-ignore // env.canvas is an external monkey-patch
2021-09-23 01:27:12 +02:00
if ( typeof env . Canvas !== 'undefined' ) c = new env . Canvas ( width , height ) ;
else if ( typeof globalThis . Canvas !== 'undefined' ) c = new globalThis . Canvas ( width , height ) ;
2021-09-13 19:28:35 +02:00
}
2021-09-17 20:07:44 +02:00
// if (!c) throw new Error('cannot create canvas');
2021-09-13 19:28:35 +02:00
return c ;
}
2021-10-25 15:44:13 +02:00
// helper function to copy canvas from input to output
2021-10-21 16:26:44 +02:00
export function copy ( input : AnyCanvas , output? : AnyCanvas ) {
2021-10-10 23:52:43 +02:00
const outputCanvas = output || canvas ( input . width , input . height ) ;
const ctx = outputCanvas . getContext ( '2d' ) as CanvasRenderingContext2D ;
ctx . drawImage ( input , 0 , 0 ) ;
return outputCanvas ;
}
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-10-21 16:26:44 +02:00
export function process ( input : Input , config : Config , getTensor : boolean = true ) : { tensor : Tensor | null , canvas : AnyCanvas | null } {
2021-09-20 15:42:34 +02:00
if ( ! input ) {
// throw new Error('input is missing');
if ( config . debug ) log ( 'input is missing' ) ;
return { tensor : null , canvas : null } ; // video may become temporarily unavailable due to onresize
}
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-09-23 01:27:12 +02:00
&& ! ( typeof globalThis . Canvas !== 'undefined' && input instanceof globalThis . 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-09-17 20:07:44 +02:00
throw new Error ( 'input type is not recognized' ) ;
2021-03-30 15:03:18 +02:00
}
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-10-11 04:29:20 +02:00
if ( ( input ) [ 'isDisposedInternal' ] ) {
throw new Error ( 'input tensor is disposed' ) ;
} else if ( ! ( input as Tensor ) . shape || ( input as Tensor ) . shape . length !== 4 || ( input as Tensor ) . shape [ 0 ] !== 1 || ( input as Tensor ) . shape [ 3 ] !== 3 ) {
throw new Error ( ` input tensor shape must be [1, height, width, 3] and instead was ${ input [ 'shape' ] } ` ) ;
} else {
return { tensor : tf.clone ( input ) , canvas : ( config . filter . return ? outCanvas : null ) } ;
}
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 ) {
2021-09-20 15:42:34 +02:00
if ( config . debug ) log ( 'input stream is not ready' ) ;
2021-09-15 19:59:18 +02:00
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 ) {
2021-09-20 15:42:34 +02:00
if ( config . debug ) log ( 'cannot determine input dimensions' ) ;
2021-09-15 19:59:18 +02:00
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 ;
2021-09-25 17:51:15 +02:00
targetHeight = Math . trunc ( targetWidth * originalHeight / originalWidth ) ;
2021-03-12 22:43:36 +01:00
}
if ( targetHeight > maxSize ) {
targetHeight = maxSize ;
2021-09-25 17:51:15 +02:00
targetWidth = Math . trunc ( targetHeight * originalWidth / originalHeight ) ;
2021-03-12 22:43:36 +01:00
}
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-09-17 20:07:44 +02:00
if ( ! targetWidth || ! targetHeight ) throw new Error ( 'input cannot determine dimension' ) ;
2021-11-04 11:34:13 +01: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
2021-10-10 23:52:43 +02:00
const inCtx = inCanvas . getContext ( '2d' ) as CanvasRenderingContext2D ;
2021-09-13 19:28:35 +02:00
if ( ( typeof ImageData !== 'undefined' ) && ( input instanceof ImageData ) ) {
2021-10-10 23:52:43 +02:00
inCtx . putImageData ( input , 0 , 0 ) ;
2021-04-19 22:02:47 +02:00
} else {
2021-10-10 23:52:43 +02:00
if ( config . filter . flip && typeof inCtx . translate !== 'undefined' ) {
inCtx . translate ( originalWidth , 0 ) ;
inCtx . scale ( - 1 , 1 ) ;
2021-11-04 11:34:13 +01:00
inCtx . drawImage ( input as AnyCanvas , 0 , 0 , originalWidth , originalHeight , 0 , 0 , inCanvas . width , inCanvas . height ) ;
2021-10-10 23:52:43 +02:00
inCtx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 ) ; // resets transforms to defaults
2021-04-19 22:19:03 +02:00
} else {
2021-11-04 11:34:13 +01:00
inCtx . drawImage ( input as AnyCanvas , 0 , 0 , originalWidth , originalHeight , 0 , 0 , inCanvas . width , inCanvas . height ) ;
2021-04-19 22:02:47 +02:00
}
}
2021-10-10 23:52:43 +02:00
2021-11-04 11:34:13 +01:00
if ( ! outCanvas || ( inCanvas . width !== outCanvas . width ) || ( inCanvas . height !== outCanvas . height ) ) outCanvas = canvas ( inCanvas . width , inCanvas . height ) ; // init output canvas
2021-10-10 23:52:43 +02:00
// imagefx transforms using gl from input canvas to output canvas
2021-09-14 05:24:04 +02:00
if ( config . filter . enabled && env . webgl . supported ) {
2021-10-21 16:26:44 +02:00
if ( ! fx ) fx = env . browser ? new fxImage . GLImageFilter ( ) : null ; // && (typeof document !== 'undefined')
2021-10-10 23:52:43 +02:00
env . filter = ! ! fx ;
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 ( ) ;
2021-10-12 17:39:18 +02:00
if ( config . filter . brightness !== 0 ) fx . add ( 'brightness' , config . filter . brightness ) ;
if ( config . filter . contrast !== 0 ) fx . add ( 'contrast' , config . filter . contrast ) ;
if ( config . filter . sharpness !== 0 ) fx . add ( 'sharpen' , config . filter . sharpness ) ;
if ( config . filter . blur !== 0 ) fx . add ( 'blur' , config . filter . blur ) ;
if ( config . filter . saturation !== 0 ) fx . add ( 'saturation' , config . filter . saturation ) ;
if ( config . filter . hue !== 0 ) fx . add ( 'hue' , config . filter . hue ) ;
if ( config . filter . negative ) fx . add ( 'negative' ) ;
if ( config . filter . sepia ) fx . add ( 'sepia' ) ;
if ( config . filter . vintage ) fx . add ( 'brownie' ) ;
if ( config . filter . sepia ) fx . add ( 'sepia' ) ;
if ( config . filter . kodachrome ) fx . add ( 'kodachrome' ) ;
if ( config . filter . technicolor ) fx . add ( 'technicolor' ) ;
if ( config . filter . polaroid ) fx . add ( 'polaroid' ) ;
if ( config . filter . pixelate !== 0 ) fx . add ( 'pixelate' , config . filter . pixelate ) ;
if ( fx . get ( ) > 0 ) outCanvas = fx . apply ( inCanvas ) ;
else outCanvas = fx . draw ( inCanvas ) ;
2020-11-09 20:26:10 +01:00
} else {
2021-10-10 23:52:43 +02:00
copy ( inCanvas , outCanvas ) ; // if no filters applied, output canvas is input canvas
2021-02-21 13:20:58 +01:00
if ( fx ) fx = null ;
2021-10-10 23:52:43 +02:00
env . filter = ! ! fx ;
2020-11-04 16:18:22 +01:00
}
2021-10-10 23:52:43 +02:00
if ( ! getTensor ) return { tensor : null , canvas : outCanvas } ; // just canvas was requested
2021-10-12 15:48:00 +02:00
if ( ! outCanvas ) throw new Error ( 'cannot create output canvas' ) ;
2021-10-10 23:52:43 +02:00
// create tensor from image unless input was a tensor already
let pixels ;
let depth = 3 ;
if ( ( typeof ImageData !== 'undefined' && input instanceof ImageData ) || ( input [ 'data' ] && input [ 'width' ] && input [ 'height' ] ) ) { // if input is imagedata, just use it
if ( env . browser && tf . browser ) {
pixels = tf . browser ? tf . browser . fromPixels ( input ) : null ;
} else {
depth = input [ 'data' ] . length / input [ 'height' ] / input [ 'width' ] ;
2021-10-11 15:22:39 +02:00
// const arr = Uint8Array.from(input['data']);
const arr = new Uint8Array ( input [ 'data' ] [ 'buffer' ] ) ;
pixels = tf . tensor ( arr , [ input [ 'height' ] , input [ 'width' ] , depth ] , 'int32' ) ;
2021-10-10 23:52:43 +02:00
}
} else {
2021-11-04 11:34:13 +01:00
if ( ! tmpCanvas || ( outCanvas . width !== tmpCanvas . width ) || ( outCanvas . height !== tmpCanvas . height ) ) tmpCanvas = canvas ( outCanvas . width , outCanvas . height ) ; // init output canvas
2021-10-10 23:52:43 +02:00
if ( tf . browser && env . browser ) {
if ( config . backend === 'webgl' || config . backend === 'humangl' || config . backend === 'webgpu' ) {
pixels = tf . browser . fromPixels ( outCanvas ) ; // safe to reuse since both backend and context are gl based
2021-09-13 19:28:35 +02:00
} else {
2021-10-11 04:29:20 +02:00
tmpCanvas = copy ( outCanvas ) ; // cannot use output canvas as it already has gl context so we do a silly one more canvas
pixels = tf . browser . fromPixels ( tmpCanvas ) ;
2021-09-13 19:28:35 +02:00
}
} else {
2021-10-10 23:52:43 +02:00
const tempCanvas = copy ( outCanvas ) ; // cannot use output canvas as it already has gl context so we do a silly one more canvas
const tempCtx = tempCanvas . getContext ( '2d' ) as CanvasRenderingContext2D ;
const tempData = tempCtx . getImageData ( 0 , 0 , targetWidth , targetHeight ) ;
2021-10-11 04:29:20 +02:00
depth = tempData . data . length / targetWidth / targetHeight ;
2021-10-10 23:52:43 +02:00
const arr = new Uint8Array ( tempData . data . buffer ) ;
pixels = tf . tensor ( arr , [ targetWidth , targetHeight , depth ] ) ;
2021-08-05 16:38:04 +02:00
}
2020-11-04 16:18:22 +01:00
}
2021-10-10 23:52:43 +02:00
if ( depth === 4 ) { // rgba to rgb
const rgb = tf . slice3d ( pixels , [ 0 , 0 , 0 ] , [ - 1 , - 1 , 3 ] ) ; // strip alpha channel
tf . dispose ( pixels ) ;
pixels = rgb ;
/ *
const channels = tf . split ( pixels , 4 , 2 ) ; // split rgba to channels
tf . dispose ( pixels ) ;
const rgb = tf . stack ( [ channels [ 0 ] , channels [ 1 ] , channels [ 2 ] ] , 2 ) ; // stack channels back to rgb and ignore alpha
pixels = tf . reshape ( rgb , [ rgb . shape [ 0 ] , rgb . shape [ 1 ] , 3 ] ) ; // move extra dim from the end of tensor and use it as batch number instead
tf . dispose ( [ rgb , . . . channels ] ) ;
* /
}
if ( ! pixels ) throw new Error ( 'cannot create tensor from input' ) ;
const casted = tf . cast ( pixels , 'float32' ) ;
2021-11-05 20:09:54 +01:00
const tensor = config . filter . equalization ? enhance . histogramEqualization ( casted ) : tf . expandDims ( casted , 0 ) ;
2021-10-10 23:52:43 +02:00
tf . dispose ( [ pixels , casted ] ) ;
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-10-10 23:52:43 +02:00
let benchmarked = 0 ;
2021-09-13 19:28:35 +02:00
2021-10-10 23:52:43 +02:00
const checksum = async ( input : Tensor ) : Promise < number > = > { // use tf sum or js based sum loop depending on which is faster
const resizeFact = 48 ;
const reduced : Tensor = tf . image . resizeBilinear ( input , [ Math . trunc ( ( input . shape [ 1 ] || 1 ) / resizeFact ) , Math . trunc ( ( input . shape [ 2 ] || 1 ) / resizeFact ) ] ) ;
const tfSum = async ( ) : Promise < number > = > {
const sumT = tf . sum ( reduced ) ;
const sum0 = await sumT . data ( ) ;
tf . dispose ( sumT ) ;
return sum0 [ 0 ] ;
} ;
const jsSum = async ( ) : Promise < number > = > {
const reducedData = await reduced . data ( ) ; // raw image rgb array
let sum0 = 0 ;
for ( let i = 0 ; i < reducedData . length / 3 ; i ++ ) sum0 += reducedData [ 3 * i + 2 ] ; // look only at green value of each pixel
return sum0 ;
} ;
if ( benchmarked === 0 ) {
2021-10-19 15:13:14 +02:00
const t0 = now ( ) ;
2021-10-10 23:52:43 +02:00
await jsSum ( ) ;
2021-10-19 15:13:14 +02:00
const t1 = now ( ) ;
2021-10-10 23:52:43 +02:00
await tfSum ( ) ;
2021-10-19 15:13:14 +02:00
const t2 = now ( ) ;
2021-10-10 23:52:43 +02:00
benchmarked = t1 - t0 < t2 - t1 ? 1 : 2 ;
}
const res = benchmarked === 1 ? await jsSum ( ) : await tfSum ( ) ;
2021-09-13 19:28:35 +02:00
tf . dispose ( reduced ) ;
2021-10-10 23:52:43 +02:00
return res ;
} ;
2021-09-13 00:37:06 +02:00
2021-10-10 23:52:43 +02:00
export async function skip ( config , input : Tensor ) {
if ( config . cacheSensitivity === 0 ) return false ;
const sum = await checksum ( input ) ;
2021-09-13 00:37:06 +02:00
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-10-10 23:52:43 +02:00
let 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-10-10 23:52:43 +02:00
skipFrame = skipFrame && ( lastCacheDiff > 0 ) ; // if no cached diff value then force no skip
2021-09-13 00:37:06 +02:00
return skipFrame ;
}