rustronomy_watershed/lib.rs
1/*
2 Copyright© 2022 Raúl Wolters(1)
3
4 This file is part of rustronomy-core.
5
6 rustronomy is free software: you can redistribute it and/or modify it under
7 the terms of the European Union Public License version 1.2 or later, as
8 published by the European Commission.
9
10 rustronomy is distributed in the hope that it will be useful, but WITHOUT ANY
11 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 A PARTICULAR PURPOSE. See the European Union Public License for more details.
13
14 You should have received a copy of the EUPL in an/all official language(s) of
15 the European Union along with rustronomy. If not, see
16 <https://ec.europa.eu/info/european-union-public-licence_en/>.
17
18 (1) Resident of the Kingdom of the Netherlands; agreement between licensor and
19 licensee subject to Dutch law as per article 15 of the EUPL.
20*/
21
22#![doc(
23 html_logo_url = "https://raw.githubusercontent.com/smups/rustronomy/main/logos/Rustronomy_ferris.png?raw=true"
24)]
25//! Rustronomy-watershed is a pure-rust implementation of the segmenting and merging
26//! watershed algorithms (see Digabel & Lantuéjoul, 1978[^1]).
27//!
28//! # Features
29//! Two main versions of the watershed algorithm are included in this crate.
30//! 1. The *merging* watershed algorithm, which is a void-filling algorithm that
31//! can be used to identify connected regions in image.
32//! 2. The *segmenting* watershed algorithm, which is a well-known image
33//! segmentation algorithm.
34//!
35//! In addition, `rustronomy-watershed` provides extra functionality which can be
36//! accessed via cargo feature gates. A list of all additional features [can be found
37//! below](#cargo-feature-gates).
38//!
39//!
40//! # Quickstart
41//! To use the latest release of Rustronomy-watershed in a cargo project, add
42//! the rustronomy-watershed crate as a dependency to your `Cargo.toml` file:
43//! ```toml
44//! [dependencies]
45//! rustronomy-watershed = "0.2.0"
46//! ```
47//! To use Rustronomy-fits in a Jupyter notebook, execute a cell containing the
48//! following code:
49//! ```rust
50//! :dep rustronomy-watershed = {version = "0.2"}
51//! ```
52//! If you want to use the latest (unstable) development version of
53//! rustronomy-watershed, you can do so by using the `git` field (which fetches
54//! the latest version from the repo) rather than the `version` field
55//! (which downloads the latest released version from crates.io).
56//! ```
57//! {git = "https://github.com/smups/rustronomy-watershed"}
58//! ```
59//!
60//! ## Short example: computing the Watershed transform of a random field
61//! `rustronomy-watershed` uses the commonly used "builder pattern" to configure
62//! the watershed transform before executing it. To configure a transform,
63//! create an instance of the `TransformBuilder` struct. Once you are done specifying
64//! options for the builder struct using its associated functions, call the
65//! `build_merging()` or `build_segmenting()` functions to generate a
66//! (`Sync`&`Send`) watershed transform struct, which you can now use to
67//! execute the configured transform.
68//!
69//! In this example, we compute the watershed transform of a uniform random field.
70//! The random field can be generated with the `ndarray_rand` crate. To configure a
71//! new watershed transform, one can use the `TransformBuilder` struct which is
72//! included in the `rustronomy_watershed` prelude.
73//! ```rust
74//! use ndarray as nd;
75//! use rustronomy_watershed::prelude::*;
76//! use ndarray_rand::{rand_distr::Uniform, RandomExt};
77//!
78//! //Create a random uniform distribution
79//! let rf = nd::Array2::<u8>::random((512, 512), Uniform::new(0, 254));
80//! //Set-up the watershed transform
81//! let watershed = TransformBuilder::default().build_segmenting().unwrap();
82//! //Find minima of the random field (to be used as seeds)
83//! let rf_mins = watershed.find_local_minima(rf.view());
84//! //Execute the watershed transform
85//! let output = watershed.transform(rf.view(), &rf_mins);
86//! ```
87//! [^1]: H. Digabel and C. Lantuéjoul. **Iterative algorithms.** *In Actes du Second Symposium Européen d’Analyse Quantitative des Microstructures en Sciences des Matériaux, Biologie et Medécine*, October 1978.
88//!
89//! # Cargo feature gates
90//! *By default, all features behind cargo feature gates are **disabled***
91//! - `jemalloc`: this feature enables the [jemalloc allocator](https://jemalloc.net).
92//! From the jemalloc website: *"jemalloc is a general purpose `malloc`(3)
93//! implementation that emphasizes fragmentation avoidance and scalable concurrency
94//! support."*. Jemalloc is enabled though usage of the `jemalloc` crate, which
95//! increases compile times considerably. However, enabling this feature can also
96//! greatly improve run-time performance, especially on machines with more (>6 or so)
97//! cores. To compile `rustronomy-watershed` with the `jemalloc` feature,
98//! jemalloc must be installed on the host system.
99//! - `plots`: with this feature enabled, `rustronomy-watershed` will generate a
100//! plot of the watershed-transform each time the water level is increased.
101//! Plotting support adds the `plotters` crate as a dependency, which increases
102//! compile times and requires the installation of some packages on linux
103//! systems, [see the `plotters` documentation for details](https://docs.rs/plotters/).
104//! - `progress`: this feature enables progress bars for the watershed algorithm.
105//! Enabling this feature adds the `indicatif` crate as a dependency,
106//! which should not considerably slow down compile times.
107//! - `debug`: this feature enables debug and performance monitoring output. This
108//! can negatively impact performance. Enabling this feature does not add additional
109//! dependencies.
110//!
111//! ## `plots` feature gate
112//! Enabling the `plots` feature gate adds two new methods to the `TransformBuilder`
113//! struct: `set_plot_colour_map`, which can be used to set the colour map that
114//! will be used by `plotters` to generate the images and `set_plot_folder`, which
115//! can be used to specify folder where the generated images should be placed. If
116//! no output folder is specified when the `plots` feature is enabled, no plots will
117//! be generated (code will still compile).
118//!
119//! The generated plots are png files with no text. Each pixel in the generated
120//! images corresponds 1:1 to a pixel in the input array.
121
122//Unconditional imports
123use ndarray as nd;
124use num_traits::{Num, ToPrimitive};
125use rand::{seq::SliceRandom, Rng};
126use rayon::prelude::*;
127
128//Set Jemalloc as the global allocator for this crate
129#[cfg(feature = "jemalloc")]
130#[global_allocator]
131static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
132
133//Progress bar (conditional)
134#[cfg(feature = "progress")]
135use indicatif;
136
137//Constants for pixels that have to be left uncoloured, or have to be coloured
138pub const UNCOLOURED: usize = 0;
139pub const NORMAL_MAX: u8 = u8::MAX - 1;
140pub const ALWAYS_FILL: u8 = u8::MIN;
141pub const NEVER_FILL: u8 = u8::MAX;
142
143//Utility prelude for batch import
144pub mod prelude {
145 pub use crate::{MergingWatershed, TransformBuilder, Watershed, WatershedUtils};
146 #[cfg(feature = "plots")]
147 pub mod color_maps {
148 pub use crate::plotting::grey_scale;
149 pub use crate::plotting::inferno;
150 pub use crate::plotting::magma;
151 pub use crate::plotting::plasma;
152 pub use crate::plotting::viridis;
153 }
154}
155
156////////////////////////////////////////////////////////////////////////////////
157// HELPER FUNCTIONS //
158////////////////////////////////////////////////////////////////////////////////
159
160#[cfg(feature = "progress")]
161fn set_up_bar(water_max: u8) -> indicatif::ProgressBar {
162 const TEMPLATE: &str = "{spinner}[{elapsed}/{duration}] water level {pos}/{len}{bar:60}";
163 let style = indicatif::ProgressStyle::with_template(TEMPLATE);
164 let bar = indicatif::ProgressBar::new(water_max as u64);
165 bar.set_style(style.unwrap());
166 return bar;
167}
168
169#[inline]
170fn neighbours_8con(index: &(usize, usize)) -> Vec<(usize, usize)> {
171 let (x, y): (isize, isize) = (index.0 as isize, index.1 as isize);
172 [
173 (x + 1, y),
174 (x + 1, y + 1),
175 (x + 1, y - 1),
176 (x, y + 1),
177 (x, y - 1),
178 (x - 1, y),
179 (x - 1, y + 1),
180 (x - 1, y - 1),
181 ]
182 .iter()
183 .filter_map(|&(x, y)| if x < 0 || y < 0 { None } else { Some((x as usize, y as usize)) })
184 .collect()
185}
186
187#[inline]
188fn neighbours_4con(index: &(usize, usize)) -> Vec<(usize, usize)> {
189 let (x, y): (isize, isize) = (index.0 as isize, index.1 as isize);
190 [(x + 1, y), (x, y + 1), (x, y - 1), (x - 1, y)]
191 .iter()
192 .filter_map(|&(x, y)| if x < 0 || y < 0 { None } else { Some((x as usize, y as usize)) })
193 .collect()
194}
195
196fn find_flooded_px(
197 img: nd::ArrayView2<u8>,
198 cols: nd::ArrayView2<usize>,
199 lvl: u8,
200) -> Vec<((usize, usize), usize)> {
201 //Window size and index of center window pixel
202 const WINDOW: (usize, usize) = (3, 3);
203 const MID: (usize, usize) = (1, 1);
204
205 /*
206 We lock-step through (3x3) windows of both the input image and the output
207 watershed (coloured water). We only consider the centre pixel, since all the
208 windows overlap anyways. The index of the nd::Zip function is the (0,0) index
209 of the window, so the index of the target pixel is at window_idx + (1,1).
210
211 For each target pixel we:
212 1. Check if it is flooded: YES -> continue, NO -> ignore px
213 2. Check if it is uncoloured: YES -> continue, NO -> ignore px
214 3. Check if at least one of the window pixels is coloured
215 YES -> continue, NO -> ignore px
216 4. Find the colours of the neighbouring pixels
217 All same -> colour MID pixel with that colour
218 Different -> pick a random colour
219 */
220 nd::Zip::indexed(cols.windows(WINDOW))
221 .and(img.windows(WINDOW))
222 .into_par_iter()
223 //(1) Ignore unflooded pixels
224 .filter(|&(_idx, _col_wd, img_wd)| img_wd[MID] <= lvl)
225 //(2) Ignore already coloured pixels
226 .filter(|&(_idx, col_wd, _img_wd)| col_wd[MID] == UNCOLOURED)
227 //(3) Ignore pixels that do not border coloured pixels
228 .filter(|&(_idx, col_wd, _img_wd)| {
229 let neigh_idx_4c = neighbours_4con(&MID);
230 !neigh_idx_4c.iter().all(|&idx| col_wd[idx] == UNCOLOURED)
231 })
232 //Set idx from upper left corner to target pixel, and ignore img window
233 .map(|(idx, col_wd, _img_wd)| ((idx.0 + 1, idx.1 + 1), col_wd))
234 //(4) Decide which colour our pixel should be
235 .map(|(idx, col_wd)| {
236 //Get indices of neighbouring pixels, then ask their colours
237 let neigh_col_4c = neighbours_4con(&MID)
238 .into_iter()
239 .map(|neigh_idx| col_wd[neigh_idx])
240 //Ignore uncoloured neighbours
241 .filter(|&col| col != UNCOLOURED)
242 .collect::<Vec<usize>>();
243
244 //First neighbour will be our reference colour
245 let col0 = *neigh_col_4c.get(0).expect("All neighbours were uncoloured!");
246 if neigh_col_4c.iter().all(|&col| col == col0) {
247 //All coloured neighbours have same colour
248 (idx, col0)
249 } else {
250 //We have to pick a random colour
251 let rand_idx = rand::thread_rng().gen_range(0..neigh_col_4c.len());
252 let rand_col = *neigh_col_4c.get(rand_idx).expect("picking random px went wrong?");
253 (idx, rand_col)
254 }
255 })
256 .collect()
257}
258
259#[test]
260fn test_find_px() {
261 //This test assumes UNCOLOURED == 0, so it should fail
262 assert!(UNCOLOURED == 0);
263 let input = nd::array![
264 [0, 0, 0, 0, 0, 0, 0, 0],
265 [0, 0, 0, 0, 0, 1, 0, 0],
266 [0, 0, 1, 0, 0, 0, 0, 0],
267 [0, 0, 0, 0, 0, 5, 0, 0],
268 [0, 0, 0, 1, 0, 0, 0, 0],
269 [0, 0, 0, 5, 0, 0, 1, 0],
270 [0, 0, 5, 4, 5, 0, 0, 0],
271 [0, 0, 0, 5, 0, 0, 0, 0],
272 ];
273 let colours = nd::array![
274 [0, 0, 0, 0, 0, 0, 0, 0],
275 [0, 1, 1, 1, 1, 0, 1, 0],
276 [0, 1, 0, 1, 1, 1, 1, 0],
277 [0, 1, 1, 1, 1, 0, 1, 0],
278 [0, 1, 1, 1, 0, 0, 1, 0],
279 [0, 1, 1, 0, 1, 1, 0, 0],
280 [0, 1, 0, 0, 0, 1, 1, 0],
281 [0, 0, 0, 0, 0, 0, 0, 0]
282 ];
283 let answer1 = [(1, 5), (2, 2), (4, 4), (5, 6)];
284 let attempt1 = find_flooded_px(input.view(), colours.view(), 2)
285 .into_iter()
286 .map(|(x, _)| x)
287 .collect::<Vec<_>>();
288 for answer in answer1 {
289 assert!(attempt1.contains(&answer))
290 }
291}
292
293#[derive(Eq, Clone, Copy, Default, Debug)]
294#[repr(transparent)]
295struct Merge([usize; 2]);
296
297// Merging 1 with 2 is the same as merging 2 with 1. So [1,2] == [2,1],
298// as reflected by this impl
299impl PartialEq for Merge {
300 #[inline(always)]
301 fn eq(&self, other: &Self) -> bool {
302 let [x1, y1] = self.0;
303 let [x2, y2] = other.0;
304 (x1 == x2 && y1 == y2) || (x1 == y2 && y1 == x2)
305 }
306}
307
308#[test]
309fn test_merge_eq() {
310 assert_eq!(Merge([1, 2]), Merge([2, 1]));
311}
312
313#[inline(always)]
314fn sort_by_small_big(this: &Merge, that: &Merge) -> std::cmp::Ordering {
315 use std::cmp::Ordering::*;
316 if this == that {
317 return Equal;
318 }
319 let (self_small, self_big) =
320 if this.0[0] > this.0[1] { (this.0[0], this.0[1]) } else { (this.0[0], this.0[1]) };
321 let (other_small, other_big) =
322 if that.0[0] > that.0[1] { (that.0[0], that.0[1]) } else { (that.0[1], that.0[0]) };
323
324 //First order on the basis of the smallest elements, then the largest ones
325 if self_small < other_small {
326 Less
327 } else if self_small > other_small {
328 Greater
329 } else if self_big < other_big {
330 Less
331 } else {
332 Greater
333 }
334}
335
336#[test]
337fn test_merge_ord_small_big() {
338 use std::cmp::Ordering::*;
339 let cmp = sort_by_small_big;
340 assert_eq!(cmp(&Merge([2, 1]), &Merge([1, 1])), Greater);
341 assert_eq!(cmp(&Merge([1, 1]), &Merge([1, 2])), Less);
342 assert_eq!(cmp(&Merge([2, 1]), &Merge([1, 2])), Equal);
343 assert_eq!(cmp(&Merge([3, 8]), &Merge([4, 5])), Less);
344}
345
346#[inline(always)]
347fn sort_by_big_small(this: &Merge, that: &Merge) -> std::cmp::Ordering {
348 use std::cmp::Ordering::*;
349 if this == that {
350 return Equal;
351 }
352 let (self_small, self_big) =
353 if this.0[0] > this.0[1] { (this.0[0], this.0[1]) } else { (this.0[0], this.0[1]) };
354 let (other_small, other_big) =
355 if that.0[0] > that.0[1] { (that.0[0], that.0[1]) } else { (that.0[1], that.0[0]) };
356
357 //First order on the basis of the smallest elements, then the largest ones
358 if self_big < other_big {
359 Less
360 } else if self_big > other_big {
361 Greater
362 } else if self_small < other_small {
363 Less
364 } else {
365 Greater
366 }
367}
368
369#[test]
370fn test_merge_ord_big_small() {
371 use std::cmp::Ordering::*;
372 let cmp = sort_by_big_small;
373 assert_eq!(cmp(&Merge([2, 1]), &Merge([1, 1])), Greater);
374 assert_eq!(cmp(&Merge([1, 1]), &Merge([1, 2])), Less);
375 assert_eq!(cmp(&Merge([2, 1]), &Merge([1, 2])), Equal);
376 assert_eq!(cmp(&Merge([3, 8]), &Merge([4, 5])), Greater);
377}
378
379impl From<[usize; 2]> for Merge {
380 #[inline(always)]
381 fn from(value: [usize; 2]) -> Self {
382 Self(value)
383 }
384}
385
386impl From<Merge> for [usize; 2] {
387 #[inline(always)]
388 fn from(value: Merge) -> Self {
389 value.0
390 }
391}
392
393fn find_merge(col: nd::ArrayView2<usize>) -> Vec<Merge> {
394 //Window size and index of center window pixel
395 const WINDOW: (usize, usize) = (3, 3);
396 const MID: (usize, usize) = (1, 1);
397
398 /*
399 To find which regions to merge, we iterate in (3x3) windows over the current
400 map of coloured pixels. We only consider the centre pixel, since all the
401 windows overlap anyways.
402
403 For each target pixel we:
404 1. Check if the pixel is uncoloured. YES -> ignore, NO -> continue
405 2. Check if the pixel has coloured neighbours. YES -> continue, NO -> ignore
406 3. Check if the pixel has neighbours of different colours
407 YES -> continue, NO -> ignore (this is a lake pixel)
408 4. All neighbours that are left are now different colours than the MID px
409 AND are not uncoloured. These pairs have to be merged
410 */
411 let mut merge = nd::Zip::from(col.windows(WINDOW))
412 .into_par_iter()
413 //(1) Check target pixel colour
414 .filter(|&col_wd| col_wd.0[MID] != UNCOLOURED)
415 //Map window to array of neighbour colours
416 .map(|col_wd| -> (usize, Vec<usize>) {
417 let own_col = col_wd.0[MID];
418 let neighbour_cols = neighbours_4con(&MID)
419 .into_iter()
420 .map(|idx| col_wd.0[idx])
421 .filter(|&col| col != UNCOLOURED)
422 .collect();
423 (own_col, neighbour_cols)
424 })
425 //(2) Ignore pixels with only uncoloured neighbours
426 .filter(|(_own_col, neigh_col)| !neigh_col.is_empty())
427 //(3) Collect neighbour colours. These have to be merged
428 .map(|(own_col, neigh_col)| {
429 neigh_col
430 .into_iter()
431 //(3a) Ignore mergers that merge a region with itself! ([1,1] and the likes)
432 .filter_map(|c| if c == own_col { None } else { Some(Merge::from([own_col, c])) })
433 .collect::<Vec<_>>()
434 })
435 .flatten()
436 .collect::<Vec<_>>();
437
438 //Remove duplicates (unstable sort may reorder duplicates, we don't care
439 //because the whole point of sorting the vec is to get RID of duplicates!)
440 merge.par_sort_unstable_by(sort_by_big_small);
441 merge.dedup();
442 merge.par_sort_unstable_by(sort_by_small_big);
443 merge.dedup();
444 return merge;
445}
446
447#[test]
448fn test_find_merge() {
449 //This test assumes UNCOLOURED == 0, so it should fail
450 assert!(UNCOLOURED == 0);
451 let input = nd::array![
452 [0, 0, 0, 0, 0, 0, 0, 0],
453 [0, 1, 1, 2, 2, 0, 1, 0],
454 [0, 1, 1, 2, 2, 0, 1, 0],
455 [0, 3, 3, 3, 3, 3, 3, 0],
456 [0, 0, 0, 0, 0, 0, 0, 0],
457 [0, 4, 4, 0, 5, 5, 6, 0],
458 [0, 4, 4, 0, 0, 5, 6, 0],
459 [0, 0, 0, 0, 0, 0, 0, 0],
460 ];
461 let answer = vec![Merge([1, 2]), Merge([1, 3]), Merge([2, 3]), Merge([5, 6])];
462 let result = find_merge(input.view());
463 assert_eq!(answer.len(), result.len());
464 assert!(result.iter().all(|x| answer.contains(x)));
465}
466
467fn make_colour_map(base_map: &mut [usize], pair_mergers: &[Merge]) {
468 /* REDUCING 2-REGION MERGERS TO N-REGION MERGERS
469 We are given a list of *locally* connected regions. For instance:
470 (1,2) and (2,4)
471 We have to turn locally connected regions into globally connected regions.
472 In the example, the two locally connected regions are not connected directly,
473 but only via region 2. They still have to merge into a single region:
474 (1,2,3,4,5)
475 There may be many steps between regions:
476 (1,2) & (2,3) & (3,4) & (4,5)
477 these should merge to:
478 (1,2,3,4,5)
479 regardless of the order in which they were specified!
480 */
481 let mut full_mergers: Vec<Vec<usize>> = Vec::new();
482
483 'pair_loop: for &pair_merge in pair_mergers {
484 //If pair_merge connects two full_merge regions, they have to be merged into
485 //a single large region
486 let [col1, col2]: [usize; 2] = pair_merge.into();
487 let mut connect = [None, None];
488 for (idx, region) in full_mergers.iter().enumerate() {
489 if region.contains(&col1) && region.contains(&col2) {
490 //This pair_merge was entirely contained within another region, it must
491 //be a duplicate! We can skip to the next pair_merge
492 continue 'pair_loop;
493 } else if region.contains(&col1) || region.contains(&col2) {
494 if connect[0].is_none() {
495 connect[0] = Some(idx)
496 } else if connect[1].is_none() {
497 connect[1] = Some(idx);
498 break;
499 } else {
500 panic!("Unreachable code path!")
501 }
502 }
503 }
504
505 if connect == [None, None] {
506 //This pair_merge does not connect two full_merge regions, so it must be added
507 //as its own full_merge region
508 full_mergers.push(vec![col1, col2]);
509 } else if let [Some(reg_idx), None] = connect {
510 //This pair_merge *does* connect with another region, but only one.
511 let reg = full_mergers.get_mut(reg_idx).unwrap();
512 reg.extend_from_slice(&[col1, col2]);
513 reg.sort();
514 reg.dedup();
515 } else if let [Some(reg_idx1), Some(reg_idx2)] = connect {
516 //This pair_merge connects two regions, we must merge pair_merge AND both
517 //regions at the same time.
518
519 //Obtain a mutable ref to both regions (we'll drain one of them)
520 //This code is messy thanks to the borrow checker
521 let (reg1, reg2) = {
522 let (larger, smaller) =
523 if reg_idx1 > reg_idx2 { (reg_idx1, reg_idx2) } else { (reg_idx2, reg_idx1) };
524 let (head, tail) = full_mergers.split_at_mut(smaller + 1);
525 (&mut head[smaller], &mut tail[larger - smaller - 1])
526 };
527
528 //Drain region2 into region 1.
529 //We do not have to append col1 or col2 because they are already contained
530 //in reg1 and reg2. That is why we are merging them after all.
531 reg1.append(reg2);
532 }
533
534 //remove empty regions
535 full_mergers = full_mergers.into_iter().filter(|region| !region.is_empty()).collect();
536 }
537
538 for merge in full_mergers {
539 let merged_col = *merge.get(0).expect("tried to merge zero regions");
540 base_map.iter_mut().filter(|x| merge.contains(x)).for_each(|x| *x = merged_col);
541 }
542}
543
544#[test]
545fn test_make_colour_map() {
546 //This test assumes UNCOLOURED == 0, so it should fail
547 assert!(UNCOLOURED == 0);
548 let mut cmap;
549 let rng = &mut rand::thread_rng();
550 for _ in 0..10 {
551 //Test merging of once-connected region
552 cmap = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
553 make_colour_map(&mut cmap, &vec![Merge([1, 2])]);
554 assert!(cmap == [0, 1, 1, 3, 4, 5, 6, 7, 8, 9]);
555
556 //Now test multiple non-connected regions
557 cmap = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
558 let mut input = vec![Merge([1, 2]), Merge([8, 9])];
559 input.shuffle(rng);
560 make_colour_map(&mut cmap, &input);
561 assert!(cmap == [0, 1, 1, 3, 4, 5, 6, 7, 8, 8]);
562
563 //Now test multiple *connected* regions
564 cmap = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
565 let mut input = vec![Merge([1, 2]), Merge([2, 3])];
566 input.shuffle(rng);
567 make_colour_map(&mut cmap, &input);
568 assert!(cmap == [0, 1, 1, 1, 4, 5, 6, 7, 8, 9]);
569
570 //Two consecutive mergers
571 cmap = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
572 let mut input = vec![Merge([1, 2]), Merge([8, 9])];
573 input.shuffle(rng);
574 make_colour_map(&mut cmap, &input);
575 let mut input = vec![Merge([1, 7]), Merge([7, 8])];
576 input.shuffle(rng);
577 make_colour_map(&mut cmap, &input);
578 assert!(cmap == [0, 1, 1, 3, 4, 5, 6, 1, 1, 1]);
579
580 //Repeated merger (somehow)
581 cmap = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
582 let mut input = vec![Merge([1, 2]), Merge([3, 2]), Merge([2, 1])];
583 input.shuffle(rng);
584 make_colour_map(&mut cmap, &input);
585 assert!(cmap == [0, 1, 1, 1, 4, 5, 6, 7, 8, 9]);
586 }
587}
588
589#[inline(always)]
590fn recolour(mut canvas: nd::ArrayViewMut2<usize>, colour_map: &[usize]) {
591 canvas.mapv_inplace(|px| colour_map[px])
592}
593
594#[test]
595fn test_recolour() {
596 //This test assumes UNCOLOURED == 0, so it should fail
597 assert!(UNCOLOURED == 0);
598 let mut input = nd::array![
599 [0, 0, 0, 0, 0, 0, 0, 0],
600 [0, 1, 1, 2, 2, 0, 1, 0],
601 [0, 1, 1, 2, 2, 0, 1, 0],
602 [0, 3, 3, 3, 3, 3, 3, 0],
603 [0, 0, 0, 0, 0, 0, 0, 0],
604 [0, 4, 4, 0, 5, 5, 6, 0],
605 [0, 4, 4, 0, 0, 5, 6, 0],
606 [0, 0, 0, 0, 0, 0, 0, 0],
607 ];
608 let cmap = [0, 1, 1, 1, 4, 5, 5];
609 let answer = nd::array![
610 [0, 0, 0, 0, 0, 0, 0, 0],
611 [0, 1, 1, 1, 1, 0, 1, 0],
612 [0, 1, 1, 1, 1, 0, 1, 0],
613 [0, 1, 1, 1, 1, 1, 1, 0],
614 [0, 0, 0, 0, 0, 0, 0, 0],
615 [0, 4, 4, 0, 5, 5, 5, 0],
616 [0, 4, 4, 0, 0, 5, 5, 0],
617 [0, 0, 0, 0, 0, 0, 0, 0],
618 ];
619 recolour(input.view_mut(), &cmap);
620 assert_eq!(answer, input);
621
622 //Test that changing values no longer in the image does nothing
623 let cmap = [0, 1, 13498683, 13458, 4, 5, 134707134];
624 recolour(input.view_mut(), &cmap);
625 assert_eq!(answer, input);
626}
627
628#[inline]
629fn find_lake_sizes(ctx: HookCtx) -> (u8, Vec<usize>) {
630 let mut lake_sizes = vec![0usize; ctx.colours.len() + 1];
631 ctx.colours.iter().for_each(|&x| {
632 *lake_sizes.get_mut(x).unwrap() += 1;
633 });
634 (ctx.water_level, lake_sizes)
635}
636
637////////////////////////////////////////////////////////////////////////////////
638// OPTIONAL MODULES //
639////////////////////////////////////////////////////////////////////////////////
640#[cfg(feature = "debug")]
641mod performance_monitoring {
642
643 #[derive(Clone, Debug, Default)]
644 pub struct PerfReport {
645 pub big_iter_ms: Vec<usize>,
646 pub colouring_mus: Vec<usize>,
647 pub loops: usize,
648 pub merge_ms: usize,
649 pub lake_count_ms: usize,
650 pub total_ms: usize,
651 }
652
653 impl PerfReport {
654 pub fn iter_avg(&self) -> f64 {
655 let num = self.big_iter_ms.len() as f64;
656 self.big_iter_ms.iter().map(|&x| x as f64).sum::<f64>() / num
657 }
658 pub fn iter_total(&self) -> f64 {
659 self.big_iter_ms.iter().map(|&x| x as f64).sum()
660 }
661 pub fn colour_avg(&self) -> f64 {
662 let num = self.big_iter_ms.len() as f64;
663 self.colouring_mus.iter().map(|&x| x as f64).sum::<f64>() / num
664 }
665 pub fn colour_total(&self) -> f64 {
666 self.colouring_mus.iter().map(|&x| x as f64).sum()
667 }
668 }
669
670 impl std::fmt::Display for PerfReport {
671 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
672 writeln!(f, ">---------[Performance Summary]---------")?;
673 writeln!(f, "> Looped {}x", self.loops)?;
674 writeln!(f, "> Iteration Average: {:.1}ms; Σ {:.0}ms", self.iter_avg(), self.iter_total())?;
675 writeln!(
676 f,
677 "> Colouring Average: {:.1}µs; Σ {:.0}µs",
678 self.colour_avg(),
679 self.colour_total()
680 )?;
681 writeln!(f, "> Merging: {}ms", self.merge_ms)?;
682 writeln!(f, "> Counting Lakes: {}ms", self.lake_count_ms)?;
683 writeln!(f, ">--------------------------------+ total")?;
684 writeln!(
685 f,
686 "> {}ms with {:.1}ms overhead (Δt)",
687 self.total_ms,
688 self.total_ms as f64
689 - self.iter_total()
690 - self.colour_total() / 1000.0
691 - self.merge_ms as f64
692 - self.lake_count_ms as f64
693 )
694 }
695 }
696}
697
698#[cfg(feature = "plots")]
699/// This module contains all the code required to generate images from the
700/// watershed array, including all the included colour maps.
701pub mod plotting {
702 use ndarray as nd;
703 use num_traits::ToPrimitive;
704 use plotters::prelude::*;
705 use std::{error::Error, path::Path};
706
707 //Colour for nan px
708 const NAN_COL: RGBColor = BLACK;
709
710 //Module that contains hardcoded colourmaps from matplotlib
711 mod color_maps;
712
713 pub fn plot_slice<'a, T>(
714 slice: nd::ArrayView2<'a, T>,
715 file_name: &Path,
716 color_map: fn(count: T, min: T, max: T) -> Result<RGBColor, Box<dyn Error>>,
717 ) -> Result<(), Box<dyn Error>>
718 where
719 T: Default + std::fmt::Display + std::cmp::PartialOrd + ToPrimitive + Copy,
720 {
721 //Get min and max vals of slice
722 let min = slice.iter().fold(T::default(), |f: T, x: &T| if *x < f { *x } else { f });
723 let max = slice.iter().fold(T::default(), |f: T, x: &T| if *x > f { *x } else { f });
724
725 //Get the size of the slice
726 let x_size = slice.shape()[0] as u32;
727 let y_size = slice.shape()[1] as u32;
728
729 //Make new fig
730 let root = BitMapBackend::new(file_name, (x_size, y_size)).into_drawing_area();
731 root.fill(&WHITE)?;
732
733 //make empty drawing area in fig
734 let mut chart = ChartBuilder::on(&root).build_cartesian_2d(0..x_size, 0..y_size)?;
735 chart.configure_mesh().disable_mesh().disable_axes().draw()?;
736 let plotting_area = chart.plotting_area();
737
738 //fill pixels
739 for ((x, y), px) in slice.indexed_iter() {
740 plotting_area.draw_pixel((x as u32, y as u32), &color_map(*px, min, max)?)?
741 }
742
743 //save file
744 root.present()?;
745
746 #[cfg(feature = "debug")]
747 println!("slice saved as png: {file_name:?}; max:{max:2}, min:{min:2}");
748 Ok(())
749 }
750
751 #[inline(always)]
752 pub fn grey_scale<T>(count: T, min: T, max: T) -> Result<RGBColor, Box<dyn Error>>
753 where
754 T: std::fmt::Display + std::cmp::PartialOrd + ToPrimitive,
755 {
756 if count <= min {
757 //This is a NAN pixel, fill it with the NaN colour
758 Ok(NAN_COL)
759 } else {
760 //Grayscale value
761 let gray = ((255.0f64 * count.to_f64().unwrap() + min.to_f64().unwrap())
762 / max.to_f64().unwrap()) as u8;
763 Ok(RGBColor(gray, gray, gray))
764 }
765 }
766
767 #[inline(always)]
768 pub fn viridis<T>(count: T, min: T, max: T) -> Result<RGBColor, Box<dyn Error>>
769 where
770 T: std::fmt::Display + std::cmp::PartialOrd + ToPrimitive,
771 {
772 if count <= min {
773 //This is a NAN pixel, fill it with the NaN colour
774 Ok(NAN_COL)
775 } else {
776 //Grayscale value
777 let gray = ((255.0f64 * count.to_f64().unwrap() + min.to_f64().unwrap())
778 / max.to_f64().unwrap()) as usize;
779 let color = color_maps::VIRIDIS[gray];
780 Ok(RGBColor((color[0] * 256.0) as u8, (color[1] * 256.0) as u8, (color[2] * 256.0) as u8))
781 }
782 }
783
784 #[inline(always)]
785 pub fn magma<T>(count: T, min: T, max: T) -> Result<RGBColor, Box<dyn Error>>
786 where
787 T: std::fmt::Display + std::cmp::PartialOrd + ToPrimitive,
788 {
789 if count <= min {
790 //This is a NAN pixel, fill it with the NaN colour
791 Ok(NAN_COL)
792 } else {
793 //Grayscale value
794 let gray = ((255.0f64 * count.to_f64().unwrap() + min.to_f64().unwrap())
795 / max.to_f64().unwrap()) as usize;
796 let color = color_maps::MAGMA[gray];
797 Ok(RGBColor((color[0] * 256.0) as u8, (color[1] * 256.0) as u8, (color[2] * 256.0) as u8))
798 }
799 }
800
801 #[inline(always)]
802 pub fn plasma<T>(count: T, min: T, max: T) -> Result<RGBColor, Box<dyn Error>>
803 where
804 T: std::fmt::Display + std::cmp::PartialOrd + ToPrimitive,
805 {
806 if count <= min {
807 //This is a NAN pixel, fill it with the NaN colour
808 Ok(NAN_COL)
809 } else {
810 //Grayscale value
811 let gray = ((255.0f64 * count.to_f64().unwrap() + min.to_f64().unwrap())
812 / max.to_f64().unwrap()) as usize;
813 let color = color_maps::PLASMA[gray];
814 Ok(RGBColor((color[0] * 256.0) as u8, (color[1] * 256.0) as u8, (color[2] * 256.0) as u8))
815 }
816 }
817
818 #[inline(always)]
819 pub fn inferno<T>(count: T, min: T, max: T) -> Result<RGBColor, Box<dyn Error>>
820 where
821 T: std::fmt::Display + std::cmp::PartialOrd + ToPrimitive,
822 {
823 if count <= min {
824 //This is a NAN pixel, fill it with the NaN colour
825 Ok(NAN_COL)
826 } else {
827 //Grayscale value
828 let gray = ((255.0f64 * count.to_f64().unwrap() + min.to_f64().unwrap())
829 / max.to_f64().unwrap()) as usize;
830 let color = color_maps::INFERNO[gray];
831 Ok(RGBColor((color[0] * 256.0) as u8, (color[1] * 256.0) as u8, (color[2] * 256.0) as u8))
832 }
833 }
834}
835
836////////////////////////////////////////////////////////////////////////////////
837// WATERSHED TRANSFORMS //
838////////////////////////////////////////////////////////////////////////////////
839
840#[cfg(feature = "plots")]
841use plotters::prelude::*;
842
843#[derive(Clone)]
844pub struct HookCtx<'a> {
845 pub water_level: u8,
846 pub max_water_level: u8,
847 pub image: nd::ArrayView2<'a, u8>,
848 pub colours: nd::ArrayView2<'a, usize>,
849 pub seeds: &'a [(usize, (usize, usize))],
850}
851
852impl<'a> HookCtx<'a> {
853 fn ctx(
854 water_level: u8,
855 max_water_level: u8,
856 image: nd::ArrayView2<'a, u8>,
857 colours: nd::ArrayView2<'a, usize>,
858 seeds: &'a [(usize, (usize, usize))],
859 ) -> Self {
860 HookCtx { water_level, max_water_level, image, colours, seeds }
861 }
862}
863
864#[derive(Clone)]
865/// Builder for configuring a watershed transform.
866///
867/// Use the `new_segmenting()` associated function to start configuring a
868/// segmenting watershed transform. Use the `default()` method to start configuring
869/// a watershed transform. Once you have enabled the desired functionality,
870/// a watershed transform struct can be generated with the `build_segmenting()`
871/// and `build_merging()` associated functions. These return a struct of the type
872/// `SegmentingWatershed` and `MergingWatershed` respectively, which can be
873/// shared between threads.
874///
875/// ## `default()` vs `new()` and custom hooks
876/// tl;dr: Use `new()` if you want to run a custom hook each time a the water
877/// level is raised during the watershed transform. Otherwise, use `default()`.
878///
879/// A `TransformBuilder` struct can be obtained with both the `default()` and
880/// `new()` functions implemented for both. The main difference between the two
881/// is the resulting type:
882/// - `default()` results in a `TransformBuilder<()>`
883/// - `new()` results in a `TransformBuilder<T>`
884/// The default type for `T` is `()`. `T` is only used when specifying a custom
885/// function run by the watershed transform each time the water level is raised.
886/// This "hook" has the type `fn(HookCtx) -> T`.
887///
888/// Sadly, Rust is not able to determine that `T` should take the default `()`
889/// type if no hook is configured during the builder phase (which can be done with
890/// the `set_wlvl_hook` function). Therefore, `Default` is only implemented for
891/// `TransformBuilder<()>` (so not for all `T`) and can be used to circumvent this
892/// limitation of the type inference engine.
893///
894/// ## `plots` feature
895/// Enabling the `plots` feature gate adds two new methods to the `TransformBuilder`
896/// struct: `set_plot_colour_map`, which can be used to set the colour map that
897/// will be used by `plotters` to generate the images and `set_plot_folder`, which
898/// can be used to specify folder where the generated images should be placed. If
899/// no output folder is specified when the `plots` feature is enabled, no plots will
900/// be generated (code will still compile).
901///
902/// ## `enable_edge_correction`
903/// Calling the `enable_edge_correction` method on the builder signals the
904/// watershed implementations that they should make sure that the edges of the
905/// image are properly included in the watershed transform. This option is disabled
906/// by default since performing this "edge correction" can incur a significant
907/// performance/memory usage hit.
908pub struct TransformBuilder<T = ()> {
909 //Plotting options
910 #[cfg(feature = "plots")]
911 plot_path: Option<std::path::PathBuf>,
912 #[cfg(feature = "plots")]
913 plot_colour_map: Option<
914 fn(count: usize, min: usize, max: usize) -> Result<RGBColor, Box<dyn std::error::Error>>,
915 >,
916
917 //Basic transform options
918 max_water_level: u8,
919 edge_correction: bool,
920
921 //Hooks
922 wlvl_hook: Option<fn(HookCtx) -> T>,
923}
924
925impl Default for TransformBuilder<()> {
926 fn default() -> Self {
927 TransformBuilder::new()
928 }
929}
930
931impl<T> TransformBuilder<T> {
932 /// Creates a new instance of `TransformBuilder<T>` which can be used to
933 /// construct a watershed transform that runs custom code each time the water
934 /// level is raised. If you do not use this functionality, use `default()`
935 /// instead.
936 pub const fn new() -> Self {
937 TransformBuilder {
938 #[cfg(feature = "plots")]
939 plot_path: None,
940 #[cfg(feature = "plots")]
941 plot_colour_map: None,
942 max_water_level: NORMAL_MAX,
943 edge_correction: false,
944 wlvl_hook: None,
945 }
946 }
947
948 /// Set the maximum water level that the transform will reach. Note that the
949 /// maximum water level may not be set higher than `u8::MAX - 1` (254).
950 pub const fn set_max_water_lvl(mut self, max_water_lvl: u8) -> Self {
951 self.max_water_level = max_water_lvl;
952 self
953 }
954
955 /// Enables edge correction. Turning this setting on properly colours the edges
956 /// of the input image at the cost of increased memory consumption and two
957 /// full-image copies.
958 pub const fn enable_edge_correction(mut self) -> Self {
959 self.edge_correction = true;
960 self
961 }
962
963 /// Sets the water level hook. This function pointer is called every time the
964 /// water level is raised and may be used to implement custom statistics.
965 /// Implementations of the watershed algorithm that do no visit all water levels
966 /// are not guaranteed to call this hook at all.
967 pub const fn set_wlvl_hook(mut self, hook: fn(HookCtx) -> T) -> Self {
968 self.wlvl_hook = Some(hook);
969 self
970 }
971
972 #[cfg(feature = "plots")]
973 /// Set a custom colour map to be used by `plotters` when generating images
974 /// of the watershed transform.
975 pub const fn set_plot_colour_map(
976 mut self,
977 colour_map: fn(
978 count: usize,
979 min: usize,
980 max: usize,
981 ) -> Result<RGBColor, Box<dyn std::error::Error>>,
982 ) -> Self {
983 self.plot_colour_map = Some(colour_map);
984 self
985 }
986
987 #[cfg(feature = "plots")]
988 /// Set output folder for the images generated during the watershed transform.
989 /// If no output folder is specified, no images will be generated, even with
990 /// the `plots` feature gate enabled.
991 pub fn set_plot_folder(mut self, path: &std::path::Path) -> Self {
992 self.plot_path = Some(path.to_path_buf());
993 self
994 }
995
996 /// Build a `MergingWatershed<T>` from the current builder
997 /// configuration.
998 pub fn build_merging(self) -> Result<MergingWatershed<T>, BuildErr> {
999 //Check if the max water level makes sense
1000 if self.max_water_level > NORMAL_MAX {
1001 Err(BuildErr::MaxToHigh(self.max_water_level))?
1002 } else if self.max_water_level <= ALWAYS_FILL {
1003 Err(BuildErr::MaxToLow(self.max_water_level))?
1004 }
1005
1006 Ok(MergingWatershed {
1007 //Plot options
1008 #[cfg(feature = "plots")]
1009 plot_path: self.plot_path,
1010 #[cfg(feature = "plots")]
1011 plot_colour_map: self.plot_colour_map.unwrap_or(plotting::viridis),
1012
1013 //Required options
1014 max_water_level: self.max_water_level,
1015 edge_correction: self.edge_correction,
1016
1017 //Hooks
1018 wlvl_hook: self.wlvl_hook,
1019 })
1020 }
1021
1022 /// Build a `SegmentingWatershed<T>` from the current builder
1023 /// configuration.
1024 pub fn build_segmenting(self) -> Result<SegmentingWatershed<T>, BuildErr> {
1025 //Check if the max water level makes sense
1026 if self.max_water_level > NORMAL_MAX {
1027 Err(BuildErr::MaxToHigh(self.max_water_level))?
1028 } else if self.max_water_level <= ALWAYS_FILL {
1029 Err(BuildErr::MaxToLow(self.max_water_level))?
1030 }
1031
1032 Ok(SegmentingWatershed {
1033 //Plot options
1034 #[cfg(feature = "plots")]
1035 plot_path: self.plot_path,
1036 #[cfg(feature = "plots")]
1037 plot_colour_map: self.plot_colour_map.unwrap_or(plotting::viridis),
1038
1039 //Required options
1040 max_water_level: self.max_water_level,
1041 edge_correction: self.edge_correction,
1042
1043 //Hooks
1044 wlvl_hook: self.wlvl_hook,
1045 })
1046 }
1047}
1048
1049#[derive(Debug, Clone)]
1050/// Errors that may occur during the build process
1051pub enum BuildErr {
1052 MaxToHigh(u8),
1053 MaxToLow(u8)
1054}
1055
1056impl std::error::Error for BuildErr {}
1057impl std::fmt::Display for BuildErr {
1058 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1059 use BuildErr::*;
1060 match self {
1061 MaxToHigh(max) => write!(f, "Maximum water level set to {max}, which is higher than the maximum allowed value {NORMAL_MAX}"),
1062 MaxToLow(max) => write!(f, "Maximum water level set to {max}, which is lower than the minimum allowed value {NEVER_FILL}")
1063 }
1064 }
1065}
1066
1067/// This trait contains useful functions for preparing images to be used as input
1068/// for a watershed transform
1069pub trait WatershedUtils {
1070 /// The `pre_processor` function can convert an array of any numeric data-type
1071 /// `T` into an array of `u8`. It converts special float values (if `T` is a
1072 /// float type) to `u8` values that implementations of the watershed transform
1073 /// in this crate know how to handle.
1074 ///
1075 /// In particular: `NaN` and positive infinity are mapped to the special
1076 /// `NEVER_FILL` value, and negative infinity is mapped to the special `ALWAYS_FILL`
1077 /// value.
1078 ///
1079 /// This function also automatically clamps the pixel values of the array to
1080 /// the full range of non-special `u8` values.
1081 fn pre_processor<T, D>(&self, img: nd::ArrayView<T, D>) -> nd::Array<u8, D>
1082 where
1083 T: Num + Copy + ToPrimitive + PartialOrd,
1084 D: nd::Dimension,
1085 {
1086 self.pre_processor_with_max::<NORMAL_MAX, T, D>(img)
1087 }
1088
1089 // The `pre_processor_with` function can convert an array of any numeric data-type
1090 /// `T` into an array of `u8`. It converts special float values (if `T` is a
1091 /// float type) to `u8` values that implementations of the watershed transform
1092 /// in this crate know how to handle.
1093 ///
1094 /// In particular: `NaN` and positive infinity are mapped to the special
1095 /// `NEVER_FILL` value, and negative infinity is mapped to the special `ALWAYS_FILL`
1096 /// value.
1097 ///
1098 /// This function also automatically clamps the pixel values of the array to
1099 /// the range between 0 and the supplied MAX constant.
1100 ///
1101 /// # example usage
1102 /// This function can be used as a replacement for the pre-processor if you want
1103 /// to normalise the input to the water transform to a different range than
1104 /// `1..u8::MAX-1`, like so:
1105 /// ```rust
1106 /// use rustronomy_watershed::prelude::*;
1107 /// use ndarray as nd;
1108 /// use ndarray_rand::{rand_distr::Uniform, RandomExt};
1109 ///
1110 /// //Set custom maximum waterlevel
1111 /// const MY_MAX: u8 = 127;
1112 ///
1113 /// //Create a random uniform distribution
1114 /// let rf = nd::Array2::<f64>::random((512, 512), Uniform::new(0.0, 1.0));
1115 ///
1116 /// //Set-up the watershed transform
1117 /// let watershed = TransformBuilder::default()
1118 /// .set_max_water_lvl(MY_MAX)
1119 /// .build_segmenting()
1120 /// .unwrap();
1121 ///
1122 /// //Run pre-processor (using turbofish syntax)
1123 /// let rf = watershed.pre_processor_with_max::<{MY_MAX}, _, _>(rf.view());
1124 ///
1125 /// //Find minima of the random field (to be used as seeds)
1126 /// let rf_mins = watershed.find_local_minima(rf.view());
1127 /// //Execute the watershed transform
1128 /// let output = watershed.transform(rf.view(), &rf_mins);
1129 /// ```
1130 ///
1131 /// # panics
1132 /// This function panics if MAX is bigger than or equal to `NEVER_FILL`, or
1133 /// smaller than or equal to `ALWAYS_FILL`.
1134 fn pre_processor_with_max<const MAX: u8, T, D>(
1135 &self,
1136 img: nd::ArrayView<T, D>,
1137 ) -> nd::Array<u8, D>
1138 where
1139 T: Num + Copy + ToPrimitive + PartialOrd,
1140 D: nd::Dimension,
1141 {
1142 //Panic if MAX is invalid (dear compiler, please remove this code path ty <3)
1143 assert!(MAX < NEVER_FILL);
1144 assert!(MAX > ALWAYS_FILL);
1145
1146 //Calculate max and min values
1147 let min = img
1148 .iter()
1149 .fold(T::zero(), |acc, x| if *x < acc && x.to_f64().unwrap().is_finite() { *x } else { acc })
1150 .to_f64()
1151 .unwrap();
1152 let max = img
1153 .iter()
1154 .fold(T::zero(), |acc, x| if *x > acc && x.to_f64().unwrap().is_finite() { *x } else { acc })
1155 .to_f64()
1156 .unwrap();
1157
1158 //Map image to u8 range, taking care of NaN and infty
1159 img.mapv(|x| -> u8 {
1160 let float = x.to_f64().unwrap();
1161 if float.is_normal() {
1162 //Clamp value to [0,1] range and then to [0, u8::MAX)
1163 let normal = (float - min) / (max - min);
1164 (normal * MAX as f64).to_u8().unwrap()
1165 } else if float.is_infinite() && !float.is_sign_negative() {
1166 //negative infinity, always fill
1167 ALWAYS_FILL
1168 } else {
1169 //Nans and positive infinity, never fill
1170 NEVER_FILL
1171 }
1172 })
1173 }
1174
1175 /// returns a vec of the positions of all the pixels that have a lower value
1176 /// than all their 8-way connected neighbours. Useful for generating seeds for
1177 /// the watershed transform.
1178 fn find_local_minima(&self, img: nd::ArrayView2<u8>) -> Vec<(usize, usize)> {
1179 //Window size and index of center window pixel
1180 const WINDOW: (usize, usize) = (3, 3);
1181 const MID: (usize, usize) = (1, 1);
1182
1183 nd::Zip::indexed(img.windows(WINDOW))
1184 .into_par_iter()
1185 .filter_map(|(idx, window)| {
1186 //Yield only pixels that are lower than their surroundings
1187 let target_val = window[MID];
1188 let neighbour_vals: Vec<u8> =
1189 neighbours_8con(&MID).into_iter().map(|idx| window[idx]).collect();
1190 if neighbour_vals.into_iter().all(|val| val < target_val) {
1191 Some((idx.0 + 1, idx.1 + 1))
1192 } else {
1193 None
1194 }
1195 })
1196 .collect()
1197 }
1198}
1199
1200impl<T> WatershedUtils for MergingWatershed<T> {}
1201impl<T> WatershedUtils for SegmentingWatershed<T> {}
1202
1203/// Actual trait for performing the watershed transform. It is implemented in
1204/// different ways by different versions of the algorithm. This trait is dyn-safe,
1205/// which means that trait objects may be constructed from it.
1206pub trait Watershed<T = ()> {
1207 /// Returns watershed transform of input image.
1208 fn transform(&self, input: nd::ArrayView2<u8>, seeds: &[(usize, usize)]) -> nd::Array2<usize>;
1209
1210 /// Runs the watershed transform, executing the hook specified by the
1211 /// `TransformBuilder` (if there is one) each time the water level is raised.
1212 /// The results from running the hook each time are collected into a vec and
1213 /// returned by this function.
1214 fn transform_with_hook(&self, input: nd::ArrayView2<u8>, seeds: &[(usize, usize)]) -> Vec<T>;
1215
1216 /// Returns a Vec containing the areas of all the lakes per water level. The
1217 /// length of the nested Vec is always equal to the number of seeds, although
1218 /// some lakes may have zero area (especially for the merging transform,
1219 /// see docs for `MergingWatershed`)
1220 fn transform_to_list(
1221 &self,
1222 input: nd::ArrayView2<u8>,
1223 seeds: &[(usize, usize)],
1224 ) -> Vec<(u8, Vec<usize>)>;
1225
1226 /// Returns a list of images where each image corresponds to a snapshot of the
1227 /// watershed transform at a particular water level.
1228 ///
1229 /// **Caution**: this function has to allocate a full image each time the water
1230 /// level is raised. This significantly increases memory usage. If you just
1231 /// want plots of the intermediate images, consider turning on the `plots`
1232 /// feature instead.
1233 fn transform_history(
1234 &self,
1235 input: nd::ArrayView2<u8>,
1236 seeds: &[(usize, usize)],
1237 ) -> Vec<(u8, nd::Array2<usize>)>;
1238}
1239
1240/// Implementation of the merging watershed algorithm.
1241///
1242/// See crate-level documentation for a general introduction to the algorithm.
1243///
1244/// The merging watershed transform is a slight variation on the segmenting
1245/// algorithm (see docs of the `SegmentingWatershed` struct). Instead of creating
1246/// a wall whenever two lakes meet, the merging watershed transform merges the
1247/// two lakes.
1248///
1249/// On a statistics level, the main difference between the merging and segmenting
1250/// watershed transforms is that the number of distinct lakes in the merging
1251/// watershed transform depends on the features in the image rather than the
1252/// number of (somewhat arbitrarily chosen) lake-seeds. Therefore, one can do
1253/// statistics with the number of lakes. In addition, the output of the merging
1254/// transform does not depend strongly on the precise way the minima were chosen.
1255///
1256/// # Memory usage
1257/// The watershed transform creates an `Array2<usize>` of the same size as the
1258/// input array, which takes up a considerable amount of memory. In addition, it
1259/// allocates space for a colour-map (`Vec<usize>` with a length equal to the
1260/// number of seeds) and some other intermediate, smaller vec's. One can count on
1261/// the memory usage being about ~2.5x the size of the input array.
1262///
1263/// ## Memory usage of `transform_history`
1264/// The `transform_history` method makes a copy of the `Array2<usize>` image used
1265/// during the watershed transform to keep track of the intermediate images. As
1266/// such, it allocates a new `Array2<usize>` for each water level, which increases
1267/// memory usage by a factor equal to the maximum water level as configured by
1268/// the `TransformBuilder`.
1269///
1270/// # Output
1271/// The three methods of the `Watershed` trait each return a different set of
1272/// parameters based on the watershed transform of the input image:
1273/// - `transform` simply returns the watershed transform of the image. This is
1274/// not very interesting in the case of the merging watershed transform, since
1275/// its output is just an image with all pixels having the same colour.
1276/// - `transform_to_list` returns a list of the areas of all the lakes at each
1277/// water level. Its return type is`Vec<(u8, Vec<usize>)>`, where the `u8` equals
1278/// the water level and the `Vec<usize>` is the list of areas of all the lakes
1279/// at each water level. The `Vec<usize>` is the same length for each water level,
1280/// but may contain zero-sided lakes. The water levels are returned in order.
1281/// - `transform_history` returns a list of intermediate images of the watershed
1282/// transform at every water level. Its return type is `Vec<(u8, ndarray::Array2<usize>)`,
1283/// where the `u8` holds the water level that each `Array2` snapshot was taken at.
1284/// The water levels are returned in order.
1285///
1286/// # Artifacts and peculiarities
1287/// Due to some implementation details, the 1px-wide edges of the input array are
1288/// not accessible to the watershed transform. They will thus remain unfilled for
1289/// the entire duration of the transform.
1290///
1291/// A workaround can be enabled by calling `enable_edge_correction` on the
1292/// `TransformBuilder`. Enabling this setting copies the input image into a
1293/// new array, 1px wider on all sides. This padded array is then used as the
1294/// actual input to the watershed transform. The final output of the transform is
1295/// a copy of this intermediate array with the padding removed. The padding also
1296/// does not show up in the output of intermediate plots.
1297pub struct MergingWatershed<T = ()> {
1298 //Plot options
1299 #[cfg(feature = "plots")]
1300 plot_path: Option<std::path::PathBuf>,
1301 #[cfg(feature = "plots")]
1302 plot_colour_map:
1303 fn(count: usize, min: usize, max: usize) -> Result<RGBColor, Box<dyn std::error::Error>>,
1304
1305 //Required options
1306 max_water_level: u8,
1307 edge_correction: bool,
1308
1309 //Hooks
1310 wlvl_hook: Option<fn(HookCtx) -> T>,
1311}
1312
1313impl<T> MergingWatershed<T> {
1314 fn clone_with_hook<U>(&self, hook: fn(HookCtx) -> U) -> MergingWatershed<U> {
1315 MergingWatershed {
1316 #[cfg(feature = "plots")]
1317 plot_path: self.plot_path.clone(),
1318 #[cfg(feature = "plots")]
1319 plot_colour_map: self.plot_colour_map,
1320 max_water_level: self.max_water_level,
1321 edge_correction: self.edge_correction,
1322 wlvl_hook: Some(hook),
1323 }
1324 }
1325}
1326
1327impl<T> Watershed<T> for MergingWatershed<T> {
1328 fn transform_with_hook(&self, input: nd::ArrayView2<u8>, seeds: &[(usize, usize)]) -> Vec<T> {
1329 //(1a) make an image for holding the diffserent water colours
1330 let shape = if self.edge_correction {
1331 //If the edge correction is enabled, we have to pad the input with a 1px
1332 //wide border, which increases the size shape of the output image by two
1333 [input.shape()[0] + 2, input.shape()[1] + 2]
1334 } else {
1335 [input.shape()[0], input.shape()[1]]
1336 };
1337 let mut output = nd::Array2::<usize>::zeros(shape);
1338
1339 //(1b) reshape the input image if necessary
1340 let mut padded_input =
1341 if self.edge_correction { Some(nd::Array2::<u8>::zeros(shape)) } else { None };
1342 let input = if self.edge_correction {
1343 //Copy the input pixel values into the new padded image
1344 nd::Zip::from(
1345 padded_input
1346 .as_mut()
1347 .expect("corrected_input was None, which should be impossible. Please report this bug.")
1348 .slice_mut(nd::s![1..(shape[0] - 1), 1..(shape[1] - 1)]),
1349 )
1350 .and(input)
1351 .into_par_iter()
1352 .for_each(|(a, &b)| *a = b);
1353 padded_input.as_ref().unwrap().view()
1354 } else {
1355 input.reborrow()
1356 };
1357
1358 //(2) set "colours" for each of the starting points
1359 // The colours should range from 1 to seeds.len()
1360 let mut colours: Vec<usize> = (1..=seeds.len()).into_iter().collect();
1361 let seed_colours: Vec<_> =
1362 colours.iter().zip(seeds.iter()).map(|(col, (x, z))| (*col, (*x, *z))).collect();
1363
1364 //Colour the starting pixels
1365 for (&idx, &col) in seeds.iter().zip(colours.iter()) {
1366 output[idx] = col;
1367 }
1368 //Set the zeroth colour to UNCOLOURED!
1369 colours.insert(UNCOLOURED, UNCOLOURED);
1370
1371 #[cfg(feature = "debug")]
1372 println!("starting with {} lakes", colours.len());
1373
1374 //(3) set-up progress bar
1375 #[cfg(feature = "progress")]
1376 let bar = set_up_bar(self.max_water_level);
1377
1378 //(4) count lakes for all water levels
1379 (0..=self.max_water_level)
1380 .into_iter()
1381 .map(|water_level| {
1382 //(logging) make a new perfreport
1383 #[cfg(feature = "debug")]
1384 let mut perf = crate::performance_monitoring::PerfReport::default();
1385 #[cfg(feature = "debug")]
1386 let loop_start = std::time::Instant::now();
1387
1388 /*(i) Colour all flooded pixels connected to a source
1389 We have to loop multiple times because there may be plateau's. These
1390 require us to colour more than just one neighbouring pixel -> we need
1391 to loop until there are no more uncoloured, flooded pixels connected to
1392 a source left.
1393 */
1394 'colouring_loop: loop {
1395 #[cfg(feature = "progress")]
1396 {
1397 bar.tick(); //Tick the progressbar
1398 }
1399 #[cfg(feature = "debug")]
1400 {
1401 perf.loops += 1;
1402 }
1403
1404 #[cfg(feature = "debug")]
1405 let iter_start = std::time::Instant::now();
1406
1407 /*(A) Find pixels to colour this iteration
1408 We first look for all pixels that are uncoloured, flooded and directly
1409 attached to a coloured pixel. We do this in parallel. We cannot, however,
1410 change the pixel colours *and* look for pixels to colour at the same time.
1411 That is why we collect all pixels to colour in a vector, and later update
1412 the map.
1413 */
1414 let pix_to_colour = find_flooded_px(input.view(), output.view(), water_level);
1415
1416 #[cfg(feature = "debug")]
1417 perf.big_iter_ms.push(iter_start.elapsed().as_millis() as usize);
1418
1419 /*(B) Colour pixels that we found in step (A)
1420 If there are no pixels to be coloured anymore, we can break from this
1421 loop and raise the water level
1422 */
1423 if pix_to_colour.is_empty() {
1424 //No more connected, flooded pixels left -> raise water level
1425 break 'colouring_loop;
1426 } else {
1427 //We have pixels to colour
1428 #[cfg(feature = "debug")]
1429 let colour_start = std::time::Instant::now();
1430
1431 pix_to_colour.into_iter().for_each(|(idx, col)| {
1432 output[idx] = col;
1433 });
1434
1435 #[cfg(feature = "debug")]
1436 perf.colouring_mus.push(colour_start.elapsed().as_micros() as usize);
1437 }
1438 }
1439
1440 /* (ii) Merge all touching regions
1441 Now that we have coloured all colourable pixels, we have to start
1442 merging regions of different colours that border each other
1443 We do this by making a look-up table for the colours. Each colour can
1444 look-up what its new colour will be.
1445 */
1446 #[cfg(feature = "debug")]
1447 let merge_start = std::time::Instant::now();
1448
1449 //(A) Find all colours that have to be merged
1450 let to_merge = find_merge(output.view());
1451 let num_mergers = to_merge.len();
1452
1453 /*(B) construct a colour map
1454 The colour map holds the output colour at the index equal to the input
1455 colour. A 1->1 identity map is therefore just a vec with its index as an
1456 entry.
1457
1458 The UNCOLOURED (0) colour always has to be mapped to UNCOLOURED!
1459 */
1460 make_colour_map(&mut colours, &to_merge);
1461 assert!(colours[UNCOLOURED] == UNCOLOURED);
1462
1463 //(C) Recolour the canvas with the colour map if the map is not empty
1464 if num_mergers > 0 {
1465 recolour(output.view_mut(), &colours);
1466 }
1467 #[cfg(feature = "debug")]
1468 {
1469 perf.merge_ms = merge_start.elapsed().as_millis() as usize;
1470 }
1471
1472 //(iii) Plot current state of the watershed transform
1473 #[cfg(feature = "plots")]
1474 if let Some(ref path) = self.plot_path {
1475 if let Err(err) = plotting::plot_slice(
1476 if self.edge_correction {
1477 //Do not plot the edge correction padding
1478 output.slice(nd::s![1..(shape[0] - 1), 1..(shape[1] - 1)])
1479 } else {
1480 output.view()
1481 },
1482 &path.join(&format!("ws_lvl{water_level}.png")),
1483 self.plot_colour_map,
1484 ) {
1485 println!("Could not make watershed plot. Error: {err}")
1486 }
1487 }
1488
1489 //(iv) print performance report
1490 #[cfg(all(feature = "debug", feature = "progress"))]
1491 {
1492 //In this combination we have a progress bar, we should use it to print
1493 perf.total_ms = loop_start.elapsed().as_millis() as usize;
1494 bar.println(format!("{perf}"));
1495 }
1496 #[cfg(all(feature = "debug", not(feature = "progress")))]
1497 {
1498 //We do not have a progress bar, so a plain println! will have to do
1499 perf.total_ms = loop_start.elapsed().as_millis() as usize;
1500 println!("{perf}");
1501 }
1502
1503 //(v) Update progressbar and plot stuff
1504 #[cfg(feature = "progress")]
1505 {
1506 bar.inc(1);
1507 }
1508
1509 //(vi) Execute hook (if one is provided)
1510 self.wlvl_hook.and_then(|hook| {
1511 Some(hook(HookCtx::ctx(
1512 water_level,
1513 self.max_water_level,
1514 input.view(),
1515 output.view(),
1516 &seed_colours,
1517 )))
1518 })
1519 })
1520 .filter_map(|x| x)
1521 .collect()
1522 }
1523
1524 fn transform(&self, input: nd::ArrayView2<u8>, _seeds: &[(usize, usize)]) -> nd::Array2<usize> {
1525 //Note: the implementation of `transform` is trivial for the merging transfo
1526
1527 //(1) make an image for holding the different water colours
1528 let shape = [input.shape()[0], input.shape()[1]];
1529 let mut output = nd::Array2::<usize>::zeros(shape);
1530
1531 //(2) give all pixels except the edge a different colour
1532 output.slice_mut(nd::s![1..shape[0] - 1, 1..shape[1] - 1]).mapv_inplace(|_| 123);
1533
1534 //Return the transformed image
1535 return output;
1536 }
1537
1538 fn transform_history(
1539 &self,
1540 input: nd::ArrayView2<u8>,
1541 seeds: &[(usize, usize)],
1542 ) -> Vec<(u8, nd::Array2<usize>)> {
1543 //(1) Make a copy of self with the appropriate hook
1544 let proper_transform =
1545 self.clone_with_hook(|ctx| (ctx.water_level, ctx.colours.to_owned()));
1546
1547 //(2) Perform transform with new hook
1548 proper_transform.transform_with_hook(input, seeds)
1549 }
1550
1551 fn transform_to_list(
1552 &self,
1553 input: nd::ArrayView2<u8>,
1554 seeds: &[(usize, usize)],
1555 ) -> Vec<(u8, Vec<usize>)> {
1556 //(1) Make a copy of self with the appropriate hook
1557 let proper_transform = self.clone_with_hook(find_lake_sizes);
1558
1559 //(2) Perform transform with new hook
1560 proper_transform.transform_with_hook(input, seeds)
1561 }
1562}
1563
1564/// Implementation of the segmenting watershed algorithm.
1565///
1566/// See crate-level documentation for a general introduction to the algorithm.
1567///
1568/// The segmenting watershed algorithm forms lakes from pre-defined local minima
1569/// by raising an imaginary water level. Once the water level increases past the
1570/// height of a minimum, it starts filling neighbouring pixels that are also
1571/// below the water level. These poodles grow larger as the water level rises.
1572///
1573/// When two lakes originating from different local minima meet, an infinitely
1574/// high wall separating the two is created. This is wall-building is what makes
1575/// this version of the watershed algorithm an image *segmentation* algorithm.
1576///
1577/// ## Memory usage of `transform_history`
1578/// The `transform_history` method makes a copy of the `Array2<usize>` image used
1579/// during the watershed transform to keep track of the intermediate images. As
1580/// such, it allocates a new `Array2<usize>` for each water level, which increases
1581/// memory usage by a factor equal to the maximum water level as configured by
1582/// the `TransformBuilder`.
1583///
1584/// # Output
1585/// The three methods of the `Watershed` trait each return a different set of
1586/// parameters based on the watershed transform of the input image:
1587/// - `transform` simply returns the watershed transform of the image.
1588/// - `transform_to_list` returns a list of the areas of all the lakes at each
1589/// water level. Its return type is`Vec<(u8, Vec<usize>)>`, where the `u8` equals
1590/// the water level and the `Vec<usize>` is the list of areas of all the lakes
1591/// at each water level. The `Vec<usize>` is the same length for each water level,
1592/// but may contain zero-sided lakes. The water levels are returned in order.
1593/// - `transform_history` returns a list of intermediate images of the watershed
1594/// transform at every water level. Its return type is `Vec<(u8, ndarray::Array2<usize>)`,
1595/// where the `u8` holds the water level that each `Array2` snapshot was taken at.
1596/// The water levels are returned in order.
1597///
1598/// # Artifacts and peculiarities
1599/// Due to some implementation details, the 1px-wide edges of the input array are
1600/// not accessible to the watershed transform. They will thus remain unfilled for
1601/// the entire duration of the transform.
1602///
1603/// A workaround can be enabled by calling `enable_edge_correction` on the
1604/// `TransformBuilder`. Enabling this setting copies the input image into a
1605/// new array, 1px wider on all sides. This padded array is then used as the
1606/// actual input to the watershed transform. The final output of the transform is
1607/// a copy of this intermediate array with the padding removed. The padding also
1608/// does not show up in the output of intermediate plots.
1609pub struct SegmentingWatershed<T = ()> {
1610 //Plot options
1611 #[cfg(feature = "plots")]
1612 plot_path: Option<std::path::PathBuf>,
1613 #[cfg(feature = "plots")]
1614 plot_colour_map:
1615 fn(count: usize, min: usize, max: usize) -> Result<RGBColor, Box<dyn std::error::Error>>,
1616 max_water_level: u8,
1617 edge_correction: bool,
1618
1619 //Hooks
1620 wlvl_hook: Option<fn(HookCtx) -> T>,
1621}
1622
1623impl<T> SegmentingWatershed<T> {
1624 fn clone_with_hook<U>(&self, hook: fn(HookCtx) -> U) -> SegmentingWatershed<U> {
1625 SegmentingWatershed {
1626 #[cfg(feature = "plots")]
1627 plot_path: self.plot_path.clone(),
1628 #[cfg(feature = "plots")]
1629 plot_colour_map: self.plot_colour_map,
1630 max_water_level: self.max_water_level,
1631 edge_correction: self.edge_correction,
1632 wlvl_hook: Some(hook),
1633 }
1634 }
1635}
1636
1637impl<T> Watershed<T> for SegmentingWatershed<T> {
1638 fn transform_with_hook(&self, input: nd::ArrayView2<u8>, seeds: &[(usize, usize)]) -> Vec<T> {
1639 //(1a) make an image for holding the diffserent water colours
1640 let shape = if self.edge_correction {
1641 //If the edge correction is enabled, we have to pad the input with a 1px
1642 //wide border, which increases the size shape of the output image by two
1643 [input.shape()[0] + 2, input.shape()[1] + 2]
1644 } else {
1645 [input.shape()[0], input.shape()[1]]
1646 };
1647 let mut output = nd::Array2::<usize>::zeros(shape);
1648
1649 //(1b) reshape the input image if necessary
1650 let mut padded_input =
1651 if self.edge_correction { Some(nd::Array2::<u8>::zeros(shape)) } else { None };
1652 let input = if self.edge_correction {
1653 //Copy the input pixel values into the new padded image
1654 nd::Zip::from(
1655 padded_input
1656 .as_mut()
1657 .expect("corrected_input was None, which should be impossible. Please report this bug.")
1658 .slice_mut(nd::s![1..(shape[0] - 1), 1..(shape[1] - 1)]),
1659 )
1660 .and(input)
1661 .into_par_iter()
1662 .for_each(|(a, &b)| *a = b);
1663 padded_input.as_ref().unwrap().view()
1664 } else {
1665 input.reborrow()
1666 };
1667
1668 //(2) set "colours" for each of the starting points
1669 // The colours should range from 1 to seeds.len()
1670 let mut colours: Vec<usize> = (1..=seeds.len()).into_iter().collect();
1671 let seed_colours: Vec<_> =
1672 colours.iter().zip(seeds.iter()).map(|(col, (x, z))| (*col, (*x, *z))).collect();
1673
1674 //Colour the starting pixels
1675 for (&idx, &col) in seeds.iter().zip(colours.iter()) {
1676 output[idx] = col;
1677 }
1678 //Set the zeroth colour to UNCOLOURED!
1679 colours.insert(UNCOLOURED, UNCOLOURED);
1680
1681 #[cfg(feature = "debug")]
1682 println!("starting with {} lakes", colours.len());
1683
1684 //(3) set-up progress bar
1685 #[cfg(feature = "progress")]
1686 let bar = set_up_bar(self.max_water_level);
1687
1688 //(4) increase water level to specified maximum
1689 (0..=self.max_water_level)
1690 .into_iter()
1691 .map(|water_level| {
1692 //(logging) make a new perfreport
1693 #[cfg(feature = "debug")]
1694 let mut perf = crate::performance_monitoring::PerfReport::default();
1695 #[cfg(feature = "debug")]
1696 let loop_start = std::time::Instant::now();
1697
1698 /*(i) Colour all flooded pixels connected to a source
1699 We have to loop multiple times because there may be plateau's. These
1700 require us to colour more than just one neighbouring pixel -> we need
1701 to loop until there are no more uncoloured, flooded pixels connected to
1702 a source left.
1703 */
1704 'colouring_loop: loop {
1705 #[cfg(feature = "progress")]
1706 {
1707 bar.tick(); //Tick the progressbar
1708 }
1709 #[cfg(feature = "debug")]
1710 {
1711 perf.loops += 1;
1712 }
1713
1714 #[cfg(feature = "debug")]
1715 let iter_start = std::time::Instant::now();
1716
1717 /*(A) Find pixels to colour this iteration
1718 We first look for all pixels that are uncoloured, flooded and directly
1719 attached to a coloured pixel. We do this in parallel. We cannot, however,
1720 change the pixel colours *and* look for pixels to colour at the same time.
1721 That is why we collect all pixels to colour in a vector, and later update
1722 the map.
1723 */
1724 let pix_to_colour = find_flooded_px(input.view(), output.view(), water_level);
1725
1726 #[cfg(feature = "debug")]
1727 perf.big_iter_ms.push(iter_start.elapsed().as_millis() as usize);
1728
1729 /*(B) Colour pixels that we found in step (A)
1730 If there are no pixels to be coloured anymore, we can break from this
1731 loop and raise the water level
1732 */
1733 if pix_to_colour.is_empty() {
1734 //No more connected, flooded pixels left -> raise water level
1735 break 'colouring_loop;
1736 } else {
1737 //We have pixels to colour
1738 #[cfg(feature = "debug")]
1739 let colour_start = std::time::Instant::now();
1740
1741 pix_to_colour.into_iter().for_each(|(idx, col)| {
1742 output[idx] = col;
1743 });
1744
1745 #[cfg(feature = "debug")]
1746 perf.colouring_mus.push(colour_start.elapsed().as_micros() as usize);
1747 }
1748 }
1749
1750 /* (ii) Merge all touching regions
1751 We do not do this for the segmenting transform!
1752 */
1753 #[cfg(feature = "debug")]
1754 {
1755 perf.merge_ms = 0;
1756 }
1757
1758 //(iii) Plot current state of the watershed transform
1759 #[cfg(feature = "plots")]
1760 if let Some(ref path) = self.plot_path {
1761 if let Err(err) = plotting::plot_slice(
1762 if self.edge_correction {
1763 //Do not plot the edge correction padding
1764 output.slice(nd::s![1..(shape[0] - 1), 1..(shape[1] - 1)])
1765 } else {
1766 output.view()
1767 },
1768 &path.join(&format!("ws_lvl{water_level}.png")),
1769 self.plot_colour_map,
1770 ) {
1771 println!("Could not make watershed plot. Error: {err}")
1772 }
1773 }
1774
1775 //(iv) print performance report
1776 #[cfg(all(feature = "debug", feature = "progress"))]
1777 {
1778 //In this combination we have a progress bar, we should use it to print
1779 perf.total_ms = loop_start.elapsed().as_millis() as usize;
1780 bar.println(format!("{perf}"));
1781 }
1782 #[cfg(all(feature = "debug", not(feature = "progress")))]
1783 {
1784 //We do not have a progress bar, so a plain println! will have to do
1785 perf.total_ms = loop_start.elapsed().as_millis() as usize;
1786 println!("{perf}");
1787 }
1788
1789 //(v) Update progressbar and plot stuff
1790 #[cfg(feature = "progress")]
1791 {
1792 bar.inc(1);
1793 }
1794
1795 //(vi) Execute hook (if one is provided)
1796 self.wlvl_hook.and_then(|hook| {
1797 Some(hook(HookCtx::ctx(
1798 water_level,
1799 self.max_water_level,
1800 input.view(),
1801 output.view(),
1802 &seed_colours,
1803 )))
1804 })
1805 })
1806 .filter_map(|x| x)
1807 .collect()
1808 }
1809
1810 fn transform(&self, input: nd::ArrayView2<u8>, seeds: &[(usize, usize)]) -> nd::Array2<usize> {
1811 //(1) Make a copy of self with the appropriate hook
1812 let proper_transform = self.clone_with_hook(|ctx| {
1813 if ctx.water_level == ctx.max_water_level {
1814 Some(ctx.colours.to_owned())
1815 } else {
1816 None
1817 }
1818 });
1819
1820 //(2) Perform transform with new hook
1821 proper_transform.transform_with_hook(input, seeds)[0].as_ref().expect("no output?").clone()
1822 }
1823
1824 fn transform_history(
1825 &self,
1826 input: nd::ArrayView2<u8>,
1827 seeds: &[(usize, usize)],
1828 ) -> Vec<(u8, nd::Array2<usize>)> {
1829 //(1) Make a copy of self with the appropriate hook
1830 let proper_transform =
1831 self.clone_with_hook(|ctx| (ctx.water_level, ctx.colours.to_owned()));
1832
1833 //(2) Perform transform with new hook
1834 proper_transform.transform_with_hook(input, seeds)
1835 }
1836
1837 fn transform_to_list(
1838 &self,
1839 input: nd::ArrayView2<u8>,
1840 seeds: &[(usize, usize)],
1841 ) -> Vec<(u8, Vec<usize>)> {
1842 //(1) Make a copy of self with the appropriate hook
1843 let proper_transform = self.clone_with_hook(find_lake_sizes);
1844
1845 //(2) Perform transform with new hook
1846 proper_transform.transform_with_hook(input, seeds)
1847 }
1848
1849}