2023-11-22 00:24:05 +11:00
|
|
|
# ev-to-dicom
|
2024-06-01 18:19:57 +10:00
|
|
|
# Copyright © 2023–2024 Lee Yingtong Li
|
2023-11-22 00:24:05 +11:00
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
import msgpack
|
|
|
|
import pydicom
|
|
|
|
from pydicom.dataset import FileDataset, FileMetaDataset
|
|
|
|
from pydicom.encaps import encapsulate
|
|
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
from .ev_tlv import read_ev_tlv
|
|
|
|
|
|
|
|
def ev_files_to_dcm_file(study_uid, series_uid, series_number, image_number, input_filenames, output_filename):
|
|
|
|
# Read Enhanced Viewer input data
|
|
|
|
|
|
|
|
pixel_datas = []
|
|
|
|
|
|
|
|
for filename in input_filenames:
|
|
|
|
with open(filename, 'rb') as f:
|
|
|
|
data = f.read()
|
|
|
|
|
|
|
|
metadata, pixel_data = read_ev_tlv(data)
|
|
|
|
|
|
|
|
pixel_datas.append(pixel_data)
|
|
|
|
|
|
|
|
# Prepare DICOM file
|
|
|
|
|
|
|
|
file_meta = FileMetaDataset()
|
|
|
|
|
|
|
|
if metadata['metadata']['modality'] == 'US':
|
2023-11-23 21:32:21 +11:00
|
|
|
if len(pixel_datas) == 1:
|
2023-11-22 00:24:05 +11:00
|
|
|
file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.6.1' # Ultrasound Image Storage
|
|
|
|
else:
|
|
|
|
file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.3.1' # Ultrasound Multi-frame Image Storage
|
2024-06-01 18:19:57 +10:00
|
|
|
elif metadata['metadata']['modality'] == 'PX':
|
|
|
|
file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.1.1' # Digital X-Ray Image Storage - For Presentation
|
2023-11-22 00:24:05 +11:00
|
|
|
else:
|
|
|
|
raise Exception('Unknown modality {}'.format(metadata['metadata']['modality']))
|
|
|
|
|
|
|
|
file_meta.MediaStorageSOPInstanceUID = metadata['sopInstanceUid']
|
|
|
|
|
|
|
|
if metadata['imageFormat'] == 0:
|
|
|
|
file_meta.TransferSyntaxUID = '1.2.840.10008.1.2.4.91' # JPEG 2000
|
|
|
|
elif metadata['imageFormat'] == 1:
|
|
|
|
file_meta.TransferSyntaxUID = '1.2.840.10008.1.2.4.50' # JPEG
|
|
|
|
else:
|
|
|
|
raise Exception('Unknown imageFormat {}'.format(metadata['metadata']['imageFormat']))
|
|
|
|
|
|
|
|
ds = FileDataset(output_filename, {}, file_meta=file_meta, preamble=b'\0' * 128)
|
|
|
|
ds.SOPClassUID = file_meta.MediaStorageSOPClassUID
|
|
|
|
ds.SOPInstanceUID = metadata['sopInstanceUid']
|
|
|
|
|
|
|
|
ds.is_little_endian = True
|
|
|
|
ds.is_implicit_VR = False
|
|
|
|
|
|
|
|
ds.StudyInstanceUID = study_uid
|
|
|
|
ds.SeriesInstanceUID = series_uid
|
|
|
|
ds.SeriesNumber = series_number
|
|
|
|
ds.InstanceNumber = image_number
|
|
|
|
|
|
|
|
ds.PatientName = metadata['metadata']['patientName'].replace(', ', '^')
|
|
|
|
ds.PatientSex = metadata['metadata']['patientSex']
|
|
|
|
ds.SeriesDescription = metadata['metadata']['seriesDescription']
|
|
|
|
ds.Modality = metadata['metadata']['modality']
|
|
|
|
ds.PatientID = metadata['metadata']['patientId']
|
|
|
|
ds.StudyDate = datetime.strptime(metadata['metadata']['seriesDate'], '%Y-%m-%dT%H:%M:%S.%f').strftime('%Y%m%d')
|
|
|
|
ds.StudyTime = datetime.strptime(metadata['metadata']['seriesDate'], '%Y-%m-%dT%H:%M:%S.%f').strftime('%H%M%S.%f')
|
|
|
|
ds.SeriesDate = datetime.strptime(metadata['metadata']['seriesDate'], '%Y-%m-%dT%H:%M:%S.%f').strftime('%Y%m%d')
|
|
|
|
ds.SeriesTime = datetime.strptime(metadata['metadata']['seriesDate'], '%Y-%m-%dT%H:%M:%S.%f').strftime('%H%M%S.%f')
|
|
|
|
ds.StudyDescription = metadata['metadata']['studyDescription']
|
|
|
|
ds.AccessionNumber = metadata['metadata']['accessionNumber']
|
|
|
|
ds.PatientBirthDate = datetime.strptime(metadata['metadata']['patientBirthDate'], '%Y-%m-%d').strftime('%Y%m%d')
|
|
|
|
ds.InstitutionName = metadata['metadata']['institutionName']
|
|
|
|
ds.PatientAge = metadata['metadata']['patientAge']
|
|
|
|
ds.SamplesPerPixel = 3 if metadata['color'] else 1
|
|
|
|
ds.PixelRepresentation = 1 if metadata['isSigned'] else 0
|
|
|
|
#ds.BitsAllocated = metadata['bytesPerSample'] * 8 # Incorrect value!
|
|
|
|
#ds.BitsStored = metadata['bytesPerSample'] * 8
|
|
|
|
ds.Columns = metadata['columns']
|
|
|
|
ds.WindowCenter = metadata['windowCenter']
|
|
|
|
ds.LossyImageCompression = '01' if metadata['isLossyImage'] else '00'
|
|
|
|
ds.PixelSpacing = [metadata['rowPixelSpacing'], metadata['columnPixelSpacing']]
|
|
|
|
ds.WindowWidth = metadata['windowWidth']
|
|
|
|
ds.PlanarConfiguration = 1 if metadata['planarConfiguration'] else 0
|
|
|
|
ds.Rows = metadata['rows']
|
|
|
|
|
|
|
|
if metadata['color']:
|
2024-10-15 22:32:38 +11:00
|
|
|
# FIXME: Any proper way to detect this?
|
|
|
|
|
|
|
|
if file_meta.TransferSyntaxUID == '1.2.840.10008.1.2.4.50': # JPEG
|
|
|
|
ds.PhotometricInterpretation = 'YBR_FULL_422' # Specify YBR as that is how JPEG compression works, even though the JSON says RGB
|
|
|
|
else:
|
|
|
|
ds.PhotometricInterpretation = 'RGB'
|
2023-11-22 00:24:05 +11:00
|
|
|
else:
|
|
|
|
ds.PhotometricInterpretation = 'MONOCHROME2'
|
|
|
|
|
|
|
|
if len(pixel_datas) > 1:
|
|
|
|
ds.RecommendedDisplayFrameRate = round(metadata['recommendedDisplayFrameRate'])
|
|
|
|
ds.FrameTime = 1000 / metadata['recommendedDisplayFrameRate']
|
2023-11-23 21:33:56 +11:00
|
|
|
ds.FrameIncrementPointer = (0x0018, 0x1063) # Frame Time
|
2023-11-22 00:24:05 +11:00
|
|
|
ds.NumberOfFrames = len(pixel_datas)
|
|
|
|
|
|
|
|
# Store copy of raw metadata
|
|
|
|
block = ds.private_block(0x0075, 'RunasSudo ev-to-dicom', create=True)
|
2024-10-15 22:32:38 +11:00
|
|
|
block.add_new(0x01, 'UL', 0) # Data format version
|
2023-11-22 00:24:05 +11:00
|
|
|
del metadata['totalAvailableBytes']
|
|
|
|
block.add_new(0x02, 'OB', msgpack.packb(metadata))
|
|
|
|
|
|
|
|
if metadata['imageFormat'] == 0:
|
2023-11-23 21:33:21 +11:00
|
|
|
# JPEG 2000 - For some reason, the end of codestream marker is (always?) missing
|
|
|
|
ds.PixelData = encapsulate([pixel_data if pixel_data.endswith(b'\xff\xd9') else pixel_data + b'\xff\xd9' for pixel_data in pixel_datas])
|
2023-11-22 00:24:05 +11:00
|
|
|
else:
|
|
|
|
ds.PixelData = encapsulate(pixel_datas)
|
|
|
|
|
|
|
|
# Save DICOM data
|
|
|
|
|
|
|
|
ds.save_as(output_filename)
|