Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development #118

Merged
merged 60 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
61feed9
Create paper.md
qurit-frizi Jul 24, 2024
961a4f3
Update paper.md
qurit-frizi Jul 25, 2024
b347260
Update paper.md
qurit-frizi Jul 25, 2024
31873d9
Create paper.bib
qurit-frizi Jul 25, 2024
1665384
Update paper.bib
qurit-frizi Jul 25, 2024
415145d
Update paper.md
qurit-frizi Jul 25, 2024
783e10a
Update paper.md
qurit-frizi Aug 26, 2024
ff6acf6
Update paper.bib
qurit-frizi Aug 27, 2024
cd13f6c
Update paper.md
qurit-frizi Aug 27, 2024
3220ff4
Create CONTRIBUTING.md
qurit-frizi Aug 30, 2024
8f7fb86
Update README.md
qurit-frizi Aug 30, 2024
9c3e472
Update CONTRIBUTING.md
qurit-frizi Aug 30, 2024
79b0837
Update CONTRIBUTING.md
qurit-frizi Aug 30, 2024
67be6aa
Update paper.bib
qurit-frizi Sep 17, 2024
19f5b16
Update paper.bib
qurit-frizi Sep 19, 2024
f448691
Update python-app.yml
qurit-frizi Oct 2, 2024
25d04ff
Update and rename python-app.yml to draft-pdf.yml
qurit-frizi Oct 2, 2024
92fda89
Update paper.bib
qurit-frizi Oct 2, 2024
0295d27
Update draft-pdf.yml
qurit-frizi Oct 2, 2024
884dbed
Update draft-pdf.yml
qurit-frizi Oct 2, 2024
c25671e
Update draft-pdf.yml
qurit-frizi Oct 2, 2024
19f121e
Update draft-pdf.yml
qurit-frizi Oct 2, 2024
3333b23
Update paper.bib
qurit-frizi Oct 3, 2024
93c793a
Update paper.bib
qurit-frizi Oct 3, 2024
d52df8d
Add files via upload
qurit-frizi Oct 3, 2024
a7a763f
Update paper.md
qurit-frizi Oct 3, 2024
2625b0a
Update paper.md
qurit-frizi Oct 3, 2024
282fb1b
Update paper.bib
qurit-frizi Oct 4, 2024
7a7c9e7
Update paper.bib
qurit-frizi Oct 9, 2024
77b2de4
Update paper.bib
qurit-frizi Oct 9, 2024
92d3c49
Update paper.bib
qurit-frizi Oct 9, 2024
16c0acc
Update paper.bib
qurit-frizi Oct 9, 2024
2a9732e
Update paper.md
qurit-frizi Nov 29, 2024
f5bc1ef
Update paper.md
qurit-frizi Nov 29, 2024
3c4773c
Update CONTRIBUTING.md
qurit-frizi Nov 29, 2024
3210d96
Update paper.md
qurit-frizi Nov 30, 2024
963a799
Update paper.md
qurit-frizi Nov 30, 2024
d4aed2b
Update paper.md
qurit-frizi Nov 30, 2024
cf3936e
Update paper.md
qurit-frizi Nov 30, 2024
7b4b2c1
Update paper.md
qurit-frizi Nov 30, 2024
2d27d99
Update paper.md
qurit-frizi Nov 30, 2024
e871df5
Update README.md
qurit-frizi Dec 6, 2024
0778b58
Update README.md
qurit-frizi Dec 6, 2024
8aa6c76
Update README.md
qurit-frizi Dec 6, 2024
eb0afe4
Update README.md
qurit-frizi Dec 6, 2024
f990e2c
Add files via upload
qurit-frizi Dec 6, 2024
808da0b
Add files via upload
qurit-frizi Dec 10, 2024
ac7df73
Update paper.md
qurit-frizi Dec 10, 2024
3edcdc1
Update paper.md
qurit-frizi Dec 10, 2024
5294a99
Update paper.bib
qurit-frizi Dec 10, 2024
e8de249
Rename nifti2rt_FYR.py to nifti2rt.py
qurit-frizi Dec 11, 2024
c154d7b
Rename file_conversion_FYR.py to NIFTI_conversion.py
qurit-frizi Dec 11, 2024
2be5d45
Update paper.md
qurit-frizi Dec 24, 2024
3ca768e
Update paper.md
qurit-frizi Dec 24, 2024
273cc74
Update paper.md
qurit-frizi Dec 24, 2024
544f5b1
Update paper.md
qurit-frizi Dec 24, 2024
a6ff22a
Update README.md
qurit-frizi Jan 3, 2025
e968323
Update README.md
qurit-frizi Jan 3, 2025
40a537f
Update README.md
qurit-frizi Jan 3, 2025
b4a3fff
Add files via upload
qurit-frizi Jan 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/draft-pdf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Draft PDF
on:
push:
paths:
- Paper/** # Update to track changes in the correct directory
- .github/workflows/draft-pdf.yml

jobs:
paper:
runs-on: ubuntu-latest
name: Paper Draft
steps:
- name: Checkout
uses: actions/checkout@v4

- name: List directory contents
run: ls -R # Lists the directory contents for debugging

- name: Build draft PDF
uses: openjournals/openjournals-draft-action@master
with:
journal: joss
paper-path: Paper/paper.md # Specify the correct path to paper.md

- name: Upload
uses: actions/upload-artifact@v4
with:
name: paper
path: Paper/paper.pdf # Ensure the output path matches where the PDF is generated
38 changes: 38 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Contributing to RT-utils

Thank you for considering contributing to our project! Here are some guidelines to help you get started.

**Bugs**
1. Open an issue.
2. Provide as much context as you can about what you're running into.
3. Provide project and platform versions.

## How to Report Issues
- Before reporting an issue, check if it has already been reported.
- Include detailed information about the issue, including steps to reproduce, environment details, and screenshots if applicable.

The recommended method of contributing is as follows:
1. Create an issue on the [issues page](https://github.com/qurit/rt-utils/issues)
2. Fork the repository to your own account
3. Fix issue and push changes to your own fork on GitHub
4. Create a pull request from your fork (whatever branch you worked on) to the development branch in the main repository.

## Sample Data
The links to the example data are in the:
rt-utils/tests/mock_data/
rt-utils/tests/one_slice_data/
rt-utils/tests/oriented_data/

## How to Submit Code
- Fork the repository and create a new branch for your feature or bug fix.
- Make sure your code follows the project’s coding style and passes all tests.
- Submit a pull request with a clear description of your changes and the problem they solve.

## Code of Conduct
- Please be respectful and considerate in all interactions.

## Contact
- If you have questions or need help, reach out to yousefi.f@gmail.com.
## License

RT-utils is subject to the `MIT License`, which can be found in the project's root.
251 changes: 251 additions & 0 deletions NIFTI_conversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import os
import pydicom
import numpy as np
import SimpleITK as sitk
from rt_utils import RTStructBuilder
import csv
import json
import dateutil

# Define your DICOM root directory here:
IMAGE_FOLDER_PATH = "///data"

ATTRIBUTE_FILE_NAME = "attributes.csv"
HEADERS_FILE_NAME = "headers.json"
SAVE_JSON = False

def winapi_path(dos_path, encoding=None):
# Simplified for non-Windows usage:
return os.path.abspath(dos_path)

def bqml_to_suv(dcm_file: pydicom.FileDataset) -> float:
'''
Calculates the SUV conversion factor from Bq/mL to g/mL using DICOM header info.
This simplified version returns only the SUV factor.
'''
nuclide_dose = dcm_file[0x054, 0x0016][0][0x0018, 0x1074].value # Injected dose (Bq)
weight = dcm_file[0x0010, 0x1030].value # Patient weight (kg)
half_life = float(dcm_file[0x054, 0x0016][0][0x0018, 0x1075].value) # Half life (s)

series_time = str(dcm_file[0x0008, 0x0031].value) # Series time (HHMMSS)
series_date = str(dcm_file[0x0008, 0x0021].value) # Series date (YYYYMMDD)
series_dt = dateutil.parser.parse(series_date + ' ' + series_time)

nuclide_time = str(dcm_file[0x054, 0x0016][0][0x0018, 0x1072].value) # Injection time
nuclide_dt = dateutil.parser.parse(series_date + ' ' + nuclide_time)

delta_time = (series_dt - nuclide_dt).total_seconds()
decay_correction = 2 ** (-1 * delta_time/half_life)
suv_factor = (weight * 1000) / (decay_correction * nuclide_dose)
return suv_factor

def getDicomHeaders(file):
dicomHeaders = file.to_json_dict()
# remove pixel data from headers
dicomHeaders.pop('7FE00010', None)
return dicomHeaders

def get_patient_nifti_dir(dicom_dir):
# This function finds the patient's directory and creates a NIFTI folder inside it.
# Assuming structure: .../data/patientX/DICOM/...
# We want: .../data/patientX/NIFTI/
patient_dir = dicom_dir
# Move up until we exit DICOM directories
# Typically, dicom_dir might look like: /.../data/patientX/DICOM/studyY
# One dirname: /.../data/patientX/DICOM
# Another dirname: /.../data/patientX
patient_dir = os.path.dirname(os.path.dirname(dicom_dir)) # This should now point to patientX directory
nifti_dir = os.path.join(patient_dir, "NIFTI")
if not os.path.exists(nifti_dir):
os.makedirs(nifti_dir)
return nifti_dir

def dicomToNifti(file, seriesDir):
patientID, modality, studyDate = getattr(file, 'PatientID', None), getattr(file, 'Modality', None), getattr(file, 'StudyDate', None)
reader = sitk.ImageSeriesReader()
seriesNames = reader.GetGDCMSeriesFileNames(seriesDir)
reader.SetFileNames(seriesNames)
image = reader.Execute()

# Convert PET to SUV if needed
if modality == 'PT':
pet = pydicom.dcmread(seriesNames[0]) # read one image
suv_factor = bqml_to_suv(pet)
image = sitk.Multiply(image, suv_factor)

nifti_dir = get_patient_nifti_dir(seriesDir)
output_filename = os.path.join(nifti_dir, f'{patientID}_{modality}_{studyDate}.nii.gz')
sitk.WriteImage(image, output_filename, imageIO='NiftiImageIO')

def sortParallelLists(list1, list2):
if len(list1) > 0 and len(list2) > 0:
tuples = zip(*sorted(zip(list1, list2)))
list1, list2 = [list(tuple) for tuple in tuples]
return list1, list2

def buildMaskArray(file, seriesPath, labelPath) -> np.ndarray:
rtstruct = RTStructBuilder.create_from(dicom_series_path=seriesPath, rt_struct_path=labelPath)
rois = rtstruct.get_roi_names()
masks = [rtstruct.get_roi_mask_by_name(roi).astype(int) for roi in rois]

final_mask = sum(masks)
final_mask = np.where(final_mask>=1, 1, 0)
# Reorient mask
final_mask = np.moveaxis(final_mask, [0, 1, 2], [1, 2, 0])
return final_mask

def buildMasks(file, seriesPath, labelPath):
final_mask = buildMaskArray(file, seriesPath, labelPath)
reader = sitk.ImageSeriesReader()
dicom_names = reader.GetGDCMSeriesFileNames(seriesPath)
reader.SetFileNames(dicom_names)
ref_img = reader.Execute()

mask_img = sitk.GetImageFromArray(final_mask)
mask_img.CopyInformation(ref_img)

nifti_dir = get_patient_nifti_dir(seriesPath)
patientID, modality, studyDate = getattr(file, 'PatientID', None), getattr(file, 'Modality', None), getattr(file, 'StudyDate', None)
output_filename = os.path.join(nifti_dir, f'{patientID}_{modality}_{studyDate}_mask.nii.gz')
sitk.WriteImage(mask_img, output_filename, imageIO="NiftiImageIO")

def convertFiles():
dicomFilePaths = []
dicomFileDirs = []
dicomFileTraits = []
dicomFileHeaders = []
dicomFileHeaderKeys = []
labelInstanceUIDs = []
seriesInstanceUIDs = []
labelPaths = []
seriesPaths = []

# Rename directories with overly long names
for _ in range(3):
for root, dirs, files in os.walk(IMAGE_FOLDER_PATH):
for dir in dirs:
if len(dir) > 20:
i = 5
while os.path.exists(os.path.join(root, dir[:i])):
i += 1
newDir = dir[:i]
os.rename(os.path.join(root, dir), os.path.join(root, newDir))

# Collect DICOM file paths
for root, dirs, files in os.walk(IMAGE_FOLDER_PATH):
for file in files:
if file.endswith('.dcm'):
filePath = winapi_path(os.path.join(root, file))
fileDirname = os.path.dirname(filePath)
if len(dicomFilePaths) > 0 and fileDirname == dicomFileDirs[-1]:
dicomFilePaths[-1].append(filePath)
else:
dicomFilePaths.append([filePath])
dicomFileDirs.append(fileDirname)

# Analyze DICOM files
for i in range(len(dicomFilePaths)):
if i % 10 == 0 or i == len(dicomFilePaths)-1:
print(f'Processing {round((i + 1) / len(dicomFilePaths) * 100, 2)}% of files')
file = pydicom.dcmread(dicomFilePaths[i][0], force=True)
headers = getDicomHeaders(file)
traits = {
"Patient ID": getattr(file, 'PatientID', None),
"Patient's Sex": getattr(file, 'PatientSex', None),
"Patient's Age": getattr(file, 'PatientAge', None),
"Patient's Birth Date": getattr(file, 'PatientBirthDate', None),
"Patient's Weight": getattr(file, 'PatientWeight', None),
"Institution Name": getattr(file, 'InstitutionName', None),
"Referring Physician's Name": getattr(file, 'ReferringPhysicianName', None),
"Operator's Name": getattr(file, 'OperatorsName', None),
"Study Date": getattr(file, 'StudyDate', None),
"Study Time": getattr(file, 'StudyTime', None),
"Modality": getattr(file, 'Modality', None),
"Series Description": getattr(file, 'SeriesDescription', None),
"Dimensions": np.array(getattr(file, 'pixel_array', np.array([]))).shape,
}
for key in headers.keys():
if key not in dicomFileHeaderKeys:
dicomFileHeaderKeys.append(key)
dicomFileTraits.append(traits)
dicomFileHeaders.append(headers)

fileModality = getattr(file, 'Modality', None)

# If it's an RTSTRUCT, track the referenced SeriesInstanceUID
if fileModality == 'RTSTRUCT':
seriesInstanceUID = headers['30060010']['Value'][0]['30060012']['Value'][0]['30060014']['Value'][0]['0020000E']['Value'][0]
labelInstanceUIDs.append(seriesInstanceUID)
labelPaths.append(dicomFilePaths[i][0])

# Identify which series correspond to RTSTRUCT
for i in range(len(dicomFileDirs)):
if i % 10 == 0 or i == len(dicomFileDirs)-1:
print(f'Scanning series directories {round((i+1)/len(dicomFileDirs)*100, 2)}%')
file = pydicom.dcmread(dicomFilePaths[i][0], force=True)
fileModality = getattr(file, 'Modality', None)
seriesInstanceUID = getDicomHeaders(file)['0020000E']['Value'][0]
if fileModality != 'RTSTRUCT':
if seriesInstanceUID in labelInstanceUIDs:
seriesPaths.append(dicomFileDirs[i])
seriesInstanceUIDs.append(seriesInstanceUID)

labelInstanceUIDs, labelPaths = sortParallelLists(labelInstanceUIDs, labelPaths)
seriesInstanceUIDs, seriesPaths = sortParallelLists(seriesInstanceUIDs, seriesPaths)

# Save attributes
if len(dicomFilePaths) > 0:
data_dir = os.path.join(IMAGE_FOLDER_PATH, "data")
if not os.path.exists(data_dir):
os.makedirs(data_dir)
with open(os.path.join(IMAGE_FOLDER_PATH, ATTRIBUTE_FILE_NAME), 'w', encoding='UTF8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=dicomFileTraits[0].keys())
writer.writeheader()
writer.writerows(dicomFileTraits)

if SAVE_JSON:
with open(os.path.join(IMAGE_FOLDER_PATH, HEADERS_FILE_NAME), 'w') as f:
json.dump(dicomFileHeaders, f)

# Convert PET series to NIFTI
for i in range(len(dicomFileDirs)):
if i % 10 == 0 or i == len(dicomFileDirs)-1:
print(f'Converting PET series to NIFTI {round((i+1)/len(dicomFileDirs)*100, 2)}%')
if len(dicomFilePaths[i]) > 1:
file = pydicom.dcmread(dicomFilePaths[i][0], force=True)
fileModality = getattr(file, 'Modality', None)
if fileModality == 'PT':
dicomToNifti(file, dicomFileDirs[i])

# Convert RTSTRUCT to NIFTI masks
for i in range(min([len(labelPaths), len(seriesPaths)])):
if i % 10 == 0 or i == len(dicomFileDirs)-1:
print(f'Converting RTSTRUCT to NIFTI masks {round((i+1)/min([len(labelPaths), len(seriesPaths)])*100, 2)}%')
file_label = pydicom.dcmread(labelPaths[i], force=True)
if len(labelInstanceUIDs) != len(seriesInstanceUIDs):
# Need to match label's UID to a series UID
j = 0
if len(labelInstanceUIDs) < len(seriesInstanceUIDs):
while (i + j) < len(seriesInstanceUIDs) and labelInstanceUIDs[i] != seriesInstanceUIDs[i+j]:
j += 1
try:
buildMasks(file_label, seriesPaths[i+j], labelPaths[i])
except:
print('Failed to build mask for label: ', labelPaths[i])
else:
while (i + j) < len(labelInstanceUIDs) and seriesInstanceUIDs[i] != labelInstanceUIDs[i+j]:
j += 1
try:
buildMasks(pydicom.dcmread(labelPaths[i+j], force=True), seriesPaths[i], labelPaths[i+j])
except:
print('Failed to build mask for label: ', labelPaths[i+j])
else:
try:
buildMasks(file_label, seriesPaths[i], labelPaths[i])
except:
print('Failed to build mask for label: ', labelPaths[i])

print('Done! Created NIFTI files in the NIFTI folder inside each patient directory.')

if __name__ == '__main__':
convertFiles()
Loading
Loading