2021-05-25 14:58:20 +02:00
/ * *
* NanoDet object detection module
* /
2021-04-09 14:07:58 +02:00
import { log , join } from '../helpers' ;
2021-03-17 16:32:37 +01:00
import * as tf from '../../dist/tfjs.esm.js' ;
2021-03-23 19:46:44 +01:00
import { labels } from './labels' ;
2021-05-22 18:33:19 +02:00
import { Item } from '../result' ;
2021-06-03 15:41:53 +02:00
import { GraphModel , Tensor } from '../tfjs/types' ;
import { Config } from '../config' ;
2021-03-17 16:32:37 +01:00
let model ;
2021-05-22 18:33:19 +02:00
let last : Array < Item > = [ ] ;
2021-03-17 16:32:37 +01:00
let skipped = Number . MAX_SAFE_INTEGER ;
const scaleBox = 2.5 ; // increase box size
2021-06-03 15:41:53 +02:00
export async function load ( config : Config ) : Promise < GraphModel > {
2021-03-17 16:32:37 +01:00
if ( ! model ) {
2021-04-09 14:07:58 +02:00
model = await tf . loadGraphModel ( join ( config . modelBasePath , config . object . modelPath ) ) ;
2021-04-09 16:02:40 +02:00
const inputs = Object . values ( model . modelSignature [ 'inputs' ] ) ;
model . inputSize = Array . isArray ( inputs ) ? parseInt ( inputs [ 0 ] . tensorShape . dim [ 2 ] . size ) : null ;
if ( ! model . inputSize ) throw new Error ( ` Human: Cannot determine model inputSize: ${ config . object . modelPath } ` ) ;
2021-04-09 14:07:58 +02:00
if ( ! model || ! model . modelUrl ) log ( 'load model failed:' , config . object . modelPath ) ;
else if ( config . debug ) log ( 'load model:' , model . modelUrl ) ;
2021-04-12 14:29:52 +02:00
} else if ( config . debug ) log ( 'cached model:' , model . modelUrl ) ;
2021-03-17 16:32:37 +01:00
return model ;
}
async function process ( res , inputSize , outputShape , config ) {
2021-03-23 19:46:44 +01:00
let id = 0 ;
2021-05-24 13:16:38 +02:00
let results : Array < Item > = [ ] ;
2021-03-17 16:32:37 +01:00
for ( const strideSize of [ 1 , 2 , 4 ] ) { // try each stride size as it detects large/medium/small objects
// find scores, boxes, classes
tf . tidy ( ( ) = > { // wrap in tidy to automatically deallocate temp tensors
const baseSize = strideSize * 13 ; // 13x13=169, 26x26=676, 52x52=2704
// find boxes and scores output depending on stride
2021-03-27 15:25:31 +01:00
const scoresT = res . find ( ( a ) = > ( a . shape [ 1 ] === ( baseSize * * 2 ) && a . shape [ 2 ] === labels . length ) ) ? . squeeze ( ) ;
const featuresT = res . find ( ( a ) = > ( a . shape [ 1 ] === ( baseSize * * 2 ) && a . shape [ 2 ] < labels . length ) ) ? . squeeze ( ) ;
2021-03-23 19:46:44 +01:00
const boxesMax = featuresT . reshape ( [ - 1 , 4 , featuresT . shape [ 1 ] / 4 ] ) ; // reshape [output] to [4, output / 4] where number is number of different features inside each stride
2021-03-17 16:32:37 +01:00
const boxIdx = boxesMax . argMax ( 2 ) . arraySync ( ) ; // what we need is indexes of features with highest scores, not values itself
2021-03-27 15:25:31 +01:00
const scores = scoresT . arraySync ( ) ; // optionally use exponential scores or just as-is
2021-03-23 19:46:44 +01:00
for ( let i = 0 ; i < scoresT . shape [ 0 ] ; i ++ ) { // total strides (x * y matrix)
for ( let j = 0 ; j < scoresT . shape [ 1 ] ; j ++ ) { // one score for each class
2021-03-27 15:25:31 +01:00
const score = scores [ i ] [ j ] ; // get score for current position
if ( score > config . object . minConfidence && j !== 61 ) {
2021-03-23 19:46:44 +01:00
const cx = ( 0.5 + Math . trunc ( i % baseSize ) ) / baseSize ; // center.x normalized to range 0..1
const cy = ( 0.5 + Math . trunc ( i / baseSize ) ) / baseSize ; // center.y normalized to range 0..1
const boxOffset = boxIdx [ i ] . map ( ( a ) = > a * ( baseSize / strideSize / inputSize ) ) ; // just grab indexes of features with highest scores
2021-03-27 15:25:31 +01:00
const [ x , y ] = [
2021-03-23 19:46:44 +01:00
cx - ( scaleBox / strideSize * boxOffset [ 0 ] ) ,
cy - ( scaleBox / strideSize * boxOffset [ 1 ] ) ,
] ;
2021-03-27 15:25:31 +01:00
const [ w , h ] = [
cx + ( scaleBox / strideSize * boxOffset [ 2 ] ) - x ,
cy + ( scaleBox / strideSize * boxOffset [ 3 ] ) - y ,
] ;
let boxRaw = [ x , y , w , h ] ; // results normalized to range 0..1
2021-03-23 19:46:44 +01:00
boxRaw = boxRaw . map ( ( a ) = > Math . max ( 0 , Math . min ( a , 1 ) ) ) ; // fix out-of-bounds coords
const box = [ // results normalized to input image pixels
boxRaw [ 0 ] * outputShape [ 0 ] ,
boxRaw [ 1 ] * outputShape [ 1 ] ,
boxRaw [ 2 ] * outputShape [ 0 ] ,
boxRaw [ 3 ] * outputShape [ 1 ] ,
] ;
const result = {
id : id ++ ,
2021-06-01 13:07:01 +02:00
// strideSize,
2021-04-01 15:24:56 +02:00
score : Math.round ( 100 * score ) / 100 ,
2021-03-23 19:46:44 +01:00
class : j + 1 ,
label : labels [ j ] . label ,
2021-06-01 13:07:01 +02:00
// center: [Math.trunc(outputShape[0] * cx), Math.trunc(outputShape[1] * cy)],
// centerRaw: [cx, cy],
box : ( box . map ( ( a ) = > Math . trunc ( a ) ) ) as [ number , number , number , number ] ,
boxRaw : boxRaw as [ number , number , number , number ] ,
2021-03-23 19:46:44 +01:00
} ;
results . push ( result ) ;
}
2021-03-17 16:32:37 +01:00
}
}
} ) ;
}
// deallocate tensors
res . forEach ( ( t ) = > tf . dispose ( t ) ) ;
// normally nms is run on raw results, but since boxes need to be calculated this way we skip calulcation of
// unnecessary boxes and run nms only on good candidates (basically it just does IOU analysis as scores are already filtered)
2021-05-19 14:27:28 +02:00
const nmsBoxes = results . map ( ( a ) = > [ a . boxRaw [ 1 ] , a . boxRaw [ 0 ] , a . boxRaw [ 3 ] , a . boxRaw [ 2 ] ] ) ; // switches coordinates from x,y to y,x as expected by tf.nms
2021-03-17 16:32:37 +01:00
const nmsScores = results . map ( ( a ) = > a . score ) ;
2021-05-22 20:53:51 +02:00
let nmsIdx : Array < number > = [ ] ;
2021-03-27 15:25:31 +01:00
if ( nmsBoxes && nmsBoxes . length > 0 ) {
2021-04-25 19:16:04 +02:00
const nms = await tf . image . nonMaxSuppressionAsync ( nmsBoxes , nmsScores , config . object . maxDetected , config . object . iouThreshold , config . object . minConfidence ) ;
2021-03-27 15:25:31 +01:00
nmsIdx = nms . dataSync ( ) ;
tf . dispose ( nms ) ;
}
2021-03-17 16:32:37 +01:00
// filter & sort results
results = results
. filter ( ( a , idx ) = > nmsIdx . includes ( idx ) )
. sort ( ( a , b ) = > ( b . score - a . score ) ) ;
return results ;
}
2021-06-03 15:41:53 +02:00
export async function predict ( image : Tensor , config : Config ) : Promise < Item [ ] > {
2021-05-18 17:26:16 +02:00
if ( ( skipped < config . object . skipFrames ) && config . skipFrame && ( last . length > 0 ) ) {
2021-03-17 16:32:37 +01:00
skipped ++ ;
return last ;
}
2021-05-18 17:26:16 +02:00
skipped = 0 ;
2021-03-17 16:32:37 +01:00
return new Promise ( async ( resolve ) = > {
const outputSize = [ image . shape [ 2 ] , image . shape [ 1 ] ] ;
const resize = tf . image . resizeBilinear ( image , [ model . inputSize , model . inputSize ] , false ) ;
const norm = resize . div ( 255 ) ;
const transpose = norm . transpose ( [ 0 , 3 , 1 , 2 ] ) ;
norm . dispose ( ) ;
2021-03-27 15:25:31 +01:00
resize . dispose ( ) ;
2021-03-17 16:32:37 +01:00
let objectT ;
2021-04-25 19:16:04 +02:00
if ( config . object . enabled ) objectT = await model . predict ( transpose ) ;
2021-03-17 16:32:37 +01:00
transpose . dispose ( ) ;
const obj = await process ( objectT , model . inputSize , outputSize , config ) ;
last = obj ;
resolve ( obj ) ;
} ) ;
}