{"version":3,"file":"detector.cjs","names":[],"sources":["../../../src/vision/tasks/detector.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\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\";\n\nimport { type ImageInput, loadImage } from \"../io/image\";\nimport { detectionNumClasses, resolveInputSize } from \"../core/graph\";\nimport { modelNames } from \"../core/metadata\";\nimport { type LabelSpec, defaultLabels, resolveLabels } from \"../labels\";\nimport { decodeYolo } from \"../postprocess/detection\";\nimport { toFloat32Tensor } from \"../preprocess/image\";\nimport { LetterboxPipeline, zeroTensorData } from \"../preprocess/pipeline\";\nimport { Boxes, DetectionResults } from \"../results\";\nimport { VisionTask, requireDetections } 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    /**\n     * Model input `[width, height]` in pixels for letterboxing.\n     *\n     * Only used when the model's graph leaves its spatial axes dynamic: a graph\n     * that declares a static size always wins, since that is the only shape ONNX\n     * Runtime will accept. Defaults to `[640, 640]`.\n     */\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     * If `true`, a run that finds nothing throws {@link NoDetectionsError}\n     * instead of returning an empty envelope. Default `false`, because looking\n     * and finding nothing is a successful inference. Turn it on when an empty\n     * result means the surrounding pipeline should stop rather than carry on with\n     * zero rows. Can be overridden per `predict` call.\n     */\n    readonly raiseOnEmpty?: boolean;\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    /** Override the constructor's `raiseOnEmpty` setting for this call. */\n    readonly raiseOnEmpty?: boolean;\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        private readonly _raiseOnEmpty: boolean,\n    ) {\n        super(session);\n    }\n\n    private _pipelineCache: LetterboxPipeline | null = null;\n\n    /**\n     * Run the model once on a zero-filled tensor, paying one-time costs up front.\n     *\n     * The first inference of a session is not representative: WebGPU compiles its\n     * shaders on it and the WASM backend faults in its arenas, which on a phone\n     * can turn the first frame into seconds while every later frame is tens of\n     * milliseconds. Calling this while a loading spinner is still up moves that\n     * cost somewhere the user is already waiting.\n     *\n     * @param runs How many warm-up inferences to run. One is enough for WASM;\n     *   WebGPU sometimes settles on the second.\n     */\n    async warmup(runs: number = 1): Promise<void> {\n        const [tw, th] = this._inputSize;\n        for (let i = 0; i < runs; i++) {\n            const tensor = toFloat32Tensor(zeroTensorData(tw, th), [1, 3, th, tw]);\n            await this._session.run({ [this._session.inputName]: tensor });\n        }\n    }\n\n    /**\n     * The fused preprocessing pipeline, built on first use.\n     *\n     * Lazily, because constructing it allocates canvases: a task built in an\n     * environment without a canvas implementation stays constructible, and only\n     * fails if it is actually asked to preprocess something.\n     */\n    private get _pipeline(): LetterboxPipeline {\n        if (this._pipelineCache === null) {\n            this._pipelineCache = new LetterboxPipeline(this._inputSize[0], this._inputSize[1]);\n        }\n        return this._pipelineCache;\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 numClasses =\n            options.numClasses ?? detectionNumClasses(session.outputShape) ?? undefined;\n        const labels = resolveLabels(\n            options.labels ?? modelNames(session.metadata) ?? defaultLabels(numClasses),\n            { 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            resolveInputSize({\n                graphShape: session.inputShape,\n                requested: options.inputSize,\n                fallback: [640, 640],\n            }),\n            options.confThreshold ?? 0.25,\n            options.iouThreshold ?? 0.45,\n            options.maxDetections ?? 300,\n            options.raiseOnEmpty ?? false,\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    /**\n     * The `[width, height]` this task preprocesses to.\n     *\n     * Resolved at creation time from the model's graph when it declares a static\n     * input, so reading it back tells you the resolution inference really runs at\n     * — not merely what was requested.\n     */\n    get inputSize(): readonly [number, number] {\n        return this._inputSize;\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        this._pipeline.release();\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 threshold = options.confThreshold ?? this._confThreshold;\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: threshold,\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        requireDetections(decoded.length, {\n            raiseOnEmpty: options.raiseOnEmpty ?? this._raiseOnEmpty,\n            confThreshold: threshold,\n            classes: options.classes,\n            path,\n        });\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    /**\n     * Letterbox and pack the image into the tensor the model expects.\n     *\n     * Runs through {@link LetterboxPipeline}, which fuses the resize, the\n     * padding and the HWC-to-CHW float conversion into one `drawImage` plus one\n     * readback loop, and reuses its output buffer between frames. The buffer is\n     * handed straight to ONNX Runtime, so {@link _pipeline.release} must not be\n     * called until the run resolves.\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 fused = this._pipeline.run(image);\n        return {\n            tensor: toFloat32Tensor(fused.data, [1, 3, th, tw]),\n            scale: fused.scale,\n            padLeft: fused.padLeft,\n            padTop: fused.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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"}