Skip to content
Snippets Groups Projects
Commit 3bdab133 authored by s224361's avatar s224361
Browse files

Cleaned up modules

parents ed1aeda6 b9b92769
No related branches found
No related tags found
No related merge requests found
......@@ -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()
......
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
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)
......
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
......
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
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]))
......
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment