# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.""".. Image computation objects (see parent package :mod:`cdl.computation`)"""# pylint: disable=invalid-name # Allows short reference names like x, y, ...# MARK: Important note# --------------------# All `guidata.dataset.DataSet` classes must also be imported in the `cdl.param` module.from__future__importannotationsfromcollections.abcimportCallablefromtypingimportAny,Literalimportguidata.datasetasgdsimportnumpyasnpimportscipy.ndimageasspiimportscipy.signalasspsfromnumpyimportmafromplotpy.mathutils.geometryimportvector_rotation# Import as "csline" to avoid the function to be interpreted as a validation function# in the context of DataLab's validation process:fromplotpy.panels.csection.csitemimportcompute_line_sectionascslinefromskimageimportfiltersimportcdl.algorithms.imageasalgfromcdl.algorithms.datatypesimportclip_astype,is_integer_dtypefromcdl.computation.baseimport(ArithmeticParam,ClipParam,ConstantParam,FFTParam,GaussianParam,HistogramParam,MovingAverageParam,MovingMedianParam,NormalizeParam,SpectrumParam,calc_resultproperties,dst_11,dst_n1n,new_signal_result,)fromcdl.configimport_fromcdl.objimport(BaseProcParam,ImageObj,ResultProperties,ResultShape,ROI2DParam,SignalObj,)VALID_DTYPES_STRLIST=ImageObj.get_valid_dtypenames()
[docs]defrestore_data_outside_roi(dst:ImageObj,src:ImageObj)->None:"""Restore data outside the Region Of Interest (ROI) of the input image after a computation, only if the input image has a ROI, if the data types are compatible, and if the shapes are the same. Otherwise, do nothing. Args: dst: output image object src: input image object """if(src.maskdataisnotNoneand(dst.data.dtype==src.data.dtypeornotis_integer_dtype(dst.data.dtype))anddst.data.shape==src.data.shape):dst.data[src.maskdata]=src.data[src.maskdata]
[docs]classWrap11Func:"""Wrap a 1 array → 1 array function to produce a 1 image → 1 image function, which can be used inside DataLab's infrastructure to perform computations with :class:`cdl.core.gui.processor.image.ImageProcessor`. This wrapping mechanism using a class is necessary for the resulted function to be pickable by the ``multiprocessing`` module. The instance of this wrapper is callable and returns a :class:`cdl.obj.ImageObj` object. Example: >>> import numpy as np >>> from cdl.computation.image import Wrap11Func >>> import cdl.obj >>> def add_noise(data): ... return data + np.random.random(data.shape) >>> compute_add_noise = Wrap11Func(add_noise) >>> data= np.ones((100, 100)) >>> ima0 = cdl.obj.create_image("Example", data) >>> ima1 = compute_add_noise(ima0) Args: func: 1 array → 1 array function *args: Additional positional arguments to pass to the function **kwargs: Additional keyword arguments to pass to the function """def__init__(self,func:Callable,*args:Any,**kwargs:Any)->None:self.func=funcself.args=argsself.kwargs=kwargsself.__name__=func.__name__self.__doc__=func.__doc__self.__call__.__func__.__doc__=self.func.__doc__def__call__(self,src:ImageObj)->ImageObj:"""Compute the function on the input image and return the result image Args: src: input image object Returns: Output image object """suffix=", ".join([str(arg)forarginself.args]+[f"{k}={v}"fork,vinself.kwargs.items()ifvisnotNone])dst=dst_11(src,self.func.__name__,suffix)dst.data=self.func(src.data,*self.args,**self.kwargs)restore_data_outside_roi(dst,src)returndst
[docs]defdst_11_signal(src:ImageObj,name:str,suffix:str|None=None)->SignalObj:"""Create a result signal object, as returned by the callback function of the :func:`cdl.core.gui.processor.base.BaseProcessor.compute_11` method Args: src: input image object name: name of the processing function Returns: Output signal object """returnnew_signal_result(src,name,suffix,(src.xunit,src.zunit),(src.xlabel,src.zlabel))
# MARK: compute_n1 functions -----------------------------------------------------------# Functions with N input images and 1 output image# --------------------------------------------------------------------------------------# Those functions are perfoming a computation on N input images and return a single# output image. If we were only executing these functions locally, we would not need# to define them here, but since we are using the multiprocessing module, we need to# define them here so that they can be pickled and sent to the worker processes.# Also, we need to systematically return the output image object, even if it is already# modified in place, because the multiprocessing module will not be able to retrieve# the modified object from the worker processes.
[docs]defcompute_addition(dst:ImageObj,src:ImageObj)->ImageObj:"""Add **dst** and **src** images and return **dst** image modified in place Args: dst: output image object src: input image object Returns: Output image object (modified in place) """dst.data=np.add(dst.data,src.data,dtype=float)restore_data_outside_roi(dst,src)returndst
[docs]defcompute_product(dst:ImageObj,src:ImageObj)->ImageObj:"""Multiply **dst** and **src** images and return **dst** image modified in place Args: dst: output image object src: input image object Returns: Output image object (modified in place) """dst.data=np.multiply(dst.data,src.data,dtype=float)restore_data_outside_roi(dst,src)returndst
[docs]defcompute_addition_constant(src:ImageObj,p:ConstantParam)->ImageObj:"""Add **dst** and a constant value and return the new result image object Args: src: input image object p: constant value Returns: Result image object **src** + **p.value** (new object) """# For the addition of a constant value, we convert the constant value to the same# data type as the input image, for consistency.value=np.array(p.value).astype(dtype=src.data.dtype)dst=dst_11(src,"+",str(value))dst.data=np.add(src.data,value,dtype=float)restore_data_outside_roi(dst,src)returndst
[docs]defcompute_difference_constant(src:ImageObj,p:ConstantParam)->ImageObj:"""Subtract a constant value from an image and return the new result image object Args: src: input image object p: constant value Returns: Result image object **src** - **p.value** (new object) """# For the subtraction of a constant value, we convert the constant value to the same# data type as the input image, for consistency.value=np.array(p.value).astype(dtype=src.data.dtype)dst=dst_11(src,"-",str(value))dst.data=np.subtract(src.data,value,dtype=float)restore_data_outside_roi(dst,src)returndst
[docs]defcompute_product_constant(src:ImageObj,p:ConstantParam)->ImageObj:"""Multiply **dst** by a constant value and return the new result image object Args: src: input image object p: constant value Returns: Result image object **src** * **p.value** (new object) """# For the multiplication by a constant value, we do not convert the constant value# to the same data type as the input image, because we want to allow the user to# multiply an image by a constant value of a different data type. The final data# type conversion ensures that the output image has the same data type as the input# image.dst=dst_11(src,"×",str(p.value))dst.data=np.multiply(src.data,p.value,dtype=float)restore_data_outside_roi(dst,src)returndst
[docs]defcompute_division_constant(src:ImageObj,p:ConstantParam)->ImageObj:"""Divide an image by a constant value and return the new result image object Args: src: input image object p: constant value Returns: Result image object **src** / **p.value** (new object) """# For the division by a constant value, we do not convert the constant value to the# same data type as the input image, because we want to allow the user to divide an# image by a constant value of a different data type. The final data type conversion# ensures that the output image has the same data type as the input image.dst=dst_11(src,"/",str(p.value))dst.data=np.divide(src.data,p.value,dtype=float)restore_data_outside_roi(dst,src)returndst
# MARK: compute_n1n functions ----------------------------------------------------------# Functions with N input images + 1 input image and N output images# --------------------------------------------------------------------------------------
[docs]defcompute_arithmetic(src1:ImageObj,src2:ImageObj,p:ArithmeticParam)->ImageObj:"""Compute arithmetic operation on two images Args: src1: input image object src2: input image object p: arithmetic parameters Returns: Result image object """initial_dtype=src1.data.dtypetitle=p.operation.replace("obj1",src1.short_id).replace("obj2",src2.short_id)dst=src1.copy(title=title)o,a,b=p.operator,p.factor,p.constant# Apply operatorifoin("×","/")anda==0.0:dst.data=np.ones_like(src1.data)*belifo=="+":dst.data=np.add(src1.data,src2.data,dtype=float)*a+belifo=="-":dst.data=np.subtract(src1.data,src2.data,dtype=float)*a+belifo=="×":dst.data=np.multiply(src1.data,src2.data,dtype=float)*a+belifo=="/":dst.data=np.divide(src1.data,src2.data,dtype=float)*a+b# Eventually convert to initial data typeifp.restore_dtype:dst.data=clip_astype(dst.data,initial_dtype)restore_data_outside_roi(dst,src1)returndst
[docs]defcompute_difference(src1:ImageObj,src2:ImageObj)->ImageObj:"""Compute difference between two images Args: src1: input image object src2: input image object Returns: Result image object **src1** - **src2** (new object) """dst=dst_n1n(src1,src2,"-")dst.data=np.subtract(src1.data,src2.data,dtype=float)restore_data_outside_roi(dst,src1)returndst
[docs]defcompute_quadratic_difference(src1:ImageObj,src2:ImageObj)->ImageObj:"""Compute quadratic difference between two images Args: src1: input image object src2: input image object Returns: Result image object (**src1** - **src2**) / sqrt(2.0) (new object) """dst=dst_n1n(src1,src2,"quadratic_difference")dst.data=np.subtract(src1.data,src2.data,dtype=float)/np.sqrt(2.0)restore_data_outside_roi(dst,src1)returndst
[docs]defcompute_division(src1:ImageObj,src2:ImageObj)->ImageObj:"""Compute division between two images Args: src1: input image object src2: input image object Returns: Result image object **src1** / **src2** (new object) """dst=dst_n1n(src1,src2,"/")dst.data=np.divide(src1.data,src2.data,dtype=float)restore_data_outside_roi(dst,src1)returndst
[docs]defcompute_flatfield(src1:ImageObj,src2:ImageObj,p:FlatFieldParam)->ImageObj:"""Compute flat field correction with :py:func:`cdl.algorithms.image.flatfield` Args: src1: raw data image object src2: flat field image object p: flat field parameters Returns: Output image object """dst=dst_n1n(src1,src2,"flatfield",f"threshold={p.threshold}")dst.data=alg.flatfield(src1.data,src2.data,p.threshold)restore_data_outside_roi(dst,src1)returndst
# MARK: compute_11 functions -----------------------------------------------------------# Functions with 1 input image and 1 output image# --------------------------------------------------------------------------------------
[docs]defcompute_normalize(src:ImageObj,p:NormalizeParam)->ImageObj:""" Normalize image data depending on its maximum, with :py:func:`cdl.algorithms.image.normalize` Args: src: input image object Returns: Output image object """dst=dst_11(src,"normalize",suffix=f"ref={p.method}")dst.data=alg.normalize(src.data,p.method)# type: ignorerestore_data_outside_roi(dst,src)returndst
[docs]classRotateParam(gds.DataSet):"""Rotate parameters"""boundaries=("constant","nearest","reflect","wrap")prop=gds.ValueProp(False)angle=gds.FloatItem(f"{_('Angle')} (°)")mode=gds.ChoiceItem(_("Mode"),list(zip(boundaries,boundaries)),default=boundaries[0])cval=gds.FloatItem(_("cval"),default=0.0,help=_("Value used for points outside the ""boundaries of the input if mode is ""'constant'"),)reshape=gds.BoolItem(_("Reshape the output array"),default=False,help=_("Reshape the output array ""so that the input array is ""contained completely in the output"),)prefilter=gds.BoolItem(_("Prefilter the input image"),default=True).set_prop("display",store=prop)order=gds.IntItem(_("Order"),default=3,min=0,max=5,help=_("Spline interpolation order"),).set_prop("display",active=prop)
[docs]defrotate_obj_coords(angle:float,obj:ImageObj,orig:ImageObj,coords:np.ndarray)->None:"""Apply rotation to coords associated to image obj Args: angle: rotation angle (in degrees) obj: image object orig: original image object coords: coordinates to rotate Returns: Output data """forrowinrange(coords.shape[0]):forcolinrange(0,coords.shape[1],2):x1,y1=coords[row,col:col+2]dx1=x1-orig.xcdy1=y1-orig.ycdx2,dy2=vector_rotation(-angle*np.pi/180.0,dx1,dy1)coords[row,col:col+2]=dx2+obj.xc,dy2+obj.ycobj.roi=None
[docs]defrotate_obj_alpha(obj:ImageObj,orig:ImageObj,coords:np.ndarray,p:RotateParam)->None:"""Apply rotation to coords associated to image obj"""rotate_obj_coords(p.angle,obj,orig,coords)
[docs]defcompute_rotate(src:ImageObj,p:RotateParam)->ImageObj:"""Rotate data with :py:func:`scipy.ndimage.rotate` Args: src: input image object p: parameters Returns: Output image object """dst=dst_11(src,"rotate",f"α={p.angle:.3f}°, mode='{p.mode}'")dst.data=spi.rotate(src.data,p.angle,reshape=p.reshape,order=p.order,mode=p.mode,cval=p.cval,prefilter=p.prefilter,)dst.transform_shapes(src,rotate_obj_alpha,p)returndst
[docs]defrotate_obj_90(dst:ImageObj,src:ImageObj,coords:np.ndarray)->None:"""Apply rotation to coords associated to image obj"""rotate_obj_coords(90.0,dst,src,coords)
[docs]defcompute_rotate90(src:ImageObj)->ImageObj:"""Rotate data 90° with :py:func:`numpy.rot90` Args: src: input image object Returns: Output image object """dst=dst_11(src,"rotate90")dst.data=np.rot90(src.data)dst.transform_shapes(src,rotate_obj_90)returndst
[docs]defrotate_obj_270(dst:ImageObj,src:ImageObj,coords:np.ndarray)->None:"""Apply rotation to coords associated to image obj"""rotate_obj_coords(270.0,dst,src,coords)
[docs]defcompute_rotate270(src:ImageObj)->ImageObj:"""Rotate data 270° with :py:func:`numpy.rot90` Args: src: input image object Returns: Output image object """dst=dst_11(src,"rotate270")dst.data=np.rot90(src.data,3)dst.transform_shapes(src,rotate_obj_270)returndst
# pylint: disable=unused-argument
[docs]defhflip_coords(dst:ImageObj,src:ImageObj,coords:np.ndarray)->None:"""Apply HFlip to coords"""coords[:,::2]=dst.x0+dst.width-coords[:,::2]dst.roi=None
[docs]defcompute_fliph(src:ImageObj)->ImageObj:"""Flip data horizontally with :py:func:`numpy.fliplr` Args: src: input image object Returns: Output image object """dst=Wrap11Func(np.fliplr)(src)dst.transform_shapes(src,hflip_coords)returndst
# pylint: disable=unused-argument
[docs]defvflip_coords(dst:ImageObj,src:ImageObj,coords:np.ndarray)->None:"""Apply VFlip to coords"""coords[:,1::2]=dst.y0+dst.height-coords[:,1::2]dst.roi=None
[docs]defcompute_flipv(src:ImageObj)->ImageObj:"""Flip data vertically with :py:func:`numpy.flipud` Args: src: input image object Returns: Output image object """dst=Wrap11Func(np.flipud)(src)dst.transform_shapes(src,vflip_coords)returndst
[docs]classResizeParam(gds.DataSet):"""Resize parameters"""boundaries=("constant","nearest","reflect","wrap")prop=gds.ValueProp(False)zoom=gds.FloatItem(_("Zoom"))mode=gds.ChoiceItem(_("Mode"),list(zip(boundaries,boundaries)),default=boundaries[0])cval=gds.FloatItem(_("cval"),default=0.0,help=_("Value used for points outside the ""boundaries of the input if mode is ""'constant'"),)prefilter=gds.BoolItem(_("Prefilter the input image"),default=True).set_prop("display",store=prop)order=gds.IntItem(_("Order"),default=3,min=0,max=5,help=_("Spline interpolation order"),).set_prop("display",active=prop)
[docs]defcompute_resize(src:ImageObj,p:ResizeParam)->ImageObj:"""Zooming function with :py:func:`scipy.ndimage.zoom` Args: src: input image object p: parameters Returns: Output image object """dst=dst_11(src,"resize",f"zoom={p.zoom:.3f}")dst.data=spi.interpolation.zoom(src.data,p.zoom,order=p.order,mode=p.mode,cval=p.cval,prefilter=p.prefilter,)ifdst.dxisnotNoneanddst.dyisnotNone:dst.dx,dst.dy=dst.dx/p.zoom,dst.dy/p.zoomreturndst
[docs]classBinningParam(gds.DataSet):"""Binning parameters"""sx=gds.IntItem(_("Cluster size (X)"),default=2,min=2,help=_("Number of adjacent pixels to be combined together along X-axis."),)sy=gds.IntItem(_("Cluster size (Y)"),default=2,min=2,help=_("Number of adjacent pixels to be combined together along Y-axis."),)operations=alg.BINNING_OPERATIONSoperation=gds.ChoiceItem(_("Operation"),list(zip(operations,operations)),default=operations[0],)dtypes=["dtype"]+VALID_DTYPES_STRLISTdtype_str=gds.ChoiceItem(_("Data type"),list(zip(dtypes,dtypes)),help=_("Output image data type."),)change_pixel_size=gds.BoolItem(_("Change pixel size"),default=False,help=_("Change pixel size so that overall image size remains the same."),)
[docs]defcompute_binning(src:ImageObj,param:BinningParam)->ImageObj:"""Binning function on data with :py:func:`cdl.algorithms.image.binning` Args: src: input image object param: parameters Returns: Output image object """dst=dst_11(src,"binning",f"{param.sx}x{param.sy},{param.operation},"f"change_pixel_size={param.change_pixel_size}",)dst.data=alg.binning(src.data,sx=param.sx,sy=param.sy,operation=param.operation,dtype=Noneifparam.dtype_str=="dtype"elseparam.dtype_str,)ifparam.change_pixel_size:ifsrc.dxisnotNoneandsrc.dyisnotNone:dst.dx=src.dx*param.sxdst.dy=src.dy*param.syelse:# TODO: [P2] Instead of removing geometric shapes, apply zoomdst.remove_all_shapes()returndst
[docs]defextract_multiple_roi(src:ImageObj,group:gds.DataSetGroup)->ImageObj:"""Extract multiple regions of interest from data Args: src: input image object group: parameters defining the regions of interest Returns: Output image object """# Initialize x0, y0 with maximum values:y0,x0=ymax,xmax=src.data.shape# Initialize x1, y1 with minimum values:y1,x1=ymin,xmin=0,0forpingroup.datasets:p:ROI2DParamx0i,y0i,x1i,y1i=p.get_bounding_box_indices()x0,y0,x1,y1=min(x0,x0i),min(y0,y0i),max(x1,x1i),max(y1,y1i)x0,y0=max(x0,xmin),max(y0,ymin)x1,y1=min(x1,xmax),min(y1,ymax)suffix=Noneiflen(group.datasets)==1:p=group.datasets[0]suffix=p.get_suffix()dst=dst_11(src,"extract_multiple_roi",suffix)dst.x0+=x0*src.dxdst.y0+=y0*src.dydst.roi=Nonesrc2=src.copy()src2.roi=src2.roi.from_params(src2,group)src2.data[src2.maskdata]=0dst.data=src2.data[y0:y1,x0:x1]returndst
[docs]defextract_single_roi(src:ImageObj,p:ROI2DParam)->ImageObj:"""Extract single ROI Args: src: input image object p: ROI parameters Returns: Output image object """dst=dst_11(src,"extract_single_roi",p.get_suffix())dst.data=p.get_data(src).copy()dst.roi=p.get_extracted_roi(src)x0,y0,_x1,_y1=p.get_bounding_box_indices()dst.x0+=x0*src.dxdst.y0+=y0*src.dyreturndst
[docs]classLineProfileParam(gds.DataSet):"""Horizontal or vertical profile parameters"""_prop=gds.GetAttrProp("direction")_directions=(("horizontal",_("horizontal")),("vertical",_("vertical")))direction=gds.ChoiceItem(_("Direction"),_directions,radio=True).set_prop("display",store=_prop)row=gds.IntItem(_("Row"),default=0,min=0).set_prop("display",active=gds.FuncProp(_prop,lambdax:x=="horizontal"))col=gds.IntItem(_("Column"),default=0,min=0).set_prop("display",active=gds.FuncProp(_prop,lambdax:x=="vertical"))
[docs]defupdate_from_image(self,obj:ImageObj)->None:"""Update parameters from image"""self.__obj=objself.x0=obj.xcself.y0=obj.yc
[docs]defchoice_callback(self,item,value):"""Callback for choice item"""ifvalue=="centroid":self.y0,self.x0=alg.get_centroid_fourier(self.__obj.get_masked_view())elifvalue=="center":self.x0,self.y0=self.__obj.xc,self.__obj.yc
[docs]defcompute_radial_profile(src:ImageObj,p:RadialProfileParam)->SignalObj:"""Compute radial profile around the centroid with :py:func:`cdl.algorithms.image.get_radial_profile` Args: src: input image object p: parameters Returns: Output image object """data=src.get_masked_view()ifp.center=="centroid":y0,x0=alg.get_centroid_fourier(data)elifp.center=="center":x0,y0=src.xc,src.ycelse:x0,y0=p.x0,p.y0suffix=f"center=({x0:.3f}, {y0:.3f})"dst=dst_11_signal(src,"radial_profile",suffix)x,y=alg.get_radial_profile(data,(x0,y0))dst.set_xydata(x,y)returndst
[docs]defcompute_histogram(src:ImageObj,p:HistogramParam)->SignalObj:"""Compute histogram of the image data, with :py:func:`numpy.histogram` Args: src: input image object p: parameters Returns: Output signal object """data=src.get_masked_view().compressed()suffix=p.get_suffix(data)# Also updates p.lower and p.uppery,bin_edges=np.histogram(data,bins=p.bins,range=(p.lower,p.upper))x=(bin_edges[:-1]+bin_edges[1:])/2dst=new_signal_result(src,"histogram",suffix=suffix,units=(src.zunit,""),labels=(src.zlabel,_("Counts")),)dst.set_xydata(x,y)dst.metadata["shade"]=0.5returndst
[docs]defcompute_abs(src:ImageObj)->ImageObj:"""Compute absolute value with :py:data:`numpy.absolute` Args: src: input image object Returns: Output image object """returnWrap11Func(np.absolute)(src)
[docs]defcompute_re(src:ImageObj)->ImageObj:"""Compute real part with :py:func:`numpy.real` Args: src: input image object Returns: Output image object """returnWrap11Func(np.real)(src)
[docs]defcompute_im(src:ImageObj)->ImageObj:"""Compute imaginary part with :py:func:`numpy.imag` Args: src: input image object Returns: Output image object """returnWrap11Func(np.imag)(src)
[docs]classDataTypeIParam(gds.DataSet):"""Convert image data type parameters"""dtype_str=gds.ChoiceItem(_("Destination data type"),list(zip(VALID_DTYPES_STRLIST,VALID_DTYPES_STRLIST)),help=_("Output image data type."),)
[docs]defcompute_astype(src:ImageObj,p:DataTypeIParam)->ImageObj:"""Convert image data type with :py:func:`cdl.algorithms.datatypes.clip_astype` Args: src: input image object p: parameters Returns: Output image object """dst=dst_11(src,"clip_astype",p.dtype_str)dst.data=clip_astype(src.data,p.dtype_str)returndst
[docs]defcompute_psd(src:ImageObj,p:SpectrumParam|None=None)->ImageObj:"""Compute power spectral density with :py:func:`cdl.algorithms.image.psd` Args: src: input image object p: parameters Returns: Output image object """dst=dst_11(src,"psd")log_scale=pisnotNoneandp.logdst.data=alg.psd(src.data,log_scale=log_scale)dst.xunit=dst.yunit=dst.zunit=""dst.xlabel=dst.ylabel=_("Frequency")returndst
[docs]classButterworthParam(gds.DataSet):"""Butterworth filter parameters"""cut_off=gds.FloatItem(_("Cut-off frequency ratio"),default=0.005,min=0.0,max=0.5,help=_("Cut-off frequency ratio"),)high_pass=gds.BoolItem(_("High-pass filter"),default=False,help=_("If True, apply high-pass filter instead of low-pass"),)order=gds.IntItem(_("Order"),default=2,min=1,help=_("Order of the Butterworth filter"),)
# MARK: compute_10 functions -----------------------------------------------------------# Functions with 1 input image and 0 output image# --------------------------------------------------------------------------------------
[docs]defcalc_resultshape(title:str,shape:Literal["rectangle","circle","ellipse","segment","marker","point","polygon"],obj:ImageObj,func:Callable,*args:Any,add_label:bool=False,)->ResultShape|None:"""Calculate result shape by executing a computation function on an image object, taking into account the image origin (x0, y0), scale (dx, dy) and ROIs. Args: title: result title shape: result shape kind obj: input image object func: computation function *args: computation function arguments add_label: if True, add a label item (and the geometrical shape) to plot (default to False) Returns: Result shape object or None if no result is found .. warning:: The computation function must take either a single argument (the data) or multiple arguments (the data followed by the computation parameters). Moreover, the computation function must return a single value or a NumPy array containing the result of the computation. This array contains the coordinates of points, polygons, circles or ellipses in the form [[x, y], ...], or [[x0, y0, x1, y1, ...], ...], or [[x0, y0, r], ...], or [[x0, y0, a, b, theta], ...]. """res=[]num_cols=[]fori_roiinobj.iterate_roi_indices():data_roi=obj.get_data(i_roi)ifargsisNone:coords:np.ndarray=func(data_roi)else:coords:np.ndarray=func(data_roi,*args)# This is a very long condition, but it's still quite readable, so we keep it# as is and disable the pylint warning.## pylint: disable=too-many-boolean-expressionsifnotisinstance(coords,np.ndarray)or((coords.ndim!=2orcoords.shape[1]<2or(coords.shape[1]>5andcoords.shape[1]%2!=0))andcoords.size>0):raiseValueError(f"Computation function {func.__name__} must return a NumPy array "f"containing coordinates of points, polygons, circles or ellipses "f"(in the form [[x, y], ...], or [[x0, y0, x1, y1, ...], ...], or "f"[[x0, y0, r], ...], or [[x0, y0, a, b, theta], ...]), or an empty "f"array.")ifcoords.size:ifcoords.shape[1]%2==0:# Coordinates are in the form [x0, y0, x1, y1, ...]colx,coly=slice(None,None,2),slice(1,None,2)else:# Circle [x0, y0, r] or ellipse coordinates [x0, y0, a, b, theta]colx,coly=0,1coords[:,colx]=obj.dx*coords[:,colx]+obj.x0coords[:,coly]=obj.dy*coords[:,coly]+obj.y0ifobj.roiisnotNone:x0,y0,_x1,_y1=obj.roi.get_single_roi(i_roi).get_bounding_box(obj)coords[:,colx]+=x0coords[:,coly]+=y0idx=np.ones((coords.shape[0],1))*(0ifi_roiisNoneelsei_roi)coords=np.hstack([idx,coords])res.append(coords)num_cols.append(coords.shape[1])ifres:iflen(set(num_cols))!=1:# This happens when the number of columns is not the same for all ROIs.# As of now, this happens only for polygon contours.# We need to pad the arrays with NaNs.max_cols=max(num_cols)num_rows=sum(coords.shape[0]forcoordsinres)array=np.full((num_rows,max_cols),np.nan)row=0forcoordsinres:array[row:row+coords.shape[0],:coords.shape[1]]=coordsrow+=coords.shape[0]else:array=np.vstack(res)returnResultShape(title,array,shape,add_label=add_label)returnNone
[docs]defget_centroid_coords(data:np.ndarray)->np.ndarray:"""Return centroid coordinates with :py:func:`cdl.algorithms.image.get_centroid_fourier` Args: data: input data Returns: Centroid coordinates """y,x=alg.get_centroid_fourier(data)returnnp.array([(x,y)])