testing pos tracking

This commit is contained in:
2ManyProjects 2026-01-11 02:16:17 -06:00
parent 8ebe7a6e97
commit 34f84d760e

View file

@ -380,8 +380,8 @@ 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 = 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
@ -680,12 +680,12 @@ class StitchingScanner:
def _append_first_strip_of_row(self, frame: np.ndarray, direction: ScanDirection, alignment: AlignmentOffset): 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. Append the first strip of a new row with proper positioning.
This uses the alignment detected by _detect_row_start_alignment to properly For LEFT direction: Frame overlaps with bottom-right of mosaic
position the first strip, which sets the baseline for the entire row. For RIGHT direction: Frame overlaps with bottom-left of mosaic
""" """
BLEND_WIDTH = 10 BLEND_WIDTH = 20
with self._mosaic_lock: with self._mosaic_lock:
if self.mosaic is None: if self.mosaic is None:
@ -701,127 +701,83 @@ class StitchingScanner:
self._last_strip_alignment = alignment self._last_strip_alignment = alignment
self.log(f"Applied first-strip alignment: X={alignment.x_offset:.1f}, Y={alignment.y_offset:.1f}") self.log(f"Applied first-strip alignment: X={alignment.x_offset:.1f}, Y={alignment.y_offset:.1f}")
align_x = self._cumulative_align_x # Calculate Y position - frame overlaps with bottom of mosaic
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) row_overlap_pixels = int(fh * self.config.row_overlap)
y_offset = mh - row_overlap_pixels y_offset = mh - row_overlap_pixels + int(round(self._cumulative_align_y))
# Apply Y alignment correction
y_offset = y_offset + int(round(align_y))
y_offset = max(0, y_offset) y_offset = max(0, y_offset)
if direction == ScanDirection.LEFT: if direction == ScanDirection.LEFT:
# Starting at right edge - place frame aligned with right side of mosaic # Frame's RIGHT edge aligns with mosaic's RIGHT edge
# The frame's right edge should align with mosaic's right edge (with X correction) x_offset = mw - fw + int(round(self._cumulative_align_x))
x_offset = mw - fw + int(round(align_x)) x_offset = max(0, x_offset)
x_offset = max(0, min(x_offset, mw - BLEND_WIDTH))
self.log(f"=== First Strip of Row (LEFT) ===") self.log(f"=== First Strip of Row (LEFT) ===")
self.log(f" Mosaic: {mw}x{mh}, Frame: {fw}x{fh}") self.log(f" Mosaic: {mw}x{mh}, Frame: {fw}x{fh}")
self.log(f" Placement: X={x_offset}, Y={y_offset}") self.log(f" X offset: {x_offset} (frame right edge at {x_offset + fw})")
self.log(f" Alignment: X={align_x:.1f}, Y={align_y:.1f}") self.log(f" Y offset: {y_offset}")
# Blend this frame into the mosaic at the calculated position else: # RIGHT
# We're placing within existing bounds, so just blend in place # Frame's LEFT edge aligns with mosaic's LEFT edge
x_offset = int(round(self._cumulative_align_x))
# 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) x_offset = max(0, x_offset)
self.log(f"=== First Strip of Row (RIGHT) ===") self.log(f"=== First Strip of Row (RIGHT) ===")
self.log(f" Mosaic: {mw}x{mh}, Frame: {fw}x{fh}") self.log(f" Mosaic: {mw}x{mh}, Frame: {fw}x{fh}")
self.log(f" Placement: X={x_offset}, Y={y_offset}") self.log(f" X offset: {x_offset}")
self.log(f" Alignment: X={align_x:.1f}, Y={align_y:.1f}") self.log(f" Y offset: {y_offset}")
# Similar blending logic for right direction # Blend frame into mosaic at calculated position
result = self.mosaic.copy() result = self.mosaic.copy()
blend_region_width = min(fw, mw - x_offset)
frame_to_place = frame[:, :blend_region_width]
# Vertical blend at top edge # Calculate the region to blend
v_blend_h = min(row_overlap_pixels, BLEND_WIDTH * 2) x_end = min(x_offset + fw, mw)
if v_blend_h > 0 and y_offset > 0: y_end = min(y_offset + fh, mh)
blend_y_start = y_offset region_w = x_end - x_offset
blend_y_end = min(y_offset + v_blend_h, mh) region_h = y_end - y_offset
actual_blend_h = blend_y_end - blend_y_start
if actual_blend_h > 0: if region_w <= 0 or region_h <= 0:
mosaic_overlap = result[blend_y_start:blend_y_end, x_offset:x_offset + blend_region_width].astype(np.float32) self.log(f" WARNING: No valid region to blend")
frame_overlap = frame_to_place[:actual_blend_h, :].astype(np.float32) return
alpha_v_actual = np.linspace(0, 1, actual_blend_h, dtype=np.float32)[:, np.newaxis, np.newaxis] # Create alpha mask for blending
# Blend at top edge (with row above) and appropriate side edge
alpha = np.ones((region_h, region_w), dtype=np.float32)
min_w_blend = min(mosaic_overlap.shape[1], frame_overlap.shape[1]) # Vertical blend at top (first ~20% of overlap region)
mosaic_overlap = mosaic_overlap[:, :min_w_blend] v_blend = min(row_overlap_pixels, int(region_h * 0.3))
frame_overlap = frame_overlap[:, :min_w_blend] if v_blend > 0:
v_gradient = np.linspace(0, 1, v_blend, dtype=np.float32)[:, np.newaxis]
alpha[:v_blend, :] = v_gradient
blended = (mosaic_overlap * (1 - alpha_v_actual) + frame_overlap * alpha_v_actual).astype(np.uint8) # Horizontal blend at edge
result[blend_y_start:blend_y_end, x_offset:x_offset + min_w_blend] = blended h_blend = min(BLEND_WIDTH, int(region_w * 0.2))
if h_blend > 0:
if direction == ScanDirection.LEFT:
# Blend on right edge (where we came from)
h_gradient = np.linspace(1, 0, h_blend, dtype=np.float32)[np.newaxis, :]
alpha[:, -h_blend:] = np.minimum(alpha[:, -h_blend:], h_gradient)
else:
# Blend on left edge
h_gradient = np.linspace(0, 1, h_blend, dtype=np.float32)[np.newaxis, :]
alpha[:, :h_blend] = np.minimum(alpha[:, :h_blend], h_gradient)
if y_offset + v_blend_h < mh: # Apply blending
remaining_h = min(fh - v_blend_h, mh - (y_offset + v_blend_h)) alpha_3ch = alpha[:, :, np.newaxis]
if remaining_h > 0: mosaic_region = result[y_offset:y_end, x_offset:x_end].astype(np.float32)
result[y_offset + v_blend_h:y_offset + v_blend_h + remaining_h, frame_region = frame[:region_h, :region_w].astype(np.float32)
x_offset:x_offset + min_w_blend] = frame_to_place[v_blend_h:v_blend_h + remaining_h, :min_w_blend]
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 self.mosaic = result
# Update current position for subsequent strips # Update position tracking for subsequent strips
with self._state_lock: with self._state_lock:
if direction == ScanDirection.LEFT: self.state.current_x = x_offset # Track where we are in the mosaic
self.state.current_x = x_offset
else:
self.state.current_x = x_offset + blend_region_width
self.state.current_y = y_offset self.state.current_y = y_offset
self.state.append_count += 1 self.state.append_count += 1
self.log(f" First strip placed. Position: ({self.state.current_x}, {self.state.current_y})") self.log(f" First strip blended at ({x_offset}, {y_offset}), size {region_w}x{region_h}")
if self.on_mosaic_updated: if self.on_mosaic_updated:
self.on_mosaic_updated() self.on_mosaic_updated()
@ -1057,7 +1013,7 @@ class StitchingScanner:
frame = self._capture_frame() frame = self._capture_frame()
h, w = frame.shape[:2] h, w = frame.shape[:2]
total_x = 0 total_x = 0.0 # Track total movement in this direction
# Setup based on direction # Setup based on direction
if direction in [ScanDirection.RIGHT, ScanDirection.LEFT]: if direction in [ScanDirection.RIGHT, ScanDirection.LEFT]:
@ -1073,6 +1029,14 @@ class StitchingScanner:
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'
# For LEFT direction, we need to track how far we've traveled
# We stop when we've traveled approximately the mosaic width
if direction == ScanDirection.LEFT:
# Calculate target: we need to travel back across the mosaic
# Starting from right edge, ending at left edge
target_travel = self.state.mosaic_width - w # Approximate distance to travel
self.log(f"LEFT scan: target travel distance = {target_travel:.0f}px")
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
@ -1081,28 +1045,30 @@ 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
if current_dim() >= max_dim and direction == ScanDirection.RIGHT: # Check exit conditions based on direction
if direction == ScanDirection.RIGHT:
if current_dim() >= max_dim:
self.log(f"Max dimension reached ({current_dim()}px)") self.log(f"Max dimension reached ({current_dim()}px)")
stop_reason = 'max_dim' stop_reason = 'max_dim'
break break
if abs(self.state.current_x) >= max_dim:
if self.state.current_x >= 0 and direction == ScanDirection.LEFT: self.log(f"Current X reached max ({self.state.current_x}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' stop_reason = 'max_dim'
break break
if abs(self.state.current_x) >= self.config.max_mosaic_width and direction == ScanDirection.RIGHT: elif direction == ScanDirection.LEFT:
self.log(f"Max dimension reached ({self.config.max_mosaic_width}px)") # Check if we've traveled far enough (back to left edge)
self.log(f"Current X offset ({self.state.current_x}px)") # total_x will be negative for leftward movement
stop_reason = 'max_dim' if abs(total_x) >= target_travel:
self.log(f"Returned to left edge: traveled {abs(total_x):.0f}px of {target_travel:.0f}px")
stop_reason = 'complete'
break break
# Pulse motor # Pulse motor
@ -1115,23 +1081,20 @@ 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)
self.log(f"Scanning dx{dx} dy{dy}..")
self._displacement_since_append_x += dx self._displacement_since_append_x += dx
self._displacement_since_append_y += dy self._displacement_since_append_y += dy
total_x += dx total_x += dx
with self._state_lock:
self.state.current_x += dx
with self._state_lock: with self._state_lock:
self.state.current_x += dx
self.state.cumulative_x = self._displacement_since_append_x self.state.cumulative_x = self._displacement_since_append_x
self.state.cumulative_y = self._displacement_since_append_y self.state.cumulative_y = self._displacement_since_append_y
self.state.last_displacement = (dx, dy) self.state.last_displacement = (dx, dy)
self.state.frame_count += 1 self.state.frame_count += 1
# Edge detection # Edge detection (no movement)
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:
@ -1141,14 +1104,12 @@ class StitchingScanner:
else: else:
no_movement_count = 0 no_movement_count = 0
# Append when threshold reached (with continuous alignment) # Append when threshold reached
disp = abs(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 {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.log(f"Appended at total_x={total_x:.1f}, mosaic: {self.state.mosaic_width}x{self.state.mosaic_height}")
self._prev_frame = curr_frame.copy() self._prev_frame = curr_frame.copy()
@ -1157,7 +1118,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}") self.log(f"Direction finished: {stop_reason}, total movement: {total_x:.1f}px")
return stop_reason return stop_reason
def _move_to_next_row(self) -> bool: def _move_to_next_row(self) -> bool: