from__future__importannotationsimportcopyimportinspectimportitertoolsimportloggingimportos.pathimportuuidimportwarningsfromabcimportABC,ABCMeta,abstractmethodfromcollectionsimportdefaultdictfromcollections.abcimportGenerator,Hashable,Mapping,Sequencefromcontextlibimportcontextmanagerfromfunctoolsimportcached_propertyfromtypingimport(TYPE_CHECKING,Any,Callable,ClassVar,Optional,Union,)importmagicguiasmguiimportnumpyasnpimportpintfromnpe2importplugin_manageraspmfromnapari.layers.base._base_constantsimport(BaseProjectionMode,Blending,Mode,)fromnapari.layers.base._base_mouse_bindingsimport(highlight_box_handles,transform_with_box,)fromnapari.layers.utils._slice_inputimport_SliceInput,_ThickNDSlicefromnapari.layers.utils.interactivity_utilsimport(drag_data_to_projected_distance,)fromnapari.layers.utils.layer_utilsimport(Extent,coerce_affine,compute_multiscale_level_and_corners,convert_to_uint8,dims_displayed_world_to_layer,get_extent_world,)fromnapari.layers.utils.planeimportClippingPlane,ClippingPlaneListfromnapari.settingsimportget_settingsfromnapari.utils._dask_utilsimportconfigure_daskfromnapari.utils._magicguiimport(add_layer_to_viewer,add_layers_to_viewer,get_layers,)fromnapari.utils.eventsimportEmitterGroup,Event,EventedDictfromnapari.utils.events.eventimportWarningEmitterfromnapari.utils.geometryimport(find_front_back_face,intersect_line_with_axis_aligned_bounding_box_3d,)fromnapari.utils.key_bindingsimportKeymapProviderfromnapari.utils.migrationsimport_DeprecatingDictfromnapari.utils.miscimportStringEnumfromnapari.utils.mouse_bindingsimportMousemapProviderfromnapari.utils.namingimportmagic_namefromnapari.utils.status_messagesimportgenerate_layer_coords_statusfromnapari.utils.transformsimportAffine,CompositeAffine,TransformChainfromnapari.utils.translationsimporttransifTYPE_CHECKING:importnumpy.typingasnptfromnapari.components.dimsimportDimsfromnapari.components.overlays.baseimportOverlayfromnapari.layers._sourceimportSourcelogger=logging.getLogger('napari.layers.base.base')defno_op(layer:Layer,event:Event)->None:""" A convenient no-op event for the layer mouse binding. This makes it easier to handle many cases by inserting this as as place holder Parameters ---------- layer : Layer Current layer on which this will be bound as a callback event : Event event that triggered this mouse callback. Returns ------- None """returnclassPostInit(ABCMeta):def__init__(self,*args,**kwargs):super().__init__(*args,**kwargs)sig=inspect.signature(self.__init__)params=tuple(sig.parameters.values())self.__signature__=sig.replace(parameters=params[1:])def__call__(self,*args,**kwargs):obj=super().__call__(*args,**kwargs)obj._post_init()returnobj
[docs]@mgui.register_type(choices=get_layers,return_callback=add_layer_to_viewer)classLayer(KeymapProvider,MousemapProvider,ABC,metaclass=PostInit):"""Base layer class. Parameters ---------- data : array or list of array Data that the layer is visualizing. Can be N-dimensional. ndim : int Number of spatial dimensions. affine : n-D array or napari.utils.transforms.Affine (N+1, N+1) affine transformation matrix in homogeneous coordinates. The first (N, N) entries correspond to a linear transform and the final column is a length N translation vector and a 1 or a napari `Affine` transform object. Applied as an extra transform on top of the provided scale, rotate, and shear values. axis_labels : tuple of str, optional Dimension names of the layer data. If not provided, axis_labels will be set to (..., 'axis -2', 'axis -1'). blending : str One of a list of preset blending modes that determines how RGB and alpha values of the layer visual get mixed. Allowed values are {'opaque', 'translucent', 'translucent_no_depth', 'additive', and 'minimum'}. cache : bool Whether slices of out-of-core datasets should be cached upon retrieval. Currently, this only applies to dask arrays. experimental_clipping_planes : list of dicts, list of ClippingPlane, or ClippingPlaneList Each dict defines a clipping plane in 3D in data coordinates. Valid dictionary keys are {'position', 'normal', and 'enabled'}. Values on the negative side of the normal are discarded if the plane is enabled. metadata : dict Layer metadata. mode: str The layer's interactive mode. multiscale : bool Whether the data is multiscale or not. Multiscale data is represented by a list of data objects and should go from largest to smallest. name : str, optional Name of the layer. If not provided then will be guessed using heuristics. opacity : float Opacity of the layer visual, between 0.0 and 1.0. projection_mode : str How data outside the viewed dimensions but inside the thick Dims slice will be projected onto the viewed dimensions. Must fit to cls._projectionclass. rotate : float, 3-tuple of float, or n-D array. If a float convert into a 2D rotation matrix using that value as an angle. If 3-tuple convert into a 3D rotation matrix, using a yaw, pitch, roll convention. Otherwise assume an nD rotation. Angles are assumed to be in degrees. They can be converted from radians with np.degrees if needed. scale : tuple of float Scale factors for the layer. shear : 1-D array or n-D array Either a vector of upper triangular values, or an nD shear matrix with ones along the main diagonal. translate : tuple of float Translation values for the layer. units : tuple of str or pint.Unit, optional Units of the layer data in world coordinates. If not provided, the default units are assumed to be pixels. visible : bool Whether the layer visual is currently being displayed. Attributes ---------- affine : n-D array or napari.utils.transforms.Affine (N+1, N+1) affine transformation matrix in homogeneous coordinates. The first (N, N) entries correspond to a linear transform and the final column is a length N translation vector and a 1 or a napari `Affine` transform object. Applied as an extra transform on top of the provided scale, rotate, and shear values. axis_labels : tuple of str Dimension names of the layer data. blending : Blending Determines how RGB and alpha values get mixed. * ``Blending.OPAQUE`` Allows for only the top layer to be visible and corresponds to ``depth_test=True``, ``cull_face=False``, ``blend=False``. * ``Blending.TRANSLUCENT`` Allows for multiple layers to be blended with different opacity and corresponds to ``depth_test=True``, ``cull_face=False``, ``blend=True``, ``blend_func=('src_alpha', 'one_minus_src_alpha')``, and ``blend_equation=('func_add')``. * ``Blending.TRANSLUCENT_NO_DEPTH`` Allows for multiple layers to be blended with different opacity, but no depth testing is performed. Corresponds to ``depth_test=False``, ``cull_face=False``, ``blend=True``, ``blend_func=('src_alpha', 'one_minus_src_alpha')``, and ``blend_equation=('func_add')``. * ``Blending.ADDITIVE`` Allows for multiple layers to be blended together with different colors and opacity. Useful for creating overlays. It corresponds to ``depth_test=False``, ``cull_face=False``, ``blend=True``, ``blend_func=('src_alpha', 'one')``, and ``blend_equation=('func_add')``. * ``Blending.MINIMUM`` Allows for multiple layers to be blended together such that the minimum of each RGB component and alpha are selected. Useful for creating overlays with inverted colormaps. It corresponds to ``depth_test=False``, ``cull_face=False``, ``blend=True``, ``blend_equation=('min')``. cache : bool Whether slices of out-of-core datasets should be cached upon retrieval. Currently, this only applies to dask arrays. corner_pixels : array Coordinates of the top-left and bottom-right canvas pixels in the data coordinates of each layer. For multiscale data the coordinates are in the space of the currently viewed data level, not the highest resolution level. cursor : str String identifying which cursor displayed over canvas. cursor_size : int | None Size of cursor if custom. None yields default size help : str Displayed in status bar bottom right. interactive : bool Determine if canvas pan/zoom interactivity is enabled. This attribute is deprecated since 0.5.0 and should not be used. Use the mouse_pan and mouse_zoom attributes instead. mouse_pan : bool Determine if canvas interactive panning is enabled with the mouse. mouse_zoom : bool Determine if canvas interactive zooming is enabled with the mouse. multiscale : bool Whether the data is multiscale or not. Multiscale data is represented by a list of data objects and should go from largest to smallest. name : str Unique name of the layer. ndim : int Dimensionality of the layer. opacity : float Opacity of the layer visual, between 0.0 and 1.0. projection_mode : str How data outside the viewed dimensions but inside the thick Dims slice will be projected onto the viewed dimenions. rotate : float, 3-tuple of float, or n-D array. If a float convert into a 2D rotation matrix using that value as an angle. If 3-tuple convert into a 3D rotation matrix, using a yaw, pitch, roll convention. Otherwise assume an nD rotation. Angles are assumed to be in degrees. They can be converted from radians with np.degrees if needed. scale : tuple of float Scale factors for the layer. scale_factor : float Conversion factor from canvas coordinates to image coordinates, which depends on the current zoom level. shear : 1-D array or n-D array Either a vector of upper triangular values, or an nD shear matrix with ones along the main diagonal. source : Source source of the layer (such as a plugin or widget) status : str Displayed in status bar bottom left. translate : tuple of float Translation values for the layer. thumbnail : (N, M, 4) array Array of thumbnail data for the layer. unique_id : Hashable Unique id of the layer. Guaranteed to be unique across the lifetime of a viewer. visible : bool Whether the layer visual is currently being displayed. units: tuple of pint.Unit Units of the layer data in world coordinates. z_index : int Depth of the layer visual relative to other visuals in the scenecanvas. Notes ----- Must define the following: * `_extent_data`: property * `data` property (setter & getter) May define the following: * `_set_view_slice()`: called to set currently viewed slice * `_basename()`: base/default name of the layer """_modeclass:type[StringEnum]=Mode_projectionclass:type[StringEnum]=BaseProjectionModeModeCallable=Callable[['Layer',Event],Union[None,Generator[None,None,None]]]_drag_modes:ClassVar[dict[StringEnum,ModeCallable]]={Mode.PAN_ZOOM:no_op,Mode.TRANSFORM:transform_with_box,}_move_modes:ClassVar[dict[StringEnum,ModeCallable]]={Mode.PAN_ZOOM:no_op,Mode.TRANSFORM:highlight_box_handles,}_cursor_modes:ClassVar[dict[StringEnum,str]]={Mode.PAN_ZOOM:'standard',Mode.TRANSFORM:'standard',}events:EmitterGroupdef__init__(self,data,ndim,*,affine=None,axis_labels=None,blending='translucent',cache=True,# this should move to future "data source" object.experimental_clipping_planes=None,metadata=None,mode='pan_zoom',multiscale=False,name=None,opacity=1.0,projection_mode='none',rotate=None,scale=None,shear=None,translate=None,units=None,visible=True,):super().__init__()ifnameisNoneanddataisnotNone:name=magic_name(data)ifscaleisnotNoneandnotnp.all(scale):raiseValueError(trans._("Layer {name} is invalid because it has scale values of 0. The layer's scale is currently {scale}",deferred=True,name=repr(name),scale=repr(scale),))# Needs to be imported here to avoid circular import in _sourcefromnapari.layers._sourceimportcurrent_sourceself._highlight_visible=Trueself._unique_id=Noneself._source=current_source()self.dask_optimized_slicing=configure_dask(data,cache)self._metadata=dict(metadataor{})self._opacity=opacityself._blending=Blending(blending)self._visible=visibleself._visible_mode=Noneself._freeze=Falseself._status='Ready'self._help=''self._cursor='standard'self._cursor_size=1self._mouse_pan=Trueself._mouse_zoom=Trueself._value=Noneself.scale_factor=1self.multiscale=multiscaleself._experimental_clipping_planes=ClippingPlaneList()self._mode=self._modeclass('pan_zoom')self._projection_mode=self._projectionclass(str(projection_mode))self._refresh_blocked=Falseself._ndim=ndimself._slice_input=_SliceInput(ndisplay=2,world_slice=_ThickNDSlice.make_full(ndim=ndim),order=tuple(range(ndim)),)self._loaded:bool=Trueself._last_slice_id:int=-1# Create a transform chain consisting of four transforms:# 1. `tile2data`: An initial transform only needed to display tiles# of an image. It maps pixels of the tile into the coordinate space# of the full resolution data and can usually be represented by a# scale factor and a translation. A common use case is viewing part# of lower resolution level of a multiscale image, another is using a# downsampled version of an image when the full image size is larger# than the maximum allowed texture size of your graphics card.# 2. `data2physical`: The main transform mapping data to a world-like# physical coordinate that may also encode acquisition parameters or# sample spacing.# 3. `physical2world`: An extra transform applied in world-coordinates that# typically aligns this layer with another.# 4. `world2grid`: An additional transform mapping world-coordinates# into a grid for looking at layers side-by-side.ifscaleisNone:scale=[1]*ndimiftranslateisNone:translate=[0]*ndimself._initial_affine=coerce_affine(affine,ndim=ndim,name='physical2world')self._transforms:TransformChain[Affine]=TransformChain([Affine(np.ones(ndim),np.zeros(ndim),name='tile2data'),CompositeAffine(scale,translate,axis_labels=axis_labels,rotate=rotate,shear=shear,ndim=ndim,name='data2physical',units=units,),self._initial_affine,Affine(np.ones(ndim),np.zeros(ndim),name='world2grid'),])self.corner_pixels=np.zeros((2,ndim),dtype=int)self._editable=Trueself._array_like=Falseself._thumbnail_shape=(32,32,4)self._thumbnail=np.zeros(self._thumbnail_shape,dtype=np.uint8)self._update_properties=Trueself._name=''self.experimental_clipping_planes=experimental_clipping_planes# circular importfromnapari.components.overlays.bounding_boximportBoundingBoxOverlayfromnapari.components.overlays.interaction_boximport(SelectionBoxOverlay,TransformBoxOverlay,)self._overlays:EventedDict[str,Overlay]=EventedDict()self.events=EmitterGroup(source=self,axis_labels=Event,data=Event,metadata=Event,affine=Event,blending=Event,cursor=Event,cursor_size=Event,editable=Event,extent=Event,help=Event,loaded=Event,mode=Event,mouse_pan=Event,mouse_zoom=Event,name=Event,opacity=Event,projection_mode=Event,refresh=Event,reload=Event,rotate=Event,scale=Event,set_data=Event,shear=Event,status=Event,thumbnail=Event,translate=Event,units=Event,visible=Event,interactive=WarningEmitter(trans._('layer.events.interactive is deprecated since 0.4.18 and will be removed in 0.6.0. Please use layer.events.mouse_pan and layer.events.mouse_zoom',deferred=True,),type_name='interactive',),_extent_augmented=Event,_overlays=Event,)self.name=nameself.mode=modeself._overlays.update({'transform_box':TransformBoxOverlay(),'selection_box':SelectionBoxOverlay(),'bounding_box':BoundingBoxOverlay(),})# TODO: we try to avoid inner event connection, but this might be the only way# until we figure out nested evented objectsself._overlays.events.connect(self.events._overlays)def_post_init(self):"""Post init hook for subclasses to use."""def__str__(self)->str:"""Return self.name."""returnself.namedef__repr__(self)->str:cls=type(self)returnf'<{cls.__name__} layer {self.name!r} at {hex(id(self))}>'def_mode_setter_helper(self,mode_in:Union[Mode,str])->StringEnum:""" Helper to manage callbacks in multiple layers This will return a valid mode for the current layer, to for example refuse to set a mode that is not supported by the layer if it is not editable. This will as well manage the mouse callbacks. Parameters ---------- mode : type(self._modeclass) | str New mode for the current layer. Returns ------- mode : type(self._modeclass) New mode for the current layer. """mode=self._modeclass(mode_in)# Sub-classes can have their own Mode enum, so need to get members# from the specific mode class set on this layer.PAN_ZOOM=self._modeclass.PAN_ZOOM# type: ignore[attr-defined]TRANSFORM=self._modeclass.TRANSFORM# type: ignore[attr-defined]assertmodeisnotNoneifnotself.editableornotself.visible:mode=PAN_ZOOMifmode==self._mode:returnmodeifmodenotinself._modeclass:raiseValueError(trans._('Mode not recognized: {mode}',deferred=True,mode=mode))forcallback_list,mode_dictin[(self.mouse_drag_callbacks,self._drag_modes),(self.mouse_move_callbacks,self._move_modes),(self.mouse_double_click_callbacks,getattr(self,'_double_click_modes',defaultdict(lambda:no_op)),),]:ifmode_dict[self._mode]incallback_list:callback_list.remove(mode_dict[self._mode])callback_list.append(mode_dict[mode])self.cursor=self._cursor_modes[mode]self.mouse_pan=mode==PAN_ZOOMself._overlays['transform_box'].visible=mode==TRANSFORMifmode==TRANSFORM:self.help=trans._('hold <space> to pan/zoom, hold <shift> to preserve aspect ratio and rotate in 45° increments')elifmode==PAN_ZOOM:self.help=''returnmodedefupdate_transform_box_visibility(self,visible):if'transform_box'inself._overlays:TRANSFORM=self._modeclass.TRANSFORM# type: ignore[attr-defined]self._overlays['transform_box'].visible=(self.mode==TRANSFORMandvisible)defupdate_highlight_visibility(self,visible):self._highlight_visible=visibleself._set_highlight(force=True)@propertydefmode(self)->str:"""str: Interactive mode Interactive mode. The normal, default mode is PAN_ZOOM, which allows for normal interactivity with the canvas. TRANSFORM allows for manipulation of the layer transform. """returnstr(self._mode)@mode.setterdefmode(self,mode:Union[Mode,str])->None:mode_enum=self._mode_setter_helper(mode)ifmode_enum==self._mode:returnself._mode=mode_enumself.events.mode(mode=str(mode_enum))@propertydefprojection_mode(self):"""Mode of projection of the thick slice onto the viewed dimensions. The sliced data is described by an n-dimensional bounding box ("thick slice"), which needs to be projected onto the visible dimensions to be visible. The projection mode controls the projection logic. """returnself._projection_mode@projection_mode.setterdefprojection_mode(self,mode):mode=self._projectionclass(str(mode))ifself._projection_mode!=mode:self._projection_mode=modeself.events.projection_mode()self.refresh(extent=False)@propertydefunique_id(self)->Hashable:"""Unique ID of the layer. This is guaranteed to be unique to this specific layer instance over the lifetime of the program. """ifself._unique_idisNone:self._unique_id=uuid.uuid4()returnself._unique_id@classmethoddef_basename(cls)->str:returnf'{cls.__name__}'@propertydefname(self)->str:"""str: Unique name of the layer."""returnself._name@name.setterdefname(self,name:Optional[str])->None:ifname==self.name:returnifnotname:name=self._basename()self._name=str(name)self.events.name()@propertydefmetadata(self)->dict:"""Key/value map for user-stored data."""returnself._metadata@metadata.setterdefmetadata(self,value:dict)->None:self._metadata.clear()self._metadata.update(value)self.events.metadata()@propertydefsource(self)->Source:returnself._source@propertydefloaded(self)->bool:"""True if this layer is fully loaded in memory, False otherwise. Layers that only support sync slicing are always fully loaded. Layers that support async slicing can be temporarily not loaded while slicing is occurring. """returnself._loadeddef_set_loaded(self,loaded:bool)->None:"""Set the loaded state and notify a change with the loaded event."""ifself._loaded!=loaded:self._loaded=loadedself.events.loaded()def_set_unloaded_slice_id(self,slice_id:int)->None:"""Set this layer to be unloaded and associated with a pending slice ID. This is private but accessed externally because it is related to slice state, which is intended to be moved off the layer in the future. """self._last_slice_id=slice_idself._set_loaded(False)def_update_loaded_slice_id(self,slice_id:int)->None:"""Potentially update the loaded state based on the given completed slice ID. This is private but accessed externally because it is related to slice state, which is intended to be moved off the layer in the future. """ifself._last_slice_id==slice_id:self._set_loaded(True)@propertydefopacity(self)->float:"""float: Opacity value between 0.0 and 1.0."""returnself._opacity@opacity.setterdefopacity(self,opacity:float)->None:ifnot0.0<=opacity<=1.0:raiseValueError(trans._('opacity must be between 0.0 and 1.0; got {opacity}',deferred=True,opacity=opacity,))self._opacity=float(opacity)self._update_thumbnail()self.events.opacity()@propertydefblending(self)->str:"""Blending mode: Determines how RGB and alpha values get mixed. Blending.OPAQUE Allows for only the top layer to be visible and corresponds to depth_test=True, cull_face=False, blend=False. Blending.TRANSLUCENT Allows for multiple layers to be blended with different opacity and corresponds to depth_test=True, cull_face=False, blend=True, blend_func=('src_alpha', 'one_minus_src_alpha'), and blend_equation=('func_add'). Blending.TRANSLUCENT_NO_DEPTH Allows for multiple layers to be blended with different opacity, but no depth testing is performed. Corresponds to ``depth_test=False``, cull_face=False, blend=True, blend_func=('src_alpha', 'one_minus_src_alpha'), and blend_equation=('func_add'). Blending.ADDITIVE Allows for multiple layers to be blended together with different colors and opacity. Useful for creating overlays. It corresponds to depth_test=False, cull_face=False, blend=True, blend_func=('src_alpha', 'one'), and blend_equation=('func_add'). Blending.MINIMUM Allows for multiple layers to be blended together such that the minimum of each RGB component and alpha are selected. Useful for creating overlays with inverted colormaps. It corresponds to depth_test=False, cull_face=False, blend=True, blend_equation=('min'). """returnstr(self._blending)@blending.setterdefblending(self,blending):self._blending=Blending(blending)self.events.blending()@propertydefvisible(self)->bool:"""bool: Whether the visual is currently being displayed."""returnself._visible@visible.setterdefvisible(self,visible:bool)->None:self._visible=visibleifvisible:# needed because things might have changed while invisible# and refresh is noop while invisibleself.refresh(extent=False)self._on_visible_changed()self.events.visible()def_on_visible_changed(self)->None:"""Execute side-effects on this layer related to changes of the visible state."""ifself.visibleandself._visible_mode:self.mode=self._visible_modeelse:self._visible_mode=self.modeself.mode=self._modeclass.PAN_ZOOM# type: ignore[attr-defined]@propertydefeditable(self)->bool:"""bool: Whether the current layer data is editable from the viewer."""returnself._editable@editable.setterdefeditable(self,editable:bool)->None:ifself._editable==editable:returnself._editable=editableself._on_editable_changed()self.events.editable()def_reset_editable(self)->None:"""Reset this layer's editable state based on layer properties."""self.editable=Truedef_on_editable_changed(self)->None:"""Executes side-effects on this layer related to changes of the editable state."""@propertydefaxis_labels(self)->tuple[str,...]:"""tuple of axis labels for the layer."""returnself._transforms['data2physical'].axis_labels@axis_labels.setterdefaxis_labels(self,axis_labels:Optional[Sequence[str]])->None:prev=self._transforms['data2physical'].axis_labels# mypy bug https://github.com/python/mypy/issues/3004self._transforms['data2physical'].axis_labels=axis_labels# type: ignore[assignment]ifself._transforms['data2physical'].axis_labels!=prev:self.events.axis_labels()@propertydefunits(self)->tuple[pint.Unit,...]:"""List of units for the layer."""returnself._transforms['data2physical'].units@units.setterdefunits(self,units:Optional[Sequence[pint.Unit]])->None:prev=self.units# mypy bug https://github.com/python/mypy/issues/3004self._transforms['data2physical'].units=units# type: ignore[assignment]ifself.units!=prev:self.events.units()@propertydefscale(self)->npt.NDArray:"""array: Anisotropy factors to scale data into world coordinates."""returnself._transforms['data2physical'].scale@scale.setterdefscale(self,scale:Optional[npt.NDArray])->None:ifscaleisNone:scale=np.array([1]*self.ndim)self._transforms['data2physical'].scale=np.array(scale)self.refresh()self.events.scale()@propertydeftranslate(self)->npt.NDArray:"""array: Factors to shift the layer by in units of world coordinates."""returnself._transforms['data2physical'].translate@translate.setterdeftranslate(self,translate:npt.ArrayLike)->None:self._transforms['data2physical'].translate=np.array(translate)self.refresh()self.events.translate()@propertydefrotate(self)->npt.NDArray:"""array: Rotation matrix in world coordinates."""returnself._transforms['data2physical'].rotate@rotate.setterdefrotate(self,rotate:npt.NDArray)->None:self._transforms['data2physical'].rotate=rotateself.refresh()self.events.rotate()@propertydefshear(self)->npt.NDArray:"""array: Shear matrix in world coordinates."""returnself._transforms['data2physical'].shear@shear.setterdefshear(self,shear:npt.NDArray)->None:self._transforms['data2physical'].shear=shearself.refresh()self.events.shear()@propertydefaffine(self)->Affine:"""napari.utils.transforms.Affine: Extra affine transform to go from physical to world coordinates."""returnself._transforms['physical2world']@affine.setterdefaffine(self,affine:Union[npt.ArrayLike,Affine])->None:# Assignment by transform name is not supported by TransformChain and# EventedList, so use the integer index instead. For more details, see:# https://github.com/napari/napari/issues/3058self._transforms[2]=coerce_affine(affine,ndim=self.ndim,name='physical2world')self.refresh()self.events.affine()def_reset_affine(self)->None:self.affine=self._initial_affine@propertydef_translate_grid(self)->npt.NDArray:"""array: Factors to shift the layer by."""returnself._transforms['world2grid'].translate@_translate_grid.setterdef_translate_grid(self,translate_grid:npt.NDArray)->None:ifnp.array_equal(self._translate_grid,translate_grid):returnself._transforms['world2grid'].translate=np.array(translate_grid)self.events.translate()def_update_dims(self)->None:"""Update the dimensionality of transforms and slices when data changes."""ndim=self._get_ndim()old_ndim=self._ndimifold_ndim>ndim:keep_axes=range(old_ndim-ndim,old_ndim)self._transforms=self._transforms.set_slice(keep_axes)elifold_ndim<ndim:new_axes=range(ndim-old_ndim)self._transforms=self._transforms.expand_dims(new_axes)self._slice_input=self._slice_input.with_ndim(ndim)self._ndim=ndimself.refresh()@property@abstractmethoddefdata(self):# user writes own docstringraiseNotImplementedError@data.setter@abstractmethoddefdata(self,data):raiseNotImplementedError@property@abstractmethoddef_extent_data(self)->np.ndarray:"""Extent of layer in data coordinates. Returns ------- extent_data : array, shape (2, D) """raiseNotImplementedError@propertydef_extent_data_augmented(self)->np.ndarray:"""Extent of layer in data coordinates. Differently from Layer._extent_data, this also includes the "size" of data points; for example, Point sizes and Image pixel width are included. Returns ------- extent_data : array, shape (2, D) """returnself._extent_data@propertydef_extent_world(self)->np.ndarray:"""Range of layer in world coordinates. Returns ------- extent_world : array, shape (2, D) """# Get full nD bounding boxreturnget_extent_world(self._extent_data,self._data_to_world)@propertydef_extent_world_augmented(self)->np.ndarray:"""Range of layer in world coordinates. Differently from Layer._extent_world, this also includes the "size" of data points; for example, Point sizes and Image pixel width are included. Returns ------- extent_world : array, shape (2, D) """# Get full nD bounding boxreturnget_extent_world(self._extent_data_augmented,self._data_to_world)@cached_propertydefextent(self)->Extent:"""Extent of layer in data and world coordinates. For image-like layers, these coordinates are the locations of the pixels in `Layer.data` which are treated like sample points that are centered in the rendered version of those pixels. For other layers, these coordinates are the points or vertices stored in `Layer.data`. Lower and upper bounds are inclusive. """extent_data=self._extent_datadata_to_world=self._data_to_worldextent_world=get_extent_world(extent_data,data_to_world)returnExtent(data=extent_data,world=extent_world,step=abs(data_to_world.scale),)@cached_propertydef_extent_augmented(self)->Extent:"""Augmented extent of layer in data and world coordinates. Differently from Layer.extent, this also includes the "size" of data points; for example, Point sizes and Image pixel width are included. For image-like layers, these coordinates are the locations of the pixels in `Layer.data` which are treated like sample points that are centered in the rendered version of those pixels. For other layers, these coordinates are the points or vertices stored in `Layer.data`. """extent_data=self._extent_data_augmenteddata_to_world=self._data_to_worldextent_world=get_extent_world(extent_data,data_to_world)returnExtent(data=extent_data,world=extent_world,step=abs(data_to_world.scale),)def_clear_extent(self)->None:"""Clear extent cache and emit extent event."""if'extent'inself.__dict__:delself.extentself.events.extent()def_clear_extent_augmented(self)->None:"""Clear extent_augmented cache and emit extent_augmented event."""if'_extent_augmented'inself.__dict__:delself._extent_augmentedself.events._extent_augmented()@propertydef_data_slice(self)->_ThickNDSlice:"""Slice in data coordinates."""iflen(self._slice_input.not_displayed)==0:# all dims are displayed dimensions# early return to avoid evaluating data_to_world.inversereturn_ThickNDSlice.make_full(point=(np.nan,)*self.ndim)returnself._slice_input.data_slice(self._data_to_world.inverse,)@abstractmethoddef_get_ndim(self)->int:raiseNotImplementedErrordef_get_base_state(self)->dict[str,Any]:"""Get dictionary of attributes on base layer. This is useful for serialization and deserialization of the layer. And similarly for plugins to pass state without direct dependencies on napari types. Returns ------- dict of str to Any Dictionary of attributes on base layer. """base_dict={'affine':self.affine.affine_matrix,'axis_labels':self.axis_labels,'blending':self.blending,'experimental_clipping_planes':[plane.dict()forplaneinself.experimental_clipping_planes],'metadata':self.metadata,'name':self.name,'opacity':self.opacity,'projection_mode':self.projection_mode,'rotate':[list(r)forrinself.rotate],'scale':list(self.scale),'shear':list(self.shear),'translate':list(self.translate),'units':self.units,'visible':self.visible,}returnbase_dict@abstractmethoddef_get_state(self)->dict[str,Any]:raiseNotImplementedError@propertydef_type_string(self)->str:returnself.__class__.__name__.lower()defas_layer_data_tuple(self):state=self._get_state()state.pop('data',None)ifhasattr(self.__init__,'_rename_argument'):state=_DeprecatingDict(state)forelementinself.__init__._rename_argument:state.set_deprecated_from_rename(**element._asdict())returnself.data,state,self._type_string@propertydefthumbnail(self)->npt.NDArray[np.uint8]:"""array: Integer array of thumbnail for the layer"""returnself._thumbnail@thumbnail.setterdefthumbnail(self,thumbnail:npt.NDArray)->None:if0inthumbnail.shape:thumbnail=np.zeros(self._thumbnail_shape,dtype=np.uint8)ifthumbnail.dtype!=np.uint8:thumbnail=convert_to_uint8(thumbnail)padding_needed=np.subtract(self._thumbnail_shape,thumbnail.shape)pad_amounts=[(p//2,(p+1)//2)forpinpadding_needed]thumbnail=np.pad(thumbnail,pad_amounts,mode='constant')# blend thumbnail with opaque black backgroundbackground=np.zeros(self._thumbnail_shape,dtype=np.uint8)background[...,3]=255f_dest=thumbnail[...,3][...,None]/255f_source=1-f_destthumbnail=thumbnail*f_dest+background*f_sourceself._thumbnail=thumbnail.astype(np.uint8)self.events.thumbnail()@propertydefndim(self)->int:"""int: Number of dimensions in the data."""returnself._ndim@propertydefhelp(self)->str:"""str: displayed in status bar bottom right."""returnself._help@help.setterdefhelp(self,help_text:str)->None:ifhelp_text==self.help:returnself._help=help_textself.events.help(help=help_text)@propertydefinteractive(self)->bool:warnings.warn(trans._('Layer.interactive is deprecated since napari 0.4.18 and will be removed in 0.6.0. Please use Layer.mouse_pan and Layer.mouse_zoom instead'),FutureWarning,stacklevel=2,)returnself.mouse_panorself.mouse_zoom@interactive.setterdefinteractive(self,interactive:bool)->None:warnings.warn(trans._('Layer.interactive is deprecated since napari 0.4.18 and will be removed in 0.6.0. Please use Layer.mouse_pan and Layer.mouse_zoom instead'),FutureWarning,stacklevel=2,)withself.events.interactive.blocker():self.mouse_pan=interactiveself.mouse_zoom=interactive@propertydefmouse_pan(self)->bool:"""bool: Determine if canvas interactive panning is enabled with the mouse."""returnself._mouse_pan@mouse_pan.setterdefmouse_pan(self,mouse_pan:bool)->None:ifmouse_pan==self._mouse_pan:returnself._mouse_pan=mouse_panself.events.mouse_pan(mouse_pan=mouse_pan)self.events.interactive(interactive=self.mouse_panorself.mouse_zoom)# Deprecated since 0.5.0@propertydefmouse_zoom(self)->bool:"""bool: Determine if canvas interactive zooming is enabled with the mouse."""returnself._mouse_zoom@mouse_zoom.setterdefmouse_zoom(self,mouse_zoom:bool)->None:ifmouse_zoom==self._mouse_zoom:returnself._mouse_zoom=mouse_zoomself.events.mouse_zoom(mouse_zoom=mouse_zoom)self.events.interactive(interactive=self.mouse_panorself.mouse_zoom)# Deprecated since 0.5.0@propertydefcursor(self)->str:"""str: String identifying cursor displayed over canvas."""returnself._cursor@cursor.setterdefcursor(self,cursor:str)->None:ifcursor==self.cursor:returnself._cursor=cursorself.events.cursor(cursor=cursor)@propertydefcursor_size(self)->int:"""int: Size of cursor if custom. None yields default size."""returnself._cursor_size@cursor_size.setterdefcursor_size(self,cursor_size:int)->None:ifcursor_size==self.cursor_size:returnself._cursor_size=cursor_sizeself.events.cursor_size(cursor_size=cursor_size)@propertydefexperimental_clipping_planes(self)->ClippingPlaneList:returnself._experimental_clipping_planes@experimental_clipping_planes.setterdefexperimental_clipping_planes(self,value:Union[dict,ClippingPlane,list[Union[ClippingPlane,dict]],ClippingPlaneList,],)->None:self._experimental_clipping_planes.clear()ifvalueisNone:returnifisinstance(value,(ClippingPlane,dict)):value=[value]fornew_planeinvalue:plane=ClippingPlane()plane.update(new_plane)self._experimental_clipping_planes.append(plane)@propertydefbounding_box(self)->Overlay:returnself._overlays['bounding_box']defset_view_slice(self)->None:withself.dask_optimized_slicing():self._set_view_slice()@abstractmethoddef_set_view_slice(self):raiseNotImplementedErrordef_slice_dims(self,dims:Dims,force:bool=False,)->None:"""Slice data with values from a global dims model. Note this will likely be moved off the base layer soon. Parameters ---------- dims : Dims The dims model to use to slice this layer. force : bool True if slicing should be forced to occur, even when some cache thinks it already has a valid slice ready. False otherwise. """logger.debug('Layer._slice_dims: %s, dims=%s, force=%s',self,dims,force,)slice_input=self._make_slice_input(dims)ifforceor(self._slice_input!=slice_input):self._slice_input=slice_inputself._refresh_sync(data_displayed=True,thumbnail=True,highlight=True,extent=True,)def_make_slice_input(self,dims:Dims,)->_SliceInput:world_ndim:int=self.ndimifdimsisNoneelsedims.ndimifdimsisNone:# if no dims is given, "world" has same dimensionality of self# this happens for example if a layer is not in a viewer# in this case, we assume all dims are displayed dimensionsworld_slice=_ThickNDSlice.make_full((np.nan,)*self.ndim)else:world_slice=_ThickNDSlice.from_dims(dims)order_array=(np.arange(world_ndim)ifdims.orderisNoneelsenp.asarray(dims.order))order=tuple(self._world_to_layer_dims(world_dims=order_array,ndim_world=world_ndim,))return_SliceInput(ndisplay=dims.ndisplay,world_slice=world_slice[-self.ndim:],order=order[-self.ndim:],)@abstractmethoddef_update_thumbnail(self):raiseNotImplementedError@abstractmethoddef_get_value(self,position):"""Value of the data at a position in data coordinates. Parameters ---------- position : tuple Position in data coordinates. Returns ------- value : tuple Value of the data. """raiseNotImplementedError
[docs]defget_value(self,position:npt.ArrayLike,*,view_direction:Optional[npt.ArrayLike]=None,dims_displayed:Optional[list[int]]=None,world:bool=False,)->Optional[tuple]:"""Value of the data at a position. If the layer is not visible, return None. Parameters ---------- position : tuple of float Position in either data or world coordinates. view_direction : Optional[np.ndarray] A unit vector giving the direction of the ray in nD world coordinates. The default value is None. dims_displayed : Optional[List[int]] A list of the dimensions currently being displayed in the viewer. The default value is None. world : bool If True the position is taken to be in world coordinates and converted into data coordinates. False by default. Returns ------- value : tuple, None Value of the data. If the layer is not visible return None. """position=np.asarray(position)ifself.visible:ifworld:ndim_world=len(position)ifdims_displayedisnotNone:# convert the dims_displayed to the layer dims.This accounts# for differences in the number of dimensions in the world# dims versus the layer and for transpose and rolls.dims_displayed=dims_displayed_world_to_layer(dims_displayed,ndim_world=ndim_world,ndim_layer=self.ndim,)position=self.world_to_data(position)if(dims_displayedisnotNone)and(view_directionisnotNone):iflen(dims_displayed)==2orself.ndim==2:value=self._get_value(position=tuple(position))eliflen(dims_displayed)==3:view_direction=self._world_to_data_ray(view_direction)start_point,end_point=self.get_ray_intersections(position=position,view_direction=view_direction,dims_displayed=dims_displayed,world=False,)value=self._get_value_3d(start_point=start_point,end_point=end_point,dims_displayed=dims_displayed,)else:value=self._get_value(position)else:value=None# This should be removed as soon as possible, it is still# used in Points and Shapes.self._value=valuereturnvalue
def_get_value_3d(self,start_point:Optional[np.ndarray],end_point:Optional[np.ndarray],dims_displayed:list[int],)->Union[float,int,None,tuple[Union[float,int,None],Optional[int]]]:"""Get the layer data value along a ray Parameters ---------- start_point : np.ndarray The start position of the ray used to interrogate the data. end_point : np.ndarray The end position of the ray used to interrogate the data. dims_displayed : List[int] The indices of the dimensions currently displayed in the Viewer. Returns ------- value The data value along the supplied ray. """
[docs]defprojected_distance_from_mouse_drag(self,start_position:npt.ArrayLike,end_position:npt.ArrayLike,view_direction:npt.ArrayLike,vector:np.ndarray,dims_displayed:list[int],)->npt.NDArray:"""Calculate the length of the projection of a line between two mouse clicks onto a vector (or array of vectors) in data coordinates. Parameters ---------- start_position : np.ndarray Starting point of the drag vector in data coordinates end_position : np.ndarray End point of the drag vector in data coordinates view_direction : np.ndarray Vector defining the plane normal of the plane onto which the drag vector is projected. vector : np.ndarray (3,) unit vector or (n, 3) array thereof on which to project the drag vector from start_event to end_event. This argument is defined in data coordinates. dims_displayed : List[int] (3,) list of currently displayed dimensions Returns ------- projected_distance : (1, ) or (n, ) np.ndarray of float """start_position=np.asarray(start_position)end_position=np.asarray(end_position)view_direction=np.asarray(view_direction)start_position=self._world_to_displayed_data(start_position,dims_displayed)end_position=self._world_to_displayed_data(end_position,dims_displayed)view_direction=self._world_to_displayed_data_ray(view_direction,dims_displayed)returndrag_data_to_projected_distance(start_position,end_position,view_direction,vector)
@contextmanagerdefblock_update_properties(self)->Generator[None,None,None]:previous=self._update_propertiesself._update_properties=Falsetry:yieldfinally:self._update_properties=previousdef_set_highlight(self,force:bool=False)->None:"""Render layer highlights when appropriate. Parameters ---------- force : bool Bool that forces a redraw to occur when `True`. """@contextmanagerdef_block_refresh(self):"""Prevent refresh calls from updating view."""previous=self._refresh_blockedself._refresh_blocked=Truetry:yieldfinally:self._refresh_blocked=previous
[docs]defrefresh(self,event:Optional[Event]=None,*,thumbnail:bool=True,data_displayed:bool=True,highlight:bool=True,extent:bool=True,force:bool=False,)->None:"""Refresh all layer data based on current view slice."""ifself._refresh_blocked:logger.debug('Layer.refresh blocked: %s',self)returnlogger.debug('Layer.refresh: %s',self)# If async is enabled then emit an event that the viewer should handle.ifget_settings().experimental.async_:self.events.reload(layer=self)# Otherwise, slice immediately on the calling thread.else:self._refresh_sync(thumbnail=thumbnail,data_displayed=data_displayed,highlight=highlight,extent=extent,force=force,)
[docs]defworld_to_data(self,position:npt.ArrayLike)->npt.NDArray:"""Convert from world coordinates to data coordinates. Parameters ---------- position : tuple, list, 1D array Position in world coordinates. If longer then the number of dimensions of the layer, the later dimensions will be used. Returns ------- tuple Position in data coordinates. """position=np.asarray(position)iflen(position)>=self.ndim:coords=list(position[-self.ndim:])else:coords=[0]*(self.ndim-len(position))+list(position)simplified=self._transforms[1:].simplifiedreturnsimplified.inverse(coords)
[docs]defdata_to_world(self,position):"""Convert from data coordinates to world coordinates. Parameters ---------- position : tuple, list, 1D array Position in data coordinates. If longer then the number of dimensions of the layer, the later dimensions will be used. Returns ------- tuple Position in world coordinates. """iflen(position)>=self.ndim:coords=list(position[-self.ndim:])else:coords=[0]*(self.ndim-len(position))+list(position)returntuple(self._transforms[1:].simplified(coords))
def_world_to_displayed_data(self,position:np.ndarray,dims_displayed:list[int])->npt.NDArray:"""Convert world to data coordinates for displayed dimensions only. Parameters ---------- position : tuple, list, 1D array Position in world coordinates. If longer then the number of dimensions of the layer, the later dimensions will be used. dims_displayed : list[int] Indices of displayed dimensions of the data. Returns ------- tuple Position in data coordinates for the displayed dimensions only """position_nd=self.world_to_data(position)position_ndisplay=position_nd[dims_displayed]returnposition_ndisplay@propertydef_data_to_world(self)->Affine:"""The transform from data to world coordinates. This affine transform is composed from the affine property and the other transform properties in the following order: affine * (rotate * shear * scale + translate) """returnself._transforms[1:3].simplifieddef_world_to_data_ray(self,vector:npt.ArrayLike)->npt.NDArray:"""Convert a vector defining an orientation from world coordinates to data coordinates. For example, this would be used to convert the view ray. Parameters ---------- vector : tuple, list, 1D array A vector in world coordinates. Returns ------- tuple Vector in data coordinates. """p1=np.asarray(self.world_to_data(vector))p0=np.asarray(self.world_to_data(np.zeros_like(vector)))normalized_vector=(p1-p0)/np.linalg.norm(p1-p0)returnnormalized_vectordef_world_to_displayed_data_ray(self,vector_world:npt.ArrayLike,dims_displayed:list[int])->np.ndarray:"""Convert an orientation from world to displayed data coordinates. For example, this would be used to convert the view ray. Parameters ---------- vector_world : 1D array A vector in world coordinates. Returns ------- tuple Vector in data coordinates. """vector_data_nd=self._world_to_data_ray(vector_world)vector_data_ndisplay=vector_data_nd[dims_displayed]vector_data_ndisplay/=np.linalg.norm(vector_data_ndisplay)returnvector_data_ndisplaydef_world_to_displayed_data_normal(self,vector_world:npt.ArrayLike,dims_displayed:list[int])->np.ndarray:"""Convert a normal vector defining an orientation from world coordinates to data coordinates. Parameters ---------- vector_world : tuple, list, 1D array A vector in world coordinates. dims_displayed : list[int] Indices of displayed dimensions of the data. Returns ------- np.ndarray Transformed normal vector (unit vector) in data coordinates. Notes ----- This method is adapted from napari-threedee under BSD-3-Clause License. For more information see also: https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/transforming-normals.html """# the napari transform is from layer -> world.# We want the inverse of the world -> layer, so we just take the napari transforminverse_transform=self._transforms[1:].simplified.linear_matrix# Extract the relevant submatrix based on dims_displayedsubmatrix=inverse_transform[np.ix_(dims_displayed,dims_displayed)]transpose_inverse_transform=submatrix.T# transform the vectortransformed_vector=np.matmul(transpose_inverse_transform,vector_world)transformed_vector/=np.linalg.norm(transformed_vector)returntransformed_vectordef_world_to_layer_dims(self,*,world_dims:npt.NDArray,ndim_world:int)->np.ndarray:"""Map world dimensions to layer dimensions while maintaining order. This is used to map dimensions from the full world space defined by ``Dims`` to the subspace that a layer inhabits, so that those can be used to index the layer's data and associated coordinates. For example a world ``Dims.order`` of [2, 1, 0, 3] would map to [0, 1] for a layer with two dimensions and [1, 0, 2] for a layer with three dimensions as those correspond to the relative order of the last two and three world dimensions respectively. Let's keep in mind a few facts: - each dimension index is present exactly once. - the lowest represented dimension index will be 0 That is to say both the `world_dims` input and return results are _some_ permutation of 0...N Examples -------- `[2, 1, 0, 3]` sliced in N=2 dimensions. - we want to keep the N=2 dimensions with the biggest index - `[2, None, None, 3]` - we filter the None - `[2, 3]` - reindex so that the lowest dimension is 0 by subtracting 2 from all indices - `[0, 1]` `[2, 1, 0, 3]` sliced in N=3 dimensions. - we want to keep the N=3 dimensions with the biggest index - `[2, 1, None, 3]` - we filter the None - `[2, 1, 3]` - reindex so that the lowest dimension is 0 by subtracting 1 from all indices - `[1, 0, 2]` Conveniently if the world (layer) dimension is bigger than our displayed dims, we can return everything Parameters ---------- world_dims : ndarray The world dimensions. ndim_world : int The number of dimensions in the world coordinate system. Returns ------- ndarray The corresponding layer dimensions with the same ordering as the given world dimensions. """returnself._world_to_layer_dims_impl(world_dims,ndim_world,self.ndim)@staticmethoddef_world_to_layer_dims_impl(world_dims:npt.NDArray,ndim_world:int,ndim:int)->npt.NDArray:""" Static for ease of testing """world_dims=np.asarray(world_dims)assertworld_dims.min()==0assertworld_dims.max()==len(world_dims)-1assertworld_dims.ndim==1offset=ndim_world-ndimorder=world_dims-offsetorder=order[order>=0]returnorder-order.min()def_display_bounding_box(self,dims_displayed:list[int])->npt.NDArray:"""An axis aligned (ndisplay, 2) bounding box around the data"""returnself._extent_data[:,dims_displayed].Tdef_display_bounding_box_augmented(self,dims_displayed:list[int])->npt.NDArray:"""An augmented, axis-aligned (ndisplay, 2) bounding box. This bounding box includes the size of the layer in best resolution, including required padding """returnself._extent_data_augmented[:,dims_displayed].Tdef_display_bounding_box_augmented_data_level(self,dims_displayed:list[int])->npt.NDArray:"""An augmented, axis-aligned (ndisplay, 2) bounding box. If the layer is multiscale layer, then returns the bounding box of the data at the current level """returnself._display_bounding_box_augmented(dims_displayed)
[docs]defclick_plane_from_click_data(self,click_position:npt.ArrayLike,view_direction:npt.ArrayLike,dims_displayed:list[int],)->tuple[np.ndarray,np.ndarray]:"""Calculate a (point, normal) plane parallel to the canvas in data coordinates, centered on the centre of rotation of the camera. Parameters ---------- click_position : np.ndarray click position in world coordinates from mouse event. view_direction : np.ndarray view direction in world coordinates from mouse event. dims_displayed : List[int] dimensions of the data array currently in view. Returns ------- click_plane : Tuple[np.ndarray, np.ndarray] tuple of (plane_position, plane_normal) in data coordinates. """click_position=np.asarray(click_position)view_direction=np.asarray(view_direction)plane_position=self.world_to_data(click_position)[dims_displayed]plane_normal=self._world_to_data_ray(view_direction)[dims_displayed]returnplane_position,plane_normal
[docs]defget_ray_intersections(self,position:npt.ArrayLike,view_direction:npt.ArrayLike,dims_displayed:list[int],world:bool=True,)->tuple[Optional[np.ndarray],Optional[np.ndarray]]:"""Get the start and end point for the ray extending from a point through the data bounding box. Parameters ---------- position the position of the point in nD coordinates. World vs. data is set by the world keyword argument. view_direction : np.ndarray a unit vector giving the direction of the ray in nD coordinates. World vs. data is set by the world keyword argument. dims_displayed : List[int] a list of the dimensions currently being displayed in the viewer. world : bool True if the provided coordinates are in world coordinates. Default value is True. Returns ------- start_point : np.ndarray The point on the axis-aligned data bounding box that the cursor click intersects with. This is the point closest to the camera. The point is the full nD coordinates of the layer data. If the click does not intersect the axis-aligned data bounding box, None is returned. end_point : np.ndarray The point on the axis-aligned data bounding box that the cursor click intersects with. This is the point farthest from the camera. The point is the full nD coordinates of the layer data. If the click does not intersect the axis-aligned data bounding box, None is returned. """position=np.asarray(position)view_direction=np.asarray(view_direction)iflen(dims_displayed)!=3:returnNone,None# create the bounding box in data coordinatesbounding_box=self._display_bounding_box(dims_displayed)# bounding box is with upper limit excluded in the uses belowbounding_box[:,1]+=1start_point,end_point=self._get_ray_intersections(position=position,view_direction=view_direction,dims_displayed=dims_displayed,world=world,bounding_box=bounding_box,)returnstart_point,end_point
def_get_offset_data_position(self,position:npt.NDArray)->npt.NDArray:"""Adjust position for offset between viewer and data coordinates."""returnnp.asarray(position)def_get_ray_intersections(self,position:npt.NDArray,view_direction:np.ndarray,dims_displayed:list[int],bounding_box:npt.NDArray,world:bool=True,)->tuple[Optional[np.ndarray],Optional[np.ndarray]]:"""Get the start and end point for the ray extending from a point through the data bounding box. Parameters ---------- position the position of the point in nD coordinates. World vs. data is set by the world keyword argument. view_direction : np.ndarray a unit vector giving the direction of the ray in nD coordinates. World vs. data is set by the world keyword argument. dims_displayed : List[int] a list of the dimensions currently being displayed in the viewer. world : bool True if the provided coordinates are in world coordinates. Default value is True. bounding_box : np.ndarray A (2, 3) bounding box around the data currently in view Returns ------- start_point : np.ndarray The point on the axis-aligned data bounding box that the cursor click intersects with. This is the point closest to the camera. The point is the full nD coordinates of the layer data. If the click does not intersect the axis-aligned data bounding box, None is returned. end_point : np.ndarray The point on the axis-aligned data bounding box that the cursor click intersects with. This is the point farthest from the camera. The point is the full nD coordinates of the layer data. If the click does not intersect the axis-aligned data bounding box, None is returned."""# get the view direction and click position in data coords# for the displayed dimensions onlyifworldisTrue:view_dir=self._world_to_displayed_data_ray(view_direction,dims_displayed)click_pos_data=self._world_to_displayed_data(position,dims_displayed)else:# adjust for any offset between viewer and data coordinatesposition=self._get_offset_data_position(position)view_dir=view_direction[dims_displayed]click_pos_data=position[dims_displayed]# Determine the front and back facesfront_face_normal,back_face_normal=find_front_back_face(click_pos_data,bounding_box,view_dir)iffront_face_normalisNoneandback_face_normalisNone:# click does not intersect the data bounding boxreturnNone,None# Calculate ray-bounding box face intersectionsstart_point_displayed_dimensions=(intersect_line_with_axis_aligned_bounding_box_3d(click_pos_data,view_dir,bounding_box,front_face_normal))end_point_displayed_dimensions=(intersect_line_with_axis_aligned_bounding_box_3d(click_pos_data,view_dir,bounding_box,back_face_normal))# add the coordinates for the axes not displayedstart_point=position.copy()start_point[dims_displayed]=start_point_displayed_dimensionsend_point=position.copy()end_point[dims_displayed]=end_point_displayed_dimensionsreturnstart_point,end_pointdef_update_draw(self,scale_factor,corner_pixels_displayed,shape_threshold):"""Update canvas scale and corner values on draw. For layer multiscale determining if a new resolution level or tile is required. Parameters ---------- scale_factor : float Scale factor going from canvas to world coordinates. corner_pixels_displayed : array, shape (2, 2) Coordinates of the top-left and bottom-right canvas pixels in world coordinates. shape_threshold : tuple Requested shape of field of view in data coordinates. """self.scale_factor=scale_factordisplayed_axes=self._slice_input.displayed# we need to compute all four corners to compute a complete,# data-aligned bounding box, because top-left/bottom-right may not# remain top-left and bottom-right after transformations.all_corners=list(itertools.product(*corner_pixels_displayed.T))# Note that we ignore the first transform which is tile2datadata_corners=(self._transforms[1:].simplified.set_slice(displayed_axes).inverse(all_corners))# find the maximal data-axis-aligned bounding box containing all four# canvas corners and round them to intsdata_bbox=np.stack([np.min(data_corners,axis=0),np.max(data_corners,axis=0)])data_bbox_int=np.stack([np.floor(data_bbox[0]),np.ceil(data_bbox[1])]).astype(int)ifself._slice_input.ndisplay==2andself.multiscale:level,scaled_corners=compute_multiscale_level_and_corners(data_bbox_int,shape_threshold,self.downsample_factors[:,displayed_axes],)corners=np.zeros((2,self.ndim),dtype=int)# The corner_pixels attribute stores corners in the data# space of the selected level. Using the level's data# shape only works for images, but that's the only case we# handle now and downsample_factors is also only on image layers.max_coords=np.take(self.data[level].shape,displayed_axes)-1corners[:,displayed_axes]=np.clip(scaled_corners,0,max_coords)display_shape=tuple(corners[1,displayed_axes]-corners[0,displayed_axes])ifany(s==0forsindisplay_shape):returnifself.data_level!=levelornotnp.array_equal(self.corner_pixels,corners):self._data_level=levelself.corner_pixels=cornersself.refresh(extent=False,thumbnail=False)else:# set the data_level so that it is the lowest resolution in 3d viewifself.multiscaleisTrue:self._data_level=len(self.level_shapes)-1# The stored corner_pixels attribute must contain valid indices.corners=np.zeros((2,self.ndim),dtype=int)# Some empty layers (e.g. Points) may have a data extent that only# contains nans, in which case the integer valued corner pixels# cannot be meaningfully set.displayed_extent=self.extent.data[:,displayed_axes]ifnotnp.all(np.isnan(displayed_extent)):data_bbox_clipped=np.clip(data_bbox_int,displayed_extent[0],displayed_extent[1])corners[:,displayed_axes]=data_bbox_clippedself.corner_pixels=cornersdef_get_source_info(self)->dict:components={}ifself.source.reader_plugin:components['layer_name']=self.namecomponents['layer_base']=os.path.basename(self.source.pathor'')components['source_type']='plugin'try:components['plugin']=pm.get_manifest(self.source.reader_plugin).display_nameexceptKeyError:components['plugin']=self.source.reader_pluginreturncomponentsifself.source.sample:components['layer_name']=self.namecomponents['layer_base']=self.namecomponents['source_type']='sample'try:components['plugin']=pm.get_manifest(self.source.sample[0]).display_nameexceptKeyError:components['plugin']=self.source.sample[0]returncomponentsifself.source.widget:components['layer_name']=self.namecomponents['layer_base']=self.namecomponents['source_type']='widget'components['plugin']=self.source.widget._function.__name__returncomponentscomponents['layer_name']=self.namecomponents['layer_base']=self.namecomponents['source_type']=''components['plugin']=''returncomponentsdefget_source_str(self)->str:source_info=self._get_source_info()source_str=source_info['layer_name']ifsource_info['layer_base']!=source_info['layer_name']:source_str+='\n'+source_info['layer_base']ifsource_info['source_type']:source_str+=('\n'+source_info['source_type']+' : '+source_info['plugin'])returnsource_str
[docs]defget_status(self,position:Optional[npt.ArrayLike]=None,*,view_direction:Optional[npt.ArrayLike]=None,dims_displayed:Optional[list[int]]=None,world:bool=False,)->dict:""" Status message information of the data at a coordinate position. Parameters ---------- position : tuple of float Position in either data or world coordinates. view_direction : Optional[np.ndarray] A unit vector giving the direction of the ray in nD world coordinates. The default value is None. dims_displayed : Optional[List[int]] A list of the dimensions currently being displayed in the viewer. The default value is None. world : bool If True the position is taken to be in world coordinates and converted into data coordinates. False by default. Returns ------- source_info : dict Dictionary containing a information that can be used as a status update. """ifpositionisnotNone:position=np.asarray(position)value=self.get_value(position,view_direction=view_direction,dims_displayed=dims_displayed,world=world,)else:value=Nonesource_info=self._get_source_info()ifpositionisnotNone:source_info['coordinates']=generate_layer_coords_status(position[-self.ndim:],value)else:source_info['coordinates']=generate_layer_coords_status(position,value)returnsource_info
def_get_tooltip_text(self,position:npt.NDArray,*,view_direction:Optional[np.ndarray]=None,dims_displayed:Optional[list[int]]=None,world:bool=False,)->str:""" tooltip message of the data at a coordinate position. Parameters ---------- position : ndarray Position in either data or world coordinates. view_direction : Optional[ndarray] A unit vector giving the direction of the ray in nD world coordinates. The default value is None. dims_displayed : Optional[List[int]] A list of the dimensions currently being displayed in the viewer. The default value is None. world : bool If True the position is taken to be in world coordinates and converted into data coordinates. False by default. Returns ------- msg : string String containing a message that can be used as a tooltip. """return''
[docs]defsave(self,path:str,plugin:Optional[str]=None)->list[str]:"""Save this layer to ``path`` with default (or specified) plugin. Parameters ---------- path : str A filepath, directory, or URL to open. Extensions may be used to specify output format (provided a plugin is available for the requested format). plugin : str, optional Name of the plugin to use for saving. If ``None`` then all plugins corresponding to appropriate hook specification will be looped through to find the first one that can save the data. Returns ------- list of str File paths of any files that were written. """fromnapari.plugins.ioimportsave_layersreturnsave_layers(path,[self],plugin=plugin)
def__copy__(self):"""Create a copy of this layer. Returns ------- layer : napari.layers.Layer Copy of this layer. Notes ----- This method is defined for purpose of asv memory benchmarks. The copy of data is intentional for properly estimating memory usage for layer. If you want a to copy a layer without coping the data please use `layer.create(*layer.as_layer_data_tuple())` If you change this method, validate if memory benchmarks are still working properly. """data,meta,layer_type=self.as_layer_data_tuple()returnself.create(copy.copy(data),meta=meta,layer_type=layer_type)
[docs]@classmethoddefcreate(cls,data:Any,meta:Optional[Mapping]=None,layer_type:Optional[str]=None,)->Layer:"""Create layer from `data` of type `layer_type`. Primarily intended for usage by reader plugin hooks and creating a layer from an unwrapped layer data tuple. Parameters ---------- data : Any Data in a format that is valid for the corresponding `layer_type`. meta : dict, optional Dict of keyword arguments that will be passed to the corresponding layer constructor. If any keys in `meta` are not valid for the corresponding layer type, an exception will be raised. layer_type : str Type of layer to add. Must be the (case insensitive) name of a Layer subclass. If not provided, the layer is assumed to be "image", unless data.dtype is one of (np.int32, np.uint32, np.int64, np.uint64), in which case it is assumed to be "labels". Raises ------ ValueError If ``layer_type`` is not one of the recognized layer types. TypeError If any keyword arguments in ``meta`` are unexpected for the corresponding `add_*` method for this layer_type. Examples -------- A typical use case might be to upack a tuple of layer data with a specified layer_type. >>> data = ( ... np.random.random((10, 2)) * 20, ... {'face_color': 'blue'}, ... 'points', ... ) >>> Layer.create(*data) """fromnapariimportlayersfromnapari.layers.image._image_utilsimportguess_labelslayer_type=(layer_typeor'').lower()# assumes that big integer type arrays are likely labels.ifnotlayer_type:layer_type=guess_labels(data)iflayer_typeisNoneorlayer_typenotinlayers.NAMES:raiseValueError(trans._("Unrecognized layer_type: '{layer_type}'. Must be one of: {layer_names}.",deferred=True,layer_type=layer_type,layer_names=layers.NAMES,))Cls=getattr(layers,layer_type.title())try:returnCls(data,**(metaor{}))exceptExceptionasexc:if'unexpected keyword argument'notinstr(exc):raisebad_key=str(exc).split('keyword argument ')[-1]raiseTypeError(trans._('_add_layer_from_data received an unexpected keyword argument ({bad_key}) for layer type {layer_type}',deferred=True,bad_key=bad_key,layer_type=layer_type,))fromexc