From d194ec21f9002dfb4d35455feee75f93e4b857a5 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Thu, 16 Jul 2026 22:36:44 +0200 Subject: [PATCH 01/11] feat(core): add OpenCV-style logging facade (#80) Add a new core::logging module mirroring cv::utils::logging, built on the existing log crate facade. Provides: - LogLevel enum (Silent/Fatal/Error/Warning/Info/Debug/Verbose) with From conversions to/from log::LevelFilter and log::Level - set_log_level/get_log_level with return-previous semantics - tags submodule with per-subsystem constants (PURECV, CORE, IMGPROC, FEATURES2D, CALIB3D, VIDEO) - Level macros: cv_log_fatal/error/warning/info/debug/verbose - Once-per-call-site macros: cv_log_once_error/warning/info/debug (AtomicBool-based, no_std-friendly) - Conditional macros: cv_log_if_error/warning/info/debug Breaking change: set_log_level/get_log_level now use LogLevel instead of log::LevelFilter (pre-1.0, zero callers in codebase). Closes #80 --- src/core.rs | 2 + src/core/logging.rs | 396 ++++++++++++++++++++++++++++++++++++++++++++ src/core/tests.rs | 128 ++++++++++++++ src/core/utils.rs | 13 +- src/lib.rs | 1 + src/version.rs | 2 +- 6 files changed, 531 insertions(+), 11 deletions(-) create mode 100644 src/core/logging.rs diff --git a/src/core.rs b/src/core.rs index 30866a9..0a5150d 100644 --- a/src/core.rs +++ b/src/core.rs @@ -42,6 +42,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 +80,7 @@ pub use self::dft::{ }; pub use self::dynamic::{DynamicData, DynamicMatrix}; pub use self::error::{PureCvError, Result}; +pub use self::logging::{get_log_level, 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/logging.rs b/src/core/logging.rs new file mode 100644 index 0000000..52a9b03 --- /dev/null +++ b/src/core/logging.rs @@ -0,0 +1,396 @@ +/* + * 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()) +} + +// ── 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)+); + } + }; +} diff --git a/src/core/tests.rs b/src/core/tests.rs index 671c904..4b46ec7 100644 --- a/src/core/tests.rs +++ b/src/core/tests.rs @@ -1578,4 +1578,132 @@ 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_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/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/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)] From 90283b0a98faeab896481c84d968f1f3501e1d4d Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Sat, 18 Jul 2026 11:19:12 +0200 Subject: [PATCH 02/11] feat(core): add basic logger and migrate example applications to logging macros (#80) --- examples/filters.rs | 43 ++++++++++++++++------- examples/match_features.rs | 72 +++++++++++++++++++++++++------------- examples/optical_flow.rs | 57 +++++++++++++++++++++--------- src/core.rs | 2 +- src/core/logging.rs | 38 ++++++++++++++++++++ 5 files changed, 156 insertions(+), 56 deletions(-) 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/core.rs b/src/core.rs index 0a5150d..d758d60 100644 --- a/src/core.rs +++ b/src/core.rs @@ -80,7 +80,7 @@ pub use self::dft::{ }; pub use self::dynamic::{DynamicData, DynamicMatrix}; pub use self::error::{PureCvError, Result}; -pub use self::logging::{get_log_level, set_log_level, LogLevel}; +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/logging.rs b/src/core/logging.rs index 52a9b03..e102274 100644 --- a/src/core/logging.rs +++ b/src/core/logging.rs @@ -186,6 +186,44 @@ 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. From e8ccb3e026b61ac1876b7b3d5c29545f05d7f030 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Sat, 18 Jul 2026 13:34:28 +0200 Subject: [PATCH 03/11] chore(core): fix formatting in logging.rs (#80) --- src/core/logging.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/logging.rs b/src/core/logging.rs index e102274..35962e8 100644 --- a/src/core/logging.rs +++ b/src/core/logging.rs @@ -216,8 +216,9 @@ static LOGGER: SimpleLogger = SimpleLogger; /// 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)))?; + 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); } From f0b0cc5f87856acf2b62f749a3c5545acc436475 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Sat, 18 Jul 2026 18:01:47 +0200 Subject: [PATCH 04/11] feat(video): add debug logging inside calc_optical_flow_pyramid_lk (#80) --- src/video/optical_flow.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) 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); } From f7da5fe2e28a333e066963e85161375087ca1892 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Sat, 18 Jul 2026 18:37:12 +0200 Subject: [PATCH 05/11] feat(core): add warning logs for solve and solve_pnp_ransac failure cases (#80) --- src/calib3d/pose.rs | 7 +++++++ src/core/arithm.rs | 7 +++++++ 2 files changed, 14 insertions(+) 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/arithm.rs b/src/core/arithm.rs index 40e0086..b2588fb 100644 --- a/src/core/arithm.rs +++ b/src/core/arithm.rs @@ -36,8 +36,10 @@ use crate::core::constants::CV_2PI; use crate::core::error::{PureCvError, 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 num_traits::{Bounded, FromPrimitive, Num, SaturatingAdd, SaturatingSub, ToPrimitive}; use std::ops::{BitAnd, BitOr, BitXor, Not, Sub}; @@ -2252,6 +2254,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 } From 5a468f501de8e9472ff5006b24338bb91a504891 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Sun, 19 Jul 2026 23:16:12 +0200 Subject: [PATCH 06/11] feat(core): add cv_bail!/cv_err! log-and-return macros (#80) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fuse "log the failure with the caller's subsystem tag" and "produce the matching PureCvError" into a single call, so every error site stays a one-liner while log level and format policy live in one place. - cv_bail! — log warning, then `return Err(PureCvError::Variant(msg))` - cv_err! — log warning, yield the PureCvError value (expression position) - cv_bail_debug! / cv_err_debug! — debug-level variants for low-severity paths Add unit tests and a doctest covering all four macros. Purely additive; no call sites migrated yet. Co-Authored-By: Claude Opus 4.8 --- src/core/logging.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++ src/core/tests.rs | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/src/core/logging.rs b/src/core/logging.rs index 35962e8..b19c0d4 100644 --- a/src/core/logging.rs +++ b/src/core/logging.rs @@ -433,3 +433,72 @@ macro_rules! cv_log_if_debug { } }; } + +// ── 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/tests.rs b/src/core/tests.rs index 4b46ec7..cf016bf 100644 --- a/src/core/tests.rs +++ b/src/core/tests.rs @@ -1697,6 +1697,59 @@ mod core_tests { 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"); From fd9a28779db319b2f5aa77a1c2b14f96e8f416e4 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Tue, 21 Jul 2026 11:22:48 +0200 Subject: [PATCH 07/11] feat(core): log input-validation failures in arithm and matrix (#80) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate every `Err(PureCvError::…)` input-validation site in arithm.rs (42 sites) and matrix.rs (7 sites) to the cv_bail!/cv_err! log-and-return macros, so bad input is logged at warning level with the caller's CORE tag. Each message now names its function and interpolates the actual offending values (dims, channel counts, args) instead of a static string, e.g. "add: matrices must have the same dimensions (src1 4×4×3, src2 2×2×1)". The pre-existing solve() singular-matrix log is left untouched. Drop the now-unused PureCvError import from both files. Co-Authored-By: Claude Opus 4.8 --- src/core/arithm.rs | 501 +++++++++++++++++++++++++++++++++------------ src/core/matrix.rs | 69 ++++--- 2 files changed, 415 insertions(+), 155 deletions(-) diff --git a/src/core/arithm.rs b/src/core/arithm.rs index b2588fb..2063634 100644 --- a/src/core/arithm.rs +++ b/src/core/arithm.rs @@ -35,11 +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}; @@ -394,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); @@ -414,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); @@ -434,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); @@ -454,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); @@ -480,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); @@ -500,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); @@ -520,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); @@ -582,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); @@ -602,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); @@ -622,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); @@ -681,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); @@ -748,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); @@ -807,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); @@ -867,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); @@ -1126,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 - ))), + )), } } @@ -1275,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(()) } @@ -1306,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]; @@ -1412,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")] { @@ -1509,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); @@ -1701,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; @@ -1885,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")] @@ -1953,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 { @@ -2160,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; @@ -2215,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; @@ -2331,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); @@ -2399,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); @@ -2473,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); @@ -2550,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); @@ -2625,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; @@ -2636,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; @@ -2715,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); @@ -2820,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; @@ -2836,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(); @@ -2966,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; @@ -3016,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; @@ -3103,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; @@ -3485,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/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(); From 3cd238bfd597b60873d2d48d1807a5f91b01e749 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Tue, 21 Jul 2026 16:41:17 +0200 Subject: [PATCH 08/11] doc(core): add module-level docs for core, imgproc and features (#80) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These three modules had no `//!` summary, so they rendered with a blank description on the crate root docs page while siblings (calib3d, features2d, video, …) had one. Add overviews mirroring their style: - core: Matrix/Scalar, arithm/solvers, dft/dct, error, logging - imgproc: color, filter, edge, threshold, morph, geometric, pyramid - features: placeholder note for the not-yet-implemented FAST/ORB APIs Co-Authored-By: Claude Opus 4.8 --- src/core.rs | 18 ++++++++++++++++++ src/features.rs | 7 +++++++ src/imgproc.rs | 17 +++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/src/core.rs b/src/core.rs index d758d60..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")] 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; From 1f751ddfa5f4348bdc02d427d3c248802535e23c Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Tue, 21 Jul 2026 18:48:48 +0200 Subject: [PATCH 09/11] feat(core): log input-validation failures across remaining core modules (#80) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate every Err(PureCvError::…) input-validation site in the remaining core modules to the cv_bail!/cv_err! log-and-return macros, completing the core coverage started in arithm.rs and matrix.rs: - dct.rs (4), dft.rs (3), metrics.rs (3), rng.rs (2), types.rs (1) - dynamic.rs (9), structural.rs (16) Each message now names its function and interpolates the actual offending values (dims, channel counts, lengths). Drop the now-unused PureCvError import from these files (types.rs keeps a full-path doc link). Co-Authored-By: Claude Opus 4.8 --- src/core/dct.rs | 42 +++++++++---- src/core/dft.rs | 31 +++++++--- src/core/dynamic.rs | 72 +++++++++++++--------- src/core/metrics.rs | 43 ++++++++++--- src/core/rng.rs | 20 +++--- src/core/structural.rs | 137 +++++++++++++++++++++++++---------------- src/core/types.rs | 13 +++- 7 files changed, 235 insertions(+), 123 deletions(-) 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/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/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 { From caf69068fe28fcf1b7628fe06e07d89c9abd5fb9 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Thu, 23 Jul 2026 15:59:20 +0200 Subject: [PATCH 10/11] doc(core): document the logging facility in the README (#80) Add a `purecv-core` feature bullet for the OpenCV-style logging facade and a "Logging" usage subsection showing init_basic_logger(), set_log_level(), the warning emitted on invalid input, and the cv_log_*! macros with per-subsystem RUST_LOG filtering. CHANGELOG.md is git-cliff generated and picks these commits up at release. Co-Authored-By: Claude Opus 4.8 --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 5fd7a73..59aaf56 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Unlike existing wrappers, **PureCV** is a native rewrite. It aims to provide: - **Random Number Generation:** `randu` (uniform distribution), `randn` (normal/Gaussian distribution), `set_rng_seed`. - **Channel Management:** `split`, `merge`, `mix_channels`. - **Utilities:** `add_weighted`, `check_range`, `absdiff`, `get_tick_count`, `get_tick_frequency`. +- **Logging** (OpenCV-style): a `cv::utils::logging`-compatible facade over the [`log`](https://crates.io/crates/log) crate — a 7-level `LogLevel` with `set_log_level`/`get_log_level`, per-subsystem `tags`, `cv_log_*!` macros, and `cv_bail!`/`cv_err!` log-and-return helpers used throughout `core` to report invalid input (wrong dimensions, channel mismatches, …). Bring your own backend (`env_logger`, `tracing`, …) or call `init_basic_logger()` for quick stdout output. - **Mathematical Constants:** OpenCV-compatible constants — `CV_PI`, `CV_PI_2`, `CV_2PI`, `CV_PI_4`, `CV_LOG2`, `CV_LN2`, `CV_E`, `CV_LN10`, `CV_SQRT2` — backed by `std::f64::consts` for maximum precision. - **ndarray Interop:** Optional, zero-cost conversions to/from `ndarray::Array3` via the `ndarray` feature flag. - **SIMD Acceleration** (`simd` feature): Trait-based dispatch via `pulp` for `f32`, `f64`, and `u8` types. Accelerated operations include `add`, `sub`, `mul`, `div`, `min`, `max`, `sqrt`, `dot`, `sum`, `add_weighted`, `convert_scale_abs`, `magnitude`, `simd_row_min_max`, `simd_min_max_col`, `simd_gaussian_5tap_h/v`, and `simd_remap_bilinear_row`/`simd_remap_nearest_row`. Falls back to scalar loops at zero cost when disabled. @@ -162,6 +163,40 @@ fn main() -> Result<(), Box> { } ``` +### Logging + +PureCV mirrors OpenCV's `cv::utils::logging` on top of the [`log`](https://crates.io/crates/log) +facade, so the output backend stays your choice (`env_logger`, `tracing`, +`console_log` on WASM, …). For a quick start, `init_basic_logger()` installs a +simple stdout logger. Internally, `core` logs a warning whenever a function +rejects invalid input: + +```rust +use purecv::core::arithm; +use purecv::core::logging::{self, LogLevel}; +use purecv::core::Matrix; + +fn main() { + // Install the built-in stdout logger and let warnings through. + logging::init_basic_logger().ok(); + logging::set_log_level(LogLevel::Warning); + + // Mismatched dimensions -> logs a warning AND returns Err(..) + let a = Matrix::::new(4, 4, 3); + let b = Matrix::::new(2, 2, 1); + let _ = arithm::add(&a, &b); + // [WARN] purecv::core - add: matrices must have the same dimensions (src1 4×4×3, src2 2×2×1) +} +``` + +You can also emit your own messages with the `cv_log_*!` macros and filter per +subsystem via the standard `RUST_LOG` syntax (e.g. `RUST_LOG=purecv::core=warn`): + +```rust +use purecv::core::logging::tags; +purecv::cv_log_info!(tags::IMGPROC, "gaussian blur, ksize = {}", 5); +``` + ### ndarray Interoperability With the `ndarray` feature enabled, you can convert between `Matrix` and `ndarray::Array3`: From 56c0c91a2b1faddbb5d8ea458429c99cee2b16e3 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Thu, 23 Jul 2026 18:28:26 +0200 Subject: [PATCH 11/11] refactor(core): gate the stdout logger behind the std feature (#80) The built-in SimpleLogger / init_basic_logger write to stdout via println!, which is the only genuinely std-only code in the logging module. Gate them (and the core re-export) behind #[cfg(feature = "std")] so the module is no_std-ready ahead of the no_std work in #86. Everything else here (LogLevel, set/get_log_level, the cv_log_*! / cv_bail! / cv_err! macros, the once-macros' core::sync::atomic state) is core/alloc-only. The remaining alloc-path reconciliation for logging.rs (format!/String under no_std) belongs to the no_std core conversion in #86, where the extern-crate-alloc scaffolding and target CI exist. Co-Authored-By: Claude Opus 4.8 --- src/core.rs | 5 ++++- src/core/logging.rs | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/core.rs b/src/core.rs index 5cd1abf..13438b8 100644 --- a/src/core.rs +++ b/src/core.rs @@ -98,7 +98,10 @@ 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::logging::{get_log_level, set_log_level, LogLevel}; +// `init_basic_logger` writes to stdout and is only available with `std`. +#[cfg(feature = "std")] +pub use self::logging::init_basic_logger; 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/logging.rs b/src/core/logging.rs index b19c0d4..f4f36b7 100644 --- a/src/core/logging.rs +++ b/src/core/logging.rs @@ -186,8 +186,15 @@ pub fn get_log_level() -> LogLevel { LogLevel::from(log::max_level()) } +// The built-in logger writes to stdout via `println!`, which requires `std`. +// Under `no_std` there is no stdout to write to, so consumers must install +// their own `log` backend (e.g. `defmt`/`semihosting`) instead. Everything +// else in this module (the macros, `LogLevel`, `set_log_level`) is +// `core`/`alloc`-only and works without `std`. +#[cfg(feature = "std")] struct SimpleLogger; +#[cfg(feature = "std")] impl log::Log for SimpleLogger { fn enabled(&self, metadata: &log::Metadata) -> bool { metadata.level() <= log::max_level() @@ -207,6 +214,7 @@ impl log::Log for SimpleLogger { fn flush(&self) {} } +#[cfg(feature = "std")] static LOGGER: SimpleLogger = SimpleLogger; /// Initializes a simple stdout logger for purecv log messages. @@ -215,6 +223,10 @@ static LOGGER: SimpleLogger = SimpleLogger; /// /// This is helpful for CLI tools or examples to quickly view logs without /// pulling in external dependencies like `env_logger`. +/// +/// Requires the `std` feature (it writes to stdout). Under `no_std`, install +/// your own [`log`] backend instead. +#[cfg(feature = "std")] 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))