{"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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"}