tempest-react-sdk 0.69.0 → 0.70.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.
Files changed (77) hide show
  1. package/README.md +1 -1
  2. package/dist/vision/core/canvas.cjs +1 -1
  3. package/dist/vision/core/canvas.cjs.map +1 -1
  4. package/dist/vision/core/canvas.js +37 -13
  5. package/dist/vision/core/canvas.js.map +1 -1
  6. package/dist/vision/core/dtypes.cjs +2 -0
  7. package/dist/vision/core/dtypes.cjs.map +1 -0
  8. package/dist/vision/core/dtypes.js +33 -0
  9. package/dist/vision/core/dtypes.js.map +1 -0
  10. package/dist/vision/core/graph.cjs +1 -1
  11. package/dist/vision/core/graph.cjs.map +1 -1
  12. package/dist/vision/core/graph.js +10 -2
  13. package/dist/vision/core/graph.js.map +1 -1
  14. package/dist/vision/core/metadata.cjs +2 -2
  15. package/dist/vision/core/metadata.cjs.map +1 -1
  16. package/dist/vision/core/metadata.js +103 -13
  17. package/dist/vision/core/metadata.js.map +1 -1
  18. package/dist/vision/core/providers.cjs +1 -1
  19. package/dist/vision/core/providers.cjs.map +1 -1
  20. package/dist/vision/core/providers.js +9 -6
  21. package/dist/vision/core/providers.js.map +1 -1
  22. package/dist/vision/core/session.cjs +1 -1
  23. package/dist/vision/core/session.cjs.map +1 -1
  24. package/dist/vision/core/session.js +115 -27
  25. package/dist/vision/core/session.js.map +1 -1
  26. package/dist/vision/fusion.cjs +1 -1
  27. package/dist/vision/fusion.cjs.map +1 -1
  28. package/dist/vision/fusion.js +28 -23
  29. package/dist/vision/fusion.js.map +1 -1
  30. package/dist/vision/index.cjs +1 -1
  31. package/dist/vision/index.cjs.map +1 -1
  32. package/dist/vision/index.js +20 -19
  33. package/dist/vision/index.js.map +1 -1
  34. package/dist/vision/io/image.cjs +1 -1
  35. package/dist/vision/io/image.cjs.map +1 -1
  36. package/dist/vision/io/image.js +55 -20
  37. package/dist/vision/io/image.js.map +1 -1
  38. package/dist/vision/postprocess/detection.cjs +1 -1
  39. package/dist/vision/postprocess/detection.cjs.map +1 -1
  40. package/dist/vision/postprocess/detection.js +42 -51
  41. package/dist/vision/postprocess/detection.js.map +1 -1
  42. package/dist/vision/postprocess/segmentation.cjs +1 -1
  43. package/dist/vision/postprocess/segmentation.cjs.map +1 -1
  44. package/dist/vision/postprocess/segmentation.js +45 -31
  45. package/dist/vision/postprocess/segmentation.js.map +1 -1
  46. package/dist/vision/preprocess/pipeline.cjs +1 -1
  47. package/dist/vision/preprocess/pipeline.cjs.map +1 -1
  48. package/dist/vision/preprocess/pipeline.js +24 -25
  49. package/dist/vision/preprocess/pipeline.js.map +1 -1
  50. package/dist/vision/tasks/base.cjs +1 -1
  51. package/dist/vision/tasks/base.cjs.map +1 -1
  52. package/dist/vision/tasks/base.js +22 -7
  53. package/dist/vision/tasks/base.js.map +1 -1
  54. package/dist/vision/tasks/classifier.cjs +1 -1
  55. package/dist/vision/tasks/classifier.cjs.map +1 -1
  56. package/dist/vision/tasks/classifier.js +64 -55
  57. package/dist/vision/tasks/classifier.js.map +1 -1
  58. package/dist/vision/tasks/detectClassify.cjs +1 -1
  59. package/dist/vision/tasks/detectClassify.cjs.map +1 -1
  60. package/dist/vision/tasks/detectClassify.js +80 -69
  61. package/dist/vision/tasks/detectClassify.js.map +1 -1
  62. package/dist/vision/tasks/detector.cjs +1 -1
  63. package/dist/vision/tasks/detector.cjs.map +1 -1
  64. package/dist/vision/tasks/detector.js +74 -68
  65. package/dist/vision/tasks/detector.js.map +1 -1
  66. package/dist/vision/tasks/segmenter.cjs +1 -1
  67. package/dist/vision/tasks/segmenter.cjs.map +1 -1
  68. package/dist/vision/tasks/segmenter.js +85 -62
  69. package/dist/vision/tasks/segmenter.js.map +1 -1
  70. package/dist/vision/types.cjs +1 -1
  71. package/dist/vision/types.cjs.map +1 -1
  72. package/dist/vision/types.js +24 -4
  73. package/dist/vision/types.js.map +1 -1
  74. package/dist/vision.cjs +1 -1
  75. package/dist/vision.d.ts +1153 -777
  76. package/dist/vision.js +22 -21
  77. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"detectClassify.cjs","names":[],"sources":["../../../src/vision/tasks/detectClassify.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Run a fused detect→classify pipeline in the browser.\n *\n * The file this loads was built by the Python SDK's `ort_vision_sdk.compose`,\n * and it already contains both models plus the crop-and-resize bridge between\n * them. That matters far more in a tab than on a server: two models mean two\n * `.onnx` downloads, two WASM/WebGPU session initializations, and a per-crop\n * round trip through JavaScript to slice, resize and restack the regions before\n * the second model can see them. A fused pipeline has one download, one session\n * and no round trip — the crops are produced and consumed inside the graph.\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { FusionError } from \"../core/exceptions\";\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { SpeedTimer } from \"../core/timing\";\nimport {\n INPUT_IMAGE,\n INPUT_PAD,\n INPUT_SCALE,\n INPUT_SOURCE,\n OUTPUT_BOXES,\n OUTPUT_CLASSES,\n OUTPUT_NUM_DETECTIONS,\n OUTPUT_PROBS,\n OUTPUT_SCORES,\n type FusionSpec,\n readFusionSpec,\n} from \"../fusion\";\nimport { type ImageInput, loadImage } from \"../io/image\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport { softmax, topK } from \"../postprocess/classification\";\nimport { toCHW, toFloat32, toFloat32Tensor } from \"../preprocess/image\";\nimport { LetterboxPipeline, zeroTensorData } from \"../preprocess/pipeline\";\nimport { Boxes, DetectClassifyResults } from \"../results\";\nimport {\n BoundingBox,\n RGBImage,\n type ClassProbability,\n type ClassificationResult,\n type DetectionResult,\n} from \"../types\";\nimport { VisionTask, requireDetections } from \"./base\";\n\nexport interface DetectClassifyOptions extends OrtSessionOptions {\n /**\n * Class label spec for the **detection** stage — see {@link resolveLabels}.\n * Defaults to the names recorded at fusion time, falling back to the COCO\n * 80-class preset when the fusion recorded none.\n */\n readonly labels?: LabelSpec;\n /**\n * Class label spec for the **classification** stage. Defaults to the recorded\n * names, falling back to generated `class_<id>` names.\n */\n readonly classifierLabels?: LabelSpec;\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 DetectClassifyPredictOptions {\n /**\n * Drop detections scoring below this. The graph's own NMS threshold was fixed\n * at fusion time and cannot be lowered here — this only filters further.\n */\n readonly confThreshold?: number;\n /** If set, keep only detections whose detector `classId` is in this list. */\n readonly classes?: readonly number[];\n /** Truncate each detection's `classification.probabilities` to its top-k entries. */\n readonly topK?: number;\n /** Override the constructor's `raiseOnEmpty` setting for this call. */\n readonly raiseOnEmpty?: boolean;\n}\n\n/**\n * Detector and classifier running as a single ONNX model.\n *\n * Everything the pipeline needs to know about itself — the resolution to\n * letterbox to, whether it wants the full-resolution image as well, whether its\n * classifier output still needs a softmax, the class names of both stages — was\n * written into the file at fusion time and is read back here. Nothing is\n * restated on the JavaScript side, so nothing can drift out of step with the\n * Python side that built it.\n *\n * @example\n * ```typescript\n * const pipeline = await DetectClassify.create(\"/models/pipeline.onnx\");\n * const result = (await pipeline.predict(\"/images/flock.jpg\"))[0];\n * for (const detection of result) {\n * console.log(detection.name, detection.conf, detection.classification?.name);\n * }\n * ```\n */\nexport class DetectClassify extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _spec: FusionSpec,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _classifierLabels: readonly string[],\n private readonly _classifierNames: Readonly<Record<number, string>>,\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 zero-filled inputs, paying one-time costs up front.\n *\n * Worth more here than on a single-stage task: a fused pipeline is two models\n * plus the bridge in one graph, so the first inference compiles shaders for\n * all of it. Calling this while a loading spinner is still up moves that cost\n * 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 [width, height] = this._spec.inputSize;\n for (let i = 0; i < runs; i++) {\n const feeds: Record<string, ort.Tensor> = {\n [INPUT_IMAGE]: toFloat32Tensor(zeroTensorData(width, height), [\n 1,\n 3,\n height,\n width,\n ]),\n };\n if (this._spec.needsSourceImage) {\n feeds[INPUT_SOURCE] = toFloat32Tensor(zeroTensorData(width, height), [\n 1,\n 3,\n height,\n width,\n ]);\n feeds[INPUT_SCALE] = toFloat32Tensor(new Float32Array([1]), [1]);\n feeds[INPUT_PAD] = toFloat32Tensor(new Float32Array([0, 0]), [2]);\n }\n await this._session.run(feeds);\n }\n }\n\n /**\n * The fused preprocessing pipeline, built on first use.\n *\n * Lazily, because constructing it allocates canvases: a pipeline 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 const [width, height] = this._spec.inputSize;\n this._pipelineCache = new LetterboxPipeline(width, height);\n }\n return this._pipelineCache;\n }\n\n /**\n * Load a fused pipeline and resolve both label spaces.\n *\n * @param model The fused `.onnx` — a URL, an `ArrayBuffer`, or bytes.\n * @param options Label overrides plus the usual session options.\n * @throws {@link FusionError} when the model carries no pipeline metadata,\n * i.e. it is a plain detector or classifier rather than something\n * `ort_vision_sdk.compose` produced.\n */\n static async create(\n model: ModelSource,\n options: DetectClassifyOptions = {},\n ): Promise<DetectClassify> {\n const session = await OrtSession.create(model, options);\n const spec = readFusionSpec(session.metadata);\n if (spec === null) {\n throw new FusionError(\n \"This model carries no fused-pipeline metadata, so DetectClassify cannot tell how to \" +\n \"drive it. Build one with ort_vision_sdk.compose.fuse_detect_classify, or load a \" +\n \"plain model with Detector/Classifier instead.\",\n );\n }\n\n const labels = resolveLabels(options.labels ?? spec.detectorNames ?? \"coco\");\n const classifierLabels = resolveLabels(options.classifierLabels ?? spec.classifierNames, {\n numClasses: classifierClasses(session) ?? undefined,\n });\n return new DetectClassify(\n session,\n spec,\n labels,\n indexNames(labels),\n classifierLabels,\n indexNames(classifierLabels),\n options.raiseOnEmpty ?? false,\n );\n }\n\n /** The pipeline configuration recorded in the model at fusion time. */\n get spec(): FusionSpec {\n return this._spec;\n }\n\n /** The `[width, height]` the detection stage runs at. */\n get inputSize(): readonly [number, number] {\n return this._spec.inputSize;\n }\n\n /** Detector class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Detector class id → class name (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /** Classifier class labels indexed by class id. */\n get classifierLabels(): readonly string[] {\n return this._classifierLabels;\n }\n\n /** Classifier class id → class name. */\n get classifierNames(): Readonly<Record<number, string>> {\n return this._classifierNames;\n }\n\n /**\n * Alias for {@link predict} — call the pipeline like a torch `nn.Module`.\n *\n * Use as `pipeline.call(img)` since JavaScript class instances are not\n * callable; for direct invocation, prefer `pipeline.predict(img)`.\n */\n async call(\n image: ImageInput,\n options: DetectClassifyPredictOptions = {},\n ): Promise<DetectClassifyResults[]> {\n return this.predict(image, options);\n }\n\n /**\n * Run the pipeline on a single image.\n *\n * The returned envelope carries a {@link Speed} breakdown in `speed`. Its\n * `inference` figure covers detection *and* classification, since the\n * pipeline runs them as one graph and no boundary between them is observable\n * from outside.\n */\n async predict(\n image: ImageInput,\n options: DetectClassifyPredictOptions = {},\n ): Promise<DetectClassifyResults[]> {\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 { feeds, scale, padLeft, padTop } = this._preprocess(original);\n timer.stage(\"preprocess\");\n const outputs = await this._session.run(feeds);\n this._pipeline.release();\n timer.stage(\"inference\");\n\n const probsTensor = output(outputs, OUTPUT_PROBS);\n const boxes = floats(outputs, OUTPUT_BOXES);\n const scores = floats(outputs, OUTPUT_SCORES);\n const classes = integers(outputs, OUTPUT_CLASSES);\n const probs = probsTensor.data as Float32Array;\n const reported = integers(outputs, OUTPUT_NUM_DETECTIONS)[0] ?? 0;\n const rows = Math.min(reported, Math.floor(boxes.length / 4));\n const classCount = probsTensor.dims[probsTensor.dims.length - 1] ?? 0;\n\n const allowed = options.classes === undefined ? null : new Set(options.classes);\n const floor = options.confThreshold ?? 0;\n const detections: DetectionResult[] = [];\n for (let row = 0; row < rows; row++) {\n const classId = classes[row] ?? 0;\n const confidence = scores[row] ?? 0;\n if (confidence < floor || (allowed !== null && !allowed.has(classId))) continue;\n\n const bbox = this._toOriginal(boxes, row, { scale, padLeft, padTop, original });\n const cropped = crop(original, bbox);\n detections.push(\n detection(\n classId,\n this._names[classId] ?? `class_${classId}`,\n confidence,\n bbox,\n cropped,\n this._classify(\n probs.subarray(row * classCount, (row + 1) * classCount),\n cropped,\n options.topK,\n ),\n ),\n );\n }\n\n requireDetections(detections.length, {\n raiseOnEmpty: options.raiseOnEmpty ?? this._raiseOnEmpty,\n confThreshold: Math.max(floor, this._spec.confThreshold),\n classes: options.classes,\n path,\n });\n\n const origShape: readonly [number, number] = [original.height, original.width];\n timer.stage(\"postprocess\");\n return [\n new DetectClassifyResults(\n bulkBoxes(detections, origShape),\n detections,\n this._names,\n this._classifierNames,\n original,\n origShape,\n path,\n timer.speed(),\n ),\n ];\n }\n\n /**\n * Letterbox the image and build the graph's feeds.\n *\n * The detector input runs through {@link LetterboxPipeline}, which fuses the\n * resize, the padding and the HWC-to-CHW float conversion into one\n * `drawImage` plus one readback, and reuses its output buffer between frames.\n * That buffer goes straight to ONNX Runtime, so `_pipeline.release()` must not\n * be called until the run resolves.\n *\n * A pipeline fused with `cropSource: \"original\"` also takes the untouched\n * image as a second input, plus the scale and padding of the letterbox — that\n * is what lets the graph undo the letterbox transform internally and crop at\n * native resolution instead of from the downscaled copy. That one is **not**\n * letterboxed by definition, so it does not go through the fused path.\n */\n private _preprocess(image: RGBImage): {\n feeds: Record<string, ort.Tensor>;\n scale: number;\n padLeft: number;\n padTop: number;\n } {\n const [width, height] = this._spec.inputSize;\n const boxed = this._pipeline.run(image);\n const feeds: Record<string, ort.Tensor> = {\n [INPUT_IMAGE]: toFloat32Tensor(boxed.data, [1, 3, height, width]),\n };\n\n if (this._spec.needsSourceImage) {\n feeds[INPUT_SOURCE] = tensorOf(image);\n feeds[INPUT_SCALE] = toFloat32Tensor(new Float32Array([boxed.scale]), [1]);\n feeds[INPUT_PAD] = toFloat32Tensor(\n new Float32Array([boxed.padLeft, boxed.padTop]),\n [2],\n );\n }\n return { feeds, scale: boxed.scale, padLeft: boxed.padLeft, padTop: boxed.padTop };\n }\n\n /**\n * Map one letterboxed xyxy row back onto the original image.\n *\n * The graph always reports boxes in the detector's letterboxed pixel space,\n * whichever crop source it was fused with, so both sources agree here.\n */\n private _toOriginal(\n boxes: Float32Array,\n row: number,\n context: {\n scale: number;\n padLeft: number;\n padTop: number;\n original: RGBImage;\n },\n ): BoundingBox {\n const { scale, padLeft, padTop, original } = context;\n const at = (offset: number): number => boxes[row * 4 + offset] ?? 0;\n const clampX = (value: number): number => Math.min(Math.max(value, 0), original.width);\n const clampY = (value: number): number => Math.min(Math.max(value, 0), original.height);\n return new BoundingBox(\n clampX((at(0) - padLeft) / scale),\n clampY((at(1) - padTop) / scale),\n clampX((at(2) - padLeft) / scale),\n clampY((at(3) - padTop) / scale),\n );\n }\n\n /**\n * Turn one row of the classifier output into a result object.\n *\n * @param row The output row for this detection.\n * @param image The crop the row describes, carried so callers can display\n * what was classified.\n * @param k Optional truncation of the probability list.\n */\n private _classify(\n row: Float32Array,\n image: RGBImage,\n k: number | undefined,\n ): ClassificationResult {\n const scores = this._spec.applySoftmax ? softmax(row) : row;\n const { indices, values } = topK(scores, k ?? null);\n const probabilities: ClassProbability[] = [];\n for (let i = 0; i < indices.length; i++) {\n const classId = indices[i] ?? 0;\n const probability = values[i] ?? 0;\n const className = this._classifierLabels[classId] ?? `class_${classId}`;\n probabilities.push({\n classId,\n className,\n probability,\n cls: classId,\n name: className,\n conf: probability,\n });\n }\n const top = probabilities[0] ?? {\n classId: 0,\n className: \"class_0\",\n probability: 0,\n cls: 0,\n name: \"class_0\",\n conf: 0,\n };\n return {\n classId: top.classId,\n className: top.className,\n confidence: top.probability,\n cls: top.classId,\n name: top.className,\n conf: top.probability,\n image,\n probabilities,\n };\n }\n}\n\n/**\n * Read the classifier stage's class count off the `probs` output shape.\n *\n * @param session The loaded pipeline session.\n * @returns The class count, or `null` when the graph leaves that axis dynamic\n * or declares no `probs` output — in which case label resolution falls back\n * to whatever the fusion recorded.\n */\nfunction classifierClasses(session: OrtSession): number | null {\n const index = session.outputNames.indexOf(OUTPUT_PROBS);\n if (index < 0) return null;\n const shape = session.outputShapes[index];\n if (shape === undefined || shape.length === 0) return null;\n return shape[shape.length - 1] ?? null;\n}\n\n/**\n * Build a class id → name record from an ordered label list.\n *\n * @param labels Labels indexed by class id.\n * @returns The equivalent record.\n */\nfunction indexNames(labels: readonly string[]): Readonly<Record<number, string>> {\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) names[i] = labels[i] as string;\n return names;\n}\n\n/**\n * Fetch an output tensor by name.\n *\n * @param outputs The run's results.\n * @param name The output's name in the pipeline contract.\n * @returns The tensor.\n * @throws {@link FusionError} when the graph does not carry that output, which\n * means the file is not a pipeline this version can drive.\n */\nfunction output(outputs: Record<string, ort.Tensor>, name: string): ort.Tensor {\n const tensor = outputs[name];\n if (tensor === undefined) {\n throw new FusionError(`The fused pipeline is missing its '${name}' output.`);\n }\n return tensor;\n}\n\n/**\n * Fetch a float output by name.\n *\n * @param outputs The run's results.\n * @param name The output's name in the pipeline contract.\n * @returns The output's data.\n * @throws {@link FusionError} when the graph does not carry that output.\n */\nfunction floats(outputs: Record<string, ort.Tensor>, name: string): Float32Array {\n return output(outputs, name).data as Float32Array;\n}\n\n/**\n * Fetch an integer output by name, normalizing ORT's 64-bit representation.\n *\n * ONNX Runtime Web returns `int64` tensors as `BigInt64Array`, whose values do\n * not compare or index like numbers. Class ids and detection counts are always\n * small, so widening them to `number` here is lossless and keeps every caller\n * free of `BigInt` handling.\n *\n * @param outputs The run's results.\n * @param name The output's name in the pipeline contract.\n * @returns The output's values as plain numbers.\n * @throws {@link FusionError} when the graph does not carry that output.\n */\nfunction integers(outputs: Record<string, ort.Tensor>, name: string): number[] {\n const data = output(outputs, name).data as BigInt64Array | Int32Array | Float32Array;\n const values: number[] = [];\n for (let i = 0; i < data.length; i++) values.push(Number(data[i]));\n return values;\n}\n\n/**\n * Convert an image to the `[1, 3, H, W]` float32 tensor the graph expects.\n *\n * @param image The image to convert.\n * @returns The batched CHW tensor, scaled to `[0, 1]`.\n */\nfunction tensorOf(image: RGBImage): ort.Tensor {\n const chw = toCHW(toFloat32(image), image.width, image.height, 3);\n return toFloat32Tensor(chw, [1, 3, image.height, image.width]);\n}\n\n/**\n * Cut the box region out of the original image.\n *\n * @param image The source image.\n * @param bbox The box, in original-image pixel coordinates.\n * @returns The cropped region, or a zero-sized image for a box with no area.\n */\nfunction crop(image: RGBImage, bbox: BoundingBox): RGBImage {\n const [rawX1, rawY1, rawX2, rawY2] = bbox.asIntXyxy();\n const x1 = Math.max(0, rawX1);\n const y1 = Math.max(0, rawY1);\n const x2 = Math.min(image.width, rawX2);\n const y2 = Math.min(image.height, rawY2);\n if (x2 <= x1 || y2 <= y1) return new RGBImage(new Uint8Array(0), 0, 0);\n\n const width = x2 - x1;\n const height = y2 - y1;\n const out = new Uint8Array(width * height * 3);\n for (let row = 0; row < height; row++) {\n const offset = ((y1 + row) * image.width + x1) * 3;\n out.set(image.data.subarray(offset, offset + width * 3), row * width * 3);\n }\n return new RGBImage(out, width, height);\n}\n\n/**\n * Assemble one detection, filling the Ultralytics-style aliases.\n *\n * @param classId Detector class index.\n * @param className Detector class name.\n * @param confidence Detection score.\n * @param bbox Box in original-image coordinates.\n * @param croppedImage The region the box covers.\n * @param classification What the classification stage said about that region.\n * @returns The detection object.\n */\nfunction detection(\n classId: number,\n className: string,\n confidence: number,\n bbox: BoundingBox,\n croppedImage: RGBImage,\n classification: ClassificationResult,\n): DetectionResult {\n return {\n classId,\n className,\n confidence,\n bbox,\n cls: classId,\n name: className,\n conf: confidence,\n box: bbox,\n croppedImage,\n classification,\n };\n}\n\n/**\n * Assemble the bulk-array `Boxes` view from per-instance detections.\n *\n * @param detections The surviving detections.\n * @param origShape `[height, width]` of the original image.\n * @returns The bulk view, empty when nothing survived.\n */\nfunction bulkBoxes(\n detections: readonly DetectionResult[],\n origShape: readonly [number, number],\n): Boxes {\n const xyxy = new Float32Array(detections.length * 4);\n const cls = new Int32Array(detections.length);\n const conf = new Float32Array(detections.length);\n detections.forEach((entry, index) => {\n const [x1, y1, x2, y2] = entry.bbox.xyxy;\n xyxy[index * 4] = x1;\n xyxy[index * 4 + 1] = y1;\n xyxy[index * 4 + 2] = x2;\n xyxy[index * 4 + 3] = y2;\n cls[index] = entry.classId;\n conf[index] = entry.confidence;\n });\n return new Boxes(xyxy, cls, conf, origShape);\n}\n"],"mappings":"wYAqGA,IAAa,EAAb,MAAa,UAAuB,EAAA,UAAW,CAGtB,MACA,QACA,OACA,kBACA,iBACA,cAPrB,YACI,EACA,EACA,EACA,EACA,EACA,EACA,EACF,CACE,MAAM,CAAO,EAPI,KAAA,MAAA,EACA,KAAA,QAAA,EACA,KAAA,OAAA,EACA,KAAA,kBAAA,EACA,KAAA,iBAAA,EACA,KAAA,cAAA,CAGrB,CAEA,eAAmD,KAanD,MAAM,OAAO,EAAe,EAAkB,CAC1C,GAAM,CAAC,EAAO,GAAU,KAAK,MAAM,UACnC,IAAK,IAAI,EAAI,EAAG,EAAI,EAAM,IAAK,CAC3B,IAAM,EAAoC,EACrC,EAAA,aAAc,EAAA,gBAAgB,EAAA,eAAe,EAAO,CAAM,EAAG,CAC1D,EACA,EACA,EACA,CACJ,CAAC,CACL,EACI,KAAK,MAAM,mBACX,EAAM,EAAA,cAAgB,EAAA,gBAAgB,EAAA,eAAe,EAAO,CAAM,EAAG,CACjE,EACA,EACA,EACA,CACJ,CAAC,EACD,EAAM,EAAA,aAAe,EAAA,gBAAgB,IAAI,aAAa,CAAC,CAAC,CAAC,EAAG,CAAC,CAAC,CAAC,EAC/D,EAAM,EAAA,WAAa,EAAA,gBAAgB,IAAI,aAAa,CAAC,EAAG,CAAC,CAAC,EAAG,CAAC,CAAC,CAAC,GAEpE,MAAM,KAAK,SAAS,IAAI,CAAK,CACjC,CACJ,CASA,IAAY,WAA+B,CACvC,GAAI,KAAK,iBAAmB,KAAM,CAC9B,GAAM,CAAC,EAAO,GAAU,KAAK,MAAM,UACnC,KAAK,eAAiB,IAAI,EAAA,kBAAkB,EAAO,CAAM,CAC7D,CACA,OAAO,KAAK,cAChB,CAWA,aAAa,OACT,EACA,EAAiC,CAAC,EACX,CACvB,IAAM,EAAU,MAAM,EAAA,WAAW,OAAO,EAAO,CAAO,EAChD,EAAO,EAAA,eAAe,EAAQ,QAAQ,EAC5C,GAAI,IAAS,KACT,MAAM,IAAI,EAAA,YACN,mNAGJ,EAGJ,IAAM,EAAS,EAAA,cAAc,EAAQ,QAAU,EAAK,eAAiB,MAAM,EACrE,EAAmB,EAAA,cAAc,EAAQ,kBAAoB,EAAK,gBAAiB,CACrF,WAAY,EAAkB,CAAO,GAAK,IAAA,EAC9C,CAAC,EACD,OAAO,IAAI,EACP,EACA,EACA,EACA,EAAW,CAAM,EACjB,EACA,EAAW,CAAgB,EAC3B,EAAQ,cAAgB,EAC5B,CACJ,CAGA,IAAI,MAAmB,CACnB,OAAO,KAAK,KAChB,CAGA,IAAI,WAAuC,CACvC,OAAO,KAAK,MAAM,SACtB,CAGA,IAAI,QAA4B,CAC5B,OAAO,KAAK,OAChB,CAGA,IAAI,OAA0C,CAC1C,OAAO,KAAK,MAChB,CAGA,IAAI,kBAAsC,CACtC,OAAO,KAAK,iBAChB,CAGA,IAAI,iBAAoD,CACpD,OAAO,KAAK,gBAChB,CAQA,MAAM,KACF,EACA,EAAwC,CAAC,EACT,CAChC,OAAO,KAAK,QAAQ,EAAO,CAAO,CACtC,CAUA,MAAM,QACF,EACA,EAAwC,CAAC,EACT,CAChC,IAAM,EAAQ,IAAI,EAAA,WACZ,EAAO,OAAO,GAAU,SAAW,EAAQ,KAC3C,EAAW,MAAM,EAAA,UAAU,CAAK,EACtC,EAAM,MAAM,MAAM,EAClB,GAAM,CAAE,QAAO,QAAO,UAAS,UAAW,KAAK,YAAY,CAAQ,EACnE,EAAM,MAAM,YAAY,EACxB,IAAM,EAAU,MAAM,KAAK,SAAS,IAAI,CAAK,EAC7C,KAAK,UAAU,QAAQ,EACvB,EAAM,MAAM,WAAW,EAEvB,IAAM,EAAc,EAAO,EAAS,EAAA,YAAY,EAC1C,EAAQ,EAAO,EAAS,EAAA,YAAY,EACpC,EAAS,EAAO,EAAS,EAAA,aAAa,EACtC,EAAU,EAAS,EAAS,EAAA,cAAc,EAC1C,EAAQ,EAAY,KACpB,EAAW,EAAS,EAAA,gBAA8B,CAAC,CAAC,IAAM,EAC1D,EAAO,KAAK,IAAI,EAAU,KAAK,MAAM,EAAM,OAAS,CAAC,CAAC,EACtD,EAAa,EAAY,KAAK,EAAY,KAAK,OAAS,IAAM,EAE9D,EAAU,EAAQ,UAAY,IAAA,GAAY,KAAO,IAAI,IAAI,EAAQ,OAAO,EACxE,EAAQ,EAAQ,eAAiB,EACjC,EAAgC,CAAC,EACvC,IAAK,IAAI,EAAM,EAAG,EAAM,EAAM,IAAO,CACjC,IAAM,EAAU,EAAQ,IAAQ,EAC1B,EAAa,EAAO,IAAQ,EAClC,GAAI,EAAa,GAAU,IAAY,MAAQ,CAAC,EAAQ,IAAI,CAAO,EAAI,SAEvE,IAAM,EAAO,KAAK,YAAY,EAAO,EAAK,CAAE,QAAO,UAAS,SAAQ,UAAS,CAAC,EACxE,EAAU,EAAK,EAAU,CAAI,EACnC,EAAW,KACP,EACI,EACA,KAAK,OAAO,IAAY,SAAS,IACjC,EACA,EACA,EACA,KAAK,UACD,EAAM,SAAS,EAAM,GAAa,EAAM,GAAK,CAAU,EACvD,EACA,EAAQ,IACZ,CACJ,CACJ,CACJ,CAEA,EAAA,kBAAkB,EAAW,OAAQ,CACjC,aAAc,EAAQ,cAAgB,KAAK,cAC3C,cAAe,KAAK,IAAI,EAAO,KAAK,MAAM,aAAa,EACvD,QAAS,EAAQ,QACjB,MACJ,CAAC,EAED,IAAM,EAAuC,CAAC,EAAS,OAAQ,EAAS,KAAK,EAE7E,OADA,EAAM,MAAM,aAAa,EAClB,CACH,IAAI,EAAA,sBACA,EAAU,EAAY,CAAS,EAC/B,EACA,KAAK,OACL,KAAK,iBACL,EACA,EACA,EACA,EAAM,MAAM,CAChB,CACJ,CACJ,CAiBA,YAAoB,EAKlB,CACE,GAAM,CAAC,EAAO,GAAU,KAAK,MAAM,UAC7B,EAAQ,KAAK,UAAU,IAAI,CAAK,EAChC,EAAoC,EACrC,EAAA,aAAc,EAAA,gBAAgB,EAAM,KAAM,CAAC,EAAG,EAAG,EAAQ,CAAK,CAAC,CACpE,EAUA,OARI,KAAK,MAAM,mBACX,EAAM,EAAA,cAAgB,EAAS,CAAK,EACpC,EAAM,EAAA,aAAe,EAAA,gBAAgB,IAAI,aAAa,CAAC,EAAM,KAAK,CAAC,EAAG,CAAC,CAAC,CAAC,EACzE,EAAM,EAAA,WAAa,EAAA,gBACf,IAAI,aAAa,CAAC,EAAM,QAAS,EAAM,MAAM,CAAC,EAC9C,CAAC,CAAC,CACN,GAEG,CAAE,QAAO,MAAO,EAAM,MAAO,QAAS,EAAM,QAAS,OAAQ,EAAM,MAAO,CACrF,CAQA,YACI,EACA,EACA,EAMW,CACX,GAAM,CAAE,QAAO,UAAS,SAAQ,YAAa,EACvC,EAAM,GAA2B,EAAM,EAAM,EAAI,IAAW,EAC5D,EAAU,GAA0B,KAAK,IAAI,KAAK,IAAI,EAAO,CAAC,EAAG,EAAS,KAAK,EAC/E,EAAU,GAA0B,KAAK,IAAI,KAAK,IAAI,EAAO,CAAC,EAAG,EAAS,MAAM,EACtF,OAAO,IAAI,EAAA,YACP,GAAQ,EAAG,CAAC,EAAI,GAAW,CAAK,EAChC,GAAQ,EAAG,CAAC,EAAI,GAAU,CAAK,EAC/B,GAAQ,EAAG,CAAC,EAAI,GAAW,CAAK,EAChC,GAAQ,EAAG,CAAC,EAAI,GAAU,CAAK,CACnC,CACJ,CAUA,UACI,EACA,EACA,EACoB,CACpB,IAAM,EAAS,KAAK,MAAM,aAAe,EAAA,QAAQ,CAAG,EAAI,EAClD,CAAE,UAAS,UAAW,EAAA,KAAK,EAAQ,GAAK,IAAI,EAC5C,EAAoC,CAAC,EAC3C,IAAK,IAAI,EAAI,EAAG,EAAI,EAAQ,OAAQ,IAAK,CACrC,IAAM,EAAU,EAAQ,IAAM,EACxB,EAAc,EAAO,IAAM,EAC3B,EAAY,KAAK,kBAAkB,IAAY,SAAS,IAC9D,EAAc,KAAK,CACf,UACA,YACA,cACA,IAAK,EACL,KAAM,EACN,KAAM,CACV,CAAC,CACL,CACA,IAAM,EAAM,EAAc,IAAM,CAC5B,QAAS,EACT,UAAW,UACX,YAAa,EACb,IAAK,EACL,KAAM,UACN,KAAM,CACV,EACA,MAAO,CACH,QAAS,EAAI,QACb,UAAW,EAAI,UACf,WAAY,EAAI,YAChB,IAAK,EAAI,QACT,KAAM,EAAI,UACV,KAAM,EAAI,YACV,QACA,eACJ,CACJ,CACJ,EAUA,SAAS,EAAkB,EAAoC,CAC3D,IAAM,EAAQ,EAAQ,YAAY,QAAQ,EAAA,YAAY,EACtD,GAAI,EAAQ,EAAG,OAAO,KACtB,IAAM,EAAQ,EAAQ,aAAa,GAEnC,OADI,IAAU,IAAA,IAAa,EAAM,SAAW,EAAU,KAC/C,EAAM,EAAM,OAAS,IAAM,IACtC,CAQA,SAAS,EAAW,EAA6D,CAC7E,IAAM,EAAgC,CAAC,EACvC,IAAK,IAAI,EAAI,EAAG,EAAI,EAAO,OAAQ,IAAK,EAAM,GAAK,EAAO,GAC1D,OAAO,CACX,CAWA,SAAS,EAAO,EAAqC,EAA0B,CAC3E,IAAM,EAAS,EAAQ,GACvB,GAAI,IAAW,IAAA,GACX,MAAM,IAAI,EAAA,YAAY,sCAAsC,EAAK,UAAU,EAE/E,OAAO,CACX,CAUA,SAAS,EAAO,EAAqC,EAA4B,CAC7E,OAAO,EAAO,EAAS,CAAI,CAAC,CAAC,IACjC,CAeA,SAAS,EAAS,EAAqC,EAAwB,CAC3E,IAAM,EAAO,EAAO,EAAS,CAAI,CAAC,CAAC,KAC7B,EAAmB,CAAC,EAC1B,IAAK,IAAI,EAAI,EAAG,EAAI,EAAK,OAAQ,IAAK,EAAO,KAAK,OAAO,EAAK,EAAE,CAAC,EACjE,OAAO,CACX,CAQA,SAAS,EAAS,EAA6B,CAC3C,IAAM,EAAM,EAAA,MAAM,EAAA,UAAU,CAAK,EAAG,EAAM,MAAO,EAAM,OAAQ,CAAC,EAChE,OAAO,EAAA,gBAAgB,EAAK,CAAC,EAAG,EAAG,EAAM,OAAQ,EAAM,KAAK,CAAC,CACjE,CASA,SAAS,EAAK,EAAiB,EAA6B,CACxD,GAAM,CAAC,EAAO,EAAO,EAAO,GAAS,EAAK,UAAU,EAC9C,EAAK,KAAK,IAAI,EAAG,CAAK,EACtB,EAAK,KAAK,IAAI,EAAG,CAAK,EACtB,EAAK,KAAK,IAAI,EAAM,MAAO,CAAK,EAChC,EAAK,KAAK,IAAI,EAAM,OAAQ,CAAK,EACvC,GAAI,GAAM,GAAM,GAAM,EAAI,OAAO,IAAI,EAAA,SAAS,IAAI,WAAe,EAAG,CAAC,EAErE,IAAM,EAAQ,EAAK,EACb,EAAS,EAAK,EACd,EAAM,IAAI,WAAW,EAAQ,EAAS,CAAC,EAC7C,IAAK,IAAI,EAAM,EAAG,EAAM,EAAQ,IAAO,CACnC,IAAM,IAAW,EAAK,GAAO,EAAM,MAAQ,GAAM,EACjD,EAAI,IAAI,EAAM,KAAK,SAAS,EAAQ,EAAS,EAAQ,CAAC,EAAG,EAAM,EAAQ,CAAC,CAC5E,CACA,OAAO,IAAI,EAAA,SAAS,EAAK,EAAO,CAAM,CAC1C,CAaA,SAAS,EACL,EACA,EACA,EACA,EACA,EACA,EACe,CACf,MAAO,CACH,UACA,YACA,aACA,OACA,IAAK,EACL,KAAM,EACN,KAAM,EACN,IAAK,EACL,eACA,gBACJ,CACJ,CASA,SAAS,EACL,EACA,EACK,CACL,IAAM,EAAO,IAAI,aAAa,EAAW,OAAS,CAAC,EAC7C,EAAM,IAAI,WAAW,EAAW,MAAM,EACtC,EAAO,IAAI,aAAa,EAAW,MAAM,EAU/C,OATA,EAAW,SAAS,EAAO,IAAU,CACjC,GAAM,CAAC,EAAI,EAAI,EAAI,GAAM,EAAM,KAAK,KACpC,EAAK,EAAQ,GAAK,EAClB,EAAK,EAAQ,EAAI,GAAK,EACtB,EAAK,EAAQ,EAAI,GAAK,EACtB,EAAK,EAAQ,EAAI,GAAK,EACtB,EAAI,GAAS,EAAM,QACnB,EAAK,GAAS,EAAM,UACxB,CAAC,EACM,IAAI,EAAA,MAAM,EAAM,EAAK,EAAM,CAAS,CAC/C"}
1
+ {"version":3,"file":"detectClassify.cjs","names":[],"sources":["../../../src/vision/tasks/detectClassify.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Run a fused detect→classify pipeline in the browser.\n *\n * The file this loads was built by the Python SDK's `ort_vision_sdk.compose`,\n * and it already contains both models plus the crop-and-resize bridge between\n * them. That matters far more in a tab than on a server: two models mean two\n * `.onnx` downloads, two WASM/WebGPU session initializations, and a per-crop\n * round trip through JavaScript to slice, resize and restack the regions before\n * the second model can see them. A fused pipeline has one download, one session\n * and no round trip — the crops are produced and consumed inside the graph.\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { FusionError } from \"../core/exceptions\";\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { SpeedTimer } from \"../core/timing\";\nimport {\n INPUT_IMAGE,\n INPUT_PAD,\n INPUT_SCALE,\n INPUT_SOURCE,\n OUTPUT_BOXES,\n OUTPUT_CLASSES,\n OUTPUT_NUM_DETECTIONS,\n OUTPUT_MASKS,\n OUTPUT_PROBS,\n OUTPUT_SCORES,\n type FusionSpec,\n readFusionSpec,\n} from \"../fusion\";\nimport { type ImageInput, loadImageSource } from \"../io/image\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport { softmax, topK } from \"../postprocess/classification\";\nimport { asFloat32Array } from \"../core/dtypes\";\nimport { toCHW, toFloat32, toFloat32Tensor } from \"../preprocess/image\";\nimport { LetterboxPipeline, zeroTensorData } from \"../preprocess/pipeline\";\nimport { Boxes, DetectClassifyResults } from \"../results\";\nimport type { RGBImage } from \"../types\";\nimport {\n BoundingBox,\n Mask,\n type ClassProbability,\n type ClassificationResult,\n type DetectionResult,\n} from \"../types\";\nimport { VisionTask, cropToBox, requireDetections } from \"./base\";\n\nexport interface DetectClassifyOptions extends OrtSessionOptions {\n /**\n * Class label spec for the **detection** stage — see {@link resolveLabels}.\n * Defaults to the names recorded at fusion time, falling back to the COCO\n * 80-class preset when the fusion recorded none.\n */\n readonly labels?: LabelSpec;\n /**\n * Class label spec for the **classification** stage. Defaults to the recorded\n * names, falling back to generated `class_<id>` names.\n */\n readonly classifierLabels?: LabelSpec;\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 DetectClassifyPredictOptions {\n /**\n * Drop detections scoring below this. The graph's own NMS threshold was fixed\n * at fusion time and cannot be lowered here — this only filters further.\n */\n readonly confThreshold?: number;\n /** If set, keep only detections whose detector `classId` is in this list. */\n readonly classes?: readonly number[];\n /** Truncate each detection's `classification.probabilities` to its top-k entries. */\n readonly topK?: number;\n /** Override the constructor's `raiseOnEmpty` setting for this call. */\n readonly raiseOnEmpty?: boolean;\n}\n\n/**\n * Detector and classifier running as a single ONNX model.\n *\n * Everything the pipeline needs to know about itself — the resolution to\n * letterbox to, whether it wants the full-resolution image as well, whether its\n * classifier output still needs a softmax, the class names of both stages — was\n * written into the file at fusion time and is read back here. Nothing is\n * restated on the JavaScript side, so nothing can drift out of step with the\n * Python side that built it.\n *\n * @example\n * ```typescript\n * const pipeline = await DetectClassify.create(\"/models/pipeline.onnx\");\n * const result = (await pipeline.predict(\"/images/flock.jpg\"))[0];\n * for (const detection of result) {\n * console.log(detection.name, detection.conf, detection.classification?.name);\n * }\n * ```\n */\nexport class DetectClassify extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _spec: FusionSpec,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _classifierLabels: readonly string[],\n private readonly _classifierNames: Readonly<Record<number, string>>,\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 zero-filled inputs, paying one-time costs up front.\n *\n * Worth more here than on a single-stage task: a fused pipeline is two models\n * plus the bridge in one graph, so the first inference compiles shaders for\n * all of it. Calling this while a loading spinner is still up moves that cost\n * 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 [width, height] = this._spec.inputSize;\n for (let i = 0; i < runs; i++) {\n const feeds: Record<string, ort.Tensor> = {\n [INPUT_IMAGE]: toFloat32Tensor(zeroTensorData(width, height), [\n 1,\n 3,\n height,\n width,\n ]),\n };\n if (this._spec.needsSourceImage) {\n feeds[INPUT_SOURCE] = toFloat32Tensor(zeroTensorData(width, height), [\n 1,\n 3,\n height,\n width,\n ]);\n feeds[INPUT_SCALE] = toFloat32Tensor(new Float32Array([1]), [1]);\n feeds[INPUT_PAD] = toFloat32Tensor(new Float32Array([0, 0]), [2]);\n }\n await this._session.run(feeds);\n }\n }\n\n /**\n * The fused preprocessing pipeline, built on first use.\n *\n * Lazily, because constructing it allocates canvases: a pipeline 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 const [width, height] = this._spec.inputSize;\n this._pipelineCache = new LetterboxPipeline(width, height);\n }\n return this._pipelineCache;\n }\n\n /**\n * Load a fused pipeline and resolve both label spaces.\n *\n * @param model The fused `.onnx` — a URL, an `ArrayBuffer`, or bytes.\n * @param options Label overrides plus the usual session options.\n * @throws {@link FusionError} when the model carries no pipeline metadata,\n * i.e. it is a plain detector or classifier rather than something\n * `ort_vision_sdk.compose` produced.\n */\n static async create(\n model: ModelSource,\n options: DetectClassifyOptions = {},\n ): Promise<DetectClassify> {\n const session = await OrtSession.create(model, options);\n const spec = readFusionSpec(session.metadata);\n if (spec === null) {\n throw new FusionError(\n \"This model carries no fused-pipeline metadata, so DetectClassify cannot tell how to \" +\n \"drive it. Build one with ort_vision_sdk.compose.fuse_detect_classify, or load a \" +\n \"plain model with Detector/Classifier instead.\",\n );\n }\n\n const labels = resolveLabels(options.labels ?? spec.detectorNames ?? \"coco\");\n const classifierLabels = resolveLabels(options.classifierLabels ?? spec.classifierNames, {\n numClasses: classifierClasses(session) ?? undefined,\n });\n return new DetectClassify(\n session,\n spec,\n labels,\n indexNames(labels),\n classifierLabels,\n indexNames(classifierLabels),\n options.raiseOnEmpty ?? false,\n );\n }\n\n /** The pipeline configuration recorded in the model at fusion time. */\n get spec(): FusionSpec {\n return this._spec;\n }\n\n /** The `[width, height]` the detection stage runs at. */\n get inputSize(): readonly [number, number] {\n return this._spec.inputSize;\n }\n\n /** Detector class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Detector class id → class name (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /** Classifier class labels indexed by class id. */\n get classifierLabels(): readonly string[] {\n return this._classifierLabels;\n }\n\n /** Classifier class id → class name. */\n get classifierNames(): Readonly<Record<number, string>> {\n return this._classifierNames;\n }\n\n /**\n * Alias for {@link predict} — call the pipeline like a torch `nn.Module`.\n *\n * Use as `pipeline.call(img)` since JavaScript class instances are not\n * callable; for direct invocation, prefer `pipeline.predict(img)`.\n */\n async call(\n image: ImageInput,\n options: DetectClassifyPredictOptions = {},\n ): Promise<DetectClassifyResults[]> {\n return this.predict(image, options);\n }\n\n /**\n * Run the pipeline on a single image.\n *\n * The returned envelope carries a {@link Speed} breakdown in `speed`. Its\n * `inference` figure covers detection *and* classification, since the\n * pipeline runs them as one graph and no boundary between them is observable\n * from outside.\n */\n async predict(\n image: ImageInput,\n options: DetectClassifyPredictOptions = {},\n ): Promise<DetectClassifyResults[]> {\n const timer = new SpeedTimer();\n const path = typeof image === \"string\" ? image : null;\n const { image: original, canvas } = await loadImageSource(image);\n timer.stage(\"load\");\n const { feeds, scale, padLeft, padTop, reused } = this._preprocess(original, canvas);\n timer.stage(\"preprocess\");\n let outputs: Record<string, ort.Tensor>;\n try {\n outputs = await this._session.run(feeds);\n } finally {\n this._pipeline.release({ reused });\n }\n timer.stage(\"inference\");\n\n const probsTensor = output(outputs, OUTPUT_PROBS);\n const boxes = floats(outputs, OUTPUT_BOXES);\n const scores = floats(outputs, OUTPUT_SCORES);\n const classes = integers(outputs, OUTPUT_CLASSES);\n const probs = asFloat32Array(probsTensor.data);\n const reported = integers(outputs, OUTPUT_NUM_DETECTIONS)[0] ?? 0;\n const masks = this._spec.hasMasks ? floats(outputs, OUTPUT_MASKS) : null;\n const [cropWidth, cropHeight] = this._spec.cropSize;\n const rows = Math.min(reported, Math.floor(boxes.length / 4));\n const classCount = probsTensor.dims[probsTensor.dims.length - 1] ?? 0;\n\n const allowed = options.classes === undefined ? null : new Set(options.classes);\n const floor = options.confThreshold ?? 0;\n const detections: DetectionResult[] = [];\n for (let row = 0; row < rows; row++) {\n const classId = classes[row] ?? 0;\n const confidence = scores[row] ?? 0;\n if (confidence < floor || (allowed !== null && !allowed.has(classId))) continue;\n\n const bbox = this._toOriginal(boxes, row, { scale, padLeft, padTop, original });\n const cropped = cropToBox(original, bbox);\n detections.push(\n detection(\n classId,\n this._names[classId] ?? `class_${classId}`,\n confidence,\n bbox,\n cropped,\n this._classify(\n probs.subarray(row * classCount, (row + 1) * classCount),\n cropped,\n options.topK,\n ),\n masks === null ? null : maskToBox(masks, row, cropWidth, cropHeight, cropped),\n ),\n );\n }\n\n requireDetections(detections.length, {\n raiseOnEmpty: options.raiseOnEmpty ?? this._raiseOnEmpty,\n confThreshold: Math.max(floor, this._spec.confThreshold),\n classes: options.classes,\n path,\n });\n\n const origShape: readonly [number, number] = [original.height, original.width];\n timer.stage(\"postprocess\");\n return [\n new DetectClassifyResults(\n bulkBoxes(detections, origShape),\n detections,\n this._names,\n this._classifierNames,\n original,\n origShape,\n path,\n timer.speed(),\n ),\n ];\n }\n\n /**\n * Letterbox the image and build the graph's feeds.\n *\n * The detector input runs through {@link LetterboxPipeline}, which fuses the\n * resize, the padding and the HWC-to-CHW float conversion into one\n * `drawImage` plus one readback, and reuses its output buffer between frames.\n * That buffer goes straight to ONNX Runtime, so `_pipeline.release()` must not\n * be called until the run resolves.\n *\n * A pipeline fused with `cropSource: \"original\"` also takes the untouched\n * image as a second input, plus the scale and padding of the letterbox — that\n * is what lets the graph undo the letterbox transform internally and crop at\n * native resolution instead of from the downscaled copy. That one is **not**\n * letterboxed by definition, so it does not go through the fused path.\n *\n * @param image The decoded input.\n * @param canvas The opaque canvas it was decoded on, when there is one — see\n * {@link LetterboxPipeline.run}.\n */\n private _preprocess(\n image: RGBImage,\n canvas: CanvasImageSource | null,\n ): {\n feeds: Record<string, ort.Tensor>;\n scale: number;\n padLeft: number;\n padTop: number;\n reused: boolean;\n } {\n const [width, height] = this._spec.inputSize;\n const boxed = this._pipeline.run(image, canvas);\n const feeds: Record<string, ort.Tensor> = {\n [INPUT_IMAGE]: toFloat32Tensor(boxed.data, [1, 3, height, width]),\n };\n\n if (this._spec.needsSourceImage) {\n feeds[INPUT_SOURCE] = tensorOf(image);\n feeds[INPUT_SCALE] = toFloat32Tensor(new Float32Array([boxed.scale]), [1]);\n feeds[INPUT_PAD] = toFloat32Tensor(\n new Float32Array([boxed.padLeft, boxed.padTop]),\n [2],\n );\n }\n return {\n feeds,\n scale: boxed.scale,\n padLeft: boxed.padLeft,\n padTop: boxed.padTop,\n reused: boxed.reused,\n };\n }\n\n /**\n * Map one letterboxed xyxy row back onto the original image.\n *\n * The graph always reports boxes in the detector's letterboxed pixel space,\n * whichever crop source it was fused with, so both sources agree here.\n */\n private _toOriginal(\n boxes: Float32Array,\n row: number,\n context: {\n scale: number;\n padLeft: number;\n padTop: number;\n original: RGBImage;\n },\n ): BoundingBox {\n const { scale, padLeft, padTop, original } = context;\n const at = (offset: number): number => boxes[row * 4 + offset] ?? 0;\n const clampX = (value: number): number => Math.min(Math.max(value, 0), original.width);\n const clampY = (value: number): number => Math.min(Math.max(value, 0), original.height);\n return new BoundingBox(\n clampX((at(0) - padLeft) / scale),\n clampY((at(1) - padTop) / scale),\n clampX((at(2) - padLeft) / scale),\n clampY((at(3) - padTop) / scale),\n );\n }\n\n /**\n * Turn one row of the classifier output into a result object.\n *\n * @param row The output row for this detection.\n * @param image The crop the row describes, carried so callers can display\n * what was classified.\n * @param k Optional truncation of the probability list.\n */\n private _classify(\n row: Float32Array,\n image: RGBImage,\n k: number | undefined,\n ): ClassificationResult {\n const scores = this._spec.applySoftmax ? softmax(row) : row;\n const { indices, values } = topK(scores, k ?? null);\n const probabilities: ClassProbability[] = [];\n for (let i = 0; i < indices.length; i++) {\n const classId = indices[i] ?? 0;\n const probability = values[i] ?? 0;\n const className = this._classifierLabels[classId] ?? `class_${classId}`;\n probabilities.push({\n classId,\n className,\n probability,\n cls: classId,\n name: className,\n conf: probability,\n });\n }\n const top = probabilities[0] ?? {\n classId: 0,\n className: \"class_0\",\n probability: 0,\n cls: 0,\n name: \"class_0\",\n conf: 0,\n };\n return {\n classId: top.classId,\n className: top.className,\n confidence: top.probability,\n cls: top.classId,\n name: top.className,\n conf: top.probability,\n image,\n probabilities,\n };\n }\n}\n\n/**\n * Read the classifier stage's class count off the `probs` output shape.\n *\n * @param session The loaded pipeline session.\n * @returns The class count, or `null` when the graph leaves that axis dynamic\n * or declares no `probs` output — in which case label resolution falls back\n * to whatever the fusion recorded.\n */\nfunction classifierClasses(session: OrtSession): number | null {\n const index = session.outputNames.indexOf(OUTPUT_PROBS);\n if (index < 0) return null;\n const shape = session.outputShapes[index];\n if (shape === undefined || shape.length === 0) return null;\n return shape[shape.length - 1] ?? null;\n}\n\n/**\n * Build a class id → name record from an ordered label list.\n *\n * @param labels Labels indexed by class id.\n * @returns The equivalent record.\n */\nfunction indexNames(labels: readonly string[]): Readonly<Record<number, string>> {\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) names[i] = labels[i] as string;\n return names;\n}\n\n/**\n * Fetch an output tensor by name.\n *\n * @param outputs The run's results.\n * @param name The output's name in the pipeline contract.\n * @returns The tensor.\n * @throws {@link FusionError} when the graph does not carry that output, which\n * means the file is not a pipeline this version can drive.\n */\nfunction output(outputs: Record<string, ort.Tensor>, name: string): ort.Tensor {\n const tensor = outputs[name];\n if (tensor === undefined) {\n throw new FusionError(`The fused pipeline is missing its '${name}' output.`);\n }\n return tensor;\n}\n\n/**\n * Fetch a float output by name.\n *\n * @param outputs The run's results.\n * @param name The output's name in the pipeline contract.\n * @returns The output's data.\n * @throws {@link FusionError} when the graph does not carry that output.\n */\nfunction floats(outputs: Record<string, ort.Tensor>, name: string): Float32Array {\n return asFloat32Array(output(outputs, name).data);\n}\n\n/**\n * Fetch an integer output by name, normalizing ORT's 64-bit representation.\n *\n * ONNX Runtime Web returns `int64` tensors as `BigInt64Array`, whose values do\n * not compare or index like numbers. Class ids and detection counts are always\n * small, so widening them to `number` here is lossless and keeps every caller\n * free of `BigInt` handling.\n *\n * @param outputs The run's results.\n * @param name The output's name in the pipeline contract.\n * @returns The output's values as plain numbers.\n * @throws {@link FusionError} when the graph does not carry that output.\n */\nfunction integers(outputs: Record<string, ort.Tensor>, name: string): number[] {\n const data = output(outputs, name).data as BigInt64Array | Int32Array | Float32Array;\n const values: number[] = [];\n for (let i = 0; i < data.length; i++) values.push(Number(data[i]));\n return values;\n}\n\n/**\n * Convert an image to the `[1, 3, H, W]` float32 tensor the graph expects.\n *\n * @param image The image to convert.\n * @returns The batched CHW tensor, scaled to `[0, 1]`.\n */\nfunction tensorOf(image: RGBImage): ort.Tensor {\n const chw = toCHW(toFloat32(image), image.width, image.height, 3);\n return toFloat32Tensor(chw, [1, 3, image.height, image.width]);\n}\n\n/**\n * Assemble one detection, filling the Ultralytics-style aliases.\n *\n * @param classId Detector class index.\n * @param className Detector class name.\n * @param confidence Detection score.\n * @param bbox Box in original-image coordinates.\n * @param croppedImage The region the box covers.\n * @param classification What the classification stage said about that region.\n * @returns The detection object.\n */\nfunction detection(\n classId: number,\n className: string,\n confidence: number,\n bbox: BoundingBox,\n croppedImage: RGBImage,\n classification: ClassificationResult,\n mask: Mask | null,\n): DetectionResult {\n return {\n classId,\n className,\n confidence,\n bbox,\n cls: classId,\n name: className,\n conf: confidence,\n box: bbox,\n croppedImage,\n classification,\n mask,\n };\n}\n\n/**\n * Resample a crop-space mask onto the cropped image's own pixel grid.\n *\n * Exported for the test suite rather than for callers: it is not re-exported\n * from the package root, and a mask reaches user code already resampled, on\n * {@link DetectionResult.mask}.\n *\n * The graph computes masks at the resolution the segmenter was exported at,\n * which is rarely the size of the box in the original image. Resampling here is\n * what lets {@link DetectionResult.mask} carry the contract `Segmenter` already\n * produces, so a caller that handles one handles the other.\n *\n * Nearest-neighbour on purpose: the values are already thresholded, and\n * interpolating between them would invent edge pixels that are neither.\n *\n * @param masks The graph's `masks` output.\n * @param row Which detection row to read.\n * @param cropWidth Width of one mask in the graph's output.\n * @param cropHeight Height of one mask in the graph's output.\n * @param target The cropped image the mask must line up with.\n * @returns The mask on the crop's grid, or `null` when the box has no area.\n */\nexport function maskToBox(\n masks: Float32Array,\n row: number,\n cropWidth: number,\n cropHeight: number,\n target: RGBImage,\n): Mask | null {\n const { width, height } = target;\n if (width < 1 || height < 1) return null;\n const stride = cropWidth * cropHeight;\n const source = masks.subarray(row * stride, (row + 1) * stride);\n const data = new Uint8Array(width * height);\n for (let y = 0; y < height; y++) {\n const sourceRow = Math.min(Math.floor((y * cropHeight) / height), cropHeight - 1);\n for (let x = 0; x < width; x++) {\n const sourceColumn = Math.min(Math.floor((x * cropWidth) / width), cropWidth - 1);\n data[y * width + x] =\n (source[sourceRow * cropWidth + sourceColumn] ?? 0) > 0.5 ? 255 : 0;\n }\n }\n return new Mask(data, width, height);\n}\n\n/**\n * Assemble the bulk-array `Boxes` view from per-instance detections.\n *\n * @param detections The surviving detections.\n * @param origShape `[height, width]` of the original image.\n * @returns The bulk view, empty when nothing survived.\n */\nfunction bulkBoxes(\n detections: readonly DetectionResult[],\n origShape: readonly [number, number],\n): Boxes {\n const xyxy = new Float32Array(detections.length * 4);\n const cls = new Int32Array(detections.length);\n const conf = new Float32Array(detections.length);\n detections.forEach((entry, index) => {\n const [x1, y1, x2, y2] = entry.bbox.xyxy;\n xyxy[index * 4] = x1;\n xyxy[index * 4 + 1] = y1;\n xyxy[index * 4 + 2] = x2;\n xyxy[index * 4 + 3] = y2;\n cls[index] = entry.classId;\n conf[index] = entry.confidence;\n });\n return new Boxes(xyxy, cls, conf, origShape);\n}\n"],"mappings":"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"}
@@ -1,17 +1,18 @@
1
1
  import { FusionError as e } from "../core/exceptions.js";
2
- import { BoundingBox as t, RGBImage as n } from "../types.js";
2
+ import { BoundingBox as t, Mask as n } from "../types.js";
3
3
  import { SpeedTimer as r } from "../core/timing.js";
4
4
  import { Boxes as i, DetectClassifyResults as a } from "../results.js";
5
5
  import { resolveLabels as o } from "../labels.js";
6
- import { OrtSession as s } from "../core/session.js";
7
- import { loadImage as c } from "../io/image.js";
8
- import { INPUT_IMAGE as l, INPUT_PAD as u, INPUT_SCALE as d, INPUT_SOURCE as f, OUTPUT_BOXES as p, OUTPUT_CLASSES as m, OUTPUT_PROBS as h, OUTPUT_SCORES as g, readFusionSpec as _ } from "../fusion.js";
9
- import { toCHW as v, toFloat32 as y, toFloat32Tensor as b } from "../preprocess/image.js";
10
- import { LetterboxPipeline as x, zeroTensorData as S } from "../preprocess/pipeline.js";
11
- import { softmax as C, topK as w } from "../postprocess/classification.js";
12
- import { VisionTask as T, requireDetections as E } from "./base.js";
6
+ import { asFloat32Array as s } from "../core/dtypes.js";
7
+ import { OrtSession as c } from "../core/session.js";
8
+ import { loadImageSource as l } from "../io/image.js";
9
+ import { INPUT_IMAGE as u, INPUT_PAD as d, INPUT_SCALE as f, INPUT_SOURCE as p, OUTPUT_BOXES as m, OUTPUT_CLASSES as h, OUTPUT_MASKS as g, OUTPUT_PROBS as _, OUTPUT_SCORES as v, readFusionSpec as y } from "../fusion.js";
10
+ import { toCHW as b, toFloat32 as x, toFloat32Tensor as S } from "../preprocess/image.js";
11
+ import { LetterboxPipeline as C, zeroTensorData as w } from "../preprocess/pipeline.js";
12
+ import { softmax as T, topK as E } from "../postprocess/classification.js";
13
+ import { VisionTask as D, cropToBox as O, requireDetections as k } from "./base.js";
13
14
  //#region src/vision/tasks/detectClassify.ts
14
- var D = class n extends T {
15
+ var A = class n extends D {
15
16
  _spec;
16
17
  _labels;
17
18
  _names;
@@ -25,32 +26,32 @@ var D = class n extends T {
25
26
  async warmup(e = 1) {
26
27
  let [t, n] = this._spec.inputSize;
27
28
  for (let r = 0; r < e; r++) {
28
- let e = { [l]: b(S(t, n), [
29
+ let e = { [u]: S(w(t, n), [
29
30
  1,
30
31
  3,
31
32
  n,
32
33
  t
33
34
  ]) };
34
- this._spec.needsSourceImage && (e[f] = b(S(t, n), [
35
+ this._spec.needsSourceImage && (e[p] = S(w(t, n), [
35
36
  1,
36
37
  3,
37
38
  n,
38
39
  t
39
- ]), e[d] = b(new Float32Array([1]), [1]), e[u] = b(new Float32Array([0, 0]), [2])), await this._session.run(e);
40
+ ]), e[f] = S(new Float32Array([1]), [1]), e[d] = S(new Float32Array([0, 0]), [2])), await this._session.run(e);
40
41
  }
41
42
  }
42
43
  get _pipeline() {
43
44
  if (this._pipelineCache === null) {
44
45
  let [e, t] = this._spec.inputSize;
45
- this._pipelineCache = new x(e, t);
46
+ this._pipelineCache = new C(e, t);
46
47
  }
47
48
  return this._pipelineCache;
48
49
  }
49
50
  static async create(t, r = {}) {
50
- let i = await s.create(t, r), a = _(i.metadata);
51
+ let i = await c.create(t, r), a = y(i.metadata);
51
52
  if (a === null) throw new e("This model carries no fused-pipeline metadata, so DetectClassify cannot tell how to drive it. Build one with ort_vision_sdk.compose.fuse_detect_classify, or load a plain model with Detector/Classifier instead.");
52
- let c = o(r.labels ?? a.detectorNames ?? "coco"), l = o(r.classifierLabels ?? a.classifierNames, { numClasses: O(i) ?? void 0 });
53
- return new n(i, a, c, k(c), l, k(l), r.raiseOnEmpty ?? !1);
53
+ let s = o(r.labels ?? a.detectorNames ?? "coco"), l = o(r.classifierLabels ?? a.classifierNames, { numClasses: j(i) ?? void 0 });
54
+ return new n(i, a, s, M(s), l, M(l), r.raiseOnEmpty ?? !1);
54
55
  }
55
56
  get spec() {
56
57
  return this._spec;
@@ -74,45 +75,51 @@ var D = class n extends T {
74
75
  return this.predict(e, t);
75
76
  }
76
77
  async predict(e, t = {}) {
77
- let n = new r(), i = typeof e == "string" ? e : null, o = await c(e);
78
+ let n = new r(), i = typeof e == "string" ? e : null, { image: o, canvas: c } = await l(e);
78
79
  n.stage("load");
79
- let { feeds: s, scale: l, padLeft: u, padTop: d } = this._preprocess(o);
80
+ let { feeds: u, scale: d, padLeft: f, padTop: p, reused: y } = this._preprocess(o, c);
80
81
  n.stage("preprocess");
81
- let f = await this._session.run(s);
82
- this._pipeline.release(), n.stage("inference");
83
- let _ = A(f, h), v = j(f, p), y = j(f, g), b = M(f, m), x = _.data, S = M(f, "num_detections")[0] ?? 0, C = Math.min(S, Math.floor(v.length / 4)), w = _.dims[_.dims.length - 1] ?? 0, T = t.classes === void 0 ? null : new Set(t.classes), D = t.confThreshold ?? 0, O = [];
84
- for (let e = 0; e < C; e++) {
85
- let n = b[e] ?? 0, r = y[e] ?? 0;
86
- if (r < D || T !== null && !T.has(n)) continue;
87
- let i = this._toOriginal(v, e, {
88
- scale: l,
89
- padLeft: u,
90
- padTop: d,
82
+ let b;
83
+ try {
84
+ b = await this._session.run(u);
85
+ } finally {
86
+ this._pipeline.release({ reused: y });
87
+ }
88
+ n.stage("inference");
89
+ let x = N(b, _), S = P(b, m), C = P(b, v), w = F(b, h), T = s(x.data), E = F(b, "num_detections")[0] ?? 0, D = this._spec.hasMasks ? P(b, g) : null, [A, j] = this._spec.cropSize, M = Math.min(E, Math.floor(S.length / 4)), I = x.dims[x.dims.length - 1] ?? 0, B = t.classes === void 0 ? null : new Set(t.classes), V = t.confThreshold ?? 0, H = [];
90
+ for (let e = 0; e < M; e++) {
91
+ let n = w[e] ?? 0, r = C[e] ?? 0;
92
+ if (r < V || B !== null && !B.has(n)) continue;
93
+ let i = this._toOriginal(S, e, {
94
+ scale: d,
95
+ padLeft: f,
96
+ padTop: p,
91
97
  original: o
92
- }), a = P(o, i);
93
- O.push(F(n, this._names[n] ?? `class_${n}`, r, i, a, this._classify(x.subarray(e * w, (e + 1) * w), a, t.topK)));
98
+ }), a = O(o, i);
99
+ H.push(L(n, this._names[n] ?? `class_${n}`, r, i, a, this._classify(T.subarray(e * I, (e + 1) * I), a, t.topK), D === null ? null : R(D, e, A, j, a)));
94
100
  }
95
- E(O.length, {
101
+ k(H.length, {
96
102
  raiseOnEmpty: t.raiseOnEmpty ?? this._raiseOnEmpty,
97
- confThreshold: Math.max(D, this._spec.confThreshold),
103
+ confThreshold: Math.max(V, this._spec.confThreshold),
98
104
  classes: t.classes,
99
105
  path: i
100
106
  });
101
- let k = [o.height, o.width];
102
- return n.stage("postprocess"), [new a(I(O, k), O, this._names, this._classifierNames, o, k, i, n.speed())];
107
+ let U = [o.height, o.width];
108
+ return n.stage("postprocess"), [new a(z(H, U), H, this._names, this._classifierNames, o, U, i, n.speed())];
103
109
  }
104
- _preprocess(e) {
105
- let [t, n] = this._spec.inputSize, r = this._pipeline.run(e), i = { [l]: b(r.data, [
110
+ _preprocess(e, t) {
111
+ let [n, r] = this._spec.inputSize, i = this._pipeline.run(e, t), a = { [u]: S(i.data, [
106
112
  1,
107
113
  3,
108
- n,
109
- t
114
+ r,
115
+ n
110
116
  ]) };
111
- return this._spec.needsSourceImage && (i[f] = N(e), i[d] = b(new Float32Array([r.scale]), [1]), i[u] = b(new Float32Array([r.padLeft, r.padTop]), [2])), {
112
- feeds: i,
113
- scale: r.scale,
114
- padLeft: r.padLeft,
115
- padTop: r.padTop
117
+ return this._spec.needsSourceImage && (a[p] = I(e), a[f] = S(new Float32Array([i.scale]), [1]), a[d] = S(new Float32Array([i.padLeft, i.padTop]), [2])), {
118
+ feeds: a,
119
+ scale: i.scale,
120
+ padLeft: i.padLeft,
121
+ padTop: i.padTop,
122
+ reused: i.reused
116
123
  };
117
124
  }
118
125
  _toOriginal(e, n, r) {
@@ -120,7 +127,7 @@ var D = class n extends T {
120
127
  return new t(l((c(0) - a) / i), u((c(1) - o) / i), l((c(2) - a) / i), u((c(3) - o) / i));
121
128
  }
122
129
  _classify(e, t, n) {
123
- let r = this._spec.applySoftmax ? C(e) : e, { indices: i, values: a } = w(r, n ?? null), o = [];
130
+ let r = this._spec.applySoftmax ? T(e) : e, { indices: i, values: a } = E(r, n ?? null), o = [];
124
131
  for (let e = 0; e < i.length; e++) {
125
132
  let t = i[e] ?? 0, n = a[e] ?? 0, r = this._classifierLabels[t] ?? `class_${t}`;
126
133
  o.push({
@@ -152,50 +159,40 @@ var D = class n extends T {
152
159
  };
153
160
  }
154
161
  };
155
- function O(e) {
156
- let t = e.outputNames.indexOf(h);
162
+ function j(e) {
163
+ let t = e.outputNames.indexOf(_);
157
164
  if (t < 0) return null;
158
165
  let n = e.outputShapes[t];
159
166
  return n === void 0 || n.length === 0 ? null : n[n.length - 1] ?? null;
160
167
  }
161
- function k(e) {
168
+ function M(e) {
162
169
  let t = {};
163
170
  for (let n = 0; n < e.length; n++) t[n] = e[n];
164
171
  return t;
165
172
  }
166
- function A(t, n) {
173
+ function N(t, n) {
167
174
  let r = t[n];
168
175
  if (r === void 0) throw new e(`The fused pipeline is missing its '${n}' output.`);
169
176
  return r;
170
177
  }
171
- function j(e, t) {
172
- return A(e, t).data;
178
+ function P(e, t) {
179
+ return s(N(e, t).data);
173
180
  }
174
- function M(e, t) {
175
- let n = A(e, t).data, r = [];
181
+ function F(e, t) {
182
+ let n = N(e, t).data, r = [];
176
183
  for (let e = 0; e < n.length; e++) r.push(Number(n[e]));
177
184
  return r;
178
185
  }
179
- function N(e) {
180
- let t = v(y(e), e.width, e.height, 3);
181
- return b(t, [
186
+ function I(e) {
187
+ let t = b(x(e), e.width, e.height, 3);
188
+ return S(t, [
182
189
  1,
183
190
  3,
184
191
  e.height,
185
192
  e.width
186
193
  ]);
187
194
  }
188
- function P(e, t) {
189
- let [r, i, a, o] = t.asIntXyxy(), s = Math.max(0, r), c = Math.max(0, i), l = Math.min(e.width, a), u = Math.min(e.height, o);
190
- if (l <= s || u <= c) return new n(/* @__PURE__ */ new Uint8Array(), 0, 0);
191
- let d = l - s, f = u - c, p = new Uint8Array(d * f * 3);
192
- for (let t = 0; t < f; t++) {
193
- let n = ((c + t) * e.width + s) * 3;
194
- p.set(e.data.subarray(n, n + d * 3), t * d * 3);
195
- }
196
- return new n(p, d, f);
197
- }
198
- function F(e, t, n, r, i, a) {
195
+ function L(e, t, n, r, i, a, o) {
199
196
  return {
200
197
  classId: e,
201
198
  className: t,
@@ -206,10 +203,24 @@ function F(e, t, n, r, i, a) {
206
203
  conf: n,
207
204
  box: r,
208
205
  croppedImage: i,
209
- classification: a
206
+ classification: a,
207
+ mask: o
210
208
  };
211
209
  }
212
- function I(e, t) {
210
+ function R(e, t, r, i, a) {
211
+ let { width: o, height: s } = a;
212
+ if (o < 1 || s < 1) return null;
213
+ let c = r * i, l = e.subarray(t * c, (t + 1) * c), u = new Uint8Array(o * s);
214
+ for (let e = 0; e < s; e++) {
215
+ let t = Math.min(Math.floor(e * i / s), i - 1);
216
+ for (let n = 0; n < o; n++) {
217
+ let i = Math.min(Math.floor(n * r / o), r - 1);
218
+ u[e * o + n] = (l[t * r + i] ?? 0) > .5 ? 255 : 0;
219
+ }
220
+ }
221
+ return new n(u, o, s);
222
+ }
223
+ function z(e, t) {
213
224
  let n = new Float32Array(e.length * 4), r = new Int32Array(e.length), a = new Float32Array(e.length);
214
225
  return e.forEach((e, t) => {
215
226
  let [i, o, s, c] = e.bbox.xyxy;
@@ -217,6 +228,6 @@ function I(e, t) {
217
228
  }), new i(n, r, a, t);
218
229
  }
219
230
  //#endregion
220
- export { D as DetectClassify };
231
+ export { A as DetectClassify, R as maskToBox };
221
232
 
222
233
  //# sourceMappingURL=detectClassify.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"detectClassify.js","names":[],"sources":["../../../src/vision/tasks/detectClassify.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Run a fused detect→classify pipeline in the browser.\n *\n * The file this loads was built by the Python SDK's `ort_vision_sdk.compose`,\n * and it already contains both models plus the crop-and-resize bridge between\n * them. That matters far more in a tab than on a server: two models mean two\n * `.onnx` downloads, two WASM/WebGPU session initializations, and a per-crop\n * round trip through JavaScript to slice, resize and restack the regions before\n * the second model can see them. A fused pipeline has one download, one session\n * and no round trip — the crops are produced and consumed inside the graph.\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { FusionError } from \"../core/exceptions\";\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { SpeedTimer } from \"../core/timing\";\nimport {\n INPUT_IMAGE,\n INPUT_PAD,\n INPUT_SCALE,\n INPUT_SOURCE,\n OUTPUT_BOXES,\n OUTPUT_CLASSES,\n OUTPUT_NUM_DETECTIONS,\n OUTPUT_PROBS,\n OUTPUT_SCORES,\n type FusionSpec,\n readFusionSpec,\n} from \"../fusion\";\nimport { type ImageInput, loadImage } from \"../io/image\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport { softmax, topK } from \"../postprocess/classification\";\nimport { toCHW, toFloat32, toFloat32Tensor } from \"../preprocess/image\";\nimport { LetterboxPipeline, zeroTensorData } from \"../preprocess/pipeline\";\nimport { Boxes, DetectClassifyResults } from \"../results\";\nimport {\n BoundingBox,\n RGBImage,\n type ClassProbability,\n type ClassificationResult,\n type DetectionResult,\n} from \"../types\";\nimport { VisionTask, requireDetections } from \"./base\";\n\nexport interface DetectClassifyOptions extends OrtSessionOptions {\n /**\n * Class label spec for the **detection** stage — see {@link resolveLabels}.\n * Defaults to the names recorded at fusion time, falling back to the COCO\n * 80-class preset when the fusion recorded none.\n */\n readonly labels?: LabelSpec;\n /**\n * Class label spec for the **classification** stage. Defaults to the recorded\n * names, falling back to generated `class_<id>` names.\n */\n readonly classifierLabels?: LabelSpec;\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 DetectClassifyPredictOptions {\n /**\n * Drop detections scoring below this. The graph's own NMS threshold was fixed\n * at fusion time and cannot be lowered here — this only filters further.\n */\n readonly confThreshold?: number;\n /** If set, keep only detections whose detector `classId` is in this list. */\n readonly classes?: readonly number[];\n /** Truncate each detection's `classification.probabilities` to its top-k entries. */\n readonly topK?: number;\n /** Override the constructor's `raiseOnEmpty` setting for this call. */\n readonly raiseOnEmpty?: boolean;\n}\n\n/**\n * Detector and classifier running as a single ONNX model.\n *\n * Everything the pipeline needs to know about itself — the resolution to\n * letterbox to, whether it wants the full-resolution image as well, whether its\n * classifier output still needs a softmax, the class names of both stages — was\n * written into the file at fusion time and is read back here. Nothing is\n * restated on the JavaScript side, so nothing can drift out of step with the\n * Python side that built it.\n *\n * @example\n * ```typescript\n * const pipeline = await DetectClassify.create(\"/models/pipeline.onnx\");\n * const result = (await pipeline.predict(\"/images/flock.jpg\"))[0];\n * for (const detection of result) {\n * console.log(detection.name, detection.conf, detection.classification?.name);\n * }\n * ```\n */\nexport class DetectClassify extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _spec: FusionSpec,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _classifierLabels: readonly string[],\n private readonly _classifierNames: Readonly<Record<number, string>>,\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 zero-filled inputs, paying one-time costs up front.\n *\n * Worth more here than on a single-stage task: a fused pipeline is two models\n * plus the bridge in one graph, so the first inference compiles shaders for\n * all of it. Calling this while a loading spinner is still up moves that cost\n * 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 [width, height] = this._spec.inputSize;\n for (let i = 0; i < runs; i++) {\n const feeds: Record<string, ort.Tensor> = {\n [INPUT_IMAGE]: toFloat32Tensor(zeroTensorData(width, height), [\n 1,\n 3,\n height,\n width,\n ]),\n };\n if (this._spec.needsSourceImage) {\n feeds[INPUT_SOURCE] = toFloat32Tensor(zeroTensorData(width, height), [\n 1,\n 3,\n height,\n width,\n ]);\n feeds[INPUT_SCALE] = toFloat32Tensor(new Float32Array([1]), [1]);\n feeds[INPUT_PAD] = toFloat32Tensor(new Float32Array([0, 0]), [2]);\n }\n await this._session.run(feeds);\n }\n }\n\n /**\n * The fused preprocessing pipeline, built on first use.\n *\n * Lazily, because constructing it allocates canvases: a pipeline 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 const [width, height] = this._spec.inputSize;\n this._pipelineCache = new LetterboxPipeline(width, height);\n }\n return this._pipelineCache;\n }\n\n /**\n * Load a fused pipeline and resolve both label spaces.\n *\n * @param model The fused `.onnx` — a URL, an `ArrayBuffer`, or bytes.\n * @param options Label overrides plus the usual session options.\n * @throws {@link FusionError} when the model carries no pipeline metadata,\n * i.e. it is a plain detector or classifier rather than something\n * `ort_vision_sdk.compose` produced.\n */\n static async create(\n model: ModelSource,\n options: DetectClassifyOptions = {},\n ): Promise<DetectClassify> {\n const session = await OrtSession.create(model, options);\n const spec = readFusionSpec(session.metadata);\n if (spec === null) {\n throw new FusionError(\n \"This model carries no fused-pipeline metadata, so DetectClassify cannot tell how to \" +\n \"drive it. Build one with ort_vision_sdk.compose.fuse_detect_classify, or load a \" +\n \"plain model with Detector/Classifier instead.\",\n );\n }\n\n const labels = resolveLabels(options.labels ?? spec.detectorNames ?? \"coco\");\n const classifierLabels = resolveLabels(options.classifierLabels ?? spec.classifierNames, {\n numClasses: classifierClasses(session) ?? undefined,\n });\n return new DetectClassify(\n session,\n spec,\n labels,\n indexNames(labels),\n classifierLabels,\n indexNames(classifierLabels),\n options.raiseOnEmpty ?? false,\n );\n }\n\n /** The pipeline configuration recorded in the model at fusion time. */\n get spec(): FusionSpec {\n return this._spec;\n }\n\n /** The `[width, height]` the detection stage runs at. */\n get inputSize(): readonly [number, number] {\n return this._spec.inputSize;\n }\n\n /** Detector class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Detector class id → class name (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /** Classifier class labels indexed by class id. */\n get classifierLabels(): readonly string[] {\n return this._classifierLabels;\n }\n\n /** Classifier class id → class name. */\n get classifierNames(): Readonly<Record<number, string>> {\n return this._classifierNames;\n }\n\n /**\n * Alias for {@link predict} — call the pipeline like a torch `nn.Module`.\n *\n * Use as `pipeline.call(img)` since JavaScript class instances are not\n * callable; for direct invocation, prefer `pipeline.predict(img)`.\n */\n async call(\n image: ImageInput,\n options: DetectClassifyPredictOptions = {},\n ): Promise<DetectClassifyResults[]> {\n return this.predict(image, options);\n }\n\n /**\n * Run the pipeline on a single image.\n *\n * The returned envelope carries a {@link Speed} breakdown in `speed`. Its\n * `inference` figure covers detection *and* classification, since the\n * pipeline runs them as one graph and no boundary between them is observable\n * from outside.\n */\n async predict(\n image: ImageInput,\n options: DetectClassifyPredictOptions = {},\n ): Promise<DetectClassifyResults[]> {\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 { feeds, scale, padLeft, padTop } = this._preprocess(original);\n timer.stage(\"preprocess\");\n const outputs = await this._session.run(feeds);\n this._pipeline.release();\n timer.stage(\"inference\");\n\n const probsTensor = output(outputs, OUTPUT_PROBS);\n const boxes = floats(outputs, OUTPUT_BOXES);\n const scores = floats(outputs, OUTPUT_SCORES);\n const classes = integers(outputs, OUTPUT_CLASSES);\n const probs = probsTensor.data as Float32Array;\n const reported = integers(outputs, OUTPUT_NUM_DETECTIONS)[0] ?? 0;\n const rows = Math.min(reported, Math.floor(boxes.length / 4));\n const classCount = probsTensor.dims[probsTensor.dims.length - 1] ?? 0;\n\n const allowed = options.classes === undefined ? null : new Set(options.classes);\n const floor = options.confThreshold ?? 0;\n const detections: DetectionResult[] = [];\n for (let row = 0; row < rows; row++) {\n const classId = classes[row] ?? 0;\n const confidence = scores[row] ?? 0;\n if (confidence < floor || (allowed !== null && !allowed.has(classId))) continue;\n\n const bbox = this._toOriginal(boxes, row, { scale, padLeft, padTop, original });\n const cropped = crop(original, bbox);\n detections.push(\n detection(\n classId,\n this._names[classId] ?? `class_${classId}`,\n confidence,\n bbox,\n cropped,\n this._classify(\n probs.subarray(row * classCount, (row + 1) * classCount),\n cropped,\n options.topK,\n ),\n ),\n );\n }\n\n requireDetections(detections.length, {\n raiseOnEmpty: options.raiseOnEmpty ?? this._raiseOnEmpty,\n confThreshold: Math.max(floor, this._spec.confThreshold),\n classes: options.classes,\n path,\n });\n\n const origShape: readonly [number, number] = [original.height, original.width];\n timer.stage(\"postprocess\");\n return [\n new DetectClassifyResults(\n bulkBoxes(detections, origShape),\n detections,\n this._names,\n this._classifierNames,\n original,\n origShape,\n path,\n timer.speed(),\n ),\n ];\n }\n\n /**\n * Letterbox the image and build the graph's feeds.\n *\n * The detector input runs through {@link LetterboxPipeline}, which fuses the\n * resize, the padding and the HWC-to-CHW float conversion into one\n * `drawImage` plus one readback, and reuses its output buffer between frames.\n * That buffer goes straight to ONNX Runtime, so `_pipeline.release()` must not\n * be called until the run resolves.\n *\n * A pipeline fused with `cropSource: \"original\"` also takes the untouched\n * image as a second input, plus the scale and padding of the letterbox — that\n * is what lets the graph undo the letterbox transform internally and crop at\n * native resolution instead of from the downscaled copy. That one is **not**\n * letterboxed by definition, so it does not go through the fused path.\n */\n private _preprocess(image: RGBImage): {\n feeds: Record<string, ort.Tensor>;\n scale: number;\n padLeft: number;\n padTop: number;\n } {\n const [width, height] = this._spec.inputSize;\n const boxed = this._pipeline.run(image);\n const feeds: Record<string, ort.Tensor> = {\n [INPUT_IMAGE]: toFloat32Tensor(boxed.data, [1, 3, height, width]),\n };\n\n if (this._spec.needsSourceImage) {\n feeds[INPUT_SOURCE] = tensorOf(image);\n feeds[INPUT_SCALE] = toFloat32Tensor(new Float32Array([boxed.scale]), [1]);\n feeds[INPUT_PAD] = toFloat32Tensor(\n new Float32Array([boxed.padLeft, boxed.padTop]),\n [2],\n );\n }\n return { feeds, scale: boxed.scale, padLeft: boxed.padLeft, padTop: boxed.padTop };\n }\n\n /**\n * Map one letterboxed xyxy row back onto the original image.\n *\n * The graph always reports boxes in the detector's letterboxed pixel space,\n * whichever crop source it was fused with, so both sources agree here.\n */\n private _toOriginal(\n boxes: Float32Array,\n row: number,\n context: {\n scale: number;\n padLeft: number;\n padTop: number;\n original: RGBImage;\n },\n ): BoundingBox {\n const { scale, padLeft, padTop, original } = context;\n const at = (offset: number): number => boxes[row * 4 + offset] ?? 0;\n const clampX = (value: number): number => Math.min(Math.max(value, 0), original.width);\n const clampY = (value: number): number => Math.min(Math.max(value, 0), original.height);\n return new BoundingBox(\n clampX((at(0) - padLeft) / scale),\n clampY((at(1) - padTop) / scale),\n clampX((at(2) - padLeft) / scale),\n clampY((at(3) - padTop) / scale),\n );\n }\n\n /**\n * Turn one row of the classifier output into a result object.\n *\n * @param row The output row for this detection.\n * @param image The crop the row describes, carried so callers can display\n * what was classified.\n * @param k Optional truncation of the probability list.\n */\n private _classify(\n row: Float32Array,\n image: RGBImage,\n k: number | undefined,\n ): ClassificationResult {\n const scores = this._spec.applySoftmax ? softmax(row) : row;\n const { indices, values } = topK(scores, k ?? null);\n const probabilities: ClassProbability[] = [];\n for (let i = 0; i < indices.length; i++) {\n const classId = indices[i] ?? 0;\n const probability = values[i] ?? 0;\n const className = this._classifierLabels[classId] ?? `class_${classId}`;\n probabilities.push({\n classId,\n className,\n probability,\n cls: classId,\n name: className,\n conf: probability,\n });\n }\n const top = probabilities[0] ?? {\n classId: 0,\n className: \"class_0\",\n probability: 0,\n cls: 0,\n name: \"class_0\",\n conf: 0,\n };\n return {\n classId: top.classId,\n className: top.className,\n confidence: top.probability,\n cls: top.classId,\n name: top.className,\n conf: top.probability,\n image,\n probabilities,\n };\n }\n}\n\n/**\n * Read the classifier stage's class count off the `probs` output shape.\n *\n * @param session The loaded pipeline session.\n * @returns The class count, or `null` when the graph leaves that axis dynamic\n * or declares no `probs` output — in which case label resolution falls back\n * to whatever the fusion recorded.\n */\nfunction classifierClasses(session: OrtSession): number | null {\n const index = session.outputNames.indexOf(OUTPUT_PROBS);\n if (index < 0) return null;\n const shape = session.outputShapes[index];\n if (shape === undefined || shape.length === 0) return null;\n return shape[shape.length - 1] ?? null;\n}\n\n/**\n * Build a class id → name record from an ordered label list.\n *\n * @param labels Labels indexed by class id.\n * @returns The equivalent record.\n */\nfunction indexNames(labels: readonly string[]): Readonly<Record<number, string>> {\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) names[i] = labels[i] as string;\n return names;\n}\n\n/**\n * Fetch an output tensor by name.\n *\n * @param outputs The run's results.\n * @param name The output's name in the pipeline contract.\n * @returns The tensor.\n * @throws {@link FusionError} when the graph does not carry that output, which\n * means the file is not a pipeline this version can drive.\n */\nfunction output(outputs: Record<string, ort.Tensor>, name: string): ort.Tensor {\n const tensor = outputs[name];\n if (tensor === undefined) {\n throw new FusionError(`The fused pipeline is missing its '${name}' output.`);\n }\n return tensor;\n}\n\n/**\n * Fetch a float output by name.\n *\n * @param outputs The run's results.\n * @param name The output's name in the pipeline contract.\n * @returns The output's data.\n * @throws {@link FusionError} when the graph does not carry that output.\n */\nfunction floats(outputs: Record<string, ort.Tensor>, name: string): Float32Array {\n return output(outputs, name).data as Float32Array;\n}\n\n/**\n * Fetch an integer output by name, normalizing ORT's 64-bit representation.\n *\n * ONNX Runtime Web returns `int64` tensors as `BigInt64Array`, whose values do\n * not compare or index like numbers. Class ids and detection counts are always\n * small, so widening them to `number` here is lossless and keeps every caller\n * free of `BigInt` handling.\n *\n * @param outputs The run's results.\n * @param name The output's name in the pipeline contract.\n * @returns The output's values as plain numbers.\n * @throws {@link FusionError} when the graph does not carry that output.\n */\nfunction integers(outputs: Record<string, ort.Tensor>, name: string): number[] {\n const data = output(outputs, name).data as BigInt64Array | Int32Array | Float32Array;\n const values: number[] = [];\n for (let i = 0; i < data.length; i++) values.push(Number(data[i]));\n return values;\n}\n\n/**\n * Convert an image to the `[1, 3, H, W]` float32 tensor the graph expects.\n *\n * @param image The image to convert.\n * @returns The batched CHW tensor, scaled to `[0, 1]`.\n */\nfunction tensorOf(image: RGBImage): ort.Tensor {\n const chw = toCHW(toFloat32(image), image.width, image.height, 3);\n return toFloat32Tensor(chw, [1, 3, image.height, image.width]);\n}\n\n/**\n * Cut the box region out of the original image.\n *\n * @param image The source image.\n * @param bbox The box, in original-image pixel coordinates.\n * @returns The cropped region, or a zero-sized image for a box with no area.\n */\nfunction crop(image: RGBImage, bbox: BoundingBox): RGBImage {\n const [rawX1, rawY1, rawX2, rawY2] = bbox.asIntXyxy();\n const x1 = Math.max(0, rawX1);\n const y1 = Math.max(0, rawY1);\n const x2 = Math.min(image.width, rawX2);\n const y2 = Math.min(image.height, rawY2);\n if (x2 <= x1 || y2 <= y1) return new RGBImage(new Uint8Array(0), 0, 0);\n\n const width = x2 - x1;\n const height = y2 - y1;\n const out = new Uint8Array(width * height * 3);\n for (let row = 0; row < height; row++) {\n const offset = ((y1 + row) * image.width + x1) * 3;\n out.set(image.data.subarray(offset, offset + width * 3), row * width * 3);\n }\n return new RGBImage(out, width, height);\n}\n\n/**\n * Assemble one detection, filling the Ultralytics-style aliases.\n *\n * @param classId Detector class index.\n * @param className Detector class name.\n * @param confidence Detection score.\n * @param bbox Box in original-image coordinates.\n * @param croppedImage The region the box covers.\n * @param classification What the classification stage said about that region.\n * @returns The detection object.\n */\nfunction detection(\n classId: number,\n className: string,\n confidence: number,\n bbox: BoundingBox,\n croppedImage: RGBImage,\n classification: ClassificationResult,\n): DetectionResult {\n return {\n classId,\n className,\n confidence,\n bbox,\n cls: classId,\n name: className,\n conf: confidence,\n box: bbox,\n croppedImage,\n classification,\n };\n}\n\n/**\n * Assemble the bulk-array `Boxes` view from per-instance detections.\n *\n * @param detections The surviving detections.\n * @param origShape `[height, width]` of the original image.\n * @returns The bulk view, empty when nothing survived.\n */\nfunction bulkBoxes(\n detections: readonly DetectionResult[],\n origShape: readonly [number, number],\n): Boxes {\n const xyxy = new Float32Array(detections.length * 4);\n const cls = new Int32Array(detections.length);\n const conf = new Float32Array(detections.length);\n detections.forEach((entry, index) => {\n const [x1, y1, x2, y2] = entry.bbox.xyxy;\n xyxy[index * 4] = x1;\n xyxy[index * 4 + 1] = y1;\n xyxy[index * 4 + 2] = x2;\n xyxy[index * 4 + 3] = y2;\n cls[index] = entry.classId;\n conf[index] = entry.confidence;\n });\n return new Boxes(xyxy, cls, conf, origShape);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"detectClassify.js","names":[],"sources":["../../../src/vision/tasks/detectClassify.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Run a fused detect→classify pipeline in the browser.\n *\n * The file this loads was built by the Python SDK's `ort_vision_sdk.compose`,\n * and it already contains both models plus the crop-and-resize bridge between\n * them. That matters far more in a tab than on a server: two models mean two\n * `.onnx` downloads, two WASM/WebGPU session initializations, and a per-crop\n * round trip through JavaScript to slice, resize and restack the regions before\n * the second model can see them. A fused pipeline has one download, one session\n * and no round trip — the crops are produced and consumed inside the graph.\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { FusionError } from \"../core/exceptions\";\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { SpeedTimer } from \"../core/timing\";\nimport {\n INPUT_IMAGE,\n INPUT_PAD,\n INPUT_SCALE,\n INPUT_SOURCE,\n OUTPUT_BOXES,\n OUTPUT_CLASSES,\n OUTPUT_NUM_DETECTIONS,\n OUTPUT_MASKS,\n OUTPUT_PROBS,\n OUTPUT_SCORES,\n type FusionSpec,\n readFusionSpec,\n} from \"../fusion\";\nimport { type ImageInput, loadImageSource } from \"../io/image\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport { softmax, topK } from \"../postprocess/classification\";\nimport { asFloat32Array } from \"../core/dtypes\";\nimport { toCHW, toFloat32, toFloat32Tensor } from \"../preprocess/image\";\nimport { LetterboxPipeline, zeroTensorData } from \"../preprocess/pipeline\";\nimport { Boxes, DetectClassifyResults } from \"../results\";\nimport type { RGBImage } from \"../types\";\nimport {\n BoundingBox,\n Mask,\n type ClassProbability,\n type ClassificationResult,\n type DetectionResult,\n} from \"../types\";\nimport { VisionTask, cropToBox, requireDetections } from \"./base\";\n\nexport interface DetectClassifyOptions extends OrtSessionOptions {\n /**\n * Class label spec for the **detection** stage — see {@link resolveLabels}.\n * Defaults to the names recorded at fusion time, falling back to the COCO\n * 80-class preset when the fusion recorded none.\n */\n readonly labels?: LabelSpec;\n /**\n * Class label spec for the **classification** stage. Defaults to the recorded\n * names, falling back to generated `class_<id>` names.\n */\n readonly classifierLabels?: LabelSpec;\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 DetectClassifyPredictOptions {\n /**\n * Drop detections scoring below this. The graph's own NMS threshold was fixed\n * at fusion time and cannot be lowered here — this only filters further.\n */\n readonly confThreshold?: number;\n /** If set, keep only detections whose detector `classId` is in this list. */\n readonly classes?: readonly number[];\n /** Truncate each detection's `classification.probabilities` to its top-k entries. */\n readonly topK?: number;\n /** Override the constructor's `raiseOnEmpty` setting for this call. */\n readonly raiseOnEmpty?: boolean;\n}\n\n/**\n * Detector and classifier running as a single ONNX model.\n *\n * Everything the pipeline needs to know about itself — the resolution to\n * letterbox to, whether it wants the full-resolution image as well, whether its\n * classifier output still needs a softmax, the class names of both stages — was\n * written into the file at fusion time and is read back here. Nothing is\n * restated on the JavaScript side, so nothing can drift out of step with the\n * Python side that built it.\n *\n * @example\n * ```typescript\n * const pipeline = await DetectClassify.create(\"/models/pipeline.onnx\");\n * const result = (await pipeline.predict(\"/images/flock.jpg\"))[0];\n * for (const detection of result) {\n * console.log(detection.name, detection.conf, detection.classification?.name);\n * }\n * ```\n */\nexport class DetectClassify extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _spec: FusionSpec,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _classifierLabels: readonly string[],\n private readonly _classifierNames: Readonly<Record<number, string>>,\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 zero-filled inputs, paying one-time costs up front.\n *\n * Worth more here than on a single-stage task: a fused pipeline is two models\n * plus the bridge in one graph, so the first inference compiles shaders for\n * all of it. Calling this while a loading spinner is still up moves that cost\n * 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 [width, height] = this._spec.inputSize;\n for (let i = 0; i < runs; i++) {\n const feeds: Record<string, ort.Tensor> = {\n [INPUT_IMAGE]: toFloat32Tensor(zeroTensorData(width, height), [\n 1,\n 3,\n height,\n width,\n ]),\n };\n if (this._spec.needsSourceImage) {\n feeds[INPUT_SOURCE] = toFloat32Tensor(zeroTensorData(width, height), [\n 1,\n 3,\n height,\n width,\n ]);\n feeds[INPUT_SCALE] = toFloat32Tensor(new Float32Array([1]), [1]);\n feeds[INPUT_PAD] = toFloat32Tensor(new Float32Array([0, 0]), [2]);\n }\n await this._session.run(feeds);\n }\n }\n\n /**\n * The fused preprocessing pipeline, built on first use.\n *\n * Lazily, because constructing it allocates canvases: a pipeline 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 const [width, height] = this._spec.inputSize;\n this._pipelineCache = new LetterboxPipeline(width, height);\n }\n return this._pipelineCache;\n }\n\n /**\n * Load a fused pipeline and resolve both label spaces.\n *\n * @param model The fused `.onnx` — a URL, an `ArrayBuffer`, or bytes.\n * @param options Label overrides plus the usual session options.\n * @throws {@link FusionError} when the model carries no pipeline metadata,\n * i.e. it is a plain detector or classifier rather than something\n * `ort_vision_sdk.compose` produced.\n */\n static async create(\n model: ModelSource,\n options: DetectClassifyOptions = {},\n ): Promise<DetectClassify> {\n const session = await OrtSession.create(model, options);\n const spec = readFusionSpec(session.metadata);\n if (spec === null) {\n throw new FusionError(\n \"This model carries no fused-pipeline metadata, so DetectClassify cannot tell how to \" +\n \"drive it. Build one with ort_vision_sdk.compose.fuse_detect_classify, or load a \" +\n \"plain model with Detector/Classifier instead.\",\n );\n }\n\n const labels = resolveLabels(options.labels ?? spec.detectorNames ?? \"coco\");\n const classifierLabels = resolveLabels(options.classifierLabels ?? spec.classifierNames, {\n numClasses: classifierClasses(session) ?? undefined,\n });\n return new DetectClassify(\n session,\n spec,\n labels,\n indexNames(labels),\n classifierLabels,\n indexNames(classifierLabels),\n options.raiseOnEmpty ?? false,\n );\n }\n\n /** The pipeline configuration recorded in the model at fusion time. */\n get spec(): FusionSpec {\n return this._spec;\n }\n\n /** The `[width, height]` the detection stage runs at. */\n get inputSize(): readonly [number, number] {\n return this._spec.inputSize;\n }\n\n /** Detector class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Detector class id → class name (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /** Classifier class labels indexed by class id. */\n get classifierLabels(): readonly string[] {\n return this._classifierLabels;\n }\n\n /** Classifier class id → class name. */\n get classifierNames(): Readonly<Record<number, string>> {\n return this._classifierNames;\n }\n\n /**\n * Alias for {@link predict} — call the pipeline like a torch `nn.Module`.\n *\n * Use as `pipeline.call(img)` since JavaScript class instances are not\n * callable; for direct invocation, prefer `pipeline.predict(img)`.\n */\n async call(\n image: ImageInput,\n options: DetectClassifyPredictOptions = {},\n ): Promise<DetectClassifyResults[]> {\n return this.predict(image, options);\n }\n\n /**\n * Run the pipeline on a single image.\n *\n * The returned envelope carries a {@link Speed} breakdown in `speed`. Its\n * `inference` figure covers detection *and* classification, since the\n * pipeline runs them as one graph and no boundary between them is observable\n * from outside.\n */\n async predict(\n image: ImageInput,\n options: DetectClassifyPredictOptions = {},\n ): Promise<DetectClassifyResults[]> {\n const timer = new SpeedTimer();\n const path = typeof image === \"string\" ? image : null;\n const { image: original, canvas } = await loadImageSource(image);\n timer.stage(\"load\");\n const { feeds, scale, padLeft, padTop, reused } = this._preprocess(original, canvas);\n timer.stage(\"preprocess\");\n let outputs: Record<string, ort.Tensor>;\n try {\n outputs = await this._session.run(feeds);\n } finally {\n this._pipeline.release({ reused });\n }\n timer.stage(\"inference\");\n\n const probsTensor = output(outputs, OUTPUT_PROBS);\n const boxes = floats(outputs, OUTPUT_BOXES);\n const scores = floats(outputs, OUTPUT_SCORES);\n const classes = integers(outputs, OUTPUT_CLASSES);\n const probs = asFloat32Array(probsTensor.data);\n const reported = integers(outputs, OUTPUT_NUM_DETECTIONS)[0] ?? 0;\n const masks = this._spec.hasMasks ? floats(outputs, OUTPUT_MASKS) : null;\n const [cropWidth, cropHeight] = this._spec.cropSize;\n const rows = Math.min(reported, Math.floor(boxes.length / 4));\n const classCount = probsTensor.dims[probsTensor.dims.length - 1] ?? 0;\n\n const allowed = options.classes === undefined ? null : new Set(options.classes);\n const floor = options.confThreshold ?? 0;\n const detections: DetectionResult[] = [];\n for (let row = 0; row < rows; row++) {\n const classId = classes[row] ?? 0;\n const confidence = scores[row] ?? 0;\n if (confidence < floor || (allowed !== null && !allowed.has(classId))) continue;\n\n const bbox = this._toOriginal(boxes, row, { scale, padLeft, padTop, original });\n const cropped = cropToBox(original, bbox);\n detections.push(\n detection(\n classId,\n this._names[classId] ?? `class_${classId}`,\n confidence,\n bbox,\n cropped,\n this._classify(\n probs.subarray(row * classCount, (row + 1) * classCount),\n cropped,\n options.topK,\n ),\n masks === null ? null : maskToBox(masks, row, cropWidth, cropHeight, cropped),\n ),\n );\n }\n\n requireDetections(detections.length, {\n raiseOnEmpty: options.raiseOnEmpty ?? this._raiseOnEmpty,\n confThreshold: Math.max(floor, this._spec.confThreshold),\n classes: options.classes,\n path,\n });\n\n const origShape: readonly [number, number] = [original.height, original.width];\n timer.stage(\"postprocess\");\n return [\n new DetectClassifyResults(\n bulkBoxes(detections, origShape),\n detections,\n this._names,\n this._classifierNames,\n original,\n origShape,\n path,\n timer.speed(),\n ),\n ];\n }\n\n /**\n * Letterbox the image and build the graph's feeds.\n *\n * The detector input runs through {@link LetterboxPipeline}, which fuses the\n * resize, the padding and the HWC-to-CHW float conversion into one\n * `drawImage` plus one readback, and reuses its output buffer between frames.\n * That buffer goes straight to ONNX Runtime, so `_pipeline.release()` must not\n * be called until the run resolves.\n *\n * A pipeline fused with `cropSource: \"original\"` also takes the untouched\n * image as a second input, plus the scale and padding of the letterbox — that\n * is what lets the graph undo the letterbox transform internally and crop at\n * native resolution instead of from the downscaled copy. That one is **not**\n * letterboxed by definition, so it does not go through the fused path.\n *\n * @param image The decoded input.\n * @param canvas The opaque canvas it was decoded on, when there is one — see\n * {@link LetterboxPipeline.run}.\n */\n private _preprocess(\n image: RGBImage,\n canvas: CanvasImageSource | null,\n ): {\n feeds: Record<string, ort.Tensor>;\n scale: number;\n padLeft: number;\n padTop: number;\n reused: boolean;\n } {\n const [width, height] = this._spec.inputSize;\n const boxed = this._pipeline.run(image, canvas);\n const feeds: Record<string, ort.Tensor> = {\n [INPUT_IMAGE]: toFloat32Tensor(boxed.data, [1, 3, height, width]),\n };\n\n if (this._spec.needsSourceImage) {\n feeds[INPUT_SOURCE] = tensorOf(image);\n feeds[INPUT_SCALE] = toFloat32Tensor(new Float32Array([boxed.scale]), [1]);\n feeds[INPUT_PAD] = toFloat32Tensor(\n new Float32Array([boxed.padLeft, boxed.padTop]),\n [2],\n );\n }\n return {\n feeds,\n scale: boxed.scale,\n padLeft: boxed.padLeft,\n padTop: boxed.padTop,\n reused: boxed.reused,\n };\n }\n\n /**\n * Map one letterboxed xyxy row back onto the original image.\n *\n * The graph always reports boxes in the detector's letterboxed pixel space,\n * whichever crop source it was fused with, so both sources agree here.\n */\n private _toOriginal(\n boxes: Float32Array,\n row: number,\n context: {\n scale: number;\n padLeft: number;\n padTop: number;\n original: RGBImage;\n },\n ): BoundingBox {\n const { scale, padLeft, padTop, original } = context;\n const at = (offset: number): number => boxes[row * 4 + offset] ?? 0;\n const clampX = (value: number): number => Math.min(Math.max(value, 0), original.width);\n const clampY = (value: number): number => Math.min(Math.max(value, 0), original.height);\n return new BoundingBox(\n clampX((at(0) - padLeft) / scale),\n clampY((at(1) - padTop) / scale),\n clampX((at(2) - padLeft) / scale),\n clampY((at(3) - padTop) / scale),\n );\n }\n\n /**\n * Turn one row of the classifier output into a result object.\n *\n * @param row The output row for this detection.\n * @param image The crop the row describes, carried so callers can display\n * what was classified.\n * @param k Optional truncation of the probability list.\n */\n private _classify(\n row: Float32Array,\n image: RGBImage,\n k: number | undefined,\n ): ClassificationResult {\n const scores = this._spec.applySoftmax ? softmax(row) : row;\n const { indices, values } = topK(scores, k ?? null);\n const probabilities: ClassProbability[] = [];\n for (let i = 0; i < indices.length; i++) {\n const classId = indices[i] ?? 0;\n const probability = values[i] ?? 0;\n const className = this._classifierLabels[classId] ?? `class_${classId}`;\n probabilities.push({\n classId,\n className,\n probability,\n cls: classId,\n name: className,\n conf: probability,\n });\n }\n const top = probabilities[0] ?? {\n classId: 0,\n className: \"class_0\",\n probability: 0,\n cls: 0,\n name: \"class_0\",\n conf: 0,\n };\n return {\n classId: top.classId,\n className: top.className,\n confidence: top.probability,\n cls: top.classId,\n name: top.className,\n conf: top.probability,\n image,\n probabilities,\n };\n }\n}\n\n/**\n * Read the classifier stage's class count off the `probs` output shape.\n *\n * @param session The loaded pipeline session.\n * @returns The class count, or `null` when the graph leaves that axis dynamic\n * or declares no `probs` output — in which case label resolution falls back\n * to whatever the fusion recorded.\n */\nfunction classifierClasses(session: OrtSession): number | null {\n const index = session.outputNames.indexOf(OUTPUT_PROBS);\n if (index < 0) return null;\n const shape = session.outputShapes[index];\n if (shape === undefined || shape.length === 0) return null;\n return shape[shape.length - 1] ?? null;\n}\n\n/**\n * Build a class id → name record from an ordered label list.\n *\n * @param labels Labels indexed by class id.\n * @returns The equivalent record.\n */\nfunction indexNames(labels: readonly string[]): Readonly<Record<number, string>> {\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) names[i] = labels[i] as string;\n return names;\n}\n\n/**\n * Fetch an output tensor by name.\n *\n * @param outputs The run's results.\n * @param name The output's name in the pipeline contract.\n * @returns The tensor.\n * @throws {@link FusionError} when the graph does not carry that output, which\n * means the file is not a pipeline this version can drive.\n */\nfunction output(outputs: Record<string, ort.Tensor>, name: string): ort.Tensor {\n const tensor = outputs[name];\n if (tensor === undefined) {\n throw new FusionError(`The fused pipeline is missing its '${name}' output.`);\n }\n return tensor;\n}\n\n/**\n * Fetch a float output by name.\n *\n * @param outputs The run's results.\n * @param name The output's name in the pipeline contract.\n * @returns The output's data.\n * @throws {@link FusionError} when the graph does not carry that output.\n */\nfunction floats(outputs: Record<string, ort.Tensor>, name: string): Float32Array {\n return asFloat32Array(output(outputs, name).data);\n}\n\n/**\n * Fetch an integer output by name, normalizing ORT's 64-bit representation.\n *\n * ONNX Runtime Web returns `int64` tensors as `BigInt64Array`, whose values do\n * not compare or index like numbers. Class ids and detection counts are always\n * small, so widening them to `number` here is lossless and keeps every caller\n * free of `BigInt` handling.\n *\n * @param outputs The run's results.\n * @param name The output's name in the pipeline contract.\n * @returns The output's values as plain numbers.\n * @throws {@link FusionError} when the graph does not carry that output.\n */\nfunction integers(outputs: Record<string, ort.Tensor>, name: string): number[] {\n const data = output(outputs, name).data as BigInt64Array | Int32Array | Float32Array;\n const values: number[] = [];\n for (let i = 0; i < data.length; i++) values.push(Number(data[i]));\n return values;\n}\n\n/**\n * Convert an image to the `[1, 3, H, W]` float32 tensor the graph expects.\n *\n * @param image The image to convert.\n * @returns The batched CHW tensor, scaled to `[0, 1]`.\n */\nfunction tensorOf(image: RGBImage): ort.Tensor {\n const chw = toCHW(toFloat32(image), image.width, image.height, 3);\n return toFloat32Tensor(chw, [1, 3, image.height, image.width]);\n}\n\n/**\n * Assemble one detection, filling the Ultralytics-style aliases.\n *\n * @param classId Detector class index.\n * @param className Detector class name.\n * @param confidence Detection score.\n * @param bbox Box in original-image coordinates.\n * @param croppedImage The region the box covers.\n * @param classification What the classification stage said about that region.\n * @returns The detection object.\n */\nfunction detection(\n classId: number,\n className: string,\n confidence: number,\n bbox: BoundingBox,\n croppedImage: RGBImage,\n classification: ClassificationResult,\n mask: Mask | null,\n): DetectionResult {\n return {\n classId,\n className,\n confidence,\n bbox,\n cls: classId,\n name: className,\n conf: confidence,\n box: bbox,\n croppedImage,\n classification,\n mask,\n };\n}\n\n/**\n * Resample a crop-space mask onto the cropped image's own pixel grid.\n *\n * Exported for the test suite rather than for callers: it is not re-exported\n * from the package root, and a mask reaches user code already resampled, on\n * {@link DetectionResult.mask}.\n *\n * The graph computes masks at the resolution the segmenter was exported at,\n * which is rarely the size of the box in the original image. Resampling here is\n * what lets {@link DetectionResult.mask} carry the contract `Segmenter` already\n * produces, so a caller that handles one handles the other.\n *\n * Nearest-neighbour on purpose: the values are already thresholded, and\n * interpolating between them would invent edge pixels that are neither.\n *\n * @param masks The graph's `masks` output.\n * @param row Which detection row to read.\n * @param cropWidth Width of one mask in the graph's output.\n * @param cropHeight Height of one mask in the graph's output.\n * @param target The cropped image the mask must line up with.\n * @returns The mask on the crop's grid, or `null` when the box has no area.\n */\nexport function maskToBox(\n masks: Float32Array,\n row: number,\n cropWidth: number,\n cropHeight: number,\n target: RGBImage,\n): Mask | null {\n const { width, height } = target;\n if (width < 1 || height < 1) return null;\n const stride = cropWidth * cropHeight;\n const source = masks.subarray(row * stride, (row + 1) * stride);\n const data = new Uint8Array(width * height);\n for (let y = 0; y < height; y++) {\n const sourceRow = Math.min(Math.floor((y * cropHeight) / height), cropHeight - 1);\n for (let x = 0; x < width; x++) {\n const sourceColumn = Math.min(Math.floor((x * cropWidth) / width), cropWidth - 1);\n data[y * width + x] =\n (source[sourceRow * cropWidth + sourceColumn] ?? 0) > 0.5 ? 255 : 0;\n }\n }\n return new Mask(data, width, height);\n}\n\n/**\n * Assemble the bulk-array `Boxes` view from per-instance detections.\n *\n * @param detections The surviving detections.\n * @param origShape `[height, width]` of the original image.\n * @returns The bulk view, empty when nothing survived.\n */\nfunction bulkBoxes(\n detections: readonly DetectionResult[],\n origShape: readonly [number, number],\n): Boxes {\n const xyxy = new Float32Array(detections.length * 4);\n const cls = new Int32Array(detections.length);\n const conf = new Float32Array(detections.length);\n detections.forEach((entry, index) => {\n const [x1, y1, x2, y2] = entry.bbox.xyxy;\n xyxy[index * 4] = x1;\n xyxy[index * 4 + 1] = y1;\n xyxy[index * 4 + 2] = x2;\n xyxy[index * 4 + 3] = y2;\n cls[index] = entry.classId;\n conf[index] = entry.confidence;\n });\n return new Boxes(xyxy, cls, conf, origShape);\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../types.cjs"),t=require("../core/timing.cjs"),n=require("../results.cjs"),r=require("../labels.cjs"),i=require("../core/graph.cjs"),a=require("../core/metadata.cjs"),o=require("../core/session.cjs"),s=require("../io/image.cjs"),c=require("../preprocess/image.cjs"),l=require("../preprocess/pipeline.cjs"),u=require("../postprocess/detection.cjs"),d=require("./base.cjs");var f=class f extends d.VisionTask{_head;_labels;_names;_inputSize;_confThreshold;_iouThreshold;_maxDetections;_raiseOnEmpty;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._raiseOnEmpty=c}_pipelineCache=null;async warmup(e=1){let[t,n]=this._inputSize;for(let r=0;r<e;r++){let e=c.toFloat32Tensor(l.zeroTensorData(t,n),[1,3,n,t]);await this._session.run({[this._session.inputName]:e})}}get _pipeline(){return this._pipelineCache===null&&(this._pipelineCache=new l.LetterboxPipeline(this._inputSize[0],this._inputSize[1])),this._pipelineCache}static async create(e,t={}){let n=t.head??`yolo`;if(n!==`yolo`)throw Error(`Unsupported detector head '${n}'. Supported: 'yolo'.`);let s=await o.OrtSession.create(e,t),c=t.numClasses??i.detectionNumClasses(s.outputShape)??void 0,l=r.resolveLabels(t.labels??a.modelNames(s.metadata)??r.defaultLabels(c),{numClasses:c}),u={};for(let e=0;e<l.length;e++)u[e]=l[e];return new f(s,n,l,u,i.resolveInputSize({graphShape:s.inputShape,requested:t.inputSize,fallback:[640,640]}),t.confThreshold??.25,t.iouThreshold??.45,t.maxDetections??300,t.raiseOnEmpty??!1)}get head(){return this._head}get labels(){return this._labels}get names(){return this._names}get inputSize(){return this._inputSize}get numClasses(){return this._labels.length}async call(e,t={}){return this.predict(e,t)}async predict(e,r={}){let i=new t.SpeedTimer,a=typeof e==`string`?e:null,o=await s.loadImage(e);i.stage(`load`);let{tensor:c,scale:l,padLeft:f,padTop:p}=this._preprocess(o);i.stage(`preprocess`);let m=await this._session.run({[this._session.inputName]:c});this._pipeline.release(),i.stage(`inference`);let h=this._session.outputNames[0];if(h===void 0)throw Error(`Detector model has no outputs.`);let g=m[h];if(g===void 0)throw Error(`Detector model output ${h} missing from run() result.`);let _=r.confThreshold??this._confThreshold,v=u.decodeYolo(g.data,g.dims,{originalWidth:o.width,originalHeight:o.height,padLeft:f,padTop:p,scale:l,confThreshold:_,iouThreshold:r.iouThreshold??this._iouThreshold,maxDetections:this._maxDetections}),y=r.classes===void 0?v:(()=>{let e=new Set(r.classes);return v.filter(t=>e.has(t.classId))})();d.requireDetections(y.length,{raiseOnEmpty:r.raiseOnEmpty??this._raiseOnEmpty,confThreshold:_,classes:r.classes,path:a});let b=y.map(e=>this._buildResult(o,e.bbox,e.classId,e.confidence)),x=[o.height,o.width],S=this._buildBoxes(b,x);return i.stage(`postprocess`),[new n.DetectionResults(S,b,this._names,o,x,a,i.speed())]}_preprocess(e){let[t,n]=this._inputSize,r=this._pipeline.run(e);return{tensor:c.toFloat32Tensor(r.data,[1,3,n,t]),scale:r.scale,padLeft:r.padLeft,padTop:r.padTop}}_buildResult(t,n,r,i){let[a,o,s,c]=n.asIntXyxy(),l=Math.max(0,a),u=Math.max(0,o),d=Math.min(t.width,s),f=Math.min(t.height,c),p;if(d>l&&f>u){let n=d-l,r=f-u,i=new Uint8Array(n*r*3);for(let e=0;e<r;e++){let r=((u+e)*t.width+l)*3;i.set(t.data.subarray(r,r+n*3),e*n*3)}p=new e.RGBImage(i,n,r)}else p=new e.RGBImage(new Uint8Array,0,0);let m=this._names[r]??`class_${r}`;return{classId:r,className:m,confidence:i,bbox:n,cls:r,name:m,conf:i,box:n,croppedImage:p}}_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)}};exports.Detector=f;
1
+ const e=require("../core/timing.cjs"),t=require("../results.cjs"),n=require("../labels.cjs"),r=require("../core/dtypes.cjs"),i=require("../core/graph.cjs"),a=require("../core/metadata.cjs"),o=require("../core/session.cjs"),s=require("../io/image.cjs"),c=require("../preprocess/image.cjs"),l=require("../preprocess/pipeline.cjs"),u=require("../postprocess/detection.cjs"),d=require("./base.cjs");var f=class f extends d.VisionTask{_head;_labels;_names;_inputSize;_confThreshold;_iouThreshold;_maxDetections;_raiseOnEmpty;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._raiseOnEmpty=c}_pipelineCache=null;async warmup(e=1){let[t,n]=this._inputSize;for(let r=0;r<e;r++){let e=c.toFloat32Tensor(l.zeroTensorData(t,n),[1,3,n,t]);await this._session.run({[this._session.inputName]:e})}}get _pipeline(){return this._pipelineCache===null&&(this._pipelineCache=new l.LetterboxPipeline(this._inputSize[0],this._inputSize[1])),this._pipelineCache}static async create(e,t={}){let r=t.head??`yolo`;if(r!==`yolo`)throw Error(`Unsupported detector head '${r}'. Supported: 'yolo'.`);let s=await o.OrtSession.create(e,t),c=t.numClasses??i.detectionNumClasses(s.outputShape)??void 0,l=n.resolveLabels(t.labels??a.modelNames(s.metadata)??n.defaultLabels(c),{numClasses:c}),u={};for(let e=0;e<l.length;e++)u[e]=l[e];return new f(s,r,l,u,i.resolveInputSize({graphShape:s.inputShape,requested:t.inputSize,fallback:[640,640]}),t.confThreshold??.25,t.iouThreshold??.45,t.maxDetections??300,t.raiseOnEmpty??!1)}get head(){return this._head}get labels(){return this._labels}get names(){return this._names}get inputSize(){return this._inputSize}get numClasses(){return this._labels.length}async call(e,t={}){return this.predict(e,t)}async predict(n,i={}){let a=new e.SpeedTimer,o=typeof n==`string`?n:null,{image:c,canvas:l,owned:f}=await s.loadImageSource(n);a.stage(`load`);let{tensor:p,scale:m,padLeft:h,padTop:g,reused:_}=this._preprocess(c,l);a.stage(`preprocess`);let v;try{v=await this._session.run({[this._session.inputName]:p})}finally{this._pipeline.release({reused:_})}a.stage(`inference`);let y=this._session.outputNames[0];if(y===void 0)throw Error(`Detector model has no outputs.`);let b=v[y];if(b===void 0)throw Error(`Detector model output ${y} missing from run() result.`);let x=i.confThreshold??this._confThreshold,S=u.decodeYolo(r.asFloat32Array(b.data),b.dims,{originalWidth:c.width,originalHeight:c.height,padLeft:h,padTop:g,scale:m,confThreshold:x,iouThreshold:i.iouThreshold??this._iouThreshold,maxDetections:this._maxDetections}),C=i.classes===void 0?S:(()=>{let e=new Set(i.classes);return S.filter(t=>e.has(t.classId))})();d.requireDetections(C.length,{raiseOnEmpty:i.raiseOnEmpty??this._raiseOnEmpty,confThreshold:x,classes:i.classes,path:o});let w=C.map(e=>this._buildResult(c,e.bbox,e.classId,e.confidence,f)),T=[c.height,c.width],E=this._buildBoxes(w,T);return a.stage(`postprocess`),[new t.DetectionResults(E,w,this._names,c,T,o,a.speed())]}_preprocess(e,t){let[n,r]=this._inputSize,i=this._pipeline.run(e,t);return{tensor:c.toFloat32Tensor(i.data,[1,3,r,n]),scale:i.scale,padLeft:i.padLeft,padTop:i.padTop,reused:i.reused}}_buildResult(e,t,n,r,i){let a=this._names[n]??`class_${n}`,o={classId:n,className:a,confidence:r,bbox:t,cls:n,name:a,conf:r,box:t};if(!i)return{...o,croppedImage:d.cropToBox(e,t)};let s=d.memoize(()=>d.cropToBox(e,t));return{...o,get croppedImage(){return s()}}}_buildBoxes(e,n){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 t.Boxes(i,a,o,n)}};exports.Detector=f;
2
2
  //# sourceMappingURL=detector.cjs.map