cropro 0.1.2__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.
cropro/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Public package API for CROPro."""
2
+
3
+ from .config import CropConfig
4
+ from .core import CROPro, run
5
+
6
+ __all__ = ["CROPro", "CropConfig", "run"]
cropro/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
cropro/cli.py ADDED
@@ -0,0 +1,78 @@
1
+ """Command-line interface for CROPro."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from collections.abc import Sequence
7
+ from pathlib import Path
8
+
9
+ from .config import CropConfig
10
+ from .core import CROPro
11
+
12
+
13
+ def _boolean(value: str) -> bool:
14
+ normalized = value.strip().lower()
15
+ if normalized in {"1", "true", "t", "yes", "y", "on"}:
16
+ return True
17
+ if normalized in {"0", "false", "f", "no", "n", "off"}:
18
+ return False
19
+ raise argparse.ArgumentTypeError(f"Expected a boolean value, got {value!r}.")
20
+
21
+
22
+ def build_parser() -> argparse.ArgumentParser:
23
+ parser = argparse.ArgumentParser(
24
+ prog="cropro",
25
+ description="Crop prostate MR images using center, random, or stride strategies.",
26
+ )
27
+ parser.add_argument("--crop_method", default="center", choices=["center", "random", "stride"])
28
+ parser.add_argument("--orig_img_path_t2w", type=Path)
29
+ parser.add_argument("--orig_img_path_adc", type=Path)
30
+ parser.add_argument("--orig_img_path_hbv", type=Path)
31
+ parser.add_argument("--seg_img_path", type=Path)
32
+ parser.add_argument("--seg_img_path_lesion", type=Path)
33
+ parser.add_argument("--prostate_gland_seg_contains_lesion", type=_boolean, default=False)
34
+ parser.add_argument("--tumor_label_level", type=int, default=2)
35
+ parser.add_argument(
36
+ "--patient_status", default="negative", choices=["negative", "positive", "unknown"]
37
+ )
38
+ parser.add_argument("--pixel_spacing", type=float, default=0.5)
39
+ parser.add_argument("--crop_image_size", type=int, default=128)
40
+ parser.add_argument("--sample_number", type=int, default=12)
41
+ parser.add_argument("--crop_stride", type=int, default=32)
42
+ parser.add_argument("--sequence_type", default="T2W", choices=["T2W", "bpMRI"])
43
+ parser.add_argument("--normalized_image", type=_boolean, default=True)
44
+ parser.add_argument("--normalized_vmaxNumber", type=int, default=242)
45
+ parser.add_argument("--do_normalization", type=_boolean, default=False)
46
+ parser.add_argument("--min_percentile", type=float, default=0)
47
+ parser.add_argument("--max_percentile", type=float, default=99.5)
48
+ parser.add_argument(
49
+ "--saved_image_type",
50
+ default="tiff",
51
+ choices=["npy", "jpg", "jpeg", "png", "tiff", "tif", "nmp", "npm"],
52
+ help="Output format (nmp/npm are accepted aliases of npy for backward compatibility).",
53
+ )
54
+ parser.add_argument("--path_to_save", type=Path, default=Path("save_crop"))
55
+ parser.add_argument("--c_min_positive", type=float, default=0.2)
56
+ parser.add_argument("--c_min_negative", type=float, default=1)
57
+ parser.add_argument(
58
+ "--percentage_of_allowed_overlapping_betweeing_gland_lesions_mask",
59
+ type=float,
60
+ default=50.0,
61
+ )
62
+ parser.add_argument("--number_of_slices_to_exclude_from_mask_gland", type=int, default=1)
63
+ parser.add_argument("--keep_all_slice", type=_boolean, default=True)
64
+ parser.add_argument("--random_seed", type=int)
65
+ return parser
66
+
67
+
68
+ def parse_args(argv: Sequence[str] | None = None) -> CropConfig:
69
+ parser = build_parser()
70
+ namespace = parser.parse_args(argv)
71
+ try:
72
+ return CropConfig.from_mapping(vars(namespace))
73
+ except ValueError as exc:
74
+ parser.error(str(exc))
75
+
76
+
77
+ def main(argv: Sequence[str] | None = None) -> None:
78
+ CROPro(parse_args(argv)).run()
cropro/config.py ADDED
@@ -0,0 +1,147 @@
1
+ """Configuration objects for CROPro."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Literal
8
+
9
+ CropMethod = Literal["center", "random", "stride"]
10
+ PatientStatus = Literal["negative", "positive", "unknown"]
11
+ SequenceType = Literal["T2W", "bpMRI"]
12
+ SavedImageType = Literal["npy", "jpg", "jpeg", "png", "tiff", "tif"]
13
+
14
+ VALID_CROP_METHODS = {"center", "random", "stride"}
15
+ VALID_PATIENT_STATUSES = {"negative", "positive", "unknown"}
16
+ VALID_SEQUENCE_TYPES = {"T2W", "bpMRI"}
17
+ VALID_SAVED_IMAGE_TYPES = {"npy", "jpg", "jpeg", "png", "tiff", "tif"}
18
+ SAVED_IMAGE_TYPE_ALIASES = {
19
+ "nmp": "npy",
20
+ "npm": "npy",
21
+ }
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class CropConfig:
26
+ """User-facing crop configuration.
27
+
28
+ The legacy implementation expects an argparse-like object with attributes.
29
+ This dataclass keeps that contract while making Python usage explicit,
30
+ typed, and independent from command-line parsing.
31
+ """
32
+
33
+ crop_method: CropMethod = "center"
34
+ orig_img_path_t2w: str | Path | None = None
35
+ orig_img_path_adc: str | Path | None = None
36
+ orig_img_path_hbv: str | Path | None = None
37
+ seg_img_path: str | Path | None = None
38
+ seg_img_path_lesion: str | Path | None = None
39
+ prostate_gland_seg_contains_lesion: bool = False
40
+ tumor_label_level: int = 2
41
+ patient_status: PatientStatus = "negative"
42
+ pixel_spacing: float = 0.5
43
+ crop_image_size: int = 128
44
+ sample_number: int = 12
45
+ crop_stride: int = 32
46
+ sequence_type: SequenceType = "T2W"
47
+ normalized_image: bool = True
48
+ normalized_vmaxNumber: int = 242
49
+ do_normalization: bool = False
50
+ min_percentile: float = 0
51
+ max_percentile: float = 99.5
52
+ saved_image_type: SavedImageType = "tiff"
53
+ path_to_save: str | Path = "save_crop"
54
+ c_min_positive: float = 0.2
55
+ c_min_negative: float = 1
56
+ percentage_of_allowed_overlapping_betweeing_gland_lesions_mask: float = 50.0
57
+ number_of_slices_to_exclude_from_mask_gland: int = 1
58
+ keep_all_slice: bool = True
59
+ random_seed: int | None = None
60
+
61
+ def __post_init__(self) -> None:
62
+ self.saved_image_type = SAVED_IMAGE_TYPE_ALIASES.get(
63
+ self.saved_image_type, self.saved_image_type
64
+ )
65
+ self._validate()
66
+
67
+ def _validate(self) -> None:
68
+ if self.crop_method not in VALID_CROP_METHODS:
69
+ raise ValueError(f"Invalid crop_method {self.crop_method!r}.")
70
+
71
+ if self.patient_status not in VALID_PATIENT_STATUSES:
72
+ raise ValueError(f"Invalid patient_status {self.patient_status!r}.")
73
+
74
+ if self.sequence_type not in VALID_SEQUENCE_TYPES:
75
+ raise ValueError(f"Invalid sequence_type {self.sequence_type!r}.")
76
+
77
+ if self.saved_image_type not in VALID_SAVED_IMAGE_TYPES:
78
+ raise ValueError(
79
+ f"Invalid saved_image_type {self.saved_image_type!r}. "
80
+ f"Expected one of {sorted(VALID_SAVED_IMAGE_TYPES)}."
81
+ )
82
+
83
+ if self.pixel_spacing <= 0:
84
+ raise ValueError("pixel_spacing must be greater than 0.")
85
+
86
+ if self.crop_image_size <= 0:
87
+ raise ValueError("crop_image_size must be greater than 0.")
88
+
89
+ if self.crop_stride <= 0:
90
+ raise ValueError("crop_stride must be greater than 0.")
91
+
92
+ if self.sample_number <= 0:
93
+ raise ValueError("sample_number must be greater than 0.")
94
+
95
+ if self.min_percentile < 0 or self.max_percentile > 100:
96
+ raise ValueError("Percentiles must be in [0, 100].")
97
+
98
+ if self.min_percentile >= self.max_percentile:
99
+ raise ValueError("min_percentile must be less than max_percentile.")
100
+
101
+ if not (0 <= self.c_min_positive):
102
+ raise ValueError("c_min_positive must be greater than or equal to 0.")
103
+
104
+ if not (0 <= self.c_min_negative):
105
+ raise ValueError("c_min_negative must be greater than or equal to 0.")
106
+
107
+ if not (0 <= self.percentage_of_allowed_overlapping_betweeing_gland_lesions_mask <= 100):
108
+ raise ValueError(
109
+ "percentage_of_allowed_overlapping_betweeing_gland_lesions_mask must be in [0, 100]."
110
+ )
111
+
112
+ if self.number_of_slices_to_exclude_from_mask_gland < 0:
113
+ raise ValueError("number_of_slices_to_exclude_from_mask_gland must be >= 0.")
114
+
115
+ if self.random_seed is not None and self.random_seed < 0:
116
+ raise ValueError("random_seed must be >= 0 when provided.")
117
+
118
+ if self.orig_img_path_t2w is None:
119
+ raise ValueError("orig_img_path_t2w is required.")
120
+
121
+ if self.seg_img_path is None:
122
+ raise ValueError("seg_img_path is required.")
123
+
124
+ if self.sequence_type == "bpMRI":
125
+ if self.orig_img_path_adc is None:
126
+ raise ValueError("orig_img_path_adc is required for bpMRI.")
127
+ if self.orig_img_path_hbv is None:
128
+ raise ValueError("orig_img_path_hbv is required for bpMRI.")
129
+
130
+ if self.patient_status == "positive" and self.seg_img_path_lesion is None:
131
+ raise ValueError("seg_img_path_lesion is required for positive patient_status.")
132
+
133
+ @classmethod
134
+ def from_mapping(cls, values: dict[str, Any]) -> CropConfig:
135
+ valid_keys = cls.__dataclass_fields__.keys()
136
+ clean_values = {key: value for key, value in values.items() if key in valid_keys}
137
+
138
+ saved_image_type = clean_values.get("saved_image_type")
139
+ if saved_image_type is not None:
140
+ clean_values["saved_image_type"] = SAVED_IMAGE_TYPE_ALIASES.get(
141
+ saved_image_type, saved_image_type
142
+ )
143
+
144
+ return cls(**clean_values)
145
+
146
+ def to_dict(self) -> dict[str, Any]:
147
+ return asdict(self)
cropro/core.py ADDED
@@ -0,0 +1,49 @@
1
+ """Library entry points for CROPro."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+
10
+ from .config import CropConfig
11
+
12
+
13
+ class CROPro:
14
+ """Run CROPro cropping from Python code.
15
+
16
+ Parameters may be provided either as a :class:`CropConfig` or as keyword
17
+ arguments matching ``CropConfig`` fields.
18
+ """
19
+
20
+ def __init__(self, config: CropConfig | None = None, **overrides: Any) -> None:
21
+ if config is not None and overrides:
22
+ values = config.to_dict()
23
+ values.update({key: value for key, value in overrides.items() if value is not None})
24
+ self.config = CropConfig.from_mapping(values)
25
+ elif config is not None:
26
+ self.config = config
27
+ else:
28
+ self.config = CropConfig.from_mapping(
29
+ {key: value for key, value in overrides.items() if value is not None}
30
+ )
31
+
32
+ self.arg = self.config
33
+
34
+ if self.config.random_seed is not None:
35
+ random.seed(self.config.random_seed)
36
+ np.random.seed(self.config.random_seed)
37
+
38
+ def cropro(self) -> None:
39
+ self.run()
40
+
41
+ def run(self) -> None:
42
+ from cropro.cropping.patientCropC import patientCropC
43
+
44
+ patient_crop = patientCropC(self.arg)
45
+ patient_crop.patientCrop()
46
+
47
+
48
+ def run(config: CropConfig | None = None, **overrides: Any) -> None:
49
+ CROPro(config=config, **overrides).run()
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,304 @@
1
+ from pathlib import Path
2
+
3
+ import cv2
4
+ import numpy as np
5
+ import SimpleITK as sitk
6
+
7
+ from .negativeCenterC import negativeCenterC
8
+ from .negativeRandomC import negativeRandomC
9
+ from .negativeStrideC import negativeStrideC
10
+ from .positiveCenterC import positiveCenterC
11
+ from .positiveRandomC import positiveRandomC
12
+ from .positiveStrideC import positiveStrideC
13
+
14
+
15
+ class croppingCrontrollerClass:
16
+ """
17
+ This class controls the cropping.
18
+
19
+ Methods
20
+ -------
21
+ load_resample_itk(self, filename, is_mask))
22
+ sample_size_calculation(self,number_of_voxel)
23
+ cropTechniqueControler(self)
24
+ crop_and_save(self, orig_img_path_t2w, slice_number_correct, slice_name, name_file, pathToSave_same_as_dataset_structure , prostate_gland_arr_slice, prostate_lesion_arr_slice,patient_name, caseHealthyBoolean, seg_path=None):
25
+
26
+ """
27
+
28
+ def __init__(self, arg):
29
+ self.arg = arg
30
+
31
+ def _study_token(self):
32
+ study_id = getattr(self, "study_id", None)
33
+ if study_id in (None, ""):
34
+ return "unknown"
35
+ return str(study_id)
36
+
37
+ def log_crop_event(self, event, area_voxels=None, overlap_percentage=None):
38
+ message = f"{event} | study={self._study_token()}"
39
+ if area_voxels is not None:
40
+ message = f"{message} | area_voxels={int(area_voxels)}"
41
+ if overlap_percentage is not None:
42
+ message = f"{message} | overlap_pct={float(overlap_percentage):.1f}"
43
+ print(message)
44
+
45
+ def calculate_boundRect(self):
46
+ """
47
+ This fuction finds an approximate rectangle around the binary image. This is used only for center cropping
48
+ """
49
+ for i, curve_contours in enumerate(self.biggestAreaContour_array):
50
+ self.i = i
51
+ self.curve_contours = curve_contours
52
+ epsilon = 3 # approximation accuracy parameter. This is the maximum distance between the original curve and its approximation.
53
+ closed = True # For true the approximated curve is closed, first and last vertices are connected.
54
+ # approxPolyDP: aims to approximate a contour (shape) to a different shape with less number of vertices.
55
+ self.contours_poly[self.i] = cv2.approxPolyDP(self.curve_contours, epsilon, closed)
56
+ # boundingRect: aims to daw an approximate rectangle around the binary image.
57
+ self.boundRect[self.i] = cv2.boundingRect(self.contours_poly[self.i])
58
+
59
+ def load_resample_itk(self, filename, is_mask=False):
60
+ """
61
+ This fuction is used to load a 3D itk image or segmentation(prostate gland/lesions)
62
+ and resample with a new pixel spacing
63
+
64
+ Parameters
65
+ ----------
66
+ filename : str
67
+ The filename is the path to the segmentation.
68
+ is_mask = Boolean
69
+ If the file is segmentation or normal 3D image
70
+
71
+ Returns
72
+ -------
73
+
74
+ GetArrayFromImage
75
+ The resampled images (array) for each slice
76
+
77
+ """
78
+ filename = Path(filename)
79
+ itk_image = sitk.ReadImage(str(filename))
80
+ original_spacing = itk_image.GetSpacing()
81
+ original_size = itk_image.GetSize()
82
+ out_spacing = [self.arg.pixel_spacing, self.arg.pixel_spacing, original_spacing[2]]
83
+
84
+ out_size = [
85
+ int(np.round(original_size[0] * (original_spacing[0] / out_spacing[0]))),
86
+ int(np.round(original_size[1] * (original_spacing[1] / out_spacing[1]))),
87
+ int(np.round(original_size[2] * (original_spacing[2] / original_spacing[2]))),
88
+ ]
89
+
90
+ resample = sitk.ResampleImageFilter()
91
+ resample.SetOutputSpacing(out_spacing)
92
+ resample.SetSize(out_size)
93
+ resample.SetOutputDirection(itk_image.GetDirection())
94
+ resample.SetOutputOrigin(itk_image.GetOrigin())
95
+ resample.SetTransform(sitk.Transform())
96
+ resample.SetDefaultPixelValue(itk_image.GetPixelIDValue())
97
+
98
+ if is_mask:
99
+ resample.SetInterpolator(sitk.sitkNearestNeighbor)
100
+ else:
101
+ resample.SetInterpolator(sitk.sitkBSpline)
102
+ resampled_sitk_img = resample.Execute(itk_image)
103
+ return sitk.GetArrayFromImage(resampled_sitk_img)
104
+
105
+ def sample_size_calculation(self, number_of_voxel):
106
+ """
107
+ This fuction is used to calculate the size of random selected images to be cropped. This is when the random
108
+ technique is used.
109
+
110
+ Parameters
111
+ ----------
112
+ number_of_voxel : int
113
+ The number of voxels for a specific slice.
114
+ Returns
115
+ -------
116
+ sample_size: int
117
+ The sample size
118
+
119
+ """
120
+ sample_size = int(
121
+ np.divide(number_of_voxel, self.arg.crop_image_size**2) * float(self.arg.sample_number)
122
+ )
123
+ if sample_size < 1:
124
+ sample_size = 1
125
+ return sample_size
126
+ else:
127
+ return sample_size
128
+
129
+ def cropping_technique_selection(self):
130
+ """
131
+ This fuction is used to calculate the biggest area of contour.
132
+
133
+ Returns
134
+ -------
135
+ biggestAreaContour: numpy.ndarray
136
+
137
+ """
138
+ # if there is an area
139
+ if len(self.biggestAreaContour_array) == 1:
140
+ # choose the crop method and call the class related to it.
141
+ if self.arg.crop_method == "random":
142
+ if self.arg.patient_status == "negative" or self.arg.patient_status == "unknown":
143
+ negativeRandomC.negativeRandom(self)
144
+
145
+ elif self.arg.patient_status == "positive":
146
+ positiveRandomC.positiveRandom(self)
147
+
148
+ elif self.arg.crop_method == "stride":
149
+ if self.arg.patient_status == "negative" or self.arg.patient_status == "unknown":
150
+ negativeStrideC.negativeStride(self)
151
+
152
+ elif self.arg.patient_status == "positive":
153
+ positiveStrideC.positiveStride(self)
154
+
155
+ elif self.arg.crop_method == "center":
156
+ if self.arg.patient_status == "negative" or self.arg.patient_status == "unknown":
157
+ negativeCenterC.negativeCenter(self)
158
+ else:
159
+ positiveCenterC.positiveCenter(self)
160
+
161
+ def calculate_biggest_area_of_contour(self):
162
+ """
163
+ This fuction is used to calculate the biggest area of contour.
164
+
165
+ Returns
166
+ -------
167
+ biggestAreaContour: numpy.ndarray
168
+
169
+ """
170
+ self.canny_output = cv2.Canny(self.src_gray_blurred_whole_prostate, 100, 100 * 2)
171
+ self.contours, _ = cv2.findContours(
172
+ self.canny_output, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
173
+ )
174
+ self.contours_poly = [None] * len(self.contours)
175
+ self.boundRect = [None] * len(self.contours)
176
+ self.biggestAreaContour = max(self.contours, key=cv2.contourArea)
177
+ biggestAreaContour_array = np.array([self.biggestAreaContour])
178
+ return biggestAreaContour_array
179
+
180
+ def cropTechniqueControler(self):
181
+ """
182
+ This fuction is used to calculate the size of random selected images to be cropped. This is when the random
183
+ technique is used.
184
+ """
185
+ self.biggestAreaContour_array = self.calculate_biggest_area_of_contour()
186
+ self.drawing = np.zeros(
187
+ (self.canny_output.shape[0], self.canny_output.shape[1]), dtype=np.uint8
188
+ )
189
+
190
+ self.cropping_technique_selection()
191
+
192
+ def crop_and_save(
193
+ self,
194
+ orig_img_path_t2w,
195
+ slice_number_correct,
196
+ slice_name,
197
+ name_file,
198
+ pathToSave_same_as_dataset_structure,
199
+ prostate_gland_arr_slice,
200
+ prostate_lesion_arr_slice,
201
+ patient_id,
202
+ study_id,
203
+ caseHealthyBoolean,
204
+ seg_path=None,
205
+ ):
206
+ self.orig_img_path_t2w = orig_img_path_t2w
207
+ self.slice_number = slice_number_correct
208
+ self.slice_name = slice_name
209
+ self.name_file = name_file
210
+ self.pathToSave_same_as_dataset_structure = pathToSave_same_as_dataset_structure
211
+ self.prostate_gland_arr_slice = prostate_gland_arr_slice
212
+ self.prostate_lesion_arr_slice = prostate_lesion_arr_slice
213
+ self.patient_id = patient_id
214
+ self.study_id = study_id
215
+ self.caseHealthyBoolean = caseHealthyBoolean
216
+ self.seg_path = seg_path
217
+
218
+ def threshold_check(self, labels, IncludesWholeProstateBoolean):
219
+ thresh_number_of_voxels = round(1 / ((self.arg.pixel_spacing / 10) ** 2))
220
+ number_of_voxel = labels[0].size
221
+ if IncludesWholeProstateBoolean:
222
+ minimum_newthresh_number_of_voxel = int(
223
+ self.arg.c_min_negative * thresh_number_of_voxels
224
+ )
225
+ else:
226
+ minimum_newthresh_number_of_voxel = int(
227
+ self.arg.c_min_positive * thresh_number_of_voxels
228
+ )
229
+ if number_of_voxel >= minimum_newthresh_number_of_voxel:
230
+ return True, number_of_voxel, minimum_newthresh_number_of_voxel
231
+ else:
232
+ return False, number_of_voxel, minimum_newthresh_number_of_voxel
233
+
234
+ if caseHealthyBoolean:
235
+ self.labels = np.where(self.prostate_gland_arr_slice)
236
+ (
237
+ self.threshold_checkBoolean,
238
+ self.number_of_voxel,
239
+ self.minimum_newthresh_number_of_voxel,
240
+ ) = threshold_check(self, self.labels, caseHealthyBoolean)
241
+ if self.threshold_checkBoolean:
242
+ self.prostate_gland_arr_slice = np.array(
243
+ self.prostate_gland_arr_slice * 255, dtype=np.uint8
244
+ )
245
+ self.src_gray_blurred_whole_prostate = cv2.blur(
246
+ self.prostate_gland_arr_slice, (3, 3)
247
+ )
248
+ self.canny_output = cv2.Canny(self.src_gray_blurred_whole_prostate, 100, 100 * 2)
249
+ contours, _hierarchy = cv2.findContours(
250
+ self.canny_output, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
251
+ )
252
+ if len(contours) > 0:
253
+ self.cropTechniqueControler()
254
+ else:
255
+ if self.arg.prostate_gland_seg_contains_lesion:
256
+ self.labels_tumor_area = np.where(
257
+ self.prostate_gland_arr_slice >= self.arg.tumor_label_level
258
+ )
259
+ else:
260
+ self.labels_tumor_area = np.where(
261
+ self.prostate_lesion_arr_slice >= self.arg.tumor_label_level
262
+ )
263
+
264
+ self.labels_whole_prostate = np.where(self.prostate_gland_arr_slice)
265
+ (
266
+ self.threshold_checkBoolean_tumour_area,
267
+ self.number_of_voxel,
268
+ self.minimum_newthresh_number_of_voxel,
269
+ ) = threshold_check(
270
+ self, self.labels_tumor_area, IncludesWholeProstateBoolean=caseHealthyBoolean
271
+ )
272
+
273
+ if self.threshold_checkBoolean_tumour_area:
274
+ if self.arg.prostate_gland_seg_contains_lesion:
275
+ _, self.image_source_original_tumour = cv2.threshold(
276
+ self.prostate_gland_arr_slice,
277
+ int(self.arg.tumor_label_level - 1),
278
+ 10,
279
+ cv2.THRESH_BINARY,
280
+ )
281
+ _, self.image_source_original_whole_prostate = cv2.threshold(
282
+ self.prostate_gland_arr_slice, 0, 1, cv2.THRESH_BINARY
283
+ )
284
+ else:
285
+ self.image_source_original_tumour = self.prostate_lesion_arr_slice
286
+ self.image_source_original_whole_prostate = self.prostate_gland_arr_slice
287
+
288
+ self.image_source_original_whole_prostate = np.array(
289
+ self.image_source_original_whole_prostate * 255, dtype=np.uint8
290
+ )
291
+ self.image_source_original_tumour = np.array(
292
+ self.image_source_original_tumour * 255, dtype=np.uint8
293
+ )
294
+ self.src_gray_blurred_whole_prostate = cv2.blur(
295
+ self.image_source_original_whole_prostate, (3, 3)
296
+ )
297
+ self.canny_output = cv2.Canny(self.src_gray_blurred_whole_prostate, 100, 100 * 2)
298
+ self.contours, _rethierarchy = cv2.findContours(
299
+ self.canny_output, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
300
+ )
301
+ if len(self.contours) > 0:
302
+ self.cropTechniqueControler()
303
+ else:
304
+ self.log_crop_event("Crop skipped: lesion threshold not met", self.number_of_voxel)