diff --git a/examples/filters.rs b/examples/filters.rs index e23d043..5c6ade8 100644 --- a/examples/filters.rs +++ b/examples/filters.rs @@ -35,28 +35,42 @@ */ use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Rgb}; +use purecv::core::logging::tags; use purecv::core::{normalize, BorderTypes, Matrix, NormTypes, Size}; use purecv::imgproc::{ bilateral_filter, blur, canny, cvt_color_rgb_to_gray, gaussian_blur, laplacian, median_blur, scharr, sobel, }; use purecv::version; +use purecv::{cv_log_error, cv_log_info}; use std::path::Path; fn main() -> Result<(), Box> { - println!("--- purecv Filters Example ---"); - println!("purecv v{}", version::get_version()); + purecv::core::logging::init_basic_logger()?; + + cv_log_info!(tags::PURECV, "--- Filters Example ---"); + version::print_version(); // 1. Load the image let img_path = "examples/data/butterfly.jpg"; if !Path::new(img_path).exists() { - eprintln!("Error: {} not found. Run from the project root.", img_path); + cv_log_error!( + tags::IMGPROC, + "{} not found. Run from the project root.", + img_path + ); return Ok(()); } let img = image::open(img_path)?; let (width, height) = img.dimensions(); - println!("Loaded image: {} ({}x{})", img_path, width, height); + cv_log_info!( + tags::IMGPROC, + "loaded image: {} ({}x{})", + img_path, + width, + height + ); // 2. Convert to purecv Matrix (RGB) let rgb_img = img.to_rgb8(); @@ -68,7 +82,7 @@ fn main() -> Result<(), Box> { // --- Apply Filters --- // Blur (Box Filter) - println!("Applying Blur..."); + cv_log_info!(tags::IMGPROC, "applying blur..."); let blurred = blur( &mat_rgb, Size::new(5, 5), @@ -78,22 +92,22 @@ fn main() -> Result<(), Box> { save_matrix_rgb(&blurred, "examples/data/out/output_blur.png")?; // Gaussian Blur - println!("Applying Gaussian Blur..."); + cv_log_info!(tags::IMGPROC, "applying gaussian blur..."); let g_blurred = gaussian_blur(&mat_rgb, Size::new(5, 5), 1.5, 1.5, BorderTypes::Reflect101)?; save_matrix_rgb(&g_blurred, "examples/data/out/output_gaussian_blur.png")?; // Median Blur - println!("Applying Median Blur..."); + cv_log_info!(tags::IMGPROC, "applying median blur..."); let m_blurred = median_blur(&mat_rgb, 5)?; save_matrix_rgb(&m_blurred, "examples/data/out/output_median_blur.png")?; // Bilateral Filter - println!("Applying Bilateral Filter..."); + cv_log_info!(tags::IMGPROC, "applying bilateral filter..."); let b_filtered = bilateral_filter(&mat_rgb, 9, 75.0, 75.0, BorderTypes::Reflect101)?; save_matrix_rgb(&b_filtered, "examples/data/out/output_bilateral.png")?; // Sobel (on grayscale) - println!("Applying Sobel..."); + cv_log_info!(tags::IMGPROC, "applying sobel..."); let sob_x = sobel(&mat_gray, 1, 0, 3, 1.0, 0.0, BorderTypes::Reflect101)?; // Normalize for visualization let mut sob_x_norm = Matrix::::new(sob_x.rows, sob_x.cols, sob_x.channels); @@ -109,7 +123,7 @@ fn main() -> Result<(), Box> { save_matrix_gray(&sob_x_norm, "examples/data/out/output_sobel_x.png")?; // Scharr (on grayscale) - println!("Applying Scharr..."); + cv_log_info!(tags::IMGPROC, "applying scharr..."); let sch_y = scharr(&mat_gray, 0, 1, 1.0, 0.0, BorderTypes::Reflect101)?; let mut sch_y_norm = Matrix::::new(sch_y.rows, sch_y.cols, sch_y.channels); normalize( @@ -124,18 +138,21 @@ fn main() -> Result<(), Box> { save_matrix_gray(&sch_y_norm, "examples/data/out/output_scharr_y.png")?; // Laplacian (on grayscale) - println!("Applying Laplacian..."); + cv_log_info!(tags::IMGPROC, "applying laplacian..."); let lap = laplacian(&mat_gray, 3, 1.0, 0.0, BorderTypes::Reflect101)?; let mut lap_norm = Matrix::::new(lap.rows, lap.cols, lap.channels); normalize(&lap, &mut lap_norm, 0.0, 255.0, NormTypes::MinMax, -1, None)?; save_matrix_gray(&lap_norm, "examples/data/out/output_laplacian.png")?; // Canny (on grayscale) - println!("Applying Canny..."); + cv_log_info!(tags::IMGPROC, "applying canny..."); let edges = canny(&mat_gray, 50.0, 150.0, 3, false)?; save_matrix_gray(&edges, "examples/data/out/output_canny.png")?; - println!("\nAll filters applied successfully! Check the output_*.png files."); + cv_log_info!( + tags::IMGPROC, + "all filters applied successfully! Check the output_*.png files." + ); Ok(()) } diff --git a/examples/match_features.rs b/examples/match_features.rs index 2dd3f81..bd768b7 100644 --- a/examples/match_features.rs +++ b/examples/match_features.rs @@ -52,21 +52,26 @@ //! ``` use image::{DynamicImage, GenericImageView, ImageBuffer, Rgb}; +use purecv::core::logging::tags; use purecv::core::Matrix; use purecv::features2d::{draw_matches, BFMatcher, DescriptorMatcher, NormType, Orb, ScoreType}; use purecv::imgproc::cvt_color_rgb_to_gray; use purecv::version; +use purecv::{cv_log_debug, cv_log_error, cv_log_info}; use std::path::Path; use std::time::Instant; fn main() -> Result<(), Box> { - println!("--- purecv Feature Matching Example ---"); - println!("purecv version: v{}", version::get_version()); + purecv::core::logging::init_basic_logger()?; + + cv_log_info!(tags::PURECV, "--- Feature Matching Example ---"); + version::print_version(); let img_path = "examples/data/graf.png"; if !Path::new(img_path).exists() { - eprintln!( - "Error: {} not found. Make sure you are in the project root.", + cv_log_error!( + tags::FEATURES2D, + "{} not found. Make sure you are in the project root.", img_path ); return Ok(()); @@ -79,8 +84,9 @@ fn main() -> Result<(), Box> { let load_start = Instant::now(); let img = image::open(img_path)?; let (width, height) = img.dimensions(); - println!( - "Loaded stitched image: {} ({}x{}) in {:.2?}", + cv_log_info!( + tags::FEATURES2D, + "loaded stitched image: {} ({}x{}) in {:.2?}", img_path, width, height, @@ -88,9 +94,13 @@ fn main() -> Result<(), Box> { ); let half_width = width / 2; - println!( - "Splitting image into left half ({}x{}) and right half ({}x{})", - half_width, height, half_width, height + cv_log_debug!( + tags::FEATURES2D, + "splitting into left ({}x{}) and right ({}x{})", + half_width, + height, + half_width, + height ); // 2. Convert to RGB Matrix @@ -123,42 +133,52 @@ fn main() -> Result<(), Box> { } } } - println!("Cropped images in {:.2?}", crop_start.elapsed()); + cv_log_debug!( + tags::FEATURES2D, + "cropped images in {:.2?}", + crop_start.elapsed() + ); // 5. Detect and Compute ORB Features on both halves (extract 1000 features for higher density) let orb = Orb::new(1000, 1.2, 8, 31, 0, 2, ScoreType::Harris, 31, 20); - println!("Extracting ORB features on left half..."); + cv_log_info!(tags::FEATURES2D, "extracting ORB features on left half..."); let start_left = Instant::now(); let (kps1, desc1) = orb.detect_and_compute(&left_gray)?; - println!( - " Left: extracted {} keypoints in {:.2?}", + cv_log_info!( + tags::FEATURES2D, + " left: {} keypoints in {:.2?}", kps1.len(), start_left.elapsed() ); - println!("Extracting ORB features on right half..."); + cv_log_info!(tags::FEATURES2D, "extracting ORB features on right half..."); let start_right = Instant::now(); let (kps2, desc2) = orb.detect_and_compute(&right_gray)?; - println!( - " Right: extracted {} keypoints in {:.2?}", + cv_log_info!( + tags::FEATURES2D, + " right: {} keypoints in {:.2?}", kps2.len(), start_right.elapsed() ); // 6. Match features using BFMatcher (Hamming distance with cross-check enabled) - println!("Matching features using BFMatcher (NormHamming + Cross Check)..."); + cv_log_info!( + tags::FEATURES2D, + "matching features using BFMatcher (NormHamming + Cross Check)..." + ); let match_start = Instant::now(); let matcher = BFMatcher::new(NormType::NormHamming, true)?; let mutual_matches = matcher.match_descriptors(&desc1, &desc2)?; - println!( - " Matched in {:.2?}. Total mutual matches: {}", + cv_log_info!( + tags::FEATURES2D, + " matched in {:.2?}, {} mutual matches", match_start.elapsed(), mutual_matches.len() ); // 7. Draw matches with random line colors and no unmatched keypoint clutter - println!("Drawing matches..."); + cv_log_info!(tags::FEATURES2D, "drawing matches..."); let draw_start = Instant::now(); let matched_img = draw_matches( @@ -170,8 +190,9 @@ fn main() -> Result<(), Box> { None, // Random colors for matches None, // Do not draw unmatched keypoints )?; - println!( - " Drawn matching visualization in {:.2?}", + cv_log_debug!( + tags::FEATURES2D, + "drawn matching visualization in {:.2?}", draw_start.elapsed() ); @@ -185,12 +206,13 @@ fn main() -> Result<(), Box> { ) .ok_or("Failed to construct image buffer from matched matrix data")?; DynamicImage::ImageRgb8(img_out).save(out_path)?; - println!( - " Saved match visualization to: {} in {:.2?}", + cv_log_info!( + tags::FEATURES2D, + "saved match visualization to: {} in {:.2?}", out_path, save_start.elapsed() ); - println!("\nDone! Run 'cargo run --example match_features' to run again."); + cv_log_info!(tags::PURECV, "done!"); Ok(()) } diff --git a/examples/optical_flow.rs b/examples/optical_flow.rs index fecf045..83f0011 100644 --- a/examples/optical_flow.rs +++ b/examples/optical_flow.rs @@ -68,11 +68,13 @@ //! ``` use image::{DynamicImage, GenericImageView, ImageBuffer, Rgb}; +use purecv::core::logging::tags; use purecv::core::types::{Point2f, Size2i, TermCriteria, TermType}; use purecv::core::Matrix; use purecv::imgproc::{cvt_color_rgb_to_gray, good_features_to_track}; use purecv::version; use purecv::video::{calc_optical_flow_pyramid_lk, OPTFLOW_LK_GET_MIN_EIGENVALS}; +use purecv::{cv_log_debug, cv_log_error, cv_log_info, cv_log_warning}; use std::io::Write; use std::path::Path; @@ -81,9 +83,14 @@ const SHIFT_X: usize = 4; const SHIFT_Y: usize = 3; fn main() -> Result<(), Box> { - println!("--- purecv Optical Flow Example (static image) ---"); - println!("purecv v{}", version::get_version()); - println!("Simulating motion: +{SHIFT_X} px in x, +{SHIFT_Y} px in y\n"); + purecv::core::logging::init_basic_logger()?; + + cv_log_info!(tags::PURECV, "--- Optical Flow Example (static image) ---"); + version::print_version(); + cv_log_info!( + tags::VIDEO, + "simulating motion: +{SHIFT_X} px in x, +{SHIFT_Y} px in y" + ); // Accept an optional image path from the command line. let default_path = "examples/data/butterfly.jpg"; @@ -92,8 +99,9 @@ fn main() -> Result<(), Box> { .unwrap_or_else(|| default_path.to_string()); if !Path::new(&img_path).exists() { - eprintln!( - "Error: '{}' not found. Run from the project root.", + cv_log_error!( + tags::VIDEO, + "'{}' not found. Run from the project root.", img_path ); return Ok(()); @@ -106,7 +114,7 @@ fn main() -> Result<(), Box> { // ----------------------------------------------------------------------- let img = image::open(&img_path)?; let (width, height) = img.dimensions(); - println!("Loaded: {} ({}×{})", img_path, width, height); + cv_log_info!(tags::VIDEO, "loaded: {} ({}x{})", img_path, width, height); let rgb_img = img.to_rgb8(); let mat_rgb = Matrix::from_vec( @@ -116,9 +124,12 @@ fn main() -> Result<(), Box> { rgb_img.clone().into_raw(), ); let prev_gray = cvt_color_rgb_to_gray(&mat_rgb)?; - println!( - "Grayscale: {}×{}, {} channel(s)\n", - prev_gray.rows, prev_gray.cols, prev_gray.channels + cv_log_debug!( + tags::VIDEO, + "grayscale: {}x{}, {} channel(s)", + prev_gray.rows, + prev_gray.cols, + prev_gray.channels ); // ----------------------------------------------------------------------- @@ -141,12 +152,18 @@ fn main() -> Result<(), Box> { } } let next_gray = Matrix::::from_vec(rows, cols, 1, next_data); - println!("Synthesised next frame: translated by (+{SHIFT_X}, +{SHIFT_Y}) pixels\n"); + cv_log_debug!( + tags::VIDEO, + "synthesised next frame: translated by (+{SHIFT_X}, +{SHIFT_Y}) pixels" + ); // ----------------------------------------------------------------------- // 3. Detect good features to track in the previous (original) frame. // ----------------------------------------------------------------------- - println!("=== Step 1: Detect features (goodFeaturesToTrack) ==="); + cv_log_info!( + tags::VIDEO, + "=== Step 1: Detect features (goodFeaturesToTrack) ===" + ); let corners = good_features_to_track(&prev_gray, 100, 0.01, 10.0, 3, false, 0.04)?; println!("Detected {} corner(s)", corners.len()); for (i, pt) in corners.iter().take(5).enumerate() { @@ -157,14 +174,17 @@ fn main() -> Result<(), Box> { } if corners.is_empty() { - eprintln!("\nNo features detected — try a different image."); + cv_log_warning!(tags::VIDEO, "no features detected — try a different image."); return Ok(()); } // ----------------------------------------------------------------------- // 4. Track features from prev to next using pyramidal LK. // ----------------------------------------------------------------------- - println!("\n=== Step 2: Track features (calcOpticalFlowPyrLK) ==="); + cv_log_info!( + tags::VIDEO, + "=== Step 2: Track features (calcOpticalFlowPyrLK) ===" + ); let criteria = TermCriteria::new(TermType::Both, 30, 0.001); let (next_pts, status, err) = calc_optical_flow_pyramid_lk( &prev_gray, @@ -271,10 +291,13 @@ fn main() -> Result<(), Box> { let csv_path = "examples/data/out/optical_flow_vectors.csv"; save_flow_csv(&corners, &next_pts, &status, &err, csv_path)?; - println!("\nOutput saved to:"); - println!(" {result_path} (annotated image: green = tracked, red = lost)"); - println!(" {csv_path}"); - println!("\nDone."); + cv_log_info!(tags::VIDEO, "output saved to:"); + cv_log_info!( + tags::VIDEO, + " {result_path} (annotated image: green = tracked, red = lost)" + ); + cv_log_info!(tags::VIDEO, " {csv_path}"); + cv_log_info!(tags::PURECV, "done."); Ok(()) } diff --git a/src/calib3d/pose.rs b/src/calib3d/pose.rs index c3e854a..1532075 100644 --- a/src/calib3d/pose.rs +++ b/src/calib3d/pose.rs @@ -41,8 +41,10 @@ //! (rotation and translation vectors) in the camera coordinate system. use crate::core::error::{PureCvError, Result}; +use crate::core::logging::tags; use crate::core::types::{Point2f, Point3f}; use crate::core::Matrix; +use crate::cv_log_warning; use super::geometry::rvec_to_rmat; use super::linalg::{mat3_inv, mat3_mul, nearest_rotation, null_space_vector, Lcg}; @@ -312,6 +314,11 @@ pub fn solve_pnp_ransac( } if !found || best_count < 6 { + cv_log_warning!( + tags::CALIB3D, + "solve_pnp_ransac: pose estimation failed because RANSAC consensus inliers ({}) were below the required minimum of 6 points", + best_count + ); return Ok(false); } diff --git a/src/core.rs b/src/core.rs index 30866a9..5cd1abf 100644 --- a/src/core.rs +++ b/src/core.rs @@ -34,6 +34,24 @@ * */ +//! Core data structures and numerical routines — the `core` module. +//! +//! This module mirrors OpenCV's `core` module and provides the foundational +//! types and operations the rest of the crate builds on: +//! +//! - [`Matrix`] — the row-major, n-channel matrix/image container, together +//! with [`MatType`], [`Depth`] and the OpenCV-style type constants. +//! - [`Scalar`] — a 4-channel per-pixel constant. +//! - Element-wise and reduction [`arithm`]etic (`add`, `multiply`, `gemm`, +//! `norm`, `reduce`, …), plus [`solvers`] and matrix operations. +//! - `dft`/`dct` frequency transforms, [`rng`] random-number generation, +//! [`metrics`] and [`structural`] helpers. +//! - [`error::PureCvError`] — the crate-wide error type returned as +//! [`error::Result`]. +//! - [`logging`] — an OpenCV-compatible structured logging API built on the +//! `log` facade, with the `cv_log_*!`, [`crate::cv_bail!`] and +//! [`crate::cv_err!`] macros. + pub mod arithm; pub mod constants; #[cfg(feature = "transforms")] @@ -42,6 +60,7 @@ pub mod dct; pub mod dft; pub mod dynamic; pub mod error; +pub mod logging; pub mod matrix; pub mod metrics; pub mod rng; @@ -79,6 +98,7 @@ pub use self::dft::{ }; pub use self::dynamic::{DynamicData, DynamicMatrix}; pub use self::error::{PureCvError, Result}; +pub use self::logging::{get_log_level, init_basic_logger, set_log_level, LogLevel}; pub use self::matrix::{ DataType, Depth, MatType, Matrix, CV_16S, CV_16SC1, CV_16SC2, CV_16SC3, CV_16SC4, CV_16U, CV_16UC1, CV_16UC2, CV_16UC3, CV_16UC4, CV_32F, CV_32FC1, CV_32FC2, CV_32FC3, CV_32FC4, CV_32S, diff --git a/src/core/arithm.rs b/src/core/arithm.rs index 40e0086..2063634 100644 --- a/src/core/arithm.rs +++ b/src/core/arithm.rs @@ -35,9 +35,12 @@ */ use crate::core::constants::CV_2PI; -use crate::core::error::{PureCvError, Result}; +use crate::core::error::Result; +use crate::core::logging::tags; use crate::core::types::{CmpTypes, NormTypes, ReduceTypes, Scalar}; use crate::core::{DataType, Matrix}; +use crate::cv_log_warning; +use crate::{cv_bail, cv_err}; use num_traits::{Bounded, FromPrimitive, Num, SaturatingAdd, SaturatingSub, ToPrimitive}; use std::ops::{BitAnd, BitOr, BitXor, Not, Sub}; @@ -392,9 +395,17 @@ where T: Num + Copy + Send + Sync + PartialOrd + Bounded + Default + SimdElement + 'static, { if src1.rows != src2.rows || src1.cols != src2.cols || src1.channels != src2.channels { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "add: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -412,9 +423,17 @@ where T: Num + Copy + Send + Sync + PartialOrd + Bounded + Default + SimdElement + 'static, { if src1.rows != src2.rows || src1.cols != src2.cols || src1.channels != src2.channels { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "subtract: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -432,9 +451,17 @@ where T: Num + Copy + Send + Sync + PartialOrd + Bounded + Default + SimdElement + 'static, { if src1.rows != src2.rows || src1.cols != src2.cols || src1.channels != src2.channels { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "multiply: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -452,9 +479,17 @@ where T: Num + Copy + Send + Sync + PartialOrd + Bounded + Default + SimdElement + 'static, { if src1.rows != src2.rows || src1.cols != src2.cols || src1.channels != src2.channels { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "divide: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -478,9 +513,17 @@ where T: Copy + Send + Sync + BitAnd + Default + SimdElement + 'static, { if src1.rows != src2.rows || src1.cols != src2.cols || src1.channels != src2.channels { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "bitwise_and: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -498,9 +541,17 @@ where T: Copy + Send + Sync + BitOr + Default + SimdElement + 'static, { if src1.rows != src2.rows || src1.cols != src2.cols || src1.channels != src2.channels { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "bitwise_or: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -518,9 +569,17 @@ where T: Copy + Send + Sync + BitXor + Default + SimdElement + 'static, { if src1.rows != src2.rows || src1.cols != src2.cols || src1.channels != src2.channels { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "bitwise_xor: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -580,9 +639,17 @@ where T: Num + Copy + Send + Sync + PartialOrd + Default + SimdElement + 'static, { if !src1.dims_match(src2) { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "min: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -600,9 +667,17 @@ where T: Num + Copy + Send + Sync + PartialOrd + Default + SimdElement + 'static, { if !src1.dims_match(src2) { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "max: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -620,9 +695,17 @@ where T: Num + Copy + Send + Sync + PartialOrd + Default + SimdElement + 'static, { if !src1.dims_match(src2) { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "abs_diff: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -679,9 +762,17 @@ where T: Copy + Send + Sync + PartialOrd + Default + 'static, { if !src1.dims_match(src2) { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "compare: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -746,9 +837,20 @@ where || src.cols != upperb.cols || src.channels != upperb.channels { - return Err(PureCvError::InvalidInput( - "Size or channels mismatch".into(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "in_range: size or channels mismatch (src {}×{}×{}, lowerb {}×{}×{}, upperb {}×{}×{})", + src.rows, + src.cols, + src.channels, + lowerb.rows, + lowerb.cols, + lowerb.channels, + upperb.rows, + upperb.cols, + upperb.channels + ); } dst.create(src.rows, src.cols, 1); @@ -805,9 +907,15 @@ where T: DataType + PartialOrd + Default + Copy + Sync + Send, { if lowerb.len() < src.channels || upperb.len() < src.channels { - return Err(PureCvError::InvalidInput( - "Scalars must have at least as many elements as src channels".into(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "in_range_scalar: scalars must have at least as many elements as src channels \ + (src channels = {}, lowerb = {}, upperb = {})", + src.channels, + lowerb.len(), + upperb.len() + ); } dst.create(src.rows, src.cols, 1); @@ -865,9 +973,17 @@ where T: Num + Copy + Send + Sync + PartialOrd + Sub + Default + SimdElement + 'static, { if !src1.dims_match(src2) { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "absdiff: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -1124,10 +1240,12 @@ where Ok(sq_sum.sqrt()) } } - _ => Err(PureCvError::NotImplemented(format!( - "Norm type {:?} is not implemented", + _ => Err(cv_err!( + tags::CORE, + NotImplemented, + "norm: norm type {:?} is not implemented", norm_type - ))), + )), } } @@ -1273,11 +1391,11 @@ where } } } - _ => { - return Err(PureCvError::NotImplemented( - "Requested normalization type is not implemented yet".to_string(), - )) - } + _ => cv_bail!( + tags::CORE, + NotImplemented, + "normalize: requested normalization type is not implemented yet" + ), } Ok(()) } @@ -1304,7 +1422,12 @@ where } else if dim == 1 { (src.rows, 1) } else { - return Err(PureCvError::InvalidInput("dim must be 0 or 1".to_string())); + cv_bail!( + tags::CORE, + InvalidInput, + "reduce: dim must be 0 or 1, got {}", + dim + ); }; let mut dst_data = vec![T::default(); rows * cols * src.channels]; @@ -1410,9 +1533,12 @@ where T: Num + Copy + Send + Sync + 'static, { if src.channels != 1 { - return Err(PureCvError::InvalidInput( - "countNonZero requires single-channel matrix".into(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "count_non_zero: requires a single-channel matrix, got {} channels", + src.channels + ); } #[cfg(feature = "parallel")] { @@ -1507,9 +1633,17 @@ where + 'static, { if src1.rows != src2.rows || src1.cols != src2.cols || src1.channels != src2.channels { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "add_weighted: matrices must have the same dimensions (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } let mut dst = Matrix::::new(src1.rows, src1.cols, src1.channels); @@ -1699,10 +1833,15 @@ where }; if k1 != k2 { - return Err(PureCvError::InvalidDimensions(format!( - "Incompatible dimensions for GEMM: {}x{} and {}x{}", - m, k1, k2, n - ))); + cv_bail!( + tags::CORE, + InvalidDimensions, + "gemm: incompatible dimensions {}x{} and {}x{}", + m, + k1, + k2, + n + ); } let k = k1; @@ -1883,9 +2022,13 @@ where T: Num + Copy + Send + Sync + ToPrimitive + Default + SimdElement + 'static, { if src1.data.len() != src2.data.len() { - return Err(PureCvError::InvalidDimensions( - "Matrices must have the same number of elements".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "dot: matrices must have the same number of elements ({} vs {})", + src1.data.len(), + src2.data.len() + ); } #[cfg(feature = "simd")] @@ -1951,9 +2094,13 @@ where let len2 = src2.rows * src2.cols * src2.channels; if len1 != 3 || len2 != 3 { - return Err(PureCvError::InvalidDimensions( - "Cross product requires 3-element vectors".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "cross: cross product requires 3-element vectors (got {} and {})", + len1, + len2 + ); } let min_rows_cols = if src1.rows == 3 { @@ -2158,9 +2305,14 @@ where T: Num + Copy + Send + Sync + ToPrimitive + Default + 'static, { if src.rows != src.cols || src.channels != 1 { - return Err(PureCvError::InvalidDimensions( - "Inverse only supports single-channel square matrices".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "invert: only single-channel square matrices are supported (got {}×{}×{})", + src.rows, + src.cols, + src.channels + ); } let n = src.rows; @@ -2213,15 +2365,26 @@ where { if src1.rows != src1.cols || src1.channels != 1 || src2.channels != 1 || src1.rows != src2.rows { - return Err(PureCvError::InvalidDimensions( - "Linear system solver requires compatible single-channel matrices".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "solve: requires compatible single-channel matrices (src1 {}×{}×{}, src2 {}×{}×{})", + src1.rows, + src1.cols, + src1.channels, + src2.rows, + src2.cols, + src2.channels + ); } if flags != DecompTypes::DECOMP_LU { - return Err(PureCvError::NotImplemented( - "Only DECOMP_LU is currently supported".to_string(), - )); + cv_bail!( + tags::CORE, + NotImplemented, + "solve: only DECOMP_LU is currently supported, got {:?}", + flags + ); } let n = src1.rows; @@ -2252,6 +2415,11 @@ where } if max_abs < 1e-12 { + cv_log_warning!( + tags::CORE, + "solve: linear system solver failed because the matrix is singular or near-singular (pivot max_abs = {:.6e})", + max_abs + ); return Ok(false); // Singular matrix } @@ -2324,9 +2492,17 @@ where + 'static, { if !x.dims_match(y) { - return Err(PureCvError::InvalidDimensions( - "x and y must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "magnitude: x and y must have the same dimensions (x {}×{}×{}, y {}×{}×{})", + x.rows, + x.cols, + x.channels, + y.rows, + y.cols, + y.channels + ); } dst.create(x.rows, x.cols, x.channels); @@ -2392,9 +2568,17 @@ where T: DataType + ToPrimitive + FromPrimitive + Default + Copy + Send + Sync + 'static, { if !x.dims_match(y) { - return Err(PureCvError::InvalidDimensions( - "x and y must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "phase: x and y must have the same dimensions (x {}×{}×{}, y {}×{}×{})", + x.rows, + x.cols, + x.channels, + y.rows, + y.cols, + y.channels + ); } angle.create(x.rows, x.cols, x.channels); @@ -2466,9 +2650,17 @@ where T: DataType + ToPrimitive + FromPrimitive + Default + Copy + Send + Sync + 'static, { if !x.dims_match(y) { - return Err(PureCvError::InvalidDimensions( - "x and y must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "cart_to_polar: x and y must have the same dimensions (x {}×{}×{}, y {}×{}×{})", + x.rows, + x.cols, + x.channels, + y.rows, + y.cols, + y.channels + ); } mag.create(x.rows, x.cols, x.channels); @@ -2543,9 +2735,18 @@ where T: DataType + ToPrimitive + FromPrimitive + Default + Copy + Send + Sync + 'static, { if !mag.dims_match(ang) { - return Err(PureCvError::InvalidDimensions( - "magnitude and angle must have the same dimensions".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "polar_to_cart: magnitude and angle must have the same dimensions \ + (mag {}×{}×{}, ang {}×{}×{})", + mag.rows, + mag.cols, + mag.channels, + ang.rows, + ang.cols, + ang.channels + ); } x.create(mag.rows, mag.cols, mag.channels); @@ -2618,9 +2819,12 @@ where T: Default + Clone + Copy + FromPrimitive + ToPrimitive + Send + Sync + 'static, { if m.channels != 1 { - return Err(PureCvError::InvalidDimensions( - "transformation matrix must be single-channel".into(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "transform: transformation matrix must be single-channel, got {} channels", + m.channels + ); } let scn = src.channels; @@ -2629,10 +2833,15 @@ where // m must be dcn × scn OR dcn × (scn + 1) (affine) if m_cols != scn && m_cols != scn + 1 { - return Err(PureCvError::InvalidDimensions(format!( - "transformation matrix columns ({}) must equal src channels ({}) or src channels + 1 ({})", - m_cols, scn, scn + 1 - ))); + cv_bail!( + tags::CORE, + InvalidDimensions, + "transform: transformation matrix columns ({}) must equal src channels ({}) \ + or src channels + 1 ({})", + m_cols, + scn, + scn + 1 + ); } let affine = m_cols == scn + 1; @@ -2708,23 +2917,35 @@ where let scn = src.channels; if scn != 2 && scn != 3 { - return Err(PureCvError::InvalidDimensions( - "perspectiveTransform requires 2- or 3-channel input".into(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "perspective_transform: requires 2- or 3-channel input, got {} channels", + scn + ); } if m.channels != 1 { - return Err(PureCvError::InvalidDimensions( - "transformation matrix must be single-channel".into(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "perspective_transform: transformation matrix must be single-channel, got {} channels", + m.channels + ); } let n = scn + 1; // homogeneous dimension if m.rows != n || m.cols != n { - return Err(PureCvError::InvalidDimensions(format!( - "transformation matrix must be {}x{} for {}-channel input, got {}x{}", - n, n, scn, m.rows, m.cols - ))); + cv_bail!( + tags::CORE, + InvalidDimensions, + "perspective_transform: transformation matrix must be {}x{} for {}-channel input, got {}x{}", + n, + n, + scn, + m.rows, + m.cols + ); } dst.create(src.rows, src.cols, scn); @@ -2813,14 +3034,20 @@ pub fn solve_poly(coeffs: &Matrix, roots: &mut Matrix, max_iters: i32) // Flatten coefficients into a slice let total = coeffs.rows * coeffs.cols * coeffs.channels; if total < 2 { - return Err(PureCvError::InvalidInput( - "solvePoly requires at least 2 coefficients (degree >= 1)".into(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "solve_poly: requires at least 2 coefficients (degree >= 1), got {}", + total + ); } if coeffs.channels != 1 { - return Err(PureCvError::InvalidInput( - "solvePoly requires single-channel coefficient matrix".into(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "solve_poly: requires a single-channel coefficient matrix, got {} channels", + coeffs.channels + ); } let c = &coeffs.data; @@ -2829,9 +3056,12 @@ pub fn solve_poly(coeffs: &Matrix, roots: &mut Matrix, max_iters: i32) // Normalise so that c[n] == 1 (monic) let lead = c[n]; if lead.abs() < f64::EPSILON { - return Err(PureCvError::InvalidInput( - "Leading coefficient is zero".into(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "solve_poly: leading coefficient is zero ({:e})", + lead + ); } let a: Vec = c.iter().map(|&v| v / lead).collect(); @@ -2959,9 +3189,12 @@ where T: Default + Clone + Copy + PartialOrd + Send + Sync + 'static, { if src.channels != 1 { - return Err(PureCvError::InvalidInput( - "sort requires single-channel matrix".into(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "sort: requires a single-channel matrix, got {} channels", + src.channels + ); } dst.rows = src.rows; dst.cols = src.cols; @@ -3009,9 +3242,12 @@ where T: Default + Clone + Copy + PartialOrd + Send + Sync + 'static, { if src.channels != 1 { - return Err(PureCvError::InvalidInput( - "sortIdx requires single-channel matrix".into(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "sort_idx: requires a single-channel matrix, got {} channels", + src.channels + ); } dst.rows = src.rows; dst.cols = src.cols; @@ -3096,10 +3332,13 @@ pub fn kmeans( let dims = data.cols * data.channels; // dimensionality if k <= 0 || (k as usize) > n { - return Err(PureCvError::InvalidInput(format!( - "k ({}) must be in [1, {}]", - k, n - ))); + cv_bail!( + tags::CORE, + InvalidInput, + "kmeans: k ({}) must be in [1, {}]", + k, + n + ); } let k = k as usize; @@ -3478,18 +3717,23 @@ where { let lut_entries = lut_table.rows * lut_table.cols; if lut_entries != 256 { - return Err(PureCvError::InvalidInput(format!( - "LUT must have exactly 256 entries, got {}", + cv_bail!( + tags::CORE, + InvalidInput, + "lut: LUT must have exactly 256 entries, got {}", lut_entries - ))); + ); } let lut_cn = lut_table.channels; let src_cn = src.channels; if lut_cn != 1 && lut_cn != src_cn { - return Err(PureCvError::IncompatibleChannels(format!( - "LUT channels ({}) must be 1 or match source channels ({})", - lut_cn, src_cn - ))); + cv_bail!( + tags::CORE, + IncompatibleChannels, + "lut: LUT channels ({}) must be 1 or match source channels ({})", + lut_cn, + src_cn + ); } let total = src.rows * src.cols * src_cn; diff --git a/src/core/dct.rs b/src/core/dct.rs index 10b0849..80246ab 100644 --- a/src/core/dct.rs +++ b/src/core/dct.rs @@ -35,8 +35,10 @@ */ use crate::core::constants::CV_PI; -use crate::core::error::{PureCvError, Result}; +use crate::core::error::Result; +use crate::core::logging::tags; use crate::core::matrix::Matrix; +use crate::cv_bail; /// Discrete Cosine Transform. /// Currently implemented using a straightforward algorithm. @@ -45,18 +47,25 @@ where T: Copy + Into, { if src.channels != 1 { - return Err(PureCvError::InvalidInput( - "DCT only supports 1-channel images".into(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "dct: only 1-channel images are supported, got {} channels", + src.channels + ); } let rows = src.rows; let cols = src.cols; if rows == 0 || cols == 0 { - return Err(PureCvError::InvalidDimensions( - "Input must not be empty".into(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "dct: input must not be empty ({}×{})", + rows, + cols + ); } let n = rows * cols; @@ -131,18 +140,25 @@ where T: Copy + Into, { if src.channels != 1 { - return Err(PureCvError::InvalidInput( - "IDCT only supports 1-channel images".into(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "idct: only 1-channel images are supported, got {} channels", + src.channels + ); } let rows = src.rows; let cols = src.cols; if rows == 0 || cols == 0 { - return Err(PureCvError::InvalidDimensions( - "Input must not be empty".into(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "idct: input must not be empty ({}×{})", + rows, + cols + ); } let n = rows * cols; diff --git a/src/core/dft.rs b/src/core/dft.rs index 678e573..9c3b240 100644 --- a/src/core/dft.rs +++ b/src/core/dft.rs @@ -37,8 +37,10 @@ use rustfft::num_complex::Complex; use rustfft::FftPlanner; -use crate::core::error::{PureCvError, Result}; +use crate::core::error::Result; +use crate::core::logging::tags; use crate::core::matrix::Matrix; +use crate::cv_bail; // --------------------------------------------------------------------------- // DFT flag constants (matching OpenCV values) @@ -172,15 +174,20 @@ pub fn dft(src: &Matrix, flags: i32, nonzero_rows: usize) -> Res let want_real_output = (flags & DFT_REAL_OUTPUT) != 0; if src.channels != 1 && src.channels != 2 { - return Err(PureCvError::InvalidInput( - "dft requires 1-channel (real) or 2-channel (complex) input".into(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "dft: requires 1-channel (real) or 2-channel (complex) input, got {} channels", + src.channels + ); } if want_real_output && want_complex_output { - return Err(PureCvError::InvalidInput( - "DFT_REAL_OUTPUT and DFT_COMPLEX_OUTPUT are mutually exclusive".into(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "dft: DFT_REAL_OUTPUT and DFT_COMPLEX_OUTPUT are mutually exclusive" + ); } let is_complex_input = src.channels == 2; @@ -188,9 +195,13 @@ pub fn dft(src: &Matrix, flags: i32, nonzero_rows: usize) -> Res let cols = src.cols; if rows == 0 || cols == 0 { - return Err(PureCvError::InvalidDimensions( - "dft input must not be empty".into(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "dft: input must not be empty ({}×{})", + rows, + cols + ); } let active_rows = if nonzero_rows > 0 && nonzero_rows < rows { diff --git a/src/core/dynamic.rs b/src/core/dynamic.rs index a3824e1..df03549 100644 --- a/src/core/dynamic.rs +++ b/src/core/dynamic.rs @@ -34,8 +34,10 @@ * */ -use crate::core::error::{PureCvError, Result}; +use crate::core::error::Result; +use crate::core::logging::tags; use crate::core::matrix::{Depth, MatType, Matrix}; +use crate::cv_bail; /// An enum bridging type-erased dynamic usage to strongly typed generic `Matrix`. #[derive(Debug, Clone, PartialEq)] @@ -101,9 +103,7 @@ impl DynamicMatrix { Depth::CV_32F => DynamicData::F32(Matrix::from_vec(rows, cols, ch, vec![0f32; n])), Depth::CV_64F => DynamicData::F64(Matrix::from_vec(rows, cols, ch, vec![0f64; n])), Depth::CV_16F => { - return Err(PureCvError::InvalidInput( - "CV_16F is not yet supported".into(), - )) + cv_bail!(tags::CORE, InvalidInput, "new: CV_16F is not yet supported") } }; Ok(Self { data }) @@ -132,9 +132,11 @@ impl DynamicMatrix { Depth::CV_32F => DynamicData::F32(Matrix::from_vec(rows, cols, ch, vec![1f32; n])), Depth::CV_64F => DynamicData::F64(Matrix::from_vec(rows, cols, ch, vec![1f64; n])), Depth::CV_16F => { - return Err(PureCvError::InvalidInput( - "CV_16F is not yet supported".into(), - )) + cv_bail!( + tags::CORE, + InvalidInput, + "ones: CV_16F is not yet supported" + ) } }; Ok(Self { data }) @@ -152,14 +154,16 @@ impl DynamicMatrix { pub fn new_u8(rows: usize, cols: usize, channels: usize, data: Vec) -> Result { let expected = rows * cols * channels; if data.len() != expected { - return Err(PureCvError::InvalidInput(format!( - "Data length {} does not match {}×{}×{} = {}", + cv_bail!( + tags::CORE, + InvalidInput, + "new_u8: data length {} does not match {}×{}×{} = {}", data.len(), rows, cols, channels, expected - ))); + ); } Ok(Self { data: DynamicData::U8(Matrix::from_vec(rows, cols, channels, data)), @@ -173,14 +177,16 @@ impl DynamicMatrix { pub fn new_i8(rows: usize, cols: usize, channels: usize, data: Vec) -> Result { let expected = rows * cols * channels; if data.len() != expected { - return Err(PureCvError::InvalidInput(format!( - "Data length {} does not match {}×{}×{} = {}", + cv_bail!( + tags::CORE, + InvalidInput, + "new_i8: data length {} does not match {}×{}×{} = {}", data.len(), rows, cols, channels, expected - ))); + ); } Ok(Self { data: DynamicData::I8(Matrix::from_vec(rows, cols, channels, data)), @@ -194,14 +200,16 @@ impl DynamicMatrix { pub fn new_u16(rows: usize, cols: usize, channels: usize, data: Vec) -> Result { let expected = rows * cols * channels; if data.len() != expected { - return Err(PureCvError::InvalidInput(format!( - "Data length {} does not match {}×{}×{} = {}", + cv_bail!( + tags::CORE, + InvalidInput, + "new_u16: data length {} does not match {}×{}×{} = {}", data.len(), rows, cols, channels, expected - ))); + ); } Ok(Self { data: DynamicData::U16(Matrix::from_vec(rows, cols, channels, data)), @@ -215,14 +223,16 @@ impl DynamicMatrix { pub fn new_i16(rows: usize, cols: usize, channels: usize, data: Vec) -> Result { let expected = rows * cols * channels; if data.len() != expected { - return Err(PureCvError::InvalidInput(format!( - "Data length {} does not match {}×{}×{} = {}", + cv_bail!( + tags::CORE, + InvalidInput, + "new_i16: data length {} does not match {}×{}×{} = {}", data.len(), rows, cols, channels, expected - ))); + ); } Ok(Self { data: DynamicData::I16(Matrix::from_vec(rows, cols, channels, data)), @@ -236,14 +246,16 @@ impl DynamicMatrix { pub fn new_i32(rows: usize, cols: usize, channels: usize, data: Vec) -> Result { let expected = rows * cols * channels; if data.len() != expected { - return Err(PureCvError::InvalidInput(format!( - "Data length {} does not match {}×{}×{} = {}", + cv_bail!( + tags::CORE, + InvalidInput, + "new_i32: data length {} does not match {}×{}×{} = {}", data.len(), rows, cols, channels, expected - ))); + ); } Ok(Self { data: DynamicData::I32(Matrix::from_vec(rows, cols, channels, data)), @@ -257,14 +269,16 @@ impl DynamicMatrix { pub fn new_f32(rows: usize, cols: usize, channels: usize, data: Vec) -> Result { let expected = rows * cols * channels; if data.len() != expected { - return Err(PureCvError::InvalidInput(format!( - "Data length {} does not match {}×{}×{} = {}", + cv_bail!( + tags::CORE, + InvalidInput, + "new_f32: data length {} does not match {}×{}×{} = {}", data.len(), rows, cols, channels, expected - ))); + ); } Ok(Self { data: DynamicData::F32(Matrix::from_vec(rows, cols, channels, data)), @@ -278,14 +292,16 @@ impl DynamicMatrix { pub fn new_f64(rows: usize, cols: usize, channels: usize, data: Vec) -> Result { let expected = rows * cols * channels; if data.len() != expected { - return Err(PureCvError::InvalidInput(format!( - "Data length {} does not match {}×{}×{} = {}", + cv_bail!( + tags::CORE, + InvalidInput, + "new_f64: data length {} does not match {}×{}×{} = {}", data.len(), rows, cols, channels, expected - ))); + ); } Ok(Self { data: DynamicData::F64(Matrix::from_vec(rows, cols, channels, data)), diff --git a/src/core/logging.rs b/src/core/logging.rs new file mode 100644 index 0000000..b19c0d4 --- /dev/null +++ b/src/core/logging.rs @@ -0,0 +1,504 @@ +/* + * logging.rs + * purecv + * + * This file is part of purecv - WebARKit. + * + * purecv is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * purecv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with purecv. If not, see . + * + * As a special exception, the copyright holders of this library give you + * permission to link this library with independent modules to produce an + * executable, regardless of the license terms of these independent modules, and to + * copy and distribute the resulting executable under terms of your choice, + * provided that you also meet, for each linked independent module, the terms and + * conditions of the license of that module. An independent module is a module + * which is neither derived from nor based on this library. If you modify this + * library, you may extend this exception to your version of the library, but you + * are not obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. + * + * Copyright 2026 WebARKit. + * + * Author(s): Walter Perdan @kalwalt https://github.com/kalwalt + * + */ + +//! OpenCV-compatible structured logging API. +//! +//! This module mirrors [`cv::utils::logging`](https://docs.opencv.org/4.x/d5/d14/namespacecv_1_1utils_1_1logging.html) +//! and is built on top of the [`log`](https://crates.io/crates/log) facade so +//! the output backend stays the application's choice (e.g. `env_logger`, `fern`, +//! `tracing`, `console_log` on WASM). +//! +//! # Level Mapping +//! +//! | [`LogLevel`] | [`log::Level`] | Notes | +//! |----------------|--------------------|--------------------------------| +//! | `Silent` | *(off)* | Disables all logging | +//! | `Fatal` | `Error` | Collapsed onto `Error` | +//! | `Error` | `Error` | | +//! | `Warning` | `Warn` | | +//! | `Info` | `Info` | | +//! | `Debug` | `Debug` | | +//! | `Verbose` | `Trace` | Collapsed onto `Trace` | +//! +//! # Compile-time stripping +//! +//! The `log` crate provides cargo features `max_level_*` and +//! `release_max_level_*` that strip log calls at compile time, equivalent to +//! OpenCV's `CV_LOG_STRIP_LEVEL`. +//! +//! # Example +//! +//! ```rust +//! use purecv::core::logging::{self, tags, LogLevel}; +//! +//! let prev = logging::set_log_level(LogLevel::Info); +//! purecv::cv_log_info!(tags::IMGPROC, "gaussian blur, ksize = {}", 5); +//! ``` + +use core::fmt; + +// ── LogLevel enum ────────────────────────────────────────────────── + +/// Log severity levels mirroring OpenCV's `cv::utils::logging::LogLevel`. +/// +/// The ordering matches OpenCV: `Silent` is the most restrictive (no output) +/// and `Verbose` is the most permissive. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum LogLevel { + /// Suppress all log output. + Silent = 0, + /// Fatal error — maps to [`log::Level::Error`]. + Fatal = 1, + /// Error — maps to [`log::Level::Error`]. + Error = 2, + /// Warning — maps to [`log::Level::Warn`]. + Warning = 3, + /// Informational — maps to [`log::Level::Info`]. + Info = 4, + /// Debug — maps to [`log::Level::Debug`]. + Debug = 5, + /// Verbose / trace — maps to [`log::Level::Trace`]. + Verbose = 6, +} + +impl PartialOrd for LogLevel { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for LogLevel { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + (*self as u8).cmp(&(*other as u8)) + } +} + +impl fmt::Display for LogLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LogLevel::Silent => write!(f, "SILENT"), + LogLevel::Fatal => write!(f, "FATAL"), + LogLevel::Error => write!(f, "ERROR"), + LogLevel::Warning => write!(f, "WARNING"), + LogLevel::Info => write!(f, "INFO"), + LogLevel::Debug => write!(f, "DEBUG"), + LogLevel::Verbose => write!(f, "VERBOSE"), + } + } +} + +impl From for log::LevelFilter { + fn from(level: LogLevel) -> Self { + match level { + LogLevel::Silent => log::LevelFilter::Off, + LogLevel::Fatal | LogLevel::Error => log::LevelFilter::Error, + LogLevel::Warning => log::LevelFilter::Warn, + LogLevel::Info => log::LevelFilter::Info, + LogLevel::Debug => log::LevelFilter::Debug, + LogLevel::Verbose => log::LevelFilter::Trace, + } + } +} + +impl From for LogLevel { + fn from(filter: log::LevelFilter) -> Self { + match filter { + log::LevelFilter::Off => LogLevel::Silent, + log::LevelFilter::Error => LogLevel::Error, + log::LevelFilter::Warn => LogLevel::Warning, + log::LevelFilter::Info => LogLevel::Info, + log::LevelFilter::Debug => LogLevel::Debug, + log::LevelFilter::Trace => LogLevel::Verbose, + } + } +} + +impl From for LogLevel { + fn from(level: log::Level) -> Self { + match level { + log::Level::Error => LogLevel::Error, + log::Level::Warn => LogLevel::Warning, + log::Level::Info => LogLevel::Info, + log::Level::Debug => LogLevel::Debug, + log::Level::Trace => LogLevel::Verbose, + } + } +} + +// ── set / get ────────────────────────────────────────────────────── + +/// Sets the global log level and returns the previous level. +/// +/// This is the Rust equivalent of OpenCV's `cv::utils::logging::setLogLevel`. +/// +/// # Example +/// +/// ```rust +/// use purecv::core::logging::{set_log_level, get_log_level, LogLevel}; +/// +/// let prev = set_log_level(LogLevel::Debug); +/// assert_eq!(get_log_level(), LogLevel::Debug); +/// set_log_level(prev); // restore +/// ``` +pub fn set_log_level(level: LogLevel) -> LogLevel { + let previous = get_log_level(); + log::set_max_level(log::LevelFilter::from(level)); + previous +} + +/// Returns the current global log level. +/// +/// This is the Rust equivalent of OpenCV's `cv::utils::logging::getLogLevel`. +pub fn get_log_level() -> LogLevel { + LogLevel::from(log::max_level()) +} + +struct SimpleLogger; + +impl log::Log for SimpleLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + println!( + "[{}] {} - {}", + record.level(), + record.target(), + record.args() + ); + } + } + + fn flush(&self) {} +} + +static LOGGER: SimpleLogger = SimpleLogger; + +/// Initializes a simple stdout logger for purecv log messages. +/// +/// Returns an error if a logger has already been set. +/// +/// This is helpful for CLI tools or examples to quickly view logs without +/// pulling in external dependencies like `env_logger`. +pub fn init_basic_logger() -> Result<(), crate::core::PureCvError> { + log::set_logger(&LOGGER).map_err(|e| { + crate::core::PureCvError::InvalidInput(format!("Logger already set: {}", e)) + })?; + if log::max_level() == log::LevelFilter::Off { + log::set_max_level(log::LevelFilter::Info); + } + Ok(()) +} + +// ── Subsystem tags ───────────────────────────────────────────────── + +/// Per-subsystem tag constants for use with `cv_log_*!` macros. +/// +/// These map onto the `log` crate's `target:` field, enabling per-module +/// filtering via standard tooling, e.g.: +/// +/// ```text +/// RUST_LOG=purecv::imgproc=debug,purecv::core=warn +/// ``` +pub mod tags { + /// Root tag for the purecv crate. + pub const PURECV: &str = "purecv"; + /// Tag for `purecv::core` subsystem. + pub const CORE: &str = "purecv::core"; + /// Tag for `purecv::imgproc` subsystem. + pub const IMGPROC: &str = "purecv::imgproc"; + /// Tag for `purecv::features2d` subsystem. + pub const FEATURES2D: &str = "purecv::features2d"; + /// Tag for `purecv::calib3d` subsystem. + pub const CALIB3D: &str = "purecv::calib3d"; + /// Tag for `purecv::video` subsystem. + pub const VIDEO: &str = "purecv::video"; +} + +// ── Level macros ─────────────────────────────────────────────────── + +/// Log at **fatal** level (mapped to `log::error!`). +/// +/// OpenCV equivalent: `CV_LOG_FATAL(tag, ...)`. +/// +/// # Example +/// +/// ```rust +/// use purecv::core::logging::tags; +/// purecv::cv_log_fatal!(tags::CORE, "unrecoverable: {}", "out of memory"); +/// ``` +#[macro_export] +macro_rules! cv_log_fatal { + ($tag:expr, $($arg:tt)+) => { + log::error!(target: $tag, $($arg)+) + }; +} + +/// Log at **error** level. +/// +/// OpenCV equivalent: `CV_LOG_ERROR(tag, ...)`. +#[macro_export] +macro_rules! cv_log_error { + ($tag:expr, $($arg:tt)+) => { + log::error!(target: $tag, $($arg)+) + }; +} + +/// Log at **warning** level. +/// +/// OpenCV equivalent: `CV_LOG_WARNING(tag, ...)`. +#[macro_export] +macro_rules! cv_log_warning { + ($tag:expr, $($arg:tt)+) => { + log::warn!(target: $tag, $($arg)+) + }; +} + +/// Log at **info** level. +/// +/// OpenCV equivalent: `CV_LOG_INFO(tag, ...)`. +#[macro_export] +macro_rules! cv_log_info { + ($tag:expr, $($arg:tt)+) => { + log::info!(target: $tag, $($arg)+) + }; +} + +/// Log at **debug** level. +/// +/// OpenCV equivalent: `CV_LOG_DEBUG(tag, ...)`. +#[macro_export] +macro_rules! cv_log_debug { + ($tag:expr, $($arg:tt)+) => { + log::debug!(target: $tag, $($arg)+) + }; +} + +/// Log at **verbose** (trace) level. +/// +/// OpenCV equivalent: `CV_LOG_VERBOSE(tag, v, ...)`. +/// +/// The `v` parameter is the verbosity sub-level. Since the `log` crate does +/// not support sub-levels, `v` is accepted for API parity but not enforced. +/// All verbose messages map to [`log::trace!`]. +#[macro_export] +macro_rules! cv_log_verbose { + ($tag:expr, $v:expr, $($arg:tt)+) => { + log::trace!(target: $tag, $($arg)+) + }; +} + +// ── Once-per-call-site macros ────────────────────────────────────── + +/// Log at **error** level, but only on the first invocation at each call site. +/// +/// Uses a `core::sync::atomic::AtomicBool` per call site to track whether the +/// message has already been emitted. This is `no_std`-friendly. +#[macro_export] +macro_rules! cv_log_once_error { + ($tag:expr, $($arg:tt)+) => {{ + static LOGGED: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + if !LOGGED.swap(true, core::sync::atomic::Ordering::Relaxed) { + log::error!(target: $tag, $($arg)+); + } + }}; +} + +/// Log at **warning** level, but only on the first invocation at each call site. +#[macro_export] +macro_rules! cv_log_once_warning { + ($tag:expr, $($arg:tt)+) => {{ + static LOGGED: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + if !LOGGED.swap(true, core::sync::atomic::Ordering::Relaxed) { + log::warn!(target: $tag, $($arg)+); + } + }}; +} + +/// Log at **info** level, but only on the first invocation at each call site. +#[macro_export] +macro_rules! cv_log_once_info { + ($tag:expr, $($arg:tt)+) => {{ + static LOGGED: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + if !LOGGED.swap(true, core::sync::atomic::Ordering::Relaxed) { + log::info!(target: $tag, $($arg)+); + } + }}; +} + +/// Log at **debug** level, but only on the first invocation at each call site. +#[macro_export] +macro_rules! cv_log_once_debug { + ($tag:expr, $($arg:tt)+) => {{ + static LOGGED: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + if !LOGGED.swap(true, core::sync::atomic::Ordering::Relaxed) { + log::debug!(target: $tag, $($arg)+); + } + }}; +} + +// ── Conditional macros ───────────────────────────────────────────── + +/// Log at **error** level if `condition` is true. +/// +/// OpenCV equivalent: `CV_LOG_IF_ERROR(tag, condition, ...)`. +/// +/// # Example +/// +/// ```rust +/// use purecv::core::logging::tags; +/// let ksize = 4; +/// purecv::cv_log_if_error!(tags::CORE, ksize % 2 == 0, "even kernel size: {}", ksize); +/// ``` +#[macro_export] +macro_rules! cv_log_if_error { + ($tag:expr, $cond:expr, $($arg:tt)+) => { + if $cond { + log::error!(target: $tag, $($arg)+); + } + }; +} + +/// Log at **warning** level if `condition` is true. +/// +/// OpenCV equivalent: `CV_LOG_IF_WARNING(tag, condition, ...)`. +#[macro_export] +macro_rules! cv_log_if_warning { + ($tag:expr, $cond:expr, $($arg:tt)+) => { + if $cond { + log::warn!(target: $tag, $($arg)+); + } + }; +} + +/// Log at **info** level if `condition` is true. +/// +/// OpenCV equivalent: `CV_LOG_IF_INFO(tag, condition, ...)`. +#[macro_export] +macro_rules! cv_log_if_info { + ($tag:expr, $cond:expr, $($arg:tt)+) => { + if $cond { + log::info!(target: $tag, $($arg)+); + } + }; +} + +/// Log at **debug** level if `condition` is true. +/// +/// OpenCV equivalent: `CV_LOG_IF_DEBUG(tag, condition, ...)`. +#[macro_export] +macro_rules! cv_log_if_debug { + ($tag:expr, $cond:expr, $($arg:tt)+) => { + if $cond { + log::debug!(target: $tag, $($arg)+); + } + }; +} + +// ── Log-and-return helpers ───────────────────────────────────────── +// +// These fuse "log the failure with the caller's subsystem tag" and +// "produce the corresponding `PureCvError`" into a single call, so every +// error site stays a one-liner while the log level and format policy live +// in exactly one place. Use `cv_bail!` in statement position (it expands to +// a `return`) and `cv_err!` where a `PureCvError` *value* is needed (e.g. a +// match arm returning `Err(..)`). The `_debug` variants log at debug level +// for low-severity paths (e.g. `NotImplemented`, degenerate inputs). + +/// Log a **warning** with `$tag`, then `return Err(PureCvError::$variant(msg))`. +/// +/// `$variant` is a bare `PureCvError` variant name (e.g. `InvalidDimensions`); +/// the remaining arguments are formatted like `format!`. +/// +/// # Example +/// +/// ```rust +/// use purecv::core::logging::tags; +/// use purecv::core::error::{PureCvError, Result}; +/// fn add(a: usize, b: usize) -> Result { +/// if a != b { +/// purecv::cv_bail!(tags::CORE, InvalidDimensions, "add: {} != {}", a, b); +/// } +/// Ok(a + b) +/// } +/// assert!(add(1, 2).is_err()); +/// ``` +#[macro_export] +macro_rules! cv_bail { + ($tag:expr, $variant:ident, $($arg:tt)+) => {{ + let __msg = format!($($arg)+); + $crate::cv_log_warning!($tag, "{}", __msg); + return Err($crate::core::error::PureCvError::$variant(__msg)); + }}; +} + +/// Like [`cv_bail!`] but yields the `PureCvError` **value** instead of returning. +/// +/// Use in expression position, e.g. `Err(cv_err!(tags::CORE, NotImplemented, "..."))`. +#[macro_export] +macro_rules! cv_err { + ($tag:expr, $variant:ident, $($arg:tt)+) => {{ + let __msg = format!($($arg)+); + $crate::cv_log_warning!($tag, "{}", __msg); + $crate::core::error::PureCvError::$variant(__msg) + }}; +} + +/// Like [`cv_bail!`] but logs at **debug** level (for low-severity paths). +#[macro_export] +macro_rules! cv_bail_debug { + ($tag:expr, $variant:ident, $($arg:tt)+) => {{ + let __msg = format!($($arg)+); + $crate::cv_log_debug!($tag, "{}", __msg); + return Err($crate::core::error::PureCvError::$variant(__msg)); + }}; +} + +/// Like [`cv_err!`] but logs at **debug** level (for low-severity paths). +#[macro_export] +macro_rules! cv_err_debug { + ($tag:expr, $variant:ident, $($arg:tt)+) => {{ + let __msg = format!($($arg)+); + $crate::cv_log_debug!($tag, "{}", __msg); + $crate::core::error::PureCvError::$variant(__msg) + }}; +} diff --git a/src/core/matrix.rs b/src/core/matrix.rs index 8898220..0290e15 100644 --- a/src/core/matrix.rs +++ b/src/core/matrix.rs @@ -33,8 +33,10 @@ * Author(s): Walter Perdan @kalwalt https://github.com/kalwalt * */ -use crate::core::error::{PureCvError, Result}; +use crate::core::error::Result; +use crate::core::logging::tags; use crate::core::types::Scalar; +use crate::{cv_bail, cv_err}; /// Matrix depth: number of bits per element and its signedness/type. /// Follows OpenCV's depth conventions (CV_8U, CV_32F, etc.). @@ -338,11 +340,13 @@ impl Matrix { T: Default + Clone + DataType, { if mat_type.depth() != T::depth() { - return Err(PureCvError::InvalidInput(format!( - "MatType depth {:?} does not match element type {:?}", + cv_bail!( + tags::CORE, + InvalidInput, + "new_with_type: MatType depth {:?} does not match element type {:?}", mat_type.depth(), T::depth() - ))); + ); } Ok(Self::new(rows, cols, mat_type.channels())) } @@ -384,11 +388,13 @@ impl Matrix { T: Default + Clone + DataType, { if mat_type.depth() != T::depth() { - return Err(PureCvError::InvalidInput(format!( - "MatType depth {:?} does not match element type {:?}", + cv_bail!( + tags::CORE, + InvalidInput, + "create_with_type: MatType depth {:?} does not match element type {:?}", mat_type.depth(), T::depth() - ))); + ); } self.create(rows, cols, mat_type.channels()); Ok(()) @@ -479,7 +485,7 @@ impl Matrix { /// Channels beyond 4 are set to `T::default()` (zero). /// /// # Errors - /// Returns [`PureCvError::InvalidDimensions`] if `mask` does not have the + /// Returns [`crate::core::error::PureCvError::InvalidDimensions`] if `mask` does not have the /// same number of rows and columns as `self`. /// /// # Example @@ -494,10 +500,15 @@ impl Matrix { /// ``` pub fn set_to_masked(&mut self, s: Scalar, mask: &Matrix) -> Result<()> { if self.rows != mask.rows || self.cols != mask.cols { - return Err(PureCvError::InvalidDimensions(format!( - "mask {}×{} does not match matrix {}×{}", - mask.rows, mask.cols, self.rows, self.cols - ))); + cv_bail!( + tags::CORE, + InvalidDimensions, + "set_to_masked: mask {}×{} does not match matrix {}×{}", + mask.rows, + mask.cols, + self.rows, + self.cols + ); } for row in 0..self.rows { for col in 0..self.cols { @@ -524,7 +535,13 @@ impl Matrix { .data .iter() .map(|&x| { - U::from(x).ok_or_else(|| PureCvError::InvalidInput("Conversion failed".into())) + U::from(x).ok_or_else(|| { + cv_err!( + tags::CORE, + InvalidInput, + "convert_to: element conversion failed" + ) + }) }) .collect::>>()?; @@ -644,11 +661,13 @@ impl Matrix { T: DataType, { if mat_type.depth() != T::depth() { - return Err(PureCvError::InvalidInput(format!( - "MatType depth {:?} does not match element type {:?}", + cv_bail!( + tags::CORE, + InvalidInput, + "zeros_with_type: MatType depth {:?} does not match element type {:?}", mat_type.depth(), T::depth() - ))); + ); } Ok(Self::zeros(rows, cols, mat_type.channels())) } @@ -659,11 +678,13 @@ impl Matrix { T: DataType, { if mat_type.depth() != T::depth() { - return Err(PureCvError::InvalidInput(format!( - "MatType depth {:?} does not match element type {:?}", + cv_bail!( + tags::CORE, + InvalidInput, + "ones_with_type: MatType depth {:?} does not match element type {:?}", mat_type.depth(), T::depth() - ))); + ); } Ok(Self::ones(rows, cols, mat_type.channels())) } @@ -705,7 +726,7 @@ impl Matrix { /// mismatches are caught at runtime rather than producing silent garbage. /// /// # Errors - /// Returns [`PureCvError::InvalidInput`] when `mat_type.depth() != T::depth()`. + /// Returns [`crate::core::error::PureCvError::InvalidInput`] when `mat_type.depth() != T::depth()`. /// /// # Example /// ``` @@ -725,11 +746,13 @@ impl Matrix { s: Scalar, ) -> Result { if mat_type.depth() != T::depth() { - return Err(PureCvError::InvalidInput(format!( - "MatType depth {:?} does not match element type depth {:?}", + cv_bail!( + tags::CORE, + InvalidInput, + "new_with_scalar_typed_from_size: MatType depth {:?} does not match element type depth {:?}", mat_type.depth(), T::depth() - ))); + ); } let rows = size.height.into(); let cols = size.width.into(); diff --git a/src/core/metrics.rs b/src/core/metrics.rs index 6a2b007..a7a4789 100644 --- a/src/core/metrics.rs +++ b/src/core/metrics.rs @@ -34,8 +34,10 @@ * */ -use crate::core::error::{PureCvError, Result}; +use crate::core::error::Result; +use crate::core::logging::tags; use crate::core::matrix::Matrix; +use crate::cv_bail; /// Computes the Peak Signal-to-Noise Ratio (PSNR) between two matrices. /// @@ -64,9 +66,17 @@ where T: Copy + Into, { if img1.rows != img2.rows || img1.cols != img2.cols || img1.channels != img2.channels { - return Err(PureCvError::InvalidDimensions( - "Images must have same dimensions for PSNR".into(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "psnr: images must have the same dimensions (img1 {}×{}×{}, img2 {}×{}×{})", + img1.rows, + img1.cols, + img1.channels, + img2.rows, + img2.cols, + img2.channels + ); } let mut mse = 0.0; @@ -113,16 +123,29 @@ where { // Ensure vectors are column vectors of the same size. if src1.rows != src2.rows || src1.cols != 1 || src2.cols != 1 { - return Err(PureCvError::InvalidDimensions( - "src1 and src2 must be column vectors of the same size".into(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "mahalanobis: src1 and src2 must be column vectors of the same size \ + (src1 {}×{}, src2 {}×{})", + src1.rows, + src1.cols, + src2.rows, + src2.cols + ); } // Ensure covariance matrix is square and matches vector size if covar_inv.rows != covar_inv.cols || covar_inv.rows != src1.rows { - return Err(PureCvError::InvalidDimensions( - "covar_inv must be a square matrix matching vector size".into(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "mahalanobis: covar_inv must be a square matrix matching vector size \ + (covar_inv {}×{}, vector size {})", + covar_inv.rows, + covar_inv.cols, + src1.rows + ); } let n = src1.rows; diff --git a/src/core/rng.rs b/src/core/rng.rs index 9a5d40c..d459196 100644 --- a/src/core/rng.rs +++ b/src/core/rng.rs @@ -35,9 +35,11 @@ */ use crate::core::constants::CV_2PI; -use crate::core::error::{PureCvError, Result}; +use crate::core::error::Result; +use crate::core::logging::tags; use crate::core::types::Scalar; use crate::core::Matrix; +use crate::cv_bail; use num_traits::{FromPrimitive, ToPrimitive}; use std::cell::RefCell; @@ -177,9 +179,11 @@ where T: Default + Clone + FromPrimitive + ToPrimitive + Send + Sync, { if dst.data.is_empty() { - return Err(PureCvError::InvalidDimensions( - "destination matrix is empty".into(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "randu: destination matrix is empty" + ); } let channels = dst.channels; @@ -229,9 +233,11 @@ where T: Default + Clone + FromPrimitive + ToPrimitive + Send + Sync, { if dst.data.is_empty() { - return Err(PureCvError::InvalidDimensions( - "destination matrix is empty".into(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "randn: destination matrix is empty" + ); } let channels = dst.channels; diff --git a/src/core/structural.rs b/src/core/structural.rs index aa3ab82..a9878cc 100644 --- a/src/core/structural.rs +++ b/src/core/structural.rs @@ -34,9 +34,11 @@ * */ -use crate::core::error::{PureCvError, Result}; +use crate::core::error::Result; +use crate::core::logging::tags; use crate::core::types::Scalar; use crate::core::Matrix; +use crate::{cv_bail, cv_err}; #[cfg(feature = "parallel")] use rayon::prelude::*; @@ -219,9 +221,11 @@ where T: Copy + Send + Sync + Default + 'static, { if src.is_empty() || dst.is_empty() || from_to.is_empty() { - return Err(PureCvError::InvalidInput( - "Input/Output/from_to cannot be empty".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidInput, + "mix_channels: input/output/from_to cannot be empty" + ); } let rows = src[0].rows; @@ -230,16 +234,20 @@ where // Validate dimensions for m in src { if m.rows != rows || m.cols != cols { - return Err(PureCvError::InvalidDimensions( - "All source matrices must have the same size".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "mix_channels: all source matrices must have the same size" + ); } } for m in dst.iter() { if m.rows != rows || m.cols != cols { - return Err(PureCvError::InvalidDimensions( - "All destination matrices must have the same size".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "mix_channels: all destination matrices must have the same size" + ); } } @@ -431,9 +439,11 @@ where T: Copy + Send + Sync + Default + 'static, { if mv.is_empty() { - return Err(PureCvError::InvalidDimensions( - "Input vector is empty".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "merge: input vector is empty" + ); } let rows = mv[0].rows; @@ -442,9 +452,11 @@ where for m in mv { if m.rows != rows || m.cols != cols || m.channels != 1 { - return Err(PureCvError::InvalidDimensions( - "All matrices must have the same size and 1 channel".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "merge: all matrices must have the same size and 1 channel" + ); } } @@ -499,8 +511,11 @@ where let t = transpose(src)?; flip(&t, 0) } - _ => Err(PureCvError::InvalidDimensions( - "Invalid rotate_code. Must be 0, 1, or 2".to_string(), + _ => Err(cv_err!( + tags::CORE, + InvalidDimensions, + "rotate: invalid rotate_code {}, must be 0, 1, or 2", + rotate_code )), } } @@ -511,9 +526,13 @@ where T: Copy + Send + Sync + Default + 'static, { if ny == 0 || nx == 0 { - return Err(PureCvError::InvalidDimensions( - "ny and nx must be > 0".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "repeat: ny and nx must be > 0 (got ny = {}, nx = {})", + ny, + nx + ); } let mut dst = Matrix::::new(src.rows * ny, src.cols * nx, src.channels); @@ -570,20 +589,26 @@ where }; if !total_elements.is_multiple_of(actual_new_channels) { - return Err(PureCvError::InvalidDimensions(format!( - "Total elements ({}) is not divisible by new_channels ({})", - total_elements, actual_new_channels - ))); + cv_bail!( + tags::CORE, + InvalidDimensions, + "reshape: total elements ({}) is not divisible by new_channels ({})", + total_elements, + actual_new_channels + ); } let total_pixels = total_elements / actual_new_channels; let actual_new_rows = if new_rows == 0 { src.rows } else { new_rows }; if !total_pixels.is_multiple_of(actual_new_rows) { - return Err(PureCvError::InvalidDimensions(format!( - "Total pixels ({}) is not divisible by new_rows ({})", - total_pixels, actual_new_rows - ))); + cv_bail!( + tags::CORE, + InvalidDimensions, + "reshape: total pixels ({}) is not divisible by new_rows ({})", + total_pixels, + actual_new_rows + ); } let new_cols = total_pixels / actual_new_rows; @@ -600,9 +625,7 @@ where T: Copy + Send + Sync + Default + 'static, { if src.is_empty() { - return Err(PureCvError::InvalidInput( - "Input array is empty".to_string(), - )); + cv_bail!(tags::CORE, InvalidInput, "hconcat: input array is empty"); } let rows = src[0].rows; @@ -611,9 +634,11 @@ where for m in src { if m.rows != rows || m.channels != channels { - return Err(PureCvError::InvalidDimensions( - "All matrices must have the same number of rows and channels".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "hconcat: all matrices must have the same number of rows and channels" + ); } total_cols += m.cols; } @@ -661,9 +686,7 @@ where T: Copy + Send + Sync + Default + 'static, { if src.is_empty() { - return Err(PureCvError::InvalidInput( - "Input array is empty".to_string(), - )); + cv_bail!(tags::CORE, InvalidInput, "vconcat: input array is empty"); } let cols = src[0].cols; @@ -672,9 +695,11 @@ where for m in src { if m.cols != cols || m.channels != channels { - return Err(PureCvError::InvalidDimensions( - "All matrices must have the same number of columns and channels".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "vconcat: all matrices must have the same number of columns and channels" + ); } total_rows += m.rows; } @@ -696,10 +721,13 @@ where T: Copy + Send + Sync + Default + 'static, { if coi >= src.channels { - return Err(PureCvError::InvalidInput(format!( - "Channel index {} is out of range (total channels: {})", - coi, src.channels - ))); + cv_bail!( + tags::CORE, + InvalidInput, + "extract_channel: channel index {} is out of range (total channels: {})", + coi, + src.channels + ); } let mut dst = Matrix::::new(src.rows, src.cols, 1); @@ -728,16 +756,21 @@ where T: Copy + Send + Sync + Default + 'static, { if coi >= dst.channels { - return Err(PureCvError::InvalidInput(format!( - "Channel index {} is out of range (destination channels: {})", - coi, dst.channels - ))); + cv_bail!( + tags::CORE, + InvalidInput, + "insert_channel: channel index {} is out of range (destination channels: {})", + coi, + dst.channels + ); } if src.rows != dst.rows || src.cols != dst.cols || src.channels != 1 { - return Err(PureCvError::InvalidDimensions( - "Source must be single-channel and match destination size".to_string(), - )); + cv_bail!( + tags::CORE, + InvalidDimensions, + "insert_channel: source must be single-channel and match destination size" + ); } let channels = dst.channels; diff --git a/src/core/tests.rs b/src/core/tests.rs index 671c904..cf016bf 100644 --- a/src/core/tests.rs +++ b/src/core/tests.rs @@ -1578,4 +1578,185 @@ mod core_tests { assert_eq!(src.get(0, 0, 0), Some(&10)); // src unchanged assert_eq!(dst.get(0, 0, 0), Some(&999)); } + + // ── Logging tests ────────────────────────────────────────────────── + + use crate::core::logging::{self, tags, LogLevel}; + use crate::{ + cv_log_debug, cv_log_error, cv_log_fatal, cv_log_if_debug, cv_log_if_error, cv_log_if_info, + cv_log_if_warning, cv_log_info, cv_log_once_debug, cv_log_once_error, cv_log_once_info, + cv_log_once_warning, cv_log_verbose, cv_log_warning, + }; + + #[test] + fn test_log_level_ordering() { + assert!(LogLevel::Silent < LogLevel::Fatal); + assert!(LogLevel::Fatal < LogLevel::Error); + assert!(LogLevel::Error < LogLevel::Warning); + assert!(LogLevel::Warning < LogLevel::Info); + assert!(LogLevel::Info < LogLevel::Debug); + assert!(LogLevel::Debug < LogLevel::Verbose); + } + + #[test] + fn test_log_level_to_level_filter_conversion() { + use log::LevelFilter; + + assert_eq!(LevelFilter::from(LogLevel::Silent), LevelFilter::Off); + assert_eq!(LevelFilter::from(LogLevel::Fatal), LevelFilter::Error); + assert_eq!(LevelFilter::from(LogLevel::Error), LevelFilter::Error); + assert_eq!(LevelFilter::from(LogLevel::Warning), LevelFilter::Warn); + assert_eq!(LevelFilter::from(LogLevel::Info), LevelFilter::Info); + assert_eq!(LevelFilter::from(LogLevel::Debug), LevelFilter::Debug); + assert_eq!(LevelFilter::from(LogLevel::Verbose), LevelFilter::Trace); + } + + #[test] + fn test_level_filter_to_log_level_conversion() { + use log::LevelFilter; + + assert_eq!(LogLevel::from(LevelFilter::Off), LogLevel::Silent); + assert_eq!(LogLevel::from(LevelFilter::Error), LogLevel::Error); + assert_eq!(LogLevel::from(LevelFilter::Warn), LogLevel::Warning); + assert_eq!(LogLevel::from(LevelFilter::Info), LogLevel::Info); + assert_eq!(LogLevel::from(LevelFilter::Debug), LogLevel::Debug); + assert_eq!(LogLevel::from(LevelFilter::Trace), LogLevel::Verbose); + } + + #[test] + fn test_log_level_display() { + assert_eq!(LogLevel::Silent.to_string(), "SILENT"); + assert_eq!(LogLevel::Fatal.to_string(), "FATAL"); + assert_eq!(LogLevel::Error.to_string(), "ERROR"); + assert_eq!(LogLevel::Warning.to_string(), "WARNING"); + assert_eq!(LogLevel::Info.to_string(), "INFO"); + assert_eq!(LogLevel::Debug.to_string(), "DEBUG"); + assert_eq!(LogLevel::Verbose.to_string(), "VERBOSE"); + } + + #[test] + fn test_set_log_level_returns_previous() { + // Save the current level to restore later + let original = logging::get_log_level(); + + let prev = logging::set_log_level(LogLevel::Debug); + assert_eq!(prev, original); + + let prev2 = logging::set_log_level(LogLevel::Warning); + assert_eq!(prev2, LogLevel::Debug); + + // Restore + logging::set_log_level(original); + } + + #[test] + fn test_log_level_round_trip() { + let original = logging::get_log_level(); + + logging::set_log_level(LogLevel::Info); + assert_eq!(logging::get_log_level(), LogLevel::Info); + + logging::set_log_level(LogLevel::Verbose); + assert_eq!(logging::get_log_level(), LogLevel::Verbose); + + logging::set_log_level(LogLevel::Silent); + assert_eq!(logging::get_log_level(), LogLevel::Silent); + + // Restore + logging::set_log_level(original); + } + + #[test] + fn test_cv_log_macros_compile() { + // Smoke test: verify all macros expand and execute without panic. + // No log backend is installed, so messages are silently dropped. + cv_log_fatal!(tags::CORE, "fatal test {}", 1); + cv_log_error!(tags::CORE, "error test {}", 2); + cv_log_warning!(tags::IMGPROC, "warning test {}", 3); + cv_log_info!(tags::PURECV, "info test {}", 4); + cv_log_debug!(tags::FEATURES2D, "debug test {}", 5); + cv_log_verbose!(tags::VIDEO, 0, "verbose test {}", 6); + } + + #[test] + fn test_cv_log_once_macros_compile() { + // Once macros should compile and not panic + cv_log_once_error!(tags::CORE, "once error"); + cv_log_once_warning!(tags::IMGPROC, "once warning {}", 42); + cv_log_once_info!(tags::PURECV, "once info"); + cv_log_once_debug!(tags::CALIB3D, "once debug"); + } + + #[test] + fn test_cv_log_if_macros_compile() { + // Conditional macros with both true and false conditions + cv_log_if_error!(tags::CORE, true, "if error true"); + cv_log_if_error!(tags::CORE, false, "if error false"); + cv_log_if_warning!(tags::IMGPROC, 2 > 1, "if warning {}", "yes"); + cv_log_if_info!(tags::PURECV, false, "should not log"); + cv_log_if_debug!(tags::VIDEO, true, "if debug {}", 99); + } + + #[test] + fn test_cv_bail_logs_and_returns_error() { + use crate::core::error::{PureCvError, Result}; + + fn check(a: usize, b: usize) -> Result<()> { + if a != b { + crate::cv_bail!(tags::CORE, InvalidDimensions, "mismatch: {} != {}", a, b); + } + Ok(()) + } + + // Failing path returns the expected variant with the formatted message. + match check(1, 2) { + Err(PureCvError::InvalidDimensions(msg)) => assert_eq!(msg, "mismatch: 1 != 2"), + other => panic!("expected InvalidDimensions, got {other:?}"), + } + // Passing path returns Ok. + assert!(check(3, 3).is_ok()); + } + + #[test] + fn test_cv_err_yields_error_value() { + use crate::core::error::PureCvError; + + fn make(kind: u8) -> PureCvError { + match kind { + 0 => crate::cv_err!(tags::CORE, InvalidInput, "bad input {}", kind), + _ => crate::cv_err!(tags::CORE, NotImplemented, "todo"), + } + } + + assert_eq!( + make(0), + PureCvError::InvalidInput("bad input 0".to_string()) + ); + assert_eq!(make(9), PureCvError::NotImplemented("todo".to_string())); + } + + #[test] + fn test_cv_bail_debug_and_cv_err_debug() { + use crate::core::error::{PureCvError, Result}; + + fn bail(x: i32) -> Result<()> { + crate::cv_bail_debug!(tags::CORE, NotImplemented, "not yet: {}", x); + } + fn err() -> PureCvError { + crate::cv_err_debug!(tags::CORE, OutOfBounds, "oob") + } + + assert!(matches!(bail(7), Err(PureCvError::NotImplemented(_)))); + assert_eq!(err(), PureCvError::OutOfBounds("oob".to_string())); + } + + #[test] + fn test_tags_constants() { + assert_eq!(tags::PURECV, "purecv"); + assert_eq!(tags::CORE, "purecv::core"); + assert_eq!(tags::IMGPROC, "purecv::imgproc"); + assert_eq!(tags::FEATURES2D, "purecv::features2d"); + assert_eq!(tags::CALIB3D, "purecv::calib3d"); + assert_eq!(tags::VIDEO, "purecv::video"); + } } diff --git a/src/core/types.rs b/src/core/types.rs index 50a5844..ff5a6bc 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -38,7 +38,9 @@ use std::ops::{Add, Div, Index, IndexMut, Mul, Sub}; use num_traits::{CheckedDiv, Zero}; -use crate::core::error::{PureCvError, Result}; +use crate::core::error::Result; +use crate::core::logging::tags; +use crate::cv_err; pub type Uchar = u8; pub type Schar = i8; @@ -402,7 +404,7 @@ impl Scalar { /// `Div>` impl. /// /// # Errors - /// Returns [`PureCvError::InvalidInput`] naming the first channel whose + /// Returns [`crate::core::error::PureCvError::InvalidInput`] naming the first channel whose /// divisor is zero. /// /// # Example @@ -418,7 +420,12 @@ impl Scalar { pub fn checked_div(self, rhs: Scalar) -> Result { let div_ch = |a: T, b: T, ch: usize| { a.checked_div(&b).ok_or_else(|| { - PureCvError::InvalidInput(format!("Division by zero in channel {ch}")) + cv_err!( + tags::CORE, + InvalidInput, + "checked_div: division by zero in channel {}", + ch + ) }) }; Ok(Self { diff --git a/src/core/utils.rs b/src/core/utils.rs index a5c0600..b49f89a 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -76,16 +76,9 @@ impl Range { } } -/// Sets the global log level. -/// This is a wrapper around the `log` crate's level filter. -pub fn set_log_level(level: log::LevelFilter) { - log::set_max_level(level); -} - -/// Returns the current global log level. -pub fn get_log_level() -> log::LevelFilter { - log::max_level() -} +// Log level functions have moved to `core::logging`. +// Re-exported here for backwards-compatible import paths. +pub use crate::core::logging::{get_log_level, set_log_level}; /// Border interpolation helper. /// Returns the index of a pixel that would be at the given position `p` diff --git a/src/features.rs b/src/features.rs index 3ee1d14..f57cf70 100644 --- a/src/features.rs +++ b/src/features.rs @@ -34,6 +34,13 @@ * */ +//! Feature detection and description — the `features` module. +//! +//! Placeholder for pure-Rust keypoint detectors and descriptors (e.g. FAST, +//! ORB) mirroring OpenCV's feature-detection APIs. These are not yet +//! implemented; the module is reserved so downstream code and documentation +//! can reference a stable path as the algorithms land. + //pub mod fast; //pub mod orb; diff --git a/src/imgproc.rs b/src/imgproc.rs index d9482d8..6475188 100644 --- a/src/imgproc.rs +++ b/src/imgproc.rs @@ -34,6 +34,23 @@ * */ +//! Image processing — the `imgproc` module. +//! +//! This module mirrors OpenCV's `imgproc` module and implements common image +//! processing algorithms in pure Rust, operating on the core [`Matrix`] type: +//! +//! - [`color`] — color-space conversions ([`cvt_color`] and friends). +//! - [`filter`] — linear/nonlinear filtering (blur, Gaussian, Sobel, …). +//! - [`derivatives`] and [`edge`] — image gradients and edge detection. +//! - [`threshold`](mod@threshold) — fixed and adaptive thresholding. +//! - [`morph`] — morphological operations (erode, dilate, `morphology_ex`). +//! - [`geometric`] — geometric transforms ([`resize`](resize::resize), +//! [`warp_perspective`], `remap`). +//! - [`pyramid`] — image pyramids (`pyr_down`, `pyr_up`). +//! - [`hough`] and [`feature`] — shape/feature detection. +//! +//! [`Matrix`]: crate::core::Matrix + pub mod color; pub mod derivatives; pub mod edge; diff --git a/src/lib.rs b/src/lib.rs index 065302c..c253558 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,7 @@ pub mod prelude { find_fundamental_mat, find_homography, init_undistort_rectify_map, rodrigues, solve_pnp, solve_pnp_ransac, FundamentalMatMethod, HomographyMethod, SolvePnPMethod, }; + pub use crate::core::logging::{tags, LogLevel}; pub use crate::core::types::{ BorderTypes, Point2f, Point2i, Point3f, Rect2f, Rect2i, Scalar, Size2f, Size2i, TermCriteria, TermType, Vec2b, Vec2d, Vec2f, Vec2i, Vec2s, Vec3b, Vec3d, Vec3f, Vec3i, diff --git a/src/version.rs b/src/version.rs index 9fdeca4..a63e302 100644 --- a/src/version.rs +++ b/src/version.rs @@ -60,7 +60,7 @@ pub fn get_version() -> &'static str { /// Call this early in your application (e.g. during initialization) so the /// version appears in the console / log output. pub fn print_version() { - log::info!("purecv v{}", VERSION); + crate::cv_log_info!(crate::core::logging::tags::PURECV, "purecv v{}", VERSION); } #[cfg(test)] diff --git a/src/video/optical_flow.rs b/src/video/optical_flow.rs index 86ea0d6..30ccc4b 100644 --- a/src/video/optical_flow.rs +++ b/src/video/optical_flow.rs @@ -53,8 +53,10 @@ //! | `tryReuseInputImage` optimisation flag | not implemented (correctness only) | use crate::core::error::{PureCvError, Result}; +use crate::core::logging::tags; use crate::core::types::{BorderTypes, Point2f, Size2i, TermCriteria, TermType}; use crate::core::Matrix; +use crate::cv_log_debug; use crate::imgproc::derivatives::sobel; use crate::imgproc::pyramid::pyr_down; @@ -366,6 +368,15 @@ fn lk_single_level( let det = h00 * h11 - h01 * h01; if min_eigen < min_eigen_threshold || det.abs() < f64::EPSILON { + cv_log_debug!( + tags::VIDEO, + "LK tracking lost at ({:.2}, {:.2}): min_eigen = {:.6} (threshold = {:.6}), det = {:.6e}", + px, + py, + min_eigen, + min_eigen_threshold, + det.abs() + ); return (init_u, init_v, min_eigen, false); }