diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4158edf..03bd5d4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: - 'ubuntu-22.04' - 'ubuntu-20.04' python-version: - - '3.9' + - '3.10' steps: - name: Get home directory diff --git a/fsleyes_plugin_shimming_toolbox/components/run_component.py b/fsleyes_plugin_shimming_toolbox/components/run_component.py index 9c3e471..4f59ded 100644 --- a/fsleyes_plugin_shimming_toolbox/components/run_component.py +++ b/fsleyes_plugin_shimming_toolbox/components/run_component.py @@ -185,21 +185,21 @@ def get_run_args(self, st_function): msg = "Running " # Split is necessary if we have grouped commands (st_mask threshold) command = st_function.split(' ') - - for component in self.list_components: - + + for component in self.list_components: + cmd, output, load_in_overlay = component.get_command() command.extend(cmd) - - if st_function.split(' ')[-1] == "realtime-dynamic" and cmd[0] == "--coil": + + if st_function.split(' ')[-1] == "realtime-dynamic" and cmd and cmd[0] == "--coil": cmd_riro = ['--coil-riro' if i == '--coil' else i for i in cmd] command.extend(cmd_riro) - + self.load_in_overlay.extend(load_in_overlay) - + if output: self.output = output - + msg += ' '.join(command) + '\n' return command, msg diff --git a/fsleyes_plugin_shimming_toolbox/tabs/b0shim_tab.py b/fsleyes_plugin_shimming_toolbox/tabs/b0shim_tab.py index 39b02c1..4e7d6b8 100644 --- a/fsleyes_plugin_shimming_toolbox/tabs/b0shim_tab.py +++ b/fsleyes_plugin_shimming_toolbox/tabs/b0shim_tab.py @@ -233,6 +233,10 @@ def create_sizer_dynamic_shim(self, metadata=None): { "label": "2", "option_value": "2" + }, + { + "label": "3", + "option_value": "3" } ] @@ -241,8 +245,8 @@ def create_sizer_dynamic_shim(self, metadata=None): label="Scanner Order", checkbox_metadata=checkbox_scanner_order_metadata, option_name='scanner-coil-order', - components_dict=[{'object': dropdown_scanner_format, 'checkbox': ['f0', '1', '2']}, - {'object': component_scanner, 'checkbox': ['f0', '1', '2']}], + components_dict=[{'object': dropdown_scanner_format, 'checkbox': ['f0', '1', '2', '3']}, + {'object': component_scanner, 'checkbox': ['f0', '1', '2', '3']}], ) dropdown_ovf_metadata = [ @@ -283,6 +287,10 @@ def create_sizer_dynamic_shim(self, metadata=None): "label": "Mean Absolute Error", "option_value": "mae", }, + { + "label": "Root Mean Squared Error", + "option_value": "rmse", + } ] dropdown_crit = DropdownComponent( @@ -520,7 +528,7 @@ def create_sizer_realtime_shim(self, metadata=None): panel=self, dropdown_metadata=dropdown_scanner_format_metadata, label="Scanner Output Format", - option_name = 'output-file-format-scanner', + option_name='output-file-format-scanner', cli=realtime_cli ) @@ -531,12 +539,17 @@ def create_sizer_realtime_shim(self, metadata=None): }, { "label": "1", - "option_value": "0,1" + "option_value": "1" }, { "label": "2", - "option_value": "0,1,2" - } + "option_value": "2" + }, + { + "label": "3", + "option_value": "3" + }, + ] input_text_box_metadata_scanner = [ @@ -547,14 +560,14 @@ def create_sizer_realtime_shim(self, metadata=None): }, ] component_scanner = InputComponent(self, input_text_box_metadata_scanner, cli=realtime_cli) - + self.checkbox_scanner_order_rt = CheckboxComponent( panel=self, label="Scanner Order", checkbox_metadata=checkbox_scanner_order_metadata, option_name='scanner-coil-order', - components_dict=[{'object': dropdown_scanner_format, 'checkbox': ['f0', '1', '2']}, - {'object': component_scanner, 'checkbox': ['f0', '1', '2']}], + components_dict=[{'object': dropdown_scanner_format, 'checkbox': ['f0', '1', '2', '3']}, + {'object': component_scanner, 'checkbox': ['f0', '1', '2', '3']}], additional_sizer_dict={'info text': None, 'label': 'Scanner Order RIRO', 'option name': 'scanner-coil-order-riro', 'checkbox metadata': checkbox_scanner_order_metadata} @@ -575,7 +588,7 @@ def create_sizer_realtime_shim(self, metadata=None): panel=self, dropdown_metadata=dropdown_ovf_metadata, label="Output Value Format", - option_name = 'output-value-format', + option_name='output-value-format', cli=realtime_cli ) @@ -598,6 +611,10 @@ def create_sizer_realtime_shim(self, metadata=None): "label": "Mean Absolute Error", "option_value": "mae", }, + { + "label": "Root Mean Squared Error", + "option_value": "rmse", + } ] dropdown_crit = DropdownComponent( @@ -659,7 +676,7 @@ def create_sizer_realtime_shim(self, metadata=None): panel=self, dropdown_metadata=dropdown_slice_metadata, label="Slice Ordering", - option_name = 'slices', + option_name='slices', list_components=[self.create_empty_component(), component_slice_seq, component_slice_int, @@ -686,7 +703,7 @@ def create_sizer_realtime_shim(self, metadata=None): panel=self, dropdown_metadata=dropdown_fatsat_metadata, label="Fat Saturation", - option_name = 'fatsat', + option_name='fatsat', cli=realtime_cli ) @@ -705,7 +722,7 @@ def create_sizer_realtime_shim(self, metadata=None): panel=self, dropdown_metadata=dropdown_coil_format_metadata, label="Custom Coil Output Format", - option_name = 'output-file-format-coil', + option_name='output-file-format-coil', cli=realtime_cli, list_components=[self.create_empty_component(), dropdown_fatsat] diff --git a/installer/install_shimming_toolbox.sh b/installer/install_shimming_toolbox.sh index 630b872..10b39f2 100644 --- a/installer/install_shimming_toolbox.sh +++ b/installer/install_shimming_toolbox.sh @@ -14,7 +14,7 @@ rm -rf "${ST_DIR}/shimming-toolbox" print info "Downloading Shimming-Toolbox" -ST_VERSION="7701d3ae54f1335c101b7d197b002a46a2e569d2" +ST_VERSION="cf28d353240fd510de1e8aa492c6c23ae57e66bd" curl -L "https://github.com/shimming-toolbox/shimming-toolbox/archive/${ST_VERSION}.zip" > "shimming-toolbox-${ST_VERSION}.zip" diff --git a/setup.py b/setup.py index 844df0d..b5e00a3 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,8 @@ name='fsleyes-plugin-shimming-toolbox', install_requires=[ "imageio", - 'pre-commit>=2.10.0' + 'pre-commit>=2.10.0', + 'numpy<2.0' ], packages=find_packages(exclude=['.git']), include_package_data=True, diff --git a/test/gui/test_b0shim_tab.py b/test/gui/test_b0shim_tab.py index dc9dc05..773d464 100644 --- a/test/gui/test_b0shim_tab.py +++ b/test/gui/test_b0shim_tab.py @@ -7,6 +7,7 @@ import os import pathlib from shimmingtoolbox.masking.shapes import shapes +import shutil import tempfile import time import wx @@ -46,8 +47,8 @@ def test_st_plugin_b0shim_dyn_lsq_mae(): def _test_st_plugin_b0shim_dyn(view, overlayList, displayCtx, options=options): __test_st_plugin_b0shim_dyn(view, overlayList, displayCtx, options=options) run_with_orthopanel(_test_st_plugin_b0shim_dyn) - - + + def test_st_plugin_b0shim_dyn_lsq_mse_coil_only(): options = {'optimizer-method': 'Least Squares', 'optimizer-criteria': 'Mean Absolute Error', @@ -75,8 +76,8 @@ def test_st_plugin_b0shim_dyn_pi(): def _test_st_plugin_b0shim_dyn(view, overlayList, displayCtx, options=options): __test_st_plugin_b0shim_dyn(view, overlayList, displayCtx, options=options) run_with_orthopanel(_test_st_plugin_b0shim_dyn) - - + + def test_st_plugin_b0shim_dyn_qp(): options = {'optimizer-method': 'Quad Prog', 'optimizer-criteria': 'Mean Squared Error', @@ -96,7 +97,7 @@ def __test_st_plugin_b0shim_dyn(view, overlayList, displayCtx, options): """ Makes sure the B0 shim tab runs (Add dummy input and simulate a click) """ nb_terminal = get_notebook(view) - # Select the Fieldmap tab + # Select the b0Shim tab assert set_notebook_page(nb_terminal, 'B0 Shim') # Get the ST tab @@ -104,7 +105,7 @@ def __test_st_plugin_b0shim_dyn(view, overlayList, displayCtx, options): assert b0shim_tab is not None with tempfile.TemporaryDirectory(prefix='st_' + pathlib.Path(__file__).stem) as tmp: - nii_fmap, nii_anat, nii_mask, nii_coil, fm_data, anat_data, coil_data = _define_inputs(fmap_dim=3) + nii_fmap, nii_anat, nii_mask, nii_coil, fm_data, anat_data, coil_data, _ = _define_inputs(fmap_dim=3) fname_fmap = os.path.join(tmp, 'fmap.nii.gz') fname_fm_json = os.path.join(tmp, 'fmap.json') fname_mask = os.path.join(tmp, 'mask.nii.gz') @@ -112,7 +113,7 @@ def __test_st_plugin_b0shim_dyn(view, overlayList, displayCtx, options): fname_anat_json = os.path.join(tmp, 'anat.json') fname_coil = os.path.join(tmp, 'coil.nii.gz') fname_coil_json = os.path.join(tmp, 'coil.json') - + _save_inputs(nii_fmap=nii_fmap, fname_fmap=fname_fmap, nii_anat=nii_anat, fname_anat=fname_anat, nii_mask=nii_mask, fname_mask=fname_mask, @@ -120,7 +121,7 @@ def __test_st_plugin_b0shim_dyn(view, overlayList, displayCtx, options): fm_data=fm_data, fname_fm_json=fname_fm_json, anat_data=anat_data, fname_anat_json=fname_anat_json, coil_data=coil_data, fname_coil_json=fname_coil_json) - fname_output = os.path.join(tmp, 'fieldmap.nii.gz') + fname_output = os.path.join(tmp, 'shim') # Fill in the B0 shim tab options list_widgets = [] @@ -142,7 +143,7 @@ def __test_st_plugin_b0shim_dyn(view, overlayList, displayCtx, options): assert set_dropdown_selection(widget, options['slices']) if widget.GetName() == 'output-value-format': assert set_dropdown_selection(widget, options['output-value-format']) - + # Select the checkboxes list_widgets = [] get_all_children(b0shim_tab.sizer_run, list_widgets) @@ -150,7 +151,7 @@ def __test_st_plugin_b0shim_dyn(view, overlayList, displayCtx, options): if isinstance(widget, wx.CheckBox) and widget.IsShown(): if widget.Label in options['scanner-coil-order']: assert set_checkbox(widget) - + # Select the dropdowns that are nested list_widgets = [] get_all_children(b0shim_tab.sizer_run, list_widgets) @@ -226,6 +227,168 @@ def __test_st_plugin_b0shim_dyn(view, overlayList, displayCtx, options): assert os.path.exists(fname_output) +def test_st_plugin_b0shim_rt_lsq_mse_coil(): + options = {'optimizer-method': 'Least Squares', + 'optimizer-criteria': 'Mean Squared Error', + 'slices': 'Volume', + 'scanner-coil-order': '1', + 'output-file-format-scanner': 'Slicewise per Channel', + 'output-file-format-coil': 'Slicewise per Channel', + 'output-value-format': 'delta' + } + + def _test_st_plugin_b0shim_rt(view, overlayList, displayCtx, options=options): + __test_st_plugin_b0shim_rt(view, overlayList, displayCtx, options=options) + run_with_orthopanel(_test_st_plugin_b0shim_rt) + + +def __test_st_plugin_b0shim_rt(view, overlayList, displayCtx, options): + """ Makes sure the B0 shim tab runs (Add dummy input and simulate a click) """ + nb_terminal = get_notebook(view) + + # Select the b0Shim tab + assert set_notebook_page(nb_terminal, 'B0 Shim') + + # Get the ST tab + b0shim_tab = get_tab(nb_terminal, B0ShimTab) + assert b0shim_tab is not None + + with tempfile.TemporaryDirectory(prefix='st_' + pathlib.Path(__file__).stem) as tmp: + nii_fmap, nii_anat, nii_mask, nii_coil, fm_data, anat_data, coil_data, fname_resp = _define_inputs(fmap_dim=4) + fname_fmap = os.path.join(tmp, 'fmap.nii.gz') + fname_fm_json = os.path.join(tmp, 'fmap.json') + fname_mask = os.path.join(tmp, 'mask.nii.gz') + fname_anat = os.path.join(tmp, 'anat.nii.gz') + fname_anat_json = os.path.join(tmp, 'anat.json') + fname_coil = os.path.join(tmp, 'coil.nii.gz') + fname_coil_json = os.path.join(tmp, 'coil.json') + fname_new_resp = os.path.join(tmp, 'respiration_data.resp') + + _save_inputs(nii_fmap=nii_fmap, fname_fmap=fname_fmap, + nii_anat=nii_anat, fname_anat=fname_anat, + nii_mask=nii_mask, fname_mask=fname_mask, + nii_coil=nii_coil, fname_coil=fname_coil, + fm_data=fm_data, fname_fm_json=fname_fm_json, + anat_data=anat_data, fname_anat_json=fname_anat_json, + coil_data=coil_data, fname_coil_json=fname_coil_json, + fname_resp=fname_resp, fname_new_resp=fname_new_resp) + path_output = os.path.join(tmp, 'shim') + + # Fill in the B0 shim tab options + list_widgets = [] + get_all_children(b0shim_tab.sizer_run, list_widgets) + for widget in list_widgets: + if isinstance(widget, wx.Choice) and widget.IsShown(): + if widget.GetName() == 'b0shim_algorithms': + # Select the proper algorithm + assert set_dropdown_selection(widget, 'Realtime Dynamic') + + # Select the dropdowns + list_widgets = [] + get_all_children(b0shim_tab.sizer_run, list_widgets) + for widget in list_widgets: + if isinstance(widget, wx.Choice) and widget.IsShown(): + if widget.GetName() == 'optimizer-method': + assert set_dropdown_selection(widget, options['optimizer-method']) + if widget.GetName() == 'slices': + assert set_dropdown_selection(widget, options['slices']) + if widget.GetName() == 'output-value-format': + assert set_dropdown_selection(widget, options['output-value-format']) + + # Select the checkboxes + list_widgets = [] + get_all_children(b0shim_tab.sizer_run, list_widgets) + for widget in list_widgets: + if isinstance(widget, wx.CheckBox) and widget.IsShown(): + if widget.Label in options['scanner-coil-order']: + assert set_checkbox(widget) + + # Select the dropdowns that are nested + list_widgets = [] + get_all_children(b0shim_tab.sizer_run, list_widgets) + for widget in list_widgets: + if isinstance(widget, wx.Choice) and widget.IsShown(): + if widget.GetName() == 'optimizer-criteria': + assert set_dropdown_selection(widget, options['optimizer-criteria']) + if widget.GetName() == 'output-file-format-scanner': + assert set_dropdown_selection(widget, options['output-file-format-scanner']) + if widget.GetName() == 'output-file-format-scanner': + assert set_dropdown_selection(widget, options['output-file-format-coil']) + # Select the dropdowns that are nested + list_widgets = [] + get_all_children(b0shim_tab.sizer_run, list_widgets) + for widget in list_widgets: + if isinstance(widget, wx.Choice) and widget.IsShown(): + if widget.GetName() == 'fatsat': + assert set_dropdown_selection(widget, options['fatsat']) + + # Fill in the text boxes + list_widgets = [] + get_all_children(b0shim_tab.sizer_run, list_widgets) + for widget in list_widgets: + if isinstance(widget, wx.TextCtrl) and widget.IsShown(): + if widget.GetName() == 'no_arg_ncoils_dyn': + widget.SetValue('1') + new_widget_list = [] + counter = 0 + get_all_children(b0shim_tab.sizer_run, new_widget_list) + for new_widget in new_widget_list: + if isinstance(new_widget, wx.TextCtrl) and new_widget.IsShown(): + if new_widget.GetName() == 'input_coil_1' and counter == 0: + new_widget.SetValue(fname_coil) + counter += 1 + realYield() + elif new_widget.GetName() == 'input_coil_1' and counter == 1: + new_widget.SetValue(fname_coil_json) + realYield() + realYield() + if widget.GetName() == 'fmap': + widget.SetValue(fname_fmap) + realYield() + if widget.GetName() == 'anat': + widget.SetValue(fname_anat) + realYield() + if widget.GetName() == 'resp': + widget.SetValue(fname_new_resp) + realYield() + if widget.GetName() == 'mask': + widget.SetValue(fname_mask) + realYield() + if widget.GetName() == 'mask-dilation-kernel-size': + widget.SetValue('5') + realYield() + if widget.GetName() == 'regularization-factor': + widget.SetValue('0.1') + realYield() + if widget.GetName() == 'output': + widget.SetValue(path_output) + realYield() + + # Call the function ran when clicking run button + b0shim_tab.run_component_rt.run() + + # Search for files in the overlay for a maximum of 20s + time_limit = 20 # s + for i in range(time_limit): + realYield() + + found = os.path.exists(os.path.join(path_output, "fig_shimmed_vs_unshimmed.png")) + + # Todo once output is pushed to the overlay + # overlay_file = overlayList.find("") + # if overlay_file: + # found = True + + if found: + break + + time.sleep(1) + + # Make sure there is an output in the overlay (that would mean the ST CLI ran) + assert found + assert os.path.exists(path_output) + + def _define_inputs(fmap_dim): # fname for fmap fname_fmap = os.path.join(__dir_testing__, 'ds_b0', 'sub-realtime', 'fmap', 'sub-realtime_fieldmap.nii.gz') @@ -237,10 +400,13 @@ def _define_inputs(fmap_dim): if fmap_dim == 4: nii_fmap = nii + resp = os.path.join(__dir_testing__, 'ds_b0', 'derivatives', 'sub-realtime', 'sub-realtime_PMUresp_signal.resp') elif fmap_dim == 3: nii_fmap = nib.Nifti1Image(np.mean(nii.get_fdata(), axis=3), nii.affine, header=nii.header) + resp = None elif fmap_dim == 2: nii_fmap = nib.Nifti1Image(nii.get_fdata()[..., 0, 0], nii.affine, header=nii.header) + resp = None else: raise ValueError("Supported Dimensions are 2, 3 or 4") @@ -269,8 +435,8 @@ def _define_inputs(fmap_dim): nii_coil = nib.load(fname_coil_nii) fname_coil_json = os.path.join(__dir_testing__, 'ds_coil', 'NP15ch_config.json') coil_data = json.load(open(fname_coil_json)) - - return nii_fmap, nii_anat, nii_mask, nii_coil, fm_data, anat_data, coil_data + + return nii_fmap, nii_anat, nii_mask, nii_coil, fm_data, anat_data, coil_data, resp def _save_inputs(nii_fmap=None, fname_fmap=None, @@ -279,7 +445,9 @@ def _save_inputs(nii_fmap=None, fname_fmap=None, nii_coil=None, fname_coil=None, fm_data=None, fname_fm_json=None, anat_data=None, fname_anat_json=None, - coil_data=None, fname_coil_json=None,): + coil_data=None, fname_coil_json=None, + fname_resp=None, fname_new_resp=None): + """Save inputs if they are not None, use the respective fnames for the different inputs to save""" if nii_fmap is not None: # Save the fieldmap @@ -302,12 +470,15 @@ def _save_inputs(nii_fmap=None, fname_fmap=None, if nii_mask is not None: # Save the mask nib.save(nii_mask, fname_mask) - + if nii_coil is not None: # Save the coil nib.save(nii_coil, fname_coil) - + if coil_data is not None: # Save json with open(fname_coil_json, 'w', encoding='utf-8') as f: json.dump(coil_data, f, indent=4) + + if fname_resp is not None and fname_new_resp is not None: + shutil.copy(fname_resp, fname_new_resp)