class ImageView(QWidget): position_changed = Signal([int, int, int], [int, int]) component_clicked = Signal(int) text_info_change = Signal(str) hide_signal = Signal(bool) view_changed = Signal() image_added = Signal() def __init__( self, settings: BaseSettings, channel_property: ChannelProperty, name: str, parent: Optional[QWidget] = None, ndisplay=2, ): super().__init__(parent=parent) self.settings = settings self.channel_property = channel_property self.name = name self.image_info: Dict[str, ImageInfo] = {} self.current_image = "" self._current_order = "xy" self.components = None self.worker_list = [] self.viewer = Viewer(ndisplay=ndisplay) self.viewer.theme = self.settings.theme_name self.viewer_widget = NapariQtViewer(self.viewer) self.image_state = ImageShowState(settings, name) self.channel_control = ColorComboBoxGroup(settings, name, channel_property, height=30) self.ndim_btn = QtNDisplayButton(self.viewer) self.reset_view_button = QtViewerPushButton(self.viewer, "home", "Reset view", self._reset_view) self.roll_dim_button = QtViewerPushButton(self.viewer, "roll", "Roll dimension", self._rotate_dim) self.roll_dim_button.setContextMenuPolicy(Qt.CustomContextMenu) self.roll_dim_button.customContextMenuRequested.connect( self._dim_order_menu) self.mask_chk = QCheckBox() self.mask_label = QLabel("Mask:") self.btn_layout = QHBoxLayout() self.btn_layout.addWidget(self.reset_view_button) self.btn_layout.addWidget(self.ndim_btn) self.btn_layout.addWidget(self.roll_dim_button) self.btn_layout.addWidget(self.channel_control, 1) self.btn_layout.addWidget(self.mask_label) self.btn_layout.addWidget(self.mask_chk) self.btn_layout2 = QHBoxLayout() layout = QVBoxLayout() layout.addLayout(self.btn_layout) layout.addLayout(self.btn_layout2) layout.addWidget(self.viewer_widget) self.setLayout(layout) self.channel_control.change_channel.connect(self.change_visibility) self.viewer.events.status.connect(self.print_info) settings.mask_changed.connect(self.set_mask) settings.mask_representation_changed.connect( self.update_mask_parameters) settings.roi_changed.connect(self.set_roi) settings.roi_clean.connect(self.set_roi) settings.image_changed.connect(self.set_image) settings.image_spacing_changed.connect(self.update_spacing_info) # settings.labels_changed.connect(self.paint_layer) self.old_scene: BaseCamera = self.viewer_widget.view.scene self.image_state.coloring_changed.connect(self.update_roi_coloring) self.image_state.roi_presented_changed.connect( self.update_roi_representation) self.image_state.borders_changed.connect( self.update_roi_representation) self.mask_chk.stateChanged.connect(self.change_mask_visibility) self.viewer_widget.view.scene.transform.changed.connect( self._view_changed, position="last") try: self.viewer.dims.events.current_step.connect(self._view_changed, position="last") except AttributeError: self.viewer.dims.events.axis.connect(self._view_changed, position="last") self.viewer.dims.events.ndisplay.connect(self._view_changed, position="last") if hasattr(self.viewer.dims.events, "ndisplay"): self.viewer.dims.events.ndisplay.connect(self._view_changed, position="last") self.viewer.dims.events.ndisplay.connect(self.camera_change, position="last") else: self.viewer.dims.events.camera.connect(self._view_changed, position="last") self.viewer.dims.events.camera.connect(self.camera_change, position="last") self.viewer.events.reset_view.connect(self._view_changed, position="last") def _dim_order_menu(self, point: QPoint): menu = QMenu() for key in ORDER_DICT: action = menu.addAction(key) action.triggered.connect(partial(self._set_new_order, key)) if key == self._current_order: font = action.font() font.setBold(True) action.setFont(font) menu.exec_(self.roll_dim_button.mapToGlobal(point)) def _set_new_order(self, text: str): self._current_order = text self.viewer.dims.order = ORDER_DICT[text] self.update_roi_representation() def _reset_view(self): self._set_new_order("xy") self.viewer.dims.order = ORDER_DICT[self._current_order] self.viewer.reset_view() def _rotate_dim(self): self._set_new_order(NEXT_ORDER[self._current_order]) def camera_change(self, _args): self.old_scene.transform.changed.disconnect(self._view_changed) self.old_scene: BaseCamera = self.viewer_widget.view.camera self.old_scene.transform.changed.connect(self._view_changed, position="last") def _view_changed(self, _args): self.view_changed.emit() def get_state(self): return { "ndisplay": self.viewer.dims.ndisplay, "point": self.viewer.dims.point, "camera": self.viewer_widget.view.camera.get_state(), } def set_state(self, dkt): if "ndisplay" in dkt and self.viewer.dims.ndisplay != dkt["ndisplay"]: self.viewer.dims.ndisplay = dkt["ndisplay"] return if "point" in dkt: for i, val in enumerate(dkt["point"]): self.viewer.dims.set_point(i, val) if "camera" in dkt: try: self.viewer_widget.view.camera.set_state(dkt["camera"]) except KeyError: pass def change_mask_visibility(self): for image_info in self.image_info.values(): if image_info.mask is not None: image_info.mask.visible = self.mask_chk.isChecked() def update_spacing_info(self, image: Optional[Image] = None) -> None: """ Update spacing of image if not provide, then use image pointed by settings. :param Optional[Image] image: image which spacing should be updated. :return: None """ if image is None: image = self.settings.image if image.file_path not in self.image_info: raise ValueError("Image not registered") image_info = self.image_info[image.file_path] for layer in image_info.layers: layer.scale = image.normalized_scaling() if image_info.roi is not None: image_info.roi.scale = image.normalized_scaling() if image_info.mask is not None: image_info.mask.scale = image.normalized_scaling() def print_info(self, value): if not self.viewer.active_layer: return cords = np.array( [int(x) for x in self.viewer.active_layer.coordinates]) bright_array = [] components = [] for image_info in self.image_info.values(): if not image_info.coords_in(cords): continue moved_coords = image_info.translated_coords(cords) for layer in image_info.layers: if layer.visible: bright_array.append(layer.data[tuple(moved_coords)]) if image_info.roi_info.roi is not None and image_info.roi is not None: val = image_info.roi_info.roi[tuple(moved_coords)] if val: components.append(val) if not bright_array and not components: self.text_info_change.emit("") return text = f"{cords}: " if bright_array: if len(bright_array) == 1: text += str(bright_array[0]) else: text += str(bright_array) self.components = components if components: if len(components) == 1: text += f" component: {components[0]}" else: text += f" components: {components}" self.text_info_change.emit(text) def get_control_view(self) -> ImageShowState: return self.image_state @staticmethod def convert_to_vispy_colormap(colormap: ColorMap): return Colormap(ColorArray(create_color_map(colormap) / 255)) def mask_opacity(self) -> float: """Get mask opacity""" return self.settings.get_from_profile("mask_presentation_opacity", 1) def mask_color(self) -> Colormap: """Get mask marking color""" color = Color( np.divide( self.settings.get_from_profile("mask_presentation_color", [255, 255, 255]), 255)) return Colormap(ColorArray(["black", color.rgba])) def get_image(self, image: Optional[Image]) -> Image: if image is not None: return image if self.current_image not in self.image_info: return self.settings.image return self.image_info[self.current_image].image def set_roi(self, roi_info: Optional[ROIInfo] = None, image: Optional[Image] = None) -> None: image = self.get_image(image) if roi_info is None: roi_info = self.settings.roi_info image_info = self.image_info[image.file_path] if image_info.roi is not None: self.viewer.layers.unselect_all() image_info.roi.selected = True self.viewer.layers.remove_selected() image_info.roi = None if roi_info.roi is None: return image_info.roi_info = roi_info image_info.roi_count = max( roi_info.bound_info) if roi_info.bound_info else 0 self.add_roi_layer(image_info) image_info.roi.colormap = self.get_roi_view_parameters(image_info) image_info.roi.opacity = self.image_state.opacity def get_roi_view_parameters(self, image_info: ImageInfo) -> Colormap: colors = self.settings.label_colors / 255 if self.image_state.show_label == LabelEnum.Not_show or image_info.roi_count == 0 or colors.size == 0: colors = np.array([[0, 0, 0, 0], [0, 0, 0, 0]]) else: repeat = int(np.ceil(image_info.roi_count / colors.shape[0])) colors = np.concatenate([colors] * repeat) colors = np.concatenate( [colors, np.ones(colors.shape[0]).reshape(colors.shape[0], 1)], axis=1) colors = np.concatenate([[[0, 0, 0, 0]], colors[:image_info.roi_count]]) if self.image_state.show_label == LabelEnum.Show_selected: try: colors *= self.settings.components_mask().reshape( (colors.shape[0], 1)) except ValueError: pass control_points = [0] + list( np.linspace(1 / (2 * colors.shape[0]), 1, endpoint=True, num=colors.shape[0])) return Colormap(colors, controls=control_points, interpolation="zero") def update_roi_coloring(self): for image_info in self.image_info.values(): if image_info.roi is None: continue image_info.roi.colormap = self.get_roi_view_parameters(image_info) image_info.roi.opacity = self.image_state.opacity def remove_all_roi(self): self.viewer.layers.unselect_all() for image_info in self.image_info.values(): if image_info.roi is None: continue image_info.roi.selected = True image_info.roi = None self.viewer.layers.remove_selected() def add_roi_layer(self, image_info: ImageInfo): if image_info.roi_info.roi is None: return try: max_num = max(1, image_info.roi_count) except ValueError: max_num = 1 roi = image_info.roi_info.alternative.get( self.image_state.roi_presented, image_info.roi_info.roi) if self.image_state.only_borders: data = calculate_borders( roi.transpose(ORDER_DICT[self._current_order]), self.image_state.borders_thick // 2, self.viewer.dims.ndisplay == 2, ).transpose(np.argsort(ORDER_DICT[self._current_order])) image_info.roi = self.viewer.add_image( data, scale=image_info.image.normalized_scaling(), contrast_limits=[0, max_num], ) else: image_info.roi = self.viewer.add_image( roi, scale=image_info.image.normalized_scaling(), contrast_limits=[0, max_num], name="ROI", blending="translucent", ) image_info.roi._interpolation[3] = Interpolation3D.NEAREST def update_roi_representation(self): self.remove_all_roi() for image_info in self.image_info.values(): self.add_roi_layer(image_info) self.update_roi_coloring() def set_mask(self, mask: Optional[np.ndarray] = None, image: Optional[Image] = None) -> None: image = self.get_image(image) if image.file_path not in self.image_info: raise ValueError("Image not added to viewer") if mask is None: mask = image.mask image_info = self.image_info[image.file_path] if image_info.mask is not None: self.viewer.layers.unselect_all() image_info.mask.selected = True self.viewer.layers.remove_selected() image_info.mask = None if mask is None: return mask_marker = mask == 0 layer = self.viewer.add_image(mask_marker, scale=image.normalized_scaling(), blending="additive") layer.colormap = self.mask_color() layer.opacity = self.mask_opacity() layer.visible = self.mask_chk.isChecked() image_info.mask = layer def update_mask_parameters(self): opacity = self.mask_opacity() colormap = self.mask_color() for image_info in self.image_info.values(): if image_info.mask is not None: image_info.mask.opacity = opacity image_info.mask.colormap = colormap def set_image(self, image: Optional[Image] = None): self.image_info = {} self.add_image(image, True) def has_image(self, image: Image): return image.file_path in self.image_info @staticmethod def calculate_filter( array: np.ndarray, parameters: Tuple[NoiseFilterType, float]) -> Optional[np.ndarray]: if parameters[0] == NoiseFilterType.No or parameters[1] == 0: return array if parameters[0] == NoiseFilterType.Gauss: return gaussian(array, parameters[1]) return median(array, int(parameters[1])) def _remove_worker(self, sender): for worker in self.worker_list: signals = "_signals" if hasattr(worker, "_signals") else "signals" if sender is getattr(worker, signals): self.worker_list.remove(worker) break else: print("[_remove_worker]", sender) def _add_layer_util(self, index, layer, filters): self.viewer.add_layer(layer) def set_data(val): self._remove_worker(self.sender()) data_, layer_ = val if data_ is None: return if layer_ not in self.viewer.layers: return layer_.data = data_ @thread_worker(connect={"returned": set_data}) def calc_filter(j, layer_): if filters[j][0] == NoiseFilterType.No or filters[j][1] == 0: return None, layer_ return self.calculate_filter(layer_.data, parameters=filters[j]), layer_ worker = calc_filter(index, layer) self.worker_list.append(worker) def _add_image(self, image_data: Tuple[ImageInfo, bool]): self._remove_worker(self.sender()) image_info, replace = image_data image = image_info.image if replace: self.viewer.layers.select_all() self.viewer.layers.remove_selected() filters = self.channel_control.get_filter() for i, layer in enumerate(image_info.layers): self._add_layer_util(i, layer, filters) self.image_info[image.file_path].filter_info = filters self.image_info[image.file_path].layers = image_info.layers self.current_image = image.file_path self.viewer.reset_view() if self.viewer.layers: self.viewer.layers[-1].selected = True for i, axis in enumerate(image.axis_order): if axis == "C": continue self.viewer.dims.set_point( i, image.shape[i] * image.normalized_scaling()[i] // 2) if self.image_info[image.file_path].roi is not None: self.set_roi() if image_info.image.mask is not None: self.set_mask() self.image_added.emit() def add_image(self, image: Optional[Image], replace=False): if image is None: image = self.settings.image if not image.channels: raise ValueError("Need non empty image") if image.file_path in self.image_info: raise ValueError("Image already added") self.image_info[image.file_path] = ImageInfo(image, []) channels = image.channels if self.image_info and not replace: channels = max( channels, *[x.image.channels for x in self.image_info.values()]) self.channel_control.set_channels(channels) visibility = self.channel_control.channel_visibility limits = self.channel_control.get_limits() ranges = image.get_ranges() limits = [ ranges[i] if x is None else x for i, x in zip(range(image.channels), limits) ] gamma = self.channel_control.get_gamma() colormaps = [ self.convert_to_vispy_colormap( self.channel_control.selected_colormaps[i]) for i in range(image.channels) ] parameters = ImageParameters(limits, visibility, gamma, colormaps, image.normalized_scaling(), len(self.viewer.layers)) self._prepare_layers(image, parameters, replace) return image def _prepare_layers(self, image, parameters, replace): worker = prepare_layers(image, parameters, replace) worker.returned.connect(self._add_image) self.worker_list.append(worker) worker.start() def images_bounds(self) -> Tuple[List[int], List[int]]: ranges = [] for image_info in self.image_info.values(): if not image_info.layers: continue ranges = [(min(a, b), max(c, d), min(e, f)) for (a, c, e), ( b, d, f) in itertools.zip_longest(image_info.layers[0].dims.range, ranges, fillvalue=(np.inf, -np.inf, np.inf))] visible = [ranges[i] for i in self.viewer.dims.displayed] min_shape, max_shape, _ = zip(*visible) size = np.subtract(max_shape, min_shape) return size, min_shape @staticmethod def _shift_layer(layer: Layer, translate_2d): translate = [0] * layer.ndim translate[-2:] = translate_2d layer.translate_grid = translate def grid_view(self): """Present multiple images in grid view""" n_row = np.ceil(np.sqrt(len(self.image_info))).astype(int) n_row = max(1, n_row) scene_size, _ = self.images_bounds() for image_info, pos in zip(self.image_info.values(), itertools.product(range(n_row), repeat=2)): translate_2d = np.multiply(scene_size[-2:], pos) for layer in image_info.layers: self._shift_layer(layer, translate_2d) if image_info.mask is not None: self._shift_layer(image_info.mask, translate_2d) if image_info.roi is not None: self._shift_layer(image_info.roi, translate_2d) self.viewer.reset_view() def change_visibility(self, name: str, index: int): for image_info in self.image_info.values(): if len(image_info.layers) > index: image_info.layers[ index].visible = self.channel_control.channel_visibility[ index] if self.channel_control.channel_visibility[index]: image_info.layers[ index].colormap = self.convert_to_vispy_colormap( self.channel_control.selected_colormaps[index]) limits = self.channel_control.get_limits()[index] limits = image_info.image.get_ranges( )[index] if limits is None else limits image_info.layers[index].contrast_limits = limits image_info.layers[ index].gamma = self.channel_control.get_gamma()[index] filter_type = self.channel_control.get_filter()[index] if filter_type != image_info.filter_info[index]: image_info.layers[index].data = self.calculate_filter( image_info.image.get_channel(index), filter_type) image_info.filter_info[index] = filter_type def reset_image_size(self): self.viewer.reset_view() def set_theme(self, theme: str): self.viewer.theme = theme def closeEvent(self, event): for worker in self.worker_list: worker.quit() super().closeEvent(event) def get_tool_tip_text(self) -> str: image = self.settings.image image_info = self.image_info[image.file_path] text_list = [] for el in self.components: text_list.append( _print_dict(image_info.roi_info.annotations.get(el, {}))) return " ".join(text_list) def event(self, event: QEvent): if event.type() == QEvent.ToolTip and self.components: text = self.get_tool_tip_text() if text: QToolTip.showText(event.globalPos(), text) return super().event(event)
class ImageView(QWidget): position_changed = Signal([int, int, int], [int, int]) component_clicked = Signal(int) text_info_change = Signal(str) hide_signal = Signal(bool) view_changed = Signal() image_added = Signal() def __init__( self, settings: BaseSettings, channel_property: ChannelProperty, name: str, parent: Optional[QWidget] = None, ndisplay=2, ): super().__init__(parent=parent) self.settings = settings self.channel_property = channel_property self.name = name self.image_info: Dict[str, ImageInfo] = {} self.current_image = "" self._current_order = "xy" self.components = None self.worker_list = [] self.points_layer = None self.roi_alternative_selection = "ROI" self._search_type = SearchType.Highlight self._last_component = 1 self.viewer = Viewer(ndisplay=ndisplay) self.viewer.theme = self.settings.theme_name self.viewer_widget = NapariQtViewer(self.viewer) if hasattr(self.viewer_widget.canvas, "background_color_override"): self.viewer_widget.canvas.background_color_override = "black" self.channel_control = ColorComboBoxGroup(settings, name, channel_property, height=30) self.ndim_btn = QtNDisplayButton(self.viewer) self.reset_view_button = QtViewerPushButton(self.viewer, "home", "Reset view", self._reset_view) self.points_view_button = QtViewerPushButton( self.viewer, "new_points", "Show points", self.toggle_points_visibility) self.points_view_button.setVisible(False) self.search_roi_btn = SearchROIButton(self.settings) self.search_roi_btn.clicked.connect(self._search_component) self.search_roi_btn.setDisabled(True) self.roll_dim_button = QtViewerPushButton(self.viewer, "roll", "Roll dimension", self._rotate_dim) self.roll_dim_button.setContextMenuPolicy(Qt.CustomContextMenu) self.roll_dim_button.customContextMenuRequested.connect( self._dim_order_menu) self.mask_chk = QCheckBox() self.mask_chk.setVisible(False) self.mask_label = QLabel("Mask:") self.mask_label.setVisible(False) self.btn_layout = QHBoxLayout() self.btn_layout.addWidget(self.reset_view_button) self.btn_layout.addWidget(self.ndim_btn) self.btn_layout.addWidget(self.roll_dim_button) self.btn_layout.addWidget(self.points_view_button) self.btn_layout.addWidget(self.search_roi_btn) self.btn_layout.addWidget(self.channel_control, 1) self.btn_layout.addWidget(self.mask_label) self.btn_layout.addWidget(self.mask_chk) self.btn_layout2 = QHBoxLayout() layout = QVBoxLayout() layout.addLayout(self.btn_layout) layout.addLayout(self.btn_layout2) layout.addWidget(self.viewer_widget) self.setLayout(layout) self.channel_control.change_channel.connect(self.change_visibility) self.viewer.events.status.connect(self.print_info) settings.mask_changed.connect(self.set_mask) settings.roi_changed.connect(self.set_roi) settings.roi_clean.connect(self.set_roi) settings.image_changed.connect(self.set_image) settings.image_spacing_changed.connect(self.update_spacing_info) settings.points_changed.connect(self.update_points) settings.connect_to_profile(RENDERING_MODE_NAME, self.update_rendering) settings.labels_changed.connect(self.update_roi_coloring) settings.connect_to_profile(f"{name}.image_state.opacity", self.update_roi_coloring) settings.connect_to_profile(f"{name}.image_state.only_border", self.update_roi_border) settings.connect_to_profile(f"{name}.image_state.border_thick", self.update_roi_border) settings.connect_to_profile(f"{name}.image_state.show_label", self.update_roi_labeling) settings.connect_to_profile("mask_presentation_opacity", self.update_mask_parameters) settings.connect_to_profile("mask_presentation_color", self.update_mask_parameters) # settings.labels_changed.connect(self.paint_layer) self.old_scene: BaseCamera = self.viewer_widget.view.scene self.mask_chk.stateChanged.connect(self.change_mask_visibility) self.viewer_widget.view.scene.transform.changed.connect( self._view_changed, position="last") self.viewer.dims.events.current_step.connect(self._view_changed, position="last") self.viewer.dims.events.ndisplay.connect(self._view_changed, position="last") self.viewer.dims.events.ndisplay.connect(self._view_changed, position="last") self.viewer.dims.events.ndisplay.connect(self.camera_change, position="last") self.viewer.events.reset_view.connect(self._view_changed, position="last") def toggle_points_visibility(self): if self.points_layer is not None: self.points_layer.visible = not self.points_layer.visible def _dim_order_menu(self, point: QPoint): menu = QMenu() for key in ORDER_DICT: action = menu.addAction(key) action.triggered.connect(partial(self._set_new_order, key)) if key == self._current_order: font = action.font() font.setBold(True) action.setFont(font) menu.exec_(self.roll_dim_button.mapToGlobal(point)) def _set_new_order(self, text: str): self._current_order = text self.viewer.dims.order = ORDER_DICT[text] # self.update_roi_representation() def _reset_view(self): self._set_new_order("xy") self.viewer.dims.order = ORDER_DICT[self._current_order] self.viewer.reset_view() def _rotate_dim(self): self._set_new_order(NEXT_ORDER[self._current_order]) def camera_change(self, _args): self.old_scene.transform.changed.disconnect(self._view_changed) self.old_scene: BaseCamera = self.viewer_widget.view.camera self.old_scene.transform.changed.connect(self._view_changed, position="last") def _view_changed(self, _args): self.view_changed.emit() def get_state(self): return { "ndisplay": self.viewer.dims.ndisplay, "point": self.viewer.dims.point, "camera": self.viewer_widget.view.camera.get_state(), } def set_state(self, dkt): if "ndisplay" in dkt and self.viewer.dims.ndisplay != dkt["ndisplay"]: self.viewer.dims.ndisplay = dkt["ndisplay"] return if "point" in dkt: for i, val in enumerate(dkt["point"]): self.viewer.dims.set_point(i, val) if "camera" in dkt: with suppress(KeyError): self.viewer_widget.view.camera.set_state(dkt["camera"]) def change_mask_visibility(self): for image_info in self.image_info.values(): if image_info.mask is not None: image_info.mask.visible = self.mask_chk.isChecked() def update_spacing_info(self, image: Optional[Image] = None) -> None: """ Update spacing of image if not provide, then use image pointed by settings. :param Optional[Image] image: image which spacing should be updated. :return: None """ if image is None: image = self.settings.image if image.file_path not in self.image_info: raise ValueError("Image not registered") image_info = self.image_info[image.file_path] for layer in image_info.layers: layer.scale = image.normalized_scaling() if image_info.roi is not None: image_info.roi.scale = image.normalized_scaling() if image_info.mask is not None: image_info.mask.scale = image.normalized_scaling() def _active_layer(self): if hasattr(self.viewer.layers, "selection"): return self.viewer.layers.selection.active return self.viewer.active_layer def _coordinates(self): active_layer = self._active_layer() if active_layer is None: return if (hasattr(self.viewer, "cursor") and hasattr(self.viewer.cursor, "position") and hasattr(active_layer, "world_to_data")): return [ int(x) for x in active_layer.world_to_data( self.viewer.cursor.position) ] return [int(x) for x in active_layer.coordinates] def print_info(self, event=None): cords = self._coordinates() if cords is None: return bright_array = [] components = [] for image_info in self.image_info.values(): if not image_info.coords_in(cords): continue moved_coords = image_info.translated_coords(cords) for layer in image_info.layers: if layer.visible: bright_array.append(layer.data[tuple(moved_coords)]) if image_info.roi_info.roi is not None and image_info.roi is not None: val = image_info.roi_info.roi[tuple(moved_coords)] if val: components.append(val) if not bright_array and not components: self.text_info_change.emit("") return text = f"{cords}: " if bright_array: text += str(bright_array[0]) if len(bright_array) == 1 else str( bright_array) self.components = components if components: if len(components) == 1: text += f" component: {components[0]}" else: text += f" components: {components}" self.text_info_change.emit(text) def mask_opacity(self) -> float: """Get mask opacity""" return self.settings.get_from_profile("mask_presentation_opacity", 1) def mask_color(self) -> ColorInfo: """Get mask marking color""" color = Color( np.divide( self.settings.get_from_profile("mask_presentation_color", [255, 255, 255]), 255)) return {0: (0, 0, 0, 0), 1: color.rgba} def get_image(self, image: Optional[Image]) -> Image: if image is not None: return image if self.current_image not in self.image_info: return self.settings.image return self.image_info[self.current_image].image def update_points(self): if self.settings.points is not None: self.points_view_button.setVisible(True) if self.points_layer is None or self.points_layer not in self.viewer.layers: self.points_layer = Points( self.settings.points, scale=self.settings.image.normalized_scaling()) self.viewer.add_layer(self.points_layer) else: self.points_layer.data = self.settings.points self.points_layer.scale = self.settings.image.normalized_scaling( ) elif self.points_layer is not None and self.points_layer in self.viewer.layers: self.points_view_button.setVisible(False) self.points_layer.data = np.empty((0, 4)) def set_roi(self, roi_info: Optional[ROIInfo] = None, image: Optional[Image] = None) -> None: image = self.get_image(image) if roi_info is None: roi_info = self.settings.roi_info image_info = self.image_info[image.file_path] if image_info.roi is None and roi_info.roi is not None: image_info.roi_info = roi_info self.add_roi_layer(image_info) self.search_roi_btn.setDisabled(False) elif image_info.roi is None: return elif roi_info.roi is None: image_info.roi.visible = False self.search_roi_btn.setDisabled(True) return else: image_info.roi_info = roi_info image_info.roi.data = roi_info.alternative.get( self.roi_alternative_selection, roi_info.roi) image_info.roi.visible = True self.search_roi_btn.setDisabled(False) image_info.roi_count = max( roi_info.bound_info) if roi_info.bound_info else 0 # image_info.roi.color = self.get_roi_view_parameters(image_info) self.set_roi_colormap(image_info) image_info.roi.opacity = self.settings.get_from_profile( f"{self.name}.image_state.opacity", 1.0) image_info.roi.refresh() def get_roi_view_parameters(self, image_info: ImageInfo) -> ColorInfo: colors = self.settings.label_colors / 255 if (self.settings.get_from_profile( f"{self.name}.image_state.show_label", LabelEnum.Show_results) == LabelEnum.Not_show or image_info.roi_count == 0 or colors.size == 0): return {x: [0, 0, 0, 0] for x in range(image_info.roi_count + 1)} res = { x: colors[(x - 1) % colors.shape[0]] for x in range(1, image_info.roi_count + 1) } res[0] = [0, 0, 0, 0] return res def set_roi_colormap(self, image_info) -> None: if _napari_ge_4_13: image_info.roi.color = self.get_roi_view_parameters(image_info) return colors = self.settings.label_colors / 255 if (self.settings.get_from_profile( f"{self.name}.image_state.show_label", LabelEnum.Show_results) == LabelEnum.Not_show or image_info.roi_count == 0 or colors.size == 0): image_info.roi.colormap = Colormap([[0, 0, 0, 0], [0, 0, 0, 0]]) res = [ list(colors[(x - 1) % colors.shape[0]]) + [1] for x in range(image_info.roi_count + 1) ] res[0] = [0, 0, 0, 0] if len(res) < 2: res += [[0, 0, 0, 0]] * (2 - len(res)) image_info.roi.colormap = Colormap( colors=res, interpolation=ColormapInterpolationMode.ZERO) max_val = image_info.roi_count + 1 image_info.roi._all_vals = np.array( # pylint: disable=W0212 [0] + [(x + 1) / (max_val + 1) for x in range(1, max_val)]) def update_roi_coloring(self): for image_info in self.image_info.values(): if image_info.roi is None: continue # image_info.roi.color = self.get_roi_view_parameters(image_info) self.set_roi_colormap(image_info) image_info.roi.opacity = self.settings.get_from_profile( f"{self.name}.image_state.opacity", 1.0) def remove_all_roi(self): for image_info in self.image_info.values(): if image_info.roi is None: continue image_info.roi.visible = False def update_roi_border(self) -> None: for image_info in self.image_info.values(): if image_info.roi is None: continue roi = image_info.roi_info.alternative.get( self.roi_alternative_selection, image_info.roi_info.roi) border_thick = self.settings.get_from_profile( f"{self.name}.image_state.border_thick", 1) only_border = self.settings.get_from_profile( f"{self.name}.image_state.only_border", True) alternative = image_info.roi.metadata.get( "alternative", self.roi_alternative_selection) if alternative != self.roi_alternative_selection: image_info.roi.data = roi image_info.roi.contour = border_thick if only_border else 0 image_info.roi.metadata[ "alternative"] = self.roi_alternative_selection @ensure_main_thread def update_rendering(self): rendering = self.settings.get_from_profile(RENDERING_MODE_NAME, RENDERING_LIST[0]) for image_info in self.image_info.values(): if image_info.roi is not None and hasattr(image_info.roi, "rendering"): image_info.roi.rendering = rendering @ensure_main_thread def update_roi_labeling(self): for image_info in self.image_info.values(): if image_info.roi is not None: # image_info.roi.color = self.get_roi_view_parameters(image_info) self.set_roi_colormap(image_info) def add_roi_layer(self, image_info: ImageInfo): if image_info.roi_info.roi is None: return roi = image_info.roi_info.alternative.get( self.roi_alternative_selection, image_info.roi_info.roi) border_thick = self.settings.get_from_profile( f"{self.name}.image_state.border_thick", 1) kwargs = { "scale": image_info.image.normalized_scaling(), "name": "ROI", "blending": "translucent", "metadata": { "alternative": self.roi_alternative_selection }, } if napari_rendering: kwargs["rendering"] = self.settings.get_from_profile( RENDERING_MODE_NAME, RENDERING_LIST[0]) only_border = self.settings.get_from_profile( f"{self.name}.image_state.only_border", True) image_info.roi = self.viewer.add_labels(roi, **kwargs) image_info.roi.contour = border_thick if only_border else 0 def set_mask(self, mask: Optional[np.ndarray] = None, image: Optional[Image] = None) -> None: image = self.get_image(image) if image.file_path not in self.image_info: raise ValueError("Image not added to viewer") if mask is None: mask = image.mask image_info = self.image_info[image.file_path] if mask is None: if image_info.mask is not None: image_info.mask.visible = False image_info.mask.metadata["valid"] = False self._toggle_mask_chk_visibility() return mask_marker = mask == 0 if image_info.mask is None: image_info.mask = self.viewer.add_labels( mask_marker, scale=image.normalized_scaling(), blending="translucent", name="Mask") else: image_info.mask.data = mask_marker image_info.mask.metadata["valid"] = True image_info.mask.color = self.mask_color() image_info.mask.opacity = self.mask_opacity() image_info.mask.visible = self.mask_chk.isChecked() self._toggle_mask_chk_visibility() def _toggle_mask_chk_visibility(self): visibility = any(image_info.mask is not None and image_info.mask.metadata.get("valid", False) for image_info in self.image_info.values()) self.mask_chk.setVisible(visibility) self.mask_label.setVisible(visibility) def update_mask_parameters(self): opacity = self.mask_opacity() colormap = self.mask_color() for image_info in self.image_info.values(): if image_info.mask is not None: image_info.mask.opacity = opacity image_info.mask.color = colormap def set_image(self, image: Optional[Image] = None): self.image_info = {} self.add_image(image, True) def has_image(self, image: Image): return image.file_path in self.image_info @staticmethod def calculate_filter( array: np.ndarray, parameters: Tuple[NoiseFilterType, float]) -> Optional[np.ndarray]: if parameters[0] == NoiseFilterType.No or parameters[1] == 0: return array if parameters[0] == NoiseFilterType.Gauss: return gaussian(array, parameters[1]) if parameters[0] == NoiseFilterType.Bilateral: return bilateral(array, parameters[1]) return median(array, int(parameters[1])) def _remove_worker(self, sender): for worker in self.worker_list: if sender is worker.signals: self.worker_list.remove(worker) break else: print("[_remove_worker]", sender) def _add_layer_util(self, index, layer, filters): self.viewer.add_layer(layer) def set_data(val): self._remove_worker(self.sender()) data_, layer_ = val if data_ is None: return if layer_ not in self.viewer.layers: return layer_.data = data_ @thread_worker(connect={"returned": set_data}) def calc_filter(j, layer_): if filters[j][0] == NoiseFilterType.No or filters[j][1] == 0: return None, layer_ return self.calculate_filter(layer_.data, parameters=filters[j]), layer_ worker = calc_filter(index, layer) self.worker_list.append(worker) def _add_image(self, image_data: Tuple[ImageInfo, bool]): self._remove_worker(self.sender()) image_info, replace = image_data image = image_info.image if replace: self.viewer.layers.select_all() self.viewer.layers.remove_selected() QApplication.instance().processEvents() filters = self.channel_control.get_filter() for i, layer in enumerate(image_info.layers): try: self._add_layer_util(i, layer, filters) except AssertionError: layer.colormap = "gray" self._add_layer_util(i, layer, filters) self.image_info[image.file_path].filter_info = filters self.image_info[image.file_path].layers = image_info.layers self.current_image = image.file_path self.viewer.reset_view() if self.viewer.layers: if hasattr(self.viewer.layers, "selection"): self.viewer.layers.selection.clear() self.viewer.layers.selection.add(self.viewer.layers[-1]) else: self.viewer.layers[-1].selected = True for i, axis in enumerate(image.axis_order): if axis == "C": continue self.viewer.dims.set_point( i, image.shape[i] * image.normalized_scaling()[i] // 2) if self.image_info[image.file_path].roi is not None: self.set_roi() if image_info.image.mask is not None: self.set_mask() self._toggle_mask_chk_visibility() self.image_added.emit() def add_image(self, image: Optional[Image], replace=False): if image is None: image = self.settings.image if not image.channels: raise ValueError("Need non empty image") if image.file_path in self.image_info: raise ValueError("Image already added") self.image_info[image.file_path] = ImageInfo(image, []) channels = image.channels if self.image_info and not replace: channels = max( channels, *(x.image.channels for x in self.image_info.values())) self.channel_control.set_channels(channels) visibility = self.channel_control.channel_visibility limits = self.channel_control.get_limits() ranges = image.get_ranges() limits = [ ranges[i] if x is None else x for i, x in zip(range(image.channels), limits) ] gamma = self.channel_control.get_gamma() colormaps = [ self.channel_control.selected_colormaps[i] for i in range(image.channels) ] parameters = ImageParameters(limits, visibility, gamma, colormaps, image.normalized_scaling(), len(self.viewer.layers)) self._prepare_layers(image, parameters, replace) return image def _prepare_layers(self, image, parameters, replace): worker = prepare_layers(image, parameters, replace) worker.returned.connect(self._add_image) self.worker_list.append(worker) worker.start() def images_bounds(self) -> Tuple[List[int], List[int]]: ranges = [] for image_info in self.image_info.values(): if not image_info.layers: continue ranges = [(min(a, b), max(c, d), min(e, f)) for (a, c, e), ( b, d, f) in itertools.zip_longest(image_info.layers[0].dims.range, ranges, fillvalue=(np.inf, -np.inf, np.inf))] visible = [ranges[i] for i in self.viewer.dims.displayed] min_shape, max_shape, _ = zip(*visible) size = np.subtract(max_shape, min_shape) return size, min_shape @staticmethod def _shift_layer(layer: Layer, translate_2d): translate = [0] * layer.ndim translate[-2:] = translate_2d layer.translate = translate def grid_view(self): """Present multiple images in grid view""" n_row = np.ceil(np.sqrt(len(self.image_info))).astype(int) n_row = max(1, n_row) scene_size, _ = self.images_bounds() for image_info, pos in zip(self.image_info.values(), itertools.product(range(n_row), repeat=2)): translate_2d = np.multiply(scene_size[-2:], pos) for layer in image_info.layers: self._shift_layer(layer, translate_2d) if image_info.mask is not None: self._shift_layer(image_info.mask, translate_2d) if image_info.roi is not None: self._shift_layer(image_info.roi, translate_2d) self.viewer.reset_view() def change_visibility(self, name: str, index: int): for image_info in self.image_info.values(): if len(image_info.layers) > index: image_info.layers[ index].visible = self.channel_control.channel_visibility[ index] if self.channel_control.channel_visibility[index]: image_info.layers[ index].colormap = self.channel_control.selected_colormaps[ index] limits = self.channel_control.get_limits()[index] limits = image_info.image.get_ranges( )[index] if limits is None else limits image_info.layers[index].contrast_limits = limits image_info.layers[ index].gamma = self.channel_control.get_gamma()[index] filter_type = self.channel_control.get_filter()[index] if filter_type != image_info.filter_info[index]: image_info.layers[index].data = self.calculate_filter( image_info.image.get_channel(index), filter_type) image_info.filter_info[index] = filter_type def reset_image_size(self): self.viewer.reset_view() def set_theme(self, theme: str): self.viewer.theme = theme def closeEvent(self, event): for worker in self.worker_list: worker.quit() self.viewer.layers.clear() self.viewer_widget.close() super().closeEvent(event) def get_tool_tip_text(self) -> str: image = self.settings.image image_info = self.image_info[image.file_path] text_list = [] for el in self.components: data = image_info.roi_info.annotations.get(el, {}) if data: try: text_list.append(_print_dict(data)) except ValueError: # pragma: no cover logging.warning( "Wrong value provided as layer annotation.") return " ".join(text_list) def event(self, event: QEvent): if event.type() == QEvent.ToolTip and self.components: text = self.get_tool_tip_text() if text: QToolTip.showText(event.globalPos(), text, self) return super().event(event) def _search_component(self): max_components = max( max(image_info.roi_info.bound_info) for image_info in self.image_info.values()) if self.viewer.dims.ndisplay == 3: self._search_type = SearchType.Highlight dial = SearchComponentModal(self, self._search_type, self._last_component, max_components) if self.viewer.dims.ndisplay == 3: dial.zoom_to.setDisabled(True) dial.show_right_of_mouse() def component_unmark(self, _num): self.viewer.layers.selection.clear() for el in self.image_info.values(): if el.highlight is None: continue if "timer" in el.highlight.metadata: el.highlight.metadata["timer"].stop() el.highlight.visible = False def _mark_layer(self, num: int, flash: bool, image_info: ImageInfo): bound_info = image_info.roi_info.bound_info.get(num, None) if bound_info is None: return # TODO think about marking on bright background slices = bound_info.get_slices(1) slices[image_info.image.stack_pos] = slice(None) component_mark = image_info.roi_info.roi[tuple(slices)] == num if self.viewer.dims.ndisplay == 3: component_mark = binary_dilation(component_mark) shift_base = bound_info.lower - 1 shift_base[0] += 1 # remove shift on time axis translate = image_info.roi.translate + shift_base * image_info.roi.scale translate[image_info.image.stack_pos] = 0 if image_info.highlight is None: image_info.highlight = self.viewer.add_labels( component_mark, scale=image_info.roi.scale, blending="translucent", color={ 0: (0, 0, 0, 0), 1: "white" }, opacity=0.7, ) else: image_info.highlight.data = component_mark image_info.highlight.translate = translate image_info.highlight.visible = True if flash: layer = image_info.highlight def flash_fun(layer_=layer): opacity = layer_.opacity + 0.1 if opacity > 1: opacity = 0.1 layer_.opacity = opacity timer = QTimer() timer.setInterval(100) timer.timeout.connect(flash_fun) timer.start() layer.metadata["timer"] = timer def component_mark(self, num: int, flash: bool = False): self.component_unmark(num) self._search_type = SearchType.Highlight self._last_component = num bounding_box = self._bounding_box(num) if bounding_box is None: return for image_info in self.image_info.values(): self._mark_layer(num, flash, image_info) lower_bound, upper_bound = bounding_box self._update_point(lower_bound, upper_bound) if self.viewer.dims.ndisplay == 2: l_bound = lower_bound[-2:][::-1] u_bound = upper_bound[-2:][::-1] rect = Rect(self.viewer_widget.view.camera.get_state()["rect"]) if rect.contains(*l_bound) and rect.contains(*u_bound): return size = u_bound - l_bound rect.size = tuple(np.max([rect.size, size * 1.2], axis=0)) pos = rect.pos if rect.left > l_bound[0]: pos = l_bound[0], pos[1] if rect.right < u_bound[0]: pos = pos[0] + u_bound[0] - rect.right, pos[1] if rect.bottom > l_bound[1]: pos = pos[0], l_bound[1] if rect.top < u_bound[1]: pos = pos[0], pos[1] + (u_bound[1] - rect.top) rect.pos = pos self.viewer_widget.view.camera.set_state({"rect": rect}) def component_zoom(self, num): self.component_unmark(num) self._search_type = SearchType.Zoom_in self._last_component = num bounding_box = self._bounding_box(num) if bounding_box is None: return lower_bound, upper_bound = bounding_box diff = upper_bound - lower_bound frame = diff * 0.2 if self.viewer.dims.ndisplay == 2: rect = Rect(pos=(lower_bound - frame)[-2:][::-1], size=(diff + 2 * frame)[-2:][::-1]) self.set_state({"camera": {"rect": rect}}) self._update_point(lower_bound, upper_bound) def _update_point(self, lower_bound, upper_bound): point = (lower_bound + upper_bound) / 2 current_point = self.viewer.dims.point for i in range(self.viewer.dims.ndim - self.viewer.dims.ndisplay): if not (lower_bound[i] <= current_point[i] <= upper_bound[i]): self.viewer.dims.set_point(i, point[i]) @staticmethod def _data_to_world(layer: Layer, cords): return layer._transforms[1:3].simplified(cords) # pylint: disable=W0212 def _bounding_box(self, num) -> Optional[Tuple[np.ndarray, np.ndarray]]: lower_bound_list = [] upper_bound_list = [] for image_info in self.image_info.values(): bound_info = image_info.roi_info.bound_info.get(num, None) if bound_info is None: continue lower_bound_list.append( self._data_to_world(image_info.roi, bound_info.lower)) upper_bound_list.append( self._data_to_world(image_info.roi, bound_info.upper)) if not lower_bound_list: return lower_bound = np.min(lower_bound_list, axis=0) upper_bound = np.min(upper_bound_list, axis=0) return lower_bound, upper_bound