Abstract
How to make nibabel losslessly copy a NIfTI image preserving data type and intensity scaling parameters? In particular if the input uses an integer datatype and where scl_slope
is not one?
Details
I want to process an OpenNeuro BIDS dataset where the images have been defaced and regions outside the scalp set to zero (to avoid ghosting of facial features). Many of these clinical scans were acquired with interpolation (resulting in artificially high resolution) and large fields of view. Susequent processing is much faster if I can first crop these images to remove exterior slices that are all zeros. I wrote a simple script to do this, but by default it wants to convert the data to float32, whereas I would like to preserve the int16 datatype as this is a lossless operation and I the goal is to reduce disk and ram usage.
A minimal example of my issue is the script below, which can be applied to the uint8 single_subj_T1.nii image that is supplied with SPM12. fslhd reports a scl_slope 0.003906
and scl_inter 0.000000
, so even though the raw data is in the range 0…255, the scaled data returned by get_fdata
is 0…1, so saving astype(nifti_img.get_data_dtype()) results in an image that is essentially 0s.
- nibabel tells me that
get_data()
is deprecated, so it looks like we should try to useget_fdata()
- Strangely, when describing the header, nibabel reports
scl_slope : nan
andscl_inter : nan
import nibabel as nib
import numpy as np
import os
import glob
def crop_nifti_image(input_path, output_path):
try:
# Load NIfTI file
nifti_img = nib.load(input_path)
print(nifti_img.header)
data = nifti_img.get_fdata()
affine = nifti_img.affine
# Find non-zero indices along each axis
non_zero_rows = np.any(data != 0, axis=(1, 2))
non_zero_cols = np.any(data != 0, axis=(0, 2))
non_zero_slices = np.any(data != 0, axis=(0, 1))
# Determine cropping boundaries
start_row, end_row = np.argmax(non_zero_rows), len(non_zero_rows) - np.argmax(np.flip(non_zero_rows))
start_col, end_col = np.argmax(non_zero_cols), len(non_zero_cols) - np.argmax(np.flip(non_zero_cols))
start_slice, end_slice = np.argmax(non_zero_slices), len(non_zero_slices) - np.argmax(np.flip(non_zero_slices))
# Check if any voxels are removed
if start_row == 0 and end_row == len(non_zero_rows) and \
start_col == 0 and end_col == len(non_zero_cols) and \
start_slice == 0 and end_slice == len(non_zero_slices):
print("No voxels removed. Original file remains unchanged.")
return
# Crop data and adjust affine transformation
cropped_data = data[start_row:end_row, start_col:end_col, start_slice:end_slice]
cropped_affine = affine.copy()
cropped_affine[:3, 3] += affine[:3, :3].dot([start_row, start_col, start_slice])
# Save the cropped NIfTI file with the same data type as the original image
cropped_img = nib.Nifti1Image(cropped_data.astype(nifti_img.get_data_dtype()), cropped_affine)
nib.save(cropped_img, output_path)
# Report the number of rows, columns, and slices removed
rows_removed = len(non_zero_rows) - (end_row - start_row)
cols_removed = len(non_zero_cols) - (end_col - start_col)
slices_removed = len(non_zero_slices) - (end_slice - start_slice)
print(f"Cropped image saved to {output_path}. Rows removed: {rows_removed}, Columns removed: {cols_removed}, Slices removed: {slices_removed}")
except Exception as e:
print(f"Error: {e}")
def crop_and_save_images(file_filter, output_folder):
try:
# Find files that match the filter
matching_files = glob.glob(file_filter)
if not matching_files:
print("No files found matching the specified filter.")
return
print(f"{len(matching_files)} found")
matching_files = sorted(matching_files)
# Create the output folder if it doesn't exist
os.makedirs(output_folder, exist_ok=True)
for file_path in matching_files:
# Generate output path in the specified output folder with the same filename stem
file_name = os.path.basename(file_path)
output_path = os.path.join(output_folder, file_name.replace('.nii', '_cropped.nii.gz'))
# Apply the crop_nifti_image function and save the cropped image
crop_nifti_image(file_path, output_path)
except Exception as e:
print(f"Error: {e}")
# Example usage:
#filename_filter = '/Users/chris/Desktop/Huge/sub-*_FLAIR.nii.gz'
#output_folder = '/Users/chris/Desktop/Smallish'
filename_filter = './single_subj_T1.nii'
output_folder = './'
crop_and_save_images(filename_filter, output_folder)