@pexip/media-processor 22.2.0 → 22.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +34 -0
- package/api-docs/README.mdx +37 -0
- package/api-docs/interfaces/ProcessorEvent.mdx +1 -0
- package/api-docs/interfaces/ProcessorOptions.mdx +10 -9
- package/api-docs/interfaces/ProcessorUpdateOptions.mdx +10 -9
- package/api-docs/interfaces/SegmenterOptions.mdx +7 -6
- package/api-docs/interfaces/TensorMetadata.mdx +11 -0
- package/api-docs/type-aliases/TensorLayout.mdx +3 -0
- package/api-docs/variables/getTensorMetadata.mdx +14 -0
- package/dist/common/backends/canvas2d/postprocessor.d.ts +30 -0
- package/dist/common/backends/canvas2d/postprocessor.js +88 -0
- package/dist/common/backends/canvas2d/preprocessor.d.ts +56 -0
- package/dist/common/backends/canvas2d/preprocessor.js +139 -0
- package/dist/common/backends/webgpu/renderer.js +1 -0
- package/dist/common/backends/webgpu/webgpuUtils.d.ts +35 -0
- package/dist/common/backends/webgpu/webgpuUtils.js +84 -2
- package/dist/common/inferencer.d.ts +12 -3
- package/dist/common/inferencer.js +60 -14
- package/dist/common/tsconfig.tsbuildinfo +1 -1
- package/dist/common/types/processor.d.ts +1 -0
- package/dist/common/types/segmentation.d.ts +8 -0
- package/dist/common/utils.d.ts +12 -0
- package/dist/common/utils.js +43 -0
- package/dist/main/tsconfig.tsbuildinfo +1 -1
- package/dist/main/video/segmenter.d.ts +4 -2
- package/dist/main/video/segmenter.js +36 -11
- package/dist/main/video/video.js +10 -0
- package/dist/workers/mediaWorker.js +23 -23
- package/dist/workers/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 22.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [8b563f5]
|
|
8
|
+
- @pexip/utils@17.5.0
|
|
9
|
+
|
|
10
|
+
## 22.3.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 2f3c6b7: Add pure CPU processing capability
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- b666166: Shares one WebGPU adapter and device across the renderers, and
|
|
19
|
+
destroys the device when the processor is destroyed. One of each was requested
|
|
20
|
+
for every renderer, so each rebuild of it, on a change of the processing
|
|
21
|
+
dimensions or the backend or on a recovery from a lost context, left the
|
|
22
|
+
memory of the pipelines and the shader modules of the previous one allocated.
|
|
23
|
+
Rebuilding a renderer no longer waits for a device either.
|
|
24
|
+
- 53af4ef: Relinquishes the WebGL context of the processing canvases when they
|
|
25
|
+
are replaced. A context is bound to its canvas for life, so restarting the
|
|
26
|
+
video processing repeatedly, e.g. by changing the effect, could exhaust the
|
|
27
|
+
contexts available to the page, at which point the browser drops the oldest
|
|
28
|
+
ones and the video feed stops.
|
|
29
|
+
|
|
3
30
|
## 22.2.0
|
|
4
31
|
|
|
5
32
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -178,6 +178,40 @@ You can do it with chrome, [See here][profiling web audio].
|
|
|
178
178
|
- No `fetch` API in the `AudioWorkletGlobalScope`
|
|
179
179
|
- No `TextEncoder/Decoder` APIs in the `AudioWorkletGlobalScope`
|
|
180
180
|
|
|
181
|
+
## CPU pipeline (LiteRT + Canvas2D)
|
|
182
|
+
|
|
183
|
+
By default the media processor uses MediaPipe with a WebGL or WebGPU delegate
|
|
184
|
+
for GPU-accelerated background segmentation. On devices without GPU support, or
|
|
185
|
+
when you explicitly want a pure-CPU path, you can switch to the LiteRT inference
|
|
186
|
+
pipeline.
|
|
187
|
+
|
|
188
|
+
Provide `litertjsCoreBasePath` instead of (or in addition to) `basePath` when
|
|
189
|
+
opening the processor. Its presence is the switch that selects the LiteRT
|
|
190
|
+
pipeline:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
processor.open({
|
|
194
|
+
litertjsCoreBasePath: urls.litertjsCoreBasePath, // path to @litertjs/core/wasm/
|
|
195
|
+
imageSegmenterOptions: {
|
|
196
|
+
modelAsset: {
|
|
197
|
+
path: urls.selfieModelPath, // path to a .tflite selfie model
|
|
198
|
+
modelName: 'selfie',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The `@litertjs/core` WASM files are bundled inside `@pexip/media-assets` under
|
|
205
|
+
`@litertjs/core/wasm/`. Point `litertjsCoreBasePath` at this directory relative
|
|
206
|
+
to your web root.
|
|
207
|
+
|
|
208
|
+
### Supported models
|
|
209
|
+
|
|
210
|
+
| Pipeline | Models | Renderer |
|
|
211
|
+
| ------------------- | --------------------- | ------------- |
|
|
212
|
+
| MediaPipe (default) | `selfie`, `deeplabV3` | WebGL, WebGPU |
|
|
213
|
+
| LiteRT CPU | `selfie` | Canvas2D |
|
|
214
|
+
|
|
181
215
|
## References
|
|
182
216
|
|
|
183
217
|
- [MDN - Web Audio API][web audio api]
|
package/api-docs/README.mdx
CHANGED
|
@@ -178,6 +178,40 @@ You can do it with chrome, [See here][profiling web audio].
|
|
|
178
178
|
- No `fetch` API in the `AudioWorkletGlobalScope`
|
|
179
179
|
- No `TextEncoder/Decoder` APIs in the `AudioWorkletGlobalScope`
|
|
180
180
|
|
|
181
|
+
## CPU pipeline (LiteRT + Canvas2D)
|
|
182
|
+
|
|
183
|
+
By default the media processor uses MediaPipe with a WebGL or WebGPU delegate
|
|
184
|
+
for GPU-accelerated background segmentation. On devices without GPU support, or
|
|
185
|
+
when you explicitly want a pure-CPU path, you can switch to the LiteRT inference
|
|
186
|
+
pipeline.
|
|
187
|
+
|
|
188
|
+
Provide `litertjsCoreBasePath` instead of (or in addition to) `basePath` when
|
|
189
|
+
opening the processor. Its presence is the switch that selects the LiteRT
|
|
190
|
+
pipeline:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
processor.open({
|
|
194
|
+
litertjsCoreBasePath: urls.litertjsCoreBasePath, // path to @litertjs/core/wasm/
|
|
195
|
+
imageSegmenterOptions: {
|
|
196
|
+
modelAsset: {
|
|
197
|
+
path: urls.selfieModelPath, // path to a .tflite selfie model
|
|
198
|
+
modelName: 'selfie',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The `@litertjs/core` WASM files are bundled inside `@pexip/media-assets` under
|
|
205
|
+
`@litertjs/core/wasm/`. Point `litertjsCoreBasePath` at this directory relative
|
|
206
|
+
to your web root.
|
|
207
|
+
|
|
208
|
+
### Supported models
|
|
209
|
+
|
|
210
|
+
| Pipeline | Models | Renderer |
|
|
211
|
+
| ------------------- | --------------------- | ------------- |
|
|
212
|
+
| MediaPipe (default) | `selfie`, `deeplabV3` | WebGL, WebGPU |
|
|
213
|
+
| LiteRT CPU | `selfie` | Canvas2D |
|
|
214
|
+
|
|
181
215
|
## References
|
|
182
216
|
|
|
183
217
|
- [MDN - Web Audio API][web audio api]
|
|
@@ -234,6 +268,7 @@ A library for media analysis using Web APIs.
|
|
|
234
268
|
| [Stats](interfaces/Stats.mdx) | - |
|
|
235
269
|
| [Weights](interfaces/Weights.mdx) | - |
|
|
236
270
|
| [SelectionOptions](interfaces/SelectionOptions.mdx) | - |
|
|
271
|
+
| [TensorMetadata](interfaces/TensorMetadata.mdx) | - |
|
|
237
272
|
| [Benchmark](interfaces/Benchmark.mdx) | - |
|
|
238
273
|
| [Point](interfaces/Point.mdx) | Interface for Point consist of coordinates x and y |
|
|
239
274
|
| [Size](interfaces/Size.mdx) | - |
|
|
@@ -284,6 +319,7 @@ A library for media analysis using Web APIs.
|
|
|
284
319
|
| [IncludeMessageEventDataType](type-aliases/IncludeMessageEventDataType.mdx) | - |
|
|
285
320
|
| [TupleOf](type-aliases/TupleOf.mdx) | From https://github.com/Microsoft/TypeScript/issues/26223#issuecomment-674500430 |
|
|
286
321
|
| [OptionalKeys](type-aliases/OptionalKeys.mdx) | - |
|
|
322
|
+
| [TensorLayout](type-aliases/TensorLayout.mdx) | - |
|
|
287
323
|
| [Canvas](type-aliases/Canvas.mdx) | - |
|
|
288
324
|
| [Unsubscribe](type-aliases/Unsubscribe.mdx) | Unsubscribe the subscription |
|
|
289
325
|
| [AudioBufferFloats](type-aliases/AudioBufferFloats.mdx) | Same as [AudioBuffer](https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer) Or the return from [AnalyserNode.getFloatFrequencyData()](https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/getFloatFrequencyData) |
|
|
@@ -372,6 +408,7 @@ A library for media analysis using Web APIs.
|
|
|
372
408
|
| [cloneImageRecord](variables/cloneImageRecord.mdx) | - |
|
|
373
409
|
| [compareImageRecords](variables/compareImageRecords.mdx) | Compare provided ImageRecord |
|
|
374
410
|
| [getCanUseWebGL](variables/getCanUseWebGL.mdx) | - |
|
|
411
|
+
| [getTensorMetadata](variables/getTensorMetadata.mdx) | - |
|
|
375
412
|
| [urls](variables/urls.mdx) | - |
|
|
376
413
|
| [SILENT\_THRESHOLD](variables/SILENT_THRESHOLD.mdx) | Default silent threshold At least one LSB 16-bit data (compare is on absolute value). |
|
|
377
414
|
| [MONO\_THRESHOLD](variables/MONO_THRESHOLD.mdx) | Default mono detection threshold Data must be identical within one LSB 16-bit to be identified as mono. |
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
## Properties
|
|
6
6
|
|
|
7
|
-
| Property | Type | Inherited from |
|
|
8
|
-
| ------ | ------ | ------ |
|
|
9
|
-
| <a id="renderoptions"></a> `renderOptions?` | [`RendererOptions`](RendererOptions.mdx) | - |
|
|
10
|
-
| <a id="backgroundimage"></a> `backgroundImage?` | [`ImageRecord`](ImageRecord.mdx) | - |
|
|
11
|
-
| <a id="restart"></a> `restart?` | `boolean` | - |
|
|
12
|
-
| <a id="basepath"></a> `basePath?` | `string` | [`SegmenterOptions`](SegmenterOptions.mdx).[`basePath`](SegmenterOptions.mdx#basepath) |
|
|
13
|
-
| <a id="
|
|
14
|
-
| <a id="
|
|
15
|
-
| <a id="
|
|
7
|
+
| Property | Type | Description | Inherited from |
|
|
8
|
+
| ------ | ------ | ------ | ------ |
|
|
9
|
+
| <a id="renderoptions"></a> `renderOptions?` | [`RendererOptions`](RendererOptions.mdx) | - | - |
|
|
10
|
+
| <a id="backgroundimage"></a> `backgroundImage?` | [`ImageRecord`](ImageRecord.mdx) | - | - |
|
|
11
|
+
| <a id="restart"></a> `restart?` | `boolean` | - | - |
|
|
12
|
+
| <a id="basepath"></a> `basePath?` | `string` | MediaPipe fileset base URL. | [`SegmenterOptions`](SegmenterOptions.mdx).[`basePath`](SegmenterOptions.mdx#basepath) |
|
|
13
|
+
| <a id="litertjscorebasepath"></a> `litertjsCoreBasePath?` | `string` | LiteRT WASM base URL. Providing this selects the CPU inference pipeline (LiteRT + Canvas2D) instead of the default MediaPipe GPU pipeline. | [`SegmenterOptions`](SegmenterOptions.mdx).[`litertjsCoreBasePath`](SegmenterOptions.mdx#litertjscorebasepath) |
|
|
14
|
+
| <a id="processingwidth"></a> `processingWidth?` | `number` | - | [`SegmenterOptions`](SegmenterOptions.mdx).[`processingWidth`](SegmenterOptions.mdx#processingwidth) |
|
|
15
|
+
| <a id="processingheight"></a> `processingHeight?` | `number` | - | [`SegmenterOptions`](SegmenterOptions.mdx).[`processingHeight`](SegmenterOptions.mdx#processingheight) |
|
|
16
|
+
| <a id="imagesegmenteroptions"></a> `imageSegmenterOptions?` | [`ImageSegmenterOptions`](ImageSegmenterOptions.mdx) | - | [`SegmenterOptions`](SegmenterOptions.mdx).[`imageSegmenterOptions`](SegmenterOptions.mdx#imagesegmenteroptions) |
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
## Properties
|
|
8
8
|
|
|
9
|
-
| Property | Type | Inherited from |
|
|
10
|
-
| ------ | ------ | ------ |
|
|
11
|
-
| <a id="backgroundimage"></a> `backgroundImage?` | [`ImageRecord`](ImageRecord.mdx) | [`ProcessorOptions`](ProcessorOptions.mdx).[`backgroundImage`](ProcessorOptions.mdx#backgroundimage) |
|
|
12
|
-
| <a id="restart"></a> `restart?` | `boolean` | [`ProcessorOptions`](ProcessorOptions.mdx).[`restart`](ProcessorOptions.mdx#restart) |
|
|
13
|
-
| <a id="basepath"></a> `basePath?` | `string` | [`SegmenterOptions`](SegmenterOptions.mdx).[`basePath`](SegmenterOptions.mdx#basepath) |
|
|
14
|
-
| <a id="
|
|
15
|
-
| <a id="
|
|
16
|
-
| <a id="
|
|
17
|
-
| <a id="
|
|
9
|
+
| Property | Type | Description | Inherited from |
|
|
10
|
+
| ------ | ------ | ------ | ------ |
|
|
11
|
+
| <a id="backgroundimage"></a> `backgroundImage?` | [`ImageRecord`](ImageRecord.mdx) | - | [`ProcessorOptions`](ProcessorOptions.mdx).[`backgroundImage`](ProcessorOptions.mdx#backgroundimage) |
|
|
12
|
+
| <a id="restart"></a> `restart?` | `boolean` | - | [`ProcessorOptions`](ProcessorOptions.mdx).[`restart`](ProcessorOptions.mdx#restart) |
|
|
13
|
+
| <a id="basepath"></a> `basePath?` | `string` | MediaPipe fileset base URL. | [`SegmenterOptions`](SegmenterOptions.mdx).[`basePath`](SegmenterOptions.mdx#basepath) |
|
|
14
|
+
| <a id="litertjscorebasepath"></a> `litertjsCoreBasePath?` | `string` | LiteRT WASM base URL. Providing this selects the CPU inference pipeline (LiteRT + Canvas2D) instead of the default MediaPipe GPU pipeline. | [`SegmenterOptions`](SegmenterOptions.mdx).[`litertjsCoreBasePath`](SegmenterOptions.mdx#litertjscorebasepath) |
|
|
15
|
+
| <a id="processingwidth"></a> `processingWidth?` | `number` | - | [`SegmenterOptions`](SegmenterOptions.mdx).[`processingWidth`](SegmenterOptions.mdx#processingwidth) |
|
|
16
|
+
| <a id="processingheight"></a> `processingHeight?` | `number` | - | [`SegmenterOptions`](SegmenterOptions.mdx).[`processingHeight`](SegmenterOptions.mdx#processingheight) |
|
|
17
|
+
| <a id="imagesegmenteroptions"></a> `imageSegmenterOptions?` | [`ImageSegmenterOptions`](ImageSegmenterOptions.mdx) | - | [`SegmenterOptions`](SegmenterOptions.mdx).[`imageSegmenterOptions`](SegmenterOptions.mdx#imagesegmenteroptions) |
|
|
18
|
+
| <a id="renderoptions"></a> `renderOptions?` | `Partial`\<[`RendererOptions`](RendererOptions.mdx)\> | - | - |
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
## Properties
|
|
6
6
|
|
|
7
|
-
| Property | Type |
|
|
8
|
-
| ------ | ------ |
|
|
9
|
-
| <a id="basepath"></a> `basePath?` | `string` |
|
|
10
|
-
| <a id="
|
|
11
|
-
| <a id="
|
|
12
|
-
| <a id="
|
|
7
|
+
| Property | Type | Description |
|
|
8
|
+
| ------ | ------ | ------ |
|
|
9
|
+
| <a id="basepath"></a> `basePath?` | `string` | MediaPipe fileset base URL. |
|
|
10
|
+
| <a id="litertjscorebasepath"></a> `litertjsCoreBasePath?` | `string` | LiteRT WASM base URL. Providing this selects the CPU inference pipeline (LiteRT + Canvas2D) instead of the default MediaPipe GPU pipeline. |
|
|
11
|
+
| <a id="processingwidth"></a> `processingWidth?` | `number` | - |
|
|
12
|
+
| <a id="processingheight"></a> `processingHeight?` | `number` | - |
|
|
13
|
+
| <a id="imagesegmenteroptions"></a> `imageSegmenterOptions?` | [`ImageSegmenterOptions`](ImageSegmenterOptions.mdx) | - |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## Properties
|
|
2
|
+
|
|
3
|
+
| Property | Type |
|
|
4
|
+
| ------ | ------ |
|
|
5
|
+
| <a id="name"></a> `name` | `string` |
|
|
6
|
+
| <a id="shape"></a> `shape` | readonly `number[]` |
|
|
7
|
+
| <a id="width"></a> `width` | `number` |
|
|
8
|
+
| <a id="height"></a> `height` | `number` |
|
|
9
|
+
| <a id="layout"></a> `layout` | [`TensorLayout`](../type-aliases/TensorLayout.mdx) |
|
|
10
|
+
| <a id="dtype"></a> `dtype` | `"float32" \| "int32" \| "uint8"` |
|
|
11
|
+
| <a id="channels"></a> `channels` | `number` |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
```ts
|
|
2
|
+
const getTensorMetadata: (details, supportedChannels?) => TensorMetadata;
|
|
3
|
+
```
|
|
4
|
+
|
|
5
|
+
## Parameters
|
|
6
|
+
|
|
7
|
+
| Parameter | Type |
|
|
8
|
+
| ------ | ------ |
|
|
9
|
+
| `details` | `TensorDetails` |
|
|
10
|
+
| `supportedChannels?` | readonly `number[]` |
|
|
11
|
+
|
|
12
|
+
## Returns
|
|
13
|
+
|
|
14
|
+
[`TensorMetadata`](../interfaces/TensorMetadata.mdx)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TypedArray, TensorDetails, Tensor } from '../../inferencer';
|
|
2
|
+
import type { TensorMetadata } from '../../utils';
|
|
3
|
+
/**
|
|
4
|
+
* Extract person confidence into an 8-bit mask.
|
|
5
|
+
*
|
|
6
|
+
* For two-channel outputs, background is category 0 and person is category 1.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* extractPersonConfidence(new Float32Array([0.1, 0.9]), mask, metadata);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare const extractPersonConfidence: (source: TypedArray, target: Uint8ClampedArray, metadata: TensorMetadata) => Uint8ClampedArray;
|
|
14
|
+
/**
|
|
15
|
+
* Create a reusable converter from LiteRT selfie output to a mask canvas.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const output = createSelfieOutputProcessor(details, 256, 144);
|
|
20
|
+
* const maskCanvas = await output.process(inferenceOutputs);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare const createSelfieOutputProcessor: (output: TensorDetails, createCanvas?: (width: number, height: number) => OffscreenCanvas, createImageData?: (width: number, height: number) => ImageData) => {
|
|
24
|
+
metadata: TensorMetadata;
|
|
25
|
+
process: (inferenceOutputs: Record<string, Tensor>) => Promise<OffscreenCanvas>;
|
|
26
|
+
release: () => void;
|
|
27
|
+
readonly canvas: OffscreenCanvas | undefined;
|
|
28
|
+
readonly maskBuffer: Uint8ClampedArray<ArrayBufferLike> | undefined;
|
|
29
|
+
readonly imageData: ImageData | undefined;
|
|
30
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { getTensorMetadata } from '../../utils';
|
|
2
|
+
import { createOffscreenCanvas, getCanvasRenderingContext2D, } from '../../canvasRenderUtils';
|
|
3
|
+
/**
|
|
4
|
+
* Extract person confidence into an 8-bit mask.
|
|
5
|
+
*
|
|
6
|
+
* For two-channel outputs, background is category 0 and person is category 1.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* extractPersonConfidence(new Float32Array([0.1, 0.9]), mask, metadata);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export const extractPersonConfidence = (source, target, metadata) => {
|
|
14
|
+
const pixelCount = metadata.width * metadata.height;
|
|
15
|
+
if (target.length !== pixelCount ||
|
|
16
|
+
source.length !== pixelCount * metadata.channels) {
|
|
17
|
+
throw new Error('Output tensor and mask buffer sizes do not match');
|
|
18
|
+
}
|
|
19
|
+
const isFloat = source instanceof Float32Array;
|
|
20
|
+
for (let pixelIndex = 0; pixelIndex < pixelCount; pixelIndex++) {
|
|
21
|
+
const sourceIndex = metadata.layout === 'nhwc'
|
|
22
|
+
? pixelIndex * metadata.channels + metadata.channels - 1
|
|
23
|
+
: (metadata.channels - 1) * pixelCount + pixelIndex;
|
|
24
|
+
const confidence = source[sourceIndex] ?? 0;
|
|
25
|
+
target[pixelIndex] = isFloat
|
|
26
|
+
? Math.round(Math.min(1, Math.max(0, confidence)) * 255)
|
|
27
|
+
: confidence;
|
|
28
|
+
}
|
|
29
|
+
return target;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Create a reusable converter from LiteRT selfie output to a mask canvas.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const output = createSelfieOutputProcessor(details, 256, 144);
|
|
37
|
+
* const maskCanvas = await output.process(inferenceOutputs);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export const createSelfieOutputProcessor = (output, createCanvas = (width, height) => createOffscreenCanvas(width, height), createImageData = (width, height) => new ImageData(width, height)) => {
|
|
41
|
+
const metadata = getTensorMetadata(output, [1, 2]);
|
|
42
|
+
let canvas = createCanvas(metadata.width, metadata.height);
|
|
43
|
+
let context = getCanvasRenderingContext2D(canvas);
|
|
44
|
+
let maskBuffer = new Uint8ClampedArray(metadata.width * metadata.height);
|
|
45
|
+
let imageData = createImageData(metadata.width, metadata.height);
|
|
46
|
+
let rgbaBuffer = imageData.data;
|
|
47
|
+
const process = async (inferenceOutputs) => {
|
|
48
|
+
if (!canvas || !context || !maskBuffer || !rgbaBuffer || !imageData) {
|
|
49
|
+
throw new Error('The output processor has been released');
|
|
50
|
+
}
|
|
51
|
+
const output = inferenceOutputs[metadata.name];
|
|
52
|
+
if (!output) {
|
|
53
|
+
throw new Error(`Missing model output: ${metadata.name}`);
|
|
54
|
+
}
|
|
55
|
+
const values = await output.data();
|
|
56
|
+
extractPersonConfidence(values, maskBuffer, metadata);
|
|
57
|
+
for (let pixelIndex = 0; pixelIndex < maskBuffer.length; pixelIndex++) {
|
|
58
|
+
rgbaBuffer[pixelIndex * 4 + 3] = maskBuffer[pixelIndex] ?? 0;
|
|
59
|
+
}
|
|
60
|
+
context.putImageData(imageData, 0, 0);
|
|
61
|
+
return canvas;
|
|
62
|
+
};
|
|
63
|
+
const release = () => {
|
|
64
|
+
context = undefined;
|
|
65
|
+
maskBuffer = undefined;
|
|
66
|
+
rgbaBuffer = undefined;
|
|
67
|
+
imageData = undefined;
|
|
68
|
+
if (canvas) {
|
|
69
|
+
canvas.width = 0;
|
|
70
|
+
canvas.height = 0;
|
|
71
|
+
}
|
|
72
|
+
canvas = undefined;
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
metadata,
|
|
76
|
+
process,
|
|
77
|
+
release,
|
|
78
|
+
get canvas() {
|
|
79
|
+
return canvas;
|
|
80
|
+
},
|
|
81
|
+
get maskBuffer() {
|
|
82
|
+
return maskBuffer;
|
|
83
|
+
},
|
|
84
|
+
get imageData() {
|
|
85
|
+
return imageData;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Tensor, type TensorDetails } from '../../inferencer';
|
|
2
|
+
import { type TensorLayout } from '../../utils';
|
|
3
|
+
export { getTensorMetadata } from '../../utils';
|
|
4
|
+
/**
|
|
5
|
+
* Convert packed RGBA pixels to normalized model RGB input.
|
|
6
|
+
*
|
|
7
|
+
* Float inputs use the selfie model metadata normalization `(value - 0) / 255`.
|
|
8
|
+
* Uint8 inputs retain their original channel values.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* convertRgbaToRgbTensor(
|
|
13
|
+
* new Uint8Array([255, 128, 0, 255]),
|
|
14
|
+
* new Float32Array(3),
|
|
15
|
+
* 'nhwc',
|
|
16
|
+
* );
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare const convertRgbaToRgbTensor: (rgba: Uint8Array, target: Float32Array | Uint8Array, layout: TensorLayout) => Float32Array | Uint8Array;
|
|
20
|
+
/**
|
|
21
|
+
* Calculate a centered crop that keeps model input and output masks aligned.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* getModelResizeCoordinates(1280, 720, 256, 256);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare const getModelResizeCoordinates: (sourceWidth: number, sourceHeight: number, modelWidth: number, modelHeight: number, processingWidth?: number, processingHeight?: number) => {
|
|
29
|
+
sx: number;
|
|
30
|
+
sy: number;
|
|
31
|
+
sw: number;
|
|
32
|
+
sh: number;
|
|
33
|
+
dx: number;
|
|
34
|
+
dy: number;
|
|
35
|
+
dw: number;
|
|
36
|
+
dh: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Create a reusable Canvas2D preprocessor for a LiteRT model input.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const preprocessor = createCanvas2dPreprocessor(inputDetails);
|
|
44
|
+
* const input = await preprocessor.process(videoFrame);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare const createCanvas2dPreprocessor: (details: TensorDetails, processingWidth?: number, processingHeight?: number) => {
|
|
48
|
+
metadata: import("../..").TensorMetadata;
|
|
49
|
+
process: (frame: VideoFrame) => Promise<{
|
|
50
|
+
[x: string]: Tensor;
|
|
51
|
+
}>;
|
|
52
|
+
release: () => void;
|
|
53
|
+
readonly canvas: OffscreenCanvas | undefined;
|
|
54
|
+
readonly pixelBuffer: Uint8Array<ArrayBufferLike> | undefined;
|
|
55
|
+
readonly tensorStorage: Float32Array<ArrayBuffer> | Uint8Array<ArrayBuffer>;
|
|
56
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Tensor } from '../../inferencer';
|
|
2
|
+
import { createOffscreenCanvas, getCanvasRenderingContext2D, } from '../../canvasRenderUtils';
|
|
3
|
+
import { getImageSize } from '../../canvasRenderUtils';
|
|
4
|
+
import { resize, getTensorMetadata } from '../../utils';
|
|
5
|
+
export { getTensorMetadata } from '../../utils';
|
|
6
|
+
/**
|
|
7
|
+
* Convert packed RGBA pixels to normalized model RGB input.
|
|
8
|
+
*
|
|
9
|
+
* Float inputs use the selfie model metadata normalization `(value - 0) / 255`.
|
|
10
|
+
* Uint8 inputs retain their original channel values.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* convertRgbaToRgbTensor(
|
|
15
|
+
* new Uint8Array([255, 128, 0, 255]),
|
|
16
|
+
* new Float32Array(3),
|
|
17
|
+
* 'nhwc',
|
|
18
|
+
* );
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const convertRgbaToRgbTensor = (rgba, target, layout) => {
|
|
22
|
+
const pixelCount = rgba.length / 4;
|
|
23
|
+
if (!Number.isInteger(pixelCount) || target.length !== pixelCount * 3) {
|
|
24
|
+
throw new Error('RGBA and RGB tensor buffer sizes do not match');
|
|
25
|
+
}
|
|
26
|
+
const normalize = target instanceof Float32Array;
|
|
27
|
+
for (let pixelIndex = 0; pixelIndex < pixelCount; pixelIndex++) {
|
|
28
|
+
const rgbaOffset = pixelIndex * 4;
|
|
29
|
+
for (let channelIndex = 0; channelIndex < 3; channelIndex++) {
|
|
30
|
+
const targetIndex = layout === 'nhwc'
|
|
31
|
+
? pixelIndex * 3 + channelIndex
|
|
32
|
+
: channelIndex * pixelCount + pixelIndex;
|
|
33
|
+
const value = rgba[rgbaOffset + channelIndex] ?? 0;
|
|
34
|
+
target[targetIndex] = normalize ? value / 255 : value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return target;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Calculate a centered crop that keeps model input and output masks aligned.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* getModelResizeCoordinates(1280, 720, 256, 256);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export const getModelResizeCoordinates = (sourceWidth, sourceHeight, modelWidth, modelHeight, processingWidth = modelWidth, processingHeight = modelHeight) => {
|
|
48
|
+
if (sourceWidth <= 0 ||
|
|
49
|
+
sourceHeight <= 0 ||
|
|
50
|
+
modelWidth <= 0 ||
|
|
51
|
+
modelHeight <= 0 ||
|
|
52
|
+
processingWidth <= 0 ||
|
|
53
|
+
processingHeight <= 0) {
|
|
54
|
+
throw new Error('Source, processing, and model dimensions must be positive');
|
|
55
|
+
}
|
|
56
|
+
const sourceCrop = resize(sourceWidth, sourceHeight, processingWidth, processingHeight);
|
|
57
|
+
const scale = Math.min(modelWidth / processingWidth, modelHeight / processingHeight);
|
|
58
|
+
const destinationWidth = processingWidth * scale;
|
|
59
|
+
const destinationHeight = processingHeight * scale;
|
|
60
|
+
return {
|
|
61
|
+
sx: sourceCrop.sx,
|
|
62
|
+
sy: sourceCrop.sy,
|
|
63
|
+
sw: sourceCrop.sw,
|
|
64
|
+
sh: sourceCrop.sh,
|
|
65
|
+
dx: (modelWidth - destinationWidth) / 2,
|
|
66
|
+
dy: (modelHeight - destinationHeight) / 2,
|
|
67
|
+
dw: destinationWidth,
|
|
68
|
+
dh: destinationHeight,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Create a reusable Canvas2D preprocessor for a LiteRT model input.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const preprocessor = createCanvas2dPreprocessor(inputDetails);
|
|
77
|
+
* const input = await preprocessor.process(videoFrame);
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export const createCanvas2dPreprocessor = (details, processingWidth, processingHeight) => {
|
|
81
|
+
const metadata = getTensorMetadata(details);
|
|
82
|
+
let canvas = createOffscreenCanvas(metadata.width, metadata.height);
|
|
83
|
+
let context = getCanvasRenderingContext2D(canvas, { alpha: false });
|
|
84
|
+
let pixelBuffer = new Uint8Array(metadata.width * metadata.height * 4);
|
|
85
|
+
let tensorStorage = metadata.dtype === 'float32'
|
|
86
|
+
? new Float32Array(metadata.width * metadata.height * 3)
|
|
87
|
+
: new Uint8Array(metadata.width * metadata.height * 3);
|
|
88
|
+
let tensor;
|
|
89
|
+
const process = async (frame) => {
|
|
90
|
+
if (!canvas || !context || !pixelBuffer) {
|
|
91
|
+
throw new Error('The preprocessor has been released');
|
|
92
|
+
}
|
|
93
|
+
const sourceSize = getImageSize(frame);
|
|
94
|
+
const coordinates = getModelResizeCoordinates(sourceSize.width, sourceSize.height, metadata.width, metadata.height, processingWidth ?? metadata.width, processingHeight ?? metadata.height);
|
|
95
|
+
context.clearRect(0, 0, metadata.width, metadata.height);
|
|
96
|
+
context.drawImage(frame, coordinates.sx, coordinates.sy, coordinates.sw, coordinates.sh, coordinates.dx, coordinates.dy, coordinates.dw, coordinates.dh);
|
|
97
|
+
const resizedFrame = new VideoFrame(canvas, {
|
|
98
|
+
// VideoFrame timestamp is in microseconds
|
|
99
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame/timestamp
|
|
100
|
+
timestamp: frame.timestamp ?? performance.now() * 1000,
|
|
101
|
+
});
|
|
102
|
+
try {
|
|
103
|
+
await resizedFrame.copyTo(pixelBuffer, { format: 'RGBA' });
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
resizedFrame.close();
|
|
107
|
+
}
|
|
108
|
+
convertRgbaToRgbTensor(pixelBuffer, tensorStorage, metadata.layout);
|
|
109
|
+
tensor?.delete();
|
|
110
|
+
tensor = new Tensor(tensorStorage, Array.from(metadata.shape));
|
|
111
|
+
return { [metadata.name]: tensor };
|
|
112
|
+
};
|
|
113
|
+
const release = () => {
|
|
114
|
+
tensor?.delete();
|
|
115
|
+
tensor = undefined;
|
|
116
|
+
tensorStorage = new Uint8Array();
|
|
117
|
+
pixelBuffer = undefined;
|
|
118
|
+
context = undefined;
|
|
119
|
+
if (canvas) {
|
|
120
|
+
canvas.width = 0;
|
|
121
|
+
canvas.height = 0;
|
|
122
|
+
}
|
|
123
|
+
canvas = undefined;
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
metadata,
|
|
127
|
+
process,
|
|
128
|
+
release,
|
|
129
|
+
get canvas() {
|
|
130
|
+
return canvas;
|
|
131
|
+
},
|
|
132
|
+
get pixelBuffer() {
|
|
133
|
+
return pixelBuffer;
|
|
134
|
+
},
|
|
135
|
+
get tensorStorage() {
|
|
136
|
+
return tensorStorage;
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
};
|
|
@@ -272,6 +272,7 @@ export const createRenderer = (adapter, renderEventHandlers, canvas = new Offscr
|
|
|
272
272
|
};
|
|
273
273
|
const release = () => {
|
|
274
274
|
lazyProps.release();
|
|
275
|
+
props.device = undefined;
|
|
275
276
|
props.deviceLost = false;
|
|
276
277
|
props.prevMask2 = undefined;
|
|
277
278
|
props.prevMask4 = undefined;
|
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
export declare const hasR16FloatTextureStorage: (adapter: GPUAdapter) => boolean;
|
|
2
2
|
export declare const hasF16: (adapter: GPUAdapter) => boolean;
|
|
3
|
+
export declare class GPUDeviceRequestInvalidatedError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Get the GPU adapter, requesting it only once.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const adapter = await getGPUAdapter();
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare const getGPUAdapter: () => Promise<GPUAdapter | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Get the GPU device, requesting it only once.
|
|
17
|
+
*
|
|
18
|
+
* A device owns the memory of the pipelines and the shader modules created from
|
|
19
|
+
* it, and neither of those can be freed on their own, so it is shared by every
|
|
20
|
+
* renderer instead of being requested per renderer.
|
|
21
|
+
*
|
|
22
|
+
* @param adapter - The adapter to request the device from
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const device = await getGPUDevice(adapter);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
3
29
|
export declare const getGPUDevice: (adapter: GPUAdapter) => Promise<GPUDevice>;
|
|
30
|
+
/**
|
|
31
|
+
* Destroy the shared GPU device.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* destroyGPUDevice();
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const destroyGPUDevice: () => void;
|
|
4
39
|
/**
|
|
5
40
|
* Check if provided format is supported
|
|
6
41
|
*
|