better region matching

This commit is contained in:
2ManyProjects 2026-01-11 21:11:34 -06:00
parent b8a51dc3cf
commit 26bfd663d5

View file

@ -6,23 +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:
- Row-start alignment now checks BOTH bottom and side edges - Row-start alignment now compares with TRANSITION STRIPS, not old row 1
- Larger overlap regions for better phase correlation - Correct X position tracking after row transition
- Better strip capture with more overlap - Fixed LEFT scan stop condition
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 OLD ROW 1 content is located in mosaic - WHITE (thick): Outline of where TRANSITION STRIPS are in mosaic (X=x_offset, Y=0 to transition_height)
- BLUE line: Y=0 line (where NEW ROW should start) - BLUE line: Y=transition_height (boundary between transition strips and old row 1)
- ORANGE line: Y=frame_height (bottom of new frame placement area) - CYAN: Mosaic region used for Y alignment comparison (bottom of transition strips)
- CYAN: Mosaic region used for Y alignment comparison
- 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 - YELLOW: Mosaic region used for X alignment comparison (edge of transition strips)
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 before alignment)
- GREEN: Where strip was ACTUALLY placed (ADJUSTED position after 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 to X=x_offset+fw)
Y=transition_height to Y=mh: OLD ROW 1 (shifted down, at X=0 to X=mw)
Where x_offset = mosaic_width - initial_frame_width
And transition_height = mosaic_height - initial_frame_height
""" """
import cv2 import cv2
@ -124,6 +131,11 @@ class StitchingScanner:
# This is critical for placing strips at the correct vertical position # This is critical for placing strips at the correct vertical position
self._row_start_y: int = 0 self._row_start_y: int = 0
# Track the X position in the mosaic where the current row starts
# For row 1 (RIGHT scan): starts at X=0
# For row 2 (LEFT scan): starts at X = mosaic_width - frame_width (right edge)
self._row_start_x: int = 0
# Last strip's alignment for continuity # Last strip's alignment for continuity
self._last_strip_alignment = AlignmentOffset() self._last_strip_alignment = AlignmentOffset()
@ -200,10 +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_amount): NEW content from transition - Y=0 to Ytransition_height: TRANSITION STRIPS (placed at x_offset)
- Y(transition_amount) to Y=mosaic_height: OLD row 1 content (shifted down) - Ytransition_height to Y=mosaic_height: OLD row 1 content (shifted down)
The new frame should overlap visually with OLD row 1 content. 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() offset = AlignmentOffset()
@ -214,27 +227,30 @@ class StitchingScanner:
mh, mw = self.mosaic.shape[:2] mh, mw = self.mosaic.shape[:2]
fh, fw = frame.shape[:2] fh, fw = frame.shape[:2]
# After prepending during row transition: # Calculate where transition strips are in the mosaic
# Old row 1 content was originally at Y=0 to Y=fh in the old mosaic # x_offset is where the DOWN transition strips were placed horizontally
# Now it's shifted down by (mh - fh) pixels x_offset = max(0, mw - self.state.mosaic_init_width)
# So old row 1 starts at approximately Y = mh - fh
old_row1_start = mh - fh # 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" Row-start alignment: mosaic {mw}x{mh}, frame {fw}x{fh}")
self.log(f" Old row 1 estimated start: Y={old_row1_start}") 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}")
# ========== DEBUG: Draw WHITE border showing where OLD ROW 1 content is ========== # ========== DEBUG: Draw borders showing layout ==========
if old_row1_start >= 0: # WHITE border: Where TRANSITION STRIPS are (this is what we should compare with)
cv2.rectangle(self.mosaic, cv2.rectangle(self.mosaic,
(0, old_row1_start), (x_offset, 0),
(mw, min(old_row1_start + fh, mh)), (min(x_offset + fw, mw), transition_height),
(255, 255, 255), 3) # WHITE - old row 1 location (255, 255, 255), 3) # WHITE - transition strips location
self.log(f" DEBUG: WHITE border - OLD ROW 1 location Y={old_row1_start}:{min(old_row1_start + fh, mh)}") self.log(f" DEBUG: WHITE border - TRANSITION STRIPS at X={x_offset}:{min(x_offset + fw, mw)}, Y=0:{transition_height}")
# ========== DEBUG: Draw BLUE line at Y=0 showing where NEW ROW starts ========== # BLUE line at transition boundary (where transition ends and old row 1 begins)
cv2.line(self.mosaic, (0, 0), (mw, 0), (255, 0, 0), 3) # BLUE line at Y=0 cv2.line(self.mosaic, (0, transition_height), (mw, transition_height), (255, 0, 0), 3)
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={transition_height} (transition/row1 boundary)")
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) vertical_overlap = min(200, fh // 3)
min_overlap = 50 min_overlap = 50
@ -242,79 +258,78 @@ class StitchingScanner:
# ============================================= # =============================================
# Step 1: Detect Y alignment # Step 1: Detect Y alignment
# ============================================= # =============================================
# Compare frame's bottom with the TOP of old row 1 content # Compare frame's TOP with the BOTTOM of transition strips
# Frame's bottom: frame[fh-overlap:fh] # Frame's top portion overlaps with mosaic's transition bottom
# Old row 1's top: mosaic[old_row1_start:old_row1_start+overlap] #
# Transition strips are at Y=0 to Y=transition_height
# So transition bottom is around Y=(transition_height - overlap) to Y=transition_height
if old_row1_start >= 0 and old_row1_start + vertical_overlap <= mh: transition_compare_y_end = transition_height
expected_x = 0 if direction == ScanDirection.RIGHT else max(0, mw - fw) transition_compare_y_start = max(0, transition_height - vertical_overlap)
x_end = min(expected_x + fw, mw)
# Frame's bottom portion (will overlap with old row 1's top) # Frame's TOP should overlap with transition BOTTOM
frame_bottom = frame[fh - vertical_overlap:fh, :x_end - expected_x] frame_top = frame[:vertical_overlap, :]
# Old row 1's top portion in the mosaic # Get the X range - centered on where transition strips are
mosaic_top_of_old = self.mosaic[old_row1_start:old_row1_start + vertical_overlap, x_start = x_offset
expected_x:x_end] x_end = min(x_offset + fw, mw)
compare_width = x_end - x_start
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: top portion (what overlaps with transition)
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()
# CYAN border on frame showing the region being compared cv2.rectangle(debug_frame, (0, 0), (compare_width, vertical_overlap),
cv2.rectangle(debug_frame, (255, 255, 0), 2) # CYAN - frame's top region used for Y comparison
(0, fh - vertical_overlap), self.log(f" DEBUG: Frame Y comparison region: X=0:{compare_width}, Y=0:{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: try:
cv2.imwrite('/tmp/debug_frame_row2_start.png', debug_frame) cv2.imwrite('/tmp/debug_frame_row2_start.png', debug_frame)
self.log(f" DEBUG: Saved frame to /tmp/debug_frame_row2_start.png")
except: except:
pass pass
# ========== DEBUG: Draw borders on mosaic for comparison regions ========== # ========== DEBUG: Draw CYAN border on mosaic for Y comparison region ==========
# CYAN border: Mosaic region used for Y alignment comparison
cv2.rectangle(self.mosaic, cv2.rectangle(self.mosaic,
(expected_x, old_row1_start), (x_start, transition_compare_y_start),
(x_end, old_row1_start + vertical_overlap), (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={expected_x}:{x_end}, Y={old_row1_start}:{old_row1_start + vertical_overlap}") 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 WOULD be placed (Y=0 to fh) # MAGENTA border: Where frame is EXPECTED to be placed (at transition position)
cv2.rectangle(self.mosaic, cv2.rectangle(self.mosaic,
(expected_x, 0), (x_start, 0),
(min(expected_x + fw, mw), 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={expected_x}:{min(expected_x + fw, mw)}, Y=0:{fh}") self.log(f" DEBUG: MAGENTA border - expected frame position X={x_start}:{x_end}, Y=0:{fh}")
min_w = min(frame_bottom.shape[1], mosaic_top_of_old.shape[1]) min_w = min(frame_compare.shape[1], mosaic_transition_bottom.shape[1])
min_h = min(frame_bottom.shape[0], mosaic_top_of_old.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_bottom = frame_bottom[:min_h, :min_w] frame_compare = frame_compare[:min_h, :min_w]
mosaic_top_of_old = mosaic_top_of_old[: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_bottom) cv2.imwrite('/tmp/debug_frame_top_region.png', frame_compare)
cv2.imwrite('/tmp/debug_mosaic_top_region.png', mosaic_top_of_old) 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: how is frame_bottom shifted relative to mosaic_top_of_old? # 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_top_of_old, frame_bottom) 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[{old_row1_start}:{old_row1_start+vertical_overlap}]") 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:
# dy > 0 means frame_bottom is shifted DOWN relative to mosaic_top_of_old
# To correct in placement: move content UP
# blend formula: y_offset = row_start_y - alignment_y
# So positive alignment_y decreases y_offset, moving content UP
offset.y_offset = dy_v offset.y_offset = dy_v
offset.confidence = conf_v offset.confidence = conf_v
@ -324,22 +339,26 @@ class StitchingScanner:
horizontal_overlap = min(200, fw // 3) horizontal_overlap = min(200, fw // 3)
if direction == ScanDirection.LEFT: if direction == ScanDirection.LEFT:
# For LEFT scan: frame starts at right edge # For LEFT scan: frame starts at the transition X position
# Compare frame's right edge with mosaic's right edge at OLD row 1 position # Compare frame's RIGHT edge with mosaic's transition strip RIGHT edge
if old_row1_start >= 0 and mw >= horizontal_overlap:
y_start = old_row1_start
y_end = min(old_row1_start + fh, mh)
mosaic_edge = self.mosaic[y_start:y_end, mw - horizontal_overlap:mw] transition_x_end = min(x_offset + fw, mw)
frame_edge = frame[:mosaic_edge.shape[0], fw - horizontal_overlap:fw] transition_x_start = max(x_offset, transition_x_end - horizontal_overlap)
# ========== DEBUG: Draw border for X comparison region ========== # Y range: within the transition strip area
# YELLOW border: Mosaic region used for X alignment comparison y_start = 0
y_end = min(transition_height, fh)
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 ==========
cv2.rectangle(self.mosaic, cv2.rectangle(self.mosaic,
(mw - horizontal_overlap, y_start), (transition_x_start, y_start),
(mw, 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={mw - horizontal_overlap}:{mw}, 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])
@ -354,28 +373,26 @@ class StitchingScanner:
self.log(f" Row-start X alignment: dx={dx_h:.1f}, dy={dy_h:.1f}, conf={conf_h:.3f}") self.log(f" Row-start X alignment: dx={dx_h:.1f}, dy={dy_h:.1f}, conf={conf_h:.3f}")
if conf_h > 0.1: if conf_h > 0.1:
# dx > 0 means frame shifted RIGHT
# To correct: move LEFT (decrease x_offset)
# blend: x_offset = x_offset + alignment_x
# So negative alignment_x decreases x_offset
offset.x_offset = -dx_h offset.x_offset = -dx_h
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)
if old_row1_start >= 0 and horizontal_overlap > 0: transition_x_start = x_offset
y_start = old_row1_start transition_x_end = min(x_offset + horizontal_overlap, mw)
y_end = min(old_row1_start + fh, mh)
mosaic_edge = self.mosaic[y_start:y_end, :horizontal_overlap] y_start = 0
frame_edge = frame[:min(y_end - y_start, fh), :horizontal_overlap] y_end = min(transition_height, fh)
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]
# ========== DEBUG: Draw border for X comparison region ==========
cv2.rectangle(self.mosaic, cv2.rectangle(self.mosaic,
(0, y_start), (transition_x_start, y_start),
(horizontal_overlap, y_end), (transition_x_end, y_end),
(0, 255, 255), 2) # YELLOW - mosaic comparison region for X (0, 255, 255), 2) # YELLOW
self.log(f" DEBUG: YELLOW border - mosaic X comparison region X=0:{horizontal_overlap}, 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])
@ -1049,6 +1066,7 @@ class StitchingScanner:
self.log(f"=== Row {row + 1} ===") self.log(f"=== Row {row + 1} ===")
self.log(f"Row start Y position: {self._row_start_y}") self.log(f"Row start Y position: {self._row_start_y}")
self.log(f"Row start X position: {self.state.current_x}")
self.log(f"Cumulative alignment at row start: X={self._cumulative_align_x:.1f}, Y={self._cumulative_align_y:.1f}") self.log(f"Cumulative alignment at row start: X={self._cumulative_align_x:.1f}, Y={self._cumulative_align_y:.1f}")
# Serpentine: even rows right, odd rows left # Serpentine: even rows right, odd rows left
@ -1140,10 +1158,10 @@ class StitchingScanner:
stop_reason = 'max_dim' stop_reason = 'max_dim'
break break
if self.state.current_x >= 0 and direction == ScanDirection.LEFT: # For LEFT scan: stop when we've reached the left edge (current_x <= 0)
self.log(f"Returned to start ({self.config.max_mosaic_width}px)") if self.state.current_x <= 0 and direction == ScanDirection.LEFT:
self.log(f"Current X offset ({self.state.current_x}px) total_x ({total_x}px)") self.log(f"Reached left edge (current_x={self.state.current_x:.1f})")
stop_reason = 'max_dim' stop_reason = 'edge'
break break
if abs(self.state.current_x) >= self.config.max_mosaic_width and direction == ScanDirection.RIGHT: if abs(self.state.current_x) >= self.config.max_mosaic_width and direction == ScanDirection.RIGHT:
@ -1286,14 +1304,21 @@ class StitchingScanner:
# Calculate the Y position in the mosaic where the new row starts # Calculate the Y position in the mosaic where the new row starts
# Since we use append_below=False during DOWN movement, new content # 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 # is PREPENDED to the TOP of the mosaic. So the new row starts at Y=0
# (or close to it, accounting for overlap)
overlap_pixels = int(h * self.config.row_overlap)
self._row_start_y = 0 # New row is at the TOP after prepending 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} (mosaic height: {self.state.mosaic_height})") 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: 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
# This is where row 2 will START (then scan LEFT from here)
self.state.current_x = x_offset
self.motion.send_command('s') self.motion.send_command('s')
time.sleep(self.config.settle_time) time.sleep(self.config.settle_time)