Skip to content
Snippets Groups Projects
Commit db70eff6 authored by s224362's avatar s224362
Browse files

merging into main

parents 67169bfd c88c0c77
No related branches found
No related tags found
No related merge requests found
Showing
with 377 additions and 0 deletions
import sys
from PyQt5.QtWidgets import QApplication
from .mainWindow import MainWindow
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
\ No newline at end of file
File added
File added
File added
File added
File added
File added
File added
File added
File added
File added
File added
File added
File added
File added
File added
from PyQt5.QtWidgets import (
QPushButton, QVBoxLayout, QWidget,
QSlider, QLabel, QGridLayout, QSizePolicy
)
from PyQt5.QtGui import QPixmap, QImage, QShowEvent
from PyQt5.QtCore import Qt
import numpy as np
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 maintain aspect ratio upon resize.
"""
def __init__(self, main_window, parent: Optional[QWidget] = None):
"""
Constructor.
"""
super().__init__(parent)
self._main_window = main_window
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)
# A small grid for controls
controls_layout = QGridLayout()
# 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)
# 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)
# 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)
self.line_smoothing_slider.setRange(3, 51)
self.line_smoothing_slider.setValue(3)
self.line_smoothing_slider.valueChanged.connect(self._on_line_smoothing_slider)
controls_layout.addWidget(self.line_smoothing_slider, 1, 1)
# Contrast slider + label
self._lab_contrast = QLabel("Contrast (0.01)")
controls_layout.addWidget(self._lab_contrast, 2, 0)
self.contrast_slider = QSlider(Qt.Horizontal)
self.contrast_slider.setRange(1, 20)
self.contrast_slider.setValue(1) # i.e. 0.01
self.contrast_slider.setSingleStep(1)
self.contrast_slider.valueChanged.connect(self._on_contrast_slider)
controls_layout.addWidget(self.contrast_slider, 2, 1)
main_layout.addLayout(controls_layout)
self.setMinimumWidth(350)
# A vertical layout for the two images, each with a label above it
images_layout = QVBoxLayout()
# 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)
self.label_contrasted_blurred = QLabel()
self.label_contrasted_blurred.setAlignment(Qt.AlignCenter)
self.label_contrasted_blurred.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
images_layout.addWidget(self.label_contrasted_blurred)
# 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)
self.label_cost_image = QLabel()
self.label_cost_image.setAlignment(Qt.AlignCenter)
self.label_cost_image.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
images_layout.addWidget(self.label_cost_image)
main_layout.addLayout(images_layout)
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: QShowEvent):
"""
Keep the images at correct aspect ratio by re-scaling
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(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.label_contrasted_blurred.setPixmap(scaled_cb)
if self._last_cost_pix is not None:
scaled_cost = self._last_cost_pix.scaled(
self.label_cost_image.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
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: 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: 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: np.ndarray, cost_img_np: np.ndarray):
"""
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)
self._last_cb_pix = cb_pix
self._last_cost_pix = cost_pix
self._update_labels()
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()
if normalize:
mn, mx = arr_.min(), arr_.max()
if abs(mx - mn) < 1e-12:
arr_[:] = 0
else:
arr_ = (arr_ - mn) / (mx - mn)
arr_ = np.clip(arr_, 0, 1)
arr_255 = (arr_ * 255).astype(np.uint8)
h, w = arr_255.shape
qimage = QImage(arr_255.data, w, h, w, QImage.Format_Grayscale8)
return QPixmap.fromImage(qimage)
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 typing import Optional
# A specialized PanZoomGraphicsView for the circle editor (disk size calibration)
class CircleEditorGraphicsView(PanZoomGraphicsView):
def __init__(self, circle_editor_widget, parent: Optional[QWidget] = None):
"""
Constructor.
"""
super().__init__(parent)
self._circle_editor_widget = circle_editor_widget
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())
if clicked_item is not None:
# climb up parent chain
it = clicked_item
while it is not None and not hasattr(it, "boundingRect"):
it = it.parentItem()
if isinstance(it, DraggableCircleItem):
# Let normal item-dragging occur, no pan
return QGraphicsView.mousePressEvent(self, event)
super().mousePressEvent(event)
def wheelEvent(self, event: QWheelEvent):
"""
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)
if item_under is not None:
it = item_under
while it is not None and not hasattr(it, "boundingRect"):
it = it.parentItem()
if isinstance(it, DraggableCircleItem):
delta = event.angleDelta().y()
step = 1 if delta > 0 else -1
old_r = it.radius()
new_r = max(1, old_r + step)
it.set_radius(new_r)
self._circle_editor_widget.update_slider_value(new_r)
event.accept()
return
super().wheelEvent(event)
\ No newline at end of file
from PyQt5.QtWidgets import (
QGraphicsScene, QGraphicsPixmapItem, QPushButton,
QHBoxLayout, QVBoxLayout, QWidget, QSlider, QLabel
)
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):
"""
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
self._init_radius = init_radius
layout = QVBoxLayout(self)
self.setLayout(layout)
# 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)
big_font.setBold(True)
label_instructions.setFont(big_font)
layout.addWidget(label_instructions)
# 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)
self._image_item = QGraphicsPixmapItem(self._pixmap)
self._scene.addItem(self._image_item)
# Put circle in center
cx = self._pixmap.width() / 2
cy = self._pixmap.height() / 2
self._circle_item = DraggableCircleItem(cx, cy, radius=self._init_radius, color=Qt.red)
self._scene.addItem(self._circle_item)
# Fit in view
self._graphics_view.setSceneRect(QRectF(self._pixmap.rect()))
self._graphics_view.fitInView(self._image_item, Qt.KeepAspectRatio)
### Controls below
bottom_layout = QHBoxLayout()
layout.addLayout(bottom_layout)
# label + slider
self._lbl_size = QLabel(f"size ({self._init_radius})")
bottom_layout.addWidget(self._lbl_size)
self._slider = QSlider(Qt.Horizontal)
self._slider.setRange(1, 200)
self._slider.setValue(self._init_radius)
bottom_layout.addWidget(self._slider)
# Done button
self._btn_done = QPushButton("Done")
bottom_layout.addWidget(self._btn_done)
# Connect signals
self._slider.valueChanged.connect(self._on_slider_changed)
self._btn_done.clicked.connect(self._on_done_clicked)
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: int):
"""
Update the slider value.
"""
self._slider.blockSignals(True)
self._slider.setValue(new_radius)
self._slider.blockSignals(False)
self._lbl_size.setText(f"size ({new_radius})")
def sizeHint(self):
return QSize(800, 600)
import numpy as np
from typing import Optional
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).
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: The circle-edge-weighted kernel.
"""
if radius is None:
# By default, let the radius be half the kernel size
radius = (k_size - 1) / 2
# Create an empty kernel
kernel = np.zeros((k_size, k_size), dtype=float)
# Coordinates of the center
center = radius # same as (k_size-1)/2 if radius is default
# Fill the kernel
for y in range(k_size):
for x in range(k_size):
dist = np.sqrt((x - center)**2 + (y - center)**2)
if dist <= radius:
# Weight = distance / radius => 0 at center, 1 at boundary
kernel[y, x] = dist / radius
return kernel
\ 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