mediml 0.9.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.
- MEDiml/MEDscan.py +1696 -0
- MEDiml/__init__.py +21 -0
- MEDiml/biomarkers/BatchExtractor.py +806 -0
- MEDiml/biomarkers/BatchExtractorTexturalFilters.py +840 -0
- MEDiml/biomarkers/__init__.py +16 -0
- MEDiml/biomarkers/diagnostics.py +125 -0
- MEDiml/biomarkers/get_oriented_bound_box.py +158 -0
- MEDiml/biomarkers/glcm.py +1602 -0
- MEDiml/biomarkers/gldzm.py +523 -0
- MEDiml/biomarkers/glrlm.py +1315 -0
- MEDiml/biomarkers/glszm.py +555 -0
- MEDiml/biomarkers/int_vol_hist.py +527 -0
- MEDiml/biomarkers/intensity_histogram.py +615 -0
- MEDiml/biomarkers/local_intensity.py +89 -0
- MEDiml/biomarkers/morph.py +1756 -0
- MEDiml/biomarkers/ngldm.py +780 -0
- MEDiml/biomarkers/ngtdm.py +414 -0
- MEDiml/biomarkers/stats.py +373 -0
- MEDiml/biomarkers/utils.py +389 -0
- MEDiml/filters/TexturalFilter.py +299 -0
- MEDiml/filters/__init__.py +9 -0
- MEDiml/filters/apply_filter.py +134 -0
- MEDiml/filters/gabor.py +215 -0
- MEDiml/filters/laws.py +283 -0
- MEDiml/filters/log.py +147 -0
- MEDiml/filters/mean.py +121 -0
- MEDiml/filters/textural_filters_kernels.py +1738 -0
- MEDiml/filters/utils.py +107 -0
- MEDiml/filters/wavelet.py +237 -0
- MEDiml/learning/DataCleaner.py +198 -0
- MEDiml/learning/DesignExperiment.py +480 -0
- MEDiml/learning/FSR.py +667 -0
- MEDiml/learning/Normalization.py +112 -0
- MEDiml/learning/RadiomicsLearner.py +714 -0
- MEDiml/learning/Results.py +2237 -0
- MEDiml/learning/Stats.py +694 -0
- MEDiml/learning/__init__.py +10 -0
- MEDiml/learning/cleaning_utils.py +107 -0
- MEDiml/learning/ml_utils.py +1015 -0
- MEDiml/processing/__init__.py +6 -0
- MEDiml/processing/compute_suv_map.py +121 -0
- MEDiml/processing/discretisation.py +149 -0
- MEDiml/processing/interpolation.py +275 -0
- MEDiml/processing/resegmentation.py +66 -0
- MEDiml/processing/segmentation.py +912 -0
- MEDiml/utils/__init__.py +25 -0
- MEDiml/utils/batch_patients.py +45 -0
- MEDiml/utils/create_radiomics_table.py +131 -0
- MEDiml/utils/data_frame_export.py +42 -0
- MEDiml/utils/find_process_names.py +16 -0
- MEDiml/utils/get_file_paths.py +34 -0
- MEDiml/utils/get_full_rad_names.py +21 -0
- MEDiml/utils/get_institutions_from_ids.py +16 -0
- MEDiml/utils/get_patient_id_from_scan_name.py +22 -0
- MEDiml/utils/get_patient_names.py +26 -0
- MEDiml/utils/get_radiomic_names.py +27 -0
- MEDiml/utils/get_scan_name_from_rad_name.py +22 -0
- MEDiml/utils/image_reader_SITK.py +37 -0
- MEDiml/utils/image_volume_obj.py +22 -0
- MEDiml/utils/imref.py +340 -0
- MEDiml/utils/initialize_features_names.py +62 -0
- MEDiml/utils/inpolygon.py +159 -0
- MEDiml/utils/interp3.py +43 -0
- MEDiml/utils/json_utils.py +78 -0
- MEDiml/utils/mode.py +31 -0
- MEDiml/utils/parse_contour_string.py +58 -0
- MEDiml/utils/save_MEDscan.py +30 -0
- MEDiml/utils/strfind.py +32 -0
- MEDiml/utils/textureTools.py +188 -0
- MEDiml/utils/texture_features_names.py +115 -0
- MEDiml/utils/write_radiomics_csv.py +47 -0
- MEDiml/wrangling/DataManager.py +1724 -0
- MEDiml/wrangling/ProcessDICOM.py +512 -0
- MEDiml/wrangling/__init__.py +3 -0
- mediml-0.9.9.dist-info/LICENSE.md +674 -0
- mediml-0.9.9.dist-info/METADATA +232 -0
- mediml-0.9.9.dist-info/RECORD +78 -0
- mediml-0.9.9.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pydicom
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def compute_suv_map(raw_pet: np.ndarray,
|
|
10
|
+
dicom_h: pydicom.Dataset) -> np.ndarray:
|
|
11
|
+
"""Computes the suv_map of a raw input PET volume. It is assumed that
|
|
12
|
+
the calibration factor was applied beforehand to the PET volume.
|
|
13
|
+
**E.g: raw_pet = raw_pet*RescaleSlope + RescaleIntercept.**
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
raw_pet (ndarray):3D array representing the PET volume in raw format.
|
|
17
|
+
dicom_h (pydicom.dataset.FileDataset): DICOM header of one of the
|
|
18
|
+
corresponding slice of ``raw_pet``.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
ndarray: ``raw_pet`` converted to SUVs (standard uptake values).
|
|
22
|
+
"""
|
|
23
|
+
def dcm_hhmmss(date_str: str) -> float:
|
|
24
|
+
""""Converts to seconds
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
date_str (str): date string
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
float: total seconds
|
|
31
|
+
"""
|
|
32
|
+
# Converts to seconds
|
|
33
|
+
if not isinstance(date_str, str):
|
|
34
|
+
date_str = str(date_str)
|
|
35
|
+
hh = float(date_str[0:2])
|
|
36
|
+
mm = float(date_str[2:4])
|
|
37
|
+
ss = float(date_str[4:6])
|
|
38
|
+
tot_sec = hh*60.0*60.0 + mm*60.0 + ss
|
|
39
|
+
return tot_sec
|
|
40
|
+
|
|
41
|
+
def pydicom_has_tag(dcm_seq, tag):
|
|
42
|
+
# Checks if tag exists
|
|
43
|
+
return get_pydicom_meta_tag(dcm_seq, tag, test_tag=True)
|
|
44
|
+
|
|
45
|
+
def get_pydicom_meta_tag(dcm_seq, tag, tag_type=None, default=None,
|
|
46
|
+
test_tag=False):
|
|
47
|
+
# Reads dicom tag
|
|
48
|
+
# Initialise with default
|
|
49
|
+
tag_value = default
|
|
50
|
+
# Read from header using simple itk
|
|
51
|
+
try:
|
|
52
|
+
tag_value = dcm_seq[tag].value
|
|
53
|
+
except KeyError:
|
|
54
|
+
if test_tag:
|
|
55
|
+
return False
|
|
56
|
+
if test_tag:
|
|
57
|
+
return True
|
|
58
|
+
# Find empty entries
|
|
59
|
+
if tag_value is not None:
|
|
60
|
+
if tag_value == "":
|
|
61
|
+
tag_value = default
|
|
62
|
+
# Cast to correct type (meta tags are usually passed as strings)
|
|
63
|
+
if tag_value is not None:
|
|
64
|
+
# String
|
|
65
|
+
if tag_type == "str":
|
|
66
|
+
tag_value = str(tag_value)
|
|
67
|
+
# Float
|
|
68
|
+
elif tag_type == "float":
|
|
69
|
+
tag_value = float(tag_value)
|
|
70
|
+
# Multiple floats
|
|
71
|
+
elif tag_type == "mult_float":
|
|
72
|
+
tag_value = [float(str_num) for str_num in tag_value]
|
|
73
|
+
# Integer
|
|
74
|
+
elif tag_type == "int":
|
|
75
|
+
tag_value = int(tag_value)
|
|
76
|
+
# Multiple floats
|
|
77
|
+
elif tag_type == "mult_int":
|
|
78
|
+
tag_value = [int(str_num) for str_num in tag_value]
|
|
79
|
+
# Boolean
|
|
80
|
+
elif tag_type == "bool":
|
|
81
|
+
tag_value = bool(tag_value)
|
|
82
|
+
|
|
83
|
+
return tag_value
|
|
84
|
+
|
|
85
|
+
# Get patient weight
|
|
86
|
+
if pydicom_has_tag(dcm_seq=dicom_h, tag=(0x0010, 0x1030)):
|
|
87
|
+
weight = get_pydicom_meta_tag(dcm_seq=dicom_h, tag=(0x0010, 0x1030),
|
|
88
|
+
tag_type="float") * 1000.0 # in grams
|
|
89
|
+
else:
|
|
90
|
+
weight = None
|
|
91
|
+
if weight is None:
|
|
92
|
+
weight = 75000.0 # estimation
|
|
93
|
+
try:
|
|
94
|
+
# Get Scan time
|
|
95
|
+
scantime = dcm_hhmmss(date_str=get_pydicom_meta_tag(
|
|
96
|
+
dcm_seq=dicom_h, tag=(0x0008, 0x0032), tag_type="str"))
|
|
97
|
+
# Start Time for the Radiopharmaceutical Injection
|
|
98
|
+
injection_time = dcm_hhmmss(date_str=get_pydicom_meta_tag(
|
|
99
|
+
dcm_seq=dicom_h[0x0054, 0x0016][0],
|
|
100
|
+
tag=(0x0018, 0x1072), tag_type="str"))
|
|
101
|
+
# Half Life for Radionuclide
|
|
102
|
+
half_life = get_pydicom_meta_tag(
|
|
103
|
+
dcm_seq=dicom_h[0x0054, 0x0016][0],
|
|
104
|
+
tag=(0x0018, 0x1075), tag_type="float")
|
|
105
|
+
# Total dose injected for Radionuclide
|
|
106
|
+
injected_dose = get_pydicom_meta_tag(
|
|
107
|
+
dcm_seq=dicom_h[0x0054, 0x0016][0],
|
|
108
|
+
tag=(0x0018, 0x1074), tag_type="float")
|
|
109
|
+
# Calculate decay
|
|
110
|
+
decay = np.exp(-np.log(2)*(scantime-injection_time)/half_life)
|
|
111
|
+
# Calculate the dose decayed during procedure
|
|
112
|
+
injected_dose_decay = injected_dose*decay # in Bq
|
|
113
|
+
except KeyError:
|
|
114
|
+
# 90 min waiting time, 15 min preparation
|
|
115
|
+
decay = np.exp(-np.log(2)*(1.75*3600)/6588)
|
|
116
|
+
injected_dose_decay = 420000000 * decay # 420 MBq
|
|
117
|
+
|
|
118
|
+
# Calculate SUV
|
|
119
|
+
suv_map = raw_pet * weight / injected_dose_decay
|
|
120
|
+
|
|
121
|
+
return suv_map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from skimage.exposure import equalize_hist
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def equalization(vol_re: np.ndarray) -> np.ndarray:
|
|
13
|
+
"""Performs histogram equalisation of the ROI imaging intensities.
|
|
14
|
+
|
|
15
|
+
Note:
|
|
16
|
+
This is a pure "what is contained within the roi" equalization. this is
|
|
17
|
+
not influenced by the :func:`user_set_min_val()` used for FBS discretisation.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
vol_re (ndarray): 3D array of the image volume that will be studied with
|
|
21
|
+
NaN value for the excluded voxels (voxels outside the ROI mask).
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
ndarray: Same input image volume but with redistributed intensities.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# AZ: This was made part of the function call
|
|
28
|
+
# n_g = 64
|
|
29
|
+
# This is the default we will use. It means that when using 'FBS',
|
|
30
|
+
# n_q should be chosen wisely such
|
|
31
|
+
# that the total number of grey levels does not exceed 64, for all
|
|
32
|
+
# patients (recommended).
|
|
33
|
+
# This choice was amde by considering that the best equalization
|
|
34
|
+
# performance for "histeq.m" is obtained with low n_g.
|
|
35
|
+
# WARNING: The effective number of grey levels coming out of "histeq.m"
|
|
36
|
+
# may be lower than n_g.
|
|
37
|
+
|
|
38
|
+
# CONSERVE THE INDICES OF THE ROI
|
|
39
|
+
x_gl = np.ravel(vol_re)
|
|
40
|
+
ind_roi = np.where(~np.isnan(vol_re))
|
|
41
|
+
x_gl = x_gl[~np.isnan(x_gl)]
|
|
42
|
+
|
|
43
|
+
# ADJUST RANGE BETWEEN 0 and 1
|
|
44
|
+
min_val = np.min(x_gl)
|
|
45
|
+
max_val = np.max(x_gl)
|
|
46
|
+
x_gl_01 = (x_gl - min_val)/(max_val - min_val)
|
|
47
|
+
|
|
48
|
+
# EQUALIZATION
|
|
49
|
+
# x_gl_equal = equalize_hist(x_gl_01, nbins=n_g)
|
|
50
|
+
# AT THE MOMENT, WE CHOOSE TO USE THE DEFAULT NUMBER OF BINS OF
|
|
51
|
+
# equalize_hist.py (256)
|
|
52
|
+
x_gl_equal = equalize_hist(x_gl_01)
|
|
53
|
+
# RE-ADJUST TO CORRECT RANGE
|
|
54
|
+
x_gl_equal = (x_gl_equal - np.min(x_gl_equal)) / \
|
|
55
|
+
(np.max(x_gl_equal) - np.min(x_gl_equal))
|
|
56
|
+
x_gl_equal = x_gl_equal * (max_val - min_val)
|
|
57
|
+
x_gl_equal = x_gl_equal + min_val
|
|
58
|
+
|
|
59
|
+
# RECONSTRUCT THE VOLUME WITH EQUALIZED VALUES
|
|
60
|
+
vol_equal_re = deepcopy(vol_re)
|
|
61
|
+
|
|
62
|
+
vol_equal_re[ind_roi] = x_gl_equal
|
|
63
|
+
|
|
64
|
+
return vol_equal_re
|
|
65
|
+
|
|
66
|
+
def discretize(vol_re: np.ndarray,
|
|
67
|
+
discr_type: str,
|
|
68
|
+
n_q: float=None,
|
|
69
|
+
user_set_min_val: float=None,
|
|
70
|
+
ivh=False) -> Tuple[np.ndarray, float]:
|
|
71
|
+
"""Quantisizes the image intensities inside the ROI.
|
|
72
|
+
|
|
73
|
+
Note:
|
|
74
|
+
For 'FBS' type, it is assumed that re-segmentation with
|
|
75
|
+
proper range was already performed
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
vol_re (ndarray): 3D array of the image volume that will be studied with
|
|
79
|
+
NaN value for the excluded voxels (voxels outside the ROI mask).
|
|
80
|
+
discr_type (str): Discretisaion approach/type must be: "FBS", "FBN", "FBSequal"
|
|
81
|
+
or "FBNequal".
|
|
82
|
+
n_q (float): Number of bins for FBS algorithm and bin width for FBN algorithm.
|
|
83
|
+
user_set_min_val (float): Minimum of range re-segmentation for FBS discretisation,
|
|
84
|
+
for FBN discretisation, this value has no importance as an argument
|
|
85
|
+
and will not be used.
|
|
86
|
+
ivh (bool): Must be set to True for IVH (Intensity-Volume histogram) features.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
2-element tuple containing
|
|
90
|
+
|
|
91
|
+
- ndarray: Same input image volume but with discretised intensities.
|
|
92
|
+
- float: bin width.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# AZ: NOTE: the "type" variable that appeared in the MATLAB source code
|
|
96
|
+
# matches the name of a standard python function. I have therefore renamed
|
|
97
|
+
# this variable "discr_type"
|
|
98
|
+
|
|
99
|
+
# PARSING ARGUMENTS
|
|
100
|
+
vol_quant_re = deepcopy(vol_re)
|
|
101
|
+
|
|
102
|
+
if n_q is None:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
if not isinstance(n_q, float):
|
|
106
|
+
n_q = float(n_q)
|
|
107
|
+
|
|
108
|
+
if discr_type not in ["FBS", "FBN", "FBSequal", "FBNequal"]:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
"discr_type must either be \"FBS\", \"FBN\", \"FBSequal\" or \"FBNequal\".")
|
|
111
|
+
|
|
112
|
+
# DISCRETISATION
|
|
113
|
+
if discr_type in ["FBS", "FBSequal"]:
|
|
114
|
+
if user_set_min_val:
|
|
115
|
+
min_val = deepcopy(user_set_min_val)
|
|
116
|
+
else:
|
|
117
|
+
min_val = np.nanmin(vol_quant_re)
|
|
118
|
+
else:
|
|
119
|
+
min_val = np.nanmin(vol_quant_re)
|
|
120
|
+
|
|
121
|
+
max_val = np.nanmax(vol_quant_re)
|
|
122
|
+
|
|
123
|
+
if discr_type == "FBS":
|
|
124
|
+
w_b = n_q
|
|
125
|
+
w_d = w_b
|
|
126
|
+
vol_quant_re = np.floor((vol_quant_re - min_val) / w_b) + 1.0
|
|
127
|
+
elif discr_type == "FBN":
|
|
128
|
+
w_b = (max_val - min_val) / n_q
|
|
129
|
+
w_d = 1.0
|
|
130
|
+
vol_quant_re = np.floor(
|
|
131
|
+
n_q * ((vol_quant_re - min_val)/(max_val - min_val))) + 1.0
|
|
132
|
+
vol_quant_re[vol_quant_re == np.nanmax(vol_quant_re)] = n_q
|
|
133
|
+
elif discr_type == "FBSequal":
|
|
134
|
+
w_b = n_q
|
|
135
|
+
w_d = w_b
|
|
136
|
+
vol_quant_re = equalization(vol_quant_re)
|
|
137
|
+
vol_quant_re = np.floor((vol_quant_re - min_val) / w_b) + 1.0
|
|
138
|
+
elif discr_type == "FBNequal":
|
|
139
|
+
w_b = (max_val - min_val) / n_q
|
|
140
|
+
w_d = 1.0
|
|
141
|
+
vol_quant_re = vol_quant_re.astype(np.float32)
|
|
142
|
+
vol_quant_re = equalization(vol_quant_re)
|
|
143
|
+
vol_quant_re = np.floor(
|
|
144
|
+
n_q * ((vol_quant_re - min_val)/(max_val - min_val))) + 1.0
|
|
145
|
+
vol_quant_re[vol_quant_re == np.nanmax(vol_quant_re)] = n_q
|
|
146
|
+
if ivh and discr_type in ["FBS", "FBSequal"]:
|
|
147
|
+
vol_quant_re = min_val + (vol_quant_re - 0.5) * w_b
|
|
148
|
+
|
|
149
|
+
return vol_quant_re, w_d
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from ..MEDscan import MEDscan
|
|
12
|
+
from ..processing.segmentation import compute_box
|
|
13
|
+
from ..utils.image_volume_obj import image_volume_obj
|
|
14
|
+
from ..utils.imref import imref3d, intrinsicToWorld, worldToIntrinsic
|
|
15
|
+
from ..utils.interp3 import interp3
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def interp_volume(
|
|
19
|
+
vol_obj_s: image_volume_obj,
|
|
20
|
+
medscan: MEDscan= None,
|
|
21
|
+
vox_dim: List = None,
|
|
22
|
+
interp_met: str = None,
|
|
23
|
+
round_val: float = None,
|
|
24
|
+
image_type: str = None,
|
|
25
|
+
roi_obj_s: image_volume_obj = None,
|
|
26
|
+
box_string: str = None,
|
|
27
|
+
texture: bool = False) -> image_volume_obj:
|
|
28
|
+
"""3D voxel interpolation on the input volume.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
vol_obj_s (image_volume_obj): Imaging that will be interpolated.
|
|
32
|
+
medscan (object): The MEDscan class object.
|
|
33
|
+
vox_dim (array): Array of the voxel dimension. The following format is used
|
|
34
|
+
[Xin,Yin,Zslice], where Xin and Yin are the X (left to right) and
|
|
35
|
+
Y (bottom to top) IN-PLANE resolutions, and Zslice is the slice spacing,
|
|
36
|
+
no matter the orientation of the volume (i.e. axial , sagittal, coronal).
|
|
37
|
+
interp_met (str): {nearest, linear, spline, cubic} optional, Interpolation method
|
|
38
|
+
round_val (float): Rounding value. Must be between 0 and 1 for ROI interpolation
|
|
39
|
+
and to a power of 10 for Image interpolation.
|
|
40
|
+
image_type (str): 'image' for imaging data interpolation and 'roi' for ROI mask data interpolation.
|
|
41
|
+
roi_obj_s (image_volume_obj): Mask data, will be used to compute a new specific box
|
|
42
|
+
and the new imref3d object for the imaging data.
|
|
43
|
+
box_string (str): Specifies the size if the box containing the ROI
|
|
44
|
+
|
|
45
|
+
- 'full': full imaging data as output.
|
|
46
|
+
- 'box': computes the smallest bounding box.
|
|
47
|
+
- Ex: 'box10': 10 voxels in all three dimensions are added to \
|
|
48
|
+
the smallest bounding box. The number after 'box' defines the \
|
|
49
|
+
number of voxels to add.
|
|
50
|
+
- Ex: '2box': Computes the smallest box and outputs double its \
|
|
51
|
+
size. The number before 'box' defines the multiplication in size.
|
|
52
|
+
texture (bool): If True, the texture voxel spacing of ``MEDscan`` will be used for interpolation.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
ndarray: 3D array of 1's and 0's defining the ROI mask.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
# PARSING ARGUMENTS
|
|
59
|
+
if vox_dim is None:
|
|
60
|
+
if medscan is None:
|
|
61
|
+
return deepcopy(vol_obj_s)
|
|
62
|
+
else:
|
|
63
|
+
if texture:
|
|
64
|
+
vox_dim = medscan.params.process.scale_text
|
|
65
|
+
else:
|
|
66
|
+
vox_dim = medscan.params.process.scale_non_text
|
|
67
|
+
if np.sum(vox_dim) == 0:
|
|
68
|
+
return deepcopy(vol_obj_s)
|
|
69
|
+
if len(vox_dim) == 2:
|
|
70
|
+
two_d = True
|
|
71
|
+
else:
|
|
72
|
+
two_d = False
|
|
73
|
+
|
|
74
|
+
if image_type is None:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
"The type of input image should be specified as \"image\" or \"roi\".")
|
|
77
|
+
elif image_type not in ["image", "roi"]:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"The type of input image should either be \"image\" or \"roi\".")
|
|
80
|
+
elif image_type == "image":
|
|
81
|
+
if not interp_met:
|
|
82
|
+
if medscan:
|
|
83
|
+
interp_met = medscan.params.process.vol_interp
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError("Interpolation method or MEDscan instance should be provided.")
|
|
86
|
+
if interp_met not in ["linear", "cubic", "spline"]:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"Interpolation method for images should either be \"linear\", \"cubic\" or \"spline\".")
|
|
89
|
+
if medscan and not round_val:
|
|
90
|
+
round_val = medscan.params.process.gl_round
|
|
91
|
+
if round_val is not None:
|
|
92
|
+
if np.mod(np.log10(round_val), 1):
|
|
93
|
+
raise ValueError("\"round_val\" should be a power of 10.")
|
|
94
|
+
else:
|
|
95
|
+
if not interp_met:
|
|
96
|
+
if medscan:
|
|
97
|
+
interp_met = medscan.params.process.roi_interp
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError("Interpolation method or MEDscan instance should be provided.")
|
|
100
|
+
if interp_met not in ["nearest", "linear", "cubic"]:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"Interpolation method for images should either be \"nearest\", \"linear\" or \"cubic\".")
|
|
103
|
+
if medscan and not round_val:
|
|
104
|
+
round_val = medscan.params.process.roi_pv
|
|
105
|
+
if round_val is not None:
|
|
106
|
+
if round_val < 0.0 or round_val > 1.0:
|
|
107
|
+
raise ValueError("\"round_val\" must be between 0.0 and 1.0.")
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError("\"round_val\" must be provided for \"roi\".")
|
|
110
|
+
if medscan and not box_string:
|
|
111
|
+
box_string = medscan.params.process.box_string
|
|
112
|
+
if roi_obj_s is None or box_string is None:
|
|
113
|
+
use_box = False
|
|
114
|
+
else:
|
|
115
|
+
use_box = True
|
|
116
|
+
|
|
117
|
+
# --> QUERIED POINTS: NEW INTERPOLATED VOLUME: "q" or "Q".
|
|
118
|
+
# --> SAMPLED POINTS: ORIGINAL VOLUME: "s" or "S".
|
|
119
|
+
# --> Always using XYZ coordinates (unless specifically noted),
|
|
120
|
+
# not MATLAB IJK, so beware!
|
|
121
|
+
|
|
122
|
+
# INITIALIZATION
|
|
123
|
+
res_q = vox_dim
|
|
124
|
+
if two_d:
|
|
125
|
+
# If 2D, the resolution of the slice dimension of he queried volume is
|
|
126
|
+
# set to the same as the sampled volume.
|
|
127
|
+
res_q = np.concatenate((res_q, vol_obj_s.spatialRef.PixelExtentInWorldZ))
|
|
128
|
+
|
|
129
|
+
res_s = np.array([vol_obj_s.spatialRef.PixelExtentInWorldX,
|
|
130
|
+
vol_obj_s.spatialRef.PixelExtentInWorldY,
|
|
131
|
+
vol_obj_s.spatialRef.PixelExtentInWorldZ])
|
|
132
|
+
|
|
133
|
+
if np.array_equal(res_s, res_q):
|
|
134
|
+
return deepcopy(vol_obj_s)
|
|
135
|
+
|
|
136
|
+
spatial_ref_s = vol_obj_s.spatialRef
|
|
137
|
+
extent_s = np.array([spatial_ref_s.ImageExtentInWorldX,
|
|
138
|
+
spatial_ref_s.ImageExtentInWorldY,
|
|
139
|
+
spatial_ref_s.ImageExtentInWorldZ])
|
|
140
|
+
low_limits_s = np.array([spatial_ref_s.XWorldLimits[0],
|
|
141
|
+
spatial_ref_s.YWorldLimits[0],
|
|
142
|
+
spatial_ref_s.ZWorldLimits[0]])
|
|
143
|
+
|
|
144
|
+
# CREATING QUERIED "imref3d" OBJECT CENTERED ON SAMPLED VOLUME
|
|
145
|
+
|
|
146
|
+
# Switching to IJK (matlab) reference frame for "imref3d" computation.
|
|
147
|
+
# Putting a "ceil", according to IBSI standards. This is safer than "round".
|
|
148
|
+
size_q = np.ceil(np.around(np.divide(extent_s, res_q),
|
|
149
|
+
decimals=3)).astype(int).tolist()
|
|
150
|
+
|
|
151
|
+
if two_d:
|
|
152
|
+
# If 2D, forcing the size of the queried volume in the slice dimension
|
|
153
|
+
# to be the same as the sample volume.
|
|
154
|
+
size_q[2] = vol_obj_s.spatialRef.ImageSize[2]
|
|
155
|
+
|
|
156
|
+
spatial_ref_q = imref3d(imageSize=size_q,
|
|
157
|
+
pixelExtentInWorldX=res_q[0],
|
|
158
|
+
pixelExtentInWorldY=res_q[1],
|
|
159
|
+
pixelExtentInWorldZ=res_q[2])
|
|
160
|
+
|
|
161
|
+
extent_q = np.array([spatial_ref_q.ImageExtentInWorldX,
|
|
162
|
+
spatial_ref_q.ImageExtentInWorldY,
|
|
163
|
+
spatial_ref_q.ImageExtentInWorldZ])
|
|
164
|
+
low_limits_q = np.array([spatial_ref_q.XWorldLimits[0],
|
|
165
|
+
spatial_ref_q.YWorldLimits[0],
|
|
166
|
+
spatial_ref_q.ZWorldLimits[0]])
|
|
167
|
+
diff = extent_q - extent_s
|
|
168
|
+
new_low_limits_q = low_limits_s - diff/2
|
|
169
|
+
spatial_ref_q.XWorldLimits = spatial_ref_q.XWorldLimits - \
|
|
170
|
+
(low_limits_q[0] - new_low_limits_q[0])
|
|
171
|
+
spatial_ref_q.YWorldLimits = spatial_ref_q.YWorldLimits - \
|
|
172
|
+
(low_limits_q[1] - new_low_limits_q[1])
|
|
173
|
+
spatial_ref_q.ZWorldLimits = spatial_ref_q.ZWorldLimits - \
|
|
174
|
+
(low_limits_q[2] - new_low_limits_q[2])
|
|
175
|
+
|
|
176
|
+
# REDUCE THE SIZE OF THE VOLUME PRIOR TO INTERPOLATION
|
|
177
|
+
# TODO check that compute_box vol and roi are intended to be the same!
|
|
178
|
+
if use_box:
|
|
179
|
+
_, _, tempSpatialRef = compute_box(
|
|
180
|
+
vol=roi_obj_s.data, roi=roi_obj_s.data, spatial_ref=vol_obj_s.spatialRef,
|
|
181
|
+
box_string=box_string)
|
|
182
|
+
|
|
183
|
+
size_temp = tempSpatialRef.ImageSize
|
|
184
|
+
|
|
185
|
+
# Getting world boundaries (center of voxels) of the new box
|
|
186
|
+
x_bound, y_bound, z_bound = intrinsicToWorld(R=tempSpatialRef,
|
|
187
|
+
xIntrinsic=np.array(
|
|
188
|
+
[0.0, size_temp[0]-1.0]),
|
|
189
|
+
yIntrinsic=np.array(
|
|
190
|
+
[0.0, size_temp[1]-1.0]),
|
|
191
|
+
zIntrinsic=np.array([0.0, size_temp[2]-1.0]))
|
|
192
|
+
|
|
193
|
+
# Getting the image positions of the boundaries of the new box, IN THE
|
|
194
|
+
# FULL QUERIED FRAME OF REFERENCE (centered on the sampled frame of
|
|
195
|
+
# reference).
|
|
196
|
+
x_bound, y_bound, z_bound = worldToIntrinsic(
|
|
197
|
+
R=spatial_ref_q, xWorld=x_bound, yWorld=y_bound, zWorld=z_bound)
|
|
198
|
+
|
|
199
|
+
# Rounding to the nearest image position integer
|
|
200
|
+
x_bound = np.round(x_bound).astype(int)
|
|
201
|
+
y_bound = np.round(y_bound).astype(int)
|
|
202
|
+
z_bound = np.round(z_bound).astype(int)
|
|
203
|
+
|
|
204
|
+
size_q = np.array([x_bound[1] - x_bound[0] + 1, y_bound[1] -
|
|
205
|
+
y_bound[0] + 1, z_bound[1] - z_bound[0] + 1])
|
|
206
|
+
|
|
207
|
+
# Converting back to world positions ion order to correctly define
|
|
208
|
+
# edges of the new box and thus center it onto the full queried
|
|
209
|
+
# reference frame
|
|
210
|
+
x_bound, y_bound, z_bound = intrinsicToWorld(R=spatial_ref_q,
|
|
211
|
+
xIntrinsic=x_bound,
|
|
212
|
+
yIntrinsic=y_bound,
|
|
213
|
+
zIntrinsic=z_bound)
|
|
214
|
+
|
|
215
|
+
new_low_limits_q[0] = x_bound[0] - res_q[0]/2
|
|
216
|
+
new_low_limits_q[1] = y_bound[0] - res_q[1]/2
|
|
217
|
+
new_low_limits_q[2] = z_bound[0] - res_q[2]/2
|
|
218
|
+
|
|
219
|
+
spatial_ref_q = imref3d(imageSize=size_q,
|
|
220
|
+
pixelExtentInWorldX=res_q[0],
|
|
221
|
+
pixelExtentInWorldY=res_q[1],
|
|
222
|
+
pixelExtentInWorldZ=res_q[2])
|
|
223
|
+
|
|
224
|
+
spatial_ref_q.XWorldLimits -= spatial_ref_q.XWorldLimits[0] - \
|
|
225
|
+
new_low_limits_q[0]
|
|
226
|
+
spatial_ref_q.YWorldLimits -= spatial_ref_q.YWorldLimits[0] - \
|
|
227
|
+
new_low_limits_q[1]
|
|
228
|
+
spatial_ref_q.ZWorldLimits -= spatial_ref_q.ZWorldLimits[0] - \
|
|
229
|
+
new_low_limits_q[2]
|
|
230
|
+
|
|
231
|
+
# CREATING QUERIED XYZ POINTS
|
|
232
|
+
x_q = np.arange(size_q[0])
|
|
233
|
+
y_q = np.arange(size_q[1])
|
|
234
|
+
z_q = np.arange(size_q[2])
|
|
235
|
+
x_q, y_q, z_q = np.meshgrid(x_q, y_q, z_q, indexing='ij')
|
|
236
|
+
x_q, y_q, z_q = intrinsicToWorld(
|
|
237
|
+
R=spatial_ref_q, xIntrinsic=x_q, yIntrinsic=y_q, zIntrinsic=z_q)
|
|
238
|
+
|
|
239
|
+
# CONVERTING QUERIED XZY POINTS TO INTRINSIC COORDINATES IN THE SAMPLED
|
|
240
|
+
# REFERENCE FRAME
|
|
241
|
+
x_q, y_q, z_q = worldToIntrinsic(
|
|
242
|
+
R=spatial_ref_s, xWorld=x_q, yWorld=y_q, zWorld=z_q)
|
|
243
|
+
|
|
244
|
+
# INTERPOLATING VOLUME
|
|
245
|
+
data = interp3(v=vol_obj_s.data, x_q=x_q, y_q=y_q, z_q=z_q, method=interp_met)
|
|
246
|
+
vol_obj_q = image_volume_obj(data=data, spatial_ref=spatial_ref_q)
|
|
247
|
+
|
|
248
|
+
# ROUNDING
|
|
249
|
+
if image_type == "image":
|
|
250
|
+
# Grey level rounding for "image" type
|
|
251
|
+
if round_val is not None and (type(round_val) is int or type(round_val) is float):
|
|
252
|
+
# DELETE NEXT LINE WHEN THE RADIOMICS PARAMETER OPTIONS OF
|
|
253
|
+
# interp.glRound ARE FIXED
|
|
254
|
+
round_val = (-np.log10(round_val)).astype(int)
|
|
255
|
+
vol_obj_q.data = np.around(vol_obj_q.data, decimals=round_val)
|
|
256
|
+
else:
|
|
257
|
+
vol_obj_q.data[vol_obj_q.data >= round_val] = 1.0
|
|
258
|
+
vol_obj_q.data[vol_obj_q.data < round_val] = 0.0
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
if medscan:
|
|
262
|
+
if medscan.params.radiomics.scale_name:
|
|
263
|
+
message = f"\n PROBLEM WITH INTERPOLATION:\n {e}"
|
|
264
|
+
logging.error(message)
|
|
265
|
+
medscan.radiomics.image.update(
|
|
266
|
+
{(medscan.params.radiomics.scale_name ): 'ERROR_PROCESSING'})
|
|
267
|
+
else:
|
|
268
|
+
message = f"\n PROBLEM WITH INTERPOLATION:\n {e}"
|
|
269
|
+
logging.error(message)
|
|
270
|
+
medscan.radiomics.image.update(
|
|
271
|
+
{('scale'+(str(medscan.params.process.scale_non_text[0])).replace('.','dot')): 'ERROR_PROCESSING'})
|
|
272
|
+
else:
|
|
273
|
+
print(f"\n PROBLEM WITH INTERPOLATION:\n {e}")
|
|
274
|
+
|
|
275
|
+
return vol_obj_q
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy import ndarray
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def range_re_seg(vol: np.ndarray,
|
|
11
|
+
roi: np.ndarray,
|
|
12
|
+
im_range=None) -> ndarray:
|
|
13
|
+
"""Removes voxels from the intensity mask that fall outside
|
|
14
|
+
the given range (intensities outside the range are set to 0).
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
vol (ndarray): Imaging data.
|
|
18
|
+
roi (ndarray): ROI mask with values of 0's and 1's.
|
|
19
|
+
im_range (ndarray): 1-D array with shape (1,2) of the re-segmentation intensity range.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
ndarray: Intensity mask with intensities within the re-segmentation range.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
if im_range is not None and len(im_range) == 2:
|
|
26
|
+
roi = deepcopy(roi)
|
|
27
|
+
roi[vol < im_range[0]] = 0
|
|
28
|
+
roi[vol > im_range[1]] = 0
|
|
29
|
+
|
|
30
|
+
return roi
|
|
31
|
+
|
|
32
|
+
def outlier_re_seg(vol: np.ndarray,
|
|
33
|
+
roi: np.ndarray,
|
|
34
|
+
outliers="") -> np.ndarray:
|
|
35
|
+
"""Removes voxels with outlier intensities from the given mask
|
|
36
|
+
using the Collewet method.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
vol (ndarray): Imaging data.
|
|
40
|
+
roi (ndarray): ROI mask with values of 0 and 1.
|
|
41
|
+
outliers (str, optional): Algo used to define outliers.
|
|
42
|
+
(For now this methods only implements "Collewet" method).
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
ndarray: An array with values of 0 and 1.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If `outliers` is not "Collewet" or None.
|
|
49
|
+
|
|
50
|
+
Todo:
|
|
51
|
+
* Delete outliers argument or implements others outlining methods.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
if outliers != '':
|
|
55
|
+
roi = deepcopy(roi)
|
|
56
|
+
|
|
57
|
+
if outliers == "Collewet":
|
|
58
|
+
u = np.mean(vol[roi == 1])
|
|
59
|
+
sigma = np.std(vol[roi == 1])
|
|
60
|
+
|
|
61
|
+
roi[vol > (u + 3*sigma)] = 0
|
|
62
|
+
roi[vol < (u - 3*sigma)] = 0
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError("Outlier segmentation not defined.")
|
|
65
|
+
|
|
66
|
+
return roi
|