eorst 1.0.1

Earth Observation and Remote Sensing Toolkit - library for raster processing pipelines
//! Image processing filters and OpenCV integration.
//!
//! This module provides the `Filters` trait for morphological operations
//! and blur filters, backed by OpenCV when the `use_opencv` feature is enabled.
//!
//! ## Zero-Copy OpenCV Path
//!
//! When the `use_opencv` feature is enabled, all filter operations use a
//! zero-copy approach:
//! - **Input**: `Mat::new_rows_cols_with_data` creates a 2D Mat borrowing the
//!   `Array2` data without copying
//! - **Output**: `Mat::new_rows_cols_with_data_mut` writes directly into a
//!   pre-allocated `Array2` without copying
//!
//! This eliminates 2 memory copies per operation (input + output), saving
//! significant bandwidth for large raster blocks.

use ndarray::Array2;

#[cfg(feature = "use_opencv")]
use crate::core_types::RasterType;
#[cfg(feature = "use_opencv")]
use opencv::boxed_ref::{BoxedRef, BoxedRefMut};
#[cfg(feature = "use_opencv")]
use opencv::core;
#[cfg(feature = "use_opencv")]
use opencv::imgproc;
#[cfg(feature = "use_opencv")]
use opencv::prelude::{MatTraitConst, MatTraitConstManual, MatTraitManual};
#[cfg(feature = "use_opencv")]
use opencv::Result;

/// Trait for image processing filters (requires `use_opencv` feature).
///
/// All operations use zero-copy OpenCV bindings internally — the input
/// `Array2` is borrowed (not copied) and the output is written in-place.
pub trait Filters<T> {
    /// Applies morphological erosion to the raster.
    fn erode(&self, size: usize, kernel_size: i32) -> Array2<T>;
    /// Applies morphological dilation to the raster.
    fn dilate(&self, size: usize, kernel_size: i32) -> Array2<T>;
    /// Applies median blur to the raster.
    fn median_blur(&self, size: usize) -> Array2<T>;
    /// Applies Gaussian blur to the raster.
    fn gaussian(&self, kernel_size: usize, sigma: f64) -> Array2<T>;
}

/// Type alias for a 2D raster block slice.
pub type RasterBlockSlice2<T> = Array2<T>;

// ─── Zero-Copy OpenCV Helpers ───

/// Creates a zero-copy immutable OpenCV `Mat` borrowing an `Array2` slice.
///
/// The returned `BoxedRef` reads from the `Array2`'s underlying storage
/// without copying. When dropped, no deallocation occurs.
#[cfg(feature = "use_opencv")]
fn mat_borrow<'a, T>(
    rows: i32,
    cols: i32,
    data: &'a [T],
) -> Result<BoxedRef<'a, core::Mat>>
where
    T: opencv::prelude::DataType + 'static,
{
    core::Mat::new_rows_cols_with_data::<T>(rows, cols, data)
}

/// Creates a zero-copy mutable OpenCV `Mat` that writes into an `Array2` slice.
///
/// The returned `BoxedRefMut` writes directly into the `Array2`'s underlying
/// storage. When dropped, no deallocation occurs — the `Array2` retains ownership.
#[cfg(feature = "use_opencv")]
fn mat_borrow_mut<'a, T>(
    rows: i32,
    cols: i32,
    data: &'a mut [T],
) -> Result<BoxedRefMut<'a, core::Mat>>
where
    T: opencv::prelude::DataType + 'static,
{
    core::Mat::new_rows_cols_with_data_mut::<T>(rows, cols, data)
}

// ─── Deprecated Copy-Based Helpers ───

/// Converts an `ndarray::ArrayView2` into an OpenCV `Mat` by copying data.
///
/// **Deprecated**: The `Filters` trait now uses zero-copy internally.
/// This function is retained only for backward compatibility.
#[cfg(feature = "use_opencv")]
#[deprecated(
    since = "0.3.2",
    note = "This function copies data. The Filters trait now uses zero-copy internally."
)]
pub fn arrayview2_to_mat<T>(data: ndarray::ArrayView2<'_, T>) -> Result<BoxedRef<'_, core::Mat>>
where
    T: opencv::prelude::DataType + 'static,
{
    let rows = data.dim().0 as i32;
    let cols = data.dim().1 as i32;

    let mut mat = unsafe {
        core::Mat::new_rows_cols(
            rows,
            cols,
            T::opencv_type(),
        )?
    };

    mat.data_typed_mut::<T>()?
        .copy_from_slice(data.as_slice().unwrap());

    Ok(mat.into())
}

/// Converts an OpenCV `Mat` back into an `ndarray::Array2<T>` by copying data.
///
/// **Deprecated**: The `Filters` trait now uses zero-copy internally.
/// This function is retained only for backward compatibility.
#[cfg(feature = "use_opencv")]
#[deprecated(
    since = "0.3.2",
    note = "This function copies data. The Filters trait now uses zero-copy internally."
)]
pub fn mat_to_array2<T>(mat: &core::Mat) -> Result<Array2<T>>
where
    T: opencv::prelude::DataType + Clone,
{
    let rows = mat.rows() as usize;
    let cols = mat.cols() as usize;

    let mat_slice: &[T] = mat.data_typed()?;

    let array = ndarray::ArrayView2::from_shape((rows, cols), mat_slice)
        .unwrap()
        .to_owned();

    Ok(array)
}

#[cfg(feature = "use_opencv")]
impl<T> Filters<T> for RasterBlockSlice2<T>
where
    T: RasterType + opencv::prelude::DataType,
{
    fn erode(&self, size: usize, kernel_structure: i32) -> Array2<T> {
        let (rows, cols) = self.dim();
        let mut output = Array2::<T>::zeros((rows, cols));

        // Zero-copy: src borrows self's data as 2D Mat, dst writes into output's data
        let src = mat_borrow::<T>(rows as i32, cols as i32, self.as_slice().unwrap()).unwrap();
        let mut dst = mat_borrow_mut::<T>(rows as i32, cols as i32, output.as_slice_mut().unwrap()).unwrap();
        let kernel = imgproc::get_structuring_element(
            kernel_structure,
            core::Size::new(size as i32, size as i32),
            core::Point::new(-1, -1),
        )
        .unwrap();

        imgproc::erode(
            &src,
            &mut dst,
            &kernel,
            core::Point::new(-1, -1),
            1,
            core::BORDER_REFLECT,
            core::Scalar::all(0.0),
        )
        .unwrap();

        // BoxedRef and BoxedRefMut drop without deallocating — output now holds results
        output
    }

    fn dilate(&self, size: usize, kernel_structure: i32) -> Array2<T> {
        let (rows, cols) = self.dim();
        let mut output = Array2::<T>::zeros((rows, cols));

        let src = mat_borrow::<T>(rows as i32, cols as i32, self.as_slice().unwrap()).unwrap();
        let mut dst = mat_borrow_mut::<T>(rows as i32, cols as i32, output.as_slice_mut().unwrap()).unwrap();
        let kernel = imgproc::get_structuring_element(
            kernel_structure,
            core::Size::new(size as i32, size as i32),
            core::Point::new(-1, -1),
        )
        .unwrap();

        imgproc::dilate(
            &src,
            &mut dst,
            &kernel,
            core::Point::new(-1, -1),
            1,
            core::BORDER_REFLECT,
            core::Scalar::all(0.0),
        )
        .unwrap();

        output
    }

    fn median_blur(&self, size: usize) -> Array2<T> {
        let (rows, cols) = self.dim();
        let mut output = Array2::<T>::zeros((rows, cols));

        let src = mat_borrow::<T>(rows as i32, cols as i32, self.as_slice().unwrap()).unwrap();
        let mut dst = mat_borrow_mut::<T>(rows as i32, cols as i32, output.as_slice_mut().unwrap()).unwrap();

        imgproc::median_blur(&src, &mut dst, size.try_into().unwrap()).unwrap();

        output
    }

    fn gaussian(&self, kernel_size: usize, sigma: f64) -> Array2<T> {
        let (rows, cols) = self.dim();
        let mut output = Array2::<T>::zeros((rows, cols));

        let src = mat_borrow::<T>(rows as i32, cols as i32, self.as_slice().unwrap()).unwrap();
        let mut dst = mat_borrow_mut::<T>(rows as i32, cols as i32, output.as_slice_mut().unwrap()).unwrap();

        let ksize = core::Size::new(kernel_size as i32, kernel_size as i32);
        let border_type = core::BORDER_DEFAULT;
        let hint = core::AlgorithmHint::ALGO_HINT_DEFAULT;
        imgproc::gaussian_blur(&src, &mut dst, ksize, sigma, sigma, border_type, hint).unwrap();

        output
    }
}