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-11-06 15:21:51 +01:00
import { log } from '../util/util' ;
2021-11-05 20:09:54 +01:00
import * as enhance from './enhance' ;
2021-06-05 17:54:49 +02:00
2022-04-21 15:39:40 +02:00
const maxSize = 3840 ;
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-11-06 15:21:51 +01:00
const last : { inputSum : number , cacheDiff : number , sumMethod : number , inputTensor : undefined | Tensor } = {
inputSum : 0 ,
cacheDiff : 1 ,
sumMethod : 0 ,
inputTensor : undefined ,
} ;
2021-12-27 16:59:56 +01:00
export function canvas ( width : number , height : number ) : 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-11-14 17:22:52 +01:00
if ( typeof OffscreenCanvas === 'undefined' ) throw new Error ( 'canvas error: attempted to run in web worker but OffscreenCanvas is not supported' ) ;
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-11-14 17:22:52 +01:00
if ( typeof document === 'undefined' ) throw new Error ( 'canvas error: attempted to run in browser but DOM is not defined' ) ;
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-11-14 17:22:52 +01:00
// else throw new Error('canvas error: attempted to use canvas in nodejs without canvas support installed');
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-11-06 15:21:51 +01:00
export async function process ( input : Input , config : Config , getTensor : boolean = true ) : Promise < { tensor : Tensor | null , canvas : AnyCanvas | null } > {
2021-09-20 15:42:34 +02:00
if ( ! input ) {
// throw new Error('input is missing');
2021-11-14 17:22:52 +01:00
if ( config . debug ) log ( 'input error: input is missing' ) ;
2021-09-20 15:42:34 +02:00
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-11-14 17:22:52 +01:00
throw new Error ( 'input error: type is not recognized' ) ;
2021-03-30 15:03:18 +02:00
}
2021-11-10 01:39:18 +01:00
if ( input instanceof tf . Tensor ) { // if input is tensor use as-is without filters but correct shape as needed
let tensor : Tensor | null = null ;
2021-11-14 17:22:52 +01:00
if ( ( input as Tensor ) [ 'isDisposedInternal' ] ) throw new Error ( 'input error: attempted to use tensor but it is disposed' ) ;
2022-08-21 19:34:51 +02:00
if ( ! ( input as Tensor ) . shape ) throw new Error ( 'input error: attempted to use tensor without a shape' ) ;
2021-11-10 01:39:18 +01:00
if ( ( input as Tensor ) . shape . length === 3 ) { // [height, width, 3 || 4]
if ( ( input as Tensor ) . shape [ 2 ] === 3 ) { // [height, width, 3] so add batch
tensor = tf . expandDims ( input , 0 ) ;
} else if ( ( input as Tensor ) . shape [ 2 ] === 4 ) { // [height, width, 4] so strip alpha and add batch
const rgb = tf . slice3d ( input , [ 0 , 0 , 0 ] , [ - 1 , - 1 , 3 ] ) ;
tensor = tf . expandDims ( rgb , 0 ) ;
tf . dispose ( rgb ) ;
}
} else if ( ( input as Tensor ) . shape . length === 4 ) { // [1, width, height, 3 || 4]
if ( ( input as Tensor ) . shape [ 3 ] === 3 ) { // [1, width, height, 3] just clone
tensor = tf . clone ( input ) ;
} else if ( ( input as Tensor ) . shape [ 3 ] === 4 ) { // [1, width, height, 4] so strip alpha
tensor = tf . slice4d ( input , [ 0 , 0 , 0 , 0 ] , [ - 1 , - 1 , - 1 , 3 ] ) ;
}
2021-10-11 04:29:20 +02:00
}
2021-11-10 01:39:18 +01:00
// at the end shape must be [1, height, width, 3]
2022-08-21 19:34:51 +02:00
if ( tensor == null || ( tensor as Tensor ) . shape . length !== 4 || ( tensor as Tensor ) . shape [ 0 ] !== 1 || ( tensor as Tensor ) . shape [ 3 ] !== 3 ) throw new Error ( ` input error: attempted to use tensor with unrecognized shape: ${ ( input as Tensor ) . shape } ` ) ;
if ( ( tensor ) . dtype === 'int32' ) {
2021-11-10 01:39:18 +01:00
const cast = tf . cast ( tensor , 'float32' ) ;
tf . dispose ( tensor ) ;
tensor = cast ;
}
return { tensor , canvas : ( config . filter . return ? outCanvas : null ) } ;
2022-08-21 19:34:51 +02:00
}
// check if resizing will be needed
if ( typeof input [ 'readyState' ] !== 'undefined' && ( input as HTMLMediaElement ) . readyState <= 2 ) {
if ( config . debug ) log ( 'input stream is not ready' ) ;
return { tensor : null , canvas : inCanvas } ; // video may become temporarily unavailable due to onresize
}
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 ) ) ;
if ( ! originalWidth || ! originalHeight ) {
if ( config . debug ) log ( 'cannot determine input dimensions' ) ;
return { tensor : null , canvas : inCanvas } ; // video may become temporarily unavailable due to onresize
}
let targetWidth = originalWidth ;
let targetHeight = originalHeight ;
if ( targetWidth > maxSize ) {
targetWidth = maxSize ;
targetHeight = Math . trunc ( targetWidth * originalHeight / originalWidth ) ;
}
if ( targetHeight > maxSize ) {
targetHeight = maxSize ;
targetWidth = Math . trunc ( targetHeight * originalWidth / originalHeight ) ;
}
2021-04-19 22:19:03 +02:00
2022-08-21 19:34:51 +02:00
// create our canvas and resize it if needed
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 ) ;
if ( ! targetWidth || ! targetHeight ) throw new Error ( 'input error: cannot determine dimension' ) ;
if ( ! inCanvas || ( inCanvas . width !== targetWidth ) || ( inCanvas . height !== targetHeight ) ) inCanvas = canvas ( targetWidth , targetHeight ) ;
2021-04-19 22:02:47 +02:00
2022-08-21 19:34:51 +02:00
// draw input to our canvas
const inCtx = inCanvas . getContext ( '2d' ) as CanvasRenderingContext2D ;
if ( ( typeof ImageData !== 'undefined' ) && ( input instanceof ImageData ) ) {
inCtx . putImageData ( input , 0 , 0 ) ;
} else {
if ( config . filter . flip && typeof inCtx . translate !== 'undefined' ) {
inCtx . translate ( originalWidth , 0 ) ;
inCtx . scale ( - 1 , 1 ) ;
inCtx . drawImage ( input as AnyCanvas , 0 , 0 , originalWidth , originalHeight , 0 , 0 , inCanvas . width , inCanvas . height ) ;
inCtx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 ) ; // resets transforms to defaults
2021-04-19 22:02:47 +02:00
} else {
2022-08-21 19:34:51 +02:00
inCtx . drawImage ( input as AnyCanvas , 0 , 0 , originalWidth , originalHeight , 0 , 0 , inCanvas . width , inCanvas . height ) ;
2021-04-19 22:02:47 +02:00
}
2022-08-21 19:34:51 +02:00
}
2021-10-10 23:52:43 +02:00
2022-08-21 19:34:51 +02: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
2022-08-21 19:34:51 +02:00
// imagefx transforms using gl from input canvas to output canvas
if ( config . filter . enabled && env . webgl . supported ) {
if ( ! fx ) fx = env . browser ? new fxImage . GLImageFilter ( ) : null ; // && (typeof document !== 'undefined')
env . filter = ! ! fx ;
if ( ! fx || ! fx . add ) {
if ( config . debug ) log ( 'input process error: cannot initialize filters' ) ;
env . webgl . supported = false ;
config . filter . enabled = false ;
copy ( inCanvas , outCanvas ) ; // filter failed to initialize
// return { tensor: null, canvas: inCanvas };
2020-11-09 20:26:10 +01:00
} else {
2022-08-21 19:34:51 +02:00
fx . reset ( ) ;
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-04 16:18:22 +01:00
}
2022-08-21 19:34:51 +02:00
} else {
copy ( inCanvas , outCanvas ) ; // if no filters applied, output canvas is input canvas
if ( fx ) fx = null ;
env . filter = ! ! fx ;
}
2021-10-10 23:52:43 +02:00
2022-08-21 19:34:51 +02:00
if ( ! getTensor ) return { tensor : null , canvas : outCanvas } ; // just canvas was requested
if ( ! outCanvas ) throw new Error ( 'canvas error: cannot create output' ) ;
2021-10-10 23:52:43 +02:00
2022-08-21 19:34:51 +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 as ImageData ) . data && ( input as ImageData ) . width && ( input as ImageData ) . height ) ) { // if input is imagedata, just use it
if ( env . browser && tf . browser ) {
pixels = tf . browser ? tf . browser . fromPixels ( input ) : null ;
2021-10-10 23:52:43 +02:00
} else {
2022-08-21 19:34:51 +02:00
depth = ( input as ImageData ) . data . length / ( input as ImageData ) . height / ( input as ImageData ) . width ;
// const arr = Uint8Array.from(input['data']);
const arr = new Uint8Array ( ( input as ImageData ) . data . buffer ) ;
pixels = tf . tensor ( arr , [ ( input as ImageData ) . height , ( input as ImageData ) . width , depth ] , 'int32' ) ;
}
} else {
if ( ! tmpCanvas || ( outCanvas . width !== tmpCanvas . width ) || ( outCanvas . height !== tmpCanvas . height ) ) tmpCanvas = canvas ( outCanvas . width , outCanvas . height ) ; // init output canvas
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 {
2022-08-21 19:34:51 +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-08-05 16:38:04 +02:00
}
2022-08-21 19:34:51 +02:00
} else {
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 ) ;
depth = tempData . data . length / targetWidth / targetHeight ;
const arr = new Uint8Array ( tempData . data . buffer ) ;
pixels = tf . tensor ( arr , [ targetWidth , targetHeight , depth ] ) ;
2020-11-04 16:18:22 +01:00
}
}
2022-08-21 19:34:51 +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 ;
}
if ( ! pixels ) throw new Error ( 'input error: cannot create tensor' ) ;
const casted = tf . cast ( pixels , 'float32' ) ;
const tensor = config . filter . equalization ? await enhance . histogramEqualization ( casted ) : tf . expandDims ( casted , 0 ) ;
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
2021-11-06 15:21:51 +01: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 ;
} ;
2021-11-06 15:21:51 +01:00
if ( last . sumMethod === 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-11-06 15:21:51 +01:00
last . sumMethod = t1 - t0 < t2 - t1 ? 1 : 2 ;
2021-10-10 23:52:43 +02:00
}
2021-11-06 15:21:51 +01:00
const res = last . sumMethod === 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-11-06 15:21:51 +01:00
* /
2021-09-13 00:37:06 +02:00
2021-12-27 16:59:56 +01:00
export async function skip ( config : Partial < Config > , input : Tensor ) {
2021-11-06 15:21:51 +01:00
let skipFrame = false ;
2021-11-16 19:07:44 +01:00
if ( config . cacheSensitivity === 0 || ! input . shape || input . shape . length !== 4 || input . shape [ 1 ] > 2048 || input . shape [ 2 ] > 2048 ) return skipFrame ; // cache disabled or input is invalid or too large for cache analysis
2021-11-06 15:21:51 +01:00
/ *
const checkSum = await checksum ( input ) ;
const diff = 100 * ( Math . max ( checkSum , last . inputSum ) / Math . min ( checkSum , last . inputSum ) - 1 ) ;
last . inputSum = checkSum ;
2021-09-13 00:37:06 +02:00
// 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-11-06 15:21:51 +01:00
let skipFrame = diff < Math . max ( config . cacheSensitivity , last . cacheDiff ) ;
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-11-06 15:21:51 +01:00
last . cacheDiff = diff > 10 * config . cacheSensitivity ? 0 : diff ;
skipFrame = skipFrame && ( last . cacheDiff > 0 ) ; // if no cached diff value then force no skip
* /
if ( ! last . inputTensor ) {
last . inputTensor = tf . clone ( input ) ;
} else if ( last . inputTensor . shape [ 1 ] !== input . shape [ 1 ] || last . inputTensor . shape [ 2 ] !== input . shape [ 2 ] ) { // input resolution changed
tf . dispose ( last . inputTensor ) ;
last . inputTensor = tf . clone ( input ) ;
} else {
const t : Record < string , Tensor > = { } ;
t . diff = tf . sub ( input , last . inputTensor ) ;
t . squared = tf . mul ( t . diff , t . diff ) ;
t . sum = tf . sum ( t . squared ) ;
const diffSum = await t . sum . data ( ) ;
const diffRelative = diffSum [ 0 ] / ( input . shape [ 1 ] || 1 ) / ( input . shape [ 2 ] || 1 ) / 255 / 3 ; // squared difference relative to input resolution and averaged per channel
tf . dispose ( [ last . inputTensor , t . diff , t . squared , t . sum ] ) ;
last . inputTensor = tf . clone ( input ) ;
2021-12-27 16:59:56 +01:00
skipFrame = diffRelative <= ( config . cacheSensitivity || 0 ) ;
2021-11-06 15:21:51 +01:00
}
2021-09-13 00:37:06 +02:00
return skipFrame ;
}
2021-11-07 16:03:33 +01:00
2021-12-27 16:59:56 +01:00
export async function compare ( config : Partial < Config > , input1 : Tensor , input2 : Tensor ) : Promise < number > {
2021-11-07 16:03:33 +01:00
const t : Record < string , Tensor > = { } ;
if ( ! input1 || ! input2 || input1 . shape . length !== 4 || input1 . shape . length !== input2 . shape . length ) {
if ( ! config . debug ) log ( 'invalid input tensor or tensor shapes do not match:' , input1 . shape , input2 . shape ) ;
return 0 ;
}
if ( input1 . shape [ 0 ] !== 1 || input2 . shape [ 0 ] !== 1 || input1 . shape [ 3 ] !== 3 || input2 . shape [ 3 ] !== 3 ) {
if ( ! config . debug ) log ( 'input tensors must be of shape [1, height, width, 3]:' , input1 . shape , input2 . shape ) ;
return 0 ;
}
t . input1 = tf . clone ( input1 ) ;
t . input2 = ( input1 . shape [ 1 ] !== input2 . shape [ 1 ] || input1 . shape [ 2 ] !== input2 . shape [ 2 ] ) ? tf . image . resizeBilinear ( input2 , [ input1 . shape [ 1 ] , input1 . shape [ 2 ] ] ) : tf . clone ( input2 ) ;
t . diff = tf . sub ( t . input1 , t . input2 ) ;
t . squared = tf . mul ( t . diff , t . diff ) ;
t . sum = tf . sum ( t . squared ) ;
const diffSum = await t . sum . data ( ) ;
const diffRelative = diffSum [ 0 ] / ( input1 . shape [ 1 ] || 1 ) / ( input1 . shape [ 2 ] || 1 ) / 255 / 3 ;
tf . dispose ( [ t . input1 , t . input2 , t . diff , t . squared , t . sum ] ) ;
return diffRelative ;
}