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.
Files changed (78) hide show
  1. MEDiml/MEDscan.py +1696 -0
  2. MEDiml/__init__.py +21 -0
  3. MEDiml/biomarkers/BatchExtractor.py +806 -0
  4. MEDiml/biomarkers/BatchExtractorTexturalFilters.py +840 -0
  5. MEDiml/biomarkers/__init__.py +16 -0
  6. MEDiml/biomarkers/diagnostics.py +125 -0
  7. MEDiml/biomarkers/get_oriented_bound_box.py +158 -0
  8. MEDiml/biomarkers/glcm.py +1602 -0
  9. MEDiml/biomarkers/gldzm.py +523 -0
  10. MEDiml/biomarkers/glrlm.py +1315 -0
  11. MEDiml/biomarkers/glszm.py +555 -0
  12. MEDiml/biomarkers/int_vol_hist.py +527 -0
  13. MEDiml/biomarkers/intensity_histogram.py +615 -0
  14. MEDiml/biomarkers/local_intensity.py +89 -0
  15. MEDiml/biomarkers/morph.py +1756 -0
  16. MEDiml/biomarkers/ngldm.py +780 -0
  17. MEDiml/biomarkers/ngtdm.py +414 -0
  18. MEDiml/biomarkers/stats.py +373 -0
  19. MEDiml/biomarkers/utils.py +389 -0
  20. MEDiml/filters/TexturalFilter.py +299 -0
  21. MEDiml/filters/__init__.py +9 -0
  22. MEDiml/filters/apply_filter.py +134 -0
  23. MEDiml/filters/gabor.py +215 -0
  24. MEDiml/filters/laws.py +283 -0
  25. MEDiml/filters/log.py +147 -0
  26. MEDiml/filters/mean.py +121 -0
  27. MEDiml/filters/textural_filters_kernels.py +1738 -0
  28. MEDiml/filters/utils.py +107 -0
  29. MEDiml/filters/wavelet.py +237 -0
  30. MEDiml/learning/DataCleaner.py +198 -0
  31. MEDiml/learning/DesignExperiment.py +480 -0
  32. MEDiml/learning/FSR.py +667 -0
  33. MEDiml/learning/Normalization.py +112 -0
  34. MEDiml/learning/RadiomicsLearner.py +714 -0
  35. MEDiml/learning/Results.py +2237 -0
  36. MEDiml/learning/Stats.py +694 -0
  37. MEDiml/learning/__init__.py +10 -0
  38. MEDiml/learning/cleaning_utils.py +107 -0
  39. MEDiml/learning/ml_utils.py +1015 -0
  40. MEDiml/processing/__init__.py +6 -0
  41. MEDiml/processing/compute_suv_map.py +121 -0
  42. MEDiml/processing/discretisation.py +149 -0
  43. MEDiml/processing/interpolation.py +275 -0
  44. MEDiml/processing/resegmentation.py +66 -0
  45. MEDiml/processing/segmentation.py +912 -0
  46. MEDiml/utils/__init__.py +25 -0
  47. MEDiml/utils/batch_patients.py +45 -0
  48. MEDiml/utils/create_radiomics_table.py +131 -0
  49. MEDiml/utils/data_frame_export.py +42 -0
  50. MEDiml/utils/find_process_names.py +16 -0
  51. MEDiml/utils/get_file_paths.py +34 -0
  52. MEDiml/utils/get_full_rad_names.py +21 -0
  53. MEDiml/utils/get_institutions_from_ids.py +16 -0
  54. MEDiml/utils/get_patient_id_from_scan_name.py +22 -0
  55. MEDiml/utils/get_patient_names.py +26 -0
  56. MEDiml/utils/get_radiomic_names.py +27 -0
  57. MEDiml/utils/get_scan_name_from_rad_name.py +22 -0
  58. MEDiml/utils/image_reader_SITK.py +37 -0
  59. MEDiml/utils/image_volume_obj.py +22 -0
  60. MEDiml/utils/imref.py +340 -0
  61. MEDiml/utils/initialize_features_names.py +62 -0
  62. MEDiml/utils/inpolygon.py +159 -0
  63. MEDiml/utils/interp3.py +43 -0
  64. MEDiml/utils/json_utils.py +78 -0
  65. MEDiml/utils/mode.py +31 -0
  66. MEDiml/utils/parse_contour_string.py +58 -0
  67. MEDiml/utils/save_MEDscan.py +30 -0
  68. MEDiml/utils/strfind.py +32 -0
  69. MEDiml/utils/textureTools.py +188 -0
  70. MEDiml/utils/texture_features_names.py +115 -0
  71. MEDiml/utils/write_radiomics_csv.py +47 -0
  72. MEDiml/wrangling/DataManager.py +1724 -0
  73. MEDiml/wrangling/ProcessDICOM.py +512 -0
  74. MEDiml/wrangling/__init__.py +3 -0
  75. mediml-0.9.9.dist-info/LICENSE.md +674 -0
  76. mediml-0.9.9.dist-info/METADATA +232 -0
  77. mediml-0.9.9.dist-info/RECORD +78 -0
  78. mediml-0.9.9.dist-info/WHEEL +4 -0
@@ -0,0 +1,6 @@
1
+ from . import *
2
+ from .compute_suv_map import *
3
+ from .discretisation import *
4
+ from .interpolation import *
5
+ from .resegmentation import *
6
+ from .segmentation import *
@@ -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