marching_squares/
lib.rs

1//! This crate provides an (optionally parallelizable)
2//! marching squares algorithm to generate isolines from
3//! a heightmap of `Vec<Vec<i16>>` values.
4//!
5//! Adapted from [https://github.com/d-dorazio/marching-squares](https://github.com/d-dorazio/marching-squares)
6//!
7//! # Warning
8//!
9//! - The returned lines may only have two points.
10//!
11//! # Example
12//!
13//! ```rust
14//! # extern crate marching_squares;
15//! use marching_squares::{Field, Point};
16//!
17//! fn main() {
18//!
19//!     let width = 1600_usize;
20//!     let height = 1600_usize;
21//!
22//!     let n_steps = 10_usize;
23//!     let mut min_val = 0;
24//!     let mut max_val = 0;
25//!
26//!     // Build the heightmap data (here: randomly generated from a function)
27//!     let z_values = (0..height).map(|y| {
28//!         (0..width).map(|x| {
29//!             let x = (x as f64 - width as f64 / 2.0) / 150.0;
30//!             let y = (y as f64 - height as f64 / 2.0) / 150.0;
31//!             let val = ((1.3 * x).sin() * (0.9 * y).cos() + (0.8 * x).cos() * (1.9 * y).sin() + (y * 0.2 * x).cos()) as i16;
32//!             min_val = min_val.min(val);
33//!             max_val = max_val.max(val);
34//!             val
35//!         }).collect()
36//!     }).collect::<Vec<Vec<i16>>>();
37//!
38//!     let field = Field {
39//!         dimensions: (width, height),
40//!         top_left: Point { x: 0.0, y: 0.0 },
41//!         pixel_size: (1.0, 1.0),
42//!         values: &z_values,
43//!     };
44//!
45//!     let step_size = (max_val - min_val) as f32 / n_steps as f32;
46//!
47//!     // Generate 10 isolines
48//!     // note: you could do this in parallel using rayon
49//!     for step in 0..n_steps {
50//!         let isoline_height = min_val as f32 + (step_size * step as f32);
51//!         println!("{:#?}", field.get_contours(isoline_height as i16));
52//!     }
53//! }
54//! ```
55
56#[cfg(feature = "parallel")]
57extern crate rayon;
58
59use std::collections::{HashMap, HashSet};
60
61/// 2-dimensional Point (x, y)
62#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
63pub struct Point { pub x: f32, pub y: f32 }
64
65/// Line containing a `Vec<Point>`
66#[derive(Debug, Clone, PartialEq, PartialOrd)]
67pub struct Line { pub points: Vec<Point> }
68
69/// Raster heightmap field containing the z-values of each pixel
70#[derive(Debug, Clone, PartialEq)]
71pub struct Field<'a> {
72    /// Amount of x and y pixels in the values field
73    pub dimensions: (usize, usize),
74    /// Top left coordinates
75    pub top_left: Point,
76    /// Size of each pixel (x and y)
77    pub pixel_size: (f32, f32),
78    /// Z-values of each pixel, stored in rows, then columns
79    pub values: &'a Vec<Vec<i16>>,
80}
81
82impl<'a> Field<'a> {
83    pub fn get_contours(&self, z: i16) -> Vec<Line> {
84        #[cfg(not(feature = "parallel"))] {
85            march(self, z)
86        }
87        #[cfg(feature = "parallel")] {
88            march_parallel(self, z)
89        }
90    }
91}
92
93/// A `SegmentsMap` is used to speedup contour building on the average case. It's simply a map from
94/// the start position of the segment rounded with integers coordinates to the list of all the
95/// segments that start in that position. Usually, shapes have very few segments that start at the
96/// same integer position thus this simple optimization allows to find the next segment in O(1)
97/// which is great.
98///
99/// Note that a valid `SegmentsMap` must not have entries for an empty list of segments.
100type SegmentsMap = HashMap<(u64, u64), Vec<(Point, Point)>>;
101
102fn add_seg(segments: &mut SegmentsMap, start: Point, end: Point) {
103    segments
104        .entry((start.x as u64, start.y as u64))
105        .or_default()
106        .push((start, end));
107}
108
109/// Find the contours of a given scalar field using `z` as the threshold value.
110#[cfg(not(feature = "parallel"))]
111#[inline]
112fn march<'a>(field: &Field<'a>, z: i16) -> Vec<Line> {
113
114    let (width, height) = field.dimensions;
115
116    let mut segments: SegmentsMap = HashMap::new();
117
118    field.values
119    .windows(2)
120    .filter_map(|r| match &r { &[cur, next] => { Some((cur, next)) }, _ => None })
121    .enumerate()
122    .for_each(|(cur_y, (current_row_zs, next_row_zs))| {
123        current_row_zs
124        .windows(2)
125        .filter_map(|c| match &c { &[cur, next] => { Some((cur, next)) }, _ => None })
126        .enumerate()
127        .map(|(cur_x, (cur, next))| (cur_x, cur_y, cur, next))
128        .zip(
129            next_row_zs
130            .windows(2)
131            .filter_map(|c| match &c { &[cur, next] => { Some((cur, next)) }, _ => None })
132        )
133        .for_each(|((x, y, ulz, urz), (blz, brz))| {
134            let (a, b) = get_segments(
135                (x, y),
136                (width, height),
137                (*ulz, *urz),
138                (*blz, *brz),
139                z
140            );
141            if let Some(a) = a {
142                add_seg(&mut segments, a.0, a.1);
143            }
144            if let Some(b) = b {
145                add_seg(&mut segments, b.0, b.1);
146            }
147        })
148    });
149
150    let mut contours = build_contours(segments, (width as u64, height as u64));
151    normalize_contours(&mut contours, field.top_left, field.pixel_size);
152    contours
153}
154
155#[inline(always)]
156fn get_segments(
157    (x, y): (usize, usize),
158    (width, height): (usize, usize),
159    (ulz, urz): (i16, i16),
160    (blz, brz): (i16, i16),
161    z: i16
162) -> (Option<(Point, Point)>, Option<(Point, Point)>) {
163
164    let mut case = 0_u8;
165
166    if blz > z { case |= 1; }
167    if brz > z { case |= 2; }
168    if urz > z { case |= 4; }
169    if ulz > z { case |= 8; }
170
171    let x = x as f32;
172    let y = y as f32;
173
174    let top         = Point { x: x + fraction(z, (ulz, urz)), y };
175    let bottom      = Point { x: x + fraction(z, (blz, brz)), y: y + 1.0 };
176    let left        = Point { x, y: y + fraction(z, (ulz, blz)) };
177    let right       = Point { x: x + 1.0, y: y + fraction(z, (urz, brz)) };
178
179    match case {
180        1 => (Some((bottom, left)), None),
181        2 => (Some((right, bottom)), None),
182        3 => (Some((right, left)), None),
183        4 => (Some((top, right)), None),
184        5 => (Some((top, left)), Some((bottom, right))),
185        6 => (Some((top, bottom)), None),
186        7 => (Some((top, left)), None),
187        8 => (Some((left, top)), None),
188        9 => (Some((bottom, top)), None),
189        10 => (Some((left, bottom)), Some((right, top))),
190        11 => (Some((right, top)), None),
191        12 => (Some((left, right)), None),
192        13 => (Some((bottom, right)), None),
193        14 => (Some((left, bottom)), None),
194        _ => (None, None),
195    }
196}
197
198#[cfg(feature = "parallel")]
199#[inline]
200fn march_parallel<'a>(field: &Field<'a>, z: i16) -> Vec<Line> {
201    use rayon::prelude::ParallelSlice;
202    use rayon::iter::ParallelIterator;
203    use rayon::iter::IndexedParallelIterator;
204
205    let (width, height) = field.dimensions;
206
207    let duplicated_items = field.values
208    .par_windows(2)
209    .enumerate()
210    .filter_map(|(cur_y, r)| match &r { &[cur, next] => { Some((cur_y, cur, next)) }, _ => None })
211    .map(|(cur_y, current_row_zs, next_row_zs)| {
212        current_row_zs
213        .par_windows(2)
214        .enumerate()
215        .filter_map(|(cur_x, c)| match &c { &[cur, next] => { Some((cur_x, cur, next)) }, _ => None })
216        .filter_map(|(cur_x, ulz, urz)| {
217            let blz = next_row_zs.get(cur_x)?;
218            let brz = next_row_zs.get(cur_x + 1)?;
219            Some(get_segments(
220                (cur_x, cur_y),
221                (width, height),
222                (*ulz, *urz),
223                (*blz, *brz),
224                z
225            ))
226        })
227        .collect::<Vec<_>>()
228    })
229    .collect::<Vec<Vec<(Option<(Point, Point)>, Option<(Point, Point)>)>>>();
230
231    let mut segments: SegmentsMap = HashMap::new();
232
233    for r in duplicated_items {
234        for (a, b) in r {
235            if let Some(a) = a {
236                add_seg(&mut segments, a.0, a.1);
237            }
238            if let Some(b) = b {
239                add_seg(&mut segments, b.0, b.1);
240            }
241        }
242    }
243
244    let mut contours = build_contours(segments, (width as u64, height as u64));
245    normalize_contours(&mut contours, field.top_left, field.pixel_size);
246    contours
247}
248
249#[inline]
250fn build_contours(mut segments: SegmentsMap, (w, h): (u64, u64)) -> Vec<Line> {
251    use std::collections::hash_map::Entry;
252
253    let mut contours = vec![];
254
255    let mut boundaries = segments
256        .keys()
257        .cloned()
258        .filter(|s| s.0 == 0 || s.0 == w - 1 || s.1 == 0 || s.1 == h - 1)
259        .collect::<HashSet<_>>();
260
261    while !segments.is_empty() {
262        // prefer to start on a boundary, but if no point lie on a bounday just
263        // pick a random one. This allows to connect open paths entirely without
264        // breaking them in multiple chunks.
265        let first_k = boundaries
266            .iter()
267            .next()
268            .map_or_else(|| *segments.keys().next().unwrap(), |k| *k);
269
270        let mut first_e = match segments.entry(first_k) {
271            Entry::Occupied(o) => o,
272            Entry::Vacant(_) => unreachable!(),
273        };
274
275        let first = first_e.get_mut().pop().unwrap();
276        if first_e.get().is_empty() {
277            first_e.remove_entry();
278            boundaries.remove(&first_k);
279        }
280
281        let mut contour = vec![first.0, first.1];
282
283        loop {
284            let prev = contour[contour.len() - 1];
285
286            let segments_k = (prev.x as u64, prev.y as u64);
287            let mut segments = match segments.entry(segments_k) {
288                Entry::Vacant(_) => break,
289                Entry::Occupied(o) => o,
290            };
291
292            let next = segments
293                .get()
294                .iter()
295                .enumerate()
296                .find(|(_, (s, _))| s == &prev);
297
298            match next {
299                None => break,
300                Some((i, seg)) => {
301                    contour.push(seg.1);
302
303                    segments.get_mut().swap_remove(i);
304                    if segments.get().is_empty() {
305                        segments.remove_entry();
306                        boundaries.remove(&segments_k);
307                    }
308                }
309            }
310        }
311
312        contours.push(Line { points: contour });
313    }
314
315    contours
316}
317
318#[inline]
319fn fraction(z: i16, (z0, z1): (i16, i16)) -> f32 {
320    if z0 == z1 {
321        return 0.5;
322    }
323
324    let t = (z - z0) as f32 / (z1 - z0) as f32;
325    t.max(0.0).min(1.0)
326}
327
328#[cfg(not(feature = "parallel"))]
329fn normalize_contours(
330    lines: &mut Vec<Line>,
331    top_left: Point,
332    pixel_size: (f32, f32)
333) {
334    for l in lines.iter_mut() {
335        for c in l.points.iter_mut() {
336            c.x = top_left.x + (pixel_size.0 * c.x);
337            c.y = top_left.y + (pixel_size.1 * c.y);
338        }
339    }
340}
341
342#[cfg(feature = "parallel")]
343fn normalize_contours(
344    lines: &mut Vec<Line>,
345    top_left: Point,
346    pixel_size: (f32, f32)
347) {
348    use rayon::iter::{ParallelIterator, IntoParallelRefMutIterator};
349    lines.par_iter_mut().for_each(|l| {
350        l.points.par_iter_mut().for_each(|c| {
351            c.x = top_left.x + (pixel_size.0 * c.x);
352            c.y = top_left.y + (pixel_size.1 * c.y);
353        });
354    });
355}