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.
Files changed (42) hide show
  1. package/README.md +218 -0
  2. package/canvas-toolkit.d.ts +98 -0
  3. package/canvas-toolkit.js +1 -0
  4. package/contours.d.ts +70 -0
  5. package/contours.js +1 -0
  6. package/image-analysis.d.ts +35 -0
  7. package/image-analysis.js +1 -0
  8. package/image-processor.d.ts +122 -0
  9. package/image-processor.js +1 -0
  10. package/index.d.ts +11 -0
  11. package/index.interface.d.ts +16 -0
  12. package/index.js +1 -0
  13. package/operations/adaptive-threshold.d.ts +20 -0
  14. package/operations/adaptive-threshold.js +1 -0
  15. package/operations/blur.d.ts +14 -0
  16. package/operations/blur.js +1 -0
  17. package/operations/border.d.ts +16 -0
  18. package/operations/border.js +1 -0
  19. package/operations/canny.d.ts +14 -0
  20. package/operations/canny.js +1 -0
  21. package/operations/dilate.d.ts +14 -0
  22. package/operations/dilate.js +1 -0
  23. package/operations/erode.d.ts +14 -0
  24. package/operations/erode.js +1 -0
  25. package/operations/grayscale.d.ts +10 -0
  26. package/operations/grayscale.js +1 -0
  27. package/operations/invert.d.ts +10 -0
  28. package/operations/invert.js +1 -0
  29. package/operations/morphological-gradient.d.ts +12 -0
  30. package/operations/morphological-gradient.js +1 -0
  31. package/operations/resize.d.ts +14 -0
  32. package/operations/resize.js +1 -0
  33. package/operations/threshold.d.ts +16 -0
  34. package/operations/threshold.js +1 -0
  35. package/operations/warp.d.ts +17 -0
  36. package/operations/warp.js +1 -0
  37. package/package.json +53 -0
  38. package/pipeline/index.d.ts +26 -0
  39. package/pipeline/index.js +1 -0
  40. package/pipeline/registry.d.ts +13 -0
  41. package/pipeline/registry.js +1 -0
  42. 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
+ ![ppu-ocv pipeline demo](https://raw.githubusercontent.com/PT-Perkasa-Pilar-Utama/ppu-ocv/refs/heads/main/assets/ppu-ocv-demo.jpg)
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 {};