Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 12 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name = "purecv"
version = "0.6.1"
authors = ["Walter Perdan <https://github.com/kalwalt>"]
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"
Expand All @@ -16,23 +17,24 @@ 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"
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]]
Expand Down Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions crates/no-std-smoke/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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]
65 changes: 65 additions & 0 deletions crates/no-std-smoke/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
* 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<f64> {
let a = Matrix::<f32>::from_vec(2, 2, 1, vec![1.0, 2.0, 3.0, 4.0]);
let b = Matrix::<f32>::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)
}
4 changes: 3 additions & 1 deletion crates/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
5 changes: 4 additions & 1 deletion src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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};
53 changes: 30 additions & 23 deletions src/core/arithm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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.
Expand All @@ -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);
Expand All @@ -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,
)
Expand All @@ -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(),
)
Expand Down Expand Up @@ -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.
Expand All @@ -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);
Expand All @@ -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,
)
Expand All @@ -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(),
)
Expand Down Expand Up @@ -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::<T>() == std::any::TypeId::of::<f32>()
|| std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>())
if (core::any::TypeId::of::<T>() == core::any::TypeId::of::<f32>()
|| core::any::TypeId::of::<T>() == core::any::TypeId::of::<f64>())
&& <T as SimdElement>::has_simd()
{
<T>::simd_add_weighted(&mut dst.data, &src1.data, &src2.data, alpha, beta, gamma);
Expand Down Expand Up @@ -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::<T>() == std::any::TypeId::of::<f32>()
|| std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>())
if (core::any::TypeId::of::<T>() == core::any::TypeId::of::<f32>()
|| core::any::TypeId::of::<T>() == core::any::TypeId::of::<f64>())
&& <T as SimdElement>::has_simd()
{
<T>::simd_convert_scale_abs(&mut dst.data, &src.data, alpha, beta);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -1891,8 +1898,8 @@ where
#[cfg(feature = "simd")]
{
// Only f32/f64 have simd_dot; others use the scalar fallback.
if (std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>()
|| std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>())
if (core::any::TypeId::of::<T>() == core::any::TypeId::of::<f32>()
|| core::any::TypeId::of::<T>() == core::any::TypeId::of::<f64>())
&& <T as SimdElement>::has_simd()
{
if let Some(result) = <T>::simd_dot(&src1.data, &src2.data) {
Expand Down Expand Up @@ -1996,7 +2003,7 @@ pub fn trace<T>(src: &Matrix<T>) -> Scalar<f64>
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];
Expand Down Expand Up @@ -2334,8 +2341,8 @@ where
#[cfg(feature = "simd")]
{
// Only f32/f64 have simd_magnitude; others use the scalar fallback.
if (std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>()
|| std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>())
if (core::any::TypeId::of::<T>() == core::any::TypeId::of::<f32>()
|| core::any::TypeId::of::<T>() == core::any::TypeId::of::<f64>())
&& <T as SimdElement>::has_simd()
{
<T>::simd_magnitude(&mut dst.data, &x.data, &y.data);
Expand Down Expand Up @@ -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();
}
Expand All @@ -2986,7 +2993,7 @@ where
// Sort every column: extract column, sort, put back
for c in 0..dst.cols {
let mut col: Vec<T> = (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();
}
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down
Loading