Exemple #1
0
    def __init__(self, parent, orig_tab_data):
        xrcfr_overview_acq.__init__(self, parent)

        self.conf = get_acqui_conf()

        # True when acquisition occurs
        self.acquiring = False
        self.data = None

        # a ProgressiveFuture if the acquisition is going on
        self.acq_future = None
        self._acq_future_connector = None

        self._main_data_model = orig_tab_data.main

        # duplicate the interface, but with only one view
        self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data)

        # The pattern to use for storing each tile file individually
        # None disables storing them
        self.filename_tiles = create_filename(self.conf.last_path, "{datelng}-{timelng}-overview",
                                              ".ome.tiff")

        # Create a new settings controller for the acquisition dialog
        self._settings_controller = LocalizationSettingsController(
            self,
            self._tab_data_model,
            highlight_change=True  # also adds a "Reset" context menu
        )

        self.zsteps = model.IntContinuous(1, range=(1, 51))
        self._zsteps_vac = VigilantAttributeConnector(self.zsteps, self.zstack_steps, events=wx.EVT_SLIDER)

        orig_view = orig_tab_data.focussedView.value
        self._view = self._tab_data_model.focussedView.value

        self.streambar_controller = StreamBarController(self._tab_data_model,
                                                        self.pnl_secom_streams,
                                                        static=True,
                                                        ignore_view=True)
        # The streams currently displayed are the one visible
        self.add_streams()

        # The list of streams ready for acquisition (just used as a cache)
        self._acq_streams = {}

        # Compute the preset values for each preset
        self._orig_entries = get_global_settings_entries(self._settings_controller)
        self._orig_settings = preset_as_is(self._orig_entries)
        for sc in self.streambar_controller.stream_controllers:
            self._orig_entries += get_local_settings_entries(sc)

        self.start_listening_to_va()

        # make sure the view displays the same thing as the one we are
        # duplicating
        self._view.view_pos.value = orig_view.view_pos.value
        self._view.mpp.value = orig_view.mpp.value
        self._view.merge_ratio.value = orig_view.merge_ratio.value

        # attach the view to the viewport
        self.pnl_view_acq.canvas.fit_view_to_next_image = False
        self.pnl_view_acq.setView(self._view, self._tab_data_model)

        self.Bind(wx.EVT_CHAR_HOOK, self.on_key)

        self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close)
        self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire)
        self.Bind(wx.EVT_CLOSE, self.on_close)
        # on_streams_changed is compatible because it doesn't use the args

        # To update the estimated time when streams are removed/added
        self._view.stream_tree.flat.subscribe(self.on_streams_changed)

        # Set parameters for tiled acq
        self.overlap = 0.2
        try:
            # Use the stage range, which can be overridden by the MD_POS_ACTIVE_RANGE,
            # which can be overridden by MD_OVERVIEW_RANGE.
            # Note: this last one might be temporary, until we have a RoA tool provided in the GUI.
            stage_rng = {
                "x": self._main_data_model.stage.axes["x"].range,
                "y": self._main_data_model.stage.axes["y"].range
            }

            stage_md = self._main_data_model.stage.getMetadata()
            if model.MD_POS_ACTIVE_RANGE in stage_md:
                stage_rng.update(stage_md[model.MD_POS_ACTIVE_RANGE])
            if model.MD_OVERVIEW_RANGE in stage_md:
                stage_rng.update(stage_md[model.MD_OVERVIEW_RANGE])

            # left, bottom, right, top
            self.area = (stage_rng["x"][0], stage_rng["y"][0], stage_rng["x"][1], stage_rng["y"][1])
        except (KeyError, IndexError):
            raise ValueError("Failed to find stage.MD_POS_ACTIVE_RANGE with x and y range")

        # Note: It should never be possible to reach here with no streams
        streams = self.get_acq_streams()
        for s in streams:
            self._view.addStream(s)

        self.update_acquisition_time()
Exemple #2
0
    def __init__(self, parent, orig_tab_data):
        xrcfr_overview_acq.__init__(self, parent)

        self.conf = get_acqui_conf()

        # True when acquisition occurs
        self.acquiring = False
        self.data = None

        # a ProgressiveFuture if the acquisition is going on
        self.acq_future = None
        self._acq_future_connector = None

        self._main_data_model = orig_tab_data.main

        # duplicate the interface, but with only one view
        self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data)

        # Store the final image as {datelng}-{timelng}-overview
        # The pattern to store them in a sub folder, with the name xxxx-overview-tiles/xxx-overview-NxM.ome.tiff
        # The pattern to use for storing each tile file individually
        # None disables storing them
        save_dir = self.conf.last_path
        if isinstance(orig_tab_data, guimodel.CryoGUIData):
            save_dir = self.conf.pj_last_path
        self.filename = create_filename(save_dir,
                                        "{datelng}-{timelng}-overview",
                                        ".ome.tiff")
        assert self.filename.endswith(".ome.tiff")
        dirname, basename = os.path.split(self.filename)
        tiles_dir = os.path.join(dirname,
                                 basename[:-len(".ome.tiff")] + "-tiles")
        self.filename_tiles = os.path.join(tiles_dir, basename)

        # Create a new settings controller for the acquisition dialog
        self._settings_controller = LocalizationSettingsController(
            self,
            self._tab_data_model,
        )

        self.zsteps = model.IntContinuous(1, range=(1, 51))
        # The depth of field is an indication of how far the focus needs to move
        # to see the current in-focus position out-of-focus. So it's a good default
        # value for the zstep size. We use 2x to "really" see something else.
        # Typically, it's about 1 µm.
        dof = self._main_data_model.ccd.depthOfField.value
        self.zstep_size = model.FloatContinuous(2 * dof,
                                                range=(1e-9, 100e-6),
                                                unit="m")
        self._zstep_size_vac = VigilantAttributeConnector(
            self.zstep_size, self.zstep_size_ctrl, events=wx.EVT_COMMAND_ENTER)

        self.tiles_nx = model.IntContinuous(5, range=(1, 1000))
        self.tiles_ny = model.IntContinuous(5, range=(1, 1000))
        self._zsteps_vac = VigilantAttributeConnector(self.zsteps,
                                                      self.zstack_steps,
                                                      events=wx.EVT_SLIDER)
        self._tiles_n_vacx = VigilantAttributeConnector(
            self.tiles_nx, self.tiles_number_x, events=wx.EVT_COMMAND_ENTER)
        self._tiles_n_vacy = VigilantAttributeConnector(
            self.tiles_ny, self.tiles_number_y, events=wx.EVT_COMMAND_ENTER)

        self.area = None  # None or 4 floats: left, top, right, bottom positions of the acquisition area (in m)

        orig_view = orig_tab_data.focussedView.value
        self._view = self._tab_data_model.focussedView.value

        self.streambar_controller = StreamBarController(self._tab_data_model,
                                                        self.pnl_secom_streams,
                                                        static=True,
                                                        ignore_view=True)
        # The streams currently displayed are the one visible
        self.add_streams()

        # The list of streams ready for acquisition (just used as a cache)
        self._acq_streams = {}

        # Find every setting, and listen to it
        self._orig_entries = get_global_settings_entries(
            self._settings_controller)
        for sc in self.streambar_controller.stream_controllers:
            self._orig_entries += get_local_settings_entries(sc)

        self.start_listening_to_va()

        # make sure the view displays the same thing as the one we are
        # duplicating
        self._view.view_pos.value = orig_view.view_pos.value
        self._view.mpp.value = orig_view.mpp.value
        self._view.merge_ratio.value = orig_view.merge_ratio.value

        # attach the view to the viewport
        self.pnl_view_acq.canvas.fit_view_to_next_image = False
        self.pnl_view_acq.setView(self._view, self._tab_data_model)

        self.Bind(wx.EVT_CHAR_HOOK, self.on_key)

        self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close)
        self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire)
        self.Bind(wx.EVT_CLOSE, self.on_close)

        # Set parameters for tiled acq
        self.overlap = 0.2
        try:
            # Use the stage range, which can be overridden by the MD_POS_ACTIVE_RANGE.
            # Note: this last one might be temporary, until we have a RoA tool provided in the GUI.
            self._tiling_rng = {
                "x": self._main_data_model.stage.axes["x"].range,
                "y": self._main_data_model.stage.axes["y"].range
            }

            stage_md = self._main_data_model.stage.getMetadata()
            if model.MD_POS_ACTIVE_RANGE in stage_md:
                self._tiling_rng.update(stage_md[model.MD_POS_ACTIVE_RANGE])
        except (KeyError, IndexError):
            raise ValueError(
                "Failed to find stage.MD_POS_ACTIVE_RANGE with x and y range")

        # Note: It should never be possible to reach here with no streams
        streams = self.get_acq_streams()
        for s in streams:
            self._view.addStream(s)

        # To update the estimated time & area when streams are removed/added
        self._view.stream_tree.flat.subscribe(self.on_streams_changed,
                                              init=True)
Exemple #3
0
class OverviewAcquisitionDialog(xrcfr_overview_acq):
    """
    Class used to control the cryo-secom overview dialog
    """
    def __init__(self, parent, orig_tab_data):
        xrcfr_overview_acq.__init__(self, parent)

        self.conf = get_acqui_conf()

        # True when acquisition occurs
        self.acquiring = False
        self.data = None

        # a ProgressiveFuture if the acquisition is going on
        self.acq_future = None
        self._acq_future_connector = None

        self._main_data_model = orig_tab_data.main

        # duplicate the interface, but with only one view
        self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data)

        # The pattern to use for storing each tile file individually
        # None disables storing them
        self.filename_tiles = create_filename(self.conf.last_path, "{datelng}-{timelng}-overview",
                                              ".ome.tiff")

        # Create a new settings controller for the acquisition dialog
        self._settings_controller = LocalizationSettingsController(
            self,
            self._tab_data_model,
            highlight_change=True  # also adds a "Reset" context menu
        )

        self.zsteps = model.IntContinuous(1, range=(1, 51))
        self._zsteps_vac = VigilantAttributeConnector(self.zsteps, self.zstack_steps, events=wx.EVT_SLIDER)

        orig_view = orig_tab_data.focussedView.value
        self._view = self._tab_data_model.focussedView.value

        self.streambar_controller = StreamBarController(self._tab_data_model,
                                                        self.pnl_secom_streams,
                                                        static=True,
                                                        ignore_view=True)
        # The streams currently displayed are the one visible
        self.add_streams()

        # The list of streams ready for acquisition (just used as a cache)
        self._acq_streams = {}

        # Compute the preset values for each preset
        self._orig_entries = get_global_settings_entries(self._settings_controller)
        self._orig_settings = preset_as_is(self._orig_entries)
        for sc in self.streambar_controller.stream_controllers:
            self._orig_entries += get_local_settings_entries(sc)

        self.start_listening_to_va()

        # make sure the view displays the same thing as the one we are
        # duplicating
        self._view.view_pos.value = orig_view.view_pos.value
        self._view.mpp.value = orig_view.mpp.value
        self._view.merge_ratio.value = orig_view.merge_ratio.value

        # attach the view to the viewport
        self.pnl_view_acq.canvas.fit_view_to_next_image = False
        self.pnl_view_acq.setView(self._view, self._tab_data_model)

        self.Bind(wx.EVT_CHAR_HOOK, self.on_key)

        self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close)
        self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire)
        self.Bind(wx.EVT_CLOSE, self.on_close)
        # on_streams_changed is compatible because it doesn't use the args

        # To update the estimated time when streams are removed/added
        self._view.stream_tree.flat.subscribe(self.on_streams_changed)

        # Set parameters for tiled acq
        self.overlap = 0.2
        try:
            # Use the stage range, which can be overridden by the MD_POS_ACTIVE_RANGE,
            # which can be overridden by MD_OVERVIEW_RANGE.
            # Note: this last one might be temporary, until we have a RoA tool provided in the GUI.
            stage_rng = {
                "x": self._main_data_model.stage.axes["x"].range,
                "y": self._main_data_model.stage.axes["y"].range
            }

            stage_md = self._main_data_model.stage.getMetadata()
            if model.MD_POS_ACTIVE_RANGE in stage_md:
                stage_rng.update(stage_md[model.MD_POS_ACTIVE_RANGE])
            if model.MD_OVERVIEW_RANGE in stage_md:
                stage_rng.update(stage_md[model.MD_OVERVIEW_RANGE])

            # left, bottom, right, top
            self.area = (stage_rng["x"][0], stage_rng["y"][0], stage_rng["x"][1], stage_rng["y"][1])
        except (KeyError, IndexError):
            raise ValueError("Failed to find stage.MD_POS_ACTIVE_RANGE with x and y range")

        # Note: It should never be possible to reach here with no streams
        streams = self.get_acq_streams()
        for s in streams:
            self._view.addStream(s)

        self.update_acquisition_time()

    def start_listening_to_va(self):
        # Get all the VA's from the stream and subscribe to them for changes.
        for entry in self._orig_entries:
            if hasattr(entry, "vigilattr"):
                entry.vigilattr.subscribe(self.on_setting_change)

        self.zsteps.subscribe(self.on_setting_change)

    def stop_listening_to_va(self):
        for entry in self._orig_entries:
            if hasattr(entry, "vigilattr"):
                entry.vigilattr.unsubscribe(self.on_setting_change)

        self.zsteps.unsubscribe(self.on_setting_change)

    def duplicate_tab_data_model(self, orig):
        """
        Duplicate a MicroscopyGUIData and adapt it for the acquisition window
        The streams will be shared, but not the views
        orig (MicroscopyGUIData)
        return (MicroscopyGUIData)
        """
        data_model = AcquisitionWindowData(orig.main)
        data_model.streams.value.extend(orig.streams.value)

        # create view (which cannot move or focus)
        view = guimodel.MicroscopeView("All")
        data_model.views.value = [view]
        data_model.focussedView.value = view
        return data_model

    def add_streams(self):
        """
        Add live streams
        """
        # go through all the streams available in the interface model
        for s in self._tab_data_model.streams.value:

            if not isinstance(s, LiveStream):
                continue

            self.streambar_controller.addStream(s, add_to_view=self._view)

    def remove_all_streams(self):
        """
        Remove the streams we added to the view on creation
        Must be called in the main GUI thread
        """
        # Ensure we don't update the view after the window is destroyed
        self.streambar_controller.clear()

        # TODO: need to have a .clear() on the settings_controller to clean up?
        self._settings_controller = None
        self._acq_streams = {}  # also empty the cache

        gc.collect()  # To help reclaiming some memory

    def get_acq_streams(self):
        """
        return (list of Streams): the streams to be acquired
        """
        # Only acquire the streams which are displayed
        streams = self._view.getStreams()
        return streams

    @wxlimit_invocation(0.1)
    def update_setting_display(self):
        if not self:
            return

        # if gauge was left over from an error => now hide it
        if self.acquiring:
            self.gauge_acq.Show()
            return
        elif self.gauge_acq.IsShown():
            self.gauge_acq.Hide()
            self.Layout()

        self.update_acquisition_time()

        # update highlight
        for se, value in self._orig_settings.items():
            se.highlight(se.vigilattr.value != value)

    def on_streams_changed(self, val):
        """
        When the list of streams to acquire has changed
        """
        self.update_setting_display()

    def on_setting_change(self, _=None):
        self.update_setting_display()

    def update_acquisition_time(self, acq_time=999):
        """
        Must be called in the main GUI thread.
        """
        if not self.acquiring:
            streams = self.get_acq_streams()
            if not streams:
                acq_time = 0
            else:
                acq_time = stitching.estimateTiledAcquisitionTime(streams,
                                                              self._main_data_model.stage,
                                                              self.area, self.overlap,
                                                              zlevels=self._get_zstack_levels())

        txt = "The estimated acquisition time is {}."
        txt = txt.format(units.readable_time(acq_time))

        self.lbl_acqestimate.SetLabel(txt)

    def on_key(self, evt):
        """ Dialog key press handler. """
        if evt.GetKeyCode() == wx.WXK_ESCAPE:
            self.Close()
        else:
            evt.Skip()

    def terminate_listeners(self):
        """
        Disconnect all the connections to the streams.
        Must be called in the main GUI thread.
        """
        # stop listening to events
        self._view.stream_tree.flat.unsubscribe(self.on_streams_changed)
        self.stop_listening_to_va()

        self.remove_all_streams()

    def on_close(self, evt):
        """ Close event handler that executes various cleanup actions
        """
        if self.acq_future:
            # TODO: ask for confirmation before cancelling?
            # What to do if the acquisition is done while asking for
            # confirmation?
            msg = "Cancelling acquisition due to closing the acquisition window"
            logging.info(msg)
            self.acq_future.cancel()

        self.terminate_listeners()
        # Set the streambar controller to None so it wouldn't be a listener to stream.remove
        self.streambar_controller = None

        self.EndModal(wx.ID_CANCEL)

    def _pause_settings(self):
        """ Pause the settings of the GUI and save the values for restoring them later """
        self._settings_controller.pause()
        self._settings_controller.enable(False)

        self.streambar_controller.pause()
        self.streambar_controller.enable(False)

    def _resume_settings(self):
        """ Resume the settings of the GUI and save the values for restoring them later """
        self._settings_controller.enable(True)
        self._settings_controller.resume()

        self.streambar_controller.enable(True)
        self.streambar_controller.resume()

    def _get_zstack_levels(self):
        """
        Calculate the zstack levels from the current focus position and zsteps value
        :returns (list(float) or None) zstack levels for zstack acquisition
        """
        focus_value = self._main_data_model.focus.position.value['z']
        # Clip zsteps value to allowed range
        zsteps = self.zsteps.value
        if zsteps == 1:
            return None
        # Create focus zlevels from the given zsteps number
        zlevels = numpy.linspace(focus_value - (zsteps / 2 * ZSTEP), focus_value + (zsteps / 2 * ZSTEP), zsteps).tolist()
        return zlevels

    def _fit_view_to_area(self):
        center = ((self.area[0] + self.area[2]) / 2,
                  (self.area[1] + self.area[3]) / 2)
        self._view.view_pos.value = center

        fov = (self.area[2] - self.area[0], self.area[3] - self.area[1])
        self.pnl_view_acq.set_mpp_from_fov(fov)

    def on_acquire(self, evt):
        """ Start the actual acquisition """
        logging.info("Acquire button clicked, starting acquisition")
        acq_streams = self.get_acq_streams()
        if not acq_streams:
            logging.info("No stream to acquire, ending immediately")
            self.on_close(evt)  # Nothing to do, so it's the same as closing the window

        self.acquiring = True

        # Adjust view FoV to the whole area, so that it's possible to follow the acquisition
        self._fit_view_to_area()

        self.btn_secom_acquire.Disable()

        # disable estimation time updates during acquisition
        self._view.lastUpdate.unsubscribe(self.on_streams_changed)

        # Freeze all the settings so that it's not possible to change anything
        self._pause_settings()

        self.gauge_acq.Show()
        self.Layout()  # to put the gauge at the right place

        # For now, always indicate the best quality
        if self._main_data_model.opm:
            self._main_data_model.opm.setAcqQuality(path.ACQ_QUALITY_BEST)

        zlevels = self._get_zstack_levels()
        logging.info("Acquisition tiles logged at %s", self.filename_tiles)
        self.acq_future = stitching.acquireTiledArea(acq_streams, self._main_data_model.stage, area=self.area,
                                                     overlap=self.overlap,
                                                     settings_obs=self._main_data_model.settings_obs,
                                                     log_path=self.filename_tiles, zlevels=zlevels,
                                                     weaver=WEAVER_COLLAGE_REVERSE)
        self._acq_future_connector = ProgressiveFutureConnector(self.acq_future,
                                                                self.gauge_acq,
                                                                self.lbl_acqestimate)
        # TODO: Build-up the complete image during the acquisition, so that the
        #       progress can be followed live.
        self.acq_future.add_done_callback(self.on_acquisition_done)

        self.btn_cancel.SetLabel("Cancel")
        self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel)

    def on_cancel(self, evt):
        """ Handle acquisition cancel button click """
        logging.info("Cancel button clicked, stopping acquisition")
        self.acq_future.cancel()
        self.acquiring = False
        self.btn_cancel.SetLabel("Close")
        # all the rest will be handled by on_acquisition_done()

    @call_in_wx_main
    def on_acquisition_done(self, future):
        """ Callback called when the acquisition is finished (either successfully or cancelled) """
        if self._main_data_model.opm:
            self._main_data_model.opm.setAcqQuality(path.ACQ_QUALITY_FAST)

        # bind button back to direct closure
        self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close)
        self._resume_settings()

        self.acquiring = False

        # re-enable estimation time updates
        self._view.lastUpdate.subscribe(self.on_streams_changed)

        self.acq_future = None  # To avoid holding the ref in memory
        self._acq_future_connector = None

        try:
            # TODO: Add code for getting the data from the acquisition future
            self.data = future.result(1)  # timeout is just for safety
            self.conf.fn_count = update_counter(self.conf.fn_count)
        except CancelledError:
            # put back to original state:
            # re-enable the acquire button
            self.btn_secom_acquire.Enable()

            # hide progress bar (+ put pack estimated time)
            self.update_acquisition_time()
            self.gauge_acq.Hide()
            self.Layout()
            return
        except Exception:
            # We cannot do much: just warn the user and pretend it was cancelled
            logging.exception("Acquisition failed")
            self.btn_secom_acquire.Enable()
            self.lbl_acqestimate.SetLabel("Acquisition failed.")
            self.lbl_acqestimate.Parent.Layout()
            # leave the gauge, to give a hint on what went wrong.
            return

        self.terminate_listeners()
        self.EndModal(wx.ID_OPEN)
Exemple #4
0
class OverviewAcquisitionDialog(xrcfr_overview_acq):
    """
    Class used to control the overview acquisition dialog
    The data acquired is stored in a file, with predefined name, available on
      .filename and it is opened (as pyramidal data) in .data .
    """
    def __init__(self, parent, orig_tab_data):
        xrcfr_overview_acq.__init__(self, parent)

        self.conf = get_acqui_conf()

        # True when acquisition occurs
        self.acquiring = False
        self.data = None

        # a ProgressiveFuture if the acquisition is going on
        self.acq_future = None
        self._acq_future_connector = None

        self._main_data_model = orig_tab_data.main

        # duplicate the interface, but with only one view
        self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data)

        # Store the final image as {datelng}-{timelng}-overview
        # The pattern to store them in a sub folder, with the name xxxx-overview-tiles/xxx-overview-NxM.ome.tiff
        # The pattern to use for storing each tile file individually
        # None disables storing them
        save_dir = self.conf.last_path
        if isinstance(orig_tab_data, guimodel.CryoGUIData):
            save_dir = self.conf.pj_last_path
        self.filename = create_filename(save_dir,
                                        "{datelng}-{timelng}-overview",
                                        ".ome.tiff")
        assert self.filename.endswith(".ome.tiff")
        dirname, basename = os.path.split(self.filename)
        tiles_dir = os.path.join(dirname,
                                 basename[:-len(".ome.tiff")] + "-tiles")
        self.filename_tiles = os.path.join(tiles_dir, basename)

        # Create a new settings controller for the acquisition dialog
        self._settings_controller = LocalizationSettingsController(
            self,
            self._tab_data_model,
        )

        self.zsteps = model.IntContinuous(1, range=(1, 51))
        # The depth of field is an indication of how far the focus needs to move
        # to see the current in-focus position out-of-focus. So it's a good default
        # value for the zstep size. We use 2x to "really" see something else.
        # Typically, it's about 1 µm.
        dof = self._main_data_model.ccd.depthOfField.value
        self.zstep_size = model.FloatContinuous(2 * dof,
                                                range=(1e-9, 100e-6),
                                                unit="m")
        self._zstep_size_vac = VigilantAttributeConnector(
            self.zstep_size, self.zstep_size_ctrl, events=wx.EVT_COMMAND_ENTER)

        self.tiles_nx = model.IntContinuous(5, range=(1, 1000))
        self.tiles_ny = model.IntContinuous(5, range=(1, 1000))
        self._zsteps_vac = VigilantAttributeConnector(self.zsteps,
                                                      self.zstack_steps,
                                                      events=wx.EVT_SLIDER)
        self._tiles_n_vacx = VigilantAttributeConnector(
            self.tiles_nx, self.tiles_number_x, events=wx.EVT_COMMAND_ENTER)
        self._tiles_n_vacy = VigilantAttributeConnector(
            self.tiles_ny, self.tiles_number_y, events=wx.EVT_COMMAND_ENTER)

        self.area = None  # None or 4 floats: left, top, right, bottom positions of the acquisition area (in m)

        orig_view = orig_tab_data.focussedView.value
        self._view = self._tab_data_model.focussedView.value

        self.streambar_controller = StreamBarController(self._tab_data_model,
                                                        self.pnl_secom_streams,
                                                        static=True,
                                                        ignore_view=True)
        # The streams currently displayed are the one visible
        self.add_streams()

        # The list of streams ready for acquisition (just used as a cache)
        self._acq_streams = {}

        # Find every setting, and listen to it
        self._orig_entries = get_global_settings_entries(
            self._settings_controller)
        for sc in self.streambar_controller.stream_controllers:
            self._orig_entries += get_local_settings_entries(sc)

        self.start_listening_to_va()

        # make sure the view displays the same thing as the one we are
        # duplicating
        self._view.view_pos.value = orig_view.view_pos.value
        self._view.mpp.value = orig_view.mpp.value
        self._view.merge_ratio.value = orig_view.merge_ratio.value

        # attach the view to the viewport
        self.pnl_view_acq.canvas.fit_view_to_next_image = False
        self.pnl_view_acq.setView(self._view, self._tab_data_model)

        self.Bind(wx.EVT_CHAR_HOOK, self.on_key)

        self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close)
        self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire)
        self.Bind(wx.EVT_CLOSE, self.on_close)

        # Set parameters for tiled acq
        self.overlap = 0.2
        try:
            # Use the stage range, which can be overridden by the MD_POS_ACTIVE_RANGE.
            # Note: this last one might be temporary, until we have a RoA tool provided in the GUI.
            self._tiling_rng = {
                "x": self._main_data_model.stage.axes["x"].range,
                "y": self._main_data_model.stage.axes["y"].range
            }

            stage_md = self._main_data_model.stage.getMetadata()
            if model.MD_POS_ACTIVE_RANGE in stage_md:
                self._tiling_rng.update(stage_md[model.MD_POS_ACTIVE_RANGE])
        except (KeyError, IndexError):
            raise ValueError(
                "Failed to find stage.MD_POS_ACTIVE_RANGE with x and y range")

        # Note: It should never be possible to reach here with no streams
        streams = self.get_acq_streams()
        for s in streams:
            self._view.addStream(s)

        # To update the estimated time & area when streams are removed/added
        self._view.stream_tree.flat.subscribe(self.on_streams_changed,
                                              init=True)

    def start_listening_to_va(self):
        # Get all the VA's from the stream and subscribe to them for changes.
        for entry in self._orig_entries:
            if hasattr(entry, "vigilattr"):
                entry.vigilattr.subscribe(self.on_setting_change)

        self.zsteps.subscribe(self.on_setting_change)
        self.tiles_nx.subscribe(self.on_tiles_number)
        self.tiles_ny.subscribe(self.on_tiles_number)

    def stop_listening_to_va(self):
        for entry in self._orig_entries:
            if hasattr(entry, "vigilattr"):
                entry.vigilattr.unsubscribe(self.on_setting_change)

        self.zsteps.unsubscribe(self.on_setting_change)
        self.tiles_nx.unsubscribe(self.on_tiles_number)
        self.tiles_ny.unsubscribe(self.on_tiles_number)

    def duplicate_tab_data_model(self, orig):
        """
        Duplicate a MicroscopyGUIData and adapt it for the acquisition window
        The streams will be shared, but not the views
        orig (MicroscopyGUIData)
        return (MicroscopyGUIData)
        """
        data_model = AcquisitionWindowData(orig.main)
        data_model.streams.value.extend(orig.streams.value)

        # create view (which cannot move or focus)
        view = guimodel.MicroscopeView("All")
        data_model.views.value = [view]
        data_model.focussedView.value = view
        return data_model

    def add_streams(self):
        """
        Add live streams
        """
        # go through all the streams available in the interface model
        for s in self._tab_data_model.streams.value:

            if not isinstance(s, LiveStream):
                continue

            self.streambar_controller.addStream(s, add_to_view=self._view)

    def remove_all_streams(self):
        """
        Remove the streams we added to the view on creation
        Must be called in the main GUI thread
        """
        # Ensure we don't update the view after the window is destroyed
        self.streambar_controller.clear()

        # TODO: need to have a .clear() on the settings_controller to clean up?
        self._settings_controller = None
        self._acq_streams = {}  # also empty the cache

        gc.collect()  # To help reclaiming some memory

    def get_acq_streams(self):
        """
        return (list of Streams): the streams to be acquired
        """
        # Only acquire the streams which are displayed
        streams = self._view.getStreams()
        return streams

    def update_area_size(self):
        """
        Calculates the requested tiling area size, based on the tiles number
        """
        # get smallest fov
        fovs = [self.get_fov(s) for s in self.get_acq_streams()]
        if not fovs:
            # fall back to a small fov (default)
            self.fov = DEFAULT_FOV
        else:
            # smallest fov
            self.fov = (min(f[0] for f in fovs), min(f[1] for f in fovs))

        nx = self.tiles_nx.value
        ny = self.tiles_ny.value
        # these formulas for w and h have to match the ones used in the 'stitching' module.
        w = nx * self.fov[0] * (1 - self.overlap)
        h = ny * self.fov[1] * (1 - self.overlap)

        pos = self._tab_data_model.main.stage.position.value
        # Note the area can accept LTRB or LBRT.
        self.area = self.clip_tiling_area_to_range(w, h, pos, self._tiling_rng)
        if self.area is None:
            # there is no intersection
            logging.warning(
                "Couldn't find intersection between stage pos %s and tiling range %s"
                % (pos, self._tiling_rng))

    @wxlimit_invocation(0.1)
    def update_setting_display(self):
        if not self:
            return

        # if gauge was left over from an error => now hide it
        if self.acquiring:
            self.gauge_acq.Show()
            return
        elif self.gauge_acq.IsShown():
            self.gauge_acq.Hide()
            self.Layout()

        # Some settings can affect the FoV. Also, adding/removing the stream with
        # the smallest FoV would also affect the area.
        self.update_area_size()

        # Disable acquisition button if no area
        self.btn_secom_acquire.Enable(self.area is not None)

        if self.area is None:
            self.area_size_txt.SetLabel("Invalid stage position")
            return

        area_size = self.area[2] - self.area[0], self.area[3] - self.area[1]
        area_size_str = util.readable_str(area_size, unit="m", sig=3)
        self.area_size_txt.SetLabel(area_size_str)

        self.update_acquisition_time()

    def on_streams_changed(self, _=None):
        """
        When the list of streams to acquire has changed
        """
        self.update_setting_display()

    def on_tiles_number(self, _=None):
        """
        Called when the user enters values for the tiles number in the GUI.
        """
        self.update_setting_display()

    def on_setting_change(self, _=None):
        self.update_setting_display()

    def update_acquisition_time(self):
        """
        Must be called in the main GUI thread.
        """
        if self.acquiring:
            return

        if not self.area:
            logging.debug(
                "Unknown acquisition area, cannot estimate acquisition time")
            return

        streams = self.get_acq_streams()
        if not streams:
            acq_time = 0
        else:
            zlevels = self._get_zstack_levels()
            focus_mtd = FocusingMethod.MAX_INTENSITY_PROJECTION if zlevels else FocusingMethod.NONE
            acq_time = stitching.estimateTiledAcquisitionTime(
                streams,
                self._main_data_model.stage,
                self.area,
                self.overlap,
                zlevels=zlevels,
                focusing_method=focus_mtd)

        txt = "The estimated acquisition time is {}."
        txt = txt.format(units.readable_time(acq_time))
        self.lbl_acqestimate.SetLabel(txt)

    def get_fov(self, s):
        try:
            return s.guessFoV()
        except (NotImplementedError, AttributeError):
            raise TypeError(
                "Unsupported Stream %s, it doesn't have a .guessFoV()" % (s, ))

    @staticmethod
    def clip_tiling_area_to_range(w, h, pos, tiling_rng):
        """
        Finds the intersection between the requested tiling area and the tiling range.
        w (float): width of the tiling area
        h (float): height of the tiling area
        pos (dict -> float): current position of the stage
        tiling_rng (dict -> list): the tiling range along x and y axes as
          (xmin, ymin, xmax, ymax), or (xmin, ymax, xmax, ymin)
        return (None or tuple of 4 floats): None if there is no intersection, or
          the rectangle representing the intersection as (xmin, ymin, xmax, ymax).
        """
        area_req = (pos["x"] - w / 2, pos["y"] - h / 2, pos["x"] + w / 2,
                    pos["y"] + h / 2)
        # clip the tiling area, if needed (or find the intersection between the active range and the requested area)
        return rect_intersect(area_req,
                              (tiling_rng["x"][0], tiling_rng["y"][1],
                               tiling_rng["x"][1], tiling_rng["y"][0]))

    def on_key(self, evt):
        """ Dialog key press handler. """
        if evt.GetKeyCode() == wx.WXK_ESCAPE:
            self.Close()
        else:
            evt.Skip()

    def terminate_listeners(self):
        """
        Disconnect all the connections to the streams.
        Must be called in the main GUI thread.
        """
        # stop listening to events
        self._view.stream_tree.flat.unsubscribe(self.on_streams_changed)
        self.stop_listening_to_va()

        self.remove_all_streams()
        # Set the streambar controller to None so it wouldn't be a listener to stream.remove
        self.streambar_controller = None

    def on_close(self, evt):
        """ Close event handler that executes various cleanup actions
        """
        if self.acq_future:
            # TODO: ask for confirmation before cancelling?
            # What to do if the acquisition is done while asking for
            # confirmation?
            msg = "Cancelling acquisition due to closing the acquisition window"
            logging.info(msg)
            self.acq_future.cancel()

        self.terminate_listeners()

        self.EndModal(wx.ID_CANCEL)

    def _pause_settings(self):
        """ Pause the settings of the GUI and save the values for restoring them later """
        self._settings_controller.pause()
        self._settings_controller.enable(False)

        self.streambar_controller.pause()
        self.streambar_controller.enable(False)

    def _resume_settings(self):
        """ Resume the settings of the GUI and save the values for restoring them later """
        self._settings_controller.enable(True)
        self._settings_controller.resume()

        self.streambar_controller.enable(True)
        self.streambar_controller.resume()

    def _get_zstack_levels(self):
        """
        Calculate the zstack levels from the current focus position and zsteps value
        :returns (list(float) or None) zstack levels for zstack acquisition.
          return None if only one zstep is requested.
        """
        zsteps = self.zsteps.value
        if zsteps == 1:
            return None

        # Clip zsteps value to allowed range
        focus_value = self._main_data_model.focus.position.value['z']
        focus_range = self._main_data_model.focus.axes['z'].range
        zmin = focus_value - (zsteps / 2 * self.zstep_size.value)
        zmax = focus_value + (zsteps / 2 * self.zstep_size.value)
        if (zmax - zmin) > (focus_range[1] - focus_range[0]):
            # Corner case: it'd be larger than the entire range => limit to the entire range
            zmin = focus_range[0]
            zmax = focus_range[1]
        if zmax > focus_range[1]:
            # Too high => shift down
            zmax -= zmax - focus_range[1]
            zmin -= zmax - focus_range[1]
        if zmin < focus_range[0]:
            # Too low => shift up
            zmin += focus_range[0] - zmin
            zmax += focus_range[0] - zmin

        # Create focus zlevels from the given zsteps number
        zlevels = numpy.linspace(zmin, zmax, zsteps).tolist()
        return zlevels

    def _fit_view_to_area(self):
        if self.area is None:
            logging.warning("Unknown area, cannot fit view")
            return

        center = ((self.area[0] + self.area[2]) / 2,
                  (self.area[1] + self.area[3]) / 2)
        self._view.view_pos.value = center

        fov = (self.area[2] - self.area[0], self.area[3] - self.area[1])
        self.pnl_view_acq.set_mpp_from_fov(fov)

    def on_acquire(self, evt):
        """ Start the actual acquisition """
        logging.info("Acquire button clicked, starting acquisition")
        acq_streams = self.get_acq_streams()
        if not acq_streams:
            logging.info("No stream to acquire, ending immediately")
            self.on_close(
                evt)  # Nothing to do, so it's the same as closing the window

        self.acquiring = True

        # Adjust view FoV to the whole area, so that it's possible to follow the acquisition
        self._fit_view_to_area()

        self.btn_secom_acquire.Disable()

        # Freeze all the settings so that it's not possible to change anything
        self._pause_settings()

        self.gauge_acq.Show()
        self.Layout()  # to put the gauge at the right place

        # For now, always indicate the best quality
        if self._main_data_model.opm:
            self._main_data_model.opm.setAcqQuality(path.ACQ_QUALITY_BEST)

        zlevels = self._get_zstack_levels()
        focus_mtd = FocusingMethod.MAX_INTENSITY_PROJECTION if zlevels else FocusingMethod.NONE

        if self.filename_tiles:
            logging.info("Acquisition tiles logged at %s", self.filename_tiles)
            os.makedirs(os.path.dirname(self.filename_tiles))

        self.acq_future = stitching.acquireTiledArea(
            acq_streams,
            self._main_data_model.stage,
            area=self.area,
            overlap=self.overlap,
            settings_obs=self._main_data_model.settings_obs,
            log_path=self.filename_tiles,
            weaver=WEAVER_COLLAGE_REVERSE,
            zlevels=zlevels,
            focusing_method=focus_mtd)
        self._acq_future_connector = ProgressiveFutureConnector(
            self.acq_future, self.gauge_acq, self.lbl_acqestimate)
        # TODO: Build-up the complete image during the acquisition, so that the
        #       progress can be followed live.
        self.acq_future.add_done_callback(self.on_acquisition_done)

        self.btn_cancel.SetLabel("Cancel")
        self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel)

    def on_cancel(self, evt):
        """ Handle acquisition cancel button click """
        logging.info("Cancel button clicked, stopping acquisition")
        self.acq_future.cancel()
        self.acquiring = False
        self.btn_cancel.SetLabel("Close")
        # all the rest will be handled by on_acquisition_done()

    @call_in_wx_main
    def on_acquisition_done(self, future):
        """ Callback called when the acquisition is finished (either successfully or cancelled) """
        if self._main_data_model.opm:
            self._main_data_model.opm.setAcqQuality(path.ACQ_QUALITY_FAST)

        # bind button back to direct closure
        self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close)
        self._resume_settings()

        self.acquiring = False

        self.acq_future = None  # To avoid holding the ref in memory
        self._acq_future_connector = None

        try:
            data = future.result(1)  # timeout is just for safety
            self.conf.fn_count = update_counter(self.conf.fn_count)
        except CancelledError:
            # put back to original state:
            # re-enable the acquire button
            self.btn_secom_acquire.Enable()

            # hide progress bar (+ put pack estimated time)
            self.update_acquisition_time()
            self.gauge_acq.Hide()
            self.Layout()
            return
        except Exception:
            # We cannot do much: just warn the user and pretend it was cancelled
            logging.exception("Acquisition failed")
            self.btn_secom_acquire.Enable()
            self.lbl_acqestimate.SetLabel("Acquisition failed.")
            self.lbl_acqestimate.Parent.Layout()
            # leave the gauge, to give a hint on what went wrong.
            return

        # Now store the data (as pyramidal data), and open it again (but now it's
        # backed with the persistent storage.
        try:
            exporter = dataio.find_fittest_converter(self.filename)
            if exporter.CAN_SAVE_PYRAMID:
                exporter.export(self.filename, data, pyramid=True)
            else:
                logging.warning(
                    "File format doesn't support saving image in pyramidal form"
                )
                exporter.export(self.filename)
            self.data = exporter.open_data(self.filename).content
        except Exception:
            # We cannot do much: just warn the user and pretend it was cancelled
            logging.exception("Storage failed")
            self.btn_secom_acquire.Enable()
            self.lbl_acqestimate.SetLabel("Storage failed.")
            self.lbl_acqestimate.Parent.Layout()
            return

        self.terminate_listeners()
        self.EndModal(wx.ID_OPEN)