Skip to content

Commit

Permalink
Add axis filter(Getting x-axis location from user) framework implemen…
Browse files Browse the repository at this point in the history
…ted.
  • Loading branch information
hannalee2 committed May 6, 2024
1 parent 7e5b98d commit 765dbaf
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 6 deletions.
150 changes: 150 additions & 0 deletions parallax/axis_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""
NoFilter serves as a pass-through component in a frame processing pipeline,
employing a worker-thread model to asynchronously handle frames without modification,
facilitating integration and optional processing steps.
"""

import logging
import time

import cv2
from PyQt5.QtCore import QObject, QThread, pyqtSignal

# Set logger name
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

class AxisFilter(QObject):
"""Class representing no filter."""

name = "None"
frame_processed = pyqtSignal(object)

class Worker(QObject):
"""Worker class for processing frames in a separate thread."""

finished = pyqtSignal()
frame_processed = pyqtSignal(object)

def __init__(self, name):
"""Initialize the worker object."""
QObject.__init__(self)
self.name = name
self.running = False
self.new = False
self.frame = None

def update_frame(self, frame):
"""Update the frame to be processed.
Args:
frame: The frame to be processed.
"""
self.frame = frame
self.new = True

def process(self, frame):
"""Process nothing (no filter) and emit the frame_processed signal.
Args:
frame: The frame to be processed.
"""
cv2.circle(frame, (2000,1500), 10, (0, 255, 0), -1) # Test
self.frame_processed.emit(frame)

def stop_running(self):
"""Stop the worker from running."""
self.running = False

def start_running(self):
"""Start the worker running."""
self.running = True

def run(self):
"""Run the worker thread."""
while self.running:
if self.new:
self.process(self.frame)
self.new = False
time.sleep(0.001)
self.finished.emit()
logger.debug(f"thread finished {self.name}")

def set_name(self, name):
"""Set name as camera serial number."""
self.name = name

def __init__(self, camera_name):
"""Initialize the filter object."""
logger.debug("Init axis filter manager")
super().__init__()
self.worker = None
self.name = camera_name
self.thread = None
self.thread = None

def init_thread(self):
"""Initialize or reinitialize the worker and thread"""
if self.thread is not None:
self.clean() # Clean up existing thread and worker before reinitializing
self.thread = QThread()
self.worker = self.Worker(self.name)
self.worker.moveToThread(self.thread)

self.thread.started.connect(self.worker.run)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.destroyed.connect(self.onThreadDestroyed)
self.threadDeleted = False

#self.worker.frame_processed.connect(self.frame_processed)
self.worker.frame_processed.connect(self.frame_processed.emit)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.thread.deleteLater)
logger.debug(f"init camera name: {self.name}")

def process(self, frame):
"""Process the frame using the worker.
Args:
frame: The frame to be processed.
"""
if self.worker is not None:
self.worker.update_frame(frame)

def start(self):
"""Start the filter by reinitializing and starting the worker and thread."""
self.init_thread() # Reinitialize and start the worker and thread
self.worker.start_running()
self.thread.start()
logger.debug(f"thread started {self.name}")

def stop(self):
"""Stop the filter by stopping the worker."""
if self.worker is not None:
self.worker.stop_running()

def set_name(self, camera_name):
"""Set camera name."""
self.name = camera_name
if self.worker is not None:
self.worker.set_name(self.name)
logger.debug(f"camera name: {self.name}")

def clean(self):
"""Safely clean up the reticle detection manager."""
logger.debug("Cleaning the thread")
if self.worker is not None:
self.worker.stop_running() # Signal the worker to stop
if not self.threadDeleted and self.thread.isRunning():
self.thread.quit() # Ask the thread to quit
self.thread.wait() # Wait for the thread to finish
self.thread = None # Clear the reference to the thread
self.worker = None # Clear the reference to the worker

def onThreadDestroyed(self):
"""Flag if thread is deleted"""
self.threadDeleted = True

def __del__(self):
"""Destructor for the filter object."""
self.clean()
21 changes: 20 additions & 1 deletion parallax/screen_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .no_filter import NoFilter
from .probe_detect_manager import ProbeDetectManager
from .reticle_detect_manager import ReticleDetectManager
from .axis_filter import AxisFilter

# Set logger name
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -77,6 +78,10 @@ def __init__(self, camera, filename=None, model=None, parent=None):
self.filter = NoFilter()
self.filter.frame_processed.connect(self.set_image_item_from_data)

# Axis Filter
self.axisFilter = AxisFilter(camera_name)
self.axisFilter.frame_processed.connect(self.set_image_item_from_data)

# Reticle Detection
self.reticleDetector = ReticleDetectManager(camera_name)
self.reticleDetector.frame_processed.connect(
Expand Down Expand Up @@ -146,6 +151,7 @@ def set_data(self, data):
Set the data displayed in the screen widget.
"""
self.filter.process(data)
self.axisFilter.process(data)
self.reticleDetector.process(data)
captured_time = self.camera.get_last_capture_time(millisecond=True)
self.probeDetector.process(data, captured_time)
Expand Down Expand Up @@ -254,7 +260,10 @@ def image_clicked(self, event):
Handle the image click event.
"""
if event.button() == QtCore.Qt.MouseButton.LeftButton:
self.select(event.pos())
x, y = event.pos().x(), event.pos().y()
x, y = int(round(x)), int(round(y))
self.select((x,y))
print(f"Clicked position on {self.get_camera_name()}: ({x}, {y})")
elif event.button() == QtCore.Qt.MouseButton.MiddleButton:
self.zoom_out()

Expand Down Expand Up @@ -283,6 +292,7 @@ def set_camera(self, camera):
camera_sn = self.get_camera_name()
self.reticleDetector.set_name(camera_sn)
self.probeDetector.set_name(camera_sn)
self.axisFilter.set_name(camera_sn)

def run_reticle_detection(self):
"""Run reticle detection by stopping the filter and starting the reticle detector."""
Expand All @@ -301,8 +311,17 @@ def run_no_filter(self):
logger.debug("run_no_filter")
self.reticleDetector.stop()
self.probeDetector.stop()
self.axisFilter.stop()
self.filter.start()

def run_axis_filter(self):
"""Run without any filter by stopping the reticle detector and probe detector."""
logger.debug("run_axis_filter")
self.filter.stop()
self.reticleDetector.stop()
logger.debug("reticleDetector stopped")
self.axisFilter.start()

def found_reticle_coords(self, x_coords, y_coords, mtx, dist):
"""Store the found reticle coordinates, camera matrix, and distortion coefficients."""
self.reticle_coords = [x_coords, y_coords]
Expand Down
27 changes: 22 additions & 5 deletions parallax/stage_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from .stage_ui import StageUI

logger = logging.getLogger(__name__)
logger.setLevel(logging.WARNING)
logger.setLevel(logging.DEBUG)


class StageWidget(QWidget):
Expand Down Expand Up @@ -87,7 +87,7 @@ def __init__(self, model, ui_dir, screen_widgets):

# Reticle Widget
self.reticle_detection_status = (
None # options: default, process, detected, accepted
None # options: default, process, detected, accepted, request_axis
)
self.reticle_calibration_btn.clicked.connect(
self.reticle_detection_button_handler
Expand Down Expand Up @@ -288,11 +288,21 @@ def reticle_detect_detected_status(self):
"background-color: #bc9e44;"
)

def get_positive_x_axis_from_user(self):
"""Get the positive x-axis coordinate of the reticle from the user."""
for screen in self.screen_widgets:
screen.reticle_coords_detected.disconnect(
self.reticle_detect_two_screens
)
screen.run_axis_filter()
self.filter = "axis_filter"

def reticle_detect_accept_detected_status(self):
"""
Finalizes the reticle detection process, accepting the detected reticle position and updating the UI accordingly.
"""
self.reticle_detection_status = "accepted"
logger.debug(f"1 self.filter: {self.filter}")

# Change the button to green.
self.reticle_calibration_btn.setStyleSheet(
Expand All @@ -303,20 +313,27 @@ def reticle_detect_accept_detected_status(self):
self.acceptButton.hide()
self.rejectButton.hide()

# TODO Get user input of positive x coordiante of the reticle
self.get_positive_x_axis_from_user()
logger.debug(f"2 self.filter: {self.filter}")

# Perform stereo calibration
result = self.calibrate_stereo()
if result:
self.reticleCalibrationLabel.setText(
f"<span style='color:green;'><small>Coords Reproj RMSE:<br></small>"
f"<span style='color:green;'>{result*1000:.1f} µm³</span>"
)

"""
for screen in self.screen_widgets:
screen.reticle_coords_detected.disconnect(
self.reticle_detect_two_screens
)
screen.run_no_filter()
self.filter = "no_filter"

logger.debug(f"3 self.filter: {self.filter}")
"""
# Enable reticle_calibration_btn button
if not self.reticle_calibration_btn.isEnabled():
self.reticle_calibration_btn.setEnabled(True)
Expand Down Expand Up @@ -609,7 +626,7 @@ def probe_detect_default_status(self):
self.probe_detect_two_screens
)
screen.run_no_filter()

self.filter = "no_filter"
self.probeCalibration.clear()

Expand All @@ -620,7 +637,7 @@ def probe_detect_process_status(self):
"""
Updates the UI and internal state to reflect that the probe detection process is underway.
"""
self.probe_detection_status = "process"
self.probe_detection_status = " "
self.probe_calibration_btn.setStyleSheet(
"color: white;"
"background-color: #bc9e44;"
Expand Down

0 comments on commit 765dbaf

Please sign in to comment.