@camstack/addon-pipeline 1.1.63 → 1.1.65
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/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +157 -17
- package/dist/detection-pipeline/index.mjs +157 -17
- package/dist/{dist-BzVZ2I4P.mjs → dist-D6tHM9GA.mjs} +0 -55
- package/dist/{dist-B2jTt7Lq.js → dist-WW3gH8hF.js} +0 -55
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +2 -2
- package/dist/pipeline-runner/index.mjs +2 -2
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/{step-definitions-DXkY5Zor.js → step-definitions-CXZpwwT0.js} +12 -1
- package/dist/{step-definitions-B07M9qhj.mjs → step-definitions-Dbsm64_I.mjs} +12 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-dTMki_1D.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CbU_HuOC.mjs} +2 -2
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-bAh96WNb.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CxZvj8X1.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-BzNJyeNE.mjs +26 -0
- package/dist/stream-broker/{hostInit-BY2oQAEX.mjs → hostInit-QaDEOY0Z.mjs} +2 -2
- package/dist/stream-broker/index.js +51 -3
- package/dist/stream-broker/index.mjs +51 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C03fx7Sp.js → MaskShapeCanvas-DI4BY7W2-C7H0kVAH.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BTxzn1rQ.js → MotionZonesSettings-NcxxQN8r-CHfEFlEF.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-GKO8O7rC.js → PrivacyMaskSettings-APgPLF7p-Bc-trVxX.js} +1 -1
- package/embed-dist/assets/{index-QdssILAA.js → index-eZGm_itM.js} +9 -9
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/python/postprocessors/scrfd.py +4 -1
- package/python/postprocessors/test_yolo_nms.py +45 -0
- package/python/postprocessors/yolo.py +4 -1
- package/python/postprocessors/yolo_seg.py +4 -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-CV2WbUaO.mjs +0 -26
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-D6vf50IK.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-WW3gH8hF.js");
|
|
7
7
|
const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
|
|
8
8
|
const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
|
|
9
9
|
let node_fs = require("node:fs");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __require } from "../chunk-BdkLduGY.mjs";
|
|
2
|
-
import { B as errMsg, K as hydrateSchema, T as mapAudioLabelToMacro, V as BaseAddon, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, u as HF_BASE_URL } from "../dist-
|
|
2
|
+
import { B as errMsg, K as hydrateSchema, T as mapAudioLabelToMacro, V as BaseAddon, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, u as HF_BASE_URL } from "../dist-D6tHM9GA.mjs";
|
|
3
3
|
import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
|
|
4
4
|
import { i as downloadFile } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
|
|
5
5
|
import * as fs from "node:fs";
|
|
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-D6vf50IK.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_step_definitions = require("../step-definitions-
|
|
6
|
+
const require_dist = require("../dist-WW3gH8hF.js");
|
|
7
|
+
const require_step_definitions = require("../step-definitions-CXZpwwT0.js");
|
|
8
8
|
const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
|
|
9
9
|
const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
|
|
10
10
|
let node_child_process = require("node:child_process");
|
|
@@ -316,7 +316,7 @@ function projectNodeEngine(store, nodeId) {
|
|
|
316
316
|
//#endregion
|
|
317
317
|
//#region src/detection-pipeline/default-detection-model.ts
|
|
318
318
|
/** The object-detection step id — the only slot this resolver applies to. */
|
|
319
|
-
var OBJECT_DETECTION_STEP_ID = "object-detection";
|
|
319
|
+
var OBJECT_DETECTION_STEP_ID$1 = "object-detection";
|
|
320
320
|
/**
|
|
321
321
|
* Balanced default object-detection model per accelerator class (measured on
|
|
322
322
|
* COCO val2017). `'cpu'` maps to `null` → the caller substitutes the step's
|
|
@@ -376,7 +376,7 @@ function classifyAccelerator(backend, device) {
|
|
|
376
376
|
function resolveDefaultDetectionModel(backend, device, format, getStepDef = require_step_definitions.getStepDefinition) {
|
|
377
377
|
let def;
|
|
378
378
|
try {
|
|
379
|
-
def = getStepDef(OBJECT_DETECTION_STEP_ID);
|
|
379
|
+
def = getStepDef(OBJECT_DETECTION_STEP_ID$1);
|
|
380
380
|
} catch {
|
|
381
381
|
return "yolo26n";
|
|
382
382
|
}
|
|
@@ -951,6 +951,7 @@ function serializeModelConfig(config) {
|
|
|
951
951
|
postprocessor: config.postprocessor,
|
|
952
952
|
confidence: config.confidence
|
|
953
953
|
};
|
|
954
|
+
if (config.nmsIouThreshold !== void 0) result["nmsIouThreshold"] = config.nmsIouThreshold;
|
|
954
955
|
if (config.inputWidth !== void 0) result["inputWidth"] = config.inputWidth;
|
|
955
956
|
if (config.inputHeight !== void 0) result["inputHeight"] = config.inputHeight;
|
|
956
957
|
if (config.inputChannels !== void 0) result["inputChannels"] = config.inputChannels;
|
|
@@ -1258,6 +1259,31 @@ var RUNTIME_TO_FORMAT = {
|
|
|
1258
1259
|
onnxruntime: "onnx",
|
|
1259
1260
|
edgetpu: "tflite"
|
|
1260
1261
|
};
|
|
1262
|
+
/** The step whose detections are re-filtered downstream by the executor's
|
|
1263
|
+
* per-macro `minConfidence*` sliders (`matchesMacroFilter`). Its pool-side
|
|
1264
|
+
* floor must stay BELOW those sliders or it silently pre-empts them. */
|
|
1265
|
+
var OBJECT_DETECTION_STEP_ID = "object-detection";
|
|
1266
|
+
/**
|
|
1267
|
+
* Pool-side confidence FLOOR for the object-detection step (#4 foot-gun fix).
|
|
1268
|
+
*
|
|
1269
|
+
* The pool used to receive `def.defaultConfidence` (0.5) as its floor and drop
|
|
1270
|
+
* every box < 0.5 BEFORE the executor's per-macro `minConfidence*` sliders run —
|
|
1271
|
+
* so a slider set below 0.5 was a silent no-op. Lower the pool floor to a small
|
|
1272
|
+
* constant so the operator-visible per-macro sliders are the authoritative
|
|
1273
|
+
* filter across their full 0–1 range. The per-macro slider DEFAULTS stay at 0.5
|
|
1274
|
+
* (see `step-definitions.ts` getConfigSchema), so default UI behaviour is
|
|
1275
|
+
* unchanged; only sub-0.5 sliders start having an effect. Non-object-detection
|
|
1276
|
+
* steps keep their own `def.defaultConfidence` floor (no per-macro re-filter). */
|
|
1277
|
+
var OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR = .05;
|
|
1278
|
+
/**
|
|
1279
|
+
* Resolve the pool-side confidence FLOOR for a step (#4). The object-detection
|
|
1280
|
+
* step gets the small {@link OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR} so its
|
|
1281
|
+
* per-macro `minConfidence*` sliders (applied downstream by the executor) are
|
|
1282
|
+
* the authoritative filter; every other step keeps its own `defaultConfidence`.
|
|
1283
|
+
*/
|
|
1284
|
+
function resolvePoolConfidenceFloor(stepId, defaultConfidence) {
|
|
1285
|
+
return stepId === OBJECT_DETECTION_STEP_ID ? OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR : defaultConfidence;
|
|
1286
|
+
}
|
|
1261
1287
|
var EngineFactory = class {
|
|
1262
1288
|
pool = null;
|
|
1263
1289
|
poolManager = null;
|
|
@@ -1441,7 +1467,8 @@ var EngineFactory = class {
|
|
|
1441
1467
|
inputChannels,
|
|
1442
1468
|
preprocessMode: modelEntry.preprocessMode ?? "letterbox",
|
|
1443
1469
|
postprocessor: modelEntry.postprocessor ?? def.postprocessor,
|
|
1444
|
-
confidence: def.defaultConfidence,
|
|
1470
|
+
confidence: resolvePoolConfidenceFloor(stepId, def.defaultConfidence),
|
|
1471
|
+
nmsIouThreshold: this.opts.nmsIouThreshold ?? .45,
|
|
1445
1472
|
labels,
|
|
1446
1473
|
charset: def.charset,
|
|
1447
1474
|
numClasses: labels?.length,
|
|
@@ -1560,6 +1587,9 @@ var EngineProvisioner = class {
|
|
|
1560
1587
|
};
|
|
1561
1588
|
//#endregion
|
|
1562
1589
|
//#region src/detection-pipeline/postprocess/dispatch.ts
|
|
1590
|
+
/** Detector NMS IoU fallback — the historical hardcoded literal, used when the
|
|
1591
|
+
* step carries no `nmsIouThreshold` setting (unset == today). */
|
|
1592
|
+
var DEFAULT_NMS_IOU = .45;
|
|
1563
1593
|
var VALID_KINDS = new Set([
|
|
1564
1594
|
"detections",
|
|
1565
1595
|
"classifications",
|
|
@@ -1587,7 +1617,7 @@ function tryStructured(output) {
|
|
|
1587
1617
|
if (!isStepOutput(output.structured)) throw new Error(`Python pool returned structured output with unexpected kind: ${JSON.stringify(output.structured["kind"])}`);
|
|
1588
1618
|
return output.structured;
|
|
1589
1619
|
}
|
|
1590
|
-
function postprocessYolo(output, stepDef) {
|
|
1620
|
+
function postprocessYolo(output, stepDef, nmsIouThreshold) {
|
|
1591
1621
|
const structured = tryStructured(output);
|
|
1592
1622
|
if (structured) return structured;
|
|
1593
1623
|
const tensor = output.tensor;
|
|
@@ -1636,7 +1666,7 @@ function postprocessYolo(output, stepDef) {
|
|
|
1636
1666
|
}
|
|
1637
1667
|
return {
|
|
1638
1668
|
kind: "detections",
|
|
1639
|
-
detections: simpleNms(dets,
|
|
1669
|
+
detections: simpleNms(dets, nmsIouThreshold)
|
|
1640
1670
|
};
|
|
1641
1671
|
}
|
|
1642
1672
|
function postprocessSsd(output, _stepDef) {
|
|
@@ -1644,7 +1674,7 @@ function postprocessSsd(output, _stepDef) {
|
|
|
1644
1674
|
if (structured) return structured;
|
|
1645
1675
|
throw new Error("SSD postprocessing runs in the Python inference pool (ssd.py); no TypeScript raw-tensor path.");
|
|
1646
1676
|
}
|
|
1647
|
-
function postprocessScrfd(output, stepDef) {
|
|
1677
|
+
function postprocessScrfd(output, stepDef, nmsIouThreshold) {
|
|
1648
1678
|
const structured = tryStructured(output);
|
|
1649
1679
|
if (structured) return structured;
|
|
1650
1680
|
if (!output.tensors) throw new Error("SCRFD postprocessor: no tensors in engine output");
|
|
@@ -1722,7 +1752,7 @@ function postprocessScrfd(output, stepDef) {
|
|
|
1722
1752
|
score: c.score,
|
|
1723
1753
|
bbox: c.bbox,
|
|
1724
1754
|
...c.landmarks ? { landmarks: c.landmarks } : {}
|
|
1725
|
-
})),
|
|
1755
|
+
})), nmsIouThreshold)
|
|
1726
1756
|
};
|
|
1727
1757
|
}
|
|
1728
1758
|
/** Match ONNX output tensors to SCRFD strides by expected anchor count. */
|
|
@@ -1927,7 +1957,7 @@ function postprocessSaliency(output, _stepDef) {
|
|
|
1927
1957
|
maskHeight: side
|
|
1928
1958
|
};
|
|
1929
1959
|
}
|
|
1930
|
-
function postprocessYoloSeg(output, stepDef) {
|
|
1960
|
+
function postprocessYoloSeg(output, stepDef, nmsIouThreshold) {
|
|
1931
1961
|
const structured = tryStructured(output);
|
|
1932
1962
|
if (structured) return structured;
|
|
1933
1963
|
const tensors = output.tensors;
|
|
@@ -2052,7 +2082,7 @@ function postprocessYoloSeg(output, stepDef) {
|
|
|
2052
2082
|
class: d.class,
|
|
2053
2083
|
score: d.score,
|
|
2054
2084
|
bbox: d.bbox
|
|
2055
|
-
})),
|
|
2085
|
+
})), nmsIouThreshold);
|
|
2056
2086
|
const keptSet = new Set(keptBoxes.map((d) => `${d.bbox[0]}_${d.bbox[1]}_${d.bbox[2]}_${d.bbox[3]}_${d.score}`));
|
|
2057
2087
|
return {
|
|
2058
2088
|
kind: "detections",
|
|
@@ -2085,10 +2115,11 @@ var DISPATCH = {
|
|
|
2085
2115
|
* Both paths pass through normalizeOutput() to ensure consistent format
|
|
2086
2116
|
* (e.g., classification top-K, alternates).
|
|
2087
2117
|
*/
|
|
2088
|
-
function dispatchPostprocess(output, stepDef) {
|
|
2118
|
+
function dispatchPostprocess(output, stepDef, settings) {
|
|
2089
2119
|
const fn = DISPATCH[stepDef.postprocessor];
|
|
2090
2120
|
if (!fn) throw new Error(`Unknown postprocessor type: "${stepDef.postprocessor}"`);
|
|
2091
|
-
|
|
2121
|
+
const raw = settings?.["nmsIouThreshold"];
|
|
2122
|
+
return normalizeOutput(fn(output, stepDef, typeof raw === "number" && raw > 0 ? raw : DEFAULT_NMS_IOU));
|
|
2092
2123
|
}
|
|
2093
2124
|
/**
|
|
2094
2125
|
* Normalize any StepOutput to a consistent format.
|
|
@@ -2889,6 +2920,26 @@ var NATIVE_FACE_CROP_MAX_WIDTH = 320;
|
|
|
2889
2920
|
function isEnrichmentOutput(output) {
|
|
2890
2921
|
return output.kind === "classifications" || output.kind === "embedding" || output.kind === "text";
|
|
2891
2922
|
}
|
|
2923
|
+
/**
|
|
2924
|
+
* Re-project a `crop-zone` root's detections from crop-relative coordinates
|
|
2925
|
+
* back to full-frame image space by the crop's origin — the root analogue of
|
|
2926
|
+
* the crop-roi child re-projection (`transformBboxToImageSpace`). Landmarks (if
|
|
2927
|
+
* any) are shifted by the same origin. Immutable: returns a fresh output.
|
|
2928
|
+
*/
|
|
2929
|
+
function reprojectCropZoneDetections(output, cropOrigin) {
|
|
2930
|
+
const [ox, oy] = cropOrigin;
|
|
2931
|
+
return {
|
|
2932
|
+
kind: "detections",
|
|
2933
|
+
detections: output.detections.map((det) => ({
|
|
2934
|
+
...det,
|
|
2935
|
+
bbox: transformBboxToImageSpace(det.bbox, cropOrigin),
|
|
2936
|
+
...det.landmarks ? { landmarks: det.landmarks.map((l) => ({
|
|
2937
|
+
x: l.x + ox,
|
|
2938
|
+
y: l.y + oy
|
|
2939
|
+
})) } : {}
|
|
2940
|
+
}))
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2892
2943
|
var PipelineExecutor = class {
|
|
2893
2944
|
opts;
|
|
2894
2945
|
constructor(opts) {
|
|
@@ -2942,7 +2993,7 @@ var PipelineExecutor = class {
|
|
|
2942
2993
|
*
|
|
2943
2994
|
* @returns FrameResult + optional trace
|
|
2944
2995
|
*/
|
|
2945
|
-
async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeFaceCropProvider) {
|
|
2996
|
+
async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeFaceCropProvider, cropZoneBbox) {
|
|
2946
2997
|
const startMs = Date.now();
|
|
2947
2998
|
const verbosity = runOpts?.traceVerbosity ?? "off";
|
|
2948
2999
|
const traceBuilder = new ExecutionTraceBuilder(verbosity, deviceId, imageWidth, imageHeight, this.opts.engineRuntime);
|
|
@@ -2962,9 +3013,11 @@ var PipelineExecutor = class {
|
|
|
2962
3013
|
};
|
|
2963
3014
|
for (const rootStep of tree.roots) {
|
|
2964
3015
|
if (debug) console.log(`[executor] rootStep=${rootStep.stepId} settings=${JSON.stringify(rootStep.settings ?? {})}`);
|
|
3016
|
+
const cropPrep = rootStep.definition.extractMode === "crop-zone" && cropZoneBbox !== void 0 ? await this.prepareCropZoneInput(cropZoneBbox, fullFrameJpegProvider, imageWidth, imageHeight, deviceId) : null;
|
|
2965
3017
|
const rootStart = Date.now();
|
|
2966
|
-
const
|
|
3018
|
+
const rawRootOutput = await this.executeStep(rootStep, cropPrep?.input ?? rootInput, cropPrep?.width ?? imageWidth, cropPrep?.height ?? imageHeight, "full-frame", void 0, void 0, traceBuilder, stepTimings, poolAgg);
|
|
2967
3019
|
const rootMs = Date.now() - rootStart;
|
|
3020
|
+
const rootOutput = cropPrep && rawRootOutput.kind === "detections" ? reprojectCropZoneDetections(rawRootOutput, cropPrep.origin) : rawRootOutput;
|
|
2968
3021
|
if (rootOutput.kind !== "detections") {
|
|
2969
3022
|
if (isEnrichmentOutput(rootOutput)) {
|
|
2970
3023
|
const mutable = this.synthesizeRootDetection(rootOutput, rootStep, idGen, rootMs, imageWidth, imageHeight);
|
|
@@ -3059,6 +3112,39 @@ var PipelineExecutor = class {
|
|
|
3059
3112
|
]
|
|
3060
3113
|
}, rootStep, idGen, rootStepLatencyMs);
|
|
3061
3114
|
}
|
|
3115
|
+
/**
|
|
3116
|
+
* Crop the full frame to a `crop-zone` root's absolute-pixel zone bbox and
|
|
3117
|
+
* return the cropped JPEG input + the exact origin used, so detections can be
|
|
3118
|
+
* re-projected. Returns null (⇒ full-frame fallback) when the bbox is
|
|
3119
|
+
* degenerate or the crop/encode fails — a package frame is never dropped for
|
|
3120
|
+
* a crop failure. The origin is computed to match `cropJpeg`'s clamp+round
|
|
3121
|
+
* exactly so the re-projection is pixel-accurate.
|
|
3122
|
+
*/
|
|
3123
|
+
async prepareCropZoneInput(cropZoneBbox, fullFrameJpegProvider, imageWidth, imageHeight, deviceId) {
|
|
3124
|
+
if (isBboxDegenerate(cropZoneBbox)) return null;
|
|
3125
|
+
try {
|
|
3126
|
+
const crop = await cropJpeg(await fullFrameJpegProvider(), cropZoneBbox, imageWidth, imageHeight);
|
|
3127
|
+
const ox = Math.max(0, Math.round(cropZoneBbox[0]));
|
|
3128
|
+
const oy = Math.max(0, Math.round(cropZoneBbox[1]));
|
|
3129
|
+
return {
|
|
3130
|
+
input: {
|
|
3131
|
+
kind: "jpeg",
|
|
3132
|
+
data: crop.jpeg
|
|
3133
|
+
},
|
|
3134
|
+
width: crop.width,
|
|
3135
|
+
height: crop.height,
|
|
3136
|
+
origin: [
|
|
3137
|
+
ox,
|
|
3138
|
+
oy,
|
|
3139
|
+
ox + crop.width,
|
|
3140
|
+
oy + crop.height
|
|
3141
|
+
]
|
|
3142
|
+
};
|
|
3143
|
+
} catch (err) {
|
|
3144
|
+
this.throttledWarn(`crop-zone-prep:${deviceId}`, 3e4, "crop-zone frame preparation failed — running package-detection full-frame", { error: require_dist.errMsg(err) }, deviceId);
|
|
3145
|
+
return null;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3062
3148
|
async executeStep(step, input, inputWidth, inputHeight, inputType, _parentBbox, parentClass, traceBuilder, stepTimings, poolAgg) {
|
|
3063
3149
|
const preprocessStart = Date.now();
|
|
3064
3150
|
const preprocessMs = Date.now() - preprocessStart;
|
|
@@ -3101,7 +3187,7 @@ var PipelineExecutor = class {
|
|
|
3101
3187
|
if (typeof structured.predictMs === "number") poolAgg.predictMs += structured.predictMs;
|
|
3102
3188
|
if (typeof structured.batchSize === "number" && structured.batchSize > poolAgg.batchSize) poolAgg.batchSize = structured.batchSize;
|
|
3103
3189
|
const postprocessStart = Date.now();
|
|
3104
|
-
const output = dispatchPostprocess(engineOutput, step.definition);
|
|
3190
|
+
const output = dispatchPostprocess(engineOutput, step.definition, step.settings);
|
|
3105
3191
|
const postprocessMs = Date.now() - postprocessStart;
|
|
3106
3192
|
stepTimings.push({
|
|
3107
3193
|
source: step.stepId,
|
|
@@ -3248,6 +3334,54 @@ function capitalize(s) {
|
|
|
3248
3334
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
3249
3335
|
}
|
|
3250
3336
|
//#endregion
|
|
3337
|
+
//#region src/detection-pipeline/pipeline/crop-zone.ts
|
|
3338
|
+
/**
|
|
3339
|
+
* Resolve the absolute-pixel crop bbox for a `crop-zone` root detector: the
|
|
3340
|
+
* bounding box of the UNION of every polygon referenced by the device's
|
|
3341
|
+
* ENABLED `package`-stage zone rules. Zone polygons are normalized (0..1); the
|
|
3342
|
+
* union is clamped to [0,1] then scaled to the frame's pixel dimensions.
|
|
3343
|
+
*
|
|
3344
|
+
* Returns `null` when there is nothing usable to crop to — no enabled rules, no
|
|
3345
|
+
* referenced geometry, or a degenerate result — in which case the caller falls
|
|
3346
|
+
* back to full-frame inference (behaviour identical to before the crop-zone
|
|
3347
|
+
* plumbing).
|
|
3348
|
+
*/
|
|
3349
|
+
function resolvePackageCropBbox(zones, packageRules, frameWidth, frameHeight) {
|
|
3350
|
+
if (frameWidth <= 0 || frameHeight <= 0) return null;
|
|
3351
|
+
const enabledRules = packageRules.filter((r) => r.enabled !== false);
|
|
3352
|
+
if (enabledRules.length === 0) return null;
|
|
3353
|
+
const zoneIds = /* @__PURE__ */ new Set();
|
|
3354
|
+
for (const rule of enabledRules) for (const id of rule.zoneIds) zoneIds.add(id);
|
|
3355
|
+
if (zoneIds.size === 0) return null;
|
|
3356
|
+
const zoneById = new Map(zones.map((z) => [z.id, z]));
|
|
3357
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
3358
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
3359
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
3360
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
3361
|
+
let sawPoint = false;
|
|
3362
|
+
for (const id of zoneIds) {
|
|
3363
|
+
const zone = zoneById.get(id);
|
|
3364
|
+
if (!zone || zone.polygon.length === 0) continue;
|
|
3365
|
+
for (const point of zone.polygon) {
|
|
3366
|
+
sawPoint = true;
|
|
3367
|
+
if (point.x < minX) minX = point.x;
|
|
3368
|
+
if (point.y < minY) minY = point.y;
|
|
3369
|
+
if (point.x > maxX) maxX = point.x;
|
|
3370
|
+
if (point.y > maxY) maxY = point.y;
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
if (!sawPoint) return null;
|
|
3374
|
+
const clamp01 = (v) => Math.max(0, Math.min(1, v));
|
|
3375
|
+
const bbox = [
|
|
3376
|
+
clamp01(minX) * frameWidth,
|
|
3377
|
+
clamp01(minY) * frameHeight,
|
|
3378
|
+
clamp01(maxX) * frameWidth,
|
|
3379
|
+
clamp01(maxY) * frameHeight
|
|
3380
|
+
];
|
|
3381
|
+
if (isBboxDegenerate(bbox)) return null;
|
|
3382
|
+
return bbox;
|
|
3383
|
+
}
|
|
3384
|
+
//#endregion
|
|
3251
3385
|
//#region src/detection-pipeline/pipeline/tree-builder.ts
|
|
3252
3386
|
/**
|
|
3253
3387
|
* Build an executable tree from user config.
|
|
@@ -4985,10 +5119,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4985
5119
|
const deviceOverrides = effectiveDeviceId > 0 ? await this.readDeviceStore(effectiveDeviceId) : {};
|
|
4986
5120
|
const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
|
|
4987
5121
|
const nativeFaceCropProvider = this.buildNativeFaceCropProvider(input.frameHandle);
|
|
5122
|
+
let cropZoneBbox;
|
|
5123
|
+
if (isRuntime && effectiveDeviceId > 0 && effectiveTree.roots.some((r) => r.definition.extractMode === "crop-zone")) {
|
|
5124
|
+
await this.ensureDeviceProxy(effectiveDeviceId);
|
|
5125
|
+
const proxy = this.deviceProxies.get(effectiveDeviceId);
|
|
5126
|
+
if (proxy) cropZoneBbox = resolvePackageCropBbox(proxy.state.zones.value?.zones ?? [], proxy.state.zoneRules.value?.package ?? [], imageWidth, imageHeight) ?? void 0;
|
|
5127
|
+
}
|
|
4988
5128
|
const { result, trace } = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, {
|
|
4989
5129
|
traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full",
|
|
4990
5130
|
plane: input.plane
|
|
4991
|
-
}, nativeFaceCropProvider);
|
|
5131
|
+
}, nativeFaceCropProvider, cropZoneBbox);
|
|
4992
5132
|
if (isRuntime) {
|
|
4993
5133
|
if (trace && this.eventBus) this.eventBus.emit(require_dist.createEvent(require_dist.EventCategory.PipelineTrace, {
|
|
4994
5134
|
type: "device",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as __require } from "../chunk-BdkLduGY.mjs";
|
|
2
|
-
import { $ as sleep, A as pipelineExecutorCapability, B as errMsg, C as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, S as detectionPipelineCapability, V as BaseAddon, X as parseJsonUnknown, b as defaultDeviceFor$1, dt as EventCategory, k as nodePin, m as YAMNET_TO_MACRO, t as APPLE_SA_TO_MACRO } from "../dist-
|
|
3
|
-
import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-
|
|
2
|
+
import { $ as sleep, A as pipelineExecutorCapability, B as errMsg, C as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, S as detectionPipelineCapability, V as BaseAddon, X as parseJsonUnknown, b as defaultDeviceFor$1, dt as EventCategory, k as nodePin, m as YAMNET_TO_MACRO, t as APPLE_SA_TO_MACRO } from "../dist-D6tHM9GA.mjs";
|
|
3
|
+
import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-Dbsm64_I.mjs";
|
|
4
4
|
import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
|
|
5
5
|
import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
@@ -308,7 +308,7 @@ function projectNodeEngine(store, nodeId) {
|
|
|
308
308
|
//#endregion
|
|
309
309
|
//#region src/detection-pipeline/default-detection-model.ts
|
|
310
310
|
/** The object-detection step id — the only slot this resolver applies to. */
|
|
311
|
-
var OBJECT_DETECTION_STEP_ID = "object-detection";
|
|
311
|
+
var OBJECT_DETECTION_STEP_ID$1 = "object-detection";
|
|
312
312
|
/**
|
|
313
313
|
* Balanced default object-detection model per accelerator class (measured on
|
|
314
314
|
* COCO val2017). `'cpu'` maps to `null` → the caller substitutes the step's
|
|
@@ -368,7 +368,7 @@ function classifyAccelerator(backend, device) {
|
|
|
368
368
|
function resolveDefaultDetectionModel(backend, device, format, getStepDef = getStepDefinition) {
|
|
369
369
|
let def;
|
|
370
370
|
try {
|
|
371
|
-
def = getStepDef(OBJECT_DETECTION_STEP_ID);
|
|
371
|
+
def = getStepDef(OBJECT_DETECTION_STEP_ID$1);
|
|
372
372
|
} catch {
|
|
373
373
|
return "yolo26n";
|
|
374
374
|
}
|
|
@@ -943,6 +943,7 @@ function serializeModelConfig(config) {
|
|
|
943
943
|
postprocessor: config.postprocessor,
|
|
944
944
|
confidence: config.confidence
|
|
945
945
|
};
|
|
946
|
+
if (config.nmsIouThreshold !== void 0) result["nmsIouThreshold"] = config.nmsIouThreshold;
|
|
946
947
|
if (config.inputWidth !== void 0) result["inputWidth"] = config.inputWidth;
|
|
947
948
|
if (config.inputHeight !== void 0) result["inputHeight"] = config.inputHeight;
|
|
948
949
|
if (config.inputChannels !== void 0) result["inputChannels"] = config.inputChannels;
|
|
@@ -1250,6 +1251,31 @@ var RUNTIME_TO_FORMAT = {
|
|
|
1250
1251
|
onnxruntime: "onnx",
|
|
1251
1252
|
edgetpu: "tflite"
|
|
1252
1253
|
};
|
|
1254
|
+
/** The step whose detections are re-filtered downstream by the executor's
|
|
1255
|
+
* per-macro `minConfidence*` sliders (`matchesMacroFilter`). Its pool-side
|
|
1256
|
+
* floor must stay BELOW those sliders or it silently pre-empts them. */
|
|
1257
|
+
var OBJECT_DETECTION_STEP_ID = "object-detection";
|
|
1258
|
+
/**
|
|
1259
|
+
* Pool-side confidence FLOOR for the object-detection step (#4 foot-gun fix).
|
|
1260
|
+
*
|
|
1261
|
+
* The pool used to receive `def.defaultConfidence` (0.5) as its floor and drop
|
|
1262
|
+
* every box < 0.5 BEFORE the executor's per-macro `minConfidence*` sliders run —
|
|
1263
|
+
* so a slider set below 0.5 was a silent no-op. Lower the pool floor to a small
|
|
1264
|
+
* constant so the operator-visible per-macro sliders are the authoritative
|
|
1265
|
+
* filter across their full 0–1 range. The per-macro slider DEFAULTS stay at 0.5
|
|
1266
|
+
* (see `step-definitions.ts` getConfigSchema), so default UI behaviour is
|
|
1267
|
+
* unchanged; only sub-0.5 sliders start having an effect. Non-object-detection
|
|
1268
|
+
* steps keep their own `def.defaultConfidence` floor (no per-macro re-filter). */
|
|
1269
|
+
var OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR = .05;
|
|
1270
|
+
/**
|
|
1271
|
+
* Resolve the pool-side confidence FLOOR for a step (#4). The object-detection
|
|
1272
|
+
* step gets the small {@link OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR} so its
|
|
1273
|
+
* per-macro `minConfidence*` sliders (applied downstream by the executor) are
|
|
1274
|
+
* the authoritative filter; every other step keeps its own `defaultConfidence`.
|
|
1275
|
+
*/
|
|
1276
|
+
function resolvePoolConfidenceFloor(stepId, defaultConfidence) {
|
|
1277
|
+
return stepId === OBJECT_DETECTION_STEP_ID ? OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR : defaultConfidence;
|
|
1278
|
+
}
|
|
1253
1279
|
var EngineFactory = class {
|
|
1254
1280
|
pool = null;
|
|
1255
1281
|
poolManager = null;
|
|
@@ -1433,7 +1459,8 @@ var EngineFactory = class {
|
|
|
1433
1459
|
inputChannels,
|
|
1434
1460
|
preprocessMode: modelEntry.preprocessMode ?? "letterbox",
|
|
1435
1461
|
postprocessor: modelEntry.postprocessor ?? def.postprocessor,
|
|
1436
|
-
confidence: def.defaultConfidence,
|
|
1462
|
+
confidence: resolvePoolConfidenceFloor(stepId, def.defaultConfidence),
|
|
1463
|
+
nmsIouThreshold: this.opts.nmsIouThreshold ?? .45,
|
|
1437
1464
|
labels,
|
|
1438
1465
|
charset: def.charset,
|
|
1439
1466
|
numClasses: labels?.length,
|
|
@@ -1552,6 +1579,9 @@ var EngineProvisioner = class {
|
|
|
1552
1579
|
};
|
|
1553
1580
|
//#endregion
|
|
1554
1581
|
//#region src/detection-pipeline/postprocess/dispatch.ts
|
|
1582
|
+
/** Detector NMS IoU fallback — the historical hardcoded literal, used when the
|
|
1583
|
+
* step carries no `nmsIouThreshold` setting (unset == today). */
|
|
1584
|
+
var DEFAULT_NMS_IOU = .45;
|
|
1555
1585
|
var VALID_KINDS = new Set([
|
|
1556
1586
|
"detections",
|
|
1557
1587
|
"classifications",
|
|
@@ -1579,7 +1609,7 @@ function tryStructured(output) {
|
|
|
1579
1609
|
if (!isStepOutput(output.structured)) throw new Error(`Python pool returned structured output with unexpected kind: ${JSON.stringify(output.structured["kind"])}`);
|
|
1580
1610
|
return output.structured;
|
|
1581
1611
|
}
|
|
1582
|
-
function postprocessYolo(output, stepDef) {
|
|
1612
|
+
function postprocessYolo(output, stepDef, nmsIouThreshold) {
|
|
1583
1613
|
const structured = tryStructured(output);
|
|
1584
1614
|
if (structured) return structured;
|
|
1585
1615
|
const tensor = output.tensor;
|
|
@@ -1628,7 +1658,7 @@ function postprocessYolo(output, stepDef) {
|
|
|
1628
1658
|
}
|
|
1629
1659
|
return {
|
|
1630
1660
|
kind: "detections",
|
|
1631
|
-
detections: simpleNms(dets,
|
|
1661
|
+
detections: simpleNms(dets, nmsIouThreshold)
|
|
1632
1662
|
};
|
|
1633
1663
|
}
|
|
1634
1664
|
function postprocessSsd(output, _stepDef) {
|
|
@@ -1636,7 +1666,7 @@ function postprocessSsd(output, _stepDef) {
|
|
|
1636
1666
|
if (structured) return structured;
|
|
1637
1667
|
throw new Error("SSD postprocessing runs in the Python inference pool (ssd.py); no TypeScript raw-tensor path.");
|
|
1638
1668
|
}
|
|
1639
|
-
function postprocessScrfd(output, stepDef) {
|
|
1669
|
+
function postprocessScrfd(output, stepDef, nmsIouThreshold) {
|
|
1640
1670
|
const structured = tryStructured(output);
|
|
1641
1671
|
if (structured) return structured;
|
|
1642
1672
|
if (!output.tensors) throw new Error("SCRFD postprocessor: no tensors in engine output");
|
|
@@ -1714,7 +1744,7 @@ function postprocessScrfd(output, stepDef) {
|
|
|
1714
1744
|
score: c.score,
|
|
1715
1745
|
bbox: c.bbox,
|
|
1716
1746
|
...c.landmarks ? { landmarks: c.landmarks } : {}
|
|
1717
|
-
})),
|
|
1747
|
+
})), nmsIouThreshold)
|
|
1718
1748
|
};
|
|
1719
1749
|
}
|
|
1720
1750
|
/** Match ONNX output tensors to SCRFD strides by expected anchor count. */
|
|
@@ -1919,7 +1949,7 @@ function postprocessSaliency(output, _stepDef) {
|
|
|
1919
1949
|
maskHeight: side
|
|
1920
1950
|
};
|
|
1921
1951
|
}
|
|
1922
|
-
function postprocessYoloSeg(output, stepDef) {
|
|
1952
|
+
function postprocessYoloSeg(output, stepDef, nmsIouThreshold) {
|
|
1923
1953
|
const structured = tryStructured(output);
|
|
1924
1954
|
if (structured) return structured;
|
|
1925
1955
|
const tensors = output.tensors;
|
|
@@ -2044,7 +2074,7 @@ function postprocessYoloSeg(output, stepDef) {
|
|
|
2044
2074
|
class: d.class,
|
|
2045
2075
|
score: d.score,
|
|
2046
2076
|
bbox: d.bbox
|
|
2047
|
-
})),
|
|
2077
|
+
})), nmsIouThreshold);
|
|
2048
2078
|
const keptSet = new Set(keptBoxes.map((d) => `${d.bbox[0]}_${d.bbox[1]}_${d.bbox[2]}_${d.bbox[3]}_${d.score}`));
|
|
2049
2079
|
return {
|
|
2050
2080
|
kind: "detections",
|
|
@@ -2077,10 +2107,11 @@ var DISPATCH = {
|
|
|
2077
2107
|
* Both paths pass through normalizeOutput() to ensure consistent format
|
|
2078
2108
|
* (e.g., classification top-K, alternates).
|
|
2079
2109
|
*/
|
|
2080
|
-
function dispatchPostprocess(output, stepDef) {
|
|
2110
|
+
function dispatchPostprocess(output, stepDef, settings) {
|
|
2081
2111
|
const fn = DISPATCH[stepDef.postprocessor];
|
|
2082
2112
|
if (!fn) throw new Error(`Unknown postprocessor type: "${stepDef.postprocessor}"`);
|
|
2083
|
-
|
|
2113
|
+
const raw = settings?.["nmsIouThreshold"];
|
|
2114
|
+
return normalizeOutput(fn(output, stepDef, typeof raw === "number" && raw > 0 ? raw : DEFAULT_NMS_IOU));
|
|
2084
2115
|
}
|
|
2085
2116
|
/**
|
|
2086
2117
|
* Normalize any StepOutput to a consistent format.
|
|
@@ -2881,6 +2912,26 @@ var NATIVE_FACE_CROP_MAX_WIDTH = 320;
|
|
|
2881
2912
|
function isEnrichmentOutput(output) {
|
|
2882
2913
|
return output.kind === "classifications" || output.kind === "embedding" || output.kind === "text";
|
|
2883
2914
|
}
|
|
2915
|
+
/**
|
|
2916
|
+
* Re-project a `crop-zone` root's detections from crop-relative coordinates
|
|
2917
|
+
* back to full-frame image space by the crop's origin — the root analogue of
|
|
2918
|
+
* the crop-roi child re-projection (`transformBboxToImageSpace`). Landmarks (if
|
|
2919
|
+
* any) are shifted by the same origin. Immutable: returns a fresh output.
|
|
2920
|
+
*/
|
|
2921
|
+
function reprojectCropZoneDetections(output, cropOrigin) {
|
|
2922
|
+
const [ox, oy] = cropOrigin;
|
|
2923
|
+
return {
|
|
2924
|
+
kind: "detections",
|
|
2925
|
+
detections: output.detections.map((det) => ({
|
|
2926
|
+
...det,
|
|
2927
|
+
bbox: transformBboxToImageSpace(det.bbox, cropOrigin),
|
|
2928
|
+
...det.landmarks ? { landmarks: det.landmarks.map((l) => ({
|
|
2929
|
+
x: l.x + ox,
|
|
2930
|
+
y: l.y + oy
|
|
2931
|
+
})) } : {}
|
|
2932
|
+
}))
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2884
2935
|
var PipelineExecutor = class {
|
|
2885
2936
|
opts;
|
|
2886
2937
|
constructor(opts) {
|
|
@@ -2934,7 +2985,7 @@ var PipelineExecutor = class {
|
|
|
2934
2985
|
*
|
|
2935
2986
|
* @returns FrameResult + optional trace
|
|
2936
2987
|
*/
|
|
2937
|
-
async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeFaceCropProvider) {
|
|
2988
|
+
async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeFaceCropProvider, cropZoneBbox) {
|
|
2938
2989
|
const startMs = Date.now();
|
|
2939
2990
|
const verbosity = runOpts?.traceVerbosity ?? "off";
|
|
2940
2991
|
const traceBuilder = new ExecutionTraceBuilder(verbosity, deviceId, imageWidth, imageHeight, this.opts.engineRuntime);
|
|
@@ -2954,9 +3005,11 @@ var PipelineExecutor = class {
|
|
|
2954
3005
|
};
|
|
2955
3006
|
for (const rootStep of tree.roots) {
|
|
2956
3007
|
if (debug) console.log(`[executor] rootStep=${rootStep.stepId} settings=${JSON.stringify(rootStep.settings ?? {})}`);
|
|
3008
|
+
const cropPrep = rootStep.definition.extractMode === "crop-zone" && cropZoneBbox !== void 0 ? await this.prepareCropZoneInput(cropZoneBbox, fullFrameJpegProvider, imageWidth, imageHeight, deviceId) : null;
|
|
2957
3009
|
const rootStart = Date.now();
|
|
2958
|
-
const
|
|
3010
|
+
const rawRootOutput = await this.executeStep(rootStep, cropPrep?.input ?? rootInput, cropPrep?.width ?? imageWidth, cropPrep?.height ?? imageHeight, "full-frame", void 0, void 0, traceBuilder, stepTimings, poolAgg);
|
|
2959
3011
|
const rootMs = Date.now() - rootStart;
|
|
3012
|
+
const rootOutput = cropPrep && rawRootOutput.kind === "detections" ? reprojectCropZoneDetections(rawRootOutput, cropPrep.origin) : rawRootOutput;
|
|
2960
3013
|
if (rootOutput.kind !== "detections") {
|
|
2961
3014
|
if (isEnrichmentOutput(rootOutput)) {
|
|
2962
3015
|
const mutable = this.synthesizeRootDetection(rootOutput, rootStep, idGen, rootMs, imageWidth, imageHeight);
|
|
@@ -3051,6 +3104,39 @@ var PipelineExecutor = class {
|
|
|
3051
3104
|
]
|
|
3052
3105
|
}, rootStep, idGen, rootStepLatencyMs);
|
|
3053
3106
|
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Crop the full frame to a `crop-zone` root's absolute-pixel zone bbox and
|
|
3109
|
+
* return the cropped JPEG input + the exact origin used, so detections can be
|
|
3110
|
+
* re-projected. Returns null (⇒ full-frame fallback) when the bbox is
|
|
3111
|
+
* degenerate or the crop/encode fails — a package frame is never dropped for
|
|
3112
|
+
* a crop failure. The origin is computed to match `cropJpeg`'s clamp+round
|
|
3113
|
+
* exactly so the re-projection is pixel-accurate.
|
|
3114
|
+
*/
|
|
3115
|
+
async prepareCropZoneInput(cropZoneBbox, fullFrameJpegProvider, imageWidth, imageHeight, deviceId) {
|
|
3116
|
+
if (isBboxDegenerate(cropZoneBbox)) return null;
|
|
3117
|
+
try {
|
|
3118
|
+
const crop = await cropJpeg(await fullFrameJpegProvider(), cropZoneBbox, imageWidth, imageHeight);
|
|
3119
|
+
const ox = Math.max(0, Math.round(cropZoneBbox[0]));
|
|
3120
|
+
const oy = Math.max(0, Math.round(cropZoneBbox[1]));
|
|
3121
|
+
return {
|
|
3122
|
+
input: {
|
|
3123
|
+
kind: "jpeg",
|
|
3124
|
+
data: crop.jpeg
|
|
3125
|
+
},
|
|
3126
|
+
width: crop.width,
|
|
3127
|
+
height: crop.height,
|
|
3128
|
+
origin: [
|
|
3129
|
+
ox,
|
|
3130
|
+
oy,
|
|
3131
|
+
ox + crop.width,
|
|
3132
|
+
oy + crop.height
|
|
3133
|
+
]
|
|
3134
|
+
};
|
|
3135
|
+
} catch (err) {
|
|
3136
|
+
this.throttledWarn(`crop-zone-prep:${deviceId}`, 3e4, "crop-zone frame preparation failed — running package-detection full-frame", { error: errMsg(err) }, deviceId);
|
|
3137
|
+
return null;
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3054
3140
|
async executeStep(step, input, inputWidth, inputHeight, inputType, _parentBbox, parentClass, traceBuilder, stepTimings, poolAgg) {
|
|
3055
3141
|
const preprocessStart = Date.now();
|
|
3056
3142
|
const preprocessMs = Date.now() - preprocessStart;
|
|
@@ -3093,7 +3179,7 @@ var PipelineExecutor = class {
|
|
|
3093
3179
|
if (typeof structured.predictMs === "number") poolAgg.predictMs += structured.predictMs;
|
|
3094
3180
|
if (typeof structured.batchSize === "number" && structured.batchSize > poolAgg.batchSize) poolAgg.batchSize = structured.batchSize;
|
|
3095
3181
|
const postprocessStart = Date.now();
|
|
3096
|
-
const output = dispatchPostprocess(engineOutput, step.definition);
|
|
3182
|
+
const output = dispatchPostprocess(engineOutput, step.definition, step.settings);
|
|
3097
3183
|
const postprocessMs = Date.now() - postprocessStart;
|
|
3098
3184
|
stepTimings.push({
|
|
3099
3185
|
source: step.stepId,
|
|
@@ -3240,6 +3326,54 @@ function capitalize(s) {
|
|
|
3240
3326
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
3241
3327
|
}
|
|
3242
3328
|
//#endregion
|
|
3329
|
+
//#region src/detection-pipeline/pipeline/crop-zone.ts
|
|
3330
|
+
/**
|
|
3331
|
+
* Resolve the absolute-pixel crop bbox for a `crop-zone` root detector: the
|
|
3332
|
+
* bounding box of the UNION of every polygon referenced by the device's
|
|
3333
|
+
* ENABLED `package`-stage zone rules. Zone polygons are normalized (0..1); the
|
|
3334
|
+
* union is clamped to [0,1] then scaled to the frame's pixel dimensions.
|
|
3335
|
+
*
|
|
3336
|
+
* Returns `null` when there is nothing usable to crop to — no enabled rules, no
|
|
3337
|
+
* referenced geometry, or a degenerate result — in which case the caller falls
|
|
3338
|
+
* back to full-frame inference (behaviour identical to before the crop-zone
|
|
3339
|
+
* plumbing).
|
|
3340
|
+
*/
|
|
3341
|
+
function resolvePackageCropBbox(zones, packageRules, frameWidth, frameHeight) {
|
|
3342
|
+
if (frameWidth <= 0 || frameHeight <= 0) return null;
|
|
3343
|
+
const enabledRules = packageRules.filter((r) => r.enabled !== false);
|
|
3344
|
+
if (enabledRules.length === 0) return null;
|
|
3345
|
+
const zoneIds = /* @__PURE__ */ new Set();
|
|
3346
|
+
for (const rule of enabledRules) for (const id of rule.zoneIds) zoneIds.add(id);
|
|
3347
|
+
if (zoneIds.size === 0) return null;
|
|
3348
|
+
const zoneById = new Map(zones.map((z) => [z.id, z]));
|
|
3349
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
3350
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
3351
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
3352
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
3353
|
+
let sawPoint = false;
|
|
3354
|
+
for (const id of zoneIds) {
|
|
3355
|
+
const zone = zoneById.get(id);
|
|
3356
|
+
if (!zone || zone.polygon.length === 0) continue;
|
|
3357
|
+
for (const point of zone.polygon) {
|
|
3358
|
+
sawPoint = true;
|
|
3359
|
+
if (point.x < minX) minX = point.x;
|
|
3360
|
+
if (point.y < minY) minY = point.y;
|
|
3361
|
+
if (point.x > maxX) maxX = point.x;
|
|
3362
|
+
if (point.y > maxY) maxY = point.y;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
if (!sawPoint) return null;
|
|
3366
|
+
const clamp01 = (v) => Math.max(0, Math.min(1, v));
|
|
3367
|
+
const bbox = [
|
|
3368
|
+
clamp01(minX) * frameWidth,
|
|
3369
|
+
clamp01(minY) * frameHeight,
|
|
3370
|
+
clamp01(maxX) * frameWidth,
|
|
3371
|
+
clamp01(maxY) * frameHeight
|
|
3372
|
+
];
|
|
3373
|
+
if (isBboxDegenerate(bbox)) return null;
|
|
3374
|
+
return bbox;
|
|
3375
|
+
}
|
|
3376
|
+
//#endregion
|
|
3243
3377
|
//#region src/detection-pipeline/pipeline/tree-builder.ts
|
|
3244
3378
|
/**
|
|
3245
3379
|
* Build an executable tree from user config.
|
|
@@ -4977,10 +5111,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4977
5111
|
const deviceOverrides = effectiveDeviceId > 0 ? await this.readDeviceStore(effectiveDeviceId) : {};
|
|
4978
5112
|
const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
|
|
4979
5113
|
const nativeFaceCropProvider = this.buildNativeFaceCropProvider(input.frameHandle);
|
|
5114
|
+
let cropZoneBbox;
|
|
5115
|
+
if (isRuntime && effectiveDeviceId > 0 && effectiveTree.roots.some((r) => r.definition.extractMode === "crop-zone")) {
|
|
5116
|
+
await this.ensureDeviceProxy(effectiveDeviceId);
|
|
5117
|
+
const proxy = this.deviceProxies.get(effectiveDeviceId);
|
|
5118
|
+
if (proxy) cropZoneBbox = resolvePackageCropBbox(proxy.state.zones.value?.zones ?? [], proxy.state.zoneRules.value?.package ?? [], imageWidth, imageHeight) ?? void 0;
|
|
5119
|
+
}
|
|
4980
5120
|
const { result, trace } = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, {
|
|
4981
5121
|
traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full",
|
|
4982
5122
|
plane: input.plane
|
|
4983
|
-
}, nativeFaceCropProvider);
|
|
5123
|
+
}, nativeFaceCropProvider, cropZoneBbox);
|
|
4984
5124
|
if (isRuntime) {
|
|
4985
5125
|
if (trace && this.eventBus) this.eventBus.emit(createEvent(EventCategory.PipelineTrace, {
|
|
4986
5126
|
type: "device",
|