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 = [
|
VISION_LIBS = [
|
||||||
"//mediapipe/tasks/web/core:fileset_resolver",
|
"//mediapipe/tasks/web/core:fileset_resolver",
|
||||||
"//mediapipe/tasks/web/vision/core:drawing_utils",
|
"//mediapipe/tasks/web/vision/core:drawing_utils",
|
||||||
|
"//mediapipe/tasks/web/vision/core:image",
|
||||||
"//mediapipe/tasks/web/vision/face_detector",
|
"//mediapipe/tasks/web/vision/face_detector",
|
||||||
"//mediapipe/tasks/web/vision/face_landmarker",
|
"//mediapipe/tasks/web/vision/face_landmarker",
|
||||||
"//mediapipe/tasks/web/vision/face_stylizer",
|
"//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(
|
mediapipe_ts_library(
|
||||||
name = "vision_task_runner",
|
name = "vision_task_runner",
|
||||||
srcs = ["vision_task_runner.ts"],
|
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 {FilesetResolver as FilesetResolverImpl} from '../../../tasks/web/core/fileset_resolver';
|
||||||
import {DrawingUtils as DrawingUtilsImpl} from '../../../tasks/web/vision/core/drawing_utils';
|
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 {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 {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';
|
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.
|
// as exports.
|
||||||
const DrawingUtils = DrawingUtilsImpl;
|
const DrawingUtils = DrawingUtilsImpl;
|
||||||
const FilesetResolver = FilesetResolverImpl;
|
const FilesetResolver = FilesetResolverImpl;
|
||||||
|
const MPImage = MPImageImpl;
|
||||||
const FaceDetector = FaceDetectorImpl;
|
const FaceDetector = FaceDetectorImpl;
|
||||||
const FaceLandmarker = FaceLandmarkerImpl;
|
const FaceLandmarker = FaceLandmarkerImpl;
|
||||||
const FaceLandmarksConnections = FaceLandmarksConnectionsImpl;
|
const FaceLandmarksConnections = FaceLandmarksConnectionsImpl;
|
||||||
|
@ -46,6 +48,7 @@ const ObjectDetector = ObjectDetectorImpl;
|
||||||
export {
|
export {
|
||||||
DrawingUtils,
|
DrawingUtils,
|
||||||
FilesetResolver,
|
FilesetResolver,
|
||||||
|
MPImage,
|
||||||
FaceDetector,
|
FaceDetector,
|
||||||
FaceLandmarker,
|
FaceLandmarker,
|
||||||
FaceLandmarksConnections,
|
FaceLandmarksConnections,
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
export * from '../../../tasks/web/core/fileset_resolver';
|
export * from '../../../tasks/web/core/fileset_resolver';
|
||||||
export * from '../../../tasks/web/vision/core/drawing_utils';
|
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_detector/face_detector';
|
||||||
export * from '../../../tasks/web/vision/face_landmarker/face_landmarker';
|
export * from '../../../tasks/web/vision/face_landmarker/face_landmarker';
|
||||||
export * from '../../../tasks/web/vision/face_stylizer/face_stylizer';
|
export * from '../../../tasks/web/vision/face_stylizer/face_stylizer';
|
||||||
|
|
Loading…
Reference in New Issue
Block a user