diff --git a/qim3d/io/load.py b/qim3d/io/load.py index 3df15287e88588928041e401595a05a04579c7ef..2a48536aabb8ddba00560389695d42391d448bae 100644 --- a/qim3d/io/load.py +++ b/qim3d/io/load.py @@ -12,16 +12,16 @@ Example: import difflib import os +import re +import struct from pathlib import Path +import dask.array as da import h5py import nibabel as nib import numpy as np import olefile -import struct -import re -import dask.array as da -from pathlib import Path +import pydicom import tifffile from PIL import Image, UnidentifiedImageError @@ -30,6 +30,7 @@ from qim3d.io.logger import log from qim3d.utils.internal_tools import sizeof, stringify_path from qim3d.utils.system import Memory + class DataLoader: """Utility class for loading data from different file formats. @@ -437,6 +438,39 @@ class DataLoader: return vol, meta_data else: return vol + + def load_dicom(self, path): + """ Load a DICOM file + + Args: + path (str): Path to file + """ + dcm_data = pydicom.dcmread(path) + + if self.return_metadata: + return dcm_data.pixel_array, dcm_data + else: + return dcm_data.pixel_array + + def load_dicom_dir(self, path): + """ Load a directory of DICOM files into a numpy 3d array + + Args: + path (str): Directory path + """ + # loop over all .dcm files in the directory + files = [f for f in os.listdir(path) if f.endswith('.dcm')] + files.sort() + # dicom_list contains the dicom objects with metadata + dicom_list = [pydicom.dcmread(os.path.join(path, f)) for f in files] + # vol contains the pixel data + vol = np.stack([dicom.pixel_array for dicom in dicom_list], axis=0) + + if self.return_metadata: + return vol, dicom_list + else: + return vol + def load(self, path): """ @@ -474,6 +508,8 @@ class DataLoader: return self.load_nifti(path) elif path.endswith((".vol",".vgi")): return self.load_vol(path) + elif path.endswith((".dcm",".DCM")): + return self.load_dicom(path) else: try: return self.load_pil(path) @@ -482,7 +518,11 @@ class DataLoader: # Load a directory elif os.path.isdir(path): - return self.load_tiff_stack(path) + # load dicom if directory contains dicom files else load tiff stack as default + if any([f.endswith('.dcm') for f in os.listdir(path)]): + return self.load_dicom_dir(path) + else: + return self.load_tiff_stack(path) # Fails else: diff --git a/qim3d/io/save.py b/qim3d/io/save.py index 04cb72d392ec5ec35a8cfd44fbc40e2285bb2583..64a6a1ca5adfd9efab8b7f8d2cb7ebb4f5b25b0f 100644 --- a/qim3d/io/save.py +++ b/qim3d/io/save.py @@ -21,13 +21,17 @@ Example: ``` """ +import datetime import os import h5py import nibabel as nib import numpy as np import PIL +import pydicom import tifffile +from pydicom.dataset import FileDataset, FileMetaDataset +from pydicom.uid import UID from qim3d.io.logger import log from qim3d.utils.internal_tools import sizeof, stringify_path @@ -179,6 +183,59 @@ class DataSaver: with h5py.File(path, "w") as f: f.create_dataset("dataset", data=data, compression="gzip" if self.compression else None) + def save_dicom(self, path, data): + """ Save data to a DICOM file to the given path. + + Args: + path (str): The path to save file to + data (numpy.ndarray): The data to be saved + """ + # based on https://pydicom.github.io/pydicom/stable/auto_examples/input_output/plot_write_dicom.html + + # Populate required values for file meta information + file_meta = FileMetaDataset() + file_meta.MediaStorageSOPClassUID = UID('1.2.840.10008.5.1.4.1.1.2') + file_meta.MediaStorageSOPInstanceUID = UID("1.2.3") + file_meta.ImplementationClassUID = UID("1.2.3.4") + + # Create the FileDataset instance (initially no data elements, but file_meta + # supplied) + ds = FileDataset(path, {}, + file_meta=file_meta, preamble=b"\0" * 128) + + ds.PatientName = "Test^Firstname" + ds.PatientID = "123456" + ds.StudyInstanceUID = "1.2.3.4.5" + ds.SamplesPerPixel = 1 + ds.PixelRepresentation = 0 + ds.BitsStored = 16 + ds.BitsAllocated = 16 + ds.PhotometricInterpretation = "MONOCHROME2" + ds.Rows = data.shape[1] + ds.Columns = data.shape[2] + ds.NumberOfFrames = data.shape[0] + # Set the transfer syntax + ds.is_little_endian = True + ds.is_implicit_VR = True + + # Set creation date/time + dt = datetime.datetime.now() + ds.ContentDate = dt.strftime('%Y%m%d') + timeStr = dt.strftime('%H%M%S.%f') # long format with micro seconds + ds.ContentTime = timeStr + # Needs to be here because of bug in pydicom + ds.file_meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian + + # Reshape the data into a 1D array and convert to uint16 + data_1d = data.ravel().astype(np.uint16) + # Convert the data to bytes + data_bytes = data_1d.tobytes() + # Add the data to the DICOM file + ds.PixelData = data_bytes + + ds.save_as(path) + + def save_PIL(self, path, data): """ Save data to a PIL file to the given path. @@ -271,6 +328,8 @@ class DataSaver: return self.save_h5(path, data) elif path.endswith((".vol",".vgi")): return self.save_vol(path, data) + elif path.endswith((".dcm",".DCM")): + return self.save_dicom(path, data) elif path.endswith((".jpeg",".jpg", ".png")): return self.save_PIL(path, data) else: diff --git a/requirements.txt b/requirements.txt index 3338fefbe0a3dbd1e5e042a7dd9c1e706deb16b7..30211e80501de6ae6e74d878cebaf520b2acdd41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ Pillow>=10.0.1, plotly>=5.14.1, scipy>=1.11.2, seaborn>=0.12.2, +pydicom>=2.4.4, setuptools>=68.0.0, tifffile>=2023.4.12, torch>=2.0.1, diff --git a/setup.py b/setup.py index 92b4f0ec7e13262cfac416ac9720bbff345f24fc..93087983974f465470edc846fa8ba8d84eb1d037 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ setup( "h5py>=3.9.0", "localthickness>=0.1.2", "matplotlib>=3.8.0", + "pydicom>=2.4.4", "monai>=1.2.0", "numpy>=1.26.0", "outputformat>=0.1.3",