larger overlap
This commit is contained in:
parent
c31220532b
commit
e9436f1bb5
1 changed files with 349 additions and 391 deletions
|
|
@ -4,6 +4,11 @@ Stitching Scanner v2 - Simplified unified approach
|
||||||
Same displacement-based stitching for both horizontal rows and vertical row transitions.
|
Same displacement-based stitching for both horizontal rows and vertical row transitions.
|
||||||
No complex visual matching - just track displacement and append strips.
|
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:
|
||||||
|
- Row-start alignment now checks BOTH bottom and side edges
|
||||||
|
- Larger overlap regions for better phase correlation
|
||||||
|
- Better strip capture with more overlap
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
|
|
@ -91,7 +96,7 @@ class StitchingScanner:
|
||||||
self._state_lock = threading.Lock()
|
self._state_lock = threading.Lock()
|
||||||
|
|
||||||
self.mosaic: Optional[np.ndarray] = None
|
self.mosaic: Optional[np.ndarray] = None
|
||||||
self._mosaic_lock = threading.Lock()
|
self._mosaic_lock = threading.RLock() # Changed to RLock to avoid deadlocks
|
||||||
|
|
||||||
self._prev_frame: Optional[np.ndarray] = None
|
self._prev_frame: Optional[np.ndarray] = None
|
||||||
self._displacement_since_append_x: float = 0.0
|
self._displacement_since_append_x: float = 0.0
|
||||||
|
|
@ -172,6 +177,133 @@ class StitchingScanner:
|
||||||
|
|
||||||
return (dx, dy)
|
return (dx, dy)
|
||||||
|
|
||||||
|
def _detect_row_start_alignment(self, frame: np.ndarray, direction: ScanDirection) -> AlignmentOffset:
|
||||||
|
"""
|
||||||
|
Detect alignment at the START of a new row by comparing against BOTH:
|
||||||
|
1. The bottom edge of the mosaic (for Y alignment from vertical movement)
|
||||||
|
2. The appropriate side edge (for X alignment)
|
||||||
|
|
||||||
|
This is called after a row transition to properly position the first strip.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: Current camera frame at the start of the new row
|
||||||
|
direction: The horizontal scan direction for this row (LEFT or RIGHT)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AlignmentOffset with combined X/Y correction needed
|
||||||
|
"""
|
||||||
|
offset = AlignmentOffset()
|
||||||
|
|
||||||
|
with self._mosaic_lock:
|
||||||
|
if self.mosaic is None:
|
||||||
|
return offset
|
||||||
|
|
||||||
|
mh, mw = self.mosaic.shape[:2]
|
||||||
|
fh, fw = frame.shape[:2]
|
||||||
|
|
||||||
|
# Use larger overlap regions for better alignment at row start
|
||||||
|
max_overlap = 300 # Increased from 200 for better detection
|
||||||
|
min_overlap = 50 # Increased minimum for reliability
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Step 1: Detect Y alignment from bottom edge
|
||||||
|
# =============================================
|
||||||
|
vertical_overlap = min(fh // 2, max_overlap)
|
||||||
|
|
||||||
|
if vertical_overlap >= min_overlap:
|
||||||
|
# Get the position where we expect the frame to be
|
||||||
|
# After row transition, the frame should overlap with the bottom of mosaic
|
||||||
|
expected_x = int(self.state.current_x)
|
||||||
|
|
||||||
|
# Clamp X position
|
||||||
|
if direction == ScanDirection.LEFT:
|
||||||
|
# For LEFT scanning, we're at the right edge of the mosaic
|
||||||
|
expected_x = max(0, mw - fw)
|
||||||
|
else:
|
||||||
|
# For RIGHT scanning, we're at the left edge
|
||||||
|
expected_x = 0
|
||||||
|
|
||||||
|
# Extract bottom of mosaic
|
||||||
|
mosaic_bottom = self.mosaic[mh - vertical_overlap:mh,
|
||||||
|
expected_x:min(expected_x + fw, mw)]
|
||||||
|
frame_top = frame[:vertical_overlap, :mosaic_bottom.shape[1]]
|
||||||
|
|
||||||
|
if mosaic_bottom.shape[0] >= min_overlap and mosaic_bottom.shape[1] >= min_overlap:
|
||||||
|
# Ensure same size
|
||||||
|
min_h = min(mosaic_bottom.shape[0], frame_top.shape[0])
|
||||||
|
min_w = min(mosaic_bottom.shape[1], frame_top.shape[1])
|
||||||
|
|
||||||
|
if min_h >= min_overlap and min_w >= min_overlap:
|
||||||
|
mosaic_bottom = mosaic_bottom[:min_h, :min_w]
|
||||||
|
frame_top = frame_top[:min_h, :min_w]
|
||||||
|
|
||||||
|
dx_v, dy_v, conf_v = self._detect_displacement_with_confidence(
|
||||||
|
mosaic_bottom, frame_top)
|
||||||
|
|
||||||
|
self.log(f" Row-start vertical alignment: dx={dx_v:.1f}, dy={dy_v:.1f}, conf={conf_v:.3f}")
|
||||||
|
|
||||||
|
if conf_v > 0.05: # Lower threshold for row start
|
||||||
|
offset.y_offset = dy_v
|
||||||
|
offset.confidence = conf_v
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Step 2: Detect X alignment from side edge
|
||||||
|
# =============================================
|
||||||
|
horizontal_overlap = min(fw // 2, max_overlap)
|
||||||
|
|
||||||
|
if horizontal_overlap >= min_overlap:
|
||||||
|
# Calculate where we expect to align horizontally
|
||||||
|
# The frame's bottom portion should overlap with mosaic's bottom
|
||||||
|
expected_y = max(0, mh - fh) # Y position based on row overlap
|
||||||
|
|
||||||
|
if direction == ScanDirection.LEFT:
|
||||||
|
# For LEFT scan: compare left portion of frame with right edge of mosaic
|
||||||
|
mosaic_right = self.mosaic[expected_y:min(expected_y + fh, mh),
|
||||||
|
mw - horizontal_overlap:mw]
|
||||||
|
frame_left = frame[:mosaic_right.shape[0], :horizontal_overlap]
|
||||||
|
else:
|
||||||
|
# For RIGHT scan: compare right portion of frame with left edge of mosaic
|
||||||
|
# (This is mainly for row 0, but kept for completeness)
|
||||||
|
mosaic_left = self.mosaic[expected_y:min(expected_y + fh, mh),
|
||||||
|
:horizontal_overlap]
|
||||||
|
frame_right = frame[:mosaic_left.shape[0], fw - horizontal_overlap:]
|
||||||
|
mosaic_right = mosaic_left
|
||||||
|
frame_left = frame_right
|
||||||
|
|
||||||
|
if mosaic_right.shape[0] >= min_overlap and mosaic_right.shape[1] >= min_overlap:
|
||||||
|
# Ensure same size
|
||||||
|
min_h = min(mosaic_right.shape[0], frame_left.shape[0])
|
||||||
|
min_w = min(mosaic_right.shape[1], frame_left.shape[1])
|
||||||
|
|
||||||
|
if min_h >= min_overlap and min_w >= min_overlap:
|
||||||
|
mosaic_right = mosaic_right[:min_h, :min_w]
|
||||||
|
frame_left = frame_left[:min_h, :min_w]
|
||||||
|
|
||||||
|
dx_h, dy_h, conf_h = self._detect_displacement_with_confidence(
|
||||||
|
mosaic_right, frame_left)
|
||||||
|
|
||||||
|
self.log(f" Row-start horizontal alignment: dx={dx_h:.1f}, dy={dy_h:.1f}, conf={conf_h:.3f}")
|
||||||
|
|
||||||
|
if conf_h > 0.05: # Lower threshold for row start
|
||||||
|
offset.x_offset = dx_h
|
||||||
|
# Use higher confidence of the two
|
||||||
|
if conf_h > offset.confidence:
|
||||||
|
offset.confidence = conf_h
|
||||||
|
|
||||||
|
# Validate combined offset
|
||||||
|
max_adjust = 100 # Allow larger adjustment at row start
|
||||||
|
if abs(offset.x_offset) > max_adjust or abs(offset.y_offset) > max_adjust:
|
||||||
|
self.log(f" Row-start alignment: offset too large ({offset.x_offset:.1f}, {offset.y_offset:.1f}), limiting")
|
||||||
|
offset.x_offset = max(-max_adjust, min(max_adjust, offset.x_offset))
|
||||||
|
offset.y_offset = max(-max_adjust, min(max_adjust, offset.y_offset))
|
||||||
|
|
||||||
|
offset.valid = offset.confidence > 0.05
|
||||||
|
|
||||||
|
if offset.valid:
|
||||||
|
self.log(f" Row-start alignment FINAL: X={offset.x_offset:.1f}, Y={offset.y_offset:.1f}, conf={offset.confidence:.3f}")
|
||||||
|
|
||||||
|
return offset
|
||||||
|
|
||||||
def _detect_strip_alignment(self, frame: np.ndarray, direction: ScanDirection,
|
def _detect_strip_alignment(self, frame: np.ndarray, direction: ScanDirection,
|
||||||
expected_x: int, expected_y: int) -> AlignmentOffset:
|
expected_x: int, expected_y: int) -> AlignmentOffset:
|
||||||
"""
|
"""
|
||||||
|
|
@ -191,95 +323,100 @@ class StitchingScanner:
|
||||||
"""
|
"""
|
||||||
offset = AlignmentOffset()
|
offset = AlignmentOffset()
|
||||||
|
|
||||||
if self.mosaic is None:
|
with self._mosaic_lock:
|
||||||
return offset
|
if self.mosaic is None:
|
||||||
|
|
||||||
mh, mw = self.mosaic.shape[:2]
|
|
||||||
fh, fw = frame.shape[:2]
|
|
||||||
|
|
||||||
# Clamp expected positions
|
|
||||||
expected_y = max(0, min(expected_y, mh - fh))
|
|
||||||
expected_x = max(0, min(expected_x, mw - fw))
|
|
||||||
|
|
||||||
if direction == ScanDirection.RIGHT:
|
|
||||||
# We're appending to the right
|
|
||||||
# Compare left portion of frame with right edge of mosaic
|
|
||||||
overlap_width = min(fw // 2, mw - expected_x, 200) # Use up to 200px overlap
|
|
||||||
|
|
||||||
if overlap_width < 30:
|
|
||||||
return offset
|
return offset
|
||||||
|
|
||||||
# Extract regions
|
mh, mw = self.mosaic.shape[:2]
|
||||||
mosaic_region = self.mosaic[expected_y:expected_y + fh, mw - overlap_width:mw]
|
fh, fw = frame.shape[:2]
|
||||||
frame_region = frame[:, :overlap_width]
|
|
||||||
|
|
||||||
elif direction == ScanDirection.LEFT:
|
# Clamp expected positions
|
||||||
# We're placing within existing mosaic, moving left
|
expected_y = max(0, min(expected_y, mh - fh))
|
||||||
# Compare right portion of frame with mosaic at expected position
|
expected_x = max(0, min(expected_x, mw - fw))
|
||||||
overlap_width = min(fw // 2, mw - expected_x, 200)
|
|
||||||
|
|
||||||
if overlap_width < 30:
|
# Increased overlap for better detection
|
||||||
|
max_overlap = 250 # Increased from 200
|
||||||
|
min_overlap = 40 # Increased from 30
|
||||||
|
|
||||||
|
if direction == ScanDirection.RIGHT:
|
||||||
|
# We're appending to the right
|
||||||
|
# Compare left portion of frame with right edge of mosaic
|
||||||
|
overlap_width = min(fw // 2, mw - expected_x, max_overlap)
|
||||||
|
|
||||||
|
if overlap_width < min_overlap:
|
||||||
|
return offset
|
||||||
|
|
||||||
|
# Extract regions
|
||||||
|
mosaic_region = self.mosaic[expected_y:expected_y + fh, mw - overlap_width:mw]
|
||||||
|
frame_region = frame[:, :overlap_width]
|
||||||
|
|
||||||
|
elif direction == ScanDirection.LEFT:
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
if overlap_width < min_overlap:
|
||||||
|
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:
|
||||||
|
# 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
|
||||||
|
|
||||||
|
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, expected_y, max_overlap)
|
||||||
|
|
||||||
|
if overlap_height < min_overlap:
|
||||||
|
return offset
|
||||||
|
|
||||||
|
mosaic_region = self.mosaic[:overlap_height, expected_x:expected_x + fw]
|
||||||
|
frame_region = frame[fh - overlap_height:, :]
|
||||||
|
|
||||||
|
# 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])
|
||||||
|
|
||||||
|
if min_h < min_overlap or min_w < min_overlap:
|
||||||
|
self.log(f"Strip alignment: overlap too small ({min_w}x{min_h})")
|
||||||
return offset
|
return offset
|
||||||
|
|
||||||
# The frame's right edge should align with mosaic at expected_x + fw
|
mosaic_region = mosaic_region[:min_h, :min_w]
|
||||||
mosaic_x_end = min(expected_x + fw, mw)
|
frame_region = frame_region[:min_h, :min_w]
|
||||||
mosaic_x_start = max(mosaic_x_end - overlap_width, 0)
|
|
||||||
actual_overlap = mosaic_x_end - mosaic_x_start
|
|
||||||
|
|
||||||
if actual_overlap < 30:
|
# Detect displacement with confidence
|
||||||
|
dx, dy, confidence = self._detect_displacement_with_confidence(mosaic_region, frame_region)
|
||||||
|
|
||||||
|
# Sanity check - reject large displacements
|
||||||
|
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
|
return offset
|
||||||
|
|
||||||
mosaic_region = self.mosaic[expected_y:expected_y + fh, mosaic_x_start:mosaic_x_end]
|
offset.x_offset = dx
|
||||||
frame_region = frame[:, fw - actual_overlap:]
|
offset.y_offset = dy
|
||||||
|
offset.confidence = confidence
|
||||||
|
offset.valid = confidence > 0.1 # Require minimum confidence
|
||||||
|
|
||||||
elif direction == ScanDirection.DOWN:
|
if offset.valid:
|
||||||
# We're appending below
|
self.log(f" Strip alignment: X={dx:.1f}, Y={dy:.1f}, conf={confidence:.3f}")
|
||||||
# Compare top portion of frame with bottom edge of mosaic
|
|
||||||
overlap_height = min(fh // 2, mh - expected_y, 200)
|
|
||||||
|
|
||||||
if overlap_height < 30:
|
|
||||||
return offset
|
|
||||||
|
|
||||||
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, expected_y, 200)
|
|
||||||
|
|
||||||
if overlap_height < 30:
|
|
||||||
return offset
|
|
||||||
|
|
||||||
mosaic_region = self.mosaic[:overlap_height, expected_x:expected_x + fw]
|
|
||||||
frame_region = frame[fh - overlap_height:, :]
|
|
||||||
|
|
||||||
# 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])
|
|
||||||
|
|
||||||
if min_h < 30 or min_w < 30:
|
|
||||||
self.log(f"Strip alignment: overlap too small ({min_w}x{min_h})")
|
|
||||||
return offset
|
|
||||||
|
|
||||||
mosaic_region = mosaic_region[:min_h, :min_w]
|
|
||||||
frame_region = frame_region[:min_h, :min_w]
|
|
||||||
|
|
||||||
# Detect displacement with confidence
|
|
||||||
dx, dy, confidence = self._detect_displacement_with_confidence(mosaic_region, frame_region)
|
|
||||||
|
|
||||||
# Sanity check - reject large displacements
|
|
||||||
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
|
|
||||||
|
|
||||||
offset.x_offset = dx
|
|
||||||
offset.y_offset = dy
|
|
||||||
offset.confidence = 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}")
|
|
||||||
|
|
||||||
return offset
|
return offset
|
||||||
|
|
||||||
|
|
@ -380,8 +517,7 @@ class StitchingScanner:
|
||||||
y_offset = y_offset - int(round(alignment_y))
|
y_offset = y_offset - int(round(alignment_y))
|
||||||
|
|
||||||
# Clamp x_offset to valid range
|
# Clamp x_offset to valid range
|
||||||
x_offset = max(0, min(x_offset, w_base - blend_w))
|
x_offset = 0 - min(x_offset, w_base)
|
||||||
# x_offset = 0 - min(x_offset, w_base)
|
|
||||||
|
|
||||||
# Handle strip cropping if y_offset is negative (strip protrudes above frame)
|
# Handle strip cropping if y_offset is negative (strip protrudes above frame)
|
||||||
strip_y_start = 0 # How much to crop from top of strip
|
strip_y_start = 0 # How much to crop from top of strip
|
||||||
|
|
@ -590,218 +726,9 @@ class StitchingScanner:
|
||||||
result[sh - blend_h:sh] = blended
|
result[sh - blend_h:sh] = blended
|
||||||
result[sh:] = base[blend_h:]
|
result[sh:] = base[blend_h:]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _detect_row_start_alignment(self, frame: np.ndarray, direction: ScanDirection) -> AlignmentOffset:
|
|
||||||
"""
|
|
||||||
Detect alignment at the start of a new row by comparing the current frame
|
|
||||||
with a large region of the mosaic.
|
|
||||||
|
|
||||||
Uses a LARGE overlap (most of the frame) because after row transition:
|
|
||||||
- There's both vertical overlap (from row above) and horizontal overlap (from start position)
|
|
||||||
- More overlap = better phase correlation accuracy
|
|
||||||
- First strip alignment is critical for the entire row
|
|
||||||
|
|
||||||
For LEFT direction (starting at right edge): compare against bottom-right of mosaic
|
|
||||||
For RIGHT direction (starting at left edge): compare against bottom-left of mosaic
|
|
||||||
"""
|
|
||||||
offset = AlignmentOffset()
|
|
||||||
|
|
||||||
if self.mosaic is None:
|
|
||||||
return offset
|
|
||||||
|
|
||||||
mh, mw = self.mosaic.shape[:2]
|
|
||||||
fh, fw = frame.shape[:2]
|
|
||||||
|
|
||||||
# Use LARGE overlap - 75% of frame dimensions for better matching
|
|
||||||
overlap_width = int(fw * 0.75)
|
|
||||||
overlap_height = int(fh * 0.75)
|
|
||||||
|
|
||||||
# Ensure we don't exceed mosaic dimensions
|
|
||||||
overlap_width = min(overlap_width, mw)
|
|
||||||
overlap_height = min(overlap_height, mh)
|
|
||||||
|
|
||||||
if overlap_width < 100 or overlap_height < 100:
|
|
||||||
self.log(f"Row start alignment: overlap too small ({overlap_width}x{overlap_height})")
|
|
||||||
return offset
|
|
||||||
|
|
||||||
if direction == ScanDirection.LEFT:
|
|
||||||
# Starting at right edge, going left
|
|
||||||
# Compare frame's top-right region with mosaic's bottom-right region
|
|
||||||
|
|
||||||
# Frame region: top-right (where it overlaps with existing mosaic)
|
|
||||||
frame_region = frame[:overlap_height, fw - overlap_width:]
|
|
||||||
|
|
||||||
# Mosaic region: bottom-right corner
|
|
||||||
mosaic_region = self.mosaic[mh - overlap_height:mh, mw - overlap_width:mw]
|
|
||||||
|
|
||||||
else: # RIGHT direction
|
|
||||||
# Starting at left edge, going right
|
|
||||||
# Compare frame's top-left region with mosaic's bottom-left region
|
|
||||||
|
|
||||||
# Frame region: top-left
|
|
||||||
frame_region = frame[:overlap_height, :overlap_width]
|
|
||||||
|
|
||||||
# Mosaic region: bottom-left corner
|
|
||||||
mosaic_region = self.mosaic[mh - overlap_height:mh, :overlap_width]
|
|
||||||
|
|
||||||
# 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])
|
|
||||||
|
|
||||||
if min_h < 100 or min_w < 100:
|
|
||||||
self.log(f"Row start alignment: region too small ({min_w}x{min_h})")
|
|
||||||
return offset
|
|
||||||
|
|
||||||
mosaic_region = mosaic_region[:min_h, :min_w]
|
|
||||||
frame_region = frame_region[:min_h, :min_w]
|
|
||||||
|
|
||||||
# Detect displacement with confidence
|
|
||||||
dx, dy, confidence = self._detect_displacement_with_confidence(mosaic_region, frame_region)
|
|
||||||
|
|
||||||
# Sanity check - allow larger adjustment at row start due to gear backlash
|
|
||||||
max_adjust = 150
|
|
||||||
if abs(dx) > max_adjust or abs(dy) > max_adjust:
|
|
||||||
self.log(f"Row start 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.05 # Lower threshold - large overlap should give good confidence
|
|
||||||
|
|
||||||
self.log(f"=== Row Start Alignment ({direction.value}) ===")
|
|
||||||
self.log(f" Mosaic: {mw}x{mh}, Frame: {fw}x{fh}")
|
|
||||||
self.log(f" Overlap region: {min_w}x{min_h} (75% of frame)")
|
|
||||||
self.log(f" Detected offset: X={dx:.1f}, Y={dy:.1f}, conf={confidence:.3f}")
|
|
||||||
self.log(f" Valid: {offset.valid}")
|
|
||||||
|
|
||||||
return offset
|
|
||||||
|
|
||||||
|
|
||||||
def _append_first_strip_of_row(self, frame: np.ndarray, direction: ScanDirection, alignment: AlignmentOffset):
|
|
||||||
"""
|
|
||||||
Append the first strip of a new row with proper positioning.
|
|
||||||
|
|
||||||
For LEFT direction: Frame overlaps with bottom-right of mosaic
|
|
||||||
For RIGHT direction: Frame overlaps with bottom-left of mosaic
|
|
||||||
"""
|
|
||||||
BLEND_WIDTH = 20
|
|
||||||
|
|
||||||
with self._mosaic_lock:
|
|
||||||
if self.mosaic is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
mh, mw = self.mosaic.shape[:2]
|
|
||||||
fh, fw = frame.shape[:2]
|
|
||||||
|
|
||||||
self.log(f"=== First Strip of Row ({direction.value}) ===")
|
|
||||||
self.log(f" Mosaic: {mw}x{mh}, Frame: {fw}x{fh}")
|
|
||||||
self.log(f" Alignment input: X={alignment.x_offset:.1f}, Y={alignment.y_offset:.1f}, valid={alignment.valid}")
|
|
||||||
|
|
||||||
# Apply alignment to cumulative tracking
|
|
||||||
if alignment.valid:
|
|
||||||
self._cumulative_align_x += alignment.x_offset
|
|
||||||
self._cumulative_align_y += alignment.y_offset
|
|
||||||
self._last_strip_alignment = alignment
|
|
||||||
|
|
||||||
# Calculate Y position - frame overlaps with bottom of mosaic
|
|
||||||
row_overlap_pixels = int(fh * self.config.row_overlap)
|
|
||||||
y_offset = mh - row_overlap_pixels + int(round(self._cumulative_align_y))
|
|
||||||
y_offset = max(0, min(y_offset, mh - fh)) # Clamp to valid range
|
|
||||||
|
|
||||||
if direction == ScanDirection.LEFT:
|
|
||||||
# Starting at RIGHT edge, going LEFT
|
|
||||||
# Frame's RIGHT edge aligns with mosaic's RIGHT edge
|
|
||||||
# x_offset is where the LEFT edge of the frame goes
|
|
||||||
x_offset = mw - fw + int(round(self._cumulative_align_x))
|
|
||||||
x_offset = max(0, min(x_offset, mw - fw))
|
|
||||||
|
|
||||||
# For LEFT scanning, current_x tracks where LEFT edge of current frame is
|
|
||||||
# This will DECREASE as we scan left
|
|
||||||
start_x_for_scanning = x_offset
|
|
||||||
|
|
||||||
else: # RIGHT
|
|
||||||
# Starting at LEFT edge, going RIGHT
|
|
||||||
x_offset = int(round(self._cumulative_align_x))
|
|
||||||
x_offset = max(0, x_offset)
|
|
||||||
start_x_for_scanning = 0
|
|
||||||
|
|
||||||
self.log(f" Calculated x_offset: {x_offset}, y_offset: {y_offset}")
|
|
||||||
|
|
||||||
# Blend frame into mosaic at calculated position
|
|
||||||
# Simply overwrite with blending - no expansion needed
|
|
||||||
result = self.mosaic.copy()
|
|
||||||
|
|
||||||
# Calculate valid region
|
|
||||||
x_end = min(x_offset + fw, mw)
|
|
||||||
y_end = min(y_offset + fh, mh)
|
|
||||||
frame_x_end = x_end - x_offset
|
|
||||||
frame_y_end = y_end - y_offset
|
|
||||||
|
|
||||||
if frame_x_end <= 0 or frame_y_end <= 0:
|
|
||||||
self.log(f" WARNING: No valid region to blend")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.log(f" Blending region: mosaic[{y_offset}:{y_end}, {x_offset}:{x_end}]")
|
|
||||||
self.log(f" Frame region: frame[0:{frame_y_end}, 0:{frame_x_end}]")
|
|
||||||
|
|
||||||
# Create alpha mask for smooth blending
|
|
||||||
alpha = np.ones((frame_y_end, frame_x_end), dtype=np.float32)
|
|
||||||
|
|
||||||
# Vertical blend at top (blending with row above)
|
|
||||||
v_blend = min(row_overlap_pixels // 2, frame_y_end // 3)
|
|
||||||
if v_blend > 5:
|
|
||||||
v_gradient = np.linspace(0, 1, v_blend, dtype=np.float32)[:, np.newaxis]
|
|
||||||
alpha[:v_blend, :] *= v_gradient
|
|
||||||
|
|
||||||
# Horizontal blend at the edge we came from
|
|
||||||
h_blend = min(BLEND_WIDTH, frame_x_end // 4)
|
|
||||||
if h_blend > 5:
|
|
||||||
if direction == ScanDirection.LEFT:
|
|
||||||
# Came from right, blend right edge
|
|
||||||
h_gradient = np.linspace(1, 0, h_blend, dtype=np.float32)[np.newaxis, :]
|
|
||||||
alpha[:, -h_blend:] *= h_gradient
|
|
||||||
else:
|
|
||||||
# Came from left (or starting), blend left edge if not at edge
|
|
||||||
if x_offset > 0:
|
|
||||||
h_gradient = np.linspace(0, 1, h_blend, dtype=np.float32)[np.newaxis, :]
|
|
||||||
alpha[:, :h_blend] *= h_gradient
|
|
||||||
|
|
||||||
# Apply blending
|
|
||||||
alpha_3ch = alpha[:, :, np.newaxis]
|
|
||||||
mosaic_region = result[y_offset:y_end, x_offset:x_end].astype(np.float32)
|
|
||||||
frame_region = frame[:frame_y_end, :frame_x_end].astype(np.float32)
|
|
||||||
|
|
||||||
blended = (mosaic_region * (1 - alpha_3ch) + frame_region * alpha_3ch).astype(np.uint8)
|
|
||||||
result[y_offset:y_end, x_offset:x_end] = blended
|
|
||||||
|
|
||||||
self.mosaic = result
|
|
||||||
|
|
||||||
# Update position tracking OUTSIDE the mosaic lock
|
|
||||||
with self._state_lock:
|
|
||||||
if direction == ScanDirection.LEFT:
|
|
||||||
# For LEFT scanning: current_x is LEFT edge of where we are
|
|
||||||
# Start at right side, will decrease as we move left
|
|
||||||
self.state.current_x = x_offset
|
|
||||||
else:
|
|
||||||
# For RIGHT scanning: current_x is RIGHT edge of mosaic
|
|
||||||
self.state.current_x = 0
|
|
||||||
|
|
||||||
self.state.current_y = y_offset
|
|
||||||
self.state.append_count += 1
|
|
||||||
|
|
||||||
self.log(f" First strip placed. current_x={self.state.current_x}, current_y={self.state.current_y}")
|
|
||||||
|
|
||||||
# Reset displacement tracking for subsequent strips
|
|
||||||
self._displacement_since_append_x = 0.0
|
|
||||||
self._displacement_since_append_y = 0.0
|
|
||||||
self._prev_frame = frame.copy()
|
|
||||||
|
|
||||||
if self.on_mosaic_updated:
|
|
||||||
self.on_mosaic_updated()
|
|
||||||
|
|
||||||
def _append_strip(self, frame: np.ndarray, direction: ScanDirection):
|
def _append_strip(self, frame: np.ndarray, direction: ScanDirection):
|
||||||
"""Append strip to mosaic based on accumulated displacement."""
|
"""Append strip to mosaic based on accumulated displacement with continuous alignment."""
|
||||||
BLEND_WIDTH = 10
|
BLEND_WIDTH = 10
|
||||||
SAFETY_MARGIN = 2
|
SAFETY_MARGIN = 2
|
||||||
|
|
||||||
|
|
@ -815,6 +742,23 @@ class StitchingScanner:
|
||||||
dx = abs(self._displacement_since_append_x)
|
dx = abs(self._displacement_since_append_x)
|
||||||
dy = abs(self._displacement_since_append_y)
|
dy = abs(self._displacement_since_append_y)
|
||||||
|
|
||||||
|
# Calculate expected position for alignment detection
|
||||||
|
expected_x = int(self.state.current_x + self._cumulative_align_x)
|
||||||
|
expected_y = int(self.state.current_y + self._cumulative_align_y)
|
||||||
|
|
||||||
|
# Detect alignment for this strip
|
||||||
|
alignment = self._detect_strip_alignment(frame, direction, expected_x, expected_y)
|
||||||
|
|
||||||
|
if alignment.valid:
|
||||||
|
# Update cumulative alignment
|
||||||
|
self._cumulative_align_x += alignment.x_offset
|
||||||
|
self._cumulative_align_y += alignment.y_offset
|
||||||
|
self._last_strip_alignment = alignment
|
||||||
|
|
||||||
|
# Get total alignment offsets
|
||||||
|
align_x = self._cumulative_align_x
|
||||||
|
align_y = self._cumulative_align_y
|
||||||
|
|
||||||
if direction in [ScanDirection.RIGHT, ScanDirection.LEFT]:
|
if direction in [ScanDirection.RIGHT, ScanDirection.LEFT]:
|
||||||
append_width = round(dx) + SAFETY_MARGIN
|
append_width = round(dx) + SAFETY_MARGIN
|
||||||
append_width = min(append_width, w - BLEND_WIDTH - 5)
|
append_width = min(append_width, w - BLEND_WIDTH - 5)
|
||||||
|
|
@ -825,72 +769,54 @@ class StitchingScanner:
|
||||||
pixels_consumed = append_width - SAFETY_MARGIN
|
pixels_consumed = append_width - SAFETY_MARGIN
|
||||||
fractional_remainder = dx - pixels_consumed
|
fractional_remainder = dx - pixels_consumed
|
||||||
|
|
||||||
|
# Calculate Y offset for current row
|
||||||
y_offset = int(self.state.current_y)
|
y_offset = int(self.state.current_y)
|
||||||
y_offset = max(0, min(y_offset, mh - h))
|
|
||||||
|
|
||||||
if direction == ScanDirection.RIGHT:
|
if direction == ScanDirection.RIGHT:
|
||||||
# Expanding to the right
|
|
||||||
strip_start = max(0, w - append_width - BLEND_WIDTH)
|
strip_start = max(0, w - append_width - BLEND_WIDTH)
|
||||||
new_strip = frame[:, strip_start:]
|
new_strip = frame[:, strip_start:]
|
||||||
|
|
||||||
self.log(f"RIGHT append: strip from col {strip_start}, width {new_strip.shape[1]}")
|
|
||||||
|
|
||||||
self.mosaic = self._blend_horizontal_at_y(
|
self.mosaic = self._blend_horizontal_at_y(
|
||||||
self.mosaic, new_strip, BLEND_WIDTH, append_right=True,
|
self.mosaic, new_strip, BLEND_WIDTH, append_right=True,
|
||||||
y_offset=y_offset)
|
x_offset=int(self.state.current_x), y_offset=y_offset,
|
||||||
|
alignment_x=align_x, alignment_y=align_y)
|
||||||
else: # LEFT - placing within existing mosaic
|
else:
|
||||||
# current_x is where the LEFT edge of current view is
|
|
||||||
# We're moving left, so new content is on the LEFT of the frame
|
|
||||||
# We want to place the LEFT portion of the frame
|
|
||||||
|
|
||||||
strip_end = min(w, append_width + BLEND_WIDTH)
|
strip_end = min(w, append_width + BLEND_WIDTH)
|
||||||
new_strip = frame[:, :strip_end]
|
new_strip = frame[:, :strip_end]
|
||||||
|
self.mosaic = self._blend_horizontal_at_y(
|
||||||
# Calculate where to place this strip
|
self.mosaic, new_strip, BLEND_WIDTH, append_right=False,
|
||||||
# current_x is decreasing as we move left
|
x_offset=int(self.state.current_x), y_offset=y_offset,
|
||||||
# The strip goes at current_x - append_width
|
alignment_x=align_x, alignment_y=align_y)
|
||||||
new_x = int(self.state.current_x) - append_width
|
|
||||||
new_x = max(0, new_x)
|
|
||||||
|
|
||||||
self.log(f"LEFT append: current_x={self.state.current_x}, new_x={new_x}, strip width={new_strip.shape[1]}")
|
|
||||||
|
|
||||||
# Blend into existing mosaic
|
|
||||||
result = self.mosaic.copy()
|
|
||||||
|
|
||||||
strip_h, strip_w = new_strip.shape[:2]
|
|
||||||
x_end = min(new_x + strip_w, mw)
|
|
||||||
y_end = min(y_offset + strip_h, mh)
|
|
||||||
actual_w = x_end - new_x
|
|
||||||
actual_h = y_end - y_offset
|
|
||||||
|
|
||||||
if actual_w > BLEND_WIDTH and actual_h > 0:
|
|
||||||
# Create horizontal blend on RIGHT side (blending with existing content)
|
|
||||||
alpha = np.ones((actual_h, actual_w), dtype=np.float32)
|
|
||||||
blend_w = min(BLEND_WIDTH, actual_w // 2)
|
|
||||||
if blend_w > 0:
|
|
||||||
h_gradient = np.linspace(1, 0, blend_w, dtype=np.float32)[np.newaxis, :]
|
|
||||||
alpha[:, -blend_w:] = h_gradient
|
|
||||||
|
|
||||||
alpha_3ch = alpha[:, :, np.newaxis]
|
|
||||||
mosaic_region = result[y_offset:y_end, new_x:x_end].astype(np.float32)
|
|
||||||
frame_region = new_strip[:actual_h, :actual_w].astype(np.float32)
|
|
||||||
|
|
||||||
blended = (mosaic_region * (1 - alpha_3ch) + frame_region * alpha_3ch).astype(np.uint8)
|
|
||||||
result[y_offset:y_end, new_x:x_end] = blended
|
|
||||||
|
|
||||||
self.mosaic = result
|
|
||||||
|
|
||||||
# Update current_x to new position (moving left)
|
|
||||||
with self._state_lock:
|
|
||||||
self.state.current_x = new_x
|
|
||||||
|
|
||||||
self._displacement_since_append_x = fractional_remainder
|
self._displacement_since_append_x = fractional_remainder
|
||||||
self._displacement_since_append_y = 0.0
|
self._displacement_since_append_y = 0.0
|
||||||
|
|
||||||
elif direction in [ScanDirection.DOWN, ScanDirection.UP]:
|
elif direction in [ScanDirection.DOWN, ScanDirection.UP]:
|
||||||
# ... keep existing vertical logic ...
|
append_height = round(dy) + SAFETY_MARGIN
|
||||||
pass
|
append_height = min(append_height, h - BLEND_WIDTH - 5)
|
||||||
|
|
||||||
|
if append_height < 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
pixels_consumed = append_height - SAFETY_MARGIN
|
||||||
|
fractional_remainder = dy - pixels_consumed
|
||||||
|
|
||||||
|
if direction == ScanDirection.DOWN:
|
||||||
|
strip_end = min(h, append_height + BLEND_WIDTH)
|
||||||
|
new_strip = frame[:strip_end, :]
|
||||||
|
self.mosaic = self._blend_vertical_at_x(
|
||||||
|
self.mosaic, new_strip, BLEND_WIDTH, append_below=False,
|
||||||
|
x_off=int(self.state.current_x),
|
||||||
|
alignment_x=align_x, alignment_y=align_y)
|
||||||
|
else:
|
||||||
|
strip_start = max(0, h - append_height - BLEND_WIDTH)
|
||||||
|
new_strip = frame[strip_start:, :]
|
||||||
|
self.mosaic = self._blend_vertical_at_x(
|
||||||
|
self.mosaic, new_strip, BLEND_WIDTH, append_below=True,
|
||||||
|
x_off=int(self.state.current_x),
|
||||||
|
alignment_x=align_x, alignment_y=align_y)
|
||||||
|
|
||||||
|
self._displacement_since_append_x = 0.0
|
||||||
|
self._displacement_since_append_y = fractional_remainder
|
||||||
|
|
||||||
new_mh, new_mw = self.mosaic.shape[:2]
|
new_mh, new_mw = self.mosaic.shape[:2]
|
||||||
|
|
||||||
|
|
@ -901,6 +827,7 @@ class StitchingScanner:
|
||||||
|
|
||||||
if self.on_mosaic_updated:
|
if self.on_mosaic_updated:
|
||||||
self.on_mosaic_updated()
|
self.on_mosaic_updated()
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Scan Control
|
# Scan Control
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
@ -983,15 +910,16 @@ class StitchingScanner:
|
||||||
# Serpentine: even rows right, odd rows left
|
# Serpentine: even rows right, odd rows left
|
||||||
h_direction = ScanDirection.RIGHT if row % 2 == 0 else ScanDirection.LEFT
|
h_direction = ScanDirection.RIGHT if row % 2 == 0 else ScanDirection.LEFT
|
||||||
|
|
||||||
# For rows after the first, detect and apply row-start alignment with large overlap
|
# For rows > 0, detect alignment against both edges before scanning
|
||||||
if row > 0:
|
if row > 0:
|
||||||
frame = self._capture_frame()
|
frame = self._capture_frame()
|
||||||
row_alignment = self._detect_row_start_alignment(frame, h_direction)
|
row_alignment = self._detect_row_start_alignment(frame, h_direction)
|
||||||
|
|
||||||
# Append the first strip with the detected alignment
|
if row_alignment.valid:
|
||||||
self._append_first_strip_of_row(frame, h_direction, row_alignment)
|
self.log(f"Applying row-start alignment: X={row_alignment.x_offset:.1f}, Y={row_alignment.y_offset:.1f}")
|
||||||
|
self._cumulative_align_x += row_alignment.x_offset
|
||||||
self.log(f"After first strip - cumulative: X={self._cumulative_align_x:.1f}, Y={self._cumulative_align_y:.1f}")
|
self._cumulative_align_y += row_alignment.y_offset
|
||||||
|
self.log(f"New cumulative alignment: X={self._cumulative_align_x:.1f}, Y={self._cumulative_align_y:.1f}")
|
||||||
|
|
||||||
stop_reason = self._scan_direction(h_direction)
|
stop_reason = self._scan_direction(h_direction)
|
||||||
|
|
||||||
|
|
@ -1003,7 +931,7 @@ class StitchingScanner:
|
||||||
self.log(f"Max height reached ({self.state.mosaic_height}px)")
|
self.log(f"Max height reached ({self.state.mosaic_height}px)")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Move to next row
|
# Move to next row using same stitching approach
|
||||||
if not self._move_to_next_row():
|
if not self._move_to_next_row():
|
||||||
self.log("Failed to move to next row")
|
self.log("Failed to move to next row")
|
||||||
break
|
break
|
||||||
|
|
@ -1032,27 +960,22 @@ class StitchingScanner:
|
||||||
|
|
||||||
frame = self._capture_frame()
|
frame = self._capture_frame()
|
||||||
h, w = frame.shape[:2]
|
h, w = frame.shape[:2]
|
||||||
|
total_x = 0
|
||||||
|
|
||||||
# Setup based on direction
|
# Setup based on direction
|
||||||
if direction in [ScanDirection.RIGHT, ScanDirection.LEFT]:
|
if direction in [ScanDirection.RIGHT, ScanDirection.LEFT]:
|
||||||
threshold_pixels = w * self.config.displacement_threshold
|
threshold_pixels = w * self.config.displacement_threshold
|
||||||
|
max_dim = self.config.max_mosaic_width
|
||||||
|
current_dim = lambda: self.state.mosaic_width
|
||||||
start_cmd = 'E' if direction == ScanDirection.RIGHT else 'W'
|
start_cmd = 'E' if direction == ScanDirection.RIGHT else 'W'
|
||||||
stop_cmd = 'e' if direction == ScanDirection.RIGHT else 'w'
|
stop_cmd = 'e' if direction == ScanDirection.RIGHT else 'w'
|
||||||
else:
|
else:
|
||||||
threshold_pixels = h * self.config.displacement_threshold
|
threshold_pixels = h * self.config.displacement_threshold
|
||||||
|
max_dim = self.config.max_mosaic_height
|
||||||
|
current_dim = lambda: self.state.mosaic_height
|
||||||
start_cmd = 'S' if direction == ScanDirection.DOWN else 'N'
|
start_cmd = 'S' if direction == ScanDirection.DOWN else 'N'
|
||||||
stop_cmd = 's' if direction == ScanDirection.DOWN else 'n'
|
stop_cmd = 's' if direction == ScanDirection.DOWN else 'n'
|
||||||
|
|
||||||
# Track starting position and target for LEFT direction
|
|
||||||
if direction == ScanDirection.LEFT:
|
|
||||||
start_x = self.state.current_x
|
|
||||||
target_x = 0 # We want to reach the left edge
|
|
||||||
self.log(f"LEFT scan: starting at x={start_x}, target x={target_x}")
|
|
||||||
elif direction == ScanDirection.RIGHT:
|
|
||||||
start_x = self.state.current_x
|
|
||||||
target_x = self.config.max_mosaic_width
|
|
||||||
self.log(f"RIGHT scan: starting at x={start_x}, target x={target_x}")
|
|
||||||
|
|
||||||
self._prev_frame = frame.copy()
|
self._prev_frame = frame.copy()
|
||||||
self._displacement_since_append_x = 0.0
|
self._displacement_since_append_x = 0.0
|
||||||
self._displacement_since_append_y = 0.0
|
self._displacement_since_append_y = 0.0
|
||||||
|
|
@ -1061,26 +984,29 @@ class StitchingScanner:
|
||||||
no_movement_count = 0
|
no_movement_count = 0
|
||||||
max_no_movement = 50
|
max_no_movement = 50
|
||||||
stop_reason = 'stopped'
|
stop_reason = 'stopped'
|
||||||
|
self.log(f"Scanning 2..")
|
||||||
while self.running and not self.paused:
|
while self.running and not self.paused:
|
||||||
if time.time() - start_time > self.config.max_scan_time:
|
if time.time() - start_time > self.config.max_scan_time:
|
||||||
self.log("Scan timeout")
|
self.log("Scan timeout")
|
||||||
stop_reason = 'timeout'
|
stop_reason = 'timeout'
|
||||||
break
|
break
|
||||||
|
|
||||||
# Check exit conditions
|
if current_dim() >= max_dim and direction == ScanDirection.RIGHT:
|
||||||
if direction == ScanDirection.RIGHT:
|
self.log(f"Max dimension reached ({current_dim()}px)")
|
||||||
if self.state.mosaic_width >= self.config.max_mosaic_width:
|
stop_reason = 'max_dim'
|
||||||
self.log(f"Max width reached ({self.state.mosaic_width}px)")
|
break
|
||||||
stop_reason = 'max_dim'
|
|
||||||
break
|
if self.state.current_x >= 0 and direction == ScanDirection.LEFT:
|
||||||
|
self.log(f"Returned to start ({self.config.max_mosaic_width}px)")
|
||||||
elif direction == ScanDirection.LEFT:
|
self.log(f"Current X offset ({self.state.current_x}px) total_x ({total_x}px)")
|
||||||
# Stop when we reach the left edge
|
stop_reason = 'max_dim'
|
||||||
if self.state.current_x <= 0:
|
break
|
||||||
self.log(f"Reached left edge (current_x={self.state.current_x})")
|
|
||||||
stop_reason = 'complete'
|
if abs(self.state.current_x) >= self.config.max_mosaic_width and direction == ScanDirection.RIGHT:
|
||||||
break
|
self.log(f"Max dimension reached ({self.config.max_mosaic_width}px)")
|
||||||
|
self.log(f"Current X offset ({self.state.current_x}px)")
|
||||||
|
stop_reason = 'max_dim'
|
||||||
|
break
|
||||||
|
|
||||||
# Pulse motor
|
# Pulse motor
|
||||||
self.motion.send_command(start_cmd)
|
self.motion.send_command(start_cmd)
|
||||||
|
|
@ -1092,19 +1018,12 @@ class StitchingScanner:
|
||||||
curr_frame = self._capture_frame()
|
curr_frame = self._capture_frame()
|
||||||
dx, dy = self._detect_displacement_robust(self._prev_frame, curr_frame)
|
dx, dy = self._detect_displacement_robust(self._prev_frame, curr_frame)
|
||||||
|
|
||||||
# Accumulate displacement magnitude
|
self.log(f"Scanning dx{dx} dy{dy}..")
|
||||||
self._displacement_since_append_x += abs(dx)
|
self._displacement_since_append_x += dx
|
||||||
self._displacement_since_append_y += dy
|
self._displacement_since_append_y += dy
|
||||||
|
total_x += dx
|
||||||
# For LEFT direction, current_x DECREASES
|
with self._state_lock:
|
||||||
# Phase correlation: when camera moves LEFT, content shifts RIGHT, dx > 0
|
self.state.current_x += dx
|
||||||
# So for LEFT scanning, we subtract dx from current_x
|
|
||||||
if direction == ScanDirection.LEFT:
|
|
||||||
with self._state_lock:
|
|
||||||
self.state.current_x -= abs(dx) # Decrease as we go left
|
|
||||||
elif direction == ScanDirection.RIGHT:
|
|
||||||
with self._state_lock:
|
|
||||||
self.state.current_x += abs(dx) # Increase as we go right
|
|
||||||
|
|
||||||
with self._state_lock:
|
with self._state_lock:
|
||||||
self.state.cumulative_x = self._displacement_since_append_x
|
self.state.cumulative_x = self._displacement_since_append_x
|
||||||
|
|
@ -1115,6 +1034,7 @@ class StitchingScanner:
|
||||||
# Edge detection
|
# Edge detection
|
||||||
movement = abs(dx) if direction in [ScanDirection.RIGHT, ScanDirection.LEFT] else abs(dy)
|
movement = abs(dx) if direction in [ScanDirection.RIGHT, ScanDirection.LEFT] else abs(dy)
|
||||||
|
|
||||||
|
self.log(f"Scanning movement{movement}..")
|
||||||
if movement < 1.0:
|
if movement < 1.0:
|
||||||
no_movement_count += 1
|
no_movement_count += 1
|
||||||
if no_movement_count >= max_no_movement:
|
if no_movement_count >= max_no_movement:
|
||||||
|
|
@ -1124,12 +1044,14 @@ class StitchingScanner:
|
||||||
else:
|
else:
|
||||||
no_movement_count = 0
|
no_movement_count = 0
|
||||||
|
|
||||||
# Append when threshold reached
|
# Append when threshold reached (with continuous alignment)
|
||||||
disp = self._displacement_since_append_x if direction in [ScanDirection.RIGHT, ScanDirection.LEFT] else abs(self._displacement_since_append_y)
|
disp = abs(self._displacement_since_append_x) if direction in [ScanDirection.RIGHT, ScanDirection.LEFT] else abs(self._displacement_since_append_y)
|
||||||
|
|
||||||
|
self.log(f"Scanning disp{disp}..")
|
||||||
if disp >= threshold_pixels:
|
if disp >= threshold_pixels:
|
||||||
|
self.log(f"Scanning threshold_pixels..")
|
||||||
self._append_strip(curr_frame, direction)
|
self._append_strip(curr_frame, direction)
|
||||||
self.log(f"Appended, current_x={self.state.current_x:.0f}, mosaic: {self.state.mosaic_width}x{self.state.mosaic_height}")
|
self.log(f"Appended {disp:.1f}px, mosaic: {self.state.mosaic_width}x{self.state.mosaic_height}, align: ({self._cumulative_align_x:.1f}, {self._cumulative_align_y:.1f})")
|
||||||
|
|
||||||
self._prev_frame = curr_frame.copy()
|
self._prev_frame = curr_frame.copy()
|
||||||
|
|
||||||
|
|
@ -1138,7 +1060,7 @@ class StitchingScanner:
|
||||||
|
|
||||||
self.motion.send_command(stop_cmd)
|
self.motion.send_command(stop_cmd)
|
||||||
time.sleep(self.config.settle_time)
|
time.sleep(self.config.settle_time)
|
||||||
self.log(f"Direction finished: {stop_reason}, final current_x={self.state.current_x}")
|
self.log(f"Direction finished: {stop_reason}")
|
||||||
return stop_reason
|
return stop_reason
|
||||||
|
|
||||||
def _move_to_next_row(self) -> bool:
|
def _move_to_next_row(self) -> bool:
|
||||||
|
|
@ -1322,6 +1244,42 @@ class StitchingScanner:
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
def test_row_start_alignment(self, direction: str = 'left') -> dict:
|
||||||
|
"""Test row-start alignment detection."""
|
||||||
|
results = {
|
||||||
|
'success': False,
|
||||||
|
'x_offset': 0.0,
|
||||||
|
'y_offset': 0.0,
|
||||||
|
'confidence': 0.0,
|
||||||
|
'error': None
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.log("Testing row-start alignment detection...")
|
||||||
|
|
||||||
|
if self.mosaic is None:
|
||||||
|
self.log("No mosaic - initializing...")
|
||||||
|
frame = self._capture_frame()
|
||||||
|
self._init_mosaic(frame)
|
||||||
|
|
||||||
|
frame = self._capture_frame()
|
||||||
|
scan_dir = ScanDirection.LEFT if direction == 'left' else ScanDirection.RIGHT
|
||||||
|
|
||||||
|
alignment = self._detect_row_start_alignment(frame, scan_dir)
|
||||||
|
|
||||||
|
results['success'] = alignment.valid
|
||||||
|
results['x_offset'] = alignment.x_offset
|
||||||
|
results['y_offset'] = alignment.y_offset
|
||||||
|
results['confidence'] = alignment.confidence
|
||||||
|
|
||||||
|
self.log(f"Row-start alignment: valid={alignment.valid}, X={alignment.x_offset:.1f}, Y={alignment.y_offset:.1f}, conf={alignment.confidence:.3f}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
results['error'] = str(e)
|
||||||
|
self.log(f"Test error: {e}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
def test_row_transition(self) -> dict:
|
def test_row_transition(self) -> dict:
|
||||||
"""Test row transition using displacement stitching."""
|
"""Test row transition using displacement stitching."""
|
||||||
results = {
|
results = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue