diff --git a/bin/dwibiascorrect b/bin/dwibiascorrect index 4b701d9713..be497005a6 100755 --- a/bin/dwibiascorrect +++ b/bin/dwibiascorrect @@ -51,10 +51,6 @@ if app.args.fsl: app.error('Could not find FSL program fast; please verify FSL install') fsl_suffix = fsl.suffix() - if fast_cmd == 'fast': - fast_suffix = fsl_suffix - else: - fast_suffix = '.nii.gz' elif app.args.ants: @@ -101,10 +97,12 @@ else: run.command('dwiextract in.mif - -bzero | mrmath - mean mean_bzero.mif -axis 3') if app.args.fsl: + # FAST doesn't accept a mask input; therefore need to explicitly mask the input image run.command('mrcalc mean_bzero.mif mask.mif -mult - | mrconvert - mean_bzero_masked.nii -stride -1,+2,+3') run.command(fast_cmd + ' -t 2 -o fast -n 3 -b mean_bzero_masked.nii') - bias_path = 'fast_bias' + fast_suffix + bias_path = fsl.findImage('fast_bias') + elif app.args.ants: # If the mask image was provided manually, and doesn't match the input image perfectly @@ -118,7 +116,7 @@ elif app.args.ants: run.command('mrconvert mean_bzero.mif mean_bzero.nii -stride +1,+2,+3') run.command('mrconvert mask.mif mask.nii -stride +1,+2,+3') bias_path = 'bias.nii' - run.command('N4BiasFieldCorrection -d 3 -i mean_bzero.nii -w mask.nii -o [corrected.nii,' + bias_path + '] -s 2 -b [150] -c [200x200,0.0]') + run.command('N4BiasFieldCorrection -d 3 -i mean_bzero.nii -w mask.nii -o [corrected.nii,' + bias_path + '] -b [150,3] -c [1000x1000,0.0]') run.command('mrcalc in.mif ' + bias_path + ' -div result.mif') run.command('mrconvert result.mif ' + path.fromUser(app.args.output, True) + (' -force' if app.force else '')) diff --git a/bin/dwipreproc b/bin/dwipreproc index 8802f4d5ae..e0003d7244 100755 --- a/bin/dwipreproc +++ b/bin/dwipreproc @@ -2,7 +2,7 @@ # Script for performing DWI pre-processing using FSL 5.0 tools eddy / topup / applytopup -# This script is generally the first operation that will be applied to diffusion image data (with the possible exception of dwidenoise). The precise details of how this image pre-processing takes place depends heavily on the DWI acquisition; specifically, the presence or absence of reversed phase-encoding data for the purposes of EPI susceptibility distortion correction. +# This script is generally one of the first operations that will be applied to diffusion image data. The precise details of how this image pre-processing takes place depends heavily on the DWI acquisition; specifically, the presence or absence of reversed phase-encoding data for the purposes of EPI susceptibility distortion correction. # The script is capable of handling a wide range of DWI acquisitions with respect to the design of phase encoding directions. This is dependent upon information regarding the phase encoding being embedded within theimage headers. The relevant information should be captured by MRtrix when importing DICOM images; it should also be the case for BIDS-compatible datasets. If the user expects this information to be present within the image headers, the -rpe_header option must be specified. @@ -24,7 +24,7 @@ if not os.path.isdir(lib_folder): sys.path.insert(0, lib_folder) -import math +import math, shutil from distutils.spawn import find_executable from mrtrix3 import app, file, fsl, image, path, phaseEncoding, run @@ -182,6 +182,27 @@ if not len(grad) == num_volumes: do_topup = (not PE_design == 'None') +def grads_match(one, two): + # Dot product between gradient directions + # First, need to check for zero-norm vectors: + # - If both are zero, skip this check + # - If one is zero and the other is not, volumes don't match + # - If neither is zero, test the dot product + if any([val for val in one[0:3]]): + if not any([val for val in two[0:3]]): + return False + dot_product = one[0]*two[0] + one[1]*two[1] + one[2]*two[2] + if abs(dot_product) < 0.999: + return False + elif any([val for val in two[0:3]]): + return False + # b-value + if abs(one[3]-two[3]) > 10.0: + return False + return True + + + # Manually generate a phase-encoding table for the input DWI based on user input dwi_manual_pe_scheme = None topup_manual_pe_scheme = None @@ -220,26 +241,39 @@ if manual_pe_dir: # If -rpe_all, need to scan through grad and figure out the pairings # This will be required if relying on user-specified phase encode direction # It will also be required at the end of the script for the manual recombination + # Update: The possible permutations of volume-matched acquisition is limited within the + # context of the -rpe_all option. In particular, the potential for having more + # than one b=0 volume within each half means that it is not possible to permit + # arbitrary ordering of those pairs, since b=0 volumes would then be matched + # despite having the same phase-encoding direction. Instead, explicitly enforce + # that volumes must be matched between the first and second halves of the DWI data. elif PE_design == 'All': - grad_matchings = [ num_volumes ] * num_volumes + if num_volumes%2: + app.error('If using -rpe_all option, input image must contain an even number of volumes') + grads_matched = [ num_volumes ] * num_volumes grad_pairs = [ ] - for index1 in range(num_volumes): - if grad_matchings[index1] == num_volumes: # As yet unpaired - for index2 in range(index1+1, num_volumes): - if grad_matchings[index2] == num_volumes: # Also as yet unpaired - if grad[index1] == grad[index2]: - grad_matchings[index1] = index2; - grad_matchings[index2] = index1; + app.debug('Commencing gradient direction matching; ' + str(num_volumes) + ' volumes') + for index1 in range(int(num_volumes/2)): + if grads_matched[index1] == num_volumes: # As yet unpaired + for index2 in range(int(num_volumes/2), num_volumes): + if grads_matched[index2] == num_volumes: # Also as yet unpaired + if grads_match(grad[index1], grad[index2]): + grads_matched[index1] = index2; + grads_matched[index2] = index1; grad_pairs.append([index1, index2]) + app.debug('Matched volume ' + str(index1) + ' with ' + str(index2) + ': ' + str(grad[index1]) + ' ' + str(grad[index2])) + break + else: + app.error('Unable to determine matching reversed phase-encode direction volume for DWI volume ' + str(index1)) if not len(grad_pairs) == num_volumes/2: - app.error('Unable to determine matching DWI volume pairs for reversed phase-encode combination') + app.error('Unable to determine complete matching DWI volume pairs for reversed phase-encode combination') # Construct manual PE scheme here: # Regardless of whether or not there's a scheme in the header, need to have it: # if there's one in the header, want to compare to the manually-generated one dwi_manual_pe_scheme = [ ] for index in range(0, num_volumes): line = list(manual_pe_dir) - if grad_matchings[index] < index: + if index >= int(num_volumes/2): line = [ (-i if i else 0.0) for i in line ] line.append(trt) dwi_manual_pe_scheme.append(line) @@ -435,22 +469,36 @@ if do_topup: else: run.command('mrinfo dwi.mif -export_pe_eddy applytopup_config.txt applytopup_indices.txt') - applytopup_index_list = [ ] + # Update: Call applytopup separately for each unique phase-encoding + # This should be the most compatible option with more complex phase-encoding acquisition designs, + # since we don't need to worry about applytopup performing volume recombination + # Plus, recombination doesn't need to be optimal; we're only using this to derive a brain mask applytopup_image_list = [ ] index = 1 with open('applytopup_config.txt', 'r') as f: for line in f: - image_path = 'dwi_pe_' + str(index) + '.nii' - run.command('dwiextract dwi.mif' + import_dwi_pe_table_option + ' -pe ' + ','.join(line.split()) + ' ' + image_path) - applytopup_index_list.append(str(index)) - applytopup_image_list.append(image_path) + input_path = 'dwi_pe_' + str(index) + '.nii' + json_path = 'dwi_pe_' + str(index) + '.json' + temp_path = 'dwi_pe_' + str(index) + '_topup' + fsl_suffix + output_path = 'dwi_pe_' + str(index) + '_topup.mif' + run.command('dwiextract dwi.mif' + import_dwi_pe_table_option + ' -pe ' + ','.join(line.split()) + ' - | mrconvert - ' + input_path + ' -json_export ' + json_path) + run.command(applytopup_cmd + ' --imain=' + input_path + ' --datain=applytopup_config.txt --inindex=' + str(index) + ' --topup=field --out=' + temp_path + ' --method=jac') + file.delTempFile(input_path) + temp_path = fsl.findImage(temp_path) + run.command('mrconvert ' + temp_path + ' ' + output_path + ' -json_import ' + json_path) + file.delTempFile(json_path) + file.delTempFile(temp_path) + applytopup_image_list.append(output_path) index += 1 - # Finally ready to run applytopup - run.command(applytopup_cmd + ' --imain=' + ','.join(applytopup_image_list) + ' --datain=applytopup_config.txt --inindex=' + ','.join(applytopup_index_list) + ' --topup=field --out=dwi_post_topup' + fsl_suffix + ' --method=jac') - # Use the initial corrected volumes to derive a brain mask for eddy - run.command('mrconvert dwi_post_topup' + fsl_suffix + ' -grad grad.b - | dwi2mask - - | maskfilter - dilate - | mrconvert - mask.nii -datatype float32 -stride -1,+2,+3') + if len(applytopup_image_list) == 1: + run.command('dwi2mask ' + applytopup_image_list[0] + ' - | maskfilter - dilate - | mrconvert - mask.nii -datatype float32 -stride -1,+2,+3') + else: + run.command('mrcat ' + ' '.join(applytopup_image_list) + ' - | dwi2mask - - | maskfilter - dilate - | mrconvert - mask.nii -datatype float32 -stride -1,+2,+3') + + for entry in applytopup_image_list: + file.delTempFile(entry) eddy_in_topup_option = ' --topup=field' @@ -491,22 +539,26 @@ if not os.path.isfile(bvecs_path): # The phase-encoding scheme needs to be checked also volume_matchings = [ num_volumes ] * num_volumes volume_pairs = [ ] +app.debug('Commencing gradient direction matching; ' + str(num_volumes) + ' volumes') for index1 in range(num_volumes): if volume_matchings[index1] == num_volumes: # As yet unpaired for index2 in range(index1+1, num_volumes): if volume_matchings[index2] == num_volumes: # Also as yet unpaired # Here, need to check both gradient matching and reversed phase-encode direction - if not any(dwi_pe_scheme[index1][i] + dwi_pe_scheme[index2][i] for i in range(0,3)) and grad[index1] == grad[index2]: + if not any(dwi_pe_scheme[index1][i] + dwi_pe_scheme[index2][i] for i in range(0,3)) and grads_match(grad[index1], grad[index2]): volume_matchings[index1] = index2; volume_matchings[index2] = index1; volume_pairs.append([index1, index2]) + app.debug('Matched volume ' + str(index1) + ' with ' + str(index2) + '\n' + + 'Phase encoding: ' + str(dwi_pe_scheme[index1]) + ' ' + str(dwi_pe_scheme[index2]) + '\n' + + 'Gradients: ' + str(grad[index1]) + ' ' + str(grad[index2])) + break - -if not len(volume_pairs) == num_volumes/2: +if not len(volume_pairs) == int(num_volumes/2): # Convert the resulting volume to the output image, and re-insert the diffusion encoding - run.command('mrconvert dwi_post_eddy' + fsl_suffix + ' result.mif' + stride_option + ' -fslgrad ' + bvecs_path + ' bvals') + run.command('mrconvert ' + fsl.findImage('dwi_post_eddy') + ' result.mif' + stride_option + ' -fslgrad ' + bvecs_path + ' bvals') else: app.console('Detected matching DWI volumes with opposing phase encoding; performing explicit volume recombination') @@ -533,6 +585,14 @@ else: float(bvecs[1][pair[0]]) + float(bvecs[1][pair[1]]), float(bvecs[2][pair[0]]) + float(bvecs[2][pair[1]]) ] norm2 = bvec_sum[0]*bvec_sum[0] + bvec_sum[1]*bvec_sum[1] + bvec_sum[2]*bvec_sum[2] + # If one diffusion sensitisation gradient direction is reversed with respect to + # the other, still want to enable their recombination; but need to explicitly + # account for this when averaging the two directions + if norm2 < 0.0: + bvec_sum = [ float(bvecs[0][pair[0]]) - float(bvecs[0][pair[1]]), + float(bvecs[1][pair[0]]) - float(bvecs[1][pair[1]]), + float(bvecs[2][pair[0]]) - float(bvecs[2][pair[1]]) ] + norm2 = bvec_sum[0]*bvec_sum[0] + bvec_sum[1]*bvec_sum[1] + bvec_sum[2]*bvec_sum[2] # Occasionally a bzero volume can have a zero vector if norm2: factor = 1.0 / math.sqrt(norm2) @@ -559,12 +619,12 @@ else: # Detect this, and manually replace the transform if necessary # (even if this doesn't cause an issue with the subsequent mrcalc command, it may in the future, it's better for # visualising the script temporary files, and it gives the user a warning about an out-of-date FSL) - field_map_image = 'field_map' + fsl_suffix + field_map_image = fsl.findImage('field_map') if not image.match('topup_in.nii', field_map_image): app.warn('topup output field image has erroneous header; recommend updating FSL to version 5.0.8 or later') - run.command('mrtransform ' + field_map_image + ' -replace topup_in.nii field_map_fix' + fsl_suffix) + run.command('mrtransform ' + field_map_image + ' -replace topup_in.nii field_map_fix.mif') file.delTempFile(field_map_image) - field_map_image = 'field_map_fix' + fsl_suffix + field_map_image = 'field_map_fix.mif' @@ -594,18 +654,28 @@ else: run.command('mrcalc ' + jacobian_path + ' ' + jacobian_path + ' -mult weight' + str(index+1) + '.mif') file.delTempFile(jacobian_path) + # If eddy provides its main image output in a compressed format, the code block below will need to + # uncompress that image independently for every volume pair. Instead, if this is the case, let's + # convert it to an uncompressed format before we do anything with it. + eddy_output = fsl.findImage('dwi_post_eddy') + if eddy_output.endswith('.gz'): + run.command('mrconvert ' + eddy_output + ' dwi_post_eddy.nii') + file.delTempFile(eddy_output) + eddy_output = 'dwi_post_eddy.nii' + # This section extracts the two volumes corresponding to each reversed phase-encoded volume pair, and # derives a single image volume based on the recombination equation combined_image_list = [ ] for index, volumes in enumerate(volume_pairs): pe_indices = [ eddy_indices[i] for i in volumes ] - run.command('mrconvert dwi_post_eddy' + fsl_suffix + ' volume0.mif -coord 3 ' + str(volumes[0])) - run.command('mrconvert dwi_post_eddy' + fsl_suffix + ' volume1.mif -coord 3 ' + str(volumes[1])) + run.command('mrconvert ' + eddy_output + ' volume0.mif -coord 3 ' + str(volumes[0])) + run.command('mrconvert ' + eddy_output + ' volume1.mif -coord 3 ' + str(volumes[1])) # Volume recombination equation described in Skare and Bammer 2010 run.command('mrcalc volume0.mif weight' + str(pe_indices[0]) + '.mif -mult volume1.mif weight' + str(pe_indices[1]) + '.mif -mult -add weight' + str(pe_indices[0]) + '.mif weight' + str(pe_indices[1]) + '.mif -add -divide 0.0 -max combined' + str(index) + '.mif') combined_image_list.append('combined' + str(index) + '.mif') - file.delTempFile('volume0.mif') - file.delTempFile('volume1.mif') + os.remove('volume0.mif') + os.remove('volume1.mif') + file.delTempFile(eddy_output) for index in range(0, len(eddy_config)): file.delTempFile('weight' + str(index+1) + '.mif') @@ -613,8 +683,8 @@ else: # Finally the recombined volumes must be concatenated to produce the resulting image series run.command('mrcat ' + ' '.join(combined_image_list) + ' - -axis 3 | mrconvert - result.mif -fslgrad bvecs_combined bvals_combined' + stride_option) - for path in combined_image_list: - file.delTempFile(path) + for entry in combined_image_list: + file.delTempFile(entry) diff --git a/bin/labelsgmfix b/bin/labelsgmfix index b1488b5913..2c87a17d74 100755 --- a/bin/labelsgmfix +++ b/bin/labelsgmfix @@ -83,7 +83,7 @@ if app.args.premasked: first_input_is_brain_extracted = ' -b' run.command(first_cmd + ' -s ' + ','.join(structure_map.keys()) + ' -i T1.nii' + first_input_is_brain_extracted + ' -o first') -# Generate an empty image that will be used to contruct the new SGM nodes +# Generate an empty image that will be used to construct the new SGM nodes run.command('mrcalc parc.mif 0 -min sgm.mif') # Read the local connectome LUT file diff --git a/bin/population_template b/bin/population_template index 82cb2cbd8c..8989bcc99b 100755 --- a/bin/population_template +++ b/bin/population_template @@ -188,6 +188,7 @@ nloptions.add_argument('-nl_grad_step', default='0.5', help='The gradient step s options = app.cmdline.add_argument_group('Input, output and general options') options.add_argument('-type', help='Specifiy the types of registration stages to perform. Options are "rigid" (perform rigid registration only which might be useful for intra-subject registration in longitudinal analysis), "affine" (perform affine registration) and "nonlinear" as well as cominations of registration types: %s. Default: rigid_affine_nonlinear' % ', '.join('"'+x+'"' for x in registration_modes if "_" in x), default='rigid_affine_nonlinear') +options.add_argument('-voxel_size', help='Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma separated values.') options.add_argument('-initial_alignment', default='mass', help='Method of alignment to form the initial template. Options are "mass" (default), "geometric" and "none".') options.add_argument('-mask_dir', help='Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly') options.add_argument('-warp_dir', help='Output a directory containing warps from each input to the template. If the folder does not exist it will be created') @@ -216,6 +217,18 @@ if len(inFiles) <= 1: else: app.console('Generating a population-average template from ' + str(len(inFiles)) + ' input images') +voxel_size = None +if app.args.voxel_size: + voxel_size = app.args.voxel_size.split() + if len(voxel_size) == 1: + voxel_size = voxel_size * 3 + try: + if len(voxel_size) != 3: + raise + [float(v) for v in voxel_size] + except: + app.error('voxel size needs to be a single or three comma separated floating point numbers, received: '+str(app.args.voxel_size)) + initial_alignment = app.args.initial_alignment if initial_alignment not in ["mass", "geometric", "none"]: message.error('initial_alignment must be one of ' + " ".join(["mass", "geometric", "none"])); @@ -428,7 +441,10 @@ app.console('Generating initial template') input_filenames = [] for i in input: input_filenames.append (abspath(i.directory, i.filename)); -run.command('mraverageheader ' + ' '.join(input_filenames) + ' average_header.mif' ' -fill') +if voxel_size is None: + run.command('mraverageheader ' + ' '.join(input_filenames) + ' average_header.mif -fill') +else: + run.command('mraverageheader -fill ' + ' '.join(input_filenames) + ' - | mrresize - -voxel '+','.join(voxel_size)+' average_header.mif') # crop average space to extent defined by original masks if useMasks: @@ -480,7 +496,10 @@ else: ' ' + os.path.join('masks_transformed', i.prefix + '_translated.mif')) # update average space to new extent run.command('mraverageheader ' + ' '.join([os.path.join('input_transformed', i.prefix + '_translated.mif') for i in input]) + ' average_header_tight.mif') - run.command('mrpad -uniform 10 average_header_tight.mif average_header.mif -force') + if voxel_size is None: + run.command('mrpad -uniform 10 average_header_tight.mif average_header.mif -force') + else: + run.command('mrpad -uniform 10 average_header_tight.mif - | mrresize - -voxel '+','.join(voxel_size)+' average_header.mif -force') run.function(remove, 'average_header_tight.mif') if useMasks: # reslice masks diff --git a/check_memalign b/check_memalign index 1efdbdb119..ee0de9ac22 100755 --- a/check_memalign +++ b/check_memalign @@ -21,21 +21,28 @@ for f in $(find cmd core src -type f -name '*.h' -o -name '*.cpp' | grep -v '_mo "$f" -ef "core/signal_handler.cpp" ]] && continue -# detect classes not declared MEMALIGN or NOMEMALIGN: - res=$( \ - cat $f | \ + + + # process the file to strip comments, macros, etc: + cat $f | \ # remove C preprocessor macros: - grep -v '^#' | \ + grep -v '^#' | \ # remove C++ single-line comments: - perl -pe 's|//.*$||' | \ + perl -pe 's|//.*$||' | \ # remove all newlines to make file one long line: - tr '\n' ' ' | \ + tr '\n' ' ' | \ # remove any duplicate spaces: - perl -pe 's|\s+| |g' | \ + perl -pe 's|\s+| |g' | \ # remove C-style comments: - perl -pe 's|/\*.*?\*/||g' | \ + perl -pe 's|/\*.*?\*/||g' | \ # remove quoted strings: - perl -pe 's/(")(\\"|.)*?"//g' | \ + perl -pe 's/(")(\\"|.)*?"//g' > .check_memalign.tmp + + +# detect classes not declared MEMALIGN or NOMEMALIGN: + + res=$( + cat .check_memalign.tmp | \ # remove any text within a template declaration (i.e. within <>): perl -pe 's|<[^{};<]*?>||g' | \ # and do it multiple times to handle nested declarations: @@ -47,49 +54,31 @@ for f in $(find cmd core src -type f -name '*.h' -o -name '*.cpp' | grep -v '_mo # remove matches that correspond to an enum class declaration: grep -Ev '\benum\s*class\b' | \ # remove matches that are properly specified: - grep -Ev '\b(class|struct)\b[^;{]*?{(\s*(MEMALIGN\s*\([^\)]*\)|NOMEMALIGN))' + grep -Ev '\b(class|struct)\b[^;{]*?{(\s*(MEMALIGN\s*\([^\)]*\)|NOMEMALIGN))' ) # detect any instances of std::vector: - res="$res"$( \ - cat $f | \ -# remove C preprocessor macros: - grep -v '^#' | \ -# remove C++ single-line comments: - perl -pe 's|//.*$||' | \ -# remove all newlines to make file one long line: - tr '\n' ' ' | \ -# remove any duplicate spaces: - perl -pe 's|\s+| |g' | \ -# remove C-style comments: - perl -pe 's|/\*.*?\*/||g' | \ -# remove quoted strings: - perl -pe 's/(")(\\"|.)*?"//g' | \ + res="$res"$( + cat .check_memalign.tmp | \ # match for the parts we're interested in and output just the bits that match: grep -Po '(?> $LOG diff --git a/cmd/amp2sh.cpp b/cmd/amp2sh.cpp index 982e6e7885..b34a1915cb 100644 --- a/cmd/amp2sh.cpp +++ b/cmd/amp2sh.cpp @@ -13,8 +13,9 @@ #include "command.h" -#include "progressbar.h" #include "image.h" +#include "phase_encoding.h" +#include "progressbar.h" #include "math/SH.h" #include "dwi/gradient.h" #include "dwi/shells.h" @@ -232,6 +233,7 @@ void run () DWI::stash_DW_scheme (header, grad); } } + PhaseEncoding::clear_scheme (header); auto sh2amp = DWI::compute_SH2amp_mapping (dirs, true, 8); diff --git a/cmd/connectome2tck.cpp b/cmd/connectome2tck.cpp index 47e7a52cd8..6dc73d0ca4 100644 --- a/cmd/connectome2tck.cpp +++ b/cmd/connectome2tck.cpp @@ -14,10 +14,12 @@ #include #include -#include #include "command.h" +#include "image.h" #include "progressbar.h" +#include "thread_queue.h" +#include "types.h" #include "connectome/connectome.h" @@ -28,11 +30,6 @@ #include "dwi/tractography/connectome/streamline.h" #include "dwi/tractography/mapping/loader.h" -#include "image.h" - -#include "thread_queue.h" - - using namespace MR; diff --git a/cmd/connectomestats.cpp b/cmd/connectomestats.cpp index 84094bf62a..f3bc064525 100644 --- a/cmd/connectomestats.cpp +++ b/cmd/connectomestats.cpp @@ -12,10 +12,9 @@ */ -#include - #include "command.h" #include "progressbar.h" +#include "types.h" #include "file/path.h" #include "math/stats/glm.h" diff --git a/cmd/dcminfo.cpp b/cmd/dcminfo.cpp index c45e6a8248..0ac2ae7bc6 100644 --- a/cmd/dcminfo.cpp +++ b/cmd/dcminfo.cpp @@ -75,7 +75,7 @@ void run () while (item.read()) { for (size_t n = 0; n < opt.size(); ++n) if (item.is (tags[n].group, tags[n].element)) - tags[n].value = item.get_string()[0]; + tags[n].value = join (item.get_string(), " "); } for (size_t n = 0; n < opt.size(); ++n) diff --git a/cmd/dirflip.cpp b/cmd/dirflip.cpp index e636fb6019..a0508d57d0 100644 --- a/cmd/dirflip.cpp +++ b/cmd/dirflip.cpp @@ -82,7 +82,7 @@ class Shared { MEMALIGN(Shared) vector3_type b = { directions(j,0), directions(j,1), directions(j,2) }; if (signs[i] < 0) a = -a; if (signs[j] < 0) b = -b; - return 1.0 / (a-b).squaredNorm(); + return 1.0 / (a-b).norm(); } diff --git a/cmd/dirgen.cpp b/cmd/dirgen.cpp index a1911766ba..eaf5823d5a 100644 --- a/cmd/dirgen.cpp +++ b/cmd/dirgen.cpp @@ -15,12 +15,14 @@ #include "command.h" #include "progressbar.h" #include "math/rng.h" +#include "thread.h" #include "math/gradient_descent.h" #include "math/check_gradient.h" #include "dwi/directions/file.h" -#define DEFAULT_POWER 2 +#define DEFAULT_POWER 1 #define DEFAULT_NITER 10000 +#define DEFAULT_RESTARTS 10 using namespace MR; @@ -29,33 +31,44 @@ using namespace App; void usage () { -AUTHOR = "J-Donald Tournier (jdtournier@gmail.com)"; + AUTHOR = "J-Donald Tournier (jdtournier@gmail.com)"; -SYNOPSIS = "Generate a set of uniformly distributed directions using a bipolar electrostatic repulsion model"; + SYNOPSIS = "Generate a set of uniformly distributed directions using a bipolar electrostatic repulsion model"; -REFERENCES - + "Jones, D.; Horsfield, M. & Simmons, A. " - "Optimal strategies for measuring diffusion in anisotropic systems by magnetic resonance imaging. " - "Magnetic Resonance in Medicine, 1999, 42: 515-525" - - + "Papadakis, N. G.; Murrills, C. D.; Hall, L. D.; Huang, C. L.-H. & Adrian Carpenter, T. " - "Minimal gradient encoding for robust estimation of diffusion anisotropy. " - "Magnetic Resonance Imaging, 2000, 18: 671-679"; + DESCRIPTION + + "Directions are distributed by analogy to an electrostatic repulsion system, with each direction " + "corresponding to a single electrostatic charge (for -unipolar), or a pair of diametrically opposed charges " + "(for the default bipolar case). The energy of the system is determined based on the Coulomb repulsion, " + "which assumes the form 1/r^power, where r is the distance between any pair of charges, and p is the power " + "assumed for the repulsion law (default: 1). The minimum energy state is obtained by gradient descent."; -ARGUMENTS - + Argument ("ndir", "the number of directions to generate.").type_integer (6, std::numeric_limits::max()) - + Argument ("dirs", "the text file to write the directions to, as [ az el ] pairs.").type_file_out(); -OPTIONS - + Option ("power", "specify exponent to use for repulsion power law (default: " + str(DEFAULT_POWER) + "). This must be a power of 2 (i.e. 2, 4, 8, 16, ...).") - + Argument ("exp").type_integer (2, std::numeric_limits::max()) + REFERENCES + + "Jones, D.; Horsfield, M. & Simmons, A. " + "Optimal strategies for measuring diffusion in anisotropic systems by magnetic resonance imaging. " + "Magnetic Resonance in Medicine, 1999, 42: 515-525" - + Option ("niter", "specify the maximum number of iterations to perform (default: " + str(DEFAULT_NITER) + ").") - + Argument ("num").type_integer (1, std::numeric_limits::max()) + + "Papadakis, N. G.; Murrills, C. D.; Hall, L. D.; Huang, C. L.-H. & Adrian Carpenter, T. " + "Minimal gradient encoding for robust estimation of diffusion anisotropy. " + "Magnetic Resonance Imaging, 2000, 18: 671-679"; - + Option ("unipolar", "optimise assuming a unipolar electrostatic repulsion model rather than the bipolar model normally assumed in DWI") + ARGUMENTS + + Argument ("ndir", "the number of directions to generate.").type_integer (6, std::numeric_limits::max()) + + Argument ("dirs", "the text file to write the directions to, as [ az el ] pairs.").type_file_out(); - + Option ("cartesian", "Output the directions in Cartesian coordinates [x y z] instead of [az el]."); + OPTIONS + + Option ("power", "specify exponent to use for repulsion power law (default: " + str(DEFAULT_POWER) + "). This must be a power of 2 (i.e. 1, 2, 4, 8, 16, ...).") + + Argument ("exp").type_integer (1, std::numeric_limits::max()) + + + Option ("niter", "specify the maximum number of iterations to perform (default: " + str(DEFAULT_NITER) + ").") + + Argument ("num").type_integer (1, std::numeric_limits::max()) + + + Option ("restarts", "specify the number of restarts to perform (default: " + str(DEFAULT_RESTARTS) + ").") + + Argument ("num").type_integer (1, std::numeric_limits::max()) + + + Option ("unipolar", "optimise assuming a unipolar electrostatic repulsion model rather than the bipolar model normally assumed in DWI") + + + Option ("cartesian", "Output the directions in Cartesian coordinates [x y z] instead of [az el]."); } @@ -79,13 +92,18 @@ class ProjectedUpdate { MEMALIGN(ProjectedUpdate) + + + + class Energy { MEMALIGN(Energy) public: - Energy (size_t n_directions, int power, bool bipolar, const Eigen::VectorXd& init_directions) : - ndir (n_directions), - power (power), - bipolar (bipolar), - init_directions (init_directions) { } + Energy (ProgressBar& progress) : + progress (progress), + ndirs (to (argument[0])), + bipolar (!(get_options ("unipolar").size())), + power (0), + directions (3 * ndirs) { } FORCE_INLINE double fast_pow (double x, int p) { return p == 1 ? x : fast_pow (x*x, p/2); @@ -93,30 +111,42 @@ class Energy { MEMALIGN(Energy) using value_type = double; - size_t size () const { return 3 * ndir; } + size_t size () const { return 3 * ndirs; } // set x to original directions provided in constructor. // The idea is to save the directions from one run to initialise next run // at higher power. - double init (Eigen::VectorXd& x) { - x = init_directions; + double init (Eigen::VectorXd& x) + { + Math::RNG::Normal rng; + for (ssize_t n = 0; n < ndirs; ++n) { + auto d = x.segment (3*n,3); + d[0] = rng(); + d[1] = rng(); + d[2] = rng(); + d.normalize(); + } return 0.01; } + + + // function executed by optimiser at each iteration: double operator() (const Eigen::VectorXd& x, Eigen::VectorXd& g) { double E = 0.0; g.setZero(); - for (size_t i = 0; i < ndir-1; ++i) { + for (size_t i = 0; i < ndirs-1; ++i) { auto d1 = x.segment (3*i, 3); auto g1 = g.segment (3*i, 3); - for (size_t j = i+1; j < ndir; ++j) { + for (size_t j = i+1; j < ndirs; ++j) { auto d2 = x.segment (3*j, 3); auto g2 = g.segment (3*j, 3); Eigen::Vector3d r = d1-d2; double _1_r2 = 1.0 / r.squaredNorm(); - double e = fast_pow (_1_r2, power/2); + double _1_r = std::sqrt (_1_r2); + double e = fast_pow (_1_r, power); E += e; g1 -= (power * e * _1_r2) * r; g2 += (power * e * _1_r2) * r; @@ -124,7 +154,8 @@ class Energy { MEMALIGN(Energy) if (bipolar) { r = d1+d2; _1_r2 = 1.0 / r.squaredNorm(); - e = fast_pow (_1_r2, power/2); + _1_r = std::sqrt (_1_r2); + e = fast_pow (_1_r, power); E += e; g1 -= (power * e * _1_r2) * r; g2 -= (power * e * _1_r2) * r; @@ -134,77 +165,102 @@ class Energy { MEMALIGN(Energy) } // constrain gradients to lie tangent to unit sphere: - for (size_t n = 0; n < ndir; ++n) + for (size_t n = 0; n < ndirs; ++n) g.segment(3*n,3) -= x.segment(3*n,3).dot (g.segment(3*n,3)) * x.segment(3*n,3); return E; } - protected: - size_t ndir; - int power; - bool bipolar; - const Eigen::VectorXd& init_directions; -}; + // function executed per thread: + void execute () + { + size_t this_start = 0; + while ((this_start = current_start++) < restarts) { + INFO ("launching start " + str (this_start)); + double E = 0.0; + for (power = 1; power <= target_power; power *= 2) { + Math::GradientDescent optim (*this, ProjectedUpdate()); -void run () { - size_t niter = get_option_value ("niter", DEFAULT_NITER); - int target_power = get_option_value ("power", DEFAULT_POWER); - bool bipolar = !(get_options ("unipolar").size()); - int ndirs = to (argument[0]); + INFO ("start " + str(this_start) + ": setting power = " + str (power)); + optim.init(); - Eigen::VectorXd directions (3*ndirs); + size_t iter = 0; + for (; iter < niter; iter++) { + if (!optim.iterate()) + break; - { // random initialisation: - Math::RNG::Normal rng; - for (ssize_t n = 0; n < ndirs; ++n) { - auto d = directions.segment (3*n,3); - d[0] = rng(); - d[1] = rng(); - d[2] = rng(); - d.normalize(); + DEBUG ("start " + str(this_start) + ": [ " + str (iter) + " ] (pow = " + str (power) + ") E = " + str (optim.value(), 8) + + ", grad = " + str (optim.gradient_norm(), 8)); + + std::lock_guard lock (mutex); + ++progress; + } + + directions = optim.state(); + E = optim.value(); + } + + + + std::lock_guard lock (mutex); + if (E < best_E) { + best_E = E; + best_directions = directions; + } + } } - } - // optimisation proper: - { - ProgressBar progress ("Optimising directions"); - for (int power = 2; power <= target_power; power *= 2) { - Energy energy (ndirs, power, bipolar, directions); - Math::GradientDescent optim (energy, ProjectedUpdate()); + static size_t restarts; + static int target_power; + static size_t niter; + static double best_E; + static Eigen::VectorXd best_directions; - INFO ("setting power = " + str (power)); - optim.init(); + protected: + ProgressBar& progress; + size_t ndirs; + bool bipolar; + int power; + Eigen::VectorXd directions; + double E; + + static std::mutex mutex; + static std::atomic current_start; +}; - //Math::check_function_gradient (energy, optim.state(), 0.001); - //return; - size_t iter = 0; - for (; iter < niter; iter++) { - if (!optim.iterate()) - break; +size_t Energy::restarts (DEFAULT_RESTARTS); +int Energy::target_power (DEFAULT_POWER); +size_t Energy::niter (DEFAULT_NITER); +std::mutex Energy::mutex; +std::atomic Energy::current_start (0); +double Energy::best_E = std::numeric_limits::infinity(); +Eigen::VectorXd Energy::best_directions; - INFO ("[ " + str (iter) + " ] (pow = " + str (power) + ") E = " + str (optim.value(), 8) - + ", grad = " + str (optim.gradient_norm(), 8)); - progress.update ([&]() { return "Optimising directions (power " + str(power) - + ", energy: " + str(optim.value(), 8) + ", gradient: " + str(optim.gradient_norm(), 8) + ", iteration " + str(iter) + ")"; }); - } - directions = optim.state(); - progress.set_text ("Optimising directions (power " + str(power) - + ", energy: " + str(optim.value(), 8) + ", gradient: " + str(optim.gradient_norm(), 8) + ", iteration " + str(iter) + ")"); - } +void run () +{ + Energy::restarts = get_option_value ("restarts", DEFAULT_RESTARTS); + Energy::target_power = get_option_value ("power", DEFAULT_POWER); + Energy::niter = get_option_value ("niter", DEFAULT_NITER); + + { + ProgressBar progress ("Optimising directions up to power " + str(Energy::target_power) + " (" + str(Energy::restarts) + " restarts)"); + Energy energy_functor (progress); + auto threads = Thread::run (Thread::multi (energy_functor), "energy function"); } + CONSOLE ("final energy = " + str(Energy::best_E)); + size_t ndirs = Energy::best_directions.size()/3; Eigen::MatrixXd directions_matrix (ndirs, 3); for (int n = 0; n < ndirs; ++n) - directions_matrix.row (n) = directions.segment (3*n, 3); + directions_matrix.row (n) = Energy::best_directions.segment (3*n, 3); DWI::Directions::save (directions_matrix, argument[1], get_options ("cartesian").size()); } diff --git a/cmd/dirmerge.cpp b/cmd/dirmerge.cpp index 6e1c287381..e65379bc4b 100644 --- a/cmd/dirmerge.cpp +++ b/cmd/dirmerge.cpp @@ -38,12 +38,17 @@ ARGUMENTS + Argument ("out", "the output directions file, with each row listing " "the X Y Z gradient directions, the b-value, and an index representing " "the phase encode direction").type_file_out(); + +OPTIONS + + Option ("unipolar_weight", + "set the weight given to the unipolar electrostatic repulsion model compared to the " + "bipolar model (default: 0.2)."); } using value_type = double; -using Direction = std::array; +using Direction = Eigen::Matrix; using DirectionSet = vector; @@ -62,6 +67,8 @@ inline std::ostream& operator<< (std::ostream& stream, const OutDir& d) { void run () { size_t num_subsets = argument[0]; + value_type unipolar_weight = App::get_option_value ("unipolar_weight", 0.2); + value_type bipolar_weight = 1.0 - unipolar_weight; vector> dirs; @@ -80,7 +87,7 @@ void run () auto m = DWI::Directions::load_cartesian (argument[current++]); DirectionSet set; for (ssize_t r = 0; r < m.rows(); ++r) - set.push_back ({ { m(r,0), m(r,1), m(r,2) } }); + set.push_back (Direction (m(r,0), m(r,1), m(r,2))); d.push_back (set); } INFO ("found b = " + str(bvalue[nb]) + ", " + @@ -104,24 +111,18 @@ void run () auto push = [&](size_t b, size_t p, size_t n) { - merged.push_back ({ { { dirs[b][p][n][0], dirs[b][p][n][1], dirs[b][p][n][2] } }, b, p }); + merged.push_back ({ Direction (dirs[b][p][n][0], dirs[b][p][n][1], dirs[b][p][n][2]), b, p }); dirs[b][p].erase (dirs[b][p].begin()+n); }; - auto energy_pair = [](const Direction& a, const Direction& b) + auto energy_pair = [&](const Direction& a, const Direction& b) { // use combination of mono- and bi-polar electrostatic repulsion models // to ensure adequate coverage of eddy-current space as well as // orientation space. Use a moderate bias, favouring the bipolar model. - return 1.2 / ( - Math::pow2 (b[0] - a[0]) + - Math::pow2 (b[1] - a[1]) + - Math::pow2 (b[2] - a[2]) - ) + 1.0 / ( - Math::pow2 (b[0] + a[0]) + - Math::pow2 (b[1] + a[1]) + - Math::pow2 (b[2] + a[2]) - ); + + return (unipolar_weight+bipolar_weight) / (b-a).norm() + + bipolar_weight / (b+a).norm(); }; auto energy = [&](size_t b, size_t p, size_t n) diff --git a/cmd/dirorder.cpp b/cmd/dirorder.cpp index 03b520aee9..2112c46226 100644 --- a/cmd/dirorder.cpp +++ b/cmd/dirorder.cpp @@ -74,14 +74,8 @@ void run () ssize_t a = remaining[n]; for (size_t i = 0; i < indices.size(); ++i) { ssize_t b = indices[i]; - E += 1.0 / ( - Math::pow2 (directions(a,0)-directions(b,0)) + - Math::pow2 (directions(a,1)-directions(b,1)) + - Math::pow2 (directions(a,2)-directions(b,2))); - E += 1.0 / ( - Math::pow2 (directions(a,0)+directions(b,0)) + - Math::pow2 (directions(a,1)+directions(b,1)) + - Math::pow2 (directions(a,2)+directions(b,2))); + E += 1.0 / (directions.row(a) - directions.row(b)).norm(); + E += 1.0 / (directions.row(a) + directions.row(b)).norm(); } if (E < best_E) { best_E = E; diff --git a/cmd/dirsplit.cpp b/cmd/dirsplit.cpp index 1d2afef63b..64c86b90ee 100644 --- a/cmd/dirsplit.cpp +++ b/cmd/dirsplit.cpp @@ -85,7 +85,7 @@ class Shared { MEMALIGN(Shared) value_type energy (size_t i, size_t j) const { vector3_type a = { directions(i,0), directions(i,1), directions(i,2) }; vector3_type b = { directions(j,0), directions(j,1), directions(j,2) }; - return 1.0 / (a-b).squaredNorm() + 1.0 / (a+b).squaredNorm(); + return 1.0 / (a-b).norm() + 1.0 / (a+b).norm(); } diff --git a/cmd/dirstat.cpp b/cmd/dirstat.cpp index 3839f00b99..960de4b7b4 100644 --- a/cmd/dirstat.cpp +++ b/cmd/dirstat.cpp @@ -56,12 +56,12 @@ void report (const std::string& title, const Eigen::MatrixXd& directions) NN_bipolar[i] = std::max (NN_bipolar[i], cos_angle); NN_bipolar[j] = std::max (NN_bipolar[j], cos_angle); - double E = 1.0 / (directions.row(i).head(3) - directions.row(j).head(3)).squaredNorm(); + double E = 1.0 / (directions.row(i).head(3) - directions.row(j).head(3)).norm(); E_unipolar[i] += E; E_unipolar[j] += E; - E += 1.0 / (directions.row(i).head(3) + directions.row(j).head(3)).squaredNorm(); + E += 1.0 / (directions.row(i).head(3) + directions.row(j).head(3)).norm(); E_bipolar[i] += E; E_bipolar[j] += E; @@ -98,7 +98,7 @@ void report (const std::string& title, const Eigen::MatrixXd& directions) min = std::min (min, e); max = std::max (max, e); } - print (" energy: total = " + str(total, precision) + ", mean = " + str(total/E.size(), precision) + ", range [ " + str(min, precision) + " - " + str(max, precision) + " ]\n"); + print (" energy: total = " + str(0.5*total, precision) + ", mean = " + str(total/E.size(), precision) + ", range [ " + str(min, precision) + " - " + str(max, precision) + " ]\n"); }; diff --git a/cmd/dwi2adc.cpp b/cmd/dwi2adc.cpp index 345631d78f..3b76ed1c0c 100644 --- a/cmd/dwi2adc.cpp +++ b/cmd/dwi2adc.cpp @@ -13,8 +13,9 @@ #include "command.h" -#include "progressbar.h" #include "image.h" +#include "phase_encoding.h" +#include "progressbar.h" #include "algo/threaded_copy.h" #include "math/least_squares.h" #include "dwi/gradient.h" @@ -22,7 +23,6 @@ using namespace MR; using namespace App; -using namespace std; void usage () @@ -99,6 +99,7 @@ void run () { header.ndim() = 4; header.size(3) = 2; DWI::stash_DW_scheme (header, grad); + PhaseEncoding::clear_scheme (header); auto adc = Image::create (argument[1], header); diff --git a/cmd/dwi2fod.cpp b/cmd/dwi2fod.cpp index 20d9ce1bbc..e6e1b3b4b2 100644 --- a/cmd/dwi2fod.cpp +++ b/cmd/dwi2fod.cpp @@ -15,6 +15,7 @@ #include "command.h" #include "header.h" #include "image.h" +#include "phase_encoding.h" #include "algo/threaded_loop.h" #include "dwi/gradient.h" #include "dwi/shells.h" @@ -249,6 +250,7 @@ void run () header_out.size(3) = shared.nSH(); DWI::stash_DW_scheme (header_out, shared.grad); + PhaseEncoding::clear_scheme (header_out); auto fod = Image::create (argument[3], header_out); CSD_Processor processor (shared, mask); diff --git a/cmd/dwi2mask.cpp b/cmd/dwi2mask.cpp index e36efd0b92..9b1e146d29 100644 --- a/cmd/dwi2mask.cpp +++ b/cmd/dwi2mask.cpp @@ -75,7 +75,10 @@ void run () { auto temp_mask = Image::scratch (dwi_brain_mask_filter, "brain mask"); dwi_brain_mask_filter (input, temp_mask); - auto output = Image::create (argument[1], temp_mask); + Header H_out (temp_mask); + DWI::stash_DW_scheme (H_out, grad); + PhaseEncoding::clear_scheme (H_out); + auto output = Image::create (argument[1], H_out); unsigned int scale = get_option_value ("clean_scale", DEFAULT_CLEAN_SCALE); @@ -84,7 +87,7 @@ void run () { Filter::MaskClean clean_filter (temp_mask, std::string("applying mask cleaning filter")); clean_filter.set_scale (scale); clean_filter (temp_mask, output); - } catch (Exception& e) { + } catch (...) { WARN("Unable to run mask cleaning filter (image is not truly 3D); skipping"); for (auto l = Loop (0,3) (temp_mask, output); l; ++l) output.value() = temp_mask.value(); diff --git a/cmd/dwi2noise.cpp b/cmd/dwi2noise.cpp index c744bde862..d96eb64df8 100644 --- a/cmd/dwi2noise.cpp +++ b/cmd/dwi2noise.cpp @@ -14,6 +14,7 @@ #include "command.h" #include "image.h" +#include "phase_encoding.h" #include "adapter/extract.h" #include "dwi/gradient.h" #include "math/least_squares.h" @@ -83,6 +84,7 @@ void run () header.ndim() = 3; header.datatype() = DataType::Float32; DWI::stash_DW_scheme (header, grad); + PhaseEncoding::clear_scheme (header); auto noise = Image::create (argument[1], header); DWI::estimate_noise (dwi, noise, mapping); diff --git a/cmd/dwi2tensor.cpp b/cmd/dwi2tensor.cpp index a7c240d402..717b214e54 100644 --- a/cmd/dwi2tensor.cpp +++ b/cmd/dwi2tensor.cpp @@ -13,6 +13,7 @@ #include "command.h" +#include "phase_encoding.h" #include "progressbar.h" #include "image.h" #include "algo/threaded_copy.h" @@ -21,7 +22,6 @@ using namespace MR; using namespace App; -using namespace std; using value_type = float; @@ -180,6 +180,7 @@ void run () header.datatype() = DataType::Float32; header.ndim() = 4; DWI::stash_DW_scheme (header, grad); + PhaseEncoding::clear_scheme (header); Image* predict = nullptr; opt = get_options ("predicted_signal"); diff --git a/cmd/dwidenoise.cpp b/cmd/dwidenoise.cpp index db1694420b..c18fd50ee5 100644 --- a/cmd/dwidenoise.cpp +++ b/cmd/dwidenoise.cpp @@ -197,6 +197,9 @@ void run () { auto dwi_in = Image::open (argument[0]).with_direct_io(3); + if (dwi_in.ndim() != 4 || dwi_in.size(3) <= 1) + throw Exception ("input image must be 4-dimensional"); + Image mask; auto opt = get_options ("mask"); if (opt.size()) { diff --git a/cmd/fixelcfestats.cpp b/cmd/fixelcfestats.cpp index b6e60bfbe7..fbcd485a22 100644 --- a/cmd/fixelcfestats.cpp +++ b/cmd/fixelcfestats.cpp @@ -39,6 +39,7 @@ using namespace MR::DWI::Tractography::Mapping; using namespace MR::Math::Stats; using Stats::CFE::direction_type; using Stats::CFE::connectivity_value_type; +using Stats::CFE::index_type; #define DEFAULT_CFE_DH 0.1 #define DEFAULT_CFE_E 2.0 @@ -50,10 +51,17 @@ using Stats::CFE::connectivity_value_type; void usage () { - AUTHOR = "David Raffelt (david.raffelt@florey.edu.au)"; + AUTHOR = "David Raffelt (david.raffelt@florey.edu.au) and Robert E. Smith (robert.smith@florey.edu.au)"; SYNOPSIS = "Fixel-based analysis using connectivity-based fixel enhancement and non-parametric permutation testing"; + DESCRIPTION + + "Note that if the -mask option is used, the output fixel directory will still contain the same set of fixels as that " + "present in the input fixel template, in order to retain fixel correspondence. However a consequence of this is that " + "all fixels in the template will be initialy visible when the output fixel directory is loaded in mrview. Those fixels " + "outside the processing mask will immediately disappear from view as soon as any data-file-based fixel colouring or " + "thresholding is applied."; + REFERENCES + "Raffelt, D.; Smith, RE.; Ridgway, GR.; Tournier, JD.; Vaughan, DN.; Rose, S.; Henderson, R.; Connelly, A." // Internal "Connectivity-based fixel enhancement: Whole-brain statistical analysis of diffusion MRI measures in the presence of crossing fibres. \n" @@ -109,7 +117,10 @@ void usage () + Argument ("threshold").type_float (0.0, 1.0) + Option ("angle", "the max angle threshold for assigning streamline tangents to fixels (Default: " + str(DEFAULT_ANGLE_THRESHOLD, 2) + " degrees)") - + Argument ("value").type_float (0.0, 90.0); + + Argument ("value").type_float (0.0, 90.0) + + + Option ("mask", "provide a fixel data file containing a mask of those fixels to be used during processing") + + Argument ("file").type_image_in(); } @@ -118,12 +129,14 @@ void usage () template void write_fixel_output (const std::string& filename, const VectorType& data, + const vector& fixel2row, const Header& header) { + assert (size_t(header.size(0)) == fixel2row.size()); auto output = Image::create (filename, header); - for (uint32_t i = 0; i < data.size(); ++i) { + for (size_t i = 0; i != fixel2row.size(); ++i) { output.index(0) = i; - output.value() = data[i]; + output.value() = (fixel2row[i] >= 0) ? data[fixel2row[i]] : NaN; } } @@ -147,11 +160,45 @@ void run() { const std::string input_fixel_directory = argument[0]; Header index_header = Fixel::find_index_header (input_fixel_directory); - auto index_image = index_header.get_image(); + auto index_image = index_header.get_image(); - const uint32_t num_fixels = Fixel::get_number_of_fixels (index_header); + const index_type num_fixels = Fixel::get_number_of_fixels (index_header); CONSOLE ("number of fixels: " + str(num_fixels)); + Image mask; + index_type mask_fixels; + // Lookup table that maps from input fixel index to row number + // Note that if a fixel is masked out, it will have a value of -1 + // in this array; hence require a signed integer type + vector fixel2row (num_fixels); + opt = get_options ("mask"); + if (opt.size()) { + mask = Image::open (opt[0][0]); + Fixel::check_data_file (mask); + if (!Fixel::fixels_match (index_header, mask)) + throw Exception ("Mask image provided using -mask option does not match fixel template"); + mask_fixels = 0; + for (mask.index(0) = 0; mask.index(0) != num_fixels; ++mask.index(0)) + fixel2row[mask.index(0)] = mask.value() ? mask_fixels++ : -1; + CONSOLE ("Fixel mask contains " + str(mask_fixels) + " fixels"); + } else { + Header data_header; + data_header.ndim() = 3; + data_header.size(0) = num_fixels; + data_header.size(1) = 1; + data_header.size(2) = 1; + data_header.spacing(0) = data_header.spacing(1) = data_header.spacing(2) = NaN; + data_header.stride(0) = 1; data_header.stride(1) = 2; data_header.stride(2) = 3; + mask = Image::scratch (data_header, "scratch fixel mask"); + for (index_type f = 0; f != num_fixels; ++f) { + mask.index(0) = f; + mask.value() = true; + } + mask_fixels = num_fixels; + for (index_type f = 0; f != num_fixels; ++f) + fixel2row[f] = f; + } + vector positions (num_fixels); vector directions (num_fixels); @@ -163,13 +210,14 @@ void run() { // Load template fixel directions Transform image_transform (index_image); for (auto i = Loop ("loading template fixel directions and positions", index_image, 0, 3)(index_image); i; ++i) { - const Eigen::Vector3 vox ((default_type)index_image.index(0), (default_type)index_image.index(1), (default_type)index_image.index(2)); + Eigen::Vector3 vox ((default_type)index_image.index(0), (default_type)index_image.index(1), (default_type)index_image.index(2)); + vox = image_transform.voxel2scanner * vox; index_image.index(3) = 1; - uint32_t offset = index_image.value(); + const index_type offset = index_image.value(); size_t fixel_index = 0; for (auto f = Fixel::Loop (index_image) (directions_data); f; ++f, ++fixel_index) { directions[offset + fixel_index] = directions_data.row(1); - positions[offset + fixel_index] = image_transform.voxel2scanner * vox; + positions[offset + fixel_index] = vox; } } } @@ -177,7 +225,7 @@ void run() { vector identifiers; Header header; { - ProgressBar progress ("validating input files..."); + ProgressBar progress ("validating input files"); std::ifstream ifs (argument[1].c_str()); std::string temp; while (getline (ifs, temp)) { @@ -228,35 +276,36 @@ void run() { throw Exception ("only a single contrast vector (defined as a row) is currently supported"); // Compute fixel-fixel connectivity - vector > connectivity_matrix (num_fixels); - vector fixel_TDI (num_fixels, 0.0); + Stats::CFE::init_connectivity_matrix_type connectivity_matrix (num_fixels); + vector fixel_TDI (num_fixels, 0); const std::string track_filename = argument[4]; DWI::Tractography::Properties properties; DWI::Tractography::Reader track_file (track_filename, properties); // Read in tracts, and compute whole-brain fixel-fixel connectivity - const size_t num_tracks = properties["count"].empty() ? 0 : to (properties["count"]); + const size_t num_tracks = properties["count"].empty() ? 0 : to (properties["count"]); if (!num_tracks) throw Exception ("no tracks found in input file"); if (num_tracks < 1000000) WARN ("more than 1 million tracks should be used to ensure robust fixel-fixel connectivity"); { - using SetVoxelDir = DWI::Tractography::Mapping::SetVoxelDir; DWI::Tractography::Mapping::TrackLoader loader (track_file, num_tracks, "pre-computing fixel-fixel connectivity"); DWI::Tractography::Mapping::TrackMapperBase mapper (index_image); mapper.set_upsample_ratio (DWI::Tractography::Mapping::determine_upsample_ratio (index_header, properties, 0.333f)); mapper.set_use_precise_mapping (true); - Stats::CFE::TrackProcessor tract_processor (index_image, directions, fixel_TDI, connectivity_matrix, angular_threshold); + Stats::CFE::TrackProcessor tract_processor (index_image, directions, mask, fixel_TDI, connectivity_matrix, angular_threshold); Thread::run_queue ( loader, Thread::batch (DWI::Tractography::Streamline()), mapper, - Thread::batch (SetVoxelDir()), + Thread::batch (DWI::Tractography::Mapping::SetVoxelDir()), tract_processor); } track_file.close(); - // Normalise connectivity matrix and threshold, pre-compute fixel-fixel weights for smoothing. - vector > smoothing_weights (num_fixels); + // Normalise connectivity matrix, threshold, and put in a more efficient format + Stats::CFE::norm_connectivity_matrix_type norm_connectivity_matrix (mask_fixels); + // Also pre-compute fixel-fixel weights for smoothing. + Stats::CFE::norm_connectivity_matrix_type smoothing_weights (mask_fixels); bool do_smoothing = false; const float gaussian_const2 = 2.0 * smooth_std_dev * smooth_std_dev; @@ -268,44 +317,73 @@ void run() { { ProgressBar progress ("normalising and thresholding fixel-fixel connectivity matrix", num_fixels); - for (uint32_t fixel = 0; fixel < num_fixels; ++fixel) { - - auto it = connectivity_matrix[fixel].begin(); - while (it != connectivity_matrix[fixel].end()) { - const connectivity_value_type connectivity = it->second.value / connectivity_value_type (fixel_TDI[fixel]); - if (connectivity < connectivity_threshold) { - connectivity_matrix[fixel].erase (it++); - } else { - if (do_smoothing) { - const value_type distance = std::sqrt (Math::pow2 (positions[fixel][0] - positions[it->first][0]) + - Math::pow2 (positions[fixel][1] - positions[it->first][1]) + - Math::pow2 (positions[fixel][2] - positions[it->first][2])); - const connectivity_value_type smoothing_weight = connectivity * gaussian_const1 * std::exp (-std::pow (distance, 2) / gaussian_const2); - if (smoothing_weight > 0.01) - smoothing_weights[fixel].insert (std::pair (it->first, smoothing_weight)); + for (index_type fixel = 0; fixel < num_fixels; ++fixel) { + mask.index(0) = fixel; + const int32_t row = fixel2row[fixel]; + + if (mask.value()) { + + // Here, the connectivity matrix needs to be modified to reflect the + // fact that fixel indices in the template fixel image may not + // correspond to rows in the statistical analysis + connectivity_value_type sum_weights = 0.0; + + for (auto& it : connectivity_matrix[fixel]) { +#ifndef NDEBUG + // Even if this fixel is within the mask, it should still not + // connect to any fixel that is outside the mask + mask.index(0) = it.first; + assert (mask.value()); +#endif + const connectivity_value_type connectivity = it.second.value / connectivity_value_type (fixel_TDI[fixel]); + if (connectivity >= connectivity_threshold) { + if (do_smoothing) { + const value_type distance = std::sqrt (Math::pow2 (positions[fixel][0] - positions[it.first][0]) + + Math::pow2 (positions[fixel][1] - positions[it.first][1]) + + Math::pow2 (positions[fixel][2] - positions[it.first][2])); + const connectivity_value_type smoothing_weight = connectivity * gaussian_const1 * std::exp (-Math::pow2 (distance) / gaussian_const2); + if (smoothing_weight >= connectivity_threshold) { + smoothing_weights[row].push_back (Stats::CFE::NormMatrixElement (fixel2row[it.first], smoothing_weight)); + sum_weights += smoothing_weight; + } + } + // Here we pre-exponentiate each connectivity value by C + norm_connectivity_matrix[row].push_back (Stats::CFE::NormMatrixElement (fixel2row[it.first], std::pow (connectivity, cfe_c))); } - // Here we pre-exponentiate each connectivity value by C - it->second.value = std::pow (connectivity, cfe_c); - ++it; } + + // Make sure the fixel is fully connected to itself + norm_connectivity_matrix[row].push_back (Stats::CFE::NormMatrixElement (uint32_t(row), connectivity_value_type(1.0))); + smoothing_weights[row].push_back (Stats::CFE::NormMatrixElement (uint32_t(row), connectivity_value_type(gaussian_const1))); + sum_weights += connectivity_value_type(gaussian_const1); + + // Normalise smoothing weights + const connectivity_value_type norm_factor = connectivity_value_type(1.0) / sum_weights; + for (auto i : smoothing_weights[row]) + i.normalise (norm_factor); + + // Force deallocation of memory used for this fixel in the original matrix + std::map().swap (connectivity_matrix[fixel]); + + } else { + + // If fixel is not in the mask, tract_processor should never assign + // any connections to it + assert (connectivity_matrix[fixel].empty()); + } - // Make sure the fixel is fully connected to itself - connectivity_matrix[fixel].insert (std::pair (fixel, Stats::CFE::connectivity (1.0))); - smoothing_weights[fixel].insert (std::pair (fixel, gaussian_const1)); - - // Normalise smoothing weights - value_type sum = 0.0; - for (auto smooth_it = smoothing_weights[fixel].begin(); smooth_it != smoothing_weights[fixel].end(); ++smooth_it) { - sum += smooth_it->second; - } - value_type norm_factor = 1.0 / sum; - for (auto smooth_it = smoothing_weights[fixel].begin(); smooth_it != smoothing_weights[fixel].end(); ++smooth_it) { - smooth_it->second *= norm_factor; - } + progress++; } } + // The connectivity matrix is now in vector rather than matrix form; + // throw out the structure holding the original data + // (Note however that all entries in the original structure should + // have been deleted during the prior loop) + Stats::CFE::init_connectivity_matrix_type().swap (connectivity_matrix); + + Header output_header (header); output_header.keyval()["num permutations"] = str(num_perms); output_header.keyval()["dh"] = str(cfe_dh); @@ -318,15 +396,15 @@ void run() { // Load input data - matrix_type data (num_fixels, identifiers.size()); + matrix_type data (mask_fixels, identifiers.size()); data.setZero(); { - ProgressBar progress ("loading input images", identifiers.size()); + ProgressBar progress (std::string ("loading input images") + (do_smoothing ? " and smoothing" : ""), identifiers.size()); for (size_t subject = 0; subject < identifiers.size(); subject++) { LogLevelLatch log_level (0); auto subject_data = Image::open (identifiers[subject]).with_direct_io(); - vector subject_data_vector (num_fixels, 0.0); + vector subject_data_vector (mask_fixels, 0.0); for (auto i = Loop (index_image, 0, 3)(index_image); i; ++i) { index_image.index(3) = 1; uint32_t offset = index_image.value(); @@ -334,23 +412,30 @@ void run() { for (auto f = Fixel::Loop (index_image) (subject_data); f; ++f, ++fixel_index) { if (!std::isfinite(static_cast(subject_data.value()))) throw Exception ("subject data file " + identifiers[subject] + " contains non-finite value: " + str(subject_data.value())); - subject_data_vector[offset + fixel_index] = subject_data.value(); + // Note that immediately on import, data are re-arranged according to fixel mask + const int32_t row = fixel2row[offset+fixel_index]; + if (row >= 0) + subject_data_vector[row] = subject_data.value(); } } // Smooth the data - for (size_t fixel = 0; fixel < num_fixels; ++fixel) { - value_type value = 0.0; - std::map::const_iterator it = smoothing_weights[fixel].begin(); - for (; it != smoothing_weights[fixel].end(); ++it) { - value += subject_data_vector[it->first] * it->second; + if (do_smoothing) { + for (size_t fixel = 0; fixel < mask_fixels; ++fixel) { + value_type value = 0.0; + for (auto i : smoothing_weights[fixel]) + value += subject_data_vector[i.index()] * i.value(); + data (fixel, subject) = value; } - data (fixel, subject) = value; + } else { + data.col (subject) = Eigen::Map > (subject_data_vector.data(), mask_fixels); } progress++; } } + // Free the memory occupied by the data smoothing filter; no longer required + Stats::CFE::norm_connectivity_matrix_type().swap (smoothing_weights); if (!data.allFinite()) throw Exception ("input data contains non-finite value(s)"); @@ -359,20 +444,20 @@ void run() { auto temp = Math::Stats::GLM::solve_betas (data, design); for (ssize_t i = 0; i < contrast.cols(); ++i) { - write_fixel_output (Path::join (output_fixel_directory, "beta" + str(i) + ".mif"), temp.row(i), output_header); + write_fixel_output (Path::join (output_fixel_directory, "beta" + str(i) + ".mif"), temp.row(i), fixel2row, output_header); ++progress; } temp = Math::Stats::GLM::abs_effect_size (data, design, contrast); ++progress; - write_fixel_output (Path::join (output_fixel_directory, "abs_effect.mif"), temp.row(0), output_header); ++progress; + write_fixel_output (Path::join (output_fixel_directory, "abs_effect.mif"), temp.row(0), fixel2row, output_header); ++progress; temp = Math::Stats::GLM::std_effect_size (data, design, contrast); ++progress; - write_fixel_output (Path::join (output_fixel_directory, "std_effect.mif"), temp.row(0), output_header); ++progress; + write_fixel_output (Path::join (output_fixel_directory, "std_effect.mif"), temp.row(0), fixel2row, output_header); ++progress; temp = Math::Stats::GLM::stdev (data, design); ++progress; - write_fixel_output (Path::join (output_fixel_directory, "std_dev.mif"), temp.row(0), output_header); + write_fixel_output (Path::join (output_fixel_directory, "std_dev.mif"), temp.row(0), fixel2row, output_header); } Math::Stats::GLMTTest glm_ttest (data, design, contrast); std::shared_ptr cfe_integrator; - cfe_integrator.reset (new Stats::CFE::Enhancer (connectivity_matrix, cfe_dh, cfe_e, cfe_h)); + cfe_integrator.reset (new Stats::CFE::Enhancer (norm_connectivity_matrix, cfe_dh, cfe_e, cfe_h)); vector_type empirical_cfe_statistic; // If performing non-stationarity adjustment we need to pre-compute the empirical CFE statistic @@ -386,37 +471,40 @@ void run() { Stats::PermTest::precompute_empirical_stat (glm_ttest, cfe_integrator, permutations, empirical_cfe_statistic); } output_header.keyval()["nonstationary adjustment"] = str(true); - write_fixel_output (Path::join (output_fixel_directory, "cfe_empirical.mif"), empirical_cfe_statistic, output_header); + write_fixel_output (Path::join (output_fixel_directory, "cfe_empirical.mif"), empirical_cfe_statistic, fixel2row, output_header); } else { output_header.keyval()["nonstationary adjustment"] = str(false); } // Precompute default statistic and CFE statistic - vector_type cfe_output (num_fixels); + vector_type cfe_output (mask_fixels); std::shared_ptr cfe_output_neg; - vector_type tvalue_output (num_fixels); + vector_type tvalue_output (mask_fixels); if (compute_negative_contrast) - cfe_output_neg.reset (new vector_type (num_fixels)); + cfe_output_neg.reset (new vector_type (mask_fixels)); Stats::PermTest::precompute_default_permutation (glm_ttest, cfe_integrator, empirical_cfe_statistic, cfe_output, cfe_output_neg, tvalue_output); - write_fixel_output (Path::join (output_fixel_directory, "cfe.mif"), cfe_output, output_header); - write_fixel_output (Path::join (output_fixel_directory, "tvalue.mif"), tvalue_output, output_header); + write_fixel_output (Path::join (output_fixel_directory, "cfe.mif"), cfe_output, fixel2row, output_header); + write_fixel_output (Path::join (output_fixel_directory, "tvalue.mif"), tvalue_output, fixel2row, output_header); if (compute_negative_contrast) - write_fixel_output (Path::join (output_fixel_directory, "cfe_neg.mif"), *cfe_output_neg, output_header); + write_fixel_output (Path::join (output_fixel_directory, "cfe_neg.mif"), *cfe_output_neg, fixel2row, output_header); // Perform permutation testing if (!get_options ("notest").size()) { vector_type perm_distribution (num_perms); std::shared_ptr perm_distribution_neg; - vector_type uncorrected_pvalues (num_fixels); + vector_type uncorrected_pvalues (mask_fixels); std::shared_ptr uncorrected_pvalues_neg; if (compute_negative_contrast) { perm_distribution_neg.reset (new vector_type (num_perms)); - uncorrected_pvalues_neg.reset (new vector_type (num_fixels)); + uncorrected_pvalues_neg.reset (new vector_type (mask_fixels)); } + // FIXME fixelcfestats is hanging here for some reason... + // Even when no mask is supplied + if (permutations.size()) { Stats::PermTest::run_permutations (permutations, glm_ttest, cfe_integrator, empirical_cfe_statistic, cfe_output, cfe_output_neg, @@ -432,17 +520,17 @@ void run() { ProgressBar progress ("outputting final results"); save_matrix (perm_distribution, Path::join (output_fixel_directory, "perm_dist.txt")); ++progress; - vector_type pvalue_output (num_fixels); + vector_type pvalue_output (mask_fixels); Math::Stats::Permutation::statistic2pvalue (perm_distribution, cfe_output, pvalue_output); ++progress; - write_fixel_output (Path::join (output_fixel_directory, "fwe_pvalue.mif"), pvalue_output, output_header); ++progress; - write_fixel_output (Path::join (output_fixel_directory, "uncorrected_pvalue.mif"), uncorrected_pvalues, output_header); ++progress; + write_fixel_output (Path::join (output_fixel_directory, "fwe_pvalue.mif"), pvalue_output, fixel2row, output_header); ++progress; + write_fixel_output (Path::join (output_fixel_directory, "uncorrected_pvalue.mif"), uncorrected_pvalues, fixel2row, output_header); ++progress; if (compute_negative_contrast) { save_matrix (*perm_distribution_neg, Path::join (output_fixel_directory, "perm_dist_neg.txt")); ++progress; - vector_type pvalue_output_neg (num_fixels); + vector_type pvalue_output_neg (mask_fixels); Math::Stats::Permutation::statistic2pvalue (*perm_distribution_neg, *cfe_output_neg, pvalue_output_neg); ++progress; - write_fixel_output (Path::join (output_fixel_directory, "fwe_pvalue_neg.mif"), pvalue_output_neg, output_header); ++progress; - write_fixel_output (Path::join (output_fixel_directory, "uncorrected_pvalue_neg.mif"), *uncorrected_pvalues_neg, output_header); + write_fixel_output (Path::join (output_fixel_directory, "fwe_pvalue_neg.mif"), pvalue_output_neg, fixel2row, output_header); ++progress; + write_fixel_output (Path::join (output_fixel_directory, "uncorrected_pvalue_neg.mif"), *uncorrected_pvalues_neg, fixel2row, output_header); } } } diff --git a/cmd/label2colour.cpp b/cmd/label2colour.cpp index 15d97a7124..a18cd9efb5 100644 --- a/cmd/label2colour.cpp +++ b/cmd/label2colour.cpp @@ -13,7 +13,6 @@ #include -#include #include "command.h" #include "image.h" diff --git a/cmd/label2mesh.cpp b/cmd/label2mesh.cpp index bc82ae6e0e..f7dccc0ed1 100644 --- a/cmd/label2mesh.cpp +++ b/cmd/label2mesh.cpp @@ -13,14 +13,13 @@ #include -#include #include "command.h" +#include "image.h" #include "progressbar.h" #include "thread_queue.h" +#include "types.h" -#include "image.h" -#include "image_helpers.h" #include "algo/loop.h" #include "adapter/subset.h" diff --git a/cmd/mrcalc.cpp b/cmd/mrcalc.cpp index b4caaf91a3..d2849fa2b1 100644 --- a/cmd/mrcalc.cpp +++ b/cmd/mrcalc.cpp @@ -15,8 +15,10 @@ #include "command.h" #include "image.h" #include "memory.h" +#include "phase_encoding.h" #include "math/rng.h" #include "algo/threaded_copy.h" +#include "dwi/gradient.h" using namespace MR; @@ -591,6 +593,23 @@ void get_header (const StackEntry& entry, Header& header) header.spacing(n) = entry.image->spacing(n); } + const auto header_grad = DWI::parse_DW_scheme (header); + if (header_grad.rows()) { + const auto entry_grad = DWI::parse_DW_scheme (*entry.image); + if (entry_grad.rows()) { + if (!entry_grad.isApprox (header_grad)) + DWI::clear_DW_scheme (header); + } + } + + const auto header_pe = PhaseEncoding::get_scheme (header); + if (header_pe.rows()) { + const auto entry_pe = PhaseEncoding::get_scheme (*entry.image); + if (entry_pe.rows()) { + if (!entry_pe.isApprox (header_pe)) + PhaseEncoding::clear_scheme (header); + } + } } diff --git a/cmd/mrdegibbs.cpp b/cmd/mrdegibbs.cpp new file mode 100644 index 0000000000..17a9d9c880 --- /dev/null +++ b/cmd/mrdegibbs.cpp @@ -0,0 +1,336 @@ +/* Copyright (c) 2008-2017 the MRtrix3 contributors. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + * + * MRtrix is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * For more details, see http://www.mrtrix.org/. + */ + + +#include + +#include "command.h" +#include "progressbar.h" +#include "image.h" +#include "algo/threaded_loop.h" +#include + +using namespace MR; +using namespace App; + +void usage () +{ + AUTHOR = "Ben Jeurissen (ben.jeurissen@uantwerpen.be) & J-Donald Tournier (jdtournier@gmail.com)"; + + SYNOPSIS = "Remove Gibbs Ringing Artifacts"; + + DESCRIPTION + + "This application attempts to remove Gibbs ringing artefacts from MRI images using the method " + "of local subvoxel-shifts proposed by Kellner et al. (see reference below for details)." + + + "This command is designed to run on data directly after it has been reconstructed by the scanner, " + "before any interpolation of any kind has taken place. You should not run this command after any " + "form of motion correction (e.g. not after dwipreproc). Similarly, if you intend running dwidenoise, " + "you should run this command afterwards, since it has the potential to alter the noise structure, " + "which would impact on dwidenoise's performance." + + + "Note that this method is designed to work on images acquired with full k-space coverage. " + "Running this method on partial Fourier ('half-scan') data may lead to suboptimal and/or biased " + "results, as noted in the original reference below. There is currently no means of dealing with this; " + "users should exercise caution when using this method on partial Fourier data, and inspect its output " + "for any obvious artefacts. "; + + + ARGUMENTS + + Argument ("in", "the input image.").type_image_in () + + Argument ("out", "the output image.").type_image_out (); + + + OPTIONS + + Option ("axes", + "select the slice axes (default: 0,1 - i.e. x-y).") + + Argument ("list").type_sequence_int () + + + Option ("nshifts", "discretization of subpixel spacing (default: 20).") + + Argument ("value").type_integer (8, 128) + + + Option ("minW", "left border of window used for TV computation (default: 1).") + + Argument ("value").type_integer (0, 10) + + + Option ("maxW", "right border of window used for TV computation (default: 3).") + + Argument ("value").type_integer (0, 128) + + + DataType::options(); + + + REFERENCES + + "Kellner, E; Dhital, B; Kiselev, V.G & Reisert, M. " + "Gibbs-ringing artifact removal based on local subvoxel-shifts. " + "Magnetic Resonance in Medicine, 2016, 76, 1574–1581."; + +} + + +typedef double value_type; + + + + +class ComputeSlice +{ MEMALIGN (ComputeSlice) + public: + ComputeSlice (const vector& outer_axes, const vector& slice_axes, const int& nsh, const int& minW, const int& maxW, Image& in, Image& out) : + outer_axes (outer_axes), + slice_axes (slice_axes), + nsh (nsh), + minW (minW), + maxW (maxW), + in (in), + out (out), + im1 (in.size(slice_axes[0]), in.size(slice_axes[1])), + im2 (im1.rows(), im1.cols()) { + prealloc_FFT(); + } + + ComputeSlice (const ComputeSlice& other) : + outer_axes (other.outer_axes), + slice_axes (other.slice_axes), + nsh (other.nsh), + minW (other.minW), + maxW (other.maxW), + in (other.in), + out (other.out), + fft (), + im1 (in.size(slice_axes[0]), in.size(slice_axes[1])), + im2 (im1.rows(), im1.cols()) { + prealloc_FFT(); + } + + + void operator() (const Iterator& pos) + { + const int X = slice_axes[0]; + const int Y = slice_axes[1]; + assign_pos_of (pos, outer_axes).to (in, out); + + for (auto l = Loop (slice_axes) (in); l; ++l) + im1 (in.index(X), in.index(Y)) = cdouble (in.value(), 0.0); + + unring_2d (); + + for (auto l = Loop (slice_axes) (out); l; ++l) + out.value() = im1 (out.index(X), out.index(Y)).real(); + } + + private: + const vector& outer_axes; + const vector& slice_axes; + const int nsh, minW, maxW; + Image in, out; + Eigen::FFT fft; + Eigen::MatrixXcd im1, im2, shifted; + Eigen::VectorXcd v; + + void prealloc_FFT () { + // needed to avoid within-thread allocations, + // which aren't thread-safe in FFTW: +#ifdef EIGEN_FFTW_DEFAULT + Eigen::VectorXcd tmp (im1.rows()); + FFT (tmp); + iFFT (tmp); + tmp.resize (im1.cols()); + FFT (tmp); + iFFT (tmp); +#endif + } + + template FORCE_INLINE void FFT (Eigen::MatrixBase&& vec) { fft.fwd (v, vec); vec = v; } + template FORCE_INLINE void FFT (Eigen::MatrixBase& vec) { FFT (std::move (vec)); } + template FORCE_INLINE void iFFT (Eigen::MatrixBase&& vec) { fft.inv (v, vec); vec = v; } + template FORCE_INLINE void iFFT (Eigen::MatrixBase& vec) { iFFT (std::move (vec)); } + template FORCE_INLINE void row_FFT (Eigen::MatrixBase& mat) { for (auto n = 0; n < mat.rows(); ++n) FFT (mat.row(n)); } + template FORCE_INLINE void row_iFFT (Eigen::MatrixBase& mat) { for (auto n = 0; n < mat.rows(); ++n) iFFT (mat.row(n)); } + template FORCE_INLINE void col_FFT (Eigen::MatrixBase& mat) { for (auto n = 0; n < mat.cols(); ++n) FFT (mat.col(n)); } + template FORCE_INLINE void col_iFFT (Eigen::MatrixBase& mat) { for (auto n = 0; n < mat.cols(); ++n) iFFT (mat.col(n)); } + + + + FORCE_INLINE void unring_2d () + { + row_FFT (im1); + col_FFT (im1); + + for (int k = 0; k < im1.cols(); k++) { + double ck = (1.0+cos(2.0*Math::pi*(double(k)/im1.cols())))*0.5; + for (int j = 0 ; j < im1.rows(); j++) { + double cj = (1.0+cos(2.0*Math::pi*(double(j)/im1.rows())))*0.5; + + if (ck+cj != 0.0) { + im2(j,k) = im1(j,k) * cj / (ck+cj); + im1(j,k) *= ck / (ck+cj); + } + else + im1(j,k) = im2(j,k) = cdouble(0.0, 0.0); + } + } + + row_iFFT (im1); + col_iFFT (im2); + + unring_1d (im1); + unring_1d (im2.transpose()); + + im1 += im2; + } + + + + + + template + FORCE_INLINE void unring_1d (Eigen::MatrixBase&& eig) + { + const int n = eig.rows(); + const int numlines = eig.cols(); + shifted.resize (n, 2*nsh+1); + + int shifts [2*nsh+1]; + shifts[0] = 0; + for (int j = 0; j < nsh; j++) { + shifts[j+1] = j+1; + shifts[1+nsh+j] = -(j+1); + } + + double TV1arr[2*nsh+1]; + double TV2arr[2*nsh+1]; + + for (int k = 0; k < numlines; k++) { + shifted.col(0) = eig.col(k); + + const int maxn = (n&1) ? (n-1)/2 : n/2-1; + + for (int j = 1; j < 2*nsh+1; j++) { + double phi = Math::pi*double(shifts[j])/double(n*nsh); + cdouble u (std::cos(phi), std::sin(phi)); + cdouble e (1.0, 0.0); + shifted(0,j) = shifted(0,0); + + if (!(n&1)) + shifted(n/2,j) = cdouble(0.0, 0.0); + + for (int l = 0; l < maxn; l++) { + e = u*e; + int L = l+1; shifted(L,j) = e * shifted(L,0); + L = n-1-l; shifted(L,j) = std::conj(e) * shifted(L,0); + } + } + + + col_iFFT (shifted); + + for (int j = 0; j < 2*nsh+1; ++j) { + TV1arr[j] = 0.0; + TV2arr[j] = 0.0; + for (int t = minW; t <= maxW; t++) { + TV1arr[j] += std::abs (shifted((n-t)%n,j).real() - shifted((n-t-1)%n,j).real()); + TV1arr[j] += std::abs (shifted((n-t)%n,j).imag() - shifted((n-t-1)%n,j).imag()); + TV2arr[j] += std::abs (shifted((n+t)%n,j).real() - shifted((n+t+1)%n,j).real()); + TV2arr[j] += std::abs (shifted((n+t)%n,j).imag() - shifted((n+t+1)%n,j).imag()); + } + } + + for (int l = 0; l < n; ++l) { + double minTV = std::numeric_limits::max(); + int minidx = 0; + for (int j = 0; j < 2*nsh+1; ++j) { + + if (TV1arr[j] < minTV) { + minTV = TV1arr[j]; + minidx = j; + } + if (TV2arr[j] < minTV) { + minTV = TV2arr[j]; + minidx = j; + } + + TV1arr[j] += std::abs (shifted((l-minW+1+n)%n,j).real() - shifted((l-(minW )+n)%n,j).real()); + TV1arr[j] -= std::abs (shifted((l-maxW +n)%n,j).real() - shifted((l-(maxW+1)+n)%n,j).real()); + TV2arr[j] += std::abs (shifted((l+maxW+1+n)%n,j).real() - shifted((l+(maxW+2)+n)%n,j).real()); + TV2arr[j] -= std::abs (shifted((l+minW +n)%n,j).real() - shifted((l+(minW+1)+n)%n,j).real()); + + TV1arr[j] += std::abs (shifted((l-minW+1+n)%n,j).imag() - shifted((l-(minW )+n)%n,j).imag()); + TV1arr[j] -= std::abs (shifted((l-maxW +n)%n,j).imag() - shifted((l-(maxW+1)+n)%n,j).imag()); + TV2arr[j] += std::abs (shifted((l+maxW+1+n)%n,j).imag() - shifted((l+(maxW+2)+n)%n,j).imag()); + TV2arr[j] -= std::abs (shifted((l+minW +n)%n,j).imag() - shifted((l+(minW+1)+n)%n,j).imag()); + } + + double a0r = shifted((l-1+n)%n,minidx).real(); + double a1r = shifted(l,minidx).real(); + double a2r = shifted((l+1+n)%n,minidx).real(); + double a0i = shifted((l-1+n)%n,minidx).imag(); + double a1i = shifted(l,minidx).imag(); + double a2i = shifted((l+1+n)%n,minidx).imag(); + double s = double(shifts[minidx])/(2.0*nsh); + + if (s > 0.0) + eig(l,k) = cdouble (a1r*(1.0-s) + a0r*s, a1i*(1.0-s) + a0i*s); + else + eig(l,k) = cdouble (a1r*(1.0+s) - a2r*s, a1i*(1.0+s) - a2i*s); + } + } + } + + template + FORCE_INLINE void unring_1d (Eigen::MatrixBase& eig) { unring_1d (std::move (eig)); } + +}; + + + + + + + +void run () +{ + const int nshifts = App::get_option_value ("nshifts", 20); + const int minW = App::get_option_value ("minW", 1); + const int maxW = App::get_option_value ("maxW", 3); + + if (minW >= maxW) + throw Exception ("minW must be smaller than maxW"); + + auto in = Image::open (argument[0]); + Header header (in); + + header.datatype() = DataType::from_command_line (DataType::Float32); + auto out = Image::create (argument[1], header); + + vector slice_axes = { 0, 1 }; + auto opt = get_options ("axes"); + if (opt.size()) { + vector axes = opt[0][0]; + if (slice_axes.size() != 2) + throw Exception ("slice axes must be specified as a comma-separated 2-vector"); + slice_axes = { size_t(axes[0]), size_t(axes[1]) }; + } + + // build vector of outer axes: + vector outer_axes (header.ndim()); + std::iota (outer_axes.begin(), outer_axes.end(), 0); + for (const auto axis : slice_axes) { + auto it = std::find (outer_axes.begin(), outer_axes.end(), axis); + if (it == outer_axes.end()) + throw Exception ("slice axis out of range!"); + outer_axes.erase (it); + } + + ThreadedLoop ("performing Gibbs ringing removal", in, outer_axes, slice_axes) + .run_outer (ComputeSlice (outer_axes, slice_axes, nshifts, minW, maxW, in, out)); +} + diff --git a/cmd/mredit.cpp b/cmd/mredit.cpp index 2e8c12aaed..471535e19e 100644 --- a/cmd/mredit.cpp +++ b/cmd/mredit.cpp @@ -13,12 +13,13 @@ #include -#include #include "command.h" #include "image.h" #include "image_helpers.h" #include "transform.h" +#include "types.h" + #include "algo/copy.h" using namespace MR; diff --git a/cmd/mrhistmatch.cpp b/cmd/mrhistmatch.cpp index c5104d08a1..3b49588ec6 100644 --- a/cmd/mrhistmatch.cpp +++ b/cmd/mrhistmatch.cpp @@ -14,12 +14,12 @@ #include #include -#include #include "command.h" #include "datatype.h" #include "header.h" #include "image.h" + #include "algo/histogram.h" #include "algo/loop.h" diff --git a/cmd/mrinfo.cpp b/cmd/mrinfo.cpp index 23ef14d6eb..40ac83da44 100644 --- a/cmd/mrinfo.cpp +++ b/cmd/mrinfo.cpp @@ -14,11 +14,11 @@ #include #include -#include #include "command.h" #include "header.h" #include "phase_encoding.h" +#include "types.h" #include "file/json.h" #include "dwi/gradient.h" diff --git a/cmd/mrmath.cpp b/cmd/mrmath.cpp index 2915b8f0f5..5a32ea6e1c 100644 --- a/cmd/mrmath.cpp +++ b/cmd/mrmath.cpp @@ -12,16 +12,19 @@ */ +#include + #include "command.h" -#include "progressbar.h" -#include "memory.h" #include "image.h" +#include "memory.h" +#include "phase_encoding.h" +#include "progressbar.h" #include "algo/threaded_loop.h" #include "math/math.h" #include "math/median.h" +#include "dwi/gradient.h" #include -#include using namespace MR; @@ -346,9 +349,17 @@ void run () if (axis >= image_in.ndim()) throw Exception ("Cannot perform operation along axis " + str (axis) + "; image only has " + str(image_in.ndim()) + " axes"); - Header header_out (image_in); + if (axis == 3) { + try { + const auto DW_scheme = DWI::parse_DW_scheme (header_out); + DWI::stash_DW_scheme (header_out, DW_scheme); + } catch (...) { } + DWI::clear_DW_scheme (header_out); + PhaseEncoding::clear_scheme (header_out); + } + header_out.datatype() = DataType::from_command_line (DataType::Float32); header_out.size(axis) = 1; squeeze_dim (header_out); @@ -408,6 +419,11 @@ void run () } } + // Wipe any header information that can't be guaranteed to still be accurate + // after applying an operator across multiple images + header.keyval().erase ("dw_scheme"); + PhaseEncoding::clear_scheme (header); + // Instantiate a kernel depending on the operation requested std::unique_ptr kernel; switch (op) { diff --git a/cmd/mrmodelfield.cpp b/cmd/mrmodelfield.cpp deleted file mode 100644 index 4700dda05d..0000000000 --- a/cmd/mrmodelfield.cpp +++ /dev/null @@ -1,131 +0,0 @@ -/* Copyright (c) 2008-2017 the MRtrix3 contributors. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at http://mozilla.org/MPL/2.0/. - * - * MRtrix is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty - * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * - * For more details, see http://www.mrtrix.org/. - */ - - -#include "command.h" -#include "image.h" -#include "algo/loop.h" -#include "adapter/extract.h" -#include "filter/optimal_threshold.h" -#include "transform.h" - -using namespace MR; -using namespace App; - - - -void usage () -{ - AUTHOR = "David Raffelt (david.raffelt@florey.edu.au) & Rami Tabbara (rami.tabbara@florey.edu.au)"; - - SYNOPSIS = "Model an input image using low frequency 3D polynomial basis functions"; - - DESCRIPTION - + "This command was designed to estimate a DWI bias field using the sum of normalised multi-tissue CSD compartments."; - - ARGUMENTS - + Argument ("input", "the input image").type_image_in() - + Argument ("output", "the output image representing the fit").type_image_out(); - - - OPTIONS - + Option ("mask", "use only voxels within the supplied mask for the model fit. If not supplied this command will compute a mask") - + Argument ("image").type_image_in (); -} - - -Eigen::VectorXf basis_function (Eigen::Vector3 pos) { - float x = (float)pos[0]; - float y = (float)pos[1]; - float z = (float)pos[2]; - Eigen::VectorXf basis(19); - basis(0) = 1.0; - basis(1) = x; - basis(2) = y; - basis(3) = z; - basis(4) = x * y; - basis(5) = x * z; - basis(6) = y * z; - basis(7) = x * x; - basis(8) = y * y; - basis(9)= z * x; - basis(10)= x * x * y; - basis(11) = x * x * z; - basis(12) = y * y * x; - basis(13) = y * y * z; - basis(14) = z * z * x; - basis(15) = z * z * y; - basis(16) = x * x * x; - basis(17) = y * y * y; - basis(18) = z * z * z; - return basis; -} - - - - - -void run () -{ - auto input = Image::open (argument[0]); - auto output = Image::create (argument[1], input); - - if (input.ndim() != 3) - throw Exception ("input image must be 3D"); - - Image mask; - auto opt = get_options("mask"); - if (opt.size()) { - mask = Image::open (opt[0][0]); - } else { - Filter::OptimalThreshold threshold_filter (input); - mask = Image::scratch (threshold_filter); - threshold_filter (input, mask); - } - - - size_t num_voxels = 0; - for (auto i = Loop (mask) (mask); i; ++i) { - if (mask.value()) - num_voxels++; - } - - Eigen::MatrixXf X (num_voxels, 19); - Eigen::VectorXf y (num_voxels); - y.setOnes(); - - { - ProgressBar progress ("fitting model..."); - size_t counter = 0; - Transform transform (input); - for (auto i = Loop (mask) (input, mask); i; ++i) { - if (mask.value()) { - y [counter] = input.value(); - Eigen::Vector3 vox (input.index(0), input.index(1), input.index(2)); - Eigen::Vector3 pos = transform.voxel2scanner * vox; - X.row (counter++) = basis_function (pos); - } - } - progress++; - - Eigen::VectorXf w = X.colPivHouseholderQr().solve(y); - - progress++; - for (auto i = Loop (output) (output); i; ++i) { - Eigen::Vector3 vox (output.index(0), output.index(1), output.index(2)); - Eigen::Vector3 pos = transform.voxel2scanner * vox; - output.value() = basis_function (pos).dot (w); - } - } -} - diff --git a/cmd/mrstats.cpp b/cmd/mrstats.cpp index 94cc35dd43..1f8b948cc4 100644 --- a/cmd/mrstats.cpp +++ b/cmd/mrstats.cpp @@ -13,17 +13,19 @@ #include -#include #include "command.h" #include "datatype.h" #include "image.h" #include "image_helpers.h" #include "memory.h" +#include "stats.h" +#include "types.h" + #include "algo/histogram.h" #include "algo/loop.h" #include "file/ofstream.h" -#include "stats.h" + using namespace MR; diff --git a/cmd/mrthreshold.cpp b/cmd/mrthreshold.cpp index 35a6033b87..54a940d1d5 100644 --- a/cmd/mrthreshold.cpp +++ b/cmd/mrthreshold.cpp @@ -13,13 +13,13 @@ #include -#include #include "command.h" #include "image.h" #include "image_helpers.h" #include "memory.h" #include "progressbar.h" +#include "types.h" #include "algo/loop.h" #include "filter/optimal_threshold.h" diff --git a/cmd/mrtransform.cpp b/cmd/mrtransform.cpp index 4133cf3448..9f9bec01e7 100644 --- a/cmd/mrtransform.cpp +++ b/cmd/mrtransform.cpp @@ -123,6 +123,14 @@ void usage () "set the interpolation method to use when reslicing (choices: nearest, linear, cubic, sinc. Default: cubic).") + Argument ("method").type_choice (interp_choices) + + Option ("oversample", + "set the amount of over-sampling (in the target space) to perform when regridding. This is particularly " + "relevant when downsamping a high-resolution image to a low-resolution image, to avoid aliasing artefacts. " + "This can consist of a single integer, or a comma-separated list of 3 integers if different oversampling " + "factors are desired along the different axes. Default is determined from ratio of voxel dimensions (disabled " + "for nearest-neighbour interpolation).") + + Argument ("factor").type_sequence_int() + + OptionGroup ("Non-linear transformation options") // TODO point users to a documentation page describing the warp field format @@ -170,19 +178,20 @@ void usage () "Use NaN as the out of bounds value (Default: 0.0)"); } -void apply_warp (Image& input, Image& output, Image& warp, const int interp, const float out_of_bounds_value) { +void apply_warp (Image& input, Image& output, Image& warp, + const int interp, const float out_of_bounds_value, const vector& oversample) { switch (interp) { case 0: - Filter::warp (input, output, warp, out_of_bounds_value); + Filter::warp (input, output, warp, out_of_bounds_value, oversample); break; case 1: - Filter::warp (input, output, warp, out_of_bounds_value); + Filter::warp (input, output, warp, out_of_bounds_value, oversample); break; case 2: - Filter::warp (input, output, warp, out_of_bounds_value); + Filter::warp (input, output, warp, out_of_bounds_value, oversample); break; case 3: - Filter::warp (input, output, warp, out_of_bounds_value); + Filter::warp (input, output, warp, out_of_bounds_value, oversample); break; default: assert (0); @@ -441,6 +450,27 @@ void run () WARN ("interpolator choice ignored since the input image will not be regridded"); } + // over-sampling + vector oversample = Adapter::AutoOverSample; + opt = get_options ("oversample"); + if (opt.size()) { + if (!template_header.valid() && !warp) + throw Exception ("-oversample option applies only to regridding using the template option or to non-linear transformations"); + oversample = opt[0][0]; + if (oversample.size() == 1) + oversample.resize (3, oversample[0]); + else if (oversample.size() != 3) + throw Exception ("-oversample option requires either a single integer, or a comma-separated list of 3 integers"); + for (const auto x : oversample) + if (x < 1) + throw Exception ("-oversample factors must be positive integers"); + } + else if (interp == 0) + // default for nearest-neighbour is no oversampling + oversample = { 1, 1, 1 }; + + + // Out of bounds value float out_of_bounds_value = 0.0; opt = get_options ("nan"); @@ -478,16 +508,16 @@ void run () switch (interp) { case 0: - Filter::reslice (input, output, linear_transform, Adapter::AutoOverSample, out_of_bounds_value); + Filter::reslice (input, output, linear_transform, oversample, out_of_bounds_value); break; case 1: - Filter::reslice (input, output, linear_transform, Adapter::AutoOverSample, out_of_bounds_value); + Filter::reslice (input, output, linear_transform, oversample, out_of_bounds_value); break; case 2: - Filter::reslice (input, output, linear_transform, Adapter::AutoOverSample, out_of_bounds_value); + Filter::reslice (input, output, linear_transform, oversample, out_of_bounds_value); break; case 3: - Filter::reslice (input, output, linear_transform, Adapter::AutoOverSample, out_of_bounds_value); + Filter::reslice (input, output, linear_transform, oversample, out_of_bounds_value); break; default: assert (0); @@ -523,7 +553,7 @@ void run () } else { warp_deform = Registration::Warp::compute_full_deformation (warp, template_header, from); } - apply_warp (input, output, warp_deform, interp, out_of_bounds_value); + apply_warp (input, output, warp_deform, interp, out_of_bounds_value, oversample); if (fod_reorientation) Registration::Transform::reorient_warp ("reorienting", output, warp_deform, directions_cartesian.transpose(), modulate); @@ -531,13 +561,13 @@ void run () } else if (warp.ndim() == 4 && linear) { auto warp_composed = Image::scratch (warp); Registration::Warp::compose_linear_deformation (linear_transform, warp, warp_composed); - apply_warp (input, output, warp_composed, interp, out_of_bounds_value); + apply_warp (input, output, warp_composed, interp, out_of_bounds_value, oversample); if (fod_reorientation) Registration::Transform::reorient_warp ("reorienting", output, warp_composed, directions_cartesian.transpose(), modulate); // Apply 4D deformation field only } else { - apply_warp (input, output, warp, interp, out_of_bounds_value); + apply_warp (input, output, warp, interp, out_of_bounds_value, oversample); if (fod_reorientation) Registration::Transform::reorient_warp ("reorienting", output, warp, directions_cartesian.transpose(), modulate); } diff --git a/cmd/mtbin.cpp b/cmd/mtbin.cpp index d91b36da06..40d5bf6f3b 100644 --- a/cmd/mtbin.cpp +++ b/cmd/mtbin.cpp @@ -33,19 +33,10 @@ void usage () { AUTHOR = "David Raffelt (david.raffelt@florey.edu.au), Rami Tabbara (rami.tabbara@florey.edu.au) and Thijs Dhollander (thijs.dhollander@gmail.com)"; - SYNOPSIS = "Multi-Tissue Bias field correction and Intensity Normalisation (MTBIN)"; + SYNOPSIS = "Multi-Tissue Bias field correction and Intensity Normalisation (WARNING: deprecated)."; DESCRIPTION - + "This command inputs N number of tissue components " - "(e.g. from multi-tissue CSD), and outputs N corrected tissue components. Intensity normalisation is performed by either " - "determining a common global normalisation factor for all tissue types (default) or by normalising each tissue type independently " - "with a single tissue-specific global scale factor." - - + "The -mask option is mandatory, and is optimally provided with a brain mask, such as the one obtained from dwi2mask earlier in the processing pipeline." - - + "Example usage: mtbin wm.mif wm_norm.mif gm.mif gm_norm.mif csf.mif csf_norm.mif -mask mask.mif." - - + "The estimated multiplicative bias field is guaranteed to have a mean of 1 over all voxels within the mask."; + + "WARNING: this command is deprecated and may produce highly inappropriate results in several cases. Not recommended and at your own discretion. Please use the new mtnormalise command instead for reliable results."; ARGUMENTS + Argument ("input output", "list of all input and output tissue compartment files. See example usage in the description. " @@ -69,7 +60,9 @@ void usage () + Argument ("number").type_integer() + Option ("check", "check the final mask used to compute the bias field. This mask excludes outlier regions ignored by the bias field fitting procedure. However, these regions are still corrected for bias fields based on the other image data.") - + Argument ("image").type_image_out (); + + Argument ("image").type_image_out () + + + Option ("override", "consciously use this deprecated command. Not recommended and at your own discretion."); } const int n_basis_vecs (20); @@ -133,6 +126,12 @@ FORCE_INLINE void refine_mask (Image& summed, void run () { + + WARN ("This command is deprecated and may produce inappropriate results in several cases. Please use the new mtnormalise."); + + if (!get_options("override").size()) + throw Exception ("This command is deprecated and not recommended for proper use of its original advertised functions. Check the option list for an option to consciously use this command anyway."); + if (argument.size() % 2) throw Exception ("The number of input arguments must be even. There must be an output file provided for every input tissue image"); @@ -368,4 +367,7 @@ void run () output_image.value() = scale_factors(j, 0) * input_images[j].value() / bias_field.value(); } } + + WARN ("Using the output images as part of a pipeline to analyse one's data is discouraged."); + } diff --git a/cmd/mtnormalise.cpp b/cmd/mtnormalise.cpp index 27d2c082f9..d36adf1520 100644 --- a/cmd/mtnormalise.cpp +++ b/cmd/mtnormalise.cpp @@ -15,124 +15,513 @@ #include "command.h" #include "image.h" #include "algo/loop.h" -#include "adapter/extract.h" -#include "filter/optimal_threshold.h" +#include "transform.h" +#include "math/least_squares.h" +#include "algo/threaded_copy.h" +#include "adapter/replicate.h" using namespace MR; using namespace App; -#define DEFAULT_NORM_VALUE 0.282094 +#define DEFAULT_NORM_VALUE 0.28209479177 +#define DEFAULT_MAIN_ITER_VALUE 15 +#define DEFAULT_BALANCE_MAXITER_VALUE 7 +#define DEFAULT_POLY_ORDER 3 + +const char* poly_order_choices[] = { "0", "1", "2", "3", nullptr }; void usage () { - AUTHOR = "David Raffelt (david.raffelt@florey.edu.au)"; + AUTHOR = "Thijs Dhollander (thijs.dhollander@gmail.com), Rami Tabbara (rami.tabbara@florey.edu.au) and David Raffelt (david.raffelt@florey.edu.au)"; - SYNOPSIS = "Multi-tissue normalise"; + SYNOPSIS = "Multi-tissue informed log-domain intensity normalisation"; DESCRIPTION - + "Globally normalise multiple tissue compartments (e.g. WM, GM, CSF) " - "from multi-tissue CSD such that their sum (of their DC terms) within each voxel is as close to a scalar as possible " - "(Default: sqrt(1/(4*pi)). Normalisation is performed by solving for a single scale factor to adjust each tissue type." + + "This command inputs any number of tissue components (e.g. from multi-tissue CSD) " + "and outputs corresponding normalised tissue components. Intensity normalisation is " + "performed in the log-domain, and can smoothly vary spatially to accomodate the " + "effects of (residual) intensity inhomogeneities." + + + "The -mask option is mandatory and is optimally provided with a brain mask " + "(such as the one obtained from dwi2mask earlier in the processing pipeline). " + "Outlier areas with exceptionally low or high combined tissue contributions are " + "accounted for and reoptimised as the intensity inhomogeneity estimation becomes " + "more accurate." - + "Example usage: mtnormalise wm.mif wm_norm.mif gm.mif gm_norm.mif csf.mif csf_norm.mif"; + + "Example usage: mtnormalise wmfod.mif wmfod_norm.mif gm.mif gm_norm.mif csf.mif csf_norm.mif -mask mask.mif."; ARGUMENTS - + Argument ("input output", "list of all input and output tissue compartment files. See example usage in the description. " - "Note that any number of tissues can be normalised").type_image_in().allow_multiple(); + + Argument ("input output", "list of all input and output tissue compartment files. See example usage in the description.").type_image_in().allow_multiple(); OPTIONS - + Option ("mask", "define the mask to compute the normalisation within. If not supplied this is estimated automatically") + + Option ("mask", "the mask defines the data used to compute the intensity normalisation. This option is mandatory.").required () + Argument ("image").type_image_in () - + Option ("value", "specify the value to which the summed tissue compartments will be to (Default: sqrt(1/(4*pi) = " + str(DEFAULT_NORM_VALUE, 3) + ")") - + Argument ("number").type_float (); + + + Option ("order", "the maximum order of the polynomial basis used to fit the normalisation field in the log-domain. An order of 0 is equivalent to not allowing spatial variance of the intensity normalisation factor. (default: " + str(DEFAULT_POLY_ORDER) + ")") + + Argument ("number").type_choice (poly_order_choices) + + + Option ("niter", "set the number of iterations. (default: " + str(DEFAULT_MAIN_ITER_VALUE) + ")") + + Argument ("number").type_integer() + + + Option ("check_norm", "output the final estimated spatially varying intensity level that is used for normalisation.") + + Argument ("image").type_image_out () + + + Option ("check_mask", "output the final mask used to compute the normalisation. " + "This mask excludes regions identified as outliers by the optimisation process.") + + Argument ("image").type_image_out () + + + Option ("value", "specify the (positive) reference value to which the summed tissue compartments will be normalised. " + "(default: " + str(DEFAULT_NORM_VALUE, 6) + ", SH DC term for unit angular integral)") + + Argument ("number").type_float (std::numeric_limits::min()); + } +template +struct PolyBasisFunction { MEMALIGN (PolyBasisFunction) + + const int n_basis_vecs = 20; + + FORCE_INLINE Eigen::MatrixXd operator () (const Eigen::Vector3& pos) { + double x = pos[0]; + double y = pos[1]; + double z = pos[2]; + Eigen::MatrixXd basis(n_basis_vecs, 1); + basis(0) = 1.0; + basis(1) = x; + basis(2) = y; + basis(3) = z; + basis(4) = x * x; + basis(5) = y * y; + basis(6) = z * z; + basis(7) = x * y; + basis(8) = x * z; + basis(9) = y * z; + basis(10) = x * x * x; + basis(11) = y * y * y; + basis(12) = z * z * z; + basis(13) = x * x * y; + basis(14) = x * x * z; + basis(15) = y * y * x; + basis(16) = y * y * z; + basis(17) = z * z * x; + basis(18) = z * z * y; + basis(19) = x * y * z; + return basis; + } +}; + + +template <> +struct PolyBasisFunction<0> { MEMALIGN (PolyBasisFunction<0>) + const int n_basis_vecs = 1; + + PolyBasisFunction() {} + FORCE_INLINE Eigen::MatrixXd operator () (const Eigen::Vector3&) { + Eigen::MatrixXd basis(n_basis_vecs, 1); + basis(0) = 1.0; + return basis; + } +}; + + +template <> +struct PolyBasisFunction<1> { MEMALIGN (PolyBasisFunction<1>) + const int n_basis_vecs = 4; + + FORCE_INLINE Eigen::MatrixXd operator () (const Eigen::Vector3& pos) { + double x = pos[0]; + double y = pos[1]; + double z = pos[2]; + Eigen::MatrixXd basis(n_basis_vecs, 1); + basis(0) = 1.0; + basis(1) = x; + basis(2) = y; + basis(3) = z; + return basis; + } +}; + + +template <> +struct PolyBasisFunction<2> { MEMALIGN (PolyBasisFunction<2>) + const int n_basis_vecs = 10; + + FORCE_INLINE Eigen::MatrixXd operator () (const Eigen::Vector3& pos) { + double x = pos[0]; + double y = pos[1]; + double z = pos[2]; + Eigen::MatrixXd basis(n_basis_vecs, 1); + basis(0) = 1.0; + basis(1) = x; + basis(2) = y; + basis(3) = z; + basis(4) = x * x; + basis(5) = y * y; + basis(6) = z * z; + basis(7) = x * y; + basis(8) = x * z; + basis(9) = y * z; + return basis; + } +}; + + +// Removes non-physical voxels from the mask +FORCE_INLINE void refine_mask (Image& summed, + Image& initial_mask, + Image& refined_mask) { + + for (auto i = Loop (summed, 0, 3) (summed, initial_mask, refined_mask); i; ++i) { + if (std::isfinite((float) summed.value ()) && summed.value () > 0.f && initial_mask.value ()) + refined_mask.value () = true; + else + refined_mask.value () = false; + } +} + + +template void run_primitive (); + void run () { - if (argument.size() % 2) - throw Exception ("The number of input arguments must be even. There must be an output file provided for every input tissue image"); + throw Exception ("The number of arguments must be even, provided as pairs of each input and its corresponding output file."); + + // Get poly-order of basis function + auto opt = get_options ("order"); + if (opt.size ()) { + const int order = int(opt[0][0]); + switch (order) { + case 0: + run_primitive<0> (); + break; + case 1: + run_primitive<1> (); + break; + case 2: + run_primitive<2> (); + break; + default: + run_primitive (); + break; + } + } else + run_primitive (); +} - vector> input_images; + +template +void run_primitive () { + + PolyBasisFunction basis_function; + + using ImageType = Image; + using MaskType = Image; + + vector> input_images; vector
output_headers; vector output_filenames; - vector sh_image_indexes; + ProgressBar input_progress ("loading input images"); + + // Open input images and prepare output image headers for (size_t i = 0; i < argument.size(); i += 2) { - Header header = Header::open (argument[i]); - if (header.ndim() == 4 && header.size(3) > 1) { // assume SH image to extract DC term - auto dc = Adapter::make (header.get_image(), 3, vector (1, 0)); - input_images.emplace_back (Image::scratch(dc)); - threaded_copy_with_progress_message ("loading image", dc, input_images[i / 2]); - sh_image_indexes.push_back (i / 2); - } else { - input_images.emplace_back (Image::open(argument[i])); - } - if (i) + input_progress++; + + auto image = ImageType::open (argument[i]); + + if (image.ndim () > 4) + throw Exception ("Input image \"" + image.name() + "\" contains more than 4 dimensions."); + + // Elevate image dimensions to ensure it is 4-dimensional + // e.g. x,y,z -> x,y,z,1 + // This ensures consistency across multiple tissue input images + Header h_image4d (image); + h_image4d.ndim() = 4; + + input_images.emplace_back (image, h_image4d); + + if (i > 0) check_dimensions (input_images[0], input_images[i / 2], 0, 3); - // we can't create the image yet if we want to put the scale factor into the output header + + if (Path::exists (argument[i + 1]) && !App::overwrite_files) + throw Exception ("Output file \"" + argument[i] + "\" already exists. (use -force option to force overwrite)"); + + output_headers.push_back (std::move (h_image4d)); output_filenames.push_back (argument[i + 1]); - output_headers.emplace_back (header); } - Image mask; - auto opt = get_options("mask"); - if (opt.size()) { - mask = Image::open (opt[0][0]); - } else { - auto summed = Image::scratch (input_images[0], "summed image"); + const size_t n_tissue_types = input_images.size(); + + // Load the mask and refine the initial mask to exclude non-positive summed tissue components + Header header_3D (input_images[0]); + header_3D.ndim() = 3; + auto opt = get_options ("mask"); + + auto orig_mask = MaskType::open (opt[0][0]); + auto initial_mask = MaskType::scratch (orig_mask, "Initial processing mask"); + auto mask = MaskType::scratch (orig_mask, "Processing mask"); + auto prev_mask = MaskType::scratch (orig_mask, "Previous processing mask"); + + { + auto summed = ImageType::scratch (header_3D, "Summed tissue volumes"); for (size_t j = 0; j < input_images.size(); ++j) { - for (auto i = Loop (summed, 0, 3) (summed, input_images[j]); i; ++i) + input_progress++; + + for (auto i = Loop (0, 3) (summed, input_images[j]); i; ++i) summed.value() += input_images[j].value(); } - Filter::OptimalThreshold threshold_filter (summed); - mask = Image::scratch (threshold_filter); - threshold_filter (summed, mask); + refine_mask (summed, orig_mask, initial_mask); } + + threaded_copy (initial_mask, mask); + + + // Load input images into single 4d-image and zero-clamp combined-tissue image + Header h_combined_tissue (input_images[0]); + h_combined_tissue.ndim () = 4; + h_combined_tissue.size (3) = n_tissue_types; + auto combined_tissue = ImageType::scratch (h_combined_tissue, "Tissue components"); + + for (size_t i = 0; i < n_tissue_types; ++i) { + input_progress++; + + combined_tissue.index (3) = i; + for (auto l = Loop (0, 3) (combined_tissue, input_images[i]); l; ++l) { + combined_tissue.value () = std::max(input_images[i].value (), 0.f); + } + } + size_t num_voxels = 0; for (auto i = Loop (mask) (mask); i; ++i) { if (mask.value()) num_voxels++; } + if (!num_voxels) + throw Exception ("Mask contains no valid voxels."); + + const float normalisation_value = get_option_value ("value", DEFAULT_NORM_VALUE); + const float log_norm_value = std::log (normalisation_value); + const size_t max_iter = get_option_value ("niter", DEFAULT_MAIN_ITER_VALUE); + const size_t max_balance_iter = DEFAULT_BALANCE_MAXITER_VALUE; - { - ProgressBar progress ("normalising tissue compartments..."); - Eigen::MatrixXf X (num_voxels, input_images.size()); - Eigen::MatrixXf y (num_voxels, 1); - y.fill (normalisation_value); - uint32_t counter = 0; - for (auto i = Loop (mask) (mask); i; ++i) { + // Initialise normalisation fields in both image and log domain + Eigen::MatrixXd norm_field_weights; + + auto norm_field_image = ImageType::scratch (header_3D, "Normalisation field (intensity)"); + auto norm_field_log = ImageType::scratch (header_3D, "Normalisation field (log-domain)"); + + for (auto i = Loop(norm_field_log) (norm_field_image, norm_field_log); i; ++i) { + norm_field_image.value() = 1.f; + norm_field_log.value() = 0.f; + } + + Eigen::VectorXd balance_factors (Eigen::VectorXd::Ones (n_tissue_types)); + + size_t iter = 1; + + // Store lambda-function for performing outlier-rejection. + // We perform a coarse outlier-rejection initially as well as + // a finer outlier-rejection within each iteration of the + // tissue (re)balancing loop + auto outlier_rejection = [&](float outlier_range) { + + auto summed_log = ImageType::scratch (header_3D, "Log of summed tissue volumes"); + for (size_t j = 0; j < n_tissue_types; ++j) { + for (auto i = Loop (0, 3) (summed_log, combined_tissue, norm_field_image); i; ++i) { + combined_tissue.index(3) = j; + summed_log.value() += balance_factors(j) * combined_tissue.value() / norm_field_image.value(); + } + summed_log.value() = std::log(summed_log.value()); + } + + threaded_copy (initial_mask, mask); + + vector summed_log_values; + summed_log_values.reserve (num_voxels); + for (auto i = Loop (0, 3) (mask, summed_log); i; ++i) { + if (mask.value()) + summed_log_values.push_back (summed_log.value()); + } + + num_voxels = summed_log_values.size(); + + const auto lower_quartile_it = summed_log_values.begin() + std::round ((float)num_voxels * 0.25f); + std::nth_element (summed_log_values.begin(), lower_quartile_it, summed_log_values.end()); + const float lower_quartile = *lower_quartile_it; + const auto upper_quartile_it = summed_log_values.begin() + std::round ((float)num_voxels * 0.75f); + std::nth_element (lower_quartile_it, upper_quartile_it, summed_log_values.end()); + const float upper_quartile = *upper_quartile_it; + const float lower_outlier_threshold = lower_quartile - outlier_range * (upper_quartile - lower_quartile); + const float upper_outlier_threshold = upper_quartile + outlier_range * (upper_quartile - lower_quartile); + + for (auto i = Loop (0, 3) (mask, summed_log); i; ++i) { if (mask.value()) { - for (size_t j = 0; j < input_images.size(); ++j) { - assign_pos_of (mask, 0, 3).to (input_images[j]); - X (counter, j) = input_images[j].value(); + if (summed_log.value() < lower_outlier_threshold || summed_log.value() > upper_outlier_threshold) { + mask.value() = 0; + num_voxels--; } - ++counter; } } - progress++; - Eigen::MatrixXf w = X.jacobiSvd (Eigen::ComputeThinU | Eigen::ComputeThinV).solve(y); - progress++; - for (size_t j = 0; j < input_images.size(); ++j) { - float scale_factor = w(j,0); - // If scale factor already present, we accumulate (for use in mtbin script) - if (input_images[j].keyval().count("normalisation_scale_factor")) - scale_factor *= std::stof(input_images[j].keyval().at("normalisation_scale_factor")); - output_headers[j].keyval()["normalisation_scale_factor"] = str(scale_factor); - auto output_image = Image::create (output_filenames[j], output_headers[j]); - if (std::find (sh_image_indexes.begin(), sh_image_indexes.end(), j) != sh_image_indexes.end()) { - auto input = Image::open (argument[j * 2]); - for (auto i = Loop (input) (input, output_image); i; ++i) - output_image.value() = input.value() * w(j,0); - } else { - for (auto i = Loop (input_images[j]) (input_images[j], output_image); i; ++i) - output_image.value() = input_images[j].value() * w(j,0); + + if (log_level >= 3) + display (mask); + }; + + input_progress.done (); + ProgressBar progress ("performing log-domain intensity normalisation", max_iter); + + // Perform an initial outlier rejection prior to the first iteration + outlier_rejection (3.f); + + threaded_copy (mask, prev_mask); + + while (iter <= max_iter) { + + INFO ("Iteration: " + str(iter)); + + // Iteratively compute tissue balance factors with outlier rejection + size_t balance_iter = 1; + bool balance_converged = false; + + while (!balance_converged && balance_iter <= max_balance_iter) { + + DEBUG ("Balance and outlier rejection iteration " + str(balance_iter) + " starts."); + + if (n_tissue_types > 1) { + + // Solve for tissue balance factors + Eigen::MatrixXd X (num_voxels, n_tissue_types); + Eigen::VectorXd y (Eigen::VectorXd::Ones (num_voxels)); + uint32_t index = 0; + + for (auto i = Loop (0, 3) (mask, combined_tissue, norm_field_image); i; ++i) { + if (mask.value()) { + for (size_t j = 0; j < n_tissue_types; ++j) { + combined_tissue.index (3) = j; + X (index, j) = combined_tissue.value() / norm_field_image.value(); + } + ++index; + } + } + + balance_factors = X.colPivHouseholderQr().solve(y); + + // Ensure our balance factors satisfy the condition that sum(log(balance_factors)) = 0 + double log_sum = 0.0; + for (size_t j = 0; j < n_tissue_types; ++j) { + if (balance_factors(j) <= 0.0) + throw Exception ("Non-positive tissue balance factor was computed." + " Tissue index: " + str(j+1) + " Balance factor: " + str(balance_factors(j)) + + " Needs to be strictly positive!"); + log_sum += std::log (balance_factors(j)); + } + balance_factors /= std::exp (log_sum / n_tissue_types); + } + + INFO ("Balance factors (" + str(balance_iter) + "): " + str(balance_factors.transpose())); + + // Perform outlier rejection on log-domain of summed images + outlier_rejection(1.5f); + + // Check for convergence + balance_converged = true; + for (auto i = Loop (0, 3) (mask, prev_mask); i; ++i) { + if (mask.value() != prev_mask.value()) { + balance_converged = false; + break; + } } - progress++; + + threaded_copy (mask, prev_mask); + + balance_iter++; } + + + // Solve for normalisation field weights in the log domain + Transform transform (mask); + Eigen::MatrixXd norm_field_basis (num_voxels, basis_function.n_basis_vecs); + Eigen::VectorXd y (num_voxels); + uint32_t index = 0; + for (auto i = Loop (0, 3) (mask, combined_tissue); i; ++i) { + if (mask.value()) { + Eigen::Vector3 vox (mask.index(0), mask.index(1), mask.index(2)); + Eigen::Vector3 pos = transform.voxel2scanner * vox; + norm_field_basis.row (index) = basis_function (pos).col(0); + + double sum = 0.0; + for (size_t j = 0; j < n_tissue_types; ++j) { + combined_tissue.index(3) = j; + sum += balance_factors(j) * combined_tissue.value() ; + } + y (index++) = std::log(sum) - log_norm_value; + } + } + + norm_field_weights = norm_field_basis.colPivHouseholderQr().solve(y); + + // Generate normalisation field in the log domain + for (auto i = Loop (0, 3) (norm_field_log); i; ++i) { + Eigen::Vector3 vox (norm_field_log.index(0), norm_field_log.index(1), norm_field_log.index(2)); + Eigen::Vector3 pos = transform.voxel2scanner * vox; + norm_field_log.value() = basis_function (pos).col(0).dot (norm_field_weights.col(0)); + } + + // Generate normalisation field in the image domain + for (auto i = Loop (0, 3) (norm_field_log, norm_field_image); i; ++i) + norm_field_image.value () = std::exp(norm_field_log.value()); + + progress++; + iter++; } -} + progress.done(); + + ProgressBar output_progress("writing output images"); + + opt = get_options ("check_norm"); + if (opt.size()) { + auto norm_field_output = ImageType::create (opt[0][0], header_3D); + threaded_copy (norm_field_image, norm_field_output); + } + + opt = get_options ("check_mask"); + if (opt.size()) { + auto mask_output = ImageType::create (opt[0][0], mask); + threaded_copy (mask, mask_output); + } + + // Compute log-norm scale parameter (geometric mean of normalisation field in outlier-free mask). + float lognorm_scale (0.f); + if (num_voxels) { + for (auto i = Loop (0,3) (mask, norm_field_log); i; ++i) { + if (mask.value ()) + lognorm_scale += norm_field_log.value (); + } + + lognorm_scale = std::exp(lognorm_scale / (float)num_voxels); + } + + + for (size_t j = 0; j < output_filenames.size(); ++j) { + output_progress++; + + output_headers[j].keyval()["lognorm_scale"] = str(lognorm_scale); + auto output_image = ImageType::create (output_filenames[j], output_headers[j]); + const size_t n_vols = input_images[j].size(3); + const Eigen::VectorXf zero_vec = Eigen::VectorXf::Zero (n_vols); + + for (auto i = Loop (0,3) (output_image, input_images[j], norm_field_image); i; ++i) { + input_images[j].index(3) = 0; + + if (input_images[j].value() < 0.f) + output_image.row(3) = zero_vec; + else + output_image.row(3) = Eigen::VectorXf{input_images[j].row(3)} / norm_field_image.value(); + } + } +} diff --git a/cmd/peaks2amp.cpp b/cmd/peaks2amp.cpp index 5e1177cb4a..163d7100bc 100644 --- a/cmd/peaks2amp.cpp +++ b/cmd/peaks2amp.cpp @@ -17,7 +17,6 @@ #include "algo/loop.h" -using namespace std; using namespace MR; using namespace App; diff --git a/cmd/shbasis.cpp b/cmd/shbasis.cpp index 78d3019cca..8702fe660f 100644 --- a/cmd/shbasis.cpp +++ b/cmd/shbasis.cpp @@ -12,17 +12,16 @@ */ -#include - #include "app.h" #include "bitset.h" #include "command.h" #include "datatype.h" -#include "progressbar.h" -#include "memory.h" - #include "header.h" #include "image.h" +#include "memory.h" +#include "progressbar.h" +#include "types.h" + #include "algo/loop.h" #include "math/SH.h" diff --git a/cmd/tck2connectome.cpp b/cmd/tck2connectome.cpp index 56ac1219db..900137b5d6 100644 --- a/cmd/tck2connectome.cpp +++ b/cmd/tck2connectome.cpp @@ -12,12 +12,12 @@ */ -#include #include #include "command.h" #include "image.h" #include "thread_queue.h" +#include "types.h" #include "dwi/tractography/file.h" #include "dwi/tractography/properties.h" diff --git a/cmd/tckedit.cpp b/cmd/tckedit.cpp index 30b388f539..664dcdcc51 100644 --- a/cmd/tckedit.cpp +++ b/cmd/tckedit.cpp @@ -13,13 +13,12 @@ #include -#include #include "command.h" #include "exception.h" #include "mrtrix.h" - #include "thread_queue.h" +#include "types.h" #include "dwi/tractography/file.h" #include "dwi/tractography/properties.h" diff --git a/cmd/tckmap.cpp b/cmd/tckmap.cpp index 576e7dc1e8..9d38274c92 100644 --- a/cmd/tckmap.cpp +++ b/cmd/tckmap.cpp @@ -12,15 +12,14 @@ */ -#include #include #include "command.h" -#include "progressbar.h" -#include "memory.h" - #include "image.h" +#include "memory.h" +#include "progressbar.h" #include "thread_queue.h" +#include "types.h" #include "dwi/gradient.h" #include "dwi/tractography/file.h" diff --git a/cmd/tcksift.cpp b/cmd/tcksift.cpp index 26dbfc8b13..ab625897eb 100644 --- a/cmd/tcksift.cpp +++ b/cmd/tcksift.cpp @@ -12,11 +12,9 @@ */ -#include - #include "command.h" - #include "image.h" +#include "types.h" #include "math/SH.h" diff --git a/cmd/tckstats.cpp b/cmd/tckstats.cpp index b4655d50fe..89d69c0c73 100644 --- a/cmd/tckstats.cpp +++ b/cmd/tckstats.cpp @@ -12,11 +12,10 @@ */ -#include - #include "command.h" -#include "progressbar.h" #include "memory.h" +#include "progressbar.h" +#include "types.h" #include "file/ofstream.h" diff --git a/cmd/tensor2metric.cpp b/cmd/tensor2metric.cpp index 3dbad85d80..c007aed420 100644 --- a/cmd/tensor2metric.cpp +++ b/cmd/tensor2metric.cpp @@ -127,7 +127,10 @@ class Processor { MEMALIGN(Processor) value_img (value_img), vector_img (vector_img), vals (vals), - modulate (modulate) { } + modulate (modulate) { + for (auto& n : this->vals) + --n; + } void operator() (Image& dt_img) { @@ -159,7 +162,7 @@ class Processor { MEMALIGN(Processor) fa_img.value() = fa; } - bool need_eigenvalues = value_img.valid() || (vector_img.valid() && (modulate == 2)) || ad_img.valid() || rd_img.valid() || cl_img.valid() || cp_img.valid() || cs_img.valid(); + bool need_eigenvalues = value_img.valid() || vector_img.valid() || ad_img.valid() || rd_img.valid() || cl_img.valid() || cp_img.valid() || cs_img.valid(); Eigen::SelfAdjointEigenSolver es; if (need_eigenvalues || vector_img.valid()) { @@ -174,8 +177,13 @@ class Processor { MEMALIGN(Processor) } Eigen::Vector3d eigval; - if (need_eigenvalues) + ssize_t ith_eig[3] = { 2, 1, 0 }; + if (need_eigenvalues) { eigval = es.eigenvalues(); + ith_eig[0] = 0; ith_eig[1] = 1; ith_eig[2] = 2; + std::sort (std::begin (ith_eig), std::end (ith_eig), + [&eigval](size_t a, size_t b) { return std::abs(eigval[a]) > std::abs(eigval[b]); }); + } /* output value */ if (value_img.valid()) { @@ -183,10 +191,10 @@ class Processor { MEMALIGN(Processor) if (vals.size() > 1) { auto l = Loop(3)(value_img); for (size_t i = 0; i < vals.size(); i++) { - value_img.value() = eigval(3-vals[i]); l++; + value_img.value() = eigval(ith_eig[vals[i]]); l++; } } else { - value_img.value() = eigval(3-vals[0]); + value_img.value() = eigval(ith_eig[vals[0]]); } } @@ -231,10 +239,10 @@ class Processor { MEMALIGN(Processor) if (modulate == 1) fact = fa; else if (modulate == 2) - fact = eigval(3-vals[i]); - vector_img.value() = eigvec(0,3-vals[i])*fact; l++; - vector_img.value() = eigvec(1,3-vals[i])*fact; l++; - vector_img.value() = eigvec(2,3-vals[i])*fact; l++; + fact = eigval(ith_eig[vals[i]]); + vector_img.value() = eigvec(0,ith_eig[vals[i]])*fact; l++; + vector_img.value() = eigvec(1,ith_eig[vals[i]])*fact; l++; + vector_img.value() = eigvec(2,ith_eig[vals[i]])*fact; l++; } } } @@ -254,6 +262,13 @@ class Processor { MEMALIGN(Processor) int modulate; }; + + + + + + + void run () { auto dt_img = Image::open (argument[0]); @@ -265,12 +280,15 @@ void run () mask_img = Image::open (opt[0][0]); check_dimensions (dt_img, mask_img, 0, 3); } + + size_t metric_count = 0; auto adc_img = Image(); opt = get_options ("adc"); if (opt.size()) { header.ndim() = 3; adc_img = Image::create (opt[0][0], header); + metric_count++; } auto fa_img = Image(); @@ -278,6 +296,7 @@ void run () if (opt.size()) { header.ndim() = 3; fa_img = Image::create (opt[0][0], header); + metric_count++; } auto ad_img = Image(); @@ -285,6 +304,7 @@ void run () if (opt.size()) { header.ndim() = 3; ad_img = Image::create (opt[0][0], header); + metric_count++; } auto rd_img = Image(); @@ -292,6 +312,7 @@ void run () if (opt.size()) { header.ndim() = 3; rd_img = Image::create (opt[0][0], header); + metric_count++; } auto cl_img = Image(); @@ -299,6 +320,7 @@ void run () if (opt.size()) { header.ndim() = 3; cl_img = Image::create (opt[0][0], header); + metric_count++; } auto cp_img = Image(); @@ -306,6 +328,7 @@ void run () if (opt.size()) { header.ndim() = 3; cp_img = Image::create (opt[0][0], header); + metric_count++; } auto cs_img = Image(); @@ -313,17 +336,18 @@ void run () if (opt.size()) { header.ndim() = 3; cs_img = Image::create (opt[0][0], header); + metric_count++; } vector vals = {1}; opt = get_options ("num"); if (opt.size()) { vals = opt[0][0]; - if (vals.empty()) - throw Exception ("invalid eigenvalue/eigenvector number specifier"); - for (size_t i = 0; i < vals.size(); ++i) - if (vals[i] < 1 || vals[i] > 3) - throw Exception ("eigenvalue/eigenvector number is out of bounds"); + if (vals.empty()) + throw Exception ("invalid eigenvalue/eigenvector number specifier"); + for (size_t i = 0; i < vals.size(); ++i) + if (vals[i] < 1 || vals[i] > 3) + throw Exception ("eigenvalue/eigenvector number is out of bounds"); } float modulate = get_option_value ("modulate", 1); @@ -337,6 +361,7 @@ void run () header.size (3) = vals.size(); } value_img = Image::create (opt[0][0], header); + metric_count++; } auto vector_img = Image(); @@ -345,8 +370,12 @@ void run () header.ndim() = 4; header.size (3) = vals.size()*3; vector_img = Image::create (opt[0][0], header); + metric_count++; } + + if (!metric_count) + throw Exception ("No output specified; must request at least one metric of interest using the available command-line options"); - ThreadedLoop ("computing metrics", dt_img, 0, 3) + ThreadedLoop (std::string("computing metric") + (metric_count > 1 ? "s" : ""), dt_img, 0, 3) .run (Processor (mask_img, adc_img, fa_img, ad_img, rd_img, cl_img, cp_img, cs_img, value_img, vector_img, vals, modulate), dt_img); } diff --git a/cmd/vectorstats.cpp b/cmd/vectorstats.cpp index 88a678c00e..4eaea1d94f 100644 --- a/cmd/vectorstats.cpp +++ b/cmd/vectorstats.cpp @@ -12,10 +12,9 @@ */ -#include - #include "command.h" #include "progressbar.h" +#include "types.h" #include "file/path.h" #include "math/stats/glm.h" diff --git a/configure b/configure index ef06af4538..263f17c486 100755 --- a/configure +++ b/configure @@ -118,6 +118,12 @@ ENVIRONMENT VARIABLES TIFF_LDFLAGS Any flags required to link with the TIFF library. + FFTW_CFLAGS + Any flags required to compile with the FFTW library. + + FFTW_LDFLAGS + Any flags required to link with the FFTW library. + QMAKE The command to invoke Qt's qmake (default: qmake). @@ -253,6 +259,9 @@ zlib_ldflags = [ '-lz' ] tiff_cflags = [] tiff_ldflags = [ '-ltiff' ] +fftw_cflags = [] +fftw_ldflags = [ '-lfftw3' ] + class TempFile: def __init__ (self, suffix): @@ -833,6 +842,32 @@ if not openmp: +# FFTW: + +report ('Checking for FFTW library: ') + +if 'FFTW_CFLAGS' in os.environ.keys(): fftw_cflags = shlex.split (os.environ['FFTW_CFLAGS']) +if 'FFTW_LDFLAGS' in os.environ.keys(): fftw_ldflags = shlex.split (os.environ['FFTW_LDFLAGS']) + +try: + fftw_version = compile (''' +#include +#include + +int main() { + std::cout << fftw_version << "\\n"; + return (0); +} +''', cpp_flags + fftw_cflags, ld_flags + fftw_ldflags) + report (fftw_version.splitlines()[0] + '\n') + cpp_flags += [ '-DEIGEN_FFTW_DEFAULT' ] + fftw_cflags + ld_flags += fftw_ldflags + ld_lib_flags += fftw_ldflags +except: + report ('not found - FFTW support disabled\n'); + + + # add openmp flags if required and available diff --git a/core/algo/histogram.h b/core/algo/histogram.h index 2379c51e3c..f1c6f0ebd5 100644 --- a/core/algo/histogram.h +++ b/core/algo/histogram.h @@ -15,10 +15,10 @@ #ifndef __algo_histogram_h__ #define __algo_histogram_h__ -#include #include #include "image_helpers.h" +#include "types.h" #include "algo/loop.h" namespace MR diff --git a/core/algo/iterator.h b/core/algo/iterator.h index 358d820be5..9bcb62d8f8 100644 --- a/core/algo/iterator.h +++ b/core/algo/iterator.h @@ -15,8 +15,6 @@ #ifndef __algo_iterator_h__ #define __algo_iterator_h__ -#include - #include "types.h" namespace MR diff --git a/core/algo/neighbourhooditerator.h b/core/algo/neighbourhooditerator.h index 84fc787f2a..0e96d2897a 100644 --- a/core/algo/neighbourhooditerator.h +++ b/core/algo/neighbourhooditerator.h @@ -15,9 +15,8 @@ #ifndef __algo_neighbourhooditerator_h__ #define __algo_neighbourhooditerator_h__ -#include -#include "algo/iterator.h" #include "types.h" +#include "algo/iterator.h" namespace MR { diff --git a/core/app.h b/core/app.h index 10ccf60439..9b6877026d 100644 --- a/core/app.h +++ b/core/app.h @@ -16,17 +16,17 @@ #define __app_h__ #include -#include -#include #include +#include #ifdef None # undef None #endif #include "cmdline_option.h" -#include "file/path.h" #include "signal_handler.h" +#include "types.h" +#include "file/path.h" extern void usage (); diff --git a/core/cmdline_option.h b/core/cmdline_option.h index 927bf95655..3579e98f3e 100644 --- a/core/cmdline_option.h +++ b/core/cmdline_option.h @@ -16,15 +16,15 @@ #define __cmdline_option_h__ #include -#include -#include #include +#include #ifdef None # undef None #endif #include "mrtrix.h" +#include "types.h" namespace MR { diff --git a/core/exception.h b/core/exception.h index 50b37d48a6..53d64ab015 100644 --- a/core/exception.h +++ b/core/exception.h @@ -16,9 +16,8 @@ #define __mrtrix_exception_h__ #include -#include -#include #include +#include #include "types.h" diff --git a/core/file/dicom/element.h b/core/file/dicom/element.h index f0cbe70498..d63f0aca2e 100644 --- a/core/file/dicom/element.h +++ b/core/file/dicom/element.h @@ -15,11 +15,11 @@ #ifndef __file_dicom_element_h__ #define __file_dicom_element_h__ -#include #include #include "memory.h" #include "raw.h" +#include "types.h" #include "file/mmap.h" #include "file/dicom/definitions.h" diff --git a/core/file/dicom/image.cpp b/core/file/dicom/image.cpp index e3b4929fdf..ef52fe6ed3 100644 --- a/core/file/dicom/image.cpp +++ b/core/file/dicom/image.cpp @@ -43,6 +43,10 @@ namespace MR { // process image-specific or per-frame items here: if (is_toplevel) { switch (item.group) { + case 0x0008U: + if (item.element == 0x0008U) + image_type = join (item.get_string(), " "); + return; case 0x0018U: switch (item.element) { case 0x0050U: diff --git a/core/file/dicom/image.h b/core/file/dicom/image.h index ed0eacf017..3508048298 100644 --- a/core/file/dicom/image.h +++ b/core/file/dicom/image.h @@ -31,21 +31,21 @@ namespace MR { public: Frame () { acq_dim[0] = acq_dim[1] = dim[0] = dim[1] = instance = series_num = acq = sequence = UINT_MAX; - position_vector[0] = position_vector[1] = position_vector[2] = NAN; - orientation_x[0] = orientation_x[1] = orientation_x[2] = NAN; - orientation_y[0] = orientation_y[1] = orientation_y[2] = NAN; - orientation_z[0] = orientation_z[1] = orientation_z[2] = NAN; - distance = NAN; - pixel_size[0] = pixel_size[1] = slice_thickness = slice_spacing = NAN; + position_vector[0] = position_vector[1] = position_vector[2] = NaN; + orientation_x[0] = orientation_x[1] = orientation_x[2] = NaN; + orientation_y[0] = orientation_y[1] = orientation_y[2] = NaN; + orientation_z[0] = orientation_z[1] = orientation_z[2] = NaN; + distance = NaN; + pixel_size[0] = pixel_size[1] = slice_thickness = slice_spacing = NaN; scale_intercept = 0.0; scale_slope = 1.0; - bvalue = G[0] = G[1] = G[2] = NAN; + bvalue = G[0] = G[1] = G[2] = NaN; data = bits_alloc = data_size = frame_offset = 0; DW_scheme_wrt_image = false; transfer_syntax_supported = true; pe_axis = 3; pe_sign = 0; - pixel_bandwidth = bandwidth_per_pixel_phase_encode = echo_time = NAN; + pixel_bandwidth = bandwidth_per_pixel_phase_encode = echo_time = NaN; echo_train_length = 0; } @@ -53,7 +53,7 @@ namespace MR { Eigen::Vector3 position_vector, orientation_x, orientation_y, orientation_z, G; default_type distance, pixel_size[2], slice_thickness, slice_spacing, scale_slope, scale_intercept, bvalue; size_t data, bits_alloc, data_size, frame_offset; - std::string filename; + std::string filename, image_type; bool DW_scheme_wrt_image, transfer_syntax_supported; size_t pe_axis; int pe_sign; @@ -64,6 +64,8 @@ namespace MR { bool operator< (const Frame& frame) const { if (series_num != frame.series_num) return series_num < frame.series_num; + if (image_type != frame.image_type) + return image_type < frame.image_type; if (acq != frame.acq) return acq < frame.acq; assert (std::isfinite (distance)); diff --git a/core/file/dicom/mapper.cpp b/core/file/dicom/mapper.cpp index 1e2d78a132..8b66b56aeb 100644 --- a/core/file/dicom/mapper.cpp +++ b/core/file/dicom/mapper.cpp @@ -71,7 +71,8 @@ namespace MR { if (image_it->frames.size()) { std::sort (image_it->frames.begin(), image_it->frames.end(), compare_ptr_contents()); for (auto frame_it : image_it->frames) - frames.push_back (frame_it.get()); + if (frame_it->image_type == series_it->image_type) + frames.push_back (frame_it.get()); } // otherwise add image frame: else @@ -99,7 +100,7 @@ namespace MR { default_type slice_separation = Frame::get_slice_separation (frames, dim[1]); if (series[0]->study->name.size()) - add_line (H.keyval()["comments"], std::string ("study: " + series[0]->study->name)); + add_line (H.keyval()["comments"], std::string ("study: " + series[0]->study->name + " [ " + series[0]->image_type + " ]")); if (patient->DOB.size()) add_line (H.keyval()["comments"], std::string ("DOB: " + format_date (patient->DOB))); @@ -112,11 +113,12 @@ namespace MR { } const Image& image (*(*series[0])[0]); + const Frame& frame (*frames[0]); - if (std::isfinite (image.echo_time)) - H.keyval()["EchoTime"] = str (0.001 * image.echo_time, 6); + if (std::isfinite (frame.echo_time)) + H.keyval()["EchoTime"] = str (0.001 * frame.echo_time, 6); - size_t nchannels = image.frames.size() ? 1 : image.data_size / (image.dim[0] * image.dim[1] * (image.bits_alloc/8)); + size_t nchannels = image.frames.size() ? 1 : image.data_size / (frame.dim[0] * frame.dim[1] * (frame.bits_alloc/8)); if (nchannels > 1) INFO ("data segment is larger than expected from image dimensions - interpreting as multi-channel data"); @@ -131,12 +133,12 @@ namespace MR { } H.stride(0) = ++current_axis; - H.size(0) = image.dim[0]; - H.spacing(0) = image.pixel_size[0]; + H.size(0) = frame.dim[0]; + H.spacing(0) = frame.pixel_size[0]; H.stride(1) = ++current_axis; - H.size(1) = image.dim[1]; - H.spacing(1) = image.pixel_size[1]; + H.size(1) = frame.dim[1]; + H.spacing(1) = frame.pixel_size[1]; H.stride(2) = ++current_axis; H.size(2) = dim[1]; @@ -149,42 +151,41 @@ namespace MR { } - if (image.bits_alloc == 8) + if (frame.bits_alloc == 8) H.datatype() = DataType::UInt8; - else if (image.bits_alloc == 16) { + else if (frame.bits_alloc == 16) { H.datatype() = DataType::UInt16; if (image.is_BE) H.datatype() = DataType::UInt16 | DataType::BigEndian; else H.datatype() = DataType::UInt16 | DataType::LittleEndian; } - else throw Exception ("unexpected number of allocated bits per pixel (" + str (image.bits_alloc) + else throw Exception ("unexpected number of allocated bits per pixel (" + str (frame.bits_alloc) + ") in file \"" + H.name() + "\""); - H.set_intensity_scaling (image.scale_slope, image.scale_intercept); + H.set_intensity_scaling (frame.scale_slope, frame.scale_intercept); // If multi-frame, take the transform information from the sorted frames; the first entry in the // vector should be the first slice of the first volume { transform_type M; - const Frame* frame (image.frames.size() ? image.frames[0].get() : &static_cast (image)); - M(0,0) = -frame->orientation_x[0]; - M(1,0) = -frame->orientation_x[1]; - M(2,0) = +frame->orientation_x[2]; + M(0,0) = -frame.orientation_x[0]; + M(1,0) = -frame.orientation_x[1]; + M(2,0) = +frame.orientation_x[2]; - M(0,1) = -frame->orientation_y[0]; - M(1,1) = -frame->orientation_y[1]; - M(2,1) = +frame->orientation_y[2]; + M(0,1) = -frame.orientation_y[0]; + M(1,1) = -frame.orientation_y[1]; + M(2,1) = +frame.orientation_y[2]; - M(0,2) = -frame->orientation_z[0]; - M(1,2) = -frame->orientation_z[1]; - M(2,2) = +frame->orientation_z[2]; + M(0,2) = -frame.orientation_z[0]; + M(1,2) = -frame.orientation_z[1]; + M(2,2) = +frame.orientation_z[2]; - M(0,3) = -frame->position_vector[0]; - M(1,3) = -frame->position_vector[1]; - M(2,3) = +frame->position_vector[2]; + M(0,3) = -frame.position_vector[0]; + M(1,3) = -frame.position_vector[1]; + M(2,3) = +frame.position_vector[2]; H.transform() = M; std::string dw_scheme = Frame::get_DW_scheme (frames, dim[1], M); @@ -205,24 +206,24 @@ namespace MR { if (H.size (2) != 1) throw Exception ("DICOM mosaic contains multiple slices in image \"" + H.name() + "\""); - H.size(0) = image.acq_dim[0]; - H.size(1) = image.acq_dim[1]; + H.size(0) = frame.acq_dim[0]; + H.size(1) = frame.acq_dim[1]; H.size(2) = image.images_in_mosaic; - if (image.dim[0] % image.acq_dim[0] || image.dim[1] % image.acq_dim[1]) { - WARN ("acquisition matrix [ " + str (image.acq_dim[0]) + " " + str (image.acq_dim[1]) - + " ] does not fit into DICOM mosaic [ " + str (image.dim[0]) + " " + str (image.dim[1]) + if (frame.dim[0] % frame.acq_dim[0] || frame.dim[1] % frame.acq_dim[1]) { + WARN ("acquisition matrix [ " + str (frame.acq_dim[0]) + " " + str (frame.acq_dim[1]) + + " ] does not fit into DICOM mosaic [ " + str (frame.dim[0]) + " " + str (frame.dim[1]) + " ] - adjusting matrix size to suit"); - H.size(0) = image.dim[0] / size_t (float(image.dim[0]) / float(image.acq_dim[0])); - H.size(1) = image.dim[1] / size_t (float(image.dim[1]) / float(image.acq_dim[1])); + H.size(0) = frame.dim[0] / size_t (float(frame.dim[0]) / float(frame.acq_dim[0])); + H.size(1) = frame.dim[1] / size_t (float(frame.dim[1]) / float(frame.acq_dim[1])); } - float xinc = H.spacing(0) * (image.dim[0] - H.size(0)) / 2.0; - float yinc = H.spacing(1) * (image.dim[1] - H.size(1)) / 2.0; + float xinc = H.spacing(0) * (frame.dim[0] - H.size(0)) / 2.0; + float yinc = H.spacing(1) * (frame.dim[1] - H.size(1)) / 2.0; for (size_t i = 0; i < 3; i++) H.transform()(i,3) += xinc * H.transform()(i,0) + yinc * H.transform()(i,1); - io_handler.reset (new MR::ImageIO::Mosaic (H, image.dim[0], image.dim[1], H.size (0), H.size (1), H.size (2))); + io_handler.reset (new MR::ImageIO::Mosaic (H, frame.dim[0], frame.dim[1], H.size (0), H.size (1), H.size (2))); } else io_handler.reset (new MR::ImageIO::Default (H)); diff --git a/core/file/dicom/quick_scan.cpp b/core/file/dicom/quick_scan.cpp index 310c62a89b..e69a02e514 100644 --- a/core/file/dicom/quick_scan.cpp +++ b/core/file/dicom/quick_scan.cpp @@ -16,6 +16,7 @@ #include "file/dicom/definitions.h" #include "file/dicom/element.h" #include "file/dicom/csa_entry.h" +#include "debug.h" namespace MR { namespace File { @@ -33,6 +34,7 @@ namespace MR { study_ID.clear(); study_time.clear(); series.clear(); + image_type.clear(); series_date.clear(); series_time.clear(); sequence.clear(); @@ -42,9 +44,12 @@ namespace MR { Element item; try { item.set (filename, force_read); + std::string current_image_type; + bool in_frames = false; while (item.read()) { - if (item.is (0x0008U, 0x0020U)) study_date = item.get_string()[0]; + if (item.is (0x0008U, 0x0008U)) current_image_type = join (item.get_string(), " "); + else if (item.is (0x0008U, 0x0020U)) study_date = item.get_string()[0]; else if (item.is (0x0008U, 0x0021U)) series_date = item.get_string()[0]; else if (item.is (0x0008U, 0x0030U)) study_time = item.get_string()[0]; else if (item.is (0x0008U, 0x0031U)) series_time = item.get_string()[0]; @@ -61,13 +66,12 @@ namespace MR { else if (item.is (0x0028U, 0x0011U)) dim[0] = item.get_uint()[0]; else if (item.is (0x0028U, 0x0100U)) bits_alloc = item.get_uint()[0]; else if (item.is (0x7FE0U, 0x0010U)) data = item.offset (item.data); - else if (item.is (0x0008U, 0x0008U)) { - // exclude Siemens MPR info image: - // TODO: could handle this by splitting on basis on this entry - vector V (item.get_string()); - for (size_t n = 0; n < V.size(); n++) { - if (uppercase (V[n]) == "CSAPARALLEL") - return true; + else if (item.is (0xFFFEU, 0xE000U)) { + if (item.parents.size() && + item.parents.back().group == 0x5200U && + item.parents.back().element == 0x9230U) { // multi-frame item + if (in_frames) ++image_type[current_image_type]; + else in_frames = true; } } @@ -83,6 +87,8 @@ namespace MR { } } + ++image_type[current_image_type]; + transfer_syntax_supported = item.transfer_syntax_supported; } @@ -114,8 +120,10 @@ namespace MR { << file.series_number << "] " << ( file.series.size() ? file.series : "[unspecified]" ) << " - " << format_date (file.series_date) << " " - << format_time (file.series_time) << "\n sequence: " - << ( file.sequence.size() ? file.sequence : "[unspecified]" ) << "\n"; + << format_time (file.series_time) << "\n"; + for (const auto& type : file.image_type) + stream << " image type: " << type.first << " [ " << type.second << " frames ]\n"; + stream << " sequence: " << ( file.sequence.size() ? file.sequence : "[unspecified]" ) << "\n"; return stream; } diff --git a/core/file/dicom/quick_scan.h b/core/file/dicom/quick_scan.h index afbed9df92..1ee8eb9e29 100644 --- a/core/file/dicom/quick_scan.h +++ b/core/file/dicom/quick_scan.h @@ -15,6 +15,7 @@ #ifndef __file_dicom_quick_scan_h__ #define __file_dicom_quick_scan_h__ +#include #include "mrtrix.h" namespace MR { @@ -30,6 +31,7 @@ namespace MR { std::string patient, patient_ID, patient_DOB; std::string study, study_ID, study_date, study_time; std::string series, series_date, series_time, sequence; + std::map image_type; size_t series_number, bits_alloc, dim[2], data; bool transfer_syntax_supported; }; diff --git a/core/file/dicom/select_cmdline.cpp b/core/file/dicom/select_cmdline.cpp index 0c5b737c32..01a4f536e5 100644 --- a/core/file/dicom/select_cmdline.cpp +++ b/core/file/dicom/select_cmdline.cpp @@ -115,14 +115,14 @@ namespace MR { while (series.size() == 0) { fprintf (stderr, "Select series ('q' to abort):\n"); for (size_t i = 0; i < study.size(); i++) { - fprintf (stderr, " %2zu - %4zu %s images %8s %s (%s) [%zu]\n", + fprintf (stderr, " %2zu - %4zu %s images %8s %s (%s) [%zu] %s\n", i, study[i]->size(), ( study[i]->modality.size() ? study[i]->modality.c_str() : "" ), format_time (study[i]->time).c_str(), ( study[i]->name.size() ? study[i]->name.c_str() : "unnamed" ), ( (*study[i])[0]->sequence_name.size() ? (*study[i])[0]->sequence_name.c_str() : "?" ), - study[i]->number); + study[i]->number, study[i]->image_type.c_str()); } std::cerr << "? "; std::cin >> buf; diff --git a/core/file/dicom/series.cpp b/core/file/dicom/series.cpp index aeed76a878..dc860d7ce8 100644 --- a/core/file/dicom/series.cpp +++ b/core/file/dicom/series.cpp @@ -80,13 +80,14 @@ namespace MR { std::ostream& operator<< (std::ostream& stream, const Series& item) { - stream << MR::printf (" %4u - %4u %4s images %10s %8s %s\n", + stream << MR::printf (" %4u - %4u %4s images %10s %8s %s [ %s ]\n", item.number, item.size(), ( item.modality.size() ? item.modality.c_str() : "(?)" ), format_date(item.date).c_str(), format_time(item.time).c_str(), - item.name.c_str() ); + item.name.c_str(), + item.image_type.c_str() ); for (size_t n = 0; n < item.size(); n++) stream << *item[n]; diff --git a/core/file/dicom/series.h b/core/file/dicom/series.h index 90914b45a1..c5dca4bcdc 100644 --- a/core/file/dicom/series.h +++ b/core/file/dicom/series.h @@ -28,15 +28,15 @@ namespace MR { class Series : public vector> { NOMEMALIGN public: - Series (Study* parent, const std::string& series_name, size_t series_number, + Series (Study* parent, const std::string& series_name, size_t series_number, const std::string& image_type, const std::string& series_modality = "", const std::string& series_date = "", const std::string& series_time = "") : - study (parent), name (series_name), modality (series_modality), + study (parent), name (series_name), image_type (image_type), modality (series_modality), date (series_date), time (series_time) { number = series_number; } Study* study; - std::string name; + std::string name, image_type; size_t number; std::string modality; std::string date; diff --git a/core/file/dicom/study.cpp b/core/file/dicom/study.cpp index 8b674e0aac..c236026a0f 100644 --- a/core/file/dicom/study.cpp +++ b/core/file/dicom/study.cpp @@ -24,13 +24,15 @@ namespace MR { bool series_time_mismatch_warning_issued = false; } - std::shared_ptr Study::find (const std::string& series_name, size_t series_number, + std::shared_ptr Study::find (const std::string& series_name, size_t series_number, const std::string& image_type, const std::string& series_modality, const std::string& series_date, const std::string& series_time) { for (size_t n = 0; n < size(); n++) { bool match = true; if (series_name == (*this)[n]->name) { if (series_number == (*this)[n]->number) { + if (image_type != (*this)[n]->image_type) + match = false; if (series_modality.size() && (*this)[n]->modality.size()) if (series_modality != (*this)[n]->modality) match = false; @@ -59,7 +61,7 @@ namespace MR { } } - push_back (std::shared_ptr (new Series (this, series_name, series_number, series_modality, series_date, series_time))); + push_back (std::shared_ptr (new Series (this, series_name, series_number, image_type, series_modality, series_date, series_time))); return back(); } diff --git a/core/file/dicom/study.h b/core/file/dicom/study.h index 96de363608..f91241c90d 100644 --- a/core/file/dicom/study.h +++ b/core/file/dicom/study.h @@ -35,7 +35,7 @@ namespace MR { Patient* patient; std::string name, ID, date, time; - std::shared_ptr find (const std::string& series_name, size_t series_number, + std::shared_ptr find (const std::string& series_name, size_t series_number, const std::string& image_type, const std::string& series_modality = "", const std::string& series_date = "", const std::string& series_time = ""); }; diff --git a/core/file/dicom/tree.cpp b/core/file/dicom/tree.cpp index 5c44e43802..4f7f08a2a0 100644 --- a/core/file/dicom/tree.cpp +++ b/core/file/dicom/tree.cpp @@ -96,14 +96,17 @@ namespace MR { std::shared_ptr patient = find (reader.patient, reader.patient_ID, reader.patient_DOB); std::shared_ptr study = patient->find (reader.study, reader.study_ID, reader.study_date, reader.study_time); - std::shared_ptr series = study->find (reader.series, reader.series_number, reader.modality, reader.series_date, reader.series_time); - - std::shared_ptr image (new Image); - image->filename = filename; - image->series = series.get(); - image->sequence_name = reader.sequence; - image->transfer_syntax_supported = reader.transfer_syntax_supported; - series->push_back (image); + for (const auto& image_type : reader.image_type) { + std::shared_ptr series = study->find (reader.series, reader.series_number, image_type.first, reader.modality, reader.series_date, reader.series_time); + + std::shared_ptr image (new Image); + image->filename = filename; + image->series = series.get(); + image->sequence_name = reader.sequence; + image->image_type = image_type.first; + image->transfer_syntax_supported = reader.transfer_syntax_supported; + series->push_back (image); + } } diff --git a/core/file/json_utils.cpp b/core/file/json_utils.cpp index 50053a789d..cd9baecd1a 100644 --- a/core/file/json_utils.cpp +++ b/core/file/json_utils.cpp @@ -13,7 +13,6 @@ #include -#include #include "file/json_utils.h" #include "file/nifti_utils.h" @@ -22,6 +21,7 @@ #include "header.h" #include "mrtrix.h" #include "phase_encoding.h" +#include "types.h" #include "file/ofstream.h" namespace MR diff --git a/core/file/nifti_utils.h b/core/file/nifti_utils.h index ba232f38f9..b5b2d93985 100644 --- a/core/file/nifti_utils.h +++ b/core/file/nifti_utils.h @@ -15,8 +15,6 @@ #ifndef __file_nifti_utils_h__ #define __file_nifti_utils_h__ -#include - #include "types.h" namespace MR diff --git a/core/file/tiff.cpp b/core/file/tiff.cpp index 3dead34950..e0774271cb 100644 --- a/core/file/tiff.cpp +++ b/core/file/tiff.cpp @@ -1,18 +1,17 @@ -/* - * Copyright (c) 2008-2016 the MRtrix3 contributors - * +/* Copyright (c) 2008-2017 the MRtrix3 contributors. + * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/ - * + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + * * MRtrix is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * - * For more details, see www.mrtrix.org - * + * but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * For more details, see http://www.mrtrix.org/. */ + #ifdef MRTRIX_TIFF_SUPPORT #include "file/tiff.h" diff --git a/core/file/tiff.h b/core/file/tiff.h index 7e25c9f309..e95bdca821 100644 --- a/core/file/tiff.h +++ b/core/file/tiff.h @@ -1,18 +1,17 @@ -/* - * Copyright (c) 2008-2016 the MRtrix3 contributors - * +/* Copyright (c) 2008-2017 the MRtrix3 contributors. + * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/ - * + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + * * MRtrix is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * - * For more details, see www.mrtrix.org - * + * but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * For more details, see http://www.mrtrix.org/. */ + #ifndef __file_tiff_h__ #define __file_tiff_h__ diff --git a/core/filter/dwi_brain_mask.h b/core/filter/dwi_brain_mask.h index 21a5a75ebd..8c7ecd392b 100644 --- a/core/filter/dwi_brain_mask.h +++ b/core/filter/dwi_brain_mask.h @@ -17,6 +17,8 @@ #include "memory.h" #include "image.h" +#include "phase_encoding.h" +#include "progressbar.h" #include "filter/base.h" #include "filter/connected_components.h" #include "filter/median.h" @@ -25,7 +27,7 @@ #include "algo/copy.h" #include "algo/loop.h" #include "dwi/gradient.h" -#include "progressbar.h" + namespace MR { @@ -70,7 +72,6 @@ namespace MR Header header (input); header.ndim() = 3; - DWI::stash_DW_scheme (header, grad); // Generate a 'master' scratch buffer mask, to which all shells will contribute auto mask_image = Image::scratch (header, "DWI mask"); diff --git a/core/filter/resize.h b/core/filter/resize.h index 7ba663c20b..e656671f01 100644 --- a/core/filter/resize.h +++ b/core/filter/resize.h @@ -131,7 +131,8 @@ namespace MR { switch (interp_type) { case 0: - reslice (input, output); + // Prevent use of oversampling when using nearest-neighbour interpolation + reslice (input, output, Adapter::NoTransform, { 1, 1, 1 }); break; case 1: reslice (input, output); diff --git a/core/filter/warp.h b/core/filter/warp.h index 0a9f5e651d..cc3b47f5db 100644 --- a/core/filter/warp.h +++ b/core/filter/warp.h @@ -63,7 +63,8 @@ namespace MR ImageTypeSource& source, ImageTypeDestination& destination, WarpType& warp, - const typename ImageTypeDestination::value_type value_when_out_of_bounds = Interpolator::default_out_of_bounds_value()) + const typename ImageTypeDestination::value_type value_when_out_of_bounds = Interpolator::default_out_of_bounds_value(), + vector oversample = Adapter::AutoOverSample) { // reslice warp onto destination grid @@ -76,7 +77,8 @@ namespace MR header.size(3) = 3; Stride::set (header, Stride::contiguous_along_axis (3)); auto warp_resliced = Image::scratch (header); - reslice (warp, warp_resliced); + transform_type identity_transform; + reslice (warp, warp_resliced, identity_transform, oversample); Adapter::Warp > interp (source, warp_resliced, value_when_out_of_bounds); if (destination.ndim() == 4) diff --git a/core/formats/mrtrix_utils.h b/core/formats/mrtrix_utils.h index 0d97ec774d..22c1ead8ea 100644 --- a/core/formats/mrtrix_utils.h +++ b/core/formats/mrtrix_utils.h @@ -15,9 +15,9 @@ #ifndef __formats_mrtrix_utils_h__ #define __formats_mrtrix_utils_h__ -#include - #include "header.h" +#include "types.h" + #include "file/gz.h" #include "file/key_value.h" #include "file/ofstream.h" diff --git a/core/formats/tiff.cpp b/core/formats/tiff.cpp index 6ad3240735..842bb23eb8 100644 --- a/core/formats/tiff.cpp +++ b/core/formats/tiff.cpp @@ -1,17 +1,17 @@ -/* - * Copyright (c) 2008-2016 the MRtrix3 contributors - * +/* Copyright (c) 2008-2017 the MRtrix3 contributors. + * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/ - * + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + * * MRtrix is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * - * For more details, see www.mrtrix.org - * + * but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * For more details, see http://www.mrtrix.org/. */ + + #ifdef MRTRIX_TIFF_SUPPORT #include "file/config.h" diff --git a/core/header.cpp b/core/header.cpp index 2d887a2301..c1efbdcdc1 100644 --- a/core/header.cpp +++ b/core/header.cpp @@ -12,6 +12,7 @@ */ +#include "mrtrix.h" #include "header.h" #include "phase_encoding.h" #include "stride.h" @@ -66,6 +67,20 @@ namespace MR } + namespace { + + std::string short_description (const Header& H) + { + vector dims; + for (size_t n = 0; n < H.ndim(); ++n) + dims.push_back (str(H.size(n))); + vector vox; + for (size_t n = 0; n < H.ndim(); ++n) + vox.push_back (str(H.spacing(n))); + + return " with dimensions " + join (dims, "x") + ", voxel spacing " + join (vox, "x") + ", datatype " + H.datatype().specifier(); + } + } @@ -128,12 +143,13 @@ namespace MR H.sanitise(); if (!do_not_realign_transform) H.realign_transform(); - } catch (Exception& E) { throw Exception (E, "error opening image \"" + image_name + "\""); } + INFO ("image \"" + H.name() + "\" opened" + short_description (H)); + return H; } @@ -291,6 +307,8 @@ namespace MR WARN (std::string ("requested datatype (") + previous_datatype.specifier() + ") not supported - substituting with " + H.datatype().specifier()); } + INFO ("image \"" + H.name() + "\" created" + short_description (H)); + return H; } diff --git a/core/image_diff.h b/core/image_diff.h index 0cb62ec979..afc703ea4c 100644 --- a/core/image_diff.h +++ b/core/image_diff.h @@ -28,7 +28,7 @@ namespace MR check_dimensions (in1, in2); for (size_t i = 0; i < in1.ndim(); ++i) { if (std::isfinite (in1.spacing(i))) - if (in1.spacing(i) != in2.spacing(i)) + if (std::abs ((in1.spacing(i) - in2.spacing(i)) / (in1.spacing(i) + in2.spacing(i))) > 1e-4) throw Exception ("images \"" + in1.name() + "\" and \"" + in2.name() + "\" do not have matching voxel spacings " + str(in1.spacing(i)) + " vs " + str(in2.spacing(i))); } diff --git a/core/image_io/base.h b/core/image_io/base.h index 033919a7fb..a7d66bb30a 100644 --- a/core/image_io/base.h +++ b/core/image_io/base.h @@ -15,13 +15,13 @@ #ifndef __image_io_base_h__ #define __image_io_base_h__ -#include +#include #include #include -#include #include "memory.h" #include "mrtrix.h" +#include "types.h" #include "file/entry.h" #define MAX_FILES_PER_IMAGE 256U diff --git a/core/image_io/scratch.cpp b/core/image_io/scratch.cpp index 4347f7d8a8..04a6933f79 100644 --- a/core/image_io/scratch.cpp +++ b/core/image_io/scratch.cpp @@ -13,7 +13,6 @@ #include -#include #include "image_io/scratch.h" #include "header.h" diff --git a/core/image_io/tiff.cpp b/core/image_io/tiff.cpp index 1ae9b39ad3..8fbd6808a6 100644 --- a/core/image_io/tiff.cpp +++ b/core/image_io/tiff.cpp @@ -1,18 +1,17 @@ -/* - * Copyright (c) 2008-2016 the MRtrix3 contributors - * +/* Copyright (c) 2008-2017 the MRtrix3 contributors. + * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/ - * + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + * * MRtrix is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * - * For more details, see www.mrtrix.org - * + * but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * For more details, see http://www.mrtrix.org/. */ + #ifdef MRTRIX_TIFF_SUPPORT #include "header.h" diff --git a/core/image_io/tiff.h b/core/image_io/tiff.h index d455f07160..ae68aa771b 100644 --- a/core/image_io/tiff.h +++ b/core/image_io/tiff.h @@ -1,18 +1,17 @@ -/* - * Copyright (c) 2008-2016 the MRtrix3 contributors - * +/* Copyright (c) 2008-2017 the MRtrix3 contributors. + * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/ - * + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + * * MRtrix is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * - * For more details, see www.mrtrix.org - * + * but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * For more details, see http://www.mrtrix.org/. */ + #ifndef __image_handler_tiff_h__ #define __image_handler_tiff_h__ diff --git a/core/math/Sn_scale_estimator.h b/core/math/Sn_scale_estimator.h index 95a2160008..d358014a82 100644 --- a/core/math/Sn_scale_estimator.h +++ b/core/math/Sn_scale_estimator.h @@ -15,8 +15,6 @@ #ifndef __math_Sn_scale_estimator_h__ #define __math_Sn_scale_estimator_h__ -#include - #include "types.h" #include "math/median.h" diff --git a/core/math/median.h b/core/math/median.h index ad255e4d04..831e5ccbc0 100644 --- a/core/math/median.h +++ b/core/math/median.h @@ -15,10 +15,8 @@ #ifndef __math_median_h__ #define __math_median_h__ -#include -#include - #include +#include #include "types.h" diff --git a/core/math/stats/permutation.h b/core/math/stats/permutation.h index bc4019e9b7..d9d6c9d8d9 100644 --- a/core/math/stats/permutation.h +++ b/core/math/stats/permutation.h @@ -15,8 +15,7 @@ #ifndef __math_stats_permutation_h__ #define __math_stats_permutation_h__ -#include - +#include "types.h" #include "math/stats/typedefs.h" namespace MR diff --git a/core/mrtrix.h b/core/mrtrix.h index 56094d687a..70f437e307 100644 --- a/core/mrtrix.h +++ b/core/mrtrix.h @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include diff --git a/core/phase_encoding.cpp b/core/phase_encoding.cpp index 8a0dd7c5b8..baa03159c0 100644 --- a/core/phase_encoding.cpp +++ b/core/phase_encoding.cpp @@ -81,6 +81,16 @@ namespace MR + void clear_scheme (Header& header) + { + auto erase = [&] (const std::string& s) { auto it = header.keyval().find (s); if (it != header.keyval().end()) header.keyval().erase (it); }; + erase ("pe_scheme"); + erase ("PhaseEncodingDirection"); + erase ("TotalReadoutTime"); + } + + + Eigen::MatrixXd parse_scheme (const Header& header) { Eigen::MatrixXd PE; diff --git a/core/phase_encoding.h b/core/phase_encoding.h index 3ed3b09a10..0c27b93389 100644 --- a/core/phase_encoding.h +++ b/core/phase_encoding.h @@ -88,10 +88,11 @@ namespace MR template void set_scheme (Header& header, const MatrixType& PE) { + auto erase = [&] (const std::string& s) { auto it = header.keyval().find (s); if (it != header.keyval().end()) header.keyval().erase (it); }; if (!PE.rows()) { - header.keyval().erase ("pe_scheme"); - header.keyval().erase ("PhaseEncodingDirection"); - header.keyval().erase ("TotalReadoutTime"); + erase ("pe_scheme"); + erase ("PhaseEncodingDirection"); + erase ("TotalReadoutTime"); return; } PhaseEncoding::check (header, PE); @@ -112,21 +113,29 @@ namespace MR } if (variation) { header.keyval()["pe_scheme"] = pe_scheme; - header.keyval().erase ("PhaseEncodingDirection"); - header.keyval().erase ("TotalReadoutTime"); + erase ("PhaseEncodingDirection"); + erase ("TotalReadoutTime"); } else { - header.keyval().erase ("pe_scheme"); + erase ("pe_scheme"); const Eigen::Vector3 dir { PE(0, 0), PE(0, 1), PE(0, 2) }; header.keyval()["PhaseEncodingDirection"] = dir2id (dir); if (PE.cols() >= 4) header.keyval()["TotalReadoutTime"] = str(PE(0, 3), 3); else - header.keyval().erase ("TotalReadoutTime"); + erase ("TotalReadoutTime"); } } + //! clear the phase encoding matrix from a header + /*! this will delete any trace of phase encoding information + * from the Header::keyval() structure of \a header. + */ + void clear_scheme (Header& header); + + + //! parse the phase encoding matrix from a header /*! extract the phase encoding matrix stored in the \a header if one * is present. This is expected to be stored in the Header::keyval() diff --git a/docs/concepts/global_intensity_normalisation.rst b/docs/concepts/global_intensity_normalisation.rst index 18b0a8f516..e2d4ce1931 100644 --- a/docs/concepts/global_intensity_normalisation.rst +++ b/docs/concepts/global_intensity_normalisation.rst @@ -4,14 +4,13 @@ Global intensity normalisation ============================== -Most DWI models derive quantitative measures by using the ratio of the DW signal to the b=0 signal within each voxel. This voxel-wise b=0 normalisation implicitly removes intensity variations due to T2-weighting and RF inhomogeneity. However, unless all compartments within white matter (e.g. intra- and extra-axonal space, myelin, cerebral spinal fluid (CSF) and grey matter partial volumes) are modelled accurately (i.e. with appropriate assumptions/modelling of both the compartment diffusion and T2), the proportion of one compartment in a voxel may influence another. For example, if CSF partial volume at the border of white matter and the ventricles is not taken into account, then a voxel-wise normalisation performed by dividing by the b=0 (which has a long T2 and appears brighter in CSF than white matter in the T2-weighted b=0 image), will artificially reduce the DW signal from the white matter intra-axonal (restricted) compartment, ultimately changing the derived quantiative measures. Multi-compartment diffusion MRI models aspire to model many/all of the compartments as accurately as possible. However, in practice current models are limited by restrictions/assumptions placed on the different compartments, and all require multiple b-value acquisitions and therefore longer scan times. +Most DWI models derive quantitative measures by using the ratio of the DW signal to the b=0 signal within each voxel. This voxel-wise b=0 normalisation implicitly removes intensity variations due to T2-weighting and RF inhomogeneity. However, unless all compartments within white matter (e.g. intra- and extra-axonal space, myelin, cerebral spinal fluid (CSF) and grey matter partial volumes) are modelled accurately (i.e. with appropriate assumptions/modelling of both the compartment diffusion and T2), the proportion of one compartment in a voxel may influence another. For example, if CSF partial volume at the border of white matter and the ventricles is not taken into account, then a voxel-wise normalisation performed by dividing by the b=0 (which has a long T2 and appears brighter in CSF than white matter in the T2-weighted b=0 image), will artificially reduce the DW signal from the white matter intra-axonal (restricted) compartment, ultimately changing the derived quantitative measures. -In our previous work investigating differences in `Apparent Fibre Density (AFD) `_ we opt to perform a *global intensity normalisation*. This avoids the aforementioned issues, but also comes with its own set of problems and assumptions. Aside from the problem of how to select the region to perform global intensity normalisation (that is unbiased with respect to the groups in the analysis), the data must also be first bias field corrected to eliminate low frequency intensity inhomogeneities across the image. +In our previous work investigating differences in `Apparent Fibre Density (AFD) `_ we opt to perform a *global intensity normalisation* between subjects. This avoids the aforementioned issues, but also comes with its own set of challenges and assumptions inherent to specific strategies to deal with this. Aside from the problem of how to select the region to perform global intensity normalisation (that is unbiased with respect to the groups in the analysis), the data must also be bias field corrected first, to eliminate low frequency intensity inhomogeneities across the image. - -A sound approach to global intensity normalisation would be to normalise based on the median CSF b=0 intensity across all subjects (on the assumption that the CSF T2 is unlikely to be affected by pathology). However, in practice it is difficult to obtain a robust partial-volume-free estimate of the CSF intensity due to the typical low resolution of DW images. For participants less than 50 years old (with reasonably small ventricles), it can be difficult to identify pure CSF voxels at 2-2.5mm resolutions. We therefore recommend performing a global intensity normalisation using the median white matter b=0 intensity. While the white matter b=0 intensity may be influenced by pathology-induced changes in T2, our assumption is that such changes will be local to the pathology and therefore have little influence on the median b=0 value. +In theory, a sound approach to global intensity normalisation would be to normalise based on the median CSF b=0 intensity across all subjects (on the assumption that the CSF T2 is unlikely to be affected by pathology). However, in practice it is surprisingly difficult to obtain a robust partial-volume-free estimate of the CSF intensity due to the typical low resolution of DW images. For participants less than 50 years old, due to reasonably small ventricles, it can be difficult to identify pure CSF voxels at 2-2.5mm resolutions. We therefore recommend performing a global intensity normalisation using the *median* white matter b=0 intensity. While the white matter b=0 intensity may be influenced by pathology-induced changes in T2, our assumption is that such changes will be local to the pathology and therefore have little influence on the median b=0 value. We have included the :ref:`dwiintensitynorm` script in MRtrix to perform an automatic global normalisation using the median white matter b=0 value. The script input requires two folders: a folder containing all DW images in the study (in .mif format) and a folder containing the corresponding whole brain mask images (with the same filename prefix). The script runs by first computing diffusion tensor Fractional Anisotropy (FA) maps, registering these to a study-specific template, then thresholding the template FA map to obtain an approximate white matter mask. The mask is then transformed back into the space of each subject image and used in the :ref:`dwinormalise` command to normalise the input DW images to have the same b=0 white matter median value. All intensity normalised data will be output in a single folder. As previously mentioned all DWI data must be bias field corrected before using :ref:`dwiintensitynorm`, for example using :code:`dwibiascorrect`. -As an alternative to the :code:`dwiintensitynorm` script, we have recently provided a new command called :ref:`mtbin`, which simultaneously performs bias field correction and global intensity normalisation on multi-tissue CSD compartments. The benefit of the :ref:`mtbin` command is that normalisation can be performed independently on each subject, and therefore does not require a computationally expensive registration step to a group template. However, to perform multi-tissue CSD currently requires DWI data with multiple b-values, and therefore the :ref:`mtbin` method is currently not suitable for single-shell DWI. +As an alternative to the :code:`dwiintensitynorm` script, we have recently provided a new command called :ref:`mtnormalise`, which performs multi-tissue informed intensity normalisation in the log-domain. The benefit of the :ref:`mtnormalise` command is that normalisation can be performed independently on each subject, and therefore does not require a computationally expensive registration step to a group template. However, to perform multi-tissue CSD with 3 tissue types (WM, GM, CSF) currently requires DWI data with multiple b-values (this will change at some stage, when the implementation of single-shell 3-tissue CSD becomes available). diff --git a/docs/fixel_based_analysis/common_fba_steps/population_template.rst b/docs/fixel_based_analysis/common_fba_steps/population_template.rst index 9f5383db76..5144c0ed99 100644 --- a/docs/fixel_based_analysis/common_fba_steps/population_template.rst +++ b/docs/fixel_based_analysis/common_fba_steps/population_template.rst @@ -1,4 +1,4 @@ -Population template creation is the most time consuming step in a fixel-based analysis. If you have a large number of subjects in your study, we recommend building the template from a subset of 20-40 individuals. Subjects should be chosen to ensure the generated template is representative of your population (i.e. equal number of patients and controls). To build a template, place all FOD images in a single folder. We also recommend placing a set of corresponding mask images (with the same prefix as the FOD images) in another folder. Using masks can speed up registration significantly:: +Population template creation is a very time consuming step in a fixel-based analysis. If you have a very large number of subjects in your study, we recommend building the template from a limited subset of 30-40 individuals. Subjects should be chosen to ensure the generated template is representative of your population (e.g. similar number of patients and controls). To build a template, place all FOD images in a single folder. We also highly recommend placing a set of corresponding mask images (with the same prefix as the FOD images) in another folder. Using masks can speed up registration significantly as well as result in a much more accurate template in specific scenarios:: mkdir -p ../template/fod_input mkdir ../template/mask_input diff --git a/docs/fixel_based_analysis/common_fba_steps/population_template2.rst b/docs/fixel_based_analysis/common_fba_steps/population_template2.rst index 3a86eea0cb..d56206ed51 100644 --- a/docs/fixel_based_analysis/common_fba_steps/population_template2.rst +++ b/docs/fixel_based_analysis/common_fba_steps/population_template2.rst @@ -2,7 +2,7 @@ Run the template building script as follows: .. code-block:: console - $ population_template ../template/fod_input -mask_dir ../template/mask_input ../template/fod_template.mif + $ population_template ../template/fod_input -mask_dir ../template/mask_input ../template/wmfod_template.mif -voxel_size 1.25 **If you are building a template from your entire study population**, run the population_template script use the :code:`-warp_dir warps` option to output a @@ -13,6 +13,6 @@ forward and reverse warps (see :ref:`mrregister` for more info). After population template creation is complete, to convert this warp format to a more conventional 4D deformation field format ready for the subsequent steps, run -.. code-block:: console +.. code-block:: console - $ foreach ../template/warps/* : warpconvert -type warpfull2deformation -template ../template/fod_template.mif IN PRE/subject2template_warp.mif + $ foreach ../template/warps/* : warpconvert -type warpfull2deformation -template ../template/wmfod_template.mif IN PRE/subject2template_warp.mif diff --git a/docs/fixel_based_analysis/common_fba_steps/statistics.rst b/docs/fixel_based_analysis/common_fba_steps/statistics.rst index 4a221a2d21..19d65c9ca9 100644 --- a/docs/fixel_based_analysis/common_fba_steps/statistics.rst +++ b/docs/fixel_based_analysis/common_fba_steps/statistics.rst @@ -4,6 +4,6 @@ Statistical analysis is performed using `connectivity-based fixel enhancement `_ exploits connectivity information derived from probabilistic fibre tractography. To generate a whole-brain tractogram from the FOD template. Note the remaining steps from here on are executed from the template directory:: cd ../template - tckgen -angle 22.5 -maxlen 250 -minlen 10 -power 1.0 fod_template.mif -seed_image voxel_mask.mif -mask voxel_mask.mif -select 20000000 tracks_20_million.tck + tckgen -angle 22.5 -maxlen 250 -minlen 10 -power 1.0 wmfod_template.mif -seed_image voxel_mask.mif -mask voxel_mask.mif -select 20000000 tracks_20_million.tck diff --git a/docs/fixel_based_analysis/mt_fibre_density_cross-section.rst b/docs/fixel_based_analysis/mt_fibre_density_cross-section.rst index dbcae71d1a..c96ca88cb1 100644 --- a/docs/fixel_based_analysis/mt_fibre_density_cross-section.rst +++ b/docs/fixel_based_analysis/mt_fibre_density_cross-section.rst @@ -10,11 +10,11 @@ This tutorial explains how to perform `fixel-based analysis of fibre density and All steps in this tutorial have written as if the commands are being **run on a cohort of images**, and make extensive use of the :ref:`foreach script to simplify batch processing `. This tutorial also assumes that the imaging dataset is organised with one directory identifying the subject, and all files within identifying the image type. For example:: study/subjects/001_patient/dwi.mif - study/subjects/001_patient/fod.mif + study/subjects/001_patient/wmfod.mif study/subjects/002_control/dwi.mif - study/subjects/002_control/fod.mif + study/subjects/002_control/wmfod.mif -.. NOTE:: All commands in this tutorial are run **from the subjects path** up until step 18, where we change directory to the template path +.. NOTE:: All commands in this tutorial are run **from the subjects path** up until step 19, where we change directory to the template path For all MRtrix scripts and commands, additional information on the command usage and available command-line options can be found by invoking the command with the :code:`-help` option. Please post any questions or issues on the `MRtrix community forum `_. @@ -32,151 +32,157 @@ Pre-processsing steps .. include:: common_fba_steps/dwipreproc.rst +3. Bias field correction +^^^^^^^^^^^^^^^^^^^^^^^^ +Bias field correction is important to deal with spatial intensity inhomogeneities. Even though this FBA pipeline will account for these as well (in the later :ref:`mtnormalise` step, which is furthermore crucial to correct for global intensity differences between subjects), performing bias field correction at this stage will allow for more accurate estimation of the tissue response functions as well as the individual subject brain masks. + +This can be done in a single step using the :ref:`dwibiascorrect` script in MRtrix. The script uses bias field correction algorthims available in `ANTS `_ or `FSL `_. In our experience the `N4 algorithm `_ in ANTS gives far superior results. To install N4, install the `ANTS `_ package, then perform bias field correction on DW images using:: + + foreach * : dwibiascorrect -ants IN/dwi_denoised_preproc.mif IN/dwi_denoised_preproc_bias.mif + Fixel-based analysis steps --------------------------- -3. Computing group average tissue response functions +4. Computing group average tissue response functions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ As described `here `__, using the same response function when estimating FOD images for all subjects enables differences in the intra-axonal volume (and therefore DW signal) across subjects to be detected as differences in the FOD amplitude (the AFD). To ensure the response function is representative of your study population, a group average response function can be computed by first estimating a response function per subject, then averaging with the script:: - foreach * : dwi2response dhollander IN/dwi_denoised_preproc.mif IN/response_wm.txt IN/response_gm.txt IN/response_csf.txt + foreach * : dwi2response dhollander IN/dwi_denoised_preproc_bias.mif IN/response_wm.txt IN/response_gm.txt IN/response_csf.txt average_response */response_wm.txt ../group_average_response_wm.txt average_response */response_gm.txt ../group_average_response_gm.txt average_response */response_csf.txt ../group_average_response_csf.txt -4. Upsampling DW images +5. Upsampling DW images ^^^^^^^^^^^^^^^^^^^^^^^ - Upsampling DWI data before computing FODs can `increase anatomical contrast `_ and improve downstream spatial normalisation and statistics. We recommend upsampling to a voxel size of 1.25mm (for human brains). If you have data that has smaller voxels than 1.25mm, then we recommend you can skip this step:: - foreach * : mrresize IN/dwi_denoised_preproc.mif -vox 1.25 IN/dwi_denoised_preproc_upsampled.mif + foreach * : mrresize IN/dwi_denoised_preproc_bias.mif -vox 1.25 IN/dwi_denoised_preproc_bias_upsampled.mif -5. Compute upsampled brain mask images +6. Compute upsampled brain mask images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Compute a whole brain mask from the upsampled DW images:: - foreach * : dwi2mask IN/dwi_denoised_preproc_upsampled.mif IN/dwi_mask_upsampled.mif - -Depending on your data, you may find that computing masks on native resolution DWIs gives superiour masks (with less holes), then upsampling. This can be performed using:: - - foreach * : dwi2mask IN/dwi_denoised_preproc.mif - \| mrresize - -scale 2.0 -interp nearest IN/dwi_mask_upsampled.mif + foreach * : dwi2mask IN/dwi_denoised_preproc_bias_upsampled.mif IN/dwi_mask_upsampled.mif - -6. Fibre Orientation Distribution estimation +7. Fibre Orientation Distribution estimation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When performing analysis of AFD, Constrained Spherical Deconvolution (CSD) should be performed using the group average response functions computed at step 3:: - foreach * : dwi2fod msmt_csd IN/dwi_denoised_preproc_upsampled.mif ../group_average_response_wm.txt IN/fod.mif ../group_average_response_gm.txt IN/gm.mif ../group_average_response_csf.txt IN/csf.mif -mask IN/dwi_mask_upsampled.mif + foreach * : dwi2fod msmt_csd IN/dwi_denoised_preproc_bias_upsampled.mif ../group_average_response_wm.txt IN/wmfod.mif ../group_average_response_gm.txt IN/gm.mif ../group_average_response_csf.txt IN/csf.mif -mask IN/dwi_mask_upsampled.mif + + +8. Intensity normalisation +^^^^^^^^^^^^^^^^^^^^^^^^^^ +This step performs :ref:`global intensity normalisation ` in the log-domain by scaling all tissue types with a spatially smoothly varying normalisation field:: + foreach * : mtnormalise IN/wmfod.mif IN/wmfod_norm.mif IN/gm.mif IN/gm_norm.mif IN/csf.mif IN/csf_norm.mif -mask IN/dwi_mask_upsampled.mif -7. Perform simultaneous bias field correction and intensity normalisation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This step performs :ref:`global intensity normalisation ` by scaling all tissue types based on a single scale factor. A single multiplicative bias field is also estimated and applied to correct the output:: +If CSD was performed with the same single set of (average) WM, GM and CSF response functions for all subjects, then the resulting output of :ref:`mtnormalise` should make the amplitudes comparable between those subjects as well. - foreach * : mtbin IN/fod.mif IN/fod_bias_norm.mif IN/gm.mif IN/gm_bias_norm.mif IN/csf.mif IN/csf_bias_norm.mif -mask IN/dwi_mask_upsampled.mif +Note that this step is crucial in the FBA pipeline, even if bias field correction was applied during the preprocessing stage, as the latter does not correct for global intensity differences between subjects. -.. WARNING:: We strongly recommend you that you check the scale factors applied during intensity normalisation are not influenced by the variable of interest in your study. For example if one group contains global changes in white matter T2 then this may directly influence the intensity normalisation and therefore bias downstream AFD analysis. To check this we recommend you perform an equivalence test to ensure mean scale factors are the same between groups. To output the scale factor applied for all subjects use :code:`foreach * : mrinfo IN/fod_bias_norm.mif -property normalisation_scale_factor`. +.. WARNING:: We recommend you that you check that the normalisation scale (computed during intensity normalisation) is not influenced by the variable of interest in your study. For example if one group contains global (widespread) changes in white matter T2, then this may directly influence the intensity normalisation and therefore bias downstream analysis of apparent fibre density (FD). To check this, you can perform an equivalence test to ensure the overall normalisation scale does not differ between groups. To output these overall normalisation scales for all subjects use :code:`mrinfo */wmfod_norm.mif -property lognorm_scale`. -8. Generate a study-specific unbiased FOD template -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +9. Generate a study-specific unbiased FOD template +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. include:: common_fba_steps/population_template.rst Symbolic link all FOD images (and masks) into a single input folder. If you have fewer than 40 subjects in your study, you can use the entire population to build the template:: - foreach * : ln -sr IN/fod_bias_norm.mif ../template/fod_input/PRE.mif + foreach * : ln -sr IN/wmfod_norm.mif ../template/fod_input/PRE.mif foreach * : ln -sr IN/dwi_mask_upsampled.mif ../template/mask_input/PRE.mif Alternatively, if you have more than 40 subjects you can randomly select a subset of the individuals. If your study has multiple groups, then ideally you want to select the same number of subjects from each group to ensure the template is un-biased. Assuming the subject directory labels can be used to identify members of each group, you could use:: - foreach `ls -d *patient | sort -R | tail -20` : ln -sr IN/fod_bias_norm.mif ../template/fod_input/PRE.mif ";" ln -sr IN/dwi_mask_upsampled.mif ../template/mask_input/PRE.mif - foreach `ls -d *control | sort -R | tail -20` : ln -sr IN/fod_bias_norm.mif ../template/fod_input/PRE.mif ";" ln -sr IN/dwi_mask_upsampled.mif ../template/mask_input/PRE.mif + foreach `ls -d *patient | sort -R | tail -20` : ln -sr IN/wmfod_norm.mif ../template/fod_input/PRE.mif ";" ln -sr IN/dwi_mask_upsampled.mif ../template/mask_input/PRE.mif + foreach `ls -d *control | sort -R | tail -20` : ln -sr IN/wmfod_norm.mif ../template/fod_input/PRE.mif ";" ln -sr IN/dwi_mask_upsampled.mif ../template/mask_input/PRE.mif .. include:: common_fba_steps/population_template2.rst -9. Register all subject FOD images to the FOD template +10. Register all subject FOD images to the FOD template ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Register the FOD image from all subjects to the FOD template image. Note you can skip this step if you built your template from your entire population and saved the warps (see previous step):: - foreach * : mrregister IN/fod_wm_bias_norm.mif -mask1 IN/dwi_mask_upsampled.mif ../template/fod_template.mif -nl_warp IN/subject2template_warp.mif IN/template2subject_warp.mif + foreach * : mrregister IN/wmfod_norm.mif -mask1 IN/dwi_mask_upsampled.mif ../template/wmfod_template.mif -nl_warp IN/subject2template_warp.mif IN/template2subject_warp.mif -10. Compute the intersection of all subject masks in template space +11. Compute the intersection of all subject masks in template space ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. include:: common_fba_steps/mask_intersection.rst -11. Compute a white matter analysis voxel & fixel mask -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +12. Compute a white matter analysis voxel & fixel mask +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Here we first identify all voxels having some white matter by thresholding the DC term (first SH coefficient) of the multi-tissue FOD image:: - mrconvert ../template/fod_template.mif -coord 3 0 - | mrthreshold - ../template/voxel_mask.mif + mrconvert ../template/wmfod_template.mif -coord 3 0 - | mrthreshold - ../template/voxel_mask.mif Next we segment all fixels from each FOD in the template image (see `here `__ for more information about a analysis fixel mask). Note that the fixel image output from this step is stored using the :ref:`fixel_format`, which exploits the filesystem to store all fixel data in a directory:: - fod2fixel -mask ../template/voxel_mask.mif -fmls_peak_value 0.2 ../template/fod_template.mif ../template/fixel_mask + fod2fixel -mask ../template/voxel_mask.mif -fmls_peak_value 0.1 ../template/wmfod_template.mif ../template/fixel_mask You can visualise the output fixels using the fixel plot tool from :ref:`mrview`, and opening either the :code:`index.mif` or :code:`directions.mif` found in :code:`../template/fixel_mask`. The automatic thresholding step used above should give you a mask that nicely covers all of white matter, however if not you can always try manually adjusting the threshold with the :code:`mrthreshold -abs` option. -.. NOTE:: We recommend having no more than 500,000 fixels in the analysis_fixel_mask (you can check this by :code:`mrinfo -size ../template/fixel_mask/directions.mif`, and looking at the size of the image along the 1st dimension), otherwise downstream statistical analysis (using :ref:`fixelcfestats`) will run out of RAM). A mask with 500,000 fixels will require a PC with 128GB of RAM for the statistical analysis step. To reduce the number of fixels, try either reducing the number of voxels in the voxel mask by applying a manual threshold using :code:`-abs`, increasing the :code:`-fmls_peak_value`, or reducing the extent of upsampling in step 4. +.. NOTE:: We recommend having no more than 500,000 fixels in the analysis_fixel_mask (you can check this by :code:`mrinfo -size ../template/fixel_mask/directions.mif`, and looking at the size of the image along the 1st dimension), otherwise downstream statistical analysis (using :ref:`fixelcfestats`) may run out of RAM). A mask with 500,000 fixels will require a PC with 128GB of RAM for the statistical analysis step. To reduce the number of fixels, try either reducing the number of voxels in the voxel mask by applying a manual threshold using :code:`-abs`, increasing the :code:`-fmls_peak_value`, or reducing the extent of upsampling in step 4. -12. Warp FOD images to template space -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +13. Warp FOD images to template space +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Note that here we warp FOD images into template space *without* FOD reorientation. Reorientation will be performed in a separate subsequent step:: - foreach * : mrtransform IN/fod_bias_norm.mif -warp IN/subject2template_warp.mif -noreorientation IN/fod_in_template_space.mif + foreach * : mrtransform IN/wmfod_norm.mif -warp IN/subject2template_warp.mif -noreorientation IN/fod_in_template_space.mif -13. Segment FOD images to estimate fixels and their apparent fibre density (FD) +14. Segment FOD images to estimate fixels and their apparent fibre density (FD) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. include:: common_fba_steps/compute_AFD.rst -14. Reorient fixel orientations +15. Reorient fixel orientations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. include:: common_fba_steps/reorient_fixels.rst -15. Assign subject fixels to template fixels +16. Assign subject fixels to template fixels ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In step 8 & 9 we obtained spatial correspondence between subject and template. In step 14 we corrected the fixel orientations to ensure angular correspondence of the segmented peaks of subject and template. Here, for each fixel in the template fixel analysis mask, we identify the corresponding fixel in each voxel of the subject image and assign the FD value of the subject fixel to the corresponding fixel in template space. If no fixel exists in the subject that corresponds to the template fixel then it is assigned a value of zero. See `this paper `__ for more information. In the command below, you will note that the output fixel directory is the same for all subjects. This directory now stores data for all subjects at corresponding fixels, ready for input to :code:`fixelcfestats` in step 20 below:: foreach * : fixelcorrespondence IN/fixel_in_template_space/fd.mif ../template/fixel_mask ../template/fd PRE.mif -16. Compute fibre cross-section (FC) metric +17. Compute fibre cross-section (FC) metric ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. include:: common_fba_steps/compute_FC.rst -17. Compute a combined measure of fibre density and cross-section (FDC) +18. Compute a combined measure of fibre density and cross-section (FDC) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. include:: common_fba_steps/compute_FDC.rst -18. Perform whole-brain fibre tractography on the FOD template +19. Perform whole-brain fibre tractography on the FOD template ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. include:: common_fba_steps/tractography.rst -19. Reduce biases in tractogram densities +20. Reduce biases in tractogram densities ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. include:: common_fba_steps/sift.rst -20. Perform statistical analysis of FD, FC, and FDC +21. Perform statistical analysis of FD, FC, and FDC ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. include:: common_fba_steps/statistics.rst -21. Visualise the results +22. Visualise the results ^^^^^^^^^^^^^^^^^^^^^^^^^ .. include:: common_fba_steps/visualisation.rst diff --git a/docs/fixel_based_analysis/ss_fibre_density_cross-section.rst b/docs/fixel_based_analysis/ss_fibre_density_cross-section.rst index fbb331192e..c4ec1f9241 100644 --- a/docs/fixel_based_analysis/ss_fibre_density_cross-section.rst +++ b/docs/fixel_based_analysis/ss_fibre_density_cross-section.rst @@ -10,9 +10,9 @@ This tutorial explains how to perform `fixel-based analysis of fibre density and All steps in this tutorial have written as if the commands are being **run on a cohort of images**, and make extensive use of the :ref:`foreach script to simplify batch processing `. This tutorial also assumes that the imaging dataset is organised with one directory identifying the subject, and all files within identifying the image type. For example:: study/subjects/001_patient/dwi.mif - study/subjects/001_patient/fod.mif + study/subjects/001_patient/wmfod.mif study/subjects/002_control/dwi.mif - study/subjects/002_control/fod.mif + study/subjects/002_control/wmfod.mif .. NOTE:: All commands in this tutorial are run **from the subjects path** up until step 20, where we change directory to the template path @@ -96,28 +96,21 @@ Fixel-based analysis steps 7. Upsampling DW images ^^^^^^^^^^^^^^^^^^^^^^^ - Upsampling DWI data before computing FODs can `increase anatomical contrast `_ and improve downstream spatial normalisation and statistics. We recommend upsampling to a voxel size of 1.25mm (for human brains). If you have data that has smaller voxels than 1.25mm, then we recommend you can skip this step:: foreach * : mrresize IN/dwi_denoised_preproc_bias_norm.mif -vox 1.25 IN/dwi_denoised_preproc_bias_norm_upsampled.mif 8. Compute upsampled brain mask images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Compute a whole brain mask from the upsampled DW images:: foreach * : dwi2mask IN/dwi_denoised_preproc_bias_norm_upsampled.mif IN/dwi_mask_upsampled.mif -Depending on your data, you may find that upsampling the low-resolution masks from step 3 gives superiour masks (with less holes). This can be performed using:: - - foreach * : mrresize IN/dwi_mask.mif -scale 2.0 -inter nearest IN/dwi_mask_upsampled.mif - - 9. Fibre Orientation Distribution estimation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When performing analysis of AFD, Constrained Spherical Deconvolution (CSD) should be performed using the group average response function computed at step . If not using AFD in the fixel-based analysis (and therefore you have skipped steps 4-6), however you still want to compute FODs for image registration, then you can use a subject-specific response function. Note that :code:`dwi2fod csd` can be used, however here we use :code:`dwi2fod msmt_csd` (even with single shell data) to benefit from the hard non-negativity constraint:: - foreach * : dwiextract IN/dwi_denoised_preproc_bias_norm_upsampled.mif - \| dwi2fod msmt_csd - ../group_average_response.txt IN/fod.mif -mask IN/dwi_mask_upsampled.mif + foreach * : dwiextract IN/dwi_denoised_preproc_bias_norm_upsampled.mif - \| dwi2fod msmt_csd - ../group_average_response.txt IN/wmfod.mif -mask IN/dwi_mask_upsampled.mif 10. Generate a study-specific unbiased FOD template ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -126,13 +119,13 @@ When performing analysis of AFD, Constrained Spherical Deconvolution (CSD) shoul Symbolic link all FOD images (and masks) into a single input folder. If you have fewer than 40 subjects in your study, you can use the entire population to build the template:: - foreach * : ln -sr IN/fod.mif ../template/fod_input/PRE.mif + foreach * : ln -sr IN/wmfod.mif ../template/fod_input/PRE.mif foreach * : ln -sr IN/dwi_mask_upsampled.mif ../template/mask_input/PRE.mif Alternatively, if you have more than 40 subjects you can randomly select a subset of the individuals. If your study has multiple groups, then ideally you want to select the same number of subjects from each group to ensure the template is un-biased. Assuming the subject directory labels can be used to identify members of each group, you could use:: - foreach `ls -d *patient | sort -R | tail -20` : ln -sr IN/fod.mif ../template/fod_input/PRE.mif ";" ln -sr IN/dwi_mask_upsampled.mif ../template/mask_input/PRE.mif - foreach `ls -d *control | sort -R | tail -20` : ln -sr IN/fod.mif ../template/fod_input/PRE.mif ";" ln -sr IN/dwi_mask_upsampled.mif ../template/mask_input/PRE.mif + foreach `ls -d *patient | sort -R | tail -20` : ln -sr IN/wmfod.mif ../template/fod_input/PRE.mif ";" ln -sr IN/dwi_mask_upsampled.mif ../template/mask_input/PRE.mif + foreach `ls -d *control | sort -R | tail -20` : ln -sr IN/wmfod.mif ../template/fod_input/PRE.mif ";" ln -sr IN/dwi_mask_upsampled.mif ../template/mask_input/PRE.mif .. include:: common_fba_steps/population_template2.rst @@ -141,7 +134,7 @@ Alternatively, if you have more than 40 subjects you can randomly select a subse Register the FOD image from all subjects to the FOD template image:: - foreach * : mrregister IN/fod.mif -mask1 IN/dwi_mask_upsampled.mif ../template/fod_template.mif -nl_warp IN/subject2template_warp.mif IN/template2subject_warp.mif + foreach * : mrregister IN/wmfod.mif -mask1 IN/dwi_mask_upsampled.mif ../template/wmfod_template.mif -nl_warp IN/subject2template_warp.mif IN/template2subject_warp.mif 12. Compute the intersection of all subject masks in template space @@ -156,7 +149,7 @@ Here we perform a 2-step threshold to identify template white matter fixels to b Compute a template AFD peaks fixel image:: - fod2fixel ../template/fod_template.mif -mask ../template/mask_intersection.mif ../template/fixel_temp -peak peaks.mif + fod2fixel ../template/wmfod_template.mif -mask ../template/mask_intersection.mif ../template/fixel_temp -peak peaks.mif .. NOTE:: Fixel images in this step are stored using the :ref:`fixel_format`, which exploits the filesystem to store all fixel data in a directory. @@ -173,7 +166,7 @@ Generate an analysis voxel mask from the fixel mask. The median filter in this s Recompute the fixel mask using the analysis voxel mask. Using the mask allows us to use a lower AFD threshold than possible in the steps above, to ensure we have included fixels with low AFD inside white matter (e.g. areas with fibre crossings):: - fod2fixel -mask ../template/voxel_mask.mif -fmls_peak_value 0.2 ../template/fod_template.mif ../template/fixel_mask + fod2fixel -mask ../template/voxel_mask.mif -fmls_peak_value 0.2 ../template/wmfod_template.mif ../template/fixel_mask .. NOTE:: We recommend having no more than 500,000 fixels in the analysis fixel mask (you can check this by :code:`mrinfo -size ../template/fixel/mask.mif`, and looking at the size of the image along the 1st dimension), otherwise downstream statistical analysis (using :ref:`fixelcfestats`) will run out of RAM). A mask with 500,000 fixels will require a PC with 128GB of RAM for the statistical analysis step. To reduce the number of fixels, try changing the thresholds in this step, or reduce the extent of upsampling in step 7. @@ -182,7 +175,7 @@ Recompute the fixel mask using the analysis voxel mask. Using the mask allows us Note that here we warp FOD images into template space *without* FOD reorientation. Reorientation will be performed in a separate subsequent step:: - foreach * : mrtransform IN/fod.mif -warp IN/subject2template_warp.mif -noreorientation IN/fod_in_template_space.mif + foreach * : mrtransform IN/wmfod.mif -warp IN/subject2template_warp.mif -noreorientation IN/fod_in_template_space.mif 15. Segment FOD images to estimate fixels and their apparent fibre density (FD) diff --git a/docs/quantitative_structural_connectivity/act.rst b/docs/quantitative_structural_connectivity/act.rst index f077aa9c8a..3b191ab2f9 100644 --- a/docs/quantitative_structural_connectivity/act.rst +++ b/docs/quantitative_structural_connectivity/act.rst @@ -69,5 +69,5 @@ Alternative tissue segmentation software Users who wish to experiment with using tissue segmentations from different software sources are encouraged to do so; if a particular approach is shown to be effective we can add an appropriate script to MRtrix. The ``5ttgen`` script has a second algorithm, ``freesurfer``, which demonstrates how the output of different software can be manipulated to provide the tissue segmentations in the appropriate format. It is however not recommended to actually use this alternative algorithm for patient studies; many midbrain structures are not segmented by FreeSurfer, so the tracking may not behave as desired. -Users who wish to try manipulating the tissue segmentations from some alternative software into the 5TT format may find it most convenient to make a copy of one of the existing algorithms within the ``scripts/src/_5ttgen//`` directory, and modify accordingly. The ``5ttgen`` script will automatically detect the presence of the new algorithm, and make it available at the command-line. +Users who wish to try manipulating the tissue segmentations from some alternative software into the 5TT format may find it most convenient to make a copy of one of the existing algorithms within the ``lib/mrtrix3/_5ttgen/`` directory, and modify accordingly. The ``5ttgen`` script will automatically detect the presence of the new algorithm, and make it available at the command-line. diff --git a/docs/quantitative_structural_connectivity/ismrm_hcp_tutorial.rst b/docs/quantitative_structural_connectivity/ismrm_hcp_tutorial.rst index 44a9ca8f18..4fe2664545 100644 --- a/docs/quantitative_structural_connectivity/ismrm_hcp_tutorial.rst +++ b/docs/quantitative_structural_connectivity/ismrm_hcp_tutorial.rst @@ -7,6 +7,10 @@ connectome for quintessential Human Connectome Project subject 100307. Some of these instructions will be specific to HCP data, others will be more general recommendations. +Note that this page is being retained as a reference of the steps +demonstrated during the ISMRM 2015 meeting; it does *not* constitute an +up-to-date 'recommended' processing pipeline for HCP data. + Necessary files --------------- diff --git a/docs/reference/commands/dirgen.rst b/docs/reference/commands/dirgen.rst index d7fe6eb6e6..ee52425850 100644 --- a/docs/reference/commands/dirgen.rst +++ b/docs/reference/commands/dirgen.rst @@ -18,13 +18,20 @@ Usage - *ndir*: the number of directions to generate. - *dirs*: the text file to write the directions to, as [ az el ] pairs. +Description +----------- + +Directions are distributed by analogy to an electrostatic repulsion system, with each direction corresponding to a single electrostatic charge (for -unipolar), or a pair of diametrically opposed charges (for the default bipolar case). The energy of the system is determined based on the Coulomb repulsion, which assumes the form 1/r^power, where r is the distance between any pair of charges, and p is the power assumed for the repulsion law (default: 1). The minimum energy state is obtained by gradient descent. + Options ------- -- **-power exp** specify exponent to use for repulsion power law (default: 2). This must be a power of 2 (i.e. 2, 4, 8, 16, ...). +- **-power exp** specify exponent to use for repulsion power law (default: 1). This must be a power of 2 (i.e. 1, 2, 4, 8, 16, ...). - **-niter num** specify the maximum number of iterations to perform (default: 10000). +- **-restarts num** specify the number of restarts to perform (default: 10). + - **-unipolar** optimise assuming a unipolar electrostatic repulsion model rather than the bipolar model normally assumed in DWI - **-cartesian** Output the directions in Cartesian coordinates [x y z] instead of [az el]. diff --git a/docs/reference/commands/dirmerge.rst b/docs/reference/commands/dirmerge.rst index 67e377793e..ea00e52b90 100644 --- a/docs/reference/commands/dirmerge.rst +++ b/docs/reference/commands/dirmerge.rst @@ -22,6 +22,8 @@ Usage Options ------- +- **-unipolar_weight** set the weight given to the unipolar electrostatic repulsion model compared to the bipolar model (default: 0.2). + Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/fixelcfestats.rst b/docs/reference/commands/fixelcfestats.rst index f04ed4dfdb..7e8655a4bd 100644 --- a/docs/reference/commands/fixelcfestats.rst +++ b/docs/reference/commands/fixelcfestats.rst @@ -22,6 +22,11 @@ Usage - *tracks*: the tracks used to determine fixel-fixel connectivity - *out_fixel_directory*: the output directory where results will be saved. Will be created if it does not exist +Description +----------- + +Note that if the -mask option is used, the output fixel directory will still contain the same set of fixels as that present in the input fixel template, in order to retain fixel correspondence. However a consequence of this is that all fixels in the template will be initialy visible when the output fixel directory is loaded in mrview. Those fixels outside the processing mask will immediately disappear from view as soon as any data-file-based fixel colouring or thresholding is applied. + Options ------- @@ -62,6 +67,8 @@ Additional options for fixelcfestats - **-angle value** the max angle threshold for assigning streamline tangents to fixels (Default: 45 degrees) +- **-mask file** provide a fixel data file containing a mask of those fixels to be used during processing + Standard options ^^^^^^^^^^^^^^^^ @@ -92,7 +99,7 @@ Raffelt, D.; Smith, RE.; Ridgway, GR.; Tournier, JD.; Vaughan, DN.; Rose, S.; He -**Author:** David Raffelt (david.raffelt@florey.edu.au) +**Author:** David Raffelt (david.raffelt@florey.edu.au) and Robert E. Smith (robert.smith@florey.edu.au) **Copyright:** Copyright (c) 2008-2017 the MRtrix3 contributors. diff --git a/docs/reference/commands/mrdegibbs.rst b/docs/reference/commands/mrdegibbs.rst new file mode 100644 index 0000000000..7a2c033cfc --- /dev/null +++ b/docs/reference/commands/mrdegibbs.rst @@ -0,0 +1,88 @@ +.. _mrdegibbs: + +mrdegibbs +=================== + +Synopsis +-------- + +Remove Gibbs Ringing Artifacts + +Usage +-------- + +:: + + mrdegibbs [ options ] in out + +- *in*: the input image. +- *out*: the output image. + +Description +----------- + +This application attempts to remove Gibbs ringing artefacts from MRI images using the method of local subvoxel-shifts proposed by Kellner et al. (see reference below for details). + +This command is designed to run on data directly after it has been reconstructed by the scanner, before any interpolation of any kind has taken place. You should not run this command after any form of motion correction (e.g. not after dwipreproc). Similarly, if you intend running dwidenoise, you should run this command afterwards, since it has the potential to alter the noise structure, which would impact on dwidenoise's performance. + +Note that this method is designed to work on images acquired with full k-space coverage. Running this method on partial Fourier ('half-scan') data may lead to suboptimal and/or biased results, as noted in the original reference below. There is currently no means of dealing with this; users should exercise caution when using this method on partial Fourier data, and inspect its output for any obvious artefacts. + +Options +------- + +- **-axes list** select the slice axes (default: 0,1 - i.e. x-y). + +- **-nshifts value** discretization of subpixel spacing (default: 20). + +- **-minW value** left border of window used for TV computation (default: 1). + +- **-maxW value** right border of window used for TV computation (default: 3). + +Data type options +^^^^^^^^^^^^^^^^^ + +- **-datatype spec** specify output image data type. Valid choices are: float32, float32le, float32be, float64, float64le, float64be, int64, uint64, int64le, uint64le, int64be, uint64be, int32, uint32, int32le, uint32le, int32be, uint32be, int16, uint16, int16le, uint16le, int16be, uint16be, cfloat32, cfloat32le, cfloat32be, cfloat64, cfloat64le, cfloat64be, int8, uint8, bit. + +Standard options +^^^^^^^^^^^^^^^^ + +- **-info** display information messages. + +- **-quiet** do not display information messages or progress status. + +- **-debug** display debugging messages. + +- **-force** force overwrite of output files. Caution: Using the same file as input and output might cause unexpected behaviour. + +- **-nthreads number** use this number of threads in multi-threaded applications (set to 0 to disable multi-threading) + +- **-failonwarn** terminate program if a warning is produced + +- **-help** display this information page and exit. + +- **-version** display version information and exit. + +References +^^^^^^^^^^ + +Kellner, E; Dhital, B; Kiselev, V.G & Reisert, M. Gibbs-ringing artifact removal based on local subvoxel-shifts. Magnetic Resonance in Medicine, 2016, 76, 1574–1581. + +-------------- + + + +**Author:** Ben Jeurissen (ben.jeurissen@uantwerpen.be) & J-Donald Tournier (jdtournier@gmail.com) + +**Copyright:** Copyright (c) 2008-2017 the MRtrix3 contributors. + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, you can obtain one at http://mozilla.org/MPL/2.0/. + +MRtrix is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty +of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +For more details, see http://www.mrtrix.org/. + + diff --git a/docs/reference/commands/mrmodelfield.rst b/docs/reference/commands/mrmodelfield.rst deleted file mode 100644 index 12fb383ed6..0000000000 --- a/docs/reference/commands/mrmodelfield.rst +++ /dev/null @@ -1,68 +0,0 @@ -.. _mrmodelfield: - -mrmodelfield -=================== - -Synopsis --------- - -Model an input image using low frequency 3D polynomial basis functions - -Usage --------- - -:: - - mrmodelfield [ options ] input output - -- *input*: the input image -- *output*: the output image representing the fit - -Description ------------ - -This command was designed to estimate a DWI bias field using the sum of normalised multi-tissue CSD compartments. - -Options -------- - -- **-mask image** use only voxels within the supplied mask for the model fit. If not supplied this command will compute a mask - -Standard options -^^^^^^^^^^^^^^^^ - -- **-info** display information messages. - -- **-quiet** do not display information messages or progress status. - -- **-debug** display debugging messages. - -- **-force** force overwrite of output files. Caution: Using the same file as input and output might cause unexpected behaviour. - -- **-nthreads number** use this number of threads in multi-threaded applications (set to 0 to disable multi-threading) - -- **-failonwarn** terminate program if a warning is produced - -- **-help** display this information page and exit. - -- **-version** display version information and exit. - --------------- - - - -**Author:** David Raffelt (david.raffelt@florey.edu.au) & Rami Tabbara (rami.tabbara@florey.edu.au) - -**Copyright:** Copyright (c) 2008-2017 the MRtrix3 contributors. - -This Source Code Form is subject to the terms of the Mozilla Public -License, v. 2.0. If a copy of the MPL was not distributed with this -file, you can obtain one at http://mozilla.org/MPL/2.0/. - -MRtrix is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty -of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - -For more details, see http://www.mrtrix.org/. - - diff --git a/docs/reference/commands/mrtransform.rst b/docs/reference/commands/mrtransform.rst index 409b58ac88..6cee9b69b0 100644 --- a/docs/reference/commands/mrtransform.rst +++ b/docs/reference/commands/mrtransform.rst @@ -54,6 +54,8 @@ Regridding options - **-interp method** set the interpolation method to use when reslicing (choices: nearest, linear, cubic, sinc. Default: cubic). +- **-oversample factor** set the amount of over-sampling (in the target space) to perform when regridding. This is particularly relevant when downsamping a high-resolution image to a low-resolution image, to avoid aliasing artefacts. This can consist of a single integer, or a comma-separated list of 3 integers if different oversampling factors are desired along the different axes. Default is determined from ratio of voxel dimensions (disabled for nearest-neighbour interpolation). + Non-linear transformation options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/mrview.rst b/docs/reference/commands/mrview.rst index 605b4d0bcb..542e303ed0 100644 --- a/docs/reference/commands/mrview.rst +++ b/docs/reference/commands/mrview.rst @@ -126,6 +126,14 @@ Tractography tool options - **-tractography.opacity value** Opacity of tractography display, [0.0, 1.0], default is 1.0. +- **-tractography.slab value** Slab thickness of tractography display, in mm. -1 to turn off crop to slab. + +- **-tractography.tsf_load tsf** Load the specified tractography scalar file. + +- **-tractography.tsf_range range** Set range for the tractography scalar file. Requires tractography.tsf_load already provided. RangeMin,RangeMax + +- **-tractography.tsf_thresh thresh** Set thresholds for the tractography scalar file. Requires tractography.tsf_load already provided. ThresholdMin,ThesholdMax + ODF tool options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/mtbin.rst b/docs/reference/commands/mtbin.rst index fbe1ac0b17..15324d8da3 100644 --- a/docs/reference/commands/mtbin.rst +++ b/docs/reference/commands/mtbin.rst @@ -6,7 +6,7 @@ mtbin Synopsis -------- -Multi-Tissue Bias field correction and Intensity Normalisation (MTBIN) +Multi-Tissue Bias field correction and Intensity Normalisation (WARNING: deprecated). Usage -------- @@ -20,13 +20,7 @@ Usage Description ----------- -This command inputs N number of tissue components (e.g. from multi-tissue CSD), and outputs N corrected tissue components. Intensity normalisation is performed by either determining a common global normalisation factor for all tissue types (default) or by normalising each tissue type independently with a single tissue-specific global scale factor. - -The -mask option is mandatory, and is optimally provided with a brain mask, such as the one obtained from dwi2mask earlier in the processing pipeline. - -Example usage: mtbin wm.mif wm_norm.mif gm.mif gm_norm.mif csf.mif csf_norm.mif -mask mask.mif. - -The estimated multiplicative bias field is guaranteed to have a mean of 1 over all voxels within the mask. +WARNING: this command is deprecated and may produce highly inappropriate results in several cases. Not recommended and at your own discretion. Please use the new mtnormalise command instead for reliable results. Options ------- @@ -43,6 +37,8 @@ Options - **-check image** check the final mask used to compute the bias field. This mask excludes outlier regions ignored by the bias field fitting procedure. However, these regions are still corrected for bias fields based on the other image data. +- **-override** consciously use this deprecated command. Not recommended and at your own discretion. + Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/mtnormalise.rst b/docs/reference/commands/mtnormalise.rst index a74f2d78d9..4bef89ae0c 100644 --- a/docs/reference/commands/mtnormalise.rst +++ b/docs/reference/commands/mtnormalise.rst @@ -6,7 +6,7 @@ mtnormalise Synopsis -------- -Multi-tissue normalise +Multi-tissue informed log-domain intensity normalisation Usage -------- @@ -15,21 +15,31 @@ Usage mtnormalise [ options ] input output [ input output ... ] -- *input output*: list of all input and output tissue compartment files. See example usage in the description. Note that any number of tissues can be normalised +- *input output*: list of all input and output tissue compartment files. See example usage in the description. Description ----------- -Globally normalise multiple tissue compartments (e.g. WM, GM, CSF) from multi-tissue CSD such that their sum (of their DC terms) within each voxel is as close to a scalar as possible (Default: sqrt(1/(4*pi)). Normalisation is performed by solving for a single scale factor to adjust each tissue type. +This command inputs any number of tissue components (e.g. from multi-tissue CSD) and outputs corresponding normalised tissue components. Intensity normalisation is performed in the log-domain, and can smoothly vary spatially to accomodate the effects of (residual) intensity inhomogeneities. -Example usage: mtnormalise wm.mif wm_norm.mif gm.mif gm_norm.mif csf.mif csf_norm.mif +The -mask option is mandatory and is optimally provided with a brain mask (such as the one obtained from dwi2mask earlier in the processing pipeline). Outlier areas with exceptionally low or high combined tissue contributions are accounted for and reoptimised as the intensity inhomogeneity estimation becomes more accurate. + +Example usage: mtnormalise wmfod.mif wmfod_norm.mif gm.mif gm_norm.mif csf.mif csf_norm.mif -mask mask.mif. Options ------- -- **-mask image** define the mask to compute the normalisation within. If not supplied this is estimated automatically +- **-mask image** the mask defines the data used to compute the intensity normalisation. This option is mandatory. + +- **-order number** the maximum order of the polynomial basis used to fit the normalisation field in the log-domain. An order of 0 is equivalent to not allowing spatial variance of the intensity normalisation factor. (default: 3) + +- **-niter number** set the number of iterations. (default: 15) + +- **-check_norm image** output the final estimated spatially varying intensity level that is used for normalisation. + +- **-check_mask image** output the final mask used to compute the normalisation. This mask excludes regions identified as outliers by the optimisation process. -- **-value number** specify the value to which the summed tissue compartments will be to (Default: sqrt(1/(4*pi) = 0.282) +- **-value number** specify the (positive) reference value to which the summed tissue compartments will be normalised. (default: 0.282095, SH DC term for unit angular integral) Standard options ^^^^^^^^^^^^^^^^ @@ -54,7 +64,7 @@ Standard options -**Author:** David Raffelt (david.raffelt@florey.edu.au) +**Author:** Thijs Dhollander (thijs.dhollander@gmail.com), Rami Tabbara (rami.tabbara@florey.edu.au) and David Raffelt (david.raffelt@florey.edu.au) **Copyright:** Copyright (c) 2008-2017 the MRtrix3 contributors. diff --git a/docs/reference/commands_list.rst b/docs/reference/commands_list.rst index e2dda2196c..d20aefb4dc 100644 --- a/docs/reference/commands_list.rst +++ b/docs/reference/commands_list.rst @@ -61,6 +61,7 @@ List of MRtrix3 commands commands/mrclusterstats commands/mrconvert commands/mrcrop + commands/mrdegibbs commands/mrdump commands/mredit commands/mrfilter @@ -70,7 +71,6 @@ List of MRtrix3 commands commands/mrmath commands/mrmesh commands/mrmetric - commands/mrmodelfield commands/mrpad commands/mrregister commands/mrresize @@ -172,6 +172,7 @@ List of MRtrix3 commands :ref:`mrclusterstats`, "Voxel-based analysis using permutation testing and threshold-free cluster enhancement" :ref:`mrconvert`, "Perform conversion between different file types and optionally extract a subset of the input image" :ref:`mrcrop`, "Crop an image to a reduced field of view" + :ref:`mrdegibbs`, "Remove Gibbs Ringing Artifacts" :ref:`mrdump`, "Print out the values within an image" :ref:`mredit`, "Directly edit the intensities within an image from the command-line" :ref:`mrfilter`, "Perform filtering operations on 3D / 4D MR images" @@ -181,7 +182,6 @@ List of MRtrix3 commands :ref:`mrmath`, "Compute summary statistic on image intensities either across images, or along a specified axis of a single image" :ref:`mrmesh`, "Generate a mesh file from an image" :ref:`mrmetric`, "Computes a dissimilarity metric between two images" - :ref:`mrmodelfield`, "Model an input image using low frequency 3D polynomial basis functions" :ref:`mrpad`, "Pad an image to increase the FOV" :ref:`mrregister`, "Register two images together using a symmetric rigid, affine or non-linear transformation model" :ref:`mrresize`, "Resize an image by defining the new image resolution, voxel size or a scale factor" @@ -189,8 +189,8 @@ List of MRtrix3 commands :ref:`mrthreshold`, "Create bitwise image by thresholding image intensity" :ref:`mrtransform`, "Apply spatial transformations to an image" :ref:`mrview`, "The MRtrix image viewer." - :ref:`mtbin`, "Multi-Tissue Bias field correction and Intensity Normalisation (MTBIN)" - :ref:`mtnormalise`, "Multi-tissue normalise" + :ref:`mtbin`, "Multi-Tissue Bias field correction and Intensity Normalisation (WARNING: deprecated)." + :ref:`mtnormalise`, "Multi-tissue informed log-domain intensity normalisation" :ref:`peaks2amp`, "Convert peak directions image to amplitudes" :ref:`sh2amp`, "Evaluate the amplitude of an image of spherical harmonic functions along specified directions" :ref:`sh2peaks`, "Extract the peaks of a spherical harmonic function at each voxel, by commencing a Newton search along a set of specified directions" diff --git a/docs/reference/scripts/population_template.rst b/docs/reference/scripts/population_template.rst index c72f878d74..a16dddb286 100644 --- a/docs/reference/scripts/population_template.rst +++ b/docs/reference/scripts/population_template.rst @@ -31,6 +31,8 @@ Input, output and general options - **-type** Specifiy the types of registration stages to perform. Options are "rigid" (perform rigid registration only which might be useful for intra-subject registration in longitudinal analysis), "affine" (perform affine registration) and "nonlinear" as well as cominations of registration types: "rigid_affine", "rigid_nonlinear", "affine_nonlinear", "rigid_affine_nonlinear". Default: rigid_affine_nonlinear +- **-voxel_size** Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma separated values. + - **-initial_alignment** Method of alignment to form the initial template. Options are "mass" (default), "geometric" and "none". - **-mask_dir** Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly diff --git a/docs/troubleshooting/FAQ.rst b/docs/troubleshooting/FAQ.rst index 3f3163817b..fca499c1f2 100644 --- a/docs/troubleshooting/FAQ.rst +++ b/docs/troubleshooting/FAQ.rst @@ -175,10 +175,10 @@ location of those libraries; e.g.: .. code-block:: console - $ export PYTHONPATH=/home/user/mrtrix3/scripts:$PYTHONPATH + $ export PYTHONPATH=/home/user/mrtrix3/lib:$PYTHONPATH $ ./my_script [arguments] (options) -(Replace the path to the *MRtrix3* scripts directory with the location of your +(Replace the path to the *MRtrix3* "lib" directory with the location of your own installation) ``tck2connectome`` no longer has the ``-contrast X`` option...? diff --git a/lib/mrtrix3/_5ttgen/fsl.py b/lib/mrtrix3/_5ttgen/fsl.py index 9c25c883ed..d82a684674 100644 --- a/lib/mrtrix3/_5ttgen/fsl.py +++ b/lib/mrtrix3/_5ttgen/fsl.py @@ -135,14 +135,11 @@ def execute(): run.command(ssroi_cmd + ' T1.nii T1_preBET' + fsl_suffix + ' -maskMASK mni_mask.nii' + ssroi_roi_option, False) else: run.command(ssroi_cmd + ' T1.nii T1_preBET' + fsl_suffix + ' -b', False) - - # For whatever reason, the output file from standard_space_roi may not be - # completed before BET is run - file.waitFor('T1_preBET' + fsl_suffix) + pre_bet_image = fsl.findImage('T1_preBET') # BET - fast_t1_input = 'T1_BET' + fsl_suffix - run.command(bet_cmd + ' T1_preBET' + fsl_suffix + ' ' + fast_t1_input + ' -f 0.15 -R') + run.command(bet_cmd + ' ' + pre_bet_image + ' T1_BET' + fsl_suffix + ' -f 0.15 -R') + fast_t1_input = fsl.findImage('T1_BET' + fsl_suffix) if os.path.exists('T2.nii'): if app.args.nocrop: @@ -159,7 +156,6 @@ def execute(): run.command(fast_cmd + ' -S 2 ' + fast_t2_input + ' ' + fast_t1_input) else: run.command(fast_cmd + ' ' + fast_t1_input) - fast_output_prefix = fast_t1_input.split('.')[0] # FIRST first_input_is_brain_extracted = '' @@ -178,7 +174,10 @@ def execute(): app.console('(note however that FIRST may fail silently, and hence this script may hang indefinitely)') file.waitFor(combined_image_path) else: - app.error('FSL FIRST has failed; not all structures were segmented successfully (check ' + path.toTemp('first.logs', False) + ')') + combined_image_path = fsl.findImage('first_all_none_firstseg') + if not os.path.isfile(combined_image_path): + app.error('FSL FIRST has failed; not all structures were segmented successfully (check ' + +path.toTemp('first.logs', False) + ')') # Convert FIRST meshes to partial volume images pve_image_list = [ ] @@ -192,23 +191,23 @@ def execute(): pve_cat = ' '.join(pve_image_list) run.command('mrmath ' + pve_cat + ' sum - | mrcalc - 1.0 -min all_sgms.mif') - # Looks like FAST in 5.0 ignores FSLOUTPUTTYPE when writing the PVE images - # Will have to wait and see whether this changes, and update the script accordingly - if fast_cmd == 'fast': - fast_suffix = fsl_suffix - else: - fast_suffix = '.nii.gz' - # Combine the tissue images into the 5TT format within the script itself + fast_output_prefix = fast_t1_input.split('.')[0] + fast_csf_output = fsl.findImage(fast_output_prefix + '_pve_0') + fast_gm_output = fsl.findImage(fast_output_prefix + '_pve_1') + fast_wm_output = fsl.findImage(fast_output_prefix + '_pve_2') # Step 1: Run LCC on the WM image - run.command('mrthreshold ' + fast_output_prefix + '_pve_2' + fast_suffix + ' - -abs 0.001 | maskfilter - connect - -connectivity | mrcalc 1 - 1 -gt -sub remove_unconnected_wm_mask.mif -datatype bit') - # Step 2: Generate the images in the same fashion as the 5ttgen command - run.command('mrcalc ' + fast_output_prefix + '_pve_0' + fast_suffix + ' remove_unconnected_wm_mask.mif -mult csf.mif') + run.command('mrthreshold ' + fast_wm_output + ' - -abs 0.001 | maskfilter - connect - -connectivity | mrcalc 1 - 1 -gt -sub remove_unconnected_wm_mask.mif -datatype bit') + # Step 2: Generate the images in the same fashion as the old 5ttgen binary used to: + # - Preserve CSF as-is + # - Preserve SGM, unless it results in a sum of volume fractions greater than 1, in which case clamp + # - Multiply the FAST volume fractions of GM and CSF, so that the sum of CSF, SGM, CGM and WM is 1.0 + run.command('mrcalc ' + fast_csf_output + ' remove_unconnected_wm_mask.mif -mult csf.mif') run.command('mrcalc 1.0 csf.mif -sub all_sgms.mif -min sgm.mif') - run.command('mrcalc 1.0 csf.mif sgm.mif -add -sub ' + fast_output_prefix + '_pve_1' + fast_suffix + ' ' + fast_output_prefix + '_pve_2' + fast_suffix + ' -add -div multiplier.mif') + run.command('mrcalc 1.0 csf.mif sgm.mif -add -sub ' + fast_gm_output + ' ' + fast_wm_output + ' -add -div multiplier.mif') run.command('mrcalc multiplier.mif -finite multiplier.mif 0.0 -if multiplier_noNAN.mif') - run.command('mrcalc ' + fast_output_prefix + '_pve_1' + fast_suffix + ' multiplier_noNAN.mif -mult remove_unconnected_wm_mask.mif -mult cgm.mif') - run.command('mrcalc ' + fast_output_prefix + '_pve_2' + fast_suffix + ' multiplier_noNAN.mif -mult remove_unconnected_wm_mask.mif -mult wm.mif') + run.command('mrcalc ' + fast_gm_output + ' multiplier_noNAN.mif -mult remove_unconnected_wm_mask.mif -mult cgm.mif') + run.command('mrcalc ' + fast_wm_output + ' multiplier_noNAN.mif -mult remove_unconnected_wm_mask.mif -mult wm.mif') run.command('mrcalc 0 wm.mif -min path.mif') run.command('mrcat cgm.mif sgm.mif wm.mif csf.mif path.mif - -axis 3 | mrconvert - combined_precrop.mif -stride +2,+3,+4,+1') diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index ae1306e8c8..d79964beb6 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -7,13 +7,11 @@ # - cmdline.addCitation(), cmdline.addDescription(), cmdline.setCopyright() as needed # - Add arguments and options to 'cmdline' as needed # - parse() -# - checkOutputFile() as needed +# - checkOutputPath() as needed # - makeTempDir() if the script requires a temporary directory # - gotoTempDir() if the script is using a temporary directory # - complete() # -# checkOutputFile() can be called at any time after parse() to check if an intended output -# file already exists diff --git a/lib/mrtrix3/fsl.py b/lib/mrtrix3/fsl.py index 45c132dad4..c9871b9f1b 100644 --- a/lib/mrtrix3/fsl.py +++ b/lib/mrtrix3/fsl.py @@ -1,3 +1,5 @@ +_suffix = '' + # Functions that may be useful for scripts that interface with FMRIB FSL tools # Get the name of the binary file that should be invoked to run eddy; @@ -27,6 +29,25 @@ def eddyBinary(cuda): +# In some versions of FSL, even though we try to predict the names of image files that +# FSL commands will generate based on the suffix() function, the FSL binaries themselves +# ignore the FSLOUTPUTTYPE environment variable. Therefore, the safest approach is: +# Whenever receiving an output image from an FSL command, explicitly search for the path +def findImage(name): + import os + from mrtrix3 import app + basename = name.split('.')[0] + if os.path.isfile(basename + suffix()): + app.debug('Image at expected location: \"' + basename + suffix() + '\"') + return basename + suffix() + for suf in ['.nii', '.nii.gz', '.img']: + if os.path.isfile(basename + suf): + app.debug('Expected image at \"' + basename + suffix() + '\", but found at \"' + basename + suf + '\"') + return basename + suf + app.error('Unable to find FSL output file for path \"' + name + '\"') + + + # For many FSL commands, the format of any output images will depend on the string # stored in 'FSLOUTPUTTYPE'. This may even override a filename extension provided # to the relevant command. Therefore use this function to 'guess' what the names @@ -34,18 +55,26 @@ def eddyBinary(cuda): def suffix(): import os from mrtrix3 import app + global _suffix + if _suffix: + return _suffix fsl_output_type = os.environ.get('FSLOUTPUTTYPE', '') if fsl_output_type == 'NIFTI': app.debug('NIFTI -> .nii') - return '.nii' - if fsl_output_type == 'NIFTI_GZ': + _suffix = '.nii' + elif fsl_output_type == 'NIFTI_GZ': app.debug('NIFTI_GZ -> .nii.gz') - return '.nii.gz' - if fsl_output_type == 'NIFTI_PAIR': + _suffix = '.nii.gz' + elif fsl_output_type == 'NIFTI_PAIR': app.debug('NIFTI_PAIR -> .img') - return '.img' - if fsl_output_type == 'NIFTI_PAIR_GZ': + _suffix = '.img' + elif fsl_output_type == 'NIFTI_PAIR_GZ': app.error('MRtrix3 does not support compressed NIFTI pairs; please change FSLOUTPUTTYPE environment variable') - app.warn('Environment variable FSLOUTPUTTYPE not set; FSL commands may fail, or script may fail to locate FSL command outputs') - return '.nii.gz' + elif fsl_output_type: + app.warn('Unrecognised value for environment variable FSLOUTPUTTYPE (\"' + fsl_output_type + '\"): Expecting compressed NIfTIs, but FSL commands may fail') + _suffix = '.nii.gz' + else: + app.warn('Environment variable FSLOUTPUTTYPE not set; FSL commands may fail, or script may fail to locate FSL command outputs') + _suffix = '.nii.gz' + return _suffix diff --git a/lib/mrtrix3/image.py b/lib/mrtrix3/image.py index 4ca0a97ad7..25934ed6c5 100644 --- a/lib/mrtrix3/image.py +++ b/lib/mrtrix3/image.py @@ -8,7 +8,7 @@ def check3DNonunity(image_path): dim = [ int(i) for i in headerField(image_path, 'size').strip().split() ] if len(dim) < 3: app.error('Image \'' + image_path + '\' does not contain 3 spatial dimensions') - if min(dim) == 1: + if min(dim[:3]) == 1: app.error('Image \'' + image_path + '\' does not contain 3D spatial information (axis with size 1)') diff --git a/matlab/write_mrtrix.m b/matlab/write_mrtrix.m index 5b4bdca7da..5ef9bba830 100644 --- a/matlab/write_mrtrix.m +++ b/matlab/write_mrtrix.m @@ -82,12 +82,6 @@ function write_mrtrix (image, filename) fprintf (fid, '\nmrtrix_version: %s', 'matlab'); -if isstruct (image) && isfield (image, 'comments') - for i=1:numel(image.comments) - fprintf (fid, '\ncomments: %s', image.comments{i}); - end -end - if isstruct (image) && isfield (image, 'transform') fprintf (fid, '\ntransform: %.6f', image.transform(1,1)); fprintf (fid, ',%.6f', image.transform(1,2:4)); @@ -104,6 +98,32 @@ function write_mrtrix (image, filename) end end +% write out any other fields: +if isstruct (image) + f = fieldnames(image); + % don't worry about fields that have already been dealt with: + f(strcmp(f,'dim')) = []; + f(strcmp(f,'vox')) = []; + f(strcmp(f,'layout')) = []; + f(strcmp(f,'datatype')) = []; + f(strcmp(f,'transform')) = []; + f(strcmp(f,'dw_scheme')) = []; + f(strcmp(f,'data')) = []; + + % write out contents of the remainging fields: + for n = 1:numel(f) + val = getfield (image, f{n}); + if iscell (val) + for i=1:numel(val) + fprintf (fid, '\n%s: %s', f{n}, val{i}); + end + else + fprintf (fid, '\n%s: %s', f{n}, val); + end + end +end + + if strcmp(filename(end-3:end), '.mif') datafile = filename; dataoffset = ftell (fid) + 24; diff --git a/src/connectome/enhance.h b/src/connectome/enhance.h index 3fcf5401e7..57859a6bf1 100644 --- a/src/connectome/enhance.h +++ b/src/connectome/enhance.h @@ -17,9 +17,9 @@ #include #include -#include #include "bitset.h" +#include "types.h" #include "connectome/mat2vec.h" #include "stats/enhance.h" diff --git a/src/connectome/lut.h b/src/connectome/lut.h index b77b80d826..bb6671a249 100644 --- a/src/connectome/lut.h +++ b/src/connectome/lut.h @@ -15,14 +15,14 @@ #ifndef __connectome_lut_h__ #define __connectome_lut_h__ +#include +#include #include "app.h" +#include "types.h" #include "connectome/connectome.h" -#include -#include -#include namespace MR { diff --git a/src/dwi/directions/set.cpp b/src/dwi/directions/set.cpp index e7ec06bc23..a7e77c975d 100644 --- a/src/dwi/directions/set.cpp +++ b/src/dwi/directions/set.cpp @@ -16,7 +16,6 @@ #include #include -#include #include "bitset.h" #include "math/rng.h" diff --git a/src/dwi/directions/set.h b/src/dwi/directions/set.h index feebbaf64e..8ed73a5f3b 100644 --- a/src/dwi/directions/set.h +++ b/src/dwi/directions/set.h @@ -16,13 +16,13 @@ #define __dwi_directions_set_h__ - #include -#include #include "progressbar.h" +#include "types.h" #include "dwi/directions/predefined.h" + namespace MR { namespace DWI { namespace Directions { diff --git a/src/dwi/gradient.cpp b/src/dwi/gradient.cpp index c161219703..e81069d442 100644 --- a/src/dwi/gradient.cpp +++ b/src/dwi/gradient.cpp @@ -187,6 +187,15 @@ namespace MR + void clear_DW_scheme (Header& header) + { + auto it = header.keyval().find ("dw_scheme"); + if (it != header.keyval().end()) + header.keyval().erase (it); + } + + + Eigen::MatrixXd get_DW_scheme (const Header& header) { DEBUG ("searching for suitable gradient encoding..."); diff --git a/src/dwi/gradient.h b/src/dwi/gradient.h index fc3aa4ec1a..a367f1fe27 100644 --- a/src/dwi/gradient.h +++ b/src/dwi/gradient.h @@ -140,7 +140,9 @@ namespace MR void set_DW_scheme (Header& header, const MatrixType& G) { if (!G.rows()) { - header.keyval().erase ("dw_scheme"); + auto it = header.keyval().find ("dw_scheme"); + if (it != header.keyval().end()) + header.keyval().erase (it); return; } std::string dw_scheme; @@ -183,11 +185,17 @@ namespace MR { set_DW_scheme (header, grad); auto dw_scheme = header.keyval().find ("dw_scheme"); - header.keyval()["prior_dw_scheme"] = dw_scheme->second; - header.keyval().erase (dw_scheme); + if (dw_scheme != header.keyval().end()) { + header.keyval()["prior_dw_scheme"] = dw_scheme->second; + header.keyval().erase (dw_scheme); + } } + //! clear any DW gradient encoding scheme from the header + void clear_DW_scheme (Header&); + + //! get the DW gradient encoding matrix diff --git a/src/dwi/sdeconv/msmt_csd.h b/src/dwi/sdeconv/msmt_csd.h index fa929ff002..ed3dadb55c 100644 --- a/src/dwi/sdeconv/msmt_csd.h +++ b/src/dwi/sdeconv/msmt_csd.h @@ -15,9 +15,8 @@ #ifndef __dwi_sdeconv_msmt_csd_h__ #define __dwi_sdeconv_msmt_csd_h__ -#include - #include "header.h" +#include "types.h" #include "math/constrained_least_squares.h" #include "math/math.h" @@ -46,7 +45,7 @@ namespace MR Shared (const Header& dwi_header) : grad (DWI::get_valid_DW_scheme (dwi_header)), shells (grad), - HR_dirs (DWI::Directions::electrostatic_repulsion_300()) { } + HR_dirs (DWI::Directions::electrostatic_repulsion_300()) { shells.select_shells(false,false,false); } void parse_cmdline_options() @@ -210,7 +209,7 @@ namespace MR const Eigen::MatrixXd grad; - const DWI::Shells shells; + DWI::Shells shells; Eigen::MatrixXd HR_dirs; vector lmax, lmax_response; vector responses; diff --git a/src/dwi/shells.h b/src/dwi/shells.h index ba673cdbe2..cc9a578cd0 100644 --- a/src/dwi/shells.h +++ b/src/dwi/shells.h @@ -18,10 +18,10 @@ #include #include -#include #include "app.h" #include "bitset.h" +#include "types.h" #include "file/config.h" diff --git a/src/dwi/tractography/GT/energy.h b/src/dwi/tractography/GT/energy.h index 40e3485a52..837e4ed486 100644 --- a/src/dwi/tractography/GT/energy.h +++ b/src/dwi/tractography/GT/energy.h @@ -15,8 +15,6 @@ #ifndef __gt_energy_h__ #define __gt_energy_h__ -#include - #include "dwi/tractography/GT/particle.h" #include "dwi/tractography/GT/gt.h" diff --git a/src/dwi/tractography/GT/gt.h b/src/dwi/tractography/GT/gt.h index 2eb67f39c2..b026773587 100644 --- a/src/dwi/tractography/GT/gt.h +++ b/src/dwi/tractography/GT/gt.h @@ -20,12 +20,12 @@ #define FRAC_PHASEOUT 10 #include -#include #include #include #include "progressbar.h" +#include "types.h" namespace MR { diff --git a/src/dwi/tractography/GT/internalenergy.h b/src/dwi/tractography/GT/internalenergy.h index f2bcfdd5b8..105cf2f44c 100644 --- a/src/dwi/tractography/GT/internalenergy.h +++ b/src/dwi/tractography/GT/internalenergy.h @@ -15,7 +15,7 @@ #ifndef __gt_internalenergy_h__ #define __gt_internalenergy_h__ -#include +#include "types.h" #include "math/math.h" #include "math/rng.h" diff --git a/src/dwi/tractography/GT/spatiallock.h b/src/dwi/tractography/GT/spatiallock.h index 64fd78c7db..255b8615a9 100644 --- a/src/dwi/tractography/GT/spatiallock.h +++ b/src/dwi/tractography/GT/spatiallock.h @@ -17,7 +17,8 @@ #include #include -#include + +#include "types.h" namespace MR { diff --git a/src/dwi/tractography/SIFT/gradient_sort.h b/src/dwi/tractography/SIFT/gradient_sort.h index fbb23c6e2e..d2a808e2d7 100644 --- a/src/dwi/tractography/SIFT/gradient_sort.h +++ b/src/dwi/tractography/SIFT/gradient_sort.h @@ -17,7 +17,8 @@ #include -#include + +#include "types.h" #include "dwi/tractography/SIFT/track_index_range.h" #include "dwi/tractography/SIFT/types.h" diff --git a/src/dwi/tractography/SIFT/model.h b/src/dwi/tractography/SIFT/model.h index aa497b40b3..7aff2e6e61 100644 --- a/src/dwi/tractography/SIFT/model.h +++ b/src/dwi/tractography/SIFT/model.h @@ -16,9 +16,9 @@ #define __dwi_tractography_sift_model_h__ -#include - #include "app.h" +#include "thread_queue.h" +#include "types.h" #include "dwi/fixel_map.h" @@ -38,7 +38,7 @@ #include "dwi/tractography/SIFT/track_index_range.h" #include "dwi/tractography/SIFT/types.h" -#include "thread_queue.h" + diff --git a/src/dwi/tractography/SIFT/sifter.h b/src/dwi/tractography/SIFT/sifter.h index e2fe263c03..af365e77a7 100644 --- a/src/dwi/tractography/SIFT/sifter.h +++ b/src/dwi/tractography/SIFT/sifter.h @@ -16,10 +16,12 @@ #define __dwi_tractography_sift_sifter_h__ -#include -#include "math/rng.h" #include "image.h" +#include "types.h" + +#include "math/rng.h" + #include "dwi/fixel_map.h" #include "dwi/directions/set.h" #include "dwi/tractography/SIFT/fixel.h" diff --git a/src/dwi/tractography/SIFT2/coeff_optimiser.h b/src/dwi/tractography/SIFT2/coeff_optimiser.h index e23de78559..82ccc1d8e7 100644 --- a/src/dwi/tractography/SIFT2/coeff_optimiser.h +++ b/src/dwi/tractography/SIFT2/coeff_optimiser.h @@ -16,8 +16,6 @@ #define __dwi_tractography_sift2_coeff_optimiser_h__ -#include - #include "bitset.h" #include "math/golden_section_search.h" diff --git a/src/dwi/tractography/SIFT2/fixel_updater.h b/src/dwi/tractography/SIFT2/fixel_updater.h index 78f2ceab1e..6db1ec50c5 100644 --- a/src/dwi/tractography/SIFT2/fixel_updater.h +++ b/src/dwi/tractography/SIFT2/fixel_updater.h @@ -16,7 +16,7 @@ #define __dwi_tractography_sift2_fixel_updater_h__ -#include +#include "types.h" #include "dwi/tractography/SIFT/track_index_range.h" #include "dwi/tractography/SIFT/types.h" diff --git a/src/dwi/tractography/SIFT2/line_search.h b/src/dwi/tractography/SIFT2/line_search.h index 5db06a0392..42e9b0aacb 100644 --- a/src/dwi/tractography/SIFT2/line_search.h +++ b/src/dwi/tractography/SIFT2/line_search.h @@ -16,7 +16,7 @@ #define __dwi_tractography_sift2_line_search_h__ -#include +#include "types.h" #include "dwi/tractography/SIFT/track_contribution.h" #include "dwi/tractography/SIFT/types.h" diff --git a/src/dwi/tractography/connectome/matrix.h b/src/dwi/tractography/connectome/matrix.h index 4a1341f1bc..d6a62dadbc 100644 --- a/src/dwi/tractography/connectome/matrix.h +++ b/src/dwi/tractography/connectome/matrix.h @@ -16,7 +16,8 @@ #define __dwi_tractography_connectome_matrix_h__ #include -#include + +#include "types.h" #include "connectome/connectome.h" #include "connectome/mat2vec.h" diff --git a/src/dwi/tractography/connectome/metric.h b/src/dwi/tractography/connectome/metric.h index 324ab0c588..c1f1c208d2 100644 --- a/src/dwi/tractography/connectome/metric.h +++ b/src/dwi/tractography/connectome/metric.h @@ -16,9 +16,8 @@ #define __dwi_tractography_connectome_metric_h__ -#include - #include "image.h" +#include "types.h" #include "algo/loop.h" #include "interp/linear.h" diff --git a/src/dwi/tractography/connectome/tck2nodes.h b/src/dwi/tractography/connectome/tck2nodes.h index d02ce2a7e5..03aee27730 100644 --- a/src/dwi/tractography/connectome/tck2nodes.h +++ b/src/dwi/tractography/connectome/tck2nodes.h @@ -16,9 +16,8 @@ #define __dwi_tractography_connectome_tck2nodes_h__ -#include - #include "image.h" +#include "types.h" #include "interp/linear.h" #include "interp/nearest.h" diff --git a/src/dwi/tractography/editing/loader.h b/src/dwi/tractography/editing/loader.h index b3f51bd817..e9c29884ba 100644 --- a/src/dwi/tractography/editing/loader.h +++ b/src/dwi/tractography/editing/loader.h @@ -17,9 +17,10 @@ #include -#include #include "memory.h" +#include "types.h" + #include "dwi/tractography/file.h" #include "dwi/tractography/properties.h" #include "dwi/tractography/streamline.h" diff --git a/src/dwi/tractography/editing/worker.h b/src/dwi/tractography/editing/worker.h index 65cfb5b2c2..0bb80c9b3f 100644 --- a/src/dwi/tractography/editing/worker.h +++ b/src/dwi/tractography/editing/worker.h @@ -17,7 +17,8 @@ #include -#include + +#include "types.h" #include "dwi/tractography/properties.h" #include "dwi/tractography/streamline.h" diff --git a/src/dwi/tractography/file.h b/src/dwi/tractography/file.h index f2b03039e2..0ddfa99179 100644 --- a/src/dwi/tractography/file.h +++ b/src/dwi/tractography/file.h @@ -16,7 +16,6 @@ #define __dwi_tractography_file_h__ #include -#include #include "app.h" #include "types.h" diff --git a/src/dwi/tractography/mapping/buffer_scratch_dump.h b/src/dwi/tractography/mapping/buffer_scratch_dump.h index cbfab95b0c..f6de0a378c 100644 --- a/src/dwi/tractography/mapping/buffer_scratch_dump.h +++ b/src/dwi/tractography/mapping/buffer_scratch_dump.h @@ -17,10 +17,12 @@ #include -#include -#include "file/ofstream.h" #include "image.h" +#include "types.h" + +#include "file/ofstream.h" + namespace MR { diff --git a/src/dwi/tractography/mapping/mapper.h b/src/dwi/tractography/mapping/mapper.h index eda3a720fe..e2eb91fc5c 100644 --- a/src/dwi/tractography/mapping/mapper.h +++ b/src/dwi/tractography/mapping/mapper.h @@ -16,12 +16,10 @@ #define __dwi_tractography_mapping_mapper_h__ - -#include - #include "image.h" -#include "transform.h" #include "thread_queue.h" +#include "transform.h" +#include "types.h" #include "dwi/directions/set.h" diff --git a/src/dwi/tractography/mapping/mapper_plugins.h b/src/dwi/tractography/mapping/mapper_plugins.h index ad268d73e1..0a9dcf9e6a 100644 --- a/src/dwi/tractography/mapping/mapper_plugins.h +++ b/src/dwi/tractography/mapping/mapper_plugins.h @@ -16,9 +16,8 @@ #define __dwi_tractography_mapping_mapper_plugins_h__ -#include - #include "image.h" +#include "types.h" #include "interp/linear.h" #include "math/SH.h" diff --git a/src/dwi/tractography/mapping/mapping.h b/src/dwi/tractography/mapping/mapping.h index b713d177a7..a0d386e184 100644 --- a/src/dwi/tractography/mapping/mapping.h +++ b/src/dwi/tractography/mapping/mapping.h @@ -15,10 +15,10 @@ #ifndef __dwi_tractography_mapping_mapping_h__ #define __dwi_tractography_mapping_mapping_h__ -#include - #include "header.h" #include "progressbar.h" +#include "types.h" + #include "dwi/tractography/properties.h" #include "dwi/tractography/file.h" diff --git a/src/dwi/tractography/resampling/downsampler.h b/src/dwi/tractography/resampling/downsampler.h index 62a753008b..64af49174b 100644 --- a/src/dwi/tractography/resampling/downsampler.h +++ b/src/dwi/tractography/resampling/downsampler.h @@ -16,8 +16,6 @@ #define __dwi_tractography_resampling_downsampler_h__ -#include - #include "dwi/tractography/tracking/generated_track.h" #include "dwi/tractography/resampling/resampling.h" diff --git a/src/dwi/tractography/resampling/resampling.cpp b/src/dwi/tractography/resampling/resampling.cpp index 647489fd88..9a47839fea 100644 --- a/src/dwi/tractography/resampling/resampling.cpp +++ b/src/dwi/tractography/resampling/resampling.cpp @@ -14,6 +14,8 @@ #include "dwi/tractography/resampling/resampling.h" +#include "types.h" + #include "dwi/tractography/resampling/arc.h" #include "dwi/tractography/resampling/downsampler.h" #include "dwi/tractography/resampling/endpoints.h" diff --git a/src/dwi/tractography/resampling/resampling.h b/src/dwi/tractography/resampling/resampling.h index 6bdfe79dbc..3d82e4a840 100644 --- a/src/dwi/tractography/resampling/resampling.h +++ b/src/dwi/tractography/resampling/resampling.h @@ -16,8 +16,6 @@ #define __dwi_tractography_resampling_resampling_h__ -#include - #include "app.h" #include "dwi/tractography/streamline.h" diff --git a/src/dwi/tractography/resampling/upsampler.h b/src/dwi/tractography/resampling/upsampler.h index c0d27c9b50..c7734ec159 100644 --- a/src/dwi/tractography/resampling/upsampler.h +++ b/src/dwi/tractography/resampling/upsampler.h @@ -16,8 +16,6 @@ #define __dwi_tractography_resampling_upsampler_h__ -#include - #include "dwi/tractography/resampling/resampling.h" diff --git a/src/dwi/tractography/seeding/list.h b/src/dwi/tractography/seeding/list.h index 8850369870..f5de9b7c1d 100644 --- a/src/dwi/tractography/seeding/list.h +++ b/src/dwi/tractography/seeding/list.h @@ -16,10 +16,9 @@ #define __dwi_tractography_seeding_list_h__ -#include "dwi/tractography/seeding/base.h" - -#include +#include "types.h" +#include "dwi/tractography/seeding/base.h" namespace MR diff --git a/src/dwi/tractography/streamline.h b/src/dwi/tractography/streamline.h index e0a3951c21..6b0d2c5602 100644 --- a/src/dwi/tractography/streamline.h +++ b/src/dwi/tractography/streamline.h @@ -16,8 +16,6 @@ #define __dwi_tractography_streamline_h__ -#include - #include "types.h" diff --git a/src/dwi/tractography/tracking/generated_track.h b/src/dwi/tractography/tracking/generated_track.h index c0c0172250..46a9ae3c93 100644 --- a/src/dwi/tractography/tracking/generated_track.h +++ b/src/dwi/tractography/tracking/generated_track.h @@ -16,7 +16,7 @@ #define __dwi_tractography_tracking_generated_track_h__ -#include +#include "types.h" #include "dwi/tractography/tracking/types.h" diff --git a/src/dwi/tractography/tracking/shared.h b/src/dwi/tractography/tracking/shared.h index d14a33ec1f..de6b8c2fc0 100644 --- a/src/dwi/tractography/tracking/shared.h +++ b/src/dwi/tractography/tracking/shared.h @@ -16,7 +16,6 @@ #define __dwi_tractography_tracking_shared_h__ #include -#include #include "header.h" #include "image.h" diff --git a/src/dwi/tractography/tracking/write_kernel.h b/src/dwi/tractography/tracking/write_kernel.h index 329725f5c8..f693ccb00f 100644 --- a/src/dwi/tractography/tracking/write_kernel.h +++ b/src/dwi/tractography/tracking/write_kernel.h @@ -15,9 +15,8 @@ #ifndef __dwi_tractography_tracking_write_kernel_h__ #define __dwi_tractography_tracking_write_kernel_h__ -#include -#include #include +#include #include "timer.h" #include "file/ofstream.h" diff --git a/src/gui/dialog/dicom.cpp b/src/gui/dialog/dicom.cpp index 2d908f756f..a8c794afce 100644 --- a/src/gui/dialog/dicom.cpp +++ b/src/gui/dialog/dicom.cpp @@ -45,7 +45,7 @@ namespace MR itemData = (str (p->size()) + " " + (p->modality.size() ? p->modality : std::string()) + " images " + format_time (p->time) + " " + (p->name.size() ? p->name : std::string ("unnamed")) + " (" + ( (*p) [0]->sequence_name.size() ? (*p) [0]->sequence_name : std::string ("?")) + - ") [" + str (p->number) + "]").c_str(); + ") [" + str (p->number) + "] " + p->image_type).c_str(); } ~Item() { qDeleteAll (childItems); diff --git a/src/gui/mrview/gui_image.cpp b/src/gui/mrview/gui_image.cpp index 3ee24f9ee2..4889cdbcb0 100644 --- a/src/gui/mrview/gui_image.cpp +++ b/src/gui/mrview/gui_image.cpp @@ -307,6 +307,8 @@ namespace MR void Image::update_texture3D () { + lookup_texture_4D_cache(); + // Binding also guarantees texture interpolation is updated bind(); @@ -417,8 +419,34 @@ namespace MR copy_texture_3D_complex(); min_max_set (); + update_texture_4D_cache (); } + + inline void Image::lookup_texture_4D_cache () + { + if (!volume_unchanged() && !texture_mode_changed) { + size_t vol_idx = image.index(3); + auto cached_tex = tex_4d_cache.find(vol_idx); + if (cached_tex != tex_4d_cache.end()) { + _texture.cache_copy (cached_tex->second); + tex_positions[3] = vol_idx; + } else { + _texture.cache_copy(GL::Texture()); + tex_positions[3] = -1; + } + + bind(); + } + } + + inline void Image::update_texture_4D_cache () + { + if (image.ndim() == 4) + tex_4d_cache[image.index(3)].cache_copy(_texture); + } + + // required to shut up clang's compiler warnings about std::abs() when // instantiating Image::copy_texture_3D() with unsigned types: template diff --git a/src/gui/mrview/gui_image.h b/src/gui/mrview/gui_image.h index ecad949784..6cba1f1552 100644 --- a/src/gui/mrview/gui_image.h +++ b/src/gui/mrview/gui_image.h @@ -21,6 +21,7 @@ #include "gui/mrview/volume.h" #include "interp/linear.h" #include "interp/nearest.h" +#include namespace MR @@ -87,6 +88,7 @@ namespace MR friend class Tool::ODF; std::array slice_min, slice_max; + std::unordered_map tex_4d_cache; private: bool volume_unchanged (); @@ -95,6 +97,8 @@ namespace MR template void copy_texture_3D (); void copy_texture_3D_complex (); + void lookup_texture_4D_cache (); + void update_texture_4D_cache (); vector _comments; diff --git a/src/gui/mrview/mode/lightbox.cpp b/src/gui/mrview/mode/lightbox.cpp index dcd0d601b5..8cf63c185a 100644 --- a/src/gui/mrview/mode/lightbox.cpp +++ b/src/gui/mrview/mode/lightbox.cpp @@ -24,23 +24,27 @@ namespace MR { bool LightBox::show_grid_lines(true); + bool LightBox::show_volumes(false); size_t LightBox::n_rows(3); size_t LightBox::n_cols(5); + size_t LightBox::volume_increment(1); float LightBox::slice_focus_increment(1.f); float LightBox::slice_focus_inc_adjust_rate(0.2f); std::string LightBox::prev_image_name; + ssize_t LightBox::current_slice_index((LightBox::n_rows*LightBox::n_cols) / 2); LightBox::LightBox () : layout_is_dirty(true), - current_slice_index((n_rows*n_cols) / 2), slices_proj_focusdelta(n_rows*n_cols, proj_focusdelta(projection, 0.f)) { Image* img = image(); if(!img || prev_image_name != img->header().name()) image_changed_event(); - else + else { + set_volume_increment(volume_increment); set_slice_increment(slice_focus_increment); + } } @@ -58,6 +62,13 @@ namespace MR updateGL(); } + void LightBox::set_volume_increment(size_t vol_inc) + { + volume_increment = vol_inc; + update_volume_indices(); + updateGL(); + } + void LightBox::set_slice_increment(float inc) { slice_focus_increment = inc; @@ -71,32 +82,58 @@ namespace MR updateGL(); } + void LightBox::set_show_volumes(bool show_vol) + { + show_volumes = show_vol; + if (show_vol) + update_volume_indices(); + else { + // Force focus update + ssize_t prev_index = current_slice_index; + current_slice_index = -1; + set_current_slice_index(prev_index); + } + updateGL(); + } + + inline bool LightBox::render_volumes() + { + return show_volumes && image () && image()->image.ndim() == 4; + } + inline void LightBox::update_layout() { // Can't use vector resize() because Projection needs to be assignable slices_proj_focusdelta = vector( n_cols * n_rows, proj_focusdelta(projection, 0.f)); - set_current_slice_index((n_rows * n_cols) / 2); + + update_volume_indices(); + + ssize_t slice_idx = std::min(current_slice_index, (n_rows * n_cols) - 1); + + set_current_slice_index(slice_idx); update_slices_focusdelta(); frame_VB.clear(); frame_VAO.clear(); } - void LightBox::set_current_slice_index(size_t slice_index) + void LightBox::set_current_slice_index(ssize_t slice_index) { - size_t prev_index = current_slice_index; + int prev_index = current_slice_index; current_slice_index = slice_index; - if(prev_index != current_slice_index) { + if (!render_volumes() && prev_index != (int)current_slice_index) { const Projection& slice_proj = slices_proj_focusdelta[current_slice_index].first; float focus_delta = slices_proj_focusdelta[current_slice_index].second; const Eigen::Vector3f slice_focus = move_in_out_displacement(focus_delta, slice_proj); set_focus(focus() + slice_focus); - update_slices_focusdelta(); - } + } else if (volume_indices[slice_index] == -1) + current_slice_index = prev_index; + + update_slices_focusdelta(); } void LightBox::update_slices_focusdelta() @@ -108,6 +145,35 @@ namespace MR } } + void LightBox::update_volume_indices() + { + bool is_4d = image () && image()->image.ndim() == 4; + volume_indices.resize (n_rows * n_cols, 0); + + if (!is_4d) + return; + + int n_vols = image()->image.size(3); + int current_vol = image()->image.index(3); + + int initial_vol = current_vol + (int)volume_increment *- (int)current_slice_index; + if (initial_vol < 0) + current_vol = volume_increment * current_slice_index; + + + for(int i = 0, N = volume_indices.size(); i < N; ++i) { + int vol_index = current_vol + (int)volume_increment * (i - (int)current_slice_index); + vol_index = vol_index < 0 || vol_index >= n_vols ? -1 : vol_index; + volume_indices[i] = vol_index; + } + + for (ssize_t i = current_slice_index; i >= 0; --i) { + current_slice_index = i; + if (volume_indices[i] >= 0) + break; + } + } + void LightBox::draw_plane_primitive (int axis, Displayable::Shader& shader_program, Projection& with_projection) { ASSERT_GL_MRVIEW_CONTEXT_IS_CURRENT; @@ -132,16 +198,20 @@ namespace MR const Eigen::Vector3f orig_focus = window().focus(); - if(layout_is_dirty) { + if (layout_is_dirty) { update_layout(); layout_is_dirty = false; } - size_t slice_idx = 0; + bool rend_vols = render_volumes(); + + ssize_t slice_idx = 0; for(size_t row = 0; row < n_rows; ++row) { for(size_t col = 0; col < n_cols; ++col, ++slice_idx) { Projection& slice_proj = slices_proj_focusdelta[slice_idx].first; + bool render_plane = true; + // Place the first slice in the top-left corner slice_proj.set_viewport(window(), x + dw * col, y + h - (dh * (row+1)), dw, dh); @@ -149,11 +219,21 @@ namespace MR // because move_in_out_displacement is reliant on MVP setup_projection (plane(), slice_proj); - float focus_delta = slices_proj_focusdelta[slice_idx].second; - Eigen::Vector3f slice_focus = move_in_out_displacement(focus_delta, slice_proj); - set_focus(orig_focus + slice_focus); + if (rend_vols) { + if (volume_indices[slice_idx] != -1) + image()->image.index(3) = volume_indices[slice_idx]; + else + render_plane = false; + } + + else { + float focus_delta = slices_proj_focusdelta[slice_idx].second; + Eigen::Vector3f slice_focus = move_in_out_displacement(focus_delta, slice_proj); + set_focus(orig_focus + slice_focus); + } - draw_plane_primitive(plane(), slice_shader, slice_proj); + if (render_plane) + draw_plane_primitive(plane(), slice_shader, slice_proj); if(slice_idx == current_slice_index) { // Drawing plane may alter the depth test state @@ -166,6 +246,8 @@ namespace MR } // Restore view state + if(rend_vols && volume_indices[current_slice_index] >= 0) + image()->image.index(3) = volume_indices[current_slice_index]; set_focus(orig_focus); projection.set_viewport(window(), x, y, w, h); @@ -278,6 +360,8 @@ namespace MR { Base::image_changed_event(); + update_volume_indices(); + if(image()) { const auto& header = image()->header(); if (prev_image_name.empty()) { diff --git a/src/gui/mrview/mode/lightbox.h b/src/gui/mrview/mode/lightbox.h index 6d12e37659..277ec7388d 100644 --- a/src/gui/mrview/mode/lightbox.h +++ b/src/gui/mrview/mode/lightbox.h @@ -44,20 +44,27 @@ namespace MR static size_t get_rows() { return n_rows; } static size_t get_cols() { return n_cols; } + static size_t get_volume_increment() { return volume_increment; } static float get_slice_increment() { return slice_focus_increment; } static float get_slice_inc_adjust_rate() { return slice_focus_inc_adjust_rate; } static bool get_show_grid() { return show_grid_lines; } + static bool get_show_volumes() { return show_volumes; } void set_rows(size_t rows); void set_cols(size_t cols); + void set_volume_increment(size_t vol_inc); void set_slice_increment(float inc); void set_show_grid(bool show_grid); + void set_show_volumes(bool show_vol); public slots: void nrows_slot(int value) { set_rows(static_cast(value)); } void ncolumns_slot(int value) { set_cols(static_cast(value));} - void slice_inc_slot(float value) { set_slice_increment(value);} + void slice_inc_slot(float value) { set_slice_increment(value); } + void volume_inc_slot(int value) { set_volume_increment(value); } void show_grid_slot (bool value) { set_show_grid(value); } + void show_volumes_slot (bool value) { set_show_volumes(value); } + void image_volume_changed_slot() { update_volume_indices(); } protected: void draw_plane_primitive(int axis, Displayable::Shader& shader_program, @@ -70,19 +77,22 @@ namespace MR } void update_layout(); + void update_volume_indices(); void update_slices_focusdelta(); - void set_current_slice_index(size_t slice_index); + void set_current_slice_index(ssize_t slice_index); void draw_grid(); + bool render_volumes(); // Want layout state to persist even after instance is destroyed - static bool show_grid_lines; + static bool show_grid_lines, show_volumes; static std::string prev_image_name; - static size_t n_rows, n_cols; + static size_t n_rows, n_cols, volume_increment; static float slice_focus_increment; static float slice_focus_inc_adjust_rate; bool layout_is_dirty; - size_t current_slice_index; + static ssize_t current_slice_index; + vector volume_indices; vector slices_proj_focusdelta; GL::VertexBuffer frame_VB; diff --git a/src/gui/mrview/tool/connectome/connectome.h b/src/gui/mrview/tool/connectome/connectome.h index 634c150595..957f5ecd3e 100644 --- a/src/gui/mrview/tool/connectome/connectome.h +++ b/src/gui/mrview/tool/connectome/connectome.h @@ -16,10 +16,10 @@ #define __gui_mrview_tool_connectome_connectome_h__ #include -#include #include "bitset.h" #include "image.h" +#include "types.h" #include "gui/opengl/gl.h" #include "gui/opengl/lighting.h" diff --git a/src/gui/mrview/tool/connectome/node.cpp b/src/gui/mrview/tool/connectome/node.cpp index 91703c32f8..2d3f097e5e 100644 --- a/src/gui/mrview/tool/connectome/node.cpp +++ b/src/gui/mrview/tool/connectome/node.cpp @@ -15,9 +15,8 @@ #include "gui/mrview/window.h" #include "gui/mrview/tool/connectome/node.h" -#include - #include "exception.h" +#include "types.h" #include "gui/mrview/window.h" namespace MR diff --git a/src/gui/mrview/tool/connectome/node_overlay.cpp b/src/gui/mrview/tool/connectome/node_overlay.cpp index 7f4f362a98..d2c6935dcf 100644 --- a/src/gui/mrview/tool/connectome/node_overlay.cpp +++ b/src/gui/mrview/tool/connectome/node_overlay.cpp @@ -15,7 +15,8 @@ #include "gui/mrview/tool/connectome/node_overlay.h" #include -#include + +#include "types.h" namespace MR { diff --git a/src/gui/mrview/tool/fixel/base_fixel.cpp b/src/gui/mrview/tool/fixel/base_fixel.cpp index 0084937d0d..a21333dae5 100644 --- a/src/gui/mrview/tool/fixel/base_fixel.cpp +++ b/src/gui/mrview/tool/fixel/base_fixel.cpp @@ -118,35 +118,37 @@ namespace MR "void main() {\n"; if (fixel.use_discard_lower()) - source += " if (v_threshold[0] < lower) return;\n"; + source += " if (v_threshold[0] < lower || isnan(v_threshold[0])) return;\n"; if (fixel.use_discard_upper()) - source += " if (v_threshold[0] > upper) return;\n"; + source += " if (v_threshold[0] > upper || isnan(v_threshold[0])) return;\n"; switch (scale_type) { case Unity: - source += " vec4 line_offset = length_mult * vec4 (v_dir[0], 0);\n"; + source += " vec4 line_offset = length_mult * vec4 (v_dir[0], 0);\n"; break; case Value: - source += " vec4 line_offset = length_mult * v_scale[0] * vec4 (v_dir[0], 0);\n"; + source += " if (isnan(v_scale[0])) return;\n" + " vec4 line_offset = length_mult * v_scale[0] * vec4 (v_dir[0], 0);\n"; break; } switch (color_type) { case CValue: if (!ColourMap::maps[colourmap].special) { - source += " float amplitude = clamp ("; + source += " if (isnan(v_colour[0])) return;\n" + " float amplitude = clamp ("; if (fixel.scale_inverted()) source += "1.0 -"; source += " scale * (v_colour[0] - offset), 0.0, 1.0);\n"; } source += - std::string (" vec3 color;\n") + + std::string (" vec3 color;\n") + ColourMap::maps[colourmap].glsl_mapping + - " fColour = color;\n"; + " fColour = color;\n"; break; case Direction: source += - " fColour = normalize (abs (v_dir[0]));\n"; + " fColour = normalize (abs (v_dir[0]));\n"; break; default: break; diff --git a/src/gui/mrview/tool/odf/model.h b/src/gui/mrview/tool/odf/model.h index 39e812ea8d..8bd721bb03 100644 --- a/src/gui/mrview/tool/odf/model.h +++ b/src/gui/mrview/tool/odf/model.h @@ -17,7 +17,8 @@ #include #include -#include + +#include "types.h" #include "gui/mrview/tool/odf/item.h" #include "gui/mrview/tool/odf/type.h" diff --git a/src/gui/mrview/tool/roi_editor/item.cpp b/src/gui/mrview/tool/roi_editor/item.cpp index 76ae50a7c0..df60e20417 100644 --- a/src/gui/mrview/tool/roi_editor/item.cpp +++ b/src/gui/mrview/tool/roi_editor/item.cpp @@ -14,7 +14,6 @@ #include #include -#include #include "gui/mrview/tool/roi_editor/item.h" diff --git a/src/gui/mrview/tool/roi_editor/item.h b/src/gui/mrview/tool/roi_editor/item.h index 3c1fe25824..bd6e7914c7 100644 --- a/src/gui/mrview/tool/roi_editor/item.h +++ b/src/gui/mrview/tool/roi_editor/item.h @@ -17,9 +17,9 @@ #include -#include #include "header.h" +#include "types.h" #include "algo/loop.h" #include "gui/mrview/volume.h" #include "gui/mrview/tool/roi_editor/undoentry.h" diff --git a/src/gui/mrview/tool/roi_editor/roi.h b/src/gui/mrview/tool/roi_editor/roi.h index 7d833096c7..aaf386c175 100644 --- a/src/gui/mrview/tool/roi_editor/roi.h +++ b/src/gui/mrview/tool/roi_editor/roi.h @@ -15,10 +15,9 @@ #ifndef __gui_mrview_tool_roi_editor_roi_h__ #define __gui_mrview_tool_roi_editor_roi_h__ -#include - #include "memory.h" #include "transform.h" +#include "types.h" #include "gui/mrview/mode/base.h" #include "gui/mrview/tool/base.h" diff --git a/src/gui/mrview/tool/roi_editor/undoentry.cpp b/src/gui/mrview/tool/roi_editor/undoentry.cpp index 95dbf21f4e..8a7f55b6e6 100644 --- a/src/gui/mrview/tool/roi_editor/undoentry.cpp +++ b/src/gui/mrview/tool/roi_editor/undoentry.cpp @@ -13,7 +13,6 @@ #include -#include #include "gui/mrview/window.h" #include "gui/mrview/tool/roi_editor/item.h" diff --git a/src/gui/mrview/tool/roi_editor/undoentry.h b/src/gui/mrview/tool/roi_editor/undoentry.h index 49fed88e4c..99cd3a351e 100644 --- a/src/gui/mrview/tool/roi_editor/undoentry.h +++ b/src/gui/mrview/tool/roi_editor/undoentry.h @@ -17,7 +17,8 @@ #include #include -#include + +#include "types.h" #include "gui/opengl/shader.h" #include "gui/opengl/gl.h" diff --git a/src/gui/mrview/tool/tractography/track_scalar_file.cpp b/src/gui/mrview/tool/tractography/track_scalar_file.cpp index a90aa609c4..9bebbd598b 100644 --- a/src/gui/mrview/tool/tractography/track_scalar_file.cpp +++ b/src/gui/mrview/tool/tractography/track_scalar_file.cpp @@ -246,6 +246,14 @@ namespace MR bool TrackScalarFileOptions::open_intensity_track_scalar_file_slot () { std::string scalar_file = Dialog::File::get_file (this, "Select scalar text file or Track Scalar file (.tsf) to open", ""); + return open_intensity_track_scalar_file_slot(scalar_file); + } + + + + + bool TrackScalarFileOptions::open_intensity_track_scalar_file_slot(std::string scalar_file) + { if (!scalar_file.empty()) { try { tractogram->load_intensity_track_scalars (scalar_file); @@ -261,7 +269,6 @@ namespace MR return scalar_file.size(); } - void TrackScalarFileOptions::show_colour_bar_slot () { if (tractogram) { @@ -284,6 +291,35 @@ namespace MR } + + void TrackScalarFileOptions::set_threshold(GUI::MRView::Tool::TrackThresholdType dataSource, default_type min, default_type max)//TrackThresholdType dataSource + { + if (tractogram) { + //Source + tractogram->set_threshold_type(dataSource); + //Range + if (dataSource != TrackThresholdType::None) + { + tractogram->lessthan = min; + tractogram->greaterthan = max; + threshold_lower_box->setChecked(true); + threshold_upper_box->setChecked(true); + } + + update_UI(); + window().updateGL(); + } + } + + void TrackScalarFileOptions::set_scaling(default_type min, default_type max) + { + if (tractogram) { + tractogram->set_windowing(min,max); + update_UI(); + window().updateGL(); + } + } + void TrackScalarFileOptions::on_set_scaling_slot () { if (tractogram) { diff --git a/src/gui/mrview/tool/tractography/track_scalar_file.h b/src/gui/mrview/tool/tractography/track_scalar_file.h index 4a7472f389..f38e2dbcc8 100644 --- a/src/gui/mrview/tool/tractography/track_scalar_file.h +++ b/src/gui/mrview/tool/tractography/track_scalar_file.h @@ -18,6 +18,8 @@ #include "gui/mrview/adjust_button.h" #include "gui/mrview/displayable.h" #include "gui/mrview/tool/base.h" +#include "gui/mrview/tool/tractography/tractogram_enums.h" + namespace MR @@ -29,7 +31,6 @@ namespace MR namespace Tool { - class Tractogram; class TrackScalarFileOptions : public QGroupBox, public DisplayableVisitor @@ -45,9 +46,14 @@ namespace MR void render_tractogram_colourbar (const Tool::Tractogram&) override; void update_UI(); + void set_scaling(default_type min, default_type max); + void set_threshold(GUI::MRView::Tool::TrackThresholdType dataSource, default_type min, default_type max); + public slots: bool open_intensity_track_scalar_file_slot (); + bool open_intensity_track_scalar_file_slot(std::string); + private slots: void show_colour_bar_slot(); diff --git a/src/gui/mrview/tool/tractography/tractogram.cpp b/src/gui/mrview/tool/tractography/tractogram.cpp index ddc58c7651..eb86bbe666 100644 --- a/src/gui/mrview/tool/tractography/tractogram.cpp +++ b/src/gui/mrview/tool/tractography/tractogram.cpp @@ -39,7 +39,7 @@ namespace MR { const Tractogram& tractogram = dynamic_cast(displayable); - std::string source = + std::string source = "layout (location = 0) in vec3 vertex;\n" "layout (location = 1) in vec3 prev_vertex;\n" "layout (location = 2) in vec3 next_vertex;\n"; @@ -140,7 +140,7 @@ namespace MR source += "out vec3 g_tangent;\n"; if (color_type == TrackColourType::ScalarFile || color_type == TrackColourType::Ends) - source += + source += "in vec3 v_colour[];\n" "out vec3 fColour;\n"; @@ -151,8 +151,8 @@ namespace MR source += "void main() {\n"; - if (do_crop_to_slab) - source += + if (do_crop_to_slab) + source += " if (v_include[0] < 0.0 && v_include[1] < 0.0) return;\n" " if (v_include[0] > 1.0 && v_include[1] > 1.0) return;\n"; @@ -168,7 +168,7 @@ namespace MR if (use_lighting) source += " g_height = 0.0;\n"; - source += + source += " gl_Position = gl_in[0].gl_Position - vec4(v_end[0],0,0);\n" " EmitVertex();\n"; @@ -196,7 +196,7 @@ namespace MR if (use_lighting) source += " g_height = PI;\n"; - source += + source += " gl_Position = gl_in[1].gl_Position + vec4 (v_end[1],0,0);\n" " EmitVertex();\n" "}\n"; @@ -224,7 +224,7 @@ namespace MR source += "in float g_amp;\n"; if (use_lighting) - source += + source += "uniform float ambient, diffuse, specular, shine;\n" "uniform vec3 light_pos;\n" "in float g_height;\n"; @@ -304,7 +304,7 @@ namespace MR - void Tractogram::Shader::update (const Displayable& object) + void Tractogram::Shader::update (const Displayable& object) { const Tractogram& tractogram (dynamic_cast (object)); do_crop_to_slab = tractogram.tractography_tool.crop_to_slab(); @@ -578,10 +578,10 @@ namespace MR file.close(); ASSERT_GL_MRVIEW_CONTEXT_IS_CURRENT; } - - - - + + + + void Tractogram::load_end_colours() { // These data are now retained in memory - no need to re-scan track file @@ -794,9 +794,9 @@ namespace MR ASSERT_GL_MRVIEW_CONTEXT_IS_CURRENT; } - - - + + + void Tractogram::erase_colour_data() { MRView::GrabContext context; @@ -899,11 +899,7 @@ namespace MR tck_count = 0; ASSERT_GL_MRVIEW_CONTEXT_IS_CURRENT; } - - - - - + void Tractogram::load_end_colours_onto_GPU (vector& buffer) { ASSERT_GL_MRVIEW_CONTEXT_IS_CURRENT; diff --git a/src/gui/mrview/tool/tractography/tractogram.h b/src/gui/mrview/tool/tractography/tractogram.h index 733df94ca1..41afa4ca48 100644 --- a/src/gui/mrview/tool/tractography/tractogram.h +++ b/src/gui/mrview/tool/tractography/tractogram.h @@ -15,6 +15,7 @@ #ifndef __gui_mrview_tool_tractogram_h__ #define __gui_mrview_tool_tractogram_h__ +//#include "gui/mrview/tool/tractography/tractogram_enums.h" #include "gui/mrview/displayable.h" #include "dwi/tractography/properties.h" #include "gui/mrview/tool/tractography/tractography.h" @@ -35,9 +36,6 @@ namespace MR namespace Tool { - enum class TrackColourType { Direction, Ends, Manual, ScalarFile }; - enum class TrackThresholdType { None, UseColourFile, SeparateFile }; - class Tractogram : public Displayable { MEMALIGN(Tractogram) Q_OBJECT @@ -153,7 +151,7 @@ namespace MR vector& starts, vector& sizes, size_t& tck_count); - + void load_end_colours_onto_GPU (vector&); void load_intensity_scalars_onto_GPU (vector& buffer); diff --git a/src/gui/mrview/tool/tractography/tractogram_enums.h b/src/gui/mrview/tool/tractography/tractogram_enums.h new file mode 100644 index 0000000000..8de9c312dd --- /dev/null +++ b/src/gui/mrview/tool/tractography/tractogram_enums.h @@ -0,0 +1,35 @@ +/* Copyright (c) 2008-2017 the MRtrix3 contributors. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + * + * MRtrix is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * For more details, see http://www.mrtrix.org/. + */ + + +#ifndef __gui_mrview_tool_tractogram_enums_h__ +#define __gui_mrview_tool_tractogram_enums_h__ + +namespace MR +{ + namespace GUI + { + namespace MRView + { + namespace Tool + { + enum class TrackColourType { Direction, Ends, Manual, ScalarFile }; + enum class TrackThresholdType { None, UseColourFile, SeparateFile }; + } + } + } +} + + +#endif + diff --git a/src/gui/mrview/tool/tractography/tractography.cpp b/src/gui/mrview/tool/tractography/tractography.cpp index 1c8482350a..ba142daf11 100644 --- a/src/gui/mrview/tool/tractography/tractography.cpp +++ b/src/gui/mrview/tool/tractography/tractography.cpp @@ -182,7 +182,7 @@ namespace MR general_opt_grid->addWidget (new QLabel ("line thickness"), 1, 0); general_opt_grid->addWidget (thickness_slider, 1, 1); - QGroupBox* slab_group_box = new QGroupBox (tr("crop to slab")); + slab_group_box = new QGroupBox (tr("crop to slab")); slab_group_box->setCheckable (true); slab_group_box->setChecked (true); general_opt_grid->addWidget (slab_group_box, 4, 0, 1, 2); @@ -213,7 +213,7 @@ namespace MR main_box->addWidget (general_groupbox, 0); - lighting = new GL::Lighting (parent); + lighting = new GL::Lighting (parent); lighting->diffuse = 0.8; lighting->shine = 5.0; connect (lighting, SIGNAL (changed()), SLOT (hide_all_slot())); @@ -287,19 +287,33 @@ namespace MR void Tractography::tractogram_open_slot () { + vector list = Dialog::File::get_files (this, "Select tractograms to open", "Tractograms (*.tck)"); + add_tractogram(list); + } + + + + + + void Tractography::add_tractogram (vector& list) + { if (list.empty()) - return; + { return; } try { tractogram_list_model->add_items (list, *this); - window().updateGL(); + select_last_added_tractogram(); } catch (Exception& E) { E.display(); } + } + + + void Tractography::dropEvent (QDropEvent* event) { static constexpr int max_files = 32; @@ -336,7 +350,7 @@ namespace MR } - void Tractography::toggle_shown_slot (const QModelIndex& index, const QModelIndex& index2) + void Tractography::toggle_shown_slot (const QModelIndex& index, const QModelIndex& index2) { if (index.row() == index2.row()) { tractogram_list_view->setCurrentIndex(index); @@ -443,8 +457,8 @@ namespace MR update_scalar_options(); window().updateGL(); } - - + + void Tractography::colour_track_by_ends_slot() { QModelIndexList indices = tractogram_list_view->selectionModel()->selectedIndexes(); @@ -532,6 +546,7 @@ namespace MR QMessageBox::Ok); return; } + Tractogram* tractogram = tractogram_list_model->get_tractogram (indices[0]); scalar_file_options->set_tractogram (tractogram); if (tractogram->intensity_scalar_filename.empty()) { @@ -656,8 +671,8 @@ namespace MR - void Tractography::add_commandline_options (MR::App::OptionList& options) - { + void Tractography::add_commandline_options (MR::App::OptionList& options) + { using namespace MR::App; options + OptionGroup ("Tractography tool options") @@ -670,26 +685,106 @@ namespace MR + Option ("tractography.opacity", "Opacity of tractography display, [0.0, 1.0], default is 1.0.").allow_multiple() + Argument("value").type_float ( 0.0, 1.0 ) + + + Option("tractography.slab", "Slab thickness of tractography display, in mm. -1 to turn off crop to slab.").allow_multiple() + + Argument("value").type_float(-1, 1e6) + + + Option ("tractography.tsf_load", "Load the specified tractography scalar file.").allow_multiple() + + Argument("tsf").type_file_in() + + + Option ("tractography.tsf_range", "Set range for the tractography scalar file. Requires tractography.tsf_load already provided. RangeMin,RangeMax").allow_multiple() + + Argument("range").type_sequence_float() + + + Option ("tractography.tsf_thresh", "Set thresholds for the tractography scalar file. Requires tractography.tsf_load already provided. ThresholdMin,ThesholdMax").allow_multiple() + + Argument("thresh").type_sequence_float() ; - + + } - bool Tractography::process_commandline_option (const MR::App::ParsedOption& opt) + /* + Selects the last tractogram in the tractogram_list_view and updates the window. If no tractograms are in the list view, no action is taken. + */ + void Tractography::select_last_added_tractogram() + { + int count = tractogram_list_model->rowCount(); + if(count != 0){ + QModelIndex index = tractogram_list_view->model()->index(count-1, 0); + tractogram_list_view->setCurrentIndex(index); + window().updateGL(); + } + } + + + + + + bool Tractography::process_commandline_option (const MR::App::ParsedOption& opt) { - if (opt.opt->is ("tractography.load")) { + + if (opt.opt->is ("tractography.load")) + { vector list (1, std::string(opt[0])); - try { - tractogram_list_model->add_items (list, *this); - window().updateGL(); + add_tractogram(list); + return true; + } + + + if (opt.opt->is ("tractography.tsf_load")) + { + try { + + if (process_commandline_option_tsf_check_tracto_loaded()) { + QModelIndexList indices = tractogram_list_view->selectionModel()->selectedIndexes(); + + if (indices.size() == 1) {//just in case future edits break this assumption + Tractogram* tractogram = tractogram_list_model->get_tractogram (indices[0]); + + //set its tsf filename and load the tsf file + scalar_file_options->set_tractogram (tractogram); + scalar_file_options->open_intensity_track_scalar_file_slot (std::string(opt[0])); + + //Set the GUI to use the file for visualisation + colour_combobox->setCurrentIndex(4); // Set combobox to "File" + } + } } catch (Exception& E) { E.display(); } + + return true; + } + + if (opt.opt->is ("tractography.tsf_range")) + { + try { + //Set the tsf visualisation range + vector range; + if (process_commandline_option_tsf_option(opt,2, range)) + scalar_file_options->set_scaling (range[0], range[1]); + } + catch (Exception& E) { E.display(); } + return true; + } + + + if (opt.opt->is ("tractography.tsf_thresh")) + { + try { + //Set the tsf visualisation threshold + vector range; + if (process_commandline_option_tsf_option(opt,2, range)) + scalar_file_options->set_threshold (TrackThresholdType::UseColourFile,range[0], range[1]); + } + catch(Exception& E) { E.display(); } return true; } + + if (opt.opt->is ("tractography.thickness")) { - // Thickness runs from -1000 to 1000, + // Thickness runs from -1000 to 1000, float thickness = float(opt[0]) * 1000.0f; - try { + try { thickness_slider->setValue(thickness); } catch (Exception& E) { E.display(); } @@ -706,11 +801,72 @@ namespace MR return true; } + if (opt.opt->is("tractography.slab")) { + float thickness = opt[0]; + try { + bool crop = thickness > 0; + slab_group_box->setChecked(crop); + on_crop_to_slab_slot(crop);//Needs to be manually bumped + if(crop) + { + slab_entry->setValue(thickness); + on_slab_thickness_slot();//Needs to be manually bumped + } + } + catch (Exception& E) { E.display(); } + return true; + } + return false; } + + + + /*Checks whether any tractography has been loaded and warns the user if it has not*/ + bool Tractography::process_commandline_option_tsf_check_tracto_loaded() + { + int count = tractogram_list_model->rowCount(); + if (count == 0){ + //Error to std error to save many dialogs appearing for a single missed argument + std::cerr << "TSF argument specified but no tractography loaded. Ensure TSF arguments follow the tractography.load argument.\n"; + } + return count != 0; + } + + + + + + + /*Checks whether legal to apply tsf options and prepares the scalar_file_options to do so. Returns the vector of floats parsed from the options, or null on fail*/ + bool Tractography::process_commandline_option_tsf_option(const MR::App::ParsedOption& opt, uint reqArgSize, vector& range) + { + if(process_commandline_option_tsf_check_tracto_loaded()){ + QModelIndexList indices = tractogram_list_view->selectionModel()->selectedIndexes(); + range = opt[0].as_sequence_float(); + if(indices.size() == 1 && range.size() == reqArgSize){ + //values supplied + Tractogram* tractogram = tractogram_list_model->get_tractogram (indices[0]); + if(tractogram->get_color_type() == TrackColourType::ScalarFile){ + //prereq options supplied/executed + scalar_file_options->set_tractogram(tractogram); + return true; + } + else + { + std::cerr << "Could not apply TSF argument - tractography.load_tsf not supplied.\n"; + } + } + else + { + std::cerr << "Could not apply TSF argument - insufficient number of arguments provided.\n"; + } + } + return false; + } } } } diff --git a/src/gui/mrview/tool/tractography/tractography.h b/src/gui/mrview/tool/tractography/tractography.h index 8a7968b422..6f35bc1c13 100644 --- a/src/gui/mrview/tool/tractography/tractography.h +++ b/src/gui/mrview/tool/tractography/tractography.h @@ -101,11 +101,16 @@ namespace MR TrackScalarFileOptions *scalar_file_options; LightingDock *lighting_dock; + QGroupBox* slab_group_box; QSlider* opacity_slider; QSlider* thickness_slider; - + void dropEvent (QDropEvent* event) override; void update_scalar_options(); + void add_tractogram (vector& list); + void select_last_added_tractogram(); + bool process_commandline_option_tsf_check_tracto_loaded (); + bool process_commandline_option_tsf_option (const MR::App::ParsedOption&, uint, vector& range); }; } diff --git a/src/gui/mrview/tool/view.cpp b/src/gui/mrview/tool/view.cpp index 34fe085c59..d244ef93ed 100644 --- a/src/gui/mrview/tool/view.cpp +++ b/src/gui/mrview/tool/view.cpp @@ -509,6 +509,8 @@ namespace MR setEnabled (image); + reset_light_box_gui_controls(); + if (!image) return; @@ -973,6 +975,10 @@ namespace MR reset_light_box_gui_controls(); } + void View::light_box_toggle_volumes_slot(bool) + { + reset_light_box_gui_controls(); + } @@ -981,6 +987,7 @@ namespace MR using LightBoxEditButton = MRView::Mode::LightBoxViewControls::LightBoxEditButton; light_box_slice_inc = new AdjustButton(this); + light_box_volume_inc = new LightBoxEditButton(this); light_box_rows = new LightBoxEditButton(this); light_box_cols = new LightBoxEditButton(this); @@ -991,31 +998,51 @@ namespace MR GridLayout* grid_layout = new GridLayout; lightbox_box->setLayout(grid_layout); - grid_layout->addWidget(new QLabel (tr("Slice increment (mm):")), 0, 1); - grid_layout->addWidget(light_box_slice_inc, 0, 2); + light_box_slice_inc_label = new QLabel (tr("Slice increment (mm):")); + grid_layout->addWidget(light_box_slice_inc_label, 1, 0); + grid_layout->addWidget(light_box_slice_inc, 1, 2); + light_box_volume_inc_label = new QLabel (tr("Volume increment:")); + grid_layout->addWidget(light_box_volume_inc_label, 1, 0); + grid_layout->addWidget(light_box_volume_inc, 1, 2); - grid_layout->addWidget(new QLabel (tr("Rows:")), 1, 1); - grid_layout->addWidget(light_box_rows, 1, 2); + grid_layout->addWidget(new QLabel (tr("Rows:")), 2, 0); + grid_layout->addWidget(light_box_rows, 2, 2); - grid_layout->addWidget (new QLabel (tr("Columns:")), 2, 1); - grid_layout->addWidget(light_box_cols, 2, 2); + grid_layout->addWidget (new QLabel (tr("Columns:")), 3, 0); + grid_layout->addWidget(light_box_cols, 3, 2); + + light_box_show_4d = new QCheckBox(tr("Cycle through volumes"), this); + grid_layout->addWidget(light_box_show_4d, 4, 0, 1, 2); light_box_show_grid = new QCheckBox(tr("Show grid"), this); - grid_layout->addWidget(light_box_show_grid, 3, 0, 1, 2); + grid_layout->addWidget(light_box_show_grid, 5, 0, 1, 2); } - - void View::reset_light_box_gui_controls() { - light_box_rows->setValue(static_cast(Mode::LightBox::get_rows())); - light_box_cols->setValue(static_cast(Mode::LightBox::get_cols())); - light_box_slice_inc->setValue(Mode::LightBox::get_slice_increment()); - light_box_slice_inc->setRate(Mode::LightBox::get_slice_inc_adjust_rate()); - light_box_show_grid->setChecked(Mode::LightBox::get_show_grid()); + if (!lightbox_box) + return; + + bool img_4d = window ().image() && window ().image()->image.ndim() == 4; + bool show_volumes = Mode::LightBox::get_show_volumes (); + bool can_show_vol = img_4d && show_volumes; + + light_box_rows->setValue (static_cast(Mode::LightBox::get_rows ())); + light_box_cols->setValue (static_cast(Mode::LightBox::get_cols ())); + light_box_slice_inc->setValue (Mode::LightBox::get_slice_increment ()); + light_box_slice_inc->setRate (Mode::LightBox::get_slice_inc_adjust_rate ()); + light_box_volume_inc->setValue (Mode::LightBox::get_volume_increment ()); + light_box_show_grid->setChecked (Mode::LightBox::get_show_grid ()); + + light_box_show_4d->setEnabled (img_4d); + light_box_show_4d->setChecked (can_show_vol); + light_box_slice_inc_label->setVisible (!can_show_vol); + light_box_slice_inc->setVisible (!can_show_vol); + light_box_volume_inc_label->setVisible (can_show_vol); + light_box_volume_inc->setVisible (can_show_vol); } @@ -1031,7 +1058,11 @@ namespace MR connect(light_box_rows, SIGNAL (valueChanged(int)), &mode, SLOT (nrows_slot(int))); connect(light_box_cols, SIGNAL (valueChanged(int)), &mode, SLOT (ncolumns_slot(int))); connect(light_box_slice_inc, SIGNAL (valueChanged(float)), &mode, SLOT (slice_inc_slot(float))); + connect(light_box_volume_inc, SIGNAL (valueChanged(int)), &mode, SLOT (volume_inc_slot(int))); connect(light_box_show_grid, SIGNAL (toggled(bool)), &mode, SLOT (show_grid_slot(bool))); + connect(light_box_show_4d, SIGNAL (toggled(bool)), &mode, SLOT (show_volumes_slot(bool))); + connect(light_box_show_4d, SIGNAL (toggled(bool)), this, SLOT (light_box_toggle_volumes_slot(bool))); + connect(&window(), SIGNAL (volumeChanged(size_t)), &mode, SLOT (image_volume_changed_slot())); reset_light_box_gui_controls(); } diff --git a/src/gui/mrview/tool/view.h b/src/gui/mrview/tool/view.h index f1253e48e4..f781d5e657 100644 --- a/src/gui/mrview/tool/view.h +++ b/src/gui/mrview/tool/view.h @@ -101,6 +101,7 @@ namespace MR void clip_planes_clear_slot (); void light_box_slice_inc_reset_slot (); + void light_box_toggle_volumes_slot (bool); private: QPushButton *hide_button; @@ -120,9 +121,11 @@ namespace MR QAction *clip_planes_new_axial_action, *clip_planes_new_sagittal_action, *clip_planes_new_coronal_action; QAction *clip_planes_reset_axial_action, *clip_planes_reset_sagittal_action, *clip_planes_reset_coronal_action; QAction *clip_planes_invert_action, *clip_planes_remove_action, *clip_planes_clear_action; - AdjustButton* light_box_slice_inc; - SpinBox *light_box_rows, *light_box_cols; - QCheckBox *light_box_show_grid; + + QLabel *light_box_slice_inc_label, *light_box_volume_inc_label; + AdjustButton *light_box_slice_inc; + SpinBox *light_box_rows, *light_box_cols, *light_box_volume_inc; + QCheckBox *light_box_show_grid, *light_box_show_4d; class ClipPlaneModel; ClipPlaneModel* clip_planes_model; diff --git a/src/gui/opengl/gl.h b/src/gui/opengl/gl.h index c021728576..842e2f9419 100644 --- a/src/gui/opengl/gl.h +++ b/src/gui/opengl/gl.h @@ -114,8 +114,9 @@ namespace MR Texture () : id (0) { } ~Texture () { clear(); } Texture (const Texture&) : id (0) { } - Texture (Texture&& t) : id (t.id) { t.id = 0; } - Texture& operator= (Texture&& t) { clear(); id = t.id; t.id = 0; return *this; } + Texture (Texture&& t) : id (t.id), tex_type (t.tex_type) { t.id = 0; } + Texture& operator= (Texture&& t) { clear(); id = t.id; tex_type = t.tex_type; t.id = 0; return *this; } + void cache_copy(const Texture& t) { id = t.id; tex_type = t.tex_type; } operator GLuint () const { return id; } void gen (GLenum target, GLint interp_type = gl::LINEAR) { if (!id) { diff --git a/src/gui/shapes/cylinder.cpp b/src/gui/shapes/cylinder.cpp index 7aa321485b..c83f65507f 100644 --- a/src/gui/shapes/cylinder.cpp +++ b/src/gui/shapes/cylinder.cpp @@ -14,7 +14,7 @@ #include "gui/shapes/cylinder.h" -#include +#include "types.h" #include "math/math.h" diff --git a/src/gui/shapes/cylinder.h b/src/gui/shapes/cylinder.h index 16c29f9fd6..e75915e587 100644 --- a/src/gui/shapes/cylinder.h +++ b/src/gui/shapes/cylinder.h @@ -15,8 +15,6 @@ #ifndef __gui_shapes_cylinder_h__ #define __gui_shapes_cylinder_h__ -#include - #include "gui/opengl/gl.h" #include "gui/opengl/gl_core_3_3.h" diff --git a/src/gui/shapes/halfsphere.h b/src/gui/shapes/halfsphere.h index 737aa3565a..5c84b7d828 100644 --- a/src/gui/shapes/halfsphere.h +++ b/src/gui/shapes/halfsphere.h @@ -15,7 +15,7 @@ #ifndef __gui_shapes_halfsphere_h__ #define __gui_shapes_halfsphere_h__ -#include +#include "types.h" #include "gui/opengl/gl.h" #include "gui/opengl/gl_core_3_3.h" diff --git a/src/gui/shapes/sphere.h b/src/gui/shapes/sphere.h index f7ffe6f369..70608a642b 100644 --- a/src/gui/shapes/sphere.h +++ b/src/gui/shapes/sphere.h @@ -15,7 +15,7 @@ #ifndef __gui_shapes_sphere_h__ #define __gui_shapes_sphere_h__ -#include +#include "types.h" #include "gui/opengl/gl.h" #include "gui/opengl/gl_core_3_3.h" diff --git a/src/registration/linear.h b/src/registration/linear.h index 1f1a8357c9..f562694059 100644 --- a/src/registration/linear.h +++ b/src/registration/linear.h @@ -15,10 +15,11 @@ #ifndef __registration_linear_h__ #define __registration_linear_h__ -#include +#include #include "app.h" #include "image.h" +#include "types.h" #include "math/average_space.h" #include "filter/normalise.h" #include "filter/resize.h" @@ -36,7 +37,7 @@ // #include "math/check_gradient.h" #include "math/rng.h" #include "math/math.h" -#include + #include "registration/multi_resolution_lmax.h" namespace MR diff --git a/src/registration/nonlinear.h b/src/registration/nonlinear.h index 43444f23f0..0fb1a16e51 100644 --- a/src/registration/nonlinear.h +++ b/src/registration/nonlinear.h @@ -15,8 +15,9 @@ #ifndef __registration_nonlinear_h__ #define __registration_nonlinear_h__ -#include #include "image.h" +#include "types.h" + #include "filter/warp.h" #include "filter/resize.h" #include "registration/transform/reorient.h" diff --git a/src/registration/transform/search.h b/src/registration/transform/search.h index 52b4c1467f..4e23814013 100644 --- a/src/registration/transform/search.h +++ b/src/registration/transform/search.h @@ -15,17 +15,19 @@ #ifndef __registration_transform_search_h__ #define __registration_transform_search_h__ -#include #include #include #include +#include "debug.h" +#include "image.h" +#include "progressbar.h" +#include "types.h" + #include "math/math.h" #include "math/median.h" #include "math/rng.h" #include "math/gradient_descent.h" -#include "image.h" -#include "debug.h" #include "math/average_space.h" #include "filter/resize.h" #include "filter/reslice.h" @@ -40,7 +42,6 @@ #include "registration/metric/thread_kernel.h" #include "registration/transform/initialiser.h" #include "registration/transform/rigid.h" -#include "progressbar.h" #include "file/config.h" namespace MR diff --git a/src/stats/cfe.cpp b/src/stats/cfe.cpp index 79c6bb017b..83d21dbef5 100644 --- a/src/stats/cfe.cpp +++ b/src/stats/cfe.cpp @@ -23,13 +23,15 @@ namespace MR - TrackProcessor::TrackProcessor (Image& fixel_indexer, + TrackProcessor::TrackProcessor (Image& fixel_indexer, const vector& fixel_directions, + Image& fixel_mask, vector& fixel_TDI, - vector >& connectivity_matrix, + init_connectivity_matrix_type& connectivity_matrix, const value_type angular_threshold) : fixel_indexer (fixel_indexer) , fixel_directions (fixel_directions), + fixel_mask (fixel_mask), fixel_TDI (fixel_TDI), connectivity_matrix (connectivity_matrix), angular_threshold_dp (std::cos (angular_threshold * (Math::pi/180.0))) { } @@ -39,26 +41,30 @@ namespace MR bool TrackProcessor::operator() (const SetVoxelDir& in) { // For each voxel tract tangent, assign to a fixel - vector tract_fixel_indices; + vector tract_fixel_indices; for (SetVoxelDir::const_iterator i = in.begin(); i != in.end(); ++i) { assign_pos_of (*i).to (fixel_indexer); fixel_indexer.index(3) = 0; - uint32_t num_fibres = fixel_indexer.value(); - if (num_fibres > 0) { + const index_type num_fixels = fixel_indexer.value(); + if (num_fixels > 0) { fixel_indexer.index(3) = 1; - uint32_t first_index = fixel_indexer.value(); - uint32_t last_index = first_index + num_fibres; - uint32_t closest_fixel_index = 0; + const index_type first_index = fixel_indexer.value(); + const index_type last_index = first_index + num_fixels; + // Note: Streamlines can still be assigned to a fixel that is outside the mask; + // however this will not be permitted to contribute to the matrix + index_type closest_fixel_index = num_fixels; value_type largest_dp = 0.0; const direction_type dir (i->get_dir().normalized()); - for (uint32_t j = first_index; j < last_index; ++j) { + for (index_type j = first_index; j < last_index; ++j) { const value_type dp = std::abs (dir.dot (fixel_directions[j])); if (dp > largest_dp) { largest_dp = dp; - closest_fixel_index = j; + fixel_mask.index(0) = j; + if (fixel_mask.value()) + closest_fixel_index = j; } } - if (largest_dp > angular_threshold_dp) { + if (closest_fixel_index != num_fixels && largest_dp > angular_threshold_dp) { tract_fixel_indices.push_back (closest_fixel_index); fixel_TDI[closest_fixel_index]++; } @@ -86,11 +92,11 @@ namespace MR - Enhancer::Enhancer (const vector >& connectivity_map, + Enhancer::Enhancer (const norm_connectivity_matrix_type& connectivity_matrix, const value_type dh, const value_type E, const value_type H) : - connectivity_map (connectivity_map), + connectivity_matrix (connectivity_matrix), dh (dh), E (E), H (H) { } @@ -101,13 +107,13 @@ namespace MR { enhanced_stats = vector_type::Zero (stats.size()); value_type max_enhanced_stat = 0.0; - for (size_t fixel = 0; fixel < connectivity_map.size(); ++fixel) { - std::map::const_iterator connected_fixel; + vector::const_iterator connected_fixel; + for (size_t fixel = 0; fixel < connectivity_matrix.size(); ++fixel) { for (value_type h = this->dh; h < stats[fixel]; h += this->dh) { value_type extent = 0.0; - for (connected_fixel = connectivity_map[fixel].begin(); connected_fixel != connectivity_map[fixel].end(); ++connected_fixel) - if (stats[connected_fixel->first] > h) - extent += connected_fixel->second.value; + for (connected_fixel = connectivity_matrix[fixel].begin(); connected_fixel != connectivity_matrix[fixel].end(); ++connected_fixel) + if (stats[connected_fixel->index()] > h) + extent += connected_fixel->value(); enhanced_stats[fixel] += std::pow (extent, E) * std::pow (h, H); } if (enhanced_stats[fixel] > max_enhanced_stat) diff --git a/src/stats/cfe.h b/src/stats/cfe.h index 9560e30756..5c62d8cb7d 100644 --- a/src/stats/cfe.h +++ b/src/stats/cfe.h @@ -17,6 +17,7 @@ #include "image.h" #include "image_helpers.h" +#include "types.h" #include "math/math.h" #include "math/stats/typedefs.h" @@ -30,11 +31,11 @@ namespace MR namespace CFE { + using index_type = uint32_t; using value_type = Math::Stats::value_type; using vector_type = Math::Stats::vector_type; using connectivity_value_type = float; using direction_type = Eigen::Matrix; - using connectivity_vector_type = Eigen::Array; using SetVoxelDir = DWI::Tractography::Mapping::SetVoxelDir; @@ -43,7 +44,7 @@ namespace MR @{ */ - class connectivity { MEMALIGN(connectivity) + class connectivity { NOMEMALIGN public: connectivity () : value (0.0) { } connectivity (const connectivity_value_type v) : value (v) { } @@ -51,6 +52,30 @@ namespace MR }; + // A class to store fixel index / connectivity value pairs + // only after the connectivity matrix has been thresholded / normalised + class NormMatrixElement + { NOMEMALIGN + public: + NormMatrixElement (const index_type fixel_index, + const connectivity_value_type connectivity_value) : + fixel_index (fixel_index), + connectivity_value (connectivity_value) { } + FORCE_INLINE index_type index() const { return fixel_index; } + FORCE_INLINE connectivity_value_type value() const { return connectivity_value; } + FORCE_INLINE void normalise (const connectivity_value_type norm_factor) { connectivity_value *= norm_factor; } + private: + const index_type fixel_index; + connectivity_value_type connectivity_value; + }; + + + + // Different types are used depending on whether the connectivity matrix + // is in the process of being built, or whether it has been normalised + using init_connectivity_matrix_type = vector>; + using norm_connectivity_matrix_type = vector>; + /** @@ -59,19 +84,21 @@ namespace MR class TrackProcessor { MEMALIGN(TrackProcessor) public: - TrackProcessor (Image& fixel_indexer, + TrackProcessor (Image& fixel_indexer, const vector& fixel_directions, + Image& fixel_mask, vector& fixel_TDI, - vector >& connectivity_matrix, + init_connectivity_matrix_type& connectivity_matrix, const value_type angular_threshold); bool operator () (const SetVoxelDir& in); private: - Image fixel_indexer; + Image fixel_indexer; const vector& fixel_directions; + Image fixel_mask; vector& fixel_TDI; - vector >& connectivity_matrix; + init_connectivity_matrix_type& connectivity_matrix; const value_type angular_threshold_dp; }; @@ -80,7 +107,7 @@ namespace MR class Enhancer : public Stats::EnhancerBase { MEMALIGN (Enhancer) public: - Enhancer (const vector >& connectivity_map, + Enhancer (const norm_connectivity_matrix_type& connectivity_matrix, const value_type dh, const value_type E, const value_type H); @@ -88,7 +115,7 @@ namespace MR protected: - const vector >& connectivity_map; + const norm_connectivity_matrix_type& connectivity_matrix; const value_type dh, E, H; }; diff --git a/src/stats/permstack.h b/src/stats/permstack.h index 9fa6152e22..605319b96b 100644 --- a/src/stats/permstack.h +++ b/src/stats/permstack.h @@ -17,9 +17,9 @@ #include #include -#include #include "progressbar.h" +#include "types.h" #include "math/stats/permutation.h" namespace MR diff --git a/src/surface/algo/image2mesh.h b/src/surface/algo/image2mesh.h index 3c08e1b021..bfc2df8472 100644 --- a/src/surface/algo/image2mesh.h +++ b/src/surface/algo/image2mesh.h @@ -17,10 +17,10 @@ #include #include -#include #include "image_helpers.h" #include "transform.h" +#include "types.h" #include "surface/mesh.h" #include "surface/types.h" diff --git a/src/surface/algo/mesh2image.cpp b/src/surface/algo/mesh2image.cpp index fc29bd6e60..b4280330b7 100644 --- a/src/surface/algo/mesh2image.cpp +++ b/src/surface/algo/mesh2image.cpp @@ -15,10 +15,10 @@ #include "surface/algo/mesh2image.h" #include -#include #include "header.h" #include "progressbar.h" +#include "types.h" #include "surface/types.h" #include "surface/utils.h" diff --git a/src/surface/mesh.cpp b/src/surface/mesh.cpp index 016fd89027..53bffa8e65 100644 --- a/src/surface/mesh.cpp +++ b/src/surface/mesh.cpp @@ -17,7 +17,8 @@ #include #include #include -#include + +#include "types.h" #include "surface/freesurfer.h" #include "surface/utils.h" diff --git a/src/surface/mesh.h b/src/surface/mesh.h index f0474b7ce4..aaefe03bc8 100644 --- a/src/surface/mesh.h +++ b/src/surface/mesh.h @@ -16,9 +16,8 @@ #define __surface_mesh_h__ -#include -#include #include +#include #include "header.h" #include "image.h" diff --git a/src/surface/mesh_multi.h b/src/surface/mesh_multi.h index 4b8932a033..ef4d3ba1d1 100644 --- a/src/surface/mesh_multi.h +++ b/src/surface/mesh_multi.h @@ -16,7 +16,7 @@ #define __surface_mesh_multi_h__ -#include +#include "types.h" #include "surface/mesh.h" diff --git a/testing/data b/testing/data index add3412305..72e0a40550 160000 --- a/testing/data +++ b/testing/data @@ -1 +1 @@ -Subproject commit add34123053ec1ab5fb207f5225204c3eadf4e84 +Subproject commit 72e0a4055092a9f180fd77a7f6096df90b7622c0 diff --git a/testing/tests/mrdegibbs b/testing/tests/mrdegibbs new file mode 100644 index 0000000000..fb102b4f00 --- /dev/null +++ b/testing/tests/mrdegibbs @@ -0,0 +1 @@ +mrdegibbs b0.nii.gz - | testing_diff_image - mrdegibbs/b0_unring.nii.gz -abs 0.2