ppu-ocv 3.1.5 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 PT. Perkasa Pilar Utama
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ppu-ocv
2
2
 
3
- [![NPM](https://img.shields.io/npm/dw/ppu-ocv)](https://www.npmjs.com/package/ppu-ocv) [![JSR](https://jsr.io/badges/@snowfluke/ppu-ocv)](https://jsr.io/@snowfluke/ppu-ocv)
3
+ [![NPM](https://img.shields.io/npm/dw/ppu-ocv)](https://www.npmjs.com/package/ppu-ocv) [![JSR](https://jsr.io/badges/@snowfluke/ppu-ocv)](https://jsr.io/@snowfluke/ppu-ocv) [![npm version](https://img.shields.io/npm/v/ppu-ocv)](https://www.npmjs.com/package/ppu-ocv) [![Provenance](https://img.shields.io/badge/npm-signed%20provenance-blue?logo=npm)](https://www.npmjs.com/package/ppu-ocv#provenance) [![License: MIT](https://img.shields.io/npm/l/ppu-ocv)](./LICENSE) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/PT-Perkasa-Pilar-Utama/ppu-ocv/badge)](https://scorecard.dev/viewer/?uri=github.com/PT-Perkasa-Pilar-Utama/ppu-ocv) [![Socket Badge](https://socket.dev/api/badge/npm/package/ppu-ocv)](https://socket.dev/npm/package/ppu-ocv) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12961/badge)](https://www.bestpractices.dev/projects/12961)
4
4
 
5
5
  A type-safe, modular, chainable image processing library built on top of OpenCV.js with a fluent API leveraging pipeline processing. Decoupled canvas utilities run anywhere — Node, Bun, browsers, browser extensions, and service workers — with or without OpenCV.
6
6
 
@@ -168,14 +168,24 @@ processor.destroy();
168
168
 
169
169
  ### Vanilla HTML (no bundler)
170
170
 
171
- `initRuntime()` automatically loads `@techstark/opencv-js` from the npm CDN if it's not already available. No extra script tags or import maps needed:
171
+ The web build does not bundle OpenCV load `opencv.js` yourself so it is on
172
+ `globalThis.cv`, then `initRuntime()` waits for the WASM to finish initializing:
172
173
 
173
174
  ```html
175
+ <!-- Load OpenCV; sets globalThis.cv (WASM initializes asynchronously). -->
176
+ <script async src="https://docs.opencv.org/4.10.0/opencv.js"></script>
174
177
  <script type="module">
175
178
  import {
176
179
  CanvasProcessor,
177
180
  ImageProcessor,
178
181
  } from "https://cdn.jsdelivr.net/npm/ppu-ocv@3/index.web.js";
182
+
183
+ // Wait until the opencv.js script tag is present, then for the runtime.
184
+ await new Promise((resolve) => {
185
+ const ready = () => globalThis.cv && globalThis.cv.Mat;
186
+ const tick = () => (ready() ? resolve() : setTimeout(tick, 30));
187
+ tick();
188
+ });
179
189
  await ImageProcessor.initRuntime();
180
190
 
181
191
  const response = await fetch("/my-image.jpg");
@@ -192,6 +202,9 @@ processor.destroy();
192
202
  </script>
193
203
  ```
194
204
 
205
+ > With a bundler, importing `@techstark/opencv-js` once (it self-registers on
206
+ > `globalThis.cv`) is enough — no script tag needed.
207
+
195
208
  > **Note:** ES modules require HTTP/HTTPS — use a local server (`npx serve .`) for dev, or deploy to GitHub Pages.
196
209
 
197
210
  See the [interactive demo](./index.html) for a full working example.
@@ -231,21 +244,22 @@ setPlatform(myPlatform);
231
244
 
232
245
  To avoid bloat, we only ship essential operations for chaining. Currently shipped operations are:
233
246
 
234
- | Operation | Depends on… | Why |
235
- | ------------------------- | ------------------------------------------- | --------------------------------------------------------------- |
236
- | **grayscale** | – | Converts to single‐channel; many ops expect a gray image first. |
237
- | **blur** | _(ideally after)_ grayscale | Noise reduction works best on 1-channel data. |
238
- | **threshold** | _(after)_ grayscale | Produces a binary image; needs gray levels. |
239
- | **adaptiveThreshold** | _(after)_ grayscale (and optionally blur) | Local thresholding on gray values (smoother if blurred first). |
240
- | **invert** | _(after)_ threshold or adaptiveThreshold | Inverting a binary mask flips foreground/background. |
241
- | **canny** | _(after)_ grayscale + blur | Edge detection expects a smoothed gray image. |
242
- | **dilate** | _(after)_ threshold or edge detection | Expands foreground regions—usually on a binary mask. |
243
- | **erode** | _(after)_ threshold or edge detection | Shrinks or cleans up binary regions. |
244
- | **morphologicalGradient** | _(after)_ dilation + erosion (or threshold) | Highlights boundaries by subtracting eroded from dilated image. |
245
- | **warp** | | Geometric transform; can be applied at any point. |
246
- | **resize** | – | Also independent; purely geometry. |
247
- | **border** | – | Independent; purely geometry. |
248
- | **rotate** | – | Independent. |
247
+ | Operation | Depends on… | Why |
248
+ | ------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------- |
249
+ | **grayscale** | – | Converts to single‐channel; many ops expect a gray image first. |
250
+ | **blur** | _(ideally after)_ grayscale | Noise reduction works best on 1-channel data. |
251
+ | **equalize** | _(after)_ grayscale | Histogram equalisation (CLAHE or global) for contrast normalisation before thresholding. |
252
+ | **threshold** | _(after)_ grayscale | Produces a binary image; needs gray levels. |
253
+ | **adaptiveThreshold** | _(after)_ grayscale (and optionally blur) | Local thresholding on gray values (smoother if blurred first). |
254
+ | **invert** | _(after)_ threshold or adaptiveThreshold | Inverting a binary mask flips foreground/background. |
255
+ | **canny** | _(after)_ grayscale + blur | Edge detection expects a smoothed gray image. |
256
+ | **dilate** | _(after)_ threshold or edge detection | Expands foreground regions—usually on a binary mask. |
257
+ | **erode** | _(after)_ threshold or edge detection | Shrinks or cleans up binary regions. |
258
+ | **morphologicalGradient** | _(after)_ dilation + erosion (or threshold) | Highlights boundaries by subtracting eroded from dilated image. |
259
+ | **warp** | – | Geometric transform; can be applied at any point. |
260
+ | **resize** | – | Also independent; purely geometry. |
261
+ | **border** | – | Independent; purely geometry. |
262
+ | **rotate** | – | Independent. |
249
263
 
250
264
  ## Extending operations
251
265
 
@@ -3,7 +3,7 @@
3
3
  * Allows ppu-ocv to work with both @napi-rs/canvas (Node) and browser canvas APIs.
4
4
  */
5
5
  /** Structural type satisfied by both @napi-rs/canvas Canvas and HTMLCanvasElement/OffscreenCanvas. */
6
- export interface CanvasLike {
6
+ export type CanvasLike = {
7
7
  /** Canvas width in pixels. */
8
8
  width: number;
9
9
  /** Canvas height in pixels. */
@@ -14,9 +14,16 @@ export interface CanvasLike {
14
14
  toBuffer?: (...args: any[]) => Buffer;
15
15
  /** Serialize the canvas to a data-URL string (browser canvases). Absent on Node-side `@napi-rs/canvas`. */
16
16
  toDataURL?: (...args: any[]) => string;
17
- }
17
+ /** Asynchronously serialize to a `Blob` via callback (browser `HTMLCanvasElement`). */
18
+ toBlob?: (callback: (blob: Blob | null) => void, type?: string, quality?: number) => void;
19
+ /** Asynchronously serialize to a `Blob` (`OffscreenCanvas`, used in workers and extensions). */
20
+ convertToBlob?: (options?: {
21
+ type?: string;
22
+ quality?: number;
23
+ }) => Promise<Blob>;
24
+ };
18
25
  /** Structural type for 2D rendering context, matching the cross-runtime subset used by ppu-ocv. */
19
- export interface Context2DLike {
26
+ export type Context2DLike = {
20
27
  /** The canvas this context is bound to. */
21
28
  canvas: any;
22
29
  /** Draw an image, canvas, or bitmap onto the context. Signature follows the standard Canvas2D `drawImage`. */
@@ -59,17 +66,28 @@ export interface Context2DLike {
59
66
  translate(x: number, y: number): void;
60
67
  /** Rotate the coordinate system clockwise by `angle` radians. */
61
68
  rotate(angle: number): void;
62
- }
69
+ };
63
70
  /** Platform-specific canvas operations. Each runtime entry point registers an implementation via {@link setPlatform}. */
64
- export interface CanvasPlatform {
71
+ export type CanvasPlatform = {
65
72
  /** Create a blank canvas of the given width and height. */
66
73
  createCanvas(width: number, height: number): CanvasLike;
67
74
  /** Decode an image from a buffer or URL and draw it onto a fresh canvas. */
68
75
  loadImage(source: ArrayBuffer | string): Promise<CanvasLike>;
69
76
  /** Type guard for "is this value a canvas of this platform?". */
70
77
  isCanvas(value: unknown): value is CanvasLike;
71
- }
78
+ };
72
79
  /** Register the platform-specific canvas implementation */
73
80
  export declare function setPlatform(platform: CanvasPlatform): void;
74
81
  /** Get the registered platform. Throws if none has been set. */
75
82
  export declare function getPlatform(): CanvasPlatform;
83
+ /**
84
+ * Structural ("duck-typed") canvas check, independent of the registered
85
+ * platform. A value is canvas-like if it exposes `width`/`height` numbers and a
86
+ * `getContext` function — true for both `@napi-rs/canvas` (Node) and browser
87
+ * `HTMLCanvasElement`/`OffscreenCanvas`.
88
+ *
89
+ * Unlike {@link CanvasPlatform.isCanvas}, this does not depend on which platform
90
+ * is globally registered, so it stays correct when the Node and web entry
91
+ * points are loaded in the same process (e.g. a dual-target test suite).
92
+ */
93
+ export declare function isCanvasLike(value: unknown): value is CanvasLike;
package/canvas-factory.js CHANGED
@@ -1 +1 @@
1
- let _platform=null;export function setPlatform(platform){_platform=platform}export function getPlatform(){if(!_platform){throw new Error("No canvas platform registered. "+'Import "ppu-ocv" (Node), "ppu-ocv/web" (browser), '+'"ppu-ocv/canvas" (Node canvas-only), or "ppu-ocv/canvas-web" (browser canvas-only) to auto-register.')}return _platform}
1
+ let _platform=null;export function setPlatform(platform){_platform=platform}export function getPlatform(){if(!_platform){throw new Error("No canvas platform registered. "+'Import "ppu-ocv" (Node), "ppu-ocv/web" (browser), '+'"ppu-ocv/canvas" (Node canvas-only), or "ppu-ocv/canvas-web" (browser canvas-only) to auto-register.')}return _platform}export function isCanvasLike(value){return typeof value==="object"&&value!==null&&typeof value.getContext==="function"&&typeof value.width==="number"&&typeof value.height==="number"}
package/canvas-io.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Canvas <-> ArrayBuffer conversion helpers, shared by `CanvasProcessor`'s
3
+ * static `prepareCanvas` / `prepareBuffer`. Kept separate so the processor
4
+ * class stays focused on the pixel pipeline.
5
+ */
6
+ import type { CanvasLike } from "./canvas-factory.js";
7
+ /**
8
+ * Convert an ArrayBuffer (image file bytes) to a CanvasLike. If the value is
9
+ * already a CanvasLike it is returned as-is.
10
+ */
11
+ export declare function bufferToCanvas(file: ArrayBuffer): Promise<CanvasLike>;
12
+ /**
13
+ * Convert a CanvasLike to an ArrayBuffer (PNG bytes). If the value is already
14
+ * an ArrayBuffer it is returned as-is.
15
+ */
16
+ export declare function canvasToBuffer(canvas: CanvasLike): Promise<ArrayBuffer>;
package/canvas-io.js ADDED
@@ -0,0 +1 @@
1
+ import{getPlatform,isCanvasLike}from"./canvas-factory.js";export async function bufferToCanvas(file){if(isCanvasLike(file))return file;return getPlatform().loadImage(file)}export async function canvasToBuffer(canvas){if(canvas instanceof ArrayBuffer)return canvas;if(typeof canvas.toBuffer==="function"){let buffer=canvas.toBuffer("image/png");let arrayBuffer=new ArrayBuffer(buffer.byteLength);new Uint8Array(arrayBuffer).set(new Uint8Array(buffer));return arrayBuffer}let toBlob=canvas.toBlob;if(typeof toBlob==="function"){let blob=await new Promise((resolve,reject)=>{toBlob.call(canvas,(b)=>b?resolve(b):reject(new Error("toBlob returned null")),"image/png")});return blob.arrayBuffer()}if(typeof canvas.convertToBlob==="function"){let blob=await canvas.convertToBlob({type:"image/png"});return blob.arrayBuffer()}if(typeof canvas.toDataURL==="function"){let dataURL=canvas.toDataURL("image/png");let base64Data=dataURL.replace(/^data:image\/png;base64,/,"");let binaryString=atob(base64Data);let arrayBuffer=new ArrayBuffer(binaryString.length);let bytes=new Uint8Array(arrayBuffer);for(let i=0;i<binaryString.length;i++){bytes[i]=binaryString.charCodeAt(i)}return arrayBuffer}let ctx=canvas.getContext("2d");let imageData=ctx.getImageData(0,0,canvas.width,canvas.height);let canvasBuffer=new ArrayBuffer(imageData.data.byteLength);new Uint8Array(canvasBuffer).set(new Uint8Array(imageData.data.buffer,imageData.data.byteOffset,imageData.data.byteLength));return canvasBuffer}
@@ -1,14 +1,6 @@
1
- import type { BoundingBox } from "./index.interface.js";
2
1
  import type { CanvasLike } from "./canvas-factory.js";
3
- /**
4
- * A detected region returned by {@link CanvasProcessor.findRegions}.
5
- */
6
- export interface DetectedRegion {
7
- /** Axis-aligned bounding box of the region (x1/y1 are exclusive). */
8
- bbox: BoundingBox;
9
- /** Number of foreground pixels in the region. */
10
- area: number;
11
- }
2
+ import type { DetectedRegion, FindRegionsOptions } from "./canvas-regions.js";
3
+ export type { DetectedRegion } from "./canvas-regions.js";
12
4
  /**
13
5
  * Canvas-native image processing with no OpenCV dependency.
14
6
  *
@@ -175,17 +167,7 @@ export declare class CanvasProcessor {
175
167
  * });
176
168
  * ```
177
169
  */
178
- findRegions(options?: {
179
- foreground?: "light" | "dark";
180
- thresh?: number;
181
- minArea?: number;
182
- maxArea?: number;
183
- padding?: {
184
- vertical?: number;
185
- horizontal?: number;
186
- };
187
- scale?: number;
188
- }): DetectedRegion[];
170
+ findRegions(options?: FindRegionsOptions): DetectedRegion[];
189
171
  /**
190
172
  * Return the current canvas state.
191
173
  */
@@ -1 +1 @@
1
- export class CanvasProcessor{_canvas;constructor(source){this._canvas=source}get width(){return this._canvas.width}get height(){return this._canvas.height}resize(options){const{width,height}=options;let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").drawImage(this._canvas,0,0,width,height);this._canvas=dst;return this}grayscale(){const{width,height}=this._canvas;let imageData=this._canvas.getContext("2d").getImageData(0,0,width,height);let d=imageData.data;for(let i=0;i<d.length;i+=4){let luma=Math.round(0.299*(d[i]??0)+0.587*(d[i+1]??0)+0.114*(d[i+2]??0));d[i]=luma;d[i+1]=luma;d[i+2]=luma}let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").putImageData(imageData,0,0);this._canvas=dst;return this}convert(options={}){const{alpha=1,beta=0}=options;if(alpha===1&&beta===0)return this;const{width,height}=this._canvas;let imageData=this._canvas.getContext("2d").getImageData(0,0,width,height);let d=imageData.data;for(let i=0;i<d.length;i+=4){d[i]=Math.round((d[i]??0)*alpha+beta);d[i+1]=Math.round((d[i+1]??0)*alpha+beta);d[i+2]=Math.round((d[i+2]??0)*alpha+beta)}let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").putImageData(imageData,0,0);this._canvas=dst;return this}invert(){const{width,height}=this._canvas;let imageData=this._canvas.getContext("2d").getImageData(0,0,width,height);let d=imageData.data;for(let i=0;i<d.length;i+=4){d[i]=255-(d[i]??0);d[i+1]=255-(d[i+1]??0);d[i+2]=255-(d[i+2]??0)}let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").putImageData(imageData,0,0);this._canvas=dst;return this}threshold(options={}){const{thresh=127,maxValue=255}=options;const{width,height}=this._canvas;let imageData=this._canvas.getContext("2d").getImageData(0,0,width,height);let d=imageData.data;for(let i=0;i<d.length;i+=4){let luma=d[i]===d[i+1]&&d[i+1]===d[i+2]?d[i]??0:Math.round(0.299*(d[i]??0)+0.587*(d[i+1]??0)+0.114*(d[i+2]??0));let val=luma>thresh?maxValue:0;d[i]=val;d[i+1]=val;d[i+2]=val}let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").putImageData(imageData,0,0);this._canvas=dst;return this}border(options={}){const{size=10,color="white"}=options;const{width,height}=this._canvas;let dst=getPlatform().createCanvas(width+size*2,height+size*2);let ctx=dst.getContext("2d");ctx.fillStyle=color;ctx.fillRect(0,0,dst.width,dst.height);ctx.drawImage(this._canvas,size,size);this._canvas=dst;return this}rotate(options){const{angle,cx=this._canvas.width/2,cy=this._canvas.height/2}=options;if(angle===0)return this;const{width,height}=this._canvas;let dst=getPlatform().createCanvas(width,height);let ctx=dst.getContext("2d");ctx.save();ctx.translate(cx,cy);ctx.rotate(-angle*Math.PI/180);ctx.drawImage(this._canvas,-cx,-cy);ctx.restore();this._canvas=dst;return this}findRegions(options={}){const{foreground="light",thresh=127,minArea=1,maxArea=1/0,padding,scale=1}=options;const{width,height}=this._canvas;let data=this._canvas.getContext("2d").getImageData(0,0,width,height).data;let visited=new Uint8Array(width*height);let regions=[];let neighbours=[[-1,-1],[0,-1],[1,-1],[-1,0],[1,0],[-1,1],[0,1],[1,1]];let isForeground=(pixelIdx)=>{let r=data[pixelIdx]??0;return foreground==="light"?r>thresh:r<=thresh};for(let startY=0;startY<height;startY++){for(let startX=0;startX<width;startX++){let startFlat=startY*width+startX;if(visited[startFlat])continue;visited[startFlat]=1;if(!isForeground(startFlat*4))continue;let stack=[startFlat];let minX=startX,maxX=startX;let minY=startY,maxY=startY;let area=0;while(stack.length>0){let flat=stack.pop();if(flat===undefined)break;area++;let x=flat%width;let y=(flat-x)/width;if(x<minX)minX=x;else if(x>maxX)maxX=x;if(y<minY)minY=y;else if(y>maxY)maxY=y;for(const[dx,dy]of neighbours){let nx=x+dx;let ny=y+dy;if(nx<0||nx>=width||ny<0||ny>=height)continue;let nFlat=ny*width+nx;if(visited[nFlat])continue;visited[nFlat]=1;if(isForeground(nFlat*4))stack.push(nFlat)}}if(area>=minArea&&area<=maxArea){let x0=minX;let y0=minY;let x1=maxX+1;let y1=maxY+1;if(padding){let bboxH=y1-y0;let vPad=Math.round(bboxH*(padding.vertical??0));let hPad=Math.round(bboxH*(padding.horizontal??0));x0=Math.max(0,x0-hPad);y0=Math.max(0,y0-vPad);x1=Math.min(width,x1+hPad);y1=Math.min(height,y1+vPad)}if(scale!==1){x0=Math.max(0,Math.round(x0*scale));y0=Math.max(0,Math.round(y0*scale));x1=Math.round(x1*scale);y1=Math.round(y1*scale)}regions.push({bbox:{x0,y0,x1,y1},area})}}}return regions}toCanvas(){return this._canvas}static async prepareCanvas(file){if(getPlatform().isCanvas(file))return file;return getPlatform().loadImage(file)}static async prepareBuffer(canvas){if(canvas instanceof ArrayBuffer)return canvas;if(typeof canvas.toBuffer==="function"){let buffer=canvas.toBuffer("image/png");let arrayBuffer=new ArrayBuffer(buffer.byteLength);new Uint8Array(arrayBuffer).set(new Uint8Array(buffer));return arrayBuffer}if(typeof canvas.toDataURL==="function"){let dataURL=canvas.toDataURL("image/png");let base64Data=dataURL.replace(/^data:image\/png;base64,/,"");let binaryString=atob(base64Data);let arrayBuffer=new ArrayBuffer(binaryString.length);let bytes=new Uint8Array(arrayBuffer);for(let i=0;i<binaryString.length;i++){bytes[i]=binaryString.charCodeAt(i)}return arrayBuffer}let ctx=canvas.getContext("2d");let imageData=ctx.getImageData(0,0,canvas.width,canvas.height);let canvasBuffer=new ArrayBuffer(imageData.data.byteLength);new Uint8Array(canvasBuffer).set(new Uint8Array(imageData.data.buffer,imageData.data.byteOffset,imageData.data.byteLength));return canvasBuffer}}import{getPlatform}from"./canvas-factory.js";
1
+ export class CanvasProcessor{_canvas;constructor(source){this._canvas=source}get width(){return this._canvas.width}get height(){return this._canvas.height}resize(options){const{width,height}=options;let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").drawImage(this._canvas,0,0,width,height);this._canvas=dst;return this}grayscale(){const{width,height}=this._canvas;let imageData=this._canvas.getContext("2d").getImageData(0,0,width,height);let d=imageData.data;for(let i=0;i<d.length;i+=4){let luma=Math.round(0.299*(d[i]??0)+0.587*(d[i+1]??0)+0.114*(d[i+2]??0));d[i]=luma;d[i+1]=luma;d[i+2]=luma}let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").putImageData(imageData,0,0);this._canvas=dst;return this}convert(options={}){const{alpha=1,beta=0}=options;if(alpha===1&&beta===0)return this;const{width,height}=this._canvas;let imageData=this._canvas.getContext("2d").getImageData(0,0,width,height);let d=imageData.data;for(let i=0;i<d.length;i+=4){d[i]=Math.round((d[i]??0)*alpha+beta);d[i+1]=Math.round((d[i+1]??0)*alpha+beta);d[i+2]=Math.round((d[i+2]??0)*alpha+beta)}let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").putImageData(imageData,0,0);this._canvas=dst;return this}invert(){const{width,height}=this._canvas;let imageData=this._canvas.getContext("2d").getImageData(0,0,width,height);let d=imageData.data;for(let i=0;i<d.length;i+=4){d[i]=255-(d[i]??0);d[i+1]=255-(d[i+1]??0);d[i+2]=255-(d[i+2]??0)}let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").putImageData(imageData,0,0);this._canvas=dst;return this}threshold(options={}){const{thresh=127,maxValue=255}=options;const{width,height}=this._canvas;let imageData=this._canvas.getContext("2d").getImageData(0,0,width,height);let d=imageData.data;for(let i=0;i<d.length;i+=4){let luma=d[i]===d[i+1]&&d[i+1]===d[i+2]?d[i]??0:Math.round(0.299*(d[i]??0)+0.587*(d[i+1]??0)+0.114*(d[i+2]??0));let val=luma>thresh?maxValue:0;d[i]=val;d[i+1]=val;d[i+2]=val}let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").putImageData(imageData,0,0);this._canvas=dst;return this}border(options={}){const{size=10,color="white"}=options;const{width,height}=this._canvas;let dst=getPlatform().createCanvas(width+size*2,height+size*2);let ctx=dst.getContext("2d");ctx.fillStyle=color;ctx.fillRect(0,0,dst.width,dst.height);ctx.drawImage(this._canvas,size,size);this._canvas=dst;return this}rotate(options){const{angle,cx=this._canvas.width/2,cy=this._canvas.height/2}=options;if(angle===0)return this;const{width,height}=this._canvas;let dst=getPlatform().createCanvas(width,height);let ctx=dst.getContext("2d");ctx.save();ctx.translate(cx,cy);ctx.rotate(-angle*Math.PI/180);ctx.drawImage(this._canvas,-cx,-cy);ctx.restore();this._canvas=dst;return this}findRegions(options={}){const{width,height}=this._canvas;let data=this._canvas.getContext("2d").getImageData(0,0,width,height).data;return detectRegions(data,width,height,options)}toCanvas(){return this._canvas}static async prepareCanvas(file){return bufferToCanvas(file)}static async prepareBuffer(canvas){return canvasToBuffer(canvas)}}import{getPlatform}from"./canvas-factory.js";import{bufferToCanvas,canvasToBuffer}from"./canvas-io.js";import{detectRegions}from"./canvas-regions.js";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Canvas-native region detection: 8-connected flood-fill bounding-box
3
+ * extraction on a binary image's raw RGBA pixels. Extracted from
4
+ * `CanvasProcessor.findRegions` so the processor stays a thin wrapper over this
5
+ * pure algorithm.
6
+ */
7
+ import type { BoundingBox } from "./index.interface.js";
8
+ /** A detected region: its bounding box and foreground-pixel count. */
9
+ export type DetectedRegion = {
10
+ /** Axis-aligned bounding box of the region (x1/y1 are exclusive). */
11
+ bbox: BoundingBox;
12
+ /** Number of foreground pixels in the region. */
13
+ area: number;
14
+ };
15
+ /** Options for {@link detectRegions} / `CanvasProcessor.findRegions`. */
16
+ export type FindRegionsOptions = {
17
+ /** Which pixels count as foreground relative to `thresh`. @default "light" */
18
+ foreground?: "light" | "dark";
19
+ /** Grayscale threshold separating foreground from background. @default 127 */
20
+ thresh?: number;
21
+ /** Discard regions with fewer than this many pixels. @default 1 */
22
+ minArea?: number;
23
+ /** Discard regions with more than this many pixels. @default Infinity */
24
+ maxArea?: number;
25
+ /** Padding per box as a fraction of its height (vertical/horizontal). */
26
+ padding?: {
27
+ vertical?: number;
28
+ horizontal?: number;
29
+ };
30
+ /** Multiply all bbox coordinates by this factor after padding. @default 1 */
31
+ scale?: number;
32
+ };
33
+ /**
34
+ * Detect foreground regions in raw RGBA pixel data via 8-connected flood fill.
35
+ *
36
+ * @param data - RGBA pixel data (`width * height * 4` bytes).
37
+ * @param width - Image width in pixels.
38
+ * @param height - Image height in pixels.
39
+ * @param options - See {@link FindRegionsOptions}.
40
+ */
41
+ export declare function detectRegions(data: Uint8ClampedArray, width: number, height: number, options?: FindRegionsOptions): DetectedRegion[];
@@ -0,0 +1 @@
1
+ export function detectRegions(data,width,height,options={}){const{foreground="light",thresh=127,minArea=1,maxArea=1/0,padding,scale=1}=options;let visited=new Uint8Array(width*height);let regions=[];let neighbours=[[-1,-1],[0,-1],[1,-1],[-1,0],[1,0],[-1,1],[0,1],[1,1]];let isForeground=(pixelIdx)=>{let r=data[pixelIdx]??0;return foreground==="light"?r>thresh:r<=thresh};for(let startY=0;startY<height;startY++){for(let startX=0;startX<width;startX++){let startFlat=startY*width+startX;if(visited[startFlat])continue;visited[startFlat]=1;if(!isForeground(startFlat*4))continue;let stack=[startFlat];let minX=startX,maxX=startX;let minY=startY,maxY=startY;let area=0;while(stack.length>0){let flat=stack.pop();if(flat===undefined)break;area++;let x=flat%width;let y=(flat-x)/width;if(x<minX)minX=x;else if(x>maxX)maxX=x;if(y<minY)minY=y;else if(y>maxY)maxY=y;for(const[dx,dy]of neighbours){let nx=x+dx;let ny=y+dy;if(nx<0||nx>=width||ny<0||ny>=height)continue;let nFlat=ny*width+nx;if(visited[nFlat])continue;visited[nFlat]=1;if(isForeground(nFlat*4))stack.push(nFlat)}}if(area>=minArea&&area<=maxArea){let x0=minX;let y0=minY;let x1=maxX+1;let y1=maxY+1;if(padding){let bboxH=y1-y0;let vPad=Math.round(bboxH*(padding.vertical??0));let hPad=Math.round(bboxH*(padding.horizontal??0));x0=Math.max(0,x0-hPad);y0=Math.max(0,y0-vPad);x1=Math.min(width,x1+hPad);y1=Math.min(height,y1+vPad)}if(scale!==1){x0=Math.max(0,Math.round(x0*scale));y0=Math.max(0,Math.round(y0*scale));x1=Math.round(x1*scale);y1=Math.round(y1*scale)}regions.push({bbox:{x0,y0,x1,y1},area})}}}return regions}
@@ -1,10 +1,10 @@
1
1
  import type { BoundingBox } from "./index.interface.js";
2
2
  import type { CanvasLike, Context2DLike } from "./canvas-factory.js";
3
- /** Structural interface for contour-like objects with 32-bit signed integer point data. */
4
- export interface ContourLike {
3
+ /** Structural type for contour-like objects with 32-bit signed integer point data. */
4
+ export type ContourLike = {
5
5
  /** Flat `[x0, y0, x1, y1, ...]` point array. Matches `cv.Mat.data32S` for contour Mats. */
6
6
  data32S: Int32Array | number[];
7
- }
7
+ };
8
8
  /**
9
9
  * Cross-platform base class for canvas manipulation utilities.
10
10
  * Contains only methods that work in both Node and browser environments.
package/contours.d.ts CHANGED
@@ -2,12 +2,12 @@ import type { CanvasLike } from "./canvas-factory.js";
2
2
  import { cv } from "./cv-provider.js";
3
3
  import type { BoundingBox, Points } from "./index.interface.js";
4
4
  /** Options for configuring contour detection. */
5
- export interface ContoursOptions {
5
+ export type ContoursOptions = {
6
6
  /** The contour retrieval mode. (cv.RETR_...) */
7
7
  mode: cv.RetrievalModes;
8
8
  /** The contour approximation method. (cv.CHAIN_...) */
9
9
  method: cv.ContourApproximationModes;
10
- }
10
+ };
11
11
  /**
12
12
  * Wrapper around OpenCV's `findContours` that provides convenient accessors
13
13
  * for iterating, filtering, and analyzing contours in a binary image.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Skew-angle estimators used by {@link DeskewService}. Pure helpers extracted
3
+ * from `deskew.ts`: each takes detected text regions (or a binary Mat) and
4
+ * returns candidate angles with weights, plus a consensus reducer. Methods that
5
+ * log accept a `log` callback so they stay free of service state.
6
+ */
7
+ import { cv } from "./cv-provider.js";
8
+ /** A candidate skew angle with its confidence weight. */
9
+ export type WeightedAngle = {
10
+ angle: number;
11
+ weight: number;
12
+ };
13
+ /** A detected text region: its contour and basic shape metrics. */
14
+ export type TextRegion = {
15
+ contour: cv.Mat;
16
+ area: number;
17
+ aspectRatio: number;
18
+ };
19
+ /** Least-squares slope of a point set, as an angle in degrees clamped to ±45°. */
20
+ export declare function calculateLineAngle(points: Array<{
21
+ x: number;
22
+ y: number;
23
+ }>): number;
24
+ /** Angles from each region's minimum-area rectangle, weighted by area and aspect. */
25
+ export declare function calculateMinRectAngles(textRegions: TextRegion[]): WeightedAngle[];
26
+ /** Angles from each region's text baseline (line fit through bottom points). */
27
+ export declare function calculateBaselineAngles(textRegions: TextRegion[]): WeightedAngle[];
28
+ /** Angles from a probabilistic Hough line transform of the morphed binary Mat. */
29
+ export declare function calculateHoughAngles(mat: cv.Mat, minAngle: number, maxAngle: number, log: (message: string) => void): WeightedAngle[];
30
+ /** Robust consensus over all candidate angles: IQR outlier rejection + weighted mean. */
31
+ export declare function calculateConsensusAngle(angles: Array<WeightedAngle & {
32
+ method: string;
33
+ }>, minAngle: number, maxAngle: number, log: (message: string) => void): number;
@@ -0,0 +1 @@
1
+ import{cv}from"./cv-provider.js";export function calculateLineAngle(points){if(points.length<2)return 0;let n=points.length;let sumX=points.reduce((sum,p)=>sum+p.x,0);let sumY=points.reduce((sum,p)=>sum+p.y,0);let sumXY=points.reduce((sum,p)=>sum+p.x*p.y,0);let sumXX=points.reduce((sum,p)=>sum+p.x*p.x,0);let denominator=n*sumXX-sumX*sumX;if(Math.abs(denominator)<0.0000000001)return 0;let slope=(n*sumXY-sumX*sumY)/denominator;let angle=Math.atan(slope)*180/Math.PI;if(angle>45)angle-=90;if(angle<-45)angle+=90;return angle}export function calculateMinRectAngles(textRegions){let angles=[];for(let region of textRegions){try{let minRect=cv.minAreaRect(region.contour);if(!minRect)continue;let angle=minRect.angle;if(angle>45){angle-=90}else if(angle<-45){angle+=90}let areaWeight=Math.log(region.area+1);let aspectWeight=Math.min(region.aspectRatio,1/region.aspectRatio)*2;let weight=areaWeight*aspectWeight;angles.push({angle,weight})}catch{continue}}return angles}export function calculateBaselineAngles(textRegions){let angles=[];for(let region of textRegions){try{let points=region.contour.data32S;if(!points||points.length<8)continue;let bottomPoints=[];for(let i=0;i<points.length;i+=2){let x=points[i];let y=points[i+1];if(x!==undefined&&y!==undefined){bottomPoints.push({x,y})}}if(bottomPoints.length<3)continue;bottomPoints.sort((a,b)=>a.x-b.x);let segments=3;let segmentSize=Math.floor(bottomPoints.length/segments);let baselinePoints=[];for(let seg=0;seg<segments;seg++){let start=seg*segmentSize;let end=seg===segments-1?bottomPoints.length:(seg+1)*segmentSize;let segmentPoints=bottomPoints.slice(start,end);if(segmentPoints.length>0){let maxYPoint=segmentPoints.reduce((max,point)=>point.y>max.y?point:max);baselinePoints.push(maxYPoint)}}if(baselinePoints.length>=2){let angle=calculateLineAngle(baselinePoints);let weight=region.area*Math.min(region.aspectRatio,1/region.aspectRatio);angles.push({angle,weight})}}catch{continue}}return angles}export function calculateHoughAngles(mat,minAngle,maxAngle,log){let angles=[];try{let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(3,1));let morphed=new cv.Mat;cv.morphologyEx(mat,morphed,cv.MORPH_CLOSE,kernel);let lines=new cv.Mat;cv.HoughLinesP(morphed,lines,1,Math.PI/180,30,50,10);for(let i=0;i<lines.rows;i++){let line=lines.data32S.subarray(i*4,(i+1)*4);const[x1,y1,x2,y2]=line;if(x1!==undefined&&y1!==undefined&&x2!==undefined&&y2!==undefined){let dx=x2-x1;let dy=y2-y1;if(Math.abs(dx)>1){let angle=Math.atan2(dy,dx)*180/Math.PI;if(angle>45)angle-=90;if(angle<-45)angle+=90;if(angle>=minAngle&&angle<=maxAngle){let lineLength=Math.sqrt(dx*dx+dy*dy);angles.push({angle,weight:lineLength})}}}}morphed.delete();lines.delete();kernel.delete()}catch{log("Hough transform failed, skipping this method.")}return angles}export function calculateConsensusAngle(angles,minAngle,maxAngle,log){if(angles.length===0)return 0;let sortedAngles=[...angles].sort((a,b)=>a.angle-b.angle);let q1Index=Math.floor(sortedAngles.length*0.25);let q3Index=Math.floor(sortedAngles.length*0.75);let q1=sortedAngles[q1Index]?.angle||0;let q3=sortedAngles[q3Index]?.angle||0;let iqr=q3-q1;let lowerBound=q1-1.5*iqr;let upperBound=q3+1.5*iqr;let filteredAngles=angles.filter((a)=>a.angle>=lowerBound&&a.angle<=upperBound&&a.angle>=minAngle&&a.angle<=maxAngle);if(filteredAngles.length===0){log("All angles filtered out as outliers, using median of original set.");let medianIndex=Math.floor(sortedAngles.length/2);return sortedAngles[medianIndex]?.angle||0}let totalWeight=filteredAngles.reduce((sum,a)=>sum+a.weight,0);if(totalWeight===0){let average=filteredAngles.reduce((sum,a)=>sum+a.angle,0)/filteredAngles.length;return average}let weightedSum=filteredAngles.reduce((sum,a)=>sum+a.angle*a.weight,0);let weightedAverage=weightedSum/totalWeight;let methodCounts=filteredAngles.reduce((counts,a)=>{counts[a.method]=(counts[a.method]||0)+1;return counts},{});log(`Angle methods used: ${Object.entries(methodCounts).map(([method,count])=>`${method}:${count}`).join(", ")}`);return Math.max(minAngle,Math.min(maxAngle,weightedAverage))}
package/deskew.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { CanvasLike } from "./canvas-factory.js";
2
2
  /**
3
3
  * Options for configuring the deskew service
4
4
  */
5
- export interface DeskewOptions {
5
+ export type DeskewOptions = {
6
6
  /**
7
7
  * Enable detailed logging of each processing step.
8
8
  * @default false
@@ -14,7 +14,7 @@ export interface DeskewOptions {
14
14
  * @default 20
15
15
  */
16
16
  minimumAreaThreshold?: number;
17
- }
17
+ };
18
18
  /**
19
19
  * Service for calculating the skew angle of an image containing text.
20
20
  *
@@ -64,9 +64,4 @@ export declare class DeskewService {
64
64
  * @returns A new canvas with the deskewed image
65
65
  */
66
66
  deskewImage(canvas: CanvasLike): Promise<CanvasLike>;
67
- private calculateMinRectAngles;
68
- private calculateBaselineAngles;
69
- private calculateHoughAngles;
70
- private calculateLineAngle;
71
- private calculateConsensusAngle;
72
67
  }
package/deskew.js CHANGED
@@ -1 +1 @@
1
- export class DeskewService{verbose;minimumAreaThreshold;constructor(options={}){this.verbose=options.verbose??false;this.minimumAreaThreshold=options.minimumAreaThreshold??20}log(message){if(this.verbose){console.log(`[DeskewService] ${message}`)}}async calculateSkewAngle(canvas){let processor=new ImageProcessor(canvas);let mat=processor.grayscale().threshold({lower:0,upper:255,type:cv.THRESH_BINARY+cv.THRESH_OTSU}).toMat();let contours=new Contours(mat,{mode:cv.RETR_LIST,method:cv.CHAIN_APPROX_SIMPLE});processor.destroy();let minAngle=-20;let maxAngle=20;let minArea=this.minimumAreaThreshold;let textRegions=[];contours.iterate((contour)=>{let rect=contours.getRect(contour);let area=rect.width*rect.height;if(area<minArea)return;let aspectRatio=rect.width/rect.height;if(aspectRatio>0.2&&aspectRatio<10){textRegions.push({rect,contour,area,aspectRatio})}});if(textRegions.length===0){this.log("No valid text regions found for skew calculation.");contours.destroy();return 0}let averageHeight=textRegions.reduce((sum,region)=>sum+region.rect.height,0)/textRegions.length;let filteredRegions=textRegions.filter((region)=>{return region.rect.height<=averageHeight*1.5});this.log(`Found ${filteredRegions.length} text regions for skew analysis.`);let minRectAngles=this.calculateMinRectAngles(filteredRegions,contours);let baselineAngles=this.calculateBaselineAngles(filteredRegions);let houghAngles=this.calculateHoughAngles(mat,minAngle,maxAngle);contours.destroy();let allAngles=[...minRectAngles.map((a)=>({...a,method:"minRect"})),...baselineAngles.map((a)=>({...a,method:"baseline"})),...houghAngles.map((a)=>({...a,method:"hough"}))];if(allAngles.length===0){this.log("No angles detected from any method.");return 0}let consensusAngle=this.calculateConsensusAngle(allAngles,minAngle,maxAngle);this.log(`Calculated skew angle: ${consensusAngle.toFixed(3)}° (from ${allAngles.length} measurements)`);return consensusAngle}async deskewImage(canvas){this.log("Starting image deskewing process");let angle=await this.calculateSkewAngle(canvas);this.log(`Detected skew angle: ${angle.toFixed(2)}°. Rotating image by ${-angle.toFixed(2)}°...`);let processor=new ImageProcessor(canvas);try{let rotatedCanvas=processor.rotate({angle}).toCanvas();return rotatedCanvas}finally{processor.destroy()}}calculateMinRectAngles(textRegions,_contours){let angles=[];for(let region of textRegions){try{let minRect=cv.minAreaRect(region.contour);if(!minRect)continue;let angle=minRect.angle;if(angle>45){angle-=90}else if(angle<-45){angle+=90}let areaWeight=Math.log(region.area+1);let aspectWeight=Math.min(region.aspectRatio,1/region.aspectRatio)*2;let weight=areaWeight*aspectWeight;angles.push({angle,weight})}catch{continue}}return angles}calculateBaselineAngles(textRegions){let angles=[];for(let region of textRegions){try{let points=region.contour.data32S;if(!points||points.length<8)continue;let bottomPoints=[];for(let i=0;i<points.length;i+=2){let x=points[i];let y=points[i+1];if(x!==undefined&&y!==undefined){bottomPoints.push({x,y})}}if(bottomPoints.length<3)continue;bottomPoints.sort((a,b)=>a.x-b.x);let segments=3;let segmentSize=Math.floor(bottomPoints.length/segments);let baselinePoints=[];for(let seg=0;seg<segments;seg++){let start=seg*segmentSize;let end=seg===segments-1?bottomPoints.length:(seg+1)*segmentSize;let segmentPoints=bottomPoints.slice(start,end);if(segmentPoints.length>0){let maxYPoint=segmentPoints.reduce((max,point)=>point.y>max.y?point:max);baselinePoints.push(maxYPoint)}}if(baselinePoints.length>=2){let angle=this.calculateLineAngle(baselinePoints);let weight=region.area*Math.min(region.aspectRatio,1/region.aspectRatio);angles.push({angle,weight})}}catch{continue}}return angles}calculateHoughAngles(mat,minAngle,maxAngle){let angles=[];try{let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(3,1));let morphed=new cv.Mat;cv.morphologyEx(mat,morphed,cv.MORPH_CLOSE,kernel);let lines=new cv.Mat;cv.HoughLinesP(morphed,lines,1,Math.PI/180,30,50,10);for(let i=0;i<lines.rows;i++){let line=lines.data32S.subarray(i*4,(i+1)*4);const[x1,y1,x2,y2]=line;if(x1!==undefined&&y1!==undefined&&x2!==undefined&&y2!==undefined){let dx=x2-x1;let dy=y2-y1;if(Math.abs(dx)>1){let angle=Math.atan2(dy,dx)*180/Math.PI;if(angle>45)angle-=90;if(angle<-45)angle+=90;if(angle>=minAngle&&angle<=maxAngle){let lineLength=Math.sqrt(dx*dx+dy*dy);angles.push({angle,weight:lineLength})}}}}morphed.delete();lines.delete();kernel.delete()}catch{this.log("Hough transform failed, skipping this method.")}return angles}calculateLineAngle(points){if(points.length<2)return 0;let n=points.length;let sumX=points.reduce((sum,p)=>sum+p.x,0);let sumY=points.reduce((sum,p)=>sum+p.y,0);let sumXY=points.reduce((sum,p)=>sum+p.x*p.y,0);let sumXX=points.reduce((sum,p)=>sum+p.x*p.x,0);let denominator=n*sumXX-sumX*sumX;if(Math.abs(denominator)<0.0000000001)return 0;let slope=(n*sumXY-sumX*sumY)/denominator;let angle=Math.atan(slope)*180/Math.PI;if(angle>45)angle-=90;if(angle<-45)angle+=90;return angle}calculateConsensusAngle(angles,minAngle,maxAngle){if(angles.length===0)return 0;let sortedAngles=[...angles].sort((a,b)=>a.angle-b.angle);let q1Index=Math.floor(sortedAngles.length*0.25);let q3Index=Math.floor(sortedAngles.length*0.75);let q1=sortedAngles[q1Index]?.angle||0;let q3=sortedAngles[q3Index]?.angle||0;let iqr=q3-q1;let lowerBound=q1-1.5*iqr;let upperBound=q3+1.5*iqr;let filteredAngles=angles.filter((a)=>a.angle>=lowerBound&&a.angle<=upperBound&&a.angle>=minAngle&&a.angle<=maxAngle);if(filteredAngles.length===0){this.log("All angles filtered out as outliers, using median of original set.");let medianIndex=Math.floor(sortedAngles.length/2);return sortedAngles[medianIndex]?.angle||0}let totalWeight=filteredAngles.reduce((sum,a)=>sum+a.weight,0);if(totalWeight===0){let average=filteredAngles.reduce((sum,a)=>sum+a.angle,0)/filteredAngles.length;return average}let weightedSum=filteredAngles.reduce((sum,a)=>sum+a.angle*a.weight,0);let weightedAverage=weightedSum/totalWeight;let methodCounts=filteredAngles.reduce((counts,a)=>{counts[a.method]=(counts[a.method]||0)+1;return counts},{});this.log(`Angle methods used: ${Object.entries(methodCounts).map(([method,count])=>`${method}:${count}`).join(", ")}`);return Math.max(minAngle,Math.min(maxAngle,weightedAverage))}}import{Contours}from"./contours.js";import{cv}from"./cv-provider.js";import{ImageProcessor}from"./image-processor.js";
1
+ export class DeskewService{verbose;minimumAreaThreshold;constructor(options={}){this.verbose=options.verbose??false;this.minimumAreaThreshold=options.minimumAreaThreshold??20}log(message){if(this.verbose){console.log(`[DeskewService] ${message}`)}}async calculateSkewAngle(canvas){let processor=new ImageProcessor(canvas);let mat=processor.grayscale().threshold({lower:0,upper:255,type:cv.THRESH_BINARY+cv.THRESH_OTSU}).toMat();let contours=new Contours(mat,{mode:cv.RETR_LIST,method:cv.CHAIN_APPROX_SIMPLE});processor.destroy();let minAngle=-20;let maxAngle=20;let minArea=this.minimumAreaThreshold;let textRegions=[];contours.iterate((contour)=>{let rect=contours.getRect(contour);let area=rect.width*rect.height;if(area<minArea)return;let aspectRatio=rect.width/rect.height;if(aspectRatio>0.2&&aspectRatio<10){textRegions.push({rect,contour,area,aspectRatio})}});if(textRegions.length===0){this.log("No valid text regions found for skew calculation.");contours.destroy();return 0}let averageHeight=textRegions.reduce((sum,region)=>sum+region.rect.height,0)/textRegions.length;let filteredRegions=textRegions.filter((region)=>{return region.rect.height<=averageHeight*1.5});this.log(`Found ${filteredRegions.length} text regions for skew analysis.`);let minRectAngles=calculateMinRectAngles(filteredRegions);let baselineAngles=calculateBaselineAngles(filteredRegions);let houghAngles=calculateHoughAngles(mat,minAngle,maxAngle,(m)=>this.log(m));contours.destroy();let allAngles=[...minRectAngles.map((a)=>({...a,method:"minRect"})),...baselineAngles.map((a)=>({...a,method:"baseline"})),...houghAngles.map((a)=>({...a,method:"hough"}))];if(allAngles.length===0){this.log("No angles detected from any method.");return 0}let consensusAngle=calculateConsensusAngle(allAngles,minAngle,maxAngle,(m)=>this.log(m));this.log(`Calculated skew angle: ${consensusAngle.toFixed(3)}° (from ${allAngles.length} measurements)`);return consensusAngle}async deskewImage(canvas){this.log("Starting image deskewing process");let angle=await this.calculateSkewAngle(canvas);this.log(`Detected skew angle: ${angle.toFixed(2)}°. Rotating image by ${-angle.toFixed(2)}°...`);let processor=new ImageProcessor(canvas);try{let rotatedCanvas=processor.rotate({angle}).toCanvas();return rotatedCanvas}finally{processor.destroy()}}}import{Contours}from"./contours.js";import{cv}from"./cv-provider.js";import{calculateBaselineAngles,calculateConsensusAngle,calculateHoughAngles,calculateMinRectAngles}from"./deskew-angles.js";import{ImageProcessor}from"./image-processor.js";
@@ -7,7 +7,7 @@ import type { CanvasLike } from "./canvas-factory.js";
7
7
  /**
8
8
  * Options for calculating mean Lab lightness.
9
9
  */
10
- export interface CalculateMeanLightnessOptions {
10
+ export type CalculateMeanLightnessOptions = {
11
11
  /** The canvas containing the image to be processed. */
12
12
  canvas: CanvasLike;
13
13
  /** The target dimensions for analysis (resizes internally). */
@@ -15,7 +15,7 @@ export interface CalculateMeanLightnessOptions {
15
15
  width: number;
16
16
  height: number;
17
17
  };
18
- }
18
+ };
19
19
  /**
20
20
  * Calculates the mean normalized lightness of an image using the L channel of the Lab color space.
21
21
  * Lightness is normalized based on the image's own maximum lightness value before averaging.
@@ -1,6 +1,6 @@
1
1
  import type { CanvasLike } from "./canvas-factory.js";
2
2
  import { cv } from "./cv-provider.js";
3
- import type { AdaptiveThresholdOptions, BlurOptions, BorderOptions, CannyOptions, ConvertOptions, DilateOptions, ErodeOptions, GrayscaleOptions, InvertOptions, MorphologicalGradientOptions, OperationName, OperationOptions, RequiredOptions, ResizeOptions, RotateOptions, ThresholdOptions, WarpOptions } from "./pipeline/index.js";
3
+ import type { AdaptiveThresholdOptions, BlurOptions, BorderOptions, CannyOptions, ConvertOptions, DilateOptions, EqualizeOptions, ErodeOptions, GrayscaleOptions, InvertOptions, MorphologicalGradientOptions, OperationName, OperationOptions, RequiredOptions, ResizeOptions, RotateOptions, ThresholdOptions, WarpOptions } from "./pipeline/index.js";
4
4
  type NameWithRequiredOptions = {
5
5
  [N in OperationName]: OperationOptions<N> extends RequiredOptions ? N : never;
6
6
  }[OperationName];
@@ -83,6 +83,12 @@ export declare class ImageProcessor {
83
83
  * @param options Optional configuration for inversion
84
84
  */
85
85
  invert(options?: Partial<InvertOptions>): this;
86
+ /**
87
+ * Equalise image contrast using histogram equalization
88
+ * @description Usage order: (after) grayscale — input must be single-channel
89
+ * @param options Equalization configuration options
90
+ */
91
+ equalize(options?: Partial<EqualizeOptions>): this;
86
92
  /**
87
93
  * Canny edge detection to detect edges in the image
88
94
  * @description Usage order: (after) grayscale + blur
@@ -1 +1 @@
1
- export class ImageProcessor{img;width;height;constructor(source){if(getPlatform().isCanvas(source)){let ctx=source.getContext("2d");let imageData=ctx.getImageData(0,0,source.width,source.height);this.img=cv.matFromImageData(imageData);this.width=source.width;this.height=source.height}else if(source instanceof cv.Mat){this.img=source;this.width=source.cols;this.height=source.rows}else{throw new Error("Invalid source type. Must be either Canvas or cv.Mat.")}}static async initRuntime(){return new Promise((res)=>{if(cv&&cv.Mat){res()}else{cv["onRuntimeInitialized"]=()=>{res()}}})}execute(operationName,options){let result=executeOperation(operationName,this.img,options);this.img=result.img;this.width=result.width;this.height=result.height;return this}grayscale(options){return this.execute("grayscale",options)}blur(options){return this.execute("blur",options)}threshold(options){return this.execute("threshold",options)}adaptiveThreshold(options){return this.execute("adaptiveThreshold",options)}invert(options){return this.execute("invert",options)}canny(options){return this.execute("canny",options)}dilate(options){return this.execute("dilate",options)}erode(options){return this.execute("erode",options)}border(options){return this.execute("border",options)}resize(options){return this.execute("resize",options)}rotate(options){return this.execute("rotate",options)}warp(options){return this.execute("warp",options)}convert(options){return this.execute("convert",options)}morphologicalGradient(options){return this.execute("morphologicalGradient",options)}toMat(){return this.img}toCanvas(){let platform=getPlatform();let canvas=platform.createCanvas(this.width,this.height);try{cv.imshow(canvas,this.img)}catch{let ctx=canvas.getContext("2d");if(!ctx)throw new Error("Could not get 2d context from canvas");let imgData=ctx.createImageData(this.width,this.height);if(this.img.channels()===1){let data=imgData.data;let gray=new Uint8Array(this.img.data);for(let i=0;i<gray.length;i++){data[i*4]=gray[i];data[i*4+1]=gray[i];data[i*4+2]=gray[i];data[i*4+3]=255}}else{imgData.data.set(new Uint8ClampedArray(this.img.data))}ctx.putImageData(imgData,0,0)}return canvas}destroy(){try{this.img.delete()}catch{}}}import{getPlatform}from"./canvas-factory.js";import{cv}from"./cv-provider.js";import{executeOperation}from"./pipeline/index.js";
1
+ export class ImageProcessor{img;width;height;constructor(source){if(isCanvasLike(source)){let ctx=source.getContext("2d");let imageData=ctx.getImageData(0,0,source.width,source.height);this.img=cv.matFromImageData(imageData);this.width=source.width;this.height=source.height}else if(source instanceof cv.Mat){this.img=source;this.width=source.cols;this.height=source.rows}else{throw new Error("Invalid source type. Must be either Canvas or cv.Mat.")}}static async initRuntime(){return new Promise((res)=>{if(cv&&cv.Mat){res()}else{cv["onRuntimeInitialized"]=()=>{res()}}})}execute(operationName,options){let result=executeOperation(operationName,this.img,options);this.img=result.img;this.width=result.width;this.height=result.height;return this}grayscale(options){return this.execute("grayscale",options)}blur(options){return this.execute("blur",options)}threshold(options){return this.execute("threshold",options)}adaptiveThreshold(options){return this.execute("adaptiveThreshold",options)}invert(options){return this.execute("invert",options)}equalize(options){return this.execute("equalize",options)}canny(options){return this.execute("canny",options)}dilate(options){return this.execute("dilate",options)}erode(options){return this.execute("erode",options)}border(options){return this.execute("border",options)}resize(options){return this.execute("resize",options)}rotate(options){return this.execute("rotate",options)}warp(options){return this.execute("warp",options)}convert(options){return this.execute("convert",options)}morphologicalGradient(options){return this.execute("morphologicalGradient",options)}toMat(){return this.img}toCanvas(){let platform=getPlatform();let canvas=platform.createCanvas(this.width,this.height);try{cv.imshow(canvas,this.img)}catch{let ctx=canvas.getContext("2d");if(!ctx)throw new Error("Could not get 2d context from canvas");let imgData=ctx.createImageData(this.width,this.height);if(this.img.channels()===1){let data=imgData.data;let gray=new Uint8Array(this.img.data);for(let i=0;i<gray.length;i++){data[i*4]=gray[i];data[i*4+1]=gray[i];data[i*4+2]=gray[i];data[i*4+3]=255}}else{imgData.data.set(new Uint8ClampedArray(this.img.data))}ctx.putImageData(imgData,0,0)}return canvas}destroy(){try{this.img.delete()}catch{}}}import{getPlatform,isCanvasLike}from"./canvas-factory.js";import{cv}from"./cv-provider.js";import{executeOperation}from"./pipeline/index.js";
package/index.d.ts CHANGED
@@ -13,4 +13,4 @@ export { Contours } from "./contours.js";
13
13
  export { calculateMeanGrayscaleValue, calculateMeanNormalizedLabLightness, type CalculateMeanLightnessOptions, } from "./image-analysis.js";
14
14
  export { ImageProcessor } from "./image-processor.js";
15
15
  export { DeskewService, type DeskewOptions } from "./deskew.js";
16
- export type { AdaptiveThresholdOptions, BlurOptions, BorderOptions, CannyOptions, DilateOptions, ErodeOptions, GrayscaleOptions, InvertOptions, MorphologicalGradientOptions, OperationFunction, OperationName, OperationOptions, OperationResult, PartialOptions, RegisteredOperations, RequiredOptions, ResizeOptions, RotateOptions, ThresholdOptions, WarpOptions, } from "./pipeline/index.js";
16
+ export type { AdaptiveThresholdOptions, BlurOptions, BorderOptions, CannyOptions, DilateOptions, EqualizeOptions, ErodeOptions, GrayscaleOptions, InvertOptions, MorphologicalGradientOptions, OperationFunction, OperationName, OperationOptions, OperationResult, PartialOptions, RegisteredOperations, RequiredOptions, ResizeOptions, RotateOptions, ThresholdOptions, WarpOptions, } from "./pipeline/index.js";
@@ -1,12 +1,12 @@
1
1
  /** A 2D point in canvas pixel coordinates. */
2
- export interface Coordinate {
2
+ export type Coordinate = {
3
3
  /** X coordinate in pixels, measured from the left edge. */
4
4
  x: number;
5
5
  /** Y coordinate in pixels, measured from the top edge. */
6
6
  y: number;
7
- }
7
+ };
8
8
  /** The four corner points of an axis-aligned rectangle, e.g. a contour bounding box. */
9
- export interface Points {
9
+ export type Points = {
10
10
  /** Top-left corner. */
11
11
  topLeft: Coordinate;
12
12
  /** Top-right corner. */
@@ -15,12 +15,12 @@ export interface Points {
15
15
  bottomLeft: Coordinate;
16
16
  /** Bottom-right corner. */
17
17
  bottomRight: Coordinate;
18
- }
18
+ };
19
19
  /**
20
20
  * An axis-aligned bounding box with exclusive `x1` / `y1`.
21
21
  * Width is `x1 - x0`; height is `y1 - y0`.
22
22
  */
23
- export interface BoundingBox {
23
+ export type BoundingBox = {
24
24
  /** Left edge, inclusive. */
25
25
  x0: number;
26
26
  /** Top edge, inclusive. */
@@ -29,4 +29,4 @@ export interface BoundingBox {
29
29
  x1: number;
30
30
  /** Bottom edge, exclusive. */
31
31
  y1: number;
32
- }
32
+ };
package/index.web.d.ts CHANGED
@@ -35,4 +35,4 @@ export { Contours } from "./contours.js";
35
35
  export { calculateMeanGrayscaleValue, calculateMeanNormalizedLabLightness, type CalculateMeanLightnessOptions, } from "./image-analysis.js";
36
36
  export { ImageProcessor } from "./image-processor.js";
37
37
  export { DeskewService, type DeskewOptions } from "./deskew.js";
38
- export type { AdaptiveThresholdOptions, BlurOptions, BorderOptions, CannyOptions, DilateOptions, ErodeOptions, GrayscaleOptions, InvertOptions, MorphologicalGradientOptions, OperationFunction, OperationName, OperationOptions, OperationResult, PartialOptions, RegisteredOperations, RequiredOptions, ResizeOptions, RotateOptions, ThresholdOptions, WarpOptions, } from "./pipeline/index.js";
38
+ export type { AdaptiveThresholdOptions, BlurOptions, BorderOptions, CannyOptions, DilateOptions, EqualizeOptions, ErodeOptions, GrayscaleOptions, InvertOptions, MorphologicalGradientOptions, OperationFunction, OperationName, OperationOptions, OperationResult, PartialOptions, RegisteredOperations, RequiredOptions, ResizeOptions, RotateOptions, ThresholdOptions, WarpOptions, } from "./pipeline/index.js";
@@ -1,7 +1,7 @@
1
1
  import { cv } from "../cv-provider.js";
2
2
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
3
3
  /** Options for the adaptive threshold operation. */
4
- export interface AdaptiveThresholdOptions extends PartialOptions {
4
+ export type AdaptiveThresholdOptions = PartialOptions & {
5
5
  /** Upper threshold value (0-255) */
6
6
  upper: number;
7
7
  /** Adaptive threshold method (cv.ADAPTIVE_THRESH_...) */
@@ -12,6 +12,6 @@ export interface AdaptiveThresholdOptions extends PartialOptions {
12
12
  size: number;
13
13
  /** Constant subtracted from the mean or weighted mean */
14
14
  constant: number;
15
- }
15
+ };
16
16
  /** Apply adaptive thresholding to convert a grayscale image to binary. */
17
17
  export declare function adaptiveThreshold(img: cv.Mat, options: AdaptiveThresholdOptions): OperationResult;
@@ -1,11 +1,11 @@
1
1
  import { cv } from "../cv-provider.js";
2
2
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
3
3
  /** Options for the Gaussian blur operation. */
4
- export interface BlurOptions extends PartialOptions {
4
+ export type BlurOptions = PartialOptions & {
5
5
  /** Size of the blur [x, y] */
6
6
  size: [number, number];
7
7
  /** Gaussian kernel standard deviation on x axis */
8
8
  sigma: number;
9
- }
9
+ };
10
10
  /** Apply Gaussian blur to reduce noise. */
11
11
  export declare function blur(img: cv.Mat, options: BlurOptions): OperationResult;
@@ -1,13 +1,13 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
3
  /** Options for adding a constant-color border around the image. */
4
- export interface BorderOptions extends PartialOptions {
4
+ export type BorderOptions = PartialOptions & {
5
5
  /** Size of the border in pixels */
6
6
  size: number;
7
7
  /** Border type (e.g., cv.BORDER_CONSTANT) */
8
8
  borderType: cv.BorderTypes;
9
9
  /** Border color in [B, G, R, A] format */
10
10
  borderColor: [cv.int, cv.int, cv.int, cv.int];
11
- }
11
+ };
12
12
  /** Add a uniform border around the image using `cv.copyMakeBorder`. */
13
13
  export declare function border(img: cv.Mat, options: BorderOptions): OperationResult;
@@ -1,11 +1,11 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
3
  /** Options for the Canny edge-detection operation. */
4
- export interface CannyOptions extends PartialOptions {
4
+ export type CannyOptions = PartialOptions & {
5
5
  /** Lower threshold for the hysteresis procedure (0-255) */
6
6
  lower: number;
7
7
  /** Upper threshold for the hysteresis procedure (0-255) */
8
8
  upper: number;
9
- }
9
+ };
10
10
  /** Detect edges using the Canny algorithm. */
11
11
  export declare function canny(img: cv.Mat, options: CannyOptions): OperationResult;
@@ -1,9 +1,9 @@
1
1
  import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
3
  /** Options for converting a Mat to a different depth/channel type. */
4
- export interface ConvertOptions extends RequiredOptions {
4
+ export type ConvertOptions = RequiredOptions & {
5
5
  /** Desired matrix type (cv.CV_...) if negative, it will be the same as input */
6
6
  rtype: number;
7
- }
7
+ };
8
8
  /** Convert the image matrix to a new type via `Mat.convertTo`. */
9
9
  export declare function convert(img: cv.Mat, options: ConvertOptions): OperationResult;
@@ -1,11 +1,11 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
3
  /** Options for the morphological dilation operation. */
4
- export interface DilateOptions extends PartialOptions {
4
+ export type DilateOptions = PartialOptions & {
5
5
  /** Size of the block [x, y] */
6
6
  size: [number, number];
7
7
  /** Number of iterations for the dilation operation */
8
8
  iter: number;
9
- }
9
+ };
10
10
  /** Dilate the image to expand foreground regions. */
11
11
  export declare function dilate(img: cv.Mat, options: DilateOptions): OperationResult;
@@ -0,0 +1,34 @@
1
+ import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
+ import { cv } from "../cv-provider.js";
3
+ /** Options for the histogram equalization operation. */
4
+ export type EqualizeOptions = PartialOptions & {
5
+ /**
6
+ * Equalization algorithm to use.
7
+ * - `"clahe"` (default) — Contrast Limited Adaptive Histogram Equalization;
8
+ * preserves local contrast and avoids over-amplification in bright regions.
9
+ * - `"global"` — Standard global histogram equalization via `cv.equalizeHist`;
10
+ * faster but may blow out highlights.
11
+ */
12
+ method: "clahe" | "global";
13
+ /**
14
+ * CLAHE only — clip limit for contrast limiting (default `2.0`).
15
+ * Higher values allow more contrast; lower values are closer to global equalization.
16
+ */
17
+ clipLimit: number;
18
+ /**
19
+ * CLAHE only — tile grid size in pixels (default `8` → 8×8 tiles).
20
+ * The image is divided into this many tiles in each dimension.
21
+ */
22
+ tileGridSize: number;
23
+ };
24
+ /**
25
+ * Equalise histogram contrast on a single-channel (grayscale) image.
26
+ *
27
+ * Supports two algorithms selectable via {@link EqualizeOptions.method}:
28
+ * - `"clahe"` (default) — locally adaptive, clip-limited equalization.
29
+ * - `"global"` — standard whole-image histogram spreading.
30
+ *
31
+ * Input `img` must be an 8-bit single-channel `cv.Mat` (run `.grayscale()` first).
32
+ * The input Mat is deleted by this operation.
33
+ */
34
+ export declare function equalize(img: cv.Mat, options: EqualizeOptions): OperationResult;
@@ -0,0 +1 @@
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{method:"clahe",clipLimit:2,tileGridSize:8}}export function equalize(img,options){let dst=new cv.Mat;if(options.method==="global"){cv.equalizeHist(img,dst)}else{let tileSize=new cv.Size(options.tileGridSize,options.tileGridSize);let clahe=new cv.CLAHE(options.clipLimit,tileSize);clahe.apply(img,dst);clahe.delete()}img.delete();return{img:dst,width:dst.cols,height:dst.rows}}registry.register("equalize",equalize,defaultOptions);
@@ -1,11 +1,11 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
3
  /** Options for the morphological erosion operation. */
4
- export interface ErodeOptions extends PartialOptions {
4
+ export type ErodeOptions = PartialOptions & {
5
5
  /** Size of the block [x, y] */
6
6
  size: [number, number];
7
7
  /** Number of iterations for the erosion operation */
8
8
  iter: number;
9
- }
9
+ };
10
10
  /** Erode the image to shrink foreground regions and remove small noise. */
11
11
  export declare function erode(img: cv.Mat, options: ErodeOptions): OperationResult;
@@ -1,7 +1,6 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
3
  /** Options for the grayscale conversion operation (no configurable fields). */
4
- export interface GrayscaleOptions extends PartialOptions {
5
- }
4
+ export type GrayscaleOptions = PartialOptions;
6
5
  /** Convert the image to grayscale using `COLOR_RGBA2GRAY`. */
7
6
  export declare function grayscale(img: cv.Mat, _options: GrayscaleOptions): OperationResult;
@@ -1,7 +1,6 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
3
  /** Options for the bitwise-NOT color inversion operation (no configurable fields). */
4
- export interface InvertOptions extends PartialOptions {
5
- }
4
+ export type InvertOptions = PartialOptions;
6
5
  /** Invert all pixel values using `cv.bitwise_not`. */
7
6
  export declare function invert(img: cv.Mat, _options: InvertOptions): OperationResult;
@@ -1,9 +1,9 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
3
  /** Options for the morphological gradient operation. */
4
- export interface MorphologicalGradientOptions extends PartialOptions {
4
+ export type MorphologicalGradientOptions = PartialOptions & {
5
5
  /** Kernel size for the morphological gradient operation [x, y] */
6
6
  size: [number, number];
7
- }
7
+ };
8
8
  /** Apply morphological gradient to highlight edges (dilation minus erosion). */
9
9
  export declare function morphologicalGradient(img: cv.Mat, options: MorphologicalGradientOptions): OperationResult;
@@ -1,11 +1,11 @@
1
1
  import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
3
  /** Options for resizing the image to exact pixel dimensions. */
4
- export interface ResizeOptions extends RequiredOptions {
4
+ export type ResizeOptions = RequiredOptions & {
5
5
  /** Width of the resized image */
6
6
  width: number;
7
7
  /** Height of the resized image */
8
8
  height: number;
9
- }
9
+ };
10
10
  /** Resize the image to the given width and height. */
11
11
  export declare function resize(img: cv.Mat, options: ResizeOptions): OperationResult;
@@ -1,11 +1,11 @@
1
1
  import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
3
  /** Options for rotating the image around a pivot point. */
4
- export interface RotateOptions extends RequiredOptions {
4
+ export type RotateOptions = RequiredOptions & {
5
5
  /** Angle of rotation in degrees (positive for counter-clockwise) */
6
6
  angle: number;
7
7
  /** Optional center of rotation. Defaults to the image center. */
8
8
  center?: cv.Point;
9
- }
9
+ };
10
10
  /** Rotate the image by the given angle around its centre (or a custom pivot). */
11
11
  export declare function rotate(img: cv.Mat, options: RotateOptions): OperationResult;
@@ -1,13 +1,13 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
3
  /** Options for the global threshold operation. */
4
- export interface ThresholdOptions extends PartialOptions {
4
+ export type ThresholdOptions = PartialOptions & {
5
5
  /** Lower threshold value (0-255) */
6
6
  lower: number;
7
7
  /** Upper threshold value (0-255) */
8
8
  upper: number;
9
9
  /** Type of thresholding (cv.THRESH_...) */
10
10
  type: cv.ThresholdTypes;
11
- }
11
+ };
12
12
  /** Apply a global threshold to convert a grayscale image to binary. */
13
13
  export declare function threshold(img: cv.Mat, options: ThresholdOptions): OperationResult;
@@ -2,7 +2,7 @@ import { cv } from "../cv-provider.js";
2
2
  import type { BoundingBox, Points } from "../index.interface.js";
3
3
  import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
4
4
  /** Options for the perspective warp (four-point transform) operation. */
5
- export interface WarpOptions extends RequiredOptions {
5
+ export type WarpOptions = RequiredOptions & {
6
6
  /** Four points of the source image containing x and y point in
7
7
  * topLeft, topRight, bottomLeft and BottomRight.
8
8
  * Use Contours class instance to get the points
@@ -10,6 +10,6 @@ export interface WarpOptions extends RequiredOptions {
10
10
  points: Points;
11
11
  /** A destination canvas bounding box for cropping the original canvas */
12
12
  bbox: BoundingBox;
13
- }
13
+ };
14
14
  /** Apply a perspective warp using four source/destination corner points. */
15
15
  export declare function warp(img: cv.Mat, options: WarpOptions): OperationResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ppu-ocv",
3
- "version": "3.1.5",
3
+ "version": "3.2.1",
4
4
  "description": "A type-safe, modular, chainable image processing library built on top of OpenCV.js with a fluent API leveraging pipeline processing.",
5
5
  "keywords": [
6
6
  "open-cv",
@@ -43,34 +43,6 @@
43
43
  "default": "./index.canvas-web.js"
44
44
  }
45
45
  },
46
- "scripts": {
47
- "task": "bun scripts/task.ts",
48
- "build": "bun task build",
49
- "build:test": "bun task build && bun test",
50
- "build:publish": "bun task build && bun task report-size && bun task publish",
51
- "type-check": "bunx tsgo --noEmit",
52
- "test": "bun test --parallel=$(ls tests/*.test.ts | wc -l | tr -d ' ')",
53
- "lint": "oxlint",
54
- "lint:fix": "oxlint --fix",
55
- "fmt": "oxfmt --check",
56
- "fmt:fix": "oxfmt .",
57
- "demo": "bunx -y serve -l 4567 .",
58
- "prepare": "husky"
59
- },
60
- "devDependencies": {
61
- "@types/bun": "latest",
62
- "@types/uglify-js": "latest",
63
- "@typescript/native-preview": "^7.0.0-dev.20260513.1",
64
- "canvas": "^3.2.3",
65
- "husky": "^9.1.7",
66
- "lint-staged": "^17.0.4",
67
- "mitata": "latest",
68
- "oxfmt": "^0.49.0",
69
- "oxlint": "^1.63.0",
70
- "tsx": "latest",
71
- "typescript": "latest",
72
- "uglify-js": ">=2.4.24"
73
- },
74
46
  "repository": {
75
47
  "type": "git",
76
48
  "url": "https://github.com/PT-Perkasa-Pilar-Utama/ppu-ocv.git"
@@ -7,6 +7,7 @@ import "../operations/canny.js";
7
7
  import "../operations/convert.js";
8
8
  import "../operations/dilate.js";
9
9
  import "../operations/erode.js";
10
+ import "../operations/equalize.js";
10
11
  import "../operations/grayscale.js";
11
12
  import "../operations/invert.js";
12
13
  import "../operations/morphological-gradient.js";
@@ -21,6 +22,7 @@ export type { CannyOptions } from "../operations/canny.js";
21
22
  export type { ConvertOptions } from "../operations/convert.js";
22
23
  export type { DilateOptions } from "../operations/dilate.js";
23
24
  export type { ErodeOptions } from "../operations/erode.js";
25
+ export type { EqualizeOptions } from "../operations/equalize.js";
24
26
  export type { GrayscaleOptions } from "../operations/grayscale.js";
25
27
  export type { InvertOptions } from "../operations/invert.js";
26
28
  export type { MorphologicalGradientOptions } from "../operations/morphological-gradient.js";
package/pipeline/index.js CHANGED
@@ -1 +1 @@
1
- export{executeOperation,OperationRegistry,registry}from"./registry.js";import"../operations/adaptive-threshold.js";import"../operations/blur.js";import"../operations/border.js";import"../operations/canny.js";import"../operations/convert.js";import"../operations/dilate.js";import"../operations/erode.js";import"../operations/grayscale.js";import"../operations/invert.js";import"../operations/morphological-gradient.js";import"../operations/resize.js";import"../operations/rotate.js";import"../operations/threshold.js";import"../operations/warp.js";
1
+ export{executeOperation,OperationRegistry,registry}from"./registry.js";import"../operations/adaptive-threshold.js";import"../operations/blur.js";import"../operations/border.js";import"../operations/canny.js";import"../operations/convert.js";import"../operations/dilate.js";import"../operations/erode.js";import"../operations/equalize.js";import"../operations/grayscale.js";import"../operations/invert.js";import"../operations/morphological-gradient.js";import"../operations/resize.js";import"../operations/rotate.js";import"../operations/threshold.js";import"../operations/warp.js";
@@ -6,6 +6,7 @@ import type { CannyOptions } from "../operations/canny.js";
6
6
  import type { ConvertOptions } from "../operations/convert.js";
7
7
  import type { DilateOptions } from "../operations/dilate.js";
8
8
  import type { ErodeOptions } from "../operations/erode.js";
9
+ import type { EqualizeOptions } from "../operations/equalize.js";
9
10
  import type { GrayscaleOptions } from "../operations/grayscale.js";
10
11
  import type { InvertOptions } from "../operations/invert.js";
11
12
  import type { MorphologicalGradientOptions } from "../operations/morphological-gradient.js";
@@ -14,31 +15,31 @@ import type { RotateOptions } from "../operations/rotate.js";
14
15
  import type { ThresholdOptions } from "../operations/threshold.js";
15
16
  import type { WarpOptions } from "../operations/warp.js";
16
17
  /** The output produced by every pipeline operation: the transformed Mat plus its dimensions. */
17
- export interface OperationResult {
18
+ export type OperationResult = {
18
19
  /** Resulting OpenCV Mat after the operation. The caller is responsible for deleting it. */
19
20
  img: cv.Mat;
20
21
  /** Width of the resulting image in pixels. */
21
22
  width: number;
22
23
  /** Height of the resulting image in pixels. */
23
24
  height: number;
24
- }
25
+ };
25
26
  declare const RequiredBrand: unique symbol;
26
27
  /**
27
- * Marker interface for operation options that have no usable defaults and
28
- * must be supplied by the caller. Operation option types that extend this
28
+ * Marker type for operation options that have no usable defaults and
29
+ * must be supplied by the caller. Operation option types that intersect this
29
30
  * cannot be omitted when calling {@link ImageProcessor.execute}.
30
31
  */
31
- export interface RequiredOptions {
32
+ export type RequiredOptions = {
32
33
  [RequiredBrand]?: never;
33
- }
34
+ };
34
35
  declare const PartialBrand: unique symbol;
35
36
  /**
36
- * Marker interface for operation options that have sensible defaults.
37
- * Operation option types that extend this may be omitted or partially supplied.
37
+ * Marker type for operation options that have sensible defaults.
38
+ * Operation option types that intersect this may be omitted or partially supplied.
38
39
  */
39
- export interface PartialOptions {
40
+ export type PartialOptions = {
40
41
  [PartialBrand]?: never;
41
- }
42
+ };
42
43
  /** Signature every registered operation function must conform to. */
43
44
  export type OperationFunction<T> = (img: cv.Mat, options: T) => OperationResult;
44
45
  /**
@@ -66,6 +67,8 @@ export interface RegisteredOperations {
66
67
  convert: ConvertOptions;
67
68
  /** Morphological dilation. See {@link DilateOptions}. */
68
69
  dilate: DilateOptions;
70
+ /** Histogram equalisation (CLAHE or global). See {@link EqualizeOptions}. */
71
+ equalize: EqualizeOptions;
69
72
  /** Morphological erosion. See {@link ErodeOptions}. */
70
73
  erode: ErodeOptions;
71
74
  /** Convert to grayscale via `COLOR_RGBA2GRAY`. See {@link GrayscaleOptions}. */