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.
Files changed (71) hide show
  1. package/dist/index.d.ts +12 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +864 -127
  4. package/dist/index.js.map +1 -1
  5. package/dist/mask-preparation.worker.js +91 -59
  6. package/dist/mask-preparation.worker.js.map +1 -1
  7. package/dist/media/media-errors.d.ts +32 -0
  8. package/dist/media/media-errors.d.ts.map +1 -0
  9. package/dist/media/media-source-state.d.ts.map +1 -1
  10. package/dist/media/media-stream-media-source.d.ts +31 -0
  11. package/dist/media/media-stream-media-source.d.ts.map +1 -1
  12. package/dist/media/mediabunny-media-source.d.ts.map +1 -1
  13. package/dist/media/presentation-timeline-media-source.d.ts +17 -0
  14. package/dist/media/presentation-timeline-media-source.d.ts.map +1 -0
  15. package/dist/media/static-image-media-source.d.ts.map +1 -1
  16. package/dist/post-processing/default-tracking-worker.d.ts +3 -0
  17. package/dist/post-processing/default-tracking-worker.d.ts.map +1 -0
  18. package/dist/post-processing/detection-post-processing-pipeline.d.ts +7 -0
  19. package/dist/post-processing/detection-post-processing-pipeline.d.ts.map +1 -0
  20. package/dist/post-processing/embedded-tracking-worker.d.ts +2 -0
  21. package/dist/post-processing/embedded-tracking-worker.d.ts.map +1 -0
  22. package/dist/post-processing/tracking-worker-protocol.d.ts +29 -0
  23. package/dist/post-processing/tracking-worker-protocol.d.ts.map +1 -0
  24. package/dist/post-processing/tracking.worker.d.ts +2 -0
  25. package/dist/post-processing/tracking.worker.d.ts.map +1 -0
  26. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  27. package/dist/renderers/media-renderer-state.d.ts.map +1 -1
  28. package/dist/sessions/media-session-detections.d.ts.map +1 -1
  29. package/dist/sessions/media-session.d.ts +2 -2
  30. package/dist/sessions/media-session.d.ts.map +1 -1
  31. package/dist/tracking.worker.js +1509 -0
  32. package/dist/tracking.worker.js.map +1 -0
  33. package/dist/types/detection-post-processing.d.ts +62 -0
  34. package/dist/types/detection-post-processing.d.ts.map +1 -0
  35. package/dist/types/media-session.d.ts +57 -1
  36. package/dist/types/media-session.d.ts.map +1 -1
  37. package/dist/workers/worker-rpc-client.d.ts +2 -1
  38. package/dist/workers/worker-rpc-client.d.ts.map +1 -1
  39. package/node_modules/supervision-js-core/dist/detections/composite-detection-frame-source.d.ts.map +1 -1
  40. package/node_modules/supervision-js-core/dist/detections/memory-cold-detection-frame-store.d.ts.map +1 -1
  41. package/node_modules/supervision-js-core/dist/detections/projected-detection-frame-source.d.ts +22 -0
  42. package/node_modules/supervision-js-core/dist/detections/projected-detection-frame-source.d.ts.map +1 -0
  43. package/node_modules/supervision-js-core/dist/detections/writable-detection-frame-source.d.ts +2 -7
  44. package/node_modules/supervision-js-core/dist/detections/writable-detection-frame-source.d.ts.map +1 -1
  45. package/node_modules/supervision-js-core/dist/index.d.ts +11 -3
  46. package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
  47. package/node_modules/supervision-js-core/dist/index.js +2289 -329
  48. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  49. package/node_modules/supervision-js-core/dist/post-processing/byte-track-tracker.d.ts +4 -0
  50. package/node_modules/supervision-js-core/dist/post-processing/byte-track-tracker.d.ts.map +1 -0
  51. package/node_modules/supervision-js-core/dist/post-processing/cbiou-tracker.d.ts +4 -0
  52. package/node_modules/supervision-js-core/dist/post-processing/cbiou-tracker.d.ts.map +1 -0
  53. package/node_modules/supervision-js-core/dist/post-processing/oc-sort-tracker.d.ts +4 -0
  54. package/node_modules/supervision-js-core/dist/post-processing/oc-sort-tracker.d.ts.map +1 -0
  55. package/node_modules/supervision-js-core/dist/post-processing/sort-tracker.d.ts +4 -0
  56. package/node_modules/supervision-js-core/dist/post-processing/sort-tracker.d.ts.map +1 -0
  57. package/node_modules/supervision-js-core/dist/post-processing/tracking.d.ts +9 -0
  58. package/node_modules/supervision-js-core/dist/post-processing/tracking.d.ts.map +1 -0
  59. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +113 -3
  60. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
  61. package/node_modules/supervision-js-core/dist/types/detections.d.ts +33 -3
  62. package/node_modules/supervision-js-core/dist/types/detections.d.ts.map +1 -1
  63. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +31 -0
  64. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
  65. package/node_modules/supervision-js-core/dist/types/post-processing.d.ts +148 -0
  66. package/node_modules/supervision-js-core/dist/types/post-processing.d.ts.map +1 -0
  67. package/node_modules/supervision-js-core/dist/utils/detection-frames.d.ts.map +1 -1
  68. package/node_modules/supervision-js-core/dist/utils/detection-projection.d.ts +20 -0
  69. package/node_modules/supervision-js-core/dist/utils/detection-projection.d.ts.map +1 -0
  70. package/node_modules/supervision-js-core/package.json +12 -5
  71. 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 loadedSources = await Promise.all(sources.map(async (source) => ({
920
- ...source,
921
- frames: copySortedDetectionFrames(await source.source.loadFrames(startTime, endTime)),
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
- const nextSummary = await options.store.appendFrames(writeOptions(frames));
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
- const changedSourceRanges = getDetectionFrameRanges(frames);
1449
- const retainedSummary = await applyRetention(nextSummary);
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 (retainedSummary !== nextSummary) {
1452
- return recordAllRangesWrite(retainedSummary);
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 recordRangeWrite(nextSummary, changedSourceRanges);
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
- const nextSummary = await options.store.putFrames(writeOptions(frames));
1463
- assertActive();
1464
- const retainedSummary = await applyRetention(nextSummary);
1465
- assertActive();
1466
- return recordAllRangesWrite(retainedSummary);
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 options.store.clearDataset(options.datasetId);
1471
- assertActive();
1472
- summary = null;
1473
- version += 1;
1474
- allRangeVersion = version;
1475
- journalFloorVersion = version;
1476
- changedRanges.length = 0;
1477
- availableRanges.length = 0;
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 changedRanges.reduce((rangeVersion, changedRange) => rangesOverlap(range, changedRange)
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, changedRanges.reduce((changedVersion, changedRange) => rangesOverlap(range, changedRange)
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 < allRangeVersion ||
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
- ranges.some((range) => rangesOverlap(range, changedRange)))
1536
- .map(({ endTime, startTime }) => ({ endTime, startTime }));
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 nextSummary;
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
- const retentionStartTime = Math.max(0, nextSummary.endTime - retentionWindowSeconds);
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 nextSummary;
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 options.store.putFrames(writeOptions(retainedFrames));
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
- var DetectionPickTarget;
1661
- (function (DetectionPickTarget) {
1662
- DetectionPickTarget["Box"] = "box";
1663
- DetectionPickTarget["Edge"] = "edge";
1664
- DetectionPickTarget["Keypoint"] = "keypoint";
1665
- DetectionPickTarget["Label"] = "label";
1666
- DetectionPickTarget["Mask"] = "mask";
1667
- DetectionPickTarget["Polygon"] = "polygon";
1668
- DetectionPickTarget["Polyline"] = "polyline";
1669
- })(DetectionPickTarget || (DetectionPickTarget = {}));
1670
- var MediaInteractionMode;
1671
- (function (MediaInteractionMode) {
1672
- MediaInteractionMode["Always"] = "always";
1673
- MediaInteractionMode["Disabled"] = "disabled";
1674
- MediaInteractionMode["PausedOnly"] = "pausedOnly";
1675
- })(MediaInteractionMode || (MediaInteractionMode = {}));
1676
-
1677
- function centerRectToTopLeftRect(rect) {
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
- height: rect.height,
1680
- width: rect.width,
1681
- x: rect.x - rect.width / 2,
1682
- y: rect.y - rect.height / 2,
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
- function topLeftRectToCenterRect(rect) {
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
- height: rect.height,
1688
- width: rect.width,
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
- function getPointsRect(points) {
1694
- if (points.length === 0) {
1695
- return undefined;
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