Skip to content

Commit

Permalink
Draw shapes + minor changes
Browse files Browse the repository at this point in the history
- Add drawing of line, rectangle and ellipse to images (in monitoring and in scan of images)
- Add checkbox to disable locked aspect ratio for images
- Disable autolevels and autorange for images

- Now don't change the parameter name if set a new parameter with the same name
- Prevent changing the recipe during a scan by using ctrl+z ctrl+y
- Change default QTableView to pyqtgraph version for more options (slower to render but should be fine in most cases < 1000 points)
- Change progression bar of scan to red if stop a scan
  • Loading branch information
Python-simulation committed Nov 8, 2024
1 parent a33c5db commit fefe2f7
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 19 deletions.
127 changes: 127 additions & 0 deletions autolab/core/gui/GUI_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,41 @@ def __init__(self, *args, **kwargs):
# update slice when change frame number in scanner
self.timeLine.sigPositionChanged.connect(self.updateLineROI)

# Create a checkbox for aspect ratio
self.aspect_ratio_checkbox = QtWidgets.QCheckBox("Lock aspect ratio")
self.aspect_ratio_checkbox.setChecked(True)
self.aspect_ratio_checkbox.stateChanged.connect(self.toggle_aspect_ratio)

# Store shapes in a list with visibility and remove information
self.shapes = []

# Create menu bar
shape_toolbutton = QtWidgets.QToolButton(self)
shape_toolbutton.setText("Shapes ")
shape_toolbutton.setPopupMode(QtWidgets.QToolButton.InstantPopup)

menu_bar = QtWidgets.QMenu(self)
shape_toolbutton.setMenu(menu_bar)

add_menu = menu_bar.addMenu("Add")

add_line_action = QtWidgets.QAction("Line", add_menu)
add_line_action.triggered.connect(self.add_line)
add_menu.addAction(add_line_action)

add_rectangle_action = QtWidgets.QAction("Rectangle", add_menu)
add_rectangle_action.triggered.connect(self.add_rectangle)
add_menu.addAction(add_rectangle_action)

add_ellipse_action = QtWidgets.QAction("Ellipse", add_menu)
add_ellipse_action.triggered.connect(self.add_ellipse)
add_menu.addAction(add_ellipse_action)

# Create a container for shape visibility toggles and delete buttons
self.shape_properties_menu = menu_bar.addMenu("Properties")

self.update_shape_properties_menu() # to create the 'All shapes' menu

slice_pushButton = QtWidgets.QPushButton('Slice')
slice_pushButton.state = False
slice_pushButton.setMinimumSize(0, 23)
Expand All @@ -262,6 +297,8 @@ def __init__(self, *args, **kwargs):
horizontalLayoutButton = QtWidgets.QHBoxLayout()
horizontalLayoutButton.setSpacing(0)
horizontalLayoutButton.setContentsMargins(0,0,0,0)
horizontalLayoutButton.addWidget(self.aspect_ratio_checkbox)
horizontalLayoutButton.addWidget(shape_toolbutton)
horizontalLayoutButton.addStretch()
horizontalLayoutButton.addWidget(self.slice_pushButton)

Expand Down Expand Up @@ -311,6 +348,10 @@ def update_ticks(self):
tick.currentPen = tick.pen
tick.hoverPen = pg.mkPen(200, 120, 0)

def toggle_aspect_ratio(self):
self.getView().setAspectLocked(
self.aspect_ratio_checkbox.isChecked())

def slice_pushButtonClicked(self):
self.slice_pushButton.state = not self.slice_pushButton.state
self.display_line()
Expand Down Expand Up @@ -348,8 +389,94 @@ def updateLineROI(self):
d2 = self.lineROI.getArrayRegion(img, self.imageItem, axes=(x+1, y+1))
self.plot.setData(d2[0])

def add_line(self):
position_point = ((10, 10), (50, 10)) # Start and end position
center_point = (0, 0) # center point (x, y)

line = pg.LineSegmentROI(position_point, center_point)
line.setPen(pg.mkPen(QtGui.QColor(0, 255, 0), width=2)) # green

self.addItem(line)
shape_info = {'shape': line, 'name': 'Line', 'visible': True}
self.shapes.append(shape_info)

self.add_visibility_checkbox(shape_info)

def add_rectangle(self):
rect = pg.RectROI([10, 20], [30, 20]) # Position (x, y), size (width, height)
rect.setPen(pg.mkPen(QtGui.QColor(255, 0, 0), width=2)) # red

rect.addRotateHandle([0, 0], [0.5, 0.5]) # position at top-left corner, center at center of ROI

self.addItem(rect)
shape_info = {'shape': rect, 'name': 'Rectangle', 'visible': True}
self.shapes.append(shape_info)

self.add_visibility_checkbox(shape_info)

def add_ellipse(self):
ellipse = pg.EllipseROI([10, 50], [20, 20]) # Position (x, y), size (width, height)
ellipse.setPen(pg.mkPen(QtGui.QColor(0, 0, 255), width=2)) # blue

self.addItem(ellipse)
shape_info = {'shape': ellipse, 'name': 'Ellipse', 'visible': True}
self.shapes.append(shape_info)

self.add_visibility_checkbox(shape_info)

def add_visibility_checkbox(self, shape_info: dict):
"""Add a checkbox and delete button to toggle visibility and delete shapes."""
def toggle_visibility(checked: bool):
shape_info['shape'].setVisible(checked)

shape_menu = QtWidgets.QMenu(shape_info['name'], self)

visibility_action = shape_menu.addAction(f"Show {shape_info['name']}")
visibility_action.setCheckable(True)
visibility_action.setChecked(shape_info['visible'])
visibility_action.triggered.connect(toggle_visibility)

delete_action = shape_menu.addAction(f"Delete {shape_info['name']}")
delete_action.triggered.connect(lambda: self.delete_shape(shape_info))

self.shape_properties_menu.addMenu(shape_menu)

def delete_shape(self, shape_info: dict):
"""Delete a shape from the scene and update the shapes list and visibility menu."""
self.getView().scene().removeItem(shape_info['shape'])
self.shapes = [s for s in self.shapes if s != shape_info] # Update the shapes list to remove the shape

self.update_shape_properties_menu()

def delete_all_shapes(self):
for shape_info in self.shapes:
self.delete_shape(shape_info)

def toggle_all_visibility(self, checked: bool):
for shape_info in self.shapes:
shape_info['shape'].setVisible(checked)

def update_shape_properties_menu(self):
"""Update the visibility menu to show only existing shapes."""
self.shape_properties_menu.clear() # Clear old entries

all_menu = self.shape_properties_menu.addMenu('All')
all_show = all_menu.addAction('Show')
all_show.setCheckable(True)
all_show.setChecked(True)
all_show.triggered.connect(
lambda: self.toggle_all_visibility(all_show.isChecked()))
all_delete = all_menu.addAction('Delete')
all_delete.triggered.connect(self.delete_all_shapes)

self.shape_properties_menu.addSeparator()

for shape_info in self.shapes:
self.add_visibility_checkbox(shape_info)

def close(self):
self.figLineROI.deleteLater()
self.delete_all_shapes()
super().close()


Expand Down
3 changes: 2 additions & 1 deletion autolab/core/gui/monitoring/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ def __init__(self, gui: QtWidgets.QMainWindow):
def update(self, xlist: list, ylist: list) -> None:
""" This function update the figure in the GUI """
if xlist is None: # image
self.figMap.setImage(ylist, autoRange=False, autoLevels=False, autoHistogramRange=False)
if self.fig.isVisible():
self.fig.hide()
self.gui.min_checkBox.hide()
self.gui.mean_checkBox.hide()
self.gui.max_checkBox.hide()
self.figMap.show()
self.figMap.setImage(ylist)
self.figMap.autoLevels() # Only set levels for first acquisition (others are autoLevels disabled)
return None

if not self.fig.isVisible():
Expand Down
19 changes: 12 additions & 7 deletions autolab/core/gui/scanning/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ def addNewConfig(self):
self.gui.undo.setEnabled(True)
self.gui.redo.setEnabled(False)


def addRecipe(self, recipe_name: str):
""" Adds new recipe to config """
if not self.gui.scanManager.isStarted():
Expand Down Expand Up @@ -364,8 +363,12 @@ def setParameter(self, recipe_name: str, param_name: str,
param = self.getParameter(recipe_name, param_name)

param['element'] = element
if newName is None: newName = self.getUniqueName(recipe_name,
element.name)
if newName is None:
if param_name == element.name:
newName = param_name
else:
newName = self.getUniqueName(
recipe_name, element.name)
param['name'] = newName
self.gui._refreshParameterRange(recipe_name, param_name, newName)
self.addNewConfig()
Expand Down Expand Up @@ -1153,13 +1156,15 @@ def load_configPars(self, configPars: dict, append: bool = False):

def undoClicked(self):
""" Undos an action from parameter, recipe or range """
self.configHistory.go_down()
self.changeConfig()
if not self.gui.scanManager.isStarted():
self.configHistory.go_down()
self.changeConfig()

def redoClicked(self):
""" Redos an action from parameter, recipe or range """
self.configHistory.go_up()
self.changeConfig()
if not self.gui.scanManager.isStarted():
self.configHistory.go_up()
self.changeConfig()

def changeConfig(self):
""" Gets config from history and enables/disables undo/redo button accordingly """
Expand Down
16 changes: 9 additions & 7 deletions autolab/core/gui/scanning/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
"""
import pandas as pd
from qtpy import QtCore, QtWidgets
# import pyqtgraph as pg
# TODO: need to choose between fast (QTableView+TableModel) or fancy (pg.TableWidget)
import pyqtgraph as pg


class DisplayValues(QtWidgets.QWidget):
Expand All @@ -24,18 +23,20 @@ def __init__(self, name: str, size: QtCore.QSize = (250, 400)):

self.setWindowTitle(name)
self.resize(size)
self.tableView = QtWidgets.QTableView() # too basic (can't save/copy data)
# self.tableView = pg.TableWidget(sortable=False) # too slow
# I choose pg.TableWidget to have more features at the cost of slower execution than QTableView+TableModel
# self.tableView = QtWidgets.QTableView() # basic (can't save/copy data)
self.tableView = pg.TableWidget() # slow
self.tableView.setFormat('%.16g')

layoutWindow = QtWidgets.QVBoxLayout()
layoutWindow.addWidget(self.tableView)
self.setLayout(layoutWindow)

def refresh(self, data: pd.DataFrame):
""" data is pd.DataFrame """
tableModel = TableModel(data)
self.tableView.setModel(tableModel)
# self.tableView.setData(data.to_dict('records'))
# tableModel = TableModel(data)
# self.tableView.setModel(tableModel)
self.tableView.setData(data.to_dict('records'))

def show(self):
if not self.active:
Expand All @@ -55,6 +56,7 @@ def close(self):
super().close()


# OBSOLETE
class TableModel(QtCore.QAbstractTableModel):
"From https://www.pythonguis.com/tutorials/pyqt6-qtableview-modelviews-numpy-pandas/"
def __init__(self, data: pd.DataFrame):
Expand Down
7 changes: 4 additions & 3 deletions autolab/core/gui/scanning/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def start(self):
self.gui.pause_pushButton.setEnabled(True)
self.gui.clear_pushButton.setEnabled(False)
self.gui.progressBar.setValue(0)
self.gui.progressBar.setStyleSheet("")
self.gui.importAction.setEnabled(False)
self.gui.openRecentMenu.setEnabled(False)
self.gui.undo.setEnabled(False)
Expand Down Expand Up @@ -208,15 +209,16 @@ def handler_user_input(self, stepInfos: dict):
def scanCompleted(self):
if not qt_object_exists(self.gui.progressBar):
return None
self.gui.progressBar.setStyleSheet("")

if self.thread.stopFlag.is_set():
pass
self.gui.progressBar.setStyleSheet(
"QProgressBar::chunk {background-color: red;}")
# self.gui.setStatus('Scan stopped!', 5000) # not good because hide error message
else:
self.gui.setStatus('Scan finished!', 5000)
self.gui.progressBar.setMaximum(1)
self.gui.progressBar.setValue(1)
self.gui.progressBar.setStyleSheet("")

# Start monitors if option selected in monitors
for var_id in set([id(step['element'])
Expand Down Expand Up @@ -249,7 +251,6 @@ def stop(self):
if self.main_dialog and qt_object_exists(self.main_dialog):
self.main_dialog.deleteLater()
self.thread.wait()

# SIGNALS
#############################################################################

Expand Down
2 changes: 1 addition & 1 deletion autolab/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.0
2.0.1

0 comments on commit fefe2f7

Please sign in to comment.