use pyo3::prelude::*;
use pyo3::types::PyDict;
use scirs2_core::ndarray::Array2;
use scirs2_numpy::{IntoPyArray, PyArray2, PyArray3, PyArrayMethods};
use scirs2_vision::error::VisionError;
use image::{DynamicImage, GrayImage, ImageBuffer, Luma, Rgb};
use scirs2_vision::feature::canny::{canny, PreprocessMode};
use scirs2_vision::{
bilateral_filter, clahe, detect_and_compute, equalize_histogram, find_homography,
gaussian_blur, harris_corners, labels_to_color_image, laplacian_edges, median_filter,
normalize_brightness, prewitt_edges, rgb_to_grayscale, rgb_to_hsv, sobel_edges, unsharp_mask,
watershed,
};
fn numpy_to_gray_image(arr: &Bound<'_, PyArray2<u8>>) -> Result<DynamicImage, VisionError> {
let binding = arr.readonly();
let array = binding.as_array();
let (height, width) = array.dim();
let mut img = GrayImage::new(width as u32, height as u32);
for y in 0..height {
for x in 0..width {
img.put_pixel(x as u32, y as u32, Luma([array[[y, x]]]));
}
}
Ok(DynamicImage::ImageLuma8(img))
}
fn numpy_to_rgb_image(arr: &Bound<'_, PyArray3<u8>>) -> Result<DynamicImage, VisionError> {
let binding = arr.readonly();
let array = binding.as_array();
let shape = array.shape();
if shape.len() != 3 || shape[2] != 3 {
return Err(VisionError::InvalidParameter(
"Expected array with shape (height, width, 3)".to_string(),
));
}
let height = shape[0];
let width = shape[1];
let mut img = ImageBuffer::new(width as u32, height as u32);
for y in 0..height {
for x in 0..width {
img.put_pixel(
x as u32,
y as u32,
Rgb([array[[y, x, 0]], array[[y, x, 1]], array[[y, x, 2]]]),
);
}
}
Ok(DynamicImage::ImageRgb8(img))
}
fn gray_image_to_numpy(py: Python, img: &DynamicImage) -> Py<PyArray2<u8>> {
let gray = img.to_luma8();
let (width, height) = gray.dimensions();
let mut array = Array2::zeros((height as usize, width as usize));
for y in 0..height {
for x in 0..width {
array[[y as usize, x as usize]] = gray.get_pixel(x, y)[0];
}
}
array.into_pyarray(py).unbind()
}
fn rgb_image_to_numpy(py: Python, img: &DynamicImage) -> Py<PyArray3<u8>> {
let rgb = img.to_rgb8();
let (width, height) = rgb.dimensions();
let mut array = scirs2_core::ndarray::Array3::zeros((height as usize, width as usize, 3));
for y in 0..height {
for x in 0..width {
let pixel = rgb.get_pixel(x, y);
array[[y as usize, x as usize, 0]] = pixel[0];
array[[y as usize, x as usize, 1]] = pixel[1];
array[[y as usize, x as usize, 2]] = pixel[2];
}
}
array.into_pyarray(py).unbind()
}
#[pyfunction]
#[pyo3(signature = (image, diameter, sigma_space, sigma_color))]
fn bilateral_filter_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
diameter: u32,
sigma_space: f32,
sigma_color: f32,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let filtered = bilateral_filter(&img, diameter, sigma_space, sigma_color).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Bilateral filter error: {}", e))
})?;
Ok(gray_image_to_numpy(py, &filtered))
}
#[pyfunction]
#[pyo3(signature = (image, sigma))]
fn gaussian_blur_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
sigma: f32,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let blurred = gaussian_blur(&img, sigma).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Gaussian blur error: {}", e))
})?;
Ok(gray_image_to_numpy(py, &blurred))
}
#[pyfunction]
#[pyo3(signature = (image, kernel_size))]
fn median_filter_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
kernel_size: u32,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let filtered = median_filter(&img, kernel_size).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Median filter error: {}", e))
})?;
Ok(gray_image_to_numpy(py, &filtered))
}
#[pyfunction]
#[pyo3(signature = (image, tile_size=8, clip_limit=2.0))]
fn clahe_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
tile_size: u32,
clip_limit: f32,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let enhanced = clahe(&img, tile_size, clip_limit).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("CLAHE error: {}", e))
})?;
Ok(gray_image_to_numpy(py, &enhanced))
}
#[pyfunction]
fn equalize_histogram_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let equalized = equalize_histogram(&img).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Histogram equalization error: {}",
e
))
})?;
Ok(gray_image_to_numpy(py, &equalized))
}
#[pyfunction]
#[pyo3(signature = (image, min_out=0.0, max_out=1.0))]
fn normalize_brightness_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
min_out: f32,
max_out: f32,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let normalized = normalize_brightness(&img, min_out, max_out).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Normalize brightness error: {}",
e
))
})?;
Ok(gray_image_to_numpy(py, &normalized))
}
#[pyfunction]
#[pyo3(signature = (image, sigma=1.0, amount=1.0))]
fn unsharp_mask_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
sigma: f32,
amount: f32,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let sharpened = unsharp_mask(&img, sigma, amount).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Unsharp mask error: {}", e))
})?;
Ok(gray_image_to_numpy(py, &sharpened))
}
#[pyfunction]
#[pyo3(signature = (image, weights=None))]
fn rgb_to_grayscale_py(
py: Python,
image: &Bound<'_, PyArray3<u8>>,
weights: Option<[f32; 3]>,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_rgb_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let gray = rgb_to_grayscale(&img, weights).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("RGB to grayscale error: {}", e))
})?;
Ok(gray_image_to_numpy(py, &gray))
}
#[pyfunction]
fn rgb_to_hsv_py(py: Python, image: &Bound<'_, PyArray3<u8>>) -> PyResult<Py<PyArray3<u8>>> {
let img = numpy_to_rgb_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let hsv = rgb_to_hsv(&img).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("RGB to HSV error: {}", e))
})?;
Ok(rgb_image_to_numpy(py, &hsv))
}
#[pyfunction]
#[pyo3(signature = (image, threshold=0.1))]
fn sobel_edges_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
threshold: f32,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let edges = sobel_edges(&img, threshold).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Sobel edges error: {}", e))
})?;
Ok(gray_image_to_numpy(py, &DynamicImage::ImageLuma8(edges)))
}
#[pyfunction]
#[pyo3(signature = (image, sigma=1.4, low_threshold=0.05, high_threshold=0.15))]
fn canny_edges_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
sigma: f32,
low_threshold: f32,
high_threshold: f32,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let edges = canny(
&img,
sigma,
Some(low_threshold),
Some(high_threshold),
None,
false,
PreprocessMode::Reflect,
)
.map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Canny edges error: {}", e))
})?;
Ok(gray_image_to_numpy(py, &DynamicImage::ImageLuma8(edges)))
}
#[pyfunction]
#[pyo3(signature = (image, threshold=0.1))]
fn prewitt_edges_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
threshold: f32,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let edges = prewitt_edges(&img, threshold).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Prewitt edges error: {}", e))
})?;
Ok(gray_image_to_numpy(py, &DynamicImage::ImageLuma8(edges)))
}
#[pyfunction]
#[pyo3(signature = (image, threshold=0.1, use_diagonal=true))]
fn laplacian_edges_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
threshold: f32,
use_diagonal: bool,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let edges = laplacian_edges(&img, threshold, use_diagonal).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Laplacian edges error: {}", e))
})?;
Ok(gray_image_to_numpy(py, &DynamicImage::ImageLuma8(edges)))
}
#[pyfunction]
#[pyo3(signature = (image, block_size=3, k=0.04, threshold=100.0))]
fn harris_corners_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
block_size: usize,
k: f32,
threshold: f32,
) -> PyResult<Py<PyArray2<u8>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let corners_img = harris_corners(&img, block_size, k, threshold).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Harris corners error: {}", e))
})?;
let (width, height) = corners_img.dimensions();
let mut array = Array2::zeros((height as usize, width as usize));
for y in 0..height {
for x in 0..width {
array[[y as usize, x as usize]] = corners_img.get_pixel(x, y)[0];
}
}
Ok(array.into_pyarray(py).unbind())
}
#[pyfunction]
#[pyo3(signature = (image, max_features=500, contrast_threshold=0.03))]
fn detect_and_compute_sift_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
max_features: usize,
contrast_threshold: f32,
) -> PyResult<Vec<Py<PyDict>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let descriptors = detect_and_compute(&img, max_features, contrast_threshold).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("SIFT error: {}", e))
})?;
let mut result = Vec::new();
for desc in descriptors {
let dict = PyDict::new(py);
dict.set_item("x", desc.keypoint.x)?;
dict.set_item("y", desc.keypoint.y)?;
dict.set_item("scale", desc.keypoint.scale)?;
dict.set_item("orientation", desc.keypoint.orientation)?;
dict.set_item("descriptor", desc.vector.into_pyarray(py))?;
result.push(dict.into());
}
Ok(result)
}
#[pyfunction]
#[pyo3(signature = (image, markers=None, connectivity=8))]
fn watershed_py(
py: Python,
image: &Bound<'_, PyArray2<u8>>,
markers: Option<&Bound<'_, PyArray2<u32>>>,
connectivity: u8,
) -> PyResult<Py<PyArray2<u32>>> {
let img = numpy_to_gray_image(image).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Image conversion error: {}", e))
})?;
let marker_array =
markers.map(|m: &Bound<'_, PyArray2<u32>>| m.readonly().as_array().to_owned());
let labels = watershed(&img, marker_array.as_ref(), connectivity).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Watershed error: {}", e))
})?;
Ok(labels.into_pyarray(py).unbind())
}
#[pyfunction]
fn labels_to_color_image_py(
py: Python,
labels: &Bound<'_, PyArray2<u32>>,
) -> PyResult<Py<PyArray3<u8>>> {
let label_array = labels.readonly().as_array().to_owned();
let color_img = labels_to_color_image(&label_array, None);
Ok(rgb_image_to_numpy(py, &DynamicImage::ImageRgb8(color_img)))
}
#[pyfunction]
#[pyo3(signature = (src_points, dst_points, threshold=3.0, confidence=0.99))]
fn find_homography_py(
py: Python<'_>,
src_points: Vec<(f64, f64)>,
dst_points: Vec<(f64, f64)>,
threshold: f64,
confidence: f64,
) -> PyResult<(Py<PyArray2<f64>>, Vec<bool>)> {
let (h, inliers) =
find_homography(&src_points, &dst_points, threshold, confidence).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Find homography error: {}",
e
))
})?;
Ok((h.matrix.into_pyarray(py).unbind(), inliers))
}
pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(bilateral_filter_py, m)?)?;
m.add_function(wrap_pyfunction!(gaussian_blur_py, m)?)?;
m.add_function(wrap_pyfunction!(median_filter_py, m)?)?;
m.add_function(wrap_pyfunction!(clahe_py, m)?)?;
m.add_function(wrap_pyfunction!(equalize_histogram_py, m)?)?;
m.add_function(wrap_pyfunction!(normalize_brightness_py, m)?)?;
m.add_function(wrap_pyfunction!(unsharp_mask_py, m)?)?;
m.add_function(wrap_pyfunction!(rgb_to_grayscale_py, m)?)?;
m.add_function(wrap_pyfunction!(rgb_to_hsv_py, m)?)?;
m.add_function(wrap_pyfunction!(sobel_edges_py, m)?)?;
m.add_function(wrap_pyfunction!(canny_edges_py, m)?)?;
m.add_function(wrap_pyfunction!(prewitt_edges_py, m)?)?;
m.add_function(wrap_pyfunction!(laplacian_edges_py, m)?)?;
m.add_function(wrap_pyfunction!(harris_corners_py, m)?)?;
m.add_function(wrap_pyfunction!(detect_and_compute_sift_py, m)?)?;
m.add_function(wrap_pyfunction!(watershed_py, m)?)?;
m.add_function(wrap_pyfunction!(labels_to_color_image_py, m)?)?;
m.add_function(wrap_pyfunction!(find_homography_py, m)?)?;
Ok(())
}