diff --git a/modules/advancedSettingsWidget.py b/modules/advancedSettingsWidget.py index c11186352d905f3ca41d6f3abacdae3b0415ddba..8983d37ec11bfbb78686e1ff9734f9c237d79c75 100644 --- a/modules/advancedSettingsWidget.py +++ b/modules/advancedSettingsWidget.py @@ -2,22 +2,27 @@ from PyQt5.QtWidgets import ( QPushButton, QVBoxLayout, QWidget, QSlider, QLabel, QGridLayout, QSizePolicy ) -from PyQt5.QtGui import QPixmap, QImage +from PyQt5.QtGui import QPixmap, QImage, QShowEvent from PyQt5.QtCore import Qt import numpy as np +from mainWindow import MainWindow +from typing import Optional class AdvancedSettingsWidget(QWidget): """ Shows toggle rainbow, circle editor, line smoothing slider, contrast slider, plus two image previews (contrasted-blurred and cost). - The images should maintain aspect ratio upon resize. + The images maintain aspect ratio upon resize. """ - def __init__(self, main_window, parent=None): + def __init__(self, main_window: MainWindow, parent: Optional[QWidget] = None): + """ + Constructor. + """ super().__init__(parent) self._main_window = main_window - self._last_cb_pix = None # store QPixmap for contrasted-blurred - self._last_cost_pix = None # store QPixmap for cost + self._last_cb_pix = None # store QPixmap for contrasted-blurred image + self._last_cost_pix = None # store QPixmap for cost image main_layout = QVBoxLayout() self.setLayout(main_layout) @@ -25,17 +30,17 @@ class AdvancedSettingsWidget(QWidget): # A small grid for controls controls_layout = QGridLayout() - # 1) Rainbow toggle + # Rainbow toggle self.btn_toggle_rainbow = QPushButton("Toggle Rainbow") self.btn_toggle_rainbow.clicked.connect(self._on_toggle_rainbow) controls_layout.addWidget(self.btn_toggle_rainbow, 0, 0) - # 2) Circle editor + # Disk size calibration (Circle editor) self.btn_circle_editor = QPushButton("Calibrate Kernel Size") self.btn_circle_editor.clicked.connect(self._main_window.open_circle_editor) controls_layout.addWidget(self.btn_circle_editor, 0, 1) - # 3) Line smoothing slider + label + # Line smoothing slider + label self._lab_smoothing = QLabel("Line smoothing (3)") controls_layout.addWidget(self._lab_smoothing, 1, 0) self.line_smoothing_slider = QSlider(Qt.Horizontal) @@ -44,7 +49,7 @@ class AdvancedSettingsWidget(QWidget): self.line_smoothing_slider.valueChanged.connect(self._on_line_smoothing_slider) controls_layout.addWidget(self.line_smoothing_slider, 1, 1) - # 4) Contrast slider + label + # Contrast slider + label self._lab_contrast = QLabel("Contrast (0.01)") controls_layout.addWidget(self._lab_contrast, 2, 0) self.contrast_slider = QSlider(Qt.Horizontal) @@ -56,14 +61,12 @@ class AdvancedSettingsWidget(QWidget): main_layout.addLayout(controls_layout) - # We'll set a minimum width so that the main window expands - # rather than overlapping the image self.setMinimumWidth(350) - # Now a vertical layout for the two images, each with a label above it + # A vertical layout for the two images, each with a label above it images_layout = QVBoxLayout() - # 1) Contrasted-blurred label + image + # Contrasted-blurred label + image self.label_cb_title = QLabel("Contrasted Blurred Image") self.label_cb_title.setAlignment(Qt.AlignCenter) images_layout.addWidget(self.label_cb_title) @@ -73,7 +76,7 @@ class AdvancedSettingsWidget(QWidget): self.label_contrasted_blurred.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) images_layout.addWidget(self.label_contrasted_blurred) - # 2) Cost image label + image + # Cost image label + image self.label_cost_title = QLabel("Current COST IMAGE") self.label_cost_title.setAlignment(Qt.AlignCenter) images_layout.addWidget(self.label_cost_title) @@ -85,21 +88,24 @@ class AdvancedSettingsWidget(QWidget): main_layout.addLayout(images_layout) - def showEvent(self, event): + def showEvent(self, event: QShowEvent): """ When shown, ask parent to resize to accommodate. """ super().showEvent(event) if self.parentWidget(): self.parentWidget().adjustSize() - def resizeEvent(self, event): + def resizeEvent(self, event: QShowEvent): """ Keep the images at correct aspect ratio by re-scaling - our stored pixmaps to the new label sizes. + stored pixmaps to the new label sizes. """ super().resizeEvent(event) self._update_labels() def _update_labels(self): + """ + Re-scale stored pixmaps to the new label sizes. + """ if self._last_cb_pix is not None: scaled_cb = self._last_cb_pix.scaled( self.label_contrasted_blurred.size(), @@ -117,21 +123,29 @@ class AdvancedSettingsWidget(QWidget): self.label_cost_image.setPixmap(scaled_cost) def _on_toggle_rainbow(self): + """ + Called when the rainbow toggle button is clicked. + """ self._main_window.toggle_rainbow() - def _on_line_smoothing_slider(self, value): + def _on_line_smoothing_slider(self, value: int): + """ + Called when the line smoothing slider is moved. + """ self._lab_smoothing.setText(f"Line smoothing ({value})") self._main_window.image_view.set_savgol_window_length(value) - def _on_contrast_slider(self, value): + def _on_contrast_slider(self, value: int): + """ + Called when the contrast slider is moved. + """ clip_limit = value / 100.0 self._lab_contrast.setText(f"Contrast ({clip_limit:.2f})") self._main_window.update_contrast(clip_limit) - def update_displays(self, contrasted_img_np, cost_img_np): + def update_displays(self, contrasted_img_np: np.ndarray, cost_img_np: np.ndarray): """ - Called by main_window to refresh the two images in the advanced panel. - We'll store them as QPixmaps, then do the re-scale in _update_labels(). + Update the contrasted-blurred and cost images. """ cb_pix = self._np_array_to_qpixmap(contrasted_img_np) cost_pix = self._np_array_to_qpixmap(cost_img_np, normalize=True) @@ -140,7 +154,10 @@ class AdvancedSettingsWidget(QWidget): self._last_cost_pix = cost_pix self._update_labels() - def _np_array_to_qpixmap(self, arr, normalize=False): + def _np_array_to_qpixmap(self, arr: np.ndarray, normalize: bool = False) -> QPixmap: + """ + Convert a numpy array to a QPixmap. + """ if arr is None: return None arr_ = arr.copy() diff --git a/modules/circleEditorGraphicsView.py b/modules/circleEditorGraphicsView.py index d3a35173d58d3a4b0b09b558e0c187c12750e057..5134f532e5f25206c8302ade24c95d1f3acd3a2c 100644 --- a/modules/circleEditorGraphicsView.py +++ b/modules/circleEditorGraphicsView.py @@ -1,15 +1,24 @@ -from PyQt5.QtWidgets import QGraphicsView +from PyQt5.QtWidgets import QGraphicsView, QWidget from panZoomGraphicsView import PanZoomGraphicsView from PyQt5.QtCore import Qt +from PyQt5.QtGui import QMouseEvent, QWheelEvent from draggableCircleItem import DraggableCircleItem +from circleEditorWidget import CircleEditorWidget +from typing import Optional -# A specialized PanZoomGraphicsView for the circle editor +# A specialized PanZoomGraphicsView for the circle editor (disk size calibration) class CircleEditorGraphicsView(PanZoomGraphicsView): - def __init__(self, circle_editor_widget, parent=None): + def __init__(self, circle_editor_widget: CircleEditorWidget, parent: Optional[QWidget] = None): + """ + Constructor. + """ super().__init__(parent) self._circle_editor_widget = circle_editor_widget - def mousePressEvent(self, event): + def mousePressEvent(self, event: QMouseEvent): + """ + If the user clicks on the circle, we let the circle item handle the event. + """ if event.button() == Qt.LeftButton: # Check if user clicked on the circle item clicked_item = self.itemAt(event.pos()) @@ -24,10 +33,9 @@ class CircleEditorGraphicsView(PanZoomGraphicsView): return QGraphicsView.mousePressEvent(self, event) super().mousePressEvent(event) - def wheelEvent(self, event): + def wheelEvent(self, event: QWheelEvent): """ - If the mouse is hovering over the circle, we adjust the circle's radius - instead of zooming the image. + If the user scrolls the mouse wheel over the circle, we change the circle """ pos_in_widget = event.pos() item_under = self.itemAt(pos_in_widget) @@ -46,4 +54,4 @@ class CircleEditorGraphicsView(PanZoomGraphicsView): event.accept() return - super().wheelEvent(event) + super().wheelEvent(event) \ No newline at end of file diff --git a/modules/circleEditorWidget.py b/modules/circleEditorWidget.py index aef3101258b59c15ecb1a9475204800be90ad02a..b8230e8ef1e5350888cf1124ebeb864ccfafeb6f 100644 --- a/modules/circleEditorWidget.py +++ b/modules/circleEditorWidget.py @@ -1,14 +1,21 @@ from PyQt5.QtWidgets import ( - QGraphicsScene, QGraphicsPixmapItem, QPushButton, + QGraphicsScene, QGraphicsPixmapItem, QPushButton, QHBoxLayout, QVBoxLayout, QWidget, QSlider, QLabel ) -from PyQt5.QtGui import QFont +from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtCore import Qt, QRectF, QSize from circleEditorGraphicsView import CircleEditorGraphicsView from draggableCircleItem import DraggableCircleItem +from typing import Optional, Callable class CircleEditorWidget(QWidget): - def __init__(self, pixmap, init_radius=20, done_callback=None, parent=None): + """ + A widget for the user to calibrate the disk size (kernel size) for the ridge detection. + """ + def __init__(self, pixmap: QPixmap, init_radius: int = 20, done_callback: Optional[Callable[[], None]] = None, parent: Optional[QWidget] = None): + """ + Constructor. + """ super().__init__(parent) self._pixmap = pixmap self._done_callback = done_callback @@ -17,9 +24,7 @@ class CircleEditorWidget(QWidget): layout = QVBoxLayout(self) self.setLayout(layout) - # - # 1) ADD A CENTERED LABEL ABOVE THE IMAGE, WITH BIGGER FONT - # + # Add centered label above image label_instructions = QLabel("Scale the dot to be of the size of your ridge") label_instructions.setAlignment(Qt.AlignCenter) big_font = QFont("Arial", 20) @@ -27,15 +32,12 @@ class CircleEditorWidget(QWidget): label_instructions.setFont(big_font) layout.addWidget(label_instructions) - # - # 2) THE SPECIALIZED GRAPHICS VIEW THAT SHOWS THE IMAGE - # + # Show the image self._graphics_view = CircleEditorGraphicsView(circle_editor_widget=self) self._scene = QGraphicsScene(self) self._graphics_view.setScene(self._scene) layout.addWidget(self._graphics_view) - # Show the image self._image_item = QGraphicsPixmapItem(self._pixmap) self._scene.addItem(self._image_item) @@ -49,9 +51,7 @@ class CircleEditorWidget(QWidget): self._graphics_view.setSceneRect(QRectF(self._pixmap.rect())) self._graphics_view.fitInView(self._image_item, Qt.KeepAspectRatio) - # - # 3) CONTROLS BELOW - # + ### Controls below bottom_layout = QHBoxLayout() layout.addLayout(bottom_layout) @@ -64,7 +64,7 @@ class CircleEditorWidget(QWidget): self._slider.setValue(self._init_radius) bottom_layout.addWidget(self._slider) - # done button + # Done button self._btn_done = QPushButton("Done") bottom_layout.addWidget(self._btn_done) @@ -72,16 +72,25 @@ class CircleEditorWidget(QWidget): self._slider.valueChanged.connect(self._on_slider_changed) self._btn_done.clicked.connect(self._on_done_clicked) - def _on_slider_changed(self, value): + def _on_slider_changed(self, value: int): + """ + Handle slider value changes. + """ self._circle_item.set_radius(value) self._lbl_size.setText(f"size ({value})") def _on_done_clicked(self): + """ + Handle the user clicking the "Done" button. + """ final_radius = self._circle_item.radius() if self._done_callback is not None: self._done_callback(final_radius) - def update_slider_value(self, new_radius): + def update_slider_value(self, new_radius: int): + """ + Update the slider value. + """ self._slider.blockSignals(True) self._slider.setValue(new_radius) self._slider.blockSignals(False) diff --git a/modules/circle_edge_kernel.py b/modules/circle_edge_kernel.py index 4270fba0203817b67eaf879f011078a981e94ca6..f8587459ca36433198857e31488018368068747f 100644 --- a/modules/circle_edge_kernel.py +++ b/modules/circle_edge_kernel.py @@ -1,21 +1,17 @@ import numpy as np +from typing import Optional -def circle_edge_kernel(k_size=5, radius=None): +def circle_edge_kernel(k_size: int = 5, radius: Optional[int] = None) -> np.ndarray: """ Create a k_size x k_size array whose values increase linearly from 0 at the center to 1 at the circle boundary (radius). - Parameters - ---------- - k_size : int - The size (width and height) of the kernel array. - radius : float, optional - The circle's radius. By default, set to (k_size-1)/2. + Args: + k_size: The size (width and height) of the kernel array. + radius: The circle's radius. By default, set to (k_size-1)/2. - Returns - ------- - kernel : 2D numpy array of shape (k_size, k_size) - The circle-edge-weighted kernel. + Returns: + kernel: The circle-edge-weighted kernel. """ if radius is None: # By default, let the radius be half the kernel size diff --git a/modules/compute_disk_size.py b/modules/compute_disk_size.py index c4cb24d66a0d4c84587d84eff3bc342606d5e74f..84f9b6d6d2938c13ddd3d728fc3cf53af48bc99c 100644 --- a/modules/compute_disk_size.py +++ b/modules/compute_disk_size.py @@ -1,4 +1,14 @@ import numpy as np -def compute_disk_size(user_radius, upscale_factor=1.2): +def compute_disk_size(user_radius: int, upscale_factor: float = 1.2) -> int: + """ + Compute the size of the disk to be used in the cost image computation. + + Args: + user_radius: The radius in pixels. + upscale_factor: The factor by which the disk size will be upscaled. + + Returns: + The size of the disk. + """ return int(np.ceil(upscale_factor * 2 * user_radius + 1) // 2 * 2 + 1) \ No newline at end of file diff --git a/modules/downscale.py b/modules/downscale.py index 2387e26bfd74f79861cfbcc3c58336f22554be65..ca95b390b444b9ffe271b3422f93045ec3ea7fd7 100644 --- a/modules/downscale.py +++ b/modules/downscale.py @@ -1,10 +1,19 @@ import cv2 +import numpy as np +from typing import Tuple # Currently not implemented -def downscale(img, points, scale_percent): +def downscale(img: np.ndarray, points: Tuple[Tuple[int, int], Tuple[int, int]], scale_percent: int) -> Tuple[np.ndarray, Tuple[Tuple[int, int], Tuple[int, int]]]: """ - Downsample `img` to `scale_percent` size and scale the given points accordingly. - Returns (downsampled_img, (scaled_seed, scaled_target)). + Downscale an image and its corresponding points. + + Args: + img: The image. + points: The points to downscale. + scale_percent: The percentage to downscale to. E.g. scale_percent = 60 results in a new image 60% of the original image's size. + + Returns: + The downsampled image and the downsampled points. """ if scale_percent == 100: return img, (tuple(points[0]), tuple(points[1])) diff --git a/modules/draggableCircleItem.py b/modules/draggableCircleItem.py index 8d0e6fa15fe823b36a54bd89a96b1ca4fe0b49b2..884dc41b0fa58eccff8cde148f13e42dab892b1e 100644 --- a/modules/draggableCircleItem.py +++ b/modules/draggableCircleItem.py @@ -1,9 +1,16 @@ -from PyQt5.QtWidgets import QGraphicsEllipseItem -from PyQt5.QtGui import QPen, QBrush +from PyQt5.QtWidgets import QGraphicsEllipseItem, QGraphicsItem +from PyQt5.QtGui import QPen, QBrush, QColor from PyQt5.QtCore import Qt +from typing import Optional class DraggableCircleItem(QGraphicsEllipseItem): - def __init__(self, x, y, radius=20, color=Qt.red, parent=None): + """ + A QGraphicsEllipseItem that can be dragged around. + """ + def __init__(self, x: float, y: float, radius: float = 20, color: QColor = Qt.red, parent: Optional[QGraphicsItem] = None): + """ + Constructor. + """ super().__init__(0, 0, 2*radius, 2*radius, parent) self._r = radius @@ -20,7 +27,10 @@ class DraggableCircleItem(QGraphicsEllipseItem): # Position so that (x, y) is the center self.setPos(x - radius, y - radius) - def set_radius(self, r): + def set_radius(self, r: float): + """ + Set the radius of the circle + """ old_center = self.sceneBoundingRect().center() self._r = r self.setRect(0, 0, 2*r, 2*r) @@ -30,4 +40,7 @@ class DraggableCircleItem(QGraphicsEllipseItem): self.moveBy(diff_x, diff_y) def radius(self): + """ + Get the radius of the circle + """ return self._r \ No newline at end of file