tempest-react-sdk 0.34.0 → 0.36.0
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/README.md +72 -68
- package/dist/audio/audio-output.cjs +2 -0
- package/dist/audio/audio-output.cjs.map +1 -0
- package/dist/audio/audio-output.js +17 -0
- package/dist/audio/audio-output.js.map +1 -0
- package/dist/audio/audio-player.cjs +1 -1
- package/dist/audio/audio-player.cjs.map +1 -1
- package/dist/audio/audio-player.js +21 -20
- package/dist/audio/audio-player.js.map +1 -1
- package/dist/audio/audio-recorder.cjs +2 -0
- package/dist/audio/audio-recorder.cjs.map +1 -0
- package/dist/audio/audio-recorder.js +30 -0
- package/dist/audio/audio-recorder.js.map +1 -0
- package/dist/audio/duration.cjs +2 -0
- package/dist/audio/duration.cjs.map +1 -0
- package/dist/audio/duration.js +10 -0
- package/dist/audio/duration.js.map +1 -0
- package/dist/audio/level-meter.cjs +2 -0
- package/dist/audio/level-meter.cjs.map +1 -0
- package/dist/audio/level-meter.js +34 -0
- package/dist/audio/level-meter.js.map +1 -0
- package/dist/audio/media-access.cjs +2 -0
- package/dist/audio/media-access.cjs.map +1 -0
- package/dist/audio/media-access.js +51 -0
- package/dist/audio/media-access.js.map +1 -0
- package/dist/audio/use-audio-recorder.cjs +2 -0
- package/dist/audio/use-audio-recorder.cjs.map +1 -0
- package/dist/audio/use-audio-recorder.js +94 -0
- package/dist/audio/use-audio-recorder.js.map +1 -0
- package/dist/audio/use-audio.cjs.map +1 -1
- package/dist/audio/use-audio.js.map +1 -1
- package/dist/audio/use-microphone.cjs +2 -0
- package/dist/audio/use-microphone.cjs.map +1 -0
- package/dist/audio/use-microphone.js +54 -0
- package/dist/audio/use-microphone.js.map +1 -0
- package/dist/audio/wav.cjs +2 -0
- package/dist/audio/wav.cjs.map +1 -0
- package/dist/audio/wav.js +49 -0
- package/dist/audio/wav.js.map +1 -0
- package/dist/auth/passkey.cjs +2 -0
- package/dist/auth/passkey.cjs.map +1 -0
- package/dist/auth/passkey.js +171 -0
- package/dist/auth/passkey.js.map +1 -0
- package/dist/auth/use-passkey.cjs +2 -0
- package/dist/auth/use-passkey.cjs.map +1 -0
- package/dist/auth/use-passkey.js +120 -0
- package/dist/auth/use-passkey.js.map +1 -0
- package/dist/br/PixQRCode.cjs +2 -0
- package/dist/br/PixQRCode.cjs.map +1 -0
- package/dist/br/PixQRCode.js +70 -0
- package/dist/br/PixQRCode.js.map +1 -0
- package/dist/br/PixQRCode.module.cjs +2 -0
- package/dist/br/PixQRCode.module.cjs.map +1 -0
- package/dist/br/PixQRCode.module.js +15 -0
- package/dist/br/PixQRCode.module.js.map +1 -0
- package/dist/br/boleto.cjs +2 -0
- package/dist/br/boleto.cjs.map +1 -0
- package/dist/br/boleto.js +225 -0
- package/dist/br/boleto.js.map +1 -0
- package/dist/br/holidays.cjs +2 -0
- package/dist/br/holidays.cjs.map +1 -0
- package/dist/br/holidays.js +148 -0
- package/dist/br/holidays.js.map +1 -0
- package/dist/br/nfe.cjs +2 -0
- package/dist/br/nfe.cjs.map +1 -0
- package/dist/br/nfe.js +104 -0
- package/dist/br/nfe.js.map +1 -0
- package/dist/br/pix.cjs +2 -0
- package/dist/br/pix.cjs.map +1 -0
- package/dist/br/pix.js +148 -0
- package/dist/br/pix.js.map +1 -0
- package/dist/br.cjs +1 -1
- package/dist/br.cjs.map +1 -1
- package/dist/br.d.ts +878 -0
- package/dist/br.js +7 -2
- package/dist/br.js.map +1 -1
- package/dist/capture/barcode.cjs +2 -0
- package/dist/capture/barcode.cjs.map +1 -0
- package/dist/capture/barcode.js +59 -0
- package/dist/capture/barcode.js.map +1 -0
- package/dist/capture/media-recorder.cjs +2 -0
- package/dist/capture/media-recorder.cjs.map +1 -0
- package/dist/capture/media-recorder.js +64 -0
- package/dist/capture/media-recorder.js.map +1 -0
- package/dist/capture/use-barcode-scanner.cjs +2 -0
- package/dist/capture/use-barcode-scanner.cjs.map +1 -0
- package/dist/capture/use-barcode-scanner.js +86 -0
- package/dist/capture/use-barcode-scanner.js.map +1 -0
- package/dist/capture/use-screen-capture.cjs +2 -0
- package/dist/capture/use-screen-capture.cjs.map +1 -0
- package/dist/capture/use-screen-capture.js +79 -0
- package/dist/capture/use-screen-capture.js.map +1 -0
- package/dist/capture/use-speech-recognition.cjs +2 -0
- package/dist/capture/use-speech-recognition.cjs.map +1 -0
- package/dist/capture/use-speech-recognition.js +118 -0
- package/dist/capture/use-speech-recognition.js.map +1 -0
- package/dist/capture/use-torch.cjs +2 -0
- package/dist/capture/use-torch.cjs.map +1 -0
- package/dist/capture/use-torch.js +33 -0
- package/dist/capture/use-torch.js.map +1 -0
- package/dist/capture/use-video-recorder.cjs +2 -0
- package/dist/capture/use-video-recorder.cjs.map +1 -0
- package/dist/capture/use-video-recorder.js +90 -0
- package/dist/capture/use-video-recorder.js.map +1 -0
- package/dist/capture/video-recorder.cjs +2 -0
- package/dist/capture/video-recorder.cjs.map +1 -0
- package/dist/capture/video-recorder.js +31 -0
- package/dist/capture/video-recorder.js.map +1 -0
- package/dist/components/AIChat/AIChat.cjs +1 -1
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js +93 -97
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
- package/dist/components/AIChat/AIChatComposer.js +2 -1
- package/dist/components/AIChat/AIChatComposer.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs +2 -2
- package/dist/components/AIChat/AIChatTurn.js +6 -6
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/Alert/Alert.module.cjs.map +1 -1
- package/dist/components/Alert/Alert.module.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs +2 -0
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.js +123 -0
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.cjs +2 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.cjs.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.js +12 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.js.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.cjs +2 -0
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.js +179 -0
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.cjs +2 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.cjs.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.js +19 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.js.map +1 -0
- package/dist/components/Badge/Badge.module.cjs.map +1 -1
- package/dist/components/Badge/Badge.module.js.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs +2 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.js +123 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.cjs +2 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.cjs.map +1 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.js +22 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.js.map +1 -0
- package/dist/components/BottomNavigation/BottomNavigation.module.cjs.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.module.js.map +1 -1
- package/dist/components/Button/Button.module.cjs.map +1 -1
- package/dist/components/Button/Button.module.js.map +1 -1
- package/dist/components/Combobox/Combobox.module.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.module.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +203 -66
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/DataTable.module.cjs +1 -1
- package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.module.js +9 -3
- package/dist/components/DataTable/DataTable.module.js.map +1 -1
- package/dist/components/DataTable/EditableCell.cjs +2 -0
- package/dist/components/DataTable/EditableCell.cjs.map +1 -0
- package/dist/components/DataTable/EditableCell.js +77 -0
- package/dist/components/DataTable/EditableCell.js.map +1 -0
- package/dist/components/DataTable/edit-labels.cjs +2 -0
- package/dist/components/DataTable/edit-labels.cjs.map +1 -0
- package/dist/components/DataTable/edit-labels.js +11 -0
- package/dist/components/DataTable/edit-labels.js.map +1 -0
- package/dist/components/DateRangePicker/DateRangePicker.module.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.module.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.module.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.module.js.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.module.cjs.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.module.js.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.js +33 -32
- package/dist/components/SyncStatusBadge/SyncStatusBadge.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +1 -2
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/hooks/use-announce.cjs +2 -0
- package/dist/hooks/use-announce.cjs.map +1 -0
- package/dist/hooks/use-announce.js +50 -0
- package/dist/hooks/use-announce.js.map +1 -0
- package/dist/hooks/use-media-devices.cjs +2 -0
- package/dist/hooks/use-media-devices.cjs.map +1 -0
- package/dist/hooks/use-media-devices.js +46 -0
- package/dist/hooks/use-media-devices.js.map +1 -0
- package/dist/hooks/use-media-permission.cjs +2 -0
- package/dist/hooks/use-media-permission.cjs.map +1 -0
- package/dist/hooks/use-media-permission.js +33 -0
- package/dist/hooks/use-media-permission.js.map +1 -0
- package/dist/http/resumable-upload.cjs +2 -0
- package/dist/http/resumable-upload.cjs.map +1 -0
- package/dist/http/resumable-upload.js +249 -0
- package/dist/http/resumable-upload.js.map +1 -0
- package/dist/perf/cache-size.cjs +2 -0
- package/dist/perf/cache-size.cjs.map +1 -0
- package/dist/perf/cache-size.js +16 -0
- package/dist/perf/cache-size.js.map +1 -0
- package/dist/perf/device.cjs +2 -0
- package/dist/perf/device.cjs.map +1 -0
- package/dist/perf/device.js +22 -0
- package/dist/perf/device.js.map +1 -0
- package/dist/perf/format.cjs +2 -0
- package/dist/perf/format.cjs.map +1 -0
- package/dist/perf/format.js +8 -0
- package/dist/perf/format.js.map +1 -0
- package/dist/perf/profiler.cjs +2 -0
- package/dist/perf/profiler.cjs.map +1 -0
- package/dist/perf/profiler.js +46 -0
- package/dist/perf/profiler.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +2705 -38
- package/dist/tempest-react-sdk.js +268 -240
- package/dist/vision/core/timing.cjs +2 -0
- package/dist/vision/core/timing.cjs.map +1 -0
- package/dist/vision/core/timing.js +24 -0
- package/dist/vision/core/timing.js.map +1 -0
- package/dist/vision/index.cjs +1 -1
- package/dist/vision/index.cjs.map +1 -1
- package/dist/vision/index.js +16 -15
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/postprocess/detection.cjs +1 -1
- package/dist/vision/postprocess/detection.cjs.map +1 -1
- package/dist/vision/postprocess/detection.js +2 -2
- package/dist/vision/postprocess/detection.js.map +1 -1
- package/dist/vision/postprocess/segmentation.cjs +1 -1
- package/dist/vision/postprocess/segmentation.cjs.map +1 -1
- package/dist/vision/postprocess/segmentation.js +1 -1
- package/dist/vision/postprocess/segmentation.js.map +1 -1
- package/dist/vision/results.cjs +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js +18 -13
- package/dist/vision/results.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js +46 -39
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision/tasks/detector.cjs +1 -1
- package/dist/vision/tasks/detector.cjs.map +1 -1
- package/dist/vision/tasks/detector.js +40 -33
- package/dist/vision/tasks/detector.js.map +1 -1
- package/dist/vision/tasks/segmenter.cjs +1 -1
- package/dist/vision/tasks/segmenter.cjs.map +1 -1
- package/dist/vision/tasks/segmenter.js +35 -28
- package/dist/vision/tasks/segmenter.js.map +1 -1
- package/dist/vision/use-camera-stream.cjs +1 -1
- package/dist/vision/use-camera-stream.cjs.map +1 -1
- package/dist/vision/use-camera-stream.js +40 -57
- package/dist/vision/use-camera-stream.js.map +1 -1
- package/dist/vision.cjs +1 -1
- package/dist/vision.d.ts +93 -13
- package/dist/vision.js +21 -20
- package/package.json +1 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { RGBImage as e } from "../types.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
2
|
+
import { SpeedTimer as t } from "../core/timing.js";
|
|
3
|
+
import { Boxes as n, DetectionResults as r } from "../results.js";
|
|
4
|
+
import { resolveLabels as i } from "../labels.js";
|
|
5
|
+
import { OrtSession as a } from "../core/session.js";
|
|
6
|
+
import { loadImage as o } from "../io/image.js";
|
|
7
|
+
import { letterbox as s, toCHW as c, toFloat32 as l, toFloat32Tensor as u } from "../preprocess/image.js";
|
|
8
|
+
import { decodeYolo as d } from "../postprocess/detection.js";
|
|
9
|
+
import { VisionTask as f } from "./base.js";
|
|
9
10
|
//#region src/vision/tasks/detector.ts
|
|
10
|
-
var
|
|
11
|
+
var p = class p extends f {
|
|
11
12
|
_head;
|
|
12
13
|
_labels;
|
|
13
14
|
_names;
|
|
@@ -21,9 +22,9 @@ var f = class f extends d {
|
|
|
21
22
|
static async create(e, t = {}) {
|
|
22
23
|
let n = t.head ?? "yolo";
|
|
23
24
|
if (n !== "yolo") throw Error(`Unsupported detector head '${n}'. Supported: 'yolo'.`);
|
|
24
|
-
let
|
|
25
|
+
let r = await a.create(e, t), o = i(t.labels ?? "coco", { numClasses: t.numClasses }), s = {};
|
|
25
26
|
for (let e = 0; e < o.length; e++) s[e] = o[e];
|
|
26
|
-
return new
|
|
27
|
+
return new p(r, n, o, s, t.inputSize ?? [640, 640], t.confThreshold ?? .25, t.iouThreshold ?? .45, t.maxDetections ?? 300);
|
|
27
28
|
}
|
|
28
29
|
get head() {
|
|
29
30
|
return this._head;
|
|
@@ -40,30 +41,36 @@ var f = class f extends d {
|
|
|
40
41
|
async call(e, t = {}) {
|
|
41
42
|
return this.predict(e, t);
|
|
42
43
|
}
|
|
43
|
-
async predict(e,
|
|
44
|
-
let
|
|
45
|
-
|
|
46
|
-
let
|
|
47
|
-
|
|
48
|
-
let
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
async predict(e, n = {}) {
|
|
45
|
+
let i = new t(), a = typeof e == "string" ? e : null, s = await o(e);
|
|
46
|
+
i.stage("load");
|
|
47
|
+
let { tensor: c, scale: l, padLeft: u, padTop: f } = this._preprocess(s);
|
|
48
|
+
i.stage("preprocess");
|
|
49
|
+
let p = await this._session.run({ [this._session.inputName]: c });
|
|
50
|
+
i.stage("inference");
|
|
51
|
+
let m = this._session.outputNames[0];
|
|
52
|
+
if (m === void 0) throw Error("Detector model has no outputs.");
|
|
53
|
+
let h = p[m];
|
|
54
|
+
if (h === void 0) throw Error(`Detector model output ${m} missing from run() result.`);
|
|
55
|
+
let g = d(h.data, h.dims, {
|
|
56
|
+
originalWidth: s.width,
|
|
57
|
+
originalHeight: s.height,
|
|
58
|
+
padLeft: u,
|
|
59
|
+
padTop: f,
|
|
60
|
+
scale: l,
|
|
61
|
+
confThreshold: n.confThreshold ?? this._confThreshold,
|
|
62
|
+
iouThreshold: n.iouThreshold ?? this._iouThreshold,
|
|
56
63
|
maxDetections: this._maxDetections
|
|
57
|
-
}),
|
|
58
|
-
let e = new Set(
|
|
59
|
-
return
|
|
60
|
-
})()).map((e) => this._buildResult(
|
|
61
|
-
return [new
|
|
64
|
+
}), _ = (n.classes === void 0 ? g : (() => {
|
|
65
|
+
let e = new Set(n.classes);
|
|
66
|
+
return g.filter((t) => e.has(t.classId));
|
|
67
|
+
})()).map((e) => this._buildResult(s, e.bbox, e.classId, e.confidence)), v = [s.height, s.width], y = this._buildBoxes(_, v);
|
|
68
|
+
return i.stage("postprocess"), [new r(y, _, this._names, s, v, a, i.speed())];
|
|
62
69
|
}
|
|
63
70
|
_preprocess(e) {
|
|
64
|
-
let [t, n] = this._inputSize, r =
|
|
71
|
+
let [t, n] = this._inputSize, r = s(e, t, n);
|
|
65
72
|
return {
|
|
66
|
-
tensor:
|
|
73
|
+
tensor: u(c(l(r.image), r.image.width, r.image.height, 3), [
|
|
67
74
|
1,
|
|
68
75
|
3,
|
|
69
76
|
r.image.height,
|
|
@@ -97,16 +104,16 @@ var f = class f extends d {
|
|
|
97
104
|
croppedImage: p
|
|
98
105
|
};
|
|
99
106
|
}
|
|
100
|
-
_buildBoxes(e,
|
|
107
|
+
_buildBoxes(e, t) {
|
|
101
108
|
let r = e.length, i = new Float32Array(r * 4), a = new Int32Array(r), o = new Float32Array(r);
|
|
102
109
|
for (let t = 0; t < r; t++) {
|
|
103
110
|
let n = e[t];
|
|
104
111
|
i[t * 4] = n.bbox.x1, i[t * 4 + 1] = n.bbox.y1, i[t * 4 + 2] = n.bbox.x2, i[t * 4 + 3] = n.bbox.y2, a[t] = n.classId, o[t] = n.confidence;
|
|
105
112
|
}
|
|
106
|
-
return new
|
|
113
|
+
return new n(i, a, o, t);
|
|
107
114
|
}
|
|
108
115
|
};
|
|
109
116
|
//#endregion
|
|
110
|
-
export {
|
|
117
|
+
export { p as Detector };
|
|
111
118
|
|
|
112
119
|
//# sourceMappingURL=detector.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detector.js","names":[],"sources":["../../../src/vision/tasks/detector.ts"],"sourcesContent":["/**\n * Object detection task using anchor-free YOLO ONNX models (v8/v9/v10/v11/v12).\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { type ImageInput, loadImage } from \"../io/image\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport { decodeYolo } from \"../postprocess/detection\";\nimport { letterbox, toCHW, toFloat32, toFloat32Tensor } from \"../preprocess/image\";\nimport { Boxes, DetectionResults } from \"../results\";\nimport { VisionTask } from \"./base\";\nimport { type BoundingBox, type DetectionResult, RGBImage } from \"../types\";\n\n/**\n * Decoder family for the model's detection head.\n *\n * - `\"yolo\"`: anchor-free YOLO head with output shape `[1, 4 + nc, N]` —\n * covers YOLOv8, v9, v10, v11, v12, v26 detect exports.\n *\n * The SDK does **not** auto-detect the head from the model — the caller is\n * responsible for picking a head that matches their export. Future families\n * (v5/v6/v7 with `[1, N, 5+nc]`) will be added as new literal members.\n */\nexport type DetectorHead = \"yolo\";\n\nexport interface DetectorOptions extends OrtSessionOptions {\n /**\n * Decoder family for the detection head. Default `\"yolo\"` covers\n * YOLOv8/v9/v10/v11/v12/v26.\n */\n readonly head?: DetectorHead;\n /** Class label spec — see {@link resolveLabels}. Defaults to the COCO 80-class preset. */\n readonly labels?: LabelSpec;\n /** Number of classes — used to validate the supplied labels. */\n readonly numClasses?: number;\n /** Model input `[width, height]` for letterboxing. Defaults to `[640, 640]`. */\n readonly inputSize?: readonly [number, number];\n /** Default minimum class score to keep a candidate. */\n readonly confThreshold?: number;\n /** Default IoU threshold for non-maximum suppression. */\n readonly iouThreshold?: number;\n /** Maximum number of detections per image. */\n readonly maxDetections?: number;\n}\n\nexport interface DetectorPredictOptions {\n /** Override the default confidence threshold. */\n readonly confThreshold?: number;\n /** Override the default IoU threshold. */\n readonly iouThreshold?: number;\n /**\n * If set, keep only detections whose `classId` is in this list.\n * Mirrors Ultralytics' `model.predict(img, classes=[0, 16])`.\n */\n readonly classes?: readonly number[];\n}\n\n/**\n * Object detector for anchor-free YOLO ONNX models (v8/v9/v10/v11/v12).\n *\n * `predict()` returns `Promise<DetectionResults[]>` (length 1 for a single\n * image), mirroring Ultralytics' `YOLO(\"img.jpg\")`. Iterate the envelope for\n * per-instance dataclasses, or use the bulk `boxes` view (`.xyxy`, `.xywh`,\n * `.xyxyn`, `.xywhn`, `.cls`, `.conf`).\n *\n * @example\n * ```typescript\n * const det = await Detector.create(\"/models/yolov8n.onnx\");\n * const results = await det.predict(\"/images/street.jpg\");\n * const r = results[0];\n * console.log(r.boxes.xyxy, r.boxes.cls, r.boxes.conf, r.names);\n * for (const d of r) {\n * console.log(d.cls, d.conf, d.box.xyxy);\n * }\n * ```\n */\nexport class Detector extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _head: DetectorHead,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _inputSize: readonly [number, number],\n private readonly _confThreshold: number,\n private readonly _iouThreshold: number,\n private readonly _maxDetections: number,\n ) {\n super(session);\n }\n\n /** Load the model and resolve labels. */\n static async create(model: ModelSource, options: DetectorOptions = {}): Promise<Detector> {\n const head: DetectorHead = options.head ?? \"yolo\";\n if (head !== \"yolo\") {\n throw new Error(`Unsupported detector head '${head}'. Supported: 'yolo'.`);\n }\n const session = await OrtSession.create(model, options);\n const labels = resolveLabels(options.labels ?? \"coco\", {\n numClasses: options.numClasses,\n });\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n names[i] = labels[i] as string;\n }\n return new Detector(\n session,\n head,\n labels,\n names,\n options.inputSize ?? [640, 640],\n options.confThreshold ?? 0.25,\n options.iouThreshold ?? 0.45,\n options.maxDetections ?? 300,\n );\n }\n\n /** The decoder family used to interpret the model's output. */\n get head(): DetectorHead {\n return this._head;\n }\n\n /** Class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Class id → class name dict (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /** Number of classes the model predicts. */\n get numClasses(): number {\n return this._labels.length;\n }\n\n /**\n * Alias for {@link predict} — call the detector like a torch `nn.Module`.\n *\n * Use as `det.call(img)` since JavaScript class instances are not callable;\n * for direct invocation, prefer `det.predict(img)`. The full\n * {@link DetectorPredictOptions} (including `classes`) is supported.\n */\n async call(\n image: ImageInput,\n options: DetectorPredictOptions = {},\n ): Promise<DetectionResults[]> {\n return this.predict(image, options);\n }\n\n /** Run detection on a single image. */\n async predict(\n image: ImageInput,\n options: DetectorPredictOptions = {},\n ): Promise<DetectionResults[]> {\n const path = typeof image === \"string\" ? image : null;\n const original = await loadImage(image);\n const { tensor, scale, padLeft, padTop } = this._preprocess(original);\n const outputs = await this._session.run({ [this._session.inputName]: tensor });\n\n const firstOutputName = this._session.outputNames[0];\n if (firstOutputName === undefined) {\n throw new Error(\"Detector model has no outputs.\");\n }\n const raw = outputs[firstOutputName];\n if (raw === undefined) {\n throw new Error(`Detector model output ${firstOutputName} missing from run() result.`);\n }\n\n const decodedAll = decodeYolo(raw.data as Float32Array, raw.dims, {\n originalWidth: original.width,\n originalHeight: original.height,\n padLeft,\n padTop,\n scale,\n confThreshold: options.confThreshold ?? this._confThreshold,\n iouThreshold: options.iouThreshold ?? this._iouThreshold,\n maxDetections: this._maxDetections,\n });\n\n const decoded =\n options.classes !== undefined\n ? (() => {\n const allowed = new Set(options.classes);\n return decodedAll.filter((d) => allowed.has(d.classId));\n })()\n : decodedAll;\n\n const detections = decoded.map((d) =>\n this._buildResult(original, d.bbox, d.classId, d.confidence),\n );\n\n const orig: readonly [number, number] = [original.height, original.width];\n return [\n new DetectionResults(\n this._buildBoxes(detections, orig),\n detections,\n this._names,\n original,\n orig,\n path,\n ),\n ];\n }\n\n private _preprocess(image: RGBImage): {\n tensor: ort.Tensor;\n scale: number;\n padLeft: number;\n padTop: number;\n } {\n const [tw, th] = this._inputSize;\n const lb = letterbox(image, tw, th);\n const f32 = toFloat32(lb.image);\n const chw = toCHW(f32, lb.image.width, lb.image.height, 3);\n return {\n tensor: toFloat32Tensor(chw, [1, 3, lb.image.height, lb.image.width]),\n scale: lb.scale,\n padLeft: lb.padLeft,\n padTop: lb.padTop,\n };\n }\n\n private _buildResult(\n original: RGBImage,\n bbox: BoundingBox,\n classId: number,\n confidence: number,\n ): DetectionResult {\n const [x1, y1, x2, y2] = bbox.asIntXyxy();\n const cx1 = Math.max(0, x1);\n const cy1 = Math.max(0, y1);\n const cx2 = Math.min(original.width, x2);\n const cy2 = Math.min(original.height, y2);\n\n let cropped: RGBImage;\n if (cx2 > cx1 && cy2 > cy1) {\n const cw = cx2 - cx1;\n const ch = cy2 - cy1;\n const out = new Uint8Array(cw * ch * 3);\n for (let row = 0; row < ch; row++) {\n const srcOffset = ((cy1 + row) * original.width + cx1) * 3;\n out.set(original.data.subarray(srcOffset, srcOffset + cw * 3), row * cw * 3);\n }\n cropped = new RGBImage(out, cw, ch);\n } else {\n cropped = new RGBImage(new Uint8Array(0), 0, 0);\n }\n\n const className = this._names[classId] ?? `class_${classId}`;\n\n return {\n classId,\n className,\n confidence,\n bbox,\n cls: classId,\n name: className,\n conf: confidence,\n box: bbox,\n croppedImage: cropped,\n };\n }\n\n private _buildBoxes(\n detections: readonly DetectionResult[],\n origShape: readonly [number, number],\n ): Boxes {\n const n = detections.length;\n const xyxy = new Float32Array(n * 4);\n const cls = new Int32Array(n);\n const conf = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const d = detections[i] as DetectionResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n cls[i] = d.classId;\n conf[i] = d.confidence;\n }\n return new Boxes(xyxy, cls, conf, origShape);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"detector.js","names":[],"sources":["../../../src/vision/tasks/detector.ts"],"sourcesContent":["/**\n * Object detection task using anchor-free YOLO ONNX models (v8/v9/v10/v11/v12).\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { SpeedTimer } from \"../core/timing\";\nimport { type ImageInput, loadImage } from \"../io/image\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport { decodeYolo } from \"../postprocess/detection\";\nimport { letterbox, toCHW, toFloat32, toFloat32Tensor } from \"../preprocess/image\";\nimport { Boxes, DetectionResults } from \"../results\";\nimport { VisionTask } from \"./base\";\nimport { type BoundingBox, type DetectionResult, RGBImage } from \"../types\";\n\n/**\n * Decoder family for the model's detection head.\n *\n * - `\"yolo\"`: anchor-free YOLO head with output shape `[1, 4 + nc, N]` —\n * covers YOLOv8, v9, v10, v11, v12, v26 detect exports.\n *\n * The SDK does **not** auto-detect the head from the model — the caller is\n * responsible for picking a head that matches their export. Future families\n * (v5/v6/v7 with `[1, N, 5+nc]`) will be added as new literal members.\n */\nexport type DetectorHead = \"yolo\";\n\nexport interface DetectorOptions extends OrtSessionOptions {\n /**\n * Decoder family for the detection head. Default `\"yolo\"` covers\n * YOLOv8/v9/v10/v11/v12/v26.\n */\n readonly head?: DetectorHead;\n /** Class label spec — see {@link resolveLabels}. Defaults to the COCO 80-class preset. */\n readonly labels?: LabelSpec;\n /** Number of classes — used to validate the supplied labels. */\n readonly numClasses?: number;\n /** Model input `[width, height]` for letterboxing. Defaults to `[640, 640]`. */\n readonly inputSize?: readonly [number, number];\n /** Default minimum class score to keep a candidate. */\n readonly confThreshold?: number;\n /** Default IoU threshold for non-maximum suppression. */\n readonly iouThreshold?: number;\n /** Maximum number of detections per image. */\n readonly maxDetections?: number;\n}\n\nexport interface DetectorPredictOptions {\n /** Override the default confidence threshold. */\n readonly confThreshold?: number;\n /** Override the default IoU threshold. */\n readonly iouThreshold?: number;\n /**\n * If set, keep only detections whose `classId` is in this list.\n * Mirrors Ultralytics' `model.predict(img, classes=[0, 16])`.\n */\n readonly classes?: readonly number[];\n}\n\n/**\n * Object detector for anchor-free YOLO ONNX models (v8/v9/v10/v11/v12).\n *\n * `predict()` returns `Promise<DetectionResults[]>` (length 1 for a single\n * image), mirroring Ultralytics' `YOLO(\"img.jpg\")`. Iterate the envelope for\n * per-instance dataclasses, or use the bulk `boxes` view (`.xyxy`, `.xywh`,\n * `.xyxyn`, `.xywhn`, `.cls`, `.conf`).\n *\n * @example\n * ```typescript\n * const det = await Detector.create(\"/models/yolov8n.onnx\");\n * const results = await det.predict(\"/images/street.jpg\");\n * const r = results[0];\n * console.log(r.boxes.xyxy, r.boxes.cls, r.boxes.conf, r.names);\n * for (const d of r) {\n * console.log(d.cls, d.conf, d.box.xyxy);\n * }\n * ```\n */\nexport class Detector extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _head: DetectorHead,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _inputSize: readonly [number, number],\n private readonly _confThreshold: number,\n private readonly _iouThreshold: number,\n private readonly _maxDetections: number,\n ) {\n super(session);\n }\n\n /** Load the model and resolve labels. */\n static async create(model: ModelSource, options: DetectorOptions = {}): Promise<Detector> {\n const head: DetectorHead = options.head ?? \"yolo\";\n if (head !== \"yolo\") {\n throw new Error(`Unsupported detector head '${head}'. Supported: 'yolo'.`);\n }\n const session = await OrtSession.create(model, options);\n const labels = resolveLabels(options.labels ?? \"coco\", {\n numClasses: options.numClasses,\n });\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n names[i] = labels[i] as string;\n }\n return new Detector(\n session,\n head,\n labels,\n names,\n options.inputSize ?? [640, 640],\n options.confThreshold ?? 0.25,\n options.iouThreshold ?? 0.45,\n options.maxDetections ?? 300,\n );\n }\n\n /** The decoder family used to interpret the model's output. */\n get head(): DetectorHead {\n return this._head;\n }\n\n /** Class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Class id → class name dict (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /** Number of classes the model predicts. */\n get numClasses(): number {\n return this._labels.length;\n }\n\n /**\n * Alias for {@link predict} — call the detector like a torch `nn.Module`.\n *\n * Use as `det.call(img)` since JavaScript class instances are not callable;\n * for direct invocation, prefer `det.predict(img)`. The full\n * {@link DetectorPredictOptions} (including `classes`) is supported.\n */\n async call(\n image: ImageInput,\n options: DetectorPredictOptions = {},\n ): Promise<DetectionResults[]> {\n return this.predict(image, options);\n }\n\n /**\n * Run detection on a single image.\n *\n * The returned envelope carries a {@link Speed} breakdown in `speed`,\n * mirroring Ultralytics' `results[0].speed`.\n */\n async predict(\n image: ImageInput,\n options: DetectorPredictOptions = {},\n ): Promise<DetectionResults[]> {\n const timer = new SpeedTimer();\n const path = typeof image === \"string\" ? image : null;\n const original = await loadImage(image);\n timer.stage(\"load\");\n const { tensor, scale, padLeft, padTop } = this._preprocess(original);\n timer.stage(\"preprocess\");\n const outputs = await this._session.run({ [this._session.inputName]: tensor });\n timer.stage(\"inference\");\n\n const firstOutputName = this._session.outputNames[0];\n if (firstOutputName === undefined) {\n throw new Error(\"Detector model has no outputs.\");\n }\n const raw = outputs[firstOutputName];\n if (raw === undefined) {\n throw new Error(`Detector model output ${firstOutputName} missing from run() result.`);\n }\n\n const decodedAll = decodeYolo(raw.data as Float32Array, raw.dims, {\n originalWidth: original.width,\n originalHeight: original.height,\n padLeft,\n padTop,\n scale,\n confThreshold: options.confThreshold ?? this._confThreshold,\n iouThreshold: options.iouThreshold ?? this._iouThreshold,\n maxDetections: this._maxDetections,\n });\n\n const decoded =\n options.classes !== undefined\n ? (() => {\n const allowed = new Set(options.classes);\n return decodedAll.filter((d) => allowed.has(d.classId));\n })()\n : decodedAll;\n\n const detections = decoded.map((d) =>\n this._buildResult(original, d.bbox, d.classId, d.confidence),\n );\n\n const orig: readonly [number, number] = [original.height, original.width];\n const boxes = this._buildBoxes(detections, orig);\n timer.stage(\"postprocess\");\n return [\n new DetectionResults(\n boxes,\n detections,\n this._names,\n original,\n orig,\n path,\n timer.speed(),\n ),\n ];\n }\n\n private _preprocess(image: RGBImage): {\n tensor: ort.Tensor;\n scale: number;\n padLeft: number;\n padTop: number;\n } {\n const [tw, th] = this._inputSize;\n const lb = letterbox(image, tw, th);\n const f32 = toFloat32(lb.image);\n const chw = toCHW(f32, lb.image.width, lb.image.height, 3);\n return {\n tensor: toFloat32Tensor(chw, [1, 3, lb.image.height, lb.image.width]),\n scale: lb.scale,\n padLeft: lb.padLeft,\n padTop: lb.padTop,\n };\n }\n\n private _buildResult(\n original: RGBImage,\n bbox: BoundingBox,\n classId: number,\n confidence: number,\n ): DetectionResult {\n const [x1, y1, x2, y2] = bbox.asIntXyxy();\n const cx1 = Math.max(0, x1);\n const cy1 = Math.max(0, y1);\n const cx2 = Math.min(original.width, x2);\n const cy2 = Math.min(original.height, y2);\n\n let cropped: RGBImage;\n if (cx2 > cx1 && cy2 > cy1) {\n const cw = cx2 - cx1;\n const ch = cy2 - cy1;\n const out = new Uint8Array(cw * ch * 3);\n for (let row = 0; row < ch; row++) {\n const srcOffset = ((cy1 + row) * original.width + cx1) * 3;\n out.set(original.data.subarray(srcOffset, srcOffset + cw * 3), row * cw * 3);\n }\n cropped = new RGBImage(out, cw, ch);\n } else {\n cropped = new RGBImage(new Uint8Array(0), 0, 0);\n }\n\n const className = this._names[classId] ?? `class_${classId}`;\n\n return {\n classId,\n className,\n confidence,\n bbox,\n cls: classId,\n name: className,\n conf: confidence,\n box: bbox,\n croppedImage: cropped,\n };\n }\n\n private _buildBoxes(\n detections: readonly DetectionResult[],\n origShape: readonly [number, number],\n ): Boxes {\n const n = detections.length;\n const xyxy = new Float32Array(n * 4);\n const cls = new Int32Array(n);\n const conf = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const d = detections[i] as DetectionResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n cls[i] = d.classId;\n conf[i] = d.confidence;\n }\n return new Boxes(xyxy, cls, conf, origShape);\n }\n}\n"],"mappings":";;;;;;;;;;AA+EA,IAAa,IAAb,MAAa,UAAiB,EAAW;CAGhB;CACA;CACA;CACA;CACA;CACA;CACA;CARrB,YACI,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACF;EADmB,AAEjB,MAAM,CAAO,GARI,KAAA,QAAA,GACA,KAAA,UAAA,GACA,KAAA,SAAA,GACA,KAAA,aAAA,GACA,KAAA,iBAAA,GACA,KAAA,gBAAA,GACA,KAAA,iBAAA;CAGrB;CAGA,aAAa,OAAO,GAAoB,IAA2B,CAAC,GAAsB;EACtF,IAAM,IAAqB,EAAQ,QAAQ;EAC3C,IAAI,MAAS,QACT,MAAU,MAAM,8BAA8B,EAAK,sBAAsB;EAE7E,IAAM,IAAU,MAAM,EAAW,OAAO,GAAO,CAAO,GAChD,IAAS,EAAc,EAAQ,UAAU,QAAQ,EACnD,YAAY,EAAQ,WACxB,CAAC,GACK,IAAgC,CAAC;EACvC,KAAK,IAAI,IAAI,GAAG,IAAI,EAAO,QAAQ,KAC/B,EAAM,KAAK,EAAO;EAEtB,OAAO,IAAI,EACP,GACA,GACA,GACA,GACA,EAAQ,aAAa,CAAC,KAAK,GAAG,GAC9B,EAAQ,iBAAiB,KACzB,EAAQ,gBAAgB,KACxB,EAAQ,iBAAiB,GAC7B;CACJ;CAGA,IAAI,OAAqB;EACrB,OAAO,KAAK;CAChB;CAGA,IAAI,SAA4B;EAC5B,OAAO,KAAK;CAChB;CAGA,IAAI,QAA0C;EAC1C,OAAO,KAAK;CAChB;CAGA,IAAI,aAAqB;EACrB,OAAO,KAAK,QAAQ;CACxB;CASA,MAAM,KACF,GACA,IAAkC,CAAC,GACR;EAC3B,OAAO,KAAK,QAAQ,GAAO,CAAO;CACtC;CAQA,MAAM,QACF,GACA,IAAkC,CAAC,GACR;EAC3B,IAAM,IAAQ,IAAI,EAAW,GACvB,IAAO,OAAO,KAAU,WAAW,IAAQ,MAC3C,IAAW,MAAM,EAAU,CAAK;EACtC,EAAM,MAAM,MAAM;EAClB,IAAM,EAAE,WAAQ,UAAO,YAAS,cAAW,KAAK,YAAY,CAAQ;EACpE,EAAM,MAAM,YAAY;EACxB,IAAM,IAAU,MAAM,KAAK,SAAS,IAAI,GAAG,KAAK,SAAS,YAAY,EAAO,CAAC;EAC7E,EAAM,MAAM,WAAW;EAEvB,IAAM,IAAkB,KAAK,SAAS,YAAY;EAClD,IAAI,MAAoB,KAAA,GACpB,MAAU,MAAM,gCAAgC;EAEpD,IAAM,IAAM,EAAQ;EACpB,IAAI,MAAQ,KAAA,GACR,MAAU,MAAM,yBAAyB,EAAgB,4BAA4B;EAGzF,IAAM,IAAa,EAAW,EAAI,MAAsB,EAAI,MAAM;GAC9D,eAAe,EAAS;GACxB,gBAAgB,EAAS;GACzB;GACA;GACA;GACA,eAAe,EAAQ,iBAAiB,KAAK;GAC7C,cAAc,EAAQ,gBAAgB,KAAK;GAC3C,eAAe,KAAK;EACxB,CAAC,GAUK,KAPF,EAAQ,YAAY,KAAA,IAKd,WAJO;GACH,IAAM,IAAU,IAAI,IAAI,EAAQ,OAAO;GACvC,OAAO,EAAW,QAAQ,MAAM,EAAQ,IAAI,EAAE,OAAO,CAAC;EAC1D,EAAA,CAAG,EACH,CAEiB,KAAK,MAC5B,KAAK,aAAa,GAAU,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,CAC/D,GAEM,IAAkC,CAAC,EAAS,QAAQ,EAAS,KAAK,GAClE,IAAQ,KAAK,YAAY,GAAY,CAAI;EAE/C,OADA,EAAM,MAAM,aAAa,GAClB,CACH,IAAI,EACA,GACA,GACA,KAAK,QACL,GACA,GACA,GACA,EAAM,MAAM,CAChB,CACJ;CACJ;CAEA,YAAoB,GAKlB;EACE,IAAM,CAAC,GAAI,KAAM,KAAK,YAChB,IAAK,EAAU,GAAO,GAAI,CAAE;EAGlC,OAAO;GACH,QAAQ,EAFA,EADA,EAAU,EAAG,KACP,GAAK,EAAG,MAAM,OAAO,EAAG,MAAM,QAAQ,CAE5B,GAAK;IAAC;IAAG;IAAG,EAAG,MAAM;IAAQ,EAAG,MAAM;GAAK,CAAC;GACpE,OAAO,EAAG;GACV,SAAS,EAAG;GACZ,QAAQ,EAAG;EACf;CACJ;CAEA,aACI,GACA,GACA,GACA,GACe;EACf,IAAM,CAAC,GAAI,GAAI,GAAI,KAAM,EAAK,UAAU,GAClC,IAAM,KAAK,IAAI,GAAG,CAAE,GACpB,IAAM,KAAK,IAAI,GAAG,CAAE,GACpB,IAAM,KAAK,IAAI,EAAS,OAAO,CAAE,GACjC,IAAM,KAAK,IAAI,EAAS,QAAQ,CAAE,GAEpC;EACJ,IAAI,IAAM,KAAO,IAAM,GAAK;GACxB,IAAM,IAAK,IAAM,GACX,IAAK,IAAM,GACX,IAAM,IAAI,WAAW,IAAK,IAAK,CAAC;GACtC,KAAK,IAAI,IAAM,GAAG,IAAM,GAAI,KAAO;IAC/B,IAAM,MAAc,IAAM,KAAO,EAAS,QAAQ,KAAO;IACzD,EAAI,IAAI,EAAS,KAAK,SAAS,GAAW,IAAY,IAAK,CAAC,GAAG,IAAM,IAAK,CAAC;GAC/E;GACA,IAAU,IAAI,EAAS,GAAK,GAAI,CAAE;EACtC,OACI,IAAU,IAAI,kBAAS,IAAI,WAAY,GAAG,GAAG,CAAC;EAGlD,IAAM,IAAY,KAAK,OAAO,MAAY,SAAS;EAEnD,OAAO;GACH;GACA;GACA;GACA;GACA,KAAK;GACL,MAAM;GACN,MAAM;GACN,KAAK;GACL,cAAc;EAClB;CACJ;CAEA,YACI,GACA,GACK;EACL,IAAM,IAAI,EAAW,QACf,IAAO,IAAI,aAAa,IAAI,CAAC,GAC7B,IAAM,IAAI,WAAW,CAAC,GACtB,IAAO,IAAI,aAAa,CAAC;EAC/B,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK;GACxB,IAAM,IAAI,EAAW;GAMrB,AALA,EAAK,IAAI,KAAK,EAAE,KAAK,IACrB,EAAK,IAAI,IAAI,KAAK,EAAE,KAAK,IACzB,EAAK,IAAI,IAAI,KAAK,EAAE,KAAK,IACzB,EAAK,IAAI,IAAI,KAAK,EAAE,KAAK,IACzB,EAAI,KAAK,EAAE,SACX,EAAK,KAAK,EAAE;EAChB;EACA,OAAO,IAAI,EAAM,GAAM,GAAK,GAAM,CAAS;CAC/C;AACJ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../types.cjs"),t=require("../
|
|
1
|
+
const e=require("../types.cjs"),t=require("../core/timing.cjs"),n=require("../results.cjs"),r=require("../labels.cjs"),i=require("../core/session.cjs"),a=require("../io/image.cjs"),o=require("../preprocess/image.cjs"),s=require("../postprocess/segmentation.cjs"),c=require("./base.cjs");var l=class l extends c.VisionTask{_head;_labels;_names;_inputSize;_confThreshold;_iouThreshold;_maxDetections;_maskThreshold;constructor(e,t,n,r,i,a,o,s,c){super(e),this._head=t,this._labels=n,this._names=r,this._inputSize=i,this._confThreshold=a,this._iouThreshold=o,this._maxDetections=s,this._maskThreshold=c}static async create(e,t={}){let n=t.head??`yolo-seg`;if(n!==`yolo-seg`)throw Error(`Unsupported segmenter head '${n}'. Supported: 'yolo-seg'.`);let a=await i.OrtSession.create(e,t),o=r.resolveLabels(t.labels??`coco`,{numClasses:t.numClasses}),s={};for(let e=0;e<o.length;e++)s[e]=o[e];return new l(a,n,o,s,t.inputSize??[640,640],t.confThreshold??.25,t.iouThreshold??.45,t.maxDetections??300,t.maskThreshold??.5)}get head(){return this._head}get labels(){return this._labels}get names(){return this._names}get numClasses(){return this._labels.length}async call(e,t={}){return this.predict(e,t)}async predict(e,r={}){let i=new t.SpeedTimer,o=typeof e==`string`?e:null,c=await a.loadImage(e);i.stage(`load`);let{tensor:l,scale:u,padLeft:d,padTop:f}=this._preprocess(c);i.stage(`preprocess`);let p=await this._session.run({[this._session.inputName]:l});i.stage(`inference`);let{perAnchor:m,prototypes:h}=this._splitOutputs(p),g=s.decodeYoloSeg(m.data,m.dims,h.data,h.dims,{numClasses:this._labels.length,inputWidth:this._inputSize[0],inputHeight:this._inputSize[1],originalWidth:c.width,originalHeight:c.height,padLeft:d,padTop:f,scale:u,confThreshold:r.confThreshold??this._confThreshold,iouThreshold:r.iouThreshold??this._iouThreshold,maxDetections:this._maxDetections,maskThreshold:this._maskThreshold}),_=(r.classes===void 0?g:(()=>{let e=new Set(r.classes);return g.filter(t=>e.has(t.classId))})()).map(e=>this._buildResult(c,e.bbox,e.classId,e.confidence,e.mask)),v=[c.height,c.width],y=this._buildBoxes(_,v),b=this._buildMasks(_,v);return i.stage(`postprocess`),[new n.SegmentationResults(y,b,_,this._names,c,v,o,i.speed())]}_preprocess(e){let[t,n]=this._inputSize,r=o.letterbox(e,t,n);return{tensor:o.toFloat32Tensor(o.toCHW(o.toFloat32(r.image),r.image.width,r.image.height,3),[1,3,r.image.height,r.image.width]),scale:r.scale,padLeft:r.padLeft,padTop:r.padTop}}_splitOutputs(e){let t,n;for(let r of this._session.outputNames){let i=e[r];i!==void 0&&(i.dims.length===3&&t===void 0?t=i:i.dims.length===4&&n===void 0&&(n=i))}if(t===void 0||n===void 0){let t=this._session.outputNames.map(t=>`${t}: ${JSON.stringify(e[t]?.dims??[])}`);throw Error(`Segmenter expected one 3-D and one 4-D output, got [${t.join(`, `)}].`)}return{perAnchor:t,prototypes:n}}_buildResult(t,n,r,i,a){let[o,s,c,l]=n.asIntXyxy(),u=Math.max(0,o),d=Math.max(0,s),f=Math.min(t.width,c),p=Math.min(t.height,l),m,h=a;if(f>u&&p>d&&a.data.length>0){let n=f-u,r=p-d,i=Math.min(a.width,n),o=Math.min(a.height,r),s=new Uint8Array(i*o*3);for(let e=0;e<o;e++){let n=((d+e)*t.width+u)*3,r=e*i*3,o=e*a.width;for(let e=0;e<i;e++)if(a.data[o+e]!==0){let i=n+e*3,a=r+e*3;s[a]=t.data[i],s[a+1]=t.data[i+1],s[a+2]=t.data[i+2]}}if(m=new e.RGBImage(s,i,o),i!==a.width||o!==a.height){let t=new Uint8Array(i*o);for(let e=0;e<o;e++)t.set(a.data.subarray(e*a.width,e*a.width+i),e*i);h=new e.Mask(t,i,o)}}else h=new e.Mask(new Uint8Array,0,0),m=new e.RGBImage(new Uint8Array,0,0);let g=this._names[r]??`class_${r}`;return{classId:r,className:g,confidence:i,bbox:n,cls:r,name:g,conf:i,box:n,mask:h,segmentedImage:m}}_buildBoxes(e,t){let r=e.length,i=new Float32Array(r*4),a=new Int32Array(r),o=new Float32Array(r);for(let t=0;t<r;t++){let n=e[t];i[t*4]=n.bbox.x1,i[t*4+1]=n.bbox.y1,i[t*4+2]=n.bbox.x2,i[t*4+3]=n.bbox.y2,a[t]=n.classId,o[t]=n.confidence}return new n.Boxes(i,a,o,t)}_buildMasks(e,t){let r=new Float32Array(e.length*4);for(let t=0;t<e.length;t++){let n=e[t];r[t*4]=n.bbox.x1,r[t*4+1]=n.bbox.y1,r[t*4+2]=n.bbox.x2,r[t*4+3]=n.bbox.y2}return new n.Masks(e.map(e=>e.mask),r,t)}};exports.Segmenter=l;
|
|
2
2
|
//# sourceMappingURL=segmenter.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segmenter.cjs","names":[],"sources":["../../../src/vision/tasks/segmenter.ts"],"sourcesContent":["/**\n * Instance-segmentation task using YOLO seg ONNX models (v8-seg / v11-seg / ...).\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { type ImageInput, loadImage } from \"../io/image\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport { decodeYoloSeg } from \"../postprocess/segmentation\";\nimport { letterbox, toCHW, toFloat32, toFloat32Tensor } from \"../preprocess/image\";\nimport { Boxes, Masks, SegmentationResults } from \"../results\";\nimport { VisionTask } from \"./base\";\nimport { type BoundingBox, type SegmentationResult, Mask, RGBImage } from \"../types\";\n\n/**\n * Decoder family for the segmentation head.\n *\n * - `\"yolo-seg\"`: YOLO instance-segmentation head with two outputs —\n * `[1, 4 + nc + nm, N]` per-anchor predictions plus `[1, nm, mh, mw]`\n * prototype masks. Covers YOLOv8-seg, v11-seg, v26-seg.\n *\n * The SDK does **not** auto-detect this — the caller is responsible for\n * picking a head that matches their export.\n */\nexport type SegmenterHead = \"yolo-seg\";\n\nexport interface SegmenterOptions extends OrtSessionOptions {\n /**\n * Decoder family for the segmentation head. Default `\"yolo-seg\"` covers\n * YOLOv8-seg/v11-seg/v26-seg.\n */\n readonly head?: SegmenterHead;\n /** Class label spec — see {@link resolveLabels}. Defaults to the COCO 80-class preset. */\n readonly labels?: LabelSpec;\n /** Number of classes — used to validate the supplied labels. */\n readonly numClasses?: number;\n /** Model input `[width, height]` for letterboxing. Defaults to `[640, 640]`. */\n readonly inputSize?: readonly [number, number];\n /** Default minimum class score to keep a candidate. */\n readonly confThreshold?: number;\n /** Default IoU threshold for non-maximum suppression. */\n readonly iouThreshold?: number;\n /** Maximum number of instances per image. */\n readonly maxDetections?: number;\n /** Probability cutoff applied to soft masks. Defaults to `0.5`. */\n readonly maskThreshold?: number;\n}\n\nexport interface SegmenterPredictOptions {\n readonly confThreshold?: number;\n readonly iouThreshold?: number;\n /**\n * If set, keep only instances whose `classId` is in this list.\n * Mirrors Ultralytics' `model.predict(img, classes=[0, 16])`.\n */\n readonly classes?: readonly number[];\n}\n\n/**\n * Instance segmenter for YOLO seg ONNX models (v8-seg / v11-seg / ...).\n *\n * The model is expected to expose two outputs:\n *\n * 1. `output0`: `(1, 4 + numClasses + numMaskCoefs, numAnchors)` — per-anchor\n * predictions (boxes, class scores, mask coefficients).\n * 2. `output1`: `(1, numMaskCoefs, maskH, maskW)` — prototype masks.\n *\n * `predict()` returns `Promise<SegmentationResults[]>` (length 1 for a\n * single image), mirroring Ultralytics' API. The envelope exposes:\n *\n * - `boxes`: bulk numpy view (`xyxy`, `xywh`, `xyxyn`, `xywhn`, `cls`, `conf`).\n * - `masks`: per-instance binary masks cropped to each box.\n * - per-instance {@link SegmentationResult} via iteration.\n *\n * @example\n * ```typescript\n * const seg = await Segmenter.create(\"/models/yolov8n-seg.onnx\");\n * const r = (await seg.predict(\"/images/street.jpg\"))[0];\n * for (const inst of r) {\n * console.log(inst.cls, inst.conf, inst.box.xyxy);\n * }\n * ```\n */\nexport class Segmenter extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _head: SegmenterHead,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _inputSize: readonly [number, number],\n private readonly _confThreshold: number,\n private readonly _iouThreshold: number,\n private readonly _maxDetections: number,\n private readonly _maskThreshold: number,\n ) {\n super(session);\n }\n\n /** Load the model and resolve labels. */\n static async create(model: ModelSource, options: SegmenterOptions = {}): Promise<Segmenter> {\n const head: SegmenterHead = options.head ?? \"yolo-seg\";\n if (head !== \"yolo-seg\") {\n throw new Error(`Unsupported segmenter head '${head}'. Supported: 'yolo-seg'.`);\n }\n const session = await OrtSession.create(model, options);\n const labels = resolveLabels(options.labels ?? \"coco\", {\n numClasses: options.numClasses,\n });\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n names[i] = labels[i] as string;\n }\n return new Segmenter(\n session,\n head,\n labels,\n names,\n options.inputSize ?? [640, 640],\n options.confThreshold ?? 0.25,\n options.iouThreshold ?? 0.45,\n options.maxDetections ?? 300,\n options.maskThreshold ?? 0.5,\n );\n }\n\n /** The decoder family used to interpret the model's output. */\n get head(): SegmenterHead {\n return this._head;\n }\n\n /** Class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Class id → class name dict (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /** Number of classes the model predicts. */\n get numClasses(): number {\n return this._labels.length;\n }\n\n /** Alias for {@link predict} (parity with PyTorch `nn.Module.__call__`). */\n async call(\n image: ImageInput,\n options: SegmenterPredictOptions = {},\n ): Promise<SegmentationResults[]> {\n return this.predict(image, options);\n }\n\n /** Run instance segmentation on a single image. */\n async predict(\n image: ImageInput,\n options: SegmenterPredictOptions = {},\n ): Promise<SegmentationResults[]> {\n const path = typeof image === \"string\" ? image : null;\n const original = await loadImage(image);\n const { tensor, scale, padLeft, padTop } = this._preprocess(original);\n const outputs = await this._session.run({ [this._session.inputName]: tensor });\n\n const { perAnchor, prototypes } = this._splitOutputs(outputs);\n\n const decodedAll = decodeYoloSeg(\n perAnchor.data as Float32Array,\n perAnchor.dims,\n prototypes.data as Float32Array,\n prototypes.dims,\n {\n numClasses: this._labels.length,\n inputWidth: this._inputSize[0],\n inputHeight: this._inputSize[1],\n originalWidth: original.width,\n originalHeight: original.height,\n padLeft,\n padTop,\n scale,\n confThreshold: options.confThreshold ?? this._confThreshold,\n iouThreshold: options.iouThreshold ?? this._iouThreshold,\n maxDetections: this._maxDetections,\n maskThreshold: this._maskThreshold,\n },\n );\n\n const decoded =\n options.classes !== undefined\n ? (() => {\n const allowed = new Set(options.classes);\n return decodedAll.filter((d) => allowed.has(d.classId));\n })()\n : decodedAll;\n\n const detections = decoded.map((d) =>\n this._buildResult(original, d.bbox, d.classId, d.confidence, d.mask),\n );\n\n const orig: readonly [number, number] = [original.height, original.width];\n return [\n new SegmentationResults(\n this._buildBoxes(detections, orig),\n this._buildMasks(detections, orig),\n detections,\n this._names,\n original,\n orig,\n path,\n ),\n ];\n }\n\n private _preprocess(image: RGBImage): {\n tensor: ort.Tensor;\n scale: number;\n padLeft: number;\n padTop: number;\n } {\n const [tw, th] = this._inputSize;\n const lb = letterbox(image, tw, th);\n const f32 = toFloat32(lb.image);\n const chw = toCHW(f32, lb.image.width, lb.image.height, 3);\n return {\n tensor: toFloat32Tensor(chw, [1, 3, lb.image.height, lb.image.width]),\n scale: lb.scale,\n padLeft: lb.padLeft,\n padTop: lb.padTop,\n };\n }\n\n private _splitOutputs(outputs: Record<string, ort.Tensor>): {\n perAnchor: ort.Tensor;\n prototypes: ort.Tensor;\n } {\n let perAnchor: ort.Tensor | undefined;\n let prototypes: ort.Tensor | undefined;\n for (const name of this._session.outputNames) {\n const t = outputs[name];\n if (t === undefined) continue;\n if (t.dims.length === 3 && perAnchor === undefined) {\n perAnchor = t;\n } else if (t.dims.length === 4 && prototypes === undefined) {\n prototypes = t;\n }\n }\n if (perAnchor === undefined || prototypes === undefined) {\n const shapes = this._session.outputNames.map(\n (n) => `${n}: ${JSON.stringify(outputs[n]?.dims ?? [])}`,\n );\n throw new Error(\n `Segmenter expected one 3-D and one 4-D output, got [${shapes.join(\", \")}].`,\n );\n }\n return { perAnchor, prototypes };\n }\n\n private _buildResult(\n original: RGBImage,\n bbox: BoundingBox,\n classId: number,\n confidence: number,\n mask: Mask,\n ): SegmentationResult {\n const [x1, y1, x2, y2] = bbox.asIntXyxy();\n const cx1 = Math.max(0, x1);\n const cy1 = Math.max(0, y1);\n const cx2 = Math.min(original.width, x2);\n const cy2 = Math.min(original.height, y2);\n\n let segmentedImage: RGBImage;\n let finalMask = mask;\n if (cx2 > cx1 && cy2 > cy1 && mask.data.length > 0) {\n const cropW = cx2 - cx1;\n const cropH = cy2 - cy1;\n const mw = Math.min(mask.width, cropW);\n const mh = Math.min(mask.height, cropH);\n const segData = new Uint8Array(mw * mh * 3);\n for (let row = 0; row < mh; row++) {\n const srcRowOffset = ((cy1 + row) * original.width + cx1) * 3;\n const dstRowOffset = row * mw * 3;\n const maskRowOffset = row * mask.width;\n for (let col = 0; col < mw; col++) {\n const m = mask.data[maskRowOffset + col];\n if (m !== 0) {\n const s = srcRowOffset + col * 3;\n const d = dstRowOffset + col * 3;\n segData[d] = original.data[s];\n segData[d + 1] = original.data[s + 1];\n segData[d + 2] = original.data[s + 2];\n }\n }\n }\n segmentedImage = new RGBImage(segData, mw, mh);\n if (mw !== mask.width || mh !== mask.height) {\n const trimmed = new Uint8Array(mw * mh);\n for (let row = 0; row < mh; row++) {\n trimmed.set(\n mask.data.subarray(row * mask.width, row * mask.width + mw),\n row * mw,\n );\n }\n finalMask = new Mask(trimmed, mw, mh);\n }\n } else {\n finalMask = new Mask(new Uint8Array(0), 0, 0);\n segmentedImage = new RGBImage(new Uint8Array(0), 0, 0);\n }\n\n const className = this._names[classId] ?? `class_${classId}`;\n\n return {\n classId,\n className,\n confidence,\n bbox,\n cls: classId,\n name: className,\n conf: confidence,\n box: bbox,\n mask: finalMask,\n segmentedImage,\n };\n }\n\n private _buildBoxes(\n detections: readonly SegmentationResult[],\n origShape: readonly [number, number],\n ): Boxes {\n const n = detections.length;\n const xyxy = new Float32Array(n * 4);\n const cls = new Int32Array(n);\n const conf = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const d = detections[i] as SegmentationResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n cls[i] = d.classId;\n conf[i] = d.confidence;\n }\n return new Boxes(xyxy, cls, conf, origShape);\n }\n\n private _buildMasks(\n detections: readonly SegmentationResult[],\n origShape: readonly [number, number],\n ): Masks {\n const xyxy = new Float32Array(detections.length * 4);\n for (let i = 0; i < detections.length; i++) {\n const d = detections[i] as SegmentationResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n }\n return new Masks(\n detections.map((d) => d.mask),\n xyxy,\n origShape,\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"segmenter.cjs","names":[],"sources":["../../../src/vision/tasks/segmenter.ts"],"sourcesContent":["/**\n * Instance-segmentation task using YOLO seg ONNX models (v8-seg / v11-seg / ...).\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { SpeedTimer } from \"../core/timing\";\nimport { type ImageInput, loadImage } from \"../io/image\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport { decodeYoloSeg } from \"../postprocess/segmentation\";\nimport { letterbox, toCHW, toFloat32, toFloat32Tensor } from \"../preprocess/image\";\nimport { Boxes, Masks, SegmentationResults } from \"../results\";\nimport { VisionTask } from \"./base\";\nimport { type BoundingBox, type SegmentationResult, Mask, RGBImage } from \"../types\";\n\n/**\n * Decoder family for the segmentation head.\n *\n * - `\"yolo-seg\"`: YOLO instance-segmentation head with two outputs —\n * `[1, 4 + nc + nm, N]` per-anchor predictions plus `[1, nm, mh, mw]`\n * prototype masks. Covers YOLOv8-seg, v11-seg, v26-seg.\n *\n * The SDK does **not** auto-detect this — the caller is responsible for\n * picking a head that matches their export.\n */\nexport type SegmenterHead = \"yolo-seg\";\n\nexport interface SegmenterOptions extends OrtSessionOptions {\n /**\n * Decoder family for the segmentation head. Default `\"yolo-seg\"` covers\n * YOLOv8-seg/v11-seg/v26-seg.\n */\n readonly head?: SegmenterHead;\n /** Class label spec — see {@link resolveLabels}. Defaults to the COCO 80-class preset. */\n readonly labels?: LabelSpec;\n /** Number of classes — used to validate the supplied labels. */\n readonly numClasses?: number;\n /** Model input `[width, height]` for letterboxing. Defaults to `[640, 640]`. */\n readonly inputSize?: readonly [number, number];\n /** Default minimum class score to keep a candidate. */\n readonly confThreshold?: number;\n /** Default IoU threshold for non-maximum suppression. */\n readonly iouThreshold?: number;\n /** Maximum number of instances per image. */\n readonly maxDetections?: number;\n /** Probability cutoff applied to soft masks. Defaults to `0.5`. */\n readonly maskThreshold?: number;\n}\n\nexport interface SegmenterPredictOptions {\n readonly confThreshold?: number;\n readonly iouThreshold?: number;\n /**\n * If set, keep only instances whose `classId` is in this list.\n * Mirrors Ultralytics' `model.predict(img, classes=[0, 16])`.\n */\n readonly classes?: readonly number[];\n}\n\n/**\n * Instance segmenter for YOLO seg ONNX models (v8-seg / v11-seg / ...).\n *\n * The model is expected to expose two outputs:\n *\n * 1. `output0`: `(1, 4 + numClasses + numMaskCoefs, numAnchors)` — per-anchor\n * predictions (boxes, class scores, mask coefficients).\n * 2. `output1`: `(1, numMaskCoefs, maskH, maskW)` — prototype masks.\n *\n * `predict()` returns `Promise<SegmentationResults[]>` (length 1 for a\n * single image), mirroring Ultralytics' API. The envelope exposes:\n *\n * - `boxes`: bulk numpy view (`xyxy`, `xywh`, `xyxyn`, `xywhn`, `cls`, `conf`).\n * - `masks`: per-instance binary masks cropped to each box.\n * - per-instance {@link SegmentationResult} via iteration.\n *\n * @example\n * ```typescript\n * const seg = await Segmenter.create(\"/models/yolov8n-seg.onnx\");\n * const r = (await seg.predict(\"/images/street.jpg\"))[0];\n * for (const inst of r) {\n * console.log(inst.cls, inst.conf, inst.box.xyxy);\n * }\n * ```\n */\nexport class Segmenter extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _head: SegmenterHead,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _inputSize: readonly [number, number],\n private readonly _confThreshold: number,\n private readonly _iouThreshold: number,\n private readonly _maxDetections: number,\n private readonly _maskThreshold: number,\n ) {\n super(session);\n }\n\n /** Load the model and resolve labels. */\n static async create(model: ModelSource, options: SegmenterOptions = {}): Promise<Segmenter> {\n const head: SegmenterHead = options.head ?? \"yolo-seg\";\n if (head !== \"yolo-seg\") {\n throw new Error(`Unsupported segmenter head '${head}'. Supported: 'yolo-seg'.`);\n }\n const session = await OrtSession.create(model, options);\n const labels = resolveLabels(options.labels ?? \"coco\", {\n numClasses: options.numClasses,\n });\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n names[i] = labels[i] as string;\n }\n return new Segmenter(\n session,\n head,\n labels,\n names,\n options.inputSize ?? [640, 640],\n options.confThreshold ?? 0.25,\n options.iouThreshold ?? 0.45,\n options.maxDetections ?? 300,\n options.maskThreshold ?? 0.5,\n );\n }\n\n /** The decoder family used to interpret the model's output. */\n get head(): SegmenterHead {\n return this._head;\n }\n\n /** Class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Class id → class name dict (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /** Number of classes the model predicts. */\n get numClasses(): number {\n return this._labels.length;\n }\n\n /** Alias for {@link predict} (parity with PyTorch `nn.Module.__call__`). */\n async call(\n image: ImageInput,\n options: SegmenterPredictOptions = {},\n ): Promise<SegmentationResults[]> {\n return this.predict(image, options);\n }\n\n /** Run instance segmentation on a single image. */\n async predict(\n image: ImageInput,\n options: SegmenterPredictOptions = {},\n ): Promise<SegmentationResults[]> {\n const timer = new SpeedTimer();\n const path = typeof image === \"string\" ? image : null;\n const original = await loadImage(image);\n timer.stage(\"load\");\n const { tensor, scale, padLeft, padTop } = this._preprocess(original);\n timer.stage(\"preprocess\");\n const outputs = await this._session.run({ [this._session.inputName]: tensor });\n timer.stage(\"inference\");\n\n const { perAnchor, prototypes } = this._splitOutputs(outputs);\n\n const decodedAll = decodeYoloSeg(\n perAnchor.data as Float32Array,\n perAnchor.dims,\n prototypes.data as Float32Array,\n prototypes.dims,\n {\n numClasses: this._labels.length,\n inputWidth: this._inputSize[0],\n inputHeight: this._inputSize[1],\n originalWidth: original.width,\n originalHeight: original.height,\n padLeft,\n padTop,\n scale,\n confThreshold: options.confThreshold ?? this._confThreshold,\n iouThreshold: options.iouThreshold ?? this._iouThreshold,\n maxDetections: this._maxDetections,\n maskThreshold: this._maskThreshold,\n },\n );\n\n const decoded =\n options.classes !== undefined\n ? (() => {\n const allowed = new Set(options.classes);\n return decodedAll.filter((d) => allowed.has(d.classId));\n })()\n : decodedAll;\n\n const detections = decoded.map((d) =>\n this._buildResult(original, d.bbox, d.classId, d.confidence, d.mask),\n );\n\n const orig: readonly [number, number] = [original.height, original.width];\n const boxes = this._buildBoxes(detections, orig);\n const masks = this._buildMasks(detections, orig);\n timer.stage(\"postprocess\");\n return [\n new SegmentationResults(\n boxes,\n masks,\n detections,\n this._names,\n original,\n orig,\n path,\n timer.speed(),\n ),\n ];\n }\n\n private _preprocess(image: RGBImage): {\n tensor: ort.Tensor;\n scale: number;\n padLeft: number;\n padTop: number;\n } {\n const [tw, th] = this._inputSize;\n const lb = letterbox(image, tw, th);\n const f32 = toFloat32(lb.image);\n const chw = toCHW(f32, lb.image.width, lb.image.height, 3);\n return {\n tensor: toFloat32Tensor(chw, [1, 3, lb.image.height, lb.image.width]),\n scale: lb.scale,\n padLeft: lb.padLeft,\n padTop: lb.padTop,\n };\n }\n\n private _splitOutputs(outputs: Record<string, ort.Tensor>): {\n perAnchor: ort.Tensor;\n prototypes: ort.Tensor;\n } {\n let perAnchor: ort.Tensor | undefined;\n let prototypes: ort.Tensor | undefined;\n for (const name of this._session.outputNames) {\n const t = outputs[name];\n if (t === undefined) continue;\n if (t.dims.length === 3 && perAnchor === undefined) {\n perAnchor = t;\n } else if (t.dims.length === 4 && prototypes === undefined) {\n prototypes = t;\n }\n }\n if (perAnchor === undefined || prototypes === undefined) {\n const shapes = this._session.outputNames.map(\n (n) => `${n}: ${JSON.stringify(outputs[n]?.dims ?? [])}`,\n );\n throw new Error(\n `Segmenter expected one 3-D and one 4-D output, got [${shapes.join(\", \")}].`,\n );\n }\n return { perAnchor, prototypes };\n }\n\n private _buildResult(\n original: RGBImage,\n bbox: BoundingBox,\n classId: number,\n confidence: number,\n mask: Mask,\n ): SegmentationResult {\n const [x1, y1, x2, y2] = bbox.asIntXyxy();\n const cx1 = Math.max(0, x1);\n const cy1 = Math.max(0, y1);\n const cx2 = Math.min(original.width, x2);\n const cy2 = Math.min(original.height, y2);\n\n let segmentedImage: RGBImage;\n let finalMask = mask;\n if (cx2 > cx1 && cy2 > cy1 && mask.data.length > 0) {\n const cropW = cx2 - cx1;\n const cropH = cy2 - cy1;\n const mw = Math.min(mask.width, cropW);\n const mh = Math.min(mask.height, cropH);\n const segData = new Uint8Array(mw * mh * 3);\n for (let row = 0; row < mh; row++) {\n const srcRowOffset = ((cy1 + row) * original.width + cx1) * 3;\n const dstRowOffset = row * mw * 3;\n const maskRowOffset = row * mask.width;\n for (let col = 0; col < mw; col++) {\n const m = mask.data[maskRowOffset + col];\n if (m !== 0) {\n const s = srcRowOffset + col * 3;\n const d = dstRowOffset + col * 3;\n segData[d] = original.data[s];\n segData[d + 1] = original.data[s + 1];\n segData[d + 2] = original.data[s + 2];\n }\n }\n }\n segmentedImage = new RGBImage(segData, mw, mh);\n if (mw !== mask.width || mh !== mask.height) {\n const trimmed = new Uint8Array(mw * mh);\n for (let row = 0; row < mh; row++) {\n trimmed.set(\n mask.data.subarray(row * mask.width, row * mask.width + mw),\n row * mw,\n );\n }\n finalMask = new Mask(trimmed, mw, mh);\n }\n } else {\n finalMask = new Mask(new Uint8Array(0), 0, 0);\n segmentedImage = new RGBImage(new Uint8Array(0), 0, 0);\n }\n\n const className = this._names[classId] ?? `class_${classId}`;\n\n return {\n classId,\n className,\n confidence,\n bbox,\n cls: classId,\n name: className,\n conf: confidence,\n box: bbox,\n mask: finalMask,\n segmentedImage,\n };\n }\n\n private _buildBoxes(\n detections: readonly SegmentationResult[],\n origShape: readonly [number, number],\n ): Boxes {\n const n = detections.length;\n const xyxy = new Float32Array(n * 4);\n const cls = new Int32Array(n);\n const conf = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const d = detections[i] as SegmentationResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n cls[i] = d.classId;\n conf[i] = d.confidence;\n }\n return new Boxes(xyxy, cls, conf, origShape);\n }\n\n private _buildMasks(\n detections: readonly SegmentationResult[],\n origShape: readonly [number, number],\n ): Masks {\n const xyxy = new Float32Array(detections.length * 4);\n for (let i = 0; i < detections.length; i++) {\n const d = detections[i] as SegmentationResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n }\n return new Masks(\n detections.map((d) => d.mask),\n xyxy,\n origShape,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Mask as e, RGBImage as t } from "../types.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
2
|
+
import { SpeedTimer as n } from "../core/timing.js";
|
|
3
|
+
import { Boxes as r, Masks as i, SegmentationResults as a } from "../results.js";
|
|
4
|
+
import { resolveLabels as o } from "../labels.js";
|
|
5
|
+
import { OrtSession as s } from "../core/session.js";
|
|
6
|
+
import { loadImage as c } from "../io/image.js";
|
|
7
|
+
import { letterbox as l, toCHW as u, toFloat32 as d, toFloat32Tensor as f } from "../preprocess/image.js";
|
|
8
|
+
import { decodeYoloSeg as p } from "../postprocess/segmentation.js";
|
|
9
|
+
import { VisionTask as m } from "./base.js";
|
|
9
10
|
//#region src/vision/tasks/segmenter.ts
|
|
10
|
-
var
|
|
11
|
+
var h = class h extends m {
|
|
11
12
|
_head;
|
|
12
13
|
_labels;
|
|
13
14
|
_names;
|
|
@@ -22,9 +23,9 @@ var m = class m extends p {
|
|
|
22
23
|
static async create(e, t = {}) {
|
|
23
24
|
let n = t.head ?? "yolo-seg";
|
|
24
25
|
if (n !== "yolo-seg") throw Error(`Unsupported segmenter head '${n}'. Supported: 'yolo-seg'.`);
|
|
25
|
-
let r = await
|
|
26
|
-
for (let e = 0; e < i.length; e++)
|
|
27
|
-
return new
|
|
26
|
+
let r = await s.create(e, t), i = o(t.labels ?? "coco", { numClasses: t.numClasses }), a = {};
|
|
27
|
+
for (let e = 0; e < i.length; e++) a[e] = i[e];
|
|
28
|
+
return new h(r, n, i, a, t.inputSize ?? [640, 640], t.confThreshold ?? .25, t.iouThreshold ?? .45, t.maxDetections ?? 300, t.maskThreshold ?? .5);
|
|
28
29
|
}
|
|
29
30
|
get head() {
|
|
30
31
|
return this._head;
|
|
@@ -42,29 +43,35 @@ var m = class m extends p {
|
|
|
42
43
|
return this.predict(e, t);
|
|
43
44
|
}
|
|
44
45
|
async predict(e, t = {}) {
|
|
45
|
-
let n = typeof e == "string" ? e : null,
|
|
46
|
+
let r = new n(), i = typeof e == "string" ? e : null, o = await c(e);
|
|
47
|
+
r.stage("load");
|
|
48
|
+
let { tensor: s, scale: l, padLeft: u, padTop: d } = this._preprocess(o);
|
|
49
|
+
r.stage("preprocess");
|
|
50
|
+
let f = await this._session.run({ [this._session.inputName]: s });
|
|
51
|
+
r.stage("inference");
|
|
52
|
+
let { perAnchor: m, prototypes: h } = this._splitOutputs(f), g = p(m.data, m.dims, h.data, h.dims, {
|
|
46
53
|
numClasses: this._labels.length,
|
|
47
54
|
inputWidth: this._inputSize[0],
|
|
48
55
|
inputHeight: this._inputSize[1],
|
|
49
|
-
originalWidth:
|
|
50
|
-
originalHeight:
|
|
51
|
-
padLeft:
|
|
52
|
-
padTop:
|
|
53
|
-
scale:
|
|
56
|
+
originalWidth: o.width,
|
|
57
|
+
originalHeight: o.height,
|
|
58
|
+
padLeft: u,
|
|
59
|
+
padTop: d,
|
|
60
|
+
scale: l,
|
|
54
61
|
confThreshold: t.confThreshold ?? this._confThreshold,
|
|
55
62
|
iouThreshold: t.iouThreshold ?? this._iouThreshold,
|
|
56
63
|
maxDetections: this._maxDetections,
|
|
57
64
|
maskThreshold: this._maskThreshold
|
|
58
|
-
}),
|
|
65
|
+
}), _ = (t.classes === void 0 ? g : (() => {
|
|
59
66
|
let e = new Set(t.classes);
|
|
60
|
-
return
|
|
61
|
-
})()).map((e) => this._buildResult(
|
|
62
|
-
return
|
|
67
|
+
return g.filter((t) => e.has(t.classId));
|
|
68
|
+
})()).map((e) => this._buildResult(o, e.bbox, e.classId, e.confidence, e.mask)), v = [o.height, o.width], y = this._buildBoxes(_, v), b = this._buildMasks(_, v);
|
|
69
|
+
return r.stage("postprocess"), [new a(y, b, _, this._names, o, v, i, r.speed())];
|
|
63
70
|
}
|
|
64
71
|
_preprocess(e) {
|
|
65
|
-
let [t, n] = this._inputSize, r =
|
|
72
|
+
let [t, n] = this._inputSize, r = l(e, t, n);
|
|
66
73
|
return {
|
|
67
|
-
tensor:
|
|
74
|
+
tensor: f(u(d(r.image), r.image.width, r.image.height, 3), [
|
|
68
75
|
1,
|
|
69
76
|
3,
|
|
70
77
|
r.image.height,
|
|
@@ -122,12 +129,12 @@ var m = class m extends p {
|
|
|
122
129
|
};
|
|
123
130
|
}
|
|
124
131
|
_buildBoxes(e, t) {
|
|
125
|
-
let
|
|
126
|
-
for (let t = 0; t <
|
|
132
|
+
let n = e.length, i = new Float32Array(n * 4), a = new Int32Array(n), o = new Float32Array(n);
|
|
133
|
+
for (let t = 0; t < n; t++) {
|
|
127
134
|
let n = e[t];
|
|
128
135
|
i[t * 4] = n.bbox.x1, i[t * 4 + 1] = n.bbox.y1, i[t * 4 + 2] = n.bbox.x2, i[t * 4 + 3] = n.bbox.y2, a[t] = n.classId, o[t] = n.confidence;
|
|
129
136
|
}
|
|
130
|
-
return new
|
|
137
|
+
return new r(i, a, o, t);
|
|
131
138
|
}
|
|
132
139
|
_buildMasks(e, t) {
|
|
133
140
|
let n = new Float32Array(e.length * 4);
|
|
@@ -135,10 +142,10 @@ var m = class m extends p {
|
|
|
135
142
|
let r = e[t];
|
|
136
143
|
n[t * 4] = r.bbox.x1, n[t * 4 + 1] = r.bbox.y1, n[t * 4 + 2] = r.bbox.x2, n[t * 4 + 3] = r.bbox.y2;
|
|
137
144
|
}
|
|
138
|
-
return new
|
|
145
|
+
return new i(e.map((e) => e.mask), n, t);
|
|
139
146
|
}
|
|
140
147
|
};
|
|
141
148
|
//#endregion
|
|
142
|
-
export {
|
|
149
|
+
export { h as Segmenter };
|
|
143
150
|
|
|
144
151
|
//# sourceMappingURL=segmenter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segmenter.js","names":[],"sources":["../../../src/vision/tasks/segmenter.ts"],"sourcesContent":["/**\n * Instance-segmentation task using YOLO seg ONNX models (v8-seg / v11-seg / ...).\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { type ImageInput, loadImage } from \"../io/image\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport { decodeYoloSeg } from \"../postprocess/segmentation\";\nimport { letterbox, toCHW, toFloat32, toFloat32Tensor } from \"../preprocess/image\";\nimport { Boxes, Masks, SegmentationResults } from \"../results\";\nimport { VisionTask } from \"./base\";\nimport { type BoundingBox, type SegmentationResult, Mask, RGBImage } from \"../types\";\n\n/**\n * Decoder family for the segmentation head.\n *\n * - `\"yolo-seg\"`: YOLO instance-segmentation head with two outputs —\n * `[1, 4 + nc + nm, N]` per-anchor predictions plus `[1, nm, mh, mw]`\n * prototype masks. Covers YOLOv8-seg, v11-seg, v26-seg.\n *\n * The SDK does **not** auto-detect this — the caller is responsible for\n * picking a head that matches their export.\n */\nexport type SegmenterHead = \"yolo-seg\";\n\nexport interface SegmenterOptions extends OrtSessionOptions {\n /**\n * Decoder family for the segmentation head. Default `\"yolo-seg\"` covers\n * YOLOv8-seg/v11-seg/v26-seg.\n */\n readonly head?: SegmenterHead;\n /** Class label spec — see {@link resolveLabels}. Defaults to the COCO 80-class preset. */\n readonly labels?: LabelSpec;\n /** Number of classes — used to validate the supplied labels. */\n readonly numClasses?: number;\n /** Model input `[width, height]` for letterboxing. Defaults to `[640, 640]`. */\n readonly inputSize?: readonly [number, number];\n /** Default minimum class score to keep a candidate. */\n readonly confThreshold?: number;\n /** Default IoU threshold for non-maximum suppression. */\n readonly iouThreshold?: number;\n /** Maximum number of instances per image. */\n readonly maxDetections?: number;\n /** Probability cutoff applied to soft masks. Defaults to `0.5`. */\n readonly maskThreshold?: number;\n}\n\nexport interface SegmenterPredictOptions {\n readonly confThreshold?: number;\n readonly iouThreshold?: number;\n /**\n * If set, keep only instances whose `classId` is in this list.\n * Mirrors Ultralytics' `model.predict(img, classes=[0, 16])`.\n */\n readonly classes?: readonly number[];\n}\n\n/**\n * Instance segmenter for YOLO seg ONNX models (v8-seg / v11-seg / ...).\n *\n * The model is expected to expose two outputs:\n *\n * 1. `output0`: `(1, 4 + numClasses + numMaskCoefs, numAnchors)` — per-anchor\n * predictions (boxes, class scores, mask coefficients).\n * 2. `output1`: `(1, numMaskCoefs, maskH, maskW)` — prototype masks.\n *\n * `predict()` returns `Promise<SegmentationResults[]>` (length 1 for a\n * single image), mirroring Ultralytics' API. The envelope exposes:\n *\n * - `boxes`: bulk numpy view (`xyxy`, `xywh`, `xyxyn`, `xywhn`, `cls`, `conf`).\n * - `masks`: per-instance binary masks cropped to each box.\n * - per-instance {@link SegmentationResult} via iteration.\n *\n * @example\n * ```typescript\n * const seg = await Segmenter.create(\"/models/yolov8n-seg.onnx\");\n * const r = (await seg.predict(\"/images/street.jpg\"))[0];\n * for (const inst of r) {\n * console.log(inst.cls, inst.conf, inst.box.xyxy);\n * }\n * ```\n */\nexport class Segmenter extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _head: SegmenterHead,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _inputSize: readonly [number, number],\n private readonly _confThreshold: number,\n private readonly _iouThreshold: number,\n private readonly _maxDetections: number,\n private readonly _maskThreshold: number,\n ) {\n super(session);\n }\n\n /** Load the model and resolve labels. */\n static async create(model: ModelSource, options: SegmenterOptions = {}): Promise<Segmenter> {\n const head: SegmenterHead = options.head ?? \"yolo-seg\";\n if (head !== \"yolo-seg\") {\n throw new Error(`Unsupported segmenter head '${head}'. Supported: 'yolo-seg'.`);\n }\n const session = await OrtSession.create(model, options);\n const labels = resolveLabels(options.labels ?? \"coco\", {\n numClasses: options.numClasses,\n });\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n names[i] = labels[i] as string;\n }\n return new Segmenter(\n session,\n head,\n labels,\n names,\n options.inputSize ?? [640, 640],\n options.confThreshold ?? 0.25,\n options.iouThreshold ?? 0.45,\n options.maxDetections ?? 300,\n options.maskThreshold ?? 0.5,\n );\n }\n\n /** The decoder family used to interpret the model's output. */\n get head(): SegmenterHead {\n return this._head;\n }\n\n /** Class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Class id → class name dict (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /** Number of classes the model predicts. */\n get numClasses(): number {\n return this._labels.length;\n }\n\n /** Alias for {@link predict} (parity with PyTorch `nn.Module.__call__`). */\n async call(\n image: ImageInput,\n options: SegmenterPredictOptions = {},\n ): Promise<SegmentationResults[]> {\n return this.predict(image, options);\n }\n\n /** Run instance segmentation on a single image. */\n async predict(\n image: ImageInput,\n options: SegmenterPredictOptions = {},\n ): Promise<SegmentationResults[]> {\n const path = typeof image === \"string\" ? image : null;\n const original = await loadImage(image);\n const { tensor, scale, padLeft, padTop } = this._preprocess(original);\n const outputs = await this._session.run({ [this._session.inputName]: tensor });\n\n const { perAnchor, prototypes } = this._splitOutputs(outputs);\n\n const decodedAll = decodeYoloSeg(\n perAnchor.data as Float32Array,\n perAnchor.dims,\n prototypes.data as Float32Array,\n prototypes.dims,\n {\n numClasses: this._labels.length,\n inputWidth: this._inputSize[0],\n inputHeight: this._inputSize[1],\n originalWidth: original.width,\n originalHeight: original.height,\n padLeft,\n padTop,\n scale,\n confThreshold: options.confThreshold ?? this._confThreshold,\n iouThreshold: options.iouThreshold ?? this._iouThreshold,\n maxDetections: this._maxDetections,\n maskThreshold: this._maskThreshold,\n },\n );\n\n const decoded =\n options.classes !== undefined\n ? (() => {\n const allowed = new Set(options.classes);\n return decodedAll.filter((d) => allowed.has(d.classId));\n })()\n : decodedAll;\n\n const detections = decoded.map((d) =>\n this._buildResult(original, d.bbox, d.classId, d.confidence, d.mask),\n );\n\n const orig: readonly [number, number] = [original.height, original.width];\n return [\n new SegmentationResults(\n this._buildBoxes(detections, orig),\n this._buildMasks(detections, orig),\n detections,\n this._names,\n original,\n orig,\n path,\n ),\n ];\n }\n\n private _preprocess(image: RGBImage): {\n tensor: ort.Tensor;\n scale: number;\n padLeft: number;\n padTop: number;\n } {\n const [tw, th] = this._inputSize;\n const lb = letterbox(image, tw, th);\n const f32 = toFloat32(lb.image);\n const chw = toCHW(f32, lb.image.width, lb.image.height, 3);\n return {\n tensor: toFloat32Tensor(chw, [1, 3, lb.image.height, lb.image.width]),\n scale: lb.scale,\n padLeft: lb.padLeft,\n padTop: lb.padTop,\n };\n }\n\n private _splitOutputs(outputs: Record<string, ort.Tensor>): {\n perAnchor: ort.Tensor;\n prototypes: ort.Tensor;\n } {\n let perAnchor: ort.Tensor | undefined;\n let prototypes: ort.Tensor | undefined;\n for (const name of this._session.outputNames) {\n const t = outputs[name];\n if (t === undefined) continue;\n if (t.dims.length === 3 && perAnchor === undefined) {\n perAnchor = t;\n } else if (t.dims.length === 4 && prototypes === undefined) {\n prototypes = t;\n }\n }\n if (perAnchor === undefined || prototypes === undefined) {\n const shapes = this._session.outputNames.map(\n (n) => `${n}: ${JSON.stringify(outputs[n]?.dims ?? [])}`,\n );\n throw new Error(\n `Segmenter expected one 3-D and one 4-D output, got [${shapes.join(\", \")}].`,\n );\n }\n return { perAnchor, prototypes };\n }\n\n private _buildResult(\n original: RGBImage,\n bbox: BoundingBox,\n classId: number,\n confidence: number,\n mask: Mask,\n ): SegmentationResult {\n const [x1, y1, x2, y2] = bbox.asIntXyxy();\n const cx1 = Math.max(0, x1);\n const cy1 = Math.max(0, y1);\n const cx2 = Math.min(original.width, x2);\n const cy2 = Math.min(original.height, y2);\n\n let segmentedImage: RGBImage;\n let finalMask = mask;\n if (cx2 > cx1 && cy2 > cy1 && mask.data.length > 0) {\n const cropW = cx2 - cx1;\n const cropH = cy2 - cy1;\n const mw = Math.min(mask.width, cropW);\n const mh = Math.min(mask.height, cropH);\n const segData = new Uint8Array(mw * mh * 3);\n for (let row = 0; row < mh; row++) {\n const srcRowOffset = ((cy1 + row) * original.width + cx1) * 3;\n const dstRowOffset = row * mw * 3;\n const maskRowOffset = row * mask.width;\n for (let col = 0; col < mw; col++) {\n const m = mask.data[maskRowOffset + col];\n if (m !== 0) {\n const s = srcRowOffset + col * 3;\n const d = dstRowOffset + col * 3;\n segData[d] = original.data[s];\n segData[d + 1] = original.data[s + 1];\n segData[d + 2] = original.data[s + 2];\n }\n }\n }\n segmentedImage = new RGBImage(segData, mw, mh);\n if (mw !== mask.width || mh !== mask.height) {\n const trimmed = new Uint8Array(mw * mh);\n for (let row = 0; row < mh; row++) {\n trimmed.set(\n mask.data.subarray(row * mask.width, row * mask.width + mw),\n row * mw,\n );\n }\n finalMask = new Mask(trimmed, mw, mh);\n }\n } else {\n finalMask = new Mask(new Uint8Array(0), 0, 0);\n segmentedImage = new RGBImage(new Uint8Array(0), 0, 0);\n }\n\n const className = this._names[classId] ?? `class_${classId}`;\n\n return {\n classId,\n className,\n confidence,\n bbox,\n cls: classId,\n name: className,\n conf: confidence,\n box: bbox,\n mask: finalMask,\n segmentedImage,\n };\n }\n\n private _buildBoxes(\n detections: readonly SegmentationResult[],\n origShape: readonly [number, number],\n ): Boxes {\n const n = detections.length;\n const xyxy = new Float32Array(n * 4);\n const cls = new Int32Array(n);\n const conf = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const d = detections[i] as SegmentationResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n cls[i] = d.classId;\n conf[i] = d.confidence;\n }\n return new Boxes(xyxy, cls, conf, origShape);\n }\n\n private _buildMasks(\n detections: readonly SegmentationResult[],\n origShape: readonly [number, number],\n ): Masks {\n const xyxy = new Float32Array(detections.length * 4);\n for (let i = 0; i < detections.length; i++) {\n const d = detections[i] as SegmentationResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n }\n return new Masks(\n detections.map((d) => d.mask),\n xyxy,\n origShape,\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"segmenter.js","names":[],"sources":["../../../src/vision/tasks/segmenter.ts"],"sourcesContent":["/**\n * Instance-segmentation task using YOLO seg ONNX models (v8-seg / v11-seg / ...).\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { SpeedTimer } from \"../core/timing\";\nimport { type ImageInput, loadImage } from \"../io/image\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport { decodeYoloSeg } from \"../postprocess/segmentation\";\nimport { letterbox, toCHW, toFloat32, toFloat32Tensor } from \"../preprocess/image\";\nimport { Boxes, Masks, SegmentationResults } from \"../results\";\nimport { VisionTask } from \"./base\";\nimport { type BoundingBox, type SegmentationResult, Mask, RGBImage } from \"../types\";\n\n/**\n * Decoder family for the segmentation head.\n *\n * - `\"yolo-seg\"`: YOLO instance-segmentation head with two outputs —\n * `[1, 4 + nc + nm, N]` per-anchor predictions plus `[1, nm, mh, mw]`\n * prototype masks. Covers YOLOv8-seg, v11-seg, v26-seg.\n *\n * The SDK does **not** auto-detect this — the caller is responsible for\n * picking a head that matches their export.\n */\nexport type SegmenterHead = \"yolo-seg\";\n\nexport interface SegmenterOptions extends OrtSessionOptions {\n /**\n * Decoder family for the segmentation head. Default `\"yolo-seg\"` covers\n * YOLOv8-seg/v11-seg/v26-seg.\n */\n readonly head?: SegmenterHead;\n /** Class label spec — see {@link resolveLabels}. Defaults to the COCO 80-class preset. */\n readonly labels?: LabelSpec;\n /** Number of classes — used to validate the supplied labels. */\n readonly numClasses?: number;\n /** Model input `[width, height]` for letterboxing. Defaults to `[640, 640]`. */\n readonly inputSize?: readonly [number, number];\n /** Default minimum class score to keep a candidate. */\n readonly confThreshold?: number;\n /** Default IoU threshold for non-maximum suppression. */\n readonly iouThreshold?: number;\n /** Maximum number of instances per image. */\n readonly maxDetections?: number;\n /** Probability cutoff applied to soft masks. Defaults to `0.5`. */\n readonly maskThreshold?: number;\n}\n\nexport interface SegmenterPredictOptions {\n readonly confThreshold?: number;\n readonly iouThreshold?: number;\n /**\n * If set, keep only instances whose `classId` is in this list.\n * Mirrors Ultralytics' `model.predict(img, classes=[0, 16])`.\n */\n readonly classes?: readonly number[];\n}\n\n/**\n * Instance segmenter for YOLO seg ONNX models (v8-seg / v11-seg / ...).\n *\n * The model is expected to expose two outputs:\n *\n * 1. `output0`: `(1, 4 + numClasses + numMaskCoefs, numAnchors)` — per-anchor\n * predictions (boxes, class scores, mask coefficients).\n * 2. `output1`: `(1, numMaskCoefs, maskH, maskW)` — prototype masks.\n *\n * `predict()` returns `Promise<SegmentationResults[]>` (length 1 for a\n * single image), mirroring Ultralytics' API. The envelope exposes:\n *\n * - `boxes`: bulk numpy view (`xyxy`, `xywh`, `xyxyn`, `xywhn`, `cls`, `conf`).\n * - `masks`: per-instance binary masks cropped to each box.\n * - per-instance {@link SegmentationResult} via iteration.\n *\n * @example\n * ```typescript\n * const seg = await Segmenter.create(\"/models/yolov8n-seg.onnx\");\n * const r = (await seg.predict(\"/images/street.jpg\"))[0];\n * for (const inst of r) {\n * console.log(inst.cls, inst.conf, inst.box.xyxy);\n * }\n * ```\n */\nexport class Segmenter extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _head: SegmenterHead,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _inputSize: readonly [number, number],\n private readonly _confThreshold: number,\n private readonly _iouThreshold: number,\n private readonly _maxDetections: number,\n private readonly _maskThreshold: number,\n ) {\n super(session);\n }\n\n /** Load the model and resolve labels. */\n static async create(model: ModelSource, options: SegmenterOptions = {}): Promise<Segmenter> {\n const head: SegmenterHead = options.head ?? \"yolo-seg\";\n if (head !== \"yolo-seg\") {\n throw new Error(`Unsupported segmenter head '${head}'. Supported: 'yolo-seg'.`);\n }\n const session = await OrtSession.create(model, options);\n const labels = resolveLabels(options.labels ?? \"coco\", {\n numClasses: options.numClasses,\n });\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n names[i] = labels[i] as string;\n }\n return new Segmenter(\n session,\n head,\n labels,\n names,\n options.inputSize ?? [640, 640],\n options.confThreshold ?? 0.25,\n options.iouThreshold ?? 0.45,\n options.maxDetections ?? 300,\n options.maskThreshold ?? 0.5,\n );\n }\n\n /** The decoder family used to interpret the model's output. */\n get head(): SegmenterHead {\n return this._head;\n }\n\n /** Class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Class id → class name dict (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /** Number of classes the model predicts. */\n get numClasses(): number {\n return this._labels.length;\n }\n\n /** Alias for {@link predict} (parity with PyTorch `nn.Module.__call__`). */\n async call(\n image: ImageInput,\n options: SegmenterPredictOptions = {},\n ): Promise<SegmentationResults[]> {\n return this.predict(image, options);\n }\n\n /** Run instance segmentation on a single image. */\n async predict(\n image: ImageInput,\n options: SegmenterPredictOptions = {},\n ): Promise<SegmentationResults[]> {\n const timer = new SpeedTimer();\n const path = typeof image === \"string\" ? image : null;\n const original = await loadImage(image);\n timer.stage(\"load\");\n const { tensor, scale, padLeft, padTop } = this._preprocess(original);\n timer.stage(\"preprocess\");\n const outputs = await this._session.run({ [this._session.inputName]: tensor });\n timer.stage(\"inference\");\n\n const { perAnchor, prototypes } = this._splitOutputs(outputs);\n\n const decodedAll = decodeYoloSeg(\n perAnchor.data as Float32Array,\n perAnchor.dims,\n prototypes.data as Float32Array,\n prototypes.dims,\n {\n numClasses: this._labels.length,\n inputWidth: this._inputSize[0],\n inputHeight: this._inputSize[1],\n originalWidth: original.width,\n originalHeight: original.height,\n padLeft,\n padTop,\n scale,\n confThreshold: options.confThreshold ?? this._confThreshold,\n iouThreshold: options.iouThreshold ?? this._iouThreshold,\n maxDetections: this._maxDetections,\n maskThreshold: this._maskThreshold,\n },\n );\n\n const decoded =\n options.classes !== undefined\n ? (() => {\n const allowed = new Set(options.classes);\n return decodedAll.filter((d) => allowed.has(d.classId));\n })()\n : decodedAll;\n\n const detections = decoded.map((d) =>\n this._buildResult(original, d.bbox, d.classId, d.confidence, d.mask),\n );\n\n const orig: readonly [number, number] = [original.height, original.width];\n const boxes = this._buildBoxes(detections, orig);\n const masks = this._buildMasks(detections, orig);\n timer.stage(\"postprocess\");\n return [\n new SegmentationResults(\n boxes,\n masks,\n detections,\n this._names,\n original,\n orig,\n path,\n timer.speed(),\n ),\n ];\n }\n\n private _preprocess(image: RGBImage): {\n tensor: ort.Tensor;\n scale: number;\n padLeft: number;\n padTop: number;\n } {\n const [tw, th] = this._inputSize;\n const lb = letterbox(image, tw, th);\n const f32 = toFloat32(lb.image);\n const chw = toCHW(f32, lb.image.width, lb.image.height, 3);\n return {\n tensor: toFloat32Tensor(chw, [1, 3, lb.image.height, lb.image.width]),\n scale: lb.scale,\n padLeft: lb.padLeft,\n padTop: lb.padTop,\n };\n }\n\n private _splitOutputs(outputs: Record<string, ort.Tensor>): {\n perAnchor: ort.Tensor;\n prototypes: ort.Tensor;\n } {\n let perAnchor: ort.Tensor | undefined;\n let prototypes: ort.Tensor | undefined;\n for (const name of this._session.outputNames) {\n const t = outputs[name];\n if (t === undefined) continue;\n if (t.dims.length === 3 && perAnchor === undefined) {\n perAnchor = t;\n } else if (t.dims.length === 4 && prototypes === undefined) {\n prototypes = t;\n }\n }\n if (perAnchor === undefined || prototypes === undefined) {\n const shapes = this._session.outputNames.map(\n (n) => `${n}: ${JSON.stringify(outputs[n]?.dims ?? [])}`,\n );\n throw new Error(\n `Segmenter expected one 3-D and one 4-D output, got [${shapes.join(\", \")}].`,\n );\n }\n return { perAnchor, prototypes };\n }\n\n private _buildResult(\n original: RGBImage,\n bbox: BoundingBox,\n classId: number,\n confidence: number,\n mask: Mask,\n ): SegmentationResult {\n const [x1, y1, x2, y2] = bbox.asIntXyxy();\n const cx1 = Math.max(0, x1);\n const cy1 = Math.max(0, y1);\n const cx2 = Math.min(original.width, x2);\n const cy2 = Math.min(original.height, y2);\n\n let segmentedImage: RGBImage;\n let finalMask = mask;\n if (cx2 > cx1 && cy2 > cy1 && mask.data.length > 0) {\n const cropW = cx2 - cx1;\n const cropH = cy2 - cy1;\n const mw = Math.min(mask.width, cropW);\n const mh = Math.min(mask.height, cropH);\n const segData = new Uint8Array(mw * mh * 3);\n for (let row = 0; row < mh; row++) {\n const srcRowOffset = ((cy1 + row) * original.width + cx1) * 3;\n const dstRowOffset = row * mw * 3;\n const maskRowOffset = row * mask.width;\n for (let col = 0; col < mw; col++) {\n const m = mask.data[maskRowOffset + col];\n if (m !== 0) {\n const s = srcRowOffset + col * 3;\n const d = dstRowOffset + col * 3;\n segData[d] = original.data[s];\n segData[d + 1] = original.data[s + 1];\n segData[d + 2] = original.data[s + 2];\n }\n }\n }\n segmentedImage = new RGBImage(segData, mw, mh);\n if (mw !== mask.width || mh !== mask.height) {\n const trimmed = new Uint8Array(mw * mh);\n for (let row = 0; row < mh; row++) {\n trimmed.set(\n mask.data.subarray(row * mask.width, row * mask.width + mw),\n row * mw,\n );\n }\n finalMask = new Mask(trimmed, mw, mh);\n }\n } else {\n finalMask = new Mask(new Uint8Array(0), 0, 0);\n segmentedImage = new RGBImage(new Uint8Array(0), 0, 0);\n }\n\n const className = this._names[classId] ?? `class_${classId}`;\n\n return {\n classId,\n className,\n confidence,\n bbox,\n cls: classId,\n name: className,\n conf: confidence,\n box: bbox,\n mask: finalMask,\n segmentedImage,\n };\n }\n\n private _buildBoxes(\n detections: readonly SegmentationResult[],\n origShape: readonly [number, number],\n ): Boxes {\n const n = detections.length;\n const xyxy = new Float32Array(n * 4);\n const cls = new Int32Array(n);\n const conf = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const d = detections[i] as SegmentationResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n cls[i] = d.classId;\n conf[i] = d.confidence;\n }\n return new Boxes(xyxy, cls, conf, origShape);\n }\n\n private _buildMasks(\n detections: readonly SegmentationResult[],\n origShape: readonly [number, number],\n ): Masks {\n const xyxy = new Float32Array(detections.length * 4);\n for (let i = 0; i < detections.length; i++) {\n const d = detections[i] as SegmentationResult;\n xyxy[i * 4] = d.bbox.x1;\n xyxy[i * 4 + 1] = d.bbox.y1;\n xyxy[i * 4 + 2] = d.bbox.x2;\n xyxy[i * 4 + 3] = d.bbox.y2;\n }\n return new Masks(\n detections.map((d) => d.mask),\n xyxy,\n origShape,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
const e=require("../audio/media-access.cjs");let t=require("react");var n={video:{facingMode:{ideal:`environment`},width:{ideal:1920},height:{ideal:1080}},audio:!1};function r({kind:e,message:t}){return{kind:e===`not-found`?`no-camera`:e,message:t}}function i(t){return r(e.classifyMediaError(t,`camera`))}function a(a={}){let[o,s]=(0,t.useState)(`loading`),[c,l]=(0,t.useState)(null),[u,d]=(0,t.useState)(null),[f,p]=(0,t.useState)(0),m=(0,t.useRef)(null),h=(0,t.useRef)(a.constraints??n);(0,t.useEffect)(()=>{h.current=a.constraints??n},[a.constraints]);let g=a.enabled??!0;return(0,t.useEffect)(()=>{let t=!1,n=null,a=null;async function o(){if(!g){s(`idle`),l(null);return}if(s(`loading`),l(null),typeof navigator>`u`||!navigator.mediaDevices||typeof navigator.mediaDevices.getUserMedia!=`function`){t||(l(r(e.missingCaptureApiError(`camera`))),s(`error`));return}try{if(n=await navigator.mediaDevices.getUserMedia(h.current),t){n.getTracks().forEach(e=>e.stop());return}let e=m.current;if(!e){n.getTracks().forEach(e=>e.stop());return}a=e,e.srcObject=n,d(n),await e.play().catch(()=>void 0),t||s(`ready`)}catch(e){t||(l(i(e)),s(`error`))}}return o(),()=>{t=!0,n&&n.getTracks().forEach(e=>e.stop()),a&&(a.srcObject=null),d(null)}},[f,g]),{status:o,error:c,videoRef:m,stream:u,retry:()=>p(e=>e+1)}}exports.useCameraStream=a;
|
|
2
2
|
//# sourceMappingURL=use-camera-stream.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-camera-stream.cjs","names":[],"sources":["../../src/vision/use-camera-stream.ts"],"sourcesContent":["import { useEffect, useRef, useState, type RefObject } from \"react\";\n\n/** Lifecycle status of the camera stream. */\nexport type CameraStreamStatus = \"idle\" | \"loading\" | \"ready\" | \"error\";\n\n/** Classified reason a camera stream could not be acquired. */\nexport type CameraStreamErrorKind =\n \"unsupported\" | \"permission-denied\" | \"no-camera\" | \"in-use\" | \"insecure\" | \"unknown\";\n\n/** A classified camera error with a human-readable, English message. */\nexport interface CameraStreamError {\n kind: CameraStreamErrorKind;\n message: string;\n}\n\n/** Options for {@link useCameraStream}. */\nexport interface UseCameraStreamOptions {\n /**\n * Constraints passed to `getUserMedia`. Defaults to the rear\n * (\"environment\") camera at Full-HD ideal resolution with audio off.\n * Read when the stream (re)starts — change it and call `retry()` to apply.\n */\n constraints?: MediaStreamConstraints;\n}\n\n/** Value returned by {@link useCameraStream}. */\nexport interface UseCameraStreamApi {\n /** Current lifecycle status. */\n status: CameraStreamStatus;\n /** The classified error, or `null` while not in the `error` status. */\n error: CameraStreamError | null;\n /** Attach to a `<video ref={…} />`. The stream is wired to it once ready. */\n videoRef: RefObject<HTMLVideoElement | null>;\n /** Manually re-attempt after an error (e.g. the user changed permissions). */\n retry: () => void;\n}\n\n/** Rear-camera Full-HD defaults used when no `constraints` are supplied. */\nconst DEFAULT_CONSTRAINTS: MediaStreamConstraints = {\n video: {\n facingMode: { ideal: \"environment\" },\n width: { ideal: 1920 },\n height: { ideal: 1080 },\n },\n audio: false,\n};\n\n/**\n * Map an unknown failure into a {@link CameraStreamError}. Secure-context and\n * environment checks run first (they are the reason `getUserMedia` is missing\n * or rejects), then the `DOMException.name` is mapped to a stable `kind`.\n */\nfunction classifyError(err: unknown): CameraStreamError {\n if (typeof window === \"undefined\") {\n return { kind: \"unsupported\", message: \"Camera is unavailable in this environment.\" };\n }\n if (!window.isSecureContext) {\n return {\n kind: \"insecure\",\n message: \"Camera access requires a secure (HTTPS) connection.\",\n };\n }\n if (err instanceof DOMException) {\n switch (err.name) {\n case \"NotAllowedError\":\n case \"SecurityError\":\n return {\n kind: \"permission-denied\",\n message: \"Camera permission denied. Enable access in your browser settings.\",\n };\n case \"NotFoundError\":\n case \"OverconstrainedError\":\n return {\n kind: \"no-camera\",\n message: \"No camera available on this device.\",\n };\n case \"NotReadableError\":\n case \"AbortError\":\n return {\n kind: \"in-use\",\n message: \"The camera is in use by another app. Close it and try again.\",\n };\n }\n }\n return {\n kind: \"unknown\",\n message:\n err instanceof Error ? err.message : \"Unexpected error while accessing the camera.\",\n };\n}\n\n/**\n * Acquire a `MediaStream` via `getUserMedia`, attach it to a `<video>` element,\n * and expose status/error so the page can render permission and error states.\n * The stream is automatically released on unmount or retry.\n *\n * Defaults to the rear (\"environment\") camera; desktops fall back to whatever\n * single camera they expose. Pass `options.constraints` to override.\n *\n * Implementation notes:\n * - Cleanup detaches the stream from a *snapshotted* video node, so it releases\n * the same element it attached to even if the page remounts the `<video>`.\n * - When `getUserMedia` is missing, an insecure context is the usual cause, so\n * the hook prefers that (actionable) error; otherwise it reports `unsupported`.\n * - `video.play()` rejections are swallowed: autoplay may be blocked, but the\n * user gesture that opened the camera usually counts and playback resumes on\n * the next interaction.\n *\n * @param options - optional configuration (see {@link UseCameraStreamOptions}).\n * @returns The stream status, classified error, a `videoRef` to attach, and a\n * `retry()` to re-attempt acquisition.\n */\nexport function useCameraStream(options: UseCameraStreamOptions = {}): UseCameraStreamApi {\n const [status, setStatus] = useState<CameraStreamStatus>(\"loading\");\n const [error, setError] = useState<CameraStreamError | null>(null);\n const [retryToken, setRetryToken] = useState(0);\n const videoRef = useRef<HTMLVideoElement | null>(null);\n const constraintsRef = useRef<MediaStreamConstraints>(\n options.constraints ?? DEFAULT_CONSTRAINTS,\n );\n useEffect(() => {\n constraintsRef.current = options.constraints ?? DEFAULT_CONSTRAINTS;\n }, [options.constraints]);\n\n useEffect(() => {\n let cancelled = false;\n let stream: MediaStream | null = null;\n let attachedVideo: HTMLVideoElement | null = null;\n\n async function start(): Promise<void> {\n setStatus(\"loading\");\n setError(null);\n\n if (\n typeof navigator === \"undefined\" ||\n !navigator.mediaDevices ||\n typeof navigator.mediaDevices.getUserMedia !== \"function\"\n ) {\n if (!cancelled) {\n setError(\n typeof window !== \"undefined\" && !window.isSecureContext\n ? classifyError(null)\n : {\n kind: \"unsupported\",\n message: \"Camera capture is not supported in this browser.\",\n },\n );\n setStatus(\"error\");\n }\n return;\n }\n\n try {\n stream = await navigator.mediaDevices.getUserMedia(constraintsRef.current);\n if (cancelled) {\n stream.getTracks().forEach((track) => track.stop());\n return;\n }\n const video = videoRef.current;\n if (!video) {\n stream.getTracks().forEach((track) => track.stop());\n return;\n }\n attachedVideo = video;\n video.srcObject = stream;\n await video.play().catch(() => undefined);\n if (!cancelled) setStatus(\"ready\");\n } catch (err) {\n if (!cancelled) {\n setError(classifyError(err));\n setStatus(\"error\");\n }\n }\n }\n\n void start();\n\n return () => {\n cancelled = true;\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n if (attachedVideo) {\n attachedVideo.srcObject = null;\n }\n };\n }, [retryToken]);\n\n return {\n status,\n error,\n videoRef,\n retry: () => setRetryToken((n) => n + 1),\n };\n}\n"],"mappings":"uBAsCA,IAAM,EAA8C,CAChD,MAAO,CACH,WAAY,CAAE,MAAO,aAAc,EACnC,MAAO,CAAE,MAAO,IAAK,EACrB,OAAQ,CAAE,MAAO,IAAK,CAC1B,EACA,MAAO,EACX,EAOA,SAAS,EAAc,EAAiC,CACpD,GAAI,OAAO,OAAW,IAClB,MAAO,CAAE,KAAM,cAAe,QAAS,4CAA6C,EAExF,GAAI,CAAC,OAAO,gBACR,MAAO,CACH,KAAM,WACN,QAAS,qDACb,EAEJ,GAAI,aAAe,aACf,OAAQ,EAAI,KAAZ,CACI,IAAK,kBACL,IAAK,gBACD,MAAO,CACH,KAAM,oBACN,QAAS,mEACb,EACJ,IAAK,gBACL,IAAK,uBACD,MAAO,CACH,KAAM,YACN,QAAS,qCACb,EACJ,IAAK,mBACL,IAAK,aACD,MAAO,CACH,KAAM,SACN,QAAS,8DACb,CACR,CAEJ,MAAO,CACH,KAAM,UACN,QACI,aAAe,MAAQ,EAAI,QAAU,8CAC7C,CACJ,CAuBA,SAAgB,EAAgB,EAAkC,CAAC,EAAuB,CACtF,GAAM,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,CAA0C,SAAS,EAC5D,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAA+C,IAAI,EAC3D,CAAC,EAAY,IAAA,EAAA,EAAA,SAAA,CAA0B,CAAC,EACxC,GAAA,EAAA,EAAA,OAAA,CAA2C,IAAI,EAC/C,GAAA,EAAA,EAAA,OAAA,CACF,EAAQ,aAAe,CAC3B,EAqEA,OApEA,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAe,QAAU,EAAQ,aAAe,CACpD,EAAG,CAAC,EAAQ,WAAW,CAAC,GAExB,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAI,EAAY,GACZ,EAA6B,KAC7B,EAAyC,KAE7C,eAAe,GAAuB,CAIlC,GAHA,EAAU,SAAS,EACnB,EAAS,IAAI,EAGT,OAAO,UAAc,KACrB,CAAC,UAAU,cACX,OAAO,UAAU,aAAa,cAAiB,WACjD,CACO,IACD,EACI,OAAO,OAAW,KAAe,CAAC,OAAO,gBACnC,EAAc,IAAI,EAClB,CACI,KAAM,cACN,QAAS,kDACb,CACV,EACA,EAAU,OAAO,GAErB,MACJ,CAEA,GAAI,CAEA,GADA,EAAS,MAAM,UAAU,aAAa,aAAa,EAAe,OAAO,EACrE,EAAW,CACX,EAAO,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAClD,MACJ,CACA,IAAM,EAAQ,EAAS,QACvB,GAAI,CAAC,EAAO,CACR,EAAO,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAClD,MACJ,CACA,EAAgB,EAChB,EAAM,UAAY,EAClB,MAAM,EAAM,KAAK,CAAC,CAAC,UAAY,IAAA,EAAS,EACnC,GAAW,EAAU,OAAO,CACrC,OAAS,EAAK,CACL,IACD,EAAS,EAAc,CAAG,CAAC,EAC3B,EAAU,OAAO,EAEzB,CACJ,CAIA,OAFA,EAAW,MAEE,CACT,EAAY,GACR,GACA,EAAO,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAElD,IACA,EAAc,UAAY,KAElC,CACJ,EAAG,CAAC,CAAU,CAAC,EAER,CACH,SACA,QACA,WACA,UAAa,EAAe,GAAM,EAAI,CAAC,CAC3C,CACJ"}
|
|
1
|
+
{"version":3,"file":"use-camera-stream.cjs","names":[],"sources":["../../src/vision/use-camera-stream.ts"],"sourcesContent":["import { useEffect, useRef, useState, type RefObject } from \"react\";\n\nimport {\n classifyMediaError,\n missingCaptureApiError,\n type MediaAccessError,\n} from \"@/audio/media-access\";\n\n/** Lifecycle status of the camera stream. */\nexport type CameraStreamStatus = \"idle\" | \"loading\" | \"ready\" | \"error\";\n\n/** Classified reason a camera stream could not be acquired. */\nexport type CameraStreamErrorKind =\n \"unsupported\" | \"permission-denied\" | \"no-camera\" | \"in-use\" | \"insecure\" | \"unknown\";\n\n/** A classified camera error with a human-readable, English message. */\nexport interface CameraStreamError {\n kind: CameraStreamErrorKind;\n message: string;\n}\n\n/** Options for {@link useCameraStream}. */\nexport interface UseCameraStreamOptions {\n /**\n * Constraints passed to `getUserMedia`. Defaults to the rear\n * (\"environment\") camera at Full-HD ideal resolution with audio off.\n * Read when the stream (re)starts — change it and call `retry()` to apply.\n */\n constraints?: MediaStreamConstraints;\n /**\n * Hold off acquiring the camera until this is `true`. Default `true`.\n *\n * The point is to be able to *not* prompt. A permission prompt costs the user a\n * decision and, if they refuse, costs the app the feature permanently — so a\n * surface that already knows it cannot do its job (a barcode scanner in a browser\n * with no decoder) must not open the camera to then say so. Flipping this to `false`\n * also releases a stream that is already live.\n */\n enabled?: boolean;\n}\n\n/** Value returned by {@link useCameraStream}. */\nexport interface UseCameraStreamApi {\n /** Current lifecycle status. */\n status: CameraStreamStatus;\n /** The classified error, or `null` while not in the `error` status. */\n error: CameraStreamError | null;\n /** Attach to a `<video ref={…} />`. The stream is wired to it once ready. */\n videoRef: RefObject<HTMLVideoElement | null>;\n /**\n * The live stream, or `null`.\n *\n * Exposed for the things that need the **track** rather than the picture — the LED\n * torch (`useTorch`), the real frame size from `getSettings()`, recording it with\n * `useVideoRecorder`. Do not stop it yourself: the hook owns its lifetime and\n * releases it on unmount and on `retry()`.\n */\n stream: MediaStream | null;\n /** Manually re-attempt after an error (e.g. the user changed permissions). */\n retry: () => void;\n}\n\n/** Rear-camera Full-HD defaults used when no `constraints` are supplied. */\nconst DEFAULT_CONSTRAINTS: MediaStreamConstraints = {\n video: {\n facingMode: { ideal: \"environment\" },\n width: { ideal: 1920 },\n height: { ideal: 1080 },\n },\n audio: false,\n};\n\n/**\n * Re-label a {@link MediaAccessError} as a {@link CameraStreamError}.\n *\n * The classification itself lives in one place for the whole SDK\n * (`classifyMediaError`), so a microphone and a camera failure are never explained\n * two different ways. Only the name of one kind differs: this surface shipped\n * `\"no-camera\"` where the shared taxonomy says `\"not-found\"`, and renaming a\n * published union member would break every consumer switching on it.\n *\n * @param error - The shared classification.\n * @returns The same error under this module's kind names.\n */\nfunction toCameraError({ kind, message }: MediaAccessError): CameraStreamError {\n return { kind: kind === \"not-found\" ? \"no-camera\" : kind, message };\n}\n\n/**\n * Map an unknown `getUserMedia` failure into a {@link CameraStreamError}. Secure-context\n * and environment checks run first (they are the reason `getUserMedia` is missing or\n * rejects), then the `DOMException.name` is mapped to a stable `kind`.\n */\nfunction classifyError(err: unknown): CameraStreamError {\n return toCameraError(classifyMediaError(err, \"camera\"));\n}\n\n/**\n * Acquire a `MediaStream` via `getUserMedia`, attach it to a `<video>` element,\n * and expose status/error so the page can render permission and error states.\n * The stream is automatically released on unmount or retry.\n *\n * Defaults to the rear (\"environment\") camera; desktops fall back to whatever\n * single camera they expose. Pass `options.constraints` to override, or\n * `options.enabled: false` to render the surface without prompting for the camera\n * at all.\n *\n * Implementation notes:\n * - Cleanup detaches the stream from a *snapshotted* video node, so it releases\n * the same element it attached to even if the page remounts the `<video>`.\n * - When `getUserMedia` is missing, an insecure context is the usual cause, so\n * the hook prefers that (actionable) error; otherwise it reports `unsupported`.\n * - `video.play()` rejections are swallowed: autoplay may be blocked, but the\n * user gesture that opened the camera usually counts and playback resumes on\n * the next interaction.\n *\n * @param options - optional configuration (see {@link UseCameraStreamOptions}).\n * @returns The stream status, classified error, a `videoRef` to attach, the live\n * `stream` for whatever needs the track itself, and a `retry()` to re-attempt\n * acquisition.\n */\nexport function useCameraStream(options: UseCameraStreamOptions = {}): UseCameraStreamApi {\n const [status, setStatus] = useState<CameraStreamStatus>(\"loading\");\n const [error, setError] = useState<CameraStreamError | null>(null);\n const [live, setLive] = useState<MediaStream | null>(null);\n const [retryToken, setRetryToken] = useState(0);\n const videoRef = useRef<HTMLVideoElement | null>(null);\n const constraintsRef = useRef<MediaStreamConstraints>(\n options.constraints ?? DEFAULT_CONSTRAINTS,\n );\n useEffect(() => {\n constraintsRef.current = options.constraints ?? DEFAULT_CONSTRAINTS;\n }, [options.constraints]);\n\n const enabled = options.enabled ?? true;\n\n useEffect(() => {\n let cancelled = false;\n let stream: MediaStream | null = null;\n let attachedVideo: HTMLVideoElement | null = null;\n\n async function start(): Promise<void> {\n if (!enabled) {\n setStatus(\"idle\");\n setError(null);\n return;\n }\n setStatus(\"loading\");\n setError(null);\n\n if (\n typeof navigator === \"undefined\" ||\n !navigator.mediaDevices ||\n typeof navigator.mediaDevices.getUserMedia !== \"function\"\n ) {\n if (!cancelled) {\n setError(toCameraError(missingCaptureApiError(\"camera\")));\n setStatus(\"error\");\n }\n return;\n }\n\n try {\n stream = await navigator.mediaDevices.getUserMedia(constraintsRef.current);\n if (cancelled) {\n stream.getTracks().forEach((track) => track.stop());\n return;\n }\n const video = videoRef.current;\n if (!video) {\n stream.getTracks().forEach((track) => track.stop());\n return;\n }\n attachedVideo = video;\n video.srcObject = stream;\n setLive(stream);\n await video.play().catch(() => undefined);\n if (!cancelled) setStatus(\"ready\");\n } catch (err) {\n if (!cancelled) {\n setError(classifyError(err));\n setStatus(\"error\");\n }\n }\n }\n\n void start();\n\n return () => {\n cancelled = true;\n if (stream) {\n stream.getTracks().forEach((track) => track.stop());\n }\n if (attachedVideo) {\n attachedVideo.srcObject = null;\n }\n setLive(null);\n };\n }, [retryToken, enabled]);\n\n return {\n status,\n error,\n videoRef,\n stream: live,\n retry: () => setRetryToken((n) => n + 1),\n };\n}\n"],"mappings":"oEA+DA,IAAM,EAA8C,CAChD,MAAO,CACH,WAAY,CAAE,MAAO,aAAc,EACnC,MAAO,CAAE,MAAO,IAAK,EACrB,OAAQ,CAAE,MAAO,IAAK,CAC1B,EACA,MAAO,EACX,EAcA,SAAS,EAAc,CAAE,OAAM,WAAgD,CAC3E,MAAO,CAAE,KAAM,IAAS,YAAc,YAAc,EAAM,SAAQ,CACtE,CAOA,SAAS,EAAc,EAAiC,CACpD,OAAO,EAAc,EAAA,mBAAmB,EAAK,QAAQ,CAAC,CAC1D,CA0BA,SAAgB,EAAgB,EAAkC,CAAC,EAAuB,CACtF,GAAM,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,CAA0C,SAAS,EAC5D,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAA+C,IAAI,EAC3D,CAAC,EAAM,IAAA,EAAA,EAAA,SAAA,CAAwC,IAAI,EACnD,CAAC,EAAY,IAAA,EAAA,EAAA,SAAA,CAA0B,CAAC,EACxC,GAAA,EAAA,EAAA,OAAA,CAA2C,IAAI,EAC/C,GAAA,EAAA,EAAA,OAAA,CACF,EAAQ,aAAe,CAC3B,GACA,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAe,QAAU,EAAQ,aAAe,CACpD,EAAG,CAAC,EAAQ,WAAW,CAAC,EAExB,IAAM,EAAU,EAAQ,SAAW,GAkEnC,OAhEA,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAI,EAAY,GACZ,EAA6B,KAC7B,EAAyC,KAE7C,eAAe,GAAuB,CAClC,GAAI,CAAC,EAAS,CACV,EAAU,MAAM,EAChB,EAAS,IAAI,EACb,MACJ,CAIA,GAHA,EAAU,SAAS,EACnB,EAAS,IAAI,EAGT,OAAO,UAAc,KACrB,CAAC,UAAU,cACX,OAAO,UAAU,aAAa,cAAiB,WACjD,CACO,IACD,EAAS,EAAc,EAAA,uBAAuB,QAAQ,CAAC,CAAC,EACxD,EAAU,OAAO,GAErB,MACJ,CAEA,GAAI,CAEA,GADA,EAAS,MAAM,UAAU,aAAa,aAAa,EAAe,OAAO,EACrE,EAAW,CACX,EAAO,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAClD,MACJ,CACA,IAAM,EAAQ,EAAS,QACvB,GAAI,CAAC,EAAO,CACR,EAAO,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAClD,MACJ,CACA,EAAgB,EAChB,EAAM,UAAY,EAClB,EAAQ,CAAM,EACd,MAAM,EAAM,KAAK,CAAC,CAAC,UAAY,IAAA,EAAS,EACnC,GAAW,EAAU,OAAO,CACrC,OAAS,EAAK,CACL,IACD,EAAS,EAAc,CAAG,CAAC,EAC3B,EAAU,OAAO,EAEzB,CACJ,CAIA,OAFA,EAAW,MAEE,CACT,EAAY,GACR,GACA,EAAO,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAElD,IACA,EAAc,UAAY,MAE9B,EAAQ,IAAI,CAChB,CACJ,EAAG,CAAC,EAAY,CAAO,CAAC,EAEjB,CACH,SACA,QACA,WACA,OAAQ,EACR,UAAa,EAAe,GAAM,EAAI,CAAC,CAC3C,CACJ"}
|