import pyvista as pv
import numpy as np
import string
import glob
import os
import nrrd
nrrdLibraryPath='/Users/alessandromastrofini/LocalFiles/DataPatient/Clinical_data/'
[docs]
def cleanPtName(pt):
"""
Cleans the patient's name by removing any letters and underscores, leaving only numeric characters.
Args:
pt (str): The name of the patient (string with potential letters and underscores).
Returns:
str: A cleaned string containing only the numeric part of the patient name.
"""
pt_no_letter = pt.translate(str.maketrans('', '', string.ascii_letters))
pt_num=pt_no_letter.replace('_','')
return pt_num
[docs]
def findAllPtFields(pt):
"""
Finds all available NRRD fields for a given patient by searching the NRRD library directory.
Args:
pt (str): The name of the patient.
Returns:
list: A list of available field names extracted from the NRRD files.
"""
ptNum=cleanPtName(pt)
directory=nrrdLibraryPath+ptNum
name_pattern='*.nrrd'
files = glob.glob(f'{directory}/{name_pattern}')
files=[os.path.basename(file) for file in files]
files_useful = [file for file in files if 'multi' not in file]
files_useful = [file.replace('.nrrd','') for file in files_useful]
files_useful = [file.split('_')[-1] for file in files_useful]
#print(files_useful)
return files_useful
[docs]
def findPtData(pt, mat, strToRemove='multi'):
"""
Finds the path to a specific NRRD data file for the patient, given the material name (e.g., 'lumen').
Args:
pt (str): The name of the patient.
mat (str): The material name to search for (e.g., 'lumen').
strToRemove (str, optional): String to filter out unwanted files (e.g., 'multi'). Defaults to 'multi'.
Returns:
str: The full file path to the patient's NRRD data file.
"""
ptNum=cleanPtName(pt)
directory=nrrdLibraryPath+ptNum
name_pattern='*'+mat+'*.nrrd'
files = glob.glob(f'{directory}/{name_pattern}')
files_useful = [file for file in files if strToRemove not in file]
return files_useful[0]
[docs]
def setUpNRRDField(self):
filename=findPtData(self.name,mat='lumen')
reader = pv.get_reader(filename)
mesh = reader.read()
data, header = nrrd.read(filename)
scale=[header['space directions'][i,i] for i in range(3)]
mesh=mesh.scale(scale)
mesh=mesh.scale(0.001)
self.clinicalData=mesh
self.clinicalData.point_data["ImageFile"]=np.where(mesh.point_data["ImageFile"] < 0, 0, 0) #empty field
return self.clinicalData
[docs]
def loadNRRD(self, mat='general'):
"""
Loads a specific NRRD file for the patient and integrates the data into the patient's clinical mesh.
The mesh is scaled based on the header's spatial information, and the data is thresholded to create a segmentation field.
Args:
mat (str, optional): The material type to load (e.g., 'lumen', 'wall'). Defaults to 'general'.
Returns:
pyvista.PolyData: The updated clinical data mesh with the new segmentation field.
"""
filename=findPtData(self.name,mat=mat)
#print(filename)
data, header = nrrd.read(filename)
#print(header['space directions'])
scale=[header['space directions'][i,i] for i in range(3)]
#print(scale)
reader = pv.get_reader(filename)
mesh = reader.read()
#print(mesh.bounds)
mesh=mesh.scale(scale)
mesh=mesh.scale(0.001)
#print(mesh.bounds)
data = mesh.point_data["ImageFile"]
data = data-np.min(data) #makw all data positive
threshold = np.percentile(data, 10)
#self.clinicalData=mesh
self.clinicalData.point_data[mat]=np.where(data < threshold, 1, 0)
self.nrrdHeader[mat]=header
return self.clinicalData
[docs]
def loadNRRDcomposition(self):
"""
Loads the NRRD composition file for the patient, which contains multi-region data (e.g., plaque composition).
The data for each region is thresholded and combined with the wall data to compute segmented fields for each region.
Returns:
self: The patient object with updated clinical data and available region information.
"""
filename=findPtData(self.name,mat='composition',strToRemove='Batch') #remove Batch_xxx
# this is the complex header containing the composition of the plaque
# like (:,:,:,i) where i is the number of the material
data, header = nrrd.read(filename)
regions = []
for i in range(data.shape[-1]):
regions.append(header['slice'+str(i)+'_region'])
#print(regions)
self.info['AvailableRegions']=regions
# now we can add the composition to the clinicalData
data_wall=self.clinicalData.point_data['wall']
data_bin=np.zeros((len(regions),len(data_wall)))
for i in range(len(regions)):
data_temp=data[:,:,:,i].flatten(order='F')
data_temp=data_temp-np.min(data_temp) #makw all data positive
threshold = np.percentile(data_temp, 10) # Set this to whatever threshold you want
data_bin[i]=np.where(data_temp<threshold, 1, 0)
# boolean product between wall and region
self.clinicalData.point_data[regions[i]]=data_bin[i]*data_wall
self.nrrdHeader['composition']=header
self.nrrdHeader['composition']['region']='General composition'
return self
[docs]
def postProcessNRRD(self):
"""
Post-processes the loaded clinical data by computing the wall segmentation as the difference between the total wall and the lumen segmentation.
Returns:
pyvista.PolyData: The updated clinical data mesh with the new 'wall' field.
"""
self.clinicalData.point_data['lumen']=self.clinicalData.point_data['lumenSegmentation']
lumenData=self.clinicalData.point_data['lumenSegmentation']
wallData=self.clinicalData.point_data['wallSegmentation']
self.clinicalData.point_data['wall']=np.where(wallData-lumenData>0, 1, 0)
return self.clinicalData
[docs]
def computeVolumetricData(mesh, mat='ImageFile', returnBounds=False):
"""
Computes the volumetric data for a specified region of the mesh, based on a scalar field (e.g., 'ImageFile').
The mesh is thresholded, and the volume and bounds are computed.
Args:
mesh (pyvista.PolyData): The clinical data mesh.
mat (str, optional): The scalar field to threshold and compute. Defaults to 'ImageFile'.
returnBounds (bool, optional): If True, returns the bounds and sizes of the volume. Defaults to False.
Returns:
float: The volume of the thresholded region if `returnBounds` is False.
tuple: If `returnBounds` is True, returns the volume, bounds, and sizes of the region.
"""
thresholded = mesh.threshold([0.5,1],scalars=mat)
bounds=thresholded.bounds
sizes=[bounds[1]-bounds[0],bounds[3]-bounds[2],bounds[5]-bounds[4]]
#vol=sizes[0]*sizes[1]*sizes[2]
vol=thresholded.volume
if returnBounds:
return vol, bounds, sizes
else:
return thresholded