ppu-ocv 3.1.0 → 3.1.3
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/README.md +40 -47
- package/canvas-processor.d.ts +3 -0
- package/canvas-processor.js +1 -1
- package/canvas-toolkit.base.d.ts +1 -0
- package/canvas-toolkit.base.js +1 -1
- package/canvas-toolkit.js +1 -1
- package/contours.d.ts +5 -0
- package/contours.js +1 -1
- package/cv-provider.d.ts +24 -0
- package/deskew.js +1 -1
- package/image-processor.d.ts +21 -5
- package/image-processor.js +1 -1
- package/index.canvas-web.d.ts +1 -1
- package/index.canvas.d.ts +1 -1
- package/index.d.ts +2 -2
- package/index.interface.d.ts +16 -0
- package/index.web.d.ts +26 -2
- package/operations/adaptive-threshold.d.ts +2 -5
- package/operations/blur.d.ts +2 -5
- package/operations/border.d.ts +2 -5
- package/operations/canny.d.ts +2 -5
- package/operations/convert.d.ts +2 -5
- package/operations/dilate.d.ts +2 -5
- package/operations/erode.d.ts +2 -5
- package/operations/grayscale.d.ts +3 -6
- package/operations/grayscale.js +1 -1
- package/operations/invert.d.ts +3 -6
- package/operations/invert.js +1 -1
- package/operations/morphological-gradient.d.ts +2 -5
- package/operations/resize.d.ts +2 -5
- package/operations/rotate.d.ts +2 -5
- package/operations/threshold.d.ts +2 -5
- package/operations/warp.d.ts +2 -5
- package/package.json +16 -7
- package/pipeline/registry.d.ts +27 -1
- package/pipeline/types.d.ts +55 -4
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# ppu-ocv
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/ppu-ocv) [](https://jsr.io/@snowfluke/ppu-ocv)
|
|
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
|
|
|
7
|
-
|
|
7
|
+

|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
10
|
const processor = new ImageProcessor(canvas);
|
|
@@ -17,23 +17,33 @@ const result = processor
|
|
|
17
17
|
.dilate({ size: [20, 20], iter: 5 })
|
|
18
18
|
.toCanvas();
|
|
19
19
|
|
|
20
|
-
// Memory cleanup
|
|
21
20
|
processor.destroy();
|
|
22
21
|
```
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
Based on [TechStark/opencv-js](https://github.com/TechStark/opencv-js).
|
|
24
|
+
|
|
25
|
+
## Table of Contents
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
- [Why ppu-ocv?](#why-ppu-ocv)
|
|
28
|
+
- [Installation](#installation)
|
|
29
|
+
- [Usage (Node.js / Bun)](#usage-nodejs--bun)
|
|
30
|
+
- [Canvas-only Usage (no OpenCV)](#canvas-only-usage-no-opencv)
|
|
31
|
+
- [Web / Browser Support](#web--browser-support)
|
|
32
|
+
- [Built-in Pipeline Operations](#built-in-pipeline-operations)
|
|
33
|
+
- [Extending Operations](#extending-operations)
|
|
34
|
+
- [Class Documentation](#class-documentation)
|
|
35
|
+
- [Migrating from v2](#migrating-from-v2)
|
|
36
|
+
- [Contributing](#contributing)
|
|
37
|
+
- [License](#license)
|
|
27
38
|
|
|
28
|
-
|
|
39
|
+
## Why ppu-ocv?
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
7. **Loosely Coupled**: Canvas utilities are fully decoupled from OpenCV. Usable in Browser Extensions, Service Workers, and other constrained environments where OpenCV cannot be initialised
|
|
41
|
+
- **Simplified API** — chainable methods that hide OpenCV's verbose Mat allocation
|
|
42
|
+
- **No memory management** — automatic Mat lifecycle within the pipeline
|
|
43
|
+
- **Type-safe** — full TypeScript inference for operations and options
|
|
44
|
+
- **Extensible** — register custom operations with `registry.register(...)` without forking
|
|
45
|
+
- **Cross-platform** — same API in Node, Bun, browsers, and constrained runtimes
|
|
46
|
+
- **Loosely coupled** — canvas utilities work standalone; OpenCV is only loaded when actually needed
|
|
37
47
|
|
|
38
48
|
## Installation
|
|
39
49
|
|
|
@@ -125,9 +135,7 @@ const buffer = await CanvasProcessor.prepareBuffer(cropped);
|
|
|
125
135
|
import { CanvasProcessor, CanvasToolkit } from "ppu-ocv/canvas-web";
|
|
126
136
|
|
|
127
137
|
const response = await fetch("/image.jpg");
|
|
128
|
-
const canvas = await CanvasProcessor.prepareCanvas(
|
|
129
|
-
await response.arrayBuffer(),
|
|
130
|
-
);
|
|
138
|
+
const canvas = await CanvasProcessor.prepareCanvas(await response.arrayBuffer());
|
|
131
139
|
```
|
|
132
140
|
|
|
133
141
|
## Web / Browser Support
|
|
@@ -171,9 +179,7 @@ processor.destroy();
|
|
|
171
179
|
await ImageProcessor.initRuntime();
|
|
172
180
|
|
|
173
181
|
const response = await fetch("/my-image.jpg");
|
|
174
|
-
const canvas = await CanvasProcessor.prepareCanvas(
|
|
175
|
-
await response.arrayBuffer(),
|
|
176
|
-
);
|
|
182
|
+
const canvas = await CanvasProcessor.prepareCanvas(await response.arrayBuffer());
|
|
177
183
|
|
|
178
184
|
const processor = new ImageProcessor(canvas);
|
|
179
185
|
processor
|
|
@@ -296,8 +302,8 @@ regions.sort((a, b) => b.area - a.area); // largest first
|
|
|
296
302
|
|
|
297
303
|
**Region detection** (returns data, does not mutate)
|
|
298
304
|
|
|
299
|
-
| Method | Options
|
|
300
|
-
| ------------- |
|
|
305
|
+
| Method | Options | Description |
|
|
306
|
+
| ------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
301
307
|
| `findRegions` | `foreground?` (`"light"`), `thresh?` (127), `minArea?`, `maxArea?`, `padding?`, `scale?` | 8-connected flood-fill on a binary canvas → `DetectedRegion[]` |
|
|
302
308
|
|
|
303
309
|
`DetectedRegion` shape: `{ bbox: BoundingBox, area: number }` where `bbox` is `{ x0, y0, x1, y1 }` (x1/y1 exclusive). Equivalent to OpenCV's `findContours(RETR_EXTERNAL) + boundingRect` — all matched bboxes agree within ±1 px on solid binary images. ³
|
|
@@ -368,37 +374,24 @@ A collection of utility functions for analyzing image properties (requires OpenC
|
|
|
368
374
|
|
|
369
375
|
## Contributing
|
|
370
376
|
|
|
371
|
-
|
|
377
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide — setup, commit conventions, quality checks, and PR flow. Also:
|
|
372
378
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
4. **Submit a Pull Request:** Open a pull request to discuss your changes and get feedback.
|
|
379
|
+
- [Code of Conduct](./CODE_OF_CONDUCT.md) — community standards.
|
|
380
|
+
- [Security policy](./SECURITY.md) — how to report vulnerabilities privately.
|
|
381
|
+
- [Issue tracker](https://github.com/PT-Perkasa-Pilar-Utama/ppu-ocv/issues) — bug reports, feature requests, and docs gaps each have a template.
|
|
377
382
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
This project uses Bun for testing. To run the tests locally, execute:
|
|
383
|
+
Quick local commands:
|
|
381
384
|
|
|
382
385
|
```bash
|
|
383
|
-
bun
|
|
386
|
+
bun install
|
|
387
|
+
bun test # run unit tests
|
|
388
|
+
bun run fmt # check formatting
|
|
389
|
+
bun run lint # check lint
|
|
390
|
+
bun run type-check # tsgo --noEmit
|
|
391
|
+
bun task build # emit ./lib
|
|
392
|
+
bun task bench # micro-bench the operations registry
|
|
384
393
|
```
|
|
385
394
|
|
|
386
|
-
Ensure that all tests pass before submitting your pull request.
|
|
387
|
-
|
|
388
|
-
## Scripts
|
|
389
|
-
|
|
390
|
-
Recommended development environment is in a Linux-based environment.
|
|
391
|
-
|
|
392
|
-
Library template: https://github.com/aquapi/lib-template
|
|
393
|
-
|
|
394
|
-
### [Build](./scripts/build.ts)
|
|
395
|
-
|
|
396
|
-
Emit `.js` and `.d.ts` files to [`lib`](./lib).
|
|
397
|
-
|
|
398
|
-
### [Publish](./scripts/publish.ts)
|
|
399
|
-
|
|
400
|
-
Move [`package.json`](./package.json), [`README.md`](./README.md) to [`lib`](./lib) and publish the package.
|
|
401
|
-
|
|
402
395
|
## Migrating from v2
|
|
403
396
|
|
|
404
397
|
See [MIGRATION.md](./MIGRATION.md) for a full guide. The short version:
|
package/canvas-processor.d.ts
CHANGED
|
@@ -45,8 +45,11 @@ export interface DetectedRegion {
|
|
|
45
45
|
*/
|
|
46
46
|
export declare class CanvasProcessor {
|
|
47
47
|
private _canvas;
|
|
48
|
+
/** Create a processor wrapping the given canvas. */
|
|
48
49
|
constructor(source: CanvasLike);
|
|
50
|
+
/** Current canvas width in pixels. */
|
|
49
51
|
get width(): number;
|
|
52
|
+
/** Current canvas height in pixels. */
|
|
50
53
|
get height(): number;
|
|
51
54
|
/**
|
|
52
55
|
* Scale the canvas to new dimensions.
|
package/canvas-processor.js
CHANGED
|
@@ -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.587*d[i+1]+0.114*d[i+2]);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]*alpha+beta);d[i+1]=Math.round(d[i+1]*alpha+beta);d[i+2]=Math.round(d[i+2]*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];d[i+1]=255-d[i+1];d[i+2]=255-d[i+2]}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]:Math.round(0.299*d[i]+0.587*d[i+1]+0.114*d[i+2]);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];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();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{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";
|
package/canvas-toolkit.base.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare class CanvasToolkitBase {
|
|
|
12
12
|
protected static _baseInstance: CanvasToolkitBase | null;
|
|
13
13
|
protected step: number;
|
|
14
14
|
protected constructor();
|
|
15
|
+
/** Return the singleton instance of {@link CanvasToolkitBase}. */
|
|
15
16
|
static getInstance(): CanvasToolkitBase;
|
|
16
17
|
/**
|
|
17
18
|
* Crop a part of source canvas and return a new canvas of the cropped part
|
package/canvas-toolkit.base.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export class CanvasToolkitBase{static _baseInstance=null;step=0;constructor(){}static getInstance(){if(!CanvasToolkitBase._baseInstance){CanvasToolkitBase._baseInstance=new CanvasToolkitBase}return CanvasToolkitBase._baseInstance}crop(options){const{bbox,canvas}=options;let croppedCanvas=getPlatform().createCanvas(bbox.x1-bbox.x0,bbox.y1-bbox.y0);let croppedCtx=croppedCanvas.getContext("2d");croppedCtx.drawImage(canvas,bbox.x0,bbox.y0,bbox.x1-bbox.x0,bbox.y1-bbox.y0,0,0,croppedCanvas.width,croppedCanvas.height);return croppedCanvas}isDirty(options){const{canvas,threshold=127.5,majorColorThreshold=0.97}=options;let whiteCount=0;let blackCount=0;let borderlessCanvas=this.crop({bbox:{x0:canvas.width*0.1,y0:canvas.height*0.1,x1:canvas.width*0.9,y1:canvas.height*0.9},canvas});let ctx=borderlessCanvas.getContext("2d");let colorData=ctx.getImageData(0,0,borderlessCanvas.width,borderlessCanvas.height).data;for(let i=0;i<colorData.length;i+=4){let red=colorData[i];let green=colorData[i+1];let blue=colorData[i+2];if(red>=threshold&&green>=threshold&&blue>=threshold){whiteCount++}else{blackCount++}}let majorColorRatio=Math.max(whiteCount,blackCount)/(blackCount+whiteCount);return majorColorRatio<majorColorThreshold}drawLine(options){const{ctx,x,y,width,height,lineWidth=2,color="blue"}=options;ctx.beginPath();ctx.strokeStyle=color;ctx.lineWidth=lineWidth;ctx.strokeRect(x,y,width,height);ctx.closePath()}drawContour(options){const{ctx,contour,strokeStyle="red",lineWidth=2}=options;let pts=contour.data32S;if(pts.length<4)return;ctx.strokeStyle=strokeStyle;ctx.lineWidth=lineWidth;ctx.beginPath();ctx.moveTo(pts[0],pts[1]);for(let i=2;i<pts.length;i+=2){ctx.lineTo(pts[i],pts[i+1])}ctx.closePath();ctx.stroke()}}import{getPlatform}from"./canvas-factory.js";
|
|
1
|
+
export class CanvasToolkitBase{static _baseInstance=null;step=0;constructor(){}static getInstance(){if(!CanvasToolkitBase._baseInstance){CanvasToolkitBase._baseInstance=new CanvasToolkitBase}return CanvasToolkitBase._baseInstance}crop(options){const{bbox,canvas}=options;let croppedCanvas=getPlatform().createCanvas(bbox.x1-bbox.x0,bbox.y1-bbox.y0);let croppedCtx=croppedCanvas.getContext("2d");croppedCtx.drawImage(canvas,bbox.x0,bbox.y0,bbox.x1-bbox.x0,bbox.y1-bbox.y0,0,0,croppedCanvas.width,croppedCanvas.height);return croppedCanvas}isDirty(options){const{canvas,threshold=127.5,majorColorThreshold=0.97}=options;let whiteCount=0;let blackCount=0;let borderlessCanvas=this.crop({bbox:{x0:canvas.width*0.1,y0:canvas.height*0.1,x1:canvas.width*0.9,y1:canvas.height*0.9},canvas});let ctx=borderlessCanvas.getContext("2d");let colorData=ctx.getImageData(0,0,borderlessCanvas.width,borderlessCanvas.height).data;for(let i=0;i<colorData.length;i+=4){let red=colorData[i];let green=colorData[i+1];let blue=colorData[i+2];if(red>=threshold&&green>=threshold&&blue>=threshold){whiteCount++}else{blackCount++}}let majorColorRatio=Math.max(whiteCount,blackCount)/(blackCount+whiteCount);return majorColorRatio<majorColorThreshold}drawLine(options){const{ctx,x,y,width,height,lineWidth=2,color="blue"}=options;ctx.beginPath();ctx.strokeStyle=color;ctx.lineWidth=lineWidth;ctx.strokeRect(x,y,width,height);ctx.closePath()}drawContour(options){const{ctx,contour,strokeStyle="red",lineWidth=2}=options;let pts=contour.data32S;if(pts.length<4)return;ctx.strokeStyle=strokeStyle;ctx.lineWidth=lineWidth;ctx.beginPath();ctx.moveTo(pts[0]??0,pts[1]??0);for(let i=2;i<pts.length;i+=2){ctx.lineTo(pts[i]??0,pts[i+1]??0)}ctx.closePath();ctx.stroke()}}import{getPlatform}from"./canvas-factory.js";
|
package/canvas-toolkit.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CanvasToolkitBase}from"./canvas-toolkit.base.js";import{createWriteStream,existsSync,mkdirSync,readdirSync,unlinkSync}from"fs";import{join}from"path";export class CanvasToolkit extends CanvasToolkitBase{static _nodeInstance=null;constructor(){super()}static getInstance(){if(!CanvasToolkit._nodeInstance){CanvasToolkit._nodeInstance=new CanvasToolkit}return CanvasToolkit._nodeInstance}saveImage(options){const{canvas,filename,path="out"}=options;let folderPath=join(process.cwd(),path);if(!existsSync(folderPath)){mkdirSync(folderPath,{recursive:true})}let filePath=join(folderPath,`${this.step++}. ${filename}.png`);let out=createWriteStream(filePath);let buffer=canvas.toBuffer("image/png");return new Promise((res,rej)=>{out.write(buffer,(err)=>{if(err){rej(err)}else{res()}})})}clearOutput(path="out"){let folderPath=join(process.cwd(),path);if(existsSync(folderPath)){let files=readdirSync(folderPath);for(let file of files){if(file===".gitignore")continue;let filePath=join(folderPath,file);unlinkSync(filePath)}}}}
|
|
1
|
+
import{CanvasToolkitBase}from"./canvas-toolkit.base.js";import{createWriteStream,existsSync,mkdirSync,readdirSync,unlinkSync}from"fs";import{join}from"path";export class CanvasToolkit extends CanvasToolkitBase{static _nodeInstance=null;constructor(){super()}static getInstance(){if(!CanvasToolkit._nodeInstance){CanvasToolkit._nodeInstance=new CanvasToolkit}return CanvasToolkit._nodeInstance}saveImage(options){const{canvas,filename,path="out"}=options;let folderPath=join(process.cwd(),path);if(!existsSync(folderPath)){mkdirSync(folderPath,{recursive:true})}let filePath=join(folderPath,`${this.step++}. ${filename}.png`);let out=createWriteStream(filePath);if(typeof canvas.toBuffer!=="function")throw new Error("toBuffer not available on this canvas");let buffer=canvas.toBuffer("image/png");return new Promise((res,rej)=>{out.write(buffer,(err)=>{if(err){rej(err)}else{res()}})})}clearOutput(path="out"){let folderPath=join(process.cwd(),path);if(existsSync(folderPath)){let files=readdirSync(folderPath);for(let file of files){if(file===".gitignore")continue;let filePath=join(folderPath,file);unlinkSync(filePath)}}}}
|
package/contours.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
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
|
+
/** Options for configuring contour detection. */
|
|
4
5
|
export interface ContoursOptions {
|
|
5
6
|
/** The contour retrieval mode. (cv.RETR_...) */
|
|
6
7
|
mode: cv.RetrievalModes;
|
|
7
8
|
/** The contour approximation method. (cv.CHAIN_...) */
|
|
8
9
|
method: cv.ContourApproximationModes;
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Wrapper around OpenCV's `findContours` that provides convenient accessors
|
|
13
|
+
* for iterating, filtering, and analyzing contours in a binary image.
|
|
14
|
+
*/
|
|
10
15
|
export declare class Contours {
|
|
11
16
|
private contours;
|
|
12
17
|
/** The constructor for the Contours class. It takes an image and options as parameters. */
|
package/contours.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export class Contours{contours;constructor(img,options={}){let opts={...defaultOptions(),...options};if(img instanceof cv.Mat){let contours=new cv.MatVector;let hierarchy=new cv.Mat;
|
|
1
|
+
export class Contours{contours;constructor(img,options={}){let opts={...defaultOptions(),...options};if(img instanceof cv.Mat){let contours=new cv.MatVector;let hierarchy=new cv.Mat;cv.findContours(img,contours,hierarchy,opts.mode,opts.method);hierarchy.delete();this.contours=contours}else{throw new Error("Invalid img type. Must be cv.Mat.")}}getAll(){return this.contours}getSize(){return this.contours.size()}getFromIndex(index){if(index<this.contours.size()){return this.contours.get(index)}return new cv.Mat}getRect(contour){return cv.boundingRect(contour)}iterate(callback){for(let i=0,len=this.contours.size();i<len;i++){let contour=this.contours.get(i);callback(contour)}return this}getLargestContourArea(){let maxArea=0;let largestContour=null;this.iterate((contour)=>{let area=cv.contourArea(contour);if(area>maxArea){maxArea=area;largestContour=contour}});return largestContour}getCornerPoints(options){const{canvas,contour=this.getLargestContourArea()}=options;let bbox={x0:0,y0:0,x1:canvas.width,y1:canvas.height};if(!contour){return{points:{topLeft:{x:bbox.x0,y:bbox.y0},topRight:{x:bbox.x1,y:bbox.y0},bottomLeft:{x:bbox.x0,y:bbox.y1},bottomRight:{x:bbox.x1,y:bbox.y1}},bbox}}let rect=cv.minAreaRect(contour);let vertices=cv.RotatedRect.points(rect);let points={topLeft:{x:0,y:0},topRight:{x:0,y:0},bottomRight:{x:0,y:0},bottomLeft:{x:0,y:0}};let sums=vertices.map((pt)=>pt.x+pt.y);let diffs=vertices.map((pt)=>pt.y-pt.x);let topLeftIdx=sums.indexOf(Math.min(...sums));let topRightIdx=diffs.indexOf(Math.min(...diffs));let bottomRightIdx=sums.indexOf(Math.max(...sums));let bottomLeftIdx=diffs.indexOf(Math.max(...diffs));if(!vertices[topLeftIdx]||!vertices[topRightIdx]||!vertices[bottomRightIdx]||!vertices[bottomLeftIdx]){return{points:{topLeft:{x:bbox.x0,y:bbox.y0},topRight:{x:bbox.x1,y:bbox.y0},bottomLeft:{x:bbox.x0,y:bbox.y1},bottomRight:{x:bbox.x1,y:bbox.y1}},bbox}}points.topLeft={x:vertices[topLeftIdx].x,y:vertices[topLeftIdx].y};points.topRight={x:vertices[topRightIdx].x,y:vertices[topRightIdx].y};points.bottomRight={x:vertices[bottomRightIdx].x,y:vertices[bottomRightIdx].y};points.bottomLeft={x:vertices[bottomLeftIdx].x,y:vertices[bottomLeftIdx].y};contour.delete();let ensureInBounds=(p)=>{p.x=Math.max(0,Math.min(canvas.width,p.x));p.y=Math.max(0,Math.min(canvas.height,p.y));return p};points.topLeft=ensureInBounds(points.topLeft);points.topRight=ensureInBounds(points.topRight);points.bottomLeft=ensureInBounds(points.bottomLeft);points.bottomRight=ensureInBounds(points.bottomRight);return{points,bbox}}getApproximateRectangleContour(options){const{threshold=0.02,contour=this.getLargestContourArea()}=options??{};if(!contour)return;let epsilon=threshold*cv.arcLength(contour,true);let approxContour=new cv.Mat;cv.approxPolyDP(contour,approxContour,epsilon,true);contour.delete();return approxContour}destroy(){try{this.contours.delete()}catch{}}}import{cv}from"./cv-provider.js";function defaultOptions(){return{mode:cv.RETR_EXTERNAL,method:cv.CHAIN_APPROX_SIMPLE}}
|
package/cv-provider.d.ts
CHANGED
|
@@ -23,23 +23,47 @@ export declare function setCv(instance: CV): void;
|
|
|
23
23
|
* get BOTH the types (e.g. `cv.Mat`) AND the runtime Proxy object.
|
|
24
24
|
*/
|
|
25
25
|
export declare namespace cv {
|
|
26
|
+
/** OpenCV Mat (matrix / image buffer). */
|
|
26
27
|
type Mat = _cvType.Mat;
|
|
28
|
+
/** A vector of Mat objects, used for contours. */
|
|
27
29
|
type MatVector = _cvType.MatVector;
|
|
30
|
+
/** A 2D point `{ x, y }`. */
|
|
28
31
|
type Point = _cvType.Point;
|
|
32
|
+
/** An axis-aligned rectangle `{ x, y, width, height }`. */
|
|
29
33
|
type Rect = _cvType.Rect;
|
|
34
|
+
/** A 2D size `{ width, height }`. */
|
|
30
35
|
type Size = _cvType.Size;
|
|
36
|
+
/** A 4-element scalar value, often used for colors `[b, g, r, a]`. */
|
|
31
37
|
type Scalar = _cvType.Scalar;
|
|
38
|
+
/** Adaptive thresholding method constants (e.g., `cv.ADAPTIVE_THRESH_GAUSSIAN_C`). */
|
|
32
39
|
type AdaptiveThresholdTypes = _cvType.AdaptiveThresholdTypes;
|
|
40
|
+
/** Thresholding type constants (e.g., `cv.THRESH_BINARY`). */
|
|
33
41
|
type ThresholdTypes = _cvType.ThresholdTypes;
|
|
42
|
+
/** Line type constants (e.g., `cv.LINE_8`). */
|
|
34
43
|
type LineTypes = _cvType.LineTypes;
|
|
44
|
+
/** Contour retrieval mode constants (e.g., `cv.RETR_EXTERNAL`). */
|
|
35
45
|
type RetrievalModes = _cvType.RetrievalModes;
|
|
46
|
+
/** Contour approximation method constants (e.g., `cv.CHAIN_APPROX_SIMPLE`). */
|
|
36
47
|
type ContourApproximationModes = _cvType.ContourApproximationModes;
|
|
48
|
+
/** Border type constants (e.g., `cv.BORDER_CONSTANT`). */
|
|
37
49
|
type BorderTypes = _cvType.BorderTypes;
|
|
50
|
+
/** Interpolation flag constants (e.g., `cv.INTER_LINEAR`). */
|
|
38
51
|
type InterpolationFlags = _cvType.InterpolationFlags;
|
|
52
|
+
/** Color conversion code constants (e.g., `cv.COLOR_RGBA2GRAY`). */
|
|
39
53
|
type ColorConversionCodes = _cvType.ColorConversionCodes;
|
|
54
|
+
/** Morphological structuring element shape constants (e.g., `cv.MORPH_RECT`). */
|
|
40
55
|
type MorphShapes = _cvType.MorphShapes;
|
|
56
|
+
/** Morphological operation type constants (e.g., `cv.MORPH_GRADIENT`). */
|
|
41
57
|
type MorphTypes = _cvType.MorphTypes;
|
|
58
|
+
/** Integer alias — opencv-js represents `int` as a plain `number`. */
|
|
42
59
|
type int = number;
|
|
43
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Lazy proxy for the OpenCV runtime.
|
|
63
|
+
* Access any OpenCV constant or constructor (e.g. `cv.Mat`, `cv.RETR_EXTERNAL`)
|
|
64
|
+
* through this object. The underlying instance is resolved on first access,
|
|
65
|
+
* so importing this module never throws at module-load time — only when a
|
|
66
|
+
* property is actually accessed before {@link ImageProcessor.initRuntime} has run.
|
|
67
|
+
*/
|
|
44
68
|
export declare const cv: CV;
|
|
45
69
|
export {};
|
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,
|
|
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";
|
package/image-processor.d.ts
CHANGED
|
@@ -5,6 +5,25 @@ type NameWithRequiredOptions = {
|
|
|
5
5
|
[N in OperationName]: OperationOptions<N> extends RequiredOptions ? N : never;
|
|
6
6
|
}[OperationName];
|
|
7
7
|
type NameWithOptionalOptions = Exclude<OperationName, NameWithRequiredOptions>;
|
|
8
|
+
/**
|
|
9
|
+
* OpenCV-powered image processing pipeline.
|
|
10
|
+
*
|
|
11
|
+
* Wraps a `cv.Mat` and exposes a chainable API of named operations
|
|
12
|
+
* (grayscale, blur, threshold, etc.). Each method mutates the internal
|
|
13
|
+
* state and returns `this`, so operations can be chained fluently.
|
|
14
|
+
*
|
|
15
|
+
* Call {@link ImageProcessor.initRuntime} once before creating any instances.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* await ImageProcessor.initRuntime();
|
|
20
|
+
* const result = new ImageProcessor(canvas)
|
|
21
|
+
* .grayscale()
|
|
22
|
+
* .blur()
|
|
23
|
+
* .threshold()
|
|
24
|
+
* .toCanvas();
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
8
27
|
export declare class ImageProcessor {
|
|
9
28
|
img: cv.Mat;
|
|
10
29
|
width: number;
|
|
@@ -15,11 +34,8 @@ export declare class ImageProcessor {
|
|
|
15
34
|
*/
|
|
16
35
|
constructor(source: CanvasLike | cv.Mat);
|
|
17
36
|
/**
|
|
18
|
-
* Initialize OpenCV runtime.
|
|
19
|
-
*
|
|
20
|
-
* - **Node.js**: Uses `@techstark/opencv-js` from node_modules (loaded by entry point).
|
|
21
|
-
* - **Browser with bundler**: Resolves `@techstark/opencv-js` via the bundler.
|
|
22
|
-
* - **Browser without bundler**: Falls back to loading `@techstark/opencv-js` from npm CDN.
|
|
37
|
+
* Initialize OpenCV runtime. This is recommended to be called before any
|
|
38
|
+
* image processing.
|
|
23
39
|
*/
|
|
24
40
|
static initRuntime(): Promise<void>;
|
|
25
41
|
/**
|
package/image-processor.js
CHANGED
|
@@ -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(){
|
|
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";
|
package/index.canvas-web.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { BoundingBox, Coordinate, Points } from "./index.interface.js";
|
|
2
2
|
export { getPlatform, setPlatform } from "./canvas-factory.js";
|
|
3
|
-
export type { CanvasLike, CanvasPlatform, Context2DLike
|
|
3
|
+
export type { CanvasLike, CanvasPlatform, Context2DLike } from "./canvas-factory.js";
|
|
4
4
|
export { webPlatform } from "./platform/web.js";
|
|
5
5
|
export { CanvasToolkitBase as CanvasToolkit, CanvasToolkitBase, type ContourLike, } from "./canvas-toolkit.base.js";
|
|
6
6
|
export { CanvasProcessor, type DetectedRegion } from "./canvas-processor.js";
|
package/index.canvas.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { Canvas, createCanvas, ImageData, loadImage } from "@napi-rs/canvas";
|
|
|
2
2
|
export type { SKRSContext2D } from "@napi-rs/canvas";
|
|
3
3
|
export type { BoundingBox, Coordinate, Points } from "./index.interface.js";
|
|
4
4
|
export { getPlatform, setPlatform } from "./canvas-factory.js";
|
|
5
|
-
export type { CanvasLike, CanvasPlatform, Context2DLike
|
|
5
|
+
export type { CanvasLike, CanvasPlatform, Context2DLike } from "./canvas-factory.js";
|
|
6
6
|
export { CanvasToolkitBase, type ContourLike } from "./canvas-toolkit.base.js";
|
|
7
7
|
export { CanvasToolkit } from "./canvas-toolkit.js";
|
|
8
8
|
export { CanvasProcessor, type DetectedRegion } from "./canvas-processor.js";
|
package/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ export { cv };
|
|
|
3
3
|
export { Canvas, createCanvas, ImageData, loadImage } from "@napi-rs/canvas";
|
|
4
4
|
export type { SKRSContext2D } from "@napi-rs/canvas";
|
|
5
5
|
export type { BoundingBox, Coordinate, Points } from "./index.interface.js";
|
|
6
|
-
export { executeOperation, OperationRegistry, registry
|
|
6
|
+
export { executeOperation, OperationRegistry, registry } from "./pipeline/index.js";
|
|
7
7
|
export { getPlatform, setPlatform } from "./canvas-factory.js";
|
|
8
|
-
export type { CanvasLike, CanvasPlatform, Context2DLike
|
|
8
|
+
export type { CanvasLike, CanvasPlatform, Context2DLike } from "./canvas-factory.js";
|
|
9
9
|
export { CanvasToolkitBase, type ContourLike } from "./canvas-toolkit.base.js";
|
|
10
10
|
export { CanvasToolkit } from "./canvas-toolkit.js";
|
|
11
11
|
export { CanvasProcessor, type DetectedRegion } from "./canvas-processor.js";
|
package/index.interface.d.ts
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
|
+
/** A 2D point in canvas pixel coordinates. */
|
|
1
2
|
export interface Coordinate {
|
|
3
|
+
/** X coordinate in pixels, measured from the left edge. */
|
|
2
4
|
x: number;
|
|
5
|
+
/** Y coordinate in pixels, measured from the top edge. */
|
|
3
6
|
y: number;
|
|
4
7
|
}
|
|
8
|
+
/** The four corner points of an axis-aligned rectangle, e.g. a contour bounding box. */
|
|
5
9
|
export interface Points {
|
|
10
|
+
/** Top-left corner. */
|
|
6
11
|
topLeft: Coordinate;
|
|
12
|
+
/** Top-right corner. */
|
|
7
13
|
topRight: Coordinate;
|
|
14
|
+
/** Bottom-left corner. */
|
|
8
15
|
bottomLeft: Coordinate;
|
|
16
|
+
/** Bottom-right corner. */
|
|
9
17
|
bottomRight: Coordinate;
|
|
10
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* An axis-aligned bounding box with exclusive `x1` / `y1`.
|
|
21
|
+
* Width is `x1 - x0`; height is `y1 - y0`.
|
|
22
|
+
*/
|
|
11
23
|
export interface BoundingBox {
|
|
24
|
+
/** Left edge, inclusive. */
|
|
12
25
|
x0: number;
|
|
26
|
+
/** Top edge, inclusive. */
|
|
13
27
|
y0: number;
|
|
28
|
+
/** Right edge, exclusive. */
|
|
14
29
|
x1: number;
|
|
30
|
+
/** Bottom edge, exclusive. */
|
|
15
31
|
y1: number;
|
|
16
32
|
}
|
package/index.web.d.ts
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web entry point — browsers with OpenCV + DOM canvas.
|
|
3
|
+
*
|
|
4
|
+
* Use this when you need the full image-processing pipeline in a browser.
|
|
5
|
+
* Canvas operations target `HTMLCanvasElement` / `OffscreenCanvas` instead
|
|
6
|
+
* of `@napi-rs/canvas`. OpenCV is loaded via `ImageProcessor.initRuntime()`
|
|
7
|
+
* — either from the bundled `@techstark/opencv-js` or from jsDelivr CDN
|
|
8
|
+
* when no bundler is in play.
|
|
9
|
+
*
|
|
10
|
+
* Not suitable for Manifest V3 Chrome extensions or other CSP-restricted
|
|
11
|
+
* runtimes — OpenCV.js uses Emscripten embind which calls `new Function`.
|
|
12
|
+
* For those environments, import `ppu-ocv/canvas-web` (no OpenCV).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { ImageProcessor, CanvasProcessor } from "ppu-ocv/web";
|
|
17
|
+
*
|
|
18
|
+
* await ImageProcessor.initRuntime();
|
|
19
|
+
* const canvas = await CanvasProcessor.prepareCanvas(await response.arrayBuffer());
|
|
20
|
+
* const result = new ImageProcessor(canvas).grayscale().toCanvas();
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
1
25
|
import { cv } from "./cv-provider.js";
|
|
2
26
|
export { cv };
|
|
3
27
|
export { getPlatform, setPlatform } from "./canvas-factory.js";
|
|
4
|
-
export type { CanvasLike, CanvasPlatform, Context2DLike
|
|
28
|
+
export type { CanvasLike, CanvasPlatform, Context2DLike } from "./canvas-factory.js";
|
|
5
29
|
export { webPlatform } from "./platform/web.js";
|
|
6
30
|
export type { BoundingBox, Coordinate, Points } from "./index.interface.js";
|
|
7
|
-
export { executeOperation, OperationRegistry, registry
|
|
31
|
+
export { executeOperation, OperationRegistry, registry } from "./pipeline/index.js";
|
|
8
32
|
export { CanvasToolkitBase as CanvasToolkit, CanvasToolkitBase, type ContourLike, } from "./canvas-toolkit.base.js";
|
|
9
33
|
export { CanvasProcessor, type DetectedRegion } from "./canvas-processor.js";
|
|
10
34
|
export { Contours } from "./contours.js";
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { cv } from "../cv-provider.js";
|
|
2
2
|
import type { OperationResult, PartialOptions } from "../pipeline/types.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
adaptiveThreshold: AdaptiveThresholdOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for the adaptive threshold operation. */
|
|
8
4
|
export interface AdaptiveThresholdOptions extends PartialOptions {
|
|
9
5
|
/** Upper threshold value (0-255) */
|
|
10
6
|
upper: number;
|
|
@@ -17,4 +13,5 @@ export interface AdaptiveThresholdOptions extends PartialOptions {
|
|
|
17
13
|
/** Constant subtracted from the mean or weighted mean */
|
|
18
14
|
constant: number;
|
|
19
15
|
}
|
|
16
|
+
/** Apply adaptive thresholding to convert a grayscale image to binary. */
|
|
20
17
|
export declare function adaptiveThreshold(img: cv.Mat, options: AdaptiveThresholdOptions): OperationResult;
|
package/operations/blur.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { cv } from "../cv-provider.js";
|
|
2
2
|
import type { OperationResult, PartialOptions } from "../pipeline/types.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
blur: BlurOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for the Gaussian blur operation. */
|
|
8
4
|
export interface BlurOptions extends PartialOptions {
|
|
9
5
|
/** Size of the blur [x, y] */
|
|
10
6
|
size: [number, number];
|
|
11
7
|
/** Gaussian kernel standard deviation on x axis */
|
|
12
8
|
sigma: number;
|
|
13
9
|
}
|
|
10
|
+
/** Apply Gaussian blur to reduce noise. */
|
|
14
11
|
export declare function blur(img: cv.Mat, options: BlurOptions): OperationResult;
|
package/operations/border.d.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type { OperationResult, PartialOptions } from "../pipeline/types.js";
|
|
2
2
|
import { cv } from "../cv-provider.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
border: BorderOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for adding a constant-color border around the image. */
|
|
8
4
|
export interface BorderOptions extends PartialOptions {
|
|
9
5
|
/** Size of the border in pixels */
|
|
10
6
|
size: number;
|
|
@@ -13,4 +9,5 @@ export interface BorderOptions extends PartialOptions {
|
|
|
13
9
|
/** Border color in [B, G, R, A] format */
|
|
14
10
|
borderColor: [cv.int, cv.int, cv.int, cv.int];
|
|
15
11
|
}
|
|
12
|
+
/** Add a uniform border around the image using `cv.copyMakeBorder`. */
|
|
16
13
|
export declare function border(img: cv.Mat, options: BorderOptions): OperationResult;
|
package/operations/canny.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { OperationResult, PartialOptions } from "../pipeline/types.js";
|
|
2
2
|
import { cv } from "../cv-provider.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
canny: CannyOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for the Canny edge-detection operation. */
|
|
8
4
|
export interface CannyOptions extends PartialOptions {
|
|
9
5
|
/** Lower threshold for the hysteresis procedure (0-255) */
|
|
10
6
|
lower: number;
|
|
11
7
|
/** Upper threshold for the hysteresis procedure (0-255) */
|
|
12
8
|
upper: number;
|
|
13
9
|
}
|
|
10
|
+
/** Detect edges using the Canny algorithm. */
|
|
14
11
|
export declare function canny(img: cv.Mat, options: CannyOptions): OperationResult;
|
package/operations/convert.d.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
|
|
2
2
|
import { cv } from "../cv-provider.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
convert: ConvertOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for converting a Mat to a different depth/channel type. */
|
|
8
4
|
export interface ConvertOptions extends RequiredOptions {
|
|
9
5
|
/** Desired matrix type (cv.CV_...) if negative, it will be the same as input */
|
|
10
6
|
rtype: number;
|
|
11
7
|
}
|
|
8
|
+
/** Convert the image matrix to a new type via `Mat.convertTo`. */
|
|
12
9
|
export declare function convert(img: cv.Mat, options: ConvertOptions): OperationResult;
|
package/operations/dilate.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { OperationResult, PartialOptions } from "../pipeline/types.js";
|
|
2
2
|
import { cv } from "../cv-provider.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
dilate: DilateOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for the morphological dilation operation. */
|
|
8
4
|
export interface DilateOptions extends PartialOptions {
|
|
9
5
|
/** Size of the block [x, y] */
|
|
10
6
|
size: [number, number];
|
|
11
7
|
/** Number of iterations for the dilation operation */
|
|
12
8
|
iter: number;
|
|
13
9
|
}
|
|
10
|
+
/** Dilate the image to expand foreground regions. */
|
|
14
11
|
export declare function dilate(img: cv.Mat, options: DilateOptions): OperationResult;
|
package/operations/erode.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { OperationResult, PartialOptions } from "../pipeline/types.js";
|
|
2
2
|
import { cv } from "../cv-provider.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
erode: ErodeOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for the morphological erosion operation. */
|
|
8
4
|
export interface ErodeOptions extends PartialOptions {
|
|
9
5
|
/** Size of the block [x, y] */
|
|
10
6
|
size: [number, number];
|
|
11
7
|
/** Number of iterations for the erosion operation */
|
|
12
8
|
iter: number;
|
|
13
9
|
}
|
|
10
|
+
/** Erode the image to shrink foreground regions and remove small noise. */
|
|
14
11
|
export declare function erode(img: cv.Mat, options: ErodeOptions): OperationResult;
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { OperationResult, PartialOptions } from "../pipeline/types.js";
|
|
2
2
|
import { cv } from "../cv-provider.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
grayscale: GrayscaleOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for the grayscale conversion operation (no configurable fields). */
|
|
8
4
|
export interface GrayscaleOptions extends PartialOptions {
|
|
9
5
|
}
|
|
10
|
-
|
|
6
|
+
/** Convert the image to grayscale using `COLOR_RGBA2GRAY`. */
|
|
7
|
+
export declare function grayscale(img: cv.Mat, _options: GrayscaleOptions): OperationResult;
|
package/operations/grayscale.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{}}export function grayscale(img,
|
|
1
|
+
import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{}}export function grayscale(img,_options){let imgGrayscale=new cv.Mat;cv.cvtColor(img,imgGrayscale,cv.COLOR_RGBA2GRAY);img.delete();return{img:imgGrayscale,width:imgGrayscale.cols,height:imgGrayscale.rows}}registry.register("grayscale",grayscale,defaultOptions);
|
package/operations/invert.d.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { OperationResult, PartialOptions } from "../pipeline/types.js";
|
|
2
2
|
import { cv } from "../cv-provider.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
invert: InvertOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for the bitwise-NOT color inversion operation (no configurable fields). */
|
|
8
4
|
export interface InvertOptions extends PartialOptions {
|
|
9
5
|
}
|
|
10
|
-
|
|
6
|
+
/** Invert all pixel values using `cv.bitwise_not`. */
|
|
7
|
+
export declare function invert(img: cv.Mat, _options: InvertOptions): OperationResult;
|
package/operations/invert.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{}}export function invert(img,
|
|
1
|
+
import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{}}export function invert(img,_options){let imgInvert=new cv.Mat;cv.bitwise_not(img,imgInvert);img.delete();return{img:imgInvert,width:imgInvert.cols,height:imgInvert.rows}}registry.register("invert",invert,defaultOptions);
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import type { OperationResult, PartialOptions } from "../pipeline/types.js";
|
|
2
2
|
import { cv } from "../cv-provider.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
morphologicalGradient: MorphologicalGradientOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for the morphological gradient operation. */
|
|
8
4
|
export interface MorphologicalGradientOptions extends PartialOptions {
|
|
9
5
|
/** Kernel size for the morphological gradient operation [x, y] */
|
|
10
6
|
size: [number, number];
|
|
11
7
|
}
|
|
8
|
+
/** Apply morphological gradient to highlight edges (dilation minus erosion). */
|
|
12
9
|
export declare function morphologicalGradient(img: cv.Mat, options: MorphologicalGradientOptions): OperationResult;
|
package/operations/resize.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
|
|
2
2
|
import { cv } from "../cv-provider.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
resize: ResizeOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for resizing the image to exact pixel dimensions. */
|
|
8
4
|
export interface ResizeOptions extends RequiredOptions {
|
|
9
5
|
/** Width of the resized image */
|
|
10
6
|
width: number;
|
|
11
7
|
/** Height of the resized image */
|
|
12
8
|
height: number;
|
|
13
9
|
}
|
|
10
|
+
/** Resize the image to the given width and height. */
|
|
14
11
|
export declare function resize(img: cv.Mat, options: ResizeOptions): OperationResult;
|
package/operations/rotate.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
|
|
2
2
|
import { cv } from "../cv-provider.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
rotate: RotateOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for rotating the image around a pivot point. */
|
|
8
4
|
export interface RotateOptions extends RequiredOptions {
|
|
9
5
|
/** Angle of rotation in degrees (positive for counter-clockwise) */
|
|
10
6
|
angle: number;
|
|
11
7
|
/** Optional center of rotation. Defaults to the image center. */
|
|
12
8
|
center?: cv.Point;
|
|
13
9
|
}
|
|
10
|
+
/** Rotate the image by the given angle around its centre (or a custom pivot). */
|
|
14
11
|
export declare function rotate(img: cv.Mat, options: RotateOptions): OperationResult;
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type { OperationResult, PartialOptions } from "../pipeline/types.js";
|
|
2
2
|
import { cv } from "../cv-provider.js";
|
|
3
|
-
|
|
4
|
-
interface RegisteredOperations {
|
|
5
|
-
threshold: ThresholdOptions;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
3
|
+
/** Options for the global threshold operation. */
|
|
8
4
|
export interface ThresholdOptions extends PartialOptions {
|
|
9
5
|
/** Lower threshold value (0-255) */
|
|
10
6
|
lower: number;
|
|
@@ -13,4 +9,5 @@ export interface ThresholdOptions extends PartialOptions {
|
|
|
13
9
|
/** Type of thresholding (cv.THRESH_...) */
|
|
14
10
|
type: cv.ThresholdTypes;
|
|
15
11
|
}
|
|
12
|
+
/** Apply a global threshold to convert a grayscale image to binary. */
|
|
16
13
|
export declare function threshold(img: cv.Mat, options: ThresholdOptions): OperationResult;
|
package/operations/warp.d.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
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
|
-
|
|
5
|
-
interface RegisteredOperations {
|
|
6
|
-
warp: WarpOptions;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
4
|
+
/** Options for the perspective warp (four-point transform) operation. */
|
|
9
5
|
export interface WarpOptions extends RequiredOptions {
|
|
10
6
|
/** Four points of the source image containing x and y point in
|
|
11
7
|
* topLeft, topRight, bottomLeft and BottomRight.
|
|
@@ -15,4 +11,5 @@ export interface WarpOptions extends RequiredOptions {
|
|
|
15
11
|
/** A destination canvas bounding box for cropping the original canvas */
|
|
16
12
|
bbox: BoundingBox;
|
|
17
13
|
}
|
|
14
|
+
/** Apply a perspective warp using four source/destination corner points. */
|
|
18
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.
|
|
3
|
+
"version": "3.1.3",
|
|
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",
|
|
@@ -45,21 +45,30 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"task": "bun scripts/task.ts",
|
|
48
|
+
"build": "bun task build",
|
|
48
49
|
"build:test": "bun task build && bun test",
|
|
49
50
|
"build:publish": "bun task build && bun task report-size && bun task publish",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
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"
|
|
53
59
|
},
|
|
54
60
|
"devDependencies": {
|
|
55
|
-
"@stylistic/eslint-plugin": "latest",
|
|
56
61
|
"@types/bun": "latest",
|
|
57
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": "^16.0.0",
|
|
58
67
|
"mitata": "latest",
|
|
59
|
-
"
|
|
68
|
+
"oxfmt": "^0.48.0",
|
|
69
|
+
"oxlint": "^1.63.0",
|
|
60
70
|
"tsx": "latest",
|
|
61
71
|
"typescript": "latest",
|
|
62
|
-
"typescript-eslint": "latest",
|
|
63
72
|
"uglify-js": ">=2.4.24"
|
|
64
73
|
},
|
|
65
74
|
"repository": {
|
package/pipeline/registry.d.ts
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
|
-
import { cv } from "../cv-provider.js";
|
|
1
|
+
import type { cv } from "../cv-provider.js";
|
|
2
2
|
import type { OperationFunction, OperationName, OperationOptions, OperationResult } from "./index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registry that maps operation names to their implementation functions and
|
|
5
|
+
* default-option factories. Operation files call {@link OperationRegistry.register}
|
|
6
|
+
* at module-load time to make themselves available to {@link executeOperation}.
|
|
7
|
+
*/
|
|
3
8
|
export declare class OperationRegistry {
|
|
4
9
|
private operations;
|
|
5
10
|
private defaultOptions;
|
|
11
|
+
/**
|
|
12
|
+
* Register a named operation.
|
|
13
|
+
* @param name Unique operation name (must match a key in {@link RegisteredOperations}).
|
|
14
|
+
* @param operation The function that performs the operation.
|
|
15
|
+
* @param defaultOptions Optional factory returning default option values.
|
|
16
|
+
*/
|
|
6
17
|
register<Name extends OperationName>(name: Name, operation: OperationFunction<OperationOptions<Name>>, defaultOptions?: () => Partial<OperationOptions<Name>>): void;
|
|
18
|
+
/** Look up the implementation for an operation by name. */
|
|
7
19
|
getOperation(name: string): OperationFunction<any> | undefined;
|
|
20
|
+
/** Return the default-options factory for an operation, or an empty object if none was registered. */
|
|
8
21
|
getDefaultOptionsGenerator(name: string): any;
|
|
22
|
+
/** Return `true` if an operation with the given name has been registered. */
|
|
9
23
|
hasOperation(name: string): boolean;
|
|
24
|
+
/** Return the names of all registered operations. */
|
|
10
25
|
getOperationNames(): OperationName[];
|
|
11
26
|
}
|
|
27
|
+
/** Singleton registry populated by each `src/operations/*.ts` module at load time. */
|
|
12
28
|
export declare const registry: OperationRegistry;
|
|
29
|
+
/**
|
|
30
|
+
* Look up and execute a registered operation, merging caller-supplied options
|
|
31
|
+
* with the operation's defaults.
|
|
32
|
+
*
|
|
33
|
+
* @param operationName Name of the registered operation.
|
|
34
|
+
* @param img Input OpenCV Mat. The Mat is consumed (deleted) by the operation.
|
|
35
|
+
* @param options Partial options to merge with the operation's defaults.
|
|
36
|
+
* @returns The operation result containing the transformed Mat and its dimensions.
|
|
37
|
+
* @throws {Error} If no operation with `operationName` is registered.
|
|
38
|
+
*/
|
|
13
39
|
export declare function executeOperation<Name extends OperationName>(operationName: Name, img: cv.Mat, options?: Partial<OperationOptions<Name>>): OperationResult;
|
package/pipeline/types.d.ts
CHANGED
|
@@ -1,25 +1,76 @@
|
|
|
1
|
-
import { cv } from "../cv-provider.js";
|
|
1
|
+
import type { cv } from "../cv-provider.js";
|
|
2
|
+
import type { AdaptiveThresholdOptions } from "../operations/adaptive-threshold.js";
|
|
3
|
+
import type { BlurOptions } from "../operations/blur.js";
|
|
4
|
+
import type { BorderOptions } from "../operations/border.js";
|
|
5
|
+
import type { CannyOptions } from "../operations/canny.js";
|
|
6
|
+
import type { ConvertOptions } from "../operations/convert.js";
|
|
7
|
+
import type { DilateOptions } from "../operations/dilate.js";
|
|
8
|
+
import type { ErodeOptions } from "../operations/erode.js";
|
|
9
|
+
import type { GrayscaleOptions } from "../operations/grayscale.js";
|
|
10
|
+
import type { InvertOptions } from "../operations/invert.js";
|
|
11
|
+
import type { MorphologicalGradientOptions } from "../operations/morphological-gradient.js";
|
|
12
|
+
import type { ResizeOptions } from "../operations/resize.js";
|
|
13
|
+
import type { RotateOptions } from "../operations/rotate.js";
|
|
14
|
+
import type { ThresholdOptions } from "../operations/threshold.js";
|
|
15
|
+
import type { WarpOptions } from "../operations/warp.js";
|
|
16
|
+
/** The output produced by every pipeline operation: the transformed Mat plus its dimensions. */
|
|
2
17
|
export interface OperationResult {
|
|
18
|
+
/** Resulting OpenCV Mat after the operation. The caller is responsible for deleting it. */
|
|
3
19
|
img: cv.Mat;
|
|
20
|
+
/** Width of the resulting image in pixels. */
|
|
4
21
|
width: number;
|
|
22
|
+
/** Height of the resulting image in pixels. */
|
|
5
23
|
height: number;
|
|
6
24
|
}
|
|
7
25
|
declare const RequiredBrand: unique symbol;
|
|
26
|
+
/**
|
|
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
|
|
29
|
+
* cannot be omitted when calling {@link ImageProcessor.execute}.
|
|
30
|
+
*/
|
|
8
31
|
export interface RequiredOptions {
|
|
9
32
|
[RequiredBrand]?: never;
|
|
10
33
|
}
|
|
11
34
|
declare const PartialBrand: unique symbol;
|
|
35
|
+
/**
|
|
36
|
+
* Marker interface for operation options that have sensible defaults.
|
|
37
|
+
* Operation option types that extend this may be omitted or partially supplied.
|
|
38
|
+
*/
|
|
12
39
|
export interface PartialOptions {
|
|
13
40
|
[PartialBrand]?: never;
|
|
14
41
|
}
|
|
42
|
+
/** Signature every registered operation function must conform to. */
|
|
15
43
|
export type OperationFunction<T> = (img: cv.Mat, options: T) => OperationResult;
|
|
16
44
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
45
|
+
* Central registry mapping operation names to their option types. Each entry
|
|
46
|
+
* is the options type exported by the corresponding `src/operations/*.ts`
|
|
47
|
+
* file. Adding a new operation requires three changes: create the file,
|
|
48
|
+
* export the Options type, and add the entry below.
|
|
49
|
+
*
|
|
50
|
+
* Previously this used `declare module` augmentation so each operation file
|
|
51
|
+
* could register itself. JSR rejects that pattern because it modifies global
|
|
52
|
+
* types, so the registry is now explicit. Consumers can still extend this
|
|
53
|
+
* interface from their own code via `declare module "ppu-ocv"` — that's why
|
|
54
|
+
* it stays an interface rather than a type alias.
|
|
20
55
|
*/
|
|
21
56
|
export interface RegisteredOperations {
|
|
57
|
+
adaptiveThreshold: AdaptiveThresholdOptions;
|
|
58
|
+
blur: BlurOptions;
|
|
59
|
+
border: BorderOptions;
|
|
60
|
+
canny: CannyOptions;
|
|
61
|
+
convert: ConvertOptions;
|
|
62
|
+
dilate: DilateOptions;
|
|
63
|
+
erode: ErodeOptions;
|
|
64
|
+
grayscale: GrayscaleOptions;
|
|
65
|
+
invert: InvertOptions;
|
|
66
|
+
morphologicalGradient: MorphologicalGradientOptions;
|
|
67
|
+
resize: ResizeOptions;
|
|
68
|
+
rotate: RotateOptions;
|
|
69
|
+
threshold: ThresholdOptions;
|
|
70
|
+
warp: WarpOptions;
|
|
22
71
|
}
|
|
72
|
+
/** Union of all registered operation names. Extend {@link RegisteredOperations} to add new ones. */
|
|
23
73
|
export type OperationName = keyof RegisteredOperations;
|
|
74
|
+
/** Resolve the options type for a given operation name. */
|
|
24
75
|
export type OperationOptions<N extends OperationName> = RegisteredOperations[N];
|
|
25
76
|
export {};
|