inference.postproc.postprocess
Post-processing
Provides functionalities related to computing connected components and postprocessing segmentation predictions by removing small connected components based on thresholds.
1""" 2### Post-processing 3 4Provides functionalities related to computing connected components and postprocessing segmentation predictions by removing small connected components based on thresholds. 5""" 6 7 8from pathlib import Path 9import pandas as pd 10import pickle 11import argparse 12import json 13import numpy as np 14import nibabel as nib 15import cc3d 16import SimpleITK as sitk 17import glob 18import os 19from tqdm import tqdm 20import shutil 21import warnings 22from typing import Any, List, Tuple, Dict 23 24 25def maybe_make_dir(path: str) -> None: 26 """ 27 Creates a directory at the specified path if it does not exist. 28 29 Args: 30 path (str): A string representing the directory path. 31 """ 32 if not os.path.isdir(path): 33 os.makedirs(path) 34 35 36def read_image(path: Path) -> Tuple[sitk.Image, np.ndarray]: 37 """ 38 Reads an image file and returns the SimpleITK image object and its NumPy array. 39 40 Args: 41 path (Path): Path to the image file. 42 43 Returns: 44 Tuple[sitk.Image, np.ndarray]: The SimpleITK image object and its corresponding NumPy array. 45 """ 46 img_sitk = sitk.ReadImage(path) 47 img = sitk.GetArrayFromImage(img_sitk) 48 return img_sitk, img 49 50 51def convert_labels_back_to_BraTS(seg: np.ndarray) -> np.ndarray: 52 """ 53 Converts segmentation labels back to BraTS format. 54 55 Args: 56 seg (np.ndarray): The segmentation array. 57 58 Returns: 59 np.ndarray: The converted segmentation array. 60 """ 61 new_seg = np.zeros_like(seg) 62 new_seg[seg == 1] = 2 63 new_seg[seg == 3] = 3 64 new_seg[seg == 2] = 1 65 return new_seg 66 67 68def get_ratio_ncr_wt(seg: np.ndarray) -> float: 69 """ 70 Calculates the ratio of necrotic and non-enhancing tumor core (NCR) voxels to whole tumor (WT) voxels. 71 72 Args: 73 seg (np.ndarray): The segmentation array. 74 75 Returns: 76 float: The NCR to WT voxel ratio. 77 """ 78 ncr_voxels = np.sum(seg == 1) 79 wt_voxels = np.sum(seg != 0) 80 if wt_voxels == 0: 81 return 1.0 82 return ncr_voxels / wt_voxels 83 84 85def get_ratio_ed_wt(seg: np.ndarray) -> float: 86 """ 87 Calculates the ratio of peritumoral edema (ED) voxels to whole tumor (WT) voxels. 88 89 Args: 90 seg (np.ndarray): The segmentation array. 91 92 Returns: 93 float: The ED to WT voxel ratio. 94 """ 95 ed_voxels = np.sum(seg == 2) 96 wt_voxels = np.sum(seg != 0) 97 if wt_voxels == 0: 98 return 1.0 99 return ed_voxels / wt_voxels 100 101 102def get_ratio_et_wt(seg: np.ndarray) -> float: 103 """ 104 Calculates the ratio of enhancing tumor (ET) voxels to whole tumor (WT) voxels. 105 106 Args: 107 seg (np.ndarray): The segmentation array. 108 109 Returns: 110 float: The ET to WT voxel ratio. 111 """ 112 et_voxels = np.sum(seg == 3) 113 wt_voxels = np.sum(seg != 0) 114 if wt_voxels == 0: 115 return 1.0 116 return et_voxels / wt_voxels 117 118 119def get_ratio_tc_wt(seg: np.ndarray) -> float: 120 """ 121 Calculates the ratio of tumor core (TC) voxels to whole tumor (WT) voxels. 122 123 Args: 124 seg (np.ndarray): The segmentation array. 125 126 Returns: 127 float: The TC to WT voxel ratio. 128 """ 129 tc_voxels = np.sum((seg == 1) & (seg == 3)) 130 wt_voxels = np.sum(seg != 0) 131 if wt_voxels == 0: 132 return 1.0 133 return tc_voxels / wt_voxels 134 135 136def convert_et_to_ncr(seg: np.ndarray) -> np.ndarray: 137 """ 138 Converts enhancing tumor (ET) labels to necrotic and non-enhancing tumor core (NCR). 139 140 Args: 141 seg (np.ndarray): The segmentation array. 142 143 Returns: 144 np.ndarray: The modified segmentation array. 145 """ 146 seg[seg == 3] = 1 147 return seg 148 149 150def convert_ed_to_ncr(seg: np.ndarray) -> np.ndarray: 151 """ 152 Converts peritumoral edema (ED) labels to necrotic and non-enhancing tumor core (NCR). 153 154 Args: 155 seg (np.ndarray): The segmentation array. 156 157 Returns: 158 np.ndarray: The modified segmentation array. 159 """ 160 seg[seg == 2] = 1 161 return seg 162 163 164def get_greatest_label(seg: np.ndarray) -> Tuple[str, float]: 165 """ 166 Determines the label with the highest ratio to whole tumor (WT) voxels. 167 168 Args: 169 seg (np.ndarray): The segmentation array. 170 171 Returns: 172 Tuple[str, float]: The label with the highest ratio and its corresponding ratio value. 173 """ 174 ratios = { 175 "ncr": get_ratio_ncr_wt(seg), 176 "ed": get_ratio_ed_wt(seg), 177 "et": get_ratio_et_wt(seg), 178 # "tc": get_ratio_tc_wt(seg), 179 } 180 greatest_label = max(ratios, key=ratios.get) 181 return greatest_label, ratios[greatest_label] 182 183 184def redefine_et_ed_labels( 185 seg_file: Path, 186 out_file: Path, 187 label: str = "et", 188 ratio: float = 0.0 189) -> np.ndarray: 190 """ 191 Redefines ET or ED labels to NCR in the segmentation based on a specified ratio. 192 193 Args: 194 seg_file (Path): Path to the input segmentation file. 195 out_file (Path): Path to save the postprocessed segmentation file. 196 label (str, optional): Label to optimize ("et" or "ed"). Defaults to "et". 197 ratio (float, optional): Threshold ratio for label optimization. Defaults to 0.0. 198 199 Returns: 200 np.ndarray: The modified segmentation array. 201 """ 202 seg_obj = nib.load(seg_file) 203 seg = seg_obj.get_fdata() 204 if label == "et": 205 ratio_et_wt = get_ratio_et_wt(seg) 206 if ratio_et_wt < ratio: 207 seg = convert_et_to_ncr(seg) 208 elif label == "ed": 209 ratio_ed_wt = get_ratio_ed_wt(seg) 210 if ratio_ed_wt < ratio: 211 seg = convert_ed_to_ncr(seg) 212 new_obj = nib.Nifti1Image(seg.astype(np.int8), seg_obj.affine) 213 nib.save(new_obj, out_file) 214 return seg 215 216 217def postprocess_image( 218 seg: np.ndarray, 219 label: str, 220 ratio: float = 0.04 221) -> np.ndarray: 222 """ 223 Postprocesses the segmentation image by redefining ET or ED labels based on a ratio. 224 225 Args: 226 seg (np.ndarray): The segmentation array. 227 label (str): Label to optimize ("et" or "ed"). 228 ratio (float, optional): Threshold ratio for label optimization. Defaults to 0.04. 229 230 Returns: 231 np.ndarray: The postprocessed segmentation array. 232 """ 233 if label == "et": 234 ratio_et_wt = get_ratio_et_wt(seg) 235 if ratio_et_wt < ratio: 236 seg = convert_et_to_ncr(seg) 237 elif label == "ed": 238 ratio_ed_wt = get_ratio_ed_wt(seg) 239 if ratio_ed_wt < ratio: 240 seg = convert_ed_to_ncr(seg) 241 return seg 242 243 244def save_image( 245 img: np.ndarray, 246 img_sitk: sitk.Image, 247 out_path: Path 248) -> None: 249 """ 250 Saves the NumPy array as a NIfTI image with the original image's metadata. 251 252 Args: 253 img (np.ndarray): The image array to save. 254 img_sitk (sitk.Image): The original SimpleITK image object. 255 out_path (Path): Path to save the new image. 256 """ 257 new_img_sitk = sitk.GetImageFromArray(img) 258 new_img_sitk.CopyInformation(img_sitk) 259 sitk.WriteImage(new_img_sitk, out_path) 260 261 262def postprocess_batch( 263 input_folder: Path, 264 output_folder: Path, 265 label_to_optimize: str, 266 ratio: float = 0.04, 267 convert_to_brats_labels: bool = False 268) -> None: 269 """ 270 Postprocesses a batch of segmentation files by optimizing specified labels. 271 272 Args: 273 input_folder (Path): Path to the input directory containing segmentation files. 274 output_folder (Path): Path to the output directory to save postprocessed files. 275 label_to_optimize (str): Label to optimize ("et" or "ed"). 276 ratio (float, optional): Threshold ratio for label optimization. Defaults to 0.04. 277 convert_to_brats_labels (bool, optional): Whether to convert labels back to BraTS format. Defaults to False. 278 """ 279 seg_list = sorted(glob.glob(os.path.join(input_folder, "*.nii.gz"))) 280 for seg_path in tqdm(seg_list): 281 seg_sitk, seg = read_image(Path(seg_path)) 282 if convert_to_brats_labels: 283 seg = convert_labels_back_to_BraTS(seg) 284 seg_pp = postprocess_image(seg, label_to_optimize, ratio) 285 out_path = output_folder / Path(seg_path).name 286 save_image(seg_pp, seg_sitk, out_path) 287 288 289def get_connected_labels(seg_file: Path) -> Tuple[np.ndarray, np.ndarray, np.ndarray, int, int, int]: 290 """ 291 Identifies connected components for NCR, ED, and ET labels in the segmentation. 292 293 Args: 294 seg_file (Path): Path to the segmentation file. 295 296 Returns: 297 Tuple[np.ndarray, np.ndarray, np.ndarray, int, int, int]: 298 - Connected labels for NCR, ED, ET 299 - Number of connected components for NCR, ED, ET 300 """ 301 seg_obj = nib.load(seg_file) 302 seg = seg_obj.get_fdata() 303 seg_ncr = np.where(seg == 1, 1, 0) 304 seg_ed = np.where(seg == 2, 2, 0) 305 seg_et = np.where(seg == 3, 3, 0) 306 labels_ncr, n_ncr = cc3d.connected_components(seg_ncr, connectivity=26, return_N=True) 307 labels_ed, n_ed = cc3d.connected_components(seg_ed, connectivity=26, return_N=True) 308 labels_et, n_et = cc3d.connected_components(seg_et, connectivity=26, return_N=True) 309 return labels_ncr, labels_ed, labels_et, n_ncr, n_ed, n_et 310 311 312def remove_disconnected( 313 seg_file: Path, 314 out_file: Path, 315 t_ncr: int = 50, 316 t_ed: int = 50, 317 t_et: int = 50 318) -> Tuple[int, int, int, int, int, int]: 319 """ 320 Removes disconnected small components from the segmentation based on thresholds. 321 322 Args: 323 seg_file (Path): Path to the input segmentation file. 324 out_file (Path): Path to save the cleaned segmentation file. 325 t_ncr (int, optional): Threshold for NCR voxel count. Defaults to 50. 326 t_ed (int, optional): Threshold for ED voxel count. Defaults to 50. 327 t_et (int, optional): Threshold for ET voxel count. Defaults to 50. 328 329 Returns: 330 Tuple[int, int, int, int, int, int]: 331 Number of removed NCR, total NCR, removed ED, total ED, removed ET, total ET. 332 """ 333 seg_obj = nib.load(seg_file) 334 labels_ncr, labels_ed, labels_et, n_ncr, n_ed, n_et = get_connected_labels(seg_file) 335 336 # Process NCR 337 vols = [] 338 for i in range(n_ncr): 339 tmp = np.where(labels_ncr == i + 1, 1, 0) 340 vol = np.count_nonzero(tmp) 341 if vol < t_ncr: 342 labels_ncr = np.where(labels_ncr == i + 1, 0, labels_ncr) 343 vols.append(vol) 344 removed_ncr = len(vols) 345 346 # Process ED 347 vols = [] 348 for i in range(n_ed): 349 tmp = np.where(labels_ed == i + 1, 1, 0) 350 vol = np.count_nonzero(tmp) 351 if vol < t_ed: 352 labels_ed = np.where(labels_ed == i + 1, 0, labels_ed) 353 vols.append(vol) 354 removed_ed = len(vols) 355 356 # Process ET 357 vols = [] 358 for i in range(n_et): 359 tmp = np.where(labels_et == i + 1, 1, 0) 360 vol = np.count_nonzero(tmp) 361 if vol < t_et: 362 labels_et = np.where(labels_et == i + 1, 0, labels_et) 363 vols.append(vol) 364 removed_et = len(vols) 365 366 # Combine the cleaned labels 367 new_ncr = np.where(labels_ncr != 0, 1, 0) 368 new_ed = np.where(labels_ed != 0, 2, 0) 369 new_et = np.where(labels_et != 0, 3, 0) 370 new_seg = new_ncr + new_ed + new_et 371 new_obj = nib.Nifti1Image(new_seg.astype(np.int8), seg_obj.affine) 372 nib.save(new_obj, out_file) 373 return removed_ncr, n_ncr, removed_ed, n_ed, removed_et, n_et 374 375 376def remove_disconnected_from_dir( 377 input_dir: Path, 378 output_dir: Path, 379 t_ncr: int = 50, 380 t_ed: int = 50, 381 t_et: int = 50 382) -> Path: 383 """ 384 Removes disconnected small components from all segmentation files in a directory. 385 386 Args: 387 input_dir (Path): Path to the input directory containing segmentation files. 388 output_dir (Path): Path to the output directory to save cleaned segmentation files. 389 t_ncr (int, optional): Threshold for NCR voxel count. Defaults to 50. 390 t_ed (int, optional): Threshold for ED voxel count. Defaults to 50. 391 t_et (int, optional): Threshold for ET voxel count. Defaults to 50. 392 393 Returns: 394 Path: Path to the output directory. 395 """ 396 if not output_dir.exists(): 397 output_dir.mkdir(parents=True) 398 399 for seg1 in input_dir.iterdir(): 400 if seg1.name.endswith('.nii.gz'): 401 casename = seg1.name 402 seg2 = output_dir / casename 403 removed_ncr, n_ncr, removed_ed, n_ed, removed_et, n_et = remove_disconnected( 404 seg1, seg2, t_ncr=t_ncr, t_ed=t_ed, t_et=t_et 405 ) 406 print( 407 f"{casename} removed regions NCR {removed_ncr:03d}/{n_ncr:03d} " 408 f"ED {removed_ed:03d}/{n_ed:03d} ET {removed_et:03d}/{n_et:03d}" 409 ) 410 else: 411 print("Wrong input file!") 412 413 return output_dir
26def maybe_make_dir(path: str) -> None: 27 """ 28 Creates a directory at the specified path if it does not exist. 29 30 Args: 31 path (str): A string representing the directory path. 32 """ 33 if not os.path.isdir(path): 34 os.makedirs(path)
Creates a directory at the specified path if it does not exist.
Args: path (str): A string representing the directory path.
37def read_image(path: Path) -> Tuple[sitk.Image, np.ndarray]: 38 """ 39 Reads an image file and returns the SimpleITK image object and its NumPy array. 40 41 Args: 42 path (Path): Path to the image file. 43 44 Returns: 45 Tuple[sitk.Image, np.ndarray]: The SimpleITK image object and its corresponding NumPy array. 46 """ 47 img_sitk = sitk.ReadImage(path) 48 img = sitk.GetArrayFromImage(img_sitk) 49 return img_sitk, img
Reads an image file and returns the SimpleITK image object and its NumPy array.
Args: path (Path): Path to the image file.
Returns: Tuple[sitk.Image, np.ndarray]: The SimpleITK image object and its corresponding NumPy array.
52def convert_labels_back_to_BraTS(seg: np.ndarray) -> np.ndarray: 53 """ 54 Converts segmentation labels back to BraTS format. 55 56 Args: 57 seg (np.ndarray): The segmentation array. 58 59 Returns: 60 np.ndarray: The converted segmentation array. 61 """ 62 new_seg = np.zeros_like(seg) 63 new_seg[seg == 1] = 2 64 new_seg[seg == 3] = 3 65 new_seg[seg == 2] = 1 66 return new_seg
Converts segmentation labels back to BraTS format.
Args: seg (np.ndarray): The segmentation array.
Returns: np.ndarray: The converted segmentation array.
69def get_ratio_ncr_wt(seg: np.ndarray) -> float: 70 """ 71 Calculates the ratio of necrotic and non-enhancing tumor core (NCR) voxels to whole tumor (WT) voxels. 72 73 Args: 74 seg (np.ndarray): The segmentation array. 75 76 Returns: 77 float: The NCR to WT voxel ratio. 78 """ 79 ncr_voxels = np.sum(seg == 1) 80 wt_voxels = np.sum(seg != 0) 81 if wt_voxels == 0: 82 return 1.0 83 return ncr_voxels / wt_voxels
Calculates the ratio of necrotic and non-enhancing tumor core (NCR) voxels to whole tumor (WT) voxels.
Args: seg (np.ndarray): The segmentation array.
Returns: float: The NCR to WT voxel ratio.
86def get_ratio_ed_wt(seg: np.ndarray) -> float: 87 """ 88 Calculates the ratio of peritumoral edema (ED) voxels to whole tumor (WT) voxels. 89 90 Args: 91 seg (np.ndarray): The segmentation array. 92 93 Returns: 94 float: The ED to WT voxel ratio. 95 """ 96 ed_voxels = np.sum(seg == 2) 97 wt_voxels = np.sum(seg != 0) 98 if wt_voxels == 0: 99 return 1.0 100 return ed_voxels / wt_voxels
Calculates the ratio of peritumoral edema (ED) voxels to whole tumor (WT) voxels.
Args: seg (np.ndarray): The segmentation array.
Returns: float: The ED to WT voxel ratio.
103def get_ratio_et_wt(seg: np.ndarray) -> float: 104 """ 105 Calculates the ratio of enhancing tumor (ET) voxels to whole tumor (WT) voxels. 106 107 Args: 108 seg (np.ndarray): The segmentation array. 109 110 Returns: 111 float: The ET to WT voxel ratio. 112 """ 113 et_voxels = np.sum(seg == 3) 114 wt_voxels = np.sum(seg != 0) 115 if wt_voxels == 0: 116 return 1.0 117 return et_voxels / wt_voxels
Calculates the ratio of enhancing tumor (ET) voxels to whole tumor (WT) voxels.
Args: seg (np.ndarray): The segmentation array.
Returns: float: The ET to WT voxel ratio.
120def get_ratio_tc_wt(seg: np.ndarray) -> float: 121 """ 122 Calculates the ratio of tumor core (TC) voxels to whole tumor (WT) voxels. 123 124 Args: 125 seg (np.ndarray): The segmentation array. 126 127 Returns: 128 float: The TC to WT voxel ratio. 129 """ 130 tc_voxels = np.sum((seg == 1) & (seg == 3)) 131 wt_voxels = np.sum(seg != 0) 132 if wt_voxels == 0: 133 return 1.0 134 return tc_voxels / wt_voxels
Calculates the ratio of tumor core (TC) voxels to whole tumor (WT) voxels.
Args: seg (np.ndarray): The segmentation array.
Returns: float: The TC to WT voxel ratio.
137def convert_et_to_ncr(seg: np.ndarray) -> np.ndarray: 138 """ 139 Converts enhancing tumor (ET) labels to necrotic and non-enhancing tumor core (NCR). 140 141 Args: 142 seg (np.ndarray): The segmentation array. 143 144 Returns: 145 np.ndarray: The modified segmentation array. 146 """ 147 seg[seg == 3] = 1 148 return seg
Converts enhancing tumor (ET) labels to necrotic and non-enhancing tumor core (NCR).
Args: seg (np.ndarray): The segmentation array.
Returns: np.ndarray: The modified segmentation array.
151def convert_ed_to_ncr(seg: np.ndarray) -> np.ndarray: 152 """ 153 Converts peritumoral edema (ED) labels to necrotic and non-enhancing tumor core (NCR). 154 155 Args: 156 seg (np.ndarray): The segmentation array. 157 158 Returns: 159 np.ndarray: The modified segmentation array. 160 """ 161 seg[seg == 2] = 1 162 return seg
Converts peritumoral edema (ED) labels to necrotic and non-enhancing tumor core (NCR).
Args: seg (np.ndarray): The segmentation array.
Returns: np.ndarray: The modified segmentation array.
165def get_greatest_label(seg: np.ndarray) -> Tuple[str, float]: 166 """ 167 Determines the label with the highest ratio to whole tumor (WT) voxels. 168 169 Args: 170 seg (np.ndarray): The segmentation array. 171 172 Returns: 173 Tuple[str, float]: The label with the highest ratio and its corresponding ratio value. 174 """ 175 ratios = { 176 "ncr": get_ratio_ncr_wt(seg), 177 "ed": get_ratio_ed_wt(seg), 178 "et": get_ratio_et_wt(seg), 179 # "tc": get_ratio_tc_wt(seg), 180 } 181 greatest_label = max(ratios, key=ratios.get) 182 return greatest_label, ratios[greatest_label]
Determines the label with the highest ratio to whole tumor (WT) voxels.
Args: seg (np.ndarray): The segmentation array.
Returns: Tuple[str, float]: The label with the highest ratio and its corresponding ratio value.
185def redefine_et_ed_labels( 186 seg_file: Path, 187 out_file: Path, 188 label: str = "et", 189 ratio: float = 0.0 190) -> np.ndarray: 191 """ 192 Redefines ET or ED labels to NCR in the segmentation based on a specified ratio. 193 194 Args: 195 seg_file (Path): Path to the input segmentation file. 196 out_file (Path): Path to save the postprocessed segmentation file. 197 label (str, optional): Label to optimize ("et" or "ed"). Defaults to "et". 198 ratio (float, optional): Threshold ratio for label optimization. Defaults to 0.0. 199 200 Returns: 201 np.ndarray: The modified segmentation array. 202 """ 203 seg_obj = nib.load(seg_file) 204 seg = seg_obj.get_fdata() 205 if label == "et": 206 ratio_et_wt = get_ratio_et_wt(seg) 207 if ratio_et_wt < ratio: 208 seg = convert_et_to_ncr(seg) 209 elif label == "ed": 210 ratio_ed_wt = get_ratio_ed_wt(seg) 211 if ratio_ed_wt < ratio: 212 seg = convert_ed_to_ncr(seg) 213 new_obj = nib.Nifti1Image(seg.astype(np.int8), seg_obj.affine) 214 nib.save(new_obj, out_file) 215 return seg
Redefines ET or ED labels to NCR in the segmentation based on a specified ratio.
Args: seg_file (Path): Path to the input segmentation file. out_file (Path): Path to save the postprocessed segmentation file. label (str, optional): Label to optimize ("et" or "ed"). Defaults to "et". ratio (float, optional): Threshold ratio for label optimization. Defaults to 0.0.
Returns: np.ndarray: The modified segmentation array.
218def postprocess_image( 219 seg: np.ndarray, 220 label: str, 221 ratio: float = 0.04 222) -> np.ndarray: 223 """ 224 Postprocesses the segmentation image by redefining ET or ED labels based on a ratio. 225 226 Args: 227 seg (np.ndarray): The segmentation array. 228 label (str): Label to optimize ("et" or "ed"). 229 ratio (float, optional): Threshold ratio for label optimization. Defaults to 0.04. 230 231 Returns: 232 np.ndarray: The postprocessed segmentation array. 233 """ 234 if label == "et": 235 ratio_et_wt = get_ratio_et_wt(seg) 236 if ratio_et_wt < ratio: 237 seg = convert_et_to_ncr(seg) 238 elif label == "ed": 239 ratio_ed_wt = get_ratio_ed_wt(seg) 240 if ratio_ed_wt < ratio: 241 seg = convert_ed_to_ncr(seg) 242 return seg
Postprocesses the segmentation image by redefining ET or ED labels based on a ratio.
Args: seg (np.ndarray): The segmentation array. label (str): Label to optimize ("et" or "ed"). ratio (float, optional): Threshold ratio for label optimization. Defaults to 0.04.
Returns: np.ndarray: The postprocessed segmentation array.
245def save_image( 246 img: np.ndarray, 247 img_sitk: sitk.Image, 248 out_path: Path 249) -> None: 250 """ 251 Saves the NumPy array as a NIfTI image with the original image's metadata. 252 253 Args: 254 img (np.ndarray): The image array to save. 255 img_sitk (sitk.Image): The original SimpleITK image object. 256 out_path (Path): Path to save the new image. 257 """ 258 new_img_sitk = sitk.GetImageFromArray(img) 259 new_img_sitk.CopyInformation(img_sitk) 260 sitk.WriteImage(new_img_sitk, out_path)
Saves the NumPy array as a NIfTI image with the original image's metadata.
Args: img (np.ndarray): The image array to save. img_sitk (sitk.Image): The original SimpleITK image object. out_path (Path): Path to save the new image.
263def postprocess_batch( 264 input_folder: Path, 265 output_folder: Path, 266 label_to_optimize: str, 267 ratio: float = 0.04, 268 convert_to_brats_labels: bool = False 269) -> None: 270 """ 271 Postprocesses a batch of segmentation files by optimizing specified labels. 272 273 Args: 274 input_folder (Path): Path to the input directory containing segmentation files. 275 output_folder (Path): Path to the output directory to save postprocessed files. 276 label_to_optimize (str): Label to optimize ("et" or "ed"). 277 ratio (float, optional): Threshold ratio for label optimization. Defaults to 0.04. 278 convert_to_brats_labels (bool, optional): Whether to convert labels back to BraTS format. Defaults to False. 279 """ 280 seg_list = sorted(glob.glob(os.path.join(input_folder, "*.nii.gz"))) 281 for seg_path in tqdm(seg_list): 282 seg_sitk, seg = read_image(Path(seg_path)) 283 if convert_to_brats_labels: 284 seg = convert_labels_back_to_BraTS(seg) 285 seg_pp = postprocess_image(seg, label_to_optimize, ratio) 286 out_path = output_folder / Path(seg_path).name 287 save_image(seg_pp, seg_sitk, out_path)
Postprocesses a batch of segmentation files by optimizing specified labels.
Args: input_folder (Path): Path to the input directory containing segmentation files. output_folder (Path): Path to the output directory to save postprocessed files. label_to_optimize (str): Label to optimize ("et" or "ed"). ratio (float, optional): Threshold ratio for label optimization. Defaults to 0.04. convert_to_brats_labels (bool, optional): Whether to convert labels back to BraTS format. Defaults to False.
290def get_connected_labels(seg_file: Path) -> Tuple[np.ndarray, np.ndarray, np.ndarray, int, int, int]: 291 """ 292 Identifies connected components for NCR, ED, and ET labels in the segmentation. 293 294 Args: 295 seg_file (Path): Path to the segmentation file. 296 297 Returns: 298 Tuple[np.ndarray, np.ndarray, np.ndarray, int, int, int]: 299 - Connected labels for NCR, ED, ET 300 - Number of connected components for NCR, ED, ET 301 """ 302 seg_obj = nib.load(seg_file) 303 seg = seg_obj.get_fdata() 304 seg_ncr = np.where(seg == 1, 1, 0) 305 seg_ed = np.where(seg == 2, 2, 0) 306 seg_et = np.where(seg == 3, 3, 0) 307 labels_ncr, n_ncr = cc3d.connected_components(seg_ncr, connectivity=26, return_N=True) 308 labels_ed, n_ed = cc3d.connected_components(seg_ed, connectivity=26, return_N=True) 309 labels_et, n_et = cc3d.connected_components(seg_et, connectivity=26, return_N=True) 310 return labels_ncr, labels_ed, labels_et, n_ncr, n_ed, n_et
Identifies connected components for NCR, ED, and ET labels in the segmentation.
Args: seg_file (Path): Path to the segmentation file.
Returns: Tuple[np.ndarray, np.ndarray, np.ndarray, int, int, int]: - Connected labels for NCR, ED, ET - Number of connected components for NCR, ED, ET
313def remove_disconnected( 314 seg_file: Path, 315 out_file: Path, 316 t_ncr: int = 50, 317 t_ed: int = 50, 318 t_et: int = 50 319) -> Tuple[int, int, int, int, int, int]: 320 """ 321 Removes disconnected small components from the segmentation based on thresholds. 322 323 Args: 324 seg_file (Path): Path to the input segmentation file. 325 out_file (Path): Path to save the cleaned segmentation file. 326 t_ncr (int, optional): Threshold for NCR voxel count. Defaults to 50. 327 t_ed (int, optional): Threshold for ED voxel count. Defaults to 50. 328 t_et (int, optional): Threshold for ET voxel count. Defaults to 50. 329 330 Returns: 331 Tuple[int, int, int, int, int, int]: 332 Number of removed NCR, total NCR, removed ED, total ED, removed ET, total ET. 333 """ 334 seg_obj = nib.load(seg_file) 335 labels_ncr, labels_ed, labels_et, n_ncr, n_ed, n_et = get_connected_labels(seg_file) 336 337 # Process NCR 338 vols = [] 339 for i in range(n_ncr): 340 tmp = np.where(labels_ncr == i + 1, 1, 0) 341 vol = np.count_nonzero(tmp) 342 if vol < t_ncr: 343 labels_ncr = np.where(labels_ncr == i + 1, 0, labels_ncr) 344 vols.append(vol) 345 removed_ncr = len(vols) 346 347 # Process ED 348 vols = [] 349 for i in range(n_ed): 350 tmp = np.where(labels_ed == i + 1, 1, 0) 351 vol = np.count_nonzero(tmp) 352 if vol < t_ed: 353 labels_ed = np.where(labels_ed == i + 1, 0, labels_ed) 354 vols.append(vol) 355 removed_ed = len(vols) 356 357 # Process ET 358 vols = [] 359 for i in range(n_et): 360 tmp = np.where(labels_et == i + 1, 1, 0) 361 vol = np.count_nonzero(tmp) 362 if vol < t_et: 363 labels_et = np.where(labels_et == i + 1, 0, labels_et) 364 vols.append(vol) 365 removed_et = len(vols) 366 367 # Combine the cleaned labels 368 new_ncr = np.where(labels_ncr != 0, 1, 0) 369 new_ed = np.where(labels_ed != 0, 2, 0) 370 new_et = np.where(labels_et != 0, 3, 0) 371 new_seg = new_ncr + new_ed + new_et 372 new_obj = nib.Nifti1Image(new_seg.astype(np.int8), seg_obj.affine) 373 nib.save(new_obj, out_file) 374 return removed_ncr, n_ncr, removed_ed, n_ed, removed_et, n_et
Removes disconnected small components from the segmentation based on thresholds.
Args: seg_file (Path): Path to the input segmentation file. out_file (Path): Path to save the cleaned segmentation file. t_ncr (int, optional): Threshold for NCR voxel count. Defaults to 50. t_ed (int, optional): Threshold for ED voxel count. Defaults to 50. t_et (int, optional): Threshold for ET voxel count. Defaults to 50.
Returns: Tuple[int, int, int, int, int, int]: Number of removed NCR, total NCR, removed ED, total ED, removed ET, total ET.
377def remove_disconnected_from_dir( 378 input_dir: Path, 379 output_dir: Path, 380 t_ncr: int = 50, 381 t_ed: int = 50, 382 t_et: int = 50 383) -> Path: 384 """ 385 Removes disconnected small components from all segmentation files in a directory. 386 387 Args: 388 input_dir (Path): Path to the input directory containing segmentation files. 389 output_dir (Path): Path to the output directory to save cleaned segmentation files. 390 t_ncr (int, optional): Threshold for NCR voxel count. Defaults to 50. 391 t_ed (int, optional): Threshold for ED voxel count. Defaults to 50. 392 t_et (int, optional): Threshold for ET voxel count. Defaults to 50. 393 394 Returns: 395 Path: Path to the output directory. 396 """ 397 if not output_dir.exists(): 398 output_dir.mkdir(parents=True) 399 400 for seg1 in input_dir.iterdir(): 401 if seg1.name.endswith('.nii.gz'): 402 casename = seg1.name 403 seg2 = output_dir / casename 404 removed_ncr, n_ncr, removed_ed, n_ed, removed_et, n_et = remove_disconnected( 405 seg1, seg2, t_ncr=t_ncr, t_ed=t_ed, t_et=t_et 406 ) 407 print( 408 f"{casename} removed regions NCR {removed_ncr:03d}/{n_ncr:03d} " 409 f"ED {removed_ed:03d}/{n_ed:03d} ET {removed_et:03d}/{n_et:03d}" 410 ) 411 else: 412 print("Wrong input file!") 413 414 return output_dir
Removes disconnected small components from all segmentation files in a directory.
Args: input_dir (Path): Path to the input directory containing segmentation files. output_dir (Path): Path to the output directory to save cleaned segmentation files. t_ncr (int, optional): Threshold for NCR voxel count. Defaults to 50. t_ed (int, optional): Threshold for ED voxel count. Defaults to 50. t_et (int, optional): Threshold for ET voxel count. Defaults to 50.
Returns: Path: Path to the output directory.