def test_contrast_limits(): """Test adding multichannel image with custom contrast limits.""" viewer = ViewerModel() np.random.seed(0) data = np.random.random((15, 10, 5)) clims = [0.3, 0.7] viewer.add_multichannel(data, contrast_limits=clims) assert len(viewer.layers) == data.shape[-1] for i in range(data.shape[-1]): assert viewer.layers[i].contrast_limits == clims viewer = ViewerModel() clims = [[0.3, 0.7], [0.1, 0.9], [0.3, 0.9], [0.4, 0.9], [0.2, 0.9]] viewer.add_multichannel(data, contrast_limits=clims) assert len(viewer.layers) == data.shape[-1] for i in range(data.shape[-1]): assert viewer.layers[i].contrast_limits == clims[i]
def test_not_mutable_fields(field): """Test appropriate fields are not mutable.""" viewer = ViewerModel() # Check attribute lives on the viewer assert hasattr(viewer, field) # Check attribute does not have an event emitter assert not hasattr(viewer.events, field) # Check attribute is not settable with pytest.raises(TypeError) as err: setattr(viewer, field, 'test') assert 'has allow_mutation set to False and cannot be assigned' in str( err.value)
def test_colormaps(): """Test adding multichannel image with custom colormaps.""" viewer = ViewerModel() np.random.seed(0) data = np.random.random((15, 10, 5)) colormap = 'gray' viewer.add_multichannel(data, colormap=colormap) assert len(viewer.layers) == data.shape[-1] for i in range(data.shape[-1]): assert viewer.layers[i].colormap[0] == colormap viewer = ViewerModel() colormaps = ['gray', 'blue', 'red', 'green', 'yellow'] viewer.add_multichannel(data, colormap=colormaps) assert len(viewer.layers) == data.shape[-1] for i in range(data.shape[-1]): assert viewer.layers[i].colormap[0] == colormaps[i]
def test_gamma(): """Test adding multichannel image with custom gamma.""" viewer = ViewerModel() np.random.seed(0) data = np.random.random((15, 10, 5)) gamma = 0.7 viewer.add_image(data, gamma=gamma, channel_axis=-1) assert len(viewer.layers) == data.shape[-1] for i in range(data.shape[-1]): assert viewer.layers[i].gamma == gamma viewer = ViewerModel() gammas = [0.3, 0.4, 0.5, 0.6, 0.7] viewer.add_image(data, gamma=gammas, channel_axis=-1) assert len(viewer.layers) == data.shape[-1] for i in range(data.shape[-1]): assert viewer.layers[i].gamma == gammas[i]
def test_qt_viewer(qtbot): """Test instantiating viewer.""" viewer = ViewerModel() view = QtViewer(viewer) qtbot.addWidget(view) assert viewer.title == 'napari' assert view.viewer == viewer assert len(viewer.layers) == 0 assert view.layers.vbox_layout.count() == 2 assert viewer.dims.ndim == 2 assert view.dims.nsliders == 0 assert np.sum(view.dims._displayed) == 0
def test_names(): """Test adding multichannel image with custom names.""" viewer = ViewerModel() np.random.seed(0) data = np.random.random((15, 10, 5)) names = ['multi ' + str(i + 3) for i in range(data.shape[-1])] viewer.add_image(data, name=names, channel_axis=-1) assert len(viewer.layers) == data.shape[-1] for i in range(data.shape[-1]): assert viewer.layers[i].name == names[i] viewer = ViewerModel() name = 'example' names = [name] + [name + f' [{i + 1}]' for i in range(data.shape[-1] - 1)] viewer.add_image(data, name=name, channel_axis=-1) assert len(viewer.layers) == data.shape[-1] for i in range(data.shape[-1]): assert viewer.layers[i].name == names[i]
def test_add_multi_png_defaults(two_pngs): image_files = two_pngs viewer = ViewerModel() viewer.open(image_files, stack=True, plugin='builtins') assert len(viewer.layers) == 1 assert viewer.dims.ndim == 3 assert isinstance(viewer.layers[0].data, da.Array) assert viewer.layers[0].data.shape == (2, 512, 512) viewer.open(image_files, stack=False, plugin='builtins') assert len(viewer.layers) == 3
def test_active_layer_status_update(): """Test status updates from active layer on cursor move.""" viewer = ViewerModel() np.random.seed(0) viewer.add_image(np.random.random((5, 5, 10, 15))) viewer.add_image(np.random.random((5, 6, 5, 10, 15))) assert len(viewer.layers) == 2 assert viewer.active_layer == viewer.layers[1] viewer.cursor.position = [1, 1, 1, 1, 1] assert viewer.status == viewer.active_layer.status
def test_active_layer_status_update(): """Test status updates from active layer on cursor move.""" viewer = ViewerModel() np.random.seed(0) viewer.add_image(np.random.random((5, 5, 10, 15))) viewer.add_image(np.random.random((5, 6, 5, 10, 15))) assert len(viewer.layers) == 2 assert viewer.layers.selection.active == viewer.layers[1] # wait 1 s to avoid the cursor event throttling time.sleep(1) viewer._mouse_over_canvas = True viewer.cursor.position = [1, 1, 1, 1, 1] assert viewer.status == viewer.layers.selection.active.get_status( viewer.cursor.position, world=True)
def test_new_labels_image(): """Test adding new labels layer with image present.""" viewer = ViewerModel() np.random.seed(0) data = np.random.random((10, 15)) viewer.add_image(data) viewer._new_labels() assert len(viewer.layers) == 2 assert np.max(viewer.layers[1].data) == 0 assert viewer.dims.ndim == 2 np.testing.assert_equal(viewer.layers[1].data.shape, (10, 15)) np.testing.assert_equal(viewer.layers[1].scale, (1, 1)) np.testing.assert_equal(viewer.layers[1].translate, (0, 0))
def test_new_labels_scaled_translated_image(): """Test adding new labels layer with transformed image present.""" viewer = ViewerModel() np.random.seed(0) data = np.random.random((10, 15)) viewer.add_image(data, scale=(3, 3), translate=(20, -5)) viewer._new_labels() assert len(viewer.layers) == 2 assert np.max(viewer.layers[1].data) == 0 assert viewer.dims.ndim == 2 np.testing.assert_almost_equal(viewer.layers[1].data.shape, (10, 15)) np.testing.assert_almost_equal(viewer.layers[1].scale, (3, 3)) np.testing.assert_almost_equal(viewer.layers[1].translate, (20, -5))
def test_add_delete_layers(): """Test adding and deleting layers with different dims.""" viewer = ViewerModel() np.random.seed(0) viewer.add_image(np.random.random((5, 5, 10, 15))) assert len(viewer.layers) == 1 assert viewer.dims.ndim == 4 viewer.add_image(np.random.random((5, 6, 5, 10, 15))) assert len(viewer.layers) == 2 assert viewer.dims.ndim == 5 viewer.layers.remove_selected() assert len(viewer.layers) == 1 assert viewer.dims.ndim == 4
def test_scaled_images(): """Test two scaled images.""" viewer = ViewerModel() np.random.seed(0) data = np.random.random((10, 10, 10)) viewer.add_image(data) viewer.add_image(data[::2], scale=[2, 1, 1]) assert viewer.dims.range[0] == (0, 10 - 1, 1) assert viewer.dims.range[1] == (0, 10 - 1, 1) assert viewer.dims.range[2] == (0, 10 - 1, 1) assert viewer.dims.nsteps == (10, 10, 10) for i in range(viewer.dims.nsteps[0]): viewer.dims.set_current_step(0, i) assert viewer.dims.current_step[0] == i
def test_translated_images(): """Test two translated images.""" viewer = ViewerModel() np.random.seed(0) data = np.random.random((10, 10, 10)) viewer.add_image(data) viewer.add_image(data, translate=[10, 0, 0]) assert viewer.dims.range[0] == (0, 20 - 1, 1) assert viewer.dims.range[1] == (0, 10 - 1, 1) assert viewer.dims.range[2] == (0, 10 - 1, 1) assert viewer.dims.nsteps == (20, 10, 10) for i in range(viewer.dims.nsteps[0]): viewer.dims.set_current_step(0, i) assert viewer.dims.current_step[0] == i
def test_both_scaled_and_translated_images(): """Test both scaled and translated images.""" viewer = ViewerModel() np.random.seed(0) data = np.random.random((10, 10, 10)) viewer.add_image(data, scale=[2, 1, 1]) viewer.add_image(data, scale=[2, 1, 1], translate=[20, 0, 0]) assert viewer.dims.range[0] == (0, 40 - 2, 2) assert viewer.dims.range[1] == (0, 10 - 1, 1) assert viewer.dims.range[2] == (0, 10 - 1, 1) assert viewer.dims.nsteps == (20, 10, 10) for i in range(viewer.dims.nsteps[0]): viewer.dims.set_current_step(0, i) assert viewer.dims.current_step[0] == i
def test_dask_cache_resizing(delayed_dask_stack): """Test that we can spin up, resize, and spin down the cache.""" # make sure we have a cache # big enough for 10+ (10, 10, 10) "timepoints" resize_dask_cache(100000) # add dask stack to the viewer, making sure to pass multiscale and clims v = ViewerModel() dask_stack = delayed_dask_stack['stack'] v.add_image(dask_stack) assert _dask_utils._DASK_CACHE.cache.available_bytes > 0 # make sure the cache actually has been populated assert len(_dask_utils._DASK_CACHE.cache.heap.heap) > 0 # we can resize that cache back to 0 bytes resize_dask_cache(0) assert _dask_utils._DASK_CACHE.cache.available_bytes == 0 # adding a 2nd stack should not adjust the cache size once created v.add_image(dask_stack) assert _dask_utils._DASK_CACHE.cache.available_bytes == 0 # and the cache will remain empty regardless of what we do for i in range(3): v.dims.set_point(1, i) assert len(_dask_utils._DASK_CACHE.cache.heap.heap) == 0 # but we can always spin it up again resize_dask_cache(1e4) assert _dask_utils._DASK_CACHE.cache.available_bytes == 1e4 # and adding a new image doesn't change the size v.add_image(dask_stack) assert _dask_utils._DASK_CACHE.cache.available_bytes == 1e4 # but the cache heap is getting populated again for i in range(3): v.dims.set_point(0, i) assert len(_dask_utils._DASK_CACHE.cache.heap.heap) > 0
def test_mix_dims(): """Test adding images of mixed dimensionality.""" viewer = ViewerModel() np.random.seed(0) data = np.random.random((10, 15)) viewer.add_image(data) assert len(viewer.layers) == 1 assert np.all(viewer.layers[0].data == data) assert viewer.dims.ndim == 2 data = np.random.random((6, 10, 15)) viewer.add_image(data) assert len(viewer.layers) == 2 assert np.all(viewer.layers[1].data == data) assert viewer.dims.ndim == 3
def test_active_layer_cursor_size(): """Test cursor size update on active layer.""" viewer = ViewerModel() np.random.seed(0) viewer.add_image(np.random.random((10, 10))) # Base layer has a default cursor size of 1 assert viewer.cursor.size == 1 viewer.add_labels(np.random.random((10, 10))) assert len(viewer.layers) == 2 assert viewer.active_layer == viewer.layers[1] viewer.layers[1].mode = 'paint' # Labels layer has a default cursor size of 10 # due to paintbrush assert viewer.cursor.size == 10
def test_add_layer_from_data_raises(): # make sure that adding invalid data or kwargs raises the right errors viewer = ViewerModel() # unrecognized layer type raises Value Error with pytest.raises(ValueError): # 'layer' is not a valid type # (even though there is an add_layer method) viewer._add_layer_from_data(np.random.random((10, 10)), layer_type='layer') # even with the correct meta kwargs, the underlying add_* method may raise with pytest.raises(ValueError): # improper dims for rgb data viewer._add_layer_from_data(np.random.random((10, 10, 6)), {'rgb': True}) # using a kwarg in the meta dict that is invalid for the corresponding # add_* method raises a TypeError with pytest.raises(TypeError): viewer._add_layer_from_data( np.random.random((10, 2, 2)) * 20, {'rgb': True}, # vectors do not have an 'rgb' kwarg layer_type='vectors', )
def test_scaled_and_translated_images(): """Test scaled and translated images.""" viewer = ViewerModel() np.random.seed(0) data = np.random.random((10, 10, 10)) viewer.add_image(data) viewer.add_image(data[::2], scale=[2, 1, 1], translate=[10, 0, 0]) assert viewer.dims.range[0] == ( 0, 19.5, 1, ) # TODO: non-integer with mixed scale? assert viewer.dims.range[1] == (0, 10, 1) assert viewer.dims.range[2] == (0, 10, 1) assert viewer.dims.nsteps == (19, 10, 10) for i in range(viewer.dims.nsteps[0]): viewer.dims.set_current_step(0, i) assert viewer.dims.current_step[0] == i
def test_add_remove_layer_external_callbacks(Layer, data, ndim): """Test external callbacks for layer emmitters preserved.""" viewer = ViewerModel() layer = Layer(data) # Check layer has been correctly created assert layer.ndim == ndim # Connect a custom callback def my_custom_callback(event): return layer.events.connect(my_custom_callback) # Check that no internal callbacks have been registered len(layer.events.callbacks) == 1 for em in layer.events.emitters.values(): if not isinstance(em, WarningEmitter): assert len(em.callbacks) == 1 viewer.layers.append(layer) # Check layer added correctly assert len(viewer.layers) == 1 # check that adding a layer created new callbacks assert any(len(em.callbacks) > 0 for em in layer.events.emitters.values()) viewer.layers.remove(layer) # Check layer added correctly assert len(viewer.layers) == 0 # Check that all internal callbacks have been removed assert len(layer.events.callbacks) == 1 for em in layer.events.emitters.values(): if not isinstance(em, WarningEmitter): assert len(em.callbacks) == 1
def test_add_image_colormap_variants(): """Test adding image with all valid colormap argument types.""" viewer = ViewerModel() np.random.seed(0) data = np.random.random((10, 15)) # as string assert viewer.add_image(data, colormap='green') # as string that is valid, but not a default colormap assert viewer.add_image(data, colormap='cubehelix') # as tuple cmap_tuple = ("my_colormap", Colormap(['g', 'm', 'y'])) assert viewer.add_image(data, colormap=cmap_tuple) # as dict cmap_dict = {"your_colormap": Colormap(['g', 'r', 'y'])} assert viewer.add_image(data, colormap=cmap_dict) # as Colormap instance blue_cmap = AVAILABLE_COLORMAPS['blue'] assert viewer.add_image(data, colormap=blue_cmap) # string values must be known colormap types with pytest.raises(KeyError) as err: viewer.add_image(data, colormap='nonsense') assert 'Colormap "nonsense" not found' in str(err.value) # lists are only valid with channel_axis with pytest.raises(TypeError) as err: viewer.add_image(data, colormap=['green', 'red']) assert "did you mean to specify a 'channel_axis'" in str(err.value)
def test_svg(): "Test generating svg" viewer = ViewerModel() np.random.seed(0) # Add image data = np.random.random((10, 15)) viewer.add_image(data) # Add labels data = np.random.randint(20, size=(10, 15)) viewer.add_labels(data) # Add points data = 20 * np.random.random((10, 2)) viewer.add_points(data) # Add vectors data = 20 * np.random.random((10, 2, 2)) viewer.add_vectors(data) # Add shapes data = 20 * np.random.random((10, 4, 2)) viewer.add_shapes(data) # Generate svg svg = viewer.to_svg() assert type(svg) == str
def test_viewer_object_event_sources(): viewer = ViewerModel() assert viewer.cursor.events.source is viewer.cursor assert viewer.camera.events.source is viewer.camera
def test_add_empty_points_to_empty_viewer(): viewer = ViewerModel() pts = viewer.add_points(name='empty points') assert pts.dims.ndim == 2 pts.add([1000.0, 27.0]) assert pts.data.shape == (1, 2)
def test_screenshot(qtbot): "Test taking a screenshot" viewer = ViewerModel() view = QtViewer(viewer) qtbot.addWidget(view) np.random.seed(0) # Add image data = np.random.random((10, 15)) viewer.add_image(data) # Add labels data = np.random.randint(20, size=(10, 15)) viewer.add_labels(data) # Add points data = 20 * np.random.random((10, 2)) viewer.add_points(data) # Add vectors data = 20 * np.random.random((10, 2, 2)) viewer.add_vectors(data) # Add shapes data = 20 * np.random.random((10, 4, 2)) viewer.add_shapes(data) # Take screenshot screenshot = view.screenshot() assert screenshot.ndim == 3
def test_add_image_multichannel_share_memory(): viewer = ViewerModel() image = np.random.random((10, 5, 64, 64)) layers = viewer.add_image(image, channel_axis=1) for layer in layers: assert np.may_share_memory(image, layer.data)
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)
def test_add_empty_points_to_empty_viewer(): viewer = ViewerModel() layer = viewer.add_points(name='empty points') assert layer.ndim == 2 layer.add([1000.0, 27.0]) assert layer.data.shape == (1, 2)
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")