use super::cell_type::*;
use super::source_3d::*;
use crate::errors::*;
use crate::mesh_3d::{Shape3D};
use image::{DynamicImage, GenericImageView};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct Source3DFromImage {
width: usize,
height: usize,
depth: usize,
cells: Vec<Vec<Vec<CellType>>>,
}
impl Source3DFromImage {
pub const DEFAULT_THRESHOLD: f32 = 0.5;
pub fn from_paths<P: AsRef<Path>>(paths: &[P], threshold: f32) -> Result<Self> {
if paths.is_empty() {
return Err(Error::report_bug("No image paths provided"));
}
let mut images = Vec::with_capacity(paths.len());
for path in paths {
let img = image::open(path)
.map_err(|e| Error::report_bug(&format!("Failed to load image: {e}")))?;
images.push(img);
}
Self::from_images(&images, threshold)
}
pub fn from_images(images: &[DynamicImage], threshold: f32) -> Result<Self> {
if images.is_empty() {
return Err(Error::report_bug("No images provided"));
}
let width = images[0].width() as usize;
let height = images[0].height() as usize;
let depth = images.len();
if width == 0 || height == 0 {
return Err(Error::report_bug("Images have zero dimensions"));
}
for (z, img) in images.iter().enumerate() {
if img.width() as usize != width || img.height() as usize != height {
return Err(Error::report_bug(&format!(
"Image at z={z} has different dimensions: {}x{} vs {}x{}",
img.width(),
img.height(),
width,
height
)));
}
}
let mut cells = Vec::with_capacity(depth);
for img in images {
let mut layer = Vec::with_capacity(height);
for y in 0..height {
let mut row = Vec::with_capacity(width);
for x in 0..width {
let pixel = img.get_pixel(x as u32, y as u32);
let r = pixel[0] as f32 / 255.0;
let g = pixel[1] as f32 / 255.0;
let b = pixel[2] as f32 / 255.0;
let luma = 0.299 * r + 0.587 * g + 0.114 * b;
let cell_type = if luma < threshold {
CellType::WALL
} else {
CellType::FLOOR
};
row.push(cell_type);
}
layer.push(row);
}
cells.push(layer);
}
Ok(Self {
width,
height,
depth,
cells,
})
}
pub fn new(images: &[DynamicImage]) -> Result<Self> {
Self::from_images(images, Self::DEFAULT_THRESHOLD)
}
}
impl Source3D for Source3DFromImage {
fn get(&self, x: usize, y: usize, z: usize) -> Result<CellType> {
if z >= self.depth || y >= self.height || x >= self.width {
return Err(Error::invalid_xyz(x, y, z));
}
Ok(self.cells[z][y][x])
}
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
fn depth(&self) -> usize {
self.depth
}
}
impl Shape3D for Source3DFromImage {
fn shape(&self) -> (usize, usize, usize) {
(self.width, self.height, self.depth)
}
}
impl std::fmt::Display for Source3DFromImage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Source3DFromImage ({}x{}x{}):", self.width(), self.height(), self.depth())?;
write!(f, "{}", crate::mesh_source::repr::repr_source_3d(self))
}
}
#[cfg(test)]
mod tests {
use super::*;
use image::{GrayImage, Luma};
fn create_test_image(width: u32, height: u32, brightness: u8) -> DynamicImage {
let img = GrayImage::from_fn(width, height, |_, _| Luma([brightness]));
DynamicImage::ImageLuma8(img)
}
fn create_test_image_pattern(width: u32, height: u32) -> DynamicImage {
let img = GrayImage::from_fn(width, height, |x, y| {
if (x + y) % 2 == 0 {
Luma([255]) } else {
Luma([0]) }
});
DynamicImage::ImageLuma8(img)
}
#[test]
fn test_dimensions() {
let layer0 = create_test_image(10, 8, 255);
let layer1 = create_test_image(10, 8, 255);
let layer2 = create_test_image(10, 8, 255);
let source = Source3DFromImage::new(&[layer0, layer1, layer2]).unwrap();
assert_eq!(source.width(), 10);
assert_eq!(source.height(), 8);
assert_eq!(source.depth(), 3);
assert_eq!(Shape3D::shape(&source), (10, 8, 3));
}
#[test]
fn test_threshold() {
let bright = create_test_image(5, 5, 200); let dark = create_test_image(5, 5, 50);
let source = Source3DFromImage::new(&[bright, dark]).unwrap();
assert_eq!(source.get(0, 0, 0).unwrap(), CellType::FLOOR);
assert_eq!(source.get(4, 4, 0).unwrap(), CellType::FLOOR);
assert_eq!(source.get(2, 2, 0).unwrap(), CellType::FLOOR);
assert_eq!(source.get(0, 0, 1).unwrap(), CellType::WALL);
assert_eq!(source.get(4, 4, 1).unwrap(), CellType::WALL);
assert_eq!(source.get(2, 2, 1).unwrap(), CellType::WALL);
}
#[test]
fn test_pattern() {
let layer = create_test_image_pattern(4, 4);
let source = Source3DFromImage::new(&[layer]).unwrap();
assert_eq!(source.get(0, 0, 0).unwrap(), CellType::FLOOR); assert_eq!(source.get(1, 0, 0).unwrap(), CellType::WALL); assert_eq!(source.get(0, 1, 0).unwrap(), CellType::WALL); assert_eq!(source.get(1, 1, 0).unwrap(), CellType::FLOOR); }
#[test]
fn test_out_of_bounds() {
let layer = create_test_image(5, 5, 255);
let source = Source3DFromImage::new(&[layer]).unwrap();
assert!(source.get(5, 0, 0).is_err());
assert!(source.get(0, 5, 0).is_err());
assert!(source.get(0, 0, 1).is_err());
assert!(source.get(10, 10, 10).is_err());
}
#[test]
fn test_mismatched_dimensions() {
let layer0 = create_test_image(10, 8, 255);
let layer1 = create_test_image(10, 10, 255);
let result = Source3DFromImage::new(&[layer0, layer1]);
assert!(result.is_err());
}
#[test]
fn test_empty_images() {
let result = Source3DFromImage::new(&[]);
assert!(result.is_err());
}
#[test]
fn test_from_paths_testdata() {
use crate::mesh_source::repr_source_3d;
use crate::mesh_source::testutil::testdata_path;
let paths = vec![
testdata_path("3d/blob1-20x10.png"),
testdata_path("3d/blob2-20x10.png"),
testdata_path("3d/blob3-20x10.png"),
];
let source = Source3DFromImage::from_paths(&paths, 0.5).unwrap();
assert_eq!(source.width(), 20);
assert_eq!(source.height(), 10);
assert_eq!(source.depth(), 3);
let repr = repr_source_3d(&source);
let expected = "\
....................
....................
....#.##............
..###########.......
...############.....
....###########.....
......######........
....................
....................
....................
----
................####
.................###
....#.##.........###
..###########....###
...############...##
....###########....#
#.....######........
#...................
##..................
###.................
----
................####
.................###
....#.#............#
..#####............#
...####............#
....###............#
#.....#.............
#...................
##..................
###.................
----
";
assert_eq!(repr, expected, "3D image representation should match expected pattern");
}
#[test]
fn test_from_paths_with_threshold_testdata() {
use crate::mesh_source::testutil::testdata_path;
let paths = vec![
testdata_path("3d/blob1-20x10.png"),
testdata_path("3d/blob2-20x10.png"),
testdata_path("3d/blob3-20x10.png"),
];
let source_low = Source3DFromImage::from_paths(&paths, 0.1).unwrap();
let source_high = Source3DFromImage::from_paths(&paths, 0.9).unwrap();
let mut free_count_low = 0;
let mut free_count_high = 0;
for z in 0..source_low.depth() {
for y in 0..source_low.height() {
for x in 0..source_low.width() {
if source_low.get(x, y, z).unwrap() == CellType::FLOOR {
free_count_low += 1;
}
if source_high.get(x, y, z).unwrap() == CellType::FLOOR {
free_count_high += 1;
}
}
}
}
assert!(
free_count_low >= free_count_high,
"Lower threshold should have more or equal free cells than higher threshold"
);
}
#[test]
fn test_from_images_testdata() {
use crate::mesh_source::testutil::testdata_path;
let img1 = image::open(testdata_path("3d/blob1-20x10.png")).unwrap();
let img2 = image::open(testdata_path("3d/blob2-20x10.png")).unwrap();
let img3 = image::open(testdata_path("3d/blob3-20x10.png")).unwrap();
let images = vec![img1, img2, img3];
let source = Source3DFromImage::from_images(&images, 0.5).unwrap();
assert_eq!(source.width(), 20);
assert_eq!(source.height(), 10);
assert_eq!(source.depth(), 3);
assert!(source.get(0, 0, 0).is_ok());
assert!(source.get(19, 9, 2).is_ok());
assert!(source.get(20, 0, 0).is_err()); assert!(source.get(0, 0, 3).is_err()); }
}