diff --git a/cellpose/gui/gui.py b/cellpose/gui/gui.py
index fabcd9c5..b6dfa06d 100644
--- a/cellpose/gui/gui.py
+++ b/cellpose/gui/gui.py
@@ -37,17 +37,20 @@ class Slider(QRangeSlider):
def __init__(self, parent, name, color):
super().__init__(Horizontal)
self.setEnabled(False)
- self.valueChanged.connect(lambda: self.levelChanged(parent))
self.name = name
self.setStyleSheet(""" QSlider{
background-color: transparent;
}
""")
+ self.setMinimum(-.1)
+ self.setMaximum(255.1)
+ self.setValue([0, 255])
+ self.setToolTip(
+ "NOTE: manually changing the saturation bars does not affect normalization in segmentation"
+ )
self.show()
- def levelChanged(self, parent):
- parent.level_change(self.name)
class QHLine(QFrame):
@@ -328,24 +331,24 @@ def make_buttons(self):
self.l0.addWidget(self.satBox, b, 0, 1, 9)
widget_row = 0
- self.RGBDropDown = QComboBox()
+ # self.RGBDropDown = QComboBox()
# This is duplication and in the future these things should be tied
# together in a class:
- self.RGBDropDown.addItems(
- ["RGB", "red=R", "green=G", "blue=B", "gray", "spectral"])
- self.RGBDropDown.name_map = {
- "rgb" : "rgb",
- "red" : "red=r",
- "green" : "green=g",
- "blue" : "blue=b",
- "gray" : "gray",
- "spectral": "spectral",
- }
- self.RGBDropDown.setFont(self.medfont)
- self.RGBDropDown.currentIndexChanged.connect(self.color_choose)
- self.satBoxG.addWidget(self.RGBDropDown, widget_row, 0, 1, 3)
- self.color = 'RGB' # 0=RGB, 1=gray, 2=R, 3=G, 4=B
+ # self.RGBDropDown.addItems(
+ # ["Ch1", "Ch2", "Ch3"])
+ # self.RGBDropDown.name_map = {
+ # "rgb" : "rgb",
+ # "red" : "red=r",
+ # "green" : "green=g",
+ # "blue" : "blue=b",
+ # "gray" : "gray",
+ # "spectral": "spectral",
+ # }
+ # self.RGBDropDown.setFont(self.medfont)
+ # self.RGBDropDown.currentIndexChanged.connect(self.color_choose)
+ # self.satBoxG.addWidget(self.RGBDropDown, widget_row, 0, 1, 3)
+ # self.color = 'RGB' # 0=RGB, 1=gray, 2=R, 3=G, 4=B
label = QLabel("
[↑ / ↓ or W/S]
")
label.setFont(self.smallfont)
@@ -383,24 +386,20 @@ def make_buttons(self):
widget_row += 1
self.sliders = []
colors = [[255, 0, 0], [0, 255, 0], [0, 0, 255], [100, 100, 100]]
- colornames = ["red", "Chartreuse", "DodgerBlue"]
- names = ["red", "green", "blue"]
+ colornames = ["red", "green", "blue"]
+ self.labels: dict[str, ClickableColorLabel] = {}
for r in range(3):
widget_row += 1
- if r == 0:
- label = QLabel('gray/
red')
- else:
- label = QLabel(names[r] + ":")
- label.setStyleSheet(f"color: {colornames[r]}")
+ name = "Ch" + str(r+1)
+ label = ClickableColorLabel("Ch" + str(r+1) + ":", color_name=colornames[r])
+ label.sigColorMapChoose.connect(lambda cmap, idx=r: self.set_cmap(idx, cmap))
+ label.sigEnableToggled.connect(self.update_plot)
label.setFont(self.boldmedfont)
+ self.labels[name.lower()] = label
self.satBoxG.addWidget(label, widget_row, 0, 1, 2)
- self.sliders.append(Slider(self, names[r], colors[r]))
- self.sliders[-1].setMinimum(-.1)
- self.sliders[-1].setMaximum(255.1)
- self.sliders[-1].setValue([0, 255])
- self.sliders[-1].setToolTip(
- "NOTE: manually changing the saturation bars does not affect normalization in segmentation"
- )
+ slider = Slider(self, name, colors[r])
+ slider.valueChanged.connect(lambda lvl, idx=r: self.level_change(idx, lvl))
+ self.sliders.append(slider)
self.satBoxG.addWidget(self.sliders[-1], widget_row, 2, 1, 7)
b += 1
@@ -660,11 +659,13 @@ def make_buttons(self):
return b
+
+ def set_cmap(self, idx: int, new_cmap):
+ self.cmap[idx + 1] = new_cmap
+ self.update_plot()
- def level_change(self, r):
- r = ["red", "green", "blue"].index(r)
+ def level_change(self, r, sval):
if self.loaded:
- sval = self.sliders[r].value()
self.saturation[r][self.currentZ] = sval
if not self.autobtn.isChecked():
for r in range(3):
@@ -713,33 +714,35 @@ def keyPressEvent(self, event):
# can change background or stroke size if cell not finished
if event.key() == QtCore.Qt.Key_Up or event.key() == QtCore.Qt.Key_W:
- self.go_next_previous_dropdown(self.RGBDropDown, -1)
+ # self.go_next_previous_dropdown(self.RGBDropDown, -1)
+ print('ERROR not cycling colors')
event.accept()
return
elif event.key() == QtCore.Qt.Key_Down or event.key(
) == QtCore.Qt.Key_S:
- self.go_next_previous_dropdown(self.RGBDropDown, 1)
+ # self.go_next_previous_dropdown(self.RGBDropDown, 1)
+ print('ERROR not cycling colors')
event.accept()
return
elif event.key() == QtCore.Qt.Key_R:
- if self.color != 'red':
- self.color = 'red'
+ if self.active_channels != 'red':
+ self.active_channels = 'red'
else:
- self.color = 'rgb'
+ self.active_channels = 'rgb'
event.accept()
return
elif event.key() == QtCore.Qt.Key_G:
- if self.color != 'green':
- self.color = 'green'
+ if self.active_channels != 'green':
+ self.active_channels = 'green'
else:
- self.color = 'rgb'
+ self.active_channels = 'rgb'
event.accept()
return
elif event.key() == QtCore.Qt.Key_B:
- if self.color != 'blue':
- self.color = 'blue'
+ if self.active_channels != 'blue':
+ self.active_channels = 'blue'
else:
- self.color = 'rgb'
+ self.active_channels = 'rgb'
event.accept()
return
elif (event.key() == QtCore.Qt.Key_Comma or
@@ -999,8 +1002,8 @@ def reset(self):
self.ismanual = np.zeros(0, "bool")
# -- set menus to default -- #
- with QtCore.QSignalBlocker(self.RGBDropDown):
- self.color = 'RGB'
+ # with QtCore.QSignalBlocker(self.RGBDropDown):
+ # self.color = 'RGB'
with QtCore.QSignalBlocker(self.ViewDropDown):
self.view = 'image'
self.delete_restore()
@@ -1017,52 +1020,84 @@ def reset(self):
self.remove_roi_obj = None
@property
- def color(self):
- """Current color display mode as a lowercase string.
-
- Reflects the current selection of the RGBDropDown widget. Possible
- values are ``'rgb'``, ``'red'``, ``'green'``, ``'blue'``, ``'gray'``,
- and ``'spectral'``.
+ def active_channels(self) -> list[str]:
+ """Return the list of active channel names
- Returns
- -------
- str
- The current color mode, always lowercase.
"""
- # invert mapping
- inv_name_map = {v: k for k, v in self.RGBDropDown.name_map.items()}
- return inv_name_map[self.RGBDropDown.currentText().lower()].lower()
+ print("Error, didn't get color")
+ l = [k for k, v in self.labels.items() if v.enabled]
+ return l
- @color.setter
- def color(self, value: str|int):
- """Set the color display mode by name or dropdown index.
+ @property
+ def inactive_channels_idxs(self) -> tuple[int]:
+ """Return the idxs of inactive channels
- Updates the RGBDropDown widget, which triggers any connected signals
- (e.g. ``update_plot``).
+ """
+ l = []
+ keys = sorted(self.labels.keys())
+ for i, k in enumerate(keys):
+ if not self.labels[k].enabled:
+ l.append(i)
+ return tuple(l)
- Parameters
- ----------
- value : str or int
- If ``str``, a case-insensitive color name (``'rgb'``, ``'red'``,
- ``'green'``, ``'blue'``, ``'gray'``, ``'spectral'``). The name is
- looked up via ``RGBDropDown.name_map`` before matching against the
- dropdown items, so aliases defined in that map are also accepted.
- If ``int``, the zero-based index of the desired dropdown item.
+ @property
+ def active_channels_idxs(self) -> tuple[int]:
+ """Return the idxs of active channels
- Raises
- ------
- ValueError
- If ``value`` is neither a ``str`` nor an ``int``.
"""
- if isinstance(value, int):
- self.RGBDropDown.setCurrentIndex(value)
- elif isinstance(value, str):
- value = self.RGBDropDown.name_map[value.lower()]
- items = [self.RGBDropDown.itemText(i).lower() for i in range(self.RGBDropDown.count())]
- if value in items:
- self.RGBDropDown.setCurrentIndex(items.index(value))
- else:
- raise ValueError('Imcompatible color drop down setting')
+ l = []
+ keys = sorted(self.labels.keys())
+ for i, k in enumerate(keys):
+ if self.labels[k].enabled:
+ l.append(i)
+ return tuple(l)
+
+ @property
+ def active_luts(self) -> list[tuple[int, np.ndarray]]:
+ """ List of the active 8-bit LUTs """
+ l = []
+ keys = sorted(self.labels.keys())
+ for i, k in enumerate(keys):
+ labels_k = self.labels[k]
+ if labels_k.enabled and labels_k.colormap is not None:
+ l.append((i, labels_k.colormap))
+ return l
+
+
+
+
+ # @color.setter
+ # def color(self, value: str|int):
+ # """Set the color display mode by name or dropdown index.
+
+ # Updates the RGBDropDown widget, which triggers any connected signals
+ # (e.g. ``update_plot``).
+
+ # Parameters
+ # ----------
+ # value : str or int
+ # If ``str``, a case-insensitive color name (``'rgb'``, ``'red'``,
+ # ``'green'``, ``'blue'``, ``'gray'``, ``'spectral'``). The name is
+ # looked up via ``RGBDropDown.name_map`` before matching against the
+ # dropdown items, so aliases defined in that map are also accepted.
+ # If ``int``, the zero-based index of the desired dropdown item.
+
+ # Raises
+ # ------
+ # ValueError
+ # If ``value`` is neither a ``str`` nor an ``int``.
+ # """
+ # # if isinstance(value, int):
+ # # self.RGBDropDown.setCurrentIndex(value)
+ # # elif isinstance(value, str):
+ # # value = self.RGBDropDown.name_map[value.lower()]
+ # # items = [self.RGBDropDown.itemText(i).lower() for i in range(self.RGBDropDown.count())]
+ # # if value in items:
+ # # self.RGBDropDown.setCurrentIndex(items.index(value))
+ # # else:
+ # # raise ValueError('Imcompatible color drop down setting')
+ # print('ERROR not setting color')
+ # pass
@property
def view(self):
@@ -1452,58 +1487,63 @@ def update_plot(self):
is_image_view = self.view == 'image'
is_restored_view = self.view == 'restored'
- flowp_map = {
- 'gradXY' : 0,
- 'cellprob' : 1,
- 'gradZ' : 4,
- }
- rgb_list = ['red', 'green', 'blue']
-
if is_image_view or is_restored_view:
if is_image_view:
- image = self.stack[self.currentZ]
+ image = self.stack[self.currentZ].copy()
else:
- image = self.stack_filtered[self.currentZ]
- if self.color == 'rgb':
- self.img.setImage(image, autoLevels=False, lut=None)
- if self.nchan > 1:
- levels = np.array([
- self.saturation[0][self.currentZ],
- self.saturation[1][self.currentZ],
- self.saturation[2][self.currentZ]
- ])
- self.img.setLevels(levels)
- else:
- self.img.setLevels(self.saturation[0][self.currentZ])
- elif self.color in rgb_list:
- color_index = rgb_list.index(self.color)
- if self.nchan > 1:
- image = image[:, :, color_index]
- self.img.setImage(image, autoLevels=False, lut=self.cmap[color_index+1])
- if self.nchan > 1:
- self.img.setLevels(self.saturation[color_index][self.currentZ])
- else:
- self.img.setLevels(self.saturation[0][self.currentZ])
- elif self.color == 'gray':
- if self.nchan > 1:
- # exclude channels with no data:
- ranges = np.ptp(image, tuple(range(image.ndim-1)))
- range_mask = ranges > 1e-5
- image = image[..., range_mask]
- image = image.mean(axis=-1)
- self.img.setImage(image, autoLevels=False, lut=None)
- self.img.setLevels(self.saturation[0][self.currentZ])
- elif self.color == 'spectral':
- if self.nchan > 1:
- image = image.mean(axis=-1)
- self.img.setImage(image, autoLevels=False, lut=self.cmap[0])
- self.img.setLevels(self.saturation[0][self.currentZ])
- else:
- image = np.zeros((self.Ly, self.Lx), np.uint8)
- if len(self.flows[flowp_map[self.view]]) > 0:
- image = self.flows[flowp_map[self.view]][self.currentZ]
- self.img.setImage(image, autoLevels=False, lut=self.bwr)
- self.img.setLevels([0.0, 255.0])
+ image = self.stack_filtered[self.currentZ].copy()
+
+ image_out = np.zeros_like(image)
+ for chan, lut in self.active_luts:
+ sat = self.saturation[chan][self.currentZ]
+ img_c = np.clip(image[:, :, chan], np.uint8(sat[0]), np.uint8(sat[1])).astype(np.uint8)
+ img_lut = lut[img_c]
+ image_out += img_lut
+
+ image_out = np.clip(image_out, np.uint8(0), np.uint8(255)).astype(np.uint8)
+ self.img.setImage(image_out, autoLevels=False, lut=None)
+ # self.img.setLevels([self.saturation[i][self.currentZ] for i in range(3)])
+
+ # if self.active_channels == 'rgb':
+ # self.img.setImage(image, autoLevels=False, lut=None)
+ # if self.nchan > 1:
+ # levels = np.array([
+ # self.saturation[0][self.currentZ],
+ # self.saturation[1][self.currentZ],
+ # self.saturation[2][self.currentZ]
+ # ])
+ # self.img.setLevels(levels)
+ # else:
+ # self.img.setLevels(self.saturation[0][self.currentZ])
+ # elif self.active_channels in rgb_list:
+ # color_index = rgb_list.index(self.active_channels)
+ # if self.nchan > 1:
+ # image = image[:, :, color_index]
+ # self.img.setImage(image, autoLevels=False, lut=self.cmap[color_index+1])
+ # if self.nchan > 1:
+ # self.img.setLevels(self.saturation[color_index][self.currentZ])
+ # else:
+ # self.img.setLevels(self.saturation[0][self.currentZ])
+ # elif self.active_channels == 'gray':
+ # if self.nchan > 1:
+ # # exclude channels with no data:
+ # ranges = np.ptp(image, tuple(range(image.ndim-1)))
+ # range_mask = ranges > 1e-5
+ # image = image[..., range_mask]
+ # image = image.mean(axis=-1)
+ # self.img.setImage(image, autoLevels=False, lut=None)
+ # self.img.setLevels(self.saturation[0][self.currentZ])
+ # elif self.active_channels == 'spectral':
+ # if self.nchan > 1:
+ # image = image.mean(axis=-1)
+ # self.img.setImage(image, autoLevels=False, lut=self.cmap[0])
+ # self.img.setLevels(self.saturation[0][self.currentZ])
+ # else:
+ # image = np.zeros((self.Ly, self.Lx), np.uint8)
+ # if len(self.flows[flowp_map[self.view]]) > 0:
+ # image = self.flows[flowp_map[self.view]][self.currentZ]
+ # self.img.setImage(image, autoLevels=False, lut=self.bwr)
+ # self.img.setLevels([0.0, 255.0])
for r in range(3):
# setValue on the slider triggers update_plot() so it needs to be suppressed
@@ -2141,4 +2181,93 @@ def go_next_previous_dropdown(self, dropdown, increment=1):
return
idx += increment
- self.logger.error('Could not find an emabled dropdown item.')
\ No newline at end of file
+ self.logger.error('Could not find an emabled dropdown item.')
+
+import matplotlib as mpl
+
+class ClickableColorLabel(QLabel):
+
+ sigColorMapChoose = QtCore.Signal(object)
+ sigEnableToggled = QtCore.Signal(bool)
+
+
+ def __init__(self, *args, color_name, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._enabled = True
+ self.color_name = color_name
+ self.setStyleSheet(f"color: {self.color_name}")
+
+ with QtCore.QSignalBlocker(self):
+ self.colormap = self.color_name
+
+ def mousePressEvent(self, ev):
+ if ev.button() == QtCore.Qt.LeftButton:
+ self.toggle()
+ if self._enabled and ev.button() == QtCore.Qt.RightButton:
+ menu = QMenu(self)
+ menu.addAction('red')
+ menu.addAction('green')
+ menu.addAction('blue')
+ menu.addAction('Grays')
+ menu.addAction('Purples')
+ menu.addAction('viridis')
+ menu.addAction('magenta')
+ menu.addAction('cyan')
+ action = menu.exec_(ev.globalPos())
+ if action:
+ self.colormap = action.text()
+ self.sigColorMapChoose.emit(self.colormap)
+ self.setText(action.text()+":")
+
+ @property
+ def enabled(self):
+ return self._enabled
+
+ @enabled.setter
+ def enabled(self, val: bool):
+ if isinstance(val, bool):
+ if val != self._enabled:
+ self._enabled = val
+ print('emitting value')
+ self.sigEnableToggled.emit(val)
+ else:
+ raise ValueError('incompatible value')
+
+ def toggle(self):
+ print('toggling!')
+ if self.enabled:
+ self.enabled = False
+ self.setStyleSheet("color: gray")
+ else:
+ self.enabled = True
+ self.setStyleSheet(f"color: {self.color_name}")
+
+ @property
+ def colormap(self) -> np.ndarray:
+ return self._cmap
+
+ @colormap.setter
+ def colormap(self, name: str):
+ if name in list(mpl.colormaps.keys()):
+ cmap = mpl.colormaps[name](np.linspace(0, 1, 512))[:, :3]
+ cmap *= 255
+ cmap = cmap.astype(np.uint8)
+ elif name == 'magenta':
+ cmap = np.linspace(0, 255, 512, dtype=np.uint8)
+ cmap = np.stack([cmap, np.zeros(512, dtype=np.uint8), cmap], axis=1)
+ elif name == 'red':
+ cmap = np.linspace(0, 255, 512, dtype=np.uint8)
+ cmap = np.stack([cmap, np.zeros(512, dtype=np.uint8), np.zeros(512, dtype=np.uint8)], axis=1)
+ elif name == 'green':
+ cmap = np.linspace(0, 255, 512, dtype=np.uint8)
+ cmap = np.stack([np.zeros(512, dtype=np.uint8), cmap, np.zeros(512, dtype=np.uint8)], axis=1)
+ elif name == 'blue':
+ cmap = np.linspace(0, 255, 512, dtype=np.uint8)
+ cmap = np.stack([np.zeros(512, dtype=np.uint8), np.zeros(512, dtype=np.uint8), cmap], axis=1)
+ elif name == 'cyan':
+ cmap = np.linspace(0, 255, 512, dtype=np.uint8)
+ cmap = np.stack([np.zeros(512, dtype=np.uint8), cmap, cmap], axis=1)
+ else:
+ raise ValueError(f'unrecognized cmap: {name}')
+ self._cmap = cmap
+
\ No newline at end of file
diff --git a/cellpose/gui/gui3d.py b/cellpose/gui/gui3d.py
index 78160b1b..3292064d 100644
--- a/cellpose/gui/gui3d.py
+++ b/cellpose/gui/gui3d.py
@@ -471,7 +471,7 @@ def update_ortho(self):
if self.nchan == 1:
# show single channel
image = image[..., 0]
- if self.color == 'rgb':
+ if self.active_channels == 'rgb':
self.imgOrtho[j].setImage(image, autoLevels=False, lut=None)
if self.nchan > 1:
levels = np.array([
@@ -483,8 +483,8 @@ def update_ortho(self):
else:
self.imgOrtho[j].setLevels(
self.saturation[0][self.currentZ])
- elif self.color in rgb_list:
- color_index = rgb_list.index(self.color)
+ elif self.active_channels in rgb_list:
+ color_index = rgb_list.index(self.active_channels)
if self.nchan > 1:
image = image[..., color_index]
self.imgOrtho[j].setImage(image, autoLevels=False,
@@ -495,7 +495,7 @@ def update_ortho(self):
else:
self.imgOrtho[j].setLevels(
self.saturation[0][self.currentZ])
- elif self.color == 'gray':
+ elif self.active_channels == 'gray':
if image.ndim > 2:
# exclude blank channels:
ranges = np.ptp(image, tuple(range(image.ndim-1)))
@@ -504,7 +504,7 @@ def update_ortho(self):
image = image.astype("float32").mean(axis=2).astype("uint8")
self.imgOrtho[j].setImage(image, autoLevels=False, lut=None)
self.imgOrtho[j].setLevels(self.saturation[0][self.currentZ])
- elif self.color == 'spectral':
+ elif self.active_channels == 'spectral':
if image.ndim > 2:
image = image.astype("float32").mean(axis=2).astype("uint8")
self.imgOrtho[j].setImage(image, autoLevels=False,