ppu-ocv 0.0.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/README.md +218 -0
- package/canvas-toolkit.d.ts +98 -0
- package/canvas-toolkit.js +1 -0
- package/contours.d.ts +70 -0
- package/contours.js +1 -0
- package/image-analysis.d.ts +35 -0
- package/image-analysis.js +1 -0
- package/image-processor.d.ts +122 -0
- package/image-processor.js +1 -0
- package/index.d.ts +11 -0
- package/index.interface.d.ts +16 -0
- package/index.js +1 -0
- package/operations/adaptive-threshold.d.ts +20 -0
- package/operations/adaptive-threshold.js +1 -0
- package/operations/blur.d.ts +14 -0
- package/operations/blur.js +1 -0
- package/operations/border.d.ts +16 -0
- package/operations/border.js +1 -0
- package/operations/canny.d.ts +14 -0
- package/operations/canny.js +1 -0
- package/operations/dilate.d.ts +14 -0
- package/operations/dilate.js +1 -0
- package/operations/erode.d.ts +14 -0
- package/operations/erode.js +1 -0
- package/operations/grayscale.d.ts +10 -0
- package/operations/grayscale.js +1 -0
- package/operations/invert.d.ts +10 -0
- package/operations/invert.js +1 -0
- package/operations/morphological-gradient.d.ts +12 -0
- package/operations/morphological-gradient.js +1 -0
- package/operations/resize.d.ts +14 -0
- package/operations/resize.js +1 -0
- package/operations/threshold.d.ts +16 -0
- package/operations/threshold.js +1 -0
- package/operations/warp.d.ts +17 -0
- package/operations/warp.js +1 -0
- package/package.json +53 -0
- package/pipeline/index.d.ts +26 -0
- package/pipeline/index.js +1 -0
- package/pipeline/registry.d.ts +13 -0
- package/pipeline/registry.js +1 -0
- package/pipeline/types.d.ts +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# ppu-ocv
|
|
2
|
+
|
|
3
|
+
A type-safe, modular, chainable image processing library built on top of OpenCV.js with a fluent API leveraging pipeline processing.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Image manipulation as easy as:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
const processor = new ImageProcessor(canvas);
|
|
11
|
+
|
|
12
|
+
const result = processor
|
|
13
|
+
.grayscale()
|
|
14
|
+
.blur({ size: 5 })
|
|
15
|
+
.threshold()
|
|
16
|
+
.invert()
|
|
17
|
+
.dilate({ size: [20, 20], iter: 5 })
|
|
18
|
+
.toCanvas();
|
|
19
|
+
|
|
20
|
+
// Memory cleanup
|
|
21
|
+
processor.destroy();
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This work is based on https://github.com/TechStark/opencv-js.
|
|
25
|
+
|
|
26
|
+
## Why use this library?
|
|
27
|
+
|
|
28
|
+
OpenCV is powerful but can be cumbersome to use directly. This library provides:
|
|
29
|
+
|
|
30
|
+
1. **Simplified API**: Transform complex OpenCV calls into simple chainable methods
|
|
31
|
+
2. **Reduced Boilerplate**: No need to manage memory, conversions, or dimensions manually
|
|
32
|
+
3. **Development Speed**: Add image processing to your app in minutes, not hours
|
|
33
|
+
4. **Extensibility**: Custom operations for your specific needs without library modifications
|
|
34
|
+
5. **TypeScript Integration**: Full IntelliSense support with parameter validation
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
Install using your preferred package manager:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install ppu-ocv
|
|
42
|
+
yarn add ppu-ocv
|
|
43
|
+
bun add ppu-ocv
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> [!NOTE]
|
|
47
|
+
> This project is developed and tested primarily with Bun.
|
|
48
|
+
> Support for Node.js, Deno, or browser environments is **not guaranteed**.
|
|
49
|
+
> If you choose to use it outside of Bun and encounter any issues, feel free to report them.
|
|
50
|
+
> I'm open to fixing bugs for other runtimes with community help.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
Note that Operation order is matter, you should atleast know some basic in using OpenCV. See the operations table below this.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { ImageProcessor } from "ppu-ocv";
|
|
58
|
+
|
|
59
|
+
const file = Bun.file("./assets/receipt.jpg");
|
|
60
|
+
const image = await file.arrayBuffer();
|
|
61
|
+
|
|
62
|
+
const canvas = await ImageProcessor.prepareCanvas(image);
|
|
63
|
+
await ImageProcessor.initRuntime();
|
|
64
|
+
|
|
65
|
+
const processor = new ImageProcessor(canvas);
|
|
66
|
+
processor.grayscale().blur({ size: 5 }).threshold();
|
|
67
|
+
|
|
68
|
+
const resultCanvas = processor.toCanvas();
|
|
69
|
+
processor.destroy();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Or you can do directly via execute api:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { CanvasToolkit, ImageProcessor, cv } from "ppu-ocv";
|
|
76
|
+
|
|
77
|
+
const file = Bun.file("./assets/receipt.jpg");
|
|
78
|
+
const image = await file.arrayBuffer();
|
|
79
|
+
|
|
80
|
+
const canvasToolkit = new CanvasToolkit();
|
|
81
|
+
const canvas = await ImageProcessor.prepareCanvas(image);
|
|
82
|
+
await ImageProcessor.initRuntime();
|
|
83
|
+
|
|
84
|
+
const processor = new ImageProcessor(canvas);
|
|
85
|
+
const grayscaleImg = processor.execute("grayscale").toCanvas();
|
|
86
|
+
|
|
87
|
+
// the pipeline operation continued from grayscaled image
|
|
88
|
+
const thresholdImg = processor
|
|
89
|
+
.execute("blur")
|
|
90
|
+
.execute("threshold", {
|
|
91
|
+
type: cv.THRESH_BINARY_INV + cv.THRESH_OTSU,
|
|
92
|
+
})
|
|
93
|
+
.toCanvas();
|
|
94
|
+
|
|
95
|
+
await canvasToolkit.saveImage({
|
|
96
|
+
canvas: thresholdImg,
|
|
97
|
+
filename: "threshold",
|
|
98
|
+
path: "out",
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
For more advanced usage, see: [Example usage of ppu-ocv](./examples)
|
|
103
|
+
|
|
104
|
+
## Built-in pipeline operations
|
|
105
|
+
|
|
106
|
+
To avoid bloat, we only ship essential operations for chaining. Currently shipped operations are:
|
|
107
|
+
|
|
108
|
+
| Operation | Depends on… | Why |
|
|
109
|
+
| ------------------------- | ------------------------------------------- | --------------------------------------------------------------- |
|
|
110
|
+
| **grayscale** | – | Converts to single‐channel; many ops expect a gray image first. |
|
|
111
|
+
| **blur** | _(ideally after)_ grayscale | Noise reduction works best on 1-channel data. |
|
|
112
|
+
| **threshold** | _(after)_ grayscale | Produces a binary image; needs gray levels. |
|
|
113
|
+
| **adaptiveThreshold** | _(after)_ grayscale (and optionally blur) | Local thresholding on gray values (smoother if blurred first). |
|
|
114
|
+
| **invert** | _(after)_ threshold or adaptiveThreshold | Inverting a binary mask flips foreground/background. |
|
|
115
|
+
| **canny** | _(after)_ grayscale + blur | Edge detection expects a smoothed gray image. |
|
|
116
|
+
| **dilate** | _(after)_ threshold or edge detection | Expands foreground regions—usually on a binary mask. |
|
|
117
|
+
| **erode** | _(after)_ threshold or edge detection | Shrinks or cleans up binary regions. |
|
|
118
|
+
| **morphologicalGradient** | _(after)_ dilation + erosion (or threshold) | Highlights boundaries by subtracting eroded from dilated image. |
|
|
119
|
+
| **warp** | – | Geometric transform; can be applied at any point. |
|
|
120
|
+
| **resize** | – | Also independent; purely geometry. |
|
|
121
|
+
| **border** | – | Independent; purely geometry. |
|
|
122
|
+
|
|
123
|
+
## Extending operations
|
|
124
|
+
|
|
125
|
+
You can easily add your own by creating a prototype method or extend the class of `ImageProcessor`.
|
|
126
|
+
|
|
127
|
+
See: [How to extend ppu-ocv operations](./docs/how-to-extend-ppu-ocv-operations.md)
|
|
128
|
+
|
|
129
|
+
## Class documentation
|
|
130
|
+
|
|
131
|
+
#### `ImageProcessor`
|
|
132
|
+
|
|
133
|
+
| Method | Args | Description |
|
|
134
|
+
| ---------------------- | ---------------- | --------------------------------------------------------------------------- |
|
|
135
|
+
| constructor | cv.Mat or Canvas | Instantiate processor with initial image |
|
|
136
|
+
| static `prepareCanvas` | ArrayBuffer | Utility to load image from file buffer to canvas |
|
|
137
|
+
| static `initRuntime` | | Important open-cv runtime initialization, required to call once per runtime |
|
|
138
|
+
| operations | depends | Chainable operations like `blur`, `grayscale`, `resize` and so on |
|
|
139
|
+
| `execute` | name, options | Chainable operations directly via `execute` api |
|
|
140
|
+
| outputs | | Non-chainable & non-interupting method for output like `toMat`, `toCanvas` |
|
|
141
|
+
| `destroy` | | Non-chainable clean-up memory to destroy the object and the state |
|
|
142
|
+
|
|
143
|
+
#### `CanvasToolkit`
|
|
144
|
+
|
|
145
|
+
| Method | Args | Description |
|
|
146
|
+
| ------------- | ---------------------- | ----------------------------------------------------------------------------------------- |
|
|
147
|
+
| `crop` | BoundingBox, Canvas | Crop a part of source canvas and return a new canvas of the cropped part |
|
|
148
|
+
| `isDirty` | Canvas, threshold | Check whether a binary canvas is dirty (full of major color either black or white) or not |
|
|
149
|
+
| `saveImage` | Canvas, filename, path | Save a canvas to an image file |
|
|
150
|
+
| `clearOutput` | path | Clear the output folder |
|
|
151
|
+
| `drawLine` | ctx, coordinate, style | Draw a non-filled rectangle outline on the canvas |
|
|
152
|
+
| `drawContour` | ctx, contour, style | Draw a contour on the canvas |
|
|
153
|
+
|
|
154
|
+
#### `Contours`
|
|
155
|
+
|
|
156
|
+
| Method | Args | Description |
|
|
157
|
+
| ----------------------- | --------------- | ----------------------------------------------------------------------------------------- |
|
|
158
|
+
| constructor | cv.Mat, options | Instantiate Contours and automatically find & store contour list from args |
|
|
159
|
+
| `getAll` | | Crop a part of source canvas and return a new canvas of the cropped part |
|
|
160
|
+
| `getFromIndex` | index | Get contour at a specific index |
|
|
161
|
+
| `getRect` | | Get the rectangle that bounds the contour |
|
|
162
|
+
| `iterate` | callback | Iterate over all contours and call the callback function for each contour |
|
|
163
|
+
| `getLargestContourArea` | | Get the largest contour area |
|
|
164
|
+
| `getCornerPoints` | Canvas, contour | Get four corner points for a given contour. Useful for perspective transformation (warp). |
|
|
165
|
+
| `destroy` | | Destroy & clean-up the memory from the contours |
|
|
166
|
+
|
|
167
|
+
#### `ImageAnalysis`
|
|
168
|
+
|
|
169
|
+
Just a collection of utility functions for analyzing image properties.
|
|
170
|
+
|
|
171
|
+
- `calculateMeanNormalizedLabLightness`: Calculates the mean normalized lightness of an image using the L channel of the Lab color space. Lightness is normalized based on the image's own maximum lightness value before averaging.
|
|
172
|
+
|
|
173
|
+
- `calculateMeanGrayscaleValue`: Calculates the mean pixel value of the image after converting it to grayscale.
|
|
174
|
+
|
|
175
|
+
## Contributing
|
|
176
|
+
|
|
177
|
+
Contributions are welcome! If you would like to contribute, please follow these steps:
|
|
178
|
+
|
|
179
|
+
1. **Fork the Repository:** Create your own fork of the project.
|
|
180
|
+
2. **Create a Feature Branch:** Use a descriptive branch name for your changes.
|
|
181
|
+
3. **Implement Changes:** Make your modifications, add tests, and ensure everything passes.
|
|
182
|
+
4. **Submit a Pull Request:** Open a pull request to discuss your changes and get feedback.
|
|
183
|
+
|
|
184
|
+
### Running Tests
|
|
185
|
+
|
|
186
|
+
This project uses Bun for testing. To run the tests locally, execute:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
bun test
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Ensure that all tests pass before submitting your pull request.
|
|
193
|
+
|
|
194
|
+
## Scripts
|
|
195
|
+
|
|
196
|
+
Recommended development environment is in linux-based environment.
|
|
197
|
+
|
|
198
|
+
Library template: https://github.com/aquapi/lib-template
|
|
199
|
+
|
|
200
|
+
All script sources and usage.
|
|
201
|
+
|
|
202
|
+
### [Build](./scripts/build.ts)
|
|
203
|
+
|
|
204
|
+
Emit `.js` and `.d.ts` files to [`lib`](./lib).
|
|
205
|
+
|
|
206
|
+
### [Publish](./scripts/publish.ts)
|
|
207
|
+
|
|
208
|
+
Move [`package.json`](./package.json), [`README.md`](./README.md) to [`lib`](./lib) and publish the package.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
213
|
+
|
|
214
|
+
## Support
|
|
215
|
+
|
|
216
|
+
If you encounter any issues or have suggestions, please open an issue in the repository.
|
|
217
|
+
|
|
218
|
+
Happy coding!
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { BoundingBox, SKRSContext2D } from "@/index";
|
|
2
|
+
import { Canvas, cv } from "@/index";
|
|
3
|
+
export declare class CanvasToolkit {
|
|
4
|
+
private step;
|
|
5
|
+
/**
|
|
6
|
+
* Crop a part of source canvas and return a new canvas of the cropped part
|
|
7
|
+
* @param options
|
|
8
|
+
* @param options.bbox Bounding box of the cropped part
|
|
9
|
+
* @param options.canvas Source canvas
|
|
10
|
+
* @returns A new canvas of the cropped part
|
|
11
|
+
* @example
|
|
12
|
+
* const croppedCanvas = canvasToolkit.crop({
|
|
13
|
+
* bbox: { x0: 10, y0: 10, x1: 100, y1: 100 },
|
|
14
|
+
* canvas: sourceCanvas,
|
|
15
|
+
* });
|
|
16
|
+
*/
|
|
17
|
+
crop(options: {
|
|
18
|
+
bbox: BoundingBox;
|
|
19
|
+
canvas: Canvas;
|
|
20
|
+
}): Canvas;
|
|
21
|
+
/**
|
|
22
|
+
* Check whether a binary canvas is dirty (full of major color either black or white) or not
|
|
23
|
+
* @param options
|
|
24
|
+
* @param options.canvas Source canvas
|
|
25
|
+
* @param options.threshold Threshold for color detection (default: 127.5)
|
|
26
|
+
* @param options.majorColorThreshold Major color threshold (default: 0.97)
|
|
27
|
+
* @returns true if the canvas is dirty, false otherwise
|
|
28
|
+
* @example
|
|
29
|
+
* const isDirty = canvasToolkit.isDirty({
|
|
30
|
+
* canvas: sourceCanvas,
|
|
31
|
+
* threshold: 127.5,
|
|
32
|
+
* majorColorThreshold: 0.97,
|
|
33
|
+
* });
|
|
34
|
+
* console.log(isDirty); // true or false
|
|
35
|
+
*/
|
|
36
|
+
isDirty(options: {
|
|
37
|
+
canvas: Canvas;
|
|
38
|
+
threshold?: number;
|
|
39
|
+
majorColorThreshold?: number;
|
|
40
|
+
}): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Save a canvas to an image file
|
|
43
|
+
* @param options
|
|
44
|
+
* @param options.canvas Source canvas
|
|
45
|
+
* @param options.filename Filename of the image file
|
|
46
|
+
* @param options.path Path to save the image file (default: "out")
|
|
47
|
+
* @returns A promise that resolves when the image is saved
|
|
48
|
+
* @example
|
|
49
|
+
* await canvasToolkit.saveImage({
|
|
50
|
+
* canvas: sourceCanvas,
|
|
51
|
+
* filename: "output.png",
|
|
52
|
+
* });
|
|
53
|
+
*/
|
|
54
|
+
saveImage(options: {
|
|
55
|
+
canvas: Canvas;
|
|
56
|
+
filename: string;
|
|
57
|
+
path: string;
|
|
58
|
+
}): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Clear the output folder
|
|
61
|
+
* @param path Path to the output folder (default: "out")
|
|
62
|
+
*/
|
|
63
|
+
clearOutput(path?: string): void;
|
|
64
|
+
/**
|
|
65
|
+
* Draw a non-filled rectangle on the canvas
|
|
66
|
+
* @param options
|
|
67
|
+
* @param options.ctx Canvas rendering context
|
|
68
|
+
* @param options.x X coordinate of the top-left corner
|
|
69
|
+
* @param options.y Y coordinate of the top-left corner
|
|
70
|
+
* @param options.width Width of the rectangle
|
|
71
|
+
* @param options.height Height of the rectangle
|
|
72
|
+
* @param options.lineWidth Line width (default: 2)
|
|
73
|
+
* @param options.color Color of the rectangle (default: "blue")
|
|
74
|
+
*/
|
|
75
|
+
drawLine(options: {
|
|
76
|
+
ctx: SKRSContext2D;
|
|
77
|
+
x: number;
|
|
78
|
+
y: number;
|
|
79
|
+
width: number;
|
|
80
|
+
height: number;
|
|
81
|
+
lineWidth?: number;
|
|
82
|
+
color?: string;
|
|
83
|
+
}): void;
|
|
84
|
+
/**
|
|
85
|
+
* Draw a contour on the canvas
|
|
86
|
+
* @param options
|
|
87
|
+
* @param options.ctx Canvas rendering context
|
|
88
|
+
* @param options.contour Contour to be drawn
|
|
89
|
+
* @param options.strokeStyle Stroke color (default: "red")
|
|
90
|
+
* @param options.lineWidth Line width (default: 2)
|
|
91
|
+
*/
|
|
92
|
+
drawContour(options: {
|
|
93
|
+
ctx: SKRSContext2D;
|
|
94
|
+
contour: cv.Mat;
|
|
95
|
+
strokeStyle?: string;
|
|
96
|
+
lineWidth?: number;
|
|
97
|
+
}): void;
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class CanvasToolkit{step=0;crop(options){const{bbox,canvas}=options;let croppedCanvas=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}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)}}}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{createCanvas}from"@/index";import{createWriteStream,existsSync,mkdirSync,readdirSync,unlinkSync}from"fs";import{join}from"path";
|
package/contours.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { BoundingBox, Canvas, Points } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
export interface ContoursOptions {
|
|
4
|
+
/** The contour retrieval mode. (cv.RETR_...) */
|
|
5
|
+
mode: cv.RetrievalModes;
|
|
6
|
+
/** The contour approximation method. (cv.CHAIN_...) */
|
|
7
|
+
method: cv.ContourApproximationModes;
|
|
8
|
+
}
|
|
9
|
+
export declare class Contours {
|
|
10
|
+
private contours;
|
|
11
|
+
/** The constructor for the Contours class. It takes an image and options as parameters. */
|
|
12
|
+
/**
|
|
13
|
+
* @param img - The image to find contours in.
|
|
14
|
+
* @param options.mode - The contour retrieval mode. (cv.RETR_...)
|
|
15
|
+
* @param options.method - The contour approximation method. (cv.CHAIN_...)
|
|
16
|
+
* @example
|
|
17
|
+
* const contours = new Contours(image, {
|
|
18
|
+
* mode: cv.RETR_EXTERNAL,
|
|
19
|
+
* method: cv.CHAIN_APPROX_SIMPLE,
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
constructor(img: cv.Mat, options?: Partial<ContoursOptions>);
|
|
23
|
+
/**
|
|
24
|
+
* Get the all of contours found in the image.
|
|
25
|
+
* @returns The number of contours found in the image (cv.MatVector).
|
|
26
|
+
*/
|
|
27
|
+
getAll(): cv.MatVector;
|
|
28
|
+
/**
|
|
29
|
+
* Get contour at a specific index.
|
|
30
|
+
* @param index - The index of the contour to get.
|
|
31
|
+
* @returns The contour at the specified index (cv.Mat).
|
|
32
|
+
*/
|
|
33
|
+
getFromIndex(index: number): cv.Mat;
|
|
34
|
+
/**
|
|
35
|
+
* Get the rectangle that bounds the contour.
|
|
36
|
+
* @param contour - The contour to get the bounding rectangle for.
|
|
37
|
+
* @returns The bounding rectangle for the contour (cv.Rect).
|
|
38
|
+
*/
|
|
39
|
+
getRect(contour: cv.Mat): cv.Rect;
|
|
40
|
+
/**
|
|
41
|
+
* Iterate over all contours and call the callback function for each contour.
|
|
42
|
+
* @param callback - The callback function to call for each contour.
|
|
43
|
+
* The callback function takes a contour as a parameter.
|
|
44
|
+
* @returns void
|
|
45
|
+
*/
|
|
46
|
+
iterate(callback: (contour: cv.Mat) => any): Contours;
|
|
47
|
+
/**
|
|
48
|
+
* Get the largest contour area.
|
|
49
|
+
* @returns The largest contour area (cv.Mat).
|
|
50
|
+
*/
|
|
51
|
+
getLargestContourArea(): cv.Mat | null;
|
|
52
|
+
/**
|
|
53
|
+
* Get four corner points for a given contour.
|
|
54
|
+
* Useful for perspective transformation (warp).
|
|
55
|
+
* @param options.canvas - The canvas to get the corner points for.
|
|
56
|
+
* @param options.contour - The contour to get the corner points for. If not provided, the largest contour area will be used.
|
|
57
|
+
* @returns The four corner points of the contour (topLeft, topRight, bottomLeft, bottomRight) and the bounding box.
|
|
58
|
+
*/
|
|
59
|
+
getCornerPoints(options: {
|
|
60
|
+
canvas: Canvas;
|
|
61
|
+
contour?: cv.Mat;
|
|
62
|
+
}): {
|
|
63
|
+
points: Points;
|
|
64
|
+
bbox: BoundingBox;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Delete the contours object.
|
|
68
|
+
*/
|
|
69
|
+
destroy(): void;
|
|
70
|
+
}
|
package/contours.js
ADDED
|
@@ -0,0 +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;try{cv.findContours(img,contours,hierarchy,opts.mode,opts.method)}catch(error){throw error}hierarchy.delete();this.contours=contours}else{throw new Error("Invalid img type. Must be cv.Mat.")}}getAll(){return this.contours}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}}destroy(){try{this.contours.delete()}catch(error){}}}import{cv}from"@/index";function defaultOptions(){return{mode:cv.RETR_EXTERNAL,method:cv.CHAIN_APPROX_SIMPLE}}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This module provides utility functions for analyzing image properties.
|
|
3
|
+
* !IMPORTANT: Ensure ImageProcessor.initRuntime() has been called successfully
|
|
4
|
+
* once before using any functions from this module.
|
|
5
|
+
*/
|
|
6
|
+
import type { Canvas } from "@/index";
|
|
7
|
+
/**
|
|
8
|
+
* Options for calculating mean Lab lightness.
|
|
9
|
+
*/
|
|
10
|
+
export interface CalculateMeanLightnessOptions {
|
|
11
|
+
/** The canvas containing the image to be processed. */
|
|
12
|
+
canvas: Canvas;
|
|
13
|
+
/** The target dimensions for analysis (resizes internally). */
|
|
14
|
+
dimension: {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Calculates the mean normalized lightness of an image using the L channel of the Lab color space.
|
|
21
|
+
* Lightness is normalized based on the image's own maximum lightness value before averaging.
|
|
22
|
+
*
|
|
23
|
+
* @param options - Configuration options.
|
|
24
|
+
* @returns A promise resolving to the mean normalized lightness (0-1).
|
|
25
|
+
* @throws Error if OpenCV operations fail.
|
|
26
|
+
*/
|
|
27
|
+
export declare function calculateMeanNormalizedLabLightness(options: CalculateMeanLightnessOptions): Promise<number>;
|
|
28
|
+
/**
|
|
29
|
+
* Calculates the mean pixel value of the image after converting it to grayscale.
|
|
30
|
+
*
|
|
31
|
+
* @param canvas - The source canvas to be processed.
|
|
32
|
+
* @returns A promise resolving to the mean grayscale value (typically 0-255).
|
|
33
|
+
* @throws Error if OpenCV operations fail.
|
|
34
|
+
*/
|
|
35
|
+
export declare function calculateMeanGrayscaleValue(canvas: Canvas): Promise<number>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ImageProcessor,cv}from"@/index";export async function calculateMeanNormalizedLabLightness(options){const{canvas,dimension}=options;let processor=null;let resized=null;let labImg=null;let channels=null;let L=null;let mask=null;let scalarMat=null;try{processor=new ImageProcessor(canvas);resized=processor.execute("resize",{width:dimension.width,height:dimension.height}).toMat();labImg=new cv.Mat;cv.cvtColor(resized,labImg,cv.COLOR_BGR2Lab);channels=new cv.MatVector;cv.split(labImg,channels);L=channels.get(0);mask=new cv.Mat;let maxLocResult=cv.minMaxLoc(L,mask);let maxPixelValue=maxLocResult.maxVal;if(maxPixelValue===0){return 0}scalarMat=new cv.Mat(L.rows,L.cols,L.type(),new cv.Scalar(maxPixelValue));cv.divide(L,scalarMat,L,1,-1);let meanL=cv.mean(L)[0];return meanL??0}finally{processor?.destroy();labImg?.delete();channels?.delete();L?.delete();mask?.delete();scalarMat?.delete()}}export async function calculateMeanGrayscaleValue(canvas){let processor=null;let grayscaleImg=null;try{processor=new ImageProcessor(canvas);grayscaleImg=processor.blur().grayscale().toMat();let mean=cv.mean(grayscaleImg)[0];return mean??0}finally{processor?.destroy()}}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Canvas, cv } from "@/index";
|
|
2
|
+
import type { AdaptiveThresholdOptions, BlurOptions, BorderOptions, CannyOptions, DilateOptions, ErodeOptions, GrayscaleOptions, InvertOptions, MorphologicalGradientOptions, OperationName, OperationOptions, RequiredOptions, ResizeOptions, ThresholdOptions, WarpOptions } from "@/index";
|
|
3
|
+
type NameWithRequiredOptions = {
|
|
4
|
+
[N in OperationName]: OperationOptions<N> extends RequiredOptions ? N : never;
|
|
5
|
+
}[OperationName];
|
|
6
|
+
type NameWithOptionalOptions = Exclude<OperationName, NameWithRequiredOptions>;
|
|
7
|
+
export declare class ImageProcessor {
|
|
8
|
+
img: cv.Mat;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
/**
|
|
12
|
+
* Create an ImageProcessor instance from a Canvas or cv.Mat
|
|
13
|
+
* @param source Source image as Canvas or cv.Mat
|
|
14
|
+
*/
|
|
15
|
+
constructor(source: Canvas | cv.Mat);
|
|
16
|
+
static prepareCanvas(file: ArrayBuffer): Promise<Canvas>;
|
|
17
|
+
/**
|
|
18
|
+
* Initialize OpenCV runtime, this is recommended to be called before any image processing
|
|
19
|
+
*/
|
|
20
|
+
static initRuntime(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Execute a registered pipeline operation that requires options.
|
|
23
|
+
* @param operationName Name of the operation (e.g., "resize")
|
|
24
|
+
* @param options Required options for the operation
|
|
25
|
+
*/
|
|
26
|
+
execute<Name extends NameWithRequiredOptions>(operationName: Name, options: OperationOptions<Name>): this;
|
|
27
|
+
/**
|
|
28
|
+
* Execute a registered pipeline operation where options are optional or have defaults.
|
|
29
|
+
* @param operationName Name of the operation (e.g., "blur", "grayscale")
|
|
30
|
+
* @param options Optional or partial options for the operation
|
|
31
|
+
*/
|
|
32
|
+
execute<Name extends NameWithOptionalOptions>(operationName: Name, options?: Partial<OperationOptions<Name>>): this;
|
|
33
|
+
/**
|
|
34
|
+
* Convert image to grayscale
|
|
35
|
+
* @description Usage order: independent
|
|
36
|
+
* @param options Optional configuration for grayscale conversion
|
|
37
|
+
*/
|
|
38
|
+
grayscale(options?: Partial<GrayscaleOptions>): this;
|
|
39
|
+
/**
|
|
40
|
+
* Invert image colors
|
|
41
|
+
* @description Usage order: ideally (after) threshold or adaptiveThreshold
|
|
42
|
+
* @param options Optional configuration for inversion
|
|
43
|
+
*/
|
|
44
|
+
invert(options?: Partial<InvertOptions>): this;
|
|
45
|
+
/**
|
|
46
|
+
* Add border to image
|
|
47
|
+
* @description Usage order: independent
|
|
48
|
+
* @param options Border configuration options
|
|
49
|
+
*/
|
|
50
|
+
border(options?: Partial<BorderOptions>): this;
|
|
51
|
+
/**
|
|
52
|
+
* Bluring image to reduce noise using Gaussian Blur
|
|
53
|
+
* @description Usage order: (ideally after) grayscale
|
|
54
|
+
* @param options Blur configuration options
|
|
55
|
+
*/
|
|
56
|
+
blur(options?: Partial<BlurOptions>): this;
|
|
57
|
+
/** Thresholding to convert image to binary
|
|
58
|
+
* @description Usage order: (after) grayscale (and optionally blur)
|
|
59
|
+
* @param options Thresholding configuration options
|
|
60
|
+
*/
|
|
61
|
+
threshold(options?: Partial<ThresholdOptions>): this;
|
|
62
|
+
/** Adaptive thresholding to convert image to binary
|
|
63
|
+
* @description Usage order: (after) grayscale (and optionally blur)
|
|
64
|
+
* @param options Adaptive thresholding configuration options
|
|
65
|
+
*/
|
|
66
|
+
adaptiveThreshold(options?: Partial<AdaptiveThresholdOptions>): this;
|
|
67
|
+
/**
|
|
68
|
+
* Canny edge detection to detect edges in the image
|
|
69
|
+
* @description Usage order: (after) grayscale + blur
|
|
70
|
+
* @param options Canny edge detection configuration options
|
|
71
|
+
*/
|
|
72
|
+
canny(options?: Partial<CannyOptions>): this;
|
|
73
|
+
/**
|
|
74
|
+
* Morphological gradient to highlight the edges in the image
|
|
75
|
+
* @description Usage order: (after) dilation + erosion (or threshold)
|
|
76
|
+
* @param options Morphological gradient configuration options
|
|
77
|
+
*/
|
|
78
|
+
morphologicalGradient(options?: Partial<MorphologicalGradientOptions>): this;
|
|
79
|
+
/**
|
|
80
|
+
* Erode image to reduce noise
|
|
81
|
+
* @description Usage order: (after) threshold or edge detection
|
|
82
|
+
* @param options Erosion configuration options
|
|
83
|
+
*/
|
|
84
|
+
erode(options?: Partial<ErodeOptions>): this;
|
|
85
|
+
/**
|
|
86
|
+
* Dilate image to increase the size of the foreground object
|
|
87
|
+
* @description Usage order: (after) threshold or edge detection
|
|
88
|
+
* @param options Dilation configuration options
|
|
89
|
+
*/
|
|
90
|
+
dilate(options?: Partial<DilateOptions>): this;
|
|
91
|
+
/**
|
|
92
|
+
* Resize image to a new width and height
|
|
93
|
+
* @description Usage order: independent
|
|
94
|
+
* @param options Resize configuration options
|
|
95
|
+
*/
|
|
96
|
+
resize(options: ResizeOptions): this;
|
|
97
|
+
/**
|
|
98
|
+
* Warp image to a new perspective
|
|
99
|
+
* @description Usage order: independent
|
|
100
|
+
* @param options Warp configuration options
|
|
101
|
+
*/
|
|
102
|
+
warp(options: WarpOptions): this;
|
|
103
|
+
/**
|
|
104
|
+
* Destroy the image (cv.Mat) stored in image processor state
|
|
105
|
+
* @kind non-chainable
|
|
106
|
+
* @returns void
|
|
107
|
+
*/
|
|
108
|
+
destroy(): void;
|
|
109
|
+
/**
|
|
110
|
+
* Convert image to cv.Mat
|
|
111
|
+
* @kind non-chainable
|
|
112
|
+
* @returns cv.Mat
|
|
113
|
+
*/
|
|
114
|
+
toMat(): cv.Mat;
|
|
115
|
+
/**
|
|
116
|
+
* Convert image (cv.Mat) to Canvas
|
|
117
|
+
* @kind non-chainable
|
|
118
|
+
* @returns Canvas
|
|
119
|
+
*/
|
|
120
|
+
toCanvas(): Canvas;
|
|
121
|
+
}
|
|
122
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class ImageProcessor{img;width;height;constructor(source){if(source instanceof Canvas){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 prepareCanvas(file){let img=await loadImage(file);let canvas=createCanvas(img.width,img.height);let ctx=canvas.getContext("2d");ctx.drawImage(img,0,0);return canvas}static async initRuntime(){return new Promise((res)=>{if(cv&&cv.Mat){res()}else{cv["onRuntimeInitialized"]=()=>{res()}}})}execute(operationName,options){if(!registry.hasOperation(operationName)){throw new Error(`Operation "${operationName}" not found`)}try{let result=executeOperation(operationName,this.img,options);this.img=result.img;this.width=result.width;this.height=result.height}catch(error){console.error(`Error executing operation "${operationName}":`,error);throw error}return this}grayscale(options={}){return this.execute("grayscale",options)}invert(options={}){return this.execute("invert",options)}border(options={}){return this.execute("border",options)}blur(options={}){return this.execute("blur",options)}threshold(options={}){return this.execute("threshold",options)}adaptiveThreshold(options={}){return this.execute("adaptiveThreshold",options)}canny(options={}){return this.execute("canny",options)}morphologicalGradient(options={}){return this.execute("morphologicalGradient",options)}erode(options={}){return this.execute("erode",options)}dilate(options={}){return this.execute("dilate",options)}resize(options){return this.execute("resize",options)}warp(options){return this.execute("warp",options)}destroy(){this.img.delete()}toMat(){return this.img}toCanvas(){let canvas=createCanvas(this.width,this.height);let ctx=canvas.getContext("2d");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}}import{Canvas,createCanvas,cv,loadImage}from"@/index";import{executeOperation,registry}from"@/index";
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import cv from "@techstark/opencv-js";
|
|
2
|
+
export { cv };
|
|
3
|
+
export type { BoundingBox, Coordinate, Points } from "@/index.interface";
|
|
4
|
+
export { executeOperation, OperationRegistry, registry } from "@/pipeline";
|
|
5
|
+
export { Canvas, createCanvas, loadImage } from "@napi-rs/canvas";
|
|
6
|
+
export type { SKRSContext2D } from "@napi-rs/canvas";
|
|
7
|
+
export { CanvasToolkit } from "@/canvas-toolkit";
|
|
8
|
+
export { Contours } from "@/contours";
|
|
9
|
+
export { calculateMeanGrayscaleValue, calculateMeanNormalizedLabLightness, type CalculateMeanLightnessOptions, } from "@/image-analysis";
|
|
10
|
+
export { ImageProcessor } from "@/image-processor";
|
|
11
|
+
export type { AdaptiveThresholdOptions, BlurOptions, BorderOptions, CannyOptions, DilateOptions, ErodeOptions, GrayscaleOptions, InvertOptions, MorphologicalGradientOptions, OperationFunction, OperationName, OperationOptions, OperationResult, PartialOptions, RegisteredOperations, RequiredOptions, ResizeOptions, ThresholdOptions, WarpOptions, } from "@/pipeline";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface Coordinate {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
export interface Points {
|
|
6
|
+
topLeft: Coordinate;
|
|
7
|
+
topRight: Coordinate;
|
|
8
|
+
bottomLeft: Coordinate;
|
|
9
|
+
bottomRight: Coordinate;
|
|
10
|
+
}
|
|
11
|
+
export interface BoundingBox {
|
|
12
|
+
x0: number;
|
|
13
|
+
y0: number;
|
|
14
|
+
x1: number;
|
|
15
|
+
y1: number;
|
|
16
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import cv from"@techstark/opencv-js";export{cv};export{executeOperation,OperationRegistry,registry}from"@/pipeline";export{Canvas,createCanvas,loadImage}from"@napi-rs/canvas";export{CanvasToolkit}from"@/canvas-toolkit";export{Contours}from"@/contours";export{calculateMeanGrayscaleValue,calculateMeanNormalizedLabLightness}from"@/image-analysis";export{ImageProcessor}from"@/image-processor";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { OperationResult, PartialOptions } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
declare module "@/pipeline/types" {
|
|
4
|
+
interface RegisteredOperations {
|
|
5
|
+
adaptiveThreshold: AdaptiveThresholdOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface AdaptiveThresholdOptions extends PartialOptions {
|
|
9
|
+
/** Upper threshold value (0-255) */
|
|
10
|
+
upper: number;
|
|
11
|
+
/** Adaptive threshold method (cv.ADAPTIVE_THRESH_...) */
|
|
12
|
+
method: cv.AdaptiveThresholdTypes;
|
|
13
|
+
/** Type of thresholding (cv.THRESH_...) */
|
|
14
|
+
type: cv.ThresholdTypes;
|
|
15
|
+
/** Block size for adaptive thresholding (must be odd) */
|
|
16
|
+
size: number;
|
|
17
|
+
/** Constant subtracted from the mean or weighted mean */
|
|
18
|
+
constant: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function adaptiveThreshold(img: cv.Mat, options: AdaptiveThresholdOptions): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cv,registry}from"@/index";function defaultOptions(){return{upper:255,method:cv.ADAPTIVE_THRESH_GAUSSIAN_C,type:cv.THRESH_BINARY_INV,size:7,constant:2}}export function adaptiveThreshold(img,options){let imgAdaptiveThreshold=new cv.Mat;cv.adaptiveThreshold(img,imgAdaptiveThreshold,options.upper,options.method,options.type,options.size,options.constant);img.delete();return{img:imgAdaptiveThreshold,width:imgAdaptiveThreshold.cols,height:imgAdaptiveThreshold.rows}}registry.register("adaptiveThreshold",adaptiveThreshold,defaultOptions);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OperationResult, PartialOptions } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
declare module "@/pipeline/types" {
|
|
4
|
+
interface RegisteredOperations {
|
|
5
|
+
blur: BlurOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface BlurOptions extends PartialOptions {
|
|
9
|
+
/** Size of the blur [x, y] */
|
|
10
|
+
size: [number, number];
|
|
11
|
+
/** Gaussian kernel standard deviation on x axis */
|
|
12
|
+
sigma: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function blur(img: cv.Mat, options: BlurOptions): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cv,registry}from"@/index";function defaultOptions(){return{size:[5,5],sigma:0}}export function blur(img,options){let imgBlur=new cv.Mat;cv.GaussianBlur(img,imgBlur,new cv.Size(options.size[0],options.size[1]),options.sigma);img.delete();return{img:imgBlur,width:imgBlur.cols,height:imgBlur.rows}}registry.register("blur",blur,defaultOptions);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { OperationResult, PartialOptions } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
declare module "@/pipeline/types" {
|
|
4
|
+
interface RegisteredOperations {
|
|
5
|
+
border: BorderOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface BorderOptions extends PartialOptions {
|
|
9
|
+
/** Size of the border in pixels */
|
|
10
|
+
size: number;
|
|
11
|
+
/** Border type (e.g., cv.BORDER_CONSTANT) */
|
|
12
|
+
borderType: cv.BorderTypes;
|
|
13
|
+
/** Border color in [B, G, R, A] format */
|
|
14
|
+
borderColor: [cv.int, cv.int, cv.int, cv.int];
|
|
15
|
+
}
|
|
16
|
+
export declare function border(img: cv.Mat, options: BorderOptions): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cv,registry}from"@/index";function defaultOptions(){return{size:10,borderType:cv.BORDER_CONSTANT,borderColor:[255,255,255,255]}}export function border(img,options){let imgBorder=new cv.Mat;cv.copyMakeBorder(img,imgBorder,options.size,options.size,options.size,options.size,options.borderType,options.borderColor);img.delete();return{img:imgBorder,width:imgBorder.cols,height:imgBorder.rows}}registry.register("border",border,defaultOptions);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OperationResult, PartialOptions } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
declare module "@/pipeline/types" {
|
|
4
|
+
interface RegisteredOperations {
|
|
5
|
+
canny: CannyOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface CannyOptions extends PartialOptions {
|
|
9
|
+
/** Lower threshold for the hysteresis procedure (0-255) */
|
|
10
|
+
lower: number;
|
|
11
|
+
/** Upper threshold for the hysteresis procedure (0-255) */
|
|
12
|
+
upper: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function canny(img: cv.Mat, options: CannyOptions): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cv,registry}from"@/index";function defaultOptions(){return{lower:50,upper:150}}export function canny(img,options){let imgCanny=new cv.Mat;cv.Canny(img,imgCanny,options.lower,options.upper);img.delete();return{img:imgCanny,width:imgCanny.cols,height:imgCanny.rows}}registry.register("canny",canny,defaultOptions);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OperationResult, PartialOptions } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
declare module "@/pipeline/types" {
|
|
4
|
+
interface RegisteredOperations {
|
|
5
|
+
dilate: DilateOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface DilateOptions extends PartialOptions {
|
|
9
|
+
/** Size of the block [x, y] */
|
|
10
|
+
size: [number, number];
|
|
11
|
+
/** Number of iterations for the dilation operation */
|
|
12
|
+
iter: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function dilate(img: cv.Mat, options: DilateOptions): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cv,registry}from"@/index";function defaultOptions(){return{size:[5,5],iter:1}}export function dilate(img,options){let imgDilate=new cv.Mat;let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(options.size[0],options.size[1]));cv.dilate(img,imgDilate,kernel,new cv.Point(-1,-1),options.iter);img.delete();return{img:imgDilate,width:imgDilate.cols,height:imgDilate.rows}}registry.register("dilate",dilate,defaultOptions);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OperationResult, PartialOptions } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
declare module "@/pipeline/types" {
|
|
4
|
+
interface RegisteredOperations {
|
|
5
|
+
erode: ErodeOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface ErodeOptions extends PartialOptions {
|
|
9
|
+
/** Size of the block [x, y] */
|
|
10
|
+
size: [number, number];
|
|
11
|
+
/** Number of iterations for the erosion operation */
|
|
12
|
+
iter: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function erode(img: cv.Mat, options: ErodeOptions): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cv,registry}from"@/index";function defaultOptions(){return{size:[5,5],iter:1}}export function erode(img,options){let imgErode=new cv.Mat;let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(options.size[0],options.size[1]));cv.erode(img,imgErode,kernel,new cv.Point(-1,-1),options.iter);img.delete();return{img:imgErode,width:imgErode.cols,height:imgErode.rows}}registry.register("erode",erode,defaultOptions);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { OperationResult, PartialOptions } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
declare module "@/pipeline/types" {
|
|
4
|
+
interface RegisteredOperations {
|
|
5
|
+
grayscale: GrayscaleOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface GrayscaleOptions extends PartialOptions {
|
|
9
|
+
}
|
|
10
|
+
export declare function grayscale(img: cv.Mat, options: GrayscaleOptions): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cv,registry}from"@/index";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);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { OperationResult, PartialOptions } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
declare module "@/pipeline/types" {
|
|
4
|
+
interface RegisteredOperations {
|
|
5
|
+
invert: InvertOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface InvertOptions extends PartialOptions {
|
|
9
|
+
}
|
|
10
|
+
export declare function invert(img: cv.Mat, options: InvertOptions): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cv,registry}from"@/index";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);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { OperationResult, PartialOptions } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
declare module "@/pipeline/types" {
|
|
4
|
+
interface RegisteredOperations {
|
|
5
|
+
morphologicalGradient: MorphologicalGradientOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface MorphologicalGradientOptions extends PartialOptions {
|
|
9
|
+
/** Kernel size for the morphological gradient operation [x, y] */
|
|
10
|
+
size: [number, number];
|
|
11
|
+
}
|
|
12
|
+
export declare function morphologicalGradient(img: cv.Mat, options: MorphologicalGradientOptions): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cv,registry}from"@/index";function defaultOptions(){return{size:[3,3]}}export function morphologicalGradient(img,options){let imgMorphologicalGradient=new cv.Mat;let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(options.size[0],options.size[1]));cv.morphologyEx(img,imgMorphologicalGradient,cv.MORPH_GRADIENT,kernel);img.delete();return{img:imgMorphologicalGradient,width:imgMorphologicalGradient.cols,height:imgMorphologicalGradient.rows}}registry.register("morphologicalGradient",morphologicalGradient,defaultOptions);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OperationResult, RequiredOptions } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
declare module "@/pipeline/types" {
|
|
4
|
+
interface RegisteredOperations {
|
|
5
|
+
resize: ResizeOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface ResizeOptions extends RequiredOptions {
|
|
9
|
+
/** Width of the resized image */
|
|
10
|
+
width: number;
|
|
11
|
+
/** Height of the resized image */
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function resize(img: cv.Mat, options: ResizeOptions): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cv,registry}from"@/index";export function resize(img,options){if(!options.width||!options.height){throw new Error("Invalid options: width and height are required")}let imgResize=new cv.Mat;cv.resize(img,imgResize,new cv.Size(options.width,options.height));img.delete();return{img:imgResize,width:imgResize.cols,height:imgResize.rows}}registry.register("resize",resize);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { OperationResult, PartialOptions } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
declare module "@/pipeline/types" {
|
|
4
|
+
interface RegisteredOperations {
|
|
5
|
+
threshold: ThresholdOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface ThresholdOptions extends PartialOptions {
|
|
9
|
+
/** Lower threshold value (0-255) */
|
|
10
|
+
lower: number;
|
|
11
|
+
/** Upper threshold value (0-255) */
|
|
12
|
+
upper: number;
|
|
13
|
+
/** Type of thresholding (cv.THRESH_...) */
|
|
14
|
+
type: cv.ThresholdTypes;
|
|
15
|
+
}
|
|
16
|
+
export declare function threshold(img: cv.Mat, options: ThresholdOptions): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cv,registry}from"@/index";function defaultOptions(){return{lower:0,upper:255,type:cv.THRESH_BINARY_INV+cv.THRESH_OTSU}}export function threshold(img,options){let imgThreshold=new cv.Mat;cv.threshold(img,imgThreshold,options.lower,options.upper,options.type);img.delete();return{img:imgThreshold,width:imgThreshold.cols,height:imgThreshold.rows}}registry.register("threshold",threshold,defaultOptions);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BoundingBox, OperationResult, Points, RequiredOptions } from "@/index";
|
|
2
|
+
import { cv } from "@/index";
|
|
3
|
+
declare module "@/pipeline/types" {
|
|
4
|
+
interface RegisteredOperations {
|
|
5
|
+
warp: WarpOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface WarpOptions extends RequiredOptions {
|
|
9
|
+
/** Four points of the source image containing x and y point in
|
|
10
|
+
* topLeft, topRight, bottomLeft and BottomRight.
|
|
11
|
+
* Use Contours class instance to get the points
|
|
12
|
+
*/
|
|
13
|
+
points: Points;
|
|
14
|
+
/** A destination canvas bounding box for cropping the original canvas */
|
|
15
|
+
bbox: BoundingBox;
|
|
16
|
+
}
|
|
17
|
+
export declare function warp(img: cv.Mat, options: WarpOptions): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cv,registry}from"@/index";export function warp(img,options){if(!options.points||!options.bbox){throw new Error("Invalid options: points and bbox are required")}const{points,bbox}=options;let imgWarp=new cv.Mat;let targetWidth=bbox.x1-bbox.x0;let targetHeight=bbox.y1-bbox.y0;let destArray=[0,0,targetWidth-1,0,targetWidth-1,targetHeight-1,0,targetHeight-1];let srcArray=[points.topLeft.x,points.topLeft.y,points.topRight.x,points.topRight.y,points.bottomRight.x,points.bottomRight.y,points.bottomLeft.x,points.bottomLeft.y];let dest=cv.matFromArray(4,1,cv.CV_32FC2,destArray);let src=cv.matFromArray(4,1,cv.CV_32FC2,srcArray);let M=cv.getPerspectiveTransform(src,dest);let dsize=new cv.Size(targetWidth,targetHeight);cv.warpPerspective(img,imgWarp,M,dsize);M.delete();src.delete();dest.delete();img.delete();return{img:imgWarp,width:imgWarp.cols,height:imgWarp.rows}}registry.register("warp",warp);
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ppu-ocv",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A type-safe, modular, chainable image processing library built on top of OpenCV.js with a fluent API leveraging pipeline processing.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"open-cv",
|
|
7
|
+
"image-processor",
|
|
8
|
+
"canvas",
|
|
9
|
+
"threshold",
|
|
10
|
+
"computer-vision",
|
|
11
|
+
"opencv-js",
|
|
12
|
+
"perspective-transformation",
|
|
13
|
+
"edge-detection",
|
|
14
|
+
"contours",
|
|
15
|
+
"pipeline",
|
|
16
|
+
"type-safe",
|
|
17
|
+
"modular",
|
|
18
|
+
"chainable",
|
|
19
|
+
"bun"
|
|
20
|
+
],
|
|
21
|
+
"author": "snowfluke",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./index.js",
|
|
25
|
+
"types": "./index.d.ts",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"task": "bun scripts/task.ts",
|
|
28
|
+
"build:test": "bun task build && bun test",
|
|
29
|
+
"build:publish": "bun task build && bun task report-size && bun task publish",
|
|
30
|
+
"lint": "eslint ./src",
|
|
31
|
+
"lint:fix": "eslint ./src --fix"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@stylistic/eslint-plugin": "latest",
|
|
35
|
+
"@types/bun": "latest",
|
|
36
|
+
"@types/uglify-js": "latest",
|
|
37
|
+
"eslint": "latest",
|
|
38
|
+
"eslint-plugin-jsdoc": "latest",
|
|
39
|
+
"mitata": "latest",
|
|
40
|
+
"tsx": "latest",
|
|
41
|
+
"typescript": "latest",
|
|
42
|
+
"typescript-eslint": "latest",
|
|
43
|
+
"uglify-js": ">=2.4.24"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/PT-Perkasa-Pilar-Utama/ppu-ocv.git"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@napi-rs/canvas": "^0.1.69",
|
|
51
|
+
"@techstark/opencv-js": "^4.10.0-release.1"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export { executeOperation, OperationRegistry, registry } from "./registry";
|
|
2
|
+
export type { OperationFunction, OperationName, OperationOptions, OperationResult, PartialOptions, RegisteredOperations, RequiredOptions, } from "./types";
|
|
3
|
+
import "@/operations/adaptive-threshold";
|
|
4
|
+
import "@/operations/blur";
|
|
5
|
+
import "@/operations/border";
|
|
6
|
+
import "@/operations/canny";
|
|
7
|
+
import "@/operations/dilate";
|
|
8
|
+
import "@/operations/erode";
|
|
9
|
+
import "@/operations/grayscale";
|
|
10
|
+
import "@/operations/invert";
|
|
11
|
+
import "@/operations/morphological-gradient";
|
|
12
|
+
import "@/operations/resize";
|
|
13
|
+
import "@/operations/threshold";
|
|
14
|
+
import "@/operations/warp";
|
|
15
|
+
export type { AdaptiveThresholdOptions } from "@/operations/adaptive-threshold";
|
|
16
|
+
export type { BlurOptions } from "@/operations/blur";
|
|
17
|
+
export type { BorderOptions } from "@/operations/border";
|
|
18
|
+
export type { CannyOptions } from "@/operations/canny";
|
|
19
|
+
export type { DilateOptions } from "@/operations/dilate";
|
|
20
|
+
export type { ErodeOptions } from "@/operations/erode";
|
|
21
|
+
export type { GrayscaleOptions } from "@/operations/grayscale";
|
|
22
|
+
export type { InvertOptions } from "@/operations/invert";
|
|
23
|
+
export type { MorphologicalGradientOptions } from "@/operations/morphological-gradient";
|
|
24
|
+
export type { ResizeOptions } from "@/operations/resize";
|
|
25
|
+
export type { ThresholdOptions } from "@/operations/threshold";
|
|
26
|
+
export type { WarpOptions } from "@/operations/warp";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{executeOperation,OperationRegistry,registry}from"./registry";import"@/operations/adaptive-threshold";import"@/operations/blur";import"@/operations/border";import"@/operations/canny";import"@/operations/dilate";import"@/operations/erode";import"@/operations/grayscale";import"@/operations/invert";import"@/operations/morphological-gradient";import"@/operations/resize";import"@/operations/threshold";import"@/operations/warp";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OperationFunction, OperationName, OperationOptions, OperationResult } from "@/index";
|
|
2
|
+
import cv from "@techstark/opencv-js";
|
|
3
|
+
export declare class OperationRegistry {
|
|
4
|
+
private operations;
|
|
5
|
+
private defaultOptions;
|
|
6
|
+
register<Name extends OperationName>(name: Name, operation: OperationFunction<OperationOptions<Name>>, defaultOptions?: () => Partial<OperationOptions<Name>>): void;
|
|
7
|
+
getOperation(name: string): OperationFunction<any> | undefined;
|
|
8
|
+
getDefaultOptionsGenerator(name: string): any;
|
|
9
|
+
hasOperation(name: string): boolean;
|
|
10
|
+
getOperationNames(): OperationName[];
|
|
11
|
+
}
|
|
12
|
+
export declare const registry: OperationRegistry;
|
|
13
|
+
export declare function executeOperation<Name extends OperationName>(operationName: Name, img: cv.Mat, options?: Partial<OperationOptions<Name>>): OperationResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class OperationRegistry{operations=new Map;defaultOptions=new Map;register(name,operation,defaultOptions){this.operations.set(name,operation);if(defaultOptions){this.defaultOptions.set(name,defaultOptions)}}getOperation(name){return this.operations.get(name)}getDefaultOptionsGenerator(name){return this.defaultOptions.get(name)||{}}hasOperation(name){return this.operations.has(name)}getOperationNames(){return Array.from(this.operations.keys())}}export let registry=new OperationRegistry;export function executeOperation(operationName,img,options){let operation=registry.getOperation(operationName);if(!operation){throw new Error(`Operation "${operationName}" not found in registry`)}let maybeGenerator=registry.getDefaultOptionsGenerator(operationName);let defaultOptionsGenerator=typeof maybeGenerator==="function"?maybeGenerator:()=>({});let mergedOptions={...defaultOptionsGenerator(),...options};return operation(img,mergedOptions)}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import cv from "@techstark/opencv-js";
|
|
2
|
+
export interface OperationResult {
|
|
3
|
+
img: cv.Mat;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
declare const RequiredBrand: unique symbol;
|
|
8
|
+
export interface RequiredOptions {
|
|
9
|
+
[RequiredBrand]?: never;
|
|
10
|
+
}
|
|
11
|
+
declare const PartialBrand: unique symbol;
|
|
12
|
+
export interface PartialOptions {
|
|
13
|
+
[PartialBrand]?: never;
|
|
14
|
+
}
|
|
15
|
+
export type OperationFunction<T> = (img: cv.Mat, options: T) => OperationResult;
|
|
16
|
+
/**
|
|
17
|
+
* @description
|
|
18
|
+
* Central registry mapping operation names to their specific option types.
|
|
19
|
+
* Operation modules MUST augment this interface.
|
|
20
|
+
*/
|
|
21
|
+
export interface RegisteredOperations {
|
|
22
|
+
}
|
|
23
|
+
export type OperationName = keyof RegisteredOperations;
|
|
24
|
+
export type OperationOptions<N extends OperationName> = RegisteredOperations[N];
|
|
25
|
+
export {};
|