Skip to content

Commit e357136

Browse files
support tensor inputs in high level API
1 parent 958ca95 commit e357136

File tree

10 files changed

+228
-20
lines changed

10 files changed

+228
-20
lines changed

jasmine-node.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11

2-
const spec_files = ['**/*.test.ts'].concat(
2+
let spec_files = ['**/*.test.ts'].concat(
33
process.env.EXCLUDE_UNCOMPRESSED
44
? ['!**/*.uncompressed.test.ts']
55
: []
66
)
77

8+
// exclude browser tests
9+
spec_files = spec_files.concat(['!**/*.browser.test.ts'])
10+
811
module.exports = {
912
spec_dir: 'test',
1013
spec_files,

karma.conf.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ exclude = exclude.concat(
3636
: []
3737
)
3838

39+
// exclude nodejs tests
40+
exclude = exclude.concat(['**/*.node.test.ts'])
41+
42+
3943
module.exports = function(config) {
4044
const args = []
4145
if (process.env.BACKEND_CPU) {

src/dom/extractFaceTensors.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as tf from '@tensorflow/tfjs-core';
2-
import { isTensor4D, Rect } from 'tfjs-image-recognition-base';
2+
import { isTensor4D, Rect, isTensor3D } from 'tfjs-image-recognition-base';
33

44
import { FaceDetection } from '../classes/FaceDetection';
55

@@ -18,6 +18,10 @@ export async function extractFaceTensors(
1818
detections: Array<FaceDetection | Rect>
1919
): Promise<tf.Tensor3D[]> {
2020

21+
if (!isTensor3D(imageTensor) && !isTensor4D(imageTensor)) {
22+
throw new Error('extractFaceTensors - expected image tensor to be 3D or 4D')
23+
}
24+
2125
if (isTensor4D(imageTensor) && imageTensor.shape[0] > 1) {
2226
throw new Error('extractFaceTensors - batchSize > 1 not supported')
2327
}

src/dom/extractFaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
createCanvas,
3+
env,
34
getContext2dOrThrow,
45
imageTensorToCanvas,
56
Rect,
@@ -8,7 +9,6 @@ import {
89
} from 'tfjs-image-recognition-base';
910

1011
import { FaceDetection } from '../classes/FaceDetection';
11-
import { env } from 'tfjs-image-recognition-base';
1212

1313
/**
1414
* Extracts the image regions containing the detected faces.

src/globalApi/ComputeFaceDescriptorsTasks.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import * as tf from '@tensorflow/tfjs-core';
12
import { TNetInput } from 'tfjs-image-recognition-base';
23

34
import { FaceDetectionWithLandmarks } from '../classes/FaceDetectionWithLandmarks';
45
import { FullFaceDescription } from '../classes/FullFaceDescription';
5-
import { extractFaces } from '../dom';
6+
import { extractFaces, extractFaceTensors } from '../dom';
67
import { ComposableTask } from './ComposableTask';
78
import { nets } from './nets';
89

@@ -20,15 +21,20 @@ export class ComputeAllFaceDescriptorsTask extends ComputeFaceDescriptorsTaskBas
2021
public async run(): Promise<FullFaceDescription[]> {
2122

2223
const facesWithLandmarks = await this.detectFaceLandmarksTask
23-
const alignedFaceCanvases = await extractFaces(
24-
this.input,
25-
facesWithLandmarks.map(({ landmarks }) => landmarks.align())
26-
)
2724

28-
return await Promise.all(facesWithLandmarks.map(async ({ detection, landmarks }, i) => {
29-
const descriptor = await nets.faceRecognitionNet.computeFaceDescriptor(alignedFaceCanvases[i]) as Float32Array
25+
const alignedRects = facesWithLandmarks.map(({ alignedRect }) => alignedRect)
26+
const alignedFaces: Array<HTMLCanvasElement | tf.Tensor3D> = this.input instanceof tf.Tensor
27+
? await extractFaceTensors(this.input, alignedRects)
28+
: await extractFaces(this.input, alignedRects)
29+
30+
const fullFaceDescriptions = await Promise.all(facesWithLandmarks.map(async ({ detection, landmarks }, i) => {
31+
const descriptor = await nets.faceRecognitionNet.computeFaceDescriptor(alignedFaces[i]) as Float32Array
3032
return new FullFaceDescription(detection, landmarks, descriptor)
3133
}))
34+
35+
alignedFaces.forEach(f => f instanceof tf.Tensor && f.dispose())
36+
37+
return fullFaceDescriptions
3238
}
3339
}
3440

@@ -42,8 +48,12 @@ export class ComputeSingleFaceDescriptorTask extends ComputeFaceDescriptorsTaskB
4248
}
4349

4450
const { detection, landmarks, alignedRect } = detectionWithLandmarks
45-
const alignedFaceCanvas = (await extractFaces(this.input, [alignedRect]))[0]
46-
const descriptor = await nets.faceRecognitionNet.computeFaceDescriptor(alignedFaceCanvas) as Float32Array
51+
const alignedFaces: Array<HTMLCanvasElement | tf.Tensor3D> = this.input instanceof tf.Tensor
52+
? await extractFaceTensors(this.input, [alignedRect])
53+
: await extractFaces(this.input, [alignedRect])
54+
const descriptor = await nets.faceRecognitionNet.computeFaceDescriptor(alignedFaces[0]) as Float32Array
55+
56+
alignedFaces.forEach(f => f instanceof tf.Tensor && f.dispose())
4757

4858
return new FullFaceDescription(detection, landmarks, descriptor)
4959
}

src/globalApi/DetectFaceLandmarksTasks.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import * as tf from '@tensorflow/tfjs-core';
12
import { TNetInput } from 'tfjs-image-recognition-base';
23

34
import { FaceDetection } from '../classes/FaceDetection';
45
import { FaceDetectionWithLandmarks } from '../classes/FaceDetectionWithLandmarks';
56
import { FaceLandmarks68 } from '../classes/FaceLandmarks68';
6-
import { extractFaces } from '../dom';
7+
import { extractFaces, extractFaceTensors } from '../dom';
78
import { FaceLandmark68Net } from '../faceLandmarkNet/FaceLandmark68Net';
89
import { FaceLandmark68TinyNet } from '../faceLandmarkNet/FaceLandmark68TinyNet';
910
import { ComposableTask } from './ComposableTask';
@@ -31,12 +32,17 @@ export class DetectAllFaceLandmarksTask extends DetectFaceLandmarksTaskBase<Face
3132
public async run(): Promise<FaceDetectionWithLandmarks[]> {
3233

3334
const detections = await this.detectFacesTask
34-
const faceCanvases = await extractFaces(this.input, detections)
3535

36-
const faceLandmarksByFace = await Promise.all(faceCanvases.map(
37-
canvas => this.landmarkNet.detectLandmarks(canvas)
36+
const faces: Array<HTMLCanvasElement | tf.Tensor3D> = this.input instanceof tf.Tensor
37+
? await extractFaceTensors(this.input, detections)
38+
: await extractFaces(this.input, detections)
39+
40+
const faceLandmarksByFace = await Promise.all(faces.map(
41+
face => this.landmarkNet.detectLandmarks(face)
3842
)) as FaceLandmarks68[]
3943

44+
faces.forEach(f => f instanceof tf.Tensor && f.dispose())
45+
4046
return detections.map((detection, i) =>
4147
new FaceDetectionWithLandmarks(detection, faceLandmarksByFace[i])
4248
)
@@ -56,10 +62,18 @@ export class DetectSingleFaceLandmarksTask extends DetectFaceLandmarksTaskBase<F
5662
return
5763
}
5864

59-
const faceCanvas = (await extractFaces(this.input, [detection]))[0]
65+
const faces: Array<HTMLCanvasElement | tf.Tensor3D> = this.input instanceof tf.Tensor
66+
? await extractFaceTensors(this.input, [detection])
67+
: await extractFaces(this.input, [detection])
68+
69+
70+
const landmarks = await this.landmarkNet.detectLandmarks(faces[0]) as FaceLandmarks68
71+
72+
faces.forEach(f => f instanceof tf.Tensor && f.dispose())
73+
6074
return new FaceDetectionWithLandmarks(
6175
detection,
62-
await this.landmarkNet.detectLandmarks(faceCanvas) as FaceLandmarks68
76+
landmarks
6377
)
6478
}
6579

test/tests/mtcnn/mtcnn.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('mtcnn', () => {
2323
expectedFullFaceDescriptions = await assembleExpectedFullFaceDescriptions(expectedMtcnnBoxes)
2424
})
2525

26-
describeWithNets('detectAllFaces', { withAllFacesMtcnn: true }, () => {
26+
describeWithNets('globalApi', { withAllFacesMtcnn: true }, () => {
2727

2828
it('detectAllFaces', async () => {
2929
const options = new MtcnnOptions({
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as faceapi from '../../../src';
2+
import { describeWithNets, expectAllTensorsReleased, assembleExpectedFullFaceDescriptions, ExpectedFullFaceDescription } from '../../utils';
3+
import { SsdMobilenetv1Options, createCanvasFromMedia } from '../../../src';
4+
import { expectFaceDetections } from '../../expectFaceDetections';
5+
import { expectFullFaceDescriptions } from '../../expectFullFaceDescriptions';
6+
import { expectFaceDetectionsWithLandmarks } from '../../expectFaceDetectionsWithLandmarks';
7+
import { expectedSsdBoxes } from './expectedBoxes';
8+
import { loadImage } from '../../env';
9+
import * as tf from '@tensorflow/tfjs-core';
10+
11+
describe('ssdMobilenetv1 - node', () => {
12+
13+
let imgTensor: faceapi.tf.Tensor3D
14+
let expectedFullFaceDescriptions: ExpectedFullFaceDescription[]
15+
const expectedScores = [0.54, 0.81, 0.97, 0.88, 0.84, 0.61]
16+
17+
beforeAll(async () => {
18+
imgTensor = tf.fromPixels(createCanvasFromMedia(await loadImage('test/images/faces.jpg')))
19+
expectedFullFaceDescriptions = await assembleExpectedFullFaceDescriptions(expectedSsdBoxes)
20+
})
21+
22+
describeWithNets('globalApi, tensor inputs', { withAllFacesSsdMobilenetv1: true }, () => {
23+
24+
it('detectAllFaces', async () => {
25+
const options = new SsdMobilenetv1Options({
26+
minConfidence: 0.5
27+
})
28+
29+
const results = await faceapi.detectAllFaces(imgTensor, options)
30+
31+
const maxScoreDelta = 0.05
32+
const maxBoxDelta = 5
33+
expect(results.length).toEqual(6)
34+
expectFaceDetections(results, expectedSsdBoxes, expectedScores, maxScoreDelta, maxBoxDelta)
35+
})
36+
37+
it('detectAllFaces.withFaceLandmarks()', async () => {
38+
const options = new SsdMobilenetv1Options({
39+
minConfidence: 0.5
40+
})
41+
42+
const results = await faceapi
43+
.detectAllFaces(imgTensor, options)
44+
.withFaceLandmarks()
45+
46+
const deltas = {
47+
maxScoreDelta: 0.05,
48+
maxBoxDelta: 5,
49+
maxLandmarksDelta: 4
50+
}
51+
expect(results.length).toEqual(6)
52+
expectFaceDetectionsWithLandmarks(results, expectedFullFaceDescriptions, expectedScores, deltas)
53+
})
54+
55+
it('detectAllFaces.withFaceLandmarks().withFaceDescriptors()', async () => {
56+
const options = new SsdMobilenetv1Options({
57+
minConfidence: 0.5
58+
})
59+
60+
const results = await faceapi
61+
.detectAllFaces(imgTensor, options)
62+
.withFaceLandmarks()
63+
.withFaceDescriptors()
64+
65+
const deltas = {
66+
maxScoreDelta: 0.05,
67+
maxBoxDelta: 5,
68+
maxLandmarksDelta: 4,
69+
maxDescriptorDelta: 0.2
70+
}
71+
expect(results.length).toEqual(6)
72+
expectFullFaceDescriptions(results, expectedFullFaceDescriptions, expectedScores, deltas)
73+
})
74+
75+
it('no memory leaks', async () => {
76+
await expectAllTensorsReleased(async () => {
77+
await faceapi
78+
.detectAllFaces(imgTensor, new SsdMobilenetv1Options())
79+
.withFaceLandmarks()
80+
.withFaceDescriptors()
81+
})
82+
})
83+
84+
})
85+
86+
})
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as faceapi from '../../../src';
2+
import { describeWithNets, expectAllTensorsReleased, assembleExpectedFullFaceDescriptions, ExpectedFullFaceDescription } from '../../utils';
3+
import { TinyFaceDetectorOptions, createCanvasFromMedia } from '../../../src';
4+
import { expectFaceDetections } from '../../expectFaceDetections';
5+
import { expectFullFaceDescriptions } from '../../expectFullFaceDescriptions';
6+
import { expectFaceDetectionsWithLandmarks } from '../../expectFaceDetectionsWithLandmarks';
7+
import { expectedTinyFaceDetectorBoxes } from './expectedBoxes';
8+
import { loadImage } from '../../env';
9+
import * as tf from '@tensorflow/tfjs-core';
10+
11+
describe('tinyFaceDetector - node', () => {
12+
13+
let imgTensor: faceapi.tf.Tensor3D
14+
let expectedFullFaceDescriptions: ExpectedFullFaceDescription[]
15+
const expectedScores = [0.7, 0.82, 0.93, 0.86, 0.79, 0.84]
16+
17+
beforeAll(async () => {
18+
imgTensor = tf.fromPixels(createCanvasFromMedia(await loadImage('test/images/faces.jpg')))
19+
expectedFullFaceDescriptions = await assembleExpectedFullFaceDescriptions(expectedTinyFaceDetectorBoxes)
20+
})
21+
22+
describeWithNets('globalApi, tensor inputs', { withAllFacesTinyFaceDetector: true }, () => {
23+
24+
it('detectAllFaces', async () => {
25+
const options = new TinyFaceDetectorOptions({
26+
inputSize: 416
27+
})
28+
29+
const results = await faceapi.detectAllFaces(imgTensor, options)
30+
31+
const maxScoreDelta = 0.05
32+
const maxBoxDelta = 5
33+
expect(results.length).toEqual(6)
34+
expectFaceDetections(results, expectedTinyFaceDetectorBoxes, expectedScores, maxScoreDelta, maxBoxDelta)
35+
})
36+
37+
it('detectAllFaces.withFaceLandmarks()', async () => {
38+
const options = new TinyFaceDetectorOptions({
39+
inputSize: 416
40+
})
41+
42+
const results = await faceapi
43+
.detectAllFaces(imgTensor, options)
44+
.withFaceLandmarks()
45+
46+
const deltas = {
47+
maxScoreDelta: 0.05,
48+
maxBoxDelta: 5,
49+
maxLandmarksDelta: 10
50+
}
51+
expect(results.length).toEqual(6)
52+
expectFaceDetectionsWithLandmarks(results, expectedFullFaceDescriptions, expectedScores, deltas)
53+
})
54+
55+
it('detectAllFaces.withFaceLandmarks().withFaceDescriptors()', async () => {
56+
const options = new TinyFaceDetectorOptions({
57+
inputSize: 416
58+
})
59+
60+
const results = await faceapi
61+
.detectAllFaces(imgTensor, options)
62+
.withFaceLandmarks()
63+
.withFaceDescriptors()
64+
65+
const deltas = {
66+
maxScoreDelta: 0.05,
67+
maxBoxDelta: 5,
68+
maxLandmarksDelta: 10,
69+
maxDescriptorDelta: 0.2
70+
}
71+
expect(results.length).toEqual(6)
72+
expectFullFaceDescriptions(results, expectedFullFaceDescriptions, expectedScores, deltas)
73+
})
74+
75+
it('no memory leaks', async () => {
76+
await expectAllTensorsReleased(async () => {
77+
await faceapi
78+
.detectAllFaces(imgTensor, new TinyFaceDetectorOptions())
79+
.withFaceLandmarks()
80+
.withFaceDescriptors()
81+
})
82+
})
83+
84+
})
85+
86+
})

test/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as tf from '@tensorflow/tfjs-core';
2+
import { getContext2dOrThrow } from 'tfjs-image-recognition-base';
23

34
import * as faceapi from '../src';
4-
import { FaceRecognitionNet, IPoint, IRect, Mtcnn, TinyYolov2 } from '../src/';
5+
import { createCanvasFromMedia, FaceRecognitionNet, IPoint, IRect, Mtcnn, TinyYolov2 } from '../src/';
56
import { FaceDetection } from '../src/classes/FaceDetection';
67
import { FaceLandmarks } from '../src/classes/FaceLandmarks';
78
import { FaceLandmark68Net } from '../src/faceLandmarkNet/FaceLandmark68Net';

0 commit comments

Comments
 (0)