Skip to content
Merged
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Unlike existing wrappers, **PureCV** is a native rewrite. It aims to provide:
- **Utilities:** `add_weighted`, `check_range`, `absdiff`, `get_tick_count`, `get_tick_frequency`.
- **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`, and `simd_gaussian_5tap_h/v`. Falls back to scalar loops at zero cost when disabled.
- **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.

### `purecv-imgproc`
- **Color Conversions:** High-performance `cvt_color` supporting RGB, BGR, Gray, RGBA, BGRA and more. Up to **6.6× speedup** with Parallel + SIMD. SIMD-accelerated paths (`simd` feature) use fixed-point integer arithmetic (coefficients 77/150/29 ≈ 0.299/0.587/0.114 × 256) for all `*_to_gray` conversions — portable to x86 SSE/AVX, ARM NEON, and WASM `simd128` via `pulp`.
Expand All @@ -56,6 +56,7 @@ Unlike existing wrappers, **PureCV** is a native rewrite. It aims to provide:
- **Feature Detection:** `corner_harris`, `corner_min_eigen_val` (Shi-Tomasi), `good_features_to_track`, `corner_sub_pix` refinement, and structure tensor computation via `corner_eigen_vals_and_vecs`. Supports both Harris and Shi-Tomasi responses with non-maximum suppression.
- **Hough Transform:** Standard (`hough_lines`) and Probabilistic (`hough_lines_p`) line detection, plus Hough Circle Transform (`hough_circles`) using internally computed Sobel gradients. Fully parallelized via the `parallel` feature.
- **Resizing:** `resize` function utilizing high-performance bilinear interpolation, fully compatible with `parallel` Rayon multi-threading.
- **Geometric Transformations:** `remap` (with bilinear and nearest-neighbor interpolation) and `warp_perspective` (perspective transformations) fully parallelized and SIMD-accelerated.

### `purecv-features2d`
- **FAST Feature Detector:** Real-time corner detector (`FastFeatureDetector`) supporting Type 5_8, 7_12, and 9_16 neighborhood configurations, plus optional non-maximum suppression.
Expand All @@ -67,6 +68,8 @@ Unlike existing wrappers, **PureCV** is a native rewrite. It aims to provide:
- **Optical Flow:** Pyramidal Lucas-Kanade optical flow implementation with `calc_optical_flow_pyr_lk` and `build_optical_flow_pyramid`. Includes robust window-based tracking, sub-pixel accuracy, spatial gradient optimization, and iterative refinement.

### `purecv-calib3d`
- **Camera Undistortion:** `init_undistort_rectify_map` to compute lens undistortion and rectification maps.
- **Epipolar Geometry:** `find_fundamental_mat` supporting the normalized 8-Point algorithm and robust RANSAC estimation with Sampson distance.
- **Pose Estimation:** Camera pose estimation using `solve_pnp` (Iterative) and `solve_pnp_ransac`.
- **Homography:** Direct Linear Transformation (DLT) and RANSAC-based `find_homography` for robust planar perspective mapping.
- **Geometry:** `rodrigues` for converting between rotation vectors and 3x3 rotation matrices.
Expand Down Expand Up @@ -250,6 +253,9 @@ cargo run --example optical_flow_video

# Pose Estimation (solve_pnp and rodrigues)
cargo run --example pose_estimation

# Camera Undistortion and Perspective Warping
cargo run --example rectification
```

## 🧪 Testing & Benchmarking
Expand Down
1 change: 1 addition & 0 deletions crates/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ js-sys = "0.3.69"
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 }

[features]
Expand Down
1 change: 1 addition & 0 deletions crates/wasm/scripts/build-dual.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

$ErrorActionPreference = "Stop"


# Get the directory where the script is located
$scriptDir = $PSScriptRoot
$wasmDir = Join-Path $scriptDir ".." | Resolve-Path
Expand Down
237 changes: 237 additions & 0 deletions crates/wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,136 @@ pub fn laplacian(
})
}

/// Helper to convert a JS integer into an `InterpolationFlags`.
fn interpolation_flags_from_i32(val: i32) -> Result<purecv::imgproc::InterpolationFlags, JsError> {
match val {
0 => Ok(purecv::imgproc::InterpolationFlags::Nearest),
1 => Ok(purecv::imgproc::InterpolationFlags::Linear),
_ => Err(JsError::new(&format!("Unknown interpolation type: {val}"))),
}
}

/// Applies a generic geometrical transformation to an image.
#[wasm_bindgen(js_name = "remap")]
pub fn remap_wasm(
src: &Mat,
map1: &Mat,
map2: &Mat,
interpolation: i32,
border_mode: i32,
border_value: &Scalar,
) -> Result<Mat, JsError> {
use purecv::core::types::Scalar as CoreScalar;
use purecv::imgproc::remap;

let m1 = require_f32(map1, "remap (map1)")?;
let m2 = require_f32(map2, "remap (map2)")?;
let interp = interpolation_flags_from_i32(interpolation)?;
let border = border_type_from_i32(border_mode)?;

let data = match &src.inner.data {
DynamicData::U8(m) => {
let bv = CoreScalar::new(
num_traits::FromPrimitive::from_f64(border_value.v0).unwrap_or(0),
num_traits::FromPrimitive::from_f64(border_value.v1).unwrap_or(0),
num_traits::FromPrimitive::from_f64(border_value.v2).unwrap_or(0),
num_traits::FromPrimitive::from_f64(border_value.v3).unwrap_or(0),
);
let result =
remap(m, m1, m2, interp, border, bv).map_err(|e| JsError::new(&format!("{e}")))?;
DynamicData::U8(result)
}
DynamicData::F32(m) => {
let bv = CoreScalar::new(
num_traits::FromPrimitive::from_f64(border_value.v0).unwrap_or(0.0),
num_traits::FromPrimitive::from_f64(border_value.v1).unwrap_or(0.0),
num_traits::FromPrimitive::from_f64(border_value.v2).unwrap_or(0.0),
num_traits::FromPrimitive::from_f64(border_value.v3).unwrap_or(0.0),
);
let result =
remap(m, m1, m2, interp, border, bv).map_err(|e| JsError::new(&format!("{e}")))?;
DynamicData::F32(result)
}
DynamicData::F64(m) => {
let bv = CoreScalar::new(
border_value.v0,
border_value.v1,
border_value.v2,
border_value.v3,
);
let result =
remap(m, m1, m2, interp, border, bv).map_err(|e| JsError::new(&format!("{e}")))?;
DynamicData::F64(result)
}
_ => return Err(JsError::new("remap: unsupported depth type")),
};

Ok(Mat {
inner: DynamicMatrix { data },
})
}

/// Applies a perspective transformation to an image.
#[wasm_bindgen(js_name = "warpPerspective")]
pub fn warp_perspective_wasm(
src: &Mat,
m: &Mat,
dsize_width: i32,
dsize_height: i32,
flags: i32,
border_mode: i32,
border_value: &Scalar,
) -> Result<Mat, JsError> {
use purecv::core::types::{Scalar as CoreScalar, Size2i};
use purecv::imgproc::warp_perspective;

let m_mat = require_f64(m, "warpPerspective (M)")?;
let interp = interpolation_flags_from_i32(flags)?;
let border = border_type_from_i32(border_mode)?;
let dsize = Size2i::new(dsize_width, dsize_height);

let data = match &src.inner.data {
DynamicData::U8(m) => {
let bv = CoreScalar::new(
num_traits::FromPrimitive::from_f64(border_value.v0).unwrap_or(0),
num_traits::FromPrimitive::from_f64(border_value.v1).unwrap_or(0),
num_traits::FromPrimitive::from_f64(border_value.v2).unwrap_or(0),
num_traits::FromPrimitive::from_f64(border_value.v3).unwrap_or(0),
);
let result = warp_perspective(m, m_mat, dsize, interp, border, bv)
.map_err(|e| JsError::new(&format!("{e}")))?;
DynamicData::U8(result)
}
DynamicData::F32(m) => {
let bv = CoreScalar::new(
num_traits::FromPrimitive::from_f64(border_value.v0).unwrap_or(0.0),
num_traits::FromPrimitive::from_f64(border_value.v1).unwrap_or(0.0),
num_traits::FromPrimitive::from_f64(border_value.v2).unwrap_or(0.0),
num_traits::FromPrimitive::from_f64(border_value.v3).unwrap_or(0.0),
);
let result = warp_perspective(m, m_mat, dsize, interp, border, bv)
.map_err(|e| JsError::new(&format!("{e}")))?;
DynamicData::F32(result)
}
DynamicData::F64(m) => {
let bv = CoreScalar::new(
border_value.v0,
border_value.v1,
border_value.v2,
border_value.v3,
);
let result = warp_perspective(m, m_mat, dsize, interp, border, bv)
.map_err(|e| JsError::new(&format!("{e}")))?;
DynamicData::F64(result)
}
_ => return Err(JsError::new("warpPerspective: unsupported depth type")),
};

Ok(Mat {
inner: DynamicMatrix { data },
})
}

// ---------------------------------------------------------------------------
// Blur / filter operations (require f32 depth)
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -2220,6 +2350,113 @@ pub fn rodrigues_wasm(src: &Mat, dst: &mut Mat) -> Result<(), JsError> {
rodrigues(src_mat, dst_mat).map_err(|e| JsError::new(&format!("{e}")))
}

#[wasm_bindgen(js_name = "initUndistortRectifyMap")]
pub fn init_undistort_rectify_map_wasm(
camera_matrix: &Mat,
dist_coeffs: &Mat,
r: Option<Mat>,
new_camera_matrix: &Mat,
size_width: i32,
size_height: i32,
) -> Result<JsValue, JsError> {
use purecv::calib3d::undistort::init_undistort_rectify_map;
use purecv::core::types::Size2i;

let cam_mat = require_f64(camera_matrix, "initUndistortRectifyMap (camera_matrix)")?;
let dist = require_f64(dist_coeffs, "initUndistortRectifyMap (dist_coeffs)")?;

let r_mat = match &r {
Some(mat) => Some(require_f64(mat, "initUndistortRectifyMap (R)")?),
None => None,
};

let new_cam_mat = require_f64(
new_camera_matrix,
"initUndistortRectifyMap (new_camera_matrix)",
)?;

let (map1, map2) = init_undistort_rectify_map(
cam_mat,
dist,
r_mat,
new_cam_mat,
Size2i::new(size_width, size_height),
)
.map_err(|e| JsError::new(&format!("{e}")))?;

let obj = js_sys::Object::new();
let js_map1 = Mat {
inner: purecv::core::dynamic::DynamicMatrix {
data: purecv::core::dynamic::DynamicData::F32(map1),
},
};
let js_map2 = Mat {
inner: purecv::core::dynamic::DynamicMatrix {
data: purecv::core::dynamic::DynamicData::F32(map2),
},
};

js_sys::Reflect::set(&obj, &JsValue::from_str("map1"), &JsValue::from(js_map1))
.map_err(|_| JsError::new("Failed to set map1"))?;
js_sys::Reflect::set(&obj, &JsValue::from_str("map2"), &JsValue::from(js_map2))
.map_err(|_| JsError::new("Failed to set map2"))?;

Ok(obj.into())
}

#[wasm_bindgen(js_name = "findFundamentalMat")]
pub fn find_fundamental_mat_wasm(
points1: &Point2fVector,
points2: &Point2fVector,
method: i32,
ransac_reproj_threshold: f64,
confidence: f64,
max_iters: usize,
) -> Result<JsValue, JsError> {
use purecv::calib3d::fundamental::{find_fundamental_mat, FundamentalMatMethod};

let method_enum = match method {
2 => FundamentalMatMethod::FM_8POINT,
8 => FundamentalMatMethod::FM_RANSAC,
_ => {
return Err(JsError::new(&format!(
"Unknown fundamental matrix method: {method}"
)))
}
};

let mut mask_vec = Vec::new();
let f_mat = find_fundamental_mat(
&points1.inner,
&points2.inner,
method_enum,
ransac_reproj_threshold,
confidence,
max_iters,
Some(&mut mask_vec),
)
.map_err(|e| JsError::new(&format!("{e}")))?;

let obj = js_sys::Object::new();
let js_f = Mat {
inner: purecv::core::dynamic::DynamicMatrix {
data: purecv::core::dynamic::DynamicData::F64(f_mat),
},
};
js_sys::Reflect::set(
&obj,
&JsValue::from_str("fundamental"),
&JsValue::from(js_f),
)
.map_err(|_| JsError::new("Failed to set fundamental"))?;

let js_mask = js_sys::Uint8Array::from(mask_vec.as_slice());
js_sys::Reflect::set(&obj, &JsValue::from_str("mask"), &js_mask)
.map_err(|_| JsError::new("Failed to set mask"))?;

Ok(obj.into())
}

// ---------------------------------------------------------------------------
// features2d — KeyPoint, FAST, and ORB bindings
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading