use crate::{
Bitmaps, Error, Gray, Percentile, Shift, Shrink, compute_bitmaps, disagreement, shrink2,
};
const CANDIDATES: [(i32, i32); 9] = [
(0, 0),
(-1, -1),
(-1, 0),
(-1, 1),
(0, -1),
(0, 1),
(1, -1),
(1, 0),
(1, 1),
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Options {
pub bits: u32,
pub tolerance: u8,
pub percentile: Percentile,
pub shrink: Shrink,
}
impl Default for Options {
fn default() -> Self {
Self {
bits: 6,
tolerance: 4,
percentile: Percentile::MEDIAN,
shrink: Shrink::Average,
}
}
}
impl Options {
pub fn opencv() -> Self {
Self {
shrink: Shrink::Subsample,
..Self::default()
}
}
}
pub fn shift(reference: &Gray, target: &Gray, options: &Options) -> Result<Shift, Error> {
let size = |gray: &Gray| (gray.width(), gray.height());
if size(reference) != size(target) {
return Err(Error::SizeMismatch {
reference: size(reference),
target: size(target),
});
}
let levels = shrinks(reference.width(), reference.height(), options.bits);
let reference_pyramid = pyramid(reference, levels, options.shrink);
let target_pyramid = pyramid(target, levels, options.shrink);
Ok((0..=levels).rev().fold(Shift::ZERO, |coarser, level| {
let bitmaps = |pyramid: &[Gray]| {
compute_bitmaps(&pyramid[level], options.percentile, options.tolerance)
};
refine(
&bitmaps(&reference_pyramid),
&bitmaps(&target_pyramid),
coarser.doubled(),
)
}))
}
pub fn align_stack(
images: &[Gray],
reference: usize,
options: &Options,
) -> Result<Vec<Shift>, Error> {
if reference >= images.len() {
return Err(Error::NoSuchReference {
reference,
exposures: images.len(),
});
}
let mut shifts = vec![Shift::ZERO; images.len()];
for index in (0..reference).rev() {
let step = shift(&images[index + 1], &images[index], options)?;
shifts[index] = shifts[index + 1].offset(step.x, step.y);
}
for index in reference + 1..images.len() {
let step = shift(&images[index - 1], &images[index], options)?;
shifts[index] = shifts[index - 1].offset(step.x, step.y);
}
Ok(shifts)
}
fn shrinks(width: usize, height: usize, bits: u32) -> usize {
if width == 0 || height == 0 {
return 0;
}
let longest = width.max(height).ilog2().saturating_sub(1);
let shortest = width.min(height).ilog2();
bits.saturating_sub(1).min(longest).min(shortest) as usize
}
fn pyramid(gray: &Gray, levels: usize, shrink: Shrink) -> Vec<Gray> {
let mut pyramid = Vec::with_capacity(levels + 1);
pyramid.push(gray.clone());
for _ in 0..levels {
let coarser = shrink2(pyramid.last().expect("just pushed"), shrink);
pyramid.push(coarser);
}
pyramid
}
fn refine(reference: &Bitmaps, target: &Bitmaps, around: Shift) -> Shift {
let mut best = around;
let mut least = u64::MAX;
for (x, y) in CANDIDATES {
let candidate = around.offset(x, y);
let error = disagreement(reference, target, candidate);
if error < least {
least = error;
best = candidate;
}
}
best
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_defaults_are_wards_recommendations() {
let options = Options::default();
assert_eq!(options.bits, 6);
assert_eq!(options.tolerance, 4);
assert_eq!(options.percentile, Percentile::MEDIAN);
assert_eq!(options.shrink, Shrink::Average);
assert_eq!(Options::opencv().shrink, Shrink::Subsample);
}
#[test]
fn the_level_count_follows_the_longest_side_until_the_bits_run_out() {
assert_eq!(shrinks(8256, 6192, 6), 5);
assert_eq!(shrinks(4, 4, 6), 1);
assert_eq!(shrinks(3, 3, 6), 0);
assert_eq!(shrinks(2, 2, 6), 0);
assert_eq!(shrinks(1, 1, 6), 0);
assert_eq!(shrinks(1024, 1024, 3), 2);
}
#[test]
fn an_extreme_aspect_ratio_stops_before_a_level_vanishes() {
assert_eq!(shrinks(1000, 3, 6), 1);
assert_eq!(shrinks(1000, 1, 6), 0);
}
#[test]
fn a_pyramid_keeps_the_source_and_one_plane_per_halving() {
let levels = pyramid(
&Gray::from_vec(vec![0; 64 * 64], 64, 64),
3,
Shrink::Average,
);
assert_eq!(levels.len(), 4);
assert_eq!((levels[0].width(), levels[0].height()), (64, 64));
assert_eq!((levels[3].width(), levels[3].height()), (8, 8));
}
#[test]
fn an_exposure_with_no_usable_signal_reports_no_shift() {
let flat = Gray::from_vec(vec![128; 160 * 120], 160, 120);
assert_eq!(shift(&flat, &flat, &Options::default()), Ok(Shift::ZERO));
}
#[test]
fn a_single_pixel_image_reports_no_shift() {
let dot = Gray::from_vec(vec![200], 1, 1);
assert_eq!(shift(&dot, &dot, &Options::default()), Ok(Shift::ZERO));
}
#[test]
fn aligning_mismatched_sizes_is_an_error() {
let small = Gray::from_vec(vec![0; 16], 4, 4);
let large = Gray::from_vec(vec![0; 25], 5, 5);
assert_eq!(
shift(&small, &large, &Options::default()),
Err(Error::SizeMismatch {
reference: (4, 4),
target: (5, 5)
})
);
}
#[test]
fn a_reference_outside_the_stack_is_an_error() {
let one = [Gray::from_vec(vec![0; 4], 2, 2)];
assert_eq!(
align_stack(&one, 3, &Options::default()),
Err(Error::NoSuchReference {
reference: 3,
exposures: 1
})
);
}
}