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}