@camstack/addon-pipeline 1.2.4 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +499 -30
- package/dist/detection-pipeline/index.mjs +493 -24
- package/dist/{dist-D7-LF079.js → dist-Bh0H8jM6.js} +61 -4
- package/dist/{dist-DVjpngiC.mjs → dist-Bw_V5L0r.mjs} +61 -4
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +19 -9
- package/dist/pipeline-runner/index.mjs +19 -9
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/{step-definitions-BPwND8Zg.mjs → step-definitions-aY4D9Loy.mjs} +241 -116
- package/dist/{step-definitions-BfXdDokR.js → step-definitions-xxdpJX9-.js} +241 -116
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DHZ9LbMC.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DX-Ef8CP.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-gchR4WlX.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BdxY9zld.mjs +26 -0
- package/dist/stream-broker/{hostInit-CpoOj-Zh.mjs → hostInit-DUL71Vjq.mjs} +2 -2
- package/dist/stream-broker/index.js +1 -1
- package/dist/stream-broker/index.mjs +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-L6rscpzL.js → MaskShapeCanvas-DI4BY7W2-CGWEeRkz.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CK9BVkOT.js → MotionZonesSettings-NcxxQN8r-4YbTa3Wp.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CNcYwFLg.js → PrivacyMaskSettings-APgPLF7p-DL_tAs9-.js} +1 -1
- package/embed-dist/assets/{index-CBhJRpiC.js → index-CvBwcYl_.js} +13 -13
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/python/postprocessors/ctc.py +228 -4
- package/python/postprocessors/test_ctc.py +312 -0
- package/python/postprocessors/testdata/ctc_degenerate_lowres_logits.npy.gz.b64 +1 -0
- package/python/postprocessors/testdata/ctc_dwro309_logits.npy.gz.b64 +1 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CQC_f4Yv.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-5Zc9wj-A.mjs +0 -26
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as __require } from "../chunk-BdkLduGY.mjs";
|
|
2
|
-
import { B as supportedRuntimes$1, C as detectionPipelineCapability, H as errMsg, J as hydrateSchema, L as runtimeDevices$1, M as pipelineExecutorCapability, Q as parseJsonUnknown, T as evaluateZoneRules, U as BaseAddon, dt as string, ft as union, h as YAMNET_TO_MACRO, j as nodePin, lt as object, pt as EventCategory, q as createEvent, rt as array, s as DEVICE_BACKEND_TO_FORMAT, t as APPLE_SA_TO_MACRO, tt as sleep, w as enumerateInferenceDevices, x as defaultDeviceFor$1 } 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 { B as supportedRuntimes$1, C as detectionPipelineCapability, H as errMsg, J as hydrateSchema, L as runtimeDevices$1, M as pipelineExecutorCapability, Q as parseJsonUnknown, T as evaluateZoneRules, U as BaseAddon, dt as string, ft as union, h as YAMNET_TO_MACRO, j as nodePin, lt as object, pt as EventCategory, q as createEvent, rt as array, s as DEVICE_BACKEND_TO_FORMAT, t as APPLE_SA_TO_MACRO, tt as sleep, w as enumerateInferenceDevices, x as defaultDeviceFor$1 } from "../dist-Bw_V5L0r.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-aY4D9Loy.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";
|
|
@@ -213,6 +213,43 @@ function normalizeEngineNodeId(rawNodeId) {
|
|
|
213
213
|
return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
|
|
214
214
|
}
|
|
215
215
|
//#endregion
|
|
216
|
+
//#region src/detection-pipeline/pipeline/native-crop-compose.ts
|
|
217
|
+
/** Clamp `v` into `[lo, hi]`. */
|
|
218
|
+
function clamp(v, lo, hi) {
|
|
219
|
+
if (v < lo) return lo;
|
|
220
|
+
if (v > hi) return hi;
|
|
221
|
+
return v;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Compose a child `roi` given in the parent CROP's normalized space into the
|
|
225
|
+
* FRAME's normalized space, given the crop's frame-space pixel rectangle and the
|
|
226
|
+
* frame dimensions. The result is clamped to the unit square (origin in
|
|
227
|
+
* `[0,1]`, extent bounded so `x + w ≤ 1` and `y + h ≤ 1`). Returns `null` for
|
|
228
|
+
* degenerate inputs (non-finite frame dims, empty crop rect, or an ROI that
|
|
229
|
+
* clamps to zero area) so the caller falls back to the downscaled tile crop.
|
|
230
|
+
*
|
|
231
|
+
* Pure — never mutates its arguments.
|
|
232
|
+
*/
|
|
233
|
+
function composeCropRoiToFrameNorm(roi, cropFrameSpace, frameWidth, frameHeight) {
|
|
234
|
+
if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
|
|
235
|
+
if (!(cropFrameSpace.w > 0) || !(cropFrameSpace.h > 0)) return null;
|
|
236
|
+
const fx = (cropFrameSpace.x + roi.x * cropFrameSpace.w) / frameWidth;
|
|
237
|
+
const fy = (cropFrameSpace.y + roi.y * cropFrameSpace.h) / frameHeight;
|
|
238
|
+
const fw = roi.w * cropFrameSpace.w / frameWidth;
|
|
239
|
+
const fh = roi.h * cropFrameSpace.h / frameHeight;
|
|
240
|
+
const x = clamp(fx, 0, 1);
|
|
241
|
+
const y = clamp(fy, 0, 1);
|
|
242
|
+
const w = clamp(fw, 0, 1 - x);
|
|
243
|
+
const h = clamp(fh, 0, 1 - y);
|
|
244
|
+
if (!(w > 0) || !(h > 0)) return null;
|
|
245
|
+
return {
|
|
246
|
+
x,
|
|
247
|
+
y,
|
|
248
|
+
w,
|
|
249
|
+
h
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
216
253
|
//#region src/detection-pipeline/default-detection-model.ts
|
|
217
254
|
/** The object-detection step id — the only slot this resolver applies to. */
|
|
218
255
|
var OBJECT_DETECTION_STEP_ID$1 = "object-detection";
|
|
@@ -901,6 +938,9 @@ function serializeModelConfig(config) {
|
|
|
901
938
|
if (config.inputChannels !== void 0) result["inputChannels"] = config.inputChannels;
|
|
902
939
|
if (config.labels) result["labels"] = config.labels;
|
|
903
940
|
if (config.charset) result["charset"] = config.charset;
|
|
941
|
+
if (config.plateRegion !== void 0) result["plateRegion"] = config.plateRegion;
|
|
942
|
+
if (config.minTextLength !== void 0) result["minTextLength"] = config.minTextLength;
|
|
943
|
+
if (config.minTextConfidence !== void 0) result["minTextConfidence"] = config.minTextConfidence;
|
|
904
944
|
if (config.numClasses) result["numClasses"] = config.numClasses;
|
|
905
945
|
if (config.strides) result["strides"] = config.strides;
|
|
906
946
|
if (config.maskThreshold !== void 0) result["maskThreshold"] = config.maskThreshold;
|
|
@@ -1435,6 +1475,9 @@ var EngineFactory = class {
|
|
|
1435
1475
|
nmsIouThreshold: this.opts.nmsIouThreshold ?? .45,
|
|
1436
1476
|
labels,
|
|
1437
1477
|
charset: def.charset,
|
|
1478
|
+
plateRegion: def.plateRegion,
|
|
1479
|
+
minTextLength: def.minTextLength,
|
|
1480
|
+
minTextConfidence: def.minTextConfidence,
|
|
1438
1481
|
numClasses: labels?.length,
|
|
1439
1482
|
strides: def.postprocessor === "scrfd" ? [
|
|
1440
1483
|
8,
|
|
@@ -1639,6 +1682,14 @@ var EngineProvisioner = class {
|
|
|
1639
1682
|
/** Detector NMS IoU fallback — the historical hardcoded literal, used when the
|
|
1640
1683
|
* step carries no `nmsIouThreshold` setting (unset == today). */
|
|
1641
1684
|
var DEFAULT_NMS_IOU = .45;
|
|
1685
|
+
/**
|
|
1686
|
+
* Region plate grammars for the TS fallback CTC decoder (nodejs+onnx path).
|
|
1687
|
+
* VALIDATION-ONLY: this mirror annotates `formatValid` but does NOT rescore
|
|
1688
|
+
* confusables — the confusable repair needs the aligned per-timestep softmax
|
|
1689
|
+
* that only the Python path retains (see `python/postprocessors/ctc.py`). Keep
|
|
1690
|
+
* the pattern set in sync with `PLATE_GRAMMARS` there. `'off'`/unset disables it.
|
|
1691
|
+
*/
|
|
1692
|
+
var TS_PLATE_GRAMMARS = { DE: /^[A-ZÄÖÜ]{1,3}[- ]?[A-Z]{1,2}[- ]?[0-9]{1,4}[EH]?$/ };
|
|
1642
1693
|
var VALID_KINDS = new Set([
|
|
1643
1694
|
"detections",
|
|
1644
1695
|
"classifications",
|
|
@@ -1915,9 +1966,20 @@ function postprocessCtc(output, stepDef) {
|
|
|
1915
1966
|
if (bestIdx !== 0 && bestIdx !== prev) chars.push(charset[bestIdx] ?? "");
|
|
1916
1967
|
prev = bestIdx;
|
|
1917
1968
|
}
|
|
1969
|
+
const text = chars.join("");
|
|
1970
|
+
const region = stepDef.plateRegion;
|
|
1971
|
+
if (region !== void 0 && region !== "off") {
|
|
1972
|
+
const grammar = TS_PLATE_GRAMMARS[region];
|
|
1973
|
+
if (grammar) return {
|
|
1974
|
+
kind: "text",
|
|
1975
|
+
text,
|
|
1976
|
+
confidence: seqLen > 0 ? totalScore / seqLen : 0,
|
|
1977
|
+
formatValid: grammar.test(text)
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1918
1980
|
return {
|
|
1919
1981
|
kind: "text",
|
|
1920
|
-
text
|
|
1982
|
+
text,
|
|
1921
1983
|
confidence: seqLen > 0 ? totalScore / seqLen : 0
|
|
1922
1984
|
};
|
|
1923
1985
|
}
|
|
@@ -2560,6 +2622,353 @@ async function alignFaceCrop(fullFrameJpeg, faceBbox, landmarksImageSpace, image
|
|
|
2560
2622
|
};
|
|
2561
2623
|
}
|
|
2562
2624
|
//#endregion
|
|
2625
|
+
//#region src/detection-pipeline/pipeline/native-child-crop.ts
|
|
2626
|
+
/**
|
|
2627
|
+
* NATIVE-resolution crop of a LEAF crop-child ROI (plate-ocr, leaf classifiers)
|
|
2628
|
+
* — the plate-side mirror of the face path's {@link ./face-align.buildAlignedFaceCrop}.
|
|
2629
|
+
*
|
|
2630
|
+
* On the detail plane the executor runs a child model against a parent crop
|
|
2631
|
+
* (e.g. the native vehicle crop). A leaf child's ROI (`parentDetection.bbox`,
|
|
2632
|
+
* in the parent crop's PIXEL space) is what the child model reads. Cutting it
|
|
2633
|
+
* from the parent tile with {@link ./crop-utils.cropJpeg} bounds the child input
|
|
2634
|
+
* to the tile's resolution — fatal for OCR on a small, distant plate. When a
|
|
2635
|
+
* native-crop provider is bound to the frame, we instead resolve the SAME ROI
|
|
2636
|
+
* straight from the frame's retained NATIVE surface (the provider re-composes
|
|
2637
|
+
* the crop-normalized ROI into frame space — see {@link ./native-crop-compose}),
|
|
2638
|
+
* giving the model native pixels. A miss/degenerate returns `null` so the caller
|
|
2639
|
+
* falls back to the existing tile crop (never an upscale — native or the tile).
|
|
2640
|
+
*/
|
|
2641
|
+
/**
|
|
2642
|
+
* Generous native-width cap for a leaf child crop. A plate ROI is a few hundred
|
|
2643
|
+
* native pixels wide, so this never binds for plates (effectively uncapped —
|
|
2644
|
+
* the quality path the task requires); it only bounds a pathologically large
|
|
2645
|
+
* whole-object classifier ROI so the in-process native fetch/encode stays cheap.
|
|
2646
|
+
*/
|
|
2647
|
+
var NATIVE_CHILD_CROP_MAX_WIDTH = 1920;
|
|
2648
|
+
/**
|
|
2649
|
+
* Resolve `bbox` (a parent-crop PIXEL rectangle `[x1,y1,x2,y2]`) at native
|
|
2650
|
+
* resolution via `provider`, returning a JPEG crop + its native dimensions.
|
|
2651
|
+
* Returns `null` on a degenerate ROI or any provider miss (so the caller uses
|
|
2652
|
+
* the downscaled tile crop instead). Pure w.r.t. its inputs; the only effect is
|
|
2653
|
+
* the sharp encode of the returned pixels.
|
|
2654
|
+
*/
|
|
2655
|
+
async function nativeChildCrop(provider, bbox, imageWidth, imageHeight, maxWidth) {
|
|
2656
|
+
if (!(imageWidth > 0) || !(imageHeight > 0)) return null;
|
|
2657
|
+
const x1 = Math.max(0, bbox[0]);
|
|
2658
|
+
const y1 = Math.max(0, bbox[1]);
|
|
2659
|
+
const x2 = Math.min(imageWidth, bbox[2]);
|
|
2660
|
+
const y2 = Math.min(imageHeight, bbox[3]);
|
|
2661
|
+
const w = x2 - x1;
|
|
2662
|
+
const h = y2 - y1;
|
|
2663
|
+
if (w < 1 || h < 1) return null;
|
|
2664
|
+
const native = await provider({
|
|
2665
|
+
x: x1 / imageWidth,
|
|
2666
|
+
y: y1 / imageHeight,
|
|
2667
|
+
w: w / imageWidth,
|
|
2668
|
+
h: h / imageHeight
|
|
2669
|
+
}, maxWidth);
|
|
2670
|
+
if (!native || native.width < 2 || native.height < 2 || native.bytes.length < native.width * native.height * 3) return null;
|
|
2671
|
+
return {
|
|
2672
|
+
jpeg: await sharp(Buffer.from(native.bytes), { raw: {
|
|
2673
|
+
width: native.width,
|
|
2674
|
+
height: native.height,
|
|
2675
|
+
channels: 3
|
|
2676
|
+
} }).jpeg({ quality: 90 }).toBuffer(),
|
|
2677
|
+
width: native.width,
|
|
2678
|
+
height: native.height
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
//#endregion
|
|
2682
|
+
//#region src/detection-pipeline/pipeline/plate-deskew.ts
|
|
2683
|
+
/**
|
|
2684
|
+
* Plate-crop DESKEW — rotation rectification for oblique licence-plate crops.
|
|
2685
|
+
*
|
|
2686
|
+
* Plate crops reach the CTC recognizer as an axis-aligned bbox tile with NO
|
|
2687
|
+
* geometric correction. On steeply oblique cameras (cam 617) the plate text is
|
|
2688
|
+
* skewed, and CTC collapses beyond ~10-15° of baseline tilt. This module
|
|
2689
|
+
* estimates the plate's dominant rotation from the crop ALONE (no plate quad is
|
|
2690
|
+
* available) and rotates the tile so the text baseline is horizontal before OCR.
|
|
2691
|
+
*
|
|
2692
|
+
* Approach (classic projection-profile deskew, pure TS — no OpenCV, no new
|
|
2693
|
+
* native deps; sharp is used only to decode/encode JPEG, mirroring the sibling
|
|
2694
|
+
* `native-child-crop.ts` / `face-align.ts`):
|
|
2695
|
+
* 1. grayscale → isotropic Sobel gradient magnitude (an orientation-agnostic
|
|
2696
|
+
* "ink/edge" map, robust to plate polarity and lighting).
|
|
2697
|
+
* 2. for a small set of candidate CORRECTION angles, rotate the gradient map
|
|
2698
|
+
* (bilinear, same primitive family as the face warp) and score the
|
|
2699
|
+
* horizontal projection profile by its energy `Σ rowSum²`. That energy is
|
|
2700
|
+
* MAXIMISED when the text edges land on horizontal rows — the profile is
|
|
2701
|
+
* peaky (a few high rows) rather than smeared across many rows.
|
|
2702
|
+
* 3. pick the angle maximising the score (coarse 2° sweep + a 0.5° local
|
|
2703
|
+
* refinement); apply it to the full-resolution RGB tile only when it is
|
|
2704
|
+
* meaningfully non-zero.
|
|
2705
|
+
*
|
|
2706
|
+
* All of the geometry is pure functions over raw buffers, so the estimator is
|
|
2707
|
+
* fully deterministic and unit-testable without any image I/O.
|
|
2708
|
+
*/
|
|
2709
|
+
var DEG2RAD = Math.PI / 180;
|
|
2710
|
+
/** Convert an interleaved RGB/RGBA buffer to single-channel luma. */
|
|
2711
|
+
function toGrayscale(rgb) {
|
|
2712
|
+
const { data, width, height, channels } = rgb;
|
|
2713
|
+
const out = new Uint8Array(width * height);
|
|
2714
|
+
for (let i = 0, p = 0; i < out.length; i++, p += channels) out[i] = data[p] * 77 + data[p + 1] * 150 + data[p + 2] * 29 >> 8;
|
|
2715
|
+
return {
|
|
2716
|
+
data: out,
|
|
2717
|
+
width,
|
|
2718
|
+
height
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Isotropic Sobel gradient magnitude of a grayscale image, clamped to [0,255].
|
|
2723
|
+
* Border pixels are 0 (no valid 3×3 neighbourhood) — they carry no baseline
|
|
2724
|
+
* signal anyway. The magnitude is polarity-agnostic, so it responds to both
|
|
2725
|
+
* dark-on-light and light-on-dark plates.
|
|
2726
|
+
*/
|
|
2727
|
+
function sobelMagnitude(gray) {
|
|
2728
|
+
const { data, width, height } = gray;
|
|
2729
|
+
const out = new Uint8Array(width * height);
|
|
2730
|
+
for (let y = 1; y < height - 1; y++) for (let x = 1; x < width - 1; x++) {
|
|
2731
|
+
const i = y * width + x;
|
|
2732
|
+
const tl = data[i - width - 1];
|
|
2733
|
+
const tc = data[i - width];
|
|
2734
|
+
const tr = data[i - width + 1];
|
|
2735
|
+
const ml = data[i - 1];
|
|
2736
|
+
const mr = data[i + 1];
|
|
2737
|
+
const bl = data[i + width - 1];
|
|
2738
|
+
const bc = data[i + width];
|
|
2739
|
+
const br = data[i + width + 1];
|
|
2740
|
+
const gx = tr + 2 * mr + br - (tl + 2 * ml + bl);
|
|
2741
|
+
const gy = bl + 2 * bc + br - (tl + 2 * tc + tr);
|
|
2742
|
+
const mag = Math.sqrt(gx * gx + gy * gy);
|
|
2743
|
+
out[i] = mag > 255 ? 255 : mag;
|
|
2744
|
+
}
|
|
2745
|
+
return {
|
|
2746
|
+
data: out,
|
|
2747
|
+
width,
|
|
2748
|
+
height
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Rotate a raw image (any channel count) about its centre by `angleRad`
|
|
2753
|
+
* (positive = counter-clockwise) using bilinear interpolation — the same
|
|
2754
|
+
* resampling family as the face-alignment warp. When `expand` is true the
|
|
2755
|
+
* output canvas grows to contain the whole rotated rectangle (no corner
|
|
2756
|
+
* clipping); otherwise it keeps the source dimensions. Out-of-bounds samples
|
|
2757
|
+
* take the `fill` value (default 0), so an expanded canvas gets a flat border.
|
|
2758
|
+
*/
|
|
2759
|
+
function rotateRaw(src, angleRad, opts = {}) {
|
|
2760
|
+
const { data, width: w, height: h, channels: ch } = src;
|
|
2761
|
+
const expand = opts.expand ?? false;
|
|
2762
|
+
const fill = opts.fill ?? 0;
|
|
2763
|
+
const c = Math.cos(angleRad);
|
|
2764
|
+
const s = Math.sin(angleRad);
|
|
2765
|
+
let ow = w;
|
|
2766
|
+
let oh = h;
|
|
2767
|
+
if (expand) {
|
|
2768
|
+
ow = Math.max(1, Math.ceil(Math.abs(w * c) + Math.abs(h * s)));
|
|
2769
|
+
oh = Math.max(1, Math.ceil(Math.abs(w * s) + Math.abs(h * c)));
|
|
2770
|
+
}
|
|
2771
|
+
const cx = (w - 1) / 2;
|
|
2772
|
+
const cy = (h - 1) / 2;
|
|
2773
|
+
const ocx = (ow - 1) / 2;
|
|
2774
|
+
const ocy = (oh - 1) / 2;
|
|
2775
|
+
const out = new Uint8Array(ow * oh * ch);
|
|
2776
|
+
const maxX = w - 1;
|
|
2777
|
+
const maxY = h - 1;
|
|
2778
|
+
for (let oy = 0; oy < oh; oy++) {
|
|
2779
|
+
const dyo = oy - ocy;
|
|
2780
|
+
for (let ox = 0; ox < ow; ox++) {
|
|
2781
|
+
const dxo = ox - ocx;
|
|
2782
|
+
const sx = c * dxo + s * dyo + cx;
|
|
2783
|
+
const sy = -s * dxo + c * dyo + cy;
|
|
2784
|
+
const o = (oy * ow + ox) * ch;
|
|
2785
|
+
if (sx < 0 || sx > maxX || sy < 0 || sy > maxY) {
|
|
2786
|
+
for (let k = 0; k < ch; k++) out[o + k] = fill;
|
|
2787
|
+
continue;
|
|
2788
|
+
}
|
|
2789
|
+
const x0 = Math.floor(sx);
|
|
2790
|
+
const y0 = Math.floor(sy);
|
|
2791
|
+
const x1 = x0 < maxX ? x0 + 1 : x0;
|
|
2792
|
+
const y1 = y0 < maxY ? y0 + 1 : y0;
|
|
2793
|
+
const fx = sx - x0;
|
|
2794
|
+
const fy = sy - y0;
|
|
2795
|
+
const w00 = (1 - fx) * (1 - fy);
|
|
2796
|
+
const w10 = fx * (1 - fy);
|
|
2797
|
+
const w01 = (1 - fx) * fy;
|
|
2798
|
+
const w11 = fx * fy;
|
|
2799
|
+
const i00 = (y0 * w + x0) * ch;
|
|
2800
|
+
const i10 = (y0 * w + x1) * ch;
|
|
2801
|
+
const i01 = (y1 * w + x0) * ch;
|
|
2802
|
+
const i11 = (y1 * w + x1) * ch;
|
|
2803
|
+
for (let k = 0; k < ch; k++) out[o + k] = data[i00 + k] * w00 + data[i10 + k] * w10 + data[i01 + k] * w01 + data[i11 + k] * w11 + .5 | 0;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
return {
|
|
2807
|
+
data: out,
|
|
2808
|
+
width: ow,
|
|
2809
|
+
height: oh,
|
|
2810
|
+
channels: ch
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Horizontal-projection-profile ENERGY of a single-channel map: `Σ_row rowSum²`.
|
|
2815
|
+
* Higher when the map's mass is concentrated in a few rows (aligned text) rather
|
|
2816
|
+
* than smeared across many (skewed text). Total mass is ~conserved by the
|
|
2817
|
+
* bilinear rotation, so this energy cleanly ranks candidate rotations.
|
|
2818
|
+
*/
|
|
2819
|
+
function projectionEnergy(gray) {
|
|
2820
|
+
const { data, width, height } = gray;
|
|
2821
|
+
let energy = 0;
|
|
2822
|
+
for (let y = 0; y < height; y++) {
|
|
2823
|
+
let rowSum = 0;
|
|
2824
|
+
const base = y * width;
|
|
2825
|
+
for (let x = 0; x < width; x++) rowSum += data[base + x];
|
|
2826
|
+
energy += rowSum * rowSum;
|
|
2827
|
+
}
|
|
2828
|
+
return energy;
|
|
2829
|
+
}
|
|
2830
|
+
/** Area-average downscale of a gray map so its longest side ≤ `maxDim` (never upscales). */
|
|
2831
|
+
function downscaleGray(gray, maxDim) {
|
|
2832
|
+
const { data, width, height } = gray;
|
|
2833
|
+
const longest = Math.max(width, height);
|
|
2834
|
+
if (longest <= maxDim) return gray;
|
|
2835
|
+
const scale = maxDim / longest;
|
|
2836
|
+
const nw = Math.max(1, Math.round(width * scale));
|
|
2837
|
+
const nh = Math.max(1, Math.round(height * scale));
|
|
2838
|
+
const out = new Uint8Array(nw * nh);
|
|
2839
|
+
const fx = width / nw;
|
|
2840
|
+
const fy = height / nh;
|
|
2841
|
+
for (let y = 0; y < nh; y++) {
|
|
2842
|
+
const sy0 = Math.floor(y * fy);
|
|
2843
|
+
const sy1 = Math.min(height, Math.floor((y + 1) * fy));
|
|
2844
|
+
for (let x = 0; x < nw; x++) {
|
|
2845
|
+
const sx0 = Math.floor(x * fx);
|
|
2846
|
+
const sx1 = Math.min(width, Math.floor((x + 1) * fx));
|
|
2847
|
+
let sum = 0;
|
|
2848
|
+
let n = 0;
|
|
2849
|
+
for (let yy = sy0; yy < Math.max(sy0 + 1, sy1); yy++) {
|
|
2850
|
+
const base = yy * width;
|
|
2851
|
+
for (let xx = sx0; xx < Math.max(sx0 + 1, sx1); xx++) {
|
|
2852
|
+
sum += data[base + xx];
|
|
2853
|
+
n++;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
out[y * nw + x] = n > 0 ? sum / n | 0 : 0;
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
return {
|
|
2860
|
+
data: out,
|
|
2861
|
+
width: nw,
|
|
2862
|
+
height: nh
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
/** Score one correction angle: rotate the gradient map and take its profile energy. */
|
|
2866
|
+
function scoreAngle(gradient, angleRad) {
|
|
2867
|
+
if (angleRad === 0) return projectionEnergy(gradient);
|
|
2868
|
+
const rotated = rotateRaw({
|
|
2869
|
+
data: gradient.data,
|
|
2870
|
+
width: gradient.width,
|
|
2871
|
+
height: gradient.height,
|
|
2872
|
+
channels: 1
|
|
2873
|
+
}, angleRad, {
|
|
2874
|
+
expand: true,
|
|
2875
|
+
fill: 0
|
|
2876
|
+
});
|
|
2877
|
+
return projectionEnergy({
|
|
2878
|
+
data: rotated.data,
|
|
2879
|
+
width: rotated.width,
|
|
2880
|
+
height: rotated.height
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2883
|
+
/**
|
|
2884
|
+
* Estimate the dominant text-baseline skew of a grayscale plate crop and return
|
|
2885
|
+
* the CORRECTION angle (degrees) that levels it. A coarse sweep over
|
|
2886
|
+
* ±`maxAngleDeg` in `coarseStepDeg` steps finds the best bucket; a `fineStepDeg`
|
|
2887
|
+
* local search refines it. Returns `0` when the flat (no-rotation) profile is
|
|
2888
|
+
* already the strongest response.
|
|
2889
|
+
*/
|
|
2890
|
+
function estimateSkewAngle(gray, opts = {}) {
|
|
2891
|
+
const maxAngleDeg = opts.maxAngleDeg ?? 30;
|
|
2892
|
+
const coarseStepDeg = opts.coarseStepDeg ?? 2;
|
|
2893
|
+
const fineStepDeg = opts.fineStepDeg ?? .5;
|
|
2894
|
+
const scoreMaxDim = opts.scoreMaxDim ?? 480;
|
|
2895
|
+
if (gray.width < 4 || gray.height < 4) return {
|
|
2896
|
+
angleDeg: 0,
|
|
2897
|
+
score: 0
|
|
2898
|
+
};
|
|
2899
|
+
const gradient = sobelMagnitude(downscaleGray(gray, scoreMaxDim));
|
|
2900
|
+
let bestAngle = 0;
|
|
2901
|
+
let bestScore = scoreAngle(gradient, 0);
|
|
2902
|
+
for (let deg = -maxAngleDeg; deg <= maxAngleDeg + 1e-9; deg += coarseStepDeg) {
|
|
2903
|
+
if (deg === 0) continue;
|
|
2904
|
+
const score = scoreAngle(gradient, deg * DEG2RAD);
|
|
2905
|
+
if (score > bestScore) {
|
|
2906
|
+
bestScore = score;
|
|
2907
|
+
bestAngle = deg;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
const lo = bestAngle - coarseStepDeg;
|
|
2911
|
+
const hi = bestAngle + coarseStepDeg;
|
|
2912
|
+
for (let deg = lo; deg <= hi + 1e-9; deg += fineStepDeg) {
|
|
2913
|
+
if (deg < -maxAngleDeg || deg > maxAngleDeg) continue;
|
|
2914
|
+
const score = scoreAngle(gradient, deg * DEG2RAD);
|
|
2915
|
+
if (score > bestScore) {
|
|
2916
|
+
bestScore = score;
|
|
2917
|
+
bestAngle = deg;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
return {
|
|
2921
|
+
angleDeg: bestAngle,
|
|
2922
|
+
score: bestScore
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
/**
|
|
2926
|
+
* Rectify an oblique plate crop: decode the JPEG, estimate the baseline skew,
|
|
2927
|
+
* and — only when it exceeds {@link MIN_RECTIFY_ANGLE_DEG} — rotate the
|
|
2928
|
+
* full-resolution RGB tile to level the text and re-encode. Returns `null` when
|
|
2929
|
+
* no meaningful rotation is needed (caller keeps the original tile) or on any
|
|
2930
|
+
* decode/degenerate failure (never worse than the un-rectified input).
|
|
2931
|
+
*
|
|
2932
|
+
* The returned `angleDeg` is the applied correction (0 is never returned with a
|
|
2933
|
+
* jpeg — a null means "unchanged").
|
|
2934
|
+
*/
|
|
2935
|
+
async function rectifyPlateJpeg(jpeg, opts = {}) {
|
|
2936
|
+
const raw = await sharp(jpeg).removeAlpha().raw().toBuffer({ resolveWithObject: true }).catch(() => null);
|
|
2937
|
+
if (!raw) return null;
|
|
2938
|
+
const { data, info } = raw;
|
|
2939
|
+
if (info.width < 4 || info.height < 4) return null;
|
|
2940
|
+
const rgb = {
|
|
2941
|
+
data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
|
|
2942
|
+
width: info.width,
|
|
2943
|
+
height: info.height,
|
|
2944
|
+
channels: info.channels
|
|
2945
|
+
};
|
|
2946
|
+
const gray = toGrayscale(rgb);
|
|
2947
|
+
const { angleDeg } = estimateSkewAngle(gray, opts);
|
|
2948
|
+
if (Math.abs(angleDeg) < 2) return null;
|
|
2949
|
+
let mean = 0;
|
|
2950
|
+
for (let i = 0; i < gray.data.length; i++) mean += gray.data[i];
|
|
2951
|
+
const fill = gray.data.length > 0 ? mean / gray.data.length | 0 : 0;
|
|
2952
|
+
const rotated = rotateRaw(rgb, angleDeg * DEG2RAD, {
|
|
2953
|
+
expand: true,
|
|
2954
|
+
fill
|
|
2955
|
+
});
|
|
2956
|
+
try {
|
|
2957
|
+
return {
|
|
2958
|
+
jpeg: await sharp(Buffer.from(rotated.data), { raw: {
|
|
2959
|
+
width: rotated.width,
|
|
2960
|
+
height: rotated.height,
|
|
2961
|
+
channels: rotated.channels
|
|
2962
|
+
} }).jpeg({ quality: opts.quality ?? 90 }).toBuffer(),
|
|
2963
|
+
width: rotated.width,
|
|
2964
|
+
height: rotated.height,
|
|
2965
|
+
angleDeg
|
|
2966
|
+
};
|
|
2967
|
+
} catch {
|
|
2968
|
+
return null;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
//#endregion
|
|
2563
2972
|
//#region src/detection-pipeline/pipeline/result-assembler.ts
|
|
2564
2973
|
/**
|
|
2565
2974
|
* Small deterministic id generator scoped to a single frame. Each call
|
|
@@ -2657,7 +3066,7 @@ function applyChildOutput(parent, childStep, output, stepLatencyMs, ctx) {
|
|
|
2657
3066
|
});
|
|
2658
3067
|
break;
|
|
2659
3068
|
case "text":
|
|
2660
|
-
parent.ownLabels.push({
|
|
3069
|
+
if (output.text.trim().length > 0) parent.ownLabels.push({
|
|
2661
3070
|
label: output.text,
|
|
2662
3071
|
score: output.confidence
|
|
2663
3072
|
});
|
|
@@ -2740,7 +3149,8 @@ function buildFrameResult(input) {
|
|
|
2740
3149
|
embedding,
|
|
2741
3150
|
...embeddingModelId !== void 0 ? { embeddingModelId } : {}
|
|
2742
3151
|
} : {},
|
|
2743
|
-
...m.faceAlignedCrop !== void 0 ? { faceAlignedCrop: m.faceAlignedCrop } : {}
|
|
3152
|
+
...m.faceAlignedCrop !== void 0 ? { faceAlignedCrop: m.faceAlignedCrop } : {},
|
|
3153
|
+
...m.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: m.nativeFaceShortSidePx } : {}
|
|
2744
3154
|
};
|
|
2745
3155
|
if (input.debug) {
|
|
2746
3156
|
const cleanDebug = pruneUndefined({
|
|
@@ -3043,7 +3453,7 @@ var PipelineExecutor = class {
|
|
|
3043
3453
|
*
|
|
3044
3454
|
* @returns FrameResult + optional trace
|
|
3045
3455
|
*/
|
|
3046
|
-
async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts,
|
|
3456
|
+
async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeCropProvider, cropZoneBbox) {
|
|
3047
3457
|
const startMs = Date.now();
|
|
3048
3458
|
const verbosity = runOpts?.traceVerbosity ?? "off";
|
|
3049
3459
|
const traceBuilder = new ExecutionTraceBuilder(verbosity, deviceId, imageWidth, imageHeight, this.opts.engineRuntime);
|
|
@@ -3073,7 +3483,7 @@ var PipelineExecutor = class {
|
|
|
3073
3483
|
const mutable = this.synthesizeRootDetection(rootOutput, rootStep, idGen, rootMs, imageWidth, imageHeight);
|
|
3074
3484
|
applyChildOutput(mutable, rootStep, rootOutput, rootMs, ctx);
|
|
3075
3485
|
try {
|
|
3076
|
-
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId,
|
|
3486
|
+
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId, nativeCropProvider);
|
|
3077
3487
|
} catch (err) {
|
|
3078
3488
|
this.opts.logger?.warn("Pipeline child execution failed — keeping parent detection", {
|
|
3079
3489
|
tags: { deviceId },
|
|
@@ -3103,7 +3513,7 @@ var PipelineExecutor = class {
|
|
|
3103
3513
|
continue;
|
|
3104
3514
|
}
|
|
3105
3515
|
try {
|
|
3106
|
-
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId,
|
|
3516
|
+
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId, nativeCropProvider);
|
|
3107
3517
|
} catch (err) {
|
|
3108
3518
|
this.opts.logger?.warn("Pipeline child execution failed — keeping parent detection", {
|
|
3109
3519
|
tags: { deviceId },
|
|
@@ -3261,7 +3671,7 @@ var PipelineExecutor = class {
|
|
|
3261
3671
|
});
|
|
3262
3672
|
return output;
|
|
3263
3673
|
}
|
|
3264
|
-
async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, plane, deviceId,
|
|
3674
|
+
async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, plane, deviceId, nativeCropProvider) {
|
|
3265
3675
|
for (const child of children) {
|
|
3266
3676
|
if (plane === "frame" && child.definition.inputClasses !== null) continue;
|
|
3267
3677
|
if (!this.matchesInputClasses(parentDetection.macroClass, child.inputClasses)) continue;
|
|
@@ -3284,7 +3694,7 @@ var PipelineExecutor = class {
|
|
|
3284
3694
|
}
|
|
3285
3695
|
const lms = parentDetection.landmarks;
|
|
3286
3696
|
if (lms && lms.length >= 5) {
|
|
3287
|
-
if (!
|
|
3697
|
+
if (!nativeCropProvider && detShortSide < minFaceSize) {
|
|
3288
3698
|
this.warnFaceTooSmall(child, detShortSide, minFaceSize, void 0, deviceId);
|
|
3289
3699
|
continue;
|
|
3290
3700
|
}
|
|
@@ -3295,8 +3705,8 @@ var PipelineExecutor = class {
|
|
|
3295
3705
|
imageWidth,
|
|
3296
3706
|
imageHeight,
|
|
3297
3707
|
outSize: modelEntry.inputSize.width,
|
|
3298
|
-
...
|
|
3299
|
-
nativeCropProvider
|
|
3708
|
+
...nativeCropProvider ? {
|
|
3709
|
+
nativeCropProvider,
|
|
3300
3710
|
nativeMaxWidth: NATIVE_FACE_CROP_MAX_WIDTH
|
|
3301
3711
|
} : {}
|
|
3302
3712
|
});
|
|
@@ -3309,6 +3719,7 @@ var PipelineExecutor = class {
|
|
|
3309
3719
|
cropW = aligned.width;
|
|
3310
3720
|
cropH = aligned.height;
|
|
3311
3721
|
parentDetection.faceAlignedCrop = cropJpegBuf.toString("base64");
|
|
3722
|
+
if (aligned.nativeFaceShortSidePx !== void 0) parentDetection.nativeFaceShortSidePx = aligned.nativeFaceShortSidePx;
|
|
3312
3723
|
} else {
|
|
3313
3724
|
if (detShortSide < minFaceSize) {
|
|
3314
3725
|
this.warnFaceTooSmall(child, detShortSide, minFaceSize, void 0, deviceId);
|
|
@@ -3320,10 +3731,31 @@ var PipelineExecutor = class {
|
|
|
3320
3731
|
cropH = crop.height;
|
|
3321
3732
|
}
|
|
3322
3733
|
} else {
|
|
3323
|
-
const
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3734
|
+
const nativeChild = nativeCropProvider && child.children.length === 0 ? await nativeChildCrop(nativeCropProvider, parentDetection.bbox, imageWidth, imageHeight, NATIVE_CHILD_CROP_MAX_WIDTH) : null;
|
|
3735
|
+
if (nativeChild) {
|
|
3736
|
+
cropJpegBuf = nativeChild.jpeg;
|
|
3737
|
+
cropW = nativeChild.width;
|
|
3738
|
+
cropH = nativeChild.height;
|
|
3739
|
+
} else {
|
|
3740
|
+
const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
|
|
3741
|
+
cropJpegBuf = crop.jpeg;
|
|
3742
|
+
cropW = crop.width;
|
|
3743
|
+
cropH = crop.height;
|
|
3744
|
+
}
|
|
3745
|
+
if (child.definition.postprocessor === "ctc" && child.settings?.["rectify"] !== false) try {
|
|
3746
|
+
const rectified = await rectifyPlateJpeg(cropJpegBuf);
|
|
3747
|
+
if (rectified) {
|
|
3748
|
+
cropJpegBuf = rectified.jpeg;
|
|
3749
|
+
cropW = rectified.width;
|
|
3750
|
+
cropH = rectified.height;
|
|
3751
|
+
this.opts.logger?.debug("Plate crop deskewed before OCR", { meta: {
|
|
3752
|
+
stepId: child.definition.id,
|
|
3753
|
+
angleDeg: Number(rectified.angleDeg.toFixed(2)),
|
|
3754
|
+
width: cropW,
|
|
3755
|
+
height: cropH
|
|
3756
|
+
} });
|
|
3757
|
+
}
|
|
3758
|
+
} catch {}
|
|
3327
3759
|
}
|
|
3328
3760
|
const childOutput = await this.executeStep(child, {
|
|
3329
3761
|
kind: "jpeg",
|
|
@@ -3343,7 +3775,7 @@ var PipelineExecutor = class {
|
|
|
3343
3775
|
y: l.y + py1
|
|
3344
3776
|
}));
|
|
3345
3777
|
}
|
|
3346
|
-
await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, plane, deviceId,
|
|
3778
|
+
await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, plane, deviceId, nativeCropProvider);
|
|
3347
3779
|
}
|
|
3348
3780
|
}
|
|
3349
3781
|
} catch (err) {
|
|
@@ -4451,13 +4883,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4451
4883
|
}
|
|
4452
4884
|
async getSchema(engine) {
|
|
4453
4885
|
if (!engine || !engine.runtime) engine = await this.getSelectedEngine();
|
|
4454
|
-
const format = engine.format;
|
|
4455
4886
|
const customByStep = await this.getCustomModels();
|
|
4456
|
-
const slots = buildSchemaSlots(format, this.modelsDir, customByStep);
|
|
4457
4887
|
const { hardware } = await this.fetchProbeGatingData();
|
|
4458
4888
|
const env = runtimeEnvFromProcess(toProbedHardware(hardware));
|
|
4889
|
+
const enginesWithDevices = this.getAvailableEnginesWithDevices(env);
|
|
4890
|
+
const slots = buildSchemaSlots(new Set([engine.format, ...enginesWithDevices.map((e) => e.engine.format)]), this.modelsDir, customByStep);
|
|
4459
4891
|
return {
|
|
4460
|
-
availableEngines:
|
|
4892
|
+
availableEngines: enginesWithDevices.map((e) => this.toAvailableEngine(e)),
|
|
4461
4893
|
selectedEngine: { ...engine },
|
|
4462
4894
|
slots
|
|
4463
4895
|
};
|
|
@@ -4896,6 +5328,42 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4896
5328
|
}
|
|
4897
5329
|
};
|
|
4898
5330
|
}
|
|
5331
|
+
/**
|
|
5332
|
+
* Build the native-crop provider for the DETAIL plane (`nativeCropRef` set by
|
|
5333
|
+
* `runDetailSubtree` when the parent crop came from the frame's native surface).
|
|
5334
|
+
* Same best-effort round-trip as {@link buildNativeFaceCropProvider}, but the
|
|
5335
|
+
* executor's ROI arrives normalized in the PARENT CROP's space, so it is first
|
|
5336
|
+
* re-composed into FRAME-normalized coordinates (via `cropFrameSpace`) before
|
|
5337
|
+
* hitting the native surface. This is what lets a leaf child (plate-ocr,
|
|
5338
|
+
* face-embedding) read native pixels even though the executor is running on a
|
|
5339
|
+
* cut of the frame, not the whole frame. A compose/native miss → `null` (the
|
|
5340
|
+
* executor falls back to the downscaled tile crop).
|
|
5341
|
+
*/
|
|
5342
|
+
buildNativeCropProviderFromRef(ref) {
|
|
5343
|
+
if (!ref) return void 0;
|
|
5344
|
+
const api = this.addonCtx?.api;
|
|
5345
|
+
if (!api) return void 0;
|
|
5346
|
+
const { handle, cropFrameSpace } = ref;
|
|
5347
|
+
return async (roi, maxWidth) => {
|
|
5348
|
+
try {
|
|
5349
|
+
const frameRoi = composeCropRoiToFrameNorm(roi, cropFrameSpace, handle.width, handle.height);
|
|
5350
|
+
if (!frameRoi) return null;
|
|
5351
|
+
const native = await api.pipelineRunner.getNativeCrop.query({
|
|
5352
|
+
handle,
|
|
5353
|
+
bbox: frameRoi,
|
|
5354
|
+
...maxWidth !== void 0 ? { maxWidth } : {}
|
|
5355
|
+
}, nodePin(handle.nodeId));
|
|
5356
|
+
if (!native || !native.bytes || native.width <= 0 || native.height <= 0) return null;
|
|
5357
|
+
return {
|
|
5358
|
+
bytes: native.bytes,
|
|
5359
|
+
width: native.width,
|
|
5360
|
+
height: native.height
|
|
5361
|
+
};
|
|
5362
|
+
} catch {
|
|
5363
|
+
return null;
|
|
5364
|
+
}
|
|
5365
|
+
};
|
|
5366
|
+
}
|
|
4899
5367
|
async runPipeline(input, onProgress) {
|
|
4900
5368
|
const nodeId = this.addonCtx?.kernel?.localNodeId ?? "hub";
|
|
4901
5369
|
const sessionId = input.sessionId ?? `run-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -5154,7 +5622,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5154
5622
|
const effectiveDeviceId = input.deviceId ?? 0;
|
|
5155
5623
|
const deviceOverrides = effectiveDeviceId > 0 ? await this.readDeviceStore(effectiveDeviceId) : {};
|
|
5156
5624
|
const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
|
|
5157
|
-
const
|
|
5625
|
+
const nativeCropProvider = this.buildNativeCropProviderFromRef(input.nativeCropRef) ?? this.buildNativeFaceCropProvider(input.frameHandle);
|
|
5158
5626
|
let cropZoneBbox;
|
|
5159
5627
|
if (isRuntime && effectiveDeviceId > 0 && effectiveTree.roots.some((r) => r.definition.extractMode === "crop-zone")) {
|
|
5160
5628
|
await this.ensureDeviceProxy(effectiveDeviceId);
|
|
@@ -5164,7 +5632,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5164
5632
|
const { result, trace } = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, {
|
|
5165
5633
|
traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full",
|
|
5166
5634
|
plane: input.plane
|
|
5167
|
-
},
|
|
5635
|
+
}, nativeCropProvider, cropZoneBbox);
|
|
5168
5636
|
if (isRuntime) {
|
|
5169
5637
|
if (trace && this.eventBus) this.eventBus.emit(createEvent(EventCategory.PipelineTrace, {
|
|
5170
5638
|
type: "device",
|
|
@@ -6225,11 +6693,12 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6225
6693
|
}
|
|
6226
6694
|
}
|
|
6227
6695
|
};
|
|
6228
|
-
function buildSchemaSlots(
|
|
6696
|
+
function buildSchemaSlots(formats, modelsDir, customByStep) {
|
|
6229
6697
|
const slotMap = /* @__PURE__ */ new Map();
|
|
6698
|
+
const formatList = [...formats];
|
|
6230
6699
|
for (const pipelineStep of ALL_PIPELINE_STEPS) {
|
|
6231
6700
|
const step = pipelineStep.definition;
|
|
6232
|
-
const availableModels = mergeCustomModels(step.models, customByStep?.get(step.id) ?? []).filter((m) => m.
|
|
6701
|
+
const availableModels = mergeCustomModels(step.models, customByStep?.get(step.id) ?? []).filter((m) => m.legacy !== true && formatList.some((f) => m.formats[f]));
|
|
6233
6702
|
if (availableModels.length === 0) continue;
|
|
6234
6703
|
const slot = step.slot;
|
|
6235
6704
|
if (!slotMap.has(slot)) slotMap.set(slot, []);
|