reset region comp

This commit is contained in:
2ManyProjects 2026-01-12 18:11:47 -06:00
parent c8695c1216
commit 4eaf692edb

View file

@ -6,36 +6,30 @@ No complex visual matching - just track displacement and append strips.
Continuous alignment correction for gear slippage compensation. Continuous alignment correction for gear slippage compensation.
FIXES: FIXES:
- Y comparison: Compare frame's BOTTOM with ROW 1's TOP (at Y=transition_height) - Row-start alignment now compares with TRANSITION STRIPS, not old row 1
- X comparison (LEFT scan): Compare frame's RIGHT with transition strip's LEFT edge - Correct X position tracking after row transition
- row_start_y now positions row 2 so its bottom overlaps with row 1's top - Fixed LEFT scan stop condition
- _detect_strip_alignment now uses SAME bounding boxes as UI draws
DEBUG BORDER COLORS (for debugging row 2 placement): DEBUG BORDER COLORS (for debugging row 2 placement):
================================================= =================================================
In _detect_row_start_alignment(): In _detect_row_start_alignment():
- WHITE (thick): Outline of TRANSITION STRIPS (X=x_offset, Y=0 to transition_height) - 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 row 1) - BLUE line: Y=transition_height (boundary between transition strips and old row 1)
- CYAN: Y comparison region - TOP of ROW 1 (just below blue line) - CYAN: Mosaic region used for Y alignment comparison (bottom of transition strips)
- YELLOW: X comparison region - at expected_x position
- MAGENTA: Where the new frame is EXPECTED to be placed - MAGENTA: Where the new frame is EXPECTED to be placed
- YELLOW: Mosaic region used for X alignment comparison (edge of transition strips)
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)
In _blend_horizontal_at_y (append_left): In _blend_horizontal_at_y (append_left):
- RED (thick): Where strip WOULD have been placed (ORIGINAL position) - RED (thick): Where strip WOULD have been placed (ORIGINAL position before alignment)
- GREEN: Where strip was ACTUALLY placed (ADJUSTED position) - GREEN: Where strip was ACTUALLY placed (ADJUSTED position after alignment)
MOSAIC LAYOUT AFTER DOWN TRANSITION: MOSAIC LAYOUT AFTER DOWN TRANSITION:
==================================== ====================================
Y=0 to Y=transition_height: TRANSITION STRIPS (at X=x_offset) Y=0 to Y=transition_height: TRANSITION STRIPS (at X=x_offset to X=x_offset+fw)
Y=transition_height to Y=mh: ROW 1 (shifted down) 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 Where x_offset = mosaic_width - initial_frame_width
(so row 2's bottom overlaps with row 1's top) And transition_height = mosaic_height - initial_frame_height
""" """
import cv2 import cv2
@ -218,11 +212,11 @@ class StitchingScanner:
Detect alignment at the START of a new row. Detect alignment at the START of a new row.
After a DOWN transition with prepend, the mosaic layout is: After a DOWN transition with prepend, the mosaic layout is:
- Y=0 to Y=transition_height: TRANSITION STRIPS (at X=x_offset) - Y=0 to Ytransition_height: TRANSITION STRIPS (placed at x_offset)
- Y=transition_height to Y=mh: OLD ROW 1 (shifted down) - Ytransition_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) The current frame should overlap with the TRANSITION STRIPS, not old row 1.
For X alignment (LEFT scan): Compare frame's RIGHT with transition strip's LEFT edge The camera is at the X position where transition strips were placed.
""" """
offset = AlignmentOffset() offset = AlignmentOffset()
@ -238,14 +232,16 @@ class StitchingScanner:
x_offset = max(0, mw - self.state.mosaic_init_width) x_offset = max(0, mw - self.state.mosaic_init_width)
# Transition height is how much was added during DOWN transition # 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 transition_height = mh - fh
self.log(f" Row-start alignment: mosaic {mw}x{mh}, frame {fw}x{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" 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 ========== # ========== 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, cv2.rectangle(self.mosaic,
(x_offset, 0), (x_offset, 0),
(min(x_offset + fw, mw), transition_height), (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)") self.log(f" DEBUG: BLUE line at Y={transition_height} (transition/row1 boundary)")
vertical_overlap = min(200, fh // 3) vertical_overlap = min(200, fh // 3)
horizontal_overlap = min(200, fw // 3)
min_overlap = 50 min_overlap = 50
# ============================================= # =============================================
# Step 1: Detect Y alignment # Step 1: Detect Y alignment
# ============================================= # =============================================
# Compare frame's BOTTOM with ROW 1's TOP (at Y=transition_height) # Compare frame's TOP with the BOTTOM of transition strips
# Frame's bottom should overlap with the area just below the blue line # Frame's top portion overlaps with mosaic's transition bottom
# #
# Row 1's top is at Y=transition_height # Transition strips are at Y=0 to Y=transition_height
# So compare mosaic[transition_height : transition_height+overlap] # So transition bottom is around Y=(transition_height - overlap) to Y=transition_height
row1_top_start = transition_height transition_compare_y_start = transition_height
row1_top_end = min(transition_height + vertical_overlap, mh) transition_compare_y_end = max(0, transition_height + vertical_overlap)
# Frame's BOTTOM portion (will overlap with row 1's top) # Frame's TOP should overlap with transition BOTTOM
frame_bottom = frame[fh - vertical_overlap:fh, :] frame_top = frame[:vertical_overlap, :]
# Get the X range - centered on where transition strips are # Get the X range - centered on where transition strips are
x_start = x_offset x_end = x_offset
x_end = min(x_offset + fw, mw) x_start = min(x_offset - fw, mw)
compare_width = x_end - x_start compare_width = x_end - x_start
if row1_top_end > row1_top_start and compare_width >= min_overlap: if transition_compare_y_start >= 0 and compare_width >= min_overlap:
# Mosaic region: TOP of row 1 (just below the transition) # Mosaic region: bottom of transition strips
mosaic_row1_top = self.mosaic[row1_top_start:row1_top_end, x_start:x_end] mosaic_transition_bottom = self.mosaic[transition_compare_y_start:transition_compare_y_end,
x_start:x_end]
# Frame region: bottom portion # Frame region: top portion (what overlaps with transition)
frame_compare = frame_bottom[:, :compare_width] frame_compare = frame_top[:, :compare_width]
# ========== DEBUG: Save frame with comparison region marked ========== # ========== DEBUG: Save frame with comparison region marked ==========
debug_frame = frame.copy() debug_frame = frame.copy()
cv2.rectangle(debug_frame, (0, fh - vertical_overlap), (compare_width, fh), cv2.rectangle(debug_frame, (0, 0), (compare_width, vertical_overlap),
(255, 255, 0), 2) # CYAN - frame's bottom region used for Y comparison (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={fh - vertical_overlap}:{fh}") self.log(f" DEBUG: Frame Y comparison region: X=0:{compare_width}, Y=0:{vertical_overlap}")
try: try:
cv2.imwrite('/tmp/debug_frame_row2_start.png', debug_frame) cv2.imwrite('/tmp/debug_frame_row2_start.png', debug_frame)
except: except:
pass pass
# ========== DEBUG: Draw CYAN border on mosaic for Y comparison region ========== # ========== DEBUG: Draw CYAN border on mosaic for Y comparison region ==========
# CYAN at TOP of row 1 (just below blue line)
cv2.rectangle(self.mosaic, cv2.rectangle(self.mosaic,
(x_start, row1_top_start), (x_start, transition_compare_y_start),
(x_end, row1_top_end), (x_end, transition_compare_y_end),
(255, 255, 0), 2) # CYAN - mosaic comparison region for Y (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 # MAGENTA border: Where frame is EXPECTED to be placed (at transition position)
expected_y_start = transition_height - fh + vertical_overlap # Frame overlaps with row 1
expected_y_start = max(0, expected_y_start)
cv2.rectangle(self.mosaic, cv2.rectangle(self.mosaic,
(x_start, expected_y_start), (x_start, 0),
(x_end, expected_y_start + fh), (x_end, fh),
(255, 0, 255), 2) # MAGENTA - expected frame position (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_w = min(frame_compare.shape[1], mosaic_transition_bottom.shape[1])
min_h = min(frame_compare.shape[0], mosaic_row1_top.shape[0]) min_h = min(frame_compare.shape[0], mosaic_transition_bottom.shape[0])
if min_w >= min_overlap and min_h >= min_overlap: if min_w >= min_overlap and min_h >= min_overlap:
frame_compare = frame_compare[:min_h, :min_w] 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 ========== # ========== DEBUG: Save the comparison regions as images ==========
try: try:
cv2.imwrite('/tmp/debug_frame_bottom_region.png', frame_compare) cv2.imwrite('/tmp/debug_frame_top_region.png', frame_compare)
cv2.imwrite('/tmp/debug_mosaic_row1top_region.png', mosaic_row1_top) cv2.imwrite('/tmp/debug_mosaic_transition_region.png', mosaic_transition_bottom)
self.log(f" DEBUG: Saved comparison regions to /tmp/debug_*.png") self.log(f" DEBUG: Saved comparison regions to /tmp/debug_*.png")
except: except:
pass pass
# Detect displacement # Detect displacement
dx_v, dy_v, conf_v = self._detect_displacement_with_confidence( 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" 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: if conf_v > 0.1:
offset.y_offset = dy_v offset.y_offset = dy_v
@ -343,34 +336,29 @@ class StitchingScanner:
# ============================================= # =============================================
# Step 2: Detect X alignment # Step 2: Detect X alignment
# ============================================= # =============================================
horizontal_overlap = min(200, fw // 3)
if direction == ScanDirection.LEFT: if direction == ScanDirection.LEFT:
# For LEFT scan: frame will move LEFT # For LEFT scan: frame starts at the transition X position
# Frame's RIGHT edge will overlap with existing content's LEFT edge # Compare frame's RIGHT edge with mosaic's transition strip RIGHT edge
# The LEFT edge of transition strips is at X=x_offset
# Compare frame's RIGHT with mosaic's LEFT edge of transition strips
# Mosaic region: LEFT edge of transition strips transition_x_start = x_offset
mosaic_x_start = x_offset transition_x_end = max(x_offset, x_offset + horizontal_overlap)
mosaic_x_end = min(x_offset + horizontal_overlap, mw)
# Y range: within the transition strip area AND row 1 top # Y range: within the transition strip area
y_start = max(0, transition_height - fh // 2) y_start = 0
y_end = min(transition_height + fh // 2, mh) y_end = min(transition_height, fh)
if mosaic_x_end > mosaic_x_start and y_end > y_start: if transition_x_end - transition_x_start >= min_overlap:
mosaic_edge = self.mosaic[y_start:y_end, mosaic_x_start:mosaic_x_end] 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]
# Frame's RIGHT edge
frame_edge = frame[:min(y_end - y_start, fh), fw - (mosaic_x_end - mosaic_x_start):fw]
# ========== DEBUG: Draw YELLOW border for X comparison region ========== # ========== DEBUG: Draw YELLOW border for X comparison region ==========
# YELLOW on LEFT side of transition strips
cv2.rectangle(self.mosaic, cv2.rectangle(self.mosaic,
(mosaic_x_start, y_start), (transition_x_start, y_start),
(mosaic_x_end, y_end), (transition_x_end, y_end),
(0, 255, 255), 2) # YELLOW - mosaic comparison region for X (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_h = min(mosaic_edge.shape[0], frame_edge.shape[0])
min_w = min(mosaic_edge.shape[1], frame_edge.shape[1]) min_w = min(mosaic_edge.shape[1], frame_edge.shape[1])
@ -389,23 +377,22 @@ class StitchingScanner:
if conf_h > offset.confidence: if conf_h > offset.confidence:
offset.confidence = conf_h offset.confidence = conf_h
else: else:
# For RIGHT scan at row start # For RIGHT scan at row start (similar logic but for left edge)
# Frame's LEFT edge will overlap with existing content's RIGHT edge transition_x_start = x_offset
mosaic_x_end = min(x_offset + fw, mw) transition_x_end = min(x_offset + horizontal_overlap, mw)
mosaic_x_start = max(mosaic_x_end - horizontal_overlap, x_offset)
y_start = max(0, transition_height - fh // 2) y_start = 0
y_end = min(transition_height + fh // 2, mh) y_end = min(transition_height, fh)
if mosaic_x_end > mosaic_x_start and y_end > y_start: if transition_x_end - transition_x_start >= min_overlap:
mosaic_edge = self.mosaic[y_start:y_end, mosaic_x_start:mosaic_x_end] mosaic_edge = self.mosaic[y_start:y_end, transition_x_start:transition_x_end]
frame_edge = frame[:min(y_end - y_start, fh), :mosaic_x_end - mosaic_x_start] frame_edge = frame[:y_end, :transition_x_end - transition_x_start]
cv2.rectangle(self.mosaic, cv2.rectangle(self.mosaic,
(mosaic_x_start, y_start), (transition_x_start, y_start),
(mosaic_x_end, y_end), (transition_x_end, y_end),
(0, 255, 255), 2) # YELLOW (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_h = min(mosaic_edge.shape[0], frame_edge.shape[0])
min_w = min(mosaic_edge.shape[1], frame_edge.shape[1]) 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. with the expected overlap region of the mosaic.
This provides continuous correction for gear slippage during scanning. This provides continuous correction for gear slippage during scanning.
Uses the SAME bounding boxes as drawn in the UI for consistency.
Args: Args:
frame: Current camera frame frame: Current camera frame
@ -467,147 +453,67 @@ class StitchingScanner:
mh, mw = self.mosaic.shape[:2] mh, mw = self.mosaic.shape[:2]
fh, fw = frame.shape[:2] fh, fw = frame.shape[:2]
# Calculate key positions - SAME as _detect_row_start_alignment # Clamp expected positions
x_offset = max(0, mw - self.state.mosaic_init_width) expected_y = max(0, min(expected_y, mh - fh))
transition_height = mh - fh expected_x = max(0, min(expected_x, mw - fw))
# Overlap sizes - SAME as UI # Increased overlap for better detection
vertical_overlap = min(200, fh // 3) max_overlap = 250 # Increased from 200
horizontal_overlap = min(200, fw // 3) min_overlap = 40 # Increased from 30
min_overlap = 50
if direction == ScanDirection.RIGHT: 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 # 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: if overlap_width < min_overlap:
return offset 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 # Extract regions
mosaic_region = self.mosaic[y_start:y_end, mw - overlap_width:mw] mosaic_region = self.mosaic[expected_y:expected_y + fh, mw - overlap_width:mw]
frame_region = frame[:y_end - y_start, :overlap_width] frame_region = frame[:, :overlap_width]
elif direction == ScanDirection.LEFT: elif direction == ScanDirection.LEFT:
# Row 2+: Scanning left within existing mosaic # We're placing within existing mosaic, moving left
# Use SAME regions as UI (CYAN for Y, YELLOW for X) # Compare right portion of frame with mosaic at expected position
overlap_width = min(fw // 2, mw - expected_x, max_overlap)
# ============================================= if overlap_width < min_overlap:
# Y comparison: CYAN region return offset
# 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)
# X range for Y comparison - at the current expected position # The frame's right edge should align with mosaic at expected_x + fw
x_start = max(0, int(expected_x)) mosaic_x_end = min(expected_x + fw, mw)
x_end = min(int(expected_x) + fw, mw) mosaic_x_start = max(mosaic_x_end - overlap_width, 0)
compare_width = x_end - x_start actual_overlap = mosaic_x_end - mosaic_x_start
if y_compare_end > y_compare_start and compare_width >= min_overlap: if actual_overlap < min_overlap:
# Frame's TOP portion return offset
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
# ============================================= mosaic_region = self.mosaic[expected_y:expected_y + fh, mosaic_x_start:mosaic_x_end]
# X comparison: YELLOW region frame_region = frame[:, fw - actual_overlap:]
# 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
elif direction == ScanDirection.DOWN: elif direction == ScanDirection.DOWN:
# Vertical transition: appending above (prepend) # We're appending below
# Compare top portion of frame with bottom edge of existing content # Compare top portion of frame with bottom edge of mosaic
overlap_height = min(fh // 2, mh, vertical_overlap) overlap_height = min(fh // 2, mh - expected_y, max_overlap)
if overlap_height < min_overlap: if overlap_height < min_overlap:
return offset return offset
# X position at x_offset (where transition strips go) mosaic_region = self.mosaic[mh - overlap_height:mh, expected_x:expected_x + fw]
x_start = x_offset frame_region = frame[:overlap_height, :]
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]
else: # UP else: # UP
# Compare bottom portion of frame with top edge of mosaic # 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: if overlap_height < min_overlap:
return offset return offset
x_start = x_offset mosaic_region = self.mosaic[:overlap_height, expected_x:expected_x + fw]
x_end = min(x_offset + fw, mw) frame_region = frame[fh - overlap_height:, :]
mosaic_region = self.mosaic[:overlap_height, x_start:x_end]
frame_region = frame[fh - overlap_height:, :x_end - x_start]
# 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_h = min(mosaic_region.shape[0], frame_region.shape[0])
min_w = min(mosaic_region.shape[1], frame_region.shape[1]) 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) dx, dy, confidence = self._detect_displacement_with_confidence(mosaic_region, frame_region)
# Sanity check - reject large displacements # 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: if abs(dx) > max_adjust or abs(dy) > max_adjust:
self.log(f"Strip alignment: displacement too large ({dx:.1f}, {dy:.1f}), limiting") self.log(f"Strip alignment: displacement too large ({dx:.1f}, {dy:.1f}), ignoring")
dx = max(-max_adjust, min(max_adjust, dx)) return offset
dy = max(-max_adjust, min(max_adjust, dy))
offset.x_offset = dx offset.x_offset = dx
offset.y_offset = dy offset.y_offset = dy
offset.confidence = confidence offset.confidence = confidence
offset.valid = confidence > 0.1 offset.valid = confidence > 0.1 # Require minimum confidence
if offset.valid: if offset.valid:
self.log(f" Strip alignment: X={dx:.1f}, Y={dy:.1f}, conf={confidence:.3f}") 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"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}") 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 # Calculate the Y position in the mosaic where the new row starts
# After prepending, the layout is: # Since we use append_below=False during DOWN movement, new content
# Y=0 to Y=transition_height: transition strips # is PREPENDED to the TOP of the mosaic. So the new row starts at Y=0
# Y=transition_height to Y=mh: old row 1 (shifted down) self._row_start_y = 0 # New row is at the TOP after prepending
#
# 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 X position where transition strips were placed # 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) 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)") self.log(f"New row X position: {x_offset} (transition strip location)")
with self._state_lock: with self._state_lock:
self.state.current_y = 0 self.state.current_y = 0
# Set current_x to the x_offset where transition strips are # 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.state.current_x = x_offset
self.motion.send_command('s') self.motion.send_command('s')