tempest-react-sdk 0.68.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 (83) hide show
  1. package/README.md +1 -1
  2. package/dist/sw/create-push-handler.cjs +1 -1
  3. package/dist/sw/create-push-handler.cjs.map +1 -1
  4. package/dist/sw/create-push-handler.js +58 -30
  5. package/dist/sw/create-push-handler.js.map +1 -1
  6. package/dist/sw.d.ts +53 -2
  7. package/dist/tempest-react-sdk.d.ts +53 -2
  8. package/dist/vision/core/canvas.cjs +1 -1
  9. package/dist/vision/core/canvas.cjs.map +1 -1
  10. package/dist/vision/core/canvas.js +37 -13
  11. package/dist/vision/core/canvas.js.map +1 -1
  12. package/dist/vision/core/dtypes.cjs +2 -0
  13. package/dist/vision/core/dtypes.cjs.map +1 -0
  14. package/dist/vision/core/dtypes.js +33 -0
  15. package/dist/vision/core/dtypes.js.map +1 -0
  16. package/dist/vision/core/graph.cjs +1 -1
  17. package/dist/vision/core/graph.cjs.map +1 -1
  18. package/dist/vision/core/graph.js +10 -2
  19. package/dist/vision/core/graph.js.map +1 -1
  20. package/dist/vision/core/metadata.cjs +2 -2
  21. package/dist/vision/core/metadata.cjs.map +1 -1
  22. package/dist/vision/core/metadata.js +103 -13
  23. package/dist/vision/core/metadata.js.map +1 -1
  24. package/dist/vision/core/providers.cjs +1 -1
  25. package/dist/vision/core/providers.cjs.map +1 -1
  26. package/dist/vision/core/providers.js +9 -6
  27. package/dist/vision/core/providers.js.map +1 -1
  28. package/dist/vision/core/session.cjs +1 -1
  29. package/dist/vision/core/session.cjs.map +1 -1
  30. package/dist/vision/core/session.js +115 -27
  31. package/dist/vision/core/session.js.map +1 -1
  32. package/dist/vision/fusion.cjs +1 -1
  33. package/dist/vision/fusion.cjs.map +1 -1
  34. package/dist/vision/fusion.js +28 -23
  35. package/dist/vision/fusion.js.map +1 -1
  36. package/dist/vision/index.cjs +1 -1
  37. package/dist/vision/index.cjs.map +1 -1
  38. package/dist/vision/index.js +20 -19
  39. package/dist/vision/index.js.map +1 -1
  40. package/dist/vision/io/image.cjs +1 -1
  41. package/dist/vision/io/image.cjs.map +1 -1
  42. package/dist/vision/io/image.js +55 -20
  43. package/dist/vision/io/image.js.map +1 -1
  44. package/dist/vision/postprocess/detection.cjs +1 -1
  45. package/dist/vision/postprocess/detection.cjs.map +1 -1
  46. package/dist/vision/postprocess/detection.js +42 -51
  47. package/dist/vision/postprocess/detection.js.map +1 -1
  48. package/dist/vision/postprocess/segmentation.cjs +1 -1
  49. package/dist/vision/postprocess/segmentation.cjs.map +1 -1
  50. package/dist/vision/postprocess/segmentation.js +45 -31
  51. package/dist/vision/postprocess/segmentation.js.map +1 -1
  52. package/dist/vision/preprocess/pipeline.cjs +1 -1
  53. package/dist/vision/preprocess/pipeline.cjs.map +1 -1
  54. package/dist/vision/preprocess/pipeline.js +24 -25
  55. package/dist/vision/preprocess/pipeline.js.map +1 -1
  56. package/dist/vision/tasks/base.cjs +1 -1
  57. package/dist/vision/tasks/base.cjs.map +1 -1
  58. package/dist/vision/tasks/base.js +22 -7
  59. package/dist/vision/tasks/base.js.map +1 -1
  60. package/dist/vision/tasks/classifier.cjs +1 -1
  61. package/dist/vision/tasks/classifier.cjs.map +1 -1
  62. package/dist/vision/tasks/classifier.js +64 -55
  63. package/dist/vision/tasks/classifier.js.map +1 -1
  64. package/dist/vision/tasks/detectClassify.cjs +1 -1
  65. package/dist/vision/tasks/detectClassify.cjs.map +1 -1
  66. package/dist/vision/tasks/detectClassify.js +80 -69
  67. package/dist/vision/tasks/detectClassify.js.map +1 -1
  68. package/dist/vision/tasks/detector.cjs +1 -1
  69. package/dist/vision/tasks/detector.cjs.map +1 -1
  70. package/dist/vision/tasks/detector.js +74 -68
  71. package/dist/vision/tasks/detector.js.map +1 -1
  72. package/dist/vision/tasks/segmenter.cjs +1 -1
  73. package/dist/vision/tasks/segmenter.cjs.map +1 -1
  74. package/dist/vision/tasks/segmenter.js +85 -62
  75. package/dist/vision/tasks/segmenter.js.map +1 -1
  76. package/dist/vision/types.cjs +1 -1
  77. package/dist/vision/types.cjs.map +1 -1
  78. package/dist/vision/types.js +24 -4
  79. package/dist/vision/types.js.map +1 -1
  80. package/dist/vision.cjs +1 -1
  81. package/dist/vision.d.ts +1153 -777
  82. package/dist/vision.js +22 -21
  83. package/package.json +2 -2
@@ -1,2 +1,2 @@
1
- const e=require("../core/canvas.cjs");var t=1/255,n=class{_size;_buffer;_inUse=!1;constructor(e){this._size=e,this._buffer=new Float32Array(e)}claim(){return this._inUse?{data:new Float32Array(this._size),reused:!1}:(this._buffer.length!==this._size&&(this._buffer=new Float32Array(this._size)),this._inUse=!0,{data:this._buffer,reused:!0})}release(){this._inUse=!1}},r=class{_targetWidth;_targetHeight;_fill;_target;_targetContext;_buffer;_source=null;_sourceContext=null;constructor(t,r,i=[114,114,114]){if(t<=0||r<=0)throw Error(`Invalid letterbox target ${t}x${r}.`);this._targetWidth=t,this._targetHeight=r,this._fill=i,this._target=e.createCanvas(t,r),this._targetContext=e.get2DContext(this._target,{willReadFrequently:!0}),this._targetContext.imageSmoothingEnabled=!0,this._targetContext.imageSmoothingQuality=`high`,this._buffer=new n(3*r*t)}get targetSize(){return[this._targetWidth,this._targetHeight]}run(n){let r=this._targetWidth,i=this._targetHeight,a=Math.min(r/n.width,i/n.height),o=Math.round(n.width*a),s=Math.round(n.height*a),c=Math.floor((r-o)/2),l=Math.floor((i-s)/2);this._ensureSource(n.width,n.height).putImageData(e.rgbToImageData(n),0,0);let u=this._targetContext;(c>0||l>0||o!==r||s!==i)&&(u.fillStyle=`rgb(${this._fill[0]},${this._fill[1]},${this._fill[2]})`,u.fillRect(0,0,r,i)),u.drawImage(this._source,0,0,n.width,n.height,c,l,o,s);let d=u.getImageData(0,0,r,i).data,{data:f,reused:p}=this._buffer.claim(),m=r*i;for(let e=0,n=0;e<m;e++,n+=4)f[e]=d[n]*t,f[m+e]=d[n+1]*t,f[2*m+e]=d[n+2]*t;return{data:f,scale:a,padLeft:c,padTop:l,reused:p}}release(){this._buffer.release()}_ensureSource(t,n){return(this._source===null||this._source.width!==t||this._source.height!==n)&&(this._source=e.createCanvas(t,n),this._sourceContext=e.get2DContext(this._source)),this._sourceContext}};function i(e,n,r,i,a,o,s=4){let c=n*r,l=i[0],u=i[1],d=i[2],f=a[0],p=a[1],m=a[2];for(let n=0,r=0;n<c;n++,r+=s)o[n]=(e[r]*t-l)/f,o[c+n]=(e[r+1]*t-u)/p,o[2*c+n]=(e[r+2]*t-d)/m}var a=class{_targetWidth;_targetHeight;_mean;_std;_buffer;_target=null;_targetContext=null;_source=null;_sourceContext=null;constructor(e,t,r=[0,0,0],i=[1,1,1]){if(e<=0||t<=0)throw Error(`Invalid resize target ${e}x${t}.`);this._targetWidth=e,this._targetHeight=t,this._mean=r,this._std=i,this._buffer=new n(3*t*e)}get targetSize(){return[this._targetWidth,this._targetHeight]}run(t){let n=this._targetWidth,r=this._targetHeight,{data:a,reused:o}=this._buffer.claim();if(t.width===n&&t.height===r)return i(t.data,n,r,this._mean,this._std,a,3),{data:a,reused:o};this._ensureSource(t.width,t.height).putImageData(e.rgbToImageData(t),0,0);let s=this._ensureTarget();s.drawImage(this._source,0,0,n,r);let c=s.getImageData(0,0,n,r).data;return i(c,n,r,this._mean,this._std,a),{data:a,reused:o}}release(){this._buffer.release()}_ensureTarget(){return this._target===null&&(this._target=e.createCanvas(this._targetWidth,this._targetHeight),this._targetContext=e.get2DContext(this._target,{willReadFrequently:!0}),this._targetContext.imageSmoothingEnabled=!0,this._targetContext.imageSmoothingQuality=`high`),this._targetContext}_ensureSource(t,n){return(this._source===null||this._source.width!==t||this._source.height!==n)&&(this._source=e.createCanvas(t,n),this._sourceContext=e.get2DContext(this._source)),this._sourceContext}};function o(e,t,n,r=[0,0,0],i=[1,1,1]){return new a(t,n,r,i).run(e)}function s(e,t){return new Float32Array(3*t*e)}function c(e,t,n,i=[114,114,114]){return new r(t,n,i).run(e)}exports.LetterboxPipeline=r,exports.ResizePipeline=a,exports.letterboxToTensorData=c,exports.resizeToTensorData=o,exports.writePlanarFloat32=i,exports.zeroTensorData=s;
1
+ const e=require("../core/canvas.cjs");var t=1/255,n=class{_size;_buffer;_inUse=!1;constructor(e){this._size=e,this._buffer=new Float32Array(e)}claim(){return this._inUse?{data:new Float32Array(this._size),reused:!1}:(this._buffer.length!==this._size&&(this._buffer=new Float32Array(this._size)),this._inUse=!0,{data:this._buffer,reused:!0})}release(){this._inUse=!1}},r=class{_targetWidth;_targetHeight;_fill;_target;_targetContext;_buffer;_source=null;_sourceContext=null;constructor(t,r,i=[114,114,114]){if(t<=0||r<=0)throw Error(`Invalid letterbox target ${t}x${r}.`);this._targetWidth=t,this._targetHeight=r,this._fill=i,this._target=e.createCanvas(t,r),this._targetContext=e.get2DContext(this._target,{willReadFrequently:!0}),this._targetContext.imageSmoothingEnabled=!0,this._targetContext.imageSmoothingQuality=`high`,this._buffer=new n(3*r*t)}get targetSize(){return[this._targetWidth,this._targetHeight]}run(e,n=null){let r=this._targetWidth,i=this._targetHeight,a=Math.min(r/e.width,i/e.height),o=Math.round(e.width*a),s=Math.round(e.height*a),c=Math.floor((r-o)/2),l=Math.floor((i-s)/2),u=n??this._paintSource(e),d=this._targetContext;(c>0||l>0||o!==r||s!==i)&&(d.fillStyle=`rgb(${this._fill[0]},${this._fill[1]},${this._fill[2]})`,d.fillRect(0,0,r,i)),d.drawImage(u,0,0,e.width,e.height,c,l,o,s);let f=d.getImageData(0,0,r,i).data,{data:p,reused:m}=this._buffer.claim(),h=r*i;for(let e=0,n=0;e<h;e++,n+=4)p[e]=f[n]*t,p[h+e]=f[n+1]*t,p[2*h+e]=f[n+2]*t;return{data:p,scale:a,padLeft:c,padTop:l,reused:m}}release(e){(e===void 0||e.reused)&&this._buffer.release()}_paintSource(t){let{width:n,height:r}=t;return(this._source===null||this._source.width!==n||this._source.height!==r)&&(this._source=e.createCanvas(n,r),this._sourceContext=e.get2DContext(this._source)),this._sourceContext.putImageData(e.rgbToImageData(t),0,0),this._source}};function i(e,n,r,i,a,o,s=4){let c=n*r,l=i[0],u=i[1],d=i[2],f=a[0],p=a[1],m=a[2];for(let n=0,r=0;n<c;n++,r+=s)o[n]=(e[r]*t-l)/f,o[c+n]=(e[r+1]*t-u)/p,o[2*c+n]=(e[r+2]*t-d)/m}var a=class{_targetWidth;_targetHeight;_mean;_std;_buffer;_target=null;_targetContext=null;_source=null;_sourceContext=null;constructor(e,t,r=[0,0,0],i=[1,1,1]){if(e<=0||t<=0)throw Error(`Invalid resize target ${e}x${t}.`);this._targetWidth=e,this._targetHeight=t,this._mean=r,this._std=i,this._buffer=new n(3*t*e)}get targetSize(){return[this._targetWidth,this._targetHeight]}run(e,t=null){let n=this._targetWidth,r=this._targetHeight,{data:a,reused:o}=this._buffer.claim();if(e.width===n&&e.height===r)return i(e.data,n,r,this._mean,this._std,a,3),{data:a,reused:o};let s=t??this._paintSource(e),c=this._ensureTarget();c.drawImage(s,0,0,e.width,e.height,0,0,n,r);let l=c.getImageData(0,0,n,r).data;return i(l,n,r,this._mean,this._std,a),{data:a,reused:o}}release(e){(e===void 0||e.reused)&&this._buffer.release()}_ensureTarget(){return this._target===null&&(this._target=e.createCanvas(this._targetWidth,this._targetHeight),this._targetContext=e.get2DContext(this._target,{willReadFrequently:!0}),this._targetContext.imageSmoothingEnabled=!0,this._targetContext.imageSmoothingQuality=`high`),this._targetContext}_paintSource(t){let{width:n,height:r}=t;return(this._source===null||this._source.width!==n||this._source.height!==r)&&(this._source=e.createCanvas(n,r),this._sourceContext=e.get2DContext(this._source)),this._sourceContext.putImageData(e.rgbToImageData(t),0,0),this._source}};function o(e,t,n,r=[0,0,0],i=[1,1,1]){return new a(t,n,r,i).run(e)}function s(e,t){return new Float32Array(3*t*e)}function c(e,t,n,i=[114,114,114]){return new r(t,n,i).run(e)}exports.LetterboxPipeline=r,exports.ResizePipeline=a,exports.letterboxToTensorData=c,exports.resizeToTensorData=o,exports.writePlanarFloat32=i,exports.zeroTensorData=s;
2
2
  //# sourceMappingURL=pipeline.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.cjs","names":[],"sources":["../../../src/vision/preprocess/pipeline.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Fused letterbox → CHW float32 pipeline with reusable buffers.\n *\n * The composable primitives in {@link ./image.js} each allocate and each walk\n * their input end to end, which is the right shape for a library but the wrong\n * shape for a video loop. Chaining them costs eleven full-buffer passes and six\n * large allocations per frame:\n *\n * `getImageData` → RGBA→RGB → RGB→RGBA → `putImageData` → `drawImage` →\n * `getImageData` → RGBA→RGB → fill → row copies → `toFloat32` → `toCHW`.\n *\n * This module collapses the second half of that into two: one `drawImage` that\n * resizes *and* positions the content inside the padded target in a single\n * accelerated operation, and one loop that reads the resulting RGBA and writes\n * planar float32 directly. The intermediate `RGBImage` at target size, the fill\n * loop, the row copies and the two 4.9 MB `Float32Array` allocations all go\n * away.\n *\n * The primitives stay exactly as they are — they are public API and they are\n * what makes a custom pipeline writable. This is the fast path the built-in\n * tasks take.\n */\n\nimport {\n createCanvas,\n get2DContext,\n rgbToImageData,\n type Canvas2D,\n type Context2D,\n} from \"../core/canvas\";\nimport type { RGBImage } from \"../types\";\n\nconst INV_255 = 1 / 255;\n\n/** One claim on a {@link ReusableBuffer}. */\ninterface BufferClaim {\n /** The buffer to write into, always `size` long. */\n readonly data: Float32Array;\n /** Whether {@link data} is the held buffer rather than a fresh allocation. */\n readonly reused: boolean;\n}\n\n/**\n * A `Float32Array` held across calls, handed out one claim at a time.\n *\n * Both pipelines want the same thing — allocate once, write into it every frame,\n * and fall back to a fresh array when a previous result has not been released\n * yet — so the bookkeeping lives here instead of twice.\n *\n * The part that is not obvious is {@link claim} re-allocating a buffer that is\n * *detached*. `ort.env.wasm.proxy` runs ONNX Runtime in a worker and posts the\n * input tensors with their `ArrayBuffer`s in the transfer list, which detaches\n * them on this side. A detached `Float32Array` is silently 0 long: writing to it\n * is a no-op, and the next `InferenceSession.run` rejects with\n * `Tensor's size(N) does not match data length(0)` on every other call — once for\n * the detached buffer, then the throw leaves the claim outstanding so the call\n * after it allocates and succeeds. Treating a buffer that changed length as spent\n * turns that into one extra allocation per transfer, which is what reuse was\n * avoiding, and keeps it correct for any consumer that transfers the tensor\n * rather than copying it.\n */\nclass ReusableBuffer {\n private readonly _size: number;\n private _buffer: Float32Array;\n private _inUse = false;\n\n /** @param size Length in floats of every buffer this hands out. */\n constructor(size: number) {\n this._size = size;\n this._buffer = new Float32Array(size);\n }\n\n /**\n * Take the held buffer, or a fresh one when it is unavailable.\n *\n * Unavailable means either still checked out by an unreleased claim, or\n * detached by whoever it was handed to. The first case allocates for this call\n * only; the second replaces the held buffer, so the allocation is paid once per\n * transfer rather than on every call after it.\n */\n claim(): BufferClaim {\n if (this._inUse) return { data: new Float32Array(this._size), reused: false };\n if (this._buffer.length !== this._size) this._buffer = new Float32Array(this._size);\n this._inUse = true;\n return { data: this._buffer, reused: true };\n }\n\n /** Mark the held buffer free for the next {@link claim}. */\n release(): void {\n this._inUse = false;\n }\n}\n\n/** Geometry of a letterbox, plus the planar tensor data it produced. */\nexport interface FusedLetterboxResult {\n /** CHW float32 in `[0, 1]`, length `3 * targetHeight * targetWidth`. */\n readonly data: Float32Array;\n /** Factor applied to the original image (`< 1` if downscaled). */\n readonly scale: number;\n /** Horizontal padding in pixels. */\n readonly padLeft: number;\n /** Vertical padding in pixels. */\n readonly padTop: number;\n /**\n * Whether {@link data} is the pipeline's reusable buffer.\n *\n * `true` means the next {@link LetterboxPipeline.run} overwrites it, so a\n * caller keeping the values past its own inference has to copy them.\n */\n readonly reused: boolean;\n}\n\n/**\n * Reusable letterbox → tensor pipeline for one target resolution.\n *\n * Holds a target canvas and an output buffer across calls, so a steady stream\n * of frames at the same size allocates nothing. Create one per task, not per\n * frame.\n */\nexport class LetterboxPipeline {\n private readonly _targetWidth: number;\n private readonly _targetHeight: number;\n private readonly _fill: readonly [number, number, number];\n private readonly _target: Canvas2D;\n private readonly _targetContext: Context2D;\n private readonly _buffer: ReusableBuffer;\n private _source: Canvas2D | null = null;\n private _sourceContext: Context2D | null = null;\n\n /**\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param fill RGB padding colour; defaults to YOLO grey.\n */\n constructor(\n targetWidth: number,\n targetHeight: number,\n fill: readonly [number, number, number] = [114, 114, 114],\n ) {\n if (targetWidth <= 0 || targetHeight <= 0) {\n throw new Error(`Invalid letterbox target ${targetWidth}x${targetHeight}.`);\n }\n this._targetWidth = targetWidth;\n this._targetHeight = targetHeight;\n this._fill = fill;\n this._target = createCanvas(targetWidth, targetHeight);\n this._targetContext = get2DContext(this._target, { willReadFrequently: true });\n this._targetContext.imageSmoothingEnabled = true;\n this._targetContext.imageSmoothingQuality = \"high\";\n this._buffer = new ReusableBuffer(3 * targetHeight * targetWidth);\n }\n\n /** The `[width, height]` this pipeline letterboxes into. */\n get targetSize(): readonly [number, number] {\n return [this._targetWidth, this._targetHeight];\n }\n\n /**\n * Letterbox an image and write it as planar float32.\n *\n * The returned buffer is reused between calls unless a previous result is\n * still checked out — {@link release} marks it free again. A second `run`\n * before the first is released allocates a fresh buffer rather than\n * corrupting it, so concurrent `predict()` calls on one task stay correct at\n * the cost of the allocation they were trying to avoid. A buffer that was\n * detached by a consumer that transferred it is replaced rather than written\n * into — see {@link ReusableBuffer}.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n */\n run(image: RGBImage): FusedLetterboxResult {\n const targetWidth = this._targetWidth;\n const targetHeight = this._targetHeight;\n const scale = Math.min(targetWidth / image.width, targetHeight / image.height);\n const scaledWidth = Math.round(image.width * scale);\n const scaledHeight = Math.round(image.height * scale);\n const padLeft = Math.floor((targetWidth - scaledWidth) / 2);\n const padTop = Math.floor((targetHeight - scaledHeight) / 2);\n\n const source = this._ensureSource(image.width, image.height);\n source.putImageData(rgbToImageData(image), 0, 0);\n\n const context = this._targetContext;\n if (\n padLeft > 0 ||\n padTop > 0 ||\n scaledWidth !== targetWidth ||\n scaledHeight !== targetHeight\n ) {\n context.fillStyle = `rgb(${this._fill[0]},${this._fill[1]},${this._fill[2]})`;\n context.fillRect(0, 0, targetWidth, targetHeight);\n }\n context.drawImage(\n this._source as CanvasImageSource,\n 0,\n 0,\n image.width,\n image.height,\n padLeft,\n padTop,\n scaledWidth,\n scaledHeight,\n );\n\n const rgba = context.getImageData(0, 0, targetWidth, targetHeight).data;\n const { data, reused } = this._buffer.claim();\n\n const plane = targetWidth * targetHeight;\n for (let pixel = 0, offset = 0; pixel < plane; pixel++, offset += 4) {\n data[pixel] = (rgba[offset] as number) * INV_255;\n data[plane + pixel] = (rgba[offset + 1] as number) * INV_255;\n data[2 * plane + pixel] = (rgba[offset + 2] as number) * INV_255;\n }\n\n return { data, scale, padLeft, padTop, reused };\n }\n\n /**\n * Mark the reusable buffer free again.\n *\n * Call it once the tensor built from a {@link run} result has been handed to\n * ONNX Runtime and the run has resolved — after that the values are inside\n * the WASM heap and the buffer can be overwritten.\n */\n release(): void {\n this._buffer.release();\n }\n\n /**\n * Grow the scratch source canvas to fit an image, reusing it when possible.\n *\n * A canvas is only reallocated when a frame arrives at a different size than\n * the last one, which for a camera or video source is never after the first.\n *\n * @param width Source width in pixels.\n * @param height Source height in pixels.\n */\n private _ensureSource(width: number, height: number): Context2D {\n if (\n this._source === null ||\n this._source.width !== width ||\n this._source.height !== height\n ) {\n this._source = createCanvas(width, height);\n this._sourceContext = get2DContext(this._source);\n }\n return this._sourceContext as Context2D;\n }\n}\n\n/**\n * Write an RGBA buffer as normalized planar float32.\n *\n * Shared by {@link ResizePipeline} and exported for a custom pipeline that\n * already holds pixels and wants the SDK's exact arithmetic.\n *\n * The expression is `(value / 255 - mean) / std`, evaluated in that order on\n * purpose: folding it into a single multiply-add (`value * k + b`) computes the\n * same quantity but rounds differently, and the tasks' output is asserted to be\n * bit-identical to the composable `normalize` → `toCHW` path.\n *\n * @param rgba Source pixels, 4 bytes per pixel, `width * height` long.\n * @param width Image width in pixels.\n * @param height Image height in pixels.\n * @param mean Per-channel RGB mean, already in `[0, 1]`.\n * @param std Per-channel RGB standard deviation.\n * @param out Destination buffer, `3 * width * height` long.\n * @param stride Bytes per source pixel: 4 for canvas RGBA, 3 for packed RGB.\n */\nexport function writePlanarFloat32(\n rgba: Uint8ClampedArray | Uint8Array,\n width: number,\n height: number,\n mean: readonly [number, number, number],\n std: readonly [number, number, number],\n out: Float32Array,\n stride: number = 4,\n): void {\n const plane = width * height;\n const m0 = mean[0];\n const m1 = mean[1];\n const m2 = mean[2];\n const s0 = std[0];\n const s1 = std[1];\n const s2 = std[2];\n for (let pixel = 0, offset = 0; pixel < plane; pixel++, offset += stride) {\n out[pixel] = ((rgba[offset] as number) * INV_255 - m0) / s0;\n out[plane + pixel] = ((rgba[offset + 1] as number) * INV_255 - m1) / s1;\n out[2 * plane + pixel] = ((rgba[offset + 2] as number) * INV_255 - m2) / s2;\n }\n}\n\n/** Planar tensor data produced by {@link ResizePipeline}. */\nexport interface FusedResizeResult {\n /** CHW float32, normalized, length `3 * targetHeight * targetWidth`. */\n readonly data: Float32Array;\n /**\n * Whether {@link data} is the pipeline's reusable buffer.\n *\n * `true` means the next {@link ResizePipeline.run} overwrites it, so a caller\n * keeping the values past its own inference has to copy them.\n */\n readonly reused: boolean;\n}\n\n/**\n * Reusable stretch-resize → normalized tensor pipeline for one target size.\n *\n * The classification counterpart of {@link LetterboxPipeline}. A classifier\n * stretches to the model's square input instead of letterboxing into it — no\n * padding, no scale to invert later, because nothing is mapped back onto the\n * source image afterwards. That difference is why it cannot simply reuse the\n * letterbox path.\n *\n * What it does share is the technique. The composable route\n * (`resize` → `normalize` → `toCHW`) allocates an `RGBImage` and two\n * `Float32Array`s and walks each end to end on every call: about 1.4 MB of\n * fresh garbage per 224×224 `predict()`, produced at the exact moment a phone\n * near its memory ceiling can least afford it. Here one `drawImage` resizes,\n * and one loop reads the resulting RGBA and writes normalized planar float32\n * into a buffer held across calls.\n *\n * Create one per task, not per frame.\n */\nexport class ResizePipeline {\n private readonly _targetWidth: number;\n private readonly _targetHeight: number;\n private readonly _mean: readonly [number, number, number];\n private readonly _std: readonly [number, number, number];\n private readonly _buffer: ReusableBuffer;\n private _target: Canvas2D | null = null;\n private _targetContext: Context2D | null = null;\n private _source: Canvas2D | null = null;\n private _sourceContext: Context2D | null = null;\n\n /**\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param mean Per-channel RGB mean in `[0, 1]`. Defaults to no shift.\n * @param std Per-channel RGB standard deviation. Defaults to no scaling.\n */\n constructor(\n targetWidth: number,\n targetHeight: number,\n mean: readonly [number, number, number] = [0, 0, 0],\n std: readonly [number, number, number] = [1, 1, 1],\n ) {\n if (targetWidth <= 0 || targetHeight <= 0) {\n throw new Error(`Invalid resize target ${targetWidth}x${targetHeight}.`);\n }\n this._targetWidth = targetWidth;\n this._targetHeight = targetHeight;\n this._mean = mean;\n this._std = std;\n this._buffer = new ReusableBuffer(3 * targetHeight * targetWidth);\n }\n\n /** The `[width, height]` this pipeline resizes into. */\n get targetSize(): readonly [number, number] {\n return [this._targetWidth, this._targetHeight];\n }\n\n /**\n * Resize an image to the target size and write it as normalized planar float32.\n *\n * An image that already arrives at the target size skips the canvas entirely\n * and is read straight out of its packed RGB — which is both faster and what\n * keeps the result identical to `resize()`, whose own fast path returns the\n * input untouched.\n *\n * Buffer reuse follows {@link ReusableBuffer}: held across calls, replaced when\n * a consumer detached it by transferring the tensor.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n */\n run(image: RGBImage): FusedResizeResult {\n const targetWidth = this._targetWidth;\n const targetHeight = this._targetHeight;\n const { data, reused } = this._buffer.claim();\n\n if (image.width === targetWidth && image.height === targetHeight) {\n writePlanarFloat32(\n image.data,\n targetWidth,\n targetHeight,\n this._mean,\n this._std,\n data,\n 3,\n );\n return { data, reused };\n }\n\n const source = this._ensureSource(image.width, image.height);\n source.putImageData(rgbToImageData(image), 0, 0);\n\n const context = this._ensureTarget();\n context.drawImage(this._source as CanvasImageSource, 0, 0, targetWidth, targetHeight);\n\n const rgba = context.getImageData(0, 0, targetWidth, targetHeight).data;\n writePlanarFloat32(rgba, targetWidth, targetHeight, this._mean, this._std, data);\n return { data, reused };\n }\n\n /**\n * Mark the reusable buffer free again.\n *\n * Call it once the tensor built from a {@link run} result has been handed to\n * ONNX Runtime and the run has resolved — after that the values are inside\n * the WASM heap and the buffer can be overwritten.\n */\n release(): void {\n this._buffer.release();\n }\n\n /**\n * Build the target canvas on first use.\n *\n * Lazily, so a pipeline constructed where no canvas implementation exists\n * (a Node test, a worker without OffscreenCanvas) only fails if it is asked\n * to resize something.\n */\n private _ensureTarget(): Context2D {\n if (this._target === null) {\n this._target = createCanvas(this._targetWidth, this._targetHeight);\n this._targetContext = get2DContext(this._target, { willReadFrequently: true });\n this._targetContext.imageSmoothingEnabled = true;\n this._targetContext.imageSmoothingQuality = \"high\";\n }\n return this._targetContext as Context2D;\n }\n\n /** Grow the scratch source canvas to fit an image, reusing it when possible. */\n private _ensureSource(width: number, height: number): Context2D {\n if (\n this._source === null ||\n this._source.width !== width ||\n this._source.height !== height\n ) {\n this._source = createCanvas(width, height);\n this._sourceContext = get2DContext(this._source);\n }\n return this._sourceContext as Context2D;\n }\n}\n\n/**\n * Resize an image into normalized planar float32 without keeping any state.\n *\n * The allocation-free path is {@link ResizePipeline}; this is the one-shot\n * form, for a caller who wants the fused behaviour without owning a pipeline.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param mean Per-channel RGB mean in `[0, 1]`. Defaults to no shift.\n * @param std Per-channel RGB standard deviation. Defaults to no scaling.\n */\nexport function resizeToTensorData(\n image: RGBImage,\n targetWidth: number,\n targetHeight: number,\n mean: readonly [number, number, number] = [0, 0, 0],\n std: readonly [number, number, number] = [1, 1, 1],\n): FusedResizeResult {\n return new ResizePipeline(targetWidth, targetHeight, mean, std).run(image);\n}\n\n/**\n * Build a zero-filled CHW tensor payload for a warm-up run.\n *\n * @param width Model input width in pixels.\n * @param height Model input height in pixels.\n */\nexport function zeroTensorData(width: number, height: number): Float32Array {\n return new Float32Array(3 * height * width);\n}\n\n/**\n * Letterbox an image into planar float32 without keeping any state.\n *\n * The allocation-free path is {@link LetterboxPipeline}; this is the one-shot\n * form, for a caller who wants the fused behaviour without owning a pipeline.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param fill RGB padding colour; defaults to YOLO grey.\n */\nexport function letterboxToTensorData(\n image: RGBImage,\n targetWidth: number,\n targetHeight: number,\n fill: readonly [number, number, number] = [114, 114, 114],\n): FusedLetterboxResult {\n return new LetterboxPipeline(targetWidth, targetHeight, fill).run(image);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pipeline.cjs","names":[],"sources":["../../../src/vision/preprocess/pipeline.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Fused letterbox → CHW float32 pipeline with reusable buffers.\n *\n * The composable primitives in {@link ./image.js} each allocate and each walk\n * their input end to end, which is the right shape for a library but the wrong\n * shape for a video loop. Chaining them costs eleven full-buffer passes and six\n * large allocations per frame:\n *\n * `getImageData` → RGBA→RGB → RGB→RGBA → `putImageData` → `drawImage` →\n * `getImageData` → RGBA→RGB → fill → row copies → `toFloat32` → `toCHW`.\n *\n * This module collapses the second half of that into two: one `drawImage` that\n * resizes *and* positions the content inside the padded target in a single\n * accelerated operation, and one loop that reads the resulting RGBA and writes\n * planar float32 directly. The intermediate `RGBImage` at target size, the fill\n * loop, the row copies and the two 4.9 MB `Float32Array` allocations all go\n * away.\n *\n * The primitives stay exactly as they are — they are public API and they are\n * what makes a custom pipeline writable. This is the fast path the built-in\n * tasks take.\n */\n\nimport {\n createCanvas,\n get2DContext,\n rgbToImageData,\n type Canvas2D,\n type Context2D,\n} from \"../core/canvas\";\nimport type { RGBImage } from \"../types\";\n\nconst INV_255 = 1 / 255;\n\n/** One claim on a {@link ReusableBuffer}. */\ninterface BufferClaim {\n /** The buffer to write into, always `size` long. */\n readonly data: Float32Array;\n /** Whether {@link data} is the held buffer rather than a fresh allocation. */\n readonly reused: boolean;\n}\n\n/**\n * A `Float32Array` held across calls, handed out one claim at a time.\n *\n * Both pipelines want the same thing — allocate once, write into it every frame,\n * and fall back to a fresh array when a previous result has not been released\n * yet — so the bookkeeping lives here instead of twice.\n *\n * The part that is not obvious is {@link claim} re-allocating a buffer that is\n * *detached*. `ort.env.wasm.proxy` runs ONNX Runtime in a worker and posts the\n * input tensors with their `ArrayBuffer`s in the transfer list, which detaches\n * them on this side. A detached `Float32Array` is silently 0 long: writing to it\n * is a no-op, and the next `InferenceSession.run` rejects with\n * `Tensor's size(N) does not match data length(0)` on every other call — once for\n * the detached buffer, then the throw leaves the claim outstanding so the call\n * after it allocates and succeeds. Treating a buffer that changed length as spent\n * turns that into one extra allocation per transfer, which is what reuse was\n * avoiding, and keeps it correct for any consumer that transfers the tensor\n * rather than copying it.\n */\nclass ReusableBuffer {\n private readonly _size: number;\n private _buffer: Float32Array;\n private _inUse = false;\n\n /** @param size Length in floats of every buffer this hands out. */\n constructor(size: number) {\n this._size = size;\n this._buffer = new Float32Array(size);\n }\n\n /**\n * Take the held buffer, or a fresh one when it is unavailable.\n *\n * Unavailable means either still checked out by an unreleased claim, or\n * detached by whoever it was handed to. The first case allocates for this call\n * only; the second replaces the held buffer, so the allocation is paid once per\n * transfer rather than on every call after it.\n */\n claim(): BufferClaim {\n if (this._inUse) return { data: new Float32Array(this._size), reused: false };\n if (this._buffer.length !== this._size) this._buffer = new Float32Array(this._size);\n this._inUse = true;\n return { data: this._buffer, reused: true };\n }\n\n /** Mark the held buffer free for the next {@link claim}. */\n release(): void {\n this._inUse = false;\n }\n}\n\n/** Geometry of a letterbox, plus the planar tensor data it produced. */\nexport interface FusedLetterboxResult {\n /** CHW float32 in `[0, 1]`, length `3 * targetHeight * targetWidth`. */\n readonly data: Float32Array;\n /** Factor applied to the original image (`< 1` if downscaled). */\n readonly scale: number;\n /** Horizontal padding in pixels. */\n readonly padLeft: number;\n /** Vertical padding in pixels. */\n readonly padTop: number;\n /**\n * Whether {@link data} is the pipeline's reusable buffer.\n *\n * `true` means the next {@link LetterboxPipeline.run} overwrites it, so a\n * caller keeping the values past its own inference has to copy them.\n */\n readonly reused: boolean;\n}\n\n/**\n * Reusable letterbox → tensor pipeline for one target resolution.\n *\n * Holds a target canvas and an output buffer across calls, so a steady stream\n * of frames at the same size allocates nothing. Create one per task, not per\n * frame.\n */\nexport class LetterboxPipeline {\n private readonly _targetWidth: number;\n private readonly _targetHeight: number;\n private readonly _fill: readonly [number, number, number];\n private readonly _target: Canvas2D;\n private readonly _targetContext: Context2D;\n private readonly _buffer: ReusableBuffer;\n private _source: Canvas2D | null = null;\n private _sourceContext: Context2D | null = null;\n\n /**\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param fill RGB padding colour; defaults to YOLO grey.\n */\n constructor(\n targetWidth: number,\n targetHeight: number,\n fill: readonly [number, number, number] = [114, 114, 114],\n ) {\n if (targetWidth <= 0 || targetHeight <= 0) {\n throw new Error(`Invalid letterbox target ${targetWidth}x${targetHeight}.`);\n }\n this._targetWidth = targetWidth;\n this._targetHeight = targetHeight;\n this._fill = fill;\n this._target = createCanvas(targetWidth, targetHeight);\n this._targetContext = get2DContext(this._target, { willReadFrequently: true });\n this._targetContext.imageSmoothingEnabled = true;\n this._targetContext.imageSmoothingQuality = \"high\";\n this._buffer = new ReusableBuffer(3 * targetHeight * targetWidth);\n }\n\n /** The `[width, height]` this pipeline letterboxes into. */\n get targetSize(): readonly [number, number] {\n return [this._targetWidth, this._targetHeight];\n }\n\n /**\n * Letterbox an image and write it as planar float32.\n *\n * The returned buffer is reused between calls unless a previous result is\n * still checked out — {@link release} marks it free again. A second `run`\n * before the first is released allocates a fresh buffer rather than\n * corrupting it, so concurrent `predict()` calls on one task stay correct at\n * the cost of the allocation they were trying to avoid. A buffer that was\n * detached by a consumer that transferred it is replaced rather than written\n * into — see {@link ReusableBuffer}.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n * @param canvas Optional drawable already holding exactly `image`'s pixels at\n * `image.width × image.height`, fully opaque. When given it is drawn\n * straight into the target, skipping the RGB→RGBA rebuild and the\n * `putImageData` onto the scratch canvas — two full-resolution passes per\n * frame. The tasks pass the canvas `loadImage` decoded onto; a caller with a\n * translucent source must not, since the result would composite its alpha.\n */\n run(image: RGBImage, canvas: CanvasImageSource | null = null): FusedLetterboxResult {\n const targetWidth = this._targetWidth;\n const targetHeight = this._targetHeight;\n const scale = Math.min(targetWidth / image.width, targetHeight / image.height);\n const scaledWidth = Math.round(image.width * scale);\n const scaledHeight = Math.round(image.height * scale);\n const padLeft = Math.floor((targetWidth - scaledWidth) / 2);\n const padTop = Math.floor((targetHeight - scaledHeight) / 2);\n\n const drawable = canvas ?? this._paintSource(image);\n\n const context = this._targetContext;\n if (\n padLeft > 0 ||\n padTop > 0 ||\n scaledWidth !== targetWidth ||\n scaledHeight !== targetHeight\n ) {\n context.fillStyle = `rgb(${this._fill[0]},${this._fill[1]},${this._fill[2]})`;\n context.fillRect(0, 0, targetWidth, targetHeight);\n }\n context.drawImage(\n drawable,\n 0,\n 0,\n image.width,\n image.height,\n padLeft,\n padTop,\n scaledWidth,\n scaledHeight,\n );\n\n const rgba = context.getImageData(0, 0, targetWidth, targetHeight).data;\n const { data, reused } = this._buffer.claim();\n\n const plane = targetWidth * targetHeight;\n for (let pixel = 0, offset = 0; pixel < plane; pixel++, offset += 4) {\n data[pixel] = (rgba[offset] as number) * INV_255;\n data[plane + pixel] = (rgba[offset + 1] as number) * INV_255;\n data[2 * plane + pixel] = (rgba[offset + 2] as number) * INV_255;\n }\n\n return { data, scale, padLeft, padTop, reused };\n }\n\n /**\n * Mark the reusable buffer free again.\n *\n * Call it once the tensor built from a {@link run} result has been handed to\n * ONNX Runtime and the run has resolved — after that the values are inside\n * the WASM heap and the buffer can be overwritten.\n *\n * Pass the result being released when several runs can be outstanding at\n * once. A result that got a fresh allocation (`reused: false`) holds nothing\n * of the pipeline's, so releasing it is a no-op; without that check, the\n * caller finishing with a fresh buffer would free the shared one while\n * another caller still had its frame in it. Called without an argument it\n * frees the shared buffer unconditionally, as before.\n *\n * @param result The {@link run} result being released.\n */\n release(result?: { readonly reused: boolean }): void {\n if (result !== undefined && !result.reused) return;\n this._buffer.release();\n }\n\n /**\n * Paint an image onto the scratch source canvas and return that canvas.\n *\n * The canvas is only reallocated when a frame arrives at a different size\n * than the last one, which for a camera or video source is never after the\n * first.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n */\n private _paintSource(image: RGBImage): CanvasImageSource {\n const { width, height } = image;\n if (\n this._source === null ||\n this._source.width !== width ||\n this._source.height !== height\n ) {\n this._source = createCanvas(width, height);\n this._sourceContext = get2DContext(this._source);\n }\n (this._sourceContext as Context2D).putImageData(rgbToImageData(image), 0, 0);\n return this._source as CanvasImageSource;\n }\n}\n\n/**\n * Write an RGBA buffer as normalized planar float32.\n *\n * Shared by {@link ResizePipeline} and exported for a custom pipeline that\n * already holds pixels and wants the SDK's exact arithmetic.\n *\n * The expression is `(value / 255 - mean) / std`, evaluated in that order on\n * purpose: folding it into a single multiply-add (`value * k + b`) computes the\n * same quantity but rounds differently, and the tasks' output is asserted to be\n * bit-identical to the composable `normalize` → `toCHW` path.\n *\n * @param rgba Source pixels, 4 bytes per pixel, `width * height` long.\n * @param width Image width in pixels.\n * @param height Image height in pixels.\n * @param mean Per-channel RGB mean, already in `[0, 1]`.\n * @param std Per-channel RGB standard deviation.\n * @param out Destination buffer, `3 * width * height` long.\n * @param stride Bytes per source pixel: 4 for canvas RGBA, 3 for packed RGB.\n */\nexport function writePlanarFloat32(\n rgba: Uint8ClampedArray | Uint8Array,\n width: number,\n height: number,\n mean: readonly [number, number, number],\n std: readonly [number, number, number],\n out: Float32Array,\n stride: number = 4,\n): void {\n const plane = width * height;\n const m0 = mean[0];\n const m1 = mean[1];\n const m2 = mean[2];\n const s0 = std[0];\n const s1 = std[1];\n const s2 = std[2];\n for (let pixel = 0, offset = 0; pixel < plane; pixel++, offset += stride) {\n out[pixel] = ((rgba[offset] as number) * INV_255 - m0) / s0;\n out[plane + pixel] = ((rgba[offset + 1] as number) * INV_255 - m1) / s1;\n out[2 * plane + pixel] = ((rgba[offset + 2] as number) * INV_255 - m2) / s2;\n }\n}\n\n/** Planar tensor data produced by {@link ResizePipeline}. */\nexport interface FusedResizeResult {\n /** CHW float32, normalized, length `3 * targetHeight * targetWidth`. */\n readonly data: Float32Array;\n /**\n * Whether {@link data} is the pipeline's reusable buffer.\n *\n * `true` means the next {@link ResizePipeline.run} overwrites it, so a caller\n * keeping the values past its own inference has to copy them.\n */\n readonly reused: boolean;\n}\n\n/**\n * Reusable stretch-resize → normalized tensor pipeline for one target size.\n *\n * The classification counterpart of {@link LetterboxPipeline}. A classifier\n * stretches to the model's square input instead of letterboxing into it — no\n * padding, no scale to invert later, because nothing is mapped back onto the\n * source image afterwards. That difference is why it cannot simply reuse the\n * letterbox path.\n *\n * What it does share is the technique. The composable route\n * (`resize` → `normalize` → `toCHW`) allocates an `RGBImage` and two\n * `Float32Array`s and walks each end to end on every call: about 1.4 MB of\n * fresh garbage per 224×224 `predict()`, produced at the exact moment a phone\n * near its memory ceiling can least afford it. Here one `drawImage` resizes,\n * and one loop reads the resulting RGBA and writes normalized planar float32\n * into a buffer held across calls.\n *\n * Create one per task, not per frame.\n */\nexport class ResizePipeline {\n private readonly _targetWidth: number;\n private readonly _targetHeight: number;\n private readonly _mean: readonly [number, number, number];\n private readonly _std: readonly [number, number, number];\n private readonly _buffer: ReusableBuffer;\n private _target: Canvas2D | null = null;\n private _targetContext: Context2D | null = null;\n private _source: Canvas2D | null = null;\n private _sourceContext: Context2D | null = null;\n\n /**\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param mean Per-channel RGB mean in `[0, 1]`. Defaults to no shift.\n * @param std Per-channel RGB standard deviation. Defaults to no scaling.\n */\n constructor(\n targetWidth: number,\n targetHeight: number,\n mean: readonly [number, number, number] = [0, 0, 0],\n std: readonly [number, number, number] = [1, 1, 1],\n ) {\n if (targetWidth <= 0 || targetHeight <= 0) {\n throw new Error(`Invalid resize target ${targetWidth}x${targetHeight}.`);\n }\n this._targetWidth = targetWidth;\n this._targetHeight = targetHeight;\n this._mean = mean;\n this._std = std;\n this._buffer = new ReusableBuffer(3 * targetHeight * targetWidth);\n }\n\n /** The `[width, height]` this pipeline resizes into. */\n get targetSize(): readonly [number, number] {\n return [this._targetWidth, this._targetHeight];\n }\n\n /**\n * Resize an image to the target size and write it as normalized planar float32.\n *\n * An image that already arrives at the target size skips the canvas entirely\n * and is read straight out of its packed RGB — which is both faster and what\n * keeps the result identical to `resize()`, whose own fast path returns the\n * input untouched.\n *\n * Buffer reuse follows {@link ReusableBuffer}: held across calls, replaced when\n * a consumer detached it by transferring the tensor.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n * @param canvas Optional opaque drawable holding exactly `image`'s pixels —\n * see {@link LetterboxPipeline.run}. Ignored on the same-size fast path,\n * which never touches a canvas.\n */\n run(image: RGBImage, canvas: CanvasImageSource | null = null): FusedResizeResult {\n const targetWidth = this._targetWidth;\n const targetHeight = this._targetHeight;\n const { data, reused } = this._buffer.claim();\n\n if (image.width === targetWidth && image.height === targetHeight) {\n writePlanarFloat32(\n image.data,\n targetWidth,\n targetHeight,\n this._mean,\n this._std,\n data,\n 3,\n );\n return { data, reused };\n }\n\n const drawable = canvas ?? this._paintSource(image);\n const context = this._ensureTarget();\n context.drawImage(\n drawable,\n 0,\n 0,\n image.width,\n image.height,\n 0,\n 0,\n targetWidth,\n targetHeight,\n );\n\n const rgba = context.getImageData(0, 0, targetWidth, targetHeight).data;\n writePlanarFloat32(rgba, targetWidth, targetHeight, this._mean, this._std, data);\n return { data, reused };\n }\n\n /**\n * Mark the reusable buffer free again.\n *\n * Call it once the tensor built from a {@link run} result has been handed to\n * ONNX Runtime and the run has resolved — after that the values are inside\n * the WASM heap and the buffer can be overwritten.\n *\n * Pass the result being released when several runs can be outstanding at\n * once. A result that got a fresh allocation (`reused: false`) holds nothing\n * of the pipeline's, so releasing it is a no-op; without that check, the\n * caller finishing with a fresh buffer would free the shared one while\n * another caller still had its frame in it. Called without an argument it\n * frees the shared buffer unconditionally, as before.\n *\n * @param result The {@link run} result being released.\n */\n release(result?: { readonly reused: boolean }): void {\n if (result !== undefined && !result.reused) return;\n this._buffer.release();\n }\n\n /**\n * Build the target canvas on first use.\n *\n * Lazily, so a pipeline constructed where no canvas implementation exists\n * (a Node test, a worker without OffscreenCanvas) only fails if it is asked\n * to resize something.\n */\n private _ensureTarget(): Context2D {\n if (this._target === null) {\n this._target = createCanvas(this._targetWidth, this._targetHeight);\n this._targetContext = get2DContext(this._target, { willReadFrequently: true });\n this._targetContext.imageSmoothingEnabled = true;\n this._targetContext.imageSmoothingQuality = \"high\";\n }\n return this._targetContext as Context2D;\n }\n\n /**\n * Paint an image onto the scratch source canvas and return that canvas.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n */\n private _paintSource(image: RGBImage): CanvasImageSource {\n const { width, height } = image;\n if (\n this._source === null ||\n this._source.width !== width ||\n this._source.height !== height\n ) {\n this._source = createCanvas(width, height);\n this._sourceContext = get2DContext(this._source);\n }\n (this._sourceContext as Context2D).putImageData(rgbToImageData(image), 0, 0);\n return this._source as CanvasImageSource;\n }\n}\n\n/**\n * Resize an image into normalized planar float32 without keeping any state.\n *\n * The allocation-free path is {@link ResizePipeline}; this is the one-shot\n * form, for a caller who wants the fused behaviour without owning a pipeline.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param mean Per-channel RGB mean in `[0, 1]`. Defaults to no shift.\n * @param std Per-channel RGB standard deviation. Defaults to no scaling.\n */\nexport function resizeToTensorData(\n image: RGBImage,\n targetWidth: number,\n targetHeight: number,\n mean: readonly [number, number, number] = [0, 0, 0],\n std: readonly [number, number, number] = [1, 1, 1],\n): FusedResizeResult {\n return new ResizePipeline(targetWidth, targetHeight, mean, std).run(image);\n}\n\n/**\n * Build a zero-filled CHW tensor payload for a warm-up run.\n *\n * @param width Model input width in pixels.\n * @param height Model input height in pixels.\n */\nexport function zeroTensorData(width: number, height: number): Float32Array {\n return new Float32Array(3 * height * width);\n}\n\n/**\n * Letterbox an image into planar float32 without keeping any state.\n *\n * The allocation-free path is {@link LetterboxPipeline}; this is the one-shot\n * form, for a caller who wants the fused behaviour without owning a pipeline.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param fill RGB padding colour; defaults to YOLO grey.\n */\nexport function letterboxToTensorData(\n image: RGBImage,\n targetWidth: number,\n targetHeight: number,\n fill: readonly [number, number, number] = [114, 114, 114],\n): FusedLetterboxResult {\n return new LetterboxPipeline(targetWidth, targetHeight, fill).run(image);\n}\n"],"mappings":"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"}
@@ -39,26 +39,25 @@ var r = 1 / 255, i = class {
39
39
  get targetSize() {
40
40
  return [this._targetWidth, this._targetHeight];
41
41
  }
42
- run(e) {
43
- let t = this._targetWidth, i = this._targetHeight, a = Math.min(t / e.width, i / e.height), o = Math.round(e.width * a), s = Math.round(e.height * a), c = Math.floor((t - o) / 2), l = Math.floor((i - s) / 2);
44
- this._ensureSource(e.width, e.height).putImageData(n(e), 0, 0);
45
- let u = this._targetContext;
46
- (c > 0 || l > 0 || o !== t || s !== i) && (u.fillStyle = `rgb(${this._fill[0]},${this._fill[1]},${this._fill[2]})`, u.fillRect(0, 0, t, i)), u.drawImage(this._source, 0, 0, e.width, e.height, c, l, o, s);
47
- let d = u.getImageData(0, 0, t, i).data, { data: f, reused: p } = this._buffer.claim(), m = t * i;
48
- for (let e = 0, t = 0; e < m; e++, t += 4) f[e] = d[t] * r, f[m + e] = d[t + 1] * r, f[2 * m + e] = d[t + 2] * r;
42
+ run(e, t = null) {
43
+ let n = this._targetWidth, i = this._targetHeight, a = Math.min(n / e.width, i / e.height), o = Math.round(e.width * a), s = Math.round(e.height * a), c = Math.floor((n - o) / 2), l = Math.floor((i - s) / 2), u = t ?? this._paintSource(e), d = this._targetContext;
44
+ (c > 0 || l > 0 || o !== n || s !== i) && (d.fillStyle = `rgb(${this._fill[0]},${this._fill[1]},${this._fill[2]})`, d.fillRect(0, 0, n, i)), d.drawImage(u, 0, 0, e.width, e.height, c, l, o, s);
45
+ let f = d.getImageData(0, 0, n, i).data, { data: p, reused: m } = this._buffer.claim(), h = n * i;
46
+ for (let e = 0, t = 0; e < h; e++, t += 4) p[e] = f[t] * r, p[h + e] = f[t + 1] * r, p[2 * h + e] = f[t + 2] * r;
49
47
  return {
50
- data: f,
48
+ data: p,
51
49
  scale: a,
52
50
  padLeft: c,
53
51
  padTop: l,
54
- reused: p
52
+ reused: m
55
53
  };
56
54
  }
57
- release() {
58
- this._buffer.release();
55
+ release(e) {
56
+ (e === void 0 || e.reused) && this._buffer.release();
59
57
  }
60
- _ensureSource(n, r) {
61
- return (this._source === null || this._source.width !== n || this._source.height !== r) && (this._source = e(n, r), this._sourceContext = t(this._source)), this._sourceContext;
58
+ _paintSource(r) {
59
+ let { width: i, height: a } = r;
60
+ return (this._source === null || this._source.width !== i || this._source.height !== a) && (this._source = e(i, a), this._sourceContext = t(this._source)), this._sourceContext.putImageData(n(r), 0, 0), this._source;
62
61
  }
63
62
  };
64
63
  function o(e, t, n, i, a, o, s = 4) {
@@ -90,29 +89,29 @@ var s = class {
90
89
  get targetSize() {
91
90
  return [this._targetWidth, this._targetHeight];
92
91
  }
93
- run(e) {
94
- let t = this._targetWidth, r = this._targetHeight, { data: i, reused: a } = this._buffer.claim();
95
- if (e.width === t && e.height === r) return o(e.data, t, r, this._mean, this._std, i, 3), {
92
+ run(e, t = null) {
93
+ let n = this._targetWidth, r = this._targetHeight, { data: i, reused: a } = this._buffer.claim();
94
+ if (e.width === n && e.height === r) return o(e.data, n, r, this._mean, this._std, i, 3), {
96
95
  data: i,
97
96
  reused: a
98
97
  };
99
- this._ensureSource(e.width, e.height).putImageData(n(e), 0, 0);
100
- let s = this._ensureTarget();
101
- s.drawImage(this._source, 0, 0, t, r);
102
- let c = s.getImageData(0, 0, t, r).data;
103
- return o(c, t, r, this._mean, this._std, i), {
98
+ let s = t ?? this._paintSource(e), c = this._ensureTarget();
99
+ c.drawImage(s, 0, 0, e.width, e.height, 0, 0, n, r);
100
+ let l = c.getImageData(0, 0, n, r).data;
101
+ return o(l, n, r, this._mean, this._std, i), {
104
102
  data: i,
105
103
  reused: a
106
104
  };
107
105
  }
108
- release() {
109
- this._buffer.release();
106
+ release(e) {
107
+ (e === void 0 || e.reused) && this._buffer.release();
110
108
  }
111
109
  _ensureTarget() {
112
110
  return this._target === null && (this._target = e(this._targetWidth, this._targetHeight), this._targetContext = t(this._target, { willReadFrequently: !0 }), this._targetContext.imageSmoothingEnabled = !0, this._targetContext.imageSmoothingQuality = "high"), this._targetContext;
113
111
  }
114
- _ensureSource(n, r) {
115
- return (this._source === null || this._source.width !== n || this._source.height !== r) && (this._source = e(n, r), this._sourceContext = t(this._source)), this._sourceContext;
112
+ _paintSource(r) {
113
+ let { width: i, height: a } = r;
114
+ return (this._source === null || this._source.width !== i || this._source.height !== a) && (this._source = e(i, a), this._sourceContext = t(this._source)), this._sourceContext.putImageData(n(r), 0, 0), this._source;
116
115
  }
117
116
  };
118
117
  function c(e, t, n, r = [
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.js","names":[],"sources":["../../../src/vision/preprocess/pipeline.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Fused letterbox → CHW float32 pipeline with reusable buffers.\n *\n * The composable primitives in {@link ./image.js} each allocate and each walk\n * their input end to end, which is the right shape for a library but the wrong\n * shape for a video loop. Chaining them costs eleven full-buffer passes and six\n * large allocations per frame:\n *\n * `getImageData` → RGBA→RGB → RGB→RGBA → `putImageData` → `drawImage` →\n * `getImageData` → RGBA→RGB → fill → row copies → `toFloat32` → `toCHW`.\n *\n * This module collapses the second half of that into two: one `drawImage` that\n * resizes *and* positions the content inside the padded target in a single\n * accelerated operation, and one loop that reads the resulting RGBA and writes\n * planar float32 directly. The intermediate `RGBImage` at target size, the fill\n * loop, the row copies and the two 4.9 MB `Float32Array` allocations all go\n * away.\n *\n * The primitives stay exactly as they are — they are public API and they are\n * what makes a custom pipeline writable. This is the fast path the built-in\n * tasks take.\n */\n\nimport {\n createCanvas,\n get2DContext,\n rgbToImageData,\n type Canvas2D,\n type Context2D,\n} from \"../core/canvas\";\nimport type { RGBImage } from \"../types\";\n\nconst INV_255 = 1 / 255;\n\n/** One claim on a {@link ReusableBuffer}. */\ninterface BufferClaim {\n /** The buffer to write into, always `size` long. */\n readonly data: Float32Array;\n /** Whether {@link data} is the held buffer rather than a fresh allocation. */\n readonly reused: boolean;\n}\n\n/**\n * A `Float32Array` held across calls, handed out one claim at a time.\n *\n * Both pipelines want the same thing — allocate once, write into it every frame,\n * and fall back to a fresh array when a previous result has not been released\n * yet — so the bookkeeping lives here instead of twice.\n *\n * The part that is not obvious is {@link claim} re-allocating a buffer that is\n * *detached*. `ort.env.wasm.proxy` runs ONNX Runtime in a worker and posts the\n * input tensors with their `ArrayBuffer`s in the transfer list, which detaches\n * them on this side. A detached `Float32Array` is silently 0 long: writing to it\n * is a no-op, and the next `InferenceSession.run` rejects with\n * `Tensor's size(N) does not match data length(0)` on every other call — once for\n * the detached buffer, then the throw leaves the claim outstanding so the call\n * after it allocates and succeeds. Treating a buffer that changed length as spent\n * turns that into one extra allocation per transfer, which is what reuse was\n * avoiding, and keeps it correct for any consumer that transfers the tensor\n * rather than copying it.\n */\nclass ReusableBuffer {\n private readonly _size: number;\n private _buffer: Float32Array;\n private _inUse = false;\n\n /** @param size Length in floats of every buffer this hands out. */\n constructor(size: number) {\n this._size = size;\n this._buffer = new Float32Array(size);\n }\n\n /**\n * Take the held buffer, or a fresh one when it is unavailable.\n *\n * Unavailable means either still checked out by an unreleased claim, or\n * detached by whoever it was handed to. The first case allocates for this call\n * only; the second replaces the held buffer, so the allocation is paid once per\n * transfer rather than on every call after it.\n */\n claim(): BufferClaim {\n if (this._inUse) return { data: new Float32Array(this._size), reused: false };\n if (this._buffer.length !== this._size) this._buffer = new Float32Array(this._size);\n this._inUse = true;\n return { data: this._buffer, reused: true };\n }\n\n /** Mark the held buffer free for the next {@link claim}. */\n release(): void {\n this._inUse = false;\n }\n}\n\n/** Geometry of a letterbox, plus the planar tensor data it produced. */\nexport interface FusedLetterboxResult {\n /** CHW float32 in `[0, 1]`, length `3 * targetHeight * targetWidth`. */\n readonly data: Float32Array;\n /** Factor applied to the original image (`< 1` if downscaled). */\n readonly scale: number;\n /** Horizontal padding in pixels. */\n readonly padLeft: number;\n /** Vertical padding in pixels. */\n readonly padTop: number;\n /**\n * Whether {@link data} is the pipeline's reusable buffer.\n *\n * `true` means the next {@link LetterboxPipeline.run} overwrites it, so a\n * caller keeping the values past its own inference has to copy them.\n */\n readonly reused: boolean;\n}\n\n/**\n * Reusable letterbox → tensor pipeline for one target resolution.\n *\n * Holds a target canvas and an output buffer across calls, so a steady stream\n * of frames at the same size allocates nothing. Create one per task, not per\n * frame.\n */\nexport class LetterboxPipeline {\n private readonly _targetWidth: number;\n private readonly _targetHeight: number;\n private readonly _fill: readonly [number, number, number];\n private readonly _target: Canvas2D;\n private readonly _targetContext: Context2D;\n private readonly _buffer: ReusableBuffer;\n private _source: Canvas2D | null = null;\n private _sourceContext: Context2D | null = null;\n\n /**\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param fill RGB padding colour; defaults to YOLO grey.\n */\n constructor(\n targetWidth: number,\n targetHeight: number,\n fill: readonly [number, number, number] = [114, 114, 114],\n ) {\n if (targetWidth <= 0 || targetHeight <= 0) {\n throw new Error(`Invalid letterbox target ${targetWidth}x${targetHeight}.`);\n }\n this._targetWidth = targetWidth;\n this._targetHeight = targetHeight;\n this._fill = fill;\n this._target = createCanvas(targetWidth, targetHeight);\n this._targetContext = get2DContext(this._target, { willReadFrequently: true });\n this._targetContext.imageSmoothingEnabled = true;\n this._targetContext.imageSmoothingQuality = \"high\";\n this._buffer = new ReusableBuffer(3 * targetHeight * targetWidth);\n }\n\n /** The `[width, height]` this pipeline letterboxes into. */\n get targetSize(): readonly [number, number] {\n return [this._targetWidth, this._targetHeight];\n }\n\n /**\n * Letterbox an image and write it as planar float32.\n *\n * The returned buffer is reused between calls unless a previous result is\n * still checked out — {@link release} marks it free again. A second `run`\n * before the first is released allocates a fresh buffer rather than\n * corrupting it, so concurrent `predict()` calls on one task stay correct at\n * the cost of the allocation they were trying to avoid. A buffer that was\n * detached by a consumer that transferred it is replaced rather than written\n * into — see {@link ReusableBuffer}.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n */\n run(image: RGBImage): FusedLetterboxResult {\n const targetWidth = this._targetWidth;\n const targetHeight = this._targetHeight;\n const scale = Math.min(targetWidth / image.width, targetHeight / image.height);\n const scaledWidth = Math.round(image.width * scale);\n const scaledHeight = Math.round(image.height * scale);\n const padLeft = Math.floor((targetWidth - scaledWidth) / 2);\n const padTop = Math.floor((targetHeight - scaledHeight) / 2);\n\n const source = this._ensureSource(image.width, image.height);\n source.putImageData(rgbToImageData(image), 0, 0);\n\n const context = this._targetContext;\n if (\n padLeft > 0 ||\n padTop > 0 ||\n scaledWidth !== targetWidth ||\n scaledHeight !== targetHeight\n ) {\n context.fillStyle = `rgb(${this._fill[0]},${this._fill[1]},${this._fill[2]})`;\n context.fillRect(0, 0, targetWidth, targetHeight);\n }\n context.drawImage(\n this._source as CanvasImageSource,\n 0,\n 0,\n image.width,\n image.height,\n padLeft,\n padTop,\n scaledWidth,\n scaledHeight,\n );\n\n const rgba = context.getImageData(0, 0, targetWidth, targetHeight).data;\n const { data, reused } = this._buffer.claim();\n\n const plane = targetWidth * targetHeight;\n for (let pixel = 0, offset = 0; pixel < plane; pixel++, offset += 4) {\n data[pixel] = (rgba[offset] as number) * INV_255;\n data[plane + pixel] = (rgba[offset + 1] as number) * INV_255;\n data[2 * plane + pixel] = (rgba[offset + 2] as number) * INV_255;\n }\n\n return { data, scale, padLeft, padTop, reused };\n }\n\n /**\n * Mark the reusable buffer free again.\n *\n * Call it once the tensor built from a {@link run} result has been handed to\n * ONNX Runtime and the run has resolved — after that the values are inside\n * the WASM heap and the buffer can be overwritten.\n */\n release(): void {\n this._buffer.release();\n }\n\n /**\n * Grow the scratch source canvas to fit an image, reusing it when possible.\n *\n * A canvas is only reallocated when a frame arrives at a different size than\n * the last one, which for a camera or video source is never after the first.\n *\n * @param width Source width in pixels.\n * @param height Source height in pixels.\n */\n private _ensureSource(width: number, height: number): Context2D {\n if (\n this._source === null ||\n this._source.width !== width ||\n this._source.height !== height\n ) {\n this._source = createCanvas(width, height);\n this._sourceContext = get2DContext(this._source);\n }\n return this._sourceContext as Context2D;\n }\n}\n\n/**\n * Write an RGBA buffer as normalized planar float32.\n *\n * Shared by {@link ResizePipeline} and exported for a custom pipeline that\n * already holds pixels and wants the SDK's exact arithmetic.\n *\n * The expression is `(value / 255 - mean) / std`, evaluated in that order on\n * purpose: folding it into a single multiply-add (`value * k + b`) computes the\n * same quantity but rounds differently, and the tasks' output is asserted to be\n * bit-identical to the composable `normalize` → `toCHW` path.\n *\n * @param rgba Source pixels, 4 bytes per pixel, `width * height` long.\n * @param width Image width in pixels.\n * @param height Image height in pixels.\n * @param mean Per-channel RGB mean, already in `[0, 1]`.\n * @param std Per-channel RGB standard deviation.\n * @param out Destination buffer, `3 * width * height` long.\n * @param stride Bytes per source pixel: 4 for canvas RGBA, 3 for packed RGB.\n */\nexport function writePlanarFloat32(\n rgba: Uint8ClampedArray | Uint8Array,\n width: number,\n height: number,\n mean: readonly [number, number, number],\n std: readonly [number, number, number],\n out: Float32Array,\n stride: number = 4,\n): void {\n const plane = width * height;\n const m0 = mean[0];\n const m1 = mean[1];\n const m2 = mean[2];\n const s0 = std[0];\n const s1 = std[1];\n const s2 = std[2];\n for (let pixel = 0, offset = 0; pixel < plane; pixel++, offset += stride) {\n out[pixel] = ((rgba[offset] as number) * INV_255 - m0) / s0;\n out[plane + pixel] = ((rgba[offset + 1] as number) * INV_255 - m1) / s1;\n out[2 * plane + pixel] = ((rgba[offset + 2] as number) * INV_255 - m2) / s2;\n }\n}\n\n/** Planar tensor data produced by {@link ResizePipeline}. */\nexport interface FusedResizeResult {\n /** CHW float32, normalized, length `3 * targetHeight * targetWidth`. */\n readonly data: Float32Array;\n /**\n * Whether {@link data} is the pipeline's reusable buffer.\n *\n * `true` means the next {@link ResizePipeline.run} overwrites it, so a caller\n * keeping the values past its own inference has to copy them.\n */\n readonly reused: boolean;\n}\n\n/**\n * Reusable stretch-resize → normalized tensor pipeline for one target size.\n *\n * The classification counterpart of {@link LetterboxPipeline}. A classifier\n * stretches to the model's square input instead of letterboxing into it — no\n * padding, no scale to invert later, because nothing is mapped back onto the\n * source image afterwards. That difference is why it cannot simply reuse the\n * letterbox path.\n *\n * What it does share is the technique. The composable route\n * (`resize` → `normalize` → `toCHW`) allocates an `RGBImage` and two\n * `Float32Array`s and walks each end to end on every call: about 1.4 MB of\n * fresh garbage per 224×224 `predict()`, produced at the exact moment a phone\n * near its memory ceiling can least afford it. Here one `drawImage` resizes,\n * and one loop reads the resulting RGBA and writes normalized planar float32\n * into a buffer held across calls.\n *\n * Create one per task, not per frame.\n */\nexport class ResizePipeline {\n private readonly _targetWidth: number;\n private readonly _targetHeight: number;\n private readonly _mean: readonly [number, number, number];\n private readonly _std: readonly [number, number, number];\n private readonly _buffer: ReusableBuffer;\n private _target: Canvas2D | null = null;\n private _targetContext: Context2D | null = null;\n private _source: Canvas2D | null = null;\n private _sourceContext: Context2D | null = null;\n\n /**\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param mean Per-channel RGB mean in `[0, 1]`. Defaults to no shift.\n * @param std Per-channel RGB standard deviation. Defaults to no scaling.\n */\n constructor(\n targetWidth: number,\n targetHeight: number,\n mean: readonly [number, number, number] = [0, 0, 0],\n std: readonly [number, number, number] = [1, 1, 1],\n ) {\n if (targetWidth <= 0 || targetHeight <= 0) {\n throw new Error(`Invalid resize target ${targetWidth}x${targetHeight}.`);\n }\n this._targetWidth = targetWidth;\n this._targetHeight = targetHeight;\n this._mean = mean;\n this._std = std;\n this._buffer = new ReusableBuffer(3 * targetHeight * targetWidth);\n }\n\n /** The `[width, height]` this pipeline resizes into. */\n get targetSize(): readonly [number, number] {\n return [this._targetWidth, this._targetHeight];\n }\n\n /**\n * Resize an image to the target size and write it as normalized planar float32.\n *\n * An image that already arrives at the target size skips the canvas entirely\n * and is read straight out of its packed RGB — which is both faster and what\n * keeps the result identical to `resize()`, whose own fast path returns the\n * input untouched.\n *\n * Buffer reuse follows {@link ReusableBuffer}: held across calls, replaced when\n * a consumer detached it by transferring the tensor.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n */\n run(image: RGBImage): FusedResizeResult {\n const targetWidth = this._targetWidth;\n const targetHeight = this._targetHeight;\n const { data, reused } = this._buffer.claim();\n\n if (image.width === targetWidth && image.height === targetHeight) {\n writePlanarFloat32(\n image.data,\n targetWidth,\n targetHeight,\n this._mean,\n this._std,\n data,\n 3,\n );\n return { data, reused };\n }\n\n const source = this._ensureSource(image.width, image.height);\n source.putImageData(rgbToImageData(image), 0, 0);\n\n const context = this._ensureTarget();\n context.drawImage(this._source as CanvasImageSource, 0, 0, targetWidth, targetHeight);\n\n const rgba = context.getImageData(0, 0, targetWidth, targetHeight).data;\n writePlanarFloat32(rgba, targetWidth, targetHeight, this._mean, this._std, data);\n return { data, reused };\n }\n\n /**\n * Mark the reusable buffer free again.\n *\n * Call it once the tensor built from a {@link run} result has been handed to\n * ONNX Runtime and the run has resolved — after that the values are inside\n * the WASM heap and the buffer can be overwritten.\n */\n release(): void {\n this._buffer.release();\n }\n\n /**\n * Build the target canvas on first use.\n *\n * Lazily, so a pipeline constructed where no canvas implementation exists\n * (a Node test, a worker without OffscreenCanvas) only fails if it is asked\n * to resize something.\n */\n private _ensureTarget(): Context2D {\n if (this._target === null) {\n this._target = createCanvas(this._targetWidth, this._targetHeight);\n this._targetContext = get2DContext(this._target, { willReadFrequently: true });\n this._targetContext.imageSmoothingEnabled = true;\n this._targetContext.imageSmoothingQuality = \"high\";\n }\n return this._targetContext as Context2D;\n }\n\n /** Grow the scratch source canvas to fit an image, reusing it when possible. */\n private _ensureSource(width: number, height: number): Context2D {\n if (\n this._source === null ||\n this._source.width !== width ||\n this._source.height !== height\n ) {\n this._source = createCanvas(width, height);\n this._sourceContext = get2DContext(this._source);\n }\n return this._sourceContext as Context2D;\n }\n}\n\n/**\n * Resize an image into normalized planar float32 without keeping any state.\n *\n * The allocation-free path is {@link ResizePipeline}; this is the one-shot\n * form, for a caller who wants the fused behaviour without owning a pipeline.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param mean Per-channel RGB mean in `[0, 1]`. Defaults to no shift.\n * @param std Per-channel RGB standard deviation. Defaults to no scaling.\n */\nexport function resizeToTensorData(\n image: RGBImage,\n targetWidth: number,\n targetHeight: number,\n mean: readonly [number, number, number] = [0, 0, 0],\n std: readonly [number, number, number] = [1, 1, 1],\n): FusedResizeResult {\n return new ResizePipeline(targetWidth, targetHeight, mean, std).run(image);\n}\n\n/**\n * Build a zero-filled CHW tensor payload for a warm-up run.\n *\n * @param width Model input width in pixels.\n * @param height Model input height in pixels.\n */\nexport function zeroTensorData(width: number, height: number): Float32Array {\n return new Float32Array(3 * height * width);\n}\n\n/**\n * Letterbox an image into planar float32 without keeping any state.\n *\n * The allocation-free path is {@link LetterboxPipeline}; this is the one-shot\n * form, for a caller who wants the fused behaviour without owning a pipeline.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param fill RGB padding colour; defaults to YOLO grey.\n */\nexport function letterboxToTensorData(\n image: RGBImage,\n targetWidth: number,\n targetHeight: number,\n fill: readonly [number, number, number] = [114, 114, 114],\n): FusedLetterboxResult {\n return new LetterboxPipeline(targetWidth, targetHeight, fill).run(image);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pipeline.js","names":[],"sources":["../../../src/vision/preprocess/pipeline.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Fused letterbox → CHW float32 pipeline with reusable buffers.\n *\n * The composable primitives in {@link ./image.js} each allocate and each walk\n * their input end to end, which is the right shape for a library but the wrong\n * shape for a video loop. Chaining them costs eleven full-buffer passes and six\n * large allocations per frame:\n *\n * `getImageData` → RGBA→RGB → RGB→RGBA → `putImageData` → `drawImage` →\n * `getImageData` → RGBA→RGB → fill → row copies → `toFloat32` → `toCHW`.\n *\n * This module collapses the second half of that into two: one `drawImage` that\n * resizes *and* positions the content inside the padded target in a single\n * accelerated operation, and one loop that reads the resulting RGBA and writes\n * planar float32 directly. The intermediate `RGBImage` at target size, the fill\n * loop, the row copies and the two 4.9 MB `Float32Array` allocations all go\n * away.\n *\n * The primitives stay exactly as they are — they are public API and they are\n * what makes a custom pipeline writable. This is the fast path the built-in\n * tasks take.\n */\n\nimport {\n createCanvas,\n get2DContext,\n rgbToImageData,\n type Canvas2D,\n type Context2D,\n} from \"../core/canvas\";\nimport type { RGBImage } from \"../types\";\n\nconst INV_255 = 1 / 255;\n\n/** One claim on a {@link ReusableBuffer}. */\ninterface BufferClaim {\n /** The buffer to write into, always `size` long. */\n readonly data: Float32Array;\n /** Whether {@link data} is the held buffer rather than a fresh allocation. */\n readonly reused: boolean;\n}\n\n/**\n * A `Float32Array` held across calls, handed out one claim at a time.\n *\n * Both pipelines want the same thing — allocate once, write into it every frame,\n * and fall back to a fresh array when a previous result has not been released\n * yet — so the bookkeeping lives here instead of twice.\n *\n * The part that is not obvious is {@link claim} re-allocating a buffer that is\n * *detached*. `ort.env.wasm.proxy` runs ONNX Runtime in a worker and posts the\n * input tensors with their `ArrayBuffer`s in the transfer list, which detaches\n * them on this side. A detached `Float32Array` is silently 0 long: writing to it\n * is a no-op, and the next `InferenceSession.run` rejects with\n * `Tensor's size(N) does not match data length(0)` on every other call — once for\n * the detached buffer, then the throw leaves the claim outstanding so the call\n * after it allocates and succeeds. Treating a buffer that changed length as spent\n * turns that into one extra allocation per transfer, which is what reuse was\n * avoiding, and keeps it correct for any consumer that transfers the tensor\n * rather than copying it.\n */\nclass ReusableBuffer {\n private readonly _size: number;\n private _buffer: Float32Array;\n private _inUse = false;\n\n /** @param size Length in floats of every buffer this hands out. */\n constructor(size: number) {\n this._size = size;\n this._buffer = new Float32Array(size);\n }\n\n /**\n * Take the held buffer, or a fresh one when it is unavailable.\n *\n * Unavailable means either still checked out by an unreleased claim, or\n * detached by whoever it was handed to. The first case allocates for this call\n * only; the second replaces the held buffer, so the allocation is paid once per\n * transfer rather than on every call after it.\n */\n claim(): BufferClaim {\n if (this._inUse) return { data: new Float32Array(this._size), reused: false };\n if (this._buffer.length !== this._size) this._buffer = new Float32Array(this._size);\n this._inUse = true;\n return { data: this._buffer, reused: true };\n }\n\n /** Mark the held buffer free for the next {@link claim}. */\n release(): void {\n this._inUse = false;\n }\n}\n\n/** Geometry of a letterbox, plus the planar tensor data it produced. */\nexport interface FusedLetterboxResult {\n /** CHW float32 in `[0, 1]`, length `3 * targetHeight * targetWidth`. */\n readonly data: Float32Array;\n /** Factor applied to the original image (`< 1` if downscaled). */\n readonly scale: number;\n /** Horizontal padding in pixels. */\n readonly padLeft: number;\n /** Vertical padding in pixels. */\n readonly padTop: number;\n /**\n * Whether {@link data} is the pipeline's reusable buffer.\n *\n * `true` means the next {@link LetterboxPipeline.run} overwrites it, so a\n * caller keeping the values past its own inference has to copy them.\n */\n readonly reused: boolean;\n}\n\n/**\n * Reusable letterbox → tensor pipeline for one target resolution.\n *\n * Holds a target canvas and an output buffer across calls, so a steady stream\n * of frames at the same size allocates nothing. Create one per task, not per\n * frame.\n */\nexport class LetterboxPipeline {\n private readonly _targetWidth: number;\n private readonly _targetHeight: number;\n private readonly _fill: readonly [number, number, number];\n private readonly _target: Canvas2D;\n private readonly _targetContext: Context2D;\n private readonly _buffer: ReusableBuffer;\n private _source: Canvas2D | null = null;\n private _sourceContext: Context2D | null = null;\n\n /**\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param fill RGB padding colour; defaults to YOLO grey.\n */\n constructor(\n targetWidth: number,\n targetHeight: number,\n fill: readonly [number, number, number] = [114, 114, 114],\n ) {\n if (targetWidth <= 0 || targetHeight <= 0) {\n throw new Error(`Invalid letterbox target ${targetWidth}x${targetHeight}.`);\n }\n this._targetWidth = targetWidth;\n this._targetHeight = targetHeight;\n this._fill = fill;\n this._target = createCanvas(targetWidth, targetHeight);\n this._targetContext = get2DContext(this._target, { willReadFrequently: true });\n this._targetContext.imageSmoothingEnabled = true;\n this._targetContext.imageSmoothingQuality = \"high\";\n this._buffer = new ReusableBuffer(3 * targetHeight * targetWidth);\n }\n\n /** The `[width, height]` this pipeline letterboxes into. */\n get targetSize(): readonly [number, number] {\n return [this._targetWidth, this._targetHeight];\n }\n\n /**\n * Letterbox an image and write it as planar float32.\n *\n * The returned buffer is reused between calls unless a previous result is\n * still checked out — {@link release} marks it free again. A second `run`\n * before the first is released allocates a fresh buffer rather than\n * corrupting it, so concurrent `predict()` calls on one task stay correct at\n * the cost of the allocation they were trying to avoid. A buffer that was\n * detached by a consumer that transferred it is replaced rather than written\n * into — see {@link ReusableBuffer}.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n * @param canvas Optional drawable already holding exactly `image`'s pixels at\n * `image.width × image.height`, fully opaque. When given it is drawn\n * straight into the target, skipping the RGB→RGBA rebuild and the\n * `putImageData` onto the scratch canvas — two full-resolution passes per\n * frame. The tasks pass the canvas `loadImage` decoded onto; a caller with a\n * translucent source must not, since the result would composite its alpha.\n */\n run(image: RGBImage, canvas: CanvasImageSource | null = null): FusedLetterboxResult {\n const targetWidth = this._targetWidth;\n const targetHeight = this._targetHeight;\n const scale = Math.min(targetWidth / image.width, targetHeight / image.height);\n const scaledWidth = Math.round(image.width * scale);\n const scaledHeight = Math.round(image.height * scale);\n const padLeft = Math.floor((targetWidth - scaledWidth) / 2);\n const padTop = Math.floor((targetHeight - scaledHeight) / 2);\n\n const drawable = canvas ?? this._paintSource(image);\n\n const context = this._targetContext;\n if (\n padLeft > 0 ||\n padTop > 0 ||\n scaledWidth !== targetWidth ||\n scaledHeight !== targetHeight\n ) {\n context.fillStyle = `rgb(${this._fill[0]},${this._fill[1]},${this._fill[2]})`;\n context.fillRect(0, 0, targetWidth, targetHeight);\n }\n context.drawImage(\n drawable,\n 0,\n 0,\n image.width,\n image.height,\n padLeft,\n padTop,\n scaledWidth,\n scaledHeight,\n );\n\n const rgba = context.getImageData(0, 0, targetWidth, targetHeight).data;\n const { data, reused } = this._buffer.claim();\n\n const plane = targetWidth * targetHeight;\n for (let pixel = 0, offset = 0; pixel < plane; pixel++, offset += 4) {\n data[pixel] = (rgba[offset] as number) * INV_255;\n data[plane + pixel] = (rgba[offset + 1] as number) * INV_255;\n data[2 * plane + pixel] = (rgba[offset + 2] as number) * INV_255;\n }\n\n return { data, scale, padLeft, padTop, reused };\n }\n\n /**\n * Mark the reusable buffer free again.\n *\n * Call it once the tensor built from a {@link run} result has been handed to\n * ONNX Runtime and the run has resolved — after that the values are inside\n * the WASM heap and the buffer can be overwritten.\n *\n * Pass the result being released when several runs can be outstanding at\n * once. A result that got a fresh allocation (`reused: false`) holds nothing\n * of the pipeline's, so releasing it is a no-op; without that check, the\n * caller finishing with a fresh buffer would free the shared one while\n * another caller still had its frame in it. Called without an argument it\n * frees the shared buffer unconditionally, as before.\n *\n * @param result The {@link run} result being released.\n */\n release(result?: { readonly reused: boolean }): void {\n if (result !== undefined && !result.reused) return;\n this._buffer.release();\n }\n\n /**\n * Paint an image onto the scratch source canvas and return that canvas.\n *\n * The canvas is only reallocated when a frame arrives at a different size\n * than the last one, which for a camera or video source is never after the\n * first.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n */\n private _paintSource(image: RGBImage): CanvasImageSource {\n const { width, height } = image;\n if (\n this._source === null ||\n this._source.width !== width ||\n this._source.height !== height\n ) {\n this._source = createCanvas(width, height);\n this._sourceContext = get2DContext(this._source);\n }\n (this._sourceContext as Context2D).putImageData(rgbToImageData(image), 0, 0);\n return this._source as CanvasImageSource;\n }\n}\n\n/**\n * Write an RGBA buffer as normalized planar float32.\n *\n * Shared by {@link ResizePipeline} and exported for a custom pipeline that\n * already holds pixels and wants the SDK's exact arithmetic.\n *\n * The expression is `(value / 255 - mean) / std`, evaluated in that order on\n * purpose: folding it into a single multiply-add (`value * k + b`) computes the\n * same quantity but rounds differently, and the tasks' output is asserted to be\n * bit-identical to the composable `normalize` → `toCHW` path.\n *\n * @param rgba Source pixels, 4 bytes per pixel, `width * height` long.\n * @param width Image width in pixels.\n * @param height Image height in pixels.\n * @param mean Per-channel RGB mean, already in `[0, 1]`.\n * @param std Per-channel RGB standard deviation.\n * @param out Destination buffer, `3 * width * height` long.\n * @param stride Bytes per source pixel: 4 for canvas RGBA, 3 for packed RGB.\n */\nexport function writePlanarFloat32(\n rgba: Uint8ClampedArray | Uint8Array,\n width: number,\n height: number,\n mean: readonly [number, number, number],\n std: readonly [number, number, number],\n out: Float32Array,\n stride: number = 4,\n): void {\n const plane = width * height;\n const m0 = mean[0];\n const m1 = mean[1];\n const m2 = mean[2];\n const s0 = std[0];\n const s1 = std[1];\n const s2 = std[2];\n for (let pixel = 0, offset = 0; pixel < plane; pixel++, offset += stride) {\n out[pixel] = ((rgba[offset] as number) * INV_255 - m0) / s0;\n out[plane + pixel] = ((rgba[offset + 1] as number) * INV_255 - m1) / s1;\n out[2 * plane + pixel] = ((rgba[offset + 2] as number) * INV_255 - m2) / s2;\n }\n}\n\n/** Planar tensor data produced by {@link ResizePipeline}. */\nexport interface FusedResizeResult {\n /** CHW float32, normalized, length `3 * targetHeight * targetWidth`. */\n readonly data: Float32Array;\n /**\n * Whether {@link data} is the pipeline's reusable buffer.\n *\n * `true` means the next {@link ResizePipeline.run} overwrites it, so a caller\n * keeping the values past its own inference has to copy them.\n */\n readonly reused: boolean;\n}\n\n/**\n * Reusable stretch-resize → normalized tensor pipeline for one target size.\n *\n * The classification counterpart of {@link LetterboxPipeline}. A classifier\n * stretches to the model's square input instead of letterboxing into it — no\n * padding, no scale to invert later, because nothing is mapped back onto the\n * source image afterwards. That difference is why it cannot simply reuse the\n * letterbox path.\n *\n * What it does share is the technique. The composable route\n * (`resize` → `normalize` → `toCHW`) allocates an `RGBImage` and two\n * `Float32Array`s and walks each end to end on every call: about 1.4 MB of\n * fresh garbage per 224×224 `predict()`, produced at the exact moment a phone\n * near its memory ceiling can least afford it. Here one `drawImage` resizes,\n * and one loop reads the resulting RGBA and writes normalized planar float32\n * into a buffer held across calls.\n *\n * Create one per task, not per frame.\n */\nexport class ResizePipeline {\n private readonly _targetWidth: number;\n private readonly _targetHeight: number;\n private readonly _mean: readonly [number, number, number];\n private readonly _std: readonly [number, number, number];\n private readonly _buffer: ReusableBuffer;\n private _target: Canvas2D | null = null;\n private _targetContext: Context2D | null = null;\n private _source: Canvas2D | null = null;\n private _sourceContext: Context2D | null = null;\n\n /**\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param mean Per-channel RGB mean in `[0, 1]`. Defaults to no shift.\n * @param std Per-channel RGB standard deviation. Defaults to no scaling.\n */\n constructor(\n targetWidth: number,\n targetHeight: number,\n mean: readonly [number, number, number] = [0, 0, 0],\n std: readonly [number, number, number] = [1, 1, 1],\n ) {\n if (targetWidth <= 0 || targetHeight <= 0) {\n throw new Error(`Invalid resize target ${targetWidth}x${targetHeight}.`);\n }\n this._targetWidth = targetWidth;\n this._targetHeight = targetHeight;\n this._mean = mean;\n this._std = std;\n this._buffer = new ReusableBuffer(3 * targetHeight * targetWidth);\n }\n\n /** The `[width, height]` this pipeline resizes into. */\n get targetSize(): readonly [number, number] {\n return [this._targetWidth, this._targetHeight];\n }\n\n /**\n * Resize an image to the target size and write it as normalized planar float32.\n *\n * An image that already arrives at the target size skips the canvas entirely\n * and is read straight out of its packed RGB — which is both faster and what\n * keeps the result identical to `resize()`, whose own fast path returns the\n * input untouched.\n *\n * Buffer reuse follows {@link ReusableBuffer}: held across calls, replaced when\n * a consumer detached it by transferring the tensor.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n * @param canvas Optional opaque drawable holding exactly `image`'s pixels —\n * see {@link LetterboxPipeline.run}. Ignored on the same-size fast path,\n * which never touches a canvas.\n */\n run(image: RGBImage, canvas: CanvasImageSource | null = null): FusedResizeResult {\n const targetWidth = this._targetWidth;\n const targetHeight = this._targetHeight;\n const { data, reused } = this._buffer.claim();\n\n if (image.width === targetWidth && image.height === targetHeight) {\n writePlanarFloat32(\n image.data,\n targetWidth,\n targetHeight,\n this._mean,\n this._std,\n data,\n 3,\n );\n return { data, reused };\n }\n\n const drawable = canvas ?? this._paintSource(image);\n const context = this._ensureTarget();\n context.drawImage(\n drawable,\n 0,\n 0,\n image.width,\n image.height,\n 0,\n 0,\n targetWidth,\n targetHeight,\n );\n\n const rgba = context.getImageData(0, 0, targetWidth, targetHeight).data;\n writePlanarFloat32(rgba, targetWidth, targetHeight, this._mean, this._std, data);\n return { data, reused };\n }\n\n /**\n * Mark the reusable buffer free again.\n *\n * Call it once the tensor built from a {@link run} result has been handed to\n * ONNX Runtime and the run has resolved — after that the values are inside\n * the WASM heap and the buffer can be overwritten.\n *\n * Pass the result being released when several runs can be outstanding at\n * once. A result that got a fresh allocation (`reused: false`) holds nothing\n * of the pipeline's, so releasing it is a no-op; without that check, the\n * caller finishing with a fresh buffer would free the shared one while\n * another caller still had its frame in it. Called without an argument it\n * frees the shared buffer unconditionally, as before.\n *\n * @param result The {@link run} result being released.\n */\n release(result?: { readonly reused: boolean }): void {\n if (result !== undefined && !result.reused) return;\n this._buffer.release();\n }\n\n /**\n * Build the target canvas on first use.\n *\n * Lazily, so a pipeline constructed where no canvas implementation exists\n * (a Node test, a worker without OffscreenCanvas) only fails if it is asked\n * to resize something.\n */\n private _ensureTarget(): Context2D {\n if (this._target === null) {\n this._target = createCanvas(this._targetWidth, this._targetHeight);\n this._targetContext = get2DContext(this._target, { willReadFrequently: true });\n this._targetContext.imageSmoothingEnabled = true;\n this._targetContext.imageSmoothingQuality = \"high\";\n }\n return this._targetContext as Context2D;\n }\n\n /**\n * Paint an image onto the scratch source canvas and return that canvas.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n */\n private _paintSource(image: RGBImage): CanvasImageSource {\n const { width, height } = image;\n if (\n this._source === null ||\n this._source.width !== width ||\n this._source.height !== height\n ) {\n this._source = createCanvas(width, height);\n this._sourceContext = get2DContext(this._source);\n }\n (this._sourceContext as Context2D).putImageData(rgbToImageData(image), 0, 0);\n return this._source as CanvasImageSource;\n }\n}\n\n/**\n * Resize an image into normalized planar float32 without keeping any state.\n *\n * The allocation-free path is {@link ResizePipeline}; this is the one-shot\n * form, for a caller who wants the fused behaviour without owning a pipeline.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param mean Per-channel RGB mean in `[0, 1]`. Defaults to no shift.\n * @param std Per-channel RGB standard deviation. Defaults to no scaling.\n */\nexport function resizeToTensorData(\n image: RGBImage,\n targetWidth: number,\n targetHeight: number,\n mean: readonly [number, number, number] = [0, 0, 0],\n std: readonly [number, number, number] = [1, 1, 1],\n): FusedResizeResult {\n return new ResizePipeline(targetWidth, targetHeight, mean, std).run(image);\n}\n\n/**\n * Build a zero-filled CHW tensor payload for a warm-up run.\n *\n * @param width Model input width in pixels.\n * @param height Model input height in pixels.\n */\nexport function zeroTensorData(width: number, height: number): Float32Array {\n return new Float32Array(3 * height * width);\n}\n\n/**\n * Letterbox an image into planar float32 without keeping any state.\n *\n * The allocation-free path is {@link LetterboxPipeline}; this is the one-shot\n * form, for a caller who wants the fused behaviour without owning a pipeline.\n *\n * @param image Source image in the SDK's canonical HWC RGB layout.\n * @param targetWidth Model input width in pixels.\n * @param targetHeight Model input height in pixels.\n * @param fill RGB padding colour; defaults to YOLO grey.\n */\nexport function letterboxToTensorData(\n image: RGBImage,\n targetWidth: number,\n targetHeight: number,\n fill: readonly [number, number, number] = [114, 114, 114],\n): FusedLetterboxResult {\n return new LetterboxPipeline(targetWidth, targetHeight, fill).run(image);\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../core/exceptions.cjs");var t=class{_session;constructor(e){this._session=e}get session(){return this._session}};function n(e){return e.toFixed(6).replace(/0+$/,``).replace(/\.$/,``)}function r(t,r){if(!r.raiseOnEmpty||t>0)return;let i=r.path?` in ${r.path}`:``,a=r.classes===void 0?``:` among classes [${[...r.classes].sort((e,t)=>e-t).join(`, `)}]`;throw new e.NoDetectionsError(`No detections${i}${a}: nothing cleared confThreshold=${n(r.confThreshold)}.`)}exports.VisionTask=t,exports.requireDetections=r;
1
+ const e=require("../core/exceptions.cjs"),t=require("../types.cjs");var n=class{_session;constructor(e){this._session=e}get session(){return this._session}};function r(e){return e.toFixed(6).replace(/0+$/,``).replace(/\.$/,``)}function i(t,n){if(!n.raiseOnEmpty||t>0)return;let i=n.path?` in ${n.path}`:``,a=n.classes===void 0?``:` among classes [${[...n.classes].sort((e,t)=>e-t).join(`, `)}]`;throw new e.NoDetectionsError(`No detections${i}${a}: nothing cleared confThreshold=${r(n.confThreshold)}.`)}function a(e,n){let[r,i,a,o]=n.asIntXyxy(),s=Math.max(0,r),c=Math.max(0,i),l=Math.min(e.width,a),u=Math.min(e.height,o);if(l<=s||u<=c)return new t.RGBImage(new Uint8Array,0,0);let d=l-s,f=u-c,p=new Uint8Array(d*f*3);for(let t=0;t<f;t++){let n=((c+t)*e.width+s)*3;p.set(e.data.subarray(n,n+d*3),t*d*3)}return new t.RGBImage(p,d,f)}function o(e){let t=!1,n;return()=>(t||=(n=e(),!0),n)}exports.VisionTask=n,exports.cropToBox=a,exports.memoize=o,exports.requireDetections=i;
2
2
  //# sourceMappingURL=base.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"base.cjs","names":[],"sources":["../../../src/vision/tasks/base.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Common foundation for task-oriented vision SDK objects.\n *\n * The base only owns the {@link OrtSession} — label resolution lives in each\n * subclass because how `numClasses` is read from the model differs per task.\n */\n\nimport { NoDetectionsError } from \"../core/exceptions\";\nimport type { OrtSession } from \"../core/session\";\n\nexport abstract class VisionTask {\n protected constructor(protected readonly _session: OrtSession) {}\n\n /** The underlying {@link OrtSession} used to run inference. */\n get session(): OrtSession {\n return this._session;\n }\n}\n\n/**\n * Render a confidence threshold the way the Python SDK renders it.\n *\n * JavaScript and Python disagree on how a number becomes text. A whole\n * threshold is `1` here and `1.0` there; JavaScript holds off on exponent\n * notation until `1e-7` while Python switches at `1e-5`. A fused pipeline is\n * built once and runs under both runtimes from the same file, so a message that\n * quotes the threshold has to quote it identically — otherwise the two SDKs\n * describe the same run with two different numbers.\n *\n * Six decimals with the trailing zeros trimmed covers every threshold a caller\n * can meaningfully set and agrees byte for byte with `_format_threshold` in the\n * Python SDK's `tasks/base.py`. The pairing is fixed by a shared table in both\n * test suites, so a change on one side that is not mirrored on the other fails.\n *\n * @param value The threshold to render.\n * @returns The threshold as text, without trailing zeros or a trailing dot.\n */\nfunction formatThreshold(value: number): string {\n return value.toFixed(6).replace(/0+$/, \"\").replace(/\\.$/, \"\");\n}\n\n/**\n * Turn an empty result into an error, when the caller asked for that.\n *\n * Shared by every task that can come back with nothing — {@link Detector},\n * {@link Segmenter} and {@link DetectClassify} — so the three agree on when\n * they throw and on what the message says. The message names the two settings\n * that decide the outcome, because \"no detections\" on its own leaves the reader\n * unable to tell a blank image from a threshold set too high.\n *\n * @param count How many detections survived every filter.\n * @param options The flag for this call, the threshold actually applied (after\n * any per-call override), the class allowlist if one narrowed the search, and\n * the source path when the input was one.\n * @throws {@link NoDetectionsError} when the flag is set and `count` is zero.\n */\nexport function requireDetections(\n count: number,\n options: {\n readonly raiseOnEmpty: boolean;\n readonly confThreshold: number;\n readonly classes: readonly number[] | undefined;\n readonly path: string | null;\n },\n): void {\n if (!options.raiseOnEmpty || count > 0) return;\n const where = options.path ? ` in ${options.path}` : \"\";\n const narrowed =\n options.classes === undefined\n ? \"\"\n : ` among classes [${[...options.classes].sort((a, b) => a - b).join(\", \")}]`;\n throw new NoDetectionsError(\n `No detections${where}${narrowed}: nothing cleared confThreshold=${formatThreshold(options.confThreshold)}.`,\n );\n}\n"],"mappings":"0CAWA,IAAsB,EAAtB,KAAiC,CACY,SAAzC,YAAsB,EAAyC,CAAtB,KAAA,SAAA,CAAuB,CAGhE,IAAI,SAAsB,CACtB,OAAO,KAAK,QAChB,CACJ,EAoBA,SAAS,EAAgB,EAAuB,CAC5C,OAAO,EAAM,QAAQ,CAAC,CAAC,CAAC,QAAQ,MAAO,EAAE,CAAC,CAAC,QAAQ,MAAO,EAAE,CAChE,CAiBA,SAAgB,EACZ,EACA,EAMI,CACJ,GAAI,CAAC,EAAQ,cAAgB,EAAQ,EAAG,OACxC,IAAM,EAAQ,EAAQ,KAAO,OAAO,EAAQ,OAAS,GAC/C,EACF,EAAQ,UAAY,IAAA,GACd,GACA,mBAAmB,CAAC,GAAG,EAAQ,OAAO,CAAC,CAAC,MAAM,EAAG,IAAM,EAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,GACnF,MAAM,IAAI,EAAA,kBACN,gBAAgB,IAAQ,EAAS,kCAAkC,EAAgB,EAAQ,aAAa,EAAE,EAC9G,CACJ"}
1
+ {"version":3,"file":"base.cjs","names":[],"sources":["../../../src/vision/tasks/base.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Common foundation for task-oriented vision SDK objects.\n *\n * The base only owns the {@link OrtSession} — label resolution lives in each\n * subclass because how `numClasses` is read from the model differs per task.\n */\n\nimport { NoDetectionsError } from \"../core/exceptions\";\nimport type { OrtSession } from \"../core/session\";\nimport { type BoundingBox, RGBImage } from \"../types\";\n\nexport abstract class VisionTask {\n protected constructor(protected readonly _session: OrtSession) {}\n\n /** The underlying {@link OrtSession} used to run inference. */\n get session(): OrtSession {\n return this._session;\n }\n}\n\n/**\n * Render a confidence threshold the way the Python SDK renders it.\n *\n * JavaScript and Python disagree on how a number becomes text. A whole\n * threshold is `1` here and `1.0` there; JavaScript holds off on exponent\n * notation until `1e-7` while Python switches at `1e-5`. A fused pipeline is\n * built once and runs under both runtimes from the same file, so a message that\n * quotes the threshold has to quote it identically — otherwise the two SDKs\n * describe the same run with two different numbers.\n *\n * Six decimals with the trailing zeros trimmed covers every threshold a caller\n * can meaningfully set and agrees byte for byte with `_format_threshold` in the\n * Python SDK's `tasks/base.py`. The pairing is fixed by a shared table in both\n * test suites, so a change on one side that is not mirrored on the other fails.\n *\n * @param value The threshold to render.\n * @returns The threshold as text, without trailing zeros or a trailing dot.\n */\nfunction formatThreshold(value: number): string {\n return value.toFixed(6).replace(/0+$/, \"\").replace(/\\.$/, \"\");\n}\n\n/**\n * Turn an empty result into an error, when the caller asked for that.\n *\n * Shared by every task that can come back with nothing — {@link Detector},\n * {@link Segmenter} and {@link DetectClassify} — so the three agree on when\n * they throw and on what the message says. The message names the two settings\n * that decide the outcome, because \"no detections\" on its own leaves the reader\n * unable to tell a blank image from a threshold set too high.\n *\n * @param count How many detections survived every filter.\n * @param options The flag for this call, the threshold actually applied (after\n * any per-call override), the class allowlist if one narrowed the search, and\n * the source path when the input was one.\n * @throws {@link NoDetectionsError} when the flag is set and `count` is zero.\n */\nexport function requireDetections(\n count: number,\n options: {\n readonly raiseOnEmpty: boolean;\n readonly confThreshold: number;\n readonly classes: readonly number[] | undefined;\n readonly path: string | null;\n },\n): void {\n if (!options.raiseOnEmpty || count > 0) return;\n const where = options.path ? ` in ${options.path}` : \"\";\n const narrowed =\n options.classes === undefined\n ? \"\"\n : ` among classes [${[...options.classes].sort((a, b) => a - b).join(\", \")}]`;\n throw new NoDetectionsError(\n `No detections${where}${narrowed}: nothing cleared confThreshold=${formatThreshold(options.confThreshold)}.`,\n );\n}\n\n/**\n * Cut the box region out of an image.\n *\n * The box is truncated to whole pixels and clamped to the image, the same\n * rounding every task applies, so a crop always matches `bbox.asIntXyxy()`\n * within the frame.\n *\n * @param image The source image.\n * @param bbox The box, in the image's pixel coordinates.\n * @returns The cropped region, or a zero-sized image for a box with no area.\n */\nexport function cropToBox(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 * Wrap a computation so it runs on first call and is remembered after.\n *\n * Backs the per-detection images (`croppedImage`, `segmentedImage`): most\n * callers read a box and a class and never look at the pixels, yet building\n * them eagerly copied every box region out of the frame on every\n * `predict()` — up to `maxDetections` allocations nobody asked for.\n *\n * @param compute Produces the value; called at most once.\n * @returns A getter returning the value, computing it on the first call.\n */\nexport function memoize<T>(compute: () => T): () => T {\n let done = false;\n let value: T;\n return () => {\n if (!done) {\n value = compute();\n done = true;\n }\n return value;\n };\n}\n"],"mappings":"oEAYA,IAAsB,EAAtB,KAAiC,CACY,SAAzC,YAAsB,EAAyC,CAAtB,KAAA,SAAA,CAAuB,CAGhE,IAAI,SAAsB,CACtB,OAAO,KAAK,QAChB,CACJ,EAoBA,SAAS,EAAgB,EAAuB,CAC5C,OAAO,EAAM,QAAQ,CAAC,CAAC,CAAC,QAAQ,MAAO,EAAE,CAAC,CAAC,QAAQ,MAAO,EAAE,CAChE,CAiBA,SAAgB,EACZ,EACA,EAMI,CACJ,GAAI,CAAC,EAAQ,cAAgB,EAAQ,EAAG,OACxC,IAAM,EAAQ,EAAQ,KAAO,OAAO,EAAQ,OAAS,GAC/C,EACF,EAAQ,UAAY,IAAA,GACd,GACA,mBAAmB,CAAC,GAAG,EAAQ,OAAO,CAAC,CAAC,MAAM,EAAG,IAAM,EAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,GACnF,MAAM,IAAI,EAAA,kBACN,gBAAgB,IAAQ,EAAS,kCAAkC,EAAgB,EAAQ,aAAa,EAAE,EAC9G,CACJ,CAaA,SAAgB,EAAU,EAAiB,EAA6B,CACpE,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,SAAgB,EAAW,EAA2B,CAClD,IAAI,EAAO,GACP,EACJ,WACI,AAEI,KADA,EAAQ,EAAQ,EACT,IAEJ,EAEf"}
@@ -1,6 +1,7 @@
1
1
  import { NoDetectionsError as e } from "../core/exceptions.js";
2
+ import { RGBImage as t } from "../types.js";
2
3
  //#region src/vision/tasks/base.ts
3
- var t = class {
4
+ var n = class {
4
5
  _session;
5
6
  constructor(e) {
6
7
  this._session = e;
@@ -9,15 +10,29 @@ var t = class {
9
10
  return this._session;
10
11
  }
11
12
  };
12
- function n(e) {
13
+ function r(e) {
13
14
  return e.toFixed(6).replace(/0+$/, "").replace(/\.$/, "");
14
15
  }
15
- function r(t, r) {
16
- if (!r.raiseOnEmpty || t > 0) return;
17
- let i = r.path ? ` in ${r.path}` : "", a = r.classes === void 0 ? "" : ` among classes [${[...r.classes].sort((e, t) => e - t).join(", ")}]`;
18
- throw new e(`No detections${i}${a}: nothing cleared confThreshold=${n(r.confThreshold)}.`);
16
+ function i(t, n) {
17
+ if (!n.raiseOnEmpty || t > 0) return;
18
+ let i = n.path ? ` in ${n.path}` : "", a = n.classes === void 0 ? "" : ` among classes [${[...n.classes].sort((e, t) => e - t).join(", ")}]`;
19
+ throw new e(`No detections${i}${a}: nothing cleared confThreshold=${r(n.confThreshold)}.`);
20
+ }
21
+ function a(e, n) {
22
+ let [r, i, a, o] = n.asIntXyxy(), s = Math.max(0, r), c = Math.max(0, i), l = Math.min(e.width, a), u = Math.min(e.height, o);
23
+ if (l <= s || u <= c) return new t(/* @__PURE__ */ new Uint8Array(), 0, 0);
24
+ let d = l - s, f = u - c, p = new Uint8Array(d * f * 3);
25
+ for (let t = 0; t < f; t++) {
26
+ let n = ((c + t) * e.width + s) * 3;
27
+ p.set(e.data.subarray(n, n + d * 3), t * d * 3);
28
+ }
29
+ return new t(p, d, f);
30
+ }
31
+ function o(e) {
32
+ let t = !1, n;
33
+ return () => (t ||= (n = e(), !0), n);
19
34
  }
20
35
  //#endregion
21
- export { t as VisionTask, r as requireDetections };
36
+ export { n as VisionTask, a as cropToBox, o as memoize, i as requireDetections };
22
37
 
23
38
  //# sourceMappingURL=base.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"base.js","names":[],"sources":["../../../src/vision/tasks/base.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Common foundation for task-oriented vision SDK objects.\n *\n * The base only owns the {@link OrtSession} — label resolution lives in each\n * subclass because how `numClasses` is read from the model differs per task.\n */\n\nimport { NoDetectionsError } from \"../core/exceptions\";\nimport type { OrtSession } from \"../core/session\";\n\nexport abstract class VisionTask {\n protected constructor(protected readonly _session: OrtSession) {}\n\n /** The underlying {@link OrtSession} used to run inference. */\n get session(): OrtSession {\n return this._session;\n }\n}\n\n/**\n * Render a confidence threshold the way the Python SDK renders it.\n *\n * JavaScript and Python disagree on how a number becomes text. A whole\n * threshold is `1` here and `1.0` there; JavaScript holds off on exponent\n * notation until `1e-7` while Python switches at `1e-5`. A fused pipeline is\n * built once and runs under both runtimes from the same file, so a message that\n * quotes the threshold has to quote it identically — otherwise the two SDKs\n * describe the same run with two different numbers.\n *\n * Six decimals with the trailing zeros trimmed covers every threshold a caller\n * can meaningfully set and agrees byte for byte with `_format_threshold` in the\n * Python SDK's `tasks/base.py`. The pairing is fixed by a shared table in both\n * test suites, so a change on one side that is not mirrored on the other fails.\n *\n * @param value The threshold to render.\n * @returns The threshold as text, without trailing zeros or a trailing dot.\n */\nfunction formatThreshold(value: number): string {\n return value.toFixed(6).replace(/0+$/, \"\").replace(/\\.$/, \"\");\n}\n\n/**\n * Turn an empty result into an error, when the caller asked for that.\n *\n * Shared by every task that can come back with nothing — {@link Detector},\n * {@link Segmenter} and {@link DetectClassify} — so the three agree on when\n * they throw and on what the message says. The message names the two settings\n * that decide the outcome, because \"no detections\" on its own leaves the reader\n * unable to tell a blank image from a threshold set too high.\n *\n * @param count How many detections survived every filter.\n * @param options The flag for this call, the threshold actually applied (after\n * any per-call override), the class allowlist if one narrowed the search, and\n * the source path when the input was one.\n * @throws {@link NoDetectionsError} when the flag is set and `count` is zero.\n */\nexport function requireDetections(\n count: number,\n options: {\n readonly raiseOnEmpty: boolean;\n readonly confThreshold: number;\n readonly classes: readonly number[] | undefined;\n readonly path: string | null;\n },\n): void {\n if (!options.raiseOnEmpty || count > 0) return;\n const where = options.path ? ` in ${options.path}` : \"\";\n const narrowed =\n options.classes === undefined\n ? \"\"\n : ` among classes [${[...options.classes].sort((a, b) => a - b).join(\", \")}]`;\n throw new NoDetectionsError(\n `No detections${where}${narrowed}: nothing cleared confThreshold=${formatThreshold(options.confThreshold)}.`,\n );\n}\n"],"mappings":";;AAWA,IAAsB,IAAtB,MAAiC;CACY;CAAzC,YAAsB,GAAyC;EAAtB,KAAA,WAAA;CAAuB;CAGhE,IAAI,UAAsB;EACtB,OAAO,KAAK;CAChB;AACJ;AAoBA,SAAS,EAAgB,GAAuB;CAC5C,OAAO,EAAM,QAAQ,CAAC,CAAC,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAC,QAAQ,OAAO,EAAE;AAChE;AAiBA,SAAgB,EACZ,GACA,GAMI;CACJ,IAAI,CAAC,EAAQ,gBAAgB,IAAQ,GAAG;CACxC,IAAM,IAAQ,EAAQ,OAAO,OAAO,EAAQ,SAAS,IAC/C,IACF,EAAQ,YAAY,KAAA,IACd,KACA,mBAAmB,CAAC,GAAG,EAAQ,OAAO,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE;CACnF,MAAM,IAAI,EACN,gBAAgB,IAAQ,EAAS,kCAAkC,EAAgB,EAAQ,aAAa,EAAE,EAC9G;AACJ"}
1
+ {"version":3,"file":"base.js","names":[],"sources":["../../../src/vision/tasks/base.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Common foundation for task-oriented vision SDK objects.\n *\n * The base only owns the {@link OrtSession} — label resolution lives in each\n * subclass because how `numClasses` is read from the model differs per task.\n */\n\nimport { NoDetectionsError } from \"../core/exceptions\";\nimport type { OrtSession } from \"../core/session\";\nimport { type BoundingBox, RGBImage } from \"../types\";\n\nexport abstract class VisionTask {\n protected constructor(protected readonly _session: OrtSession) {}\n\n /** The underlying {@link OrtSession} used to run inference. */\n get session(): OrtSession {\n return this._session;\n }\n}\n\n/**\n * Render a confidence threshold the way the Python SDK renders it.\n *\n * JavaScript and Python disagree on how a number becomes text. A whole\n * threshold is `1` here and `1.0` there; JavaScript holds off on exponent\n * notation until `1e-7` while Python switches at `1e-5`. A fused pipeline is\n * built once and runs under both runtimes from the same file, so a message that\n * quotes the threshold has to quote it identically — otherwise the two SDKs\n * describe the same run with two different numbers.\n *\n * Six decimals with the trailing zeros trimmed covers every threshold a caller\n * can meaningfully set and agrees byte for byte with `_format_threshold` in the\n * Python SDK's `tasks/base.py`. The pairing is fixed by a shared table in both\n * test suites, so a change on one side that is not mirrored on the other fails.\n *\n * @param value The threshold to render.\n * @returns The threshold as text, without trailing zeros or a trailing dot.\n */\nfunction formatThreshold(value: number): string {\n return value.toFixed(6).replace(/0+$/, \"\").replace(/\\.$/, \"\");\n}\n\n/**\n * Turn an empty result into an error, when the caller asked for that.\n *\n * Shared by every task that can come back with nothing — {@link Detector},\n * {@link Segmenter} and {@link DetectClassify} — so the three agree on when\n * they throw and on what the message says. The message names the two settings\n * that decide the outcome, because \"no detections\" on its own leaves the reader\n * unable to tell a blank image from a threshold set too high.\n *\n * @param count How many detections survived every filter.\n * @param options The flag for this call, the threshold actually applied (after\n * any per-call override), the class allowlist if one narrowed the search, and\n * the source path when the input was one.\n * @throws {@link NoDetectionsError} when the flag is set and `count` is zero.\n */\nexport function requireDetections(\n count: number,\n options: {\n readonly raiseOnEmpty: boolean;\n readonly confThreshold: number;\n readonly classes: readonly number[] | undefined;\n readonly path: string | null;\n },\n): void {\n if (!options.raiseOnEmpty || count > 0) return;\n const where = options.path ? ` in ${options.path}` : \"\";\n const narrowed =\n options.classes === undefined\n ? \"\"\n : ` among classes [${[...options.classes].sort((a, b) => a - b).join(\", \")}]`;\n throw new NoDetectionsError(\n `No detections${where}${narrowed}: nothing cleared confThreshold=${formatThreshold(options.confThreshold)}.`,\n );\n}\n\n/**\n * Cut the box region out of an image.\n *\n * The box is truncated to whole pixels and clamped to the image, the same\n * rounding every task applies, so a crop always matches `bbox.asIntXyxy()`\n * within the frame.\n *\n * @param image The source image.\n * @param bbox The box, in the image's pixel coordinates.\n * @returns The cropped region, or a zero-sized image for a box with no area.\n */\nexport function cropToBox(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 * Wrap a computation so it runs on first call and is remembered after.\n *\n * Backs the per-detection images (`croppedImage`, `segmentedImage`): most\n * callers read a box and a class and never look at the pixels, yet building\n * them eagerly copied every box region out of the frame on every\n * `predict()` — up to `maxDetections` allocations nobody asked for.\n *\n * @param compute Produces the value; called at most once.\n * @returns A getter returning the value, computing it on the first call.\n */\nexport function memoize<T>(compute: () => T): () => T {\n let done = false;\n let value: T;\n return () => {\n if (!done) {\n value = compute();\n done = true;\n }\n return value;\n };\n}\n"],"mappings":";;;AAYA,IAAsB,IAAtB,MAAiC;CACY;CAAzC,YAAsB,GAAyC;EAAtB,KAAA,WAAA;CAAuB;CAGhE,IAAI,UAAsB;EACtB,OAAO,KAAK;CAChB;AACJ;AAoBA,SAAS,EAAgB,GAAuB;CAC5C,OAAO,EAAM,QAAQ,CAAC,CAAC,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAC,QAAQ,OAAO,EAAE;AAChE;AAiBA,SAAgB,EACZ,GACA,GAMI;CACJ,IAAI,CAAC,EAAQ,gBAAgB,IAAQ,GAAG;CACxC,IAAM,IAAQ,EAAQ,OAAO,OAAO,EAAQ,SAAS,IAC/C,IACF,EAAQ,YAAY,KAAA,IACd,KACA,mBAAmB,CAAC,GAAG,EAAQ,OAAO,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE;CACnF,MAAM,IAAI,EACN,gBAAgB,IAAQ,EAAS,kCAAkC,EAAgB,EAAQ,aAAa,EAAE,EAC9G;AACJ;AAaA,SAAgB,EAAU,GAAiB,GAA6B;CACpE,IAAM,CAAC,GAAO,GAAO,GAAO,KAAS,EAAK,UAAU,GAC9C,IAAK,KAAK,IAAI,GAAG,CAAK,GACtB,IAAK,KAAK,IAAI,GAAG,CAAK,GACtB,IAAK,KAAK,IAAI,EAAM,OAAO,CAAK,GAChC,IAAK,KAAK,IAAI,EAAM,QAAQ,CAAK;CACvC,IAAI,KAAM,KAAM,KAAM,GAAI,OAAO,IAAI,kBAAS,IAAI,WAAY,GAAG,GAAG,CAAC;CAErE,IAAM,IAAQ,IAAK,GACb,IAAS,IAAK,GACd,IAAM,IAAI,WAAW,IAAQ,IAAS,CAAC;CAC7C,KAAK,IAAI,IAAM,GAAG,IAAM,GAAQ,KAAO;EACnC,IAAM,MAAW,IAAK,KAAO,EAAM,QAAQ,KAAM;EACjD,EAAI,IAAI,EAAM,KAAK,SAAS,GAAQ,IAAS,IAAQ,CAAC,GAAG,IAAM,IAAQ,CAAC;CAC5E;CACA,OAAO,IAAI,EAAS,GAAK,GAAO,CAAM;AAC1C;AAaA,SAAgB,EAAW,GAA2B;CAClD,IAAI,IAAO,IACP;CACJ,cACI,AAEI,OADA,IAAQ,EAAQ,GACT,KAEJ;AAEf"}
@@ -1,2 +1,2 @@
1
- const e=require("../core/timing.cjs"),t=require("../results.cjs"),n=require("../labels.cjs"),r=require("../core/graph.cjs"),i=require("../core/metadata.cjs"),a=require("../core/session.cjs"),o=require("../normalization.cjs"),s=require("../io/image.cjs"),c=require("../preprocess/image.cjs"),l=require("../preprocess/pipeline.cjs"),u=require("../postprocess/classification.cjs"),d=require("./base.cjs");var f=class f extends d.VisionTask{_labels;_names;_inputSize;_mean;_std;_applySoftmax;_normalization;constructor(e,t,n,r,i,a,o,s){super(e),this._labels=t,this._names=n,this._inputSize=r,this._mean=i,this._std=a,this._applySoftmax=o,this._normalization=s}get appliesSoftmax(){return this._applySoftmax}get normalization(){return this._normalization}_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(){if(this._pipelineCache===null){let[e,t]=this._inputSize;this._pipelineCache=new l.ResizePipeline(e,t,this._mean,this._std)}return this._pipelineCache}static async create(e,t={}){let s=await a.OrtSession.create(e,t),c=o.resolveNormalization(s.metadata,{normalization:t.normalization,mean:t.mean,std:t.std}),l=t.numClasses??r.classificationNumClasses(s.outputShape)??void 0,u=n.resolveLabels(t.labels??i.modelNames(s.metadata),{numClasses:l}),d={};for(let e=0;e<u.length;e++)d[e]=u[e];return new f(s,u,d,r.resolveInputSize({graphShape:s.inputShape,requested:t.inputSize,fallback:[224,224]}),c.mean,c.std,t.applySoftmax??!o.isUltralyticsClassifier(s.metadata),c.name)}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,r={}){let i=new e.SpeedTimer,a=typeof n==`string`?n:null,o=await s.loadImage(n);i.stage(`load`);let c=this._preprocess(o);i.stage(`preprocess`);let l=await this._session.run({[this._session.inputName]:c});this._pipeline.release(),i.stage(`inference`);let d=this._session.outputNames[0];if(d===void 0)throw Error(`Classifier model has no outputs.`);let f=l[d];if(f===void 0)throw Error(`Classifier model output ${d} missing from run() result.`);let p=this._postprocess(f.data),{indices:m,values:h}=u.topK(p,r.topK??null),g=[];for(let e=0;e<m.length;e++){let t=m[e],n=this._labels[t]??`class_${t}`;g.push({classId:t,className:n,probability:h[e],cls:t,name:n,conf:h[e]})}if(g.length===0)throw Error(`Classifier produced no probabilities (empty output).`);let _=g[0],v={classId:_.classId,className:_.className,confidence:_.probability,cls:_.classId,name:_.className,conf:_.probability,image:o,probabilities:g},y=[o.height,o.width],b=new t.Probs(p);return i.stage(`postprocess`),[new t.ClassificationResults(b,v,this._names,o,y,a,i.speed())]}_preprocess(e){let[t,n]=this._inputSize,{data:r}=this._pipeline.run(e);return c.toFloat32Tensor(r,[1,3,n,t])}_postprocess(e){return this._applySoftmax?u.softmax(e):new Float32Array(e)}};exports.Classifier=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("../normalization.cjs"),c=require("../io/image.cjs"),l=require("../preprocess/image.cjs"),u=require("../preprocess/pipeline.cjs"),d=require("../postprocess/classification.cjs"),f=require("./base.cjs");var p=class p extends f.VisionTask{_labels;_names;_inputSize;_mean;_std;_applySoftmax;_normalization;constructor(e,t,n,r,i,a,o,s){super(e),this._labels=t,this._names=n,this._inputSize=r,this._mean=i,this._std=a,this._applySoftmax=o,this._normalization=s}get appliesSoftmax(){return this._applySoftmax}get normalization(){return this._normalization}_pipelineCache=null;async warmup(e=1){let[t,n]=this._inputSize;for(let r=0;r<e;r++){let e=l.toFloat32Tensor(u.zeroTensorData(t,n),[1,3,n,t]);await this._session.run({[this._session.inputName]:e})}}get _pipeline(){if(this._pipelineCache===null){let[e,t]=this._inputSize;this._pipelineCache=new u.ResizePipeline(e,t,this._mean,this._std)}return this._pipelineCache}static async create(e,t={}){let r=await o.OrtSession.create(e,t),c=s.resolveNormalization(r.metadata,{normalization:t.normalization,mean:t.mean,std:t.std}),l=t.numClasses??i.classificationNumClasses(r.outputShape)??void 0,u=n.resolveLabels(t.labels??a.modelNames(r.metadata),{numClasses:l}),d={};for(let e=0;e<u.length;e++)d[e]=u[e];return new p(r,u,d,i.resolveInputSize({graphShape:r.inputShape,requested:t.inputSize,fallback:[224,224]}),c.mean,c.std,t.applySoftmax??!s.isUltralyticsClassifier(r.metadata),c.name)}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:s,canvas:l}=await c.loadImageSource(n);a.stage(`load`);let{tensor:u,reused:f}=this._preprocess(s,l);a.stage(`preprocess`);let p;try{p=await this._session.run({[this._session.inputName]:u})}finally{this._pipeline.release({reused:f})}a.stage(`inference`);let m=this._session.outputNames[0];if(m===void 0)throw Error(`Classifier model has no outputs.`);let h=p[m];if(h===void 0)throw Error(`Classifier model output ${m} missing from run() result.`);let g=this._postprocess(r.asFloat32Array(h.data)),{indices:_,values:v}=d.topK(g,i.topK??null),y=[];for(let e=0;e<_.length;e++){let t=_[e],n=this._labels[t]??`class_${t}`;y.push({classId:t,className:n,probability:v[e],cls:t,name:n,conf:v[e]})}if(y.length===0)throw Error(`Classifier produced no probabilities (empty output).`);let b=y[0],x={classId:b.classId,className:b.className,confidence:b.probability,cls:b.classId,name:b.className,conf:b.probability,image:s,probabilities:y},S=[s.height,s.width],C=new t.Probs(g);return a.stage(`postprocess`),[new t.ClassificationResults(C,x,this._names,s,S,o,a.speed())]}_preprocess(e,t){let[n,r]=this._inputSize,{data:i,reused:a}=this._pipeline.run(e,t);return{tensor:l.toFloat32Tensor(i,[1,3,r,n]),reused:a}}_postprocess(e){return this._applySoftmax?d.softmax(e):new Float32Array(e)}};exports.Classifier=p;
2
2
  //# sourceMappingURL=classifier.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"classifier.cjs","names":[],"sources":["../../../src/vision/tasks/classifier.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Image classification task using ONNX Runtime Web.\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { SpeedTimer } from \"../core/timing\";\nimport { type ImageInput, loadImage } from \"../io/image\";\nimport { classificationNumClasses, resolveInputSize } from \"../core/graph\";\nimport { modelNames } from \"../core/metadata\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport {\n type Normalization,\n isUltralyticsClassifier,\n resolveNormalization,\n} from \"../normalization\";\nimport { softmax, topK } from \"../postprocess/classification\";\nimport { toFloat32Tensor } from \"../preprocess/image\";\nimport { ResizePipeline, zeroTensorData } from \"../preprocess/pipeline\";\nimport { ClassificationResults, Probs } from \"../results\";\nimport { VisionTask } from \"./base\";\nimport { type ClassProbability, type ClassificationResult, type RGBImage } from \"../types\";\n\nexport interface ClassifierOptions extends OrtSessionOptions {\n /**\n * Class label spec — see {@link resolveLabels}.\n *\n * Optional: when omitted, the names the export baked into the model are used\n * (Ultralytics writes them as `names` in the metadata map). Only when the\n * model carries none does this fall back to generated `class_<id>` labels.\n * Passing a spec always wins, for a model whose names are wrong or absent.\n */\n readonly labels?: LabelSpec;\n /**\n * Number of classes the model can predict.\n *\n * Optional: inferred from the classification head's declared output shape\n * `(B, nc)`. Pass it to validate that the supplied labels match the model.\n */\n readonly numClasses?: number;\n /**\n * Model input `[width, height]` in pixels.\n *\n * Only used when the model's graph leaves its spatial axes dynamic: a graph\n * that declares a static size always wins, since that is the only shape ONNX\n * Runtime will accept. Defaults to `[224, 224]`.\n */\n readonly inputSize?: readonly [number, number];\n /**\n * Which preprocessing this model expects — see {@link Normalization}.\n *\n * Defaults to `\"auto\"`, which reads the model's own export metadata and picks\n * `\"ultralytics\"` (raw `[0, 1]`) for an Ultralytics classification head and\n * `\"imagenet\"` for everything else.\n */\n readonly normalization?: Normalization;\n /** Per-channel RGB mean, overriding the preset. Defaults to the preset's. */\n readonly mean?: readonly [number, number, number];\n /** Per-channel RGB standard deviation, overriding the preset. */\n readonly std?: readonly [number, number, number];\n /**\n * Whether the model's output still needs a softmax.\n *\n * Left undefined (the default), this reads the model's metadata and answers\n * `false` for an Ultralytics classification export, whose graph already ends\n * in one — applying a second softmax to a probability vector keeps the\n * ranking but flattens the confidences, so the top-1 stays right while every\n * number attached to it is wrong. Detection covers that family; for any other\n * model that already emits probabilities, pass `false` explicitly.\n */\n readonly applySoftmax?: boolean;\n}\n\nexport interface ClassifierPredictOptions {\n /**\n * If set, the per-class probability list in `results[0].result.probabilities`\n * is truncated to the top-K entries. The bulk `probs` view always exposes\n * the full vector.\n */\n readonly topK?: number;\n}\n\n/**\n * Image classifier wrapping an ONNX model with ImageNet-style preprocessing.\n *\n * `predict()` returns `Promise<ClassificationResults[]>` (length 1 for a\n * single image), mirroring Ultralytics' API. The envelope exposes a `probs`\n * collection (`top1`, `top1conf`, `top5`, `top5conf`, `data`) plus the\n * legacy per-class probability list with names already resolved.\n *\n * Defaults: 224×224 RGB input, `float32` normalized with ImageNet mean/std,\n * NCHW layout, batch size 1, softmax applied to the raw output.\n *\n * @example\n * ```typescript\n * const clf = await Classifier.create(\"/models/resnet50.onnx\", {\n * labels: [\"tench\", \"goldfish\", ...] // 1000 ImageNet labels\n * });\n * const r = (await clf.predict(\"/images/dog.jpg\"))[0];\n * console.log(r.cls, r.conf, r.name);\n * console.log(r.probs.top5, r.probs.top5conf);\n * ```\n */\nexport class Classifier extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _inputSize: readonly [number, number],\n private readonly _mean: readonly [number, number, number],\n private readonly _std: readonly [number, number, number],\n private readonly _applySoftmax: boolean,\n private readonly _normalization: string,\n ) {\n super(session);\n }\n\n /**\n * Whether a softmax is applied to the model's output before ranking.\n *\n * Resolved once at construction. Worth reading when confidences look\n * compressed: a second softmax over an already-normalized vector leaves the\n * ordering intact and the numbers meaningless, which is invisible to any check\n * that only looks at the predicted class.\n */\n get appliesSoftmax(): boolean {\n return this._applySoftmax;\n }\n\n /**\n * Which preprocessing this classifier applies to every image.\n *\n * One of the {@link Normalization} preset names, or `\"custom\"` when the caller\n * supplied `mean`/`std` directly. Worth reading when a model underperforms:\n * feeding a classifier a differently prepared tensor than it was trained on\n * degrades it without throwing anything, so \"what does this assume\" is the\n * first question.\n */\n get normalization(): string {\n return this._normalization;\n }\n\n private _pipelineCache: ResizePipeline | null = null;\n\n /**\n * Run the model once on a zero tensor, paying one-time costs up front.\n *\n * The first inference of a session is not representative: WebGPU compiles its\n * shaders on it and the WASM backend faults in its arenas. Calling this while\n * a loading spinner is still up moves that cost somewhere the user is already\n * waiting — which matters most for a classifier running as the last step of\n * an on-device analysis, where the delay lands right before the answer shows.\n *\n * @param runs How many warm-up inferences to run. One is enough for WASM;\n * WebGPU sometimes settles on the second.\n */\n async warmup(runs: number = 1): Promise<void> {\n const [tw, th] = this._inputSize;\n for (let i = 0; i < runs; i++) {\n const tensor = toFloat32Tensor(zeroTensorData(tw, th), [1, 3, th, tw]);\n await this._session.run({ [this._session.inputName]: tensor });\n }\n }\n\n /**\n * The fused preprocessing pipeline, built on first use.\n *\n * Lazily, because constructing it reserves the output buffer: a task built in\n * an environment without a canvas implementation stays constructible, and only\n * fails if it is actually asked to preprocess something.\n */\n private get _pipeline(): ResizePipeline {\n if (this._pipelineCache === null) {\n const [tw, th] = this._inputSize;\n this._pipelineCache = new ResizePipeline(tw, th, this._mean, this._std);\n }\n return this._pipelineCache;\n }\n\n /**\n * Load the model, resolve labels, and settle the preprocessing.\n *\n * @param model The model source — a URL, or the bytes.\n * @param options Labels, input size, normalization, and session options.\n * @throws {RangeError} If `normalization` names an unknown preset, or names\n * one while `mean`/`std` are also given.\n */\n static async create(model: ModelSource, options: ClassifierOptions = {}): Promise<Classifier> {\n const session = await OrtSession.create(model, options);\n const normalization = resolveNormalization(session.metadata, {\n normalization: options.normalization,\n mean: options.mean,\n std: options.std,\n });\n const numClasses =\n options.numClasses ?? classificationNumClasses(session.outputShape) ?? undefined;\n const labels = resolveLabels(options.labels ?? modelNames(session.metadata), {\n numClasses,\n });\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n names[i] = labels[i] as string;\n }\n return new Classifier(\n session,\n labels,\n names,\n resolveInputSize({\n graphShape: session.inputShape,\n requested: options.inputSize,\n fallback: [224, 224],\n }),\n normalization.mean,\n normalization.std,\n options.applySoftmax ?? !isUltralyticsClassifier(session.metadata),\n normalization.name,\n );\n }\n\n /** Class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Class id → class name dict (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /**\n * The `[width, height]` this task preprocesses to.\n *\n * Resolved at creation time from the model's graph when it declares a static\n * input, so reading it back tells you the resolution inference really runs at\n * — not merely what was requested.\n */\n get inputSize(): readonly [number, number] {\n return this._inputSize;\n }\n\n /** Number of classes the model can predict. */\n get numClasses(): number {\n return this._labels.length;\n }\n\n /** Alias for {@link predict} (parity with PyTorch `nn.Module.__call__`). */\n async call(\n image: ImageInput,\n options: ClassifierPredictOptions = {},\n ): Promise<ClassificationResults[]> {\n return this.predict(image, options);\n }\n\n /** Run classification on a single image. */\n async predict(\n image: ImageInput,\n options: ClassifierPredictOptions = {},\n ): Promise<ClassificationResults[]> {\n const timer = new SpeedTimer();\n const path = typeof image === \"string\" ? image : null;\n const original = await loadImage(image);\n timer.stage(\"load\");\n const tensor = this._preprocess(original);\n timer.stage(\"preprocess\");\n const outputs = await this._session.run({ [this._session.inputName]: tensor });\n this._pipeline.release();\n timer.stage(\"inference\");\n const firstOutputName = this._session.outputNames[0];\n if (firstOutputName === undefined) {\n throw new Error(\"Classifier model has no outputs.\");\n }\n const raw = outputs[firstOutputName];\n if (raw === undefined) {\n throw new Error(\n `Classifier model output ${firstOutputName} missing from run() result.`,\n );\n }\n const fullProbs = this._postprocess(raw.data as Float32Array);\n\n const { indices, values } = topK(fullProbs, options.topK ?? null);\n const probabilities: ClassProbability[] = [];\n for (let i = 0; i < indices.length; i++) {\n const id = indices[i] as number;\n const className = this._labels[id] ?? `class_${id}`;\n probabilities.push({\n classId: id,\n className,\n probability: values[i] as number,\n cls: id,\n name: className,\n conf: values[i] as number,\n });\n }\n if (probabilities.length === 0) {\n throw new Error(\"Classifier produced no probabilities (empty output).\");\n }\n\n const top = probabilities[0] as ClassProbability;\n const result: ClassificationResult = {\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: original,\n probabilities,\n };\n\n const orig: readonly [number, number] = [original.height, original.width];\n const probs = new Probs(fullProbs);\n timer.stage(\"postprocess\");\n return [\n new ClassificationResults(\n probs,\n result,\n this._names,\n original,\n orig,\n path,\n timer.speed(),\n ),\n ];\n }\n\n private _preprocess(image: RGBImage): ort.Tensor {\n const [tw, th] = this._inputSize;\n const { data } = this._pipeline.run(image);\n return toFloat32Tensor(data, [1, 3, th, tw]);\n }\n\n private _postprocess(raw: Float32Array): Float32Array {\n return this._applySoftmax ? softmax(raw) : new Float32Array(raw);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"classifier.cjs","names":[],"sources":["../../../src/vision/tasks/classifier.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * Image classification task using ONNX Runtime Web.\n */\n\nimport type * as ort from \"onnxruntime-web\";\n\nimport { type ModelSource, type OrtSessionOptions, OrtSession } from \"../core/session\";\nimport { SpeedTimer } from \"../core/timing\";\nimport { type ImageInput, loadImageSource } from \"../io/image\";\nimport { classificationNumClasses, resolveInputSize } from \"../core/graph\";\nimport { modelNames } from \"../core/metadata\";\nimport { type LabelSpec, resolveLabels } from \"../labels\";\nimport {\n type Normalization,\n isUltralyticsClassifier,\n resolveNormalization,\n} from \"../normalization\";\nimport { softmax, topK } from \"../postprocess/classification\";\nimport { asFloat32Array } from \"../core/dtypes\";\nimport { toFloat32Tensor } from \"../preprocess/image\";\nimport { ResizePipeline, zeroTensorData } from \"../preprocess/pipeline\";\nimport { ClassificationResults, Probs } from \"../results\";\nimport { VisionTask } from \"./base\";\nimport { type ClassProbability, type ClassificationResult, type RGBImage } from \"../types\";\n\nexport interface ClassifierOptions extends OrtSessionOptions {\n /**\n * Class label spec — see {@link resolveLabels}.\n *\n * Optional: when omitted, the names the export baked into the model are used\n * (Ultralytics writes them as `names` in the metadata map). Only when the\n * model carries none does this fall back to generated `class_<id>` labels.\n * Passing a spec always wins, for a model whose names are wrong or absent.\n */\n readonly labels?: LabelSpec;\n /**\n * Number of classes the model can predict.\n *\n * Optional: inferred from the classification head's declared output shape\n * `(B, nc)`. Pass it to validate that the supplied labels match the model.\n */\n readonly numClasses?: number;\n /**\n * Model input `[width, height]` in pixels.\n *\n * Only used when the model's graph leaves its spatial axes dynamic: a graph\n * that declares a static size always wins, since that is the only shape ONNX\n * Runtime will accept. Defaults to `[224, 224]`.\n */\n readonly inputSize?: readonly [number, number];\n /**\n * Which preprocessing this model expects — see {@link Normalization}.\n *\n * Defaults to `\"auto\"`, which reads the model's own export metadata and picks\n * `\"ultralytics\"` (raw `[0, 1]`) for an Ultralytics classification head and\n * `\"imagenet\"` for everything else.\n */\n readonly normalization?: Normalization;\n /** Per-channel RGB mean, overriding the preset. Defaults to the preset's. */\n readonly mean?: readonly [number, number, number];\n /** Per-channel RGB standard deviation, overriding the preset. */\n readonly std?: readonly [number, number, number];\n /**\n * Whether the model's output still needs a softmax.\n *\n * Left undefined (the default), this reads the model's metadata and answers\n * `false` for an Ultralytics classification export, whose graph already ends\n * in one — applying a second softmax to a probability vector keeps the\n * ranking but flattens the confidences, so the top-1 stays right while every\n * number attached to it is wrong. Detection covers that family; for any other\n * model that already emits probabilities, pass `false` explicitly.\n */\n readonly applySoftmax?: boolean;\n}\n\nexport interface ClassifierPredictOptions {\n /**\n * If set, the per-class probability list in `results[0].result.probabilities`\n * is truncated to the top-K entries. The bulk `probs` view always exposes\n * the full vector.\n */\n readonly topK?: number;\n}\n\n/**\n * Image classifier wrapping an ONNX model with ImageNet-style preprocessing.\n *\n * `predict()` returns `Promise<ClassificationResults[]>` (length 1 for a\n * single image), mirroring Ultralytics' API. The envelope exposes a `probs`\n * collection (`top1`, `top1conf`, `top5`, `top5conf`, `data`) plus the\n * legacy per-class probability list with names already resolved.\n *\n * Defaults: 224×224 RGB input, `float32` normalized with ImageNet mean/std,\n * NCHW layout, batch size 1, softmax applied to the raw output.\n *\n * @example\n * ```typescript\n * const clf = await Classifier.create(\"/models/resnet50.onnx\", {\n * labels: [\"tench\", \"goldfish\", ...] // 1000 ImageNet labels\n * });\n * const r = (await clf.predict(\"/images/dog.jpg\"))[0];\n * console.log(r.cls, r.conf, r.name);\n * console.log(r.probs.top5, r.probs.top5conf);\n * ```\n */\nexport class Classifier extends VisionTask {\n private constructor(\n session: OrtSession,\n private readonly _labels: readonly string[],\n private readonly _names: Readonly<Record<number, string>>,\n private readonly _inputSize: readonly [number, number],\n private readonly _mean: readonly [number, number, number],\n private readonly _std: readonly [number, number, number],\n private readonly _applySoftmax: boolean,\n private readonly _normalization: string,\n ) {\n super(session);\n }\n\n /**\n * Whether a softmax is applied to the model's output before ranking.\n *\n * Resolved once at construction. Worth reading when confidences look\n * compressed: a second softmax over an already-normalized vector leaves the\n * ordering intact and the numbers meaningless, which is invisible to any check\n * that only looks at the predicted class.\n */\n get appliesSoftmax(): boolean {\n return this._applySoftmax;\n }\n\n /**\n * Which preprocessing this classifier applies to every image.\n *\n * One of the {@link Normalization} preset names, or `\"custom\"` when the caller\n * supplied `mean`/`std` directly. Worth reading when a model underperforms:\n * feeding a classifier a differently prepared tensor than it was trained on\n * degrades it without throwing anything, so \"what does this assume\" is the\n * first question.\n */\n get normalization(): string {\n return this._normalization;\n }\n\n private _pipelineCache: ResizePipeline | null = null;\n\n /**\n * Run the model once on a zero tensor, paying one-time costs up front.\n *\n * The first inference of a session is not representative: WebGPU compiles its\n * shaders on it and the WASM backend faults in its arenas. Calling this while\n * a loading spinner is still up moves that cost somewhere the user is already\n * waiting — which matters most for a classifier running as the last step of\n * an on-device analysis, where the delay lands right before the answer shows.\n *\n * @param runs How many warm-up inferences to run. One is enough for WASM;\n * WebGPU sometimes settles on the second.\n */\n async warmup(runs: number = 1): Promise<void> {\n const [tw, th] = this._inputSize;\n for (let i = 0; i < runs; i++) {\n const tensor = toFloat32Tensor(zeroTensorData(tw, th), [1, 3, th, tw]);\n await this._session.run({ [this._session.inputName]: tensor });\n }\n }\n\n /**\n * The fused preprocessing pipeline, built on first use.\n *\n * Lazily, because constructing it reserves the output buffer: a task built in\n * an environment without a canvas implementation stays constructible, and only\n * fails if it is actually asked to preprocess something.\n */\n private get _pipeline(): ResizePipeline {\n if (this._pipelineCache === null) {\n const [tw, th] = this._inputSize;\n this._pipelineCache = new ResizePipeline(tw, th, this._mean, this._std);\n }\n return this._pipelineCache;\n }\n\n /**\n * Load the model, resolve labels, and settle the preprocessing.\n *\n * @param model The model source — a URL, or the bytes.\n * @param options Labels, input size, normalization, and session options.\n * @throws {RangeError} If `normalization` names an unknown preset, or names\n * one while `mean`/`std` are also given.\n */\n static async create(model: ModelSource, options: ClassifierOptions = {}): Promise<Classifier> {\n const session = await OrtSession.create(model, options);\n const normalization = resolveNormalization(session.metadata, {\n normalization: options.normalization,\n mean: options.mean,\n std: options.std,\n });\n const numClasses =\n options.numClasses ?? classificationNumClasses(session.outputShape) ?? undefined;\n const labels = resolveLabels(options.labels ?? modelNames(session.metadata), {\n numClasses,\n });\n const names: Record<number, string> = {};\n for (let i = 0; i < labels.length; i++) {\n names[i] = labels[i] as string;\n }\n return new Classifier(\n session,\n labels,\n names,\n resolveInputSize({\n graphShape: session.inputShape,\n requested: options.inputSize,\n fallback: [224, 224],\n }),\n normalization.mean,\n normalization.std,\n options.applySoftmax ?? !isUltralyticsClassifier(session.metadata),\n normalization.name,\n );\n }\n\n /** Class labels indexed by class id. */\n get labels(): readonly string[] {\n return this._labels;\n }\n\n /** Class id → class name dict (matches Ultralytics' `model.names`). */\n get names(): Readonly<Record<number, string>> {\n return this._names;\n }\n\n /**\n * The `[width, height]` this task preprocesses to.\n *\n * Resolved at creation time from the model's graph when it declares a static\n * input, so reading it back tells you the resolution inference really runs at\n * — not merely what was requested.\n */\n get inputSize(): readonly [number, number] {\n return this._inputSize;\n }\n\n /** Number of classes the model can predict. */\n get numClasses(): number {\n return this._labels.length;\n }\n\n /** Alias for {@link predict} (parity with PyTorch `nn.Module.__call__`). */\n async call(\n image: ImageInput,\n options: ClassifierPredictOptions = {},\n ): Promise<ClassificationResults[]> {\n return this.predict(image, options);\n }\n\n /** Run classification on a single image. */\n async predict(\n image: ImageInput,\n options: ClassifierPredictOptions = {},\n ): Promise<ClassificationResults[]> {\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 { tensor, reused } = this._preprocess(original, canvas);\n timer.stage(\"preprocess\");\n let outputs: Record<string, ort.Tensor>;\n try {\n outputs = await this._session.run({ [this._session.inputName]: tensor });\n } finally {\n this._pipeline.release({ reused });\n }\n timer.stage(\"inference\");\n const firstOutputName = this._session.outputNames[0];\n if (firstOutputName === undefined) {\n throw new Error(\"Classifier model has no outputs.\");\n }\n const raw = outputs[firstOutputName];\n if (raw === undefined) {\n throw new Error(\n `Classifier model output ${firstOutputName} missing from run() result.`,\n );\n }\n const fullProbs = this._postprocess(asFloat32Array(raw.data));\n\n const { indices, values } = topK(fullProbs, options.topK ?? null);\n const probabilities: ClassProbability[] = [];\n for (let i = 0; i < indices.length; i++) {\n const id = indices[i] as number;\n const className = this._labels[id] ?? `class_${id}`;\n probabilities.push({\n classId: id,\n className,\n probability: values[i] as number,\n cls: id,\n name: className,\n conf: values[i] as number,\n });\n }\n if (probabilities.length === 0) {\n throw new Error(\"Classifier produced no probabilities (empty output).\");\n }\n\n const top = probabilities[0] as ClassProbability;\n const result: ClassificationResult = {\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: original,\n probabilities,\n };\n\n const orig: readonly [number, number] = [original.height, original.width];\n const probs = new Probs(fullProbs);\n timer.stage(\"postprocess\");\n return [\n new ClassificationResults(\n probs,\n result,\n this._names,\n original,\n orig,\n path,\n timer.speed(),\n ),\n ];\n }\n\n /**\n * Resize and pack the image into the tensor the model expects.\n *\n * @param image The decoded input.\n * @param canvas The opaque canvas it was decoded on, when there is one — see\n * {@link ResizePipeline.run}.\n */\n private _preprocess(\n image: RGBImage,\n canvas: CanvasImageSource | null,\n ): { tensor: ort.Tensor; reused: boolean } {\n const [tw, th] = this._inputSize;\n const { data, reused } = this._pipeline.run(image, canvas);\n return { tensor: toFloat32Tensor(data, [1, 3, th, tw]), reused };\n }\n\n private _postprocess(raw: Float32Array): Float32Array {\n return this._applySoftmax ? softmax(raw) : new Float32Array(raw);\n }\n}\n"],"mappings":"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"}