larger fram for comparison
This commit is contained in:
parent
750f7164c7
commit
083c4454d6
1 changed files with 187 additions and 30 deletions
|
|
@ -594,12 +594,15 @@ class StitchingScanner:
|
|||
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 the corner region of the mosaic.
|
||||
with a large region of the mosaic.
|
||||
|
||||
For LEFT direction (starting at right edge): compare against bottom-right corner
|
||||
For RIGHT direction (starting at left edge): compare against bottom-left corner
|
||||
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
|
||||
|
||||
This handles the combined X+Y offset from gear backlash during row transitions.
|
||||
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()
|
||||
|
||||
|
|
@ -609,38 +612,43 @@ class StitchingScanner:
|
|||
mh, mw = self.mosaic.shape[:2]
|
||||
fh, fw = frame.shape[:2]
|
||||
|
||||
# Use a corner region for alignment - this captures both X and Y offset
|
||||
overlap_size = min(fw // 2, fh // 2, 200) # Square-ish overlap region
|
||||
# Use LARGE overlap - 75% of frame dimensions for better matching
|
||||
overlap_width = int(fw * 0.75)
|
||||
overlap_height = int(fh * 0.75)
|
||||
|
||||
if overlap_size < 50:
|
||||
self.log(f"Row start alignment: overlap too small ({overlap_size})")
|
||||
# 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 corner with mosaic's bottom-right corner
|
||||
# Compare frame's top-right region with mosaic's bottom-right region
|
||||
|
||||
# Frame region: top-right corner
|
||||
frame_region = frame[:overlap_size, fw - overlap_size:]
|
||||
# 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_size:mh, mw - overlap_size:mw]
|
||||
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 corner with mosaic's bottom-left corner
|
||||
# Compare frame's top-left region with mosaic's bottom-left region
|
||||
|
||||
# Frame region: top-left corner
|
||||
frame_region = frame[:overlap_size, :overlap_size]
|
||||
# Frame region: top-left
|
||||
frame_region = frame[:overlap_height, :overlap_width]
|
||||
|
||||
# Mosaic region: bottom-left corner
|
||||
mosaic_region = self.mosaic[mh - overlap_size:mh, :overlap_size]
|
||||
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 < 50 or min_w < 50:
|
||||
if min_h < 100 or min_w < 100:
|
||||
self.log(f"Row start alignment: region too small ({min_w}x{min_h})")
|
||||
return offset
|
||||
|
||||
|
|
@ -650,8 +658,8 @@ class StitchingScanner:
|
|||
# Detect displacement with confidence
|
||||
dx, dy, confidence = self._detect_displacement_with_confidence(mosaic_region, frame_region)
|
||||
|
||||
# Sanity check - reject very large displacements
|
||||
max_adjust = 100 # Allow larger adjustment at row start
|
||||
# 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
|
||||
|
|
@ -659,15 +667,164 @@ class StitchingScanner:
|
|||
offset.x_offset = dx
|
||||
offset.y_offset = dy
|
||||
offset.confidence = confidence
|
||||
offset.valid = confidence > 0.05 # Lower threshold for row start
|
||||
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}")
|
||||
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 special handling for large overlap.
|
||||
|
||||
This uses the alignment detected by _detect_row_start_alignment to properly
|
||||
position the first strip, which sets the baseline for the entire row.
|
||||
"""
|
||||
BLEND_WIDTH = 10
|
||||
|
||||
with self._mosaic_lock:
|
||||
if self.mosaic is None:
|
||||
return
|
||||
|
||||
mh, mw = self.mosaic.shape[:2]
|
||||
fh, fw = frame.shape[:2]
|
||||
|
||||
# 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
|
||||
self.log(f"Applied first-strip alignment: X={alignment.x_offset:.1f}, Y={alignment.y_offset:.1f}")
|
||||
|
||||
align_x = self._cumulative_align_x
|
||||
align_y = self._cumulative_align_y
|
||||
|
||||
# Calculate Y position - we're at the bottom of existing mosaic
|
||||
# The frame overlaps with the last ~row_overlap portion of existing content
|
||||
row_overlap_pixels = int(fh * self.config.row_overlap)
|
||||
y_offset = mh - row_overlap_pixels
|
||||
|
||||
# Apply Y alignment correction
|
||||
y_offset = y_offset + int(round(align_y))
|
||||
y_offset = max(0, y_offset)
|
||||
|
||||
if direction == ScanDirection.LEFT:
|
||||
# Starting at right edge - place frame aligned with right side of mosaic
|
||||
# The frame's right edge should align with mosaic's right edge (with X correction)
|
||||
x_offset = mw - fw + int(round(align_x))
|
||||
x_offset = max(0, min(x_offset, mw - BLEND_WIDTH))
|
||||
|
||||
self.log(f"=== First Strip of Row (LEFT) ===")
|
||||
self.log(f" Mosaic: {mw}x{mh}, Frame: {fw}x{fh}")
|
||||
self.log(f" Placement: X={x_offset}, Y={y_offset}")
|
||||
self.log(f" Alignment: X={align_x:.1f}, Y={align_y:.1f}")
|
||||
|
||||
# Blend this frame into the mosaic at the calculated position
|
||||
# We're placing within existing bounds, so just blend in place
|
||||
|
||||
# Calculate what portion of the frame to use
|
||||
# Most of it overlaps, we just need to blend it properly
|
||||
blend_region_width = min(fw, mw - x_offset)
|
||||
|
||||
# Create blended result
|
||||
result = self.mosaic.copy()
|
||||
|
||||
# Blend the overlapping region
|
||||
frame_to_place = frame[:, :blend_region_width]
|
||||
|
||||
# Vertical blend at top edge (blending with row above)
|
||||
v_blend_h = min(row_overlap_pixels, BLEND_WIDTH * 2)
|
||||
if v_blend_h > 0 and y_offset > 0:
|
||||
alpha_v = np.linspace(0, 1, v_blend_h, dtype=np.float32)[:, np.newaxis, np.newaxis]
|
||||
|
||||
blend_y_start = y_offset
|
||||
blend_y_end = min(y_offset + v_blend_h, mh)
|
||||
actual_blend_h = blend_y_end - blend_y_start
|
||||
|
||||
if actual_blend_h > 0:
|
||||
mosaic_overlap = result[blend_y_start:blend_y_end, x_offset:x_offset + blend_region_width].astype(np.float32)
|
||||
frame_overlap = frame_to_place[:actual_blend_h, :].astype(np.float32)
|
||||
|
||||
# Resize alpha if needed
|
||||
alpha_v_actual = np.linspace(0, 1, actual_blend_h, dtype=np.float32)[:, np.newaxis, np.newaxis]
|
||||
|
||||
min_w_blend = min(mosaic_overlap.shape[1], frame_overlap.shape[1])
|
||||
mosaic_overlap = mosaic_overlap[:, :min_w_blend]
|
||||
frame_overlap = frame_overlap[:, :min_w_blend]
|
||||
|
||||
blended = (mosaic_overlap * (1 - alpha_v_actual) + frame_overlap * alpha_v_actual).astype(np.uint8)
|
||||
result[blend_y_start:blend_y_end, x_offset:x_offset + min_w_blend] = blended
|
||||
|
||||
# Place rest of frame below blend zone
|
||||
if y_offset + v_blend_h < mh:
|
||||
remaining_h = min(fh - v_blend_h, mh - (y_offset + v_blend_h))
|
||||
if remaining_h > 0:
|
||||
result[y_offset + v_blend_h:y_offset + v_blend_h + remaining_h,
|
||||
x_offset:x_offset + min_w_blend] = frame_to_place[v_blend_h:v_blend_h + remaining_h, :min_w_blend]
|
||||
|
||||
self.mosaic = result
|
||||
|
||||
else: # RIGHT direction
|
||||
# Starting at left edge - place frame aligned with left side of mosaic
|
||||
x_offset = int(round(align_x))
|
||||
x_offset = max(0, x_offset)
|
||||
|
||||
self.log(f"=== First Strip of Row (RIGHT) ===")
|
||||
self.log(f" Mosaic: {mw}x{mh}, Frame: {fw}x{fh}")
|
||||
self.log(f" Placement: X={x_offset}, Y={y_offset}")
|
||||
self.log(f" Alignment: X={align_x:.1f}, Y={align_y:.1f}")
|
||||
|
||||
# Similar blending logic for right direction
|
||||
result = self.mosaic.copy()
|
||||
blend_region_width = min(fw, mw - x_offset)
|
||||
frame_to_place = frame[:, :blend_region_width]
|
||||
|
||||
# Vertical blend at top edge
|
||||
v_blend_h = min(row_overlap_pixels, BLEND_WIDTH * 2)
|
||||
if v_blend_h > 0 and y_offset > 0:
|
||||
blend_y_start = y_offset
|
||||
blend_y_end = min(y_offset + v_blend_h, mh)
|
||||
actual_blend_h = blend_y_end - blend_y_start
|
||||
|
||||
if actual_blend_h > 0:
|
||||
mosaic_overlap = result[blend_y_start:blend_y_end, x_offset:x_offset + blend_region_width].astype(np.float32)
|
||||
frame_overlap = frame_to_place[:actual_blend_h, :].astype(np.float32)
|
||||
|
||||
alpha_v_actual = np.linspace(0, 1, actual_blend_h, dtype=np.float32)[:, np.newaxis, np.newaxis]
|
||||
|
||||
min_w_blend = min(mosaic_overlap.shape[1], frame_overlap.shape[1])
|
||||
mosaic_overlap = mosaic_overlap[:, :min_w_blend]
|
||||
frame_overlap = frame_overlap[:, :min_w_blend]
|
||||
|
||||
blended = (mosaic_overlap * (1 - alpha_v_actual) + frame_overlap * alpha_v_actual).astype(np.uint8)
|
||||
result[blend_y_start:blend_y_end, x_offset:x_offset + min_w_blend] = blended
|
||||
|
||||
if y_offset + v_blend_h < mh:
|
||||
remaining_h = min(fh - v_blend_h, mh - (y_offset + v_blend_h))
|
||||
if remaining_h > 0:
|
||||
result[y_offset + v_blend_h:y_offset + v_blend_h + remaining_h,
|
||||
x_offset:x_offset + min_w_blend] = frame_to_place[v_blend_h:v_blend_h + remaining_h, :min_w_blend]
|
||||
|
||||
self.mosaic = result
|
||||
|
||||
# Update current position for subsequent strips
|
||||
with self._state_lock:
|
||||
if direction == ScanDirection.LEFT:
|
||||
self.state.current_x = x_offset
|
||||
else:
|
||||
self.state.current_x = x_offset + blend_region_width
|
||||
self.state.current_y = y_offset
|
||||
self.state.append_count += 1
|
||||
|
||||
self.log(f" First strip placed. Position: ({self.state.current_x}, {self.state.current_y})")
|
||||
|
||||
if self.on_mosaic_updated:
|
||||
self.on_mosaic_updated()
|
||||
def _append_strip(self, frame: np.ndarray, direction: ScanDirection):
|
||||
"""Append strip to mosaic based on accumulated displacement with continuous alignment."""
|
||||
BLEND_WIDTH = 10
|
||||
|
|
@ -851,16 +1008,15 @@ class StitchingScanner:
|
|||
# Serpentine: even rows right, odd rows left
|
||||
h_direction = ScanDirection.RIGHT if row % 2 == 0 else ScanDirection.LEFT
|
||||
|
||||
# For rows after the first, detect row-start alignment
|
||||
# For rows after the first, detect and apply row-start alignment with large overlap
|
||||
if row > 0:
|
||||
frame = self._capture_frame()
|
||||
row_alignment = self._detect_row_start_alignment(frame, h_direction)
|
||||
if row_alignment.valid:
|
||||
# Apply row-start alignment to cumulative
|
||||
self._cumulative_align_x += row_alignment.x_offset
|
||||
self._cumulative_align_y += row_alignment.y_offset
|
||||
self.log(f"Applied row-start alignment: X={row_alignment.x_offset:.1f}, Y={row_alignment.y_offset:.1f}")
|
||||
self.log(f"New cumulative: X={self._cumulative_align_x:.1f}, Y={self._cumulative_align_y:.1f}")
|
||||
|
||||
# Append the first strip with the detected alignment
|
||||
self._append_first_strip_of_row(frame, h_direction, row_alignment)
|
||||
|
||||
self.log(f"After first strip - cumulative: X={self._cumulative_align_x:.1f}, Y={self._cumulative_align_y:.1f}")
|
||||
|
||||
stop_reason = self._scan_direction(h_direction)
|
||||
|
||||
|
|
@ -872,7 +1028,7 @@ class StitchingScanner:
|
|||
self.log(f"Max height reached ({self.state.mosaic_height}px)")
|
||||
break
|
||||
|
||||
# Move to next row using same stitching approach
|
||||
# Move to next row
|
||||
if not self._move_to_next_row():
|
||||
self.log("Failed to move to next row")
|
||||
break
|
||||
|
|
@ -891,6 +1047,7 @@ class StitchingScanner:
|
|||
self.motion.stop_all()
|
||||
with self._state_lock:
|
||||
self.state.is_scanning = False
|
||||
|
||||
def _scan_direction(self, direction: ScanDirection) -> str:
|
||||
"""Scan in a direction until edge or max dimension reached."""
|
||||
self.log(f"Scanning {direction.value}...")
|
||||
|
|
|
|||
Loading…
Reference in a new issue