diff --git a/fsleyes_plugin_shimming_toolbox/st_plugin.py b/fsleyes_plugin_shimming_toolbox/st_plugin.py index 27f56e7..c1fb295 100644 --- a/fsleyes_plugin_shimming_toolbox/st_plugin.py +++ b/fsleyes_plugin_shimming_toolbox/st_plugin.py @@ -131,6 +131,13 @@ def create_sizer(self): sizer.Add(self.sizer_run, 2) return sizer + def create_sizer_run(self): + """Create the run sizer containing tab-specific functionality.""" + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.SetMinSize(400, 300) + sizer.AddSpacer(10) + return sizer + def create_empty_component(self): component = InputComponent(panel=self, input_text_box_metadata=[]) return component @@ -203,11 +210,14 @@ def open_documentation_url(self, event): class InputComponent(Component): - def __init__(self, panel, input_text_box_metadata): + """ Define cli to automatically generate help text """ + def __init__(self, panel, input_text_box_metadata, cli=None): super().__init__(panel) self.sizer = self.create_sizer() self.input_text_boxes = {} self.input_text_box_metadata = input_text_box_metadata + self.cli = cli + self.add_text_info() self.add_input_text_boxes() def create_sizer(self): @@ -215,6 +225,17 @@ def create_sizer(self): sizer = wx.BoxSizer(wx.VERTICAL) return sizer + def add_text_info(self): + """ This function adds the help text to the metadata. This function + needs to be called before creating the buttons. + + """ + for i, twb_dict in enumerate(self.input_text_box_metadata): + if not ('info_text' in twb_dict.keys()) and (self.cli is not None) and ('name' in twb_dict.keys()): + description = get_help_text(self.cli, twb_dict['name']) + self.input_text_box_metadata[i]['info_text'] = description + + def add_input_text_boxes(self, spacer_size=10): """Add a list of input text boxes (TextWithButton) to the sizer_input. @@ -244,7 +265,8 @@ def add_input_text_boxes(self, spacer_size=10): n_text_boxes=twb_dict.get("n_text_boxes", 1), name=twb_dict.get("name", "default"), info_text=twb_dict.get("info_text", ""), - required=twb_dict.get("required", False) + required=twb_dict.get("required", False), + load_in_overlay=twb_dict.get("load_in_overlay", False) ) self.add_input_text_box(text_with_button, twb_dict.get("name", "default")) @@ -275,37 +297,51 @@ def button_do_something(self, event): class DropdownComponent(Component): - def __init__(self, panel, dropdown_metadata, name, list_components=[], info_text=""): + def __init__(self, panel, dropdown_metadata, label, option_name, list_components=[], info_text=None, cli=None): """ Create a dropdown list Args: panel (wx.Panel): A panel is a window on which controls are placed. dropdown_metadata (list)(dict): A list of dictionaries where the dictionaries have the - required keys: ``label``, ``option_name``, ``option_value``. + required keys: ``label``, ``option_value``. .. code:: { "label": The label for the dropdown box - "option_name": The name of the option in the CLI "option_value": The value linked to the option in the CLI } - name (str): Label of the button describing the dropdown + label (str): Label of the button describing the dropdown + option_name (str): Name of the options of the dropdown, set to 'no_arg' is not an option list_components (list): list of InputComponents - info_text (str): Info message displayed when hovering over the "i" icon. + info_text (str): Info message displayed when hovering over the "i" icon. Leave blank to auto fill using option_name + cli (function): CLI function used by the dropdown """ super().__init__(panel, list_components) self.dropdown_metadata = dropdown_metadata - self.name = name + self.label = label self.info_text = info_text self.positions = {} self.input_text_boxes = {} self.sizer = self.create_sizer() self.dropdown_choices = [item["label"] for item in self.dropdown_metadata] + self.option_name = option_name + self.cli = cli + self.add_text_info() self.create_choice_box() self.create_dropdown_sizers() self.on_choice(None) + def add_text_info(self): + """ This function adds the help text. """ + + if self.info_text is None: + if self.cli is not None: + description = get_help_text(self.cli, self.option_name) + self.info_text = description + else: + self.info_text = "" + def create_dropdown_sizers(self): for index in range(len(self.list_components)): sizer = self.list_components[index].sizer @@ -321,7 +357,7 @@ def unshow_choice_box_sizers(self): def create_choice_box(self): self.choice_box = wx.Choice(self.panel, choices=self.dropdown_choices) self.choice_box.Bind(wx.EVT_CHOICE, self.on_choice) - button = wx.Button(self.panel, -1, label=self.name) + button = wx.Button(self.panel, -1, label=self.label) self.choice_box_sizer = wx.BoxSizer(wx.HORIZONTAL) self.choice_box_sizer.Add(create_info_icon(self.panel, self.info_text), 0, wx.ALIGN_LEFT | wx.RIGHT, 7) self.choice_box_sizer.Add(button, 0, wx.ALIGN_LEFT | wx.RIGHT, 10) @@ -347,9 +383,10 @@ def on_choice(self, event): self.input_text_boxes = self.list_components[index].input_text_boxes # Add the dropdown to the list of options - self.input_text_boxes[self.dropdown_metadata[index]["option_name"]] = \ - [self.dropdown_metadata[index]["option_value"]] + self.input_text_boxes[self.option_name] = [self.dropdown_metadata[index]["option_value"]] + # Update the window + self.panel.SetVirtualSize(self.panel.sizer_run.GetMinSize()) self.panel.Layout() def find_index(self, label): @@ -386,6 +423,7 @@ def __init__(self, panel, st_function, list_components=[], output_paths=[]): self.output = "" self.output_paths_original = output_paths self.output_paths = output_paths.copy() + self.load_in_overlay = [] self.worker = None self.panel.Bind(EVT_RESULT, self.on_result) @@ -427,6 +465,7 @@ def on_result(self, event): return data = event.get_data() + # Return code is 0 if everything ran smoothly if data == 0: msg = f"Run {self.st_function} completed successfully\n" self.panel.terminal_component.log_to_terminal(msg, level="INFO") @@ -445,6 +484,10 @@ def on_result(self, event): if os.path.isfile(self.output): self.output_paths.append(self.output) + # Append files that are a direct output from "load_in_overlay" + for fname in self.load_in_overlay: + self.output_paths.append(fname) + if self.st_function == "st_dicom_to_nifti": # If its dicom_to_nifti, output all .nii found in the subject folder to the overlay try: @@ -480,9 +523,10 @@ def on_result(self, event): else: # The error message should already be displayed - self.panel.terminal_component.log_to_terminal("") + self.panel.terminal_component.log_to_terminal(str(data), level="ERROR") self.worker = None + self.load_in_overlay = [] event.Skip() def button_run_on_click(self, event): @@ -492,7 +536,12 @@ def button_run_on_click(self, event): """ if not self.worker: - command, msg = self.get_run_args(self.st_function) + try: + command, msg = self.get_run_args(self.st_function) + except RunArgumentErrorST as err: + self.panel.terminal_component.log_to_terminal(err, level="ERROR") + return + self.panel.terminal_component.log_to_terminal(msg, level="INFO") self.worker = WorkerThread(self.panel, command, name=self.st_function) @@ -501,7 +550,7 @@ def send_output_to_overlay(self): if os.path.isfile(output_path): try: # Display the overlay - window = self.panel.GetGrandParent().GetParent() + window = self.panel.GetGrandParent() if output_path[-4:] == ".png": load_png_image_from_path(window, output_path, colormap="greyscale") elif output_path[-7:] == ".nii.gz" or output_path[-4:] == ".nii": @@ -517,18 +566,20 @@ def send_output_to_overlay(self): def get_run_args(self, st_function): """The option are a list of tuples where the tuple: (name, [value1, value2])""" msg = "Running " - command = st_function + # Split is necessary if we have grouped commands (st_mask threshold) + command = st_function.split(' ') self.output = "" command_list_arguments = [] command_list_options = [] for component in self.list_components: for name, input_text_box_list in component.input_text_boxes.items(): + if name == "no_arg": continue for input_text_box in input_text_box_list: - # Allows to chose from a dropdown + # Allows to choose from a dropdown if type(input_text_box) == str: command_list_options.append((name, [input_text_box])) @@ -540,7 +591,7 @@ def get_run_args(self, st_function): arg = textctrl.GetValue() if arg == "" or arg is None: if input_text_box.required is True: - raise self.RunArgumentErrorST( + raise RunArgumentErrorST( f"Argument {name} is missing a value, please enter a valid input" ) else: @@ -552,6 +603,8 @@ def get_run_args(self, st_function): else: if name == "output": self.output = arg + elif input_text_box.load_in_overlay: + self.load_in_overlay.append(arg) option_values.append(arg) @@ -561,20 +614,17 @@ def get_run_args(self, st_function): # Arguments don't need "-" for arg in command_list_arguments: - command += f" {arg}" + command += [arg] # Handles options for name, args in command_list_options: - command += f" --{name}" + command += ['--' + name] for arg in args: - command += f" {arg}" - msg += command - msg += "\n" - return command, msg + command += [arg] - class RunArgumentErrorST(Exception): - """Exception for missing input arguments for CLI call.""" - pass + msg += ' '.join(command) + '\n' + + return command, msg def fetch_paths_dicom_to_nifti(self): if self.st_function == "st_dicom_to_nifti": @@ -648,12 +698,15 @@ def on_choice(self, event): # items to show the appropriate things according to their current choice. if selection == 'Dynamic': self.dropdown_slice_dyn.on_choice(None) + self.dropdown_coil_format_dyn.on_choice(None) elif selection == 'Realtime Dynamic': self.dropdown_slice_rt.on_choice(None) + self.dropdown_coil_format_rt.on_choice(None) else: pass # Update the window + self.SetVirtualSize(self.sizer_run.GetMinSize()) self.Layout() def unshow_choice_box_sizers(self): @@ -687,53 +740,47 @@ def create_sizer_dynamic_shim(self, metadata=None): "button_label": "Input Fieldmap", "name": "fmap", "button_function": "select_from_overlay", - "info_text": f"{dynamic_cli.params[1].help}", "required": True }, { "button_label": "Input Anat", "name": "anat", "button_function": "select_from_overlay", - "info_text": f"{dynamic_cli.params[2].help}", "required": True }, { "button_label": "Input Mask", "name": "mask", "button_function": "select_from_overlay", - "info_text": f"{dynamic_cli.params[3].help}" }, { "button_label": "Mask Dilation Kernel Size", "name": "mask-dilation-kernel-size", - "info_text": f"{dynamic_cli.params[9].help}", "default_text": "3", } ] - component_inputs = InputComponent(self, input_text_box_metadata_inputs) + component_inputs = InputComponent(self, input_text_box_metadata_inputs, cli=dynamic_cli) input_text_box_metadata_scanner = [ { "button_label": "Scanner constraints", "button_function": "select_file", "name": "scanner-coil-constraints", - "info_text": f"{dynamic_cli.params[5].help}", "default_text": f"{os.path.join(ST_DIR, 'coil_config.json')}", }, ] - component_scanner = InputComponent(self, input_text_box_metadata_scanner) + component_scanner = InputComponent(self, input_text_box_metadata_scanner, cli=dynamic_cli) input_text_box_metadata_slice = [ { "button_label": "Slice Factor", "name": "slice-factor", - "info_text": f"{dynamic_cli.params[7].help}", "default_text": "1", }, ] - component_slice_int = InputComponent(self, input_text_box_metadata_slice) - component_slice_seq = InputComponent(self, input_text_box_metadata_slice) + component_slice_int = InputComponent(self, input_text_box_metadata_slice, cli=dynamic_cli) + component_slice_seq = InputComponent(self, input_text_box_metadata_slice, cli=dynamic_cli) output_metadata = [ { @@ -741,30 +788,25 @@ def create_sizer_dynamic_shim(self, metadata=None): "button_function": "select_folder", "default_text": path_output, "name": "output", - "info_text": f"{dynamic_cli.params[10].help}" } ] - component_output = InputComponent(self, output_metadata) + component_output = InputComponent(self, output_metadata, cli=dynamic_cli) dropdown_scanner_order_metadata = [ { "label": "-1", - "option_name": "scanner-coil-order", "option_value": "-1" }, { "label": "0", - "option_name": "scanner-coil-order", "option_value": "0" }, { "label": "1", - "option_name": "scanner-coil-order", "option_value": "1" }, { "label": "2", - "option_name": "scanner-coil-order", "option_value": "2" } ] @@ -772,19 +814,18 @@ def create_sizer_dynamic_shim(self, metadata=None): dropdown_scanner_order = DropdownComponent( panel=self, dropdown_metadata=dropdown_scanner_order_metadata, - name="Scanner Order", - info_text=f"{dynamic_cli.params[4].help}" + label="Scanner Order", + option_name = 'scanner-coil-order', + cli=dynamic_cli ) dropdown_ovf_metadata = [ { "label": "delta", - "option_name": "output-value-format", "option_value": "delta" }, { "label": "absolute", - "option_name": "output-value-format", "option_value": "absolute" } ] @@ -792,19 +833,27 @@ def create_sizer_dynamic_shim(self, metadata=None): dropdown_ovf = DropdownComponent( panel=self, dropdown_metadata=dropdown_ovf_metadata, - name="Output Value Format", - info_text=f"{dynamic_cli.params[13].help}" + label="Output Value Format", + option_name = 'output-value-format', + cli=dynamic_cli ) + reg_factor_metadata = [ + { + "button_label": "Regularization factor", + "default_text": '0.0', + "name": 'regularization-factor', + } + ] + component_reg_factor = InputComponent(self, reg_factor_metadata, cli=dynamic_cli) + dropdown_opt_metadata = [ { "label": "Least Squares", - "option_name": "optimizer-method", "option_value": "least_squares" }, { "label": "Pseudo Inverse", - "option_name": "optimizer-method", "option_value": "pseudo_inverse" }, ] @@ -812,24 +861,28 @@ def create_sizer_dynamic_shim(self, metadata=None): dropdown_opt = DropdownComponent( panel=self, dropdown_metadata=dropdown_opt_metadata, - name="Optimizer", - info_text=f"{dynamic_cli.params[8].help}" + label="Optimizer", + option_name = 'optimizer-method', + list_components = [component_reg_factor, + self.create_empty_component()], + cli=dynamic_cli ) dropdown_slice_metadata = [ + { + "label": "Auto detect", + "option_value": "auto" + }, { "label": "Sequential", - "option_name": "slices", "option_value": "sequential" }, { "label": "Interleaved", - "option_name": "slices", "option_value": "interleaved" }, { "label": "Volume", - "option_name": "slices", "option_value": "volume" }, ] @@ -837,65 +890,97 @@ def create_sizer_dynamic_shim(self, metadata=None): self.dropdown_slice_dyn = DropdownComponent( panel=self, dropdown_metadata=dropdown_slice_metadata, - name="Slice Ordering", - info_text="Defines the slice ordering.", - list_components=[component_slice_int, component_slice_seq, self.create_empty_component()] + label="Slice Ordering", + cli=dynamic_cli, + option_name = 'slices', + list_components=[self.create_empty_component(), + component_slice_seq, + component_slice_int, + self.create_empty_component()] ) dropdown_coil_format_metadata = [ { "label": "Slicewise per Channel", - "option_name": "output-file-format-coil", "option_value": "slicewise-ch" }, { "label": "Slicewise per Coil", - "option_name": "output-file-format-coil", "option_value": "slicewise-coil" }, { "label": "Chronological per Channel", - "option_name": "output-file-format-coil", "option_value": "chronological-ch" }, { "label": "Chronological per Coil", - "option_name": "output-file-format-coil", "option_value": "chronological-coil" }, ] - dropdown_coil_format = DropdownComponent( + dropdown_fatsat_metadata = [ + { + "label": "Auto detect", + "option_value": "auto" + }, + { + "label": "Yes", + "option_value": "yes" + }, + { + "label": "No", + "option_value": "no" + }, + ] + + dropdown_fatsat1 = DropdownComponent( + panel=self, + dropdown_metadata=dropdown_fatsat_metadata, + option_name = 'fatsat', + label="Fat Saturation", + cli=dynamic_cli + ) + + dropdown_fatsat2 = DropdownComponent( + panel=self, + dropdown_metadata=dropdown_fatsat_metadata, + option_name = 'fatsat', + label="Fat Saturation", + cli=dynamic_cli + ) + + self.dropdown_coil_format_dyn = DropdownComponent( panel=self, dropdown_metadata=dropdown_coil_format_metadata, - name="Custom Coil Output Format", - info_text=f"{dynamic_cli.params[11].help}" + label="Custom Coil Output Format", + option_name = 'output-file-format-coil', + cli=dynamic_cli, + list_components=[self.create_empty_component(), + self.create_empty_component(), + dropdown_fatsat1, + dropdown_fatsat2] ) dropdown_scanner_format_metadata = [ { "label": "Slicewise per Channel", - "option_name": "output-file-format-scanner", "option_value": "slicewise-ch" }, { "label": "Slicewise per Coil", - "option_name": "output-file-format-scanner", "option_value": "slicewise-coil" }, { "label": "Chronological per Channel", - "option_name": "output-file-format-scanner", + "option_value": "chronological-ch" }, { "label": "Chronological per Coil", - "option_name": "output-file-format-scanner", "option_value": "chronological-coil" }, { "label": "Gradient per Channel", - "option_name": "output-file-format-scanner", "option_value": "gradient" }, ] @@ -903,15 +988,16 @@ def create_sizer_dynamic_shim(self, metadata=None): dropdown_scanner_format = DropdownComponent( panel=self, dropdown_metadata=dropdown_scanner_format_metadata, - name="Scanner Output Format", - info_text=f"{dynamic_cli.params[12].help}" + option_name = 'output-file-format-scanner', + label="Scanner Output Format", + cli=dynamic_cli ) run_component = RunComponent( panel=self, list_components=[self.component_coils_dyn, component_inputs, dropdown_opt, self.dropdown_slice_dyn, - dropdown_scanner_order, component_scanner, dropdown_scanner_format, dropdown_coil_format, - dropdown_ovf, component_output], + dropdown_scanner_order, component_scanner, dropdown_scanner_format, + self.dropdown_coil_format_dyn, dropdown_ovf, component_output], st_function="st_b0shim dynamic", output_paths=["fieldmap_calculated_shim_masked.nii.gz", "fieldmap_calculated_shim.nii.gz"] @@ -931,73 +1017,65 @@ def create_sizer_realtime_shim(self, metadata=None): "info_text": "Number of phase NIfTI files to be used. Must be an integer > 0.", } ] - self.component_coils_rt = InputComponent(self, input_text_box_metadata_coil) + self.component_coils_rt = InputComponent(self, input_text_box_metadata_coil, cli=realtime_cli) input_text_box_metadata_inputs = [ { "button_label": "Input Fieldmap", "name": "fmap", "button_function": "select_from_overlay", - "info_text": f"{realtime_cli.params[1].help}", "required": True }, { "button_label": "Input Anat", "name": "anat", "button_function": "select_from_overlay", - "info_text": f"{realtime_cli.params[2].help}", "required": True }, { "button_label": "Input Respiratory Trace", "name": "resp", "button_function": "select_file", - "info_text": f"{realtime_cli.params[3].help}", "required": True }, { "button_label": "Input Mask Static", "name": "mask-static", "button_function": "select_from_overlay", - "info_text": f"{realtime_cli.params[4].help}" }, { "button_label": "Input Mask Realtime", "name": "mask-riro", "button_function": "select_from_overlay", - "info_text": f"{realtime_cli.params[5].help}" }, { "button_label": "Mask Dilation Kernel Size", "name": "mask-dilation-kernel-size", - "info_text": f"{realtime_cli.params[11].help}", "default_text": "3", } ] - component_inputs = InputComponent(self, input_text_box_metadata_inputs) + component_inputs = InputComponent(self, input_text_box_metadata_inputs, cli=realtime_cli) input_text_box_metadata_scanner = [ { "button_label": "Scanner constraints", "button_function": "select_file", "name": "scanner-coil-constraints", - "info_text": f"{realtime_cli.params[7].help}", "default_text": f"{os.path.join(ST_DIR, 'coil_config.json')}", }, ] - component_scanner = InputComponent(self, input_text_box_metadata_scanner) + component_scanner = InputComponent(self, input_text_box_metadata_scanner, cli=realtime_cli) input_text_box_metadata_slice = [ { "button_label": "Slice Factor", "name": "slice-factor", - "info_text": f"{realtime_cli.params[9].help}", "default_text": "1", }, ] - component_slice_int = InputComponent(self, input_text_box_metadata_slice) - component_slice_seq = InputComponent(self, input_text_box_metadata_slice) + component_slice_int = InputComponent(self, input_text_box_metadata_slice, cli=realtime_cli) + component_slice_seq = InputComponent(self, input_text_box_metadata_slice, cli=realtime_cli) output_metadata = [ { @@ -1005,30 +1083,25 @@ def create_sizer_realtime_shim(self, metadata=None): "button_function": "select_folder", "default_text": path_output, "name": "output", - "info_text": f"{realtime_cli.params[12].help}" } ] - component_output = InputComponent(self, output_metadata) + component_output = InputComponent(self, output_metadata, cli=realtime_cli) dropdown_scanner_order_metadata = [ { "label": "-1", - "option_name": "scanner-coil-order", "option_value": "-1" }, { "label": "0", - "option_name": "scanner-coil-order", "option_value": "0" }, { "label": "1", - "option_name": "scanner-coil-order", "option_value": "1" }, { "label": "2", - "option_name": "scanner-coil-order", "option_value": "2" } ] @@ -1036,19 +1109,18 @@ def create_sizer_realtime_shim(self, metadata=None): dropdown_scanner_order = DropdownComponent( panel=self, dropdown_metadata=dropdown_scanner_order_metadata, - name="Scanner Order", - info_text=f"{realtime_cli.params[6].help}" + label="Scanner Order", + option_name = 'scanner-coil-order', + cli=realtime_cli ) dropdown_ovf_metadata = [ { "label": "delta", - "option_name": "output-value-format", "option_value": "delta" }, { "label": "absolute", - "option_name": "output-value-format", "option_value": "absolute" } ] @@ -1056,19 +1128,28 @@ def create_sizer_realtime_shim(self, metadata=None): dropdown_ovf = DropdownComponent( panel=self, dropdown_metadata=dropdown_ovf_metadata, - name="Output Value Format", - info_text=f"{realtime_cli.params[15].help}" + label="Output Value Format", + option_name = 'output-value-format', + cli=realtime_cli ) + + reg_factor_metadata = [ + { + "button_label": "Regularization factor", + "default_text": '0.0', + "name": 'regularization-factor', + } + ] + component_reg_factor = InputComponent(self, reg_factor_metadata, cli=dynamic_cli) + dropdown_opt_metadata = [ { "label": "Least Squares", - "option_name": "optimizer-method", "option_value": "least_squares" }, { "label": "Pseudo Inverse", - "option_name": "optimizer-method", "option_value": "pseudo_inverse" }, ] @@ -1076,24 +1157,28 @@ def create_sizer_realtime_shim(self, metadata=None): dropdown_opt = DropdownComponent( panel=self, dropdown_metadata=dropdown_opt_metadata, - name="Optimizer", - info_text=f"{realtime_cli.params[10].help}" + label="Optimizer", + option_name = 'optimizer-method', + list_components= [component_reg_factor, + self.create_empty_component()], + cli=realtime_cli ) dropdown_slice_metadata = [ + { + "label": "Auto detect", + "option_value": "auto" + }, { "label": "Sequential", - "option_name": "slices", "option_value": "sequential" }, { "label": "Interleaved", - "option_name": "slices", "option_value": "interleaved" }, { "label": "Volume", - "option_name": "slices", "option_value": "volume" }, ] @@ -1101,61 +1186,87 @@ def create_sizer_realtime_shim(self, metadata=None): self.dropdown_slice_rt = DropdownComponent( panel=self, dropdown_metadata=dropdown_slice_metadata, - name="Slice Ordering", - info_text=f"{realtime_cli.params[8].help}", - list_components=[component_slice_int, component_slice_seq, self.create_empty_component()] + label="Slice Ordering", + option_name = 'slices', + list_components=[self.create_empty_component(), + component_slice_seq, + component_slice_int, + self.create_empty_component()], + cli=realtime_cli ) - dropdown_coil_format_metadata = [ + dropdown_scanner_format_metadata = [ { "label": "Slicewise per Channel", - "option_name": "output-file-format-coil", "option_value": "slicewise-ch" }, { "label": "Chronological per Channel", - "option_name": "output-file-format-coil", "option_value": "chronological-ch" - } + }, + { + "label": "Gradient per Channel", + "option_value": "gradient" + }, ] - dropdown_coil_format = DropdownComponent( + dropdown_scanner_format = DropdownComponent( panel=self, - dropdown_metadata=dropdown_coil_format_metadata, - name="Custom Coil Output Format", - info_text=f"{realtime_cli.params[13].help}" + dropdown_metadata=dropdown_scanner_format_metadata, + label="Scanner Output Format", + option_name = 'output-file-format-scanner', + cli=realtime_cli ) - dropdown_scanner_format_metadata = [ + dropdown_fatsat_metadata = [ + { + "label": "Auto detect", + "option_value": "auto" + }, + { + "label": "Yes", + "option_value": "yes" + }, + { + "label": "No", + "option_value": "no" + }, + ] + + dropdown_fatsat = DropdownComponent( + panel=self, + dropdown_metadata=dropdown_fatsat_metadata, + label="Fat Saturation", + option_name = 'fatsat', + cli=realtime_cli + ) + + dropdown_coil_format_metadata = [ { "label": "Slicewise per Channel", - "option_name": "output-file-format-scanner", "option_value": "slicewise-ch" }, { "label": "Chronological per Channel", - "option_name": "output-file-format-scanner", "option_value": "chronological-ch" - }, - { - "label": "Gradient per Channel", - "option_name": "output-file-format-scanner", - "option_value": "gradient" - }, + } ] - dropdown_scanner_format = DropdownComponent( + self.dropdown_coil_format_rt = DropdownComponent( panel=self, - dropdown_metadata=dropdown_scanner_format_metadata, - name="Scanner Output Format", - info_text=f"{realtime_cli.params[14].help}" + dropdown_metadata=dropdown_coil_format_metadata, + label="Custom Coil Output Format", + option_name = 'output-file-format-coil', + cli=realtime_cli, + list_components=[self.create_empty_component(), + dropdown_fatsat] ) run_component = RunComponent( panel=self, list_components=[self.component_coils_rt, component_inputs, dropdown_opt, self.dropdown_slice_rt, dropdown_scanner_order, component_scanner, dropdown_scanner_format, - dropdown_coil_format, dropdown_ovf, component_output], + self.dropdown_coil_format_rt, dropdown_ovf, component_output], st_function="st_b0shim realtime-dynamic", # TODO: output paths output_paths=[] @@ -1163,13 +1274,6 @@ def create_sizer_realtime_shim(self, metadata=None): sizer = run_component.sizer return sizer - def create_sizer_run(self): - """Create the centre sizer containing tab-specific functionality.""" - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.SetMinSize(400, 300) - sizer.AddSpacer(10) - return sizer - class B1ShimTab(Tab): def __init__(self, parent, title=r"B1+ Shim"): @@ -1228,6 +1332,7 @@ def on_choice(self, event): pass # Update the window + self.SetVirtualSize(self.sizer_run.GetMinSize()) self.Layout() def unshow_choice_box_sizers(self): @@ -1249,37 +1354,32 @@ def create_sizer_cv(self, metadata=None): "button_label": "Input B1+ map", "name": "b1", "button_function": "select_from_overlay", - "info_text": f"{b1shim_cli.params[0].help}", "required": True }, { "button_label": "Input Mask", "name": "mask", "button_function": "select_from_overlay", - "info_text": f"{b1shim_cli.params[1].help}" }, { "button_label": "Input VOP file", "name": "vop", "button_function": "select_file", - "info_text": f"{b1shim_cli.params[4].help}" }, { "button_label": "SAR factor", "name": "sar_factor", "default_text": "1.5", - "info_text": f"{b1shim_cli.params[5].help}" }, { "button_label": "Output Folder", "button_function": "select_folder", "default_text": path_output, "name": "output", - "info_text": f"{b1shim_cli.params[6].help}" } ] - component = InputComponent(self, input_text_box_metadata) + component = InputComponent(self, input_text_box_metadata, cli=b1shim_cli) run_component = RunComponent( panel=self, list_components=[component], @@ -1296,44 +1396,38 @@ def create_sizer_target(self, metadata=None): "button_label": "Input B1+ map", "name": "b1", "button_function": "select_from_overlay", - "info_text": f"{b1shim_cli.params[0].help}", "required": True }, { "button_label": "Input Mask", "name": "mask", "button_function": "select_from_overlay", - "info_text": f"{b1shim_cli.params[1].help}" }, { "button_label": "Target value (nT/V)", "name": "target", "default_text": "20", - "info_text": f"{b1shim_cli.params[3].help}", "required": True }, { "button_label": "Input VOP file", "name": "vop", "button_function": "select_file", - "info_text": f"{b1shim_cli.params[4].help}" }, { "button_label": "SAR factor", "name": "sar_factor", "default_text": "1.5", - "info_text": f"{b1shim_cli.params[5].help}" }, { "button_label": "Output Folder", "button_function": "select_folder", "default_text": path_output, "name": "output", - "info_text": f"{b1shim_cli.params[6].help}" } ] - component = InputComponent(self, input_text_box_metadata) + component = InputComponent(self, input_text_box_metadata, cli=b1shim_cli) run_component = RunComponent( panel=self, list_components=[component], @@ -1350,38 +1444,33 @@ def create_sizer_sar_eff(self, metadata=None): "button_label": "Input B1+ map", "name": "b1", "button_function": "select_from_overlay", - "info_text": f"{b1shim_cli.params[0].help}", "required": True }, { "button_label": "Input Mask", "name": "mask", "button_function": "select_from_overlay", - "info_text": f"{b1shim_cli.params[1].help}" }, { "button_label": "Input VOP file", "name": "vop", "button_function": "select_file", - "info_text": f"{b1shim_cli.params[4].help}", "required": True }, { "button_label": "SAR factor", "name": "sar_factor", "default_text": "1.5", - "info_text": f"{b1shim_cli.params[5].help}" }, { "button_label": "Output Folder", "button_function": "select_folder", "default_text": path_output, "name": "output", - "info_text": f"{b1shim_cli.params[6].help}" } ] - component = InputComponent(self, input_text_box_metadata) + component = InputComponent(self, input_text_box_metadata, cli=b1shim_cli) run_component = RunComponent( panel=self, list_components=[component], @@ -1398,24 +1487,21 @@ def create_sizer_phase_only(self, metadata=None): "button_label": "Input B1+ maps", "name": "b1", "button_function": "select_from_overlay", - "info_text": f"{b1shim_cli.params[0].help}", "required": True }, { "button_label": "Input Mask", "name": "mask", "button_function": "select_from_overlay", - "info_text": f"{b1shim_cli.params[1].help}" }, { "button_label": "Output Folder", "button_function": "select_folder", "default_text": path_output, "name": "output", - "info_text": f"{b1shim_cli.params[6].help}" } ] - component = InputComponent(self, input_text_box_metadata) + component = InputComponent(self, input_text_box_metadata, cli=b1shim_cli) run_component = RunComponent( panel=self, list_components=[component], @@ -1425,13 +1511,6 @@ def create_sizer_phase_only(self, metadata=None): sizer = run_component.sizer return sizer - def create_sizer_run(self): - """Create the centre sizer containing tab-specific functionality.""" - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.SetMinSize(400, 300) - sizer.AddSpacer(10) - return sizer - class FieldMapTab(Tab): def __init__(self, parent, title="Fieldmap"): @@ -1439,7 +1518,17 @@ def __init__(self, parent, title="Fieldmap"): "Enter the Number of Echoes then press the `Number of Echoes` button.\n\n" \ "Select the unwrapper from the dropdown list." super().__init__(parent, title, description) + + self.sizer_run = self.create_sizer_run() self.n_echoes = 0 + sizer = self.create_fieldmap_sizer() + self.sizer_run.Add(sizer, 0, wx.EXPAND) + + self.parent_sizer = self.create_sizer() + self.SetSizer(self.parent_sizer) + + def create_fieldmap_sizer(self): + input_text_box_metadata_input = [ { "button_label": "Number of Echoes", @@ -1449,73 +1538,89 @@ def __init__(self, parent, title="Fieldmap"): "required": True } ] - dropdown_metadata = [ + self.component_input = InputComponent( + panel=self, + input_text_box_metadata=input_text_box_metadata_input + ) + + dropdown_metadata_unwrapper = [ { "label": "prelude", - "option_name": "unwrapper", "option_value": "prelude" } ] - - dropdown_mask_threshold = [ - { - "label": "mask", - "option_name": "no_arg", - "option_value": "" - }, - { - "label": "threshold", - "option_name": "no_arg", - "option_value": "" - }, - ] - - path_output = os.path.join(CURR_DIR, "output_fieldmap") - - input_text_box_metadata_output = [ - { - "button_label": "Output File", - "button_function": "select_folder", - "default_text": os.path.join(path_output, "fieldmap.nii.gz"), - "name": "output", - "info_text": f"{prepare_fieldmap_cli.params[3].help}", - "required": True - } - ] + self.dropdown = DropdownComponent( + panel=self, + dropdown_metadata=dropdown_metadata_unwrapper, + label="Unwrapper", + option_name = 'unwrapper', + cli=prepare_fieldmap_cli + ) mask_metadata = [ { "button_label": "Input Mask", "button_function": "select_from_overlay", "name": "mask", - "info_text": f"{prepare_fieldmap_cli.params[5].help}" } ] - self.component_mask = InputComponent( panel=self, - input_text_box_metadata=mask_metadata + input_text_box_metadata=mask_metadata, + cli=prepare_fieldmap_cli ) threshold_metadata = [ { "button_label": "Threshold", "name": "threshold", - "info_text": f"{prepare_fieldmap_cli.params[6].help}" + }, + { + "button_label": "Output Calculated Mask", + "name": "savemask", + "load_in_overlay": True } ] - self.component_threshold = InputComponent( panel=self, - input_text_box_metadata=threshold_metadata + input_text_box_metadata=threshold_metadata, + cli=prepare_fieldmap_cli ) + dropdown_mask_threshold = [ + { + "label": "mask", + "option_value": "" + }, + { + "label": "threshold", + "option_value": "" + }, + ] self.dropdown_mask_threshold = DropdownComponent( panel=self, dropdown_metadata=dropdown_mask_threshold, - name="Mask/Threshold", + label="Mask/Threshold", info_text="Masking methods either with a file input or a threshold", - list_components=[self.component_mask, self.component_threshold] + option_name = 'no_arg', + list_components=[self.component_mask, self.component_threshold], + cli=prepare_fieldmap_cli + ) + + path_output = os.path.join(CURR_DIR, "output_fieldmap") + input_text_box_metadata_output = [ + { + "button_label": "Output File", + "button_function": "select_folder", + "default_text": os.path.join(path_output, "fieldmap.nii.gz"), + "name": "output", + "required": True + } + ] + self.component_output = InputComponent( + panel=self, + input_text_box_metadata=input_text_box_metadata_output, + cli=prepare_fieldmap_cli ) input_text_box_metadata_input2 = [ @@ -1523,37 +1628,23 @@ def __init__(self, parent, title="Fieldmap"): "button_label": "Input Magnitude", "button_function": "select_from_overlay", "name": "mag", - "info_text": f"{prepare_fieldmap_cli.params[1].help}", "required": True } ] - self.component_input = InputComponent( - panel=self, - input_text_box_metadata=input_text_box_metadata_input - ) self.component_input2 = InputComponent( panel=self, - input_text_box_metadata=input_text_box_metadata_input2 - ) - self.dropdown = DropdownComponent( - panel=self, - dropdown_metadata=dropdown_metadata, - name="Unwrapper", - info_text=f"{prepare_fieldmap_cli.params[2].help}" - ) - self.component_output = InputComponent( - panel=self, - input_text_box_metadata=input_text_box_metadata_output + input_text_box_metadata=input_text_box_metadata_input2, + cli=prepare_fieldmap_cli ) + self.run_component = RunComponent( panel=self, list_components=[self.component_input, self.component_input2, self.dropdown_mask_threshold, self.dropdown, self.component_output], st_function="st_prepare_fieldmap" ) - self.sizer_run = self.run_component.sizer - sizer = self.create_sizer() - self.SetSizer(sizer) + + return self.run_component.sizer class MaskTab(Tab): @@ -1607,8 +1698,8 @@ def on_choice(self, event): pass # Update the window + self.SetVirtualSize(self.sizer_run.GetMinSize()) self.Layout() - self.GetParent().Layout() def unshow_choice_box_sizers(self): """Set the Show variable to false for all sizers of the choice box widget""" @@ -1629,24 +1720,21 @@ def create_sizer_threshold(self, metadata=None): "button_label": "Input", "button_function": "select_from_overlay", "name": "input", - "info_text": f"{threshold.params[0].help}", "required": True }, { "button_label": "Threshold", "default_text": "30", "name": "thr", - "info_text": f"{threshold.params[2].help}" }, { "button_label": "Output File", "button_function": "select_folder", "default_text": os.path.join(path_output, "mask.nii.gz"), "name": "output", - "info_text": f"{threshold.params[1].help}" } ] - component = InputComponent(self, input_text_box_metadata) + component = InputComponent(self, input_text_box_metadata, cli=threshold) run_component = RunComponent( panel=self, list_components=[component], @@ -1662,31 +1750,27 @@ def create_sizer_rect(self): "button_label": "Input", "button_function": "select_from_overlay", "name": "input", - "info_text": f"{rect.params[0].help}", "required": True }, { "button_label": "Size", "name": "size", "n_text_boxes": 2, - "info_text": f"{rect.params[2].help}", "required": True }, { "button_label": "Center", "name": "center", "n_text_boxes": 2, - "info_text": f"{rect.params[3].help}" }, { "button_label": "Output File", "button_function": "select_folder", "default_text": os.path.join(path_output, "mask.nii.gz"), "name": "output", - "info_text": f"{rect.params[1].help}" } ] - component = InputComponent(self, input_text_box_metadata) + component = InputComponent(self, input_text_box_metadata, cli=rect) run_component = RunComponent( panel=self, list_components=[component], @@ -1702,31 +1786,27 @@ def create_sizer_box(self): "button_label": "Input", "button_function": "select_from_overlay", "name": "input", - "info_text": f"{box.params[0].help}", "required": True }, { "button_label": "Size", "name": "size", "n_text_boxes": 3, - "info_text": f"{box.params[2].help}", "required": True }, { "button_label": "Center", "name": "center", "n_text_boxes": 3, - "info_text": f"{box.params[3].help}" }, { "button_label": "Output File", "button_function": "select_folder", "default_text": os.path.join(path_output, "mask.nii.gz"), "name": "output", - "info_text": f"{box.params[1].help}" } ] - component = InputComponent(self, input_text_box_metadata) + component = InputComponent(self, input_text_box_metadata, box) run_component = RunComponent( panel=self, list_components=[component], @@ -1735,31 +1815,31 @@ def create_sizer_box(self): sizer = run_component.sizer return sizer - def create_sizer_run(self): - """Create the centre sizer containing tab-specific functionality.""" - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.SetMinSize(400, 300) - sizer.AddSpacer(10) - return sizer - class DicomToNiftiTab(Tab): def __init__(self, parent, title="Dicom to Nifti"): description = "Convert DICOM files into NIfTI following the BIDS data structure" super().__init__(parent, title, description) + + self.sizer_run = self.create_sizer_run() + sizer = self.create_dicom_to_nifti_sizer() + self.sizer_run.Add(sizer, 0, wx.EXPAND) + + self.parent_sizer = self.create_sizer() + self.SetSizer(self.parent_sizer) + + def create_dicom_to_nifti_sizer(self): path_output = os.path.join(CURR_DIR, "output_dicom_to_nifti") input_text_box_metadata = [ { "button_label": "Input Folder", "button_function": "select_folder", "name": "input", - "info_text": f"{dicom_to_nifti_cli.params[0].help}", "required": True }, { "button_label": "Subject Name", "name": "subject", - "info_text": f"{dicom_to_nifti_cli.params[1].help}", "required": True }, { @@ -1767,21 +1847,17 @@ def __init__(self, parent, title="Dicom to Nifti"): "button_function": "select_file", "default_text": os.path.join(ST_DIR, "dcm2bids.json"), "name": "config", - "info_text": f"{dicom_to_nifti_cli.params[3].help}" }, { "button_label": "Output Folder", "button_function": "select_folder", "default_text": path_output, "name": "output", - "info_text": f"{dicom_to_nifti_cli.params[2].help}" } ] - component = InputComponent(self, input_text_box_metadata) + component = InputComponent(self, input_text_box_metadata, cli=dicom_to_nifti_cli) run_component = RunComponent(panel=self, list_components=[component], st_function="st_dicom_to_nifti") - self.sizer_run = run_component.sizer - sizer = self.create_sizer() - self.SetSizer(sizer) + return run_component.sizer class TextWithButton: @@ -1811,7 +1887,7 @@ class TextWithButton: """ def __init__(self, panel, button_label, button_function, name="default", default_text="", - n_text_boxes=1, info_text="", required=False): + n_text_boxes=1, info_text="", required=False, load_in_overlay=False): self.panel = panel self.button_label = button_label if type(button_function) is not list: @@ -1823,6 +1899,7 @@ def __init__(self, panel, button_label, button_function, name="default", default self.name = name self.info_text = info_text self.required = required + self.load_in_overlay = load_in_overlay def create(self): text_with_button_box = wx.BoxSizer(wx.HORIZONTAL) @@ -1972,7 +2049,7 @@ def select_from_overlay(event, tab, ctrl, focus=False): # This is messy and wont work if we change any class hierarchy.. using GetTopLevelParent() only # works if the pane is not floating # Get the displayCtx class initialized in STControlPanel - window = tab.GetGrandParent().GetParent() + window = tab.GetGrandParent() selected_overlay = window.displayCtx.getSelectedOverlay() if selected_overlay is not None: filename_path = selected_overlay.dataSource @@ -2217,3 +2294,19 @@ def load_png_image_from_path(fsl_panel, image_path, is_mask=False, add_to_overla opts.cmap = colormap return img_overlay + + +def get_help_text(cli_function, name): + """ Returns the help text of a cli function depending on its name. """ + for param in cli_function.params: + # Try different versions of the input dashes + for dashes in ['', '-', '--']: + new_name = dashes + name + if (new_name in param.opts) or new_name == param.human_readable_name: + return param.help + + raise ValueError(f"Could not find param: {name} in {cli_function.name}") + +class RunArgumentErrorST(Exception): + """Exception for missing input arguments for CLI call.""" + pass diff --git a/fsleyes_plugin_shimming_toolbox/worker_thread.py b/fsleyes_plugin_shimming_toolbox/worker_thread.py index e944776..9729596 100644 --- a/fsleyes_plugin_shimming_toolbox/worker_thread.py +++ b/fsleyes_plugin_shimming_toolbox/worker_thread.py @@ -29,9 +29,9 @@ def run(self): # It seems to default to the Python executalble instead of the Shebang, removing it fixes it env["PYTHONEXECUTABLE"] = "" env["PATH"] = PATH_ST_VENV + ":" + env["PATH"] - + # Run command using realtime output - process = subprocess.Popen(self.cmd.split(' '), + process = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, diff --git a/installer/create_st_dir.sh b/installer/create_st_dir.sh index 16e7a52..48cafec 100644 --- a/installer/create_st_dir.sh +++ b/installer/create_st_dir.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" -source $SCRIPT_DIR/utils.sh +source "$SCRIPT_DIR/utils.sh" ST_DIR="$HOME/shimming-toolbox" PYTHON_DIR="python" diff --git a/installer/create_venv.sh b/installer/create_venv.sh index 14444ce..766fa7c 100644 --- a/installer/create_venv.sh +++ b/installer/create_venv.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" -source $SCRIPT_DIR/utils.sh +source "$SCRIPT_DIR/utils.sh" ST_DIR="$HOME/shimming-toolbox" PYTHON_DIR=python diff --git a/installer/install_conda.sh b/installer/install_conda.sh index 69a23ae..e58431b 100644 --- a/installer/install_conda.sh +++ b/installer/install_conda.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" -source $SCRIPT_DIR/utils.sh +source "$SCRIPT_DIR/utils.sh" print info "Installing conda in $ST_DIR/$PYTHON_DIR" diff --git a/installer/install_plugin.sh b/installer/install_plugin.sh index dbf4d91..f625c16 100644 --- a/installer/install_plugin.sh +++ b/installer/install_plugin.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" -source $SCRIPT_DIR/utils.sh +source "$SCRIPT_DIR/utils.sh" set -e diff --git a/installer/install_shimming_toolbox.sh b/installer/install_shimming_toolbox.sh index 38a8bae..6592639 100644 --- a/installer/install_shimming_toolbox.sh +++ b/installer/install_shimming_toolbox.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" -source $SCRIPT_DIR/utils.sh +source "$SCRIPT_DIR/utils.sh" set -e @@ -14,7 +14,7 @@ rm -rf "${ST_DIR}/shimming-toolbox" print info "Downloading Shimming-Toolbox" -ST_VERSION=311d177ef17551f8df401cdcf936f051cade145f +ST_VERSION=7087b9f20ef2bb0f1366dd8e6a8dfc440b349aed curl -L "https://github.com/shimming-toolbox/shimming-toolbox/archive/${ST_VERSION}.zip" > "shimming-toolbox-${ST_VERSION}.zip"