depth_map_processor/
lib.rs

1use colorgrad::Gradient;
2use image::{ImageBuffer, Luma, RgbImage};
3use std::path::Path;
4
5/// Processing stats from a depth-map normalization run.
6pub struct DepthMapStats {
7    pub width: u32,
8    pub height: u32,
9    pub min_val: u16,
10    pub max_val: u16,
11    pub sample_depth_m: f32,
12}
13
14/// Normalize a 16-bit depth map to 8-bit grayscale and optionally save a
15/// Turbo colormap visualization.
16pub fn process_depth_map(
17    input_path: &Path,
18    output_path: &Path,
19    viz_path: Option<&Path>,
20) -> Result<DepthMapStats, Box<dyn std::error::Error>> {
21    // The image crate automatically handles formats.
22    // .into_luma16() ensures we treat it as a single-channel 16-bit image (uint16).
23    let img = image::open(input_path)
24        .map_err(|_| {
25            format!(
26                "Could not open or find the image at: {}",
27                input_path.display()
28            )
29        })?
30        .into_luma16();
31
32    let (width, height) = img.dimensions();
33
34    // Accessing pixel at (0, 0). Note: image crate uses (x, y) indexing.
35    let pixel_val = img.get_pixel(0, 0)[0];
36    let depth_in_meters = pixel_val as f32 / 1000.0;
37
38    // Normalization Logic (cv2.normalize with NORM_MINMAX)
39    // First, find min and max values in the 16-bit buffer
40    let (min_val, max_val) = img.pixels().fold((u16::MAX, u16::MIN), |(min, max), p| {
41        (min.min(p[0]), max.max(p[0]))
42    });
43
44    // Create a new 8-bit grayscale image buffer
45    let mut norm_img: ImageBuffer<Luma<u8>, Vec<u8>> = ImageBuffer::new(width, height);
46
47    // Apply normalization: (val - min) / (max - min) * 255
48    let range = (max_val - min_val) as f32;
49
50    for (x, y, pixel) in img.enumerate_pixels() {
51        let val = pixel[0];
52        let normalized = if range > 0.0 {
53            ((val as f32 - min_val as f32) / range * 255.0) as u8
54        } else {
55            0
56        };
57        norm_img.put_pixel(x, y, Luma([normalized]));
58    }
59
60    // Apply a colormap (mimicking cv2.applyColorMap(..., cv2.COLORMAP_TURBO))
61    let grad = colorgrad::preset::turbo();
62    let mut colored_img: RgbImage = ImageBuffer::new(width, height);
63
64    for (x, y, pixel) in norm_img.enumerate_pixels() {
65        // colorgrad expects a float between 0.0 and 1.0
66        let t = pixel[0] as f32 / 255.0f32;
67        let rgba = grad.at(t).to_rgba8();
68        colored_img.put_pixel(x, y, image::Rgb([rgba[0], rgba[1], rgba[2]]));
69    }
70
71    // Save to file (Visualization)
72    if let Some(path) = viz_path {
73        colored_img.save(path)?;
74    }
75
76    // Save to file with grayscale normalization
77    norm_img.save(output_path)?;
78
79    Ok(DepthMapStats {
80        width,
81        height,
82        min_val,
83        max_val,
84        sample_depth_m: depth_in_meters,
85    })
86}