diff --git a/README.md b/README.md index 185c096..6b030a2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ -# Displasias -Concatenación Código Displasias +# ratCortex-dMRIstreams + +Code to process dMRI volumes and create cortical streamlines in rat data. :information_source: [**Info & details**](https://hackmd.io/@servindc/ratCortex-dMRI) + +## Setup + +1. Install the [MINC](https://en.wikibooks.org/wiki/MINC) tools required for the code: + ```bash + sudo apt install minc-tools + ``` + +2. Create a conda environment: + ```bash + conda env create --file environment.yml + ``` + +3. Activate the environment: + ```bash + conda activate cx-streams-env + ``` + +4. Display script help: + ```bash + nii2streams.sh -h + ``` \ No newline at end of file diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..f439529 --- /dev/null +++ b/environment.yml @@ -0,0 +1,17 @@ +# run: conda env create --file environment.yml +name: cx-streams-env +channels: + - conda-forge + - anaconda + - mrtrix3 +dependencies: + - python + - matplotlib + - numpy + - nibabel + - dipy + - networkx + - scipy + - scikit-image + - scikit-learn + - mrtrix3 \ No newline at end of file diff --git a/get_seeds.py b/get_seeds.py new file mode 100644 index 0000000..fdd8e2a --- /dev/null +++ b/get_seeds.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Adjust seed line + +@author: dcortex +""" +from os.path import join, basename +from os import chdir +import numpy as np +import nibabel as nib +from sklearn.neighbors import NearestNeighbors +import networkx as nx +from scipy.interpolate import splprep, splev + +def sort_points(seeds): + """ + Returns sorted 'seeds' (np.ndarray) by their nearest neighbors + """ + if len(seeds)==0: return np.array([]) + clf = NearestNeighbors(n_neighbors=2).fit(seeds) + G = clf.kneighbors_graph() # sparse N x N matrix + + T = nx.from_scipy_sparse_matrix(G) + + paths = [list(nx.dfs_preorder_nodes(T, i)) for i in range(len(seeds))] + + mindist = np.inf + minidx = 0 + + for i in range(len(seeds)): + p = paths[i] # order of nodes + ordered = seeds[p] # ordered nodes + # find cost of that order by the sum of euclidean distances between points (i) and (i+1) + cost = (((ordered[:-1] - ordered[1:])**2).sum(1)).sum() + if cost < mindist: + mindist = cost + minidx = i + seeds = seeds[paths[minidx]] + # Medial a lateral + if seeds[0][1] < seeds[-1][1]: seeds = seeds[::-1] + return seeds + +def smooth_curve(x, y, s=0.5): + tck, u = splprep([x, y], s=s) + smooth_points = np.array(splev(u, tck)).T + return smooth_points + +def get_seeds_from_nii(f_name, subject, side='l', smooth=False, save=True, s=0.1, + save_folder='~/Descargas',): + lines_volume = nib.load(f_name).get_fdata() + + seeds_dict = {} + for slice_n in range(10,16): + + sx, sy = np.array(np.nonzero(lines_volume[:,:,slice_n])) + seeds = np.array([sx,sy]).T + + if len(seeds)==0: continue + + seeds = sort_points(seeds) + if smooth: seeds = smooth_curve(*seeds.T, s=s) + + # Add z coordinate + ones = np.ones([len(seeds), 1]) + seeds = np.concatenate((seeds, slice_n*ones), axis=1)[1:,:] + # remove first entry because of reasons ^ + + if save: + seeds_name = f'{subject}_{side}_{slice_n}_seeds' + if smooth: seeds_name += '_smooth' + + np.savetxt(join(save_folder, basename(seeds_name)+'.txt'), seeds) + print(f'\n Created file: {basename(seeds_name)}.txt in: {save_folder}') + + seeds_dict[seeds_name] = seeds + + return seeds_dict + +#_____________________________________________________________________________ + + +if __name__ == "__main__": + import sys + import subprocess + from os.path import dirname + + #subject = sys.argv[1] # subject = '37A + #side = 'l' + + #f_name = f'minc/{subject}_{side}_outline.nii' + f_name = sys.argv[1] + out_dir = sys.argv[2] + prefix = sys.argv[3] + try: + n_seeds = sys.argv[4] + except IndexError: + n_seeds = 150 + print(f'\n Using {n_seeds} seeds') + + #subject = basename(f_name).split('_')[0] + side = basename(f_name).split('_')[1] + seeds = get_seeds_from_nii(f_name, subject=prefix, side=side, smooth=True, save=True, + s=10, save_folder=out_dir, ) + + #out_dir = dirname(f_name) + for seeds_name in list(seeds.keys()): + + convert1 = (f"tckconvert {join(out_dir, seeds_name)}.txt" + f" {join(out_dir, seeds_name)}.tck ") + # -voxel2scanner ../{subject}_x2.nii + resample = (f"tckresample -num_points {n_seeds} -nthreads 0" + f" {join(out_dir, seeds_name)}.tck" + f" {join(out_dir, seeds_name)}_resampled.tck") + convert2 = (f"tckconvert {join(out_dir, seeds_name)}_resampled.tck" + f" {join(out_dir, seeds_name)}_resampled_[].txt") + rename = (f"mv {join(out_dir, seeds_name)}_resampled_0000000.txt" + f" {join(out_dir, seeds_name)}_resampled.txt") + for my_command in [convert1, resample, convert2, rename]: + process = subprocess.Popen(my_command.split(), stdout=subprocess.PIPE) + output, error = process.communicate() + + print((f"\n Created file: {join(out_dir, seeds_name)}_resampled" + f"(txt & tck) in: {out_dir}\n")) + +#!tckconvert {seeds_name}.txt {seeds_name}.tck -voxel2scanner ../{subject}_x2.nii +#!tckresample -num_points 150 -nthreads 0 {seeds_name}.tck {seeds_name}_resampled.tck +#!tckconvert {seeds_name}_resampled.tck {seeds_name}_resampled_[].txt + + sys.exit() + + + + + + + + \ No newline at end of file diff --git a/make_grid.sh b/make_grid.sh new file mode 100644 index 0000000..48d1140 --- /dev/null +++ b/make_grid.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +if [ "$1" == "-h" -o "$1" == "--help" -o "$1" == "" ]; then + echo -e "\n Usage: `basename $0` grid_in grid_mid [output_path]" + echo -e "\n Computes grid for the mincLaplace script." + echo -e "\n Returns '_grid_123.nii.gz' & '_grid_123.mnc'.\n" + exit 0 +fi + +grid_in=$1 +grid_mid=$2 +out_folder=${3:-$(dirname $grid_in)} + +echo -e "\n Making new grid in: '$out_folder'" # -e enables interpretation of backslash escapes +echo -e "\n Using: '$grid_in' & '$grid_mid'\n" + +# Area's default values: +in0=${4:-1} +mid=${5:-2} +out=${6:-3} + +gridsufix=grid_${in0}${mid}${out} +grid_name=$(basename ${grid_in%_grid_in*}) # e.g. 37A_l + +#echo $out_folder/${grid_name}_${gridsufix}.nii.gz + +#for side in l #r # left right + +#grid_in=$(find $out_folder -type f -iname "*_${side}_grid_in.nii.gz") +#grid_mid=$(find $out_folder -type f -iname "*_${side}_grid_mid.nii.gz") + +#rat_id=$(basename ${grid_in%%_*}) + +#if [ -f "${out_folder}/${rat_id}_${side}_${gridsufix}.nii.gz" ] +#then +# echo -e "\n Archivo '${rat_id}_${side}_${gridsufix}.nii.gz' ya existe.\n\n" + +#elif [ -f "${rat_id}_${side}_grid_in.nii.gz" -a -f "${rat_id}_${side}_grid_mid.nii.gz" ] +#elif [ -f $grid_in -a -f $grid_mid ] + +if [ -f "$out_folder/${grid_name}_${gridsufix}.nii.gz" ] +then + echo -e "\n File '${grid_name}_${gridsufix}.nii.gz' already exists.\n" +elif [ -f $grid_in -a -f $grid_mid ] +then + #mrcalc ${rat_id}_${side}_grid_in.nii.gz 0 -mult $out -add base.nii + mrcalc $grid_in 0 -mult $out -add ${out_folder}/base.nii + + mid_scalar="$(($mid-$out))" + #mrcalc ${rat_id}_${side}_grid_mid.nii.gz $mid_scalar -mult base2.nii + mrcalc $grid_mid $mid_scalar -mult ${out_folder}/base2.nii + + in_scalar="$(($in0-$out))" + #mrcalc ${rat_id}_${side}_grid_in.nii.gz $in_scalar -mult base3.nii + mrcalc $grid_in $in_scalar -mult ${out_folder}/base3.nii + + #mrcalc ${rat_id}_${side}_grid_in.nii.gz ${rat_id}_${side}_grid_mid.nii.gz -min $mid -mult base4.nii + mrcalc $grid_in $grid_mid -min $mid -mult ${out_folder}/base4.nii + + #mrcalc base.nii base2.nii -add base3.nii -add base4.nii -add "${rat_id}_${side}_grid_${in0}${mid}${out}.nii.gz" + a=${out_folder}/base.nii; b=${out_folder}/base2.nii + c=${out_folder}/base3.nii; d=${out_folder}/base4.nii + #mrcalc $a $b -add $c -add $d -add ${out_folder}/${rat_id}_${side}_${gridsufix}.nii.gz + mrcalc $a $b -add $c -add $d -add $out_folder/${grid_name}_${gridsufix}.nii.gz + + gio trash ${out_folder}/base*.nii + + #nii2mnc ${out_folder}/${rat_id}_${side}_${gridsufix}.nii.gz ${out_folder}/${rat_id}_${side}_${gridsufix}.mnc + nii2mnc $out_folder/${grid_name}_${gridsufix}.nii.gz $out_folder/${grid_name}_${gridsufix}.mnc + + echo -e "\n Grids '$out_folder/${grid_name}_${gridsufix}' (.mnc & .nii.gz) created in '$out_folder'\n" +else + echo -e "\n Error: files '$grid_in', '$grid_mid' missing.\n" +fi + + diff --git a/mask_closing.py b/mask_closing.py new file mode 100644 index 0000000..2f537eb --- /dev/null +++ b/mask_closing.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@author: dcortex +""" +import nibabel as nib +import numpy as np +from scipy.ndimage.morphology import binary_fill_holes +from skimage import draw + +def close_mask_in(im_slice_2d, side): + """ + Returns the closed form of a '_{side}_inline.nii.gz' mask in numpy array + and also the clipped array + """ + new_slice = im_slice_2d.copy() + + x_no_0, y_no_0 = np.nonzero(im_slice_2d) + if len(x_no_0) == 0: return new_slice, new_slice + #breakpoint() + x1 = x_no_0.min() + x2 = x_no_0.max() + if side == "l": + x_mid = x2; x_aux1 = x_mid - 9 + 1; x_aux2 = x2 + 1 + elif side == "r": + x_mid = x1; x_aux2 = x_mid + 9; x_aux1 = x1 + + y_mid = y_no_0[np.where(x_no_0==x_mid)[0]].min() + y_min = y_no_0.min() + + # inferior line + new_slice[x1:x2+1, y_min] = 1 + # medial line + new_slice[x_mid, y_min:y_mid+1] = 1 + new_slice = binary_fill_holes(new_slice) + # in_short array: + other_slice = new_slice.copy() + other_slice[x_aux1:x_aux2, :] = 0 + + return new_slice, other_slice + +def endpoints(line_points): + """ + Returns the 2 end-points of an array of points from a line + """ + neighbors = [] + for p in line_points: + aux = 0 + for q in line_points: + if np.linalg.norm(p-q) == 1: + aux += 1 + neighbors.append(aux) + e_points = np.where(np.array(neighbors)==1) + return line_points[e_points] + +def close_mask_pair(im_slice_2d_in, im_slice_2d_out): + new_slice = im_slice_2d_in + im_slice_2d_out + + p_in = np.array([*np.nonzero(im_slice_in)]).T + p_out = np.array([*np.nonzero(im_slice_out)]).T + if len(p_in) == 0: return new_slice + + e_in = endpoints(p_in) + e_out = endpoints(p_out) + #e_in.sort(axis=0); e_out.sort(axis=0) + + for i in [0, -1]: + [x1, y1], [x2, y2] = e_in[i], e_out[i] + line = draw.line(x1, y1, x2, y2) + new_slice[line] = 1 + + return binary_fill_holes(new_slice) + +if __name__ == "__main__": + import sys + from os.path import exists, basename + from argparse import ArgumentParser + + parser = ArgumentParser( + description=""" Creates the 'in' and 'mid' closed masks from the + '*line.nii.gz' pair of masks.""") + parser.add_argument('inline', type=str, + help="Nifti image of inline mask.") + parser.add_argument('outline', type=str, + help="Nifti image of outline mask.") + parser.add_argument('out_dir', type=str, + help="Output directory.") + parser.add_argument('prefix', type=str, default=None, + help="String prefix for the output files.") + parser.add_argument('-s', '--side', type=str, default=None, + help=("Hemisphere side (left 'l' or right 'r').")) + args = parser.parse_args() + + inline = args.inline + outline = args.outline + out_dir = args.out_dir + prefix = args.prefix + s = args.side + + if s==None: s = inline.split('.')[0].split('_')[-2] + + # output filenames inline.split('.')[0][:-7] + #grid_in = out_dir + "/" + prefix + s + "_grid_in.nii.gz" + grid_in = f"{out_dir}/{prefix}_{s}_grid_in.nii.gz" + grid_in9= f"{out_dir}/{prefix}_{s}_grid_in_short.nii.gz" + grid_out= f"{out_dir}/{prefix}_{s}_grid_mid.nii.gz" + filenames = [grid_in, grid_in9, grid_out] + + for file in filenames: + if exists(grid_in): + print(f"\n {parser.prog}: File '{grid_in}' already exists"); sys.exit() + + line_in = nib.load(inline) + line_out = nib.load(outline) + + + array_in = line_in.get_fdata() + array_out = line_out.get_fdata() + + array_grid_in = array_in.copy() + array_grid_in_short = array_in.copy() + array_grid_out = array_in.copy() + + for k in range(array_grid_in.shape[2]): + im_slice_in = array_in[:,:,k] + im_slice_out = array_out[:,:,k] + + closed_in, closed_in_short = close_mask_in(im_slice_in, side=s) + closed_out = close_mask_pair(im_slice_in, im_slice_out) + + array_grid_in[:,:,k] = closed_in + array_grid_in_short[:,:,k] = closed_in_short + array_grid_out[:,:,k] = closed_out + + for i, a in enumerate([array_grid_in, array_grid_in_short, array_grid_out]): + img = nib.Nifti1Image(a, line_in.affine) + nib.save(img, filenames[i]) + print(f"\n Created file: {filenames[i]}") + + sys.exit() + +############## Tests diff --git a/mask_dilation.py b/mask_dilation.py new file mode 100644 index 0000000..18ba1d8 --- /dev/null +++ b/mask_dilation.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@author: dcortex + +Creates a binary dilation from the input mask +Default is a 2D coronal (xy) dilation + +Use: + python mask_dilation.py input_mask n_iterations [ax_dilated] +""" +from os.path import join, dirname, basename, isfile +from sys import argv +import numpy as np +import nibabel as nib +from scipy.ndimage import binary_dilation, generate_binary_structure + +#mask_file = "37A_l_grid_in.nii.gz" +#mask_path = "/data/Documentos/Maestría/LabC13/hoy/masks" +mask_file = argv[1] +n_iter = int(argv[2]) +try: + ax_dilated = argv[3] # 'x', 'y', 'xy' +except IndexError: + ax_dilated = 'xy' + + +mask_path = dirname(mask_file) +mask_basename = basename(mask_file).split('.')[0] +mask_new_name = mask_basename + f'_dilatedx{n_iter}.nii.gz' + +if isfile(join(mask_path, mask_new_name)): + print('\n mask_dilation.py: Dilated mask already exists\n') + quit() + +#img = nib.load(join(mask_path, mask_file)) +img = nib.load(join(mask_file)) +mask = img.get_fdata() +affine = img.affine + +kernel_2d = np.zeros([3,3,3]) + +if ax_dilated == 'xy': + kernel_2d[:,:,1] = generate_binary_structure(2, 1) +elif ax_dilated == 'x': + kernel_2d[:,:,1] = np.array([[0,1,0],[0,1,0],[0,1,0]]) +elif ax_dilated == 'y': + kernel_2d[:,:,1] = np.array([[0,0,0],[1,1,1],[0,0,0]]) + +mask_dilated = binary_dilation(mask, structure=kernel_2d, + iterations=n_iter).astype(mask.dtype) + +# import matplotlib.pyplot as plt +# n_slice=14 +# f, ax = plt.subplots(1,2) +# ax[0].imshow(mask[:,:,n_slice].T, origin="lower", cmap='Greys', aspect='equal') +# ax[1].imshow(mask_dilated[:,:,n_slice].T, origin="lower", cmap='Greys', aspect='equal') + + +#save +img_new = nib.Nifti1Image(mask_dilated, affine) +print(f'\n mask_dilation.py: Creating dilated mask:\n{join(mask_path, mask_new_name)}') +nib.save(img_new, join(mask_path, mask_new_name) ) + + +#import argparse + + + + + + + diff --git a/my_MincLaplaceDist b/my_MincLaplaceDist new file mode 100644 index 0000000..7c670bd Binary files /dev/null and b/my_MincLaplaceDist differ diff --git a/nii2streams.sh b/nii2streams.sh new file mode 100644 index 0000000..8938e86 --- /dev/null +++ b/nii2streams.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +reset="\e[0m" # reset color +color="\e[1;33m" # yellow + +help(){ + echo -e "\n ${color}Usage:${reset} `basename $0` inline_roi outline_roi ref_image [out_dir] [side] [prefix]" + echo -e "\n ${color}Info:${reset} Generates columnar & laminar cortical streamlines within" + echo -e " the area between the provided ROIs." + echo -e "\n ${color}Parameters:${reset} inline_roi - Cortical inline (nifti format)" + echo -e " outline_roi - Interior (nifti format)" + echo -e " ref_image - reference subject image to get the affine transform (nifti format)" + echo -e " out_dir - Output dirctory (default = 'inline_roi' directory)" + echo -e " side - hemisphere side of the given ROIs (MANDATORY if 'prefix' is set)" + echo -e " prefix - string to prefix created filenames (default = prefix of 'inline_roi') " + echo -e "\n ${color}Returns:${reset} '*_out_resampled.tck' & '*_out_resampled_h.tck' streamlines files.\n" +} + +if [ "$1" == "-h" -o "$1" == "--help" -o "$1" == "" ]; then help; exit 0; fi + + +#out_folder=${3:-$(dirname $grid_in)} + +inline=$1 +outline=$2 +ref_image=$3 +dir_name=${4:-`dirname $inline`} # output directory +#no_ext=`basename ${inline%%.*}` # 'inline_roi' basename without file extension +#IFS='_' read -ra split_inline <<< "$no_ext" +#prefix_default=${no_ext::-9} # 'inline_roi' subject ID with path +#prefix=${6:-$prefix_default} +IFS='_' read -ra split_inline <<< `basename $inline` +side=${5:-${split_inline[-2]}} # gets 'side' from the 'inline_roi' filename +prefix=${6:-${split_inline[-3]}} +script_dir=`dirname $0` + +# print parameters +#echo -e "\n Parameters: " $inline $outline $ref_image $dir_name $side $prefix"\n" + +# Masks construction: +echo -e "\n Running: mask_closing.py $inline $outline $dir_name ${prefix} -s ${side}" +$script_dir/mask_closing.py $inline $outline $dir_name $prefix -s ${side} + +# Dilate cortical mask (9 pixels): +input1=$dir_name/${prefix}_${side}_grid_mid.nii.gz; voxels_dilated=9 +echo -e "\n Running: mask_dilation.py ${input1} $voxels_dilated" +$script_dir/mask_dilation.py $input1 $voxels_dilated + +# Create grid for mincLaplace input: +input1=$dir_name/${prefix}_${side}_grid_in_short.nii.gz +input2=$dir_name/${prefix}_${side}_grid_mid_dilatedx${voxels_dilated}.nii.gz +echo -e "\n Running: make_grid.sh $input1 $input2" +$script_dir/make_grid.sh $input1 $input2 + + +# Run 'mincLaplace' to generate `*_minc_thick_*.nii`: +input1=$dir_name/${prefix}_${side}_grid_123.mnc +echo -e "\n Running: run_grid.sh $input1 '_thick'" +$script_dir/run_grid.sh $input1 '_thick' + +# Apply cortical mask `*_mid.nii.gz*` to `*_Grad[X-Z].nii`: +input1=$dir_name/${prefix}_${side}_grid_mid.nii.gz +for a in GradX GradY GradZ RGB +do + input2=$dir_name/${prefix}_${side}_minc_thick_${a}.nii.gz + output=$dir_name/${prefix}_${side}_minc_${a}.nii.gz + echo -e "\n Applying cortical mask '$input1' to '$input2':" + mrcalc $input1 $input2 -mult $output +done + + +# Generate seed points for streamlines +#input1=$dir_name/${prefix}_${side}_outline.nii.gz +echo -e "\n Running: get_seeds.py $outline $dir_name" +$script_dir/get_seeds.py $outline $dir_name $prefix + +# Create streamlines +mkdir $dir_name/tck +input1=$dir_name/${prefix}_${side}_minc_RGB.nii.gz +for seed_file in $dir_name/${prefix}_${side}_??_seeds_smooth_resampled.txt; +do + IFS='_' read -ra split_name <<< "$seed_file" + j=${split_name[-4]} # <- slice_n + #input2=$seed_file + input2=$dir_name/${prefix}_${side}_${j}_seeds_smooth_resampled.txt + output=$dir_name/tck/${prefix}_${side}_${j}_out + echo -e "\n Running: vector2streams.py $input1 $input2 $ref_image $output \n" + $script_dir/vector2streams.py $input1 $input2 $ref_image $output +done + +# zip .txt files (-v to verbose) +tar -czf $dir_name/tck/${prefix}_${side}_${j}_out.tar.gz $dir_name/tck/${prefix}_${side}_${j}_out_*.txt + +# remove '*.txt' +rm $dir_name/tck/${prefix}_${side}_${j}_out_???.txt + +# Check streamlines: +echo -e "\n ${color}To check created streamlines run:\n${reset}" +echo -e " mrview $ref_image -tractography.load $dir_name/tck/${prefix}_${side}_${j}_out_resampled.tck -plane 2 -fullscreen & \n" + +printf \\a; sleep 0.1; printf \\a # sound in bash \ No newline at end of file diff --git a/run_grid.sh b/run_grid.sh new file mode 100644 index 0000000..fdeab60 --- /dev/null +++ b/run_grid.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +if [ "$1" == "-h" -o "$1" == "--help" -o "$1" == "" ]; then + echo -e "\n Usage: `basename $0` grid (.mnc or .nii) [suffix_str] [alpha]" + echo -e "\n Runs 'my_MincLaplaceDist' on the given grid." + echo -e "\n Returns '_grid_123.nii.gz' & '_grid_123.mnc'.\n" + exit 0 +fi + +input_grid=$1 +suffix=${2:-''} +alpha=${3:-0.1} + +# String before patern '_grid' +outputname=${input_grid%_grid*}_minc${suffix} + +filename="${input_grid%.*}" +#extension="${input_grid##*.}" + +#if [ ${input_grid: -4} == ".nii" ] +if [ -f ${outputname}_RGB.nii.gz ] +then + echo -e "\n '${outputname}_RGB.nii.gz' already exists \n" + exit 1 +fi + +if [ ! -f ${filename}.mnc ] # if input isn't .mnc +then + echo -e "\n Converting '$input_grid' to .mnc format \n" + nii2mnc ${filename}.nii +fi + +echo -e "\n Creating grid: '${outputname}' \n" +my_mincL=`dirname $0`"/my_MincLaplaceDist" + +$my_mincL -i ${filename}.mnc -o ${outputname} -like ${filename}.mnc -alpha $alpha + +echo "alpha weight = " $alpha + +mnc2nii ${outputname}_GradX.mnc ${outputname}_GradX.nii +mnc2nii ${outputname}_GradY.mnc ${outputname}_GradY.nii +mnc2nii ${outputname}_GradZ.mnc ${outputname}_GradZ.nii + +mrcat ${outputname}_GradX.nii ${outputname}_GradY.nii ${outputname}_GradZ.nii ${outputname}_RGB.nii.gz + +gzip ${outputname}_GradX.nii; gzip ${outputname}_GradY.nii; gzip ${outputname}_GradZ.nii +gio trash ${outputname}_Grad*.mnc \ No newline at end of file diff --git a/vector2streams.py b/vector2streams.py new file mode 100644 index 0000000..febba72 --- /dev/null +++ b/vector2streams.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@author: dcortex + +New: python vector2streams.py minc_RGB seeds_txt img_ref stream_name +""" +import numpy as np +import nibabel as nib +from dipy.io.image import load_nifti +import matplotlib.pyplot as plt + +def get_tck_list(segments): + """ + Receives the flat list of segments of all streamlines + Returns a list of lists. Each nested list is a streamline + """ + lines = [] + current_line = [] + for s in segments: + if current_line==[]: + current_line.append(s[0]) + current_line.append(s[1]) + continue + if np.array_equal(s[0], current_line[-1]): + current_line.append(s[1]) + else: + lines.append(np.array(current_line)) + current_line = [] + lines.append(np.array(current_line)) + return lines + +def vectorfield2streams_2d(x, y, gX, gY, seeds, slice_n, side= 'l', + start_line='outline'): + """" + Returns list of ndarrays (streams in 2D) + """ + if start_line=='outline': int_dir = 'forward' + elif start_line=='inline': int_dir = 'backward' + streams = [] + fig = plt.figure() + for seed in seeds: + strm = plt.streamplot(x.T[0], y[0], + gX[:,:,slice_n].T, gY[:,:, slice_n].T, + linewidth=1, density=20, + minlength=0.01, + start_points=seed.reshape([1,2]), + integration_direction=int_dir # 'forward','backward','both' + ) + segments = strm.lines.get_segments() + lines = get_tck_list(segments) + streams.append(lines[0]) + + plt.close(fig) + return streams + + +if __name__ == "__main__": + import argparse + import subprocess + from os.path import dirname, isdir + from dipy.io.streamline import load_tractogram, save_tractogram + from dipy.io.stateful_tractogram import Space, StatefulTractogram, Origin + + parser = argparse.ArgumentParser( + description='Creates streamlines from the given vector field.') + parser.add_argument('minc_RGB', type=str, + help='vector space created by mincLaplace (nifti)') + parser.add_argument('seeds_txt', type=str, + help='txt file with the seeds 3D coordinates') + parser.add_argument('dwi_ref', type=str, + help='DWI reference image to get the affine matrix (nifti)') + parser.add_argument('stream_name', type=str, + help='basename for new streamline files') + + #parser.print_help() + args = parser.parse_args() + + # test values + # minc_RGB = '37A_l_minc_RGB.nii.gz' + # dwi_ref = '../37A_3d_ref.nii' + # seeds_txt = "37A_l_14_seeds_smooth_resampled.txt" + # stream_name = 'tck/37A_l_14_out' + minc_RGB = args.minc_RGB + dwi_ref = args.dwi_ref + seeds_txt = args.seeds_txt + stream_name = args.stream_name + + + out_dir = dirname(stream_name) + + if not isdir(out_dir): + print(f"\n vector2streams.py: Directory '{out_dir}' doesn't exists\n") + quit() + + img_RGB = nib.load(minc_RGB).get_fdata() + _, affine = load_nifti(dwi_ref) + seeds = np.loadtxt(seeds_txt) + slice_n = int(seeds[0,-1]) + + gX = img_RGB[:,:,:,0] + gY = img_RGB[:,:,:,1] + x, y = np.mgrid[0:gX.shape[0], 0:gX.shape[1]] + + streams_2d = vectorfield2streams_2d(x, y, gX, gY, seeds[:,:2], slice_n, + side= 'r', start_line='outline') + + # add z-coordinate & save + for i, s in enumerate(streams_2d): + ones = np.ones([len(s), 1]) + v = np.concatenate((s, slice_n*ones, ones), axis=1) + aux = np.diag([0.5,0.5,1,1]) # we were working on 2x size images + new_s = (affine@aux@v.T).T + #save + j = f"{i:0{3}}" # zero padding + np.savetxt(f"{stream_name}_{j}.txt", new_s[:,:3]) + + # convert to .tck & resample + convert = f"tckconvert {stream_name}_[].txt {stream_name}.tck" + resample = (f"tckresample -num_points 20 -nthreads 0 {stream_name}.tck" + f" {stream_name}_resampled.tck") + + for my_command in [convert, resample]: + process = subprocess.Popen(my_command.split(), stdout=subprocess.PIPE) + output, error = process.communicate() + + # Get orthogonal streamlines + tck_v = load_tractogram(f"{stream_name}_resampled.tck", dwi_ref, + #shifted_origin=False # corner of the voxel (DEPRECATED) + to_origin=Origin('corner') + # ^ NIFTI center, TRACKVIS corner (of the voxel) + ) + v_strms = tck_v.streamlines + + #v_strms_data = v_strms.data.reshape([len(v_strms), *v_strms[0].shape]) + v_strms_data = v_strms.get_data().reshape([len(v_strms), *v_strms[0].shape]) + h_strms_data = v_strms_data.transpose(1,0,2) + + # for s in v_strms_data: plt.plot(*s[:,:2].T, color='blue') + # for s in v_strms_data.transpose(1,0,2): plt.plot(*s[:,:2].T,color='gray') + # plt.gca().axis('equal') + + #sft_h = StatefulTractogram(h_strms_data, dwi_ref, Space.RASMM) + sft_h = StatefulTractogram(h_strms_data, dwi_ref, space=Space.RASMM, + origin=Origin('corner')) + save_tractogram(sft_h, f"{stream_name}_resampled_h.tck") + + +