cavass 1.2.7__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.
- cavass/__init__.py +0 -0
- cavass/_io.py +71 -0
- cavass/_log.py +9 -0
- cavass/constants.py +1 -0
- cavass/contrast_enhancement.py +30 -0
- cavass/converters.py +175 -0
- cavass/dicom.py +167 -0
- cavass/integration.py +145 -0
- cavass/match.py +38 -0
- cavass/nifti2dicom.py +154 -0
- cavass/ops.py +538 -0
- cavass/slice_range.py +34 -0
- cavass/utils.py +11 -0
- cavass-1.2.7.dist-info/METADATA +289 -0
- cavass-1.2.7.dist-info/RECORD +16 -0
- cavass-1.2.7.dist-info/WHEEL +4 -0
cavass/__init__.py
ADDED
|
File without changes
|
cavass/_io.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def read_mat(input_file, key="scene"):
|
|
7
|
+
from scipy.io import loadmat
|
|
8
|
+
if not os.path.isfile(input_file):
|
|
9
|
+
raise FileNotFoundError(f"Input file {input_file} not found.")
|
|
10
|
+
data = loadmat(input_file)[key]
|
|
11
|
+
return data
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def save_mat(output_file, data, key="scene"):
|
|
15
|
+
from scipy.io import savemat
|
|
16
|
+
ensure_output_file_dir_existence(output_file)
|
|
17
|
+
savemat(output_file, {key: data})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def save_nifti(output_file,
|
|
21
|
+
data,
|
|
22
|
+
voxel_spacing=None,
|
|
23
|
+
orientation="LPI"):
|
|
24
|
+
"""
|
|
25
|
+
Save image with nii format.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
output_file (str):
|
|
29
|
+
data (numpy.ndarray):
|
|
30
|
+
voxel_spacing (sequence or None, optional, default=None): `tuple(x, y, z)`. Voxel spacing of each axis. If None,
|
|
31
|
+
make `voxel_spacing` as `(1.0, 1.0, 1.0)`.
|
|
32
|
+
orientation (str, optional, default="LPI"): "LPI" | "ARI". LPI: Left-Posterior-Inferior;
|
|
33
|
+
ARI: Anterior-Right-Inferior.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
if voxel_spacing is None:
|
|
39
|
+
voxel_spacing = (1.0, 1.0, 1.0) # replace this with your desired voxel spacing in millimeters
|
|
40
|
+
|
|
41
|
+
match orientation:
|
|
42
|
+
case "LPI":
|
|
43
|
+
affine_matrix = np.diag(list(voxel_spacing) + [1.0])
|
|
44
|
+
case "ARI":
|
|
45
|
+
# calculate the affine matrix based on the desired voxel spacing and ARI orientation
|
|
46
|
+
affine_matrix = np.array([
|
|
47
|
+
[0, -voxel_spacing[0], 0, 0],
|
|
48
|
+
[-voxel_spacing[1], 0, 0, 0],
|
|
49
|
+
[0, 0, voxel_spacing[2], 0],
|
|
50
|
+
[0, 0, 0, 1]
|
|
51
|
+
])
|
|
52
|
+
case _:
|
|
53
|
+
raise ValueError(f"Unsupported orientation {orientation}.")
|
|
54
|
+
|
|
55
|
+
# create a NIfTI image object
|
|
56
|
+
import nibabel as nib
|
|
57
|
+
ensure_output_file_dir_existence(output_file)
|
|
58
|
+
nii_img = nib.Nifti1Image(data, affine=affine_matrix)
|
|
59
|
+
nib.save(nii_img, output_file)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def ensure_output_dir_existence(output_dir):
|
|
63
|
+
mk_output_dir = not os.path.exists(output_dir)
|
|
64
|
+
if mk_output_dir:
|
|
65
|
+
os.makedirs(output_dir)
|
|
66
|
+
return mk_output_dir, output_dir
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def ensure_output_file_dir_existence(output_file):
|
|
70
|
+
output_dir = os.path.split(output_file)[0]
|
|
71
|
+
return ensure_output_dir_existence(output_dir)
|
cavass/_log.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger("CAVASS")
|
|
5
|
+
logger.setLevel(logging.INFO)
|
|
6
|
+
log_format = logging.Formatter("%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s")
|
|
7
|
+
console_handler = logging.StreamHandler(stream=sys.stdout)
|
|
8
|
+
console_handler.setFormatter(log_format)
|
|
9
|
+
logger.addHandler(console_handler)
|
cavass/constants.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
CAVASS_START_INDEX = 1
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def windowing(input_data, center, window_width, invert=False):
|
|
5
|
+
min_ = (2 * center - window_width) / 2 + 0.5
|
|
6
|
+
max_ = (2 * center + window_width) / 2 + 0.5
|
|
7
|
+
factor = 255 / (max_ - min_)
|
|
8
|
+
|
|
9
|
+
data = (input_data - min_) * factor
|
|
10
|
+
|
|
11
|
+
data = np.where(data < 0, 0, data)
|
|
12
|
+
data = np.where(data > 255, 255, data)
|
|
13
|
+
data = data.astype(np.uint8)
|
|
14
|
+
|
|
15
|
+
if invert:
|
|
16
|
+
data = 255 - data
|
|
17
|
+
|
|
18
|
+
return data
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def cavass_soft_tissue_windowing(input_data):
|
|
22
|
+
return windowing(input_data, 1000, 500)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cavass_bone_windowing(input_data):
|
|
26
|
+
return windowing(input_data, 2000, 4000)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cavass_pet_windowing(input_data):
|
|
30
|
+
return windowing(input_data, 1200, 3500, invert=True)
|
cavass/converters.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from uuid import uuid1
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from cavass._io import ensure_output_file_dir_existence, save_nifti
|
|
8
|
+
from cavass.dicom import Modality
|
|
9
|
+
from cavass.nifti2dicom import nifti2dicom
|
|
10
|
+
from cavass.ops import execute_cmd, get_voxel_spacing, read_cavass_file, copy_pose
|
|
11
|
+
from cavass.utils import one_hot
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def dicom2cavass(input_dicom_dir, output_file, offset=0, copy_pose_file=None):
|
|
15
|
+
"""
|
|
16
|
+
Note that if the output file path is too long, this command may be failed.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
input_dicom_dir (str):
|
|
20
|
+
output_file (str):
|
|
21
|
+
offset (int, optional, default=0):
|
|
22
|
+
copy_pose_file (str, optional, default=None): if `copy_pose_file` is given, copy pose of this
|
|
23
|
+
file to the output file.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
if not os.path.exists(input_dicom_dir):
|
|
28
|
+
raise ValueError(f"Input DICOM series {input_dicom_dir} not found.")
|
|
29
|
+
if copy_pose_file is not None:
|
|
30
|
+
if not os.path.isfile(copy_pose_file):
|
|
31
|
+
raise FileNotFoundError(f"Copy pose file {copy_pose_file} not found.")
|
|
32
|
+
|
|
33
|
+
made_output_dir, output_dir = ensure_output_file_dir_existence(output_file)
|
|
34
|
+
|
|
35
|
+
input_dicom_dir = input_dicom_dir.replace(" ", "\ ")
|
|
36
|
+
output_file = output_file.replace(" ", "\ ")
|
|
37
|
+
try:
|
|
38
|
+
if copy_pose_file is None:
|
|
39
|
+
r = execute_cmd(f"from_dicom {input_dicom_dir}/* {output_file} +{offset}")
|
|
40
|
+
else:
|
|
41
|
+
split = os.path.splitext(output_file)
|
|
42
|
+
root = split[0]
|
|
43
|
+
extension = split[1]
|
|
44
|
+
output_tmp_file = root + "_TMP" + extension
|
|
45
|
+
r = execute_cmd(f"from_dicom {input_dicom_dir}/* {output_tmp_file} +{offset}")
|
|
46
|
+
copy_pose(output_tmp_file, copy_pose_file, output_file)
|
|
47
|
+
|
|
48
|
+
except Exception as e:
|
|
49
|
+
if made_output_dir and os.path.isdir(output_dir):
|
|
50
|
+
shutil.rmtree(output_dir)
|
|
51
|
+
|
|
52
|
+
if copy_pose_file is not None and os.path.exists(output_tmp_file):
|
|
53
|
+
os.remove(output_tmp_file)
|
|
54
|
+
|
|
55
|
+
if os.path.exists(output_file):
|
|
56
|
+
os.remove(output_file)
|
|
57
|
+
raise e
|
|
58
|
+
return r
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def nifti2cavass(input_nifti_file, output_file, modality, offset=0, copy_pose_file=None):
|
|
62
|
+
"""
|
|
63
|
+
Convert NIfTI image to cavass image.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
input_nifti_file (str):
|
|
67
|
+
output_file (str):
|
|
68
|
+
modality (Modality):
|
|
69
|
+
offset (int, optional, default=0):
|
|
70
|
+
copy_pose_file (str, optional, default=None):
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
if not os.path.isfile(input_nifti_file):
|
|
74
|
+
raise FileNotFoundError(f"Input NIfTI file {input_nifti_file} not found.")
|
|
75
|
+
|
|
76
|
+
if copy_pose_file is not None:
|
|
77
|
+
if not os.path.isfile(copy_pose_file):
|
|
78
|
+
raise FileNotFoundError(f"Copy pose file {input_nifti_file} not found.")
|
|
79
|
+
|
|
80
|
+
made_output_dir, output_dir = ensure_output_file_dir_existence(output_file)
|
|
81
|
+
|
|
82
|
+
tmp_dicom_dir = os.path.join(output_dir, f"{uuid1()}")
|
|
83
|
+
try:
|
|
84
|
+
r1 = nifti2dicom(input_nifti_file, tmp_dicom_dir, modality=modality, force_overwrite=True)
|
|
85
|
+
r2 = dicom2cavass(tmp_dicom_dir, output_file, offset, copy_pose_file)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
if made_output_dir and os.path.isdir(output_dir):
|
|
88
|
+
shutil.rmtree(output_dir)
|
|
89
|
+
if os.path.isdir(tmp_dicom_dir):
|
|
90
|
+
shutil.rmtree(tmp_dicom_dir)
|
|
91
|
+
if os.path.exists(output_file):
|
|
92
|
+
os.remove(output_file)
|
|
93
|
+
|
|
94
|
+
raise e
|
|
95
|
+
shutil.rmtree(tmp_dicom_dir)
|
|
96
|
+
return r1, r2
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def cavass2nifti(input_file, output_file, orientation="ARI"):
|
|
100
|
+
"""
|
|
101
|
+
Convert cavass IM0 and BIM formats to NIfTI.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
input_file (str):
|
|
105
|
+
output_file (str):
|
|
106
|
+
orientation (str, optional, default="ARI"): image orientation of NIfTI file, "ARI" or "LPI"
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
if not os.path.isfile(input_file):
|
|
113
|
+
raise FileNotFoundError(f"Input file {input_file} not found.")
|
|
114
|
+
|
|
115
|
+
spacing = get_voxel_spacing(input_file)
|
|
116
|
+
data = read_cavass_file(input_file)
|
|
117
|
+
save_nifti(output_file, data, spacing, orientation=orientation)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def nifti_label2cavass(input_nifti_file, output_file, objects,
|
|
121
|
+
modality=Modality.CT, discard_background=True, copy_pose_file=None):
|
|
122
|
+
"""
|
|
123
|
+
Convert NIfTI format segmentation file to cavass BIM format file. A NIfTI file in where contains arbitrary categories
|
|
124
|
+
of objects will convert to multiple CAVASS BIM files, which matches to the number of object categories.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
input_nifti_file (str):
|
|
128
|
+
output_file (str): the final saved file for category i in input segmentation will be
|
|
129
|
+
`output_file_prefix_{objects[i]}.BIM`
|
|
130
|
+
objects (sequence or str): objects is an array or a string with comma splitter of object categories,
|
|
131
|
+
where the index of the category in the array is the number that indicates the category in the segmentation.
|
|
132
|
+
modality (Modality, optional, default=Modality.CT):
|
|
133
|
+
discard_background (bool, optional, default True): if True, the regions with label of 0 in the segmentation
|
|
134
|
+
(typically refer to the background region) will not be saved.
|
|
135
|
+
copy_pose_file (str, optional, default=None):
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
import nibabel as nib
|
|
141
|
+
|
|
142
|
+
if not os.path.isfile(input_nifti_file):
|
|
143
|
+
raise FileNotFoundError(f"Input NIfTI file {input_nifti_file} not found.")
|
|
144
|
+
|
|
145
|
+
if copy_pose_file is not None:
|
|
146
|
+
if not os.path.isfile(copy_pose_file):
|
|
147
|
+
raise FileNotFoundError(f"Copy pose file {copy_pose_file} not found.")
|
|
148
|
+
|
|
149
|
+
input_data = nib.load(input_nifti_file)
|
|
150
|
+
image_data = input_data.get_fdata()
|
|
151
|
+
|
|
152
|
+
if isinstance(objects, str):
|
|
153
|
+
objects = objects.split(",")
|
|
154
|
+
n_classes = len(objects) + 1 if discard_background else len(objects)
|
|
155
|
+
one_hot_arr = one_hot(image_data, num_classes=n_classes)
|
|
156
|
+
|
|
157
|
+
start = 1 if discard_background else 0
|
|
158
|
+
for i in range(start, one_hot_arr.shape[3]):
|
|
159
|
+
nifti_label_image = nib.Nifti1Image(one_hot_arr[..., i], input_data.affine, input_data.header, dtype=np.uint8)
|
|
160
|
+
if discard_background:
|
|
161
|
+
obj = objects[i - 1]
|
|
162
|
+
else:
|
|
163
|
+
obj = objects[i]
|
|
164
|
+
tmp_nifti_file = f"{output_file}_{obj}.nii.gz"
|
|
165
|
+
made_output_dir, output_dir = ensure_output_file_dir_existence(tmp_nifti_file)
|
|
166
|
+
try:
|
|
167
|
+
nib.save(nifti_label_image, tmp_nifti_file)
|
|
168
|
+
nifti2cavass(tmp_nifti_file, f"{output_file}_{obj}.BIM", modality, copy_pose_file=copy_pose_file)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
if made_output_dir and os.path.isdir(output_dir):
|
|
171
|
+
shutil.rmtree(output_dir)
|
|
172
|
+
if os.path.exists(tmp_nifti_file):
|
|
173
|
+
os.remove(tmp_nifti_file)
|
|
174
|
+
raise e
|
|
175
|
+
os.remove(tmp_nifti_file)
|
cavass/dicom.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from random import randint
|
|
4
|
+
|
|
5
|
+
from pydicom import Dataset
|
|
6
|
+
from pydicom.dataset import FileDataset, FileMetaDataset
|
|
7
|
+
from pydicom.uid import generate_uid
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Modality(Enum):
|
|
11
|
+
CT = 1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_dicom_dataset(name, modality: Modality) -> FileDataset:
|
|
15
|
+
file_meta = FileMetaDataset()
|
|
16
|
+
file_meta.TransferSyntaxUID = "1.2.840.10008.1.2.1"
|
|
17
|
+
file_meta.ImplementationVersionName = "DICOM"
|
|
18
|
+
|
|
19
|
+
ds = FileDataset(name, {}, file_meta=file_meta, preamble=b"\0" * 128)
|
|
20
|
+
ds.is_implicit_VR = False
|
|
21
|
+
ds.is_little_endian = True
|
|
22
|
+
ds.ImageType = ["DERIVED", "SECONDARY"]
|
|
23
|
+
|
|
24
|
+
# add properties
|
|
25
|
+
patient(ds)
|
|
26
|
+
general_study(ds)
|
|
27
|
+
patient_study(ds)
|
|
28
|
+
frame_of_reference(ds)
|
|
29
|
+
general_equipment(ds)
|
|
30
|
+
general_image(ds)
|
|
31
|
+
general_acquisition(ds)
|
|
32
|
+
image_plane(ds)
|
|
33
|
+
image_pixel(ds)
|
|
34
|
+
SOP_common(ds)
|
|
35
|
+
VOI_LUT(ds)
|
|
36
|
+
general_series(ds, file_meta, modality)
|
|
37
|
+
|
|
38
|
+
# set datatime
|
|
39
|
+
dt = datetime.datetime.now()
|
|
40
|
+
date_str = dt.strftime("%Y%m%d")
|
|
41
|
+
time_str = dt.strftime("%H%M%S.%f") # long format with micro seconds
|
|
42
|
+
|
|
43
|
+
ds.ContentDate = date_str
|
|
44
|
+
ds.ContentTime = time_str
|
|
45
|
+
ds.StudyDate = date_str
|
|
46
|
+
ds.StudyTime = time_str
|
|
47
|
+
ds.SeriesDate = date_str
|
|
48
|
+
ds.SeriesTime = time_str
|
|
49
|
+
ds.AcquisitionDate = date_str
|
|
50
|
+
ds.AcquisitionTime = time_str
|
|
51
|
+
ds.InstanceCreationDate = date_str
|
|
52
|
+
ds.InstanceCreationTime = time_str
|
|
53
|
+
|
|
54
|
+
ds.RescaleIntercept = ""
|
|
55
|
+
ds.RescaleSlope = ""
|
|
56
|
+
|
|
57
|
+
return ds
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def patient(ds: Dataset):
|
|
61
|
+
ds.PatientName = "Patient Name"
|
|
62
|
+
ds.PatientID = "1"
|
|
63
|
+
ds.PatientSex = ""
|
|
64
|
+
ds.PatientBirthDate = ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def general_study(ds: Dataset):
|
|
68
|
+
ds.StudyInstanceUID = generate_uid()
|
|
69
|
+
ds.StudyDescription = ""
|
|
70
|
+
ds.ReferringPhysicianName = ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def patient_study(ds: Dataset):
|
|
74
|
+
ds.PatientAge = ""
|
|
75
|
+
ds.PatientWeight = ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def general_series(ds: Dataset, file_meta: FileMetaDataset, modality: Modality):
|
|
79
|
+
ds.Modality = modality.name
|
|
80
|
+
match modality:
|
|
81
|
+
case Modality.CT:
|
|
82
|
+
file_meta.MediaStorageSOPClassUID = "1.2.840.10008.5.1.4.1.1.2"
|
|
83
|
+
ds.SOPClassUID = "1.2.840.10008.5.1.4.1.1.2"
|
|
84
|
+
CT_image(ds)
|
|
85
|
+
|
|
86
|
+
ds.SeriesInstanceUID = generate_uid(None)
|
|
87
|
+
ds.SeriesNumber = str(randint(1000, 9999))
|
|
88
|
+
ds.ProtocolName = "DICOM"
|
|
89
|
+
ds.PatientPosition = ""
|
|
90
|
+
ds.AccessionNumber = "123456"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def frame_of_reference(ds: Dataset):
|
|
94
|
+
ds.FrameOfReferenceUID = generate_uid(None)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def general_equipment(ds: Dataset):
|
|
98
|
+
ds.Manufacturer = ""
|
|
99
|
+
ds.InstitutionName = "INSTITUTION_NAME_UNDEFINED"
|
|
100
|
+
ds.ManufacturerModelName = ""
|
|
101
|
+
ds.SoftwareVersions = ""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def general_image(ds: Dataset):
|
|
105
|
+
ds.InstanceNumber = ""
|
|
106
|
+
ds.PatientOrientation = ""
|
|
107
|
+
ds.ContentDate = ""
|
|
108
|
+
ds.ContentTime = ""
|
|
109
|
+
ds.ImageType = ["SECONDARY", "DERIVED"]
|
|
110
|
+
ds.LossyImageCompression = "00"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def general_acquisition(ds: Dataset):
|
|
114
|
+
ds.AcquisitionNumber = ""
|
|
115
|
+
ds.AcquisitionDate = ""
|
|
116
|
+
ds.AcquisitionTime = ""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def image_plane(ds: Dataset):
|
|
120
|
+
ds.PixelSpacing = ""
|
|
121
|
+
ds.ImageOrientationPatient = ["1", "0", "0", "0", "1", "0"]
|
|
122
|
+
ds.ImagePositionPatient = ["0", "0", "0"]
|
|
123
|
+
ds.SliceThickness = ""
|
|
124
|
+
ds.SpacingBetweenSlices = ""
|
|
125
|
+
ds.SliceLocation = ""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def image_pixel(ds: Dataset):
|
|
129
|
+
ds.Rows = 0
|
|
130
|
+
ds.Columns = 0
|
|
131
|
+
|
|
132
|
+
ds.BitsAllocated = 0
|
|
133
|
+
ds.BitsStored = 0
|
|
134
|
+
ds.HighBit = 0
|
|
135
|
+
|
|
136
|
+
ds.PixelRepresentation = 1
|
|
137
|
+
|
|
138
|
+
ds.SmallestImagePixelValue = ""
|
|
139
|
+
ds.LargestImagePixelValue = ""
|
|
140
|
+
|
|
141
|
+
ds.PixelData = ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def SOP_common(ds: Dataset):
|
|
145
|
+
ds.SOPClassUID = ""
|
|
146
|
+
ds.SOPInstanceUID = ""
|
|
147
|
+
|
|
148
|
+
ds.SpecificCharacterSet = "ISO_IR 100"
|
|
149
|
+
ds.InstanceCreationDate = ""
|
|
150
|
+
ds.InstanceCreationTime = ""
|
|
151
|
+
|
|
152
|
+
ds.InstanceCreatorUID = ""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def VOI_LUT(ds: Dataset):
|
|
156
|
+
ds.WindowCenter = ""
|
|
157
|
+
ds.WindowWidth = ""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def CT_image(ds: Dataset):
|
|
161
|
+
ds.SamplesPerPixel = 1
|
|
162
|
+
|
|
163
|
+
ds.PhotometricInterpretation = "MONOCHROME2"
|
|
164
|
+
|
|
165
|
+
ds.BitsAllocated = 16
|
|
166
|
+
ds.BitsStored = 12
|
|
167
|
+
ds.HighBit = ds.BitsStored - 1
|
cavass/integration.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
import shutil
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
from cavass._io import ensure_output_file_dir_existence
|
|
7
|
+
from cavass.constants import CAVASS_START_INDEX
|
|
8
|
+
from cavass.ops import ndvoi, matched_reslice, bin_ops
|
|
9
|
+
from cavass.slice_range import get_slice_range
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def integrate_trimmed_images(trimmed_files: Union[list[str], tuple[str, ...]],
|
|
13
|
+
reference_file: str,
|
|
14
|
+
output_file_1: str,
|
|
15
|
+
output_file_2: str, ):
|
|
16
|
+
"""
|
|
17
|
+
Suture trimmed CAVASS files.
|
|
18
|
+
For the situation that the whole file is trimmed into multiple files of body regions, this script integrates
|
|
19
|
+
multiple trimmed files into one file.
|
|
20
|
+
The ROI of the new file is determined by the provided trimmed files, which is the region exactly contains all
|
|
21
|
+
trimmed files and may not be the same as the ROI of original untrimmed file.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
trimmed_files (tuple or list): trimmed files. trimmed files must be arranged correctly in the order of form inferior to superior.
|
|
25
|
+
reference_file (str): file to match.
|
|
26
|
+
output_file_1 (str): `output_file_1` is the output file trimmed from the reference file according to the ROI obtained from the trimmed files.`
|
|
27
|
+
output_file_2 (str): `output_file_2` is the output file of integrated files of the untrimmed files.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
if not os.path.isfile(reference_file):
|
|
34
|
+
raise FileNotFoundError(f"Reference file {reference_file} not found.")
|
|
35
|
+
for each in trimmed_files:
|
|
36
|
+
if not os.path.isfile(each):
|
|
37
|
+
raise FileNotFoundError(f"Trimmed file {each} not found.")
|
|
38
|
+
|
|
39
|
+
# Get ROI from the first and last untrimmed files.
|
|
40
|
+
# The inferior location is the inferior location of the first trimmed image in the untrimmed image.
|
|
41
|
+
# The superior location is the superior location of the last trimmed image in the untrimmed image.
|
|
42
|
+
all_unmatched_files = []
|
|
43
|
+
inferior_slice_idx, _, unmatched_files = get_slice_range(trimmed_files[0], reference_file)
|
|
44
|
+
all_unmatched_files.extend(unmatched_files)
|
|
45
|
+
_, superior_slice_idx, unmatched_files = get_slice_range(trimmed_files[-1], reference_file)
|
|
46
|
+
all_unmatched_files.extend(unmatched_files)
|
|
47
|
+
|
|
48
|
+
# CAVASS uses index started from 1.
|
|
49
|
+
inferior_slice_idx -= CAVASS_START_INDEX
|
|
50
|
+
superior_slice_idx -= CAVASS_START_INDEX
|
|
51
|
+
|
|
52
|
+
# The new ROI is [inferior_slice_idx, superior_slice_idx]
|
|
53
|
+
# Trim from original untrimmed file.
|
|
54
|
+
|
|
55
|
+
made_output_dir_1, output_dir_1 = ensure_output_file_dir_existence(output_file_1)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
ndvoi(reference_file, output_file_1, min_slice_dim_3=inferior_slice_idx, max_slice_dim_3=superior_slice_idx)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
if made_output_dir_1 and os.path.isdir(output_dir_1):
|
|
61
|
+
shutil.rmtree(output_dir_1)
|
|
62
|
+
if os.path.exists(output_file_1):
|
|
63
|
+
os.remove(output_file_1)
|
|
64
|
+
raise e
|
|
65
|
+
|
|
66
|
+
made_output_dir_2, output_dir_2 = ensure_output_file_dir_existence(output_file_2)
|
|
67
|
+
|
|
68
|
+
# create reslice files of trimmed files w.r.t untrimmed file.
|
|
69
|
+
tmp_files = []
|
|
70
|
+
reslice_files = []
|
|
71
|
+
file_type = os.path.splitext(trimmed_files[0])[1][1:]
|
|
72
|
+
interpolation_method = "nearest" if file_type == "BIM" else "linear"
|
|
73
|
+
for trimmed_file in trimmed_files:
|
|
74
|
+
reslice_file = os.path.join(output_dir_2, f"{uuid.uuid1()}.{file_type}")
|
|
75
|
+
tmp_files.append(reslice_file)
|
|
76
|
+
reslice_files.append(reslice_file)
|
|
77
|
+
try:
|
|
78
|
+
matched_reslice(trimmed_file, reference_file, reslice_file, interpolation_method=interpolation_method)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
if made_output_dir_1 and os.path.isdir(output_dir_1):
|
|
81
|
+
shutil.rmtree(output_dir_1)
|
|
82
|
+
if os.path.exists(output_file_1):
|
|
83
|
+
os.remove(output_file_1)
|
|
84
|
+
|
|
85
|
+
if made_output_dir_2 and os.path.isdir(output_dir_2):
|
|
86
|
+
shutil.rmtree(output_dir_2)
|
|
87
|
+
for each in tmp_files:
|
|
88
|
+
if os.path.exists(each):
|
|
89
|
+
os.remove(each)
|
|
90
|
+
raise e
|
|
91
|
+
|
|
92
|
+
# integrate reslice files by OR operation.
|
|
93
|
+
if len(reslice_files) > 1:
|
|
94
|
+
or_op_file_1 = reslice_files[0]
|
|
95
|
+
or_op_file_2 = reslice_files[1]
|
|
96
|
+
or_op_output_file = os.path.join(output_dir_2, f"{uuid.uuid1()}.{file_type}")
|
|
97
|
+
or_op_output_files = [or_op_output_file]
|
|
98
|
+
tmp_files.append(or_op_output_file)
|
|
99
|
+
bin_ops(or_op_file_1, or_op_file_2, or_op_output_file, op="or")
|
|
100
|
+
if len(reslice_files) > 2:
|
|
101
|
+
for reslice_file in reslice_files[2:]:
|
|
102
|
+
or_op_file_1 = or_op_output_files[-1]
|
|
103
|
+
or_op_file_2 = reslice_file
|
|
104
|
+
or_op_output_file = os.path.join(output_dir_2, f"{uuid.uuid1()}.{file_type}")
|
|
105
|
+
or_op_output_files.append(or_op_output_file)
|
|
106
|
+
tmp_files.append(or_op_output_file)
|
|
107
|
+
try:
|
|
108
|
+
bin_ops(or_op_file_1, or_op_file_2, or_op_output_file, op="or")
|
|
109
|
+
except Exception as e:
|
|
110
|
+
if made_output_dir_1 and os.path.isdir(output_dir_1):
|
|
111
|
+
shutil.rmtree(output_dir_1)
|
|
112
|
+
if os.path.exists(output_file_1):
|
|
113
|
+
os.remove(output_file_1)
|
|
114
|
+
|
|
115
|
+
if made_output_dir_2 and os.path.isdir(output_dir_2):
|
|
116
|
+
shutil.rmtree(output_dir_2)
|
|
117
|
+
for each in tmp_files:
|
|
118
|
+
if os.path.exists(each):
|
|
119
|
+
os.remove(each)
|
|
120
|
+
raise e
|
|
121
|
+
|
|
122
|
+
output_file = or_op_output_files[-1]
|
|
123
|
+
else:
|
|
124
|
+
output_file = reslice_files[0]
|
|
125
|
+
|
|
126
|
+
# reslice to otuput file 1
|
|
127
|
+
try:
|
|
128
|
+
matched_reslice(output_file, output_file_1, output_file_2, interpolation_method=interpolation_method)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
if made_output_dir_1 and os.path.isdir(output_dir_1):
|
|
131
|
+
shutil.rmtree(output_dir_1)
|
|
132
|
+
if os.path.exists(output_file_1):
|
|
133
|
+
os.remove(output_file_1)
|
|
134
|
+
|
|
135
|
+
if made_output_dir_2 and os.path.isdir(output_dir_2):
|
|
136
|
+
shutil.rmtree(output_dir_2)
|
|
137
|
+
for each in tmp_files:
|
|
138
|
+
if os.path.exists(each):
|
|
139
|
+
os.remove(each)
|
|
140
|
+
raise e
|
|
141
|
+
|
|
142
|
+
for each in tmp_files:
|
|
143
|
+
if os.path.exists(each):
|
|
144
|
+
os.remove(each)
|
|
145
|
+
return all_unmatched_files
|
cavass/match.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
|
|
4
|
+
from cavass._io import ensure_output_file_dir_existence
|
|
5
|
+
from cavass.constants import CAVASS_START_INDEX
|
|
6
|
+
from cavass.ops import matched_reslice
|
|
7
|
+
from cavass.slice_range import get_slice_range
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def match(unmatched_file: str,
|
|
11
|
+
file_to_match: str,
|
|
12
|
+
output_file: str):
|
|
13
|
+
"""
|
|
14
|
+
Match body region.
|
|
15
|
+
Args:
|
|
16
|
+
unmatched_file (str):
|
|
17
|
+
file_to_match (str):
|
|
18
|
+
output_file (str):
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
inferior_slice_idx, superior_slice_idx, unmatched_files = get_slice_range(unmatched_file, file_to_match)
|
|
24
|
+
inferior_slice_idx -= CAVASS_START_INDEX
|
|
25
|
+
superior_slice_idx -= CAVASS_START_INDEX
|
|
26
|
+
|
|
27
|
+
made_output_dir, output_dir = ensure_output_file_dir_existence(output_file)
|
|
28
|
+
file_type = os.path.splitext(unmatched_file[0])[1][1:]
|
|
29
|
+
interpolation_method = "nearest" if file_type == "BIM" else "linear"
|
|
30
|
+
try:
|
|
31
|
+
matched_reslice(unmatched_file, file_to_match, output_file, interpolation_method=interpolation_method)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
if made_output_dir and os.path.isdir(output_dir):
|
|
34
|
+
shutil.rmtree(output_dir)
|
|
35
|
+
if os.path.exists(output_file):
|
|
36
|
+
os.remove(output_file)
|
|
37
|
+
raise e
|
|
38
|
+
return unmatched_files
|