@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":"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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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/schemas/image.d.ts
|
|
2
|
+
/** Port of `scaledp/schemas/Image.py`. */
|
|
3
|
+
type ImageFormat = 'png' | 'webp' | 'jpeg';
|
|
4
|
+
interface ScaleDpImage {
|
|
5
|
+
path: string;
|
|
6
|
+
/** DPI the image was rendered at; 0 when unknown. */
|
|
7
|
+
resolution: number;
|
|
8
|
+
/** Encoded image bytes (PNG unless `imageType` says otherwise). */
|
|
9
|
+
data: Uint8Array;
|
|
10
|
+
imageType: ImageFormat;
|
|
11
|
+
exception: string;
|
|
12
|
+
height: number;
|
|
13
|
+
width: number;
|
|
14
|
+
}
|
|
15
|
+
declare function createImage(init?: Partial<ScaleDpImage>): ScaleDpImage;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { ScaleDpImage as n, createImage as r, ImageFormat as t };
|
|
18
|
+
//# sourceMappingURL=image-Dc5TSg46.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/schemas/document.ts
|
|
2
|
+
function createDocument(init = {}) {
|
|
3
|
+
return {
|
|
4
|
+
path: init.path ?? "memory",
|
|
5
|
+
text: init.text ?? "",
|
|
6
|
+
type: init.type ?? "text",
|
|
7
|
+
bboxes: init.bboxes ?? [],
|
|
8
|
+
exception: init.exception ?? ""
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Python's `Document.merge` puts the *argument* first and joins with a newline. */
|
|
12
|
+
function mergeDocuments(self, other) {
|
|
13
|
+
return {
|
|
14
|
+
path: self.path,
|
|
15
|
+
text: `${other.text}\n${self.text}`,
|
|
16
|
+
type: self.type,
|
|
17
|
+
bboxes: [...other.bboxes, ...self.bboxes],
|
|
18
|
+
exception: self.exception || other.exception
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/schemas/image.ts
|
|
23
|
+
function createImage(init = {}) {
|
|
24
|
+
return {
|
|
25
|
+
path: init.path ?? "memory",
|
|
26
|
+
resolution: init.resolution ?? 0,
|
|
27
|
+
data: init.data ?? /* @__PURE__ */ new Uint8Array(0),
|
|
28
|
+
imageType: init.imageType ?? "png",
|
|
29
|
+
exception: init.exception ?? "",
|
|
30
|
+
height: init.height ?? 0,
|
|
31
|
+
width: init.width ?? 0
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { createDocument as n, mergeDocuments as r, createImage as t };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=image-DoZDJkcR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-DoZDJkcR.js","names":[],"sources":["../src/schemas/document.ts","../src/schemas/image.ts"],"sourcesContent":["/** Port of `scaledp/schemas/Document.py`. */\n\nimport type { Box } from './box.js'\n\nexport interface Document {\n path: string\n text: string\n /** Producer of this document: 'text' | 'ocr' | 'pdf' | an engine name. */\n type: string\n bboxes: Box[]\n exception: string\n}\n\nexport function createDocument(init: Partial<Document> = {}): Document {\n return {\n path: init.path ?? 'memory',\n text: init.text ?? '',\n type: init.type ?? 'text',\n bboxes: init.bboxes ?? [],\n exception: init.exception ?? '',\n }\n}\n\n/** Python's `Document.merge` puts the *argument* first and joins with a newline. */\nexport function mergeDocuments(self: Document, other: Document): Document {\n return {\n path: self.path,\n text: `${other.text}\\n${self.text}`,\n type: self.type,\n bboxes: [...other.bboxes, ...self.bboxes],\n exception: self.exception || other.exception,\n }\n}\n","/** Port of `scaledp/schemas/Image.py`. */\n\nexport type ImageFormat = 'png' | 'webp' | 'jpeg'\n\nexport interface ScaleDpImage {\n path: string\n /** DPI the image was rendered at; 0 when unknown. */\n resolution: number\n /** Encoded image bytes (PNG unless `imageType` says otherwise). */\n data: Uint8Array\n imageType: ImageFormat\n exception: string\n height: number\n width: number\n}\n\nexport function createImage(init: Partial<ScaleDpImage> = {}): ScaleDpImage {\n return {\n path: init.path ?? 'memory',\n resolution: init.resolution ?? 0,\n data: init.data ?? new Uint8Array(0),\n imageType: init.imageType ?? 'png',\n exception: init.exception ?? '',\n height: init.height ?? 0,\n width: init.width ?? 0,\n }\n}\n"],"mappings":";AAaA,SAAgB,eAAe,OAA0B,CAAC,GAAa;CACnE,OAAO;EACH,MAAM,KAAK,QAAQ;EACnB,MAAM,KAAK,QAAQ;EACnB,MAAM,KAAK,QAAQ;EACnB,QAAQ,KAAK,UAAU,CAAC;EACxB,WAAW,KAAK,aAAa;CACjC;AACJ;;AAGA,SAAgB,eAAe,MAAgB,OAA2B;CACtE,OAAO;EACH,MAAM,KAAK;EACX,MAAM,GAAG,MAAM,KAAK,IAAI,KAAK;EAC7B,MAAM,KAAK;EACX,QAAQ,CAAC,GAAG,MAAM,QAAQ,GAAG,KAAK,MAAM;EACxC,WAAW,KAAK,aAAa,MAAM;CACvC;AACJ;;;AChBA,SAAgB,YAAY,OAA8B,CAAC,GAAiB;CACxE,OAAO;EACH,MAAM,KAAK,QAAQ;EACnB,YAAY,KAAK,cAAc;EAC/B,MAAM,KAAK,wBAAQ,IAAI,WAAW,CAAC;EACnC,WAAW,KAAK,aAAa;EAC7B,WAAW,KAAK,aAAa;EAC7B,QAAQ,KAAK,UAAU;EACvB,OAAO,KAAK,SAAS;CACzB;AACJ"}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { c as ImageError, i as Stage } from "./pipeline-DACqGkpN.js";
|
|
2
|
+
import { S as isRotated, a as cropBox, c as encodeImage, i as createCanvas, r as context2d, s as decodeImage } from "./image-CAH2rLv9.js";
|
|
3
|
+
import { i as resolveParams, t as BASE_STAGE_DEFAULTS } from "./params-DapwK9Ns.js";
|
|
4
|
+
import { t as createImage } from "./image-DoZDJkcR.js";
|
|
5
|
+
//#region src/stages/image-crop-boxes.ts
|
|
6
|
+
/**
|
|
7
|
+
* Port of `scaledp/image/ImageCropBoxes.py`: crop each detected box out of the
|
|
8
|
+
* page, emitting one row per crop.
|
|
9
|
+
*
|
|
10
|
+
* Rotated boxes are straightened rather than cropped to their envelope, which
|
|
11
|
+
* is what makes the crops usable as recognizer input.
|
|
12
|
+
*/
|
|
13
|
+
const IMAGE_CROP_BOXES_DEFAULTS = Object.freeze({
|
|
14
|
+
...BASE_STAGE_DEFAULTS,
|
|
15
|
+
inputCol: "image",
|
|
16
|
+
inputCols: ["image", "boxes"],
|
|
17
|
+
outputCol: "cropped_image",
|
|
18
|
+
keepInputData: true,
|
|
19
|
+
imageType: "png",
|
|
20
|
+
padding: 0,
|
|
21
|
+
limit: 0,
|
|
22
|
+
autoRotate: true,
|
|
23
|
+
returnEmpty: false,
|
|
24
|
+
boxCol: "box"
|
|
25
|
+
});
|
|
26
|
+
function boxesOf(source) {
|
|
27
|
+
if (typeof source !== "object" || source === null) return [];
|
|
28
|
+
return source.bboxes ?? [];
|
|
29
|
+
}
|
|
30
|
+
var ImageCropBoxes = class extends Stage {
|
|
31
|
+
name = "ImageCropBoxes";
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
super(resolveParams(IMAGE_CROP_BOXES_DEFAULTS, options, { inputCols: (value) => {
|
|
34
|
+
if (value.length !== 2) throw new RangeError("inputCols must be [imageColumn, boxColumn]");
|
|
35
|
+
} }));
|
|
36
|
+
}
|
|
37
|
+
async expand(_input, row, ctx) {
|
|
38
|
+
const { inputCols, outputCol, boxCol, limit, padding, autoRotate, imageType } = this.params;
|
|
39
|
+
const [imageCol, boxSourceCol] = inputCols;
|
|
40
|
+
const image = row[imageCol];
|
|
41
|
+
if (image?.exception) throw new ImageError(`Upstream stage failed: ${image.exception}`, this.name);
|
|
42
|
+
if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) throw new ImageError("Expected an Image with decoded bytes", this.name);
|
|
43
|
+
let boxes = boxesOf(row[boxSourceCol]);
|
|
44
|
+
if (limit > 0) boxes = boxes.slice(0, limit);
|
|
45
|
+
if (boxes.length === 0) {
|
|
46
|
+
if (!this.params.returnEmpty) throw new ImageError("No boxes to crop", this.name);
|
|
47
|
+
return [{
|
|
48
|
+
...row,
|
|
49
|
+
[outputCol]: image,
|
|
50
|
+
[boxCol]: null
|
|
51
|
+
}];
|
|
52
|
+
}
|
|
53
|
+
const bitmap = await decodeImage(image.data);
|
|
54
|
+
try {
|
|
55
|
+
const rows = [];
|
|
56
|
+
for (const box of boxes) {
|
|
57
|
+
ctx.signal?.throwIfAborted();
|
|
58
|
+
let canvas = cropBox(bitmap, box, { padding });
|
|
59
|
+
if (autoRotate && canvas.height > canvas.width) canvas = rotateQuarterTurn(canvas);
|
|
60
|
+
rows.push({
|
|
61
|
+
...row,
|
|
62
|
+
[boxCol]: box,
|
|
63
|
+
[outputCol]: createImage({
|
|
64
|
+
path: image.path,
|
|
65
|
+
resolution: image.resolution,
|
|
66
|
+
data: await encodeImage(canvas, `image/${imageType}`),
|
|
67
|
+
imageType,
|
|
68
|
+
width: canvas.width,
|
|
69
|
+
height: canvas.height
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return rows;
|
|
74
|
+
} finally {
|
|
75
|
+
bitmap.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async apply() {
|
|
79
|
+
throw new ImageError("unreachable: expand handles every row", this.name);
|
|
80
|
+
}
|
|
81
|
+
onError(message, row) {
|
|
82
|
+
return createImage({
|
|
83
|
+
path: String(row[this.params.pathCol] ?? "memory"),
|
|
84
|
+
exception: message
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/** Rotate 90 degrees counter-clockwise, swapping the canvas dimensions. */
|
|
89
|
+
function rotateQuarterTurn(source) {
|
|
90
|
+
const rotated = new OffscreenCanvas(source.height, source.width);
|
|
91
|
+
const ctx = rotated.getContext("2d");
|
|
92
|
+
if (!ctx) throw new ImageError("Failed to acquire a 2D context", "ImageCropBoxes");
|
|
93
|
+
ctx.translate(0, rotated.height);
|
|
94
|
+
ctx.rotate(-Math.PI / 2);
|
|
95
|
+
ctx.drawImage(source, 0, 0);
|
|
96
|
+
return rotated;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/stages/image-draw-boxes.ts
|
|
100
|
+
/**
|
|
101
|
+
* Port of `scaledp/image/ImageDrawBoxes.py`: draw detection or NER boxes onto
|
|
102
|
+
* the page.
|
|
103
|
+
*
|
|
104
|
+
* Takes several input columns -- an image plus one or more box sources -- and
|
|
105
|
+
* writes a new image. A source is treated as NER output when it has `entities`,
|
|
106
|
+
* and as boxes when it has `bboxes`.
|
|
107
|
+
*/
|
|
108
|
+
const IMAGE_DRAW_BOXES_DEFAULTS = Object.freeze({
|
|
109
|
+
...BASE_STAGE_DEFAULTS,
|
|
110
|
+
inputCol: "image",
|
|
111
|
+
inputCols: ["image", "boxes"],
|
|
112
|
+
outputCol: "image_with_boxes",
|
|
113
|
+
keepInputData: true,
|
|
114
|
+
imageType: "png",
|
|
115
|
+
filled: false,
|
|
116
|
+
color: null,
|
|
117
|
+
lineWidth: 1,
|
|
118
|
+
textSize: 12,
|
|
119
|
+
displayDataList: [],
|
|
120
|
+
padding: 0,
|
|
121
|
+
whiteList: [],
|
|
122
|
+
blackList: []
|
|
123
|
+
});
|
|
124
|
+
/**
|
|
125
|
+
* A stable colour per group name.
|
|
126
|
+
*
|
|
127
|
+
* Python picks a random colour per group, which changes on every run and makes
|
|
128
|
+
* two renders of the same document impossible to compare. Hashing the name
|
|
129
|
+
* instead keeps 'PERSON' the same colour everywhere, and the fixed saturation
|
|
130
|
+
* and lightness keep every colour legible on a white page.
|
|
131
|
+
*/
|
|
132
|
+
function colorForGroup(name) {
|
|
133
|
+
let hash = 0;
|
|
134
|
+
for (let i = 0; i < name.length; i++) hash = hash * 31 + name.charCodeAt(i) | 0;
|
|
135
|
+
return `hsl(${Math.abs(hash) % 360}, 70%, 45%)`;
|
|
136
|
+
}
|
|
137
|
+
function labelFor(source, fields) {
|
|
138
|
+
const parts = [];
|
|
139
|
+
for (const field of fields) {
|
|
140
|
+
const value = source[field];
|
|
141
|
+
if (value === void 0 || value === null) continue;
|
|
142
|
+
parts.push(typeof value === "number" ? value.toFixed(2) : String(value));
|
|
143
|
+
}
|
|
144
|
+
return parts.join(":");
|
|
145
|
+
}
|
|
146
|
+
function isNerOutput(value) {
|
|
147
|
+
return typeof value === "object" && value !== null && "entities" in value;
|
|
148
|
+
}
|
|
149
|
+
var ImageDrawBoxes = class extends Stage {
|
|
150
|
+
name = "ImageDrawBoxes";
|
|
151
|
+
constructor(options = {}) {
|
|
152
|
+
super(resolveParams(IMAGE_DRAW_BOXES_DEFAULTS, options, { inputCols: (value) => {
|
|
153
|
+
if (value.length < 2) throw new RangeError("inputCols needs an image column and at least one box column");
|
|
154
|
+
} }));
|
|
155
|
+
}
|
|
156
|
+
/** The base class drives `apply` off inputCol; this stage reads several. */
|
|
157
|
+
async apply(_input, row) {
|
|
158
|
+
const [imageCol, ...boxCols] = this.params.inputCols;
|
|
159
|
+
const image = row[imageCol];
|
|
160
|
+
if (image?.exception) throw new ImageError(`Upstream stage failed: ${image.exception}`, this.name);
|
|
161
|
+
if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) throw new ImageError("Expected an Image with decoded bytes", this.name);
|
|
162
|
+
const bitmap = await decodeImage(image.data);
|
|
163
|
+
try {
|
|
164
|
+
const canvas = createCanvas(bitmap.width, bitmap.height);
|
|
165
|
+
const ctx = context2d(canvas);
|
|
166
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
167
|
+
for (const column of boxCols) {
|
|
168
|
+
const source = row[column];
|
|
169
|
+
if (!source) continue;
|
|
170
|
+
if (isNerOutput(source)) this.drawEntities(ctx, source.entities);
|
|
171
|
+
else this.drawBoxes(ctx, source.bboxes);
|
|
172
|
+
}
|
|
173
|
+
return createImage({
|
|
174
|
+
path: image.path,
|
|
175
|
+
resolution: image.resolution,
|
|
176
|
+
data: await encodeImage(canvas, `image/${this.params.imageType}`),
|
|
177
|
+
imageType: this.params.imageType,
|
|
178
|
+
width: canvas.width,
|
|
179
|
+
height: canvas.height
|
|
180
|
+
});
|
|
181
|
+
} finally {
|
|
182
|
+
bitmap.close();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
drawBoxes(ctx, boxes) {
|
|
186
|
+
for (const box of boxes) {
|
|
187
|
+
const color = this.params.color ?? colorForGroup(box.text || "default");
|
|
188
|
+
this.drawBox(ctx, box, color);
|
|
189
|
+
const label = labelFor(box, this.params.displayDataList);
|
|
190
|
+
if (label) this.drawLabel(ctx, box, label, color);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
drawEntities(ctx, entities) {
|
|
194
|
+
const { whiteList, blackList } = this.params;
|
|
195
|
+
for (const entity of entities) {
|
|
196
|
+
if (whiteList.length > 0 && !whiteList.includes(entity.entity_group)) continue;
|
|
197
|
+
if (blackList.includes(entity.entity_group)) continue;
|
|
198
|
+
const color = this.params.color ?? colorForGroup(entity.entity_group);
|
|
199
|
+
for (const box of entity.boxes) {
|
|
200
|
+
this.drawBox(ctx, box, color);
|
|
201
|
+
const label = labelFor(entity, this.params.displayDataList);
|
|
202
|
+
if (label) this.drawLabel(ctx, box, label, color);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
drawBox(ctx, box, color) {
|
|
207
|
+
const { padding, lineWidth, filled } = this.params;
|
|
208
|
+
ctx.strokeStyle = color;
|
|
209
|
+
ctx.lineWidth = lineWidth;
|
|
210
|
+
ctx.fillStyle = color;
|
|
211
|
+
if (!isRotated(box)) {
|
|
212
|
+
ctx.beginPath();
|
|
213
|
+
ctx.roundRect(box.x - padding, box.y - padding, box.width + padding * 2, box.height + padding * 2, 4);
|
|
214
|
+
if (filled) ctx.fill();
|
|
215
|
+
ctx.stroke();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const cx = box.x + box.width / 2;
|
|
219
|
+
const cy = box.y + box.height / 2;
|
|
220
|
+
const rad = box.angle * Math.PI / 180;
|
|
221
|
+
const cos = Math.cos(rad);
|
|
222
|
+
const sin = Math.sin(rad);
|
|
223
|
+
const half = [
|
|
224
|
+
[-box.width / 2 - padding, -box.height / 2 - padding],
|
|
225
|
+
[box.width / 2 + padding, -box.height / 2 - padding],
|
|
226
|
+
[box.width / 2 + padding, box.height / 2 + padding],
|
|
227
|
+
[-box.width / 2 - padding, box.height / 2 + padding]
|
|
228
|
+
];
|
|
229
|
+
ctx.beginPath();
|
|
230
|
+
half.forEach(([px, py], index) => {
|
|
231
|
+
const x = px * cos - py * sin + cx;
|
|
232
|
+
const y = px * sin + py * cos + cy;
|
|
233
|
+
if (index === 0) ctx.moveTo(x, y);
|
|
234
|
+
else ctx.lineTo(x, y);
|
|
235
|
+
});
|
|
236
|
+
ctx.closePath();
|
|
237
|
+
if (filled) ctx.fill();
|
|
238
|
+
ctx.stroke();
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The label chip, anchored to the box's own top edge.
|
|
242
|
+
*
|
|
243
|
+
* Python places it at `box.x, box.y`, which is the corner of the *axis-
|
|
244
|
+
* aligned envelope*. For an upright box that is the top-left corner and the
|
|
245
|
+
* label sits where you expect; for a rotated one the envelope corner can be
|
|
246
|
+
* a long way off the box -- a vertical line's label lands beside its middle,
|
|
247
|
+
* detached from the box it names. Here the chip is drawn in the box's own
|
|
248
|
+
* frame instead, so it rides the top edge at the box's angle whatever that
|
|
249
|
+
* angle is. Upright boxes come out exactly where Python puts them.
|
|
250
|
+
*/
|
|
251
|
+
drawLabel(ctx, box, label, color) {
|
|
252
|
+
const { textSize, padding } = this.params;
|
|
253
|
+
ctx.font = `${textSize}px sans-serif`;
|
|
254
|
+
const width = ctx.measureText(label).width;
|
|
255
|
+
const height = textSize * 1.2;
|
|
256
|
+
const chip = (x, y) => {
|
|
257
|
+
ctx.fillStyle = color;
|
|
258
|
+
ctx.fillRect(x, y, width + 6, height);
|
|
259
|
+
ctx.fillStyle = "#ffffff";
|
|
260
|
+
ctx.textBaseline = "top";
|
|
261
|
+
ctx.fillText(label, x + 3, y + 1);
|
|
262
|
+
};
|
|
263
|
+
if (!isRotated(box)) {
|
|
264
|
+
chip(box.x - padding, box.y - height - padding);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const left = -box.width / 2 - padding;
|
|
268
|
+
const top = -box.height / 2 - padding;
|
|
269
|
+
ctx.save();
|
|
270
|
+
ctx.translate(box.x + box.width / 2, box.y + box.height / 2);
|
|
271
|
+
ctx.rotate(box.angle * Math.PI / 180);
|
|
272
|
+
chip(left, top - height);
|
|
273
|
+
ctx.restore();
|
|
274
|
+
}
|
|
275
|
+
onError(message, row) {
|
|
276
|
+
return createImage({
|
|
277
|
+
path: String(row[this.params.pathCol] ?? "memory"),
|
|
278
|
+
exception: message
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
//#endregion
|
|
283
|
+
export { ImageCropBoxes as a, IMAGE_CROP_BOXES_DEFAULTS as i, ImageDrawBoxes as n, colorForGroup as r, IMAGE_DRAW_BOXES_DEFAULTS as t };
|
|
284
|
+
|
|
285
|
+
//# sourceMappingURL=image-draw-boxes-De0QbFv9.js.map
|
|
@@ -0,0 +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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"}
|