"""Modules to generate different imaging artefacts."""importnumpyasnpimportrandomfromtypingimportUnion,Any
[docs]defartefacts_mix(data:np.ndarray,**artefacts_dict:Any)->Union[np.ndarray,list]:"""A module to generate and apply a mix of various typical imaging artefacts to add to the simulated data. One can build various dictionaries with keywords arguments specified bellow and pass it to the function as: `artefacts_mix(data, **noise_dict, **zingers, **etc)`. DISCLAIMER: Note that most of the features are experimental and do not always reflect the accurate modelling of the real imaging artefacts. Args: data (np.ndarray): 2D or 3D numpy array (sinogram or 3D projection data). The input data must be of the following shape: 2D sinogram (anglesDim, DetectorsHoriz), 3D projection data (DetectorsVert, anglesDim, DetectorsHoriz). **artefacts_dict (dict): A dictionary with keyword arguments related to different artefact type. Keyword Args: noise_type (str): Define noise type as 'Poisson' or 'Gaussian'. noise_amplitude (int, float): Photon flux for Poisson or variance for Gaussian noise. noise_seed (int): Seeds for noise generator. 'None' defines random generation. noise_prelog (bool): Set to True if pre-log (raw) data required. zingers_percentage (float): The amount of zingers (dead pixels, outliers) to be added to the data. zingers_modulus (int): Modulus to control the amount of 4/6 pixel clusters to be added. stripes_percentage (float): The amount of stripes in the data (rings in reconstruction). stripes_maxthickness (int): Maxthickness defines the maximal thickness of a stripe. stripes_intensity (float): Controls the intensity levels of stripes. stripe_type (str): Stripe types can be 'partial' or 'full'. stripes_variability (float): Variability multiplier to incorporate the change of intensity in stripe. datashifts_maxamplitude_pixel (int): Controls pixel-wise (integer) misalignment in data. datashifts_maxamplitude_subpixel (float): Controls subpixel misalignment in data. pve_strength (int): The strength of partial volume effect, responsible for the limited resolution of a scanner. fresnel_dist_observation (int): The distance for observation for fresnel propagator. fresnel_scale_factor (float): Fresnel propagator scaling. fresnel_wavelenght (float): Fresnel propagator wavelength. verbose (bool): Make the output of modules verbose. Returns: np.ndarray: 2D/3D numpy array with artefacts applied to input data. [np.ndarray, shifts]: a list of 2D/3D numpy array and simulated shifts. """####### VERBOSE ########if"verbose"notinartefacts_dict:verbose=Trueelse:verbose=artefacts_dict["verbose"]####### NOISE DICTIONARY########_noise_:dict={}if"noise_type"notinartefacts_dict:_noise_["noise_type"]=Noneelse:_noise_["noise_type"]=artefacts_dict["noise_type"]if"noise_amplitude"notinartefacts_dict:_noise_["noise_amplitude"]=1e5else:_noise_["noise_amplitude"]=artefacts_dict["noise_amplitude"]if"noise_seed"notinartefacts_dict:_noise_["noise_seed"]=Noneelse:_noise_["noise_seed"]=artefacts_dict["noise_seed"]if"noise_prelog"notinartefacts_dict:_noise_["noise_prelog"]=Noneelse:_noise_["noise_prelog"]=artefacts_dict["noise_prelog"]####### ZINGERS ########_zingers_:dict={}if"zingers_percentage"notinartefacts_dict:_zingers_["zingers_percentage"]=Noneelse:_zingers_["zingers_percentage"]=artefacts_dict["zingers_percentage"]if"zingers_modulus"notinartefacts_dict:_zingers_["zingers_modulus"]=10else:_zingers_["zingers_modulus"]=artefacts_dict["zingers_modulus"]####### STRIPES ########_stripes_:dict={}if"stripes_percentage"notinartefacts_dict:_stripes_["stripes_percentage"]=Noneelse:_stripes_["stripes_percentage"]=artefacts_dict["stripes_percentage"]if"stripes_maxthickness"notinartefacts_dict:_stripes_["stripes_maxthickness"]=1.0else:_stripes_["stripes_maxthickness"]=artefacts_dict["stripes_maxthickness"]if"stripes_intensity"notinartefacts_dict:_stripes_["stripes_intensity"]=0.1else:_stripes_["stripes_intensity"]=artefacts_dict["stripes_intensity"]if"stripes_type"notinartefacts_dict:_stripes_["stripes_type"]="full"else:_stripes_["stripes_type"]=artefacts_dict["stripes_type"]if"stripes_variability"notinartefacts_dict:_stripes_["stripes_variability"]=0.0else:_stripes_["stripes_variability"]=artefacts_dict["stripes_variability"]####### DATASHIFTS ########_datashifts_:dict={}if"datashifts_maxamplitude_pixel"notinartefacts_dict:_datashifts_["datashifts_maxamplitude_pixel"]=Noneelse:_datashifts_["datashifts_maxamplitude_pixel"]=artefacts_dict["datashifts_maxamplitude_pixel"]if"datashifts_maxamplitude_subpixel"notinartefacts_dict:_datashifts_["datashifts_maxamplitude_subpixel"]=Noneelse:_datashifts_["datashifts_maxamplitude_subpixel"]=artefacts_dict["datashifts_maxamplitude_subpixel"]####### PVE ########_pve_:dict={}if"pve_strength"notinartefacts_dict:_pve_["pve_strength"]=Noneelse:_pve_["pve_strength"]=artefacts_dict["pve_strength"]_fresnel_propagator_:dict={}if"fresnel_dist_observation"notinartefacts_dict:_fresnel_propagator_["fresnel_dist_observation"]=Noneelse:_fresnel_propagator_["fresnel_dist_observation"]=artefacts_dict["fresnel_dist_observation"]if"fresnel_scale_factor"notinartefacts_dict:_fresnel_propagator_["fresnel_scale_factor"]=10else:_fresnel_propagator_["fresnel_scale_factor"]=artefacts_dict["fresnel_scale_factor"]if"fresnel_wavelenght"notinartefacts_dict:_fresnel_propagator_["fresnel_wavelenght"]=0.0001else:_fresnel_propagator_["fresnel_wavelenght"]=artefacts_dict["fresnel_wavelenght"]###########################################################################################Applying artefacts and noise to the data############################################################################################### PARTIAL VOLUME EFFECTif_pve_["pve_strength"]isnotNone:sino_artifacts=np.float32(pve(data=data,pve_strength=_pve_["pve_strength"]))ifverboseisTrue:print("Partial volume effect (PVE) has been simulated.")else:sino_artifacts=np.float32(data)# FRESNEL PROPAGATORif_fresnel_propagator_["fresnel_dist_observation"]isnotNone:sino_artifacts=np.float32(fresnel_propagator(data=sino_artifacts,dist_observation=_fresnel_propagator_["fresnel_dist_observation"],scale_factor=_fresnel_propagator_["fresnel_scale_factor"],wavelenght=_fresnel_propagator_["fresnel_wavelenght"],))ifverboseisTrue:print("Fresnel propagator has been simulated.")# ZINGERSif_zingers_["zingers_percentage"]isnotNone:sino_artifacts=np.float32(zingers(data=sino_artifacts,percentage=_zingers_["zingers_percentage"],modulus=_zingers_["zingers_modulus"],))ifverboseisTrue:print("Zingers have been added to the data.")# STRIPESif_stripes_["stripes_percentage"]isnotNone:sino_artifacts=np.float32(stripes(data=sino_artifacts,percentage=_stripes_["stripes_percentage"],maxthickness=_stripes_["stripes_maxthickness"],intensity_thresh=_stripes_["stripes_intensity"],stripe_type=_stripes_["stripes_type"],variability=_stripes_["stripes_variability"],))ifverboseisTrue:print("Stripes leading to ring artefacts have been simulated.")# DATASHIFTSif_datashifts_["datashifts_maxamplitude_pixel"]isnotNone:[sino_artifacts,shifts]=datashifts(data=sino_artifacts,maxamplitude=_datashifts_["datashifts_maxamplitude_pixel"],)ifverboseisTrue:print("Data shifts have been simulated.")if_datashifts_["datashifts_maxamplitude_subpixel"]isnotNone:[sino_artifacts,shifts]=datashifts_subpixel(data=sino_artifacts,maxamplitude=_datashifts_["datashifts_maxamplitude_subpixel"],)ifverboseisTrue:print("Data shifts (in subpixel precision) have been simulated.")# NOISEif_noise_["noise_type"]isnotNone:sino_artifacts=noise(data=sino_artifacts,sigma=_noise_["noise_amplitude"],noisetype=_noise_["noise_type"],seed=_noise_["noise_seed"],prelog=_noise_["noise_prelog"],)ifverboseisTrue:print("{} noise has been added to the data.".format(_noise_["noise_type"]))if(_datashifts_["datashifts_maxamplitude_pixel"])or(_datashifts_["datashifts_maxamplitude_subpixel"])isnotNone:return[np.float32(sino_artifacts),shifts]else:returnnp.float32(sino_artifacts)
[docs]defstripes(data:np.ndarray,percentage:float,maxthickness:int,intensity_thresh:float,stripe_type:str,variability:float,)->np.ndarray:"""Function to add stripes (constant offsets) to sinograms or 3D projection data which results in rings in the reconstructed image. Args: data (np.ndarray): 2D sinogram (anglesDim, DetectorsHoriz) or 3D projection data (DetectorsVert, anglesDim, DetectorsHoriz). percentage (float): Percentage defines the amount of stripes in the data. maxthickness (int): Defines the maximal thickness of a stripe. intensity_thresh (float): Controls the intensity levels of stripes. stripe_type (str): Choose between 'partial' or 'full'. variability (float): Variability multiplier to incorporate change of intensity in the stripe. Raises: ValueError: Percentage is out of range. ValueError: Thickness is out of range. Returns: np.ndarray: 2D sinogram or 3D projection data with stripes """ifdata.ndim==2:(anglesDim,DetectorsDimH)=np.shape(data)else:(DetectorsDimV,anglesDim,DetectorsDimH)=np.shape(data)if0<percentage<=100:passelse:raiseValueError("percentage must be larger than zero but smaller than 100")if0<=maxthickness<=10:passelse:raiseValueError("maximum thickness must be in [0,10] range")ifstripe_type!="partial":stripe_type="full"sino_stripes=data.copy()max_intensity=np.max(sino_stripes)range_detect=int((np.float32(DetectorsDimH))*(np.float32(percentage)/100.0))ifdata.ndim==2:forxinrange(range_detect):formminrange(0,20):randind=random.randint(0,DetectorsDimH-1)# generate random indexifsino_stripes[0,randind]!=0.0:breakifstripe_type=="partial":randind_ang1=random.randint(0,anglesDim)randind_ang2=random.randint(0,anglesDim)else:randind_ang1=0randind_ang2=anglesDimrandthickness=random.randint(0,maxthickness)# generate random thicknessrandintens=random.uniform(-1.0,0.5)# generate random multiplierintensity=max_intensity*randintens*intensity_threshif(randind>0+randthickness)&(randind<DetectorsDimH-randthickness):forx1inrange(-randthickness,randthickness+1):ifvariability!=0.0:intensity_off=variability*max_intensityelse:intensity_off=0.0forllinrange(randind_ang1,randind_ang2):sino_stripes[ll,randind+x1]+=intensity+intensity_offintensity_off+=([-1,1][random.randrange(2)]*variability*max_intensity)# sino_stripes[randind_ang1:randind_ang2,randind+x1] += intensityelse:forjinrange(DetectorsDimV):forxinrange(range_detect):formminrange(0,20):randind=random.randint(0,DetectorsDimH-1)# generate random indexifsino_stripes[j,0,randind]!=0.0:breakifstripe_type=="partial":randind_ang1=random.randint(0,anglesDim)randind_ang2=random.randint(0,anglesDim)else:randind_ang1=0randind_ang2=anglesDimrandthickness=random.randint(0,maxthickness)# generate random thicknessrandintens=random.uniform(-1,0.5)# generate random multiplierintensity=max_intensity*randintens*intensity_threshif(randind>0+randthickness)&(randind<DetectorsDimH-randthickness):forx1inrange(-randthickness,randthickness+1):ifvariability!=0.0:intensity_off=variability*max_intensityelse:intensity_off=0.0forllinrange(randind_ang1,randind_ang2):sino_stripes[j,ll,randind+x1]+=(intensity+intensity_off)intensity_off+=([-1,1][random.randrange(2)]*variability*max_intensity)returnsino_stripes
[docs]defzingers(data:np.ndarray,percentage:float,modulus:int)->np.ndarray:"""Adding zingers (zero single pixels or small 4 pixels clusters) to sinograms or 6 voxels to 3D projection data. Args: data (np.ndarray): 2D sinogram or 3D projection data. The input data must be of the following shape: 2D sinogram (anglesDim, DetectorsHoriz), 3D projection data (DetectorsVert, anglesDim, DetectorsHoriz). percentage (float): The amount of zingers to be added to the data. modulus (int): Modulus to control the amount of 4/6 pixel clusters to be added. Raises: ValueError: Percentage is out of range. ValueError: Modulus must be positive. Returns: np.ndarray: 2D or 3D array with zingers. """ifdata.ndim==2:(anglesDim,DetectorsDimH)=np.shape(data)else:(DetectorsDimV,anglesDim,DetectorsDimH)=np.shape(data)if0.0<percentage<=100.0:passelse:raiseValueError("percentage must be larger than zero but smaller than 100")ifmodulus>0:passelse:raiseValueError("Modulus integer must be positive")sino_zingers=data.copy()length_sino=np.size(sino_zingers)num_values=int((length_sino)*(np.float32(percentage)/100.0))sino_zingers_fl=sino_zingers.flatten()forxinrange(num_values):randind=random.randint(0,length_sino-1)# generate random indexsino_zingers_fl[randind]=0if(x%int(modulus))==0:ifdata.ndim==2:if(randind>DetectorsDimH)&(randind<length_sino-DetectorsDimH):sino_zingers_fl[randind+1]=0sino_zingers_fl[randind-1]=0sino_zingers_fl[randind+DetectorsDimH]=0sino_zingers_fl[randind-DetectorsDimH]=0else:if(randind>DetectorsDimH*DetectorsDimV)&(randind<length_sino-DetectorsDimH*DetectorsDimV):sino_zingers_fl[randind+1]=0sino_zingers_fl[randind-1]=0sino_zingers_fl[randind+DetectorsDimH]=0sino_zingers_fl[randind-DetectorsDimH]=0sino_zingers_fl[randind+DetectorsDimH*DetectorsDimV]=0sino_zingers_fl[randind-DetectorsDimH*DetectorsDimV]=0sino_zingers[:]=sino_zingers_fl.reshape(sino_zingers.shape)returnsino_zingers
[docs]defnoise(data:np.ndarray,sigma:Union[int,float],noisetype:str,seed:bool=True,prelog:bool=False,)->Union[list,np.ndarray]:"""Adding random noise to data (adapted from LD-CT simulator) Args: data (np.ndarray): N-d array sigma (Union[int, float]): int for Poisson or float for Gaussian noisetype (str): 'Gaussian' or 'Poisson' seed (bool, optional): Initiate random seed with True. Defaults to True. prelog (bool, optional): get raw data if true, returns a list [noisy, raw]!. Defaults to False. Returns: np.ndarray: N-d array """data_noisy=data.copy()maxData=np.max(data)data_noisy=data/maxData# normalisingifseedisTrue:np.random.seed(int(seed))ifnoisetype=="Gaussian":# add normal Gaussian noisedata_noisy+=np.random.normal(loc=0.0,scale=sigma,size=np.shape(data_noisy))data_noisy[data_noisy<0]=0data_noisy*=maxDataelifnoisetype=="Poisson":# add Poisson noiseifmaxData>0:ri=1.0sig=np.sqrt(11)# standard variance of electronic noise, a characteristic of a CT scanneryb=(sigma*np.exp(-data_noisy)+ri)# exponential transform to incident fluxdata_raw=np.random.poisson(yb)+np.sqrt(sig)*np.reshape(np.random.randn(np.size(yb)),np.shape(yb))li_hat=-np.log((data_raw-ri)/sigma)*maxData# log corrected datali_hat[(data_raw-ri)<=0]=0.0data_noisy=li_hat.copy()else:print("Select 'Gaussian' or 'Poisson' for the noise type")ifprelogisTrue:return[data_noisy,data_raw]else:returndata_noisy
[docs]defdatashifts(data:np.ndarray,maxamplitude:int)->list:"""Function to add random pixel shifts to 3D or 3D data as an offset for each angular position. Args: data (np.ndarray): 2D sinogram or 3D projection data. The input data must be of the following shape: 2D sinogram (anglesDim, DetectorsHoriz), 3D projection data (DetectorsVert, anglesDim, DetectorsHoriz). maxamplitude (int): The misilighnment ammplitude. Defines the maximal amplitude of each shift in pixels. Returns: list: 2D or 3d data with misalignment and shifts vectors [data, shifts]. """ifdata.ndim==2:(anglesDim,DetectorsDimH)=np.shape(data)shifts=np.zeros(anglesDim,dtype="int8")# the vector of shiftselse:(DetectorsDimV,anglesDim,DetectorsDimH)=np.shape(data)shifts=np.zeros([anglesDim,2],dtype="int8")# the 2D vector of shiftssino_shifts=np.zeros(np.shape(data),dtype="float32")non=lambdas:sifs<0elseNonemom=lambdas:max(0,s)forxinrange(anglesDim):rand_shift=random.randint(-maxamplitude,maxamplitude)# generate random shift (int)ifdata.ndim==2:shifts[x]=rand_shiftprojection=data[x,:]# extract 1D projectionprojection_shift=np.zeros(np.shape(projection),dtype="float32")projection_shift[mom(rand_shift):non(rand_shift)]=projection[mom(-rand_shift):non(-rand_shift)]sino_shifts[x,:]=projection_shiftelse:rand_shift2=random.randint(-maxamplitude,maxamplitude)# generate random shift (int)shifts[x,0]=rand_shift2shifts[x,1]=rand_shiftprojection2D=data[:,x,:]# extract 2D projectionprojection2D_shift=np.zeros(np.shape(projection2D),dtype="float32")projection2D_shift[mom(rand_shift):non(rand_shift),mom(rand_shift2):non(rand_shift2)]=projection2D[mom(-rand_shift):non(-rand_shift),mom(-rand_shift2):non(-rand_shift2),]sino_shifts[:,x,:]=projection2D_shiftreturn[sino_shifts,shifts]
[docs]defdatashifts_subpixel(data:np.ndarray,maxamplitude:float)->list:"""Function to add random sub-pixel shifts to 3D or 3D data as an offset for each angular position. Args: data (np.ndarray): 2D sinogram or 3D projection data. The input data must be of the following shape: 2D sinogram (anglesDim, DetectorsHoriz), 3D projection data (DetectorsVert, anglesDim, DetectorsHoriz). maxamplitude (float): The misilighnment ammplitude. Defines the maximal amplitude of each shift. Returns: list: 2D or 3d data with misalignment and shifts vectors [data, shifts]. """fromskimageimporttransformastfifdata.ndim==2:shifts=np.zeros([1,2],dtype="float32")random_shift_x=random.uniform(-maxamplitude,maxamplitude)# generate a random floating point numberrandom_shift_y=random.uniform(-maxamplitude,maxamplitude)# generate a random floating point numbertform=tf.SimilarityTransform(translation=(-random_shift_x,-random_shift_y))sino_shifts=tf.warp(data,tform,order=5)else:(DetectorsDimV,anglesDim,DetectorsDimH)=np.shape(data)shifts=np.zeros([anglesDim,2],dtype="float32")# the 2D vector of shiftssino_shifts=np.zeros(np.shape(data),dtype="float32")forxinrange(anglesDim):random_shift_x=random.uniform(-maxamplitude,maxamplitude)# generate a random floating point numberrandom_shift_y=random.uniform(-maxamplitude,maxamplitude)# generate a random floating point numberprojection2D=data[:,x,:]# extract 2D projectiontform=tf.SimilarityTransform(translation=(-random_shift_x,-random_shift_y))projection_shifted=tf.warp(projection2D,tform,order=5)shifts[x,0]=random_shift_xshifts[x,1]=random_shift_ysino_shifts[:,x,:]=projection_shiftedreturn[sino_shifts,shifts]
[docs]defpve(data:np.ndarray,pve_strength:int)->np.ndarray:"""Applying Partial Volume effect (smoothing) to data. Args: data (np.ndarray): 2D sinogram or 3D projection data. The input data must be of the following shape: 2D sinogram (anglesDim, DetectorsHoriz), 3D projection data (DetectorsVert, anglesDim, DetectorsHoriz). pve_strength (int): The level of smoothing, defined by kernel size. Raises: ValueError: Smoothing kernel must be positive Returns: np.ndarray: Smoothed 2D or 3D data. """fromscipy.ndimageimportgaussian_filterdata_pve=data.copy()ifpve_strength>0:passelse:raiseValueError("Smoothing kernel must be positive")ifdata.ndim==2:(anglesDim,DetectorsDimH)=np.shape(data)forxinrange(anglesDim):data_pve[x,:]=gaussian_filter(data_pve[x,:],pve_strength)else:(DetectorsDimV,anglesDim,DetectorsDimH)=np.shape(data)forxinrange(anglesDim):data_pve[:,x,:]=gaussian_filter(data_pve[:,x,:],pve_strength)returndata_pve
[docs]deffresnel_propagator(data:np.ndarray,dist_observation:int,scale_factor:float,wavelenght:float)->np.ndarray:"""Fresnel propagator applied to data. Adapted from the script by Adrián Carbajal-Domínguez, adrian.carbajal@ujat.mx Args: data (np.ndarray): 2D sinogram or 3D projection data. The input data must be of the following shape: 2D sinogram (anglesDim, DetectorsHoriz), 3D projection data (DetectorsVert, anglesDim, DetectorsHoriz). dist_observation (int): The distance for obervation for fresnel propagator. scale_factor (float): Scaling. wavelenght (float): Wavelength. Returns: np.ndarray: 2D or 3D data with propagation. """data_fresnel=data.copy()ifdata.ndim==2:(anglesDim,DetectorsDimH)=np.shape(data)n1=DetectorsDimH*0.5# Define the angular spectrum coordinatesu=np.arange(-n1,n1,1)# Define the propagation matrixpropagator=np.exp(2*np.pi*1j*(dist_observation/scale_factor)*np.sqrt((1/wavelenght)**2-(u/10)**2))#### Compute the Fast Fourier Transform of each 1D projectionforxinrange(anglesDim):f=np.fft.fft(data_fresnel[x,:])# Correct the low and high frequenciesfshift=np.fft.fftshift(f)# multiply both matrices: Fourier transform and the propagator matrices.field=fshift*propagator# Calculate the inverse Fourier transformfield2=np.fft.ifft(field)data_fresnel[x,:]=np.abs(field2)else:(DetectorsDimV,anglesDim,DetectorsDimH)=np.shape(data)####Define the size of the propagation function p(u,v). It has to be of the same size of the image.n1=DetectorsDimV*0.5n2=DetectorsDimH*0.5# Define the angular spectrum coordinatesu=np.arange(-n1,n1,1)v=np.arange(-n2,n2,1)U,V=np.meshgrid(u,v)# Define the propagation matrixpropagator=np.exp(2*np.pi*1j*(dist_observation/scale_factor)*np.sqrt((1/wavelenght)**2-(U/scale_factor)**2-(V/scale_factor)**2))#### Compute the Fast Fourier Transform of each 2D projectionforxinrange(anglesDim):f=np.fft.fft2(data_fresnel[:,x,:])# Correct the low and high frequenciesfshift=np.fft.fftshift(f)# multiply both matrices: Fourier transform and the propagator matrices.field=fshift*np.transpose(propagator)# Calculate the inverse Fourier transformfield2=np.fft.ifft2(field)data_fresnel[:,x,:]=np.abs(field2)returndata_fresnel