""" Usage example: python split_labels.py --input_dir /projects/bodymaps/Pedro/data/JHH_liver_segments/ --output_dir /projects/bodymaps/Pedro/data/JHH_liver_segments_sep_labels/ --JHH --parts 3 --part 0 python split_labels.py --input_dir /projects/bodymaps/Data/UFO_OrganSubSegments_nnUNet/subSegments_output --output_dir /projects/bodymaps/Data/UCSF_pseudo_masks_subsegments --parts 3 --part 0 """ import os import glob import argparse import numpy as np import nibabel as nib from concurrent.futures import ProcessPoolExecutor import concurrent from tqdm import tqdm labels = ['background', 'kidney_right', 'kidney_left', 'kidney_lesion', 'kidney_lesion_kidney_right', 'kidney_lesion_kidney_left', 'pancreas', 'pancreatic_lesion', 'liver', 'liver_lesion', 'spleen', 'spleen_lesion', 'colon', 'colon_lesion', 'stomach', 'stomach_lesion', 'duodenum', 'duodenum_lesion', 'common_bile_duct', 'intestine', 'aorta', 'postcava', 'adrenal_gland_left', 'adrenal_gland_right', 'adrenal_lesion', 'adrenal_lesion_adrenal_gland_left', 'adrenal_lesion_adrenal_gland_right', 'gall_bladder', 'gallbladder_lesion', 'gallbladder_lesion_gall_bladder', 'bladder', 'bladder_lesion', 'esophagus', 'esophagus_lesion', 'hepatic_vessel', 'portal_vein_and_splenic_vein', 'lung_left', 'lung_right', 'lung_lesion', 'lung_lesion_lung_left', 'lung_lesion_lung_right', 'prostate', 'prostate_lesion', 'uterus_lesion', 'uterus_lesion_prostate',#this is a bug, our prostate annotation should actually be called prostate/uterus 'femur_left', 'femur_right', 'superior_mesenteric_artery', 'veins', 'bone_lesion', 'breast_lesion',] labels = {name: i for i, name in enumerate(labels, 0)} # Classes we want to output explicitly #out_labels = [ # "liver", # "pancreas", # "kidney_right", # "kidney_left", # "liver_lesion", # "kidney_lesion", # "pancreatic_lesion", # "pancreas_head", # "pancreas_body", # "pancreas_tail" #] # Add "liver_segment_1" through "liver_segment_8" #for i in range(1, 9): # out_labels.append("liver_segment_" + str(i)) out_labels = list(labels.keys()) #out_labels+=['liver','pancreas'] def parse_args(): parser = argparse.ArgumentParser( description="Split segmentation labels into multiple .nii.gz files." ) parser.add_argument( "--input_dir", type=str, required=True, help="Directory containing input .nii.gz files to process.", ) parser.add_argument( "--output_dir", type=str, required=True, help="Directory where the splitted .nii.gz files will be saved.", ) parser.add_argument( "--num_workers", type=int, default=20, help="Number of parallel processes to use for splitting.", ) parser.add_argument( "--restart", action='store_true', help="Overwrites files already saved.", ) parser.add_argument( "--parts", type=int, default=1, help="Total number of parts to split the files into.", ) parser.add_argument( "--part", type=int, default=0, help="Index of the part to process (0-indexed).", ) parser.add_argument( "--JHH", action='store_true', help="Processes only JHH files.", ) return parser.parse_args() def build_mapping(): """ Build the 'mapping' dictionary: Each key is an output class name, each value is a list of label IDs that should be merged into that class. """ # Map from lesion label to the organ sub-parts it can overlap with lesion_to_organs = { 'kidney_lesion': ['kidney_right', 'kidney_left'], 'adrenal_lesion': ['adrenal_gland_left', 'adrenal_gland_right'], 'gallbladder_lesion': ['gall_bladder'], 'lung_lesion': ['lung_left', 'lung_right'], } mapping = {} for key in out_labels: ids = [] for name, val in labels.items(): if name == key or name.startswith(key + "_"): ids.append(val) # Include overlap labels where this organ is the sub-segment # e.g., kidney_lesion_kidney_right should be included for kidney_right for lesion_name, organ_keys in lesion_to_organs.items(): if key in organ_keys: overlap_label = lesion_name + "_" + key if overlap_label in labels: ids.append(labels[overlap_label]) # Special case: "pancreas" also includes "pancreatic" if key == "pancreas": for name, val in labels.items(): if name == "pancreatic_lesion" or name.startswith("pancreatic_lesion_"): ids.append(val) mapping[key] = sorted(set(ids)) return mapping def split_segmentation(input_nifti_path, output_folder, classes_dict): """ Reads a NIfTI segmentation, then saves separate .nii.gz masks for each class (merging labels if necessary). """ print('Splitting', input_nifti_path) seg_img = nib.load(input_nifti_path) # *** Fix: load as float, then cast to int16 (or directly from dataobj) *** seg_data = seg_img.get_fdata().astype(np.int16) # Or use: seg_data = np.asanyarray(seg_img.dataobj).astype(np.int16) # Ensure output_folder exists os.makedirs(output_folder, exist_ok=True) # We'll name output files using: # _.nii.gz base_name = os.path.basename(input_nifti_path) if base_name.endswith(".nii.gz"): base_name = base_name[:-7] else: base_name = os.path.splitext(base_name)[0] # Optionally create a subfolder for each input file this_out_folder = os.path.join(output_folder, base_name, 'predictions') os.makedirs(this_out_folder, exist_ok=True) for class_name, label_list in classes_dict.items(): # Create binary mask: 1 if label in label_list, else 0 mask = np.isin(seg_data, label_list).astype(np.uint8) mask_nifti = nib.Nifti1Image(mask, seg_img.affine, seg_img.header) out_file = os.path.join(this_out_folder, f"{class_name}.nii.gz") nib.save(mask_nifti, out_file) print(f"Finished splitting {input_nifti_path}!") def process_file(input_path, output_dir, classes_dict): """Wrapper to process a single file.""" split_segmentation(input_path, output_dir, classes_dict) def main(): args = parse_args() os.makedirs(args.output_dir, exist_ok=True) # Build the label mapping once classes_dict = build_mapping() print(classes_dict) # Gather all .nii.gz files in the input directory nifti_files = glob.glob(os.path.join(args.input_dir, "*.nii.gz")) if len(nifti_files) == 0: print(f"No NIfTI files found in {args.input_dir}!") return if args.JHH: nifti_files = [f for f in nifti_files if (('BDMAP_A' in f) or ('BDMAP_V' in f))] # remove from the list all files aleady saved if not args.restart: new_nifti_files = [] for fpath in nifti_files: base_name = os.path.basename(fpath) if base_name.endswith(".nii.gz"): base_name = base_name[:-7] else: base_name = os.path.splitext(base_name)[0] # The same logic used in split_segmentation to build the output folder this_out_folder = os.path.join(args.output_dir, base_name, 'predictions') # Check if every class_name has a corresponding .nii.gz all_exist = True for class_name in classes_dict.keys(): out_file = os.path.join(this_out_folder, f"{class_name}.nii.gz") if not os.path.exists(out_file): all_exist = False break # Keep this file if not all masks are present if not all_exist: new_nifti_files.append(fpath) nifti_files = new_nifti_files # Split the nifti_files list into parts and process only the selected part. total_files = len(nifti_files) if args.parts > 1: files_per_part = (total_files + args.parts - 1) // args.parts # round up start_idx = args.part * files_per_part end_idx = min(start_idx + files_per_part, total_files) nifti_files = nifti_files[start_idx:end_idx] print(f"Processing part {args.part+1}/{args.parts}: files {start_idx} to {end_idx-1}") else: print("Processing all files (only one part).") print(f"Total files to process: {len(nifti_files)}") # Use parallel processing with ProcessPoolExecutor(max_workers=args.num_workers) as executor: futures = [] for fpath in nifti_files: futures.append( executor.submit(process_file, fpath, args.output_dir, classes_dict) ) #for future in futures: # future.result() for future in tqdm(concurrent.futures.as_completed(futures), total=len(futures), desc="Processing files"): future.result() print(f"All files processed. Output saved to: {args.output_dir}") if __name__ == "__main__": main() # version 2 - fixed label dict inversion, fixed substring matching in build_mapping (bladder/gall_bladder, prostate/uterus ambiguities)