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 +6 -0
- cropro/__main__.py +4 -0
- cropro/cli.py +78 -0
- cropro/config.py +147 -0
- cropro/core.py +49 -0
- cropro/cropping/__init__.py +1 -0
- cropro/cropping/croppingCrontrollerClass.py +304 -0
- cropro/cropping/negativeCenterC.py +118 -0
- cropro/cropping/negativeRandomC.py +78 -0
- cropro/cropping/negativeStrideC.py +136 -0
- cropro/cropping/patientCropC.py +179 -0
- cropro/cropping/positiveCenterC.py +144 -0
- cropro/cropping/positiveRandomC.py +153 -0
- cropro/cropping/positiveStrideC.py +192 -0
- cropro/cropping/saveFilesC.py +400 -0
- cropro-0.1.2.dist-info/METADATA +601 -0
- cropro-0.1.2.dist-info/RECORD +21 -0
- cropro-0.1.2.dist-info/WHEEL +5 -0
- cropro-0.1.2.dist-info/entry_points.txt +2 -0
- cropro-0.1.2.dist-info/licenses/LICENSE +21 -0
- cropro-0.1.2.dist-info/top_level.txt +1 -0
cropro/__init__.py
ADDED
cropro/__main__.py
ADDED
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)
|