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,