1use crate::{
2 Bitmaps, Error, Gray, Percentile, Shift, Shrink, compute_bitmaps, disagreement, shrink2,
3};
4
5const CANDIDATES: [(i32, i32); 9] = [
8 (0, 0),
9 (-1, -1),
10 (-1, 0),
11 (-1, 1),
12 (0, -1),
13 (0, 1),
14 (1, -1),
15 (1, 0),
16 (1, 1),
17];
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct Options {
22 pub bits: u32,
28 pub tolerance: u8,
30 pub percentile: Percentile,
34 pub shrink: Shrink,
35}
36
37impl Default for Options {
38 fn default() -> Self {
39 Self {
40 bits: 6,
41 tolerance: 4,
42 percentile: Percentile::MEDIAN,
43 shrink: Shrink::Average,
44 }
45 }
46}
47
48impl Options {
49 pub fn opencv() -> Self {
51 Self {
52 shrink: Shrink::Subsample,
53 ..Self::default()
54 }
55 }
56}
57
58pub fn shift(reference: &Gray, target: &Gray, options: &Options) -> Result<Shift, Error> {
60 let size = |gray: &Gray| (gray.width(), gray.height());
61 if size(reference) != size(target) {
62 return Err(Error::SizeMismatch {
63 reference: size(reference),
64 target: size(target),
65 });
66 }
67
68 let levels = shrinks(reference.width(), reference.height(), options.bits);
69 let reference_pyramid = pyramid(reference, levels, options.shrink);
70 let target_pyramid = pyramid(target, levels, options.shrink);
71
72 Ok((0..=levels).rev().fold(Shift::ZERO, |coarser, level| {
74 let bitmaps = |pyramid: &[Gray]| {
75 compute_bitmaps(&pyramid[level], options.percentile, options.tolerance)
76 };
77
78 refine(
80 &bitmaps(&reference_pyramid),
81 &bitmaps(&target_pyramid),
82 coarser.doubled(),
83 )
84 }))
85}
86
87pub fn align_stack(
94 images: &[Gray],
95 reference: usize,
96 options: &Options,
97) -> Result<Vec<Shift>, Error> {
98 if reference >= images.len() {
99 return Err(Error::NoSuchReference {
100 reference,
101 exposures: images.len(),
102 });
103 }
104
105 let mut shifts = vec![Shift::ZERO; images.len()];
106
107 for index in (0..reference).rev() {
109 let step = shift(&images[index + 1], &images[index], options)?;
110 shifts[index] = shifts[index + 1].offset(step.x, step.y);
111 }
112
113 for index in reference + 1..images.len() {
114 let step = shift(&images[index - 1], &images[index], options)?;
115 shifts[index] = shifts[index - 1].offset(step.x, step.y);
116 }
117
118 Ok(shifts)
119}
120
121fn shrinks(width: usize, height: usize, bits: u32) -> usize {
124 if width == 0 || height == 0 {
125 return 0;
126 }
127
128 let longest = width.max(height).ilog2().saturating_sub(1);
129 let shortest = width.min(height).ilog2();
130
131 bits.saturating_sub(1).min(longest).min(shortest) as usize
132}
133
134fn pyramid(gray: &Gray, levels: usize, shrink: Shrink) -> Vec<Gray> {
135 let mut pyramid = Vec::with_capacity(levels + 1);
136 pyramid.push(gray.clone());
137
138 for _ in 0..levels {
139 let coarser = shrink2(pyramid.last().expect("just pushed"), shrink);
140 pyramid.push(coarser);
141 }
142
143 pyramid
144}
145
146fn refine(reference: &Bitmaps, target: &Bitmaps, around: Shift) -> Shift {
148 let mut best = around;
149 let mut least = u64::MAX;
150
151 for (x, y) in CANDIDATES {
152 let candidate = around.offset(x, y);
153 let error = disagreement(reference, target, candidate);
154
155 if error < least {
156 least = error;
157 best = candidate;
158 }
159 }
160
161 best
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
170 fn the_defaults_are_wards_recommendations() {
171 let options = Options::default();
172
173 assert_eq!(options.bits, 6);
174 assert_eq!(options.tolerance, 4);
175 assert_eq!(options.percentile, Percentile::MEDIAN);
176 assert_eq!(options.shrink, Shrink::Average);
177 assert_eq!(Options::opencv().shrink, Shrink::Subsample);
178 }
179
180 #[test]
181 fn the_level_count_follows_the_longest_side_until_the_bits_run_out() {
182 assert_eq!(shrinks(8256, 6192, 6), 5);
183 assert_eq!(shrinks(4, 4, 6), 1);
184 assert_eq!(shrinks(3, 3, 6), 0);
185 assert_eq!(shrinks(2, 2, 6), 0);
186 assert_eq!(shrinks(1, 1, 6), 0);
187 assert_eq!(shrinks(1024, 1024, 3), 2);
188 }
189
190 #[test]
191 fn an_extreme_aspect_ratio_stops_before_a_level_vanishes() {
192 assert_eq!(shrinks(1000, 3, 6), 1);
193 assert_eq!(shrinks(1000, 1, 6), 0);
194 }
195
196 #[test]
197 fn a_pyramid_keeps_the_source_and_one_plane_per_halving() {
198 let levels = pyramid(
199 &Gray::from_vec(vec![0; 64 * 64], 64, 64),
200 3,
201 Shrink::Average,
202 );
203
204 assert_eq!(levels.len(), 4);
205 assert_eq!((levels[0].width(), levels[0].height()), (64, 64));
206 assert_eq!((levels[3].width(), levels[3].height()), (8, 8));
207 }
208
209 #[test]
211 fn an_exposure_with_no_usable_signal_reports_no_shift() {
212 let flat = Gray::from_vec(vec![128; 160 * 120], 160, 120);
213
214 assert_eq!(shift(&flat, &flat, &Options::default()), Ok(Shift::ZERO));
215 }
216
217 #[test]
218 fn a_single_pixel_image_reports_no_shift() {
219 let dot = Gray::from_vec(vec![200], 1, 1);
220
221 assert_eq!(shift(&dot, &dot, &Options::default()), Ok(Shift::ZERO));
222 }
223
224 #[test]
225 fn aligning_mismatched_sizes_is_an_error() {
226 let small = Gray::from_vec(vec![0; 16], 4, 4);
227 let large = Gray::from_vec(vec![0; 25], 5, 5);
228
229 assert_eq!(
230 shift(&small, &large, &Options::default()),
231 Err(Error::SizeMismatch {
232 reference: (4, 4),
233 target: (5, 5)
234 })
235 );
236 }
237
238 #[test]
239 fn a_reference_outside_the_stack_is_an_error() {
240 let one = [Gray::from_vec(vec![0; 4], 2, 2)];
241
242 assert_eq!(
243 align_stack(&one, 3, &Options::default()),
244 Err(Error::NoSuchReference {
245 reference: 3,
246 exposures: 1
247 })
248 );
249 }
250}