Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

grayscale morphology operators support #581

Merged
Merged
Changes from 3 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a08890b
first draft ; all function, no doc, no test
Apr 28, 2024
863ff99
followed clippy advice
Apr 28, 2024
3e90887
clippy v2
Apr 28, 2024
4bf9ed9
greatly optimised disk()
Apr 28, 2024
712746f
cargo fmt
Apr 28, 2024
5cae330
clippy advice
Apr 28, 2024
e5a9d73
Mask doc + cartesian product
Apr 28, 2024
a246916
followed clippy
Apr 28, 2024
10eb31d
fix wrong bounds + doc for erode and dilate
Apr 29, 2024
7a296da
completed doc with doctest
Apr 29, 2024
878ba41
ran cargo fmt
Apr 29, 2024
5d290de
fixed edge case for `grayscale_erode`
Apr 29, 2024
d3bb0c3
added all mask tests
Apr 29, 2024
735a681
fix `test_mask_from_image_outside`
Apr 29, 2024
4b378f1
did all tests for erode and dilate except empty mask and arbitrary mask
Apr 29, 2024
0afc16e
added tests for arbitrary masks
Apr 29, 2024
10f6b72
fixed default values for `grayscale_dilate` and `grayscale_erode` + t…
Apr 29, 2024
6046176
fix dumb mistake
Morgane55440 Apr 29, 2024
94f4749
fixed the 2nd comment
Morgane55440 Apr 29, 2024
b0f4c88
removed the _mut variants
Apr 29, 2024
a0e725a
Merge branch 'greyscale_morphology_operators' of github.com:Morgane55…
Apr 29, 2024
6d5f924
changed from_image to be more user-friendly
Apr 29, 2024
83c6c34
added specs to the Mask struct
Apr 30, 2024
b0503f4
cargo fmt
Apr 30, 2024
0c51543
remove unnecessary use and reworded `from_image` doc
Apr 30, 2024
0305484
added doc regarding the default value for `grayscale_erode` and `gray…
Apr 30, 2024
8f67df0
removed parralelism (will be reimplemented correctly in a later optim…
Apr 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 179 additions & 1 deletion src/morphology.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use crate::distance_transform::{
distance_transform_impl, distance_transform_mut, DistanceFrom, Norm,
};
use image::GrayImage;
use image::{GenericImageView, GrayImage, Luma};

/// Sets all pixels within distance `k` of a foreground pixel to white.
///
Expand Down Expand Up @@ -349,6 +349,184 @@ pub fn close_mut(image: &mut GrayImage, norm: Norm, k: u8) {
erode_mut(image, norm, k);
}

/// A struct representing a mask used in morphological operations
///
/// the mask is represented by a list of the positions of its pixels
/// relative to its center, with a maximum distance of 255
/// along each axis.
/// This means that in the most extreme case, the mask could have
/// a size of 513 by 513 pixels.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Mask {
elements: Vec<(i16, i16)>,
Morgane55440 marked this conversation as resolved.
Show resolved Hide resolved
}

impl Mask {
/// todo!()
pub fn square(radius: u8) -> Self {
Self {
elements: (-(radius as i16)..=(radius as i16))
.flat_map(|x| (-(radius as i16)..=(radius as i16)).map(move |y| (x, y)))
.collect(),
}
}

/// todo!()
pub fn diamond(radius: u8) -> Self {
Self {
elements: (-(radius as i16)..=(radius as i16))
.flat_map(|x| {
((x.abs() - radius as i16)..=(radius as i16 - x.abs())).map(move |y| (x, y))
})
.collect(),
}
}

/// todo!()
pub fn disk(radius: u8) -> Self {
let mut image = GrayImage::new(2 * radius as u32 + 1, 2 * radius as u32 + 1);
image.iter_mut().for_each(|p| *p = 0); // might not unnecessary
image.put_pixel(radius as u32, radius as u32, Luma::from([u8::MAX]));
dilate_mut(&mut image, Norm::L2, radius);
theotherphil marked this conversation as resolved.
Show resolved Hide resolved
let im_ref = &image;
Self {
elements: (-(radius as i16)..=(radius as i16))
.flat_map(|x| {
(-(radius as i16)..=(radius as i16))
.filter(move |&y| im_ref.get_pixel(x as u32, y as u32).0[0] != 0)
.map(move |y| (x, y))
})
.collect(),
}
}

/// todo!()
pub fn from_image(image: &GrayImage, center_x: u8, center_y: u8) -> Self {
assert!(
(image.width() - center_x as u32) < (u8::MAX as u32),
"all pixels of the mask must be at most {} pixels from the center",
u8::MAX
);
assert!(
(image.height() - center_y as u32) < (u8::MAX as u32),
"all pixels of the mask must be at most {} pixels from the center",
u8::MAX
);
Self {
elements: (0..image.width() as i16)
.flat_map(|x| {
(0..(image.height() as i16))
.filter(move |&y| image.get_pixel(x as u32, y as u32).0[0] != 0)
Morgane55440 marked this conversation as resolved.
Show resolved Hide resolved
.map(move |y| (x - center_x as i16, y - center_y as i16))
})
.collect(),
}
}

fn apply<'a, 'b: 'a, 'c: 'a>(
&'c self,
image: &'b GrayImage,
x: u32,
y: u32,
) -> impl Iterator<Item = &'a Luma<u8>> {
self.elements
.iter()
.map(move |(i, j)| (x as i64 + *i as i64, y as i64 + *j as i64))
.filter(move |(i, j)| {
0 < *i && *i < image.width() as i64 && 0 < *j && *j < image.height() as i64
})
.map(move |(i, j)| image.get_pixel(i as u32, j as u32))
}
}

/// todo!()
pub fn grayscale_dilate(image: &GrayImage, mask: &Mask) -> GrayImage {
#[cfg(feature = "rayon")]
Morgane55440 marked this conversation as resolved.
Show resolved Hide resolved
let result = GrayImage::from_par_fn(image.width(), image.height(), |x, y| {
Luma([mask
.apply(image, x, y)
.map(|l| l.0[0])
.max()
.unwrap_or(u8::MAX)])
});
#[cfg(not(feature = "rayon"))]
let result = GrayImage::from_fn(image.width(), image.height(), |x, y| {
Luma([mask
.apply(image, x, y)
.map(|l| l.0[0])
.max()
.unwrap_or(u8::MAX)])
});
result
}

/// todo!()
pub fn grayscale_dilate_mut(image: &mut GrayImage, mask: &Mask) {
let dilated = grayscale_dilate(image, mask);
image
.iter_mut()
.zip(dilated.iter())
.for_each(|(dst, src)| *dst = *src);
}

/// todo!()
pub fn grayscale_erode(image: &GrayImage, mask: &Mask) -> GrayImage {
#[cfg(feature = "rayon")]
let result = GrayImage::from_par_fn(image.width(), image.height(), |x, y| {
Luma([mask
.apply(image, x, y)
.map(|l| l.0[0])
.min()
.unwrap_or(u8::MAX)])
});
#[cfg(not(feature = "rayon"))]
let result = GrayImage::from_fn(image.width(), image.height(), |x, y| {
Luma([mask
.apply(image, x, y)
.map(|l| l.0[0])
.min()
.unwrap_or(u8::MAX)])
});
result
}

/// todo!()
pub fn grayscale_erode_mut(image: &mut GrayImage, mask: &Mask) {
Morgane55440 marked this conversation as resolved.
Show resolved Hide resolved
let dilated = grayscale_dilate(image, mask);
image
.iter_mut()
.zip(dilated.iter())
.for_each(|(dst, src)| *dst = *src);
}

/// todo!()
pub fn grayscale_open(image: &GrayImage, mask: &Mask) -> GrayImage {
grayscale_dilate(&grayscale_erode(image, mask), mask)
}

/// todo!()
pub fn grayscale_open_mut(image: &mut GrayImage, mask: &Mask) {
let opened = grayscale_open(image, mask);
image
.iter_mut()
.zip(opened.iter())
.for_each(|(dst, src)| *dst = *src);
}

/// todo!()
pub fn grayscale_close(image: &GrayImage, mask: &Mask) -> GrayImage {
grayscale_erode(&grayscale_dilate(image, mask), mask)
}

/// todo!()
pub fn grayscale_close_mut(image: &mut GrayImage, mask: &Mask) {
let closed = grayscale_erode(&grayscale_dilate(image, mask), mask);
image
.iter_mut()
.zip(closed.iter())
.for_each(|(dst, src)| *dst = *src);
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading