Skip to content

Commit

Permalink
Add drawConfidenceMask() to our public API
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 582647409
  • Loading branch information
schmidt-sebastian authored and copybara-github committed Nov 15, 2023
1 parent e440a4d commit 47e2178
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 84 deletions.
6 changes: 1 addition & 5 deletions mediapipe/tasks/web/vision/core/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mediapipe_ts_library(
srcs = [
"drawing_utils.ts",
"drawing_utils_category_mask.ts",
"drawing_utils_confidence_mask.ts",
],
deps = [
":image",
Expand Down Expand Up @@ -149,11 +150,6 @@ mediapipe_ts_library(
],
)

mediapipe_ts_library(
name = "render_utils",
srcs = ["render_utils.ts"],
)

jasmine_node_test(
name = "vision_task_runner_test",
deps = [":vision_task_runner_test_lib"],
Expand Down
94 changes: 94 additions & 0 deletions mediapipe/tasks/web/vision/core/drawing_utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,100 @@ if (skip) {
drawingUtilsWebGL.close();
});

describe(
'drawConfidenceMask() blends background with foreground color', () => {
const foreground = new ImageData(
new Uint8ClampedArray(
[0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255]),
WIDTH, HEIGHT);
const background = [255, 255, 255, 255];
const expectedResult = new Uint8Array([
255, 255, 255, 255, 178, 178, 178, 255, 102, 102, 102, 255, 0, 0, 0,
255
]);

it('on 2D canvas', () => {
const confidenceMask = new MPMask(
[new Float32Array([0.0, 0.3, 0.6, 1.0])],
/* ownsWebGLTexture= */ false, canvas2D, shaderContext, WIDTH,
HEIGHT);

drawingUtils2D.drawConfidenceMask(
confidenceMask, background, foreground);

const actualResult = context2D.getImageData(0, 0, WIDTH, HEIGHT).data;
expect(actualResult)
.toEqual(new Uint8ClampedArray(expectedResult.buffer));
});

it('on WebGL canvas', () => {
const confidenceMask = new MPMask(
[new Float32Array(
[0.6, 1.0, 0.0, 0.3])], // Note: Vertically flipped
/* ownsWebGLTexture= */ false, canvasWebGL, shaderContext, WIDTH,
HEIGHT);

drawingUtilsWebGL.drawConfidenceMask(
confidenceMask, background, foreground);

const actualResult = new Uint8Array(WIDTH * HEIGHT * 4);
contextWebGL.readPixels(
0, 0, WIDTH, HEIGHT, contextWebGL.RGBA,
contextWebGL.UNSIGNED_BYTE, actualResult);
expect(actualResult).toEqual(expectedResult);
});
});


describe(
'drawConfidenceMask() blends background with foreground image', () => {
const foreground = new ImageData(
new Uint8ClampedArray(
[0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255]),
WIDTH, HEIGHT);
const background = new ImageData(
new Uint8ClampedArray([
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255
]),
WIDTH, HEIGHT);
const expectedResult = new Uint8Array([
255, 255, 255, 255, 178, 178, 178, 255, 102, 102, 102, 255, 0, 0, 0,
255
]);

it('on 2D canvas', () => {
const confidenceMask = new MPMask(
[new Float32Array([0.0, 0.3, 0.6, 1.0])],
/* ownsWebGLTexture= */ false, canvas2D, shaderContext, WIDTH,
HEIGHT);

drawingUtils2D.drawConfidenceMask(
confidenceMask, background, foreground);

const actualResult = context2D.getImageData(0, 0, WIDTH, HEIGHT).data;
expect(actualResult)
.toEqual(new Uint8ClampedArray(expectedResult.buffer));
});

it('on WebGL canvas', () => {
const confidenceMask = new MPMask(
[new Float32Array(
[0.6, 1.0, 0.0, 0.3])], // Note: Vertically flipped
/* ownsWebGLTexture= */ false, canvasWebGL, shaderContext, WIDTH,
HEIGHT);

drawingUtilsWebGL.drawConfidenceMask(
confidenceMask, background, foreground);

const actualResult = new Uint8Array(WIDTH * HEIGHT * 4);
contextWebGL.readPixels(
0, 0, WIDTH, HEIGHT, contextWebGL.RGBA,
contextWebGL.UNSIGNED_BYTE, actualResult);
expect(actualResult).toEqual(expectedResult);
});
});

describe('drawCategoryMask() ', () => {
const colors = [
[0, 0, 0, 255],
Expand Down
75 changes: 75 additions & 0 deletions mediapipe/tasks/web/vision/core/drawing_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import {BoundingBox} from '../../../../tasks/web/components/containers/bounding_box';
import {NormalizedLandmark} from '../../../../tasks/web/components/containers/landmark';
import {CategoryMaskShaderContext, CategoryToColorMap, RGBAColor} from '../../../../tasks/web/vision/core/drawing_utils_category_mask';
import {ConfidenceMaskShaderContext} from '../../../../tasks/web/vision/core/drawing_utils_confidence_mask';
import {MPImageShaderContext} from '../../../../tasks/web/vision/core/image_shader_context';
import {MPMask} from '../../../../tasks/web/vision/core/mask';
import {Connection} from '../../../../tasks/web/vision/core/types';
Expand Down Expand Up @@ -115,6 +116,7 @@ export {RGBAColor, CategoryToColorMap};
/** Helper class to visualize the result of a MediaPipe Vision task. */
export class DrawingUtils {
private categoryMaskShaderContext?: CategoryMaskShaderContext;
private confidenceMaskShaderContext?: ConfidenceMaskShaderContext;
private convertToWebGLTextureShaderContext?: MPImageShaderContext;
private readonly context2d?: CanvasRenderingContext2D|
OffscreenCanvasRenderingContext2D;
Expand Down Expand Up @@ -213,6 +215,13 @@ export class DrawingUtils {
return this.categoryMaskShaderContext;
}

private getConfidenceMaskShaderContext(): ConfidenceMaskShaderContext {
if (!this.confidenceMaskShaderContext) {
this.confidenceMaskShaderContext = new ConfidenceMaskShaderContext();
}
return this.confidenceMaskShaderContext;
}

/**
* Draws circles onto the provided landmarks.
*
Expand Down Expand Up @@ -422,13 +431,79 @@ export class DrawingUtils {
callback(mask.getAsWebGLTexture());
}
}

/** Draws a confidence mask on a WebGL2RenderingContext2D. */
private drawConfidenceMaskWebGL(
maskTexture: WebGLTexture, defaultTexture: RGBAColor|ImageSource,
overlayTexture: RGBAColor|ImageSource): void {
const gl = this.getWebGLRenderingContext();
const shaderContext = this.getConfidenceMaskShaderContext();
const defaultImage = Array.isArray(defaultTexture) ?
new ImageData(new Uint8ClampedArray(defaultTexture), 1, 1) :
defaultTexture;
const overlayImage = Array.isArray(overlayTexture) ?
new ImageData(new Uint8ClampedArray(overlayTexture), 1, 1) :
overlayTexture;

shaderContext.run(gl, /* flipTexturesVertically= */ true, () => {
shaderContext.bindAndUploadTextures(
defaultImage, overlayImage, maskTexture);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
gl.bindTexture(gl.TEXTURE_2D, null);
shaderContext.unbindTextures();
});
}

/** Draws a confidence mask on a CanvasRenderingContext2D. */
private drawConfidenceMask2D(
mask: MPMask, defaultTexture: RGBAColor|ImageSource,
overlayTexture: RGBAColor|ImageSource): void {
// Use the WebGL renderer to draw result on our internal canvas.
const gl = this.getWebGLRenderingContext();
this.runWithWebGLTexture(mask, texture => {
this.drawConfidenceMaskWebGL(texture, defaultTexture, overlayTexture);
// Draw the result on the user canvas.
const ctx = this.getCanvasRenderingContext();
ctx.drawImage(gl.canvas, 0, 0, ctx.canvas.width, ctx.canvas.height);
});
}

/**
* Blends two images using the provided confidence mask.
*
* If you are using an `ImageData` or `HTMLImageElement` as your data source
* and drawing the result onto a `WebGL2RenderingContext`, this method uploads
* the image data to the GPU. For still image input that gets re-used every
* frame, you can reduce the cost of re-uploading these images by passing a
* `HTMLCanvasElement` instead.
*
* @param mask A confidence mask that was returned from a segmentation task.
* @param defaultTexture An image or a four-channel color that will be used
* when confidence values are low.
* @param overlayTexture An image or four-channel color that will be used when
* confidence values are high.
*/
drawConfidenceMask(
mask: MPMask, defaultTexture: RGBAColor|ImageSource,
overlayTexture: RGBAColor|ImageSource): void {
if (this.context2d) {
this.drawConfidenceMask2D(mask, defaultTexture, overlayTexture);
} else {
this.drawConfidenceMaskWebGL(
mask.getAsWebGLTexture(), defaultTexture, overlayTexture);
}
}
/**
* Frees all WebGL resources held by this class.
* @export
*/
close(): void {
this.categoryMaskShaderContext?.close();
this.categoryMaskShaderContext = undefined;
this.confidenceMaskShaderContext?.close();
this.confidenceMaskShaderContext = undefined;
this.convertToWebGLTextureShaderContext?.close();
this.convertToWebGLTextureShaderContext = undefined;
}
Expand Down
125 changes: 125 additions & 0 deletions mediapipe/tasks/web/vision/core/drawing_utils_confidence_mask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Copyright 2023 The MediaPipe Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {assertNotNull, MPImageShaderContext} from '../../../../tasks/web/vision/core/image_shader_context';
import {ImageSource} from '../../../../web/graph_runner/graph_runner';

/**
* A fragment shader that blends a default image and overlay texture based on an
* input texture that contains confidence values.
*/
const FRAGMENT_SHADER = `
precision mediump float;
uniform sampler2D maskTexture;
uniform sampler2D defaultTexture;
uniform sampler2D overlayTexture;
varying vec2 vTex;
void main() {
float confidence = texture2D(maskTexture, vTex).r;
vec4 defaultColor = texture2D(defaultTexture, vTex);
vec4 overlayColor = texture2D(overlayTexture, vTex);
// Apply the alpha from the overlay and merge in the default color
overlayColor = mix(defaultColor, overlayColor, overlayColor.a);
gl_FragColor = mix(defaultColor, overlayColor, confidence);
}
`;

/** A drawing util class for confidence masks. */
export class ConfidenceMaskShaderContext extends MPImageShaderContext {
defaultTexture?: WebGLTexture;
overlayTexture?: WebGLTexture;
defaultTextureUniform?: WebGLUniformLocation;
overlayTextureUniform?: WebGLUniformLocation;
maskTextureUniform?: WebGLUniformLocation;

protected override getFragmentShader(): string {
return FRAGMENT_SHADER;
}

protected override setupTextures(): void {
const gl = this.gl!;
gl.activeTexture(gl.TEXTURE0);
this.defaultTexture = this.createTexture(gl);
gl.activeTexture(gl.TEXTURE1);
this.overlayTexture = this.createTexture(gl);
}

protected override setupShaders(): void {
super.setupShaders();
const gl = this.gl!;
this.defaultTextureUniform = assertNotNull(
gl.getUniformLocation(this.program!, 'defaultTexture'),
'Uniform location');
this.overlayTextureUniform = assertNotNull(
gl.getUniformLocation(this.program!, 'overlayTexture'),
'Uniform location');
this.maskTextureUniform = assertNotNull(
gl.getUniformLocation(this.program!, 'maskTexture'),
'Uniform location');
}

protected override configureUniforms(): void {
super.configureUniforms();
const gl = this.gl!;
gl.uniform1i(this.defaultTextureUniform!, 0);
gl.uniform1i(this.overlayTextureUniform!, 1);
gl.uniform1i(this.maskTextureUniform!, 2);
}

bindAndUploadTextures(
defaultImage: ImageSource, overlayImage: ImageSource,
confidenceMask: WebGLTexture) {
// TODO: We should avoid uploading textures from CPU to GPU
// if the textures haven't changed. This can lead to drastic performance
// slowdowns (~50ms per frame). Users can reduce the penalty by passing a
// canvas object instead of ImageData/HTMLImageElement.
const gl = this.gl!;
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.defaultTexture!);
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, defaultImage);

gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, this.overlayTexture!);
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, overlayImage);

gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, confidenceMask);
}

unbindTextures() {
const gl = this.gl!;
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, null);

gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, null);

gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, null);
}

override close(): void {
if (this.defaultTexture) {
this.gl!.deleteTexture(this.defaultTexture);
}
if (this.overlayTexture) {
this.gl!.deleteTexture(this.overlayTexture);
}
super.close();
}
}
Loading

0 comments on commit 47e2178

Please sign in to comment.