@stabrise/scaledp 0.1.0 → 0.1.1
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/CHANGELOG.md +67 -0
- package/README.md +3 -2
- package/dist/{data-to-image-DoZ4jQ3R.js → data-to-image-CRvh92wt.js} +2 -2
- package/dist/{data-to-image-DoZ4jQ3R.js.map → data-to-image-CRvh92wt.js.map} +1 -1
- package/dist/detect/index.js +1 -1
- package/dist/{detect-q8AI_Jdj.js → detect-DpTc5Wtc.js} +3 -3
- package/dist/{detect-q8AI_Jdj.js.map → detect-DpTc5Wtc.js.map} +1 -1
- package/dist/{image-CAH2rLv9.js → image-DRBsbv7G.js} +16 -2
- package/dist/{image-CAH2rLv9.js.map → image-DRBsbv7G.js.map} +1 -1
- package/dist/{image-draw-boxes-De0QbFv9.js → image-draw-boxes-DuR8eBeg.js} +2 -2
- package/dist/{image-draw-boxes-De0QbFv9.js.map → image-draw-boxes-DuR8eBeg.js.map} +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +4 -4
- package/dist/ner/index.js +1 -1
- package/dist/{ner-SsZLZ6ed.js → ner-BB3vGNxO.js} +2 -2
- package/dist/{ner-SsZLZ6ed.js.map → ner-BB3vGNxO.js.map} +1 -1
- package/dist/ocr/index.d.ts +100 -4
- package/dist/ocr/index.js +3 -3
- package/dist/{ocr-OHX2WM3e.js → ocr-Dkkbk0Wl.js} +282 -25
- package/dist/ocr-Dkkbk0Wl.js.map +1 -0
- package/dist/{ort-CXDoPrtw.js → ort-DZEG14nY.js} +25 -4
- package/dist/ort-DZEG14nY.js.map +1 -0
- package/dist/pdf/index.js +1 -1
- package/dist/{pdf-BQl0dneD.js → pdf-qbdnOnqZ.js} +3 -3
- package/dist/{pdf-BQl0dneD.js.map → pdf-qbdnOnqZ.js.map} +1 -1
- package/dist/registry/index.js +97 -8
- package/dist/registry/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/ocr-OHX2WM3e.js.map +0 -1
- package/dist/ort-CXDoPrtw.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.1] - 29.08.2026
|
|
4
|
+
|
|
5
|
+
### 🚀 Features
|
|
6
|
+
|
|
7
|
+
- Added [`PaddleRecognizer`](https://scaledp-ts.stabrise.com/docs/stages/recognise/paddle-recognizer)
|
|
8
|
+
to `@stabrise/scaledp/ocr` — PaddleOCR recognition over the boxes a *separate*
|
|
9
|
+
detector found, the PP-OCR counterpart to `TesseractRecognizer`. Reads
|
|
10
|
+
`[image, boxes]` and writes a `Document`, so `DbnetOnnxDetector`,
|
|
11
|
+
`YoloOnnxDetector` or `PaddleTextDetector` can now feed PP-OCR. `PaddleTextRecognizer`
|
|
12
|
+
detects internally in one pass and still ignores any detector's boxes.
|
|
13
|
+
- Each box is cropped and straightened before reading, so a rotated region is
|
|
14
|
+
read level rather than as its axis-aligned envelope. The box that comes back
|
|
15
|
+
is the detector's own, angle included, carrying the recognised text and score.
|
|
16
|
+
- Crops are stacked onto a sheet canvas and read in one batched call.
|
|
17
|
+
PaddleOCR only batches within a single call and cuts its crops from one
|
|
18
|
+
canvas, so a forty-line page costs a handful of inferences rather than forty.
|
|
19
|
+
- Only the recognition model and character dictionary are downloaded — never
|
|
20
|
+
the detection half of the preset. Cache keys are shared with
|
|
21
|
+
`PaddleTextDetector` and `PaddleTextRecognizer`, so a pipeline using both
|
|
22
|
+
pays for each file once.
|
|
23
|
+
- No `strategy` parameter: `'per-line'` and `'cross-line'` merge boxes before
|
|
24
|
+
reading them, and the contract here is one result per box handed in.
|
|
25
|
+
- `detectLineOrientation` defaults to `false`, unlike `TesseractRecognizer`.
|
|
26
|
+
PaddleOCR already turns crops markedly taller than wide, so only the 180°
|
|
27
|
+
case is missing, and catching it costs a separate ~9 MB model.
|
|
28
|
+
- `@stabrise/scaledp/ocr` gained `getPaddleRecognizer()` for building a
|
|
29
|
+
recognition-only service directly, with the `PaddleRecognitionService` and
|
|
30
|
+
`PaddleRecognizerOptions` types.
|
|
31
|
+
- `createSession()` gained an opt-in `fallbackToWasm`, plus the exported
|
|
32
|
+
`wasmFallbackProviders()` behind it. WebGPU rewrites convolutions into its
|
|
33
|
+
`com.ms.internal.nhwc` domain and fails at *session creation* on a graph it has
|
|
34
|
+
no kernel for — PP-OCR's recognition model among them. The recognition session
|
|
35
|
+
opts in, matching what `PaddleOcrService` already did internally. Off by
|
|
36
|
+
default, so a genuinely misconfigured provider still fails loudly.
|
|
37
|
+
- `validatePreset()` moved to `@stabrise/scaledp/ocr` `presets` and is now
|
|
38
|
+
exported, alongside `isKnownPreset()`.
|
|
39
|
+
- `rotate180()` is now exported from the core image helpers, having been private
|
|
40
|
+
to the Tesseract recognizer.
|
|
41
|
+
|
|
42
|
+
### 💥 Breaking
|
|
43
|
+
|
|
44
|
+
- Renamed the two PaddleOCR defaults constants to match their stage class names,
|
|
45
|
+
freeing `PADDLE_RECOGNIZER_DEFAULTS` for the new stage:
|
|
46
|
+
- `PADDLE_DETECTOR_DEFAULTS` → `PADDLE_TEXT_DETECTOR_DEFAULTS`
|
|
47
|
+
- `PADDLE_RECOGNIZER_DEFAULTS` → `PADDLE_TEXT_RECOGNIZER_DEFAULTS`
|
|
48
|
+
|
|
49
|
+
`PADDLE_RECOGNIZER_DEFAULTS` still exists but now belongs to `PaddleRecognizer`
|
|
50
|
+
and has a different shape — it carries `inputCols` and no `strategy`. Code
|
|
51
|
+
importing it for `PaddleTextRecognizer` will not silently misbehave, but it
|
|
52
|
+
will not type-check either. No stage `type` string changed, so saved pipeline
|
|
53
|
+
descriptors are unaffected.
|
|
54
|
+
|
|
55
|
+
### 📚 Documentation
|
|
56
|
+
|
|
57
|
+
- New stage page for `PaddleRecognizer`, with its parameter table generated from
|
|
58
|
+
the stage registry as usual.
|
|
59
|
+
- The installation peer table, the Python porting map, the stage index, and the
|
|
60
|
+
`PaddleTextDetector` / `TesseractRecognizer` / `TesseractOcr` pages now point
|
|
61
|
+
at the new stage.
|
|
62
|
+
- The demo gained a "Detect, then read with PaddleOCR" preset beside the existing
|
|
63
|
+
Tesseract one.
|
|
64
|
+
|
|
65
|
+
## [0.1.0] - 29.08.2026
|
|
66
|
+
|
|
67
|
+
Initial release.
|
package/README.md
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<a href="https://
|
|
13
|
-
<a href="https://
|
|
12
|
+
<a href="https://scaledp-ts.stabrise.com/docs"><img alt="Documentation" src="https://img.shields.io/badge/docs-scaledp--ts.stabrise.com-3fc9f5.svg"></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/@stabrise/scaledp" alt="Package on npm"><img alt="npm" src="https://img.shields.io/npm/v/@stabrise/scaledp.svg?color=blue" /></a>
|
|
14
|
+
<a href="https://github.com/StabRise/scaledp-ts/blob/main/LICENSE"><img alt="License: AGPL-3.0-or-later" src="https://img.shields.io/badge/license-AGPL--3.0--or--later-blue.svg"></a>
|
|
14
15
|
<a href="https://stabrise.com"><img alt="StabRise" src="https://img.shields.io/badge/powered%20by-StabRise-orange.svg?style=flat&colorA=E1523D&colorB=blue"></a>
|
|
15
16
|
</p>
|
|
16
17
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as ImageError, i as Stage } from "./pipeline-DACqGkpN.js";
|
|
2
|
-
import { d as probeImageSize } from "./image-
|
|
2
|
+
import { d as probeImageSize } from "./image-DRBsbv7G.js";
|
|
3
3
|
import { i as resolveParams, t as BASE_STAGE_DEFAULTS } from "./params-DapwK9Ns.js";
|
|
4
4
|
import { t as createImage } from "./image-DoZDJkcR.js";
|
|
5
5
|
//#region src/stages/data-to-image.ts
|
|
@@ -51,4 +51,4 @@ function toBytes(input) {
|
|
|
51
51
|
//#endregion
|
|
52
52
|
export { DataToImage as n, toBytes as r, DATA_TO_IMAGE_DEFAULTS as t };
|
|
53
53
|
|
|
54
|
-
//# sourceMappingURL=data-to-image-
|
|
54
|
+
//# sourceMappingURL=data-to-image-CRvh92wt.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data-to-image-
|
|
1
|
+
{"version":3,"file":"data-to-image-CRvh92wt.js","names":[],"sources":["../src/stages/data-to-image.ts"],"sourcesContent":["/**\n * Port of `scaledp/image/DataToImage.py`: raw bytes -> an `Image` record.\n *\n * Dimensions are probed without retaining the decoded bitmap, mirroring\n * Python's use of `imagesize.get` rather than a full PIL decode.\n */\n\nimport { ImageError } from '../core/errors.js'\nimport { probeImageSize } from '../core/image.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage } from '../core/pipeline.js'\nimport { createImage, type ImageFormat, type ScaleDpImage } from '../schemas/image.js'\n\nexport interface DataToImageParams extends BaseStageParams {\n /** Encoding recorded on the output; the bytes are passed through unchanged. */\n imageType: ImageFormat\n /** DPI to record when the row carries no `resolution` field. */\n resolution: number\n}\n\nexport const DATA_TO_IMAGE_DEFAULTS: DataToImageParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'content',\n outputCol: 'image',\n imageType: 'png' as ImageFormat,\n resolution: 0,\n})\n\nexport class DataToImage extends Stage<DataToImageParams> {\n readonly name = 'DataToImage'\n\n constructor(options: Partial<DataToImageParams> = {}) {\n super(resolveParams(DATA_TO_IMAGE_DEFAULTS, options))\n }\n\n protected async apply(input: unknown, row: Row): Promise<ScaleDpImage> {\n const data = toBytes(input)\n if (data.byteLength === 0) {\n throw new ImageError('Empty image data', this.name)\n }\n const { width, height } = await probeImageSize(data)\n return createImage({\n path: String(row[this.params.pathCol] ?? 'memory'),\n resolution: Number(row.resolution ?? this.params.resolution) || 0,\n data,\n imageType: this.params.imageType,\n width,\n height,\n })\n }\n\n protected onError(message: string, row: Row): ScaleDpImage {\n return createImage({\n path: String(row[this.params.pathCol] ?? 'memory'),\n exception: message,\n })\n }\n}\n\n/** Coerce any accepted binary representation into bytes. */\nexport function toBytes(input: unknown): Uint8Array {\n if (input instanceof Uint8Array) return input\n if (input instanceof ArrayBuffer) return new Uint8Array(input)\n if (ArrayBuffer.isView(input)) {\n return new Uint8Array(input.buffer, input.byteOffset, input.byteLength)\n }\n throw new ImageError(`Expected binary image data, received ${typeof input}`, 'toBytes')\n}\n"],"mappings":";;;;;;;;;;;AAoBA,MAAa,yBAA4C,OAAO,OAAO;CACnE,GAAG;CACH,UAAU;CACV,WAAW;CACX,WAAW;CACX,YAAY;AAChB,CAAC;AAED,IAAa,cAAb,cAAiC,MAAyB;CACtD,OAAgB;CAEhB,YAAY,UAAsC,CAAC,GAAG;EAClD,MAAM,cAAc,wBAAwB,OAAO,CAAC;CACxD;CAEA,MAAgB,MAAM,OAAgB,KAAiC;EACnE,MAAM,OAAO,QAAQ,KAAK;EAC1B,IAAI,KAAK,eAAe,GACpB,MAAM,IAAI,WAAW,oBAAoB,KAAK,IAAI;EAEtD,MAAM,EAAE,OAAO,WAAW,MAAM,eAAe,IAAI;EACnD,OAAO,YAAY;GACf,MAAM,OAAO,IAAI,KAAK,OAAO,YAAY,QAAQ;GACjD,YAAY,OAAO,IAAI,cAAc,KAAK,OAAO,UAAU,KAAK;GAChE;GACA,WAAW,KAAK,OAAO;GACvB;GACA;EACJ,CAAC;CACL;CAEA,QAAkB,SAAiB,KAAwB;EACvD,OAAO,YAAY;GACf,MAAM,OAAO,IAAI,KAAK,OAAO,YAAY,QAAQ;GACjD,WAAW;EACf,CAAC;CACL;AACJ;;AAGA,SAAgB,QAAQ,OAA4B;CAChD,IAAI,iBAAiB,YAAY,OAAO;CACxC,IAAI,iBAAiB,aAAa,OAAO,IAAI,WAAW,KAAK;CAC7D,IAAI,YAAY,OAAO,KAAK,GACxB,OAAO,IAAI,WAAW,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU;CAE1E,MAAM,IAAI,WAAW,wCAAwC,OAAO,SAAS,SAAS;AAC1F"}
|
package/dist/detect/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as YoloOnnxDetector, c as nonMaximumSuppression, i as YOLO_DETECTOR_DEFAULTS, n as SignatureDetector, o as decodeYoloOutput, r as DEFAULT_YOLO_INPUT, s as iou, t as FaceDetector } from "../detect-
|
|
1
|
+
import { a as YoloOnnxDetector, c as nonMaximumSuppression, i as YOLO_DETECTOR_DEFAULTS, n as SignatureDetector, o as decodeYoloOutput, r as DEFAULT_YOLO_INPUT, s as iou, t as FaceDetector } from "../detect-DpTc5Wtc.js";
|
|
2
2
|
export { DEFAULT_YOLO_INPUT, FaceDetector, SignatureDetector, YOLO_DETECTOR_DEFAULTS, YoloOnnxDetector, decodeYoloOutput, iou, nonMaximumSuppression };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { h as getConfig, i as Stage, s as DetectionError } from "./pipeline-DACqGkpN.js";
|
|
2
|
-
import {
|
|
2
|
+
import { h as toNchwFloat32, m as toImageData, s as decodeImage, u as letterbox, v as boxFromBBox } from "./image-DRBsbv7G.js";
|
|
3
3
|
import { n as ensureModelFiles } from "./model-cache-BEaqqRZ9.js";
|
|
4
4
|
import { i as resolveParams, t as BASE_STAGE_DEFAULTS } from "./params-DapwK9Ns.js";
|
|
5
5
|
import { t as createDetectorOutput } from "./detector-output-lyF1Mqb8.js";
|
|
6
|
-
import { t as createSession } from "./ort-
|
|
6
|
+
import { t as createSession } from "./ort-DZEG14nY.js";
|
|
7
7
|
//#region src/detect/yolo-onnx.ts
|
|
8
8
|
/**
|
|
9
9
|
* YOLO object detection on onnxruntime-web.
|
|
@@ -271,4 +271,4 @@ var FaceDetector = class extends YoloOnnxDetector {
|
|
|
271
271
|
//#endregion
|
|
272
272
|
export { YoloOnnxDetector as a, nonMaximumSuppression as c, YOLO_DETECTOR_DEFAULTS as i, SignatureDetector as n, decodeYoloOutput as o, DEFAULT_YOLO_INPUT as r, iou as s, FaceDetector as t };
|
|
273
273
|
|
|
274
|
-
//# sourceMappingURL=detect-
|
|
274
|
+
//# sourceMappingURL=detect-DpTc5Wtc.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect-q8AI_Jdj.js","names":[],"sources":["../src/detect/yolo-onnx.ts","../src/detect/index.ts"],"sourcesContent":["/**\n * YOLO object detection on onnxruntime-web.\n *\n * Mirrors `scaledp/models/detectors/YoloOnnxDetector.py`. Note the\n * preprocessing differs from the text detector: YOLO letterboxes with\n * *centred* padding and no mean/std normalisation, where DBNet pads bottom and\n * right and applies ImageNet statistics. Centred padding means the pad offsets\n * must be subtracted before unscaling.\n */\n\nimport { getConfig } from '../core/config.js'\nimport { DetectionError } from '../core/errors.js'\nimport { decodeImage, letterbox, toImageData, toNchwFloat32 } from '../core/image.js'\nimport { ensureModelFiles } from '../core/model-cache.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage } from '../core/pipeline.js'\nimport { createSession } from '../ocr/ort.js'\nimport { type Box, boxFromBBox } from '../schemas/box.js'\nimport { createDetectorOutput, type DetectorOutput } from '../schemas/detector-output.js'\nimport type { ScaleDpImage } from '../schemas/image.js'\n\n/** Fallback when the graph declares a symbolic input size. */\nexport const DEFAULT_YOLO_INPUT = 960\n\nexport interface YoloOnnxDetectorParams extends BaseStageParams {\n model: string\n /** Class index -> label. An empty list falls back to `class_<n>`. */\n labels: readonly string[]\n scoreThreshold: number\n /** IoU above which two same-class boxes are considered duplicates. */\n iouThreshold: number\n /** Grow each box by this fraction of its size, to avoid clipping edges. */\n padding: number\n /** Emitted as DetectorOutput.type. */\n outputType: string\n}\n\nexport const YOLO_DETECTOR_DEFAULTS: YoloOnnxDetectorParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n outputCol: 'boxes',\n keepInputData: true,\n model: '',\n labels: [] as readonly string[],\n scoreThreshold: 0.2,\n iouThreshold: 0.5,\n padding: 0,\n outputType: 'yolo',\n})\n\nexport interface Detection {\n /** [x0, y0, x1, y1] in source-image coordinates. */\n bbox: [number, number, number, number]\n score: number\n classId: number\n label: string\n}\n\n/** Axis-aligned IoU over xyxy boxes. */\nexport function iou(a: readonly number[], b: readonly number[]): number {\n const x0 = Math.max(a[0] as number, b[0] as number)\n const y0 = Math.max(a[1] as number, b[1] as number)\n const x1 = Math.min(a[2] as number, b[2] as number)\n const y1 = Math.min(a[3] as number, b[3] as number)\n if (x1 <= x0 || y1 <= y0) return 0\n\n const intersection = (x1 - x0) * (y1 - y0)\n const areaA = ((a[2] as number) - (a[0] as number)) * ((a[3] as number) - (a[1] as number))\n const areaB = ((b[2] as number) - (b[0] as number)) * ((b[3] as number) - (b[1] as number))\n const union = areaA + areaB - intersection\n return union <= 0 ? 0 : intersection / union\n}\n\n/** Per-class non-maximum suppression; different classes never suppress each other. */\nexport function nonMaximumSuppression(detections: readonly Detection[], iouThreshold: number): Detection[] {\n const byScore = [...detections].sort((a, b) => b.score - a.score)\n const kept: Detection[] = []\n for (const detection of byScore) {\n const suppressed = kept.some(\n (other) => other.classId === detection.classId && iou(other.bbox, detection.bbox) > iouThreshold\n )\n if (!suppressed) kept.push(detection)\n }\n return kept\n}\n\n/**\n * Decode a YOLO output tensor.\n *\n * Two layouts are handled, because exports differ:\n * - [1, 4 + numClasses, numAnchors] -- the classic v8/v11 transposed form,\n * with cx,cy,w,h then per-class scores.\n * - [1, numDetections, 6] -- an end-to-end NMS export: x0,y0,x1,y1,score,class.\n */\nexport function decodeYoloOutput(\n data: Float32Array,\n dims: readonly number[],\n scoreThreshold: number\n): { bbox: [number, number, number, number]; score: number; classId: number }[] {\n const out: { bbox: [number, number, number, number]; score: number; classId: number }[] = []\n\n if (dims.length === 3 && dims[2] === 6) {\n const count = dims[1] as number\n for (let i = 0; i < count; i++) {\n const o = i * 6\n const score = data[o + 4] as number\n if (score < scoreThreshold) continue\n out.push({\n bbox: [\n data[o] as number,\n data[o + 1] as number,\n data[o + 2] as number,\n data[o + 3] as number,\n ],\n score,\n classId: Math.round(data[o + 5] as number),\n })\n }\n return out\n }\n\n if (dims.length !== 3) {\n throw new DetectionError(`Unsupported YOLO output shape [${dims.join(', ')}]`, 'decodeYoloOutput')\n }\n\n const channels = dims[1] as number\n const anchors = dims[2] as number\n const classCount = channels - 4\n\n for (let a = 0; a < anchors; a++) {\n let best = -1\n let bestScore = 0\n for (let c = 0; c < classCount; c++) {\n const score = data[(4 + c) * anchors + a] as number\n if (score > bestScore) {\n bestScore = score\n best = c\n }\n }\n if (best < 0 || bestScore < scoreThreshold) continue\n\n const cx = data[a] as number\n const cy = data[anchors + a] as number\n const w = data[2 * anchors + a] as number\n const h = data[3 * anchors + a] as number\n out.push({\n bbox: [cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2],\n score: bestScore,\n classId: best,\n })\n }\n return out\n}\n\nexport class YoloOnnxDetector extends Stage<YoloOnnxDetectorParams> {\n // Annotated as string rather than inferred as a literal, so SignatureDetector\n // and FaceDetector can narrow it to their own names.\n readonly name: string = 'YoloOnnxDetector'\n\n private session: import('onnxruntime-web').InferenceSession | null = null\n private loading: Promise<import('onnxruntime-web').InferenceSession> | null = null\n private inputSize = DEFAULT_YOLO_INPUT\n\n constructor(options: Partial<YoloOnnxDetectorParams> = {}) {\n super(\n resolveParams(YOLO_DETECTOR_DEFAULTS, options, {\n model: (value) => {\n if (!value) throw new RangeError('model is required')\n },\n })\n )\n }\n\n override async init(): Promise<void> {\n await this.getSession()\n }\n\n private getSession(): Promise<import('onnxruntime-web').InferenceSession> {\n if (this.session) return Promise.resolve(this.session)\n if (this.loading) return this.loading\n\n this.loading = (async () => {\n const { model } = this.params\n const spec = /^https?:\\/\\//.test(model)\n ? { repo: 'yolo', files: [{ path: model }] }\n : { repo: model, files: [{ path: 'model.onnx' }] }\n\n const files = await ensureModelFiles(spec)\n const bytes = files[spec.files[0]?.path ?? '']\n if (!bytes) throw new DetectionError(`Model ${model} not found`, this.name)\n\n const session = await createSession(bytes, {\n executionProviders: getConfig().executionProviders,\n })\n this.inputSize = readInputSize(session)\n this.session = session\n return session\n })()\n\n this.loading.catch(() => {\n this.loading = null\n })\n return this.loading\n }\n\n override async dispose(): Promise<void> {\n await this.session?.release()\n this.session = null\n this.loading = null\n }\n\n protected async apply(input: unknown, row: Row): Promise<DetectorOutput> {\n const image = input as ScaleDpImage | undefined\n // Check `exception` first. A failed upstream stage returns a well-formed but\n // empty Image, so testing the bytes first would report \"no decoded bytes\" and\n // bury the real cause.\n if (image?.exception) {\n throw new DetectionError(`Upstream stage failed: ${image.exception}`, this.name)\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new DetectionError('Expected an Image with decoded bytes', this.name)\n }\n\n const bitmap = await decodeImage(image.data)\n let detections: Detection[]\n try {\n detections = await this.detect(bitmap)\n } finally {\n bitmap.close()\n }\n\n const bboxes: Box[] = detections.map((d) => boxFromBBox(d.bbox, { text: d.label, score: d.score }))\n return createDetectorOutput({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: this.params.outputType,\n bboxes,\n })\n }\n\n /** Run the model over one decoded image, returning source-space detections. */\n async detect(source: ImageBitmap | OffscreenCanvas): Promise<Detection[]> {\n const session = await this.getSession()\n const size = this.inputSize\n\n // Centred padding, no mean/std -- YOLO's convention, unlike DBNet's.\n const fitted = letterbox(\n source,\n { width: size, height: size },\n {\n padding: 'center',\n fill: '#ffffff',\n }\n )\n const tensorData = toNchwFloat32(toImageData(fitted.canvas))\n\n const { Tensor } = await import('onnxruntime-web')\n const inputName = session.inputNames[0]\n const outputName = session.outputNames[0]\n if (!inputName || !outputName) {\n throw new DetectionError('Model exposes no input or output', this.name)\n }\n\n const outputs = await session.run({\n [inputName]: new Tensor('float32', tensorData, [1, 3, size, size]),\n })\n const output = outputs[outputName]\n if (!output) throw new DetectionError(`Model produced no \"${outputName}\" output`, this.name)\n\n const raw = decodeYoloOutput(output.data as Float32Array, output.dims, this.params.scoreThreshold)\n\n const padX = Math.trunc((size - fitted.resized.width) / 2)\n const padY = Math.trunc((size - fitted.resized.height) / 2)\n const { labels, padding } = this.params\n\n const detections: Detection[] = raw.map(({ bbox, score, classId }) => {\n // Undo the centred pad first, then the scale.\n const x0 = (bbox[0] - padX) / fitted.scale\n const y0 = (bbox[1] - padY) / fitted.scale\n const x1 = (bbox[2] - padX) / fitted.scale\n const y1 = (bbox[3] - padY) / fitted.scale\n\n const padW = (x1 - x0) * padding\n const padH = (y1 - y0) * padding\n return {\n bbox: [\n Math.max(0, x0 - padW),\n Math.max(0, y0 - padH),\n Math.min(source.width, x1 + padW),\n Math.min(source.height, y1 + padH),\n ],\n score,\n classId,\n label: labels[classId] ?? `class_${classId}`,\n }\n })\n\n return nonMaximumSuppression(detections, this.params.iouThreshold)\n }\n\n protected onError(message: string, row: Row): DetectorOutput {\n return createDetectorOutput({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: this.params.outputType,\n exception: message,\n })\n }\n}\n\n/** Input side length from the graph, falling back when the dim is symbolic. */\nfunction readInputSize(session: import('onnxruntime-web').InferenceSession): number {\n const meta = (\n session as unknown as {\n inputMetadata?: { dims?: (number | string)[] }[]\n }\n ).inputMetadata?.[0]\n const dims = meta?.dims\n const height = dims?.[2]\n return typeof height === 'number' && height > 0 ? height : DEFAULT_YOLO_INPUT\n}\n","/**\n * Object detection for @stabrise/scaledp.\n *\n * Mirrors ScaleDP's YOLO detector stages. Requires the optional peer dependency\n * `onnxruntime-web`.\n */\n\nimport { YOLO_DETECTOR_DEFAULTS, YoloOnnxDetector, type YoloOnnxDetectorParams } from './yolo-onnx.js'\n\nexport type { Detection, YoloOnnxDetectorParams } from './yolo-onnx.js'\nexport {\n DEFAULT_YOLO_INPUT,\n decodeYoloOutput,\n iou,\n nonMaximumSuppression,\n YOLO_DETECTOR_DEFAULTS,\n YoloOnnxDetector,\n} from './yolo-onnx.js'\n\n/** Signature detection, mirroring ScaleDP's `SignatureDetector`. */\nexport class SignatureDetector extends YoloOnnxDetector {\n override readonly name = 'SignatureDetector'\n\n constructor(options: Partial<YoloOnnxDetectorParams> = {}) {\n super({\n ...YOLO_DETECTOR_DEFAULTS,\n model: 'StabRise/signature_detection',\n labels: ['signature'],\n outputCol: 'signatures',\n outputType: 'signature',\n scoreThreshold: 0.2,\n ...options,\n })\n }\n}\n\n/** Face detection, mirroring ScaleDP's `FaceDetector`. */\nexport class FaceDetector extends YoloOnnxDetector {\n override readonly name = 'FaceDetector'\n\n constructor(options: Partial<YoloOnnxDetectorParams> = {}) {\n super({\n ...YOLO_DETECTOR_DEFAULTS,\n model: 'StabRise/face_detection',\n labels: ['face'],\n outputCol: 'faces',\n outputType: 'face',\n scoreThreshold: 0.2,\n ...options,\n })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"detect-DpTc5Wtc.js","names":[],"sources":["../src/detect/yolo-onnx.ts","../src/detect/index.ts"],"sourcesContent":["/**\n * YOLO object detection on onnxruntime-web.\n *\n * Mirrors `scaledp/models/detectors/YoloOnnxDetector.py`. Note the\n * preprocessing differs from the text detector: YOLO letterboxes with\n * *centred* padding and no mean/std normalisation, where DBNet pads bottom and\n * right and applies ImageNet statistics. Centred padding means the pad offsets\n * must be subtracted before unscaling.\n */\n\nimport { getConfig } from '../core/config.js'\nimport { DetectionError } from '../core/errors.js'\nimport { decodeImage, letterbox, toImageData, toNchwFloat32 } from '../core/image.js'\nimport { ensureModelFiles } from '../core/model-cache.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage } from '../core/pipeline.js'\nimport { createSession } from '../ocr/ort.js'\nimport { type Box, boxFromBBox } from '../schemas/box.js'\nimport { createDetectorOutput, type DetectorOutput } from '../schemas/detector-output.js'\nimport type { ScaleDpImage } from '../schemas/image.js'\n\n/** Fallback when the graph declares a symbolic input size. */\nexport const DEFAULT_YOLO_INPUT = 960\n\nexport interface YoloOnnxDetectorParams extends BaseStageParams {\n model: string\n /** Class index -> label. An empty list falls back to `class_<n>`. */\n labels: readonly string[]\n scoreThreshold: number\n /** IoU above which two same-class boxes are considered duplicates. */\n iouThreshold: number\n /** Grow each box by this fraction of its size, to avoid clipping edges. */\n padding: number\n /** Emitted as DetectorOutput.type. */\n outputType: string\n}\n\nexport const YOLO_DETECTOR_DEFAULTS: YoloOnnxDetectorParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n outputCol: 'boxes',\n keepInputData: true,\n model: '',\n labels: [] as readonly string[],\n scoreThreshold: 0.2,\n iouThreshold: 0.5,\n padding: 0,\n outputType: 'yolo',\n})\n\nexport interface Detection {\n /** [x0, y0, x1, y1] in source-image coordinates. */\n bbox: [number, number, number, number]\n score: number\n classId: number\n label: string\n}\n\n/** Axis-aligned IoU over xyxy boxes. */\nexport function iou(a: readonly number[], b: readonly number[]): number {\n const x0 = Math.max(a[0] as number, b[0] as number)\n const y0 = Math.max(a[1] as number, b[1] as number)\n const x1 = Math.min(a[2] as number, b[2] as number)\n const y1 = Math.min(a[3] as number, b[3] as number)\n if (x1 <= x0 || y1 <= y0) return 0\n\n const intersection = (x1 - x0) * (y1 - y0)\n const areaA = ((a[2] as number) - (a[0] as number)) * ((a[3] as number) - (a[1] as number))\n const areaB = ((b[2] as number) - (b[0] as number)) * ((b[3] as number) - (b[1] as number))\n const union = areaA + areaB - intersection\n return union <= 0 ? 0 : intersection / union\n}\n\n/** Per-class non-maximum suppression; different classes never suppress each other. */\nexport function nonMaximumSuppression(detections: readonly Detection[], iouThreshold: number): Detection[] {\n const byScore = [...detections].sort((a, b) => b.score - a.score)\n const kept: Detection[] = []\n for (const detection of byScore) {\n const suppressed = kept.some(\n (other) => other.classId === detection.classId && iou(other.bbox, detection.bbox) > iouThreshold\n )\n if (!suppressed) kept.push(detection)\n }\n return kept\n}\n\n/**\n * Decode a YOLO output tensor.\n *\n * Two layouts are handled, because exports differ:\n * - [1, 4 + numClasses, numAnchors] -- the classic v8/v11 transposed form,\n * with cx,cy,w,h then per-class scores.\n * - [1, numDetections, 6] -- an end-to-end NMS export: x0,y0,x1,y1,score,class.\n */\nexport function decodeYoloOutput(\n data: Float32Array,\n dims: readonly number[],\n scoreThreshold: number\n): { bbox: [number, number, number, number]; score: number; classId: number }[] {\n const out: { bbox: [number, number, number, number]; score: number; classId: number }[] = []\n\n if (dims.length === 3 && dims[2] === 6) {\n const count = dims[1] as number\n for (let i = 0; i < count; i++) {\n const o = i * 6\n const score = data[o + 4] as number\n if (score < scoreThreshold) continue\n out.push({\n bbox: [\n data[o] as number,\n data[o + 1] as number,\n data[o + 2] as number,\n data[o + 3] as number,\n ],\n score,\n classId: Math.round(data[o + 5] as number),\n })\n }\n return out\n }\n\n if (dims.length !== 3) {\n throw new DetectionError(`Unsupported YOLO output shape [${dims.join(', ')}]`, 'decodeYoloOutput')\n }\n\n const channels = dims[1] as number\n const anchors = dims[2] as number\n const classCount = channels - 4\n\n for (let a = 0; a < anchors; a++) {\n let best = -1\n let bestScore = 0\n for (let c = 0; c < classCount; c++) {\n const score = data[(4 + c) * anchors + a] as number\n if (score > bestScore) {\n bestScore = score\n best = c\n }\n }\n if (best < 0 || bestScore < scoreThreshold) continue\n\n const cx = data[a] as number\n const cy = data[anchors + a] as number\n const w = data[2 * anchors + a] as number\n const h = data[3 * anchors + a] as number\n out.push({\n bbox: [cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2],\n score: bestScore,\n classId: best,\n })\n }\n return out\n}\n\nexport class YoloOnnxDetector extends Stage<YoloOnnxDetectorParams> {\n // Annotated as string rather than inferred as a literal, so SignatureDetector\n // and FaceDetector can narrow it to their own names.\n readonly name: string = 'YoloOnnxDetector'\n\n private session: import('onnxruntime-web').InferenceSession | null = null\n private loading: Promise<import('onnxruntime-web').InferenceSession> | null = null\n private inputSize = DEFAULT_YOLO_INPUT\n\n constructor(options: Partial<YoloOnnxDetectorParams> = {}) {\n super(\n resolveParams(YOLO_DETECTOR_DEFAULTS, options, {\n model: (value) => {\n if (!value) throw new RangeError('model is required')\n },\n })\n )\n }\n\n override async init(): Promise<void> {\n await this.getSession()\n }\n\n private getSession(): Promise<import('onnxruntime-web').InferenceSession> {\n if (this.session) return Promise.resolve(this.session)\n if (this.loading) return this.loading\n\n this.loading = (async () => {\n const { model } = this.params\n const spec = /^https?:\\/\\//.test(model)\n ? { repo: 'yolo', files: [{ path: model }] }\n : { repo: model, files: [{ path: 'model.onnx' }] }\n\n const files = await ensureModelFiles(spec)\n const bytes = files[spec.files[0]?.path ?? '']\n if (!bytes) throw new DetectionError(`Model ${model} not found`, this.name)\n\n const session = await createSession(bytes, {\n executionProviders: getConfig().executionProviders,\n })\n this.inputSize = readInputSize(session)\n this.session = session\n return session\n })()\n\n this.loading.catch(() => {\n this.loading = null\n })\n return this.loading\n }\n\n override async dispose(): Promise<void> {\n await this.session?.release()\n this.session = null\n this.loading = null\n }\n\n protected async apply(input: unknown, row: Row): Promise<DetectorOutput> {\n const image = input as ScaleDpImage | undefined\n // Check `exception` first. A failed upstream stage returns a well-formed but\n // empty Image, so testing the bytes first would report \"no decoded bytes\" and\n // bury the real cause.\n if (image?.exception) {\n throw new DetectionError(`Upstream stage failed: ${image.exception}`, this.name)\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new DetectionError('Expected an Image with decoded bytes', this.name)\n }\n\n const bitmap = await decodeImage(image.data)\n let detections: Detection[]\n try {\n detections = await this.detect(bitmap)\n } finally {\n bitmap.close()\n }\n\n const bboxes: Box[] = detections.map((d) => boxFromBBox(d.bbox, { text: d.label, score: d.score }))\n return createDetectorOutput({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: this.params.outputType,\n bboxes,\n })\n }\n\n /** Run the model over one decoded image, returning source-space detections. */\n async detect(source: ImageBitmap | OffscreenCanvas): Promise<Detection[]> {\n const session = await this.getSession()\n const size = this.inputSize\n\n // Centred padding, no mean/std -- YOLO's convention, unlike DBNet's.\n const fitted = letterbox(\n source,\n { width: size, height: size },\n {\n padding: 'center',\n fill: '#ffffff',\n }\n )\n const tensorData = toNchwFloat32(toImageData(fitted.canvas))\n\n const { Tensor } = await import('onnxruntime-web')\n const inputName = session.inputNames[0]\n const outputName = session.outputNames[0]\n if (!inputName || !outputName) {\n throw new DetectionError('Model exposes no input or output', this.name)\n }\n\n const outputs = await session.run({\n [inputName]: new Tensor('float32', tensorData, [1, 3, size, size]),\n })\n const output = outputs[outputName]\n if (!output) throw new DetectionError(`Model produced no \"${outputName}\" output`, this.name)\n\n const raw = decodeYoloOutput(output.data as Float32Array, output.dims, this.params.scoreThreshold)\n\n const padX = Math.trunc((size - fitted.resized.width) / 2)\n const padY = Math.trunc((size - fitted.resized.height) / 2)\n const { labels, padding } = this.params\n\n const detections: Detection[] = raw.map(({ bbox, score, classId }) => {\n // Undo the centred pad first, then the scale.\n const x0 = (bbox[0] - padX) / fitted.scale\n const y0 = (bbox[1] - padY) / fitted.scale\n const x1 = (bbox[2] - padX) / fitted.scale\n const y1 = (bbox[3] - padY) / fitted.scale\n\n const padW = (x1 - x0) * padding\n const padH = (y1 - y0) * padding\n return {\n bbox: [\n Math.max(0, x0 - padW),\n Math.max(0, y0 - padH),\n Math.min(source.width, x1 + padW),\n Math.min(source.height, y1 + padH),\n ],\n score,\n classId,\n label: labels[classId] ?? `class_${classId}`,\n }\n })\n\n return nonMaximumSuppression(detections, this.params.iouThreshold)\n }\n\n protected onError(message: string, row: Row): DetectorOutput {\n return createDetectorOutput({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: this.params.outputType,\n exception: message,\n })\n }\n}\n\n/** Input side length from the graph, falling back when the dim is symbolic. */\nfunction readInputSize(session: import('onnxruntime-web').InferenceSession): number {\n const meta = (\n session as unknown as {\n inputMetadata?: { dims?: (number | string)[] }[]\n }\n ).inputMetadata?.[0]\n const dims = meta?.dims\n const height = dims?.[2]\n return typeof height === 'number' && height > 0 ? height : DEFAULT_YOLO_INPUT\n}\n","/**\n * Object detection for @stabrise/scaledp.\n *\n * Mirrors ScaleDP's YOLO detector stages. Requires the optional peer dependency\n * `onnxruntime-web`.\n */\n\nimport { YOLO_DETECTOR_DEFAULTS, YoloOnnxDetector, type YoloOnnxDetectorParams } from './yolo-onnx.js'\n\nexport type { Detection, YoloOnnxDetectorParams } from './yolo-onnx.js'\nexport {\n DEFAULT_YOLO_INPUT,\n decodeYoloOutput,\n iou,\n nonMaximumSuppression,\n YOLO_DETECTOR_DEFAULTS,\n YoloOnnxDetector,\n} from './yolo-onnx.js'\n\n/** Signature detection, mirroring ScaleDP's `SignatureDetector`. */\nexport class SignatureDetector extends YoloOnnxDetector {\n override readonly name = 'SignatureDetector'\n\n constructor(options: Partial<YoloOnnxDetectorParams> = {}) {\n super({\n ...YOLO_DETECTOR_DEFAULTS,\n model: 'StabRise/signature_detection',\n labels: ['signature'],\n outputCol: 'signatures',\n outputType: 'signature',\n scoreThreshold: 0.2,\n ...options,\n })\n }\n}\n\n/** Face detection, mirroring ScaleDP's `FaceDetector`. */\nexport class FaceDetector extends YoloOnnxDetector {\n override readonly name = 'FaceDetector'\n\n constructor(options: Partial<YoloOnnxDetectorParams> = {}) {\n super({\n ...YOLO_DETECTOR_DEFAULTS,\n model: 'StabRise/face_detection',\n labels: ['face'],\n outputCol: 'faces',\n outputType: 'face',\n scoreThreshold: 0.2,\n ...options,\n })\n }\n}\n"],"mappings":"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"}
|
|
@@ -459,6 +459,20 @@ function cropBox(source, box, opts = {}) {
|
|
|
459
459
|
return canvas;
|
|
460
460
|
}
|
|
461
461
|
/**
|
|
462
|
+
* Turn a crop 180 degrees, so an inverted line reads the right way up.
|
|
463
|
+
*
|
|
464
|
+
* Maps (x, y) to (w - x, h - y) -- a recognizer mapping word boxes back out of
|
|
465
|
+
* an inverted crop has to undo exactly that.
|
|
466
|
+
*/
|
|
467
|
+
function rotate180(source) {
|
|
468
|
+
const out = createCanvas(source.width, source.height);
|
|
469
|
+
const ctx = context2d(out);
|
|
470
|
+
ctx.translate(source.width / 2, source.height / 2);
|
|
471
|
+
ctx.rotate(Math.PI);
|
|
472
|
+
ctx.drawImage(source, -source.width / 2, -source.height / 2);
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
462
476
|
* Build an NCHW float32 tensor from an image.
|
|
463
477
|
*
|
|
464
478
|
* `bgr` exists because ScaleDP's DBNet path converts RGB->BGR and never swaps
|
|
@@ -506,6 +520,6 @@ const IMAGENET_STD = [
|
|
|
506
520
|
.225
|
|
507
521
|
];
|
|
508
522
|
//#endregion
|
|
509
|
-
export {
|
|
523
|
+
export { minAreaRect as A, isRotated as C, shape as D, scaleBox as E, polygonPerimeter as M, boxPoints as O, isOnSameLine as S, mergeOverlappingBoxes as T, bbox as _, cropBox as a, boxIou as b, encodeImage as c, probeImageSize as d, resize as f, ROTATION_EPSILON_DEGREES as g, toNchwFloat32 as h, createCanvas as i, polygonArea as j, convexHull as k, imageDataToCanvas as l, toImageData as m, IMAGENET_STD as n, cropGeometry as o, rotate180 as p, context2d as r, decodeImage as s, IMAGENET_MEAN as t, letterbox as u, boxFromBBox as v, mergeBoxes as w, createBox as x, boxFromPolygon as y };
|
|
510
524
|
|
|
511
|
-
//# sourceMappingURL=image-
|
|
525
|
+
//# sourceMappingURL=image-DRBsbv7G.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-CAH2rLv9.js","names":[],"sources":["../src/core/geometry.ts","../src/schemas/box.ts","../src/core/image.ts"],"sourcesContent":["/**\n * OpenCV-compatible planar geometry, hand-ported so the browser build needs no\n * opencv.js. Conventions match cv2 exactly — `Box.from_polygon` and the DBNet\n * post-processor both depend on that, and a divergence here shifts every box.\n */\n\nexport type Point = [number, number]\n\nexport interface RotatedRect {\n /** Centre of the rectangle. */\n center: Point\n /** Side lengths along the rect's own axes. */\n size: [number, number]\n /**\n * Degrees in (0, 90], with `size[0]` (the width) lying along that direction.\n *\n * Verified against cv2 4.11 across 18 orientations. One caveat: for a\n * perfectly axis-aligned rect cv2 itself is inconsistent, returning either\n * `-0.0` or `90` depending on which hull edge its scan lands on. Those two\n * describe the same rectangle (a rectangle is invariant under a 180-degree\n * rotation once the sides are swapped), so we always report the `90` form.\n * Nothing downstream can distinguish them -- ImageDrawBoxes renders angle\n * and angle+180 identically.\n */\n angle: number\n}\n\nconst EPSILON = 1e-9\n\n/** Cross product of (o->a) and (o->b). > 0 means counter-clockwise. */\nfunction cross(o: Point, a: Point, b: Point): number {\n return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])\n}\n\n/**\n * Monotone-chain convex hull. Returns hull points counter-clockwise in a\n * y-down image coordinate system, without the duplicated closing point.\n */\nexport function convexHull(points: readonly Point[]): Point[] {\n const pts = [...points].sort((p, q) => (p[0] === q[0] ? p[1] - q[1] : p[0] - q[0]))\n // Drop exact duplicates; collinear runs are handled by the cross-product test.\n const uniq: Point[] = []\n for (const p of pts) {\n const last = uniq[uniq.length - 1]\n if (!last || last[0] !== p[0] || last[1] !== p[1]) uniq.push(p)\n }\n if (uniq.length < 3) return uniq\n\n const build = (source: Point[]): Point[] => {\n const chain: Point[] = []\n for (const p of source) {\n while (\n chain.length >= 2 &&\n cross(chain[chain.length - 2] as Point, chain[chain.length - 1] as Point, p) <= 0\n ) {\n chain.pop()\n }\n chain.push(p)\n }\n chain.pop()\n return chain\n }\n return [...build(uniq), ...build([...uniq].reverse())]\n}\n\n/**\n * Minimum-area enclosing rectangle via rotating calipers.\n *\n * The minimum-area rectangle always has a side flush with a convex-hull edge,\n * so testing one orientation per hull edge is exhaustive.\n */\nexport function minAreaRect(points: readonly Point[]): RotatedRect {\n if (points.length === 0) return { center: [0, 0], size: [0, 0], angle: 0 }\n\n const hull = convexHull(points)\n if (hull.length < 2) {\n const p = (hull[0] ?? points[0]) as Point\n return { center: [p[0], p[1]], size: [0, 0], angle: 0 }\n }\n\n let best: { area: number; center: Point; w: number; h: number; angle: number } | null = null\n\n for (let i = 0; i < hull.length; i++) {\n const a = hull[i] as Point\n const b = hull[(i + 1) % hull.length] as Point\n const len = Math.hypot(b[0] - a[0], b[1] - a[1])\n if (len < EPSILON) continue\n\n // Orthonormal frame with u along this hull edge.\n const ux = (b[0] - a[0]) / len\n const uy = (b[1] - a[1]) / len\n const vx = -uy\n const vy = ux\n\n let minU = Infinity\n let maxU = -Infinity\n let minV = Infinity\n let maxV = -Infinity\n for (const p of hull) {\n const pu = p[0] * ux + p[1] * uy\n const pv = p[0] * vx + p[1] * vy\n if (pu < minU) minU = pu\n if (pu > maxU) maxU = pu\n if (pv < minV) minV = pv\n if (pv > maxV) maxV = pv\n }\n\n const w = maxU - minU\n const h = maxV - minV\n const area = w * h\n if (best === null || area < best.area) {\n const cu = (minU + maxU) / 2\n const cv = (minV + maxV) / 2\n best = {\n area,\n center: [cu * ux + cv * vx, cu * uy + cv * vy],\n w,\n h,\n angle: (Math.atan2(uy, ux) * 180) / Math.PI,\n }\n }\n }\n\n if (best === null) return { center: [0, 0], size: [0, 0], angle: 0 }\n\n // cv2 reports the angle in (0, 90] -- half-open at zero, so a perfectly\n // axis-aligned rect is reported as 90 degrees with the sides swapped rather\n // than as 0. Each 90 degrees folded away swaps which side is \"width\",\n // because the frame's u and v axes trade places.\n let { angle, w, h } = best\n while (angle <= 0) {\n angle += 90\n ;[w, h] = [h, w]\n }\n while (angle > 90) {\n angle -= 90\n ;[w, h] = [h, w]\n }\n\n return { center: best.center, size: [w, h], angle }\n}\n\n/**\n * The 4 corners of a rotated rect, in cv2.boxPoints order.\n * For an upright rect in image coordinates that is: BL, TL, TR, BR.\n */\nexport function boxPoints(rect: RotatedRect): [Point, Point, Point, Point] {\n const [cx, cy] = rect.center\n const [w, h] = rect.size\n const rad = (rect.angle * Math.PI) / 180\n const b = Math.cos(rad) * 0.5\n const a = Math.sin(rad) * 0.5\n\n const p0: Point = [cx - a * h - b * w, cy + b * h - a * w]\n const p1: Point = [cx + a * h - b * w, cy - b * h - a * w]\n const p2: Point = [2 * cx - p0[0], 2 * cy - p0[1]]\n const p3: Point = [2 * cx - p1[0], 2 * cy - p1[1]]\n return [p0, p1, p2, p3]\n}\n\n/** Shoelace area of a simple polygon; always non-negative. */\nexport function polygonArea(points: readonly Point[]): number {\n let sum = 0\n for (let i = 0; i < points.length; i++) {\n const a = points[i] as Point\n const b = points[(i + 1) % points.length] as Point\n sum += a[0] * b[1] - b[0] * a[1]\n }\n return Math.abs(sum) / 2\n}\n\n/** Perimeter of a closed polygon. */\nexport function polygonPerimeter(points: readonly Point[]): number {\n let sum = 0\n for (let i = 0; i < points.length; i++) {\n const a = points[i] as Point\n const b = points[(i + 1) % points.length] as Point\n sum += Math.hypot(b[0] - a[0], b[1] - a[1])\n }\n return sum\n}\n","/**\n * Port of `scaledp/schemas/Box.py`.\n *\n * A Box is NOT xyxy and NOT a polygon. `x`/`y` is the top-left of the\n * *axis-aligned* box of the same size centred on the rotated rect's centre,\n * and `angle` is degrees about that same centre. `width` is always the longer\n * side. Getting this wrong silently shifts every downstream consumer, so the\n * conversions below mirror the Python implementation exactly.\n */\n\nimport { minAreaRect, type Point } from '../core/geometry.js'\n\nexport interface Box {\n text: string\n score: number\n /** Top-left x of the axis-aligned box of size width x height centred on the rect centre. */\n x: number\n y: number\n /** Always the longer side. */\n width: number\n height: number\n /** Degrees about the box centre, normalised to (-90, 270]. */\n angle: number\n}\n\n/** Two-corner shape: [[x0, y0], [x1, y1]]. */\nexport type BoxShape = [[number, number], [number, number]]\n\n/** Axis-aligned bounds: [x0, y0, x1, y1]. */\nexport type BBox = [number, number, number, number]\n\nexport type { Point }\n\n/** `abs(angle) >= 3` — matches Python's `is_rotated`, which tolerates OCR jitter. */\nexport const ROTATION_EPSILON_DEGREES = 3\n\nexport function createBox(init: Partial<Box> = {}): Box {\n return {\n text: init.text ?? '',\n score: init.score ?? 0,\n x: init.x ?? 0,\n y: init.y ?? 0,\n width: init.width ?? 0,\n height: init.height ?? 0,\n angle: init.angle ?? 0,\n }\n}\n\nexport function isRotated(box: Box): boolean {\n return Math.abs(box.angle) >= ROTATION_EPSILON_DEGREES\n}\n\nexport function bbox(box: Box, padding = 0): BBox {\n return [box.x - padding, box.y - padding, box.x + box.width + padding, box.y + box.height + padding]\n}\n\nexport function shape(box: Box, padding = 0): BoxShape {\n const [x0, y0, x1, y1] = bbox(box, padding)\n return [\n [x0, y0],\n [x1, y1],\n ]\n}\n\n/**\n * Scale a box, then apply padding.\n *\n * Note the padding is asymmetric, and deliberately so: Python subtracts it from\n * the origin and adds it to the size, so the box grows by `padding` on its left\n * and top while its right and bottom edges stay put. Growing all four sides\n * would need `padding * 2` on the size.\n */\nexport function scaleBox(box: Box, factor: number, padding = 0): Box {\n return {\n ...box,\n x: Math.round(box.x * factor - padding),\n y: Math.round(box.y * factor - padding),\n width: Math.round(box.width * factor + padding),\n height: Math.round(box.height * factor + padding),\n }\n}\n\nexport function boxFromBBox(box: BBox, opts: { angle?: number; text?: string; score?: number } = {}): Box {\n const [x0, y0, x1, y1] = box\n return {\n text: opts.text ?? '',\n score: opts.score ?? 0,\n x: Math.round(x0),\n y: Math.round(y0),\n width: Math.round(x1 - x0),\n height: Math.round(y1 - y0),\n angle: opts.angle ?? 0,\n }\n}\n\n/**\n * Build a Box from exactly 4 polygon points — port of Python `Box.from_polygon`.\n *\n * `width` is forced to be the longer side (subtracting 90 degrees from the angle\n * to compensate), then the angle is normalised to (-90, 270]. `x`/`y` are derived\n * from the centre, NOT from the polygon's bounding box.\n */\nexport function boxFromPolygon(\n points: readonly Point[],\n opts: { text?: string; score?: number; padding?: number } = {}\n): Box {\n if (points.length !== 4) {\n throw new Error(`boxFromPolygon expects exactly 4 points, received ${points.length}`)\n }\n const padding = opts.padding ?? 0\n const rect = minAreaRect(points)\n const [cx, cy] = rect.center\n\n let [width, height] = rect.size\n let angle = rect.angle\n if (width < height) {\n ;[width, height] = [height, width]\n angle -= 90\n }\n\n // Normalise to (-90, 270]. Note a rectangle is invariant under a 180-degree\n // rotation, so `angle` and `angle + 180` are interchangeable; which one you\n // get depends on the orientation minAreaRect happened to report.\n angle = ((angle % 360) + 360) % 360\n if (angle > 270) angle -= 360\n\n // Python clamps both dimensions to at least 1px so degenerate detections\n // stay usable as crop regions.\n width = Math.max(1, Math.round(width) + padding * 2)\n height = Math.max(1, Math.round(height) + padding * 2)\n\n return {\n text: opts.text ?? '',\n score: opts.score ?? 1,\n x: Math.round(cx - width / 2),\n y: Math.round(cy - height / 2),\n width,\n height,\n angle,\n }\n}\n\n/** Axis-aligned intersection-over-union. Ignores `angle`, exactly as Python does. */\nexport function boxIou(a: Box, b: Box): number {\n const [ax0, ay0, ax1, ay1] = bbox(a)\n const [bx0, by0, bx1, by1] = bbox(b)\n\n const ix = Math.min(ax1, bx1) - Math.max(ax0, bx0)\n const iy = Math.min(ay1, by1) - Math.max(ay0, by0)\n if (ix <= 0 || iy <= 0) return 0\n\n const intersection = ix * iy\n const union = a.width * a.height + b.width * b.height - intersection\n return union <= 0 ? 0 : intersection / union\n}\n\n/** Union of two boxes. Merging discards rotation — Python resets `angle` to 0. */\nexport function mergeBoxes(a: Box, b: Box): Box {\n const [ax0, ay0, ax1, ay1] = bbox(a)\n const [bx0, by0, bx1, by1] = bbox(b)\n const x = Math.min(ax0, bx0)\n const y = Math.min(ay0, by0)\n\n return {\n text: `${a.text} ${b.text}`.trim(),\n score: Math.min(a.score, b.score),\n x,\n y,\n width: Math.max(ax1, bx1) - x,\n height: Math.max(ay1, by1) - y,\n angle: 0,\n }\n}\n\n/**\n * Whether two boxes sit on the same text line.\n *\n * For near-horizontal boxes this compares vertical centres against the average\n * height. For rotated boxes it projects the centre offset onto the line's normal\n * (dx = -sin, dy = cos), so the test follows the text's own baseline.\n */\nexport function isOnSameLine(a: Box, b: Box, angleThresh = 10, lineThresh = 0.5): boolean {\n if (Math.abs(a.angle - b.angle) > angleThresh) return false\n\n const avgHeight = (a.height + b.height) / 2\n if (avgHeight <= 0) return false\n\n const acx = a.x + a.width / 2\n const acy = a.y + a.height / 2\n const bcx = b.x + b.width / 2\n const bcy = b.y + b.height / 2\n\n // Python branches on the caller's `angleThresh`, not on the much smaller\n // rotation epsilon: a box a few degrees off is still treated as horizontal\n // here, and compared by raw vertical distance. Using the epsilon instead\n // sends slightly skewed boxes down the projection path, where a large\n // horizontal gap cancels most of the vertical one and unrelated lines start\n // reading as the same line.\n if (Math.abs(a.angle) < angleThresh) {\n return Math.abs(acy - bcy) < avgHeight * lineThresh\n }\n\n const rad = (a.angle * Math.PI) / 180\n const nx = -Math.sin(rad)\n const ny = Math.cos(rad)\n const distance = Math.abs((bcx - acx) * nx + (bcy - acy) * ny)\n return distance < avgHeight * lineThresh\n}\n\n/**\n * Greedily merge boxes that overlap and share a line. Port of Python\n * `Box.merge_overlapping_boxes`.\n *\n * Restarts the scan after each merge so a chain of boxes collapses fully in one\n * call, matching Python's behaviour.\n */\nexport function mergeOverlappingBoxes(\n boxes: readonly Box[],\n iouThreshold = 0.3,\n angleThresh = 10,\n lineThresh = 0.5\n): Box[] {\n // One greedy pass, exactly as Python does it: each box either starts a\n // group or is absorbed into an earlier one, and a group that has been\n // emitted is never revisited. Iterating to a fixed point instead merges\n // transitively -- a chain of boxes that each overlap their neighbour\n // collapses into one -- which quietly turns detections into whole lines.\n const merged: Box[] = []\n const used = new Array<boolean>(boxes.length).fill(false)\n\n for (let i = 0; i < boxes.length; i++) {\n if (used[i]) continue\n let current = boxes[i] as Box\n\n // Compares against the *growing* box, so a group can still extend as it\n // absorbs -- but only forwards, never back into what is already emitted.\n for (let j = i + 1; j < boxes.length; j++) {\n if (used[j]) continue\n const other = boxes[j] as Box\n if (\n boxIou(current, other) > iouThreshold &&\n isOnSameLine(current, other, angleThresh, lineThresh)\n ) {\n current = mergeBoxes(current, other)\n used[j] = true\n }\n }\n merged.push(current)\n used[i] = true\n }\n return merged\n}\n","/**\n * Image helpers built on OffscreenCanvas/ImageBitmap only.\n *\n * Deliberately DOM-free: no `document.createElement`, no `HTMLImageElement`,\n * no `toDataURL`. That is what lets the whole pipeline -- OCR and detection\n * included -- run inside a worker.\n */\n\nimport type { Box } from '../schemas/box.js'\nimport { scaleBox } from '../schemas/box.js'\nimport { boxPoints, type Point } from './geometry.js'\n\nexport interface Size {\n width: number\n height: number\n}\n\nfunction assertCanvasSupport(): void {\n if (typeof OffscreenCanvas === 'undefined') {\n throw new Error('OffscreenCanvas is unavailable. scaledp requires a browser or worker context.')\n }\n}\n\nexport function createCanvas(width: number, height: number): OffscreenCanvas {\n assertCanvasSupport()\n return new OffscreenCanvas(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)))\n}\n\nexport function context2d(canvas: OffscreenCanvas): OffscreenCanvasRenderingContext2D {\n const ctx = canvas.getContext('2d', { willReadFrequently: true })\n if (!ctx) throw new Error('Failed to acquire a 2D context')\n return ctx\n}\n\nexport async function decodeImage(data: Uint8Array | Blob): Promise<ImageBitmap> {\n if (data instanceof Blob) return createImageBitmap(data)\n // Copy into a fresh ArrayBuffer: the view may be a slice of a larger (or\n // shared) buffer, and Blob only accepts a plain ArrayBuffer.\n const bytes = new Uint8Array(data.byteLength)\n bytes.set(data)\n return createImageBitmap(new Blob([bytes.buffer]))\n}\n\nexport function toImageData(source: ImageBitmap | OffscreenCanvas): ImageData {\n if (source instanceof OffscreenCanvas) {\n return context2d(source).getImageData(0, 0, source.width, source.height)\n }\n const canvas = createCanvas(source.width, source.height)\n context2d(canvas).drawImage(source, 0, 0)\n return context2d(canvas).getImageData(0, 0, canvas.width, canvas.height)\n}\n\nexport function imageDataToCanvas(image: ImageData): OffscreenCanvas {\n const canvas = createCanvas(image.width, image.height)\n context2d(canvas).putImageData(image, 0, 0)\n return canvas\n}\n\nexport async function encodeImage(\n source: ImageData | OffscreenCanvas,\n type: 'image/png' | 'image/webp' | 'image/jpeg' = 'image/png',\n quality?: number\n): Promise<Uint8Array> {\n const canvas = source instanceof OffscreenCanvas ? source : imageDataToCanvas(source)\n const blob = await canvas.convertToBlob({ type, quality })\n return new Uint8Array(await blob.arrayBuffer())\n}\n\n/** Read the intrinsic size of encoded image bytes without keeping the bitmap. */\nexport async function probeImageSize(data: Uint8Array | Blob): Promise<Size> {\n const bitmap = await decodeImage(data)\n try {\n return { width: bitmap.width, height: bitmap.height }\n } finally {\n bitmap.close()\n }\n}\n\nexport interface LetterboxResult {\n canvas: OffscreenCanvas\n /** Uniform scale applied to the source. */\n scale: number\n /** Size the source occupies inside the target canvas. */\n resized: Size\n /** Original source size. */\n source: Size\n}\n\n/**\n * Fit an image into `target` preserving aspect ratio.\n *\n * `padding: 'end'` pads bottom and right only, matching PaddleOCR's detection\n * preprocessing -- coordinates then restore by dividing by `scale`, with no\n * offset to subtract. `padding: 'center'` centres the image, matching the YOLO\n * preprocessing, where the pad offsets must be subtracted before unscaling.\n */\nexport function letterbox(\n source: ImageBitmap | OffscreenCanvas,\n target: Size,\n opts: { padding?: 'end' | 'center'; fill?: string } = {}\n): LetterboxResult {\n const padding = opts.padding ?? 'end'\n const canvas = createCanvas(target.width, target.height)\n const ctx = context2d(canvas)\n\n ctx.fillStyle = opts.fill ?? '#ffffff'\n ctx.fillRect(0, 0, canvas.width, canvas.height)\n\n const scale = Math.min(target.width / source.width, target.height / source.height)\n const width = Math.trunc(source.width * scale)\n const height = Math.trunc(source.height * scale)\n const dx = padding === 'center' ? Math.trunc((target.width - width) / 2) : 0\n const dy = padding === 'center' ? Math.trunc((target.height - height) / 2) : 0\n\n ctx.drawImage(source, dx, dy, width, height)\n return {\n canvas,\n scale,\n resized: { width, height },\n source: { width: source.width, height: source.height },\n }\n}\n\n/** Uniform resize by a scale factor. */\nexport function resize(source: ImageBitmap | OffscreenCanvas, factor: number): OffscreenCanvas {\n const canvas = createCanvas(source.width * factor, source.height * factor)\n context2d(canvas).drawImage(source, 0, 0, canvas.width, canvas.height)\n return canvas\n}\n\n/**\n * Crop a box out of an image, straightening it if it is rotated.\n *\n * Port of `TesseractRecognizer._prepare_box_for_ocr`. For upright boxes this is\n * a plain crop; for rotated ones it applies the affine transform that maps the\n * box's own corners onto the destination rectangle, which is what makes a\n * recognizer see level text.\n */\n/**\n * The transform `cropBox` applies, as a function from crop pixel to source\n * pixel.\n *\n * Kept separate so anything that needs to map *back* -- word boxes recognised\n * inside a crop, say -- uses the same geometry the crop was made with, rather\n * than a second copy of it that can drift.\n */\nexport interface CropGeometry {\n /** The box after scaling and padding, in source coordinates. */\n scaled: Box\n width: number\n height: number\n /** Crop pixel -> source pixel. */\n map: (x: number, y: number) => Point\n}\n\nexport function cropGeometry(box: Box, opts: { scaleFactor?: number; padding?: number } = {}): CropGeometry {\n const scaled = scaleBox(box, opts.scaleFactor ?? 1, opts.padding ?? 0)\n const width = Math.max(1, scaled.width)\n const height = Math.max(1, scaled.height)\n\n const axisAligned: CropGeometry = {\n scaled,\n width,\n height,\n map: (x, y) => [scaled.x + x, scaled.y + y],\n }\n if (Math.abs(scaled.angle) < 3) return axisAligned\n\n // cv2.boxPoints order is BL, TL, TR, BR. Mapping TL/TR/BL onto the\n // destination corners fixes the affine transform; the fourth corner\n // follows because the source really is a parallelogram.\n const centre: Point = [scaled.x + width / 2, scaled.y + height / 2]\n const [, tl, tr, br] = boxPoints({\n center: centre,\n size: [width, height],\n angle: scaled.angle,\n })\n const bl: Point = [tl[0] + (br[0] - tr[0]), tl[1] + (br[1] - tr[1])]\n\n const ex: Point = [(tr[0] - tl[0]) / width, (tr[1] - tl[1]) / width]\n const ey: Point = [(bl[0] - tl[0]) / height, (bl[1] - tl[1]) / height]\n const det = ex[0] * ey[1] - ey[0] * ex[1]\n if (Math.abs(det) < 1e-9) return axisAligned\n\n return {\n scaled,\n width,\n height,\n map: (x, y) => [tl[0] + x * ex[0] + y * ey[0], tl[1] + x * ex[1] + y * ey[1]],\n }\n}\n\nexport function cropBox(\n source: ImageBitmap | OffscreenCanvas,\n box: Box,\n opts: { scaleFactor?: number; padding?: number } = {}\n): OffscreenCanvas {\n const { scaled, width, height, map } = cropGeometry(box, opts)\n const canvas = createCanvas(width, height)\n const ctx = context2d(canvas)\n\n // The origin and the two edge vectors are exactly the affine matrix, read\n // back off the mapping so the crop and the inverse cannot disagree.\n const [ox, oy] = map(0, 0)\n const [x1, y1] = map(1, 0)\n const [x2, y2] = map(0, 1)\n const ex: Point = [x1 - ox, y1 - oy]\n const ey: Point = [x2 - ox, y2 - oy]\n\n if (ex[1] === 0 && ey[0] === 0 && ex[0] === 1 && ey[1] === 1) {\n ctx.drawImage(source, scaled.x, scaled.y, width, height, 0, 0, width, height)\n return canvas\n }\n\n // Invert the source -> crop affine so the draw samples the right pixels.\n const det = ex[0] * ey[1] - ey[0] * ex[1]\n const a = ey[1] / det\n const b = -ex[1] / det\n const c = -ey[0] / det\n const d = ex[0] / det\n ctx.setTransform(a, b, c, d, -(a * ox + c * oy), -(b * ox + d * oy))\n ctx.drawImage(source, 0, 0)\n ctx.setTransform(1, 0, 0, 1, 0, 0)\n return canvas\n}\n\n/**\n * Build an NCHW float32 tensor from an image.\n *\n * `bgr` exists because ScaleDP's DBNet path converts RGB->BGR and never swaps\n * back, so the model is fed BGR channels against RGB ImageNet statistics.\n * Replicating that quirk is required for box parity.\n */\nexport function toNchwFloat32(\n image: ImageData,\n opts: { mean?: readonly number[]; std?: readonly number[]; scale?: number; bgr?: boolean } = {}\n): Float32Array {\n const { width, height, data } = image\n const scale = opts.scale ?? 1 / 255\n const mean = opts.mean ?? [0, 0, 0]\n const std = opts.std ?? [1, 1, 1]\n const order = opts.bgr ? [2, 1, 0] : [0, 1, 2]\n\n const plane = width * height\n const out = new Float32Array(3 * plane)\n for (let i = 0; i < plane; i++) {\n for (let c = 0; c < 3; c++) {\n const value = (data[i * 4 + (order[c] as number)] as number) * scale\n out[c * plane + i] = (value - (mean[c] as number)) / (std[c] as number)\n }\n }\n return out\n}\n\nexport const IMAGENET_MEAN = [0.485, 0.456, 0.406] as const\nexport const IMAGENET_STD = [0.229, 0.224, 0.225] as const\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"image-DRBsbv7G.js","names":[],"sources":["../src/core/geometry.ts","../src/schemas/box.ts","../src/core/image.ts"],"sourcesContent":["/**\n * OpenCV-compatible planar geometry, hand-ported so the browser build needs no\n * opencv.js. Conventions match cv2 exactly — `Box.from_polygon` and the DBNet\n * post-processor both depend on that, and a divergence here shifts every box.\n */\n\nexport type Point = [number, number]\n\nexport interface RotatedRect {\n /** Centre of the rectangle. */\n center: Point\n /** Side lengths along the rect's own axes. */\n size: [number, number]\n /**\n * Degrees in (0, 90], with `size[0]` (the width) lying along that direction.\n *\n * Verified against cv2 4.11 across 18 orientations. One caveat: for a\n * perfectly axis-aligned rect cv2 itself is inconsistent, returning either\n * `-0.0` or `90` depending on which hull edge its scan lands on. Those two\n * describe the same rectangle (a rectangle is invariant under a 180-degree\n * rotation once the sides are swapped), so we always report the `90` form.\n * Nothing downstream can distinguish them -- ImageDrawBoxes renders angle\n * and angle+180 identically.\n */\n angle: number\n}\n\nconst EPSILON = 1e-9\n\n/** Cross product of (o->a) and (o->b). > 0 means counter-clockwise. */\nfunction cross(o: Point, a: Point, b: Point): number {\n return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])\n}\n\n/**\n * Monotone-chain convex hull. Returns hull points counter-clockwise in a\n * y-down image coordinate system, without the duplicated closing point.\n */\nexport function convexHull(points: readonly Point[]): Point[] {\n const pts = [...points].sort((p, q) => (p[0] === q[0] ? p[1] - q[1] : p[0] - q[0]))\n // Drop exact duplicates; collinear runs are handled by the cross-product test.\n const uniq: Point[] = []\n for (const p of pts) {\n const last = uniq[uniq.length - 1]\n if (!last || last[0] !== p[0] || last[1] !== p[1]) uniq.push(p)\n }\n if (uniq.length < 3) return uniq\n\n const build = (source: Point[]): Point[] => {\n const chain: Point[] = []\n for (const p of source) {\n while (\n chain.length >= 2 &&\n cross(chain[chain.length - 2] as Point, chain[chain.length - 1] as Point, p) <= 0\n ) {\n chain.pop()\n }\n chain.push(p)\n }\n chain.pop()\n return chain\n }\n return [...build(uniq), ...build([...uniq].reverse())]\n}\n\n/**\n * Minimum-area enclosing rectangle via rotating calipers.\n *\n * The minimum-area rectangle always has a side flush with a convex-hull edge,\n * so testing one orientation per hull edge is exhaustive.\n */\nexport function minAreaRect(points: readonly Point[]): RotatedRect {\n if (points.length === 0) return { center: [0, 0], size: [0, 0], angle: 0 }\n\n const hull = convexHull(points)\n if (hull.length < 2) {\n const p = (hull[0] ?? points[0]) as Point\n return { center: [p[0], p[1]], size: [0, 0], angle: 0 }\n }\n\n let best: { area: number; center: Point; w: number; h: number; angle: number } | null = null\n\n for (let i = 0; i < hull.length; i++) {\n const a = hull[i] as Point\n const b = hull[(i + 1) % hull.length] as Point\n const len = Math.hypot(b[0] - a[0], b[1] - a[1])\n if (len < EPSILON) continue\n\n // Orthonormal frame with u along this hull edge.\n const ux = (b[0] - a[0]) / len\n const uy = (b[1] - a[1]) / len\n const vx = -uy\n const vy = ux\n\n let minU = Infinity\n let maxU = -Infinity\n let minV = Infinity\n let maxV = -Infinity\n for (const p of hull) {\n const pu = p[0] * ux + p[1] * uy\n const pv = p[0] * vx + p[1] * vy\n if (pu < minU) minU = pu\n if (pu > maxU) maxU = pu\n if (pv < minV) minV = pv\n if (pv > maxV) maxV = pv\n }\n\n const w = maxU - minU\n const h = maxV - minV\n const area = w * h\n if (best === null || area < best.area) {\n const cu = (minU + maxU) / 2\n const cv = (minV + maxV) / 2\n best = {\n area,\n center: [cu * ux + cv * vx, cu * uy + cv * vy],\n w,\n h,\n angle: (Math.atan2(uy, ux) * 180) / Math.PI,\n }\n }\n }\n\n if (best === null) return { center: [0, 0], size: [0, 0], angle: 0 }\n\n // cv2 reports the angle in (0, 90] -- half-open at zero, so a perfectly\n // axis-aligned rect is reported as 90 degrees with the sides swapped rather\n // than as 0. Each 90 degrees folded away swaps which side is \"width\",\n // because the frame's u and v axes trade places.\n let { angle, w, h } = best\n while (angle <= 0) {\n angle += 90\n ;[w, h] = [h, w]\n }\n while (angle > 90) {\n angle -= 90\n ;[w, h] = [h, w]\n }\n\n return { center: best.center, size: [w, h], angle }\n}\n\n/**\n * The 4 corners of a rotated rect, in cv2.boxPoints order.\n * For an upright rect in image coordinates that is: BL, TL, TR, BR.\n */\nexport function boxPoints(rect: RotatedRect): [Point, Point, Point, Point] {\n const [cx, cy] = rect.center\n const [w, h] = rect.size\n const rad = (rect.angle * Math.PI) / 180\n const b = Math.cos(rad) * 0.5\n const a = Math.sin(rad) * 0.5\n\n const p0: Point = [cx - a * h - b * w, cy + b * h - a * w]\n const p1: Point = [cx + a * h - b * w, cy - b * h - a * w]\n const p2: Point = [2 * cx - p0[0], 2 * cy - p0[1]]\n const p3: Point = [2 * cx - p1[0], 2 * cy - p1[1]]\n return [p0, p1, p2, p3]\n}\n\n/** Shoelace area of a simple polygon; always non-negative. */\nexport function polygonArea(points: readonly Point[]): number {\n let sum = 0\n for (let i = 0; i < points.length; i++) {\n const a = points[i] as Point\n const b = points[(i + 1) % points.length] as Point\n sum += a[0] * b[1] - b[0] * a[1]\n }\n return Math.abs(sum) / 2\n}\n\n/** Perimeter of a closed polygon. */\nexport function polygonPerimeter(points: readonly Point[]): number {\n let sum = 0\n for (let i = 0; i < points.length; i++) {\n const a = points[i] as Point\n const b = points[(i + 1) % points.length] as Point\n sum += Math.hypot(b[0] - a[0], b[1] - a[1])\n }\n return sum\n}\n","/**\n * Port of `scaledp/schemas/Box.py`.\n *\n * A Box is NOT xyxy and NOT a polygon. `x`/`y` is the top-left of the\n * *axis-aligned* box of the same size centred on the rotated rect's centre,\n * and `angle` is degrees about that same centre. `width` is always the longer\n * side. Getting this wrong silently shifts every downstream consumer, so the\n * conversions below mirror the Python implementation exactly.\n */\n\nimport { minAreaRect, type Point } from '../core/geometry.js'\n\nexport interface Box {\n text: string\n score: number\n /** Top-left x of the axis-aligned box of size width x height centred on the rect centre. */\n x: number\n y: number\n /** Always the longer side. */\n width: number\n height: number\n /** Degrees about the box centre, normalised to (-90, 270]. */\n angle: number\n}\n\n/** Two-corner shape: [[x0, y0], [x1, y1]]. */\nexport type BoxShape = [[number, number], [number, number]]\n\n/** Axis-aligned bounds: [x0, y0, x1, y1]. */\nexport type BBox = [number, number, number, number]\n\nexport type { Point }\n\n/** `abs(angle) >= 3` — matches Python's `is_rotated`, which tolerates OCR jitter. */\nexport const ROTATION_EPSILON_DEGREES = 3\n\nexport function createBox(init: Partial<Box> = {}): Box {\n return {\n text: init.text ?? '',\n score: init.score ?? 0,\n x: init.x ?? 0,\n y: init.y ?? 0,\n width: init.width ?? 0,\n height: init.height ?? 0,\n angle: init.angle ?? 0,\n }\n}\n\nexport function isRotated(box: Box): boolean {\n return Math.abs(box.angle) >= ROTATION_EPSILON_DEGREES\n}\n\nexport function bbox(box: Box, padding = 0): BBox {\n return [box.x - padding, box.y - padding, box.x + box.width + padding, box.y + box.height + padding]\n}\n\nexport function shape(box: Box, padding = 0): BoxShape {\n const [x0, y0, x1, y1] = bbox(box, padding)\n return [\n [x0, y0],\n [x1, y1],\n ]\n}\n\n/**\n * Scale a box, then apply padding.\n *\n * Note the padding is asymmetric, and deliberately so: Python subtracts it from\n * the origin and adds it to the size, so the box grows by `padding` on its left\n * and top while its right and bottom edges stay put. Growing all four sides\n * would need `padding * 2` on the size.\n */\nexport function scaleBox(box: Box, factor: number, padding = 0): Box {\n return {\n ...box,\n x: Math.round(box.x * factor - padding),\n y: Math.round(box.y * factor - padding),\n width: Math.round(box.width * factor + padding),\n height: Math.round(box.height * factor + padding),\n }\n}\n\nexport function boxFromBBox(box: BBox, opts: { angle?: number; text?: string; score?: number } = {}): Box {\n const [x0, y0, x1, y1] = box\n return {\n text: opts.text ?? '',\n score: opts.score ?? 0,\n x: Math.round(x0),\n y: Math.round(y0),\n width: Math.round(x1 - x0),\n height: Math.round(y1 - y0),\n angle: opts.angle ?? 0,\n }\n}\n\n/**\n * Build a Box from exactly 4 polygon points — port of Python `Box.from_polygon`.\n *\n * `width` is forced to be the longer side (subtracting 90 degrees from the angle\n * to compensate), then the angle is normalised to (-90, 270]. `x`/`y` are derived\n * from the centre, NOT from the polygon's bounding box.\n */\nexport function boxFromPolygon(\n points: readonly Point[],\n opts: { text?: string; score?: number; padding?: number } = {}\n): Box {\n if (points.length !== 4) {\n throw new Error(`boxFromPolygon expects exactly 4 points, received ${points.length}`)\n }\n const padding = opts.padding ?? 0\n const rect = minAreaRect(points)\n const [cx, cy] = rect.center\n\n let [width, height] = rect.size\n let angle = rect.angle\n if (width < height) {\n ;[width, height] = [height, width]\n angle -= 90\n }\n\n // Normalise to (-90, 270]. Note a rectangle is invariant under a 180-degree\n // rotation, so `angle` and `angle + 180` are interchangeable; which one you\n // get depends on the orientation minAreaRect happened to report.\n angle = ((angle % 360) + 360) % 360\n if (angle > 270) angle -= 360\n\n // Python clamps both dimensions to at least 1px so degenerate detections\n // stay usable as crop regions.\n width = Math.max(1, Math.round(width) + padding * 2)\n height = Math.max(1, Math.round(height) + padding * 2)\n\n return {\n text: opts.text ?? '',\n score: opts.score ?? 1,\n x: Math.round(cx - width / 2),\n y: Math.round(cy - height / 2),\n width,\n height,\n angle,\n }\n}\n\n/** Axis-aligned intersection-over-union. Ignores `angle`, exactly as Python does. */\nexport function boxIou(a: Box, b: Box): number {\n const [ax0, ay0, ax1, ay1] = bbox(a)\n const [bx0, by0, bx1, by1] = bbox(b)\n\n const ix = Math.min(ax1, bx1) - Math.max(ax0, bx0)\n const iy = Math.min(ay1, by1) - Math.max(ay0, by0)\n if (ix <= 0 || iy <= 0) return 0\n\n const intersection = ix * iy\n const union = a.width * a.height + b.width * b.height - intersection\n return union <= 0 ? 0 : intersection / union\n}\n\n/** Union of two boxes. Merging discards rotation — Python resets `angle` to 0. */\nexport function mergeBoxes(a: Box, b: Box): Box {\n const [ax0, ay0, ax1, ay1] = bbox(a)\n const [bx0, by0, bx1, by1] = bbox(b)\n const x = Math.min(ax0, bx0)\n const y = Math.min(ay0, by0)\n\n return {\n text: `${a.text} ${b.text}`.trim(),\n score: Math.min(a.score, b.score),\n x,\n y,\n width: Math.max(ax1, bx1) - x,\n height: Math.max(ay1, by1) - y,\n angle: 0,\n }\n}\n\n/**\n * Whether two boxes sit on the same text line.\n *\n * For near-horizontal boxes this compares vertical centres against the average\n * height. For rotated boxes it projects the centre offset onto the line's normal\n * (dx = -sin, dy = cos), so the test follows the text's own baseline.\n */\nexport function isOnSameLine(a: Box, b: Box, angleThresh = 10, lineThresh = 0.5): boolean {\n if (Math.abs(a.angle - b.angle) > angleThresh) return false\n\n const avgHeight = (a.height + b.height) / 2\n if (avgHeight <= 0) return false\n\n const acx = a.x + a.width / 2\n const acy = a.y + a.height / 2\n const bcx = b.x + b.width / 2\n const bcy = b.y + b.height / 2\n\n // Python branches on the caller's `angleThresh`, not on the much smaller\n // rotation epsilon: a box a few degrees off is still treated as horizontal\n // here, and compared by raw vertical distance. Using the epsilon instead\n // sends slightly skewed boxes down the projection path, where a large\n // horizontal gap cancels most of the vertical one and unrelated lines start\n // reading as the same line.\n if (Math.abs(a.angle) < angleThresh) {\n return Math.abs(acy - bcy) < avgHeight * lineThresh\n }\n\n const rad = (a.angle * Math.PI) / 180\n const nx = -Math.sin(rad)\n const ny = Math.cos(rad)\n const distance = Math.abs((bcx - acx) * nx + (bcy - acy) * ny)\n return distance < avgHeight * lineThresh\n}\n\n/**\n * Greedily merge boxes that overlap and share a line. Port of Python\n * `Box.merge_overlapping_boxes`.\n *\n * Restarts the scan after each merge so a chain of boxes collapses fully in one\n * call, matching Python's behaviour.\n */\nexport function mergeOverlappingBoxes(\n boxes: readonly Box[],\n iouThreshold = 0.3,\n angleThresh = 10,\n lineThresh = 0.5\n): Box[] {\n // One greedy pass, exactly as Python does it: each box either starts a\n // group or is absorbed into an earlier one, and a group that has been\n // emitted is never revisited. Iterating to a fixed point instead merges\n // transitively -- a chain of boxes that each overlap their neighbour\n // collapses into one -- which quietly turns detections into whole lines.\n const merged: Box[] = []\n const used = new Array<boolean>(boxes.length).fill(false)\n\n for (let i = 0; i < boxes.length; i++) {\n if (used[i]) continue\n let current = boxes[i] as Box\n\n // Compares against the *growing* box, so a group can still extend as it\n // absorbs -- but only forwards, never back into what is already emitted.\n for (let j = i + 1; j < boxes.length; j++) {\n if (used[j]) continue\n const other = boxes[j] as Box\n if (\n boxIou(current, other) > iouThreshold &&\n isOnSameLine(current, other, angleThresh, lineThresh)\n ) {\n current = mergeBoxes(current, other)\n used[j] = true\n }\n }\n merged.push(current)\n used[i] = true\n }\n return merged\n}\n","/**\n * Image helpers built on OffscreenCanvas/ImageBitmap only.\n *\n * Deliberately DOM-free: no `document.createElement`, no `HTMLImageElement`,\n * no `toDataURL`. That is what lets the whole pipeline -- OCR and detection\n * included -- run inside a worker.\n */\n\nimport type { Box } from '../schemas/box.js'\nimport { scaleBox } from '../schemas/box.js'\nimport { boxPoints, type Point } from './geometry.js'\n\nexport interface Size {\n width: number\n height: number\n}\n\nfunction assertCanvasSupport(): void {\n if (typeof OffscreenCanvas === 'undefined') {\n throw new Error('OffscreenCanvas is unavailable. scaledp requires a browser or worker context.')\n }\n}\n\nexport function createCanvas(width: number, height: number): OffscreenCanvas {\n assertCanvasSupport()\n return new OffscreenCanvas(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)))\n}\n\nexport function context2d(canvas: OffscreenCanvas): OffscreenCanvasRenderingContext2D {\n const ctx = canvas.getContext('2d', { willReadFrequently: true })\n if (!ctx) throw new Error('Failed to acquire a 2D context')\n return ctx\n}\n\nexport async function decodeImage(data: Uint8Array | Blob): Promise<ImageBitmap> {\n if (data instanceof Blob) return createImageBitmap(data)\n // Copy into a fresh ArrayBuffer: the view may be a slice of a larger (or\n // shared) buffer, and Blob only accepts a plain ArrayBuffer.\n const bytes = new Uint8Array(data.byteLength)\n bytes.set(data)\n return createImageBitmap(new Blob([bytes.buffer]))\n}\n\nexport function toImageData(source: ImageBitmap | OffscreenCanvas): ImageData {\n if (source instanceof OffscreenCanvas) {\n return context2d(source).getImageData(0, 0, source.width, source.height)\n }\n const canvas = createCanvas(source.width, source.height)\n context2d(canvas).drawImage(source, 0, 0)\n return context2d(canvas).getImageData(0, 0, canvas.width, canvas.height)\n}\n\nexport function imageDataToCanvas(image: ImageData): OffscreenCanvas {\n const canvas = createCanvas(image.width, image.height)\n context2d(canvas).putImageData(image, 0, 0)\n return canvas\n}\n\nexport async function encodeImage(\n source: ImageData | OffscreenCanvas,\n type: 'image/png' | 'image/webp' | 'image/jpeg' = 'image/png',\n quality?: number\n): Promise<Uint8Array> {\n const canvas = source instanceof OffscreenCanvas ? source : imageDataToCanvas(source)\n const blob = await canvas.convertToBlob({ type, quality })\n return new Uint8Array(await blob.arrayBuffer())\n}\n\n/** Read the intrinsic size of encoded image bytes without keeping the bitmap. */\nexport async function probeImageSize(data: Uint8Array | Blob): Promise<Size> {\n const bitmap = await decodeImage(data)\n try {\n return { width: bitmap.width, height: bitmap.height }\n } finally {\n bitmap.close()\n }\n}\n\nexport interface LetterboxResult {\n canvas: OffscreenCanvas\n /** Uniform scale applied to the source. */\n scale: number\n /** Size the source occupies inside the target canvas. */\n resized: Size\n /** Original source size. */\n source: Size\n}\n\n/**\n * Fit an image into `target` preserving aspect ratio.\n *\n * `padding: 'end'` pads bottom and right only, matching PaddleOCR's detection\n * preprocessing -- coordinates then restore by dividing by `scale`, with no\n * offset to subtract. `padding: 'center'` centres the image, matching the YOLO\n * preprocessing, where the pad offsets must be subtracted before unscaling.\n */\nexport function letterbox(\n source: ImageBitmap | OffscreenCanvas,\n target: Size,\n opts: { padding?: 'end' | 'center'; fill?: string } = {}\n): LetterboxResult {\n const padding = opts.padding ?? 'end'\n const canvas = createCanvas(target.width, target.height)\n const ctx = context2d(canvas)\n\n ctx.fillStyle = opts.fill ?? '#ffffff'\n ctx.fillRect(0, 0, canvas.width, canvas.height)\n\n const scale = Math.min(target.width / source.width, target.height / source.height)\n const width = Math.trunc(source.width * scale)\n const height = Math.trunc(source.height * scale)\n const dx = padding === 'center' ? Math.trunc((target.width - width) / 2) : 0\n const dy = padding === 'center' ? Math.trunc((target.height - height) / 2) : 0\n\n ctx.drawImage(source, dx, dy, width, height)\n return {\n canvas,\n scale,\n resized: { width, height },\n source: { width: source.width, height: source.height },\n }\n}\n\n/** Uniform resize by a scale factor. */\nexport function resize(source: ImageBitmap | OffscreenCanvas, factor: number): OffscreenCanvas {\n const canvas = createCanvas(source.width * factor, source.height * factor)\n context2d(canvas).drawImage(source, 0, 0, canvas.width, canvas.height)\n return canvas\n}\n\n/**\n * Crop a box out of an image, straightening it if it is rotated.\n *\n * Port of `TesseractRecognizer._prepare_box_for_ocr`. For upright boxes this is\n * a plain crop; for rotated ones it applies the affine transform that maps the\n * box's own corners onto the destination rectangle, which is what makes a\n * recognizer see level text.\n */\n/**\n * The transform `cropBox` applies, as a function from crop pixel to source\n * pixel.\n *\n * Kept separate so anything that needs to map *back* -- word boxes recognised\n * inside a crop, say -- uses the same geometry the crop was made with, rather\n * than a second copy of it that can drift.\n */\nexport interface CropGeometry {\n /** The box after scaling and padding, in source coordinates. */\n scaled: Box\n width: number\n height: number\n /** Crop pixel -> source pixel. */\n map: (x: number, y: number) => Point\n}\n\nexport function cropGeometry(box: Box, opts: { scaleFactor?: number; padding?: number } = {}): CropGeometry {\n const scaled = scaleBox(box, opts.scaleFactor ?? 1, opts.padding ?? 0)\n const width = Math.max(1, scaled.width)\n const height = Math.max(1, scaled.height)\n\n const axisAligned: CropGeometry = {\n scaled,\n width,\n height,\n map: (x, y) => [scaled.x + x, scaled.y + y],\n }\n if (Math.abs(scaled.angle) < 3) return axisAligned\n\n // cv2.boxPoints order is BL, TL, TR, BR. Mapping TL/TR/BL onto the\n // destination corners fixes the affine transform; the fourth corner\n // follows because the source really is a parallelogram.\n const centre: Point = [scaled.x + width / 2, scaled.y + height / 2]\n const [, tl, tr, br] = boxPoints({\n center: centre,\n size: [width, height],\n angle: scaled.angle,\n })\n const bl: Point = [tl[0] + (br[0] - tr[0]), tl[1] + (br[1] - tr[1])]\n\n const ex: Point = [(tr[0] - tl[0]) / width, (tr[1] - tl[1]) / width]\n const ey: Point = [(bl[0] - tl[0]) / height, (bl[1] - tl[1]) / height]\n const det = ex[0] * ey[1] - ey[0] * ex[1]\n if (Math.abs(det) < 1e-9) return axisAligned\n\n return {\n scaled,\n width,\n height,\n map: (x, y) => [tl[0] + x * ex[0] + y * ey[0], tl[1] + x * ex[1] + y * ey[1]],\n }\n}\n\nexport function cropBox(\n source: ImageBitmap | OffscreenCanvas,\n box: Box,\n opts: { scaleFactor?: number; padding?: number } = {}\n): OffscreenCanvas {\n const { scaled, width, height, map } = cropGeometry(box, opts)\n const canvas = createCanvas(width, height)\n const ctx = context2d(canvas)\n\n // The origin and the two edge vectors are exactly the affine matrix, read\n // back off the mapping so the crop and the inverse cannot disagree.\n const [ox, oy] = map(0, 0)\n const [x1, y1] = map(1, 0)\n const [x2, y2] = map(0, 1)\n const ex: Point = [x1 - ox, y1 - oy]\n const ey: Point = [x2 - ox, y2 - oy]\n\n if (ex[1] === 0 && ey[0] === 0 && ex[0] === 1 && ey[1] === 1) {\n ctx.drawImage(source, scaled.x, scaled.y, width, height, 0, 0, width, height)\n return canvas\n }\n\n // Invert the source -> crop affine so the draw samples the right pixels.\n const det = ex[0] * ey[1] - ey[0] * ex[1]\n const a = ey[1] / det\n const b = -ex[1] / det\n const c = -ey[0] / det\n const d = ex[0] / det\n ctx.setTransform(a, b, c, d, -(a * ox + c * oy), -(b * ox + d * oy))\n ctx.drawImage(source, 0, 0)\n ctx.setTransform(1, 0, 0, 1, 0, 0)\n return canvas\n}\n\n/**\n * Turn a crop 180 degrees, so an inverted line reads the right way up.\n *\n * Maps (x, y) to (w - x, h - y) -- a recognizer mapping word boxes back out of\n * an inverted crop has to undo exactly that.\n */\nexport function rotate180(source: ImageBitmap | OffscreenCanvas): OffscreenCanvas {\n const out = createCanvas(source.width, source.height)\n const ctx = context2d(out)\n ctx.translate(source.width / 2, source.height / 2)\n ctx.rotate(Math.PI)\n ctx.drawImage(source, -source.width / 2, -source.height / 2)\n return out\n}\n\n/**\n * Build an NCHW float32 tensor from an image.\n *\n * `bgr` exists because ScaleDP's DBNet path converts RGB->BGR and never swaps\n * back, so the model is fed BGR channels against RGB ImageNet statistics.\n * Replicating that quirk is required for box parity.\n */\nexport function toNchwFloat32(\n image: ImageData,\n opts: { mean?: readonly number[]; std?: readonly number[]; scale?: number; bgr?: boolean } = {}\n): Float32Array {\n const { width, height, data } = image\n const scale = opts.scale ?? 1 / 255\n const mean = opts.mean ?? [0, 0, 0]\n const std = opts.std ?? [1, 1, 1]\n const order = opts.bgr ? [2, 1, 0] : [0, 1, 2]\n\n const plane = width * height\n const out = new Float32Array(3 * plane)\n for (let i = 0; i < plane; i++) {\n for (let c = 0; c < 3; c++) {\n const value = (data[i * 4 + (order[c] as number)] as number) * scale\n out[c * plane + i] = (value - (mean[c] as number)) / (std[c] as number)\n }\n }\n return out\n}\n\nexport const IMAGENET_MEAN = [0.485, 0.456, 0.406] as const\nexport const IMAGENET_STD = [0.229, 0.224, 0.225] as const\n"],"mappings":";AA2BA,MAAM,UAAU;;AAGhB,SAAS,MAAM,GAAU,GAAU,GAAkB;CACjD,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE;AACrE;;;;;AAMA,SAAgB,WAAW,QAAmC;CAC1D,MAAM,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,MAAM,GAAG,MAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAG;CAElF,MAAM,OAAgB,CAAC;CACvB,KAAK,MAAM,KAAK,KAAK;EACjB,MAAM,OAAO,KAAK,KAAK,SAAS;EAChC,IAAI,CAAC,QAAQ,KAAK,OAAO,EAAE,MAAM,KAAK,OAAO,EAAE,IAAI,KAAK,KAAK,CAAC;CAClE;CACA,IAAI,KAAK,SAAS,GAAG,OAAO;CAE5B,MAAM,SAAS,WAA6B;EACxC,MAAM,QAAiB,CAAC;EACxB,KAAK,MAAM,KAAK,QAAQ;GACpB,OACI,MAAM,UAAU,KAChB,MAAM,MAAM,MAAM,SAAS,IAAa,MAAM,MAAM,SAAS,IAAa,CAAC,KAAK,GAEhF,MAAM,IAAI;GAEd,MAAM,KAAK,CAAC;EAChB;EACA,MAAM,IAAI;EACV,OAAO;CACX;CACA,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;AACzD;;;;;;;AAQA,SAAgB,YAAY,QAAuC;CAC/D,IAAI,OAAO,WAAW,GAAG,OAAO;EAAE,QAAQ,CAAC,GAAG,CAAC;EAAG,MAAM,CAAC,GAAG,CAAC;EAAG,OAAO;CAAE;CAEzE,MAAM,OAAO,WAAW,MAAM;CAC9B,IAAI,KAAK,SAAS,GAAG;EACjB,MAAM,IAAK,KAAK,MAAM,OAAO;EAC7B,OAAO;GAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,EAAE;GAAG,MAAM,CAAC,GAAG,CAAC;GAAG,OAAO;EAAE;CAC1D;CAEA,IAAI,OAAoF;CAExF,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EAClC,MAAM,IAAI,KAAK;EACf,MAAM,IAAI,MAAM,IAAI,KAAK,KAAK;EAC9B,MAAM,MAAM,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;EAC/C,IAAI,MAAM,SAAS;EAGnB,MAAM,MAAM,EAAE,KAAK,EAAE,MAAM;EAC3B,MAAM,MAAM,EAAE,KAAK,EAAE,MAAM;EAC3B,MAAM,KAAK,CAAC;EACZ,MAAM,KAAK;EAEX,IAAI,OAAO;EACX,IAAI,OAAO;EACX,IAAI,OAAO;EACX,IAAI,OAAO;EACX,KAAK,MAAM,KAAK,MAAM;GAClB,MAAM,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK;GAC9B,MAAM,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK;GAC9B,IAAI,KAAK,MAAM,OAAO;GACtB,IAAI,KAAK,MAAM,OAAO;GACtB,IAAI,KAAK,MAAM,OAAO;GACtB,IAAI,KAAK,MAAM,OAAO;EAC1B;EAEA,MAAM,IAAI,OAAO;EACjB,MAAM,IAAI,OAAO;EACjB,MAAM,OAAO,IAAI;EACjB,IAAI,SAAS,QAAQ,OAAO,KAAK,MAAM;GACnC,MAAM,MAAM,OAAO,QAAQ;GAC3B,MAAM,MAAM,OAAO,QAAQ;GAC3B,OAAO;IACH;IACA,QAAQ,CAAC,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,EAAE;IAC7C;IACA;IACA,OAAQ,KAAK,MAAM,IAAI,EAAE,IAAI,MAAO,KAAK;GAC7C;EACJ;CACJ;CAEA,IAAI,SAAS,MAAM,OAAO;EAAE,QAAQ,CAAC,GAAG,CAAC;EAAG,MAAM,CAAC,GAAG,CAAC;EAAG,OAAO;CAAE;CAMnE,IAAI,EAAE,OAAO,GAAG,MAAM;CACtB,OAAO,SAAS,GAAG;EACf,SAAS;EACR,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC;CACnB;CACA,OAAO,QAAQ,IAAI;EACf,SAAS;EACR,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC;CACnB;CAEA,OAAO;EAAE,QAAQ,KAAK;EAAQ,MAAM,CAAC,GAAG,CAAC;EAAG;CAAM;AACtD;;;;;AAMA,SAAgB,UAAU,MAAiD;CACvE,MAAM,CAAC,IAAI,MAAM,KAAK;CACtB,MAAM,CAAC,GAAG,KAAK,KAAK;CACpB,MAAM,MAAO,KAAK,QAAQ,KAAK,KAAM;CACrC,MAAM,IAAI,KAAK,IAAI,GAAG,IAAI;CAC1B,MAAM,IAAI,KAAK,IAAI,GAAG,IAAI;CAE1B,MAAM,KAAY,CAAC,KAAK,IAAI,IAAI,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,CAAC;CACzD,MAAM,KAAY,CAAC,KAAK,IAAI,IAAI,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,CAAC;CAGzD,OAAO;EAAC;EAAI;EAAI,CAFG,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAE9B;EAAG,CADD,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAC1B;CAAC;AAC1B;;AAGA,SAAgB,YAAY,QAAkC;CAC1D,IAAI,MAAM;CACV,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;EACpC,MAAM,IAAI,OAAO;EACjB,MAAM,IAAI,QAAQ,IAAI,KAAK,OAAO;EAClC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE;CAClC;CACA,OAAO,KAAK,IAAI,GAAG,IAAI;AAC3B;;AAGA,SAAgB,iBAAiB,QAAkC;CAC/D,IAAI,MAAM;CACV,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;EACpC,MAAM,IAAI,OAAO;EACjB,MAAM,IAAI,QAAQ,IAAI,KAAK,OAAO;EAClC,OAAO,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;CAC9C;CACA,OAAO;AACX;;;;;;;;;;;;;AClJA,MAAa,2BAA2B;AAExC,SAAgB,UAAU,OAAqB,CAAC,GAAQ;CACpD,OAAO;EACH,MAAM,KAAK,QAAQ;EACnB,OAAO,KAAK,SAAS;EACrB,GAAG,KAAK,KAAK;EACb,GAAG,KAAK,KAAK;EACb,OAAO,KAAK,SAAS;EACrB,QAAQ,KAAK,UAAU;EACvB,OAAO,KAAK,SAAS;CACzB;AACJ;AAEA,SAAgB,UAAU,KAAmB;CACzC,OAAO,KAAK,IAAI,IAAI,KAAK,KAAA;AAC7B;AAEA,SAAgB,KAAK,KAAU,UAAU,GAAS;CAC9C,OAAO;EAAC,IAAI,IAAI;EAAS,IAAI,IAAI;EAAS,IAAI,IAAI,IAAI,QAAQ;EAAS,IAAI,IAAI,IAAI,SAAS;CAAO;AACvG;AAEA,SAAgB,MAAM,KAAU,UAAU,GAAa;CACnD,MAAM,CAAC,IAAI,IAAI,IAAI,MAAM,KAAK,KAAK,OAAO;CAC1C,OAAO,CACH,CAAC,IAAI,EAAE,GACP,CAAC,IAAI,EAAE,CACX;AACJ;;;;;;;;;AAUA,SAAgB,SAAS,KAAU,QAAgB,UAAU,GAAQ;CACjE,OAAO;EACH,GAAG;EACH,GAAG,KAAK,MAAM,IAAI,IAAI,SAAS,OAAO;EACtC,GAAG,KAAK,MAAM,IAAI,IAAI,SAAS,OAAO;EACtC,OAAO,KAAK,MAAM,IAAI,QAAQ,SAAS,OAAO;EAC9C,QAAQ,KAAK,MAAM,IAAI,SAAS,SAAS,OAAO;CACpD;AACJ;AAEA,SAAgB,YAAY,KAAW,OAA0D,CAAC,GAAQ;CACtG,MAAM,CAAC,IAAI,IAAI,IAAI,MAAM;CACzB,OAAO;EACH,MAAM,KAAK,QAAQ;EACnB,OAAO,KAAK,SAAS;EACrB,GAAG,KAAK,MAAM,EAAE;EAChB,GAAG,KAAK,MAAM,EAAE;EAChB,OAAO,KAAK,MAAM,KAAK,EAAE;EACzB,QAAQ,KAAK,MAAM,KAAK,EAAE;EAC1B,OAAO,KAAK,SAAS;CACzB;AACJ;;;;;;;;AASA,SAAgB,eACZ,QACA,OAA4D,CAAC,GAC1D;CACH,IAAI,OAAO,WAAW,GAClB,MAAM,IAAI,MAAM,qDAAqD,OAAO,QAAQ;CAExF,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,CAAC,IAAI,MAAM,KAAK;CAEtB,IAAI,CAAC,OAAO,UAAU,KAAK;CAC3B,IAAI,QAAQ,KAAK;CACjB,IAAI,QAAQ,QAAQ;EACf,CAAC,OAAO,UAAU,CAAC,QAAQ,KAAK;EACjC,SAAS;CACb;CAKA,SAAU,QAAQ,MAAO,OAAO;CAChC,IAAI,QAAQ,KAAK,SAAS;CAI1B,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC;CACnD,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,IAAI,UAAU,CAAC;CAErD,OAAO;EACH,MAAM,KAAK,QAAQ;EACnB,OAAO,KAAK,SAAS;EACrB,GAAG,KAAK,MAAM,KAAK,QAAQ,CAAC;EAC5B,GAAG,KAAK,MAAM,KAAK,SAAS,CAAC;EAC7B;EACA;EACA;CACJ;AACJ;;AAGA,SAAgB,OAAO,GAAQ,GAAgB;CAC3C,MAAM,CAAC,KAAK,KAAK,KAAK,OAAO,KAAK,CAAC;CACnC,MAAM,CAAC,KAAK,KAAK,KAAK,OAAO,KAAK,CAAC;CAEnC,MAAM,KAAK,KAAK,IAAI,KAAK,GAAG,IAAI,KAAK,IAAI,KAAK,GAAG;CACjD,MAAM,KAAK,KAAK,IAAI,KAAK,GAAG,IAAI,KAAK,IAAI,KAAK,GAAG;CACjD,IAAI,MAAM,KAAK,MAAM,GAAG,OAAO;CAE/B,MAAM,eAAe,KAAK;CAC1B,MAAM,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS;CACxD,OAAO,SAAS,IAAI,IAAI,eAAe;AAC3C;;AAGA,SAAgB,WAAW,GAAQ,GAAa;CAC5C,MAAM,CAAC,KAAK,KAAK,KAAK,OAAO,KAAK,CAAC;CACnC,MAAM,CAAC,KAAK,KAAK,KAAK,OAAO,KAAK,CAAC;CACnC,MAAM,IAAI,KAAK,IAAI,KAAK,GAAG;CAC3B,MAAM,IAAI,KAAK,IAAI,KAAK,GAAG;CAE3B,OAAO;EACH,MAAM,GAAG,EAAE,KAAK,GAAG,EAAE,OAAO,KAAK;EACjC,OAAO,KAAK,IAAI,EAAE,OAAO,EAAE,KAAK;EAChC;EACA;EACA,OAAO,KAAK,IAAI,KAAK,GAAG,IAAI;EAC5B,QAAQ,KAAK,IAAI,KAAK,GAAG,IAAI;EAC7B,OAAO;CACX;AACJ;;;;;;;;AASA,SAAgB,aAAa,GAAQ,GAAQ,cAAc,IAAI,aAAa,IAAc;CACtF,IAAI,KAAK,IAAI,EAAE,QAAQ,EAAE,KAAK,IAAI,aAAa,OAAO;CAEtD,MAAM,aAAa,EAAE,SAAS,EAAE,UAAU;CAC1C,IAAI,aAAa,GAAG,OAAO;CAE3B,MAAM,MAAM,EAAE,IAAI,EAAE,QAAQ;CAC5B,MAAM,MAAM,EAAE,IAAI,EAAE,SAAS;CAC7B,MAAM,MAAM,EAAE,IAAI,EAAE,QAAQ;CAC5B,MAAM,MAAM,EAAE,IAAI,EAAE,SAAS;CAQ7B,IAAI,KAAK,IAAI,EAAE,KAAK,IAAI,aACpB,OAAO,KAAK,IAAI,MAAM,GAAG,IAAI,YAAY;CAG7C,MAAM,MAAO,EAAE,QAAQ,KAAK,KAAM;CAClC,MAAM,KAAK,CAAC,KAAK,IAAI,GAAG;CACxB,MAAM,KAAK,KAAK,IAAI,GAAG;CAEvB,OADiB,KAAK,KAAK,MAAM,OAAO,MAAM,MAAM,OAAO,EAC7C,IAAI,YAAY;AAClC;;;;;;;;AASA,SAAgB,sBACZ,OACA,eAAe,IACf,cAAc,IACd,aAAa,IACR;CAML,MAAM,SAAgB,CAAC;CACvB,MAAM,OAAO,IAAI,MAAe,MAAM,MAAM,CAAC,CAAC,KAAK,KAAK;CAExD,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACnC,IAAI,KAAK,IAAI;EACb,IAAI,UAAU,MAAM;EAIpB,KAAK,IAAI,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACvC,IAAI,KAAK,IAAI;GACb,MAAM,QAAQ,MAAM;GACpB,IACI,OAAO,SAAS,KAAK,IAAI,gBACzB,aAAa,SAAS,OAAO,aAAa,UAAU,GACtD;IACE,UAAU,WAAW,SAAS,KAAK;IACnC,KAAK,KAAK;GACd;EACJ;EACA,OAAO,KAAK,OAAO;EACnB,KAAK,KAAK;CACd;CACA,OAAO;AACX;;;AC1OA,SAAS,sBAA4B;CACjC,IAAI,OAAO,oBAAoB,aAC3B,MAAM,IAAI,MAAM,+EAA+E;AAEvG;AAEA,SAAgB,aAAa,OAAe,QAAiC;CACzE,oBAAoB;CACpB,OAAO,IAAI,gBAAgB,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,CAAC,CAAC;AAC9F;AAEA,SAAgB,UAAU,QAA4D;CAClF,MAAM,MAAM,OAAO,WAAW,MAAM,EAAE,oBAAoB,KAAK,CAAC;CAChE,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,gCAAgC;CAC1D,OAAO;AACX;AAEA,eAAsB,YAAY,MAA+C;CAC7E,IAAI,gBAAgB,MAAM,OAAO,kBAAkB,IAAI;CAGvD,MAAM,QAAQ,IAAI,WAAW,KAAK,UAAU;CAC5C,MAAM,IAAI,IAAI;CACd,OAAO,kBAAkB,IAAI,KAAK,CAAC,MAAM,MAAM,CAAC,CAAC;AACrD;AAEA,SAAgB,YAAY,QAAkD;CAC1E,IAAI,kBAAkB,iBAClB,OAAO,UAAU,MAAM,CAAC,CAAC,aAAa,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;CAE3E,MAAM,SAAS,aAAa,OAAO,OAAO,OAAO,MAAM;CACvD,UAAU,MAAM,CAAC,CAAC,UAAU,QAAQ,GAAG,CAAC;CACxC,OAAO,UAAU,MAAM,CAAC,CAAC,aAAa,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AAC3E;AAEA,SAAgB,kBAAkB,OAAmC;CACjE,MAAM,SAAS,aAAa,MAAM,OAAO,MAAM,MAAM;CACrD,UAAU,MAAM,CAAC,CAAC,aAAa,OAAO,GAAG,CAAC;CAC1C,OAAO;AACX;AAEA,eAAsB,YAClB,QACA,OAAkD,aAClD,SACmB;CAEnB,MAAM,OAAO,OADE,kBAAkB,kBAAkB,SAAS,kBAAkB,MAAM,EAAA,CAC1D,cAAc;EAAE;EAAM;CAAQ,CAAC;CACzD,OAAO,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;AAClD;;AAGA,eAAsB,eAAe,MAAwC;CACzE,MAAM,SAAS,MAAM,YAAY,IAAI;CACrC,IAAI;EACA,OAAO;GAAE,OAAO,OAAO;GAAO,QAAQ,OAAO;EAAO;CACxD,UAAU;EACN,OAAO,MAAM;CACjB;AACJ;;;;;;;;;AAoBA,SAAgB,UACZ,QACA,QACA,OAAsD,CAAC,GACxC;CACf,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,SAAS,aAAa,OAAO,OAAO,OAAO,MAAM;CACvD,MAAM,MAAM,UAAU,MAAM;CAE5B,IAAI,YAAY,KAAK,QAAQ;CAC7B,IAAI,SAAS,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;CAE9C,MAAM,QAAQ,KAAK,IAAI,OAAO,QAAQ,OAAO,OAAO,OAAO,SAAS,OAAO,MAAM;CACjF,MAAM,QAAQ,KAAK,MAAM,OAAO,QAAQ,KAAK;CAC7C,MAAM,SAAS,KAAK,MAAM,OAAO,SAAS,KAAK;CAC/C,MAAM,KAAK,YAAY,WAAW,KAAK,OAAO,OAAO,QAAQ,SAAS,CAAC,IAAI;CAC3E,MAAM,KAAK,YAAY,WAAW,KAAK,OAAO,OAAO,SAAS,UAAU,CAAC,IAAI;CAE7E,IAAI,UAAU,QAAQ,IAAI,IAAI,OAAO,MAAM;CAC3C,OAAO;EACH;EACA;EACA,SAAS;GAAE;GAAO;EAAO;EACzB,QAAQ;GAAE,OAAO,OAAO;GAAO,QAAQ,OAAO;EAAO;CACzD;AACJ;;AAGA,SAAgB,OAAO,QAAuC,QAAiC;CAC3F,MAAM,SAAS,aAAa,OAAO,QAAQ,QAAQ,OAAO,SAAS,MAAM;CACzE,UAAU,MAAM,CAAC,CAAC,UAAU,QAAQ,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;CACrE,OAAO;AACX;AA2BA,SAAgB,aAAa,KAAU,OAAmD,CAAC,GAAiB;CACxG,MAAM,SAAS,SAAS,KAAK,KAAK,eAAe,GAAG,KAAK,WAAW,CAAC;CACrE,MAAM,QAAQ,KAAK,IAAI,GAAG,OAAO,KAAK;CACtC,MAAM,SAAS,KAAK,IAAI,GAAG,OAAO,MAAM;CAExC,MAAM,cAA4B;EAC9B;EACA;EACA;EACA,MAAM,GAAG,MAAM,CAAC,OAAO,IAAI,GAAG,OAAO,IAAI,CAAC;CAC9C;CACA,IAAI,KAAK,IAAI,OAAO,KAAK,IAAI,GAAG,OAAO;CAMvC,MAAM,GAAG,IAAI,IAAI,MAAM,UAAU;EAC7B,QAAQ,CAFW,OAAO,IAAI,QAAQ,GAAG,OAAO,IAAI,SAAS,CAErD;EACR,MAAM,CAAC,OAAO,MAAM;EACpB,OAAO,OAAO;CAClB,CAAC;CACD,MAAM,KAAY,CAAC,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,GAAG;CAEnE,MAAM,KAAY,EAAE,GAAG,KAAK,GAAG,MAAM,QAAQ,GAAG,KAAK,GAAG,MAAM,KAAK;CACnE,MAAM,KAAY,EAAE,GAAG,KAAK,GAAG,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,MAAM;CACrE,MAAM,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG;CACvC,IAAI,KAAK,IAAI,GAAG,IAAI,MAAM,OAAO;CAEjC,OAAO;EACH;EACA;EACA;EACA,MAAM,GAAG,MAAM,CAAC,GAAG,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,EAAE;CAChF;AACJ;AAEA,SAAgB,QACZ,QACA,KACA,OAAmD,CAAC,GACrC;CACf,MAAM,EAAE,QAAQ,OAAO,QAAQ,QAAQ,aAAa,KAAK,IAAI;CAC7D,MAAM,SAAS,aAAa,OAAO,MAAM;CACzC,MAAM,MAAM,UAAU,MAAM;CAI5B,MAAM,CAAC,IAAI,MAAM,IAAI,GAAG,CAAC;CACzB,MAAM,CAAC,IAAI,MAAM,IAAI,GAAG,CAAC;CACzB,MAAM,CAAC,IAAI,MAAM,IAAI,GAAG,CAAC;CACzB,MAAM,KAAY,CAAC,KAAK,IAAI,KAAK,EAAE;CACnC,MAAM,KAAY,CAAC,KAAK,IAAI,KAAK,EAAE;CAEnC,IAAI,GAAG,OAAO,KAAK,GAAG,OAAO,KAAK,GAAG,OAAO,KAAK,GAAG,OAAO,GAAG;EAC1D,IAAI,UAAU,QAAQ,OAAO,GAAG,OAAO,GAAG,OAAO,QAAQ,GAAG,GAAG,OAAO,MAAM;EAC5E,OAAO;CACX;CAGA,MAAM,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG;CACvC,MAAM,IAAI,GAAG,KAAK;CAClB,MAAM,IAAI,CAAC,GAAG,KAAK;CACnB,MAAM,IAAI,CAAC,GAAG,KAAK;CACnB,MAAM,IAAI,GAAG,KAAK;CAClB,IAAI,aAAa,GAAG,GAAG,GAAG,GAAG,EAAE,IAAI,KAAK,IAAI,KAAK,EAAE,IAAI,KAAK,IAAI,GAAG;CACnE,IAAI,UAAU,QAAQ,GAAG,CAAC;CAC1B,IAAI,aAAa,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;CACjC,OAAO;AACX;;;;;;;AAQA,SAAgB,UAAU,QAAwD;CAC9E,MAAM,MAAM,aAAa,OAAO,OAAO,OAAO,MAAM;CACpD,MAAM,MAAM,UAAU,GAAG;CACzB,IAAI,UAAU,OAAO,QAAQ,GAAG,OAAO,SAAS,CAAC;CACjD,IAAI,OAAO,KAAK,EAAE;CAClB,IAAI,UAAU,QAAQ,CAAC,OAAO,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC;CAC3D,OAAO;AACX;;;;;;;;AASA,SAAgB,cACZ,OACA,OAA6F,CAAC,GAClF;CACZ,MAAM,EAAE,OAAO,QAAQ,SAAS;CAChC,MAAM,QAAQ,KAAK,SAAS,IAAI;CAChC,MAAM,OAAO,KAAK,QAAQ;EAAC;EAAG;EAAG;CAAC;CAClC,MAAM,MAAM,KAAK,OAAO;EAAC;EAAG;EAAG;CAAC;CAChC,MAAM,QAAQ,KAAK,MAAM;EAAC;EAAG;EAAG;CAAC,IAAI;EAAC;EAAG;EAAG;CAAC;CAE7C,MAAM,QAAQ,QAAQ;CACtB,MAAM,MAAM,IAAI,aAAa,IAAI,KAAK;CACtC,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KACvB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK;EACxB,MAAM,QAAS,KAAK,IAAI,IAAK,MAAM,MAA4B;EAC/D,IAAI,IAAI,QAAQ,MAAM,QAAS,KAAK,MAAkB,IAAI;CAC9D;CAEJ,OAAO;AACX;AAEA,MAAa,gBAAgB;CAAC;CAAO;CAAO;AAAK;AACjD,MAAa,eAAe;CAAC;CAAO;CAAO;AAAK"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as ImageError, i as Stage } from "./pipeline-DACqGkpN.js";
|
|
2
|
-
import {
|
|
2
|
+
import { C as isRotated, a as cropBox, c as encodeImage, i as createCanvas, r as context2d, s as decodeImage } from "./image-DRBsbv7G.js";
|
|
3
3
|
import { i as resolveParams, t as BASE_STAGE_DEFAULTS } from "./params-DapwK9Ns.js";
|
|
4
4
|
import { t as createImage } from "./image-DoZDJkcR.js";
|
|
5
5
|
//#region src/stages/image-crop-boxes.ts
|
|
@@ -282,4 +282,4 @@ var ImageDrawBoxes = class extends Stage {
|
|
|
282
282
|
//#endregion
|
|
283
283
|
export { ImageCropBoxes as a, IMAGE_CROP_BOXES_DEFAULTS as i, ImageDrawBoxes as n, colorForGroup as r, IMAGE_DRAW_BOXES_DEFAULTS as t };
|
|
284
284
|
|
|
285
|
-
//# sourceMappingURL=image-draw-boxes-
|
|
285
|
+
//# sourceMappingURL=image-draw-boxes-DuR8eBeg.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-draw-boxes-De0QbFv9.js","names":[],"sources":["../src/stages/image-crop-boxes.ts","../src/stages/image-draw-boxes.ts"],"sourcesContent":["/**\n * Port of `scaledp/image/ImageCropBoxes.py`: crop each detected box out of the\n * page, emitting one row per crop.\n *\n * Rotated boxes are straightened rather than cropped to their envelope, which\n * is what makes the crops usable as recognizer input.\n */\n\nimport { ImageError } from '../core/errors.js'\nimport { cropBox, decodeImage, encodeImage } from '../core/image.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage, type StageContext } from '../core/pipeline.js'\nimport type { Box } from '../schemas/box.js'\nimport type { DetectorOutput } from '../schemas/detector-output.js'\nimport type { Document } from '../schemas/document.js'\nimport { createImage, type ImageFormat, type ScaleDpImage } from '../schemas/image.js'\n\nexport interface ImageCropBoxesParams extends BaseStageParams {\n /** [imageColumn, boxColumn]. */\n inputCols: string[]\n imageType: ImageFormat\n /** Grow each box by this many pixels before cropping. */\n padding: number\n /** Maximum crops per page; 0 means all of them. */\n limit: number\n /** Rotate portrait crops a quarter turn, so text reads horizontally. */\n autoRotate: boolean\n /** Emit the whole page when nothing was detected, instead of failing. */\n returnEmpty: boolean\n /** Column to write the source box alongside each crop. */\n boxCol: string\n}\n\nexport const IMAGE_CROP_BOXES_DEFAULTS: ImageCropBoxesParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n inputCols: ['image', 'boxes'],\n outputCol: 'cropped_image',\n keepInputData: true,\n imageType: 'png' as ImageFormat,\n padding: 0,\n limit: 0,\n autoRotate: true,\n returnEmpty: false,\n boxCol: 'box',\n})\n\nfunction boxesOf(source: unknown): Box[] {\n if (typeof source !== 'object' || source === null) return []\n return (source as DetectorOutput | Document).bboxes ?? []\n}\n\nexport class ImageCropBoxes extends Stage<ImageCropBoxesParams> {\n readonly name = 'ImageCropBoxes'\n\n constructor(options: Partial<ImageCropBoxesParams> = {}) {\n super(\n resolveParams(IMAGE_CROP_BOXES_DEFAULTS, options, {\n inputCols: (value) => {\n if (value.length !== 2) {\n throw new RangeError('inputCols must be [imageColumn, boxColumn]')\n }\n },\n })\n )\n }\n\n protected override async expand(_input: unknown, row: Row, ctx: StageContext): Promise<Row[]> {\n const { inputCols, outputCol, boxCol, limit, padding, autoRotate, imageType } = this.params\n const [imageCol, boxSourceCol] = inputCols as [string, string]\n const image = row[imageCol] as ScaleDpImage | undefined\n\n if (image?.exception) {\n throw new ImageError(`Upstream stage failed: ${image.exception}`, this.name)\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new ImageError('Expected an Image with decoded bytes', this.name)\n }\n\n let boxes = boxesOf(row[boxSourceCol])\n if (limit > 0) boxes = boxes.slice(0, limit)\n\n if (boxes.length === 0) {\n if (!this.params.returnEmpty) {\n throw new ImageError('No boxes to crop', this.name)\n }\n return [{ ...row, [outputCol]: image, [boxCol]: null }]\n }\n\n const bitmap = await decodeImage(image.data)\n try {\n const rows: Row[] = []\n for (const box of boxes) {\n ctx.signal?.throwIfAborted()\n let canvas = cropBox(bitmap, box, { padding })\n\n // A crop taller than it is wide is almost always vertical text;\n // a quarter turn makes it readable to a horizontal recognizer.\n if (autoRotate && canvas.height > canvas.width) canvas = rotateQuarterTurn(canvas)\n\n rows.push({\n ...row,\n [boxCol]: box,\n [outputCol]: createImage({\n path: image.path,\n resolution: image.resolution,\n data: await encodeImage(canvas, `image/${imageType}` as never),\n imageType,\n width: canvas.width,\n height: canvas.height,\n }),\n })\n }\n return rows\n } finally {\n bitmap.close()\n }\n }\n\n protected async apply(): Promise<never> {\n throw new ImageError('unreachable: expand handles every row', this.name)\n }\n\n protected onError(message: string, row: Row): ScaleDpImage {\n return createImage({\n path: String(row[this.params.pathCol] ?? 'memory'),\n exception: message,\n })\n }\n}\n\n/** Rotate 90 degrees counter-clockwise, swapping the canvas dimensions. */\nfunction rotateQuarterTurn(source: OffscreenCanvas): OffscreenCanvas {\n const rotated = new OffscreenCanvas(source.height, source.width)\n const ctx = rotated.getContext('2d')\n if (!ctx) throw new ImageError('Failed to acquire a 2D context', 'ImageCropBoxes')\n ctx.translate(0, rotated.height)\n ctx.rotate(-Math.PI / 2)\n ctx.drawImage(source, 0, 0)\n return rotated\n}\n","/**\n * Port of `scaledp/image/ImageDrawBoxes.py`: draw detection or NER boxes onto\n * the page.\n *\n * Takes several input columns -- an image plus one or more box sources -- and\n * writes a new image. A source is treated as NER output when it has `entities`,\n * and as boxes when it has `bboxes`.\n */\n\nimport { ImageError } from '../core/errors.js'\nimport { context2d, createCanvas, decodeImage, encodeImage } from '../core/image.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage } from '../core/pipeline.js'\nimport { type Box, isRotated } from '../schemas/box.js'\nimport type { DetectorOutput } from '../schemas/detector-output.js'\nimport type { Document } from '../schemas/document.js'\nimport type { Entity, NerOutput } from '../schemas/entity.js'\nimport { createImage, type ImageFormat, type ScaleDpImage } from '../schemas/image.js'\n\nexport interface ImageDrawBoxesParams extends BaseStageParams {\n /**\n * First entry is the image; the rest are box or entity sources. `inputCol`\n * is inherited and unused here -- multi-input stages address their columns\n * through this list, as they do in Python.\n */\n inputCols: string[]\n imageType: ImageFormat\n /** Fill boxes as well as outlining them. */\n filled: boolean\n /** Fixed colour for every box. Unset colours by group instead. */\n color: string | null\n lineWidth: number\n textSize: number\n /**\n * Box or entity fields to render as a label above each box, joined by ':'.\n * e.g. ['entity_group'] or ['text', 'score'].\n */\n displayDataList: string[]\n /** Grow each box by this many pixels before drawing. */\n padding: number\n /** Only draw these entity groups; empty draws all. */\n whiteList: string[]\n /** Never draw these entity groups. */\n blackList: string[]\n}\n\nexport const IMAGE_DRAW_BOXES_DEFAULTS: ImageDrawBoxesParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n inputCols: ['image', 'boxes'],\n outputCol: 'image_with_boxes',\n keepInputData: true,\n imageType: 'png' as ImageFormat,\n filled: false,\n color: null,\n lineWidth: 1,\n textSize: 12,\n displayDataList: [] as string[],\n padding: 0,\n whiteList: [] as string[],\n blackList: [] as string[],\n})\n\n/**\n * A stable colour per group name.\n *\n * Python picks a random colour per group, which changes on every run and makes\n * two renders of the same document impossible to compare. Hashing the name\n * instead keeps 'PERSON' the same colour everywhere, and the fixed saturation\n * and lightness keep every colour legible on a white page.\n */\nexport function colorForGroup(name: string): string {\n let hash = 0\n for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) | 0\n return `hsl(${Math.abs(hash) % 360}, 70%, 45%)`\n}\n\nfunction labelFor(source: Record<string, unknown>, fields: readonly string[]): string {\n const parts: string[] = []\n for (const field of fields) {\n const value = source[field]\n if (value === undefined || value === null) continue\n parts.push(typeof value === 'number' ? value.toFixed(2) : String(value))\n }\n return parts.join(':')\n}\n\ntype BoxSource = DetectorOutput | Document | NerOutput\n\nfunction isNerOutput(value: unknown): value is NerOutput {\n return typeof value === 'object' && value !== null && 'entities' in value\n}\n\nexport class ImageDrawBoxes extends Stage<ImageDrawBoxesParams> {\n readonly name = 'ImageDrawBoxes'\n\n constructor(options: Partial<ImageDrawBoxesParams> = {}) {\n super(\n resolveParams(IMAGE_DRAW_BOXES_DEFAULTS, options, {\n inputCols: (value) => {\n if (value.length < 2) {\n throw new RangeError('inputCols needs an image column and at least one box column')\n }\n },\n })\n )\n }\n\n /** The base class drives `apply` off inputCol; this stage reads several. */\n protected async apply(_input: unknown, row: Row): Promise<ScaleDpImage> {\n const [imageCol, ...boxCols] = this.params.inputCols\n const image = row[imageCol as string] as ScaleDpImage | undefined\n\n if (image?.exception) {\n throw new ImageError(`Upstream stage failed: ${image.exception}`, this.name)\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new ImageError('Expected an Image with decoded bytes', this.name)\n }\n\n const bitmap = await decodeImage(image.data)\n try {\n const canvas = createCanvas(bitmap.width, bitmap.height)\n const ctx = context2d(canvas)\n ctx.drawImage(bitmap, 0, 0)\n\n for (const column of boxCols) {\n const source = row[column] as BoxSource | undefined\n if (!source) continue\n if (isNerOutput(source)) this.drawEntities(ctx, source.entities)\n else this.drawBoxes(ctx, source.bboxes)\n }\n\n return createImage({\n path: image.path,\n resolution: image.resolution,\n data: await encodeImage(canvas, `image/${this.params.imageType}` as never),\n imageType: this.params.imageType,\n width: canvas.width,\n height: canvas.height,\n })\n } finally {\n bitmap.close()\n }\n }\n\n private drawBoxes(ctx: OffscreenCanvasRenderingContext2D, boxes: readonly Box[]): void {\n for (const box of boxes) {\n // Colour by box text so repeated labels share a colour, matching\n // Python's grouping.\n const color = this.params.color ?? colorForGroup(box.text || 'default')\n this.drawBox(ctx, box, color)\n const label = labelFor(box as unknown as Record<string, unknown>, this.params.displayDataList)\n if (label) this.drawLabel(ctx, box, label, color)\n }\n }\n\n private drawEntities(ctx: OffscreenCanvasRenderingContext2D, entities: readonly Entity[]): void {\n const { whiteList, blackList } = this.params\n for (const entity of entities) {\n if (whiteList.length > 0 && !whiteList.includes(entity.entity_group)) continue\n if (blackList.includes(entity.entity_group)) continue\n\n const color = this.params.color ?? colorForGroup(entity.entity_group)\n for (const box of entity.boxes) {\n this.drawBox(ctx, box, color)\n const label = labelFor(\n entity as unknown as Record<string, unknown>,\n this.params.displayDataList\n )\n if (label) this.drawLabel(ctx, box, label, color)\n }\n }\n }\n\n private drawBox(ctx: OffscreenCanvasRenderingContext2D, box: Box, color: string): void {\n const { padding, lineWidth, filled } = this.params\n ctx.strokeStyle = color\n ctx.lineWidth = lineWidth\n ctx.fillStyle = color\n\n if (!isRotated(box)) {\n ctx.beginPath()\n ctx.roundRect(\n box.x - padding,\n box.y - padding,\n box.width + padding * 2,\n box.height + padding * 2,\n 4\n )\n if (filled) ctx.fill()\n ctx.stroke()\n return\n }\n\n // Rotate the four corners about the box centre, matching how ScaleDP\n // interprets `angle` when it renders.\n const cx = box.x + box.width / 2\n const cy = box.y + box.height / 2\n const rad = (box.angle * Math.PI) / 180\n const cos = Math.cos(rad)\n const sin = Math.sin(rad)\n const half: [number, number][] = [\n [-box.width / 2 - padding, -box.height / 2 - padding],\n [box.width / 2 + padding, -box.height / 2 - padding],\n [box.width / 2 + padding, box.height / 2 + padding],\n [-box.width / 2 - padding, box.height / 2 + padding],\n ]\n\n ctx.beginPath()\n half.forEach(([px, py], index) => {\n const x = px * cos - py * sin + cx\n const y = px * sin + py * cos + cy\n if (index === 0) ctx.moveTo(x, y)\n else ctx.lineTo(x, y)\n })\n ctx.closePath()\n if (filled) ctx.fill()\n ctx.stroke()\n }\n\n /**\n * The label chip, anchored to the box's own top edge.\n *\n * Python places it at `box.x, box.y`, which is the corner of the *axis-\n * aligned envelope*. For an upright box that is the top-left corner and the\n * label sits where you expect; for a rotated one the envelope corner can be\n * a long way off the box -- a vertical line's label lands beside its middle,\n * detached from the box it names. Here the chip is drawn in the box's own\n * frame instead, so it rides the top edge at the box's angle whatever that\n * angle is. Upright boxes come out exactly where Python puts them.\n */\n private drawLabel(ctx: OffscreenCanvasRenderingContext2D, box: Box, label: string, color: string): void {\n const { textSize, padding } = this.params\n ctx.font = `${textSize}px sans-serif`\n const width = ctx.measureText(label).width\n const height = textSize * 1.2\n\n const chip = (x: number, y: number) => {\n ctx.fillStyle = color\n ctx.fillRect(x, y, width + 6, height)\n ctx.fillStyle = '#ffffff'\n ctx.textBaseline = 'top'\n ctx.fillText(label, x + 3, y + 1)\n }\n\n if (!isRotated(box)) {\n chip(box.x - padding, box.y - height - padding)\n return\n }\n\n // Top-left corner of the padded box, in the box's own frame.\n const left = -box.width / 2 - padding\n const top = -box.height / 2 - padding\n\n ctx.save()\n ctx.translate(box.x + box.width / 2, box.y + box.height / 2)\n ctx.rotate((box.angle * Math.PI) / 180)\n chip(left, top - height)\n ctx.restore()\n }\n\n protected onError(message: string, row: Row): ScaleDpImage {\n return createImage({\n path: String(row[this.params.pathCol] ?? 'memory'),\n exception: message,\n })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"image-draw-boxes-DuR8eBeg.js","names":[],"sources":["../src/stages/image-crop-boxes.ts","../src/stages/image-draw-boxes.ts"],"sourcesContent":["/**\n * Port of `scaledp/image/ImageCropBoxes.py`: crop each detected box out of the\n * page, emitting one row per crop.\n *\n * Rotated boxes are straightened rather than cropped to their envelope, which\n * is what makes the crops usable as recognizer input.\n */\n\nimport { ImageError } from '../core/errors.js'\nimport { cropBox, decodeImage, encodeImage } from '../core/image.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage, type StageContext } from '../core/pipeline.js'\nimport type { Box } from '../schemas/box.js'\nimport type { DetectorOutput } from '../schemas/detector-output.js'\nimport type { Document } from '../schemas/document.js'\nimport { createImage, type ImageFormat, type ScaleDpImage } from '../schemas/image.js'\n\nexport interface ImageCropBoxesParams extends BaseStageParams {\n /** [imageColumn, boxColumn]. */\n inputCols: string[]\n imageType: ImageFormat\n /** Grow each box by this many pixels before cropping. */\n padding: number\n /** Maximum crops per page; 0 means all of them. */\n limit: number\n /** Rotate portrait crops a quarter turn, so text reads horizontally. */\n autoRotate: boolean\n /** Emit the whole page when nothing was detected, instead of failing. */\n returnEmpty: boolean\n /** Column to write the source box alongside each crop. */\n boxCol: string\n}\n\nexport const IMAGE_CROP_BOXES_DEFAULTS: ImageCropBoxesParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n inputCols: ['image', 'boxes'],\n outputCol: 'cropped_image',\n keepInputData: true,\n imageType: 'png' as ImageFormat,\n padding: 0,\n limit: 0,\n autoRotate: true,\n returnEmpty: false,\n boxCol: 'box',\n})\n\nfunction boxesOf(source: unknown): Box[] {\n if (typeof source !== 'object' || source === null) return []\n return (source as DetectorOutput | Document).bboxes ?? []\n}\n\nexport class ImageCropBoxes extends Stage<ImageCropBoxesParams> {\n readonly name = 'ImageCropBoxes'\n\n constructor(options: Partial<ImageCropBoxesParams> = {}) {\n super(\n resolveParams(IMAGE_CROP_BOXES_DEFAULTS, options, {\n inputCols: (value) => {\n if (value.length !== 2) {\n throw new RangeError('inputCols must be [imageColumn, boxColumn]')\n }\n },\n })\n )\n }\n\n protected override async expand(_input: unknown, row: Row, ctx: StageContext): Promise<Row[]> {\n const { inputCols, outputCol, boxCol, limit, padding, autoRotate, imageType } = this.params\n const [imageCol, boxSourceCol] = inputCols as [string, string]\n const image = row[imageCol] as ScaleDpImage | undefined\n\n if (image?.exception) {\n throw new ImageError(`Upstream stage failed: ${image.exception}`, this.name)\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new ImageError('Expected an Image with decoded bytes', this.name)\n }\n\n let boxes = boxesOf(row[boxSourceCol])\n if (limit > 0) boxes = boxes.slice(0, limit)\n\n if (boxes.length === 0) {\n if (!this.params.returnEmpty) {\n throw new ImageError('No boxes to crop', this.name)\n }\n return [{ ...row, [outputCol]: image, [boxCol]: null }]\n }\n\n const bitmap = await decodeImage(image.data)\n try {\n const rows: Row[] = []\n for (const box of boxes) {\n ctx.signal?.throwIfAborted()\n let canvas = cropBox(bitmap, box, { padding })\n\n // A crop taller than it is wide is almost always vertical text;\n // a quarter turn makes it readable to a horizontal recognizer.\n if (autoRotate && canvas.height > canvas.width) canvas = rotateQuarterTurn(canvas)\n\n rows.push({\n ...row,\n [boxCol]: box,\n [outputCol]: createImage({\n path: image.path,\n resolution: image.resolution,\n data: await encodeImage(canvas, `image/${imageType}` as never),\n imageType,\n width: canvas.width,\n height: canvas.height,\n }),\n })\n }\n return rows\n } finally {\n bitmap.close()\n }\n }\n\n protected async apply(): Promise<never> {\n throw new ImageError('unreachable: expand handles every row', this.name)\n }\n\n protected onError(message: string, row: Row): ScaleDpImage {\n return createImage({\n path: String(row[this.params.pathCol] ?? 'memory'),\n exception: message,\n })\n }\n}\n\n/** Rotate 90 degrees counter-clockwise, swapping the canvas dimensions. */\nfunction rotateQuarterTurn(source: OffscreenCanvas): OffscreenCanvas {\n const rotated = new OffscreenCanvas(source.height, source.width)\n const ctx = rotated.getContext('2d')\n if (!ctx) throw new ImageError('Failed to acquire a 2D context', 'ImageCropBoxes')\n ctx.translate(0, rotated.height)\n ctx.rotate(-Math.PI / 2)\n ctx.drawImage(source, 0, 0)\n return rotated\n}\n","/**\n * Port of `scaledp/image/ImageDrawBoxes.py`: draw detection or NER boxes onto\n * the page.\n *\n * Takes several input columns -- an image plus one or more box sources -- and\n * writes a new image. A source is treated as NER output when it has `entities`,\n * and as boxes when it has `bboxes`.\n */\n\nimport { ImageError } from '../core/errors.js'\nimport { context2d, createCanvas, decodeImage, encodeImage } from '../core/image.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage } from '../core/pipeline.js'\nimport { type Box, isRotated } from '../schemas/box.js'\nimport type { DetectorOutput } from '../schemas/detector-output.js'\nimport type { Document } from '../schemas/document.js'\nimport type { Entity, NerOutput } from '../schemas/entity.js'\nimport { createImage, type ImageFormat, type ScaleDpImage } from '../schemas/image.js'\n\nexport interface ImageDrawBoxesParams extends BaseStageParams {\n /**\n * First entry is the image; the rest are box or entity sources. `inputCol`\n * is inherited and unused here -- multi-input stages address their columns\n * through this list, as they do in Python.\n */\n inputCols: string[]\n imageType: ImageFormat\n /** Fill boxes as well as outlining them. */\n filled: boolean\n /** Fixed colour for every box. Unset colours by group instead. */\n color: string | null\n lineWidth: number\n textSize: number\n /**\n * Box or entity fields to render as a label above each box, joined by ':'.\n * e.g. ['entity_group'] or ['text', 'score'].\n */\n displayDataList: string[]\n /** Grow each box by this many pixels before drawing. */\n padding: number\n /** Only draw these entity groups; empty draws all. */\n whiteList: string[]\n /** Never draw these entity groups. */\n blackList: string[]\n}\n\nexport const IMAGE_DRAW_BOXES_DEFAULTS: ImageDrawBoxesParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n inputCols: ['image', 'boxes'],\n outputCol: 'image_with_boxes',\n keepInputData: true,\n imageType: 'png' as ImageFormat,\n filled: false,\n color: null,\n lineWidth: 1,\n textSize: 12,\n displayDataList: [] as string[],\n padding: 0,\n whiteList: [] as string[],\n blackList: [] as string[],\n})\n\n/**\n * A stable colour per group name.\n *\n * Python picks a random colour per group, which changes on every run and makes\n * two renders of the same document impossible to compare. Hashing the name\n * instead keeps 'PERSON' the same colour everywhere, and the fixed saturation\n * and lightness keep every colour legible on a white page.\n */\nexport function colorForGroup(name: string): string {\n let hash = 0\n for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) | 0\n return `hsl(${Math.abs(hash) % 360}, 70%, 45%)`\n}\n\nfunction labelFor(source: Record<string, unknown>, fields: readonly string[]): string {\n const parts: string[] = []\n for (const field of fields) {\n const value = source[field]\n if (value === undefined || value === null) continue\n parts.push(typeof value === 'number' ? value.toFixed(2) : String(value))\n }\n return parts.join(':')\n}\n\ntype BoxSource = DetectorOutput | Document | NerOutput\n\nfunction isNerOutput(value: unknown): value is NerOutput {\n return typeof value === 'object' && value !== null && 'entities' in value\n}\n\nexport class ImageDrawBoxes extends Stage<ImageDrawBoxesParams> {\n readonly name = 'ImageDrawBoxes'\n\n constructor(options: Partial<ImageDrawBoxesParams> = {}) {\n super(\n resolveParams(IMAGE_DRAW_BOXES_DEFAULTS, options, {\n inputCols: (value) => {\n if (value.length < 2) {\n throw new RangeError('inputCols needs an image column and at least one box column')\n }\n },\n })\n )\n }\n\n /** The base class drives `apply` off inputCol; this stage reads several. */\n protected async apply(_input: unknown, row: Row): Promise<ScaleDpImage> {\n const [imageCol, ...boxCols] = this.params.inputCols\n const image = row[imageCol as string] as ScaleDpImage | undefined\n\n if (image?.exception) {\n throw new ImageError(`Upstream stage failed: ${image.exception}`, this.name)\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new ImageError('Expected an Image with decoded bytes', this.name)\n }\n\n const bitmap = await decodeImage(image.data)\n try {\n const canvas = createCanvas(bitmap.width, bitmap.height)\n const ctx = context2d(canvas)\n ctx.drawImage(bitmap, 0, 0)\n\n for (const column of boxCols) {\n const source = row[column] as BoxSource | undefined\n if (!source) continue\n if (isNerOutput(source)) this.drawEntities(ctx, source.entities)\n else this.drawBoxes(ctx, source.bboxes)\n }\n\n return createImage({\n path: image.path,\n resolution: image.resolution,\n data: await encodeImage(canvas, `image/${this.params.imageType}` as never),\n imageType: this.params.imageType,\n width: canvas.width,\n height: canvas.height,\n })\n } finally {\n bitmap.close()\n }\n }\n\n private drawBoxes(ctx: OffscreenCanvasRenderingContext2D, boxes: readonly Box[]): void {\n for (const box of boxes) {\n // Colour by box text so repeated labels share a colour, matching\n // Python's grouping.\n const color = this.params.color ?? colorForGroup(box.text || 'default')\n this.drawBox(ctx, box, color)\n const label = labelFor(box as unknown as Record<string, unknown>, this.params.displayDataList)\n if (label) this.drawLabel(ctx, box, label, color)\n }\n }\n\n private drawEntities(ctx: OffscreenCanvasRenderingContext2D, entities: readonly Entity[]): void {\n const { whiteList, blackList } = this.params\n for (const entity of entities) {\n if (whiteList.length > 0 && !whiteList.includes(entity.entity_group)) continue\n if (blackList.includes(entity.entity_group)) continue\n\n const color = this.params.color ?? colorForGroup(entity.entity_group)\n for (const box of entity.boxes) {\n this.drawBox(ctx, box, color)\n const label = labelFor(\n entity as unknown as Record<string, unknown>,\n this.params.displayDataList\n )\n if (label) this.drawLabel(ctx, box, label, color)\n }\n }\n }\n\n private drawBox(ctx: OffscreenCanvasRenderingContext2D, box: Box, color: string): void {\n const { padding, lineWidth, filled } = this.params\n ctx.strokeStyle = color\n ctx.lineWidth = lineWidth\n ctx.fillStyle = color\n\n if (!isRotated(box)) {\n ctx.beginPath()\n ctx.roundRect(\n box.x - padding,\n box.y - padding,\n box.width + padding * 2,\n box.height + padding * 2,\n 4\n )\n if (filled) ctx.fill()\n ctx.stroke()\n return\n }\n\n // Rotate the four corners about the box centre, matching how ScaleDP\n // interprets `angle` when it renders.\n const cx = box.x + box.width / 2\n const cy = box.y + box.height / 2\n const rad = (box.angle * Math.PI) / 180\n const cos = Math.cos(rad)\n const sin = Math.sin(rad)\n const half: [number, number][] = [\n [-box.width / 2 - padding, -box.height / 2 - padding],\n [box.width / 2 + padding, -box.height / 2 - padding],\n [box.width / 2 + padding, box.height / 2 + padding],\n [-box.width / 2 - padding, box.height / 2 + padding],\n ]\n\n ctx.beginPath()\n half.forEach(([px, py], index) => {\n const x = px * cos - py * sin + cx\n const y = px * sin + py * cos + cy\n if (index === 0) ctx.moveTo(x, y)\n else ctx.lineTo(x, y)\n })\n ctx.closePath()\n if (filled) ctx.fill()\n ctx.stroke()\n }\n\n /**\n * The label chip, anchored to the box's own top edge.\n *\n * Python places it at `box.x, box.y`, which is the corner of the *axis-\n * aligned envelope*. For an upright box that is the top-left corner and the\n * label sits where you expect; for a rotated one the envelope corner can be\n * a long way off the box -- a vertical line's label lands beside its middle,\n * detached from the box it names. Here the chip is drawn in the box's own\n * frame instead, so it rides the top edge at the box's angle whatever that\n * angle is. Upright boxes come out exactly where Python puts them.\n */\n private drawLabel(ctx: OffscreenCanvasRenderingContext2D, box: Box, label: string, color: string): void {\n const { textSize, padding } = this.params\n ctx.font = `${textSize}px sans-serif`\n const width = ctx.measureText(label).width\n const height = textSize * 1.2\n\n const chip = (x: number, y: number) => {\n ctx.fillStyle = color\n ctx.fillRect(x, y, width + 6, height)\n ctx.fillStyle = '#ffffff'\n ctx.textBaseline = 'top'\n ctx.fillText(label, x + 3, y + 1)\n }\n\n if (!isRotated(box)) {\n chip(box.x - padding, box.y - height - padding)\n return\n }\n\n // Top-left corner of the padded box, in the box's own frame.\n const left = -box.width / 2 - padding\n const top = -box.height / 2 - padding\n\n ctx.save()\n ctx.translate(box.x + box.width / 2, box.y + box.height / 2)\n ctx.rotate((box.angle * Math.PI) / 180)\n chip(left, top - height)\n ctx.restore()\n }\n\n protected onError(message: string, row: Row): ScaleDpImage {\n return createImage({\n path: String(row[this.params.pathCol] ?? 'memory'),\n exception: message,\n })\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -103,6 +103,13 @@ declare function cropBox(source: ImageBitmap | OffscreenCanvas, box: Box, opts?:
|
|
|
103
103
|
scaleFactor?: number;
|
|
104
104
|
padding?: number;
|
|
105
105
|
}): OffscreenCanvas;
|
|
106
|
+
/**
|
|
107
|
+
* Turn a crop 180 degrees, so an inverted line reads the right way up.
|
|
108
|
+
*
|
|
109
|
+
* Maps (x, y) to (w - x, h - y) -- a recognizer mapping word boxes back out of
|
|
110
|
+
* an inverted crop has to undo exactly that.
|
|
111
|
+
*/
|
|
112
|
+
declare function rotate180(source: ImageBitmap | OffscreenCanvas): OffscreenCanvas;
|
|
106
113
|
/**
|
|
107
114
|
* Build an NCHW float32 tensor from an image.
|
|
108
115
|
*
|
|
@@ -265,5 +272,5 @@ declare class ImageDrawBoxes extends Stage<ImageDrawBoxesParams> {
|
|
|
265
272
|
protected onError(message: string, row: Row): ScaleDpImage;
|
|
266
273
|
}
|
|
267
274
|
//#endregion
|
|
268
|
-
export { BASE_STAGE_DEFAULTS, BBox, type BaseStageParams, Box, BoxShape, ConfigError, CropGeometry, DATA_TO_IMAGE_DEFAULTS, DataToImage, type DataToImageParams, DetectionError, DetectorOutput, Document, EXECUTION_TIME_COL, Entity, type ExecutionTime, IMAGENET_MEAN, IMAGENET_STD, IMAGE_CROP_BOXES_DEFAULTS, IMAGE_DRAW_BOXES_DEFAULTS, ImageCropBoxes, type ImageCropBoxesParams, ImageDrawBoxes, type ImageDrawBoxesParams, ImageError, ImageFormat, LetterboxResult, type ModelFile, type ModelFiles, type ModelProgress, type ModelSpec, NerError, NerOutput, OcrError, Pipeline, type PipelineInput, type PipelineOptions, Point, ROTATION_EPSILON_DEGREES, ROW_TIME_COL, RotatedRect, type Row, type RowTime, type ScaleDpConfig, ScaleDpError, ScaleDpImage, Size, Stage, type StageContext, type StageDescriptor, type Validator, assertInRange, assertPositiveInt, bbox, boxFromBBox, boxFromPolygon, boxIou, boxPoints, boxesToFormattedText, boxesToText, cacheKey, cluster, colorForGroup, configure, context2d, convexHull, createBox, createCanvas, createDetectorOutput, createDocument, createImage, createNerOutput, cropBox, cropGeometry, decodeImage, defaultNumThreads, encodeImage, ensureModelFiles, evict, fileUrl, formatException, getCharacterWidth, getConfig, getSize, groupBoxesIntoLines, imageDataToCanvas, isCached, isOnSameLine, isRotated, letterbox, linesToFormattedText, mergeBoxes, mergeDocuments, mergeOverlappingBoxes, minAreaRect, polygonArea, polygonPerimeter, probeImageSize, resetConfig, resize, resolveNumThreads, resolveParams, scaleBox, shape, toBytes, toImageData, toNchwFloat32, toRows };
|
|
275
|
+
export { BASE_STAGE_DEFAULTS, BBox, type BaseStageParams, Box, BoxShape, ConfigError, CropGeometry, DATA_TO_IMAGE_DEFAULTS, DataToImage, type DataToImageParams, DetectionError, DetectorOutput, Document, EXECUTION_TIME_COL, Entity, type ExecutionTime, IMAGENET_MEAN, IMAGENET_STD, IMAGE_CROP_BOXES_DEFAULTS, IMAGE_DRAW_BOXES_DEFAULTS, ImageCropBoxes, type ImageCropBoxesParams, ImageDrawBoxes, type ImageDrawBoxesParams, ImageError, ImageFormat, LetterboxResult, type ModelFile, type ModelFiles, type ModelProgress, type ModelSpec, NerError, NerOutput, OcrError, Pipeline, type PipelineInput, type PipelineOptions, Point, ROTATION_EPSILON_DEGREES, ROW_TIME_COL, RotatedRect, type Row, type RowTime, type ScaleDpConfig, ScaleDpError, ScaleDpImage, Size, Stage, type StageContext, type StageDescriptor, type Validator, assertInRange, assertPositiveInt, bbox, boxFromBBox, boxFromPolygon, boxIou, boxPoints, boxesToFormattedText, boxesToText, cacheKey, cluster, colorForGroup, configure, context2d, convexHull, createBox, createCanvas, createDetectorOutput, createDocument, createImage, createNerOutput, cropBox, cropGeometry, decodeImage, defaultNumThreads, encodeImage, ensureModelFiles, evict, fileUrl, formatException, getCharacterWidth, getConfig, getSize, groupBoxesIntoLines, imageDataToCanvas, isCached, isOnSameLine, isRotated, letterbox, linesToFormattedText, mergeBoxes, mergeDocuments, mergeOverlappingBoxes, minAreaRect, polygonArea, polygonPerimeter, probeImageSize, resetConfig, resize, resolveNumThreads, resolveParams, rotate180, scaleBox, shape, toBytes, toImageData, toNchwFloat32, toRows };
|
|
269
276
|
//# sourceMappingURL=index.d.ts.map
|