Module amaazetools.dicom
Expand source code
#Utilities for processing dicom volumes
import pandas as pd, numpy as np
import matplotlib.pyplot as plt
import pydicom as dicom
import scipy.ndimage as ndimage
import scipy.stats as stats
from skimage import measure
from skimage.transform import rescale
import os, multiprocessing
import sys
from joblib import Parallel, delayed
from . import trimesh as tm
#Withiness is a measure of how well 1D data clusters into two groups
def withiness(x):
""" Computes withiness (how well 1-D data clusters into two groups).
Parameters
----------
x : (n,1) float array
A 1-D collection of data.
Returns
-------
w : float
The withiness of the data.
m : float
The point at which to split the data into 2 clusters.
"""
x = np.sort(x)
sigma = np.std(x)
n = x.shape[0]
v = np.zeros(n-1,)
for i in range(n-1):
x1 = x[:(i+1)]
x2 = x[(i+1):]
m1 = np.mean(x1);
m2 = np.mean(x2);
v[i] = (np.sum((x1-m1)**2) + np.sum((x2-m2)**2))/(sigma**2*n);
ind = np.argmin(v)
m = x[ind]
w = v[ind]
return w,m
def read_dicom_list(files):
""" Reads dicom images from a provided list files, stores in a volume I, and returns resolution dx and dz and a list of files; some may be ommitted if they are not dicom.
Parameters
----------
files : str list
List of files to read from.
Returns
-------
I : Volume of images.
dx : Image resolution dimension.
dz : Image resolution dimension.
dicom_files : List of dicom files.
"""
num_slices = len(files)
dicom_files = []
i = 0
for f in files:
try:
A = dicom.dcmread(f)
dicom_files.append(f)
if i == 0:
s = A.pixel_array.shape
I = np.zeros((num_slices, s[0], s[1]), dtype=int)
dx = np.zeros((num_slices, 2))
dz = np.zeros(num_slices)
I[i, :, :] = A.pixel_array
dx[i, :] = A.PixelSpacing
dz[i] = A.SliceThickness
i += 1
except:
print(f + ' is not a DICOM file.')
if i == 0:
print('No DICOM files found.')
return (None, None, None, None)
else:
print('Found %d DICOM files.' % i)
return (I[:i, :, :], dx[:i, :], dz[:i], dicom_files)
def find_dicom_subdir(directory):
""" Finds subdirectory with the most dicom files
Parameters
----------
directory : str
Directory to search within.
Returns
-------
dicom_dir : str
Subdirectory with the most dicom files.
"""
dicom_dir = None
num_dicom_files = 0
for root, subdirs, files in os.walk(directory):
num = len(files)
if num > num_dicom_files:
num_dicom_files = num
dicom_dir = root
return dicom_dir
def read_dicom_dir(directory):
""" Finds and reads dicom volume from directory; finds the subdirectory with the most dicom files and loads those into a volume.
Parameters
----------
directory : str
Directory to read from.
Returns
-------
I : Volume of images.
dx : Image resolution dimension.
dz : Image resolution dimension.
dicom_files : List of dicom files.
"""
dicom_dir = find_dicom_subdir(directory)
files = os.listdir(dicom_dir)
files.sort()
files = [os.path.join(dicom_dir, f) for f in files]
return read_dicom_list(files)
def add_border(I):
""" Adds border to a given image.
Parameters
----------
I : float array
Image to add border to.
Returns
-------
I : float array
Image with border added.
"""
I[0,:]=1
I[-1,:]=1
I[:,0]=1
I[:,-1]=1
return I
def bone_overview(I,mask=False):
""" Returns an overview of the scan from top and side.
Parameters
----------
I : float array
Image of scan.
mask : boolean, default is False
If True, only work with positive values of I.
Returns
-------
J : float array
Overview of scan from top and side.
"""
m0 = I.shape[0]
m1 = I.shape[1]
m2 = I.shape[2]
I0 = np.sum(I, axis=0)
I1 = np.sum(I, axis=1)
I2 = np.sum(I, axis=2)
if mask:
I0 = I0>0
I1 = I1>0
I2 = I2>0
else:
I0 = I0 / np.max(I0)
I1 = I1 / np.max(I1)
I2 = I2 / np.max(I2)
I0 = add_border(I0)
I1 = add_border(I1)
I2 = add_border(I2)
J = np.zeros((max(m0,m1),2*m2+m1))
J[:m1,:m2] = I0
J[:m0,m2:2*m2] = I1
J[:m0,2*m2:] = I2
return J
def scan_overview(I,true_mean=False):
""" Returns an overview of the scan from top and side.
Parameters
----------
I : float array
Image of scan.
true_mean : boolean, default is False
If True, take mean instead.
Returns
-------
An overview of scan from top and side.
"""
if true_mean:
I1 = np.mean(I,axis=1)
I2 = np.mean(I,axis=2)
else:
I1 = np.sum(I, axis=1)
I2 = np.sum(I, axis=2)
I1 = I1 / np.max(I1)
I2 = I2 / np.max(I2)
return np.hstack((I1, I2)).T
def trim(I, v, padding=20, erosion_width=5):
""" Trim to v level with padding.
Parameters
----------
I : float array
Image of scan.
v : float
Level to be trimmed to.
padding : float, default is 20
erosion_width : float, default is 5
Returns
-------
I : float array
Trimmed image.
"""
J = I > v
#Add extra to account for erosion
#padding = padding + 4*erosion_width
#kernel = np.ones((3,3), dtype=int)
#erosion = cv2.erode(J.astype(float),kernel,iterations=erosion_width)
#J = cv2.dilate(erosion,kernel,iterations=erosion_width)
K = np.sum(np.sum(J,axis=2),axis=1) > 0
ind = np.arange(len(K))[K]
z1 = max(ind.min()-padding,0)
z2 = min(ind.max()+padding,I.shape[0])
K = np.sum(np.sum(J,axis=0),axis=1) > 0
ind = np.arange(len(K))[K]
x1 = max(ind.min()-padding,0)
x2 = min(ind.max()+padding,I.shape[1])
K = np.sum(np.sum(J,axis=0),axis=0) > 0
ind = np.arange(len(K))[K]
y1 = max(ind.min()-padding,0)
y2 = min(ind.max()+padding,I.shape[2])
return I[z1:z2,x1:x2,y1:y2]
def chop_up_scan(I, num_bones=5):
""" Chop up a scan into num_bones fragments.
Parameters
----------
I : float array
Image of scan.
num_bones : int, default is 5
Number of fragments to be chopped into.
Returns
-------
The image of the bone split into fragments.
"""
#Project to 1D
J = np.sum(I, axis=2)
J = np.sum(J, axis=1)
J = J / J.max()
#Gaussian filtering
sigma = len(J) / (num_bones * 8)
K = ndimage.gaussian_filter1d(J, sigma)
#Compute locations of maxima and minima
Km = K[1:-1]
minima = np.arange(len(J) - 2)[((Km < K[2:]) & (Km < K[:-2]))]
maxima = np.arange(len(J) - 2)[((Km > K[2:]) & (Km > K[:-2]))]
m1 = maxima.min()
m2 = maxima.max()
#Get chop locations
chop_loc = minima[((minima > m1) & (minima < m2))]
#Some sanity checking
if len(chop_loc) >= num_bones:
vals = Km[chop_loc]
sort = np.sort(vals)
cutoff = sort[(num_bones - 1)]
chop_loc = chop_loc[(vals < cutoff)]
if len(chop_loc) < num_bones - 1:
print('Warning: Did not find enough bones when chopping up!')
#Adjust the final positions with a finer Gaussian fileter
sigma = len(J) / (num_bones * 16)
K = ndimage.gaussian_filter1d(J, sigma)
Km = K[1:-1]
minima = np.arange(len(J) - 2)[((Km < K[2:]) & (Km < K[:-2]))]
for i in range(len(chop_loc)):
chop_loc[i] = int((chop_loc[i] + minima[np.argmin(np.absolute(minima - chop_loc[i]))]) / 2)
return chop_loc + 1
def imshow(J):
""" Make showing grayscale images easier.
Parameters
----------
J : float array
Image to show.
Returns
-------
None
"""
plt.figure()
plt.imshow(J, cmap='gray')
def surface_bones(directory, iso=2500, write_gif=False):
""" Processes all npz files in directory creating surface and saving to a ply file.
Parameters
----------
directory : str
Directory to work within.
iso : float (optional), default is 2500
Iso level to be used for surfacing.
write_gif : bool (optional), default=False
Whether to output rotating gifs for each object. Requires mayavi, which can be hard to install.
Returns
-------
None
"""
for filename in os.listdir(directory):
if filename.endswith(".npz"):
print('Loading '+filename+'...')
M = np.load(os.path.join(directory,filename))
I = M['I']; dx = M['dx']; dz = M['dz']
#Rescale image to account for different dx/dz dimensions
J = rescale(I.astype(float),(dz/dx,1,1),mode='constant')
#Marching cubes for isosurface
iso_level = iso
verts,faces,normals,values = measure.marching_cubes(J,iso_level)
mesh = tm.mesh(dx*verts,faces) #Multiplication by dx fixes units
#Reverse orientation of triangles (marching_cubes returns inward normals)
mesh.flip_normals()
#Write to ply file
mesh_filename = os.path.join(directory,filename[:-4]+'_iso%d'%iso_level)
print('Saving mesh to '+mesh_filename+'...')
mesh.to_ply(mesh_filename+'.ply')
if write_gif:
mesh.to_gif(mesh_filename+'.gif')
def process_dicom(directory, scanlayout, CTdir='ScanOverviews', Meshdir='Meshes',
chopsheet=None, threshold=2000, padding=15):
""" Processes all dicom scans from scanlayout in a given directory, producing
CT volumes for each object.
Parameters
----------
directory : str
Directory to be working within.
scanlayout : pandas DataFrame
1st column is CT scan data, 2nd column is ScanPacket,
listing the subdirectories for each scan, third column
indicating L2R or R2L, and the next columns indicating
the specimens in that scan
CTdir : str, default is 'ScanOverviews'
The directory to save all results.
Meshdir : str, default is 'Meshes'
The directory to save individual bone fragments
chopsheet : default is None
Gives the option for the user to adjust the automatic chopping of bones.
threshold : float, default is 2000
Threshold to use with CT_side_seg function.
padding : float, default is 15
Padding to use when drawing bounding boxes.
Returns
-------
df : pandas DataFrame
Chopsheet recording locations where objects were cropped. Returned only if chopsheet=None.
"""
#Number of bones in each scan
num_bones = scanlayout.count(axis=1) - 4
#Number of scans
num_scans = len(scanlayout)
#Make CTdir if it doesn't exist
if not os.path.isdir(CTdir):
os.mkdir(CTdir)
#Make Meshdir if it doesn't exist
if not os.path.isdir(Meshdir):
os.mkdir(Meshdir)
#Dataframe to hold chopsheet
if chopsheet is None:
df = pd.DataFrame(columns=['ScanPacket','Process','x1','x2','y1','y2','z1','z2'])
#Loop over all scans
for i in range(num_scans):
#Check if we should process this scan or not
if (chopsheet is None) or chopsheet['Process'][i]:
#Get packet name
subdir = scanlayout['ScanPacket'][i]
if not isinstance(subdir, str):
subdir = str(subdir)
d = os.path.join(directory, subdir)
print('\nLoading scan ' + d + '...')
#Get bone names
bone_names = scanlayout.iloc[i, 4:4+num_bones[i]].values.tolist()
if scanlayout['CTHead2Tail'][i] =='R2L':
print('Reversed')
bone_names.reverse()
#Read CT volume
I, dx, dz, dicom_files = read_dicom_dir(d)
#Process resolutions and check that they are all the same
if np.sum(dx[:,0]!=dx[:,1]):
sys.exit('Error: x,y resolutions are different!!')
dx = dx[:,0]
dx_mode = stats.mode(dx).mode.flatten()[0]
dz_mode = stats.mode(dz).mode.flatten()[0]
ind = (dx != dx_mode) | (dz != dz_mode)
num_diff = np.sum(ind)
if num_diff:
print('Found %d DICOM images with different resolution, removing those...'%num_diff)
I = I[~ind,:,:]
dx = dx_mode
dz = dz_mode
if I is not None:
if chopsheet is None: #Then chop and save to spreadsheet
K1 = CT_side_seg(I,num_bones[i],threshold=threshold,axis=1)
K2 = CT_side_seg(I,num_bones[i],threshold=threshold,axis=2)
x1,x2,y1,y2,z1,z2 = bone_bounding_boxes(K1,K2)
str_x1 = np.array2string(x1, separator=',')
str_x2 = np.array2string(x2, separator=',')
str_y1 = np.array2string(y1, separator=',')
str_y2 = np.array2string(y2, separator=',')
str_z1 = np.array2string(z1, separator=',')
str_z2 = np.array2string(z2, separator=',')
df = df.append({'ScanPacket':subdir, 'Process':True, 'x1':str_x1, 'x2':str_x2,
'y1':str_y1, 'y2':str_y2,
'z1':str_z1, 'z2':str_z2,}, ignore_index=True)
else: #Load chop locations from spreadsheet
x1 = np.fromstring(chopsheet['x1'][i][1:-1], sep=',').astype(int)
x2 = np.fromstring(chopsheet['x2'][i][1:-1], sep=',').astype(int)
y1 = np.fromstring(chopsheet['y1'][i][1:-1], sep=',').astype(int)
y2 = np.fromstring(chopsheet['y2'][i][1:-1], sep=',').astype(int)
z1 = np.fromstring(chopsheet['z1'][i][1:-1], sep=',').astype(int)
z2 = np.fromstring(chopsheet['z2'][i][1:-1], sep=',').astype(int)
J = draw_bounding_boxes(I,x1,x2,y1,y2,z1,z2,padding=padding)
plt.imsave(os.path.join(CTdir, subdir + '.png'), J, cmap='gray')
#Chop, and create overview of CT image of bone
for j in range(len(bone_names)):
bonename = bone_names[j] + '_' + scanlayout['CT'][i]
print('Saving '+bonename+'...')
#Chop
Isub = crop_image(I,x1[j],x2[j],y1[j],y2[j],z1[j],z2[j],padding=padding)
#Correct for mirroring on some scans
if scanlayout['Mirrored'][i] == 'yes':
print('Mirrored')
Isub = Isub[::-1,:,:]
#Create and save overview of bone
Jsub = bone_overview(Isub)
plt.imsave(os.path.join(Meshdir,bonename + '.png'), Jsub, cmap='gray')
#Save 3D bone volume
np.savez_compressed(os.path.join(Meshdir,bonename + '.npz'), I=Isub, dx=dx, dz=dz, bonename=bonename)
if chopsheet is None:
return df
#Segment bones on a side view of CT scanning bed
def CT_side_seg(I,num_bones,threshold=3000,axis=1):
""" Segments bones on a side view of the CT scanning bed.
Parameters
----------
I : float array
Image of scan.
num_bones : int
Number of bones present.
threshold : float, default is 3000
Threshold value to use on I.
axis : int, default is 1
Axis on which to sum I.
Returns
-------
K : float array
Segmented image.
"""
J = np.sum(I > threshold,axis=axis).T > 0
J = J.astype(float)
n = J.shape[0]; m = J.shape[1]
labels = measure.label(J)
#Restrict to the largest num_bones regions
L = np.unique(labels)
num = len(L)
sizes = np.zeros(num)
for i in range(num):
sizes[i] = np.sum(labels==L[i])
sort_ind = np.argsort(-sizes)[1:num_bones+1]
L = L[sort_ind]
#Sort the bones from left to right in the scanning bed
pos = np.zeros(num_bones)
X = np.ones((n,1))@np.reshape(np.arange(m),(1,m))
for i in range(num_bones):
pos[i] = np.mean(X[labels == L[i]])
sort_ind = np.argsort(pos)
L = L[sort_ind]
#Format segmented image to return
K = np.zeros_like(J)
for i in range(num_bones):
K[labels == L[i]] = i+1
return K
#Finds bounding boxes for bones from CT_side_seg images
def bone_bounding_boxes(K1,K2):
""" Finds the bounding boxes for the bones produced by CT_side_seg.
Parameters
----------
K1 : float array
First axis of side segmentation.
K2 : float array
Second axis of side segmentation.
Returns
-------
x1 : (num_bones,1) float array
Min x dimension for each fragment.
x2 : (num_bones,1) float array
Max x dimension for each fragment.
y1 : (num_bones,1) float array
Min y dimension for each fragment.
y2 : (num_bones,1) float array
Max y dimension for each fragment.
z1 : (num_bones,1) float array
Min z dimension for each fragment.
z2 : (num_bones,1) float array
Max z dimension for each fragment.
"""
n = K1.shape[0]; m = K1.shape[1]
num_bones = int(np.max(K1))
X = np.ones((n,1))@np.reshape(np.arange(m),(1,m))
Z = np.reshape(np.arange(n),(n,1))@np.ones((1,m))
x1 = np.zeros(num_bones,dtype=int)
x2 = np.zeros(num_bones,dtype=int)
y1 = np.zeros(num_bones,dtype=int)
y2 = np.zeros(num_bones,dtype=int)
z1 = np.zeros(num_bones,dtype=int)
z2 = np.zeros(num_bones,dtype=int)
for i in range(num_bones):
x1[i] = np.min(X[K1 == i+1])
x2[i] = np.max(X[K1 == i+1])
y1[i] = np.min(Z[K2 == i+1])
y2[i] = np.max(Z[K2 == i+1])
z1[i] = np.min(Z[K1 == i+1])
z2[i] = np.max(Z[K1 == i+1])
return x1,x2,y1,y2,z1,z2
#Crop with padding
def crop_image(I,x1,x2,y1,y2,z1,z2,padding=15):
""" Crop an image with padding.
Parameters
----------
I : float array
Image of scan.
x1 : (num_bones,1) float array
Min x dimension for each fragment.
x2 : (num_bones,1) float array
Max x dimension for each fragment.
y1 : (num_bones,1) float array
Min y dimension for each fragment.
y2 : (num_bones,1) float array
Max y dimension for each fragment.
z1 : (num_bones,1) float array
Min z dimension for each fragment.
z2 : (num_bones,1) float array
Max z dimension for each fragment.
padding : float, default is 15
Padding to use around bounding boxes.
Returns
-------
Cropped image.
"""
#Add padding to boxes
x1 = max(x1 - padding,0)
y1 = max(y1 - padding,0)
z1 = max(z1 - padding,0)
x2 = min(x2 + padding,I.shape[0]-1)
y2 = min(y2 + padding,I.shape[1]-1)
z2 = min(z2 + padding,I.shape[2]-1)
return I[x1:x2,y1:y2,z1:z2]
#Draw bounding boxes on scan
def draw_bounding_boxes(I,x1,x2,y1,y2,z1,z2,padding=15):
""" Draw bounding boxes on a scan.
Parameters
----------
I : float array
Image of a scan.
x1 : (num_bones,1) float array
Min x dimension for each fragment.
x2 : (num_bones,1) float array
Max x dimension for each fragment.
y1 : (num_bones,1) float array
Min y dimension for each fragment.
y2 : (num_bones,1) float array
Max y dimension for each fragment.
z1 : (num_bones,1) float array
Min z dimension for each fragment.
z2 : (num_bones,1) float array
Max z dimension for each fragment.
padding : float, default is 15
Padding to use around bounding boxes.
Returns
-------
Image with bounding boxes around it.
"""
num_bones = len(x1)
#Create side views
I1 = np.sum(I, axis=1)
I2 = np.sum(I, axis=2)
I1 = I1 / np.max(I1)
I2 = I2 / np.max(I2)
#Add padding to boxes
x1 = np.maximum(x1 - padding,0)
y1 = np.maximum(y1 - padding,0)
z1 = np.maximum(z1 - padding,0)
x2 = np.minimum(x2 + padding,I.shape[0]-1)
y2 = np.minimum(y2 + padding,I.shape[1]-1)
z2 = np.minimum(z2 + padding,I.shape[2]-1)
#Draw boxes on I1
for i in range(num_bones):
I1[x1[i]:x2[i],z1[i]] = 1
I1[x1[i]:x2[i],z2[i]] = 1
I1[x1[i],z1[i]:z2[i]] = 1
I1[x2[i],z1[i]:z2[i]] = 1
#Draw boxes on I2
for i in range(num_bones):
I2[x1[i]:x2[i],y1[i]] = 1
I2[x1[i]:x2[i],y2[i]] = 1
I2[x1[i],y1[i]:y2[i]] = 1
I2[x2[i],y1[i]:y2[i]] = 1
return np.hstack((I1,I2)).T
def image_segmentation(I,lam=1,eps=0.5,min_iter=20,max_iter=200,stopping_crit=10,visualize=False):
""" Segment a given image.
Parameters
----------
I : float array
Image of a scan.
lam : float, default is 1
Used to calculate changes.
eps : float, default is 0.5
Used to calculate changes.
min_iter : int, default is 20
Minimum number of iterations on which to run.
max_iter : int, default is 200
Maximum number of iterations on which to run.
stopping_crit : float, default is 10
Used to stop before max iterations.
visualize : boolean, default is False
If True, plot results.
Returns
-------
Positive values of u.
"""
n = I.shape[0]
m = I.shape[1]
Y,X = np.mgrid[:n,:m]
u = np.sin(20*X*np.pi/m)*np.sin(20*Y*np.pi/n)
print(np.max(X))
print(np.max(Y))
num_changed = np.inf
count = 0
tin_old = np.inf
#Set up indexing arrays
Yp = np.arange(1,n+1); Yp[n-1]=n-2
Ym = np.arange(-1,n-1); Ym[0]=1
Xp = np.arange(1,m+1); Xp[m-1]=m-2
Xm = np.arange(-1,m-1); Xm[0]=1
if visualize:
plt.imshow(I,cmap='gray')
plt.axis('off')
seg = plt.contour(X,Y,u,levels=[0],colors=('r'))
dt = np.pi*eps**2/4/lam
while (count < min_iter) or (num_changed > stopping_crit and count < max_iter):
Rin = u>0; Rout = u<=0
Iin = I[Rin]; Iout = I[Rout]
tin = np.sum(Rin); tout= np.sum(Rout)
cin = 0; cout =0
if tin > 0:
cin = np.mean(Iin)
if tout > 0:
cout = np.mean(Iout)
num_changed = abs(tin - tin_old)
tin_old = tin;
#Compute gradient
GE = u[Yp,:] - u
GW = u[Ym,:] - u
GN = u[:,Xm] - u
GS = u[:,Xp] - u
delta = eps/(np.pi*(eps**2 + u**2))
div = (GN/np.sqrt(eps**2 + GN**2) + GE/np.sqrt(eps**2 + GE**2) + GS/np.sqrt(eps**2 + GS**2) + GW/np.sqrt(eps**2 + GW**2))
fid = (I - cout)**2 - (I - cin)**2
u = u + dt*delta*(lam*div + fid)
if visualize:
seg.collections[0].remove()
seg = plt.contour(X,Y,u,levels=[0],colors=('r'))
plt.pause(0.01)
count = count+1;
print(count)
if cin < cout:
u = -u
return u > 0
def seg_plot(I,u):
""" Plot the segmented image.
Parameters
----------
I : float array
Image of a scan.
u : boolean array
The segmentation of that image.
Returns
-------
None
"""
n = I.shape[0]
m = I.shape[1]
Y,X = np.mgrid[:n,:m]
plt.imshow(I,cmap='gray')
plt.axis('off')
plt.contour(X,Y,u,levels=[0],colors=('r'))
def seg_adjacency_matrix(u):
n = I.shape[0]
m = I.shape[1]
Y,X = np.mgrid[:n,:m]
X = X.flatten()
Y = Y.flatten()
u = u.flatten()
C = np.ravel_multi_index((X,Y),(m,n),mode='clip')
E = np.ravel_multi_index((X+1,Y),(m,n),mode='clip')
W = np.ravel_multi_index((X-1,Y),(m,n),mode='clip')
N = np.ravel_multi_index((X,Y-1),(m,n),mode='clip')
S = np.ravel_multi_index((X,Y+1),(m,n),mode='clip')
WE = u(C) == u(E)
WW = u(C) == u(W)
WS = u(C) == u(S)
WN = u(C) == u(N)
ME = sparse.coo_matrix((WE, (C,E)),shape=(n*m,n*m)).to_csr()
MW = sparse.coo_matrix((WW, (C,W)),shape=(n*m,n*m)).to_csr()
MS = sparse.coo_matrix((WS, (C,S)),shape=(n*m,n*m)).to_csr()
MN = sparse.coo_matrix((WN, (C,N)),shape=(n*m,n*m)).to_csr()
M = ME + MW + MS + MN
M = M - sparse.spdiags(M.diagonal(),0,n*m,n*m)
ind = u > 0
M = M[u>0,:]
M = M[:,u>0]
X = X[u>0]
Y = Y[u>0]
return M,X,Y
def surfacing_subproc(filename,directory,iso_level,write_gif=False):
print('Loading '+filename+'...')
M = np.load(os.path.join(directory,filename))
I = M['I']; dx = M['dx']; dz = M['dz']
#Rescale image to account for different dx/dz dimensions
J = rescale(I.astype(float),(dz/dx,1,1),mode='constant')
try:
verts,faces,normals,values = measure.marching_cubes(J,iso_level)
mesh = tm.mesh(dx*verts,faces) #Multiplication by dx fixes units
#Reverse orientation of triangles (marching_cubes returns inward normals)
mesh.flip_normals()
#Write to ply file
mesh_filename = os.path.join(directory,filename[:-4]+'_iso%d'%iso_level)
print('Saving mesh to '+mesh_filename+'...')
mesh.to_ply(mesh_filename+'.ply')
if write_gif:
mesh.to_gif(mesh_filename+'.gif')
return '0'
except Exception as error:
print('surfacing error with ', filename, ': ', error)
return filename
def surface_bones_parallel(directory, iso=2500, write_gif=False,error_fname='./surfacing_errors.csv',ncores='all'):
""" parallelized implementation of surface_bones with also surfacing error support.
Processes all npz files in directory creating surface and saving to a ply file.
Parameters
----------
directory : str
Directory to work within.
iso : float (optional), default is 2500
Iso level to be used for surfacing.
write_gif : bool (optional), default=False
Whether to output rotating gifs for each object. Requires mayavi, which can be hard to install.
error_fname
Returns
-------
None
"""
ddd = os.listdir(directory)
fnames = []
for f in ddd:
if f.endswith('.npz'):
fnames.append(f)
if isinstance(ncores,int):
num_cores = ncores
else:
num_cores =multiprocessing.cpu_count()
errs = Parallel(n_jobs=num_cores)(delayed(surfacing_subproc)(f,directory,iso,write_gif) for f in fnames)
errs = np.array(errs)
errs = errs[errs!='0']
if len(errs)==0:
print('no errors, not saving an error csv.')
else:
print('there were ' + str(len(errs)) +' errors. saving CSV to ',error_fname)
pd.DataFrame(errs).to_csv(error_fname,header=False, index=False)
Functions
def CT_side_seg(I, num_bones, threshold=3000, axis=1)
-
Segments bones on a side view of the CT scanning bed.
Parameters
I
:float array
- Image of scan.
num_bones
:int
- Number of bones present.
threshold
:float
, defaultis 3000
- Threshold value to use on I.
axis
:int
, defaultis 1
- Axis on which to sum I.
Returns
K : float array Segmented image.
Expand source code
def CT_side_seg(I,num_bones,threshold=3000,axis=1): """ Segments bones on a side view of the CT scanning bed. Parameters ---------- I : float array Image of scan. num_bones : int Number of bones present. threshold : float, default is 3000 Threshold value to use on I. axis : int, default is 1 Axis on which to sum I. Returns ------- K : float array Segmented image. """ J = np.sum(I > threshold,axis=axis).T > 0 J = J.astype(float) n = J.shape[0]; m = J.shape[1] labels = measure.label(J) #Restrict to the largest num_bones regions L = np.unique(labels) num = len(L) sizes = np.zeros(num) for i in range(num): sizes[i] = np.sum(labels==L[i]) sort_ind = np.argsort(-sizes)[1:num_bones+1] L = L[sort_ind] #Sort the bones from left to right in the scanning bed pos = np.zeros(num_bones) X = np.ones((n,1))@np.reshape(np.arange(m),(1,m)) for i in range(num_bones): pos[i] = np.mean(X[labels == L[i]]) sort_ind = np.argsort(pos) L = L[sort_ind] #Format segmented image to return K = np.zeros_like(J) for i in range(num_bones): K[labels == L[i]] = i+1 return K
def add_border(I)
-
Adds border to a given image.
Parameters
I
:float array
- Image to add border to.
Returns
I
:float array
- Image with border added.
Expand source code
def add_border(I): """ Adds border to a given image. Parameters ---------- I : float array Image to add border to. Returns ------- I : float array Image with border added. """ I[0,:]=1 I[-1,:]=1 I[:,0]=1 I[:,-1]=1 return I
def bone_bounding_boxes(K1, K2)
-
Finds the bounding boxes for the bones produced by CT_side_seg.
Parameters
K1
:float array
- First axis of side segmentation.
K2
:float array
- Second axis of side segmentation.
Returns
x1
:(num_bones,1) float array
- Min x dimension for each fragment.
x2
:(num_bones,1) float array
- Max x dimension for each fragment.
y1
:(num_bones,1) float array
- Min y dimension for each fragment.
y2
:(num_bones,1) float array
- Max y dimension for each fragment.
z1
:(num_bones,1) float array
- Min z dimension for each fragment.
z2
:(num_bones,1) float array
- Max z dimension for each fragment.
Expand source code
def bone_bounding_boxes(K1,K2): """ Finds the bounding boxes for the bones produced by CT_side_seg. Parameters ---------- K1 : float array First axis of side segmentation. K2 : float array Second axis of side segmentation. Returns ------- x1 : (num_bones,1) float array Min x dimension for each fragment. x2 : (num_bones,1) float array Max x dimension for each fragment. y1 : (num_bones,1) float array Min y dimension for each fragment. y2 : (num_bones,1) float array Max y dimension for each fragment. z1 : (num_bones,1) float array Min z dimension for each fragment. z2 : (num_bones,1) float array Max z dimension for each fragment. """ n = K1.shape[0]; m = K1.shape[1] num_bones = int(np.max(K1)) X = np.ones((n,1))@np.reshape(np.arange(m),(1,m)) Z = np.reshape(np.arange(n),(n,1))@np.ones((1,m)) x1 = np.zeros(num_bones,dtype=int) x2 = np.zeros(num_bones,dtype=int) y1 = np.zeros(num_bones,dtype=int) y2 = np.zeros(num_bones,dtype=int) z1 = np.zeros(num_bones,dtype=int) z2 = np.zeros(num_bones,dtype=int) for i in range(num_bones): x1[i] = np.min(X[K1 == i+1]) x2[i] = np.max(X[K1 == i+1]) y1[i] = np.min(Z[K2 == i+1]) y2[i] = np.max(Z[K2 == i+1]) z1[i] = np.min(Z[K1 == i+1]) z2[i] = np.max(Z[K1 == i+1]) return x1,x2,y1,y2,z1,z2
def bone_overview(I, mask=False)
-
Returns an overview of the scan from top and side.
Parameters
I
:float array
- Image of scan.
mask
:boolean
, defaultis False
- If True, only work with positive values of I.
Returns
J
:float array
- Overview of scan from top and side.
Expand source code
def bone_overview(I,mask=False): """ Returns an overview of the scan from top and side. Parameters ---------- I : float array Image of scan. mask : boolean, default is False If True, only work with positive values of I. Returns ------- J : float array Overview of scan from top and side. """ m0 = I.shape[0] m1 = I.shape[1] m2 = I.shape[2] I0 = np.sum(I, axis=0) I1 = np.sum(I, axis=1) I2 = np.sum(I, axis=2) if mask: I0 = I0>0 I1 = I1>0 I2 = I2>0 else: I0 = I0 / np.max(I0) I1 = I1 / np.max(I1) I2 = I2 / np.max(I2) I0 = add_border(I0) I1 = add_border(I1) I2 = add_border(I2) J = np.zeros((max(m0,m1),2*m2+m1)) J[:m1,:m2] = I0 J[:m0,m2:2*m2] = I1 J[:m0,2*m2:] = I2 return J
def chop_up_scan(I, num_bones=5)
-
Chop up a scan into num_bones fragments.
Parameters
I
:float array
- Image of scan.
num_bones
:int
, defaultis 5
- Number of fragments to be chopped into.
Returns
The image of the bone split into fragments.
Expand source code
def chop_up_scan(I, num_bones=5): """ Chop up a scan into num_bones fragments. Parameters ---------- I : float array Image of scan. num_bones : int, default is 5 Number of fragments to be chopped into. Returns ------- The image of the bone split into fragments. """ #Project to 1D J = np.sum(I, axis=2) J = np.sum(J, axis=1) J = J / J.max() #Gaussian filtering sigma = len(J) / (num_bones * 8) K = ndimage.gaussian_filter1d(J, sigma) #Compute locations of maxima and minima Km = K[1:-1] minima = np.arange(len(J) - 2)[((Km < K[2:]) & (Km < K[:-2]))] maxima = np.arange(len(J) - 2)[((Km > K[2:]) & (Km > K[:-2]))] m1 = maxima.min() m2 = maxima.max() #Get chop locations chop_loc = minima[((minima > m1) & (minima < m2))] #Some sanity checking if len(chop_loc) >= num_bones: vals = Km[chop_loc] sort = np.sort(vals) cutoff = sort[(num_bones - 1)] chop_loc = chop_loc[(vals < cutoff)] if len(chop_loc) < num_bones - 1: print('Warning: Did not find enough bones when chopping up!') #Adjust the final positions with a finer Gaussian fileter sigma = len(J) / (num_bones * 16) K = ndimage.gaussian_filter1d(J, sigma) Km = K[1:-1] minima = np.arange(len(J) - 2)[((Km < K[2:]) & (Km < K[:-2]))] for i in range(len(chop_loc)): chop_loc[i] = int((chop_loc[i] + minima[np.argmin(np.absolute(minima - chop_loc[i]))]) / 2) return chop_loc + 1
def crop_image(I, x1, x2, y1, y2, z1, z2, padding=15)
-
Crop an image with padding.
Parameters
I
:float array
- Image of scan.
x1
:(num_bones,1) float array
- Min x dimension for each fragment.
x2
:(num_bones,1) float array
- Max x dimension for each fragment.
y1
:(num_bones,1) float array
- Min y dimension for each fragment.
y2
:(num_bones,1) float array
- Max y dimension for each fragment.
z1
:(num_bones,1) float array
- Min z dimension for each fragment.
z2
:(num_bones,1) float array
- Max z dimension for each fragment.
padding
:float
, defaultis 15
- Padding to use around bounding boxes.
Returns
Cropped image.
Expand source code
def crop_image(I,x1,x2,y1,y2,z1,z2,padding=15): """ Crop an image with padding. Parameters ---------- I : float array Image of scan. x1 : (num_bones,1) float array Min x dimension for each fragment. x2 : (num_bones,1) float array Max x dimension for each fragment. y1 : (num_bones,1) float array Min y dimension for each fragment. y2 : (num_bones,1) float array Max y dimension for each fragment. z1 : (num_bones,1) float array Min z dimension for each fragment. z2 : (num_bones,1) float array Max z dimension for each fragment. padding : float, default is 15 Padding to use around bounding boxes. Returns ------- Cropped image. """ #Add padding to boxes x1 = max(x1 - padding,0) y1 = max(y1 - padding,0) z1 = max(z1 - padding,0) x2 = min(x2 + padding,I.shape[0]-1) y2 = min(y2 + padding,I.shape[1]-1) z2 = min(z2 + padding,I.shape[2]-1) return I[x1:x2,y1:y2,z1:z2]
def draw_bounding_boxes(I, x1, x2, y1, y2, z1, z2, padding=15)
-
Draw bounding boxes on a scan.
Parameters
I
:float array
- Image of a scan.
x1
:(num_bones,1) float array
- Min x dimension for each fragment.
x2
:(num_bones,1) float array
- Max x dimension for each fragment.
y1
:(num_bones,1) float array
- Min y dimension for each fragment.
y2
:(num_bones,1) float array
- Max y dimension for each fragment.
z1
:(num_bones,1) float array
- Min z dimension for each fragment.
z2
:(num_bones,1) float array
- Max z dimension for each fragment.
padding
:float
, defaultis 15
- Padding to use around bounding boxes.
Returns
Image with bounding boxes around it.
Expand source code
def draw_bounding_boxes(I,x1,x2,y1,y2,z1,z2,padding=15): """ Draw bounding boxes on a scan. Parameters ---------- I : float array Image of a scan. x1 : (num_bones,1) float array Min x dimension for each fragment. x2 : (num_bones,1) float array Max x dimension for each fragment. y1 : (num_bones,1) float array Min y dimension for each fragment. y2 : (num_bones,1) float array Max y dimension for each fragment. z1 : (num_bones,1) float array Min z dimension for each fragment. z2 : (num_bones,1) float array Max z dimension for each fragment. padding : float, default is 15 Padding to use around bounding boxes. Returns ------- Image with bounding boxes around it. """ num_bones = len(x1) #Create side views I1 = np.sum(I, axis=1) I2 = np.sum(I, axis=2) I1 = I1 / np.max(I1) I2 = I2 / np.max(I2) #Add padding to boxes x1 = np.maximum(x1 - padding,0) y1 = np.maximum(y1 - padding,0) z1 = np.maximum(z1 - padding,0) x2 = np.minimum(x2 + padding,I.shape[0]-1) y2 = np.minimum(y2 + padding,I.shape[1]-1) z2 = np.minimum(z2 + padding,I.shape[2]-1) #Draw boxes on I1 for i in range(num_bones): I1[x1[i]:x2[i],z1[i]] = 1 I1[x1[i]:x2[i],z2[i]] = 1 I1[x1[i],z1[i]:z2[i]] = 1 I1[x2[i],z1[i]:z2[i]] = 1 #Draw boxes on I2 for i in range(num_bones): I2[x1[i]:x2[i],y1[i]] = 1 I2[x1[i]:x2[i],y2[i]] = 1 I2[x1[i],y1[i]:y2[i]] = 1 I2[x2[i],y1[i]:y2[i]] = 1 return np.hstack((I1,I2)).T
def find_dicom_subdir(directory)
-
Finds subdirectory with the most dicom files
Parameters
directory
:str
- Directory to search within.
Returns
dicom_dir
:str
- Subdirectory with the most dicom files.
Expand source code
def find_dicom_subdir(directory): """ Finds subdirectory with the most dicom files Parameters ---------- directory : str Directory to search within. Returns ------- dicom_dir : str Subdirectory with the most dicom files. """ dicom_dir = None num_dicom_files = 0 for root, subdirs, files in os.walk(directory): num = len(files) if num > num_dicom_files: num_dicom_files = num dicom_dir = root return dicom_dir
def image_segmentation(I, lam=1, eps=0.5, min_iter=20, max_iter=200, stopping_crit=10, visualize=False)
-
Segment a given image.
Parameters
I
:float array
- Image of a scan.
lam
:float
, defaultis 1
- Used to calculate changes.
eps
:float
, defaultis 0.5
- Used to calculate changes.
min_iter
:int
, defaultis 20
- Minimum number of iterations on which to run.
max_iter
:int
, defaultis 200
- Maximum number of iterations on which to run.
stopping_crit
:float
, defaultis 10
- Used to stop before max iterations.
visualize
:boolean
, defaultis False
- If True, plot results.
Returns
Positive values of u.
Expand source code
def image_segmentation(I,lam=1,eps=0.5,min_iter=20,max_iter=200,stopping_crit=10,visualize=False): """ Segment a given image. Parameters ---------- I : float array Image of a scan. lam : float, default is 1 Used to calculate changes. eps : float, default is 0.5 Used to calculate changes. min_iter : int, default is 20 Minimum number of iterations on which to run. max_iter : int, default is 200 Maximum number of iterations on which to run. stopping_crit : float, default is 10 Used to stop before max iterations. visualize : boolean, default is False If True, plot results. Returns ------- Positive values of u. """ n = I.shape[0] m = I.shape[1] Y,X = np.mgrid[:n,:m] u = np.sin(20*X*np.pi/m)*np.sin(20*Y*np.pi/n) print(np.max(X)) print(np.max(Y)) num_changed = np.inf count = 0 tin_old = np.inf #Set up indexing arrays Yp = np.arange(1,n+1); Yp[n-1]=n-2 Ym = np.arange(-1,n-1); Ym[0]=1 Xp = np.arange(1,m+1); Xp[m-1]=m-2 Xm = np.arange(-1,m-1); Xm[0]=1 if visualize: plt.imshow(I,cmap='gray') plt.axis('off') seg = plt.contour(X,Y,u,levels=[0],colors=('r')) dt = np.pi*eps**2/4/lam while (count < min_iter) or (num_changed > stopping_crit and count < max_iter): Rin = u>0; Rout = u<=0 Iin = I[Rin]; Iout = I[Rout] tin = np.sum(Rin); tout= np.sum(Rout) cin = 0; cout =0 if tin > 0: cin = np.mean(Iin) if tout > 0: cout = np.mean(Iout) num_changed = abs(tin - tin_old) tin_old = tin; #Compute gradient GE = u[Yp,:] - u GW = u[Ym,:] - u GN = u[:,Xm] - u GS = u[:,Xp] - u delta = eps/(np.pi*(eps**2 + u**2)) div = (GN/np.sqrt(eps**2 + GN**2) + GE/np.sqrt(eps**2 + GE**2) + GS/np.sqrt(eps**2 + GS**2) + GW/np.sqrt(eps**2 + GW**2)) fid = (I - cout)**2 - (I - cin)**2 u = u + dt*delta*(lam*div + fid) if visualize: seg.collections[0].remove() seg = plt.contour(X,Y,u,levels=[0],colors=('r')) plt.pause(0.01) count = count+1; print(count) if cin < cout: u = -u return u > 0
def imshow(J)
-
Make showing grayscale images easier.
Parameters
J
:float array
- Image to show.
Returns
None
Expand source code
def imshow(J): """ Make showing grayscale images easier. Parameters ---------- J : float array Image to show. Returns ------- None """ plt.figure() plt.imshow(J, cmap='gray')
def process_dicom(directory, scanlayout, CTdir='ScanOverviews', Meshdir='Meshes', chopsheet=None, threshold=2000, padding=15)
-
Processes all dicom scans from scanlayout in a given directory, producing CT volumes for each object.
Parameters
directory
:str
- Directory to be working within.
scanlayout
:pandas DataFrame
- 1st column is CT scan data, 2nd column is ScanPacket, listing the subdirectories for each scan, third column indicating L2R or R2L, and the next columns indicating the specimens in that scan
CTdir
:str
, defaultis 'ScanOverviews'
- The directory to save all results.
Meshdir
:str
, defaultis 'Meshes'
- The directory to save individual bone fragments
chopsheet
:default is None
- Gives the option for the user to adjust the automatic chopping of bones.
threshold
:float
, defaultis 2000
- Threshold to use with CT_side_seg function.
padding
:float
, defaultis 15
- Padding to use when drawing bounding boxes.
Returns
df
:pandas DataFrame
- Chopsheet recording locations where objects were cropped. Returned only if chopsheet=None.
Expand source code
def process_dicom(directory, scanlayout, CTdir='ScanOverviews', Meshdir='Meshes', chopsheet=None, threshold=2000, padding=15): """ Processes all dicom scans from scanlayout in a given directory, producing CT volumes for each object. Parameters ---------- directory : str Directory to be working within. scanlayout : pandas DataFrame 1st column is CT scan data, 2nd column is ScanPacket, listing the subdirectories for each scan, third column indicating L2R or R2L, and the next columns indicating the specimens in that scan CTdir : str, default is 'ScanOverviews' The directory to save all results. Meshdir : str, default is 'Meshes' The directory to save individual bone fragments chopsheet : default is None Gives the option for the user to adjust the automatic chopping of bones. threshold : float, default is 2000 Threshold to use with CT_side_seg function. padding : float, default is 15 Padding to use when drawing bounding boxes. Returns ------- df : pandas DataFrame Chopsheet recording locations where objects were cropped. Returned only if chopsheet=None. """ #Number of bones in each scan num_bones = scanlayout.count(axis=1) - 4 #Number of scans num_scans = len(scanlayout) #Make CTdir if it doesn't exist if not os.path.isdir(CTdir): os.mkdir(CTdir) #Make Meshdir if it doesn't exist if not os.path.isdir(Meshdir): os.mkdir(Meshdir) #Dataframe to hold chopsheet if chopsheet is None: df = pd.DataFrame(columns=['ScanPacket','Process','x1','x2','y1','y2','z1','z2']) #Loop over all scans for i in range(num_scans): #Check if we should process this scan or not if (chopsheet is None) or chopsheet['Process'][i]: #Get packet name subdir = scanlayout['ScanPacket'][i] if not isinstance(subdir, str): subdir = str(subdir) d = os.path.join(directory, subdir) print('\nLoading scan ' + d + '...') #Get bone names bone_names = scanlayout.iloc[i, 4:4+num_bones[i]].values.tolist() if scanlayout['CTHead2Tail'][i] =='R2L': print('Reversed') bone_names.reverse() #Read CT volume I, dx, dz, dicom_files = read_dicom_dir(d) #Process resolutions and check that they are all the same if np.sum(dx[:,0]!=dx[:,1]): sys.exit('Error: x,y resolutions are different!!') dx = dx[:,0] dx_mode = stats.mode(dx).mode.flatten()[0] dz_mode = stats.mode(dz).mode.flatten()[0] ind = (dx != dx_mode) | (dz != dz_mode) num_diff = np.sum(ind) if num_diff: print('Found %d DICOM images with different resolution, removing those...'%num_diff) I = I[~ind,:,:] dx = dx_mode dz = dz_mode if I is not None: if chopsheet is None: #Then chop and save to spreadsheet K1 = CT_side_seg(I,num_bones[i],threshold=threshold,axis=1) K2 = CT_side_seg(I,num_bones[i],threshold=threshold,axis=2) x1,x2,y1,y2,z1,z2 = bone_bounding_boxes(K1,K2) str_x1 = np.array2string(x1, separator=',') str_x2 = np.array2string(x2, separator=',') str_y1 = np.array2string(y1, separator=',') str_y2 = np.array2string(y2, separator=',') str_z1 = np.array2string(z1, separator=',') str_z2 = np.array2string(z2, separator=',') df = df.append({'ScanPacket':subdir, 'Process':True, 'x1':str_x1, 'x2':str_x2, 'y1':str_y1, 'y2':str_y2, 'z1':str_z1, 'z2':str_z2,}, ignore_index=True) else: #Load chop locations from spreadsheet x1 = np.fromstring(chopsheet['x1'][i][1:-1], sep=',').astype(int) x2 = np.fromstring(chopsheet['x2'][i][1:-1], sep=',').astype(int) y1 = np.fromstring(chopsheet['y1'][i][1:-1], sep=',').astype(int) y2 = np.fromstring(chopsheet['y2'][i][1:-1], sep=',').astype(int) z1 = np.fromstring(chopsheet['z1'][i][1:-1], sep=',').astype(int) z2 = np.fromstring(chopsheet['z2'][i][1:-1], sep=',').astype(int) J = draw_bounding_boxes(I,x1,x2,y1,y2,z1,z2,padding=padding) plt.imsave(os.path.join(CTdir, subdir + '.png'), J, cmap='gray') #Chop, and create overview of CT image of bone for j in range(len(bone_names)): bonename = bone_names[j] + '_' + scanlayout['CT'][i] print('Saving '+bonename+'...') #Chop Isub = crop_image(I,x1[j],x2[j],y1[j],y2[j],z1[j],z2[j],padding=padding) #Correct for mirroring on some scans if scanlayout['Mirrored'][i] == 'yes': print('Mirrored') Isub = Isub[::-1,:,:] #Create and save overview of bone Jsub = bone_overview(Isub) plt.imsave(os.path.join(Meshdir,bonename + '.png'), Jsub, cmap='gray') #Save 3D bone volume np.savez_compressed(os.path.join(Meshdir,bonename + '.npz'), I=Isub, dx=dx, dz=dz, bonename=bonename) if chopsheet is None: return df
def read_dicom_dir(directory)
-
Finds and reads dicom volume from directory; finds the subdirectory with the most dicom files and loads those into a volume.
Parameters
directory
:str
- Directory to read from.
Returns
I : Volume of images. dx : Image resolution dimension. dz : Image resolution dimension. dicom_files : List of dicom files.
Expand source code
def read_dicom_dir(directory): """ Finds and reads dicom volume from directory; finds the subdirectory with the most dicom files and loads those into a volume. Parameters ---------- directory : str Directory to read from. Returns ------- I : Volume of images. dx : Image resolution dimension. dz : Image resolution dimension. dicom_files : List of dicom files. """ dicom_dir = find_dicom_subdir(directory) files = os.listdir(dicom_dir) files.sort() files = [os.path.join(dicom_dir, f) for f in files] return read_dicom_list(files)
def read_dicom_list(files)
-
Reads dicom images from a provided list files, stores in a volume I, and returns resolution dx and dz and a list of files; some may be ommitted if they are not dicom.
Parameters
files
:str list
- List of files to read from.
Returns
I : Volume of images. dx : Image resolution dimension. dz : Image resolution dimension. dicom_files : List of dicom files.
Expand source code
def read_dicom_list(files): """ Reads dicom images from a provided list files, stores in a volume I, and returns resolution dx and dz and a list of files; some may be ommitted if they are not dicom. Parameters ---------- files : str list List of files to read from. Returns ------- I : Volume of images. dx : Image resolution dimension. dz : Image resolution dimension. dicom_files : List of dicom files. """ num_slices = len(files) dicom_files = [] i = 0 for f in files: try: A = dicom.dcmread(f) dicom_files.append(f) if i == 0: s = A.pixel_array.shape I = np.zeros((num_slices, s[0], s[1]), dtype=int) dx = np.zeros((num_slices, 2)) dz = np.zeros(num_slices) I[i, :, :] = A.pixel_array dx[i, :] = A.PixelSpacing dz[i] = A.SliceThickness i += 1 except: print(f + ' is not a DICOM file.') if i == 0: print('No DICOM files found.') return (None, None, None, None) else: print('Found %d DICOM files.' % i) return (I[:i, :, :], dx[:i, :], dz[:i], dicom_files)
def scan_overview(I, true_mean=False)
-
Returns an overview of the scan from top and side.
Parameters
I
:float array
- Image of scan.
true_mean
:boolean
, defaultis False
- If True, take mean instead.
Returns
An overview of scan from top and side.
Expand source code
def scan_overview(I,true_mean=False): """ Returns an overview of the scan from top and side. Parameters ---------- I : float array Image of scan. true_mean : boolean, default is False If True, take mean instead. Returns ------- An overview of scan from top and side. """ if true_mean: I1 = np.mean(I,axis=1) I2 = np.mean(I,axis=2) else: I1 = np.sum(I, axis=1) I2 = np.sum(I, axis=2) I1 = I1 / np.max(I1) I2 = I2 / np.max(I2) return np.hstack((I1, I2)).T
def seg_adjacency_matrix(u)
-
Expand source code
def seg_adjacency_matrix(u): n = I.shape[0] m = I.shape[1] Y,X = np.mgrid[:n,:m] X = X.flatten() Y = Y.flatten() u = u.flatten() C = np.ravel_multi_index((X,Y),(m,n),mode='clip') E = np.ravel_multi_index((X+1,Y),(m,n),mode='clip') W = np.ravel_multi_index((X-1,Y),(m,n),mode='clip') N = np.ravel_multi_index((X,Y-1),(m,n),mode='clip') S = np.ravel_multi_index((X,Y+1),(m,n),mode='clip') WE = u(C) == u(E) WW = u(C) == u(W) WS = u(C) == u(S) WN = u(C) == u(N) ME = sparse.coo_matrix((WE, (C,E)),shape=(n*m,n*m)).to_csr() MW = sparse.coo_matrix((WW, (C,W)),shape=(n*m,n*m)).to_csr() MS = sparse.coo_matrix((WS, (C,S)),shape=(n*m,n*m)).to_csr() MN = sparse.coo_matrix((WN, (C,N)),shape=(n*m,n*m)).to_csr() M = ME + MW + MS + MN M = M - sparse.spdiags(M.diagonal(),0,n*m,n*m) ind = u > 0 M = M[u>0,:] M = M[:,u>0] X = X[u>0] Y = Y[u>0] return M,X,Y
def seg_plot(I, u)
-
Plot the segmented image.
Parameters
I
:float array
- Image of a scan.
u
:boolean array
- The segmentation of that image.
Returns
None
Expand source code
def seg_plot(I,u): """ Plot the segmented image. Parameters ---------- I : float array Image of a scan. u : boolean array The segmentation of that image. Returns ------- None """ n = I.shape[0] m = I.shape[1] Y,X = np.mgrid[:n,:m] plt.imshow(I,cmap='gray') plt.axis('off') plt.contour(X,Y,u,levels=[0],colors=('r'))
def surface_bones(directory, iso=2500, write_gif=False)
-
Processes all npz files in directory creating surface and saving to a ply file.
Parameters
directory
:str
- Directory to work within.
iso
:float (optional)
, defaultis 2500
- Iso level to be used for surfacing.
write_gif
:bool (optional)
, default=False
- Whether to output rotating gifs for each object. Requires mayavi, which can be hard to install.
Returns
None
Expand source code
def surface_bones(directory, iso=2500, write_gif=False): """ Processes all npz files in directory creating surface and saving to a ply file. Parameters ---------- directory : str Directory to work within. iso : float (optional), default is 2500 Iso level to be used for surfacing. write_gif : bool (optional), default=False Whether to output rotating gifs for each object. Requires mayavi, which can be hard to install. Returns ------- None """ for filename in os.listdir(directory): if filename.endswith(".npz"): print('Loading '+filename+'...') M = np.load(os.path.join(directory,filename)) I = M['I']; dx = M['dx']; dz = M['dz'] #Rescale image to account for different dx/dz dimensions J = rescale(I.astype(float),(dz/dx,1,1),mode='constant') #Marching cubes for isosurface iso_level = iso verts,faces,normals,values = measure.marching_cubes(J,iso_level) mesh = tm.mesh(dx*verts,faces) #Multiplication by dx fixes units #Reverse orientation of triangles (marching_cubes returns inward normals) mesh.flip_normals() #Write to ply file mesh_filename = os.path.join(directory,filename[:-4]+'_iso%d'%iso_level) print('Saving mesh to '+mesh_filename+'...') mesh.to_ply(mesh_filename+'.ply') if write_gif: mesh.to_gif(mesh_filename+'.gif')
def surface_bones_parallel(directory, iso=2500, write_gif=False, error_fname='./surfacing_errors.csv', ncores='all')
-
parallelized implementation of surface_bones with also surfacing error support. Processes all npz files in directory creating surface and saving to a ply file.
Parameters
directory
:str
- Directory to work within.
iso
:float (optional)
, defaultis 2500
- Iso level to be used for surfacing.
write_gif
:bool (optional)
, default=False
- Whether to output rotating gifs for each object. Requires mayavi, which can be hard to install.
error_fname
Returns
None
Expand source code
def surface_bones_parallel(directory, iso=2500, write_gif=False,error_fname='./surfacing_errors.csv',ncores='all'): """ parallelized implementation of surface_bones with also surfacing error support. Processes all npz files in directory creating surface and saving to a ply file. Parameters ---------- directory : str Directory to work within. iso : float (optional), default is 2500 Iso level to be used for surfacing. write_gif : bool (optional), default=False Whether to output rotating gifs for each object. Requires mayavi, which can be hard to install. error_fname Returns ------- None """ ddd = os.listdir(directory) fnames = [] for f in ddd: if f.endswith('.npz'): fnames.append(f) if isinstance(ncores,int): num_cores = ncores else: num_cores =multiprocessing.cpu_count() errs = Parallel(n_jobs=num_cores)(delayed(surfacing_subproc)(f,directory,iso,write_gif) for f in fnames) errs = np.array(errs) errs = errs[errs!='0'] if len(errs)==0: print('no errors, not saving an error csv.') else: print('there were ' + str(len(errs)) +' errors. saving CSV to ',error_fname) pd.DataFrame(errs).to_csv(error_fname,header=False, index=False)
def surfacing_subproc(filename, directory, iso_level, write_gif=False)
-
Expand source code
def surfacing_subproc(filename,directory,iso_level,write_gif=False): print('Loading '+filename+'...') M = np.load(os.path.join(directory,filename)) I = M['I']; dx = M['dx']; dz = M['dz'] #Rescale image to account for different dx/dz dimensions J = rescale(I.astype(float),(dz/dx,1,1),mode='constant') try: verts,faces,normals,values = measure.marching_cubes(J,iso_level) mesh = tm.mesh(dx*verts,faces) #Multiplication by dx fixes units #Reverse orientation of triangles (marching_cubes returns inward normals) mesh.flip_normals() #Write to ply file mesh_filename = os.path.join(directory,filename[:-4]+'_iso%d'%iso_level) print('Saving mesh to '+mesh_filename+'...') mesh.to_ply(mesh_filename+'.ply') if write_gif: mesh.to_gif(mesh_filename+'.gif') return '0' except Exception as error: print('surfacing error with ', filename, ': ', error) return filename
def trim(I, v, padding=20, erosion_width=5)
-
Trim to v level with padding.
Parameters
I
:float array
- Image of scan.
v
:float
- Level to be trimmed to.
padding
:float
, defaultis 20
erosion_width
:float
, defaultis 5
Returns
I
:float array
- Trimmed image.
Expand source code
def trim(I, v, padding=20, erosion_width=5): """ Trim to v level with padding. Parameters ---------- I : float array Image of scan. v : float Level to be trimmed to. padding : float, default is 20 erosion_width : float, default is 5 Returns ------- I : float array Trimmed image. """ J = I > v #Add extra to account for erosion #padding = padding + 4*erosion_width #kernel = np.ones((3,3), dtype=int) #erosion = cv2.erode(J.astype(float),kernel,iterations=erosion_width) #J = cv2.dilate(erosion,kernel,iterations=erosion_width) K = np.sum(np.sum(J,axis=2),axis=1) > 0 ind = np.arange(len(K))[K] z1 = max(ind.min()-padding,0) z2 = min(ind.max()+padding,I.shape[0]) K = np.sum(np.sum(J,axis=0),axis=1) > 0 ind = np.arange(len(K))[K] x1 = max(ind.min()-padding,0) x2 = min(ind.max()+padding,I.shape[1]) K = np.sum(np.sum(J,axis=0),axis=0) > 0 ind = np.arange(len(K))[K] y1 = max(ind.min()-padding,0) y2 = min(ind.max()+padding,I.shape[2]) return I[z1:z2,x1:x2,y1:y2]
def withiness(x)
-
Computes withiness (how well 1-D data clusters into two groups).
Parameters
x
:(n,1) float array
- A 1-D collection of data.
Returns
w
:float
- The withiness of the data.
m
:float
- The point at which to split the data into 2 clusters.
Expand source code
def withiness(x): """ Computes withiness (how well 1-D data clusters into two groups). Parameters ---------- x : (n,1) float array A 1-D collection of data. Returns ------- w : float The withiness of the data. m : float The point at which to split the data into 2 clusters. """ x = np.sort(x) sigma = np.std(x) n = x.shape[0] v = np.zeros(n-1,) for i in range(n-1): x1 = x[:(i+1)] x2 = x[(i+1):] m1 = np.mean(x1); m2 = np.mean(x2); v[i] = (np.sum((x1-m1)**2) + np.sum((x2-m2)**2))/(sigma**2*n); ind = np.argmin(v) m = x[ind] w = v[ind] return w,m