region matched

This commit is contained in:
2ManyProjects 2026-01-12 01:27:10 -06:00
parent 3f935a59bb
commit c8695c1216

View file

@ -6,30 +6,36 @@ 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:
- Row-start alignment now compares with TRANSITION STRIPS, not old row 1 - Y comparison: Compare frame's BOTTOM with ROW 1's TOP (at Y=transition_height)
- Correct X position tracking after row transition - X comparison (LEFT scan): Compare frame's RIGHT with transition strip's LEFT edge
- Fixed LEFT scan stop condition - 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
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 where TRANSITION STRIPS are in mosaic (X=x_offset, Y=0 to transition_height) - WHITE (thick): Outline of TRANSITION STRIPS (X=x_offset, Y=0 to transition_height)
- BLUE line: Y=transition_height (boundary between transition strips and old row 1) - BLUE line: Y=transition_height (boundary between transition strips and row 1)
- CYAN: Mosaic region used for Y alignment comparison (bottom of transition strips) - CYAN: Y comparison region - TOP of ROW 1 (just below blue line)
- 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 before alignment) - RED (thick): Where strip WOULD have been placed (ORIGINAL position)
- GREEN: Where strip was ACTUALLY placed (ADJUSTED position after alignment) - GREEN: Where strip was ACTUALLY placed (ADJUSTED position)
MOSAIC LAYOUT AFTER DOWN TRANSITION: MOSAIC LAYOUT AFTER DOWN TRANSITION:
==================================== ====================================
Y=0 to Y=transition_height: TRANSITION STRIPS (at X=x_offset to X=x_offset+fw) Y=0 to Y=transition_height: TRANSITION STRIPS (at X=x_offset)
Y=transition_height to Y=mh: OLD ROW 1 (shifted down, at X=0 to X=mw) Y=transition_height to Y=mh: ROW 1 (shifted down)
Where x_offset = mosaic_width - initial_frame_width Row 2 placement: Y = transition_height - fh + overlap
And transition_height = mosaic_height - initial_frame_height (so row 2's bottom overlaps with row 1's top)
""" """
import cv2 import cv2
@ -212,11 +218,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 Ytransition_height: TRANSITION STRIPS (placed at x_offset) - Y=0 to Y=transition_height: TRANSITION STRIPS (at X=x_offset)
- Ytransition_height to Y=mosaic_height: OLD row 1 content (shifted down) - Y=transition_height to Y=mh: OLD ROW 1 (shifted down)
The current frame should overlap with the TRANSITION STRIPS, not old row 1. For Y alignment: Compare frame's BOTTOM with ROW 1's TOP (at Y=transition_height)
The camera is at the X position where transition strips were placed. For X alignment (LEFT scan): Compare frame's RIGHT with transition strip's LEFT edge
""" """
offset = AlignmentOffset() offset = AlignmentOffset()
@ -232,16 +238,14 @@ 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 shifted to: Y={transition_height} to Y={mh}") self.log(f" Old row 1 at: Y={transition_height} to Y={mh}")
# ========== DEBUG: Draw borders showing layout ========== # ========== DEBUG: Draw borders showing layout ==========
# WHITE border: Where TRANSITION STRIPS are (this is what we should compare with) # WHITE border: Where TRANSITION STRIPS are
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),
@ -253,81 +257,84 @@ 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 TOP with the BOTTOM of transition strips # Compare frame's BOTTOM with ROW 1's TOP (at Y=transition_height)
# Frame's top portion overlaps with mosaic's transition bottom # Frame's bottom should overlap with the area just below the blue line
# #
# Transition strips are at Y=0 to Y=transition_height # Row 1's top is at Y=transition_height
# So transition bottom is around Y=(transition_height - overlap) to Y=transition_height # So compare mosaic[transition_height : transition_height+overlap]
transition_compare_y_start = transition_height row1_top_start = transition_height
transition_compare_y_end = max(0, transition_height + vertical_overlap) row1_top_end = min(transition_height + vertical_overlap, mh)
# Frame's TOP should overlap with transition BOTTOM # Frame's BOTTOM portion (will overlap with row 1's top)
frame_top = frame[:vertical_overlap, :] frame_bottom = frame[fh - vertical_overlap:fh, :]
# Get the X range - centered on where transition strips are # Get the X range - centered on where transition strips are
x_end = x_offset x_start = x_offset
x_start = min(x_offset - fw, mw) x_end = min(x_offset + fw, mw)
compare_width = x_end - x_start compare_width = x_end - x_start
if transition_compare_y_start >= 0 and compare_width >= min_overlap: if row1_top_end > row1_top_start and compare_width >= min_overlap:
# Mosaic region: bottom of transition strips # Mosaic region: TOP of row 1 (just below the transition)
mosaic_transition_bottom = self.mosaic[transition_compare_y_start:transition_compare_y_end, mosaic_row1_top = self.mosaic[row1_top_start:row1_top_end, x_start:x_end]
x_start:x_end]
# Frame region: top portion (what overlaps with transition) # Frame region: bottom portion
frame_compare = frame_top[:, :compare_width] frame_compare = frame_bottom[:, :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, 0), (compare_width, vertical_overlap), cv2.rectangle(debug_frame, (0, fh - vertical_overlap), (compare_width, fh),
(255, 255, 0), 2) # CYAN - frame's top region used for Y comparison (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=0:{vertical_overlap}") self.log(f" DEBUG: Frame Y comparison region: X=0:{compare_width}, Y={fh - vertical_overlap}:{fh}")
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, transition_compare_y_start), (x_start, row1_top_start),
(x_end, transition_compare_y_end), (x_end, row1_top_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={transition_compare_y_start}:{transition_compare_y_end}") self.log(f" DEBUG: CYAN border - mosaic Y comparison region X={x_start}:{x_end}, Y={row1_top_start}:{row1_top_end}")
# MAGENTA border: Where frame is EXPECTED to be placed (at transition position) # 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)
cv2.rectangle(self.mosaic, cv2.rectangle(self.mosaic,
(x_start, 0), (x_start, expected_y_start),
(x_end, fh), (x_end, expected_y_start + 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=0:{fh}") self.log(f" DEBUG: MAGENTA border - expected frame position X={x_start}:{x_end}, Y={expected_y_start}:{expected_y_start + fh}")
min_w = min(frame_compare.shape[1], mosaic_transition_bottom.shape[1]) min_w = min(frame_compare.shape[1], mosaic_row1_top.shape[1])
min_h = min(frame_compare.shape[0], mosaic_transition_bottom.shape[0]) min_h = min(frame_compare.shape[0], mosaic_row1_top.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_transition_bottom = mosaic_transition_bottom[:min_h, :min_w] mosaic_row1_top = mosaic_row1_top[: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_top_region.png', frame_compare) cv2.imwrite('/tmp/debug_frame_bottom_region.png', frame_compare)
cv2.imwrite('/tmp/debug_mosaic_transition_region.png', mosaic_transition_bottom) cv2.imwrite('/tmp/debug_mosaic_row1top_region.png', mosaic_row1_top)
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_transition_bottom, frame_compare) mosaic_row1_top, 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[0:{vertical_overlap}] with mosaic[{transition_compare_y_start}:{transition_compare_y_end}]") self.log(f" Compared frame[{fh - vertical_overlap}:{fh}] with mosaic[{row1_top_start}:{row1_top_end}]")
if conf_v > 0.1: if conf_v > 0.1:
offset.y_offset = dy_v offset.y_offset = dy_v
@ -336,29 +343,34 @@ 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 starts at the transition X position # For LEFT scan: frame will move LEFT
# Compare frame's RIGHT edge with mosaic's transition strip RIGHT edge # 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
transition_x_start = x_offset # Mosaic region: LEFT edge of transition strips
transition_x_end = max(x_offset, x_offset + horizontal_overlap) mosaic_x_start = x_offset
mosaic_x_end = min(x_offset + horizontal_overlap, mw)
# Y range: within the transition strip area # Y range: within the transition strip area AND row 1 top
y_start = 0 y_start = max(0, transition_height - fh // 2)
y_end = min(transition_height, fh) y_end = min(transition_height + fh // 2, mh)
if transition_x_end - transition_x_start >= min_overlap: if mosaic_x_end > mosaic_x_start and y_end > y_start:
mosaic_edge = self.mosaic[y_start:y_end, transition_x_start:transition_x_end] mosaic_edge = self.mosaic[y_start:y_end, mosaic_x_start:mosaic_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,
(transition_x_start, y_start), (mosaic_x_start, y_start),
(transition_x_end, y_end), (mosaic_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={transition_x_start}:{transition_x_end}, Y={y_start}:{y_end}") self.log(f" DEBUG: YELLOW border - mosaic X comparison region X={mosaic_x_start}:{mosaic_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])
@ -377,22 +389,23 @@ 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 (similar logic but for left edge) # For RIGHT scan at row start
transition_x_start = x_offset # Frame's LEFT edge will overlap with existing content's RIGHT edge
transition_x_end = min(x_offset + horizontal_overlap, mw) mosaic_x_end = min(x_offset + fw, mw)
mosaic_x_start = max(mosaic_x_end - horizontal_overlap, x_offset)
y_start = 0 y_start = max(0, transition_height - fh // 2)
y_end = min(transition_height, fh) y_end = min(transition_height + fh // 2, mh)
if transition_x_end - transition_x_start >= min_overlap: if mosaic_x_end > mosaic_x_start and y_end > y_start:
mosaic_edge = self.mosaic[y_start:y_end, transition_x_start:transition_x_end] mosaic_edge = self.mosaic[y_start:y_end, mosaic_x_start:mosaic_x_end]
frame_edge = frame[:y_end, :transition_x_end - transition_x_start] frame_edge = frame[:min(y_end - y_start, fh), :mosaic_x_end - mosaic_x_start]
cv2.rectangle(self.mosaic, cv2.rectangle(self.mosaic,
(transition_x_start, y_start), (mosaic_x_start, y_start),
(transition_x_end, y_end), (mosaic_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={transition_x_start}:{transition_x_end}, Y={y_start}:{y_end}") self.log(f" DEBUG: YELLOW border - mosaic X comparison region X={mosaic_x_start}:{mosaic_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])
@ -434,6 +447,7 @@ 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
@ -453,67 +467,147 @@ class StitchingScanner:
mh, mw = self.mosaic.shape[:2] mh, mw = self.mosaic.shape[:2]
fh, fw = frame.shape[:2] fh, fw = frame.shape[:2]
# Clamp expected positions # Calculate key positions - SAME as _detect_row_start_alignment
expected_y = max(0, min(expected_y, mh - fh)) x_offset = max(0, mw - self.state.mosaic_init_width)
expected_x = max(0, min(expected_x, mw - fw)) transition_height = mh - fh
# Increased overlap for better detection # Overlap sizes - SAME as UI
max_overlap = 250 # Increased from 200 vertical_overlap = min(200, fh // 3)
min_overlap = 40 # Increased from 30 horizontal_overlap = min(200, fw // 3)
min_overlap = 50
if direction == ScanDirection.RIGHT: if direction == ScanDirection.RIGHT:
# We're appending to the right # Row 1: 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 - expected_x, max_overlap) overlap_width = min(fw // 2, mw, horizontal_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[expected_y:expected_y + fh, mw - overlap_width:mw] mosaic_region = self.mosaic[y_start:y_end, mw - overlap_width:mw]
frame_region = frame[:, :overlap_width] frame_region = frame[:y_end - y_start, :overlap_width]
elif direction == ScanDirection.LEFT: elif direction == ScanDirection.LEFT:
# We're placing within existing mosaic, moving left # Row 2+: Scanning left within existing mosaic
# Compare right portion of frame with mosaic at expected position # Use SAME regions as UI (CYAN for Y, YELLOW for X)
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)
# 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
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
# =============================================
# 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}")
if overlap_width < min_overlap:
return offset return offset
# 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 actual_overlap < min_overlap:
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: elif direction == ScanDirection.DOWN:
# We're appending below # Vertical transition: appending above (prepend)
# Compare top portion of frame with bottom edge of mosaic # Compare top portion of frame with bottom edge of existing content
overlap_height = min(fh // 2, mh - expected_y, max_overlap) overlap_height = min(fh // 2, mh, vertical_overlap)
if overlap_height < min_overlap: if overlap_height < min_overlap:
return offset return offset
mosaic_region = self.mosaic[mh - overlap_height:mh, expected_x:expected_x + fw] # X position at x_offset (where transition strips go)
frame_region = frame[:overlap_height, :] 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]
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, expected_y, max_overlap) overlap_height = min(fh // 2, mh, vertical_overlap)
if overlap_height < min_overlap: if overlap_height < min_overlap:
return offset return offset
mosaic_region = self.mosaic[:overlap_height, expected_x:expected_x + fw] x_start = x_offset
frame_region = frame[fh - overlap_height:, :] x_end = min(x_offset + fw, mw)
# Ensure regions have the same size 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)
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])
@ -528,15 +622,16 @@ 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 = 500 # Max pixels to adjust max_adjust = 80
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}), ignoring") self.log(f"Strip alignment: displacement too large ({dx:.1f}, {dy:.1f}), limiting")
return offset dx = max(-max_adjust, min(max_adjust, dx))
dy = max(-max_adjust, min(max_adjust, dy))
offset.x_offset = dx * -1 offset.x_offset = dx
offset.y_offset = dy offset.y_offset = dy
offset.confidence = confidence offset.confidence = confidence
offset.valid = confidence > 0.1 # Require minimum confidence offset.valid = confidence > 0.1
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}")
@ -1301,22 +1396,32 @@ 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 starts # Calculate the Y position in the mosaic where the new row should be placed
# Since we use append_below=False during DOWN movement, new content # After prepending, the layout is:
# is PREPENDED to the TOP of the mosaic. So the new row starts at Y=0 # Y=0 to Y=transition_height: transition strips
self._row_start_y = 0 # New row is at the TOP after prepending # 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 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} (mosaic height: {self.state.mosaic_height})") self.log(f"New row Y position: {self._row_start_y} (transition_height={transition_height}, overlap={overlap_pixels})")
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')