diff --git a/cellpose/gui/gui.py b/cellpose/gui/gui.py
index 8aa8677b..ff3ae5b6 100644
--- a/cellpose/gui/gui.py
+++ b/cellpose/gui/gui.py
@@ -54,8 +54,6 @@ def __init__(self, parent, name, color):
""")
self.show()
-
-
def levelChanged(self, parent):
parent.level_change(self.name)
@@ -147,6 +145,15 @@ def make_cmap(cm=0):
return cmap
def rgb_to_hex(rgb_tuple):
+ """
+ Converts an RGB tuple to a hex color string.
+
+ Args:
+ rgb_tuple (tuple): The RGB tuple (e.g., (255, 0, 0) for red).
+
+ Returns:
+ str: The hex color string (e.g., '#ff0000' for red).
+ """
return '#{:02x}{:02x}{:02x}'.format(rgb_tuple[0], rgb_tuple[1], rgb_tuple[2])
@@ -308,6 +315,9 @@ def __init__(self, image=None, logger=None):
self.reset()
self.minimap_window_instance = None
+ # if the view of the image is changed, the method onViewChanged is called
+ self.p0.sigRangeChanged.connect(self.onViewChanged)
+
# Custom multi-page tiff image stack
self.grayscale_image_stack = []
self.colors_stack = []
@@ -364,8 +374,6 @@ def minimap_window(self):
self.minimap_window_instance.deleteLater()
self.minimap_window_instance = None
-
-
def color_initialization(self):
"""
Initializes the color stack by assigning every layer a standard color by repeating colors in the list.
@@ -514,6 +522,55 @@ def center_view_on_position(self, normalized_x, normalized_y):
self.p0.setXRange(*new_x_range, padding=0)
self.p0.setYRange(*new_y_range, padding=0)
+ def onViewChanged(self):
+ """
+ This function is called whenever the view of the image in the view box is changed.
+ This includes it being zoomed in or zoomed out, as well as it being moved around.
+ It normalizes coordinates and dimensions of the view box in relation to the current image.
+
+ Returns:
+ Normalized coordinates of the view box (normalized_x, normalized_y)
+ Normalized dimensions of the view box (normalized_width, normalized_height)
+ """
+
+ # Access the positional values of the view box p0 in form of a rectangle using viewRect()
+ view_rect = self.p0.viewRect()
+
+ # Extract the x and y coordinates of the view box
+ x_coordinates = [view_rect.left(), view_rect.right()]
+ y_coordinates = [view_rect.top(), view_rect.bottom()]
+
+ # Extract the dimensions of the view box
+ width = view_rect.width()
+ height = view_rect.height()
+
+ try:
+
+ # Get the size of the image
+ img_height = self.img.image.shape[0]
+ img_width = self.img.image.shape[1]
+
+ # Calculate the normalized coordinates in relation to the image size
+ normalized_x = tuple(
+ coordinate / img_width for coordinate in x_coordinates)
+ normalized_y = tuple(
+ coordinate / img_height for coordinate in y_coordinates)
+
+ # Calculate the normalized dimensions
+ normalized_width = width / img_width
+ normalized_height = height / img_height
+
+ # Set the highlight area in the minimap window
+ self.minimap_window_instance.set_highlight_area(normalized_x[0], normalized_y[0], normalized_width, normalized_height)
+
+ return normalized_x, normalized_y, normalized_width, normalized_height
+
+ except Exception as e:
+
+ # if an exception of any kind occurs, the specific exception is printed to the console
+ print(f"An error occurred while changing the view: {e}")
+
+
def generate_multi_channel_ui(self, n):
c = 0 # Position der Elemente im Layout
@@ -617,25 +674,32 @@ def make_buttons(self):
self.autobtn.setChecked(True)
self.satBoxG.addWidget(self.autobtn, b0, 1, 1, 8)
-
+ #--- Initialization of Non-Tiff cases ---#
c = 0 # position of the elements in the right side menu
+ # Define color names and labels for non-TIFF images
+ colornames = ["red", "Chartreuse", "DodgerBlue"]
+ names = ["red", "green", "blue"]
+ colors = ["red", "green", "blue"]
+
+ # Initialize sliders list
self.sliders = []
# ---Create a list (extendable) of color/on-off buttons ---#
- colors = ["red", "green", "blue"]
# self.marker_buttons = [self.create_color_button(color, None) for color in colors]
# self.on_off_buttons = [self.create_on_off_button(i) for i in range self.]
+ # Add labels and sliders for non-TIFF images
for r in range(3):
c += 1
- label = QLabel(f'Marker {r + 1}') # create a label for each marker
- # color_button = self.marker_buttons[r] # get the corresponding color button
- self.marker_buttons = [self.create_color_button(color, None) for color in colors]
- # on_off_button = self.on_off_buttons[r] # get the corresponding on-off button
- label.setStyleSheet("color: white")
- label.setFont(self.boldmedfont)
+ # Create a label for each color channel
+ if r == 0:
+ label = QLabel('gray/
red')
+ else:
+ label = QLabel(names[r] + ":")
+ label.setStyleSheet(f"color: {colornames[r]}")
self.rightBoxLayout.addWidget(label, c, 0, 1, 1)
+
# self.rightBoxLayout.addWidget(color_button, c, 9, 1, 1) # add the color button to the layout
# self.rightBoxLayout.addWidget(on_off_button, c, 10, 1, 1) # add the on-off button to the layout
self.sliders.append(Slider(self, colors[r], None))
@@ -1230,7 +1294,7 @@ def get_color_button_style(self, color_name):
height: 12px;
width: 12px;
}}
- """
+ """
def set_image_opacity(self, image, opacity):
@@ -1298,12 +1362,12 @@ def adjust_channel_bounds(self, channel, bounds):
def level_change(self, r):
- if self.tiff_loaded:
- if int(r) < len(self.sliders):
+ if self.tiff_loaded: # if tiff is loaded, sliders adjust contrast of each layer
+ if int(r) < len(self.sliders): # make sure the list of sliders already filled
r_index = r
if self.on_off_buttons[r].isChecked():
- self.adjust_channel_bounds(r_index, self.sliders[r_index].value())
- self.update_plot()
+ self.adjust_channel_bounds(r_index, self.sliders[r_index].value()) # adjust contrast of layer
+ self.update_plot() # update plot
else:
r = ["red", "green", "blue"].index(r)
@@ -1504,6 +1568,7 @@ def disable_buttons_removeROIs(self):
self.saveSet.setEnabled(False)
self.savePNG.setEnabled(False)
self.saveFlows.setEnabled(False)
+ self.saveFeaturesCsv.setEnabled(False)
self.saveOutlines.setEnabled(False)
self.saveROIs.setEnabled(False)
self.minimapWindow.setEnabled(False)
@@ -1532,6 +1597,26 @@ def toggle_saving(self):
self.saveOutlines.setEnabled(False)
self.saveROIs.setEnabled(False)
+ def toggle_save_features_csv(self):
+ """
+ Toggles the save features csv button based on the image file type.
+ If the image is a tiff or tif file, the button is enabled. Otherwise, it is disabled.
+ This method is called after a segmentation has taken place.
+
+ Args:
+ None
+
+ Returns:
+ None
+ """
+
+ filetype = imghdr.what(self.filename)
+
+ if filetype in ['tiff', 'tif']:
+ self.saveFeaturesCsv.setEnabled(True)
+ else:
+ self.saveFeaturesCsv.setEnabled(False)
+
def toggle_removals(self):
if self.ncells > 0:
self.ClearButton.setEnabled(True)
diff --git a/cellpose/gui/guiparts.py b/cellpose/gui/guiparts.py
index c6ad4389..12223de1 100644
--- a/cellpose/gui/guiparts.py
+++ b/cellpose/gui/guiparts.py
@@ -451,6 +451,20 @@ def __init__(self, parent=None):
# This marks the menu button as checked
parent.minimapWindow.setChecked(True)
+ # Create and add a highlight rectangle to the minimap with initial position [0, 0] and size [100, 100], outlined
+ # in white with a 3-pixel width.
+ self.highlight_area = pg.RectROI([0, 0], [100, 100], pen=pg.mkPen('w', width=3), resizable=False, movable=False)
+ self.highlight_area.hoverEvent = lambda event: None
+ # Remove all resize handles after initialization
+ QtCore.QTimer.singleShot(0, lambda: [self.highlight_area.removeHandle(handle) for handle in
+ self.highlight_area.getHandles()])
+
+
+ # Set the highlight area to cover the entire image by default
+ self.set_highlight_area(0, 0, 1, 1)
+ # Add the highlight area to the viewbox
+ self.viewbox.addItem(self.highlight_area)
+
def closeEvent(self, event: QEvent):
"""
Method to uncheck the button in the menu if the window is closed.
@@ -501,6 +515,45 @@ def update_minimap(self, parent):
else:
self.setFixedSize(self.minimapSize, self.minimapSize)
+ def set_highlight_area(self, normalized_x, normalized_y, normalized_width, normalized_height):
+ """
+ Method to set the highlight area on the minimap.
+ The position and size of the rectangle are set based on the calculated normalized coordinates from the
+ onViewChanged method.
+
+ Parameters:
+ normalized_x (float): Normalized x-coordinate for the position.
+ normalized_y (float): Normalized y-coordinate for the position.
+ normalized_width (float): Normalized width for the size.
+ normalized_height (float): Normalized height for the size.
+
+ Returns:
+ tuple: The calculated (x, y, width, height) coordinates.
+ """
+
+ if self.parent().img.image is not None:
+ # Retrieve the height and width of the image
+ img_height = self.parent().img.image.shape[0]
+ img_width = self.parent().img.image.shape[1]
+
+ # Calculate the position and size of the highlight area based on the normalized coordinates
+ x = normalized_x * img_width
+ y = normalized_y * img_height
+ width = normalized_width * img_width
+ height = normalized_height * img_height
+
+ # Set the position of the rectangle area on the minimap
+ # Move the rectangle to the calculated position
+ self.highlight_area.setPos(x, y)
+
+ # Set the size of the rectangle on the minimap
+ # Adjust the rectangle's size to the calculated width and height
+ self.highlight_area.setSize([width, height])
+
+ else:
+ print("Error: No image loaded in parent.")
+
+
def sliderValueChanged(self, value):
"""
Method to change the size of the minimap based on the slider value.
diff --git a/cellpose/gui/io.py b/cellpose/gui/io.py
index 20718c6d..3ed61b6b 100644
--- a/cellpose/gui/io.py
+++ b/cellpose/gui/io.py
@@ -175,6 +175,8 @@ def _load_image(parent, filename=None, load_seg=True, load_3D=False):
parent.minimap_window_instance = guiparts.MinimapWindow(parent)
parent.minimap_window_instance.show()
+ parent.saveFeaturesCsv.setEnabled(False)
+
def _initialize_images(parent, image, load_3D=False):
""" format image for GUI """
@@ -503,6 +505,8 @@ def _load_masks(parent, filename=None):
if parent.ncells > 0:
parent.draw_layer()
parent.toggle_mask_ops()
+ # features can only be saved if masks are loaded
+ parent.toggle_save_features_csv()
del masks
gc.collect()
parent.update_layer()
@@ -582,6 +586,8 @@ def _masks_to_gui(parent, masks, outlines=None, colors=None):
if parent.ncells > 0:
parent.draw_layer()
parent.toggle_mask_ops()
+ # features can only be saved if masks are loaded
+ parent.toggle_save_features_csv()
parent.ismanual = np.zeros(parent.ncells, bool)
parent.zdraw = list(-1 * np.ones(parent.ncells, np.int16))
@@ -591,6 +597,7 @@ def _masks_to_gui(parent, masks, outlines=None, colors=None):
else:
parent.ViewDropDown.setCurrentIndex(0)
+
def _save_features_csv(parent):
"""
Saves features to CSV if dataset is 2D.
@@ -605,11 +612,18 @@ def _save_features_csv(parent):
return
filename = parent.filename
+
base = os.path.splitext(filename)[0] + "_features.csv"
+
# check if the dataset is 2D (NZ == 1 implies a single z-layer)
if parent.NZ == 1:
print("GUI_INFO: saving features to CSV file")
- save_features_csv(parent.filename)
+ # this gives us the channels that are currently loaded by converting the images to an array
+ stacked_images = parent.convert_images_to_array(parent.grayscale_image_stack)
+ # reduction to only the relevant light intensity channel
+ channels = stacked_images[:, :, :, 1]
+ # save the features to a CSV file using method in cellpose.io
+ save_features_csv(parent.filename, parent.cellpix, channels)
else:
print("ERROR: cannot save features")
diff --git a/cellpose/gui/menus.py b/cellpose/gui/menus.py
index 74d9fe0a..5af6702b 100644
--- a/cellpose/gui/menus.py
+++ b/cellpose/gui/menus.py
@@ -81,12 +81,13 @@ def mainmenu(parent):
This adds a new menu item for saving features as a .csv file.
The user can activate this function to export specific data directly from the GUI.
The function `_save_features_as_csv` from the `io` module is called when the user clicks on the menu item.
+ It is disabled by default and only activated if an image is segmented and of the type tif/tiff.
"""
parent.saveFeaturesCsv = QAction("Save Features as .&csv", parent)
parent.saveFeaturesCsv.setShortcut("Ctrl+Shift+C")
parent.saveFeaturesCsv.triggered.connect(lambda: io._save_features_csv(parent))
file_menu.addAction(parent.saveFeaturesCsv)
- parent.saveFeaturesCsv.setEnabled(True)
+ parent.saveFeaturesCsv.setEnabled(False)
"""
This creates a new menu item for the minimap that the user can activate.
diff --git a/cellpose/io.py b/cellpose/io.py
index 115d3c95..2433e7e5 100644
--- a/cellpose/io.py
+++ b/cellpose/io.py
@@ -69,6 +69,7 @@ def logger_setup():
return logger, log_file
+
from . import utils, plot, transforms
@@ -561,11 +562,17 @@ def masks_flows_to_seg(images, masks, flows, file_names, diams=30., channels=Non
np.save(base + "_seg.npy", dat)
-def save_features_csv(file_name):
+def save_features_csv(file_name, cellpix, channels):
"""
- Save features to .csv file and remove if it already exists
+ This method saves the features of a segmentation to .csv file and replaces the old one if it exists.
+ It is saved in the folder containing the current image.
+ The features are the average marker intensity for each cell in each channel.
+ They are calculates in form of a matrix: Rows represent cells, columns represent channels.
+
Args:
file_name (str): Target CSV file name
+ cellpix (np.ndarray): Mask array where each cell has a unique ID
+ channels (ist of np.ndarray): List of channel images
Returns:
None
@@ -573,9 +580,55 @@ def save_features_csv(file_name):
file_name = os.path.splitext(file_name)[0] + "_cp_features.csv"
if os.path.exists(file_name):
os.remove(file_name)
+
+ # Get unique cell ids (excluding background which is assumed to be 0)
+ cell_ids = np.unique(cellpix)
+ cell_ids = cell_ids[cell_ids != 0]
+
+ # Number of cells and channels
+ num_cells = len(cell_ids)
+ num_channels = len(channels)
+
+ # Initialize feature matrix
+ features = np.zeros((num_cells, num_channels))
+
+ # Calculate features for each cell and each channel
+ for i, cell_id in enumerate(cell_ids):
+ # Create a mask for the current cell
+ cell_mask = cellpix == cell_id
+ # Returns a boolean matrix indicating the location of the current cell
+
+ # Count the number of pixels in the current cell
+ num_pixels = np.sum(cell_mask)
+
+ # Skip the cell if it has no pixels
+ if num_pixels == 0:
+ continue
+
+ # Calculate the feature for each channel
+ for j in range(num_channels):
+
+ # Sum the marker intensities for the current cell in the current channel
+ marker_intensity_sum = np.sum(channels[j] * cell_mask)
+
+ # Calculate the average marker intensity for the current cell in the current channel
+ features[i, j] = marker_intensity_sum / num_pixels
+
+ # Create row and column labels
+ row_labels = [f'cell {i + 1}' for i in range(num_cells)]
+ column_labels = [f'marker {j + 1}' for j in range(num_channels)]
+
+ # creating a csv file or clearing the existing one
with open(file_name, mode='w', newline='') as f:
- # creating an empty csv file or clearing the existing one
- pass
+
+ writer = csv.writer(f)
+
+ # Write the header
+ writer.writerow([''] + column_labels)
+
+ # Write the data
+ for i, row_label in enumerate(row_labels):
+ writer.writerow([row_label] + list(features[i]))
def save_to_png(images, masks, flows, file_names):
""" deprecated (runs io.save_masks with png=True)