From 88c1abc3c57c92063f0b9d546b2baa66058ff018 Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Wed, 6 Apr 2022 20:31:59 -0400 Subject: [PATCH 01/13] PEP --- visualqc/config.py | 88 +++++++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/visualqc/config.py b/visualqc/config.py index 82ee075..5967945 100644 --- a/visualqc/config.py +++ b/visualqc/config.py @@ -19,12 +19,14 @@ statistic_in_histogram_freesurfer = 'ThickAvg' title_histogram_freesurfer = 'mean thickness (label-wise)' num_bins_histogram_display = 30 -xlim_histogram_freesurfer_all = { 'ThickAvg' : [1.0, 6.0], } -xlim_histogram_freesurfer = xlim_histogram_freesurfer_all[statistic_in_histogram_freesurfer] +xlim_histogram_freesurfer_all = {'ThickAvg': [1.0, 6.0], } +xlim_histogram_freesurfer = xlim_histogram_freesurfer_all[ + statistic_in_histogram_freesurfer] xticks_histogram_freesurfer = np.arange(1.5, 6.01, 1.0) color_histogram_freesurfer = ('#c9ae74') # sandstone -freesurfer_features_outlier_detection = ('cortical', 'subcortical', 'both', 'whole_brain') +freesurfer_features_outlier_detection = ('cortical', 'subcortical', + 'both', 'whole_brain') outlier_list_prefix = 'possible_outliers' alert_background_color = 'xkcd:coral' alert_colors_outlier = dict(cortical='xkcd:hot pink', @@ -84,7 +86,7 @@ mri_zorder_freesurfer = 0 seg_zorder_freesurfer = 1 -background_value = 0 # for segmentations or MRI +background_value = 0 # for segmentations or MRI default_views = (0, 1, 2) default_num_slices = 12 @@ -113,13 +115,14 @@ # there is a delay in processing the mouse events # hence we increased delta t for double click from typical defaults of 0.5 sec to 1 # this also helps improving accessibility for those who cant click too fast -double_click_time_delta = 1.1 # seconds +double_click_time_delta = 1.1 # seconds # for serialization delimiter = ',' # when ratings or notes contain the above delimiter, it will be replaced by this delimiter_replacement = ';' -# when ratings are multiple (in some use cases), how to concat them into a single string without a delimiter +# when ratings are multiple (in some use cases), +# how to concat them into a single string without a delimiter rating_joiner = '+' textbox_title = '' @@ -154,8 +157,8 @@ bottom=0.06, top=0.98, wspace=0.0, hspace=0.0) bounding_box_review = (review_area['left'], review_area['bottom'], - review_area['right']-review_area['left'], - review_area['top']-review_area['bottom']) + review_area['right'] - review_area['left'], + review_area['top'] - review_area['bottom']) no_blank_area = dict(left=0.01, right=0.99, bottom=0.01, top=0.99, wspace=0.05, hspace=0.02) @@ -208,7 +211,7 @@ 'Background only', 'Tails_trimmed', 'Original') -saturate_perc_t1 = 33 # supra-threshold values are saturated. +saturate_perc_t1 = 33 # supra-threshold values are saturated. num_bins_histogram_intensity_distribution = 100 num_bins_histogram_contrast_enhancement = 256 @@ -243,12 +246,14 @@ t="i'm Tired", v='reView later') func_mri_default_issue_list = list(abbreviation_func_mri_default_issue_list.values()) -func_mri_default_rating_list_shortform = abbreviation_func_mri_default_issue_list.keys() +func_mri_default_rating_list_shortform = \ + abbreviation_func_mri_default_issue_list.keys() func_outlier_features = None func_mri_BIDS_filters = dict(modalities='func') -# usually done in analyses to try keep the numbers in numerical calculations away from small values +# usually done in analyses to try keep the numbers in numerical calculations away +# from small values # not important here, just for display, doing it anyways. scale_factor_BOLD = 1000 @@ -274,29 +279,32 @@ diffusion_mri_pass_indicator = visual_qc_pass_indicator # f, s, l are matplotlib builtin shortcuts -abbreviation_diffusion_mri_default_issue_list = OrderedDict(p=diffusion_mri_pass_indicator, - m='Motion', - d='Dropout', - v='Vibration', - r='Ringing', - s='Spikes', - f='driFt (scanner)', - l='puLsation', - g='Geometric', - h='gHosting', - o='Orient/FOV', - i='Implausible', - e='othEr', - t="i'm Tired", - w='revieW later') +abbreviation_diffusion_mri_default_issue_list = OrderedDict( + p=diffusion_mri_pass_indicator, + m='Motion', + d='Dropout', + v='Vibration', + r='Ringing', + s='Spikes', + f='driFt (scanner)', + l='puLsation', + g='Geometric', + h='gHosting', + o='Orient/FOV', + i='Implausible', + e='othEr', + t="i'm Tired", + w='revieW later') diffusion_mri_default_issue_list = list( abbreviation_diffusion_mri_default_issue_list.values()) -diffusion_mri_default_rating_list_shortform = abbreviation_diffusion_mri_default_issue_list.keys() +diffusion_mri_default_rating_list_shortform = \ + abbreviation_diffusion_mri_default_issue_list.keys() diffusion_outlier_features = None diffusion_mri_BIDS_filters = dict(modalities='dwi', types='dwi') -# usually done in analyses to try keep the numbers in numerical calculations away from small values +# usually done in analyses to try keep the numbers in numerical calculations away +# from small values # not important here, just for display, doing it anyways. scale_factor_diffusion = 1000 @@ -322,7 +330,7 @@ 'Align b=0 edges') fontsize_radio_button_align_method_diffusion = 8 -position_rating_checkbox_diffusion = [0.899, 0.30, 0.095, 0.35] +position_rating_checkbox_diffusion = [0.899, 0.30, 0.095, 0.35] position_alignment_method_diffusion = [0.899, 0.66, 0.095, 0.10] ## ---------------------------------------------------------------------------- @@ -331,7 +339,7 @@ # Registration and alignment specific ## ---------------------------------------------------------------------------- -alignment_features_OLD = ('MSE', ) +alignment_features_OLD = ('MSE',) alignment_cmap = OrderedDict(Animate='gray', Checkerboard='gray', Voxelwise_diff='seismic', @@ -339,15 +347,15 @@ Edges_Diffused=None, Color_mix=None) choices_alignment_comparison = alignment_cmap.keys() -alignment_default_vis_type = 'Edges_Thinner' # 'Checkerboard' # 'Animate' +alignment_default_vis_type = 'Edges_Thinner' # 'Checkerboard' # 'Animate' -default_checkerboard_size = None # 25 +default_checkerboard_size = None # 25 edge_threshold_alignment = 0.4 default_color_mix_alphas = (1, 1) position_alignment_radio_button_method = [0.895, 0.45, 0.1, 0.19] position_alignment_radio_button_rating = [0.895, 0.25, 0.1, 0.25] -position_text_input_alignment = [0.900, 0.20, 0.09, 0.1] +position_text_input_alignment = [0.900, 0.20, 0.09, 0.1] position_next_button_alignment = [0.905, 0.10, 0.07, 0.03] position_quit_button_alignment = [0.905, 0.03, 0.07, 0.03] position_toggle_animation = [0.925, 0.63, 0.07, 0.05] @@ -356,16 +364,16 @@ annotate_foreground_properties = dict(fontsize='medium', color='xkcd:pale orange') position_histogram_alignment = [0.905, 0.7, 0.09, 0.1] -title_histogram_alignment = 'voxel-wise diff' +title_histogram_alignment = 'voxel-wise diff' num_bins_histogram_alignment = 20 xticks_histogram_alignment = np.arange(0.1, 1.01, 0.2) -color_histogram_alignment = ('#c9ae74') # sandstone +color_histogram_alignment = ('#c9ae74') # sandstone delay_in_animation = 0.5 num_times_to_animate = 5 # edge detection and filtering -weak_edge_threshold = 60 # percentile: must be integer >1 and < 100 +weak_edge_threshold = 60 # percentile: must be integer >1 and < 100 num_iter_weak_edge_removal = 10 median_filter_size = 3 @@ -377,19 +385,19 @@ ## ---------------------------------------------------------------------------- -alignment_features_groupwise = ('MSE', ) +alignment_features_groupwise = ('MSE',) alignment_cmap_groupwise = 'seismic' choices_alignment_comparison_groupwise = ('Std. dev map', 'Animate through', 'Animate with ref', 'show outliers') -alignment_groupwise_default_vis_type = 'Std. dev map' # 'Checkerboard' # 'Animate' +alignment_groupwise_default_vis_type = 'Std. dev map' # 'Checkerboard' # 'Animate' ## ---------------------------------------------------------------------------- outlier_feature_folder_name = 'features_outlier_detection' -features_outlier_detection = freesurfer_features_outlier_detection + t1_mri_features_OLD + func_mri_features_OLD - +features_outlier_detection = freesurfer_features_outlier_detection + \ + t1_mri_features_OLD + func_mri_features_OLD ## ---------------------------------------------------------------------------- # defacing MRI scan quality From 89ec583d86fd81b5f49c6de73fd84393fa227f8d Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Wed, 6 Apr 2022 20:32:26 -0400 Subject: [PATCH 02/13] tweaking rating options --- visualqc/config.py | 21 ++++++++++++--------- visualqc/defacing.py | 4 ++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/visualqc/config.py b/visualqc/config.py index 5967945..59531f9 100644 --- a/visualqc/config.py +++ b/visualqc/config.py @@ -407,15 +407,18 @@ default_render_name = 'render.png' defacing_pass_indicator = visual_qc_pass_indicator -abbreviation_defacing_default_issue_list = {'p': defacing_pass_indicator, - 'f': 'Fail', - 'o': 'Overstripped', - 'u': 'Understripped', - 'e': 'something Else', - 't': "i'm Tired", - 'l': 'review Later'} -defacing_default_issue_list = list(abbreviation_defacing_default_issue_list.values()) -defacing_default_rating_list_shortform = abbreviation_defacing_default_issue_list.keys() +abbrev_defacing_default_issue_list = {'p': defacing_pass_indicator, + 'f': 'Fail', + 'b': 'Brain removed', + 'c': 'faCial features', + 'y': 'eYes visible', + 'r': 'eaRs visible', + 'u': 'Unique mark(s)', + 'e': 'something Else', + 't': "i'm Tired", + 'l': 'review Later'} +defacing_default_issue_list = list(abbrev_defacing_default_issue_list.values()) +defacing_default_rating_list_shortform = abbrev_defacing_default_issue_list.keys() bbox_defacing_MRI_review = (0.02, 0.02, 0.88, 0.7) bbox_defacing_render_review = (0.02, 0.72, 0.88, 0.3) diff --git a/visualqc/defacing.py b/visualqc/defacing.py index 3f72927..7a489e7 100644 --- a/visualqc/defacing.py +++ b/visualqc/defacing.py @@ -277,8 +277,8 @@ def on_keyboard(self, key_in): # notice parentheses at the end self.map_key_to_callback[key_pressed]() else: - if key_pressed in cfg.abbreviation_defacing_default_issue_list: - checked_label = cfg.abbreviation_defacing_default_issue_list[ + if key_pressed in cfg.abbrev_defacing_default_issue_list: + checked_label = cfg.abbrev_defacing_default_issue_list[ key_pressed] self.checkbox.set_active( cfg.defacing_default_issue_list.index(checked_label)) From 4ffcd27ac9da241e04a15b26b512a3a75519fc1a Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Wed, 6 Apr 2022 21:16:13 -0400 Subject: [PATCH 03/13] new param to allow user choice in slice lcoation --- visualqc/defacing.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/visualqc/defacing.py b/visualqc/defacing.py index 7a489e7..acdddac 100644 --- a/visualqc/defacing.py +++ b/visualqc/defacing.py @@ -625,6 +625,14 @@ def get_parser(): Default: a new folder called ``{}`` will be created inside the input folder \n""".format(cfg.default_out_dir_name)) + help_text_slice_locations = textwrap.dedent(""" + Specifies the locations to slice the MRI volumes at, in percentage of depth + from one end to the other i.e. 1 indicates the first non-zero slice in that + view (coronal, sagittal etc), 50 would be in the middle, and 100 would be the + other end. + Default: {}. + \n""".format(cfg.defacing_slice_locations)) + in_out = parser.add_argument_group('Input and output', ' ') in_out.add_argument("-u", "--user_dir", action="store", dest="user_dir", @@ -650,6 +658,12 @@ def get_parser(): in_out.add_argument("-i", "--id_list", action="store", dest="id_list", default=None, required=False, help=help_text_id_list) + layout = parser.add_argument_group('Layout options', ' ') + + layout.add_argument("-s", "--slice_locations", action="store", dest="slice_loc", + default=cfg.defacing_slice_locations, required=False, + nargs='+', help=help_text_slice_locations) + return parser @@ -671,15 +685,15 @@ def make_workflow_from_user_options(): vis_type = 'defacing' - user_dir, id_list, images_for_id, defaced_name, mri_name, render_name \ - = check_inputs_defacing(user_args.user_dir, user_args.defaced_name, - user_args.mri_name, user_args.render_name, - user_args.id_list) + user_dir, id_list, images_for_id, defaced_name, mri_name, render_name, \ + slice_loc = check_inputs_defacing( + user_args.user_dir, user_args.defaced_name, user_args.mri_name, + user_args.render_name, user_args.id_list, user_args.slice_loc) out_dir = check_out_dir(user_args.out_dir, user_dir) wf = RatingWorkflowDefacing(id_list, images_for_id, user_dir, out_dir, - defaced_name, mri_name, render_name, + defaced_name, mri_name, render_name, slice_loc, cfg.defacing_default_issue_list, vis_type) return wf From b237f7102738d3fa6652e7d94b4806077e9e5716 Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Wed, 6 Apr 2022 21:16:54 -0400 Subject: [PATCH 04/13] input validation --- visualqc/utils.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/visualqc/utils.py b/visualqc/utils.py index e9442c2..e3f465d 100644 --- a/visualqc/utils.py +++ b/visualqc/utils.py @@ -1020,6 +1020,17 @@ def check_labels(vis_type, label_set): return vis_type, label_set +def check_slice_locations_percent(slice_loc): + """Checks they are numbers between 0 and 100 inclusive""" + + slice_loc = [float(sl) for sl in slice_loc] + if any([sl < 1 or sl > 100 for sl in slice_loc]): + raise ValueError('One or more slice locations specified is invalid. ' + 'They must be between 1 and 100.') + + return slice_loc + + def check_views(views): """Checks which views were selected.""" @@ -1053,7 +1064,8 @@ def check_string_is_nonempty(string, string_type='string'): return string -def check_inputs_defacing(in_dir, defaced_name, mri_name, render_name, id_list_in): +def check_inputs_defacing(in_dir, defaced_name, mri_name, render_name, id_list_in, + slice_loc): """Validates the integrity of the inputs""" in_dir = realpath(in_dir) @@ -1064,6 +1076,8 @@ def check_inputs_defacing(in_dir, defaced_name, mri_name, render_name, id_list_i mri_name = check_string_is_nonempty(mri_name, 'original MRI scan') render_name = check_string_is_nonempty(render_name, '3D rendered image') + slice_loc = check_slice_locations_percent(slice_loc) + if id_list_in is not None: if not pexists(id_list_in): raise IOError('Given ID list does not exist @ \n' @@ -1129,7 +1143,7 @@ def check_inputs_defacing(in_dir, defaced_name, mri_name, render_name, id_list_i print('{} subjects are usable for review.'.format(len(id_list_out))) return in_dir, np.array(id_list_out), images_for_id, \ - defaced_name, mri_name, render_name + defaced_name, mri_name, render_name, slice_loc def print_time_stamp(): From 062c19489ac35e7a5e8c1238c8aea2266cdddfde Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Wed, 6 Apr 2022 21:17:29 -0400 Subject: [PATCH 05/13] implementing the input to display engine --- visualqc/defacing.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/visualqc/defacing.py b/visualqc/defacing.py index acdddac..bc80e0f 100644 --- a/visualqc/defacing.py +++ b/visualqc/defacing.py @@ -300,6 +300,7 @@ def __init__(self, defaced_name, mri_name, render_name, + slice_loc, issue_list=cfg.defacing_default_issue_list, vis_type='defacing'): """Constructor""" @@ -315,13 +316,14 @@ def __init__(self, self.defaced_name = defaced_name self.mri_name = mri_name self.render_name = render_name + self.slice_loc = slice_loc self.images_for_id = images_for_id self.expt_id = 'rate_defaced_mri_{}'.format(self.defaced_name) self.suffix = self.expt_id self.current_alert_msg = None - self.init_layout() + self.init_layout(num_slices_per_view=len(self.slice_loc)) self.__module_type__ = 'defacing' @@ -458,7 +460,7 @@ def load_unit(self, unit_id): self.slice_picker = SlicePicker(self.orig_img, view_set=self.collage.view_set, num_slices=self.collage.num_slices, - sampler=cfg.defacing_slice_locations) + sampler=self.slice_loc) return skip_subject From 2e5e040d50084ea4baeb6eb7610a1fdd10b180bc Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Wed, 6 Apr 2022 21:17:47 -0400 Subject: [PATCH 06/13] updated test --- visualqc/tests/test_defacing.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/visualqc/tests/test_defacing.py b/visualqc/tests/test_defacing.py index 7fd8105..20995e5 100644 --- a/visualqc/tests/test_defacing.py +++ b/visualqc/tests/test_defacing.py @@ -9,14 +9,19 @@ base_dir = realpath(pjoin(test_dir, '..', '..', 'example_datasets')) id_list = pjoin(base_dir, 'id_list_defacing') -name_defaced = 'defaced_noniso2.nii.gz' # 'defaced.nii.gz' -name_mri = 'orig_noniso2.nii.gz' # 'orig.nii.gz' +name_defaced = 'defaced_noniso2.nii.gz' # 'defaced.nii.gz' +name_mri = 'orig_noniso2.nii.gz' # 'orig.nii.gz' name_rendered = 'rendered.png' +slice_locations = ' -s 40 45 47 50 55 57 60 ' + out_dir = pjoin(base_dir, 'out_defacing') makedirs(out_dir, exist_ok=True) -sys.argv = shlex.split('visualqc_defacing -u {} -i {} -o {} -d {} -m {} -r {}' +sys.argv = shlex.split('visualqc_defacing -u {} -i {} -o {} ' + '-d {} -m {} -r {} ' + ' {} ' # slice locations ''.format(base_dir, id_list, out_dir, - name_defaced, name_mri, name_rendered)) + name_defaced, name_mri, name_rendered, + slice_locations)) cli_run() From fb6e60a7b96a41fe422bfef3f6618c137b60ac68 Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Thu, 7 Apr 2022 09:39:40 -0400 Subject: [PATCH 07/13] code quality / PEP --- visualqc/utils.py | 9 ++++----- visualqc/workflows.py | 6 ++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/visualqc/utils.py b/visualqc/utils.py index e3f465d..b5948b3 100644 --- a/visualqc/utils.py +++ b/visualqc/utils.py @@ -234,7 +234,7 @@ def pick_slices(img, view_set, num_slices): slices_in_dim = [sn for sn in slices_in_dim if sn >= 0 or sn <= num_non_empty] # adding view and slice # at the same time. - slices.extend([(view, slice) for slice in slices_in_dim]) + slices.extend([(view, slice_) for slice_ in slices_in_dim]) return slices @@ -421,7 +421,7 @@ def restore_previous_ratings(qcw): incomplete_list = list(qcw.id_list) prev_done = [] # empty list - ratings_file, backup_name_ratings = get_ratings_path_info(qcw) + ratings_file, _ = get_ratings_path_info(qcw) if pexists(ratings_file): ratings, notes = load_ratings_csv(ratings_file) @@ -433,8 +433,7 @@ def restore_previous_ratings(qcw): notes = dict() if len(prev_done) > 0: - print('\nRatings for {}/{} subjects were restored.'.format(len(prev_done), - len(qcw.id_list))) + print(f'\nRestored {len(prev_done)}/{len(qcw.id_list)} previous ratings') if len(incomplete_list) < 1: print('No subjects to review/rate - exiting.') @@ -503,7 +502,7 @@ def summarize_ratings(ratings_file, out_dir=None): import re clean = lambda lbl: re.sub(r'\W+', '_', lbl.lower()) - rating_dict, notes = load_ratings_csv(ratings_file) + rating_dict, _ = load_ratings_csv(ratings_file) rating_list = list() uniq_labels = set() diff --git a/visualqc/workflows.py b/visualqc/workflows.py index d2f9db6..401827c 100644 --- a/visualqc/workflows.py +++ b/visualqc/workflows.py @@ -314,8 +314,7 @@ def quit(self, input_event_to_ignore=None): self.quit_now = True else: print('You have not rated the current subject! ' - 'Please rate it before you can advance ' - 'to next subject, or to quit..') + 'Rate it before you can quit.') def next(self, input_event_to_ignore=None): @@ -326,8 +325,7 @@ def next(self, input_event_to_ignore=None): self.quit_now = False else: print('You have not rated the current subject! ' - 'Please rate it before you can advance ' - 'to next subject, or to quit..') + 'Rate it before you can advance to the next subject.') def prepare_to_advance(self): From ea46c97a8402706c964288d9ffad08c30332bb3d Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Fri, 8 Apr 2022 11:24:57 -0400 Subject: [PATCH 08/13] tweaking options: adding Brows, but removing generic facial features --- visualqc/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/visualqc/config.py b/visualqc/config.py index 59531f9..db224af 100644 --- a/visualqc/config.py +++ b/visualqc/config.py @@ -410,9 +410,9 @@ abbrev_defacing_default_issue_list = {'p': defacing_pass_indicator, 'f': 'Fail', 'b': 'Brain removed', - 'c': 'faCial features', - 'y': 'eYes visible', - 'r': 'eaRs visible', + 'r': 'eaRs visible', + 'y': 'eYes visible', + 'w': 'broWs visible', 'u': 'Unique mark(s)', 'e': 'something Else', 't': "i'm Tired", From 158e69fb7b2ddcd276b086b43b0bde2dd32395ec Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Thu, 21 Apr 2022 13:22:16 -0400 Subject: [PATCH 09/13] code quality --- visualqc/defacing.py | 2 +- visualqc/freesurfer.py | 63 +++++++++++++++++++++++++----------------- visualqc/workflows.py | 5 ++-- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/visualqc/defacing.py b/visualqc/defacing.py index bc80e0f..ddc50df 100644 --- a/visualqc/defacing.py +++ b/visualqc/defacing.py @@ -688,7 +688,7 @@ def make_workflow_from_user_options(): vis_type = 'defacing' user_dir, id_list, images_for_id, defaced_name, mri_name, render_name, \ - slice_loc = check_inputs_defacing( + slice_loc = check_inputs_defacing( user_args.user_dir, user_args.defaced_name, user_args.mri_name, user_args.render_name, user_args.id_list, user_args.slice_loc) diff --git a/visualqc/freesurfer.py b/visualqc/freesurfer.py index 01fa8fd..b5c729f 100644 --- a/visualqc/freesurfer.py +++ b/visualqc/freesurfer.py @@ -38,6 +38,7 @@ next_click = time.monotonic() + class FreesurferReviewInterface(BaseReviewInterface): """Custom interface for rating the quality of Freesurfer parcellation.""" @@ -525,16 +526,17 @@ def add_histogram_panel(self): def update_histogram(self): """Updates histogram with current image data""" - # to update thickness histogram, you need access to full FS output or aparc.stats + # to update thickness histogram, we need access to full FS output/aparc.stats try: - distribution_to_show = read_aparc_stats_wholebrain(self.in_dir, self.current_unit_id, - subset=(cfg.statistic_in_histogram_freesurfer,)) + distrib_to_show = read_aparc_stats_wholebrain( + self.in_dir, self.current_unit_id, + subset=(cfg.statistic_in_histogram_freesurfer,)) except: # do nothing return - # number of vertices is too high - so presenting mean ROI thickness is smarter! - _, _, patches_hist = self.ax_hist.hist(distribution_to_show, density=True, + # number of vertices is too high so presenting mean ROI thickness is smarter! + _, _, patches_hist = self.ax_hist.hist(distrib_to_show, density=True, bins=cfg.num_bins_histogram_display) self.ax_hist.set_xlim(cfg.xlim_histogram_freesurfer) self.ax_hist.relim(visible_only=True) @@ -560,8 +562,8 @@ def add_alerts(self): if flagged_as_outlier: alerts_list = self.by_sample.get(self.current_unit_id, None) # None, if id not in dict - print('\n\tFlagged as a possible outlier by these measures:\n\t\t{}'.format( - '\t'.join(alerts_list))) + print('\n\tFlagged as a possible outlier by these measures:\n\t\t{}' + ''.format('\t'.join(alerts_list))) strings_to_show = ['Flagged as an outlier:', ] + alerts_list self.current_alert_msg = '\n'.join(strings_to_show) @@ -586,7 +588,8 @@ def load_unit(self, unit_id): skip_subject = False if self.vis_type in ('cortical_volumetric', 'cortical_contour'): - temp_seg_uncropped, roi_set_is_empty = void_subcortical_symmetrize_cortical(temp_fs_seg) + temp_seg_uncropped, roi_set_is_empty = \ + void_subcortical_symmetrize_cortical(temp_fs_seg) elif self.vis_type in ('labels_volumetric', 'labels_contour'): if self.label_set is not None: # TODO same colors for same labels is not guaranteed @@ -595,7 +598,7 @@ def load_unit(self, unit_id): temp_seg_uncropped, roi_set_is_empty = get_label_set(temp_fs_seg, self.label_set) else: - raise ValueError('--label_set must be specified for visualization types: ' + raise ValueError('--label_set must be specified for vis types: ' ' labels_volumetric and labels_contour') else: raise NotImplementedError('Invalid visualization type - ' @@ -848,7 +851,8 @@ def run_tksurfer_script(fs_dir, subject_id, hemi, script_file): try: cmd_args = ['tksurfer', '-sdir', fs_dir, subject_id, hemi, 'pial', '-tcl', script_file] - txt_out = check_output(cmd_args, shell=False, stderr=subprocess.STDOUT, universal_newlines=True) + txt_out = check_output(cmd_args, shell=False, stderr=subprocess.STDOUT, + universal_newlines=True) except subprocess.CalledProcessError as tksurfer_exc: exit_code = tksurfer_exc.returncode txt_out = tksurfer_exc.output @@ -865,7 +869,8 @@ def run_freeview_script(script_file): try: cmd_args = ['freeview', '--command', script_file] - txt_out = check_output(cmd_args, shell=False, stderr=subprocess.STDOUT, universal_newlines=True) + txt_out = check_output(cmd_args, shell=False, stderr=subprocess.STDOUT, + universal_newlines=True) except subprocess.CalledProcessError as tksurfer_exc: exit_code = tksurfer_exc.returncode txt_out = tksurfer_exc.output @@ -886,7 +891,7 @@ def get_parser(): 'of Freesurfer reconstruction.') help_text_fs_dir = textwrap.dedent(""" - Absolute path to ``SUBJECTS_DIR`` containing the finished runs of Freesurfer parcellation + Absolute path to ``SUBJECTS_DIR`` containing the finished Freesurfer processing. Each subject will be queried after its ID in the metadata file. E.g. ``--fs_dir /project/freesurfer_v5.3`` @@ -934,24 +939,26 @@ def get_parser(): \n""".format(cfg.default_vis_type)) help_text_label = textwrap.dedent(""" - Specifies the set of labels to include for overlay. - - Atleast one label must be specified when vis_type is labels_volumetric or labels_contour + Specifies the set of labels to include for overlay. Atleast one label must be + specified when vis_type is labels_volumetric or labels_contour Default: None (show nothing) \n""") help_text_contour_color = textwrap.dedent(""" - Specifies the color to use for the contours overlaid on MRI (when vis_type requested prescribes contours). - Color can be specified in many ways as documented in https://matplotlib.org/users/colors.html + Specifies the color to use for the contours overlaid on MRI, when vis_type + requested prescribes contours). Color can be specified in many ways + as documented in https://matplotlib.org/users/colors.html Default: {}. \n""".format(cfg.default_contour_face_color)) help_text_alphas = textwrap.dedent(""" Alpha values to control the transparency of MRI and aseg. - This must be a set of two values (between 0 and 1.0) separated by a space e.g. --alphas 0.7 0.5. + This must be a set of two values (between 0 and 1.0) separated by a space + e.g. --alphas 0.7 0.5. - Default: {} {}. Play with these values to find something that works for you and the dataset. + Default: {} {}. + Play with these values to find something that works for you and the dataset. \n""".format(cfg.default_alpha_mri, cfg.default_alpha_seg)) help_text_views = textwrap.dedent(""" @@ -972,9 +979,13 @@ def get_parser(): \n""".format(cfg.default_num_rows)) help_text_no_surface_vis = textwrap.dedent(""" - This flag disables batch-generation of 3d surface visualizations, which are shown along with cross-sectional overlays. This is not recommended, but could be used in situations where you do not have Freesurfer installed or want to focus solely on cross-sectional views. + This flag disables batch-generation of 3d surface visualizations, which are shown + along with cross-sectional overlays. This is not recommended, but could be used + in situations where you do not have Freesurfer installed, or want to focus + solely on cross-sectional views. - Default: False (required visualizations are generated at the beginning, which can take 5-10 seconds for each subject). + Default: False (required visualizations are generated at the beginning, + which can take 5-10 seconds for each subject). \n""") help_text_outlier_detection_method = textwrap.dedent(""" @@ -1051,7 +1062,8 @@ def get_parser(): required=False, help=help_text_alphas) outliers = parser.add_argument_group('Outlier detection', - 'options related to automatically detecting possible outliers') + 'options related to automatically detecting ' + 'possible outliers') outliers.add_argument("-olm", "--outlier_method", action="store", dest="outlier_method", default=cfg.default_outlier_detection_method, required=False, @@ -1084,9 +1096,10 @@ def get_parser(): default=cfg.default_num_rows, required=False, help=help_text_num_rows) - wf_args = parser.add_argument_group('Workflow', 'Options related to workflow ' - 'e.g. to pre-compute resource-intensive features, ' - 'and pre-generate all the visualizations required') + wf_args = parser.add_argument_group( + 'Workflow', 'Options related to workflow e.g. to pre-compute ' + 'resource-intensive features, and pre-generate all the ' + 'visualizations required') wf_args.add_argument("-ns", "--no_surface_vis", action="store_true", dest="no_surface_vis", help=help_text_no_surface_vis) diff --git a/visualqc/workflows.py b/visualqc/workflows.py index 401827c..2d07b66 100644 --- a/visualqc/workflows.py +++ b/visualqc/workflows.py @@ -179,6 +179,7 @@ def _join_ratings(str_list): else: return str_list + def save_time_spent(self): """Saves time spent on each unit""" @@ -349,8 +350,8 @@ def print_rating(self, subject_id): if subject_id in self.ratings: # checking if "i'm tired" or 'review later' appear in ratings - do_not_save = any([ rt.lower() in cfg.ratings_not_to_be_recorded - for rt in self.ratings[subject_id]]) + do_not_save = any([rt.lower() in cfg.ratings_not_to_be_recorded + for rt in self.ratings[subject_id]]) # not saving ratings meant not to be saved! if do_not_save: From 2d72723dd09915ca6a51bd8f41f3f994947cdce1 Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Fri, 22 Jul 2022 18:21:15 -0400 Subject: [PATCH 10/13] bug in keybd shortcut for fmri qc --- visualqc/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/visualqc/config.py b/visualqc/config.py index db224af..80509d2 100644 --- a/visualqc/config.py +++ b/visualqc/config.py @@ -238,7 +238,7 @@ abbreviation_func_mri_default_issue_list = OrderedDict(p=func_mri_pass_indicator, m='Motion', r='Ringing', - s='Spikes', + k='spiKes', g='Ghosting', o='Orient/FOV', w='Weird', From 52f62fa6bdffa418059b67a373c6135dc1f78e93 Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Fri, 22 Jul 2022 18:26:13 -0400 Subject: [PATCH 11/13] allowing use of pre-generated surf vis, regardless of whether FS is installed or not --- visualqc/freesurfer.py | 129 ++++++++++++++++++++++++----------------- visualqc/utils.py | 3 +- 2 files changed, 77 insertions(+), 55 deletions(-) diff --git a/visualqc/freesurfer.py b/visualqc/freesurfer.py index b5c729f..0a65070 100644 --- a/visualqc/freesurfer.py +++ b/visualqc/freesurfer.py @@ -346,9 +346,6 @@ def generate_surface_vis(self): print('\nAttempting to generate surface visualizations of parcellation ...') self._freesurfer_installed, self._fs_vis_tool = \ freesurfer_vis_tool_installed() - if not self._freesurfer_installed: - print('Freesurfer does not seem to be installed ' - '- skipping surface visualizations.') self.surface_vis_paths = dict() for sid in self.id_list: @@ -704,7 +701,7 @@ def cleanup(self): plt.close('all') -def make_vis_pial_surface(fs_dir, subject_id, out_dir, +def make_vis_pial_surface(fs_dir, subj_id, out_dir, FREESURFER_INSTALLED, annot_file='aparc.annot', vis_tool=cfg.freesurfer_vis_cmd): @@ -718,59 +715,86 @@ def make_vis_pial_surface(fs_dir, subject_id, out_dir, hemis_long = ('left', 'right') vis_list = dict() - print('Processing {}'.format(subject_id)) + print('Processing {}'.format(subj_id)) for hemi, hemi_l in zip(hemis, hemis_long): - # generating necessary scripts vis_list[hemi_l] = dict() - if vis_tool == "freeview": - script_file, vis_files = make_freeview_script_vis_annot( - fs_dir, subject_id, hemi, out_vis_dir, annot_file) - elif vis_tool == "tksurfer": - script_file, vis_files = make_tcl_script_vis_annot( - subject_id, hemi_l, out_vis_dir, annot_file) - else: - pass - - # not running the scripts if required files dont exist - surf_path = fs_dir / subject_id / 'surf' / '{}.pial'.format(hemi) - annot_path = fs_dir / subject_id / 'label' / '{}.{}'.format(hemi, annot_file) - if not surf_path.exists(): - print(f"surface for {subject_id} {hemi_l} doesn't exist @\n {surf_path}") - continue - if not annot_path.exists(): - print(f"Annot for {subject_id} {hemi_l} doesn't exist @\n{annot_path}") - continue - - try: - # run the script only if all the visualizations were not generated before - all_vis_exist = all([vp.exists() for vp in vis_files.values()]) - if not all_vis_exist and FREESURFER_INSTALLED: - if vis_tool == "freeview": - _, _ = run_freeview_script(script_file) - elif vis_tool == "tksurfer": - _, _ = run_tksurfer_script(fs_dir, subject_id, hemi, script_file) - else: - pass - - vis_list[hemi_l].update(vis_files) - except: - traceback.print_exc() - print(f'unable to generate 3D surf vis for {hemi} hemi - skipping') - - # flattening it for easier use later on + vis_list[hemi_l] = make_vis_paths(subj_id, hemi, out_vis_dir, vis_tool) + all_vis_exist = all([vp.exists() for vp in vis_list[hemi_l].values()]) + if not all_vis_exist: + # generating necessary scripts + if vis_tool == "freeview": + script_file, vis_files = make_freeview_script_vis_annot( + fs_dir, subj_id, hemi, out_vis_dir, annot_file) + elif vis_tool == "tksurfer": + script_file, vis_files = make_tcl_script_vis_annot( + subj_id, hemi_l, out_vis_dir, annot_file) + else: # when freesurfer is not installed or not in path + vis_files = make_vis_paths(subj_id, hemi, out_vis_dir) + + # not running the scripts if required files don't exist + surf_path = fs_dir / subj_id / 'surf' / f'{hemi}.pial' + annot_path = fs_dir / subj_id / 'label' / f'{hemi}.{annot_file}' + + if surf_path.exists() and annot_path.exists(): + try: + # run script only if all vis were not generated before + if FREESURFER_INSTALLED: + if vis_tool == "freeview": + _, _ = run_freeview_script(script_file) + elif vis_tool == "tksurfer": + _, _ = run_tksurfer_script(fs_dir, subj_id, hemi, script_file) + else: + pass + + vis_list[hemi_l].update(vis_files) + except: + traceback.print_exc() + print(f'unable to generate 3D surf vis for {hemi} hemi. ' + f'skipping') + else: + if not surf_path.exists(): + print(f"surface for {subj_id} {hemi_l} doesn't exist @" + f"\n {surf_path}") + if not annot_path.exists(): + print(f"Annot for {subj_id} {hemi_l} doesn't exist @" + f"\n{annot_path}") + + # flattening / reordering it for easier use later on out_vis_list = dict() for hemi_l, view in cfg.view_pref_order[vis_tool]: try: if vis_list[hemi_l][view].exists(): out_vis_list[(hemi_l, view)] = vis_list[hemi_l][view] + else: + print(f'no surf vis for {subj_id} {hemi_l} {view}') except: - # not file hemi/view combinations have files generated + # not all hemi/view combinations have vis files generated pass return out_vis_list +def make_vis_paths(subject_id, hemi, out_vis_dir, vis_tool='freeview'): + """util to make output paths to store visualizations""" + + vis_path = dict() + vis_tool = vis_tool.lower() + if vis_tool == 'freeview': + img_ext = 'png' + angles = cfg.freeview_surface_vis_angles + elif vis_tool == 'tksurfer': + img_ext = 'tif' + angles = cfg.tksurfer_surface_vis_angles + else: + raise ValueError('Invalid vis_tool, it must be freeview or tksurfer') + + for view in angles: + vis_path[view] = out_vis_dir / f'{subject_id}_{hemi}_{view}.{img_ext}' + + return vis_path + + def make_freeview_script_vis_annot(fs_dir, subject_id, hemi, out_vis_dir, annot_file='aparc.annot'): """Generates a tksurfer script to make visualizations""" @@ -778,13 +802,12 @@ def make_freeview_script_vis_annot(fs_dir, subject_id, hemi, out_vis_dir, fs_dir = Path(fs_dir).resolve() out_vis_dir = Path(out_vis_dir).resolve() + vis_path = make_vis_paths(subject_id, hemi, out_vis_dir, vis_tool='freeview') + surf_path = fs_dir / subject_id / 'surf' / '{}.pial'.format(hemi) annot_path = fs_dir / subject_id / 'label' / '{}.{}'.format(hemi, annot_file) script_file = out_vis_dir / 'vis_annot_{}.freeview.cmd'.format(hemi) - vis_path = dict() - for view in cfg.freeview_surface_vis_angles: - vis_path[view] = out_vis_dir / '{}_{}_{}.png'.format(subject_id, hemi, view) # NOTES reg freeview commands # --screenshot @@ -814,10 +837,7 @@ def make_freeview_script_vis_annot(fs_dir, subject_id, hemi, out_vis_dir, def make_tcl_script_vis_annot(subject_id, hemi, out_vis_dir, annot_file='aparc.annot'): """Generates a tksurfer script to make visualizations""" - script_file = out_vis_dir / f'vis_annot_{hemi}.tcl' - vis = dict() - for view in cfg.tksurfer_surface_vis_angles: - vis[view] = out_vis_dir / f'{subject_id}_{hemi}_{view}.tif' + vis_path = make_vis_paths(subject_id, hemi, out_vis_dir, vis_tool='tksurfer') img_format = 'tiff' # rgb does not work @@ -826,23 +846,24 @@ def make_tcl_script_vis_annot(subject_id, hemi, out_vis_dir, annot_file='aparc.a cmds.append("labl_import_annotation {}".format(annot_file)) cmds.append("scale_brain 1.37") cmds.append("redraw") - cmds.append("save_{} {}".format(img_format, vis['lateral'])) + cmds.append("save_{} {}".format(img_format, vis_path['lateral'])) cmds.append("rotate_brain_y 180.0") cmds.append("redraw") - cmds.append("save_{} {}".format(img_format, vis['medial'])) + cmds.append("save_{} {}".format(img_format, vis_path['medial'])) cmds.append("rotate_brain_z -90.0") cmds.append("rotate_brain_y 135.0") cmds.append("redraw") - cmds.append("save_{} {}".format(img_format, vis['transverse'])) + cmds.append("save_{} {}".format(img_format, vis_path['transverse'])) cmds.append("exit 0") + script_file = out_vis_dir / f'vis_annot_{hemi}.tcl' try: with open(script_file, 'w') as sf: sf.write('\n'.join(cmds)) except: raise IOError('Unable to write the script file to\n {}'.format(script_file)) - return script_file, vis + return script_file, vis_path def run_tksurfer_script(fs_dir, subject_id, hemi, script_file): diff --git a/visualqc/utils.py b/visualqc/utils.py index b5948b3..b839733 100644 --- a/visualqc/utils.py +++ b/visualqc/utils.py @@ -699,7 +699,8 @@ def freesurfer_vis_tool_installed(): if os.getenv('FREESURFER_HOME') is None: print('the environment variable FREESURFER_HOME is not set!') - return False, None + # using freeview here instead of None to recognize existing visualizations + return False, "freeview" fv_callable = which('freeview') is not None if fv_callable: From 03d098d59c5e2ec71afd1e31a74c1803589209d2 Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Thu, 16 Mar 2023 12:09:13 -0400 Subject: [PATCH 12/13] getting started with native generation of 3d renders --- visualqc/config.py | 2 ++ visualqc/defacing.py | 77 +++++++++++++++++++++++++++++++++++++++++--- visualqc/utils.py | 10 ++++++ 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/visualqc/config.py b/visualqc/config.py index 80509d2..cadfcfa 100644 --- a/visualqc/config.py +++ b/visualqc/config.py @@ -435,3 +435,5 @@ 'Mixed') defacing_trim_percentile = 1 + +defacing_render_vis_angles = (-45, -22, 0, 22, 45) diff --git a/visualqc/defacing.py b/visualqc/defacing.py index ddc50df..9d58713 100644 --- a/visualqc/defacing.py +++ b/visualqc/defacing.py @@ -22,7 +22,7 @@ from visualqc.utils import (check_event_in_axes, check_inputs_defacing, check_out_dir, compute_cell_extents_grid, pixdim_nifti_header, read_image, set_fig_window_title, - slice_aspect_ratio) + slice_aspect_ratio, rendering_tool_installed) from visualqc.workflows import BaseWorkflowVisualQC @@ -302,6 +302,7 @@ def __init__(self, render_name, slice_loc, issue_list=cfg.defacing_default_issue_list, + disable_3d_renderings=False, vis_type='defacing'): """Constructor""" @@ -311,7 +312,6 @@ def __init__(self, outlier_feat_types=None, disable_outlier_detection=None) - self.vis_type = vis_type self.issue_list = issue_list self.defaced_name = defaced_name self.mri_name = mri_name @@ -323,6 +323,9 @@ def __init__(self, self.suffix = self.expt_id self.current_alert_msg = None + self.vis_type = vis_type + self.disable_3d_renderings = disable_3d_renderings + self.init_layout(num_slices_per_view=len(self.slice_loc)) self.__module_type__ = 'defacing' @@ -331,7 +334,8 @@ def __init__(self, def preprocess(self): """Preprocessing if necessary.""" - pass + if not self.disable_3d_renderings: + self.generate_3d_renderings() def init_layout(self, @@ -364,6 +368,20 @@ def init_layout(self, self.padding = padding + def generate_3d_renderings(self): + """batch generation of 3D renderings of the MRI volumes prior to review""" + + print('\nAttempting to generate surface visualizations of parcellation ...') + self._rendering_tool_installed, self._rendering_tool = \ + rendering_tool_installed() + + self.render_vis_paths = dict() + for sid in self.id_list: + self.render_vis_paths[sid] = \ + make_3d_render(self.images_for_id[sid]['original'], self.out_dir, + self._rendering_tool_installed, self._rendering_tool) + + def prepare_UI(self): """Main method to run the entire workflow""" @@ -571,6 +589,16 @@ def cleanup(self): plt.close('all') +def make_3d_render(mri_path, out_dir, too_exists_flag, tool_path): + """generates 3D render of an MRI volume in different angles""" + + vis_paths = list() # list of paths to renderings at different angles + + # fsleyes render --scene 3d -rot 45 0 -30 -dr 30 250 -cr 30 500 + # -in spline -bf 0.225 -r 100 -ns 500 --outfile img_render.png orig.nii.gz + return vis_paths + + def get_parser(): """Parser to specify arguments and their defaults.""" @@ -635,6 +663,17 @@ def get_parser(): Default: {}. \n""".format(cfg.defacing_slice_locations)) + help_text_no_3d_renderings = textwrap.dedent(""" + This flag disables batch-generation of 3d rendering of the MRI volume, which are + shown along with cross-sectional overlays. This is not recommended, but could + be used in situations where you can not generate them, or want to + focus solely on cross-sectional views. + + Default: False (required visualizations are generated at the beginning, + if they don't exist already, which can take 5-10 seconds for each subject). + \n""") + + in_out = parser.add_argument_group('Input and output', ' ') in_out.add_argument("-u", "--user_dir", action="store", dest="user_dir", @@ -666,9 +705,38 @@ def get_parser(): default=cfg.defacing_slice_locations, required=False, nargs='+', help=help_text_slice_locations) + wf_args = parser.add_argument_group( + 'Workflow', 'Options related to workflow e.g. to pre-compute ' + 'resource-intensive features, and pre-generate all the ' + 'visualizations required') + + wf_args.add_argument("-nr", "--no_3d_renderings", dest="disable_3d_renderings", + action="store_true", default=False, + help=help_text_no_3d_renderings) + return parser +def make_vis_paths(subject_id, out_vis_dir, vis_tool='fsleyes'): + """util to make output paths to store visualizations""" + + vis_path = dict() + vis_tool = vis_tool.lower() + if vis_tool == 'fsleyes': + img_ext = 'png' + angles = cfg.freeview_surface_vis_angles + elif vis_tool == 'tksurfer': + img_ext = 'tif' + angles = cfg.tksurfer_surface_vis_angles + else: + raise ValueError('Invalid vis_tool, it must be freeview or tksurfer') + + for view in angles: + vis_path[view] = out_vis_dir / f'{subject_id}_{hemi}_{view}.{img_ext}' + + return vis_path + + def make_workflow_from_user_options(): """Parser/validator for the cmd line args.""" @@ -696,7 +764,8 @@ def make_workflow_from_user_options(): wf = RatingWorkflowDefacing(id_list, images_for_id, user_dir, out_dir, defaced_name, mri_name, render_name, slice_loc, - cfg.defacing_default_issue_list, vis_type) + cfg.defacing_default_issue_list, + user_args.disable_3d_renderings, vis_type) return wf diff --git a/visualqc/utils.py b/visualqc/utils.py index b839733..9118b28 100644 --- a/visualqc/utils.py +++ b/visualqc/utils.py @@ -718,6 +718,16 @@ def freesurfer_vis_tool_installed(): return False +def rendering_tool_installed(): + """checks if the tool to generate 3D renderings of MRI volumes is installed""" + + fe_callable = which('fsleyes') is not None + if fe_callable: + return True, "fsleyes" + else: + return False, None + + def check_out_dir(out_dir, base_dir): """Creates the output folder.""" From 5b1227a7fbd3bf4ad3976f64b60fd5d6d8503632 Mon Sep 17 00:00:00 2001 From: Pradeep Reddy Raamana Date: Thu, 16 Mar 2023 12:09:29 -0400 Subject: [PATCH 13/13] docs update --- visualqc/freesurfer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/visualqc/freesurfer.py b/visualqc/freesurfer.py index 0a65070..4f0a4e4 100644 --- a/visualqc/freesurfer.py +++ b/visualqc/freesurfer.py @@ -1006,7 +1006,7 @@ def get_parser(): solely on cross-sectional views. Default: False (required visualizations are generated at the beginning, - which can take 5-10 seconds for each subject). + if they don't exist already, which can take 5-10 seconds for each subject). \n""") help_text_outlier_detection_method = textwrap.dedent(""" @@ -1122,8 +1122,9 @@ def get_parser(): 'resource-intensive features, and pre-generate all the ' 'visualizations required') - wf_args.add_argument("-ns", "--no_surface_vis", action="store_true", - dest="no_surface_vis", help=help_text_no_surface_vis) + wf_args.add_argument("-ns", "--no_surface_vis", dest="no_surface_vis", + action="store_true", default=False, + help=help_text_no_surface_vis) return parser