supervision 0.1.4 → 0.1.6
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/dist/index.d.ts +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +864 -127
- package/dist/index.js.map +1 -1
- package/dist/mask-preparation.worker.js +91 -59
- package/dist/mask-preparation.worker.js.map +1 -1
- package/dist/media/media-errors.d.ts +32 -0
- package/dist/media/media-errors.d.ts.map +1 -0
- package/dist/media/media-source-state.d.ts.map +1 -1
- package/dist/media/media-stream-media-source.d.ts +31 -0
- package/dist/media/media-stream-media-source.d.ts.map +1 -1
- package/dist/media/mediabunny-media-source.d.ts.map +1 -1
- package/dist/media/presentation-timeline-media-source.d.ts +17 -0
- package/dist/media/presentation-timeline-media-source.d.ts.map +1 -0
- package/dist/media/static-image-media-source.d.ts.map +1 -1
- package/dist/post-processing/default-tracking-worker.d.ts +3 -0
- package/dist/post-processing/default-tracking-worker.d.ts.map +1 -0
- package/dist/post-processing/detection-post-processing-pipeline.d.ts +7 -0
- package/dist/post-processing/detection-post-processing-pipeline.d.ts.map +1 -0
- package/dist/post-processing/embedded-tracking-worker.d.ts +2 -0
- package/dist/post-processing/embedded-tracking-worker.d.ts.map +1 -0
- package/dist/post-processing/tracking-worker-protocol.d.ts +29 -0
- package/dist/post-processing/tracking-worker-protocol.d.ts.map +1 -0
- package/dist/post-processing/tracking.worker.d.ts +2 -0
- package/dist/post-processing/tracking.worker.d.ts.map +1 -0
- package/dist/renderers/media-renderer-core.d.ts.map +1 -1
- package/dist/renderers/media-renderer-state.d.ts.map +1 -1
- package/dist/sessions/media-session-detections.d.ts.map +1 -1
- package/dist/sessions/media-session.d.ts +2 -2
- package/dist/sessions/media-session.d.ts.map +1 -1
- package/dist/tracking.worker.js +1509 -0
- package/dist/tracking.worker.js.map +1 -0
- package/dist/types/detection-post-processing.d.ts +62 -0
- package/dist/types/detection-post-processing.d.ts.map +1 -0
- package/dist/types/media-session.d.ts +57 -1
- package/dist/types/media-session.d.ts.map +1 -1
- package/dist/workers/worker-rpc-client.d.ts +2 -1
- package/dist/workers/worker-rpc-client.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/detections/composite-detection-frame-source.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/detections/memory-cold-detection-frame-store.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/detections/projected-detection-frame-source.d.ts +22 -0
- package/node_modules/supervision-js-core/dist/detections/projected-detection-frame-source.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/detections/writable-detection-frame-source.d.ts +2 -7
- package/node_modules/supervision-js-core/dist/detections/writable-detection-frame-source.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/index.d.ts +11 -3
- package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/index.js +2289 -329
- package/node_modules/supervision-js-core/dist/index.js.map +1 -1
- package/node_modules/supervision-js-core/dist/post-processing/byte-track-tracker.d.ts +4 -0
- package/node_modules/supervision-js-core/dist/post-processing/byte-track-tracker.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/post-processing/cbiou-tracker.d.ts +4 -0
- package/node_modules/supervision-js-core/dist/post-processing/cbiou-tracker.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/post-processing/oc-sort-tracker.d.ts +4 -0
- package/node_modules/supervision-js-core/dist/post-processing/oc-sort-tracker.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/post-processing/sort-tracker.d.ts +4 -0
- package/node_modules/supervision-js-core/dist/post-processing/sort-tracker.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/post-processing/tracking.d.ts +9 -0
- package/node_modules/supervision-js-core/dist/post-processing/tracking.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +113 -3
- package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/detections.d.ts +33 -3
- package/node_modules/supervision-js-core/dist/types/detections.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +31 -0
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/post-processing.d.ts +148 -0
- package/node_modules/supervision-js-core/dist/types/post-processing.d.ts.map +1 -0
- package/node_modules/supervision-js-core/dist/utils/detection-frames.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/utils/detection-projection.d.ts +20 -0
- package/node_modules/supervision-js-core/dist/utils/detection-projection.d.ts.map +1 -0
- package/node_modules/supervision-js-core/package.json +12 -5
- package/package.json +7 -2
|
@@ -88,6 +88,9 @@ function copySortedDetectionFrames(detectionFrames) {
|
|
|
88
88
|
: undefined,
|
|
89
89
|
rect: detection.rect ? { ...detection.rect } : undefined,
|
|
90
90
|
})),
|
|
91
|
+
coordinateSpace: frame.coordinateSpace
|
|
92
|
+
? { ...frame.coordinateSpace }
|
|
93
|
+
: undefined,
|
|
91
94
|
endTime: frame.endTime,
|
|
92
95
|
frameIndex: frame.frameIndex,
|
|
93
96
|
mediaTime: frame.mediaTime,
|
|
@@ -129,6 +132,10 @@ function validateDetectionFrames(detectionFrames) {
|
|
|
129
132
|
min: 0,
|
|
130
133
|
});
|
|
131
134
|
}
|
|
135
|
+
if (frame.coordinateSpace !== undefined) {
|
|
136
|
+
validateNumber(frame.coordinateSpace.width, `frames[${frameOffset}].coordinateSpace.width`, { exclusiveMin: 0 });
|
|
137
|
+
validateNumber(frame.coordinateSpace.height, `frames[${frameOffset}].coordinateSpace.height`, { exclusiveMin: 0 });
|
|
138
|
+
}
|
|
132
139
|
for (const [detectionOffset, detection] of frame.detections.entries()) {
|
|
133
140
|
const detectionPath = `frames[${frameOffset}].detections[${detectionOffset}]`;
|
|
134
141
|
if (detection.confidence !== undefined) {
|
|
@@ -140,6 +147,12 @@ function validateDetectionFrames(detectionFrames) {
|
|
|
140
147
|
if (detection.zIndex !== undefined) {
|
|
141
148
|
validateNumber(detection.zIndex, `${detectionPath}.zIndex`);
|
|
142
149
|
}
|
|
150
|
+
if (detection.trackerId !== undefined) {
|
|
151
|
+
validateNumber(detection.trackerId, `${detectionPath}.trackerId`, {
|
|
152
|
+
integer: true,
|
|
153
|
+
min: 0,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
143
156
|
if (detection.sourceId !== undefined &&
|
|
144
157
|
typeof detection.sourceId !== "string") {
|
|
145
158
|
throw new Error(`${detectionPath}.sourceId must be a string.`);
|
|
@@ -882,7 +895,7 @@ function mergeIncrementalFrames(currentFrames, changedFrames, changedRanges) {
|
|
|
882
895
|
for (const frame of changedFrames) {
|
|
883
896
|
framesByIdentity.set(getDetectionFrameIdentity(frame), frame);
|
|
884
897
|
}
|
|
885
|
-
return Array.from(framesByIdentity.values()).sort(compareDetectionFrames);
|
|
898
|
+
return Array.from(framesByIdentity.values()).sort(compareDetectionFrames$1);
|
|
886
899
|
}
|
|
887
900
|
function reuseBufferedFrameSnapshots(currentFrames, loadedFrames) {
|
|
888
901
|
const currentFramesByIdentity = new Map(currentFrames.map((frame) => [getDetectionFrameIdentity(frame), frame]));
|
|
@@ -893,7 +906,7 @@ function getDetectionFrameIdentity(frame) {
|
|
|
893
906
|
? `time:${frame.mediaTime}`
|
|
894
907
|
: `index:${frame.frameIndex}`;
|
|
895
908
|
}
|
|
896
|
-
function compareDetectionFrames(left, right) {
|
|
909
|
+
function compareDetectionFrames$1(left, right) {
|
|
897
910
|
if (left.mediaTime !== right.mediaTime) {
|
|
898
911
|
return left.mediaTime - right.mediaTime;
|
|
899
912
|
}
|
|
@@ -912,21 +925,118 @@ function createColdDetectionFrameSource(options) {
|
|
|
912
925
|
};
|
|
913
926
|
}
|
|
914
927
|
|
|
928
|
+
/**
|
|
929
|
+
* Projects one detection frame from its declared source coordinate space into
|
|
930
|
+
* `target`.
|
|
931
|
+
*
|
|
932
|
+
* Rectangles, polygons, polylines, and keypoints are scaled. Masks are left
|
|
933
|
+
* untouched because a `DetectionMask` already carries the mask-pixel
|
|
934
|
+
* dimensions its `counts` are encoded against; scaling them here would apply
|
|
935
|
+
* the ratio twice. Frames without `coordinateSpace`, with a degenerate space,
|
|
936
|
+
* or already in the target space are returned unchanged so mixed producers can
|
|
937
|
+
* share one source.
|
|
938
|
+
*/
|
|
939
|
+
function projectDetectionFrame(frame, target) {
|
|
940
|
+
const space = frame.coordinateSpace;
|
|
941
|
+
if (!space ||
|
|
942
|
+
!isUsableCoordinateSpace(space) ||
|
|
943
|
+
!isUsableCoordinateSpace(target) ||
|
|
944
|
+
(space.width === target.width && space.height === target.height)) {
|
|
945
|
+
return frame;
|
|
946
|
+
}
|
|
947
|
+
const scaleX = target.width / space.width;
|
|
948
|
+
const scaleY = target.height / space.height;
|
|
949
|
+
return {
|
|
950
|
+
...frame,
|
|
951
|
+
coordinateSpace: { height: target.height, width: target.width },
|
|
952
|
+
detections: frame.detections.map((detection) => projectDetection(detection, scaleX, scaleY)),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Projects every frame that declares a source coordinate space into `target`.
|
|
957
|
+
*
|
|
958
|
+
* Frames without coordinate metadata pass through by reference.
|
|
959
|
+
*/
|
|
960
|
+
function projectDetectionFrames(frames, target) {
|
|
961
|
+
let didProject = false;
|
|
962
|
+
const projectedFrames = frames.map((frame) => {
|
|
963
|
+
const projectedFrame = projectDetectionFrame(frame, target);
|
|
964
|
+
didProject ||= projectedFrame !== frame;
|
|
965
|
+
return projectedFrame;
|
|
966
|
+
});
|
|
967
|
+
return didProject ? projectedFrames : frames;
|
|
968
|
+
}
|
|
969
|
+
function projectDetection(detection, scaleX, scaleY) {
|
|
970
|
+
return {
|
|
971
|
+
...detection,
|
|
972
|
+
...(detection.rect
|
|
973
|
+
? { rect: scaleRect(detection.rect, scaleX, scaleY) }
|
|
974
|
+
: {}),
|
|
975
|
+
...(detection.polygon
|
|
976
|
+
? { polygon: scalePolygon(detection.polygon, scaleX, scaleY) }
|
|
977
|
+
: {}),
|
|
978
|
+
...(detection.polyline
|
|
979
|
+
? { polyline: scalePolyline(detection.polyline, scaleX, scaleY) }
|
|
980
|
+
: {}),
|
|
981
|
+
...(detection.keypoints
|
|
982
|
+
? { keypoints: scaleKeypoints(detection.keypoints, scaleX, scaleY) }
|
|
983
|
+
: {}),
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function scaleRect(rect, scaleX, scaleY) {
|
|
987
|
+
return {
|
|
988
|
+
height: rect.height * scaleY,
|
|
989
|
+
width: rect.width * scaleX,
|
|
990
|
+
x: rect.x * scaleX,
|
|
991
|
+
y: rect.y * scaleY,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function scalePolygon(polygon, scaleX, scaleY) {
|
|
995
|
+
return { points: scalePoints(polygon.points, scaleX, scaleY) };
|
|
996
|
+
}
|
|
997
|
+
function scalePolyline(polyline, scaleX, scaleY) {
|
|
998
|
+
return { points: scalePoints(polyline.points, scaleX, scaleY) };
|
|
999
|
+
}
|
|
1000
|
+
function scaleKeypoints(keypoints, scaleX, scaleY) {
|
|
1001
|
+
return {
|
|
1002
|
+
...keypoints,
|
|
1003
|
+
points: scalePoints(keypoints.points, scaleX, scaleY),
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
function scalePoints(points, scaleX, scaleY) {
|
|
1007
|
+
return points.map((point) => ({ x: point.x * scaleX, y: point.y * scaleY }));
|
|
1008
|
+
}
|
|
1009
|
+
function isUsableCoordinateSpace(space) {
|
|
1010
|
+
return (Number.isFinite(space.width) &&
|
|
1011
|
+
Number.isFinite(space.height) &&
|
|
1012
|
+
space.width > 0 &&
|
|
1013
|
+
space.height > 0);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
915
1016
|
function createCompositeDetectionFrameSource(options) {
|
|
916
1017
|
const sources = normalizeCompositeSources(options.sources);
|
|
917
1018
|
return {
|
|
918
|
-
async loadFrames(startTime, endTime) {
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
frames
|
|
922
|
-
|
|
1019
|
+
async loadFrames(startTime, endTime, loadOptions) {
|
|
1020
|
+
const target = loadOptions?.coordinateSpace;
|
|
1021
|
+
const loadedSources = await Promise.all(sources.map(async (source) => {
|
|
1022
|
+
const frames = copySortedDetectionFrames(await source.source.loadFrames(startTime, endTime, loadOptions));
|
|
1023
|
+
return {
|
|
1024
|
+
...source,
|
|
1025
|
+
// Composition flattens child detections into one frame, which can
|
|
1026
|
+
// only carry one coordinate space. Each child is projected here,
|
|
1027
|
+
// while its own `coordinateSpace` is still attached to its own
|
|
1028
|
+
// detections, so children inferred at different sizes compose
|
|
1029
|
+
// correctly. Masks keep their intrinsic dimensions.
|
|
1030
|
+
frames: target ? projectDetectionFrames(frames, target) : frames,
|
|
1031
|
+
};
|
|
1032
|
+
}));
|
|
923
1033
|
if (options.selectionMode === DetectionFrameSelectionMode.NearestFrameIndex) {
|
|
924
|
-
const nearestFrames = composeNearestFrameIndexFrames(loadedSources, startTime, endTime, options);
|
|
1034
|
+
const nearestFrames = composeNearestFrameIndexFrames(loadedSources, startTime, endTime, options, target);
|
|
925
1035
|
if (nearestFrames) {
|
|
926
1036
|
return nearestFrames;
|
|
927
1037
|
}
|
|
928
1038
|
}
|
|
929
|
-
return composeIntervalFrames(loadedSources, startTime, endTime, options);
|
|
1039
|
+
return composeIntervalFrames(loadedSources, startTime, endTime, options, target);
|
|
930
1040
|
},
|
|
931
1041
|
async waitForRange(range) {
|
|
932
1042
|
await Promise.all(sources
|
|
@@ -973,7 +1083,7 @@ function normalizeCompositeSources(entries) {
|
|
|
973
1083
|
.sort((left, right) => left.order - right.order ||
|
|
974
1084
|
left.declarationIndex - right.declarationIndex);
|
|
975
1085
|
}
|
|
976
|
-
function composeIntervalFrames(sources, startTime, endTime, options) {
|
|
1086
|
+
function composeIntervalFrames(sources, startTime, endTime, options, coordinateSpace) {
|
|
977
1087
|
const boundaryTimes = new Set([startTime]);
|
|
978
1088
|
for (const source of sources) {
|
|
979
1089
|
for (const frame of source.frames) {
|
|
@@ -995,17 +1105,14 @@ function composeIntervalFrames(sources, startTime, endTime, options) {
|
|
|
995
1105
|
}
|
|
996
1106
|
const nextBoundaryTime = sortedBoundaryTimes[boundaryIndex + 1] ?? endTime;
|
|
997
1107
|
const endTimeForFrame = Math.min(nextBoundaryTime, endTime);
|
|
998
|
-
const frame = composeFrameAtTime(sources, mediaTime, endTimeForFrame, {
|
|
999
|
-
...options,
|
|
1000
|
-
selectionMode: DetectionFrameSelectionMode.Interval,
|
|
1001
|
-
});
|
|
1108
|
+
const frame = composeFrameAtTime(sources, mediaTime, endTimeForFrame, { ...options, selectionMode: DetectionFrameSelectionMode.Interval }, undefined, coordinateSpace);
|
|
1002
1109
|
if (frame) {
|
|
1003
1110
|
frames.push(frame);
|
|
1004
1111
|
}
|
|
1005
1112
|
}
|
|
1006
1113
|
return frames;
|
|
1007
1114
|
}
|
|
1008
|
-
function composeNearestFrameIndexFrames(sources, startTime, endTime, options) {
|
|
1115
|
+
function composeNearestFrameIndexFrames(sources, startTime, endTime, options, coordinateSpace) {
|
|
1009
1116
|
const frameRate = options.frameRate;
|
|
1010
1117
|
if (!frameRate || !Number.isFinite(frameRate) || frameRate <= 0) {
|
|
1011
1118
|
return null;
|
|
@@ -1031,14 +1138,14 @@ function composeNearestFrameIndexFrames(sources, startTime, endTime, options) {
|
|
|
1031
1138
|
const frame = composeFrameAtTime(sources, mediaTime, Math.min(mediaTime + 1 / frameRate, endTime), {
|
|
1032
1139
|
...options,
|
|
1033
1140
|
selectionMode: DetectionFrameSelectionMode.NearestFrameIndex,
|
|
1034
|
-
}, frameIndex);
|
|
1141
|
+
}, frameIndex, coordinateSpace);
|
|
1035
1142
|
if (frame) {
|
|
1036
1143
|
frames.push(frame);
|
|
1037
1144
|
}
|
|
1038
1145
|
}
|
|
1039
1146
|
return frames;
|
|
1040
1147
|
}
|
|
1041
|
-
function composeFrameAtTime(sources, mediaTime, endTime, options, frameIndex) {
|
|
1148
|
+
function composeFrameAtTime(sources, mediaTime, endTime, options, frameIndex, coordinateSpace) {
|
|
1042
1149
|
const detections = [];
|
|
1043
1150
|
const activeFrameIndexes = [];
|
|
1044
1151
|
for (const source of sources) {
|
|
@@ -1064,6 +1171,9 @@ function composeFrameAtTime(sources, mediaTime, endTime, options, frameIndex) {
|
|
|
1064
1171
|
endTime,
|
|
1065
1172
|
frameIndex: frameIndex ?? resolveComposedFrameIndex(activeFrameIndexes) ?? undefined,
|
|
1066
1173
|
mediaTime,
|
|
1174
|
+
// Children were projected before composition, so a composed frame is
|
|
1175
|
+
// already in the coordinate space the renderer presents.
|
|
1176
|
+
...(coordinateSpace ? { coordinateSpace } : {}),
|
|
1067
1177
|
};
|
|
1068
1178
|
}
|
|
1069
1179
|
function copyDetectionWithSource(detection, sourceId, sourceDetectionIndex) {
|
|
@@ -1278,6 +1388,16 @@ function createMemoryColdDetectionFrameStore() {
|
|
|
1278
1388
|
detectionFrameOverlapsRange(frame, startTime, endTime));
|
|
1279
1389
|
return copySortedDetectionFrames(frames);
|
|
1280
1390
|
},
|
|
1391
|
+
async pruneFrames(options) {
|
|
1392
|
+
assertActive();
|
|
1393
|
+
assertRetentionFloor(options.startTime);
|
|
1394
|
+
const dataset = datasets.get(options.datasetId);
|
|
1395
|
+
if (!dataset) {
|
|
1396
|
+
return createWriteSummary(options.datasetId, createDataset(DEFAULT_CHUNK_DURATION_SECONDS));
|
|
1397
|
+
}
|
|
1398
|
+
pruneDatasetBefore(dataset, options.startTime);
|
|
1399
|
+
return createWriteSummary(options.datasetId, dataset);
|
|
1400
|
+
},
|
|
1281
1401
|
async clearDataset(datasetId) {
|
|
1282
1402
|
assertActive();
|
|
1283
1403
|
datasets.delete(datasetId);
|
|
@@ -1316,7 +1436,7 @@ function upsertFrames(dataset, frames) {
|
|
|
1316
1436
|
dataset.framesByKey.set(frameKey, { chunkIndexes, frame });
|
|
1317
1437
|
dataset.detectionCount += frame.detections.length;
|
|
1318
1438
|
dataset.startTime = Math.min(dataset.startTime ?? frame.mediaTime, frame.mediaTime);
|
|
1319
|
-
dataset.endTime = Math.max(dataset.endTime ?? getFrameEndTime(frame), getFrameEndTime(frame));
|
|
1439
|
+
dataset.endTime = Math.max(dataset.endTime ?? getFrameEndTime$1(frame), getFrameEndTime$1(frame));
|
|
1320
1440
|
for (const chunkIndex of chunkIndexes) {
|
|
1321
1441
|
const frameKeys = dataset.frameKeysByChunk.get(chunkIndex) ?? new Set();
|
|
1322
1442
|
frameKeys.add(frameKey);
|
|
@@ -1338,18 +1458,81 @@ function removeStoredFrame(dataset, frameKey, storedFrame) {
|
|
|
1338
1458
|
}
|
|
1339
1459
|
}
|
|
1340
1460
|
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Drops frames that end before `startTime`.
|
|
1463
|
+
*
|
|
1464
|
+
* Only chunks at or below the retention boundary are visited, and the new
|
|
1465
|
+
* start bound is read from the lowest surviving chunk, so eviction costs work
|
|
1466
|
+
* proportional to the evicted range rather than to retained history.
|
|
1467
|
+
*/
|
|
1468
|
+
function pruneDatasetBefore(dataset, startTime) {
|
|
1469
|
+
const boundaryChunkIndex = getChunkIndex(Math.max(0, startTime), dataset.chunkDurationSeconds);
|
|
1470
|
+
let didRemoveFrame = false;
|
|
1471
|
+
for (const chunkIndex of Array.from(dataset.frameKeysByChunk.keys())) {
|
|
1472
|
+
if (chunkIndex > boundaryChunkIndex) {
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
for (const frameKey of Array.from(dataset.frameKeysByChunk.get(chunkIndex) ?? [])) {
|
|
1476
|
+
const storedFrame = dataset.framesByKey.get(frameKey);
|
|
1477
|
+
if (!storedFrame || isFrameRetainedFrom(storedFrame.frame, startTime)) {
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
removeStoredFrame(dataset, frameKey, storedFrame);
|
|
1481
|
+
didRemoveFrame = true;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (!didRemoveFrame) {
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
if (dataset.framesByKey.size === 0) {
|
|
1488
|
+
dataset.startTime = null;
|
|
1489
|
+
dataset.endTime = null;
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
// Every frame is registered in the chunk its `mediaTime` falls in, so the
|
|
1493
|
+
// earliest retained frame lives in the lowest remaining chunk. Removing
|
|
1494
|
+
// frames can only raise the start bound; `endTime` is unchanged because only
|
|
1495
|
+
// frames ending before `startTime` were dropped.
|
|
1496
|
+
dataset.startTime = getEarliestChunkStartTime(dataset);
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Mirrors `detectionFrameOverlapsRange` so pruning keeps exactly the frames a
|
|
1500
|
+
* load from `startTime` would return.
|
|
1501
|
+
*/
|
|
1502
|
+
function isFrameRetainedFrom(frame, startTime) {
|
|
1503
|
+
return frame.endTime === undefined
|
|
1504
|
+
? frame.mediaTime >= startTime
|
|
1505
|
+
: frame.endTime > startTime;
|
|
1506
|
+
}
|
|
1507
|
+
function getEarliestChunkStartTime(dataset) {
|
|
1508
|
+
let earliestChunkIndex = null;
|
|
1509
|
+
for (const chunkIndex of dataset.frameKeysByChunk.keys()) {
|
|
1510
|
+
if (earliestChunkIndex === null || chunkIndex < earliestChunkIndex) {
|
|
1511
|
+
earliestChunkIndex = chunkIndex;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
let startTime = null;
|
|
1515
|
+
for (const frameKey of dataset.frameKeysByChunk.get(earliestChunkIndex ?? 0) ?? []) {
|
|
1516
|
+
const storedFrame = dataset.framesByKey.get(frameKey);
|
|
1517
|
+
if (!storedFrame) {
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
startTime = Math.min(startTime ?? storedFrame.frame.mediaTime, storedFrame.frame.mediaTime);
|
|
1521
|
+
}
|
|
1522
|
+
return startTime;
|
|
1523
|
+
}
|
|
1341
1524
|
function replacementCanShrinkBounds(dataset, previousFrame, nextFrame) {
|
|
1342
1525
|
return ((dataset.startTime === previousFrame.mediaTime &&
|
|
1343
1526
|
nextFrame.mediaTime > previousFrame.mediaTime) ||
|
|
1344
|
-
(dataset.endTime === getFrameEndTime(previousFrame) &&
|
|
1345
|
-
getFrameEndTime(nextFrame) < getFrameEndTime(previousFrame)));
|
|
1527
|
+
(dataset.endTime === getFrameEndTime$1(previousFrame) &&
|
|
1528
|
+
getFrameEndTime$1(nextFrame) < getFrameEndTime$1(previousFrame)));
|
|
1346
1529
|
}
|
|
1347
1530
|
function recalculateBounds(dataset) {
|
|
1348
1531
|
let startTime = null;
|
|
1349
1532
|
let endTime = null;
|
|
1350
1533
|
for (const { frame } of dataset.framesByKey.values()) {
|
|
1351
1534
|
startTime = Math.min(startTime ?? frame.mediaTime, frame.mediaTime);
|
|
1352
|
-
endTime = Math.max(endTime ?? getFrameEndTime(frame), getFrameEndTime(frame));
|
|
1535
|
+
endTime = Math.max(endTime ?? getFrameEndTime$1(frame), getFrameEndTime$1(frame));
|
|
1353
1536
|
}
|
|
1354
1537
|
dataset.startTime = startTime;
|
|
1355
1538
|
dataset.endTime = endTime;
|
|
@@ -1381,10 +1564,10 @@ function createWriteSummary(datasetId, dataset) {
|
|
|
1381
1564
|
}
|
|
1382
1565
|
function getFrameChunkIndexes(frame, chunkDurationSeconds) {
|
|
1383
1566
|
const startIndex = getChunkIndex(frame.mediaTime, chunkDurationSeconds);
|
|
1384
|
-
const endIndex = Math.max(startIndex, Math.ceil(getFrameEndTime(frame) / chunkDurationSeconds) - 1);
|
|
1567
|
+
const endIndex = Math.max(startIndex, Math.ceil(getFrameEndTime$1(frame) / chunkDurationSeconds) - 1);
|
|
1385
1568
|
return Array.from({ length: endIndex - startIndex + 1 }, (_, offset) => startIndex + offset);
|
|
1386
1569
|
}
|
|
1387
|
-
function getFrameEndTime(frame) {
|
|
1570
|
+
function getFrameEndTime$1(frame) {
|
|
1388
1571
|
return frame.endTime ?? frame.mediaTime;
|
|
1389
1572
|
}
|
|
1390
1573
|
function getChunkIndex(mediaTime, chunkDurationSeconds) {
|
|
@@ -1395,11 +1578,87 @@ function getDetectionFrameDedupeKey(frame) {
|
|
|
1395
1578
|
? `time:${frame.mediaTime}`
|
|
1396
1579
|
: `index:${frame.frameIndex}`;
|
|
1397
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Rejects a retention floor that could not describe a real media time.
|
|
1583
|
+
*
|
|
1584
|
+
* Pruning mutates retained history in place, and `NaN` compares false against
|
|
1585
|
+
* every retained frame, so an unvalidated floor would silently erase a dataset
|
|
1586
|
+
* instead of evicting a bounded range.
|
|
1587
|
+
*/
|
|
1588
|
+
function assertRetentionFloor(startTime) {
|
|
1589
|
+
if (!Number.isFinite(startTime) || startTime < 0) {
|
|
1590
|
+
throw new RangeError("pruneFrames requires a finite, non-negative startTime.");
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/**
|
|
1595
|
+
* Wraps a detection source so every loaded frame that declares a source
|
|
1596
|
+
* coordinate space is projected into the target space.
|
|
1597
|
+
*
|
|
1598
|
+
* The target is resolved per load because media dimensions are only known once
|
|
1599
|
+
* the media has opened, while sources are composed before that. Frames without
|
|
1600
|
+
* `coordinateSpace`, and loads made before a target exists, pass through by
|
|
1601
|
+
* reference, so a producer already emitting media-pixel geometry pays nothing.
|
|
1602
|
+
*
|
|
1603
|
+
* The target is also handed to the wrapped source through
|
|
1604
|
+
* `DetectionFrameLoadOptions`. A source that flattens child frames, such as a
|
|
1605
|
+
* composite source, needs it to project each child while that child's own
|
|
1606
|
+
* `coordinateSpace` is still attached; projecting the flattened result again
|
|
1607
|
+
* here is then a no-op.
|
|
1608
|
+
*
|
|
1609
|
+
* Optional source hooks are forwarded only when the wrapped source implements
|
|
1610
|
+
* them, so capability detection keeps working through the wrapper.
|
|
1611
|
+
*/
|
|
1612
|
+
function createProjectedDetectionFrameSource(source, resolveTarget) {
|
|
1613
|
+
const { getAvailableRanges, getChangesSince, getVersion, waitForRange } = source;
|
|
1614
|
+
return {
|
|
1615
|
+
async loadFrames(startTime, endTime, options) {
|
|
1616
|
+
const target = resolveTarget();
|
|
1617
|
+
const frames = await source.loadFrames(startTime, endTime, {
|
|
1618
|
+
...options,
|
|
1619
|
+
...(target ? { coordinateSpace: target } : {}),
|
|
1620
|
+
});
|
|
1621
|
+
return target ? projectDetectionFrames(frames, target) : frames;
|
|
1622
|
+
},
|
|
1623
|
+
...(waitForRange
|
|
1624
|
+
? {
|
|
1625
|
+
waitForRange: (range) => waitForRange.call(source, range),
|
|
1626
|
+
}
|
|
1627
|
+
: {}),
|
|
1628
|
+
...(getAvailableRanges
|
|
1629
|
+
? { getAvailableRanges: () => getAvailableRanges.call(source) }
|
|
1630
|
+
: {}),
|
|
1631
|
+
...(getVersion
|
|
1632
|
+
? {
|
|
1633
|
+
getVersion: (range) => getVersion.call(source, range),
|
|
1634
|
+
}
|
|
1635
|
+
: {}),
|
|
1636
|
+
...(getChangesSince
|
|
1637
|
+
? {
|
|
1638
|
+
getChangesSince: (version, ranges) => getChangesSince.call(source, version, ranges),
|
|
1639
|
+
}
|
|
1640
|
+
: {}),
|
|
1641
|
+
...(source.destroy ? { destroy: () => source.destroy?.() } : {}),
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1398
1644
|
|
|
1399
1645
|
const RANGE_EPSILON_SECONDS = 1e-6;
|
|
1400
1646
|
const MAX_CHANGED_RANGE_JOURNAL_LENGTH = 512;
|
|
1647
|
+
const DEFAULT_LIVE_HOLD_SECONDS = 60;
|
|
1648
|
+
const NO_COVERAGE_INVALIDATION = { ranges: [] };
|
|
1401
1649
|
function createWritableDetectionFrameSource(options) {
|
|
1650
|
+
const liveHoldSeconds = resolveLiveHoldSeconds(options.live?.holdSeconds);
|
|
1402
1651
|
let summary = null;
|
|
1652
|
+
let latestFrame = null;
|
|
1653
|
+
let heldLiveFrame = null;
|
|
1654
|
+
// Newest live `mediaTime` accepted so far. It survives retention so a late
|
|
1655
|
+
// result cannot reopen coverage the source already moved past.
|
|
1656
|
+
let liveFrontierTime = null;
|
|
1657
|
+
// Newest media time actually covered by producer-declared data. Live frames
|
|
1658
|
+
// are written with a synthetic hold end, so the summary end time cannot be
|
|
1659
|
+
// used to place the retention window without pushing it into the future.
|
|
1660
|
+
let coverageFrontierTime = null;
|
|
1661
|
+
let writeQueue = Promise.resolve();
|
|
1403
1662
|
let version = 0;
|
|
1404
1663
|
let allRangeVersion = 0;
|
|
1405
1664
|
let journalFloorVersion = 0;
|
|
@@ -1412,13 +1671,25 @@ function createWritableDetectionFrameSource(options) {
|
|
|
1412
1671
|
datasetId: options.datasetId,
|
|
1413
1672
|
frames,
|
|
1414
1673
|
});
|
|
1415
|
-
const recordRangeWrite = (nextSummary, changedSourceRanges) => {
|
|
1674
|
+
const recordRangeWrite = (nextSummary, changedSourceRanges, invalidation, prunedRange) => {
|
|
1416
1675
|
summary = nextSummary;
|
|
1417
1676
|
version += 1;
|
|
1418
1677
|
for (const changedRange of changedSourceRanges) {
|
|
1419
1678
|
changedRanges.push({ ...changedRange, version });
|
|
1420
1679
|
recordAvailableRange(changedRange, availableRanges);
|
|
1421
1680
|
}
|
|
1681
|
+
for (const invalidatedRange of invalidation.ranges) {
|
|
1682
|
+
// Coverage a write took away is journaled but never made available: a
|
|
1683
|
+
// consumer parked there has to redraw, and nothing new can be loaded.
|
|
1684
|
+
changedRanges.push({ ...invalidatedRange, version });
|
|
1685
|
+
}
|
|
1686
|
+
if (prunedRange) {
|
|
1687
|
+
// Evicting old history is a change to that range only. Recording it
|
|
1688
|
+
// alongside the append keeps a buffered timeline patching two small
|
|
1689
|
+
// ranges instead of invalidating and rebuilding everything it retains.
|
|
1690
|
+
changedRanges.push({ ...prunedRange, version });
|
|
1691
|
+
dropAvailableRangesBefore(prunedRange.endTime, availableRanges);
|
|
1692
|
+
}
|
|
1422
1693
|
compactChangedRangeJournal();
|
|
1423
1694
|
resolveCoveredWaiters();
|
|
1424
1695
|
return nextSummary;
|
|
@@ -1443,38 +1714,113 @@ function createWritableDetectionFrameSource(options) {
|
|
|
1443
1714
|
datasetId: options.datasetId,
|
|
1444
1715
|
async appendFrames(frames) {
|
|
1445
1716
|
assertActive();
|
|
1446
|
-
|
|
1717
|
+
return enqueueWrite(async () => {
|
|
1718
|
+
const nextSummary = await options.store.appendFrames(writeOptions(frames));
|
|
1719
|
+
assertActive();
|
|
1720
|
+
const changedSourceRanges = getDetectionFrameRanges(frames);
|
|
1721
|
+
const invalidation = getInvalidatedCoverage(frames, changedSourceRanges);
|
|
1722
|
+
recordLatestFrame(frames);
|
|
1723
|
+
recordCoverageFrontierFromFrames(frames);
|
|
1724
|
+
return retainAndRecord(nextSummary, changedSourceRanges, invalidation);
|
|
1725
|
+
});
|
|
1726
|
+
},
|
|
1727
|
+
async appendLiveFrame(frame) {
|
|
1447
1728
|
assertActive();
|
|
1448
|
-
|
|
1449
|
-
|
|
1729
|
+
return enqueueWrite(async () => {
|
|
1730
|
+
if (isStaleLiveFrame(frame)) {
|
|
1731
|
+
// A result older than the live frontier no longer describes what is
|
|
1732
|
+
// on screen. Writing it would reopen coverage the source already
|
|
1733
|
+
// closed, so the newest causal frame simply wins.
|
|
1734
|
+
return summary ? { ...summary } : createEmptyWriteSummary();
|
|
1735
|
+
}
|
|
1736
|
+
const framesToWrite = createLiveFrameWrite(frame);
|
|
1737
|
+
const nextSummary = await options.store.appendFrames(writeOptions(framesToWrite));
|
|
1738
|
+
assertActive();
|
|
1739
|
+
const changedSourceRanges = getDetectionFrameRanges(framesToWrite);
|
|
1740
|
+
const invalidation = getInvalidatedCoverage(framesToWrite, changedSourceRanges);
|
|
1741
|
+
heldLiveFrame = framesToWrite.at(-1) ?? null;
|
|
1742
|
+
liveFrontierTime = Math.max(liveFrontierTime ?? frame.mediaTime, frame.mediaTime);
|
|
1743
|
+
recordLatestFrame(framesToWrite);
|
|
1744
|
+
// The hold end is a placeholder for "still current", not covered data.
|
|
1745
|
+
recordCoverageFrontier(getFrameEndTime(frame));
|
|
1746
|
+
return retainAndRecord(nextSummary, changedSourceRanges, invalidation);
|
|
1747
|
+
});
|
|
1748
|
+
},
|
|
1749
|
+
async finalizeCoverage(endTime) {
|
|
1450
1750
|
assertActive();
|
|
1451
|
-
if (
|
|
1452
|
-
|
|
1453
|
-
}
|
|
1454
|
-
if (changedSourceRanges.length === 0) {
|
|
1455
|
-
summary = nextSummary;
|
|
1456
|
-
return nextSummary;
|
|
1751
|
+
if (!Number.isFinite(endTime)) {
|
|
1752
|
+
throw new RangeError("finalizeCoverage requires a finite endTime.");
|
|
1457
1753
|
}
|
|
1458
|
-
return
|
|
1754
|
+
return enqueueWrite(async () => {
|
|
1755
|
+
const frameToFinalize = latestFrame;
|
|
1756
|
+
if (!frameToFinalize ||
|
|
1757
|
+
// A frame that starts at or after the end of media cannot describe a
|
|
1758
|
+
// non-empty terminal interval.
|
|
1759
|
+
frameToFinalize.mediaTime >= endTime ||
|
|
1760
|
+
getFrameEndTime(frameToFinalize) === endTime) {
|
|
1761
|
+
return summary ? { ...summary } : null;
|
|
1762
|
+
}
|
|
1763
|
+
const previousEndTime = getFrameEndTime(frameToFinalize);
|
|
1764
|
+
const finalizedFrame = { ...frameToFinalize, endTime };
|
|
1765
|
+
const nextSummary = await options.store.appendFrames(writeOptions([finalizedFrame]));
|
|
1766
|
+
assertActive();
|
|
1767
|
+
const changedSourceRanges = [
|
|
1768
|
+
{
|
|
1769
|
+
endTime: Math.max(endTime, previousEndTime),
|
|
1770
|
+
startTime: frameToFinalize.mediaTime,
|
|
1771
|
+
},
|
|
1772
|
+
];
|
|
1773
|
+
const invalidation = getInvalidatedCoverage([finalizedFrame], changedSourceRanges);
|
|
1774
|
+
latestFrame = finalizedFrame;
|
|
1775
|
+
if (heldLiveFrame &&
|
|
1776
|
+
haveSameDetectionFrameIdentity(heldLiveFrame, finalizedFrame)) {
|
|
1777
|
+
heldLiveFrame = finalizedFrame;
|
|
1778
|
+
}
|
|
1779
|
+
coverageFrontierTime = endTime;
|
|
1780
|
+
const finalSummary = await retainAndRecord(nextSummary, changedSourceRanges,
|
|
1781
|
+
// Finalizing usually closes at the end of media, where the tail an
|
|
1782
|
+
// open-ended frame gives up is unreachable. The caller may finalize
|
|
1783
|
+
// earlier, though, so report it rather than assume.
|
|
1784
|
+
invalidation);
|
|
1785
|
+
// Closing a held live frame shortens coverage. Reported availability
|
|
1786
|
+
// has to shrink with it, or a playback gate would still believe the
|
|
1787
|
+
// source covers time past the end of media.
|
|
1788
|
+
clipAvailableRangesAfter(endTime);
|
|
1789
|
+
return finalSummary;
|
|
1790
|
+
});
|
|
1459
1791
|
},
|
|
1460
1792
|
async replaceFrames(frames) {
|
|
1461
1793
|
assertActive();
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1794
|
+
return enqueueWrite(async () => {
|
|
1795
|
+
const nextSummary = await options.store.putFrames(writeOptions(frames));
|
|
1796
|
+
assertActive();
|
|
1797
|
+
latestFrame = null;
|
|
1798
|
+
heldLiveFrame = null;
|
|
1799
|
+
liveFrontierTime = null;
|
|
1800
|
+
coverageFrontierTime = null;
|
|
1801
|
+
recordLatestFrame(frames);
|
|
1802
|
+
recordCoverageFrontierFromFrames(frames);
|
|
1803
|
+
const retention = await applyRetention(nextSummary);
|
|
1804
|
+
assertActive();
|
|
1805
|
+
return recordAllRangesWrite(retention.kind === "unchanged" ? nextSummary : retention.summary);
|
|
1806
|
+
});
|
|
1467
1807
|
},
|
|
1468
1808
|
async clear() {
|
|
1469
1809
|
assertActive();
|
|
1470
|
-
await
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1810
|
+
await enqueueWrite(async () => {
|
|
1811
|
+
await options.store.clearDataset(options.datasetId);
|
|
1812
|
+
assertActive();
|
|
1813
|
+
summary = null;
|
|
1814
|
+
latestFrame = null;
|
|
1815
|
+
heldLiveFrame = null;
|
|
1816
|
+
liveFrontierTime = null;
|
|
1817
|
+
coverageFrontierTime = null;
|
|
1818
|
+
version += 1;
|
|
1819
|
+
allRangeVersion = version;
|
|
1820
|
+
journalFloorVersion = version;
|
|
1821
|
+
changedRanges.length = 0;
|
|
1822
|
+
availableRanges.length = 0;
|
|
1823
|
+
});
|
|
1478
1824
|
},
|
|
1479
1825
|
async loadFrames(startTime, endTime) {
|
|
1480
1826
|
assertActive();
|
|
@@ -1507,14 +1853,10 @@ function createWritableDetectionFrameSource(options) {
|
|
|
1507
1853
|
if (!range) {
|
|
1508
1854
|
return version;
|
|
1509
1855
|
}
|
|
1510
|
-
return
|
|
1511
|
-
? Math.max(rangeVersion, changedRange.version)
|
|
1512
|
-
: rangeVersion, Math.max(allRangeVersion, journalFloorVersion));
|
|
1856
|
+
return getRangeVersion(range);
|
|
1513
1857
|
},
|
|
1514
1858
|
getChangesSince(previousVersion, ranges) {
|
|
1515
|
-
const relevantVersion = ranges.reduce((rangeVersion, range) => Math.max(rangeVersion,
|
|
1516
|
-
? Math.max(changedVersion, changedRange.version)
|
|
1517
|
-
: changedVersion, Math.max(allRangeVersion, journalFloorVersion))), Math.max(allRangeVersion, journalFloorVersion));
|
|
1859
|
+
const relevantVersion = ranges.reduce((rangeVersion, range) => Math.max(rangeVersion, getRangeVersion(range)), getReloadFloorVersion());
|
|
1518
1860
|
if (relevantVersion <= previousVersion) {
|
|
1519
1861
|
return {
|
|
1520
1862
|
ranges: [],
|
|
@@ -1522,18 +1864,34 @@ function createWritableDetectionFrameSource(options) {
|
|
|
1522
1864
|
version: relevantVersion,
|
|
1523
1865
|
};
|
|
1524
1866
|
}
|
|
1525
|
-
if (previousVersion <
|
|
1526
|
-
previousVersion < journalFloorVersion) {
|
|
1867
|
+
if (previousVersion < getReloadFloorVersion()) {
|
|
1527
1868
|
return {
|
|
1528
1869
|
ranges: [],
|
|
1529
1870
|
requiresReload: true,
|
|
1530
1871
|
version: relevantVersion,
|
|
1531
1872
|
};
|
|
1532
1873
|
}
|
|
1874
|
+
// Clipped to the requested ranges: a consumer gets back exactly the part
|
|
1875
|
+
// of the change it asked about, and an unbounded invalidation stays a
|
|
1876
|
+
// bounded, loadable answer.
|
|
1533
1877
|
const changedSourceRanges = changedRanges
|
|
1534
|
-
.filter((changedRange) => changedRange.version > previousVersion
|
|
1535
|
-
|
|
1536
|
-
.
|
|
1878
|
+
.filter((changedRange) => changedRange.version > previousVersion)
|
|
1879
|
+
.flatMap((changedRange) => ranges
|
|
1880
|
+
.filter((range) => rangesOverlap(range, changedRange))
|
|
1881
|
+
.map((range) => {
|
|
1882
|
+
const startTime = Math.max(range.startTime, changedRange.startTime);
|
|
1883
|
+
const endTime = Math.min(range.endTime, changedRange.endTime);
|
|
1884
|
+
return {
|
|
1885
|
+
// An unbounded ask against an unbounded invalidation would
|
|
1886
|
+
// otherwise hand back an infinite range, and a consumer would
|
|
1887
|
+
// take it straight to `loadFrames`. Nothing past the coverage
|
|
1888
|
+
// the source knows about can be loaded.
|
|
1889
|
+
endTime: Number.isFinite(endTime)
|
|
1890
|
+
? endTime
|
|
1891
|
+
: Math.max(startTime, getKnownEndTime()),
|
|
1892
|
+
startTime,
|
|
1893
|
+
};
|
|
1894
|
+
}));
|
|
1537
1895
|
return {
|
|
1538
1896
|
ranges: mergeRanges(changedSourceRanges),
|
|
1539
1897
|
requiresReload: false,
|
|
@@ -1557,12 +1915,112 @@ function createWritableDetectionFrameSource(options) {
|
|
|
1557
1915
|
throw createDestroyedError();
|
|
1558
1916
|
}
|
|
1559
1917
|
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Oldest version the journal can still describe incrementally. Anything
|
|
1920
|
+
* older has to reload, because replacement, clearing, or journal compaction
|
|
1921
|
+
* dropped the intervening changes.
|
|
1922
|
+
*/
|
|
1923
|
+
function getReloadFloorVersion() {
|
|
1924
|
+
return Math.max(allRangeVersion, journalFloorVersion);
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Latest media time the source can actually serve frames for.
|
|
1928
|
+
*/
|
|
1929
|
+
function getKnownEndTime() {
|
|
1930
|
+
return Math.max(coverageFrontierTime ?? 0, summary?.endTime ?? 0);
|
|
1931
|
+
}
|
|
1932
|
+
function getRangeVersion(range) {
|
|
1933
|
+
return changedRanges.reduce((rangeVersion, changedRange) => rangesOverlap(range, changedRange)
|
|
1934
|
+
? Math.max(rangeVersion, changedRange.version)
|
|
1935
|
+
: rangeVersion, getReloadFloorVersion());
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Runs mutating writes one at a time.
|
|
1939
|
+
*
|
|
1940
|
+
* Every write reads the held live frame, the latest frame, and the coverage
|
|
1941
|
+
* frontier before it awaits storage. Without serialization two concurrent
|
|
1942
|
+
* live appends would both observe the pre-write state and both stay open,
|
|
1943
|
+
* which is exactly the stale overlay live semantics exist to prevent.
|
|
1944
|
+
*/
|
|
1945
|
+
function enqueueWrite(run) {
|
|
1946
|
+
const result = writeQueue.then(() => {
|
|
1947
|
+
assertActive();
|
|
1948
|
+
return run();
|
|
1949
|
+
});
|
|
1950
|
+
writeQueue = result.then(() => undefined, () => undefined);
|
|
1951
|
+
return result;
|
|
1952
|
+
}
|
|
1953
|
+
function isStaleLiveFrame(frame) {
|
|
1954
|
+
if (liveFrontierTime === null || frame.mediaTime > liveFrontierTime) {
|
|
1955
|
+
return false;
|
|
1956
|
+
}
|
|
1957
|
+
// A frame at the frontier that shares the held frame's identity is a
|
|
1958
|
+
// revision of the current result, not a late duplicate of an older one.
|
|
1959
|
+
return !(heldLiveFrame && haveSameDetectionFrameIdentity(heldLiveFrame, frame));
|
|
1960
|
+
}
|
|
1961
|
+
function recordCoverageFrontier(endTime) {
|
|
1962
|
+
coverageFrontierTime =
|
|
1963
|
+
coverageFrontierTime === null
|
|
1964
|
+
? endTime
|
|
1965
|
+
: Math.max(coverageFrontierTime, endTime);
|
|
1966
|
+
}
|
|
1967
|
+
function recordCoverageFrontierFromFrames(frames) {
|
|
1968
|
+
for (const frame of frames) {
|
|
1969
|
+
recordCoverageFrontier(getFrameEndTime(frame));
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
function clipAvailableRangesAfter(endTime) {
|
|
1973
|
+
let writeIndex = 0;
|
|
1974
|
+
for (const range of availableRanges) {
|
|
1975
|
+
if (range.startTime >= endTime) {
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
availableRanges[writeIndex] = {
|
|
1979
|
+
endTime: Math.min(range.endTime, endTime),
|
|
1980
|
+
startTime: range.startTime,
|
|
1981
|
+
};
|
|
1982
|
+
writeIndex += 1;
|
|
1983
|
+
}
|
|
1984
|
+
availableRanges.length = writeIndex;
|
|
1985
|
+
}
|
|
1986
|
+
function createEmptyWriteSummary() {
|
|
1987
|
+
return {
|
|
1988
|
+
chunkCount: 0,
|
|
1989
|
+
chunkDurationSeconds: options.chunkDurationSeconds ?? 1,
|
|
1990
|
+
datasetId: options.datasetId,
|
|
1991
|
+
detectionCount: 0,
|
|
1992
|
+
endTime: null,
|
|
1993
|
+
frameCount: 0,
|
|
1994
|
+
startTime: null,
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Applies retention and records the result as an incremental change when the
|
|
1999
|
+
* store can prune in place, or as a full rewrite when it cannot.
|
|
2000
|
+
*/
|
|
2001
|
+
async function retainAndRecord(nextSummary, changedSourceRanges, invalidation = NO_COVERAGE_INVALIDATION) {
|
|
2002
|
+
const retention = await applyRetention(nextSummary);
|
|
2003
|
+
assertActive();
|
|
2004
|
+
if (retention.kind === "rewritten") {
|
|
2005
|
+
return recordAllRangesWrite(retention.summary);
|
|
2006
|
+
}
|
|
2007
|
+
if (retention.kind === "unchanged") {
|
|
2008
|
+
if (changedSourceRanges.length === 0 &&
|
|
2009
|
+
invalidation.ranges.length === 0) {
|
|
2010
|
+
summary = nextSummary;
|
|
2011
|
+
return nextSummary;
|
|
2012
|
+
}
|
|
2013
|
+
return recordRangeWrite(nextSummary, changedSourceRanges, invalidation);
|
|
2014
|
+
}
|
|
2015
|
+
forgetFramesBefore(retention.prunedRange.endTime);
|
|
2016
|
+
return recordRangeWrite(retention.summary, changedSourceRanges, invalidation, retention.prunedRange);
|
|
2017
|
+
}
|
|
1560
2018
|
async function applyRetention(nextSummary) {
|
|
1561
2019
|
const retention = options.retention;
|
|
1562
2020
|
if (retention === undefined ||
|
|
1563
2021
|
!shouldApplyWindowRetention(retention) ||
|
|
1564
2022
|
nextSummary.endTime === null) {
|
|
1565
|
-
return
|
|
2023
|
+
return { kind: "unchanged" };
|
|
1566
2024
|
}
|
|
1567
2025
|
const retentionWindowSeconds = retention.windowSeconds;
|
|
1568
2026
|
if (retentionWindowSeconds === undefined ||
|
|
@@ -1570,17 +2028,119 @@ function createWritableDetectionFrameSource(options) {
|
|
|
1570
2028
|
retentionWindowSeconds <= 0) {
|
|
1571
2029
|
throw new Error("retention.windowSeconds must be greater than 0.");
|
|
1572
2030
|
}
|
|
1573
|
-
|
|
2031
|
+
// Place the window against real producer coverage. A live frame held open
|
|
2032
|
+
// for `holdSeconds` reports a summary end far in the future, and anchoring
|
|
2033
|
+
// eviction there would evict everything the producer just wrote.
|
|
2034
|
+
const retentionStartTime = Math.max(0, Math.min(coverageFrontierTime ?? nextSummary.endTime, nextSummary.endTime) - retentionWindowSeconds);
|
|
1574
2035
|
if (nextSummary.startTime !== null &&
|
|
1575
2036
|
nextSummary.startTime + RANGE_EPSILON_SECONDS >= retentionStartTime) {
|
|
1576
|
-
return
|
|
2037
|
+
return { kind: "unchanged" };
|
|
1577
2038
|
}
|
|
2039
|
+
if (options.store.pruneFrames) {
|
|
2040
|
+
return {
|
|
2041
|
+
kind: "pruned",
|
|
2042
|
+
prunedRange: {
|
|
2043
|
+
endTime: retentionStartTime,
|
|
2044
|
+
startTime: nextSummary.startTime ?? 0,
|
|
2045
|
+
},
|
|
2046
|
+
summary: await options.store.pruneFrames({
|
|
2047
|
+
datasetId: options.datasetId,
|
|
2048
|
+
startTime: retentionStartTime,
|
|
2049
|
+
}),
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
// Stores without in-place pruning still get a correct retention window,
|
|
2053
|
+
// at the cost of reloading and rewriting everything they keep.
|
|
1578
2054
|
const retainedFrames = await options.store.loadFrames({
|
|
1579
2055
|
datasetId: options.datasetId,
|
|
1580
2056
|
endTime: nextSummary.endTime,
|
|
1581
2057
|
startTime: retentionStartTime,
|
|
1582
2058
|
});
|
|
1583
|
-
return
|
|
2059
|
+
return {
|
|
2060
|
+
kind: "rewritten",
|
|
2061
|
+
summary: await options.store.putFrames(writeOptions(retainedFrames)),
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Builds the one or two frames a live append writes: the newest frame held
|
|
2066
|
+
* open, plus the previously held frame closed where the newest begins.
|
|
2067
|
+
*/
|
|
2068
|
+
function createLiveFrameWrite(frame) {
|
|
2069
|
+
const openFrame = {
|
|
2070
|
+
...frame,
|
|
2071
|
+
endTime: Math.max(frame.endTime ?? frame.mediaTime, frame.mediaTime + liveHoldSeconds),
|
|
2072
|
+
};
|
|
2073
|
+
const heldFrame = heldLiveFrame;
|
|
2074
|
+
// Stale results are rejected before this point, so a held frame that is
|
|
2075
|
+
// neither the same frame nor already closed always ends strictly later.
|
|
2076
|
+
if (!heldFrame ||
|
|
2077
|
+
haveSameDetectionFrameIdentity(heldFrame, openFrame) ||
|
|
2078
|
+
getFrameEndTime(heldFrame) <= frame.mediaTime) {
|
|
2079
|
+
return [openFrame];
|
|
2080
|
+
}
|
|
2081
|
+
return [{ ...heldFrame, endTime: frame.mediaTime }, openFrame];
|
|
2082
|
+
}
|
|
2083
|
+
function recordLatestFrame(frames) {
|
|
2084
|
+
for (const frame of frames) {
|
|
2085
|
+
if (!latestFrame ||
|
|
2086
|
+
compareDetectionFrames(frame, latestFrame) > 0 ||
|
|
2087
|
+
// A write that shares the tracked frame's identity replaces its stored
|
|
2088
|
+
// record. Keeping the superseded contents here would let a later
|
|
2089
|
+
// `finalizeCoverage` write them back over the revision.
|
|
2090
|
+
haveSameDetectionFrameIdentity(frame, latestFrame)) {
|
|
2091
|
+
latestFrame = frame;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Coverage a write takes away from a frame the source already wrote.
|
|
2097
|
+
*
|
|
2098
|
+
* The journal describes a write by the interval it covers, which is enough
|
|
2099
|
+
* while writes only add. Rewriting a frame with a shorter interval, or at a
|
|
2100
|
+
* different time, instead stops covering times the new interval never
|
|
2101
|
+
* mentions, and a consumer parked in one of them would keep showing a frame
|
|
2102
|
+
* the source no longer selects.
|
|
2103
|
+
*
|
|
2104
|
+
* A frame without an `endTime` was selected with no upper bound, so what it
|
|
2105
|
+
* gives up is genuinely unbounded and is journaled that way. The journal
|
|
2106
|
+
* stays range-scoped either way: a consumer asking about times the write
|
|
2107
|
+
* never touched still hears that nothing changed.
|
|
2108
|
+
*/
|
|
2109
|
+
function getInvalidatedCoverage(frames, coveredRanges) {
|
|
2110
|
+
const invalidatedRanges = [];
|
|
2111
|
+
for (const frame of frames) {
|
|
2112
|
+
for (const trackedFrame of [latestFrame, heldLiveFrame]) {
|
|
2113
|
+
if (!trackedFrame ||
|
|
2114
|
+
!haveSameDetectionFrameIdentity(trackedFrame, frame)) {
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
const previousRange = {
|
|
2118
|
+
endTime: trackedFrame.endTime ?? Number.POSITIVE_INFINITY,
|
|
2119
|
+
startTime: trackedFrame.mediaTime,
|
|
2120
|
+
};
|
|
2121
|
+
const nextEndTime = frame.endTime ?? Number.POSITIVE_INFINITY;
|
|
2122
|
+
if (trackedFrame.mediaTime >= frame.mediaTime &&
|
|
2123
|
+
previousRange.endTime <= nextEndTime) {
|
|
2124
|
+
continue;
|
|
2125
|
+
}
|
|
2126
|
+
// A live append closes the held frame and reopens it further out in the
|
|
2127
|
+
// same write, so the write's own range already describes the tail. Only
|
|
2128
|
+
// coverage nothing else reported is worth a second journal entry.
|
|
2129
|
+
if (isRangeCovered(previousRange, coveredRanges)) {
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
invalidatedRanges.push(previousRange);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
return { ranges: mergeRanges(invalidatedRanges) };
|
|
2136
|
+
}
|
|
2137
|
+
function forgetFramesBefore(startTime) {
|
|
2138
|
+
if (latestFrame && getFrameEndTime(latestFrame) < startTime) {
|
|
2139
|
+
latestFrame = null;
|
|
2140
|
+
}
|
|
2141
|
+
if (heldLiveFrame && getFrameEndTime(heldLiveFrame) < startTime) {
|
|
2142
|
+
heldLiveFrame = null;
|
|
2143
|
+
}
|
|
1584
2144
|
}
|
|
1585
2145
|
function resolveCoveredWaiters() {
|
|
1586
2146
|
for (let index = waiters.length - 1; index >= 0; index -= 1) {
|
|
@@ -1604,6 +2164,43 @@ function createWritableDetectionFrameSource(options) {
|
|
|
1604
2164
|
function createDestroyedError() {
|
|
1605
2165
|
return new Error("Detection frame source has been destroyed.");
|
|
1606
2166
|
}
|
|
2167
|
+
function resolveLiveHoldSeconds(holdSeconds) {
|
|
2168
|
+
if (holdSeconds === undefined) {
|
|
2169
|
+
return DEFAULT_LIVE_HOLD_SECONDS;
|
|
2170
|
+
}
|
|
2171
|
+
if (!Number.isFinite(holdSeconds) || holdSeconds <= 0) {
|
|
2172
|
+
throw new RangeError("live.holdSeconds must be greater than 0.");
|
|
2173
|
+
}
|
|
2174
|
+
return holdSeconds;
|
|
2175
|
+
}
|
|
2176
|
+
function getFrameEndTime(frame) {
|
|
2177
|
+
return frame.endTime ?? frame.mediaTime;
|
|
2178
|
+
}
|
|
2179
|
+
function haveSameDetectionFrameIdentity(left, right) {
|
|
2180
|
+
return left.frameIndex === undefined || right.frameIndex === undefined
|
|
2181
|
+
? left.frameIndex === right.frameIndex && left.mediaTime === right.mediaTime
|
|
2182
|
+
: left.frameIndex === right.frameIndex;
|
|
2183
|
+
}
|
|
2184
|
+
function compareDetectionFrames(left, right) {
|
|
2185
|
+
if (left.mediaTime !== right.mediaTime) {
|
|
2186
|
+
return left.mediaTime - right.mediaTime;
|
|
2187
|
+
}
|
|
2188
|
+
return (left.frameIndex ?? 0) - (right.frameIndex ?? 0);
|
|
2189
|
+
}
|
|
2190
|
+
function dropAvailableRangesBefore(startTime, availableRanges) {
|
|
2191
|
+
let writeIndex = 0;
|
|
2192
|
+
for (const range of availableRanges) {
|
|
2193
|
+
if (range.endTime <= startTime) {
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
availableRanges[writeIndex] = {
|
|
2197
|
+
endTime: range.endTime,
|
|
2198
|
+
startTime: Math.max(range.startTime, startTime),
|
|
2199
|
+
};
|
|
2200
|
+
writeIndex += 1;
|
|
2201
|
+
}
|
|
2202
|
+
availableRanges.length = writeIndex;
|
|
2203
|
+
}
|
|
1607
2204
|
function shouldApplyWindowRetention(retention) {
|
|
1608
2205
|
return (retention?.mode === DetectionFrameRetentionMode.PersistWindow ||
|
|
1609
2206
|
retention?.mode === DetectionFrameRetentionMode.MemoryOnly);
|
|
@@ -1657,42 +2254,1407 @@ function isRangeCovered(range, availableRanges) {
|
|
|
1657
2254
|
availableRange.endTime + RANGE_EPSILON_SECONDS >= range.endTime);
|
|
1658
2255
|
}
|
|
1659
2256
|
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2257
|
+
/**
|
|
2258
|
+
* Creates one stateful SORT tracker for a single ordered media sequence.
|
|
2259
|
+
*
|
|
2260
|
+
* Defaults, lifecycle semantics, and observation-only output mirror
|
|
2261
|
+
* roboflow/trackers SORT. Motion predictions remain internal to association.
|
|
2262
|
+
*/
|
|
2263
|
+
function createSortTracker$1(options = {}) {
|
|
2264
|
+
const lostTrackBuffer = normalizeNonNegativeInteger$1(options.lostTrackBuffer ?? 30, "lostTrackBuffer");
|
|
2265
|
+
const frameRate = options.frameRate ?? 30;
|
|
2266
|
+
const trackActivationThreshold = options.trackActivationThreshold ?? 0.25;
|
|
2267
|
+
const minimumConsecutiveFrames = normalizePositiveInteger$1(options.minimumConsecutiveFrames ?? 3, "minimumConsecutiveFrames");
|
|
2268
|
+
const minimumIouThreshold = options.minimumIouThreshold ?? 0.3;
|
|
2269
|
+
if (!Number.isFinite(frameRate) || frameRate <= 0) {
|
|
2270
|
+
throw new Error("frameRate must be a finite positive value.");
|
|
2271
|
+
}
|
|
2272
|
+
normalizeUnitInterval$1(trackActivationThreshold, "trackActivationThreshold");
|
|
2273
|
+
normalizeUnitInterval$1(minimumIouThreshold, "minimumIouThreshold");
|
|
2274
|
+
const scaledLostTrackBuffer = (frameRate / 30) * lostTrackBuffer;
|
|
2275
|
+
if (!Number.isFinite(scaledLostTrackBuffer)) {
|
|
2276
|
+
throw new Error("Scaled lostTrackBuffer overflows: frameRate / 30 * lostTrackBuffer must be finite.");
|
|
2277
|
+
}
|
|
2278
|
+
const maximumFramesWithoutUpdate = lostTrackBuffer === 0 ? 0 : Math.max(1, Math.ceil(scaledLostTrackBuffer));
|
|
2279
|
+
let tracks = [];
|
|
2280
|
+
let nextTrackerId = 0;
|
|
2281
|
+
let previousFrameIndex;
|
|
1678
2282
|
return {
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2283
|
+
reset() {
|
|
2284
|
+
tracks = [];
|
|
2285
|
+
nextTrackerId = 0;
|
|
2286
|
+
previousFrameIndex = undefined;
|
|
2287
|
+
},
|
|
2288
|
+
update(detections, frameIndex) {
|
|
2289
|
+
const frameStep = resolveFrameStep(frameIndex, previousFrameIndex);
|
|
2290
|
+
previousFrameIndex = frameIndex ?? previousFrameIndex;
|
|
2291
|
+
const predicted = tracks.map((track) => track.predict(frameStep, frameRate));
|
|
2292
|
+
const { matches, unmatchedDetections } = associateDetectionsToTracks(detections, predicted, minimumIouThreshold);
|
|
2293
|
+
const trackerIds = new Map();
|
|
2294
|
+
for (const match of matches) {
|
|
2295
|
+
const track = tracks[match.trackIndex];
|
|
2296
|
+
const detection = detections[match.detectionIndex];
|
|
2297
|
+
track.update(detection);
|
|
2298
|
+
if (track.trackerId === undefined &&
|
|
2299
|
+
track.successfulUpdates >= minimumConsecutiveFrames) {
|
|
2300
|
+
track.trackerId = nextTrackerId;
|
|
2301
|
+
nextTrackerId += 1;
|
|
2302
|
+
}
|
|
2303
|
+
if (track.trackerId !== undefined) {
|
|
2304
|
+
trackerIds.set(detection.detectionIndex, track.trackerId);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
for (const detectionIndex of unmatchedDetections) {
|
|
2308
|
+
const detection = detections[detectionIndex];
|
|
2309
|
+
if ((detection.confidence ?? 1) >= trackActivationThreshold) {
|
|
2310
|
+
tracks.push(new KalmanBoxTrack(detection));
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
tracks = tracks.filter((track) => track.timeSinceUpdate <= maximumFramesWithoutUpdate &&
|
|
2314
|
+
(track.successfulUpdates >= minimumConsecutiveFrames ||
|
|
2315
|
+
track.timeSinceUpdate === 0));
|
|
2316
|
+
return {
|
|
2317
|
+
activeTrackCount: tracks.length,
|
|
2318
|
+
assignments: detections.flatMap((detection) => {
|
|
2319
|
+
const trackerId = trackerIds.get(detection.detectionIndex);
|
|
2320
|
+
return trackerId === undefined
|
|
2321
|
+
? []
|
|
2322
|
+
: [{ detectionIndex: detection.detectionIndex, trackerId }];
|
|
2323
|
+
}),
|
|
2324
|
+
confirmedTrackCount: tracks.filter((track) => track.trackerId !== undefined).length,
|
|
2325
|
+
};
|
|
2326
|
+
},
|
|
1683
2327
|
};
|
|
1684
2328
|
}
|
|
1685
|
-
|
|
2329
|
+
class KalmanBoxTrack {
|
|
2330
|
+
consecutiveUpdates;
|
|
2331
|
+
trackerId;
|
|
2332
|
+
successfulUpdates = 1;
|
|
2333
|
+
timeSinceUpdate = 0;
|
|
2334
|
+
state;
|
|
2335
|
+
covariance = identity(8);
|
|
2336
|
+
constructor(detection, consecutiveUpdates = false) {
|
|
2337
|
+
this.consecutiveUpdates = consecutiveUpdates;
|
|
2338
|
+
this.state = [...rectToXyxy(detection.rect), 0, 0, 0, 0].map((value) => [
|
|
2339
|
+
value,
|
|
2340
|
+
]);
|
|
2341
|
+
}
|
|
2342
|
+
predict(frameStep, frameRate) {
|
|
2343
|
+
if (this.consecutiveUpdates && this.timeSinceUpdate > 0) {
|
|
2344
|
+
this.successfulUpdates = 0;
|
|
2345
|
+
}
|
|
2346
|
+
const transition = identity(8);
|
|
2347
|
+
for (let index = 0; index < 4; index += 1) {
|
|
2348
|
+
transition[index][index + 4] = frameStep;
|
|
2349
|
+
}
|
|
2350
|
+
this.state = multiply(transition, this.state);
|
|
2351
|
+
this.covariance = add(multiply(multiply(transition, this.covariance), transpose(transition)), createProcessNoise(frameStep, frameRate));
|
|
2352
|
+
// Python SORT counts update calls for the fixed-rate lost-track budget.
|
|
2353
|
+
this.timeSinceUpdate += 1;
|
|
2354
|
+
return stateToRect(this.state);
|
|
2355
|
+
}
|
|
2356
|
+
update(detection) {
|
|
2357
|
+
const measurement = rectToXyxy(detection.rect).map((value) => [value]);
|
|
2358
|
+
const observation = [
|
|
2359
|
+
[1, 0, 0, 0, 0, 0, 0, 0],
|
|
2360
|
+
[0, 1, 0, 0, 0, 0, 0, 0],
|
|
2361
|
+
[0, 0, 1, 0, 0, 0, 0, 0],
|
|
2362
|
+
[0, 0, 0, 1, 0, 0, 0, 0],
|
|
2363
|
+
];
|
|
2364
|
+
const measurementNoise = scale(identity(4), 0.1);
|
|
2365
|
+
const innovation = subtract(measurement, multiply(observation, this.state));
|
|
2366
|
+
const covarianceObservationTranspose = multiply(this.covariance, transpose(observation));
|
|
2367
|
+
const innovationCovariance = add(multiply(observation, covarianceObservationTranspose), measurementNoise);
|
|
2368
|
+
const gain = multiply(covarianceObservationTranspose, inverse(innovationCovariance));
|
|
2369
|
+
this.state = add(this.state, multiply(gain, innovation));
|
|
2370
|
+
const identityMinusGainObservation = subtract(identity(8), multiply(gain, observation));
|
|
2371
|
+
// Joseph form matches the Python implementation and is more stable.
|
|
2372
|
+
this.covariance = add(multiply(multiply(identityMinusGainObservation, this.covariance), transpose(identityMinusGainObservation)), multiply(multiply(gain, measurementNoise), transpose(gain)));
|
|
2373
|
+
this.timeSinceUpdate = 0;
|
|
2374
|
+
this.successfulUpdates += 1;
|
|
2375
|
+
}
|
|
2376
|
+
getStateRect() {
|
|
2377
|
+
return stateToRect(this.state);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
function associateDetectionsToTracks(detections, predicted, minimumIouThreshold) {
|
|
2381
|
+
if (predicted.length === 0 || detections.length === 0) {
|
|
2382
|
+
return {
|
|
2383
|
+
matches: [],
|
|
2384
|
+
unmatchedDetections: detections.map((_, index) => index),
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
// Python SORT associates all detections class-agnostically using standard IoU.
|
|
2388
|
+
const scores = predicted.map((rect) => detections.map((detection) => intersectionOverUnion(rect, detection.rect)));
|
|
2389
|
+
const candidateMatches = maximizeAssignment(scores);
|
|
2390
|
+
const matches = [];
|
|
2391
|
+
const matchedDetections = new Set();
|
|
2392
|
+
for (const [trackIndex, detectionIndex] of candidateMatches) {
|
|
2393
|
+
if (scores[trackIndex][detectionIndex] < minimumIouThreshold)
|
|
2394
|
+
continue;
|
|
2395
|
+
matches.push({ detectionIndex, trackIndex });
|
|
2396
|
+
matchedDetections.add(detectionIndex);
|
|
2397
|
+
}
|
|
2398
|
+
matches.sort((left, right) => left.trackIndex - right.trackIndex);
|
|
1686
2399
|
return {
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
x: rect.x + rect.width / 2,
|
|
1690
|
-
y: rect.y + rect.height / 2,
|
|
2400
|
+
matches,
|
|
2401
|
+
unmatchedDetections: detections.flatMap((_, index) => matchedDetections.has(index) ? [] : [index]),
|
|
1691
2402
|
};
|
|
1692
2403
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2404
|
+
/** Hungarian assignment for a rectangular score matrix. */
|
|
2405
|
+
function maximizeAssignment(scores) {
|
|
2406
|
+
if (scores.length === 0 || scores[0]?.length === 0) {
|
|
2407
|
+
return [];
|
|
2408
|
+
}
|
|
2409
|
+
const rowCount = scores.length;
|
|
2410
|
+
const columnCount = scores[0].length;
|
|
2411
|
+
const transposed = rowCount > columnCount;
|
|
2412
|
+
const costs = transposed
|
|
2413
|
+
? Array.from({ length: columnCount }, (_, row) => Array.from({ length: rowCount }, (_, column) => 1 - scores[column][row]))
|
|
2414
|
+
: scores.map((row) => row.map((score) => 1 - score));
|
|
2415
|
+
const rows = costs.length;
|
|
2416
|
+
const columns = costs[0].length;
|
|
2417
|
+
const u = new Array(rows + 1).fill(0);
|
|
2418
|
+
const v = new Array(columns + 1).fill(0);
|
|
2419
|
+
const p = new Array(columns + 1).fill(0);
|
|
2420
|
+
const way = new Array(columns + 1).fill(0);
|
|
2421
|
+
for (let row = 1; row <= rows; row += 1) {
|
|
2422
|
+
p[0] = row;
|
|
2423
|
+
let column0 = 0;
|
|
2424
|
+
const minValue = new Array(columns + 1).fill(Number.POSITIVE_INFINITY);
|
|
2425
|
+
const used = new Array(columns + 1).fill(false);
|
|
2426
|
+
do {
|
|
2427
|
+
used[column0] = true;
|
|
2428
|
+
const row0 = p[column0];
|
|
2429
|
+
let delta = Number.POSITIVE_INFINITY;
|
|
2430
|
+
let column1 = 0;
|
|
2431
|
+
for (let column = 1; column <= columns; column += 1) {
|
|
2432
|
+
if (used[column])
|
|
2433
|
+
continue;
|
|
2434
|
+
const current = costs[row0 - 1][column - 1] - u[row0] - v[column];
|
|
2435
|
+
if (current < minValue[column]) {
|
|
2436
|
+
minValue[column] = current;
|
|
2437
|
+
way[column] = column0;
|
|
2438
|
+
}
|
|
2439
|
+
if (minValue[column] < delta) {
|
|
2440
|
+
delta = minValue[column];
|
|
2441
|
+
column1 = column;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
for (let column = 0; column <= columns; column += 1) {
|
|
2445
|
+
if (used[column]) {
|
|
2446
|
+
u[p[column]] += delta;
|
|
2447
|
+
v[column] -= delta;
|
|
2448
|
+
}
|
|
2449
|
+
else {
|
|
2450
|
+
minValue[column] -= delta;
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
column0 = column1;
|
|
2454
|
+
} while (p[column0] !== 0);
|
|
2455
|
+
do {
|
|
2456
|
+
const column1 = way[column0];
|
|
2457
|
+
p[column0] = p[column1];
|
|
2458
|
+
column0 = column1;
|
|
2459
|
+
} while (column0 !== 0);
|
|
2460
|
+
}
|
|
2461
|
+
const assignments = [];
|
|
2462
|
+
for (let column = 1; column <= columns; column += 1) {
|
|
2463
|
+
if (p[column] === 0)
|
|
2464
|
+
continue;
|
|
2465
|
+
const row = p[column] - 1;
|
|
2466
|
+
assignments.push(transposed ? [column - 1, row] : [row, column - 1]);
|
|
2467
|
+
}
|
|
2468
|
+
return assignments;
|
|
2469
|
+
}
|
|
2470
|
+
function rectToXyxy(rect) {
|
|
2471
|
+
return [
|
|
2472
|
+
rect.x - rect.width / 2,
|
|
2473
|
+
rect.y - rect.height / 2,
|
|
2474
|
+
rect.x + rect.width / 2,
|
|
2475
|
+
rect.y + rect.height / 2,
|
|
2476
|
+
];
|
|
2477
|
+
}
|
|
2478
|
+
function stateToRect(state) {
|
|
2479
|
+
// The Python XYXY estimator intentionally leaves corner velocities
|
|
2480
|
+
// unconstrained. Normalize only at the browser Rect boundary so a crossing
|
|
2481
|
+
// prediction cannot violate the positive-width/height storage contract.
|
|
2482
|
+
const x1 = Math.min(state[0][0], state[2][0]);
|
|
2483
|
+
const y1 = Math.min(state[1][0], state[3][0]);
|
|
2484
|
+
const x2 = Math.max(state[0][0], state[2][0]);
|
|
2485
|
+
const y2 = Math.max(state[1][0], state[3][0]);
|
|
2486
|
+
return {
|
|
2487
|
+
height: Math.max(Number.EPSILON, y2 - y1),
|
|
2488
|
+
width: Math.max(Number.EPSILON, x2 - x1),
|
|
2489
|
+
x: (x1 + x2) / 2,
|
|
2490
|
+
y: (y1 + y2) / 2,
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
function createProcessNoise(frameStep, frameRate) {
|
|
2494
|
+
if (Math.abs(frameStep - 1) <= 0.004 * frameRate) {
|
|
2495
|
+
return scale(identity(8), 0.01);
|
|
2496
|
+
}
|
|
2497
|
+
const result = Array.from({ length: 8 }, () => new Array(8).fill(0));
|
|
2498
|
+
const dt2 = frameStep * frameStep;
|
|
2499
|
+
const dt3 = dt2 * frameStep;
|
|
2500
|
+
const dt4 = dt2 * dt2;
|
|
2501
|
+
for (let index = 0; index < 4; index += 1) {
|
|
2502
|
+
const velocityIndex = index + 4;
|
|
2503
|
+
result[index][index] = (0.01 * dt4) / 4;
|
|
2504
|
+
result[index][velocityIndex] = (0.01 * dt3) / 2;
|
|
2505
|
+
result[velocityIndex][index] = (0.01 * dt3) / 2;
|
|
2506
|
+
result[velocityIndex][velocityIndex] = 0.01 * dt2;
|
|
2507
|
+
}
|
|
2508
|
+
return result;
|
|
2509
|
+
}
|
|
2510
|
+
function intersectionOverUnion(left, right) {
|
|
2511
|
+
const leftX = left.x - left.width / 2;
|
|
2512
|
+
const leftY = left.y - left.height / 2;
|
|
2513
|
+
const rightX = right.x - right.width / 2;
|
|
2514
|
+
const rightY = right.y - right.height / 2;
|
|
2515
|
+
const intersectionWidth = Math.max(0, Math.min(leftX + left.width, rightX + right.width) -
|
|
2516
|
+
Math.max(leftX, rightX));
|
|
2517
|
+
const intersectionHeight = Math.max(0, Math.min(leftY + left.height, rightY + right.height) -
|
|
2518
|
+
Math.max(leftY, rightY));
|
|
2519
|
+
const intersection = intersectionWidth * intersectionHeight;
|
|
2520
|
+
const union = left.width * left.height + right.width * right.height - intersection;
|
|
2521
|
+
return union <= 0 ? 0 : intersection / union;
|
|
2522
|
+
}
|
|
2523
|
+
function resolveFrameStep(current, previous) {
|
|
2524
|
+
if (current === undefined || previous === undefined)
|
|
2525
|
+
return 1;
|
|
2526
|
+
return Math.max(1, current - previous);
|
|
2527
|
+
}
|
|
2528
|
+
function normalizePositiveInteger$1(value, label) {
|
|
2529
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
2530
|
+
throw new Error(`${label} must be a positive integer.`);
|
|
2531
|
+
}
|
|
2532
|
+
return value;
|
|
2533
|
+
}
|
|
2534
|
+
function normalizeNonNegativeInteger$1(value, label) {
|
|
2535
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
2536
|
+
throw new Error(`${label} must be a non-negative integer.`);
|
|
2537
|
+
}
|
|
2538
|
+
return value;
|
|
2539
|
+
}
|
|
2540
|
+
function normalizeUnitInterval$1(value, label) {
|
|
2541
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
2542
|
+
throw new Error(`${label} must be between 0 and 1.`);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
function identity(size) {
|
|
2546
|
+
return Array.from({ length: size }, (_, row) => Array.from({ length: size }, (_, column) => (row === column ? 1 : 0)));
|
|
2547
|
+
}
|
|
2548
|
+
function scale(matrix, factor) {
|
|
2549
|
+
return matrix.map((row) => row.map((value) => value * factor));
|
|
2550
|
+
}
|
|
2551
|
+
function transpose(matrix) {
|
|
2552
|
+
return matrix[0].map((_, column) => matrix.map((row) => row[column]));
|
|
2553
|
+
}
|
|
2554
|
+
function add(left, right) {
|
|
2555
|
+
return left.map((row, rowIndex) => row.map((value, columnIndex) => value + right[rowIndex][columnIndex]));
|
|
2556
|
+
}
|
|
2557
|
+
function subtract(left, right) {
|
|
2558
|
+
return left.map((row, rowIndex) => row.map((value, columnIndex) => value - right[rowIndex][columnIndex]));
|
|
2559
|
+
}
|
|
2560
|
+
function multiply(left, right) {
|
|
2561
|
+
return left.map((row) => right[0].map((_, column) => row.reduce((sum, value, index) => sum + value * right[index][column], 0)));
|
|
2562
|
+
}
|
|
2563
|
+
function inverse(matrix) {
|
|
2564
|
+
const size = matrix.length;
|
|
2565
|
+
const augmented = matrix.map((row, index) => [
|
|
2566
|
+
...row,
|
|
2567
|
+
...identity(size)[index],
|
|
2568
|
+
]);
|
|
2569
|
+
for (let column = 0; column < size; column += 1) {
|
|
2570
|
+
let pivot = column;
|
|
2571
|
+
for (let row = column + 1; row < size; row += 1) {
|
|
2572
|
+
if (Math.abs(augmented[row][column]) >
|
|
2573
|
+
Math.abs(augmented[pivot][column])) {
|
|
2574
|
+
pivot = row;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
if (Math.abs(augmented[pivot][column]) < 1e-12) {
|
|
2578
|
+
throw new Error("SORT Kalman covariance is singular.");
|
|
2579
|
+
}
|
|
2580
|
+
[augmented[column], augmented[pivot]] = [
|
|
2581
|
+
augmented[pivot],
|
|
2582
|
+
augmented[column],
|
|
2583
|
+
];
|
|
2584
|
+
const divisor = augmented[column][column];
|
|
2585
|
+
augmented[column] = augmented[column].map((value) => value / divisor);
|
|
2586
|
+
for (let row = 0; row < size; row += 1) {
|
|
2587
|
+
if (row === column)
|
|
2588
|
+
continue;
|
|
2589
|
+
const factor = augmented[row][column];
|
|
2590
|
+
augmented[row] = augmented[row].map((value, index) => value - factor * augmented[column][index]);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
return augmented.map((row) => row.slice(size));
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
/**
|
|
2597
|
+
* Creates one stateful ByteTrack tracker for a single ordered media sequence.
|
|
2598
|
+
*
|
|
2599
|
+
* Defaults and two-stage association mirror roboflow/trackers ByteTrack at
|
|
2600
|
+
* source commit 60b21c8a48676784085fbee455559f16b75a7c9a.
|
|
2601
|
+
* Motion predictions remain internal; only observed detections receive IDs.
|
|
2602
|
+
*/
|
|
2603
|
+
function createByteTrackTracker$1(options = {}) {
|
|
2604
|
+
const lostTrackBuffer = normalizeNonNegativeInteger$1(options.lostTrackBuffer ?? 30, "lostTrackBuffer");
|
|
2605
|
+
const frameRate = options.frameRate ?? 30;
|
|
2606
|
+
const trackActivationThreshold = options.trackActivationThreshold ?? 0.7;
|
|
2607
|
+
const minimumConsecutiveFrames = normalizePositiveInteger$1(options.minimumConsecutiveFrames ?? 2, "minimumConsecutiveFrames");
|
|
2608
|
+
const minimumIouThreshold = options.minimumIouThreshold ?? 0.1;
|
|
2609
|
+
const highConfidenceDetectionThreshold = options.highConfidenceDetectionThreshold ?? 0.6;
|
|
2610
|
+
if (!Number.isFinite(frameRate) || frameRate <= 0) {
|
|
2611
|
+
throw new Error("frameRate must be a finite positive value.");
|
|
2612
|
+
}
|
|
2613
|
+
normalizeUnitInterval$1(trackActivationThreshold, "trackActivationThreshold");
|
|
2614
|
+
normalizeUnitInterval$1(minimumIouThreshold, "minimumIouThreshold");
|
|
2615
|
+
normalizeUnitInterval$1(highConfidenceDetectionThreshold, "highConfidenceDetectionThreshold");
|
|
2616
|
+
const scaledLostTrackBuffer = (frameRate / 30) * lostTrackBuffer;
|
|
2617
|
+
if (!Number.isFinite(scaledLostTrackBuffer)) {
|
|
2618
|
+
throw new Error("Scaled lostTrackBuffer overflows: frameRate / 30 * lostTrackBuffer must be finite.");
|
|
2619
|
+
}
|
|
2620
|
+
const maximumFramesWithoutUpdate = lostTrackBuffer === 0 ? 0 : Math.max(1, Math.ceil(scaledLostTrackBuffer));
|
|
2621
|
+
let tracks = [];
|
|
2622
|
+
let nextTrackerId = 0;
|
|
2623
|
+
let previousFrameIndex;
|
|
2624
|
+
return {
|
|
2625
|
+
reset() {
|
|
2626
|
+
tracks = [];
|
|
2627
|
+
nextTrackerId = 0;
|
|
2628
|
+
previousFrameIndex = undefined;
|
|
2629
|
+
},
|
|
2630
|
+
update(detections, frameIndex) {
|
|
2631
|
+
const frameStep = resolveFrameStep(frameIndex, previousFrameIndex);
|
|
2632
|
+
previousFrameIndex = frameIndex ?? previousFrameIndex;
|
|
2633
|
+
tracks.forEach((track) => track.predict(frameStep, frameRate));
|
|
2634
|
+
const highDetectionIndexes = [];
|
|
2635
|
+
const lowDetectionIndexes = [];
|
|
2636
|
+
detections.forEach((detection, index) => {
|
|
2637
|
+
if ((detection.confidence ?? 1) >= highConfidenceDetectionThreshold) {
|
|
2638
|
+
highDetectionIndexes.push(index);
|
|
2639
|
+
}
|
|
2640
|
+
else {
|
|
2641
|
+
lowDetectionIndexes.push(index);
|
|
2642
|
+
}
|
|
2643
|
+
});
|
|
2644
|
+
const assignments = new Map();
|
|
2645
|
+
const firstStage = associate(tracks, detections, highDetectionIndexes, minimumIouThreshold);
|
|
2646
|
+
for (const match of firstStage.matches) {
|
|
2647
|
+
updateMatchedTrack(tracks[match.trackIndex], detections[match.detectionIndex], assignments);
|
|
2648
|
+
}
|
|
2649
|
+
const remainingTracks = firstStage.unmatchedTrackIndexes.map((index) => tracks[index]);
|
|
2650
|
+
const secondStage = associate(remainingTracks, detections, lowDetectionIndexes, minimumIouThreshold);
|
|
2651
|
+
for (const match of secondStage.matches) {
|
|
2652
|
+
updateMatchedTrack(remainingTracks[match.trackIndex], detections[match.detectionIndex], assignments);
|
|
2653
|
+
}
|
|
2654
|
+
// Only unmatched high-confidence observations can start a track.
|
|
2655
|
+
for (const detectionIndex of firstStage.unmatchedDetectionIndexes) {
|
|
2656
|
+
const detection = detections[detectionIndex];
|
|
2657
|
+
if ((detection.confidence ?? 1) >= trackActivationThreshold) {
|
|
2658
|
+
tracks.push(new KalmanBoxTrack(detection, true));
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
// Confirmation is sticky once an ID has been allocated. An unmatched
|
|
2662
|
+
// unconfirmed track is discarded immediately, as in Python ByteTrack.
|
|
2663
|
+
tracks = tracks.filter((track) => track.timeSinceUpdate <= maximumFramesWithoutUpdate &&
|
|
2664
|
+
(track.trackerId !== undefined ||
|
|
2665
|
+
track.successfulUpdates >= minimumConsecutiveFrames ||
|
|
2666
|
+
track.timeSinceUpdate === 0));
|
|
2667
|
+
return {
|
|
2668
|
+
activeTrackCount: tracks.length,
|
|
2669
|
+
assignments: detections.flatMap((detection) => {
|
|
2670
|
+
const trackerId = assignments.get(detection.detectionIndex);
|
|
2671
|
+
return trackerId === undefined
|
|
2672
|
+
? []
|
|
2673
|
+
: [{ detectionIndex: detection.detectionIndex, trackerId }];
|
|
2674
|
+
}),
|
|
2675
|
+
confirmedTrackCount: tracks.filter((track) => track.trackerId !== undefined).length,
|
|
2676
|
+
};
|
|
2677
|
+
},
|
|
2678
|
+
};
|
|
2679
|
+
function updateMatchedTrack(track, detection, assignments) {
|
|
2680
|
+
track.update(detection);
|
|
2681
|
+
if (track.trackerId === undefined &&
|
|
2682
|
+
track.successfulUpdates >= minimumConsecutiveFrames) {
|
|
2683
|
+
track.trackerId = nextTrackerId;
|
|
2684
|
+
nextTrackerId += 1;
|
|
2685
|
+
}
|
|
2686
|
+
if (track.trackerId !== undefined) {
|
|
2687
|
+
assignments.set(detection.detectionIndex, track.trackerId);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
function associate(tracks, detections, detectionIndexes, minimumIouThreshold) {
|
|
2692
|
+
if (tracks.length === 0 || detectionIndexes.length === 0) {
|
|
2693
|
+
return {
|
|
2694
|
+
matches: [],
|
|
2695
|
+
unmatchedDetectionIndexes: [...detectionIndexes],
|
|
2696
|
+
unmatchedTrackIndexes: tracks.map((_, index) => index),
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
const scores = tracks.map((track) => {
|
|
2700
|
+
const predictedRect = track.getStateRect();
|
|
2701
|
+
return detectionIndexes.map((detectionIndex) => intersectionOverUnion(predictedRect, detections[detectionIndex].rect));
|
|
2702
|
+
});
|
|
2703
|
+
const matches = [];
|
|
2704
|
+
const matchedTracks = new Set();
|
|
2705
|
+
const matchedDetections = new Set();
|
|
2706
|
+
for (const [trackIndex, localDetectionIndex] of maximizeAssignment(scores)) {
|
|
2707
|
+
if (scores[trackIndex][localDetectionIndex] < minimumIouThreshold) {
|
|
2708
|
+
continue;
|
|
2709
|
+
}
|
|
2710
|
+
const detectionIndex = detectionIndexes[localDetectionIndex];
|
|
2711
|
+
matches.push({ detectionIndex, trackIndex });
|
|
2712
|
+
matchedTracks.add(trackIndex);
|
|
2713
|
+
matchedDetections.add(detectionIndex);
|
|
2714
|
+
}
|
|
2715
|
+
matches.sort((left, right) => left.trackIndex - right.trackIndex);
|
|
2716
|
+
return {
|
|
2717
|
+
matches,
|
|
2718
|
+
unmatchedDetectionIndexes: detectionIndexes.filter((index) => !matchedDetections.has(index)),
|
|
2719
|
+
unmatchedTrackIndexes: tracks.flatMap((_, index) => matchedTracks.has(index) ? [] : [index]),
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
function associateTrackingScores(scores, trackCount, detectionCount, minimumScore, acceptanceScores = scores) {
|
|
2724
|
+
const matches = [];
|
|
2725
|
+
const matchedTracks = new Set();
|
|
2726
|
+
const matchedDetections = new Set();
|
|
2727
|
+
for (const [trackIndex, detectionIndex] of maximizeAssignment(scores)) {
|
|
2728
|
+
if (acceptanceScores[trackIndex][detectionIndex] < minimumScore)
|
|
2729
|
+
continue;
|
|
2730
|
+
matches.push({ detectionIndex, trackIndex });
|
|
2731
|
+
matchedTracks.add(trackIndex);
|
|
2732
|
+
matchedDetections.add(detectionIndex);
|
|
2733
|
+
}
|
|
2734
|
+
matches.sort((left, right) => left.trackIndex - right.trackIndex);
|
|
2735
|
+
return {
|
|
2736
|
+
matches,
|
|
2737
|
+
unmatchedDetectionIndexes: Array.from({ length: detectionCount }, (_, index) => index).filter((index) => !matchedDetections.has(index)),
|
|
2738
|
+
unmatchedTrackIndexes: Array.from({ length: trackCount }, (_, index) => index).filter((index) => !matchedTracks.has(index)),
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
function pairwiseIou(tracks, detections, bufferRatio = 0) {
|
|
2742
|
+
return tracks.map((track) => detections.map((detection) => bufferedIntersectionOverUnion(track, detection, bufferRatio)));
|
|
2743
|
+
}
|
|
2744
|
+
function bufferedIntersectionOverUnion(left, right, bufferRatio) {
|
|
2745
|
+
const leftWidth = left.width * (1 + 2 * bufferRatio);
|
|
2746
|
+
const leftHeight = left.height * (1 + 2 * bufferRatio);
|
|
2747
|
+
const rightWidth = right.width * (1 + 2 * bufferRatio);
|
|
2748
|
+
const rightHeight = right.height * (1 + 2 * bufferRatio);
|
|
2749
|
+
const leftX = left.x - leftWidth / 2;
|
|
2750
|
+
const leftY = left.y - leftHeight / 2;
|
|
2751
|
+
const rightX = right.x - rightWidth / 2;
|
|
2752
|
+
const rightY = right.y - rightHeight / 2;
|
|
2753
|
+
const intersectionWidth = Math.max(0, Math.min(leftX + leftWidth, rightX + rightWidth) - Math.max(leftX, rightX));
|
|
2754
|
+
const intersectionHeight = Math.max(0, Math.min(leftY + leftHeight, rightY + rightHeight) -
|
|
2755
|
+
Math.max(leftY, rightY));
|
|
2756
|
+
const intersection = intersectionWidth * intersectionHeight;
|
|
2757
|
+
const union = leftWidth * leftHeight + rightWidth * rightHeight - intersection;
|
|
2758
|
+
return union <= 0 ? 0 : intersection / union;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
/**
|
|
2762
|
+
* Kalman state estimator shared by the browser C-BIoU and OC-SORT ports.
|
|
2763
|
+
* Its layouts, covariance update, and gap-scaled constant-velocity model
|
|
2764
|
+
* mirror roboflow/trackers at 60b21c8.
|
|
2765
|
+
*/
|
|
2766
|
+
class TrackingKalmanEstimator {
|
|
2767
|
+
representation;
|
|
2768
|
+
dimension;
|
|
2769
|
+
measurementDimension = 4;
|
|
2770
|
+
covariance;
|
|
2771
|
+
measurementNoise = identity(4);
|
|
2772
|
+
processNoise;
|
|
2773
|
+
state;
|
|
2774
|
+
baselineProcessNoise;
|
|
2775
|
+
positionIndexes;
|
|
2776
|
+
velocityIndexes;
|
|
2777
|
+
constructor(initialRect, representation) {
|
|
2778
|
+
this.representation = representation;
|
|
2779
|
+
const measurement = rectToMeasurement(initialRect, representation);
|
|
2780
|
+
this.dimension = representation === "xcycsr" ? 7 : 8;
|
|
2781
|
+
this.positionIndexes =
|
|
2782
|
+
representation === "xcycsr" ? [0, 1, 2] : [0, 1, 2, 3];
|
|
2783
|
+
this.velocityIndexes =
|
|
2784
|
+
representation === "xcycsr" ? [4, 5, 6] : [4, 5, 6, 7];
|
|
2785
|
+
this.state = Array.from({ length: this.dimension }, (_, index) => [
|
|
2786
|
+
measurement[index] ?? 0,
|
|
2787
|
+
]);
|
|
2788
|
+
this.covariance = identity(this.dimension);
|
|
2789
|
+
this.processNoise = identity(this.dimension);
|
|
2790
|
+
this.baselineProcessNoise = identity(this.dimension);
|
|
2791
|
+
}
|
|
2792
|
+
predict(frameStep, frameRate) {
|
|
2793
|
+
if (this.representation === "xcycsr" &&
|
|
2794
|
+
this.state[2][0] + frameStep * this.state[6][0] <= 0) {
|
|
2795
|
+
this.state[6][0] = 0;
|
|
2796
|
+
}
|
|
2797
|
+
const transition = identity(this.dimension);
|
|
2798
|
+
this.positionIndexes.forEach((positionIndex, index) => {
|
|
2799
|
+
transition[positionIndex][this.velocityIndexes[index]] = frameStep;
|
|
2800
|
+
});
|
|
2801
|
+
this.processNoise = this.createProcessNoise(frameStep, frameRate);
|
|
2802
|
+
this.state = multiply(transition, this.state);
|
|
2803
|
+
this.covariance = add(multiply(multiply(transition, this.covariance), transpose(transition)), this.processNoise);
|
|
2804
|
+
}
|
|
2805
|
+
update(rect) {
|
|
2806
|
+
this.updateMeasurement(rectToMeasurement(rect, this.representation));
|
|
2807
|
+
}
|
|
2808
|
+
updateMeasurement(measurement) {
|
|
2809
|
+
const observation = Array.from({ length: 4 }, (_, row) => Array.from({ length: this.dimension }, (_, column) => row === column ? 1 : 0));
|
|
2810
|
+
const measurementColumn = measurement.map((value) => [value]);
|
|
2811
|
+
const innovation = subtract(measurementColumn, multiply(observation, this.state));
|
|
2812
|
+
const covarianceObservationTranspose = multiply(this.covariance, transpose(observation));
|
|
2813
|
+
const innovationCovariance = add(multiply(observation, covarianceObservationTranspose), this.measurementNoise);
|
|
2814
|
+
const gain = multiply(covarianceObservationTranspose, inverse(innovationCovariance));
|
|
2815
|
+
this.state = add(this.state, multiply(gain, innovation));
|
|
2816
|
+
const identityMinusGainObservation = subtract(identity(this.dimension), multiply(gain, observation));
|
|
2817
|
+
this.covariance = add(multiply(multiply(identityMinusGainObservation, this.covariance), transpose(identityMinusGainObservation)), multiply(multiply(gain, this.measurementNoise), transpose(gain)));
|
|
2818
|
+
}
|
|
2819
|
+
getRect() {
|
|
2820
|
+
return measurementToRect(this.state.slice(0, 4).map((row) => row[0]), this.representation);
|
|
2821
|
+
}
|
|
2822
|
+
setCovariances(options) {
|
|
2823
|
+
if (options.covariance)
|
|
2824
|
+
this.covariance = clone(options.covariance);
|
|
2825
|
+
if (options.measurementNoise) {
|
|
2826
|
+
this.measurementNoise = clone(options.measurementNoise);
|
|
2827
|
+
}
|
|
2828
|
+
if (options.processNoise) {
|
|
2829
|
+
this.processNoise = clone(options.processNoise);
|
|
2830
|
+
this.baselineProcessNoise = clone(options.processNoise);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
snapshot() {
|
|
2834
|
+
return {
|
|
2835
|
+
covariance: clone(this.covariance),
|
|
2836
|
+
measurementNoise: clone(this.measurementNoise),
|
|
2837
|
+
processNoise: clone(this.processNoise),
|
|
2838
|
+
state: clone(this.state),
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
restore(snapshot) {
|
|
2842
|
+
this.covariance = clone(snapshot.covariance);
|
|
2843
|
+
this.measurementNoise = clone(snapshot.measurementNoise);
|
|
2844
|
+
this.processNoise = clone(snapshot.processNoise);
|
|
2845
|
+
this.state = clone(snapshot.state);
|
|
2846
|
+
}
|
|
2847
|
+
createProcessNoise(frameStep, frameRate) {
|
|
2848
|
+
if (Math.abs(frameStep - 1) <= 0.004 * frameRate) {
|
|
2849
|
+
return clone(this.baselineProcessNoise);
|
|
2850
|
+
}
|
|
2851
|
+
const result = Array.from({ length: this.dimension }, () => new Array(this.dimension).fill(0));
|
|
2852
|
+
const dt2 = frameStep * frameStep;
|
|
2853
|
+
const dt3 = dt2 * frameStep;
|
|
2854
|
+
const dt4 = dt2 * dt2;
|
|
2855
|
+
const kinematicIndexes = new Set([
|
|
2856
|
+
...this.positionIndexes,
|
|
2857
|
+
...this.velocityIndexes,
|
|
2858
|
+
]);
|
|
2859
|
+
this.positionIndexes.forEach((positionIndex, index) => {
|
|
2860
|
+
const velocityIndex = this.velocityIndexes[index];
|
|
2861
|
+
const accelerationVariance = this.baselineProcessNoise[velocityIndex][velocityIndex];
|
|
2862
|
+
result[positionIndex][positionIndex] = (accelerationVariance * dt4) / 4;
|
|
2863
|
+
result[positionIndex][velocityIndex] = (accelerationVariance * dt3) / 2;
|
|
2864
|
+
result[velocityIndex][positionIndex] = (accelerationVariance * dt3) / 2;
|
|
2865
|
+
result[velocityIndex][velocityIndex] = accelerationVariance * dt2;
|
|
2866
|
+
});
|
|
2867
|
+
for (let index = 0; index < this.dimension; index += 1) {
|
|
2868
|
+
if (!kinematicIndexes.has(index)) {
|
|
2869
|
+
result[index][index] = this.baselineProcessNoise[index][index];
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
return result;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
function rectToMeasurement(rect, representation) {
|
|
2876
|
+
if (representation === "xcycwh") {
|
|
2877
|
+
return [rect.x, rect.y, rect.width, rect.height];
|
|
2878
|
+
}
|
|
2879
|
+
return [
|
|
2880
|
+
rect.x,
|
|
2881
|
+
rect.y,
|
|
2882
|
+
rect.width * rect.height,
|
|
2883
|
+
rect.width / (rect.height + 1e-6),
|
|
2884
|
+
];
|
|
2885
|
+
}
|
|
2886
|
+
function measurementToRect(measurement, representation) {
|
|
2887
|
+
const [x, y, third, fourth] = measurement;
|
|
2888
|
+
if (representation === "xcycwh") {
|
|
2889
|
+
return {
|
|
2890
|
+
height: Math.max(1e-3, fourth),
|
|
2891
|
+
width: Math.max(1e-3, third),
|
|
2892
|
+
x,
|
|
2893
|
+
y,
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
const width = Math.sqrt(third * fourth);
|
|
2897
|
+
const height = width === 0 ? 0 : third / width;
|
|
2898
|
+
return {
|
|
2899
|
+
height: Math.max(Number.EPSILON, height),
|
|
2900
|
+
width: Math.max(Number.EPSILON, width),
|
|
2901
|
+
x,
|
|
2902
|
+
y,
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
function diagonal(values) {
|
|
2906
|
+
return values.map((value, row) => values.map((_, column) => (row === column ? value : 0)));
|
|
2907
|
+
}
|
|
2908
|
+
function scaleMatrix(matrix, factor) {
|
|
2909
|
+
return scale(matrix, factor);
|
|
2910
|
+
}
|
|
2911
|
+
function clone(matrix) {
|
|
2912
|
+
return matrix.map((row) => [...row]);
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
const MINIMUM_DETECTION_CONFIDENCE = 0.1;
|
|
2916
|
+
class CBIoUTrack {
|
|
2917
|
+
trackerId;
|
|
2918
|
+
successfulUpdates = 1;
|
|
2919
|
+
timeSinceUpdate = 0;
|
|
2920
|
+
estimator;
|
|
2921
|
+
constructor(initial) {
|
|
2922
|
+
this.estimator = new TrackingKalmanEstimator(initial.rect, "xcycwh");
|
|
2923
|
+
this.setInitialNoise(initial.rect.width, initial.rect.height);
|
|
2924
|
+
}
|
|
2925
|
+
predict(frameStep, frameRate) {
|
|
2926
|
+
const current = this.estimator.getRect();
|
|
2927
|
+
this.estimator.setCovariances({
|
|
2928
|
+
processNoise: this.buildProcessNoise(Math.max(current.width, 1e-3), Math.max(current.height, 1e-3)),
|
|
2929
|
+
});
|
|
2930
|
+
this.estimator.predict(frameStep, frameRate);
|
|
2931
|
+
this.clampState();
|
|
2932
|
+
this.timeSinceUpdate += 1;
|
|
2933
|
+
}
|
|
2934
|
+
update(detection) {
|
|
2935
|
+
const current = this.estimator.getRect();
|
|
2936
|
+
this.estimator.setCovariances({
|
|
2937
|
+
measurementNoise: this.buildMeasurementNoise(Math.max(current.width, 1e-3), Math.max(current.height, 1e-3)),
|
|
2938
|
+
});
|
|
2939
|
+
this.estimator.update(detection.rect);
|
|
2940
|
+
this.clampState();
|
|
2941
|
+
this.timeSinceUpdate = 0;
|
|
2942
|
+
this.successfulUpdates += 1;
|
|
2943
|
+
}
|
|
2944
|
+
getRect() {
|
|
2945
|
+
return this.estimator.getRect();
|
|
2946
|
+
}
|
|
2947
|
+
setInitialNoise(width, height) {
|
|
2948
|
+
const sigmaPosition = 0.05;
|
|
2949
|
+
const sigmaVelocity = 0.00625;
|
|
2950
|
+
this.estimator.setCovariances({
|
|
2951
|
+
covariance: diagonal([
|
|
2952
|
+
(2 * sigmaPosition * width) ** 2,
|
|
2953
|
+
(2 * sigmaPosition * height) ** 2,
|
|
2954
|
+
(2 * sigmaPosition * width) ** 2,
|
|
2955
|
+
(2 * sigmaPosition * height) ** 2,
|
|
2956
|
+
(10 * sigmaVelocity * width) ** 2,
|
|
2957
|
+
(10 * sigmaVelocity * height) ** 2,
|
|
2958
|
+
(10 * sigmaVelocity * width) ** 2,
|
|
2959
|
+
(10 * sigmaVelocity * height) ** 2,
|
|
2960
|
+
]),
|
|
2961
|
+
measurementNoise: this.buildMeasurementNoise(width, height),
|
|
2962
|
+
processNoise: this.buildProcessNoise(width, height),
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
buildProcessNoise(width, height) {
|
|
2966
|
+
const sigmaPosition = 0.05;
|
|
2967
|
+
const sigmaVelocity = 0.00625;
|
|
2968
|
+
return diagonal([
|
|
2969
|
+
(sigmaPosition * width) ** 2,
|
|
2970
|
+
(sigmaPosition * height) ** 2,
|
|
2971
|
+
(sigmaPosition * width) ** 2,
|
|
2972
|
+
(sigmaPosition * height) ** 2,
|
|
2973
|
+
(sigmaVelocity * width) ** 2,
|
|
2974
|
+
(sigmaVelocity * height) ** 2,
|
|
2975
|
+
(sigmaVelocity * width) ** 2,
|
|
2976
|
+
(sigmaVelocity * height) ** 2,
|
|
2977
|
+
]);
|
|
2978
|
+
}
|
|
2979
|
+
buildMeasurementNoise(width, height) {
|
|
2980
|
+
const sigmaMeasurement = 0.05;
|
|
2981
|
+
return diagonal([
|
|
2982
|
+
(sigmaMeasurement * width) ** 2,
|
|
2983
|
+
(sigmaMeasurement * height) ** 2,
|
|
2984
|
+
(sigmaMeasurement * width) ** 2,
|
|
2985
|
+
(sigmaMeasurement * height) ** 2,
|
|
2986
|
+
]);
|
|
2987
|
+
}
|
|
2988
|
+
clampState() {
|
|
2989
|
+
this.estimator.state[2][0] = Math.max(this.estimator.state[2][0], 1e-3);
|
|
2990
|
+
this.estimator.state[3][0] = Math.max(this.estimator.state[3][0], 1e-3);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
/** Creates the detection-only C-BIoU implementation from roboflow/trackers. */
|
|
2994
|
+
function createCBIoUTracker$1(options = {}) {
|
|
2995
|
+
const lostTrackBuffer = normalizeNonNegativeInteger$1(options.lostTrackBuffer ?? 30, "lostTrackBuffer");
|
|
2996
|
+
const frameRate = options.frameRate ?? 30;
|
|
2997
|
+
const trackActivationThreshold = options.trackActivationThreshold ?? 0.7;
|
|
2998
|
+
const minimumConsecutiveFrames = normalizePositiveInteger$1(options.minimumConsecutiveFrames ?? 2, "minimumConsecutiveFrames");
|
|
2999
|
+
const minimumIouThresholdFirstAssociation = options.minimumIouThresholdFirstAssociation ?? 0.2;
|
|
3000
|
+
const minimumIouThresholdSecondAssociation = options.minimumIouThresholdSecondAssociation ?? 0.5;
|
|
3001
|
+
const minimumIouThresholdUnconfirmedAssociation = options.minimumIouThresholdUnconfirmedAssociation ?? 0.3;
|
|
3002
|
+
const highConfidenceDetectionThreshold = options.highConfidenceDetectionThreshold ?? 0.6;
|
|
3003
|
+
const instantFirstFrameActivation = options.instantFirstFrameActivation ?? true;
|
|
3004
|
+
const bufferRatioFirst = options.bufferRatioFirst ?? 0.3;
|
|
3005
|
+
const bufferRatioSecond = options.bufferRatioSecond ?? 0.5;
|
|
3006
|
+
if (!Number.isFinite(frameRate) || frameRate <= 0) {
|
|
3007
|
+
throw new Error("frameRate must be a finite positive value.");
|
|
3008
|
+
}
|
|
3009
|
+
normalizeUnitInterval$1(trackActivationThreshold, "trackActivationThreshold");
|
|
3010
|
+
normalizeUnitInterval$1(minimumIouThresholdFirstAssociation, "minimumIouThresholdFirstAssociation");
|
|
3011
|
+
normalizeUnitInterval$1(minimumIouThresholdSecondAssociation, "minimumIouThresholdSecondAssociation");
|
|
3012
|
+
normalizeUnitInterval$1(minimumIouThresholdUnconfirmedAssociation, "minimumIouThresholdUnconfirmedAssociation");
|
|
3013
|
+
normalizeUnitInterval$1(highConfidenceDetectionThreshold, "highConfidenceDetectionThreshold");
|
|
3014
|
+
if (!Number.isFinite(bufferRatioFirst) || bufferRatioFirst < 0) {
|
|
3015
|
+
throw new Error("bufferRatioFirst must be a finite non-negative value.");
|
|
3016
|
+
}
|
|
3017
|
+
if (!Number.isFinite(bufferRatioSecond) || bufferRatioSecond < 0) {
|
|
3018
|
+
throw new Error("bufferRatioSecond must be a finite non-negative value.");
|
|
3019
|
+
}
|
|
3020
|
+
const scaledLostTrackBuffer = (frameRate / 30) * lostTrackBuffer;
|
|
3021
|
+
if (!Number.isFinite(scaledLostTrackBuffer)) {
|
|
3022
|
+
throw new Error("Scaled lostTrackBuffer overflows: frameRate / 30 * lostTrackBuffer must be finite.");
|
|
3023
|
+
}
|
|
3024
|
+
const maximumFramesWithoutUpdate = lostTrackBuffer === 0 ? 0 : Math.max(1, Math.ceil(scaledLostTrackBuffer));
|
|
3025
|
+
let tracks = [];
|
|
3026
|
+
let nextTrackerId = 0;
|
|
3027
|
+
let frameId = 0;
|
|
3028
|
+
let previousFrameIndex;
|
|
3029
|
+
return {
|
|
3030
|
+
reset() {
|
|
3031
|
+
tracks = [];
|
|
3032
|
+
nextTrackerId = 0;
|
|
3033
|
+
frameId = 0;
|
|
3034
|
+
previousFrameIndex = undefined;
|
|
3035
|
+
},
|
|
3036
|
+
update(detections, frameIndex) {
|
|
3037
|
+
const frameStep = resolveFrameStep(frameIndex, previousFrameIndex);
|
|
3038
|
+
previousFrameIndex = frameIndex ?? previousFrameIndex;
|
|
3039
|
+
frameId += 1;
|
|
3040
|
+
tracks.forEach((track) => track.predict(frameStep, frameRate));
|
|
3041
|
+
const highDetectionIndexes = [];
|
|
3042
|
+
const lowDetectionIndexes = [];
|
|
3043
|
+
detections.forEach((detection, index) => {
|
|
3044
|
+
const confidence = detection.confidence ?? 1;
|
|
3045
|
+
if (confidence >= highConfidenceDetectionThreshold) {
|
|
3046
|
+
highDetectionIndexes.push(index);
|
|
3047
|
+
}
|
|
3048
|
+
else if (confidence > MINIMUM_DETECTION_CONFIDENCE) {
|
|
3049
|
+
lowDetectionIndexes.push(index);
|
|
3050
|
+
}
|
|
3051
|
+
});
|
|
3052
|
+
const confirmed = [];
|
|
3053
|
+
const unconfirmed = [];
|
|
3054
|
+
const lost = [];
|
|
3055
|
+
for (const track of tracks) {
|
|
3056
|
+
if (track.timeSinceUpdate > 1)
|
|
3057
|
+
lost.push(track);
|
|
3058
|
+
else if (track.trackerId !== undefined ||
|
|
3059
|
+
track.successfulUpdates >= minimumConsecutiveFrames) {
|
|
3060
|
+
confirmed.push(track);
|
|
3061
|
+
}
|
|
3062
|
+
else
|
|
3063
|
+
unconfirmed.push(track);
|
|
3064
|
+
}
|
|
3065
|
+
const assignments = new Map();
|
|
3066
|
+
const pool = [...confirmed, ...lost];
|
|
3067
|
+
const highDetections = highDetectionIndexes.map((index) => detections[index]);
|
|
3068
|
+
const firstScores = pairwiseIou(pool.map((track) => track.getRect()), highDetections.map((detection) => detection.rect), bufferRatioFirst).map((row) => row.map((score, index) => score * (highDetections[index].confidence ?? 1)));
|
|
3069
|
+
const firstAssociation = associateTrackingScores(firstScores, pool.length, highDetections.length, minimumIouThresholdFirstAssociation);
|
|
3070
|
+
for (const match of firstAssociation.matches) {
|
|
3071
|
+
updateMatchedTrack(pool[match.trackIndex], highDetections[match.detectionIndex], assignments);
|
|
3072
|
+
}
|
|
3073
|
+
const remainingTracked = firstAssociation.unmatchedTrackIndexes
|
|
3074
|
+
.map((index) => pool[index])
|
|
3075
|
+
.filter((track) => track.timeSinceUpdate === 1);
|
|
3076
|
+
const lowDetections = lowDetectionIndexes.map((index) => detections[index]);
|
|
3077
|
+
const secondScores = pairwiseIou(remainingTracked.map((track) => track.getRect()), lowDetections.map((detection) => detection.rect), bufferRatioSecond);
|
|
3078
|
+
const secondAssociation = associateTrackingScores(secondScores, remainingTracked.length, lowDetections.length, minimumIouThresholdSecondAssociation);
|
|
3079
|
+
for (const match of secondAssociation.matches) {
|
|
3080
|
+
updateMatchedTrack(remainingTracked[match.trackIndex], lowDetections[match.detectionIndex], assignments);
|
|
3081
|
+
}
|
|
3082
|
+
let unmatchedHighLocal = [...firstAssociation.unmatchedDetectionIndexes];
|
|
3083
|
+
let unmatchedUnconfirmed = unconfirmed.map((_, index) => index);
|
|
3084
|
+
if (unconfirmed.length > 0 && unmatchedHighLocal.length > 0) {
|
|
3085
|
+
const remainingHigh = unmatchedHighLocal.map((index) => highDetections[index]);
|
|
3086
|
+
const unconfirmedScores = pairwiseIou(unconfirmed.map((track) => track.getRect()), remainingHigh.map((detection) => detection.rect), bufferRatioFirst).map((row) => row.map((score, index) => score * (remainingHigh[index].confidence ?? 1)));
|
|
3087
|
+
const unconfirmedAssociation = associateTrackingScores(unconfirmedScores, unconfirmed.length, remainingHigh.length, minimumIouThresholdUnconfirmedAssociation);
|
|
3088
|
+
unmatchedUnconfirmed = unconfirmedAssociation.unmatchedTrackIndexes;
|
|
3089
|
+
for (const match of unconfirmedAssociation.matches) {
|
|
3090
|
+
updateMatchedTrack(unconfirmed[match.trackIndex], remainingHigh[match.detectionIndex], assignments);
|
|
3091
|
+
}
|
|
3092
|
+
unmatchedHighLocal =
|
|
3093
|
+
unconfirmedAssociation.unmatchedDetectionIndexes.map((index) => unmatchedHighLocal[index]);
|
|
3094
|
+
}
|
|
3095
|
+
const unmatchedUnconfirmedTracks = new Set(unmatchedUnconfirmed.map((index) => unconfirmed[index]));
|
|
3096
|
+
tracks = tracks.filter((track) => !unmatchedUnconfirmedTracks.has(track));
|
|
3097
|
+
for (const localIndex of unmatchedHighLocal) {
|
|
3098
|
+
const detection = highDetections[localIndex];
|
|
3099
|
+
if ((detection.confidence ?? 1) < trackActivationThreshold)
|
|
3100
|
+
continue;
|
|
3101
|
+
const track = new CBIoUTrack(detection);
|
|
3102
|
+
if (frameId === 1 && instantFirstFrameActivation) {
|
|
3103
|
+
track.trackerId = nextTrackerId++;
|
|
3104
|
+
assignments.set(detection.detectionIndex, track.trackerId);
|
|
3105
|
+
}
|
|
3106
|
+
tracks.push(track);
|
|
3107
|
+
}
|
|
3108
|
+
tracks = tracks.filter((track) => track.timeSinceUpdate <= maximumFramesWithoutUpdate &&
|
|
3109
|
+
(track.timeSinceUpdate === 0 ||
|
|
3110
|
+
track.trackerId !== undefined ||
|
|
3111
|
+
track.successfulUpdates >= minimumConsecutiveFrames));
|
|
3112
|
+
return createUpdate(detections, assignments, tracks);
|
|
3113
|
+
},
|
|
3114
|
+
};
|
|
3115
|
+
function updateMatchedTrack(track, detection, assignments) {
|
|
3116
|
+
track.update(detection);
|
|
3117
|
+
if (track.trackerId === undefined &&
|
|
3118
|
+
track.successfulUpdates >= minimumConsecutiveFrames) {
|
|
3119
|
+
track.trackerId = nextTrackerId++;
|
|
3120
|
+
}
|
|
3121
|
+
if (track.trackerId !== undefined) {
|
|
3122
|
+
assignments.set(detection.detectionIndex, track.trackerId);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
function createUpdate(detections, assignments, tracks) {
|
|
3127
|
+
const resolvedAssignments = detections.flatMap((detection) => {
|
|
3128
|
+
const trackerId = assignments.get(detection.detectionIndex);
|
|
3129
|
+
return trackerId === undefined
|
|
3130
|
+
? []
|
|
3131
|
+
: [{ detectionIndex: detection.detectionIndex, trackerId }];
|
|
3132
|
+
});
|
|
3133
|
+
return {
|
|
3134
|
+
activeTrackCount: tracks.length,
|
|
3135
|
+
assignments: resolvedAssignments,
|
|
3136
|
+
confirmedTrackCount: tracks.filter((track) => track.trackerId !== undefined)
|
|
3137
|
+
.length,
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
class OCSortTrack {
|
|
3142
|
+
deltaT;
|
|
3143
|
+
age = 0;
|
|
3144
|
+
lastObservation;
|
|
3145
|
+
trackerId;
|
|
3146
|
+
successfulConsecutiveUpdates = 0;
|
|
3147
|
+
timeSinceUpdate = 0;
|
|
3148
|
+
velocity;
|
|
3149
|
+
frozenState;
|
|
3150
|
+
observed = true;
|
|
3151
|
+
observations = new Map();
|
|
3152
|
+
estimator;
|
|
3153
|
+
constructor(initial, deltaT) {
|
|
3154
|
+
this.deltaT = deltaT;
|
|
3155
|
+
this.lastObservation = initial;
|
|
3156
|
+
this.estimator = new TrackingKalmanEstimator(initial.rect, "xcycsr");
|
|
3157
|
+
this.configureNoise();
|
|
3158
|
+
}
|
|
3159
|
+
predict(frameStep, frameRate) {
|
|
3160
|
+
if (this.observed && this.timeSinceUpdate > 0) {
|
|
3161
|
+
this.frozenState = this.estimator.snapshot();
|
|
3162
|
+
this.observed = false;
|
|
3163
|
+
}
|
|
3164
|
+
this.estimator.predict(frameStep, frameRate);
|
|
3165
|
+
if (this.timeSinceUpdate > 0) {
|
|
3166
|
+
this.successfulConsecutiveUpdates = 0;
|
|
3167
|
+
}
|
|
3168
|
+
this.timeSinceUpdate += 1;
|
|
3169
|
+
this.age += 1;
|
|
3170
|
+
}
|
|
3171
|
+
update(detection, frameStep, frameRate) {
|
|
3172
|
+
const previous = this.getPreviousObservation();
|
|
3173
|
+
if (previous) {
|
|
3174
|
+
this.velocity = computeVelocity(previous, detection);
|
|
3175
|
+
}
|
|
3176
|
+
if (!this.observed && this.frozenState) {
|
|
3177
|
+
this.unfreeze(detection, frameStep, frameRate);
|
|
3178
|
+
}
|
|
3179
|
+
this.estimator.update(detection.rect);
|
|
3180
|
+
this.observed = true;
|
|
3181
|
+
this.timeSinceUpdate = 0;
|
|
3182
|
+
this.successfulConsecutiveUpdates += 1;
|
|
3183
|
+
this.lastObservation = detection;
|
|
3184
|
+
this.observations.set(this.age, detection);
|
|
3185
|
+
const cutoff = this.age - this.deltaT;
|
|
3186
|
+
for (const age of this.observations.keys()) {
|
|
3187
|
+
if (age < cutoff)
|
|
3188
|
+
this.observations.delete(age);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
getRect() {
|
|
3192
|
+
return this.estimator.getRect();
|
|
3193
|
+
}
|
|
3194
|
+
getPreviousObservation() {
|
|
3195
|
+
if (this.observations.size === 0)
|
|
3196
|
+
return undefined;
|
|
3197
|
+
for (let index = 0; index < this.deltaT; index += 1) {
|
|
3198
|
+
const delta = this.deltaT - index;
|
|
3199
|
+
const observation = this.observations.get(this.age - delta);
|
|
3200
|
+
if (observation)
|
|
3201
|
+
return observation;
|
|
3202
|
+
}
|
|
3203
|
+
const latestAge = Math.max(...this.observations.keys());
|
|
3204
|
+
return this.observations.get(latestAge);
|
|
3205
|
+
}
|
|
3206
|
+
configureNoise() {
|
|
3207
|
+
const measurementNoise = identity(4);
|
|
3208
|
+
for (let index = 2; index < 4; index += 1) {
|
|
3209
|
+
measurementNoise[index][index] *= 10;
|
|
3210
|
+
}
|
|
3211
|
+
const covariance = scaleMatrix(identity(7), 10);
|
|
3212
|
+
for (let index = 4; index < 7; index += 1) {
|
|
3213
|
+
covariance[index][index] *= 1000;
|
|
3214
|
+
}
|
|
3215
|
+
const processNoise = identity(7);
|
|
3216
|
+
processNoise[6][6] *= 0.01;
|
|
3217
|
+
for (let index = 4; index < 7; index += 1) {
|
|
3218
|
+
processNoise[index][index] *= 0.01;
|
|
3219
|
+
}
|
|
3220
|
+
this.estimator.setCovariances({
|
|
3221
|
+
covariance,
|
|
3222
|
+
measurementNoise,
|
|
3223
|
+
processNoise,
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
unfreeze(detection, frameStep, frameRate) {
|
|
3227
|
+
if (!this.frozenState || this.timeSinceUpdate === 0)
|
|
3228
|
+
return;
|
|
3229
|
+
this.estimator.restore(this.frozenState);
|
|
3230
|
+
const timeGap = this.timeSinceUpdate;
|
|
3231
|
+
const start = rectToMeasurement(this.lastObservation.rect, "xcycsr");
|
|
3232
|
+
const end = rectToMeasurement(detection.rect, "xcycsr");
|
|
3233
|
+
const startWidth = Math.sqrt(start[2] * start[3]);
|
|
3234
|
+
const startHeight = start[3] === 0 ? 0 : Math.sqrt(start[2] / start[3]);
|
|
3235
|
+
const endWidth = Math.sqrt(end[2] * end[3]);
|
|
3236
|
+
const endHeight = end[3] === 0 ? 0 : Math.sqrt(end[2] / end[3]);
|
|
3237
|
+
for (let index = 0; index < timeGap; index += 1) {
|
|
3238
|
+
const progress = (index + 1) / timeGap;
|
|
3239
|
+
const x = start[0] + progress * (end[0] - start[0]);
|
|
3240
|
+
const y = start[1] + progress * (end[1] - start[1]);
|
|
3241
|
+
const width = startWidth + progress * (endWidth - startWidth);
|
|
3242
|
+
const height = startHeight + progress * (endHeight - startHeight);
|
|
3243
|
+
this.estimator.updateMeasurement([x, y, width * height, width / height]);
|
|
3244
|
+
if (index < timeGap - 1) {
|
|
3245
|
+
this.estimator.predict(frameStep, frameRate);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
this.frozenState = undefined;
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
/** Creates the observation-centric SORT implementation from roboflow/trackers. */
|
|
3252
|
+
function createOCSortTracker$1(options = {}) {
|
|
3253
|
+
const lostTrackBuffer = normalizeNonNegativeInteger$1(options.lostTrackBuffer ?? 30, "lostTrackBuffer");
|
|
3254
|
+
const frameRate = options.frameRate ?? 30;
|
|
3255
|
+
const minimumConsecutiveFrames = normalizePositiveInteger$1(options.minimumConsecutiveFrames ?? 3, "minimumConsecutiveFrames");
|
|
3256
|
+
const minimumIouThreshold = options.minimumIouThreshold ?? 0.3;
|
|
3257
|
+
const directionConsistencyWeight = options.directionConsistencyWeight ?? 0.2;
|
|
3258
|
+
const highConfidenceDetectionThreshold = options.highConfidenceDetectionThreshold ?? 0.6;
|
|
3259
|
+
const deltaT = normalizePositiveInteger$1(options.deltaT ?? 3, "deltaT");
|
|
3260
|
+
if (!Number.isFinite(frameRate) || frameRate <= 0) {
|
|
3261
|
+
throw new Error("frameRate must be a finite positive value.");
|
|
3262
|
+
}
|
|
3263
|
+
normalizeUnitInterval$1(minimumIouThreshold, "minimumIouThreshold");
|
|
3264
|
+
normalizeUnitInterval$1(directionConsistencyWeight, "directionConsistencyWeight");
|
|
3265
|
+
normalizeUnitInterval$1(highConfidenceDetectionThreshold, "highConfidenceDetectionThreshold");
|
|
3266
|
+
const scaledLostTrackBuffer = (frameRate / 30) * lostTrackBuffer;
|
|
3267
|
+
if (!Number.isFinite(scaledLostTrackBuffer)) {
|
|
3268
|
+
throw new Error("Scaled lostTrackBuffer overflows: frameRate / 30 * lostTrackBuffer must be finite.");
|
|
3269
|
+
}
|
|
3270
|
+
const maximumFramesWithoutUpdate = lostTrackBuffer === 0 ? 0 : Math.max(1, Math.ceil(scaledLostTrackBuffer));
|
|
3271
|
+
let tracks = [];
|
|
3272
|
+
let frameCount = 0;
|
|
3273
|
+
let nextTrackerId = 0;
|
|
3274
|
+
let previousFrameIndex;
|
|
3275
|
+
return {
|
|
3276
|
+
reset() {
|
|
3277
|
+
tracks = [];
|
|
3278
|
+
frameCount = 0;
|
|
3279
|
+
nextTrackerId = 0;
|
|
3280
|
+
previousFrameIndex = undefined;
|
|
3281
|
+
},
|
|
3282
|
+
update(detections, frameIndex) {
|
|
3283
|
+
const frameStep = resolveFrameStep(frameIndex, previousFrameIndex);
|
|
3284
|
+
previousFrameIndex = frameIndex ?? previousFrameIndex;
|
|
3285
|
+
// Match roboflow/trackers: an empty stream before any track exists is
|
|
3286
|
+
// not part of OC-SORT's early-sequence activation window.
|
|
3287
|
+
if (tracks.length === 0 && detections.length === 0) {
|
|
3288
|
+
return {
|
|
3289
|
+
activeTrackCount: 0,
|
|
3290
|
+
assignments: [],
|
|
3291
|
+
confirmedTrackCount: 0,
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
tracks.forEach((track) => track.predict(frameStep, frameRate));
|
|
3295
|
+
const highDetectionIndexes = detections.flatMap((detection, index) => detection.confidence === undefined ||
|
|
3296
|
+
detection.confidence >= highConfidenceDetectionThreshold
|
|
3297
|
+
? [index]
|
|
3298
|
+
: []);
|
|
3299
|
+
const highDetections = highDetectionIndexes.map((index) => detections[index]);
|
|
3300
|
+
const iouScores = pairwiseIou(tracks.map((track) => track.getRect()), highDetections.map((detection) => detection.rect));
|
|
3301
|
+
const combinedScores = iouScores.map((row, trackIndex) => row.map((iou, detectionIndex) => {
|
|
3302
|
+
if (directionConsistencyWeight === 0)
|
|
3303
|
+
return iou;
|
|
3304
|
+
return (iou +
|
|
3305
|
+
directionConsistencyWeight *
|
|
3306
|
+
directionConsistency(tracks[trackIndex], highDetections[detectionIndex]) *
|
|
3307
|
+
(highDetections[detectionIndex].confidence ?? 1));
|
|
3308
|
+
}));
|
|
3309
|
+
const primary = associateTrackingScores(combinedScores, tracks.length, highDetections.length, minimumIouThreshold, iouScores);
|
|
3310
|
+
const assignments = new Map();
|
|
3311
|
+
for (const match of primary.matches) {
|
|
3312
|
+
updateMatchedTrack(tracks[match.trackIndex], highDetections[match.detectionIndex], assignments, frameStep);
|
|
3313
|
+
}
|
|
3314
|
+
let remainingHigh = [...primary.unmatchedDetectionIndexes];
|
|
3315
|
+
if (primary.unmatchedTrackIndexes.length > 0 &&
|
|
3316
|
+
remainingHigh.length > 0) {
|
|
3317
|
+
const unmatchedTracks = primary.unmatchedTrackIndexes.map((index) => tracks[index]);
|
|
3318
|
+
const unmatchedDetections = remainingHigh.map((index) => highDetections[index]);
|
|
3319
|
+
const recoveryScores = pairwiseIou(unmatchedTracks.map((track) => track.lastObservation.rect), unmatchedDetections.map((detection) => detection.rect));
|
|
3320
|
+
const recovery = associateTrackingScores(recoveryScores, unmatchedTracks.length, unmatchedDetections.length, minimumIouThreshold);
|
|
3321
|
+
for (const match of recovery.matches) {
|
|
3322
|
+
updateMatchedTrack(unmatchedTracks[match.trackIndex], unmatchedDetections[match.detectionIndex], assignments, frameStep);
|
|
3323
|
+
}
|
|
3324
|
+
remainingHigh = recovery.unmatchedDetectionIndexes.map((index) => remainingHigh[index]);
|
|
3325
|
+
}
|
|
3326
|
+
for (const localIndex of remainingHigh) {
|
|
3327
|
+
tracks.push(new OCSortTrack(highDetections[localIndex], deltaT));
|
|
3328
|
+
}
|
|
3329
|
+
tracks = tracks.filter((track) => track.timeSinceUpdate <= maximumFramesWithoutUpdate);
|
|
3330
|
+
frameCount += 1;
|
|
3331
|
+
const resolvedAssignments = detections.flatMap((detection) => {
|
|
3332
|
+
const trackerId = assignments.get(detection.detectionIndex);
|
|
3333
|
+
return trackerId === undefined
|
|
3334
|
+
? []
|
|
3335
|
+
: [{ detectionIndex: detection.detectionIndex, trackerId }];
|
|
3336
|
+
});
|
|
3337
|
+
return {
|
|
3338
|
+
activeTrackCount: tracks.length,
|
|
3339
|
+
assignments: resolvedAssignments,
|
|
3340
|
+
confirmedTrackCount: tracks.filter((track) => track.trackerId !== undefined).length,
|
|
3341
|
+
};
|
|
3342
|
+
},
|
|
3343
|
+
};
|
|
3344
|
+
function updateMatchedTrack(track, detection, assignments, frameStep) {
|
|
3345
|
+
track.update(detection, frameStep, frameRate);
|
|
3346
|
+
const earlySequence = frameCount <= minimumConsecutiveFrames;
|
|
3347
|
+
const shouldEmit = (earlySequence && track.timeSinceUpdate === 0) ||
|
|
3348
|
+
track.successfulConsecutiveUpdates >= minimumConsecutiveFrames;
|
|
3349
|
+
if (shouldEmit) {
|
|
3350
|
+
if (track.trackerId === undefined) {
|
|
3351
|
+
track.trackerId = nextTrackerId++;
|
|
3352
|
+
}
|
|
3353
|
+
assignments.set(detection.detectionIndex, track.trackerId);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
function computeVelocity(previous, current) {
|
|
3358
|
+
const deltaX = current.rect.x - previous.rect.x;
|
|
3359
|
+
const deltaY = current.rect.y - previous.rect.y;
|
|
3360
|
+
const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY) + 1e-6;
|
|
3361
|
+
return { x: deltaX / norm, y: deltaY / norm };
|
|
3362
|
+
}
|
|
3363
|
+
function directionConsistency(track, detection) {
|
|
3364
|
+
if (!track.velocity)
|
|
3365
|
+
return 0;
|
|
3366
|
+
const reference = track.getPreviousObservation() ?? track.lastObservation;
|
|
3367
|
+
const deltaX = detection.rect.x - reference.rect.x;
|
|
3368
|
+
const deltaY = detection.rect.y - reference.rect.y;
|
|
3369
|
+
const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY) + 1e-6;
|
|
3370
|
+
const cosine = Math.max(-1, Math.min(1, track.velocity.x * (deltaX / norm) + track.velocity.y * (deltaY / norm)));
|
|
3371
|
+
const angle = Math.acos(cosine);
|
|
3372
|
+
return (Math.PI / 2 - Math.abs(angle)) / Math.PI;
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
/** Public core facade for the internal tracker engine workspace. */
|
|
3376
|
+
function createSortTracker(options = {}) {
|
|
3377
|
+
return createSortTracker$1(options);
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
/** Public core facade for the internal tracker engine workspace. */
|
|
3381
|
+
function createByteTrackTracker(options = {}) {
|
|
3382
|
+
return createByteTrackTracker$1(options);
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
/** Public core facade for the internal tracker engine workspace. */
|
|
3386
|
+
function createCBIoUTracker(options = {}) {
|
|
3387
|
+
return createCBIoUTracker$1(options);
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
/** Public core facade for the internal tracker engine workspace. */
|
|
3391
|
+
function createOCSortTracker(options = {}) {
|
|
3392
|
+
return createOCSortTracker$1(options);
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
/** Geometry projected into a tracker. The original detection payload is retained. */
|
|
3396
|
+
var TrackingGeometry;
|
|
3397
|
+
(function (TrackingGeometry) {
|
|
3398
|
+
TrackingGeometry["Box"] = "box";
|
|
3399
|
+
TrackingGeometry["Mask"] = "mask";
|
|
3400
|
+
TrackingGeometry["Keypoints"] = "keypoints";
|
|
3401
|
+
})(TrackingGeometry || (TrackingGeometry = {}));
|
|
3402
|
+
|
|
3403
|
+
var DetectionMaskPayloadFormat;
|
|
3404
|
+
(function (DetectionMaskPayloadFormat) {
|
|
3405
|
+
DetectionMaskPayloadFormat["RawCocoRle"] = "rawCocoRle";
|
|
3406
|
+
DetectionMaskPayloadFormat["DeflatedBase64"] = "deflatedBase64";
|
|
3407
|
+
})(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));
|
|
3408
|
+
function encodeBinaryMask(data, width, height) {
|
|
3409
|
+
assertMaskDimensions(data, width, height);
|
|
3410
|
+
const runs = [];
|
|
3411
|
+
let currentValue = 0;
|
|
3412
|
+
let runLength = 0;
|
|
3413
|
+
for (let x = 0; x < width; x += 1) {
|
|
3414
|
+
for (let y = 0; y < height; y += 1) {
|
|
3415
|
+
const value = data[y * width + x] ? 1 : 0;
|
|
3416
|
+
if (value === currentValue) {
|
|
3417
|
+
runLength += 1;
|
|
3418
|
+
}
|
|
3419
|
+
else {
|
|
3420
|
+
runs.push(runLength);
|
|
3421
|
+
currentValue = value;
|
|
3422
|
+
runLength = 1;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
runs.push(runLength);
|
|
3427
|
+
return {
|
|
3428
|
+
counts: encodeCompressedRleCounts(runs),
|
|
3429
|
+
encoding: DetectionMaskEncoding.CompressedRle,
|
|
3430
|
+
height,
|
|
3431
|
+
width,
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3434
|
+
/** Encodes a binary mask and derives its bounds in the same raster traversal. */
|
|
3435
|
+
function encodeBinaryMaskWithBounds(data, width, height) {
|
|
3436
|
+
assertMaskDimensions(data, width, height);
|
|
3437
|
+
const runs = [];
|
|
3438
|
+
let currentValue = 0;
|
|
3439
|
+
let runLength = 0;
|
|
3440
|
+
let minX = width;
|
|
3441
|
+
let minY = height;
|
|
3442
|
+
let maxX = -1;
|
|
3443
|
+
let maxY = -1;
|
|
3444
|
+
for (let x = 0; x < width; x += 1) {
|
|
3445
|
+
for (let y = 0; y < height; y += 1) {
|
|
3446
|
+
const value = data[y * width + x] ? 1 : 0;
|
|
3447
|
+
if (value) {
|
|
3448
|
+
minX = Math.min(minX, x);
|
|
3449
|
+
minY = Math.min(minY, y);
|
|
3450
|
+
maxX = Math.max(maxX, x);
|
|
3451
|
+
maxY = Math.max(maxY, y);
|
|
3452
|
+
}
|
|
3453
|
+
if (value === currentValue) {
|
|
3454
|
+
runLength += 1;
|
|
3455
|
+
}
|
|
3456
|
+
else {
|
|
3457
|
+
runs.push(runLength);
|
|
3458
|
+
currentValue = value;
|
|
3459
|
+
runLength = 1;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
runs.push(runLength);
|
|
3464
|
+
return {
|
|
3465
|
+
bounds: maxX < 0
|
|
3466
|
+
? null
|
|
3467
|
+
: {
|
|
3468
|
+
height: maxY - minY + 1,
|
|
3469
|
+
width: maxX - minX + 1,
|
|
3470
|
+
x: minX + (maxX - minX + 1) / 2,
|
|
3471
|
+
y: minY + (maxY - minY + 1) / 2,
|
|
3472
|
+
},
|
|
3473
|
+
mask: {
|
|
3474
|
+
counts: encodeCompressedRleCounts(runs),
|
|
3475
|
+
encoding: DetectionMaskEncoding.CompressedRle,
|
|
3476
|
+
height,
|
|
3477
|
+
width,
|
|
3478
|
+
},
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3481
|
+
function encodeDetectionMaskPayload(mask, codec) {
|
|
3482
|
+
return codec ? codec.deflate(mask.counts) : mask.counts;
|
|
3483
|
+
}
|
|
3484
|
+
function decodeDetectionMaskPayload(payload, width, height, options = {}) {
|
|
3485
|
+
const format = options.format ??
|
|
3486
|
+
(isDeflatedBase64DetectionMaskPayload(payload)
|
|
3487
|
+
? DetectionMaskPayloadFormat.DeflatedBase64
|
|
3488
|
+
: DetectionMaskPayloadFormat.RawCocoRle);
|
|
3489
|
+
if (format === DetectionMaskPayloadFormat.DeflatedBase64 && !options.codec) {
|
|
3490
|
+
throw new Error("A detection mask compression codec is required for deflated payloads.");
|
|
3491
|
+
}
|
|
3492
|
+
return {
|
|
3493
|
+
counts: format === DetectionMaskPayloadFormat.DeflatedBase64
|
|
3494
|
+
? options.codec.inflate(payload)
|
|
3495
|
+
: payload,
|
|
3496
|
+
encoding: DetectionMaskEncoding.CompressedRle,
|
|
3497
|
+
height,
|
|
3498
|
+
width,
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
/** Matches the annotation editor's legacy transport-format heuristic. */
|
|
3502
|
+
function isDeflatedBase64DetectionMaskPayload(value) {
|
|
3503
|
+
return value.length > 100 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
|
|
3504
|
+
}
|
|
3505
|
+
function computeMaskBounds(data, width, height) {
|
|
3506
|
+
assertMaskDimensions(data, width, height);
|
|
3507
|
+
let minX = width;
|
|
3508
|
+
let minY = height;
|
|
3509
|
+
let maxX = -1;
|
|
3510
|
+
let maxY = -1;
|
|
3511
|
+
for (let y = 0; y < height; y += 1) {
|
|
3512
|
+
for (let x = 0; x < width; x += 1) {
|
|
3513
|
+
if (!data[y * width + x]) {
|
|
3514
|
+
continue;
|
|
3515
|
+
}
|
|
3516
|
+
minX = Math.min(minX, x);
|
|
3517
|
+
minY = Math.min(minY, y);
|
|
3518
|
+
maxX = Math.max(maxX, x);
|
|
3519
|
+
maxY = Math.max(maxY, y);
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
if (maxX < 0) {
|
|
3523
|
+
return null;
|
|
3524
|
+
}
|
|
3525
|
+
const boundsWidth = maxX - minX + 1;
|
|
3526
|
+
const boundsHeight = maxY - minY + 1;
|
|
3527
|
+
return {
|
|
3528
|
+
height: boundsHeight,
|
|
3529
|
+
width: boundsWidth,
|
|
3530
|
+
x: minX + boundsWidth / 2,
|
|
3531
|
+
y: minY + boundsHeight / 2,
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
function computeDetectionMaskRect(mask) {
|
|
3535
|
+
const decoded = decodeCompressedRleMask(mask);
|
|
3536
|
+
const bounds = computeMaskBounds(decoded.data, decoded.width, decoded.height);
|
|
3537
|
+
return bounds ?? undefined;
|
|
3538
|
+
}
|
|
3539
|
+
function detectMaskBorders(data, width, height) {
|
|
3540
|
+
assertMaskDimensions(data, width, height);
|
|
3541
|
+
const borders = new Uint8Array(data.length);
|
|
3542
|
+
for (let y = 0; y < height; y += 1) {
|
|
3543
|
+
for (let x = 0; x < width; x += 1) {
|
|
3544
|
+
const offset = y * width + x;
|
|
3545
|
+
if (!data[offset]) {
|
|
3546
|
+
continue;
|
|
3547
|
+
}
|
|
3548
|
+
if (x === 0 ||
|
|
3549
|
+
y === 0 ||
|
|
3550
|
+
x === width - 1 ||
|
|
3551
|
+
y === height - 1 ||
|
|
3552
|
+
!data[offset - 1] ||
|
|
3553
|
+
!data[offset + 1] ||
|
|
3554
|
+
!data[offset - width] ||
|
|
3555
|
+
!data[offset + width]) {
|
|
3556
|
+
borders[offset] = 1;
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
return borders;
|
|
3561
|
+
}
|
|
3562
|
+
function extractMaskContour(data, width, height) {
|
|
3563
|
+
assertMaskDimensions(data, width, height);
|
|
3564
|
+
const stride = Math.max(1, Math.floor(height / 100));
|
|
3565
|
+
const leftEdge = [];
|
|
3566
|
+
const rightEdge = [];
|
|
3567
|
+
for (let y = 0; y < height; y += stride) {
|
|
3568
|
+
let left = -1;
|
|
3569
|
+
let right = -1;
|
|
3570
|
+
for (let x = 0; x < width; x += 1) {
|
|
3571
|
+
if (data[y * width + x]) {
|
|
3572
|
+
left = left === -1 ? x : left;
|
|
3573
|
+
right = x;
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
if (left !== -1) {
|
|
3577
|
+
leftEdge.push({ x: left, y });
|
|
3578
|
+
rightEdge.push({ x: right, y });
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
return leftEdge.length < 2
|
|
3582
|
+
? undefined
|
|
3583
|
+
: [...leftEdge, ...rightEdge.reverse()];
|
|
3584
|
+
}
|
|
3585
|
+
function extractMaskRectRuns(data, width, height) {
|
|
3586
|
+
assertMaskDimensions(data, width, height);
|
|
3587
|
+
const rects = [];
|
|
3588
|
+
const openRects = new Map();
|
|
3589
|
+
for (let y = 0; y < height; y += 1) {
|
|
3590
|
+
const activeSpans = new Set();
|
|
3591
|
+
let x = 0;
|
|
3592
|
+
while (x < width) {
|
|
3593
|
+
while (x < width && !data[y * width + x]) {
|
|
3594
|
+
x += 1;
|
|
3595
|
+
}
|
|
3596
|
+
if (x >= width) {
|
|
3597
|
+
break;
|
|
3598
|
+
}
|
|
3599
|
+
const startX = x;
|
|
3600
|
+
while (x < width && data[y * width + x]) {
|
|
3601
|
+
x += 1;
|
|
3602
|
+
}
|
|
3603
|
+
const runWidth = x - startX;
|
|
3604
|
+
const key = `${startX}:${runWidth}`;
|
|
3605
|
+
const openRect = openRects.get(key);
|
|
3606
|
+
activeSpans.add(key);
|
|
3607
|
+
if (openRect && openRect.y + openRect.height === y) {
|
|
3608
|
+
openRects.set(key, { ...openRect, height: openRect.height + 1 });
|
|
3609
|
+
}
|
|
3610
|
+
else {
|
|
3611
|
+
if (openRect) {
|
|
3612
|
+
rects.push(openRect);
|
|
3613
|
+
}
|
|
3614
|
+
openRects.set(key, { height: 1, width: runWidth, x: startX, y });
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
for (const [key, openRect] of openRects) {
|
|
3618
|
+
if (!activeSpans.has(key)) {
|
|
3619
|
+
rects.push(openRect);
|
|
3620
|
+
openRects.delete(key);
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
rects.push(...openRects.values());
|
|
3625
|
+
return rects.length > 0 ? rects : undefined;
|
|
3626
|
+
}
|
|
3627
|
+
function assertMaskDimensions(data, width, height) {
|
|
3628
|
+
if (!Number.isInteger(width) || width <= 0) {
|
|
3629
|
+
throw new Error("Mask width must be a positive integer.");
|
|
3630
|
+
}
|
|
3631
|
+
if (!Number.isInteger(height) || height <= 0) {
|
|
3632
|
+
throw new Error("Mask height must be a positive integer.");
|
|
3633
|
+
}
|
|
3634
|
+
if (data.length !== width * height) {
|
|
3635
|
+
throw new Error("Mask data length must equal width * height.");
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
function centerRectToTopLeftRect(rect) {
|
|
3640
|
+
return {
|
|
3641
|
+
height: rect.height,
|
|
3642
|
+
width: rect.width,
|
|
3643
|
+
x: rect.x - rect.width / 2,
|
|
3644
|
+
y: rect.y - rect.height / 2,
|
|
3645
|
+
};
|
|
3646
|
+
}
|
|
3647
|
+
function topLeftRectToCenterRect(rect) {
|
|
3648
|
+
return {
|
|
3649
|
+
height: rect.height,
|
|
3650
|
+
width: rect.width,
|
|
3651
|
+
x: rect.x + rect.width / 2,
|
|
3652
|
+
y: rect.y + rect.height / 2,
|
|
3653
|
+
};
|
|
3654
|
+
}
|
|
3655
|
+
function getPointsRect(points) {
|
|
3656
|
+
if (points.length === 0) {
|
|
3657
|
+
return undefined;
|
|
1696
3658
|
}
|
|
1697
3659
|
let minX = Number.POSITIVE_INFINITY;
|
|
1698
3660
|
let minY = Number.POSITIVE_INFINITY;
|
|
@@ -1773,6 +3735,216 @@ function rectArea(rect) {
|
|
|
1773
3735
|
return rect ? Math.max(0, rect.width) * Math.max(0, rect.height) : 0;
|
|
1774
3736
|
}
|
|
1775
3737
|
|
|
3738
|
+
const DEFAULT_LOST_TRACK_BUFFER = 30;
|
|
3739
|
+
const DEFAULT_FRAME_RATE = 30;
|
|
3740
|
+
const DEFAULT_TRACK_ACTIVATION_THRESHOLD = 0.25;
|
|
3741
|
+
const DEFAULT_MINIMUM_CONSECUTIVE_FRAMES = 3;
|
|
3742
|
+
const DEFAULT_MINIMUM_IOU_THRESHOLD = 0.3;
|
|
3743
|
+
const DEFAULT_BYTE_TRACK_ACTIVATION_THRESHOLD = 0.7;
|
|
3744
|
+
const DEFAULT_BYTE_MINIMUM_CONSECUTIVE_FRAMES = 2;
|
|
3745
|
+
const DEFAULT_BYTE_MINIMUM_IOU_THRESHOLD = 0.1;
|
|
3746
|
+
const DEFAULT_HIGH_CONFIDENCE_DETECTION_THRESHOLD = 0.6;
|
|
3747
|
+
const DEFAULT_CBIOU_FIRST_IOU_THRESHOLD = 0.2;
|
|
3748
|
+
const DEFAULT_CBIOU_SECOND_IOU_THRESHOLD = 0.5;
|
|
3749
|
+
const DEFAULT_CBIOU_UNCONFIRMED_IOU_THRESHOLD = 0.3;
|
|
3750
|
+
const DEFAULT_CBIOU_FIRST_BUFFER_RATIO = 0.3;
|
|
3751
|
+
const DEFAULT_CBIOU_SECOND_BUFFER_RATIO = 0.5;
|
|
3752
|
+
const DEFAULT_OCSORT_DIRECTION_CONSISTENCY_WEIGHT = 0.2;
|
|
3753
|
+
const DEFAULT_OCSORT_DELTA_T = 3;
|
|
3754
|
+
const detectionPostProcessors = {
|
|
3755
|
+
tracking: ((options = {}) => {
|
|
3756
|
+
const isByteTrack = options.algorithm === "bytetrack";
|
|
3757
|
+
const isCBIoU = options.algorithm === "cbiou";
|
|
3758
|
+
const isOCSort = options.algorithm === "ocsort";
|
|
3759
|
+
const lostTrackBuffer = normalizeNonNegativeInteger(options.lostTrackBuffer, DEFAULT_LOST_TRACK_BUFFER, "lostTrackBuffer");
|
|
3760
|
+
const frameRate = options.frameRate ?? DEFAULT_FRAME_RATE;
|
|
3761
|
+
const trackActivationThreshold = options.trackActivationThreshold ??
|
|
3762
|
+
(isByteTrack || isCBIoU
|
|
3763
|
+
? DEFAULT_BYTE_TRACK_ACTIVATION_THRESHOLD
|
|
3764
|
+
: DEFAULT_TRACK_ACTIVATION_THRESHOLD);
|
|
3765
|
+
const minimumConsecutiveFrames = normalizePositiveInteger(options.minimumConsecutiveFrames, isByteTrack || isCBIoU
|
|
3766
|
+
? DEFAULT_BYTE_MINIMUM_CONSECUTIVE_FRAMES
|
|
3767
|
+
: DEFAULT_MINIMUM_CONSECUTIVE_FRAMES, "minimumConsecutiveFrames");
|
|
3768
|
+
const minimumIouThreshold = options.minimumIouThreshold ??
|
|
3769
|
+
(isByteTrack
|
|
3770
|
+
? DEFAULT_BYTE_MINIMUM_IOU_THRESHOLD
|
|
3771
|
+
: DEFAULT_MINIMUM_IOU_THRESHOLD);
|
|
3772
|
+
if (!Number.isFinite(frameRate) || frameRate <= 0) {
|
|
3773
|
+
throw new Error("frameRate must be a finite positive value.");
|
|
3774
|
+
}
|
|
3775
|
+
normalizeUnitInterval(trackActivationThreshold, "trackActivationThreshold");
|
|
3776
|
+
normalizeUnitInterval(minimumIouThreshold, "minimumIouThreshold");
|
|
3777
|
+
const base = {
|
|
3778
|
+
geometry: options.geometry ?? TrackingGeometry.Box,
|
|
3779
|
+
kind: "tracking",
|
|
3780
|
+
};
|
|
3781
|
+
if (isCBIoU) {
|
|
3782
|
+
const highConfidenceDetectionThreshold = options.highConfidenceDetectionThreshold ??
|
|
3783
|
+
DEFAULT_HIGH_CONFIDENCE_DETECTION_THRESHOLD;
|
|
3784
|
+
const minimumIouThresholdFirstAssociation = options.minimumIouThresholdFirstAssociation ??
|
|
3785
|
+
DEFAULT_CBIOU_FIRST_IOU_THRESHOLD;
|
|
3786
|
+
const minimumIouThresholdSecondAssociation = options.minimumIouThresholdSecondAssociation ??
|
|
3787
|
+
DEFAULT_CBIOU_SECOND_IOU_THRESHOLD;
|
|
3788
|
+
const minimumIouThresholdUnconfirmedAssociation = options.minimumIouThresholdUnconfirmedAssociation ??
|
|
3789
|
+
DEFAULT_CBIOU_UNCONFIRMED_IOU_THRESHOLD;
|
|
3790
|
+
const bufferRatioFirst = options.bufferRatioFirst ?? DEFAULT_CBIOU_FIRST_BUFFER_RATIO;
|
|
3791
|
+
const bufferRatioSecond = options.bufferRatioSecond ?? DEFAULT_CBIOU_SECOND_BUFFER_RATIO;
|
|
3792
|
+
normalizeUnitInterval(highConfidenceDetectionThreshold, "highConfidenceDetectionThreshold");
|
|
3793
|
+
normalizeUnitInterval(minimumIouThresholdFirstAssociation, "minimumIouThresholdFirstAssociation");
|
|
3794
|
+
normalizeUnitInterval(minimumIouThresholdSecondAssociation, "minimumIouThresholdSecondAssociation");
|
|
3795
|
+
normalizeUnitInterval(minimumIouThresholdUnconfirmedAssociation, "minimumIouThresholdUnconfirmedAssociation");
|
|
3796
|
+
normalizeNonNegativeFinite(bufferRatioFirst, "bufferRatioFirst");
|
|
3797
|
+
normalizeNonNegativeFinite(bufferRatioSecond, "bufferRatioSecond");
|
|
3798
|
+
return {
|
|
3799
|
+
...base,
|
|
3800
|
+
algorithm: "cbiou",
|
|
3801
|
+
options: {
|
|
3802
|
+
bufferRatioFirst,
|
|
3803
|
+
bufferRatioSecond,
|
|
3804
|
+
frameRate,
|
|
3805
|
+
highConfidenceDetectionThreshold,
|
|
3806
|
+
instantFirstFrameActivation: options.instantFirstFrameActivation ?? true,
|
|
3807
|
+
lostTrackBuffer,
|
|
3808
|
+
minimumConsecutiveFrames,
|
|
3809
|
+
minimumIouThresholdFirstAssociation,
|
|
3810
|
+
minimumIouThresholdSecondAssociation,
|
|
3811
|
+
minimumIouThresholdUnconfirmedAssociation,
|
|
3812
|
+
trackActivationThreshold,
|
|
3813
|
+
},
|
|
3814
|
+
};
|
|
3815
|
+
}
|
|
3816
|
+
if (isOCSort) {
|
|
3817
|
+
const directionConsistencyWeight = options.directionConsistencyWeight ??
|
|
3818
|
+
DEFAULT_OCSORT_DIRECTION_CONSISTENCY_WEIGHT;
|
|
3819
|
+
const highConfidenceDetectionThreshold = options.highConfidenceDetectionThreshold ??
|
|
3820
|
+
DEFAULT_HIGH_CONFIDENCE_DETECTION_THRESHOLD;
|
|
3821
|
+
const deltaT = normalizePositiveInteger(options.deltaT, DEFAULT_OCSORT_DELTA_T, "deltaT");
|
|
3822
|
+
normalizeUnitInterval(directionConsistencyWeight, "directionConsistencyWeight");
|
|
3823
|
+
normalizeUnitInterval(highConfidenceDetectionThreshold, "highConfidenceDetectionThreshold");
|
|
3824
|
+
return {
|
|
3825
|
+
...base,
|
|
3826
|
+
algorithm: "ocsort",
|
|
3827
|
+
options: {
|
|
3828
|
+
deltaT,
|
|
3829
|
+
directionConsistencyWeight,
|
|
3830
|
+
frameRate,
|
|
3831
|
+
highConfidenceDetectionThreshold,
|
|
3832
|
+
lostTrackBuffer,
|
|
3833
|
+
minimumConsecutiveFrames,
|
|
3834
|
+
minimumIouThreshold,
|
|
3835
|
+
},
|
|
3836
|
+
};
|
|
3837
|
+
}
|
|
3838
|
+
if (isByteTrack) {
|
|
3839
|
+
const highConfidenceDetectionThreshold = options.highConfidenceDetectionThreshold ??
|
|
3840
|
+
DEFAULT_HIGH_CONFIDENCE_DETECTION_THRESHOLD;
|
|
3841
|
+
normalizeUnitInterval(highConfidenceDetectionThreshold, "highConfidenceDetectionThreshold");
|
|
3842
|
+
return {
|
|
3843
|
+
...base,
|
|
3844
|
+
algorithm: "bytetrack",
|
|
3845
|
+
options: {
|
|
3846
|
+
frameRate,
|
|
3847
|
+
highConfidenceDetectionThreshold,
|
|
3848
|
+
lostTrackBuffer,
|
|
3849
|
+
minimumConsecutiveFrames,
|
|
3850
|
+
minimumIouThreshold,
|
|
3851
|
+
trackActivationThreshold,
|
|
3852
|
+
},
|
|
3853
|
+
};
|
|
3854
|
+
}
|
|
3855
|
+
return {
|
|
3856
|
+
...base,
|
|
3857
|
+
algorithm: "sort",
|
|
3858
|
+
options: {
|
|
3859
|
+
frameRate,
|
|
3860
|
+
lostTrackBuffer,
|
|
3861
|
+
minimumConsecutiveFrames,
|
|
3862
|
+
minimumIouThreshold,
|
|
3863
|
+
trackActivationThreshold,
|
|
3864
|
+
},
|
|
3865
|
+
};
|
|
3866
|
+
}),
|
|
3867
|
+
};
|
|
3868
|
+
/**
|
|
3869
|
+
* Creates the lightweight projection sent to a tracker or tracking worker.
|
|
3870
|
+
* Masks and keypoint arrays never cross the worker boundary.
|
|
3871
|
+
*/
|
|
3872
|
+
function projectDetectionFrameForTracking(frame, geometry) {
|
|
3873
|
+
return frame.detections.flatMap((detection, detectionIndex) => {
|
|
3874
|
+
const rect = resolveTrackingRect(detection, geometry);
|
|
3875
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
3876
|
+
return [];
|
|
3877
|
+
}
|
|
3878
|
+
return [
|
|
3879
|
+
{
|
|
3880
|
+
...(detection.confidence === undefined
|
|
3881
|
+
? {}
|
|
3882
|
+
: { confidence: detection.confidence }),
|
|
3883
|
+
detectionIndex,
|
|
3884
|
+
rect,
|
|
3885
|
+
},
|
|
3886
|
+
];
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
function resolveTrackingRect(detection, geometry) {
|
|
3890
|
+
switch (geometry) {
|
|
3891
|
+
case TrackingGeometry.Box:
|
|
3892
|
+
return detection.rect;
|
|
3893
|
+
case TrackingGeometry.Mask:
|
|
3894
|
+
if (!detection.mask) {
|
|
3895
|
+
return undefined;
|
|
3896
|
+
}
|
|
3897
|
+
// Most segmentation producers already supply the exact mask bounds as a
|
|
3898
|
+
// rect. Decode RLE only when that inexpensive projection is absent.
|
|
3899
|
+
return detection.rect ?? computeDetectionMaskRect(detection.mask);
|
|
3900
|
+
case TrackingGeometry.Keypoints:
|
|
3901
|
+
return detection.keypoints
|
|
3902
|
+
? getPointsRect(detection.keypoints.points)
|
|
3903
|
+
: undefined;
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
function normalizePositiveInteger(value, fallback, label) {
|
|
3907
|
+
const resolved = value ?? fallback;
|
|
3908
|
+
if (!Number.isInteger(resolved) || resolved < 1) {
|
|
3909
|
+
throw new Error(`${label} must be a positive integer.`);
|
|
3910
|
+
}
|
|
3911
|
+
return resolved;
|
|
3912
|
+
}
|
|
3913
|
+
function normalizeNonNegativeInteger(value, fallback, label) {
|
|
3914
|
+
const resolved = value ?? fallback;
|
|
3915
|
+
if (!Number.isInteger(resolved) || resolved < 0) {
|
|
3916
|
+
throw new Error(`${label} must be a non-negative integer.`);
|
|
3917
|
+
}
|
|
3918
|
+
return resolved;
|
|
3919
|
+
}
|
|
3920
|
+
function normalizeUnitInterval(value, label) {
|
|
3921
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
3922
|
+
throw new Error(`${label} must be between 0 and 1.`);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
function normalizeNonNegativeFinite(value, label) {
|
|
3926
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
3927
|
+
throw new Error(`${label} must be a finite non-negative value.`);
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
var DetectionPickTarget;
|
|
3932
|
+
(function (DetectionPickTarget) {
|
|
3933
|
+
DetectionPickTarget["Box"] = "box";
|
|
3934
|
+
DetectionPickTarget["Edge"] = "edge";
|
|
3935
|
+
DetectionPickTarget["Keypoint"] = "keypoint";
|
|
3936
|
+
DetectionPickTarget["Label"] = "label";
|
|
3937
|
+
DetectionPickTarget["Mask"] = "mask";
|
|
3938
|
+
DetectionPickTarget["Polygon"] = "polygon";
|
|
3939
|
+
DetectionPickTarget["Polyline"] = "polyline";
|
|
3940
|
+
})(DetectionPickTarget || (DetectionPickTarget = {}));
|
|
3941
|
+
var MediaInteractionMode;
|
|
3942
|
+
(function (MediaInteractionMode) {
|
|
3943
|
+
MediaInteractionMode["Always"] = "always";
|
|
3944
|
+
MediaInteractionMode["Disabled"] = "disabled";
|
|
3945
|
+
MediaInteractionMode["PausedOnly"] = "pausedOnly";
|
|
3946
|
+
})(MediaInteractionMode || (MediaInteractionMode = {}));
|
|
3947
|
+
|
|
1776
3948
|
const decodedMaskCache = new WeakMap();
|
|
1777
3949
|
function pickDetectionAtPoint(frame, point, options = {}) {
|
|
1778
3950
|
if (!frame) {
|
|
@@ -4284,242 +6456,6 @@ function normalizeGlobalKeypointStyle(style) {
|
|
|
4284
6456
|
return style === undefined ? new BaseKeypointStyle() : style;
|
|
4285
6457
|
}
|
|
4286
6458
|
|
|
4287
|
-
var DetectionMaskPayloadFormat;
|
|
4288
|
-
(function (DetectionMaskPayloadFormat) {
|
|
4289
|
-
DetectionMaskPayloadFormat["RawCocoRle"] = "rawCocoRle";
|
|
4290
|
-
DetectionMaskPayloadFormat["DeflatedBase64"] = "deflatedBase64";
|
|
4291
|
-
})(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));
|
|
4292
|
-
function encodeBinaryMask(data, width, height) {
|
|
4293
|
-
assertMaskDimensions(data, width, height);
|
|
4294
|
-
const runs = [];
|
|
4295
|
-
let currentValue = 0;
|
|
4296
|
-
let runLength = 0;
|
|
4297
|
-
for (let x = 0; x < width; x += 1) {
|
|
4298
|
-
for (let y = 0; y < height; y += 1) {
|
|
4299
|
-
const value = data[y * width + x] ? 1 : 0;
|
|
4300
|
-
if (value === currentValue) {
|
|
4301
|
-
runLength += 1;
|
|
4302
|
-
}
|
|
4303
|
-
else {
|
|
4304
|
-
runs.push(runLength);
|
|
4305
|
-
currentValue = value;
|
|
4306
|
-
runLength = 1;
|
|
4307
|
-
}
|
|
4308
|
-
}
|
|
4309
|
-
}
|
|
4310
|
-
runs.push(runLength);
|
|
4311
|
-
return {
|
|
4312
|
-
counts: encodeCompressedRleCounts(runs),
|
|
4313
|
-
encoding: DetectionMaskEncoding.CompressedRle,
|
|
4314
|
-
height,
|
|
4315
|
-
width,
|
|
4316
|
-
};
|
|
4317
|
-
}
|
|
4318
|
-
/** Encodes a binary mask and derives its bounds in the same raster traversal. */
|
|
4319
|
-
function encodeBinaryMaskWithBounds(data, width, height) {
|
|
4320
|
-
assertMaskDimensions(data, width, height);
|
|
4321
|
-
const runs = [];
|
|
4322
|
-
let currentValue = 0;
|
|
4323
|
-
let runLength = 0;
|
|
4324
|
-
let minX = width;
|
|
4325
|
-
let minY = height;
|
|
4326
|
-
let maxX = -1;
|
|
4327
|
-
let maxY = -1;
|
|
4328
|
-
for (let x = 0; x < width; x += 1) {
|
|
4329
|
-
for (let y = 0; y < height; y += 1) {
|
|
4330
|
-
const value = data[y * width + x] ? 1 : 0;
|
|
4331
|
-
if (value) {
|
|
4332
|
-
minX = Math.min(minX, x);
|
|
4333
|
-
minY = Math.min(minY, y);
|
|
4334
|
-
maxX = Math.max(maxX, x);
|
|
4335
|
-
maxY = Math.max(maxY, y);
|
|
4336
|
-
}
|
|
4337
|
-
if (value === currentValue) {
|
|
4338
|
-
runLength += 1;
|
|
4339
|
-
}
|
|
4340
|
-
else {
|
|
4341
|
-
runs.push(runLength);
|
|
4342
|
-
currentValue = value;
|
|
4343
|
-
runLength = 1;
|
|
4344
|
-
}
|
|
4345
|
-
}
|
|
4346
|
-
}
|
|
4347
|
-
runs.push(runLength);
|
|
4348
|
-
return {
|
|
4349
|
-
bounds: maxX < 0
|
|
4350
|
-
? null
|
|
4351
|
-
: {
|
|
4352
|
-
height: maxY - minY + 1,
|
|
4353
|
-
width: maxX - minX + 1,
|
|
4354
|
-
x: minX + (maxX - minX + 1) / 2,
|
|
4355
|
-
y: minY + (maxY - minY + 1) / 2,
|
|
4356
|
-
},
|
|
4357
|
-
mask: {
|
|
4358
|
-
counts: encodeCompressedRleCounts(runs),
|
|
4359
|
-
encoding: DetectionMaskEncoding.CompressedRle,
|
|
4360
|
-
height,
|
|
4361
|
-
width,
|
|
4362
|
-
},
|
|
4363
|
-
};
|
|
4364
|
-
}
|
|
4365
|
-
function encodeDetectionMaskPayload(mask, codec) {
|
|
4366
|
-
return codec ? codec.deflate(mask.counts) : mask.counts;
|
|
4367
|
-
}
|
|
4368
|
-
function decodeDetectionMaskPayload(payload, width, height, options = {}) {
|
|
4369
|
-
const format = options.format ??
|
|
4370
|
-
(isDeflatedBase64DetectionMaskPayload(payload)
|
|
4371
|
-
? DetectionMaskPayloadFormat.DeflatedBase64
|
|
4372
|
-
: DetectionMaskPayloadFormat.RawCocoRle);
|
|
4373
|
-
if (format === DetectionMaskPayloadFormat.DeflatedBase64 && !options.codec) {
|
|
4374
|
-
throw new Error("A detection mask compression codec is required for deflated payloads.");
|
|
4375
|
-
}
|
|
4376
|
-
return {
|
|
4377
|
-
counts: format === DetectionMaskPayloadFormat.DeflatedBase64
|
|
4378
|
-
? options.codec.inflate(payload)
|
|
4379
|
-
: payload,
|
|
4380
|
-
encoding: DetectionMaskEncoding.CompressedRle,
|
|
4381
|
-
height,
|
|
4382
|
-
width,
|
|
4383
|
-
};
|
|
4384
|
-
}
|
|
4385
|
-
/** Matches the annotation editor's legacy transport-format heuristic. */
|
|
4386
|
-
function isDeflatedBase64DetectionMaskPayload(value) {
|
|
4387
|
-
return value.length > 100 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
|
|
4388
|
-
}
|
|
4389
|
-
function computeMaskBounds(data, width, height) {
|
|
4390
|
-
assertMaskDimensions(data, width, height);
|
|
4391
|
-
let minX = width;
|
|
4392
|
-
let minY = height;
|
|
4393
|
-
let maxX = -1;
|
|
4394
|
-
let maxY = -1;
|
|
4395
|
-
for (let y = 0; y < height; y += 1) {
|
|
4396
|
-
for (let x = 0; x < width; x += 1) {
|
|
4397
|
-
if (!data[y * width + x]) {
|
|
4398
|
-
continue;
|
|
4399
|
-
}
|
|
4400
|
-
minX = Math.min(minX, x);
|
|
4401
|
-
minY = Math.min(minY, y);
|
|
4402
|
-
maxX = Math.max(maxX, x);
|
|
4403
|
-
maxY = Math.max(maxY, y);
|
|
4404
|
-
}
|
|
4405
|
-
}
|
|
4406
|
-
if (maxX < 0) {
|
|
4407
|
-
return null;
|
|
4408
|
-
}
|
|
4409
|
-
const boundsWidth = maxX - minX + 1;
|
|
4410
|
-
const boundsHeight = maxY - minY + 1;
|
|
4411
|
-
return {
|
|
4412
|
-
height: boundsHeight,
|
|
4413
|
-
width: boundsWidth,
|
|
4414
|
-
x: minX + boundsWidth / 2,
|
|
4415
|
-
y: minY + boundsHeight / 2,
|
|
4416
|
-
};
|
|
4417
|
-
}
|
|
4418
|
-
function computeDetectionMaskRect(mask) {
|
|
4419
|
-
const decoded = decodeCompressedRleMask(mask);
|
|
4420
|
-
const bounds = computeMaskBounds(decoded.data, decoded.width, decoded.height);
|
|
4421
|
-
return bounds ?? undefined;
|
|
4422
|
-
}
|
|
4423
|
-
function detectMaskBorders(data, width, height) {
|
|
4424
|
-
assertMaskDimensions(data, width, height);
|
|
4425
|
-
const borders = new Uint8Array(data.length);
|
|
4426
|
-
for (let y = 0; y < height; y += 1) {
|
|
4427
|
-
for (let x = 0; x < width; x += 1) {
|
|
4428
|
-
const offset = y * width + x;
|
|
4429
|
-
if (!data[offset]) {
|
|
4430
|
-
continue;
|
|
4431
|
-
}
|
|
4432
|
-
if (x === 0 ||
|
|
4433
|
-
y === 0 ||
|
|
4434
|
-
x === width - 1 ||
|
|
4435
|
-
y === height - 1 ||
|
|
4436
|
-
!data[offset - 1] ||
|
|
4437
|
-
!data[offset + 1] ||
|
|
4438
|
-
!data[offset - width] ||
|
|
4439
|
-
!data[offset + width]) {
|
|
4440
|
-
borders[offset] = 1;
|
|
4441
|
-
}
|
|
4442
|
-
}
|
|
4443
|
-
}
|
|
4444
|
-
return borders;
|
|
4445
|
-
}
|
|
4446
|
-
function extractMaskContour(data, width, height) {
|
|
4447
|
-
assertMaskDimensions(data, width, height);
|
|
4448
|
-
const stride = Math.max(1, Math.floor(height / 100));
|
|
4449
|
-
const leftEdge = [];
|
|
4450
|
-
const rightEdge = [];
|
|
4451
|
-
for (let y = 0; y < height; y += stride) {
|
|
4452
|
-
let left = -1;
|
|
4453
|
-
let right = -1;
|
|
4454
|
-
for (let x = 0; x < width; x += 1) {
|
|
4455
|
-
if (data[y * width + x]) {
|
|
4456
|
-
left = left === -1 ? x : left;
|
|
4457
|
-
right = x;
|
|
4458
|
-
}
|
|
4459
|
-
}
|
|
4460
|
-
if (left !== -1) {
|
|
4461
|
-
leftEdge.push({ x: left, y });
|
|
4462
|
-
rightEdge.push({ x: right, y });
|
|
4463
|
-
}
|
|
4464
|
-
}
|
|
4465
|
-
return leftEdge.length < 2
|
|
4466
|
-
? undefined
|
|
4467
|
-
: [...leftEdge, ...rightEdge.reverse()];
|
|
4468
|
-
}
|
|
4469
|
-
function extractMaskRectRuns(data, width, height) {
|
|
4470
|
-
assertMaskDimensions(data, width, height);
|
|
4471
|
-
const rects = [];
|
|
4472
|
-
const openRects = new Map();
|
|
4473
|
-
for (let y = 0; y < height; y += 1) {
|
|
4474
|
-
const activeSpans = new Set();
|
|
4475
|
-
let x = 0;
|
|
4476
|
-
while (x < width) {
|
|
4477
|
-
while (x < width && !data[y * width + x]) {
|
|
4478
|
-
x += 1;
|
|
4479
|
-
}
|
|
4480
|
-
if (x >= width) {
|
|
4481
|
-
break;
|
|
4482
|
-
}
|
|
4483
|
-
const startX = x;
|
|
4484
|
-
while (x < width && data[y * width + x]) {
|
|
4485
|
-
x += 1;
|
|
4486
|
-
}
|
|
4487
|
-
const runWidth = x - startX;
|
|
4488
|
-
const key = `${startX}:${runWidth}`;
|
|
4489
|
-
const openRect = openRects.get(key);
|
|
4490
|
-
activeSpans.add(key);
|
|
4491
|
-
if (openRect && openRect.y + openRect.height === y) {
|
|
4492
|
-
openRects.set(key, { ...openRect, height: openRect.height + 1 });
|
|
4493
|
-
}
|
|
4494
|
-
else {
|
|
4495
|
-
if (openRect) {
|
|
4496
|
-
rects.push(openRect);
|
|
4497
|
-
}
|
|
4498
|
-
openRects.set(key, { height: 1, width: runWidth, x: startX, y });
|
|
4499
|
-
}
|
|
4500
|
-
}
|
|
4501
|
-
for (const [key, openRect] of openRects) {
|
|
4502
|
-
if (!activeSpans.has(key)) {
|
|
4503
|
-
rects.push(openRect);
|
|
4504
|
-
openRects.delete(key);
|
|
4505
|
-
}
|
|
4506
|
-
}
|
|
4507
|
-
}
|
|
4508
|
-
rects.push(...openRects.values());
|
|
4509
|
-
return rects.length > 0 ? rects : undefined;
|
|
4510
|
-
}
|
|
4511
|
-
function assertMaskDimensions(data, width, height) {
|
|
4512
|
-
if (!Number.isInteger(width) || width <= 0) {
|
|
4513
|
-
throw new Error("Mask width must be a positive integer.");
|
|
4514
|
-
}
|
|
4515
|
-
if (!Number.isInteger(height) || height <= 0) {
|
|
4516
|
-
throw new Error("Mask height must be a positive integer.");
|
|
4517
|
-
}
|
|
4518
|
-
if (data.length !== width * height) {
|
|
4519
|
-
throw new Error("Mask data length must equal width * height.");
|
|
4520
|
-
}
|
|
4521
|
-
}
|
|
4522
|
-
|
|
4523
6459
|
function rectToPolygon(rect) {
|
|
4524
6460
|
const halfWidth = rect.width / 2;
|
|
4525
6461
|
const halfHeight = rect.height / 2;
|
|
@@ -4992,6 +6928,30 @@ var MediaRendererPlaybackState;
|
|
|
4992
6928
|
MediaRendererPlaybackState["Error"] = "error";
|
|
4993
6929
|
MediaRendererPlaybackState["Destroyed"] = "destroyed";
|
|
4994
6930
|
})(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));
|
|
6931
|
+
/**
|
|
6932
|
+
* Stable classification of a media failure.
|
|
6933
|
+
*
|
|
6934
|
+
* Applications branch on these kinds instead of parsing decoder, demuxer, or
|
|
6935
|
+
* container error text, and still own their localized user-facing copy. New
|
|
6936
|
+
* kinds may be added over time, so treat unrecognized values like `Unknown`.
|
|
6937
|
+
*/
|
|
6938
|
+
var MediaErrorKind;
|
|
6939
|
+
(function (MediaErrorKind) {
|
|
6940
|
+
/** The media could not be opened or read at all. */
|
|
6941
|
+
MediaErrorKind["Unreadable"] = "unreadable";
|
|
6942
|
+
/** The container or codec is not supported by this platform. */
|
|
6943
|
+
MediaErrorKind["UnsupportedFormat"] = "unsupportedFormat";
|
|
6944
|
+
/** The media opened but carries no usable video track. */
|
|
6945
|
+
MediaErrorKind["NoVideoTrack"] = "noVideoTrack";
|
|
6946
|
+
/** Decoding a sample failed after the source opened. */
|
|
6947
|
+
MediaErrorKind["Decode"] = "decode";
|
|
6948
|
+
/** The media could not be fetched. */
|
|
6949
|
+
MediaErrorKind["Network"] = "network";
|
|
6950
|
+
/** The host environment lacks the APIs this media source requires. */
|
|
6951
|
+
MediaErrorKind["EnvironmentUnsupported"] = "environmentUnsupported";
|
|
6952
|
+
/** The failure could not be classified. */
|
|
6953
|
+
MediaErrorKind["Unknown"] = "unknown";
|
|
6954
|
+
})(MediaErrorKind || (MediaErrorKind = {}));
|
|
4995
6955
|
/**
|
|
4996
6956
|
* Lower-level media source readiness.
|
|
4997
6957
|
*/
|
|
@@ -5060,5 +7020,5 @@ var MediaSessionActivityStatus;
|
|
|
5060
7020
|
MediaSessionActivityStatus["Waiting"] = "waiting";
|
|
5061
7021
|
})(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));
|
|
5062
7022
|
|
|
5063
|
-
export { AnnotationFrameMutationKind, AnnotationGeometryKind, AnnotationGestureStateKind, AnnotationHandleKind, BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, StrokeAlignment as 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, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, RegionRendererComposeMode, RegionRendererRegionKind, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, ShapeInstructionKind, StrokeAlignment, annotationRendererKinds, annotationRenderers, 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, followDetectionPickAcrossFrames, getAnnotationHandles, getBufferedDetectionTimelineFrameSnapshot, getDetectionRect, getPointsRect, haveSameDetectionPickIdentities, includeDefined, isDeflatedBase64DetectionMaskPayload, lightenColor, mediaToScreen, mergeDetectionMasks, mergeDetectionPolygonsByClass, normalizeDetectionClassName, offsetDetection, pickAnnotationHandle, pickDetectionAtPoint, pickDetectionByMaskId, pointInPolygon, polygonArea, polygonToRect, rasterizePolygonToMask, rasterizeRectToMask, rebaseDetectionPickToFrame, rectArea, rectToPolygon, resolveAnnotationRendererPresentation, resolveAnnotationStyleState, resolveContrastTextColor, resolveDetectionClassColorStyle, resolveEllipseSegmentCount, resolveMarkerGeometry, resolveMaskStyleOpacity, resolveStyleValue, sampleEllipseArc, screenToMedia, selectDetectionFrame, topLeftRectToCenterRect, validateDetectionFrames };
|
|
7023
|
+
export { AnnotationFrameMutationKind, AnnotationGeometryKind, AnnotationGestureStateKind, AnnotationHandleKind, BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, StrokeAlignment as 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, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaErrorKind, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, RegionRendererComposeMode, RegionRendererRegionKind, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, ShapeInstructionKind, StrokeAlignment, TrackingGeometry, annotationRendererKinds, annotationRenderers, applyAnnotationHandleDrag, canReuseMaskStyleArtifacts, centerRectToTopLeftRect, computeDetectionMaskRect, computeMaskBounds, containsPoint, convertDetectionBoxToMask, convertDetectionBoxToPolygon, convertDetectionMaskToBox, convertDetectionMaskToPolygon, convertDetectionPolygonToBox, convertDetectionPolygonToMask, copySortedDetectionFrames, createAnnotationEditingEngine, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createByteTrackTracker, createCBIoUTracker, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createDetectionPickKey, createEditableAnnotationFrameSession, createIdMaskFrame, createIdleDetectionBufferState, createMemoryColdDetectionFrameStore, createOCSortTracker, createProjectedDetectionFrameSource, createSortTracker, createSourceAwarePresentation, createViewportController, createWritableDetectionFrameSource, decodeCompressedRleCounts, decodeCompressedRleMask, decodeDetectionMaskPayload, deleteAnnotationVertex, detectMaskBorders, detectionFrameOverlapsRange, detectionPostProcessors, distanceToSegment, encodeBinaryMask, encodeBinaryMaskWithBounds, encodeCompressedRleCounts, encodeDetectionMaskPayload, extractMaskContour, extractMaskRectRuns, filterDetectionFramesForRange, findClosestAnnotationSegment, followDetectionPickAcrossFrames, getAnnotationHandles, getBufferedDetectionTimelineFrameSnapshot, getDetectionRect, getPointsRect, haveSameDetectionPickIdentities, includeDefined, isDeflatedBase64DetectionMaskPayload, lightenColor, mediaToScreen, mergeDetectionMasks, mergeDetectionPolygonsByClass, normalizeDetectionClassName, offsetDetection, pickAnnotationHandle, pickDetectionAtPoint, pickDetectionByMaskId, pointInPolygon, polygonArea, polygonToRect, projectDetectionFrame, projectDetectionFrameForTracking, projectDetectionFrames, rasterizePolygonToMask, rasterizeRectToMask, rebaseDetectionPickToFrame, rectArea, rectToPolygon, resolveAnnotationRendererPresentation, resolveAnnotationStyleState, resolveContrastTextColor, resolveDetectionClassColorStyle, resolveEllipseSegmentCount, resolveMarkerGeometry, resolveMaskStyleOpacity, resolveStyleValue, sampleEllipseArc, screenToMedia, selectDetectionFrame, topLeftRectToCenterRect, validateDetectionFrames };
|
|
5064
7024
|
//# sourceMappingURL=index.js.map
|