pydicomrt 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydicomrt/__init__.py +2 -0
- pydicomrt/ct/ct_ds_iod.py +40 -0
- pydicomrt/dose/__init__.py +0 -0
- pydicomrt/dose/builder.py +156 -0
- pydicomrt/dose/dose_ds_iod.py +42 -0
- pydicomrt/reg/__init__.py +0 -0
- pydicomrt/reg/builder.py +21 -0
- pydicomrt/reg/check.py +30 -0
- pydicomrt/reg/ds_reg_ds_iod.py +40 -0
- pydicomrt/reg/parser.py +29 -0
- pydicomrt/reg/s_reg_ds_iod.py +38 -0
- pydicomrt/reg/sitk_reg_method.py +141 -0
- pydicomrt/rs/__init__.py +46 -0
- pydicomrt/rs/add_new_roi.py +50 -0
- pydicomrt/rs/builder.py +130 -0
- pydicomrt/rs/checker.py +79 -0
- pydicomrt/rs/contour_process_method.py +218 -0
- pydicomrt/rs/make_contour_sequence.py +147 -0
- pydicomrt/rs/packer.py +165 -0
- pydicomrt/rs/parser.py +76 -0
- pydicomrt/rs/rs_ds_iod.py +63 -0
- pydicomrt/rs/rs_to_volume.py +362 -0
- pydicomrt/utils/__init__.py +12 -0
- pydicomrt/utils/coordinate_transform.py +82 -0
- pydicomrt/utils/image_series_loader.py +75 -0
- pydicomrt/utils/sitk_transform.py +150 -0
- pydicomrt/utils/validate_dcm_info.py +122 -0
- pydicomrt-0.4.0.dist-info/METADATA +125 -0
- pydicomrt-0.4.0.dist-info/RECORD +32 -0
- pydicomrt-0.4.0.dist-info/WHEEL +4 -0
- pydicomrt-0.4.0.dist-info/entry_points.txt +2 -0
- pydicomrt-0.4.0.dist-info/licenses/LICENSE +21 -0
pydicomrt/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from pydicom.uid import UID
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
CT_IMAGE_IOD = {
|
|
5
|
+
"PatientID": {},
|
|
6
|
+
"PatientName": {},
|
|
7
|
+
"PatientBirthDate": {},
|
|
8
|
+
"PatientSex": {},
|
|
9
|
+
"StudyInstanceUID": {},
|
|
10
|
+
"StudyID": {},
|
|
11
|
+
"StudyDate": {},
|
|
12
|
+
"StudyTime": {},
|
|
13
|
+
"AccessionNumber": {},
|
|
14
|
+
"SeriesInstanceUID": {"type": UID},
|
|
15
|
+
"SeriesNumber": {},
|
|
16
|
+
"Modality": {"value": "CT"},
|
|
17
|
+
"Manufacturer": {},
|
|
18
|
+
"FrameOfReferenceUID": {"type": UID},
|
|
19
|
+
"SOPClassUID": {"type": UID},
|
|
20
|
+
"SOPInstanceUID": {"type": UID},
|
|
21
|
+
"InstanceNumber": {},
|
|
22
|
+
|
|
23
|
+
"SliceThickness": {},
|
|
24
|
+
"ImagePositionPatient": {},
|
|
25
|
+
"ImageOrientationPatient": {},
|
|
26
|
+
"PixelSpacing": {},
|
|
27
|
+
|
|
28
|
+
"Rows": {},
|
|
29
|
+
"Columns": {},
|
|
30
|
+
|
|
31
|
+
"ImageType": {},
|
|
32
|
+
"BitsAllocated": {},
|
|
33
|
+
"BitsStored": {},
|
|
34
|
+
"HighBit": {},
|
|
35
|
+
"RescaleIntercept": {},
|
|
36
|
+
"RescaleSlope": {},
|
|
37
|
+
|
|
38
|
+
"PixelData": {},
|
|
39
|
+
|
|
40
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import SimpleITK as sitk
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pydicom.tag import Tag
|
|
7
|
+
from pydicom.dataset import Dataset, FileDataset, FileMetaDataset
|
|
8
|
+
from pydicom.sequence import Sequence
|
|
9
|
+
from pydicom.uid import generate_uid
|
|
10
|
+
from pydicom.uid import ImplicitVRLittleEndian
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DICOM_UID_PREFIX = os.getenv('DICOM_UID_PREFIX', "1.2.826.0.1.3680043.8.498.") # PYDICOM_ROOT_UID
|
|
14
|
+
|
|
15
|
+
def generate_base_dataset() -> FileDataset:
|
|
16
|
+
file_name = "bear_dose"
|
|
17
|
+
file_meta = get_file_meta()
|
|
18
|
+
ds = FileDataset(file_name, {}, file_meta=file_meta, preamble=b"\0" * 128)
|
|
19
|
+
add_required_elements_to_ds(ds)
|
|
20
|
+
add_sequence_lists_to_ds(ds)
|
|
21
|
+
return ds
|
|
22
|
+
|
|
23
|
+
def get_file_meta() -> FileMetaDataset:
|
|
24
|
+
file_meta = FileMetaDataset()
|
|
25
|
+
file_meta.FileMetaInformationGroupLength = 190
|
|
26
|
+
file_meta.FileMetaInformationVersion = b"\x00\x01"
|
|
27
|
+
file_meta.TransferSyntaxUID = ImplicitVRLittleEndian
|
|
28
|
+
file_meta.MediaStorageSOPClassUID = "1.2.840.10008.5.1.4.1.1.481.2"
|
|
29
|
+
file_meta.MediaStorageSOPInstanceUID = (
|
|
30
|
+
generate_uid(prefix=DICOM_UID_PREFIX)
|
|
31
|
+
)
|
|
32
|
+
file_meta.ImplementationClassUID = DICOM_UID_PREFIX + "1"
|
|
33
|
+
# TODO file_meta.ImplementationVersionName = ""
|
|
34
|
+
return file_meta
|
|
35
|
+
|
|
36
|
+
def add_required_elements_to_ds(ds: FileDataset):
|
|
37
|
+
dt = datetime.now()
|
|
38
|
+
# Append data elements required by the DICOM standarad
|
|
39
|
+
ds.SpecificCharacterSet = "ISO_IR 192"
|
|
40
|
+
ds.InstanceCreationDate = dt.strftime("%Y%m%d")
|
|
41
|
+
ds.InstanceCreationTime = dt.strftime("%H%M%S")
|
|
42
|
+
ds.Modality = "RTDOSE"
|
|
43
|
+
ds.Manufacturer = "higumalu"
|
|
44
|
+
ds.ManufacturerModelName = "modelv1"
|
|
45
|
+
ds.InstitutionName = "higumalu"
|
|
46
|
+
# Set the transfer syntax
|
|
47
|
+
ds.is_little_endian = True
|
|
48
|
+
ds.is_implicit_VR = True
|
|
49
|
+
# Set values already defined in the file meta
|
|
50
|
+
ds.SOPClassUID = ds.file_meta.MediaStorageSOPClassUID
|
|
51
|
+
ds.SOPInstanceUID = ds.file_meta.MediaStorageSOPInstanceUID
|
|
52
|
+
|
|
53
|
+
ds.ApprovalStatus = "UNAPPROVED"
|
|
54
|
+
|
|
55
|
+
def add_sequence_lists_to_ds(ds: FileDataset):
|
|
56
|
+
ds.ReferencedRTPlanSequence = Sequence()
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------------------- #
|
|
59
|
+
|
|
60
|
+
def add_patient_information(ds: FileDataset, reference_ds: Dataset):
|
|
61
|
+
ds.PatientName = getattr(reference_ds, "PatientName", "Unknown")
|
|
62
|
+
ds.PatientID = getattr(reference_ds, "PatientID", "Unknown")
|
|
63
|
+
ds.PatientBirthDate = getattr(reference_ds, "PatientBirthDate", "")
|
|
64
|
+
ds.PatientSex = getattr(reference_ds, "PatientSex", "")
|
|
65
|
+
ds.PatientAge = getattr(reference_ds, "PatientAge", "")
|
|
66
|
+
ds.PatientSize = getattr(reference_ds, "PatientSize", "")
|
|
67
|
+
ds.PatientWeight = getattr(reference_ds, "PatientWeight", "")
|
|
68
|
+
return ds
|
|
69
|
+
|
|
70
|
+
def add_study_information(ds: FileDataset, reference_ds: Dataset):
|
|
71
|
+
dt = datetime.now()
|
|
72
|
+
ds.StudyInstanceUID = getattr(reference_ds, "StudyInstanceUID", generate_uid(prefix=DICOM_UID_PREFIX))
|
|
73
|
+
ds.StudyDate = getattr(reference_ds, "StudyDate", dt.strftime("%Y%m%d"))
|
|
74
|
+
ds.StudyTime = getattr(reference_ds, "StudyTime", dt.strftime("%H%M%S"))
|
|
75
|
+
ds.StudyID = getattr(reference_ds, "StudyID", "")
|
|
76
|
+
ds.AccessionNumber = getattr(reference_ds, "AccessionNumber", "")
|
|
77
|
+
ds.ReferringPhysicianName = getattr(reference_ds, "ReferringPhysicianName", "")
|
|
78
|
+
return ds
|
|
79
|
+
|
|
80
|
+
def add_series_information(ds: FileDataset, reference_ds: Dataset, series_number: int = 1):
|
|
81
|
+
dt = datetime.now()
|
|
82
|
+
ds.Modality = "RTDOSE"
|
|
83
|
+
ds.SeriesInstanceUID = generate_uid(prefix=DICOM_UID_PREFIX)
|
|
84
|
+
ds.SeriesDate = dt.strftime("%Y%m%d")
|
|
85
|
+
ds.SeriesTime = dt.strftime("%H%M%S")
|
|
86
|
+
ds.SeriesDescription = getattr(reference_ds, "SeriesDescription", "") + "_dose" + dt.strftime("%Y%m%d%H%M%S")
|
|
87
|
+
ds.SeriesNumber = getattr(reference_ds, "SeriesNumber", series_number)
|
|
88
|
+
ds.OperatorsName = getattr(reference_ds, "OperatorsName", "")
|
|
89
|
+
return ds
|
|
90
|
+
|
|
91
|
+
def add_frame_of_reference_information(ds: FileDataset, reference_ds: Dataset):
|
|
92
|
+
ds.FrameOfReferenceUID = getattr(reference_ds, "FrameOfReferenceUID", generate_uid(prefix=DICOM_UID_PREFIX))
|
|
93
|
+
ds.PositionReferenceIndicator = getattr(reference_ds, "PositionReferenceIndicator", "")
|
|
94
|
+
return ds
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------------------- #
|
|
97
|
+
|
|
98
|
+
def cp_information_from_ds(
|
|
99
|
+
ds: FileDataset,
|
|
100
|
+
reference_ds: Dataset,
|
|
101
|
+
):
|
|
102
|
+
ds = add_patient_information(ds, reference_ds)
|
|
103
|
+
ds = add_study_information(ds, reference_ds)
|
|
104
|
+
ds = add_series_information(ds, reference_ds)
|
|
105
|
+
ds = add_frame_of_reference_information(ds, reference_ds)
|
|
106
|
+
return ds
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------------------- #
|
|
109
|
+
|
|
110
|
+
def add_dose_grid_to_ds(
|
|
111
|
+
ds: FileDataset,
|
|
112
|
+
dose_sitk_image: sitk.Image,
|
|
113
|
+
):
|
|
114
|
+
scaling = 1e-7
|
|
115
|
+
array = sitk.GetArrayFromImage(dose_sitk_image)
|
|
116
|
+
size = list(dose_sitk_image.GetSize())
|
|
117
|
+
spacing = list(dose_sitk_image.GetSpacing())
|
|
118
|
+
direction = list(dose_sitk_image.GetDirection())
|
|
119
|
+
origin = list(dose_sitk_image.GetOrigin())
|
|
120
|
+
|
|
121
|
+
ds.SamplesPerPixel = 1
|
|
122
|
+
ds.PhotometricInterpretation = "MONOCHROME2"
|
|
123
|
+
ds.BitsAllocated = 32
|
|
124
|
+
ds.BitsStored = 32
|
|
125
|
+
ds.HighBit = 31
|
|
126
|
+
ds.PixelRepresentation = True
|
|
127
|
+
ds.DoseUnits = "GY"
|
|
128
|
+
ds.DoseType = "EFFECTIVE"
|
|
129
|
+
ds.DoseSummationType = "PLAN"
|
|
130
|
+
ds.NumberOfFrames = size[2]
|
|
131
|
+
ds.FrameIncrementPointer = Tag(0x3004, 0x000C)
|
|
132
|
+
|
|
133
|
+
ds.ImagePositionPatient = origin
|
|
134
|
+
ds.ImageOrientationPatient = direction
|
|
135
|
+
ds.PixelSpacing = spacing[0:2]
|
|
136
|
+
ds.SliceThickness = spacing[2]
|
|
137
|
+
ds.Columns = size[0]
|
|
138
|
+
ds.Rows = size[1]
|
|
139
|
+
ds.GridFrameOffsetVector = [ov * spacing[2] for ov in range(size[2])]
|
|
140
|
+
ds.DoseGridScaling = scaling
|
|
141
|
+
ds.PixelData = (array // scaling).astype(np.uint32).tobytes()
|
|
142
|
+
return ds
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# -------------------------------- example --------------------------------------------------- #
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
import pydicom
|
|
149
|
+
import SimpleITK as sitk
|
|
150
|
+
dose_sitk_image = sitk.ReadImage("example/data/DF_001/RD_003/CT_001_dose.dcm")
|
|
151
|
+
ds = pydicom.dcmread("example/data/DF_001/RD_003/CT_001.dcm")
|
|
152
|
+
|
|
153
|
+
dose_ds = generate_base_dataset()
|
|
154
|
+
dose_ds = cp_information_from_ds(dose_ds, ds)
|
|
155
|
+
dose_ds = add_dose_grid_to_ds(dose_ds, dose_sitk_image)
|
|
156
|
+
dose_ds.save_as("example/data/DF_001/RD_003/dose.dcm")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from pydicom.uid import UID
|
|
2
|
+
|
|
3
|
+
# RT Dose CIOD
|
|
4
|
+
RT_DOSE_IOD = {
|
|
5
|
+
"PatientID": {},
|
|
6
|
+
"PatientName": {},
|
|
7
|
+
"PatientBirthDate": {},
|
|
8
|
+
"PatientSex": {},
|
|
9
|
+
"StudyInstanceUID": {},
|
|
10
|
+
"StudyID": {},
|
|
11
|
+
"StudyDate": {},
|
|
12
|
+
"StudyTime": {},
|
|
13
|
+
"AccessionNumber": {},
|
|
14
|
+
"SeriesInstanceUID": {"type": UID},
|
|
15
|
+
"SeriesNumber": {},
|
|
16
|
+
"Modality": {"value": "RTDOSE"},
|
|
17
|
+
"Manufacturer": {},
|
|
18
|
+
"FrameOfReferenceUID": {"type": UID},
|
|
19
|
+
"SOPClassUID": {"type": UID},
|
|
20
|
+
"SOPInstanceUID": {"type": UID},
|
|
21
|
+
"InstanceNumber": {},
|
|
22
|
+
|
|
23
|
+
"NumberOfFrames": {},
|
|
24
|
+
"Rows": {},
|
|
25
|
+
"Columns": {},
|
|
26
|
+
"PixelSpacing": {},
|
|
27
|
+
"ImageOrientationPatient": {},
|
|
28
|
+
"ImagePositionPatient": {}, # if value == (3004,000C) GridFrameOffsetVector is required
|
|
29
|
+
"SliceThickness": {"optional": True},
|
|
30
|
+
|
|
31
|
+
"BitsAllocated": {},
|
|
32
|
+
"BitsStored": {},
|
|
33
|
+
"HighBit": {},
|
|
34
|
+
"PixelRepresentation": {},
|
|
35
|
+
|
|
36
|
+
"DoseUnits": {"value": "GY"},
|
|
37
|
+
"DoseType": {},
|
|
38
|
+
"DoseSummationType": {"value": "PLAN"},
|
|
39
|
+
"GridFrameOffsetVector": {},
|
|
40
|
+
|
|
41
|
+
"PixelData": {},
|
|
42
|
+
}
|
|
File without changes
|
pydicomrt/reg/builder.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class SpatialRegistrationBuilder:
|
|
4
|
+
def __init__(self, fixed_ds_list, moving_ds_list, transform_matrix):
|
|
5
|
+
self.fixed_ds_list = fixed_ds_list
|
|
6
|
+
self.moving_ds_list = moving_ds_list
|
|
7
|
+
self.transform_matrix = transform_matrix
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeformableSpatialRegistrationBuilder:
|
|
11
|
+
def __init__(
|
|
12
|
+
self, fixed_ds_list, moving_ds_list,
|
|
13
|
+
pre_transform,
|
|
14
|
+
vectorial_field,
|
|
15
|
+
post_transform
|
|
16
|
+
):
|
|
17
|
+
self.fixed_ds_list = fixed_ds_list
|
|
18
|
+
self.moving_ds_list = moving_ds_list
|
|
19
|
+
self.pre_transform = pre_transform
|
|
20
|
+
self.vectorial_field = vectorial_field
|
|
21
|
+
self.post_transform = post_transform
|
pydicomrt/reg/check.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from pydicom.dataset import Dataset
|
|
2
|
+
from .ds_reg_ds_iod import DEFORMABLE_SPATIAL_REGSITRATION_IOD
|
|
3
|
+
from .s_reg_ds_iod import SPATIAL_REGSITRATION_IOD
|
|
4
|
+
from pydicomrt.utils.validate_dcm_info import check_iod
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def check_ds_reg_iod(rs_ds: Dataset) -> dict:
|
|
8
|
+
result_dict = {
|
|
9
|
+
"result": True,
|
|
10
|
+
"content": []
|
|
11
|
+
}
|
|
12
|
+
failed_item_list = []
|
|
13
|
+
failed_item_list = check_iod(ds=rs_ds, config_map=DEFORMABLE_SPATIAL_REGSITRATION_IOD)
|
|
14
|
+
if len(failed_item_list) > 0:
|
|
15
|
+
result_dict["result"] = False
|
|
16
|
+
result_dict["content"] = failed_item_list
|
|
17
|
+
return result_dict
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_s_reg_iod(rs_ds: Dataset) -> dict:
|
|
21
|
+
result_dict = {
|
|
22
|
+
"result": True,
|
|
23
|
+
"content": []
|
|
24
|
+
}
|
|
25
|
+
failed_item_list = []
|
|
26
|
+
failed_item_list = check_iod(ds=rs_ds, config_map=SPATIAL_REGSITRATION_IOD)
|
|
27
|
+
if len(failed_item_list) > 0:
|
|
28
|
+
result_dict["result"] = False
|
|
29
|
+
result_dict["content"] = failed_item_list
|
|
30
|
+
return result_dict
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from pydicom.uid import UID
|
|
2
|
+
|
|
3
|
+
# Deformable Spatial Registration CIOD
|
|
4
|
+
DEFORMABLE_SPATIAL_REGSITRATION_IOD = {
|
|
5
|
+
"PatientID": {},
|
|
6
|
+
"PatientName": {},
|
|
7
|
+
"PatientBirthDate": {},
|
|
8
|
+
"PatientSex": {},
|
|
9
|
+
"StudyInstanceUID": {},
|
|
10
|
+
"StudyID": {},
|
|
11
|
+
"StudyDate": {},
|
|
12
|
+
"StudyTime": {},
|
|
13
|
+
"AccessionNumber": {},
|
|
14
|
+
"SeriesInstanceUID": {"type": UID},
|
|
15
|
+
"SeriesNumber": {},
|
|
16
|
+
"Modality": {"value": "REG"},
|
|
17
|
+
"Manufacturer": {},
|
|
18
|
+
"FrameOfReferenceUID": {"type": UID},
|
|
19
|
+
"SOPClassUID": {"type": UID},
|
|
20
|
+
"SOPInstanceUID": {"type": UID},
|
|
21
|
+
"InstanceNumber": {},
|
|
22
|
+
"ContentLabel": {},
|
|
23
|
+
"ContentDescription": {},
|
|
24
|
+
"DeformableRegistrationSequence": {
|
|
25
|
+
"SourceFrameOfReferenceUID": {"type": UID},
|
|
26
|
+
"submap": {
|
|
27
|
+
"DeformableRegistrationGridSequence": {
|
|
28
|
+
"ImagePositionPatient": {},
|
|
29
|
+
"ImageOrientationPatient": {},
|
|
30
|
+
"GridDimensions": {},
|
|
31
|
+
"GridResolution": {},
|
|
32
|
+
"VectorGridData": {}
|
|
33
|
+
},
|
|
34
|
+
"PreDeformationMatrixRegistrationSequence": {},
|
|
35
|
+
"PostDeformationMatrixRegistrationSequence": {},
|
|
36
|
+
"RegistrationTypeCodeSequence": {},
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
}
|
pydicomrt/reg/parser.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
import numpy as np
|
|
3
|
+
from struct import unpack
|
|
4
|
+
|
|
5
|
+
from pydicom.dataset import Dataset
|
|
6
|
+
|
|
7
|
+
def get_deformable_reg_list(reg_ds: Dataset) -> list:
|
|
8
|
+
reg_dict_list = []
|
|
9
|
+
for reg in reg_ds.DeformableRegistrationSequence:
|
|
10
|
+
reg_dict = {}
|
|
11
|
+
reg_dict['SourceFrameOfReferenceUID'] = reg.SourceFrameOfReferenceUID
|
|
12
|
+
reg_dict['PreDeformationMatrixRegistration'] = reg.PreDeformationMatrixRegistrationSequence[-1].FrameOfReferenceTransformationMatrix
|
|
13
|
+
reg_dict['PostDeformationMatrixRegistration'] = reg.PostDeformationMatrixRegistrationSequence[-1].FrameOfReferenceTransformationMatrix
|
|
14
|
+
reg_dict['DeformableRegistrationGrid'] = {}
|
|
15
|
+
deformed_grid_data = reg.DeformableRegistrationGridSequence[-1]
|
|
16
|
+
|
|
17
|
+
reg_dict['DeformableRegistrationGrid']['GridDimensions'] = deformed_grid_data.GridDimensions
|
|
18
|
+
reg_dict['DeformableRegistrationGrid']['GridResolution'] = deformed_grid_data.GridResolution
|
|
19
|
+
reg_dict['DeformableRegistrationGrid']['ImagePositionPatient'] = deformed_grid_data.ImagePositionPatient
|
|
20
|
+
|
|
21
|
+
grid_dim = deformed_grid_data.GridDimensions
|
|
22
|
+
vector_grid_data = deformed_grid_data.VectorGridData
|
|
23
|
+
vector_grid_data = unpack(f"<{len(vector_grid_data) // 4}f", vector_grid_data)
|
|
24
|
+
deformed_array = np.reshape(vector_grid_data, grid_dim[::-1] + [3,])
|
|
25
|
+
|
|
26
|
+
reg_dict['DeformableRegistrationGrid']['VectorGridData'] = deformed_array
|
|
27
|
+
reg_dict_list.append(reg_dict)
|
|
28
|
+
|
|
29
|
+
return reg_dict_list
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from pydicom.uid import UID
|
|
2
|
+
|
|
3
|
+
# Spatial Registration CIOD
|
|
4
|
+
SPATIAL_REGSITRATION_IOD = {
|
|
5
|
+
"PatientID": {},
|
|
6
|
+
"PatientName": {},
|
|
7
|
+
"PatientBirthDate": {},
|
|
8
|
+
"PatientSex": {},
|
|
9
|
+
"StudyInstanceUID": {},
|
|
10
|
+
"StudyID": {},
|
|
11
|
+
"StudyDate": {},
|
|
12
|
+
"StudyTime": {},
|
|
13
|
+
"AccessionNumber": {},
|
|
14
|
+
"SeriesInstanceUID": {"type": UID},
|
|
15
|
+
"SeriesNumber": {},
|
|
16
|
+
"Modality": {"value": "REG"},
|
|
17
|
+
"Manufacturer": {},
|
|
18
|
+
"FrameOfReferenceUID": {"type": UID},
|
|
19
|
+
"SOPClassUID": {"type": UID},
|
|
20
|
+
"SOPInstanceUID": {"type": UID},
|
|
21
|
+
"InstanceNumber": {},
|
|
22
|
+
"ContentLabel": {},
|
|
23
|
+
"ContentDescription": {},
|
|
24
|
+
"RegistrationSequence": {
|
|
25
|
+
"FrameOfReferenceUID": {"type": UID},
|
|
26
|
+
"submap": {
|
|
27
|
+
"MatrixRegistrationSequence": {
|
|
28
|
+
"submap": {
|
|
29
|
+
"MatrixSequence": {
|
|
30
|
+
"FrameOfReferenceTransformationMatrixType": {},
|
|
31
|
+
"FrameOfReferenceTransformationMatrix": {},
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import SimpleITK as sitk
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def rigid_registration(
|
|
6
|
+
fixed_image: sitk.Image,
|
|
7
|
+
moving_image: sitk.Image,
|
|
8
|
+
histogram_bins: int = 50,
|
|
9
|
+
learning_rate: float = 1.0,
|
|
10
|
+
iterations: int = 100,
|
|
11
|
+
convergence_minimum_value: float = 1e-6,
|
|
12
|
+
convergence_window_size: int = 10,
|
|
13
|
+
) -> sitk.Transform:
|
|
14
|
+
"""
|
|
15
|
+
Perform rigid registration (translation and rotation only) between two 3D SimpleITK images and return an affine transform.
|
|
16
|
+
Args:
|
|
17
|
+
fixed_image (sitk.Image)
|
|
18
|
+
moving_image (sitk.Image)
|
|
19
|
+
histogram_bins (int)
|
|
20
|
+
learning_rate (float)
|
|
21
|
+
iterations (int)
|
|
22
|
+
convergence_minimum_value (float)
|
|
23
|
+
convergence_window_size (int)
|
|
24
|
+
Returns:
|
|
25
|
+
sitk.Transform: The resulting affine transform (containing only rotation + translation)
|
|
26
|
+
"""
|
|
27
|
+
fixed_image = sitk.Cast(fixed_image, sitk.sitkFloat32)
|
|
28
|
+
moving_image = sitk.Cast(moving_image, sitk.sitkFloat32)
|
|
29
|
+
|
|
30
|
+
registration_method = sitk.ImageRegistrationMethod()
|
|
31
|
+
registration_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=histogram_bins)
|
|
32
|
+
registration_method.SetInterpolator(sitk.sitkLinear)
|
|
33
|
+
registration_method.SetOptimizerAsGradientDescent(
|
|
34
|
+
learningRate=learning_rate,
|
|
35
|
+
numberOfIterations=iterations,
|
|
36
|
+
convergenceMinimumValue=convergence_minimum_value,
|
|
37
|
+
convergenceWindowSize=convergence_window_size,
|
|
38
|
+
)
|
|
39
|
+
registration_method.SetOptimizerScalesFromPhysicalShift()
|
|
40
|
+
initial_transform = sitk.CenteredTransformInitializer(
|
|
41
|
+
fixed_image,
|
|
42
|
+
moving_image,
|
|
43
|
+
sitk.VersorRigid3DTransform(),
|
|
44
|
+
sitk.CenteredTransformInitializerFilter.GEOMETRY
|
|
45
|
+
)
|
|
46
|
+
initial_transform.SetCenter((0.0, 0.0, 0.0))
|
|
47
|
+
registration_method.SetInitialTransform(initial_transform, inPlace=False)
|
|
48
|
+
final_transform = registration_method.Execute(fixed_image, moving_image)
|
|
49
|
+
if isinstance(final_transform, sitk.CompositeTransform):
|
|
50
|
+
transform = final_transform.GetNthTransform(0)
|
|
51
|
+
else:
|
|
52
|
+
transform = final_transform
|
|
53
|
+
affine_transform = sitk.AffineTransform(3)
|
|
54
|
+
affine_transform.SetMatrix(transform.GetMatrix())
|
|
55
|
+
affine_transform.SetTranslation(transform.GetTranslation())
|
|
56
|
+
return affine_transform
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def deformable_registration(fixed_image, moving_image, mesh_size=8):
|
|
60
|
+
"""
|
|
61
|
+
Perform deformable registration and output displacement field (numpy array with spatial information) and SimpleITK transform object.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
fixed_image (sitk.Image): Fixed image (reference)
|
|
65
|
+
moving_image (sitk.Image): Moving image (to be registered)
|
|
66
|
+
mesh_size (int or list): B-spline mesh size (default 8, can be adjusted based on image size)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
dfield_transform (sitk.DisplacementFieldTransform): SimpleITK displacement transform
|
|
70
|
+
"""
|
|
71
|
+
if isinstance(mesh_size, int):
|
|
72
|
+
mesh_size = [mesh_size] * fixed_image.GetDimension()
|
|
73
|
+
tx = sitk.BSplineTransformInitializer(fixed_image, mesh_size)
|
|
74
|
+
|
|
75
|
+
registration_method = sitk.ImageRegistrationMethod()
|
|
76
|
+
registration_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=100)
|
|
77
|
+
registration_method.SetOptimizerAsGradientDescent(
|
|
78
|
+
learningRate=1.0,
|
|
79
|
+
numberOfIterations=50,
|
|
80
|
+
convergenceMinimumValue=1e-6,
|
|
81
|
+
convergenceWindowSize=10
|
|
82
|
+
)
|
|
83
|
+
registration_method.SetInterpolator(sitk.sitkLinear)
|
|
84
|
+
registration_method.SetInitialTransform(tx, inPlace=False)
|
|
85
|
+
# registration_method.SetShrinkFactorsPerLevel(shrinkFactors=[4, 2, 1]) # 4 2 1
|
|
86
|
+
# registration_method.SetSmoothingSigmasPerLevel(smoothingSigmas=[2, 1, 0]) # 2 1 0
|
|
87
|
+
# registration_method.SmoothingSigmasAreSpecifiedInPhysicalUnitsOn()
|
|
88
|
+
|
|
89
|
+
outTx = registration_method.Execute(fixed_image, moving_image)
|
|
90
|
+
|
|
91
|
+
displacement_field = sitk.TransformToDisplacementField(
|
|
92
|
+
outTx,
|
|
93
|
+
sitk.sitkVectorFloat64,
|
|
94
|
+
fixed_image.GetSize(),
|
|
95
|
+
fixed_image.GetOrigin(),
|
|
96
|
+
fixed_image.GetSpacing(),
|
|
97
|
+
fixed_image.GetDirection()
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
dfield_transform = sitk.DisplacementFieldTransform(displacement_field)
|
|
101
|
+
return dfield_transform
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def affine_to_homogeneous_matrix(transform: sitk.AffineTransform) -> np.ndarray:
|
|
105
|
+
"""
|
|
106
|
+
Convert a SimpleITK AffineTransform to a 4x4 homogeneous transformation matrix.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
transform (sitk.AffineTransform): The affine transform to convert.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
np.ndarray: 4x4 homogeneous matrix (dtype float64).
|
|
113
|
+
"""
|
|
114
|
+
matrix3x3 = np.array(transform.GetMatrix()).reshape((3, 3))
|
|
115
|
+
translation = np.array(transform.GetTranslation())
|
|
116
|
+
hom_mat = np.eye(4)
|
|
117
|
+
hom_mat[:3, :3] = matrix3x3
|
|
118
|
+
hom_mat[:3, 3] = translation
|
|
119
|
+
return hom_mat
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def displacement_field_to_dict(transform: sitk.DisplacementFieldTransform) -> dict:
|
|
123
|
+
"""
|
|
124
|
+
Convert a SimpleITK DisplacementFieldTransform to a dictionary.
|
|
125
|
+
Args:
|
|
126
|
+
transform (sitk.DisplacementFieldTransform): The displacement field transform to convert.
|
|
127
|
+
Returns:
|
|
128
|
+
dict: Dictionary containing displacement field information.
|
|
129
|
+
"""
|
|
130
|
+
displacement_field = sitk.GetArrayFromImage(transform.GetDisplacementField())
|
|
131
|
+
origin = transform.GetOrigin()
|
|
132
|
+
spacing = transform.GetSpacing()
|
|
133
|
+
direction = transform.GetDirection()
|
|
134
|
+
size = transform.GetSize()
|
|
135
|
+
return {
|
|
136
|
+
"vector_grid": displacement_field,
|
|
137
|
+
"origin": origin,
|
|
138
|
+
"spacing": spacing,
|
|
139
|
+
"direction": direction,
|
|
140
|
+
"size": size,
|
|
141
|
+
}
|
pydicomrt/rs/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Radiation Therapy Structure Set module"""
|
|
2
|
+
|
|
3
|
+
from .builder import create_rtstruct_dataset
|
|
4
|
+
|
|
5
|
+
from .add_new_roi import create_roi_into_rs_ds
|
|
6
|
+
|
|
7
|
+
from .make_contour_sequence import (
|
|
8
|
+
add_contour_sequence_from_mask3d,
|
|
9
|
+
add_contour_sequence_from_dcm_ctr_dict
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .parser import (
|
|
13
|
+
get_contour_dict,
|
|
14
|
+
get_roi_number_to_name
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .checker import (
|
|
18
|
+
is_rtstruct_matching_series,
|
|
19
|
+
check_rs_iod
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .rs_to_volume import (
|
|
23
|
+
rtstruct_to_mask_dict,
|
|
24
|
+
calc_image_series_affine_mapping,
|
|
25
|
+
calc_rs_affine_mapping
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
'create_rtstruct_dataset',
|
|
31
|
+
'create_roi_into_rs_ds',
|
|
32
|
+
|
|
33
|
+
'add_contour_sequence_from_mask3d',
|
|
34
|
+
'add_contour_sequence_from_dcm_ctr_dict',
|
|
35
|
+
|
|
36
|
+
'get_contour_dict',
|
|
37
|
+
'get_roi_number_to_name',
|
|
38
|
+
|
|
39
|
+
'is_rtstruct_matching_series',
|
|
40
|
+
'check_rs_iod',
|
|
41
|
+
|
|
42
|
+
'rtstruct_to_mask_dict',
|
|
43
|
+
'calc_image_series_affine_mapping',
|
|
44
|
+
'calc_rs_affine_mapping',
|
|
45
|
+
|
|
46
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from pydicom.dataset import Dataset
|
|
2
|
+
from pydicom.sequence import Sequence
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def create_roi_into_rs_ds(
|
|
6
|
+
rs_ds: Dataset,
|
|
7
|
+
roi_color: list,
|
|
8
|
+
roi_number: int,
|
|
9
|
+
roi_name: str,
|
|
10
|
+
roi_description: str,
|
|
11
|
+
roi_interpreted_type: str = "ORGAN",
|
|
12
|
+
) -> Dataset:
|
|
13
|
+
frame_of_ref_uid = rs_ds.FrameOfReferenceUID
|
|
14
|
+
rs_ds.StructureSetROISequence.append(create_empty_structure_set_roi(roi_number, roi_name, roi_description, frame_of_ref_uid))
|
|
15
|
+
rs_ds.ROIContourSequence.append(create_empty_roi_contour_sequence(roi_number, roi_color))
|
|
16
|
+
rs_ds.RTROIObservationsSequence.append(create_empty_rtroi_observation(roi_number, roi_interpreted_type))
|
|
17
|
+
return rs_ds
|
|
18
|
+
|
|
19
|
+
def create_empty_structure_set_roi(roi_number,
|
|
20
|
+
roi_name,
|
|
21
|
+
roi_description,
|
|
22
|
+
frame_of_ref_uid,
|
|
23
|
+
roi_generation_algorithm="AUTOMATIC",
|
|
24
|
+
roi_generation_description="KumaGenerated",
|
|
25
|
+
) -> Dataset:
|
|
26
|
+
structure_set_roi = Dataset()
|
|
27
|
+
structure_set_roi.ROINumber = roi_number
|
|
28
|
+
structure_set_roi.ROIName = roi_name
|
|
29
|
+
structure_set_roi.ROIDescription = roi_description
|
|
30
|
+
structure_set_roi.ROIGenerationAlgorithm = roi_generation_algorithm
|
|
31
|
+
structure_set_roi.ROIGenerationDescription = roi_generation_description
|
|
32
|
+
structure_set_roi.ReferencedFrameOfReferenceUID = frame_of_ref_uid
|
|
33
|
+
return structure_set_roi
|
|
34
|
+
|
|
35
|
+
def create_empty_roi_contour_sequence(roi_number, roi_color) -> Dataset:
|
|
36
|
+
roi_contour_sequence = Dataset()
|
|
37
|
+
roi_contour_sequence.ReferencedROINumber = roi_number # also in create_structure_set_roi
|
|
38
|
+
roi_contour_sequence.ROIDisplayColor = roi_color
|
|
39
|
+
roi_contour_sequence.ContourSequence = Sequence()
|
|
40
|
+
return roi_contour_sequence
|
|
41
|
+
|
|
42
|
+
def create_empty_rtroi_observation(roi_number, roi_interpreted_type='ORGAN') -> Dataset:
|
|
43
|
+
rtroi_observation = Dataset()
|
|
44
|
+
rtroi_observation.ObservationNumber = roi_number
|
|
45
|
+
rtroi_observation.ReferencedROINumber = roi_number
|
|
46
|
+
rtroi_observation.ROIObservationDescription = "Type:Soft,Range:*/*,Fill:0,Opacity:0.0,Thickness:1,LineThickness:2,read-only:false"
|
|
47
|
+
rtroi_observation.private_creators = "higumalu"
|
|
48
|
+
rtroi_observation.RTROIInterpretedType = roi_interpreted_type
|
|
49
|
+
rtroi_observation.ROIInterpreter = ""
|
|
50
|
+
return rtroi_observation
|