constant displacement check

This commit is contained in:
2ManyProjects 2026-01-10 18:20:24 -06:00
parent 16cb8b360b
commit 5177eea77e

View file

@ -3,6 +3,7 @@ Stitching Scanner v2 - Simplified unified approach
Same displacement-based stitching for both horizontal rows and vertical row transitions.
No complex visual matching - just track displacement and append strips.
Continuous alignment correction for gear slippage compensation.
"""
import cv2
@ -55,17 +56,19 @@ class StitchState:
@dataclass
class RowAlignmentOffset:
"""Stores the alignment offset calculated at the start of each row"""
class AlignmentOffset:
"""Stores alignment offset for strip placement"""
x_offset: float = 0.0
y_offset: float = 0.0
valid: bool = False
confidence: float = 0.0 # Phase correlation response
class StitchingScanner:
"""
Slide scanner using continuous stitching.
Unified approach for horizontal and vertical movement.
Continuous alignment correction for gear slippage compensation.
"""
def __init__(self, camera, motion_controller, autofocus_controller=None,
@ -94,9 +97,12 @@ class StitchingScanner:
self._displacement_since_append_x: float = 0.0
self._displacement_since_append_y: float = 0.0
# Row alignment offset - calculated at the start of each row
self._row_alignment = RowAlignmentOffset()
self._is_first_strip_of_row: bool = True
# Cumulative alignment drift - tracks total correction applied
self._cumulative_align_x: float = 0.0
self._cumulative_align_y: float = 0.0
# Last strip's alignment for continuity
self._last_strip_alignment = AlignmentOffset()
self._thread: Optional[threading.Thread] = None
@ -130,9 +136,29 @@ class StitchingScanner:
prev_f = prev_f * window
curr_f = curr_f * window
shift, _ = cv2.phaseCorrelate(prev_f, curr_f)
shift, response = cv2.phaseCorrelate(prev_f, curr_f)
return shift
def _detect_displacement_with_confidence(self, prev_frame: np.ndarray,
curr_frame: np.ndarray) -> Tuple[float, float, float]:
"""Detect displacement and return confidence (phase correlation response)."""
prev_gray = self._to_grayscale(prev_frame)
curr_gray = self._to_grayscale(curr_frame)
if prev_gray.shape != curr_gray.shape:
return (0.0, 0.0, 0.0)
prev_f = prev_gray.astype(np.float32)
curr_f = curr_gray.astype(np.float32)
h, w = prev_gray.shape
window = cv2.createHanningWindow((w, h), cv2.CV_32F)
prev_f = prev_f * window
curr_f = curr_f * window
shift, response = cv2.phaseCorrelate(prev_f, curr_f)
return (shift[0], shift[1], response)
def _detect_displacement_robust(self, prev_frame: np.ndarray,
curr_frame: np.ndarray) -> Tuple[float, float]:
dx, dy = self._detect_displacement(prev_frame, curr_frame)
@ -146,76 +172,115 @@ class StitchingScanner:
return (dx, dy)
def _detect_row_alignment(self, current_frame: np.ndarray, direction: ScanDirection) -> RowAlignmentOffset:
def _detect_strip_alignment(self, frame: np.ndarray, direction: ScanDirection,
expected_x: int, expected_y: int) -> AlignmentOffset:
"""
Detect alignment offset at the start of a new row by comparing
the current frame with the overlapping region of the mosaic.
Detect alignment offset for a strip by comparing the current frame
with the expected overlap region of the mosaic.
This compensates for backlash in control gears that causes circular
motion instead of right angles when transitioning from vertical to horizontal.
This provides continuous correction for gear slippage during scanning.
Args:
frame: Current camera frame
direction: Scan direction
expected_x: Expected X position in mosaic
expected_y: Expected Y position in mosaic
Returns:
AlignmentOffset with X/Y correction needed
"""
offset = RowAlignmentOffset()
offset = AlignmentOffset()
with self._mosaic_lock:
if self.mosaic is None:
self.log("No mosaic for row alignment detection")
return offset
mh, mw = self.mosaic.shape[:2]
fh, fw = current_frame.shape[:2]
fh, fw = frame.shape[:2]
# Determine where in the mosaic we expect to overlap
# For LEFT direction: we're at the right edge of the mosaic
# For RIGHT direction: we're at the left edge (but this is row 0, shouldn't need alignment)
# Clamp expected positions
expected_y = max(0, min(expected_y, mh - fh))
expected_x = max(0, min(expected_x, mw - fw))
if direction == ScanDirection.LEFT:
# We're starting from the right side, moving left
# The current frame should overlap with the right edge of the mosaic
# Extract the rightmost portion of the mosaic that should overlap
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
# The overlap region is at the bottom-right of the mosaic
# Current Y position in mosaic
y_pos = int(self.state.current_y)
y_pos = max(0, min(y_pos, mh - fh))
if overlap_width < 30:
return offset
# Extract overlap region from mosaic (right edge)
overlap_width = min(fw, mw)
mosaic_region = self.mosaic[y_pos:y_pos + fh, mw - overlap_width:mw]
frame_region = current_frame[:, :overlap_width]
# Extract regions
mosaic_region = self.mosaic[expected_y:expected_y + fh, mw - overlap_width:mw]
frame_region = frame[:, :overlap_width]
else: # RIGHT direction
# We're starting from the left side, moving right
# Current Y position in mosaic
y_pos = int(self.state.current_y)
y_pos = max(0, min(y_pos, mh - fh))
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, 200)
# Extract overlap region from mosaic (left edge)
overlap_width = min(fw, mw)
mosaic_region = self.mosaic[y_pos:y_pos + fh, :overlap_width]
frame_region = current_frame[:, :overlap_width]
if overlap_width < 30:
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 < 30:
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, 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 < 50 or min_w < 50:
self.log(f"Overlap region too small for alignment: {min_w}x{min_h}")
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 between mosaic region and current frame
dx, dy = self._detect_displacement_robust(mosaic_region, frame_region)
# 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.valid = True
offset.confidence = confidence
offset.valid = confidence > 0.1 # Require minimum confidence
self.log(f"=== Row Alignment Detection ===")
self.log(f" Direction: {direction.value}")
self.log(f" Mosaic region: {mosaic_region.shape[1]}x{mosaic_region.shape[0]} at Y={y_pos}")
self.log(f" Detected offset: X={dx:.1f}, Y={dy:.1f}")
if offset.valid:
self.log(f" Strip alignment: X={dx:.1f}, Y={dy:.1f}, conf={confidence:.3f}")
return offset
@ -231,9 +296,10 @@ class StitchingScanner:
self._displacement_since_append_x = 0.0
self._displacement_since_append_y = 0.0
# Reset row alignment for first row (no alignment needed)
self._row_alignment = RowAlignmentOffset()
self._is_first_strip_of_row = False # First row doesn't need alignment
# Reset cumulative alignment
self._cumulative_align_x = 0.0
self._cumulative_align_y = 0.0
self._last_strip_alignment = AlignmentOffset()
with self._state_lock:
h, w = frame.shape[:2]
@ -262,8 +328,8 @@ class StitchingScanner:
append_right: True to append to right, False to append left
x_offset: X position for left-append mode
y_offset: Y position in the mosaic
alignment_x: Additional X alignment offset (from row alignment detection)
alignment_y: Additional Y alignment offset (from row alignment detection)
alignment_x: Additional X alignment offset (from strip alignment detection)
alignment_y: Additional Y alignment offset (from strip alignment detection)
"""
h_base, w_base = base.shape[:2]
h_strip, w_strip = strip.shape[:2]
@ -310,13 +376,12 @@ class StitchingScanner:
if x_offset is None:
x_offset = 0
# Apply alignment offsets (detected at start of row)
# Apply alignment offsets (continuous correction)
x_offset = x_offset + int(round(alignment_x))
y_offset = y_offset - int(round(alignment_y))
# 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 = max(0, min(x_offset, w_base - blend_w))
# Handle strip cropping if y_offset is negative (strip protrudes above frame)
strip_y_start = 0 # How much to crop from top of strip
@ -343,6 +408,7 @@ class StitchingScanner:
self.log(f" base: {w_base}x{h_base}, strip: {w_strip}x{h_strip}")
self.log(f" x_offset: {x_offset}, y_offset: {y_offset}, blend_w: {blend_w}")
self.log(f" alignment: X={alignment_x:.1f}, Y={alignment_y:.1f}")
self.log(f" cumulative: X={self._cumulative_align_x:.1f}, Y={self._cumulative_align_y:.1f}")
self.log(f" Strip crop: rows [{strip_y_start}:{strip_y_end}] -> height {h_cropped}")
# Result is same size as base (no expansion when going left)
@ -426,12 +492,15 @@ class StitchingScanner:
def _blend_vertical_at_x(self, base: np.ndarray, strip: np.ndarray,
blend_height: int, append_below: bool,
x_off: int = 0) -> np.ndarray:
x_off: int = 0,
alignment_x: float = 0.0, alignment_y: float = 0.0) -> np.ndarray:
h_base, w_base = base.shape[:2]
h_strip, w_strip = strip.shape[:2]
# Clamp x_offset to valid range
# Apply alignment offset for X position
x_offset = max(0, w_base - self.state.mosaic_init_width)
x_offset = x_offset + int(round(alignment_x))
x_offset = max(0, min(x_offset, w_base - w_strip)) if w_strip < w_base else 0
# Create full-width strip with strip placed at x_offset
full_strip = np.zeros((h_strip, w_base, 3), dtype=np.uint8)
@ -439,16 +508,16 @@ class StitchingScanner:
copy_width = min(w_strip, available_width)
full_strip[:, x_offset:x_offset + copy_width] = strip[:, :copy_width]
self.log(f"=== _blend_vertical_at_x ===")
self.log(f" base: {w_base}x{h_base}, strip: {w_strip}x{h_strip}")
self.log(f" x_offset: {x_offset}, alignment: X={alignment_x:.1f}, Y={alignment_y:.1f}")
# Early exit: no blending possible
if blend_height <= 0 or blend_height >= h_strip:
if append_below:
return np.vstack([base, full_strip])
return np.vstack([full_strip, base])
# Height mismatch shouldn't happen with full_strip, but safety check
if w_strip > w_base:
self.log(f"Warning: strip wider than base ({w_strip} > {w_base})")
blend_h = min(blend_height, h_strip, h_base)
if append_below:
@ -523,7 +592,7 @@ class StitchingScanner:
return result
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
SAFETY_MARGIN = 2
@ -537,9 +606,22 @@ class StitchingScanner:
dx = abs(self._displacement_since_append_x)
dy = abs(self._displacement_since_append_y)
# Get alignment offsets for this row
align_x = self._row_alignment.x_offset if self._row_alignment.valid else 0.0
align_y = self._row_alignment.y_offset if self._row_alignment.valid else 0.0
# 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]:
append_width = round(dx) + SAFETY_MARGIN
@ -584,14 +666,18 @@ class StitchingScanner:
if direction == ScanDirection.DOWN:
strip_end = min(h, append_height + BLEND_WIDTH)
new_strip = frame[:strip_end:, :]
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))
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, :]
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))
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
@ -628,8 +714,11 @@ class StitchingScanner:
self._prev_frame = None
self._displacement_since_append_x = 0.0
self._displacement_since_append_y = 0.0
self._row_alignment = RowAlignmentOffset()
self._is_first_strip_of_row = False # First row doesn't need alignment
# Reset cumulative alignment
self._cumulative_align_x = 0.0
self._cumulative_align_y = 0.0
self._last_strip_alignment = AlignmentOffset()
self._thread = threading.Thread(target=self._scan_loop, daemon=True)
self._thread.start()
@ -680,26 +769,13 @@ class StitchingScanner:
self.state.total_rows = row + 1
self.log(f"=== Row {row + 1} ===")
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
h_direction = ScanDirection.RIGHT if row % 2 == 0 else ScanDirection.LEFT
# For rows after the first, detect alignment at the start
if row > 0:
self._is_first_strip_of_row = True
frame = self._capture_frame()
self._row_alignment = self._detect_row_alignment(frame, h_direction)
self.log(f"Row {row + 1} alignment: X={self._row_alignment.x_offset:.1f}, Y={self._row_alignment.y_offset:.1f}")
else:
# First row - no alignment needed
self._row_alignment = RowAlignmentOffset()
self._is_first_strip_of_row = False
stop_reason = self._scan_direction(h_direction)
# After first strip is appended, clear the flag
self._is_first_strip_of_row = False
if not self.running:
break
@ -716,6 +792,7 @@ class StitchingScanner:
row += 1
self.log(f"Scan complete! Final: {self.state.mosaic_width}x{self.state.mosaic_height}")
self.log(f"Final cumulative alignment: X={self._cumulative_align_x:.1f}, Y={self._cumulative_align_y:.1f}")
except Exception as e:
self.log(f"Scan error: {e}")
@ -773,7 +850,7 @@ class StitchingScanner:
break
if self.state.current_x >= 0 and direction == ScanDirection.LEFT:
self.log(f"Max dimension reached ({self.config.max_mosaic_width}px)")
self.log(f"Returned to start ({self.config.max_mosaic_width}px)")
self.log(f"Current X offset ({self.state.current_x}px) total_x ({total_x}px)")
stop_reason = 'max_dim'
break
@ -799,7 +876,6 @@ class StitchingScanner:
total_x += dx
with self._state_lock:
self.state.current_x += dx
self.log(f"Current X offset ({self.state.current_x}px)")
with self._state_lock:
self.state.cumulative_x = self._displacement_since_append_x
@ -818,11 +894,11 @@ class StitchingScanner:
else:
no_movement_count = 0
# Append when threshold reached
# Append when threshold reached (with continuous alignment)
disp = abs(self._displacement_since_append_x) if direction in [ScanDirection.RIGHT, ScanDirection.LEFT] else abs(self._displacement_since_append_y)
if disp >= threshold_pixels:
self._append_strip(curr_frame, direction)
self.log(f"Appended {disp:.1f}px, 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()
@ -837,9 +913,10 @@ class StitchingScanner:
def _move_to_next_row(self) -> bool:
"""
Move down to next row using displacement-based stitching.
Same approach as horizontal scanning.
Same approach as horizontal scanning with continuous alignment.
"""
self.log("Moving to next row...")
self.log(f"Alignment before row transition: X={self._cumulative_align_x:.1f}, Y={self._cumulative_align_y:.1f}")
frame = self._capture_frame()
h, w = frame.shape[:2]
@ -895,14 +972,16 @@ class StitchingScanner:
else:
no_movement_count = 0
# Append strip when threshold reached
# Append strip when threshold reached (with continuous alignment)
if abs(self._displacement_since_append_y) >= threshold_pixels:
self._append_strip(curr_frame, ScanDirection.DOWN)
self.log(f" Row transition: appended, total Y: {abs(total_y):.1f}px")
self.log(f" Row transition: appended, total Y: {abs(total_y):.1f}px, align: ({self._cumulative_align_x:.1f}, {self._cumulative_align_y:.1f})")
# Done when we've moved enough
if abs(total_y) >= target_displacement:
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}")
with self._state_lock:
self.state.current_y = 0
self.motion.send_command('s')
@ -951,6 +1030,19 @@ class StitchingScanner:
append_count=self.state.append_count
)
def get_alignment_state(self) -> dict:
"""Get current alignment correction state."""
return {
'cumulative_x': self._cumulative_align_x,
'cumulative_y': self._cumulative_align_y,
'last_alignment': {
'x': self._last_strip_alignment.x_offset,
'y': self._last_strip_alignment.y_offset,
'confidence': self._last_strip_alignment.confidence,
'valid': self._last_strip_alignment.valid
}
}
def get_mosaic(self) -> Optional[np.ndarray]:
with self._mosaic_lock:
if self.mosaic is not None:
@ -1006,6 +1098,8 @@ class StitchingScanner:
'y_moved': 0.0,
'mosaic_before': (0, 0),
'mosaic_after': (0, 0),
'alignment_before': (0.0, 0.0),
'alignment_after': (0.0, 0.0),
'error': None
}
@ -1017,6 +1111,7 @@ class StitchingScanner:
self._init_mosaic(frame)
results['mosaic_before'] = (self.state.mosaic_width, self.state.mosaic_height)
results['alignment_before'] = (self._cumulative_align_x, self._cumulative_align_y)
with self._state_lock:
self.state.cumulative_y = 0.0
@ -1028,8 +1123,10 @@ class StitchingScanner:
results['success'] = success
results['y_moved'] = self.state.cumulative_y
results['mosaic_after'] = (self.state.mosaic_width, self.state.mosaic_height)
results['alignment_after'] = (self._cumulative_align_x, self._cumulative_align_y)
self.log(f"Row transition: {'SUCCESS' if success else 'FAILED'}, Y: {results['y_moved']:.1f}px")
self.log(f"Alignment change: ({results['alignment_before'][0]:.1f}, {results['alignment_before'][1]:.1f}) -> ({results['alignment_after'][0]:.1f}, {results['alignment_after'][1]:.1f})")
except Exception as e:
results['error'] = str(e)
@ -1046,6 +1143,8 @@ class StitchingScanner:
'appends': 0,
'mosaic_before': (0, 0),
'mosaic_after': (0, 0),
'alignment_before': (0.0, 0.0),
'alignment_after': (0.0, 0.0),
'error': None
}
@ -1057,6 +1156,7 @@ class StitchingScanner:
self._init_mosaic(frame)
results['mosaic_before'] = (self.state.mosaic_width, self.state.mosaic_height)
results['alignment_before'] = (self._cumulative_align_x, self._cumulative_align_y)
appends_before = self.state.append_count
self.motion.set_speed(self.config.scan_speed_index)
@ -1064,13 +1164,6 @@ class StitchingScanner:
self.running = True
scan_dir = ScanDirection.RIGHT if direction == 'right' else ScanDirection.LEFT
# Test row alignment detection if not first row
if self.state.current_row > 0 or self.mosaic is not None:
frame = self._capture_frame()
self._row_alignment = self._detect_row_alignment(frame, scan_dir)
self.log(f"Test row alignment: X={self._row_alignment.x_offset:.1f}, Y={self._row_alignment.y_offset:.1f}")
stop_reason = self._scan_direction(scan_dir)
self.running = False
@ -1078,6 +1171,9 @@ class StitchingScanner:
results['stop_reason'] = stop_reason
results['appends'] = self.state.append_count - appends_before
results['mosaic_after'] = (self.state.mosaic_width, self.state.mosaic_height)
results['alignment_after'] = (self._cumulative_align_x, self._cumulative_align_y)
self.log(f"Alignment change: ({results['alignment_before'][0]:.1f}, {results['alignment_before'][1]:.1f}) -> ({results['alignment_after'][0]:.1f}, {results['alignment_after'][1]:.1f})")
except Exception as e:
results['error'] = str(e)
@ -1085,17 +1181,18 @@ class StitchingScanner:
return results
def test_row_alignment(self, direction: str = 'left') -> dict:
"""Test row alignment detection without scanning."""
def test_strip_alignment(self) -> dict:
"""Test strip alignment detection at current position."""
results = {
'success': False,
'x_offset': 0.0,
'y_offset': 0.0,
'confidence': 0.0,
'error': None
}
try:
self.log(f"Testing row alignment detection ({direction})...")
self.log("Testing strip alignment detection...")
if self.mosaic is None:
self.log("No mosaic - initializing...")
@ -1103,14 +1200,20 @@ class StitchingScanner:
self._init_mosaic(frame)
frame = self._capture_frame()
scan_dir = ScanDirection.LEFT if direction == 'left' else ScanDirection.RIGHT
alignment = self._detect_row_alignment(frame, scan_dir)
expected_x = int(self.state.current_x + self._cumulative_align_x)
expected_y = int(self.state.current_y + self._cumulative_align_y)
# Test for both directions
for direction in [ScanDirection.RIGHT, ScanDirection.LEFT]:
alignment = self._detect_strip_alignment(frame, direction, expected_x, expected_y)
self.log(f" {direction.value}: valid={alignment.valid}, X={alignment.x_offset:.1f}, Y={alignment.y_offset:.1f}, conf={alignment.confidence:.3f}")
# Return RIGHT direction result
alignment = self._detect_strip_alignment(frame, ScanDirection.RIGHT, expected_x, expected_y)
results['success'] = alignment.valid
results['x_offset'] = alignment.x_offset
results['y_offset'] = alignment.y_offset
self.log(f"Alignment result: valid={alignment.valid}, X={alignment.x_offset:.1f}, Y={alignment.y_offset:.1f}")
results['confidence'] = alignment.confidence
except Exception as e:
results['error'] = str(e)
@ -1118,6 +1221,13 @@ class StitchingScanner:
return results
def reset_alignment(self):
"""Reset cumulative alignment to zero."""
self._cumulative_align_x = 0.0
self._cumulative_align_y = 0.0
self._last_strip_alignment = AlignmentOffset()
self.log("Alignment reset to (0, 0)")
def get_memory_estimate(self) -> dict:
current_bytes = self.mosaic.nbytes if self.mosaic is not None else 0
max_bytes = self.config.max_mosaic_width * self.config.max_mosaic_height * 3