Skip to content

Commit 34969ad

Browse files
authored
Fix memory leak and input sizing for SelfieSegmentation (#264)
* Dispose selfiesegmentation tensors Adds tensor disposal code taken from the official tensorflow examples. Without it, ml5.tf.memory().numTensors shows the number of tensors in memory when running the SelfieSegmentation model kept increasing indefinitely. This wasn't an issue with the BodyPix model. * Control selfieSegmentation input size Added a helper function in the imageUtilities that resizes an image from an HTML img/video element to the given dimensions, and returns it as a tensor. Used it in SelfieSegmentation to make sure the input is the right size and therefore the output received by the user is not of an unexpected size. * Return resized tensor immediately * Adapt to suggested comment format * Add more descriptive comment to the fix
1 parent db0b171 commit 34969ad

File tree

2 files changed

+67
-3
lines changed

2 files changed

+67
-3
lines changed

src/BodySegmentation/index.js

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import BODYPIX_PALETTE from "./BODYPIX_PALETTE";
1616
import { mediaReady } from "../utils/imageUtilities";
1717
import handleOptions from "../utils/handleOptions";
1818
import { handleModelName } from "../utils/handleOptions";
19+
import { resizeImageAsTensor } from "../utils/imageUtilities";
1920

2021
class BodySegmentation {
2122
/**
@@ -209,11 +210,27 @@ class BodySegmentation {
209210

210211
await mediaReady(image, false);
211212

213+
let inputForSegmenter = image;
214+
215+
// If using SelfieSegmentation, make sure the input is actually the size the user expects
216+
// this addresses a sizing bug not present in BodyPix.
217+
if (
218+
this.modelName == "SelfieSegmentation" &&
219+
(inputForSegmenter instanceof HTMLVideoElement ||
220+
inputForSegmenter instanceof HTMLImageElement)
221+
) {
222+
inputForSegmenter = resizeImageAsTensor(image, image.width, image.height);
223+
}
224+
212225
const segmentation = await this.model.segmentPeople(
213-
image,
226+
inputForSegmenter,
214227
this.runtimeConfig
215228
);
216229

230+
if (inputForSegmenter.dispose) {
231+
inputForSegmenter.dispose();
232+
}
233+
217234
const result = {};
218235

219236
// add array of raw values to output
@@ -259,6 +276,11 @@ class BodySegmentation {
259276
}
260277
result.mask = this.generateP5Image(result.maskImageData);
261278

279+
// Dispose segmentation tensors
280+
segmentation.map((singleSegmentation) =>
281+
singleSegmentation.mask.toTensor().then((tensor) => tensor.dispose())
282+
);
283+
262284
if (callback) callback(result);
263285
return result;
264286
}
@@ -310,11 +332,31 @@ class BodySegmentation {
310332
async detectLoop() {
311333
await mediaReady(this.detectMedia, false);
312334
while (!this.signalStop) {
335+
let inputForSegmenter = this.detectMedia;
336+
337+
// If using SelfieSegmentation, make sure the input is actually the size the user expects.
338+
// this addresses a sizing bug not present in BodyPix.
339+
if (
340+
this.modelName == "SelfieSegmentation" &&
341+
(inputForSegmenter instanceof HTMLVideoElement ||
342+
inputForSegmenter instanceof HTMLImageElement)
343+
) {
344+
inputForSegmenter = resizeImageAsTensor(
345+
this.detectMedia,
346+
this.detectMedia.width,
347+
this.detectMedia.height
348+
);
349+
}
350+
313351
const segmentation = await this.model.segmentPeople(
314-
this.detectMedia,
352+
inputForSegmenter,
315353
this.runtimeConfig
316354
);
317355

356+
if (inputForSegmenter.dispose) {
357+
inputForSegmenter.dispose();
358+
}
359+
318360
const result = {};
319361

320362
// add array of raw values to output
@@ -361,6 +403,11 @@ class BodySegmentation {
361403
}
362404
result.mask = this.generateP5Image(result.maskImageData);
363405

406+
// Dispose segmentation tensors
407+
segmentation.map((singleSegmentation) =>
408+
singleSegmentation.mask.toTensor().then((tensor) => tensor.dispose())
409+
);
410+
364411
this.detectCallback(result);
365412
await tf.nextFrame();
366413
}
@@ -408,7 +455,7 @@ class BodySegmentation {
408455
}
409456

410457
/**
411-
* Factory function that returns a Facemesh instance
458+
* Factory function that returns a bodySegmentation instance
412459
* @returns {Object} A new bodySegmentation instance
413460
*/
414461
const bodySegmentation = (...inputs) => {

src/utils/imageUtilities.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,22 @@ async function mediaReady(input, nextFrame) {
200200
}
201201
}
202202

203+
/**
204+
* Useful when models are ignoring the display size of the input HTML element
205+
* and instead using the intrinsic dimensions. This function will turn the
206+
* input into a tensor with the given dimensions and return it.
207+
* @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement} input
208+
* @param {number} width
209+
* @param {number} height
210+
* @return {tf.Tensor3D}
211+
*/
212+
function resizeImageAsTensor(input, width, height) {
213+
return tf.tidy(() => {
214+
const sourcePixelsTensor = tf.browser.fromPixels(input);
215+
return tf.image.resizeBilinear(sourcePixelsTensor, [height, width]).clipByValue(0, 255);
216+
});
217+
}
218+
203219
export {
204220
array3DToImage,
205221
processVideo,
@@ -209,4 +225,5 @@ export {
209225
flipImage,
210226
imgToPixelArray,
211227
mediaReady,
228+
resizeImageAsTensor
212229
};

0 commit comments

Comments
 (0)