├── src ├── lsd.rs ├── optimize │ ├── mod.rs │ ├── adc_direct.rs │ └── direct.rs ├── graph │ ├── mod.rs │ ├── mst.rs │ └── tsp.rs ├── math.rs ├── dither.rs ├── voronoi │ ├── hull.rs │ └── mod.rs ├── color │ ├── approx.rs │ ├── mod.rs │ └── transform.rs ├── main.rs ├── render.rs └── filter.rs ├── .github ├── FUNDING.yml └── workflows │ └── rust.yml ├── peppers_demo.png ├── .gitignore ├── Cargo.toml ├── examples └── config.json ├── README.md └── LICENSE /src/lsd.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ['sameer'] 2 | -------------------------------------------------------------------------------- /src/optimize/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod adc_direct; 2 | pub mod direct; 3 | -------------------------------------------------------------------------------- /peppers_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sameer/raster2svg/HEAD/peppers_demo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | /target 3 | /*.png 4 | /*.svg 5 | /*.jpg 6 | /*.jpeg 7 | /*.tiff 8 | -------------------------------------------------------------------------------- /src/graph/mod.rs: -------------------------------------------------------------------------------- 1 | /// Find the [Minimum Spanning Tree (MST)](https://en.wikipedia.org/wiki/Minimum_spanning_tree) 2 | pub mod mst; 3 | /// Approximately solve the [Traveling Salesman Problem (TSP)](https://en.wikipedia.org/wiki/Travelling_salesman_problem) 4 | pub mod tsp; 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "raster2svg" 3 | version = "0.1.0" 4 | authors = ["Sameer Puri "] 5 | edition = "2021" 6 | description = "Convert raster graphics to stylish SVGs" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | cairo-rs = { version = "^0", default-features = false, features = [ 12 | "svg", 13 | "v1_16", 14 | ] } 15 | clap = { version = "4", features = ["derive"] } 16 | image = { version = "0", features = ["jpeg", "png", "tiff", "bmp"] } 17 | lyon_geom = "0" 18 | ndarray = { version = "0", features = ["rayon", "serde"] } 19 | ndarray-stats = "0" 20 | num-rational = { version = "0.4", default-features = false, features = ["std"] } 21 | num-traits = "0" 22 | paste = "1" 23 | rand = { version = "0.8", features = ["alloc"] } 24 | rayon = "1" 25 | rustc-hash = "1" 26 | serde = { version = "1", features = ["derive"] } 27 | serde_json = "1" 28 | serde_with = "1" 29 | spade = "1" 30 | tracing = "0.1.41" 31 | tracing-subscriber = "0.3.19" 32 | uom = { version = "0", features = ["use_serde"] } 33 | 34 | [dev-dependencies] 35 | pretty_assertions = "1" 36 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: raster2svg 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Build 14 | run: cargo build 15 | coverage: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: nightly 24 | override: true 25 | profile: minimal 26 | components: llvm-tools-preview 27 | - uses: actions-rs/install@v0.1 28 | with: 29 | crate: grcov 30 | version: 0.8.0 31 | use-tool-cache: true 32 | - uses: actions-rs/cargo@v1 33 | with: 34 | command: build 35 | env: 36 | RUSTFLAGS: '-Zinstrument-coverage' 37 | RUSTDOCFLAGS: '-Zinstrument-coverage' 38 | - uses: actions-rs/cargo@v1 39 | with: 40 | command: test 41 | args: --all-features --no-fail-fast 42 | env: 43 | RUSTFLAGS: '-Zinstrument-coverage' 44 | RUSTDOCFLAGS: '-Zinstrument-coverage' 45 | LLVM_PROFILE_FILE: 'codecov-instrumentation-%p-%m.profraw' 46 | - name: grcov 47 | run: grcov . -s . --binary-path ./target/debug/ -t lcov --branch -o lcov.info 48 | - uses: codecov/codecov-action@v1 49 | with: 50 | token: ${{secrets.CODECOV_TOKEN}} 51 | -------------------------------------------------------------------------------- /examples/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "dots_per_inch": 96, 3 | "style": "stipple", 4 | "color_model": "cielab", 5 | "color_method": "vector", 6 | "super_sample": 1, 7 | "implements": [ 8 | { 9 | "Pen": { 10 | "diameter": 0.001, 11 | "color": "#158fd4" 12 | } 13 | }, 14 | { 15 | "Pen": { 16 | "diameter": 0.001, 17 | "color": "#382375" 18 | } 19 | }, 20 | { 21 | "Pen": { 22 | "diameter": 0.001, 23 | "color": "#000000" 24 | } 25 | }, 26 | { 27 | "Pen": { 28 | "diameter": 0.001, 29 | "color": "#222c8f" 30 | } 31 | }, 32 | { 33 | "Pen": { 34 | "diameter": 0.001, 35 | "color": "#517b23" 36 | } 37 | }, 38 | { 39 | "Pen": { 40 | "diameter": 0.001, 41 | "color": "#d62e69" 42 | } 43 | }, 44 | { 45 | "Pen": { 46 | "diameter": 0.001, 47 | "color": "#dc12fa" 48 | } 49 | }, 50 | { 51 | "Pen": { 52 | "diameter": 0.001, 53 | "color": "#2547b4" 54 | } 55 | }, 56 | { 57 | "Pen": { 58 | "diameter": 0.001, 59 | "color": "#20835f" 60 | } 61 | }, 62 | { 63 | "Pen": { 64 | "diameter": 0.001, 65 | "color": "#c72537" 66 | } 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /src/math.rs: -------------------------------------------------------------------------------- 1 | use num_traits::{PrimInt, Signed}; 2 | use std::fmt::Debug; 3 | 4 | #[macro_export] 5 | /// Implementation of the Kahan-Babushka-Neumaier algorithm for reduced numerical error in summation 6 | /// 7 | /// 8 | macro_rules! kbn_summation { 9 | (for $pat: pat in $expr: expr => { 10 | $('loop: { $(let $loopvar: ident = $loopvar_expr: expr;)* })? 11 | $($var: ident += $var_expr: expr;)* 12 | }) => { 13 | let ($($var,)*) = { 14 | use paste::paste; 15 | paste! { 16 | $( 17 | let mut $var: f64 = 0.; 18 | let mut [<$var compensation>] = 0.; 19 | )* 20 | for $pat in $expr { 21 | $($(let $loopvar = $loopvar_expr;)*)? 22 | $( 23 | let input = $var_expr; 24 | let t = $var + input; 25 | [<$var compensation>] += if $var.abs() >= input.abs() { 26 | ($var - t) + input 27 | } else { 28 | (input - t) + $var 29 | }; 30 | $var = t; 31 | )* 32 | } 33 | ($($var + [<$var compensation>],)*) 34 | } 35 | }; 36 | }; 37 | } 38 | 39 | /// Square of the Euclidean distance between signed 2D coordinates 40 | #[inline] 41 | pub fn abs_distance_squared( 42 | a: [T; N], 43 | b: [T; N], 44 | ) -> T { 45 | let mut acc = T::zero(); 46 | for i in 0..N { 47 | acc = acc + (a[i] - b[i]).pow(2); 48 | } 49 | acc 50 | } 51 | 52 | #[cfg(test)] 53 | #[test] 54 | fn test_summation() { 55 | use std::f64::consts::*; 56 | let input = [FRAC_PI_8, FRAC_PI_2, FRAC_PI_6, FRAC_PI_3, FRAC_PI_4]; 57 | kbn_summation! { 58 | for x in input => { 59 | out += x; 60 | } 61 | } 62 | 63 | assert_ne!(input.iter().sum::(), out); 64 | assert_eq!(out, 4.31968989868596570288) 65 | } 66 | -------------------------------------------------------------------------------- /src/dither.rs: -------------------------------------------------------------------------------- 1 | use ndarray::{azip, s, Array2, ArrayView, ArrayView1, ArrayView3}; 2 | 3 | pub trait Dither { 4 | fn dither(&self, image: ArrayView3, palette: &[[f64; N]]) 5 | -> Array2; 6 | fn find_closest_palette_color( 7 | &self, 8 | palette: &[ArrayView1], 9 | color: ArrayView1, 10 | ) -> Option { 11 | palette 12 | .iter() 13 | .enumerate() 14 | .min_by(|(_, a), (_, b)| { 15 | let mut color_a_sq = color.to_owned() - *a; 16 | color_a_sq.mapv_inplace(|x| x.powi(2)); 17 | let mut color_b_sq = color.to_owned() - *b; 18 | color_b_sq.mapv_inplace(|x| x.powi(2)); 19 | 20 | color_a_sq.sum().partial_cmp(&color_b_sq.sum()).unwrap() 21 | }) 22 | .map(|(i, _)| i) 23 | } 24 | } 25 | 26 | pub struct FloydSteinberg; 27 | 28 | impl FloydSteinberg {} 29 | 30 | impl Dither for FloydSteinberg { 31 | fn dither( 32 | &self, 33 | image: ArrayView3, 34 | palette: &[[f64; N]], 35 | ) -> Array2 { 36 | let palette_as_array = palette.iter().map(ArrayView::from).collect::>(); 37 | let (_colors, width, height) = image.dim(); 38 | 39 | let mut dithered = Array2::zeros((width, height)); 40 | let mut compensated_image = image.to_owned(); 41 | for y in 0..height { 42 | for x in 0..width { 43 | let old = compensated_image.slice(s![.., x, y]).to_owned(); 44 | let new = self 45 | .find_closest_palette_color(&palette_as_array, old.view()) 46 | .unwrap_or(0); 47 | dithered[[x, y]] = new; 48 | let quantization_error = old - palette_as_array[new]; 49 | for ([i, j], error_fraction) in [ 50 | ([1, 0], 7. / 16.), 51 | ([-1isize, 1], 3. / 16.), 52 | ([0, 1], 5. / 16.), 53 | ([1, 1], 1. / 16.), 54 | ] { 55 | let x_i = match i { 56 | 1 if x + 1 == width => { 57 | continue; 58 | } 59 | -1 if x == 0 => { 60 | continue; 61 | } 62 | 1 => x + 1, 63 | -1 => x - 1, 64 | 0 => x, 65 | _ => unreachable!(), 66 | }; 67 | let y_j = match j { 68 | 1 if y + 1 == height => { 69 | continue; 70 | } 71 | 1 => y + 1, 72 | 0 => y, 73 | _ => unreachable!(), 74 | }; 75 | let pixel = compensated_image.slice_mut(s![.., x_i, y_j]); 76 | azip! { 77 | (p in pixel, q in &quantization_error) *p += q * error_fraction 78 | } 79 | } 80 | } 81 | } 82 | dithered 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/voronoi/hull.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp::PartialOrd, 3 | ops::{Add, Mul}, 4 | }; 5 | 6 | use num_traits::Zero; 7 | 8 | /// Andrew's monotone chain convex hull algorithm 9 | /// 10 | /// Points must be sorted by x-coordinate and tie-broken by y-coordinate. Collinear points will be excluded from the hull. 11 | /// 12 | /// 13 | pub fn convex_hull(points: &[[T; 2]]) -> Vec<[T; 2]> 14 | where 15 | T: Zero + Mul + Add + Copy + PartialOrd, 16 | { 17 | if points.len() <= 3 { 18 | return points.to_vec(); 19 | } 20 | 21 | let mut lower = lower_convex_hull(points); 22 | lower.pop(); 23 | 24 | let mut upper = upper_convex_hull(points); 25 | upper.pop(); 26 | 27 | lower.append(&mut upper); 28 | lower 29 | } 30 | 31 | /// Lower half of [`convex_hull`]. 32 | pub fn lower_convex_hull(points: &[[T; 2]]) -> Vec<[T; 2]> 33 | where 34 | T: Zero + Mul + Add + Copy + PartialOrd, 35 | { 36 | if points.len() <= 3 { 37 | return points.to_vec(); 38 | } 39 | 40 | let mut lower = Vec::with_capacity(points.len() / 2); 41 | for point in points { 42 | while lower.len() >= 2 43 | && !is_counter_clockwise(lower[lower.len() - 2], lower[lower.len() - 1], *point) 44 | { 45 | lower.pop(); 46 | } 47 | lower.push(*point); 48 | } 49 | lower 50 | } 51 | 52 | /// Upper half of [`convex_hull`]. 53 | pub fn upper_convex_hull(points: &[[T; 2]]) -> Vec<[T; 2]> 54 | where 55 | T: Zero + Mul + Add + Copy + PartialOrd, 56 | { 57 | if points.len() <= 3 { 58 | return points.to_vec(); 59 | } 60 | 61 | let mut upper = Vec::with_capacity(points.len() / 2); 62 | for point in points.iter().rev() { 63 | while upper.len() >= 2 64 | && !is_counter_clockwise(upper[upper.len() - 2], upper[upper.len() - 1], *point) 65 | { 66 | upper.pop(); 67 | } 68 | upper.push(*point); 69 | } 70 | upper 71 | } 72 | 73 | /// Check whether there is a counter-clockwise turn using the cross product of ca and cb interpreted as 3D vectors. 74 | fn is_counter_clockwise(a: [T; 2], b: [T; 2], c: [T; 2]) -> bool 75 | where 76 | T: Zero + Mul + Add + Copy + PartialOrd, 77 | { 78 | #[allow(clippy::suspicious_operation_groupings)] 79 | let positive = a[0] * b[1] + c[0] * c[1] + a[1] * c[0] + c[1] * b[0]; 80 | #[allow(clippy::suspicious_operation_groupings)] 81 | let negative = a[0] * c[1] + c[0] * b[1] + a[1] * b[0] + c[1] * c[0]; 82 | 83 | positive > negative 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use crate::voronoi::hull::lower_convex_hull; 89 | 90 | use super::convex_hull; 91 | 92 | #[test] 93 | fn test_convex_hull_of_triangle() { 94 | let points = [[0, 0], [0, 1], [1, 0]]; 95 | assert_eq!(convex_hull(&points), points); 96 | } 97 | 98 | #[test] 99 | fn test_convex_hull_of_square() { 100 | let points = [[0, 0], [0, 1], [1, 0], [1, 1]]; 101 | assert_eq!( 102 | convex_hull(&points), 103 | [points[0], points[2], points[3], points[1]] 104 | ); 105 | } 106 | 107 | #[test] 108 | fn test_convex_hull_of_square_with_inscribed_point() { 109 | let points = [[0, 0], [0, 2], [1, 1], [2, 0], [2, 2]]; 110 | assert_eq!( 111 | convex_hull(&points), 112 | [points[0], points[3], points[4], points[1]] 113 | ); 114 | } 115 | 116 | #[test] 117 | fn test_lower_convex_hull_of_line_segments() { 118 | let points = [ 119 | [0, 0], 120 | [0, 1], 121 | [1, 0], 122 | [2, 0], 123 | [3, 0], 124 | [3, 2], 125 | [4, 4], 126 | [5, 5], 127 | [6, 8], 128 | ]; 129 | assert_eq!( 130 | lower_convex_hull(&points), 131 | [points[0], points[4], points[7], points[8]] 132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/color/approx.rs: -------------------------------------------------------------------------------- 1 | use lyon_geom::euclid::default::Vector3D; 2 | use ndarray::{s, Array, Array3, ArrayView1, ArrayView3}; 3 | use tracing::debug; 4 | 5 | use crate::{kbn_summation, optimize::adc_direct::AdcDirect, ColorModel}; 6 | 7 | use super::Color; 8 | 9 | impl ColorModel { 10 | pub fn approximate(self, image: ArrayView3, palette: &[Color]) -> Array3 { 11 | let (_, width, height) = image.dim(); 12 | 13 | let mut image_in_cylindrical_color_model = { 14 | let image_in_color_model = self.convert(image); 15 | self.cylindrical(image_in_color_model.view()) 16 | }; 17 | let implements_in_color_model = palette 18 | .iter() 19 | .map(|c| self.convert_single(c)) 20 | .collect::>(); 21 | let mut implements_in_cylindrical_color_model = implements_in_color_model 22 | .into_iter() 23 | .map(|c| self.cylindrical_single(c)) 24 | .collect::>(); 25 | 26 | let white_in_cylindrical_color_model = 27 | self.cylindrical_single(self.convert_single(&Color::from([1.; 3]))); 28 | 29 | // Convert lightness into darkness and clamp it 30 | image_in_cylindrical_color_model 31 | .slice_mut(s![2, .., ..]) 32 | .mapv_inplace(|lightness| (white_in_cylindrical_color_model[2] - lightness).max(0.)); 33 | implements_in_cylindrical_color_model 34 | .iter_mut() 35 | .for_each(|[_, _, lightness]| { 36 | *lightness = (white_in_cylindrical_color_model[2] - *lightness).max(0.); 37 | }); 38 | 39 | let implement_hue_vectors = implements_in_cylindrical_color_model 40 | .into_iter() 41 | .map(|[hue, magnitude, darkness]| { 42 | let (sin, cos) = hue.sin_cos(); 43 | Vector3D::new(cos * magnitude, sin * magnitude, darkness) 44 | }) 45 | .collect::>(); 46 | 47 | let mut image_in_implements = Array3::::zeros((palette.len(), width, height)); 48 | // let mut cached_colors = FxHashMap::default(); 49 | for y in 0..height { 50 | debug!(y); 51 | for x in 0..width { 52 | let desired: [f64; 3] = image_in_cylindrical_color_model 53 | .slice(s![.., x, y]) 54 | .to_vec() 55 | .try_into() 56 | .expect("image slice is a pixel"); 57 | 58 | let direct = AdcDirect { 59 | function: self.objective_function(desired, &implement_hue_vectors), 60 | bounds: Array::from_elem(implement_hue_vectors.len(), [0., 1.]), 61 | // max_evaluations: None, 62 | // max_iterations: Some(100), 63 | max_evaluations: Some(10_000), 64 | max_iterations: None, 65 | }; 66 | 67 | let (best, _best_cost) = direct.run(); 68 | 69 | image_in_implements 70 | .slice_mut(s![.., x, y]) 71 | .assign(&best.view()); 72 | } 73 | } 74 | 75 | image_in_implements 76 | } 77 | 78 | pub fn objective_function( 79 | self, 80 | desired: [f64; 3], 81 | implement_hue_vectors: &'_ [Vector3D], 82 | ) -> impl Fn(ArrayView1) -> f64 + '_ { 83 | move |param| { 84 | kbn_summation! { 85 | for i in 0..implement_hue_vectors.len() => { 86 | weighted_vector_x += implement_hue_vectors[i].x * param[i]; 87 | weighted_vector_y += implement_hue_vectors[i].y * param[i]; 88 | weighted_vector_z += implement_hue_vectors[i].z * param[i]; 89 | } 90 | } 91 | let weighted_vector = 92 | Vector3D::::from([weighted_vector_x, weighted_vector_y, weighted_vector_z]); 93 | // Convert back to cylindrical model (hue, chroma, darkness) 94 | let actual = [ 95 | weighted_vector.y.atan2(weighted_vector.x), 96 | weighted_vector.to_2d().length(), 97 | weighted_vector.z, 98 | ]; 99 | ColorModel::Cielab.cylindrical_diff(desired, actual) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/graph/mst.rs: -------------------------------------------------------------------------------- 1 | use num_traits::{FromPrimitive, PrimInt, Signed}; 2 | use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; 3 | use spade::{ 4 | delaunay::{DelaunayTreeLocate, IntDelaunayTriangulation}, 5 | SpadeNum, 6 | }; 7 | use std::{ 8 | cmp::Reverse, 9 | collections::BinaryHeap, 10 | fmt::Debug, 11 | hash::{BuildHasherDefault, Hash}, 12 | }; 13 | 14 | use crate::math::abs_distance_squared; 15 | 16 | #[derive(PartialEq, Eq, Hash, Debug)] 17 | struct PriorityQueueEdge { 18 | from: [T; 2], 19 | to: [T; 2], 20 | } 21 | 22 | impl PartialOrd 23 | for PriorityQueueEdge 24 | { 25 | fn partial_cmp(&self, other: &Self) -> Option { 26 | Some( 27 | abs_distance_squared(self.from, self.to) 28 | .cmp(&abs_distance_squared(other.from, other.to)), 29 | ) 30 | } 31 | } 32 | 33 | impl Ord for PriorityQueueEdge { 34 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 35 | self.partial_cmp(other).unwrap() 36 | } 37 | } 38 | 39 | /// Given the delaunay triangulation of 2D points, compute the MST with Prim's algorithm in O(E log(v)) time. 40 | /// 41 | /// 42 | pub fn compute_mst( 43 | points: &[[T; 2]], 44 | delaunay: &IntDelaunayTriangulation<[T; 2], DelaunayTreeLocate<[T; 2]>>, 45 | ) -> Vec<[[T; 2]; 2]> 46 | where 47 | T: PrimInt + Signed + FromPrimitive + Hash + Debug + SpadeNum, 48 | { 49 | let edges_by_vertex: HashMap<_, Vec<_>> = delaunay 50 | .vertices() 51 | .map(|vertex| { 52 | let from = *vertex; 53 | ( 54 | from, 55 | vertex 56 | .ccw_out_edges() 57 | .map(|edge| { 58 | let to = *edge.to(); 59 | PriorityQueueEdge { from, to } 60 | }) 61 | .collect(), 62 | ) 63 | }) 64 | .collect(); 65 | 66 | let mut in_mst = HashSet::with_capacity_and_hasher(points.len(), BuildHasherDefault::default()); 67 | let mut edge_priority_queue = BinaryHeap::new(); 68 | 69 | // Kickstart MST with 1 vertex 70 | if !points.is_empty() { 71 | let first_vertex = points[0]; 72 | in_mst.insert(first_vertex); 73 | edge_priority_queue.extend(edges_by_vertex[&first_vertex].iter().map(Reverse)); 74 | } 75 | 76 | let mut mst = Vec::with_capacity(points.len().saturating_sub(1)); 77 | while let Some(shortest_edge) = edge_priority_queue.pop() { 78 | // Claim: we know the "from" of the shortest edge will always be 79 | // in the MST, because all edges in the priority queue point 80 | // outwards from the tree built so far. 81 | match in_mst.contains(&shortest_edge.0.to) { 82 | // Edge would not add a new point to the MST 83 | true => continue, 84 | // Add edges introduced by new vertex 85 | false => { 86 | let to = &shortest_edge.0.to; 87 | in_mst.insert(*to); 88 | mst.push([shortest_edge.0.from, shortest_edge.0.to]); 89 | 90 | edge_priority_queue.extend( 91 | edges_by_vertex[&shortest_edge.0.to] 92 | .iter() 93 | .filter(|edge| !in_mst.contains(&edge.to)) 94 | .map(Reverse), 95 | ); 96 | if mst.len() == points.len().saturating_sub(1) { 97 | // Early stopping condition, MST already has all the edges 98 | break; 99 | } 100 | } 101 | } 102 | } 103 | mst 104 | } 105 | 106 | #[cfg(test)] 107 | #[test] 108 | fn test_mst_is_correct_for_trivial_case() { 109 | let mut delaunay = IntDelaunayTriangulation::new(); 110 | let points = [[0, 0], [1, 1], [2, 2]]; 111 | for point in &points { 112 | delaunay.insert(*point); 113 | } 114 | assert_eq!( 115 | compute_mst(&points, &delaunay), 116 | &[[[0, 0], [1, 1]], [[1, 1], [2, 2]]] 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # raster2svg 2 | 3 | [![raster2svg](https://github.com/sameer/raster2svg/actions/workflows/rust.yml/badge.svg)](https://github.com/sameer/raster2svg/actions/workflows/rust.yml) 4 | [![codecov](https://codecov.io/gh/sameer/raster2svg/branch/main/graph/badge.svg?token=85CHPyYu7y)](https://codecov.io/gh/sameer/raster2svg) 5 | 6 | Convert raster graphics into stylistic art renderings. The output of this program is intended for direct use with [svg2gcode](https://github.com/sameer/svg2gcode) to draw with a pen plotter. 7 | 8 | ## Demo 9 | 10 | Using *4.2.07 Peppers* from the [SIPI image database](http://sipi.usc.edu/database/database.php?volume=misc&image=13#top): 11 | 12 | ``` 13 | cargo run --release -- peppers.tiff -o peppers.svg --style tsp 14 | ``` 15 | 16 | ![Peppers comparison](peppers_demo.png) 17 | 18 | ## Pipeline 19 | 20 | 1. Image preprocessing 21 | * Derive key (black) using D50 illumination lightness (D65 sRGB to D50 CIELAB) 22 | * Derive colors by vector projection onto CIELAB or HSL hue + chroma 23 | * Square pixel values 24 | 1. Do Linde-Buzo-Gray stippling 25 | * Find Voronoi tesselation with Jump flooding algorithm (JFA) 26 | * Split stippling points along the cell's second-order moments when they cover too much capacity 27 | * Remove stippling points when they do not cover enough capacity 28 | * Calculate cell centroids and move points to them 29 | * Repeat until no splits/removes occur 30 | 1. Get Delaunay triangulation using [spade](https://github.com/Stoeoef/spade) 31 | 1. Find Euclidean MST with Prim's algorithm using edges from the Delaunay triangulation 32 | 1. Approximate an open-loop TSP path through the points 33 | * MST to TSP 34 | * Local Improvement with 4 operators: relocate, disentangle, 2-opt, and link swap 35 | 1. Draw to SVG 36 | 37 | 38 | # References 39 | 40 | Grouped by pipeline stage, all of the papers/pages below provided guidance in creating raster2svg. 41 | 42 | Notice for lawyers: no papers are hosted here, they are all provided by the authors or from sci-hub. 43 | 44 | ## Preprocessing 45 | 46 | * Color conversion matrices https://github.com/colour-science/colour/ 47 | * https://en.wikipedia.org/wiki/SRGB 48 | * https://en.wikipedia.org/wiki/CIE_1931_color_space 49 | * https://en.wikipedia.org/wiki/CIELAB_color_space 50 | * From Stippling to Scribbling http://archive.bridgesmathart.org/2015/bridges2015-267.pdf 51 | 52 | ## Stippling 53 | 54 | * Weighted Voronoi Stippling https://www.cs.ubc.ca/labs/imager/tr/2002/secord2002b/secord.2002b.pdf 55 | * Weighted Linde-Buzo-Gray Stippling http://graphics.uni-konstanz.de/publikationen/Deussen2017LindeBuzoGray/WeightedLindeBuzoGrayStippling_authorversion.pdf 56 | * Multi-Class Inverted Stippling https://kops.uni-konstanz.de/bitstream/handle/123456789/55976/Schulz_2-3pieljazuoer1.pdf?sequence=1&isAllowed=y 57 | * Linking soft computing to art introduction of efficient k-continuous line drawing https://ieyjzhou.github.io/CIEG/Paper/KCLD_2018_Published_Version.pdf 58 | * Andrew's monotone chain convex hull algorithm https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain 59 | * Beyond Stippling -- Methods for Distributing Objects on the Plane: http://kops.uni-konstanz.de/bitstream/handle/123456789/6219/Beyond_Stippling_Methods_for_Distributing_Objects_on_the_Plane_2003.pdf?sequence=1&isAllowed=y 60 | 61 | ### Voronoi Diagram 62 | 63 | * Jump Flooding in GPU with Applications to Voronoi Diagram and Distance Transform https://www.comp.nus.edu.sg/~tants/jfa/i3d06.pdf 64 | 65 | ## MST 66 | 67 | * Algorithms for computing Euclidean MSTs in two dimensions https://en.wikipedia.org/wiki/Euclidean_minimum_spanning_tree#Algorithms_for_computing_EMSTs_in_two_dimensions 68 | 69 | ## TSP 70 | 71 | * Converting MST to TSP Path by Branch Elimination http://cs.uef.fi/sipu/pub/applsci-11-00177.pdf 72 | * Which Local Search Operator Works Best for the Open-Loop TSP? https://www.mdpi.com/2076-3417/9/19/3985/pdf 73 | * TSP Art https://archive.bridgesmathart.org/2005/bridges2005-301.pdf 74 | 75 | ## Edges + Hatching 76 | 77 | * Flow-Based Image Abstraction http://www.cs.umsl.edu/~kang/Papers/kang_tvcg09.pdf 78 | * Coherent Line Drawing http://umsl.edu/cmpsci/about/People/Faculty/HenryKang/coon.pdf 79 | 80 | ## Honorable mentions 81 | 82 | There are also some noteworthy papers that while interesting did not directly influence raster2svg (yet!). 83 | 84 | * Amplitude Modulated Line-Based Halftoning http://graphics.uni-konstanz.de/publikationen/Ahmed2016AmplitudeModulatedLine/paper.pdf 85 | * Structure grid for directional stippling http://www.cs.umsl.edu/~kang/Papers/kang_gm2011.pdf 86 | * Halftoning and Stippling http://graphics.uni-konstanz.de/publikationen/Deussen2013HalftoningStippling/Deussen2013HalftoningStippling.pdf 87 | * Capacity-constrained point distributions https://sci-hub.st/https://doi.org/10.1145/1576246.1531392 88 | * Fast Capacity Constrained Voronoi Tessellation https://www.microsoft.com/en-us/research/wp-content/uploads/2009/10/paper-1.pdf 89 | * Continuous-Line-Based Halftoning with Pinwheel Tiles http://abdallagafar.com/abdalla/wp-content/uploads/2020/11/Continuous-Line-Based-Halftoning-with-Pinwheel-Tiles.pdf 90 | * Tone- and Feature-Aware Circular Scribble Art https://sci-hub.st/10.1111/cgf.12761 91 | * A comprehensive survey on non-photorealistic rendering and benchmark developments for image abstraction and stylization https://sci-hub.st/https://doi.org/10.1007/S42044-019-00034-1 92 | * Opt Art: Special Cases https://archive.bridgesmathart.org/2011/bridges2011-249.pdf 93 | * Edge-Constrained Tile Mosaics http://archive.bridgesmathart.org/2007/bridges2007-351.pdf 94 | * Modular line-based halftoning via recursive division https://sci-hub.st/10.1145/2630397.2630403 95 | * Hilbert halftone art: https://possiblywrong.wordpress.com/2017/12/26/hilbert-halftone-art/ 96 | * Abstracting images into continuous-line artistic styles https://sci-hub.st/https://doi.org/10.1007/s00371-013-0809-1 97 | * A Graph‐based Approach to Continuous Line Illustrations with Variable Levels of Detail https://www.u-aizu.ac.jp/~shigeo/pdf/pg2011c-preprint.pdf 98 | * Depth-aware coherent line drawings https://kops.uni-konstanz.de/bitstream/123456789/32742/1/Spicker_0-307233.pdf 99 | * Hamiltonian cycle art: Surface covering wire sculptures and duotone surfaces http://people.tamu.edu/~ergun/research/topology/papers/candg12.pdf 100 | * Generating Color Scribble Images using Multi‐layered Monochromatic Strokes Dithering http://cgv.cs.nthu.edu.tw/~seanyhl/project/ColorScribbleArt/MLMSD_eg19.pdf 101 | * LinesLab: A Flexible Low\u2010Cost Approach for the Generation of Physical Monochrome Art https://vis.uib.no/wp-content/papercite-data/pdfs/Stoppel-2019-LFL.pdf 102 | * State of the "Art": A Taxonomy of Artistic Stylization Techniques for Images and Video http://epubs.surrey.ac.uk/721461/1/Collomosse-TVCG-2012.pdf 103 | * XDoG: Advanced Image Stylization with eXtended Difference-of-Gaussians https://sci-hub.st/10.1145/2024676.2024700 104 | * Colored Pencil Filter with Custom Colors https://sci-hub.st/https://doi.org/10.1109/PCCGA.2004.1348364 105 | * PencilArt: A Chromatic Penciling Style Generation Framework https://sci-hub.st/https://doi.org/10.1111/cgf.13334 106 | * A Texture-Based Approach for Hatching Color Photographs https://sci-hub.st/https://doi.org/10.1007/978-3-642-17289-2_9 107 | * Perception-Motivated High-Quality Stylization https://curve.carleton.ca/system/files/etd/e1a12c4d-890e-4626-8f57-768f661f3121/etd_pdf/10e988374e6e4a31b58b17568881b47f/li-perceptionmotivatedhighqualitystylization.pdf 108 | * Abstract line drawings from photographs using flow-based filters https://sci-hub.st/https://doi.org/10.1016/j.cag.2012.02.011 109 | * LSD: a Line Segment Detector https://api.semanticscholar.org/CorpusID:5847178 110 | * The Multiscale Line Segment Detector https://hal-enpc.archives-ouvertes.fr/hal-01571615/file/workshop_mlsd.pdf 111 | * An extended flow-based difference-of-Gaussians method of line drawing for polyhedral image https://sci-hub.st/https://doi.org/10.1016/J.IJLEO.2014.05.031 112 | * Edge Drawing: A combined real-time edge and segment detector http://c-viz.eskisehir.edu.tr/pdfs/ED.pdf 113 | * A Parameterless Line Segment and Elliptical Arc Detector with Enhanced Ellipse Fitting https://sci-hub.st/https://doi.org/10.1007/978-3-642-33709-3_41 114 | * EDLines: A real-time line segment detector with a false detection control https://sci-hub.st/https://doi.org/10.1016/j.patrec.2011.06.001 115 | * EDCircles: A real-time circle detector with a false detection control https://sci-hub.st/https://doi.org/10.1016/j.patcog.2012.09.020 -------------------------------------------------------------------------------- /src/color/mod.rs: -------------------------------------------------------------------------------- 1 | use ndarray::prelude::*; 2 | use ndarray::Array3; 3 | use std::fmt; 4 | use std::fmt::Display; 5 | use std::num::ParseIntError; 6 | use std::ops::Index; 7 | use std::str::FromStr; 8 | 9 | use self::transform::{cielab_to_ciehcl, ciexyz_to_cielab, srgb_to_ciexyz, srgb_to_hsl}; 10 | use crate::ColorModel; 11 | 12 | /// Approximate an image with a fixed palette. 13 | mod approx; 14 | 15 | /// Color space transformations. 16 | mod transform; 17 | 18 | impl ColorModel { 19 | pub fn convert(&self, rgb: ArrayView3) -> Array3 { 20 | match self { 21 | ColorModel::Cielab => ciexyz_to_cielab(srgb_to_ciexyz(rgb).view()), 22 | ColorModel::Rgb => rgb.to_owned(), 23 | } 24 | } 25 | 26 | pub fn convert_single(&self, color: &Color) -> [f64; 3] { 27 | let rgb = a_to_nd(color.as_ref()); 28 | nd_to_a::<3>(match self { 29 | ColorModel::Cielab => ciexyz_to_cielab(srgb_to_ciexyz(rgb.view()).view()), 30 | ColorModel::Rgb => rgb, 31 | }) 32 | } 33 | 34 | pub fn cylindrical(&self, image: ArrayView3) -> Array3 { 35 | match self { 36 | ColorModel::Cielab => cielab_to_ciehcl(image), 37 | ColorModel::Rgb => srgb_to_hsl(image), 38 | } 39 | } 40 | 41 | pub fn cylindrical_single(&self, color: [f64; 3]) -> [f64; 3] { 42 | let color = a_to_nd(&color); 43 | nd_to_a(match self { 44 | ColorModel::Cielab => cielab_to_ciehcl(color.view()), 45 | ColorModel::Rgb => srgb_to_hsl(color.view()), 46 | }) 47 | } 48 | 49 | /// CIEDE2000 Perceptual color distance metric. 50 | /// 51 | /// CIEHCL: based on CIEDE2000 52 | /// 53 | /// 54 | pub fn cylindrical_diff(&self, reference: [f64; 3], actual: [f64; 3]) -> f64 { 55 | match self { 56 | ColorModel::Cielab => { 57 | fn g(cmid: f64) -> f64 { 58 | 0.5 * (1. - (cmid.powi(7) / (cmid.powi(7) + 25.0_f64.powi(7))).sqrt()) 59 | } 60 | let [h1, c1, l1] = reference; 61 | let (b1, a1) = { 62 | let (a, b) = h1.sin_cos(); 63 | (a * c1, b * c1) 64 | }; 65 | let [h2, c2, l2] = actual; 66 | let (b2, a2) = { 67 | let (a, b) = h2.sin_cos(); 68 | (a * c2, b * c2) 69 | }; 70 | 71 | let δ_lprime = l2 - l1; 72 | let lmid = (l1 + l2) / 2.; 73 | let cmid = (c1 + c2) / 2.; 74 | 75 | let a1_prime = a1 * (1. + g(cmid)); 76 | let a2_prime = a2 * (1. + g(cmid)); 77 | let c1_prime = (a1_prime.powi(2) + b1.powi(2)).sqrt(); 78 | let c2_prime = (a2_prime.powi(2) + b2.powi(2)).sqrt(); 79 | let cmid_prime = (c1_prime + c2_prime) / 2.; 80 | let δ_cprime = c2_prime - c1_prime; 81 | 82 | let h1_prime = (b1.atan2(a1_prime).to_degrees() + 360.) % 360.; 83 | let h2_prime = (b2.atan2(a2_prime).to_degrees() + 360.) % 360.; 84 | let raw_δ_hprime = (h1_prime - h2_prime).abs(); 85 | let δ_hprime_precursor = if raw_δ_hprime <= 180. { 86 | h2_prime - h1_prime 87 | } else if h2_prime <= h1_prime { 88 | h2_prime - h1_prime + 360. 89 | } else { 90 | h2_prime - h1_prime - 360. 91 | }; 92 | let δ_hprime = 2.0 93 | * (c1_prime * c2_prime).sqrt() 94 | * (δ_hprime_precursor / 2.).to_radians().sin(); 95 | let hmid_prime = if raw_δ_hprime <= 180. { 96 | (h1_prime + h2_prime) / 2. 97 | } else if h1_prime + h2_prime < 360. { 98 | (h1_prime + h2_prime + 360.) / 2. 99 | } else { 100 | (h1_prime + h2_prime - 360.) / 2. 101 | }; 102 | 103 | let t = 1. - 0.17 * (hmid_prime - 30.).to_radians().cos() 104 | + 0.24 * (2. * hmid_prime).to_radians().cos() 105 | + 0.32 * (3. * hmid_prime + 6.).to_radians().cos() 106 | - 0.20 * (4. * hmid_prime - 63.).to_radians().cos(); 107 | let sl = 1. + (0.015 * (lmid - 50.).powi(2)) / (20. + (lmid - 50.).powi(2)).sqrt(); 108 | let sc = 1. + 0.045 * cmid_prime; 109 | let sh = 1. + 0.015 * cmid_prime * t; 110 | 111 | let dtheta = 30. * (-((hmid_prime - 275.) / 25.).powi(2)).exp(); 112 | let rc = 2. * (cmid_prime.powi(7) / (cmid_prime.powi(7) + 25.0_f64.powi(7))).sqrt(); 113 | let rt = -(2.0 * dtheta).to_radians().sin() * rc; 114 | 115 | let kl = 1.; 116 | let kc = 1.; 117 | let kh = 1.; 118 | 119 | ((δ_lprime / (kl * sl)).powi(2) 120 | + (δ_cprime / (kc * sc)).powi(2) 121 | + (δ_hprime / (kh * sh)).powi(2) 122 | + rt * δ_cprime / (kc * sc) * δ_hprime / (kh * sh)) 123 | .sqrt() 124 | } 125 | ColorModel::Rgb => todo!(), 126 | } 127 | } 128 | } 129 | 130 | #[derive(Clone, Copy, Debug)] 131 | pub struct Color([f64; 3]); 132 | 133 | impl Index for Color { 134 | type Output = f64; 135 | fn index(&'_ self, i: usize) -> &'_ Self::Output { 136 | &self.0[i] 137 | } 138 | } 139 | 140 | impl From<[f64; 3]> for Color { 141 | fn from(color: [f64; 3]) -> Self { 142 | Self(color) 143 | } 144 | } 145 | 146 | impl AsRef<[f64; 3]> for Color { 147 | fn as_ref(&self) -> &[f64; 3] { 148 | &self.0 149 | } 150 | } 151 | 152 | impl From for [u8; 3] { 153 | fn from(c: Color) -> Self { 154 | [ 155 | (c[0] * 255.).round() as u8, 156 | (c[1] * 255.).round() as u8, 157 | (c[2] * 255.).round() as u8, 158 | ] 159 | } 160 | } 161 | 162 | impl Display for Color { 163 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 164 | write!( 165 | f, 166 | "#{:02x}{:02x}{:02x}", 167 | (self[0] * 255.).round() as u8, 168 | (self[1] * 255.).round() as u8, 169 | (self[2] * 255.).round() as u8 170 | ) 171 | } 172 | } 173 | 174 | pub enum ColorParseError { 175 | Int(ParseIntError), 176 | Length(usize), 177 | MissingPound, 178 | } 179 | 180 | impl fmt::Display for ColorParseError { 181 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 182 | match self { 183 | Self::Int(e) => write!(f, "{}", e), 184 | Self::Length(l) => write!(f, "Unexpected length {} should be 3 or 6", l), 185 | Self::MissingPound => write!(f, "Color should be preceded by a pound symbol"), 186 | } 187 | } 188 | } 189 | 190 | impl From for ColorParseError { 191 | fn from(e: ParseIntError) -> Self { 192 | Self::Int(e) 193 | } 194 | } 195 | 196 | impl FromStr for Color { 197 | type Err = ColorParseError; 198 | 199 | fn from_str(input: &str) -> Result { 200 | if let Some(hex) = input.strip_prefix('#') { 201 | let parsed = u32::from_str_radix(hex, 16)?; 202 | let mut res = [0.; 3]; 203 | match hex.len() { 204 | 3 => { 205 | for (i, res_i) in res.iter_mut().enumerate() { 206 | // Hex shorthand: convert 0xFFF into 1.0, 1.0, 1.0 207 | let digit = (parsed >> (8 - 4 * i) & 0xF) as u8; 208 | *res_i = (digit << 4 | digit) as f64 / 255.; 209 | } 210 | } 211 | 6 => { 212 | for (i, res_i) in res.iter_mut().enumerate() { 213 | *res_i = ((parsed >> (16 - 8 * i) & 0xFF) as u8) as f64 / 255.; 214 | } 215 | } 216 | other => return Err(ColorParseError::Length(other)), 217 | } 218 | Ok(Self(res)) 219 | } else { 220 | Err(ColorParseError::MissingPound) 221 | } 222 | } 223 | } 224 | 225 | /// Convert a 1D Rust array into its (N, 1, 1) [ndarray] equivalent. 226 | fn a_to_nd(x: &[f64; N]) -> Array3 { 227 | Array3::::from_shape_vec((N, 1, 1), x.as_ref().to_vec()).unwrap() 228 | } 229 | 230 | /// Convert a (N, 1, 1) [ndarray] array into its Rust equivalent. 231 | fn nd_to_a(a: Array3) -> [f64; N] { 232 | let view = a.slice(s![.., 0, 0]); 233 | let mut res = [0.; N]; 234 | for i in 0..N { 235 | res[i] = view[i]; 236 | } 237 | res 238 | } 239 | -------------------------------------------------------------------------------- /src/optimize/adc_direct.rs: -------------------------------------------------------------------------------- 1 | //! Adaptive Diagonal Curves DIRECT algorithm 2 | //! 3 | //! This approach differs from DIRECT in a few key areas: 4 | //! 5 | //! - Two extrema of each rectangle is sampled, rather than the center 6 | //! - Extremely important when optima lie at boundaries 7 | //! - Only one largest dimension is chosen when splitting a rectangle 8 | //! - Potentially optimal rectangles are selected using a convex hull approach 9 | //! 10 | //! Differences from the ADC DIRECT paper: 11 | //! 12 | //! - L1 norm for rectangle size instead of L2 13 | //! - Track repeated function evaluations by using rational (fraction) coordinates 14 | //! - When splitting one of several largest dimensions, pick the one that has been split least often 15 | //! 16 | //! 17 | //! 18 | 19 | use std::collections::BinaryHeap; 20 | 21 | use ndarray::{azip, Array1, ArrayView1}; 22 | use num_rational::Rational64; 23 | use num_traits::{One, Signed, ToPrimitive, Zero}; 24 | use rustc_hash::FxHashMap as HashMap; 25 | 26 | use crate::voronoi::hull::lower_convex_hull; 27 | 28 | pub struct AdcDirect 29 | where 30 | F: Fn(ArrayView1) -> f64, 31 | { 32 | pub function: F, 33 | pub bounds: Array1<[f64; 2]>, 34 | pub max_evaluations: Option, 35 | pub max_iterations: Option, 36 | } 37 | 38 | #[derive(Debug)] 39 | struct AdcDirectState { 40 | iterations: usize, 41 | evaluations: HashMap, f64>, 42 | rectangles_by_size: Vec, 43 | dimension_split_counters: Vec, 44 | xmin: Array1, 45 | fmin: f64, 46 | } 47 | 48 | /// Hyper-rectangle as defined by the DIRECT algorithm. 49 | #[derive(Debug, PartialEq)] 50 | struct Rectangle { 51 | /// Lower bound 52 | a: Array1, 53 | /// Upper bound 54 | b: Array1, 55 | /// Where this rectangle lies on the Y-axis of the graph built during [`AdcDirect::split`] 56 | f_graph_pos: f64, 57 | } 58 | 59 | impl Rectangle { 60 | fn new(a: Array1, b: Array1, f_a: f64, f_b: f64) -> Self { 61 | Self { 62 | a, 63 | b, 64 | f_graph_pos: (f_a + f_b) / 2., 65 | } 66 | } 67 | } 68 | 69 | impl Eq for Rectangle {} 70 | 71 | impl PartialOrd for Rectangle { 72 | fn partial_cmp(&self, other: &Self) -> Option { 73 | Some(self.cmp(other)) 74 | } 75 | } 76 | 77 | /// For ergonomics, the order is reversed here instead of with a [std::cmp::Reverse] wrapper. 78 | impl Ord for Rectangle { 79 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 80 | self.f_graph_pos 81 | .partial_cmp(&other.f_graph_pos) 82 | .unwrap() 83 | .reverse() 84 | } 85 | } 86 | 87 | /// A set of [Rectangles](Rectangle) with the same size, or area. 88 | #[derive(Debug)] 89 | struct Group { 90 | size: Rational64, 91 | rectangles: BinaryHeap, 92 | } 93 | 94 | impl Group { 95 | fn min_f_graph_pos(&self) -> f64 { 96 | self.rectangles.peek().expect("non empty").f_graph_pos 97 | } 98 | } 99 | 100 | impl AdcDirect 101 | where 102 | F: Fn(ArrayView1) -> f64, 103 | { 104 | pub fn run(&self) -> (Array1, f64) { 105 | let mut state = AdcDirectState { 106 | iterations: 0, 107 | evaluations: HashMap::default(), 108 | rectangles_by_size: vec![], 109 | dimension_split_counters: vec![0; self.bounds.len()], 110 | xmin: Array1::zeros(self.bounds.len()), 111 | fmin: 0., 112 | }; 113 | self.initialize(&mut state); 114 | 115 | loop { 116 | if let Some(max_evaluations) = self.max_evaluations { 117 | if state.evaluations.len() >= max_evaluations { 118 | break; 119 | } 120 | } 121 | if let Some(max_iterations) = self.max_iterations { 122 | if state.iterations >= max_iterations { 123 | break; 124 | } 125 | } 126 | 127 | let potentially_optimal = self.extract_potentially_optimal(&mut state); 128 | if potentially_optimal.is_empty() { 129 | break; 130 | } 131 | for rectangle in potentially_optimal { 132 | self.split(rectangle, &mut state); 133 | } 134 | state.iterations += 1; 135 | } 136 | (self.denormalize_point(state.xmin.view()), state.fmin) 137 | } 138 | 139 | /// Initialize data structures following Section 3.2 140 | fn initialize(&self, state: &mut AdcDirectState) { 141 | let dimensions = self.bounds.len(); 142 | 143 | let a = Array1::from_elem(dimensions, Rational64::zero()); 144 | let b = Array1::from_elem(dimensions, Rational64::one()); 145 | let f_a = (self.function)(self.denormalize_point(a.view()).view()); 146 | let f_b = (self.function)(self.denormalize_point(b.view()).view()); 147 | state 148 | .evaluations 149 | .extend([(a.clone(), f_a), (b.clone(), f_b)]); 150 | state.xmin = if f_a < f_b { a.clone() } else { b.clone() }; 151 | state.fmin = f_a.min(f_b); 152 | 153 | let rectangle = Rectangle::new(a, b, f_a, f_b); 154 | self.split(rectangle, state); 155 | } 156 | 157 | /// Identify and extract potentially optimal rectangles. 158 | fn extract_potentially_optimal( 159 | &self, 160 | AdcDirectState { 161 | rectangles_by_size, 162 | fmin, 163 | .. 164 | }: &mut AdcDirectState, 165 | ) -> Vec { 166 | let points = [[0., *fmin]] 167 | .into_iter() 168 | .chain( 169 | rectangles_by_size 170 | .iter() 171 | .map(|g| [g.size.to_f64().unwrap(), g.min_f_graph_pos()]), 172 | ) 173 | .collect::>(); 174 | 175 | let lower_hull_optimal = lower_convex_hull(&points); 176 | 177 | let mut group_it = rectangles_by_size.iter_mut(); 178 | let mut potentially_optimal = vec![]; 179 | // Ignore first point 180 | for [group_size, group_min_f_graph_pos] in lower_hull_optimal.iter().skip(1) { 181 | // Find group and extract potentially optimal 182 | let group = loop { 183 | let Some(group) = group_it.next() else { 184 | unreachable!("there should always be a matching group in the hull"); 185 | }; 186 | if *group_size == group.size.to_f64().unwrap() { 187 | break group; 188 | } 189 | }; 190 | while !group.rectangles.is_empty() && group.min_f_graph_pos() == *group_min_f_graph_pos 191 | { 192 | potentially_optimal.push(group.rectangles.pop().unwrap()); 193 | } 194 | } 195 | rectangles_by_size.retain(|g| !g.rectangles.is_empty()); 196 | 197 | potentially_optimal 198 | } 199 | 200 | /// Split the given [Rectangle]. 201 | fn split( 202 | &self, 203 | Rectangle { a, b, .. }: Rectangle, 204 | AdcDirectState { 205 | evaluations, 206 | rectangles_by_size, 207 | dimension_split_counters, 208 | xmin, 209 | fmin, 210 | .. 211 | }: &mut AdcDirectState, 212 | ) { 213 | // Pick a single longest bound, using split counts for tie-breaking. 214 | let (largest_dimension, _) = b 215 | .iter() 216 | .zip(a.iter()) 217 | .map(|(b_i, a_i)| (b_i - a_i).abs()) 218 | .enumerate() 219 | .max_by(|(i, x), (j, y)| { 220 | x.cmp(y).then( 221 | dimension_split_counters[*i] 222 | .cmp(&dimension_split_counters[*j]) 223 | .reverse(), 224 | ) 225 | }) 226 | .unwrap(); 227 | 228 | // Update split counters for tie-breaking. 229 | dimension_split_counters[largest_dimension] += 1; 230 | 231 | const SPLIT_RATIO: Rational64 = Rational64::new_raw(2, 3); 232 | let mut u = a.clone(); 233 | u[largest_dimension] += (b[largest_dimension] - a[largest_dimension]) * SPLIT_RATIO; 234 | 235 | let mut v = b.clone(); 236 | v[largest_dimension] += (a[largest_dimension] - b[largest_dimension]) * SPLIT_RATIO; 237 | 238 | let f_a = *evaluations.get(&a).unwrap(); 239 | let f_b = *evaluations.get(&b).unwrap(); 240 | let f_u = *evaluations 241 | .entry(u.clone()) 242 | .or_insert_with(|| (self.function)(self.denormalize_point(u.view()).view())); 243 | let f_v = *evaluations 244 | .entry(v.clone()) 245 | .or_insert_with(|| (self.function)(self.denormalize_point(v.view()).view())); 246 | 247 | if f_u < *fmin { 248 | *xmin = u.clone(); 249 | *fmin = f_u; 250 | } 251 | if f_v < *fmin { 252 | *xmin = v.clone(); 253 | *fmin = f_v; 254 | } 255 | 256 | // This is actually the L1 norm which speeds up convergence significantly. 257 | // I suspect this is because the rectangles are more grouped up, forcing more local search. 258 | let size = b 259 | .iter() 260 | .zip(a.iter()) 261 | .map(|(b_i, a_i)| (b_i - a_i).abs()) 262 | .sum::(); 263 | let new_rectangles = [ 264 | Rectangle::new(u.clone(), v.clone(), f_u, f_v), 265 | Rectangle::new(a, v, f_a, f_v), 266 | Rectangle::new(u, b, f_u, f_b), 267 | ]; 268 | 269 | match rectangles_by_size.binary_search_by(|g| g.size.cmp(&size)) { 270 | Ok(i) => rectangles_by_size[i].rectangles.extend(new_rectangles), 271 | Err(i) => { 272 | rectangles_by_size.insert( 273 | i, 274 | Group { 275 | size, 276 | rectangles: BinaryHeap::from(new_rectangles), 277 | }, 278 | ) 279 | } 280 | } 281 | } 282 | 283 | /// Convert a point from the hypercube range back into user range. 284 | fn denormalize_point(&self, hypercube_point: ArrayView1) -> Array1 { 285 | let mut denormalized = hypercube_point.mapv(|x| x.to_f64().unwrap()); 286 | azip!( 287 | (x in &mut denormalized, bound in &self.bounds) *x = *x * (bound[1] - bound[0]) + bound[0] 288 | ); 289 | denormalized 290 | } 291 | } 292 | 293 | #[cfg(test)] 294 | mod test { 295 | use lyon_geom::euclid::default::Vector3D; 296 | use ndarray::Array; 297 | 298 | use super::AdcDirect; 299 | use crate::ColorModel; 300 | 301 | #[test] 302 | fn test_direct() { 303 | let direct = AdcDirect { 304 | function: |val| val[0].powi(2) + val[1].powi(2), 305 | bounds: Array::from_elem(2, [-10., 10.]), 306 | max_evaluations: Some(10_000), 307 | max_iterations: None, 308 | }; 309 | assert_eq!(direct.run().1, 0.); 310 | } 311 | 312 | #[test] 313 | fn test_direct_real() { 314 | // abL 315 | let implements: Vec> = vec![ 316 | Vector3D::from((-12.33001605954215, -45.54515542156117, 44.2098529479848)), 317 | Vector3D::from((27.880276413952384, -45.45097702564241, 79.59139231597462)), 318 | Vector3D::from((0.0, 0.0, 100.0)), 319 | Vector3D::from((25.872063973881424, -58.18583421858581, 77.27752311788944)), 320 | Vector3D::from((-26.443894809510233, 42.307075530106964, 52.73969688418173)), 321 | Vector3D::from((66.72215124694603, 8.94553594498204, 50.895965946274)), 322 | Vector3D::from((86.860821880907, -69.34347889122935, 46.303948069777704)), 323 | Vector3D::from((21.03625782707445, -63.798798964168235, 67.01735205659284)), 324 | Vector3D::from((-35.98529688090144, 11.606079999533165, 51.30132332650257)), 325 | Vector3D::from((62.596792812655295, 33.336563699816914, 55.46042775958594)), 326 | ]; 327 | let model = ColorModel::Cielab; 328 | // hue, chroma, darkness 329 | let desired = [1.4826900028611403, 5.177699004088122, 0.27727267822882595]; 330 | let direct = AdcDirect { 331 | function: model.objective_function(desired, &implements), 332 | bounds: Array::from_elem(implements.len(), [0., 1.]), 333 | max_evaluations: Some(10_000), 334 | max_iterations: None, 335 | }; 336 | let (res, cost) = direct.run(); 337 | let weighted_vector = implements 338 | .iter() 339 | .zip(res.iter()) 340 | .map(|(p, x)| *p * *x) 341 | .sum::>(); 342 | // Convert back to cylindrical model (hue, chroma, darkness) 343 | let actual = [ 344 | weighted_vector.y.atan2(weighted_vector.x), 345 | weighted_vector.to_2d().length(), 346 | weighted_vector.z, 347 | ]; 348 | dbg!(cost, &res, &actual, model.cylindrical_diff(desired, actual)); 349 | assert!(cost <= 4.0, "ciede2000 less than 4"); 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use cairo::{Context, Matrix, SvgUnit}; 2 | use clap::{Parser, ValueEnum}; 3 | use dither::{Dither, FloydSteinberg}; 4 | use image::ImageReader; 5 | #[cfg(debug_assertions)] 6 | use image::{Rgb, RgbImage}; 7 | use ndarray::{prelude::*, SliceInfo, SliceInfoElem}; 8 | use serde::{Deserialize, Serialize}; 9 | use std::{ 10 | env, 11 | fmt::Debug, 12 | fs::File, 13 | io::{self, Read}, 14 | path::PathBuf, 15 | str::FromStr, 16 | vec, 17 | }; 18 | use tracing::{info, level_filters::LevelFilter, warn}; 19 | use uom::si::f64::Length; 20 | use uom::si::length::{inch, millimeter}; 21 | 22 | use crate::color::Color; 23 | use crate::render::render_stipple_based; 24 | 25 | /// Adjust image color 26 | mod color; 27 | /// Dither an image given a predefined set of colors 28 | mod dither; 29 | /// Image filter algorithms (i.e. Sobel operator, FDoG, ETF) 30 | mod filter; 31 | /// Graph algorithms 32 | mod graph; 33 | /// Line segment drawing and related algorithms 34 | mod lsd; 35 | /// Pure math routines 36 | mod math; 37 | /// Constrained global optimization algorithms 38 | mod optimize; 39 | /// Routines for creating the final SVG using [Cairo](cairographics.org) 40 | mod render; 41 | /// Construct the [Voronoi diagram](https://en.wikipedia.org/wiki/Voronoi_diagram) and calculate related properties 42 | mod voronoi; 43 | 44 | #[derive(Parser, Debug, Deserialize, Serialize)] 45 | #[command(author, about)] 46 | struct Opt { 47 | /// A path to an image, else reads from stdin 48 | file: Option, 49 | 50 | #[arg(long)] 51 | config: Option, 52 | 53 | /// Determines the scaling of the output SVG 54 | #[arg(long, alias = "dpi", default_value = "96.")] 55 | dots_per_inch: f64, 56 | 57 | /// Color model to use for additive coloring 58 | #[arg(long, default_value = "cielab", ignore_case = true)] 59 | #[serde(with = "serde_with::rust::display_fromstr")] 60 | color_model: ColorModel, 61 | 62 | /// Coloring method to use 63 | #[arg(long, default_value = "vector", ignore_case = true)] 64 | #[serde(with = "serde_with::rust::display_fromstr")] 65 | color_method: ColorMethod, 66 | 67 | /// SVG drawing style 68 | #[arg(long, default_value = "mst", ignore_case = true)] 69 | #[serde(with = "serde_with::rust::display_fromstr")] 70 | style: Style, 71 | 72 | /// The drawing implement(s) used by your plotter 73 | #[arg(long = "implement", ignore_case = true)] 74 | implements: Vec, 75 | 76 | /// Super-sampling factor for finer detail control 77 | #[arg(long, default_value = "1")] 78 | super_sample: usize, 79 | 80 | /// Output file path (overwrites old files), else writes to stdout 81 | #[arg(short, long)] 82 | out: Option, 83 | } 84 | 85 | macro_rules! opt { 86 | ($name: ident { 87 | $( 88 | $variant: ident $({ 89 | $($member: ident; $ty: ty,)* 90 | })?, 91 | )* 92 | }) => { 93 | #[derive(ValueEnum, Debug, Clone, Copy)] 94 | pub enum $name { 95 | $( 96 | $variant $({ 97 | $($member: $ty,)* 98 | })?, 99 | )* 100 | } 101 | 102 | paste::paste! { 103 | // impl $name { 104 | // const NUM_VARIANTS: usize = 0 $( 105 | // + { let _ = stringify!(Self::$variant); 1 } 106 | // )*; 107 | 108 | // fn raw_variants() -> [&'static str; Self::NUM_VARIANTS] { 109 | // [ 110 | // $( 111 | // stringify!([<$variant:snake>]), 112 | // )* 113 | // ] 114 | // } 115 | // } 116 | 117 | impl std::fmt::Display for $name { 118 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 119 | match self { 120 | $( 121 | Self::$variant => f.write_str(stringify!([<$variant:snake>])), 122 | )* 123 | } 124 | } 125 | } 126 | 127 | impl FromStr for $name { 128 | type Err = &'static str; 129 | 130 | fn from_str(data: &str) -> Result { 131 | $( 132 | if stringify!([<$variant:snake>]) == data { 133 | return Ok(Self::$variant); 134 | } 135 | )* 136 | 137 | return Err("Must be one of variants"); 138 | } 139 | } 140 | } 141 | }; 142 | } 143 | 144 | opt! { 145 | ColorModel { 146 | Cielab, 147 | Rgb, 148 | } 149 | } 150 | 151 | opt! { 152 | ColorMethod { 153 | Dither, 154 | Vector, 155 | } 156 | } 157 | 158 | opt! { 159 | Style { 160 | Stipple, 161 | EdgesPlusHatching, 162 | Tsp, 163 | Mst, 164 | Triangulation, 165 | Voronoi, 166 | } 167 | } 168 | 169 | #[derive(Clone, Debug, Deserialize, Serialize)] 170 | pub enum Implement { 171 | Pen { 172 | diameter: Length, 173 | #[serde(with = "serde_with::rust::display_fromstr")] 174 | color: Color, 175 | }, 176 | Pencil, 177 | Marker, 178 | } 179 | 180 | impl FromStr for Implement { 181 | type Err = serde_json::Error; 182 | 183 | fn from_str(s: &str) -> Result { 184 | serde_json::from_str(s) 185 | } 186 | } 187 | 188 | fn main() -> io::Result<()> { 189 | if env::var("RUST_LOG").is_err() { 190 | env::set_var("RUST_LOG", "raster2svg=info") 191 | } 192 | tracing::subscriber::set_global_default( 193 | tracing_subscriber::fmt() 194 | .with_max_level(LevelFilter::DEBUG) 195 | .compact() 196 | .finish(), 197 | ) 198 | .unwrap(); 199 | 200 | let ref opt @ Opt { 201 | ref file, 202 | config: _, 203 | dots_per_inch: _, 204 | ref color_model, 205 | ref color_method, 206 | ref style, 207 | ref implements, 208 | super_sample: _, 209 | out: _, 210 | } = { 211 | let mut opt = Opt::parse(); 212 | 213 | if let Some(config) = opt.config { 214 | let mut config = serde_json::from_reader::<_, Opt>(File::open(&config)?)?; 215 | config.file = opt.file.or(config.file); 216 | config.out = opt.out.or(config.out); 217 | config.implements.append(&mut opt.implements); 218 | config 219 | } else { 220 | opt 221 | } 222 | }; 223 | 224 | let image = match file { 225 | Some(ref filepath) => ImageReader::open(filepath)?.decode(), 226 | None => { 227 | info!("Reading from stdin"); 228 | let mut bytes = vec![]; 229 | io::stdin().read_to_end(&mut bytes)?; 230 | let cursor = io::Cursor::new(bytes); 231 | ImageReader::new(cursor).decode() 232 | } 233 | } 234 | .expect("not an image") 235 | .to_rgb16(); 236 | 237 | let image = Array::from_iter( 238 | image 239 | .pixels() 240 | .flat_map(|p| p.0) 241 | .map(|p| p as f64 / u16::MAX as f64), 242 | ) 243 | .into_shape_clone((image.height() as usize, image.width() as usize, 3)) 244 | .unwrap() 245 | .reversed_axes(); 246 | 247 | let palette = implements 248 | .iter() 249 | .map(|implement| { 250 | if let Implement::Pen { color, .. } = implement { 251 | *color 252 | } else { 253 | unimplemented!() 254 | } 255 | }) 256 | .collect::>(); 257 | 258 | let mut image_in_implements: Array3; 259 | match color_method { 260 | ColorMethod::Dither => { 261 | let (_, width, height) = image.dim(); 262 | let image_in_color_model = color_model.convert(image.view()); 263 | let implements_in_color_model = palette 264 | .iter() 265 | .map(|c| color_model.convert_single(c)) 266 | .collect::>(); 267 | image_in_implements = Array3::::zeros((palette.len(), width, height)); 268 | if !matches!(color_model, ColorModel::Rgb) { 269 | warn!("Non-rgb color model + dither doesn't work well"); 270 | } 271 | // Add white for background 272 | let colors_float = implements_in_color_model 273 | .into_iter() 274 | .chain(std::iter::once( 275 | color_model.convert_single(&Color::from([1.; 3])), 276 | )) 277 | .collect::>(); 278 | let dithered = FloydSteinberg.dither(image_in_color_model.view(), &colors_float); 279 | #[cfg(debug_assertions)] 280 | let mut buf = RgbImage::new(width as u32, height as u32); 281 | for y in 0..height { 282 | for x in 0..width { 283 | let k = dithered[[x, y]]; 284 | if k == implements.len() { 285 | #[cfg(debug_assertions)] 286 | buf.put_pixel(x as u32, y as u32, Rgb([255; 3])); 287 | continue; 288 | } 289 | #[cfg(debug_assertions)] 290 | buf.put_pixel(x as u32, y as u32, Rgb((palette[k]).into())); 291 | image_in_implements[[k, x, y]] = 1.0; 292 | } 293 | } 294 | 295 | #[cfg(debug_assertions)] 296 | buf.save("x.png").unwrap(); 297 | } 298 | ColorMethod::Vector => { 299 | image_in_implements = color_model.approximate(image.view(), &palette); 300 | } 301 | } 302 | 303 | // Linearize color mapping for line drawings 304 | if matches!(style, Style::Tsp | Style::Mst) { 305 | image_in_implements.mapv_inplace(|v| v.powi(2)); 306 | } else if matches!(style, Style::Triangulation | Style::Voronoi) { 307 | image_in_implements.mapv_inplace(|v| v.powi(3)); 308 | } 309 | 310 | draw(image_in_implements.view(), &opt); 311 | Ok(()) 312 | } 313 | 314 | fn draw(image_in_implements: ArrayView3, opt: &Opt) { 315 | let mm_per_inch = Length::new::(1.).get::(); 316 | let dots_per_mm = opt.dots_per_inch / mm_per_inch; 317 | 318 | let width = 319 | (image_in_implements.raw_dim()[1] as f64 / dots_per_mm / opt.super_sample as f64).round(); 320 | let height = 321 | (image_in_implements.raw_dim()[2] as f64 / dots_per_mm / opt.super_sample as f64).round(); 322 | 323 | let mut surf = match &opt.out { 324 | Some(_) => cairo::SvgSurface::new(width, height, opt.out.as_ref()).unwrap(), 325 | None => cairo::SvgSurface::for_stream(width, height, std::io::stdout()).unwrap(), 326 | }; 327 | surf.set_document_unit(SvgUnit::Mm); 328 | let ctx = Context::new(&surf).unwrap(); 329 | 330 | ctx.set_source_rgb(1., 1., 1.); 331 | ctx.rectangle(0., 0., width, height); 332 | ctx.fill().unwrap(); 333 | 334 | match opt.style { 335 | Style::Stipple | Style::Tsp | Style::Mst | Style::Triangulation | Style::Voronoi => { 336 | render_stipple_based( 337 | image_in_implements.view(), 338 | &opt.implements 339 | .iter() 340 | .map(|implement| { 341 | if let Implement::Pen { diameter, .. } = implement { 342 | diameter.get::() * dots_per_mm 343 | } else { 344 | todo!() 345 | } 346 | }) 347 | .collect::>(), 348 | &opt.implements 349 | .iter() 350 | .map(|implement| { 351 | if let Implement::Pen { color, .. } = implement { 352 | *color 353 | } else { 354 | todo!() 355 | } 356 | }) 357 | .collect::>(), 358 | opt.super_sample, 359 | opt.style, 360 | &ctx, 361 | { 362 | let mut mat = Matrix::identity(); 363 | mat.scale( 364 | 1.0 / dots_per_mm / opt.super_sample as f64, 365 | 1.0 / dots_per_mm / opt.super_sample as f64, 366 | ); 367 | mat 368 | }, 369 | ) 370 | } 371 | Style::EdgesPlusHatching => { 372 | todo!() 373 | // render_fdog_based( 374 | // image_in_implements.slice(s![k, .., ..]), 375 | // opt.super_sample, 376 | // implement_diameter_in_pixels, 377 | // opt.style, 378 | // &ctx, 379 | // { 380 | // let mut mat = Matrix::identity(); 381 | // mat.scale( 382 | // 1.0 / dots_per_mm / opt.super_sample as f64, 383 | // 1.0 / dots_per_mm / opt.super_sample as f64, 384 | // ); 385 | // mat 386 | // }, 387 | // ) 388 | } 389 | } 390 | } 391 | 392 | /// Utility function for applying windowed offset functions like convolution on a 2D ndarray array 393 | #[inline] 394 | pub(crate) fn get_slice_info_for_offset( 395 | x: i32, 396 | y: i32, 397 | ) -> SliceInfo<[SliceInfoElem; 2], Dim<[usize; 2]>, Dim<[usize; 2]>> { 398 | match (x.signum(), y.signum()) { 399 | (-1, -1) => s![..x, ..y], 400 | (0, -1) => s![.., ..y], 401 | (-1, 0) => s![..x, ..], 402 | (0, 0) => s![.., ..], 403 | (1, 0) => s![x.., ..], 404 | (0, 1) => s![.., y..], 405 | (-1, 1) => s![..x, y..], 406 | (1, -1) => s![x.., ..y], 407 | (1, 1) => s![x.., y..], 408 | _ => unreachable!(), 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /src/color/transform.rs: -------------------------------------------------------------------------------- 1 | use ndarray::prelude::*; 2 | use ndarray::Array3; 3 | 4 | /// sRGB to Hue, Saturation, Lightness (HSL) 5 | /// 6 | /// Hue is in radians, saturation in [0,1], lightness in [0,1]. 7 | /// 8 | /// 9 | pub fn srgb_to_hsl(srgb: ArrayView3) -> Array3 { 10 | let mut hsl = srgb.to_owned(); 11 | hsl.slice_mut(s![0, .., ..]) 12 | .assign(&srgb.map_axis(Axis(0), |rgb| { 13 | let v = rgb[0].max(rgb[1]).max(rgb[2]); 14 | let c = v - rgb[0].min(rgb[1]).min(rgb[2]); 15 | if c <= f64::EPSILON { 16 | 0. 17 | } else if (v - rgb[0]).abs() <= f64::EPSILON { 18 | std::f64::consts::FRAC_PI_3 * (0. + (rgb[1] - rgb[2]) / c) 19 | } else if (v - rgb[1]).abs() <= f64::EPSILON { 20 | std::f64::consts::FRAC_PI_3 * (2. + (rgb[2] - rgb[0]) / c) 21 | } else { 22 | std::f64::consts::FRAC_PI_3 * (4. + (rgb[0] - rgb[1]) / c) 23 | } 24 | })); 25 | 26 | hsl.slice_mut(s![1, .., ..]) 27 | .assign(&srgb.map_axis(Axis(0), |rgb| { 28 | let v = rgb[0].max(rgb[1]).max(rgb[2]); 29 | let c = v - rgb[0].min(rgb[1]).min(rgb[2]); 30 | let l = v - c / 2.; 31 | 32 | if l.abs() <= f64::EPSILON || (l - 1.).abs() <= f64::EPSILON { 33 | 0. 34 | } else { 35 | (v - l) / (l.min(1. - l)) 36 | } 37 | })); 38 | 39 | hsl.slice_mut(s![2, .., ..]) 40 | .assign(&srgb.map_axis(Axis(0), |rgb| { 41 | let v = rgb[0].max(rgb[1]).max(rgb[2]); 42 | let c = v - rgb[0].min(rgb[1]).min(rgb[2]); 43 | v - c / 2. 44 | })); 45 | 46 | hsl 47 | } 48 | 49 | /// CAT02 D65 -> D50 Illuminant transform 50 | /// 51 | /// Derived using [colour-science](https://colour.readthedocs.io/en/develop/index.html): 52 | /// ```python 53 | /// from colour import CCS_ILLUMINANTS 54 | /// from colour.adaptation import matrix_chromatic_adaptation_VonKries 55 | /// from colour.models import xy_to_xyY, xyY_to_XYZ 56 | /// 57 | /// illuminant_RGB = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"] 58 | /// illuminant_XYZ = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D50"] 59 | /// chromatic_adaptation_transform = "CAT02" 60 | /// print( 61 | /// matrix_chromatic_adaptation_VonKries( 62 | /// xyY_to_XYZ(xy_to_xyY(illuminant_RGB)), 63 | /// xyY_to_XYZ(xy_to_xyY(illuminant_XYZ)), 64 | /// transform=chromatic_adaptation_transform, 65 | /// ) 66 | /// ) 67 | /// ``` 68 | const CHROMATIC_ADAPTATION_TRANSFORM: [[f64; 3]; 3] = [ 69 | [1.04257389, 0.03089108, -0.05281257], 70 | [0.02219345, 1.00185663, -0.02107375], 71 | [-0.00116488, -0.00342053, 0.76178908], 72 | ]; 73 | 74 | /// sRGB under D65 illuminant to CIEXYZ under D50 illuminant 75 | /// 76 | /// 77 | pub fn srgb_to_ciexyz(srgb: ArrayView3) -> Array3 { 78 | let mut ciexyz = Array3::::zeros(srgb.raw_dim()); 79 | 80 | const SRGB_TO_CIEXYZ: [[f64; 3]; 3] = [ 81 | [0.4124, 0.3576, 0.1805], 82 | [0.2126, 0.7152, 0.0722], 83 | [0.0193, 0.1192, 0.9505], 84 | ]; 85 | 86 | for i in 0..ciexyz.raw_dim()[1] { 87 | for j in 0..ciexyz.raw_dim()[2] { 88 | let mut ciexyz_under_d65 = [0.; 3]; 89 | for k in 0..3 { 90 | let gamma_expanded = gamma_expand_rgb(srgb[[k, i, j]]); 91 | for (l, ciexyz_under_d65_l) in ciexyz_under_d65.iter_mut().enumerate() { 92 | *ciexyz_under_d65_l += SRGB_TO_CIEXYZ[l][k] * gamma_expanded; 93 | } 94 | } 95 | for ciexyz_under_d65_l in ciexyz_under_d65.iter_mut() { 96 | *ciexyz_under_d65_l = ciexyz_under_d65_l.clamp(0., 1.); 97 | } 98 | 99 | for k in 0..3 { 100 | for (l, ciexyz_under_d65_l) in IntoIterator::into_iter(ciexyz_under_d65).enumerate() 101 | { 102 | ciexyz[[k, i, j]] += CHROMATIC_ADAPTATION_TRANSFORM[k][l] * ciexyz_under_d65_l; 103 | } 104 | ciexyz[[k, i, j]] = ciexyz[[k, i, j]].clamp(0., 1.); 105 | } 106 | } 107 | } 108 | 109 | ciexyz 110 | } 111 | 112 | /// CIEXYZ to CIELAB, both under D50 illuminant 113 | /// 114 | /// 115 | pub fn ciexyz_to_cielab(ciexyz: ArrayView3) -> Array3 { 116 | // Can't find my source for these, but one derivation is on https://www.mathworks.com/help/images/ref/whitepoint.html 117 | const X_N: f64 = 0.96429568; 118 | const Y_N: f64 = 1.; 119 | const Z_N: f64 = 0.8251046; 120 | let mut cielab = Array3::::zeros(ciexyz.raw_dim()); 121 | cielab 122 | .slice_mut(s![0, .., ..]) 123 | .assign(&ciexyz.map_axis(Axis(0), |xyz| { 124 | let y = xyz[1]; 125 | let l = 116. * cielab_f(y / Y_N) - 16.; 126 | l.clamp(0., 100.) 127 | })); 128 | 129 | cielab 130 | .slice_mut(s![1, .., ..]) 131 | .assign(&ciexyz.map_axis(Axis(0), |xyz| { 132 | let x = xyz[0]; 133 | let y = xyz[1]; 134 | 500. * (cielab_f(x / X_N) - cielab_f(y / Y_N)) 135 | })); 136 | 137 | cielab 138 | .slice_mut(s![2, .., ..]) 139 | .assign(&ciexyz.map_axis(Axis(0), |xyz| { 140 | let y = xyz[1]; 141 | let z = xyz[2]; 142 | 200. * (cielab_f(y / Y_N) - cielab_f(z / Z_N)) 143 | })); 144 | 145 | cielab 146 | } 147 | 148 | /// CIELAB to its cylindrical equivalent CIEHCL 149 | /// 150 | /// This is usually LCH or HLC, but it is made HCL here to 151 | /// align with HSL 152 | pub fn cielab_to_ciehcl(cielab: ArrayView3) -> Array3 { 153 | let mut ciehcl = Array3::::zeros(cielab.raw_dim()); 154 | 155 | // L remains the same 156 | ciehcl 157 | .slice_mut(s![2, .., ..]) 158 | .assign(&cielab.slice(s![0, .., ..])); 159 | 160 | // Euclidean distance from origin to define chromaticity 161 | ciehcl 162 | .slice_mut(s![1, .., ..]) 163 | .assign(&cielab.map_axis(Axis(0), |lab| (lab[1].powi(2) + lab[2].powi(2)).sqrt())); 164 | 165 | // Hue angle 166 | ciehcl 167 | .slice_mut(s![0, .., ..]) 168 | .assign(&cielab.map_axis(Axis(0), |lab| lab[2].atan2(lab[1]))); 169 | 170 | ciehcl 171 | } 172 | 173 | /// Function defined in CIEXYZ to CIELAB conversion 174 | /// 175 | /// 176 | fn cielab_f(t: f64) -> f64 { 177 | const DELTA_POW3: f64 = 216. / 24389.; 178 | const THREE_DELTA_POW2: f64 = 108. / 841.; 179 | const FOUR_OVER_TWENTY_NINE: f64 = 4. / 29.; 180 | 181 | if t > DELTA_POW3 { 182 | t.cbrt() 183 | } else { 184 | t / THREE_DELTA_POW2 + FOUR_OVER_TWENTY_NINE 185 | } 186 | } 187 | 188 | /// Gamma-expand (or linearize) an sRGB value 189 | /// 190 | /// 191 | fn gamma_expand_rgb(component: f64) -> f64 { 192 | if component <= 0.04045 { 193 | component / 12.92 194 | } else { 195 | ((component + 0.055) / 1.055).powf(2.4) 196 | } 197 | } 198 | 199 | #[cfg(test)] 200 | mod tests { 201 | use crate::ColorModel; 202 | 203 | use super::*; 204 | 205 | #[test] 206 | fn srgb_to_hsl_is_correct() { 207 | let arr = array![[ 208 | [1., 0., 0.], 209 | [0., 1., 0.], 210 | [0., 0., 1.], 211 | [1., 1., 1.], 212 | [0.89, 0.89, 0.89], 213 | [0., 0., 0.], 214 | ]] 215 | .reversed_axes(); 216 | assert_eq!( 217 | srgb_to_hsl(arr.view()).reversed_axes(), 218 | array![[ 219 | [0., 1., 0.5], 220 | [std::f64::consts::FRAC_PI_3 * 2., 1., 0.5], 221 | [std::f64::consts::FRAC_PI_3 * 4., 1., 0.5], 222 | [0., 0., 1.], 223 | [0., 0., 0.89], 224 | [0., 0., 0.], 225 | ]] 226 | ); 227 | } 228 | 229 | #[test] 230 | fn srgb_to_ciexyz_is_correct() { 231 | let arr = array![[ 232 | [1., 0., 0.], 233 | [0., 1., 0.], 234 | [0., 0., 1.], 235 | [1., 1., 1.], 236 | [0., 0., 0.] 237 | ]] 238 | .reversed_axes(); 239 | assert_eq!( 240 | srgb_to_ciexyz(arr.view()).reversed_axes(), 241 | array![[ 242 | [0.43550563324299996, 0.221740574943, 0.013494928054000002], 243 | [0.3886224651359999, 0.7219522484959999, 0.08794233419200001], 244 | [0.14021657533599996, 0.05630936703600001, 0.723623297434], 245 | [0.969044992445, 1.0, 0.75726133156], 246 | [0.0, 0.0, 0.0] 247 | ]] 248 | ); 249 | } 250 | 251 | #[test] 252 | fn ciexyz_to_cielab_is_correct() { 253 | let arr = array![[ 254 | [0.43550563324299996, 0.221740574943, 0.013494928054000002], 255 | [0.3886224651359999, 0.7219522484959999, 0.08794233419200001], 256 | [0.14021657533599996, 0.05630936703600001, 0.723623297434], 257 | [0.969044992445, 1.0, 0.75726133156], 258 | [0.0, 0.0, 0.0] 259 | ]] 260 | .reversed_axes(); 261 | assert_eq!( 262 | ciexyz_to_cielab(arr.view()).reversed_axes(), 263 | array![[ 264 | [54.21119728870907, 80.98253915088577, 70.28651365985937], 265 | [88.06247407807324, -79.21970052415361, 84.5923008745313], 266 | [28.461577872330834, 71.28097404863831, -114.78144041108523], 267 | [100.0, 0.8195163861430821, 5.639091770210247], 268 | [0.0, 0.0, 0.0] 269 | ]] 270 | ); 271 | } 272 | 273 | #[test] 274 | fn test_ciede2000() { 275 | let color_model = ColorModel::Cielab; 276 | 277 | let test_data: Vec<[f64; 19]> = vec![ 278 | [ 279 | 1., 50., 2.6772, -79.7751, 50., 0., -82.7485, 79.82, 82.7485, 271.9222, 270., 280 | 270.9611, 0.0001, 0.6907, 1., 4.6578, 1.8421, -1.7042, 2.0425, 281 | ], 282 | [ 283 | 2., 50., 3.1571, -77.2803, 50., 0., -82.7485, 77.3448, 82.7485, 272.3395, 270., 284 | 271.1698, 0.0001, 0.6843, 1., 4.6021, 1.8216, -1.707, 2.8615, 285 | ], 286 | [ 287 | 3., 50., 2.8361, -74.02, 50., 0., -82.7485, 74.0743, 82.7485, 272.1944, 270., 288 | 271.0972, 0.0001, 0.6865, 1., 4.5285, 1.8074, -1.706, 3.4412, 289 | ], 290 | [ 291 | 4., 50., -1.3802, -84.2814, 50., 0., -82.7485, 84.2927, 82.7485, 269.0618, 270., 292 | 269.5309, 0.0001, 0.7357, 1., 4.7584, 1.9217, -1.6809, 1., 293 | ], 294 | [ 295 | 5., 50., -1.1848, -84.8006, 50., 0., -82.7485, 84.8089, 82.7485, 269.1995, 270., 296 | 269.5997, 0.0001, 0.7335, 1., 4.77, 1.9218, -1.6822, 1., 297 | ], 298 | [ 299 | 6., 50., -0.9009, -85.5211, 50., 0., -82.7485, 85.5258, 82.7485, 269.3964, 270., 300 | 269.6982, 0.0001, 0.7303, 1., 4.7862, 1.9217, -1.684, 1., 301 | ], 302 | [ 303 | 7., 50., 0., 0., 50., -1., 2., 0., 2.5, 0., 126.8697, 126.8697, 0.5, 1.22, 1., 304 | 1.0562, 1.0229, 0., 2.3669, 305 | ], 306 | [ 307 | 8., 50., -1., 2., 50., 0., 0., 2.5, 0., 126.8697, 0., 126.8697, 0.5, 1.22, 1., 308 | 1.0562, 1.0229, 0., 2.3669, 309 | ], 310 | [ 311 | 9., 50., 2.49, -0.001, 50., -2.49, 0.0009, 3.7346, 3.7346, 359.9847, 179.9862, 312 | 269.9854, 0.4998, 0.7212, 1., 1.1681, 1.0404, -0.0022, 7.1792, 313 | ], 314 | [ 315 | 10., 50., 2.49, -0.001, 50., -2.49, 0.001, 3.7346, 3.7346, 359.9847, 179.9847, 316 | 269.9847, 0.4998, 0.7212, 1., 1.1681, 1.0404, -0.0022, 7.1792, 317 | ], 318 | [ 319 | 11., 50., 2.49, -0.001, 50., -2.49, 0.0011, 3.7346, 3.7346, 359.9847, 179.9831, 320 | 89.9839, 0.4998, 0.6175, 1., 1.1681, 1.0346, 0., 7.2195, 321 | ], 322 | [ 323 | 12., 50., 2.49, -0.001, 50., -2.49, 0.0012, 3.7346, 3.7346, 359.9847, 179.9816, 324 | 89.9831, 0.4998, 0.6175, 1., 1.1681, 1.0346, 0., 7.2195, 325 | ], 326 | [ 327 | 13., 50., -0.001, 2.49, 50., 0.0009, -2.49, 2.49, 2.49, 90.0345, 270.0311, 328 | 180.0328, 0.4998, 0.9779, 1., 1.1121, 1.0365, 0., 4.8045, 329 | ], 330 | [ 331 | 14., 50., -0.001, 2.49, 50., 0.001, -2.49, 2.49, 2.49, 90.0345, 270.0345, 180.0345, 332 | 0.4998, 0.9779, 1., 1.1121, 1.0365, 0., 4.8045, 333 | ], 334 | [ 335 | 15., 50., -0.001, 2.49, 50., 0.0011, -2.49, 2.49, 2.49, 90.0345, 270.038, 0.0362, 336 | 0.4998, 1.3197, 1., 1.1121, 1.0493, 0., 4.7461, 337 | ], 338 | [ 339 | 16., 50., 2.5, 0., 50., 0., -2.5, 3.7496, 2.5, 0., 270., 315., 0.4998, 0.8454, 1., 340 | 1.1406, 1.0396, -0.0001, 4.3065, 341 | ], 342 | [ 343 | 17., 50., 2.5, 0., 73., 25., -18., 3.4569, 38.9743, 0., 332.4939, 346.247, 0.3827, 344 | 1.4453, 1.1608, 1.9547, 1.4599, -0.0003, 27.1492, 345 | ], 346 | [ 347 | 18., 50., 2.5, 0., 61., -5., 29., 3.4954, 29.8307, 0., 103.5532, 51.7766, 0.3981, 348 | 0.6447, 1.064, 1.7498, 1.1612, 0., 22.8977, 349 | ], 350 | [ 351 | 19., 50., 2.5, 0., 56., -27., -3., 3.5514, 38.4728, 0., 184.4723, 272.2362, 0.4206, 352 | 0.6521, 1.0251, 1.9455, 1.2055, -0.8219, 31.903, 353 | ], 354 | [ 355 | 20., 50., 2.5, 0., 58., 24., 15., 3.5244, 37.0102, 0., 23.9095, 11.9548, 0.4098, 356 | 1.1031, 1.04, 1.912, 1.3353, 0., 19.4535, 357 | ], 358 | [ 359 | 21., 50., 2.5, 0., 50., 3.1736, 0.5854, 3.7494, 4.7954, 0., 7.0113, 3.5056, 0.4997, 360 | 1.2616, 1., 1.1923, 1.0808, 0., 1., 361 | ], 362 | [ 363 | 22., 50., 2.5, 0., 50., 3.2972, 0., 3.7493, 4.945, 0., 0., 0., 0.4997, 1.3202, 1., 364 | 1.1956, 1.0861, 0., 1., 365 | ], 366 | [ 367 | 23., 50., 2.5, 0., 50., 1.8634, 0.5757, 3.7497, 2.8536, 0., 11.638, 5.819, 0.4999, 368 | 1.2197, 1., 1.1486, 1.0604, 0., 1., 369 | ], 370 | [ 371 | 24., 50., 2.5, 0., 50., 3.2592, 0.335, 3.7493, 4.8994, 0., 3.9206, 1.9603, 0.4997, 372 | 1.2883, 1., 1.1946, 1.0836, 0., 1., 373 | ], 374 | [ 375 | 25., 60.2574, -34.0099, 36.2677, 60.4626, -34.1751, 39.4387, 49.759, 52.2238, 376 | 133.2085, 130.9584, 132.0835, 0.0017, 1.301, 1.1427, 3.2946, 1.9951, 0., 1.2644, 377 | ], 378 | [ 379 | 26., 63.0109, -31.0961, -5.8663, 62.8187, -29.7946, -4.0864, 33.1427, 31.5202, 380 | 190.1951, 187.449, 188.8221, 0.049, 0.9402, 1.1831, 2.4549, 1.456, 0., 1.263, 381 | ], 382 | [ 383 | 27., 61.2901, 3.7196, -5.3901, 61.4292, 2.248, -4.962, 7.7487, 5.995, 315.924, 384 | 304.1385, 310.0313, 0.4966, 0.6952, 1.1586, 1.3092, 1.0717, -0.0032, 1.8731, 385 | ], 386 | [ 387 | 28., 35.0831, -44.1164, 3.7933, 35.0232, -40.0716, 1.5901, 44.5557, 40.355, 388 | 175.1161, 177.7418, 176.429, 0.0063, 1.0168, 1.2148, 2.9105, 1.6476, 0., 1.8645, 389 | ], 390 | [ 391 | 29., 22.7233, 20.0904, -46.694, 23.0331, 14.973, -42.5619, 50.8532, 45.1317, 392 | 293.3339, 289.4279, 291.3809, 0.0026, 0.3636, 1.4014, 3.1597, 1.2617, -1.2537, 393 | 2.0373, 394 | ], 395 | [ 396 | 30., 36.4612, 47.858, 18.3852, 36.2715, 50.5065, 21.2231, 51.3256, 54.8444, 397 | 20.9901, 22.766, 21.8781, 0.0013, 0.9239, 1.1943, 3.3888, 1.7357, 0., 1.4146, 398 | ], 399 | [ 400 | 31., 90.8027, -2.0831, 1.441, 91.1528, -1.6435, 0.0447, 3.4408, 2.4655, 155.241, 401 | 178.9612, 167.1011, 0.4999, 1.1546, 1.611, 1.1329, 1.0511, 0., 1.4441, 402 | ], 403 | [ 404 | 32., 90.9257, -0.5406, -0.9208, 88.6381, -0.8985, -0.7239, 1.227, 1.5298, 228.6315, 405 | 208.2412, 218.4363, 0.5, 1.3916, 1.593, 1.062, 1.0288, 0., 1.5381, 406 | ], 407 | [ 408 | 33., 6.7747, -0.2908, -2.4247, 5.8714, -0.0985, -2.2286, 2.4636, 2.2335, 259.8025, 409 | 266.2073, 263.0049, 0.4999, 0.9556, 1.6517, 1.1057, 1.0337, -0.0004, 0.6377, 410 | ], 411 | [ 412 | 34., 2.0776, 0.0795, -1.135, 0.9033, -0.0636, -0.5514, 1.1412, 0.5596, 275.9978, 413 | 260.1842, 268.091, 0.5, 0.7826, 1.7246, 1.0383, 1.01, 0., 0.9082, 414 | ], 415 | ]; 416 | for [pair, l1, a1, b1, l2, a2, b2, c1, c2, h1, h2, h_avg, g, t, sl, sc, sh, rt, expected] in 417 | test_data 418 | { 419 | let hcl_a = color_model.cylindrical_single([l1, a1, b1]); 420 | let hcl_b = color_model.cylindrical_single([l2, a2, b2]); 421 | let epsilon = 1e-4; 422 | let actual = color_model.cylindrical_diff(hcl_a, hcl_b); 423 | assert!( 424 | (expected - actual).abs() < epsilon, 425 | "{expected} {actual} {pair} {c1} {c2} {h1} {h2} {h_avg} {g} {t} {sl} {sc} {sh} {rt}", 426 | ); 427 | } 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /src/render.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Once; 2 | 3 | use crate::{ 4 | color::Color, 5 | filter::{edge_tangent_flow, flow_based_difference_of_gaussians}, 6 | graph::{mst, tsp}, 7 | kbn_summation, 8 | voronoi::{ 9 | calculate_centroid, calculate_density, colors_to_assignments, jump_flooding_voronoi, 10 | AnnotatedSite, CellProperties, 11 | }, 12 | Style, 13 | }; 14 | use cairo::{Context, LineCap, LineJoin, Matrix}; 15 | use lyon_geom::{point, Point}; 16 | use ndarray::prelude::*; 17 | use rand::{thread_rng, Rng}; 18 | use spade::delaunay::IntDelaunayTriangulation; 19 | use tracing::{debug, info, warn}; 20 | 21 | pub fn render_fdog_based( 22 | image: ArrayView2, 23 | _super_sample: usize, 24 | _instrument_diameter_in_pixels: f64, 25 | _style: Style, 26 | ctx: &Context, 27 | matrix: Matrix, 28 | ) { 29 | let mut image = image.to_owned(); 30 | // Need to invert image for the sake of fdog 31 | image.par_mapv_inplace(|x| 1.0 - x); 32 | let etf = edge_tangent_flow(image.view()); 33 | let fdog = flow_based_difference_of_gaussians(image.view(), etf.view()); 34 | for (pos, value) in fdog.indexed_iter() { 35 | if *value < 1.0 { 36 | ctx.set_source_rgb(*value, *value, *value); 37 | ctx.set_matrix(matrix); 38 | ctx.move_to(pos.0 as f64, pos.1 as f64); 39 | ctx.rectangle(pos.0 as f64, pos.1 as f64, 1.0, 1.0); 40 | ctx.set_matrix(Matrix::identity()); 41 | ctx.fill().unwrap(); 42 | } 43 | } 44 | } 45 | 46 | /// Run an algorithm to produce a stippling and customize the output according to the desired style. 47 | /// 48 | /// TODO: implement is assumed to be circular, can this support non-circular implements? 49 | pub fn render_stipple_based( 50 | class_images: ArrayView3, 51 | implement_diameters_in_pixels: &[f64], 52 | colors: &[Color], 53 | super_sample: usize, 54 | style: Style, 55 | ctx: &Context, 56 | matrix: Matrix, 57 | ) { 58 | let (_, width, height) = class_images.dim(); 59 | let class_to_sites = 60 | run_mlbg_stippling(class_images, implement_diameters_in_pixels, super_sample); 61 | 62 | for (((image, implement_diameter_in_pixels), color), mut voronoi_sites) in class_images 63 | .axis_iter(Axis(0)) 64 | .zip(implement_diameters_in_pixels.iter()) 65 | .zip(colors.iter()) 66 | .zip(class_to_sites.into_iter()) 67 | { 68 | // On the off chance 2 sites end up being the same... 69 | voronoi_sites.sort_unstable(); 70 | voronoi_sites.dedup(); 71 | 72 | if voronoi_sites.len() < 3 { 73 | warn!( 74 | "Channel has too few vertices ({}) to draw, skipping", 75 | voronoi_sites.len() 76 | ); 77 | continue; 78 | } 79 | info!("Visualizing {}", color); 80 | 81 | ctx.set_line_cap(LineCap::Round); 82 | ctx.set_line_join(LineJoin::Round); 83 | ctx.set_source_rgb(color[0], color[1], color[2]); 84 | ctx.set_line_width(*implement_diameter_in_pixels); 85 | 86 | match style { 87 | Style::Stipple => { 88 | debug!("Draw to svg"); 89 | ctx.set_matrix(matrix); 90 | for site in voronoi_sites { 91 | ctx.move_to(site[0] as f64, site[1] as f64); 92 | ctx.arc( 93 | site[0] as f64, 94 | site[1] as f64, 95 | implement_diameter_in_pixels / 2.0, 96 | 0., 97 | std::f64::consts::TAU, 98 | ); 99 | } 100 | ctx.set_matrix(Matrix::identity()); 101 | ctx.fill().unwrap(); 102 | } 103 | Style::Voronoi => { 104 | debug!("Draw to svg"); 105 | let sites_to_points = colors_to_assignments( 106 | &voronoi_sites, 107 | jump_flooding_voronoi(&voronoi_sites, [width, height]).view(), 108 | ); 109 | for points in sites_to_points { 110 | let properties = CellProperties::calculate(image, &points); 111 | if let Some(hull) = properties.hull { 112 | ctx.set_matrix(matrix); 113 | if let Some(first) = hull.first() { 114 | ctx.move_to(first[0] as f64, first[1] as f64); 115 | } 116 | for point in hull.iter().skip(1) { 117 | ctx.line_to(point[0] as f64, point[1] as f64); 118 | } 119 | ctx.set_matrix(Matrix::identity()); 120 | ctx.stroke().unwrap(); 121 | } 122 | } 123 | } 124 | Style::Triangulation | Style::Mst | Style::Tsp => { 125 | let mut delaunay = IntDelaunayTriangulation::with_tree_locate(); 126 | for vertex in &voronoi_sites { 127 | delaunay.insert([vertex[0], vertex[1]]); 128 | } 129 | 130 | if let Style::Triangulation = style { 131 | debug!("Draw to svg"); 132 | ctx.set_matrix(matrix); 133 | for edge in delaunay.edges() { 134 | let from: &[i64; 2] = &edge.from(); 135 | let to: &[i64; 2] = &edge.to(); 136 | ctx.move_to(from[0] as f64, from[1] as f64); 137 | ctx.line_to(to[0] as f64, to[1] as f64); 138 | } 139 | ctx.set_matrix(Matrix::identity()); 140 | ctx.stroke().unwrap(); 141 | } else { 142 | let tree = mst::compute_mst(&voronoi_sites, &delaunay); 143 | if let Style::Mst = style { 144 | debug!("Draw to svg"); 145 | ctx.set_matrix(matrix); 146 | ctx.move_to(tree[0][0][0] as f64, tree[0][0][1] as f64); 147 | for edge in &tree { 148 | ctx.move_to(edge[0][0] as f64, edge[0][1] as f64); 149 | ctx.line_to(edge[1][0] as f64, edge[1][1] as f64); 150 | } 151 | ctx.stroke().unwrap(); 152 | ctx.set_matrix(Matrix::identity()); 153 | } else { 154 | let tsp = tsp::approximate_tsp_with_mst(&voronoi_sites, &tree); 155 | debug!("Draw to svg"); 156 | ctx.set_matrix(matrix); 157 | if let Some(first) = tsp.first() { 158 | ctx.move_to(first[0] as f64, first[1] as f64); 159 | } 160 | for next in tsp.iter().skip(1) { 161 | ctx.line_to(next[0] as f64, next[1] as f64); 162 | } 163 | ctx.stroke().unwrap(); 164 | ctx.set_matrix(Matrix::identity()); 165 | } 166 | } 167 | } 168 | Style::EdgesPlusHatching => unreachable!(), 169 | } 170 | } 171 | } 172 | 173 | /// Run Weighted Multi-Class Linde-Buzo-Gray Stippling and returns the per class stipples 174 | /// 175 | /// TODO: implement is assumed to be circular, can this support non-circular implements? 176 | /// 177 | /// 178 | /// 179 | fn run_mlbg_stippling( 180 | class_images: ArrayView3, 181 | implement_diameters_in_pixels: &[f64], 182 | super_sample: usize, 183 | ) -> Vec> { 184 | const INITIAL_HYSTERESIS: f64 = 0.6; 185 | const HYSTERESIS_DELTA: f64 = 0.01; 186 | const ZERO: Point = Point::new(0., 0.); 187 | 188 | let implement_areas = implement_diameters_in_pixels 189 | .iter() 190 | .copied() 191 | .map(|diameter| (diameter / 2.).powi(2) * std::f64::consts::PI) 192 | .collect::>(); 193 | let (classes, width, height) = class_images.dim(); 194 | let upper_bound = point((width - 1) as f64, (height - 1) as f64); 195 | let mut rng = thread_rng(); 196 | let mut class_to_sites = (0..classes) 197 | .map(|_| { 198 | vec![[ 199 | rng.gen_range(0..width as i64), 200 | rng.gen_range(0..height as i64), 201 | ]] 202 | }) 203 | .collect::>(); 204 | 205 | for iteration in 0..140 { 206 | let current_hysteresis = INITIAL_HYSTERESIS + iteration as f64 * HYSTERESIS_DELTA; 207 | let remove_threshold = 1. - current_hysteresis / 2.; 208 | let split_threshold = 1. + current_hysteresis / 2.; 209 | 210 | debug!("Computing global cell centroids of all sites"); 211 | 212 | let class_to_site_to_global_centroids = { 213 | let mut acc = class_to_sites 214 | .iter() 215 | .map(|voronoi_sites| vec![None; voronoi_sites.len()]) 216 | .collect::>(); 217 | let global_sites = class_to_sites 218 | .iter() 219 | .enumerate() 220 | .flat_map(|(i, voronoi_sites)| { 221 | voronoi_sites 222 | .iter() 223 | .copied() 224 | .enumerate() 225 | .map(move |(j, site)| AnnotatedSite { 226 | site, 227 | annotation: (i, j), 228 | }) 229 | }) 230 | .collect::>(); 231 | let global_colored_pixels = jump_flooding_voronoi(&global_sites, [width, height]); 232 | let global_sites_to_points = 233 | colors_to_assignments(&global_sites, global_colored_pixels.view()); 234 | global_sites_to_points 235 | .into_iter() 236 | .zip(global_sites.iter()) 237 | .for_each(|(points, annotated_site)| { 238 | // Calculate the centroid of the points in each class and average it 239 | let (num_centroids, summed_centroid) = (0..classes) 240 | .filter_map(|k| { 241 | let class_image = class_images.slice(s![k, .., ..]); 242 | calculate_centroid(class_image, &points) 243 | }) 244 | .fold( 245 | (0, ZERO), 246 | |(num_centroids, summed_centroid), class_centroid| { 247 | ( 248 | num_centroids + 1, 249 | summed_centroid + class_centroid.to_vector(), 250 | ) 251 | }, 252 | ); 253 | if num_centroids > 0 { 254 | acc[annotated_site.annotation.0][annotated_site.annotation.1] = 255 | Some(summed_centroid / num_centroids as f64); 256 | } 257 | }); 258 | acc 259 | }; 260 | 261 | let changed = Once::new(); 262 | class_to_sites = (0..classes) 263 | // .into_par_iter() 264 | .map(|k| (k, class_images.slice(s![k, .., ..]))) 265 | .zip(implement_areas.iter()) 266 | .zip(class_to_sites.iter()) 267 | .zip(class_to_site_to_global_centroids.iter()) 268 | // .zip(implement_areas.par_iter()) 269 | // .zip(class_to_sites.par_iter()) 270 | // .zip(class_to_site_to_global_centroids.par_iter()) 271 | .map( 272 | |((((k, class_image), implement_area), sites), site_to_global_centroid)| { 273 | if sites.is_empty() { 274 | return vec![]; 275 | } 276 | debug!("JFA class {k}"); 277 | let colored_pixels = jump_flooding_voronoi(sites, [width, height]); 278 | let site_to_points = colors_to_assignments(sites, colored_pixels.view()); 279 | debug!("Assign class {k}"); 280 | 281 | let mut rng = thread_rng(); 282 | sites 283 | // .par_iter() 284 | // .zip(site_to_points.par_iter()) 285 | // .zip(site_to_global_centroid.par_iter()) 286 | .iter() 287 | .zip(site_to_points.iter()) 288 | .zip(site_to_global_centroid.iter()) 289 | .flat_map(|((site, points), global_centroid)| { 290 | let cell_properties = CellProperties::calculate(class_image.view(), points); 291 | let moments = &cell_properties.moments; 292 | 293 | // Density is very low, remove this point early 294 | if moments.m00 <= f64::EPSILON { 295 | changed.call_once(|| {}); 296 | return vec![]; 297 | } 298 | let should_use_global = global_centroid.is_some() && { 299 | kbn_summation! { 300 | for class_image in (0..classes).map(|k| class_images.slice(s![k, .., ..])) => { 301 | sum_class_densities += calculate_density(class_image, points); 302 | } 303 | } 304 | let average_density = cell_properties.moments.m00 / points.len() as f64; 305 | let should_use_class = rng.gen_bool((sum_class_densities - average_density).clamp(0., 1.)); 306 | !should_use_class 307 | }; 308 | let centroid = if should_use_global { global_centroid.unwrap() } else {cell_properties.centroid.unwrap()}; 309 | let scaled_density = moments.m00 / super_sample.pow(2) as f64; 310 | let stipple_area = implement_area; 311 | 312 | match ( 313 | scaled_density < remove_threshold * stipple_area, 314 | scaled_density < split_threshold * stipple_area, 315 | cell_properties.phi_oriented_segment_through_centroid, 316 | ) { 317 | // Below remove threshold, remove point 318 | (true, _, _) => { 319 | changed.call_once(|| {}); 320 | vec![] 321 | } 322 | // Below split threshold, keep as centroid 323 | (false, true, _) | (_, _, None) => { 324 | let new_site = centroid 325 | .clamp(ZERO, upper_bound) 326 | .round() 327 | .cast::() 328 | .to_array(); 329 | if *site != new_site { 330 | changed.call_once(|| {}); 331 | } 332 | vec![new_site] 333 | } 334 | // Above split threshold, split along phi from the centroid 335 | (false, false, Some(line_segment)) => { 336 | if line_segment.length() < f64::EPSILON { 337 | warn!("It shouldn't be possible to have a phi segment of 0 length here: {:?}", &cell_properties.centroid); 338 | } 339 | let left = line_segment 340 | .sample(1. / 3.) 341 | .clamp(ZERO, upper_bound) 342 | .round() 343 | .cast::() 344 | .to_array(); 345 | let right = line_segment 346 | .sample(2. / 3.) 347 | .clamp(ZERO, upper_bound) 348 | .round() 349 | .cast::() 350 | .to_array(); 351 | 352 | changed.call_once(|| {}); 353 | if left == right { 354 | warn!( 355 | "Splitting a point produced the same point: {:?}", 356 | &cell_properties.centroid 357 | ); 358 | vec![left] 359 | } else { 360 | vec![left, right] 361 | } 362 | } 363 | } 364 | }) 365 | .collect() 366 | }, 367 | ) 368 | .collect(); 369 | debug!( 370 | "Check stopping condition (iteration = {}, sites: {})", 371 | iteration, 372 | class_to_sites 373 | .iter() 374 | .map(|sites| sites.len()) 375 | .sum::() 376 | ); 377 | if !changed.is_completed() { 378 | break; 379 | } 380 | } 381 | 382 | class_to_sites 383 | } 384 | -------------------------------------------------------------------------------- /src/filter.rs: -------------------------------------------------------------------------------- 1 | use lyon_geom::point; 2 | use lyon_geom::vector; 3 | use lyon_geom::Line; 4 | use lyon_geom::Vector; 5 | use ndarray::par_azip; 6 | use ndarray::prelude::*; 7 | use ndarray::Zip; 8 | use ndarray_stats::QuantileExt; 9 | 10 | use crate::get_slice_info_for_offset; 11 | 12 | /// 99.73% of values are accounted for within 3 standard deviations 13 | /// 14 | /// 15 | const NUM_STANDARD_DEVIATIONS: f64 = 3.; 16 | 17 | /// [Sobel operator](https://en.wikipedia.org/wiki/Sobel_operator) 18 | /// 19 | /// Edge handling is a kernel crop without compensation. 20 | pub fn sobel_operator(image: ArrayView2) -> Array2> { 21 | /// [Scharr operator](https://en.wikipedia.org/wiki/Sobel_operator#Alternative_operators) with better rotational symmetry. 22 | const SOBEL_X: [f64; 9] = [3., 0., -3., 10., 0., -10., 3., 0., -3.]; 23 | 24 | let sobel_x = ArrayView2::from_shape((3, 3), &SOBEL_X).unwrap(); 25 | let sobel_y = sobel_x.t(); 26 | let g_x = convolve(image.view(), sobel_x.view()); 27 | let g_y = convolve(image.view(), sobel_y); 28 | 29 | Zip::from(&g_x) 30 | .and(&g_y) 31 | .par_map_collect(|g_x, g_y| vector(*g_x, *g_y)) 32 | } 33 | 34 | /// Construct the edge tangent flow. 35 | /// 36 | /// Array of the vectors tangent to the gradient at each pixel. 37 | /// 38 | /// Edge handling is a kernel crop without compensation. 39 | /// 40 | /// 41 | /// 42 | /// 43 | pub fn edge_tangent_flow(image: ArrayView2) -> Array2> { 44 | let radius = 3; 45 | let iterations = 3; 46 | 47 | // Magnitude and initial ETF based on Sobel operator 48 | let (mut ĝ, mut t) = { 49 | let mut t0 = sobel_operator(image); 50 | // CCW vector is tangent to the gradient 51 | t0.mapv_inplace(|v| vector(-v.y, v.x)); 52 | (t0.mapv(Vector::length), t0) 53 | }; 54 | // Normalization 55 | { 56 | let ĝ_max = *ĝ.max().unwrap(); 57 | ĝ.par_mapv_inplace(|ĝ_x| ĝ_x / ĝ_max); 58 | t.par_mapv_inplace(Vector::normalize); 59 | } 60 | 61 | for _ in 0..iterations { 62 | let mut t_prime = Array2::from_elem(image.raw_dim(), Vector::zero()); 63 | for i in -radius..=radius { 64 | for j in -radius..=radius { 65 | let center_slice = get_slice_info_for_offset(-i, -j); 66 | let kernel_slice = get_slice_info_for_offset(i, j); 67 | // w_s determines whether this computation is useful 68 | let w_s = i.pow(2) + j.pow(2) < radius.pow(2); 69 | if w_s { 70 | par_azip! {(t_prime_x in &mut t_prime.slice_mut(center_slice), t_y in t.slice(kernel_slice), t_x in t.slice(center_slice), ĝ_y in ĝ.slice(kernel_slice), ĝ_x in ĝ.slice(center_slice)) { 71 | // Some implementations use tanh here. This is only required if the fall-off rate is greater than 1. 72 | let w_m = (ĝ_y - ĝ_x + 1.) / 2.; 73 | // Note that due to normalization, this is actually just the cosine of the angle between the vectors. 74 | let dot_product = t_x.dot(*t_y); 75 | let w_d = dot_product.abs(); 76 | let phi = dot_product.signum(); 77 | *t_prime_x += *t_y * phi * w_m * w_d; 78 | } 79 | } 80 | } 81 | } 82 | } 83 | t_prime.par_mapv_inplace(Vector::normalize); 84 | t.assign(&t_prime); 85 | } 86 | 87 | t 88 | } 89 | 90 | /// Apply the FDoG filter to the image using the edge tangent flow. 91 | /// 92 | /// Uses parameter values recommended in the paper. 93 | /// 94 | /// 95 | /// 96 | pub fn flow_based_difference_of_gaussians( 97 | image: ArrayView2, 98 | etf: ArrayView2>, 99 | ) -> Array2 { 100 | let sigma_c = 1.; 101 | let sigma_s = 1.6 * sigma_c; 102 | let t_range = (NUM_STANDARD_DEVIATIONS * sigma_s).ceil() as usize; 103 | let rho = 0.99; 104 | let sigma_m = 3.; 105 | let s_range = (NUM_STANDARD_DEVIATIONS * sigma_m).ceil() as usize; 106 | let tau = 0.5; 107 | let iterations = 3; 108 | 109 | let (width, height) = image.dim(); 110 | 111 | let positions = Array::from_iter((0..width).flat_map(|x| (0..height).map(move |y| [x, y]))) 112 | .into_shape_clone((width, height)) 113 | .unwrap(); 114 | 115 | let mut i = image.to_owned(); 116 | let mut ĥ = Array2::zeros((width, height)); 117 | 118 | for _ in 0..iterations { 119 | let mut integrated_over_t = Array2::::zeros((width, height)); 120 | Zip::from(&mut integrated_over_t) 121 | .and(&positions) 122 | .par_for_each(|i_x, position| { 123 | let mut f_t_sum = 0.; 124 | // Rotate ETF vector 90° clockwise 125 | let gradient_perpendicular_vector = 126 | vector(etf[*position].y, -etf[*position].x).normalize(); 127 | let iterate_by_y = 128 | gradient_perpendicular_vector.y.abs() > gradient_perpendicular_vector.x.abs(); 129 | let line_equation = Line { 130 | point: point(position[0] as f64, position[1] as f64), 131 | vector: gradient_perpendicular_vector, 132 | } 133 | .equation(); 134 | for direction in [-1.0, 1.0] { 135 | let mut integration_position = *position; 136 | 137 | for t in 0..=t_range { 138 | if !(t == 0 && direction > 0.0) { 139 | let f_t = gaussian_pdf(t as f64, sigma_c) 140 | - rho * gaussian_pdf(t as f64, sigma_s); 141 | f_t_sum += f_t; 142 | *i_x += i[integration_position] * f_t; 143 | } 144 | 145 | let mut reached_edge_of_image = false; 146 | 147 | // Bresenham's line algorithm 148 | let solved = if iterate_by_y { 149 | let y = position[1] as f64 + (t + 1) as f64 * direction; 150 | [line_equation.solve_x_for_y(y).unwrap(), y] 151 | } else { 152 | let x = position[0] as f64 + (t + 1) as f64 * direction; 153 | [x, line_equation.solve_y_for_x(x).unwrap()] 154 | }; 155 | 156 | for ((dim_position, dim_solved), dim_limit) in integration_position 157 | .iter_mut() 158 | .zip(solved.iter()) 159 | .zip([width, height]) 160 | { 161 | let dim_solved = dim_solved.round(); 162 | if dim_solved < 0. || dim_solved >= (dim_limit - 1) as f64 { 163 | reached_edge_of_image = true; 164 | break; 165 | } else { 166 | *dim_position = dim_solved as usize; 167 | } 168 | } 169 | if reached_edge_of_image { 170 | break; 171 | } 172 | } 173 | } 174 | *i_x /= f_t_sum; 175 | }); 176 | 177 | let mut h = Array2::::zeros((width, height)); 178 | 179 | Zip::from(&mut h) 180 | .and(&positions) 181 | .par_for_each(|h_x, position| { 182 | let mut g_m_sum = 0.; 183 | for direction in [-1.0, 1.0] { 184 | let mut integration_position = *position; 185 | for s in 0..=s_range { 186 | // An important distinction here: as opposed to the gradient_perpendicular_vector, 187 | // the flow_vector is NOT fixed and changes as the position is updated to follow the curve. 188 | let flow_vector = etf[integration_position]; 189 | 190 | let iterate_by_y = flow_vector.y.abs() > flow_vector.x.abs(); 191 | let line_equation = Line { 192 | point: point( 193 | integration_position[0] as f64, 194 | integration_position[1] as f64, 195 | ), 196 | vector: flow_vector, 197 | } 198 | .equation(); 199 | 200 | if !(s == 0 && direction > 0.0) { 201 | let g_m = gaussian_pdf(s as f64, sigma_m); 202 | g_m_sum += g_m; 203 | *h_x += integrated_over_t[integration_position] * g_m; 204 | } 205 | 206 | let mut reached_edge_of_image = false; 207 | 208 | // Bresenham's line algorithm 209 | let solved = if iterate_by_y { 210 | let y = integration_position[1] as f64 + direction; 211 | [line_equation.solve_x_for_y(y).unwrap(), y] 212 | } else { 213 | let x = integration_position[0] as f64 + direction; 214 | [x, line_equation.solve_y_for_x(x).unwrap()] 215 | }; 216 | 217 | for ((dim_position, dim_solved), dim_limit) in integration_position 218 | .iter_mut() 219 | .zip(solved.iter()) 220 | .zip([width, height]) 221 | { 222 | let dim_solved = dim_solved.round(); 223 | if dim_solved < 0. || dim_solved >= (dim_limit - 1) as f64 { 224 | reached_edge_of_image = true; 225 | break; 226 | } else { 227 | *dim_position = dim_solved as usize; 228 | } 229 | } 230 | if reached_edge_of_image { 231 | break; 232 | } 233 | } 234 | } 235 | *h_x /= g_m_sum; 236 | }); 237 | 238 | par_azip! { 239 | (ĥ_x in &mut ĥ, h_x in &h) { 240 | *ĥ_x = if *h_x < 0. && 1. + h_x.tanh() < tau { 241 | 0. 242 | } else { 243 | 1. 244 | }; 245 | } 246 | }; 247 | 248 | par_azip! { 249 | (i_x in &mut i, ĥ_x in &ĥ) { 250 | *i_x = i_x.min(*ĥ_x); 251 | } 252 | }; 253 | } 254 | 255 | ĥ 256 | } 257 | 258 | pub struct EdgeFlowEstimate { 259 | /// measure of local contrast 260 | pub eigenvalues: Array2<[f64; 2]>, 261 | /// direction of maximum and minimum local contrast 262 | pub eigenvectors: Array2<[Vector; 2]>, 263 | /// ranges from 0 (isotropic) to 1 (strongly oriented) 264 | pub local_anisotropy: Array2, 265 | } 266 | 267 | /// Edge flow estimation using local contrast eigenvectors 268 | /// 269 | /// This expects an input image to be smoothed with a Gaussian filter. 270 | /// Otherwise the eigenvectors will have a high degree of discontinuity. 271 | /// 272 | /// 273 | pub fn edge_flow_estimation(image: ArrayView2) -> EdgeFlowEstimate { 274 | let gradient_vectors: Array2> = sobel_operator(image); 275 | let eigenvalues = gradient_vectors.mapv(|v| { 276 | let e = v.x.powi(2); 277 | let f = v.x * v.y; 278 | let g = v.y.powi(2); 279 | let e_g_sum = e + g; 280 | let sqrt_sum = ((e - g).powi(2) + 4. * f.powi(2)).sqrt(); 281 | [(e_g_sum + sqrt_sum) / 2., (e_g_sum - sqrt_sum) / 2.] 282 | }); 283 | let eigenvectors = Zip::from(&gradient_vectors) 284 | .and(&eigenvalues) 285 | .map_collect(|v, e| { 286 | [ 287 | vector(v.x * v.y, e[0] - v.x.powi(2)), 288 | vector(e[1] - v.y.powi(2), v.x * v.y), 289 | ] 290 | }); 291 | let local_anisotropy = eigenvalues.mapv(|e| (e[0] - e[1]) / (e[0] + e[1])); 292 | EdgeFlowEstimate { 293 | eigenvalues, 294 | eigenvectors, 295 | local_anisotropy, 296 | } 297 | } 298 | 299 | /// Step edge detection using the edge flow estimate for conditioning. 300 | /// 301 | /// 302 | pub fn step_edge_detection( 303 | image: ArrayView2, 304 | edge_flow_estimate: ArrayView2<[Vector; 2]>, 305 | ) -> Array2 { 306 | let sigma_c = 1.; 307 | let sigma_s = 1.6 * sigma_c; 308 | let rho = 1.; 309 | let phi_e_sharpness = 0.25; 310 | let threshold = 0.3; 311 | 312 | let (width, height) = image.dim(); 313 | 314 | let t_range = (NUM_STANDARD_DEVIATIONS * sigma_s).ceil() as usize; 315 | 316 | let positions = Array::from_iter((0..width).flat_map(|x| (0..height).map(move |y| [x, y]))) 317 | .into_shape_clone(image.raw_dim()) 318 | .unwrap(); 319 | 320 | let mut d = Array2::::zeros(image.raw_dim()); 321 | Zip::from(&mut d) 322 | .and(&positions) 323 | .par_for_each(|d_x, position| { 324 | let mut first_derivative_component = 0.; 325 | let mut first_derivative_weight_sum = 0.; 326 | let mut laplacian_of_gaussian_component = 0.; 327 | let mut laplacian_of_gaussian_weight_sum = 0.; 328 | 329 | let gradient_perpendicular_vector = edge_flow_estimate[*position][0].normalize(); 330 | let iterate_by_y = 331 | gradient_perpendicular_vector.y.abs() > gradient_perpendicular_vector.x.abs(); 332 | let line_equation = Line { 333 | point: point(position[0] as f64, position[1] as f64), 334 | vector: gradient_perpendicular_vector, 335 | } 336 | .equation(); 337 | for direction in [-1.0, 1.0] { 338 | let mut sum_position = *position; 339 | 340 | for t in 0..=t_range { 341 | if !(t == 0 && direction > 0.0) { 342 | let dist = (vector(position[0] as f64, position[1] as f64) 343 | - vector(sum_position[0] as f64, sum_position[1] as f64)) 344 | .length(); 345 | let first_derivative_weight = gaussian_pdf_first_derivative(dist, sigma_c); 346 | first_derivative_weight_sum += first_derivative_weight; 347 | first_derivative_component += first_derivative_weight * image[sum_position]; 348 | 349 | let laplacian_of_gaussian_weight = 350 | gaussian_pdf(dist, sigma_c) - rho * gaussian_pdf(dist, sigma_s); 351 | laplacian_of_gaussian_weight_sum += laplacian_of_gaussian_weight; 352 | laplacian_of_gaussian_component += 353 | laplacian_of_gaussian_weight * image[sum_position]; 354 | } 355 | 356 | let mut reached_edge_of_image = false; 357 | 358 | // Bresenham's line algorithm 359 | let solved = if iterate_by_y { 360 | let y = position[1] as f64 + (t + 1) as f64 * direction; 361 | [line_equation.solve_x_for_y(y).unwrap(), y] 362 | } else { 363 | let x = position[0] as f64 + (t + 1) as f64 * direction; 364 | [x, line_equation.solve_y_for_x(x).unwrap()] 365 | }; 366 | 367 | for ((dim_position, dim_solved), dim_limit) in sum_position 368 | .iter_mut() 369 | .zip(solved.iter()) 370 | .zip([width, height]) 371 | { 372 | let dim_solved = dim_solved.round(); 373 | if dim_solved < 0. || dim_solved >= (dim_limit - 1) as f64 { 374 | reached_edge_of_image = true; 375 | break; 376 | } else { 377 | *dim_position = dim_solved as usize; 378 | } 379 | } 380 | if reached_edge_of_image { 381 | break; 382 | } 383 | } 384 | } 385 | *d_x = first_derivative_component.abs() - laplacian_of_gaussian_component.abs(); 386 | }); 387 | 388 | #[allow(clippy::let_and_return)] 389 | let h = { 390 | // let min = *d.min().unwrap(); 391 | // let max = *d.max().unwrap(); 392 | d.mapv_inplace(|d_x| { 393 | // (d_x - min) / (max - min) 394 | if d_x < threshold { 395 | 1. 396 | } else { 397 | 1. - (phi_e_sharpness * d_x).tanh() 398 | } 399 | }); 400 | d 401 | }; 402 | 403 | h 404 | } 405 | 406 | /// 407 | #[inline] 408 | fn gaussian_pdf(x: f64, sigma: f64) -> f64 { 409 | (-0.5 * (x / sigma).powi(2)).exp() 410 | / (std::f64::consts::PI.sqrt() * std::f64::consts::SQRT_2 * sigma) 411 | } 412 | 413 | #[inline] 414 | fn gaussian_pdf_first_derivative(x: f64, sigma: f64) -> f64 { 415 | (-0.5 * (x / sigma).powi(2)).exp() * -x 416 | / (std::f64::consts::PI.sqrt() * std::f64::consts::SQRT_2 * sigma.powi(3)) 417 | } 418 | 419 | #[inline] 420 | fn gaussian_cdf(x: f64, sigma: f64) -> f64 { 421 | 0.5 * (1. + erf(x / (sigma * std::f64::consts::SQRT_2))) 422 | } 423 | 424 | fn erf(x: f64) -> f64 { 425 | let sign = x.signum(); 426 | let x = x.abs(); 427 | let p = 0.47047; 428 | let a1 = 0.3480242; 429 | let a2 = -0.0958798; 430 | let a3 = 0.7478556; 431 | let t = 1. / (1. + p * x); 432 | let tau = (a1 * t + a2 * t.powi(2) + a3 * t.powi(3)) * (-x.powi(2)).exp(); 433 | if sign >= 0. { 434 | 1. - tau 435 | } else { 436 | tau - 1. 437 | } 438 | } 439 | 440 | /// Use an NxN [kernel](https://en.wikipedia.org/wiki/Kernel_(image_processing)) to do convolution on an image. 441 | /// 442 | /// Edge handling is a kernel crop without compensation. 443 | fn convolve<'a>(image: ArrayView2<'a, f64>, kernel: ArrayView2<'a, f64>) -> Array2 { 444 | let kernel_transpose = kernel.t(); 445 | let (kernel_width, kernel_height) = kernel.raw_dim().into_pattern(); 446 | let mut it = kernel_transpose.iter(); 447 | let mut convolved = Array::zeros(image.raw_dim()); 448 | for i in -(kernel_width as i32) / 2..=kernel_width as i32 / 2 { 449 | for j in -(kernel_height as i32) / 2..=kernel_height as i32 / 2 { 450 | let center_slice = get_slice_info_for_offset(-i, -j); 451 | let kernel_slice = get_slice_info_for_offset(i, j); 452 | let coefficient = it.next().unwrap(); 453 | Zip::from(convolved.slice_mut(center_slice)) 454 | .and(image.slice(kernel_slice)) 455 | .par_for_each(|dest, kernel| *dest += coefficient * *kernel); 456 | } 457 | } 458 | convolved 459 | } 460 | 461 | #[cfg(test)] 462 | mod tests { 463 | use super::*; 464 | #[test] 465 | fn test_convolution_operator() { 466 | let image = Array2::ones((3, 3)); 467 | let kernel = array![[1., 2., 3.], [4., 5., 6.], [7., 8., 9.]]; 468 | let result = convolve(image.view(), kernel.view()); 469 | assert_eq!(result[[1, 1]], (1..=9).sum::() as f64); 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /src/optimize/direct.rs: -------------------------------------------------------------------------------- 1 | //! DIRECT algorithm implementation, with added improvements. 2 | //! 3 | //! 4 | //! 5 | 6 | use std::collections::BinaryHeap; 7 | 8 | use ndarray::{azip, s, stack, Array1, Array2, ArrayView1, Axis}; 9 | use num_rational::Rational64; 10 | use num_traits::ToPrimitive; 11 | 12 | pub struct Direct 13 | where 14 | F: Fn(ArrayView1) -> f64, 15 | { 16 | pub function: F, 17 | pub bounds: Array1<[f64; 2]>, 18 | pub max_evaluations: Option, 19 | pub max_iterations: Option, 20 | /// Enables DIRECT-restart. 21 | pub adapt_epsilon: bool, 22 | /// Enables recommended DIRECT revisions that reduce global drag. 23 | pub reduce_global_drag: bool, 24 | /// Controls how DIRECT groups rectangles 25 | pub size_metric: SizeMetric, 26 | } 27 | 28 | #[derive(Debug)] 29 | struct DirectState { 30 | epsilon: AdaptiveEpsilon, 31 | iterations: usize, 32 | evaluations: usize, 33 | rectangles_by_size: Vec, 34 | dimension_split_counters: Vec, 35 | xmin: Array1, 36 | fmin: f64, 37 | } 38 | 39 | pub enum SizeMetric { 40 | /// Taxicab 41 | L1, 42 | /// Euclidean 43 | L2, 44 | /// Largest dimension 45 | Linf, 46 | } 47 | 48 | /// Hyper-rectangle as defined by the DIRECT algorithm. 49 | #[derive(Debug, PartialEq)] 50 | struct Rectangle { 51 | /// Bounds are represented as their length, rather than the endpoints, which can be derived from the center. 52 | bound_lengths: Array1, 53 | size: Rational64, 54 | center: Array1, 55 | fmin: f64, 56 | } 57 | 58 | impl Eq for Rectangle {} 59 | 60 | impl PartialOrd for Rectangle { 61 | fn partial_cmp(&self, other: &Self) -> Option { 62 | Some(self.cmp(other)) 63 | } 64 | } 65 | 66 | /// For ergonomics, the order is reversed here instead of with a [std::cmp::Reverse] wrapper. 67 | impl Ord for Rectangle { 68 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 69 | self.fmin.partial_cmp(&other.fmin).unwrap().reverse() 70 | } 71 | } 72 | 73 | /// A set of [Rectangles](Rectangle) with the same size, or area. 74 | #[derive(Debug)] 75 | struct Group { 76 | size: Rational64, 77 | rectangles: BinaryHeap, 78 | } 79 | 80 | impl Group { 81 | fn fmin(&self) -> f64 { 82 | self.rectangles.peek().expect("non empty").fmin 83 | } 84 | } 85 | 86 | impl Direct 87 | where 88 | F: Fn(ArrayView1) -> f64, 89 | { 90 | pub fn run(&self) -> (Array1, f64) { 91 | let mut state = DirectState { 92 | epsilon: AdaptiveEpsilon::new(self.adapt_epsilon), 93 | iterations: 0, 94 | evaluations: 0, 95 | rectangles_by_size: vec![], 96 | dimension_split_counters: vec![0; self.bounds.len()], 97 | xmin: Array1::zeros(self.bounds.len()), 98 | fmin: 0., 99 | }; 100 | self.initialize(&mut state); 101 | 102 | loop { 103 | if let Some(max_evaluations) = self.max_evaluations { 104 | if state.evaluations >= max_evaluations { 105 | break; 106 | } 107 | } 108 | if let Some(max_iterations) = self.max_iterations { 109 | if state.iterations >= max_iterations { 110 | break; 111 | } 112 | } 113 | 114 | let potentially_optimal = self.extract_potentially_optimal(&mut state); 115 | if potentially_optimal.is_empty() { 116 | break; 117 | } 118 | for rectangle in potentially_optimal { 119 | let (split_xmin, split_fmin) = self.split(rectangle, &mut state); 120 | if split_fmin < state.fmin { 121 | state.xmin = split_xmin; 122 | state.fmin = split_fmin; 123 | state.epsilon.improved(state.iterations); 124 | } else { 125 | state.epsilon.no_improvement(state.iterations); 126 | } 127 | } 128 | state.iterations += 1; 129 | } 130 | (self.denormalize_point(state.xmin.view()), state.fmin) 131 | } 132 | 133 | /// Initialize data structures following Section 3.2 134 | fn initialize(&self, state: &mut DirectState) { 135 | let dimensions = self.bounds.len(); 136 | let center = Array1::from_elem(dimensions, Rational64::new_raw(1, 2)); 137 | let bound_lengths = Array1::ones(dimensions); 138 | 139 | let center_eval = (self.function)(self.denormalize_point(center.view()).view()); 140 | state.evaluations += 1; 141 | let rectangle = Rectangle { 142 | center, 143 | bound_lengths, 144 | size: match self.size_metric { 145 | SizeMetric::L1 => Rational64::new_raw(dimensions as i64, 1), 146 | SizeMetric::L2 => Rational64::new_raw(1, 2).pow(2) * dimensions as i64, 147 | SizeMetric::Linf => Rational64::ONE, 148 | }, 149 | fmin: center_eval, 150 | }; 151 | 152 | let (xmin, fmin) = self.split(rectangle, state); 153 | state.xmin = xmin; 154 | state.fmin = fmin; 155 | } 156 | 157 | /// Identify and extract potentially optimal rectangles. 158 | fn extract_potentially_optimal( 159 | &self, 160 | DirectState { 161 | epsilon, 162 | rectangles_by_size, 163 | fmin, 164 | .. 165 | }: &mut DirectState, 166 | ) -> Vec { 167 | let fmin_is_zero = fmin.abs() < f64::EPSILON; 168 | 169 | let mut potentially_optimal_group_indices = vec![]; 170 | 171 | for (j, group) in rectangles_by_size.iter().enumerate() { 172 | // Lemma 3.3 (7) values 173 | let maximum_smaller_diff = rectangles_by_size[..j] 174 | .iter() 175 | .map(|smaller_group| { 176 | (group.fmin() - smaller_group.fmin()) 177 | / (group.size - smaller_group.size).to_f64().unwrap() 178 | }) 179 | .max_by(|a, b| a.partial_cmp(b).unwrap()); 180 | let minimum_larger_diff = rectangles_by_size[j + 1..] 181 | .iter() 182 | .map(|larger_group| { 183 | (larger_group.fmin() - group.fmin()) 184 | / (larger_group.size - group.size).to_f64().unwrap() 185 | }) 186 | .min_by(|a, b| a.partial_cmp(b).unwrap()); 187 | 188 | let is_potentially_optimal = if let Some(minimum_larger_diff) = minimum_larger_diff { 189 | // Lemma 3.3 (7) 190 | let lemma_7_satisfied = if let Some(maximum_smaller_diff) = maximum_smaller_diff { 191 | minimum_larger_diff > 0. && maximum_smaller_diff <= minimum_larger_diff 192 | } else { 193 | true 194 | }; 195 | let lemma_8_or_9_satisfied = if !fmin_is_zero { 196 | // Lemma 3.3 (8) 197 | epsilon.value() 198 | <= (*fmin - group.fmin()) / fmin.abs() 199 | + group.size.to_f64().unwrap() / fmin.abs() * minimum_larger_diff 200 | } else { 201 | // Lemma 3.3 (9) 202 | group.fmin() <= group.size.to_f64().unwrap() * minimum_larger_diff 203 | }; 204 | 205 | lemma_7_satisfied && lemma_8_or_9_satisfied 206 | } else { 207 | true 208 | }; 209 | 210 | if is_potentially_optimal { 211 | // dbg!(group.size, minimum_larger_diff, maximum_smaller_diff); 212 | // Lemma 3.3 (6) 213 | potentially_optimal_group_indices.push(j); 214 | } 215 | } 216 | 217 | let mut potentially_optimal = vec![]; 218 | for i in potentially_optimal_group_indices { 219 | let group = &mut rectangles_by_size[i]; 220 | let group_fmin = group.fmin(); 221 | while let Some(rectangle) = group.rectangles.peek() { 222 | if (rectangle.fmin - group_fmin).abs() < f64::EPSILON { 223 | potentially_optimal.push(group.rectangles.pop().unwrap()); 224 | } else { 225 | break; 226 | } 227 | 228 | if self.reduce_global_drag { 229 | break; 230 | } 231 | } 232 | } 233 | 234 | // Update data structures after extracting rectangles. 235 | rectangles_by_size.retain(|g| !g.rectangles.is_empty()); 236 | 237 | potentially_optimal 238 | } 239 | 240 | /// Split the given [Rectangle]. 241 | fn split( 242 | &self, 243 | rectangle: Rectangle, 244 | DirectState { 245 | evaluations, 246 | rectangles_by_size, 247 | dimension_split_counters, 248 | .. 249 | }: &mut DirectState, 250 | ) -> (Array1, f64) { 251 | let dimensions = rectangle.bound_lengths.len(); 252 | 253 | let indices = if self.reduce_global_drag { 254 | // Pick a single longest bound, using split counts for tie-breaking. 255 | let bound = rectangle 256 | .bound_lengths 257 | .iter() 258 | .copied() 259 | .enumerate() 260 | .max_by(|(i, a), (j, b)| { 261 | a.partial_cmp(b).unwrap().then( 262 | dimension_split_counters[*i] 263 | .cmp(&dimension_split_counters[*j]) 264 | .reverse(), 265 | ) 266 | }) 267 | .map(|(i, _)| i) 268 | .unwrap(); 269 | vec![bound] 270 | } else { 271 | // Find the longest bound. 272 | let longest_bound_length = rectangle.bound_lengths.iter().copied().max().unwrap(); 273 | 274 | // Split along all bounds that are the same length as the longest bound. 275 | rectangle 276 | .bound_lengths 277 | .indexed_iter() 278 | .filter(|(_, len)| **len == longest_bound_length) 279 | .map(|(i, _)| i) 280 | .collect::>() 281 | }; 282 | 283 | // Update split counters for tie-breaking in reduced global drag. 284 | for i in &indices { 285 | dimension_split_counters[*i] += 1; 286 | } 287 | 288 | // Difference vector for each dimension being split (indices x dimensions) 289 | let mut δ_e = Array2::zeros((indices.len(), dimensions)); 290 | for (i, dim) in indices.iter().enumerate() { 291 | δ_e[[i, *dim]] = rectangle.bound_lengths[*dim] / 3; 292 | } 293 | 294 | // function inputs (indices x 2 x dimensions) 295 | let c_δ_e = stack![Axis(1), &rectangle.center - &δ_e, &rectangle.center + &δ_e]; 296 | 297 | // evaluate function for each c +/- δ_e (indices x 2) 298 | let f_c_δ_e = c_δ_e.map_axis(Axis(2), |x| { 299 | (self.function)(self.denormalize_point(x).view()) 300 | }); 301 | *evaluations += f_c_δ_e.len(); 302 | 303 | let (f_c_δ_e_min_index, f_c_δ_e_min_value) = f_c_δ_e 304 | .indexed_iter() 305 | .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()) 306 | .unwrap(); 307 | // The minimum function evaluation found during this split 308 | let (split_xmin, split_fmin) = if *f_c_δ_e_min_value < rectangle.fmin { 309 | ( 310 | c_δ_e 311 | .slice(s![f_c_δ_e_min_index.0, f_c_δ_e_min_index.1, ..]) 312 | .to_owned(), 313 | *f_c_δ_e_min_value, 314 | ) 315 | } else { 316 | (rectangle.center.clone(), rectangle.fmin) 317 | }; 318 | 319 | // for each j, find wj = min(c - δ_ej, c + δ_ej) 320 | // indices 321 | let w_values = f_c_δ_e.map_axis(Axis(1), |x| { 322 | *x.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap() 323 | }); 324 | 325 | // Divide starting with the dimension with the smallest (best) wj 326 | let mut indices_that_sort_w = (0..w_values.len()).collect::>(); 327 | indices_that_sort_w 328 | .sort_unstable_by(|i, j| w_values[*i].partial_cmp(&w_values[*j]).unwrap()); 329 | 330 | // Each split divides into 3 rectangles. 331 | // Because we may have multiple splits, we keep prev_rectangle for splitting along subsequent wj. 332 | let mut prev_rectangle = rectangle; 333 | // The size shrinks after each iteration so we only need to search below a previously found size. 334 | let mut binary_search_upper_bound = rectangles_by_size.len(); 335 | 336 | for wj_index in indices_that_sort_w { 337 | let dim = indices[wj_index]; 338 | let mut bound_lengths = prev_rectangle.bound_lengths; 339 | bound_lengths[dim] /= 3; 340 | // Note: SQRT not necessary 341 | let mut size = Rational64::ZERO; 342 | for len in &bound_lengths { 343 | match self.size_metric { 344 | SizeMetric::L2 => size += (len / 2).pow(2), 345 | SizeMetric::L1 => size += len, 346 | SizeMetric::Linf => size = size.max(*len), 347 | } 348 | } 349 | 350 | // Insert left, right 351 | let left_and_right = (0..1).map(|k| Rectangle { 352 | bound_lengths: bound_lengths.clone(), 353 | size, 354 | center: c_δ_e.slice(s![wj_index, k, ..]).to_owned(), 355 | fmin: f_c_δ_e[[wj_index, k]], 356 | }); 357 | match rectangles_by_size[..binary_search_upper_bound] 358 | .binary_search_by(|g| g.size.cmp(&size)) 359 | { 360 | Ok(i) => { 361 | rectangles_by_size[i].rectangles.extend(left_and_right); 362 | binary_search_upper_bound = i + 1; 363 | } 364 | Err(i) => { 365 | rectangles_by_size.insert( 366 | i, 367 | Group { 368 | size, 369 | rectangles: BinaryHeap::from_iter(left_and_right), 370 | }, 371 | ); 372 | binary_search_upper_bound = i + 1; 373 | } 374 | } 375 | 376 | // Keep track of center for further splitting and re-insertion. 377 | prev_rectangle = Rectangle { 378 | bound_lengths, 379 | size, 380 | center: prev_rectangle.center, 381 | fmin: prev_rectangle.fmin, 382 | }; 383 | } 384 | 385 | // Put the center rectangle back in. 386 | match rectangles_by_size[..binary_search_upper_bound] 387 | .binary_search_by(|g| g.size.cmp(&prev_rectangle.size)) 388 | { 389 | Ok(i) => { 390 | rectangles_by_size[i].rectangles.push(prev_rectangle); 391 | } 392 | Err(i) => { 393 | rectangles_by_size.insert( 394 | i, 395 | Group { 396 | size: prev_rectangle.size, 397 | rectangles: BinaryHeap::from([prev_rectangle]), 398 | }, 399 | ); 400 | } 401 | } 402 | 403 | (split_xmin, split_fmin) 404 | } 405 | 406 | /// Convert a point from the hypercube range back into user range. 407 | fn denormalize_point(&self, hypercube_point: ArrayView1) -> Array1 { 408 | let mut denormalized = hypercube_point.mapv(|x| x.to_f64().unwrap()); 409 | azip!( 410 | (x in &mut denormalized, bound in &self.bounds) *x = *x * (bound[1] - bound[0]) + bound[0] 411 | ); 412 | denormalized 413 | } 414 | } 415 | 416 | /// DIRECT-restart 417 | #[derive(Debug)] 418 | struct AdaptiveEpsilon { 419 | enabled: bool, 420 | prefer_locality: bool, 421 | last_improved_iteration: usize, 422 | } 423 | 424 | impl AdaptiveEpsilon { 425 | fn new(enabled: bool) -> Self { 426 | Self { 427 | enabled, 428 | prefer_locality: true, 429 | last_improved_iteration: 0, 430 | } 431 | } 432 | 433 | fn value(&self) -> f64 { 434 | if !self.enabled { 435 | 1E-4 436 | } else { 437 | match self.prefer_locality { 438 | true => 0., 439 | false => 1E-2, 440 | } 441 | } 442 | } 443 | 444 | fn improved(&mut self, iteration: usize) { 445 | self.last_improved_iteration = iteration; 446 | self.prefer_locality = true; 447 | } 448 | 449 | fn no_improvement(&mut self, iteration: usize) { 450 | let num_consecutive_before_action = match self.prefer_locality { 451 | true => 5, 452 | false => 50, 453 | }; 454 | 455 | if iteration - self.last_improved_iteration >= num_consecutive_before_action { 456 | self.prefer_locality = !self.prefer_locality; 457 | } 458 | } 459 | } 460 | 461 | #[cfg(test)] 462 | mod test { 463 | use lyon_geom::euclid::default::Vector3D; 464 | use ndarray::Array; 465 | 466 | use super::Direct; 467 | use crate::{optimize::direct::SizeMetric, ColorModel}; 468 | 469 | #[test] 470 | fn test_direct() { 471 | let direct = Direct { 472 | function: |val| val[0].powi(2) + val[1].powi(2), 473 | bounds: Array::from_elem(2, [-10., 10.]), 474 | max_evaluations: Some(1000), 475 | max_iterations: None, 476 | adapt_epsilon: false, 477 | reduce_global_drag: false, 478 | size_metric: SizeMetric::L1, 479 | }; 480 | assert_eq!(direct.run().1, 0.); 481 | } 482 | 483 | #[test] 484 | fn test_direct_real() { 485 | // abL 486 | let implements: Vec> = vec![ 487 | Vector3D::from((-12.33001605954215, -45.54515542156117, 44.2098529479848)), 488 | Vector3D::from((27.880276413952384, -45.45097702564241, 79.59139231597462)), 489 | Vector3D::from((0.0, 0.0, 100.0)), 490 | Vector3D::from((25.872063973881424, -58.18583421858581, 77.27752311788944)), 491 | Vector3D::from((-26.443894809510233, 42.307075530106964, 52.73969688418173)), 492 | Vector3D::from((66.72215124694603, 8.94553594498204, 50.895965946274)), 493 | Vector3D::from((86.860821880907, -69.34347889122935, 46.303948069777704)), 494 | Vector3D::from((21.03625782707445, -63.798798964168235, 67.01735205659284)), 495 | Vector3D::from((-35.98529688090144, 11.606079999533165, 51.30132332650257)), 496 | Vector3D::from((62.596792812655295, 33.336563699816914, 55.46042775958594)), 497 | ]; 498 | let model = ColorModel::Cielab; 499 | // hue, chroma, darkness 500 | let desired = [1.4826900028611403, 5.177699004088122, 0.27727267822882595]; 501 | let direct = Direct { 502 | function: model.objective_function(desired, &implements), 503 | bounds: Array::from_elem(implements.len(), [0., 1.]), 504 | max_evaluations: Some(10_000), 505 | max_iterations: None, 506 | adapt_epsilon: false, 507 | reduce_global_drag: false, 508 | size_metric: SizeMetric::L1, 509 | }; 510 | let (res, cost) = direct.run(); 511 | let weighted_vector = implements 512 | .iter() 513 | .zip(res.iter()) 514 | .map(|(p, x)| *p * *x) 515 | .sum::>(); 516 | // Convert back to cylindrical model (hue, chroma, darkness) 517 | let actual = [ 518 | weighted_vector.y.atan2(weighted_vector.x), 519 | weighted_vector.to_2d().length(), 520 | weighted_vector.z, 521 | ]; 522 | dbg!(cost, &res, &actual, model.cylindrical_diff(desired, actual)); 523 | assert!(cost <= 3.0, "ciede2000 less than 3"); 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /src/voronoi/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Write}; 2 | use std::hash::Hash; 3 | use std::io::Write as IoWrite; 4 | 5 | use lyon_geom::{euclid::Vector2D, point, Angle, Line, LineSegment, Point, Vector}; 6 | use ndarray::{par_azip, prelude::*, IxDyn}; 7 | use num_traits::{FromPrimitive, PrimInt, Signed}; 8 | use rustc_hash::FxHashSet as HashSet; 9 | use tracing::info; 10 | 11 | use crate::{get_slice_info_for_offset, kbn_summation, math::abs_distance_squared}; 12 | 13 | /// Convex Hull 14 | pub mod hull; 15 | 16 | /// An arbitrary Voronoi site with up to N dimensions. 17 | pub trait Site { 18 | fn dist(&self, point: [T; N]) -> f64; 19 | fn seeds(&self, dimensions: &[usize; N]) -> Vec<[T; N]>; 20 | } 21 | 22 | /// 0D site (point) 23 | impl Site for [T; N] 24 | where 25 | T: PrimInt + Signed + FromPrimitive + Debug, 26 | { 27 | fn dist(&self, point: [T; N]) -> f64 { 28 | abs_distance_squared(*self, point).to_f64().unwrap() 29 | } 30 | fn seeds(&self, _dimensions: &[usize; N]) -> Vec<[T; N]> { 31 | vec![*self] 32 | } 33 | } 34 | 35 | pub struct AnnotatedSite { 36 | pub site: [T; N], 37 | pub annotation: U, 38 | } 39 | 40 | impl Site for AnnotatedSite 41 | where 42 | T: PrimInt + Signed + FromPrimitive + Debug, 43 | { 44 | fn dist(&self, point: [T; N]) -> f64 { 45 | abs_distance_squared(self.site, point).to_f64().unwrap() 46 | } 47 | fn seeds(&self, _dimensions: &[usize; N]) -> Vec<[T; N]> { 48 | vec![self.site] 49 | } 50 | } 51 | 52 | /// 1D site (line segment) 53 | impl Site<2, T> for LineSegment 54 | where 55 | T: PrimInt + FromPrimitive + Debug + Hash, 56 | { 57 | /// Straight line distance from p to the closest point on the line 58 | fn dist(&self, p: [T; 2]) -> f64 { 59 | let p = vertex_to_point(&p); 60 | let square_length = self.to_vector().square_length(); 61 | if square_length.abs() <= f64::EPSILON { 62 | (p - self.from).square_length(); 63 | } 64 | 65 | let t = ((p - self.from).dot(self.to - self.from) / square_length).clamp(0., 1.); 66 | 67 | (p - self.sample(t)).square_length() 68 | } 69 | 70 | /// All integer points close to the line 71 | fn seeds(&self, [width, height]: &[usize; 2]) -> Vec<[T; 2]> { 72 | let mut seeds = HashSet::default(); 73 | 74 | let width = (width - 1) as f64; 75 | let height = (height - 1) as f64; 76 | 77 | { 78 | let start_x = self.from.x.min(self.to.x).floor().clamp(0., width); 79 | let end_x = self.from.x.max(self.to.x).ceil().clamp(0., width); 80 | let mut x = start_x; 81 | while x < end_x { 82 | let y = self.solve_y_for_x(x).round().clamp(0., height); 83 | seeds.insert([T::from_f64(x).unwrap(), T::from_f64(y).unwrap()]); 84 | x += 1.; 85 | } 86 | } 87 | { 88 | let start_y = self.from.y.min(self.to.y).floor().clamp(0., height); 89 | let end_y = self.from.y.max(self.to.y).ceil().clamp(0., height); 90 | let mut y = start_y; 91 | while y < end_y { 92 | let x = self.solve_x_for_y(y).round().clamp(0., width); 93 | seeds.insert([T::from_f64(x).unwrap(), T::from_f64(y).unwrap()]); 94 | y += 1.; 95 | } 96 | } 97 | 98 | seeds.into_iter().collect() 99 | } 100 | } 101 | 102 | /// Given a set of sites in a bounding box from (0, 0) to (width, height), 103 | /// return the assignment of coordinates in that box to their nearest neighbor 104 | /// using the Jump Flooding Algorithm. 105 | /// 106 | /// Colorless cells will be usize::MAX 107 | /// 108 | /// Specifically, this is described in 109 | pub fn jump_flooding_voronoi< 110 | S: Site<2, T> + Send + Sync, 111 | T: PrimInt + FromPrimitive + Debug + Send + Sync, 112 | >( 113 | sites: &[S], 114 | dimensions: [usize; 2], 115 | ) -> Array { 116 | const N: usize = 2; 117 | if N != 2 { 118 | unimplemented!(); 119 | } 120 | // use usize::MAX to represent colorless cells 121 | let mut grid = Array::from_elem(IxDyn(&dimensions), usize::MAX); 122 | 123 | if sites.is_empty() { 124 | return grid; 125 | } 126 | 127 | // Prime JFA with seeds 128 | sites.iter().enumerate().for_each(|(color, site)| { 129 | for seed in site.seeds(&dimensions) { 130 | let mut index = [0usize; N]; 131 | for i in 0..N { 132 | index[i] = seed[i].to_usize().unwrap(); 133 | } 134 | grid[index.as_slice()] = color; 135 | } 136 | }); 137 | 138 | // Needed to parallelize JFA 139 | let width = dimensions[0]; 140 | let height = dimensions[1]; 141 | let positions = Array::from_iter((0..width).flat_map(|x| { 142 | (0..height).map(move |y| [T::from_usize(x).unwrap(), T::from_usize(y).unwrap()]) 143 | })) 144 | .into_shape_clone((width, height)) 145 | .unwrap(); 146 | 147 | let mut scratchpad = grid.clone(); 148 | 149 | // First round is of size n/2 where n is a power of 2 150 | let max_dim = dimensions.iter().max().unwrap(); 151 | let mut round_step = max_dim 152 | .checked_next_power_of_two() 153 | .map(|x| x / 2) 154 | .unwrap_or_else(|| (max_dim / 2).next_power_of_two()); 155 | 156 | while round_step != 0 { 157 | // Each grid point passes its contents on to (x+i, y+j) where i,j in {-round_step, 0, round_step} 158 | 159 | // This might look a bit weird... but it's JFA in parallel. 160 | // For each i,j it runs the jump on the entire image at once 161 | // 162 | // This works because JFA is linear: 163 | // If x,y will propagate to x+k,y+k, then x+1,y+1 will propagate to x+1+k,y+1+k 164 | for y_dir in -1..=1 { 165 | for x_dir in -1..=1 { 166 | let center_slice_info = get_slice_info_for_offset( 167 | -x_dir * round_step as i32, 168 | -y_dir * round_step as i32, 169 | ); 170 | let kernel_slice_info = 171 | get_slice_info_for_offset(x_dir * round_step as i32, y_dir * round_step as i32); 172 | par_azip! { 173 | (dest in scratchpad.slice_mut(center_slice_info), sample in grid.slice(kernel_slice_info), here in positions.slice(center_slice_info)) { 174 | *dest =if *dest == usize::MAX { 175 | *sample 176 | } else if *sample == usize::MAX { 177 | *dest 178 | } else if sites[*sample].dist(*here) < sites[*dest].dist(*here) { 179 | *sample 180 | } else { 181 | *dest 182 | }; 183 | } 184 | }; 185 | } 186 | } 187 | grid.assign(&scratchpad); 188 | round_step /= 2; 189 | } 190 | 191 | grid 192 | } 193 | 194 | /// Converts a JFA-assigned color grid into a list of points assigned to each site 195 | pub fn colors_to_assignments, T: PrimInt + FromPrimitive + Debug>( 196 | sites: &[S], 197 | grid: ArrayView, 198 | ) -> Vec> { 199 | if sites.is_empty() { 200 | return vec![]; 201 | } 202 | let expected_assignment_capacity = grid.len() / sites.len(); 203 | let mut sites_to_points = vec![Vec::with_capacity(expected_assignment_capacity); sites.len()]; 204 | grid.indexed_iter().for_each(|(idx, site)| { 205 | let mut t_idx = [T::zero(); N]; 206 | for i in 0..N { 207 | t_idx[i] = T::from_usize(idx[i]).unwrap(); 208 | } 209 | sites_to_points[*site].push(t_idx); 210 | }); 211 | sites_to_points 212 | } 213 | 214 | /// First and second order moments of a Voronoi cell 215 | #[derive(Default, Debug)] 216 | pub struct Moments { 217 | /// Sum of the values of points in the cell 218 | pub m00: f64, 219 | /// First order x moment 220 | pub m10: f64, 221 | /// First order y moment 222 | pub m01: f64, 223 | /// Second order xx central moment 224 | pub μ20: f64, 225 | /// Second order yy central moment 226 | pub μ02: f64, 227 | /// Second order xy central moment 228 | pub μ11: f64, 229 | /// Calculated centroid, may be `NaN` 230 | centroid: Point, 231 | } 232 | 233 | pub fn calculate_density(image: ArrayView2, points: &[[T; 2]]) -> f64 { 234 | if points.is_empty() { 235 | return 0.; 236 | } 237 | kbn_summation! { 238 | for [x, y] in points => { 239 | 'loop: { 240 | let x = x.to_usize().unwrap(); 241 | let y = y.to_usize().unwrap(); 242 | let value = image[[x, y]]; 243 | } 244 | m00 += value; 245 | } 246 | } 247 | m00 / points.len() as f64 248 | } 249 | 250 | pub fn calculate_centroid( 251 | image: ArrayView2, 252 | points: &[[T; 2]], 253 | ) -> Option> { 254 | kbn_summation! { 255 | for [x, y] in points => { 256 | 'loop: { 257 | let x = x.to_usize().unwrap(); 258 | let y = y.to_usize().unwrap(); 259 | let value = image[[x, y]]; 260 | let x = x as f64; 261 | let y = y as f64; 262 | } 263 | m00 += value; 264 | m10 += x * value; 265 | m01 += y * value; 266 | } 267 | } 268 | if m00 < f64::EPSILON { 269 | None 270 | } else { 271 | Some(point(m10 / m00, m01 / m00)) 272 | } 273 | } 274 | 275 | /// Hiller et al. Section 4 + Appendix B 276 | #[inline] 277 | fn calculate_moments(image: ArrayView2, points: &[[T; 2]]) -> Moments { 278 | let mut moments = Moments::default(); 279 | kbn_summation! { 280 | for [x, y] in points => { 281 | 'loop: { 282 | let x = x.to_usize().unwrap(); 283 | let y = y.to_usize().unwrap(); 284 | let value = image[[x, y]]; 285 | let x = x as f64; 286 | let y = y as f64; 287 | } 288 | m00 += value; 289 | m10 += x * value; 290 | m01 += y * value; 291 | } 292 | } 293 | moments.m00 = m00; 294 | moments.m10 = m10; 295 | moments.m01 = m01; 296 | 297 | // Hiller et al. Appendix B mass centroid 298 | let centroid = point(moments.m10 / moments.m00, moments.m01 / moments.m00); 299 | moments.centroid = centroid; 300 | 301 | // Hiller et al. Appendix B central moments 302 | kbn_summation! { 303 | for [x, y] in points => { 304 | 'loop: { 305 | let x = x.to_usize().unwrap(); 306 | let y = y.to_usize().unwrap(); 307 | let value = image[[x, y]]; 308 | let x = x as f64; 309 | let y = y as f64; 310 | } 311 | μ20 += (x - centroid.x).powi(2) * value; 312 | μ02 += (y - centroid.y).powi(2) * value; 313 | μ11 += (x - centroid.x) * (y - centroid.y) * value; 314 | } 315 | } 316 | moments.μ20 = μ20; 317 | moments.μ02 = μ02; 318 | moments.μ11 = μ11; 319 | 320 | moments 321 | } 322 | 323 | /// Deussen et al 3.3 Beyond Stippling 324 | #[derive(Default, Debug)] 325 | pub struct CellProperties { 326 | pub moments: Moments, 327 | /// Density-based center of the cell 328 | pub centroid: Option>, 329 | /// Orientation of cell's inertial axis 330 | pub phi_vector: Option>, 331 | /// Convex hull enclosing the cell 332 | pub hull: Option>, 333 | /// Used to determine the splitting direction 334 | pub phi_oriented_segment_through_centroid: Option>, 335 | } 336 | 337 | impl CellProperties { 338 | /// Calculate properties of a Voronoi cell 339 | /// 340 | /// Points must be sorted by x-coordinate and tie-broken by y-coordinate 341 | pub fn calculate(image: ArrayView2, points: &[[T; 2]]) -> Self { 342 | let mut cell_properties = Self { 343 | moments: calculate_moments(image, points), 344 | ..Default::default() 345 | }; 346 | 347 | let moments = &cell_properties.moments; 348 | 349 | // Any calculation here is pointless 350 | if moments.m00 <= f64::EPSILON { 351 | return cell_properties; 352 | } 353 | let centroid = moments.centroid; 354 | cell_properties.centroid = Some(moments.centroid); 355 | 356 | // Hiller et al. Appendix B Equation 5 357 | let phi = Angle::radians(0.5 * (2.0 * moments.μ11).atan2(moments.μ20 - moments.μ02)); 358 | let phi_vector = Vector2D::from_angle_and_length(phi, 1.); 359 | cell_properties.phi_vector = Some(phi_vector); 360 | 361 | const HULL_EPSILON: f64 = 1e-8; 362 | 363 | if points.len() >= 3 { 364 | let hull = hull::convex_hull(points); 365 | let vertex_it = hull.iter().map(vertex_to_point); 366 | let edges_it = vertex_it 367 | .clone() 368 | .zip(vertex_it.clone().skip(1)) 369 | .chain( 370 | hull.last() 371 | .filter(|_| hull.len() > 1) 372 | .map(vertex_to_point) 373 | .zip(hull.first().map(vertex_to_point)), 374 | ) 375 | .map(|(from, to)| LineSegment { from, to }); 376 | 377 | // Hull may not be valid if the points are colinear 378 | if hull.len() >= 3 { 379 | let phi_line = Line { 380 | point: centroid, 381 | vector: phi_vector, 382 | }; 383 | 384 | let centroid_is_vertex = vertex_it 385 | .clone() 386 | .any(|vertex| vertex.distance_to(centroid) < HULL_EPSILON); 387 | let centroid_on_edge = edges_it 388 | .clone() 389 | .any(|edge| edge.to_line().distance_to_point(¢roid) < HULL_EPSILON); 390 | 391 | let mut edge_intersections_with_phi_line = edges_it 392 | .clone() 393 | .filter_map(|edge| { 394 | edge.line_intersection(&phi_line).or_else(|| { 395 | // Edge case where line segments are not inclusive of their endpoints 396 | if phi_line.distance_to_point(&edge.to) < HULL_EPSILON { 397 | Some(edge.to) 398 | } else { 399 | None 400 | } 401 | }) 402 | }) 403 | .collect::>(); 404 | 405 | // We may see more than 2 intersections if: 406 | // * Phi line intersects voronoi cell at a vertex, thus intersecting two edges of its hull 407 | // * Floating point accuracy problems 408 | edge_intersections_with_phi_line.sort_by(|a, b| { 409 | a.x.partial_cmp(&b.x) 410 | .unwrap() 411 | .then(a.y.partial_cmp(&b.y).unwrap()) 412 | }); 413 | edge_intersections_with_phi_line.dedup_by(|a, b| a.distance_to(*b) < HULL_EPSILON); 414 | edge_intersections_with_phi_line.sort_by(|a, b| { 415 | a.y.partial_cmp(&b.y) 416 | .unwrap() 417 | .then(a.x.partial_cmp(&b.x).unwrap()) 418 | }); 419 | edge_intersections_with_phi_line.dedup_by(|a, b| a.distance_to(*b) < HULL_EPSILON); 420 | 421 | cell_properties.phi_oriented_segment_through_centroid = Some( 422 | match ( 423 | centroid_is_vertex, 424 | centroid_on_edge, 425 | edge_intersections_with_phi_line.as_slice(), 426 | ) { 427 | (_, _, [left, right]) => LineSegment { 428 | from: *left, 429 | to: *right, 430 | }, 431 | (false, false, other) => { 432 | edges_it.clone().for_each(|edge| { 433 | dbg!(edge.to_line().distance_to_point(¢roid)); 434 | }); 435 | to_svg(&hull, centroid, phi_vector, other); 436 | if other.len() >= 2 { 437 | LineSegment { 438 | from: other[0], 439 | to: other[1], 440 | } 441 | } else { 442 | unreachable!() 443 | } 444 | } 445 | (true, false, other) => { 446 | to_svg(&hull, centroid, phi_vector, other); 447 | unreachable!() 448 | } 449 | (false, true, other) | (true, true, other) => { 450 | // Centroid is at a vertex or on an edge. 451 | // Try to expand the segment to maximal length. 452 | let mut vertices_on_phi_line = vertex_it 453 | .clone() 454 | .filter(|v| phi_line.distance_to_point(v) < HULL_EPSILON) 455 | .collect::>(); 456 | vertices_on_phi_line.sort_by(|a, b| { 457 | a.x.partial_cmp(&b.x) 458 | .unwrap() 459 | .then(a.y.partial_cmp(&b.y).unwrap()) 460 | }); 461 | match vertices_on_phi_line.as_slice() { 462 | [] => { 463 | to_svg(&hull, centroid, phi_vector, other); 464 | unreachable!() 465 | } 466 | [single] => LineSegment { 467 | from: *single, 468 | to: *single, 469 | }, 470 | [first, .., last] => LineSegment { 471 | from: *first, 472 | to: *last, 473 | }, 474 | } 475 | } 476 | }, 477 | ); 478 | cell_properties.hull = Some(hull); 479 | } 480 | } else { 481 | let radius = (points.len() as f64 / std::f64::consts::PI).sqrt(); 482 | cell_properties.phi_oriented_segment_through_centroid = Some(LineSegment { 483 | from: centroid + phi_vector * radius, 484 | to: centroid - phi_vector * radius, 485 | }); 486 | }; 487 | 488 | cell_properties 489 | } 490 | } 491 | 492 | /// Debugging function for visualizing [calculate_cell_properties] 493 | fn to_svg( 494 | hull: &[[T; 2]], 495 | centroid: Point, 496 | phi_vector: Vector, 497 | edge_intersections_with_phi_line: &[Point], 498 | ) { 499 | let mut path = String::new(); 500 | if let Some(point) = hull.first() { 501 | write!(path, "M{:?},{:?} ", point[0], point[1]).unwrap(); 502 | } 503 | if hull.len() >= 2 { 504 | path += "L"; 505 | } 506 | for point in hull.iter().skip(1) { 507 | write!(path, "{:?},{:?} ", point[0], point[1]).unwrap(); 508 | } 509 | if hull.len() >= 3 { 510 | path += "Z"; 511 | } 512 | 513 | let [cx, cy] = centroid.to_array(); 514 | let [p1x, p1y] = (centroid + phi_vector * -10.).to_array(); 515 | let [p2x, p2y] = (centroid + phi_vector * 10.).to_array(); 516 | let mut minx = p1x.min(p2x); 517 | let mut miny = p1y.min(p2y); 518 | let mut maxx = p1x.max(p2x); 519 | let mut maxy = p1y.max(p2y); 520 | for point in hull { 521 | maxx = maxx.max(point[0].to_usize().unwrap() as f64); 522 | maxy = maxy.max(point[1].to_usize().unwrap() as f64); 523 | minx = minx.min(point[0].to_usize().unwrap() as f64); 524 | miny = miny.min(point[1].to_usize().unwrap() as f64); 525 | } 526 | minx -= 2.; 527 | miny -= 2.; 528 | maxx += 2.; 529 | maxy += 2.; 530 | let mut f = 531 | std::fs::File::create(format!("/tmp/foo{},{}.svg", centroid.x, centroid.y)).unwrap(); 532 | info!( 533 | "This is weird /tmp/foo{},{}.svg {}", 534 | centroid.x, 535 | centroid.y, 536 | edge_intersections_with_phi_line.len() 537 | ); 538 | let mut intersections = String::default(); 539 | for Point { x: tox, y: toy, .. } in edge_intersections_with_phi_line { 540 | write!( 541 | intersections, 542 | r#""# 543 | ) 544 | .unwrap(); 545 | } 546 | let width = maxx - minx; 547 | let height = maxy - miny; 548 | write!( 549 | f, 550 | r#" 551 | 552 | 553 | 554 | {intersections} 555 | 556 | "#, 557 | ).unwrap(); 558 | } 559 | 560 | /// Convenience function for making a float point from an integer point 561 | fn vertex_to_point(vertex: &[T; 2]) -> Point { 562 | point(vertex[0].to_f64().unwrap(), vertex[1].to_f64().unwrap()) 563 | } 564 | 565 | #[cfg(test)] 566 | mod tests { 567 | use super::jump_flooding_voronoi; 568 | use crate::math::abs_distance_squared; 569 | 570 | #[test] 571 | fn test_jump_flooding_voronoi() { 572 | const WIDTH: usize = 256; 573 | const HEIGHT: usize = 256; 574 | let sites = [ 575 | [0, 0], 576 | [0, HEIGHT as i64 - 1], 577 | [WIDTH as i64 - 1, 0], 578 | [WIDTH as i64 - 1, HEIGHT as i64 - 1], 579 | [WIDTH as i64 / 2, HEIGHT as i64 / 2], 580 | ]; 581 | let assignments = jump_flooding_voronoi(&sites, [WIDTH, HEIGHT]); 582 | for j in 0..HEIGHT { 583 | for i in 0..WIDTH { 584 | let min_distance = sites 585 | .iter() 586 | .map(|site| abs_distance_squared(*site, [i as i64, j as i64])) 587 | .min() 588 | .unwrap(); 589 | let actual_distance = 590 | abs_distance_squared(sites[assignments[[i, j]]], [i as i64, j as i64]); 591 | 592 | // Don't check the assigned site because of distance ties 593 | assert_eq!(min_distance, actual_distance); 594 | } 595 | } 596 | } 597 | } 598 | -------------------------------------------------------------------------------- /src/graph/tsp.rs: -------------------------------------------------------------------------------- 1 | use crate::math::abs_distance_squared; 2 | use num_traits::{FromPrimitive, PrimInt, Signed}; 3 | use rand::{distributions::Standard, prelude::Distribution, thread_rng, Rng}; 4 | use rayon::prelude::*; 5 | use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; 6 | use std::{ 7 | collections::VecDeque, 8 | fmt::Debug, 9 | hash::{BuildHasherDefault, Hash}, 10 | iter::Sum, 11 | }; 12 | use tracing::{debug, info}; 13 | 14 | #[derive(PartialEq, Eq, Debug)] 15 | struct Edge([[T; 2]; 2]); 16 | 17 | impl Edge { 18 | fn length(&self) -> T { 19 | abs_distance_squared(self.0[0], self.0[1]) 20 | } 21 | } 22 | 23 | impl PartialOrd for Edge { 24 | fn partial_cmp(&self, other: &Self) -> Option { 25 | Some(self.cmp(other)) 26 | } 27 | } 28 | 29 | impl Ord for Edge { 30 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 31 | self.length().cmp(&other.length()) 32 | } 33 | } 34 | 35 | /// Approximate an open loop TSP solution by way of greedy branch elimination + local improvement. 36 | /// 37 | /// 38 | /// 39 | /// See the two source code comments prefixed with `NOTE:` 40 | /// for how to use the non-greedy all pairs branch elimination 41 | /// variant, which takes significantly longer. 42 | /// 43 | /// TODO: consider using the Christofides algorithm which has a bound on the length of the worst path. 44 | /// The initial solution of greedy branch elimination is actually quite bad. 45 | pub fn approximate_tsp_with_mst< 46 | T: PrimInt 47 | + Signed 48 | + FromPrimitive 49 | + Eq 50 | + PartialEq 51 | + PartialOrd 52 | + Ord 53 | + Hash 54 | + Debug 55 | + Sum 56 | + Send 57 | + Sync, 58 | >( 59 | vertices: &[[T; 2]], 60 | tree: &[[[T; 2]; 2]], 61 | ) -> Vec<[T; 2]> { 62 | if vertices.len() <= 1 { 63 | return vec![]; 64 | } else if vertices.len() == 2 { 65 | return vertices.to_vec(); 66 | } 67 | let path = approximate_tsp_with_mst_greedy(vertices, tree); 68 | local_improvement_with_tabu_search::<_, false>(&path) 69 | } 70 | 71 | fn approximate_tsp_with_mst_greedy< 72 | T: PrimInt 73 | + Signed 74 | + FromPrimitive 75 | + Eq 76 | + PartialEq 77 | + PartialOrd 78 | + Ord 79 | + Hash 80 | + Debug 81 | + Sum 82 | + Send 83 | + Sync, 84 | >( 85 | vertices: &[[T; 2]], 86 | tree: &[[[T; 2]; 2]], 87 | ) -> Vec<[T; 2]> { 88 | let mut adjacency_map: Vec> = 89 | vec![HashSet::with_capacity_and_hasher(1, BuildHasherDefault::default()); vertices.len()]; 90 | { 91 | let vertex_to_index = vertices 92 | .iter() 93 | .copied() 94 | .enumerate() 95 | .map(|(i, vertex)| (vertex, i)) 96 | .collect::>(); 97 | tree.iter().for_each(|edge| { 98 | adjacency_map[vertex_to_index[&edge[0]]].insert(vertex_to_index[&edge[1]]); 99 | adjacency_map[vertex_to_index[&edge[1]]].insert(vertex_to_index[&edge[0]]); 100 | }); 101 | } 102 | 103 | let mut branch_list = adjacency_map 104 | .iter() 105 | .enumerate() 106 | .filter(|(_, adjacencies)| adjacencies.len() >= 3) 107 | .flat_map(|(branch, adjacencies)| { 108 | adjacencies 109 | .iter() 110 | .copied() 111 | .map(move |adjacency| (branch, adjacency)) 112 | }) 113 | .collect::>(); 114 | branch_list.sort_unstable_by_key(|(branch, adjacency)| { 115 | Edge([vertices[*branch], vertices[*adjacency]]) 116 | }); 117 | 118 | while let Some((branch, disconnected_node)) = branch_list.pop() { 119 | debug!( 120 | "Approximation progress: {} remaining branches", 121 | branch_list.len() 122 | ); 123 | // No longer a branching vertex 124 | if adjacency_map[branch].len() <= 2 { 125 | continue; 126 | } 127 | // This branching edge doesn't exist anymore. 128 | // The other end is a branch that was already processed. 129 | // that has already been processed. 130 | if !adjacency_map[branch].contains(&disconnected_node) { 131 | continue; 132 | } 133 | 134 | // Remove edge 135 | adjacency_map[branch].remove(&disconnected_node); 136 | adjacency_map[disconnected_node].remove(&branch); 137 | 138 | // Now there are two disconnected trees, 139 | // do a BFS to find the leaves in both. 140 | let (disconnected_tree_leaves, branch_tree_leaves) = { 141 | let mut disconnected_leaves = vec![]; 142 | let mut branch_leaves = vec![]; 143 | 144 | let mut disconnected_bfs = VecDeque::from([(disconnected_node, branch)]); 145 | let mut branch_bfs = VecDeque::from([(branch, disconnected_node)]); 146 | loop { 147 | let it = disconnected_bfs 148 | .pop_front() 149 | .map(|state| (state, &mut disconnected_leaves, &mut disconnected_bfs)) 150 | .into_iter() 151 | .chain( 152 | branch_bfs 153 | .pop_front() 154 | .map(|state| (state, &mut branch_leaves, &mut branch_bfs)) 155 | .into_iter(), 156 | ); 157 | 158 | for ((head, source), leaves, bfs) in it { 159 | // Handles the first vertex 160 | if adjacency_map[head].len() <= 1 { 161 | leaves.push(head); 162 | } 163 | let non_leaf_adjacencies = adjacency_map[head] 164 | .iter() 165 | .copied() 166 | // Optimization: we only need to make sure no backtracking happens since this 167 | // is a tree. 168 | .filter(|adj| *adj != source) 169 | .filter_map(|adj| { 170 | let adj_adj = &adjacency_map[adj]; 171 | if adj_adj.len() <= 1 { 172 | leaves.push(adj); 173 | debug_assert_eq!(*adj_adj.iter().next().unwrap(), head); 174 | None 175 | } else { 176 | Some((adj, head)) 177 | } 178 | }); 179 | bfs.extend(non_leaf_adjacencies); 180 | } 181 | 182 | if disconnected_bfs.is_empty() && branch_bfs.is_empty() { 183 | break (disconnected_leaves, branch_leaves); 184 | } 185 | } 186 | }; 187 | 188 | // Pick the shortest possible link between two leaves that would reconnect the trees 189 | let (disconnected_tree_leaf, branch_tree_leaf) = disconnected_tree_leaves 190 | .into_par_iter() 191 | .flat_map(|i| { 192 | branch_tree_leaves 193 | .clone() 194 | .into_par_iter() 195 | .map(move |j| (i, j)) 196 | }) 197 | .min_by_key(|(i, j)| abs_distance_squared(vertices[*i], vertices[*j])) 198 | .unwrap(); 199 | 200 | // Connect leaves 201 | adjacency_map[branch_tree_leaf].insert(disconnected_tree_leaf); 202 | adjacency_map[disconnected_tree_leaf].insert(branch_tree_leaf); 203 | } 204 | 205 | // Extract path from the adjacency list 206 | let mut path = Vec::with_capacity(vertices.len()); 207 | let (first_vertex, adjacencies) = adjacency_map 208 | .iter() 209 | .enumerate() 210 | .find(|(_, adjacencies)| adjacencies.len() == 1) 211 | .expect("path always has a first vertex"); 212 | path.push(first_vertex); 213 | path.push(*adjacencies.iter().next().unwrap()); 214 | 215 | // The number of edges in an open loop TSP path is equal to the number of vertices - 1 216 | while path.len() < vertices.len() { 217 | let last_vertex = *path.last().unwrap(); 218 | let second_to_last_vertex = *path.iter().rev().nth(1).unwrap(); 219 | let next_vertex = adjacency_map[last_vertex] 220 | .iter() 221 | .find(|adjacency| **adjacency != second_to_last_vertex) 222 | .unwrap(); 223 | path.push(*next_vertex); 224 | } 225 | 226 | path.into_iter().map(|i| vertices[i]).collect() 227 | } 228 | 229 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 230 | enum Operator { 231 | /// Move vertex between other vertices 232 | Relocate, 233 | /// Swap vertices between two edges 234 | /// 235 | /// This does not implement the paper's version of 2-opt where 236 | /// the terminal nodes are linked to dummy terminals. 237 | TwoOpt, 238 | /// Change the beginning and/or end of the path by swapping an edge 239 | /// 240 | /// In the words of the paper: 241 | /// > Link swap is a special case of 3–opt and relocate operator, but as the size of the neighborhood is linear, 242 | /// > it is a faster operation than both 3–opt and relocate operator. 243 | LinkSwap, 244 | } 245 | 246 | impl Operator { 247 | const NUM_OPERATORS: usize = 3; 248 | } 249 | 250 | impl Distribution for Standard { 251 | /// Based on productivity results in the paper, link swap is given a chance of 50% while relocate and 2-opt have 25% each 252 | fn sample(&self, rng: &mut R) -> Operator { 253 | match rng.gen_range(0..=3) { 254 | 0 => Operator::Relocate, 255 | 1 => Operator::TwoOpt, 256 | 2 | 3 => Operator::LinkSwap, 257 | _ => unreachable!(), 258 | } 259 | } 260 | } 261 | 262 | /// Local improvement of an open loop TSP solution using the relocate, disentangle, 2-opt, and link swap operators. 263 | /// Tabu search is used to avoid getting stuck early in local minima. 264 | /// 265 | /// 266 | /// 267 | fn local_improvement_with_tabu_search< 268 | T: PrimInt 269 | + Signed 270 | + FromPrimitive 271 | + Eq 272 | + PartialEq 273 | + PartialOrd 274 | + Ord 275 | + Hash 276 | + Debug 277 | + Sum 278 | + Send 279 | + Sync, 280 | const SHOULD_SAMPLE: bool, 281 | >( 282 | path: &[[T; 2]], 283 | ) -> Vec<[T; 2]> { 284 | let mut best = path.to_owned(); 285 | let mut best_sum = best 286 | .iter() 287 | .zip(best.iter().skip(1)) 288 | .map(|(from, to)| abs_distance_squared(*from, *to)) 289 | .sum::(); 290 | 291 | let mut current = best.clone(); 292 | let mut current_distances = current 293 | .iter() 294 | .zip(current.iter().skip(1)) 295 | .map(|(from, to)| abs_distance_squared(*from, *to)) 296 | .collect::>(); 297 | let mut current_sum = best_sum; 298 | 299 | let sample_count = (current.len() as f64 * 0.001) as usize; 300 | const ITERATIONS: usize = 20000; 301 | 302 | let mut rng = thread_rng(); 303 | 304 | /// 10% of the past moves are considered tabu 305 | const TABU_FRACTION: f64 = 0.1; 306 | let tabu_capacity = (current.len() as f64 * TABU_FRACTION) as usize; 307 | let mut tabu: VecDeque = VecDeque::with_capacity(tabu_capacity); 308 | let mut tabu_set: HashSet = HashSet::default(); 309 | tabu_set.reserve(tabu_capacity); 310 | 311 | let mut stuck_operators = HashSet::with_capacity_and_hasher(3, BuildHasherDefault::default()); 312 | 313 | for idx in 0..ITERATIONS { 314 | if stuck_operators.len() == Operator::NUM_OPERATORS && !SHOULD_SAMPLE { 315 | if tabu.is_empty() { 316 | info!("Stuck, no more local improvements can be made"); 317 | break; 318 | } else { 319 | // Try to unstick by clearing tabu 320 | tabu.clear(); 321 | tabu_set.clear(); 322 | stuck_operators.clear(); 323 | } 324 | } 325 | 326 | let operator: Operator = rng.gen(); 327 | 328 | match operator { 329 | // O(v^2) 330 | Operator::Relocate => { 331 | // Which i should be considered for relocation (i in [1, N-2]) 332 | let relocates = if SHOULD_SAMPLE { 333 | 0..sample_count 334 | } else { 335 | 1..current.len().saturating_sub(1) 336 | } 337 | .into_par_iter() 338 | .map(|i| { 339 | if SHOULD_SAMPLE { 340 | thread_rng().gen_range(1..current.len().saturating_sub(1)) 341 | } else { 342 | i 343 | } 344 | }); 345 | 346 | // move i between j and j+1 347 | let best = relocates 348 | .filter(|i| !tabu_set.contains(i)) 349 | .flat_map(|i| { 350 | // pre-computed to save time, 351 | // relies on triangle property to avoid overflow: 352 | // distance from i-1 --> i --> i+1 is strictly greater than 353 | // distance from i-1 --> i+1 354 | let unlink_i_improvement = (current_distances[i - 1] 355 | + current_distances[i]) 356 | - abs_distance_squared(current[i - 1], current[i + 1]); 357 | // j must be in [0, i-2] U [i+1, N-1] for the move to be valid 358 | (0..i.saturating_sub(1)) 359 | .into_par_iter() 360 | .chain( 361 | (i.saturating_add(1)..current.len().saturating_sub(1)) 362 | .into_par_iter(), 363 | ) 364 | .map(move |j| (i, j, unlink_i_improvement)) 365 | }) 366 | .map(|(i, j, unlink_i_improvement)| { 367 | // Old distances - pre-computed new distance: (j=>j+1, i-1=>i, i=>i+1) - i-1=>i+1 368 | let positive_diff = current_distances[j] + unlink_i_improvement; 369 | // New distances: j=>i, i=>j+1 370 | let negative_diff = abs_distance_squared(current[j], current[i]) 371 | + abs_distance_squared(current[i], current[j + 1]); 372 | (i, j, positive_diff.saturating_sub(negative_diff)) 373 | }) 374 | .max_by_key(|(.., diff)| *diff); 375 | 376 | if let Some((i, j, diff)) = best { 377 | if diff <= T::zero() { 378 | stuck_operators.insert(operator); 379 | continue; 380 | } else { 381 | stuck_operators.clear(); 382 | } 383 | let vertex = current[i]; 384 | if j < i { 385 | // j is before i in the path 386 | // shift to the right and insert vertex 387 | for idx in (j + 1..i).rev() { 388 | current[idx + 1] = current[idx]; 389 | } 390 | current[j + 1] = vertex; 391 | tabu.push_back(j + 1); 392 | tabu_set.insert(j + 1); 393 | } else { 394 | // j is after in the path 395 | // shift to the left and insert vertex 396 | for idx in i..j { 397 | current[idx] = current[idx + 1]; 398 | } 399 | current[j] = vertex; 400 | tabu.push_back(j); 401 | tabu_set.insert(j); 402 | } 403 | } else { 404 | stuck_operators.insert(operator); 405 | continue; 406 | } 407 | } 408 | // O(v^2) 409 | Operator::TwoOpt => { 410 | let edges = if SHOULD_SAMPLE { 411 | 0..sample_count 412 | } else { 413 | 0..current.len().saturating_sub(1) 414 | } 415 | .into_par_iter() 416 | .map(|i| { 417 | if SHOULD_SAMPLE { 418 | thread_rng().gen_range(0..current.len().saturating_sub(1)) 419 | } else { 420 | i 421 | } 422 | }) 423 | .map(|i| (i, i + 1)); 424 | // permute the points of the this and other edges 425 | let best = edges 426 | .flat_map(|(i, j)| { 427 | (1..i) 428 | .into_par_iter() 429 | .map(move |other_j| { 430 | let other_i = other_j - 1; 431 | (other_i, other_j) 432 | }) 433 | // Note that other and (i,j) are swapped so that 434 | // the first edge is always before the second edge 435 | .map(move |other| (other, (i, j))) 436 | // If we aren't sampling, it is pointless to use these because 437 | // we already saw them. 438 | .filter(|_| SHOULD_SAMPLE) 439 | .chain( 440 | (j.saturating_add(2)..current.len()) 441 | .into_par_iter() 442 | .map(move |other_j| { 443 | let other_i = other_j - 1; 444 | (other_i, other_j) 445 | }) 446 | .map(move |other| ((i, j), other)), 447 | ) 448 | }) 449 | .filter(|(this, other)| { 450 | !tabu_set.contains(&this.1) && !tabu_set.contains(&other.0) 451 | }) 452 | // Examine all edge pairs 453 | .map(|(this, other)| { 454 | ( 455 | this, 456 | other, 457 | // Lose this.0=>this.1 & other.0=>other.1 458 | // Gain this.0=>other.0 & this.1=>other.1 459 | (current_distances[this.0] + current_distances[other.0]) 460 | .saturating_sub( 461 | abs_distance_squared(current[this.0], current[other.0]) 462 | + abs_distance_squared(current[this.1], current[other.1]), 463 | ), 464 | ) 465 | }) 466 | .max_by_key(|(.., diff)| *diff); 467 | 468 | if let Some((this, other, diff)) = best { 469 | if diff <= T::zero() { 470 | stuck_operators.insert(operator); 471 | continue; 472 | } else { 473 | stuck_operators.clear(); 474 | } 475 | let tabu_add = [this.1, other.0]; 476 | tabu.extend(tabu_add); 477 | tabu_set.extend(tabu_add); 478 | // Reversing in-place maintains inner links, but swaps outer links 479 | current[this.1..=other.0].reverse(); 480 | } else { 481 | stuck_operators.insert(operator); 482 | continue; 483 | } 484 | } 485 | // O(v) 3*(v-1) 486 | Operator::LinkSwap => { 487 | let first = *current.first().unwrap(); 488 | let last = *current.last().unwrap(); 489 | 490 | // Change from=>to to one of from=>last, first=>to, or first=>last 491 | let best = (2..current.len().saturating_sub(1)) 492 | .into_par_iter() 493 | .map(|j| { 494 | let i = j - 1; 495 | (i, j) 496 | }) 497 | .filter(|(i, j)| !tabu_set.contains(i) && !tabu_set.contains(j)) 498 | .map(|(i, j)| { 499 | let from = current[i]; 500 | let to = current[j]; 501 | [[from, last], [first, to], [first, last]] 502 | .iter() 503 | .map(|new_edge| { 504 | let diff = current_distances[i] 505 | .saturating_sub(abs_distance_squared(new_edge[0], new_edge[1])); 506 | (i, j, [from, to], *new_edge, diff) 507 | }) 508 | .max_by_key(|(.., diff)| *diff) 509 | .expect("array is not empty") 510 | }) 511 | .max_by_key(|(.., diff)| *diff); 512 | 513 | if let Some((i, j, original_edge, new_edge, diff)) = best { 514 | if diff <= T::zero() { 515 | stuck_operators.insert(operator); 516 | continue; 517 | } else { 518 | stuck_operators.clear(); 519 | } 520 | 521 | // Change from=>to to first=>____ 522 | if new_edge[0] != original_edge[0] { 523 | tabu.push_back(i); 524 | tabu_set.insert(i); 525 | current[..=i].reverse(); 526 | } 527 | // Change from=>to to ____=>last 528 | if new_edge[1] != original_edge[1] { 529 | tabu.push_back(j); 530 | tabu_set.insert(j); 531 | current[j..].reverse(); 532 | } 533 | } else { 534 | stuck_operators.insert(operator); 535 | continue; 536 | } 537 | } 538 | } 539 | 540 | let prev_sum = current_sum; 541 | current_distances = current 542 | .iter() 543 | .zip(current.iter().skip(1)) 544 | .map(|(from, to)| abs_distance_squared(*from, *to)) 545 | .collect::>(); 546 | current_sum = current_distances.iter().copied().sum::(); 547 | 548 | debug_assert_eq!( 549 | current.len(), 550 | current.iter().copied().collect::>().len() 551 | ); 552 | assert!( 553 | prev_sum > current_sum, 554 | "operator = {:?} prev = {:?} current = {:?}", 555 | operator, 556 | prev_sum, 557 | current_sum 558 | ); 559 | 560 | if current_sum < best_sum { 561 | best = current.clone(); 562 | best_sum = current_sum; 563 | } 564 | 565 | info!( 566 | "Iteration {}/{} (best: {:?}, tabu: {}/{}, len: {})", 567 | idx, 568 | ITERATIONS, 569 | best_sum, 570 | tabu.len(), 571 | tabu_capacity, 572 | current.len(), 573 | ); 574 | 575 | while tabu.len() > tabu_capacity { 576 | tabu_set.remove(&tabu.pop_front().unwrap()); 577 | } 578 | } 579 | 580 | best 581 | } 582 | 583 | #[cfg(test)] 584 | #[test] 585 | fn tsp_is_correct_for_trivial_case() { 586 | let vertices = [[0, 0], [1, 1], [2, 2]]; 587 | let tree = [[[0, 0], [1, 1]], [[1, 1], [2, 2]]]; 588 | 589 | let path = approximate_tsp_with_mst(&vertices, &tree); 590 | let length: i64 = path 591 | .iter() 592 | .zip(path.iter().skip(1)) 593 | .map(|(from, to)| abs_distance_squared(*from, *to)) 594 | .sum(); 595 | assert_eq!(length, 4); 596 | } 597 | 598 | #[cfg(test)] 599 | #[test] 600 | fn tsp_is_correct_for_nontrivial_case() { 601 | let vertices: [[i64; 2]; 5] = [[24, 28], [371, 33], [235, 72], [157, 509], [156, 194]]; 602 | let tree = [ 603 | [[24, 28], [156, 194]], 604 | [[156, 194], [235, 72]], 605 | [[235, 72], [371, 33]], 606 | [[156, 194], [157, 509]], 607 | ]; 608 | let path = approximate_tsp_with_mst(&vertices, &tree); 609 | let length: i64 = path 610 | .iter() 611 | .zip(path.iter().skip(1)) 612 | .map(|(from, to)| abs_distance_squared(*from, *to)) 613 | .sum(); 614 | assert_eq!(length, 210680); 615 | } 616 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | 663 | --------------------------------------------------------------------------------