diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a701fd3..4158edf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,6 +51,13 @@ jobs: run: | echo "$HOME/shimming-toolbox/bin" >> $GITHUB_PATH + - name: prelude Ubuntu + if: contains(matrix.os, 'ubuntu') + run: | + echo "Download prelude" + st_download_data prelude + sudo install prelude/prelude /usr/local/bin + # - name: Run unit tests Mac # if: contains(matrix.os, 'macos') # run: | diff --git a/fsleyes_plugin_shimming_toolbox/components/dropdown_component.py b/fsleyes_plugin_shimming_toolbox/components/dropdown_component.py index bdc4dd5..130f926 100644 --- a/fsleyes_plugin_shimming_toolbox/components/dropdown_component.py +++ b/fsleyes_plugin_shimming_toolbox/components/dropdown_component.py @@ -24,14 +24,18 @@ def __init__(self, panel, dropdown_metadata, label, option_name, list_components } 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 + option_name (str): Name of the options of the dropdown, start with 'no_arg' if it is not an option list_components (list): list of Components 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 - component_to_dropdown_choice (list): Tells which component associates with which dropdown selection. - If None, assumes 1:1. + component_to_dropdown_choice (list): Tells which component associates with which dropdown selection. + If None, assumes 1:1. """ super().__init__(panel, list_components) + + self.choice_box_sizer = None + self.choice_box = None + self.dropdown_metadata = dropdown_metadata self.label = label self.info_text = info_text @@ -98,7 +102,7 @@ def unshow_choice_box_sizers(self): sizer.Show(False) def create_choice_box(self): - self.choice_box = wx.Choice(self.panel, choices=self.dropdown_choices) + self.choice_box = wx.Choice(self.panel, choices=self.dropdown_choices, name=self.option_name) self.choice_box.Bind(wx.EVT_CHOICE, self.on_choice) button = wx.Button(self.panel, -1, label=self.label) self.choice_box_sizer = wx.BoxSizer(wx.HORIZONTAL) @@ -114,7 +118,7 @@ def on_choice(self, event, is_propagating_up=True): is_propagating_up tells the dropdown to propagate the on_choice() command to the parent drop down until there is no more parent. Moreover, on each call of on_choice(), the propagation will also go down. This - allows to recalculate each dropdown. THere is some redundency since we would really just want the most + allows to recalculate each dropdown. There is some redundancy since we would really just want the most parent dropdown to send the down propagation, but this will do for now. """ # Get the selection from the choice box widget @@ -168,6 +172,6 @@ def find_index(self, label): return 0 def create_sizer(self): - """Create the a sizer containing tab-specific functionality.""" + """Create a sizer containing tab-specific functionality.""" sizer = wx.BoxSizer(wx.VERTICAL) return sizer diff --git a/fsleyes_plugin_shimming_toolbox/components/run_component.py b/fsleyes_plugin_shimming_toolbox/components/run_component.py index 9d03816..c6e33ca 100644 --- a/fsleyes_plugin_shimming_toolbox/components/run_component.py +++ b/fsleyes_plugin_shimming_toolbox/components/run_component.py @@ -14,9 +14,6 @@ from fsleyes_plugin_shimming_toolbox.events import EVT_RESULT, EVT_LOG from fsleyes_plugin_shimming_toolbox.worker_thread import WorkerThread -# Load icon resources -play_icon = wx.Bitmap(os.path.join(__DIR_ST_PLUGIN_IMG__, 'play.png'), wx.BITMAP_TYPE_PNG) - class RunComponent(Component): """Component which contains input and run button. @@ -57,6 +54,7 @@ def add_button_run(self): """Add the run button which will call the ``Shimming Toolbox`` CLI.""" button_run = wx.Button(self.panel, -1, label="Run", size=(85, 48)) button_run.Bind(wx.EVT_BUTTON, self.button_run_on_click) + play_icon = wx.Bitmap(os.path.join(__DIR_ST_PLUGIN_IMG__, 'play.png'), wx.BITMAP_TYPE_PNG) button_run.SetBitmap(play_icon, dir=wx.LEFT) self.sizer.Add(button_run, 0, wx.CENTRE) self.sizer.AddSpacer(10) @@ -150,6 +148,9 @@ def button_run_on_click(self, event): Calls the relevant ``Shimming Toolbox`` CLI command (``st_function``) in a thread """ + self.run() + + def run(self): if not self.worker: try: command, msg = self.get_run_args(self.st_function) @@ -190,7 +191,7 @@ def get_run_args(self, st_function): for component in self.list_components: for name, input_text_box_list in component.input_text_boxes.items(): - if name == "no_arg": + if name.startswith('no_arg'): continue for input_text_box in input_text_box_list: diff --git a/fsleyes_plugin_shimming_toolbox/st_plugin.py b/fsleyes_plugin_shimming_toolbox/st_plugin.py index 4a0451d..4831950 100644 --- a/fsleyes_plugin_shimming_toolbox/st_plugin.py +++ b/fsleyes_plugin_shimming_toolbox/st_plugin.py @@ -18,6 +18,7 @@ import fsleyes.controls.controlpanel as ctrlpanel import fsleyes.views.canvaspanel as canvaspanel +import textwrap import wx from fsleyes_plugin_shimming_toolbox.tabs.b0shim_tab import B0ShimTab @@ -26,6 +27,15 @@ from fsleyes_plugin_shimming_toolbox.tabs.fieldmap_tab import FieldMapTab from fsleyes_plugin_shimming_toolbox.tabs.mask_tab import MaskTab +STLayout = textwrap.dedent( + """ + fsleyes.views.orthopanel.OrthoPanel + layout2|name=OrthoPanel 1;caption=Ortho View 1;state=67376064;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=-1;besth=-1;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=22| + fsleyes.controls.orthotoolbar.OrthoToolBar,fsleyes.controls.overlaydisplaytoolbar.OverlayDisplayToolBar,fsleyes.controls.overlaylistpanel.OverlayListPanel,fsleyes.controls.locationpanel.LocationPanel,fsleyes_plugin_shimming_toolbox.st_plugin.STControlPanel;syncLocation=True,syncOverlayOrder=True,syncOverlayDisplay=True,syncOverlayVolume=True,movieRate=400,movieAxis=3;showCursor=True,bgColour=#000000ff,fgColour=#ffffffff,cursorColour=#00ff00ff,cursorGap=False,showColourBar=False,colourBarLocation=top,colourBarLabelSide=top-left,showXCanvas=True,showYCanvas=True,showZCanvas=True,showLabels=True,labelSize=12,layout=horizontal,xzoom=847.0127756479341,yzoom=847.0127756479341,zzoom=100.0 + layout2|name=Panel;caption=;state=768;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=-1;besth=-1;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=OrthoToolBar;caption=Ortho view toolbar;state=67382012;dir=1;layer=10;row=0;pos=0;prop=100000;bestw=572;besth=35;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=OverlayDisplayToolBar;caption=Display toolbar;state=67382012;dir=1;layer=11;row=0;pos=0;prop=100000;bestw=953;besth=56;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=OverlayListPanel;caption=Overlay list;state=67373052;dir=3;layer=0;row=0;pos=0;prop=100000;bestw=197;besth=80;minw=1;minh=1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=197;floath=99;notebookid=-1;transparent=255|name=LocationPanel;caption=Location;state=67373052;dir=3;layer=0;row=0;pos=1;prop=100000;bestw=391;besth=111;minw=1;minh=1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=391;floath=130;notebookid=-1;transparent=255|name=STControlPanel;caption=Shimming Toolbox;state=67373052;dir=1;layer=0;row=0;pos=0;prop=100000;bestw=600;besth=400;minw=1;minh=1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=600;floath=419;notebookid=-1;transparent=255|dock_size(5,0,0)=22|dock_size(3,0,0)=176|dock_size(1,10,0)=37|dock_size(1,11,0)=58|dock_size(1,0,0)=384| + """ +) + # We need to create a ctrlpanel.ControlPanel instance so that it can be recognized as a plugin by FSLeyes # Class hierarchy: wx.Panel > fslpanel.FSLeyesPanel > ctrlpanel.ControlPanel diff --git a/fsleyes_plugin_shimming_toolbox/tabs/b0shim_tab.py b/fsleyes_plugin_shimming_toolbox/tabs/b0shim_tab.py index f5c8ef8..aaf48a9 100644 --- a/fsleyes_plugin_shimming_toolbox/tabs/b0shim_tab.py +++ b/fsleyes_plugin_shimming_toolbox/tabs/b0shim_tab.py @@ -4,7 +4,7 @@ import os import wx -from fsleyes_plugin_shimming_toolbox import __CURR_DIR__, __ST_DIR__ +from fsleyes_plugin_shimming_toolbox import __CURR_DIR__ from fsleyes_plugin_shimming_toolbox.tabs.tab import Tab from fsleyes_plugin_shimming_toolbox.components.dropdown_component import DropdownComponent from fsleyes_plugin_shimming_toolbox.components.input_component import InputComponent @@ -18,6 +18,19 @@ class B0ShimTab(Tab): def __init__(self, parent, title="B0 Shim"): + self.dropdown_coil_format_rt = None + self.dropdown_slice_rt = None + self.dropdown_opt_rt = None + self.dropdown_scanner_order_rt = None + self.dropdown_coil_format_dyn = None + self.dropdown_slice_dyn = None + self.dropdown_opt_dyn = None + self.dropdown_scanner_order_dyn = None + self.choice_box = None + self.run_component_mi = None + self.run_component_rt = None + self.run_component_dyn = None + description = "Perform B0 shimming.\n\n" \ "Select the shimming algorithm from the dropdown list." super().__init__(parent, title, description) @@ -101,7 +114,7 @@ def unshow_choice_box_sizers(self): sizer_item.Show(False) def create_choice_box(self): - self.choice_box = wx.Choice(self, choices=self.dropdown_choices) + self.choice_box = wx.Choice(self, choices=self.dropdown_choices, name="b0shim_algorithms") self.choice_box.Bind(wx.EVT_CHOICE, self.on_choice) self.sizer_run.Add(self.choice_box) self.sizer_run.AddSpacer(10) @@ -114,7 +127,7 @@ def create_sizer_dynamic_shim(self, metadata=None): { "button_label": "Number of Custom Coils", "button_function": "add_input_coil_boxes_dyn", - "name": "no_arg", + "name": "no_arg_ncoils_dyn", "info_text": "Number of phase NIfTI files to be used. Must be an integer > 0.", } ] @@ -172,7 +185,6 @@ def create_sizer_dynamic_shim(self, metadata=None): "button_label": "Scanner constraints", "button_function": "select_file", "name": "scanner-coil-constraints", - "default_text": f"{os.path.join(__ST_DIR__, 'coil_config.json')}", }, ] component_scanner1 = InputComponent(self, input_text_box_metadata_scanner, cli=dynamic_cli) @@ -288,7 +300,8 @@ def create_sizer_dynamic_shim(self, metadata=None): "name": 'regularization-factor', } ] - component_reg_factor = InputComponent(self, reg_factor_metadata, cli=dynamic_cli) + component_reg_factor_lsq = InputComponent(self, reg_factor_metadata, cli=dynamic_cli) + component_reg_factor_qp = InputComponent(self, reg_factor_metadata, cli=dynamic_cli) criteria_dropdown_metadata = [ { @@ -318,6 +331,10 @@ def create_sizer_dynamic_shim(self, metadata=None): "label": "Pseudo Inverse", "option_value": "pseudo_inverse" }, + { + "label": "Quad Prog", + "option_value": "quad_prog" + } ] self.dropdown_opt_dyn = DropdownComponent( @@ -325,8 +342,9 @@ def create_sizer_dynamic_shim(self, metadata=None): dropdown_metadata=dropdown_opt_metadata, label="Optimizer", option_name='optimizer-method', - list_components=[dropdown_crit, component_reg_factor, self.create_empty_component()], - component_to_dropdown_choice=[0, 0, 1], + list_components=[dropdown_crit, component_reg_factor_lsq, + self.create_empty_component(), component_reg_factor_qp], + component_to_dropdown_choice=[0, 0, 1, 2], cli=dynamic_cli ) @@ -428,7 +446,7 @@ def create_sizer_dynamic_shim(self, metadata=None): dropdown_fatsat1.add_dropdown_parent(self.dropdown_coil_format_dyn) dropdown_fatsat2.add_dropdown_parent(self.dropdown_coil_format_dyn) - run_component = RunComponent( + self.run_component_dyn = RunComponent( panel=self, list_components=[self.component_coils_dyn, component_inputs, self.dropdown_opt_dyn, self.dropdown_slice_dyn, self.dropdown_scanner_order_dyn, @@ -437,7 +455,7 @@ def create_sizer_dynamic_shim(self, metadata=None): output_paths=["fieldmap_calculated_shim_masked.nii.gz", "fieldmap_calculated_shim.nii.gz"] ) - sizer = run_component.sizer + sizer = self.run_component_dyn.sizer return sizer def create_sizer_realtime_shim(self, metadata=None): @@ -448,7 +466,7 @@ def create_sizer_realtime_shim(self, metadata=None): { "button_label": "Number of Custom Coils", "button_function": "add_input_coil_boxes_rt", - "name": "no_arg", + "name": "no_arg_ncoils_rt", "info_text": "Number of phase NIfTI files to be used. Must be an integer > 0.", } ] @@ -497,7 +515,6 @@ def create_sizer_realtime_shim(self, metadata=None): "button_label": "Scanner constraints", "button_function": "select_file", "name": "scanner-coil-constraints", - "default_text": f"{os.path.join(__ST_DIR__, 'coil_config.json')}", }, ] component_scanner1 = InputComponent(self, input_text_box_metadata_scanner, cli=realtime_cli) @@ -625,7 +642,8 @@ def create_sizer_realtime_shim(self, metadata=None): "name": 'regularization-factor', } ] - component_reg_factor = InputComponent(self, reg_factor_metadata, cli=dynamic_cli) + component_reg_factor_lsq = InputComponent(self, reg_factor_metadata, cli=dynamic_cli) + component_reg_factor_qp = InputComponent(self, reg_factor_metadata, cli=dynamic_cli) criteria_dropdown_metadata = [ { @@ -655,15 +673,20 @@ def create_sizer_realtime_shim(self, metadata=None): "label": "Pseudo Inverse", "option_value": "pseudo_inverse" }, + { + "label": "Quad Prog", + "option_value": "quad_prog" + } ] self.dropdown_opt_rt = DropdownComponent( panel=self, dropdown_metadata=dropdown_opt_metadata, label="Optimizer", - option_name = 'optimizer-method', - list_components= [dropdown_crit, component_reg_factor, self.create_empty_component()], - component_to_dropdown_choice=[0, 0, 1], + option_name='optimizer-method', + list_components=[dropdown_crit, component_reg_factor_lsq, + self.create_empty_component(), component_reg_factor_qp], + component_to_dropdown_choice=[0, 0, 1, 2], cli=realtime_cli ) @@ -746,7 +769,7 @@ def create_sizer_realtime_shim(self, metadata=None): dropdown_fatsat.add_dropdown_parent(self.dropdown_coil_format_rt) - run_component = RunComponent( + self.run_component_rt = RunComponent( panel=self, list_components=[self.component_coils_rt, component_inputs, self.dropdown_opt_rt, self.dropdown_slice_rt, self.dropdown_scanner_order_rt, @@ -755,7 +778,7 @@ def create_sizer_realtime_shim(self, metadata=None): # TODO: output paths output_paths=[] ) - sizer = run_component.sizer + sizer = self.run_component_rt.sizer return sizer def create_sizer_max_intensity(self, metadata=None): @@ -781,11 +804,11 @@ def create_sizer_max_intensity(self, metadata=None): ] component_inputs = InputComponent(self, inputs_metadata, cli=max_intensity_cli) - run_component = RunComponent( + self.run_component_mi = RunComponent( panel=self, list_components=[component_inputs], st_function="st_b0shim max-intensity", output_paths=[] ) - sizer = run_component.sizer + sizer = self.run_component_mi.sizer return sizer diff --git a/fsleyes_plugin_shimming_toolbox/tabs/b1shim_tab.py b/fsleyes_plugin_shimming_toolbox/tabs/b1shim_tab.py index 2f32467..6884275 100644 --- a/fsleyes_plugin_shimming_toolbox/tabs/b1shim_tab.py +++ b/fsleyes_plugin_shimming_toolbox/tabs/b1shim_tab.py @@ -83,7 +83,7 @@ def unshow_choice_box_sizers(self): sizer_item.Show(False) def create_choice_box(self): - self.choice_box = wx.Choice(self, choices=self.dropdown_choices) + self.choice_box = wx.Choice(self, choices=self.dropdown_choices, name="b1shim_algorithms") self.choice_box.Bind(wx.EVT_CHOICE, self.on_choice) self.sizer_run.Add(self.choice_box) self.sizer_run.AddSpacer(10) diff --git a/fsleyes_plugin_shimming_toolbox/tabs/dicom_to_nifti_tab.py b/fsleyes_plugin_shimming_toolbox/tabs/dicom_to_nifti_tab.py index 55d21e9..b26f326 100644 --- a/fsleyes_plugin_shimming_toolbox/tabs/dicom_to_nifti_tab.py +++ b/fsleyes_plugin_shimming_toolbox/tabs/dicom_to_nifti_tab.py @@ -18,6 +18,7 @@ def __init__(self, parent, title="Dicom to Nifti"): super().__init__(parent, title, description) self.sizer_run = self.create_sizer_run() + self.run_component = None sizer = self.create_dicom_to_nifti_sizer() self.sizer_run.Add(sizer, 0, wx.EXPAND) @@ -52,5 +53,5 @@ def create_dicom_to_nifti_sizer(self): } ] 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") - return run_component.sizer + self.run_component = RunComponent(panel=self, list_components=[component], st_function="st_dicom_to_nifti") + return self.run_component.sizer diff --git a/fsleyes_plugin_shimming_toolbox/tabs/fieldmap_tab.py b/fsleyes_plugin_shimming_toolbox/tabs/fieldmap_tab.py index 39681da..d24819f 100644 --- a/fsleyes_plugin_shimming_toolbox/tabs/fieldmap_tab.py +++ b/fsleyes_plugin_shimming_toolbox/tabs/fieldmap_tab.py @@ -15,6 +15,18 @@ class FieldMapTab(Tab): def __init__(self, parent, title="Fieldmap"): + + self.component_save_mask1 = None + self.component_save_mask2 = None + self.run_component = None + self.component_input2 = None + self.component_output = None + self.dropdown_roi = None + self.component_threshold = None + self.component_mask = None + self.dropdown_unwrapper = None + self.component_input = None + description = "Create a B0 fieldmap.\n\n" \ "Enter the Number of Echoes then press the `Number of Echoes` button.\n\n" \ "Select the unwrapper from the dropdown list." @@ -34,7 +46,7 @@ def create_fieldmap_sizer(self): { "button_label": "Number of Echoes", "button_function": "add_input_phase_boxes", - "name": "no_arg", + "name": "no_arg_nechoes", "info_text": "Number of phase NIfTI files to be used. Must be an integer > 0.", "required": True } @@ -46,15 +58,19 @@ def create_fieldmap_sizer(self): dropdown_metadata_unwrapper = [ { - "label": "prelude", + "label": "Prelude", "option_value": "prelude" + }, + { + "label": "Skimage", + "option_value": "skimage" } ] self.dropdown_unwrapper = DropdownComponent( panel=self, dropdown_metadata=dropdown_metadata_unwrapper, label="Unwrapper", - option_name = 'unwrapper', + option_name='unwrapper', cli=prepare_fieldmap_cli ) @@ -70,16 +86,30 @@ def create_fieldmap_sizer(self): input_text_box_metadata=mask_metadata, cli=prepare_fieldmap_cli ) + save_mask_metadata = [ + { + "button_label": "Output Calculated Mask", + "name": "savemask", + "load_in_overlay": True + } + ] + + self.component_save_mask1 = InputComponent( + panel=self, + input_text_box_metadata=save_mask_metadata, + cli=prepare_fieldmap_cli + ) + + self.component_save_mask2 = InputComponent( + panel=self, + input_text_box_metadata=save_mask_metadata, + cli=prepare_fieldmap_cli + ) threshold_metadata = [ { "button_label": "Threshold", "name": "threshold", - }, - { - "button_label": "Output Calculated Mask", - "name": "savemask", - "load_in_overlay": True } ] self.component_threshold = InputComponent( @@ -108,8 +138,9 @@ def create_fieldmap_sizer(self): dropdown_metadata=dropdown_mask_threshold, label="Unwrapping region", info_text="Masking methods either with a file input or a threshold", - option_name='no_arg', - list_components=[self.create_empty_component(), self.component_mask, self.component_threshold], + option_name='no_arg_roi', + list_components=[self.component_save_mask1, self.component_mask, self.component_threshold, self.component_save_mask2], + component_to_dropdown_choice=[0, 1, 2, 2], cli=prepare_fieldmap_cli ) diff --git a/fsleyes_plugin_shimming_toolbox/tabs/mask_tab.py b/fsleyes_plugin_shimming_toolbox/tabs/mask_tab.py index 69289d8..b7ffd71 100644 --- a/fsleyes_plugin_shimming_toolbox/tabs/mask_tab.py +++ b/fsleyes_plugin_shimming_toolbox/tabs/mask_tab.py @@ -14,6 +14,13 @@ class MaskTab(Tab): def __init__(self, parent, title="Mask"): + + self.run_component_sphere = None + self.run_component_box = None + self.run_component_rect = None + self.run_component_thr = None + self.choice_box = None + description = "Create a mask.\n\n" \ "Select a shape or an algorithm from the dropdown list." super().__init__(parent, title, description) @@ -81,7 +88,7 @@ def unshow_choice_box_sizers(self): sizer.Show(False) def create_choice_box(self): - self.choice_box = wx.Choice(self, choices=self.dropdown_choices) + self.choice_box = wx.Choice(self, choices=self.dropdown_choices, name="mask_algorithms") self.choice_box.Bind(wx.EVT_CHOICE, self.on_choice) self.sizer_run.Add(self.choice_box) self.sizer_run.AddSpacer(10) @@ -108,12 +115,12 @@ def create_sizer_threshold(self, metadata=None): } ] component = InputComponent(self, input_text_box_metadata, cli=threshold) - run_component = RunComponent( + self.run_component_thr = RunComponent( panel=self, list_components=[component], st_function="st_mask threshold" ) - sizer = run_component.sizer + sizer = self.run_component_thr.sizer return sizer def create_sizer_rect(self): @@ -144,12 +151,12 @@ def create_sizer_rect(self): } ] component = InputComponent(self, input_text_box_metadata, cli=rect) - run_component = RunComponent( + self.run_component_rect = RunComponent( panel=self, list_components=[component], st_function="st_mask rect" ) - sizer = run_component.sizer + sizer = self.run_component_rect.sizer return sizer def create_sizer_box(self): @@ -180,12 +187,12 @@ def create_sizer_box(self): } ] component = InputComponent(self, input_text_box_metadata, box) - run_component = RunComponent( + self.run_component_box = RunComponent( panel=self, list_components=[component], st_function="st_mask box" ) - sizer = run_component.sizer + sizer = self.run_component_box.sizer return sizer def create_sizer_sphere(self): @@ -215,10 +222,10 @@ def create_sizer_sphere(self): } ] component = InputComponent(self, input_text_box_metadata, sphere) - run_component = RunComponent( + self.run_component_sphere = RunComponent( panel=self, list_components=[component], st_function="st_mask sphere" ) - sizer = run_component.sizer + sizer = self.run_component_sphere.sizer return sizer diff --git a/fsleyes_plugin_shimming_toolbox/tabs/tab.py b/fsleyes_plugin_shimming_toolbox/tabs/tab.py index f4ad7ad..823e101 100644 --- a/fsleyes_plugin_shimming_toolbox/tabs/tab.py +++ b/fsleyes_plugin_shimming_toolbox/tabs/tab.py @@ -8,13 +8,6 @@ from fsleyes_plugin_shimming_toolbox import __DIR_ST_PLUGIN_IMG__ from fsleyes_plugin_shimming_toolbox.components.input_component import InputComponent -# Load icon resources -rtd_logo = wx.Bitmap(os.path.join(__DIR_ST_PLUGIN_IMG__, 'RTD.png'), wx.BITMAP_TYPE_PNG) -# Load ShimmingToolbox logo saved as a png image, rescale it, and return it as a wx.Bitmap image. -st_logo = wx.Image(os.path.join(__DIR_ST_PLUGIN_IMG__, 'shimming_toolbox_logo.png'), wx.BITMAP_TYPE_PNG) -st_logo.Rescale(int(st_logo.GetWidth() * 0.2), int(st_logo.GetHeight() * 0.2), wx.IMAGE_QUALITY_HIGH) -st_logo = st_logo.ConvertToBitmap() - class Tab(wx.ScrolledWindow): def __init__(self, parent, title, description): @@ -57,12 +50,18 @@ def __init__(self, panel, description): def create_sizer(self): """Create the left sizer containing generic Shimming Toolbox information.""" sizer = wx.BoxSizer(wx.VERTICAL) + + # Load ShimmingToolbox logo saved as a png image, rescale it, and return it as a wx.Bitmap image. + st_logo = wx.Image(os.path.join(__DIR_ST_PLUGIN_IMG__, 'shimming_toolbox_logo.png'), wx.BITMAP_TYPE_PNG) + st_logo.Rescale(int(st_logo.GetWidth() * 0.2), int(st_logo.GetHeight() * 0.2), wx.IMAGE_QUALITY_HIGH) + st_logo = st_logo.ConvertToBitmap() logo = wx.StaticBitmap(parent=self.panel, id=-1, bitmap=st_logo, pos=wx.DefaultPosition) width = logo.Size[0] sizer.Add(logo, flag=wx.SHAPED, proportion=1) sizer.AddSpacer(10) # Create a "Documentation" button that redirects towards https://shimming-toolbox.org/en/latest/ + rtd_logo = wx.Bitmap(os.path.join(__DIR_ST_PLUGIN_IMG__, 'RTD.png'), wx.BITMAP_TYPE_PNG) button_documentation = wx.Button(self.panel, label="Documentation") button_documentation.Bind(wx.EVT_BUTTON, self.open_documentation_url) button_documentation.SetBitmap(rtd_logo) diff --git a/fsleyes_plugin_shimming_toolbox/text_with_button.py b/fsleyes_plugin_shimming_toolbox/text_with_button.py index 44e87d0..6dcfef6 100644 --- a/fsleyes_plugin_shimming_toolbox/text_with_button.py +++ b/fsleyes_plugin_shimming_toolbox/text_with_button.py @@ -7,10 +7,6 @@ from fsleyes_plugin_shimming_toolbox import __DIR_ST_PLUGIN_IMG__ from fsleyes_plugin_shimming_toolbox.select import select_file, select_folder, select_from_overlay -# Load icon resources -info_icon = wx.Image(os.path.join(__DIR_ST_PLUGIN_IMG__, 'info-icon.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap() -asterisk_icon = wx.Image(os.path.join(__DIR_ST_PLUGIN_IMG__, 'asterisk.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap() - class TextWithButton: """Creates a button with an input text box. @@ -102,6 +98,8 @@ def create(self): for textctrl in self.textctrl_list: text_with_button_box.Add(textctrl, 1, wx.ALIGN_LEFT | wx.LEFT, 10) if self.required: + asterisk_icon = wx.Image(os.path.join(__DIR_ST_PLUGIN_IMG__, 'asterisk.png'), + wx.BITMAP_TYPE_PNG).ConvertToBitmap() text_with_button_box.Add(wx.StaticBitmap(self.panel, bitmap=asterisk_icon), 0, wx.RIGHT, 7) return text_with_button_box @@ -280,6 +278,7 @@ def __init__(self, panel, bitmap, info_text): def create_info_icon(panel, info_text=""): + info_icon = wx.Image(os.path.join(__DIR_ST_PLUGIN_IMG__, 'info-icon.png'), wx.BITMAP_TYPE_PNG).ConvertToBitmap() image = InfoIcon(panel, bitmap=info_icon, info_text=info_text) image.Bind(wx.EVT_MOTION, on_info_icon_mouse_over) return image diff --git a/installer/install_plugin.sh b/installer/install_plugin.sh index 5fb17cb..36a83d6 100644 --- a/installer/install_plugin.sh +++ b/installer/install_plugin.sh @@ -31,23 +31,23 @@ function edit_shellrc() { fi } -source $ST_DIR/$PYTHON_DIR/bin/activate +source "${ST_DIR}/${PYTHON_DIR}/bin/activate" # Install fsleyes print info "Installing fsleyes" -"$ST_DIR"/"$PYTHON_DIR"/bin/mamba install -y -c conda-forge fsleyes=1.7.0 python=3.9 +"${ST_DIR}"/"${PYTHON_DIR}"/bin/mamba install -y -c conda-forge fsleyes=1.9.0 python=3.9 # Install fsleyes-plugin-shimming-toolbox print info "Installing fsleyes-plugin-shimming-toolbox" -"$ST_DIR"/"$PYTHON_DIR"/bin/python -m pip install . +"${ST_DIR}"/"${PYTHON_DIR}"/bin/python -m pip install . # Create launchers print info "Creating launcher for fsleyes-plugin-shimming-toolbox..." -mkdir -p $ST_DIR/$BIN_DIR +mkdir -p "${ST_DIR}/${BIN_DIR}" chmod +x shimming-toolbox.sh -cp shimming-toolbox.sh $ST_DIR/$BIN_DIR/ # || die "Problem creating launchers!" +cp shimming-toolbox.sh "${ST_DIR}/${BIN_DIR}/" # || die "Problem creating launchers!" # Activate the launchers -export PATH=$ST_DIR/$BIN_DIR:$PATH +export PATH="${ST_DIR}/${BIN_DIR}:${PATH}" edit_shellrc diff --git a/installer/install_shimming_toolbox.sh b/installer/install_shimming_toolbox.sh index f713f59..5d6e236 100644 --- a/installer/install_shimming_toolbox.sh +++ b/installer/install_shimming_toolbox.sh @@ -7,14 +7,14 @@ set -e ST_DIR=$HOME/shimming-toolbox -cd $ST_DIR +cd "${ST_DIR}" # Remove previous install rm -rf "${ST_DIR}/shimming-toolbox" print info "Downloading Shimming-Toolbox" -ST_VERSION="b095b399d0da9201d8cd61d43a2121a85eb0aa96" +ST_VERSION="c9975f7d6bc8bcc4c79ad73fef59c9b938b156dc" 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 f5cb917..0a6641e 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,9 @@ 'fsleyes_controls': [ 'Shimming Toolbox = fsleyes_plugin_shimming_toolbox.st_plugin:STControlPanel' ], + 'fsleyes_layouts': [ + 'Shimming Toolbox = fsleyes_plugin_shimming_toolbox.st_plugin:STLayout' + ], # 'fsleyes_tools': [ # 'My cool tool = myplugin:MyTool' # ] diff --git a/shimming-toolbox.sh b/shimming-toolbox.sh index 65ab77b..4689317 100755 --- a/shimming-toolbox.sh +++ b/shimming-toolbox.sh @@ -3,8 +3,8 @@ set -e ST_DIR="${HOME}/shimming-toolbox" -PYTHON_DIR=python +PYTHON_DIR="python" -source "${ST_DIR}/$PYTHON_DIR/bin/activate" +source "${ST_DIR}/${PYTHON_DIR}/bin/activate" -fsleyes & +"${ST_DIR}/${PYTHON_DIR}/bin/fsleyes" --showAllPlugins --scene 'Shimming Toolbox' & diff --git a/test/__init__.py b/test/__init__.py index f4f6ab4..0972c4f 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -145,21 +145,21 @@ def run_with_fsleyes(func, *args, **kwargs): idle.idleLoop.allowErrors = True propagateRaise = kwargs.pop('propagateRaise', True) - startingDelay = kwargs.pop('startingDelay', 500) + startingDelay = kwargs.pop('startingDelay', 500) finishingDelay = kwargs.pop('finishingDelay', 250) - callAfterApp = kwargs.pop('callAfterApp', None) + callAfterApp = kwargs.pop('callAfterApp', None) class State(object): pass - state = State() - state.result = None - state.raised = None - state.frame = None - state.app = None - state.dummy = None - state.panel = None - - glver = os.environ.get('FSLEYES_TEST_GL', None) + state = State() + state.result = None + state.raised = None + state.frame = None + state.app = None + state.dummy = None + state.panel = None + + glver = os.environ.get('FSLEYES_TEST_GL', None) if glver is not None: glver = [int(v) for v in glver.split('.')] @@ -181,7 +181,7 @@ def finish(): def run(): overlayList = fsloverlay.OverlayList() - displayCtx = dc.DisplayContext(overlayList) + displayCtx = dc.DisplayContext(overlayList) state.frame = fslframe.FSLeyesFrame(None, overlayList, displayCtx) @@ -209,7 +209,7 @@ def run(): finally: wx.CallLater(finishingDelay, finish) - state.app = fslmain.FSLeyesApp() + state.app = fslmain.FSLeyesApp() state.dummy = wx.Frame(None) state.panel = wx.Panel(state.dummy) state.sizer = wx.BoxSizer(wx.HORIZONTAL) @@ -261,7 +261,7 @@ def run(): def run_with_viewpanel(func, vptype, *args, **kwargs): def inner(frame, overlayList, displayCtx, *a, **kwa): - panel = frame.addViewPanel(vptype) + panel = frame.addViewPanel(vptype) displayCtx = panel.displayCtx try: while not panel.IsShownOnScreen(): @@ -352,8 +352,10 @@ def GetEventObject(self): sim.MouseMove(round(x), round(y)) realYield() - if stype == 0: sim.MouseClick(btn) - elif stype == 1: sim.MouseDblClick(btn) + if stype == 0: + sim.MouseClick(btn) + elif stype == 1: + sim.MouseDblClick(btn) else: sim.MouseDown(btn) sim.MouseUp(btn) diff --git a/test/gui/test_b0shim_tab.py b/test/gui/test_b0shim_tab.py new file mode 100644 index 0000000..f3ef489 --- /dev/null +++ b/test/gui/test_b0shim_tab.py @@ -0,0 +1,260 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +import json +import nibabel as nib +import numpy as np +import os +import pathlib +from shimmingtoolbox.masking.shapes import shapes +import tempfile +import time +import wx + +from .test_tabs import get_notebook, set_notebook_page, get_tab, get_all_children, set_dropdown_selection +from .. import realYield, run_with_orthopanel +from fsleyes_plugin_shimming_toolbox import __dir_testing__ +from fsleyes_plugin_shimming_toolbox.tabs.b0shim_tab import B0ShimTab + + +def test_st_plugin_b0shim_dyn_lsq_mse(): + options = {'optimizer-method': 'Least Squares', + 'optimizer-criteria': 'Mean Squared Error', + 'slices': 'Auto detect', + 'scanner-coil-order': '1', + 'output-file-format-scanner': 'Slicewise per Channel', + 'output-file-format-coil': 'Slicewise per Channel', + 'fatsat': 'Auto detect', + 'output-value-format': 'delta' + } + + 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_mae(): + options = {'optimizer-method': 'Least Squares', + 'optimizer-criteria': 'Mean Absolute Error', + 'slices': 'Auto detect', + '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_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_pi(): + options = {'optimizer-method': 'Pseudo Inverse', + 'slices': 'Auto detect', + '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_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', + 'slices': 'Auto detect', + '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_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(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 + 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, fm_data, anat_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') + fname_anat = os.path.join(tmp, 'anat.nii.gz') + fname_anat_json = os.path.join(tmp, 'anat.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, + fm_data=fm_data, fname_fm_json=fname_fm_json, + anat_data=anat_data, fname_anat_json=fname_anat_json) + fname_output = os.path.join(tmp, 'fieldmap.nii.gz') + + # 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, 'Dynamic/volume') + + # 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() == 'scanner-coil-order': + assert set_dropdown_selection(widget, options['scanner-coil-order']) + if widget.GetName() == 'slices': + assert set_dropdown_selection(widget, options['slices']) + if widget.GetName() == 'scanner-coil-order': + assert set_dropdown_selection(widget, options['scanner-coil-order']) + if widget.GetName() == 'output-value-format': + assert set_dropdown_selection(widget, options['output-value-format']) + # 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('0') + realYield() + if widget.GetName() == 'fmap': + widget.SetValue(fname_fmap) + realYield() + if widget.GetName() == 'anat': + widget.SetValue(fname_anat) + 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(fname_output) + realYield() + + # Call the function ran when clicking run button + b0shim_tab.run_component_dyn.run() + + # Search for files in the overlay for a maximum of 20s + time_limit = 20 # s + for i in range(time_limit): + realYield() + overlay_file = overlayList.find("fieldmap_calculated_shim_masked") + time.sleep(1) + if overlay_file: + break + + # Make sure there is an output in the overlay (that would mean the ST CLI ran) + assert overlay_file is not None + assert os.path.exists(fname_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') + nii = nib.load(fname_fmap) + + fname_json = os.path.join(__dir_testing__, 'ds_b0', 'sub-realtime', 'fmap', 'sub-realtime_fieldmap.json') + + fm_data = json.load(open(fname_json)) + + if fmap_dim == 4: + nii_fmap = nii + elif fmap_dim == 3: + nii_fmap = nib.Nifti1Image(np.mean(nii.get_fdata(), axis=3), nii.affine, header=nii.header) + elif fmap_dim == 2: + nii_fmap = nib.Nifti1Image(nii.get_fdata()[..., 0, 0], nii.affine, header=nii.header) + else: + raise ValueError("Supported Dimensions are 2, 3 or 4") + + # fname for anat + fname_anat = os.path.join(__dir_testing__, 'ds_b0', 'sub-realtime', 'anat', 'sub-realtime_unshimmed_e1.nii.gz') + + nii_anat = nib.load(fname_anat) + + fname_anat_json = os.path.join(__dir_testing__, 'ds_b0', 'sub-realtime', 'anat', 'sub-realtime_unshimmed_e1.json') + anat_data = json.load(open(fname_anat_json)) + anat_data['ScanOptions'] = ['FS'] + + anat = nii_anat.get_fdata() + + # Set up mask: Cube + # static + nx, ny, nz = anat.shape + mask = shapes(anat, 'cube', + center_dim1=int(nx / 2), + center_dim2=int(ny / 2), + len_dim1=10, len_dim2=10, len_dim3=nz - 10) + + nii_mask = nib.Nifti1Image(mask.astype(np.uint8), nii_anat.affine) + + return nii_fmap, nii_anat, nii_mask, fm_data, anat_data + + +def _save_inputs(nii_fmap=None, fname_fmap=None, + nii_anat=None, fname_anat=None, + nii_mask=None, fname_mask=None, + fm_data=None, fname_fm_json=None, + anat_data=None, fname_anat_json=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 + nib.save(nii_fmap, fname_fmap) + + if fm_data is not None: + # Save json + with open(fname_fm_json, 'w', encoding='utf-8') as f: + json.dump(fm_data, f, indent=4) + + if nii_anat is not None: + # Save the anat + nib.save(nii_anat, fname_anat) + + if anat_data is not None: + # Save json + with open(fname_anat_json, 'w', encoding='utf-8') as f: + json.dump(anat_data, f, indent=4) + + if nii_mask is not None: + # Save the mask + nib.save(nii_mask, fname_mask) diff --git a/test/gui/test_dicom_to_nifti_tab.py b/test/gui/test_dicom_to_nifti_tab.py new file mode 100644 index 0000000..32d4357 --- /dev/null +++ b/test/gui/test_dicom_to_nifti_tab.py @@ -0,0 +1,62 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +import os +import pathlib +import tempfile +import time +import wx + +from .test_tabs import get_notebook, set_notebook_page, get_tab, get_all_children +from .. import realYield, run_with_orthopanel +from fsleyes_plugin_shimming_toolbox import __dir_testing__ +from fsleyes_plugin_shimming_toolbox.tabs.dicom_to_nifti_tab import DicomToNiftiTab + + +def test_st_plugin_dcm2niix_run(): + run_with_orthopanel(_test_st_plugin_dcm2niix_run) + + +def _test_st_plugin_dcm2niix_run(view, overlayList, displayCtx): + """ Makes sure dicom to nifti tab can be run (Add dummy input and simulate a click) """ + + nb_terminal = get_notebook(view) + + # Select the dcm2niix tab + assert set_notebook_page(nb_terminal, 'Dicom to Nifti') + # Get the ST tab + dcm2nifti_tab = get_tab(nb_terminal, DicomToNiftiTab) + assert dcm2nifti_tab is not None + + with tempfile.TemporaryDirectory(prefix='st_' + pathlib.Path(__file__).stem) as tmp: + path_input = os.path.join(__dir_testing__, 'dicom_unsorted') + + # Fill in dicom2nifti tab options + list_widgets = [] + get_all_children(dcm2nifti_tab.sizer_run, list_widgets) + for widget in list_widgets: + if isinstance(widget, wx.TextCtrl) and widget.IsShown(): + if widget.GetName() == 'input': + widget.SetValue(path_input) + realYield() + if widget.GetName() == 'subject': + widget.SetValue('test') + realYield() + if widget.GetName() == 'output': + widget.SetValue(tmp) + realYield() + + # Call the function ran when clicking run button + dcm2nifti_tab.run_component.run() + + # Search for files in the overlay for a maximum of 20s + time_limit = 20 # s + for i in range(time_limit): + realYield() + overlay_file = overlayList.find("sub-test_phase1") + time.sleep(1) + if overlay_file: + break + + # Make sure there is an output in the overlay (that would mean the ST CLI ran) + assert overlay_file is not None diff --git a/test/gui/test_fieldmap_tab.py b/test/gui/test_fieldmap_tab.py new file mode 100644 index 0000000..74502d0 --- /dev/null +++ b/test/gui/test_fieldmap_tab.py @@ -0,0 +1,160 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +import nibabel as nib +import numpy as np +import os +import pathlib +from shimmingtoolbox.masking.shapes import shapes +import tempfile +import time +import wx + +from .test_tabs import get_notebook, set_notebook_page, get_tab, get_all_children, set_dropdown_selection +from .. import realYield, run_with_orthopanel +from fsleyes_plugin_shimming_toolbox import __dir_testing__ +from fsleyes_plugin_shimming_toolbox.tabs.fieldmap_tab import FieldMapTab + + +def test_st_plugin_fieldmap_prelude(): + options = {'no_arg_roi': 'auto threshold', + 'unwrapper': 'Prelude'} + + def _test_st_plugin_fieldmap(view, overlayList, displayCtx, options=options): + __test_st_plugin_fieldmap(view, overlayList, displayCtx, options=options) + + run_with_orthopanel(_test_st_plugin_fieldmap) + + +def test_st_plugin_fieldmap_skimage(): + options = {'no_arg_roi': 'auto threshold', + 'unwrapper': 'Skimage'} + + def _test_st_plugin_fieldmap(view, overlayList, displayCtx, options=options): + __test_st_plugin_fieldmap(view, overlayList, displayCtx, options=options) + + run_with_orthopanel(_test_st_plugin_fieldmap) + + +def test_st_plugin_fieldmap_input_mask(): + options = {'no_arg_roi': 'mask', + 'unwrapper': 'Skimage'} + + def _test_st_plugin_fieldmap(view, overlayList, displayCtx, options=options): + __test_st_plugin_fieldmap(view, overlayList, displayCtx, options=options) + + run_with_orthopanel(_test_st_plugin_fieldmap) + + +def test_st_plugin_fieldmap_threshold(): + options = {'no_arg_roi': 'threshold', + 'unwrapper': 'Skimage'} + + def _test_st_plugin_fieldmap(view, overlayList, displayCtx, options=options): + __test_st_plugin_fieldmap(view, overlayList, displayCtx, options=options) + + run_with_orthopanel(_test_st_plugin_fieldmap) + + +def __test_st_plugin_fieldmap(view, overlayList, displayCtx, options): + """ Makes sure fieldmap tab can be run (Add dummy input and simulate a click) """ + nb_terminal = get_notebook(view) + + # Select the Fieldmap tab + assert set_notebook_page(nb_terminal, 'Fieldmap') + + # Get the ST tab + fmap_tab = get_tab(nb_terminal, FieldMapTab) + assert fmap_tab is not None + + with tempfile.TemporaryDirectory(prefix='st_' + pathlib.Path(__file__).stem) as tmp: + fname_mag = os.path.join(__dir_testing__, 'ds_b0', 'sub-fieldmap', 'fmap', 'sub-fieldmap_magnitude1.nii.gz') + fname_phase1 = os.path.join(__dir_testing__, 'ds_b0', 'sub-fieldmap', 'fmap', 'sub-fieldmap_phase1.nii.gz') + fname_phase2 = os.path.join(__dir_testing__, 'ds_b0', 'sub-fieldmap', 'fmap', 'sub-fieldmap_phase2.nii.gz') + fname_output = os.path.join(tmp, 'fieldmap.nii.gz') + fname_input_mask = os.path.join(tmp, 'input_mask.nii.gz') + fname_output_mask = os.path.join(tmp, 'output_mask.nii.gz') + + # Fill in Fieldmap tab options + list_widgets = [] + get_all_children(fmap_tab.sizer_run, list_widgets) + for widget in list_widgets: + if isinstance(widget, wx.TextCtrl): + if widget.GetName() == 'no_arg_nechoes': + widget.SetValue('2') + realYield() + list_widgets = [] + get_all_children(fmap_tab.sizer_run, list_widgets) + for widget in list_widgets: + if isinstance(widget, wx.Choice) and widget.IsShown(): + if widget.GetName() == 'no_arg_roi': + assert set_dropdown_selection(widget, options['no_arg_roi']) + # auto threshold, mask, threshold + if widget.GetName() == 'unwrapper': + assert set_dropdown_selection(widget, options['unwrapper']) + # Prelude, Skimage + list_widgets = [] + get_all_children(fmap_tab.sizer_run, list_widgets) + for widget in list_widgets: + if isinstance(widget, wx.TextCtrl) and widget.IsShown(): + if widget.GetName() == 'input_phase_1': + widget.SetValue(fname_phase1) + realYield() + if widget.GetName() == 'input_phase_2': + widget.SetValue(fname_phase2) + realYield() + if widget.GetName() == 'mag': + widget.SetValue(fname_mag) + realYield() + if widget.GetName() == 'threshold': + widget.SetValue('0.1') + realYield() + if widget.GetName() == 'output': + widget.SetValue(fname_output) + realYield() + if widget.GetName() == 'mask': + nii_mask = get_mask_from_fmap() + nib.save(nii_mask, fname_input_mask) + widget.SetValue(fname_input_mask) + realYield() + if widget.GetName() == 'savemask': + widget.SetValue(fname_output_mask) + realYield() + + # Call the function ran when clicking run button + fmap_tab.run_component.run() + + # Search for files in the overlay for a maximum of 20s + time_limit = 20 # s + for i in range(time_limit): + realYield() + overlay_file = overlayList.find("fieldmap") + time.sleep(1) + if overlay_file: + break + + # Make sure there is an output in the overlay (that would mean the ST CLI ran) + assert overlay_file is not None + assert os.path.exists(fname_output) + if options['no_arg_roi'] == 'mask': + assert not os.path.exists(fname_output_mask) + elif options['no_arg_roi'] == 'threshold': + assert os.path.exists(fname_output_mask) + else: + # auto threshold + assert os.path.exists(fname_output_mask) + + + +def get_mask_from_fmap(): + fname_mag = os.path.join(__dir_testing__, 'ds_b0', 'sub-fieldmap', 'fmap', 'sub-fieldmap_magnitude1.nii.gz') + nii_fmap = nib.load(fname_mag) + nx, ny, nz = nii_fmap.shape + + mask = shapes(nii_fmap.get_fdata(), 'cube', + center_dim1=int(nx / 2), + center_dim2=int(ny / 2), + len_dim1=10, len_dim2=10, len_dim3=nz - 10) + + nii_mask = nib.Nifti1Image(mask.astype(np.uint8), nii_fmap.affine) + return nii_mask diff --git a/test/gui/test_tabs.py b/test/gui/test_tabs.py index 975eba4..00b6ded 100644 --- a/test/gui/test_tabs.py +++ b/test/gui/test_tabs.py @@ -1,14 +1,10 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- -import os -import pathlib -import tempfile -import time import wx -from .. import realYield, run_with_orthopanel, simclick -from fsleyes_plugin_shimming_toolbox import __dir_testing__ +from .. import realYield, run_with_orthopanel +from fsleyes_plugin_shimming_toolbox.st_plugin import STControlPanel, NotebookTerminal def test_st_plugin_loads(): @@ -16,7 +12,6 @@ def test_st_plugin_loads(): def _test_st_plugin_loads(view, overlayList, displayCtx): - from fsleyes_plugin_shimming_toolbox.st_plugin import STControlPanel view.togglePanel(STControlPanel) realYield() @@ -32,69 +27,8 @@ def _test_st_plugin_tabs_exist(view, overlayList, displayCtx): assert len(tabs) > 0 -def test_st_plugin_dcm2niix_run(): - run_with_orthopanel(_test_st_plugin_dcm2niix_run) - - -def _test_st_plugin_dcm2niix_run(view, overlayList, displayCtx): - """ Makes sure dicom to nifti tab can be run (Add dummy input and simulate a click) """ - from fsleyes_plugin_shimming_toolbox.tabs.dicom_to_nifti_tab import DicomToNiftiTab - nb_terminal = get_notebook(view) - - # Select the dcm2niix tab - nb_terminal.SetSelection(0) - realYield() - tabs = nb_terminal.GetChildren() - dcm2nifti_tab = None - for tab in tabs: - if isinstance(tab, DicomToNiftiTab): - dcm2nifti_tab = tab - break - - assert dcm2nifti_tab is not None - - with tempfile.TemporaryDirectory(prefix='st_' + pathlib.Path(__file__).stem) as tmp: - path_input = os.path.join(__dir_testing__, 'dicom_unsorted') - - # Fill in dicom2nifti tab options - list_widgets = [] - get_all_children(dcm2nifti_tab.sizer_run, list_widgets) - for widget in list_widgets: - if isinstance(widget, wx.TextCtrl): - if widget.GetName() == 'input': - widget.SetValue(path_input) - realYield() - if widget.GetName() == 'subject': - widget.SetValue('test') - realYield() - if widget.GetName() == 'output': - widget.SetValue(tmp) - realYield() - - # Simulate a mouse click on the run button - sim = wx.UIActionSimulator() - for widget in list_widgets: - if isinstance(widget, wx.Button): - if widget.GetLabel() == 'Run': - simclick(sim, widget) - - # Search for files in the overllay for a maximum of 20s - time_limit = 20 # s - for i in range(time_limit): - realYield() - overlay_file = overlayList.find("sub-test_phase1") - time.sleep(1) - if overlay_file: - break - - # Make sure there is an output in the overlay (that would mean the ST CLI ran) - assert overlay_file is not None - - def get_notebook(view): - - from fsleyes_plugin_shimming_toolbox.st_plugin import STControlPanel - from fsleyes_plugin_shimming_toolbox.st_plugin import NotebookTerminal + """ Returns the notebook terminal from the ST plugin.""" ctrl = view.togglePanel(STControlPanel) realYield() @@ -111,7 +45,26 @@ def get_notebook(view): return nb_terminal +def set_notebook_page(nb_terminal, page_name): + """ Sets the notebook terminal to the page with the given name.""" + for i in range(nb_terminal.GetPageCount()): + if nb_terminal.GetPageText(i) == page_name: + nb_terminal.SetSelection(i) + realYield() + return True + return False + + +def get_tab(nb_terminal, tab_instance): + """ Returns the tab instance from the ST notebook.""" + tabs = nb_terminal.GetChildren() + for tab in tabs: + if isinstance(tab, tab_instance): + return tab + + def get_all_children(item, list_widgets, depth=None): + """ Returns all children and sub children from an item.""" if depth is not None: depth -= 1 @@ -127,3 +80,14 @@ def get_all_children(item, list_widgets, depth=None): get_all_children(sizer, list_widgets, depth=depth) else: list_widgets.append(widget) + + +def set_dropdown_selection(dropdown_widget, selection_name): + """ Sets the notebook terminal to the page with the given name.""" + for i in range(dropdown_widget.GetCount()): + if dropdown_widget.GetString(i) == selection_name: + dropdown_widget.SetSelection(i) + wx.PostEvent(dropdown_widget.GetEventHandler(), wx.CommandEvent(wx.EVT_CHOICE.typeId, dropdown_widget.GetId())) + realYield() + return True + return False