This is an oddly specific, possibly vague, post that’s really just intended for my personal future reference. It’s a follow-up to this post about calculating gray matter volume from CAT12-produced ROIs and to this post about manually creating and editing ROIs with ITK-SNAP.

My goal here is to calculate total cerebrospinal fluid (CSF) volume for the left and right lateral ventricles in each subject’s native space.

ROI Creation

So, similar to my gray matter volume ROIs, I’ve saved the ventricle ROIs of interest based on the native space Neuromorphometrics atlas outputted by CAT12’s expert mode.

## ------------------------------------------------------------------------------------------------------ ##
## 		                    SAVE NATIVE SPACE ROIs FROM ATLASES				 							 
## ------------------------------------------------------------------------------------------------------ ##

# Use fslmaths to save the specific native space ROIs we're potentially interested in
if [ $save_native_ROIs == 1 ]
then  
   for SUB in "${subjects[@]}"; do
   # Set atlases 
    subjCSFpath=$CSF_folder/${SUB}/
		
   # Lateral ventricle = 25, 26; 
   fslmaths $subjCSFpath/neuromorphometrics_T1_${SUB}.nii.gz -thr 24.99999999 -uthr 25.00000001 -bin 
       $subjCSFpath/neuromorph_LLatVent_${SUB}.nii.gz
   fslmaths $subjCSFpath/neuromorphometrics_T1_${SUB}.nii.gz -thr 25.99999999 -uthr 26.00000001 -bin   
       $subjCSFpath/neuromorph_RLatVent_${SUB}.nii.gz
		
   # Note done with saving native space ROIs 
   echo 'Done saving native space ROIs for subj:' ${SUB}
   echo "Total run time so far: $((SECONDS/60)) minutes and $((SECONDS%60)) seconds";
		
done 
fi 

Next, as described here, I decided that I didn’t like the ventricle coverage of these automatically-piped-out ROIs, particularly for my older adults. So, I decided to hand correct them in ITK-SNAP.

Calculating CSF Volume in the ROIs

Now, I’m just going to go through several iterations of calculating CSF volume in different versions of my ventricle ROIs, to ensure that I know precisely what’s going on — similar to what I did in my gray matter volume calculation post, except abbreviated, because I “know” what I’m doing now. We’ll work with my subject #1001, who is a ‘healthy’ young adult.

1) CAT12’s automated calculation of left lateral ventricular volume

First, let’s start off with CAT12’s calculation based on the Neuromorphometrics atlas labels, warped to the subject’s native space. CAT12 says 6.5076 mL for left lateral ventricle. Note that for ventricles, you need to look at the the CSF spreadsheet (and not, e.g., the gray matter spreadsheet). The CSF spreadsheet is named ROI_catROI_neuromorphometrics_Vcsf.csv by default.

CAT12’s spreadsheet of CSF ROI values

2) Kathleen’s calculation of CSF volume using the “whole” ROI mask

See the end of this post for more complete code to pull CSF volumes. For now, we’ll just focus on what changes between each case.

First, I’ll note that the volume per voxel will be the same in each case. The CAT12-produced CSF segments (from which you create ventricle ROIs and pull ventricular volume) all have the same voxel sizes. This is ~0.80 mm x 0.80 mm x 0.80 mm = 0.512 mm3.

Next, I’ll note that in each case I used the capital -V flag to pull the number of non-zero voxels in the ROI mask. This number will change between each example, but the code for pulling it will be the same.

The primary flag that changes between my different examples is lowercase -m (mean of all voxels in the ROI mask) versus -M (mean of all non-zero voxels in the ROI mask).

So, first I calculated ventricular volume using the “whole” ROI mask outputted by CAT12, to match CAT12’s automated calculation. That is, I used the -m flag to calculate mean intensity based on all voxels in the mask.

meanIntens=$(fslstats $p3_image -k $ROI -m);

This gave me 6.5076 mL of CSF in the left lateral ventricle, which matches the CAT12 output!

Number of voxels in neuromorph_LLatVent in 1001 is: 15781 voxels
Mean intensity for neuromorph_LLatVent in 1001 is: 0.805408

CSF for neuromorph_LLatVent in 1001 is: 6507.593547 mm3
Or: 6.50759354700000000000 mL

And a histogram like this:

kathleen’s calc to match CAT12 output

You can see that there’s 1,385 voxels with intensity = 0 contributing to this calculation here, so these bring down the mean intensity a bit. But they still come out to produce the same result as the CAT12 spreadsheet because we’re also counting the voxels with intensity = 0 in the -V total voxel count — so, while we’re driving down mean intensity a little, we’re also including more total voxels in the multiplication to get total left lateral ventricle volume in mL.

3) Kathleen’s calculation of CSF volume using the “thresholded” ROI mask

This version goes through the calculation in a way that makes more logical sense to me, though the end result (volume in mL) is identical to version #2 above.

Here, we first threshold the ROI based on the native space CSF map (i.e., the p3*.nii output from CAT12). That is, we make a new ventricle mask that excludes any voxel with intensity = 0. This isn’t going to visually look too much different from the original mask. That’s because (different from the gray matter ROIs), the initial ventricle ROIs only have a relatively small number of voxels with intensity = 0 (i.e., ~1385/15781=~8.78%). Nonetheless, this cleans up the ventricle mask a little (because intensity=0 is definitely not CSF). And, it (imo) makes the volume calculation make more sense.

There’s many ways to do this step, but here’s one way that uses SPM in MATLAB.

function thresh_ROI(subjID, ROI)

% Set up SPM 
addpath /blue/rachaelseidler/share/FromExternal/Research_Projects_UF/spm12

for i=1:length(subjID)
 %% -------------------------------------------------------------------------------------------------------- %%
 %% 		                               GENERAL SET-UP											
 %% -------------------------------------------------------------------------------------------------------- %%
 % Note step
     disp(sprintf('Starting ROI thresholding for: %s',sprintf(subjID{i})))

 % Set up paths
     subjID{i}
     T1_folder = ['/blue/rachaelseidler/share/FromExternal/Research_Projects_UF/GABA_Aging_KH/Preproc_T1_ROIs/',subjID{i}]; 


 %% -------------------------------------------------------------------------------------------------------- %%
 %% 		                               THRESHOLD ROI												
 %% -------------------------------------------------------------------------------------------------------- %%
 % Select which files to input into imcalc 
   ROI
   file1 = spm_select('ExtFPList', fullfile(T1_folder),'^p3T1_.*\.nii$',1)
   file2 = spm_select('ExtFPList', fullfile(T1_folder),ROI)

 % Run imacalc to threshold ROI by retaining ROI mask only over voxels with intensity > 0
   clear matlabbatch
   matlabbatch{1}.spm.util.imcalc.input = [cellstr(file1); cellstr(file2)];
   matlabbatch{1}.spm.util.imcalc.output = [ROI{1},'_thresh0_',subjID{i}];
   matlabbatch{1}.spm.util.imcalc.outdir = cellstr(T1_folder);
   matlabbatch{1}.spm.util.imcalc.expression = 'i2.*(i1>0)';
   matlabbatch{1}.spm.util.imcalc.var = struct('name', {}, 'value', {});
   matlabbatch{1}.spm.util.imcalc.options.dmtx = 0;
   matlabbatch{1}.spm.util.imcalc.options.mask = 0;
   matlabbatch{1}.spm.util.imcalc.options.interp = 1;
   matlabbatch{1}.spm.util.imcalc.options.dtype = 4;
   spm_jobman('run',matlabbatch);
   disp(sprintf('Done ROI thresholding for: %s',sprintf(subjID{i})))
	
end
end

This now gives me a thresholded ventricle mask with all voxels where intensity=0 have been removed.

Now I can run similar code to above, except here I’ll use -M instead of -m to calculate mean intensity.

meanIntens=$(fslstats $p3_image -k $ROI -M);

This again gives us 6.5076 mL of CSF in the left lateral ventricle, which again matches the CAT12 spreadsheet output (hooray!).

Number of voxels in neuromorph_LLatVent_thresh0 in 1001 is: 14458 voxels
Mean intensity for neuromorph_LLatVent_thresh0 in 1001 is: 0.879108

CSF for neuromorph_LLatVent_thresh0 in 1001 is: 6507.593453 mm3
Or: 6.50759345300000000000 mL

And, it gives us a slightly-nicer-looking histogram, without the 1,385-voxel blip at intensity = 0-0.1.

kathleen’s second way of matching the CAT12 output

Do note that the mean intensity is different here compared to the above case. You likely will always be concerned with mL rather than mean intensity for ventricular volume, but it is worth noting that this would be the “correct” approach compared to the above one that includes intensity=0 voxels if you wanted to actually use the mean intensity for anything.

Also, note that there are now fewer voxels. In fact, there are (15,781 total voxels) – (1385 voxels with intensity < 0.1) + (62 voxels that had 0 < intensity < 0.1) = 14,458 voxels in the new thresholded mask.

To clarify: I say above that there are ~1,385 voxels with intensity=0 in the mask (see the first histogram above) because that is the number that fell into the 0-0.1 bin. The “correct” way to say this is actually that there are 1,385 voxels with intensity < 0.1. If we threshold based on i1>0, like in the above imcalc code, we’ll still have some voxels with 0 < intensity < 0.1; in this case, we have 62 of such voxels. If we were instead to threshold at something higher, like i1>0.099, then we’d get a histogram without anything in the 0-0.1 bin. However, this introduces some calculation error compared to the CAT12 calculation, which uses a 0 threshold, not a 0.1 threshold. [Anyhow, I just reran like this to convince myself I wasn’t still including 62 intensity=0 voxels in my calculation. Do this for an understanding check, but not for a final calculation.]

wrong — just an understanding check; thresholding at i1>0.099 to exclude all 1,365 voxels that were initially in the 0-0.1 bin; instead you *do* want to just threshold at i1>0, at least to match CAT12’s approach

TL;DR regarding CAT12 spreadsheet values

So, I’ve just shown that I understand where the CAT12 spreadsheet values are coming from. Cool. The TL;DR of this, however, is that you can go ahead and just use the CAT12 spreadsheet values without paying much attention to what’s happening under the hood, as long as you trust the ROI masks. This is where you may want to consider options for visually checking the ROI masks, and potentially correcting them if they didn’t come out well — particularly for special populations like aging brains.

As I described in more detail here, I wanted to hand correct the ventricle masks. I’ve done this and now want to compare the same two options from above on my hand-corrected masks. Primarily, I’m looking to see how the hand-corrected values compare to the uncorrected ones. And, whether I can replicate the same two methods as above on my hand-corrected masks.

3) Kathleen’s calculation of CSF volume using the “whole” hand-corrected ROI mask

First, I used the whole mask (no thresholding applied first) and the -m method of calculating mean intensity.

meanIntens=$(fslstats $p3_image -k $ROI -m);

This gave me 7.46 mL of left lateral ventricular volume.

Number of voxels in neuromorph_LLatVent_ITKcorr in 1001 is: 16485 voxels
Mean intensity for neuromorph_LLatVent_ITKcorr in 1001 is: 0.883512

CSF for neuromorph_LLatVent_ITKcorr in 1001 is: 7457.124003 mm3
Or: 7.45712400300000000000 mL

And a histogram like this, which indicates 356 voxels with 0 < intensity < 0.1.

kathleen’s first calc using her hand-corrected mask

In general, the increase in volume with hand correction makes sense (i.e., 6.51 mL to 7.46 mL). My hand-corrected masks are a bit bigger than the uncorrected ones — because the original mask was not grabbing the entire ventricle, as shown below. The red mask is the original CAT12-outputted mask based on inverse warping the Neuromorphometrics atlas, and the green is my hand-corrected mask. (Although I didn’t test this numerically… but probably should, for fun!) this underestimation effect seemed to be even worse for older adults (in this post and the image below, we’re working with a ‘healthy’ young adult).

green = my hand-corrected mask; red = the original mask outputted by CAT12 neuromorph atlas warping

4) Kathleen’s calculation of CSF volume using the “thresholded” hand-corrected ROI mask

Next, I’ll redo my calculations with thresholding of i1>0 first, just as I did above in #3. Then, when I use the -M flag to calculate mean intensity, I should end up with a volume that is identical to the #3 result.

meanIntens=$(fslstats $p3_image -k $ROI -m);

This is the case! I again get 7.46 mL for the (hand-corrected) left lateral ventricle.

Number of voxels in neuromorph_LLatVent_ITKcorr_thresh0 in 1001 is: 16173 voxels
Mean intensity for neuromorph_LLatVent_ITKcorr_thresh0 in 1001 is: 0.900556
CSF for neuromorph_LLatVent_ITKcorr_thresh0 in 1001 is: 7457.122400 mm3
Or: 7.45712240000000000000 mL

And now I also get a nicer-looking histogram, with a blip of 44 voxels (instead of 356) for the 0-0.1 bin.

kathleen’s second calc using her hand-corrected, thresholded mask

TL;DR regarding calculating ventricular volume using hand-corrected masks

I’ll be moving forward with the volumes calculated using the last approach described here (approach #4). That is, I’ve hand corrected my ventricle masks from CAT12 (because I think the automated ones under-estimate the ventricle) >> then thresholded the masks to remove voxels with intensity=0 >> then calculated # of voxels and mean intensity based on this thresholded mask >> and used these values to calculate ventricular volume.

More complete code for pulling ROI volumes

Just refer to the FSL documentation, and the descriptions above, to understand which flags you need.

## ------------------------------------------------------------------------------------------------------ ##
## 				CALC ROI SIZE, MEAN INTENSITY & FW WITHIN MASK									
## ------------------------------------------------------------------------------------------------------ ##
# First, calc size of ROI. In MNI space, this was the same for everyone. Now, in native space, it's not. 
#   Note the capital V. This will output two numbers: 
#     The first is the number of voxels greater than 0. 
#     The second is their volume in cubic millimiters. 
#     The two numbers should be the same if the ROI is 1x1x1mm3.
# Then, use FSL to calc mean intensity of voxels within the ROI mask. 
#   Multiply: (mean intensity) x (# of voxels) x (vol per voxel). 
#     Vol per voxel = 0.8x0.8x0.8 for the CAT12 CSF images
#        (fslhd: 0.80 in each dimension; SPM display: 0.8x0.8x0.8) 
#	 So vol/voxel = 0.512 mm3 
#	 So, for native space, will need to multiply (mean intensity) x (# of voxels) x (vol per voxel)
# Note: will return values in mm3. Divide by 1000 to get values in mL. 
#	E.g., 9011.7 mm3 = 9.01 mL. 
## ------------------------------------------------------------------------------------------------------ ##

if [ $pull_ROI_vols == 1 ] 
then 
   # Delete outfile if it already exists (so that echo >> won't just append to end of an old file) 
    if [ ! -f $outfile ]; then echo "No outfile found; proceeding."; echo ""; 
    else rm $outfile; echo "Removed old outfile; starting fresh"; echo ""; fi

   # Pull CSF vol within ROI mask 
    for SUB in "${subjects[@]}"; do
	# Set ROI & volume per voxel 
	subj_ROI_folder=/"$CSF_folder"/${SUB}
	ROI=$subj_ROI_folder/$ROIname'_'${SUB}.nii*
	p3_image=$subj_ROI_folder/p3T1_${SUB}.nii
	volPerVoxel=0.512
		
	# Calc size of ROI 
	numVoxels=$(fslstats $ROI -V | awk '{print $1}')
	echo "Number of voxels in $ROIname in ${SUB} is:" $numVoxels "voxels"; 
		
	# Calc mean intensity within mask 
	meanIntens=$(fslstats $p3_image -k $ROI -M); 
	echo "Mean intensity for $ROIname in ${SUB} is:" $meanIntens;		
			
	# Multiply mean intensity by number of voxels 
	CSF=$(echo $meanIntens*$numVoxels*$volPerVoxel | bc)
	CSF_mL=$(echo $CSF/1000 | bc -l)
		
	echo "CSF for $ROIname in ${SUB} is:" $CSF  "mm3";
	echo "Or:" $CSF_mL "mL"; echo ""; 

    # Save to output file  
    # 1 mm3 = 0.001 mL; so e.g., get 4,376 mm3 = 4.376 mL 
    echo -e "${SUB}\t$meanIntens\t$numVoxels\t$CSF\t$CSF_mL" >> "$outfile"
			
done
fi 

Update: -m vs. -M if you’re using thresholded images

I realized post hoc that it doesn’t actually matter whether you use -m (all voxels) or -M (non-zero voxels only) if you’ve taken the liberty of thresholding to remove all of the zero voxels (i.e., i1>0). Both will return an identical mean (duh). The case in which it does matter to use -m instead of -M is when you haven’t thresholded to remove 0 voxels and thus you want to mean over all voxels (and you’re including voxels with intensity = 0 in the total voxel count) — as in examples #1 and #3.

Categories:

Archives