Example #1
0
class IFTAnalysisSaverOptions:
    def __init__(self) -> None:
        self.bn_save_dir_parent = VariableBindable(
            None)  # type: Bindable[Optional[Path]]
        self.bn_save_dir_name = VariableBindable('')

        self.drop_residuals_figure_opts = FigureOptions(should_save=True,
                                                        dpi=300,
                                                        size_w=10,
                                                        size_h=10)

        self.ift_figure_opts = FigureOptions(should_save=True,
                                             dpi=300,
                                             size_w=15,
                                             size_h=9)

        self.volume_figure_opts = FigureOptions(should_save=True,
                                                dpi=300,
                                                size_w=15,
                                                size_h=9)

        self.surface_area_figure_opts = FigureOptions(should_save=True,
                                                      dpi=300,
                                                      size_w=15,
                                                      size_h=9)

    @property
    def save_root_dir(self) -> Path:
        return self.bn_save_dir_parent.get() / self.bn_save_dir_name.get()
Example #2
0
class LocalStorageAcquirer(ImageSequenceAcquirer):
    IS_REPLICATED = True

    def __init__(self) -> None:
        super().__init__()
        self.bn_last_loaded_paths = VariableBindable(
            tuple())  # type: VariableBindable[Sequence[Path]]

    def load_image_paths(self, image_paths: Sequence[Union[Path,
                                                           str]]) -> None:
        # Sort image paths in lexicographic order, and ignore paths to directories.
        image_paths = sorted(
            [p for p in map(Path, image_paths) if not p.is_dir()])

        images: MutableSequence[np.ndarray] = []
        for image_path in image_paths:
            # Load in grayscale to save memory.
            image = cv2.imread(str(image_path), cv2.IMREAD_GRAYSCALE)
            if image is None:
                raise ValueError(f"Failed to load image from '{image_path}'")

            image.flags.writeable = False

            images.append(image)

        self.bn_images.set(images)
        self.bn_last_loaded_paths.set(tuple(image_paths))
Example #3
0
    def __init__(
        self, *, image_acquisition: ImageAcquisitionModel,
        feature_extractor_params: FeatureExtractorParams,
        do_extract_features: Callable[[Bindable[np.ndarray]], FeatureExtractor]
    ) -> None:
        self._image_acquisition = image_acquisition
        self._feature_extractor_params = feature_extractor_params
        self._do_extract_features = do_extract_features

        self._watchers = 0

        self._acquirer_controller = None  # type: Optional[AcquirerController]

        self.bn_acquirer_controller = AccessorBindable(
            getter=lambda: self._acquirer_controller)

        self.bn_source_image = VariableBindable(
            None)  # type: Bindable[Optional[np.ndarray]]
        self.bn_foreground_detection = VariableBindable(
            None)  # type: Bindable[Optional[np.ndarray]]
        self.bn_drop_profile = VariableBindable(
            None)  # type: Bindable[Optional[np.ndarray]]

        self._image_acquisition.bn_acquirer.on_changed.connect(
            self._update_acquirer_controller, )
Example #4
0
    def __init__(self) -> None:
        self.bn_save_dir_parent = VariableBindable(None)  # type: Bindable[Optional[Path]]
        self.bn_save_dir_name = VariableBindable('')

        self.drop_residuals_figure_opts = FigureOptions(
            should_save=False,
            dpi=300,
            size_w=10,
            size_h=10
        )

        self.ift_figure_opts = FigureOptions(
            should_save=False,
            dpi=300,
            size_w=15,
            size_h=9
        )

        self.volume_figure_opts = FigureOptions(
            should_save=False,
            dpi=300,
            size_w=15,
            size_h=9
        )

        self.surface_area_figure_opts = FigureOptions(
            should_save=False,
            dpi=300,
            size_w=15,
            size_h=9
        )
Example #5
0
class ConanSaveParamsFactory:
    def __init__(self) -> None:
        self.bn_save_dir_parent = VariableBindable(None)
        self.bn_save_dir_name = VariableBindable('')

        self.angle_figure_opts = FigureOptions(
            should_save=False,
            dpi=300,
            size_w=15,
            size_h=9
        )

    @property
    def save_root_dir(self) -> Path:
        return self.bn_save_dir_parent.get() / self.bn_save_dir_name.get()

    def create(self) -> ConanSaveParams:
        parent_dir = self.bn_save_dir_parent.get()
        assert parent_dir is not None

        name = self.bn_save_dir_name.get()
        assert name

        root_dir = parent_dir / name

        return ConanSaveParams(
            root_dir,
            copy.deepcopy(self.angle_figure_opts),
        )
Example #6
0
class LocalStorageAcquirer(ImageSequenceAcquirer):
    IS_REPLICATED = True

    def __init__(self) -> None:
        super().__init__()
        self.bn_last_loaded_paths = VariableBindable(tuple())  # type: VariableBindable[Sequence[Path]]

    def load_image_paths(self, image_paths: Sequence[Union[Path, str]]) -> None:
        # Sort image paths in lexicographic order, and ignore paths to directories.
        image_paths = sorted([p for p in map(Path, image_paths) if not p.is_dir()])

        images = []  # type: MutableSequence[np.ndarray]
        for image_path in image_paths:
            image = cv2.imread(str(image_path))
            if image is None:
                raise ValueError(
                    "Failed to load image from path '{}'"
                    .format(image_path)
                )

            # OpenCV loads images in BGR mode, but the rest of the app works with images in RGB, so convert the read
            # image appropriately.
            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

            images.append(image)

        self.bn_images.set(images)
        self.bn_last_loaded_paths.set(tuple(image_paths))
Example #7
0
    def __init__(self, do_exit: Callable[[], Any], *,
                 loop: asyncio.AbstractEventLoop) -> None:
        self._loop = loop

        self._do_exit = do_exit

        self._feature_extractor_params = FeatureExtractorParams()
        self._physprops_calculator_params = PhysicalPropertiesCalculatorParams(
        )

        self._bn_analyses = VariableBindable(
            tuple())  # type: Bindable[Sequence[IFTDropAnalysis]]
        self._analyses_saved = False

        self.image_acquisition = ImageAcquisitionModel()
        self.image_acquisition.use_acquirer_type(AcquirerType.LOCAL_STORAGE)

        self.physical_parameters = PhysicalParametersModel(
            physprops_calculator_params=self._physprops_calculator_params, )

        self.image_processing = IFTImageProcessingModel(
            image_acquisition=self.image_acquisition,
            feature_extractor_params=self._feature_extractor_params,
            do_extract_features=self.extract_features,
        )

        self.results = IFTResultsModel(
            in_analyses=self._bn_analyses,
            do_cancel_analyses=self.cancel_analyses,
            do_save_analyses=self.save_analyses,
            create_save_options=self._create_save_options,
            check_if_safe_to_discard=self.check_if_safe_to_discard_analyses,
        )
    def __init__(
            self,
            features: FeatureExtractor,
            young_laplace_fit: YoungLaplaceFitter,
            params: PhysicalPropertiesCalculatorParams,
    ) -> None:
        self._extracted_features = features
        self._young_laplace_fit = young_laplace_fit

        self.params = params

        self.bn_interfacial_tension = VariableBindable(math.nan)
        self.bn_volume = VariableBindable(math.nan)
        self.bn_surface_area = VariableBindable(math.nan)
        self.bn_apex_radius = VariableBindable(math.nan)
        self.bn_worthington = VariableBindable(math.nan)

        features.bn_needle_width_px.on_changed.connect(self._recalculate)

        # Assume that bond number changes at the same time as other attributes
        young_laplace_fit.bn_bond_number.on_changed.connect(self._recalculate)

        params.bn_inner_density.on_changed.connect(self._recalculate)
        params.bn_outer_density.on_changed.connect(self._recalculate)
        params.bn_needle_width.on_changed.connect(self._recalculate)
        params.bn_gravity.on_changed.connect(self._recalculate)

        self._recalculate()
Example #9
0
    def __init__(self) -> None:
        self.bn_save_dir_parent = VariableBindable(
            None)  # type: Bindable[Optional[Path]]
        self.bn_save_dir_name = VariableBindable('')

        self.angle_figure_opts = FigureOptions(should_save=True,
                                               dpi=300,
                                               size_w=15,
                                               size_h=9)
Example #10
0
class CameraAcquirer(ImageAcquirer):
    def __init__(self) -> None:
        self._loop = asyncio.get_event_loop()

        self.bn_camera = VariableBindable(None)  # type: Bindable[Optional[Camera]]

        self.bn_num_frames = VariableBindable(1)
        self.bn_frame_interval = VariableBindable(None)  # type: Bindable[Optional[float]]

    def acquire_images(self) -> Sequence[InputImage]:
        camera = self.bn_camera.get()

        if camera is None:
            raise ValueError("'camera' can't be None")

        num_frames = self.bn_num_frames.get()

        if num_frames is None or num_frames <= 0:
            raise ValueError(
                "'num_frames' must be > 0 and not None, currently: '{}'"
                .format(num_frames)
            )

        frame_interval = self.bn_frame_interval.get()

        if frame_interval is None or frame_interval <= 0:
            if num_frames == 1:
                frame_interval = 0
            else:
                raise ValueError(
                    "'frame_interval' must be > 0 and not None, currently: '{}'"
                    .format(frame_interval)
                )

        input_images = []

        for i in range(num_frames):
            capture_delay = i * frame_interval

            input_image = _BaseCameraInputImage(
                camera=camera,
                delay=capture_delay,
                first_image=input_images[0] if input_images else None,
                loop=self._loop,
            )

            input_images.append(input_image)

        return input_images

    def get_image_size_hint(self) -> Optional[Tuple[int, int]]:
        camera = self.bn_camera.get()
        if camera is None:
            return

        return camera.get_image_size_hint()
Example #11
0
    def __init__(self) -> None:
        self.bn_save_dir_parent = VariableBindable(None)
        self.bn_save_dir_name = VariableBindable('')

        self.angle_figure_opts = FigureOptions(
            should_save=False,
            dpi=300,
            size_w=15,
            size_h=9
        )
Example #12
0
    def __init__(self, hacquirer: harvesters.ImageAcquirer) -> None:
        self._hacquirer = hacquirer
        self.bn_alive = VariableBindable(False)

        try:
            hacquirer.start_acquisition(run_in_background=True)
        except genicam.gentl.IoException as e:
            raise ValueError('Camera failed to open.') from e

        self.bn_alive.set(True)
Example #13
0
class FigureOptions:
    def __init__(self, should_save: bool, dpi: int, size_w: float,
                 size_h: float) -> None:
        self.bn_should_save = VariableBindable(should_save)
        self.bn_dpi = VariableBindable(dpi)
        self.bn_size_w = VariableBindable(size_w)
        self.bn_size_h = VariableBindable(size_h)

    @property
    def size(self) -> Tuple[float, float]:
        return self.bn_size_w.get(), self.bn_size_h.get()
Example #14
0
    def __init__(self, camera_index: int) -> None:
        self._vc = cv2.VideoCapture(camera_index)

        self.bn_alive = VariableBindable(True)

        if not self.check_vc_works(timeout=5):
            raise ValueError('Camera failed to open.')

        # For some reason, on some cameras, the first few images captured will be dark. Consume those images now so the
        # camera will be "fully operational" after initialisation.
        for i in range(self._PRECAPTURE):
            self._vc.read()
Example #15
0
class USBCamera(Camera):
    _PRECAPTURE = 5
    _CAPTURE_TIMEOUT = 0.5

    def __init__(self, camera_index: int) -> None:
        self._vc = cv2.VideoCapture(camera_index)

        self.bn_alive = VariableBindable(True)

        if not self.check_vc_works(timeout=5):
            raise ValueError('Camera failed to open.')

        # For some reason, on some cameras, the first few images captured will be dark. Consume those images now so the
        # camera will be "fully operational" after initialisation.
        for i in range(self._PRECAPTURE):
            self._vc.read()

    def check_vc_works(self, timeout: float) -> bool:
        start_time = time.time()
        while self._vc.isOpened() and (time.time() - start_time) < timeout:
            success = self._vc.grab()
            if success:
                # Camera still works
                return True
        else:
            return False

    def capture(self) -> np.ndarray:
        start_time = time.time()
        while self._vc.isOpened() and (time.time() -
                                       start_time) < self._CAPTURE_TIMEOUT:
            success, image = self._vc.read()

            if success:
                return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

        self.release()
        raise CameraCaptureError

    def get_image_size_hint(self) -> Optional[Tuple[int, int]]:
        width = self._vc.get(cv2.CAP_PROP_FRAME_WIDTH)
        height = self._vc.get(cv2.CAP_PROP_FRAME_HEIGHT)
        return width, height

    def release_if_not_working(self, timeout=_CAPTURE_TIMEOUT) -> None:
        if not self.check_vc_works(timeout):
            self.release()

    def release(self) -> None:
        self._vc.release()
        self.bn_alive.set(False)
Example #16
0
class TestVariableBindable:
    def setup(self):
        self.initial = object()
        self.bindable = VariableBindable(self.initial)

    def test_initial_value(self):
        assert self.bindable.get() == self.initial

    def test_set_and_get(self):
        new_value = object()

        self.bindable.set(new_value)

        assert self.bindable.get() == new_value
Example #17
0
class ConanAnalysisSaverOptions:
    def __init__(self) -> None:
        self.bn_save_dir_parent = VariableBindable(
            None)  # type: Bindable[Optional[Path]]
        self.bn_save_dir_name = VariableBindable('')

        self.angle_figure_opts = FigureOptions(should_save=True,
                                               dpi=300,
                                               size_w=15,
                                               size_h=9)

    @property
    def save_root_dir(self) -> Path:
        return self.bn_save_dir_parent.get() / self.bn_save_dir_name.get()
Example #18
0
    def __init__(self, in_analyses: Bindable[Sequence[ConanAnalysis]]) -> None:
        self._bn_analyses = in_analyses

        self._tracked_analyses = []
        self._tracked_analysis_unbind_tasks = {}

        self.bn_left_angle_data = VariableBindable((tuple(), tuple()))
        self.bn_right_angle_data = VariableBindable((tuple(), tuple()))

        self._bn_analyses.on_changed.connect(
            self._hdl_analyses_changed
        )

        self._hdl_analyses_changed()
    def __init__(
        self,
        *,
        image_acquisition: ImageAcquisitionService,
        params_factory: ConanParamsFactory,
        features_service: ConanFeaturesService,
    ) -> None:
        self._loop = asyncio.get_event_loop()

        self._image_acquisition = image_acquisition
        self._params_factory = params_factory

        self.bn_active_tool = VariableBindable(ToolID.BASELINE)

        region_clip = AccessorBindable(getter=self._get_region_clip)

        self.baseline_plugin = DefineLinePluginModel(
            in_line=GObjectPropertyBindable(params_factory, 'baseline'),
            in_clip=region_clip,
        )

        self.roi_plugin = DefineRegionPluginModel(
            in_region=GObjectPropertyBindable(params_factory, 'roi'),
            in_clip=region_clip,
        )

        self.thresh_plugin = ConanThreshPluginModel(
            params_factory=params_factory)

        self.preview_plugin = ConanPreviewPluginModel(
            image_acquisition=image_acquisition,
            params_factory=params_factory,
            features_service=features_service,
        )
Example #20
0
    def __init__(
        self, *, image_acquisition: ImageAcquisitionModel,
        feature_extractor_params: FeatureExtractorParams,
        conancalc_params: ContactAngleCalculatorParams,
        do_extract_features: Callable[
            [Bindable[np.ndarray], FeatureExtractorParams], FeatureExtractor]
    ) -> None:
        self._image_acquisition = image_acquisition
        self._feature_extractor_params = feature_extractor_params
        self._conancalc_params = conancalc_params
        self._do_extract_features = do_extract_features

        self.bn_active_tool = VariableBindable(ToolID.DROP_REGION)

        region_clip = AccessorBindable(getter=self._get_region_clip)

        self.drop_region_plugin = DefineRegionPluginModel(
            in_region=self._feature_extractor_params.bn_drop_region_px,
            in_clip=region_clip,
        )

        self.surface_plugin = DefineLinePluginModel(
            in_line=self._conancalc_params.bn_surface_line_px,
            in_clip=region_clip,
        )

        self.foreground_detection_plugin = ForegroundDetectionPluginModel(
            feature_extractor_params=feature_extractor_params, )

        self.preview_plugin = ConanPreviewPluginModel(
            image_acquisition=image_acquisition,
            feature_extractor_params=feature_extractor_params,
            do_extract_features=do_extract_features,
        )
Example #21
0
    def _start_fit(self, image: np.ndarray, image_timestamp: float) -> None:
        assert self._image is None

        self._image = image
        self._image_timestamp = image_timestamp

        # Set given image to be readonly to prevent introducing some accidental bugs.
        self._image.flags.writeable = False

        extracted_features = self._do_extract_features(
            VariableBindable(self._image))
        young_laplace_fit = self._do_young_laplace_fit(extracted_features)
        physical_properties = self._do_calculate_physprops(
            extracted_features, young_laplace_fit)

        self._extracted_features = extracted_features
        self._young_laplace_fit = young_laplace_fit
        self._physical_properties = physical_properties

        self._bind_fit()

        self.bn_image.poke()
        self.bn_image_timestamp.poke()

        young_laplace_fit.bn_is_busy.on_changed.connect(
            self._hdl_young_laplace_fit_is_busy_changed)

        self.bn_status.set(self.Status.FITTING)
    def __init__(
        self,
        *,
        image_acquisition: ImageAcquisitionService,
        features_params_factory: PendantFeaturesParamsFactory,
        features_service: PendantFeaturesService,
    ) -> None:
        self._image_acquisition = image_acquisition

        self.bn_active_tool = VariableBindable(ToolID.DROP_REGION)

        region_clip = AccessorBindable(getter=self._get_region_clip)

        self.drop_region_plugin = DefineRegionPluginModel(
            in_region=GObjectPropertyBindable(features_params_factory,
                                              'drop-region'),
            in_clip=region_clip,
        )

        self.needle_region_plugin = DefineRegionPluginModel(
            in_region=GObjectPropertyBindable(features_params_factory,
                                              'needle-region'),
            in_clip=region_clip,
        )

        self.preview_plugin = IFTPreviewPluginModel(
            image_acquisition=image_acquisition,
            features_params_factory=features_params_factory,
            features_service=features_service,
        )
Example #23
0
    def __init__(
            self, *,
            image_acquisition: ImageAcquisitionModel,
            feature_extractor_params: FeatureExtractorParams,
            do_extract_features: Callable[[Bindable[np.ndarray], FeatureExtractorParams], FeatureExtractor]
    ) -> None:
        self._image_acquisition = image_acquisition
        self._feature_extractor_params = feature_extractor_params
        self._do_extract_features = do_extract_features

        self.bn_active_tool = VariableBindable(ToolID.DROP_REGION)

        region_clip = AccessorBindable(
            getter=self._get_region_clip
        )

        self.drop_region_plugin = DefineRegionPluginModel(
            in_region=self._feature_extractor_params.bn_drop_region_px,
            in_clip=region_clip,
        )

        self.needle_region_plugin = DefineRegionPluginModel(
            in_region=self._feature_extractor_params.bn_needle_region_px,
            in_clip=region_clip,
        )

        self.edge_detection_plugin = EdgeDetectionPluginModel(
            feature_extractor_params=feature_extractor_params,
        )

        self.preview_plugin = IFTPreviewPluginModel(
            image_acquisition=image_acquisition,
            feature_extractor_params=feature_extractor_params,
            do_extract_features=do_extract_features,
        )
Example #24
0
    def __init__(self, *, loop: asyncio.AbstractEventLoop) -> None:
        self._loop = loop

        self.bn_mode = VariableBindable(AppMode.MAIN_MENU)

        self.main_menu = MainMenuModel(
            do_launch_ift=(
                lambda: self.bn_mode.set(AppMode.IFT)
            ),
            do_launch_conan=(
                lambda: self.bn_mode.set(AppMode.CONAN)
            ),
            do_exit=(
                lambda: self.bn_mode.set(AppMode.QUIT)
            ),
        )
Example #25
0
    def __init__(
            self, *,
            image_acquisition: ImageAcquisitionService,
            features_params_factory: PendantFeaturesParamsFactory,
            features_service: PendantFeaturesService,
    ) -> None:
        self._image_acquisition = image_acquisition
        self._features_params_factory = features_params_factory
        self._features_service = features_service

        self._watchers = 0

        self._acquirer_controller = None  # type: Optional[AcquirerController]

        self.bn_acquirer_controller = AccessorBindable(
            getter=lambda: self._acquirer_controller
        )

        self.bn_source_image = VariableBindable(None)  # type: Bindable[Optional[np.ndarray]]
        self.bn_labels = VariableBindable(None)  # type: Bindable[Optional[np.ndarray]]
        self.bn_drop_points = VariableBindable(None)  # type: Bindable[Optional[np.ndarray]]
        self.bn_needle_rect = VariableBindable(None)

        self._image_acquisition.bn_acquirer.on_changed.connect(
            self._update_acquirer_controller,
        )
Example #26
0
class ImageSequenceAcquirer(ImageAcquirer):
    IS_REPLICATED = False

    def __init__(self) -> None:
        self.bn_images = VariableBindable(
            tuple())  # type: Bindable[Sequence[np.ndarray]]

        self.bn_frame_interval = VariableBindable(
            None)  # type: Bindable[Optional[int]]

    def acquire_images(self) -> Sequence[InputImage]:
        images = self.bn_images.get()
        if len(images) == 0:
            raise ValueError("'_images' can't be empty")

        frame_interval = self.bn_frame_interval.get()
        if frame_interval is None or frame_interval <= 0:
            if len(images) == 1:
                # Since only one image, we don't care about the frame_interval.
                frame_interval = 0
            else:
                raise ValueError(
                    "'frame_interval' must be > 0 and not None, currently: '{}'"
                    .format(frame_interval))

        input_images = []

        for i, img in enumerate(images):
            input_image = _BaseImageSequenceInputImage(image=img,
                                                       timestamp=i *
                                                       frame_interval)
            input_image.is_replicated = self.IS_REPLICATED
            input_images.append(input_image)

        return input_images

    def get_image_size_hint(self) -> Optional[Tuple[int, int]]:
        images = self.bn_images.get()
        if images is None or len(images) == 0:
            return None

        first_image = images[0]
        return first_image.shape[1::-1]
Example #27
0
    def _do_init(self, model: ConanResultsModel,
                 page_controls: WizardPageControls) -> None:
        self._loop = asyncio.get_event_loop()

        self._model = model
        self._page_controls = page_controls

        self.individual_model = model.individual
        self.graphs_model = model.graphs

        self.bn_footer_status = VariableBindable(
            ResultsFooterStatus.IN_PROGRESS)
        self.bn_completion_progress = model.bn_analyses_completion_progress
        self.bn_time_elapsed = VariableBindable(math.nan)
        self.bn_time_remaining = VariableBindable(math.nan)

        self._active_save_options = None

        self.__event_connections = [
            model.bn_analyses_time_start.on_changed.connect(
                self._update_times),
            model.bn_analyses_time_est_complete.on_changed.connect(
                self._update_times),
        ]

        self._update_times()
Example #28
0
    def __init__(self) -> None:
        self._loop = asyncio.get_event_loop()

        self.bn_camera = VariableBindable(None)  # type: Bindable[Optional[Camera]]

        self.bn_num_frames = VariableBindable(1)
        self.bn_frame_interval = VariableBindable(None)  # type: Bindable[Optional[float]]
Example #29
0
    def _do_init(
        self,
        in_status: Bindable[ResultsFooterStatus],
        in_progress: Bindable[float],
        in_time_elapsed: Bindable[float],
        in_time_remaining: Bindable[float],
        do_back: Callable[[], Any],
        do_cancel: Callable[[], Any],
        do_save: Callable[[], Any],
    ) -> None:
        self._bn_status = in_status
        self._bn_time_remaining = in_time_remaining
        self._do_back = do_back
        self._do_cancel = do_cancel
        self._do_save = do_save

        self.bn_progress = in_progress
        self.bn_progress_label = VariableBindable('')
        self.bn_time_elapsed = in_time_elapsed
        self.bn_time_remaining = VariableBindable(math.nan)

        self.__event_connections = []
Example #30
0
    def __init__(
        self,
        *,
        image_acquisition: ImageAcquisitionService,
        params_factory: ConanParamsFactory,
        features_service: ConanFeaturesService,
    ) -> None:
        self._image_acquisition = image_acquisition
        self._params_factory = params_factory
        self._features_service = features_service

        self._watchers = 0

        self._acquirer_controller = None  # type: Optional[AcquirerController]

        self.bn_acquirer_controller = AccessorBindable(
            getter=lambda: self._acquirer_controller)

        self.bn_source_image = VariableBindable(None)
        self.bn_features = VariableBindable(None)

        self._image_acquisition.bn_acquirer.on_changed.connect(
            self._update_acquirer_controller, )