Skip to content

Commit 5f0b196

Browse files
added batch input example for face landmarks + fixed: return array if batch input with array of length 1
1 parent 27ec01e commit 5f0b196

File tree

7 files changed

+148
-5
lines changed

7 files changed

+148
-5
lines changed

examples/public/commons.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ function renderNavBar(navbarId, exampleUri) {
113113
{
114114
uri: 'detect_and_recognize_faces',
115115
name: 'Detect and Recognize Faces'
116+
},
117+
{
118+
uri: 'batch_face_landmarks',
119+
name: 'Batch Face Landmarks'
116120
}
117121
]
118122

@@ -152,9 +156,11 @@ function renderNavBar(navbarId, exampleUri) {
152156
menuContent.appendChild(li)
153157

154158
examples
155-
.filter(ex => ex.uri !== exampleUri)
156159
.forEach(ex => {
157160
const li = document.createElement('li')
161+
if (ex.uri === exampleUri) {
162+
li.style.background='#b0b0b0'
163+
}
158164
const a = document.createElement('a')
159165
a.classList.add('waves-effect', 'waves-light')
160166
a.href = ex.uri

examples/server.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ app.get('/detect_and_draw_faces', (req, res) => res.sendFile(path.join(viewsDir,
2424
app.get('/detect_and_draw_landmarks', (req, res) => res.sendFile(path.join(viewsDir, 'detectAndDrawLandmarks.html')))
2525
app.get('/face_alignment', (req, res) => res.sendFile(path.join(viewsDir, 'faceAlignment.html')))
2626
app.get('/detect_and_recognize_faces', (req, res) => res.sendFile(path.join(viewsDir, 'detectAndRecognizeFaces.html')))
27+
app.get('/batch_face_landmarks', (req, res) => res.sendFile(path.join(viewsDir, 'batchFaceLandmarks.html')))
28+
29+
2730

2831
app.post('/fetch_external_image', async (req, res) => {
2932
const { imageUrl } = req.body
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<script src="face-api.js"></script>
5+
<script src="commons.js"></script>
6+
<link rel="stylesheet" href="styles.css">
7+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/css/materialize.css">
8+
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
9+
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/js/materialize.min.js"></script>
10+
</head>
11+
<body>
12+
<div id="navbar"></div>
13+
<div class="center-content page-container">
14+
<div>
15+
<div class="progress" id="loader">
16+
<div class="indeterminate"></div>
17+
</div>
18+
<div class="row side-by-side">
19+
<div class="row">
20+
<label for="timeNoBatch">Time for processing each face seperately:</label>
21+
<input disabled value="-" id="timeNoBatch" type="text" class="bold"/>
22+
</div>
23+
<div class="row">
24+
<label for="timeBatch">Time for processing in Batch:</label>
25+
<input disabled value="-" id="timeBatch" type="text" class="bold"/>
26+
</div>
27+
</div>
28+
<div class="row side-by-side">
29+
<div>
30+
<label for="numImages">Num Images:</label>
31+
<input id="numImages" type="text" class="bold" value="40"/>
32+
</div>
33+
<button
34+
class="waves-effect waves-light btn"
35+
onclick="measureTimingsAndDisplay()"
36+
>
37+
Ok
38+
</button>
39+
</div>
40+
<div class="row side-by-side">
41+
<div class="center-content">
42+
<div id="faceContainer"></div>
43+
</div>
44+
</div>
45+
</div>
46+
</div>
47+
48+
<script>
49+
let images = []
50+
let landmarksByFace = []
51+
let numImages = 40
52+
53+
function onNumImagesChanged(e) {
54+
const val = parseInt(e.target.value) || 40
55+
numImages = Math.min(Math.max(val, 0), 40)
56+
e.target.value = numImages
57+
}
58+
59+
function displayTimeStats(timeNoBatch, timeBatch) {
60+
$('#timeNoBatch').val(`${timeNoBatch} ms`)
61+
$('#timeBatch').val(`${timeBatch} ms`)
62+
}
63+
64+
function drawLandmarkCanvas(img, landmarks) {
65+
const canvas = faceapi.createCanvasFromMedia(img)
66+
$('#faceContainer').append(canvas)
67+
faceapi.drawLandmarks(canvas, landmarks, { lineWidth: 2 , drawLines: true })
68+
}
69+
70+
async function runLandmarkDetection(useBatchInput) {
71+
const ts = Date.now()
72+
landmarksByFace = useBatchInput
73+
? await faceapi.detectLandmarks(images.slice(0, numImages))
74+
: await Promise.all(images.slice(0, numImages).map(img => faceapi.detectLandmarks(img)))
75+
const time = Date.now() - ts
76+
return time
77+
}
78+
79+
async function measureTimings() {
80+
const timeNoBatch = await runLandmarkDetection(false)
81+
const timeBatch = await runLandmarkDetection(true)
82+
return { timeNoBatch, timeBatch }
83+
}
84+
85+
async function measureTimingsAndDisplay() {
86+
const { timeNoBatch, timeBatch } = await measureTimings()
87+
displayTimeStats(timeNoBatch, timeBatch)
88+
$('#faceContainer').empty()
89+
landmarksByFace.forEach((landmarks, i) => drawLandmarkCanvas(images[i], landmarks))
90+
}
91+
92+
async function run() {
93+
await faceapi.loadFaceLandmarkModel('/')
94+
$('#loader').hide()
95+
const allImgUris = classes
96+
.map(clazz => Array.from(Array(5), (_, idx) => getFaceImageUri(clazz, idx + 1)))
97+
.reduce((flat, arr) => flat.concat(arr))
98+
99+
images = await Promise.all(allImgUris.map(
100+
async uri => faceapi.bufferToImage(await fetchImage(uri))
101+
))
102+
// warmup
103+
await measureTimings()
104+
// run
105+
measureTimingsAndDisplay()
106+
}
107+
108+
$(document).ready(function() {
109+
$('#numImages').on('change', onNumImagesChanged)
110+
renderNavBar('#navbar', 'batch_face_landmarks')
111+
run()
112+
})
113+
114+
</script>
115+
116+
</body>
117+
</html>

karma.conf.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ module.exports = function(config) {
2626
tsconfig: 'tsconfig.test.json'
2727
},
2828
browsers: ['Chrome'],
29-
browserNoActivityTimeout: 60000
29+
browserNoActivityTimeout: 60000,
30+
client: {
31+
jasmine: {
32+
timeoutInterval: 30000
33+
}
34+
}
3035
})
3136
}

src/NetInput.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ import { createCanvasFromMedia } from './utils';
99
export class NetInput {
1010
private _inputs: tf.Tensor3D[] = []
1111
private _isManaged: boolean = false
12+
private _isBatchInput: boolean = false
1213

1314
private _inputDimensions: number[][] = []
1415
private _paddings: Point[] = []
1516

16-
constructor(inputs: tf.Tensor4D | Array<TResolvedNetInput>) {
17+
constructor(
18+
inputs: tf.Tensor4D | Array<TResolvedNetInput>,
19+
isBatchInput: boolean = false
20+
) {
1721
if (isTensor4D(inputs)) {
1822
this._inputs = tf.unstack(inputs as tf.Tensor4D) as tf.Tensor3D[]
1923
}
@@ -30,6 +34,8 @@ export class NetInput {
3034
)
3135
})
3236
}
37+
38+
this._isBatchInput = this.batchSize > 1 || isBatchInput
3339
this._inputDimensions = this._inputs.map(t => t.shape)
3440
}
3541

@@ -41,6 +47,10 @@ export class NetInput {
4147
return this._isManaged
4248
}
4349

50+
public get isBatchInput(): boolean {
51+
return this._isBatchInput
52+
}
53+
4454
public get batchSize(): number {
4555
return this._inputs.length
4656
}

src/faceLandmarkNet/FaceLandmarkNet.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ export class FaceLandmarkNet {
128128

129129
landmarkTensors.forEach(t => t.dispose())
130130

131-
return landmarksForBatch.length === 1 ? landmarksForBatch[0] : landmarksForBatch
131+
return netInput.isBatchInput
132+
? landmarksForBatch
133+
: landmarksForBatch[0]
132134
}
133135
}

src/toNetInput.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,5 @@ export async function toNetInput(
7272
inputArray.map(input => isMediaElement(input) && awaitMediaLoaded(input))
7373
)
7474

75-
return afterCreate(new NetInput(inputArray))
75+
return afterCreate(new NetInput(inputArray, Array.isArray(inputs)))
7676
}

0 commit comments

Comments
 (0)