@camstack/addon-pipeline 1.2.193 → 1.2.194
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{local-frame-registry-av9ObDG0.mjs → default-detection-model-BGBAmtEp.mjs} +83 -1
- package/dist/{local-frame-registry-x3m6SNqf.js → default-detection-model-CJdc8j83.js} +92 -4
- package/dist/detection-pipeline/index.js +28 -110
- package/dist/detection-pipeline/index.mjs +1 -83
- package/dist/pipeline-runner/index.js +401 -10
- package/dist/pipeline-runner/index.mjs +393 -2
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CUjbLOm9.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C6K2j4VN.mjs} +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CBxobiZW.mjs +26 -0
- package/dist/stream-broker/{hostInit-DnBCyuSA.mjs → hostInit-cpmZf09I.mjs} +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CkCh5TvE.mjs +0 -26
|
@@ -4,7 +4,7 @@ Object.defineProperties(exports, {
|
|
|
4
4
|
});
|
|
5
5
|
const require_dist = require("../dist-HG5geL9t.js");
|
|
6
6
|
const require_node = require("../node--my8JTOW.js");
|
|
7
|
-
const
|
|
7
|
+
const require_default_detection_model = require("../default-detection-model-CJdc8j83.js");
|
|
8
8
|
const require_worker_protocol = require("../worker-protocol-DCsi4YS1.js");
|
|
9
9
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
10
10
|
const require_lazy_sharp = require("../lazy-sharp-DmAwZ0s9.js");
|
|
@@ -1468,7 +1468,7 @@ function createSessionDecodeCoordinator(deps) {
|
|
|
1468
1468
|
* shm-ring cameras.
|
|
1469
1469
|
*/
|
|
1470
1470
|
async function toDecodedFrame(frame, format, viewSpec) {
|
|
1471
|
-
const geometry = viewSpec ?
|
|
1471
|
+
const geometry = viewSpec ? require_default_detection_model.resolveFrameViewGeometry(frame.image.width, frame.image.height, viewSpec) : void 0;
|
|
1472
1472
|
const bytes = await frame.image.toBuffer(geometry ? {
|
|
1473
1473
|
crop: geometry.sourceCrop,
|
|
1474
1474
|
resize: geometry.output,
|
|
@@ -5156,6 +5156,191 @@ var PipelineRunner = class {
|
|
|
5156
5156
|
return null;
|
|
5157
5157
|
}
|
|
5158
5158
|
};
|
|
5159
|
+
/**
|
|
5160
|
+
* How much of a region must lie OUTSIDE every full-frame detection for the
|
|
5161
|
+
* pass to run on it. A person's own blob (dilated, with shadow) is covered
|
|
5162
|
+
* 70–90% by the person box; a person-and-dog blob is covered ~55% by it.
|
|
5163
|
+
*/
|
|
5164
|
+
var RECOVERY_MIN_UNCOVERED_FRACTION = .35;
|
|
5165
|
+
function area(r) {
|
|
5166
|
+
return Math.max(0, r.w) * Math.max(0, r.h);
|
|
5167
|
+
}
|
|
5168
|
+
function intersection(a, b) {
|
|
5169
|
+
return Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
|
|
5170
|
+
}
|
|
5171
|
+
/**
|
|
5172
|
+
* Share of `region` not covered by any detection box. The sum of pairwise
|
|
5173
|
+
* intersections over-counts where detections overlap each other, which only
|
|
5174
|
+
* makes the pass MORE conservative; clipped at 1.
|
|
5175
|
+
*/
|
|
5176
|
+
function uncoveredFraction(region, detections) {
|
|
5177
|
+
const regionArea = area(region);
|
|
5178
|
+
if (regionArea <= 0) return 0;
|
|
5179
|
+
let covered = 0;
|
|
5180
|
+
for (const detection of detections) covered += intersection(region, detection);
|
|
5181
|
+
return 1 - Math.min(1, covered / regionArea);
|
|
5182
|
+
}
|
|
5183
|
+
function squareAround(centreX, centreY, side, native) {
|
|
5184
|
+
return {
|
|
5185
|
+
left: Math.round(Math.max(0, Math.min(native.width - side, centreX - side / 2))),
|
|
5186
|
+
top: Math.round(Math.max(0, Math.min(native.height - side, centreY - side / 2))),
|
|
5187
|
+
side
|
|
5188
|
+
};
|
|
5189
|
+
}
|
|
5190
|
+
function contains(crop, x, y) {
|
|
5191
|
+
return x >= crop.left && x <= crop.left + crop.side && y >= crop.top && y <= crop.top + crop.side;
|
|
5192
|
+
}
|
|
5193
|
+
/**
|
|
5194
|
+
* Squares for one region: one when it fits the largest side, otherwise up to
|
|
5195
|
+
* two tiles along its longer axis (a person-sized blob with something small
|
|
5196
|
+
* at either end). The padded region decides the side, never the frame.
|
|
5197
|
+
*/
|
|
5198
|
+
function cropsForRegion(region, native) {
|
|
5199
|
+
const widthPx = region.w * native.width;
|
|
5200
|
+
const heightPx = region.h * native.height;
|
|
5201
|
+
const longest = Math.max(widthPx, heightPx);
|
|
5202
|
+
const padded = longest * 1.5;
|
|
5203
|
+
const side = Math.round(Math.min(Math.max(padded, 640), 800));
|
|
5204
|
+
const centreX = (region.x + region.w / 2) * native.width;
|
|
5205
|
+
const centreY = (region.y + region.h / 2) * native.height;
|
|
5206
|
+
if (padded <= 800) return [squareAround(centreX, centreY, side, native)];
|
|
5207
|
+
const alongX = widthPx >= heightPx;
|
|
5208
|
+
const quarter = longest / 4;
|
|
5209
|
+
return [squareAround(alongX ? centreX - quarter : centreX, alongX ? centreY : centreY - quarter, side, native), squareAround(alongX ? centreX + quarter : centreX, alongX ? centreY : centreY + quarter, side, native)];
|
|
5210
|
+
}
|
|
5211
|
+
/**
|
|
5212
|
+
* The crops the second pass runs on this frame — possibly none.
|
|
5213
|
+
*
|
|
5214
|
+
* A region qualifies when it is small (`RECOVERY_MAX_REGION_FRAME_FRACTION`)
|
|
5215
|
+
* and mostly uncovered by the full-frame boxes
|
|
5216
|
+
* (`RECOVERY_MIN_UNCOVERED_FRACTION`). Qualifying regions are served largest
|
|
5217
|
+
* uncovered area first under `RECOVERY_MAX_CROPS_PER_FRAME`; a crop whose
|
|
5218
|
+
* centre already lies inside a planned crop is folded into it. A frame that
|
|
5219
|
+
* cannot hold the smallest crop yields nothing: it is already seen at scale.
|
|
5220
|
+
*/
|
|
5221
|
+
function planRecoveryCrops(input) {
|
|
5222
|
+
const { native } = input;
|
|
5223
|
+
if (Math.min(native.width, native.height) < 640) return [];
|
|
5224
|
+
const ranked = input.regions.map((region) => ({
|
|
5225
|
+
region,
|
|
5226
|
+
uncovered: uncoveredFraction(region, input.detections)
|
|
5227
|
+
})).filter(({ region, uncovered }) => {
|
|
5228
|
+
if (area(region) > .25) return false;
|
|
5229
|
+
return uncovered >= RECOVERY_MIN_UNCOVERED_FRACTION;
|
|
5230
|
+
}).toSorted((a, b) => area(b.region) * b.uncovered - area(a.region) * a.uncovered);
|
|
5231
|
+
const planned = [];
|
|
5232
|
+
for (const { region } of ranked) for (const crop of cropsForRegion(region, native)) {
|
|
5233
|
+
if (planned.length >= 2) return planned;
|
|
5234
|
+
const centreX = crop.left + crop.side / 2;
|
|
5235
|
+
const centreY = crop.top + crop.side / 2;
|
|
5236
|
+
if (planned.some((existing) => contains(existing, centreX, centreY))) continue;
|
|
5237
|
+
planned.push(crop);
|
|
5238
|
+
}
|
|
5239
|
+
return planned;
|
|
5240
|
+
}
|
|
5241
|
+
/**
|
|
5242
|
+
* Boxes the executor produced on the crop VIEW → the result's own space.
|
|
5243
|
+
*
|
|
5244
|
+
* Three spaces meet here: the view the model saw (`view`, ≤640 px), the native
|
|
5245
|
+
* square it was cut from (`crop`, native px) and the space the full-frame
|
|
5246
|
+
* result reports in (`result`, native px on the `frameRef` path, the inline
|
|
5247
|
+
* ≤640 view otherwise). A box touching the view edge was clipped by the crop
|
|
5248
|
+
* and is dropped: half a person is not a person, and the tracker would take
|
|
5249
|
+
* the half as the whole.
|
|
5250
|
+
*/
|
|
5251
|
+
function remapCropDetections(detections, crop, view, result, native) {
|
|
5252
|
+
const viewToNative = crop.side / Math.max(1, view.width);
|
|
5253
|
+
const nativeToResultX = result.width / Math.max(1, native.width);
|
|
5254
|
+
const nativeToResultY = result.height / Math.max(1, native.height);
|
|
5255
|
+
const out = [];
|
|
5256
|
+
for (const detection of detections) {
|
|
5257
|
+
const { x, y, width, height } = detection.bbox;
|
|
5258
|
+
if (x <= 2 || y <= 2 || x + width >= view.width - 2 || y + height >= view.height - 2) continue;
|
|
5259
|
+
out.push({
|
|
5260
|
+
...detection,
|
|
5261
|
+
bbox: {
|
|
5262
|
+
x: (crop.left + x * viewToNative) * nativeToResultX,
|
|
5263
|
+
y: (crop.top + y * viewToNative) * nativeToResultY,
|
|
5264
|
+
width: width * viewToNative * nativeToResultX,
|
|
5265
|
+
height: height * viewToNative * nativeToResultY
|
|
5266
|
+
}
|
|
5267
|
+
});
|
|
5268
|
+
}
|
|
5269
|
+
return out;
|
|
5270
|
+
}
|
|
5271
|
+
function boxIntersection(a, b) {
|
|
5272
|
+
return Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
|
|
5273
|
+
}
|
|
5274
|
+
function sameSubject(recovered, existing) {
|
|
5275
|
+
const inter = boxIntersection(recovered, existing);
|
|
5276
|
+
if (inter <= 0) return false;
|
|
5277
|
+
const areaR = recovered.width * recovered.height;
|
|
5278
|
+
const union = areaR + existing.width * existing.height - inter;
|
|
5279
|
+
if (union > 0 && inter / union >= .5) return true;
|
|
5280
|
+
return areaR > 0 && inter / areaR >= .7;
|
|
5281
|
+
}
|
|
5282
|
+
/**
|
|
5283
|
+
* Class-aware NMS of the recovered boxes against the full-frame ones. Every
|
|
5284
|
+
* full-frame box is kept as it is — the second pass only ADDS. A recovered box
|
|
5285
|
+
* is added when no full-frame box of the same macro class is the same subject
|
|
5286
|
+
* (`RECOVERY_FUSE_IOU`) or contains it (`RECOVERY_FUSE_CONTAINMENT`). Ids are
|
|
5287
|
+
* re-minted so a crop's `d0` cannot collide with the frame's.
|
|
5288
|
+
*/
|
|
5289
|
+
function fuseRecoveredDetections(full, recovered) {
|
|
5290
|
+
const accepted = [];
|
|
5291
|
+
for (const [index, candidate] of recovered.entries()) {
|
|
5292
|
+
if ([...full, ...accepted].some((existing) => existing.macroClass === candidate.macroClass && sameSubject(candidate.bbox, existing.bbox))) continue;
|
|
5293
|
+
accepted.push({
|
|
5294
|
+
...candidate,
|
|
5295
|
+
id: `recovered-${index}-${candidate.id}`
|
|
5296
|
+
});
|
|
5297
|
+
}
|
|
5298
|
+
return [...full, ...accepted];
|
|
5299
|
+
}
|
|
5300
|
+
/**
|
|
5301
|
+
* The step the second pass runs: the enabled first-level object detector of
|
|
5302
|
+
* the camera's own tree, with its own model and no children. Same model as
|
|
5303
|
+
* the full pass so a recovered box is comparable to a full-frame one.
|
|
5304
|
+
*/
|
|
5305
|
+
function selectRecoveryStep(steps) {
|
|
5306
|
+
const detector = steps.find((step) => step.addonId === "object-detection" && step.enabled !== false);
|
|
5307
|
+
if (!detector) return null;
|
|
5308
|
+
return {
|
|
5309
|
+
addonId: detector.addonId,
|
|
5310
|
+
...detector.modelId !== void 0 ? { modelId: detector.modelId } : {},
|
|
5311
|
+
enabled: true
|
|
5312
|
+
};
|
|
5313
|
+
}
|
|
5314
|
+
/**
|
|
5315
|
+
* The analyzer's last regions per camera, normalised, with the instant they
|
|
5316
|
+
* were measured. Refreshed off the motion path; read on the detection path.
|
|
5317
|
+
* A stale entry answers nothing — the regions of two seconds ago are not this
|
|
5318
|
+
* frame's. An EMPTY analyzer result replaces the previous regions: motion that
|
|
5319
|
+
* stopped is not motion.
|
|
5320
|
+
*/
|
|
5321
|
+
var MotionRegionMirror = class {
|
|
5322
|
+
byDevice = /* @__PURE__ */ new Map();
|
|
5323
|
+
note(deviceId, regions, frameWidth, frameHeight, now) {
|
|
5324
|
+
if (frameWidth <= 0 || frameHeight <= 0) return;
|
|
5325
|
+
this.byDevice.set(deviceId, {
|
|
5326
|
+
at: now,
|
|
5327
|
+
regions: regions.map((region) => ({
|
|
5328
|
+
x: region.bbox.x / frameWidth,
|
|
5329
|
+
y: region.bbox.y / frameHeight,
|
|
5330
|
+
w: region.bbox.w / frameWidth,
|
|
5331
|
+
h: region.bbox.h / frameHeight
|
|
5332
|
+
}))
|
|
5333
|
+
});
|
|
5334
|
+
}
|
|
5335
|
+
fresh(deviceId, now) {
|
|
5336
|
+
const entry = this.byDevice.get(deviceId);
|
|
5337
|
+
if (!entry || now - entry.at > 2e3) return [];
|
|
5338
|
+
return entry.regions;
|
|
5339
|
+
}
|
|
5340
|
+
forget(deviceId) {
|
|
5341
|
+
this.byDevice.delete(deviceId);
|
|
5342
|
+
}
|
|
5343
|
+
};
|
|
5159
5344
|
//#endregion
|
|
5160
5345
|
//#region src/pipeline-runner/stateless-step.ts
|
|
5161
5346
|
function modelServability(step, modelId, format) {
|
|
@@ -5306,6 +5491,13 @@ function isUsable(bbox) {
|
|
|
5306
5491
|
* independently from the inference workers.
|
|
5307
5492
|
*/
|
|
5308
5493
|
/**
|
|
5494
|
+
* How often the small-subject recovery pass reports per camera. The pass runs
|
|
5495
|
+
* up to twice a second; one aggregated line every ten seconds says how often
|
|
5496
|
+
* it ran, what it cost and what it found — the numbers a "did the second
|
|
5497
|
+
* pass help on 615?" question is answered from.
|
|
5498
|
+
*/
|
|
5499
|
+
var RECOVERY_LOG_INTERVAL_MS = 1e4;
|
|
5500
|
+
/**
|
|
5309
5501
|
* How often a device's package zone-union geometry is re-read. An operator
|
|
5310
5502
|
* redrawing a package zone takes effect within one interval; the read is a
|
|
5311
5503
|
* cross-addon cap call reached from the per-frame package gate, so it must be
|
|
@@ -5359,7 +5551,7 @@ function writeRetainedDetectionFrame(store, deviceId, frame, retention) {
|
|
|
5359
5551
|
if (retention) return retention.admitFrame(deviceId, frame);
|
|
5360
5552
|
if (!store) return void 0;
|
|
5361
5553
|
if (!isContinuousRetainEnabled()) return store.allocateHandle(deviceId, frame) ?? void 0;
|
|
5362
|
-
return store.write(deviceId, frame, frame.frameRef ? createRetainedContentFallback(
|
|
5554
|
+
return store.write(deviceId, frame, frame.frameRef ? createRetainedContentFallback(require_default_detection_model.localFrameRegistry, frame.frameRef) : void 0) ?? void 0;
|
|
5363
5555
|
}
|
|
5364
5556
|
var DETECTION_KEEPWARM_MS = 15e3;
|
|
5365
5557
|
/**
|
|
@@ -5420,7 +5612,7 @@ function collectEnabledStepIds(steps, acc) {
|
|
|
5420
5612
|
*/
|
|
5421
5613
|
function tryGetStepDefinition(stepId) {
|
|
5422
5614
|
try {
|
|
5423
|
-
return
|
|
5615
|
+
return require_default_detection_model.getStepDefinition(stepId);
|
|
5424
5616
|
} catch {
|
|
5425
5617
|
return null;
|
|
5426
5618
|
}
|
|
@@ -5720,6 +5912,16 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5720
5912
|
* only (otherwise we'd emit on every analyzer frame). */
|
|
5721
5913
|
lastAnalyzerDetected = /* @__PURE__ */ new Map();
|
|
5722
5914
|
/**
|
|
5915
|
+
* The analyzer's last motion regions per camera — what the small-subject
|
|
5916
|
+
* recovery pass judges a full-frame result against. Written on the motion
|
|
5917
|
+
* path, read on the detection path (`small-subject-recovery.ts`).
|
|
5918
|
+
*/
|
|
5919
|
+
motionRegions = new MotionRegionMirror();
|
|
5920
|
+
/** Wall clock of the last recovery pass per camera (`RECOVERY_MIN_INTERVAL_MS`). */
|
|
5921
|
+
lastRecoveryAt = /* @__PURE__ */ new Map();
|
|
5922
|
+
/** Per-camera counters since the last recovery report line. */
|
|
5923
|
+
recoveryStats = /* @__PURE__ */ new Map();
|
|
5924
|
+
/**
|
|
5723
5925
|
* Gates the root-plane `package-detection` step to motion-settle edges +
|
|
5724
5926
|
* a low-cadence safety poll instead of every frame (packages are
|
|
5725
5927
|
* stationary — per-frame parcel inference is wasted cost). Consulted in
|
|
@@ -5889,7 +6091,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5889
6091
|
});
|
|
5890
6092
|
this.retention = new OnDemandRetentionSession({
|
|
5891
6093
|
store: this.retainedFrames,
|
|
5892
|
-
registry:
|
|
6094
|
+
registry: require_default_detection_model.localFrameRegistry,
|
|
5893
6095
|
telemetry: this.frameLazy,
|
|
5894
6096
|
nodeId: this.nodeId,
|
|
5895
6097
|
continuousRetain: isContinuousRetainEnabled()
|
|
@@ -5902,7 +6104,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5902
6104
|
minThrottledFps: this.config.minThrottledFps,
|
|
5903
6105
|
processFrame: (deviceId, frame, handle) => this.runInference(deviceId, frame, handle),
|
|
5904
6106
|
analyzeMotion: (deviceId, frame, handle) => this.runMotionAnalysis(deviceId, frame, handle),
|
|
5905
|
-
releaseFrameRef: (ref, reason) =>
|
|
6107
|
+
releaseFrameRef: (ref, reason) => require_default_detection_model.localFrameRegistry.release(ref, reason),
|
|
5906
6108
|
releaseRetention: (handle, reason) => {
|
|
5907
6109
|
this.retention?.release(handle, reason);
|
|
5908
6110
|
this.frameCutters.drop(handle);
|
|
@@ -7440,6 +7642,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7440
7642
|
this.packageZoneUnionByDevice.delete(deviceId);
|
|
7441
7643
|
this.packageZoneUnionAt.delete(deviceId);
|
|
7442
7644
|
this.lastMotionNearPackageZone.delete(deviceId);
|
|
7645
|
+
this.motionRegions.forget(deviceId);
|
|
7646
|
+
this.lastRecoveryAt.delete(deviceId);
|
|
7647
|
+
this.recoveryStats.delete(deviceId);
|
|
7443
7648
|
const warnPrefix = `${deviceId}:`;
|
|
7444
7649
|
for (const key of this.warnedNoJumpDevice) if (key.startsWith(warnPrefix)) this.warnedNoJumpDevice.delete(key);
|
|
7445
7650
|
this.attached.delete(deviceId);
|
|
@@ -7846,7 +8051,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7846
8051
|
return materialize;
|
|
7847
8052
|
},
|
|
7848
8053
|
...useLocalFrameRef ? {
|
|
7849
|
-
frameRegistry:
|
|
8054
|
+
frameRegistry: require_default_detection_model.localFrameRegistry,
|
|
7850
8055
|
inlineWhen: () => runner.detectionBackpressure(config.deviceId),
|
|
7851
8056
|
inlineViewSpec: SPLIT_DETECTION_VIEW,
|
|
7852
8057
|
onFrameRef: async (frame, nativeCrop) => {
|
|
@@ -7866,7 +8071,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7866
8071
|
const admitted = current.detection && runner.enqueueReservedDetectionFrame(config.deviceId, frame, handle);
|
|
7867
8072
|
if (admitted) nativeCrop?.markInferred();
|
|
7868
8073
|
if (current.motion) {
|
|
7869
|
-
const motion = await resolveMotionFrameView(
|
|
8074
|
+
const motion = await resolveMotionFrameView(require_default_detection_model.localFrameRegistry, frame.frameRef);
|
|
7870
8075
|
if (motion.kind === "view") runner.enqueueMotionFrame(config.deviceId, {
|
|
7871
8076
|
data: Buffer.from(motion.data),
|
|
7872
8077
|
width: motion.width,
|
|
@@ -8160,8 +8365,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8160
8365
|
raw: frame.data.byteLength,
|
|
8161
8366
|
jpeg: 0
|
|
8162
8367
|
});
|
|
8368
|
+
let result;
|
|
8163
8369
|
try {
|
|
8164
|
-
|
|
8370
|
+
result = await api.pipelineExecutor.runPipeline.mutate({
|
|
8165
8371
|
steps: [...dispatchSteps],
|
|
8166
8372
|
...image !== void 0 ? { image } : selectExecutorFrameSource(executorCoLocated, frame.frameRef, frame),
|
|
8167
8373
|
deviceId,
|
|
@@ -8178,6 +8384,190 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8178
8384
|
} });
|
|
8179
8385
|
return null;
|
|
8180
8386
|
}
|
|
8387
|
+
if (result === null) return null;
|
|
8388
|
+
return this.recoverSmallSubjects(deviceId, frame, result, dispatchSteps, camConfig?.deviceKey);
|
|
8389
|
+
}
|
|
8390
|
+
/**
|
|
8391
|
+
* The second detector pass at native resolution — `small-subject-recovery.ts`
|
|
8392
|
+
* for the why and the geometry. This method owns only what is not pure: the
|
|
8393
|
+
* per-camera rate limit, the `frameRef` lease that yields the crop view, the
|
|
8394
|
+
* executor call and the report line.
|
|
8395
|
+
*
|
|
8396
|
+
* Runs on the frame the full pass just ran on, in this process, through the
|
|
8397
|
+
* same process-local lease. Anything that makes the pass impossible (no
|
|
8398
|
+
* local frame, no detector in the tree, a stale or empty motion mirror, a
|
|
8399
|
+
* lease miss, an executor error) leaves the full-frame result EXACTLY as it
|
|
8400
|
+
* was: the pass only ever adds.
|
|
8401
|
+
*
|
|
8402
|
+
* The crop is dispatched WITHOUT `deviceId` on purpose. The executor's
|
|
8403
|
+
* provider applies the detection-stage zone gate to whatever result it
|
|
8404
|
+
* builds, in that result's own coordinates — for a crop those are crop
|
|
8405
|
+
* coordinates, and a zone polygon evaluated against them gates the wrong
|
|
8406
|
+
* pixels. The recovered boxes are re-projected into frame space here, and
|
|
8407
|
+
* the tracker on the analytics side applies the same detection rules in
|
|
8408
|
+
* frame space (`frame-processor.ts`, zone filter), so nothing is gated
|
|
8409
|
+
* twice and nothing is gated wrong. The cost is that the crop pass runs on
|
|
8410
|
+
* the node's default thresholds rather than a per-device override.
|
|
8411
|
+
*/
|
|
8412
|
+
async recoverSmallSubjects(deviceId, frame, result, steps, deviceKey) {
|
|
8413
|
+
const api = this.ctx?.api;
|
|
8414
|
+
if (!api || !frame.frameRef) return result;
|
|
8415
|
+
const now = Date.now();
|
|
8416
|
+
if (now - (this.lastRecoveryAt.get(deviceId) ?? 0) < 500) return result;
|
|
8417
|
+
const regions = this.motionRegions.fresh(deviceId, now);
|
|
8418
|
+
if (regions.length === 0) return result;
|
|
8419
|
+
const step = selectRecoveryStep(steps);
|
|
8420
|
+
if (step === null) return result;
|
|
8421
|
+
if (result.width <= 0 || result.height <= 0) return result;
|
|
8422
|
+
const native = {
|
|
8423
|
+
width: frame.frameRef.width,
|
|
8424
|
+
height: frame.frameRef.height
|
|
8425
|
+
};
|
|
8426
|
+
const crops = planRecoveryCrops({
|
|
8427
|
+
regions,
|
|
8428
|
+
detections: result.detections.filter((d) => d.kind === "first-level").map((d) => ({
|
|
8429
|
+
x: d.bbox.x / result.width,
|
|
8430
|
+
y: d.bbox.y / result.height,
|
|
8431
|
+
w: d.bbox.width / result.width,
|
|
8432
|
+
h: d.bbox.height / result.height
|
|
8433
|
+
})),
|
|
8434
|
+
native
|
|
8435
|
+
});
|
|
8436
|
+
if (crops.length === 0) return result;
|
|
8437
|
+
this.lastRecoveryAt.set(deviceId, now);
|
|
8438
|
+
const log = this.ctx.logger.withTags({ deviceId });
|
|
8439
|
+
const started = Date.now();
|
|
8440
|
+
const recovered = [];
|
|
8441
|
+
let cropsRun = 0;
|
|
8442
|
+
for (const crop of crops) {
|
|
8443
|
+
const view = await this.resolveRecoveryCropView(frame.frameRef, crop);
|
|
8444
|
+
if (view === null) continue;
|
|
8445
|
+
cropsRun += 1;
|
|
8446
|
+
try {
|
|
8447
|
+
const cropResult = await api.pipelineExecutor.runPipeline.mutate({
|
|
8448
|
+
steps: [step],
|
|
8449
|
+
image: view.jpeg,
|
|
8450
|
+
plane: "frame",
|
|
8451
|
+
...deviceKey ? { deviceKey } : {}
|
|
8452
|
+
});
|
|
8453
|
+
if (!cropResult) continue;
|
|
8454
|
+
recovered.push(...remapCropDetections(cropResult.detections, crop, {
|
|
8455
|
+
width: view.width,
|
|
8456
|
+
height: view.height
|
|
8457
|
+
}, {
|
|
8458
|
+
width: result.width,
|
|
8459
|
+
height: result.height
|
|
8460
|
+
}, native));
|
|
8461
|
+
} catch (err) {
|
|
8462
|
+
log.warn("small-subject recovery: crop pass failed — full-frame result kept", { meta: {
|
|
8463
|
+
error: require_dist.errMsg(err),
|
|
8464
|
+
crop,
|
|
8465
|
+
nodeId: this.nodeId
|
|
8466
|
+
} });
|
|
8467
|
+
}
|
|
8468
|
+
}
|
|
8469
|
+
const fused = fuseRecoveredDetections(result.detections, recovered);
|
|
8470
|
+
const added = fused.length - result.detections.length;
|
|
8471
|
+
this.reportRecovery(deviceId, {
|
|
8472
|
+
crops: cropsRun,
|
|
8473
|
+
recovered: added,
|
|
8474
|
+
ms: Date.now() - started,
|
|
8475
|
+
now,
|
|
8476
|
+
classes: fused.slice(result.detections.length).map((d) => d.macroClass)
|
|
8477
|
+
});
|
|
8478
|
+
return added > 0 ? {
|
|
8479
|
+
...result,
|
|
8480
|
+
detections: fused
|
|
8481
|
+
} : result;
|
|
8482
|
+
}
|
|
8483
|
+
/**
|
|
8484
|
+
* One compressed ≤{@link RECOVERY_VIEW_PX} px view of a native square, from
|
|
8485
|
+
* the same process-local lease the full pass resolved its view from. `null`
|
|
8486
|
+
* on a miss — the frame is gone, and the full-frame result stands.
|
|
8487
|
+
*/
|
|
8488
|
+
async resolveRecoveryCropView(ref, crop) {
|
|
8489
|
+
const lease = require_default_detection_model.localFrameRegistry.acquire(ref);
|
|
8490
|
+
if (!lease) return null;
|
|
8491
|
+
let reason = "error";
|
|
8492
|
+
try {
|
|
8493
|
+
const view = await lease.resolve({
|
|
8494
|
+
crop: {
|
|
8495
|
+
left: crop.left,
|
|
8496
|
+
top: crop.top,
|
|
8497
|
+
width: crop.side,
|
|
8498
|
+
height: crop.side
|
|
8499
|
+
},
|
|
8500
|
+
content: {
|
|
8501
|
+
width: 640,
|
|
8502
|
+
height: 640
|
|
8503
|
+
},
|
|
8504
|
+
fit: "contain",
|
|
8505
|
+
format: "jpeg"
|
|
8506
|
+
});
|
|
8507
|
+
if (view.kind !== "view") {
|
|
8508
|
+
reason = "drop";
|
|
8509
|
+
return null;
|
|
8510
|
+
}
|
|
8511
|
+
reason = "success";
|
|
8512
|
+
const jpeg = new Uint8Array(new ArrayBuffer(view.data.byteLength));
|
|
8513
|
+
jpeg.set(view.data);
|
|
8514
|
+
return {
|
|
8515
|
+
jpeg,
|
|
8516
|
+
width: view.width,
|
|
8517
|
+
height: view.height
|
|
8518
|
+
};
|
|
8519
|
+
} catch {
|
|
8520
|
+
return null;
|
|
8521
|
+
} finally {
|
|
8522
|
+
lease.release(reason);
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
/** Aggregate the pass per camera and report once per {@link RECOVERY_LOG_INTERVAL_MS}. */
|
|
8526
|
+
reportRecovery(deviceId, sample) {
|
|
8527
|
+
const stats = this.recoveryStats.get(deviceId) ?? {
|
|
8528
|
+
passes: 0,
|
|
8529
|
+
crops: 0,
|
|
8530
|
+
recovered: 0,
|
|
8531
|
+
ms: 0,
|
|
8532
|
+
reportedAt: sample.now
|
|
8533
|
+
};
|
|
8534
|
+
const next = {
|
|
8535
|
+
passes: stats.passes + 1,
|
|
8536
|
+
crops: stats.crops + sample.crops,
|
|
8537
|
+
recovered: stats.recovered + sample.recovered,
|
|
8538
|
+
ms: stats.ms + sample.ms,
|
|
8539
|
+
reportedAt: stats.reportedAt
|
|
8540
|
+
};
|
|
8541
|
+
if (sample.recovered > 0) this.ctx.logger.info("small-subject recovery: the native crop pass found what the full frame missed", {
|
|
8542
|
+
tags: { deviceId },
|
|
8543
|
+
meta: {
|
|
8544
|
+
crops: sample.crops,
|
|
8545
|
+
recovered: sample.recovered,
|
|
8546
|
+
classes: sample.classes,
|
|
8547
|
+
ms: sample.ms
|
|
8548
|
+
}
|
|
8549
|
+
});
|
|
8550
|
+
if (sample.now - stats.reportedAt >= RECOVERY_LOG_INTERVAL_MS) {
|
|
8551
|
+
this.ctx.logger.info("small-subject recovery: pass report", {
|
|
8552
|
+
tags: { deviceId },
|
|
8553
|
+
meta: {
|
|
8554
|
+
passes: next.passes,
|
|
8555
|
+
crops: next.crops,
|
|
8556
|
+
recovered: next.recovered,
|
|
8557
|
+
avgMsPerPass: next.passes > 0 ? Math.round(next.ms / next.passes) : 0,
|
|
8558
|
+
windowMs: sample.now - stats.reportedAt
|
|
8559
|
+
}
|
|
8560
|
+
});
|
|
8561
|
+
this.recoveryStats.set(deviceId, {
|
|
8562
|
+
passes: 0,
|
|
8563
|
+
crops: 0,
|
|
8564
|
+
recovered: 0,
|
|
8565
|
+
ms: 0,
|
|
8566
|
+
reportedAt: sample.now
|
|
8567
|
+
});
|
|
8568
|
+
return;
|
|
8569
|
+
}
|
|
8570
|
+
this.recoveryStats.set(deviceId, next);
|
|
8181
8571
|
}
|
|
8182
8572
|
/** Sticky true once the executor's provider shows up in THIS process' local
|
|
8183
8573
|
* registry (the `detection` execution.group) — from then on the per-frame
|
|
@@ -8258,6 +8648,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8258
8648
|
});
|
|
8259
8649
|
if (!result) return;
|
|
8260
8650
|
const detected = result.regions.length > 0;
|
|
8651
|
+
this.motionRegions.note(deviceId, result.regions, result.frameWidth, result.frameHeight, Date.now());
|
|
8261
8652
|
this.lastMotionNearPackageZone.set(deviceId, motionNearPackageZone({
|
|
8262
8653
|
regions: result.regions,
|
|
8263
8654
|
frameWidth: result.frameWidth,
|
|
@@ -8530,7 +8921,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8530
8921
|
format: "rgb"
|
|
8531
8922
|
},
|
|
8532
8923
|
...useLocalFrameRef ? {
|
|
8533
|
-
frameRegistry:
|
|
8924
|
+
frameRegistry: require_default_detection_model.localFrameRegistry,
|
|
8534
8925
|
inlineWhen: () => runner.detectionBackpressure(config.deviceId),
|
|
8535
8926
|
inlineViewSpec: SPLIT_DETECTION_VIEW,
|
|
8536
8927
|
onFrameRef: (frame, nativeCrop) => {
|