Create MPImage type for Web
PiperOrigin-RevId: 525873209
This commit is contained in:
parent
e9bb849503
commit
9be748db00
|
@ -20,6 +20,7 @@ mediapipe_files(srcs = [
|
|||
VISION_LIBS = [
|
||||
"//mediapipe/tasks/web/core:fileset_resolver",
|
||||
"//mediapipe/tasks/web/vision/core:drawing_utils",
|
||||
"//mediapipe/tasks/web/vision/core:image",
|
||||
"//mediapipe/tasks/web/vision/face_detector",
|
||||
"//mediapipe/tasks/web/vision/face_landmarker",
|
||||
"//mediapipe/tasks/web/vision/face_stylizer",
|
||||
|
|
|
@ -39,6 +39,23 @@ mediapipe_ts_library(
|
|||
],
|
||||
)
|
||||
|
||||
mediapipe_ts_library(
|
||||
name = "image",
|
||||
srcs = ["image.ts"],
|
||||
)
|
||||
|
||||
mediapipe_ts_library(
|
||||
name = "image_test_lib",
|
||||
testonly = True,
|
||||
srcs = ["image.test.ts"],
|
||||
deps = [":image"],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "image_test",
|
||||
deps = [":image_test_lib"],
|
||||
)
|
||||
|
||||
mediapipe_ts_library(
|
||||
name = "vision_task_runner",
|
||||
srcs = ["vision_task_runner.ts"],
|
||||
|
|
287
mediapipe/tasks/web/vision/core/image.test.ts
Normal file
287
mediapipe/tasks/web/vision/core/image.test.ts
Normal file
|
@ -0,0 +1,287 @@
|
|||
/**
|
||||
* Copyright 2022 The MediaPipe Authors. All Rights Reserved.
|
||||
*
|
||||
* 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 'jasmine';
|
||||
|
||||
import {MPImage, MPImageShaderContext, MPImageStorageType} from './image';
|
||||
|
||||
const WIDTH = 2;
|
||||
const HEIGHT = 2;
|
||||
|
||||
const skip = typeof document === 'undefined';
|
||||
if (skip) {
|
||||
console.log('These tests must be run in a browser.');
|
||||
}
|
||||
|
||||
/** The image types supported by MPImage. */
|
||||
type ImageType = ImageData|ImageBitmap|WebGLTexture;
|
||||
|
||||
async function createTestData(
|
||||
gl: WebGL2RenderingContext, data: number[], width: number,
|
||||
height: number): Promise<[ImageData, ImageBitmap, WebGLTexture]> {
|
||||
const imageData = new ImageData(new Uint8ClampedArray(data), width, height);
|
||||
const imageBitmap = await createImageBitmap(imageData);
|
||||
const webGlTexture = gl.createTexture()!;
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, webGlTexture);
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageBitmap);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
|
||||
return [imageData, imageBitmap, webGlTexture];
|
||||
}
|
||||
|
||||
(skip ? xdescribe : describe)('MPImage', () => {
|
||||
let canvas: OffscreenCanvas;
|
||||
let gl: WebGL2RenderingContext;
|
||||
let imageData: ImageData;
|
||||
let imageBitmap: ImageBitmap;
|
||||
let webGlTexture: WebGLTexture;
|
||||
|
||||
beforeEach(async () => {
|
||||
canvas = new OffscreenCanvas(WIDTH, HEIGHT);
|
||||
gl = canvas.getContext('webgl2') as WebGL2RenderingContext;
|
||||
|
||||
const images = await createTestData(
|
||||
gl, [1, 0, 0, 255, 2, 0, 0, 255, 3, 0, 0, 255, 4, 0, 0, 255], WIDTH,
|
||||
HEIGHT);
|
||||
imageData = images[0];
|
||||
imageBitmap = images[1];
|
||||
webGlTexture = images[2];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
gl.deleteTexture(webGlTexture);
|
||||
imageBitmap.close();
|
||||
});
|
||||
|
||||
function readPixelsFromImageBitmap(imageBitmap: ImageBitmap): ImageData {
|
||||
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
|
||||
const ctx = canvas.getContext('2d') as OffscreenCanvasRenderingContext2D;
|
||||
ctx.drawImage(imageBitmap, 0, 0);
|
||||
return ctx.getImageData(0, 0, imageBitmap.width, imageBitmap.height);
|
||||
}
|
||||
|
||||
function readPixelsFromWebGLTexture(texture: WebGLTexture): Uint8Array {
|
||||
const pixels = new Uint8Array(WIDTH * WIDTH * 4);
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
|
||||
const framebuffer = gl.createFramebuffer()!;
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
||||
gl.framebufferTexture2D(
|
||||
gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
||||
gl.readPixels(0, 0, WIDTH, HEIGHT, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
gl.deleteFramebuffer(framebuffer);
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
|
||||
return pixels;
|
||||
}
|
||||
|
||||
function assertEquality(image: MPImage, expected: ImageType): void {
|
||||
if (expected instanceof ImageData) {
|
||||
const result = image.getImage(MPImageStorageType.IMAGE_DATA);
|
||||
expect(result).toEqual(expected);
|
||||
} else if (expected instanceof ImageBitmap) {
|
||||
const result = image.getImage(MPImageStorageType.IMAGE_BITMAP);
|
||||
expect(readPixelsFromImageBitmap(result))
|
||||
.toEqual(readPixelsFromImageBitmap(expected));
|
||||
} else { // WebGLTexture
|
||||
const result = image.getImage(MPImageStorageType.WEBGL_TEXTURE);
|
||||
expect(readPixelsFromWebGLTexture(result))
|
||||
.toEqual(readPixelsFromWebGLTexture(expected));
|
||||
}
|
||||
}
|
||||
|
||||
function createImage(
|
||||
shaderContext: MPImageShaderContext, input: ImageType, width: number,
|
||||
height: number): MPImage {
|
||||
return new MPImage(
|
||||
input instanceof ImageData ? input : null,
|
||||
input instanceof ImageBitmap ? input : null,
|
||||
input instanceof WebGLTexture ? input : null,
|
||||
/* ownsImageBitmap= */ false, /* ownsWebGLTexture= */ false, canvas,
|
||||
shaderContext, width, height);
|
||||
}
|
||||
|
||||
function runConversionTest(
|
||||
input: ImageType, output: ImageType, width = WIDTH,
|
||||
height = HEIGHT): void {
|
||||
const shaderContext = new MPImageShaderContext();
|
||||
const image = createImage(shaderContext, input, width, height);
|
||||
assertEquality(image, output);
|
||||
image.close();
|
||||
shaderContext.close();
|
||||
}
|
||||
|
||||
function runCloneTest(input: ImageType): void {
|
||||
const shaderContext = new MPImageShaderContext();
|
||||
const image = createImage(shaderContext, input, WIDTH, HEIGHT);
|
||||
const clone = image.clone();
|
||||
assertEquality(clone, input);
|
||||
clone.close();
|
||||
shaderContext.close();
|
||||
}
|
||||
|
||||
it(`converts from ImageData to ImageData`, () => {
|
||||
runConversionTest(imageData, imageData);
|
||||
});
|
||||
|
||||
it(`converts from ImageData to ImageBitmap`, () => {
|
||||
runConversionTest(imageData, imageBitmap);
|
||||
});
|
||||
|
||||
it(`converts from ImageData to WebGLTexture`, () => {
|
||||
runConversionTest(imageData, webGlTexture);
|
||||
});
|
||||
|
||||
it(`converts from ImageBitmap to ImageData`, () => {
|
||||
runConversionTest(imageBitmap, imageData);
|
||||
});
|
||||
|
||||
it(`converts from ImageBitmap to ImageBitmap`, () => {
|
||||
runConversionTest(imageBitmap, imageBitmap);
|
||||
});
|
||||
|
||||
it(`converts from ImageBitmap to WebGLTexture`, () => {
|
||||
runConversionTest(imageBitmap, webGlTexture);
|
||||
});
|
||||
|
||||
it(`converts from WebGLTexture to ImageData`, () => {
|
||||
runConversionTest(webGlTexture, imageData);
|
||||
});
|
||||
|
||||
it(`converts from WebGLTexture to ImageBitmap`, () => {
|
||||
runConversionTest(webGlTexture, imageBitmap);
|
||||
});
|
||||
|
||||
it(`converts from WebGLTexture to WebGLTexture`, () => {
|
||||
runConversionTest(webGlTexture, webGlTexture);
|
||||
});
|
||||
|
||||
it(`clones ImageData`, () => {
|
||||
runCloneTest(imageData);
|
||||
});
|
||||
|
||||
it(`clones ImageBitmap`, () => {
|
||||
runCloneTest(imageBitmap);
|
||||
});
|
||||
|
||||
it(`clones WebGLTextures`, () => {
|
||||
runCloneTest(webGlTexture);
|
||||
});
|
||||
|
||||
it(`does not flip textures twice`, async () => {
|
||||
const [imageData, , webGlTexture] = await createTestData(
|
||||
gl, [1, 0, 0, 255, 2, 0, 0, 255, 3, 0, 0, 255, 4, 0, 0, 255], WIDTH,
|
||||
HEIGHT);
|
||||
|
||||
const shaderContext = new MPImageShaderContext();
|
||||
const image = new MPImage(
|
||||
/* imageData= */ null, /* imageBitmap= */ null, webGlTexture,
|
||||
/* ownsImageBitmap= */ false, /* ownsWebGLTexture= */ false, canvas,
|
||||
shaderContext, WIDTH, HEIGHT);
|
||||
|
||||
const result = image.clone().getImage(MPImageStorageType.IMAGE_DATA);
|
||||
expect(result).toEqual(imageData);
|
||||
|
||||
gl.deleteTexture(webGlTexture);
|
||||
shaderContext.close();
|
||||
});
|
||||
|
||||
it(`can clone and get image`, async () => {
|
||||
const [imageData, , webGlTexture] = await createTestData(
|
||||
gl, [1, 0, 0, 255, 2, 0, 0, 255, 3, 0, 0, 255, 4, 0, 0, 255], WIDTH,
|
||||
HEIGHT);
|
||||
|
||||
const shaderContext = new MPImageShaderContext();
|
||||
const image = new MPImage(
|
||||
/* imageData= */ null, /* imageBitmap= */ null, webGlTexture,
|
||||
/* ownsImageBitmap= */ false, /* ownsWebGLTexture= */ false, canvas,
|
||||
shaderContext, WIDTH, HEIGHT);
|
||||
|
||||
// Verify that we can mix the different shader modes by running them out of
|
||||
// order.
|
||||
let result = image.getImage(MPImageStorageType.IMAGE_DATA);
|
||||
expect(result).toEqual(imageData);
|
||||
|
||||
result = image.clone().getImage(MPImageStorageType.IMAGE_DATA);
|
||||
expect(result).toEqual(imageData);
|
||||
|
||||
result = image.getImage(MPImageStorageType.IMAGE_DATA);
|
||||
expect(result).toEqual(imageData);
|
||||
|
||||
gl.deleteTexture(webGlTexture);
|
||||
shaderContext.close();
|
||||
});
|
||||
|
||||
it('supports hasType()', async () => {
|
||||
const shaderContext = new MPImageShaderContext();
|
||||
const image = createImage(shaderContext, imageData, WIDTH, HEIGHT);
|
||||
|
||||
expect(image.hasType(MPImageStorageType.IMAGE_DATA)).toBe(true);
|
||||
expect(image.hasType(MPImageStorageType.WEBGL_TEXTURE)).toBe(false);
|
||||
expect(image.hasType(MPImageStorageType.IMAGE_BITMAP)).toBe(false);
|
||||
|
||||
image.getImage(MPImageStorageType.WEBGL_TEXTURE);
|
||||
|
||||
expect(image.hasType(MPImageStorageType.IMAGE_DATA)).toBe(true);
|
||||
expect(image.hasType(MPImageStorageType.WEBGL_TEXTURE)).toBe(true);
|
||||
expect(image.hasType(MPImageStorageType.IMAGE_BITMAP)).toBe(false);
|
||||
|
||||
await image.getImage(MPImageStorageType.IMAGE_BITMAP);
|
||||
|
||||
expect(image.hasType(MPImageStorageType.IMAGE_DATA)).toBe(true);
|
||||
expect(image.hasType(MPImageStorageType.WEBGL_TEXTURE)).toBe(true);
|
||||
expect(image.hasType(MPImageStorageType.IMAGE_BITMAP)).toBe(true);
|
||||
|
||||
image.close();
|
||||
shaderContext.close();
|
||||
});
|
||||
|
||||
it('supports image that is smaller than the canvas', async () => {
|
||||
const [imageData, imageBitmap, webGlTexture] = await createTestData(
|
||||
gl, [1, 0, 0, 255, 2, 0, 0, 255], /* width= */ 2, /* height= */ 1);
|
||||
|
||||
runConversionTest(imageData, webGlTexture, /* width= */ 2, /* height= */ 1);
|
||||
runConversionTest(
|
||||
webGlTexture, imageBitmap, /* width= */ 2, /* height= */ 1);
|
||||
runConversionTest(imageBitmap, imageData, /* width= */ 2, /* height= */ 1);
|
||||
|
||||
gl.deleteTexture(webGlTexture);
|
||||
imageBitmap.close();
|
||||
});
|
||||
|
||||
it('supports image that is larger than the canvas', async () => {
|
||||
const [imageData, imageBitmap, webGlTexture] = await createTestData(
|
||||
gl,
|
||||
[
|
||||
1, 0, 0, 255, 2, 0, 0, 255, 3, 0, 0, 255,
|
||||
4, 0, 0, 255, 5, 0, 0, 255, 6, 0, 0, 255
|
||||
],
|
||||
/* width= */ 2, /* height= */ 3);
|
||||
|
||||
runConversionTest(imageData, webGlTexture, /* width= */ 2, /* height= */ 3);
|
||||
runConversionTest(
|
||||
webGlTexture, imageBitmap, /* width= */ 2, /* height= */ 3);
|
||||
runConversionTest(imageBitmap, imageData, /* width= */ 2, /* height= */ 3);
|
||||
|
||||
gl.deleteTexture(webGlTexture);
|
||||
imageBitmap.close();
|
||||
});
|
||||
});
|
595
mediapipe/tasks/web/vision/core/image.ts
Normal file
595
mediapipe/tasks/web/vision/core/image.ts
Normal file
|
@ -0,0 +1,595 @@
|
|||
/**
|
||||
* Copyright 2023 The MediaPipe Authors. All Rights Reserved.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/** The underlying type of the image. */
|
||||
export enum MPImageStorageType {
|
||||
/** Represents the native `ImageData` type. */
|
||||
IMAGE_DATA,
|
||||
/** Represents the native `ImageBitmap` type. */
|
||||
IMAGE_BITMAP,
|
||||
/** Represents the native `WebGLTexture` type. */
|
||||
WEBGL_TEXTURE
|
||||
}
|
||||
|
||||
type MPImageNativeContainer = ImageData|ImageBitmap|WebGLTexture;
|
||||
|
||||
const VERTEX_SHADER = `
|
||||
attribute vec2 aVertex;
|
||||
attribute vec2 aTex;
|
||||
varying vec2 vTex;
|
||||
void main(void) {
|
||||
gl_Position = vec4(aVertex, 0.0, 1.0);
|
||||
vTex = aTex;
|
||||
}`;
|
||||
|
||||
const FRAGMENT_SHADER = `
|
||||
precision mediump float;
|
||||
varying vec2 vTex;
|
||||
uniform sampler2D inputTexture;
|
||||
void main() {
|
||||
gl_FragColor = texture2D(inputTexture, vTex);
|
||||
}
|
||||
`;
|
||||
|
||||
function assertNotNull<T>(value: T|null, msg: string): T {
|
||||
if (value === null) {
|
||||
throw new Error(`Unable to obtain required WebGL resource: ${msg}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class that encapsulates the buffers used by `MPImageShaderContext`.
|
||||
*/
|
||||
class MPImageShaderBuffers {
|
||||
constructor(
|
||||
private readonly gl: WebGL2RenderingContext,
|
||||
private readonly vertexArrayObject: WebGLVertexArrayObject,
|
||||
private readonly vertexBuffer: WebGLBuffer,
|
||||
private readonly textureBuffer: WebGLBuffer) {}
|
||||
|
||||
bind() {
|
||||
this.gl.bindVertexArray(this.vertexArrayObject);
|
||||
}
|
||||
|
||||
unbind() {
|
||||
this.gl.bindVertexArray(null);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.gl.deleteVertexArray(this.vertexArrayObject);
|
||||
this.gl.deleteBuffer(this.vertexBuffer);
|
||||
this.gl.deleteBuffer(this.textureBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that encapsulates the shaders used by an MPImage. Can be re-used
|
||||
* across MPImages that use the same WebGL2Rendering context.
|
||||
*/
|
||||
export class MPImageShaderContext {
|
||||
private gl?: WebGL2RenderingContext;
|
||||
private framebuffer?: WebGLFramebuffer;
|
||||
private program?: WebGLProgram;
|
||||
private vertexShader?: WebGLShader;
|
||||
private fragmentShader?: WebGLShader;
|
||||
private aVertex?: GLint;
|
||||
private aTex?: GLint;
|
||||
|
||||
/**
|
||||
* The shader buffers used for passthrough renders that don't modify the
|
||||
* input texture.
|
||||
*/
|
||||
private shaderBuffersPassthrough?: MPImageShaderBuffers;
|
||||
|
||||
/**
|
||||
* The shader buffers used for passthrough renders that flip the input texture
|
||||
* vertically before conversion to a different type. This is used to flip the
|
||||
* texture to the expected orientation for drawing in the browser.
|
||||
*/
|
||||
private shaderBuffersFlipVertically?: MPImageShaderBuffers;
|
||||
|
||||
private compileShader(source: string, type: number): WebGLShader {
|
||||
const gl = this.gl!;
|
||||
const shader =
|
||||
assertNotNull(gl.createShader(type), 'Failed to create WebGL shader');
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
const info = gl.getShaderInfoLog(shader);
|
||||
throw new Error(`Could not compile WebGL shader: ${info}`);
|
||||
}
|
||||
gl.attachShader(this.program!, shader);
|
||||
return shader;
|
||||
}
|
||||
|
||||
private setupShaders(): void {
|
||||
const gl = this.gl!;
|
||||
this.program =
|
||||
assertNotNull(gl.createProgram()!, 'Failed to create WebGL program');
|
||||
|
||||
this.vertexShader = this.compileShader(VERTEX_SHADER, gl.VERTEX_SHADER);
|
||||
this.fragmentShader =
|
||||
this.compileShader(FRAGMENT_SHADER, gl.FRAGMENT_SHADER);
|
||||
|
||||
gl.linkProgram(this.program);
|
||||
const linked = gl.getProgramParameter(this.program, gl.LINK_STATUS);
|
||||
if (!linked) {
|
||||
const info = gl.getProgramInfoLog(this.program);
|
||||
throw new Error(`Error during program linking: ${info}`);
|
||||
}
|
||||
|
||||
this.aVertex = gl.getAttribLocation(this.program, 'aVertex');
|
||||
this.aTex = gl.getAttribLocation(this.program, 'aTex');
|
||||
}
|
||||
|
||||
private createBuffers(flipVertically: boolean): MPImageShaderBuffers {
|
||||
const gl = this.gl!;
|
||||
const vertexArrayObject =
|
||||
assertNotNull(gl.createVertexArray(), 'Failed to create vertex array');
|
||||
gl.bindVertexArray(vertexArrayObject);
|
||||
|
||||
const vertexBuffer =
|
||||
assertNotNull(gl.createBuffer(), 'Failed to create buffer');
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
||||
gl.enableVertexAttribArray(this.aVertex!);
|
||||
gl.vertexAttribPointer(this.aVertex!, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]),
|
||||
gl.STATIC_DRAW);
|
||||
|
||||
const textureBuffer =
|
||||
assertNotNull(gl.createBuffer(), 'Failed to create buffer');
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
|
||||
gl.enableVertexAttribArray(this.aTex!);
|
||||
gl.vertexAttribPointer(this.aTex!, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
const bufferData =
|
||||
flipVertically ? [0, 1, 0, 0, 1, 0, 1, 1] : [0, 0, 0, 1, 1, 1, 1, 0];
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER, new Float32Array(bufferData), gl.STATIC_DRAW);
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
||||
gl.bindVertexArray(null);
|
||||
|
||||
return new MPImageShaderBuffers(
|
||||
gl, vertexArrayObject, vertexBuffer, textureBuffer);
|
||||
}
|
||||
|
||||
private getShaderBuffers(flipVertically: boolean): MPImageShaderBuffers {
|
||||
if (flipVertically) {
|
||||
if (!this.shaderBuffersFlipVertically) {
|
||||
this.shaderBuffersFlipVertically =
|
||||
this.createBuffers(/* flipVertically= */ true);
|
||||
}
|
||||
return this.shaderBuffersFlipVertically;
|
||||
} else {
|
||||
if (!this.shaderBuffersPassthrough) {
|
||||
this.shaderBuffersPassthrough =
|
||||
this.createBuffers(/* flipVertically= */ false);
|
||||
}
|
||||
return this.shaderBuffersPassthrough;
|
||||
}
|
||||
}
|
||||
|
||||
private maybeInitGL(gl: WebGL2RenderingContext): void {
|
||||
if (!this.gl) {
|
||||
this.gl = gl;
|
||||
} else if (gl !== this.gl) {
|
||||
throw new Error('Cannot change GL context once initialized');
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs the callback using the shader. */
|
||||
run<T>(
|
||||
gl: WebGL2RenderingContext, flipVertically: boolean,
|
||||
callback: () => T): T {
|
||||
this.maybeInitGL(gl);
|
||||
|
||||
if (!this.program) {
|
||||
this.setupShaders();
|
||||
}
|
||||
|
||||
const shaderBuffers = this.getShaderBuffers(flipVertically);
|
||||
gl.useProgram(this.program!);
|
||||
shaderBuffers.bind();
|
||||
const result = callback();
|
||||
shaderBuffers.unbind();
|
||||
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Binds a framebuffer to the canvas. If the framebuffer does not yet exist,
|
||||
* creates it first. Binds the provided texture to the framebuffer.
|
||||
*/
|
||||
bindFramebuffer(gl: WebGL2RenderingContext, texture: WebGLTexture): void {
|
||||
this.maybeInitGL(gl);
|
||||
if (!this.framebuffer) {
|
||||
this.framebuffer =
|
||||
assertNotNull(gl.createFramebuffer(), 'Failed to create framebuffe.');
|
||||
}
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
||||
gl.framebufferTexture2D(
|
||||
gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
||||
}
|
||||
|
||||
unbindFramebuffer(): void {
|
||||
this.gl?.bindFramebuffer(this.gl.FRAMEBUFFER, null);
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.program) {
|
||||
const gl = this.gl!;
|
||||
gl.deleteProgram(this.program);
|
||||
gl.deleteShader(this.vertexShader!);
|
||||
gl.deleteShader(this.fragmentShader!);
|
||||
}
|
||||
if (this.framebuffer) {
|
||||
this.gl!.deleteFramebuffer(this.framebuffer);
|
||||
}
|
||||
if (this.shaderBuffersPassthrough) {
|
||||
this.shaderBuffersPassthrough.close();
|
||||
}
|
||||
if (this.shaderBuffersFlipVertically) {
|
||||
this.shaderBuffersFlipVertically.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The wrapper class for MediaPipe Image objects.
|
||||
*
|
||||
* Images are stored as `ImageData`, `ImageBitmap` or `WebGLTexture` objects.
|
||||
* You can convert the underlying type to any other type by passing the
|
||||
* desired type to `getImage()`. As type conversions can be expensive, it is
|
||||
* recommended to limit these conversions. You can verify what underlying
|
||||
* types are already available by invoking `hasType()`.
|
||||
*
|
||||
* Images that are returned from a MediaPipe Tasks are owned by by the
|
||||
* underlying C++ Task. If you need to extend the lifetime of these objects,
|
||||
* you can invoke the `clone()` method. To free up the resources obtained
|
||||
* during any clone or type conversion operation, it is important to invoke
|
||||
* `close()` on the `MPImage` instance.
|
||||
*
|
||||
* Converting to and from ImageBitmap requires that the MediaPipe task is
|
||||
* initialized with an `OffscreenCanvas`. As we require WebGL2 support, this
|
||||
* places some limitations on Browser support as outlined here:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas/getContext
|
||||
*/
|
||||
export class MPImage {
|
||||
private gl?: WebGL2RenderingContext;
|
||||
|
||||
/** @hideconstructor */
|
||||
constructor(
|
||||
private imageData: ImageData|null,
|
||||
private imageBitmap: ImageBitmap|null,
|
||||
private webGLTexture: WebGLTexture|null,
|
||||
private ownsImageBitmap: boolean,
|
||||
private ownsWebGLTexture: boolean,
|
||||
/** Returns the canvas element that the image is bound to. */
|
||||
readonly canvas: HTMLCanvasElement|OffscreenCanvas|undefined,
|
||||
private shaderContext: MPImageShaderContext|undefined,
|
||||
/** Returns the width of the image. */
|
||||
readonly width: number,
|
||||
/** Returns the height of the image. */
|
||||
readonly height: number,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns whether this `MPImage` stores the image in the desired format.
|
||||
* This method can be called to reduce expensive conversion before invoking
|
||||
* `getType()`.
|
||||
*/
|
||||
hasType(type: MPImageStorageType): boolean {
|
||||
if (type === MPImageStorageType.IMAGE_DATA) {
|
||||
return !!this.imageData;
|
||||
} else if (type === MPImageStorageType.IMAGE_BITMAP) {
|
||||
return !!this.imageBitmap;
|
||||
} else if (type === MPImageStorageType.WEBGL_TEXTURE) {
|
||||
return !!this.webGLTexture;
|
||||
} else {
|
||||
throw new Error(`Type is not supported: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying image as an `ImageData` object. Note that this
|
||||
* involves an expensive GPU to CPU transfer if the current image is only
|
||||
* available as an `ImageBitmap` or `WebGLTexture`.
|
||||
*
|
||||
* @return The current image as an ImageData object.
|
||||
*/
|
||||
getImage(type: MPImageStorageType.IMAGE_DATA): ImageData;
|
||||
/**
|
||||
* Returns the underlying image as an `ImageBitmap`. Note that
|
||||
* conversions to `ImageBitmap` are expensive, especially if the data
|
||||
* currently resides on CPU.
|
||||
*
|
||||
* Processing with `ImageBitmap`s requires that the MediaPipe Task was
|
||||
* initialized with an `OffscreenCanvas` with WebGL2 support. See
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas/getContext
|
||||
* for a list of supported platforms.
|
||||
*
|
||||
* @return The current image as an ImageBitmap object.
|
||||
*/
|
||||
getImage(type: MPImageStorageType.IMAGE_BITMAP): ImageBitmap;
|
||||
/**
|
||||
* Returns the underlying image as a `WebGLTexture` object. Note that this
|
||||
* involves a CPU to GPU transfer if the current image is only available as
|
||||
* an `ImageData` object. The returned texture is bound to the current
|
||||
* canvas (see `.canvas`).
|
||||
*
|
||||
* @return The current image as a WebGLTexture.
|
||||
*/
|
||||
getImage(type: MPImageStorageType.WEBGL_TEXTURE): WebGLTexture;
|
||||
getImage(type?: MPImageStorageType): MPImageNativeContainer {
|
||||
if (type === MPImageStorageType.IMAGE_DATA) {
|
||||
return this.convertToImageData();
|
||||
} else if (type === MPImageStorageType.IMAGE_BITMAP) {
|
||||
return this.convertToImageBitmap();
|
||||
} else if (type === MPImageStorageType.WEBGL_TEXTURE) {
|
||||
return this.convertToWebGLTexture();
|
||||
} else {
|
||||
throw new Error(`Type is not supported: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of the resources stored in this `MPImage`. You can invoke
|
||||
* this method to extend the lifetime of an image returned by a MediaPipe
|
||||
* Task. Note that performance critical applications should aim to only use
|
||||
* the `MPImage` within the MediaPipe Task callback so that copies can be
|
||||
* avoided.
|
||||
*/
|
||||
clone(): MPImage {
|
||||
// TODO: We might only want to clone one backing datastructure
|
||||
// even if multiple are defined.
|
||||
let destinationImageData: ImageData|null = null;
|
||||
let destinationImageBitmap: ImageBitmap|null = null;
|
||||
let destinationWebGLTexture: WebGLTexture|null = null;
|
||||
|
||||
if (this.imageData) {
|
||||
destinationImageData =
|
||||
new ImageData(this.imageData.data, this.width, this.height);
|
||||
}
|
||||
|
||||
if (this.webGLTexture) {
|
||||
const gl = this.getGL();
|
||||
const shaderContext = this.getShaderContext();
|
||||
|
||||
// Create a new texture and use it to back a framebuffer
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
destinationWebGLTexture =
|
||||
assertNotNull(gl.createTexture(), 'Failed to create texture');
|
||||
gl.bindTexture(gl.TEXTURE_2D, destinationWebGLTexture);
|
||||
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA,
|
||||
gl.UNSIGNED_BYTE, null);
|
||||
|
||||
shaderContext.bindFramebuffer(gl, destinationWebGLTexture);
|
||||
shaderContext.run(gl, /* flipVertically= */ false, () => {
|
||||
this.bindTexture(); // This activates gl.TEXTURE0
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
|
||||
this.unbindTexture();
|
||||
});
|
||||
shaderContext.unbindFramebuffer();
|
||||
|
||||
this.unbindTexture();
|
||||
}
|
||||
|
||||
if (this.imageBitmap) {
|
||||
this.convertToWebGLTexture();
|
||||
this.bindTexture();
|
||||
destinationImageBitmap = this.copyTextureToBitmap();
|
||||
this.unbindTexture();
|
||||
}
|
||||
|
||||
return new MPImage(
|
||||
destinationImageData, destinationImageBitmap, destinationWebGLTexture,
|
||||
!!destinationImageBitmap, !!destinationWebGLTexture, this.canvas,
|
||||
this.shaderContext, this.width, this.height);
|
||||
}
|
||||
|
||||
|
||||
private getOffscreenCanvas(): OffscreenCanvas {
|
||||
if (!(this.canvas instanceof OffscreenCanvas)) {
|
||||
throw new Error(
|
||||
'Conversion to ImageBitmap requires that the MediaPipe Tasks is ' +
|
||||
'initialized with an OffscreenCanvas');
|
||||
}
|
||||
return this.canvas;
|
||||
}
|
||||
|
||||
private getGL(): WebGL2RenderingContext {
|
||||
if (!this.canvas) {
|
||||
throw new Error(
|
||||
'Conversion to different image formats require that a canvas ' +
|
||||
'is passed when iniitializing the image.');
|
||||
}
|
||||
if (!this.gl) {
|
||||
this.gl = assertNotNull(
|
||||
this.canvas.getContext('webgl2') as WebGL2RenderingContext | null,
|
||||
'You cannot use a canvas that is already bound to a different ' +
|
||||
'type of rendering context.');
|
||||
}
|
||||
return this.gl;
|
||||
}
|
||||
|
||||
private getShaderContext(): MPImageShaderContext {
|
||||
if (!this.shaderContext) {
|
||||
this.shaderContext = new MPImageShaderContext();
|
||||
}
|
||||
return this.shaderContext;
|
||||
}
|
||||
|
||||
private convertToImageBitmap(): ImageBitmap {
|
||||
if (!this.imageBitmap) {
|
||||
if (!this.webGLTexture) {
|
||||
this.webGLTexture = this.convertToWebGLTexture();
|
||||
}
|
||||
this.imageBitmap = this.convertWebGLTextureToImageBitmap();
|
||||
this.ownsImageBitmap = true;
|
||||
}
|
||||
|
||||
return this.imageBitmap;
|
||||
}
|
||||
|
||||
private convertToImageData(): ImageData {
|
||||
if (!this.imageData) {
|
||||
const gl = this.getGL();
|
||||
const shaderContext = this.getShaderContext();
|
||||
const pixels = new Uint8Array(this.width * this.height * 4);
|
||||
|
||||
// Create texture if needed
|
||||
this.convertToWebGLTexture();
|
||||
|
||||
// Create a framebuffer from the texture and read back pixels
|
||||
shaderContext.bindFramebuffer(gl, this.webGLTexture!);
|
||||
gl.readPixels(
|
||||
0, 0, this.width, this.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
shaderContext.unbindFramebuffer();
|
||||
|
||||
this.imageData = new ImageData(
|
||||
new Uint8ClampedArray(pixels.buffer), this.width, this.height);
|
||||
}
|
||||
|
||||
return this.imageData;
|
||||
}
|
||||
|
||||
private convertToWebGLTexture(): WebGLTexture {
|
||||
if (!this.webGLTexture) {
|
||||
const gl = this.getGL();
|
||||
this.bindTexture();
|
||||
const source = (this.imageBitmap || this.imageData)!;
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
||||
this.unbindTexture();
|
||||
}
|
||||
|
||||
return this.webGLTexture!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the backing texture to the canvas. If the texture does not yet
|
||||
* exist, creates it first.
|
||||
*/
|
||||
private bindTexture() {
|
||||
const gl = this.getGL();
|
||||
|
||||
gl.viewport(0, 0, this.width, this.height);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
if (!this.webGLTexture) {
|
||||
this.webGLTexture =
|
||||
assertNotNull(gl.createTexture(), 'Failed to create texture');
|
||||
this.ownsWebGLTexture = true;
|
||||
}
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.webGLTexture);
|
||||
// TODO: Ideally, we would only set these once per texture and
|
||||
// not once every frame.
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
}
|
||||
|
||||
private unbindTexture(): void {
|
||||
this.gl!.bindTexture(this.gl!.TEXTURE_2D, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a shader to render the current texture and return it as an
|
||||
* ImageBitmap
|
||||
*/
|
||||
private copyTextureToBitmap(): ImageBitmap {
|
||||
const gl = this.getGL();
|
||||
const shaderContext = this.getShaderContext();
|
||||
|
||||
return shaderContext.run(gl, /* flipVertically= */ true, () => {
|
||||
return this.runWithResizedCanvas(() => {
|
||||
// Unbind any framebuffer that may be bound since
|
||||
// `transferToImageBitmap()` requires rendering into the display (null)
|
||||
// framebuffer.
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
|
||||
return this.getOffscreenCanvas().transferToImageBitmap();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private convertWebGLTextureToImageBitmap(): ImageBitmap {
|
||||
this.bindTexture();
|
||||
const result = this.copyTextureToBitmap();
|
||||
this.unbindTexture();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily resizes the underlying canvas to match the dimensions of the
|
||||
* image. Runs the provided callback on the resized canvas.
|
||||
*
|
||||
* Note that while resizing is an expensive operation, it allows us to use
|
||||
* the synchronous `transferToImageBitmap()` API.
|
||||
*/
|
||||
private runWithResizedCanvas<T>(callback: () => T): T {
|
||||
const canvas = this.canvas!;
|
||||
|
||||
if (canvas.width === this.width && canvas.height === this.height) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
const originalWidth = canvas.width;
|
||||
const originalHeight = canvas.height;
|
||||
canvas.width = this.width;
|
||||
canvas.height = this.height;
|
||||
|
||||
const result = callback();
|
||||
|
||||
canvas.width = originalWidth;
|
||||
canvas.height = originalHeight;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees up any resources owned by this `MPImage` instance.
|
||||
*
|
||||
* Note that this method does not free images that are owned by the C++
|
||||
* Task, as these are freed automatically once you leave the MediaPipe
|
||||
* callback. Additionally, some shared state is freed only once you invoke the
|
||||
* Task's `close()` method.
|
||||
*/
|
||||
close(): void {
|
||||
if (this.ownsImageBitmap) {
|
||||
this.imageBitmap!.close();
|
||||
}
|
||||
|
||||
if (!this.gl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ownsWebGLTexture) {
|
||||
this.gl.deleteTexture(this.webGLTexture!);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
import {FilesetResolver as FilesetResolverImpl} from '../../../tasks/web/core/fileset_resolver';
|
||||
import {DrawingUtils as DrawingUtilsImpl} from '../../../tasks/web/vision/core/drawing_utils';
|
||||
import {MPImage as MPImageImpl} from '../../../tasks/web/vision/core/image';
|
||||
import {FaceDetector as FaceDetectorImpl} from '../../../tasks/web/vision/face_detector/face_detector';
|
||||
import {FaceLandmarker as FaceLandmarkerImpl, FaceLandmarksConnections as FaceLandmarksConnectionsImpl} from '../../../tasks/web/vision/face_landmarker/face_landmarker';
|
||||
import {FaceStylizer as FaceStylizerImpl} from '../../../tasks/web/vision/face_stylizer/face_stylizer';
|
||||
|
@ -31,6 +32,7 @@ import {ObjectDetector as ObjectDetectorImpl} from '../../../tasks/web/vision/ob
|
|||
// as exports.
|
||||
const DrawingUtils = DrawingUtilsImpl;
|
||||
const FilesetResolver = FilesetResolverImpl;
|
||||
const MPImage = MPImageImpl;
|
||||
const FaceDetector = FaceDetectorImpl;
|
||||
const FaceLandmarker = FaceLandmarkerImpl;
|
||||
const FaceLandmarksConnections = FaceLandmarksConnectionsImpl;
|
||||
|
@ -46,6 +48,7 @@ const ObjectDetector = ObjectDetectorImpl;
|
|||
export {
|
||||
DrawingUtils,
|
||||
FilesetResolver,
|
||||
MPImage,
|
||||
FaceDetector,
|
||||
FaceLandmarker,
|
||||
FaceLandmarksConnections,
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
export * from '../../../tasks/web/core/fileset_resolver';
|
||||
export * from '../../../tasks/web/vision/core/drawing_utils';
|
||||
export * from '../../../tasks/web/vision/core/image';
|
||||
export * from '../../../tasks/web/vision/face_detector/face_detector';
|
||||
export * from '../../../tasks/web/vision/face_landmarker/face_landmarker';
|
||||
export * from '../../../tasks/web/vision/face_stylizer/face_stylizer';
|
||||
|
|
Loading…
Reference in New Issue
Block a user