ppu-ocv 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -165,6 +165,42 @@ See: [How to extend ppu-ocv operations](./docs/how-to-extend-ppu-ocv-operations.
165
165
  | `getCornerPoints` | Canvas, contour | Get four corner points for a given contour. Useful for perspective transformation (warp). |
166
166
  | `destroy` | | Destroy & clean-up the memory from the contours |
167
167
 
168
+ #### `FaceDetector`
169
+
170
+ Singleton class for face and eye detection using Haar Cascade classifiers.
171
+
172
+ | Method | Args | Description |
173
+ | ------------- | --------------------------- | ---------------------------------------------------------------------- |
174
+ | `getInstance` | | Get the singleton instance of FaceDetector |
175
+ | `detectFace` | Canvas, options | Detect faces in the given canvas, returns `{ faces: BoundingBox[] }` |
176
+ | `detectEye` | Canvas, options | Detect eyes in the given canvas, returns `{ eyes: { left, right }[] }` |
177
+ | `alignFace` | Canvas, [leftEye, rightEye] | Align face based on eye positions, returns aligned Canvas |
178
+
179
+ **Usage Example:**
180
+
181
+ ```ts
182
+ import { FaceDetector, ImageProcessor } from "ppu-ocv";
183
+
184
+ await ImageProcessor.initRuntime();
185
+ const file = Bun.file("./image.jpg");
186
+ const canvas = await ImageProcessor.prepareCanvas(await file.arrayBuffer());
187
+
188
+ const faceDetector = await FaceDetector.getInstance();
189
+
190
+ // Detect faces
191
+ const faceResult = await faceDetector.detectFace(canvas);
192
+ console.log(`Found ${faceResult.faces.length} faces`);
193
+
194
+ // Detect eyes
195
+ const eyeResult = await faceDetector.detectEye(canvas);
196
+
197
+ // Align face if eyes detected
198
+ if (eyeResult.eyes.length > 0) {
199
+ const { left, right } = eyeResult.eyes[0];
200
+ const alignedCanvas = await faceDetector.alignFace(canvas, [left, right]);
201
+ }
202
+ ```
203
+
168
204
  #### `ImageAnalysis`
169
205
 
170
206
  Just a collection of utility functions for analyzing image properties.
@@ -0,0 +1,70 @@
1
+ import { Canvas } from "./index";
2
+ import type { BoundingBox, EyesDetectorResult, FaceDetectorResult } from "./index.interface";
3
+ export declare class FaceDetector {
4
+ private static instance;
5
+ private frontalFaceCascade;
6
+ private eyeCascade;
7
+ private eyeGlassCascade;
8
+ /**
9
+ * Private constructor to prevent direct instantiation
10
+ */
11
+ private constructor();
12
+ /**
13
+ * Get the singleton instance of FaceDetector
14
+ * @returns The singleton instance
15
+ * @example
16
+ * const faceDetector = await FaceDetector.getInstance();
17
+ */
18
+ static getInstance(): Promise<FaceDetector>;
19
+ /**
20
+ * Detect faces in the given canvas
21
+ * @param canvas Source canvas containing the image
22
+ * @param options Detection options
23
+ * @returns Promise resolving to detected faces
24
+ * @example
25
+ * const result = await faceDetector.detectFace(canvas);
26
+ * console.log(`Found ${result.faces.length} faces`);
27
+ */
28
+ detectFace(canvas: Canvas, options?: {
29
+ scaleFactor?: number;
30
+ minNeighbors?: number;
31
+ minSize?: {
32
+ width: number;
33
+ height: number;
34
+ };
35
+ }): Promise<FaceDetectorResult>;
36
+ /**
37
+ * Detect eyes in the given canvas
38
+ * @param canvas Source canvas containing the image
39
+ * @param options Detection options
40
+ * @returns Promise resolving to detected eyes
41
+ * @example
42
+ * const result = await faceDetector.detectEye(canvas);
43
+ * console.log(`Found ${result.eyes.length} eye pairs`);
44
+ */
45
+ detectEye(canvas: Canvas, options?: {
46
+ scaleFactor?: number;
47
+ minNeighbors?: number;
48
+ minSize?: {
49
+ width: number;
50
+ height: number;
51
+ };
52
+ useFallback?: boolean;
53
+ }): Promise<EyesDetectorResult>;
54
+ /**
55
+ * Detect eyes with glasses/sunglasses
56
+ * @param gray Grayscale cv.Mat image
57
+ * @param options Detection options
58
+ * @returns Detected eyes result
59
+ */
60
+ private detectEyeSunglass;
61
+ /**
62
+ * Align face based on eye positions
63
+ * @param canvas Source canvas containing the face
64
+ * @param eyes Tuple of left and right eye bounding boxes
65
+ * @returns Promise resolving to aligned face canvas
66
+ * @example
67
+ * const aligned = await faceDetector.alignFace(canvas, [leftEye, rightEye]);
68
+ */
69
+ alignFace(canvas: Canvas, eyes: [BoundingBox, BoundingBox]): Promise<Canvas>;
70
+ }
@@ -0,0 +1 @@
1
+ export class FaceDetector{static instance=null;frontalFaceCascade;eyeCascade;eyeGlassCascade;constructor(){let basePath=join(process.cwd(),"src");let frontalFaceData=readFileSync(join(basePath,"haarcascade_frontalface_default.xml"));let eyeData=readFileSync(join(basePath,"haarcascade_eye.xml"));let eyeGlassData=readFileSync(join(basePath,"haarcascade_eye_tree_eyeglasses.xml"));cv.FS.writeFile("haarcascade_frontalface_default.xml",frontalFaceData);cv.FS.writeFile("haarcascade_eye.xml",eyeData);cv.FS.writeFile("haarcascade_eye_tree_eyeglasses.xml",eyeGlassData);this.frontalFaceCascade=new cv.CascadeClassifier;this.eyeCascade=new cv.CascadeClassifier;this.eyeGlassCascade=new cv.CascadeClassifier;this.frontalFaceCascade.load("haarcascade_frontalface_default.xml");this.eyeCascade.load("haarcascade_eye.xml");this.eyeGlassCascade.load("haarcascade_eye_tree_eyeglasses.xml")}static async getInstance(){await ImageProcessor.initRuntime();if(!FaceDetector.instance){FaceDetector.instance=new FaceDetector}return FaceDetector.instance}async detectFace(canvas,options={}){const{scaleFactor=1.1,minNeighbors=3,minSize={width:30,height:30}}=options;let processor=new ImageProcessor(canvas);let faces=new cv.RectVector;let minSizeMat=new cv.Size(minSize.width,minSize.height);let detectedFaces=[];try{let grayScaled=processor.grayscale().toMat();this.frontalFaceCascade.detectMultiScale(grayScaled,faces,scaleFactor,minNeighbors,0,minSizeMat);for(let i=0;i<faces.size();i++){let face=faces.get(i);detectedFaces.push({x0:face.x,y0:face.y,x1:face.x+face.width,y1:face.y+face.height})}}catch(error){}finally{faces.delete();processor.destroy()}return{faces:detectedFaces}}async detectEye(canvas,options={}){const{scaleFactor=1.05,minNeighbors=3,minSize={width:10,height:10},useFallback=true}=options;let processor=new ImageProcessor(canvas);let grayscaled=processor.grayscale().toMat();let eyes=new cv.RectVector;let minSizeMat=new cv.Size(minSize.width,minSize.height);let result;try{this.eyeCascade.detectMultiScale(grayscaled,eyes,scaleFactor,minNeighbors,0,minSizeMat);if(eyes.size()===0&&useFallback){let eyesWithGlasses=this.detectEyeSunglass(grayscaled,{scaleFactor,minNeighbors,minSize});result=eyesWithGlasses}else{let detectedEyes=[];let eyeArray=[];for(let i=0;i<eyes.size();i++){let eye=eyes.get(i);eyeArray.push({x0:eye.x,y0:eye.y,x1:eye.x+eye.width,y1:eye.y+eye.height})}eyeArray.sort((a,b)=>a.x0-b.x0);for(let i=0;i<eyeArray.length-1;i+=2){detectedEyes.push({left:eyeArray[i],right:eyeArray[i+1]})}result={eyes:detectedEyes}}}catch(error){console.error(`[detectEyes error]: `,error);result={eyes:[]}}finally{eyes.delete();processor.destroy()}return result}detectEyeSunglass(grayscale,options){const{scaleFactor,minNeighbors,minSize}=options;let eyes=new cv.RectVector;let minSizeMat=new cv.Size(minSize.width,minSize.height);this.eyeGlassCascade.detectMultiScale(grayscale,eyes,scaleFactor,minNeighbors,0,minSizeMat);let detectedEyes=[];let eyeArray=[];for(let i=0;i<eyes.size();i++){let eye=eyes.get(i);eyeArray.push({x0:eye.x,y0:eye.y,x1:eye.x+eye.width,y1:eye.y+eye.height})}eyeArray.sort((a,b)=>a.x0-b.x0);for(let i=0;i<eyeArray.length-1;i+=2){detectedEyes.push({left:eyeArray[i],right:eyeArray[i+1]})}return{eyes:detectedEyes}}async alignFace(canvas,eyes){const[leftEye,rightEye]=eyes;let leftEyeCenter={x:(leftEye.x0+leftEye.x1)/2,y:(leftEye.y0+leftEye.y1)/2};let rightEyeCenter={x:(rightEye.x0+rightEye.x1)/2,y:(rightEye.y0+rightEye.y1)/2};let deltaX=rightEyeCenter.x-leftEyeCenter.x;let deltaY=rightEyeCenter.y-leftEyeCenter.y;let angle=Math.atan2(deltaY,deltaX)*(180/Math.PI);let centerX=(leftEyeCenter.x+rightEyeCenter.x)/2;let centerY=(leftEyeCenter.y+rightEyeCenter.y)/2;let center=new cv.Point(centerX,centerY);let processor=new ImageProcessor(canvas);let rotated=processor.rotate({angle,center}).toCanvas();return rotated}}import{readFileSync}from"fs";import{join}from"path";import{cv,ImageProcessor}from"./index";
@@ -14,7 +14,14 @@ export declare class ImageProcessor {
14
14
  * @param source Source image as Canvas or cv.Mat
15
15
  */
16
16
  constructor(source: Canvas | cv.Mat);
17
+ /**
18
+ * Convert array buffer to canvas
19
+ */
17
20
  static prepareCanvas(file: ArrayBuffer): Promise<Canvas>;
21
+ /**
22
+ * Convert canvas to array buffer
23
+ */
24
+ static prepareBuffer(canvas: Canvas): Promise<ArrayBuffer>;
18
25
  /**
19
26
  * Initialize OpenCV runtime, this is recommended to be called before any image processing
20
27
  */
@@ -1 +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)}rotate(options){return this.execute("rotate",options)}convert(options){return this.execute("convert",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";
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){if(file instanceof Canvas)return 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 prepareBuffer(canvas){if(canvas instanceof ArrayBuffer)return canvas;if(typeof canvas.toBuffer==="function"){let buffer=canvas.toBuffer("image/png");let arrayBuffer=new ArrayBuffer(buffer.byteLength);new Uint8Array(arrayBuffer).set(new Uint8Array(buffer));return arrayBuffer}if(typeof canvas.toDataURL==="function"){let dataURL=canvas.toDataURL("image/png");let base64Data=dataURL.replace(/^data:image\/png;base64,/,"");let buffer=Buffer.from(base64Data,"base64");let arrayBuffer=new ArrayBuffer(buffer.byteLength);new Uint8Array(arrayBuffer).set(new Uint8Array(buffer));return arrayBuffer}let ctx=canvas.getContext("2d");let imageData=ctx.getImageData(0,0,canvas.width,canvas.height);let canvasBuffer=new ArrayBuffer(imageData.data.byteLength);new Uint8Array(canvasBuffer).set(new Uint8Array(imageData.data.buffer,imageData.data.byteOffset,imageData.data.byteLength));return canvasBuffer}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)}rotate(options){return this.execute("rotate",options)}convert(options){return this.execute("convert",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 CHANGED
@@ -2,10 +2,11 @@ import cv from "@techstark/opencv-js";
2
2
  export { cv };
3
3
  export { Canvas, createCanvas, ImageData, loadImage } from "@napi-rs/canvas";
4
4
  export type { SKRSContext2D } from "@napi-rs/canvas";
5
- export type { BoundingBox, Coordinate, Points } from "./index.interface";
5
+ export type { BoundingBox, Coordinate, Points, EyesDetectorResult, FaceDetectorResult } from "./index.interface";
6
6
  export { executeOperation, OperationRegistry, registry } from "./pipeline";
7
7
  export { CanvasToolkit } from "./canvas-toolkit";
8
8
  export { Contours } from "./contours";
9
9
  export { calculateMeanGrayscaleValue, calculateMeanNormalizedLabLightness, type CalculateMeanLightnessOptions, } from "./image-analysis";
10
10
  export { ImageProcessor } from "./image-processor";
11
+ export { FaceDetector } from "./face-detector";
11
12
  export type { AdaptiveThresholdOptions, BlurOptions, BorderOptions, CannyOptions, DilateOptions, ErodeOptions, GrayscaleOptions, InvertOptions, MorphologicalGradientOptions, OperationFunction, OperationName, OperationOptions, OperationResult, PartialOptions, RegisteredOperations, RequiredOptions, ResizeOptions, RotateOptions, ThresholdOptions, WarpOptions, } from "./pipeline";
@@ -14,3 +14,12 @@ export interface BoundingBox {
14
14
  x1: number;
15
15
  y1: number;
16
16
  }
17
+ export interface FaceDetectorResult {
18
+ faces: BoundingBox[];
19
+ }
20
+ export interface EyesDetectorResult {
21
+ eyes: {
22
+ left: BoundingBox;
23
+ right: BoundingBox;
24
+ }[];
25
+ }
package/index.js CHANGED
@@ -1 +1 @@
1
- import cv from"@techstark/opencv-js";export{cv};export{Canvas,createCanvas,ImageData,loadImage}from"@napi-rs/canvas";export{executeOperation,OperationRegistry,registry}from"./pipeline";export{CanvasToolkit}from"./canvas-toolkit";export{Contours}from"./contours";export{calculateMeanGrayscaleValue,calculateMeanNormalizedLabLightness}from"./image-analysis";export{ImageProcessor}from"./image-processor";
1
+ import cv from"@techstark/opencv-js";export{cv};export{Canvas,createCanvas,ImageData,loadImage}from"@napi-rs/canvas";export{executeOperation,OperationRegistry,registry}from"./pipeline";export{CanvasToolkit}from"./canvas-toolkit";export{Contours}from"./contours";export{calculateMeanGrayscaleValue,calculateMeanNormalizedLabLightness}from"./image-analysis";export{ImageProcessor}from"./image-processor";export{FaceDetector}from"./face-detector";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ppu-ocv",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "A type-safe, modular, chainable image processing library built on top of OpenCV.js with a fluent API leveraging pipeline processing.",
5
5
  "keywords": [
6
6
  "open-cv",
@@ -37,6 +37,7 @@
37
37
  "eslint": "latest",
38
38
  "eslint-plugin-jsdoc": "latest",
39
39
  "mitata": "latest",
40
+ "prettier": "^3.6.2",
40
41
  "tsx": "latest",
41
42
  "typescript": "latest",
42
43
  "typescript-eslint": "latest",