Skip to content

Commit 9f1d317

Browse files
authored
rewrite column_running_sum with better safety (#731)
* refactor column_running_sum * fix unused import * fix doc indent warning
1 parent 9acc93a commit 9f1d317

File tree

5 files changed

+253
-32
lines changed

5 files changed

+253
-32
lines changed

flake.lock

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
description = "A very basic flake";
3+
4+
inputs = {
5+
nixpkgs = {
6+
type = "github";
7+
owner = "NixOS";
8+
repo = "nixpkgs";
9+
# nixpkgs-unstable:
10+
rev = "1b5c1881789eb8c86c655caeff3c918fb76fbfe6";
11+
};
12+
flake-utils = {
13+
url = "github:numtide/flake-utils";
14+
};
15+
rust-overlay = {
16+
url = "github:oxalica/rust-overlay";
17+
inputs.nixpkgs.follows = "nixpkgs";
18+
};
19+
};
20+
outputs = {
21+
nixpkgs,
22+
flake-utils,
23+
rust-overlay,
24+
...
25+
}:
26+
flake-utils.lib.eachDefaultSystem (
27+
system: let
28+
overlays = [(import rust-overlay)];
29+
pkgs = import nixpkgs {
30+
inherit system overlays;
31+
config = {
32+
allowUnfree = false;
33+
};
34+
};
35+
rustNightly = pkgs.rust-bin.nightly."2025-10-21".default.override {
36+
extensions = [
37+
"clippy"
38+
"miri"
39+
"rustfmt"
40+
"rust-src"
41+
];
42+
};
43+
in {
44+
devShells = {
45+
default = pkgs.mkShell {
46+
packages =
47+
[
48+
rustNightly
49+
]
50+
++ (with pkgs; [
51+
git
52+
]);
53+
};
54+
};
55+
}
56+
);
57+
}

src/filter/bilateral.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ where
9090
/// let filtered = bilateral_filter(&image, 2, 3., GaussianEuclideanColorDistance::new(10.0));
9191
/// ```
9292
#[must_use = "the function does not modify the original image"]
93+
#[allow(clippy::doc_overindented_list_items)]
9394
pub fn bilateral_filter<I, P, C>(
9495
image: &I,
9596
radius: u8,

src/geometric_transformations.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -961,8 +961,7 @@ pub enum Interpolation {
961961
#[cfg(test)]
962962
mod tests {
963963
use super::*;
964-
use image::{GrayImage, Luma};
965-
use std::f32::consts::PI;
964+
use image::Luma;
966965

967966
#[test]
968967
fn test_rotate_nearest_zero_radians() {

src/integral_image.rs

Lines changed: 112 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -412,50 +412,51 @@ pub fn row_running_sum(image: &GrayImage, row: u32, buffer: &mut [u32], padding:
412412
/// ```
413413
pub fn column_running_sum(image: &GrayImage, column: u32, buffer: &mut [u32], padding: u32) {
414414
// TODO: faster, more formats
415-
let (width, height) = image.dimensions();
416-
assert!(
417-
// assertion 1
418-
buffer.len() >= height as usize + 2 * padding as usize,
419-
"Buffer length {} is less than {} + 2 * {}",
420-
buffer.len(),
421-
height,
422-
padding
423-
);
415+
416+
assert!(image.height() > 0, "image is empty");
424417
assert!(
425-
// assertion 2
426-
column < width,
418+
// assertion `a`
419+
column < image.width(),
427420
"column out of bounds: {} >= {}",
428421
column,
429-
width
422+
image.width()
430423
);
424+
425+
let height = usize::try_from(image.height()).unwrap();
426+
let padding = usize::try_from(padding).unwrap();
431427
assert!(
432-
// assertion 3
433-
height > 0,
434-
"image is empty"
428+
buffer.len() >= height + 2 * padding,
429+
"Buffer length {} is less than {} + 2 * {}",
430+
buffer.len(),
431+
height,
432+
padding
435433
);
434+
let buffer = &mut buffer[..height + 2 * padding];
435+
let (head, rest) = buffer.split_at_mut(padding);
436+
let (middle, tail) = rest.split_at_mut(height);
437+
debug_assert_eq!(padding, head.len());
438+
debug_assert_eq!(height, middle.len(),);
439+
debug_assert_eq!(padding, tail.len());
436440

437441
let first = image.get_pixel(column, 0)[0] as u32;
438-
let last = image.get_pixel(column, height - 1)[0] as u32;
442+
let last = image.get_pixel(column, image.height() - 1)[0] as u32;
439443

440444
let mut sum = 0;
441-
442-
for b in &mut buffer[..padding as usize] {
445+
for b in head {
443446
sum += first;
444447
*b = sum;
445448
}
446-
// JUSTIFICATION:
447-
// Benefit
448-
// Using checked indexing here makes this function take 1.8x longer, as measured by bench_column_running_sum
449-
// Correctness
450-
// column is in bounds due to assertion 2.
451-
// height + padding - 1 < buffer.len() due to assertions 1 and 3.
452-
unsafe {
453-
for y in 0..height {
454-
sum += image.unsafe_get_pixel(column, y)[0] as u32;
455-
*buffer.get_unchecked_mut(y as usize + padding as usize) = sum;
456-
}
449+
for (y, b) in (0..image.height()).zip(middle) {
450+
// JUSTIFICATION:
451+
// Benefit
452+
// Using checked indexing here makes this function take 1.53x longer, as measured by bench_column_running_sum
453+
// Correctness
454+
// column is in bounds due to assertion `a`.
455+
// y is in bounds by construction.
456+
sum += unsafe { image.unsafe_get_pixel(column, y) }[0] as u32;
457+
*b = sum;
457458
}
458-
for b in &mut buffer[padding as usize + height as usize..] {
459+
for b in tail {
459460
sum += last;
460461
*b = sum;
461462
}
@@ -551,6 +552,28 @@ mod tests {
551552
assert_eq!(sum_image_pixels(&integral, 1, 1, 1, 1), [10, 11, 12]);
552553
}
553554

555+
#[test]
556+
fn test_column_running_sum() {
557+
let get_column_running_sum = |image: &GrayImage, column, padding| -> Vec<u32> {
558+
let mut buffer = vec![0u32; (image.height() + 2 * padding) as usize];
559+
column_running_sum(image, column, &mut buffer, padding);
560+
buffer
561+
};
562+
563+
let padding = 0;
564+
let img = gray_image!(type: u8,
565+
1, 2;
566+
3, 4;
567+
5, 6
568+
);
569+
assert_eq!(get_column_running_sum(&img, 0, padding), [1, 4, 9,]);
570+
assert_eq!(get_column_running_sum(&img, 1, padding), [2, 6, 12,]);
571+
572+
let padding = 1;
573+
assert_eq!(get_column_running_sum(&img, 0, padding), [1, 2, 5, 10, 15]);
574+
assert_eq!(get_column_running_sum(&img, 1, padding), [2, 4, 8, 14, 20]);
575+
}
576+
554577
/// Simple implementation of integral_image to validate faster versions against.
555578
pub fn integral_image_ref<I>(image: &I) -> Image<Luma<u32>>
556579
where
@@ -595,6 +618,65 @@ mod proptests {
595618

596619
assert_eq!(expected, actual);
597620
}
621+
622+
#[test]
623+
fn proptest_column_running_sum(
624+
image in arbitrary_image::<Luma<u8>>(0..10, 1..10),
625+
padding in 0..10u32,
626+
) {
627+
assert!(image.height() > 0);
628+
let mut actual = vec![0u32; (image.height() + 2 * padding) as usize];
629+
let mut expected = actual.clone();
630+
631+
for col in 0..image.width() {
632+
actual.fill(0);
633+
expected.fill(0);
634+
635+
column_running_sum(&image, col, &mut actual, padding);
636+
column_running_sum_reference(&image, col, &mut expected, padding);
637+
assert_eq!(actual, expected);
638+
}
639+
}
640+
}
641+
642+
pub fn column_running_sum_reference(
643+
image: &GrayImage,
644+
column: u32,
645+
buffer: &mut [u32],
646+
padding: u32,
647+
) {
648+
let (width, height) = image.dimensions();
649+
assert!(
650+
buffer.len() >= height as usize + 2 * padding as usize,
651+
"Buffer length {} is less than {} + 2 * {}",
652+
buffer.len(),
653+
height,
654+
padding
655+
);
656+
assert!(
657+
column < width,
658+
"column out of bounds: {} >= {}",
659+
column,
660+
width
661+
);
662+
assert!(height > 0, "image is empty");
663+
664+
let first = image.get_pixel(column, 0)[0] as u32;
665+
let last = image.get_pixel(column, height - 1)[0] as u32;
666+
667+
let mut sum = 0;
668+
for b in &mut buffer[..padding as usize] {
669+
sum += first;
670+
*b = sum;
671+
}
672+
for y in 0..height {
673+
sum += image.get_pixel(column, y)[0] as u32;
674+
buffer[y as usize + padding as usize] = sum;
675+
}
676+
for b in &mut buffer[padding as usize + height as usize..] {
677+
sum += last;
678+
*b = sum;
679+
}
598680
}
599681
}
600682

0 commit comments

Comments
 (0)