#![doc(
html_logo_url = "https://raw.githubusercontent.com/smups/rustronomy/main/logos/Rustronomy_ferris.png?raw=true"
)]
use ndarray as nd;
use num_traits::{Num, ToPrimitive};
use rand::{seq::SliceRandom, Rng};
use rayon::prelude::*;
#[cfg(feature = "jemalloc")]
#[global_allocator]
static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
#[cfg(feature = "progress")]
use indicatif;
pub const UNCOLOURED: usize = 0;
pub const NORMAL_MAX: u8 = u8::MAX - 1;
pub const ALWAYS_FILL: u8 = u8::MIN;
pub const NEVER_FILL: u8 = u8::MAX;
pub mod prelude {
pub use crate::{MergingWatershed, TransformBuilder, Watershed, WatershedUtils};
#[cfg(feature = "plots")]
pub mod color_maps {
pub use crate::plotting::grey_scale;
pub use crate::plotting::inferno;
pub use crate::plotting::magma;
pub use crate::plotting::plasma;
pub use crate::plotting::viridis;
}
}
#[cfg(feature = "progress")]
fn set_up_bar(water_max: u8) -> indicatif::ProgressBar {
const TEMPLATE: &str = "{spinner}[{elapsed}/{duration}] water level {pos}/{len}{bar:60}";
let style = indicatif::ProgressStyle::with_template(TEMPLATE);
let bar = indicatif::ProgressBar::new(water_max as u64);
bar.set_style(style.unwrap());
return bar;
}
#[inline]
fn neighbours_8con(index: &(usize, usize)) -> Vec<(usize, usize)> {
let (x, y): (isize, isize) = (index.0 as isize, index.1 as isize);
[
(x + 1, y),
(x + 1, y + 1),
(x + 1, y - 1),
(x, y + 1),
(x, y - 1),
(x - 1, y),
(x - 1, y + 1),
(x - 1, y - 1),
]
.iter()
.filter_map(|&(x, y)| if x < 0 || y < 0 { None } else { Some((x as usize, y as usize)) })
.collect()
}
#[inline]
fn neighbours_4con(index: &(usize, usize)) -> Vec<(usize, usize)> {
let (x, y): (isize, isize) = (index.0 as isize, index.1 as isize);
[(x + 1, y), (x, y + 1), (x, y - 1), (x - 1, y)]
.iter()
.filter_map(|&(x, y)| if x < 0 || y < 0 { None } else { Some((x as usize, y as usize)) })
.collect()
}
fn find_flooded_px(
img: nd::ArrayView2<u8>,
cols: nd::ArrayView2<usize>,
lvl: u8,
) -> Vec<((usize, usize), usize)> {
const WINDOW: (usize, usize) = (3, 3);
const MID: (usize, usize) = (1, 1);
nd::Zip::indexed(cols.windows(WINDOW))
.and(img.windows(WINDOW))
.into_par_iter()
.filter(|&(_idx, _col_wd, img_wd)| img_wd[MID] <= lvl)
.filter(|&(_idx, col_wd, _img_wd)| col_wd[MID] == UNCOLOURED)
.filter(|&(_idx, col_wd, _img_wd)| {
let neigh_idx_4c = neighbours_4con(&MID);
!neigh_idx_4c.iter().all(|&idx| col_wd[idx] == UNCOLOURED)
})
.map(|(idx, col_wd, _img_wd)| ((idx.0 + 1, idx.1 + 1), col_wd))
.map(|(idx, col_wd)| {
let neigh_col_4c = neighbours_4con(&MID)
.into_iter()
.map(|neigh_idx| col_wd[neigh_idx])
.filter(|&col| col != UNCOLOURED)
.collect::<Vec<usize>>();
let col0 = *neigh_col_4c.get(0).expect("All neighbours were uncoloured!");
if neigh_col_4c.iter().all(|&col| col == col0) {
(idx, col0)
} else {
let rand_idx = rand::thread_rng().gen_range(0..neigh_col_4c.len());
let rand_col = *neigh_col_4c.get(rand_idx).expect("picking random px went wrong?");
(idx, rand_col)
}
})
.collect()
}
#[test]
fn test_find_px() {
assert!(UNCOLOURED == 0);
let input = nd::array![
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 5, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 5, 0, 0, 1, 0],
[0, 0, 5, 4, 5, 0, 0, 0],
[0, 0, 0, 5, 0, 0, 0, 0],
];
let colours = nd::array![
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 0, 1, 0],
[0, 1, 0, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 0, 1, 0],
[0, 1, 1, 1, 0, 0, 1, 0],
[0, 1, 1, 0, 1, 1, 0, 0],
[0, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0]
];
let answer1 = [(1, 5), (2, 2), (4, 4), (5, 6)];
let attempt1 = find_flooded_px(input.view(), colours.view(), 2)
.into_iter()
.map(|(x, _)| x)
.collect::<Vec<_>>();
for answer in answer1 {
assert!(attempt1.contains(&answer))
}
}
#[derive(Eq, Clone, Copy, Default, Debug)]
#[repr(transparent)]
struct Merge([usize; 2]);
impl PartialEq for Merge {
#[inline(always)]
fn eq(&self, other: &Self) -> bool {
let [x1, y1] = self.0;
let [x2, y2] = other.0;
(x1 == x2 && y1 == y2) || (x1 == y2 && y1 == x2)
}
}
#[test]
fn test_merge_eq() {
assert_eq!(Merge([1, 2]), Merge([2, 1]));
}
#[inline(always)]
fn sort_by_small_big(this: &Merge, that: &Merge) -> std::cmp::Ordering {
use std::cmp::Ordering::*;
if this == that {
return Equal;
}
let (self_small, self_big) =
if this.0[0] > this.0[1] { (this.0[0], this.0[1]) } else { (this.0[0], this.0[1]) };
let (other_small, other_big) =
if that.0[0] > that.0[1] { (that.0[0], that.0[1]) } else { (that.0[1], that.0[0]) };
if self_small < other_small {
Less
} else if self_small > other_small {
Greater
} else if self_big < other_big {
Less
} else {
Greater
}
}
#[test]
fn test_merge_ord_small_big() {
use std::cmp::Ordering::*;
let cmp = sort_by_small_big;
assert_eq!(cmp(&Merge([2, 1]), &Merge([1, 1])), Greater);
assert_eq!(cmp(&Merge([1, 1]), &Merge([1, 2])), Less);
assert_eq!(cmp(&Merge([2, 1]), &Merge([1, 2])), Equal);
assert_eq!(cmp(&Merge([3, 8]), &Merge([4, 5])), Less);
}
#[inline(always)]
fn sort_by_big_small(this: &Merge, that: &Merge) -> std::cmp::Ordering {
use std::cmp::Ordering::*;
if this == that {
return Equal;
}
let (self_small, self_big) =
if this.0[0] > this.0[1] { (this.0[0], this.0[1]) } else { (this.0[0], this.0[1]) };
let (other_small, other_big) =
if that.0[0] > that.0[1] { (that.0[0], that.0[1]) } else { (that.0[1], that.0[0]) };
if self_big < other_big {
Less
} else if self_big > other_big {
Greater
} else if self_small < other_small {
Less
} else {
Greater
}
}
#[test]
fn test_merge_ord_big_small() {
use std::cmp::Ordering::*;
let cmp = sort_by_big_small;
assert_eq!(cmp(&Merge([2, 1]), &Merge([1, 1])), Greater);
assert_eq!(cmp(&Merge([1, 1]), &Merge([1, 2])), Less);
assert_eq!(cmp(&Merge([2, 1]), &Merge([1, 2])), Equal);
assert_eq!(cmp(&Merge([3, 8]), &Merge([4, 5])), Greater);
}
impl From<[usize; 2]> for Merge {
#[inline(always)]
fn from(value: [usize; 2]) -> Self {
Self(value)
}
}
impl From<Merge> for [usize; 2] {
#[inline(always)]
fn from(value: Merge) -> Self {
value.0
}
}
fn find_merge(col: nd::ArrayView2<usize>) -> Vec<Merge> {
const WINDOW: (usize, usize) = (3, 3);
const MID: (usize, usize) = (1, 1);
let mut merge = nd::Zip::from(col.windows(WINDOW))
.into_par_iter()
.filter(|&col_wd| col_wd.0[MID] != UNCOLOURED)
.map(|col_wd| -> (usize, Vec<usize>) {
let own_col = col_wd.0[MID];
let neighbour_cols = neighbours_4con(&MID)
.into_iter()
.map(|idx| col_wd.0[idx])
.filter(|&col| col != UNCOLOURED)
.collect();
(own_col, neighbour_cols)
})
.filter(|(_own_col, neigh_col)| !neigh_col.is_empty())
.map(|(own_col, neigh_col)| {
neigh_col
.into_iter()
.filter_map(|c| if c == own_col { None } else { Some(Merge::from([own_col, c])) })
.collect::<Vec<_>>()
})
.flatten()
.collect::<Vec<_>>();
merge.par_sort_unstable_by(sort_by_big_small);
merge.dedup();
merge.par_sort_unstable_by(sort_by_small_big);
merge.dedup();
return merge;
}
#[test]
fn test_find_merge() {
assert!(UNCOLOURED == 0);
let input = nd::array![
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 2, 2, 0, 1, 0],
[0, 1, 1, 2, 2, 0, 1, 0],
[0, 3, 3, 3, 3, 3, 3, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 4, 4, 0, 5, 5, 6, 0],
[0, 4, 4, 0, 0, 5, 6, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
];
let answer = vec![Merge([1, 2]), Merge([1, 3]), Merge([2, 3]), Merge([5, 6])];
let result = find_merge(input.view());
assert_eq!(answer.len(), result.len());
assert!(result.iter().all(|x| answer.contains(x)));
}
fn make_colour_map(base_map: &mut [usize], pair_mergers: &[Merge]) {
let mut full_mergers: Vec<Vec<usize>> = Vec::new();
'pair_loop: for &pair_merge in pair_mergers {
let [col1, col2]: [usize; 2] = pair_merge.into();
let mut connect = [None, None];
for (idx, region) in full_mergers.iter().enumerate() {
if region.contains(&col1) && region.contains(&col2) {
continue 'pair_loop;
} else if region.contains(&col1) || region.contains(&col2) {
if connect[0].is_none() {
connect[0] = Some(idx)
} else if connect[1].is_none() {
connect[1] = Some(idx);
break;
} else {
panic!("Unreachable code path!")
}
}
}
if connect == [None, None] {
full_mergers.push(vec![col1, col2]);
} else if let [Some(reg_idx), None] = connect {
let reg = full_mergers.get_mut(reg_idx).unwrap();
reg.extend_from_slice(&[col1, col2]);
reg.sort();
reg.dedup();
} else if let [Some(reg_idx1), Some(reg_idx2)] = connect {
let (reg1, reg2) = {
let (larger, smaller) =
if reg_idx1 > reg_idx2 { (reg_idx1, reg_idx2) } else { (reg_idx2, reg_idx1) };
let (head, tail) = full_mergers.split_at_mut(smaller + 1);
(&mut head[smaller], &mut tail[larger - smaller - 1])
};
reg1.append(reg2);
}
full_mergers = full_mergers.into_iter().filter(|region| !region.is_empty()).collect();
}
for merge in full_mergers {
let merged_col = *merge.get(0).expect("tried to merge zero regions");
base_map.iter_mut().filter(|x| merge.contains(x)).for_each(|x| *x = merged_col);
}
}
#[test]
fn test_make_colour_map() {
assert!(UNCOLOURED == 0);
let mut cmap;
let rng = &mut rand::thread_rng();
for _ in 0..10 {
cmap = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
make_colour_map(&mut cmap, &vec![Merge([1, 2])]);
assert!(cmap == [0, 1, 1, 3, 4, 5, 6, 7, 8, 9]);
cmap = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let mut input = vec![Merge([1, 2]), Merge([8, 9])];
input.shuffle(rng);
make_colour_map(&mut cmap, &input);
assert!(cmap == [0, 1, 1, 3, 4, 5, 6, 7, 8, 8]);
cmap = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let mut input = vec![Merge([1, 2]), Merge([2, 3])];
input.shuffle(rng);
make_colour_map(&mut cmap, &input);
assert!(cmap == [0, 1, 1, 1, 4, 5, 6, 7, 8, 9]);
cmap = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let mut input = vec![Merge([1, 2]), Merge([8, 9])];
input.shuffle(rng);
make_colour_map(&mut cmap, &input);
let mut input = vec![Merge([1, 7]), Merge([7, 8])];
input.shuffle(rng);
make_colour_map(&mut cmap, &input);
assert!(cmap == [0, 1, 1, 3, 4, 5, 6, 1, 1, 1]);
cmap = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let mut input = vec![Merge([1, 2]), Merge([3, 2]), Merge([2, 1])];
input.shuffle(rng);
make_colour_map(&mut cmap, &input);
assert!(cmap == [0, 1, 1, 1, 4, 5, 6, 7, 8, 9]);
}
}
#[inline(always)]
fn recolour(mut canvas: nd::ArrayViewMut2<usize>, colour_map: &[usize]) {
canvas.mapv_inplace(|px| colour_map[px])
}
#[test]
fn test_recolour() {
assert!(UNCOLOURED == 0);
let mut input = nd::array![
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 2, 2, 0, 1, 0],
[0, 1, 1, 2, 2, 0, 1, 0],
[0, 3, 3, 3, 3, 3, 3, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 4, 4, 0, 5, 5, 6, 0],
[0, 4, 4, 0, 0, 5, 6, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
];
let cmap = [0, 1, 1, 1, 4, 5, 5];
let answer = nd::array![
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 0, 1, 0],
[0, 1, 1, 1, 1, 0, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 4, 4, 0, 5, 5, 5, 0],
[0, 4, 4, 0, 0, 5, 5, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
];
recolour(input.view_mut(), &cmap);
assert_eq!(answer, input);
let cmap = [0, 1, 13498683, 13458, 4, 5, 134707134];
recolour(input.view_mut(), &cmap);
assert_eq!(answer, input);
}
#[inline]
fn find_lake_sizes(ctx: HookCtx) -> (u8, Vec<usize>) {
let mut lake_sizes = vec![0usize; ctx.colours.len() + 1];
ctx.colours.iter().for_each(|&x| {
*lake_sizes.get_mut(x).unwrap() += 1;
});
(ctx.water_level, lake_sizes)
}
#[cfg(feature = "debug")]
mod performance_monitoring {
#[derive(Clone, Debug, Default)]
pub struct PerfReport {
pub big_iter_ms: Vec<usize>,
pub colouring_mus: Vec<usize>,
pub loops: usize,
pub merge_ms: usize,
pub lake_count_ms: usize,
pub total_ms: usize,
}
impl PerfReport {
pub fn iter_avg(&self) -> f64 {
let num = self.big_iter_ms.len() as f64;
self.big_iter_ms.iter().map(|&x| x as f64).sum::<f64>() / num
}
pub fn iter_total(&self) -> f64 {
self.big_iter_ms.iter().map(|&x| x as f64).sum()
}
pub fn colour_avg(&self) -> f64 {
let num = self.big_iter_ms.len() as f64;
self.colouring_mus.iter().map(|&x| x as f64).sum::<f64>() / num
}
pub fn colour_total(&self) -> f64 {
self.colouring_mus.iter().map(|&x| x as f64).sum()
}
}
impl std::fmt::Display for PerfReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, ">---------[Performance Summary]---------")?;
writeln!(f, "> Looped {}x", self.loops)?;
writeln!(f, "> Iteration Average: {:.1}ms; Σ {:.0}ms", self.iter_avg(), self.iter_total())?;
writeln!(
f,
"> Colouring Average: {:.1}µs; Σ {:.0}µs",
self.colour_avg(),
self.colour_total()
)?;
writeln!(f, "> Merging: {}ms", self.merge_ms)?;
writeln!(f, "> Counting Lakes: {}ms", self.lake_count_ms)?;
writeln!(f, ">--------------------------------+ total")?;
writeln!(
f,
"> {}ms with {:.1}ms overhead (Δt)",
self.total_ms,
self.total_ms as f64
- self.iter_total()
- self.colour_total() / 1000.0
- self.merge_ms as f64
- self.lake_count_ms as f64
)
}
}
}
#[cfg(feature = "plots")]
pub mod plotting {
use ndarray as nd;
use num_traits::ToPrimitive;
use plotters::prelude::*;
use std::{error::Error, path::Path};
const NAN_COL: RGBColor = BLACK;
mod color_maps;
pub fn plot_slice<'a, T>(
slice: nd::ArrayView2<'a, T>,
file_name: &Path,
color_map: fn(count: T, min: T, max: T) -> Result<RGBColor, Box<dyn Error>>,
) -> Result<(), Box<dyn Error>>
where
T: Default + std::fmt::Display + std::cmp::PartialOrd + ToPrimitive + Copy,
{
let min = slice.iter().fold(T::default(), |f: T, x: &T| if *x < f { *x } else { f });
let max = slice.iter().fold(T::default(), |f: T, x: &T| if *x > f { *x } else { f });
let x_size = slice.shape()[0] as u32;
let y_size = slice.shape()[1] as u32;
let root = BitMapBackend::new(file_name, (x_size, y_size)).into_drawing_area();
root.fill(&WHITE)?;
let mut chart = ChartBuilder::on(&root).build_cartesian_2d(0..x_size, 0..y_size)?;
chart.configure_mesh().disable_mesh().disable_axes().draw()?;
let plotting_area = chart.plotting_area();
for ((x, y), px) in slice.indexed_iter() {
plotting_area.draw_pixel((x as u32, y as u32), &color_map(*px, min, max)?)?
}
root.present()?;
#[cfg(feature = "debug")]
println!("slice saved as png: {file_name:?}; max:{max:2}, min:{min:2}");
Ok(())
}
#[inline(always)]
pub fn grey_scale<T>(count: T, min: T, max: T) -> Result<RGBColor, Box<dyn Error>>
where
T: std::fmt::Display + std::cmp::PartialOrd + ToPrimitive,
{
if count <= min {
Ok(NAN_COL)
} else {
let gray = ((255.0f64 * count.to_f64().unwrap() + min.to_f64().unwrap())
/ max.to_f64().unwrap()) as u8;
Ok(RGBColor(gray, gray, gray))
}
}
#[inline(always)]
pub fn viridis<T>(count: T, min: T, max: T) -> Result<RGBColor, Box<dyn Error>>
where
T: std::fmt::Display + std::cmp::PartialOrd + ToPrimitive,
{
if count <= min {
Ok(NAN_COL)
} else {
let gray = ((255.0f64 * count.to_f64().unwrap() + min.to_f64().unwrap())
/ max.to_f64().unwrap()) as usize;
let color = color_maps::VIRIDIS[gray];
Ok(RGBColor((color[0] * 256.0) as u8, (color[1] * 256.0) as u8, (color[2] * 256.0) as u8))
}
}
#[inline(always)]
pub fn magma<T>(count: T, min: T, max: T) -> Result<RGBColor, Box<dyn Error>>
where
T: std::fmt::Display + std::cmp::PartialOrd + ToPrimitive,
{
if count <= min {
Ok(NAN_COL)
} else {
let gray = ((255.0f64 * count.to_f64().unwrap() + min.to_f64().unwrap())
/ max.to_f64().unwrap()) as usize;
let color = color_maps::MAGMA[gray];
Ok(RGBColor((color[0] * 256.0) as u8, (color[1] * 256.0) as u8, (color[2] * 256.0) as u8))
}
}
#[inline(always)]
pub fn plasma<T>(count: T, min: T, max: T) -> Result<RGBColor, Box<dyn Error>>
where
T: std::fmt::Display + std::cmp::PartialOrd + ToPrimitive,
{
if count <= min {
Ok(NAN_COL)
} else {
let gray = ((255.0f64 * count.to_f64().unwrap() + min.to_f64().unwrap())
/ max.to_f64().unwrap()) as usize;
let color = color_maps::PLASMA[gray];
Ok(RGBColor((color[0] * 256.0) as u8, (color[1] * 256.0) as u8, (color[2] * 256.0) as u8))
}
}
#[inline(always)]
pub fn inferno<T>(count: T, min: T, max: T) -> Result<RGBColor, Box<dyn Error>>
where
T: std::fmt::Display + std::cmp::PartialOrd + ToPrimitive,
{
if count <= min {
Ok(NAN_COL)
} else {
let gray = ((255.0f64 * count.to_f64().unwrap() + min.to_f64().unwrap())
/ max.to_f64().unwrap()) as usize;
let color = color_maps::INFERNO[gray];
Ok(RGBColor((color[0] * 256.0) as u8, (color[1] * 256.0) as u8, (color[2] * 256.0) as u8))
}
}
}
#[cfg(feature = "plots")]
use plotters::prelude::*;
#[derive(Clone)]
pub struct HookCtx<'a> {
pub water_level: u8,
pub max_water_level: u8,
pub image: nd::ArrayView2<'a, u8>,
pub colours: nd::ArrayView2<'a, usize>,
pub seeds: &'a [(usize, (usize, usize))],
}
impl<'a> HookCtx<'a> {
fn ctx(
water_level: u8,
max_water_level: u8,
image: nd::ArrayView2<'a, u8>,
colours: nd::ArrayView2<'a, usize>,
seeds: &'a [(usize, (usize, usize))],
) -> Self {
HookCtx { water_level, max_water_level, image, colours, seeds }
}
}
#[derive(Clone)]
pub struct TransformBuilder<T = ()> {
#[cfg(feature = "plots")]
plot_path: Option<std::path::PathBuf>,
#[cfg(feature = "plots")]
plot_colour_map: Option<
fn(count: usize, min: usize, max: usize) -> Result<RGBColor, Box<dyn std::error::Error>>,
>,
max_water_level: u8,
edge_correction: bool,
wlvl_hook: Option<fn(HookCtx) -> T>,
}
impl Default for TransformBuilder<()> {
fn default() -> Self {
TransformBuilder::new()
}
}
impl<T> TransformBuilder<T> {
pub const fn new() -> Self {
TransformBuilder {
#[cfg(feature = "plots")]
plot_path: None,
#[cfg(feature = "plots")]
plot_colour_map: None,
max_water_level: NORMAL_MAX,
edge_correction: false,
wlvl_hook: None,
}
}
pub const fn set_max_water_lvl(mut self, max_water_lvl: u8) -> Self {
self.max_water_level = max_water_lvl;
self
}
pub const fn enable_edge_correction(mut self) -> Self {
self.edge_correction = true;
self
}
pub const fn set_wlvl_hook(mut self, hook: fn(HookCtx) -> T) -> Self {
self.wlvl_hook = Some(hook);
self
}
#[cfg(feature = "plots")]
pub const fn set_plot_colour_map(
mut self,
colour_map: fn(
count: usize,
min: usize,
max: usize,
) -> Result<RGBColor, Box<dyn std::error::Error>>,
) -> Self {
self.plot_colour_map = Some(colour_map);
self
}
#[cfg(feature = "plots")]
pub fn set_plot_folder(mut self, path: &std::path::Path) -> Self {
self.plot_path = Some(path.to_path_buf());
self
}
pub fn build_merging(self) -> Result<MergingWatershed<T>, BuildErr> {
if self.max_water_level > NORMAL_MAX {
Err(BuildErr::MaxToHigh(self.max_water_level))?
} else if self.max_water_level <= ALWAYS_FILL {
Err(BuildErr::MaxToLow(self.max_water_level))?
}
Ok(MergingWatershed {
#[cfg(feature = "plots")]
plot_path: self.plot_path,
#[cfg(feature = "plots")]
plot_colour_map: self.plot_colour_map.unwrap_or(plotting::viridis),
max_water_level: self.max_water_level,
edge_correction: self.edge_correction,
wlvl_hook: self.wlvl_hook,
})
}
pub fn build_segmenting(self) -> Result<SegmentingWatershed<T>, BuildErr> {
if self.max_water_level > NORMAL_MAX {
Err(BuildErr::MaxToHigh(self.max_water_level))?
} else if self.max_water_level <= ALWAYS_FILL {
Err(BuildErr::MaxToLow(self.max_water_level))?
}
Ok(SegmentingWatershed {
#[cfg(feature = "plots")]
plot_path: self.plot_path,
#[cfg(feature = "plots")]
plot_colour_map: self.plot_colour_map.unwrap_or(plotting::viridis),
max_water_level: self.max_water_level,
edge_correction: self.edge_correction,
wlvl_hook: self.wlvl_hook,
})
}
}
#[derive(Debug, Clone)]
pub enum BuildErr {
MaxToHigh(u8),
MaxToLow(u8)
}
impl std::error::Error for BuildErr {}
impl std::fmt::Display for BuildErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use BuildErr::*;
match self {
MaxToHigh(max) => write!(f, "Maximum water level set to {max}, which is higher than the maximum allowed value {NORMAL_MAX}"),
MaxToLow(max) => write!(f, "Maximum water level set to {max}, which is lower than the minimum allowed value {NEVER_FILL}")
}
}
}
pub trait WatershedUtils {
fn pre_processor<T, D>(&self, img: nd::ArrayView<T, D>) -> nd::Array<u8, D>
where
T: Num + Copy + ToPrimitive + PartialOrd,
D: nd::Dimension,
{
self.pre_processor_with_max::<NORMAL_MAX, T, D>(img)
}
fn pre_processor_with_max<const MAX: u8, T, D>(
&self,
img: nd::ArrayView<T, D>,
) -> nd::Array<u8, D>
where
T: Num + Copy + ToPrimitive + PartialOrd,
D: nd::Dimension,
{
assert!(MAX < NEVER_FILL);
assert!(MAX > ALWAYS_FILL);
let min = img
.iter()
.fold(T::zero(), |acc, x| if *x < acc && x.to_f64().unwrap().is_finite() { *x } else { acc })
.to_f64()
.unwrap();
let max = img
.iter()
.fold(T::zero(), |acc, x| if *x > acc && x.to_f64().unwrap().is_finite() { *x } else { acc })
.to_f64()
.unwrap();
img.mapv(|x| -> u8 {
let float = x.to_f64().unwrap();
if float.is_normal() {
let normal = (float - min) / (max - min);
(normal * MAX as f64).to_u8().unwrap()
} else if float.is_infinite() && !float.is_sign_negative() {
ALWAYS_FILL
} else {
NEVER_FILL
}
})
}
fn find_local_minima(&self, img: nd::ArrayView2<u8>) -> Vec<(usize, usize)> {
const WINDOW: (usize, usize) = (3, 3);
const MID: (usize, usize) = (1, 1);
nd::Zip::indexed(img.windows(WINDOW))
.into_par_iter()
.filter_map(|(idx, window)| {
let target_val = window[MID];
let neighbour_vals: Vec<u8> =
neighbours_8con(&MID).into_iter().map(|idx| window[idx]).collect();
if neighbour_vals.into_iter().all(|val| val < target_val) {
Some((idx.0 + 1, idx.1 + 1))
} else {
None
}
})
.collect()
}
}
impl<T> WatershedUtils for MergingWatershed<T> {}
impl<T> WatershedUtils for SegmentingWatershed<T> {}
pub trait Watershed<T = ()> {
fn transform(&self, input: nd::ArrayView2<u8>, seeds: &[(usize, usize)]) -> nd::Array2<usize>;
fn transform_with_hook(&self, input: nd::ArrayView2<u8>, seeds: &[(usize, usize)]) -> Vec<T>;
fn transform_to_list(
&self,
input: nd::ArrayView2<u8>,
seeds: &[(usize, usize)],
) -> Vec<(u8, Vec<usize>)>;
fn transform_history(
&self,
input: nd::ArrayView2<u8>,
seeds: &[(usize, usize)],
) -> Vec<(u8, nd::Array2<usize>)>;
}
pub struct MergingWatershed<T = ()> {
#[cfg(feature = "plots")]
plot_path: Option<std::path::PathBuf>,
#[cfg(feature = "plots")]
plot_colour_map:
fn(count: usize, min: usize, max: usize) -> Result<RGBColor, Box<dyn std::error::Error>>,
max_water_level: u8,
edge_correction: bool,
wlvl_hook: Option<fn(HookCtx) -> T>,
}
impl<T> MergingWatershed<T> {
fn clone_with_hook<U>(&self, hook: fn(HookCtx) -> U) -> MergingWatershed<U> {
MergingWatershed {
#[cfg(feature = "plots")]
plot_path: self.plot_path.clone(),
#[cfg(feature = "plots")]
plot_colour_map: self.plot_colour_map,
max_water_level: self.max_water_level,
edge_correction: self.edge_correction,
wlvl_hook: Some(hook),
}
}
}
impl<T> Watershed<T> for MergingWatershed<T> {
fn transform_with_hook(&self, input: nd::ArrayView2<u8>, seeds: &[(usize, usize)]) -> Vec<T> {
let shape = if self.edge_correction {
[input.shape()[0] + 2, input.shape()[1] + 2]
} else {
[input.shape()[0], input.shape()[1]]
};
let mut output = nd::Array2::<usize>::zeros(shape);
let mut padded_input =
if self.edge_correction { Some(nd::Array2::<u8>::zeros(shape)) } else { None };
let input = if self.edge_correction {
nd::Zip::from(
padded_input
.as_mut()
.expect("corrected_input was None, which should be impossible. Please report this bug.")
.slice_mut(nd::s![1..(shape[0] - 1), 1..(shape[1] - 1)]),
)
.and(input)
.into_par_iter()
.for_each(|(a, &b)| *a = b);
padded_input.as_ref().unwrap().view()
} else {
input.reborrow()
};
let mut colours: Vec<usize> = (1..=seeds.len()).into_iter().collect();
let seed_colours: Vec<_> =
colours.iter().zip(seeds.iter()).map(|(col, (x, z))| (*col, (*x, *z))).collect();
for (&idx, &col) in seeds.iter().zip(colours.iter()) {
output[idx] = col;
}
colours.insert(UNCOLOURED, UNCOLOURED);
#[cfg(feature = "debug")]
println!("starting with {} lakes", colours.len());
#[cfg(feature = "progress")]
let bar = set_up_bar(self.max_water_level);
(0..=self.max_water_level)
.into_iter()
.map(|water_level| {
#[cfg(feature = "debug")]
let mut perf = crate::performance_monitoring::PerfReport::default();
#[cfg(feature = "debug")]
let loop_start = std::time::Instant::now();
'colouring_loop: loop {
#[cfg(feature = "progress")]
{
bar.tick(); }
#[cfg(feature = "debug")]
{
perf.loops += 1;
}
#[cfg(feature = "debug")]
let iter_start = std::time::Instant::now();
let pix_to_colour = find_flooded_px(input.view(), output.view(), water_level);
#[cfg(feature = "debug")]
perf.big_iter_ms.push(iter_start.elapsed().as_millis() as usize);
if pix_to_colour.is_empty() {
break 'colouring_loop;
} else {
#[cfg(feature = "debug")]
let colour_start = std::time::Instant::now();
pix_to_colour.into_iter().for_each(|(idx, col)| {
output[idx] = col;
});
#[cfg(feature = "debug")]
perf.colouring_mus.push(colour_start.elapsed().as_micros() as usize);
}
}
#[cfg(feature = "debug")]
let merge_start = std::time::Instant::now();
let to_merge = find_merge(output.view());
let num_mergers = to_merge.len();
make_colour_map(&mut colours, &to_merge);
assert!(colours[UNCOLOURED] == UNCOLOURED);
if num_mergers > 0 {
recolour(output.view_mut(), &colours);
}
#[cfg(feature = "debug")]
{
perf.merge_ms = merge_start.elapsed().as_millis() as usize;
}
#[cfg(feature = "plots")]
if let Some(ref path) = self.plot_path {
if let Err(err) = plotting::plot_slice(
if self.edge_correction {
output.slice(nd::s![1..(shape[0] - 1), 1..(shape[1] - 1)])
} else {
output.view()
},
&path.join(&format!("ws_lvl{water_level}.png")),
self.plot_colour_map,
) {
println!("Could not make watershed plot. Error: {err}")
}
}
#[cfg(all(feature = "debug", feature = "progress"))]
{
perf.total_ms = loop_start.elapsed().as_millis() as usize;
bar.println(format!("{perf}"));
}
#[cfg(all(feature = "debug", not(feature = "progress")))]
{
perf.total_ms = loop_start.elapsed().as_millis() as usize;
println!("{perf}");
}
#[cfg(feature = "progress")]
{
bar.inc(1);
}
self.wlvl_hook.and_then(|hook| {
Some(hook(HookCtx::ctx(
water_level,
self.max_water_level,
input.view(),
output.view(),
&seed_colours,
)))
})
})
.filter_map(|x| x)
.collect()
}
fn transform(&self, input: nd::ArrayView2<u8>, _seeds: &[(usize, usize)]) -> nd::Array2<usize> {
let shape = [input.shape()[0], input.shape()[1]];
let mut output = nd::Array2::<usize>::zeros(shape);
output.slice_mut(nd::s![1..shape[0] - 1, 1..shape[1] - 1]).mapv_inplace(|_| 123);
return output;
}
fn transform_history(
&self,
input: nd::ArrayView2<u8>,
seeds: &[(usize, usize)],
) -> Vec<(u8, nd::Array2<usize>)> {
let proper_transform =
self.clone_with_hook(|ctx| (ctx.water_level, ctx.colours.to_owned()));
proper_transform.transform_with_hook(input, seeds)
}
fn transform_to_list(
&self,
input: nd::ArrayView2<u8>,
seeds: &[(usize, usize)],
) -> Vec<(u8, Vec<usize>)> {
let proper_transform = self.clone_with_hook(find_lake_sizes);
proper_transform.transform_with_hook(input, seeds)
}
}
pub struct SegmentingWatershed<T = ()> {
#[cfg(feature = "plots")]
plot_path: Option<std::path::PathBuf>,
#[cfg(feature = "plots")]
plot_colour_map:
fn(count: usize, min: usize, max: usize) -> Result<RGBColor, Box<dyn std::error::Error>>,
max_water_level: u8,
edge_correction: bool,
wlvl_hook: Option<fn(HookCtx) -> T>,
}
impl<T> SegmentingWatershed<T> {
fn clone_with_hook<U>(&self, hook: fn(HookCtx) -> U) -> SegmentingWatershed<U> {
SegmentingWatershed {
#[cfg(feature = "plots")]
plot_path: self.plot_path.clone(),
#[cfg(feature = "plots")]
plot_colour_map: self.plot_colour_map,
max_water_level: self.max_water_level,
edge_correction: self.edge_correction,
wlvl_hook: Some(hook),
}
}
}
impl<T> Watershed<T> for SegmentingWatershed<T> {
fn transform_with_hook(&self, input: nd::ArrayView2<u8>, seeds: &[(usize, usize)]) -> Vec<T> {
let shape = if self.edge_correction {
[input.shape()[0] + 2, input.shape()[1] + 2]
} else {
[input.shape()[0], input.shape()[1]]
};
let mut output = nd::Array2::<usize>::zeros(shape);
let mut padded_input =
if self.edge_correction { Some(nd::Array2::<u8>::zeros(shape)) } else { None };
let input = if self.edge_correction {
nd::Zip::from(
padded_input
.as_mut()
.expect("corrected_input was None, which should be impossible. Please report this bug.")
.slice_mut(nd::s![1..(shape[0] - 1), 1..(shape[1] - 1)]),
)
.and(input)
.into_par_iter()
.for_each(|(a, &b)| *a = b);
padded_input.as_ref().unwrap().view()
} else {
input.reborrow()
};
let mut colours: Vec<usize> = (1..=seeds.len()).into_iter().collect();
let seed_colours: Vec<_> =
colours.iter().zip(seeds.iter()).map(|(col, (x, z))| (*col, (*x, *z))).collect();
for (&idx, &col) in seeds.iter().zip(colours.iter()) {
output[idx] = col;
}
colours.insert(UNCOLOURED, UNCOLOURED);
#[cfg(feature = "debug")]
println!("starting with {} lakes", colours.len());
#[cfg(feature = "progress")]
let bar = set_up_bar(self.max_water_level);
(0..=self.max_water_level)
.into_iter()
.map(|water_level| {
#[cfg(feature = "debug")]
let mut perf = crate::performance_monitoring::PerfReport::default();
#[cfg(feature = "debug")]
let loop_start = std::time::Instant::now();
'colouring_loop: loop {
#[cfg(feature = "progress")]
{
bar.tick(); }
#[cfg(feature = "debug")]
{
perf.loops += 1;
}
#[cfg(feature = "debug")]
let iter_start = std::time::Instant::now();
let pix_to_colour = find_flooded_px(input.view(), output.view(), water_level);
#[cfg(feature = "debug")]
perf.big_iter_ms.push(iter_start.elapsed().as_millis() as usize);
if pix_to_colour.is_empty() {
break 'colouring_loop;
} else {
#[cfg(feature = "debug")]
let colour_start = std::time::Instant::now();
pix_to_colour.into_iter().for_each(|(idx, col)| {
output[idx] = col;
});
#[cfg(feature = "debug")]
perf.colouring_mus.push(colour_start.elapsed().as_micros() as usize);
}
}
#[cfg(feature = "debug")]
{
perf.merge_ms = 0;
}
#[cfg(feature = "plots")]
if let Some(ref path) = self.plot_path {
if let Err(err) = plotting::plot_slice(
if self.edge_correction {
output.slice(nd::s![1..(shape[0] - 1), 1..(shape[1] - 1)])
} else {
output.view()
},
&path.join(&format!("ws_lvl{water_level}.png")),
self.plot_colour_map,
) {
println!("Could not make watershed plot. Error: {err}")
}
}
#[cfg(all(feature = "debug", feature = "progress"))]
{
perf.total_ms = loop_start.elapsed().as_millis() as usize;
bar.println(format!("{perf}"));
}
#[cfg(all(feature = "debug", not(feature = "progress")))]
{
perf.total_ms = loop_start.elapsed().as_millis() as usize;
println!("{perf}");
}
#[cfg(feature = "progress")]
{
bar.inc(1);
}
self.wlvl_hook.and_then(|hook| {
Some(hook(HookCtx::ctx(
water_level,
self.max_water_level,
input.view(),
output.view(),
&seed_colours,
)))
})
})
.filter_map(|x| x)
.collect()
}
fn transform(&self, input: nd::ArrayView2<u8>, seeds: &[(usize, usize)]) -> nd::Array2<usize> {
let proper_transform = self.clone_with_hook(|ctx| {
if ctx.water_level == ctx.max_water_level {
Some(ctx.colours.to_owned())
} else {
None
}
});
proper_transform.transform_with_hook(input, seeds)[0].as_ref().expect("no output?").clone()
}
fn transform_history(
&self,
input: nd::ArrayView2<u8>,
seeds: &[(usize, usize)],
) -> Vec<(u8, nd::Array2<usize>)> {
let proper_transform =
self.clone_with_hook(|ctx| (ctx.water_level, ctx.colours.to_owned()));
proper_transform.transform_with_hook(input, seeds)
}
fn transform_to_list(
&self,
input: nd::ArrayView2<u8>,
seeds: &[(usize, usize)],
) -> Vec<(u8, Vec<usize>)> {
let proper_transform = self.clone_with_hook(find_lake_sizes);
proper_transform.transform_with_hook(input, seeds)
}
}