@stabrise/scaledp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +218 -0
- package/dist/box-DAfzwfhA.d.ts +119 -0
- package/dist/config-g6IrKlDC.d.ts +80 -0
- package/dist/data-to-image-DoZ4jQ3R.js +54 -0
- package/dist/data-to-image-DoZ4jQ3R.js.map +1 -0
- package/dist/detect/index.d.ts +71 -0
- package/dist/detect/index.js +2 -0
- package/dist/detect-q8AI_Jdj.js +274 -0
- package/dist/detect-q8AI_Jdj.js.map +1 -0
- package/dist/detector-output-C0Qt-jEq.d.ts +13 -0
- package/dist/detector-output-lyF1Mqb8.js +13 -0
- package/dist/detector-output-lyF1Mqb8.js.map +1 -0
- package/dist/display/index.d.ts +66 -0
- package/dist/display/index.js +237 -0
- package/dist/display/index.js.map +1 -0
- package/dist/document-B8I61TiY.d.ts +16 -0
- package/dist/entity-CedtRhU1.d.ts +22 -0
- package/dist/entity-D6Hxaugj.js +13 -0
- package/dist/entity-D6Hxaugj.js.map +1 -0
- package/dist/image-CAH2rLv9.js +511 -0
- package/dist/image-CAH2rLv9.js.map +1 -0
- package/dist/image-Dc5TSg46.d.ts +18 -0
- package/dist/image-DoZDJkcR.js +37 -0
- package/dist/image-DoZDJkcR.js.map +1 -0
- package/dist/image-draw-boxes-De0QbFv9.js +285 -0
- package/dist/image-draw-boxes-De0QbFv9.js.map +1 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +11 -0
- package/dist/model-cache-BEaqqRZ9.js +182 -0
- package/dist/model-cache-BEaqqRZ9.js.map +1 -0
- package/dist/model-cache-BhFYpfZz.d.ts +36 -0
- package/dist/ner/index.d.ts +293 -0
- package/dist/ner/index.js +2 -0
- package/dist/ner-SsZLZ6ed.js +1028 -0
- package/dist/ner-SsZLZ6ed.js.map +1 -0
- package/dist/ocr/index.d.ts +440 -0
- package/dist/ocr/index.js +3 -0
- package/dist/ocr-OHX2WM3e.js +1294 -0
- package/dist/ocr-OHX2WM3e.js.map +1 -0
- package/dist/ort-CXDoPrtw.js +73 -0
- package/dist/ort-CXDoPrtw.js.map +1 -0
- package/dist/params-DapwK9Ns.js +37 -0
- package/dist/params-DapwK9Ns.js.map +1 -0
- package/dist/pdf/index.d.ts +123 -0
- package/dist/pdf/index.js +2 -0
- package/dist/pdf-BQl0dneD.js +417 -0
- package/dist/pdf-BQl0dneD.js.map +1 -0
- package/dist/pipeline-DACqGkpN.js +240 -0
- package/dist/pipeline-DACqGkpN.js.map +1 -0
- package/dist/pipeline-DeLO-OCE.d.ts +139 -0
- package/dist/registry/index.d.ts +169 -0
- package/dist/registry/index.js +1061 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/text-ahMLpxN9.js +109 -0
- package/dist/text-ahMLpxN9.js.map +1 -0
- package/dist/worker/index.d.ts +105 -0
- package/dist/worker/index.js +180 -0
- package/dist/worker/index.js.map +1 -0
- package/package.json +135 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/display/index.ts"],"sourcesContent":["/**\n * Result rendering, mirroring ScaleDP's notebook display helpers.\n *\n * Python monkey-patches `show_image`, `show_text`, `show_ner` and\n * `visualize_ner` onto the Spark DataFrame and renders Jinja templates into\n * IPython. The browser equivalent is to build DOM elements, so these return an\n * `HTMLElement` the caller places wherever it likes -- and `renderInto` is the\n * one-liner for the common case.\n *\n * Everything is built with text nodes rather than interpolated markup:\n * recognized text and entity words come straight from the document, and a page\n * containing `<` would otherwise corrupt the DOM.\n */\n\nimport type { Box } 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 type { ScaleDpImage } from '../schemas/image.js'\n\n/**\n * A stable colour per entity group.\n *\n * Python picks a random colour each run, so two renders of the same document\n * never match. Hashing the group name keeps 'PERSON' one colour everywhere.\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 element<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n props: { text?: string; className?: string; style?: Partial<CSSStyleDeclaration> } = {}\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag)\n if (props.text !== undefined) node.textContent = props.text\n if (props.className) node.className = props.className\n if (props.style) Object.assign(node.style, props.style)\n return node\n}\n\n/** Replace a container's contents with `node`. Accepts a selector or element. */\nexport function renderInto(target: string | HTMLElement, node: Node): HTMLElement {\n const host = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target\n if (!host) throw new Error(`No element matches ${String(target)}`)\n host.replaceChildren(node)\n return host\n}\n\nexport interface ShowImageOptions {\n /** CSS width, e.g. '600px' or '100%'. */\n width?: string\n alt?: string\n}\n\n/**\n * An `<img>` for a ScaleDP image. Mirrors `show_image`.\n *\n * The object URL is revoked once the image has decoded -- holding one per page\n * leaks the whole blob for the lifetime of the document.\n */\nexport function showImage(image: ScaleDpImage, options: ShowImageOptions = {}): HTMLElement {\n if (image.exception) return errorBlock(image.exception)\n\n const bytes = new Uint8Array(image.data.byteLength)\n bytes.set(image.data)\n const url = URL.createObjectURL(new Blob([bytes.buffer], { type: `image/${image.imageType}` }))\n\n const img = element('img', { style: { maxWidth: options.width ?? '100%', height: 'auto' } })\n img.alt = options.alt ?? image.path\n img.addEventListener('load', () => URL.revokeObjectURL(url), { once: true })\n img.addEventListener('error', () => URL.revokeObjectURL(url), { once: true })\n img.src = url\n return img\n}\n\nexport interface ShowTextOptions {\n /**\n * Preserve the document's own layout. Correct when the OCR stage ran with\n * `keepFormatting`, which encodes the layout in spaces and blank lines.\n */\n preserveLayout?: boolean\n maxHeight?: string\n}\n\n/** A `<pre>` of the recognized text. Mirrors `show_text`. */\nexport function showText(document_: Document, options: ShowTextOptions = {}): HTMLElement {\n if (document_.exception) return errorBlock(document_.exception)\n\n return element('pre', {\n text: document_.text,\n className: 'scaledp-text',\n style: {\n fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',\n fontSize: '12px',\n whiteSpace: options.preserveLayout === false ? 'pre-wrap' : 'pre',\n overflowX: 'auto',\n maxHeight: options.maxHeight ?? '30rem',\n margin: '0',\n },\n })\n}\n\n/** Pretty-printed JSON. Mirrors `show_json`. */\nexport function showJson(value: unknown, indent = 2): HTMLElement {\n const text = typeof value === 'string' ? value : JSON.stringify(value, null, indent)\n return element('pre', {\n text,\n className: 'scaledp-json',\n style: { fontFamily: 'ui-monospace, monospace', fontSize: '12px', overflowX: 'auto' },\n })\n}\n\nexport interface ShowNerOptions {\n /** Maximum rows; 0 shows all. Python defaults to 20. */\n limit?: number\n /** Only these groups. */\n whiteList?: readonly string[]\n}\n\n/** A table of entities. Mirrors `show_ner`. */\nexport function showNer(ner: NerOutput, options: ShowNerOptions = {}): HTMLElement {\n if (ner.exception) return errorBlock(ner.exception)\n\n const allowed = new Set(options.whiteList ?? [])\n let entities = allowed.size > 0 ? ner.entities.filter((e) => allowed.has(e.entity_group)) : ner.entities\n\n const limit = options.limit ?? 20\n const total = entities.length\n if (limit > 0) entities = entities.slice(0, limit)\n\n if (total === 0) return element('p', { text: 'No entities found.' })\n\n const table = element('table', { className: 'scaledp-ner' })\n table.style.borderCollapse = 'collapse'\n\n const header = table.insertRow()\n for (const label of ['Type', 'Text', 'Score', 'Start', 'End', 'Boxes']) {\n const th = document.createElement('th')\n th.textContent = label\n th.style.cssText = 'border:1px solid #ddd;padding:4px 8px;text-align:left'\n header.append(th)\n }\n\n for (const entity of entities) {\n const tr = table.insertRow()\n const swatch = element('span', {\n style: {\n display: 'inline-block',\n width: '8px',\n height: '8px',\n borderRadius: '50%',\n marginRight: '6px',\n background: colorForGroup(entity.entity_group),\n },\n })\n const cells: (string | Node)[] = [\n entity.entity_group,\n entity.word,\n entity.score.toFixed(3),\n String(entity.start),\n String(entity.end),\n String(entity.boxes.length),\n ]\n cells.forEach((value, index) => {\n const cell = tr.insertCell()\n cell.style.cssText = 'border:1px solid #ddd;padding:4px 8px'\n if (index === 0) cell.append(swatch)\n cell.append(typeof value === 'string' ? document.createTextNode(value) : value)\n })\n }\n\n const wrapper = element('div')\n wrapper.append(table)\n if (limit > 0 && total > limit) {\n wrapper.append(element('p', { text: `Showing ${limit} of ${total} entities.` }))\n }\n return wrapper\n}\n\nexport interface VisualizeNerOptions {\n /** Only highlight these groups. */\n labelsList?: readonly string[]\n /** Render the group name beside each highlight. */\n showLabels?: boolean\n}\n\n/**\n * The document text with entities highlighted inline. Mirrors `visualize_ner`.\n *\n * Splices spans by character offset, which is exactly what `Entity.start`/`end`\n * index. Overlapping entities are dropped rather than nested: the highest-\n * scoring one wins, because two spans cannot occupy the same characters in a\n * flat text run.\n */\nexport function visualizeNer(\n document_: Document,\n ner: NerOutput,\n options: VisualizeNerOptions = {}\n): HTMLElement {\n if (document_.exception) return errorBlock(document_.exception)\n if (ner.exception) return errorBlock(ner.exception)\n\n const allowed = new Set(options.labelsList ?? [])\n const entities = (\n allowed.size > 0 ? ner.entities.filter((e) => allowed.has(e.entity_group)) : ner.entities\n )\n .filter((e) => e.start >= 0 && e.end > e.start && e.end <= document_.text.length)\n .sort((a, b) => a.start - b.start || b.score - a.score)\n\n const container = element('div', {\n className: 'scaledp-ner-text',\n style: {\n fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',\n fontSize: '12px',\n whiteSpace: 'pre-wrap',\n lineHeight: '1.9',\n },\n })\n\n let cursor = 0\n for (const entity of entities) {\n if (entity.start < cursor) continue // overlaps an already-rendered span\n\n if (entity.start > cursor) {\n container.append(document.createTextNode(document_.text.slice(cursor, entity.start)))\n }\n\n const color = colorForGroup(entity.entity_group)\n const mark = element('span', {\n text: document_.text.slice(entity.start, entity.end),\n style: {\n background: color,\n color: '#fff',\n borderRadius: '3px',\n padding: '1px 3px',\n },\n })\n mark.title = `${entity.entity_group} (${entity.score.toFixed(3)})`\n container.append(mark)\n\n if (options.showLabels) {\n container.append(\n element('sup', {\n text: entity.entity_group,\n style: { color, fontSize: '9px', marginLeft: '2px' },\n })\n )\n }\n cursor = entity.end\n }\n container.append(document.createTextNode(document_.text.slice(cursor)))\n return container\n}\n\n/** A summary table of detected boxes. */\nexport function showBoxes(output: DetectorOutput | Document, limit = 20): HTMLElement {\n if (output.exception) return errorBlock(output.exception)\n\n const boxes: Box[] = output.bboxes\n const table = element('table')\n table.style.borderCollapse = 'collapse'\n\n const header = table.insertRow()\n for (const label of ['Text', 'Score', 'x', 'y', 'w', 'h', 'angle']) {\n const th = document.createElement('th')\n th.textContent = label\n th.style.cssText = 'border:1px solid #ddd;padding:4px 8px;text-align:left'\n header.append(th)\n }\n\n for (const box of limit > 0 ? boxes.slice(0, limit) : boxes) {\n const tr = table.insertRow()\n for (const value of [\n box.text,\n box.score.toFixed(3),\n String(box.x),\n String(box.y),\n String(box.width),\n String(box.height),\n box.angle.toFixed(1),\n ]) {\n const cell = tr.insertCell()\n cell.style.cssText = 'border:1px solid #ddd;padding:4px 8px'\n cell.textContent = value\n }\n }\n\n const wrapper = element('div')\n wrapper.append(table)\n if (limit > 0 && boxes.length > limit) {\n wrapper.append(element('p', { text: `Showing ${limit} of ${boxes.length} boxes.` }))\n }\n return wrapper\n}\n\nfunction errorBlock(message: string): HTMLElement {\n return element('pre', {\n text: message,\n style: {\n color: '#b00020',\n whiteSpace: 'pre-wrap',\n fontFamily: 'ui-monospace, monospace',\n fontSize: '12px',\n },\n })\n}\n\nexport type { DetectorOutput, Document, Entity, NerOutput, ScaleDpImage }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { n as Box } from "./box-DAfzwfhA.js";
|
|
2
|
+
//#region src/schemas/document.d.ts
|
|
3
|
+
interface Document {
|
|
4
|
+
path: string;
|
|
5
|
+
text: string;
|
|
6
|
+
/** Producer of this document: 'text' | 'ocr' | 'pdf' | an engine name. */
|
|
7
|
+
type: string;
|
|
8
|
+
bboxes: Box[];
|
|
9
|
+
exception: string;
|
|
10
|
+
}
|
|
11
|
+
declare function createDocument(init?: Partial<Document>): Document;
|
|
12
|
+
/** Python's `Document.merge` puts the *argument* first and joins with a newline. */
|
|
13
|
+
declare function mergeDocuments(self: Document, other: Document): Document;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { createDocument as n, mergeDocuments as r, Document as t };
|
|
16
|
+
//# sourceMappingURL=document-B8I61TiY.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { n as Box } from "./box-DAfzwfhA.js";
|
|
2
|
+
//#region src/schemas/entity.d.ts
|
|
3
|
+
interface Entity {
|
|
4
|
+
entity_group: string;
|
|
5
|
+
score: number;
|
|
6
|
+
word: string;
|
|
7
|
+
/** Character offset into the source document text. */
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
/** Boxes the entity's characters fall inside; empty when there is no OCR layer. */
|
|
11
|
+
boxes: Box[];
|
|
12
|
+
}
|
|
13
|
+
interface NerOutput {
|
|
14
|
+
path: string;
|
|
15
|
+
entities: Entity[];
|
|
16
|
+
exception: string;
|
|
17
|
+
json: string;
|
|
18
|
+
}
|
|
19
|
+
declare function createNerOutput(init?: Partial<NerOutput>): NerOutput;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { NerOutput as n, createNerOutput as r, Entity as t };
|
|
22
|
+
//# sourceMappingURL=entity-CedtRhU1.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/schemas/entity.ts
|
|
2
|
+
function createNerOutput(init = {}) {
|
|
3
|
+
return {
|
|
4
|
+
path: init.path ?? "memory",
|
|
5
|
+
entities: init.entities ?? [],
|
|
6
|
+
exception: init.exception ?? "",
|
|
7
|
+
json: init.json ?? ""
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { createNerOutput as t };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=entity-D6Hxaugj.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-D6Hxaugj.js","names":[],"sources":["../src/schemas/entity.ts"],"sourcesContent":["/** Port of `scaledp/schemas/Entity.py` and `NerOutput.py`. */\n\nimport type { Box } from './box.js'\n\nexport interface Entity {\n entity_group: string\n score: number\n word: string\n /** Character offset into the source document text. */\n start: number\n end: number\n /** Boxes the entity's characters fall inside; empty when there is no OCR layer. */\n boxes: Box[]\n}\n\nexport interface NerOutput {\n path: string\n entities: Entity[]\n exception: string\n json: string\n}\n\nexport function createNerOutput(init: Partial<NerOutput> = {}): NerOutput {\n return {\n path: init.path ?? 'memory',\n entities: init.entities ?? [],\n exception: init.exception ?? '',\n json: init.json ?? '',\n }\n}\n"],"mappings":";AAsBA,SAAgB,gBAAgB,OAA2B,CAAC,GAAc;CACtE,OAAO;EACH,MAAM,KAAK,QAAQ;EACnB,UAAU,KAAK,YAAY,CAAC;EAC5B,WAAW,KAAK,aAAa;EAC7B,MAAM,KAAK,QAAQ;CACvB;AACJ"}
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
//#region src/core/geometry.ts
|
|
2
|
+
const EPSILON = 1e-9;
|
|
3
|
+
/** Cross product of (o->a) and (o->b). > 0 means counter-clockwise. */
|
|
4
|
+
function cross(o, a, b) {
|
|
5
|
+
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Monotone-chain convex hull. Returns hull points counter-clockwise in a
|
|
9
|
+
* y-down image coordinate system, without the duplicated closing point.
|
|
10
|
+
*/
|
|
11
|
+
function convexHull(points) {
|
|
12
|
+
const pts = [...points].sort((p, q) => p[0] === q[0] ? p[1] - q[1] : p[0] - q[0]);
|
|
13
|
+
const uniq = [];
|
|
14
|
+
for (const p of pts) {
|
|
15
|
+
const last = uniq[uniq.length - 1];
|
|
16
|
+
if (!last || last[0] !== p[0] || last[1] !== p[1]) uniq.push(p);
|
|
17
|
+
}
|
|
18
|
+
if (uniq.length < 3) return uniq;
|
|
19
|
+
const build = (source) => {
|
|
20
|
+
const chain = [];
|
|
21
|
+
for (const p of source) {
|
|
22
|
+
while (chain.length >= 2 && cross(chain[chain.length - 2], chain[chain.length - 1], p) <= 0) chain.pop();
|
|
23
|
+
chain.push(p);
|
|
24
|
+
}
|
|
25
|
+
chain.pop();
|
|
26
|
+
return chain;
|
|
27
|
+
};
|
|
28
|
+
return [...build(uniq), ...build([...uniq].reverse())];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Minimum-area enclosing rectangle via rotating calipers.
|
|
32
|
+
*
|
|
33
|
+
* The minimum-area rectangle always has a side flush with a convex-hull edge,
|
|
34
|
+
* so testing one orientation per hull edge is exhaustive.
|
|
35
|
+
*/
|
|
36
|
+
function minAreaRect(points) {
|
|
37
|
+
if (points.length === 0) return {
|
|
38
|
+
center: [0, 0],
|
|
39
|
+
size: [0, 0],
|
|
40
|
+
angle: 0
|
|
41
|
+
};
|
|
42
|
+
const hull = convexHull(points);
|
|
43
|
+
if (hull.length < 2) {
|
|
44
|
+
const p = hull[0] ?? points[0];
|
|
45
|
+
return {
|
|
46
|
+
center: [p[0], p[1]],
|
|
47
|
+
size: [0, 0],
|
|
48
|
+
angle: 0
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let best = null;
|
|
52
|
+
for (let i = 0; i < hull.length; i++) {
|
|
53
|
+
const a = hull[i];
|
|
54
|
+
const b = hull[(i + 1) % hull.length];
|
|
55
|
+
const len = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
56
|
+
if (len < EPSILON) continue;
|
|
57
|
+
const ux = (b[0] - a[0]) / len;
|
|
58
|
+
const uy = (b[1] - a[1]) / len;
|
|
59
|
+
const vx = -uy;
|
|
60
|
+
const vy = ux;
|
|
61
|
+
let minU = Infinity;
|
|
62
|
+
let maxU = -Infinity;
|
|
63
|
+
let minV = Infinity;
|
|
64
|
+
let maxV = -Infinity;
|
|
65
|
+
for (const p of hull) {
|
|
66
|
+
const pu = p[0] * ux + p[1] * uy;
|
|
67
|
+
const pv = p[0] * vx + p[1] * vy;
|
|
68
|
+
if (pu < minU) minU = pu;
|
|
69
|
+
if (pu > maxU) maxU = pu;
|
|
70
|
+
if (pv < minV) minV = pv;
|
|
71
|
+
if (pv > maxV) maxV = pv;
|
|
72
|
+
}
|
|
73
|
+
const w = maxU - minU;
|
|
74
|
+
const h = maxV - minV;
|
|
75
|
+
const area = w * h;
|
|
76
|
+
if (best === null || area < best.area) {
|
|
77
|
+
const cu = (minU + maxU) / 2;
|
|
78
|
+
const cv = (minV + maxV) / 2;
|
|
79
|
+
best = {
|
|
80
|
+
area,
|
|
81
|
+
center: [cu * ux + cv * vx, cu * uy + cv * vy],
|
|
82
|
+
w,
|
|
83
|
+
h,
|
|
84
|
+
angle: Math.atan2(uy, ux) * 180 / Math.PI
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (best === null) return {
|
|
89
|
+
center: [0, 0],
|
|
90
|
+
size: [0, 0],
|
|
91
|
+
angle: 0
|
|
92
|
+
};
|
|
93
|
+
let { angle, w, h } = best;
|
|
94
|
+
while (angle <= 0) {
|
|
95
|
+
angle += 90;
|
|
96
|
+
[w, h] = [h, w];
|
|
97
|
+
}
|
|
98
|
+
while (angle > 90) {
|
|
99
|
+
angle -= 90;
|
|
100
|
+
[w, h] = [h, w];
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
center: best.center,
|
|
104
|
+
size: [w, h],
|
|
105
|
+
angle
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The 4 corners of a rotated rect, in cv2.boxPoints order.
|
|
110
|
+
* For an upright rect in image coordinates that is: BL, TL, TR, BR.
|
|
111
|
+
*/
|
|
112
|
+
function boxPoints(rect) {
|
|
113
|
+
const [cx, cy] = rect.center;
|
|
114
|
+
const [w, h] = rect.size;
|
|
115
|
+
const rad = rect.angle * Math.PI / 180;
|
|
116
|
+
const b = Math.cos(rad) * .5;
|
|
117
|
+
const a = Math.sin(rad) * .5;
|
|
118
|
+
const p0 = [cx - a * h - b * w, cy + b * h - a * w];
|
|
119
|
+
const p1 = [cx + a * h - b * w, cy - b * h - a * w];
|
|
120
|
+
return [
|
|
121
|
+
p0,
|
|
122
|
+
p1,
|
|
123
|
+
[2 * cx - p0[0], 2 * cy - p0[1]],
|
|
124
|
+
[2 * cx - p1[0], 2 * cy - p1[1]]
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
/** Shoelace area of a simple polygon; always non-negative. */
|
|
128
|
+
function polygonArea(points) {
|
|
129
|
+
let sum = 0;
|
|
130
|
+
for (let i = 0; i < points.length; i++) {
|
|
131
|
+
const a = points[i];
|
|
132
|
+
const b = points[(i + 1) % points.length];
|
|
133
|
+
sum += a[0] * b[1] - b[0] * a[1];
|
|
134
|
+
}
|
|
135
|
+
return Math.abs(sum) / 2;
|
|
136
|
+
}
|
|
137
|
+
/** Perimeter of a closed polygon. */
|
|
138
|
+
function polygonPerimeter(points) {
|
|
139
|
+
let sum = 0;
|
|
140
|
+
for (let i = 0; i < points.length; i++) {
|
|
141
|
+
const a = points[i];
|
|
142
|
+
const b = points[(i + 1) % points.length];
|
|
143
|
+
sum += Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
144
|
+
}
|
|
145
|
+
return sum;
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/schemas/box.ts
|
|
149
|
+
/**
|
|
150
|
+
* Port of `scaledp/schemas/Box.py`.
|
|
151
|
+
*
|
|
152
|
+
* A Box is NOT xyxy and NOT a polygon. `x`/`y` is the top-left of the
|
|
153
|
+
* *axis-aligned* box of the same size centred on the rotated rect's centre,
|
|
154
|
+
* and `angle` is degrees about that same centre. `width` is always the longer
|
|
155
|
+
* side. Getting this wrong silently shifts every downstream consumer, so the
|
|
156
|
+
* conversions below mirror the Python implementation exactly.
|
|
157
|
+
*/
|
|
158
|
+
/** `abs(angle) >= 3` — matches Python's `is_rotated`, which tolerates OCR jitter. */
|
|
159
|
+
const ROTATION_EPSILON_DEGREES = 3;
|
|
160
|
+
function createBox(init = {}) {
|
|
161
|
+
return {
|
|
162
|
+
text: init.text ?? "",
|
|
163
|
+
score: init.score ?? 0,
|
|
164
|
+
x: init.x ?? 0,
|
|
165
|
+
y: init.y ?? 0,
|
|
166
|
+
width: init.width ?? 0,
|
|
167
|
+
height: init.height ?? 0,
|
|
168
|
+
angle: init.angle ?? 0
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function isRotated(box) {
|
|
172
|
+
return Math.abs(box.angle) >= 3;
|
|
173
|
+
}
|
|
174
|
+
function bbox(box, padding = 0) {
|
|
175
|
+
return [
|
|
176
|
+
box.x - padding,
|
|
177
|
+
box.y - padding,
|
|
178
|
+
box.x + box.width + padding,
|
|
179
|
+
box.y + box.height + padding
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
function shape(box, padding = 0) {
|
|
183
|
+
const [x0, y0, x1, y1] = bbox(box, padding);
|
|
184
|
+
return [[x0, y0], [x1, y1]];
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Scale a box, then apply padding.
|
|
188
|
+
*
|
|
189
|
+
* Note the padding is asymmetric, and deliberately so: Python subtracts it from
|
|
190
|
+
* the origin and adds it to the size, so the box grows by `padding` on its left
|
|
191
|
+
* and top while its right and bottom edges stay put. Growing all four sides
|
|
192
|
+
* would need `padding * 2` on the size.
|
|
193
|
+
*/
|
|
194
|
+
function scaleBox(box, factor, padding = 0) {
|
|
195
|
+
return {
|
|
196
|
+
...box,
|
|
197
|
+
x: Math.round(box.x * factor - padding),
|
|
198
|
+
y: Math.round(box.y * factor - padding),
|
|
199
|
+
width: Math.round(box.width * factor + padding),
|
|
200
|
+
height: Math.round(box.height * factor + padding)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function boxFromBBox(box, opts = {}) {
|
|
204
|
+
const [x0, y0, x1, y1] = box;
|
|
205
|
+
return {
|
|
206
|
+
text: opts.text ?? "",
|
|
207
|
+
score: opts.score ?? 0,
|
|
208
|
+
x: Math.round(x0),
|
|
209
|
+
y: Math.round(y0),
|
|
210
|
+
width: Math.round(x1 - x0),
|
|
211
|
+
height: Math.round(y1 - y0),
|
|
212
|
+
angle: opts.angle ?? 0
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Build a Box from exactly 4 polygon points — port of Python `Box.from_polygon`.
|
|
217
|
+
*
|
|
218
|
+
* `width` is forced to be the longer side (subtracting 90 degrees from the angle
|
|
219
|
+
* to compensate), then the angle is normalised to (-90, 270]. `x`/`y` are derived
|
|
220
|
+
* from the centre, NOT from the polygon's bounding box.
|
|
221
|
+
*/
|
|
222
|
+
function boxFromPolygon(points, opts = {}) {
|
|
223
|
+
if (points.length !== 4) throw new Error(`boxFromPolygon expects exactly 4 points, received ${points.length}`);
|
|
224
|
+
const padding = opts.padding ?? 0;
|
|
225
|
+
const rect = minAreaRect(points);
|
|
226
|
+
const [cx, cy] = rect.center;
|
|
227
|
+
let [width, height] = rect.size;
|
|
228
|
+
let angle = rect.angle;
|
|
229
|
+
if (width < height) {
|
|
230
|
+
[width, height] = [height, width];
|
|
231
|
+
angle -= 90;
|
|
232
|
+
}
|
|
233
|
+
angle = (angle % 360 + 360) % 360;
|
|
234
|
+
if (angle > 270) angle -= 360;
|
|
235
|
+
width = Math.max(1, Math.round(width) + padding * 2);
|
|
236
|
+
height = Math.max(1, Math.round(height) + padding * 2);
|
|
237
|
+
return {
|
|
238
|
+
text: opts.text ?? "",
|
|
239
|
+
score: opts.score ?? 1,
|
|
240
|
+
x: Math.round(cx - width / 2),
|
|
241
|
+
y: Math.round(cy - height / 2),
|
|
242
|
+
width,
|
|
243
|
+
height,
|
|
244
|
+
angle
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** Axis-aligned intersection-over-union. Ignores `angle`, exactly as Python does. */
|
|
248
|
+
function boxIou(a, b) {
|
|
249
|
+
const [ax0, ay0, ax1, ay1] = bbox(a);
|
|
250
|
+
const [bx0, by0, bx1, by1] = bbox(b);
|
|
251
|
+
const ix = Math.min(ax1, bx1) - Math.max(ax0, bx0);
|
|
252
|
+
const iy = Math.min(ay1, by1) - Math.max(ay0, by0);
|
|
253
|
+
if (ix <= 0 || iy <= 0) return 0;
|
|
254
|
+
const intersection = ix * iy;
|
|
255
|
+
const union = a.width * a.height + b.width * b.height - intersection;
|
|
256
|
+
return union <= 0 ? 0 : intersection / union;
|
|
257
|
+
}
|
|
258
|
+
/** Union of two boxes. Merging discards rotation — Python resets `angle` to 0. */
|
|
259
|
+
function mergeBoxes(a, b) {
|
|
260
|
+
const [ax0, ay0, ax1, ay1] = bbox(a);
|
|
261
|
+
const [bx0, by0, bx1, by1] = bbox(b);
|
|
262
|
+
const x = Math.min(ax0, bx0);
|
|
263
|
+
const y = Math.min(ay0, by0);
|
|
264
|
+
return {
|
|
265
|
+
text: `${a.text} ${b.text}`.trim(),
|
|
266
|
+
score: Math.min(a.score, b.score),
|
|
267
|
+
x,
|
|
268
|
+
y,
|
|
269
|
+
width: Math.max(ax1, bx1) - x,
|
|
270
|
+
height: Math.max(ay1, by1) - y,
|
|
271
|
+
angle: 0
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Whether two boxes sit on the same text line.
|
|
276
|
+
*
|
|
277
|
+
* For near-horizontal boxes this compares vertical centres against the average
|
|
278
|
+
* height. For rotated boxes it projects the centre offset onto the line's normal
|
|
279
|
+
* (dx = -sin, dy = cos), so the test follows the text's own baseline.
|
|
280
|
+
*/
|
|
281
|
+
function isOnSameLine(a, b, angleThresh = 10, lineThresh = .5) {
|
|
282
|
+
if (Math.abs(a.angle - b.angle) > angleThresh) return false;
|
|
283
|
+
const avgHeight = (a.height + b.height) / 2;
|
|
284
|
+
if (avgHeight <= 0) return false;
|
|
285
|
+
const acx = a.x + a.width / 2;
|
|
286
|
+
const acy = a.y + a.height / 2;
|
|
287
|
+
const bcx = b.x + b.width / 2;
|
|
288
|
+
const bcy = b.y + b.height / 2;
|
|
289
|
+
if (Math.abs(a.angle) < angleThresh) return Math.abs(acy - bcy) < avgHeight * lineThresh;
|
|
290
|
+
const rad = a.angle * Math.PI / 180;
|
|
291
|
+
const nx = -Math.sin(rad);
|
|
292
|
+
const ny = Math.cos(rad);
|
|
293
|
+
return Math.abs((bcx - acx) * nx + (bcy - acy) * ny) < avgHeight * lineThresh;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Greedily merge boxes that overlap and share a line. Port of Python
|
|
297
|
+
* `Box.merge_overlapping_boxes`.
|
|
298
|
+
*
|
|
299
|
+
* Restarts the scan after each merge so a chain of boxes collapses fully in one
|
|
300
|
+
* call, matching Python's behaviour.
|
|
301
|
+
*/
|
|
302
|
+
function mergeOverlappingBoxes(boxes, iouThreshold = .3, angleThresh = 10, lineThresh = .5) {
|
|
303
|
+
const merged = [];
|
|
304
|
+
const used = new Array(boxes.length).fill(false);
|
|
305
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
306
|
+
if (used[i]) continue;
|
|
307
|
+
let current = boxes[i];
|
|
308
|
+
for (let j = i + 1; j < boxes.length; j++) {
|
|
309
|
+
if (used[j]) continue;
|
|
310
|
+
const other = boxes[j];
|
|
311
|
+
if (boxIou(current, other) > iouThreshold && isOnSameLine(current, other, angleThresh, lineThresh)) {
|
|
312
|
+
current = mergeBoxes(current, other);
|
|
313
|
+
used[j] = true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
merged.push(current);
|
|
317
|
+
used[i] = true;
|
|
318
|
+
}
|
|
319
|
+
return merged;
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/core/image.ts
|
|
323
|
+
function assertCanvasSupport() {
|
|
324
|
+
if (typeof OffscreenCanvas === "undefined") throw new Error("OffscreenCanvas is unavailable. scaledp requires a browser or worker context.");
|
|
325
|
+
}
|
|
326
|
+
function createCanvas(width, height) {
|
|
327
|
+
assertCanvasSupport();
|
|
328
|
+
return new OffscreenCanvas(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)));
|
|
329
|
+
}
|
|
330
|
+
function context2d(canvas) {
|
|
331
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
332
|
+
if (!ctx) throw new Error("Failed to acquire a 2D context");
|
|
333
|
+
return ctx;
|
|
334
|
+
}
|
|
335
|
+
async function decodeImage(data) {
|
|
336
|
+
if (data instanceof Blob) return createImageBitmap(data);
|
|
337
|
+
const bytes = new Uint8Array(data.byteLength);
|
|
338
|
+
bytes.set(data);
|
|
339
|
+
return createImageBitmap(new Blob([bytes.buffer]));
|
|
340
|
+
}
|
|
341
|
+
function toImageData(source) {
|
|
342
|
+
if (source instanceof OffscreenCanvas) return context2d(source).getImageData(0, 0, source.width, source.height);
|
|
343
|
+
const canvas = createCanvas(source.width, source.height);
|
|
344
|
+
context2d(canvas).drawImage(source, 0, 0);
|
|
345
|
+
return context2d(canvas).getImageData(0, 0, canvas.width, canvas.height);
|
|
346
|
+
}
|
|
347
|
+
function imageDataToCanvas(image) {
|
|
348
|
+
const canvas = createCanvas(image.width, image.height);
|
|
349
|
+
context2d(canvas).putImageData(image, 0, 0);
|
|
350
|
+
return canvas;
|
|
351
|
+
}
|
|
352
|
+
async function encodeImage(source, type = "image/png", quality) {
|
|
353
|
+
const blob = await (source instanceof OffscreenCanvas ? source : imageDataToCanvas(source)).convertToBlob({
|
|
354
|
+
type,
|
|
355
|
+
quality
|
|
356
|
+
});
|
|
357
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
358
|
+
}
|
|
359
|
+
/** Read the intrinsic size of encoded image bytes without keeping the bitmap. */
|
|
360
|
+
async function probeImageSize(data) {
|
|
361
|
+
const bitmap = await decodeImage(data);
|
|
362
|
+
try {
|
|
363
|
+
return {
|
|
364
|
+
width: bitmap.width,
|
|
365
|
+
height: bitmap.height
|
|
366
|
+
};
|
|
367
|
+
} finally {
|
|
368
|
+
bitmap.close();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Fit an image into `target` preserving aspect ratio.
|
|
373
|
+
*
|
|
374
|
+
* `padding: 'end'` pads bottom and right only, matching PaddleOCR's detection
|
|
375
|
+
* preprocessing -- coordinates then restore by dividing by `scale`, with no
|
|
376
|
+
* offset to subtract. `padding: 'center'` centres the image, matching the YOLO
|
|
377
|
+
* preprocessing, where the pad offsets must be subtracted before unscaling.
|
|
378
|
+
*/
|
|
379
|
+
function letterbox(source, target, opts = {}) {
|
|
380
|
+
const padding = opts.padding ?? "end";
|
|
381
|
+
const canvas = createCanvas(target.width, target.height);
|
|
382
|
+
const ctx = context2d(canvas);
|
|
383
|
+
ctx.fillStyle = opts.fill ?? "#ffffff";
|
|
384
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
385
|
+
const scale = Math.min(target.width / source.width, target.height / source.height);
|
|
386
|
+
const width = Math.trunc(source.width * scale);
|
|
387
|
+
const height = Math.trunc(source.height * scale);
|
|
388
|
+
const dx = padding === "center" ? Math.trunc((target.width - width) / 2) : 0;
|
|
389
|
+
const dy = padding === "center" ? Math.trunc((target.height - height) / 2) : 0;
|
|
390
|
+
ctx.drawImage(source, dx, dy, width, height);
|
|
391
|
+
return {
|
|
392
|
+
canvas,
|
|
393
|
+
scale,
|
|
394
|
+
resized: {
|
|
395
|
+
width,
|
|
396
|
+
height
|
|
397
|
+
},
|
|
398
|
+
source: {
|
|
399
|
+
width: source.width,
|
|
400
|
+
height: source.height
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/** Uniform resize by a scale factor. */
|
|
405
|
+
function resize(source, factor) {
|
|
406
|
+
const canvas = createCanvas(source.width * factor, source.height * factor);
|
|
407
|
+
context2d(canvas).drawImage(source, 0, 0, canvas.width, canvas.height);
|
|
408
|
+
return canvas;
|
|
409
|
+
}
|
|
410
|
+
function cropGeometry(box, opts = {}) {
|
|
411
|
+
const scaled = scaleBox(box, opts.scaleFactor ?? 1, opts.padding ?? 0);
|
|
412
|
+
const width = Math.max(1, scaled.width);
|
|
413
|
+
const height = Math.max(1, scaled.height);
|
|
414
|
+
const axisAligned = {
|
|
415
|
+
scaled,
|
|
416
|
+
width,
|
|
417
|
+
height,
|
|
418
|
+
map: (x, y) => [scaled.x + x, scaled.y + y]
|
|
419
|
+
};
|
|
420
|
+
if (Math.abs(scaled.angle) < 3) return axisAligned;
|
|
421
|
+
const [, tl, tr, br] = boxPoints({
|
|
422
|
+
center: [scaled.x + width / 2, scaled.y + height / 2],
|
|
423
|
+
size: [width, height],
|
|
424
|
+
angle: scaled.angle
|
|
425
|
+
});
|
|
426
|
+
const bl = [tl[0] + (br[0] - tr[0]), tl[1] + (br[1] - tr[1])];
|
|
427
|
+
const ex = [(tr[0] - tl[0]) / width, (tr[1] - tl[1]) / width];
|
|
428
|
+
const ey = [(bl[0] - tl[0]) / height, (bl[1] - tl[1]) / height];
|
|
429
|
+
const det = ex[0] * ey[1] - ey[0] * ex[1];
|
|
430
|
+
if (Math.abs(det) < 1e-9) return axisAligned;
|
|
431
|
+
return {
|
|
432
|
+
scaled,
|
|
433
|
+
width,
|
|
434
|
+
height,
|
|
435
|
+
map: (x, y) => [tl[0] + x * ex[0] + y * ey[0], tl[1] + x * ex[1] + y * ey[1]]
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function cropBox(source, box, opts = {}) {
|
|
439
|
+
const { scaled, width, height, map } = cropGeometry(box, opts);
|
|
440
|
+
const canvas = createCanvas(width, height);
|
|
441
|
+
const ctx = context2d(canvas);
|
|
442
|
+
const [ox, oy] = map(0, 0);
|
|
443
|
+
const [x1, y1] = map(1, 0);
|
|
444
|
+
const [x2, y2] = map(0, 1);
|
|
445
|
+
const ex = [x1 - ox, y1 - oy];
|
|
446
|
+
const ey = [x2 - ox, y2 - oy];
|
|
447
|
+
if (ex[1] === 0 && ey[0] === 0 && ex[0] === 1 && ey[1] === 1) {
|
|
448
|
+
ctx.drawImage(source, scaled.x, scaled.y, width, height, 0, 0, width, height);
|
|
449
|
+
return canvas;
|
|
450
|
+
}
|
|
451
|
+
const det = ex[0] * ey[1] - ey[0] * ex[1];
|
|
452
|
+
const a = ey[1] / det;
|
|
453
|
+
const b = -ex[1] / det;
|
|
454
|
+
const c = -ey[0] / det;
|
|
455
|
+
const d = ex[0] / det;
|
|
456
|
+
ctx.setTransform(a, b, c, d, -(a * ox + c * oy), -(b * ox + d * oy));
|
|
457
|
+
ctx.drawImage(source, 0, 0);
|
|
458
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
459
|
+
return canvas;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Build an NCHW float32 tensor from an image.
|
|
463
|
+
*
|
|
464
|
+
* `bgr` exists because ScaleDP's DBNet path converts RGB->BGR and never swaps
|
|
465
|
+
* back, so the model is fed BGR channels against RGB ImageNet statistics.
|
|
466
|
+
* Replicating that quirk is required for box parity.
|
|
467
|
+
*/
|
|
468
|
+
function toNchwFloat32(image, opts = {}) {
|
|
469
|
+
const { width, height, data } = image;
|
|
470
|
+
const scale = opts.scale ?? 1 / 255;
|
|
471
|
+
const mean = opts.mean ?? [
|
|
472
|
+
0,
|
|
473
|
+
0,
|
|
474
|
+
0
|
|
475
|
+
];
|
|
476
|
+
const std = opts.std ?? [
|
|
477
|
+
1,
|
|
478
|
+
1,
|
|
479
|
+
1
|
|
480
|
+
];
|
|
481
|
+
const order = opts.bgr ? [
|
|
482
|
+
2,
|
|
483
|
+
1,
|
|
484
|
+
0
|
|
485
|
+
] : [
|
|
486
|
+
0,
|
|
487
|
+
1,
|
|
488
|
+
2
|
|
489
|
+
];
|
|
490
|
+
const plane = width * height;
|
|
491
|
+
const out = new Float32Array(3 * plane);
|
|
492
|
+
for (let i = 0; i < plane; i++) for (let c = 0; c < 3; c++) {
|
|
493
|
+
const value = data[i * 4 + order[c]] * scale;
|
|
494
|
+
out[c * plane + i] = (value - mean[c]) / std[c];
|
|
495
|
+
}
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
498
|
+
const IMAGENET_MEAN = [
|
|
499
|
+
.485,
|
|
500
|
+
.456,
|
|
501
|
+
.406
|
|
502
|
+
];
|
|
503
|
+
const IMAGENET_STD = [
|
|
504
|
+
.229,
|
|
505
|
+
.224,
|
|
506
|
+
.225
|
|
507
|
+
];
|
|
508
|
+
//#endregion
|
|
509
|
+
export { polygonArea as A, mergeBoxes as C, boxPoints as D, shape as E, convexHull as O, isRotated as S, scaleBox as T, boxFromBBox as _, cropBox as a, createBox as b, encodeImage as c, probeImageSize as d, resize as f, bbox as g, ROTATION_EPSILON_DEGREES as h, createCanvas as i, polygonPerimeter as j, minAreaRect as k, imageDataToCanvas as l, toNchwFloat32 as m, IMAGENET_STD as n, cropGeometry as o, toImageData as p, context2d as r, decodeImage as s, IMAGENET_MEAN as t, letterbox as u, boxFromPolygon as v, mergeOverlappingBoxes as w, isOnSameLine as x, boxIou as y };
|
|
510
|
+
|
|
511
|
+
//# sourceMappingURL=image-CAH2rLv9.js.map
|