jbag 4.3.9__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.
- jbag/__init__.py +3 -0
- jbag/clustering/__init__.py +1 -0
- jbag/clustering/agglomerative_hierarchical_clustering.py +147 -0
- jbag/config.py +140 -0
- jbag/converters/__init__.py +2 -0
- jbag/converters/dicom2nifti.py +16 -0
- jbag/converters/nifti2dicom.py +155 -0
- jbag/dicom/__init__.py +0 -0
- jbag/dicom/dicom.py +164 -0
- jbag/dicom/dicom_tags.py +10 -0
- jbag/dicom/modalities.py +5 -0
- jbag/docx/__init__.py +4 -0
- jbag/docx/doc.py +13 -0
- jbag/docx/image.py +25 -0
- jbag/docx/paragraph.py +15 -0
- jbag/docx/table.py +131 -0
- jbag/docx/text.py +15 -0
- jbag/image/__init__.py +2 -0
- jbag/image/colors.py +22 -0
- jbag/image/contrast_enhancement.py +18 -0
- jbag/image/gaussian_filter.py +61 -0
- jbag/image/maximum_connectivity_region.py +15 -0
- jbag/image/overlay.py +20 -0
- jbag/io.py +370 -0
- jbag/log.py +9 -0
- jbag/metric_summary.py +44 -0
- jbag/metrics/SDF.py +26 -0
- jbag/metrics/__init__.py +0 -0
- jbag/metrics/distances.py +8 -0
- jbag/parallel_map.py +57 -0
- jbag/samplers/__init__.py +3 -0
- jbag/samplers/coordinate_generator.py +138 -0
- jbag/samplers/grid_sampler.py +109 -0
- jbag/samplers/patch_picker.py +65 -0
- jbag/samplers/preload_data_loader.py +60 -0
- jbag/samplers/preload_dataset.py +119 -0
- jbag/samplers/utils.py +103 -0
- jbag/statistics.py +37 -0
- jbag/torch/__init__.py +1 -0
- jbag/torch/checkpoint_manager.py +61 -0
- jbag/torch/lr.py +10 -0
- jbag/torch/models/__init__.py +2 -0
- jbag/torch/models/deep_supervision.py +48 -0
- jbag/torch/models/mlp.py +37 -0
- jbag/torch/models/network_weight_initialization.py +53 -0
- jbag/torch/models/unet.py +395 -0
- jbag/torch/models/unet_plus_plus.py +163 -0
- jbag/torch/models/utils.py +62 -0
- jbag/transforms/__init__.py +11 -0
- jbag/transforms/_utils.py +45 -0
- jbag/transforms/brightness.py +43 -0
- jbag/transforms/contrast.py +55 -0
- jbag/transforms/data.py +72 -0
- jbag/transforms/distance.py +22 -0
- jbag/transforms/downsample.py +36 -0
- jbag/transforms/gamma.py +66 -0
- jbag/transforms/gaussian_blur.py +57 -0
- jbag/transforms/gaussian_noise.py +44 -0
- jbag/transforms/mirroring.py +34 -0
- jbag/transforms/normalization.py +56 -0
- jbag/transforms/spatial.py +233 -0
- jbag/transforms/transform.py +31 -0
- jbag-4.3.9.dist-info/METADATA +19 -0
- jbag-4.3.9.dist-info/RECORD +66 -0
- jbag-4.3.9.dist-info/WHEEL +4 -0
- jbag-4.3.9.dist-info/licenses/LICENSE +674 -0
jbag/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .agglomerative_hierarchical_clustering import AgglomerativeHierarchicalClustering
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgglomerativeHierarchicalClustering:
|
|
7
|
+
def __init__(self, metric: Callable[[np.ndarray, np.ndarray], float], linkage: str = "complete",
|
|
8
|
+
n_clusters: int = 1):
|
|
9
|
+
"""
|
|
10
|
+
Agglomerative hierarchical clustering algorithm. Only "complete" linkage is supported for now.
|
|
11
|
+
Note that `sklearn.cluster.AgglomerativeClustering` is much faster, this implementation is used when
|
|
12
|
+
AgglomerativeClustering in sklearn fails to meet the data dimension restriction when the sample is
|
|
13
|
+
represented by a matrix/tensor with feature dimensions greater than 1. While AgglomerativeClustering in sklearn
|
|
14
|
+
only supports data shape of (n_samples, n_features).
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
metric (Callable[[np.ndarray, np.ndarray], float]): metric function for measuring element distance.
|
|
18
|
+
linkage (str, optional, default="complete"): currently, only "complete" is supported.
|
|
19
|
+
n_clusters (int, optional, default=1): the number of clusters for clustering.
|
|
20
|
+
"""
|
|
21
|
+
supported_linkages = ["complete"]
|
|
22
|
+
if linkage not in supported_linkages:
|
|
23
|
+
raise ValueError(f"linkage must be one of {supported_linkages}")
|
|
24
|
+
if n_clusters < 1:
|
|
25
|
+
raise ValueError(f"Minimum number of clusters must be >= 1")
|
|
26
|
+
self.metric = metric
|
|
27
|
+
self.linkage = linkage
|
|
28
|
+
self.n_clusters = n_clusters
|
|
29
|
+
|
|
30
|
+
def fit(self, X):
|
|
31
|
+
"""
|
|
32
|
+
Perform agglomerative hierarchical clustering on `X`.
|
|
33
|
+
Args:
|
|
34
|
+
X (np.ndarray): the input data `X` is expected to have be (n_samples, ...).
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The clustering with desired number of clusters and the full grouping tree.
|
|
38
|
+
"""
|
|
39
|
+
n_samples = X.shape[0]
|
|
40
|
+
|
|
41
|
+
if n_samples < 2:
|
|
42
|
+
raise ValueError(f"Need at least 2 samples for clustering.")
|
|
43
|
+
|
|
44
|
+
cluster_distance = np.zeros((n_samples, n_samples))
|
|
45
|
+
|
|
46
|
+
for i in range(n_samples):
|
|
47
|
+
for j in range(i + 1, n_samples):
|
|
48
|
+
distance = self.metric(X[i], X[j])
|
|
49
|
+
cluster_distance[i][j] = distance
|
|
50
|
+
cluster_distance[j][i] = distance
|
|
51
|
+
|
|
52
|
+
np.fill_diagonal(cluster_distance, np.inf)
|
|
53
|
+
|
|
54
|
+
k_steps = n_samples - self.n_clusters
|
|
55
|
+
linkage_matrix = np.zeros((k_steps, 2), dtype=int)
|
|
56
|
+
|
|
57
|
+
# active_cluster_indices is the unmerged cluster indices
|
|
58
|
+
active_cluster_indices = list(range(n_samples))
|
|
59
|
+
|
|
60
|
+
actual_cluster_ids = list(range(n_samples))
|
|
61
|
+
new_cluster_id = n_samples
|
|
62
|
+
|
|
63
|
+
for k_step in range(k_steps):
|
|
64
|
+
|
|
65
|
+
argmin = np.argmin(cluster_distance)
|
|
66
|
+
|
|
67
|
+
i_idx, j_idx = np.unravel_index(argmin, cluster_distance.shape)
|
|
68
|
+
|
|
69
|
+
i_id, j_id = actual_cluster_ids[i_idx], actual_cluster_ids[j_idx]
|
|
70
|
+
linkage_matrix[k_step][0] = i_id
|
|
71
|
+
linkage_matrix[k_step][1] = j_id
|
|
72
|
+
|
|
73
|
+
actual_cluster_ids[i_idx] = new_cluster_id
|
|
74
|
+
new_cluster_id += 1
|
|
75
|
+
|
|
76
|
+
active_cluster_indices.remove(j_idx)
|
|
77
|
+
|
|
78
|
+
for i in active_cluster_indices:
|
|
79
|
+
if i != i_idx:
|
|
80
|
+
distance = self._complete_linkage(cluster_distance, i_idx, j_idx, i)
|
|
81
|
+
cluster_distance[i][i_idx] = distance
|
|
82
|
+
cluster_distance[i_idx][i] = distance
|
|
83
|
+
|
|
84
|
+
cluster_distance[j_idx, :] = np.inf
|
|
85
|
+
cluster_distance[:, j_idx] = np.inf
|
|
86
|
+
|
|
87
|
+
clusterings = self.get_clusters(linkage_matrix, n_samples)
|
|
88
|
+
return clusterings[0], clusterings
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def get_clusters(linkage_matrix, n_samples: int):
|
|
92
|
+
"""
|
|
93
|
+
Build the clusterings from linkage matrix. This function can also be applied to
|
|
94
|
+
sklearn.cluster.AgglomerativeClustering for building the clusterings:
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
linkage_matrix:
|
|
98
|
+
n_samples:
|
|
99
|
+
|
|
100
|
+
Returns: Clusterings with different numbers of clusters.
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
>>> from sklearn.cluster import AgglomerativeClustering
|
|
104
|
+
>>> import numpy as np
|
|
105
|
+
>>> X = np.array([[1, 2], [1, 4], [1, 0],[4, 2], [4, 4], [4, 0]])
|
|
106
|
+
>>> clustering = AgglomerativeClustering(compute_full_tree=True).fit(X)
|
|
107
|
+
>>> linkage_matrix = clustering.children_
|
|
108
|
+
>>> clusterings = AgglomerativeHierarchicalClustering.get_clusters(linkage_matrix, X.shape[0])
|
|
109
|
+
>>> clusterings
|
|
110
|
+
[[[0, 1, 2, 3, 4, 5]], [[0, 1, 2], [3, 4, 5]], [[0, 1, 2], [3, 5], [4]], [[0, 1], [2], [3, 5], [4]], [[0, 1], [2], [3], [4], [5]], [[0], [1], [2], [3], [4], [5]]]
|
|
111
|
+
"""
|
|
112
|
+
cluster_contents = [[i] for i in range(n_samples)]
|
|
113
|
+
active_cluster_ids = list(range(n_samples))
|
|
114
|
+
|
|
115
|
+
history_of_cluster_sets = []
|
|
116
|
+
|
|
117
|
+
current_set_of_clusters = [[i] for i in range(n_samples)]
|
|
118
|
+
history_of_cluster_sets.append(current_set_of_clusters)
|
|
119
|
+
|
|
120
|
+
for i in range(linkage_matrix.shape[0]):
|
|
121
|
+
c1_id, c2_id = linkage_matrix[i]
|
|
122
|
+
|
|
123
|
+
# Retrieve the cluster members of merged clusters
|
|
124
|
+
samples_c1 = cluster_contents[c1_id]
|
|
125
|
+
samples_c2 = cluster_contents[c2_id]
|
|
126
|
+
|
|
127
|
+
new_cluster_samples = sorted(samples_c1 + samples_c2)
|
|
128
|
+
|
|
129
|
+
cluster_contents.append(new_cluster_samples)
|
|
130
|
+
new_cluster_id = n_samples + i
|
|
131
|
+
|
|
132
|
+
active_cluster_ids.remove(c1_id)
|
|
133
|
+
active_cluster_ids.remove(c2_id)
|
|
134
|
+
active_cluster_ids.append(new_cluster_id)
|
|
135
|
+
|
|
136
|
+
current_set_snapshot = [list(cluster_contents[k]) for k in active_cluster_ids]
|
|
137
|
+
current_set_snapshot.sort()
|
|
138
|
+
history_of_cluster_sets.append(current_set_snapshot)
|
|
139
|
+
|
|
140
|
+
return history_of_cluster_sets[::-1]
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _complete_linkage(cluster_distance, linkage_cluster_i_idx, linkage_cluster_j_idx, cluster_idx):
|
|
144
|
+
|
|
145
|
+
max_distance = max(cluster_distance[cluster_idx][linkage_cluster_i_idx],
|
|
146
|
+
cluster_distance[cluster_idx][linkage_cluster_j_idx])
|
|
147
|
+
return max_distance
|
jbag/config.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import tomllib
|
|
4
|
+
from typing import Mapping
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Config(Mapping):
|
|
8
|
+
def __init__(self, config: Mapping):
|
|
9
|
+
self._config = config
|
|
10
|
+
|
|
11
|
+
def __getattr__(self, name):
|
|
12
|
+
try:
|
|
13
|
+
value = self._config[name]
|
|
14
|
+
except KeyError:
|
|
15
|
+
raise AttributeError(f"The config has no key {name}") from None
|
|
16
|
+
if isinstance(value, Mapping):
|
|
17
|
+
return Config(value)
|
|
18
|
+
return value
|
|
19
|
+
|
|
20
|
+
def __getitem__(self, key):
|
|
21
|
+
try:
|
|
22
|
+
value = self._config[key]
|
|
23
|
+
except KeyError:
|
|
24
|
+
raise KeyError(f"The config has no key {key}") from None
|
|
25
|
+
if isinstance(value, Mapping):
|
|
26
|
+
return Config(value)
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
def as_primitive(self) -> dict:
|
|
30
|
+
return self._config
|
|
31
|
+
|
|
32
|
+
def __dir__(self) -> list:
|
|
33
|
+
return list(self._config.keys())
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return repr(self._config)
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
return f"configuration keys: {dir(self)}"
|
|
40
|
+
|
|
41
|
+
def __len__(self):
|
|
42
|
+
return len(self._config)
|
|
43
|
+
|
|
44
|
+
def __iter__(self):
|
|
45
|
+
return iter(self._config)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_config(config_file: str):
|
|
49
|
+
if not os.path.isfile(config_file):
|
|
50
|
+
raise FileNotFoundError(f"Config file {config_file} not found.")
|
|
51
|
+
|
|
52
|
+
with open(config_file, "rb") as f:
|
|
53
|
+
config = tomllib.load(f)
|
|
54
|
+
refine_nodes(config, config)
|
|
55
|
+
config = Config(config)
|
|
56
|
+
return config
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def refine_nodes(node, root):
|
|
60
|
+
for k, v in node.items():
|
|
61
|
+
if isinstance(v, str):
|
|
62
|
+
replace_str(k, node, root)
|
|
63
|
+
elif isinstance(v, dict):
|
|
64
|
+
refine_nodes(v, root)
|
|
65
|
+
elif isinstance(v, list):
|
|
66
|
+
for i, inner_v in enumerate(v):
|
|
67
|
+
if isinstance(inner_v, str):
|
|
68
|
+
replace_str(k, node, root, list_index=i)
|
|
69
|
+
elif isinstance(inner_v, dict):
|
|
70
|
+
refine_nodes(inner_v, root)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def replace_str(key, node, root, list_index=None):
|
|
74
|
+
"""
|
|
75
|
+
If `node[key]` is list type, set list_index to the index of current element of `node[key]`.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
key (str):
|
|
79
|
+
node (mapping):
|
|
80
|
+
root (mapping):
|
|
81
|
+
list_index (int or None, optional, default=None):
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
reference_patten = r"(\$\{.+?\})"
|
|
87
|
+
reference_key_patten = r"\$\{(.+)\}"
|
|
88
|
+
if list_index is not None:
|
|
89
|
+
value = node[key][list_index]
|
|
90
|
+
else:
|
|
91
|
+
value = node[key]
|
|
92
|
+
match = re.findall(reference_patten, value)
|
|
93
|
+
if not match:
|
|
94
|
+
return
|
|
95
|
+
for each in match:
|
|
96
|
+
replace_key = re.search(reference_key_patten, each).group(1)
|
|
97
|
+
replace_node, replace_key = get_node_key(replace_key, node, root)
|
|
98
|
+
if isinstance(replace_node[replace_key], str):
|
|
99
|
+
replace_str(replace_key, replace_node, root)
|
|
100
|
+
if list_index is not None:
|
|
101
|
+
node[key][list_index] = node[key][list_index].replace(each, root[replace_key])
|
|
102
|
+
else:
|
|
103
|
+
node[key] = node[key].replace(each, str(replace_node[replace_key]))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_node_key(key, node, root):
|
|
107
|
+
"""
|
|
108
|
+
Get the inner node and key.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
key (str):
|
|
112
|
+
node (dict):
|
|
113
|
+
root (mapping):
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
node_hierarchy = key.split(".")
|
|
119
|
+
# first, search on current node
|
|
120
|
+
# second, search on root if search failed on current node
|
|
121
|
+
|
|
122
|
+
node, unpacked_key = search_node(node_hierarchy, node)
|
|
123
|
+
if node and unpacked_key:
|
|
124
|
+
return node, unpacked_key
|
|
125
|
+
|
|
126
|
+
node = root
|
|
127
|
+
node, unpacked_key = search_node(node_hierarchy, node)
|
|
128
|
+
if node and unpacked_key:
|
|
129
|
+
return node, unpacked_key
|
|
130
|
+
raise ValueError(f"The config has no key {key}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def search_node(node_hierarchy, node):
|
|
134
|
+
for i, key in enumerate(node_hierarchy):
|
|
135
|
+
if key in node:
|
|
136
|
+
if i == len(node_hierarchy) - 1:
|
|
137
|
+
return node, node_hierarchy[-1]
|
|
138
|
+
else:
|
|
139
|
+
node = node[key]
|
|
140
|
+
return None, None
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import dicom2nifti as d2n
|
|
4
|
+
|
|
5
|
+
from jbag.io import ensure_output_file_dir_existence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def dicom2nifti(input_dicom_series, output_nifti_file, pydicom_read_force=False):
|
|
9
|
+
if not os.path.exists(input_dicom_series):
|
|
10
|
+
raise ValueError(f"Input DICOM series {input_dicom_series} does not exist.")
|
|
11
|
+
|
|
12
|
+
if pydicom_read_force:
|
|
13
|
+
d2n.settings.pydicom_read_force = pydicom_read_force
|
|
14
|
+
|
|
15
|
+
ensure_output_file_dir_existence(output_nifti_file)
|
|
16
|
+
d2n.dicom_series_to_nifti(input_dicom_series, output_nifti_file, reorient_nifti=False)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
|
|
4
|
+
import nibabel as nib
|
|
5
|
+
import numpy as np
|
|
6
|
+
from pydicom import Dataset
|
|
7
|
+
from pydicom.uid import generate_uid
|
|
8
|
+
|
|
9
|
+
from jbag import logger
|
|
10
|
+
from jbag.dicom.dicom import get_dicom_dataset
|
|
11
|
+
from jbag.dicom.modalities import Modality
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def write_slice(ds: Dataset, image_data, slice_index, output_dir):
|
|
15
|
+
output_filename = r"Slice_%04d.dcm" % (slice_index + 1)
|
|
16
|
+
image_slice = image_data[..., slice_index]
|
|
17
|
+
ds.SOPInstanceUID = generate_uid(None)
|
|
18
|
+
ds.PixelData = image_slice.tobytes()
|
|
19
|
+
ds.save_as(os.path.join(output_dir, output_filename), write_like_original=False)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_nii2dcm_parameters(nii_data):
|
|
23
|
+
nii_img = nii_data.get_fdata()
|
|
24
|
+
|
|
25
|
+
dim_array = nii_data.header["dim"].astype(np.int16)
|
|
26
|
+
num_x, num_y, num_z = int(dim_array[1]), int(dim_array[2]), int(dim_array[3])
|
|
27
|
+
voxel_spacing_x, voxel_spacing_y, voxel_spacing_z = (nii_data.header["pixdim"][1],
|
|
28
|
+
nii_data.header["pixdim"][2],
|
|
29
|
+
nii_data.header["pixdim"][3])
|
|
30
|
+
|
|
31
|
+
slice_indices = range(1, num_z + 1)
|
|
32
|
+
last_location_z = (voxel_spacing_z * num_z) - voxel_spacing_z
|
|
33
|
+
slice_locations = np.linspace(0, last_location_z, num=num_z)
|
|
34
|
+
|
|
35
|
+
# Windowing
|
|
36
|
+
max_i = np.max(nii_img)
|
|
37
|
+
min_i = np.min(nii_img)
|
|
38
|
+
window_center = round((max_i - min_i) / 2)
|
|
39
|
+
window_width = round(max_i - min_i)
|
|
40
|
+
|
|
41
|
+
rescale_intercept = 0
|
|
42
|
+
rescale_slope = 1
|
|
43
|
+
|
|
44
|
+
# FOV
|
|
45
|
+
fov_x = num_x * voxel_spacing_x
|
|
46
|
+
fov_y = num_y * voxel_spacing_y
|
|
47
|
+
fov_z = num_z * voxel_spacing_z
|
|
48
|
+
|
|
49
|
+
# slice positioning in 3-D space
|
|
50
|
+
# -1 for direction cosines gives consistent orientation between Nifti and DICOM in ITK-Snap
|
|
51
|
+
affine = nii_data.affine
|
|
52
|
+
dir_cos_x = -1 * affine[:3, 0] / voxel_spacing_x
|
|
53
|
+
dir_cos_y = -1 * affine[:3, 1] / voxel_spacing_y
|
|
54
|
+
|
|
55
|
+
image_pos_patient_array = []
|
|
56
|
+
for slice_i in range(0, num_z):
|
|
57
|
+
v = affine.dot([0, 0, slice_i - 1, 1])
|
|
58
|
+
image_pos_patient_array.append([v[0], v[1], v[2]])
|
|
59
|
+
|
|
60
|
+
# output dictionary
|
|
61
|
+
nii2dcm_parameters = {
|
|
62
|
+
# series parameters
|
|
63
|
+
"DimX": voxel_spacing_x,
|
|
64
|
+
"DimY": voxel_spacing_y,
|
|
65
|
+
"SliceThickness": str(voxel_spacing_z),
|
|
66
|
+
"SpacingBetweenSlices": str(voxel_spacing_z),
|
|
67
|
+
"AcquisitionMatrix": [0, num_x, num_y, 0],
|
|
68
|
+
"Rows": num_x,
|
|
69
|
+
"Columns": num_y,
|
|
70
|
+
"NumberOfSlices": num_z,
|
|
71
|
+
"NumberOfInstances": num_z,
|
|
72
|
+
"PixelSpacing": [voxel_spacing_x, voxel_spacing_y],
|
|
73
|
+
"FOV": [fov_x, fov_y, fov_z],
|
|
74
|
+
"SmallestImagePixelValue": min_i,
|
|
75
|
+
"LargestImagePixelValue": max_i,
|
|
76
|
+
"WindowCenter": str(window_center),
|
|
77
|
+
"WindowWidth": str(window_width),
|
|
78
|
+
"RescaleIntercept": str(rescale_intercept),
|
|
79
|
+
"RescaleSlope": str(rescale_slope),
|
|
80
|
+
"ImageOrientationPatient": [dir_cos_y[0], dir_cos_y[1], dir_cos_y[2], dir_cos_x[0], dir_cos_x[1], dir_cos_x[2]],
|
|
81
|
+
|
|
82
|
+
# instance parameters
|
|
83
|
+
"InstanceNumber": slice_indices,
|
|
84
|
+
"SliceLocation": slice_locations,
|
|
85
|
+
"ImagePositionPatient": image_pos_patient_array
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return nii2dcm_parameters
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def transfer_dicom_series_tags(nii2dcm_parameters: dict, ds: Dataset):
|
|
92
|
+
ds.Rows = nii2dcm_parameters["Rows"]
|
|
93
|
+
ds.Columns = nii2dcm_parameters["Columns"]
|
|
94
|
+
ds.PixelSpacing = [round(float(nii2dcm_parameters["DimX"]), 2), round(float(nii2dcm_parameters["DimY"]), 2)]
|
|
95
|
+
ds.SliceThickness = nii2dcm_parameters["SliceThickness"]
|
|
96
|
+
ds.SpacingBetweenSlices = round(float(nii2dcm_parameters["SpacingBetweenSlices"]), 2)
|
|
97
|
+
ds.ImageOrientationPatient = nii2dcm_parameters["ImageOrientationPatient"]
|
|
98
|
+
ds.AcquisitionMatrix = nii2dcm_parameters["AcquisitionMatrix"]
|
|
99
|
+
ds.SmallestImagePixelValue = int(nii2dcm_parameters["SmallestImagePixelValue"]) \
|
|
100
|
+
if int(nii2dcm_parameters["SmallestImagePixelValue"]) > 0 else 0
|
|
101
|
+
ds.LargestImagePixelValue = int(nii2dcm_parameters["LargestImagePixelValue"])
|
|
102
|
+
ds.WindowCenter = nii2dcm_parameters["WindowCenter"]
|
|
103
|
+
ds.WindowWidth = nii2dcm_parameters["WindowWidth"]
|
|
104
|
+
ds.RescaleIntercept = nii2dcm_parameters["RescaleIntercept"]
|
|
105
|
+
ds.RescaleSlope = nii2dcm_parameters["RescaleSlope"]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def transfer_dicom_instance_tags(nii2dcm_parameters: dict, ds: Dataset, instance_index: int):
|
|
109
|
+
ds.InstanceNumber = nii2dcm_parameters["InstanceNumber"][instance_index]
|
|
110
|
+
ds.SliceLocation = nii2dcm_parameters["SliceLocation"][instance_index]
|
|
111
|
+
ds.ImagePositionPatient = [
|
|
112
|
+
str(nii2dcm_parameters["ImagePositionPatient"][instance_index][0]),
|
|
113
|
+
str(nii2dcm_parameters["ImagePositionPatient"][instance_index][1]),
|
|
114
|
+
str(nii2dcm_parameters["ImagePositionPatient"][instance_index][2]),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def nifti2dicom(input_nifti_file, output_dicom_dir, modality: Modality, force_overwrite=False):
|
|
119
|
+
"""
|
|
120
|
+
Convert NIfTI image to DICOM image series. Inspired by https://github.com/tomaroberts/nii2dcm.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
input_nifti_file (str):
|
|
124
|
+
output_dicom_dir (str):
|
|
125
|
+
modality (Modality):
|
|
126
|
+
force_overwrite (bool, optional, default=False): If `Ture`, overwrite `output_dicom_dir` if it exists.`
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
if not os.path.isfile(input_nifti_file):
|
|
133
|
+
raise FileNotFoundError(f"Input NIfTI file {input_nifti_file} not found.")
|
|
134
|
+
|
|
135
|
+
if os.path.exists(output_dicom_dir):
|
|
136
|
+
if force_overwrite:
|
|
137
|
+
logger.info(f"Overwrite output DICOM dir {output_dicom_dir} as it already exists.")
|
|
138
|
+
shutil.rmtree(output_dicom_dir)
|
|
139
|
+
else:
|
|
140
|
+
raise ValueError(f"Output DICOM series dir {output_dicom_dir} already exists.")
|
|
141
|
+
|
|
142
|
+
nii_data = nib.load(input_nifti_file)
|
|
143
|
+
|
|
144
|
+
nii2dcm_properties = get_nii2dcm_parameters(nii_data)
|
|
145
|
+
file_name = os.path.splitext(os.path.basename(input_nifti_file))[0]
|
|
146
|
+
|
|
147
|
+
dicom_ds = get_dicom_dataset(file_name, modality=modality)
|
|
148
|
+
transfer_dicom_series_tags(nii2dcm_properties, dicom_ds)
|
|
149
|
+
|
|
150
|
+
image = nii_data.get_fdata()
|
|
151
|
+
image = image.astype(int)
|
|
152
|
+
os.makedirs(output_dicom_dir)
|
|
153
|
+
for instance_index in range(0, nii2dcm_properties["NumberOfInstances"]):
|
|
154
|
+
transfer_dicom_instance_tags(nii2dcm_properties, dicom_ds, instance_index)
|
|
155
|
+
write_slice(dicom_ds, image, instance_index, output_dicom_dir)
|
jbag/dicom/__init__.py
ADDED
|
File without changes
|
jbag/dicom/dicom.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from random import randint
|
|
3
|
+
|
|
4
|
+
from pydicom import Dataset
|
|
5
|
+
from pydicom.dataset import FileDataset, FileMetaDataset
|
|
6
|
+
from pydicom.uid import generate_uid
|
|
7
|
+
|
|
8
|
+
from jbag.dicom.modalities import Modality
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_dicom_dataset(name, modality: Modality) -> FileDataset:
|
|
12
|
+
file_meta = FileMetaDataset()
|
|
13
|
+
file_meta.TransferSyntaxUID = "1.2.840.10008.1.2.1"
|
|
14
|
+
file_meta.ImplementationVersionName = "DICOM"
|
|
15
|
+
|
|
16
|
+
ds = FileDataset(name, {}, file_meta=file_meta, preamble=b"\0" * 128)
|
|
17
|
+
ds.is_implicit_VR = False
|
|
18
|
+
ds.is_little_endian = True
|
|
19
|
+
ds.ImageType = ["DERIVED", "SECONDARY"]
|
|
20
|
+
|
|
21
|
+
# add properties
|
|
22
|
+
patient(ds)
|
|
23
|
+
general_study(ds)
|
|
24
|
+
patient_study(ds)
|
|
25
|
+
frame_of_reference(ds)
|
|
26
|
+
general_equipment(ds)
|
|
27
|
+
general_image(ds)
|
|
28
|
+
general_acquisition(ds)
|
|
29
|
+
image_plane(ds)
|
|
30
|
+
image_pixel(ds)
|
|
31
|
+
SOP_common(ds)
|
|
32
|
+
VOI_LUT(ds)
|
|
33
|
+
general_series(ds, file_meta, modality)
|
|
34
|
+
|
|
35
|
+
# set datatime
|
|
36
|
+
dt = datetime.datetime.now()
|
|
37
|
+
date_str = dt.strftime("%Y%m%d")
|
|
38
|
+
time_str = dt.strftime("%H%M%S.%f") # long format with micro seconds
|
|
39
|
+
|
|
40
|
+
ds.ContentDate = date_str
|
|
41
|
+
ds.ContentTime = time_str
|
|
42
|
+
ds.StudyDate = date_str
|
|
43
|
+
ds.StudyTime = time_str
|
|
44
|
+
ds.SeriesDate = date_str
|
|
45
|
+
ds.SeriesTime = time_str
|
|
46
|
+
ds.AcquisitionDate = date_str
|
|
47
|
+
ds.AcquisitionTime = time_str
|
|
48
|
+
ds.InstanceCreationDate = date_str
|
|
49
|
+
ds.InstanceCreationTime = time_str
|
|
50
|
+
|
|
51
|
+
ds.RescaleIntercept = ""
|
|
52
|
+
ds.RescaleSlope = ""
|
|
53
|
+
|
|
54
|
+
return ds
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def patient(ds: Dataset):
|
|
58
|
+
ds.PatientName = "Patient Name"
|
|
59
|
+
ds.PatientID = "1"
|
|
60
|
+
ds.PatientSex = ""
|
|
61
|
+
ds.PatientBirthDate = ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def general_study(ds: Dataset):
|
|
65
|
+
ds.StudyInstanceUID = generate_uid()
|
|
66
|
+
ds.StudyDescription = ""
|
|
67
|
+
ds.ReferringPhysicianName = ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def patient_study(ds: Dataset):
|
|
71
|
+
ds.PatientAge = ""
|
|
72
|
+
ds.PatientWeight = ""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def general_series(ds: Dataset, file_meta: FileMetaDataset, modality: Modality):
|
|
76
|
+
ds.Modality = modality.name
|
|
77
|
+
match modality:
|
|
78
|
+
case Modality.CT:
|
|
79
|
+
file_meta.MediaStorageSOPClassUID = "1.2.840.10008.5.1.4.1.1.2"
|
|
80
|
+
ds.SOPClassUID = "1.2.840.10008.5.1.4.1.1.2"
|
|
81
|
+
CT_image(ds)
|
|
82
|
+
|
|
83
|
+
ds.SeriesInstanceUID = generate_uid(None)
|
|
84
|
+
ds.SeriesNumber = str(randint(1000, 9999))
|
|
85
|
+
ds.ProtocolName = "DICOM"
|
|
86
|
+
ds.PatientPosition = ""
|
|
87
|
+
ds.AccessionNumber = "123456"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def frame_of_reference(ds: Dataset):
|
|
91
|
+
ds.FrameOfReferenceUID = generate_uid(None)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def general_equipment(ds: Dataset):
|
|
95
|
+
ds.Manufacturer = ""
|
|
96
|
+
ds.InstitutionName = "INSTITUTION_NAME_UNDEFINED"
|
|
97
|
+
ds.ManufacturerModelName = ""
|
|
98
|
+
ds.SoftwareVersions = ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def general_image(ds: Dataset):
|
|
102
|
+
ds.InstanceNumber = ""
|
|
103
|
+
ds.PatientOrientation = ""
|
|
104
|
+
ds.ContentDate = ""
|
|
105
|
+
ds.ContentTime = ""
|
|
106
|
+
ds.ImageType = ["SECONDARY", "DERIVED"]
|
|
107
|
+
ds.LossyImageCompression = "00"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def general_acquisition(ds: Dataset):
|
|
111
|
+
ds.AcquisitionNumber = ""
|
|
112
|
+
ds.AcquisitionDate = ""
|
|
113
|
+
ds.AcquisitionTime = ""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def image_plane(ds: Dataset):
|
|
117
|
+
ds.PixelSpacing = ""
|
|
118
|
+
ds.ImageOrientationPatient = ["1", "0", "0", "0", "1", "0"]
|
|
119
|
+
ds.ImagePositionPatient = ["0", "0", "0"]
|
|
120
|
+
ds.SliceThickness = ""
|
|
121
|
+
ds.SpacingBetweenSlices = ""
|
|
122
|
+
ds.SliceLocation = ""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def image_pixel(ds: Dataset):
|
|
126
|
+
ds.Rows = 0
|
|
127
|
+
ds.Columns = 0
|
|
128
|
+
|
|
129
|
+
ds.BitsAllocated = 0
|
|
130
|
+
ds.BitsStored = 0
|
|
131
|
+
ds.HighBit = 0
|
|
132
|
+
|
|
133
|
+
ds.PixelRepresentation = 1
|
|
134
|
+
|
|
135
|
+
ds.SmallestImagePixelValue = ""
|
|
136
|
+
ds.LargestImagePixelValue = ""
|
|
137
|
+
|
|
138
|
+
ds.PixelData = ""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def SOP_common(ds: Dataset):
|
|
142
|
+
ds.SOPClassUID = ""
|
|
143
|
+
ds.SOPInstanceUID = ""
|
|
144
|
+
|
|
145
|
+
ds.SpecificCharacterSet = "ISO_IR 100"
|
|
146
|
+
ds.InstanceCreationDate = ""
|
|
147
|
+
ds.InstanceCreationTime = ""
|
|
148
|
+
|
|
149
|
+
ds.InstanceCreatorUID = ""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def VOI_LUT(ds: Dataset):
|
|
153
|
+
ds.WindowCenter = ""
|
|
154
|
+
ds.WindowWidth = ""
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def CT_image(ds: Dataset):
|
|
158
|
+
ds.SamplesPerPixel = 1
|
|
159
|
+
|
|
160
|
+
ds.PhotometricInterpretation = "MONOCHROME2"
|
|
161
|
+
|
|
162
|
+
ds.BitsAllocated = 16
|
|
163
|
+
ds.BitsStored = 12
|
|
164
|
+
ds.HighBit = ds.BitsStored - 1
|
jbag/dicom/dicom_tags.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Largest Image Pixel Value
|
|
2
|
+
Largest_Image_Pixel_Value = (0x0028, 0x0107)
|
|
3
|
+
# Smallest Image Pixel Value
|
|
4
|
+
Smallest_Image_Pixel_Value = (0x0028, 0x0106)
|
|
5
|
+
# Rescale Slope
|
|
6
|
+
Rescale_Slope = (0x0028, 0x1053)
|
|
7
|
+
# Rescale Intercept
|
|
8
|
+
Rescale_Intercept = (0x0028, 0x1052)
|
|
9
|
+
# Pixel Padding Value
|
|
10
|
+
Pixel_Padding_Value = (0x0028,0x0120)
|
jbag/dicom/modalities.py
ADDED
jbag/docx/__init__.py
ADDED
jbag/docx/doc.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
|
|
3
|
+
from docx import Document
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def import_style_from_template(doc: Document, template_doc_file):
|
|
7
|
+
if not os.path.exists(template_doc_file):
|
|
8
|
+
raise FileNotFoundError(f"Template Doc file {template_doc_file} not found.")
|
|
9
|
+
|
|
10
|
+
template_doc = Document(template_doc_file)
|
|
11
|
+
|
|
12
|
+
for style in template_doc.styles:
|
|
13
|
+
doc.styles.add_style(style.style_id, style.type)
|