From 750f7164c718d3cafabc324e87143d1a5f9504cf Mon Sep 17 00:00:00 2001 From: 2ManyProjects Date: Sun, 11 Jan 2026 00:31:27 -0600 Subject: [PATCH] horizontal and veritcal drift --- src/stitching_scanner.py | 91 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/src/stitching_scanner.py b/src/stitching_scanner.py index 56845c1..e7fff7b 100644 --- a/src/stitching_scanner.py +++ b/src/stitching_scanner.py @@ -590,7 +590,84 @@ class StitchingScanner: result[sh - blend_h:sh] = blended result[sh:] = base[blend_h:] return result - + + def _detect_row_start_alignment(self, frame: np.ndarray, direction: ScanDirection) -> AlignmentOffset: + """ + Detect alignment at the start of a new row by comparing the current frame + with the corner region of the mosaic. + + For LEFT direction (starting at right edge): compare against bottom-right corner + For RIGHT direction (starting at left edge): compare against bottom-left corner + + This handles the combined X+Y offset from gear backlash during row transitions. + """ + offset = AlignmentOffset() + + if self.mosaic is None: + return offset + + mh, mw = self.mosaic.shape[:2] + fh, fw = frame.shape[:2] + + # Use a corner region for alignment - this captures both X and Y offset + overlap_size = min(fw // 2, fh // 2, 200) # Square-ish overlap region + + if overlap_size < 50: + self.log(f"Row start alignment: overlap too small ({overlap_size})") + return offset + + if direction == ScanDirection.LEFT: + # Starting at right edge, going left + # Compare frame's top-right corner with mosaic's bottom-right corner + + # Frame region: top-right corner + frame_region = frame[:overlap_size, fw - overlap_size:] + + # Mosaic region: bottom-right corner + mosaic_region = self.mosaic[mh - overlap_size:mh, mw - overlap_size:mw] + + else: # RIGHT direction + # Starting at left edge, going right + # Compare frame's top-left corner with mosaic's bottom-left corner + + # Frame region: top-left corner + frame_region = frame[:overlap_size, :overlap_size] + + # Mosaic region: bottom-left corner + mosaic_region = self.mosaic[mh - overlap_size:mh, :overlap_size] + + # Ensure regions have the same size + min_h = min(mosaic_region.shape[0], frame_region.shape[0]) + min_w = min(mosaic_region.shape[1], frame_region.shape[1]) + + if min_h < 50 or min_w < 50: + self.log(f"Row start alignment: region too small ({min_w}x{min_h})") + return offset + + mosaic_region = mosaic_region[:min_h, :min_w] + frame_region = frame_region[:min_h, :min_w] + + # Detect displacement with confidence + dx, dy, confidence = self._detect_displacement_with_confidence(mosaic_region, frame_region) + + # Sanity check - reject very large displacements + max_adjust = 100 # Allow larger adjustment at row start + if abs(dx) > max_adjust or abs(dy) > max_adjust: + self.log(f"Row start alignment: displacement too large ({dx:.1f}, {dy:.1f}), ignoring") + return offset + + offset.x_offset = dx + offset.y_offset = dy + offset.confidence = confidence + offset.valid = confidence > 0.05 # Lower threshold for row start + + self.log(f"=== Row Start Alignment ({direction.value}) ===") + self.log(f" Mosaic: {mw}x{mh}, Frame: {fw}x{fh}") + self.log(f" Overlap region: {min_w}x{min_h}") + self.log(f" Detected offset: X={dx:.1f}, Y={dy:.1f}, conf={confidence:.3f}") + + return offset + def _append_strip(self, frame: np.ndarray, direction: ScanDirection): """Append strip to mosaic based on accumulated displacement with continuous alignment.""" BLEND_WIDTH = 10 @@ -774,6 +851,17 @@ class StitchingScanner: # Serpentine: even rows right, odd rows left h_direction = ScanDirection.RIGHT if row % 2 == 0 else ScanDirection.LEFT + # For rows after the first, detect row-start alignment + if row > 0: + frame = self._capture_frame() + row_alignment = self._detect_row_start_alignment(frame, h_direction) + if row_alignment.valid: + # Apply row-start alignment to cumulative + self._cumulative_align_x += row_alignment.x_offset + self._cumulative_align_y += row_alignment.y_offset + self.log(f"Applied row-start alignment: X={row_alignment.x_offset:.1f}, Y={row_alignment.y_offset:.1f}") + self.log(f"New cumulative: X={self._cumulative_align_x:.1f}, Y={self._cumulative_align_y:.1f}") + stop_reason = self._scan_direction(h_direction) if not self.running: @@ -803,7 +891,6 @@ class StitchingScanner: self.motion.stop_all() with self._state_lock: self.state.is_scanning = False - def _scan_direction(self, direction: ScanDirection) -> str: """Scan in a direction until edge or max dimension reached.""" self.log(f"Scanning {direction.value}...")