importmathfromtypingimport*importpygfxfrom..utilsimportquick_min_maxfrom._baseimportGraphicfrom.selectorsimportLinearSelector,LinearRegionSelector,RectangleSelectorfrom._featuresimport(TextureArray,ImageCmap,ImageVmin,ImageVmax,ImageInterpolation,ImageCmapInterpolation,)class_ImageTile(pygfx.Image):""" Similar to pygfx.Image, only difference is that it modifies the pick_info by adding the data row start indices that correspond to this chunk of the big image """def__init__(self,geometry,material,data_slice:tuple[slice,slice],chunk_index:tuple[int,int],**kwargs,):super().__init__(geometry,material,**kwargs)self._data_slice=data_sliceself._chunk_index=chunk_indexdef_wgpu_get_pick_info(self,pick_value):pick_info=super()._wgpu_get_pick_info(pick_value)data_row_start,data_col_start=(self.data_slice[0].start,self.data_slice[1].start,)# add the actual data row and col start indicesx,y=pick_info["index"]x+=data_col_starty+=data_row_startpick_info["index"]=(x,y)xp,yp=pick_info["pixel_coord"]xp+=data_col_startyp+=data_row_startpick_info["pixel_coord"]=(xp,yp)# add row chunk and col chunk index to pick_info dictreturn{**pick_info,"data_slice":self.data_slice,"chunk_index":self.chunk_index,}@propertydefdata_slice(self)->tuple[slice,slice]:returnself._data_slice@propertydefchunk_index(self)->tuple[int,int]:returnself._chunk_index
[docs]classImageGraphic(Graphic):_features={"data","cmap","vmin","vmax","interpolation","cmap_interpolation"}def__init__(self,data:Any,vmin:int=None,vmax:int=None,cmap:str="plasma",interpolation:str="nearest",cmap_interpolation:str="linear",isolated_buffer:bool=True,**kwargs,):""" Create an Image Graphic Parameters ---------- data: array-like array-like, usually numpy.ndarray, must support ``memoryview()`` | shape must be ``[n_rows, n_cols]``, ``[n_rows, n_cols, 3]`` for RGB or ``[n_rows, n_cols, 4]`` for RGBA vmin: int, optional minimum value for color scaling, calculated from data if not provided vmax: int, optional maximum value for color scaling, calculated from data if not provided cmap: str, optional, default "plasma" colormap to use to display the data interpolation: str, optional, default "nearest" interpolation filter, one of "nearest" or "linear" cmap_interpolation: str, optional, default "linear" colormap interpolation method, one of "nearest" or "linear" isolated_buffer: bool, default True If True, initialize a buffer with the same shape as the input data and then set the data, useful if the data arrays are ready-only such as memmaps. If False, the input array is itself used as the buffer. kwargs: additional keyword arguments passed to Graphic """super().__init__(**kwargs)world_object=pygfx.Group()# texture array that manages the textures on the GPU for displaying this imageself._data=TextureArray(data,isolated_buffer=isolated_buffer)if(vminisNone)or(vmaxisNone):vmin,vmax=quick_min_max(data)# other graphic featuresself._vmin=ImageVmin(vmin)self._vmax=ImageVmax(vmax)self._interpolation=ImageInterpolation(interpolation)# set map to None for RGB imagesifself._data.value.ndim>2:self._cmap=None_map=Noneelse:# use TextureMap for grayscale imagesself._cmap=ImageCmap(cmap)self._cmap_interpolation=ImageCmapInterpolation(cmap_interpolation)_map=pygfx.TextureMap(self._cmap.texture,filter=self._cmap_interpolation.value,wrap="clamp-to-edge",)# one common material is used for every Texture chunkself._material=pygfx.ImageBasicMaterial(clim=(vmin,vmax),map=_map,interpolation=self._interpolation.value,pick_write=True,)# iterate through each texture chunk and create# an _ImageTIle, offset the tile using the data indicesfortexture,chunk_index,data_sliceinself._data:# create an ImageTile using the texture for this chunkimg=_ImageTile(geometry=pygfx.Geometry(grid=texture),material=self._material,data_slice=data_slice,# used to parse pick_infochunk_index=chunk_index,)# row and column start index for this chunkdata_row_start=data_slice[0].startdata_col_start=data_slice[1].start# offset tile position using the indices from the big data array# that correspond to this chunkimg.world.x=data_col_startimg.world.y=data_row_startworld_object.add(img)self._set_world_object(world_object)@propertydefdata(self)->TextureArray:"""Get or set the image data"""returnself._data@data.setterdefdata(self,data):self._data[:]=data@propertydefcmap(self)->str:"""colormap name"""ifself.data.value.ndim>2:raiseAttributeError("RGB(A) images do not have a colormap property")returnself._cmap.value@cmap.setterdefcmap(self,name:str):ifself.data.value.ndim>2:raiseAttributeError("RGB(A) images do not have a colormap property")self._cmap.set_value(self,name)@propertydefvmin(self)->float:"""lower contrast limit"""returnself._vmin.value@vmin.setterdefvmin(self,value:float):self._vmin.set_value(self,value)@propertydefvmax(self)->float:"""upper contrast limit"""returnself._vmax.value@vmax.setterdefvmax(self,value:float):self._vmax.set_value(self,value)@propertydefinterpolation(self)->str:"""image data interpolation method"""returnself._interpolation.value@interpolation.setterdefinterpolation(self,value:str):self._interpolation.set_value(self,value)@propertydefcmap_interpolation(self)->str:"""cmap interpolation method"""returnself._cmap_interpolation.value@cmap_interpolation.setterdefcmap_interpolation(self,value:str):self._cmap_interpolation.set_value(self,value)
[docs]defreset_vmin_vmax(self):""" Reset the vmin, vmax by estimating it from the data Returns ------- None """vmin,vmax=quick_min_max(self._data.value)self.vmin=vminself.vmax=vmax
[docs]defadd_linear_selector(self,selection:int=None,axis:str="x",padding:float=None,**kwargs)->LinearSelector:""" Adds a :class:`.LinearSelector`. Parameters ---------- selection: int, optional initial position of the selector padding: float, optional pad the length of the selector kwargs: passed to :class:`.LinearSelector` Returns ------- LinearSelector """ifaxis=="x":size=self._data.value.shape[0]center=size/2limits=(0,self._data.value.shape[1])elifaxis=="y":size=self._data.value.shape[1]center=size/2limits=(0,self._data.value.shape[0])else:raiseValueError("`axis` must be one of 'x' | 'y'")# default padding is 25% the height or width of the imageifpaddingisNone:size*=1.25else:size+=paddingifselectionisNone:selection=limits[0]ifselection<limits[0]orselection>limits[1]:raiseValueError(f"the passed selection: {selection} is beyond the limits: {limits}")selector=LinearSelector(selection=selection,limits=limits,size=size,center=center,axis=axis,parent=self,**kwargs,)self._plot_area.add_graphic(selector,center=False)# place selector above this graphicselector.offset=selector.offset+(0.0,0.0,self.offset[-1]+1)returnselector
[docs]defadd_linear_region_selector(self,selection:tuple[float,float]=None,axis:str="x",padding:float=0.0,fill_color=(0,0,0.35,0.2),**kwargs,)->LinearRegionSelector:""" Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a plot area just like any other ``Graphic``. Parameters ---------- selection: (float, float) initial (min, max) of the selection axis: "x" | "y" axis the selector can move along padding: float, default 100.0 Extends the linear selector along the perpendicular axis to make it easier to interact with. kwargs passed to ``LinearRegionSelector`` Returns ------- LinearRegionSelector linear selection graphic """ifaxis=="x":size=self._data.value.shape[0]center=size/2limits=(0,self._data.value.shape[1])elifaxis=="y":size=self._data.value.shape[1]center=size/2limits=(0,self._data.value.shape[0])else:raiseValueError("`axis` must be one of 'x' | 'y'")# default padding is 25% the height or width of the imageifpaddingisNone:size*=1.25else:size+=paddingifselectionisNone:selection=limits[0],int(limits[1]*0.25)ifpaddingisNone:size*=1.25else:size+=paddingselector=LinearRegionSelector(selection=selection,limits=limits,size=size,center=center,axis=axis,fill_color=fill_color,parent=self,**kwargs,)self._plot_area.add_graphic(selector,center=False)# place above this graphicselector.offset=selector.offset+(0.0,0.0,self.offset[-1]+1)returnselector
[docs]defadd_rectangle_selector(self,selection:tuple[float,float,float,float]=None,fill_color=(0,0,0.35,0.2),**kwargs,)->RectangleSelector:""" Add a :class:`.RectangleSelector`. Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a plot area just like any other ``Graphic``. Parameters ---------- selection: (float, float, float, float), optional initial (xmin, xmax, ymin, ymax) of the selection """# default selection is 25% of the diagonalifselectionisNone:diagonal=math.sqrt(self._data.value.shape[0]**2+self._data.value.shape[1]**2)selection=(0,int(diagonal/4),0,int(diagonal/4))# min/max limits are image shape# rows are ys, columns are xslimits=(0,self._data.value.shape[1],0,self._data.value.shape[0])selector=RectangleSelector(selection=selection,limits=limits,fill_color=fill_color,parent=self,**kwargs,)self._plot_area.add_graphic(selector,center=False)# place above this graphicselector.offset=selector.offset+(0.0,0.0,self.offset[-1]+1)returnselector