Skip to content

Commit

Permalink
merge master
Browse files Browse the repository at this point in the history
  • Loading branch information
cospectrum committed Apr 29, 2024
2 parents 84680cc + 45a6215 commit 6e253c8
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 59 deletions.
32 changes: 31 additions & 1 deletion src/edges.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Functions for detecting edges in images.
use crate::contrast;
use crate::definitions::{HasBlack, HasWhite};
use crate::filter::gaussian_blur_f32;
use crate::filter::{gaussian_blur_f32, laplacian_filter};
use crate::gradients::{horizontal_sobel, vertical_sobel};
use image::{GenericImageView, GrayImage, ImageBuffer, Luma};
use std::f32;
Expand Down Expand Up @@ -156,6 +157,35 @@ fn hysteresis(
out
}

/// Detects edges in a grayscale image using a Laplacian edge detector with Gaussian smoothing and Otsu thresholding.
///
/// # Arguments
///
/// * `image` - The grayscale image to be processed.
///
/// # Return value
///
/// A binary grayscale image where pixels belonging to edges are white and pixels not belonging to edges are black.
///
/// # Details
///
/// The Laplacian edge detector is applied to the image smoothed with a Gaussian filter with standard deviation `sigma`. The threshold for binarization is calculated using Otsu's method, which maximizes the variance between pixel intensity classes.
pub fn laplacian_edge_detector(image: &GrayImage) -> ImageBuffer<Luma<u8>, Vec<u8>> {
let signma = 1.4;

let blurred_img = gaussian_blur_f32(image, signma);

let laplacian_img = laplacian_filter(&blurred_img);

let otsu_threshold = contrast::otsu_level(&laplacian_img);

contrast::threshold(
&laplacian_img,
otsu_threshold,
contrast::ThresholdType::Binary,
)
}

#[cfg(not(miri))]
#[cfg(test)]
mod benches {
Expand Down
181 changes: 123 additions & 58 deletions src/filter/median.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,24 +112,27 @@ where
{
let (width, height) = image.dimensions();

// Safety note: we rely on image dimensions being non-zero for uncheched indexing to be in bounds
if width == 0 || height == 0 {
return image.clone();
}

let mut out = Image::<P>::new(width, height);
let rx = x_radius as i32;
let ry = y_radius as i32;
// Safety note: we perform unchecked indexing in several places after checking at type i32 that a coordinate is in bounds
if (width + x_radius) > i32::MAX as u32 || (height + y_radius) > i32::MAX as u32 {
panic!("(width + x_radius) and (height + y_radius) must both be <= i32::MAX");
}

let mut out = Image::<P>::new(width, height);
let mut hist = initialise_histogram_for_top_left_pixel(image, x_radius, y_radius);
slide_down_column(&mut hist, image, &mut out, 0, rx, ry);
slide_down_column(&mut hist, image, &mut out, 0, x_radius, y_radius);

for x in 1..width {
if x % 2 == 0 {
slide_right(&mut hist, image, x, 0, rx, ry);
slide_down_column(&mut hist, image, &mut out, x, rx, ry);
slide_right(&mut hist, image, x, 0, x_radius, y_radius);
slide_down_column(&mut hist, image, &mut out, x, x_radius, y_radius);
} else {
slide_right(&mut hist, image, x, height - 1, rx, ry);
slide_up_column(&mut hist, image, &mut out, x, rx, ry);
slide_right(&mut hist, image, x, height - 1, x_radius, y_radius);
slide_up_column(&mut hist, image, &mut out, x, x_radius, y_radius);
}
}
out
Expand All @@ -144,6 +147,7 @@ where
P: Pixel<Subpixel = u8>,
{
let (width, height) = image.dimensions();

let kernel_size = (2 * x_radius + 1) * (2 * y_radius + 1);
let num_channels = P::CHANNEL_COUNT;

Expand All @@ -152,32 +156,54 @@ where
let ry = y_radius as i32;

for dy in -ry..(ry + 1) {
// Safety note: 0 <= py <= height - 1
let py = min(max(0, dy), height as i32 - 1) as u32;

for dx in -rx..(rx + 1) {
// Safety note: 0 <= px <= width - 1
let px = min(max(0, dx), width as i32 - 1) as u32;

hist.incr(image, px, py);
// Safety: px and py are in bounds as explained in the 'Safety note' comments above
unsafe {
hist.incr(image, px, py);
}
}
}

hist
}

fn slide_right<P>(hist: &mut HistSet, image: &Image<P>, x: u32, y: u32, rx: i32, ry: i32)
fn slide_right<P>(hist: &mut HistSet, image: &Image<P>, x: u32, y: u32, rx: u32, ry: u32)
where
P: Pixel<Subpixel = u8>,
{
let (width, height) = image.dimensions();

// Safety note: unchecked indexing below relies on x and y being in bounds
assert!(x < width);
assert!(y < height);

// Safety note: rx and ry are both >= 0 by construction
let rx = rx as i32;
let ry = ry as i32;

// Safety note: 0 <= prev_x < width - rx - 1 < width
let prev_x = max(0, x as i32 - rx - 1) as u32;
// Safety note: 0 <= x + rx <= next_x <= width - 1
let next_x = min(x as i32 + rx, width as i32 - 1) as u32;

for dy in -ry..(ry + 1) {
// Safety note: 0 <= py <= height - 1
let py = min(max(0, y as i32 + dy), (height - 1) as i32) as u32;

hist.decr(image, prev_x, py);
hist.incr(image, next_x, py);
// Safety: prev_x and py are in bounds based on the 'Safety note' comments above
unsafe {
hist.decr(image, prev_x, py);
}
// Safety: next_x and py are in bounds based on the 'Safety note' comments above
unsafe {
hist.incr(image, next_x, py);
}
}
}

Expand All @@ -186,26 +212,50 @@ fn slide_down_column<P>(
image: &Image<P>,
out: &mut Image<P>,
x: u32,
rx: i32,
ry: i32,
rx: u32,
ry: u32,
) where
P: Pixel<Subpixel = u8>,
{
let (width, height) = image.dimensions();
hist.set_to_median(out, x, 0);

// Safety note: unchecked indexing below relies on x being in bounds
assert!(x < width);

// Safety note: rx and ry are both >= 0 by construction
let rx = rx as i32;
let ry = ry as i32;

// Safety: hist.data.len() == P::CHANNEL_COUNT by construction
unsafe {
hist.set_to_median(out, x, 0);
}

for y in 1..height {
// Safety note: 0 <= prev_y < height - ry - 1 < height
let prev_y = max(0, y as i32 - ry - 1) as u32;
// Safety note: 0 < 1 + ry <= next_y < height
let next_y = min(y as i32 + ry, height as i32 - 1) as u32;

for dx in -rx..(rx + 1) {
// Safety note: 0 <= px < width
let px = min(max(0, x as i32 + dx), (width - 1) as i32) as u32;

hist.decr(image, px, prev_y);
hist.incr(image, px, next_y);
// Safety: px and prev_y are in bounds based on the 'Safety note' comments above
unsafe {
hist.decr(image, px, prev_y);
}

// Safety: px and next_y are in bounds based on the 'Safety note' comments above
unsafe {
hist.incr(image, px, next_y);
}
}

hist.set_to_median(out, x, y);
// Safety: hist.data.len() == P::CHANNEL_COUNT by construction
unsafe {
hist.set_to_median(out, x, y);
}
}
}

Expand All @@ -214,26 +264,49 @@ fn slide_up_column<P>(
image: &Image<P>,
out: &mut Image<P>,
x: u32,
rx: i32,
ry: i32,
rx: u32,
ry: u32,
) where
P: Pixel<Subpixel = u8>,
{
let (width, height) = image.dimensions();
hist.set_to_median(out, x, height - 1);

// Safety note: unchecked indexing below relies on x being in bounds
assert!(x < width);

// Safety note: rx and ry are both >= 0 by construction
let rx = rx as i32;
let ry = ry as i32;

// Safety: hist.data.len() == P::CHANNEL_COUNT by construction
unsafe {
hist.set_to_median(out, x, height - 1);
}

for y in (0..(height - 1)).rev() {
// Safety note: 0 < ry + 1 <= prev_y <= height - 1
let prev_y = min(y as i32 + ry + 1, height as i32 - 1) as u32;
// Safety note: 0 <= next_y < height - 1 - ry < height
let next_y = max(0, y as i32 - ry) as u32;

for dx in -rx..(rx + 1) {
// Safety note: 0 <= px <= width - 1
let px = min(max(0, x as i32 + dx), (width - 1) as i32) as u32;

hist.decr(image, px, prev_y);
hist.incr(image, px, next_y);
// Safety: px and prev_y are in bounds based on the 'Safety note' comments above
unsafe {
hist.decr(image, px, prev_y);
}
// Safety: px and next_y are in bounds based on the 'Safety note' comments above
unsafe {
hist.incr(image, px, next_y);
}
}

hist.set_to_median(out, x, y);
// Safety: hist.data.len() == P::CHANNEL_COUNT by construction
unsafe {
hist.set_to_median(out, x, y);
}
}
}

Expand Down Expand Up @@ -263,64 +336,56 @@ impl HistSet {
}
}

fn incr<P>(&mut self, image: &Image<P>, x: u32, y: u32)
/// Safety: requires x and y to be within image bounds and P::CHANNEL_COUNT <= self.data.len()
unsafe fn incr<P>(&mut self, image: &Image<P>, x: u32, y: u32)
where
P: Pixel<Subpixel = u8>,
{
unsafe {
let pixel = image.unsafe_get_pixel(x, y);
let channels = pixel.channels();
for c in 0..channels.len() {
let p = *channels.get_unchecked(c) as usize;
let hist = self.data.get_unchecked_mut(c);
*hist.get_unchecked_mut(p) += 1;
}
let pixel = image.unsafe_get_pixel(x, y);
let channels = pixel.channels();
for c in 0..channels.len() {
let p = *channels.get_unchecked(c) as usize;
let hist = self.data.get_unchecked_mut(c);
*hist.get_unchecked_mut(p) += 1;
}
}

fn decr<P>(&mut self, image: &Image<P>, x: u32, y: u32)
/// Safety: requires x and y to be within image bounds and P::CHANNEL_COUNT <= self.data.len()
unsafe fn decr<P>(&mut self, image: &Image<P>, x: u32, y: u32)
where
P: Pixel<Subpixel = u8>,
{
unsafe {
let pixel = image.unsafe_get_pixel(x, y);
let channels = pixel.channels();
for c in 0..channels.len() {
let p = *channels.get_unchecked(c) as usize;
let hist = self.data.get_unchecked_mut(c);
*hist.get_unchecked_mut(p) -= 1;
}
let pixel = image.unsafe_get_pixel(x, y);
let channels = pixel.channels();
for c in 0..channels.len() {
let p = *channels.get_unchecked(c) as usize;
let hist = self.data.get_unchecked_mut(c);
*hist.get_unchecked_mut(p) -= 1;
}
}

fn set_to_median<P>(&self, image: &mut Image<P>, x: u32, y: u32)
/// Safety: requires P::CHANNEL_COUNT <= self.data.len()
unsafe fn set_to_median<P>(&self, image: &mut Image<P>, x: u32, y: u32)
where
P: Pixel<Subpixel = u8>,
{
unsafe {
let target = image.get_pixel_mut(x, y);
let channels = target.channels_mut();
for c in 0..channels.len() {
*channels.get_unchecked_mut(c) = self.channel_median(c as u8);
}
let target = image.get_pixel_mut(x, y);
let channels = target.channels_mut();
for c in 0..channels.len() {
*channels.get_unchecked_mut(c) = self.channel_median(c as u8);
}
}

fn channel_median(&self, c: u8) -> u8 {
let hist = unsafe { self.data.get_unchecked(c as usize) };

/// Safety: requires c < self.data.len()
unsafe fn channel_median(&self, c: u8) -> u8 {
let hist = self.data.get_unchecked(c as usize);
let mut count = 0;

for i in 0..256 {
unsafe {
count += *hist.get_unchecked(i);
}

count += *hist.get_unchecked(i);
if 2 * count >= self.expected_count {
return i as u8;
}
}

255
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,12 @@ where
}
}

/// Apply a Laplacian filter to an image.
#[must_use = "the function does not modify the original image"]
pub fn laplacian_filter(image: &GrayImage) -> ImageBuffer<Luma<u8>, Vec<u8>> {
filter3x3(image, &[1, 1, 1, 1, -8, 1, 1, 1, 1])
}

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

0 comments on commit 6e253c8

Please sign in to comment.