diff --git a/src/stitching_scanner.py b/src/stitching_scanner.py index 465ff0b..3abf237 100644 --- a/src/stitching_scanner.py +++ b/src/stitching_scanner.py @@ -9,6 +9,20 @@ FIXES: - Row-start alignment now checks BOTH bottom and side edges - Larger overlap regions for better phase correlation - Better strip capture with more overlap + +DEBUG BORDER COLORS (for debugging row 2 placement): +================================================= +In _detect_row_start_alignment(): + - WHITE (thick): Outline of where OLD ROW 1 content is located in mosaic + - BLUE line: Y=0 line (where NEW ROW should start) + - ORANGE line: Y=frame_height (bottom of new frame placement area) + - CYAN: Mosaic region used for Y alignment comparison + - MAGENTA: Where the new frame is EXPECTED to be placed + - YELLOW: Mosaic region used for X alignment comparison + +In _blend_horizontal_at_y (append_left): + - RED (thick): Where strip WOULD have been placed (ORIGINAL position before alignment) + - GREEN: Where strip was ACTUALLY placed (ADJUSTED position after alignment) """ import cv2 @@ -209,6 +223,19 @@ class StitchingScanner: self.log(f" Row-start alignment: mosaic {mw}x{mh}, frame {fw}x{fh}") self.log(f" Old row 1 estimated start: Y={old_row1_start}") + # ========== DEBUG: Draw WHITE border showing where OLD ROW 1 content is ========== + if old_row1_start >= 0: + cv2.rectangle(self.mosaic, + (0, old_row1_start), + (mw, min(old_row1_start + fh, mh)), + (255, 255, 255), 3) # WHITE - old row 1 location + self.log(f" DEBUG: WHITE border - OLD ROW 1 location Y={old_row1_start}:{min(old_row1_start + fh, mh)}") + + # ========== DEBUG: Draw BLUE line at Y=0 showing where NEW ROW starts ========== + cv2.line(self.mosaic, (0, 0), (mw, 0), (255, 0, 0), 3) # BLUE line at Y=0 + cv2.line(self.mosaic, (0, fh), (mw, fh), (255, 100, 0), 2) # ORANGE line at Y=fh (bottom of new frame area) + self.log(f" DEBUG: BLUE line at Y=0 (new row start), ORANGE line at Y={fh} (new frame bottom)") + vertical_overlap = min(200, fh // 3) min_overlap = 50 @@ -230,6 +257,37 @@ class StitchingScanner: mosaic_top_of_old = self.mosaic[old_row1_start:old_row1_start + vertical_overlap, expected_x:x_end] + # ========== DEBUG: Save frame with comparison region marked ========== + debug_frame = frame.copy() + # CYAN border on frame showing the region being compared + cv2.rectangle(debug_frame, + (0, fh - vertical_overlap), + (x_end - expected_x, fh), + (255, 255, 0), 2) # CYAN - frame's bottom region used for Y comparison + self.log(f" DEBUG: Frame comparison region: Y={fh - vertical_overlap}:{fh}") + + # Save debug frame + try: + cv2.imwrite('/tmp/debug_frame_row2_start.png', debug_frame) + self.log(f" DEBUG: Saved frame to /tmp/debug_frame_row2_start.png") + except: + pass + + # ========== DEBUG: Draw borders on mosaic for comparison regions ========== + # CYAN border: Mosaic region used for Y alignment comparison + cv2.rectangle(self.mosaic, + (expected_x, old_row1_start), + (x_end, old_row1_start + vertical_overlap), + (255, 255, 0), 2) # CYAN - mosaic comparison region for Y + self.log(f" DEBUG: CYAN border - mosaic Y comparison region X={expected_x}:{x_end}, Y={old_row1_start}:{old_row1_start + vertical_overlap}") + + # MAGENTA border: Where frame WOULD be placed (Y=0 to fh) + cv2.rectangle(self.mosaic, + (expected_x, 0), + (min(expected_x + fw, mw), fh), + (255, 0, 255), 2) # MAGENTA - expected frame position + self.log(f" DEBUG: MAGENTA border - expected frame position X={expected_x}:{min(expected_x + fw, mw)}, Y=0:{fh}") + min_w = min(frame_bottom.shape[1], mosaic_top_of_old.shape[1]) min_h = min(frame_bottom.shape[0], mosaic_top_of_old.shape[0]) @@ -237,6 +295,14 @@ class StitchingScanner: frame_bottom = frame_bottom[:min_h, :min_w] mosaic_top_of_old = mosaic_top_of_old[:min_h, :min_w] + # ========== DEBUG: Save the comparison regions as images ========== + try: + cv2.imwrite('/tmp/debug_frame_bottom_region.png', frame_bottom) + cv2.imwrite('/tmp/debug_mosaic_top_region.png', mosaic_top_of_old) + self.log(f" DEBUG: Saved comparison regions to /tmp/debug_*.png") + except: + pass + # Detect displacement: how is frame_bottom shifted relative to mosaic_top_of_old? dx_v, dy_v, conf_v = self._detect_displacement_with_confidence( mosaic_top_of_old, frame_bottom) @@ -267,6 +333,14 @@ class StitchingScanner: mosaic_edge = self.mosaic[y_start:y_end, mw - horizontal_overlap:mw] frame_edge = frame[:mosaic_edge.shape[0], fw - horizontal_overlap:fw] + # ========== DEBUG: Draw border for X comparison region ========== + # YELLOW border: Mosaic region used for X alignment comparison + cv2.rectangle(self.mosaic, + (mw - horizontal_overlap, y_start), + (mw, y_end), + (0, 255, 255), 2) # YELLOW - mosaic comparison region for X + self.log(f" DEBUG: YELLOW border - mosaic X comparison region X={mw - horizontal_overlap}:{mw}, 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]) @@ -296,6 +370,13 @@ class StitchingScanner: mosaic_edge = self.mosaic[y_start:y_end, :horizontal_overlap] frame_edge = frame[:min(y_end - y_start, fh), :horizontal_overlap] + # ========== DEBUG: Draw border for X comparison region ========== + cv2.rectangle(self.mosaic, + (0, y_start), + (horizontal_overlap, y_end), + (0, 255, 255), 2) # YELLOW - mosaic comparison region for X + self.log(f" DEBUG: YELLOW border - mosaic X comparison region X=0:{horizontal_overlap}, 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]) @@ -428,11 +509,9 @@ class StitchingScanner: # Detect displacement with confidence dx, dy, confidence = self._detect_displacement_with_confidence(mosaic_region, frame_region) - - self.log(f"Displacement dx ({dx} dy {dy}) con {confidence})") # Sanity check - reject large displacements - max_adjust = 400 # Max pixels to adjust + max_adjust = 50 # 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}), ignoring") return offset @@ -440,7 +519,7 @@ class StitchingScanner: offset.x_offset = dx offset.y_offset = dy offset.confidence = confidence - offset.valid = True #confidence > 0.1 # Require minimum confidence + 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}") @@ -542,6 +621,10 @@ class StitchingScanner: if x_offset is None: x_offset = 0 + # Store ORIGINAL position before alignment (for debug) + original_x_offset = x_offset + original_y_offset = y_offset + # Apply alignment offsets (continuous correction) x_offset = x_offset + int(round(alignment_x)) y_offset_before = y_offset @@ -550,7 +633,7 @@ class StitchingScanner: self.log(f" Y offset computation: {y_offset_before} - {int(round(alignment_y))} = {y_offset}") # Clamp x_offset to valid range - x_offset = 0 - min(x_offset, w_base) + x_offset = max(0, min(x_offset, w_base)) # Handle strip cropping if y_offset is negative (strip protrudes above frame) strip_y_start = 0 # How much to crop from top of strip @@ -580,6 +663,8 @@ class StitchingScanner: self.log(f" cumulative: X={self._cumulative_align_x:.1f}, Y={self._cumulative_align_y:.1f}") self.log(f" row_start_y: {self._row_start_y}") self.log(f" Strip crop: rows [{strip_y_start}:{strip_y_end}] -> height {h_cropped}") + self.log(f" ORIGINAL position: X={original_x_offset}, Y={original_y_offset}") + self.log(f" ADJUSTED position: X={x_offset}, Y={y_offset}") # Result is same size as base (no expansion when going left) result = base.copy() @@ -592,6 +677,17 @@ class StitchingScanner: self.log(f" Placing strip at X={strip_x_start}:{strip_x_end}, Y={y_offset}:{y_offset + h_cropped}") self.log(f" Strip cols to copy: {strip_cols_to_copy}") + # ========== DEBUG: Draw borders BEFORE placing strip ========== + # RED border: Where strip WOULD have been placed (original position) + orig_x_end = min(original_x_offset + w_strip, w_base) + orig_y_end = min(original_y_offset + h_strip, h_base) + if original_x_offset >= 0 and original_y_offset >= 0: + cv2.rectangle(result, + (original_x_offset, max(0, original_y_offset)), + (orig_x_end, orig_y_end), + (0, 0, 255), 3) # RED - original position + self.log(f" DEBUG: RED border at original position X={original_x_offset}:{orig_x_end}, Y={original_y_offset}:{orig_y_end}") + # Step 1: Copy strip content (non-blend portion) at correct position # For LEFT scanning, blend is on the RIGHT side of the strip non_blend_end = strip_cols_to_copy - blend_w @@ -612,6 +708,14 @@ class StitchingScanner: result[y_offset:y_offset + h_cropped, blend_x_start:blend_x_end] = blended self.log(f" Step 2: Blend zone at X={blend_x_start}:{blend_x_end}") + # ========== DEBUG: Draw border AFTER placing strip ========== + # GREEN border: Where strip was ACTUALLY placed (adjusted position) + cv2.rectangle(result, + (strip_x_start, y_offset), + (strip_x_end, y_offset + h_cropped), + (0, 255, 0), 2) # GREEN - actual position + self.log(f" DEBUG: GREEN border at actual position X={strip_x_start}:{strip_x_end}, Y={y_offset}:{y_offset + h_cropped}") + self.log(f" Final: Strip placed at X={strip_x_start}, Y={y_offset}, mosaic size unchanged: {w_base}x{h_base}") return result