1mod common;
48pub mod global;
49pub mod local;
50
51pub use global::extend_via_global_homography;
52pub use local::extend_via_local_homography;
53
54use crate::homography::HomographyQuality;
55
56#[non_exhaustive]
63#[derive(Clone, Copy, Debug)]
64pub struct ExtensionCommonParams {
65 pub search_rel: f32,
68 pub ambiguity_factor: f32,
72 pub max_iters: u32,
74 pub max_residual_rel: f32,
78}
79
80impl Default for ExtensionCommonParams {
81 fn default() -> Self {
82 Self {
83 search_rel: 0.40,
84 ambiguity_factor: 2.5,
85 max_iters: 5,
86 max_residual_rel: 0.30,
87 }
88 }
89}
90
91#[non_exhaustive]
93#[derive(Clone, Copy, Debug)]
94pub struct ExtensionParams {
95 pub common: ExtensionCommonParams,
98 pub min_labels_for_h: usize,
102 pub max_median_residual_rel: f32,
105}
106
107impl Default for ExtensionParams {
108 fn default() -> Self {
109 Self {
110 common: ExtensionCommonParams::default(),
111 min_labels_for_h: 12,
112 max_median_residual_rel: 0.10,
113 }
114 }
115}
116
117#[non_exhaustive]
119#[derive(Clone, Copy, Debug)]
120pub struct LocalExtensionParams {
121 pub common: ExtensionCommonParams,
127 pub k_nearest: usize,
130 pub min_k: usize,
134 pub extend_depth: u32,
139}
140
141impl Default for LocalExtensionParams {
142 fn default() -> Self {
143 Self {
144 common: ExtensionCommonParams {
145 max_iters: 8, ..ExtensionCommonParams::default()
147 },
148 k_nearest: 12,
149 min_k: 6,
150 extend_depth: 3,
151 }
152 }
153}
154
155#[non_exhaustive]
161#[derive(Clone, Debug, Default)]
162pub struct ExtensionStats {
163 pub iterations: usize,
164 pub h_quality: Option<HomographyQuality<f32>>,
166 pub h_residual_median_px: Option<f32>,
168 pub h_residual_max_px: Option<f32>,
169 pub h_trusted: bool,
172 pub attached: usize,
173 pub rejected_no_candidate: usize,
174 pub rejected_ambiguous: usize,
175 pub rejected_label: usize,
176 pub rejected_validator: usize,
177 pub rejected_edge: usize,
178 pub attached_indices: Vec<usize>,
180 pub attached_cells: Vec<(i32, i32)>,
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::square::grow::{Admit, GrowResult, GrowValidator, LabelledNeighbour};
188 use nalgebra::Point2;
189 use std::collections::HashMap;
190
191 struct OpenValidator;
193
194 impl GrowValidator for OpenValidator {
195 fn is_eligible(&self, _idx: usize) -> bool {
196 true
197 }
198 fn required_label_at(&self, _i: i32, _j: i32) -> Option<u8> {
199 None
200 }
201 fn label_of(&self, _idx: usize) -> Option<u8> {
202 None
203 }
204 fn accept_candidate(
205 &self,
206 _idx: usize,
207 _at: (i32, i32),
208 _prediction: Point2<f32>,
209 _neighbours: &[LabelledNeighbour],
210 ) -> Admit {
211 Admit::Accept
212 }
213 }
214
215 struct ParityValidator {
218 labels: Vec<u8>,
219 }
220
221 impl GrowValidator for ParityValidator {
222 fn is_eligible(&self, _idx: usize) -> bool {
223 true
224 }
225 fn required_label_at(&self, i: i32, j: i32) -> Option<u8> {
226 Some(((i + j).rem_euclid(2)) as u8)
227 }
228 fn label_of(&self, idx: usize) -> Option<u8> {
229 self.labels.get(idx).copied()
230 }
231 fn accept_candidate(
232 &self,
233 _idx: usize,
234 _at: (i32, i32),
235 _prediction: Point2<f32>,
236 _neighbours: &[LabelledNeighbour],
237 ) -> Admit {
238 Admit::Accept
239 }
240 }
241
242 struct EdgeRejectingValidator {
244 forbid_idx: usize,
245 }
246
247 impl GrowValidator for EdgeRejectingValidator {
248 fn is_eligible(&self, _idx: usize) -> bool {
249 true
250 }
251 fn required_label_at(&self, _i: i32, _j: i32) -> Option<u8> {
252 None
253 }
254 fn label_of(&self, _idx: usize) -> Option<u8> {
255 None
256 }
257 fn accept_candidate(
258 &self,
259 _idx: usize,
260 _at: (i32, i32),
261 _prediction: Point2<f32>,
262 _neighbours: &[LabelledNeighbour],
263 ) -> Admit {
264 Admit::Accept
265 }
266 fn edge_ok(
267 &self,
268 candidate_idx: usize,
269 neighbour_idx: usize,
270 _at_candidate: (i32, i32),
271 _at_neighbour: (i32, i32),
272 ) -> bool {
273 candidate_idx != self.forbid_idx && neighbour_idx != self.forbid_idx
274 }
275 }
276
277 fn synthetic_grid(rows: i32, cols: i32, scale: f32) -> Vec<Point2<f32>> {
278 let mut pts = Vec::with_capacity((rows * cols) as usize);
279 for j in 0..rows {
280 for i in 0..cols {
281 pts.push(Point2::new(
282 i as f32 * scale + 100.0,
283 j as f32 * scale + 50.0,
284 ));
285 }
286 }
287 pts
288 }
289
290 fn label_subgrid(
291 positions: &[Point2<f32>],
292 cols: i32,
293 i_range: std::ops::Range<i32>,
294 j_range: std::ops::Range<i32>,
295 ) -> GrowResult {
296 let mut labelled = HashMap::new();
297 let mut by_corner = HashMap::new();
298 for j in j_range {
299 for i in i_range.clone() {
300 let idx = (j * cols + i) as usize;
301 labelled.insert((i, j), idx);
302 by_corner.insert(idx, (i, j));
303 }
304 }
305 let _ = positions;
306 GrowResult {
307 labelled,
308 by_corner,
309 ..Default::default()
310 }
311 }
312
313 #[test]
314 fn extends_clean_perspective_grid() {
315 let cols = 6_i32;
316 let rows = 4_i32;
317 let scale = 50.0_f32;
318 let positions = synthetic_grid(rows, cols, scale);
319 let mut grow = label_subgrid(&positions, cols, 1..5, 1..3);
320 let starting_count = grow.labelled.len();
321 assert_eq!(starting_count, 8);
322
323 let stats = extend_via_global_homography(
324 &positions,
325 &mut grow,
326 scale,
327 &ExtensionParams {
328 min_labels_for_h: 4,
329 ..Default::default()
330 },
331 &OpenValidator,
332 );
333
334 assert!(stats.h_trusted, "H must be trusted on a clean affine grid");
335 assert!(
336 grow.labelled.len() > starting_count,
337 "extension should add corners on a clean grid"
338 );
339 }
340
341 #[test]
342 fn refuses_to_extend_when_residuals_too_high() {
343 let cols = 4_i32;
344 let rows = 4_i32;
345 let scale = 50.0_f32;
346 let mut positions = synthetic_grid(rows, cols, scale);
347 positions[(cols + 1) as usize].x += scale * 0.5;
348 let mut grow = label_subgrid(&positions, cols, 0..4, 0..4);
349
350 let stats = extend_via_global_homography(
351 &positions,
352 &mut grow,
353 scale,
354 &ExtensionParams {
355 min_labels_for_h: 4,
356 common: ExtensionCommonParams {
357 max_residual_rel: 0.30,
358 ..ExtensionCommonParams::default()
359 },
360 ..Default::default()
361 },
362 &OpenValidator,
363 );
364 assert!(!stats.h_trusted);
365 assert_eq!(stats.attached, 0);
366 }
367
368 #[test]
369 fn no_op_when_too_few_labels() {
370 let cols = 4_i32;
371 let rows = 4_i32;
372 let positions = synthetic_grid(rows, cols, 50.0);
373 let mut grow = label_subgrid(&positions, cols, 0..2, 0..2);
374 let stats = extend_via_global_homography(
375 &positions,
376 &mut grow,
377 50.0,
378 &ExtensionParams::default(),
379 &OpenValidator,
380 );
381 assert_eq!(stats.attached, 0);
382 assert!(stats.h_quality.is_none());
383 }
384
385 #[test]
386 fn rejects_wrong_parity_corner_at_h_prediction() {
387 let cols = 4_i32;
388 let rows = 4_i32;
389 let scale = 50.0_f32;
390 let positions = synthetic_grid(rows, cols, scale);
391 let mut grow = label_subgrid(&positions, cols, 1..3, 1..3);
392 let labels: Vec<u8> = (0..(rows * cols))
393 .map(|k| {
394 let i = k % cols;
395 let j = k / cols;
396 ((i + j).rem_euclid(2)) as u8
397 })
398 .collect();
399 let bad_idx = cols as usize;
400 let mut labels = labels;
401 labels[bad_idx] = 0;
402 let validator = ParityValidator { labels };
403
404 let stats = extend_via_global_homography(
405 &positions,
406 &mut grow,
407 scale,
408 &ExtensionParams {
409 min_labels_for_h: 4,
410 ..Default::default()
411 },
412 &validator,
413 );
414 assert!(stats.h_trusted);
415 assert!(!grow.labelled.contains_key(&(0, 1)) || grow.labelled[&(0, 1)] != bad_idx);
416 assert!(stats.rejected_label >= 1);
417 }
418
419 #[test]
420 fn rejects_bad_edge_via_edge_ok_gate() {
421 let cols = 4_i32;
422 let rows = 4_i32;
423 let scale = 50.0_f32;
424 let positions = synthetic_grid(rows, cols, scale);
425 let mut grow = label_subgrid(&positions, cols, 1..3, 1..3);
426
427 let bad_candidate = cols as usize;
428 let validator = EdgeRejectingValidator {
429 forbid_idx: bad_candidate,
430 };
431
432 let stats = extend_via_global_homography(
433 &positions,
434 &mut grow,
435 scale,
436 &ExtensionParams {
437 min_labels_for_h: 4,
438 ..Default::default()
439 },
440 &validator,
441 );
442 assert!(stats.h_trusted);
443 assert!(stats.rejected_edge >= 1);
444 assert!(!grow.labelled.contains_key(&(0, 1)));
445 }
446
447 #[test]
448 fn single_claim_prevents_double_attach() {
449 let scale = 50.0_f32;
450 let mut positions = Vec::new();
451 for j in 0..3_i32 {
452 for i in 0..3_i32 {
453 positions.push(Point2::new(
454 i as f32 * scale + 100.0,
455 j as f32 * scale + 50.0,
456 ));
457 }
458 }
459 positions.push(Point2::new(250.0, 125.0));
460
461 let mut labelled = HashMap::new();
462 let mut by_corner = HashMap::new();
463 for j in 0..3_i32 {
464 for i in 0..3_i32 {
465 let idx = (j * 3 + i) as usize;
466 labelled.insert((i, j), idx);
467 by_corner.insert(idx, (i, j));
468 }
469 }
470 let mut grow = GrowResult {
471 labelled,
472 by_corner,
473 ..Default::default()
474 };
475
476 let stats = extend_via_global_homography(
477 &positions,
478 &mut grow,
479 scale,
480 &ExtensionParams {
481 min_labels_for_h: 4,
482 common: ExtensionCommonParams {
483 search_rel: 1.5,
484 ambiguity_factor: 1.01,
485 ..ExtensionCommonParams::default()
486 },
487 ..Default::default()
488 },
489 &OpenValidator,
490 );
491 assert!(stats.h_trusted);
492 let attached_for_idx_9: Vec<&(i32, i32)> = grow
493 .labelled
494 .iter()
495 .filter_map(|(k, &v)| if v == 9 { Some(k) } else { None })
496 .collect();
497 assert!(
498 attached_for_idx_9.len() <= 1,
499 "corner index 9 attached to {} cells: {:?}",
500 attached_for_idx_9.len(),
501 attached_for_idx_9
502 );
503 for (&cell, &idx) in &grow.labelled {
504 assert_eq!(grow.by_corner.get(&idx), Some(&cell));
505 }
506 }
507
508 #[test]
511 fn local_h_extends_clean_perspective_grid() {
512 let cols = 6_i32;
513 let rows = 4_i32;
514 let scale = 50.0_f32;
515 let positions = synthetic_grid(rows, cols, scale);
516 let mut grow = label_subgrid(&positions, cols, 1..5, 1..3);
517 let starting_count = grow.labelled.len();
518 assert_eq!(starting_count, 8);
519
520 let stats = extend_via_local_homography(
521 &positions,
522 &mut grow,
523 scale,
524 &LocalExtensionParams {
525 min_k: 4,
526 k_nearest: 8,
527 ..Default::default()
528 },
529 &OpenValidator,
530 );
531
532 assert!(stats.h_trusted);
533 assert!(
534 grow.labelled.len() > starting_count,
535 "local-H extension should add corners on a clean grid"
536 );
537 }
538
539 #[test]
540 fn local_h_reaches_further_than_global() {
541 let cols = 8_i32;
542 let rows = 4_i32;
543 let scale = 50.0_f32;
544 let positions = synthetic_grid(rows, cols, scale);
545 let mut grow_local = label_subgrid(&positions, cols, 2..6, 0..rows);
546 assert_eq!(grow_local.labelled.len(), 16);
547
548 let stats = extend_via_local_homography(
549 &positions,
550 &mut grow_local,
551 scale,
552 &LocalExtensionParams {
553 min_k: 4,
554 ..Default::default()
555 },
556 &OpenValidator,
557 );
558
559 assert!(stats.iterations >= 2, "expected >= 2 iters");
560 assert_eq!(
561 grow_local.labelled.len(),
562 (rows * cols) as usize,
563 "local-H should reach every cell on a clean grid: {} of {}",
564 grow_local.labelled.len(),
565 rows * cols,
566 );
567 }
568
569 #[test]
570 fn local_h_no_op_when_too_few_labels() {
571 let cols = 4_i32;
572 let rows = 4_i32;
573 let positions = synthetic_grid(rows, cols, 50.0);
574 let mut grow = label_subgrid(&positions, cols, 0..2, 0..2);
575 let stats = extend_via_local_homography(
576 &positions,
577 &mut grow,
578 50.0,
579 &LocalExtensionParams {
580 min_k: 8,
581 ..Default::default()
582 },
583 &OpenValidator,
584 );
585 assert_eq!(stats.attached, 0);
586 assert!(!stats.h_trusted);
587 }
588
589 #[test]
590 fn local_h_rejects_wrong_parity() {
591 let cols = 4_i32;
592 let rows = 4_i32;
593 let scale = 50.0_f32;
594 let positions = synthetic_grid(rows, cols, scale);
595 let mut grow = label_subgrid(&positions, cols, 1..3, 1..3);
596 let labels: Vec<u8> = (0..(rows * cols))
597 .map(|k| {
598 let i = k % cols;
599 let j = k / cols;
600 ((i + j).rem_euclid(2)) as u8
601 })
602 .collect();
603 let bad_idx = cols as usize;
604 let mut labels = labels;
605 labels[bad_idx] = 0;
606 let validator = ParityValidator { labels };
607
608 let stats = extend_via_local_homography(
609 &positions,
610 &mut grow,
611 scale,
612 &LocalExtensionParams {
613 min_k: 4,
614 ..Default::default()
615 },
616 &validator,
617 );
618 assert!(!grow.labelled.contains_key(&(0, 1)) || grow.labelled[&(0, 1)] != bad_idx);
619 assert!(stats.rejected_label >= 1);
620 }
621}