[docs]classPoints(Layer):"""Points layer. Parameters ---------- data : array (N, D) Coordinates for N points in D dimensions. ndim : int Number of dimensions for shapes. When data is not None, ndim must be D. An empty points layer can be instantiated with arbitrary ndim. features : dict[str, array-like] or DataFrame Features table where each row corresponds to a point and each column is a feature. feature_defaults : dict[str, Any] or DataFrame The default value of each feature in a table with one row. properties : dict {str: array (N,)}, DataFrame Properties for each point. Each property should be an array of length N, where N is the number of points. property_choices : dict {str: array (N,)} possible values for each property. text : str, dict Text to be displayed with the points. If text is set to a key in properties, the value of that property will be displayed. Multiple properties can be composed using f-string-like syntax (e.g., '{property_1}, {float_property:.2f}). A dictionary can be provided with keyword arguments to set the text values and display properties. See TextManager.__init__() for the valid keyword arguments. For example usage, see /napari/examples/add_points_with_text.py. symbol : str, array Symbols to be used for the point markers. Must be one of the following: arrow, clobber, cross, diamond, disc, hbar, ring, square, star, tailed_arrow, triangle_down, triangle_up, vbar, x. size : float, array Size of the point marker in data pixels. If given as a scalar, all points are made the same size. If given as an array, size must be the same or broadcastable to the same shape as the data. edge_width : float, array Width of the symbol edge in pixels. edge_width_is_relative : bool If enabled, edge_width is interpreted as a fraction of the point size. edge_color : str, array-like, dict Color of the point marker border. Numeric color values should be RGB(A). edge_color_cycle : np.ndarray, list Cycle of colors (provided as string name, RGB, or RGBA) to map to edge_color if a categorical attribute is used color the vectors. edge_colormap : str, napari.utils.Colormap Colormap to set edge_color if a continuous attribute is used to set face_color. edge_contrast_limits : None, (float, float) clims for mapping the property to a color map. These are the min and max value of the specified property that are mapped to 0 and 1, respectively. The default value is None. If set the none, the clims will be set to (property.min(), property.max()) face_color : str, array-like, dict Color of the point marker body. Numeric color values should be RGB(A). face_color_cycle : np.ndarray, list Cycle of colors (provided as string name, RGB, or RGBA) to map to face_color if a categorical attribute is used color the vectors. face_colormap : str, napari.utils.Colormap Colormap to set face_color if a continuous attribute is used to set face_color. face_contrast_limits : None, (float, float) clims for mapping the property to a color map. These are the min and max value of the specified property that are mapped to 0 and 1, respectively. The default value is None. If set the none, the clims will be set to (property.min(), property.max()) out_of_slice_display : bool If True, renders points not just in central plane but also slightly out of slice according to specified point marker size. n_dimensional : bool This property will soon be deprecated in favor of 'out_of_slice_display'. Use that instead. name : str Name of the layer. metadata : dict Layer metadata. scale : tuple of float Scale factors for the layer. translate : tuple of float Translation values for the layer. 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. 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. 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. opacity : float Opacity of the layer visual, between 0.0 and 1.0. 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', and 'additive'}. visible : bool Whether the layer visual is currently being displayed. cache : bool Whether slices of out-of-core datasets should be cached upon retrieval. Currently, this only applies to dask arrays. shading : str, Shading Render lighting and shading on points. Options are: * 'none' No shading is added to the points. * 'spherical' Shading and depth buffer are changed to give a 3D spherical look to the points antialiasing: float Amount of antialiasing in canvas pixels. canvas_size_limits : tuple of float Lower and upper limits for the size of points in canvas pixels. shown : 1-D array of bool Whether to show each point. Attributes ---------- data : array (N, D) Coordinates for N points in D dimensions. features : DataFrame-like Features table where each row corresponds to a point and each column is a feature. feature_defaults : DataFrame-like Stores the default value of each feature in a table with one row. properties : dict {str: array (N,)} or DataFrame Annotations for each point. Each property should be an array of length N, where N is the number of points. text : str Text to be displayed with the points. If text is set to a key in properties, the value of that property will be displayed. Multiple properties can be composed using f-string-like syntax (e.g., '{property_1}, {float_property:.2f}). For example usage, see /napari/examples/add_points_with_text.py. symbol : array of str Array of symbols for each point. size : array (N,) Array of sizes for each point. Must have the same shape as the layer `data`. edge_width : array (N,) Width of the marker edges in pixels for all points edge_width : array (N,) Width of the marker edges for all points as a fraction of their size. edge_color : Nx4 numpy array Array of edge color RGBA values, one for each point. edge_color_cycle : np.ndarray, list Cycle of colors (provided as string name, RGB, or RGBA) to map to edge_color if a categorical attribute is used color the vectors. edge_colormap : str, napari.utils.Colormap Colormap to set edge_color if a continuous attribute is used to set face_color. edge_contrast_limits : None, (float, float) clims for mapping the property to a color map. These are the min and max value of the specified property that are mapped to 0 and 1, respectively. The default value is None. If set the none, the clims will be set to (property.min(), property.max()) face_color : Nx4 numpy array Array of face color RGBA values, one for each point. face_color_cycle : np.ndarray, list Cycle of colors (provided as string name, RGB, or RGBA) to map to face_color if a categorical attribute is used color the vectors. face_colormap : str, napari.utils.Colormap Colormap to set face_color if a continuous attribute is used to set face_color. face_contrast_limits : None, (float, float) clims for mapping the property to a color map. These are the min and max value of the specified property that are mapped to 0 and 1, respectively. The default value is None. If set the none, the clims will be set to (property.min(), property.max()) current_symbol : Symbol Symbol for the next point to be added or the currently selected points. current_size : float Size of the marker for the next point to be added or the currently selected point. current_edge_width : float Edge width of the marker for the next point to be added or the currently selected point. current_edge_color : str Edge color of the marker edge for the next point to be added or the currently selected point. current_face_color : str Face color of the marker edge for the next point to be added or the currently selected point. out_of_slice_display : bool If True, renders points not just in central plane but also slightly out of slice according to specified point marker size. selected_data : Selection Integer indices of any selected points. mode : str Interactive mode. The normal, default mode is PAN_ZOOM, which allows for normal interactivity with the canvas. In ADD mode clicks of the cursor add points at the clicked location. In SELECT mode the cursor can select points by clicking on them or by dragging a box around them. Once selected points can be moved, have their properties edited, or be deleted. face_color_mode : str Face color setting mode. DIRECT (default mode) allows each point to be set arbitrarily CYCLE allows the color to be set via a color cycle over an attribute COLORMAP allows color to be set via a color map over an attribute edge_color_mode : str Edge color setting mode. DIRECT (default mode) allows each point to be set arbitrarily CYCLE allows the color to be set via a color cycle over an attribute COLORMAP allows color to be set via a color map over an attribute shading : Shading Shading mode. antialiasing: float Amount of antialiasing in canvas pixels. canvas_size_limits : tuple of float Lower and upper limits for the size of points in canvas pixels. shown : 1-D array of bool Whether each point is shown. Notes ----- _view_data : array (M, 2) 2D coordinates of points in the currently viewed slice. _view_size : array (M, ) Size of the point markers in the currently viewed slice. _view_symbol : array (M, ) Symbols of the point markers in the currently viewed slice. _view_edge_width : array (M, ) Edge width of the point markers in the currently viewed slice. _indices_view : array (M, ) Integer indices of the points in the currently viewed slice and are shown. _selected_view : Integer indices of selected points in the currently viewed slice within the `_view_data` array. _selected_box : array (4, 2) or None Four corners of any box either around currently selected points or being created during a drag action. Starting in the top left and going clockwise. _drag_start : list or None Coordinates of first cursor click during a drag action. Gets reset to None after dragging is done. """_modeclass=Mode_drag_modes={Mode.PAN_ZOOM:no_op,Mode.TRANSFORM:transform_with_box,Mode.ADD:add,Mode.SELECT:select,}_move_modes={Mode.PAN_ZOOM:no_op,Mode.TRANSFORM:highlight_box_handles,Mode.ADD:no_op,Mode.SELECT:highlight,}_cursor_modes={Mode.PAN_ZOOM:'standard',Mode.TRANSFORM:'standard',Mode.ADD:'crosshair',Mode.SELECT:'standard',}# TODO write better documentation for edge_color and face_color# The max number of points that will ever be used to render the thumbnail# If more points are present then they are randomly subsampled_max_points_thumbnail=1024def__init__(self,data=None,*,ndim=None,features=None,feature_defaults=None,properties=None,text=None,symbol='o',size=10,edge_width=0.05,edge_width_is_relative=True,edge_color='dimgray',edge_color_cycle=None,edge_colormap='viridis',edge_contrast_limits=None,face_color='white',face_color_cycle=None,face_colormap='viridis',face_contrast_limits=None,out_of_slice_display=False,n_dimensional=None,name=None,metadata=None,scale=None,translate=None,rotate=None,shear=None,affine=None,opacity=1,blending='translucent',visible=True,cache=True,property_choices=None,experimental_clipping_planes=None,shading='none',canvas_size_limits=(2,10000),antialiasing=1,shown=True,)->None:ifndimisNone:ifscaleisnotNone:ndim=len(scale)elif(dataisnotNoneandhasattr(data,'shape')andlen(data.shape)==2):ndim=data.shape[1]data,ndim=fix_data_points(data,ndim)# Indices of selected pointsself._selected_data_stored=set()self._selected_data_history=set()# Indices of selected points within the currently viewed sliceself._selected_view=[]# Index of hovered pointself._value=Noneself._value_stored=Noneself._highlight_index=[]self._highlight_box=Noneself._drag_start=Noneself._drag_normal=Noneself._drag_up=None# initialize view dataself.__indices_view=np.empty(0,int)self._view_size_scale=[]self._drag_box=Noneself._drag_box_stored=Noneself._is_selecting=Falseself._clipboard={}self._round_index=Falsesuper().__init__(data,ndim,name=name,metadata=metadata,scale=scale,translate=translate,rotate=rotate,shear=shear,affine=affine,opacity=opacity,blending=blending,visible=visible,cache=cache,experimental_clipping_planes=experimental_clipping_planes,)self.events.add(size=Event,current_size=Event,edge_width=Event,current_edge_width=Event,edge_width_is_relative=Event,face_color=Event,current_face_color=Event,edge_color=Event,current_edge_color=Event,properties=Event,current_properties=Event,symbol=Event,current_symbol=Event,out_of_slice_display=Event,n_dimensional=Event,highlight=Event,shading=Event,antialiasing=Event,canvas_size_limits=Event,features=Event,feature_defaults=Event,)# Save the point coordinatesself._data=np.asarray(data)self._feature_table=_FeatureTable.from_layer(features=features,feature_defaults=feature_defaults,properties=properties,property_choices=property_choices,num_data=len(self.data),)self._text=TextManager._from_layer(text=text,features=self.features,)self._edge_width_is_relative=Falseself._shown=np.empty(0).astype(bool)# Indices of selected pointsself._selected_data:Selection[int]=Selection()self._selected_data_stored=set()self._selected_data_history=set()# Indices of selected points within the currently viewed sliceself._selected_view=[]# The following point properties are for the new points that will# be added. For any given property, if a list is passed to the# constructor so each point gets its own value then the default# value is used when adding new pointsself._current_size=np.asarray(size)ifnp.isscalar(size)else10self._current_edge_width=(np.asarray(edge_width)ifnp.isscalar(edge_width)else0.1)self.current_symbol=(np.asarray(symbol)ifnp.isscalar(symbol)else'o')# Index of hovered pointself._value=Noneself._value_stored=Noneself._mode=Mode.PAN_ZOOMself._status=self.modecolor_properties=(self._feature_table.properties()ifself._data.size>0elseself._feature_table.currents())self._edge=ColorManager._from_layer_kwargs(n_colors=len(data),colors=edge_color,continuous_colormap=edge_colormap,contrast_limits=edge_contrast_limits,categorical_colormap=edge_color_cycle,properties=color_properties,)self._face=ColorManager._from_layer_kwargs(n_colors=len(data),colors=face_color,continuous_colormap=face_colormap,contrast_limits=face_contrast_limits,categorical_colormap=face_color_cycle,properties=color_properties,)ifn_dimensionalisnotNone:self._out_of_slice_display=n_dimensionalelse:self._out_of_slice_display=out_of_slice_display# Save the point style paramsself.size=sizeself.shown=shownself.symbol=symbolself.edge_width=edge_widthself.edge_width_is_relative=edge_width_is_relativeself.canvas_size_limits=canvas_size_limitsself.shading=shadingself.antialiasing=antialiasing# Trigger generation of view slice and thumbnailself.refresh()@propertydefdata(self)->np.ndarray:"""(N, D) array: coordinates for N points in D dimensions."""returnself._data@data.setterdefdata(self,data:Optional[np.ndarray]):"""Set the data array and emit a corresponding event."""prior_data=len(self.data)>0data_not_empty=(dataisnotNoneand(isinstance(data,np.ndarray)anddata.size>0)or(isinstance(data,list)andlen(data)>0))kwargs={"value":self.data,"vertex_indices":((),),"data_indices":tuple(iforiinrange(len(self.data))),}ifprior_dataanddata_not_empty:kwargs["action"]=ActionType.CHANGINGelifdata_not_empty:kwargs["action"]=ActionType.ADDINGkwargs["data_indices"]=tuple(iforiinrange(len(data)))else:kwargs["action"]=ActionType.REMOVINGself.events.data(**kwargs)self._set_data(data)kwargs["data_indices"]=tuple(iforiinrange(len(self.data)))kwargs["value"]=self.dataifprior_dataanddata_not_empty:kwargs["action"]=ActionType.CHANGEDelifdata_not_empty:kwargs["data_indices"]=tuple(iforiinrange(len(data)))kwargs["action"]=ActionType.ADDEDelse:kwargs["action"]=ActionType.REMOVEDself.events.data(**kwargs)def_set_data(self,data:Optional[np.ndarray]):"""Set the .data array attribute, without emitting an event."""data,_=fix_data_points(data,self.ndim)cur_npoints=len(self._data)self._data=data# Add/remove property and style values based on the number of new points.withself.events.blocker_all(),self._edge.events.blocker_all(),self._face.events.blocker_all():self._feature_table.resize(len(data))self.text.apply(self.features)iflen(data)<cur_npoints:# If there are now fewer points, remove the size and colors of the# extra onesiflen(self._edge.colors)>len(data):self._edge._remove(np.arange(len(data),len(self._edge.colors)))iflen(self._face.colors)>len(data):self._face._remove(np.arange(len(data),len(self._face.colors)))self._shown=self._shown[:len(data)]self._size=self._size[:len(data)]self._edge_width=self._edge_width[:len(data)]self._symbol=self._symbol[:len(data)]eliflen(data)>cur_npoints:# If there are now more points, add the size and colors of the# new onesadding=len(data)-cur_npointssize=np.repeat(self.current_size,adding,axis=0)iflen(self._edge_width)>0:new_edge_width=copy(self._edge_width[-1])else:new_edge_width=self.current_edge_widthedge_width=np.repeat([new_edge_width],adding,axis=0)iflen(self._symbol)>0:new_symbol=copy(self._symbol[-1])else:new_symbol=self.current_symbolsymbol=np.repeat([new_symbol],adding,axis=0)# Add new colors, updating the current property value before# to handle any in-place modification of feature_defaults.# Also see: https://github.com/napari/napari/issues/5634current_properties=self._feature_table.currents()self._edge._update_current_properties(current_properties)self._edge._add(n_colors=adding)self._face._update_current_properties(current_properties)self._face._add(n_colors=adding)shown=np.repeat([True],adding,axis=0)self._shown=np.concatenate((self._shown,shown),axis=0)self.size=np.concatenate((self._size,size),axis=0)self.edge_width=np.concatenate((self._edge_width,edge_width),axis=0)self.symbol=np.concatenate((self._symbol,symbol),axis=0)self._update_dims()self._reset_editable()def_on_selection(self,selected):ifselected:self._set_highlight()else:self._highlight_box=Noneself._highlight_index=[]self.events.highlight()@propertydeffeatures(self):"""Dataframe-like features table. It is an implementation detail that this is a `pandas.DataFrame`. In the future, we will target the currently-in-development Data API dataframe protocol [1]. This will enable us to use alternate libraries such as xarray or cuDF for additional features without breaking existing usage of this. If you need to specifically rely on the pandas API, please coerce this to a `pandas.DataFrame` using `features_to_pandas_dataframe`. References ---------- .. [1]: https://data-apis.org/dataframe-protocol/latest/API.html """returnself._feature_table.values@features.setterdeffeatures(self,features:Union[Dict[str,np.ndarray],pd.DataFrame],)->None:self._feature_table.set_values(features,num_data=len(self.data))self._update_color_manager(self._face,self._feature_table,"face_color")self._update_color_manager(self._edge,self._feature_table,"edge_color")self.text.refresh(self.features)self.events.properties()self.events.features()@propertydeffeature_defaults(self):"""Dataframe-like with one row of feature default values. See `features` for more details on the type of this property. """returnself._feature_table.defaults@feature_defaults.setterdeffeature_defaults(self,defaults:Union[Dict[str,Any],pd.DataFrame])->None:self._feature_table.set_defaults(defaults)current_properties=self.current_propertiesself._edge._update_current_properties(current_properties)self._face._update_current_properties(current_properties)self.events.current_properties()self.events.feature_defaults()@propertydefproperty_choices(self)->Dict[str,np.ndarray]:returnself._feature_table.choices()@propertydefproperties(self)->Dict[str,np.ndarray]:"""dict {str: np.ndarray (N,)}, DataFrame: Annotations for each point"""returnself._feature_table.properties()@staticmethoddef_update_color_manager(color_manager,feature_table,name):ifcolor_manager.color_propertiesisnotNone:color_name=color_manager.color_properties.nameifcolor_namenotinfeature_table.values:color_manager.color_mode=ColorMode.DIRECTcolor_manager.color_properties=Nonewarnings.warn(trans._('property used for {name} dropped',deferred=True,name=name,),RuntimeWarning,)else:color_manager.color_properties={'name':color_name,'values':feature_table.values[color_name].to_numpy(),'current_value':feature_table.defaults[color_name][0],}@properties.setterdefproperties(self,properties:Union[Dict[str,Array],pd.DataFrame,None]):self.features=properties@propertydefcurrent_properties(self)->Dict[str,np.ndarray]:"""dict{str: np.ndarray(1,)}: properties for the next added point."""returnself._feature_table.currents()@current_properties.setterdefcurrent_properties(self,current_properties):update_indices=Noneifself._update_propertiesandlen(self.selected_data)>0:update_indices=list(self.selected_data)self._feature_table.set_currents(current_properties,update_indices=update_indices)current_properties=self.current_propertiesself._edge._update_current_properties(current_properties)self._face._update_current_properties(current_properties)self.events.current_properties()self.events.feature_defaults()ifupdate_indicesisnotNone:self.events.properties()self.events.features()@propertydeftext(self)->TextManager:"""TextManager: the TextManager object containing containing the text properties"""returnself._text@text.setterdeftext(self,text):self._text._update_from_layer(text=text,features=self.features,)
[docs]defrefresh_text(self):"""Refresh the text values. This is generally used if the features were updated without changing the data """self.text.refresh(self.features)
def_get_ndim(self)->int:"""Determine number of dimensions of the layer."""returnself.data.shape[1]@propertydef_extent_data(self)->np.ndarray:"""Extent of layer in data coordinates. Returns ------- extent_data : array, shape (2, D) """iflen(self.data)==0:extrema=np.full((2,self.ndim),np.nan)else:maxs=np.max(self.data,axis=0)mins=np.min(self.data,axis=0)extrema=np.vstack([mins,maxs])returnextrema.astype(float)@propertydef_extent_data_augmented(self):# _extent_data is a property that returns a new/copied array, which# is safe to modify belowextent=self._extent_dataiflen(self.size)==0:returnextentmax_point_size=np.max(self.size)extent[0]-=max_point_size/2extent[1]+=max_point_size/2returnextent@propertydefout_of_slice_display(self)->bool:"""bool: renders points slightly out of slice."""returnself._out_of_slice_display@out_of_slice_display.setterdefout_of_slice_display(self,out_of_slice_display:bool)->None:self._out_of_slice_display=bool(out_of_slice_display)self.events.out_of_slice_display()self.events.n_dimensional()self.refresh()@propertydefn_dimensional(self)->bool:""" This property will soon be deprecated in favor of `out_of_slice_display`. Use that instead. """returnself._out_of_slice_display@n_dimensional.setterdefn_dimensional(self,value:bool)->None:self.out_of_slice_display=value@propertydefsymbol(self)->np.ndarray:"""str: symbol used for all point markers."""returnself._symbol@symbol.setterdefsymbol(self,symbol:Union[str,np.ndarray,list])->None:symbol=np.broadcast_to(symbol,self.data.shape[0])self._symbol=coerce_symbols(symbol)self.events.symbol()self.events.highlight()@propertydefcurrent_symbol(self)->Union[int,float]:"""float: symbol of marker for the next added point."""returnself._current_symbol@current_symbol.setterdefcurrent_symbol(self,symbol:Union[None,float])->None:symbol=coerce_symbols(np.array([symbol]))[0]self._current_symbol=symbolifself._update_propertiesandlen(self.selected_data)>0:self.symbol[list(self.selected_data)]=symbolself.events.symbol()self.events.current_symbol()@propertydefsize(self)->np.ndarray:"""(N,) array: size of all N points."""returnself._size@size.setterdefsize(self,size:Union[float,np.ndarray,list])->None:try:self._size=np.broadcast_to(size,len(self.data)).copy()exceptValueErrorase:# deprecated anisotropic sizes; extra check should be removed in future versiontry:self._size=np.broadcast_to(size,self.data.shape[::-1]).T.copy()exceptValueError:raiseValueError(trans._("Size is not compatible for broadcasting",deferred=True,))fromeelse:self._size=np.mean(size,axis=1)warnings.warn(trans._("Since 0.4.18 point sizes must be isotropic; the average from each dimension will be"" used instead. This will become an error in version 0.6.0.",deferred=True,),category=DeprecationWarning,stacklevel=2,)self.refresh()@propertydefcurrent_size(self)->Union[int,float]:"""float: size of marker for the next added point."""returnself._current_size@current_size.setterdefcurrent_size(self,size:Union[None,float])->None:ifisinstance(size,(list,tuple,np.ndarray)):warnings.warn(trans._("Since 0.4.18 point sizes must be isotropic; the average from each dimension will be used instead. ""This will become an error in version 0.6.0.",deferred=True,),category=DeprecationWarning,stacklevel=2,)size=size[-1]ifnotisinstance(size,numbers.Number):raiseTypeError(trans._('currrent size must be a number',deferred=True,))ifsize<0:raiseValueError(trans._('current_size value must be positive.',deferred=True,),)self._current_size=sizeifself._update_propertiesandlen(self.selected_data)>0:idx=np.fromiter(self.selected_data,dtype=int)self.size[idx]=sizeself.refresh()self.events.size()self.events.current_size()@propertydefantialiasing(self)->float:"""Amount of antialiasing in canvas pixels."""returnself._antialiasing@antialiasing.setterdefantialiasing(self,value:float):"""Set the amount of antialiasing in canvas pixels. Values can only be positive. """ifvalue<0:warnings.warn(message=trans._('antialiasing value must be positive, value will be set to 0.',deferred=True,),category=RuntimeWarning,)self._antialiasing=max(0,value)self.events.antialiasing(value=self._antialiasing)@propertydefshading(self)->Shading:"""shading mode."""returnself._shading@shading.setterdefshading(self,value):self._shading=Shading(value)self.events.shading()@propertydefcanvas_size_limits(self)->Tuple[float,float]:"""Limit the canvas size of points"""returnself._canvas_size_limits@canvas_size_limits.setterdefcanvas_size_limits(self,value):self._canvas_size_limits=float(value[0]),float(value[1])self.events.canvas_size_limits()@propertydefshown(self):""" Boolean array determining which points to show """returnself._shown@shown.setterdefshown(self,shown):self._shown=np.broadcast_to(shown,self.data.shape[0]).astype(bool)self.refresh()@propertydefedge_width(self)->np.ndarray:"""(N, D) array: edge_width of all N points."""returnself._edge_width@edge_width.setterdefedge_width(self,edge_width:Union[float,np.ndarray,list])->None:# broadcast to np.arrayedge_width=np.broadcast_to(edge_width,self.data.shape[0]).copy()# edge width cannot be negativeifnp.any(edge_width<0):raiseValueError(trans._('All edge_width must be > 0',deferred=True,))# if relative edge width is enabled, edge_width must be between 0 and 1ifself.edge_width_is_relativeandnp.any(edge_width>1):raiseValueError(trans._('All edge_width must be between 0 and 1 if edge_width_is_relative is enabled',deferred=True,))self._edge_width=edge_widthself.refresh()@propertydefedge_width_is_relative(self)->bool:"""bool: treat edge_width as a fraction of point size."""returnself._edge_width_is_relative@edge_width_is_relative.setterdefedge_width_is_relative(self,edge_width_is_relative:bool)->None:ifedge_width_is_relativeandnp.any((self.edge_width>1)|(self.edge_width<0)):raiseValueError(trans._('edge_width_is_relative can only be enabled if edge_width is between 0 and 1',deferred=True,))self._edge_width_is_relative=edge_width_is_relativeself.events.edge_width_is_relative()@propertydefcurrent_edge_width(self)->Union[int,float]:"""float: edge_width of marker for the next added point."""returnself._current_edge_width@current_edge_width.setterdefcurrent_edge_width(self,edge_width:Union[None,float])->None:self._current_edge_width=edge_widthifself._update_propertiesandlen(self.selected_data)>0:idx=np.fromiter(self.selected_data,dtype=int)self.edge_width[idx]=edge_widthself.refresh()self.events.edge_width()self.events.current_edge_width()@propertydefedge_color(self)->np.ndarray:"""(N x 4) np.ndarray: Array of RGBA edge colors for each point"""returnself._edge.colors@edge_color.setterdefedge_color(self,edge_color):self._edge._set_color(color=edge_color,n_colors=len(self.data),properties=self.properties,current_properties=self.current_properties,)self.events.edge_color()@propertydefedge_color_cycle(self)->np.ndarray:"""Union[list, np.ndarray] : Color cycle for edge_color. Can be a list of colors defined by name, RGB or RGBA """returnself._edge.categorical_colormap.fallback_color.values@edge_color_cycle.setterdefedge_color_cycle(self,edge_color_cycle:Union[list,np.ndarray]):self._edge.categorical_colormap=edge_color_cycle@propertydefedge_colormap(self)->Colormap:"""Return the colormap to be applied to a property to get the edge color. Returns ------- colormap : napari.utils.Colormap The Colormap object. """returnself._edge.continuous_colormap@edge_colormap.setterdefedge_colormap(self,colormap:ValidColormapArg):self._edge.continuous_colormap=colormap@propertydefedge_contrast_limits(self)->Tuple[float,float]:"""None, (float, float): contrast limits for mapping the edge_color colormap property to 0 and 1 """returnself._edge.contrast_limits@edge_contrast_limits.setterdefedge_contrast_limits(self,contrast_limits:Union[None,Tuple[float,float]]):self._edge.contrast_limits=contrast_limits@propertydefcurrent_edge_color(self)->str:"""str: Edge color of marker for the next added point or the selected point(s)."""hex_=rgb_to_hex(self._edge.current_color)[0]returnhex_to_name.get(hex_,hex_)@current_edge_color.setterdefcurrent_edge_color(self,edge_color:ColorType)->None:ifself._update_propertiesandlen(self.selected_data)>0:update_indices=list(self.selected_data)else:update_indices=[]self._edge._update_current_color(edge_color,update_indices=update_indices)self.events.current_edge_color()@propertydefedge_color_mode(self)->str:"""str: Edge color setting mode DIRECT (default mode) allows each point to be set arbitrarily CYCLE allows the color to be set via a color cycle over an attribute COLORMAP allows color to be set via a color map over an attribute """returnself._edge.color_mode@edge_color_mode.setterdefedge_color_mode(self,edge_color_mode:Union[str,ColorMode]):self._set_color_mode(edge_color_mode,'edge')@propertydefface_color(self)->np.ndarray:"""(N x 4) np.ndarray: Array of RGBA face colors for each point"""returnself._face.colors@face_color.setterdefface_color(self,face_color):self._face._set_color(color=face_color,n_colors=len(self.data),properties=self.properties,current_properties=self.current_properties,)self.events.face_color()@propertydefface_color_cycle(self)->np.ndarray:"""Union[np.ndarray, cycle]: Color cycle for face_color Can be a list of colors defined by name, RGB or RGBA """returnself._face.categorical_colormap.fallback_color.values@face_color_cycle.setterdefface_color_cycle(self,face_color_cycle:Union[np.ndarray,cycle]):self._face.categorical_colormap=face_color_cycle@propertydefface_colormap(self)->Colormap:"""Return the colormap to be applied to a property to get the face color. Returns ------- colormap : napari.utils.Colormap The Colormap object. """returnself._face.continuous_colormap@face_colormap.setterdefface_colormap(self,colormap:ValidColormapArg):self._face.continuous_colormap=colormap@propertydefface_contrast_limits(self)->Union[None,Tuple[float,float]]:"""None, (float, float) : clims for mapping the face_color colormap property to 0 and 1 """returnself._face.contrast_limits@face_contrast_limits.setterdefface_contrast_limits(self,contrast_limits:Union[None,Tuple[float,float]]):self._face.contrast_limits=contrast_limits@propertydefcurrent_face_color(self)->str:"""Face color of marker for the next added point or the selected point(s)."""hex_=rgb_to_hex(self._face.current_color)[0]returnhex_to_name.get(hex_,hex_)@current_face_color.setterdefcurrent_face_color(self,face_color:ColorType)->None:ifself._update_propertiesandlen(self.selected_data)>0:update_indices=list(self.selected_data)else:update_indices=[]self._face._update_current_color(face_color,update_indices=update_indices)self.events.current_face_color()@propertydefface_color_mode(self)->str:"""str: Face color setting mode DIRECT (default mode) allows each point to be set arbitrarily CYCLE allows the color to be set via a color cycle over an attribute COLORMAP allows color to be set via a color map over an attribute """returnself._face.color_mode@face_color_mode.setterdefface_color_mode(self,face_color_mode):self._set_color_mode(face_color_mode,'face')def_set_color_mode(self,color_mode:Union[ColorMode,str],attribute:str):"""Set the face_color_mode or edge_color_mode property Parameters ---------- color_mode : str, ColorMode The value for setting edge or face_color_mode. If color_mode is a string, it should be one of: 'direct', 'cycle', or 'colormap' attribute : str in {'edge', 'face'} The name of the attribute to set the color of. Should be 'edge' for edge_color_mode or 'face' for face_color_mode. """color_mode=ColorMode(color_mode)color_manager=getattr(self,f'_{attribute}')ifcolor_mode==ColorMode.DIRECT:color_manager.color_mode=color_modeelifcolor_modein(ColorMode.CYCLE,ColorMode.COLORMAP):ifcolor_manager.color_propertiesisnotNone:color_property=color_manager.color_properties.nameelse:color_property=''ifcolor_property=='':ifself.features.shape[1]>0:new_color_property=next(iter(self.features))color_manager.color_properties={'name':new_color_property,'values':self.features[new_color_property].to_numpy(),'current_value':np.squeeze(self.current_properties[new_color_property]),}warnings.warn(trans._('_{attribute}_color_property was not set, setting to: {new_color_property}',deferred=True,attribute=attribute,new_color_property=new_color_property,))else:raiseValueError(trans._('There must be a valid Points.properties to use {color_mode}',deferred=True,color_mode=color_mode,))# ColorMode.COLORMAP can only be applied to numeric propertiescolor_property=color_manager.color_properties.nameif(color_mode==ColorMode.COLORMAP)andnotissubclass(self.features[color_property].dtype.type,np.number):raiseTypeError(trans._('selected property must be numeric to use ColorMode.COLORMAP',deferred=True,))color_manager.color_mode=color_mode
[docs]defrefresh_colors(self,update_color_mapping:bool=False):"""Calculate and update face and edge colors if using a cycle or color map Parameters ---------- update_color_mapping : bool If set to True, the function will recalculate the color cycle map or colormap (whichever is being used). If set to False, the function will use the current color cycle map or color map. For example, if you are adding/modifying points and want them to be colored with the same mapping as the other points (i.e., the new points shouldn't affect the color cycle map or colormap), set ``update_color_mapping=False``. Default value is False. """self._edge._refresh_colors(self.properties,update_color_mapping)self._face._refresh_colors(self.properties,update_color_mapping)
def_get_state(self):"""Get dictionary of layer state. Returns ------- state : dict Dictionary of layer state. """state=self._get_base_state()state.update({'symbol':self.symbolifself.data.sizeelse[self.current_symbol],'edge_width':self.edge_width,'edge_width_is_relative':self.edge_width_is_relative,'face_color':self.face_colorifself.data.sizeelse[self.current_face_color],'face_color_cycle':self.face_color_cycle,'face_colormap':self.face_colormap.name,'face_contrast_limits':self.face_contrast_limits,'edge_color':self.edge_colorifself.data.sizeelse[self.current_edge_color],'edge_color_cycle':self.edge_color_cycle,'edge_colormap':self.edge_colormap.name,'edge_contrast_limits':self.edge_contrast_limits,'properties':self.properties,'property_choices':self.property_choices,'text':self.text.dict(),'out_of_slice_display':self.out_of_slice_display,'n_dimensional':self.out_of_slice_display,'size':self.size,'ndim':self.ndim,'data':self.data,'features':self.features,'feature_defaults':self.feature_defaults,'shading':self.shading,'antialiasing':self.antialiasing,'canvas_size_limits':self.canvas_size_limits,'shown':self.shown,})returnstate@propertydefselected_data(self)->Selection[int]:"""set: set of currently selected points."""returnself._selected_data@selected_data.setterdefselected_data(self,selected_data:Sequence[int])->None:self._selected_data.clear()self._selected_data.update(set(selected_data))self._selected_view=list(np.intersect1d(np.array(list(self._selected_data)),self._indices_view,return_indices=True,)[2])# Update properties based on selected pointsifnotlen(self._selected_data):self._set_highlight()returnindex=list(self._selected_data)withself.block_update_properties():if(unique_edge_color:=_unique_element(self.edge_color[index]))isnotNone:self.current_edge_color=unique_edge_colorif(unique_face_color:=_unique_element(self.face_color[index]))isnotNone:self.current_face_color=unique_face_colorif(unique_size:=_unique_element(self.size[index]))isnotNone:self.current_size=unique_sizeif(unique_edge_width:=_unique_element(self.edge_width[index]))isnotNone:self.current_edge_width=unique_edge_widthif(unique_symbol:=_unique_element(self.symbol[index]))isnotNone:self.current_symbol=unique_symbolunique_properties={}fork,vinself.properties.items():unique_properties[k]=_unique_element(v[index])ifall(pisnotNoneforpinunique_properties.values()):self.current_properties=unique_propertiesself._set_highlight()
[docs]definteraction_box(self,index)->Optional[np.ndarray]:"""Create the interaction box around a list of points in view. Parameters ---------- index : list List of points around which to construct the interaction box. Returns ------- box : np.ndarray or None 4x2 array of corners of the interaction box in clockwise order starting in the upper-left corner. """iflen(index)>0:data=self._view_data[index]size=self._view_size[index]data=points_to_squares(data,size)returncreate_box(data)returnNone
@Layer.mode.getterdefmode(self)->str:"""str: Interactive mode Interactive mode. The normal, default mode is PAN_ZOOM, which allows for normal interactivity with the canvas. In ADD mode clicks of the cursor add points at the clicked location. In SELECT mode the cursor can select points by clicking on them or by dragging a box around them. Once selected points can be moved, have their properties edited, or be deleted. """returnstr(self._mode)def_mode_setter_helper(self,mode):mode=super()._mode_setter_helper(mode)ifmode==self._mode:returnmodeifmode==Mode.ADD:self.selected_data=set()self.mouse_pan=Trueelifmode!=Mode.SELECTorself._mode!=Mode.SELECT:self._selected_data_stored=set()self._set_highlight()returnmode@propertydef_indices_view(self):returnself.__indices_view@_indices_view.setterdef_indices_view(self,value):iflen(self._shown)==0:self.__indices_view=np.empty(0,int)else:self.__indices_view=value[self.shown[value]]@propertydef_view_data(self)->np.ndarray:"""Get the coords of the points in view Returns ------- view_data : (N x D) np.ndarray Array of coordinates for the N points in view """iflen(self._indices_view)>0:data=self.data[np.ix_(self._indices_view,self._slice_input.displayed)]else:# if no points in this slice send dummy datadata=np.zeros((0,self._slice_input.ndisplay))returndata@propertydef_view_text(self)->np.ndarray:"""Get the values of the text elements in view Returns ------- text : (N x 1) np.ndarray Array of text strings for the N text elements in view """# This may be triggered when the string encoding instance changed,# in which case it has no cached values, so generate them here.self.text.string._apply(self.features)returnself.text.view_text(self._indices_view)@propertydef_view_text_coords(self)->Tuple[np.ndarray,str,str]:"""Get the coordinates of the text elements in view Returns ------- text_coords : (N x D) np.ndarray Array of coordinates for the N text elements in view anchor_x : str The vispy text anchor for the x axis anchor_y : str The vispy text anchor for the y axis """returnself.text.compute_text_coords(self._view_data,self._slice_input.ndisplay)@propertydef_view_text_color(self)->np.ndarray:"""Get the colors of the text elements at the given indices."""self.text.color._apply(self.features)returnself.text._view_color(self._indices_view)@propertydef_view_size(self)->np.ndarray:"""Get the sizes of the points in view Returns ------- view_size : (N,) np.ndarray Array of sizes for the N points in view """iflen(self._indices_view)>0:sizes=self.size[self._indices_view]*self._view_size_scaleelse:# if no points, return an empty listsizes=np.array([])returnsizes@propertydef_view_symbol(self)->np.ndarray:"""Get the symbols of the points in view Returns ------- symbol : (N,) np.ndarray Array of symbol strings for the N points in view """returnself.symbol[self._indices_view]@propertydef_view_edge_width(self)->np.ndarray:"""Get the edge_width of the points in view Returns ------- view_edge_width : (N,) np.ndarray Array of edge_widths for the N points in view """returnself.edge_width[self._indices_view]@propertydef_view_face_color(self)->np.ndarray:"""Get the face colors of the points in view Returns ------- view_face_color : (N x 4) np.ndarray RGBA color array for the face colors of the N points in view. If there are no points in view, returns array of length 0. """returnself.face_color[self._indices_view]@propertydef_view_edge_color(self)->np.ndarray:"""Get the edge colors of the points in view Returns ------- view_edge_color : (N x 4) np.ndarray RGBA color array for the edge colors of the N points in view. If there are no points in view, returns array of length 0. """returnself.edge_color[self._indices_view]def_reset_editable(self)->None:"""Set editable mode based on layer properties."""# interaction currently does not work for 2D layers being rendered in 3Dself.editable=not(self.ndim==2andself._slice_input.ndisplay==3)def_on_editable_changed(self)->None:ifnotself.editable:self.mode=Mode.PAN_ZOOMdef_update_draw(self,scale_factor,corner_pixels_displayed,shape_threshold):prev_scale=self.scale_factorsuper()._update_draw(scale_factor,corner_pixels_displayed,shape_threshold)# update highlight only if scale has changed, otherwise causes a cycleself._set_highlight(force=(prev_scale!=self.scale_factor))def_slice_data(self,dims_indices)->Tuple[List[int],Union[float,np.ndarray]]:"""Determines the slice of points given the indices. Parameters ---------- dims_indices : sequence of int or slice Indices to slice with. Returns ------- slice_indices : list Indices of points in the currently viewed slice. scale : float, (N, ) array If in `out_of_slice_display` mode then the scale factor of points, where values of 1 corresponds to points located in the slice, and values less than 1 correspond to points located in neighboring slices. """# Get a list of the data for the points in this slicenot_disp=list(self._slice_input.not_displayed)# We want a numpy array so we can use fancy indexing with the non-displayed# indices, but as dims_indices can (and often/always does) contain slice# objects, the array has dtype=object which is then very slow for the# arithmetic below. As Points._round_index is always False, we can safely# convert to float to get a major performance improvement.not_disp_indices=np.array(dims_indices)[not_disp].astype(float)iflen(self.data)>0:ifself.out_of_slice_displayisTrueandself.ndim>2:distances=abs(self.data[:,not_disp]-not_disp_indices)view_dim=distances.shape[1]sizes=(np.repeat(self.size,view_dim).reshape(distances.shape)/2)matches=np.all(distances<=sizes,axis=1)size_match=sizes[matches]size_match[size_match==0]=1scale_per_dim=(size_match-distances[matches])/size_matchscale_per_dim[size_match==0]=1scale=np.prod(scale_per_dim,axis=1)slice_indices=np.where(matches)[0].astype(int)returnslice_indices,scaledata=self.data[:,not_disp]distances=np.abs(data-not_disp_indices)matches=np.all(distances<=0.5,axis=1)slice_indices=np.where(matches)[0].astype(int)returnslice_indices,1return[],np.empty(0)def_get_value(self,position)->Optional[int]:"""Index of the point at a given 2D position in data coordinates. Parameters ---------- position : tuple Position in data coordinates. Returns ------- value : int or None Index of point that is at the current coordinate if any. """# Display points if there are any in this sliceview_data=self._view_dataselection=Noneiflen(view_data)>0:displayed_position=[position[i]foriinself._slice_input.displayed]# positions are scaled anisotropically by scale, but sizes are not,# so we need to calculate the ratio to correctly map to screen coordinatesscale_ratio=(self.scale[self._slice_input.displayed]/self.scale[-1])# Get the point sizes# TODO: calculate distance in canvas space to account for canvas_size_limits.# Without this implementation, point hover and selection (and anything depending# on self.get_value()) won't be aware of the real extent of points, causing# unexpected behaviour. See #3734 for details.sizes=np.expand_dims(self._view_size,axis=1)/scale_ratio/2distances=abs(view_data-displayed_position)in_slice_matches=np.all(distances<=sizes,axis=1,)indices=np.where(in_slice_matches)[0]iflen(indices)>0:selection=self._indices_view[indices[-1]]returnselectiondef_get_value_3d(self,start_point:np.ndarray,end_point:np.ndarray,dims_displayed:List[int],)->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 : Union[int, None] The data value along the supplied ray. """if(start_pointisNone)or(end_pointisNone):# if the ray doesn't intersect the data volume, no points could have been intersectedreturnNoneplane_point,plane_normal=displayed_plane_from_nd_line_segment(start_point,end_point,dims_displayed)# project the in view points onto the planeprojected_points,projection_distances=project_points_onto_plane(points=self._view_data,plane_point=plane_point,plane_normal=plane_normal,)# rotate points and plane to be axis aligned with normal [0, 0, 1]rotated_points,rotation_matrix=rotate_points(points=projected_points,current_plane_normal=plane_normal,new_plane_normal=[0,0,1],)rotated_click_point=np.dot(rotation_matrix,plane_point)# positions are scaled anisotropically by scale, but sizes are not,# so we need to calculate the ratio to correctly map to screen coordinatesscale_ratio=self.scale[self._slice_input.displayed]/self.scale[-1]# find the points the click intersectssizes=np.expand_dims(self._view_size,axis=1)/scale_ratio/2distances=abs(rotated_points-rotated_click_point)in_slice_matches=np.all(distances<=sizes,axis=1,)indices=np.where(in_slice_matches)[0]iflen(indices)>0:# find the point that is most in the foregroundcandidate_point_distances=projection_distances[indices]closest_index=indices[np.argmin(candidate_point_distances)]selection=self._indices_view[closest_index]else:selection=Nonereturnselection# def _display_bounding_box_augmented(self, dims_displayed: np.ndarray):# """An augmented, axis-aligned (ndisplay, 2) bounding box.## This bounding box for includes the full size of displayed points# and enables calculation of intersections in `Layer._get_value_3d()`.# """# if len(self._view_size) == 0:# return None# max_point_size = np.max(self._view_size)# bounding_box = np.copy(# self._display_bounding_box(dims_displayed)# ).astype(float)# bounding_box[:, 0] -= max_point_size / 2# bounding_box[:, 1] += max_point_size / 2# return bounding_box
[docs]defget_ray_intersections(self,position:List[float],view_direction:np.ndarray,dims_displayed:List[int],world:bool=True,)->Union[Tuple[np.ndarray,np.ndarray],Tuple[None,None]]:"""Get the start and end point for the ray extending from a point through the displayed bounding box. This method overrides the base layer, replacing the bounding box used to calculate intersections with a larger one which includes the size of points in view. 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 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. """iflen(dims_displayed)!=3:returnNone,None# create the bounding box in data coordinatesbounding_box=self._display_bounding_box_augmented(dims_displayed)ifbounding_boxisNone:returnNone,Nonestart_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_set_view_slice(self):"""Sets the view given the indices to slice with."""# get the indices of points in viewindices,scale=self._slice_data(self._slice_indices)# Update the _view_size_scale in accordance to the self._indices_view setter.# If out_of_slice_display is False, scale is a number and not an array.# Therefore we have an additional if statement checking for# self._view_size_scale being an integer.ifnotisinstance(scale,np.ndarray):self._view_size_scale=scaleeliflen(self._shown)==0:self._view_size_scale=np.empty(0,int)else:self._view_size_scale=scale[self.shown[indices]]self._indices_view=np.array(indices,dtype=int)# get the selected points that are in viewself._selected_view=list(np.intersect1d(np.array(list(self._selected_data)),self._indices_view,return_indices=True,)[2])withself.events.highlight.blocker():self._set_highlight(force=True)def_set_highlight(self,force=False):"""Render highlights of shapes including boundaries, vertices, interaction boxes, and the drag selection box when appropriate. Highlighting only occurs in Mode.SELECT. Parameters ---------- force : bool Bool that forces a redraw to occur when `True` """# Check if any point ids have changed since last callif(self.selected_data==self._selected_data_storedandself._value==self._value_storedandnp.all(self._drag_box==self._drag_box_stored))andnotforce:returnself._selected_data_stored=Selection(self.selected_data)self._value_stored=copy(self._value)self._drag_box_stored=copy(self._drag_box)ifself._valueisnotNoneorlen(self._selected_view)>0:iflen(self._selected_view)>0:index=copy(self._selected_view)# highlight the hovered point if not in adding modeif(self._valueinself._indices_viewandself._mode==Mode.SELECTandnotself._is_selecting):hover_point=list(self._indices_view).index(self._value)ifhover_pointnotinindex:index.append(hover_point)index.sort()else:# only highlight hovered points in select modeif(self._valueinself._indices_viewandself._mode==Mode.SELECTandnotself._is_selecting):hover_point=list(self._indices_view).index(self._value)index=[hover_point]else:index=[]self._highlight_index=indexelse:self._highlight_index=[]# only display dragging selection box in 2Difself._is_selecting:ifself._drag_normalisNone:pos=create_box(self._drag_box)else:pos=_create_box_from_corners_3d(self._drag_box,self._drag_normal,self._drag_up)pos=pos[[*range(4),0]]else:pos=Noneself._highlight_box=posself.events.highlight()def_update_thumbnail(self):"""Update thumbnail with current points and colors."""colormapped=np.zeros(self._thumbnail_shape)colormapped[...,3]=1view_data=self._view_dataiflen(view_data)>0:# Get the zoom factor required to fit all data in the thumbnail.de=self._extent_datamin_vals=[de[0,i]foriinself._slice_input.displayed]shape=np.ceil([de[1,i]-de[0,i]+1foriinself._slice_input.displayed]).astype(int)zoom_factor=np.divide(self._thumbnail_shape[:2],shape[-2:]).min()# Maybe subsample the points.iflen(view_data)>self._max_points_thumbnail:thumbnail_indices=np.random.randint(0,len(view_data),self._max_points_thumbnail)points=view_data[thumbnail_indices]else:points=view_datathumbnail_indices=self._indices_view# Calculate the point coordinates in the thumbnail data space.thumbnail_shape=np.clip(np.ceil(zoom_factor*np.array(shape[:2])).astype(int),1,# smallest side should be 1 pixel wideself._thumbnail_shape[:2],)coords=np.floor((points[:,-2:]-min_vals[-2:]+0.5)*zoom_factor).astype(int)coords=np.clip(coords,0,thumbnail_shape-1)# Draw single pixel points in the colormapped thumbnail.colormapped=np.zeros((*thumbnail_shape,4))colormapped[...,3]=1colors=self._face.colors[thumbnail_indices]colormapped[coords[:,0],coords[:,1]]=colorscolormapped[...,3]*=self.opacityself.thumbnail=colormapped
[docs]defadd(self,coords):"""Adds points at coordinates. Parameters ---------- coords : array Point or points to add to the layer data. """cur_points=len(self.data)self.events.data(value=self.data,action=ActionType.ADDING,data_indices=(-1,),vertex_indices=((),),)self._set_data(np.append(self.data,np.atleast_2d(coords),axis=0))self.events.data(value=self.data,action=ActionType.ADDED,data_indices=(-1,),vertex_indices=((),),)self.selected_data=set(np.arange(cur_points,len(self.data)))
[docs]defremove_selected(self):"""Removes selected points if any."""index=list(self.selected_data)index.sort()iflen(index):self.events.data(value=self.data,action=ActionType.REMOVING,data_indices=tuple(self.selected_data,),vertex_indices=((),),)self._shown=np.delete(self._shown,index,axis=0)self._size=np.delete(self._size,index,axis=0)self._symbol=np.delete(self._symbol,index,axis=0)self._edge_width=np.delete(self._edge_width,index,axis=0)withself._edge.events.blocker_all():self._edge._remove(indices_to_remove=index)withself._face.events.blocker_all():self._face._remove(indices_to_remove=index)self._feature_table.remove(index)self.text.remove(index)ifself._valueinself.selected_data:self._value=Noneelse:ifself._valueisnotNone:# update the index of self._value to account for the# data being removedindices_removed=np.array(index)<self._valueoffset=np.sum(indices_removed)self._value-=offsetself._value_stored-=offsetself._set_data(np.delete(self.data,index,axis=0))self.events.data(value=self.data,action=ActionType.REMOVED,data_indices=tuple(self.selected_data,),vertex_indices=((),),)self.selected_data=set()
def_move(self,selection_indices:Sequence[int],position:Sequence[Union[int,float]],)->None:"""Move points relative to drag start location. Parameters ---------- selection_indices : Sequence[int] Integer indices of points to move in self.data position : tuple Position to move points to in data coordinates. """iflen(selection_indices)>0:selection_indices=list(selection_indices)disp=list(self._slice_input.displayed)self._set_drag_start(selection_indices,position)center=self.data[np.ix_(selection_indices,disp)].mean(axis=0)shift=np.array(position)[disp]-center-self._drag_startself.data[np.ix_(selection_indices,disp)]=(self.data[np.ix_(selection_indices,disp)]+shift)self.refresh()self.events.data(value=self.data,action=ActionType.CHANGED,data_indices=tuple(selection_indices),vertex_indices=((),),)def_set_drag_start(self,selection_indices:Sequence[int],position:Sequence[Union[int,float]],center_by_data:bool=True,)->None:"""Store the initial position at the start of a drag event. Parameters ---------- selection_indices : set of int integer indices of selected data used to index into self.data position : Sequence of numbers position of the drag start in data coordinates. center_by_data : bool Center the drag start based on the selected data. Used for modifier drag_box selection. """selection_indices=list(selection_indices)dims_displayed=list(self._slice_input.displayed)ifself._drag_startisNone:self._drag_start=np.array(position,dtype=float)[dims_displayed]iflen(selection_indices)>0andcenter_by_data:center=self.data[np.ix_(selection_indices,dims_displayed)].mean(axis=0)self._drag_start-=centerdef_paste_data(self):"""Paste any point from clipboard and select them."""npoints=len(self._view_data)totpoints=len(self.data)iflen(self._clipboard.keys())>0:not_disp=self._slice_input.not_displayeddata=deepcopy(self._clipboard['data'])offset=[self._slice_indices[i]-self._clipboard['indices'][i]foriinnot_disp]data[:,not_disp]=data[:,not_disp]+np.array(offset)self._data=np.append(self.data,data,axis=0)self._shown=np.append(self.shown,deepcopy(self._clipboard['shown']),axis=0)self._size=np.append(self.size,deepcopy(self._clipboard['size']),axis=0)self._symbol=np.append(self.symbol,deepcopy(self._clipboard['symbol']),axis=0)self._feature_table.append(self._clipboard['features'])self.text._paste(**self._clipboard['text'])self._edge_width=np.append(self.edge_width,deepcopy(self._clipboard['edge_width']),axis=0,)self._edge._paste(colors=self._clipboard['edge_color'],properties=_features_to_properties(self._clipboard['features']),)self._face._paste(colors=self._clipboard['face_color'],properties=_features_to_properties(self._clipboard['features']),)self._selected_view=list(range(npoints,npoints+len(self._clipboard['data'])))self._selected_data.update(set(range(totpoints,totpoints+len(self._clipboard['data']))))self.refresh()def_copy_data(self):"""Copy selected points to clipboard."""iflen(self.selected_data)>0:index=list(self.selected_data)self._clipboard={'data':deepcopy(self.data[index]),'edge_color':deepcopy(self.edge_color[index]),'face_color':deepcopy(self.face_color[index]),'shown':deepcopy(self.shown[index]),'size':deepcopy(self.size[index]),'symbol':deepcopy(self.symbol[index]),'edge_width':deepcopy(self.edge_width[index]),'features':deepcopy(self.features.iloc[index]),'indices':self._slice_indices,'text':self.text._copy(index),}else:self._clipboard={}
[docs]defto_mask(self,*,shape:tuple,data_to_world:Optional[Affine]=None,isotropic_output:bool=True,):"""Return a binary mask array of all the points as balls. Parameters ---------- shape : tuple The shape of the mask to be generated. data_to_world : Optional[Affine] The data-to-world transform of the output mask image. This likely comes from a reference image. If None, then this is the same as this layer's data-to-world transform. isotropic_output : bool If True, then force the output mask to always contain isotropic balls in data/pixel coordinates. Otherwise, allow the anisotropy in the data-to-world transform to squash the balls in certain dimensions. By default this is True, but you should set it to False if you are going to create a napari image layer from the result with the same data-to-world transform and want the visualized balls to be roughly isotropic. Returns ------- np.ndarray The output binary mask array of the given shape containing this layer's points as balls. """ifdata_to_worldisNone:data_to_world=self._data_to_worldmask=np.zeros(shape,dtype=bool)mask_world_to_data=data_to_world.inversepoints_data_to_mask_data=self._data_to_world.compose(mask_world_to_data)points_in_mask_data_coords=np.atleast_2d(points_data_to_mask_data(self.data))# Calculating the radii of the output points in the mask is complex.radii=self.size/2# Scale each radius by the geometric mean scale of the Points layer to# keep the balls isotropic when visualized in world coordinates.# Then scale each radius by the scale of the output image mask# using the geometric mean if isotropic output is desired.# The geometric means are used instead of the arithmetic mean# to maintain the volume scaling factor of the transforms.point_data_to_world_scale=gmean(np.abs(self._data_to_world.scale))mask_world_to_data_scale=(gmean(np.abs(mask_world_to_data.scale))ifisotropic_outputelsenp.abs(mask_world_to_data.scale))radii_scale=point_data_to_world_scale*mask_world_to_data_scaleoutput_data_radii=radii[:,np.newaxis]*np.atleast_2d(radii_scale)forcoords,radiiinzip(points_in_mask_data_coords,output_data_radii):# Define a minimal set of coordinates where the mask could be present# by defining an inclusive lower and exclusive upper bound for each dimension.lower_coords=np.maximum(np.floor(coords-radii),0).astype(int)upper_coords=np.minimum(np.ceil(coords+radii)+1,shape).astype(int)# Generate every possible coordinate within the bounds defined above# in a grid of size D1 x D2 x ... x Dd x D (e.g. for D=2, this might be 4x5x2).submask_coords=[range(lower_coords[i],upper_coords[i])foriinrange(self.ndim)]submask_grids=np.stack(np.meshgrid(*submask_coords,copy=False,indexing='ij'),axis=-1,)# Update the mask coordinates based on the normalized square distance# using a logical or to maintain any existing positive mask locations.normalized_square_distances=np.sum(((submask_grids-coords)/radii)**2,axis=-1)mask[np.ix_(*submask_coords)]|=normalized_square_distances<=1returnmask
[docs]defget_status(self,position:Optional[Tuple]=None,*,view_direction:Optional[np.ndarray]=None,dims_displayed:Optional[List[int]]=None,world:bool=False,)->dict:"""Status message information of the data at a coordinate position. # Parameters # ---------- # position : tuple # 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 # Dict containing information that can be used in a status update. #"""ifpositionisnotNone:value=self.get_value(position,view_direction=view_direction,dims_displayed=dims_displayed,world=world,)else:value=Nonesource_info=self._get_source_info()source_info['coordinates']=generate_layer_coords_status(position[-self.ndim:],value)# if this points layer has propertiesproperties=self._get_properties(position,view_direction=view_direction,dims_displayed=dims_displayed,world=world,)ifproperties:source_info['coordinates']+="; "+", ".join(properties)returnsource_info
def_get_tooltip_text(self,position,*,view_direction:Optional[np.ndarray]=None,dims_displayed:Optional[List[int]]=None,world:bool=False,):""" tooltip message of the data at a coordinate position. Parameters ---------- position : tuple 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 ------- msg : string String containing a message that can be used as a tooltip. """return"\n".join(self._get_properties(position,view_direction=view_direction,dims_displayed=dims_displayed,world=world,))def_get_properties(self,position,*,view_direction:Optional[np.ndarray]=None,dims_displayed:Optional[List[int]]=None,world:bool=False,)->list:ifself.features.shape[1]==0:return[]value=self.get_value(position,view_direction=view_direction,dims_displayed=dims_displayed,world=world,)# if the cursor is not outside the image or on the backgroundifvalueisNoneorvalue>self.data.shape[0]:return[]return[f'{k}: {v[value]}'fork,vinself.features.items()ifk!='index'andlen(v)>valueandv[value]isnotNoneandnot(isinstance(v[value],float)andnp.isnan(v[value]))]