Source code for unravel.image_tools.resample_points

#!/usr/bin/env python3

"""
Use `img_resample_points` from UNRAVEL to resample a set of points (coordinates) and optionally convert them to an image, accounting for the number of detections at each voxel.

Input image types:
    .czi, .nii.gz, .ome.tif series, .tif series, .h5, .zarr

Outputs:
    - Output image types: .nii.gz, .tif series, .h5, .zarr
    - A CSV file where each row represents a resampled point corresponding to a detection in the 3D image.
    - A 3D image where each voxel contains the number of detections at that location.

Usage: 
------
    img_resample_points -i path/points.csv -ri path/ref_image.nii.gz -cr 3.52 3.52 6 -tr 50 [-co path/resampled_points.csv] [-io path/resampled_image.nii.gz] [-thr 20000 or -uthr 20000] [-v]
"""

import numpy as np
import pandas as pd
from pathlib import Path
from rich import print
from rich.traceback import install


from unravel.core.help_formatter import RichArgumentParser, SuppressMetavar, SM

from unravel.core.config import Configuration
from unravel.core.img_io import load_3D_img, save_3D_img
from unravel.core.utils import log_command, verbose_start_msg, verbose_end_msg
from unravel.image_io.points_to_img import points_to_img, load_and_prepare_points


[docs] def parse_args(): parser = RichArgumentParser(formatter_class=SuppressMetavar, add_help=False, docstring=__doc__) reqs = parser.add_argument_group('Required arguments') reqs.add_argument('-i', '--input', help='CSV w/ columns: x, y, z, Region_ID (e.g., from ``rstats``)', required=True, action=SM) reqs.add_argument('-ri', '--ref_img', help='Path to a reference image .nii.gz for setting the output image resolution and shape [and saving if .nii.gz output].', required=True, action=SM) reqs.add_argument('-cr', '--current_res', help="Current resolution in micrometers (e.g., 3.52 3.52 6 for anisotropic or 10 for isotropic).", nargs='*', required=True, type=float, action=SM) reqs.add_argument('-tr', '--target_res', help="Target resolution in micrometers (e.g., 50 for isotropic).", required=True, type=float, action=SM) opts = parser.add_argument_group('Optional arguments') opts.add_argument('-co', '--csv_output', help="Optional: Path to save resampled points in a CSV.", action=SM) opts.add_argument('-io', '--img_output', help="Optional: Path to save resampled points as an image.", action=SM) opts.add_argument('-thr', '--thresh', help='Exclude region IDs below this threshold (e.g., 20000 to obtain left hemisphere data)', type=float, action=SM) opts.add_argument('-uthr', '--upper_thr', help='Exclude region IDs above this threshold (e.g., 20000 to obtain right hemisphere data)', type=float, action=SM) general = parser.add_argument_group('General arguments') general.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False) return parser.parse_args()
[docs] def resample_and_convert_points(points_csv_input_path, current_res, target_res, ref_img, thresh=None, upper_thresh=None): """Resample a set of points and optionally convert them to an image. Parameters: ----------- points_csv_input_path : str Path to the CSV file containing the points with columns 'x', 'y', 'z', and 'Region_ID'. current_res : tuple of floats or float The current resolution of the points in micrometers, as (x_res, y_res, z_res) or a single float value for isotropic resampling. target_res : tuple of floats or float The target resolution of the points in micrometers, as (x_res, y_res, z_res) or a single float value for isotropic resampling. ref_img : numpy.ndarray Reference image for the output image shape and resolution. thresh : float, optional Exclude region IDs below this threshold (e.g., 20000 to obtain left hemisphere data). upper_thresh : float, optional Exclude region IDs above this threshold (e.g., 20000 to obtain right hemisphere data). Returns: -------- points_resampled_df : pandas.DataFrame The resampled points with columns 'x', 'y', 'z', and 'Region_ID' points_resampled_img : numpy.ndarray or None The resampled image where each voxel contains the number of detections at that location. Returns `None` if `output_img_path` is not provided. """ # Check if input file exists if not Path(points_csv_input_path).exists(): raise FileNotFoundError(f"\n [red1]Input file not found: {points_csv_input_path}\n") # Handle the case where current_res is passed as a single-element list if isinstance(current_res, list) and len(current_res) == 1: current_res = current_res[0] # Ensure that the current_res and target_res are either a single float or a tuple/list of 3 floats if isinstance(current_res, (int, float)): current_res = (current_res, current_res, current_res) elif isinstance(current_res, (list, tuple)) and len(current_res) != 3: raise ValueError("\n [red1]current_res must be a single float or a tuple/list of 3 floats (x_res, y_res, z_res).\n") if isinstance(target_res, (int, float)): target_res = (target_res, target_res, target_res) elif isinstance(target_res, (list, tuple)) and len(target_res) != 3: raise ValueError("\n [red1]target_res must be a single float or a tuple/list of 3 floats (x_res, y_res, z_res).\n") # Load and prepare points points_df = load_and_prepare_points(points_csv_input_path, thresh=thresh, upper_thresh=upper_thresh) # Convert voxel coordinates to physical space points_ndarray_physical = points_df[['x', 'y', 'z']].values * np.array(current_res) # Resample the points to the target resolution points_ndarray_resampled = points_ndarray_physical / np.array(target_res) # Convert the resampled points to a DataFrame points_resampled_df = pd.DataFrame(points_ndarray_resampled, columns=['x', 'y', 'z']) if 'Region_ID' in points_df.columns: points_resampled_df['Region_ID'] = points_df['Region_ID'].values # Create an image from the points using the reference image points_resampled_img = points_to_img(points_ndarray_resampled, ref_img=ref_img) return points_resampled_df, points_resampled_img
[docs] @log_command def main(): install() args = parse_args() Configuration.verbose = args.verbose verbose_start_msg() ref_img = load_3D_img(args.ref_img) # Resample and convert the points points_resampled_df, points_resampled_img = resample_and_convert_points(args.input, args.current_res, args.target_res, ref_img, args.thresh, args.upper_thr) # Save the resampled points to a CSV file if args.csv_output: csv_output_path = Path(args.csv_output) csv_output_path.parent.mkdir(exist_ok=True, parents=True) points_resampled_df.to_csv(csv_output_path, index=False) print(f"\n Points saved to: {csv_output_path}\n") # Save the image if args.img_output: save_3D_img(points_resampled_img, args.img_output, reference_img=args.ref_img) verbose_end_msg()
if __name__ == "__main__": main()