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.
Files changed (66) hide show
  1. jbag/__init__.py +3 -0
  2. jbag/clustering/__init__.py +1 -0
  3. jbag/clustering/agglomerative_hierarchical_clustering.py +147 -0
  4. jbag/config.py +140 -0
  5. jbag/converters/__init__.py +2 -0
  6. jbag/converters/dicom2nifti.py +16 -0
  7. jbag/converters/nifti2dicom.py +155 -0
  8. jbag/dicom/__init__.py +0 -0
  9. jbag/dicom/dicom.py +164 -0
  10. jbag/dicom/dicom_tags.py +10 -0
  11. jbag/dicom/modalities.py +5 -0
  12. jbag/docx/__init__.py +4 -0
  13. jbag/docx/doc.py +13 -0
  14. jbag/docx/image.py +25 -0
  15. jbag/docx/paragraph.py +15 -0
  16. jbag/docx/table.py +131 -0
  17. jbag/docx/text.py +15 -0
  18. jbag/image/__init__.py +2 -0
  19. jbag/image/colors.py +22 -0
  20. jbag/image/contrast_enhancement.py +18 -0
  21. jbag/image/gaussian_filter.py +61 -0
  22. jbag/image/maximum_connectivity_region.py +15 -0
  23. jbag/image/overlay.py +20 -0
  24. jbag/io.py +370 -0
  25. jbag/log.py +9 -0
  26. jbag/metric_summary.py +44 -0
  27. jbag/metrics/SDF.py +26 -0
  28. jbag/metrics/__init__.py +0 -0
  29. jbag/metrics/distances.py +8 -0
  30. jbag/parallel_map.py +57 -0
  31. jbag/samplers/__init__.py +3 -0
  32. jbag/samplers/coordinate_generator.py +138 -0
  33. jbag/samplers/grid_sampler.py +109 -0
  34. jbag/samplers/patch_picker.py +65 -0
  35. jbag/samplers/preload_data_loader.py +60 -0
  36. jbag/samplers/preload_dataset.py +119 -0
  37. jbag/samplers/utils.py +103 -0
  38. jbag/statistics.py +37 -0
  39. jbag/torch/__init__.py +1 -0
  40. jbag/torch/checkpoint_manager.py +61 -0
  41. jbag/torch/lr.py +10 -0
  42. jbag/torch/models/__init__.py +2 -0
  43. jbag/torch/models/deep_supervision.py +48 -0
  44. jbag/torch/models/mlp.py +37 -0
  45. jbag/torch/models/network_weight_initialization.py +53 -0
  46. jbag/torch/models/unet.py +395 -0
  47. jbag/torch/models/unet_plus_plus.py +163 -0
  48. jbag/torch/models/utils.py +62 -0
  49. jbag/transforms/__init__.py +11 -0
  50. jbag/transforms/_utils.py +45 -0
  51. jbag/transforms/brightness.py +43 -0
  52. jbag/transforms/contrast.py +55 -0
  53. jbag/transforms/data.py +72 -0
  54. jbag/transforms/distance.py +22 -0
  55. jbag/transforms/downsample.py +36 -0
  56. jbag/transforms/gamma.py +66 -0
  57. jbag/transforms/gaussian_blur.py +57 -0
  58. jbag/transforms/gaussian_noise.py +44 -0
  59. jbag/transforms/mirroring.py +34 -0
  60. jbag/transforms/normalization.py +56 -0
  61. jbag/transforms/spatial.py +233 -0
  62. jbag/transforms/transform.py +31 -0
  63. jbag-4.3.9.dist-info/METADATA +19 -0
  64. jbag-4.3.9.dist-info/RECORD +66 -0
  65. jbag-4.3.9.dist-info/WHEEL +4 -0
  66. jbag-4.3.9.dist-info/licenses/LICENSE +674 -0
jbag/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .log import logger
2
+ from .metric_summary import MetricSummary
3
+ from .parallel_map import parallel_map
@@ -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,2 @@
1
+ from .dicom2nifti import dicom2nifti
2
+ from .nifti2dicom import nifti2dicom
@@ -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
@@ -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)
@@ -0,0 +1,5 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Modality(Enum):
5
+ CT = 1
jbag/docx/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .image import add_image
2
+ from .paragraph import add_paragraph
3
+ from .table import set_cell, set_cell_border, set_three_line_border
4
+ from .text import add_text
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)