@camstack/vision 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addons/animal-classifier/index.d.mts +25 -0
- package/dist/addons/animal-classifier/index.d.ts +25 -0
- package/dist/addons/animal-classifier/index.js +469 -0
- package/dist/addons/animal-classifier/index.js.map +1 -0
- package/dist/addons/animal-classifier/index.mjs +9 -0
- package/dist/addons/animal-classifier/index.mjs.map +1 -0
- package/dist/addons/audio-classification/index.d.mts +31 -0
- package/dist/addons/audio-classification/index.d.ts +31 -0
- package/dist/addons/audio-classification/index.js +411 -0
- package/dist/addons/audio-classification/index.js.map +1 -0
- package/dist/addons/audio-classification/index.mjs +8 -0
- package/dist/addons/audio-classification/index.mjs.map +1 -0
- package/dist/addons/bird-global-classifier/index.d.mts +26 -0
- package/dist/addons/bird-global-classifier/index.d.ts +26 -0
- package/dist/addons/bird-global-classifier/index.js +475 -0
- package/dist/addons/bird-global-classifier/index.js.map +1 -0
- package/dist/addons/bird-global-classifier/index.mjs +9 -0
- package/dist/addons/bird-global-classifier/index.mjs.map +1 -0
- package/dist/addons/bird-nabirds-classifier/index.d.mts +28 -0
- package/dist/addons/bird-nabirds-classifier/index.d.ts +28 -0
- package/dist/addons/bird-nabirds-classifier/index.js +517 -0
- package/dist/addons/bird-nabirds-classifier/index.js.map +1 -0
- package/dist/addons/bird-nabirds-classifier/index.mjs +9 -0
- package/dist/addons/bird-nabirds-classifier/index.mjs.map +1 -0
- package/dist/addons/camera-native-detection/index.d.mts +32 -0
- package/dist/addons/camera-native-detection/index.d.ts +32 -0
- package/dist/addons/camera-native-detection/index.js +99 -0
- package/dist/addons/camera-native-detection/index.js.map +1 -0
- package/dist/addons/camera-native-detection/index.mjs +7 -0
- package/dist/addons/camera-native-detection/index.mjs.map +1 -0
- package/dist/addons/face-detection/index.d.mts +24 -0
- package/dist/addons/face-detection/index.d.ts +24 -0
- package/dist/addons/face-detection/index.js +513 -0
- package/dist/addons/face-detection/index.js.map +1 -0
- package/dist/addons/face-detection/index.mjs +10 -0
- package/dist/addons/face-detection/index.mjs.map +1 -0
- package/dist/addons/face-recognition/index.d.mts +24 -0
- package/dist/addons/face-recognition/index.d.ts +24 -0
- package/dist/addons/face-recognition/index.js +437 -0
- package/dist/addons/face-recognition/index.js.map +1 -0
- package/dist/addons/face-recognition/index.mjs +9 -0
- package/dist/addons/face-recognition/index.mjs.map +1 -0
- package/dist/addons/motion-detection/index.d.mts +26 -0
- package/dist/addons/motion-detection/index.d.ts +26 -0
- package/dist/addons/motion-detection/index.js +273 -0
- package/dist/addons/motion-detection/index.js.map +1 -0
- package/dist/addons/motion-detection/index.mjs +8 -0
- package/dist/addons/motion-detection/index.mjs.map +1 -0
- package/dist/addons/object-detection/index.d.mts +25 -0
- package/dist/addons/object-detection/index.d.ts +25 -0
- package/dist/addons/object-detection/index.js +673 -0
- package/dist/addons/object-detection/index.js.map +1 -0
- package/dist/addons/object-detection/index.mjs +10 -0
- package/dist/addons/object-detection/index.mjs.map +1 -0
- package/dist/addons/plate-detection/index.d.mts +25 -0
- package/dist/addons/plate-detection/index.d.ts +25 -0
- package/dist/addons/plate-detection/index.js +477 -0
- package/dist/addons/plate-detection/index.js.map +1 -0
- package/dist/addons/plate-detection/index.mjs +10 -0
- package/dist/addons/plate-detection/index.mjs.map +1 -0
- package/dist/addons/plate-recognition/index.d.mts +25 -0
- package/dist/addons/plate-recognition/index.d.ts +25 -0
- package/dist/addons/plate-recognition/index.js +470 -0
- package/dist/addons/plate-recognition/index.js.map +1 -0
- package/dist/addons/plate-recognition/index.mjs +9 -0
- package/dist/addons/plate-recognition/index.mjs.map +1 -0
- package/dist/chunk-3BKYLBBH.mjs +229 -0
- package/dist/chunk-3BKYLBBH.mjs.map +1 -0
- package/dist/chunk-4PC262GU.mjs +203 -0
- package/dist/chunk-4PC262GU.mjs.map +1 -0
- package/dist/chunk-6OR5TE7A.mjs +101 -0
- package/dist/chunk-6OR5TE7A.mjs.map +1 -0
- package/dist/chunk-7SZAISGP.mjs +210 -0
- package/dist/chunk-7SZAISGP.mjs.map +1 -0
- package/dist/chunk-AD2TFYZA.mjs +235 -0
- package/dist/chunk-AD2TFYZA.mjs.map +1 -0
- package/dist/chunk-CGYSSHHM.mjs +363 -0
- package/dist/chunk-CGYSSHHM.mjs.map +1 -0
- package/dist/chunk-IYHMGYGP.mjs +79 -0
- package/dist/chunk-IYHMGYGP.mjs.map +1 -0
- package/dist/chunk-J3IUBPRE.mjs +187 -0
- package/dist/chunk-J3IUBPRE.mjs.map +1 -0
- package/dist/chunk-KFZDJPYL.mjs +190 -0
- package/dist/chunk-KFZDJPYL.mjs.map +1 -0
- package/dist/chunk-KUO2BVFY.mjs +90 -0
- package/dist/chunk-KUO2BVFY.mjs.map +1 -0
- package/dist/chunk-PXBY3QOA.mjs +152 -0
- package/dist/chunk-PXBY3QOA.mjs.map +1 -0
- package/dist/chunk-XUKDL23Y.mjs +216 -0
- package/dist/chunk-XUKDL23Y.mjs.map +1 -0
- package/dist/chunk-Z26BVC7S.mjs +214 -0
- package/dist/chunk-Z26BVC7S.mjs.map +1 -0
- package/dist/chunk-Z5AHZQEZ.mjs +258 -0
- package/dist/chunk-Z5AHZQEZ.mjs.map +1 -0
- package/dist/index.d.mts +152 -0
- package/dist/index.d.ts +152 -0
- package/dist/index.js +2775 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +205 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
- package/python/coreml_inference.py +67 -0
- package/python/openvino_inference.py +76 -0
- package/python/pytorch_inference.py +74 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/addons/plate-detection/index.ts","../../../src/shared/image-utils.ts","../../../src/shared/postprocess/yolo.ts","../../../src/shared/engine-resolver.ts","../../../src/shared/node-engine.ts"],"sourcesContent":["import type {\n ICropperProvider,\n IDetectionAddon,\n AddonManifest,\n AddonContext,\n CropInput,\n CropperOutput,\n ConfigUISchema,\n ClassMapDefinition,\n ProbeResult,\n ModelCatalogEntry,\n DetectionModel,\n LabelDefinition,\n IInferenceEngine,\n} from '@camstack/types'\nimport { PLATE_DETECTION_MODELS } from '@camstack/types'\nimport { cropRegion, letterbox } from '../../shared/image-utils.js'\nimport { yoloPostprocess } from '../../shared/postprocess/yolo.js'\nimport { resolveEngine } from '../../shared/engine-resolver.js'\n\nconst PLATE_LABEL: LabelDefinition = { id: 'plate', name: 'License Plate' }\nconst PLATE_LABELS: readonly LabelDefinition[] = [PLATE_LABEL]\nconst PLATE_CLASS_MAP: ClassMapDefinition = { mapping: {}, preserveOriginal: true }\n\nexport default class PlateDetectionAddon implements ICropperProvider, IDetectionAddon {\n readonly id = 'plate-detection'\n readonly slot = 'cropper' as const\n readonly inputClasses = ['vehicle'] as const\n readonly outputClasses = ['plate'] as const\n readonly slotPriority = 0\n readonly manifest: AddonManifest = {\n id: 'plate-detection',\n name: 'License Plate Detection',\n version: '0.1.0',\n description: 'YOLO-based license plate detector — crops plate regions from vehicle detections',\n packageName: '@camstack/vision',\n slot: 'cropper',\n inputClasses: ['vehicle'],\n outputClasses: ['plate'],\n supportsCustomModels: false,\n mayRequirePython: false,\n defaultConfig: {\n modelId: 'yolov8n-plate',\n runtime: 'auto',\n backend: 'cpu',\n confidence: 0.5,\n iouThreshold: 0.45,\n },\n }\n\n private engine!: IInferenceEngine\n private modelEntry!: ModelCatalogEntry\n private confidence = 0.5\n private iouThreshold = 0.45\n\n async initialize(ctx: AddonContext): Promise<void> {\n const cfg = ctx.addonConfig\n const modelId = (cfg['modelId'] as string | undefined) ?? 'yolov8n-plate'\n const runtime = (cfg['runtime'] as string | undefined) ?? 'auto'\n const backend = (cfg['backend'] as string | undefined) ?? 'cpu'\n this.confidence = (cfg['confidence'] as number | undefined) ?? 0.5\n this.iouThreshold = (cfg['iouThreshold'] as number | undefined) ?? 0.45\n\n const entry = PLATE_DETECTION_MODELS.find((m) => m.id === modelId)\n if (!entry) {\n throw new Error(`PlateDetectionAddon: unknown modelId \"${modelId}\"`)\n }\n this.modelEntry = entry\n\n const resolved = await resolveEngine({\n runtime: runtime as 'auto',\n backend,\n modelEntry: entry,\n modelsDir: ctx.locationPaths.models,\n })\n this.engine = resolved.engine\n }\n\n async crop(input: CropInput): Promise<CropperOutput> {\n const start = Date.now()\n const { width: inputW, height: inputH } = this.modelEntry.inputSize\n const targetSize = Math.max(inputW, inputH)\n\n // Crop the vehicle region from the full frame\n const vehicleCrop = await cropRegion(input.frame.data, input.roi)\n\n const lb = await letterbox(vehicleCrop, targetSize)\n const output = await this.engine.run(lb.data, [1, 3, targetSize, targetSize])\n\n const numClasses = this.modelEntry.labels.length\n const numBoxes = output.length / (4 + numClasses)\n const labels = this.modelEntry.labels.map((l) => l.id)\n\n const plates = yoloPostprocess(output, numClasses, numBoxes, {\n confidence: this.confidence,\n iouThreshold: this.iouThreshold,\n labels,\n scale: lb.scale,\n padX: lb.padX,\n padY: lb.padY,\n originalWidth: lb.originalWidth,\n originalHeight: lb.originalHeight,\n })\n\n // Override class to 'plate'\n const crops = plates.map((p) => ({ ...p, class: 'plate', originalClass: p.originalClass }))\n\n return {\n crops,\n inferenceMs: Date.now() - start,\n modelId: this.modelEntry.id,\n }\n }\n\n async shutdown(): Promise<void> {\n await this.engine?.dispose()\n }\n\n getConfigSchema(): ConfigUISchema {\n return {\n sections: [\n {\n id: 'thresholds',\n title: 'Detection Thresholds',\n columns: 2,\n fields: [\n {\n key: 'confidence',\n label: 'Confidence Threshold',\n type: 'slider',\n min: 0.1,\n max: 1.0,\n step: 0.05,\n default: 0.5,\n },\n {\n key: 'iouThreshold',\n label: 'IoU Threshold (NMS)',\n type: 'slider',\n min: 0.1,\n max: 1.0,\n step: 0.05,\n default: 0.45,\n },\n ],\n },\n ],\n }\n }\n\n getClassMap(): ClassMapDefinition {\n return PLATE_CLASS_MAP\n }\n\n getModelCatalog(): ModelCatalogEntry[] {\n return [...PLATE_DETECTION_MODELS]\n }\n\n getAvailableModels(): DetectionModel[] {\n return []\n }\n\n getActiveLabels(): readonly LabelDefinition[] {\n return PLATE_LABELS\n }\n\n async probe(): Promise<ProbeResult> {\n return {\n available: true,\n runtime: this.engine?.runtime ?? 'onnx',\n device: this.engine?.device ?? 'cpu',\n capabilities: ['fp32'],\n }\n }\n}\n","import sharp from 'sharp'\nimport type { BoundingBox } from '@camstack/types'\n\n/** Decode JPEG to raw RGB pixels */\nexport async function jpegToRgb(\n jpeg: Buffer,\n): Promise<{ data: Buffer; width: number; height: number }> {\n const { data, info } = await sharp(jpeg)\n .removeAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n return { data, width: info.width, height: info.height }\n}\n\n/** Crop a region from a JPEG buffer */\nexport async function cropRegion(jpeg: Buffer, roi: BoundingBox): Promise<Buffer> {\n return sharp(jpeg)\n .extract({\n left: Math.round(roi.x),\n top: Math.round(roi.y),\n width: Math.round(roi.w),\n height: Math.round(roi.h),\n })\n .jpeg()\n .toBuffer()\n}\n\n/** Letterbox resize for YOLO: resize preserving aspect ratio, pad to square */\nexport async function letterbox(\n jpeg: Buffer,\n targetSize: number,\n): Promise<{\n data: Float32Array\n scale: number\n padX: number\n padY: number\n originalWidth: number\n originalHeight: number\n}> {\n const meta = await sharp(jpeg).metadata()\n const originalWidth = meta.width ?? 0\n const originalHeight = meta.height ?? 0\n\n const scale = Math.min(targetSize / originalWidth, targetSize / originalHeight)\n const scaledWidth = Math.round(originalWidth * scale)\n const scaledHeight = Math.round(originalHeight * scale)\n\n const padX = Math.floor((targetSize - scaledWidth) / 2)\n const padY = Math.floor((targetSize - scaledHeight) / 2)\n\n const { data } = await sharp(jpeg)\n .resize(scaledWidth, scaledHeight)\n .extend({\n top: padY,\n bottom: targetSize - scaledHeight - padY,\n left: padX,\n right: targetSize - scaledWidth - padX,\n background: { r: 114, g: 114, b: 114 },\n })\n .removeAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n // Convert HWC uint8 to CHW float [0,1]\n const numPixels = targetSize * targetSize\n const float32 = new Float32Array(3 * numPixels)\n for (let i = 0; i < numPixels; i++) {\n const srcBase = i * 3\n float32[0 * numPixels + i] = (data[srcBase]! / 255)\n float32[1 * numPixels + i] = (data[srcBase + 1]! / 255)\n float32[2 * numPixels + i] = (data[srcBase + 2]! / 255)\n }\n\n return { data: float32, scale, padX, padY, originalWidth, originalHeight }\n}\n\n/** Resize and normalize to Float32Array */\nexport async function resizeAndNormalize(\n jpeg: Buffer,\n targetWidth: number,\n targetHeight: number,\n normalization: 'zero-one' | 'imagenet' | 'none',\n layout: 'nchw' | 'nhwc',\n): Promise<Float32Array> {\n const { data } = await sharp(jpeg)\n .resize(targetWidth, targetHeight)\n .removeAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n const numPixels = targetWidth * targetHeight\n const float32 = new Float32Array(3 * numPixels)\n\n // ImageNet mean and std per channel\n const mean = [0.485, 0.456, 0.406]\n const std = [0.229, 0.224, 0.225]\n\n if (layout === 'nchw') {\n for (let i = 0; i < numPixels; i++) {\n const srcBase = i * 3\n for (let c = 0; c < 3; c++) {\n const raw = data[srcBase + c]! / 255\n let val: number\n if (normalization === 'zero-one') {\n val = raw\n } else if (normalization === 'imagenet') {\n val = (raw - mean[c]!) / std[c]!\n } else {\n val = data[srcBase + c]!\n }\n float32[c * numPixels + i] = val\n }\n }\n } else {\n // nhwc\n for (let i = 0; i < numPixels; i++) {\n const srcBase = i * 3\n for (let c = 0; c < 3; c++) {\n const raw = data[srcBase + c]! / 255\n let val: number\n if (normalization === 'zero-one') {\n val = raw\n } else if (normalization === 'imagenet') {\n val = (raw - mean[c]!) / std[c]!\n } else {\n val = data[srcBase + c]!\n }\n float32[i * 3 + c] = val\n }\n }\n }\n\n return float32\n}\n\n/** Convert raw RGB to grayscale Uint8Array */\nexport function rgbToGrayscale(rgb: Buffer, width: number, height: number): Uint8Array {\n const numPixels = width * height\n const gray = new Uint8Array(numPixels)\n for (let i = 0; i < numPixels; i++) {\n const r = rgb[i * 3]!\n const g = rgb[i * 3 + 1]!\n const b = rgb[i * 3 + 2]!\n // BT.601 luma\n gray[i] = Math.round(0.299 * r + 0.587 * g + 0.114 * b)\n }\n return gray\n}\n","import type { SpatialDetection, BoundingBox } from '@camstack/types'\n\nexport interface YoloPostprocessOptions {\n readonly confidence: number\n readonly iouThreshold: number\n readonly labels: readonly string[]\n readonly scale: number\n readonly padX: number\n readonly padY: number\n readonly originalWidth: number\n readonly originalHeight: number\n}\n\n/** Calculate IoU between two bounding boxes */\nexport function iou(a: BoundingBox, b: BoundingBox): number {\n const ax1 = a.x\n const ay1 = a.y\n const ax2 = a.x + a.w\n const ay2 = a.y + a.h\n\n const bx1 = b.x\n const by1 = b.y\n const bx2 = b.x + b.w\n const by2 = b.y + b.h\n\n const interX1 = Math.max(ax1, bx1)\n const interY1 = Math.max(ay1, by1)\n const interX2 = Math.min(ax2, bx2)\n const interY2 = Math.min(ay2, by2)\n\n const interW = Math.max(0, interX2 - interX1)\n const interH = Math.max(0, interY2 - interY1)\n const interArea = interW * interH\n\n if (interArea === 0) return 0\n\n const areaA = a.w * a.h\n const areaB = b.w * b.h\n const unionArea = areaA + areaB - interArea\n\n return unionArea === 0 ? 0 : interArea / unionArea\n}\n\n/** Non-maximum suppression — returns indices of kept boxes (sorted by score desc) */\nexport function nms(\n boxes: ReadonlyArray<{ readonly bbox: BoundingBox; readonly score: number }>,\n iouThreshold: number,\n): number[] {\n const indices = boxes\n .map((_, i) => i)\n .sort((a, b) => (boxes[b]!.score) - (boxes[a]!.score))\n\n const kept: number[] = []\n const suppressed = new Set<number>()\n\n for (const idx of indices) {\n if (suppressed.has(idx)) continue\n kept.push(idx)\n for (const other of indices) {\n if (other === idx || suppressed.has(other)) continue\n if (iou(boxes[idx]!.bbox, boxes[other]!.bbox) > iouThreshold) {\n suppressed.add(other)\n }\n }\n }\n\n return kept\n}\n\n/** Full YOLO v8/v9 postprocessing: filter → NMS → scale back to original coords */\nexport function yoloPostprocess(\n output: Float32Array,\n numClasses: number,\n numBoxes: number,\n options: YoloPostprocessOptions,\n): SpatialDetection[] {\n const { confidence, iouThreshold, labels, scale, padX, padY, originalWidth, originalHeight } = options\n\n interface Candidate {\n readonly bbox: BoundingBox\n readonly score: number\n readonly classIdx: number\n }\n\n const candidates: Candidate[] = []\n\n for (let i = 0; i < numBoxes; i++) {\n // YOLO v8/v9 output layout: [1, 4+numClasses, numBoxes] stored row-major\n const cx = output[0 * numBoxes + i]!\n const cy = output[1 * numBoxes + i]!\n const w = output[2 * numBoxes + i]!\n const h = output[3 * numBoxes + i]!\n\n let bestScore = -Infinity\n let bestClass = 0\n\n for (let j = 0; j < numClasses; j++) {\n const score = output[(4 + j) * numBoxes + i]!\n if (score > bestScore) {\n bestScore = score\n bestClass = j\n }\n }\n\n if (bestScore < confidence) continue\n\n // Convert cx,cy,w,h to x,y,w,h (top-left origin)\n const bbox: BoundingBox = {\n x: cx - w / 2,\n y: cy - h / 2,\n w,\n h,\n }\n\n candidates.push({ bbox, score: bestScore, classIdx: bestClass })\n }\n\n if (candidates.length === 0) return []\n\n const keptIndices = nms(candidates, iouThreshold)\n\n return keptIndices.map((idx) => {\n const { bbox, score, classIdx } = candidates[idx]!\n const label = labels[classIdx] ?? String(classIdx)\n\n // Transform from letterbox coords back to original image coords\n const x = Math.max(0, Math.min(originalWidth, (bbox.x - padX) / scale))\n const y = Math.max(0, Math.min(originalHeight, (bbox.y - padY) / scale))\n const x2 = Math.max(0, Math.min(originalWidth, (bbox.x + bbox.w - padX) / scale))\n const y2 = Math.max(0, Math.min(originalHeight, (bbox.y + bbox.h - padY) / scale))\n\n const finalBbox: BoundingBox = { x, y, w: x2 - x, h: y2 - y }\n\n return {\n class: label,\n originalClass: label,\n score,\n bbox: finalBbox,\n } satisfies SpatialDetection\n })\n}\n","import type {\n IInferenceEngine,\n DetectionRuntime,\n ModelCatalogEntry,\n ModelFormat,\n} from '@camstack/types'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { NodeInferenceEngine } from './node-engine.js'\n\nexport interface EngineResolverOptions {\n readonly runtime: DetectionRuntime | 'auto'\n readonly backend: string\n readonly modelEntry: ModelCatalogEntry\n readonly modelsDir: string\n readonly pythonPath?: string\n readonly downloadModel?: (url: string, destDir: string) => Promise<string>\n}\n\nexport interface ResolvedEngine {\n readonly engine: IInferenceEngine\n readonly format: ModelFormat\n readonly modelPath: string\n}\n\n/** Priority order for auto-selection of ONNX backends */\nconst AUTO_BACKEND_PRIORITY = ['coreml', 'cuda', 'tensorrt', 'cpu'] as const\n\n/** Map backend names to the model format they require */\nconst BACKEND_TO_FORMAT: Readonly<Record<string, ModelFormat>> = {\n cpu: 'onnx',\n coreml: 'coreml',\n cuda: 'onnx',\n tensorrt: 'onnx',\n} as const\n\n/** Map DetectionRuntime to ModelFormat */\nconst RUNTIME_TO_FORMAT: Readonly<Partial<Record<DetectionRuntime, ModelFormat>>> = {\n onnx: 'onnx',\n coreml: 'coreml',\n openvino: 'openvino',\n tflite: 'tflite',\n pytorch: 'pt',\n} as const\n\nfunction modelFilePath(modelsDir: string, modelEntry: ModelCatalogEntry, format: ModelFormat): string {\n const formatEntry = modelEntry.formats[format]\n if (!formatEntry) {\n throw new Error(`Model ${modelEntry.id} has no ${format} format`)\n }\n // Derive filename from URL\n const urlParts = formatEntry.url.split('/')\n const filename = urlParts[urlParts.length - 1] ?? `${modelEntry.id}.${format}`\n return path.join(modelsDir, filename)\n}\n\nfunction modelExists(filePath: string): boolean {\n try {\n return fs.existsSync(filePath)\n } catch {\n return false\n }\n}\n\nexport async function resolveEngine(options: EngineResolverOptions): Promise<ResolvedEngine> {\n const { runtime, backend, modelEntry, modelsDir, downloadModel } = options\n\n let selectedFormat: ModelFormat\n let selectedBackend: string\n\n if (runtime === 'auto') {\n // Probe available ONNX backends and pick best\n const available = await probeOnnxBackends()\n\n // Pick first priority backend that has a corresponding model format available\n let chosen: { backend: string; format: ModelFormat } | null = null\n\n for (const b of AUTO_BACKEND_PRIORITY) {\n if (!available.includes(b)) continue\n const fmt = BACKEND_TO_FORMAT[b]\n if (!fmt) continue\n if (!modelEntry.formats[fmt]) continue\n chosen = { backend: b, format: fmt }\n break\n }\n\n if (!chosen) {\n throw new Error(\n `resolveEngine: no compatible backend found for model ${modelEntry.id}. Available backends: ${available.join(', ')}`,\n )\n }\n\n selectedFormat = chosen.format\n selectedBackend = chosen.backend\n } else {\n // Explicit runtime requested\n const fmt = RUNTIME_TO_FORMAT[runtime]\n if (!fmt) {\n throw new Error(`resolveEngine: unsupported runtime \"${runtime}\"`)\n }\n if (!modelEntry.formats[fmt]) {\n throw new Error(\n `resolveEngine: model ${modelEntry.id} has no ${fmt} format for runtime ${runtime}`,\n )\n }\n selectedFormat = fmt\n // For onnx runtime, use the provided backend; otherwise use the runtime name\n selectedBackend = runtime === 'onnx' ? (backend || 'cpu') : runtime\n }\n\n // Resolve model path\n let modelPath = modelFilePath(modelsDir, modelEntry, selectedFormat)\n\n if (!modelExists(modelPath)) {\n if (downloadModel) {\n const formatEntry = modelEntry.formats[selectedFormat]!\n modelPath = await downloadModel(formatEntry.url, modelsDir)\n } else {\n throw new Error(\n `resolveEngine: model file not found at ${modelPath} and no downloadModel function provided`,\n )\n }\n }\n\n // Only ONNX runtime is handled by NodeInferenceEngine currently\n if (selectedFormat === 'onnx' || selectedFormat === 'coreml') {\n const engine = new NodeInferenceEngine(modelPath, selectedBackend)\n await engine.initialize()\n return { engine, format: selectedFormat, modelPath }\n }\n\n // For other formats (openvino, tflite, pt), fall back to ONNX cpu with a warning\n // until those engines are implemented\n const fallbackPath = modelFilePath(modelsDir, modelEntry, 'onnx')\n if (modelEntry.formats['onnx'] && modelExists(fallbackPath)) {\n const engine = new NodeInferenceEngine(fallbackPath, 'cpu')\n await engine.initialize()\n return { engine, format: 'onnx', modelPath: fallbackPath }\n }\n\n throw new Error(\n `resolveEngine: format ${selectedFormat} is not yet supported by NodeInferenceEngine and no ONNX fallback is available`,\n )\n}\n\n/** Probe which ONNX execution providers are available on this system */\nexport async function probeOnnxBackends(): Promise<string[]> {\n const available: string[] = ['cpu'] // CPU is always available\n\n try {\n const ort = await import('onnxruntime-node')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const providers: string[] = (ort as any).env?.webgl?.disabled !== undefined\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ? ((ort as any).InferenceSession?.getAvailableProviders?.() ?? [])\n : []\n\n for (const p of providers) {\n const normalized = p.toLowerCase().replace('executionprovider', '')\n if (normalized === 'coreml') available.push('coreml')\n else if (normalized === 'cuda') available.push('cuda')\n else if (normalized === 'tensorrt') available.push('tensorrt')\n }\n } catch {\n // onnxruntime-node may not be installed; CPU only\n }\n\n // Platform-specific hints when getAvailableProviders isn't exposed\n if (process.platform === 'darwin' && !available.includes('coreml')) {\n available.push('coreml')\n }\n\n return [...new Set(available)]\n}\n","import type { IInferenceEngine, DetectionRuntime, DetectionDevice } from '@camstack/types'\nimport * as path from 'node:path'\n\nconst BACKEND_TO_PROVIDER: Readonly<Record<string, string>> = {\n cpu: 'cpu',\n coreml: 'coreml',\n cuda: 'cuda',\n tensorrt: 'tensorrt',\n dml: 'dml',\n} as const\n\nconst BACKEND_TO_DEVICE: Readonly<Record<string, DetectionDevice>> = {\n cpu: 'cpu',\n coreml: 'gpu-mps',\n cuda: 'gpu-cuda',\n tensorrt: 'tensorrt',\n} as const\n\nexport class NodeInferenceEngine implements IInferenceEngine {\n readonly runtime: DetectionRuntime = 'onnx'\n readonly device: DetectionDevice\n private session: unknown = null\n\n constructor(\n private readonly modelPath: string,\n private readonly backend: string,\n ) {\n this.device = (BACKEND_TO_DEVICE[backend] ?? 'cpu') as DetectionDevice\n }\n\n async initialize(): Promise<void> {\n const ort = await import('onnxruntime-node')\n const provider = BACKEND_TO_PROVIDER[this.backend] ?? 'cpu'\n\n // Resolve absolute path\n const absModelPath = path.isAbsolute(this.modelPath)\n ? this.modelPath\n : path.resolve(process.cwd(), this.modelPath)\n\n const sessionOptions: Record<string, unknown> = {\n executionProviders: [provider],\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this.session = await (ort as any).InferenceSession.create(absModelPath, sessionOptions)\n }\n\n async run(input: Float32Array, inputShape: readonly number[]): Promise<Float32Array> {\n if (!this.session) {\n throw new Error('NodeInferenceEngine: not initialized — call initialize() first')\n }\n\n const ort = await import('onnxruntime-node')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const sess = this.session as any\n\n // Get the first input name\n const inputName: string = sess.inputNames[0]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tensor = new (ort as any).Tensor('float32', input, [...inputShape])\n const feeds: Record<string, unknown> = { [inputName]: tensor }\n\n const results = await sess.run(feeds)\n const outputName: string = sess.outputNames[0]\n const outputTensor = results[outputName]\n\n return outputTensor.data as Float32Array\n }\n\n async runMultiOutput(\n input: Float32Array,\n inputShape: readonly number[],\n ): Promise<Record<string, Float32Array>> {\n if (!this.session) {\n throw new Error('NodeInferenceEngine: not initialized — call initialize() first')\n }\n\n const ort = await import('onnxruntime-node')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const sess = this.session as any\n\n const inputName: string = sess.inputNames[0]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tensor = new (ort as any).Tensor('float32', input, [...inputShape])\n const feeds: Record<string, unknown> = { [inputName]: tensor }\n\n const results = await sess.run(feeds)\n\n const out: Record<string, Float32Array> = {}\n for (const name of sess.outputNames as string[]) {\n out[name] = results[name].data as Float32Array\n }\n return out\n }\n\n async dispose(): Promise<void> {\n // onnxruntime-node sessions don't have explicit close in all versions\n // but we clear the reference\n this.session = null\n }\n}\n"],"mappings":"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","names":["sharp","path"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PlateDetectionAddon
|
|
3
|
+
} from "../../chunk-PXBY3QOA.mjs";
|
|
4
|
+
import "../../chunk-KUO2BVFY.mjs";
|
|
5
|
+
import "../../chunk-6OR5TE7A.mjs";
|
|
6
|
+
import "../../chunk-J3IUBPRE.mjs";
|
|
7
|
+
export {
|
|
8
|
+
PlateDetectionAddon as default
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { IClassifierProvider, IDetectionAddon, RequiredStep, AddonManifest, AddonContext, CropInput, ClassifierOutput, ConfigUISchema, ClassMapDefinition, ModelCatalogEntry, DetectionModel, LabelDefinition, ProbeResult } from '@camstack/types';
|
|
2
|
+
|
|
3
|
+
declare class PlateRecognitionAddon implements IClassifierProvider, IDetectionAddon {
|
|
4
|
+
readonly id = "plate-recognition";
|
|
5
|
+
readonly slot: "classifier";
|
|
6
|
+
readonly inputClasses: readonly ["plate"];
|
|
7
|
+
readonly outputClasses: readonly ["plate-text:*"];
|
|
8
|
+
readonly slotPriority = 0;
|
|
9
|
+
readonly requiredSteps: readonly RequiredStep[];
|
|
10
|
+
readonly manifest: AddonManifest;
|
|
11
|
+
private engine;
|
|
12
|
+
private modelEntry;
|
|
13
|
+
private minConfidence;
|
|
14
|
+
initialize(ctx: AddonContext): Promise<void>;
|
|
15
|
+
classify(input: CropInput): Promise<ClassifierOutput>;
|
|
16
|
+
shutdown(): Promise<void>;
|
|
17
|
+
getConfigSchema(): ConfigUISchema;
|
|
18
|
+
getClassMap(): ClassMapDefinition;
|
|
19
|
+
getModelCatalog(): ModelCatalogEntry[];
|
|
20
|
+
getAvailableModels(): DetectionModel[];
|
|
21
|
+
getActiveLabels(): readonly LabelDefinition[];
|
|
22
|
+
probe(): Promise<ProbeResult>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { PlateRecognitionAddon as default };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { IClassifierProvider, IDetectionAddon, RequiredStep, AddonManifest, AddonContext, CropInput, ClassifierOutput, ConfigUISchema, ClassMapDefinition, ModelCatalogEntry, DetectionModel, LabelDefinition, ProbeResult } from '@camstack/types';
|
|
2
|
+
|
|
3
|
+
declare class PlateRecognitionAddon implements IClassifierProvider, IDetectionAddon {
|
|
4
|
+
readonly id = "plate-recognition";
|
|
5
|
+
readonly slot: "classifier";
|
|
6
|
+
readonly inputClasses: readonly ["plate"];
|
|
7
|
+
readonly outputClasses: readonly ["plate-text:*"];
|
|
8
|
+
readonly slotPriority = 0;
|
|
9
|
+
readonly requiredSteps: readonly RequiredStep[];
|
|
10
|
+
readonly manifest: AddonManifest;
|
|
11
|
+
private engine;
|
|
12
|
+
private modelEntry;
|
|
13
|
+
private minConfidence;
|
|
14
|
+
initialize(ctx: AddonContext): Promise<void>;
|
|
15
|
+
classify(input: CropInput): Promise<ClassifierOutput>;
|
|
16
|
+
shutdown(): Promise<void>;
|
|
17
|
+
getConfigSchema(): ConfigUISchema;
|
|
18
|
+
getClassMap(): ClassMapDefinition;
|
|
19
|
+
getModelCatalog(): ModelCatalogEntry[];
|
|
20
|
+
getAvailableModels(): DetectionModel[];
|
|
21
|
+
getActiveLabels(): readonly LabelDefinition[];
|
|
22
|
+
probe(): Promise<ProbeResult>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { PlateRecognitionAddon as default };
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/addons/plate-recognition/index.ts
|
|
31
|
+
var plate_recognition_exports = {};
|
|
32
|
+
__export(plate_recognition_exports, {
|
|
33
|
+
default: () => PlateRecognitionAddon
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(plate_recognition_exports);
|
|
36
|
+
var import_types = require("@camstack/types");
|
|
37
|
+
|
|
38
|
+
// src/shared/image-utils.ts
|
|
39
|
+
var import_sharp = __toESM(require("sharp"));
|
|
40
|
+
async function cropRegion(jpeg, roi) {
|
|
41
|
+
return (0, import_sharp.default)(jpeg).extract({
|
|
42
|
+
left: Math.round(roi.x),
|
|
43
|
+
top: Math.round(roi.y),
|
|
44
|
+
width: Math.round(roi.w),
|
|
45
|
+
height: Math.round(roi.h)
|
|
46
|
+
}).jpeg().toBuffer();
|
|
47
|
+
}
|
|
48
|
+
async function resizeAndNormalize(jpeg, targetWidth, targetHeight, normalization, layout) {
|
|
49
|
+
const { data } = await (0, import_sharp.default)(jpeg).resize(targetWidth, targetHeight).removeAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
50
|
+
const numPixels = targetWidth * targetHeight;
|
|
51
|
+
const float32 = new Float32Array(3 * numPixels);
|
|
52
|
+
const mean = [0.485, 0.456, 0.406];
|
|
53
|
+
const std = [0.229, 0.224, 0.225];
|
|
54
|
+
if (layout === "nchw") {
|
|
55
|
+
for (let i = 0; i < numPixels; i++) {
|
|
56
|
+
const srcBase = i * 3;
|
|
57
|
+
for (let c = 0; c < 3; c++) {
|
|
58
|
+
const raw = data[srcBase + c] / 255;
|
|
59
|
+
let val;
|
|
60
|
+
if (normalization === "zero-one") {
|
|
61
|
+
val = raw;
|
|
62
|
+
} else if (normalization === "imagenet") {
|
|
63
|
+
val = (raw - mean[c]) / std[c];
|
|
64
|
+
} else {
|
|
65
|
+
val = data[srcBase + c];
|
|
66
|
+
}
|
|
67
|
+
float32[c * numPixels + i] = val;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
for (let i = 0; i < numPixels; i++) {
|
|
72
|
+
const srcBase = i * 3;
|
|
73
|
+
for (let c = 0; c < 3; c++) {
|
|
74
|
+
const raw = data[srcBase + c] / 255;
|
|
75
|
+
let val;
|
|
76
|
+
if (normalization === "zero-one") {
|
|
77
|
+
val = raw;
|
|
78
|
+
} else if (normalization === "imagenet") {
|
|
79
|
+
val = (raw - mean[c]) / std[c];
|
|
80
|
+
} else {
|
|
81
|
+
val = data[srcBase + c];
|
|
82
|
+
}
|
|
83
|
+
float32[i * 3 + c] = val;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return float32;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/shared/postprocess/paddleocr.ts
|
|
91
|
+
function ctcDecode(output, seqLen, numChars, charset) {
|
|
92
|
+
let totalLogScore = 0;
|
|
93
|
+
const rawIndices = [];
|
|
94
|
+
for (let t = 0; t < seqLen; t++) {
|
|
95
|
+
const offset = t * numChars;
|
|
96
|
+
let bestIdx = 0;
|
|
97
|
+
let bestVal = output[offset];
|
|
98
|
+
for (let c = 1; c < numChars; c++) {
|
|
99
|
+
const val = output[offset + c];
|
|
100
|
+
if (val > bestVal) {
|
|
101
|
+
bestVal = val;
|
|
102
|
+
bestIdx = c;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
rawIndices.push(bestIdx);
|
|
106
|
+
totalLogScore += bestVal;
|
|
107
|
+
}
|
|
108
|
+
const collapsed = [];
|
|
109
|
+
for (let i = 0; i < rawIndices.length; i++) {
|
|
110
|
+
const cur = rawIndices[i];
|
|
111
|
+
if (i === 0 || cur !== rawIndices[i - 1]) {
|
|
112
|
+
collapsed.push(cur);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const filtered = collapsed.filter((idx) => idx !== 0);
|
|
116
|
+
const text = filtered.map((idx) => charset[idx] ?? "").join("");
|
|
117
|
+
const confidence = seqLen > 0 ? totalLogScore / seqLen : 0;
|
|
118
|
+
return { text, confidence };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/shared/engine-resolver.ts
|
|
122
|
+
var fs = __toESM(require("fs"));
|
|
123
|
+
var path2 = __toESM(require("path"));
|
|
124
|
+
|
|
125
|
+
// src/shared/node-engine.ts
|
|
126
|
+
var path = __toESM(require("path"));
|
|
127
|
+
var BACKEND_TO_PROVIDER = {
|
|
128
|
+
cpu: "cpu",
|
|
129
|
+
coreml: "coreml",
|
|
130
|
+
cuda: "cuda",
|
|
131
|
+
tensorrt: "tensorrt",
|
|
132
|
+
dml: "dml"
|
|
133
|
+
};
|
|
134
|
+
var BACKEND_TO_DEVICE = {
|
|
135
|
+
cpu: "cpu",
|
|
136
|
+
coreml: "gpu-mps",
|
|
137
|
+
cuda: "gpu-cuda",
|
|
138
|
+
tensorrt: "tensorrt"
|
|
139
|
+
};
|
|
140
|
+
var NodeInferenceEngine = class {
|
|
141
|
+
constructor(modelPath, backend) {
|
|
142
|
+
this.modelPath = modelPath;
|
|
143
|
+
this.backend = backend;
|
|
144
|
+
this.device = BACKEND_TO_DEVICE[backend] ?? "cpu";
|
|
145
|
+
}
|
|
146
|
+
runtime = "onnx";
|
|
147
|
+
device;
|
|
148
|
+
session = null;
|
|
149
|
+
async initialize() {
|
|
150
|
+
const ort = await import("onnxruntime-node");
|
|
151
|
+
const provider = BACKEND_TO_PROVIDER[this.backend] ?? "cpu";
|
|
152
|
+
const absModelPath = path.isAbsolute(this.modelPath) ? this.modelPath : path.resolve(process.cwd(), this.modelPath);
|
|
153
|
+
const sessionOptions = {
|
|
154
|
+
executionProviders: [provider]
|
|
155
|
+
};
|
|
156
|
+
this.session = await ort.InferenceSession.create(absModelPath, sessionOptions);
|
|
157
|
+
}
|
|
158
|
+
async run(input, inputShape) {
|
|
159
|
+
if (!this.session) {
|
|
160
|
+
throw new Error("NodeInferenceEngine: not initialized \u2014 call initialize() first");
|
|
161
|
+
}
|
|
162
|
+
const ort = await import("onnxruntime-node");
|
|
163
|
+
const sess = this.session;
|
|
164
|
+
const inputName = sess.inputNames[0];
|
|
165
|
+
const tensor = new ort.Tensor("float32", input, [...inputShape]);
|
|
166
|
+
const feeds = { [inputName]: tensor };
|
|
167
|
+
const results = await sess.run(feeds);
|
|
168
|
+
const outputName = sess.outputNames[0];
|
|
169
|
+
const outputTensor = results[outputName];
|
|
170
|
+
return outputTensor.data;
|
|
171
|
+
}
|
|
172
|
+
async runMultiOutput(input, inputShape) {
|
|
173
|
+
if (!this.session) {
|
|
174
|
+
throw new Error("NodeInferenceEngine: not initialized \u2014 call initialize() first");
|
|
175
|
+
}
|
|
176
|
+
const ort = await import("onnxruntime-node");
|
|
177
|
+
const sess = this.session;
|
|
178
|
+
const inputName = sess.inputNames[0];
|
|
179
|
+
const tensor = new ort.Tensor("float32", input, [...inputShape]);
|
|
180
|
+
const feeds = { [inputName]: tensor };
|
|
181
|
+
const results = await sess.run(feeds);
|
|
182
|
+
const out = {};
|
|
183
|
+
for (const name of sess.outputNames) {
|
|
184
|
+
out[name] = results[name].data;
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
async dispose() {
|
|
189
|
+
this.session = null;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/shared/engine-resolver.ts
|
|
194
|
+
var AUTO_BACKEND_PRIORITY = ["coreml", "cuda", "tensorrt", "cpu"];
|
|
195
|
+
var BACKEND_TO_FORMAT = {
|
|
196
|
+
cpu: "onnx",
|
|
197
|
+
coreml: "coreml",
|
|
198
|
+
cuda: "onnx",
|
|
199
|
+
tensorrt: "onnx"
|
|
200
|
+
};
|
|
201
|
+
var RUNTIME_TO_FORMAT = {
|
|
202
|
+
onnx: "onnx",
|
|
203
|
+
coreml: "coreml",
|
|
204
|
+
openvino: "openvino",
|
|
205
|
+
tflite: "tflite",
|
|
206
|
+
pytorch: "pt"
|
|
207
|
+
};
|
|
208
|
+
function modelFilePath(modelsDir, modelEntry, format) {
|
|
209
|
+
const formatEntry = modelEntry.formats[format];
|
|
210
|
+
if (!formatEntry) {
|
|
211
|
+
throw new Error(`Model ${modelEntry.id} has no ${format} format`);
|
|
212
|
+
}
|
|
213
|
+
const urlParts = formatEntry.url.split("/");
|
|
214
|
+
const filename = urlParts[urlParts.length - 1] ?? `${modelEntry.id}.${format}`;
|
|
215
|
+
return path2.join(modelsDir, filename);
|
|
216
|
+
}
|
|
217
|
+
function modelExists(filePath) {
|
|
218
|
+
try {
|
|
219
|
+
return fs.existsSync(filePath);
|
|
220
|
+
} catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function resolveEngine(options) {
|
|
225
|
+
const { runtime, backend, modelEntry, modelsDir, downloadModel } = options;
|
|
226
|
+
let selectedFormat;
|
|
227
|
+
let selectedBackend;
|
|
228
|
+
if (runtime === "auto") {
|
|
229
|
+
const available = await probeOnnxBackends();
|
|
230
|
+
let chosen = null;
|
|
231
|
+
for (const b of AUTO_BACKEND_PRIORITY) {
|
|
232
|
+
if (!available.includes(b)) continue;
|
|
233
|
+
const fmt = BACKEND_TO_FORMAT[b];
|
|
234
|
+
if (!fmt) continue;
|
|
235
|
+
if (!modelEntry.formats[fmt]) continue;
|
|
236
|
+
chosen = { backend: b, format: fmt };
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
if (!chosen) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`resolveEngine: no compatible backend found for model ${modelEntry.id}. Available backends: ${available.join(", ")}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
selectedFormat = chosen.format;
|
|
245
|
+
selectedBackend = chosen.backend;
|
|
246
|
+
} else {
|
|
247
|
+
const fmt = RUNTIME_TO_FORMAT[runtime];
|
|
248
|
+
if (!fmt) {
|
|
249
|
+
throw new Error(`resolveEngine: unsupported runtime "${runtime}"`);
|
|
250
|
+
}
|
|
251
|
+
if (!modelEntry.formats[fmt]) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`resolveEngine: model ${modelEntry.id} has no ${fmt} format for runtime ${runtime}`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
selectedFormat = fmt;
|
|
257
|
+
selectedBackend = runtime === "onnx" ? backend || "cpu" : runtime;
|
|
258
|
+
}
|
|
259
|
+
let modelPath = modelFilePath(modelsDir, modelEntry, selectedFormat);
|
|
260
|
+
if (!modelExists(modelPath)) {
|
|
261
|
+
if (downloadModel) {
|
|
262
|
+
const formatEntry = modelEntry.formats[selectedFormat];
|
|
263
|
+
modelPath = await downloadModel(formatEntry.url, modelsDir);
|
|
264
|
+
} else {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`resolveEngine: model file not found at ${modelPath} and no downloadModel function provided`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (selectedFormat === "onnx" || selectedFormat === "coreml") {
|
|
271
|
+
const engine = new NodeInferenceEngine(modelPath, selectedBackend);
|
|
272
|
+
await engine.initialize();
|
|
273
|
+
return { engine, format: selectedFormat, modelPath };
|
|
274
|
+
}
|
|
275
|
+
const fallbackPath = modelFilePath(modelsDir, modelEntry, "onnx");
|
|
276
|
+
if (modelEntry.formats["onnx"] && modelExists(fallbackPath)) {
|
|
277
|
+
const engine = new NodeInferenceEngine(fallbackPath, "cpu");
|
|
278
|
+
await engine.initialize();
|
|
279
|
+
return { engine, format: "onnx", modelPath: fallbackPath };
|
|
280
|
+
}
|
|
281
|
+
throw new Error(
|
|
282
|
+
`resolveEngine: format ${selectedFormat} is not yet supported by NodeInferenceEngine and no ONNX fallback is available`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
async function probeOnnxBackends() {
|
|
286
|
+
const available = ["cpu"];
|
|
287
|
+
try {
|
|
288
|
+
const ort = await import("onnxruntime-node");
|
|
289
|
+
const providers = ort.env?.webgl?.disabled !== void 0 ? ort.InferenceSession?.getAvailableProviders?.() ?? [] : [];
|
|
290
|
+
for (const p of providers) {
|
|
291
|
+
const normalized = p.toLowerCase().replace("executionprovider", "");
|
|
292
|
+
if (normalized === "coreml") available.push("coreml");
|
|
293
|
+
else if (normalized === "cuda") available.push("cuda");
|
|
294
|
+
else if (normalized === "tensorrt") available.push("tensorrt");
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
if (process.platform === "darwin" && !available.includes("coreml")) {
|
|
299
|
+
available.push("coreml");
|
|
300
|
+
}
|
|
301
|
+
return [...new Set(available)];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/addons/plate-recognition/index.ts
|
|
305
|
+
var fs2 = __toESM(require("fs"));
|
|
306
|
+
var path3 = __toESM(require("path"));
|
|
307
|
+
var PLATE_TEXT_LABEL = { id: "plate-text", name: "Plate Text" };
|
|
308
|
+
var PLATE_TEXT_LABELS = [PLATE_TEXT_LABEL];
|
|
309
|
+
var PLATE_REC_CLASS_MAP = { mapping: {}, preserveOriginal: true };
|
|
310
|
+
function loadCharset(modelsDir, modelId) {
|
|
311
|
+
const dictNames = [
|
|
312
|
+
`camstack-${modelId}-dict.txt`,
|
|
313
|
+
`camstack-paddleocr-latin-dict.txt`,
|
|
314
|
+
`camstack-paddleocr-en-dict.txt`
|
|
315
|
+
];
|
|
316
|
+
for (const name of dictNames) {
|
|
317
|
+
const dictPath = path3.join(modelsDir, name);
|
|
318
|
+
if (fs2.existsSync(dictPath)) {
|
|
319
|
+
const lines = fs2.readFileSync(dictPath, "utf-8").split("\n").filter((l) => l.length > 0);
|
|
320
|
+
return ["", ...lines, " "];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
throw new Error(`PlateRecognitionAddon: dict.txt not found in ${modelsDir}`);
|
|
324
|
+
}
|
|
325
|
+
var CHARSET = [];
|
|
326
|
+
var REQUIRED_STEPS = [
|
|
327
|
+
{ slot: "cropper", outputClasses: ["plate"], description: "Requires a plate detector" }
|
|
328
|
+
];
|
|
329
|
+
var PlateRecognitionAddon = class {
|
|
330
|
+
id = "plate-recognition";
|
|
331
|
+
slot = "classifier";
|
|
332
|
+
inputClasses = ["plate"];
|
|
333
|
+
outputClasses = ["plate-text:*"];
|
|
334
|
+
slotPriority = 0;
|
|
335
|
+
requiredSteps = REQUIRED_STEPS;
|
|
336
|
+
manifest = {
|
|
337
|
+
id: "plate-recognition",
|
|
338
|
+
name: "License Plate Recognition (OCR)",
|
|
339
|
+
version: "0.1.0",
|
|
340
|
+
description: "PaddleOCR-based license plate text recognition",
|
|
341
|
+
packageName: "@camstack/vision",
|
|
342
|
+
slot: "classifier",
|
|
343
|
+
inputClasses: ["plate"],
|
|
344
|
+
outputClasses: ["plate-text:*"],
|
|
345
|
+
requiredSteps: REQUIRED_STEPS,
|
|
346
|
+
supportsCustomModels: false,
|
|
347
|
+
mayRequirePython: false,
|
|
348
|
+
defaultConfig: {
|
|
349
|
+
modelId: "paddleocr-latin",
|
|
350
|
+
runtime: "auto",
|
|
351
|
+
backend: "cpu",
|
|
352
|
+
minConfidence: 0.5
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
engine;
|
|
356
|
+
modelEntry;
|
|
357
|
+
minConfidence = 0.5;
|
|
358
|
+
async initialize(ctx) {
|
|
359
|
+
const cfg = ctx.addonConfig;
|
|
360
|
+
const modelId = cfg["modelId"] ?? "paddleocr-latin";
|
|
361
|
+
const runtime = cfg["runtime"] ?? "auto";
|
|
362
|
+
const backend = cfg["backend"] ?? "cpu";
|
|
363
|
+
this.minConfidence = cfg["minConfidence"] ?? 0.5;
|
|
364
|
+
const entry = import_types.PLATE_RECOGNITION_MODELS.find((m) => m.id === modelId);
|
|
365
|
+
if (!entry) {
|
|
366
|
+
throw new Error(`PlateRecognitionAddon: unknown modelId "${modelId}"`);
|
|
367
|
+
}
|
|
368
|
+
this.modelEntry = entry;
|
|
369
|
+
CHARSET = loadCharset(ctx.locationPaths.models, modelId);
|
|
370
|
+
const resolved = await resolveEngine({
|
|
371
|
+
runtime,
|
|
372
|
+
backend,
|
|
373
|
+
modelEntry: entry,
|
|
374
|
+
modelsDir: ctx.locationPaths.models
|
|
375
|
+
});
|
|
376
|
+
this.engine = resolved.engine;
|
|
377
|
+
}
|
|
378
|
+
async classify(input) {
|
|
379
|
+
const start = Date.now();
|
|
380
|
+
const { width: inputW, height: inputH } = this.modelEntry.inputSize;
|
|
381
|
+
const plateCrop = await cropRegion(input.frame.data, input.roi);
|
|
382
|
+
const normalized = await resizeAndNormalize(plateCrop, inputW, inputH, "zero-one", "nchw");
|
|
383
|
+
const output = await this.engine.run(normalized, [1, 3, inputH, inputW]);
|
|
384
|
+
const numChars = CHARSET.length;
|
|
385
|
+
const seqLen = output.length / numChars;
|
|
386
|
+
const { text, confidence } = ctcDecode(output, seqLen, numChars, CHARSET);
|
|
387
|
+
if (confidence < this.minConfidence || text.trim().length === 0) {
|
|
388
|
+
return {
|
|
389
|
+
classifications: [],
|
|
390
|
+
inferenceMs: Date.now() - start,
|
|
391
|
+
modelId: this.modelEntry.id
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
classifications: [
|
|
396
|
+
{
|
|
397
|
+
class: "plate-text",
|
|
398
|
+
score: confidence,
|
|
399
|
+
text
|
|
400
|
+
}
|
|
401
|
+
],
|
|
402
|
+
inferenceMs: Date.now() - start,
|
|
403
|
+
modelId: this.modelEntry.id
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
async shutdown() {
|
|
407
|
+
await this.engine?.dispose();
|
|
408
|
+
}
|
|
409
|
+
getConfigSchema() {
|
|
410
|
+
return {
|
|
411
|
+
sections: [
|
|
412
|
+
{
|
|
413
|
+
id: "model",
|
|
414
|
+
title: "Model",
|
|
415
|
+
columns: 1,
|
|
416
|
+
fields: [
|
|
417
|
+
{
|
|
418
|
+
key: "modelId",
|
|
419
|
+
label: "Model",
|
|
420
|
+
type: "model-selector",
|
|
421
|
+
catalog: [...import_types.PLATE_RECOGNITION_MODELS],
|
|
422
|
+
allowCustom: false,
|
|
423
|
+
allowConversion: false,
|
|
424
|
+
acceptFormats: ["onnx", "openvino"],
|
|
425
|
+
requiredMetadata: ["inputSize", "labels", "outputFormat"],
|
|
426
|
+
outputFormatHint: "ocr"
|
|
427
|
+
}
|
|
428
|
+
]
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: "thresholds",
|
|
432
|
+
title: "Recognition Settings",
|
|
433
|
+
columns: 1,
|
|
434
|
+
fields: [
|
|
435
|
+
{
|
|
436
|
+
key: "minConfidence",
|
|
437
|
+
label: "Minimum Confidence",
|
|
438
|
+
type: "slider",
|
|
439
|
+
min: 0.1,
|
|
440
|
+
max: 1,
|
|
441
|
+
step: 0.05,
|
|
442
|
+
default: 0.5
|
|
443
|
+
}
|
|
444
|
+
]
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
getClassMap() {
|
|
450
|
+
return PLATE_REC_CLASS_MAP;
|
|
451
|
+
}
|
|
452
|
+
getModelCatalog() {
|
|
453
|
+
return [...import_types.PLATE_RECOGNITION_MODELS];
|
|
454
|
+
}
|
|
455
|
+
getAvailableModels() {
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
458
|
+
getActiveLabels() {
|
|
459
|
+
return PLATE_TEXT_LABELS;
|
|
460
|
+
}
|
|
461
|
+
async probe() {
|
|
462
|
+
return {
|
|
463
|
+
available: true,
|
|
464
|
+
runtime: this.engine?.runtime ?? "onnx",
|
|
465
|
+
device: this.engine?.device ?? "cpu",
|
|
466
|
+
capabilities: ["fp32"]
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
//# sourceMappingURL=index.js.map
|