Skip to content

Commit

Permalink
Merge branch 'main' into 30_new_export_option_.csv
Browse files Browse the repository at this point in the history
  • Loading branch information
RukiyyeE committed Jul 22, 2024
2 parents 3df39ac + f6f9fdb commit 1db0d4c
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 4 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/pr-labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ jobs:
"25": "L",
"35": "XL",
"50": "XXL",
"80": "Too_Big"
"80": "XXXL",
"150": "Too_Big"
}
TooBig:
runs-on: ubuntu-latest
Expand Down
81 changes: 81 additions & 0 deletions cellpose/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ def __init__(self, image=None, logger=None):
self.restore = None
self.ratio = 1.
self.reset()
self.minimap_window_instance = None

# if called with image, load it
if image is not None:
Expand Down Expand Up @@ -334,6 +335,76 @@ def gui_window(self):
EG = guiparts.ExampleGUI(self)
EG.show()

def minimap_window(self):
"""
This function creates a new window that displays the minimap of the current image.
It uses the MinimapWindow class created in guiparts.py
"""
if self.minimapWindow.isChecked():
if self.minimap_window_instance is None:
self.minimap_window_instance = guiparts.MinimapWindow(self)
self.minimap_window_instance.show()
self.minimap_window_instance.raise_()
else:
if self.minimap_window_instance is not None:
self.minimap_window_instance.deleteLater()
self.minimap_window_instance = None

def minimap_closed(self):
"""
This function notices when the minimap window is closed (thanks to closeEvent
method in guiparts) and unchecks the minimap button in the menu
"""
try:
# Uncheck the minimap button when the minimap window is closed
self.minimapWindow.setChecked(False)
if hasattr(self, 'minimap'):
del self.minimap
except Exception as e:
print(f"An error occurred while closing the minimap: {e}")

def center_view_on_position(self, normalized_x, normalized_y):
"""
Centers the view on the given normalized coordinates (x, y).
This will be used to navigate using the minimap window.
The zoom level will be maintained.
Args:
normalized_x (float): Normalized x-coordinate (0.0 to 1.0).
normalized_y (float): Normalized y-coordinate (0.0 to 1.0).
"""
# Get the size of the image
img_height = self.img.image.shape[0]
img_width = self.img.image.shape[1]

# Calculate the actual pixel coordinates
target_x = normalized_x * img_width
target_y = normalized_y * img_height

# Get the current view range of the view box p0.
# This tells us which part of the image is currently displayed.
view_range = self.p0.viewRange()

# Extract the x and y ranges
x_range = view_range[0]
y_range = view_range[1]

# Calculate the current zoom level
zoom_x = (x_range[1] - x_range[0]) / img_width
zoom_y = (y_range[1] - y_range[0]) / img_height

# Calculate the width and height of the view range based on the zoom level
view_width = img_width * zoom_x
view_height = img_height * zoom_y

# Calculate the new view range, centered on the target position
new_x_range = [target_x - view_width / 2, target_x + view_width / 2]
new_y_range = [target_y - view_height / 2, target_y + view_height / 2]

# Set the new view range to the ViewBox
self.p0.setXRange(*new_x_range, padding=0)
self.p0.setYRange(*new_y_range, padding=0)


def make_buttons(self):
self.boldfont = QtGui.QFont("Arial", 11, QtGui.QFont.Bold)
self.boldmedfont = QtGui.QFont("Arial", 9, QtGui.QFont.Bold)
Expand Down Expand Up @@ -1133,6 +1204,7 @@ def enable_buttons(self):
self.SizeButton.setEnabled(True)
self.newmodel.setEnabled(True)
self.loadMasks.setEnabled(True)
self.minimapWindow.setEnabled(True)

for n in range(self.nchan):
self.sliders[n].setEnabled(True)
Expand All @@ -1158,6 +1230,7 @@ def disable_buttons_removeROIs(self):
self.saveFlows.setEnabled(False)
self.saveOutlines.setEnabled(False)
self.saveROIs.setEnabled(False)
self.minimapWindow.setEnabled(False)

self.MakeDeletionRegionButton.setEnabled(False)
self.DeleteMultipleROIButton.setEnabled(False)
Expand Down Expand Up @@ -1248,6 +1321,9 @@ def dropEvent(self, event):
else:
io._load_image(self, filename=files[0], load_seg=True, load_3D=self.load_3D)

self.minimapWindow.setChecked(True) # Check the minimap menu item
self.minimap_window()

def toggle_masks(self):
if self.MCheckBox.isChecked():
self.masksOn = True
Expand Down Expand Up @@ -1773,6 +1849,11 @@ def update_plot(self):
self.saturation[r][self.currentZ][0],
self.saturation[r][self.currentZ][1]
])

# If the channels are updated, the minimap is updated as well
if self.minimap_window_instance is not None:
self.minimap_window_instance.update_minimap(self)

self.win.show()
self.show()

Expand Down
168 changes: 165 additions & 3 deletions cellpose/gui/guiparts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"""

from qtpy import QtGui, QtCore, QtWidgets
from qtpy.QtGui import QPainter, QPixmap
from qtpy.QtWidgets import QApplication, QRadioButton, QWidget, QDialog, QButtonGroup, QSlider, QStyle, QStyleOptionSlider, QGridLayout, QPushButton, QLabel, QLineEdit, QDialogButtonBox, QComboBox, QCheckBox
from qtpy.QtGui import QFont
from qtpy.QtGui import QPainter, QPixmap, QImage, QFont
from qtpy.QtWidgets import QApplication, QRadioButton, QWidget, QDialog, QButtonGroup, QSlider, QStyle, QStyleOptionSlider, QGridLayout, QPushButton, QLabel, QLineEdit, QDialogButtonBox, QComboBox, QCheckBox, QDockWidget, QMenu, QWidgetAction
from qtpy.QtCore import QEvent
import pyqtgraph as pg
from pyqtgraph import functions as fn
from pyqtgraph import Point
Expand Down Expand Up @@ -395,6 +395,168 @@ def resizeEvent(self, event):
super().resizeEvent(event)


# window displaying a minimap of the current image
class MinimapWindow(QDialog):
"""
Method to initialize the Minimap Window.
It creates a title for the window and a QDialog with a basic layout.
It also takes the current picture stored in the stack and loads it in a Viewbox.
The proportions of this image stay constant.
The minimap updates with the image in the main window.
"""

def __init__(self, parent=None):
super(MinimapWindow, self).__init__(parent)
# Set the title of the window
self.title = "Minimap (click right mouse button to resize)"
self.setWindowTitle(self.title)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
# Set min, max and default size of the minimap
self.defaultSize = 400
self.minimumSize = 100
self.maximumSize = 800
self.minimapSize = self.defaultSize
self.rightClickInteraction = True

# Create a QGridLayout for the window
layout = QGridLayout()
layout.setContentsMargins(0, 0, 0, 0) # Set margins (left, top, right, bottom) to zero
self.setLayout(layout)

# Create a widget and add the layout to it
self.image_widget = pg.GraphicsLayoutWidget()
layout.addWidget(self.image_widget, 0, 0, 1, 1)

# Create custom context menu
self.createSlider()

# Create a viexbox for the minimap
self.viewbox = pg.ViewBox()
self.viewbox.setMouseEnabled(x=False, y=False)
# Invert and fix the aspect ratio of the viewbox to look like the original image
self.viewbox.invertY(True)
self.viewbox.setAspectLocked(True)

# Update the minimap so that it responds to changes in the main window
self.update_minimap(parent)
# Set the value of the slider according to the default size
self.slider.setValue(int((self.defaultSize - self.minimumSize) / self.maximumSize * 100))

# Add the viewbox to the image widget
self.image_widget.addItem(self.viewbox)

# This marks the menu button as checked
parent.minimapWindow.setChecked(True)

def closeEvent(self, event: QEvent):
"""
Method to uncheck the button in the menu if the window is closed.
It overrides closeEvent and is automatically called when the window is closed.
It calls minimap_closed from gui.py.
"""
# Notify the parent that the window is closing
self.parent().minimap_closed()
event.accept() # Accept the event and close the window

def update_minimap(self, parent):
"""
Method to update the minimap.
It takes the current image from the parent and updates the image in the minimap.
"""
if parent.img.image is not None:
# Obtain image data from the parent
image = parent.img.image
# Obtain saturation levels
levels = parent.img.levels
# Obtain current channel
current_channel = parent.color

# Set image
self.mini_image = pg.ImageItem(image)
# Display of RGB or greyscale image
if current_channel == 0 or current_channel == 4:
self.mini_image.setLookupTable(None)
# Display of image using a pre-defined colormap corresponding to a specific color channel
# (red, green, blue)
elif current_channel < 4:
self.mini_image.setLookupTable(parent.cmap[current_channel])
# Display of spectral mage
elif current_channel == 5:
self.mini_image.setLookupTable(parent.cmap[0])

self.mini_image.setLevels(levels)
self.viewbox.addItem(self.mini_image)

# Set the size of the minimap based on the aspect ratio of the image
# this ensures that the aspect ratio is always correct
aspect_ratio = self.mini_image.width() / self.mini_image.height()
self.setFixedSize(int(self.minimapSize * aspect_ratio), self.minimapSize)
# Ensure image fits viewbox
self.viewbox.setLimits(xMin=0, xMax=parent.Lx, yMin=0, yMax=parent.Ly)

# If there is no image and the minimap is checked, an empty window is opened
else:
self.setFixedSize(self.minimapSize, self.minimapSize)

def sliderValueChanged(self, value):
"""
Method to change the size of the minimap based on the slider value.
This function will be called whenever the slider's value changes
"""
# Calculate the new size of the minimap based on the slider value
upscaleFactor = ((self.maximumSize - self.minimumSize) / 100)
self.minimapSize = int(self.minimumSize + upscaleFactor * value)

# this ensures that the aspect ratio is always correct
if self.parent().img.image is not None:
aspect_ratio = self.mini_image.width() / self.mini_image.height()
self.setFixedSize(int(self.minimapSize * aspect_ratio), self.minimapSize)
else:
self.setFixedSize(self.minimapSize, self.minimapSize)

def createSlider(self):
"""
Method to create a custom context menu for the minimap. This menu contains a slider and an informative label.
"""
# Create the custom context menu
self.contextMenu = QMenu(self)

# Create a QLabel and set its text
label = QLabel()
label.setText("Adjust window size")
labelAction = QWidgetAction(self.contextMenu)
labelAction.setDefaultWidget(label)
self.contextMenu.addAction(labelAction)

# Create a QSlider and add it to the menu
self.slider = QSlider(QtCore.Qt.Horizontal)
sliderAction = QWidgetAction(self.contextMenu)
sliderAction.setDefaultWidget(self.slider)
self.contextMenu.addAction(sliderAction)

# Connect the slider's valueChanged signal to a function
self.slider.valueChanged.connect(self.sliderValueChanged)

def mousePressEvent(self, event):
"""
Method to handle mouse press events. This overrides the default mousePressEvent method. Various information
about the mouse event are passed to the method and handled accordingly. The method can distinguish between
left and right mouse button clicks. If the right mouse button is clicked, the custom context menu is displayed.
"""
# Check if the right mouse button was pressed
if event.button() == QtCore.Qt.RightButton:
# Show the custom context menu at the mouse position
self.contextMenu.exec_(event.globalPos())
# Delete hint after first interaction with the resize slider
if self.rightClickInteraction:
self.setWindowTitle("Minimap")
self.rightClickInteraction = False

else:
# If another mouse button was pressed, call the base class implementation
super().mousePressEvent(event)



class ViewBoxNoRightDrag(pg.ViewBox):

Expand Down
16 changes: 16 additions & 0 deletions cellpose/gui/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ..io import imread, imsave, outlines_to_text, add_model, remove_model, save_rois, save_settings, save_features_csv
from ..models import normalize_default, MODEL_DIR, MODEL_LIST_PATH, get_user_models
from ..utils import masks_to_outlines, outlines_list
from . import guiparts, gui

try:
import qtpy
Expand Down Expand Up @@ -138,6 +139,21 @@ def _load_image(parent, filename=None, load_seg=True, load_3D=False):
if load_mask:
_load_masks(parent, filename=mask_file)

# This calls the Minimap Window to create a new minimap, if the image is changed
if hasattr(parent, 'minimap_window_instance') and parent.minimap_window_instance is not None:
# The deleteLater() method marks the MinimapWindow object for later deletion after the current event has been processed
parent.minimap_window_instance.deleteLater()
parent.minimap_window_instance.close()
# this calls a method from gui that removes the checkmark from the minimap menu button
parent.minimap_closed()
parent.minimapWindow.setChecked(False)
# Create a new instance before calling update_image
parent.minimap_window_instance = guiparts.MinimapWindow(parent)
parent.minimap_window_instance.show()
else:
parent.minimap_window_instance = guiparts.MinimapWindow(parent)
parent.minimap_window_instance.show()


def _initialize_images(parent, image, load_3D=False):
""" format image for GUI """
Expand Down
15 changes: 15 additions & 0 deletions cellpose/gui/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def mainmenu(parent):
file_menu.addAction(parent.saveFlows)
parent.saveFlows.setEnabled(False)

# Save settings action from main

parent.saveSettings = QAction("Save Settings as .&json", parent)
parent.saveSettings.setShortcut("Ctrl+J")
parent.saveSettings.triggered.connect(lambda: io._save_settings(parent))
Expand All @@ -86,6 +88,17 @@ def mainmenu(parent):
file_menu.addAction(parent.saveFeaturesCsv)
parent.saveFeaturesCsv.setEnabled(True)

"""
This creates a new menu item for the minimap that the user can activate.
It is deactivated by default and has to be checked.
The minimap_window function is called from gui.py when the user clicks on the menu item.
"""
parent.minimapWindow = QAction("&Minimap", parent, checkable=True)
parent.minimapWindow.setChecked(False)
parent.minimapWindow.triggered.connect(parent.minimap_window)
file_menu.addAction(parent.minimapWindow)


def editmenu(parent):
main_menu = parent.menuBar()
edit_menu = main_menu.addMenu("&Edit")
Expand Down Expand Up @@ -162,3 +175,5 @@ def helpmenu(parent):
openTrainHelp = QAction("Training instructions", parent)
openTrainHelp.triggered.connect(parent.train_help_window)
help_menu.addAction(openTrainHelp)


0 comments on commit 1db0d4c

Please sign in to comment.