@camstack/addon-pipeline 1.2.5 → 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-CtQsET1Q.js → dist-Bh0H8jM6.js} +37 -2
- package/dist/{dist-ZB-B8UrH.mjs → dist-Bw_V5L0r.mjs} +37 -2
- 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-BKJI5CXI.mjs → step-definitions-aY4D9Loy.mjs} +70 -2
- package/dist/{step-definitions-DrRnEQPM.js → step-definitions-xxdpJX9-.js} +70 -2
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DPvS4Ex7.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DX-Ef8CP.mjs} +1 -1
- 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/{hostInit-CG0IeE_e.mjs → hostInit-DUL71Vjq.mjs} +1 -1
- 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-C-rkQ2rg.js → MaskShapeCanvas-DI4BY7W2-CGWEeRkz.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BtbCLveu.js → MotionZonesSettings-NcxxQN8r-4YbTa3Wp.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-C9CEgSlA.js → PrivacyMaskSettings-APgPLF7p-DL_tAs9-.js} +1 -1
- package/embed-dist/assets/{index-CgJ1frQY.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
|
@@ -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-Bh0H8jM6.js");
|
|
7
|
+
const require_step_definitions = require("../step-definitions-xxdpJX9-.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");
|
|
@@ -221,6 +221,43 @@ function normalizeEngineNodeId(rawNodeId) {
|
|
|
221
221
|
return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
|
|
222
222
|
}
|
|
223
223
|
//#endregion
|
|
224
|
+
//#region src/detection-pipeline/pipeline/native-crop-compose.ts
|
|
225
|
+
/** Clamp `v` into `[lo, hi]`. */
|
|
226
|
+
function clamp(v, lo, hi) {
|
|
227
|
+
if (v < lo) return lo;
|
|
228
|
+
if (v > hi) return hi;
|
|
229
|
+
return v;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Compose a child `roi` given in the parent CROP's normalized space into the
|
|
233
|
+
* FRAME's normalized space, given the crop's frame-space pixel rectangle and the
|
|
234
|
+
* frame dimensions. The result is clamped to the unit square (origin in
|
|
235
|
+
* `[0,1]`, extent bounded so `x + w ≤ 1` and `y + h ≤ 1`). Returns `null` for
|
|
236
|
+
* degenerate inputs (non-finite frame dims, empty crop rect, or an ROI that
|
|
237
|
+
* clamps to zero area) so the caller falls back to the downscaled tile crop.
|
|
238
|
+
*
|
|
239
|
+
* Pure — never mutates its arguments.
|
|
240
|
+
*/
|
|
241
|
+
function composeCropRoiToFrameNorm(roi, cropFrameSpace, frameWidth, frameHeight) {
|
|
242
|
+
if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
|
|
243
|
+
if (!(cropFrameSpace.w > 0) || !(cropFrameSpace.h > 0)) return null;
|
|
244
|
+
const fx = (cropFrameSpace.x + roi.x * cropFrameSpace.w) / frameWidth;
|
|
245
|
+
const fy = (cropFrameSpace.y + roi.y * cropFrameSpace.h) / frameHeight;
|
|
246
|
+
const fw = roi.w * cropFrameSpace.w / frameWidth;
|
|
247
|
+
const fh = roi.h * cropFrameSpace.h / frameHeight;
|
|
248
|
+
const x = clamp(fx, 0, 1);
|
|
249
|
+
const y = clamp(fy, 0, 1);
|
|
250
|
+
const w = clamp(fw, 0, 1 - x);
|
|
251
|
+
const h = clamp(fh, 0, 1 - y);
|
|
252
|
+
if (!(w > 0) || !(h > 0)) return null;
|
|
253
|
+
return {
|
|
254
|
+
x,
|
|
255
|
+
y,
|
|
256
|
+
w,
|
|
257
|
+
h
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
224
261
|
//#region src/detection-pipeline/default-detection-model.ts
|
|
225
262
|
/** The object-detection step id — the only slot this resolver applies to. */
|
|
226
263
|
var OBJECT_DETECTION_STEP_ID$1 = "object-detection";
|
|
@@ -909,6 +946,9 @@ function serializeModelConfig(config) {
|
|
|
909
946
|
if (config.inputChannels !== void 0) result["inputChannels"] = config.inputChannels;
|
|
910
947
|
if (config.labels) result["labels"] = config.labels;
|
|
911
948
|
if (config.charset) result["charset"] = config.charset;
|
|
949
|
+
if (config.plateRegion !== void 0) result["plateRegion"] = config.plateRegion;
|
|
950
|
+
if (config.minTextLength !== void 0) result["minTextLength"] = config.minTextLength;
|
|
951
|
+
if (config.minTextConfidence !== void 0) result["minTextConfidence"] = config.minTextConfidence;
|
|
912
952
|
if (config.numClasses) result["numClasses"] = config.numClasses;
|
|
913
953
|
if (config.strides) result["strides"] = config.strides;
|
|
914
954
|
if (config.maskThreshold !== void 0) result["maskThreshold"] = config.maskThreshold;
|
|
@@ -1443,6 +1483,9 @@ var EngineFactory = class {
|
|
|
1443
1483
|
nmsIouThreshold: this.opts.nmsIouThreshold ?? .45,
|
|
1444
1484
|
labels,
|
|
1445
1485
|
charset: def.charset,
|
|
1486
|
+
plateRegion: def.plateRegion,
|
|
1487
|
+
minTextLength: def.minTextLength,
|
|
1488
|
+
minTextConfidence: def.minTextConfidence,
|
|
1446
1489
|
numClasses: labels?.length,
|
|
1447
1490
|
strides: def.postprocessor === "scrfd" ? [
|
|
1448
1491
|
8,
|
|
@@ -1647,6 +1690,14 @@ var EngineProvisioner = class {
|
|
|
1647
1690
|
/** Detector NMS IoU fallback — the historical hardcoded literal, used when the
|
|
1648
1691
|
* step carries no `nmsIouThreshold` setting (unset == today). */
|
|
1649
1692
|
var DEFAULT_NMS_IOU = .45;
|
|
1693
|
+
/**
|
|
1694
|
+
* Region plate grammars for the TS fallback CTC decoder (nodejs+onnx path).
|
|
1695
|
+
* VALIDATION-ONLY: this mirror annotates `formatValid` but does NOT rescore
|
|
1696
|
+
* confusables — the confusable repair needs the aligned per-timestep softmax
|
|
1697
|
+
* that only the Python path retains (see `python/postprocessors/ctc.py`). Keep
|
|
1698
|
+
* the pattern set in sync with `PLATE_GRAMMARS` there. `'off'`/unset disables it.
|
|
1699
|
+
*/
|
|
1700
|
+
var TS_PLATE_GRAMMARS = { DE: /^[A-ZÄÖÜ]{1,3}[- ]?[A-Z]{1,2}[- ]?[0-9]{1,4}[EH]?$/ };
|
|
1650
1701
|
var VALID_KINDS = new Set([
|
|
1651
1702
|
"detections",
|
|
1652
1703
|
"classifications",
|
|
@@ -1923,9 +1974,20 @@ function postprocessCtc(output, stepDef) {
|
|
|
1923
1974
|
if (bestIdx !== 0 && bestIdx !== prev) chars.push(charset[bestIdx] ?? "");
|
|
1924
1975
|
prev = bestIdx;
|
|
1925
1976
|
}
|
|
1977
|
+
const text = chars.join("");
|
|
1978
|
+
const region = stepDef.plateRegion;
|
|
1979
|
+
if (region !== void 0 && region !== "off") {
|
|
1980
|
+
const grammar = TS_PLATE_GRAMMARS[region];
|
|
1981
|
+
if (grammar) return {
|
|
1982
|
+
kind: "text",
|
|
1983
|
+
text,
|
|
1984
|
+
confidence: seqLen > 0 ? totalScore / seqLen : 0,
|
|
1985
|
+
formatValid: grammar.test(text)
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1926
1988
|
return {
|
|
1927
1989
|
kind: "text",
|
|
1928
|
-
text
|
|
1990
|
+
text,
|
|
1929
1991
|
confidence: seqLen > 0 ? totalScore / seqLen : 0
|
|
1930
1992
|
};
|
|
1931
1993
|
}
|
|
@@ -2568,6 +2630,353 @@ async function alignFaceCrop(fullFrameJpeg, faceBbox, landmarksImageSpace, image
|
|
|
2568
2630
|
};
|
|
2569
2631
|
}
|
|
2570
2632
|
//#endregion
|
|
2633
|
+
//#region src/detection-pipeline/pipeline/native-child-crop.ts
|
|
2634
|
+
/**
|
|
2635
|
+
* NATIVE-resolution crop of a LEAF crop-child ROI (plate-ocr, leaf classifiers)
|
|
2636
|
+
* — the plate-side mirror of the face path's {@link ./face-align.buildAlignedFaceCrop}.
|
|
2637
|
+
*
|
|
2638
|
+
* On the detail plane the executor runs a child model against a parent crop
|
|
2639
|
+
* (e.g. the native vehicle crop). A leaf child's ROI (`parentDetection.bbox`,
|
|
2640
|
+
* in the parent crop's PIXEL space) is what the child model reads. Cutting it
|
|
2641
|
+
* from the parent tile with {@link ./crop-utils.cropJpeg} bounds the child input
|
|
2642
|
+
* to the tile's resolution — fatal for OCR on a small, distant plate. When a
|
|
2643
|
+
* native-crop provider is bound to the frame, we instead resolve the SAME ROI
|
|
2644
|
+
* straight from the frame's retained NATIVE surface (the provider re-composes
|
|
2645
|
+
* the crop-normalized ROI into frame space — see {@link ./native-crop-compose}),
|
|
2646
|
+
* giving the model native pixels. A miss/degenerate returns `null` so the caller
|
|
2647
|
+
* falls back to the existing tile crop (never an upscale — native or the tile).
|
|
2648
|
+
*/
|
|
2649
|
+
/**
|
|
2650
|
+
* Generous native-width cap for a leaf child crop. A plate ROI is a few hundred
|
|
2651
|
+
* native pixels wide, so this never binds for plates (effectively uncapped —
|
|
2652
|
+
* the quality path the task requires); it only bounds a pathologically large
|
|
2653
|
+
* whole-object classifier ROI so the in-process native fetch/encode stays cheap.
|
|
2654
|
+
*/
|
|
2655
|
+
var NATIVE_CHILD_CROP_MAX_WIDTH = 1920;
|
|
2656
|
+
/**
|
|
2657
|
+
* Resolve `bbox` (a parent-crop PIXEL rectangle `[x1,y1,x2,y2]`) at native
|
|
2658
|
+
* resolution via `provider`, returning a JPEG crop + its native dimensions.
|
|
2659
|
+
* Returns `null` on a degenerate ROI or any provider miss (so the caller uses
|
|
2660
|
+
* the downscaled tile crop instead). Pure w.r.t. its inputs; the only effect is
|
|
2661
|
+
* the sharp encode of the returned pixels.
|
|
2662
|
+
*/
|
|
2663
|
+
async function nativeChildCrop(provider, bbox, imageWidth, imageHeight, maxWidth) {
|
|
2664
|
+
if (!(imageWidth > 0) || !(imageHeight > 0)) return null;
|
|
2665
|
+
const x1 = Math.max(0, bbox[0]);
|
|
2666
|
+
const y1 = Math.max(0, bbox[1]);
|
|
2667
|
+
const x2 = Math.min(imageWidth, bbox[2]);
|
|
2668
|
+
const y2 = Math.min(imageHeight, bbox[3]);
|
|
2669
|
+
const w = x2 - x1;
|
|
2670
|
+
const h = y2 - y1;
|
|
2671
|
+
if (w < 1 || h < 1) return null;
|
|
2672
|
+
const native = await provider({
|
|
2673
|
+
x: x1 / imageWidth,
|
|
2674
|
+
y: y1 / imageHeight,
|
|
2675
|
+
w: w / imageWidth,
|
|
2676
|
+
h: h / imageHeight
|
|
2677
|
+
}, maxWidth);
|
|
2678
|
+
if (!native || native.width < 2 || native.height < 2 || native.bytes.length < native.width * native.height * 3) return null;
|
|
2679
|
+
return {
|
|
2680
|
+
jpeg: await (0, sharp.default)(Buffer.from(native.bytes), { raw: {
|
|
2681
|
+
width: native.width,
|
|
2682
|
+
height: native.height,
|
|
2683
|
+
channels: 3
|
|
2684
|
+
} }).jpeg({ quality: 90 }).toBuffer(),
|
|
2685
|
+
width: native.width,
|
|
2686
|
+
height: native.height
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
//#endregion
|
|
2690
|
+
//#region src/detection-pipeline/pipeline/plate-deskew.ts
|
|
2691
|
+
/**
|
|
2692
|
+
* Plate-crop DESKEW — rotation rectification for oblique licence-plate crops.
|
|
2693
|
+
*
|
|
2694
|
+
* Plate crops reach the CTC recognizer as an axis-aligned bbox tile with NO
|
|
2695
|
+
* geometric correction. On steeply oblique cameras (cam 617) the plate text is
|
|
2696
|
+
* skewed, and CTC collapses beyond ~10-15° of baseline tilt. This module
|
|
2697
|
+
* estimates the plate's dominant rotation from the crop ALONE (no plate quad is
|
|
2698
|
+
* available) and rotates the tile so the text baseline is horizontal before OCR.
|
|
2699
|
+
*
|
|
2700
|
+
* Approach (classic projection-profile deskew, pure TS — no OpenCV, no new
|
|
2701
|
+
* native deps; sharp is used only to decode/encode JPEG, mirroring the sibling
|
|
2702
|
+
* `native-child-crop.ts` / `face-align.ts`):
|
|
2703
|
+
* 1. grayscale → isotropic Sobel gradient magnitude (an orientation-agnostic
|
|
2704
|
+
* "ink/edge" map, robust to plate polarity and lighting).
|
|
2705
|
+
* 2. for a small set of candidate CORRECTION angles, rotate the gradient map
|
|
2706
|
+
* (bilinear, same primitive family as the face warp) and score the
|
|
2707
|
+
* horizontal projection profile by its energy `Σ rowSum²`. That energy is
|
|
2708
|
+
* MAXIMISED when the text edges land on horizontal rows — the profile is
|
|
2709
|
+
* peaky (a few high rows) rather than smeared across many rows.
|
|
2710
|
+
* 3. pick the angle maximising the score (coarse 2° sweep + a 0.5° local
|
|
2711
|
+
* refinement); apply it to the full-resolution RGB tile only when it is
|
|
2712
|
+
* meaningfully non-zero.
|
|
2713
|
+
*
|
|
2714
|
+
* All of the geometry is pure functions over raw buffers, so the estimator is
|
|
2715
|
+
* fully deterministic and unit-testable without any image I/O.
|
|
2716
|
+
*/
|
|
2717
|
+
var DEG2RAD = Math.PI / 180;
|
|
2718
|
+
/** Convert an interleaved RGB/RGBA buffer to single-channel luma. */
|
|
2719
|
+
function toGrayscale(rgb) {
|
|
2720
|
+
const { data, width, height, channels } = rgb;
|
|
2721
|
+
const out = new Uint8Array(width * height);
|
|
2722
|
+
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;
|
|
2723
|
+
return {
|
|
2724
|
+
data: out,
|
|
2725
|
+
width,
|
|
2726
|
+
height
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* Isotropic Sobel gradient magnitude of a grayscale image, clamped to [0,255].
|
|
2731
|
+
* Border pixels are 0 (no valid 3×3 neighbourhood) — they carry no baseline
|
|
2732
|
+
* signal anyway. The magnitude is polarity-agnostic, so it responds to both
|
|
2733
|
+
* dark-on-light and light-on-dark plates.
|
|
2734
|
+
*/
|
|
2735
|
+
function sobelMagnitude(gray) {
|
|
2736
|
+
const { data, width, height } = gray;
|
|
2737
|
+
const out = new Uint8Array(width * height);
|
|
2738
|
+
for (let y = 1; y < height - 1; y++) for (let x = 1; x < width - 1; x++) {
|
|
2739
|
+
const i = y * width + x;
|
|
2740
|
+
const tl = data[i - width - 1];
|
|
2741
|
+
const tc = data[i - width];
|
|
2742
|
+
const tr = data[i - width + 1];
|
|
2743
|
+
const ml = data[i - 1];
|
|
2744
|
+
const mr = data[i + 1];
|
|
2745
|
+
const bl = data[i + width - 1];
|
|
2746
|
+
const bc = data[i + width];
|
|
2747
|
+
const br = data[i + width + 1];
|
|
2748
|
+
const gx = tr + 2 * mr + br - (tl + 2 * ml + bl);
|
|
2749
|
+
const gy = bl + 2 * bc + br - (tl + 2 * tc + tr);
|
|
2750
|
+
const mag = Math.sqrt(gx * gx + gy * gy);
|
|
2751
|
+
out[i] = mag > 255 ? 255 : mag;
|
|
2752
|
+
}
|
|
2753
|
+
return {
|
|
2754
|
+
data: out,
|
|
2755
|
+
width,
|
|
2756
|
+
height
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* Rotate a raw image (any channel count) about its centre by `angleRad`
|
|
2761
|
+
* (positive = counter-clockwise) using bilinear interpolation — the same
|
|
2762
|
+
* resampling family as the face-alignment warp. When `expand` is true the
|
|
2763
|
+
* output canvas grows to contain the whole rotated rectangle (no corner
|
|
2764
|
+
* clipping); otherwise it keeps the source dimensions. Out-of-bounds samples
|
|
2765
|
+
* take the `fill` value (default 0), so an expanded canvas gets a flat border.
|
|
2766
|
+
*/
|
|
2767
|
+
function rotateRaw(src, angleRad, opts = {}) {
|
|
2768
|
+
const { data, width: w, height: h, channels: ch } = src;
|
|
2769
|
+
const expand = opts.expand ?? false;
|
|
2770
|
+
const fill = opts.fill ?? 0;
|
|
2771
|
+
const c = Math.cos(angleRad);
|
|
2772
|
+
const s = Math.sin(angleRad);
|
|
2773
|
+
let ow = w;
|
|
2774
|
+
let oh = h;
|
|
2775
|
+
if (expand) {
|
|
2776
|
+
ow = Math.max(1, Math.ceil(Math.abs(w * c) + Math.abs(h * s)));
|
|
2777
|
+
oh = Math.max(1, Math.ceil(Math.abs(w * s) + Math.abs(h * c)));
|
|
2778
|
+
}
|
|
2779
|
+
const cx = (w - 1) / 2;
|
|
2780
|
+
const cy = (h - 1) / 2;
|
|
2781
|
+
const ocx = (ow - 1) / 2;
|
|
2782
|
+
const ocy = (oh - 1) / 2;
|
|
2783
|
+
const out = new Uint8Array(ow * oh * ch);
|
|
2784
|
+
const maxX = w - 1;
|
|
2785
|
+
const maxY = h - 1;
|
|
2786
|
+
for (let oy = 0; oy < oh; oy++) {
|
|
2787
|
+
const dyo = oy - ocy;
|
|
2788
|
+
for (let ox = 0; ox < ow; ox++) {
|
|
2789
|
+
const dxo = ox - ocx;
|
|
2790
|
+
const sx = c * dxo + s * dyo + cx;
|
|
2791
|
+
const sy = -s * dxo + c * dyo + cy;
|
|
2792
|
+
const o = (oy * ow + ox) * ch;
|
|
2793
|
+
if (sx < 0 || sx > maxX || sy < 0 || sy > maxY) {
|
|
2794
|
+
for (let k = 0; k < ch; k++) out[o + k] = fill;
|
|
2795
|
+
continue;
|
|
2796
|
+
}
|
|
2797
|
+
const x0 = Math.floor(sx);
|
|
2798
|
+
const y0 = Math.floor(sy);
|
|
2799
|
+
const x1 = x0 < maxX ? x0 + 1 : x0;
|
|
2800
|
+
const y1 = y0 < maxY ? y0 + 1 : y0;
|
|
2801
|
+
const fx = sx - x0;
|
|
2802
|
+
const fy = sy - y0;
|
|
2803
|
+
const w00 = (1 - fx) * (1 - fy);
|
|
2804
|
+
const w10 = fx * (1 - fy);
|
|
2805
|
+
const w01 = (1 - fx) * fy;
|
|
2806
|
+
const w11 = fx * fy;
|
|
2807
|
+
const i00 = (y0 * w + x0) * ch;
|
|
2808
|
+
const i10 = (y0 * w + x1) * ch;
|
|
2809
|
+
const i01 = (y1 * w + x0) * ch;
|
|
2810
|
+
const i11 = (y1 * w + x1) * ch;
|
|
2811
|
+
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;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
return {
|
|
2815
|
+
data: out,
|
|
2816
|
+
width: ow,
|
|
2817
|
+
height: oh,
|
|
2818
|
+
channels: ch
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* Horizontal-projection-profile ENERGY of a single-channel map: `Σ_row rowSum²`.
|
|
2823
|
+
* Higher when the map's mass is concentrated in a few rows (aligned text) rather
|
|
2824
|
+
* than smeared across many (skewed text). Total mass is ~conserved by the
|
|
2825
|
+
* bilinear rotation, so this energy cleanly ranks candidate rotations.
|
|
2826
|
+
*/
|
|
2827
|
+
function projectionEnergy(gray) {
|
|
2828
|
+
const { data, width, height } = gray;
|
|
2829
|
+
let energy = 0;
|
|
2830
|
+
for (let y = 0; y < height; y++) {
|
|
2831
|
+
let rowSum = 0;
|
|
2832
|
+
const base = y * width;
|
|
2833
|
+
for (let x = 0; x < width; x++) rowSum += data[base + x];
|
|
2834
|
+
energy += rowSum * rowSum;
|
|
2835
|
+
}
|
|
2836
|
+
return energy;
|
|
2837
|
+
}
|
|
2838
|
+
/** Area-average downscale of a gray map so its longest side ≤ `maxDim` (never upscales). */
|
|
2839
|
+
function downscaleGray(gray, maxDim) {
|
|
2840
|
+
const { data, width, height } = gray;
|
|
2841
|
+
const longest = Math.max(width, height);
|
|
2842
|
+
if (longest <= maxDim) return gray;
|
|
2843
|
+
const scale = maxDim / longest;
|
|
2844
|
+
const nw = Math.max(1, Math.round(width * scale));
|
|
2845
|
+
const nh = Math.max(1, Math.round(height * scale));
|
|
2846
|
+
const out = new Uint8Array(nw * nh);
|
|
2847
|
+
const fx = width / nw;
|
|
2848
|
+
const fy = height / nh;
|
|
2849
|
+
for (let y = 0; y < nh; y++) {
|
|
2850
|
+
const sy0 = Math.floor(y * fy);
|
|
2851
|
+
const sy1 = Math.min(height, Math.floor((y + 1) * fy));
|
|
2852
|
+
for (let x = 0; x < nw; x++) {
|
|
2853
|
+
const sx0 = Math.floor(x * fx);
|
|
2854
|
+
const sx1 = Math.min(width, Math.floor((x + 1) * fx));
|
|
2855
|
+
let sum = 0;
|
|
2856
|
+
let n = 0;
|
|
2857
|
+
for (let yy = sy0; yy < Math.max(sy0 + 1, sy1); yy++) {
|
|
2858
|
+
const base = yy * width;
|
|
2859
|
+
for (let xx = sx0; xx < Math.max(sx0 + 1, sx1); xx++) {
|
|
2860
|
+
sum += data[base + xx];
|
|
2861
|
+
n++;
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
out[y * nw + x] = n > 0 ? sum / n | 0 : 0;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
return {
|
|
2868
|
+
data: out,
|
|
2869
|
+
width: nw,
|
|
2870
|
+
height: nh
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
/** Score one correction angle: rotate the gradient map and take its profile energy. */
|
|
2874
|
+
function scoreAngle(gradient, angleRad) {
|
|
2875
|
+
if (angleRad === 0) return projectionEnergy(gradient);
|
|
2876
|
+
const rotated = rotateRaw({
|
|
2877
|
+
data: gradient.data,
|
|
2878
|
+
width: gradient.width,
|
|
2879
|
+
height: gradient.height,
|
|
2880
|
+
channels: 1
|
|
2881
|
+
}, angleRad, {
|
|
2882
|
+
expand: true,
|
|
2883
|
+
fill: 0
|
|
2884
|
+
});
|
|
2885
|
+
return projectionEnergy({
|
|
2886
|
+
data: rotated.data,
|
|
2887
|
+
width: rotated.width,
|
|
2888
|
+
height: rotated.height
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
/**
|
|
2892
|
+
* Estimate the dominant text-baseline skew of a grayscale plate crop and return
|
|
2893
|
+
* the CORRECTION angle (degrees) that levels it. A coarse sweep over
|
|
2894
|
+
* ±`maxAngleDeg` in `coarseStepDeg` steps finds the best bucket; a `fineStepDeg`
|
|
2895
|
+
* local search refines it. Returns `0` when the flat (no-rotation) profile is
|
|
2896
|
+
* already the strongest response.
|
|
2897
|
+
*/
|
|
2898
|
+
function estimateSkewAngle(gray, opts = {}) {
|
|
2899
|
+
const maxAngleDeg = opts.maxAngleDeg ?? 30;
|
|
2900
|
+
const coarseStepDeg = opts.coarseStepDeg ?? 2;
|
|
2901
|
+
const fineStepDeg = opts.fineStepDeg ?? .5;
|
|
2902
|
+
const scoreMaxDim = opts.scoreMaxDim ?? 480;
|
|
2903
|
+
if (gray.width < 4 || gray.height < 4) return {
|
|
2904
|
+
angleDeg: 0,
|
|
2905
|
+
score: 0
|
|
2906
|
+
};
|
|
2907
|
+
const gradient = sobelMagnitude(downscaleGray(gray, scoreMaxDim));
|
|
2908
|
+
let bestAngle = 0;
|
|
2909
|
+
let bestScore = scoreAngle(gradient, 0);
|
|
2910
|
+
for (let deg = -maxAngleDeg; deg <= maxAngleDeg + 1e-9; deg += coarseStepDeg) {
|
|
2911
|
+
if (deg === 0) continue;
|
|
2912
|
+
const score = scoreAngle(gradient, deg * DEG2RAD);
|
|
2913
|
+
if (score > bestScore) {
|
|
2914
|
+
bestScore = score;
|
|
2915
|
+
bestAngle = deg;
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
const lo = bestAngle - coarseStepDeg;
|
|
2919
|
+
const hi = bestAngle + coarseStepDeg;
|
|
2920
|
+
for (let deg = lo; deg <= hi + 1e-9; deg += fineStepDeg) {
|
|
2921
|
+
if (deg < -maxAngleDeg || deg > maxAngleDeg) continue;
|
|
2922
|
+
const score = scoreAngle(gradient, deg * DEG2RAD);
|
|
2923
|
+
if (score > bestScore) {
|
|
2924
|
+
bestScore = score;
|
|
2925
|
+
bestAngle = deg;
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
return {
|
|
2929
|
+
angleDeg: bestAngle,
|
|
2930
|
+
score: bestScore
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
/**
|
|
2934
|
+
* Rectify an oblique plate crop: decode the JPEG, estimate the baseline skew,
|
|
2935
|
+
* and — only when it exceeds {@link MIN_RECTIFY_ANGLE_DEG} — rotate the
|
|
2936
|
+
* full-resolution RGB tile to level the text and re-encode. Returns `null` when
|
|
2937
|
+
* no meaningful rotation is needed (caller keeps the original tile) or on any
|
|
2938
|
+
* decode/degenerate failure (never worse than the un-rectified input).
|
|
2939
|
+
*
|
|
2940
|
+
* The returned `angleDeg` is the applied correction (0 is never returned with a
|
|
2941
|
+
* jpeg — a null means "unchanged").
|
|
2942
|
+
*/
|
|
2943
|
+
async function rectifyPlateJpeg(jpeg, opts = {}) {
|
|
2944
|
+
const raw = await (0, sharp.default)(jpeg).removeAlpha().raw().toBuffer({ resolveWithObject: true }).catch(() => null);
|
|
2945
|
+
if (!raw) return null;
|
|
2946
|
+
const { data, info } = raw;
|
|
2947
|
+
if (info.width < 4 || info.height < 4) return null;
|
|
2948
|
+
const rgb = {
|
|
2949
|
+
data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
|
|
2950
|
+
width: info.width,
|
|
2951
|
+
height: info.height,
|
|
2952
|
+
channels: info.channels
|
|
2953
|
+
};
|
|
2954
|
+
const gray = toGrayscale(rgb);
|
|
2955
|
+
const { angleDeg } = estimateSkewAngle(gray, opts);
|
|
2956
|
+
if (Math.abs(angleDeg) < 2) return null;
|
|
2957
|
+
let mean = 0;
|
|
2958
|
+
for (let i = 0; i < gray.data.length; i++) mean += gray.data[i];
|
|
2959
|
+
const fill = gray.data.length > 0 ? mean / gray.data.length | 0 : 0;
|
|
2960
|
+
const rotated = rotateRaw(rgb, angleDeg * DEG2RAD, {
|
|
2961
|
+
expand: true,
|
|
2962
|
+
fill
|
|
2963
|
+
});
|
|
2964
|
+
try {
|
|
2965
|
+
return {
|
|
2966
|
+
jpeg: await (0, sharp.default)(Buffer.from(rotated.data), { raw: {
|
|
2967
|
+
width: rotated.width,
|
|
2968
|
+
height: rotated.height,
|
|
2969
|
+
channels: rotated.channels
|
|
2970
|
+
} }).jpeg({ quality: opts.quality ?? 90 }).toBuffer(),
|
|
2971
|
+
width: rotated.width,
|
|
2972
|
+
height: rotated.height,
|
|
2973
|
+
angleDeg
|
|
2974
|
+
};
|
|
2975
|
+
} catch {
|
|
2976
|
+
return null;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
//#endregion
|
|
2571
2980
|
//#region src/detection-pipeline/pipeline/result-assembler.ts
|
|
2572
2981
|
/**
|
|
2573
2982
|
* Small deterministic id generator scoped to a single frame. Each call
|
|
@@ -2665,7 +3074,7 @@ function applyChildOutput(parent, childStep, output, stepLatencyMs, ctx) {
|
|
|
2665
3074
|
});
|
|
2666
3075
|
break;
|
|
2667
3076
|
case "text":
|
|
2668
|
-
parent.ownLabels.push({
|
|
3077
|
+
if (output.text.trim().length > 0) parent.ownLabels.push({
|
|
2669
3078
|
label: output.text,
|
|
2670
3079
|
score: output.confidence
|
|
2671
3080
|
});
|
|
@@ -2748,7 +3157,8 @@ function buildFrameResult(input) {
|
|
|
2748
3157
|
embedding,
|
|
2749
3158
|
...embeddingModelId !== void 0 ? { embeddingModelId } : {}
|
|
2750
3159
|
} : {},
|
|
2751
|
-
...m.faceAlignedCrop !== void 0 ? { faceAlignedCrop: m.faceAlignedCrop } : {}
|
|
3160
|
+
...m.faceAlignedCrop !== void 0 ? { faceAlignedCrop: m.faceAlignedCrop } : {},
|
|
3161
|
+
...m.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: m.nativeFaceShortSidePx } : {}
|
|
2752
3162
|
};
|
|
2753
3163
|
if (input.debug) {
|
|
2754
3164
|
const cleanDebug = pruneUndefined({
|
|
@@ -3051,7 +3461,7 @@ var PipelineExecutor = class {
|
|
|
3051
3461
|
*
|
|
3052
3462
|
* @returns FrameResult + optional trace
|
|
3053
3463
|
*/
|
|
3054
|
-
async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts,
|
|
3464
|
+
async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeCropProvider, cropZoneBbox) {
|
|
3055
3465
|
const startMs = Date.now();
|
|
3056
3466
|
const verbosity = runOpts?.traceVerbosity ?? "off";
|
|
3057
3467
|
const traceBuilder = new ExecutionTraceBuilder(verbosity, deviceId, imageWidth, imageHeight, this.opts.engineRuntime);
|
|
@@ -3081,7 +3491,7 @@ var PipelineExecutor = class {
|
|
|
3081
3491
|
const mutable = this.synthesizeRootDetection(rootOutput, rootStep, idGen, rootMs, imageWidth, imageHeight);
|
|
3082
3492
|
applyChildOutput(mutable, rootStep, rootOutput, rootMs, ctx);
|
|
3083
3493
|
try {
|
|
3084
|
-
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId,
|
|
3494
|
+
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId, nativeCropProvider);
|
|
3085
3495
|
} catch (err) {
|
|
3086
3496
|
this.opts.logger?.warn("Pipeline child execution failed — keeping parent detection", {
|
|
3087
3497
|
tags: { deviceId },
|
|
@@ -3111,7 +3521,7 @@ var PipelineExecutor = class {
|
|
|
3111
3521
|
continue;
|
|
3112
3522
|
}
|
|
3113
3523
|
try {
|
|
3114
|
-
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId,
|
|
3524
|
+
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId, nativeCropProvider);
|
|
3115
3525
|
} catch (err) {
|
|
3116
3526
|
this.opts.logger?.warn("Pipeline child execution failed — keeping parent detection", {
|
|
3117
3527
|
tags: { deviceId },
|
|
@@ -3269,7 +3679,7 @@ var PipelineExecutor = class {
|
|
|
3269
3679
|
});
|
|
3270
3680
|
return output;
|
|
3271
3681
|
}
|
|
3272
|
-
async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, plane, deviceId,
|
|
3682
|
+
async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, plane, deviceId, nativeCropProvider) {
|
|
3273
3683
|
for (const child of children) {
|
|
3274
3684
|
if (plane === "frame" && child.definition.inputClasses !== null) continue;
|
|
3275
3685
|
if (!this.matchesInputClasses(parentDetection.macroClass, child.inputClasses)) continue;
|
|
@@ -3292,7 +3702,7 @@ var PipelineExecutor = class {
|
|
|
3292
3702
|
}
|
|
3293
3703
|
const lms = parentDetection.landmarks;
|
|
3294
3704
|
if (lms && lms.length >= 5) {
|
|
3295
|
-
if (!
|
|
3705
|
+
if (!nativeCropProvider && detShortSide < minFaceSize) {
|
|
3296
3706
|
this.warnFaceTooSmall(child, detShortSide, minFaceSize, void 0, deviceId);
|
|
3297
3707
|
continue;
|
|
3298
3708
|
}
|
|
@@ -3303,8 +3713,8 @@ var PipelineExecutor = class {
|
|
|
3303
3713
|
imageWidth,
|
|
3304
3714
|
imageHeight,
|
|
3305
3715
|
outSize: modelEntry.inputSize.width,
|
|
3306
|
-
...
|
|
3307
|
-
nativeCropProvider
|
|
3716
|
+
...nativeCropProvider ? {
|
|
3717
|
+
nativeCropProvider,
|
|
3308
3718
|
nativeMaxWidth: NATIVE_FACE_CROP_MAX_WIDTH
|
|
3309
3719
|
} : {}
|
|
3310
3720
|
});
|
|
@@ -3317,6 +3727,7 @@ var PipelineExecutor = class {
|
|
|
3317
3727
|
cropW = aligned.width;
|
|
3318
3728
|
cropH = aligned.height;
|
|
3319
3729
|
parentDetection.faceAlignedCrop = cropJpegBuf.toString("base64");
|
|
3730
|
+
if (aligned.nativeFaceShortSidePx !== void 0) parentDetection.nativeFaceShortSidePx = aligned.nativeFaceShortSidePx;
|
|
3320
3731
|
} else {
|
|
3321
3732
|
if (detShortSide < minFaceSize) {
|
|
3322
3733
|
this.warnFaceTooSmall(child, detShortSide, minFaceSize, void 0, deviceId);
|
|
@@ -3328,10 +3739,31 @@ var PipelineExecutor = class {
|
|
|
3328
3739
|
cropH = crop.height;
|
|
3329
3740
|
}
|
|
3330
3741
|
} else {
|
|
3331
|
-
const
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3742
|
+
const nativeChild = nativeCropProvider && child.children.length === 0 ? await nativeChildCrop(nativeCropProvider, parentDetection.bbox, imageWidth, imageHeight, NATIVE_CHILD_CROP_MAX_WIDTH) : null;
|
|
3743
|
+
if (nativeChild) {
|
|
3744
|
+
cropJpegBuf = nativeChild.jpeg;
|
|
3745
|
+
cropW = nativeChild.width;
|
|
3746
|
+
cropH = nativeChild.height;
|
|
3747
|
+
} else {
|
|
3748
|
+
const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
|
|
3749
|
+
cropJpegBuf = crop.jpeg;
|
|
3750
|
+
cropW = crop.width;
|
|
3751
|
+
cropH = crop.height;
|
|
3752
|
+
}
|
|
3753
|
+
if (child.definition.postprocessor === "ctc" && child.settings?.["rectify"] !== false) try {
|
|
3754
|
+
const rectified = await rectifyPlateJpeg(cropJpegBuf);
|
|
3755
|
+
if (rectified) {
|
|
3756
|
+
cropJpegBuf = rectified.jpeg;
|
|
3757
|
+
cropW = rectified.width;
|
|
3758
|
+
cropH = rectified.height;
|
|
3759
|
+
this.opts.logger?.debug("Plate crop deskewed before OCR", { meta: {
|
|
3760
|
+
stepId: child.definition.id,
|
|
3761
|
+
angleDeg: Number(rectified.angleDeg.toFixed(2)),
|
|
3762
|
+
width: cropW,
|
|
3763
|
+
height: cropH
|
|
3764
|
+
} });
|
|
3765
|
+
}
|
|
3766
|
+
} catch {}
|
|
3335
3767
|
}
|
|
3336
3768
|
const childOutput = await this.executeStep(child, {
|
|
3337
3769
|
kind: "jpeg",
|
|
@@ -3351,7 +3783,7 @@ var PipelineExecutor = class {
|
|
|
3351
3783
|
y: l.y + py1
|
|
3352
3784
|
}));
|
|
3353
3785
|
}
|
|
3354
|
-
await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, plane, deviceId,
|
|
3786
|
+
await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, plane, deviceId, nativeCropProvider);
|
|
3355
3787
|
}
|
|
3356
3788
|
}
|
|
3357
3789
|
} catch (err) {
|
|
@@ -4459,13 +4891,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4459
4891
|
}
|
|
4460
4892
|
async getSchema(engine) {
|
|
4461
4893
|
if (!engine || !engine.runtime) engine = await this.getSelectedEngine();
|
|
4462
|
-
const format = engine.format;
|
|
4463
4894
|
const customByStep = await this.getCustomModels();
|
|
4464
|
-
const slots = buildSchemaSlots(format, this.modelsDir, customByStep);
|
|
4465
4895
|
const { hardware } = await this.fetchProbeGatingData();
|
|
4466
4896
|
const env = runtimeEnvFromProcess(toProbedHardware(hardware));
|
|
4897
|
+
const enginesWithDevices = this.getAvailableEnginesWithDevices(env);
|
|
4898
|
+
const slots = buildSchemaSlots(new Set([engine.format, ...enginesWithDevices.map((e) => e.engine.format)]), this.modelsDir, customByStep);
|
|
4467
4899
|
return {
|
|
4468
|
-
availableEngines:
|
|
4900
|
+
availableEngines: enginesWithDevices.map((e) => this.toAvailableEngine(e)),
|
|
4469
4901
|
selectedEngine: { ...engine },
|
|
4470
4902
|
slots
|
|
4471
4903
|
};
|
|
@@ -4904,6 +5336,42 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4904
5336
|
}
|
|
4905
5337
|
};
|
|
4906
5338
|
}
|
|
5339
|
+
/**
|
|
5340
|
+
* Build the native-crop provider for the DETAIL plane (`nativeCropRef` set by
|
|
5341
|
+
* `runDetailSubtree` when the parent crop came from the frame's native surface).
|
|
5342
|
+
* Same best-effort round-trip as {@link buildNativeFaceCropProvider}, but the
|
|
5343
|
+
* executor's ROI arrives normalized in the PARENT CROP's space, so it is first
|
|
5344
|
+
* re-composed into FRAME-normalized coordinates (via `cropFrameSpace`) before
|
|
5345
|
+
* hitting the native surface. This is what lets a leaf child (plate-ocr,
|
|
5346
|
+
* face-embedding) read native pixels even though the executor is running on a
|
|
5347
|
+
* cut of the frame, not the whole frame. A compose/native miss → `null` (the
|
|
5348
|
+
* executor falls back to the downscaled tile crop).
|
|
5349
|
+
*/
|
|
5350
|
+
buildNativeCropProviderFromRef(ref) {
|
|
5351
|
+
if (!ref) return void 0;
|
|
5352
|
+
const api = this.addonCtx?.api;
|
|
5353
|
+
if (!api) return void 0;
|
|
5354
|
+
const { handle, cropFrameSpace } = ref;
|
|
5355
|
+
return async (roi, maxWidth) => {
|
|
5356
|
+
try {
|
|
5357
|
+
const frameRoi = composeCropRoiToFrameNorm(roi, cropFrameSpace, handle.width, handle.height);
|
|
5358
|
+
if (!frameRoi) return null;
|
|
5359
|
+
const native = await api.pipelineRunner.getNativeCrop.query({
|
|
5360
|
+
handle,
|
|
5361
|
+
bbox: frameRoi,
|
|
5362
|
+
...maxWidth !== void 0 ? { maxWidth } : {}
|
|
5363
|
+
}, require_dist.nodePin(handle.nodeId));
|
|
5364
|
+
if (!native || !native.bytes || native.width <= 0 || native.height <= 0) return null;
|
|
5365
|
+
return {
|
|
5366
|
+
bytes: native.bytes,
|
|
5367
|
+
width: native.width,
|
|
5368
|
+
height: native.height
|
|
5369
|
+
};
|
|
5370
|
+
} catch {
|
|
5371
|
+
return null;
|
|
5372
|
+
}
|
|
5373
|
+
};
|
|
5374
|
+
}
|
|
4907
5375
|
async runPipeline(input, onProgress) {
|
|
4908
5376
|
const nodeId = this.addonCtx?.kernel?.localNodeId ?? "hub";
|
|
4909
5377
|
const sessionId = input.sessionId ?? `run-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -4962,8 +5430,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4962
5430
|
jpegProvider = async () => data;
|
|
4963
5431
|
} else if (frame.format === "rgb" || frame.format === "bgr" || frame.format === "gray") {
|
|
4964
5432
|
const channels = frame.format === "gray" ? 1 : 3;
|
|
4965
|
-
const sharp$
|
|
4966
|
-
const jpeg = await sharp$
|
|
5433
|
+
const sharp$5 = (await import("sharp")).default;
|
|
5434
|
+
const jpeg = await sharp$5(data, { raw: {
|
|
4967
5435
|
width: frame.width,
|
|
4968
5436
|
height: frame.height,
|
|
4969
5437
|
channels
|
|
@@ -4977,8 +5445,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4977
5445
|
};
|
|
4978
5446
|
jpegProvider = async () => jpeg;
|
|
4979
5447
|
} else {
|
|
4980
|
-
const sharp$
|
|
4981
|
-
const encoded = await sharp$
|
|
5448
|
+
const sharp$6 = (await import("sharp")).default;
|
|
5449
|
+
const encoded = await sharp$6(data, { raw: {
|
|
4982
5450
|
width: frame.width,
|
|
4983
5451
|
height: frame.height,
|
|
4984
5452
|
channels: 3
|
|
@@ -5162,7 +5630,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5162
5630
|
const effectiveDeviceId = input.deviceId ?? 0;
|
|
5163
5631
|
const deviceOverrides = effectiveDeviceId > 0 ? await this.readDeviceStore(effectiveDeviceId) : {};
|
|
5164
5632
|
const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
|
|
5165
|
-
const
|
|
5633
|
+
const nativeCropProvider = this.buildNativeCropProviderFromRef(input.nativeCropRef) ?? this.buildNativeFaceCropProvider(input.frameHandle);
|
|
5166
5634
|
let cropZoneBbox;
|
|
5167
5635
|
if (isRuntime && effectiveDeviceId > 0 && effectiveTree.roots.some((r) => r.definition.extractMode === "crop-zone")) {
|
|
5168
5636
|
await this.ensureDeviceProxy(effectiveDeviceId);
|
|
@@ -5172,7 +5640,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5172
5640
|
const { result, trace } = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, {
|
|
5173
5641
|
traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full",
|
|
5174
5642
|
plane: input.plane
|
|
5175
|
-
},
|
|
5643
|
+
}, nativeCropProvider, cropZoneBbox);
|
|
5176
5644
|
if (isRuntime) {
|
|
5177
5645
|
if (trace && this.eventBus) this.eventBus.emit(require_dist.createEvent(require_dist.EventCategory.PipelineTrace, {
|
|
5178
5646
|
type: "device",
|
|
@@ -5462,8 +5930,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5462
5930
|
/** Parse JPEG/PNG dimensions without decoding the full image */
|
|
5463
5931
|
async getJpegDimensions(buf) {
|
|
5464
5932
|
try {
|
|
5465
|
-
const sharp$
|
|
5466
|
-
const { width, height } = await sharp$
|
|
5933
|
+
const sharp$7 = (await import("sharp")).default;
|
|
5934
|
+
const { width, height } = await sharp$7(buf).metadata();
|
|
5467
5935
|
return {
|
|
5468
5936
|
width: width ?? 640,
|
|
5469
5937
|
height: height ?? 640
|
|
@@ -6233,11 +6701,12 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6233
6701
|
}
|
|
6234
6702
|
}
|
|
6235
6703
|
};
|
|
6236
|
-
function buildSchemaSlots(
|
|
6704
|
+
function buildSchemaSlots(formats, modelsDir, customByStep) {
|
|
6237
6705
|
const slotMap = /* @__PURE__ */ new Map();
|
|
6706
|
+
const formatList = [...formats];
|
|
6238
6707
|
for (const pipelineStep of require_step_definitions.ALL_PIPELINE_STEPS) {
|
|
6239
6708
|
const step = pipelineStep.definition;
|
|
6240
|
-
const availableModels = mergeCustomModels(step.models, customByStep?.get(step.id) ?? []).filter((m) => m.
|
|
6709
|
+
const availableModels = mergeCustomModels(step.models, customByStep?.get(step.id) ?? []).filter((m) => m.legacy !== true && formatList.some((f) => m.formats[f]));
|
|
6241
6710
|
if (availableModels.length === 0) continue;
|
|
6242
6711
|
const slot = step.slot;
|
|
6243
6712
|
if (!slotMap.has(slot)) slotMap.set(slot, []);
|