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