Skip to content
Snippets Groups Projects
Commit fb980661 authored by Christian's avatar Christian
Browse files

bug fixes and layout changes

parent 56fda2e4
No related branches found
No related tags found
No related merge requests found
......@@ -14,11 +14,11 @@ from PyQt5.QtWidgets import (
from PyQt5.QtGui import QPixmap, QPen, QBrush, QColor, QFont, QImage
from PyQt5.QtCore import Qt, QRectF, QSize
# Make sure the following imports exist in live_wire.py (or similar):
# live_wire.py must contain something like:
# from skimage import exposure
# from skimage.filters import gaussian
# def preprocess_image(image, sigma=3, clip_limit=0.01): ...
# def compute_cost_image(path, user_radius, sigma=3, clip_limit=0.01): ...
from live_wire import compute_cost_image, find_path, preprocess_image
......@@ -37,9 +37,7 @@ class PanZoomGraphicsView(QGraphicsView):
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
def wheelEvent(self, event):
"""
Zoom in/out with mouse wheel.
"""
""" Zoom in/out with mouse wheel. """
zoom_in_factor = 1.25
zoom_out_factor = 1 / zoom_in_factor
if event.angleDelta().y() > 0:
......@@ -49,9 +47,7 @@ class PanZoomGraphicsView(QGraphicsView):
event.accept()
def mousePressEvent(self, event):
"""
If left button: Start panning (unless overridden in a subclass).
"""
""" If left button: Start panning (unless overridden). """
if event.button() == Qt.LeftButton:
self._panning = True
self._pan_start = event.pos()
......@@ -59,9 +55,7 @@ class PanZoomGraphicsView(QGraphicsView):
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
"""
If panning, translate the scene.
"""
""" If panning, translate the scene. """
if self._panning and self._pan_start is not None:
delta = event.pos() - self._pan_start
self._pan_start = event.pos()
......@@ -69,9 +63,7 @@ class PanZoomGraphicsView(QGraphicsView):
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
"""
End panning.
"""
""" End panning. """
if event.button() == Qt.LeftButton:
self._panning = False
self.setCursor(Qt.ArrowCursor)
......@@ -88,50 +80,41 @@ class CircleEditorGraphicsView(PanZoomGraphicsView):
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
# Check if the user clicked on the circle item
# Check if user clicked on the circle item
clicked_item = self.itemAt(event.pos())
if clicked_item is not None:
# Walk up parent chain to see if it is our DraggableCircleItem
# 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, don't initiate panning
# Let normal item-dragging occur, no pan
return QGraphicsView.mousePressEvent(self, event)
# Otherwise proceed with normal panning logic
super().mousePressEvent(event)
def wheelEvent(self, event):
"""
Overridden so that if the mouse is hovering over the circle,
we adjust the circle's radius instead of zooming the image.
If the mouse is hovering over the circle, we adjust the circle's radius
instead of zooming the image.
"""
pos_in_widget = event.pos()
item_under = self.itemAt(pos_in_widget)
if item_under is not None:
# climb up the chain to find if it's our DraggableCircleItem
it = item_under
while it is not None and not hasattr(it, "boundingRect"):
it = it.parentItem()
if isinstance(it, DraggableCircleItem):
# Scroll up -> increase radius, scroll down -> decrease
delta = event.angleDelta().y()
# each wheel "notch" is typically 120
step = 1 if delta > 0 else -1
old_r = it.radius()
new_r = max(1, old_r + step)
it.set_radius(new_r)
# Also update the slider in the parent CircleEditorWidget
self._circle_editor_widget.update_slider_value(new_r)
event.accept()
return
# else do normal pan/zoom
super().wheelEvent(event)
......@@ -157,7 +140,6 @@ class DraggableCircleItem(QGraphicsEllipseItem):
self.setPos(x - radius, y - radius)
def set_radius(self, r):
# Keep the same center, just change radius
old_center = self.sceneBoundingRect().center()
self._r = r
self.setRect(0, 0, 2*r, 2*r)
......@@ -183,7 +165,7 @@ class CircleEditorWidget(QWidget):
layout = QVBoxLayout(self)
self.setLayout(layout)
# Use specialized CircleEditorGraphicsView
# Specialized CircleEditorGraphicsView
self._graphics_view = CircleEditorGraphicsView(circle_editor_widget=self)
self._scene = QGraphicsScene(self)
self._graphics_view.setScene(self._scene)
......@@ -202,12 +184,13 @@ class CircleEditorWidget(QWidget):
self._graphics_view.setSceneRect(QRectF(self._pixmap.rect()))
self._graphics_view.fitInView(self._image_item, Qt.KeepAspectRatio)
# Bottom controls (slider + done)
# Bottom controls
bottom_layout = QHBoxLayout()
layout.addLayout(bottom_layout)
lbl = QLabel("size:")
bottom_layout.addWidget(lbl)
# We'll store the label in an attribute so we can update with the slider value
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)
......@@ -223,6 +206,7 @@ class CircleEditorWidget(QWidget):
def _on_slider_changed(self, value):
self._circle_item.set_radius(value)
self._lbl_size.setText(f"size ({value})")
def _on_done_clicked(self):
final_radius = self._circle_item.radius()
......@@ -230,13 +214,10 @@ class CircleEditorWidget(QWidget):
self._done_callback(final_radius)
def update_slider_value(self, new_radius):
"""
Called by CircleEditorGraphicsView when the user scrolls on the circle item.
We sync the slider to the new radius.
"""
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)
......@@ -357,30 +338,20 @@ class ImageGraphicsView(PanZoomGraphicsView):
self._savgol_polyorder = 1
def set_rainbow_enabled(self, enabled: bool):
"""Enable/disable rainbow mode, then rebuild the path."""
self._rainbow_enabled = enabled
self._rebuild_full_path()
def toggle_rainbow(self):
"""Flip the rainbow mode and rebuild path."""
self._rainbow_enabled = not self._rainbow_enabled
self._rebuild_full_path()
def set_savgol_window_length(self, wlen: int):
"""
Set the window length for the Savitzky-Golay filter and update polyorder
based on window_length_polyorder_ratio.
"""
# SavGol requires window_length to be odd and >= 3
if wlen < 3:
wlen = 3
if wlen % 2 == 0:
wlen += 1
self._savgol_window_length = wlen
# polyorder is nearest integer to (window_length / 7)
# but must be >= 1 and < window_length
p = round(wlen / 7.0)
p = max(1, p)
if p >= wlen:
......@@ -415,18 +386,17 @@ class ImageGraphicsView(PanZoomGraphicsView):
# ANCHOR POINTS
# --------------------------------------------------------------------
def _insert_anchor_point(self, idx, x, y, label="", removable=True, z_val=0, radius=4):
"""Insert anchor at index=idx (or -1 => before E). Clamps x,y to image bounds."""
x_clamped = self._clamp(x, radius, self._img_w - radius)
y_clamped = self._clamp(y, radius, self._img_h - radius)
if idx < 0:
# Insert before E if there's at least 2 anchors
if len(self.anchor_points) >= 2:
idx = len(self.anchor_points) - 1
else:
idx = len(self.anchor_points)
self.anchor_points.insert(idx, (x_clamped, y_clamped))
color = Qt.green if label in ("S", "E") else Qt.red
item = LabeledPointItem(x_clamped, y_clamped,
label=label, radius=radius, color=color,
......@@ -435,8 +405,9 @@ class ImageGraphicsView(PanZoomGraphicsView):
self.scene.addItem(item)
def _add_guide_point(self, x, y):
# Ensure we clamp properly
x_clamped = self._clamp(x, self.dot_radius, self._img_w - self.dot_radius)
y_clamped = self._clamp(y, self.dot_radius, self._img_w - self.dot_radius)
y_clamped = self._clamp(y, self.dot_radius, self._img_h - self.dot_radius)
self._revert_cost_to_original()
......@@ -450,10 +421,12 @@ class ImageGraphicsView(PanZoomGraphicsView):
self._rebuild_full_path()
def _insert_anchor_between_subpath(self, x_new, y_new):
# If somehow we have no path yet
if not self._full_path_xy:
self._insert_anchor_point(-1, x_new, y_new)
return
# Find nearest point in the current full path
best_idx = None
best_d2 = float('inf')
for i, (px, py) in enumerate(self._full_path_xy):
......@@ -498,14 +471,16 @@ class ImageGraphicsView(PanZoomGraphicsView):
break
iR += 1
# If we can't find distinct anchors on left & right,
# just insert before E.
if not left_anchor_pt or not right_anchor_pt:
self._insert_anchor_point(-1, x_new, y_new)
return
if left_anchor_pt == right_anchor_pt:
self._insert_anchor_point(-1, x_new, y_new)
return
# Convert anchor coords -> anchor_points indices
left_idx = None
right_idx = None
for i, (ax, ay) in enumerate(self.anchor_points):
......@@ -518,6 +493,7 @@ class ImageGraphicsView(PanZoomGraphicsView):
self._insert_anchor_point(-1, x_new, y_new)
return
# Insert between them
if left_idx < right_idx:
insert_idx = left_idx + 1
else:
......@@ -597,10 +573,9 @@ class ImageGraphicsView(PanZoomGraphicsView):
n_points = len(big_xy)
for i, (px, py) in enumerate(big_xy):
fraction = i / (n_points - 1) if n_points > 1 else 0
color = Qt.red
if self._rainbow_enabled:
color = self._rainbow_color(fraction)
else:
color = Qt.red
path_item = LabeledPointItem(px, py, label="",
radius=self.path_radius,
......@@ -630,6 +605,7 @@ class ImageGraphicsView(PanZoomGraphicsView):
except ValueError as e:
print("Error in find_path:", e)
return []
# Convert from (row, col) to (x, y)
return [(c, r) for (r, c) in path_rc]
def _rainbow_color(self, fraction):
......@@ -674,6 +650,7 @@ class ImageGraphicsView(PanZoomGraphicsView):
self.point_items[self._dragging_idx].set_pos(x_clamped, y_clamped)
self._drag_counter += 1
# Update path every 4 moves
if self._drag_counter >= 4:
self._drag_counter = 0
self._revert_cost_to_original()
......@@ -698,10 +675,8 @@ class ImageGraphicsView(PanZoomGraphicsView):
idx = self._dragging_idx
self._dragging_idx = None
self._drag_offset = (0, 0)
newX, newY = self.point_items[idx].get_pos()
self.anchor_points[idx] = (newX, newY)
self._revert_cost_to_original()
self._apply_all_guide_points_to_cost()
self._rebuild_full_path()
......@@ -788,14 +763,24 @@ class ImageGraphicsView(PanZoomGraphicsView):
# Advanced Settings Widget
# ------------------------------------------------------------------------
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.
Now displays the images stacked vertically with labels above them.
"""
def __init__(self, main_window, parent=None):
super().__init__(parent)
self._main_window = main_window # to call e.g. main_window.open_circle_editor()
self._main_window = main_window
self._last_cb_pix = None # store QPixmap for contrasted-blurred
self._last_cost_pix = None # store QPixmap for cost
main_layout = QVBoxLayout()
self.setLayout(main_layout)
# A small grid for the controls (buttons/sliders)
# A small grid for controls
controls_layout = QGridLayout()
# 1) Rainbow toggle
......@@ -809,69 +794,108 @@ class AdvancedSettingsWidget(QWidget):
controls_layout.addWidget(self.btn_circle_editor, 0, 1)
# 3) Line smoothing slider + label
lab_smoothing = QLabel("Line smoothing (SavGol window_length)")
controls_layout.addWidget(lab_smoothing, 1, 0)
self._lab_smoothing = QLabel("Line smoothing (7)")
controls_layout.addWidget(self._lab_smoothing, 1, 0)
self.line_smoothing_slider = QSlider(Qt.Horizontal)
self.line_smoothing_slider.setRange(3, 51) # allow from 3 to 51
self.line_smoothing_slider.setValue(7) # default
self.line_smoothing_slider.setRange(3, 51)
self.line_smoothing_slider.setValue(7)
self.line_smoothing_slider.valueChanged.connect(self._on_line_smoothing_slider)
controls_layout.addWidget(self.line_smoothing_slider, 1, 1)
# 4) Contrast slider + label
lab_contrast = QLabel("Contrast (clip_limit)")
controls_layout.addWidget(lab_contrast, 2, 0)
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(0, 100) # 0..100 => 0..1 with step of 0.01
self.contrast_slider.setValue(1) # default is 0.01
self.contrast_slider.setRange(0, 100)
self.contrast_slider.setValue(1) # i.e. 0.01
self.contrast_slider.valueChanged.connect(self._on_contrast_slider)
controls_layout.addWidget(self.contrast_slider, 2, 1)
main_layout.addLayout(controls_layout)
# Now a horizontal layout for the two images
images_layout = QHBoxLayout()
# 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
images_layout = QVBoxLayout()
# 1) 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)
# Contrasted blurred
self.label_contrasted_blurred = QLabel()
self.label_contrasted_blurred.setText("CONTRASTED BLURRED IMG")
self.label_contrasted_blurred.setAlignment(Qt.AlignCenter)
self.label_contrasted_blurred.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.label_contrasted_blurred.setScaledContents(True)
images_layout.addWidget(self.label_contrasted_blurred)
# Cost image
# 2) 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.setText("Current COST IMAGE")
self.label_cost_image.setAlignment(Qt.AlignCenter)
self.label_cost_image.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.label_cost_image.setScaledContents(True)
images_layout.addWidget(self.label_cost_image)
main_layout.addLayout(images_layout)
def showEvent(self, event):
""" When shown, ask parent to resize to accommodate. """
super().showEvent(event)
if self.parentWidget():
self.parentWidget().adjustSize()
def resizeEvent(self, event):
"""
Keep the images at correct aspect ratio by re-scaling
our stored pixmaps to the new label sizes.
"""
super().resizeEvent(event)
self._update_labels()
def _update_labels(self):
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):
self._main_window.toggle_rainbow()
def _on_line_smoothing_slider(self, value):
self._lab_smoothing.setText(f"Line smoothing ({value})")
self._main_window.image_view.set_savgol_window_length(value)
def _on_contrast_slider(self, value):
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):
"""
Called by main_window to refresh the two images in the advanced panel.
contrasted_img_np = the grayscaled blurred+contrasted image as float or 0-1 array
cost_img_np = the current cost image (numpy array)
We'll store them as QPixmaps, then do the re-scale in _update_labels().
"""
cb_pix = self._np_array_to_qpixmap(contrasted_img_np)
cost_pix = self._np_array_to_qpixmap(cost_img_np, normalize=True)
if cb_pix is not None:
self.label_contrasted_blurred.setPixmap(cb_pix)
if cost_pix is not None:
self.label_cost_image.setPixmap(cost_pix)
self._last_cb_pix = cb_pix
self._last_cost_pix = cost_pix
self._update_labels()
def _np_array_to_qpixmap(self, arr, normalize=False):
if arr is None:
......@@ -908,8 +932,8 @@ class MainWindow(QMainWindow):
# Outer widget + layout
self._main_widget = QWidget()
self._main_layout = QHBoxLayout(self._main_widget) # horizontal so we can place advanced on the right
self._left_panel = QVBoxLayout() # for the image & row of buttons
self._main_layout = QHBoxLayout(self._main_widget)
self._left_panel = QVBoxLayout()
self._main_layout.addLayout(self._left_panel)
self.setCentralWidget(self._main_widget)
......@@ -931,7 +955,7 @@ class MainWindow(QMainWindow):
self.btn_clear_points.clicked.connect(self.clear_points)
btn_layout.addWidget(self.btn_clear_points)
# "Advanced Settings" button
# "Advanced Settings" toggle
self.btn_advanced = QPushButton("Advanced Settings")
self.btn_advanced.setCheckable(True)
self.btn_advanced.clicked.connect(self._toggle_advanced_settings)
......@@ -939,7 +963,7 @@ class MainWindow(QMainWindow):
self._left_panel.addLayout(btn_layout)
# Create advanced settings widget (hidden by default)
# Advanced settings widget
self._advanced_widget = AdvancedSettingsWidget(self)
self._advanced_widget.hide()
self._main_layout.addWidget(self._advanced_widget)
......@@ -953,11 +977,10 @@ class MainWindow(QMainWindow):
self._advanced_widget.show()
else:
self._advanced_widget.hide()
# Ask Qt to re-layout the window so it can expand/shrink as needed:
self.adjustSize()
def open_circle_editor(self):
"""Removes the current central widget, replaces with circle editor."""
""" Replace central widget with circle editor. """
if not self._last_loaded_pixmap:
print("No image loaded yet! Cannot open circle editor.")
return
......@@ -972,7 +995,6 @@ class MainWindow(QMainWindow):
done_callback=self._on_circle_editor_done
)
self._editor = editor
self.setCentralWidget(editor)
def _on_circle_editor_done(self, final_radius):
......@@ -1049,7 +1071,6 @@ class MainWindow(QMainWindow):
def _update_advanced_images(self):
if not self._last_loaded_pixmap:
return
pm_np = self._qpixmap_to_gray_float(self._last_loaded_pixmap)
contrasted_blurred = preprocess_image(
pm_np,
......
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