From 7f0a88c28d78498118767c05a313733b3ae5d91a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Oct 2025 09:02:22 +0000 Subject: [PATCH 1/4] Initial plan From b96ee32da031058e32829e83cf0504f017f0f71d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Oct 2025 09:21:30 +0000 Subject: [PATCH 2/4] Add core and imgproc API bindings with comprehensive functions Co-authored-by: luckyyyyy <9210430+luckyyyyy@users.noreply.github.com> --- demo/test-new-api.js | 109 +++++++++++++ index.d.ts | 161 ++++++++++++++++++++ src/constants.rs | 65 ++++++++ src/core_funcs.rs | 245 ++++++++++++++++++++++++++++++ src/imgproc.rs | 354 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 + 6 files changed, 938 insertions(+) create mode 100644 demo/test-new-api.js create mode 100644 src/core_funcs.rs create mode 100644 src/imgproc.rs diff --git a/demo/test-new-api.js b/demo/test-new-api.js new file mode 100644 index 0000000..4154184 --- /dev/null +++ b/demo/test-new-api.js @@ -0,0 +1,109 @@ +const cv = require('../index.js'); +const fs = require('fs/promises'); + +async function testNewFunctions() { + console.log('Testing new OpenCV API bindings...\n'); + + try { + // Load a test image + const imagePath = './demo/20250123-112635.png'; + const imageBuffer = await fs.readFile(imagePath); + const image = await cv.imdecode(imageBuffer); + + console.log('✓ Image loaded:', image.size); + + // Test flip + console.log('\nTesting flip...'); + const flippedH = await cv.flip(image, 1); // Flip horizontally + console.log('✓ Flipped horizontally:', flippedH.size); + + const flippedV = await cv.flip(image, 0); // Flip vertically + console.log('✓ Flipped vertically:', flippedV.size); + + const flippedBoth = await cv.flip(image, -1); // Flip both + console.log('✓ Flipped both:', flippedBoth.size); + + // Test rotate + console.log('\nTesting rotate...'); + const rotated90 = await cv.rotate(image, cv.ROTATE_90_CLOCKWISE); + console.log('✓ Rotated 90° clockwise:', rotated90.size); + + const rotated180 = await cv.rotate(image, cv.ROTATE_180); + console.log('✓ Rotated 180°:', rotated180.size); + + const rotated270 = await cv.rotate(image, cv.ROTATE_90_COUNTERCLOCKWISE); + console.log('✓ Rotated 270° clockwise:', rotated270.size); + + // Test gaussian blur + console.log('\nTesting Gaussian blur...'); + const blurred = await cv.gaussianBlur(image, 5, 5, 1.5); + console.log('✓ Gaussian blur applied:', blurred.size); + + // Test Canny edge detection + console.log('\nTesting Canny edge detection...'); + const edges = await cv.canny(image, 50, 150); + console.log('✓ Canny edges detected:', edges.size); + + // Test threshold + console.log('\nTesting threshold...'); + const thresholded = await image.threshold(127, 255, cv.THRESH_BINARY); + console.log('✓ Threshold applied:', thresholded.size); + + // Test adaptive threshold (needs grayscale image) + console.log('\nTesting adaptive threshold...'); + const grayImage = await cv.imread(imagePath, cv.IMREAD_GRAYSCALE); + const adaptiveThresh = await cv.adaptiveThreshold( + grayImage, + 255, + cv.ADAPTIVE_THRESH_GAUSSIAN_C, + cv.THRESH_BINARY, + 11, + 2 + ); + console.log('✓ Adaptive threshold applied:', adaptiveThresh.size); + + // Test in_range + console.log('\nTesting in_range...'); + const mask = await cv.inRange(image, [0, 0, 0], [128, 128, 128]); + console.log('✓ In range mask created:', mask.size); + + // Test find_contours + console.log('\nTesting find_contours...'); + const contours = await cv.findContours( + edges, + cv.RETR_EXTERNAL, + cv.CHAIN_APPROX_SIMPLE + ); + console.log('✓ Contours found:', contours.length); + + // Test draw_contours + if (contours.length > 0) { + console.log('\nTesting draw_contours...'); + const imageWithContours = await cv.drawContours( + image, + contours, + -1, // Draw all contours + { val0: 0, val1: 255, val2: 0, val3: 255 }, // Green color + 2 + ); + console.log('✓ Contours drawn:', imageWithContours.size); + } + + // Test split and merge + console.log('\nTesting split...'); + const channels = await cv.split(image); + console.log('✓ Image split into', channels.length, 'channels'); + + console.log('\nTesting merge...'); + const merged = await cv.merge(channels); + console.log('✓ Channels merged:', merged.size); + + console.log('\n✅ All tests passed!'); + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + process.exit(1); + } +} + +testNewFunctions().catch(console.error); diff --git a/index.d.ts b/index.d.ts index 315b718..fa7f9ec 100644 --- a/index.d.ts +++ b/index.d.ts @@ -20,6 +20,89 @@ export declare class Mat { } export type JSMat = Mat +export const ADAPTIVE_THRESH_GAUSSIAN_C: number + +export const ADAPTIVE_THRESH_MEAN_C: number + +/** + * Apply adaptive threshold to an array. + * + * # Arguments + * * `src` - Source 8-bit single-channel image + * * `max_value` - Non-zero value assigned to pixels for which the condition is satisfied + * * `adaptive_method` - Adaptive thresholding algorithm (ADAPTIVE_THRESH_MEAN_C or ADAPTIVE_THRESH_GAUSSIAN_C) + * * `threshold_type` - Thresholding type (THRESH_BINARY or THRESH_BINARY_INV) + * * `block_size` - Size of pixel neighborhood used to calculate threshold (must be odd and greater than 1) + * * `c` - Constant subtracted from the mean or weighted mean + */ +export declare function adaptiveThreshold(src: JSMat, maxValue: number, adaptiveMethod: number, thresholdType: number, blockSize: number, c: number, abortSignal?: AbortSignal | undefined | null): Promise + +/** + * Find edges in an image using the Canny algorithm. + * + * # Arguments + * * `src` - Source image (single-channel 8-bit) + * * `threshold1` - First threshold for the hysteresis procedure + * * `threshold2` - Second threshold for the hysteresis procedure + * * `aperture_size` - Aperture size for the Sobel operator (default: 3) + * * `l2_gradient` - Flag indicating whether to use L2 norm for gradient (default: false) + */ +export declare function canny(src: JSMat, threshold1: number, threshold2: number, apertureSize?: number | undefined | null, l2Gradient?: boolean | undefined | null, abortSignal?: AbortSignal | undefined | null): Promise + +export const CHAIN_APPROX_NONE: number + +export const CHAIN_APPROX_SIMPLE: number + +export const CHAIN_APPROX_TC89_KCOS: number + +export const CHAIN_APPROX_TC89_L1: number + +/** + * Draw contours on an image. + * + * # Arguments + * * `src` - Destination image + * * `contours` - All input contours (array of point arrays) + * * `contour_idx` - Contour to draw (-1 means all contours) + * * `color` - Color of the contours (Scalar with val0, val1, val2, val3) + * * `thickness` - Thickness of lines the contours are drawn with (negative value fills the contour) + */ +export declare function drawContours(src: JSMat, contours: Array, contourIdx: number, color: Scalar, thickness?: number | undefined | null, abortSignal?: AbortSignal | undefined | null): Promise + +/** + * Find contours in a binary image. + * + * # Arguments + * * `src` - Source 8-bit single-channel image (non-zero pixels are treated as 1s) + * * `mode` - Contour retrieval mode (RETR_EXTERNAL, RETR_LIST, RETR_CCOMP, RETR_TREE) + * * `method` - Contour approximation method (CHAIN_APPROX_NONE, CHAIN_APPROX_SIMPLE, etc.) + */ +export declare function findContours(src: JSMat, mode: number, method: number, abortSignal?: AbortSignal | undefined | null): Promise> + +/** + * Flip an image horizontally, vertically, or both. + * + * # Arguments + * * `src` - Source image + * * `flip_code` - A flag to specify how to flip the image: + * - 0: flip vertically + * - positive value (e.g., 1): flip horizontally + * - negative value (e.g., -1): flip both horizontally and vertically + */ +export declare function flip(src: JSMat, flipCode: number, abortSignal?: AbortSignal | undefined | null): Promise + +/** + * Apply Gaussian blur to an image. + * + * # Arguments + * * `src` - Source image + * * `ksize_width` - Gaussian kernel width (must be positive and odd) + * * `ksize_height` - Gaussian kernel height (must be positive and odd) + * * `sigma_x` - Gaussian kernel standard deviation in X direction + * * `sigma_y` - Gaussian kernel standard deviation in Y direction (default: 0, uses sigma_x) + */ +export declare function gaussianBlur(src: JSMat, ksizeWidth: number, ksizeHeight: number, sigmaX: number, sigmaY?: number | undefined | null, abortSignal?: AbortSignal | undefined | null): Promise + export declare function getBuildInformation(): string export declare function getTickCount(): number @@ -58,6 +141,24 @@ export const IMREAD_REDUCED_GRAYSCALE_8: number export const IMREAD_UNCHANGED: number +/** + * Check if array elements lie between the elements of two other arrays. + * + * # Arguments + * * `src` - Source array + * * `lower_bound` - Array of lower bounds (format: [b, g, r, a]) + * * `upper_bound` - Array of upper bounds (format: [b, g, r, a]) + */ +export declare function inRange(src: JSMat, lowerBound: Array, upperBound: Array, abortSignal?: AbortSignal | undefined | null): Promise + +/** + * Merge several single-channel arrays into a multi-channel array. + * + * # Arguments + * * `mats` - Array of single-channel matrices to be merged + */ +export declare function merge(mats: JSMat[], abortSignal?: AbortSignal | undefined | null): Promise + export interface MinMaxResult { minVal: number maxVal: number @@ -70,6 +171,11 @@ export interface Point { y: number } +export interface Point { + x: number + y: number +} + export interface Rect { x: number y: number @@ -84,11 +190,66 @@ export interface Rect { height: number } +export const RETR_CCOMP: number + +export const RETR_EXTERNAL: number + +export const RETR_LIST: number + +export const RETR_TREE: number + +/** + * Rotate an image by 90, 180, or 270 degrees. + * + * # Arguments + * * `src` - Source image + * * `rotate_code` - An enum to specify how to rotate the array: + * - ROTATE_90_CLOCKWISE: Rotate 90 degrees clockwise + * - ROTATE_180: Rotate 180 degrees + * - ROTATE_90_COUNTERCLOCKWISE: Rotate 270 degrees clockwise (90 degrees counterclockwise) + */ +export declare function rotate(src: JSMat, rotateCode: number, abortSignal?: AbortSignal | undefined | null): Promise + +export const ROTATE_180: number + +export const ROTATE_90_CLOCKWISE: number + +export const ROTATE_90_COUNTERCLOCKWISE: number + +export interface Scalar { + val0: number + val1: number + val2: number + val3: number +} + export interface Size { width: number height: number } +/** + * Split a multi-channel array into several single-channel arrays. + * + * # Arguments + * * `src` - Source multi-channel array + */ +export declare function split(src: JSMat, abortSignal?: AbortSignal | undefined | null): Promise> + +export const THRESH_BINARY: number + +export const THRESH_BINARY_INV: number + +export const THRESH_OTSU: number + +export const THRESH_TOZERO: number + +export const THRESH_TOZERO_INV: number + +export const THRESH_TRIANGLE: number + +export const THRESH_TRUNC: number + export const TM_CCOEFF: number export const TM_CCOEFF_NORMED: number diff --git a/src/constants.rs b/src/constants.rs index 48c53f4..f90987f 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -54,3 +54,68 @@ pub const IMREAD_REDUCED_COLOR_8: i32 = opencv::imgcodecs::IMREAD_REDUCED_COLOR_ #[napi] pub const IMREAD_IGNORE_ORIENTATION: i32 = opencv::imgcodecs::IMREAD_IGNORE_ORIENTATION; + +// Threshold types +#[napi] +pub const THRESH_BINARY: i32 = opencv::imgproc::THRESH_BINARY; + +#[napi] +pub const THRESH_BINARY_INV: i32 = opencv::imgproc::THRESH_BINARY_INV; + +#[napi] +pub const THRESH_TRUNC: i32 = opencv::imgproc::THRESH_TRUNC; + +#[napi] +pub const THRESH_TOZERO: i32 = opencv::imgproc::THRESH_TOZERO; + +#[napi] +pub const THRESH_TOZERO_INV: i32 = opencv::imgproc::THRESH_TOZERO_INV; + +#[napi] +pub const THRESH_OTSU: i32 = opencv::imgproc::THRESH_OTSU; + +#[napi] +pub const THRESH_TRIANGLE: i32 = opencv::imgproc::THRESH_TRIANGLE; + +// Adaptive threshold methods +#[napi] +pub const ADAPTIVE_THRESH_MEAN_C: i32 = opencv::imgproc::ADAPTIVE_THRESH_MEAN_C; + +#[napi] +pub const ADAPTIVE_THRESH_GAUSSIAN_C: i32 = opencv::imgproc::ADAPTIVE_THRESH_GAUSSIAN_C; + +// Contour retrieval modes +#[napi] +pub const RETR_EXTERNAL: i32 = opencv::imgproc::RETR_EXTERNAL; + +#[napi] +pub const RETR_LIST: i32 = opencv::imgproc::RETR_LIST; + +#[napi] +pub const RETR_CCOMP: i32 = opencv::imgproc::RETR_CCOMP; + +#[napi] +pub const RETR_TREE: i32 = opencv::imgproc::RETR_TREE; + +// Contour approximation methods +#[napi] +pub const CHAIN_APPROX_NONE: i32 = opencv::imgproc::CHAIN_APPROX_NONE; + +#[napi] +pub const CHAIN_APPROX_SIMPLE: i32 = opencv::imgproc::CHAIN_APPROX_SIMPLE; + +#[napi] +pub const CHAIN_APPROX_TC89_L1: i32 = opencv::imgproc::CHAIN_APPROX_TC89_L1; + +#[napi] +pub const CHAIN_APPROX_TC89_KCOS: i32 = opencv::imgproc::CHAIN_APPROX_TC89_KCOS; + +// Rotate codes +#[napi] +pub const ROTATE_90_CLOCKWISE: i32 = opencv::core::ROTATE_90_CLOCKWISE as i32; + +#[napi] +pub const ROTATE_180: i32 = opencv::core::ROTATE_180 as i32; + +#[napi] +pub const ROTATE_90_COUNTERCLOCKWISE: i32 = opencv::core::ROTATE_90_COUNTERCLOCKWISE as i32; diff --git a/src/core_funcs.rs b/src/core_funcs.rs new file mode 100644 index 0000000..f521407 --- /dev/null +++ b/src/core_funcs.rs @@ -0,0 +1,245 @@ +use crate::{JSMat, OpenCVError}; +use napi::bindgen_prelude::*; +use opencv::core as cv_core; + +/// Flip task +pub struct FlipTask { + source_mat: cv_core::Mat, + flip_code: i32, +} + +#[napi] +impl Task for FlipTask { + type Output = JSMat; + type JsValue = JSMat; + + fn compute(&mut self) -> Result { + let mut dst = cv_core::Mat::default(); + cv_core::flip(&self.source_mat, &mut dst, self.flip_code).map_err(OpenCVError)?; + Ok(JSMat { mat: dst }) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +/// Rotate task +pub struct RotateTask { + source_mat: cv_core::Mat, + rotate_code: i32, +} + +#[napi] +impl Task for RotateTask { + type Output = JSMat; + type JsValue = JSMat; + + fn compute(&mut self) -> Result { + let mut dst = cv_core::Mat::default(); + cv_core::rotate(&self.source_mat, &mut dst, self.rotate_code).map_err(OpenCVError)?; + Ok(JSMat { mat: dst }) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +/// Merge task +pub struct MergeTask { + mats: Vec, +} + +#[napi] +impl Task for MergeTask { + type Output = JSMat; + type JsValue = JSMat; + + fn compute(&mut self) -> Result { + let mut dst = cv_core::Mat::default(); + let vec: cv_core::Vector = cv_core::Vector::from_iter(self.mats.clone()); + cv_core::merge(&vec, &mut dst).map_err(OpenCVError)?; + Ok(JSMat { mat: dst }) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +/// Split task +pub struct SplitTask { + source_mat: cv_core::Mat, +} + +#[napi] +impl Task for SplitTask { + type Output = Vec; + type JsValue = Vec; + + fn compute(&mut self) -> Result { + let mut vec = cv_core::Vector::new(); + cv_core::split(&self.source_mat, &mut vec).map_err(OpenCVError)?; + let result: Vec = vec.iter().map(|mat| JSMat { mat }).collect(); + Ok(result) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +/// InRange task +pub struct InRangeTask { + source_mat: cv_core::Mat, + lower_bound: cv_core::Scalar, + upper_bound: cv_core::Scalar, +} + +#[napi] +impl Task for InRangeTask { + type Output = JSMat; + type JsValue = JSMat; + + fn compute(&mut self) -> Result { + let mut dst = cv_core::Mat::default(); + cv_core::in_range( + &self.source_mat, + &self.lower_bound, + &self.upper_bound, + &mut dst, + ) + .map_err(OpenCVError)?; + Ok(JSMat { mat: dst }) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +/// Flip an image horizontally, vertically, or both. +/// +/// # Arguments +/// * `src` - Source image +/// * `flip_code` - A flag to specify how to flip the image: +/// - 0: flip vertically +/// - positive value (e.g., 1): flip horizontally +/// - negative value (e.g., -1): flip both horizontally and vertically +#[napi] +pub fn flip( + #[napi(ts_arg_type = "JSMat")] src: &JSMat, + flip_code: i32, + abort_signal: Option, +) -> AsyncTask { + AsyncTask::with_optional_signal( + FlipTask { + source_mat: src.mat.clone(), + flip_code, + }, + abort_signal, + ) +} + +/// Rotate an image by 90, 180, or 270 degrees. +/// +/// # Arguments +/// * `src` - Source image +/// * `rotate_code` - An enum to specify how to rotate the array: +/// - ROTATE_90_CLOCKWISE: Rotate 90 degrees clockwise +/// - ROTATE_180: Rotate 180 degrees +/// - ROTATE_90_COUNTERCLOCKWISE: Rotate 270 degrees clockwise (90 degrees counterclockwise) +#[napi] +pub fn rotate( + #[napi(ts_arg_type = "JSMat")] src: &JSMat, + rotate_code: i32, + abort_signal: Option, +) -> AsyncTask { + AsyncTask::with_optional_signal( + RotateTask { + source_mat: src.mat.clone(), + rotate_code, + }, + abort_signal, + ) +} + +/// Merge several single-channel arrays into a multi-channel array. +/// +/// # Arguments +/// * `mats` - Array of single-channel matrices to be merged +#[napi] +pub fn merge( + #[napi(ts_arg_type = "JSMat[]")] mats: Vec<&JSMat>, + abort_signal: Option, +) -> AsyncTask { + let mat_vec: Vec = mats.iter().map(|m| m.mat.clone()).collect(); + AsyncTask::with_optional_signal(MergeTask { mats: mat_vec }, abort_signal) +} + +/// Split a multi-channel array into several single-channel arrays. +/// +/// # Arguments +/// * `src` - Source multi-channel array +#[napi] +pub fn split( + #[napi(ts_arg_type = "JSMat")] src: &JSMat, + abort_signal: Option, +) -> AsyncTask { + AsyncTask::with_optional_signal( + SplitTask { + source_mat: src.mat.clone(), + }, + abort_signal, + ) +} + +/// Check if array elements lie between the elements of two other arrays. +/// +/// # Arguments +/// * `src` - Source array +/// * `lower_bound` - Array of lower bounds (format: [b, g, r, a]) +/// * `upper_bound` - Array of upper bounds (format: [b, g, r, a]) +#[napi] +pub fn in_range( + #[napi(ts_arg_type = "JSMat")] src: &JSMat, + lower_bound: Vec, + upper_bound: Vec, + abort_signal: Option, +) -> AsyncTask { + let lower = match lower_bound.len() { + 1 => cv_core::Scalar::new(lower_bound[0], 0.0, 0.0, 0.0), + 2 => cv_core::Scalar::new(lower_bound[0], lower_bound[1], 0.0, 0.0), + 3 => cv_core::Scalar::new(lower_bound[0], lower_bound[1], lower_bound[2], 0.0), + 4 => cv_core::Scalar::new( + lower_bound[0], + lower_bound[1], + lower_bound[2], + lower_bound[3], + ), + _ => cv_core::Scalar::default(), + }; + + let upper = match upper_bound.len() { + 1 => cv_core::Scalar::new(upper_bound[0], 0.0, 0.0, 0.0), + 2 => cv_core::Scalar::new(upper_bound[0], upper_bound[1], 0.0, 0.0), + 3 => cv_core::Scalar::new(upper_bound[0], upper_bound[1], upper_bound[2], 0.0), + 4 => cv_core::Scalar::new( + upper_bound[0], + upper_bound[1], + upper_bound[2], + upper_bound[3], + ), + _ => cv_core::Scalar::default(), + }; + + AsyncTask::with_optional_signal( + InRangeTask { + source_mat: src.mat.clone(), + lower_bound: lower, + upper_bound: upper, + }, + abort_signal, + ) +} diff --git a/src/imgproc.rs b/src/imgproc.rs new file mode 100644 index 0000000..008ffb0 --- /dev/null +++ b/src/imgproc.rs @@ -0,0 +1,354 @@ +use crate::{JSMat, OpenCVError}; +use napi::bindgen_prelude::*; +use opencv::{core as cv_core, imgproc}; + +#[napi(object)] +#[derive(Clone)] +pub struct Scalar { + pub val0: f64, + pub val1: f64, + pub val2: f64, + pub val3: f64, +} + +/// Canny edge detection task +pub struct CannyTask { + source_mat: cv_core::Mat, + threshold1: f64, + threshold2: f64, + aperture_size: i32, + l2_gradient: bool, +} + +#[napi] +impl Task for CannyTask { + type Output = JSMat; + type JsValue = JSMat; + + fn compute(&mut self) -> Result { + let mut dst = cv_core::Mat::default(); + imgproc::canny( + &self.source_mat, + &mut dst, + self.threshold1, + self.threshold2, + self.aperture_size, + self.l2_gradient, + ) + .map_err(OpenCVError)?; + Ok(JSMat { mat: dst }) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +/// Gaussian blur task +pub struct GaussianBlurTask { + source_mat: cv_core::Mat, + ksize_width: i32, + ksize_height: i32, + sigma_x: f64, + sigma_y: f64, +} + +#[napi] +impl Task for GaussianBlurTask { + type Output = JSMat; + type JsValue = JSMat; + + fn compute(&mut self) -> Result { + let mut dst = cv_core::Mat::default(); + let ksize = cv_core::Size::new(self.ksize_width, self.ksize_height); + imgproc::gaussian_blur( + &self.source_mat, + &mut dst, + ksize, + self.sigma_x, + self.sigma_y, + cv_core::BORDER_DEFAULT, + ) + .map_err(OpenCVError)?; + Ok(JSMat { mat: dst }) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +/// Adaptive threshold task +pub struct AdaptiveThresholdTask { + source_mat: cv_core::Mat, + max_value: f64, + adaptive_method: i32, + threshold_type: i32, + block_size: i32, + c: f64, +} + +#[napi] +impl Task for AdaptiveThresholdTask { + type Output = JSMat; + type JsValue = JSMat; + + fn compute(&mut self) -> Result { + let mut dst = cv_core::Mat::default(); + imgproc::adaptive_threshold( + &self.source_mat, + &mut dst, + self.max_value, + self.adaptive_method, + self.threshold_type, + self.block_size, + self.c, + ) + .map_err(OpenCVError)?; + Ok(JSMat { mat: dst }) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +/// Find contours task +pub struct FindContoursTask { + source_mat: cv_core::Mat, + mode: i32, + method: i32, +} + +#[napi(object)] +#[derive(Clone)] +pub struct Point { + pub x: i32, + pub y: i32, +} + +pub type Contour = Vec; + +#[napi] +impl Task for FindContoursTask { + type Output = Vec; + type JsValue = Vec; + + fn compute(&mut self) -> Result { + let mut contours = cv_core::Vector::>::new(); + let mut hierarchy = cv_core::Mat::default(); + imgproc::find_contours_with_hierarchy( + &self.source_mat, + &mut contours, + &mut hierarchy, + self.mode, + self.method, + cv_core::Point::new(0, 0), + ) + .map_err(OpenCVError)?; + + let mut result: Vec = Vec::new(); + for i in 0..contours.len() { + let contour = contours.get(i).map_err(OpenCVError)?; + let points: Vec = contour + .iter() + .map(|p| Point { x: p.x, y: p.y }) + .collect(); + result.push(points); + } + Ok(result) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +/// Draw contours task +pub struct DrawContoursTask { + source_mat: cv_core::Mat, + contours: Vec, + contour_idx: i32, + color: cv_core::Scalar, + thickness: i32, +} + +#[napi] +impl Task for DrawContoursTask { + type Output = JSMat; + type JsValue = JSMat; + + fn compute(&mut self) -> Result { + let mut dst = self.source_mat.clone(); + let mut opencv_contours = cv_core::Vector::>::new(); + + for contour in &self.contours { + let mut opencv_contour = cv_core::Vector::::new(); + for point in contour { + opencv_contour.push(cv_core::Point::new(point.x, point.y)); + } + opencv_contours.push(opencv_contour); + } + + imgproc::draw_contours( + &mut dst, + &opencv_contours, + self.contour_idx, + self.color, + self.thickness, + imgproc::LINE_8, + &cv_core::Mat::default(), + i32::MAX, + cv_core::Point::new(0, 0), + ) + .map_err(OpenCVError)?; + Ok(JSMat { mat: dst }) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +/// Find edges in an image using the Canny algorithm. +/// +/// # Arguments +/// * `src` - Source image (single-channel 8-bit) +/// * `threshold1` - First threshold for the hysteresis procedure +/// * `threshold2` - Second threshold for the hysteresis procedure +/// * `aperture_size` - Aperture size for the Sobel operator (default: 3) +/// * `l2_gradient` - Flag indicating whether to use L2 norm for gradient (default: false) +#[napi] +pub fn canny( + #[napi(ts_arg_type = "JSMat")] src: &JSMat, + threshold1: f64, + threshold2: f64, + aperture_size: Option, + l2_gradient: Option, + abort_signal: Option, +) -> AsyncTask { + AsyncTask::with_optional_signal( + CannyTask { + source_mat: src.mat.clone(), + threshold1, + threshold2, + aperture_size: aperture_size.unwrap_or(3), + l2_gradient: l2_gradient.unwrap_or(false), + }, + abort_signal, + ) +} + +/// Apply Gaussian blur to an image. +/// +/// # Arguments +/// * `src` - Source image +/// * `ksize_width` - Gaussian kernel width (must be positive and odd) +/// * `ksize_height` - Gaussian kernel height (must be positive and odd) +/// * `sigma_x` - Gaussian kernel standard deviation in X direction +/// * `sigma_y` - Gaussian kernel standard deviation in Y direction (default: 0, uses sigma_x) +#[napi] +pub fn gaussian_blur( + #[napi(ts_arg_type = "JSMat")] src: &JSMat, + ksize_width: i32, + ksize_height: i32, + sigma_x: f64, + sigma_y: Option, + abort_signal: Option, +) -> AsyncTask { + AsyncTask::with_optional_signal( + GaussianBlurTask { + source_mat: src.mat.clone(), + ksize_width, + ksize_height, + sigma_x, + sigma_y: sigma_y.unwrap_or(0.0), + }, + abort_signal, + ) +} + +/// Apply adaptive threshold to an array. +/// +/// # Arguments +/// * `src` - Source 8-bit single-channel image +/// * `max_value` - Non-zero value assigned to pixels for which the condition is satisfied +/// * `adaptive_method` - Adaptive thresholding algorithm (ADAPTIVE_THRESH_MEAN_C or ADAPTIVE_THRESH_GAUSSIAN_C) +/// * `threshold_type` - Thresholding type (THRESH_BINARY or THRESH_BINARY_INV) +/// * `block_size` - Size of pixel neighborhood used to calculate threshold (must be odd and greater than 1) +/// * `c` - Constant subtracted from the mean or weighted mean +#[napi] +pub fn adaptive_threshold( + #[napi(ts_arg_type = "JSMat")] src: &JSMat, + max_value: f64, + adaptive_method: i32, + threshold_type: i32, + block_size: i32, + c: f64, + abort_signal: Option, +) -> AsyncTask { + AsyncTask::with_optional_signal( + AdaptiveThresholdTask { + source_mat: src.mat.clone(), + max_value, + adaptive_method, + threshold_type, + block_size, + c, + }, + abort_signal, + ) +} + +/// Find contours in a binary image. +/// +/// # Arguments +/// * `src` - Source 8-bit single-channel image (non-zero pixels are treated as 1s) +/// * `mode` - Contour retrieval mode (RETR_EXTERNAL, RETR_LIST, RETR_CCOMP, RETR_TREE) +/// * `method` - Contour approximation method (CHAIN_APPROX_NONE, CHAIN_APPROX_SIMPLE, etc.) +#[napi] +pub fn find_contours( + #[napi(ts_arg_type = "JSMat")] src: &JSMat, + mode: i32, + method: i32, + abort_signal: Option, +) -> AsyncTask { + AsyncTask::with_optional_signal( + FindContoursTask { + source_mat: src.mat.clone(), + mode, + method, + }, + abort_signal, + ) +} + +/// Draw contours on an image. +/// +/// # Arguments +/// * `src` - Destination image +/// * `contours` - All input contours (array of point arrays) +/// * `contour_idx` - Contour to draw (-1 means all contours) +/// * `color` - Color of the contours (Scalar with val0, val1, val2, val3) +/// * `thickness` - Thickness of lines the contours are drawn with (negative value fills the contour) +#[napi] +pub fn draw_contours( + #[napi(ts_arg_type = "JSMat")] src: &JSMat, + contours: Vec, + contour_idx: i32, + color: Scalar, + thickness: Option, + abort_signal: Option, +) -> AsyncTask { + let opencv_color = cv_core::Scalar::new(color.val0, color.val1, color.val2, color.val3); + AsyncTask::with_optional_signal( + DrawContoursTask { + source_mat: src.mat.clone(), + contours, + contour_idx, + color: opencv_color, + thickness: thickness.unwrap_or(1), + }, + abort_signal, + ) +} diff --git a/src/lib.rs b/src/lib.rs index d212468..43304c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,10 @@ #![deny(clippy::all)] mod constants; +mod core_funcs; mod error; mod image; +mod imgproc; mod mat; mod dnn; @@ -10,7 +12,9 @@ mod dnn; extern crate napi_derive; pub use constants::*; +pub use core_funcs::*; pub use error::OpenCVError; pub use image::*; +pub use imgproc::*; pub use mat::JSMat; pub use dnn::*; From cc50b00410c58e4c646b3050adf73cdfcc96cffc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Oct 2025 09:26:52 +0000 Subject: [PATCH 3/4] Fix TypeScript definitions: remove duplicates and add Contour type Co-authored-by: luckyyyyy <9210430+luckyyyyy@users.noreply.github.com> --- index.d.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/index.d.ts b/index.d.ts index fa7f9ec..ab8ac2f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -171,11 +171,6 @@ export interface Point { y: number } -export interface Point { - x: number - y: number -} - export interface Rect { x: number y: number @@ -183,12 +178,7 @@ export interface Rect { height: number } -export interface Rect { - x: number - y: number - width: number - height: number -} +export type Contour = Array export const RETR_CCOMP: number From 1562553326d3377ac5fbcb75b3550fd7cd377b23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Oct 2025 09:27:49 +0000 Subject: [PATCH 4/4] Add comprehensive README documentation for new API functions Co-authored-by: luckyyyyy <9210430+luckyyyyy@users.noreply.github.com> --- README.md | 320 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a07d6f --- /dev/null +++ b/README.md @@ -0,0 +1,320 @@ +# node-opencv + +High-performance Node.js bindings for OpenCV using napi-rs and opencv-rust. + +## Features + +This library provides a comprehensive set of OpenCV functions for Node.js applications, with support for: + +- Core operations (flip, rotate, merge, split, in_range) +- Image processing (Canny, Gaussian blur, thresholding, contour detection) +- Template matching +- Deep learning (DNN module support) +- Asynchronous operations using Node.js worker threads + +## Installation + +```bash +npm install node-opencv +``` + +## Usage + +### Basic Example + +```javascript +const cv = require('node-opencv'); + +async function processImage() { + // Load an image + const image = await cv.imread('./image.jpg'); + + // Apply Gaussian blur + const blurred = await cv.gaussianBlur(image, 5, 5, 1.5); + + // Detect edges using Canny + const edges = await cv.canny(blurred, 50, 150); + + // Find contours + const contours = await cv.findContours( + edges, + cv.RETR_EXTERNAL, + cv.CHAIN_APPROX_SIMPLE + ); + + // Draw contours on the original image + const result = await cv.drawContours( + image, + contours, + -1, // Draw all contours + { val0: 0, val1: 255, val2: 0, val3: 255 }, // Green color + 2 // Thickness + ); + + // Save the result + const buffer = await cv.imencode('.jpg', result); + require('fs').writeFileSync('./output.jpg', buffer); +} + +processImage().catch(console.error); +``` + +## API Reference + +### Image I/O + +#### `imread(path: string, flags?: number): Promise` + +Read an image from a file. + +```javascript +const image = await cv.imread('./image.jpg', cv.IMREAD_COLOR); +``` + +#### `imdecode(buffer: Buffer, flags?: number): Promise` + +Decode an image from a buffer. + +```javascript +const buffer = fs.readFileSync('./image.jpg'); +const image = await cv.imdecode(buffer, cv.IMREAD_COLOR); +``` + +#### `imencode(ext: string, mat: Mat): Promise` + +Encode an image to a buffer. + +```javascript +const buffer = await cv.imencode('.jpg', image); +``` + +### Core Functions + +#### `flip(src: Mat, flipCode: number): Promise` + +Flip an image horizontally, vertically, or both. + +- `flipCode = 0`: flip vertically +- `flipCode > 0`: flip horizontally +- `flipCode < 0`: flip both horizontally and vertically + +```javascript +const flipped = await cv.flip(image, 1); // Flip horizontally +``` + +#### `rotate(src: Mat, rotateCode: number): Promise` + +Rotate an image by 90, 180, or 270 degrees. + +```javascript +const rotated = await cv.rotate(image, cv.ROTATE_90_CLOCKWISE); +``` + +**Constants:** +- `cv.ROTATE_90_CLOCKWISE` +- `cv.ROTATE_180` +- `cv.ROTATE_90_COUNTERCLOCKWISE` + +#### `split(src: Mat): Promise` + +Split a multi-channel array into several single-channel arrays. + +```javascript +const [blue, green, red] = await cv.split(image); +``` + +#### `merge(mats: Mat[]): Promise` + +Merge several single-channel arrays into a multi-channel array. + +```javascript +const merged = await cv.merge([blue, green, red]); +``` + +#### `inRange(src: Mat, lowerBound: number[], upperBound: number[]): Promise` + +Check if array elements lie between bounds. + +```javascript +// Create a mask for pixels in the range [0, 0, 0] to [128, 128, 128] +const mask = await cv.inRange(image, [0, 0, 0], [128, 128, 128]); +``` + +### Image Processing + +#### `canny(src: Mat, threshold1: number, threshold2: number, apertureSize?: number, l2Gradient?: boolean): Promise` + +Find edges in an image using the Canny algorithm. + +```javascript +const edges = await cv.canny(image, 50, 150); +``` + +#### `gaussianBlur(src: Mat, ksizeWidth: number, ksizeHeight: number, sigmaX: number, sigmaY?: number): Promise` + +Apply Gaussian blur to an image. + +```javascript +const blurred = await cv.gaussianBlur(image, 5, 5, 1.5); +``` + +#### `threshold(src: Mat, thresh: number, maxval: number, type: number): Promise` + +Apply a fixed-level threshold to an image. + +```javascript +const thresholded = await image.threshold(127, 255, cv.THRESH_BINARY); +``` + +**Threshold types:** +- `cv.THRESH_BINARY` +- `cv.THRESH_BINARY_INV` +- `cv.THRESH_TRUNC` +- `cv.THRESH_TOZERO` +- `cv.THRESH_TOZERO_INV` +- `cv.THRESH_OTSU` +- `cv.THRESH_TRIANGLE` + +#### `adaptiveThreshold(src: Mat, maxValue: number, adaptiveMethod: number, thresholdType: number, blockSize: number, c: number): Promise` + +Apply adaptive threshold to an image. + +```javascript +const adaptive = await cv.adaptiveThreshold( + grayImage, + 255, + cv.ADAPTIVE_THRESH_GAUSSIAN_C, + cv.THRESH_BINARY, + 11, + 2 +); +``` + +**Adaptive methods:** +- `cv.ADAPTIVE_THRESH_MEAN_C` +- `cv.ADAPTIVE_THRESH_GAUSSIAN_C` + +#### `findContours(src: Mat, mode: number, method: number): Promise` + +Find contours in a binary image. + +```javascript +const contours = await cv.findContours( + binaryImage, + cv.RETR_EXTERNAL, + cv.CHAIN_APPROX_SIMPLE +); +``` + +**Retrieval modes:** +- `cv.RETR_EXTERNAL` - retrieves only the extreme outer contours +- `cv.RETR_LIST` - retrieves all contours without hierarchy +- `cv.RETR_CCOMP` - retrieves contours with 2-level hierarchy +- `cv.RETR_TREE` - retrieves all contours with full hierarchy + +**Approximation methods:** +- `cv.CHAIN_APPROX_NONE` - stores all contour points +- `cv.CHAIN_APPROX_SIMPLE` - compresses horizontal, vertical, and diagonal segments +- `cv.CHAIN_APPROX_TC89_L1` +- `cv.CHAIN_APPROX_TC89_KCOS` + +#### `drawContours(src: Mat, contours: Contour[], contourIdx: number, color: Scalar, thickness?: number): Promise` + +Draw contours on an image. + +```javascript +const result = await cv.drawContours( + image, + contours, + -1, // Draw all contours + { val0: 0, val1: 255, val2: 0, val3: 255 }, // Green in BGRA + 2 +); +``` + +### Template Matching + +#### `matchTemplate(template: Mat, method: number): Promise` + +Match a template within an image. + +```javascript +const result = await image.matchTemplate(template, cv.TM_CCOEFF_NORMED); +``` + +**Methods:** +- `cv.TM_SQDIFF` +- `cv.TM_SQDIFF_NORMED` +- `cv.TM_CCORR` +- `cv.TM_CCORR_NORMED` +- `cv.TM_CCOEFF` +- `cv.TM_CCOEFF_NORMED` + +#### `matchTemplateAll(template: Mat, method: number, score: number, nmsThreshold: number): Promise` + +Find all template matches above a threshold. + +```javascript +const matches = await image.matchTemplateAll( + template, + cv.TM_CCOEFF_NORMED, + 0.8, // Minimum score + 0.1 // NMS threshold +); +``` + +### Mat Methods + +#### `mat.rows: number` + +Get the number of rows in the matrix. + +#### `mat.cols: number` + +Get the number of columns in the matrix. + +#### `mat.size: Size` + +Get the size of the matrix. + +```javascript +const { width, height } = image.size; +``` + +#### `mat.data: Buffer` + +Get the raw pixel data as a Buffer. + +```javascript +const pixels = image.data; +``` + +#### `mat.release(): void` + +Release the matrix memory (optional, memory is managed automatically). + +## TypeScript Support + +This library includes TypeScript definitions for all functions and types: + +```typescript +import * as cv from 'node-opencv'; + +async function processImage(): Promise { + const image: cv.Mat = await cv.imread('./image.jpg'); + const edges: cv.Mat = await cv.canny(image, 50, 150); + const contours: cv.Contour[] = await cv.findContours( + edges, + cv.RETR_EXTERNAL, + cv.CHAIN_APPROX_SIMPLE + ); +} +``` + +## Performance + +All operations are asynchronous and execute on worker threads, ensuring the main event loop remains responsive. This makes the library suitable for high-performance applications and servers. + +## License + +MIT