use crate::core::{Detection, Detections};
#[derive(Debug, Clone, Copy)]
pub struct InferenceSlicer {
slice_width: u32,
slice_height: u32,
overlap_ratio_w: f32,
overlap_ratio_h: f32,
iou_threshold: f32,
}
impl InferenceSlicer {
pub fn new(slice_width: u32, slice_height: u32) -> Self {
Self {
slice_width: slice_width.max(1),
slice_height: slice_height.max(1),
overlap_ratio_w: 0.2,
overlap_ratio_h: 0.2,
iou_threshold: 0.5,
}
}
pub fn with_overlap_ratio(mut self, ratio_w: f32, ratio_h: f32) -> Self {
self.overlap_ratio_w = ratio_w.clamp(0.0, 0.9);
self.overlap_ratio_h = ratio_h.clamp(0.0, 0.9);
self
}
pub fn with_iou_threshold(mut self, iou_threshold: f32) -> Self {
self.iou_threshold = iou_threshold;
self
}
pub fn slices(&self, image_width: u32, image_height: u32) -> Vec<[f32; 4]> {
let xs = axis_starts(image_width, self.slice_width, self.overlap_ratio_w);
let ys = axis_starts(image_height, self.slice_height, self.overlap_ratio_h);
let tile_w = self.slice_width.min(image_width.max(1));
let tile_h = self.slice_height.min(image_height.max(1));
let mut tiles = Vec::with_capacity(xs.len() * ys.len());
for &y in &ys {
for &x in &xs {
tiles.push([x as f32, y as f32, (x + tile_w) as f32, (y + tile_h) as f32]);
}
}
tiles
}
pub fn run<F>(&self, image_width: u32, image_height: u32, mut callback: F) -> Detections
where
F: FnMut([f32; 4]) -> Detections,
{
let mut merged = Vec::new();
for tile in self.slices(image_width, image_height) {
let [offset_x, offset_y, _, _] = tile;
let tile_detections = callback(tile);
merged.extend(
tile_detections
.iter()
.map(|detection| offset_detection(detection, offset_x, offset_y)),
);
}
Detections::new(merged).non_max_suppression(self.iou_threshold, false)
}
#[cfg(feature = "parallel")]
pub fn run_parallel<F>(&self, image_width: u32, image_height: u32, callback: F) -> Detections
where
F: Fn([f32; 4]) -> Detections + Sync,
{
use rayon::prelude::*;
let merged: Vec<Detection> = self
.slices(image_width, image_height)
.par_iter()
.flat_map(|&tile| {
let [offset_x, offset_y, _, _] = tile;
callback(tile)
.iter()
.map(|detection| offset_detection(detection, offset_x, offset_y))
.collect::<Vec<_>>()
})
.collect();
Detections::new(merged).non_max_suppression(self.iou_threshold, false)
}
}
fn offset_detection(detection: &Detection, offset_x: f32, offset_y: f32) -> Detection {
let mut offset = detection.clone();
let [x1, y1, x2, y2] = detection.bbox;
offset.bbox = [x1 + offset_x, y1 + offset_y, x2 + offset_x, y2 + offset_y];
offset.mask = detection.mask.as_ref().map(|polygon| {
polygon
.iter()
.map(|&[x, y]| [x + offset_x, y + offset_y])
.collect()
});
offset.obb = detection
.obb
.map(|corners| corners.map(|[x, y]| [x + offset_x, y + offset_y]));
offset
}
fn axis_starts(image_len: u32, slice_len: u32, overlap_ratio: f32) -> Vec<u32> {
if image_len <= slice_len {
return vec![0];
}
let step = ((slice_len as f32) * (1.0 - overlap_ratio))
.round()
.max(1.0) as u32;
let last_start = image_len - slice_len;
let mut starts = Vec::new();
let mut x = 0u32;
while x < last_start {
starts.push(x);
x += step;
}
starts.push(last_start);
starts
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Detection;
#[test]
fn slices_cover_an_image_smaller_than_one_tile_with_a_single_tile() {
let slicer = InferenceSlicer::new(100, 100);
let tiles = slicer.slices(50, 40);
assert_eq!(tiles, vec![[0.0, 0.0, 50.0, 40.0]]);
}
#[test]
fn slices_tile_an_exact_multiple_with_no_overlap_needed() {
let slicer = InferenceSlicer::new(100, 100).with_overlap_ratio(0.0, 0.0);
let tiles = slicer.slices(200, 100);
assert_eq!(
tiles,
vec![[0.0, 0.0, 100.0, 100.0], [100.0, 0.0, 200.0, 100.0]]
);
}
#[test]
fn slices_last_tile_is_flush_with_the_far_edge_not_overshooting_it() {
let slicer = InferenceSlicer::new(100, 100).with_overlap_ratio(0.2, 0.2);
let tiles = slicer.slices(250, 100);
for [x1, _, x2, _] in &tiles {
assert!(*x2 <= 250.0, "tile [{x1}, {x2}] overshoots the image width");
}
assert!(tiles.contains(&[150.0, 0.0, 250.0, 100.0]));
}
#[test]
fn slices_cover_the_full_image_with_no_gaps() {
let slicer = InferenceSlicer::new(64, 64).with_overlap_ratio(0.1, 0.1);
let tiles = slicer.slices(300, 217);
let max_x = tiles.iter().map(|t| t[2] as u32).max().unwrap();
let max_y = tiles.iter().map(|t| t[3] as u32).max().unwrap();
assert_eq!(max_x, 300);
assert_eq!(max_y, 217);
assert!(tiles.iter().any(|t| t[0] == 0.0 && t[1] == 0.0));
}
#[test]
fn run_offsets_tile_local_detections_into_full_image_coordinates() {
let slicer = InferenceSlicer::new(10, 10).with_overlap_ratio(0.0, 0.0);
let detections = slicer.run(20, 10, |_tile| {
Detections::new(vec![Detection::new([0.0, 0.0, 2.0, 2.0], 0.9, 0)])
});
assert_eq!(detections.len(), 2);
let mut x1s: Vec<f32> = detections.iter().map(|d| d.bbox[0]).collect();
x1s.sort_by(f32::total_cmp);
assert_eq!(x1s, vec![0.0, 10.0]);
}
#[test]
fn run_merges_duplicate_detections_from_overlapping_tiles() {
let slicer = InferenceSlicer::new(10, 10)
.with_overlap_ratio(0.5, 0.0)
.with_iou_threshold(0.3);
let detections = slicer.run(15, 10, |tile| {
let local_x1 = (5.0 - tile[0]).clamp(0.0, 10.0);
Detections::new(vec![Detection::new(
[local_x1, 0.0, local_x1 + 4.0, 10.0],
0.9,
0,
)])
});
assert_eq!(detections.len(), 1);
}
}