supervision 0.0.9000 → 0.1.0
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/LICENSE +21 -0
- package/README.md +23 -2
- package/dist/constants/media-renderer.d.ts +9 -0
- package/dist/constants/media-renderer.d.ts.map +1 -0
- package/dist/detections/browser-cold-detection-frame-store.d.ts +5 -0
- package/dist/detections/browser-cold-detection-frame-store.d.ts.map +1 -0
- package/dist/detections/chunked-detection-frame-source.d.ts +3 -0
- package/dist/detections/chunked-detection-frame-source.d.ts.map +1 -0
- package/dist/detections/offset-detection-frame-source.d.ts +10 -0
- package/dist/detections/offset-detection-frame-source.d.ts.map +1 -0
- package/dist/editing/mask-brush-editor.d.ts +47 -0
- package/dist/editing/mask-brush-editor.d.ts.map +1 -0
- package/dist/editing.d.ts +8 -0
- package/dist/editing.d.ts.map +1 -0
- package/dist/editing.js +265 -0
- package/dist/editing.js.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9419 -0
- package/dist/index.js.map +1 -0
- package/dist/mask-preparation.worker.js +823 -0
- package/dist/mask-preparation.worker.js.map +1 -0
- package/dist/media/decoded-video-sample-reader.d.ts +8 -0
- package/dist/media/decoded-video-sample-reader.d.ts.map +1 -0
- package/dist/media/media-metadata.d.ts +21 -0
- package/dist/media/media-metadata.d.ts.map +1 -0
- package/dist/media/media-normalization.d.ts +6 -0
- package/dist/media/media-normalization.d.ts.map +1 -0
- package/dist/media/media-preparation.d.ts +9 -0
- package/dist/media/media-preparation.d.ts.map +1 -0
- package/dist/media/media-probe.d.ts +3 -0
- package/dist/media/media-probe.d.ts.map +1 -0
- package/dist/media/media-source-state.d.ts +5 -0
- package/dist/media/media-source-state.d.ts.map +1 -0
- package/dist/media/media-source.d.ts +38 -0
- package/dist/media/media-source.d.ts.map +1 -0
- package/dist/media/media-stream-media-source.d.ts +27 -0
- package/dist/media/media-stream-media-source.d.ts.map +1 -0
- package/dist/media/mediabunny-media-source.d.ts +13 -0
- package/dist/media/mediabunny-media-source.d.ts.map +1 -0
- package/dist/media/static-image-media-source.d.ts +13 -0
- package/dist/media/static-image-media-source.d.ts.map +1 -0
- package/dist/playback/media-playback-controller.d.ts +24 -0
- package/dist/playback/media-playback-controller.d.ts.map +1 -0
- package/dist/render-preparation/default-render-preparation-worker.d.ts +8 -0
- package/dist/render-preparation/default-render-preparation-worker.d.ts.map +1 -0
- package/dist/render-preparation/embedded-mask-preparation-worker.d.ts +2 -0
- package/dist/render-preparation/embedded-mask-preparation-worker.d.ts.map +1 -0
- package/dist/render-preparation/mask-frame-artifact.d.ts +28 -0
- package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -0
- package/dist/render-preparation/mask-frame-compositor.d.ts +14 -0
- package/dist/render-preparation/mask-frame-compositor.d.ts.map +1 -0
- package/dist/render-preparation/mask-frame-preparer.d.ts +19 -0
- package/dist/render-preparation/mask-frame-preparer.d.ts.map +1 -0
- package/dist/render-preparation/mask-preparation-worker-count.d.ts +15 -0
- package/dist/render-preparation/mask-preparation-worker-count.d.ts.map +1 -0
- package/dist/render-preparation/mask-preparation-worker-protocol.d.ts +64 -0
- package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -0
- package/dist/render-preparation/mask-preparation.worker.d.ts +2 -0
- package/dist/render-preparation/mask-preparation.worker.d.ts.map +1 -0
- package/dist/render-preparation/prepared-render-window.d.ts +46 -0
- package/dist/render-preparation/prepared-render-window.d.ts.map +1 -0
- package/dist/render-preparation/prepared-window-timeline.d.ts +16 -0
- package/dist/render-preparation/prepared-window-timeline.d.ts.map +1 -0
- package/dist/renderers/media-renderer-core.d.ts +9 -0
- package/dist/renderers/media-renderer-core.d.ts.map +1 -0
- package/dist/renderers/media-renderer-scene.d.ts +77 -0
- package/dist/renderers/media-renderer-scene.d.ts.map +1 -0
- package/dist/renderers/media-renderer-state.d.ts +44 -0
- package/dist/renderers/media-renderer-state.d.ts.map +1 -0
- package/dist/renderers/media-renderer.d.ts +8 -0
- package/dist/renderers/media-renderer.d.ts.map +1 -0
- package/dist/renderers/pixi-annotation-overlay-layer.d.ts +25 -0
- package/dist/renderers/pixi-annotation-overlay-layer.d.ts.map +1 -0
- package/dist/renderers/pixi-box-layer.d.ts +28 -0
- package/dist/renderers/pixi-box-layer.d.ts.map +1 -0
- package/dist/renderers/pixi-focus-layer.d.ts +92 -0
- package/dist/renderers/pixi-focus-layer.d.ts.map +1 -0
- package/dist/renderers/pixi-id-mask-shader.d.ts +62 -0
- package/dist/renderers/pixi-id-mask-shader.d.ts.map +1 -0
- package/dist/renderers/pixi-interaction-layer.d.ts +63 -0
- package/dist/renderers/pixi-interaction-layer.d.ts.map +1 -0
- package/dist/renderers/pixi-interaction-presentation-layer.d.ts +84 -0
- package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -0
- package/dist/renderers/pixi-label-layer.d.ts +32 -0
- package/dist/renderers/pixi-label-layer.d.ts.map +1 -0
- package/dist/renderers/pixi-mask-brush-preview.d.ts +26 -0
- package/dist/renderers/pixi-mask-brush-preview.d.ts.map +1 -0
- package/dist/renderers/pixi-mask-layer.d.ts +98 -0
- package/dist/renderers/pixi-mask-layer.d.ts.map +1 -0
- package/dist/renderers/pixi-media-scene.d.ts +6 -0
- package/dist/renderers/pixi-media-scene.d.ts.map +1 -0
- package/dist/renderers/pixi-path.d.ts +15 -0
- package/dist/renderers/pixi-path.d.ts.map +1 -0
- package/dist/renderers/pixi-polygon-layer.d.ts +32 -0
- package/dist/renderers/pixi-polygon-layer.d.ts.map +1 -0
- package/dist/renderers/pixi-scene-fit.d.ts +14 -0
- package/dist/renderers/pixi-scene-fit.d.ts.map +1 -0
- package/dist/renderers/pixi-scene-layer-slot.d.ts +23 -0
- package/dist/renderers/pixi-scene-layer-slot.d.ts.map +1 -0
- package/dist/renderers/pixi-vector-layer.d.ts +24 -0
- package/dist/renderers/pixi-vector-layer.d.ts.map +1 -0
- package/dist/sessions/media-session-defaults.d.ts +21 -0
- package/dist/sessions/media-session-defaults.d.ts.map +1 -0
- package/dist/sessions/media-session-detections.d.ts +16 -0
- package/dist/sessions/media-session-detections.d.ts.map +1 -0
- package/dist/sessions/media-session-media.d.ts +19 -0
- package/dist/sessions/media-session-media.d.ts.map +1 -0
- package/dist/sessions/media-session-state.d.ts +12 -0
- package/dist/sessions/media-session-state.d.ts.map +1 -0
- package/dist/sessions/media-session.d.ts +10 -0
- package/dist/sessions/media-session.d.ts.map +1 -0
- package/dist/types/media-normalization.d.ts +119 -0
- package/dist/types/media-normalization.d.ts.map +1 -0
- package/dist/types/media-preparation.d.ts +30 -0
- package/dist/types/media-preparation.d.ts.map +1 -0
- package/dist/types/media-renderer.d.ts +115 -0
- package/dist/types/media-renderer.d.ts.map +1 -0
- package/dist/types/media-session.d.ts +242 -0
- package/dist/types/media-session.d.ts.map +1 -0
- package/dist/types/render-preparation.d.ts +164 -0
- package/dist/types/render-preparation.d.ts.map +1 -0
- package/dist/workers/worker-rpc-client.d.ts +15 -0
- package/dist/workers/worker-rpc-client.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/detections/array-detection-frame-source.d.ts +4 -0
- package/node_modules/supervision-js-core/dist/detections/array-detection-frame-source.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/detections/buffered-detection-timeline.d.ts +14 -0
- package/node_modules/supervision-js-core/dist/detections/buffered-detection-timeline.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/detections/cold-detection-frame-source.d.ts +6 -0
- package/node_modules/supervision-js-core/dist/detections/cold-detection-frame-source.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/detections/composite-detection-frame-source.d.ts +3 -0
- package/node_modules/supervision-js-core/dist/detections/composite-detection-frame-source.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/detections/editable-annotation-frame-session.d.ts +35 -0
- package/node_modules/supervision-js-core/dist/detections/editable-annotation-frame-session.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/detections/memory-cold-detection-frame-store.d.ts +3 -0
- package/node_modules/supervision-js-core/dist/detections/memory-cold-detection-frame-store.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/detections/writable-detection-frame-source.d.ts +8 -0
- package/node_modules/supervision-js-core/dist/detections/writable-detection-frame-source.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/index.d.ts +65 -0
- package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/index.js +4277 -0
- package/node_modules/supervision-js-core/dist/index.js.map +1 -0
- package/node_modules/supervision-js-core/dist/interactions/annotation-editing-engine.d.ts +3 -0
- package/node_modules/supervision-js-core/dist/interactions/annotation-editing-engine.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/interactions/annotation-handles.d.ts +13 -0
- package/node_modules/supervision-js-core/dist/interactions/annotation-handles.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/interactions/detection-picker.d.ts +7 -0
- package/node_modules/supervision-js-core/dist/interactions/detection-picker.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/interactions/viewport-controller.d.ts +6 -0
- package/node_modules/supervision-js-core/dist/interactions/viewport-controller.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/styles/box-style.d.ts +44 -0
- package/node_modules/supervision-js-core/dist/styles/box-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/styles/default-annotation-presentation.d.ts +20 -0
- package/node_modules/supervision-js-core/dist/styles/default-annotation-presentation.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/styles/focus-style.d.ts +37 -0
- package/node_modules/supervision-js-core/dist/styles/focus-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/styles/interaction-style.d.ts +64 -0
- package/node_modules/supervision-js-core/dist/styles/interaction-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/styles/keypoint-style.d.ts +19 -0
- package/node_modules/supervision-js-core/dist/styles/keypoint-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/styles/label-style.d.ts +61 -0
- package/node_modules/supervision-js-core/dist/styles/label-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/styles/mask-style.d.ts +55 -0
- package/node_modules/supervision-js-core/dist/styles/mask-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/styles/polygon-style.d.ts +15 -0
- package/node_modules/supervision-js-core/dist/styles/polygon-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/styles/polyline-style.d.ts +14 -0
- package/node_modules/supervision-js-core/dist/styles/polyline-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/styles/source-presentation.d.ts +22 -0
- package/node_modules/supervision-js-core/dist/styles/source-presentation.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/styles/style-value.d.ts +4 -0
- package/node_modules/supervision-js-core/dist/styles/style-value.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/box-style.d.ts +58 -0
- package/node_modules/supervision-js-core/dist/types/box-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +325 -0
- package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/detections.d.ts +183 -0
- package/node_modules/supervision-js-core/dist/types/detections.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/editing.d.ts +140 -0
- package/node_modules/supervision-js-core/dist/types/editing.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/focus-style.d.ts +36 -0
- package/node_modules/supervision-js-core/dist/types/focus-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/interaction-style.d.ts +44 -0
- package/node_modules/supervision-js-core/dist/types/interaction-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/interaction.d.ts +70 -0
- package/node_modules/supervision-js-core/dist/types/interaction.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/keypoint-style.d.ts +30 -0
- package/node_modules/supervision-js-core/dist/types/keypoint-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/label-style.d.ts +65 -0
- package/node_modules/supervision-js-core/dist/types/label-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/mask-style.d.ts +59 -0
- package/node_modules/supervision-js-core/dist/types/mask-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +204 -0
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/media.d.ts +42 -0
- package/node_modules/supervision-js-core/dist/types/media.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/polygon-style.d.ts +13 -0
- package/node_modules/supervision-js-core/dist/types/polygon-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/polyline-style.d.ts +12 -0
- package/node_modules/supervision-js-core/dist/types/polyline-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/session-lifecycle.d.ts +106 -0
- package/node_modules/supervision-js-core/dist/types/session-lifecycle.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/style.d.ts +28 -0
- package/node_modules/supervision-js-core/dist/types/style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/viewport.d.ts +19 -0
- package/node_modules/supervision-js-core/dist/types/viewport.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/utils/annotation-visibility.d.ts +5 -0
- package/node_modules/supervision-js-core/dist/utils/annotation-visibility.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/utils/color-palette.d.ts +12 -0
- package/node_modules/supervision-js-core/dist/utils/color-palette.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/utils/color.d.ts +4 -0
- package/node_modules/supervision-js-core/dist/utils/color.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/utils/detection-conversions.d.ts +24 -0
- package/node_modules/supervision-js-core/dist/utils/detection-conversions.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/utils/detection-frames.d.ts +16 -0
- package/node_modules/supervision-js-core/dist/utils/detection-frames.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/utils/detection-masks.d.ts +31 -0
- package/node_modules/supervision-js-core/dist/utils/detection-masks.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/utils/geometry.d.ts +11 -0
- package/node_modules/supervision-js-core/dist/utils/geometry.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts +18 -0
- package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/utils/mask-style.d.ts +4 -0
- package/node_modules/supervision-js-core/dist/utils/mask-style.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/utils/object.d.ts +2 -0
- package/node_modules/supervision-js-core/dist/utils/object.d.ts.map +1 -0
- package/node_modules/supervision-js-core/package.json +51 -0
- package/package.json +92 -16
- package/index.js +0 -0
|
@@ -0,0 +1,4277 @@
|
|
|
1
|
+
/** COCO-compatible keypoint visibility values. */
|
|
2
|
+
var KeypointVisibility;
|
|
3
|
+
(function (KeypointVisibility) {
|
|
4
|
+
KeypointVisibility[KeypointVisibility["NotLabeled"] = 0] = "NotLabeled";
|
|
5
|
+
KeypointVisibility[KeypointVisibility["Occluded"] = 1] = "Occluded";
|
|
6
|
+
KeypointVisibility[KeypointVisibility["Visible"] = 2] = "Visible";
|
|
7
|
+
})(KeypointVisibility || (KeypointVisibility = {}));
|
|
8
|
+
var DetectionMaskEncoding;
|
|
9
|
+
(function (DetectionMaskEncoding) {
|
|
10
|
+
DetectionMaskEncoding["CompressedRle"] = "compressedRle";
|
|
11
|
+
})(DetectionMaskEncoding || (DetectionMaskEncoding = {}));
|
|
12
|
+
|
|
13
|
+
var DetectionBufferStatus;
|
|
14
|
+
(function (DetectionBufferStatus) {
|
|
15
|
+
DetectionBufferStatus["Idle"] = "idle";
|
|
16
|
+
DetectionBufferStatus["Loading"] = "loading";
|
|
17
|
+
DetectionBufferStatus["Ready"] = "ready";
|
|
18
|
+
DetectionBufferStatus["Error"] = "error";
|
|
19
|
+
DetectionBufferStatus["Destroyed"] = "destroyed";
|
|
20
|
+
})(DetectionBufferStatus || (DetectionBufferStatus = {}));
|
|
21
|
+
/**
|
|
22
|
+
* How the renderer selects the active detection frame for a media timestamp.
|
|
23
|
+
*/
|
|
24
|
+
var DetectionFrameSelectionMode;
|
|
25
|
+
(function (DetectionFrameSelectionMode) {
|
|
26
|
+
/**
|
|
27
|
+
* Select the frame whose `[mediaTime, endTime)` interval contains the media
|
|
28
|
+
* time. This is the default for interval annotations and timestamped sources.
|
|
29
|
+
*/
|
|
30
|
+
DetectionFrameSelectionMode["Interval"] = "interval";
|
|
31
|
+
/**
|
|
32
|
+
* Select from a known inference frame grid using `frameIndex`, `frameRate`,
|
|
33
|
+
* and optional `frameIndexOriginTime`. This is useful when inference was run
|
|
34
|
+
* on normalized frames and playback should snap detections to that grid.
|
|
35
|
+
*/
|
|
36
|
+
DetectionFrameSelectionMode["NearestFrameIndex"] = "nearestFrameIndex";
|
|
37
|
+
})(DetectionFrameSelectionMode || (DetectionFrameSelectionMode = {}));
|
|
38
|
+
var DetectionFrameRetentionMode;
|
|
39
|
+
(function (DetectionFrameRetentionMode) {
|
|
40
|
+
/**
|
|
41
|
+
* Keep writable detections only in an in-memory store. Useful for ephemeral
|
|
42
|
+
* live streams where old predictions should disappear when evicted.
|
|
43
|
+
*/
|
|
44
|
+
DetectionFrameRetentionMode["MemoryOnly"] = "memoryOnly";
|
|
45
|
+
/**
|
|
46
|
+
* Persist every written detection frame. Useful for finite media where seek
|
|
47
|
+
* and replay should not require recomputing detections.
|
|
48
|
+
*/
|
|
49
|
+
DetectionFrameRetentionMode["PersistAll"] = "persistAll";
|
|
50
|
+
/**
|
|
51
|
+
* Persist only the most recent retention window. Useful for long-running
|
|
52
|
+
* streams where replay is bounded to a recent time horizon.
|
|
53
|
+
*/
|
|
54
|
+
DetectionFrameRetentionMode["PersistWindow"] = "persistWindow";
|
|
55
|
+
})(DetectionFrameRetentionMode || (DetectionFrameRetentionMode = {}));
|
|
56
|
+
|
|
57
|
+
function copySortedDetectionFrames(detectionFrames) {
|
|
58
|
+
validateDetectionFrames(detectionFrames ?? []);
|
|
59
|
+
return (detectionFrames ?? [])
|
|
60
|
+
.map((frame) => ({
|
|
61
|
+
detections: frame.detections.map((detection) => ({
|
|
62
|
+
...detection,
|
|
63
|
+
attributes: detection.attributes
|
|
64
|
+
? [...detection.attributes]
|
|
65
|
+
: undefined,
|
|
66
|
+
keypoints: detection.keypoints
|
|
67
|
+
? {
|
|
68
|
+
edges: detection.keypoints.edges.map((edge) => [edge[0], edge[1]]),
|
|
69
|
+
points: detection.keypoints.points.map((point) => ({ ...point })),
|
|
70
|
+
visibility: detection.keypoints.visibility
|
|
71
|
+
? [...detection.keypoints.visibility]
|
|
72
|
+
: undefined,
|
|
73
|
+
}
|
|
74
|
+
: undefined,
|
|
75
|
+
mask: detection.mask ? { ...detection.mask } : undefined,
|
|
76
|
+
metadata: detection.metadata
|
|
77
|
+
? copyDetectionMetadata(detection.metadata)
|
|
78
|
+
: undefined,
|
|
79
|
+
polygon: detection.polygon
|
|
80
|
+
? {
|
|
81
|
+
points: detection.polygon.points.map((point) => ({ ...point })),
|
|
82
|
+
}
|
|
83
|
+
: undefined,
|
|
84
|
+
polyline: detection.polyline
|
|
85
|
+
? {
|
|
86
|
+
points: detection.polyline.points.map((point) => ({ ...point })),
|
|
87
|
+
}
|
|
88
|
+
: undefined,
|
|
89
|
+
rect: detection.rect ? { ...detection.rect } : undefined,
|
|
90
|
+
})),
|
|
91
|
+
endTime: frame.endTime,
|
|
92
|
+
frameIndex: frame.frameIndex,
|
|
93
|
+
mediaTime: frame.mediaTime,
|
|
94
|
+
}))
|
|
95
|
+
.sort((left, right) => left.mediaTime - right.mediaTime);
|
|
96
|
+
}
|
|
97
|
+
function copyDetectionMetadata(metadata) {
|
|
98
|
+
const clone = globalThis.structuredClone;
|
|
99
|
+
if (clone) {
|
|
100
|
+
return clone(metadata);
|
|
101
|
+
}
|
|
102
|
+
return copyMetadataValue(metadata);
|
|
103
|
+
}
|
|
104
|
+
function copyMetadataValue(value) {
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
return value.map(copyMetadataValue);
|
|
107
|
+
}
|
|
108
|
+
if (value && typeof value === "object") {
|
|
109
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [
|
|
110
|
+
key,
|
|
111
|
+
copyMetadataValue(child),
|
|
112
|
+
]));
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
function validateDetectionFrames(detectionFrames) {
|
|
117
|
+
for (const [frameOffset, frame] of detectionFrames.entries()) {
|
|
118
|
+
validateNumber(frame.mediaTime, `frames[${frameOffset}].mediaTime`, {
|
|
119
|
+
min: 0,
|
|
120
|
+
});
|
|
121
|
+
if (frame.endTime !== undefined) {
|
|
122
|
+
validateNumber(frame.endTime, `frames[${frameOffset}].endTime`, {
|
|
123
|
+
exclusiveMin: frame.mediaTime,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (frame.frameIndex !== undefined) {
|
|
127
|
+
validateNumber(frame.frameIndex, `frames[${frameOffset}].frameIndex`, {
|
|
128
|
+
integer: true,
|
|
129
|
+
min: 0,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
for (const [detectionOffset, detection] of frame.detections.entries()) {
|
|
133
|
+
const detectionPath = `frames[${frameOffset}].detections[${detectionOffset}]`;
|
|
134
|
+
if (detection.confidence !== undefined) {
|
|
135
|
+
validateNumber(detection.confidence, `${detectionPath}.confidence`, {
|
|
136
|
+
max: 1,
|
|
137
|
+
min: 0,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (detection.zIndex !== undefined) {
|
|
141
|
+
validateNumber(detection.zIndex, `${detectionPath}.zIndex`);
|
|
142
|
+
}
|
|
143
|
+
if (detection.sourceId !== undefined &&
|
|
144
|
+
typeof detection.sourceId !== "string") {
|
|
145
|
+
throw new Error(`${detectionPath}.sourceId must be a string.`);
|
|
146
|
+
}
|
|
147
|
+
if (detection.sourceDetectionIndex !== undefined) {
|
|
148
|
+
validateNumber(detection.sourceDetectionIndex, `${detectionPath}.sourceDetectionIndex`, {
|
|
149
|
+
integer: true,
|
|
150
|
+
min: 0,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (detection.rect) {
|
|
154
|
+
validateNumber(detection.rect.x, `${detectionPath}.rect.x`);
|
|
155
|
+
validateNumber(detection.rect.y, `${detectionPath}.rect.y`);
|
|
156
|
+
validateNumber(detection.rect.width, `${detectionPath}.rect.width`, {
|
|
157
|
+
exclusiveMin: 0,
|
|
158
|
+
});
|
|
159
|
+
validateNumber(detection.rect.height, `${detectionPath}.rect.height`, {
|
|
160
|
+
exclusiveMin: 0,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
validatePoints(detection.polygon?.points, `${detectionPath}.polygon`, 3);
|
|
164
|
+
validatePoints(detection.polyline?.points, `${detectionPath}.polyline`, 2);
|
|
165
|
+
if (detection.keypoints) {
|
|
166
|
+
validatePoints(detection.keypoints.points, `${detectionPath}.keypoints`, 1);
|
|
167
|
+
if (detection.keypoints.visibility !== undefined &&
|
|
168
|
+
detection.keypoints.visibility.length !==
|
|
169
|
+
detection.keypoints.points.length) {
|
|
170
|
+
throw new Error(`${detectionPath}.keypoints.visibility must match points length.`);
|
|
171
|
+
}
|
|
172
|
+
for (const [edgeOffset, edge] of detection.keypoints.edges.entries()) {
|
|
173
|
+
const edgePath = `${detectionPath}.keypoints.edges[${edgeOffset}]`;
|
|
174
|
+
for (const [endpointOffset, endpoint] of edge.entries()) {
|
|
175
|
+
validateNumber(endpoint, `${edgePath}[${endpointOffset}]`, {
|
|
176
|
+
integer: true,
|
|
177
|
+
min: 0,
|
|
178
|
+
});
|
|
179
|
+
if (endpoint >= detection.keypoints.points.length) {
|
|
180
|
+
throw new Error(`${edgePath}[${endpointOffset}] is out of range.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (detection.mask) {
|
|
186
|
+
validateNumber(detection.mask.width, `${detectionPath}.mask.width`, {
|
|
187
|
+
integer: true,
|
|
188
|
+
exclusiveMin: 0,
|
|
189
|
+
});
|
|
190
|
+
validateNumber(detection.mask.height, `${detectionPath}.mask.height`, {
|
|
191
|
+
integer: true,
|
|
192
|
+
exclusiveMin: 0,
|
|
193
|
+
});
|
|
194
|
+
if (detection.mask.counts.length === 0) {
|
|
195
|
+
throw new Error(`${detectionPath}.mask.counts must not be empty.`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function validatePoints(points, path, minimumLength) {
|
|
202
|
+
if (!points) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (points.length < minimumLength) {
|
|
206
|
+
throw new Error(`${path}.points must contain at least ${minimumLength} points.`);
|
|
207
|
+
}
|
|
208
|
+
for (const [pointOffset, point] of points.entries()) {
|
|
209
|
+
validateNumber(point.x, `${path}.points[${pointOffset}].x`);
|
|
210
|
+
validateNumber(point.y, `${path}.points[${pointOffset}].y`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function validateNumber(value, path, options = {}) {
|
|
214
|
+
if (!Number.isFinite(value)) {
|
|
215
|
+
throw new Error(`${path} must be a finite number.`);
|
|
216
|
+
}
|
|
217
|
+
if (options.integer && !Number.isInteger(value)) {
|
|
218
|
+
throw new Error(`${path} must be an integer.`);
|
|
219
|
+
}
|
|
220
|
+
if (options.min !== undefined && value < options.min) {
|
|
221
|
+
throw new Error(`${path} must be greater than or equal to ${options.min}.`);
|
|
222
|
+
}
|
|
223
|
+
if (options.exclusiveMin !== undefined && value <= options.exclusiveMin) {
|
|
224
|
+
throw new Error(`${path} must be greater than ${options.exclusiveMin}.`);
|
|
225
|
+
}
|
|
226
|
+
if (options.max !== undefined && value > options.max) {
|
|
227
|
+
throw new Error(`${path} must be less than or equal to ${options.max}.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function filterDetectionFramesForRange(detectionFrames, startTime, endTime) {
|
|
231
|
+
return detectionFrames.filter((frame) => detectionFrameOverlapsRange(frame, startTime, endTime));
|
|
232
|
+
}
|
|
233
|
+
function selectDetectionFrame(detectionFrames, mediaTime, options = {}) {
|
|
234
|
+
if (options.selectionMode === DetectionFrameSelectionMode.NearestFrameIndex) {
|
|
235
|
+
const selection = selectNearestFrameIndexDetectionFrame(detectionFrames, mediaTime, options.frameRate, options.frameIndexOriginTime);
|
|
236
|
+
if (selection.isApplicable) {
|
|
237
|
+
return selection.frame;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return selectIntervalDetectionFrame(detectionFrames, mediaTime);
|
|
241
|
+
}
|
|
242
|
+
function decodeCompressedRleMask(mask) {
|
|
243
|
+
if (mask.encoding !== DetectionMaskEncoding.CompressedRle) {
|
|
244
|
+
throw new Error(`Unsupported detection mask encoding: ${mask.encoding}`);
|
|
245
|
+
}
|
|
246
|
+
const data = new Uint8Array(mask.width * mask.height);
|
|
247
|
+
const counts = decodeCompressedRleCounts(mask.counts);
|
|
248
|
+
let offset = 0;
|
|
249
|
+
for (let index = 0; index < counts.length; index += 1) {
|
|
250
|
+
const runLength = counts[index] ?? 0;
|
|
251
|
+
const isForeground = index % 2 === 1;
|
|
252
|
+
if (isForeground) {
|
|
253
|
+
for (let runOffset = 0; runOffset < runLength; runOffset += 1) {
|
|
254
|
+
const maskOffset = offset + runOffset;
|
|
255
|
+
const x = Math.floor(maskOffset / mask.height);
|
|
256
|
+
const y = maskOffset % mask.height;
|
|
257
|
+
const rowMajorOffset = y * mask.width + x;
|
|
258
|
+
if (rowMajorOffset < data.length) {
|
|
259
|
+
data[rowMajorOffset] = 1;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
offset += runLength;
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
data,
|
|
267
|
+
height: mask.height,
|
|
268
|
+
width: mask.width,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function selectIntervalDetectionFrame(detectionFrames, mediaTime) {
|
|
272
|
+
let selectedFrame;
|
|
273
|
+
let low = 0;
|
|
274
|
+
let high = detectionFrames.length - 1;
|
|
275
|
+
while (low <= high) {
|
|
276
|
+
const middle = Math.floor((low + high) / 2);
|
|
277
|
+
const frame = detectionFrames[middle];
|
|
278
|
+
if (frame.mediaTime <= mediaTime) {
|
|
279
|
+
selectedFrame = frame;
|
|
280
|
+
low = middle + 1;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
high = middle - 1;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return selectedFrame && isDetectionFrameActive(selectedFrame, mediaTime)
|
|
287
|
+
? selectedFrame
|
|
288
|
+
: undefined;
|
|
289
|
+
}
|
|
290
|
+
function selectNearestFrameIndexDetectionFrame(detectionFrames, mediaTime, frameRate, frameIndexOriginTime) {
|
|
291
|
+
if (!frameRate || !Number.isFinite(frameRate) || frameRate <= 0) {
|
|
292
|
+
return { frame: undefined, isApplicable: false };
|
|
293
|
+
}
|
|
294
|
+
const firstIndexedFrame = detectionFrames.find((frame) => frame.frameIndex !== undefined);
|
|
295
|
+
if (!firstIndexedFrame || firstIndexedFrame.frameIndex === undefined) {
|
|
296
|
+
return { frame: undefined, isApplicable: false };
|
|
297
|
+
}
|
|
298
|
+
const originTime = frameIndexOriginTime !== undefined
|
|
299
|
+
? frameIndexOriginTime
|
|
300
|
+
: firstIndexedFrame.mediaTime - firstIndexedFrame.frameIndex / frameRate;
|
|
301
|
+
const targetFrameIndex = Math.round((mediaTime - originTime) * frameRate);
|
|
302
|
+
let nearestFrame;
|
|
303
|
+
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
304
|
+
for (const frame of detectionFrames) {
|
|
305
|
+
if (frame.frameIndex === undefined) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const distance = Math.abs(frame.frameIndex - targetFrameIndex);
|
|
309
|
+
if (distance < nearestDistance ||
|
|
310
|
+
(distance === nearestDistance &&
|
|
311
|
+
nearestFrame?.frameIndex !== undefined &&
|
|
312
|
+
frame.frameIndex > nearestFrame.frameIndex)) {
|
|
313
|
+
nearestFrame = frame;
|
|
314
|
+
nearestDistance = distance;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
frame: nearestDistance <= 1 ? nearestFrame : undefined,
|
|
319
|
+
isApplicable: true,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function isDetectionFrameActive(frame, mediaTime) {
|
|
323
|
+
return (frame.mediaTime <= mediaTime &&
|
|
324
|
+
(frame.endTime === undefined || mediaTime < frame.endTime));
|
|
325
|
+
}
|
|
326
|
+
function detectionFrameOverlapsRange(frame, startTime, endTime) {
|
|
327
|
+
if (frame.endTime === undefined) {
|
|
328
|
+
return frame.mediaTime >= startTime && frame.mediaTime <= endTime;
|
|
329
|
+
}
|
|
330
|
+
return frame.mediaTime <= endTime && frame.endTime > startTime;
|
|
331
|
+
}
|
|
332
|
+
function decodeCompressedRleCounts(counts) {
|
|
333
|
+
const decoded = [];
|
|
334
|
+
let index = 0;
|
|
335
|
+
while (index < counts.length) {
|
|
336
|
+
let value = 0;
|
|
337
|
+
let shift = 0;
|
|
338
|
+
let charCode;
|
|
339
|
+
do {
|
|
340
|
+
charCode = counts.charCodeAt(index) - 48;
|
|
341
|
+
index += 1;
|
|
342
|
+
value |= (charCode & 0x1f) << shift;
|
|
343
|
+
shift += 5;
|
|
344
|
+
} while (charCode & 0x20);
|
|
345
|
+
if (charCode & 0x10) {
|
|
346
|
+
value |= -1 << shift;
|
|
347
|
+
}
|
|
348
|
+
if (decoded.length > 2) {
|
|
349
|
+
value += decoded[decoded.length - 2] ?? 0;
|
|
350
|
+
}
|
|
351
|
+
decoded.push(value);
|
|
352
|
+
}
|
|
353
|
+
return decoded;
|
|
354
|
+
}
|
|
355
|
+
function encodeCompressedRleCounts(counts) {
|
|
356
|
+
return counts
|
|
357
|
+
.map((count, index) => {
|
|
358
|
+
let value = index > 2 ? count - counts[index - 2] : count;
|
|
359
|
+
let encoded = "";
|
|
360
|
+
let more = true;
|
|
361
|
+
while (more) {
|
|
362
|
+
let charCode = value & 0x1f;
|
|
363
|
+
value >>= 5;
|
|
364
|
+
more = !((value === 0 && (charCode & 0x10) === 0) ||
|
|
365
|
+
(value === -1 && (charCode & 0x10) !== 0));
|
|
366
|
+
if (more) {
|
|
367
|
+
charCode |= 0x20;
|
|
368
|
+
}
|
|
369
|
+
encoded += String.fromCharCode(charCode + 48);
|
|
370
|
+
}
|
|
371
|
+
return encoded;
|
|
372
|
+
})
|
|
373
|
+
.join("");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function createArrayDetectionFrameSource(frames) {
|
|
377
|
+
const sortedFrames = copySortedDetectionFrames(frames);
|
|
378
|
+
return {
|
|
379
|
+
async loadFrames(startTime, endTime) {
|
|
380
|
+
return copySortedDetectionFrames(filterDetectionFramesForRange(sortedFrames, startTime, endTime));
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const DEFAULT_BUFFER_AHEAD_SECONDS = 5;
|
|
386
|
+
const DEFAULT_BUFFER_BEHIND_SECONDS = 0.5;
|
|
387
|
+
const bufferedFrameSnapshots = new WeakMap();
|
|
388
|
+
/**
|
|
389
|
+
* Returns the timeline-owned hot-buffer snapshot without copying it.
|
|
390
|
+
*
|
|
391
|
+
* This is an internal platform-adapter fast path. Public callers should use
|
|
392
|
+
* `getBufferedFrames()`, which preserves the existing defensive-copy contract.
|
|
393
|
+
*/
|
|
394
|
+
function getBufferedDetectionTimelineFrameSnapshot(timeline) {
|
|
395
|
+
return (bufferedFrameSnapshots.get(timeline)?.() ?? timeline.getBufferedFrames());
|
|
396
|
+
}
|
|
397
|
+
function createBufferedDetectionTimeline(options) {
|
|
398
|
+
const bufferAheadSeconds = options.bufferAheadSeconds ?? DEFAULT_BUFFER_AHEAD_SECONDS;
|
|
399
|
+
const bufferBehindSeconds = options.bufferBehindSeconds ?? DEFAULT_BUFFER_BEHIND_SECONDS;
|
|
400
|
+
const refreshIntervalSeconds = options.refreshIntervalSeconds === undefined
|
|
401
|
+
? null
|
|
402
|
+
: Math.max(0, options.refreshIntervalSeconds);
|
|
403
|
+
const playbackGate = options.playbackGate;
|
|
404
|
+
let buffer = [];
|
|
405
|
+
let state = createIdleDetectionBufferState();
|
|
406
|
+
let destroyed = false;
|
|
407
|
+
let loadId = 0;
|
|
408
|
+
let bufferedSourceVersion = null;
|
|
409
|
+
let bufferedVersionRange = null;
|
|
410
|
+
let timelineContext = {
|
|
411
|
+
duration: null,
|
|
412
|
+
loop: false,
|
|
413
|
+
};
|
|
414
|
+
let inFlight;
|
|
415
|
+
let incrementalRefresh;
|
|
416
|
+
let pendingPrefetch;
|
|
417
|
+
let prefetchPump;
|
|
418
|
+
const getSourceVersion = (ranges) => {
|
|
419
|
+
if (!ranges) {
|
|
420
|
+
return options.source.getVersion?.() ?? 0;
|
|
421
|
+
}
|
|
422
|
+
return ranges.reduce((version, range) => Math.max(version, options.source.getVersion?.(range) ?? 0), 0);
|
|
423
|
+
};
|
|
424
|
+
const isBufferFresh = () => bufferedVersionRange !== null &&
|
|
425
|
+
bufferedSourceVersion === getSourceVersion(getBufferedSourceRanges());
|
|
426
|
+
const getLoadRange = (mediaTime) => {
|
|
427
|
+
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
428
|
+
const startTime = comparableMediaTime - bufferBehindSeconds;
|
|
429
|
+
const endTime = comparableMediaTime + bufferAheadSeconds;
|
|
430
|
+
return createLoadPlan(startTime, endTime);
|
|
431
|
+
};
|
|
432
|
+
const loadWindow = (mediaTime) => {
|
|
433
|
+
const { endTime, sourceRanges, startTime } = getLoadRange(mediaTime);
|
|
434
|
+
const versionRange = { endTime, startTime };
|
|
435
|
+
const sourceVersion = getSourceVersion(sourceRanges);
|
|
436
|
+
if (inFlight &&
|
|
437
|
+
inFlight.sourceVersion === sourceVersion &&
|
|
438
|
+
rangeContains(inFlight.startTime, inFlight.endTime, startTime, endTime)) {
|
|
439
|
+
return inFlight.promise;
|
|
440
|
+
}
|
|
441
|
+
const currentLoadId = loadId + 1;
|
|
442
|
+
loadId = currentLoadId;
|
|
443
|
+
state = {
|
|
444
|
+
...state,
|
|
445
|
+
errorMessage: null,
|
|
446
|
+
requestedEndTime: endTime,
|
|
447
|
+
requestedStartTime: startTime,
|
|
448
|
+
status: DetectionBufferStatus.Loading,
|
|
449
|
+
};
|
|
450
|
+
const promise = Promise.all(sourceRanges.map((range) => options.source.loadFrames(range.startTime, range.endTime)))
|
|
451
|
+
.then((frameRanges) => {
|
|
452
|
+
if (destroyed || currentLoadId !== loadId) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const committedSourceVersion = getSourceVersion(sourceRanges);
|
|
456
|
+
const loadedBuffer = copySortedDetectionFrames(frameRanges.flat());
|
|
457
|
+
buffer =
|
|
458
|
+
bufferedSourceVersion !== null &&
|
|
459
|
+
bufferedSourceVersion === committedSourceVersion
|
|
460
|
+
? reuseBufferedFrameSnapshots(buffer, loadedBuffer)
|
|
461
|
+
: loadedBuffer;
|
|
462
|
+
bufferedVersionRange = versionRange;
|
|
463
|
+
bufferedSourceVersion = committedSourceVersion;
|
|
464
|
+
state = {
|
|
465
|
+
bufferEndTime: endTime,
|
|
466
|
+
bufferStartTime: startTime,
|
|
467
|
+
detectionCount: countDetections(buffer),
|
|
468
|
+
errorMessage: null,
|
|
469
|
+
frameCount: buffer.length,
|
|
470
|
+
requestedEndTime: endTime,
|
|
471
|
+
requestedStartTime: startTime,
|
|
472
|
+
status: DetectionBufferStatus.Ready,
|
|
473
|
+
};
|
|
474
|
+
})
|
|
475
|
+
.catch((error) => {
|
|
476
|
+
if (!destroyed && currentLoadId === loadId) {
|
|
477
|
+
state = {
|
|
478
|
+
...state,
|
|
479
|
+
errorMessage: getErrorMessage(error),
|
|
480
|
+
status: DetectionBufferStatus.Error,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
throw error;
|
|
484
|
+
})
|
|
485
|
+
.finally(() => {
|
|
486
|
+
if (inFlight?.id === currentLoadId) {
|
|
487
|
+
inFlight = undefined;
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
inFlight = {
|
|
491
|
+
endTime,
|
|
492
|
+
id: currentLoadId,
|
|
493
|
+
promise,
|
|
494
|
+
sourceVersion,
|
|
495
|
+
startTime,
|
|
496
|
+
};
|
|
497
|
+
return promise;
|
|
498
|
+
};
|
|
499
|
+
const isBuffered = (mediaTime) => {
|
|
500
|
+
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
501
|
+
return (isBufferFresh() &&
|
|
502
|
+
state.bufferStartTime !== null &&
|
|
503
|
+
state.bufferEndTime !== null &&
|
|
504
|
+
comparableMediaTime >= state.bufferStartTime &&
|
|
505
|
+
comparableMediaTime <= state.bufferEndTime);
|
|
506
|
+
};
|
|
507
|
+
const isInsideBufferedRange = (mediaTime) => {
|
|
508
|
+
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
509
|
+
return (bufferedVersionRange !== null &&
|
|
510
|
+
state.bufferStartTime !== null &&
|
|
511
|
+
state.bufferEndTime !== null &&
|
|
512
|
+
comparableMediaTime >= state.bufferStartTime &&
|
|
513
|
+
comparableMediaTime <= state.bufferEndTime);
|
|
514
|
+
};
|
|
515
|
+
const refreshBuffer = async (mediaTime) => {
|
|
516
|
+
if (isBuffered(mediaTime)) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (isInsideBufferedRange(mediaTime) &&
|
|
520
|
+
bufferedSourceVersion !== null &&
|
|
521
|
+
options.source.getChangesSince) {
|
|
522
|
+
if (!incrementalRefresh) {
|
|
523
|
+
incrementalRefresh = applyIncrementalChanges().finally(() => {
|
|
524
|
+
incrementalRefresh = undefined;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
await incrementalRefresh;
|
|
528
|
+
if (isBuffered(mediaTime)) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
return refreshBuffer(mediaTime);
|
|
532
|
+
}
|
|
533
|
+
await loadWindow(mediaTime);
|
|
534
|
+
};
|
|
535
|
+
const shouldPrefetch = (mediaTime) => {
|
|
536
|
+
if (!isBuffered(mediaTime)) {
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
if (shouldRefreshRollingWindow(mediaTime)) {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
if (state.bufferEndTime === null || bufferAheadSeconds <= 0) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
return (getComparableMediaTime(mediaTime) + bufferAheadSeconds / 2 >=
|
|
546
|
+
state.bufferEndTime);
|
|
547
|
+
};
|
|
548
|
+
const timeline = {
|
|
549
|
+
async prepare(mediaTime, prepareOptions) {
|
|
550
|
+
if (destroyed) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (shouldWaitForPlaybackGate(prepareOptions)) {
|
|
554
|
+
await waitForPlaybackGate(mediaTime, prepareOptions);
|
|
555
|
+
if (destroyed) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
await refreshBuffer(mediaTime);
|
|
560
|
+
},
|
|
561
|
+
prefetch(mediaTime) {
|
|
562
|
+
if (destroyed || !shouldPrefetch(mediaTime)) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
pendingPrefetch = { loadId, mediaTime };
|
|
566
|
+
pumpPrefetchQueue();
|
|
567
|
+
},
|
|
568
|
+
selectFrame(mediaTime) {
|
|
569
|
+
if (!isBuffered(mediaTime)) {
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
return selectDetectionFrame(buffer, getSourceMediaTime(mediaTime), options);
|
|
573
|
+
},
|
|
574
|
+
setTimelineContext(context) {
|
|
575
|
+
timelineContext = context;
|
|
576
|
+
bufferedSourceVersion = null;
|
|
577
|
+
bufferedVersionRange = null;
|
|
578
|
+
},
|
|
579
|
+
getBufferedFrames() {
|
|
580
|
+
return copySortedDetectionFrames(buffer);
|
|
581
|
+
},
|
|
582
|
+
getState() {
|
|
583
|
+
return { ...state };
|
|
584
|
+
},
|
|
585
|
+
destroy() {
|
|
586
|
+
if (destroyed) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
destroyed = true;
|
|
590
|
+
pendingPrefetch = undefined;
|
|
591
|
+
buffer = [];
|
|
592
|
+
bufferedSourceVersion = null;
|
|
593
|
+
bufferedVersionRange = null;
|
|
594
|
+
state = {
|
|
595
|
+
...state,
|
|
596
|
+
bufferEndTime: null,
|
|
597
|
+
bufferStartTime: null,
|
|
598
|
+
detectionCount: 0,
|
|
599
|
+
frameCount: 0,
|
|
600
|
+
status: DetectionBufferStatus.Destroyed,
|
|
601
|
+
};
|
|
602
|
+
options.source.destroy?.();
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
bufferedFrameSnapshots.set(timeline, () => buffer);
|
|
606
|
+
return timeline;
|
|
607
|
+
function pumpPrefetchQueue() {
|
|
608
|
+
if (destroyed || prefetchPump) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
prefetchPump = drainPrefetchQueue().finally(() => {
|
|
612
|
+
prefetchPump = undefined;
|
|
613
|
+
if (!destroyed && pendingPrefetch) {
|
|
614
|
+
pumpPrefetchQueue();
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
async function drainPrefetchQueue() {
|
|
619
|
+
while (!destroyed && pendingPrefetch) {
|
|
620
|
+
if (inFlight) {
|
|
621
|
+
await inFlight.promise.catch(() => undefined);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const request = pendingPrefetch;
|
|
625
|
+
pendingPrefetch = undefined;
|
|
626
|
+
if (request.loadId !== loadId) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const { mediaTime } = request;
|
|
630
|
+
if (!shouldPrefetch(mediaTime)) {
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
await (shouldRefreshRollingWindow(mediaTime)
|
|
634
|
+
? loadWindow(mediaTime)
|
|
635
|
+
: refreshBuffer(mediaTime)).catch(() => undefined);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function applyIncrementalChanges() {
|
|
639
|
+
if (destroyed ||
|
|
640
|
+
bufferedSourceVersion === null ||
|
|
641
|
+
!options.source.getChangesSince) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const sourceRanges = getBufferedSourceRanges();
|
|
645
|
+
const incrementalLoadId = loadId;
|
|
646
|
+
const incrementalVersionRange = bufferedVersionRange;
|
|
647
|
+
const changes = options.source.getChangesSince(bufferedSourceVersion, sourceRanges);
|
|
648
|
+
if (changes.requiresReload) {
|
|
649
|
+
bufferedSourceVersion = null;
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (changes.ranges.length === 0) {
|
|
653
|
+
bufferedSourceVersion = changes.version;
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const changedRanges = getOverlappingRanges(changes.ranges, sourceRanges);
|
|
657
|
+
if (changedRanges.length === 0) {
|
|
658
|
+
bufferedSourceVersion = changes.version;
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
const changedFrameRanges = await Promise.all(changedRanges.map((range) => options.source.loadFrames(range.startTime, range.endTime)));
|
|
663
|
+
if (destroyed ||
|
|
664
|
+
loadId !== incrementalLoadId ||
|
|
665
|
+
bufferedVersionRange !== incrementalVersionRange) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
buffer = mergeIncrementalFrames(buffer, changedFrameRanges.flat(), changedRanges);
|
|
669
|
+
bufferedSourceVersion = changes.version;
|
|
670
|
+
state = {
|
|
671
|
+
...state,
|
|
672
|
+
detectionCount: countDetections(buffer),
|
|
673
|
+
errorMessage: null,
|
|
674
|
+
frameCount: buffer.length,
|
|
675
|
+
status: DetectionBufferStatus.Ready,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
if (!destroyed) {
|
|
680
|
+
state = {
|
|
681
|
+
...state,
|
|
682
|
+
errorMessage: getErrorMessage(error),
|
|
683
|
+
status: DetectionBufferStatus.Error,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
throw error;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function shouldWaitForPlaybackGate(prepareOptions) {
|
|
690
|
+
return (prepareOptions?.gatePlayback === true &&
|
|
691
|
+
playbackGate?.enabled === true &&
|
|
692
|
+
Boolean(options.source.waitForRange));
|
|
693
|
+
}
|
|
694
|
+
async function waitForPlaybackGate(mediaTime, prepareOptions) {
|
|
695
|
+
if (!playbackGate?.enabled || !options.source.waitForRange) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const requiredAheadSeconds = Math.max(0, playbackGate.requiredAheadSeconds ?? 0);
|
|
699
|
+
const comparableMediaTime = getComparableMediaTime(mediaTime);
|
|
700
|
+
const endTime = getRequiredCoverageEndTime({
|
|
701
|
+
duration: isLoopingTimeline() ? null : prepareOptions?.duration,
|
|
702
|
+
firstTimestamp: prepareOptions?.firstTimestamp,
|
|
703
|
+
mediaTime: comparableMediaTime,
|
|
704
|
+
requiredAheadSeconds,
|
|
705
|
+
});
|
|
706
|
+
if (endTime <= comparableMediaTime) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const coveragePlan = createLoadPlan(comparableMediaTime, endTime);
|
|
710
|
+
state = {
|
|
711
|
+
...state,
|
|
712
|
+
errorMessage: null,
|
|
713
|
+
requestedEndTime: coveragePlan.endTime,
|
|
714
|
+
requestedStartTime: coveragePlan.startTime,
|
|
715
|
+
status: DetectionBufferStatus.Loading,
|
|
716
|
+
};
|
|
717
|
+
try {
|
|
718
|
+
await Promise.all(coveragePlan.sourceRanges.map((range) => options.source.waitForRange?.(range)));
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
if (!destroyed) {
|
|
722
|
+
state = {
|
|
723
|
+
...state,
|
|
724
|
+
errorMessage: getErrorMessage(error),
|
|
725
|
+
status: DetectionBufferStatus.Error,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function shouldRefreshRollingWindow(mediaTime) {
|
|
732
|
+
if (refreshIntervalSeconds === null ||
|
|
733
|
+
refreshIntervalSeconds <= 0 ||
|
|
734
|
+
state.bufferStartTime === null ||
|
|
735
|
+
state.bufferEndTime === null) {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
const { endTime, startTime } = getLoadRange(mediaTime);
|
|
739
|
+
return (Math.abs(startTime - state.bufferStartTime) >= refreshIntervalSeconds ||
|
|
740
|
+
Math.abs(endTime - state.bufferEndTime) >= refreshIntervalSeconds);
|
|
741
|
+
}
|
|
742
|
+
function createLoadPlan(requestedStartTime, requestedEndTime) {
|
|
743
|
+
const startTime = Math.min(requestedStartTime, requestedEndTime);
|
|
744
|
+
const endTime = Math.max(startTime, requestedEndTime);
|
|
745
|
+
if (!isLoopingTimeline()) {
|
|
746
|
+
const clampedStartTime = Math.max(0, startTime);
|
|
747
|
+
const clampedEndTime = Math.max(clampedStartTime, endTime);
|
|
748
|
+
return {
|
|
749
|
+
endTime: clampedEndTime,
|
|
750
|
+
sourceRanges: [
|
|
751
|
+
{
|
|
752
|
+
endTime: clampedEndTime,
|
|
753
|
+
startTime: clampedStartTime,
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
startTime: clampedStartTime,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const duration = timelineContext.duration ?? 0;
|
|
760
|
+
if (endTime - startTime >= duration) {
|
|
761
|
+
return {
|
|
762
|
+
endTime: duration,
|
|
763
|
+
sourceRanges: [{ endTime: duration, startTime: 0 }],
|
|
764
|
+
startTime: 0,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
endTime,
|
|
769
|
+
sourceRanges: getLoopingSourceRanges(startTime, endTime, duration),
|
|
770
|
+
startTime,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function getBufferedSourceRanges() {
|
|
774
|
+
if (!bufferedVersionRange) {
|
|
775
|
+
return [];
|
|
776
|
+
}
|
|
777
|
+
return createLoadPlan(bufferedVersionRange.startTime, bufferedVersionRange.endTime).sourceRanges;
|
|
778
|
+
}
|
|
779
|
+
function isLoopingTimeline() {
|
|
780
|
+
return (timelineContext.loop &&
|
|
781
|
+
timelineContext.duration !== null &&
|
|
782
|
+
timelineContext.duration > 0);
|
|
783
|
+
}
|
|
784
|
+
function getComparableMediaTime(mediaTime) {
|
|
785
|
+
if (!isLoopingTimeline() ||
|
|
786
|
+
state.bufferStartTime === null ||
|
|
787
|
+
state.bufferEndTime === null ||
|
|
788
|
+
timelineContext.duration === null) {
|
|
789
|
+
return mediaTime;
|
|
790
|
+
}
|
|
791
|
+
const duration = timelineContext.duration;
|
|
792
|
+
let comparableMediaTime = mediaTime;
|
|
793
|
+
while (comparableMediaTime < state.bufferStartTime) {
|
|
794
|
+
comparableMediaTime += duration;
|
|
795
|
+
}
|
|
796
|
+
while (comparableMediaTime > state.bufferEndTime) {
|
|
797
|
+
comparableMediaTime -= duration;
|
|
798
|
+
}
|
|
799
|
+
return comparableMediaTime;
|
|
800
|
+
}
|
|
801
|
+
function getSourceMediaTime(mediaTime) {
|
|
802
|
+
if (!isLoopingTimeline() || timelineContext.duration === null) {
|
|
803
|
+
return mediaTime;
|
|
804
|
+
}
|
|
805
|
+
if (mediaTime >= 0 && mediaTime <= timelineContext.duration) {
|
|
806
|
+
return mediaTime;
|
|
807
|
+
}
|
|
808
|
+
return modulo(mediaTime, timelineContext.duration);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function createIdleDetectionBufferState() {
|
|
812
|
+
return {
|
|
813
|
+
bufferEndTime: null,
|
|
814
|
+
bufferStartTime: null,
|
|
815
|
+
detectionCount: 0,
|
|
816
|
+
errorMessage: null,
|
|
817
|
+
frameCount: 0,
|
|
818
|
+
requestedEndTime: null,
|
|
819
|
+
requestedStartTime: null,
|
|
820
|
+
status: DetectionBufferStatus.Idle,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function rangeContains(outerStart, outerEnd, innerStart, innerEnd) {
|
|
824
|
+
return outerStart <= innerStart && innerEnd <= outerEnd;
|
|
825
|
+
}
|
|
826
|
+
function countDetections(frames) {
|
|
827
|
+
return frames.reduce((total, frame) => total + frame.detections.length, 0);
|
|
828
|
+
}
|
|
829
|
+
function getErrorMessage(error) {
|
|
830
|
+
return error instanceof Error
|
|
831
|
+
? error.message
|
|
832
|
+
: "Detection buffer load failed.";
|
|
833
|
+
}
|
|
834
|
+
function getRequiredCoverageEndTime(options) {
|
|
835
|
+
const requestedEndTime = options.mediaTime + options.requiredAheadSeconds;
|
|
836
|
+
if (options.duration === null || options.duration === undefined) {
|
|
837
|
+
return requestedEndTime;
|
|
838
|
+
}
|
|
839
|
+
return Math.min(requestedEndTime, (options.firstTimestamp ?? 0) + Math.max(options.duration, 0));
|
|
840
|
+
}
|
|
841
|
+
function getLoopingSourceRanges(startTime, endTime, duration) {
|
|
842
|
+
const normalizedStartTime = modulo(startTime, duration);
|
|
843
|
+
const normalizedEndTime = modulo(endTime, duration);
|
|
844
|
+
const startCycle = Math.floor(startTime / duration);
|
|
845
|
+
const endCycle = Math.floor(endTime / duration);
|
|
846
|
+
if (startCycle === endCycle) {
|
|
847
|
+
return [{ endTime: normalizedEndTime, startTime: normalizedStartTime }];
|
|
848
|
+
}
|
|
849
|
+
const ranges = [];
|
|
850
|
+
if (normalizedStartTime < duration) {
|
|
851
|
+
ranges.push({ endTime: duration, startTime: normalizedStartTime });
|
|
852
|
+
}
|
|
853
|
+
if (normalizedEndTime > 0) {
|
|
854
|
+
ranges.push({ endTime: normalizedEndTime, startTime: 0 });
|
|
855
|
+
}
|
|
856
|
+
return ranges;
|
|
857
|
+
}
|
|
858
|
+
function modulo(value, modulus) {
|
|
859
|
+
return ((value % modulus) + modulus) % modulus;
|
|
860
|
+
}
|
|
861
|
+
function getOverlappingRanges(changedRanges, bufferedRanges) {
|
|
862
|
+
const intersections = [];
|
|
863
|
+
for (const changedRange of changedRanges) {
|
|
864
|
+
for (const bufferedRange of bufferedRanges) {
|
|
865
|
+
const startTime = Math.max(changedRange.startTime, bufferedRange.startTime);
|
|
866
|
+
const endTime = Math.min(changedRange.endTime, bufferedRange.endTime);
|
|
867
|
+
if (startTime <= endTime) {
|
|
868
|
+
intersections.push({ endTime, startTime });
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return intersections;
|
|
873
|
+
}
|
|
874
|
+
function mergeIncrementalFrames(currentFrames, changedFrames, changedRanges) {
|
|
875
|
+
const framesByIdentity = new Map();
|
|
876
|
+
for (const frame of currentFrames) {
|
|
877
|
+
if (changedRanges.some((range) => detectionFrameOverlapsRange(frame, range.startTime, range.endTime))) {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
framesByIdentity.set(getDetectionFrameIdentity(frame), frame);
|
|
881
|
+
}
|
|
882
|
+
for (const frame of changedFrames) {
|
|
883
|
+
framesByIdentity.set(getDetectionFrameIdentity(frame), frame);
|
|
884
|
+
}
|
|
885
|
+
return Array.from(framesByIdentity.values()).sort(compareDetectionFrames);
|
|
886
|
+
}
|
|
887
|
+
function reuseBufferedFrameSnapshots(currentFrames, loadedFrames) {
|
|
888
|
+
const currentFramesByIdentity = new Map(currentFrames.map((frame) => [getDetectionFrameIdentity(frame), frame]));
|
|
889
|
+
return loadedFrames.map((frame) => currentFramesByIdentity.get(getDetectionFrameIdentity(frame)) ?? frame);
|
|
890
|
+
}
|
|
891
|
+
function getDetectionFrameIdentity(frame) {
|
|
892
|
+
return frame.frameIndex === undefined
|
|
893
|
+
? `time:${frame.mediaTime}`
|
|
894
|
+
: `index:${frame.frameIndex}`;
|
|
895
|
+
}
|
|
896
|
+
function compareDetectionFrames(left, right) {
|
|
897
|
+
if (left.mediaTime !== right.mediaTime) {
|
|
898
|
+
return left.mediaTime - right.mediaTime;
|
|
899
|
+
}
|
|
900
|
+
return (left.frameIndex ?? 0) - (right.frameIndex ?? 0);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function createColdDetectionFrameSource(options) {
|
|
904
|
+
return {
|
|
905
|
+
loadFrames(startTime, endTime) {
|
|
906
|
+
return options.store.loadFrames({
|
|
907
|
+
datasetId: options.datasetId,
|
|
908
|
+
endTime,
|
|
909
|
+
startTime,
|
|
910
|
+
});
|
|
911
|
+
},
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function createCompositeDetectionFrameSource(options) {
|
|
916
|
+
const sources = normalizeCompositeSources(options.sources);
|
|
917
|
+
return {
|
|
918
|
+
async loadFrames(startTime, endTime) {
|
|
919
|
+
const loadedSources = await Promise.all(sources.map(async (source) => ({
|
|
920
|
+
...source,
|
|
921
|
+
frames: copySortedDetectionFrames(await source.source.loadFrames(startTime, endTime)),
|
|
922
|
+
})));
|
|
923
|
+
if (options.selectionMode === DetectionFrameSelectionMode.NearestFrameIndex) {
|
|
924
|
+
const nearestFrames = composeNearestFrameIndexFrames(loadedSources, startTime, endTime, options);
|
|
925
|
+
if (nearestFrames) {
|
|
926
|
+
return nearestFrames;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return composeIntervalFrames(loadedSources, startTime, endTime, options);
|
|
930
|
+
},
|
|
931
|
+
async waitForRange(range) {
|
|
932
|
+
await Promise.all(sources
|
|
933
|
+
.filter((source) => source.requiredForPlayback)
|
|
934
|
+
.map((source) => source.source.waitForRange?.(range)));
|
|
935
|
+
},
|
|
936
|
+
getAvailableRanges() {
|
|
937
|
+
return mergeRanges$1(sources.flatMap((source) => source.source.getAvailableRanges?.() ?? []));
|
|
938
|
+
},
|
|
939
|
+
getVersion(range) {
|
|
940
|
+
return sources.reduce((version, source) => version + (source.source.getVersion?.(range) ?? 0), 0);
|
|
941
|
+
},
|
|
942
|
+
destroy() {
|
|
943
|
+
for (const source of sources) {
|
|
944
|
+
source.source.destroy?.();
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function normalizeCompositeSources(entries) {
|
|
950
|
+
const sourceIds = new Set();
|
|
951
|
+
return entries
|
|
952
|
+
.map((entry, declarationIndex) => {
|
|
953
|
+
if (sourceIds.has(entry.id)) {
|
|
954
|
+
throw new Error(`Duplicate detection source id: ${entry.id}.`);
|
|
955
|
+
}
|
|
956
|
+
sourceIds.add(entry.id);
|
|
957
|
+
const inputCount = [
|
|
958
|
+
entry.frames !== undefined,
|
|
959
|
+
entry.source !== undefined,
|
|
960
|
+
].filter(Boolean).length;
|
|
961
|
+
if (inputCount !== 1) {
|
|
962
|
+
throw new Error(`Detection source ${entry.id} must provide exactly one input: frames or source.`);
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
declarationIndex,
|
|
966
|
+
id: entry.id,
|
|
967
|
+
order: entry.order ?? 0,
|
|
968
|
+
requiredForPlayback: entry.requiredForPlayback !== false,
|
|
969
|
+
source: entry.source ?? createArrayDetectionFrameSource(entry.frames ?? []),
|
|
970
|
+
sync: entry.sync,
|
|
971
|
+
};
|
|
972
|
+
})
|
|
973
|
+
.sort((left, right) => left.order - right.order ||
|
|
974
|
+
left.declarationIndex - right.declarationIndex);
|
|
975
|
+
}
|
|
976
|
+
function composeIntervalFrames(sources, startTime, endTime, options) {
|
|
977
|
+
const boundaryTimes = new Set([startTime]);
|
|
978
|
+
for (const source of sources) {
|
|
979
|
+
for (const frame of source.frames) {
|
|
980
|
+
if (frame.mediaTime >= startTime && frame.mediaTime < endTime) {
|
|
981
|
+
boundaryTimes.add(frame.mediaTime);
|
|
982
|
+
}
|
|
983
|
+
if (frame.endTime !== undefined &&
|
|
984
|
+
frame.endTime > startTime &&
|
|
985
|
+
frame.endTime < endTime) {
|
|
986
|
+
boundaryTimes.add(frame.endTime);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const sortedBoundaryTimes = [...boundaryTimes].sort((left, right) => left - right);
|
|
991
|
+
const frames = [];
|
|
992
|
+
for (const [boundaryIndex, mediaTime] of sortedBoundaryTimes.entries()) {
|
|
993
|
+
if (mediaTime < startTime || mediaTime >= endTime) {
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
const nextBoundaryTime = sortedBoundaryTimes[boundaryIndex + 1] ?? endTime;
|
|
997
|
+
const endTimeForFrame = Math.min(nextBoundaryTime, endTime);
|
|
998
|
+
const frame = composeFrameAtTime(sources, mediaTime, endTimeForFrame, {
|
|
999
|
+
...options,
|
|
1000
|
+
selectionMode: DetectionFrameSelectionMode.Interval,
|
|
1001
|
+
});
|
|
1002
|
+
if (frame) {
|
|
1003
|
+
frames.push(frame);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return frames;
|
|
1007
|
+
}
|
|
1008
|
+
function composeNearestFrameIndexFrames(sources, startTime, endTime, options) {
|
|
1009
|
+
const frameRate = options.frameRate;
|
|
1010
|
+
if (!frameRate || !Number.isFinite(frameRate) || frameRate <= 0) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
const indexedFrames = sources.flatMap((source) => source.frames.filter((frame) => frame.frameIndex !== undefined));
|
|
1014
|
+
const firstIndexedFrame = indexedFrames[0];
|
|
1015
|
+
if (!firstIndexedFrame || firstIndexedFrame.frameIndex === undefined) {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
const originTime = options.frameIndexOriginTime ??
|
|
1019
|
+
firstIndexedFrame.mediaTime - firstIndexedFrame.frameIndex / frameRate;
|
|
1020
|
+
const frameIndexes = [
|
|
1021
|
+
...new Set(indexedFrames
|
|
1022
|
+
.map((frame) => frame.frameIndex)
|
|
1023
|
+
.filter((frameIndex) => frameIndex !== undefined)),
|
|
1024
|
+
].sort((left, right) => left - right);
|
|
1025
|
+
const frames = [];
|
|
1026
|
+
for (const frameIndex of frameIndexes) {
|
|
1027
|
+
const mediaTime = originTime + frameIndex / frameRate;
|
|
1028
|
+
if (mediaTime < startTime || mediaTime >= endTime) {
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const frame = composeFrameAtTime(sources, mediaTime, Math.min(mediaTime + 1 / frameRate, endTime), {
|
|
1032
|
+
...options,
|
|
1033
|
+
selectionMode: DetectionFrameSelectionMode.NearestFrameIndex,
|
|
1034
|
+
}, frameIndex);
|
|
1035
|
+
if (frame) {
|
|
1036
|
+
frames.push(frame);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return frames;
|
|
1040
|
+
}
|
|
1041
|
+
function composeFrameAtTime(sources, mediaTime, endTime, options, frameIndex) {
|
|
1042
|
+
const detections = [];
|
|
1043
|
+
const activeFrameIndexes = [];
|
|
1044
|
+
for (const source of sources) {
|
|
1045
|
+
const activeFrame = selectDetectionFrame(source.frames, mediaTime, {
|
|
1046
|
+
...options,
|
|
1047
|
+
...source.sync,
|
|
1048
|
+
});
|
|
1049
|
+
if (!activeFrame) {
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
if (activeFrame.frameIndex !== undefined) {
|
|
1053
|
+
activeFrameIndexes.push(activeFrame.frameIndex);
|
|
1054
|
+
}
|
|
1055
|
+
activeFrame.detections.forEach((detection, sourceDetectionIndex) => {
|
|
1056
|
+
detections.push(copyDetectionWithSource(detection, source.id, sourceDetectionIndex));
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
if (detections.length === 0) {
|
|
1060
|
+
return undefined;
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
detections,
|
|
1064
|
+
endTime,
|
|
1065
|
+
frameIndex: frameIndex ?? resolveComposedFrameIndex(activeFrameIndexes) ?? undefined,
|
|
1066
|
+
mediaTime,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
function copyDetectionWithSource(detection, sourceId, sourceDetectionIndex) {
|
|
1070
|
+
return {
|
|
1071
|
+
...detection,
|
|
1072
|
+
mask: detection.mask ? { ...detection.mask } : undefined,
|
|
1073
|
+
metadata: detection.metadata ? { ...detection.metadata } : undefined,
|
|
1074
|
+
rect: detection.rect ? { ...detection.rect } : undefined,
|
|
1075
|
+
sourceDetectionIndex,
|
|
1076
|
+
sourceId,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function resolveComposedFrameIndex(frameIndexes) {
|
|
1080
|
+
if (frameIndexes.length === 0) {
|
|
1081
|
+
return undefined;
|
|
1082
|
+
}
|
|
1083
|
+
const firstFrameIndex = frameIndexes[0];
|
|
1084
|
+
return frameIndexes.every((frameIndex) => frameIndex === firstFrameIndex)
|
|
1085
|
+
? firstFrameIndex
|
|
1086
|
+
: undefined;
|
|
1087
|
+
}
|
|
1088
|
+
function mergeRanges$1(ranges) {
|
|
1089
|
+
const sortedRanges = [...ranges].sort((left, right) => left.startTime - right.startTime);
|
|
1090
|
+
const mergedRanges = [];
|
|
1091
|
+
for (const range of sortedRanges) {
|
|
1092
|
+
const lastRange = mergedRanges.at(-1);
|
|
1093
|
+
if (!lastRange || range.startTime > lastRange.endTime) {
|
|
1094
|
+
mergedRanges.push({ ...range });
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
mergedRanges[mergedRanges.length - 1] = {
|
|
1098
|
+
startTime: lastRange.startTime,
|
|
1099
|
+
endTime: Math.max(lastRange.endTime, range.endTime),
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
return mergedRanges;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
var AnnotationFrameMutationKind;
|
|
1106
|
+
(function (AnnotationFrameMutationKind) {
|
|
1107
|
+
AnnotationFrameMutationKind["Add"] = "add";
|
|
1108
|
+
AnnotationFrameMutationKind["Remove"] = "remove";
|
|
1109
|
+
AnnotationFrameMutationKind["Replace"] = "replace";
|
|
1110
|
+
AnnotationFrameMutationKind["Transact"] = "transact";
|
|
1111
|
+
AnnotationFrameMutationKind["Update"] = "update";
|
|
1112
|
+
})(AnnotationFrameMutationKind || (AnnotationFrameMutationKind = {}));
|
|
1113
|
+
function createEditableAnnotationFrameSession(initialFrame) {
|
|
1114
|
+
let snapshot = createSnapshot(initialFrame);
|
|
1115
|
+
let destroyed = false;
|
|
1116
|
+
const listeners = new Set();
|
|
1117
|
+
assertStableIds(snapshot);
|
|
1118
|
+
const commit = (frame, kind, detectionIds) => {
|
|
1119
|
+
assertActive();
|
|
1120
|
+
const previous = snapshot;
|
|
1121
|
+
const current = createSnapshot(frame);
|
|
1122
|
+
assertStableIds(current);
|
|
1123
|
+
snapshot = current;
|
|
1124
|
+
const mutation = Object.freeze({
|
|
1125
|
+
current,
|
|
1126
|
+
detectionIds: Object.freeze([...detectionIds]),
|
|
1127
|
+
kind,
|
|
1128
|
+
previous,
|
|
1129
|
+
});
|
|
1130
|
+
for (const listener of listeners) {
|
|
1131
|
+
listener(mutation);
|
|
1132
|
+
}
|
|
1133
|
+
return current;
|
|
1134
|
+
};
|
|
1135
|
+
const session = {
|
|
1136
|
+
getSnapshot() {
|
|
1137
|
+
assertActive();
|
|
1138
|
+
return snapshot;
|
|
1139
|
+
},
|
|
1140
|
+
add(detection, index = snapshot.detections.length) {
|
|
1141
|
+
assertActive();
|
|
1142
|
+
const id = requireDetectionId(detection);
|
|
1143
|
+
if (findDetectionIndex(snapshot, id) !== -1) {
|
|
1144
|
+
throw new Error(`Detection id ${String(id)} already exists.`);
|
|
1145
|
+
}
|
|
1146
|
+
const detections = [...snapshot.detections];
|
|
1147
|
+
const insertionIndex = Math.max(0, Math.min(index, detections.length));
|
|
1148
|
+
detections.splice(insertionIndex, 0, detection);
|
|
1149
|
+
return commit({ ...snapshot, detections }, AnnotationFrameMutationKind.Add, [id]);
|
|
1150
|
+
},
|
|
1151
|
+
update(id, update) {
|
|
1152
|
+
assertActive();
|
|
1153
|
+
const detectionIndex = findDetectionIndex(snapshot, id);
|
|
1154
|
+
if (detectionIndex === -1) {
|
|
1155
|
+
throw new Error(`Detection id ${String(id)} was not found.`);
|
|
1156
|
+
}
|
|
1157
|
+
const detections = [...snapshot.detections];
|
|
1158
|
+
const current = detections[detectionIndex];
|
|
1159
|
+
const next = typeof update === "function"
|
|
1160
|
+
? update(current)
|
|
1161
|
+
: { ...current, ...update };
|
|
1162
|
+
if (next.id !== id) {
|
|
1163
|
+
throw new Error("Detection updates must preserve the stable id.");
|
|
1164
|
+
}
|
|
1165
|
+
detections[detectionIndex] = next;
|
|
1166
|
+
return commit({ ...snapshot, detections }, AnnotationFrameMutationKind.Update, [id]);
|
|
1167
|
+
},
|
|
1168
|
+
remove(id) {
|
|
1169
|
+
assertActive();
|
|
1170
|
+
const detectionIndex = findDetectionIndex(snapshot, id);
|
|
1171
|
+
if (detectionIndex === -1) {
|
|
1172
|
+
return snapshot;
|
|
1173
|
+
}
|
|
1174
|
+
const detections = [...snapshot.detections];
|
|
1175
|
+
detections.splice(detectionIndex, 1);
|
|
1176
|
+
return commit({ ...snapshot, detections }, AnnotationFrameMutationKind.Remove, [id]);
|
|
1177
|
+
},
|
|
1178
|
+
replace(frame) {
|
|
1179
|
+
return commit(frame, AnnotationFrameMutationKind.Replace, frame.detections.map(requireDetectionId));
|
|
1180
|
+
},
|
|
1181
|
+
transact(mutate, detectionIds = []) {
|
|
1182
|
+
assertActive();
|
|
1183
|
+
const detections = copySortedDetectionFrames([snapshot])[0]
|
|
1184
|
+
.detections;
|
|
1185
|
+
mutate(detections);
|
|
1186
|
+
return commit({ ...snapshot, detections }, AnnotationFrameMutationKind.Transact, detectionIds);
|
|
1187
|
+
},
|
|
1188
|
+
subscribe(listener) {
|
|
1189
|
+
assertActive();
|
|
1190
|
+
listeners.add(listener);
|
|
1191
|
+
return () => listeners.delete(listener);
|
|
1192
|
+
},
|
|
1193
|
+
destroy() {
|
|
1194
|
+
destroyed = true;
|
|
1195
|
+
listeners.clear();
|
|
1196
|
+
},
|
|
1197
|
+
};
|
|
1198
|
+
return session;
|
|
1199
|
+
function assertActive() {
|
|
1200
|
+
if (destroyed) {
|
|
1201
|
+
throw new Error("Editable annotation frame session has been destroyed.");
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
function findDetectionIndex(frame, id) {
|
|
1206
|
+
return frame.detections.findIndex((detection) => detection.id === id);
|
|
1207
|
+
}
|
|
1208
|
+
function requireDetectionId(detection) {
|
|
1209
|
+
if (detection.id === undefined) {
|
|
1210
|
+
throw new Error("Editable annotation detections require a stable id.");
|
|
1211
|
+
}
|
|
1212
|
+
return detection.id;
|
|
1213
|
+
}
|
|
1214
|
+
function assertStableIds(frame) {
|
|
1215
|
+
const ids = new Set();
|
|
1216
|
+
for (const detection of frame.detections) {
|
|
1217
|
+
const id = requireDetectionId(detection);
|
|
1218
|
+
if (ids.has(id)) {
|
|
1219
|
+
throw new Error(`Detection id ${String(id)} is duplicated.`);
|
|
1220
|
+
}
|
|
1221
|
+
ids.add(id);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function createSnapshot(frame) {
|
|
1225
|
+
const snapshot = copySortedDetectionFrames([frame])[0];
|
|
1226
|
+
return deepFreeze(snapshot);
|
|
1227
|
+
}
|
|
1228
|
+
function deepFreeze(value) {
|
|
1229
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
1230
|
+
Object.freeze(value);
|
|
1231
|
+
for (const nested of Object.values(value)) {
|
|
1232
|
+
deepFreeze(nested);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return value;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const DEFAULT_CHUNK_DURATION_SECONDS = 1;
|
|
1239
|
+
function createMemoryColdDetectionFrameStore() {
|
|
1240
|
+
const datasets = new Map();
|
|
1241
|
+
let destroyed = false;
|
|
1242
|
+
return {
|
|
1243
|
+
async putFrames(options) {
|
|
1244
|
+
assertActive();
|
|
1245
|
+
const dataset = createDataset(resolveChunkDurationSeconds(options));
|
|
1246
|
+
upsertFrames(dataset, copySortedDetectionFrames(options.frames));
|
|
1247
|
+
datasets.set(options.datasetId, dataset);
|
|
1248
|
+
return createWriteSummary(options.datasetId, dataset);
|
|
1249
|
+
},
|
|
1250
|
+
async appendFrames(options) {
|
|
1251
|
+
assertActive();
|
|
1252
|
+
const existingDataset = datasets.get(options.datasetId);
|
|
1253
|
+
const chunkDurationSeconds = resolveChunkDurationSeconds(options, existingDataset);
|
|
1254
|
+
const dataset = existingDataset ?? createDataset(chunkDurationSeconds);
|
|
1255
|
+
upsertFrames(dataset, copySortedDetectionFrames(options.frames));
|
|
1256
|
+
datasets.set(options.datasetId, dataset);
|
|
1257
|
+
return createWriteSummary(options.datasetId, dataset);
|
|
1258
|
+
},
|
|
1259
|
+
async loadFrames(options) {
|
|
1260
|
+
assertActive();
|
|
1261
|
+
const dataset = datasets.get(options.datasetId);
|
|
1262
|
+
if (!dataset) {
|
|
1263
|
+
return [];
|
|
1264
|
+
}
|
|
1265
|
+
const startTime = Math.max(0, options.startTime);
|
|
1266
|
+
const endTime = Math.max(startTime, options.endTime);
|
|
1267
|
+
const frameKeys = new Set();
|
|
1268
|
+
const startChunkIndex = getChunkIndex(startTime, dataset.chunkDurationSeconds);
|
|
1269
|
+
const endChunkIndex = getChunkIndex(endTime, dataset.chunkDurationSeconds);
|
|
1270
|
+
for (let chunkIndex = startChunkIndex; chunkIndex <= endChunkIndex; chunkIndex += 1) {
|
|
1271
|
+
for (const frameKey of dataset.frameKeysByChunk.get(chunkIndex) ?? []) {
|
|
1272
|
+
frameKeys.add(frameKey);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const frames = Array.from(frameKeys)
|
|
1276
|
+
.map((frameKey) => dataset.framesByKey.get(frameKey)?.frame)
|
|
1277
|
+
.filter((frame) => frame !== undefined &&
|
|
1278
|
+
detectionFrameOverlapsRange(frame, startTime, endTime));
|
|
1279
|
+
return copySortedDetectionFrames(frames);
|
|
1280
|
+
},
|
|
1281
|
+
async clearDataset(datasetId) {
|
|
1282
|
+
assertActive();
|
|
1283
|
+
datasets.delete(datasetId);
|
|
1284
|
+
},
|
|
1285
|
+
destroy() {
|
|
1286
|
+
destroyed = true;
|
|
1287
|
+
datasets.clear();
|
|
1288
|
+
},
|
|
1289
|
+
};
|
|
1290
|
+
function assertActive() {
|
|
1291
|
+
if (destroyed) {
|
|
1292
|
+
throw new Error("Memory cold detection frame store has been destroyed.");
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function createDataset(chunkDurationSeconds) {
|
|
1297
|
+
return {
|
|
1298
|
+
chunkDurationSeconds,
|
|
1299
|
+
detectionCount: 0,
|
|
1300
|
+
endTime: null,
|
|
1301
|
+
frameKeysByChunk: new Map(),
|
|
1302
|
+
framesByKey: new Map(),
|
|
1303
|
+
startTime: null,
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
function upsertFrames(dataset, frames) {
|
|
1307
|
+
let shouldRecalculateBounds = false;
|
|
1308
|
+
for (const frame of frames) {
|
|
1309
|
+
const frameKey = getDetectionFrameDedupeKey(frame);
|
|
1310
|
+
const existingFrame = dataset.framesByKey.get(frameKey);
|
|
1311
|
+
if (existingFrame) {
|
|
1312
|
+
removeStoredFrame(dataset, frameKey, existingFrame);
|
|
1313
|
+
shouldRecalculateBounds ||= replacementCanShrinkBounds(dataset, existingFrame.frame, frame);
|
|
1314
|
+
}
|
|
1315
|
+
const chunkIndexes = getFrameChunkIndexes(frame, dataset.chunkDurationSeconds);
|
|
1316
|
+
dataset.framesByKey.set(frameKey, { chunkIndexes, frame });
|
|
1317
|
+
dataset.detectionCount += frame.detections.length;
|
|
1318
|
+
dataset.startTime = Math.min(dataset.startTime ?? frame.mediaTime, frame.mediaTime);
|
|
1319
|
+
dataset.endTime = Math.max(dataset.endTime ?? getFrameEndTime(frame), getFrameEndTime(frame));
|
|
1320
|
+
for (const chunkIndex of chunkIndexes) {
|
|
1321
|
+
const frameKeys = dataset.frameKeysByChunk.get(chunkIndex) ?? new Set();
|
|
1322
|
+
frameKeys.add(frameKey);
|
|
1323
|
+
dataset.frameKeysByChunk.set(chunkIndex, frameKeys);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (shouldRecalculateBounds) {
|
|
1327
|
+
recalculateBounds(dataset);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function removeStoredFrame(dataset, frameKey, storedFrame) {
|
|
1331
|
+
dataset.framesByKey.delete(frameKey);
|
|
1332
|
+
dataset.detectionCount -= storedFrame.frame.detections.length;
|
|
1333
|
+
for (const chunkIndex of storedFrame.chunkIndexes) {
|
|
1334
|
+
const frameKeys = dataset.frameKeysByChunk.get(chunkIndex);
|
|
1335
|
+
frameKeys?.delete(frameKey);
|
|
1336
|
+
if (frameKeys?.size === 0) {
|
|
1337
|
+
dataset.frameKeysByChunk.delete(chunkIndex);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
function replacementCanShrinkBounds(dataset, previousFrame, nextFrame) {
|
|
1342
|
+
return ((dataset.startTime === previousFrame.mediaTime &&
|
|
1343
|
+
nextFrame.mediaTime > previousFrame.mediaTime) ||
|
|
1344
|
+
(dataset.endTime === getFrameEndTime(previousFrame) &&
|
|
1345
|
+
getFrameEndTime(nextFrame) < getFrameEndTime(previousFrame)));
|
|
1346
|
+
}
|
|
1347
|
+
function recalculateBounds(dataset) {
|
|
1348
|
+
let startTime = null;
|
|
1349
|
+
let endTime = null;
|
|
1350
|
+
for (const { frame } of dataset.framesByKey.values()) {
|
|
1351
|
+
startTime = Math.min(startTime ?? frame.mediaTime, frame.mediaTime);
|
|
1352
|
+
endTime = Math.max(endTime ?? getFrameEndTime(frame), getFrameEndTime(frame));
|
|
1353
|
+
}
|
|
1354
|
+
dataset.startTime = startTime;
|
|
1355
|
+
dataset.endTime = endTime;
|
|
1356
|
+
}
|
|
1357
|
+
function resolveChunkDurationSeconds(options, existingDataset) {
|
|
1358
|
+
const chunkDurationSeconds = options.chunkDurationSeconds ??
|
|
1359
|
+
existingDataset?.chunkDurationSeconds ??
|
|
1360
|
+
DEFAULT_CHUNK_DURATION_SECONDS;
|
|
1361
|
+
if (chunkDurationSeconds <= 0) {
|
|
1362
|
+
throw new Error("chunkDurationSeconds must be greater than 0.");
|
|
1363
|
+
}
|
|
1364
|
+
if (existingDataset &&
|
|
1365
|
+
options.chunkDurationSeconds !== undefined &&
|
|
1366
|
+
options.chunkDurationSeconds !== existingDataset.chunkDurationSeconds) {
|
|
1367
|
+
throw new Error("chunkDurationSeconds must match the existing detection dataset.");
|
|
1368
|
+
}
|
|
1369
|
+
return chunkDurationSeconds;
|
|
1370
|
+
}
|
|
1371
|
+
function createWriteSummary(datasetId, dataset) {
|
|
1372
|
+
return {
|
|
1373
|
+
chunkCount: dataset.frameKeysByChunk.size,
|
|
1374
|
+
chunkDurationSeconds: dataset.chunkDurationSeconds,
|
|
1375
|
+
datasetId,
|
|
1376
|
+
detectionCount: dataset.detectionCount,
|
|
1377
|
+
endTime: dataset.endTime,
|
|
1378
|
+
frameCount: dataset.framesByKey.size,
|
|
1379
|
+
startTime: dataset.startTime,
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
function getFrameChunkIndexes(frame, chunkDurationSeconds) {
|
|
1383
|
+
const startIndex = getChunkIndex(frame.mediaTime, chunkDurationSeconds);
|
|
1384
|
+
const endIndex = Math.max(startIndex, Math.ceil(getFrameEndTime(frame) / chunkDurationSeconds) - 1);
|
|
1385
|
+
return Array.from({ length: endIndex - startIndex + 1 }, (_, offset) => startIndex + offset);
|
|
1386
|
+
}
|
|
1387
|
+
function getFrameEndTime(frame) {
|
|
1388
|
+
return frame.endTime ?? frame.mediaTime;
|
|
1389
|
+
}
|
|
1390
|
+
function getChunkIndex(mediaTime, chunkDurationSeconds) {
|
|
1391
|
+
return Math.floor(mediaTime / chunkDurationSeconds);
|
|
1392
|
+
}
|
|
1393
|
+
function getDetectionFrameDedupeKey(frame) {
|
|
1394
|
+
return frame.frameIndex === undefined
|
|
1395
|
+
? `time:${frame.mediaTime}`
|
|
1396
|
+
: `index:${frame.frameIndex}`;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const RANGE_EPSILON_SECONDS = 1e-6;
|
|
1400
|
+
const MAX_CHANGED_RANGE_JOURNAL_LENGTH = 512;
|
|
1401
|
+
function createWritableDetectionFrameSource(options) {
|
|
1402
|
+
let summary = null;
|
|
1403
|
+
let version = 0;
|
|
1404
|
+
let allRangeVersion = 0;
|
|
1405
|
+
let journalFloorVersion = 0;
|
|
1406
|
+
let destroyed = false;
|
|
1407
|
+
const changedRanges = [];
|
|
1408
|
+
const availableRanges = [];
|
|
1409
|
+
const waiters = [];
|
|
1410
|
+
const writeOptions = (frames) => ({
|
|
1411
|
+
chunkDurationSeconds: options.chunkDurationSeconds,
|
|
1412
|
+
datasetId: options.datasetId,
|
|
1413
|
+
frames,
|
|
1414
|
+
});
|
|
1415
|
+
const recordRangeWrite = (nextSummary, changedSourceRanges) => {
|
|
1416
|
+
summary = nextSummary;
|
|
1417
|
+
version += 1;
|
|
1418
|
+
for (const changedRange of changedSourceRanges) {
|
|
1419
|
+
changedRanges.push({ ...changedRange, version });
|
|
1420
|
+
recordAvailableRange(changedRange, availableRanges);
|
|
1421
|
+
}
|
|
1422
|
+
compactChangedRangeJournal();
|
|
1423
|
+
resolveCoveredWaiters();
|
|
1424
|
+
return nextSummary;
|
|
1425
|
+
};
|
|
1426
|
+
const recordAllRangesWrite = (nextSummary) => {
|
|
1427
|
+
summary = nextSummary;
|
|
1428
|
+
version += 1;
|
|
1429
|
+
allRangeVersion = version;
|
|
1430
|
+
journalFloorVersion = version;
|
|
1431
|
+
changedRanges.length = 0;
|
|
1432
|
+
availableRanges.length = 0;
|
|
1433
|
+
if (nextSummary.startTime !== null && nextSummary.endTime !== null) {
|
|
1434
|
+
recordAvailableRange({
|
|
1435
|
+
endTime: nextSummary.endTime,
|
|
1436
|
+
startTime: nextSummary.startTime,
|
|
1437
|
+
}, availableRanges);
|
|
1438
|
+
}
|
|
1439
|
+
resolveCoveredWaiters();
|
|
1440
|
+
return nextSummary;
|
|
1441
|
+
};
|
|
1442
|
+
return {
|
|
1443
|
+
datasetId: options.datasetId,
|
|
1444
|
+
async appendFrames(frames) {
|
|
1445
|
+
assertActive();
|
|
1446
|
+
const nextSummary = await options.store.appendFrames(writeOptions(frames));
|
|
1447
|
+
assertActive();
|
|
1448
|
+
const changedSourceRanges = getDetectionFrameRanges(frames);
|
|
1449
|
+
const retainedSummary = await applyRetention(nextSummary);
|
|
1450
|
+
assertActive();
|
|
1451
|
+
if (retainedSummary !== nextSummary) {
|
|
1452
|
+
return recordAllRangesWrite(retainedSummary);
|
|
1453
|
+
}
|
|
1454
|
+
if (changedSourceRanges.length === 0) {
|
|
1455
|
+
summary = nextSummary;
|
|
1456
|
+
return nextSummary;
|
|
1457
|
+
}
|
|
1458
|
+
return recordRangeWrite(nextSummary, changedSourceRanges);
|
|
1459
|
+
},
|
|
1460
|
+
async replaceFrames(frames) {
|
|
1461
|
+
assertActive();
|
|
1462
|
+
const nextSummary = await options.store.putFrames(writeOptions(frames));
|
|
1463
|
+
assertActive();
|
|
1464
|
+
const retainedSummary = await applyRetention(nextSummary);
|
|
1465
|
+
assertActive();
|
|
1466
|
+
return recordAllRangesWrite(retainedSummary);
|
|
1467
|
+
},
|
|
1468
|
+
async clear() {
|
|
1469
|
+
assertActive();
|
|
1470
|
+
await options.store.clearDataset(options.datasetId);
|
|
1471
|
+
assertActive();
|
|
1472
|
+
summary = null;
|
|
1473
|
+
version += 1;
|
|
1474
|
+
allRangeVersion = version;
|
|
1475
|
+
journalFloorVersion = version;
|
|
1476
|
+
changedRanges.length = 0;
|
|
1477
|
+
availableRanges.length = 0;
|
|
1478
|
+
},
|
|
1479
|
+
async loadFrames(startTime, endTime) {
|
|
1480
|
+
assertActive();
|
|
1481
|
+
const loadedFrames = await options.store.loadFrames({
|
|
1482
|
+
datasetId: options.datasetId,
|
|
1483
|
+
endTime,
|
|
1484
|
+
startTime,
|
|
1485
|
+
});
|
|
1486
|
+
assertActive();
|
|
1487
|
+
return loadedFrames;
|
|
1488
|
+
},
|
|
1489
|
+
getSummary() {
|
|
1490
|
+
return summary ? { ...summary } : null;
|
|
1491
|
+
},
|
|
1492
|
+
getAvailableRanges() {
|
|
1493
|
+
return availableRanges.map((range) => ({ ...range }));
|
|
1494
|
+
},
|
|
1495
|
+
waitForRange(range) {
|
|
1496
|
+
if (destroyed) {
|
|
1497
|
+
return Promise.reject(createDestroyedError());
|
|
1498
|
+
}
|
|
1499
|
+
if (isRangeCovered(range, availableRanges)) {
|
|
1500
|
+
return Promise.resolve();
|
|
1501
|
+
}
|
|
1502
|
+
return new Promise((resolve, reject) => {
|
|
1503
|
+
waiters.push({ range, reject, resolve });
|
|
1504
|
+
});
|
|
1505
|
+
},
|
|
1506
|
+
getVersion(range) {
|
|
1507
|
+
if (!range) {
|
|
1508
|
+
return version;
|
|
1509
|
+
}
|
|
1510
|
+
return changedRanges.reduce((rangeVersion, changedRange) => rangesOverlap(range, changedRange)
|
|
1511
|
+
? Math.max(rangeVersion, changedRange.version)
|
|
1512
|
+
: rangeVersion, Math.max(allRangeVersion, journalFloorVersion));
|
|
1513
|
+
},
|
|
1514
|
+
getChangesSince(previousVersion, ranges) {
|
|
1515
|
+
const relevantVersion = ranges.reduce((rangeVersion, range) => Math.max(rangeVersion, changedRanges.reduce((changedVersion, changedRange) => rangesOverlap(range, changedRange)
|
|
1516
|
+
? Math.max(changedVersion, changedRange.version)
|
|
1517
|
+
: changedVersion, Math.max(allRangeVersion, journalFloorVersion))), Math.max(allRangeVersion, journalFloorVersion));
|
|
1518
|
+
if (relevantVersion <= previousVersion) {
|
|
1519
|
+
return {
|
|
1520
|
+
ranges: [],
|
|
1521
|
+
requiresReload: false,
|
|
1522
|
+
version: relevantVersion,
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
if (previousVersion < allRangeVersion ||
|
|
1526
|
+
previousVersion < journalFloorVersion) {
|
|
1527
|
+
return {
|
|
1528
|
+
ranges: [],
|
|
1529
|
+
requiresReload: true,
|
|
1530
|
+
version: relevantVersion,
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
const changedSourceRanges = changedRanges
|
|
1534
|
+
.filter((changedRange) => changedRange.version > previousVersion &&
|
|
1535
|
+
ranges.some((range) => rangesOverlap(range, changedRange)))
|
|
1536
|
+
.map(({ endTime, startTime }) => ({ endTime, startTime }));
|
|
1537
|
+
return {
|
|
1538
|
+
ranges: mergeRanges(changedSourceRanges),
|
|
1539
|
+
requiresReload: false,
|
|
1540
|
+
version: relevantVersion,
|
|
1541
|
+
};
|
|
1542
|
+
},
|
|
1543
|
+
destroy() {
|
|
1544
|
+
if (destroyed) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
destroyed = true;
|
|
1548
|
+
for (const waiter of waiters) {
|
|
1549
|
+
waiter.reject(new Error("Detection frame source has been destroyed."));
|
|
1550
|
+
}
|
|
1551
|
+
waiters.length = 0;
|
|
1552
|
+
options.store.destroy?.();
|
|
1553
|
+
},
|
|
1554
|
+
};
|
|
1555
|
+
function assertActive() {
|
|
1556
|
+
if (destroyed) {
|
|
1557
|
+
throw createDestroyedError();
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
async function applyRetention(nextSummary) {
|
|
1561
|
+
const retention = options.retention;
|
|
1562
|
+
if (retention === undefined ||
|
|
1563
|
+
!shouldApplyWindowRetention(retention) ||
|
|
1564
|
+
nextSummary.endTime === null) {
|
|
1565
|
+
return nextSummary;
|
|
1566
|
+
}
|
|
1567
|
+
const retentionWindowSeconds = retention.windowSeconds;
|
|
1568
|
+
if (retentionWindowSeconds === undefined ||
|
|
1569
|
+
!Number.isFinite(retentionWindowSeconds) ||
|
|
1570
|
+
retentionWindowSeconds <= 0) {
|
|
1571
|
+
throw new Error("retention.windowSeconds must be greater than 0.");
|
|
1572
|
+
}
|
|
1573
|
+
const retentionStartTime = Math.max(0, nextSummary.endTime - retentionWindowSeconds);
|
|
1574
|
+
if (nextSummary.startTime !== null &&
|
|
1575
|
+
nextSummary.startTime + RANGE_EPSILON_SECONDS >= retentionStartTime) {
|
|
1576
|
+
return nextSummary;
|
|
1577
|
+
}
|
|
1578
|
+
const retainedFrames = await options.store.loadFrames({
|
|
1579
|
+
datasetId: options.datasetId,
|
|
1580
|
+
endTime: nextSummary.endTime,
|
|
1581
|
+
startTime: retentionStartTime,
|
|
1582
|
+
});
|
|
1583
|
+
return options.store.putFrames(writeOptions(retainedFrames));
|
|
1584
|
+
}
|
|
1585
|
+
function resolveCoveredWaiters() {
|
|
1586
|
+
for (let index = waiters.length - 1; index >= 0; index -= 1) {
|
|
1587
|
+
const waiter = waiters[index];
|
|
1588
|
+
if (!waiter || !isRangeCovered(waiter.range, availableRanges)) {
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
waiters.splice(index, 1);
|
|
1592
|
+
waiter.resolve();
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function compactChangedRangeJournal() {
|
|
1596
|
+
const overflow = changedRanges.length - MAX_CHANGED_RANGE_JOURNAL_LENGTH;
|
|
1597
|
+
if (overflow <= 0) {
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
const removedRanges = changedRanges.splice(0, overflow);
|
|
1601
|
+
journalFloorVersion = Math.max(journalFloorVersion, removedRanges.at(-1)?.version ?? 0);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
function createDestroyedError() {
|
|
1605
|
+
return new Error("Detection frame source has been destroyed.");
|
|
1606
|
+
}
|
|
1607
|
+
function shouldApplyWindowRetention(retention) {
|
|
1608
|
+
return (retention?.mode === DetectionFrameRetentionMode.PersistWindow ||
|
|
1609
|
+
retention?.mode === DetectionFrameRetentionMode.MemoryOnly);
|
|
1610
|
+
}
|
|
1611
|
+
function getDetectionFrameRanges(frames) {
|
|
1612
|
+
return mergeRanges(frames.map((frame) => ({
|
|
1613
|
+
endTime: frame.endTime ?? frame.mediaTime,
|
|
1614
|
+
startTime: frame.mediaTime,
|
|
1615
|
+
})));
|
|
1616
|
+
}
|
|
1617
|
+
function rangesOverlap(left, right) {
|
|
1618
|
+
return left.startTime <= right.endTime && right.startTime <= left.endTime;
|
|
1619
|
+
}
|
|
1620
|
+
function mergeRanges(ranges) {
|
|
1621
|
+
const sortedRanges = ranges
|
|
1622
|
+
.map((range) => ({ ...range }))
|
|
1623
|
+
.sort((left, right) => left.startTime - right.startTime);
|
|
1624
|
+
const mergedRanges = [];
|
|
1625
|
+
for (const range of sortedRanges) {
|
|
1626
|
+
const previousRange = mergedRanges.at(-1);
|
|
1627
|
+
if (previousRange &&
|
|
1628
|
+
range.startTime <= previousRange.endTime + RANGE_EPSILON_SECONDS) {
|
|
1629
|
+
previousRange.endTime = Math.max(previousRange.endTime, range.endTime);
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
mergedRanges.push({ ...range });
|
|
1633
|
+
}
|
|
1634
|
+
return mergedRanges;
|
|
1635
|
+
}
|
|
1636
|
+
function recordAvailableRange(range, availableRanges) {
|
|
1637
|
+
if (range.endTime < range.startTime) {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
availableRanges.push({ ...range });
|
|
1641
|
+
availableRanges.sort((left, right) => left.startTime - right.startTime);
|
|
1642
|
+
let writeIndex = 0;
|
|
1643
|
+
for (const nextRange of availableRanges) {
|
|
1644
|
+
const currentRange = availableRanges[writeIndex - 1];
|
|
1645
|
+
if (currentRange &&
|
|
1646
|
+
nextRange.startTime <= currentRange.endTime + RANGE_EPSILON_SECONDS) {
|
|
1647
|
+
currentRange.endTime = Math.max(currentRange.endTime, nextRange.endTime);
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
availableRanges[writeIndex] = { ...nextRange };
|
|
1651
|
+
writeIndex += 1;
|
|
1652
|
+
}
|
|
1653
|
+
availableRanges.length = writeIndex;
|
|
1654
|
+
}
|
|
1655
|
+
function isRangeCovered(range, availableRanges) {
|
|
1656
|
+
return availableRanges.some((availableRange) => availableRange.startTime <= range.startTime + RANGE_EPSILON_SECONDS &&
|
|
1657
|
+
availableRange.endTime + RANGE_EPSILON_SECONDS >= range.endTime);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
var DetectionPickTarget;
|
|
1661
|
+
(function (DetectionPickTarget) {
|
|
1662
|
+
DetectionPickTarget["Box"] = "box";
|
|
1663
|
+
DetectionPickTarget["Edge"] = "edge";
|
|
1664
|
+
DetectionPickTarget["Keypoint"] = "keypoint";
|
|
1665
|
+
DetectionPickTarget["Label"] = "label";
|
|
1666
|
+
DetectionPickTarget["Mask"] = "mask";
|
|
1667
|
+
DetectionPickTarget["Polygon"] = "polygon";
|
|
1668
|
+
DetectionPickTarget["Polyline"] = "polyline";
|
|
1669
|
+
})(DetectionPickTarget || (DetectionPickTarget = {}));
|
|
1670
|
+
var MediaInteractionMode;
|
|
1671
|
+
(function (MediaInteractionMode) {
|
|
1672
|
+
MediaInteractionMode["Always"] = "always";
|
|
1673
|
+
MediaInteractionMode["Disabled"] = "disabled";
|
|
1674
|
+
MediaInteractionMode["PausedOnly"] = "pausedOnly";
|
|
1675
|
+
})(MediaInteractionMode || (MediaInteractionMode = {}));
|
|
1676
|
+
|
|
1677
|
+
function centerRectToTopLeftRect(rect) {
|
|
1678
|
+
return {
|
|
1679
|
+
height: rect.height,
|
|
1680
|
+
width: rect.width,
|
|
1681
|
+
x: rect.x - rect.width / 2,
|
|
1682
|
+
y: rect.y - rect.height / 2,
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
function topLeftRectToCenterRect(rect) {
|
|
1686
|
+
return {
|
|
1687
|
+
height: rect.height,
|
|
1688
|
+
width: rect.width,
|
|
1689
|
+
x: rect.x + rect.width / 2,
|
|
1690
|
+
y: rect.y + rect.height / 2,
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
function getPointsRect(points) {
|
|
1694
|
+
if (points.length === 0) {
|
|
1695
|
+
return undefined;
|
|
1696
|
+
}
|
|
1697
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
1698
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
1699
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
1700
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
1701
|
+
for (const point of points) {
|
|
1702
|
+
minX = Math.min(minX, point.x);
|
|
1703
|
+
minY = Math.min(minY, point.y);
|
|
1704
|
+
maxX = Math.max(maxX, point.x);
|
|
1705
|
+
maxY = Math.max(maxY, point.y);
|
|
1706
|
+
}
|
|
1707
|
+
return {
|
|
1708
|
+
height: maxY - minY,
|
|
1709
|
+
width: maxX - minX,
|
|
1710
|
+
x: (minX + maxX) / 2,
|
|
1711
|
+
y: (minY + maxY) / 2,
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
function getDetectionRect(detection) {
|
|
1715
|
+
return (detection.rect ??
|
|
1716
|
+
getPointsRect(detection.polygon?.points ?? []) ??
|
|
1717
|
+
getPointsRect(detection.polyline?.points ?? []) ??
|
|
1718
|
+
getPointsRect(detection.keypoints?.points ?? []));
|
|
1719
|
+
}
|
|
1720
|
+
function containsPoint(rect, point, padding = 0) {
|
|
1721
|
+
const halfWidth = rect.width / 2;
|
|
1722
|
+
const halfHeight = rect.height / 2;
|
|
1723
|
+
return (point.x >= rect.x - halfWidth - padding &&
|
|
1724
|
+
point.x <= rect.x + halfWidth + padding &&
|
|
1725
|
+
point.y >= rect.y - halfHeight - padding &&
|
|
1726
|
+
point.y <= rect.y + halfHeight + padding);
|
|
1727
|
+
}
|
|
1728
|
+
function pointInPolygon(point, polygon) {
|
|
1729
|
+
if (polygon.length < 3) {
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
let inside = false;
|
|
1733
|
+
for (let currentIndex = 0, previousIndex = polygon.length - 1; currentIndex < polygon.length; previousIndex = currentIndex, currentIndex += 1) {
|
|
1734
|
+
const current = polygon[currentIndex];
|
|
1735
|
+
const previous = polygon[previousIndex];
|
|
1736
|
+
const crossesRay = current.y > point.y !== previous.y > point.y &&
|
|
1737
|
+
point.x <
|
|
1738
|
+
((previous.x - current.x) * (point.y - current.y)) /
|
|
1739
|
+
(previous.y - current.y) +
|
|
1740
|
+
current.x;
|
|
1741
|
+
if (crossesRay) {
|
|
1742
|
+
inside = !inside;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return inside;
|
|
1746
|
+
}
|
|
1747
|
+
function distanceToSegment(point, start, end) {
|
|
1748
|
+
const deltaX = end.x - start.x;
|
|
1749
|
+
const deltaY = end.y - start.y;
|
|
1750
|
+
const lengthSquared = deltaX * deltaX + deltaY * deltaY;
|
|
1751
|
+
if (lengthSquared === 0) {
|
|
1752
|
+
return Math.hypot(point.x - start.x, point.y - start.y);
|
|
1753
|
+
}
|
|
1754
|
+
const projection = Math.max(0, Math.min(1, ((point.x - start.x) * deltaX + (point.y - start.y) * deltaY) /
|
|
1755
|
+
lengthSquared));
|
|
1756
|
+
const closestX = start.x + projection * deltaX;
|
|
1757
|
+
const closestY = start.y + projection * deltaY;
|
|
1758
|
+
return Math.hypot(point.x - closestX, point.y - closestY);
|
|
1759
|
+
}
|
|
1760
|
+
function polygonArea(points) {
|
|
1761
|
+
if (points.length < 3) {
|
|
1762
|
+
return 0;
|
|
1763
|
+
}
|
|
1764
|
+
let twiceArea = 0;
|
|
1765
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
1766
|
+
const current = points[index];
|
|
1767
|
+
const next = points[(index + 1) % points.length];
|
|
1768
|
+
twiceArea += current.x * next.y - next.x * current.y;
|
|
1769
|
+
}
|
|
1770
|
+
return Math.abs(twiceArea) / 2;
|
|
1771
|
+
}
|
|
1772
|
+
function rectArea(rect) {
|
|
1773
|
+
return rect ? Math.max(0, rect.width) * Math.max(0, rect.height) : 0;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
const decodedMaskCache = new WeakMap();
|
|
1777
|
+
function pickDetectionAtPoint(frame, point, options = {}) {
|
|
1778
|
+
if (!frame) {
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
const padding = Math.max(0, options.padding ?? 0);
|
|
1782
|
+
const polylinePadding = Math.max(0, options.polylinePadding ?? 6);
|
|
1783
|
+
const keypointPadding = Math.max(0, options.keypointPadding ?? 10);
|
|
1784
|
+
const edgePadding = Math.max(0, options.edgePadding ?? 8);
|
|
1785
|
+
const candidates = [];
|
|
1786
|
+
for (let detectionIndex = 0; detectionIndex < frame.detections.length; detectionIndex += 1) {
|
|
1787
|
+
const detection = frame.detections[detectionIndex];
|
|
1788
|
+
if (!detection ||
|
|
1789
|
+
(detection.locked && options.includeLocked === false) ||
|
|
1790
|
+
options.filter?.(detection, detectionIndex) === false) {
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
const detectionArea = Math.max(1, rectArea(getDetectionRect(detection)));
|
|
1794
|
+
const pushCandidate = (target, area = detectionArea, geometryIndex) => {
|
|
1795
|
+
candidates.push({
|
|
1796
|
+
area,
|
|
1797
|
+
priority: getPickTargetPriority(target),
|
|
1798
|
+
result: {
|
|
1799
|
+
detection,
|
|
1800
|
+
detectionIndex,
|
|
1801
|
+
frame,
|
|
1802
|
+
geometryIndex,
|
|
1803
|
+
mediaTime: frame.mediaTime,
|
|
1804
|
+
point,
|
|
1805
|
+
target,
|
|
1806
|
+
},
|
|
1807
|
+
zIndex: detection.zIndex ?? detectionIndex,
|
|
1808
|
+
});
|
|
1809
|
+
};
|
|
1810
|
+
if (detection.rect && containsPoint(detection.rect, point, padding)) {
|
|
1811
|
+
pushCandidate(DetectionPickTarget.Box, rectArea(detection.rect));
|
|
1812
|
+
}
|
|
1813
|
+
if (detection.polygon && pointInPolygon(point, detection.polygon.points)) {
|
|
1814
|
+
pushCandidate(DetectionPickTarget.Polygon, Math.max(1, polygonArea(detection.polygon.points)));
|
|
1815
|
+
}
|
|
1816
|
+
if (detection.polyline) {
|
|
1817
|
+
const segmentIndex = findHitSegment(point, detection.polyline.points, polylinePadding);
|
|
1818
|
+
if (segmentIndex !== -1) {
|
|
1819
|
+
pushCandidate(DetectionPickTarget.Polyline, detectionArea, segmentIndex);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
if (detection.keypoints) {
|
|
1823
|
+
const keypointIndex = detection.keypoints.points.findIndex((keypoint, index) => detection.keypoints?.visibility?.[index] !==
|
|
1824
|
+
KeypointVisibility.NotLabeled &&
|
|
1825
|
+
Math.hypot(point.x - keypoint.x, point.y - keypoint.y) <=
|
|
1826
|
+
keypointPadding);
|
|
1827
|
+
if (keypointIndex !== -1) {
|
|
1828
|
+
pushCandidate(DetectionPickTarget.Keypoint, Math.PI * keypointPadding * keypointPadding, keypointIndex);
|
|
1829
|
+
}
|
|
1830
|
+
else {
|
|
1831
|
+
const edgeIndex = detection.keypoints.edges.findIndex(([fromIndex, toIndex]) => {
|
|
1832
|
+
const from = detection.keypoints?.points[fromIndex];
|
|
1833
|
+
const to = detection.keypoints?.points[toIndex];
|
|
1834
|
+
return Boolean(from &&
|
|
1835
|
+
to &&
|
|
1836
|
+
detection.keypoints?.visibility?.[fromIndex] !==
|
|
1837
|
+
KeypointVisibility.NotLabeled &&
|
|
1838
|
+
detection.keypoints?.visibility?.[toIndex] !==
|
|
1839
|
+
KeypointVisibility.NotLabeled &&
|
|
1840
|
+
distanceToSegment(point, from, to) <= edgePadding);
|
|
1841
|
+
});
|
|
1842
|
+
if (edgeIndex !== -1) {
|
|
1843
|
+
const [fromIndex, toIndex] = detection.keypoints.edges[edgeIndex];
|
|
1844
|
+
const from = detection.keypoints.points[fromIndex];
|
|
1845
|
+
const to = detection.keypoints.points[toIndex];
|
|
1846
|
+
const edgeArea = Math.max(1, Math.hypot(to.x - from.x, to.y - from.y) * edgePadding * 2);
|
|
1847
|
+
pushCandidate(DetectionPickTarget.Edge, edgeArea, edgeIndex);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (detection.mask && options.includeMasks !== false) {
|
|
1852
|
+
const decoded = getDecodedMask(detection.mask);
|
|
1853
|
+
const mediaDimensions = options.maskMediaDimensions;
|
|
1854
|
+
const x = Math.floor(mediaDimensions && mediaDimensions.width > 0
|
|
1855
|
+
? (point.x / mediaDimensions.width) * decoded.width
|
|
1856
|
+
: point.x);
|
|
1857
|
+
const y = Math.floor(mediaDimensions && mediaDimensions.height > 0
|
|
1858
|
+
? (point.y / mediaDimensions.height) * decoded.height
|
|
1859
|
+
: point.y);
|
|
1860
|
+
if (x >= 0 &&
|
|
1861
|
+
y >= 0 &&
|
|
1862
|
+
x < decoded.width &&
|
|
1863
|
+
y < decoded.height &&
|
|
1864
|
+
decoded.data[y * decoded.width + x]) {
|
|
1865
|
+
pushCandidate(DetectionPickTarget.Mask, getMaskMediaArea(decoded, mediaDimensions));
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
candidates.sort((left, right) => {
|
|
1870
|
+
const priorityDifference = left.priority - right.priority;
|
|
1871
|
+
if (priorityDifference !== 0) {
|
|
1872
|
+
return priorityDifference;
|
|
1873
|
+
}
|
|
1874
|
+
const areaDifference = left.area - right.area;
|
|
1875
|
+
if (areaDifference !== 0) {
|
|
1876
|
+
return areaDifference;
|
|
1877
|
+
}
|
|
1878
|
+
const zIndexDifference = right.zIndex - left.zIndex;
|
|
1879
|
+
return zIndexDifference === 0
|
|
1880
|
+
? right.result.detectionIndex - left.result.detectionIndex
|
|
1881
|
+
: zIndexDifference;
|
|
1882
|
+
});
|
|
1883
|
+
return candidates[0]?.result ?? null;
|
|
1884
|
+
}
|
|
1885
|
+
function getPickTargetPriority(target) {
|
|
1886
|
+
switch (target) {
|
|
1887
|
+
case DetectionPickTarget.Keypoint:
|
|
1888
|
+
return 0;
|
|
1889
|
+
case DetectionPickTarget.Edge:
|
|
1890
|
+
case DetectionPickTarget.Polyline:
|
|
1891
|
+
return 1;
|
|
1892
|
+
default:
|
|
1893
|
+
return 2;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
function getDecodedMask(mask) {
|
|
1897
|
+
const cached = decodedMaskCache.get(mask);
|
|
1898
|
+
if (cached)
|
|
1899
|
+
return cached;
|
|
1900
|
+
const decoded = decodeCompressedRleMask(mask);
|
|
1901
|
+
const result = {
|
|
1902
|
+
...decoded,
|
|
1903
|
+
pixelArea: countMaskPixels(decoded.data),
|
|
1904
|
+
};
|
|
1905
|
+
decodedMaskCache.set(mask, result);
|
|
1906
|
+
return result;
|
|
1907
|
+
}
|
|
1908
|
+
function getMaskMediaArea(mask, mediaDimensions) {
|
|
1909
|
+
if (!mediaDimensions ||
|
|
1910
|
+
mediaDimensions.width <= 0 ||
|
|
1911
|
+
mediaDimensions.height <= 0) {
|
|
1912
|
+
return mask.pixelArea;
|
|
1913
|
+
}
|
|
1914
|
+
return (mask.pixelArea *
|
|
1915
|
+
(mediaDimensions.width / mask.width) *
|
|
1916
|
+
(mediaDimensions.height / mask.height));
|
|
1917
|
+
}
|
|
1918
|
+
function createDetectionPickKey(pick) {
|
|
1919
|
+
if (!pick) {
|
|
1920
|
+
return null;
|
|
1921
|
+
}
|
|
1922
|
+
const frameKey = [pick.frame.frameIndex ?? "time", pick.frame.mediaTime];
|
|
1923
|
+
const detectionKey = detectionPickKey(pick);
|
|
1924
|
+
return [
|
|
1925
|
+
...frameKey,
|
|
1926
|
+
...detectionKey,
|
|
1927
|
+
pick.target,
|
|
1928
|
+
pick.geometryIndex ?? "geometry",
|
|
1929
|
+
].join(":");
|
|
1930
|
+
}
|
|
1931
|
+
function rebaseDetectionPickToFrame(pick, frame) {
|
|
1932
|
+
if (!pick || !frame) {
|
|
1933
|
+
return null;
|
|
1934
|
+
}
|
|
1935
|
+
const detectionIndex = pick.detection.id === undefined ||
|
|
1936
|
+
!hasUniqueDetectionId(pick.frame, pick.detection.id)
|
|
1937
|
+
? pick.detectionIndex
|
|
1938
|
+
: frame.detections.findIndex((detection) => detection.id === pick.detection.id);
|
|
1939
|
+
const detection = frame.detections[detectionIndex];
|
|
1940
|
+
if (!detection) {
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
const rebasedPick = {
|
|
1944
|
+
detection,
|
|
1945
|
+
detectionIndex,
|
|
1946
|
+
frame,
|
|
1947
|
+
geometryIndex: pick.geometryIndex,
|
|
1948
|
+
mediaTime: frame.mediaTime,
|
|
1949
|
+
point: pick.point,
|
|
1950
|
+
target: pick.target,
|
|
1951
|
+
};
|
|
1952
|
+
return createDetectionPickKey(rebasedPick) === createDetectionPickKey(pick)
|
|
1953
|
+
? rebasedPick
|
|
1954
|
+
: null;
|
|
1955
|
+
}
|
|
1956
|
+
function detectionPickKey(pick) {
|
|
1957
|
+
if (pick.detection.id === undefined) {
|
|
1958
|
+
return ["anonymous", pick.detectionIndex];
|
|
1959
|
+
}
|
|
1960
|
+
return hasUniqueDetectionId(pick.frame, pick.detection.id)
|
|
1961
|
+
? ["id", String(pick.detection.id)]
|
|
1962
|
+
: ["duplicate-id", String(pick.detection.id), "index", pick.detectionIndex];
|
|
1963
|
+
}
|
|
1964
|
+
function hasUniqueDetectionId(frame, id) {
|
|
1965
|
+
let count = 0;
|
|
1966
|
+
for (const detection of frame.detections) {
|
|
1967
|
+
if (detection.id !== id)
|
|
1968
|
+
continue;
|
|
1969
|
+
count += 1;
|
|
1970
|
+
if (count > 1)
|
|
1971
|
+
return false;
|
|
1972
|
+
}
|
|
1973
|
+
return count === 1;
|
|
1974
|
+
}
|
|
1975
|
+
function pickDetectionByMaskId(frame, maskId, point) {
|
|
1976
|
+
if (!frame || maskId <= 0 || !Number.isInteger(maskId)) {
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
const detectionIndex = maskId - 1;
|
|
1980
|
+
const detection = frame.detections[detectionIndex];
|
|
1981
|
+
if (!detection) {
|
|
1982
|
+
return null;
|
|
1983
|
+
}
|
|
1984
|
+
return {
|
|
1985
|
+
detection,
|
|
1986
|
+
detectionIndex,
|
|
1987
|
+
frame,
|
|
1988
|
+
mediaTime: frame.mediaTime,
|
|
1989
|
+
point,
|
|
1990
|
+
target: DetectionPickTarget.Mask,
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function findHitSegment(point, points, padding, closed) {
|
|
1994
|
+
const segmentCount = points.length - 1;
|
|
1995
|
+
for (let index = 0; index < segmentCount; index += 1) {
|
|
1996
|
+
const start = points[index];
|
|
1997
|
+
const end = points[(index + 1) % points.length];
|
|
1998
|
+
if (start && end && distanceToSegment(point, start, end) <= padding) {
|
|
1999
|
+
return index;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
return -1;
|
|
2003
|
+
}
|
|
2004
|
+
function countMaskPixels(data) {
|
|
2005
|
+
let count = 0;
|
|
2006
|
+
for (const value of data) {
|
|
2007
|
+
count += value ? 1 : 0;
|
|
2008
|
+
}
|
|
2009
|
+
return Math.max(1, count);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
const MIN_SCALE = 0.1;
|
|
2013
|
+
const MAX_SCALE = 10;
|
|
2014
|
+
const MAX_WHEEL_DELTA = 50;
|
|
2015
|
+
function createViewportController(initial = {}) {
|
|
2016
|
+
let transform = freeze({
|
|
2017
|
+
locked: initial.locked ?? false,
|
|
2018
|
+
scale: clampScale(initial.scale ?? 0.8),
|
|
2019
|
+
x: initial.x ?? 0,
|
|
2020
|
+
y: initial.y ?? 0,
|
|
2021
|
+
});
|
|
2022
|
+
const listeners = new Set();
|
|
2023
|
+
const update = (next) => {
|
|
2024
|
+
if (next.scale === transform.scale &&
|
|
2025
|
+
next.x === transform.x &&
|
|
2026
|
+
next.y === transform.y &&
|
|
2027
|
+
next.locked === transform.locked)
|
|
2028
|
+
return;
|
|
2029
|
+
transform = freeze(next);
|
|
2030
|
+
for (const listener of listeners)
|
|
2031
|
+
listener(transform);
|
|
2032
|
+
};
|
|
2033
|
+
const controller = {
|
|
2034
|
+
getTransform: () => transform,
|
|
2035
|
+
screenToMedia: (point) => screenToMedia(point, transform),
|
|
2036
|
+
mediaToScreen: (point) => mediaToScreen(point, transform),
|
|
2037
|
+
setTransform(next) {
|
|
2038
|
+
if (transform.locked)
|
|
2039
|
+
return;
|
|
2040
|
+
update({
|
|
2041
|
+
...transform,
|
|
2042
|
+
...(next.scale === undefined ? {} : { scale: clampScale(next.scale) }),
|
|
2043
|
+
...(next.x === undefined ? {} : { x: next.x }),
|
|
2044
|
+
...(next.y === undefined ? {} : { y: next.y }),
|
|
2045
|
+
});
|
|
2046
|
+
},
|
|
2047
|
+
setLocked(locked) {
|
|
2048
|
+
update({ ...transform, locked });
|
|
2049
|
+
},
|
|
2050
|
+
panBy(dx, dy) {
|
|
2051
|
+
if (transform.locked)
|
|
2052
|
+
return;
|
|
2053
|
+
update({ ...transform, x: transform.x + dx, y: transform.y + dy });
|
|
2054
|
+
},
|
|
2055
|
+
zoomAt(point, factor) {
|
|
2056
|
+
if (transform.locked || !Number.isFinite(factor) || factor <= 0)
|
|
2057
|
+
return;
|
|
2058
|
+
const mediaPoint = screenToMedia(point, transform);
|
|
2059
|
+
const scale = clampScale(transform.scale * factor);
|
|
2060
|
+
update({
|
|
2061
|
+
...transform,
|
|
2062
|
+
scale,
|
|
2063
|
+
x: point.x - mediaPoint.x * scale,
|
|
2064
|
+
y: point.y - mediaPoint.y * scale,
|
|
2065
|
+
});
|
|
2066
|
+
},
|
|
2067
|
+
zoomFromWheel(point, deltaY) {
|
|
2068
|
+
const delta = Math.max(-MAX_WHEEL_DELTA, Math.min(MAX_WHEEL_DELTA, deltaY));
|
|
2069
|
+
controller.zoomAt(point, Math.exp(-delta * 0.01));
|
|
2070
|
+
},
|
|
2071
|
+
subscribe(listener) {
|
|
2072
|
+
listeners.add(listener);
|
|
2073
|
+
return () => listeners.delete(listener);
|
|
2074
|
+
},
|
|
2075
|
+
};
|
|
2076
|
+
return controller;
|
|
2077
|
+
}
|
|
2078
|
+
function screenToMedia(point, transform) {
|
|
2079
|
+
return {
|
|
2080
|
+
x: (point.x - transform.x) / transform.scale,
|
|
2081
|
+
y: (point.y - transform.y) / transform.scale,
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
function mediaToScreen(point, transform) {
|
|
2085
|
+
return {
|
|
2086
|
+
x: point.x * transform.scale + transform.x,
|
|
2087
|
+
y: point.y * transform.scale + transform.y,
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
function clampScale(scale) {
|
|
2091
|
+
return Math.max(MIN_SCALE, Math.min(MAX_SCALE, Number.isFinite(scale) ? scale : 1));
|
|
2092
|
+
}
|
|
2093
|
+
function freeze(transform) {
|
|
2094
|
+
return Object.freeze(transform);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
var AnnotationGeometryKind;
|
|
2098
|
+
(function (AnnotationGeometryKind) {
|
|
2099
|
+
AnnotationGeometryKind["Box"] = "box";
|
|
2100
|
+
AnnotationGeometryKind["Polygon"] = "polygon";
|
|
2101
|
+
AnnotationGeometryKind["Polyline"] = "polyline";
|
|
2102
|
+
AnnotationGeometryKind["Keypoints"] = "keypoints";
|
|
2103
|
+
AnnotationGeometryKind["Mask"] = "mask";
|
|
2104
|
+
})(AnnotationGeometryKind || (AnnotationGeometryKind = {}));
|
|
2105
|
+
var AnnotationGestureStateKind;
|
|
2106
|
+
(function (AnnotationGestureStateKind) {
|
|
2107
|
+
AnnotationGestureStateKind["Idle"] = "idle";
|
|
2108
|
+
AnnotationGestureStateKind["Creating"] = "creating";
|
|
2109
|
+
AnnotationGestureStateKind["Moving"] = "moving";
|
|
2110
|
+
AnnotationGestureStateKind["Resizing"] = "resizing";
|
|
2111
|
+
AnnotationGestureStateKind["DragSelecting"] = "dragSelecting";
|
|
2112
|
+
})(AnnotationGestureStateKind || (AnnotationGestureStateKind = {}));
|
|
2113
|
+
var AnnotationHandleKind;
|
|
2114
|
+
(function (AnnotationHandleKind) {
|
|
2115
|
+
AnnotationHandleKind["Resize"] = "resize";
|
|
2116
|
+
AnnotationHandleKind["Vertex"] = "vertex";
|
|
2117
|
+
AnnotationHandleKind["AddVertex"] = "addVertex";
|
|
2118
|
+
AnnotationHandleKind["Keypoint"] = "keypoint";
|
|
2119
|
+
})(AnnotationHandleKind || (AnnotationHandleKind = {}));
|
|
2120
|
+
|
|
2121
|
+
const HANDLE_RADIUS = 6;
|
|
2122
|
+
const ADD_HANDLE_RADIUS = 3.6;
|
|
2123
|
+
const HANDLE_HIT_SIZE = 16;
|
|
2124
|
+
function getAnnotationHandles(detection, viewportScale = 1) {
|
|
2125
|
+
if (detection.locked || detection.mask)
|
|
2126
|
+
return [];
|
|
2127
|
+
const radius = HANDLE_RADIUS / viewportScale;
|
|
2128
|
+
const hitSize = HANDLE_HIT_SIZE / viewportScale;
|
|
2129
|
+
if (detection.polygon)
|
|
2130
|
+
return getPathHandles(detection.polygon.points, true, radius, hitSize);
|
|
2131
|
+
if (detection.polyline)
|
|
2132
|
+
return getPathHandles(detection.polyline.points, false, radius, hitSize);
|
|
2133
|
+
if (detection.keypoints) {
|
|
2134
|
+
return detection.keypoints.points.map((point, geometryIndex) => ({
|
|
2135
|
+
cursor: "move",
|
|
2136
|
+
geometryIndex,
|
|
2137
|
+
hitSize,
|
|
2138
|
+
id: `kp-${geometryIndex}`,
|
|
2139
|
+
kind: AnnotationHandleKind.Keypoint,
|
|
2140
|
+
point,
|
|
2141
|
+
radius,
|
|
2142
|
+
}));
|
|
2143
|
+
}
|
|
2144
|
+
if (detection.rect)
|
|
2145
|
+
return getBoxHandles(detection.rect, radius, hitSize);
|
|
2146
|
+
return [];
|
|
2147
|
+
}
|
|
2148
|
+
function pickAnnotationHandle(handles, point) {
|
|
2149
|
+
return [...handles]
|
|
2150
|
+
.reverse()
|
|
2151
|
+
.find((handle) => Math.abs(point.x - handle.point.x) <= handle.hitSize / 2 &&
|
|
2152
|
+
Math.abs(point.y - handle.point.y) <= handle.hitSize / 2);
|
|
2153
|
+
}
|
|
2154
|
+
function applyAnnotationHandleDrag(detection, handle, point) {
|
|
2155
|
+
if (detection.rect && handle.kind === AnnotationHandleKind.Resize) {
|
|
2156
|
+
return { ...detection, rect: resizeRect(detection.rect, handle.id, point) };
|
|
2157
|
+
}
|
|
2158
|
+
if (handle.kind === AnnotationHandleKind.AddVertex &&
|
|
2159
|
+
handle.edgeIndex !== undefined) {
|
|
2160
|
+
const key = detection.polygon ? "polygon" : "polyline";
|
|
2161
|
+
const geometry = detection[key];
|
|
2162
|
+
if (!geometry)
|
|
2163
|
+
return detection;
|
|
2164
|
+
const points = [...geometry.points];
|
|
2165
|
+
points.splice(handle.edgeIndex + 1, 0, point);
|
|
2166
|
+
return { ...detection, [key]: { points } };
|
|
2167
|
+
}
|
|
2168
|
+
if (handle.geometryIndex === undefined)
|
|
2169
|
+
return detection;
|
|
2170
|
+
if (detection.polygon)
|
|
2171
|
+
return replacePathPoint(detection, "polygon", handle.geometryIndex, point);
|
|
2172
|
+
if (detection.polyline)
|
|
2173
|
+
return replacePathPoint(detection, "polyline", handle.geometryIndex, point);
|
|
2174
|
+
if (detection.keypoints) {
|
|
2175
|
+
const points = [...detection.keypoints.points];
|
|
2176
|
+
points[handle.geometryIndex] = point;
|
|
2177
|
+
return { ...detection, keypoints: { ...detection.keypoints, points } };
|
|
2178
|
+
}
|
|
2179
|
+
return detection;
|
|
2180
|
+
}
|
|
2181
|
+
function deleteAnnotationVertex(detection, vertexIndex) {
|
|
2182
|
+
const geometry = detection.polygon ?? detection.polyline;
|
|
2183
|
+
const minimum = detection.polygon ? 3 : 2;
|
|
2184
|
+
if (!geometry || geometry.points.length <= minimum)
|
|
2185
|
+
return null;
|
|
2186
|
+
const points = geometry.points.filter((_, index) => index !== vertexIndex);
|
|
2187
|
+
return detection.polygon
|
|
2188
|
+
? { ...detection, polygon: { points } }
|
|
2189
|
+
: { ...detection, polyline: { points } };
|
|
2190
|
+
}
|
|
2191
|
+
function offsetDetection(detection, dx, dy) {
|
|
2192
|
+
const offsetPoint = ({ x, y }) => ({ x: x + dx, y: y + dy });
|
|
2193
|
+
return {
|
|
2194
|
+
...detection,
|
|
2195
|
+
...(detection.rect
|
|
2196
|
+
? {
|
|
2197
|
+
rect: {
|
|
2198
|
+
...detection.rect,
|
|
2199
|
+
x: detection.rect.x + dx,
|
|
2200
|
+
y: detection.rect.y + dy,
|
|
2201
|
+
},
|
|
2202
|
+
}
|
|
2203
|
+
: {}),
|
|
2204
|
+
...(detection.polygon
|
|
2205
|
+
? { polygon: { points: detection.polygon.points.map(offsetPoint) } }
|
|
2206
|
+
: {}),
|
|
2207
|
+
...(detection.polyline
|
|
2208
|
+
? { polyline: { points: detection.polyline.points.map(offsetPoint) } }
|
|
2209
|
+
: {}),
|
|
2210
|
+
...(detection.keypoints
|
|
2211
|
+
? {
|
|
2212
|
+
keypoints: {
|
|
2213
|
+
...detection.keypoints,
|
|
2214
|
+
points: detection.keypoints.points.map(offsetPoint),
|
|
2215
|
+
},
|
|
2216
|
+
}
|
|
2217
|
+
: {}),
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
function getBoxHandles(rect, radius, hitSize) {
|
|
2221
|
+
const left = rect.x - rect.width / 2;
|
|
2222
|
+
const right = rect.x + rect.width / 2;
|
|
2223
|
+
const top = rect.y - rect.height / 2;
|
|
2224
|
+
const bottom = rect.y + rect.height / 2;
|
|
2225
|
+
const definitions = [
|
|
2226
|
+
["nw", { x: left, y: top }, "nwse-resize"],
|
|
2227
|
+
["n", { x: rect.x, y: top }, "ns-resize"],
|
|
2228
|
+
["ne", { x: right, y: top }, "nesw-resize"],
|
|
2229
|
+
["e", { x: right, y: rect.y }, "ew-resize"],
|
|
2230
|
+
["se", { x: right, y: bottom }, "nwse-resize"],
|
|
2231
|
+
["s", { x: rect.x, y: bottom }, "ns-resize"],
|
|
2232
|
+
["sw", { x: left, y: bottom }, "nesw-resize"],
|
|
2233
|
+
["w", { x: left, y: rect.y }, "ew-resize"],
|
|
2234
|
+
];
|
|
2235
|
+
return definitions.map(([id, point, cursor]) => ({
|
|
2236
|
+
cursor,
|
|
2237
|
+
hitSize,
|
|
2238
|
+
id,
|
|
2239
|
+
kind: AnnotationHandleKind.Resize,
|
|
2240
|
+
point,
|
|
2241
|
+
radius,
|
|
2242
|
+
}));
|
|
2243
|
+
}
|
|
2244
|
+
function getPathHandles(points, closed, radius, hitSize) {
|
|
2245
|
+
const handles = points.map((point, geometryIndex) => ({
|
|
2246
|
+
cursor: "move",
|
|
2247
|
+
geometryIndex,
|
|
2248
|
+
hitSize,
|
|
2249
|
+
id: `vertex-${geometryIndex}`,
|
|
2250
|
+
kind: AnnotationHandleKind.Vertex,
|
|
2251
|
+
point,
|
|
2252
|
+
radius,
|
|
2253
|
+
}));
|
|
2254
|
+
const edgeCount = closed ? points.length : Math.max(0, points.length - 1);
|
|
2255
|
+
for (let edgeIndex = 0; edgeIndex < edgeCount; edgeIndex += 1) {
|
|
2256
|
+
const from = points[edgeIndex];
|
|
2257
|
+
const to = points[(edgeIndex + 1) % points.length];
|
|
2258
|
+
handles.push({
|
|
2259
|
+
cursor: "copy",
|
|
2260
|
+
edgeIndex,
|
|
2261
|
+
hitSize,
|
|
2262
|
+
id: `add-${edgeIndex}`,
|
|
2263
|
+
kind: AnnotationHandleKind.AddVertex,
|
|
2264
|
+
point: { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 },
|
|
2265
|
+
radius: ADD_HANDLE_RADIUS / (HANDLE_RADIUS / radius),
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
return handles;
|
|
2269
|
+
}
|
|
2270
|
+
function resizeRect(rect, handle, point) {
|
|
2271
|
+
let left = rect.x - rect.width / 2;
|
|
2272
|
+
let right = rect.x + rect.width / 2;
|
|
2273
|
+
let top = rect.y - rect.height / 2;
|
|
2274
|
+
let bottom = rect.y + rect.height / 2;
|
|
2275
|
+
if (handle.includes("w"))
|
|
2276
|
+
left = Math.min(point.x, right - 5);
|
|
2277
|
+
if (handle.includes("e"))
|
|
2278
|
+
right = Math.max(point.x, left + 5);
|
|
2279
|
+
if (handle.includes("n"))
|
|
2280
|
+
top = Math.min(point.y, bottom - 5);
|
|
2281
|
+
if (handle.includes("s"))
|
|
2282
|
+
bottom = Math.max(point.y, top + 5);
|
|
2283
|
+
return {
|
|
2284
|
+
x: (left + right) / 2,
|
|
2285
|
+
y: (top + bottom) / 2,
|
|
2286
|
+
width: right - left,
|
|
2287
|
+
height: bottom - top,
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
function replacePathPoint(detection, key, index, point) {
|
|
2291
|
+
const geometry = detection[key];
|
|
2292
|
+
const points = [...geometry.points];
|
|
2293
|
+
points[index] = point;
|
|
2294
|
+
return { ...detection, [key]: { points } };
|
|
2295
|
+
}
|
|
2296
|
+
/** Finds the closest path segment, useful for contextual vertex insertion. */
|
|
2297
|
+
function findClosestAnnotationSegment(points, point, closed) {
|
|
2298
|
+
const count = closed ? points.length : points.length - 1;
|
|
2299
|
+
let best;
|
|
2300
|
+
for (let index = 0; index < count; index += 1) {
|
|
2301
|
+
const distance = distanceToSegment(point, points[index], points[(index + 1) % points.length]);
|
|
2302
|
+
if (!best || distance < best.distance)
|
|
2303
|
+
best = { index, distance };
|
|
2304
|
+
}
|
|
2305
|
+
return best;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
const MOVE_THRESHOLD = 4;
|
|
2309
|
+
const CLICK_CANCEL_MS = 250;
|
|
2310
|
+
const CLICK_CANCEL_DIAGONAL = 25;
|
|
2311
|
+
const CLOSE_ZONE_SCREEN_PX = 12;
|
|
2312
|
+
function createAnnotationEditingEngine(options = {}) {
|
|
2313
|
+
let tool = null;
|
|
2314
|
+
let gesture = null;
|
|
2315
|
+
let state = idleState();
|
|
2316
|
+
const stateListeners = new Set();
|
|
2317
|
+
const fastTranslateListeners = new Set();
|
|
2318
|
+
return {
|
|
2319
|
+
getState: () => state,
|
|
2320
|
+
setCreationTool(nextTool) {
|
|
2321
|
+
if (gesture)
|
|
2322
|
+
cancel();
|
|
2323
|
+
tool = nextTool;
|
|
2324
|
+
},
|
|
2325
|
+
pointerDown(input, pick = null) {
|
|
2326
|
+
if (input.button !== undefined && input.button !== 0)
|
|
2327
|
+
return;
|
|
2328
|
+
if (tool) {
|
|
2329
|
+
beginCreation(input);
|
|
2330
|
+
}
|
|
2331
|
+
else if (pick && !pick.detection.locked && !pick.detection.mask) {
|
|
2332
|
+
beginMove(pick, input);
|
|
2333
|
+
}
|
|
2334
|
+
},
|
|
2335
|
+
pointerMove(input) {
|
|
2336
|
+
if (!gesture)
|
|
2337
|
+
return;
|
|
2338
|
+
switch (gesture.kind) {
|
|
2339
|
+
case "box":
|
|
2340
|
+
gesture = { ...gesture, current: input };
|
|
2341
|
+
setPreview(resolveBoxPreview(gesture));
|
|
2342
|
+
break;
|
|
2343
|
+
case "move": {
|
|
2344
|
+
const dx = input.point.x - gesture.start.point.x;
|
|
2345
|
+
const dy = input.point.y - gesture.start.point.y;
|
|
2346
|
+
const moved = gesture.moved || Math.hypot(dx, dy) >= MOVE_THRESHOLD / scale();
|
|
2347
|
+
gesture = { ...gesture, current: input, moved };
|
|
2348
|
+
if (moved) {
|
|
2349
|
+
const preview = offsetDetection(gesture.detection, dx, dy);
|
|
2350
|
+
setPreview(preview);
|
|
2351
|
+
if (gesture.detection.id !== undefined) {
|
|
2352
|
+
options.onFastTranslate?.(gesture.detection.id, dx, dy);
|
|
2353
|
+
for (const listener of fastTranslateListeners) {
|
|
2354
|
+
listener(gesture.detection.id, dx, dy);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
break;
|
|
2359
|
+
}
|
|
2360
|
+
case "resize":
|
|
2361
|
+
gesture = { ...gesture, current: input };
|
|
2362
|
+
setPreview(applyAnnotationHandleDrag(gesture.detection, gesture.handle, input.point));
|
|
2363
|
+
break;
|
|
2364
|
+
case "path":
|
|
2365
|
+
setPreview(resolvePathPreview([...gesture.points, input.point]));
|
|
2366
|
+
break;
|
|
2367
|
+
case "freehand": {
|
|
2368
|
+
const previous = gesture.points.at(-1);
|
|
2369
|
+
if (!previous ||
|
|
2370
|
+
previous.x !== input.point.x ||
|
|
2371
|
+
previous.y !== input.point.y) {
|
|
2372
|
+
gesture.points.push(input.point);
|
|
2373
|
+
}
|
|
2374
|
+
gesture = { ...gesture, current: input };
|
|
2375
|
+
setPreview(resolvePathPreview(gesture.points));
|
|
2376
|
+
break;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
},
|
|
2380
|
+
pointerUp(input) {
|
|
2381
|
+
if (!gesture)
|
|
2382
|
+
return;
|
|
2383
|
+
if (gesture.kind === "path")
|
|
2384
|
+
return;
|
|
2385
|
+
const active = gesture;
|
|
2386
|
+
gesture = null;
|
|
2387
|
+
release(active.start.pointerId);
|
|
2388
|
+
if (active.kind === "box") {
|
|
2389
|
+
const preview = resolveBoxPreview({ ...active, current: input });
|
|
2390
|
+
const duration = input.timestamp - active.start.timestamp;
|
|
2391
|
+
const diagonal = Math.hypot(input.point.x - active.start.point.x, input.point.y - active.start.point.y) * scale();
|
|
2392
|
+
const rect = preview.rect;
|
|
2393
|
+
if (!rect ||
|
|
2394
|
+
rect.width < 1 ||
|
|
2395
|
+
rect.height < 1 ||
|
|
2396
|
+
(duration < CLICK_CANCEL_MS && diagonal <= CLICK_CANCEL_DIAGONAL)) {
|
|
2397
|
+
cancel();
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
if (tool?.shouldCommit?.(rect) === false) {
|
|
2401
|
+
cancel();
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
commit(preview, null);
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
if (active.kind === "freehand") {
|
|
2408
|
+
const points = [...active.points, input.point];
|
|
2409
|
+
if (points.length < 2 || tool?.shouldCommit?.(points) === false) {
|
|
2410
|
+
cancel();
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
commit(tool.createDetection(points), null);
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
if (active.kind === "move") {
|
|
2417
|
+
if (!active.moved) {
|
|
2418
|
+
setState(idleState());
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
const preview = offsetDetection(active.detection, input.point.x - active.start.point.x, input.point.y - active.start.point.y);
|
|
2422
|
+
commit(preview, active.detection);
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
const preview = applyAnnotationHandleDrag(active.detection, active.handle, input.point);
|
|
2426
|
+
commit(preview, active.detection);
|
|
2427
|
+
},
|
|
2428
|
+
keyDown(key) {
|
|
2429
|
+
if (key === "Escape") {
|
|
2430
|
+
cancel();
|
|
2431
|
+
}
|
|
2432
|
+
else if (key === "Enter" && gesture?.kind === "path") {
|
|
2433
|
+
commitPath();
|
|
2434
|
+
}
|
|
2435
|
+
},
|
|
2436
|
+
beginHandleDrag(detection, handle, input) {
|
|
2437
|
+
if (detection.locked || detection.mask)
|
|
2438
|
+
return;
|
|
2439
|
+
capture(input.pointerId);
|
|
2440
|
+
gesture = {
|
|
2441
|
+
current: input,
|
|
2442
|
+
detection,
|
|
2443
|
+
handle,
|
|
2444
|
+
kind: "resize",
|
|
2445
|
+
start: input,
|
|
2446
|
+
};
|
|
2447
|
+
setState({
|
|
2448
|
+
activeDetectionId: detection.id ?? null,
|
|
2449
|
+
activeHandleId: handle.id,
|
|
2450
|
+
kind: AnnotationGestureStateKind.Resizing,
|
|
2451
|
+
pointerId: input.pointerId ?? null,
|
|
2452
|
+
preview: detection,
|
|
2453
|
+
});
|
|
2454
|
+
},
|
|
2455
|
+
deleteVertex(detection, vertexIndex) {
|
|
2456
|
+
const next = deleteAnnotationVertex(detection, vertexIndex);
|
|
2457
|
+
if (next)
|
|
2458
|
+
commit(next, detection);
|
|
2459
|
+
return next;
|
|
2460
|
+
},
|
|
2461
|
+
cancel,
|
|
2462
|
+
hasCreationTool: () => tool !== null,
|
|
2463
|
+
subscribe(listener) {
|
|
2464
|
+
stateListeners.add(listener);
|
|
2465
|
+
return () => stateListeners.delete(listener);
|
|
2466
|
+
},
|
|
2467
|
+
subscribeFastTranslate(listener) {
|
|
2468
|
+
fastTranslateListeners.add(listener);
|
|
2469
|
+
return () => fastTranslateListeners.delete(listener);
|
|
2470
|
+
},
|
|
2471
|
+
};
|
|
2472
|
+
function beginCreation(input) {
|
|
2473
|
+
if (!tool)
|
|
2474
|
+
return;
|
|
2475
|
+
const mode = tool.mode ??
|
|
2476
|
+
(tool.geometry === AnnotationGeometryKind.Box
|
|
2477
|
+
? "drag"
|
|
2478
|
+
: tool.geometry === AnnotationGeometryKind.Mask
|
|
2479
|
+
? "freehand"
|
|
2480
|
+
: "multiClick");
|
|
2481
|
+
if (mode === "drag") {
|
|
2482
|
+
capture(input.pointerId);
|
|
2483
|
+
gesture = { current: input, kind: "box", start: input };
|
|
2484
|
+
const preview = resolveBoxPreview(gesture);
|
|
2485
|
+
options.onPreview?.(preview);
|
|
2486
|
+
setState({
|
|
2487
|
+
activeDetectionId: null,
|
|
2488
|
+
activeHandleId: null,
|
|
2489
|
+
kind: AnnotationGestureStateKind.Creating,
|
|
2490
|
+
pointerId: input.pointerId ?? null,
|
|
2491
|
+
preview,
|
|
2492
|
+
});
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
if (mode === "freehand") {
|
|
2496
|
+
capture(input.pointerId);
|
|
2497
|
+
gesture = {
|
|
2498
|
+
current: input,
|
|
2499
|
+
kind: "freehand",
|
|
2500
|
+
points: [input.point],
|
|
2501
|
+
start: input,
|
|
2502
|
+
};
|
|
2503
|
+
setPreview(resolvePathPreview([input.point]));
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
if (tool.geometry !== AnnotationGeometryKind.Polygon &&
|
|
2507
|
+
tool.geometry !== AnnotationGeometryKind.Polyline &&
|
|
2508
|
+
tool.geometry !== AnnotationGeometryKind.Keypoints)
|
|
2509
|
+
return;
|
|
2510
|
+
if (gesture?.kind !== "path") {
|
|
2511
|
+
capture(input.pointerId);
|
|
2512
|
+
gesture = {
|
|
2513
|
+
kind: "path",
|
|
2514
|
+
lastTimestamp: input.timestamp,
|
|
2515
|
+
pointerId: input.pointerId ?? null,
|
|
2516
|
+
points: [input.point],
|
|
2517
|
+
};
|
|
2518
|
+
setPreview(resolvePathPreview([input.point, input.point]));
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
const minimum = tool.minVertices ??
|
|
2522
|
+
(tool.geometry === AnnotationGeometryKind.Polygon ? 3 : 2);
|
|
2523
|
+
const closeDistance = Math.hypot(input.point.x - gesture.points[0].x, input.point.y - gesture.points[0].y) * scale();
|
|
2524
|
+
const doubleClick = input.detail !== undefined
|
|
2525
|
+
? input.detail >= 2
|
|
2526
|
+
: input.timestamp - gesture.lastTimestamp < CLICK_CANCEL_MS;
|
|
2527
|
+
if (gesture.points.length >= minimum &&
|
|
2528
|
+
((tool.geometry === AnnotationGeometryKind.Polygon &&
|
|
2529
|
+
closeDistance <= CLOSE_ZONE_SCREEN_PX) ||
|
|
2530
|
+
doubleClick)) {
|
|
2531
|
+
commitPath();
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
gesture.points.push(input.point);
|
|
2535
|
+
gesture.lastTimestamp = input.timestamp;
|
|
2536
|
+
setPreview(resolvePathPreview([...gesture.points, input.point]));
|
|
2537
|
+
}
|
|
2538
|
+
function beginMove(pick, input) {
|
|
2539
|
+
capture(input.pointerId);
|
|
2540
|
+
gesture = {
|
|
2541
|
+
current: input,
|
|
2542
|
+
detection: pick.detection,
|
|
2543
|
+
kind: "move",
|
|
2544
|
+
moved: false,
|
|
2545
|
+
start: input,
|
|
2546
|
+
};
|
|
2547
|
+
setState({
|
|
2548
|
+
activeDetectionId: pick.detection.id ?? null,
|
|
2549
|
+
activeHandleId: null,
|
|
2550
|
+
kind: AnnotationGestureStateKind.Moving,
|
|
2551
|
+
pointerId: input.pointerId ?? null,
|
|
2552
|
+
preview: null,
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
function resolveBoxPreview(active) {
|
|
2556
|
+
const left = Math.min(active.start.point.x, active.current.point.x);
|
|
2557
|
+
const right = Math.max(active.start.point.x, active.current.point.x);
|
|
2558
|
+
const top = Math.min(active.start.point.y, active.current.point.y);
|
|
2559
|
+
const bottom = Math.max(active.start.point.y, active.current.point.y);
|
|
2560
|
+
const rect = {
|
|
2561
|
+
x: (left + right) / 2,
|
|
2562
|
+
y: (top + bottom) / 2,
|
|
2563
|
+
width: right - left,
|
|
2564
|
+
height: bottom - top,
|
|
2565
|
+
};
|
|
2566
|
+
return tool.createDetection(rect);
|
|
2567
|
+
}
|
|
2568
|
+
function resolvePathPreview(points) {
|
|
2569
|
+
return tool.createDetection(points);
|
|
2570
|
+
}
|
|
2571
|
+
function commitPath() {
|
|
2572
|
+
if (!tool || gesture?.kind !== "path")
|
|
2573
|
+
return;
|
|
2574
|
+
const minimum = tool.minVertices ??
|
|
2575
|
+
(tool.geometry === AnnotationGeometryKind.Polygon ? 3 : 2);
|
|
2576
|
+
if (gesture.points.length < minimum) {
|
|
2577
|
+
cancel();
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
const preview = tool.createDetection(gesture.points);
|
|
2581
|
+
if (tool.shouldCommit?.(gesture.points) === false) {
|
|
2582
|
+
cancel();
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
release(gesture.pointerId ?? undefined);
|
|
2586
|
+
gesture = null;
|
|
2587
|
+
commit(preview, null);
|
|
2588
|
+
}
|
|
2589
|
+
function commit(detection, previous) {
|
|
2590
|
+
options.onCommit?.(detection, previous);
|
|
2591
|
+
options.onPreview?.(null);
|
|
2592
|
+
setState(idleState());
|
|
2593
|
+
}
|
|
2594
|
+
function setPreview(preview) {
|
|
2595
|
+
options.onPreview?.(preview);
|
|
2596
|
+
setState({
|
|
2597
|
+
activeDetectionId: state.activeDetectionId,
|
|
2598
|
+
activeHandleId: state.activeHandleId,
|
|
2599
|
+
kind: state.kind === AnnotationGestureStateKind.Idle
|
|
2600
|
+
? AnnotationGestureStateKind.Creating
|
|
2601
|
+
: state.kind,
|
|
2602
|
+
pointerId: state.pointerId ??
|
|
2603
|
+
(gesture && "pointerId" in gesture
|
|
2604
|
+
? (gesture.pointerId ?? null)
|
|
2605
|
+
: null),
|
|
2606
|
+
preview,
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
function cancel() {
|
|
2610
|
+
const pointerId = gesture && "start" in gesture
|
|
2611
|
+
? gesture.start.pointerId
|
|
2612
|
+
: gesture?.kind === "path"
|
|
2613
|
+
? (gesture.pointerId ?? undefined)
|
|
2614
|
+
: undefined;
|
|
2615
|
+
release(pointerId);
|
|
2616
|
+
gesture = null;
|
|
2617
|
+
options.onPreview?.(null);
|
|
2618
|
+
options.onCancel?.();
|
|
2619
|
+
setState(idleState());
|
|
2620
|
+
}
|
|
2621
|
+
function setState(next) {
|
|
2622
|
+
state = Object.freeze(next);
|
|
2623
|
+
options.onStateChange?.(state);
|
|
2624
|
+
for (const listener of stateListeners)
|
|
2625
|
+
listener(state);
|
|
2626
|
+
}
|
|
2627
|
+
function scale() {
|
|
2628
|
+
return Math.max(options.viewportScale?.() ?? 1, Number.EPSILON);
|
|
2629
|
+
}
|
|
2630
|
+
function capture(pointerId) {
|
|
2631
|
+
if (pointerId !== undefined)
|
|
2632
|
+
options.capturePointer?.(pointerId);
|
|
2633
|
+
}
|
|
2634
|
+
function release(pointerId) {
|
|
2635
|
+
if (pointerId !== undefined)
|
|
2636
|
+
options.releasePointer?.(pointerId);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
function idleState() {
|
|
2640
|
+
return Object.freeze({
|
|
2641
|
+
activeDetectionId: null,
|
|
2642
|
+
activeHandleId: null,
|
|
2643
|
+
kind: AnnotationGestureStateKind.Idle,
|
|
2644
|
+
pointerId: null,
|
|
2645
|
+
preview: null,
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
function resolveStyleValue(value, detection, context) {
|
|
2650
|
+
return typeof value === "function"
|
|
2651
|
+
? value(detection, context)
|
|
2652
|
+
: value;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
var BoxShape;
|
|
2656
|
+
(function (BoxShape) {
|
|
2657
|
+
BoxShape["Rect"] = "rect";
|
|
2658
|
+
BoxShape["RoundedRect"] = "roundedRect";
|
|
2659
|
+
})(BoxShape || (BoxShape = {}));
|
|
2660
|
+
var BoxStrokeAlignment;
|
|
2661
|
+
(function (BoxStrokeAlignment) {
|
|
2662
|
+
BoxStrokeAlignment["Inside"] = "inside";
|
|
2663
|
+
BoxStrokeAlignment["Center"] = "center";
|
|
2664
|
+
BoxStrokeAlignment["Outside"] = "outside";
|
|
2665
|
+
})(BoxStrokeAlignment || (BoxStrokeAlignment = {}));
|
|
2666
|
+
|
|
2667
|
+
const DEFAULT_BOX_STROKE_ALPHA = 1;
|
|
2668
|
+
const DEFAULT_BOX_STROKE_COLOR = 0x00ff66;
|
|
2669
|
+
const DEFAULT_BOX_STROKE_WIDTH = 2;
|
|
2670
|
+
/**
|
|
2671
|
+
* Default configurable box style.
|
|
2672
|
+
*
|
|
2673
|
+
* This is the simplest `supervision-js` equivalent of a box annotator: it
|
|
2674
|
+
* converts detections with `rect` geometry into renderer-neutral box draw
|
|
2675
|
+
* instructions. Use options such as `shape`, `cornerRadius`, `stroke`, and
|
|
2676
|
+
* `fill` for static or per-detection styling.
|
|
2677
|
+
*/
|
|
2678
|
+
class BaseBoxStyle {
|
|
2679
|
+
options;
|
|
2680
|
+
constructor(options = {}) {
|
|
2681
|
+
this.options = options;
|
|
2682
|
+
}
|
|
2683
|
+
resolve(detection, context) {
|
|
2684
|
+
if (!detection.rect ||
|
|
2685
|
+
context.hidden ||
|
|
2686
|
+
this.options.shouldRender?.(detection, context) === false) {
|
|
2687
|
+
return undefined;
|
|
2688
|
+
}
|
|
2689
|
+
const shape = this.resolveShape(detection, context);
|
|
2690
|
+
const cornerRadius = this.resolveCornerRadius(detection, context, shape);
|
|
2691
|
+
const instruction = {
|
|
2692
|
+
fill: this.resolveFill(detection, context),
|
|
2693
|
+
rect: detection.rect,
|
|
2694
|
+
shape,
|
|
2695
|
+
stroke: this.resolveStroke(detection, context),
|
|
2696
|
+
};
|
|
2697
|
+
if (cornerRadius !== undefined) {
|
|
2698
|
+
return {
|
|
2699
|
+
...instruction,
|
|
2700
|
+
cornerRadius,
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
return instruction;
|
|
2704
|
+
}
|
|
2705
|
+
resolveShape(detection, context) {
|
|
2706
|
+
return (resolveStyleValue(this.options.shape, detection, context) ?? BoxShape.Rect);
|
|
2707
|
+
}
|
|
2708
|
+
resolveCornerRadius(detection, context, shape) {
|
|
2709
|
+
if (shape !== BoxShape.RoundedRect) {
|
|
2710
|
+
return undefined;
|
|
2711
|
+
}
|
|
2712
|
+
return resolveStyleValue(this.options.cornerRadius, detection, context);
|
|
2713
|
+
}
|
|
2714
|
+
resolveStroke(detection, context) {
|
|
2715
|
+
const stroke = resolveStyleValue(this.options.stroke, detection, context);
|
|
2716
|
+
if (stroke === null) {
|
|
2717
|
+
return undefined;
|
|
2718
|
+
}
|
|
2719
|
+
const resolvedStroke = {
|
|
2720
|
+
alpha: stroke?.alpha ?? DEFAULT_BOX_STROKE_ALPHA,
|
|
2721
|
+
color: stroke?.color ?? DEFAULT_BOX_STROKE_COLOR,
|
|
2722
|
+
width: stroke?.width ?? DEFAULT_BOX_STROKE_WIDTH,
|
|
2723
|
+
};
|
|
2724
|
+
return {
|
|
2725
|
+
...resolvedStroke,
|
|
2726
|
+
...(stroke?.alignment === undefined
|
|
2727
|
+
? {}
|
|
2728
|
+
: { alignment: stroke.alignment }),
|
|
2729
|
+
...(stroke?.dash === undefined
|
|
2730
|
+
? context.ephemeral
|
|
2731
|
+
? { dash: [6, 4] }
|
|
2732
|
+
: {}
|
|
2733
|
+
: { dash: stroke.dash }),
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
resolveFill(detection, context) {
|
|
2737
|
+
const fill = resolveStyleValue(this.options.fill, detection, context);
|
|
2738
|
+
if (fill === null || fill === undefined) {
|
|
2739
|
+
return undefined;
|
|
2740
|
+
}
|
|
2741
|
+
return {
|
|
2742
|
+
alpha: fill.alpha ?? 1,
|
|
2743
|
+
color: fill.color ?? DEFAULT_BOX_STROKE_COLOR,
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
var FocusTargetMode;
|
|
2749
|
+
(function (FocusTargetMode) {
|
|
2750
|
+
FocusTargetMode["Hovered"] = "hovered";
|
|
2751
|
+
FocusTargetMode["Selected"] = "selected";
|
|
2752
|
+
FocusTargetMode["HoveredAndSelected"] = "hoveredAndSelected";
|
|
2753
|
+
FocusTargetMode["Ambient"] = "ambient";
|
|
2754
|
+
})(FocusTargetMode || (FocusTargetMode = {}));
|
|
2755
|
+
|
|
2756
|
+
const DEFAULT_FOCUS_FILL_COLOR = 0x020617;
|
|
2757
|
+
const DEFAULT_FOCUS_FILL_ALPHA = 0.45;
|
|
2758
|
+
const DEFAULT_FOCUS_CORNER_RADIUS = 8;
|
|
2759
|
+
/**
|
|
2760
|
+
* Default configurable focus style.
|
|
2761
|
+
*
|
|
2762
|
+
* Focus styles dim the current media frame while leaving the selected or
|
|
2763
|
+
* hovered detections visible. Renderers may use prepared mask artifacts for
|
|
2764
|
+
* shape-accurate cutouts, falling back to detection rectangles when needed.
|
|
2765
|
+
*/
|
|
2766
|
+
class BaseFocusStyle {
|
|
2767
|
+
options;
|
|
2768
|
+
constructor(options = {}) {
|
|
2769
|
+
this.options = options;
|
|
2770
|
+
}
|
|
2771
|
+
resolve(context) {
|
|
2772
|
+
if (this.options.shouldRender?.(context) === false) {
|
|
2773
|
+
return undefined;
|
|
2774
|
+
}
|
|
2775
|
+
const targetMode = this.options.targetMode ?? FocusTargetMode.Selected;
|
|
2776
|
+
const targets = getFocusTargets(context, targetMode);
|
|
2777
|
+
const ambient = targetMode === FocusTargetMode.Ambient &&
|
|
2778
|
+
!context.selectedPick &&
|
|
2779
|
+
!context.hoveredPick;
|
|
2780
|
+
const fill = resolveFocusStyleValue(this.options.fill, context);
|
|
2781
|
+
if (targets.length === 0 || fill === null) {
|
|
2782
|
+
return undefined;
|
|
2783
|
+
}
|
|
2784
|
+
return {
|
|
2785
|
+
fallback: this.resolveFallback(context),
|
|
2786
|
+
fill: {
|
|
2787
|
+
alpha: fill?.alpha ?? DEFAULT_FOCUS_FILL_ALPHA,
|
|
2788
|
+
color: fill?.color ?? DEFAULT_FOCUS_FILL_COLOR,
|
|
2789
|
+
},
|
|
2790
|
+
targetMode,
|
|
2791
|
+
targets,
|
|
2792
|
+
...(ambient ? { ambient: true } : {}),
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
resolveFallback(context) {
|
|
2796
|
+
const shape = resolveFocusStyleValue(this.options.shape, context) ??
|
|
2797
|
+
BoxShape.RoundedRect;
|
|
2798
|
+
const cornerRadius = shape === BoxShape.RoundedRect
|
|
2799
|
+
? (resolveFocusStyleValue(this.options.cornerRadius, context) ??
|
|
2800
|
+
DEFAULT_FOCUS_CORNER_RADIUS)
|
|
2801
|
+
: undefined;
|
|
2802
|
+
if (cornerRadius === undefined) {
|
|
2803
|
+
return { shape };
|
|
2804
|
+
}
|
|
2805
|
+
return {
|
|
2806
|
+
cornerRadius,
|
|
2807
|
+
shape,
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
function getFocusTargets(context, targetMode) {
|
|
2812
|
+
const targets = [];
|
|
2813
|
+
if (targetMode === FocusTargetMode.Ambient) {
|
|
2814
|
+
if (context.selectedPick)
|
|
2815
|
+
return [context.selectedPick];
|
|
2816
|
+
if (context.hoveredPick)
|
|
2817
|
+
return [context.hoveredPick];
|
|
2818
|
+
return context.frame.detections.map((detection, detectionIndex) => ({
|
|
2819
|
+
detection,
|
|
2820
|
+
detectionIndex,
|
|
2821
|
+
frame: context.frame,
|
|
2822
|
+
mediaTime: context.mediaTime,
|
|
2823
|
+
point: detection.rect
|
|
2824
|
+
? { x: detection.rect.x, y: detection.rect.y }
|
|
2825
|
+
: { x: 0, y: 0 },
|
|
2826
|
+
target: detection.mask
|
|
2827
|
+
? DetectionPickTarget.Mask
|
|
2828
|
+
: detection.polygon
|
|
2829
|
+
? DetectionPickTarget.Polygon
|
|
2830
|
+
: DetectionPickTarget.Box,
|
|
2831
|
+
}));
|
|
2832
|
+
}
|
|
2833
|
+
if ((targetMode === FocusTargetMode.Selected ||
|
|
2834
|
+
targetMode === FocusTargetMode.HoveredAndSelected) &&
|
|
2835
|
+
context.selectedPick) {
|
|
2836
|
+
targets.push(context.selectedPick);
|
|
2837
|
+
}
|
|
2838
|
+
if ((targetMode === FocusTargetMode.Hovered ||
|
|
2839
|
+
targetMode === FocusTargetMode.HoveredAndSelected) &&
|
|
2840
|
+
context.hoveredPick) {
|
|
2841
|
+
targets.push(context.hoveredPick);
|
|
2842
|
+
}
|
|
2843
|
+
return dedupeTargetsForFrame(targets, context.frame);
|
|
2844
|
+
}
|
|
2845
|
+
function dedupeTargetsForFrame(targets, frame) {
|
|
2846
|
+
const keys = new Set();
|
|
2847
|
+
const dedupedTargets = [];
|
|
2848
|
+
for (const target of targets) {
|
|
2849
|
+
if (target.frame !== frame) {
|
|
2850
|
+
continue;
|
|
2851
|
+
}
|
|
2852
|
+
const key = `${target.detectionIndex}:${target.target}`;
|
|
2853
|
+
if (keys.has(key)) {
|
|
2854
|
+
continue;
|
|
2855
|
+
}
|
|
2856
|
+
keys.add(key);
|
|
2857
|
+
dedupedTargets.push(target);
|
|
2858
|
+
}
|
|
2859
|
+
return dedupedTargets;
|
|
2860
|
+
}
|
|
2861
|
+
function resolveFocusStyleValue(value, context) {
|
|
2862
|
+
if (typeof value === "function") {
|
|
2863
|
+
return value(context);
|
|
2864
|
+
}
|
|
2865
|
+
return value;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
var DetectionInteractionState;
|
|
2869
|
+
(function (DetectionInteractionState) {
|
|
2870
|
+
DetectionInteractionState["Hovered"] = "hovered";
|
|
2871
|
+
DetectionInteractionState["Selected"] = "selected";
|
|
2872
|
+
})(DetectionInteractionState || (DetectionInteractionState = {}));
|
|
2873
|
+
|
|
2874
|
+
const DEFAULT_HOVER_FILL_COLOR = 0x67e8f9;
|
|
2875
|
+
const DEFAULT_HOVER_FILL_ALPHA = 0.1;
|
|
2876
|
+
const DEFAULT_HOVER_STROKE_COLOR = 0x67e8f9;
|
|
2877
|
+
const DEFAULT_HOVER_STROKE_ALPHA = 0.95;
|
|
2878
|
+
const DEFAULT_HOVER_STROKE_WIDTH = 3;
|
|
2879
|
+
const DEFAULT_SELECTED_FILL_COLOR = 0xfde047;
|
|
2880
|
+
const DEFAULT_SELECTED_FILL_ALPHA = 0.18;
|
|
2881
|
+
const DEFAULT_SELECTED_STROKE_COLOR = 0xfde047;
|
|
2882
|
+
const DEFAULT_SELECTED_STROKE_ALPHA = 1;
|
|
2883
|
+
const DEFAULT_SELECTED_STROKE_WIDTH = 4;
|
|
2884
|
+
/**
|
|
2885
|
+
* Default configurable interaction style.
|
|
2886
|
+
*
|
|
2887
|
+
* It resolves hover and selected picks into state-specific presentations using
|
|
2888
|
+
* the same style contracts as normal boxes, masks, and labels. The older
|
|
2889
|
+
* rectangle options are kept as compatibility sugar and resolve to a box style.
|
|
2890
|
+
*/
|
|
2891
|
+
class BaseInteractionStyle {
|
|
2892
|
+
options;
|
|
2893
|
+
constructor(options = {}) {
|
|
2894
|
+
this.options = options;
|
|
2895
|
+
}
|
|
2896
|
+
resolve(detection, context) {
|
|
2897
|
+
if (this.options.shouldRender?.(detection, context) === false) {
|
|
2898
|
+
return undefined;
|
|
2899
|
+
}
|
|
2900
|
+
const statePresentation = context.state === DetectionInteractionState.Selected
|
|
2901
|
+
? this.options.selected
|
|
2902
|
+
: this.options.hovered;
|
|
2903
|
+
if (statePresentation !== undefined) {
|
|
2904
|
+
return statePresentation ?? undefined;
|
|
2905
|
+
}
|
|
2906
|
+
const rect = detection.rect;
|
|
2907
|
+
if (!rect) {
|
|
2908
|
+
return undefined;
|
|
2909
|
+
}
|
|
2910
|
+
return {
|
|
2911
|
+
boxStyle: createResolvedBoxStyle(this.resolveBoxInstruction(detection, context, rect)),
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
resolveBoxInstruction(detection, context, rect) {
|
|
2915
|
+
const shape = this.resolveShape(detection, context);
|
|
2916
|
+
const cornerRadius = this.resolveCornerRadius(detection, context, shape);
|
|
2917
|
+
const instruction = {
|
|
2918
|
+
fill: this.resolveFill(detection, context),
|
|
2919
|
+
rect,
|
|
2920
|
+
shape,
|
|
2921
|
+
stroke: this.resolveStroke(detection, context),
|
|
2922
|
+
};
|
|
2923
|
+
if (cornerRadius !== undefined) {
|
|
2924
|
+
return {
|
|
2925
|
+
...instruction,
|
|
2926
|
+
cornerRadius,
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
return instruction;
|
|
2930
|
+
}
|
|
2931
|
+
resolveShape(detection, context) {
|
|
2932
|
+
return (resolveStyleValue(this.options.shape, detection, context) ?? BoxShape.Rect);
|
|
2933
|
+
}
|
|
2934
|
+
resolveCornerRadius(detection, context, shape) {
|
|
2935
|
+
if (shape !== BoxShape.RoundedRect) {
|
|
2936
|
+
return undefined;
|
|
2937
|
+
}
|
|
2938
|
+
return resolveStyleValue(this.options.cornerRadius, detection, context);
|
|
2939
|
+
}
|
|
2940
|
+
resolveStroke(detection, context) {
|
|
2941
|
+
const stroke = resolveStyleValue(this.options.stroke, detection, context);
|
|
2942
|
+
if (stroke === null) {
|
|
2943
|
+
return undefined;
|
|
2944
|
+
}
|
|
2945
|
+
const defaults = getStateDefaults(context.state);
|
|
2946
|
+
const resolvedStroke = {
|
|
2947
|
+
alpha: stroke?.alpha ?? defaults.strokeAlpha,
|
|
2948
|
+
color: stroke?.color ?? defaults.strokeColor,
|
|
2949
|
+
width: stroke?.width ?? defaults.strokeWidth,
|
|
2950
|
+
};
|
|
2951
|
+
if (stroke?.alignment !== undefined) {
|
|
2952
|
+
return {
|
|
2953
|
+
...resolvedStroke,
|
|
2954
|
+
alignment: stroke.alignment,
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
return resolvedStroke;
|
|
2958
|
+
}
|
|
2959
|
+
resolveFill(detection, context) {
|
|
2960
|
+
const fill = resolveStyleValue(this.options.fill, detection, context);
|
|
2961
|
+
if (fill === null) {
|
|
2962
|
+
return undefined;
|
|
2963
|
+
}
|
|
2964
|
+
const defaults = getStateDefaults(context.state);
|
|
2965
|
+
return {
|
|
2966
|
+
alpha: fill?.alpha ?? defaults.fillAlpha,
|
|
2967
|
+
color: fill?.color ?? defaults.fillColor,
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
function createResolvedBoxStyle(instruction) {
|
|
2972
|
+
return {
|
|
2973
|
+
resolve() {
|
|
2974
|
+
return instruction;
|
|
2975
|
+
},
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
function getStateDefaults(state) {
|
|
2979
|
+
if (state === DetectionInteractionState.Selected) {
|
|
2980
|
+
return {
|
|
2981
|
+
fillAlpha: DEFAULT_SELECTED_FILL_ALPHA,
|
|
2982
|
+
fillColor: DEFAULT_SELECTED_FILL_COLOR,
|
|
2983
|
+
strokeAlpha: DEFAULT_SELECTED_STROKE_ALPHA,
|
|
2984
|
+
strokeColor: DEFAULT_SELECTED_STROKE_COLOR,
|
|
2985
|
+
strokeWidth: DEFAULT_SELECTED_STROKE_WIDTH,
|
|
2986
|
+
};
|
|
2987
|
+
}
|
|
2988
|
+
return {
|
|
2989
|
+
fillAlpha: DEFAULT_HOVER_FILL_ALPHA,
|
|
2990
|
+
fillColor: DEFAULT_HOVER_FILL_COLOR,
|
|
2991
|
+
strokeAlpha: DEFAULT_HOVER_STROKE_ALPHA,
|
|
2992
|
+
strokeColor: DEFAULT_HOVER_STROKE_COLOR,
|
|
2993
|
+
strokeWidth: DEFAULT_HOVER_STROKE_WIDTH,
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
var LabelPlacement;
|
|
2998
|
+
(function (LabelPlacement) {
|
|
2999
|
+
LabelPlacement["Top"] = "top";
|
|
3000
|
+
LabelPlacement["Bottom"] = "bottom";
|
|
3001
|
+
LabelPlacement["InsideTop"] = "insideTop";
|
|
3002
|
+
LabelPlacement["InsideBottom"] = "insideBottom";
|
|
3003
|
+
LabelPlacement["Center"] = "center";
|
|
3004
|
+
})(LabelPlacement || (LabelPlacement = {}));
|
|
3005
|
+
var LabelVisibilityMode;
|
|
3006
|
+
(function (LabelVisibilityMode) {
|
|
3007
|
+
LabelVisibilityMode["Always"] = "always";
|
|
3008
|
+
LabelVisibilityMode["HoveredOnly"] = "hoveredOnly";
|
|
3009
|
+
})(LabelVisibilityMode || (LabelVisibilityMode = {}));
|
|
3010
|
+
|
|
3011
|
+
/** Returns black or white for readable text over an RGB background. */
|
|
3012
|
+
function resolveContrastTextColor(color) {
|
|
3013
|
+
"worklet";
|
|
3014
|
+
const red = (color >> 16) & 0xff;
|
|
3015
|
+
const green = (color >> 8) & 0xff;
|
|
3016
|
+
const blue = color & 0xff;
|
|
3017
|
+
const luminance = (red * 299 + green * 587 + blue * 114) / 1000;
|
|
3018
|
+
return luminance >= 150 ? 0x111111 : 0xffffff;
|
|
3019
|
+
}
|
|
3020
|
+
function lightenColor(color, amount = 0.14) {
|
|
3021
|
+
"worklet";
|
|
3022
|
+
const mix = (value) => Math.round(value + (255 - value) * amount);
|
|
3023
|
+
return ((mix((color >> 16) & 0xff) << 16) |
|
|
3024
|
+
(mix((color >> 8) & 0xff) << 8) |
|
|
3025
|
+
mix(color & 0xff));
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
/**
|
|
3029
|
+
* Default label style.
|
|
3030
|
+
*
|
|
3031
|
+
* Resolves labels from `className`, `metadata.label`, or a custom text
|
|
3032
|
+
* resolver, optionally including confidence.
|
|
3033
|
+
*/
|
|
3034
|
+
class BaseLabelStyle {
|
|
3035
|
+
includeConfidence;
|
|
3036
|
+
offsetY;
|
|
3037
|
+
options;
|
|
3038
|
+
constructor(options = {}) {
|
|
3039
|
+
this.options = options;
|
|
3040
|
+
this.includeConfidence = options.includeConfidence ?? false;
|
|
3041
|
+
this.offsetY = options.offsetY ?? 0;
|
|
3042
|
+
}
|
|
3043
|
+
resolve(detection, context) {
|
|
3044
|
+
if (!getDetectionRect(detection) ||
|
|
3045
|
+
context.hidden ||
|
|
3046
|
+
(this.options.visibilityMode === LabelVisibilityMode.HoveredOnly &&
|
|
3047
|
+
!context.hovered) ||
|
|
3048
|
+
this.options.shouldRender?.(detection, context) === false) {
|
|
3049
|
+
return undefined;
|
|
3050
|
+
}
|
|
3051
|
+
const text = resolveStyleValue(this.options.text, detection, context) ??
|
|
3052
|
+
formatDetectionLabel(detection, this.includeConfidence);
|
|
3053
|
+
if (!text) {
|
|
3054
|
+
return undefined;
|
|
3055
|
+
}
|
|
3056
|
+
const background = this.resolveBackground(detection, context);
|
|
3057
|
+
return {
|
|
3058
|
+
background,
|
|
3059
|
+
...this.resolveOffset(detection, context),
|
|
3060
|
+
placement: this.resolvePlacement(detection, context),
|
|
3061
|
+
rect: getDetectionRect(detection),
|
|
3062
|
+
text,
|
|
3063
|
+
textStyle: this.resolveTextStyle(detection, context, background),
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
resolveBackground(detection, context) {
|
|
3067
|
+
const background = resolveStyleValue(this.options.background, detection, context);
|
|
3068
|
+
return {
|
|
3069
|
+
alpha: background?.alpha ?? 0.72,
|
|
3070
|
+
color: context.hovered
|
|
3071
|
+
? lightenColor(background?.color ?? 0x111827)
|
|
3072
|
+
: (background?.color ?? 0x111827),
|
|
3073
|
+
cornerRadius: background?.cornerRadius ?? 4,
|
|
3074
|
+
paddingX: background?.paddingX ?? 6,
|
|
3075
|
+
paddingY: background?.paddingY ?? 3,
|
|
3076
|
+
...(background?.topCornersOnly === undefined
|
|
3077
|
+
? {}
|
|
3078
|
+
: { topCornersOnly: background.topCornersOnly }),
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
resolveTextStyle(detection, context, background) {
|
|
3082
|
+
const textStyle = resolveStyleValue(this.options.textStyle, detection, context);
|
|
3083
|
+
return {
|
|
3084
|
+
alpha: textStyle?.alpha ?? 1,
|
|
3085
|
+
color: textStyle?.color ?? resolveContrastTextColor(background.color),
|
|
3086
|
+
fontFamily: textStyle?.fontFamily ?? "Inter, sans-serif",
|
|
3087
|
+
fontSize: textStyle?.fontSize ?? 13,
|
|
3088
|
+
fontWeight: textStyle?.fontWeight ?? "600",
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
3091
|
+
resolveOffset(detection, context) {
|
|
3092
|
+
const offset = resolveStyleValue(this.options.offset, detection, context);
|
|
3093
|
+
if (offset === null) {
|
|
3094
|
+
return {};
|
|
3095
|
+
}
|
|
3096
|
+
return {
|
|
3097
|
+
...(offset?.x === undefined ? {} : { offsetX: offset.x }),
|
|
3098
|
+
offsetY: offset?.y ?? this.offsetY,
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
resolvePlacement(detection, context) {
|
|
3102
|
+
return (resolveStyleValue(this.options.placement, detection, context) ??
|
|
3103
|
+
LabelPlacement.Top);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
function formatDetectionLabel(detection, includeConfidence) {
|
|
3107
|
+
const label = detection.className ??
|
|
3108
|
+
(typeof detection.metadata?.label === "string"
|
|
3109
|
+
? detection.metadata.label
|
|
3110
|
+
: undefined);
|
|
3111
|
+
if (!label) {
|
|
3112
|
+
return undefined;
|
|
3113
|
+
}
|
|
3114
|
+
if (!includeConfidence || detection.confidence === undefined) {
|
|
3115
|
+
return label;
|
|
3116
|
+
}
|
|
3117
|
+
return `${label} ${Math.round(detection.confidence * 100)}%`;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
var MaskRenderMode;
|
|
3121
|
+
(function (MaskRenderMode) {
|
|
3122
|
+
MaskRenderMode["FillAndStroke"] = "fillAndStroke";
|
|
3123
|
+
MaskRenderMode["FillOnly"] = "fillOnly";
|
|
3124
|
+
MaskRenderMode["StrokeOnly"] = "strokeOnly";
|
|
3125
|
+
})(MaskRenderMode || (MaskRenderMode = {}));
|
|
3126
|
+
|
|
3127
|
+
/**
|
|
3128
|
+
* Default compressed-RLE mask style.
|
|
3129
|
+
*
|
|
3130
|
+
* Static color/stroke options can reuse prepared mask artifacts by key. Dynamic
|
|
3131
|
+
* color or stroke resolvers intentionally do not expose an artifact key, so new
|
|
3132
|
+
* style objects rebuild prepared palettes instead of reusing stale ones.
|
|
3133
|
+
*/
|
|
3134
|
+
class BaseMaskStyle {
|
|
3135
|
+
artifactKey;
|
|
3136
|
+
opacity;
|
|
3137
|
+
options;
|
|
3138
|
+
constructor(options = {}) {
|
|
3139
|
+
this.options = options;
|
|
3140
|
+
this.opacity = clampOpacity(options.opacity ?? options.alpha ?? 0.35);
|
|
3141
|
+
this.artifactKey = createArtifactKey(options);
|
|
3142
|
+
}
|
|
3143
|
+
resolve(detection, context) {
|
|
3144
|
+
if (!detection.mask ||
|
|
3145
|
+
context.hidden ||
|
|
3146
|
+
this.options.shouldRender?.(detection, context) === false) {
|
|
3147
|
+
return undefined;
|
|
3148
|
+
}
|
|
3149
|
+
const color = resolveStyleValue(this.options.color, detection, context) ??
|
|
3150
|
+
DEFAULT_MASK_COLOR;
|
|
3151
|
+
const mode = resolveStyleValue(this.options.mode, detection, context) ??
|
|
3152
|
+
MaskRenderMode.FillAndStroke;
|
|
3153
|
+
const stroke = resolveStroke(resolveStyleValue(this.options.stroke, detection, context), color, mode);
|
|
3154
|
+
return {
|
|
3155
|
+
alpha: mode === MaskRenderMode.StrokeOnly
|
|
3156
|
+
? 0
|
|
3157
|
+
: clampOpacity(resolveStyleValue(this.options.fillAlpha, detection, context) ??
|
|
3158
|
+
1),
|
|
3159
|
+
color,
|
|
3160
|
+
mask: detection.mask,
|
|
3161
|
+
stroke: mode === MaskRenderMode.FillOnly ? undefined : stroke,
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
const DEFAULT_MASK_COLOR = 0x00ff66;
|
|
3166
|
+
const DEFAULT_MASK_STROKE_ALPHA = 1;
|
|
3167
|
+
const DEFAULT_MASK_STROKE_WIDTH = 1;
|
|
3168
|
+
function clampOpacity(opacity) {
|
|
3169
|
+
return Number.isFinite(opacity) ? Math.max(0, Math.min(opacity, 1)) : 1;
|
|
3170
|
+
}
|
|
3171
|
+
function serializeStroke(stroke) {
|
|
3172
|
+
if (!stroke) {
|
|
3173
|
+
return "none";
|
|
3174
|
+
}
|
|
3175
|
+
return `${stroke.color}:${stroke.alpha}:${stroke.width}`;
|
|
3176
|
+
}
|
|
3177
|
+
function normalizeStroke(stroke, fallbackColor) {
|
|
3178
|
+
if (!stroke) {
|
|
3179
|
+
return undefined;
|
|
3180
|
+
}
|
|
3181
|
+
return {
|
|
3182
|
+
alpha: stroke.alpha ?? DEFAULT_MASK_STROKE_ALPHA,
|
|
3183
|
+
color: stroke.color ?? fallbackColor,
|
|
3184
|
+
width: stroke.width ?? DEFAULT_MASK_STROKE_WIDTH,
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
function resolveStroke(stroke, fallbackColor, mode) {
|
|
3188
|
+
if (mode === MaskRenderMode.FillOnly) {
|
|
3189
|
+
return undefined;
|
|
3190
|
+
}
|
|
3191
|
+
if (mode === MaskRenderMode.StrokeOnly && stroke === undefined) {
|
|
3192
|
+
return normalizeStroke({}, fallbackColor);
|
|
3193
|
+
}
|
|
3194
|
+
return normalizeStroke(stroke, fallbackColor);
|
|
3195
|
+
}
|
|
3196
|
+
function createArtifactKey(options) {
|
|
3197
|
+
if (typeof options.color === "function" ||
|
|
3198
|
+
typeof options.fillAlpha === "function" ||
|
|
3199
|
+
typeof options.mode === "function" ||
|
|
3200
|
+
typeof options.stroke === "function") {
|
|
3201
|
+
return undefined;
|
|
3202
|
+
}
|
|
3203
|
+
const color = options.color ?? DEFAULT_MASK_COLOR;
|
|
3204
|
+
const fillAlpha = options.fillAlpha ?? 1;
|
|
3205
|
+
const mode = options.mode ?? MaskRenderMode.FillAndStroke;
|
|
3206
|
+
return `base:${color}:${fillAlpha}:${mode}:${serializeStroke(resolveStroke(options.stroke, color, mode))}`;
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
class BasePolygonStyle {
|
|
3210
|
+
options;
|
|
3211
|
+
constructor(options = {}) {
|
|
3212
|
+
this.options = options;
|
|
3213
|
+
}
|
|
3214
|
+
resolve(detection, context) {
|
|
3215
|
+
if (!detection.polygon ||
|
|
3216
|
+
context.hidden ||
|
|
3217
|
+
detection.polygon.points.length < 3 ||
|
|
3218
|
+
this.options.shouldRender?.(detection, context) === false) {
|
|
3219
|
+
return undefined;
|
|
3220
|
+
}
|
|
3221
|
+
const fill = resolveStyleValue(this.options.fill, detection, context);
|
|
3222
|
+
const stroke = resolveStyleValue(this.options.stroke, detection, context);
|
|
3223
|
+
return {
|
|
3224
|
+
points: detection.polygon.points,
|
|
3225
|
+
...(fill === null
|
|
3226
|
+
? {}
|
|
3227
|
+
: {
|
|
3228
|
+
fill: {
|
|
3229
|
+
alpha: fill?.alpha ?? 0.16,
|
|
3230
|
+
color: fill?.color ?? 0x00ff66,
|
|
3231
|
+
},
|
|
3232
|
+
}),
|
|
3233
|
+
...(stroke === null
|
|
3234
|
+
? {}
|
|
3235
|
+
: {
|
|
3236
|
+
stroke: {
|
|
3237
|
+
alpha: stroke?.alpha ?? 1,
|
|
3238
|
+
color: stroke?.color ?? 0x00ff66,
|
|
3239
|
+
width: stroke?.width ?? 2,
|
|
3240
|
+
...(stroke?.alignment === undefined
|
|
3241
|
+
? {}
|
|
3242
|
+
: { alignment: stroke.alignment }),
|
|
3243
|
+
...(stroke?.dash === undefined ? {} : { dash: stroke.dash }),
|
|
3244
|
+
},
|
|
3245
|
+
}),
|
|
3246
|
+
};
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
class BasePolylineStyle {
|
|
3251
|
+
options;
|
|
3252
|
+
constructor(options = {}) {
|
|
3253
|
+
this.options = options;
|
|
3254
|
+
}
|
|
3255
|
+
resolve(detection, context) {
|
|
3256
|
+
if (!detection.polyline ||
|
|
3257
|
+
context.hidden ||
|
|
3258
|
+
detection.polyline.points.length < 2 ||
|
|
3259
|
+
this.options.shouldRender?.(detection, context) === false) {
|
|
3260
|
+
return undefined;
|
|
3261
|
+
}
|
|
3262
|
+
const stroke = resolveStyleValue(this.options.stroke, detection, context);
|
|
3263
|
+
if (stroke === null) {
|
|
3264
|
+
return undefined;
|
|
3265
|
+
}
|
|
3266
|
+
return {
|
|
3267
|
+
points: detection.polyline.points,
|
|
3268
|
+
stroke: {
|
|
3269
|
+
alpha: stroke?.alpha ?? 1,
|
|
3270
|
+
color: stroke?.color ?? 0x00ff66,
|
|
3271
|
+
width: stroke?.width ?? 2,
|
|
3272
|
+
...(stroke?.alignment === undefined
|
|
3273
|
+
? {}
|
|
3274
|
+
: { alignment: stroke.alignment }),
|
|
3275
|
+
...(stroke?.dash === undefined ? {} : { dash: stroke.dash }),
|
|
3276
|
+
},
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
var KeypointMarkerShape;
|
|
3282
|
+
(function (KeypointMarkerShape) {
|
|
3283
|
+
KeypointMarkerShape["Circle"] = "circle";
|
|
3284
|
+
KeypointMarkerShape["Cross"] = "cross";
|
|
3285
|
+
})(KeypointMarkerShape || (KeypointMarkerShape = {}));
|
|
3286
|
+
|
|
3287
|
+
class BaseKeypointStyle {
|
|
3288
|
+
options;
|
|
3289
|
+
constructor(options = {}) {
|
|
3290
|
+
this.options = options;
|
|
3291
|
+
}
|
|
3292
|
+
resolve(detection, context) {
|
|
3293
|
+
const geometry = detection.keypoints;
|
|
3294
|
+
if (!geometry ||
|
|
3295
|
+
context.hidden ||
|
|
3296
|
+
this.options.shouldRender?.(detection, context) === false) {
|
|
3297
|
+
return undefined;
|
|
3298
|
+
}
|
|
3299
|
+
const definition = detection.className
|
|
3300
|
+
? this.options.definitions?.[detection.className]
|
|
3301
|
+
: undefined;
|
|
3302
|
+
const markerFill = resolveStyleValue(this.options.markerFill, detection, context);
|
|
3303
|
+
const markerStroke = resolveStyleValue(this.options.markerStroke, detection, context);
|
|
3304
|
+
const edgeStroke = resolveStyleValue(this.options.edgeStroke, detection, context);
|
|
3305
|
+
const shadowStroke = resolveStyleValue(this.options.edgeShadowStroke, detection, context);
|
|
3306
|
+
const radius = resolveStyleValue(this.options.radius, detection, context) ?? 6;
|
|
3307
|
+
const edges = geometry.edges.map(([fromIndex, toIndex], edgeIndex) => ({
|
|
3308
|
+
from: geometry.points[fromIndex],
|
|
3309
|
+
to: geometry.points[toIndex],
|
|
3310
|
+
stroke: {
|
|
3311
|
+
alpha: edgeStroke?.alpha ?? 1,
|
|
3312
|
+
color: definition?.edges[edgeIndex]?.color ?? edgeStroke?.color ?? 0x00ff66,
|
|
3313
|
+
width: edgeStroke?.width ?? 2,
|
|
3314
|
+
},
|
|
3315
|
+
...(shadowStroke === null
|
|
3316
|
+
? {}
|
|
3317
|
+
: {
|
|
3318
|
+
shadowStroke: {
|
|
3319
|
+
alpha: shadowStroke?.alpha ?? 0.65,
|
|
3320
|
+
color: shadowStroke?.color ?? 0x000000,
|
|
3321
|
+
width: shadowStroke?.width ?? 4,
|
|
3322
|
+
},
|
|
3323
|
+
}),
|
|
3324
|
+
}));
|
|
3325
|
+
const markers = geometry.points.flatMap((point, index) => {
|
|
3326
|
+
const visibility = geometry.visibility?.[index] ?? KeypointVisibility.Visible;
|
|
3327
|
+
if (visibility === KeypointVisibility.NotLabeled) {
|
|
3328
|
+
return [];
|
|
3329
|
+
}
|
|
3330
|
+
return [
|
|
3331
|
+
{
|
|
3332
|
+
fill: {
|
|
3333
|
+
alpha: markerFill?.alpha ?? 1,
|
|
3334
|
+
color: definition?.vertices[index]?.color ??
|
|
3335
|
+
markerFill?.color ??
|
|
3336
|
+
0x00ff66,
|
|
3337
|
+
},
|
|
3338
|
+
index,
|
|
3339
|
+
point,
|
|
3340
|
+
radius,
|
|
3341
|
+
shape: visibility === KeypointVisibility.Occluded
|
|
3342
|
+
? KeypointMarkerShape.Cross
|
|
3343
|
+
: KeypointMarkerShape.Circle,
|
|
3344
|
+
stroke: {
|
|
3345
|
+
alpha: markerStroke?.alpha ?? 1,
|
|
3346
|
+
color: markerStroke?.color ?? 0xffffff,
|
|
3347
|
+
width: markerStroke?.width ?? 2,
|
|
3348
|
+
},
|
|
3349
|
+
},
|
|
3350
|
+
];
|
|
3351
|
+
});
|
|
3352
|
+
return { edges, markers };
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
const SUPERVISION_ROBOFLOW_COLOR = 0xa351fb;
|
|
3357
|
+
const DEFAULT_DETECTION_COLOR_SEQUENCE = [
|
|
3358
|
+
createClassColorStyle(0x38bdf8, 0x164e63, 0xecfeff, 0x7dd3fc),
|
|
3359
|
+
createClassColorStyle(0x22c55e, 0x14532d, 0xf0fdf4, 0x86efac),
|
|
3360
|
+
createClassColorStyle(0xa78bfa, 0x4c1d95, 0xf5f3ff, 0xc4b5fd),
|
|
3361
|
+
createClassColorStyle(0xfacc15, 0x713f12, 0xfffbeb, 0xfde047),
|
|
3362
|
+
createClassColorStyle(0xf97316, 0x7c2d12, 0xfff7ed, 0xffa23a),
|
|
3363
|
+
createClassColorStyle(0xf472b6, 0x831843, 0xfdf2f8, 0xf9a8d4),
|
|
3364
|
+
createClassColorStyle(0x60a5fa, 0x1e3a8a, 0xeff6ff, 0x93c5fd),
|
|
3365
|
+
createClassColorStyle(0xfb7185, 0x881337, 0xfff1f2, 0xfda4af),
|
|
3366
|
+
createClassColorStyle(0x34d399, 0x064e3b, 0xecfdf5, 0x6ee7b7),
|
|
3367
|
+
createClassColorStyle(0xe879f9, 0x701a75, 0xfdf4ff, 0xf0abfc),
|
|
3368
|
+
];
|
|
3369
|
+
const DEFAULT_DETECTION_CLASS_STYLES = {
|
|
3370
|
+
basketball: DEFAULT_DETECTION_COLOR_SEQUENCE[4],
|
|
3371
|
+
bed: DEFAULT_DETECTION_COLOR_SEQUENCE[5],
|
|
3372
|
+
bottle: DEFAULT_DETECTION_COLOR_SEQUENCE[4],
|
|
3373
|
+
"cell phone": DEFAULT_DETECTION_COLOR_SEQUENCE[8],
|
|
3374
|
+
cow: DEFAULT_DETECTION_COLOR_SEQUENCE[2],
|
|
3375
|
+
cup: DEFAULT_DETECTION_COLOR_SEQUENCE[3],
|
|
3376
|
+
horse: DEFAULT_DETECTION_COLOR_SEQUENCE[0],
|
|
3377
|
+
keyboard: DEFAULT_DETECTION_COLOR_SEQUENCE[1],
|
|
3378
|
+
knife: DEFAULT_DETECTION_COLOR_SEQUENCE[7],
|
|
3379
|
+
laptop: DEFAULT_DETECTION_COLOR_SEQUENCE[6],
|
|
3380
|
+
person: DEFAULT_DETECTION_COLOR_SEQUENCE[1],
|
|
3381
|
+
"potted plant": DEFAULT_DETECTION_COLOR_SEQUENCE[8],
|
|
3382
|
+
"sports ball": DEFAULT_DETECTION_COLOR_SEQUENCE[4],
|
|
3383
|
+
tv: DEFAULT_DETECTION_COLOR_SEQUENCE[2],
|
|
3384
|
+
"white team player": createClassColorStyle(0xf8fafc, 0x334155, 0xffffff, 0xffffff),
|
|
3385
|
+
"yellow team player": DEFAULT_DETECTION_COLOR_SEQUENCE[3],
|
|
3386
|
+
};
|
|
3387
|
+
// The "worklet" directives below are inert everywhere except React Native:
|
|
3388
|
+
// browsers and Node see a no-op string, while React Native's worklets Babel
|
|
3389
|
+
// plugin makes these functions callable inside frame worklets, so every
|
|
3390
|
+
// platform resolves detection colors through this one implementation.
|
|
3391
|
+
// Worklet constraint on ordering: the plugin turns marked declarations into
|
|
3392
|
+
// non-hoisted assignments that capture each other at module-init time, so
|
|
3393
|
+
// helpers must be defined before the functions that call them.
|
|
3394
|
+
function normalizeDetectionClassName(className) {
|
|
3395
|
+
"worklet";
|
|
3396
|
+
return (className ?? "")
|
|
3397
|
+
.trim()
|
|
3398
|
+
.toLowerCase()
|
|
3399
|
+
.replace(/[_-]/g, " ")
|
|
3400
|
+
.replace(/\s+/g, " ");
|
|
3401
|
+
}
|
|
3402
|
+
function hashClassName(className) {
|
|
3403
|
+
"worklet";
|
|
3404
|
+
let hash = 0;
|
|
3405
|
+
for (let index = 0; index < className.length; index += 1) {
|
|
3406
|
+
hash = (hash * 31 + className.charCodeAt(index)) >>> 0;
|
|
3407
|
+
}
|
|
3408
|
+
return hash;
|
|
3409
|
+
}
|
|
3410
|
+
function resolveDetectionClassColorStyle(className) {
|
|
3411
|
+
"worklet";
|
|
3412
|
+
const normalizedClassName = normalizeDetectionClassName(className);
|
|
3413
|
+
const knownStyle = DEFAULT_DETECTION_CLASS_STYLES[normalizedClassName];
|
|
3414
|
+
if (knownStyle) {
|
|
3415
|
+
return knownStyle;
|
|
3416
|
+
}
|
|
3417
|
+
return DEFAULT_DETECTION_COLOR_SEQUENCE[hashClassName(normalizedClassName) % DEFAULT_DETECTION_COLOR_SEQUENCE.length];
|
|
3418
|
+
}
|
|
3419
|
+
function createClassColorStyle(fill, labelBackground, labelText, stroke) {
|
|
3420
|
+
return {
|
|
3421
|
+
fill,
|
|
3422
|
+
labelBackground,
|
|
3423
|
+
labelText,
|
|
3424
|
+
stroke,
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
const DEFAULT_BOX_CORNER_RADIUS = 1;
|
|
3429
|
+
const DEFAULT_OUTLINE_WIDTH = 2;
|
|
3430
|
+
const DEFAULT_FILL_ALPHA = 0.08;
|
|
3431
|
+
const DEFAULT_MASK_FILL_ALPHA = 0.45;
|
|
3432
|
+
const DEFAULT_KEYPOINT_EDGE_WIDTH = 1.5;
|
|
3433
|
+
const DEFAULT_KEYPOINT_SHADOW_WIDTH = 3;
|
|
3434
|
+
const DEFAULT_KEYPOINT_SHADOW_ALPHA = 0.25;
|
|
3435
|
+
const DEFAULT_KEYPOINT_RADIUS = 3.5;
|
|
3436
|
+
const DEFAULT_LABEL_CORNER_RADIUS = 4;
|
|
3437
|
+
const DEFAULT_LABEL_PADDING_X = 6;
|
|
3438
|
+
const DEFAULT_LABEL_PADDING_Y = 3;
|
|
3439
|
+
const DEFAULT_LABEL_FONT_SIZE = 12;
|
|
3440
|
+
const DEFAULT_LABEL_FONT_FAMILY = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace";
|
|
3441
|
+
/**
|
|
3442
|
+
* Builds the canonical, unselected annotation presentation used by Roboflow's
|
|
3443
|
+
* Core annotation editor. It deliberately excludes host-specific interaction,
|
|
3444
|
+
* editing, focus, and theme behaviour so consumers can compose those layers.
|
|
3445
|
+
*/
|
|
3446
|
+
function createDefaultAnnotationPresentation(options = {}) {
|
|
3447
|
+
const getClassColor = (detection) => resolveDefaultClassColor(detection, options.getClassColor);
|
|
3448
|
+
const shouldRenderBox = (detection) => !(detection.mask ||
|
|
3449
|
+
detection.polygon ||
|
|
3450
|
+
detection.polyline ||
|
|
3451
|
+
detection.keypoints);
|
|
3452
|
+
return {
|
|
3453
|
+
boxStyle: new BaseBoxStyle({
|
|
3454
|
+
cornerRadius: DEFAULT_BOX_CORNER_RADIUS,
|
|
3455
|
+
fill: (detection) => ({
|
|
3456
|
+
alpha: DEFAULT_FILL_ALPHA,
|
|
3457
|
+
color: getClassColor(detection),
|
|
3458
|
+
}),
|
|
3459
|
+
shape: BoxShape.RoundedRect,
|
|
3460
|
+
shouldRender: shouldRenderBox,
|
|
3461
|
+
stroke: (detection) => ({
|
|
3462
|
+
alpha: 1,
|
|
3463
|
+
color: getClassColor(detection),
|
|
3464
|
+
width: DEFAULT_OUTLINE_WIDTH,
|
|
3465
|
+
}),
|
|
3466
|
+
}),
|
|
3467
|
+
keypointStyle: new BaseKeypointStyle({
|
|
3468
|
+
...(options.skeletonDefinitions === undefined
|
|
3469
|
+
? {}
|
|
3470
|
+
: { definitions: options.skeletonDefinitions }),
|
|
3471
|
+
edgeShadowStroke: {
|
|
3472
|
+
alpha: DEFAULT_KEYPOINT_SHADOW_ALPHA,
|
|
3473
|
+
color: 0x000000,
|
|
3474
|
+
width: DEFAULT_KEYPOINT_SHADOW_WIDTH,
|
|
3475
|
+
},
|
|
3476
|
+
edgeStroke: (detection) => ({
|
|
3477
|
+
alpha: 1,
|
|
3478
|
+
color: getClassColor(detection),
|
|
3479
|
+
width: DEFAULT_KEYPOINT_EDGE_WIDTH,
|
|
3480
|
+
}),
|
|
3481
|
+
markerFill: (detection) => ({
|
|
3482
|
+
alpha: 1,
|
|
3483
|
+
color: getClassColor(detection),
|
|
3484
|
+
}),
|
|
3485
|
+
markerStroke: { alpha: 1, color: 0xffffff, width: 1 },
|
|
3486
|
+
radius: DEFAULT_KEYPOINT_RADIUS,
|
|
3487
|
+
}),
|
|
3488
|
+
labelStyle: new BaseLabelStyle({
|
|
3489
|
+
background: (detection) => ({
|
|
3490
|
+
alpha: 1,
|
|
3491
|
+
color: getClassColor(detection),
|
|
3492
|
+
cornerRadius: DEFAULT_LABEL_CORNER_RADIUS,
|
|
3493
|
+
paddingX: DEFAULT_LABEL_PADDING_X,
|
|
3494
|
+
paddingY: DEFAULT_LABEL_PADDING_Y,
|
|
3495
|
+
topCornersOnly: true,
|
|
3496
|
+
}),
|
|
3497
|
+
includeConfidence: options.includeConfidence,
|
|
3498
|
+
placement: LabelPlacement.Top,
|
|
3499
|
+
textStyle: {
|
|
3500
|
+
fontFamily: DEFAULT_LABEL_FONT_FAMILY,
|
|
3501
|
+
fontSize: DEFAULT_LABEL_FONT_SIZE,
|
|
3502
|
+
fontWeight: "600",
|
|
3503
|
+
},
|
|
3504
|
+
}),
|
|
3505
|
+
maskStyle: new BaseMaskStyle({
|
|
3506
|
+
color: (detection) => getClassColor(detection),
|
|
3507
|
+
fillAlpha: DEFAULT_MASK_FILL_ALPHA,
|
|
3508
|
+
mode: MaskRenderMode.FillAndStroke,
|
|
3509
|
+
opacity: 1,
|
|
3510
|
+
stroke: (detection) => ({
|
|
3511
|
+
alpha: 1,
|
|
3512
|
+
color: getClassColor(detection),
|
|
3513
|
+
width: DEFAULT_OUTLINE_WIDTH,
|
|
3514
|
+
}),
|
|
3515
|
+
}),
|
|
3516
|
+
polygonStyle: new BasePolygonStyle({
|
|
3517
|
+
fill: (detection) => ({
|
|
3518
|
+
alpha: DEFAULT_FILL_ALPHA,
|
|
3519
|
+
color: getClassColor(detection),
|
|
3520
|
+
}),
|
|
3521
|
+
stroke: (detection) => ({
|
|
3522
|
+
alpha: 1,
|
|
3523
|
+
color: getClassColor(detection),
|
|
3524
|
+
width: DEFAULT_OUTLINE_WIDTH,
|
|
3525
|
+
}),
|
|
3526
|
+
}),
|
|
3527
|
+
polylineStyle: new BasePolylineStyle({
|
|
3528
|
+
stroke: (detection) => ({
|
|
3529
|
+
alpha: 1,
|
|
3530
|
+
color: getClassColor(detection),
|
|
3531
|
+
width: DEFAULT_OUTLINE_WIDTH,
|
|
3532
|
+
}),
|
|
3533
|
+
}),
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
function resolveDefaultClassColor(detection, getClassColor) {
|
|
3537
|
+
const color = getClassColor?.(detection.className);
|
|
3538
|
+
return typeof color === "number" && Number.isFinite(color)
|
|
3539
|
+
? color
|
|
3540
|
+
: resolveDetectionClassColorStyle(detection.className).fill;
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
function createSourceAwarePresentation(globalPresentation = {}, sources) {
|
|
3544
|
+
if (!sources.some((source) => source.presentation !== undefined)) {
|
|
3545
|
+
return globalPresentation;
|
|
3546
|
+
}
|
|
3547
|
+
const sourcePresentations = new Map(sources.map((source) => [source.id, source.presentation]));
|
|
3548
|
+
return {
|
|
3549
|
+
...globalPresentation,
|
|
3550
|
+
boxStyle: hasSourceStyle(sources, "boxStyle")
|
|
3551
|
+
? new SourceAwareBoxStyle(normalizeGlobalBoxStyle(globalPresentation.boxStyle), sourcePresentations)
|
|
3552
|
+
: globalPresentation.boxStyle,
|
|
3553
|
+
labelStyle: hasSourceStyle(sources, "labelStyle")
|
|
3554
|
+
? new SourceAwareLabelStyle(normalizeGlobalLabelStyle(globalPresentation.labelStyle), sourcePresentations)
|
|
3555
|
+
: globalPresentation.labelStyle,
|
|
3556
|
+
maskStyle: hasSourceStyle(sources, "maskStyle")
|
|
3557
|
+
? new SourceAwareMaskStyle(normalizeGlobalMaskStyle(globalPresentation.maskStyle), sourcePresentations)
|
|
3558
|
+
: globalPresentation.maskStyle,
|
|
3559
|
+
polygonStyle: hasSourceStyle(sources, "polygonStyle")
|
|
3560
|
+
? new SourceAwarePolygonStyle(normalizeGlobalPolygonStyle(globalPresentation.polygonStyle), sourcePresentations)
|
|
3561
|
+
: globalPresentation.polygonStyle,
|
|
3562
|
+
polylineStyle: hasSourceStyle(sources, "polylineStyle")
|
|
3563
|
+
? new SourceAwarePolylineStyle(normalizeGlobalPolylineStyle(globalPresentation.polylineStyle), sourcePresentations)
|
|
3564
|
+
: globalPresentation.polylineStyle,
|
|
3565
|
+
keypointStyle: hasSourceStyle(sources, "keypointStyle")
|
|
3566
|
+
? new SourceAwareKeypointStyle(normalizeGlobalKeypointStyle(globalPresentation.keypointStyle), sourcePresentations)
|
|
3567
|
+
: globalPresentation.keypointStyle,
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
class SourceAwareBoxStyle {
|
|
3571
|
+
globalStyle;
|
|
3572
|
+
sourcePresentations;
|
|
3573
|
+
constructor(globalStyle, sourcePresentations) {
|
|
3574
|
+
this.globalStyle = globalStyle;
|
|
3575
|
+
this.sourcePresentations = sourcePresentations;
|
|
3576
|
+
}
|
|
3577
|
+
resolve(detection, context) {
|
|
3578
|
+
const style = resolveSourceStyle(detection, this.globalStyle, this.sourcePresentations, "boxStyle");
|
|
3579
|
+
return style?.resolve(detection, context);
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
class SourceAwareLabelStyle {
|
|
3583
|
+
globalStyle;
|
|
3584
|
+
sourcePresentations;
|
|
3585
|
+
constructor(globalStyle, sourcePresentations) {
|
|
3586
|
+
this.globalStyle = globalStyle;
|
|
3587
|
+
this.sourcePresentations = sourcePresentations;
|
|
3588
|
+
}
|
|
3589
|
+
resolve(detection, context) {
|
|
3590
|
+
const style = resolveSourceStyle(detection, this.globalStyle, this.sourcePresentations, "labelStyle");
|
|
3591
|
+
return style?.resolve(detection, context);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
class SourceAwareMaskStyle {
|
|
3595
|
+
globalStyle;
|
|
3596
|
+
sourcePresentations;
|
|
3597
|
+
artifactKey = undefined;
|
|
3598
|
+
opacity;
|
|
3599
|
+
constructor(globalStyle, sourcePresentations) {
|
|
3600
|
+
this.globalStyle = globalStyle;
|
|
3601
|
+
this.sourcePresentations = sourcePresentations;
|
|
3602
|
+
this.opacity = globalStyle?.opacity;
|
|
3603
|
+
}
|
|
3604
|
+
resolve(detection, context) {
|
|
3605
|
+
const style = resolveSourceStyle(detection, this.globalStyle, this.sourcePresentations, "maskStyle");
|
|
3606
|
+
return style?.resolve(detection, context);
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
class SourceAwarePolygonStyle {
|
|
3610
|
+
globalStyle;
|
|
3611
|
+
sourcePresentations;
|
|
3612
|
+
constructor(globalStyle, sourcePresentations) {
|
|
3613
|
+
this.globalStyle = globalStyle;
|
|
3614
|
+
this.sourcePresentations = sourcePresentations;
|
|
3615
|
+
}
|
|
3616
|
+
resolve(detection, context) {
|
|
3617
|
+
const style = resolveSourceStyle(detection, this.globalStyle, this.sourcePresentations, "polygonStyle");
|
|
3618
|
+
return style?.resolve(detection, context);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
class SourceAwarePolylineStyle {
|
|
3622
|
+
globalStyle;
|
|
3623
|
+
sourcePresentations;
|
|
3624
|
+
constructor(globalStyle, sourcePresentations) {
|
|
3625
|
+
this.globalStyle = globalStyle;
|
|
3626
|
+
this.sourcePresentations = sourcePresentations;
|
|
3627
|
+
}
|
|
3628
|
+
resolve(detection, context) {
|
|
3629
|
+
const style = resolveSourceStyle(detection, this.globalStyle, this.sourcePresentations, "polylineStyle");
|
|
3630
|
+
return style?.resolve(detection, context);
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
class SourceAwareKeypointStyle {
|
|
3634
|
+
globalStyle;
|
|
3635
|
+
sourcePresentations;
|
|
3636
|
+
constructor(globalStyle, sourcePresentations) {
|
|
3637
|
+
this.globalStyle = globalStyle;
|
|
3638
|
+
this.sourcePresentations = sourcePresentations;
|
|
3639
|
+
}
|
|
3640
|
+
resolve(detection, context) {
|
|
3641
|
+
const style = resolveSourceStyle(detection, this.globalStyle, this.sourcePresentations, "keypointStyle");
|
|
3642
|
+
return style?.resolve(detection, context);
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
function hasSourceStyle(sources, key) {
|
|
3646
|
+
return sources.some((source) => source.presentation?.[key] !== undefined);
|
|
3647
|
+
}
|
|
3648
|
+
function resolveSourceStyle(detection, globalStyle, sourcePresentations, key) {
|
|
3649
|
+
const sourcePresentation = detection.sourceId
|
|
3650
|
+
? sourcePresentations.get(detection.sourceId)
|
|
3651
|
+
: undefined;
|
|
3652
|
+
const sourceStyle = sourcePresentation?.[key];
|
|
3653
|
+
return sourceStyle === undefined ? globalStyle : sourceStyle;
|
|
3654
|
+
}
|
|
3655
|
+
function normalizeGlobalBoxStyle(style) {
|
|
3656
|
+
return style === undefined ? new BaseBoxStyle() : style;
|
|
3657
|
+
}
|
|
3658
|
+
function normalizeGlobalLabelStyle(style) {
|
|
3659
|
+
return style === undefined ? new BaseLabelStyle() : style;
|
|
3660
|
+
}
|
|
3661
|
+
function normalizeGlobalMaskStyle(style) {
|
|
3662
|
+
return style === undefined ? new BaseMaskStyle() : style;
|
|
3663
|
+
}
|
|
3664
|
+
function normalizeGlobalPolygonStyle(style) {
|
|
3665
|
+
return style === undefined ? new BasePolygonStyle() : style;
|
|
3666
|
+
}
|
|
3667
|
+
function normalizeGlobalPolylineStyle(style) {
|
|
3668
|
+
return style === undefined ? new BasePolylineStyle() : style;
|
|
3669
|
+
}
|
|
3670
|
+
function normalizeGlobalKeypointStyle(style) {
|
|
3671
|
+
return style === undefined ? new BaseKeypointStyle() : style;
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
var DetectionMaskPayloadFormat;
|
|
3675
|
+
(function (DetectionMaskPayloadFormat) {
|
|
3676
|
+
DetectionMaskPayloadFormat["RawCocoRle"] = "rawCocoRle";
|
|
3677
|
+
DetectionMaskPayloadFormat["DeflatedBase64"] = "deflatedBase64";
|
|
3678
|
+
})(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));
|
|
3679
|
+
function encodeBinaryMask(data, width, height) {
|
|
3680
|
+
assertMaskDimensions(data, width, height);
|
|
3681
|
+
const runs = [];
|
|
3682
|
+
let currentValue = 0;
|
|
3683
|
+
let runLength = 0;
|
|
3684
|
+
for (let x = 0; x < width; x += 1) {
|
|
3685
|
+
for (let y = 0; y < height; y += 1) {
|
|
3686
|
+
const value = data[y * width + x] ? 1 : 0;
|
|
3687
|
+
if (value === currentValue) {
|
|
3688
|
+
runLength += 1;
|
|
3689
|
+
}
|
|
3690
|
+
else {
|
|
3691
|
+
runs.push(runLength);
|
|
3692
|
+
currentValue = value;
|
|
3693
|
+
runLength = 1;
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
runs.push(runLength);
|
|
3698
|
+
return {
|
|
3699
|
+
counts: encodeCompressedRleCounts(runs),
|
|
3700
|
+
encoding: DetectionMaskEncoding.CompressedRle,
|
|
3701
|
+
height,
|
|
3702
|
+
width,
|
|
3703
|
+
};
|
|
3704
|
+
}
|
|
3705
|
+
/** Encodes a binary mask and derives its bounds in the same raster traversal. */
|
|
3706
|
+
function encodeBinaryMaskWithBounds(data, width, height) {
|
|
3707
|
+
assertMaskDimensions(data, width, height);
|
|
3708
|
+
const runs = [];
|
|
3709
|
+
let currentValue = 0;
|
|
3710
|
+
let runLength = 0;
|
|
3711
|
+
let minX = width;
|
|
3712
|
+
let minY = height;
|
|
3713
|
+
let maxX = -1;
|
|
3714
|
+
let maxY = -1;
|
|
3715
|
+
for (let x = 0; x < width; x += 1) {
|
|
3716
|
+
for (let y = 0; y < height; y += 1) {
|
|
3717
|
+
const value = data[y * width + x] ? 1 : 0;
|
|
3718
|
+
if (value) {
|
|
3719
|
+
minX = Math.min(minX, x);
|
|
3720
|
+
minY = Math.min(minY, y);
|
|
3721
|
+
maxX = Math.max(maxX, x);
|
|
3722
|
+
maxY = Math.max(maxY, y);
|
|
3723
|
+
}
|
|
3724
|
+
if (value === currentValue) {
|
|
3725
|
+
runLength += 1;
|
|
3726
|
+
}
|
|
3727
|
+
else {
|
|
3728
|
+
runs.push(runLength);
|
|
3729
|
+
currentValue = value;
|
|
3730
|
+
runLength = 1;
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
runs.push(runLength);
|
|
3735
|
+
return {
|
|
3736
|
+
bounds: maxX < 0
|
|
3737
|
+
? null
|
|
3738
|
+
: {
|
|
3739
|
+
height: maxY - minY + 1,
|
|
3740
|
+
width: maxX - minX + 1,
|
|
3741
|
+
x: minX + (maxX - minX + 1) / 2,
|
|
3742
|
+
y: minY + (maxY - minY + 1) / 2,
|
|
3743
|
+
},
|
|
3744
|
+
mask: {
|
|
3745
|
+
counts: encodeCompressedRleCounts(runs),
|
|
3746
|
+
encoding: DetectionMaskEncoding.CompressedRle,
|
|
3747
|
+
height,
|
|
3748
|
+
width,
|
|
3749
|
+
},
|
|
3750
|
+
};
|
|
3751
|
+
}
|
|
3752
|
+
function encodeDetectionMaskPayload(mask, codec) {
|
|
3753
|
+
return codec ? codec.deflate(mask.counts) : mask.counts;
|
|
3754
|
+
}
|
|
3755
|
+
function decodeDetectionMaskPayload(payload, width, height, options = {}) {
|
|
3756
|
+
const format = options.format ??
|
|
3757
|
+
(isDeflatedBase64DetectionMaskPayload(payload)
|
|
3758
|
+
? DetectionMaskPayloadFormat.DeflatedBase64
|
|
3759
|
+
: DetectionMaskPayloadFormat.RawCocoRle);
|
|
3760
|
+
if (format === DetectionMaskPayloadFormat.DeflatedBase64 && !options.codec) {
|
|
3761
|
+
throw new Error("A detection mask compression codec is required for deflated payloads.");
|
|
3762
|
+
}
|
|
3763
|
+
return {
|
|
3764
|
+
counts: format === DetectionMaskPayloadFormat.DeflatedBase64
|
|
3765
|
+
? options.codec.inflate(payload)
|
|
3766
|
+
: payload,
|
|
3767
|
+
encoding: DetectionMaskEncoding.CompressedRle,
|
|
3768
|
+
height,
|
|
3769
|
+
width,
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
/** Matches the annotation editor's legacy transport-format heuristic. */
|
|
3773
|
+
function isDeflatedBase64DetectionMaskPayload(value) {
|
|
3774
|
+
return value.length > 100 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
|
|
3775
|
+
}
|
|
3776
|
+
function computeMaskBounds(data, width, height) {
|
|
3777
|
+
assertMaskDimensions(data, width, height);
|
|
3778
|
+
let minX = width;
|
|
3779
|
+
let minY = height;
|
|
3780
|
+
let maxX = -1;
|
|
3781
|
+
let maxY = -1;
|
|
3782
|
+
for (let y = 0; y < height; y += 1) {
|
|
3783
|
+
for (let x = 0; x < width; x += 1) {
|
|
3784
|
+
if (!data[y * width + x]) {
|
|
3785
|
+
continue;
|
|
3786
|
+
}
|
|
3787
|
+
minX = Math.min(minX, x);
|
|
3788
|
+
minY = Math.min(minY, y);
|
|
3789
|
+
maxX = Math.max(maxX, x);
|
|
3790
|
+
maxY = Math.max(maxY, y);
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
if (maxX < 0) {
|
|
3794
|
+
return null;
|
|
3795
|
+
}
|
|
3796
|
+
const boundsWidth = maxX - minX + 1;
|
|
3797
|
+
const boundsHeight = maxY - minY + 1;
|
|
3798
|
+
return {
|
|
3799
|
+
height: boundsHeight,
|
|
3800
|
+
width: boundsWidth,
|
|
3801
|
+
x: minX + boundsWidth / 2,
|
|
3802
|
+
y: minY + boundsHeight / 2,
|
|
3803
|
+
};
|
|
3804
|
+
}
|
|
3805
|
+
function computeDetectionMaskRect(mask) {
|
|
3806
|
+
const decoded = decodeCompressedRleMask(mask);
|
|
3807
|
+
const bounds = computeMaskBounds(decoded.data, decoded.width, decoded.height);
|
|
3808
|
+
return bounds ?? undefined;
|
|
3809
|
+
}
|
|
3810
|
+
function detectMaskBorders(data, width, height) {
|
|
3811
|
+
assertMaskDimensions(data, width, height);
|
|
3812
|
+
const borders = new Uint8Array(data.length);
|
|
3813
|
+
for (let y = 0; y < height; y += 1) {
|
|
3814
|
+
for (let x = 0; x < width; x += 1) {
|
|
3815
|
+
const offset = y * width + x;
|
|
3816
|
+
if (!data[offset]) {
|
|
3817
|
+
continue;
|
|
3818
|
+
}
|
|
3819
|
+
if (x === 0 ||
|
|
3820
|
+
y === 0 ||
|
|
3821
|
+
x === width - 1 ||
|
|
3822
|
+
y === height - 1 ||
|
|
3823
|
+
!data[offset - 1] ||
|
|
3824
|
+
!data[offset + 1] ||
|
|
3825
|
+
!data[offset - width] ||
|
|
3826
|
+
!data[offset + width]) {
|
|
3827
|
+
borders[offset] = 1;
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
return borders;
|
|
3832
|
+
}
|
|
3833
|
+
function extractMaskContour(data, width, height) {
|
|
3834
|
+
assertMaskDimensions(data, width, height);
|
|
3835
|
+
const stride = Math.max(1, Math.floor(height / 100));
|
|
3836
|
+
const leftEdge = [];
|
|
3837
|
+
const rightEdge = [];
|
|
3838
|
+
for (let y = 0; y < height; y += stride) {
|
|
3839
|
+
let left = -1;
|
|
3840
|
+
let right = -1;
|
|
3841
|
+
for (let x = 0; x < width; x += 1) {
|
|
3842
|
+
if (data[y * width + x]) {
|
|
3843
|
+
left = left === -1 ? x : left;
|
|
3844
|
+
right = x;
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
if (left !== -1) {
|
|
3848
|
+
leftEdge.push({ x: left, y });
|
|
3849
|
+
rightEdge.push({ x: right, y });
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
return leftEdge.length < 2
|
|
3853
|
+
? undefined
|
|
3854
|
+
: [...leftEdge, ...rightEdge.reverse()];
|
|
3855
|
+
}
|
|
3856
|
+
function extractMaskRectRuns(data, width, height) {
|
|
3857
|
+
assertMaskDimensions(data, width, height);
|
|
3858
|
+
const rects = [];
|
|
3859
|
+
const openRects = new Map();
|
|
3860
|
+
for (let y = 0; y < height; y += 1) {
|
|
3861
|
+
const activeSpans = new Set();
|
|
3862
|
+
let x = 0;
|
|
3863
|
+
while (x < width) {
|
|
3864
|
+
while (x < width && !data[y * width + x]) {
|
|
3865
|
+
x += 1;
|
|
3866
|
+
}
|
|
3867
|
+
if (x >= width) {
|
|
3868
|
+
break;
|
|
3869
|
+
}
|
|
3870
|
+
const startX = x;
|
|
3871
|
+
while (x < width && data[y * width + x]) {
|
|
3872
|
+
x += 1;
|
|
3873
|
+
}
|
|
3874
|
+
const runWidth = x - startX;
|
|
3875
|
+
const key = `${startX}:${runWidth}`;
|
|
3876
|
+
const openRect = openRects.get(key);
|
|
3877
|
+
activeSpans.add(key);
|
|
3878
|
+
if (openRect && openRect.y + openRect.height === y) {
|
|
3879
|
+
openRects.set(key, { ...openRect, height: openRect.height + 1 });
|
|
3880
|
+
}
|
|
3881
|
+
else {
|
|
3882
|
+
if (openRect) {
|
|
3883
|
+
rects.push(openRect);
|
|
3884
|
+
}
|
|
3885
|
+
openRects.set(key, { height: 1, width: runWidth, x: startX, y });
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
for (const [key, openRect] of openRects) {
|
|
3889
|
+
if (!activeSpans.has(key)) {
|
|
3890
|
+
rects.push(openRect);
|
|
3891
|
+
openRects.delete(key);
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
rects.push(...openRects.values());
|
|
3896
|
+
return rects.length > 0 ? rects : undefined;
|
|
3897
|
+
}
|
|
3898
|
+
function assertMaskDimensions(data, width, height) {
|
|
3899
|
+
if (!Number.isInteger(width) || width <= 0) {
|
|
3900
|
+
throw new Error("Mask width must be a positive integer.");
|
|
3901
|
+
}
|
|
3902
|
+
if (!Number.isInteger(height) || height <= 0) {
|
|
3903
|
+
throw new Error("Mask height must be a positive integer.");
|
|
3904
|
+
}
|
|
3905
|
+
if (data.length !== width * height) {
|
|
3906
|
+
throw new Error("Mask data length must equal width * height.");
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
function rectToPolygon(rect) {
|
|
3911
|
+
const halfWidth = rect.width / 2;
|
|
3912
|
+
const halfHeight = rect.height / 2;
|
|
3913
|
+
return {
|
|
3914
|
+
points: [
|
|
3915
|
+
{ x: rect.x - halfWidth, y: rect.y - halfHeight },
|
|
3916
|
+
{ x: rect.x + halfWidth, y: rect.y - halfHeight },
|
|
3917
|
+
{ x: rect.x + halfWidth, y: rect.y + halfHeight },
|
|
3918
|
+
{ x: rect.x - halfWidth, y: rect.y + halfHeight },
|
|
3919
|
+
],
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
function polygonToRect(polygon) {
|
|
3923
|
+
return getPointsRect(polygon.points);
|
|
3924
|
+
}
|
|
3925
|
+
function rasterizeRectToMask(rect, dimensions) {
|
|
3926
|
+
const data = createEmptyMask(dimensions);
|
|
3927
|
+
const topLeft = centerRectToTopLeftRect(rect);
|
|
3928
|
+
const left = Math.max(0, Math.round(topLeft.x));
|
|
3929
|
+
const top = Math.max(0, Math.round(topLeft.y));
|
|
3930
|
+
const right = Math.min(dimensions.width - 1, Math.round(rect.x + rect.width / 2));
|
|
3931
|
+
const bottom = Math.min(dimensions.height - 1, Math.round(rect.y + rect.height / 2));
|
|
3932
|
+
for (let y = top; y <= bottom; y += 1) {
|
|
3933
|
+
for (let x = left; x <= right; x += 1) {
|
|
3934
|
+
data[y * dimensions.width + x] = 1;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
return data;
|
|
3938
|
+
}
|
|
3939
|
+
function rasterizePolygonToMask(points, dimensions) {
|
|
3940
|
+
const data = createEmptyMask(dimensions);
|
|
3941
|
+
if (points.length < 3) {
|
|
3942
|
+
return data;
|
|
3943
|
+
}
|
|
3944
|
+
const bounds = centerRectToTopLeftRect(getPointsRect(points));
|
|
3945
|
+
const startY = Math.max(0, Math.floor(bounds.y));
|
|
3946
|
+
const endY = Math.min(dimensions.height - 1, Math.ceil(bounds.y + bounds.height));
|
|
3947
|
+
for (let y = startY; y <= endY; y += 1) {
|
|
3948
|
+
const scanY = y + 0.5;
|
|
3949
|
+
const intersections = [];
|
|
3950
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
3951
|
+
const current = points[index];
|
|
3952
|
+
const next = points[(index + 1) % points.length];
|
|
3953
|
+
if ((current.y <= scanY && next.y > scanY) ||
|
|
3954
|
+
(next.y <= scanY && current.y > scanY)) {
|
|
3955
|
+
const ratio = (scanY - current.y) / (next.y - current.y);
|
|
3956
|
+
intersections.push(current.x + ratio * (next.x - current.x));
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
intersections.sort((left, right) => left - right);
|
|
3960
|
+
for (let index = 0; index < intersections.length - 1; index += 2) {
|
|
3961
|
+
const left = Math.max(0, Math.ceil(intersections[index]));
|
|
3962
|
+
const right = Math.min(dimensions.width - 1, Math.floor(intersections[index + 1]));
|
|
3963
|
+
for (let x = left; x <= right; x += 1) {
|
|
3964
|
+
data[y * dimensions.width + x] = 1;
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
return data;
|
|
3969
|
+
}
|
|
3970
|
+
function convertDetectionBoxToPolygon(detection) {
|
|
3971
|
+
return detection.rect
|
|
3972
|
+
? replaceGeometry(detection, { polygon: rectToPolygon(detection.rect) })
|
|
3973
|
+
: detection;
|
|
3974
|
+
}
|
|
3975
|
+
function convertDetectionPolygonToBox(detection) {
|
|
3976
|
+
const rect = detection.polygon ? polygonToRect(detection.polygon) : undefined;
|
|
3977
|
+
return rect ? replaceGeometry(detection, { rect }) : detection;
|
|
3978
|
+
}
|
|
3979
|
+
function convertDetectionMaskToBox(detection) {
|
|
3980
|
+
const rect = detection.mask
|
|
3981
|
+
? computeDetectionMaskRect(detection.mask)
|
|
3982
|
+
: undefined;
|
|
3983
|
+
return rect ? replaceGeometry(detection, { rect }) : detection;
|
|
3984
|
+
}
|
|
3985
|
+
function convertDetectionMaskToPolygon(detection) {
|
|
3986
|
+
if (!detection.mask) {
|
|
3987
|
+
return detection;
|
|
3988
|
+
}
|
|
3989
|
+
const decoded = decodeCompressedRleMask(detection.mask);
|
|
3990
|
+
const contour = extractMaskContour(decoded.data, decoded.width, decoded.height);
|
|
3991
|
+
const fallbackRect = computeDetectionMaskRect(detection.mask);
|
|
3992
|
+
const polygon = contour && contour.length >= 3
|
|
3993
|
+
? { points: contour }
|
|
3994
|
+
: fallbackRect
|
|
3995
|
+
? rectToPolygon(fallbackRect)
|
|
3996
|
+
: undefined;
|
|
3997
|
+
return polygon ? replaceGeometry(detection, { polygon }) : detection;
|
|
3998
|
+
}
|
|
3999
|
+
function convertDetectionBoxToMask(detection, dimensions) {
|
|
4000
|
+
if (!detection.rect) {
|
|
4001
|
+
return detection;
|
|
4002
|
+
}
|
|
4003
|
+
const mask = encodeBinaryMask(rasterizeRectToMask(detection.rect, dimensions), dimensions.width, dimensions.height);
|
|
4004
|
+
return replaceGeometry(detection, { mask });
|
|
4005
|
+
}
|
|
4006
|
+
function convertDetectionPolygonToMask(detection, dimensions) {
|
|
4007
|
+
if (!detection.polygon) {
|
|
4008
|
+
return detection;
|
|
4009
|
+
}
|
|
4010
|
+
const mask = encodeBinaryMask(rasterizePolygonToMask(detection.polygon.points, dimensions), dimensions.width, dimensions.height);
|
|
4011
|
+
return replaceGeometry(detection, { mask });
|
|
4012
|
+
}
|
|
4013
|
+
function mergeDetectionMasks(detections) {
|
|
4014
|
+
if (detections.length < 2 || detections.some(({ mask }) => !mask)) {
|
|
4015
|
+
return null;
|
|
4016
|
+
}
|
|
4017
|
+
const first = detections[0];
|
|
4018
|
+
if (detections.some((detection) => detection.className !== first.className)) {
|
|
4019
|
+
return null;
|
|
4020
|
+
}
|
|
4021
|
+
const masks = detections.map(({ mask }) => decodeCompressedRleMask(mask));
|
|
4022
|
+
const width = masks[0].width;
|
|
4023
|
+
const height = masks[0].height;
|
|
4024
|
+
if (masks.some((mask) => mask.width !== width || mask.height !== height)) {
|
|
4025
|
+
throw new Error("Merged detection masks must have matching dimensions.");
|
|
4026
|
+
}
|
|
4027
|
+
const data = new Uint8Array(width * height);
|
|
4028
|
+
for (const mask of masks) {
|
|
4029
|
+
for (let index = 0; index < data.length; index += 1) {
|
|
4030
|
+
data[index] ||= mask.data[index] ?? 0;
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
return replaceGeometry(first, {
|
|
4034
|
+
mask: encodeBinaryMask(data, width, height),
|
|
4035
|
+
});
|
|
4036
|
+
}
|
|
4037
|
+
/**
|
|
4038
|
+
* Groups polygon detections by class and returns one union bounding-box
|
|
4039
|
+
* detection per class. This intentionally converts the output to rectangles.
|
|
4040
|
+
*/
|
|
4041
|
+
function mergeDetectionPolygonsByClass(detections, options = {}) {
|
|
4042
|
+
const groups = new Map();
|
|
4043
|
+
for (const detection of detections) {
|
|
4044
|
+
if (!detection.polygon) {
|
|
4045
|
+
continue;
|
|
4046
|
+
}
|
|
4047
|
+
const group = groups.get(detection.className) ?? [];
|
|
4048
|
+
group.push(detection);
|
|
4049
|
+
groups.set(detection.className, group);
|
|
4050
|
+
}
|
|
4051
|
+
return [...groups.entries()].flatMap(([className, group], groupIndex) => {
|
|
4052
|
+
const points = group.flatMap((detection) => detection.polygon.points);
|
|
4053
|
+
const rect = getPointsRect(points);
|
|
4054
|
+
if (!rect) {
|
|
4055
|
+
return [];
|
|
4056
|
+
}
|
|
4057
|
+
const first = group[0];
|
|
4058
|
+
const id = options.createId?.(className, groupIndex) ?? first.id;
|
|
4059
|
+
return [
|
|
4060
|
+
replaceGeometry({
|
|
4061
|
+
...first,
|
|
4062
|
+
id,
|
|
4063
|
+
}, { rect }),
|
|
4064
|
+
];
|
|
4065
|
+
});
|
|
4066
|
+
}
|
|
4067
|
+
function createEmptyMask(dimensions) {
|
|
4068
|
+
if (!Number.isInteger(dimensions.width) ||
|
|
4069
|
+
dimensions.width <= 0 ||
|
|
4070
|
+
!Number.isInteger(dimensions.height) ||
|
|
4071
|
+
dimensions.height <= 0) {
|
|
4072
|
+
throw new Error("Media dimensions must be positive integers.");
|
|
4073
|
+
}
|
|
4074
|
+
return new Uint8Array(dimensions.width * dimensions.height);
|
|
4075
|
+
}
|
|
4076
|
+
function replaceGeometry(detection, geometry) {
|
|
4077
|
+
return {
|
|
4078
|
+
...detection,
|
|
4079
|
+
keypoints: undefined,
|
|
4080
|
+
mask: geometry.mask,
|
|
4081
|
+
polygon: geometry.polygon,
|
|
4082
|
+
polyline: undefined,
|
|
4083
|
+
rect: geometry.rect,
|
|
4084
|
+
};
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
function canReuseMaskStyleArtifacts(previousMaskStyle, nextMaskStyle) {
|
|
4088
|
+
if (previousMaskStyle === nextMaskStyle) {
|
|
4089
|
+
return true;
|
|
4090
|
+
}
|
|
4091
|
+
return (previousMaskStyle?.artifactKey !== undefined &&
|
|
4092
|
+
nextMaskStyle?.artifactKey !== undefined &&
|
|
4093
|
+
previousMaskStyle.artifactKey === nextMaskStyle.artifactKey);
|
|
4094
|
+
}
|
|
4095
|
+
function resolveMaskStyleOpacity(maskStyle) {
|
|
4096
|
+
const opacity = maskStyle?.opacity;
|
|
4097
|
+
if (opacity === undefined) {
|
|
4098
|
+
return 1;
|
|
4099
|
+
}
|
|
4100
|
+
return Number.isFinite(opacity) ? Math.max(0, Math.min(opacity, 1)) : 1;
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
const MAX_ID_MASK_PALETTE_ENTRIES = 64;
|
|
4104
|
+
const MAX_ID_MASK_STROKE_WIDTH = 16;
|
|
4105
|
+
function createIdMaskFrame(instructions) {
|
|
4106
|
+
if (instructions.length === 0) {
|
|
4107
|
+
return undefined;
|
|
4108
|
+
}
|
|
4109
|
+
const width = Math.max(...instructions.map(({ mask }) => mask.width));
|
|
4110
|
+
const height = Math.max(...instructions.map(({ mask }) => mask.height));
|
|
4111
|
+
const data = new Uint8Array(new ArrayBuffer(width * height));
|
|
4112
|
+
const fillPalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));
|
|
4113
|
+
const strokePalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));
|
|
4114
|
+
const strokeWidths = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));
|
|
4115
|
+
let hasStroke = false;
|
|
4116
|
+
let maxStrokeWidth = 0;
|
|
4117
|
+
for (const instruction of instructions) {
|
|
4118
|
+
const detectionMaskId = instruction.detectionIndex + 1;
|
|
4119
|
+
if (detectionMaskId <= 0 ||
|
|
4120
|
+
detectionMaskId >= MAX_ID_MASK_PALETTE_ENTRIES) {
|
|
4121
|
+
return undefined;
|
|
4122
|
+
}
|
|
4123
|
+
writePaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);
|
|
4124
|
+
if (instruction.stroke && instruction.stroke.width > 0) {
|
|
4125
|
+
const strokeWidth = Math.min(Math.max(0, instruction.stroke.width), MAX_ID_MASK_STROKE_WIDTH);
|
|
4126
|
+
hasStroke = true;
|
|
4127
|
+
strokeWidths[detectionMaskId] = strokeWidth;
|
|
4128
|
+
maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth);
|
|
4129
|
+
writePaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);
|
|
4130
|
+
}
|
|
4131
|
+
const decodedMask = decodeCompressedRleMask(instruction.mask);
|
|
4132
|
+
for (let y = 0; y < decodedMask.height; y += 1) {
|
|
4133
|
+
for (let x = 0; x < decodedMask.width; x += 1) {
|
|
4134
|
+
const maskOffset = y * decodedMask.width + x;
|
|
4135
|
+
if (decodedMask.data[maskOffset]) {
|
|
4136
|
+
data[y * width + x] = detectionMaskId;
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
return {
|
|
4142
|
+
data,
|
|
4143
|
+
fillPalette,
|
|
4144
|
+
hasStroke,
|
|
4145
|
+
height,
|
|
4146
|
+
maxStrokeWidth,
|
|
4147
|
+
strokePalette,
|
|
4148
|
+
strokeWidths,
|
|
4149
|
+
width,
|
|
4150
|
+
};
|
|
4151
|
+
}
|
|
4152
|
+
function writePaletteEntry(palette, id, color, alpha) {
|
|
4153
|
+
const offset = id * 4;
|
|
4154
|
+
palette[offset] = ((color >> 16) & 0xff) / 255;
|
|
4155
|
+
palette[offset + 1] = ((color >> 8) & 0xff) / 255;
|
|
4156
|
+
palette[offset + 2] = (color & 0xff) / 255;
|
|
4157
|
+
palette[offset + 3] = Math.max(0, Math.min(alpha, 1));
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
function includeDefined(values) {
|
|
4161
|
+
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
function resolveAnnotationStyleState(detection, visibility) {
|
|
4165
|
+
const id = detection.id;
|
|
4166
|
+
return {
|
|
4167
|
+
hidden: visibility?.annotationsHidden === true ||
|
|
4168
|
+
(detection.className !== undefined &&
|
|
4169
|
+
includes(visibility?.hiddenClasses, detection.className)) ||
|
|
4170
|
+
(id !== undefined && includes(visibility?.hiddenDetectionIds, id)),
|
|
4171
|
+
loading: id !== undefined && includes(visibility?.loadingDetectionIds, id),
|
|
4172
|
+
ephemeral: id !== undefined && includes(visibility?.ephemeralDetectionIds, id),
|
|
4173
|
+
isCreating: id !== undefined && visibility?.creatingDetectionId === id,
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
function includes(values, value) {
|
|
4177
|
+
if (!values)
|
|
4178
|
+
return false;
|
|
4179
|
+
return Array.isArray(values)
|
|
4180
|
+
? values.includes(value)
|
|
4181
|
+
: values.has(value);
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
var MediaRendererFit;
|
|
4185
|
+
(function (MediaRendererFit) {
|
|
4186
|
+
/**
|
|
4187
|
+
* Preserve media aspect ratio and fit the full frame inside the canvas.
|
|
4188
|
+
*/
|
|
4189
|
+
MediaRendererFit["Contain"] = "contain";
|
|
4190
|
+
/**
|
|
4191
|
+
* Preserve media aspect ratio and fill the canvas, cropping if necessary.
|
|
4192
|
+
*/
|
|
4193
|
+
MediaRendererFit["Cover"] = "cover";
|
|
4194
|
+
})(MediaRendererFit || (MediaRendererFit = {}));
|
|
4195
|
+
/**
|
|
4196
|
+
* Playback lifecycle state reported by a platform renderer.
|
|
4197
|
+
*/
|
|
4198
|
+
var MediaRendererPlaybackState;
|
|
4199
|
+
(function (MediaRendererPlaybackState) {
|
|
4200
|
+
MediaRendererPlaybackState["Loading"] = "loading";
|
|
4201
|
+
MediaRendererPlaybackState["Ready"] = "ready";
|
|
4202
|
+
MediaRendererPlaybackState["Playing"] = "playing";
|
|
4203
|
+
MediaRendererPlaybackState["Buffering"] = "buffering";
|
|
4204
|
+
MediaRendererPlaybackState["Paused"] = "paused";
|
|
4205
|
+
MediaRendererPlaybackState["Error"] = "error";
|
|
4206
|
+
MediaRendererPlaybackState["Destroyed"] = "destroyed";
|
|
4207
|
+
})(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));
|
|
4208
|
+
/**
|
|
4209
|
+
* Lower-level media source readiness.
|
|
4210
|
+
*/
|
|
4211
|
+
var MediaSourceStatus;
|
|
4212
|
+
(function (MediaSourceStatus) {
|
|
4213
|
+
MediaSourceStatus["Loading"] = "loading";
|
|
4214
|
+
MediaSourceStatus["Ready"] = "ready";
|
|
4215
|
+
MediaSourceStatus["Error"] = "error";
|
|
4216
|
+
MediaSourceStatus["Destroyed"] = "destroyed";
|
|
4217
|
+
})(MediaSourceStatus || (MediaSourceStatus = {}));
|
|
4218
|
+
|
|
4219
|
+
/**
|
|
4220
|
+
* Media-session operating mode.
|
|
4221
|
+
*
|
|
4222
|
+
* Core owns the names because file-like and stream-like lifecycle choices are
|
|
4223
|
+
* platform-neutral. Platform packages decide how these modes tune media,
|
|
4224
|
+
* storage, and renderer defaults.
|
|
4225
|
+
*/
|
|
4226
|
+
var MediaSessionMode;
|
|
4227
|
+
(function (MediaSessionMode) {
|
|
4228
|
+
/**
|
|
4229
|
+
* Finite media. Defaults usually favor seek/replay and persistent detection
|
|
4230
|
+
* storage.
|
|
4231
|
+
*/
|
|
4232
|
+
MediaSessionMode["File"] = "file";
|
|
4233
|
+
/**
|
|
4234
|
+
* Live or append-only media. Defaults usually favor rolling windows and
|
|
4235
|
+
* bounded retention.
|
|
4236
|
+
*/
|
|
4237
|
+
MediaSessionMode["Stream"] = "stream";
|
|
4238
|
+
})(MediaSessionMode || (MediaSessionMode = {}));
|
|
4239
|
+
/**
|
|
4240
|
+
* Aggregate lifecycle state for a media session.
|
|
4241
|
+
*/
|
|
4242
|
+
var MediaSessionStatus;
|
|
4243
|
+
(function (MediaSessionStatus) {
|
|
4244
|
+
MediaSessionStatus["Buffering"] = "buffering";
|
|
4245
|
+
MediaSessionStatus["Destroyed"] = "destroyed";
|
|
4246
|
+
MediaSessionStatus["Error"] = "error";
|
|
4247
|
+
MediaSessionStatus["Loading"] = "loading";
|
|
4248
|
+
MediaSessionStatus["Paused"] = "paused";
|
|
4249
|
+
MediaSessionStatus["Playing"] = "playing";
|
|
4250
|
+
MediaSessionStatus["Processing"] = "processing";
|
|
4251
|
+
MediaSessionStatus["Ready"] = "ready";
|
|
4252
|
+
})(MediaSessionStatus || (MediaSessionStatus = {}));
|
|
4253
|
+
/**
|
|
4254
|
+
* Subsystem currently affecting session readiness or presentation.
|
|
4255
|
+
*/
|
|
4256
|
+
var MediaSessionActivityKind;
|
|
4257
|
+
(function (MediaSessionActivityKind) {
|
|
4258
|
+
MediaSessionActivityKind["DetectionsBuffering"] = "detectionsBuffering";
|
|
4259
|
+
MediaSessionActivityKind["DetectionsLoading"] = "detectionsLoading";
|
|
4260
|
+
MediaSessionActivityKind["Error"] = "error";
|
|
4261
|
+
MediaSessionActivityKind["MediaNormalizing"] = "mediaNormalizing";
|
|
4262
|
+
MediaSessionActivityKind["MediaOpening"] = "mediaOpening";
|
|
4263
|
+
MediaSessionActivityKind["PlaybackBuffering"] = "playbackBuffering";
|
|
4264
|
+
MediaSessionActivityKind["RenderPreparing"] = "renderPreparing";
|
|
4265
|
+
})(MediaSessionActivityKind || (MediaSessionActivityKind = {}));
|
|
4266
|
+
/**
|
|
4267
|
+
* State of one session activity.
|
|
4268
|
+
*/
|
|
4269
|
+
var MediaSessionActivityStatus;
|
|
4270
|
+
(function (MediaSessionActivityStatus) {
|
|
4271
|
+
MediaSessionActivityStatus["Error"] = "error";
|
|
4272
|
+
MediaSessionActivityStatus["Running"] = "running";
|
|
4273
|
+
MediaSessionActivityStatus["Waiting"] = "waiting";
|
|
4274
|
+
})(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));
|
|
4275
|
+
|
|
4276
|
+
export { AnnotationFrameMutationKind, AnnotationGeometryKind, AnnotationGestureStateKind, AnnotationHandleKind, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMaskStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionMaskPayloadFormat, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MAX_ID_MASK_PALETTE_ENTRIES, MAX_ID_MASK_STROKE_WIDTH, MaskRenderMode, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, SUPERVISION_ROBOFLOW_COLOR, applyAnnotationHandleDrag, canReuseMaskStyleArtifacts, centerRectToTopLeftRect, computeDetectionMaskRect, computeMaskBounds, containsPoint, convertDetectionBoxToMask, convertDetectionBoxToPolygon, convertDetectionMaskToBox, convertDetectionMaskToPolygon, convertDetectionPolygonToBox, convertDetectionPolygonToMask, copySortedDetectionFrames, createAnnotationEditingEngine, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createDetectionPickKey, createEditableAnnotationFrameSession, createIdMaskFrame, createIdleDetectionBufferState, createMemoryColdDetectionFrameStore, createSourceAwarePresentation, createViewportController, createWritableDetectionFrameSource, decodeCompressedRleCounts, decodeCompressedRleMask, decodeDetectionMaskPayload, deleteAnnotationVertex, detectMaskBorders, detectionFrameOverlapsRange, distanceToSegment, encodeBinaryMask, encodeBinaryMaskWithBounds, encodeCompressedRleCounts, encodeDetectionMaskPayload, extractMaskContour, extractMaskRectRuns, filterDetectionFramesForRange, findClosestAnnotationSegment, getAnnotationHandles, getBufferedDetectionTimelineFrameSnapshot, getDetectionRect, getPointsRect, includeDefined, isDeflatedBase64DetectionMaskPayload, lightenColor, mediaToScreen, mergeDetectionMasks, mergeDetectionPolygonsByClass, normalizeDetectionClassName, offsetDetection, pickAnnotationHandle, pickDetectionAtPoint, pickDetectionByMaskId, pointInPolygon, polygonArea, polygonToRect, rasterizePolygonToMask, rasterizeRectToMask, rebaseDetectionPickToFrame, rectArea, rectToPolygon, resolveAnnotationStyleState, resolveContrastTextColor, resolveDetectionClassColorStyle, resolveMaskStyleOpacity, resolveStyleValue, screenToMedia, selectDetectionFrame, topLeftRectToCenterRect, validateDetectionFrames };
|
|
4277
|
+
//# sourceMappingURL=index.js.map
|