Scanning + frame interpolation

This commit is contained in:
Shaiv Kamat 2026-01-03 00:32:05 -08:00
parent 2acfccf6e1
commit 078274ddbd
4 changed files with 1146 additions and 592 deletions

BIN
Mos1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

BIN
Mos2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View file

@ -1,7 +1,8 @@
""" """
AutoScope GUI - Vertical Monitor Layout with Feature Visualization AutoScope GUI - Vertical Monitor Layout with Edge Comparison Overlay
Enhanced with feature overlay debugging for scanner development. Shows reference and current binary edge images overlaid directly on camera view.
Updated with interpolation controls and improved scanner UI.
""" """
import tkinter as tk import tkinter as tk
@ -97,30 +98,65 @@ class AppGUI:
self.camera_label = ttk.Label(camera_frame) self.camera_label = ttk.Label(camera_frame)
self.camera_label.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.camera_label.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Camera options row # Camera options row 1: overlays
cam_opts = ttk.Frame(camera_frame) cam_opts1 = ttk.Frame(camera_frame)
cam_opts.pack(fill=tk.X, padx=5, pady=(0, 5)) cam_opts1.pack(fill=tk.X, padx=5, pady=(0, 2))
self.show_tile_overlay_var = tk.BooleanVar(value=True) self.show_tile_overlay_var = tk.BooleanVar(value=True)
ttk.Checkbutton(cam_opts, text="Tile bounds", ttk.Checkbutton(cam_opts1, text="Tile bounds",
variable=self.show_tile_overlay_var).pack(side=tk.LEFT) variable=self.show_tile_overlay_var).pack(side=tk.LEFT)
self.show_edge_regions_var = tk.BooleanVar(value=False) self.show_edge_regions_var = tk.BooleanVar(value=False)
ttk.Checkbutton(cam_opts, text="Track regions", ttk.Checkbutton(cam_opts1, text="Edge regions",
variable=self.show_edge_regions_var).pack(side=tk.LEFT, padx=(10, 0)) variable=self.show_edge_regions_var).pack(side=tk.LEFT, padx=(10, 0))
# NEW: Feature overlay checkbox self.show_comparison_var = tk.BooleanVar(value=True)
self.show_features_var = tk.BooleanVar(value=False) ttk.Checkbutton(cam_opts1, text="Comparison overlay",
ttk.Checkbutton(cam_opts, text="Features", variable=self.show_comparison_var).pack(side=tk.LEFT, padx=(10, 0))
variable=self.show_features_var).pack(side=tk.LEFT, padx=(10, 0))
self.live_focus_var = tk.BooleanVar(value=True) self.live_focus_var = tk.BooleanVar(value=True)
ttk.Checkbutton(cam_opts, text="Live focus", ttk.Checkbutton(cam_opts1, text="Live focus",
variable=self.live_focus_var).pack(side=tk.LEFT, padx=(10, 0)) variable=self.live_focus_var).pack(side=tk.LEFT, padx=(10, 0))
self.focus_score_label = ttk.Label(cam_opts, text="Focus: --", font=('Arial', 11, 'bold')) self.focus_score_label = ttk.Label(cam_opts1, text="Focus: --", font=('Arial', 11, 'bold'))
self.focus_score_label.pack(side=tk.RIGHT) self.focus_score_label.pack(side=tk.RIGHT)
# Camera options row 2: threshold and similarity
cam_opts2 = ttk.Frame(camera_frame)
cam_opts2.pack(fill=tk.X, padx=5, pady=(0, 5))
ttk.Label(cam_opts2, text="Match threshold:").pack(side=tk.LEFT)
self.threshold_var = tk.DoubleVar(value=0.12)
self.threshold_slider = ttk.Scale(
cam_opts2,
from_=0.05,
to=0.8,
orient=tk.HORIZONTAL,
variable=self.threshold_var,
command=self._on_threshold_change,
length=100
)
self.threshold_slider.pack(side=tk.LEFT, padx=5)
self.threshold_label = ttk.Label(cam_opts2, text="0.12", width=5)
self.threshold_label.pack(side=tk.LEFT)
ttk.Button(cam_opts2, text="Test Edges", width=10,
command=self._test_edge_comparison).pack(side=tk.LEFT, padx=(10, 0))
ttk.Button(cam_opts2, text="Test Interp", width=10,
command=self._test_interpolation).pack(side=tk.LEFT, padx=(5, 0))
# Similarity display
ttk.Separator(cam_opts2, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=10)
ttk.Label(cam_opts2, text="Sim:").pack(side=tk.LEFT)
self.similarity_label = ttk.Label(cam_opts2, text="--", font=('Arial', 10, 'bold'), width=6)
self.similarity_label.pack(side=tk.LEFT)
self.match_status_label = ttk.Label(cam_opts2, text="", width=10)
self.match_status_label.pack(side=tk.LEFT, padx=(3, 0))
# === BOTTOM: Control Panel === # === BOTTOM: Control Panel ===
control_frame = ttk.Frame(main_frame) control_frame = ttk.Frame(main_frame)
control_frame.pack(fill=tk.X) control_frame.pack(fill=tk.X)
@ -128,14 +164,17 @@ class AppGUI:
# Row 1: Emergency Stop + Scanner Controls # Row 1: Emergency Stop + Scanner Controls
self._build_row1_emergency_scanner(control_frame) self._build_row1_emergency_scanner(control_frame)
# Row 2: Movement Controls # Row 2: Interpolation Settings (NEW)
self._build_row2_movement(control_frame) self._build_row2_interpolation(control_frame)
# Row 3: Speed + Autofocus # Row 3: Movement Controls
self._build_row3_speed_autofocus(control_frame) self._build_row3_movement(control_frame)
# Row 4: Status + Log # Row 4: Speed + Autofocus
self._build_row4_status_log(control_frame) self._build_row4_speed_autofocus(control_frame)
# Row 5: Status + Log
self._build_row5_status_log(control_frame)
def _build_row1_emergency_scanner(self, parent): def _build_row1_emergency_scanner(self, parent):
"""Row 1: Emergency stop and scanner controls""" """Row 1: Emergency stop and scanner controls"""
@ -145,10 +184,10 @@ class AppGUI:
# Emergency stop button # Emergency stop button
self.emergency_btn = tk.Button( self.emergency_btn = tk.Button(
row, text="⚠ STOP", command=self._emergency_stop, row, text="⚠ STOP", command=self._emergency_stop,
bg='red', fg='white', font=('Arial', 12, 'bold'), bg='#d32f2f', fg='white', font=('Arial', 11, 'bold'),
width=8, height=1 width=7, height=1, relief=tk.RAISED, bd=2
) )
self.emergency_btn.pack(side=tk.LEFT, padx=(0, 10)) self.emergency_btn.pack(side=tk.LEFT, padx=(0, 8))
# Scanner controls # Scanner controls
scanner_frame = ttk.LabelFrame(row, text="Scanner") scanner_frame = ttk.LabelFrame(row, text="Scanner")
@ -157,50 +196,141 @@ class AppGUI:
sf = ttk.Frame(scanner_frame) sf = ttk.Frame(scanner_frame)
sf.pack(fill=tk.X, padx=5, pady=3) sf.pack(fill=tk.X, padx=5, pady=3)
# Status + Progress # Status indicator
self.scan_status_label = ttk.Label(sf, text="Idle", font=('Arial', 9, 'bold'), width=8) self.scan_status_label = ttk.Label(sf, text="Idle", font=('Arial', 9, 'bold'), width=8)
self.scan_status_label.pack(side=tk.LEFT) self.scan_status_label.pack(side=tk.LEFT)
self.scan_progress_bar = ttk.Progressbar(sf, mode='indeterminate', length=80) # Progress
self.scan_progress_bar.pack(side=tk.LEFT, padx=5) self.scan_progress_bar = ttk.Progressbar(sf, mode='indeterminate', length=60)
self.scan_progress_bar.pack(side=tk.LEFT, padx=3)
self.scan_progress_label = ttk.Label(sf, text="", width=10) self.scan_progress_label = ttk.Label(sf, text="0 tiles", width=8)
self.scan_progress_label.pack(side=tk.LEFT) self.scan_progress_label.pack(side=tk.LEFT)
# Buttons # Separator
ttk.Separator(sf, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5)
# Control buttons with proper styling
btn_frame = ttk.Frame(sf)
btn_frame.pack(side=tk.LEFT)
self.scan_start_btn = tk.Button( self.scan_start_btn = tk.Button(
sf, text="▶ Start", width=7, bg='green', fg='white', btn_frame, text="▶ Start", width=7,
font=('Arial', 9, 'bold'), command=self._start_scan bg='#4CAF50', fg='white', font=('Arial', 9, 'bold'),
activebackground='#45a049', relief=tk.RAISED, bd=2,
command=self._start_scan
) )
self.scan_start_btn.pack(side=tk.LEFT, padx=2) self.scan_start_btn.pack(side=tk.LEFT, padx=2)
self.scan_pause_btn = ttk.Button(sf, text="", width=3, self.scan_pause_btn = tk.Button(
command=self._pause_scan, state='disabled') btn_frame, text="", width=3,
self.scan_pause_btn.pack(side=tk.LEFT, padx=1) bg='#2196F3', fg='white', font=('Arial', 10, 'bold'),
activebackground='#1976D2', relief=tk.RAISED, bd=2,
command=self._pause_scan, state='disabled'
)
self.scan_pause_btn.pack(side=tk.LEFT, padx=2)
self.scan_stop_btn = tk.Button( self.scan_stop_btn = tk.Button(
sf, text="", width=3, bg='orange', fg='white', btn_frame, text="", width=3,
bg='#FF9800', fg='white', font=('Arial', 10, 'bold'),
activebackground='#F57C00', relief=tk.RAISED, bd=2,
command=self._stop_scan, state='disabled' command=self._stop_scan, state='disabled'
) )
self.scan_stop_btn.pack(side=tk.LEFT, padx=1) self.scan_stop_btn.pack(side=tk.LEFT, padx=2)
ttk.Button(sf, text="📷", width=3, # Capture single tile
command=self._capture_single_tile).pack(side=tk.LEFT, padx=1) ttk.Button(btn_frame, text="📷", width=3,
command=self._capture_single_tile).pack(side=tk.LEFT, padx=2)
# Mosaic button
ttk.Button(sf, text="Mosaic", width=6, ttk.Button(sf, text="Mosaic", width=6,
command=self._show_mosaic_window).pack(side=tk.LEFT, padx=(5, 2)) command=self._show_mosaic_window).pack(side=tk.RIGHT, padx=2)
# AF interval def _build_row2_interpolation(self, parent):
ttk.Label(sf, text="AF:").pack(side=tk.LEFT, padx=(10, 2)) """Row 2: Interpolation and peak detection settings"""
self.af_every_var = tk.StringVar(value="5") row = ttk.LabelFrame(parent, text="Matching Settings")
ttk.Spinbox(sf, from_=1, to=20, width=3, row.pack(fill=tk.X, pady=(0, 3))
textvariable=self.af_every_var).pack(side=tk.LEFT)
self.af_every_row_var = tk.BooleanVar(value=True) inner = ttk.Frame(row)
ttk.Checkbutton(sf, text="Row", variable=self.af_every_row_var).pack(side=tk.LEFT) inner.pack(fill=tk.X, padx=5, pady=3)
def _build_row2_movement(self, parent): # Interpolation toggle
"""Row 2: Movement controls for all axes""" self.use_interpolation_var = tk.BooleanVar(value=True)
ttk.Checkbutton(
inner, text="Interpolation",
variable=self.use_interpolation_var,
command=self._update_scan_config
).pack(side=tk.LEFT)
# Number of interpolations
ttk.Label(inner, text="Steps:").pack(side=tk.LEFT, padx=(10, 2))
self.num_interp_var = tk.IntVar(value=10)
self.num_interp_spinbox = ttk.Spinbox(
inner, from_=5, to=30, width=4,
textvariable=self.num_interp_var,
command=self._update_scan_config
)
self.num_interp_spinbox.pack(side=tk.LEFT)
# Separator
ttk.Separator(inner, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=10)
# Peak detection toggle
self.use_peak_detection_var = tk.BooleanVar(value=True)
ttk.Checkbutton(
inner, text="Peak Detection",
variable=self.use_peak_detection_var,
command=self._update_scan_config
).pack(side=tk.LEFT)
# Peak window size
ttk.Label(inner, text="Window:").pack(side=tk.LEFT, padx=(10, 2))
self.peak_window_var = tk.IntVar(value=5)
self.peak_window_spinbox = ttk.Spinbox(
inner, from_=3, to=15, width=3,
textvariable=self.peak_window_var,
command=self._update_scan_config
)
self.peak_window_spinbox.pack(side=tk.LEFT)
# Peak drop threshold
ttk.Label(inner, text="Drop:").pack(side=tk.LEFT, padx=(10, 2))
self.peak_drop_var = tk.DoubleVar(value=0.02)
self.peak_drop_spinbox = ttk.Spinbox(
inner, from_=0.01, to=0.1, increment=0.01, width=5,
textvariable=self.peak_drop_var,
command=self._update_scan_config
)
self.peak_drop_spinbox.pack(side=tk.LEFT)
# Separator
ttk.Separator(inner, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=10)
# Comparison method dropdown
ttk.Label(inner, text="Method:").pack(side=tk.LEFT, padx=(0, 2))
self.comparison_method_var = tk.StringVar(value='template')
method_combo = ttk.Combobox(
inner, textvariable=self.comparison_method_var,
values=['template', 'ssim', 'mse', 'hst', 'phase'],
width=8, state='readonly'
)
method_combo.pack(side=tk.LEFT)
method_combo.bind('<<ComboboxSelected>>', lambda e: self._update_scan_config())
# Status indicators on right
status_frame = ttk.Frame(inner)
status_frame.pack(side=tk.RIGHT)
ttk.Label(status_frame, text="Offset:").pack(side=tk.LEFT)
self.offset_label = ttk.Label(status_frame, text="--", width=5, font=('Arial', 9))
self.offset_label.pack(side=tk.LEFT)
ttk.Label(status_frame, text="Peak:").pack(side=tk.LEFT, padx=(8, 0))
self.peak_label = ttk.Label(status_frame, text="--", width=5, font=('Arial', 9))
self.peak_label.pack(side=tk.LEFT)
def _build_row3_movement(self, parent):
"""Row 3: Movement controls for all axes"""
row = ttk.LabelFrame(parent, text="Movement") row = ttk.LabelFrame(parent, text="Movement")
row.pack(fill=tk.X, pady=(0, 3)) row.pack(fill=tk.X, pady=(0, 3))
@ -212,7 +342,7 @@ class AppGUI:
for axis in ["X", "Y", "Z"]: for axis in ["X", "Y", "Z"]:
af = ttk.Frame(inner) af = ttk.Frame(inner)
af.pack(side=tk.LEFT, padx=(0, 20)) af.pack(side=tk.LEFT, padx=(0, 15))
ttk.Label(af, text=f"{axis}:", font=('Arial', 10, 'bold')).pack(side=tk.LEFT) ttk.Label(af, text=f"{axis}:", font=('Arial', 10, 'bold')).pack(side=tk.LEFT)
@ -221,25 +351,28 @@ class AppGUI:
dir_btn.pack(side=tk.LEFT, padx=2) dir_btn.pack(side=tk.LEFT, padx=2)
self.dir_labels[axis] = dir_btn self.dir_labels[axis] = dir_btn
move_btn = tk.Button(af, text="Move", width=6, bg='#4CAF50', fg='white', move_btn = tk.Button(
command=lambda a=axis: self._toggle_movement(a)) af, text="Move", width=6,
bg='#4CAF50', fg='white', font=('Arial', 9),
activebackground='#45a049', relief=tk.RAISED,
command=lambda a=axis: self._toggle_movement(a)
)
move_btn.pack(side=tk.LEFT, padx=2) move_btn.pack(side=tk.LEFT, padx=2)
self.move_buttons[axis] = move_btn self.move_buttons[axis] = move_btn
ttk.Button(af, text="", width=2, ttk.Button(af, text="", width=2,
command=lambda a=axis: self._stop_axis(a)).pack(side=tk.LEFT) command=lambda a=axis: self._stop_axis(a)).pack(side=tk.LEFT)
# Stop all # Stop all button
tk.Button(inner, text="STOP ALL", width=10, bg='#f44336', fg='white', tk.Button(
font=('Arial', 9, 'bold'), inner, text="STOP ALL", width=10,
command=self.motion.stop_all).pack(side=tk.RIGHT) bg='#f44336', fg='white', font=('Arial', 9, 'bold'),
activebackground='#d32f2f', relief=tk.RAISED, bd=2,
command=self.motion.stop_all
).pack(side=tk.RIGHT)
# Test feature detection button def _build_row4_speed_autofocus(self, parent):
ttk.Button(inner, text="Test Features", width=11, """Row 4: Speed and Autofocus controls"""
command=self._test_feature_detection).pack(side=tk.RIGHT, padx=(0, 10))
def _build_row3_speed_autofocus(self, parent):
"""Row 3: Speed and Autofocus controls"""
row = ttk.Frame(parent) row = ttk.Frame(parent)
row.pack(fill=tk.X, pady=(0, 3)) row.pack(fill=tk.X, pady=(0, 3))
@ -257,12 +390,13 @@ class AppGUI:
ttk.Separator(sf, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5) ttk.Separator(sf, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5)
self.fine_speed_label = ttk.Label(sf, text="50", width=3) self.fine_speed_label = ttk.Label(sf, text="1", width=3)
self.fine_speed_label.pack(side=tk.LEFT) self.fine_speed_label.pack(side=tk.LEFT)
self.speed_slider = ttk.Scale(sf, from_=0, to=6, orient=tk.HORIZONTAL, length=80) self.speed_slider = ttk.Scale(sf, from_=0, to=6, orient=tk.HORIZONTAL, length=80)
self.speed_slider.set(5) self.speed_slider.set(1)
self.speed_slider.config(command=self._on_speed_slider_change) self.speed_slider.config(command=self._on_speed_slider_change)
self._on_speed_slider_change(1)
self.speed_slider.pack(side=tk.LEFT, padx=3) self.speed_slider.pack(side=tk.LEFT, padx=3)
# Autofocus controls # Autofocus controls
@ -282,8 +416,8 @@ class AppGUI:
ttk.Button(af, text="Stop", width=5, ttk.Button(af, text="Stop", width=5,
command=self._stop_autofocus).pack(side=tk.LEFT, padx=2) command=self._stop_autofocus).pack(side=tk.LEFT, padx=2)
def _build_row4_status_log(self, parent): def _build_row5_status_log(self, parent):
"""Row 4: Status and Log""" """Row 5: Status and Log"""
row = ttk.Frame(parent) row = ttk.Frame(parent)
row.pack(fill=tk.X, pady=(0, 3)) row.pack(fill=tk.X, pady=(0, 3))
@ -317,35 +451,320 @@ class AppGUI:
command=self._send_custom_command).pack(side=tk.RIGHT, padx=(5, 0)) command=self._send_custom_command).pack(side=tk.RIGHT, padx=(5, 0))
# ========================================================================= # =========================================================================
# Feature Detection Testing # Comparison Overlay Drawing
# ========================================================================= # =========================================================================
def _test_feature_detection(self): def _draw_comparison_overlay(self, frame: np.ndarray) -> np.ndarray:
"""Test and log feature detection on current frame""" """
Draw edge comparison overlay directly on camera frame.
Shows reference, current, and interpolated binary images.
"""
if not self.scanner or not self.show_comparison_var.get():
return frame
state = self.scanner.get_comparison_state()
# Show overlay if tracking OR if paused with valid reference image
if not state.is_tracking and not (self.scanner.paused and state.reference_image is not None):
return frame
h, w = frame.shape[:2]
border_pct = self.scan_config.border_percentage
# Map raw edges to display positions after 90° CW rotation
edge_to_display = {
'top': 'right',
'bottom': 'left',
'left': 'top',
'right': 'bottom'
}
ref_display_edge = edge_to_display.get(state.reference_edge, '')
target_display_edge = edge_to_display.get(state.target_edge, '')
# Draw reference image (cyan)
if state.reference_image is not None:
frame = self._draw_binary_at_edge(
frame, state.reference_image, ref_display_edge,
border_pct, color=(0, 255, 255), label="REF"
)
# Draw current/interpolated image (yellow)
# Prefer interpolated if available, otherwise current
display_image = state.interpolated_image if state.interpolated_image is not None else state.current_image
if display_image is not None:
label = "INT" if state.interpolated_image is not None else "CUR"
frame = self._draw_binary_at_edge(
frame, display_image, target_display_edge,
border_pct, color=(255, 255, 0), label=label
)
# Draw status box with extended info
frame = self._draw_comparison_status_box(frame, state)
# Draw similarity history graph if available
if state.similarity_history and len(state.similarity_history) > 1:
frame = self._draw_similarity_graph(frame, state.similarity_history, state.threshold)
return frame
def _draw_binary_at_edge(self, frame: np.ndarray, binary_img: np.ndarray,
display_edge: str, border_pct: float,
color: tuple, label: str) -> np.ndarray:
"""Draw a binary image overlaid at the specified display edge."""
h, w = frame.shape[:2]
# Convert binary to BGR and apply color tint
binary_bgr = cv2.cvtColor(binary_img, cv2.COLOR_GRAY2BGR)
mask = binary_img > 127
binary_bgr[mask] = color
# Rotate binary to match 90° CW display rotation
binary_bgr = cv2.rotate(binary_bgr, cv2.ROTATE_90_CLOCKWISE)
alpha = 0.7
if display_edge == 'left':
border_w = int(w * border_pct)
resized = cv2.resize(binary_bgr, (border_w, h))
frame[:, :border_w] = cv2.addWeighted(
frame[:, :border_w], 1 - alpha, resized, alpha, 0
)
cv2.putText(frame, label, (5, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
elif display_edge == 'right':
border_w = int(w * border_pct)
resized = cv2.resize(binary_bgr, (border_w, h))
frame[:, w-border_w:] = cv2.addWeighted(
frame[:, w-border_w:], 1 - alpha, resized, alpha, 0
)
cv2.putText(frame, label, (w - border_w + 5, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
elif display_edge == 'top':
border_h = int(h * border_pct)
resized = cv2.resize(binary_bgr, (w, border_h))
frame[:border_h, :] = cv2.addWeighted(
frame[:border_h, :], 1 - alpha, resized, alpha, 0
)
cv2.putText(frame, label, (5, border_h - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
elif display_edge == 'bottom':
border_h = int(h * border_pct)
resized = cv2.resize(binary_bgr, (w, border_h))
frame[h-border_h:, :] = cv2.addWeighted(
frame[h-border_h:, :], 1 - alpha, resized, alpha, 0
)
cv2.putText(frame, label, (5, h - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
return frame
def _draw_comparison_status_box(self, frame: np.ndarray, state) -> np.ndarray:
"""Draw a status box showing similarity, offset, and match status"""
h, w = frame.shape[:2]
# Position: top center
box_w, box_h = 240, 90
box_x = (w - box_w) // 2
box_y = 10
# Semi-transparent background
overlay = frame.copy()
cv2.rectangle(overlay, (box_x, box_y), (box_x + box_w, box_y + box_h), (0, 0, 0), -1)
frame = cv2.addWeighted(overlay, 0.6, frame, 0.4, 0)
# Text positioning
tx = box_x + 10
ty = box_y + 18
line_h = 18
# Direction info
cv2.putText(frame, f"Dir: {state.direction}", (tx, ty),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1)
ty += line_h
# Similarity with color coding
sim = state.similarity
threshold = state.threshold
is_paused = self.scanner and self.scanner.paused
if state.shift_detected or sim >= threshold:
sim_color = (0, 255, 0) # Green
elif is_paused:
sim_color = (255, 200, 0) # Cyan
else:
sim_color = (0, 165, 255) # Orange
cv2.putText(frame, f"Similarity: {sim:.3f}", (tx, ty),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, sim_color, 1)
# Threshold on same line
cv2.putText(frame, f"(thr: {threshold:.2f})", (tx + 120, ty),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (150, 150, 150), 1)
ty += line_h
# Offset info (from interpolation)
offset_text = f"Offset: {state.best_offset:.2f}" if state.best_offset > 0 else "Offset: --"
cv2.putText(frame, offset_text, (tx, ty),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (200, 200, 200), 1)
ty += line_h
# Status
if state.shift_detected:
if state.peak_detected:
status = "PEAK MATCHED!"
else:
status = "MATCHED!"
status_color = (0, 255, 0)
elif sim >= threshold:
status = "ABOVE THRESHOLD"
status_color = (0, 255, 0)
elif is_paused:
status = "PAUSED"
status_color = (255, 200, 0)
else:
status = "Searching..."
status_color = (200, 200, 200)
cv2.putText(frame, status, (tx, ty),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, status_color, 1)
return frame
def _draw_similarity_graph(self, frame: np.ndarray, history: list, threshold: float) -> np.ndarray:
"""Draw a small similarity history graph in the corner"""
h, w = frame.shape[:2]
# Graph dimensions and position (bottom right)
graph_w, graph_h = 150, 60
margin = 10
gx = w - graph_w - margin
gy = h - graph_h - margin - 50 # Above the bottom edge region
# Semi-transparent background
overlay = frame.copy()
cv2.rectangle(overlay, (gx, gy), (gx + graph_w, gy + graph_h), (0, 0, 0), -1)
frame = cv2.addWeighted(overlay, 0.5, frame, 0.5, 0)
# Draw threshold line
thr_y = int(gy + graph_h - (threshold * graph_h))
cv2.line(frame, (gx, thr_y), (gx + graph_w, thr_y), (0, 100, 255), 1)
# Draw history line
if len(history) > 1:
# Take last N points that fit in the graph
max_points = graph_w // 2
plot_history = history[-max_points:] if len(history) > max_points else history
points = []
for i, val in enumerate(plot_history):
px = gx + int((i / max(len(plot_history) - 1, 1)) * (graph_w - 1))
py = int(gy + graph_h - (min(val, 1.0) * graph_h))
points.append((px, py))
# Draw the line
for i in range(1, len(points)):
color = (0, 255, 0) if plot_history[i] >= threshold else (0, 165, 255)
cv2.line(frame, points[i-1], points[i], color, 2)
# Label
cv2.putText(frame, "History", (gx + 5, gy + 12),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (200, 200, 200), 1)
# Max value in history
if history:
max_sim = max(history)
cv2.putText(frame, f"max:{max_sim:.2f}", (gx + graph_w - 55, gy + 12),
cv2.FONT_HERSHEY_SIMPLEX, 0.3, (150, 255, 150), 1)
return frame
def _on_threshold_change(self, value):
"""Handle threshold slider change"""
val = float(value)
self.threshold_label.config(text=f"{val:.2f}")
if self.scanner:
self.scanner.config.similarity_threshold = val
def _update_similarity_display(self):
"""Update the similarity labels in the UI"""
if not self.scanner:
return
state = self.scanner.get_comparison_state()
# Show status if tracking OR if paused with valid data
if state.is_tracking or (self.scanner.paused and state.reference_image is not None):
sim = state.similarity
self.similarity_label.config(text=f"{sim:.3f}")
# Update offset and peak labels
self.offset_label.config(text=f"{state.best_offset:.2f}")
if state.similarity_history:
max_sim = max(state.similarity_history)
self.peak_label.config(text=f"{max_sim:.2f}")
if state.shift_detected or sim >= state.threshold:
self.similarity_label.config(foreground='green')
status_text = "PEAK!" if state.peak_detected else "MATCH!"
self.match_status_label.config(text=status_text, foreground='green')
elif self.scanner.paused:
self.similarity_label.config(foreground='blue')
self.match_status_label.config(text="PAUSED", foreground='blue')
else:
self.similarity_label.config(foreground='orange')
self.match_status_label.config(text="Searching", foreground='gray')
else:
self.similarity_label.config(text="--", foreground='black')
self.match_status_label.config(text="", foreground='black')
self.offset_label.config(text="--")
self.peak_label.config(text="--")
def _test_edge_comparison(self):
"""Test edge detection on current frame"""
if not self.scanner: if not self.scanner:
self.log_message("Scanner not initialized") self.log_message("Scanner not initialized")
return return
try: try:
frame = self.camera.capture_frame() results = self.scanner.test_edge_comparison()
# Detect features on all edges self.log_message("Edge test results (raw frame coordinates):")
for edge in ['left', 'right', 'top', 'bottom']: for edge in ['left', 'right', 'top', 'bottom']:
region = self.scanner.get_edge_region(frame, edge) r = results[edge]
kp, desc = self.scanner.detect_features(region) self.log_message(f" {edge.upper()}: {r['shape']}, white: {r['white_ratio']:.1%}")
count = len(kp) if kp else 0
self.log_message(f" {edge.upper()}: {count} features")
# Also update scanner's visualization data self.log_message(f" X-axis (top↔bottom): {results['x_axis_sim']:.3f}")
self.scanner._update_edge_features(frame) self.log_message(f" Y-axis (left↔right): {results['y_axis_sim']:.3f}")
# Enable feature view
self.show_features_var.set(True)
self.log_message("Feature detection test complete - enable 'Features' checkbox to see overlay")
except Exception as e: except Exception as e:
self.log_message(f"Feature detection error: {e}") self.log_message(f"Edge test error: {e}")
def _test_interpolation(self):
"""Test interpolation matching"""
if not self.scanner:
self.log_message("Scanner not initialized")
return
try:
self.log_message("Starting interpolation test (5 frames)...")
def run_test():
results = self.scanner.test_interpolation(num_frames=5)
self.root.after(0, lambda: self._show_interp_results(results))
threading.Thread(target=run_test, daemon=True).start()
except Exception as e:
self.log_message(f"Interpolation test error: {e}")
def _show_interp_results(self, results):
"""Display interpolation test results"""
self.log_message("Interpolation test results:")
for fr in results['frames']:
self.log_message(f" Frame {fr['frame_idx']}: "
f"direct={fr['direct_similarity']:.3f}, "
f"interp={fr['interpolated_similarity']:.3f}, "
f"offset={fr['best_offset']:.2f}")
# ========================================================================= # =========================================================================
# Scanner Handlers # Scanner Handlers
@ -353,16 +772,21 @@ class AppGUI:
def _update_scan_config(self): def _update_scan_config(self):
"""Update scanner config from GUI values""" """Update scanner config from GUI values"""
self.scan_config.autofocus_every_n_tiles = int(self.af_every_var.get()) self.scan_config.similarity_threshold = self.threshold_var.get()
self.scan_config.autofocus_every_row = self.af_every_row_var.get() self.scan_config.use_interpolation = self.use_interpolation_var.get()
self.scan_config.num_interpolations = self.num_interp_var.get()
self.scan_config.use_peak_detection = self.use_peak_detection_var.get()
self.scan_config.peak_window_size = self.peak_window_var.get()
self.scan_config.peak_drop_threshold = self.peak_drop_var.get()
self.scan_config.comparison_method = self.comparison_method_var.get()
# Reinitialize scanner with new config
self._init_scanner() self._init_scanner()
def _start_scan(self): def _start_scan(self):
self._update_scan_config() self._update_scan_config()
# Enable feature view during scan
self.show_features_var.set(True)
if self.scanner.start(): if self.scanner.start():
self.scan_start_btn.config(state='disabled') self.scan_start_btn.config(state='disabled', bg='#888888')
self.scan_pause_btn.config(state='normal') self.scan_pause_btn.config(state='normal')
self.scan_stop_btn.config(state='normal') self.scan_stop_btn.config(state='normal')
self.scan_status_label.config(text="Scanning") self.scan_status_label.config(text="Scanning")
@ -371,12 +795,12 @@ class AppGUI:
def _pause_scan(self): def _pause_scan(self):
if self.scanner.paused: if self.scanner.paused:
self.scanner.resume() self.scanner.resume()
self.scan_pause_btn.config(text="") self.scan_pause_btn.config(text="", bg='#2196F3')
self.scan_status_label.config(text="Scanning") self.scan_status_label.config(text="Scanning")
self.scan_progress_bar.start(10) self.scan_progress_bar.start(10)
else: else:
self.scanner.pause() self.scanner.pause()
self.scan_pause_btn.config(text="") self.scan_pause_btn.config(text="", bg='#4CAF50')
self.scan_status_label.config(text="Paused") self.scan_status_label.config(text="Paused")
self.scan_progress_bar.stop() self.scan_progress_bar.stop()
@ -385,8 +809,8 @@ class AppGUI:
self._scan_finished() self._scan_finished()
def _scan_finished(self): def _scan_finished(self):
self.scan_start_btn.config(state='normal') self.scan_start_btn.config(state='normal', bg='#4CAF50')
self.scan_pause_btn.config(state='disabled', text="") self.scan_pause_btn.config(state='disabled', text="", bg='#2196F3')
self.scan_stop_btn.config(state='disabled') self.scan_stop_btn.config(state='disabled')
self.scan_status_label.config(text="Idle") self.scan_status_label.config(text="Idle")
self.scan_progress_bar.stop() self.scan_progress_bar.stop()
@ -487,14 +911,14 @@ class AppGUI:
if self.motion.is_moving(axis): if self.motion.is_moving(axis):
self.motion.stop_axis(axis) self.motion.stop_axis(axis)
self.motion.start_movement(axis) self.motion.start_movement(axis)
self.move_buttons[axis].config(text="Moving", bg='orange') self.move_buttons[axis].config(text="Moving", bg='#FF9800')
def _toggle_movement(self, axis): def _toggle_movement(self, axis):
if self.motion.is_moving(axis): if self.motion.is_moving(axis):
self._stop_axis(axis) self._stop_axis(axis)
else: else:
self.motion.start_movement(axis) self.motion.start_movement(axis)
self.move_buttons[axis].config(text="Moving", bg='orange') self.move_buttons[axis].config(text="Moving", bg='#FF9800')
def _stop_axis(self, axis): def _stop_axis(self, axis):
self.motion.stop_axis(axis) self.motion.stop_axis(axis)
@ -579,22 +1003,18 @@ class AppGUI:
score = self.autofocus.get_focus_score() score = self.autofocus.get_focus_score()
self.focus_score_label.config(text=f"Focus: {score:.1f}") self.focus_score_label.config(text=f"Focus: {score:.1f}")
# Apply overlays # Apply standard overlays
frame = self._draw_overlays(frame) frame = self._draw_overlays(frame)
# Apply feature overlay if enabled # Apply comparison overlay (shows binary edges during tracking)
if self.show_features_var.get() and self.scanner: frame = self._draw_comparison_overlay(frame)
# Update edge features periodically
self.scanner._update_edge_features(frame)
# Get overlay from scanner
frame = self.scanner.get_feature_overlay(frame)
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Scale to fit - maximize for vertical monitor # Scale to fit
h, w = frame.shape[:2] h, w = frame.shape[:2]
max_h = 700 max_h = 700
max_w = 650 max_w = 680
scale = min(max_h / h, max_w / w) scale = min(max_h / h, max_w / w)
if scale < 1: if scale < 1:
@ -632,10 +1052,10 @@ class AppGUI:
def _draw_edge_regions(self, frame, color=(255, 255, 0), thickness=1): def _draw_edge_regions(self, frame, color=(255, 255, 0), thickness=1):
h, w = frame.shape[:2] h, w = frame.shape[:2]
bh, bw = int(h * 0.10), int(w * 0.10) bh, bw = int(h * 0.10), int(w * 0.10)
cv2.rectangle(frame, (0, 0), (bw, h), color, thickness) cv2.rectangle(frame, (0, 0), (bw, h), color, thickness) # Left
cv2.rectangle(frame, (w - bw, 0), (w, h), color, thickness) cv2.rectangle(frame, (w - bw, 0), (w, h), color, thickness) # Right
cv2.rectangle(frame, (0, 0), (w, bh), color, thickness) cv2.rectangle(frame, (0, 0), (w, bh), color, thickness) # Top
cv2.rectangle(frame, (0, h - bh), (w, h), color, thickness) cv2.rectangle(frame, (0, h - bh), (w, h), color, thickness) # Bottom
return frame return frame
# ========================================================================= # =========================================================================
@ -648,6 +1068,7 @@ class AppGUI:
self.update_camera() self.update_camera()
self._process_serial_queue() self._process_serial_queue()
self._process_tile_queue() self._process_tile_queue()
self._update_similarity_display()
if not self.autofocus.is_running(): if not self.autofocus.is_running():
self.af_button.config(state='normal') self.af_button.config(state='normal')

File diff suppressed because it is too large Load diff