183 KiB
183 KiB
In [2]:
!cls
# Libraries
import os
import copy
import numpy as np
import nibabel as nib
import matplotlib.pyplot as plt
# Input data
cwd = os.getcwd()
os.chdir(cwd)In [3]:
filename = os.path.join(cwd,'data','nifti1','avg152T1_RL_nifti.nii.gz') # Neurological Convention RAS
# Load file with NiBabel
img = nib.load(filename)
# Load the data matrix
#img_data = img.get_data() # uint8
img_data = img.get_fdata() # float32
# Display data matrix
display('Raw data matrix:')
plt.imshow(img_data[:,:,50].T, cmap="gray", origin="lower") # Data needs to be transposed for visualization
plt.show()
# Orientation information
print('qform_code:', img.header['qform_code']) # 0: unknown
print('qform matrix: \n', img.get_qform(), '\n')
print('sform_code:', img.header['sform_code']) # 4: mni_152
print('sform matrix: \n', img.get_sform(), '\n')'Raw data matrix:'
qform_code: 0 qform matrix: [[2. 0. 0. 0.] [0. 2. 0. 0.] [0. 0. 2. 0.] [0. 0. 0. 1.]] sform_code: 4 sform matrix: [[ 2. 0. 0. -90.] [ 0. 2. 0. -126.] [ 0. 0. 2. -72.] [ 0. 0. 0. 1.]]
In [4]:
filename = os.path.join(cwd,'data','nifti1','avg152T1_LR_nifti.nii.gz') # Radiological Convention LAS
# Load file with NiBabel
img = nib.load(filename)
# Load the data matrix
#img_data = img.get_data() # uint8
img_data = img.get_fdata() # float32
# Display data matrix
display('Raw data matrix:')
plt.imshow(img_data[:,:,50].T, cmap="gray", origin="lower") # Data needs to be transposed for visualization
plt.show()
# Orientation information
print('qform_code:', img.header['qform_code']) # 0: unknown
print('qform matrix: \n', img.get_qform(), '\n')
print('sform_code:', img.header['sform_code']) # 4: mni_152
print('sform matrix: \n', img.get_sform(), '\n')'Raw data matrix:'
qform_code: 0 qform matrix: [[2. 0. 0. 0.] [0. 2. 0. 0.] [0. 0. 2. 0.] [0. 0. 0. 1.]] sform_code: 4 sform matrix: [[ -2. 0. 0. 90.] [ 0. 2. 0. -126.] [ 0. 0. 2. -72.] [ 0. 0. 0. 1.]]
In [3]:
''''' Important DICOM tags
For further info:
http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_I.4.html
https://www.dicomlibrary.com/dicom/sop/
(0008,0016) SOP Class UID
1.2.840.10008.5.1.4.1.1.128 - Positron Emission Tomography Image
(0018,5100) HFS or FFS (Empty if Unknown)
HFS: Head First Supine
FFS: Feet Fitst Supine
HFP: Head First Prone
FFP: Feet First Prone
(0020,0032) x,y,z coordinates of the upper left hand corner (center of the first voxel transmitted) of the image, in mm (Required tag)
(0020,0037) Image Orientation of the patient (Expected 1\0\0\0\1\0)
Direction cosines of the first row and the first column with respect to the patient
(0028,1052) Intercept
(0028,1053) Slope
(0054,0081) Number of slices per frame
(0054,1000) Series Type of the scan
(0054,0414) Patient Gantry Relationship Code Sequence, i.e. orientation of the patient within the gantry.(Empty if Unknown)
Code Meaning | Retired code | Replacement Code
Headfirst | G-5190 | F-10470
Feetfirst | G-5191 | F-10480
(0054,1330) An index identifying the position of this image within a PET Series
'''''
!cls
## Libraries
import os
import sys
import pydicom
import numpy as np
import tkinter as tk
import nibabel as nib
from tkinter import filedialog
## Input data
# Test data
#input_dicom_dir = os.path.join('data','phantom_EARL1')
#output_dir = os.path.join('data','output')
# GUI
root = tk.Tk() # Creates a blank window with close, maximize and minimize buttons.
root.withdraw() # We don't want a full GUI, so keep the root window from appearing
input_dicom_dir = os.path.abspath(filedialog.askdirectory(title="Please select the input DICOM folder"))
root.deiconify() # Makes the window visible again
root.withdraw()
output_dir = os.path.abspath(filedialog.askdirectory(title="Please select a folder to save output NIfTI file(s)"))
root.destroy() # Destroys the root window along with all other tkinter widgets
study_name = os.path.basename(input_dicom_dir) # The study name is the same as input DICOM directory name
## Predefined variables
ideal_Image_Orientation = ['1', '0', '0', '0', '1', '0']
## Functions
def read_dicom_files(input_dicom_dir):
# Read the DICOM dataset from the input directory.
# If the DICOM dataset is not stored in accordance with the DICOM Standard File Format, SET force to True.
# Do not read files with .db or .xml suffixes (to avoid Thumbs.db and VinciDC0.xml files).
# ADD more such files to avoid later as required!
dicom_files = os.listdir(input_dicom_dir)
try:
dcm_dataset = [pydicom.dcmread(os.path.join(input_dicom_dir, filename), force = False) \
for filename in os.listdir(input_dicom_dir) \
if not filename.endswith(('.db','.xml'))]
except:
sys.exit("Invalid DICOM file(s) was found.")
return dcm_dataset
def get_z_coordinates(dcm_dataset):
# Get the z_coordinates based on the following Required tags.
# Info:
# direction cosines: https://en.wikipedia.org/wiki/Direction_cosine
# http://mathworld.wolfram.com/DirectionCosine.html
# cross product: https://en.wikipedia.org/wiki/Cross_product
# http://mathworld.wolfram.com/CrossProduct.html
# dot product: https://en.wikipedia.org/wiki/Dot_product
# http://mathworld.wolfram.com/DotProduct.html
nr_of_slices = dcm_dataset[0].NumberOfSlices
# x and y direction cosines
x_dir_cos = np.array(list(map(float,dcm_dataset[0].ImageOrientationPatient[:3])))
y_dir_cos = np.array(list(map(float,dcm_dataset[0].ImageOrientationPatient[3:])))
# For vectors a and b, a × b vector is perpendicular to both a and b and thus normal to the plane containing them
z_dir_cos = np.cross(x_dir_cos, y_dir_cos)
image_position_list = [dcm_dataset[i].ImagePositionPatient._list \
for i in range(0, nr_of_slices)]
# Converting the x,y,z coordinates from str to float
image_position_patient = [[float(coordinate) \
for coordinate in image_position] \
for image_position in image_position_list]
# Vector dot product or scalar inner product to get z coordinates
z_coordinates = np.dot(image_position_patient, z_dir_cos)
return z_coordinates, nr_of_slices
def sort_dicom_files(dcm_dataset):
# Sort the DICOM dataset based on the z_coordinates.
z_coordinates, nr_of_slices = get_z_coordinates(dcm_dataset)
sorted_dcm_dataset = [x for _,x in sorted(zip(z_coordinates, dcm_dataset))]
return sorted_dcm_dataset, nr_of_slices
def get_voxel_size_z(dcm_dataset):
# The absolute difference between two consecutive z_coordinates gives the voxel size along the z-direction, in mm.
z_coordinates, _ = get_z_coordinates(dcm_dataset)
voxel_size_z = abs(z_coordinates[0] - z_coordinates[1])
return voxel_size_z
def get_img_volume(dcm_dataset):
# 1. Get the voxel intensity array of each slice
# 2. Transpose the array to account for the change in array order from row-major to column-major
# 3. Stack it in the z-direction
img_volume_array = np.transpose(dcm_dataset[0].pixel_array)
for dcm_slice in dcm_dataset[1:]:
img_slice_array = np.transpose(dcm_slice.pixel_array)
img_volume_array = np.dstack((img_volume_array, img_slice_array))
return img_volume_array
def get_header_data(dcm_set, output_filename):
# Dump the DICOM header into a text file with the same filename as NIFTI
# Set anonymize_flag = "Y" to not add patient data tags in the text file
anonymize_flag = "Y"
patient_tags = ['PatientID', 'PatientName', 'PatientBirthDate']
output_txt_filename = output_filename + ".txt"
file = open(os.path.join(output_dir,output_txt_filename), "w")
for header_tag in dcm_set[0].iterall():
if anonymize_flag == "Y":
if header_tag not in [dcm_set[0].data_element(tag) for tag in patient_tags]:
file.write(str(header_tag) + '\n')
file.close()
def convert_to_nifti(dcm_dataset, output_nifti_file):
img_volume_array_in_LPS = get_img_volume(dcm_dataset)
# From LPS in DICOM to RAS in NIfTI
img_volume_array_in_RAS = np.flipud(np.fliplr(img_volume_array_in_LPS))
#img_volume_array_in_RAS = np.rot90(img_volume_array_in_LPS, 2) # Alternative: 180 rotation instead of two flips
pixel_spacing = dcm_dataset[0].PixelSpacing
voxel_size_z = get_voxel_size_z(dcm_dataset)
voxel_size = np.array([float(pixel_spacing[0]), float(pixel_spacing[1]), voxel_size_z])
slope = dcm_dataset[0].RescaleSlope # (0028,1053)
intercept = dcm_dataset[0].RescaleIntercept # (0028,1052)
# Change the datatype from int to float and apply slope and intercept to the array
img_volume_array = (img_volume_array_in_RAS.astype(float) * slope) + intercept
# Origin of coordinates: centre of the image
center = (voxel_size * img_volume_array.shape) / 2
# Affine matrix
apply_affine = np.diag([voxel_size[0], voxel_size[1], voxel_size[2], 1])
apply_affine[:3,3] = np.array([-center[0], -center[1], -center[2]])
nii_out = nib.Nifti1Image(img_volume_array, apply_affine)
# Adjust NIfTI header
nii_out.header['qform_code'] = 1
nii_out.header['sform_code'] = 2
nii_out.set_data_dtype(np.float32) # Float32: slope=1 and intercept=0. Software does not have to apply manually.
nii_out.header.set_xyzt_units(xyz='mm') # Set the voxel size unit manually
nii_out.header['intent_code'] = 0 # None
nii_out.header['intent_name'] = 'PET'
nii_out.header['cal_max'] = np.max(img_volume_array) # For software to apply proper color scaling for visualization
nii_out.header['cal_min'] = np.min(img_volume_array)
# Save the NIfTI file
nib.save(nii_out, output_nifti_file)
print("DICOM converted to NIfTI: ",output_nifti_file)
## Read DICOM
dcm_dataset = read_dicom_files(input_dicom_dir) # Read the DICOM files from the directory
sorted_dcm_dataset, nr_of_slices = sort_dicom_files(dcm_dataset) # Sort the DICOM files based on z-coordinates
# Check DICOM modality
if sorted_dcm_dataset[0].Modality == 'PT' and sorted_dcm_dataset[0].SOPClassUID == '1.2.840.10008.5.1.4.1.1.128':
print("PET image. Processing...")
frame_number = 1
scan_series_type = sorted_dcm_dataset[0].SeriesType[0] # (0054,1000)
if scan_series_type == 'STATIC' or scan_series_type == 'WHOLE BODY':
print("Static scan. Processing...")
scan_duration_in_sec = int(sorted_dcm_dataset[0].ActualFrameDuration / 1000)
output_filename = "s" + str(frame_number) + "_" + str(scan_duration_in_sec) + "s_" + study_name
output_nifti_filename = output_filename + ".nii"
output_nifti_file = os.path.join(output_dir, output_nifti_filename)
convert_to_nifti(sorted_dcm_dataset, output_nifti_file)
get_header_data(sorted_dcm_dataset, output_filename) # Dump the DICOM header into text file
elif scan_series_type == 'DYNAMIC':
print("Dynamic scan. Processing...")
# Split DICOM dataset in multiple of number of slices per frame.
dcm_dataset_split_by_frames = [sorted_dcm_dataset[x:x+nr_of_slices] \
for x in range(0, len(sorted_dcm_dataset), nr_of_slices)]
for dcm_dataset in dcm_dataset_split_by_frames:
scan_duration_in_sec = int(dcm_dataset[0].ActualFrameDuration / 1000)
output_nifti_filename = output_filename + ".nii"
output_nifti_file = os.path.join(output_dir, output_nifti_filename)
convert_to_nifti(sorted_dcm_dataset, output_nifti_file)
get_header_data(sorted_dcm_dataset, output_filename) # Dump the DICOM header into text file
frame_number += 1
elif scan_series_type == 'GATED':
sys.exit("GATED scans are not supported yet")
else:
sys.exit("Please check Scan Series Type (0054,1000)")
else:
sys.exit("Please check Image Modality")
PET image. Processing... Static scan. Processing... DICOM converted to NIfTI: D:\MyFiles\Documents\Work\DICOM_files\Test\s1_300s_Phantom.nii
In [11]:
!cls
# Libraries
import os
import sys
import numpy as np
import nibabel as nib
## Predefined Variables
voxel_size_EARL = np.array([3.1819, 3.1819, 2])
shape_EARL = (111,256,256)
voxel_size_Clinical = np.array([2.0364201, 2.0364201, 2])
shape_Clinical = (111,400,400)
# Reconstruction protocol used
recon_used = "Clinical" # Options: Clinical, EARL1 and EARL2
## Example dataset
cwd = os.getcwd()
os.chdir(cwd)
# ACCURATE VOIs
voi_filename = "VOI_Clinical_T1.voi"
input_file = os.path.join(cwd,'data','accurate',voi_filename)
# NIfTI Output directory and filename for VOI
voi_nifti_out_filename = 'VOI_Clinical_T1.nii'
output_file = os.path.join(cwd,'data','output',voi_nifti_out_filename)
# Read the VOI file from ACCURATE and get the VOI file data
#voi_file = os.path.join(input_dir, voi_filename)
voi_data = np.fromfile(input_file, dtype = 'byte')
mid_index = int(len(voi_data)/2) # ACCURATE VOI includes first a mask and then the VOI itself
voi_data = np.array(voi_data[mid_index:])
# Set the voxel size according to reconstruction protocol
if recon_used == 'Clinical':
reshaped_voi_data = np.reshape(voi_data, shape_Clinical)
voxel_size = voxel_size_Clinical
elif recon_used == 'EARL1' or recon_used == 'EARL2':
reshaped_voi_data = np.reshape(voi_data, shape_EARL)
voxel_size = voxel_size_EARL
else:
sys.exit('This reconstruction was not implemented yet')
# Transpose the array to change the array order from row-major to column-major
voi_swapped_axes = reshaped_voi_data.T # Also: np.swapaxes(reshaped_voi_data, 2, 0) #
# Binary Mask of 0's and 1's
voi_swapped_axes[voi_swapped_axes <= 50] = 0
voi_swapped_axes[voi_swapped_axes > 50] = 1
# RAS in NIfTI: From Posterior to Anterior
flipped_voi = np.flipud(voi_swapped_axes)
# Shape of image
matrix_size = np.array(flipped_voi.shape)
# Centre of image
center = (voxel_size * matrix_size) / 2
# affine matrix
apply_affine = np.diag([voxel_size[0], voxel_size[1], voxel_size[2], 1])
apply_affine[:3,3] = np.array([-center[0], -center[1], -center[2]])
voi_nifti_out = nib.Nifti1Image(flipped_voi, apply_affine)
# Set header
voi_nifti_out.set_data_dtype(np.int8)
voi_nifti_out.header['qform_code'] = 1
voi_nifti_out.header['sform_code'] = 2
voi_nifti_out.header.set_xyzt_units(xyz='mm') # Set the voxel size unit manually
voi_nifti_out.header.set_data_offset(352)
voi_nifti_out.header['intent_code'] = 0 # None
voi_nifti_out.header['intent_name'] = 'PET_VOI'
voi_nifti_out.header['cal_max'] = np.max(flipped_voi) # For software to apply proper color scaling for visualization
voi_nifti_out.header['cal_min'] = np.min(flipped_voi)
# Save the NIfTI file
nib.save( voi_nifti_out, os.path.join(output_dir, voi_nifti_out_filename) )
print('ACCURATE VOI was successfully saved as NIfTI file')
print(os.path.join(output_dir, voi_nifti_out_filename))
ACCURATE VOI saved as NIfTI data\output\VOI_Clinical_T1.nii
In [5]:
filename = os.path.join(cwd, 'data','pmod','avg152T1_PMOD.nii') # Data Saved by PMOD (HFS : radiological)
img = nib.load(filename)
img_data = img.get_fdata()
img_data = np.reshape(img_data, img_data.shape[0:3]) # PMOD Stores data always as 4D
# Display data matrix
display('Raw data matrix:')
plt.imshow(img_data[:,:,50].T, cmap="gray", origin="lower")
plt.show()
# Orientation information
print('qform_code:', img.header['qform_code']) # 1: Scanner-based
print('qform matrix: \n', img.get_qform(), '\n')
print('sform_code:', img.header['sform_code']) # 2: Aligned
print('sform matrix: \n', img.get_sform(), '\n')'Raw data matrix:'
qform_code: 1 qform matrix: [[ -2. 0. 0. 180.] [ 0. -2. 0. 216.] [ 0. 0. 2. 0.] [ 0. 0. 0. 1.]] sform_code: 2 sform matrix: [[ -2. -0. -0. 180.] [ -0. -2. -0. 216.] [ -0. -0. 2. 0.] [ 0. 0. 0. 1.]]
In [12]:
img_data_fix = np.rot90(img_data, 2) # LPS to RAS = 180 degree rotation
vox = img.header.get_zooms()
center = ((np.array(img.shape[0:3]) - 1) / 2) * vox[0:3] ## CHECK if -1 is needed
affine = np.diag(vox)
affine[0:3,3] = -center
# Save NIfTI file with the RAS orientation and no affine transformation
nii = nib.Nifti1Image(img_data_fix, affine)
nii.header['qform_code'] = 1
nii_filename = os.path.splitext(filename)[0] + '_fixed.nii.gz'
nib.save(nii, nii_filename)
# Display data matrix
display('Raw data matrix (PMOD Fixed):')
plt.imshow(img_data_fix[:,:,50].T, cmap="gray", origin="lower")
plt.show()
# Orientation information
print('qform_code:', nii.header['qform_code']) # 0: Unknown
print('qform matrix: \n', nii.get_qform(), '\n')
print('sform_code:', nii.header['sform_code']) # 2: Aligned
print('sform matrix: \n', nii.get_sform(), '\n')'Raw data matrix (PMOD Fixed):'
qform_code: 1 qform matrix: [[ 2. 0. 0. -90.] [ 0. 2. 0. -108.] [ 0. 0. 2. -90.] [ 0. 0. 0. 1.]] sform_code: 2 sform matrix: [[ 2. 0. 0. -90.] [ 0. 2. 0. -108.] [ 0. 0. 2. -90.] [ 0. 0. 0. 1.]]
In [7]:
# Load Neurological Convention (RAS) data
filename = os.path.join(cwd, 'data/nifti1/avg152T1_RL_nifti.nii.gz')
# Load file with NiBabel
nb_img = nib.load(filename)
nb_data = nb_img.get_fdata()
N0 = copy.deepcopy(nb_data[:,:,50]) # Example slide
# Display data matrix (NiBabel)
plt.figure()
display('NiBabel (Display = M(row,column)):')
plt.imshow(N0, cmap="gray")
plt.show()
# Example, remove part of the data
N1 = copy.deepcopy(N0)
N1[20:60,:] = 0
plt.figure()
plt.imshow(N1, cmap="gray")
plt.show()
# Load file with SimpleITK
import SimpleITK as sitk
reader = sitk.ImageFileReader()
reader.SetImageIO("NiftiImageIO")
reader.SetFileName(filename)
itk_img = reader.Execute()
itk_data = sitk.GetArrayFromImage(itk_img)
I0 = copy.deepcopy(itk_data[:,:,50])
# Display data matrix (SimpleITK)
plt.figure()
display('SimpleITK (Display = M(row,column)):')
plt.imshow(I0, cmap="gray")
plt.show()
# LPS to RAS
I2 = copy.deepcopy(itk_data.T)[:,:,50]
# Display data matrix (SimpleITK)
display('Remember that SimpleITK uses LPS convention, while NiBabel RAS')
display('To have the data in the same orientation, you need to transpose the data')
plt.figure()
plt.imshow(I2, cmap="gray")
plt.show()'NiBabel (Display = M(row,column)):'
'SimpleITK (Display = M(row,column)):'
'Remember that SimpleITK uses LPS convention, while NiBabel RAS'
'To have the data in the same orientation, you need to transpose the data'
