rustnum 0.1.0

NumPy-style n-dimensional arrays for Rust: one array type, NumPy names, pure Rust.
Documentation
//! Image operations on a [64, 64] array: normalize, flip, transpose, box blur, save as .npy.
//!
//! Run with: `cargo run --release -p rustnum --example image_ops`
//!
//! The equivalent NumPy code:
//!
//! ```python
//! import numpy as np
//!
//! rng = np.random.default_rng(7)
//! x = np.linspace(0, 255, 64)
//! xx, yy = np.meshgrid(x, x)
//! img = np.clip((xx + yy) / 2 + rng.normal(0, 20, (64, 64)), 0, 255)
//!
//! norm = (img - img.min()) / (img.max() - img.min())    # scale to [0, 1]
//! left_right = norm[:, ::-1]                            # np.flip(norm, axis=1)
//! up_down = norm[::-1, :]                               # np.flip(norm, axis=0)
//! transposed = norm.T
//!
//! # 3x3 box blur: add the 9 shifted copies of the interior, divide by 9
//! acc = np.zeros((62, 62))
//! for di in range(3):
//!     for dj in range(3):
//!         acc += norm[di:di + 62, dj:dj + 62]
//! blurred = acc / 9.0
//!
//! np.save("image_ops_blurred.npy", blurred)
//! ```
//!
//! The blurred image is 62 x 62 because the border pixels have no full 3x3 neighborhood.

use rustnum as np;

fn main() -> np::Result<()> {
    let mut rng = np::random::Rng::seed(7);

    // A synthetic image: a diagonal ramp plus noise, clipped to the 0..255 range.
    let axis = np::linspace(0.0, 255.0, 64);
    let (xx, yy) = np::meshgrid(&axis, &axis);
    let img = ((&xx + &yy) * 0.5 + rng.normal(0.0, 20.0, &[64, 64])).clip(0.0, 255.0);
    println!(
        "image shape = {:?}, min = {:.1}, max = {:.1}",
        img.shape(),
        img.min(),
        img.max()
    );

    // Normalize to [0, 1].
    let lo = img.min();
    let hi = img.max();
    let norm = (&img - lo) / (hi - lo);
    println!("normalized: min = {}, max = {}", norm.min(), norm.max());

    // Flips and transpose are views: no pixels are copied.
    let left_right = norm.flip(1);
    let up_down = norm.flip(0);
    let transposed = norm.t();
    println!(
        "flipped and transposed shapes: {:?} {:?} {:?}",
        left_right.shape(),
        up_down.shape(),
        transposed.shape()
    );
    assert_eq!(left_right[[0, 0]], norm[[0, 63]]);
    assert_eq!(up_down[[0, 0]], norm[[63, 0]]);
    assert_eq!(transposed[[2, 5]], norm[[5, 2]]);

    // 3x3 box blur with slicing: add the nine shifted views of the interior.
    let mut acc = np::zeros::<f64>(&[62, 62]);
    for di in 0..3isize {
        for dj in 0..3isize {
            acc += &norm.slice(&np::s![di..di + 62, dj..dj + 62]);
        }
    }
    let blurred = acc / 9.0;
    println!("blurred shape = {:?}", blurred.shape());
    println!(
        "std before = {:.4}, std after = {:.4}",
        norm.std(),
        blurred.std()
    );
    assert!(
        blurred.std() < norm.std(),
        "blur should reduce the spread of pixel values"
    );

    // Check one pixel by hand: blurred[0, 0] is centered on pixel [1, 1], so it is
    // the mean of norm[0..3, 0..3].
    let patch = norm.slice(&np::s![0..3, 0..3]);
    assert!((blurred[[0, 0]] - patch.mean()).abs() < 1e-12);

    // Save as .npy and read it back.
    let path = std::env::temp_dir().join("rustnum_image_ops_blurred.npy");
    let path = path.to_string_lossy().into_owned();
    np::io::save_npy(&path, &blurred)?;
    let loaded = np::io::load_npy::<f64>(&path)?;
    assert!(loaded == blurred, "saved and loaded arrays differ");
    println!("saved {path} and read it back unchanged");
    Ok(())
}