Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified StringArtRustImpl/processing.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion StringArtRustImpl/src/generators/greedy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::error::{Result, StringArtError};
use crate::state::app_state::StringArtState;
use crate::traits::StringArtGenerator;
use crate::traits::{LinePixelCache, StringArtGenerator};
use crate::utils::{
apply_line_darkness_from_pixels, calculate_line_score_from_pixels,
};
Expand Down
14 changes: 6 additions & 8 deletions StringArtRustImpl/src/rendering/image_renderer.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::error::{Result, StringArtError};
use crate::state::app_state::StringArtState;
use crate::traits::renderer::ImageRenderer as ImageRendererTrait;
use crate::utils::Coord;
use crate::traits::LinePixelCache;
use image::{ImageBuffer, Rgb, RgbImage};
use std::sync::{Arc, RwLock};

Expand All @@ -17,9 +17,11 @@ impl ImageRenderer {
}

/// Draws a line on the image.
fn draw_line(&self, img: &mut RgbImage, start: Coord, end: Coord, color: (u8, u8, u8)) {
fn draw_line(&self, img: &mut RgbImage, start: usize, end: usize, color: (u8, u8, u8)) {
let (width, height) = img.dimensions();
let pixels = crate::utils::get_line_pixels(start, end);

let pixel_cache = &self.state.read().unwrap().line_pixel_cache;
let pixels = pixel_cache.get(start, end);

for pixel in pixels {
if pixel.x >= 0 && pixel.x < width as i32 && pixel.y >= 0 && pixel.y < height as i32 {
Expand All @@ -37,7 +39,6 @@ impl ImageRendererTrait for ImageRenderer {
fn render_to_image(&self, line_color: Option<(u8, u8, u8)>) -> Result<RgbImage> {
let mut img;
let color;
let nail_coords;
let path;
{
let state = match self.state.try_read() {
Expand All @@ -53,13 +54,10 @@ impl ImageRendererTrait for ImageRenderer {
reason: "No path to render".to_string(),
});
}
nail_coords = state.nail_coords.clone();
path = state.path.clone();
}
for window in path.windows(2) {
let start = nail_coords[window[0]];
let end = nail_coords[window[1]];
self.draw_line(&mut img, start, end, color);
self.draw_line(&mut img, window[0], window[1], color);
}

Ok(img)
Expand Down
7 changes: 4 additions & 3 deletions StringArtRustImpl/src/state/app_state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::image_processing::EyeRegion;
use crate::state::config::StringArtConfig;
use crate::utils::{Coord, LinePixelCache};
use crate::state::cache::BasicLinePixelCache;
use crate::utils::{ Coord };
use ndarray::Array2;

/// Holds the shared state for the string art generation process.
Expand All @@ -14,7 +15,7 @@ pub struct StringArtState {
pub eye_protection_mask: Array2<f32>,
pub negative_space_mask: Array2<f32>,
pub path: Vec<usize>,
pub line_pixel_cache: LinePixelCache,
pub line_pixel_cache: BasicLinePixelCache,
}

impl StringArtState {
Expand All @@ -25,7 +26,7 @@ impl StringArtState {
nail_coords: Vec<Coord>,
) -> Self {
let image_size = config.image_size;
let line_pixel_cache = LinePixelCache::new(&nail_coords);
let line_pixel_cache = BasicLinePixelCache::new(&nail_coords);
Self {
config,
target_image,
Expand Down
81 changes: 81 additions & 0 deletions StringArtRustImpl/src/state/cache/basic_line_pixel_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use std::{collections::HashMap, sync::Mutex};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use crate::{traits::LinePixelCache, Coord};


/// A cache for pre-calculating and storing the pixels for every possible line.
#[derive(Debug, Clone)]
pub struct BasicLinePixelCache {
cache: HashMap<(usize, usize), Vec<Coord>>,
}

impl BasicLinePixelCache {
/// Creates a new cache and pre-calculates all line pixels.
pub fn new(nail_coords: &[Coord]) -> Self {
let num_nails = nail_coords.len();
let cache = Mutex::new(HashMap::new());

(0..num_nails).into_par_iter().for_each(|i| {
let local_results: Vec<((usize, usize), Vec<Coord>)> = ((i + 1)..num_nails)
.map(|j| {
let start = nail_coords[i];
let end = nail_coords[j];
let pixels = Self::get_line_pixels(start, end);
((i, j), pixels)
})
.collect();

let mut cache_lock = cache.lock().unwrap();
for (key, value) in local_results {
cache_lock.insert(key, value);
}
});
let unwrapped_cache = cache.into_inner().unwrap();
Self { cache: unwrapped_cache }
}

/// Traverse line pixels using Bresenham's line algorithm and apply a closure.
/// This avoids allocating a vector for the pixels.
fn get_line_pixels(start: Coord, end: Coord) -> Vec<Coord> {
let mut pixels = Vec::new();
let dx = (end.x - start.x).abs();
let dy = (end.y - start.y).abs();
let sx = if start.x < end.x { 1 } else { -1 };
let sy = if start.y < end.y { 1 } else { -1 };
let mut err = dx - dy;

let mut x = start.x;
let mut y = start.y;

loop {
pixels.push(Coord::new(x, y));

if x == end.x && y == end.y {
break;
}

let e2 = 2 * err;
if e2 > -dy {
err -= dy;
x += sx;
}
if e2 < dx {
err += dx;
y += sy;
}
}
pixels
}
}


impl LinePixelCache for BasicLinePixelCache {
fn get(&self, nail1: usize, nail2: usize) -> &Vec<Coord> {
let key = if nail1 < nail2 {
(nail1, nail2)
} else {
(nail2, nail1)
};
self.cache.get(&key).expect("Line pixels should be in cache")
}
}
3 changes: 3 additions & 0 deletions StringArtRustImpl/src/state/cache/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod basic_line_pixel_cache;

pub use self::basic_line_pixel_cache::BasicLinePixelCache;
1 change: 1 addition & 0 deletions StringArtRustImpl/src/state/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod app_state;
pub mod config;
pub mod cache;
9 changes: 9 additions & 0 deletions StringArtRustImpl/src/traits/line_pixel_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use crate::{ Coord };

/// Trait for string art path generators.
/// Implementations of this trait are responsible for calculating the optimal path
/// based on a given image and configuration.
pub trait LinePixelCache: Send + Sync {
/// Generate the string path using the specific algorithm.
fn get(&self, nail1: usize, nail2: usize) -> &Vec<Coord>;
}
4 changes: 3 additions & 1 deletion StringArtRustImpl/src/traits/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
pub mod generator;
pub mod mask;
pub mod renderer;

pub mod line_pixel_cache;

pub use self::generator::StringArtGenerator;
pub use self::mask::MaskApplicator;
pub use self::renderer::ImageRenderer;
pub use self::line_pixel_cache::LinePixelCache;
166 changes: 0 additions & 166 deletions StringArtRustImpl/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use ndarray::Array2;
use std::collections::HashMap;
use std::f64::consts::PI;
use rayon::prelude::*;
use std::sync::Mutex;

/// Represents a 2D coordinate
#[derive(Debug, Clone, Copy, PartialEq)]
Expand All @@ -17,48 +14,6 @@ impl Coord {
}
}

/// A cache for pre-calculating and storing the pixels for every possible line.
pub struct LinePixelCache {
cache: HashMap<(usize, usize), Vec<Coord>>,
}

impl LinePixelCache {
/// Creates a new cache and pre-calculates all line pixels.
pub fn new(nail_coords: &[Coord]) -> Self {
let num_nails = nail_coords.len();
let cache = Mutex::new(HashMap::new());

(0..num_nails).into_par_iter().for_each(|i| {
let local_results: Vec<((usize, usize), Vec<Coord>)> = ((i + 1)..num_nails)
.map(|j| {
let start = nail_coords[i];
let end = nail_coords[j];
let pixels = get_line_pixels(start, end);
((i, j), pixels)
})
.collect();

let mut cache_lock = cache.lock().unwrap();
for (key, value) in local_results {
cache_lock.insert(key, value);
}
});
let unwrapped_cache = cache.into_inner().unwrap();
Self { cache: unwrapped_cache }
}

/// Gets the pixels for a line between two nails.
/// Handles ordering of nail indices.
pub fn get(&self, nail1: usize, nail2: usize) -> &Vec<Coord> {
let key = if nail1 < nail2 {
(nail1, nail2)
} else {
(nail2, nail1)
};
self.cache.get(&key).expect("Line pixels should be in cache")
}
}

/// Calculate nail coordinates around a circle
pub fn calculate_nail_coords(num_nails: usize, center: Coord, radius: i32) -> Vec<Coord> {
let mut coords = Vec::with_capacity(num_nails);
Expand All @@ -72,81 +27,6 @@ pub fn calculate_nail_coords(num_nails: usize, center: Coord, radius: i32) -> Ve

coords
}

/// Traverse line pixels using Bresenham's line algorithm and apply a closure.
/// This avoids allocating a vector for the pixels.
fn traverse_line_pixels<F>(start: Coord, end: Coord, mut f: F)
where
F: FnMut(Coord),
{
let dx = (end.x - start.x).abs();
let dy = (end.y - start.y).abs();
let sx = if start.x < end.x { 1 } else { -1 };
let sy = if start.y < end.y { 1 } else { -1 };
let mut err = dx - dy;

let mut x = start.x;
let mut y = start.y;

loop {
f(Coord::new(x, y));

if x == end.x && y == end.y {
break;
}

let e2 = 2 * err;
if e2 > -dy {
err -= dy;
x += sx;
}
if e2 < dx {
err += dx;
y += sy;
}
}
}

/// Get line pixels using Bresenham's line algorithm
/// Returns a vector of coordinates representing the pixels on the line
pub fn get_line_pixels(start: Coord, end: Coord) -> Vec<Coord> {
let mut pixels = Vec::new();
traverse_line_pixels(start, end, |pixel| {
pixels.push(pixel);
});
pixels
}

/// Calculate the average value along a line in an image
pub fn calculate_line_score(
image: &Array2<f32>,
start: Coord,
end: Coord,
protection_mask: Option<&Array2<f32>>,
) -> f32 {
let pixels = get_line_pixels(start, end);
calculate_line_score_from_pixels(image, &pixels, protection_mask, None, 0.0)
}

/// Calculate line score with negative space awareness and eye enhancement
pub fn calculate_line_score_with_negative_space(
image: &Array2<f32>,
start: Coord,
end: Coord,
enhancement_mask: Option<&Array2<f32>>,
negative_space_mask: Option<&Array2<f32>>,
negative_space_penalty: f32,
) -> f32 {
let pixels = get_line_pixels(start, end);
calculate_line_score_from_pixels(
image,
&pixels,
enhancement_mask,
negative_space_mask,
negative_space_penalty,
)
}

/// Calculate line score from a pre-computed list of pixels.
pub fn calculate_line_score_from_pixels(
image: &Array2<f32>,
Expand Down Expand Up @@ -200,17 +80,6 @@ pub fn calculate_line_score_from_pixels(
final_score.max(0.0)
}

/// Apply line darkness to the residual image
pub fn apply_line_darkness(
residual: &mut Array2<f32>,
start: Coord,
end: Coord,
darkness: f32,
) {
let pixels = get_line_pixels(start, end);
apply_line_darkness_from_pixels(residual, &pixels, darkness);
}

/// Apply line darkness from a pre-computed list of pixels.
pub fn apply_line_darkness_from_pixels(
residual: &mut Array2<f32>,
Expand All @@ -229,38 +98,3 @@ pub fn apply_line_darkness_from_pixels(
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use ndarray::Array2;

#[test]
fn test_calculate_nail_coords() {
let coords = calculate_nail_coords(4, Coord::new(100, 100), 50);
assert_eq!(coords.len(), 4);

// Check first coordinate (0 degrees)
assert_eq!(coords[0], Coord::new(150, 100));
}

#[test]
fn test_get_line_pixels() {
let pixels = get_line_pixels(Coord::new(0, 0), Coord::new(2, 2));
assert_eq!(
pixels,
vec![Coord::new(0, 0), Coord::new(1, 1), Coord::new(2, 2)]
);
}

#[test]
fn test_calculate_line_score() {
let mut image = Array2::<f32>::zeros((3, 3));
image[[1, 1]] = 100.0;

let score = calculate_line_score(&image, Coord::new(0, 0), Coord::new(2, 2), None);

// Should be average of values along diagonal
assert!((score - 33.333_332).abs() < 1e-5);
}
}
Loading