From cf78f42c15a8a9cff4bf85b50c83026798897281 Mon Sep 17 00:00:00 2001 From: Shaiv Kamat Date: Fri, 26 Dec 2025 16:29:44 -0800 Subject: [PATCH] Base working code with Autofocus --- .gitignore | 2 + Workflow.txt | 5 + config/settings.py | 0 requirements.txt | 4 + src/__init__.py | 0 src/__pycache__/arduino.cpython-39.pyc | Bin 0 -> 3998 bytes src/__pycache__/camera.cpython-39.pyc | Bin 0 -> 2464 bytes src/__pycache__/gui.cpython-39.pyc | Bin 0 -> 17347 bytes src/__pycache__/vision.cpython-39.pyc | Bin 0 -> 812 bytes src/arduino.py | 120 +++++ src/camera.py | 72 +++ src/gui.py | 710 +++++++++++++++++++++++++ src/main.py | 27 + src/scanner.py | 0 src/vision.py | 21 + 15 files changed, 961 insertions(+) create mode 100644 .gitignore create mode 100644 Workflow.txt create mode 100644 config/settings.py create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/__pycache__/arduino.cpython-39.pyc create mode 100644 src/__pycache__/camera.cpython-39.pyc create mode 100644 src/__pycache__/gui.cpython-39.pyc create mode 100644 src/__pycache__/vision.cpython-39.pyc create mode 100644 src/arduino.py create mode 100644 src/camera.py create mode 100644 src/gui.py create mode 100644 src/main.py create mode 100644 src/scanner.py create mode 100644 src/vision.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8a1259 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv +env \ No newline at end of file diff --git a/Workflow.txt b/Workflow.txt new file mode 100644 index 0000000..339078d --- /dev/null +++ b/Workflow.txt @@ -0,0 +1,5 @@ + cd Autoscope + source venv/bin/activate # Enter environment + pip install something # Installs to this project only + python main.py # Runs with this project's packages + deactivate # Exit environment diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f747021 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +opencv-python>=4.5.0 +pyserial>=3.5 +numpy>=1.20.0 +Pillow>=1.0.0 \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/__pycache__/arduino.cpython-39.pyc b/src/__pycache__/arduino.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e6ec6e5606b340f0ed69f60535fea671ddf7a5a GIT binary patch literal 3998 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 IN-4JSAH9+sbpQYW literal 0 HcmV?d00001 diff --git a/src/__pycache__/gui.cpython-39.pyc b/src/__pycache__/gui.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..10adbefed0f8315accf27fbabc2d0427ca9bb89c GIT binary patch literal 17347 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 HnW_Ijav}2K literal 0 HcmV?d00001 diff --git a/src/__pycache__/vision.cpython-39.pyc b/src/__pycache__/vision.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7851d2bce1e1375e7d9d6d1f8edd7a7b8a5dedd GIT binary patch literal 812 zcmZuv&2G~`5Z+yT6Q__I%HM&D4;*R<)l!9!kSat8YA+R$QUt;m%XqiNsbgEac0?lQ z#+7$Sj=a)dIpq~NF|%n<`59}!nVsF4@qDvMzrRO7j`v5|2S&&b+}sxMkyo(v697R3 z%}KP?G-84Y=PQXE;R+7g74(7xoekV1Z&QluMqJRihOqPjKuuP(1)Q)oTT<9LE#Sn3 z9c$T&ugTJB9mqx(FZO(pVUnj!p48I6EYn8&IxUs-8=Vzb{&`a6Ntz{vKT8y-R36%x zpkthLc4WMCRu9X(RAw+7osY)x>$CCE+4$t6>HQ8ho#FfV`Lk2wK#ssQ{8A-TY1}K7 z%%fhut6;b-{GF&f(8bS#@N+qpVU>k%q`t1pDm-cG60TG-)G7^Una;}Mu$r3(>8^p; zHW1r9$cgpQU7P`w!|kIJ&Sbb??CVo4Uyjb%NqJg!_#z8Bn?NGVpJ`PR&=H zmI;23v<~7UvZhP7b#Z8mrkQ;n1 zL06$tc%raF!)+4PL7Nqoai=mV0Px# literal 0 HcmV?d00001 diff --git a/src/arduino.py b/src/arduino.py new file mode 100644 index 0000000..ebee2e1 --- /dev/null +++ b/src/arduino.py @@ -0,0 +1,120 @@ +import serial +import serial.tools.list_ports +import time + +class Arduino: + # Known Arduino USB vendor/product IDs + ARDUINO_IDS = [ + (0x2341, None), # Arduino SA + (0x2A03, None), # Arduino SA (alt) + (0x1A86, 0x7523), # CH340 (common clone chip) + (0x0403, 0x6001), # FTDI FT232 + (0x10C4, 0xEA60), # CP210x + (0x2341, 0x0043), # Arduino Uno + (0x2341, 0x0001), # Arduino Mega + (0x2341, 0x0042), # Arduino Mega 2560 + ] + + def __init__(self, baud=115200): + self.port = self.find_arduino() + + if not self.port: + raise RuntimeError("Could not find Arduino") + + print(f"Connecting to Arduino on {self.port}") + + try: + self.ser = serial.Serial(self.port, baud, timeout=1) + time.sleep(2) # Wait for Arduino reset + self.clear_buffer() + + except FileNotFoundError: + print("Shutting down...") + + @classmethod + def find_arduino(cls): + """ + Search available USB ports for an Arduino. + Returns port path or None if not found. + """ + print("Searching for Arduino...") + + ports = serial.tools.list_ports.comports() + + for port in ports: + print(f" Found: {port.device}") + print(f" VID:PID = {port.vid}:{port.pid}") + print(f" Description: {port.description}") + print(f" Manufacturer: {port.manufacturer}") + + # Check against known Arduino IDs + for vid, pid in cls.ARDUINO_IDS: + if port.vid == vid: + if pid is None or port.pid == pid: + print(f" → Arduino detected on {port.device}") + return port.device + + # Fallback: check description/manufacturer strings + desc = (port.description or "").lower() + mfr = (port.manufacturer or "").lower() + + if any(term in desc or term in mfr for term in + ["arduino", "ch340", "ftdi", "usb-serial", "cp210"]): + print(f" → Probable Arduino on {port.device}") + return port.device + + print(" No Arduino found") + return None + + def clear_buffer(self): + """Clear any pending data in serial buffer""" + self.ser.reset_input_buffer() + self.ser.reset_output_buffer() + + def send_command(self, cmd): + """Send command to Arduino""" + self.ser.write(cmd.encode()) + self.ser.flush() + + def read_response(self, timeout=1.0): + """Read line from Arduino""" + self.ser.timeout = timeout + return self.ser.readline().decode().strip() + + def move(self, axis, direction): + """ + axis: 'X', 'Y', or 'Z' + direction: '+' or '-' + """ + cmd = f"MOVE {axis}{direction}" + self.send_command(cmd) + + def stop(self, axis="ALL"): + """Stop movement. axis: 'X', 'Y', 'Z', or 'ALL'""" + self.send_command(f"STOP {axis}") + + def home_z(self): + """Home Z axis to max limit switch""" + self.send_command("HOME Z") + while True: + response = self.read_response(timeout=10.0) + if "LIMIT Z+" in response: + return True + if not response: + raise RuntimeError("Timeout waiting for Z home") + + def status(self): + """Query Arduino status""" + self.send_command("STATUS?") + return self.read_response() + + def close(self): + """Close serial connection""" + if self.ser and self.ser.is_open: + self.ser.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() \ No newline at end of file diff --git a/src/camera.py b/src/camera.py new file mode 100644 index 0000000..a2e9f30 --- /dev/null +++ b/src/camera.py @@ -0,0 +1,72 @@ +import cv2 + +class Camera: + def __init__(self, device_id=0): + self.cap = cv2.VideoCapture(device_id) + + if not self.cap.isOpened(): + raise RuntimeError("Could not open camera, stop program") + + # set resolution + # self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920) + # self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080) + + self.window_name = "AutoScope" + + def capture_frame(self): + ret, frame = self.cap.read() + if not ret: + raise RuntimeError("Failed to capture frame, stop program") + return frame + + def show_frame(self, frame): + """Display frame in window""" + cv2.imshow(self.window_name, frame) + + def wait_key(self, delay_ms=1): + """ + Wait for key press. Required for window to update. + Returns key code or -1 if no key pressed. + """ + return cv2.waitKey(delay_ms) & 0xFF + + def run_preview_frame(self): + frame = self.capture_frame() + self.show_frame(frame) + + key = self.wait_key(1) + + def close_window(self): + """Close the preview window""" + cv2.destroyWindow(self.window_name) + + def release(self): + """Release camera and close windows""" + self.cap.release() + cv2.destroyAllWindows() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.release() + + + # Unusued + def run_preview(self): + """ + Run live preview loop. + Press 'q' to quit. + """ + print("Starting preview. Press 'q' to quit.") + + while True: + frame = self.capture_frame() + self.show_frame(frame) + + key = self.wait_key(1) + if key == ord('q'): + break + + self.close_window() + \ No newline at end of file diff --git a/src/gui.py b/src/gui.py new file mode 100644 index 0000000..b1db253 --- /dev/null +++ b/src/gui.py @@ -0,0 +1,710 @@ +import tkinter as tk +from tkinter import ttk, scrolledtext +from PIL import Image, ImageTk +import cv2 +import threading +import queue + +class AppGUI: + def __init__(self, camera, arduino): + self.camera = camera + self.arduino = arduino + self.running = True + self._updating_slider = False + self.autofocus_running = False + self.autofocus_speed_range = (1, 6) # Speed indices 1-6 (values: 2, 5, 10, 30, 50, 70) + + # Track movement state for toggle buttons + self.axis_direction = {'X': 1, 'Y': 1, 'Z': 1} # 1 = positive, -1 = negative + self.axis_moving = {'X': False, 'Y': False, 'Z': False} + + # Queue for thread-safe serial log updates + self.serial_queue = queue.Queue() + + # Build the window + self.root = tk.Tk() + self.root.title("AutoScope") + self.root.geometry("1080x1080") + self.root.minsize(1080, 1080) + self.root.protocol("WM_DELETE_WINDOW", self.on_close) + + self._build_ui() + + # Start serial reader thread + self.serial_thread = threading.Thread(target=self._serial_reader, daemon=True) + self.serial_thread.start() + + def _build_ui(self): + # Main container + main_frame = ttk.Frame(self.root) + main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # Left: Camera view + self.camera_label = ttk.Label(main_frame) + self.camera_label.pack(side=tk.LEFT, padx=(0, 5)) + + # Right: Control panel + right_frame = ttk.Frame(main_frame, width=320) + right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) + right_frame.pack_propagate(False) + + # === EMERGENCY STOP (prominent at top) === + emergency_frame = ttk.Frame(right_frame) + emergency_frame.pack(fill=tk.X, pady=(0, 10)) + + self.emergency_btn = tk.Button( + emergency_frame, + text="⚠ EMERGENCY STOP ⚠", + command=lambda: self.send_command("!"), + bg='red', fg='white', font=('Arial', 12, 'bold'), + height=2 + ) + self.emergency_btn.pack(fill=tk.X) + + # === SPEED CONTROL === + speed_frame = ttk.LabelFrame(right_frame, text="Speed") + speed_frame.pack(fill=tk.X, pady=(0, 10)) + + speed_btn_frame = ttk.Frame(speed_frame) + speed_btn_frame.pack(fill=tk.X, padx=5, pady=5) + + self.speed_var = tk.StringVar(value="Medium") + + ttk.Radiobutton(speed_btn_frame, text="Slow", variable=self.speed_var, + value="Slow", command=lambda: self.send_command("L")).pack(side=tk.LEFT, padx=5) + ttk.Radiobutton(speed_btn_frame, text="Medium", variable=self.speed_var, + value="Medium", command=lambda: self.send_command("M")).pack(side=tk.LEFT, padx=5) + ttk.Radiobutton(speed_btn_frame, text="Fast", variable=self.speed_var, + value="Fast", command=lambda: self.send_command("F")).pack(side=tk.LEFT, padx=5) + # === FINE SPEED CONTROL === + fine_speed_frame = ttk.LabelFrame(right_frame, text="Fine Speed Control") + fine_speed_frame.pack(fill=tk.X, pady=(0, 10)) + + # Speed values matching Arduino speedArr (index 0-5) plus 0 for stop + self.speed_values = [0, 2, 5, 10, 30, 50, 70, 100] + + fine_inner = ttk.Frame(fine_speed_frame) + fine_inner.pack(fill=tk.X, padx=5, pady=5) + + # Current speed display + self.fine_speed_label = ttk.Label(fine_inner, text="Speed: 50", font=('Arial', 10, 'bold')) + self.fine_speed_label.pack(anchor=tk.W) + + # Slider + self.speed_slider = ttk.Scale( + fine_inner, + from_=0, + to=6, + orient=tk.HORIZONTAL, + command=self._on_speed_slider_change + ) + self.speed_slider.set(5) # Default to middle + self.speed_slider.pack(fill=tk.X, pady=(5, 0)) + + # Number buttons for direct selection + btn_row = ttk.Frame(fine_inner) + btn_row.pack(fill=tk.X, pady=(5, 0)) + + for i in range(7): + btn = ttk.Button( + btn_row, + text=str(i), + width=2, + command=lambda x=i: self._set_fine_speed(x) + ) + btn.pack(side=tk.LEFT, padx=1) + # === MOVEMENT CONTROLS === + move_frame = ttk.LabelFrame(right_frame, text="Movement Control") + move_frame.pack(fill=tk.X, pady=(0, 10)) + + # Command mapping for each axis + self.axis_commands = { + 'X': {'pos': 'E', 'neg': 'W', 'stop': 'e'}, + 'Y': {'pos': 'N', 'neg': 'S', 'stop': 'n'}, + 'Z': {'pos': 'U', 'neg': 'D', 'stop': 'u'} + } + + self.dir_labels = {} + self.move_buttons = {} + + for axis in ["X", "Y", "Z"]: + row = ttk.Frame(move_frame) + row.pack(fill=tk.X, pady=3, padx=5) + + # Axis label + ttk.Label(row, text=f"{axis}:", width=3, font=('Arial', 10, 'bold')).pack(side=tk.LEFT) + + # Direction toggle button + dir_btn = ttk.Button(row, text="+ →", width=5, + command=lambda a=axis: self.toggle_direction(a)) + dir_btn.pack(side=tk.LEFT, padx=2) + self.dir_labels[axis] = dir_btn + + # Start/Stop movement toggle + move_btn = tk.Button(row, text="Move", width=8, bg='green', fg='white', + command=lambda a=axis: self.toggle_movement(a)) + move_btn.pack(side=tk.LEFT, padx=2) + self.move_buttons[axis] = move_btn + + # Quick stop for this axis + ttk.Button(row, text="Stop", width=6, + command=lambda a=axis: self.stop_axis(a)).pack(side=tk.LEFT, padx=2) + + # Stop All button + ttk.Button(move_frame, text="Stop All Axes", + command=lambda: self.send_command("X")).pack(fill=tk.X, padx=5, pady=5) + + # === AUTOFOCUS CONTROL === + af_frame = ttk.LabelFrame(right_frame, text="Autofocus") + af_frame.pack(fill=tk.X, pady=(0, 10)) + + af_inner = ttk.Frame(af_frame) + af_inner.pack(fill=tk.X, padx=5, pady=5) + + # Focus score display + self.focus_score_label = ttk.Label(af_inner, text="Focus: --", font=('Arial', 10)) + self.focus_score_label.pack(anchor=tk.W) + + # Autofocus buttons + af_btn_row = ttk.Frame(af_inner) + af_btn_row.pack(fill=tk.X, pady=(5, 0)) + + self.af_button = ttk.Button(af_btn_row, text="Autofocus", command=self.start_autofocus) + self.af_button.pack(side=tk.LEFT, padx=2) + + ttk.Button(af_btn_row, text="Coarse AF", command=lambda: self.start_autofocus(coarse=True)).pack(side=tk.LEFT, padx=2) + ttk.Button(af_btn_row, text="Fine AF", command=lambda: self.start_autofocus(fine=True)).pack(side=tk.LEFT, padx=2) + ttk.Button(af_btn_row, text="Stop AF", command=self.stop_autofocus).pack(side=tk.LEFT, padx=2) + + # Live focus monitoring toggle + self.live_focus_var = tk.BooleanVar(value=True) + ttk.Checkbutton(af_inner, text="Show live focus score", + variable=self.live_focus_var).pack(anchor=tk.W, pady=(5, 0)) + + + # === POSITION & STATUS === + pos_frame = ttk.LabelFrame(right_frame, text="Status") + pos_frame.pack(fill=tk.X, pady=(0, 10)) + + pos_btn_frame = ttk.Frame(pos_frame) + pos_btn_frame.pack(fill=tk.X, padx=5, pady=5) + + ttk.Button(pos_btn_frame, text="Status", command=lambda: self.send_command("?")).pack(side=tk.LEFT, padx=2) + ttk.Button(pos_btn_frame, text="Zero All", command=lambda: self.send_command("Z")).pack(side=tk.LEFT, padx=2) + ttk.Button(pos_btn_frame, text="Go Origin", command=lambda: self.send_command("G")).pack(side=tk.LEFT, padx=2) + + # === MODE CONTROL === + mode_frame = ttk.LabelFrame(right_frame, text="Control Mode") + mode_frame.pack(fill=tk.X, pady=(0, 10)) + + mode_btn_frame = ttk.Frame(mode_frame) + mode_btn_frame.pack(fill=tk.X, padx=5, pady=5) + + self.mode_label = ttk.Label(mode_btn_frame, text="Mode: Serial", font=('Arial', 9)) + self.mode_label.pack(side=tk.LEFT, padx=5) + + ttk.Button(mode_btn_frame, text="Toggle Mode", command=lambda: self.send_command("T")).pack(side=tk.RIGHT, padx=2) + + # === SERIAL LOG === + ttk.Label(right_frame, text="Serial Log:").pack(anchor=tk.W) + self.serial_log = scrolledtext.ScrolledText(right_frame, height=10, width=35, state=tk.DISABLED) + self.serial_log.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) + + # === CUSTOM COMMAND === + ttk.Label(right_frame, text="Send Command:").pack(anchor=tk.W) + cmd_frame = ttk.Frame(right_frame) + cmd_frame.pack(fill=tk.X) + + self.cmd_entry = ttk.Entry(cmd_frame) + self.cmd_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + self.cmd_entry.bind("", lambda e: self.send_custom_command()) + + ttk.Button(cmd_frame, text="Send", command=self.send_custom_command).pack(side=tk.RIGHT, padx=(5, 0)) + + def toggle_direction(self, axis): + """Toggle direction for an axis""" + self.axis_direction[axis] *= -1 + direction = "+" if self.axis_direction[axis] == 1 else "-" + arrow = "→" if self.axis_direction[axis] == 1 else "←" + self.dir_labels[axis].config(text=f"{direction} {arrow}") + + # If currently moving, restart in new direction + if self.axis_moving[axis]: + self.stop_axis(axis) + self.start_movement(axis) + + def toggle_movement(self, axis): + """Toggle movement on/off for an axis""" + if self.axis_moving[axis]: + self.stop_axis(axis) + else: + self.start_movement(axis) + + def start_movement(self, axis): + """Start continuous movement on an axis""" + commands = self.axis_commands[axis] + cmd = commands['pos'] if self.axis_direction[axis] == 1 else commands['neg'] + self.send_command(cmd) + self.axis_moving[axis] = True + self.move_buttons[axis].config(text="Moving", bg='orange') + + def stop_axis(self, axis): + """Stop movement on a specific axis""" + commands = self.axis_commands[axis] + self.send_command(commands['stop']) + self.axis_moving[axis] = False + self.move_buttons[axis].config(text="Move", bg='green') + + def send_command(self, cmd): + """Send a command and log it""" + self.log_message(f"> {cmd}") + if self.arduino: + self.arduino.send_command(cmd) + + def send_custom_command(self): + """Send command from entry box""" + cmd = self.cmd_entry.get().strip() + if cmd: + self.send_command(cmd) + self.cmd_entry.delete(0, tk.END) + + def _on_speed_slider_change(self, value): + """Handle slider movement""" + if self._updating_slider: + return + # Round to nearest integer + index = round(float(value)) + self._set_fine_speed(index) + + def _set_fine_speed(self, index): + """Set fine speed by index (0-10)""" + # Prevent recursion + self._updating_slider = True + # Update slider position + self.speed_slider.set(index) + + # Update label with actual speed value + speed_val = self.speed_values[index] + self.fine_speed_label.config(text=f"Speed: {speed_val}") + self._updating_slider = False + + # Send command - '0' for index 0, '1'-'9' for 1-9, 'a' for 10 + if index == 0: + self.send_command("0") + elif index <= 9: + self.send_command(str(index)) + else: + # For index 10 (speed 300), you'd need to add a handler on Arduino + # For now, send '9' as max or add a new command + self.send_command("9") + + + + + + + + + + def get_focus_score(self): + """Get current focus score from camera""" + try: + frame = self.camera.capture_frame() + from vision import calculate_focus_score_sobel + score = calculate_focus_score_sobel(frame) + return score + except Exception as e: + print(f"Focus score error: {e}") + return 0 + + def start_autofocus(self, coarse=False, fine=False): + """Start autofocus routine in background thread""" + if self.autofocus_running: + self.log_message("Autofocus already running") + return + + self.autofocus_running = True + self.af_button.config(state='disabled') + + # Determine speed range based on mode + if coarse: + speed_range = (4, 6) # Faster speeds: 30, 50, 70 + elif fine: + speed_range = (1, 3) # Slower speeds: 2, 5, 10 + else: + speed_range = (1, 6) # Full range + + af_thread = threading.Thread( + target=self._autofocus_routine, + args=(speed_range,), + daemon=True + ) + af_thread.start() + + def stop_autofocus(self): + """Stop autofocus routine""" + self.autofocus_running = False + self.send_command('u') # Stop Z axis + self.af_button.config(state='normal') + self.log_message("Autofocus stopped") + + def _get_averaged_focus(self, samples=5, delay_between=0.05): + """ + Get averaged focus score from multiple samples. + Reduces noise in focus measurements. + """ + import time + scores = [] + for _ in range(samples): + scores.append(self.get_focus_score()) + time.sleep(delay_between) + + # Remove outliers (optional - take middle 60%) + scores.sort() + trim = len(scores) // 5 # Remove top and bottom 20% + if trim > 0: + scores = scores[trim:-trim] + + return sum(scores) / len(scores) + + def _autofocus_routine(self, speed_range): + """ + Autofocus using sweep search + hill climbing. + Phase 1: Sweep across range to find approximate peak + Phase 2: Fine tune from best position found + """ + import time + + self.log_message(f"Starting autofocus (speed range: {speed_range})") + + min_speed_idx, max_speed_idx = speed_range + + # Timing settings + SWEEP_MOVE_TIME = 2 # Movement time per sweep step + SWEEP_SETTLE_TIME = 0.25 # Settle time during sweep + SWEEP_SAMPLES = 10 + + FINE_MOVE_TIME = 0.15 + FINE_SETTLE_TIME = 0.25 + FINE_SAMPLES = 10 + + # Number of steps in each direction for sweep + SWEEP_STEPS = 15 # Total sweep = 2 * SWEEP_STEPS positions + + # ======================================== + # Phase 1: Sweep search + # ======================================== + self.log_message("Phase 1: Sweep search") + + # Use medium speed for sweep + sweep_speed = (min_speed_idx + max_speed_idx) // 2 + self._set_fine_speed_no_send(sweep_speed) + self.send_command(str(sweep_speed)) + time.sleep(0.1) + + # Record starting position score + time.sleep(SWEEP_SETTLE_TIME) + start_score = self._get_averaged_focus(samples=SWEEP_SAMPLES) + self._update_focus_display(start_score) + + # Data structure: (steps_from_start, score) + # Positive = up from start, negative = down from start + sweep_data = [(0, start_score)] + self.log_message(f"Start position: {start_score:.1f}") + + if not self.autofocus_running: + return + + # Sweep DOWN first tyo avoid objective / slide contact + self.log_message(f"Sweeping down {SWEEP_STEPS} steps...") + for i in range(1, SWEEP_STEPS + 1): + if not self.autofocus_running: + return + + self.send_command('D') + time.sleep(SWEEP_MOVE_TIME) + self.send_command('d') + time.sleep(SWEEP_SETTLE_TIME) + + score = self._get_averaged_focus(samples=SWEEP_SAMPLES) + sweep_data.append((-i, score)) + self._update_focus_display(score) + + if i % 5 == 0: + self.log_message(f" Step -{i}: {score:.1f}") + + if not self.autofocus_running: + return + + # Return to start + self.log_message("Returning to start...") + for _ in range(SWEEP_STEPS): + if not self.autofocus_running: + return + self.send_command('U') + time.sleep(SWEEP_MOVE_TIME) + self.send_command('u') + time.sleep(0.1) # Faster return, no need to sample + + time.sleep(SWEEP_SETTLE_TIME) + + if not self.autofocus_running: + return + + # Sweep UP + self.log_message(f"Sweeping up {SWEEP_STEPS} steps...") + for i in range(1, SWEEP_STEPS + 1): + if not self.autofocus_running: + return + + self.send_command('U') + time.sleep(SWEEP_MOVE_TIME) + self.send_command('u') + time.sleep(SWEEP_SETTLE_TIME) + + score = self._get_averaged_focus(samples=SWEEP_SAMPLES) + sweep_data.append((i, score)) + self._update_focus_display(score) + + if i % 5 == 0: + self.log_message(f" Step +{i}: {score:.1f}") + + if not self.autofocus_running: + return + + # ======================================== + # Find best position from sweep + # ======================================== + best_step, best_score = max(sweep_data, key=lambda x: x[1]) + self.log_message(f"Best found at step {best_step}: {best_score:.1f}") + + # Log the full sweep curve for debugging + self.log_message("Sweep curve:") + sorted_data = sorted(sweep_data, key=lambda x: x[0]) + curve_str = " ".join([f"{s[1]:.0f}" for s in sorted_data]) + self.log_message(f" {curve_str}") + + # ======================================== + # Move to best position + # ======================================== + # Currently at +SWEEP_STEPS, need to go to best_step + steps_to_move = SWEEP_STEPS - best_step # Positive = move down + + if steps_to_move > 0: + self.log_message(f"Moving down {steps_to_move} steps to best position") + move_cmd, stop_cmd = 'D', 'd' + else: + self.log_message(f"Moving up {abs(steps_to_move)} steps to best position") + move_cmd, stop_cmd = 'U', 'u' + steps_to_move = abs(steps_to_move) + + for _ in range(steps_to_move): + if not self.autofocus_running: + return + self.send_command(move_cmd) + time.sleep(SWEEP_MOVE_TIME) + self.send_command(stop_cmd) + time.sleep(0.1) + + time.sleep(SWEEP_SETTLE_TIME) + current_score = self._get_averaged_focus(samples=SWEEP_SAMPLES) + self.log_message(f"At best position: {current_score:.1f}") + + if not self.autofocus_running: + return + + # ======================================== + # Phase 2: Fine hill-climbing from best position + # ======================================== + self.log_message("Phase 2: Fine tuning") + + fine_speed = min_speed_idx + self._set_fine_speed_no_send(fine_speed) + self.send_command(str(fine_speed)) + time.sleep(0.1) + + best_score = current_score + + # Try both directions to find which improves + # Try UP first + self.send_command('D') + time.sleep(FINE_MOVE_TIME) + self.send_command('d') + time.sleep(FINE_SETTLE_TIME) + + down_score = self._get_averaged_focus(samples=FINE_SAMPLES) + + if down_score > best_score: + fine_direction = 0 # DOWN + best_score = down_score + self.log_message(f"Fine direction: UP ({down_score:.1f})") + else: + # Go back down and try down + self.send_command('U') + time.sleep(FINE_MOVE_TIME) + self.send_command('u') + time.sleep(FINE_SETTLE_TIME) + + self.send_command('U') + time.sleep(FINE_MOVE_TIME) + self.send_command('u') + time.sleep(FINE_SETTLE_TIME) + + up_score = self._get_averaged_focus(samples=FINE_SAMPLES) + + if up_score > best_score: + fine_direction = 1 # UP + best_score = up_score + self.log_message(f"Fine direction: DOWN ({up_score:.1f})") + else: + # We're already at the peak + self.log_message("Already at peak, minor adjustment only") + fine_direction = 1 # Try up anyway + # Go back to center + self.send_command('U') + time.sleep(FINE_MOVE_TIME) + self.send_command('u') + time.sleep(FINE_SETTLE_TIME) + + if not self.autofocus_running: + return + + # Fine search + move_cmd = 'U' if fine_direction else 'D' + stop_cmd = 'u' if fine_direction else 'd' + reverse_cmd = 'D' if fine_direction else 'U' + reverse_stop = 'd' if fine_direction else 'u' + + no_improvement_count = 0 + max_fine_no_improvement = 5 + best_position_offset = 0 + + while self.autofocus_running and no_improvement_count < max_fine_no_improvement: + self.send_command(move_cmd) + time.sleep(FINE_MOVE_TIME) + self.send_command(stop_cmd) + time.sleep(FINE_SETTLE_TIME) + + current_score = self._get_averaged_focus(samples=FINE_SAMPLES) + self._update_focus_display(current_score) + + if current_score > best_score: + best_score = current_score + no_improvement_count = 0 + best_position_offset = 0 + self.log_message(f"Fine better: {current_score:.1f}") + else: + no_improvement_count += 1 + best_position_offset += 1 + + if not self.autofocus_running: + return + + # Return to best fine position + if best_position_offset > 0: + self.log_message(f"Returning {best_position_offset} steps") + for _ in range(best_position_offset): + self.send_command(reverse_cmd) + time.sleep(FINE_MOVE_TIME) + self.send_command(reverse_stop) + time.sleep(0.1) + + # Final reading + time.sleep(FINE_SETTLE_TIME) + final_score = self._get_averaged_focus(samples=FINE_SAMPLES) + self._update_focus_display(final_score) + + self.autofocus_running = False + self.log_message(f"Autofocus complete. Final: {final_score:.1f}") + + self.root.after(0, lambda: self.af_button.config(state='normal')) + + + + def _set_fine_speed_no_send(self, index): + """Update slider/label without sending command (for internal use)""" + self._updating_slider = True + self.speed_slider.set(index) + speed_val = self.speed_values[index] + self.fine_speed_label.config(text=f"Speed: {speed_val}") + self._updating_slider = False + + def _update_focus_display(self, score): + """Update focus score display (thread-safe)""" + self.root.after(0, lambda: self.focus_score_label.config(text=f"Focus: {score:.1f}")) + + def log_message(self, msg): + """Add message to serial log""" + if not hasattr(self, 'serial_log'): + return + self.serial_log.config(state=tk.NORMAL) + self.serial_log.insert(tk.END, msg + "\n") + self.serial_log.see(tk.END) + self.serial_log.config(state=tk.DISABLED) + print(f" Log: {msg}") + + # Check for mode changes in received messages + if msg.startswith("< MODE:"): + mode = msg.split(":")[-1] + self.mode_label.config(text=f"Mode: {mode.capitalize()}") + + def _serial_reader(self): + """Background thread to read serial data""" + while self.running: + if self.arduino and self.arduino.ser.in_waiting: + try: + line = self.arduino.ser.readline().decode().strip() + if line: + self.serial_queue.put(line) + except Exception as e: + self.serial_queue.put(f"[Error: {e}]") + threading.Event().wait(0.05) + + def _process_serial_queue(self): + """Process queued serial messages (call from main thread)""" + while not self.serial_queue.empty(): + msg = self.serial_queue.get_nowait() + self.log_message(f"< {msg}") + + def update_camera(self): + """Capture and display one frame""" + try: + frame = self.camera.capture_frame() + + # Show live focus score if enabled + if self.live_focus_var.get(): + score = self._get_averaged_focus() + self.focus_score_label.config(text=f"Focus: {score:.1f}") + + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # Resize if needed + h, w = frame.shape[:2] + max_width = 640 + if w > max_width: + scale = max_width / w + frame = cv2.resize(frame, (int(w * scale), int(h * scale))) + + img = Image.fromarray(frame) + imgtk = ImageTk.PhotoImage(image=img) + + self.camera_label.imgtk = imgtk + self.camera_label.configure(image=imgtk) + except Exception as e: + print(f"Camera error: {e}") + + def run(self): + """Main loop""" + def update(): + if self.running: + self.update_camera() + self._process_serial_queue() + self.root.after(33, update) + + update() + self.root.mainloop() + + def on_close(self): + """Clean shutdown""" + self.running = False + self.root.destroy() \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..82358c5 --- /dev/null +++ b/src/main.py @@ -0,0 +1,27 @@ +from camera import Camera +from arduino import Arduino +from gui import AppGUI + +def main(): + camera = Camera(device_id=0) + + # Arduino is optional - GUI still works without it + try: + arduino = Arduino() + except RuntimeError as e: + print(f"Arduino not found: {e}") + arduino = None + + try: + app = AppGUI(camera, arduino) + app.run() + + except KeyboardInterrupt: + print("Shutting down...") + finally: + if arduino: + arduino.close() + camera.release() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/scanner.py b/src/scanner.py new file mode 100644 index 0000000..e69de29 diff --git a/src/vision.py b/src/vision.py new file mode 100644 index 0000000..879b47e --- /dev/null +++ b/src/vision.py @@ -0,0 +1,21 @@ +import cv2 +import numpy as np + +def calculate_focus_score(frame): + """ + Calculate focus score using Laplacian variance. + """ + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + laplacian = cv2.Laplacian(gray, cv2.CV_64F) + score = laplacian.var() + return score + +def calculate_focus_score_sobel(frame): + """ + Focus score using Sobel gradients. + """ + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3) + sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3) + score = np.mean(sobel_x**2 + sobel_y**2) + return score \ No newline at end of file