From 954fa05be842f9494c1ef332defacf4e6eb2fd40 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 13 Jul 2026 16:39:16 +0200 Subject: [PATCH 1/5] chore(core): make std an explicit opt-in feature, set MSRV 1.88 num-traits switches to libm-backed float math so core compiles without the standard library; parallel/simd/fft/ndarray require std until their own no_std phases land (#82). Default features are unchanged. Refs #83 Co-Authored-By: Claude Fable 5 --- Cargo.toml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 621b5eb..2b968da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ name = "purecv" version = "0.6.1" authors = ["Walter Perdan "] edition = "2021" +rust-version = "1.88" description = "A pure Rust, high-performance computer vision library focused on safety and portability." license = "LGPL-2.1-or-later" repository = "https://github.com/webarkit/purecv" @@ -16,11 +17,11 @@ path = "src/lib.rs" [dependencies] rayon = { version = "1.10", optional = true } ndarray = { version = "0.17", optional = true } -num-traits = "0.2" +num-traits = { version = "0.2", default-features = false, features = ["libm"] } pulp = { version = "0.22", optional = true } rustfft = { version = "6", optional = true } num-complex = { version = "0.4", optional = true } -log = "0.4" +log = { version = "0.4", default-features = false } [dev-dependencies] image = "0.25" @@ -28,11 +29,12 @@ criterion = "0.8" [features] default = ["std", "parallel"] -std = [] -parallel = ["rayon"] -ndarray = ["dep:ndarray"] -simd = ["dep:pulp"] -fft = ["dep:rustfft", "dep:num-complex"] +std = ["num-traits/std"] +# The features below require `std` until their own no_std phases land (see issue #82). +parallel = ["dep:rayon", "std"] +ndarray = ["dep:ndarray", "std"] +simd = ["dep:pulp", "std"] +fft = ["dep:rustfft", "dep:num-complex", "std"] transforms = ["fft"] [[bench]] @@ -79,6 +81,9 @@ panic = "abort" [workspace] members = ["crates/wasm"] +# Built separately against bare-metal targets (see the no-std CI job); keeping it +# out of the workspace lets `cargo build --workspace` stay host-only. +exclude = ["crates/no-std-smoke"] [workspace.package] version = "0.6.1" From b264b56f8966aee0552de8a9b58194702e1cb017 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 13 Jul 2026 16:39:53 +0200 Subject: [PATCH 2/5] feat(core): compile the core module without the standard library - add #![cfg_attr(not(feature = "std"), no_std)] and extern crate alloc - std-gate the not-yet-converted modules (calib3d, features, features2d, imgproc, video) and their prelude re-exports - switch std:: paths to core::/alloc:: throughout src/core - PureCvError now implements core::error::Error (MSRV 1.88) - float methods resolve through num_traits::Float (libm) in no_std builds - std-gate get_tick_count/get_tick_frequency (no bare-metal clock) and the thread-local RNG API; replace a debug eprintln! with log::warn! Verified on thumbv7em-none-eabihf and ESP32-S3 (Xtensa). Refs #83 Co-Authored-By: Claude Fable 5 --- src/core.rs | 5 +++- src/core/arithm.rs | 53 ++++++++++++++++++++++++------------------ src/core/constants.rs | 36 ++++++++++++++-------------- src/core/dct.rs | 2 ++ src/core/dft.rs | 2 ++ src/core/dynamic.rs | 2 ++ src/core/error.rs | 8 ++++--- src/core/matrix.rs | 15 ++++++++---- src/core/metrics.rs | 7 ++++++ src/core/rng.rs | 24 +++++++++++++++++-- src/core/solvers.rs | 7 ++++++ src/core/structural.rs | 2 ++ src/core/types.rs | 22 ++++++++++-------- src/core/utils.rs | 18 ++++++++++---- src/lib.rs | 21 +++++++++++++++++ 15 files changed, 159 insertions(+), 65 deletions(-) diff --git a/src/core.rs b/src/core.rs index 30866a9..128766e 100644 --- a/src/core.rs +++ b/src/core.rs @@ -86,6 +86,7 @@ pub use self::matrix::{ CV_8SC1, CV_8SC2, CV_8SC3, CV_8SC4, CV_8U, CV_8UC1, CV_8UC2, CV_8UC3, CV_8UC4, }; pub use self::metrics::{mahalanobis, psnr}; +#[cfg(feature = "std")] pub use self::rng::{rand_shuffle, randn, randu, set_rng_seed}; pub use self::solvers::{solve_cubic, solve_quadratic}; pub use self::types::{ @@ -96,6 +97,8 @@ pub use self::types::{ KMEANS_RANDOM_CENTERS, KMEANS_USE_INITIAL_LABELS, SORT_ASCENDING, SORT_DESCENDING, SORT_EVERY_COLUMN, SORT_EVERY_ROW, }; +pub use self::utils::border_interpolate; #[cfg(not(feature = "parallel"))] pub use self::utils::ParIterFallback; -pub use self::utils::{border_interpolate, get_tick_count, get_tick_frequency}; +#[cfg(feature = "std")] +pub use self::utils::{get_tick_count, get_tick_frequency}; diff --git a/src/core/arithm.rs b/src/core/arithm.rs index 40e0086..6c836a8 100644 --- a/src/core/arithm.rs +++ b/src/core/arithm.rs @@ -34,12 +34,19 @@ * */ +// `num_traits::Float` provides `sqrt`, `sin`, ... on `f32`/`f64` via libm +// when `std` is disabled; with `std` the inherent methods win, so the +// import is only "used" in no_std builds. +use alloc::{format, string::ToString, vec, vec::Vec}; +#[allow(unused_imports)] +use num_traits::Float; + use crate::core::constants::CV_2PI; use crate::core::error::{PureCvError, Result}; use crate::core::types::{CmpTypes, NormTypes, ReduceTypes, Scalar}; use crate::core::{DataType, Matrix}; +use core::ops::{BitAnd, BitOr, BitXor, Not, Sub}; use num_traits::{Bounded, FromPrimitive, Num, SaturatingAdd, SaturatingSub, ToPrimitive}; -use std::ops::{BitAnd, BitOr, BitXor, Not, Sub}; #[cfg(feature = "parallel")] use rayon::prelude::*; @@ -64,7 +71,7 @@ macro_rules! binary_op { #[cfg(feature = "simd")] { // SIMD fast-path: only when dst type == src type and type has SIMD support - if std::any::TypeId::of::<$t_dst>() == std::any::TypeId::of::<$t_src>() + if core::any::TypeId::of::<$t_dst>() == core::any::TypeId::of::<$t_src>() && <$t_src as SimdElement>::has_simd() { // Try the SIMD kernel. If it returns false, fall back to scalar. @@ -73,7 +80,7 @@ macro_rules! binary_op { #[cfg(feature = "parallel")] { use rayon::prelude::*; - use std::sync::atomic::{AtomicBool, Ordering}; + use core::sync::atomic::{AtomicBool, Ordering}; let chunk_size = ($dst.data.len() / rayon::current_num_threads()).max(1024); let all_ok = AtomicBool::new(true); @@ -87,7 +94,7 @@ macro_rules! binary_op { // Transmute the dst chunk to $t_src for the SIMD call // This is safe because we verified $t_dst == $t_src above let dst_as_src: &mut [$t_src] = unsafe { - std::slice::from_raw_parts_mut( + core::slice::from_raw_parts_mut( dst_chunk.as_mut_ptr() as *mut $t_src, len, ) @@ -107,7 +114,7 @@ macro_rules! binary_op { #[cfg(not(feature = "parallel"))] { let dst_as_src: &mut [$t_src] = unsafe { - std::slice::from_raw_parts_mut( + core::slice::from_raw_parts_mut( $dst.data.as_mut_ptr() as *mut $t_src, $dst.data.len(), ) @@ -215,7 +222,7 @@ macro_rules! unary_op { ($dst:expr, $src:expr, $t_dst:ty, $t_src:ty, |$d:ident, $s:ident| $body:expr, simd: $simd_fn:ident) => { #[cfg(feature = "simd")] { - if std::any::TypeId::of::<$t_dst>() == std::any::TypeId::of::<$t_src>() + if core::any::TypeId::of::<$t_dst>() == core::any::TypeId::of::<$t_src>() && <$t_src as SimdElement>::has_simd() { // Try the SIMD kernel. If it returns false, fall back to scalar. @@ -224,7 +231,7 @@ macro_rules! unary_op { #[cfg(feature = "parallel")] { use rayon::prelude::*; - use std::sync::atomic::{AtomicBool, Ordering}; + use core::sync::atomic::{AtomicBool, Ordering}; let chunk_size = ($dst.data.len() / rayon::current_num_threads()).max(1024); let all_ok = AtomicBool::new(true); @@ -236,7 +243,7 @@ macro_rules! unary_op { let offset = idx * chunk_size; let len = dst_chunk.len(); let dst_as_src: &mut [$t_src] = unsafe { - std::slice::from_raw_parts_mut( + core::slice::from_raw_parts_mut( dst_chunk.as_mut_ptr() as *mut $t_src, len, ) @@ -255,7 +262,7 @@ macro_rules! unary_op { #[cfg(not(feature = "parallel"))] { let dst_as_src: &mut [$t_src] = unsafe { - std::slice::from_raw_parts_mut( + core::slice::from_raw_parts_mut( $dst.data.as_mut_ptr() as *mut $t_src, $dst.data.len(), ) @@ -1517,8 +1524,8 @@ where #[cfg(feature = "simd")] { // Only f32/f64 have simd_add_weighted; u8 and others use the scalar fallback. - if (std::any::TypeId::of::() == std::any::TypeId::of::() - || std::any::TypeId::of::() == std::any::TypeId::of::()) + if (core::any::TypeId::of::() == core::any::TypeId::of::() + || core::any::TypeId::of::() == core::any::TypeId::of::()) && ::has_simd() { ::simd_add_weighted(&mut dst.data, &src1.data, &src2.data, alpha, beta, gamma); @@ -1647,8 +1654,8 @@ where #[cfg(feature = "simd")] { // Only f32/f64 have simd_convert_scale_abs; others use the scalar fallback. - if (std::any::TypeId::of::() == std::any::TypeId::of::() - || std::any::TypeId::of::() == std::any::TypeId::of::()) + if (core::any::TypeId::of::() == core::any::TypeId::of::() + || core::any::TypeId::of::() == core::any::TypeId::of::()) && ::has_simd() { ::simd_convert_scale_abs(&mut dst.data, &src.data, alpha, beta); @@ -1817,7 +1824,7 @@ where T: Num + Copy + Send + Sync + Default + 'static, { mtx.data.fill(T::zero()); - let n = std::cmp::min(mtx.rows, mtx.cols); + let n = core::cmp::min(mtx.rows, mtx.cols); let channels = mtx.channels; let cols = mtx.cols; @@ -1891,8 +1898,8 @@ where #[cfg(feature = "simd")] { // Only f32/f64 have simd_dot; others use the scalar fallback. - if (std::any::TypeId::of::() == std::any::TypeId::of::() - || std::any::TypeId::of::() == std::any::TypeId::of::()) + if (core::any::TypeId::of::() == core::any::TypeId::of::() + || core::any::TypeId::of::() == core::any::TypeId::of::()) && ::has_simd() { if let Some(result) = ::simd_dot(&src1.data, &src2.data) { @@ -1996,7 +2003,7 @@ pub fn trace(src: &Matrix) -> Scalar where T: Num + Copy + Send + Sync + ToPrimitive + Default + 'static, { - let n = std::cmp::min(src.rows, src.cols); + let n = core::cmp::min(src.rows, src.cols); let channels = src.channels; let cols = src.cols; let mut sum = [0.0; 4]; @@ -2334,8 +2341,8 @@ where #[cfg(feature = "simd")] { // Only f32/f64 have simd_magnitude; others use the scalar fallback. - if (std::any::TypeId::of::() == std::any::TypeId::of::() - || std::any::TypeId::of::() == std::any::TypeId::of::()) + if (core::any::TypeId::of::() == core::any::TypeId::of::() + || core::any::TypeId::of::() == core::any::TypeId::of::()) && ::has_simd() { ::simd_magnitude(&mut dst.data, &x.data, &y.data); @@ -2977,7 +2984,7 @@ where let start = r * dst.cols; let end = start + dst.cols; let row = &mut dst.data[start..end]; - row.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + row.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)); if descending { row.reverse(); } @@ -2986,7 +2993,7 @@ where // Sort every column: extract column, sort, put back for c in 0..dst.cols { let mut col: Vec = (0..dst.rows).map(|r| dst.data[r * dst.cols + c]).collect(); - col.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + col.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)); if descending { col.reverse(); } @@ -3028,7 +3035,7 @@ where indices.sort_by(|&a, &b| { src.data[start + a] .partial_cmp(&src.data[start + b]) - .unwrap_or(std::cmp::Ordering::Equal) + .unwrap_or(core::cmp::Ordering::Equal) }); if descending { indices.reverse(); @@ -3043,7 +3050,7 @@ where indices.sort_by(|&a, &b| { src.data[a * src.cols + c] .partial_cmp(&src.data[b * src.cols + c]) - .unwrap_or(std::cmp::Ordering::Equal) + .unwrap_or(core::cmp::Ordering::Equal) }); if descending { indices.reverse(); diff --git a/src/core/constants.rs b/src/core/constants.rs index 2bc7df4..fe13fce 100644 --- a/src/core/constants.rs +++ b/src/core/constants.rs @@ -36,32 +36,32 @@ //! Mathematical constants mirroring OpenCV's C++ `CV_PI`, `CV_2PI`, etc. //! -//! All values are `pub const f64` backed by [`std::f64::consts`] for +//! All values are `pub const f64` backed by [`core::f64::consts`] for //! maximum precision and cross-platform reproducibility. -/// Pi (same as `std::f64::consts::PI`). -pub const CV_PI: f64 = std::f64::consts::PI; +/// Pi (same as `core::f64::consts::PI`). +pub const CV_PI: f64 = core::f64::consts::PI; -/// Pi divided by 2 (same as `std::f64::consts::FRAC_PI_2`). -pub const CV_PI_2: f64 = std::f64::consts::FRAC_PI_2; +/// Pi divided by 2 (same as `core::f64::consts::FRAC_PI_2`). +pub const CV_PI_2: f64 = core::f64::consts::FRAC_PI_2; /// 2 * Pi — full circle in radians. -pub const CV_2PI: f64 = 2.0 * std::f64::consts::PI; +pub const CV_2PI: f64 = 2.0 * core::f64::consts::PI; -/// Pi divided by 4 (same as `std::f64::consts::FRAC_PI_4`). -pub const CV_PI_4: f64 = std::f64::consts::FRAC_PI_4; +/// Pi divided by 4 (same as `core::f64::consts::FRAC_PI_4`). +pub const CV_PI_4: f64 = core::f64::consts::FRAC_PI_4; -/// Log base 2 of e (same as `std::f64::consts::LOG2_E`). -pub const CV_LOG2: f64 = std::f64::consts::LOG2_E; +/// Log base 2 of e (same as `core::f64::consts::LOG2_E`). +pub const CV_LOG2: f64 = core::f64::consts::LOG2_E; -/// Natural logarithm of 2 (same as `std::f64::consts::LN_2`). -pub const CV_LN2: f64 = std::f64::consts::LN_2; +/// Natural logarithm of 2 (same as `core::f64::consts::LN_2`). +pub const CV_LN2: f64 = core::f64::consts::LN_2; -/// Square root of 2 (same as `std::f64::consts::SQRT_2`). -pub const CV_SQRT2: f64 = std::f64::consts::SQRT_2; +/// Square root of 2 (same as `core::f64::consts::SQRT_2`). +pub const CV_SQRT2: f64 = core::f64::consts::SQRT_2; -/// Euler's number (same as `std::f64::consts::E`). -pub const CV_E: f64 = std::f64::consts::E; +/// Euler's number (same as `core::f64::consts::E`). +pub const CV_E: f64 = core::f64::consts::E; -/// Natural logarithm of 10 (same as `std::f64::consts::LN_10`). -pub const CV_LN10: f64 = std::f64::consts::LN_10; +/// Natural logarithm of 10 (same as `core::f64::consts::LN_10`). +pub const CV_LN10: f64 = core::f64::consts::LN_10; diff --git a/src/core/dct.rs b/src/core/dct.rs index 10b0849..512497c 100644 --- a/src/core/dct.rs +++ b/src/core/dct.rs @@ -34,6 +34,8 @@ * */ +use alloc::vec; + use crate::core::constants::CV_PI; use crate::core::error::{PureCvError, Result}; use crate::core::matrix::Matrix; diff --git a/src/core/dft.rs b/src/core/dft.rs index 678e573..85c9395 100644 --- a/src/core/dft.rs +++ b/src/core/dft.rs @@ -34,6 +34,8 @@ * */ +use alloc::vec; + use rustfft::num_complex::Complex; use rustfft::FftPlanner; diff --git a/src/core/dynamic.rs b/src/core/dynamic.rs index a3824e1..e67ea98 100644 --- a/src/core/dynamic.rs +++ b/src/core/dynamic.rs @@ -34,6 +34,8 @@ * */ +use alloc::{format, vec, vec::Vec}; + use crate::core::error::{PureCvError, Result}; use crate::core::matrix::{Depth, MatType, Matrix}; diff --git a/src/core/error.rs b/src/core/error.rs index b687d38..2f8a2c9 100644 --- a/src/core/error.rs +++ b/src/core/error.rs @@ -34,7 +34,9 @@ * */ -use std::fmt; +use alloc::string::String; + +use core::fmt; /// Custom error type for the purecv library. #[derive(Debug, Clone, PartialEq)] @@ -60,7 +62,7 @@ impl fmt::Display for PureCvError { } } -impl std::error::Error for PureCvError {} +impl core::error::Error for PureCvError {} /// Standard result type for purecv. -pub type Result = std::result::Result; +pub type Result = core::result::Result; diff --git a/src/core/matrix.rs b/src/core/matrix.rs index 8898220..6b837f0 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::types::Scalar; +use alloc::{format, vec, vec::Vec}; /// Matrix depth: number of bits per element and its signedness/type. /// Follows OpenCV's depth conventions (CV_8U, CV_32F, etc.). @@ -545,7 +547,7 @@ impl Matrix { /// /// * `other` - The matrix with which to swap references logic correctly. pub fn swap(&mut self, other: &mut Self) { - std::mem::swap(self, other); + core::mem::swap(self, other); } /// Returns a raw immutable pointer to the start of the underlying data @@ -592,9 +594,14 @@ impl Matrix { pub fn copy_to(&self, dst: &mut Matrix) -> Result<()> { if self.rows != dst.rows || self.cols != dst.cols || self.channels != dst.channels { #[cfg(debug_assertions)] - eprintln!( + log::warn!( "[purecv::copy_to] dst resized from {}x{}x{} to {}x{}x{}", - dst.rows, dst.cols, dst.channels, self.rows, self.cols, self.channels + dst.rows, + dst.cols, + dst.channels, + self.rows, + self.cols, + self.channels ); dst.rows = self.rows; dst.cols = self.cols; @@ -672,7 +679,7 @@ impl Matrix { /// Following OpenCV, the diagonal has value 1 and others are 0. pub fn eye(rows: usize, cols: usize, channels: usize) -> Self { let mut mat = Self::zeros(rows, cols, channels); - let min_dim = std::cmp::min(rows, cols); + let min_dim = core::cmp::min(rows, cols); for i in 0..min_dim { for c in 0..channels { mat.set(i, i, c, T::one()); diff --git a/src/core/metrics.rs b/src/core/metrics.rs index 6a2b007..cf34f5b 100644 --- a/src/core/metrics.rs +++ b/src/core/metrics.rs @@ -34,6 +34,13 @@ * */ +// `num_traits::Float` provides `sqrt`, `sin`, ... on `f32`/`f64` via libm +// when `std` is disabled; with `std` the inherent methods win, so the +// import is only "used" in no_std builds. +use alloc::vec; +#[allow(unused_imports)] +use num_traits::Float; + use crate::core::error::{PureCvError, Result}; use crate::core::matrix::Matrix; diff --git a/src/core/rng.rs b/src/core/rng.rs index 9a5d40c..5cd439a 100644 --- a/src/core/rng.rs +++ b/src/core/rng.rs @@ -34,12 +34,22 @@ * */ +// `num_traits::Float` provides `sqrt`, `sin`, ... on `f32`/`f64` via libm +// when `std` is disabled; with `std` the inherent methods win, so the +// import is only "used" in no_std builds. use crate::core::constants::CV_2PI; +#[cfg(feature = "std")] use crate::core::error::{PureCvError, Result}; +#[cfg(feature = "std")] use crate::core::types::Scalar; +#[cfg(feature = "std")] use crate::core::Matrix; +#[cfg(feature = "std")] +use core::cell::RefCell; +#[allow(unused_imports)] +use num_traits::Float; +#[cfg(feature = "std")] use num_traits::{FromPrimitive, ToPrimitive}; -use std::cell::RefCell; // --------------------------------------------------------------------------- // Xoshiro256** — a fast, high-quality pure-Rust PRNG (public domain algorithm @@ -47,11 +57,16 @@ use std::cell::RefCell; // --------------------------------------------------------------------------- /// Internal state for the xoshiro256** generator. +// Without `std` the thread-local RNG below is compiled out, leaving this +// generator temporarily unused; a seedable no_std API is planned in #82. +#[cfg_attr(not(feature = "std"), allow(dead_code))] #[derive(Clone)] struct Xoshiro256 { s: [u64; 4], } +// See the note on `Xoshiro256`: without `std` these methods have no caller yet. +#[cfg_attr(not(feature = "std"), allow(dead_code))] impl Xoshiro256 { /// Creates a new generator seeded by expanding `seed` through SplitMix64. fn from_seed(seed: u64) -> Self { @@ -122,7 +137,8 @@ impl Xoshiro256 { // Thread-local RNG state // --------------------------------------------------------------------------- -thread_local! { +#[cfg(feature = "std")] +std::thread_local! { static THREAD_RNG: RefCell = RefCell::new(Xoshiro256::from_seed(0)); } @@ -140,6 +156,7 @@ thread_local! { /// use purecv::core::set_rng_seed; /// set_rng_seed(42); /// ``` +#[cfg(feature = "std")] pub fn set_rng_seed(seed: u64) { THREAD_RNG.with(|rng| { *rng.borrow_mut() = Xoshiro256::from_seed(seed); @@ -172,6 +189,7 @@ pub fn set_rng_seed(seed: u64) { /// let mut mat = Matrix::::new(100, 100, 1); /// randu(&mut mat, Scalar::all(0.0), Scalar::all(1.0)).unwrap(); /// ``` +#[cfg(feature = "std")] pub fn randu(dst: &mut Matrix, low: Scalar, high: Scalar) -> Result<()> where T: Default + Clone + FromPrimitive + ToPrimitive + Send + Sync, @@ -224,6 +242,7 @@ where /// let mut mat = Matrix::::new(100, 100, 1); /// randn(&mut mat, Scalar::all(0.0), Scalar::all(1.0)).unwrap(); /// ``` +#[cfg(feature = "std")] pub fn randn(dst: &mut Matrix, mean: Scalar, std_dev: Scalar) -> Result<()> where T: Default + Clone + FromPrimitive + ToPrimitive + Send + Sync, @@ -267,6 +286,7 @@ where /// /// # Arguments /// * `slice` - The slice to shuffle. +#[cfg(feature = "std")] pub fn rand_shuffle(slice: &mut [T]) { if slice.is_empty() { return; diff --git a/src/core/solvers.rs b/src/core/solvers.rs index 6b6a636..60dbca6 100644 --- a/src/core/solvers.rs +++ b/src/core/solvers.rs @@ -34,6 +34,13 @@ * */ +// `num_traits::Float` provides `sqrt`, `sin`, ... on `f32`/`f64` via libm +// when `std` is disabled; with `std` the inherent methods win, so the +// import is only "used" in no_std builds. +use alloc::{vec, vec::Vec}; +#[allow(unused_imports)] +use num_traits::Float; + use crate::core::constants::CV_PI; use crate::core::error::Result; diff --git a/src/core/structural.rs b/src/core/structural.rs index aa3ab82..0726336 100644 --- a/src/core/structural.rs +++ b/src/core/structural.rs @@ -34,6 +34,8 @@ * */ +use alloc::{format, string::ToString, vec::Vec}; + use crate::core::error::{PureCvError, Result}; use crate::core::types::Scalar; use crate::core::Matrix; diff --git a/src/core/types.rs b/src/core/types.rs index 50a5844..454a7bb 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -34,7 +34,9 @@ * */ -use std::ops::{Add, Div, Index, IndexMut, Mul, Sub}; +use alloc::format; + +use core::ops::{Add, Div, Index, IndexMut, Mul, Sub}; use num_traits::{CheckedDiv, Zero}; @@ -636,7 +638,7 @@ impl VecN { /// numeric types and guarantees that each element is the additive identity. pub fn zeros() -> Self { Self { - val: std::array::from_fn(|_| T::zero()), + val: core::array::from_fn(|_| T::zero()), } } } @@ -645,7 +647,7 @@ impl VecN { /// Returns a vector with every element set to `v`. pub fn all(v: T) -> Self { Self { - val: std::array::from_fn(|_| v), + val: core::array::from_fn(|_| v), } } } @@ -683,7 +685,7 @@ impl, const N: usize> Add for VecN { type Output = Self; fn add(self, rhs: Self) -> Self { Self { - val: std::array::from_fn(|i| self.val[i] + rhs.val[i]), + val: core::array::from_fn(|i| self.val[i] + rhs.val[i]), } } } @@ -692,7 +694,7 @@ impl, const N: usize> Sub for VecN { type Output = Self; fn sub(self, rhs: Self) -> Self { Self { - val: std::array::from_fn(|i| self.val[i] - rhs.val[i]), + val: core::array::from_fn(|i| self.val[i] - rhs.val[i]), } } } @@ -705,7 +707,7 @@ impl, const N: usize> Add> for Vec type Output = Self; fn add(self, rhs: Scalar) -> Self { Self { - val: std::array::from_fn(|i| self.val[i] + rhs.channel_or_default(i)), + val: core::array::from_fn(|i| self.val[i] + rhs.channel_or_default(i)), } } } @@ -716,7 +718,7 @@ impl, const N: usize> Sub> for Vec type Output = Self; fn sub(self, rhs: Scalar) -> Self { Self { - val: std::array::from_fn(|i| self.val[i] - rhs.channel_or_default(i)), + val: core::array::from_fn(|i| self.val[i] - rhs.channel_or_default(i)), } } } @@ -726,7 +728,7 @@ impl, const N: usize> Mul for VecN { type Output = Self; fn mul(self, rhs: Self) -> Self { Self { - val: std::array::from_fn(|i| self.val[i] * rhs.val[i]), + val: core::array::from_fn(|i| self.val[i] * rhs.val[i]), } } } @@ -736,7 +738,7 @@ impl, const N: usize> Mul for VecN { type Output = Self; fn mul(self, rhs: T) -> Self { Self { - val: std::array::from_fn(|i| self.val[i] * rhs), + val: core::array::from_fn(|i| self.val[i] * rhs), } } } @@ -746,7 +748,7 @@ impl, const N: usize> Div for VecN { type Output = Self; fn div(self, rhs: T) -> Self { Self { - val: std::array::from_fn(|i| self.val[i] / rhs), + val: core::array::from_fn(|i| self.val[i] / rhs), } } } diff --git a/src/core/utils.rs b/src/core/utils.rs index a5c0600..556ba77 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -34,19 +34,29 @@ * */ +#[cfg(feature = "std")] use std::sync::OnceLock; +#[cfg(feature = "std")] use std::time::Instant; +#[cfg(feature = "std")] static START_TIME: OnceLock = OnceLock::new(); /// Returns the number of ticks. /// In this implementation, it returns the number of nanoseconds since the first call. +/// +/// Requires the `std` feature: bare-metal targets have no portable clock, +/// so embedded users should rely on their platform timer instead. +#[cfg(feature = "std")] pub fn get_tick_count() -> i64 { let start = *START_TIME.get_or_init(Instant::now); start.elapsed().as_nanos() as i64 } /// Returns the number of ticks per second. +/// +/// Requires the `std` feature (see [`get_tick_count`]). +#[cfg(feature = "std")] pub fn get_tick_frequency() -> f64 { 1_000_000_000.0 } @@ -158,13 +168,13 @@ pub trait ParIterFallback<'a, T: 'a> { fn par_iter(&'a self) -> Self::Iter; fn par_iter_mut(&'a mut self) -> Self::IterMut; - fn par_chunks_mut(&'a mut self, size: usize) -> std::slice::ChunksMut<'a, T>; + fn par_chunks_mut(&'a mut self, size: usize) -> core::slice::ChunksMut<'a, T>; } #[cfg(not(feature = "parallel"))] impl<'a, T: 'a> ParIterFallback<'a, T> for [T] { - type Iter = std::slice::Iter<'a, T>; - type IterMut = std::slice::IterMut<'a, T>; + type Iter = core::slice::Iter<'a, T>; + type IterMut = core::slice::IterMut<'a, T>; fn par_iter(&'a self) -> Self::Iter { self.iter() @@ -172,7 +182,7 @@ impl<'a, T: 'a> ParIterFallback<'a, T> for [T] { fn par_iter_mut(&'a mut self) -> Self::IterMut { self.iter_mut() } - fn par_chunks_mut(&'a mut self, size: usize) -> std::slice::ChunksMut<'a, T> { + fn par_chunks_mut(&'a mut self, size: usize) -> core::slice::ChunksMut<'a, T> { self.chunks_mut(size) } } diff --git a/src/lib.rs b/src/lib.rs index 065302c..3bc324f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,17 +34,30 @@ * */ +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + // Global modules +// +// Only `core` and `version` build without `std` for now; the remaining modules +// are gated until their own no_std phases land (see issue #82). +#[cfg(feature = "std")] pub mod calib3d; pub mod core; +#[cfg(feature = "std")] pub mod features; +#[cfg(feature = "std")] pub mod features2d; +#[cfg(feature = "std")] pub mod imgproc; pub mod version; +#[cfg(feature = "std")] pub mod video; /// Prelude to easily import common structures pub mod prelude { + #[cfg(feature = "std")] pub use crate::calib3d::{ find_fundamental_mat, find_homography, init_undistort_rectify_map, rodrigues, solve_pnp, solve_pnp_ransac, FundamentalMatMethod, HomographyMethod, SolvePnPMethod, @@ -55,21 +68,29 @@ pub mod prelude { Vec3s, Vec4b, Vec4d, Vec4f, Vec4i, Vec4s, Vec6d, Vec6f, VecN, }; pub use crate::core::Matrix; + #[cfg(feature = "std")] pub use crate::features2d::{ draw_keypoints, draw_matches, filter_matches, BFMatcher, DMatch, DescriptorMatcher, FastFeatureDetector, FastType, KeyPoint, NormType, Orb, }; + #[cfg(feature = "std")] pub use crate::imgproc::derivatives::{laplacian, scharr, sobel}; + #[cfg(feature = "std")] pub use crate::imgproc::edge::canny; + #[cfg(feature = "std")] pub use crate::imgproc::feature::{ corner_eigen_vals_and_vecs, corner_harris, corner_min_eigen_val, corner_sub_pix, good_features_to_track, pre_corner_detect, }; + #[cfg(feature = "std")] pub use crate::imgproc::filter::{bilateral_filter, box_filter, gaussian_blur}; + #[cfg(feature = "std")] pub use crate::imgproc::threshold::{threshold, ThresholdTypes}; + #[cfg(feature = "std")] pub use crate::imgproc::{ cvt_color, remap, warp_perspective, ColorConversionCode, InterpolationFlags, }; + #[cfg(feature = "std")] pub use crate::video::optical_flow::{ build_optical_flow_pyramid, calc_optical_flow_pyramid_lk, OpticalFlowPyramid, OPTFLOW_LK_GET_MIN_EIGENVALS, OPTFLOW_USE_INITIAL_FLOW, From 08ad7bc1a3671080b4d519993821e7a580d1775a Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 13 Jul 2026 16:39:53 +0200 Subject: [PATCH 3/5] test(core): add build-only no_std smoke-test crate crates/no-std-smoke consumes the purecv public API from a #![no_std] crate; building it for a bare-metal target proves downstream no_std usability. Excluded from the workspace as its own root. Refs #83 Co-Authored-By: Claude Fable 5 --- crates/no-std-smoke/Cargo.toml | 13 +++++++ crates/no-std-smoke/src/lib.rs | 65 ++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 crates/no-std-smoke/Cargo.toml create mode 100644 crates/no-std-smoke/src/lib.rs diff --git a/crates/no-std-smoke/Cargo.toml b/crates/no-std-smoke/Cargo.toml new file mode 100644 index 0000000..208f97b --- /dev/null +++ b/crates/no-std-smoke/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "purecv-no-std-smoke" +version = "0.0.0" +edition = "2021" +publish = false +description = "Build-only smoke test: consumes the purecv public API from a no_std crate (see issue #83)." + +[dependencies] +purecv = { path = "../..", default-features = false } + +# Standalone workspace root: keeps this build-only crate out of both the +# purecv workspace and any enclosing one. +[workspace] diff --git a/crates/no-std-smoke/src/lib.rs b/crates/no-std-smoke/src/lib.rs new file mode 100644 index 0000000..c0c2e1c --- /dev/null +++ b/crates/no-std-smoke/src/lib.rs @@ -0,0 +1,65 @@ +/* + * lib.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 + * + */ + +//! Build-only `no_std` smoke test for `purecv` (issue #83). +//! +//! This crate never runs; compiling it for a bare-metal target such as +//! `thumbv7em-none-eabihf` proves that the `purecv` core API is usable +//! from a `no_std` + `alloc` consumer: +//! +//! ```sh +//! cargo build --target thumbv7em-none-eabihf +//! ``` + +#![no_std] + +extern crate alloc; + +use alloc::vec; +use purecv::core::error::Result; +use purecv::core::{add, determinant, mean, Matrix}; + +/// Exercises matrix construction, arithmetic, statistics, and linear algebra. +pub fn smoke() -> Result { + let a = Matrix::::from_vec(2, 2, 1, vec![1.0, 2.0, 3.0, 4.0]); + let b = Matrix::::from_vec(2, 2, 1, vec![5.0, 6.0, 7.0, 8.0]); + + let sum = add(&a, &b)?; + let avg = mean(&sum); + let det = determinant(&sum); + + Ok(avg.v[0] + det) +} From db2f7ccfaf1ef05b9fbf57e1b3efd5a4b1c95323 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 13 Jul 2026 16:40:13 +0200 Subject: [PATCH 4/5] chore(ci): add no_std build job Builds and lints with --no-default-features on the host, then builds purecv and the smoke-test consumer for thumbv7em-none-eabihf. Refs #83 Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 689c1a6..87d34f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,34 @@ jobs: - name: Run tests (with ndarray feature) run: cargo test --workspace --features ndarray + no-std-build: + name: no_std Build (core only, issue #83) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + targets: thumbv7em-none-eabihf + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + + - name: Build without default features (host) + run: cargo build --no-default-features + + - name: Lint without default features + run: cargo clippy --no-default-features -- -D warnings + + - name: Build for bare-metal target + run: cargo build --no-default-features --target thumbv7em-none-eabihf + + - name: Build no_std smoke-test consumer + run: cargo build --manifest-path crates/no-std-smoke/Cargo.toml --target thumbv7em-none-eabihf + wasm-build: name: WASM Dual Build runs-on: ubuntu-latest From 9399197686f5160244629268cf5be593b3e082af Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 13 Jul 2026 21:36:50 +0200 Subject: [PATCH 5/5] fix(wasm): re-enable purecv std feature for the wasm crate default-features = false previously only dropped rayon; now that std gates the non-core modules, the wasm wrapper must opt back into it. Refs #83 Co-Authored-By: Claude Fable 5 --- crates/wasm/Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/wasm/Cargo.toml b/crates/wasm/Cargo.toml index 959ca8b..7c3dc05 100644 --- a/crates/wasm/Cargo.toml +++ b/crates/wasm/Cargo.toml @@ -22,7 +22,9 @@ web-sys = { version = "0.3.69", features = ["console"] } serde = { version = "1.0", features = ["derive"] } serde-wasm-bindgen = "0.6" num-traits = "0.2" -purecv = { path = "../../", default-features = false } +# default-features = false keeps rayon (parallel) out of the wasm build; +# `std` must be re-enabled explicitly now that it gates the non-core modules. +purecv = { path = "../../", default-features = false, features = ["std"] } [features] default = []