diff --git a/src/stitching_scanner.py b/src/stitching_scanner.py index 1d2719d..e37f2fc 100644 --- a/src/stitching_scanner.py +++ b/src/stitching_scanner.py @@ -154,179 +154,119 @@ class StitchingScanner: self.state.mosaic_init_height = h self.state.frame_count = 1 self.state.append_count = 0 + self.state.current_y = 0 + self.state.current_x = 0 self.log(f"Initialized mosaic: {frame.shape[1]}x{frame.shape[0]}") - def _blend_horizontal(self, base: np.ndarray, strip: np.ndarray, - blend_width: int, append_right: bool) -> np.ndarray: - self.log(f"=== _blend_horizontal ===") - self.log(f" base.shape: {base.shape}, strip.shape: {strip.shape}") - self.log(f" blend_width: {blend_width}, append_right: {append_right}") + def _blend_horizontal_at_y(self, base: np.ndarray, strip: np.ndarray, + blend_width: int, append_right: bool, + y_offset: int = 0) -> np.ndarray: + """Blend strip horizontally at a specific Y position in the mosaic.""" + h_base, w_base = base.shape[:2] + h_strip, w_strip = strip.shape[:2] + # Clamp y_offset to valid range + y_offset = max(0, min(y_offset, h_base - h_strip)) + + # Early exit: no blending possible + if blend_width <= 0 or blend_width >= w_strip: + if append_right: + # Create result with expanded width + result_width = w_base + w_strip + result = np.zeros((h_base, result_width, 3), dtype=np.uint8) + result[:, :w_base] = base + result[y_offset:y_offset + h_strip, w_base:] = strip + return result + else: + result_width = w_base + w_strip + result = np.zeros((h_base, result_width, 3), dtype=np.uint8) + result[y_offset:y_offset + h_strip, :w_strip] = strip + result[:, w_strip:] = base + return result + + blend_w = min(blend_width, w_strip, w_base) + + if append_right: + result_width = w_base + w_strip - blend_w + result = np.zeros((h_base, result_width, 3), dtype=np.uint8) + result[:, :w_base] = base + + # Extract overlaps at the correct Y position + alpha = np.linspace(1, 0, blend_w, dtype=np.float32)[np.newaxis, :, np.newaxis] + base_overlap = base[y_offset:y_offset + h_strip, -blend_w:].astype(np.float32) + strip_overlap = strip[:, :blend_w].astype(np.float32) + blended = (base_overlap * alpha + strip_overlap * (1 - alpha)).astype(np.uint8) + + # Place blended region and remainder at correct Y + result[y_offset:y_offset + h_strip, w_base - blend_w:w_base] = blended + result[y_offset:y_offset + h_strip, w_base:] = strip[:, blend_w:] + return result + + else: # append_left + result_width = w_base + w_strip - blend_w + result = np.zeros((h_base, result_width, 3), dtype=np.uint8) + + # Place strip at correct Y position + result[y_offset:y_offset + h_strip, :w_strip] = strip + + # Copy base (shifted right) + result[:, w_strip:] = base[:, blend_w:] + + # Extract overlaps at correct Y position + alpha = np.linspace(0, 1, blend_w, dtype=np.float32)[np.newaxis, :, np.newaxis] + strip_overlap = strip[:, -blend_w:].astype(np.float32) + base_overlap = base[y_offset:y_offset + h_strip, :blend_w].astype(np.float32) + blended = (strip_overlap * (1 - alpha) + base_overlap * alpha).astype(np.uint8) + + result[y_offset:y_offset + h_strip, w_strip - blend_w:w_strip] = blended + return result + + def _blend_horizontal(self, base: np.ndarray, strip: np.ndarray, + blend_width: int, append_right: bool) -> np.ndarray: if blend_width <= 0 or blend_width >= strip.shape[1]: - self.log(f" Early exit: blend_width out of range") if append_right: return np.hstack([base, strip]) return np.hstack([strip, base]) h_base, w_base = base.shape[:2] h_strip, w_strip = strip.shape[:2] - - # Check for channel dimension - c_base = base.shape[2] if len(base.shape) > 2 else 1 - c_strip = strip.shape[2] if len(strip.shape) > 2 else 1 - self.log(f" h_base: {h_base}, w_base: {w_base}, channels_base: {c_base}") - self.log(f" h_strip: {h_strip}, w_strip: {w_strip}, channels_strip: {c_strip}") + self.log(f"Base Width: {w_base}px") if h_strip != h_base: - self.log(f" WARNING: Height mismatch! h_base={h_base}, h_strip={h_strip}") if append_right: return np.hstack([base, strip]) return np.hstack([strip, base]) - if c_base != c_strip: - self.log(f" ERROR: Channel mismatch! c_base={c_base}, c_strip={c_strip}") - blend_w = min(blend_width, w_strip, w_base) - self.log(f" blend_w (clamped): {blend_w}") if append_right: result_width = w_base + w_strip - blend_w - self.log(f" result_width: {result_width} = {w_base} + {w_strip} - {blend_w}") - result = np.zeros((h_base, result_width, 3), dtype=np.uint8) - self.log(f" result.shape: {result.shape}") - - # Step 1: Copy base - self.log(f" Step 1: result[:, :w_base] = base -> result[:, :{w_base}]") - try: - result[:, :w_base] = base - except Exception as e: - self.log(f" ERROR Step 1: {e}") - raise - - # Step 2: Create alpha and overlaps - self.log(f" Step 2: Creating overlaps") - self.log(f" base_overlap = base[:, -{blend_w}:] -> base[:, {w_base - blend_w}:]") - self.log(f" strip_overlap = strip[:, :{blend_w}]") + result[:, :w_base] = base alpha = np.linspace(1, 0, blend_w, dtype=np.float32)[np.newaxis, :, np.newaxis] - self.log(f" alpha.shape: {alpha.shape}") + base_overlap = base[:, -blend_w:].astype(np.float32) + strip_overlap = strip[:, :blend_w].astype(np.float32) + blended = (base_overlap * alpha + strip_overlap * (1 - alpha)).astype(np.uint8) - try: - base_overlap = base[:, -blend_w:].astype(np.float32) - self.log(f" base_overlap.shape: {base_overlap.shape}") - except Exception as e: - self.log(f" ERROR base_overlap: {e}") - raise - - try: - strip_overlap = strip[:, :blend_w].astype(np.float32) - self.log(f" strip_overlap.shape: {strip_overlap.shape}") - except Exception as e: - self.log(f" ERROR strip_overlap: {e}") - raise - - # Step 3: Blend - self.log(f" Step 3: Blending") - try: - blended = (base_overlap * alpha + strip_overlap * (1 - alpha)).astype(np.uint8) - self.log(f" blended.shape: {blended.shape}") - except Exception as e: - self.log(f" ERROR blending: {e}") - raise - - # Step 4: Assign blended region - self.log(f" Step 4: result[:, {w_base - blend_w}:{w_base}] = blended") - try: - result[:, w_base - blend_w:w_base] = blended - except Exception as e: - self.log(f" ERROR Step 4: {e}") - self.log(f" Slice size: [:, {w_base - blend_w}:{w_base}] = {w_base - (w_base - blend_w)} cols") - self.log(f" blended.shape: {blended.shape}") - raise - - # Step 5: Assign remainder of strip - self.log(f" Step 5: result[:, {w_base}:] = strip[:, {blend_w}:]") - try: - remainder = strip[:, blend_w:] - self.log(f" remainder.shape: {remainder.shape}") - self.log(f" target slice width: {result_width - w_base}") - result[:, w_base:] = remainder - except Exception as e: - self.log(f" ERROR Step 5: {e}") - raise - - self.log(f" Success! Returning result.shape: {result.shape}") + result[:, w_base - blend_w:w_base] = blended + result[:, w_base:] = strip[:, blend_w:] return result - - else: # append_left + else: result_width = w_base + w_strip - blend_w - self.log(f" result_width: {result_width} = {w_base} + {w_strip} - {blend_w}") - result = np.zeros((h_base, result_width, 3), dtype=np.uint8) - self.log(f" result.shape: {result.shape}") - - # Step 1: Copy strip - self.log(f" Step 1: result[:, :w_strip] = strip -> result[:, :{w_strip}]") - try: - result[:, :w_strip] = strip - except Exception as e: - self.log(f" ERROR Step 1: {e}") - raise - - # Step 2: Create alpha and overlaps - self.log(f" Step 2: Creating overlaps") - self.log(f" strip_overlap = strip[:, -{blend_w}:] -> strip[:, {w_strip - blend_w}:]") - self.log(f" base_overlap = base[:, :{blend_w}]") + result[:, :w_strip] = strip alpha = np.linspace(0, 1, blend_w, dtype=np.float32)[np.newaxis, :, np.newaxis] - self.log(f" alpha.shape: {alpha.shape}") + strip_overlap = strip[:, -blend_w:].astype(np.float32) + base_overlap = base[:, :blend_w].astype(np.float32) + blended = (strip_overlap * (1 - alpha) + base_overlap * alpha).astype(np.uint8) - try: - strip_overlap = strip[:, -blend_w:].astype(np.float32) - self.log(f" strip_overlap.shape: {strip_overlap.shape}") - except Exception as e: - self.log(f" ERROR strip_overlap: {e}") - raise - - try: - base_overlap = base[:, :blend_w].astype(np.float32) - self.log(f" base_overlap.shape: {base_overlap.shape}") - except Exception as e: - self.log(f" ERROR base_overlap: {e}") - raise - - # Step 3: Blend - self.log(f" Step 3: Blending") - try: - blended = (strip_overlap * (1 - alpha) + base_overlap * alpha).astype(np.uint8) - self.log(f" blended.shape: {blended.shape}") - except Exception as e: - self.log(f" ERROR blending: {e}") - raise - - # Step 4: Assign blended region - self.log(f" Step 4: result[:, {w_strip - blend_w}:{w_strip}] = blended") - try: - result[:, w_strip - blend_w:w_strip] = blended - except Exception as e: - self.log(f" ERROR Step 4: {e}") - raise - - # Step 5: Assign remainder of base - self.log(f" Step 5: result[:, {w_strip}:] = base[:, {blend_w}:]") - try: - remainder = base[:, blend_w:] - self.log(f" remainder.shape: {remainder.shape}") - self.log(f" target slice width: {result_width - w_strip}") - result[:, w_strip:] = remainder - except Exception as e: - self.log(f" ERROR Step 5: {e}") - raise - - self.log(f" Success! Returning result.shape: {result.shape}") + result[:, w_strip - blend_w:w_strip] = blended + result[:, w_strip:] = base[:, blend_w:] return result + def _blend_vertical_at_x(self, base: np.ndarray, strip: np.ndarray, blend_height: int, append_below: bool, x_off: int = 0) -> np.ndarray: @@ -449,16 +389,19 @@ class StitchingScanner: pixels_consumed = append_width - SAFETY_MARGIN fractional_remainder = dx - pixels_consumed + # Calculate Y offset for current row + y_offset = int(self.state.current_y) + if direction == ScanDirection.RIGHT: strip_start = max(0, w - append_width - BLEND_WIDTH) new_strip = frame[:, strip_start:] - self.mosaic = self._blend_horizontal( - self.mosaic, new_strip, BLEND_WIDTH, append_right=True) + self.mosaic = self._blend_horizontal_at_y( + self.mosaic, new_strip, BLEND_WIDTH, append_right=True, y_offset=y_offset) else: strip_end = min(w, append_width + BLEND_WIDTH) new_strip = frame[:, :strip_end] - self.mosaic = self._blend_horizontal( - self.mosaic, new_strip, BLEND_WIDTH, append_right=False) + self.mosaic = self._blend_horizontal_at_y( + self.mosaic, new_strip, BLEND_WIDTH, append_right=False, y_offset=y_offset) self._displacement_since_append_x = fractional_remainder self._displacement_since_append_y = 0.0 @@ -777,6 +720,7 @@ class StitchingScanner: # Done when we've moved enough if abs(total_y) >= target_displacement: self.log(f"Row transition complete: {abs(total_y):.1f}px") + self.state.current_y = 0 self.motion.send_command('s') time.sleep(self.config.settle_time)