From 4eaf692edbda1b766dad1212f596730e335744b5 Mon Sep 17 00:00:00 2001 From: 2ManyProjects Date: Mon, 12 Jan 2026 18:11:47 -0600 Subject: [PATCH] reset region comp --- src/stitching_scanner.py | 349 ++++++++++++++------------------------- 1 file changed, 122 insertions(+), 227 deletions(-) diff --git a/src/stitching_scanner.py b/src/stitching_scanner.py index 71d86ed..7bcf445 100644 --- a/src/stitching_scanner.py +++ b/src/stitching_scanner.py @@ -6,36 +6,30 @@ No complex visual matching - just track displacement and append strips. Continuous alignment correction for gear slippage compensation. FIXES: -- Y comparison: Compare frame's BOTTOM with ROW 1's TOP (at Y=transition_height) -- X comparison (LEFT scan): Compare frame's RIGHT with transition strip's LEFT edge -- row_start_y now positions row 2 so its bottom overlaps with row 1's top -- _detect_strip_alignment now uses SAME bounding boxes as UI draws +- Row-start alignment now compares with TRANSITION STRIPS, not old row 1 +- Correct X position tracking after row transition +- Fixed LEFT scan stop condition DEBUG BORDER COLORS (for debugging row 2 placement): ================================================= In _detect_row_start_alignment(): - - WHITE (thick): Outline of TRANSITION STRIPS (X=x_offset, Y=0 to transition_height) - - BLUE line: Y=transition_height (boundary between transition strips and row 1) - - CYAN: Y comparison region - TOP of ROW 1 (just below blue line) - - YELLOW: X comparison region - at expected_x position + - WHITE (thick): Outline of where TRANSITION STRIPS are in mosaic (X=x_offset, Y=0 to transition_height) + - BLUE line: Y=transition_height (boundary between transition strips and old row 1) + - CYAN: Mosaic region used for Y alignment comparison (bottom of transition strips) - MAGENTA: Where the new frame is EXPECTED to be placed - -In _detect_strip_alignment(): - - Uses SAME regions as _detect_row_start_alignment for consistency - - For LEFT scan: Y = CYAN region (frame top vs row 1 top) - - For LEFT scan: X = YELLOW region (frame right vs mosaic at expected_x) + - YELLOW: Mosaic region used for X alignment comparison (edge of transition strips) In _blend_horizontal_at_y (append_left): - - RED (thick): Where strip WOULD have been placed (ORIGINAL position) - - GREEN: Where strip was ACTUALLY placed (ADJUSTED position) + - RED (thick): Where strip WOULD have been placed (ORIGINAL position before alignment) + - GREEN: Where strip was ACTUALLY placed (ADJUSTED position after alignment) MOSAIC LAYOUT AFTER DOWN TRANSITION: ==================================== - Y=0 to Y=transition_height: TRANSITION STRIPS (at X=x_offset) - Y=transition_height to Y=mh: ROW 1 (shifted down) + Y=0 to Y=transition_height: TRANSITION STRIPS (at X=x_offset to X=x_offset+fw) + Y=transition_height to Y=mh: OLD ROW 1 (shifted down, at X=0 to X=mw) - Row 2 placement: Y = transition_height - fh + overlap - (so row 2's bottom overlaps with row 1's top) + Where x_offset = mosaic_width - initial_frame_width + And transition_height = mosaic_height - initial_frame_height """ import cv2 @@ -218,11 +212,11 @@ class StitchingScanner: Detect alignment at the START of a new row. After a DOWN transition with prepend, the mosaic layout is: - - Y=0 to Y=transition_height: TRANSITION STRIPS (at X=x_offset) - - Y=transition_height to Y=mh: OLD ROW 1 (shifted down) + - Y=0 to Y≈transition_height: TRANSITION STRIPS (placed at x_offset) + - Y≈transition_height to Y=mosaic_height: OLD row 1 content (shifted down) - For Y alignment: Compare frame's BOTTOM with ROW 1's TOP (at Y=transition_height) - For X alignment (LEFT scan): Compare frame's RIGHT with transition strip's LEFT edge + The current frame should overlap with the TRANSITION STRIPS, not old row 1. + The camera is at the X position where transition strips were placed. """ offset = AlignmentOffset() @@ -238,14 +232,16 @@ class StitchingScanner: x_offset = max(0, mw - self.state.mosaic_init_width) # Transition height is how much was added during DOWN transition + # transition_height = mh - original_height_before_transition + # But since row 1 was just fh tall, transition_height = mh - fh transition_height = mh - fh self.log(f" Row-start alignment: mosaic {mw}x{mh}, frame {fw}x{fh}") self.log(f" Transition strips at: X={x_offset}, Y=0 to Y={transition_height}") - self.log(f" Old row 1 at: Y={transition_height} to Y={mh}") + self.log(f" Old row 1 shifted to: Y={transition_height} to Y={mh}") # ========== DEBUG: Draw borders showing layout ========== - # WHITE border: Where TRANSITION STRIPS are + # WHITE border: Where TRANSITION STRIPS are (this is what we should compare with) cv2.rectangle(self.mosaic, (x_offset, 0), (min(x_offset + fw, mw), transition_height), @@ -257,84 +253,81 @@ class StitchingScanner: self.log(f" DEBUG: BLUE line at Y={transition_height} (transition/row1 boundary)") vertical_overlap = min(200, fh // 3) - horizontal_overlap = min(200, fw // 3) min_overlap = 50 # ============================================= # Step 1: Detect Y alignment # ============================================= - # Compare frame's BOTTOM with ROW 1's TOP (at Y=transition_height) - # Frame's bottom should overlap with the area just below the blue line + # Compare frame's TOP with the BOTTOM of transition strips + # Frame's top portion overlaps with mosaic's transition bottom # - # Row 1's top is at Y=transition_height - # So compare mosaic[transition_height : transition_height+overlap] + # Transition strips are at Y=0 to Y=transition_height + # So transition bottom is around Y=(transition_height - overlap) to Y=transition_height - row1_top_start = transition_height - row1_top_end = min(transition_height + vertical_overlap, mh) + transition_compare_y_start = transition_height + transition_compare_y_end = max(0, transition_height + vertical_overlap) - # Frame's BOTTOM portion (will overlap with row 1's top) - frame_bottom = frame[fh - vertical_overlap:fh, :] + # Frame's TOP should overlap with transition BOTTOM + frame_top = frame[:vertical_overlap, :] # Get the X range - centered on where transition strips are - x_start = x_offset - x_end = min(x_offset + fw, mw) + x_end = x_offset + x_start = min(x_offset - fw, mw) compare_width = x_end - x_start - if row1_top_end > row1_top_start and compare_width >= min_overlap: - # Mosaic region: TOP of row 1 (just below the transition) - mosaic_row1_top = self.mosaic[row1_top_start:row1_top_end, x_start:x_end] + if transition_compare_y_start >= 0 and compare_width >= min_overlap: + # Mosaic region: bottom of transition strips + mosaic_transition_bottom = self.mosaic[transition_compare_y_start:transition_compare_y_end, + x_start:x_end] - # Frame region: bottom portion - frame_compare = frame_bottom[:, :compare_width] + # Frame region: top portion (what overlaps with transition) + frame_compare = frame_top[:, :compare_width] # ========== DEBUG: Save frame with comparison region marked ========== debug_frame = frame.copy() - cv2.rectangle(debug_frame, (0, fh - vertical_overlap), (compare_width, fh), - (255, 255, 0), 2) # CYAN - frame's bottom region used for Y comparison - self.log(f" DEBUG: Frame Y comparison region: X=0:{compare_width}, Y={fh - vertical_overlap}:{fh}") + cv2.rectangle(debug_frame, (0, 0), (compare_width, vertical_overlap), + (255, 255, 0), 2) # CYAN - frame's top region used for Y comparison + self.log(f" DEBUG: Frame Y comparison region: X=0:{compare_width}, Y=0:{vertical_overlap}") try: cv2.imwrite('/tmp/debug_frame_row2_start.png', debug_frame) except: pass # ========== DEBUG: Draw CYAN border on mosaic for Y comparison region ========== - # CYAN at TOP of row 1 (just below blue line) cv2.rectangle(self.mosaic, - (x_start, row1_top_start), - (x_end, row1_top_end), + (x_start, transition_compare_y_start), + (x_end, transition_compare_y_end), (255, 255, 0), 2) # CYAN - mosaic comparison region for Y - self.log(f" DEBUG: CYAN border - mosaic Y comparison region X={x_start}:{x_end}, Y={row1_top_start}:{row1_top_end}") + self.log(f" DEBUG: CYAN border - mosaic Y comparison region X={x_start}:{x_end}, Y={transition_compare_y_start}:{transition_compare_y_end}") - # MAGENTA border: Where frame is EXPECTED to be placed - expected_y_start = transition_height - fh + vertical_overlap # Frame overlaps with row 1 - expected_y_start = max(0, expected_y_start) + # MAGENTA border: Where frame is EXPECTED to be placed (at transition position) cv2.rectangle(self.mosaic, - (x_start, expected_y_start), - (x_end, expected_y_start + fh), + (x_start, 0), + (x_end, fh), (255, 0, 255), 2) # MAGENTA - expected frame position - self.log(f" DEBUG: MAGENTA border - expected frame position X={x_start}:{x_end}, Y={expected_y_start}:{expected_y_start + fh}") + self.log(f" DEBUG: MAGENTA border - expected frame position X={x_start}:{x_end}, Y=0:{fh}") - min_w = min(frame_compare.shape[1], mosaic_row1_top.shape[1]) - min_h = min(frame_compare.shape[0], mosaic_row1_top.shape[0]) + min_w = min(frame_compare.shape[1], mosaic_transition_bottom.shape[1]) + min_h = min(frame_compare.shape[0], mosaic_transition_bottom.shape[0]) if min_w >= min_overlap and min_h >= min_overlap: frame_compare = frame_compare[:min_h, :min_w] - mosaic_row1_top = mosaic_row1_top[:min_h, :min_w] + mosaic_transition_bottom = mosaic_transition_bottom[:min_h, :min_w] # ========== DEBUG: Save the comparison regions as images ========== try: - cv2.imwrite('/tmp/debug_frame_bottom_region.png', frame_compare) - cv2.imwrite('/tmp/debug_mosaic_row1top_region.png', mosaic_row1_top) + cv2.imwrite('/tmp/debug_frame_top_region.png', frame_compare) + cv2.imwrite('/tmp/debug_mosaic_transition_region.png', mosaic_transition_bottom) self.log(f" DEBUG: Saved comparison regions to /tmp/debug_*.png") except: pass # Detect displacement dx_v, dy_v, conf_v = self._detect_displacement_with_confidence( - mosaic_row1_top, frame_compare) + mosaic_transition_bottom, frame_compare) self.log(f" Row-start Y alignment: dx={dx_v:.1f}, dy={dy_v:.1f}, conf={conf_v:.3f}") - self.log(f" Compared frame[{fh - vertical_overlap}:{fh}] with mosaic[{row1_top_start}:{row1_top_end}]") + self.log(f" Compared frame[0:{vertical_overlap}] with mosaic[{transition_compare_y_start}:{transition_compare_y_end}]") if conf_v > 0.1: offset.y_offset = dy_v @@ -343,34 +336,29 @@ class StitchingScanner: # ============================================= # Step 2: Detect X alignment # ============================================= + horizontal_overlap = min(200, fw // 3) if direction == ScanDirection.LEFT: - # For LEFT scan: frame will move LEFT - # Frame's RIGHT edge will overlap with existing content's LEFT edge - # The LEFT edge of transition strips is at X=x_offset - # Compare frame's RIGHT with mosaic's LEFT edge of transition strips + # For LEFT scan: frame starts at the transition X position + # Compare frame's RIGHT edge with mosaic's transition strip RIGHT edge - # Mosaic region: LEFT edge of transition strips - mosaic_x_start = x_offset - mosaic_x_end = min(x_offset + horizontal_overlap, mw) + transition_x_start = x_offset + transition_x_end = max(x_offset, x_offset + horizontal_overlap) - # Y range: within the transition strip area AND row 1 top - y_start = max(0, transition_height - fh // 2) - y_end = min(transition_height + fh // 2, mh) + # Y range: within the transition strip area + y_start = 0 + y_end = min(transition_height, fh) - if mosaic_x_end > mosaic_x_start and y_end > y_start: - mosaic_edge = self.mosaic[y_start:y_end, mosaic_x_start:mosaic_x_end] - - # Frame's RIGHT edge - frame_edge = frame[:min(y_end - y_start, fh), fw - (mosaic_x_end - mosaic_x_start):fw] + if transition_x_end - transition_x_start >= min_overlap: + mosaic_edge = self.mosaic[y_start:y_end, transition_x_start:transition_x_end] + frame_edge = frame[:y_end, fw - (transition_x_end - transition_x_start):fw] # ========== DEBUG: Draw YELLOW border for X comparison region ========== - # YELLOW on LEFT side of transition strips cv2.rectangle(self.mosaic, - (mosaic_x_start, y_start), - (mosaic_x_end, y_end), + (transition_x_start, y_start), + (transition_x_end, y_end), (0, 255, 255), 2) # YELLOW - mosaic comparison region for X - self.log(f" DEBUG: YELLOW border - mosaic X comparison region X={mosaic_x_start}:{mosaic_x_end}, Y={y_start}:{y_end}") + self.log(f" DEBUG: YELLOW border - mosaic X comparison region X={transition_x_start}:{transition_x_end}, Y={y_start}:{y_end}") min_h = min(mosaic_edge.shape[0], frame_edge.shape[0]) min_w = min(mosaic_edge.shape[1], frame_edge.shape[1]) @@ -389,23 +377,22 @@ class StitchingScanner: if conf_h > offset.confidence: offset.confidence = conf_h else: - # For RIGHT scan at row start - # Frame's LEFT edge will overlap with existing content's RIGHT edge - mosaic_x_end = min(x_offset + fw, mw) - mosaic_x_start = max(mosaic_x_end - horizontal_overlap, x_offset) + # For RIGHT scan at row start (similar logic but for left edge) + transition_x_start = x_offset + transition_x_end = min(x_offset + horizontal_overlap, mw) - y_start = max(0, transition_height - fh // 2) - y_end = min(transition_height + fh // 2, mh) + y_start = 0 + y_end = min(transition_height, fh) - if mosaic_x_end > mosaic_x_start and y_end > y_start: - mosaic_edge = self.mosaic[y_start:y_end, mosaic_x_start:mosaic_x_end] - frame_edge = frame[:min(y_end - y_start, fh), :mosaic_x_end - mosaic_x_start] + if transition_x_end - transition_x_start >= min_overlap: + mosaic_edge = self.mosaic[y_start:y_end, transition_x_start:transition_x_end] + frame_edge = frame[:y_end, :transition_x_end - transition_x_start] cv2.rectangle(self.mosaic, - (mosaic_x_start, y_start), - (mosaic_x_end, y_end), + (transition_x_start, y_start), + (transition_x_end, y_end), (0, 255, 255), 2) # YELLOW - self.log(f" DEBUG: YELLOW border - mosaic X comparison region X={mosaic_x_start}:{mosaic_x_end}, Y={y_start}:{y_end}") + self.log(f" DEBUG: YELLOW border - mosaic X comparison region X={transition_x_start}:{transition_x_end}, Y={y_start}:{y_end}") min_h = min(mosaic_edge.shape[0], frame_edge.shape[0]) min_w = min(mosaic_edge.shape[1], frame_edge.shape[1]) @@ -447,7 +434,6 @@ class StitchingScanner: with the expected overlap region of the mosaic. This provides continuous correction for gear slippage during scanning. - Uses the SAME bounding boxes as drawn in the UI for consistency. Args: frame: Current camera frame @@ -467,147 +453,67 @@ class StitchingScanner: mh, mw = self.mosaic.shape[:2] fh, fw = frame.shape[:2] - # Calculate key positions - SAME as _detect_row_start_alignment - x_offset = max(0, mw - self.state.mosaic_init_width) - transition_height = mh - fh + # Clamp expected positions + expected_y = max(0, min(expected_y, mh - fh)) + expected_x = max(0, min(expected_x, mw - fw)) - # Overlap sizes - SAME as UI - vertical_overlap = min(200, fh // 3) - horizontal_overlap = min(200, fw // 3) - min_overlap = 50 + # Increased overlap for better detection + max_overlap = 250 # Increased from 200 + min_overlap = 40 # Increased from 30 if direction == ScanDirection.RIGHT: - # Row 1: Appending to the right + # We're appending to the right # Compare left portion of frame with right edge of mosaic - overlap_width = min(fw // 2, mw, horizontal_overlap) + overlap_width = min(fw // 2, mw - expected_x, max_overlap) if overlap_width < min_overlap: return offset - # Y region: full frame height at expected_y - y_start = max(0, expected_y) - y_end = min(expected_y + fh, mh) - # Extract regions - mosaic_region = self.mosaic[y_start:y_end, mw - overlap_width:mw] - frame_region = frame[:y_end - y_start, :overlap_width] + mosaic_region = self.mosaic[expected_y:expected_y + fh, mw - overlap_width:mw] + frame_region = frame[:, :overlap_width] elif direction == ScanDirection.LEFT: - # Row 2+: Scanning left within existing mosaic - # Use SAME regions as UI (CYAN for Y, YELLOW for X) + # We're placing within existing mosaic, moving left + # Compare right portion of frame with mosaic at expected position + overlap_width = min(fw // 2, mw - expected_x, max_overlap) - # ============================================= - # Y comparison: CYAN region - # Compare frame's TOP with ROW 1's TOP (at Y=transition_height) - # ============================================= - y_compare_start = transition_height - y_compare_end = min(transition_height + vertical_overlap, mh) + if overlap_width < min_overlap: + return offset - # X range for Y comparison - at the current expected position - x_start = max(0, int(expected_x)) - x_end = min(int(expected_x) + fw, mw) - compare_width = x_end - x_start + # The frame's right edge should align with mosaic at expected_x + fw + mosaic_x_end = min(expected_x + fw, mw) + mosaic_x_start = max(mosaic_x_end - overlap_width, 0) + actual_overlap = mosaic_x_end - mosaic_x_start - if y_compare_end > y_compare_start and compare_width >= min_overlap: - # Frame's TOP portion - frame_top = frame[:vertical_overlap, :compare_width] - - # Mosaic region: TOP of row 1 (CYAN region) - mosaic_y_region = self.mosaic[y_compare_start:y_compare_end, x_start:x_end] - - # Match sizes - min_w = min(frame_top.shape[1], mosaic_y_region.shape[1]) - min_h = min(frame_top.shape[0], mosaic_y_region.shape[0]) - - if min_w >= min_overlap and min_h >= min_overlap: - frame_compare = frame_top[:min_h, :min_w] - mosaic_compare = mosaic_y_region[:min_h, :min_w] - - dx_v, dy_v, conf_v = self._detect_displacement_with_confidence( - mosaic_compare, frame_compare) - - if conf_v > 0.1: - offset.y_offset = dy_v - offset.confidence = conf_v + if actual_overlap < min_overlap: + return offset - # ============================================= - # X comparison: YELLOW region - # Compare frame's RIGHT with mosaic content at expected_x + fw - # ============================================= - # For LEFT scan: frame's RIGHT overlaps with mosaic content to the right - mosaic_x_start = min(int(expected_x) + fw - horizontal_overlap, mw - horizontal_overlap) - mosaic_x_start = max(0, mosaic_x_start) - mosaic_x_end = min(mosaic_x_start + horizontal_overlap, mw) - - # Y range spans transition area AND row 1 - y_start = max(0, transition_height - fh // 2) - y_end = min(transition_height + fh // 2, mh) - - if mosaic_x_end > mosaic_x_start and y_end > y_start: - mosaic_x_region = self.mosaic[y_start:y_end, mosaic_x_start:mosaic_x_end] - - # Frame's RIGHT edge - frame_right_width = mosaic_x_end - mosaic_x_start - frame_x_region = frame[:min(y_end - y_start, fh), fw - frame_right_width:fw] - - min_h = min(mosaic_x_region.shape[0], frame_x_region.shape[0]) - min_w = min(mosaic_x_region.shape[1], frame_x_region.shape[1]) - - if min_h >= min_overlap and min_w >= min_overlap: - mosaic_x_region = mosaic_x_region[:min_h, :min_w] - frame_x_region = frame_x_region[:min_h, :min_w] - - dx_h, dy_h, conf_h = self._detect_displacement_with_confidence( - mosaic_x_region, frame_x_region) - - if conf_h > 0.1: - offset.x_offset = -dx_h - if conf_h > offset.confidence: - offset.confidence = conf_h - - # Sanity check - reject large displacements - max_adjust = 80 - if abs(offset.x_offset) > max_adjust: - offset.x_offset = max(-max_adjust, min(max_adjust, offset.x_offset)) - if abs(offset.y_offset) > max_adjust: - offset.y_offset = max(-max_adjust, min(max_adjust, offset.y_offset)) - - offset.valid = offset.confidence > 0.1 - - if offset.valid: - self.log(f" Strip alignment (LEFT): X={offset.x_offset:.1f}, Y={offset.y_offset:.1f}, conf={offset.confidence:.3f}") - - return offset + mosaic_region = self.mosaic[expected_y:expected_y + fh, mosaic_x_start:mosaic_x_end] + frame_region = frame[:, fw - actual_overlap:] elif direction == ScanDirection.DOWN: - # Vertical transition: appending above (prepend) - # Compare top portion of frame with bottom edge of existing content - overlap_height = min(fh // 2, mh, vertical_overlap) + # We're appending below + # Compare top portion of frame with bottom edge of mosaic + overlap_height = min(fh // 2, mh - expected_y, max_overlap) if overlap_height < min_overlap: return offset - # X position at x_offset (where transition strips go) - x_start = x_offset - x_end = min(x_offset + fw, mw) - - mosaic_region = self.mosaic[mh - overlap_height:mh, x_start:x_end] - frame_region = frame[:overlap_height, :x_end - x_start] + mosaic_region = self.mosaic[mh - overlap_height:mh, expected_x:expected_x + fw] + frame_region = frame[:overlap_height, :] else: # UP # Compare bottom portion of frame with top edge of mosaic - overlap_height = min(fh // 2, mh, vertical_overlap) + overlap_height = min(fh // 2, expected_y, max_overlap) if overlap_height < min_overlap: return offset - x_start = x_offset - x_end = min(x_offset + fw, mw) - - mosaic_region = self.mosaic[:overlap_height, x_start:x_end] - frame_region = frame[fh - overlap_height:, :x_end - x_start] + mosaic_region = self.mosaic[:overlap_height, expected_x:expected_x + fw] + frame_region = frame[fh - overlap_height:, :] - # Ensure regions have the same size (for RIGHT, DOWN, UP) + # 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]) @@ -622,16 +528,15 @@ class StitchingScanner: dx, dy, confidence = self._detect_displacement_with_confidence(mosaic_region, frame_region) # Sanity check - reject large displacements - max_adjust = 80 + max_adjust = 500 # Max pixels to adjust if abs(dx) > max_adjust or abs(dy) > max_adjust: - self.log(f"Strip alignment: displacement too large ({dx:.1f}, {dy:.1f}), limiting") - dx = max(-max_adjust, min(max_adjust, dx)) - dy = max(-max_adjust, min(max_adjust, dy)) + self.log(f"Strip 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.1 + offset.valid = confidence > 0.1 # Require minimum confidence if offset.valid: self.log(f" Strip alignment: X={dx:.1f}, Y={dy:.1f}, conf={confidence:.3f}") @@ -1396,32 +1301,22 @@ class StitchingScanner: self.log(f"Row transition complete: {abs(total_y):.1f}px") self.log(f"Alignment after row transition: X={self._cumulative_align_x:.1f}, Y={self._cumulative_align_y:.1f}") - # Calculate the Y position in the mosaic where the new row should be placed - # After prepending, the layout is: - # Y=0 to Y=transition_height: transition strips - # Y=transition_height to Y=mh: old row 1 (shifted down) - # - # Row 2 should be placed so its BOTTOM overlaps with row 1's TOP - # transition_height = mh - fh (height added during transition) - # overlap_pixels = h * row_overlap - # - # Row 2 Y position: transition_height - fh + overlap_pixels - # This puts row 2's bottom at: (transition_height - fh + overlap) + fh = transition_height + overlap - # Which overlaps with row 1's top at Y=transition_height - - overlap_pixels = int(h * self.config.row_overlap) - transition_height = self.state.mosaic_height - h - self._row_start_y = max(0, transition_height - h + overlap_pixels) + # Calculate the Y position in the mosaic where the new row starts + # Since we use append_below=False during DOWN movement, new content + # is PREPENDED to the TOP of the mosaic. So the new row starts at Y=0 + self._row_start_y = 0 # New row is at the TOP after prepending # Calculate the X position where transition strips were placed + # This is where the camera is now (ready to start row 2) x_offset = max(0, self.state.mosaic_width - self.state.mosaic_init_width) - self.log(f"New row Y position: {self._row_start_y} (transition_height={transition_height}, overlap={overlap_pixels})") + self.log(f"New row Y position: {self._row_start_y} (mosaic height: {self.state.mosaic_height})") self.log(f"New row X position: {x_offset} (transition strip location)") with self._state_lock: self.state.current_y = 0 # Set current_x to the x_offset where transition strips are + # This is where row 2 will START (then scan LEFT from here) self.state.current_x = x_offset self.motion.send_command('s')