face-api/src/tinyYolov2/TinyYolov2Base.ts

248 lines
9.5 KiB
TypeScript
Raw Normal View History

2020-12-23 18:58:47 +01:00
import * as tf from '../../dist/tfjs.esm';
2020-08-18 13:54:53 +02:00
import { BoundingBox } from '../classes/BoundingBox';
import { Dimensions } from '../classes/Dimensions';
import { ObjectDetection } from '../classes/ObjectDetection';
2020-12-19 17:46:41 +01:00
import { convLayer } from '../common/index';
2020-08-18 13:54:53 +02:00
import { ConvParams, SeparableConvParams } from '../common/types';
2020-12-19 17:46:41 +01:00
import { toNetInput } from '../dom/index';
2020-08-18 13:54:53 +02:00
import { NetInput } from '../dom/NetInput';
import { TNetInput } from '../dom/types';
import { NeuralNetwork } from '../NeuralNetwork';
2020-12-19 17:46:41 +01:00
import { sigmoid } from '../ops/index';
2020-08-18 13:54:53 +02:00
import { nonMaxSuppression } from '../ops/nonMaxSuppression';
import { normalize } from '../ops/normalize';
import { TinyYolov2Config, validateConfig } from './config';
import { convWithBatchNorm } from './convWithBatchNorm';
import { depthwiseSeparableConv } from './depthwiseSeparableConv';
import { extractParams } from './extractParams';
2021-01-12 16:14:33 +01:00
import { extractParamsFromWeightMap } from './extractParamsFromWeightMap';
2020-08-18 13:54:53 +02:00
import { leaky } from './leaky';
import { ITinyYolov2Options, TinyYolov2Options } from './TinyYolov2Options';
import { DefaultTinyYolov2NetParams, MobilenetParams, TinyYolov2NetParams } from './types';
export class TinyYolov2Base extends NeuralNetwork<TinyYolov2NetParams> {
public static DEFAULT_FILTER_SIZES = [
2020-12-23 17:26:55 +01:00
3, 16, 32, 64, 128, 256, 512, 1024, 1024,
2020-08-18 13:54:53 +02:00
]
private _config: TinyYolov2Config
constructor(config: TinyYolov2Config) {
2020-12-23 17:26:55 +01:00
super('TinyYolov2');
validateConfig(config);
this._config = config;
2020-08-18 13:54:53 +02:00
}
public get config(): TinyYolov2Config {
2020-12-23 17:26:55 +01:00
return this._config;
2020-08-18 13:54:53 +02:00
}
public get withClassScores(): boolean {
2020-12-23 17:26:55 +01:00
return this.config.withClassScores || this.config.classes.length > 1;
2020-08-18 13:54:53 +02:00
}
public get boxEncodingSize(): number {
2020-12-23 17:26:55 +01:00
return 5 + (this.withClassScores ? this.config.classes.length : 0);
2020-08-18 13:54:53 +02:00
}
public runTinyYolov2(x: tf.Tensor4D, params: DefaultTinyYolov2NetParams): tf.Tensor4D {
2020-12-23 17:26:55 +01:00
let out = convWithBatchNorm(x, params.conv0);
out = tf.maxPool(out, [2, 2], [2, 2], 'same');
out = convWithBatchNorm(out, params.conv1);
out = tf.maxPool(out, [2, 2], [2, 2], 'same');
out = convWithBatchNorm(out, params.conv2);
out = tf.maxPool(out, [2, 2], [2, 2], 'same');
out = convWithBatchNorm(out, params.conv3);
out = tf.maxPool(out, [2, 2], [2, 2], 'same');
out = convWithBatchNorm(out, params.conv4);
out = tf.maxPool(out, [2, 2], [2, 2], 'same');
out = convWithBatchNorm(out, params.conv5);
out = tf.maxPool(out, [2, 2], [1, 1], 'same');
out = convWithBatchNorm(out, params.conv6);
out = convWithBatchNorm(out, params.conv7);
return convLayer(out, params.conv8, 'valid', false);
2020-08-18 13:54:53 +02:00
}
public runMobilenet(x: tf.Tensor4D, params: MobilenetParams): tf.Tensor4D {
let out = this.config.isFirstLayerConv2d
? leaky(convLayer(x, params.conv0 as ConvParams, 'valid', false))
2020-12-23 17:26:55 +01:00
: depthwiseSeparableConv(x, params.conv0 as SeparableConvParams);
out = tf.maxPool(out, [2, 2], [2, 2], 'same');
out = depthwiseSeparableConv(out, params.conv1);
out = tf.maxPool(out, [2, 2], [2, 2], 'same');
out = depthwiseSeparableConv(out, params.conv2);
out = tf.maxPool(out, [2, 2], [2, 2], 'same');
out = depthwiseSeparableConv(out, params.conv3);
out = tf.maxPool(out, [2, 2], [2, 2], 'same');
out = depthwiseSeparableConv(out, params.conv4);
out = tf.maxPool(out, [2, 2], [2, 2], 'same');
out = depthwiseSeparableConv(out, params.conv5);
out = tf.maxPool(out, [2, 2], [1, 1], 'same');
out = params.conv6 ? depthwiseSeparableConv(out, params.conv6) : out;
out = params.conv7 ? depthwiseSeparableConv(out, params.conv7) : out;
return convLayer(out, params.conv8, 'valid', false);
2020-08-18 13:54:53 +02:00
}
public forwardInput(input: NetInput, inputSize: number): tf.Tensor4D {
2020-12-23 17:26:55 +01:00
const { params } = this;
2020-08-18 13:54:53 +02:00
if (!params) {
2020-12-23 17:26:55 +01:00
throw new Error('TinyYolov2 - load model before inference');
2020-08-18 13:54:53 +02:00
}
return tf.tidy(() => {
// let batchTensor = input.toBatchTensor(inputSize, false).toFloat()
let batchTensor = tf.cast(input.toBatchTensor(inputSize, false), 'float32');
2020-08-18 13:54:53 +02:00
batchTensor = this.config.meanRgb
? normalize(batchTensor, this.config.meanRgb)
2020-12-23 17:26:55 +01:00
: batchTensor;
batchTensor = batchTensor.div(tf.scalar(256)) as tf.Tensor4D;
2020-08-18 13:54:53 +02:00
return this.config.withSeparableConvs
? this.runMobilenet(batchTensor, params as MobilenetParams)
2020-12-23 17:26:55 +01:00
: this.runTinyYolov2(batchTensor, params as DefaultTinyYolov2NetParams);
});
2020-08-18 13:54:53 +02:00
}
public async forward(input: TNetInput, inputSize: number): Promise<tf.Tensor4D> {
2020-12-23 17:26:55 +01:00
return this.forwardInput(await toNetInput(input), inputSize);
2020-08-18 13:54:53 +02:00
}
public async detect(input: TNetInput, forwardParams: ITinyYolov2Options = {}): Promise<ObjectDetection[]> {
2020-12-23 17:26:55 +01:00
const { inputSize, scoreThreshold } = new TinyYolov2Options(forwardParams);
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const netInput = await toNetInput(input);
const out = await this.forwardInput(netInput, inputSize);
const out0 = tf.tidy(() => tf.unstack(out)[0].expandDims()) as tf.Tensor4D;
2020-08-18 13:54:53 +02:00
const inputDimensions = {
width: netInput.getInputWidth(0),
2020-12-23 17:26:55 +01:00
height: netInput.getInputHeight(0),
};
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const results = await this.extractBoxes(out0, netInput.getReshapedInputDimensions(0), scoreThreshold);
out.dispose();
out0.dispose();
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const boxes = results.map((res) => res.box);
const scores = results.map((res) => res.score);
const classScores = results.map((res) => res.classScore);
const classNames = results.map((res) => this.config.classes[res.label]);
2020-08-18 13:54:53 +02:00
const indices = nonMaxSuppression(
2020-12-23 17:26:55 +01:00
boxes.map((box) => box.rescale(inputSize)),
2020-08-18 13:54:53 +02:00
scores,
this.config.iouThreshold,
2020-12-23 17:26:55 +01:00
true,
);
const detections = indices.map((idx) => new ObjectDetection(
scores[idx],
classScores[idx],
classNames[idx],
boxes[idx],
inputDimensions,
));
return detections;
2020-08-18 13:54:53 +02:00
}
protected getDefaultModelName(): string {
2020-12-23 17:26:55 +01:00
return '';
2020-08-18 13:54:53 +02:00
}
2021-01-12 16:14:33 +01:00
protected extractParamsFromWeightMap(weightMap: tf.NamedTensorMap) {
return extractParamsFromWeightMap(weightMap, this.config);
2020-08-18 13:54:53 +02:00
}
protected extractParams(weights: Float32Array) {
2020-12-23 17:26:55 +01:00
const filterSizes = this.config.filterSizes || TinyYolov2Base.DEFAULT_FILTER_SIZES;
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const numFilters = filterSizes ? filterSizes.length : undefined;
2020-08-18 13:54:53 +02:00
if (numFilters !== 7 && numFilters !== 8 && numFilters !== 9) {
2020-12-23 17:26:55 +01:00
throw new Error(`TinyYolov2 - expected 7 | 8 | 9 convolutional filters, but found ${numFilters} filterSizes in config`);
2020-08-18 13:54:53 +02:00
}
2020-12-23 17:26:55 +01:00
return extractParams(weights, this.config, this.boxEncodingSize, filterSizes);
2020-08-18 13:54:53 +02:00
}
protected async extractBoxes(
outputTensor: tf.Tensor4D,
inputBlobDimensions: Dimensions,
2020-12-23 17:26:55 +01:00
scoreThreshold?: number,
2020-08-18 13:54:53 +02:00
) {
2020-12-23 17:26:55 +01:00
const { width, height } = inputBlobDimensions;
const inputSize = Math.max(width, height);
const correctionFactorX = inputSize / width;
const correctionFactorY = inputSize / height;
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const numCells = outputTensor.shape[1];
const numBoxes = this.config.anchors.length;
2020-08-18 13:54:53 +02:00
const [boxesTensor, scoresTensor, classScoresTensor] = tf.tidy(() => {
2020-12-23 17:26:55 +01:00
const reshaped = outputTensor.reshape([numCells, numCells, numBoxes, this.boxEncodingSize]);
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const boxes = reshaped.slice([0, 0, 0, 0], [numCells, numCells, numBoxes, 4]);
const scores = reshaped.slice([0, 0, 0, 4], [numCells, numCells, numBoxes, 1]);
2020-08-18 13:54:53 +02:00
const classScores = this.withClassScores
? tf.softmax(reshaped.slice([0, 0, 0, 5], [numCells, numCells, numBoxes, this.config.classes.length]), 3)
2020-12-23 17:26:55 +01:00
: tf.scalar(0);
return [boxes, scores, classScores];
});
2020-08-18 13:54:53 +02:00
const results = [] as any;
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const scoresData = await scoresTensor.array();
const boxesData = await boxesTensor.array();
for (let row = 0; row < numCells; row++) {
for (let col = 0; col < numCells; col++) {
for (let anchor = 0; anchor < numBoxes; anchor++) {
2020-08-18 13:54:53 +02:00
const score = sigmoid(scoresData[row][col][anchor][0]);
if (!scoreThreshold || score > scoreThreshold) {
2020-12-23 17:26:55 +01:00
const ctX = ((col + sigmoid(boxesData[row][col][anchor][0])) / numCells) * correctionFactorX;
const ctY = ((row + sigmoid(boxesData[row][col][anchor][1])) / numCells) * correctionFactorY;
const widthLocal = ((Math.exp(boxesData[row][col][anchor][2]) * this.config.anchors[anchor].x) / numCells) * correctionFactorX;
const heightLocal = ((Math.exp(boxesData[row][col][anchor][3]) * this.config.anchors[anchor].y) / numCells) * correctionFactorY;
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const x = (ctX - (widthLocal / 2));
const y = (ctY - (heightLocal / 2));
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
const pos = { row, col, anchor };
2020-08-18 13:54:53 +02:00
const { classScore, label } = this.withClassScores
? await this.extractPredictedClass(classScoresTensor as tf.Tensor4D, pos)
2020-12-23 17:26:55 +01:00
: { classScore: 1, label: 0 };
2020-08-18 13:54:53 +02:00
results.push({
2020-12-23 17:26:55 +01:00
box: new BoundingBox(x, y, x + widthLocal, y + heightLocal),
score,
2020-08-18 13:54:53 +02:00
classScore: score * classScore,
label,
2020-12-23 17:26:55 +01:00
...pos,
});
2020-08-18 13:54:53 +02:00
}
}
}
}
2020-12-23 17:26:55 +01:00
boxesTensor.dispose();
scoresTensor.dispose();
classScoresTensor.dispose();
2020-08-18 13:54:53 +02:00
2020-12-23 17:26:55 +01:00
return results;
2020-08-18 13:54:53 +02:00
}
2020-12-23 17:26:55 +01:00
private async extractPredictedClass(classesTensor: tf.Tensor4D, pos: { row: number, col: number, anchor: number }) {
const { row, col, anchor } = pos;
const classesData = await classesTensor.array();
2020-08-18 13:54:53 +02:00
return Array(this.config.classes.length).fill(0)
.map((_, i) => classesData[row][col][anchor][i])
.map((classScore, label) => ({
classScore,
2020-12-23 17:26:55 +01:00
label,
2020-08-18 13:54:53 +02:00
}))
2020-12-23 17:26:55 +01:00
.reduce((max, curr) => (max.classScore > curr.classScore ? max : curr));
2020-08-18 13:54:53 +02:00
}
2020-12-23 17:26:55 +01:00
}