Source code for NiftiMaskThresholding_multiprocessing

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
The `MASK_THRESHOLDING` module removes voxels from a mask if their intensity falls outside a specified range, allowing for selective masking based on intensity.

Options
-------

The `MASK_THRESHOLDING` module supports the following options:

- **verbose**: Enable verbose output for more detailed process information (default: False).
- **timer**: Enable a timer to record execution time (default: False).
- **inputFolder**: Path to the folder containing the input images.
- **outputFolder**: Path to save the thresholded mask files.
- **outputFolderSuffix**: Adds a suffix to the `inputFolder` name to create the output folder.
- **log**: Path to a log file for saving detailed output information.
- **new_log_file**: Create a new log file, overwriting any existing file with the same name.
- **skip**: Path to a file listing subfolders in `inputFolder` to exclude from processing.
- **multiprocessing**: Specify the number of CPU cores to use for parallel processing.
- **image_filename**: Name of the image file to read for thresholding.
- **mask_filename**: Name of the mask file to apply the threshold.
- **suffix_name**: Suffix to append to the filename of the new thresholded mask.
- **min_threshold**: Minimum intensity threshold; voxels with lower intensity will be removed from the mask.
- **max_threshold**: Maximum intensity threshold; voxels with higher intensity will be removed from the mask.

Example Usage
-------------

The following example demonstrates how to use the `MASK_THRESHOLDING` module:

.. code-block:: bash

    MASK_THRESHOLDING:
    {
        inputFolder: /path/to/NIFTI_folder
        image_filename: img.nii.gz
        mask_filename: msk.nii.gz
        suffix_name: no_fat
        min_threshold: 0
        log: /path/to/logs/mask_thr.log
    }

In this example:

- **inputFolder**: Specifies the folder containing NIfTI files for thresholding.
- **image_filename** and **mask_filename**: Define the image and mask files to apply the thresholding.
- **suffix_name**: Adds the suffix "no_fat" to the thresholded mask filename.
- **min_threshold**: Sets the minimum voxel intensity to include.
- **log**: Specifies the log file path to record processing details.
"""


# Remove voxels from masks based on specified intensity thresholds.
# Voxels with intensities outside the defined min and max thresholds are removed.
#
#Remove values under and/or upper a threshold from masks
#
# Usage:
#     NiftiMaskThresholding_multiprocessing.py -i <inputFolder> -o <outputFolder> [options]
#
# Options:
#   -h, --help                       Show this help message and exit
#   -v, --verbose                    Enable verbose output (default: False)
#   -i, --inputFolder <inputFolder>  Input folder containing NIfTI images
#   -o, --outputFolder <outputFolder> Output folder to save thresholded masks
#   -e <suffix>                      Suffix for the output mask filenames (default: "mask_thresholding")
#       --min_thr <min threshold>    Minimum intensity threshold; remove voxels below this value
#       --max_thr <max threshold>    Maximum intensity threshold; remove voxels above this value
#       --img_name <image name>      Name of the image file to apply thresholding (default: "img.nii.gz")
#       --mask_name <mask name>      Name of the mask file to apply thresholding (default: "msk.nii.gz")
#   -S <skip file path>              Path to a file listing filenames to skip
#       --include <include file path> Path to a file listing filenames to include (default: include all)
#       --log <log file path>        Redirect stdout to a log file
#       --new_log                    Overwrite an existing log file if it exists
#   -j, --n_jobs <number of jobs>    Number of simultaneous jobs (default: 1)
#
# Help:
#     NiftiMaskThresholding_multiprocessing.py -h

import sys, getopt, os
import glob
import multiprocessing
from tqdm import tqdm
from datetime import datetime
import shutil
import nibabel as nib
import numpy as np
from utils import eprint
from utils import hprint_msg_box
from utils import hprint
from utils import format_list_multiline

[docs] def main(argv): inpath = '' outpath = '' verbose = False n_jobs=1 #nb of CPUs dif_path=False #update to true if inpath != outpath, used to know if image need to be copied in a new folder min_thr=sys.float_info.min max_thr=sys.float_info.max img_name="img.nii.gz" mask_name="msk.nii.gz" suffix = "mask_thresholding" skip_file_name='' skip_files=[] include_file_name='' include_files=[] log = '' new_log = False try: opts, args = getopt.getopt(argv, "hvi:o:j:e:S:M:I:",["log=","new_log","inputFolder=","outputFolder=","verbose","help","n_jobs=","--img_name=","--mask_name=","max_thr=","min_thr=","skip=","include="]) except getopt.GetoptError: print("NiftiMaskThresholding_multiprocessing.py [-h|--help][-v|--verbose][-i|--inputFolder <inputfolder>][-o|--outputFolder <outfolder>][-e <suffix name for output imgage and mask>][--min_thr <min threshold>][--max_thr <max threshold>][-I|--img_name <image name>][-M|--mask_name <mask name>][-j|--n_jobs <number of simultaneous jobs>]\n") sys.exit(2) for opt,arg in opts: if opt in ("-h", "--help"): print("NAME") print("\tNiftiMaskThresholding_mutiprocessing.py\n") print("SYNOPSIS") print("\tNiftiMaskThresholding_multiprocessing.py [-h|--help][-v|--verbose][-i|--inputFolder <inputfolder>][-o|--outputFolder <outfolder>][-e <suffix name for output imgage and mask>][--min_thr <min threshold>][--max_thr <max threshold>][-I|--img_name <image name>][-M|--mask_name <mask name>][-j|--n_jobs <number of simultaneous jobs>]\n") print("DESRIPTION") print("\tRemove values under and/or upper a threshold from masks\n") print("OPTIONS") print("\t -h, --help: print this help page") print("\t -v, --verbose: False by default") print("\t -i, --inputFolder: input folder with NIfTI images") print("\t -o, --outFolder: output folder to save Resampled NIfTI images") print("\t -e: suffix to use in the name for output masks (default \"mask_thresholding\")") print("\t -m, --mask_name: name of the mask to crop (default: msk.nii.gz)") print("\t --min_thr, remove voxel values under min_thr from the mask") print("\t --max_thr: remove voxel values upper max_thr from the mask") print("\t -S, --skip: path to file with filenames to skip whem processing resampling") print("\t --include: path to file with filenames to include (all files included by default)") print("\t --log: redirect stdout to a log file") print("\t --new_log: overwrite previous log file") print("\t -j, --n_jobs: number of simultaneous jobs (default:1)") sys.exit() elif opt in ("-i", "--inputFolder"): inpath = arg elif opt in ("-o", "--outputFolder"): outpath = arg elif opt in ("-v", "--verbose"): verbose = True elif opt in ("-j", "--n_jobs"): n_jobs= int(arg) elif opt in ("-M, --mask_name"): mask_name= arg elif opt in ("-I, --img_name"): img_name= arg elif opt in ("--min_thr"): min_thr= arg elif opt in ("--max_thr"): max_thr= arg elif opt in ("-e"): suffix= arg elif opt in ("--log"): log= arg elif opt in ("--new_log"): new_log= True elif opt in ("-S","--skip"): skip_file_name= arg elif opt in ("--include"): include_file_name= arg if log != '': if new_log: f = open(log,'w+') else: f = open(log,'a+') sys.stdout = f if skip_file_name != '': try: file= open(skip_file_name, 'r') skip_files = file.read().splitlines() except: print("\033[31mERROR! Unable to read the skip file\033[0m") if include_file_name != '': try: file= open(include_file_name, 'r') include_files = file.read().splitlines() except: print("\033[31mERROR! Unable to read the include file\033[0m",flush=True) if outpath =='': outpath = inpath if verbose: print("\033[33mWARNING! No output folder specify, results will be saved in the input folder\033[0m",flush=True) elif outpath==inpath: if verbose: print("\033[33mWARNING! Input and output paths are identicals, new masks will be saved in the input folder\033[0m",flush=True) else: dif_path=True if inpath == '': print("\033[31mERROR! No input folder specify\033[0m",flush=True) sys.exit() if verbose: msg =( f"Input path: {inpath}\n" f"Output path: {outpath}\n" f"Image name: {img_name}\n" f"Mask name: {mask_name}\n" f"Output suffix name: {suffix}\n" f"n_jobs: {str(n_jobs)}\n" f"Skip file: {skip_file_name}\n" f"Files to skip: {format_list_multiline(skip_files,5)}\n" f"Include file: {include_file_name}\n" f"Files to include: {format_list_multiline(include_files,5)}\n" f"Remove from the mask voxels with values under than: {str(min_thr)}\n" f"Remove from the mask voxels with values upper than: {str(max_thr)}\n" f"Log: {str(log)}\n" f"Overwrite previous log file: {str(new_log)}\n" f"Verbose: {str(verbose)}\n" ) hprint_msg_box(msg=msg, indent=2, title=f"MASK THRESHOLDING {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") if n_jobs == 1: for patient in tqdm(glob.glob(inpath+"/*"), ncols=100, desc="Masks Thresholding", bar_format="{l_bar}{bar} [time left: {remaining}, time spent: {elapsed}]", colour="yellow"): crop_volume(patient,inpath,outpath,min_thr,max_thr,img_name,mask_name,suffix,skip_files,include_files,dif_path,verbose,log) else: with multiprocessing.Pool(n_jobs) as pool: tqdm(pool.starmap(crop_volume, [(patient,inpath,outpath,min_thr,max_thr,img_name,mask_name,suffix,skip_files,include_files,dif_path,verbose,log) for patient in glob.glob(inpath+"/*")]), ncols=100, desc="Masks Thresholding", bar_format="{l_bar}{bar} [time left: {remaining}, time spent: {elapsed}]", colour="yellow")
[docs] def crop_volume(patient,inpath,outpath,min_thr,max_thr,img_name,mask_name,suffix,skip_files,include_files,dif_path,verbose,log): if log != '': f = open(log,'a+') sys.stdout = f patientID=os.path.basename(patient) if len(include_files) > 0: #if file to include are specify if patientID not in include_files: #if patient is to be excluded if verbose: print("\033[33mWARNING \n"+patientID+" ("+patient+") is not in the list of patients to include\033[0m",flush=True) return if len(skip_files) > 0: #if there are files to skip if patientID in skip_files: if verbose: print("\033[33mWARNING\nskip "+patientID+" ("+patient+")\033[0m",flush=True) return output_mask_name=os.path.splitext(os.path.splitext(os.path.basename(mask_name))[0])[0]+"_"+suffix+".nii.gz" if verbose: hprint(f"processing {patientID}",patient) if not os.path.exists(os.path.join(outpath,patientID)): os.makedirs(os.path.join(outpath,patientID)) for patient_subdirectory in glob.glob(patient+"/*"): subdirectory=os.path.basename(patient_subdirectory) if verbose: print(patientID+": "+subdirectory,flush=True) if not os.path.exists(os.path.join(outpath,patientID,subdirectory)): os.makedirs(os.path.join(outpath,patientID,subdirectory)) #Copy image from the input folder if dif_path: try: shutil.copy(os.path.join(patient_subdirectory,img_name),os.path.join(outpath,patientID,subdirectory,img_name)) except: print("\033[33mWARNING: the file "+img_name+" was not copied\033[0m",flush=True) try: img = nib.load(os.path.join(patient_subdirectory,img_name)) msk = nib.load(os.path.join(patient_subdirectory,mask_name)) except Exception as e: print("\033[31mERROR!\033[0m",e,flush=True) print("\033[31mSkipping "+patientID+" "+subdirectory+"\033[0m",flush=True) eprint("Skipping "+patientID+" "+subdirectory) continue try: img_data=img.get_fdata().astype(float) except Exception as e: print("\033[31mERROR!\033[0m",e,flush=True) print("\033[31mSkipping "+patientID+" "+subdirectory+"\033[0m",flush=True) eprint("Skipping "+patientID+" "+subdirectory) continue try: msk_data = msk.get_fdata().astype(float) msk_data = np.where((img_data >= float(min_thr)) & (img_data <= float(max_thr)), msk_data, 0) except Exception as e: print("\033[31mERROR!\033[0m",e,flush=True) print("\033[31mSkipping "+patientID+" "+subdirectory+"\033[0m",flush=True) eprint("Skipping "+patientID+" "+subdirectory) continue try: new_mask=nib.Nifti1Image(msk_data,affine=img.affine,header=img.header) nib.save(new_mask,os.path.join(outpath,patientID,subdirectory,output_mask_name)) except: print("\033[31mERROR! Saving final mask\033[0m",flush=True)
if __name__ == "__main__": main(sys.argv[1:])