Skip to content

Commit 5ff0c5c

Browse files
test cases for AgeGenderNet + fixed memory leaks and batch inputs for AgeGenderNet
1 parent 5be059a commit 5ff0c5c

File tree

4 files changed

+238
-5
lines changed

4 files changed

+238
-5
lines changed

src/ageGenderNet/AgeGenderNet.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ export class AgeGenderNet extends NeuralNetwork<NetParams> {
4242
}
4343

4444
public forwardInput(input: NetInput | tf.Tensor4D): NetOutput {
45-
const { age, gender } = this.runNet(input)
46-
return tf.tidy(() => ({ age, gender: tf.softmax(gender) }))
45+
return tf.tidy(() => {
46+
const { age, gender } = this.runNet(input)
47+
return { age, gender: tf.softmax(gender) }
48+
})
4749
}
4850

4951
public async forward(input: TNetInput): Promise<NetOutput> {
@@ -64,14 +66,18 @@ export class AgeGenderNet extends NeuralNetwork<NetParams> {
6466
const predictionsByBatch = await Promise.all(
6567
ageAndGenderTensors.map(async ({ ageTensor, genderTensor }) => {
6668
const age = (await ageTensor.data())[0]
67-
const probMale = (await out.gender.data())[0]
69+
const probMale = (await genderTensor.data())[0]
6870
const isMale = probMale > 0.5
6971
const gender = isMale ? Gender.MALE : Gender.FEMALE
7072
const genderProbability = isMale ? probMale : (1 - probMale)
7173

74+
ageTensor.dispose()
75+
genderTensor.dispose()
7276
return { age, gender, genderProbability }
7377
})
7478
)
79+
out.age.dispose()
80+
out.gender.dispose()
7581

7682
return netInput.isBatchInput
7783
? predictionsByBatch
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import * as tf from '@tensorflow/tfjs-core';
2+
3+
import { createCanvasFromMedia, NetInput, toNetInput } from '../../../src';
4+
import { AgeAndGenderPrediction } from '../../../src/ageGenderNet/types';
5+
import { loadImage } from '../../env';
6+
import { describeWithBackend, describeWithNets, expectAllTensorsReleased } from '../../utils';
7+
8+
function expectResultsAngry(result: AgeAndGenderPrediction) {
9+
expect(result.age).toBeGreaterThanOrEqual(38)
10+
expect(result.age).toBeLessThanOrEqual(42)
11+
expect(result.gender).toEqual('male')
12+
expect(result.genderProbability).toBeGreaterThanOrEqual(0.9)
13+
}
14+
15+
function expectResultsSurprised(result: AgeAndGenderPrediction) {
16+
expect(result.age).toBeGreaterThanOrEqual(24)
17+
expect(result.age).toBeLessThanOrEqual(28)
18+
expect(result.gender).toEqual('female')
19+
expect(result.genderProbability).toBeGreaterThanOrEqual(0.8)
20+
}
21+
22+
describeWithBackend('ageGenderNet', () => {
23+
24+
let imgElAngry: HTMLImageElement
25+
let imgElSurprised: HTMLImageElement
26+
27+
beforeAll(async () => {
28+
imgElAngry = await loadImage('test/images/angry_cropped.jpg')
29+
imgElSurprised = await loadImage('test/images/surprised_cropped.jpg')
30+
})
31+
32+
describeWithNets('quantized weights', { withAgeGenderNet: { quantized: true } }, ({ ageGenderNet }) => {
33+
34+
it('recognizes age and gender', async () => {
35+
const result = await ageGenderNet.predictAgeAndGender(imgElAngry) as AgeAndGenderPrediction
36+
expectResultsAngry(result)
37+
})
38+
39+
})
40+
41+
describeWithNets('batch inputs', { withAgeGenderNet: { quantized: true } }, ({ ageGenderNet }) => {
42+
43+
it('recognizes age and gender for batch of image elements', async () => {
44+
const inputs = [imgElAngry, imgElSurprised]
45+
46+
const results = await ageGenderNet.predictAgeAndGender(inputs) as AgeAndGenderPrediction[]
47+
expect(Array.isArray(results)).toBe(true)
48+
expect(results.length).toEqual(2)
49+
50+
const [resultAngry, resultSurprised] = results
51+
expectResultsAngry(resultAngry)
52+
expectResultsSurprised(resultSurprised)
53+
})
54+
55+
it('computes age and gender for batch of tf.Tensor3D', async () => {
56+
const inputs = [imgElAngry, imgElSurprised].map(el => tf.browser.fromPixels(createCanvasFromMedia(el)))
57+
58+
const results = await ageGenderNet.predictAgeAndGender(inputs) as AgeAndGenderPrediction[]
59+
expect(Array.isArray(results)).toBe(true)
60+
expect(results.length).toEqual(2)
61+
62+
const [resultAngry, resultSurprised] = results
63+
expectResultsAngry(resultAngry)
64+
expectResultsSurprised(resultSurprised)
65+
})
66+
67+
it('computes age and gender for batch of mixed inputs', async () => {
68+
const inputs = [imgElAngry, tf.browser.fromPixels(createCanvasFromMedia(imgElSurprised))]
69+
70+
const results = await ageGenderNet.predictAgeAndGender(inputs) as AgeAndGenderPrediction[]
71+
expect(Array.isArray(results)).toBe(true)
72+
expect(results.length).toEqual(2)
73+
74+
const [resultAngry, resultSurprised] = results
75+
expectResultsAngry(resultAngry)
76+
expectResultsSurprised(resultSurprised)
77+
})
78+
79+
})
80+
81+
describeWithNets('no memory leaks', { withAgeGenderNet: { quantized: true } }, ({ ageGenderNet }) => {
82+
83+
describe('forwardInput', () => {
84+
85+
it('single image element', async () => {
86+
await expectAllTensorsReleased(async () => {
87+
const netInput = new NetInput([imgElAngry])
88+
const { age, gender } = await ageGenderNet.forwardInput(netInput)
89+
age.dispose()
90+
gender.dispose()
91+
})
92+
})
93+
94+
it('multiple image elements', async () => {
95+
await expectAllTensorsReleased(async () => {
96+
const netInput = new NetInput([imgElAngry, imgElAngry])
97+
const { age, gender } = await ageGenderNet.forwardInput(netInput)
98+
age.dispose()
99+
gender.dispose()
100+
})
101+
})
102+
103+
it('single tf.Tensor3D', async () => {
104+
const tensor = tf.browser.fromPixels(createCanvasFromMedia(imgElAngry))
105+
106+
await expectAllTensorsReleased(async () => {
107+
const { age, gender } = await ageGenderNet.forwardInput(await toNetInput(tensor))
108+
age.dispose()
109+
gender.dispose()
110+
})
111+
112+
tensor.dispose()
113+
})
114+
115+
it('multiple tf.Tensor3Ds', async () => {
116+
const tensors = [imgElAngry, imgElAngry, imgElAngry].map(el => tf.browser.fromPixels(createCanvasFromMedia(el)))
117+
118+
await expectAllTensorsReleased(async () => {
119+
const { age, gender } = await ageGenderNet.forwardInput(await toNetInput(tensors))
120+
age.dispose()
121+
gender.dispose()
122+
})
123+
124+
tensors.forEach(t => t.dispose())
125+
})
126+
127+
it('single batch size 1 tf.Tensor4Ds', async () => {
128+
const tensor = tf.tidy(() => tf.browser.fromPixels(createCanvasFromMedia(imgElAngry)).expandDims()) as tf.Tensor4D
129+
130+
await expectAllTensorsReleased(async () => {
131+
const { age, gender } = await ageGenderNet.forwardInput(await toNetInput(tensor))
132+
age.dispose()
133+
gender.dispose()
134+
})
135+
136+
tensor.dispose()
137+
})
138+
139+
it('multiple batch size 1 tf.Tensor4Ds', async () => {
140+
const tensors = [imgElAngry, imgElAngry, imgElAngry]
141+
.map(el => tf.tidy(() => tf.browser.fromPixels(createCanvasFromMedia(el)).expandDims())) as tf.Tensor4D[]
142+
143+
await expectAllTensorsReleased(async () => {
144+
const { age, gender } = await ageGenderNet.forwardInput(await toNetInput(tensors))
145+
age.dispose()
146+
gender.dispose()
147+
})
148+
149+
tensors.forEach(t => t.dispose())
150+
})
151+
152+
})
153+
154+
describe('predictExpressions', () => {
155+
156+
it('single image element', async () => {
157+
await expectAllTensorsReleased(async () => {
158+
await ageGenderNet.predictAgeAndGender(imgElAngry)
159+
})
160+
})
161+
162+
it('multiple image elements', async () => {
163+
await expectAllTensorsReleased(async () => {
164+
await ageGenderNet.predictAgeAndGender([imgElAngry, imgElAngry, imgElAngry])
165+
})
166+
})
167+
168+
it('single tf.Tensor3D', async () => {
169+
const tensor = tf.browser.fromPixels(createCanvasFromMedia(imgElAngry))
170+
171+
await expectAllTensorsReleased(async () => {
172+
await ageGenderNet.predictAgeAndGender(tensor)
173+
})
174+
175+
tensor.dispose()
176+
})
177+
178+
it('multiple tf.Tensor3Ds', async () => {
179+
const tensors = [imgElAngry, imgElAngry, imgElAngry].map(el => tf.browser.fromPixels(createCanvasFromMedia(el)))
180+
181+
182+
await expectAllTensorsReleased(async () => {
183+
await ageGenderNet.predictAgeAndGender(tensors)
184+
})
185+
186+
tensors.forEach(t => t.dispose())
187+
})
188+
189+
it('single batch size 1 tf.Tensor4Ds', async () => {
190+
const tensor = tf.tidy(() => tf.browser.fromPixels(createCanvasFromMedia(imgElAngry)).expandDims()) as tf.Tensor4D
191+
192+
await expectAllTensorsReleased(async () => {
193+
await ageGenderNet.predictAgeAndGender(tensor)
194+
})
195+
196+
tensor.dispose()
197+
})
198+
199+
it('multiple batch size 1 tf.Tensor4Ds', async () => {
200+
const tensors = [imgElAngry, imgElAngry, imgElAngry]
201+
.map(el => tf.tidy(() => tf.browser.fromPixels(createCanvasFromMedia(el)).expandDims())) as tf.Tensor4D[]
202+
203+
await expectAllTensorsReleased(async () => {
204+
await ageGenderNet.predictAgeAndGender(tensors)
205+
})
206+
207+
tensors.forEach(t => t.dispose())
208+
})
209+
210+
})
211+
})
212+
213+
})
214+

test/tests/faceExpressionNet/faceExpressionNet.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describeWithBackend('faceExpressionNet', () => {
4141
expect(resultSurprised.surprised).toBeGreaterThan(0.95)
4242
})
4343

44-
it('computes face landmarks for batch of tf.Tensor3D', async () => {
44+
it('computes face expressions for batch of tf.Tensor3D', async () => {
4545
const inputs = [imgElAngry, imgElSurprised].map(el => tf.browser.fromPixels(createCanvasFromMedia(el)))
4646

4747
const results = await faceExpressionNet.predictExpressions(inputs) as FaceExpressions[]
@@ -55,7 +55,7 @@ describeWithBackend('faceExpressionNet', () => {
5555
expect(resultSurprised.surprised).toBeGreaterThan(0.95)
5656
})
5757

58-
it('computes face landmarks for batch of mixed inputs', async () => {
58+
it('computes face expressions for batch of mixed inputs', async () => {
5959
const inputs = [imgElAngry, tf.browser.fromPixels(createCanvasFromMedia(imgElSurprised))]
6060

6161
const results = await faceExpressionNet.predictExpressions(inputs) as FaceExpressions[]

test/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as tf from '@tensorflow/tfjs-core';
22

33
import * as faceapi from '../src';
44
import { FaceRecognitionNet, IPoint, IRect, Mtcnn, TinyYolov2 } from '../src/';
5+
import { AgeGenderNet } from '../src/ageGenderNet/AgeGenderNet';
56
import { FaceDetection } from '../src/classes/FaceDetection';
67
import { FaceLandmarks } from '../src/classes/FaceLandmarks';
78
import { FaceExpressionNet } from '../src/faceExpressionNet/FaceExpressionNet';
@@ -114,6 +115,7 @@ export type InjectNetArgs = {
114115
faceRecognitionNet: FaceRecognitionNet
115116
mtcnn: Mtcnn
116117
faceExpressionNet: FaceExpressionNet
118+
ageGenderNet: AgeGenderNet
117119
tinyYolov2: TinyYolov2
118120
}
119121

@@ -129,6 +131,7 @@ export type DescribeWithNetsOptions = {
129131
withFaceRecognitionNet?: WithNetOptions
130132
withMtcnn?: WithNetOptions
131133
withFaceExpressionNet?: WithNetOptions
134+
withAgeGenderNet?: WithNetOptions
132135
withTinyYolov2?: WithTinyYolov2Options
133136
}
134137

@@ -176,6 +179,7 @@ export function describeWithNets(
176179
faceRecognitionNet,
177180
mtcnn,
178181
faceExpressionNet,
182+
ageGenderNet,
179183
tinyYolov2
180184
} = faceapi.nets
181185

@@ -192,6 +196,7 @@ export function describeWithNets(
192196
withFaceRecognitionNet,
193197
withMtcnn,
194198
withFaceExpressionNet,
199+
withAgeGenderNet,
195200
withTinyYolov2
196201
} = options
197202

@@ -244,6 +249,13 @@ export function describeWithNets(
244249
)
245250
}
246251

252+
if (withAgeGenderNet) {
253+
await initNet<AgeGenderNet>(
254+
ageGenderNet,
255+
!!withAgeGenderNet && !withAgeGenderNet.quantized && 'age_gender_model.weights'
256+
)
257+
}
258+
247259
if (withTinyYolov2 || withAllFacesTinyYolov2) {
248260
await initNet<TinyYolov2>(
249261
tinyYolov2,
@@ -273,6 +285,7 @@ export function describeWithNets(
273285
faceRecognitionNet,
274286
mtcnn,
275287
faceExpressionNet,
288+
ageGenderNet,
276289
tinyYolov2
277290
})
278291
})

0 commit comments

Comments
 (0)