use std::path::Path;
#[cfg(not(target_family = "wasm"))]
use std::time;
extern crate image;
extern crate pathfinding;
use image::{GenericImage, RgbImage};
#[cfg(all(feature = "progress_bar", feature = "parallel"))]
use indicatif::ParallelProgressIterator;
#[cfg(all(feature = "progress_bar", not(feature = "parallel")))]
use indicatif::ProgressIterator;
use log::info;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[cfg(feature = "blueprint")]
use crate::blueprint::{Blueprint, Cell};
use crate::distance_matrix::DistanceMatrix;
use crate::error::Error;
use crate::macros;
use crate::master::Master;
use crate::metrics::{norm_l1, MetricFn};
use crate::utils;
#[derive(Debug, Clone)]
pub struct Mosaic {
pub master: Master,
pub tiles: Vec<RgbImage>,
pub grid_size: (u32, u32),
}
impl Mosaic {
pub fn from_file_and_dir<P: AsRef<Path>, Q: AsRef<Path>>(
master_file: P,
tile_dir: Q,
grid_size: (u32, u32),
) -> Result<Self, Error> {
let master_img = image::open(master_file)?.to_rgb8();
info!("Loading tiles");
let tiles = utils::read_images_from_dir(tile_dir)?;
Self::from_images(master_img, tiles, grid_size)
}
pub fn from_images(
master_img: RgbImage,
tiles: Vec<RgbImage>,
grid_size: (u32, u32),
) -> Result<Self, Error> {
let master = Master::from_image(master_img, grid_size)?;
if tiles.iter().any(|img| img.dimensions() != master.cell_size) {
return Err(format!(
"All tiles must be the same size as the grid cells: {}x{}",
master.cell_size.0, master.cell_size.1
)
.into());
}
if tiles.len() < grid_size.0 as usize * grid_size.1 as usize {
return Err(format!(
"Not enough tiles: {} for grid size: {}x{}",
tiles.len(),
grid_size.0,
grid_size.1
)
.into());
}
Ok(Self {
master,
tiles,
grid_size,
})
}
pub fn distance_matrix(&self) -> DistanceMatrix<i64> {
self.distance_matrix_with_metric(norm_l1)
}
pub fn distance_matrix_with_metric(&self, metric: MetricFn) -> DistanceMatrix<i64> {
#[cfg(not(target_family = "wasm"))]
info!("Starting distance matrix computation...");
#[cfg(not(target_family = "wasm"))]
let start_time = time::Instant::now();
let d_matrix = macros::maybe_progress_bar!(
macros::iter_or_par_iter!(self.master.cells),
"Computing distance matrix",
par
)
.flat_map(|cell| macros::iter_or_par_iter!(self.tiles).map(|tile| metric(tile, cell)))
.collect();
#[cfg(not(target_family = "wasm"))]
info!("Completed in {:?}", start_time.elapsed());
DistanceMatrix {
rows: self.master.cells.len(),
columns: self.tiles.len(),
data: d_matrix,
}
}
pub fn build(&self, distance_matrix: DistanceMatrix<i64>) -> Result<RgbImage, Error> {
if distance_matrix.rows != self.master.cells.len()
|| distance_matrix.columns < self.tiles.len()
{
return Err(
"The distance matrix rows must match the number of master cells, and the number of columns must be greater than or equal to the number of tiles.".into(),
);
}
let assignments = distance_matrix.assignments();
let (grid_width, grid_height) = self.grid_size;
let (cell_width, cell_height) = self.master.cell_size;
let mut mosaic_img = RgbImage::new(self.master.img.width(), self.master.img.height());
info!(
"Building mosaic, size: {}x{}, cell size: {}x{}, grid size: {}x{}",
mosaic_img.width(),
mosaic_img.height(),
cell_width,
cell_height,
grid_width,
grid_height
);
for (cell_idx, tile_idx) in assignments.into_iter().enumerate() {
let x = (cell_idx as u32 % grid_width) * cell_width;
let y = (cell_idx as u32 / grid_width) * cell_height;
let tile = self.tiles.get(tile_idx % self.tiles.len()).unwrap();
mosaic_img.copy_from(tile, x, y)?;
}
Ok(mosaic_img)
}
#[cfg(feature = "blueprint")]
pub fn build_blueprint(
&self,
distance_matrix: DistanceMatrix<i64>,
) -> Result<Blueprint, Error> {
if distance_matrix.rows != self.master.cells.len()
|| distance_matrix.columns < self.tiles.len()
{
return Err(
"The distance matrix rows must match the number of master cells, and the number of columns must be greater than or equal to the number of tiles.".into(),
);
}
let assignments = distance_matrix.assignments();
let (grid_width, grid_height) = self.grid_size;
let (cell_width, cell_height) = self.master.cell_size;
let cells = assignments
.into_iter()
.enumerate()
.map(|(cell_idx, tile_idx)| {
let x = (cell_idx as u32 % grid_width) * cell_width;
let y = (cell_idx as u32 / grid_width) * cell_height;
Cell {
tile_index: tile_idx % self.tiles.len(),
x,
y,
}
})
.collect::<Vec<_>>();
Ok(Blueprint {
cells,
cell_width,
cell_height,
grid_width,
grid_height,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn test_dir() -> PathBuf {
PathBuf::from("tests/data/mosaic")
}
fn test_master_img() -> PathBuf {
PathBuf::from("tests/data/master/master.png")
}
fn test_tile_dir() -> PathBuf {
test_dir().join("tiles/")
}
fn test_faces_dir() -> PathBuf {
test_dir().join("faces/")
}
#[test]
fn test_mosaic_creation_from_valid_data() {
let grid_size = (4, 4);
let mosaic = Mosaic::from_file_and_dir(test_master_img(), test_tile_dir(), grid_size);
assert!(mosaic.is_ok());
let mosaic = mosaic.unwrap();
assert_eq!(mosaic.master.img.width(), 256);
assert_eq!(mosaic.master.img.height(), 256);
assert!(mosaic.tiles.len() >= mosaic.master.cells.len());
assert!(mosaic
.tiles
.iter()
.all(|tile| tile.dimensions() == mosaic.master.cell_size));
}
#[test]
fn test_mosaic_creation_with_mismatched_tile_sizes() {
let grid_size = (5, 5);
let mosaic = Mosaic::from_file_and_dir(test_master_img(), test_tile_dir(), grid_size);
assert!(mosaic.is_err());
}
#[test]
fn test_invalid_master_file_path() {
let grid_size = (4, 4);
let mosaic = Mosaic::from_file_and_dir("invalid/master.png", test_tile_dir(), grid_size);
assert!(mosaic.is_err());
}
#[test]
fn test_invalid_tile_directory() {
let grid_size = (4, 4);
let mosaic = Mosaic::from_file_and_dir(test_master_img(), "invalid/tile_dir", grid_size);
assert!(mosaic.is_err());
}
#[test]
fn test_not_enough_tiles() {
let master_img = image::open(test_master_img()).unwrap().to_rgb8();
let tiles = vec![image::imageops::resize(
&master_img,
64,
64,
image::imageops::FilterType::Nearest,
)];
let mosaic = Mosaic::from_images(master_img, tiles, (4, 4));
assert!(mosaic.is_err());
}
#[test]
fn test_distance_matrix() {
let master_img = image::open(test_master_img()).unwrap().to_rgb8();
let tiles = utils::read_images_from_dir(test_tile_dir()).unwrap();
let mosaic = Mosaic::from_images(master_img, tiles, (4, 4)).unwrap();
let distance_matrix = mosaic.distance_matrix();
assert_eq!(
distance_matrix.data.len(),
mosaic.master.cells.len() * mosaic.tiles.len()
);
}
#[test]
fn test_mosaic_build() {
let master_img = image::imageops::resize(
&image::open(test_master_img()).unwrap().to_rgb8(),
240,
240,
image::imageops::FilterType::Nearest,
);
let tiles_imgs = utils::read_images_from_dir(test_faces_dir()).unwrap();
let result = Mosaic::from_images(master_img, tiles_imgs, (12, 12));
assert!(result.is_ok());
let mosaic = result.unwrap();
assert_eq!(mosaic.master.cells.len(), 144);
let d_matrix = mosaic.distance_matrix();
let result = mosaic.build(d_matrix);
assert!(result.is_ok());
let mosaic_img = result.unwrap();
assert_eq!(mosaic_img.width(), 240);
assert_eq!(mosaic_img.height(), 240);
let expected_path = test_dir().join("mosaic.png");
if std::env::var("PHOMO_UPDATE_EXPECTED").is_ok() {
mosaic_img.save(&expected_path).unwrap();
}
let expected_img = image::open(expected_path).unwrap().to_rgb8();
assert_eq!(expected_img, mosaic_img);
}
#[test]
fn test_mosaic_build_repeat() {
let master_img = image::imageops::resize(
&image::open(test_master_img()).unwrap().to_rgb8(),
240,
240,
image::imageops::FilterType::Nearest,
);
let tiles_imgs = utils::read_images_from_dir(test_faces_dir()).unwrap();
let result = Mosaic::from_images(master_img, tiles_imgs, (12, 12));
assert!(result.is_ok());
let mosaic = result.unwrap();
assert_eq!(mosaic.master.cells.len(), 144);
let d_matrix = mosaic.distance_matrix();
let repeat_amount = 2;
let d_matrix_repeat = d_matrix.with_repeat_tiles(repeat_amount);
let result = mosaic.build(d_matrix_repeat);
assert!(result.is_ok());
let mosaic_img = result.unwrap();
assert_eq!(mosaic_img.width(), 240);
assert_eq!(mosaic_img.height(), 240);
let expected_path = test_dir().join("mosaic_repeats.png");
mosaic_img.save(&expected_path).unwrap();
if std::env::var("PHOMO_UPDATE_EXPECTED").is_ok() {
mosaic_img.save(&expected_path).unwrap();
}
let expected_img = image::open(expected_path).unwrap().to_rgb8();
assert_eq!(expected_img, mosaic_img);
}
#[cfg(feature = "blueprint")]
#[test]
fn test_mosaic_build_blueprint() {
if std::env::var("CI").is_ok() {
println!("Test skipped: Running on GitHub Actions.");
return;
}
let master_img = image::imageops::resize(
&image::open(test_master_img()).unwrap().to_rgb8(),
240,
240,
image::imageops::FilterType::Nearest,
);
let tiles_imgs = utils::read_images_from_dir(test_faces_dir()).unwrap();
let result = Mosaic::from_images(master_img, tiles_imgs, (12, 12));
assert!(result.is_ok());
let mosaic = result.unwrap();
assert_eq!(mosaic.master.cells.len(), 144);
let d_matrix = mosaic.distance_matrix();
let result = mosaic.build_blueprint(d_matrix);
assert!(result.is_ok());
let blueprint = result.unwrap();
let serialized = serde_json::to_string_pretty(&blueprint).unwrap();
let expected_path = test_dir().join("mosaic_blueprint.json");
if std::env::var("PHOMO_UPDATE_EXPECTED").is_ok() {
std::fs::write(&expected_path, serialized).unwrap();
}
let expected_blueprint: Blueprint =
serde_json::from_str(&std::fs::read_to_string(&expected_path).unwrap()).unwrap();
assert_eq!(expected_blueprint, blueprint);
}
#[cfg(feature = "blueprint")]
#[test]
fn test_mosaic_blueprint_render() {
if std::env::var("CI").is_ok() {
println!("Test skipped: Running on GitHub Actions.");
return;
}
let blueprint_path = test_dir().join("mosaic_blueprint.json");
let blueprint: Blueprint =
serde_json::from_str(&std::fs::read_to_string(&blueprint_path).unwrap()).unwrap();
let master_img = image::imageops::resize(
&image::open(test_master_img()).unwrap().to_rgb8(),
240,
240,
image::imageops::FilterType::Nearest,
);
let tiles_imgs = utils::read_images_from_dir(test_faces_dir()).unwrap();
let result = blueprint.render(&master_img, &tiles_imgs);
assert!(result.is_ok());
let mosaic_img = result.unwrap();
let expected_path = test_dir().join("mosaic_blueprint_rendered.png");
if std::env::var("PHOMO_UPDATE_EXPECTED").is_ok() {
mosaic_img.save(&expected_path).unwrap();
}
let expected_img = image::open(expected_path).unwrap().to_rgb8();
assert_eq!(expected_img, mosaic_img);
}
}