quoctrong commited on
Commit
eb54f63
·
1 Parent(s): 0891c10

Remove skull stripping and slice filtering from preprocessing pipeline

Browse files
Files changed (1) hide show
  1. backend/app/services/preprocessor.py +21 -100
backend/app/services/preprocessor.py CHANGED
@@ -33,70 +33,20 @@ def _sort_dicom_files(dcm_paths: list) -> list:
33
  # Hoạt động trên Windows, không cần subprocess
34
  # ──────────────────────────────────────────────
35
 
36
- def _skull_strip_slice(img: np.ndarray) -> np.ndarray:
37
- """
38
- Gọt sọ 1 slice 2D bằng cv2 morphological operations.
39
- Tương tự kết quả SynthStrip nhưng ổn định trên mọi nền tảng.
40
-
41
- 1. Normalize 0-255
42
- 2. Otsu threshold → mask nhị phân
43
- 3. Lấy connected component lớn nhất (não)
44
- 4. Morphological closing để lấp lỗ hổng
45
- 5. Mask vào ảnh gốc
46
- """
47
- img_f = img.astype(np.float32)
48
- if img_f.max() - img_f.min() < 1e-6:
49
- return img_f
50
-
51
- # Normalize sang 8-bit
52
- img_8 = cv2.normalize(img_f, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
53
-
54
- # Otsu threshold
55
- blur = cv2.GaussianBlur(img_8, (5, 5), 0)
56
- _, mask = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
57
-
58
- # Lấy connected component lớn nhất
59
- num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
60
- if num_labels <= 1:
61
- return img_f
62
-
63
- # Component 0 là background — lấy component có area lớn nhất (trừ background)
64
- areas = stats[1:, cv2.CC_STAT_AREA]
65
- largest = int(np.argmax(areas)) + 1
66
- brain_mask = (labels == largest).astype(np.uint8) * 255
67
-
68
- # Morphological closing để lấp lỗ trong não
69
- kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
70
- brain_mask = cv2.morphologyEx(brain_mask, cv2.MORPH_CLOSE, kernel)
71
-
72
- # Fill holes bằng flood fill
73
- flood = brain_mask.copy()
74
- h, w = flood.shape
75
- fill_mask = np.zeros((h + 2, w + 2), np.uint8)
76
- cv2.floodFill(flood, fill_mask, (0, 0), 255)
77
- filled = cv2.bitwise_not(flood)
78
- brain_mask = cv2.bitwise_or(brain_mask, filled)
79
-
80
- # Áp mask vào ảnh gốc (float, giữ giá trị gốc)
81
- stripped = img_f * (brain_mask.astype(np.float32) / 255.0)
82
- return stripped
83
-
84
-
85
  # ──────────────────────────────────────────────
86
- # Xử từng lát cắt khớp training Kaggle
87
  # ──────────────────────────────────────────────
88
 
89
- def _process_slice(slice_2d: np.ndarray) -> np.ndarray | None:
90
  """
91
- Chuẩn hóa 1 slice 2D sau khi đã gọt sọ.
92
- Pipeline khớp training:
93
- - Loại blank (range < 1e-6)
94
  - Percentile clip p1–p99
95
- - Normalize 0–255
96
- - Loại uniform (std < 3)
97
  """
98
  img = slice_2d.astype(np.float32)
99
- if img.max() - img.min() < 1e-6:
 
100
  return None
101
 
102
  p_low = np.percentile(img, 1)
@@ -104,18 +54,10 @@ def _process_slice(slice_2d: np.ndarray) -> np.ndarray | None:
104
  img = np.clip(img, p_low, p_high)
105
 
106
  img_range = img.max() - img.min()
107
- img_8bit = ((img - img.min()) / (img_range + 1e-6) * 255).astype(np.uint8)
108
-
109
- # Loại slice đồng nhất
110
- if img_8bit.std() < 3:
111
- return None
112
-
113
- # Loại slice ít mô não: < 8% pixel không phải background (sau skull strip)
114
- # Ngưỡng 8% khớp hành vi SynthStrip — loại các lát đầu/cuối volume ít não
115
- tissue_ratio = np.count_nonzero(img_8bit) / img_8bit.size
116
- if tissue_ratio < 0.08:
117
  return None
118
 
 
119
  return img_8bit
120
 
121
 
@@ -125,12 +67,8 @@ def _process_slice(slice_2d: np.ndarray) -> np.ndarray | None:
125
 
126
  def preprocess_dicom_sequence(dicom_dir: str):
127
  """
128
- Pipeline khớp training Kaggle:
129
- DICOM → pixel arrays → skull strip (cv2) → axial slices (rot90 k=1) → normalize
130
-
131
- Trả về:
132
- valid_slices : list[np.ndarray uint8]
133
- useful_idx : np.ndarray[int]
134
  """
135
  # 1. Đọc và sắp xếp DICOM
136
  files = []
@@ -162,7 +100,7 @@ def preprocess_dicom_sequence(dicom_dir: str):
162
  volume = np.nan_to_num(volume, nan=0.0, posinf=0.0, neginf=0.0)
163
 
164
  n_slices = volume.shape[2]
165
- print(f"[Preprocessor] Volume shape: {volume.shape} | Starting skull strip...")
166
 
167
  # 2. Xử lý từng lát cắt axial
168
  valid_slices = []
@@ -172,10 +110,7 @@ def preprocess_dicom_sequence(dicom_dir: str):
172
  sl = volume[:, :, i]
173
  sl = np.rot90(sl, k=1) # Khớp training: rot90 k=1
174
 
175
- # Skull strip bằng cv2
176
- sl_stripped = _skull_strip_slice(sl)
177
-
178
- processed = _process_slice(sl_stripped)
179
  if processed is not None:
180
  valid_slices.append(processed)
181
  useful_idx.append(i)
@@ -183,14 +118,14 @@ def preprocess_dicom_sequence(dicom_dir: str):
183
  if not valid_slices:
184
  raise ValueError("Không tìm thấy lát cắt MRI hợp lệ nào sau khi xử lý.")
185
 
186
- print(f"[Preprocessor] {len(valid_slices)}/{n_slices} valid slices (cv2 skull-stripped)")
187
  return valid_slices, np.array(useful_idx)
188
 
189
 
190
  def preprocess_nifti_file(nifti_path: str):
191
  """
192
- Tiền xử lý 1 file NIfTI đã gọt sọ sẵn (giống logic training trên Kaggle):
193
- NIfTI -> đọc volume -> contrast clip p1-p99 -> scale 0-255 uint8 -> lọc slice (std < 3)
194
  """
195
  img_nii = nib.load(nifti_path)
196
  data = img_nii.get_fdata().astype(np.float32)
@@ -202,28 +137,14 @@ def preprocess_nifti_file(nifti_path: str):
202
  for i in range(n_slices):
203
  sl = data[:, :, i]
204
 
205
- if np.max(sl) - np.min(sl) < 1e-6:
206
- continue
207
-
208
- p_low = np.percentile(sl, 1)
209
- p_high = np.percentile(sl, 99)
210
- sl = np.clip(sl, p_low, p_high)
211
-
212
- sl_range = sl.max() - sl.min()
213
- if sl_range < 1e-6:
214
- continue
215
-
216
- sl_8bit = ((sl - sl.min()) / sl_range * 255).astype(np.uint8)
217
-
218
- if sl_8bit.std() < 3:
219
- continue
220
-
221
- valid_slices.append(sl_8bit)
222
- useful_idx.append(i)
223
 
224
  if not valid_slices:
225
  raise ValueError("Không tìm thấy lát cắt MRI hợp lệ nào trong file NIfTI.")
226
 
227
- print(f"[Preprocessor] NIfTI processed: {len(valid_slices)}/{n_slices} valid slices")
228
  return valid_slices, np.array(useful_idx)
229
 
 
33
  # Hoạt động trên Windows, không cần subprocess
34
  # ──────────────────────────────────────────────
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  # ──────────────────────────────────────────────
37
+ # Chuẩn hóa lát cắt tối giản (Không gọt sọ, không lọc)
38
  # ──────────────────────────────────────────────
39
 
40
+ def _normalize_slice(slice_2d: np.ndarray) -> np.ndarray | None:
41
  """
42
+ Chuẩn hóa 1 slice 2D không gọt sọ hay lọc lát cắt:
43
+ - Loại slice hoàn toàn trống (range < 1e-6)
 
44
  - Percentile clip p1–p99
45
+ - Normalize 0–255 uint8
 
46
  """
47
  img = slice_2d.astype(np.float32)
48
+ img_range = img.max() - img.min()
49
+ if img_range < 1e-6:
50
  return None
51
 
52
  p_low = np.percentile(img, 1)
 
54
  img = np.clip(img, p_low, p_high)
55
 
56
  img_range = img.max() - img.min()
57
+ if img_range < 1e-6:
 
 
 
 
 
 
 
 
 
58
  return None
59
 
60
+ img_8bit = ((img - img.min()) / img_range * 255).astype(np.uint8)
61
  return img_8bit
62
 
63
 
 
67
 
68
  def preprocess_dicom_sequence(dicom_dir: str):
69
  """
70
+ Tiền xử chuỗi ảnh DICOM (Không gọt sọ, không lọc):
71
+ DICOM → pixel arrays → axial slices (rot90 k=1) → normalize 0-255
 
 
 
 
72
  """
73
  # 1. Đọc và sắp xếp DICOM
74
  files = []
 
100
  volume = np.nan_to_num(volume, nan=0.0, posinf=0.0, neginf=0.0)
101
 
102
  n_slices = volume.shape[2]
103
+ print(f"[Preprocessor] Volume shape: {volume.shape} | Processing slices (No skull strip, No filtering)...")
104
 
105
  # 2. Xử lý từng lát cắt axial
106
  valid_slices = []
 
110
  sl = volume[:, :, i]
111
  sl = np.rot90(sl, k=1) # Khớp training: rot90 k=1
112
 
113
+ processed = _normalize_slice(sl)
 
 
 
114
  if processed is not None:
115
  valid_slices.append(processed)
116
  useful_idx.append(i)
 
118
  if not valid_slices:
119
  raise ValueError("Không tìm thấy lát cắt MRI hợp lệ nào sau khi xử lý.")
120
 
121
+ print(f"[Preprocessor] {len(valid_slices)}/{n_slices} valid slices (Normalized)")
122
  return valid_slices, np.array(useful_idx)
123
 
124
 
125
  def preprocess_nifti_file(nifti_path: str):
126
  """
127
+ Tiền xử lý file NIfTI (Không lọc lát cắt):
128
+ NIfTI -> đọc volume -> contrast clip p1-p99 -> scale 0-255 uint8
129
  """
130
  img_nii = nib.load(nifti_path)
131
  data = img_nii.get_fdata().astype(np.float32)
 
137
  for i in range(n_slices):
138
  sl = data[:, :, i]
139
 
140
+ processed = _normalize_slice(sl)
141
+ if processed is not None:
142
+ valid_slices.append(processed)
143
+ useful_idx.append(i)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  if not valid_slices:
146
  raise ValueError("Không tìm thấy lát cắt MRI hợp lệ nào trong file NIfTI.")
147
 
148
+ print(f"[Preprocessor] NIfTI processed (No filtering): {len(valid_slices)}/{n_slices} valid slices")
149
  return valid_slices, np.array(useful_idx)
150