예제 #1
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))
예제 #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 = []  # 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))
예제 #3
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)
예제 #4
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
예제 #5
0
class WizardController:
    def __init__(self, pages: Iterable) -> None:
        self._pages = tuple(pages)

        self.bn_current_page = VariableBindable(self._pages[0])

    def next_page(self) -> None:
        current_page = self.bn_current_page.get()
        next_page_idx = self._pages.index(current_page) + 1
        next_page = self._pages[next_page_idx]

        self.bn_current_page.set(next_page)

    def prev_page(self) -> None:
        current_page = self.bn_current_page.get()
        prev_page_idx = self._pages.index(current_page) - 1
        prev_page = self._pages[prev_page_idx]

        self.bn_current_page.set(prev_page)
예제 #6
0
class WizardModel:
    def __init__(self, pages: Iterable[Any]) -> None:
        self._pages = tuple(pages)

        self._interpage_actions = {}

        self.bn_current_page = VariableBindable(self._pages[0])

    def next_page(self) -> None:
        current_page = self.bn_current_page.get()
        next_page_idx = self._pages.index(current_page) + 1
        next_page = self._pages[next_page_idx]

        self.perform_interpage_action(current_page, next_page)

        self.bn_current_page.set(next_page)

    def prev_page(self) -> None:
        current_page = self.bn_current_page.get()
        prev_page_idx = self._pages.index(current_page) - 1
        prev_page = self._pages[prev_page_idx]

        self.perform_interpage_action(current_page, prev_page)

        self.bn_current_page.set(prev_page)

    def perform_interpage_action(self, start_page: Any, end_page: Any) -> None:
        if (start_page, end_page) not in self._interpage_actions:
            return

        callback = self._interpage_actions[(start_page, end_page)]

        callback()

    def register_interpage_action(self, start_page: Any, end_page: Any,
                                  callback: Callable[[], Any]) -> None:
        self._interpage_actions[(start_page, end_page)] = callback
예제 #7
0
class AppRootModel:
    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)
            ),
        )

    def new_ift_session(self) -> IFTSession:
        session = IFTSession(
            do_exit=(
                lambda: self.bn_mode.set(AppMode.MAIN_MENU)
            ),
            loop=self._loop,
        )

        return session

    def new_conan_session(self) -> ConanSession:
        session = ConanSession(
            do_exit=(
                lambda: self.bn_mode.set(AppMode.MAIN_MENU)
            ),
            loop=self._loop,
        )

        return session
예제 #8
0
class PhysicalPropertiesCalculator:
    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()

    def _recalculate(self) -> None:
        m_per_px = self._get_m_per_px()

        inner_density = self.params.bn_inner_density.get()
        outer_density = self.params.bn_outer_density.get()
        needle_width_m = self.params.bn_needle_width.get()
        gravity = self.params.bn_gravity.get()

        if inner_density is None or outer_density is None or needle_width_m is None or gravity is None:
            return

        bond_number = self._young_laplace_fit.bn_bond_number.get()
        apex_radius_px = self._young_laplace_fit.bn_apex_radius.get()

        apex_radius_m = m_per_px * apex_radius_px

        interfacial_tension = calculate_ift(
            inner_density=inner_density,
            outer_density=outer_density,
            bond_number=bond_number,
            apex_radius=apex_radius_m,
            gravity=gravity
        )

        volume_px3 = self._young_laplace_fit.bn_volume.get()
        volume_m3 = m_per_px**3 * volume_px3

        surface_area_px2 = self._young_laplace_fit.bn_surface_area.get()
        surface_area_m2 = m_per_px**2 * surface_area_px2

        worthington = calculate_worthington(
            inner_density=inner_density,
            outer_density=outer_density,
            gravity=gravity,
            ift=interfacial_tension,
            volume=volume_m3,
            needle_width=needle_width_m,
        )

        self.bn_interfacial_tension.set(interfacial_tension)
        self.bn_volume.set(volume_m3)
        self.bn_surface_area.set(surface_area_m2)
        self.bn_apex_radius.set(apex_radius_m)
        self.bn_worthington.set(worthington)

    def _get_m_per_px(self) -> float:
        needle_width_px = self._extracted_features.bn_needle_width_px.get()
        needle_width_m = self.params.bn_needle_width.get()

        if needle_width_px is None or needle_width_m is None:
            return math.nan

        return needle_width_m/needle_width_px
예제 #9
0
class IFTSession:
    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 start_analyses(self) -> None:
        assert len(self._bn_analyses.get()) == 0

        new_analyses = []

        input_images = self.image_acquisition.acquire_images()
        for input_image in input_images:
            new_analysis = IFTDropAnalysis(
                input_image=input_image,
                do_extract_features=self.extract_features,
                do_young_laplace_fit=self.young_laplace_fit,
                do_calculate_physprops=self.calculate_physprops)

            new_analyses.append(new_analysis)

        self._bn_analyses.set(new_analyses)
        self._analyses_saved = False

    def cancel_analyses(self) -> None:
        analyses = self._bn_analyses.get()

        for analysis in analyses:
            analysis.cancel()

    def clear_analyses(self) -> None:
        self.cancel_analyses()
        self._bn_analyses.set(tuple())

    def save_analyses(self, options: IFTAnalysisSaverOptions) -> None:
        analyses = self._bn_analyses.get()
        if len(analyses) == 0:
            return

        save_drops(analyses, options)
        self._analyses_saved = True

    def _create_save_options(self) -> IFTAnalysisSaverOptions:
        return IFTAnalysisSaverOptions()

    def check_if_safe_to_discard_analyses(self) -> bool:
        if self._analyses_saved:
            return True
        else:
            analyses = self._bn_analyses.get()
            if len(analyses) == 0:
                return True

            all_images_replicated = all(analysis.is_image_replicated
                                        for analysis in analyses)

            if not all_images_replicated:
                return False

            return True

    def extract_features(self,
                         image: Bindable[np.ndarray]) -> FeatureExtractor:
        return FeatureExtractor(
            image=image,
            params=self._feature_extractor_params,
            loop=self._loop,
        )

    def young_laplace_fit(
            self, extracted_features: FeatureExtractor) -> YoungLaplaceFitter:
        return YoungLaplaceFitter(features=extracted_features, loop=self._loop)

    def calculate_physprops(
            self, extracted_features: FeatureExtractor,
            young_laplace_fit: YoungLaplaceFitter
    ) -> PhysicalPropertiesCalculator:
        return PhysicalPropertiesCalculator(
            features=extracted_features,
            young_laplace_fit=young_laplace_fit,
            params=self._physprops_calculator_params,
        )

    def exit(self) -> None:
        self.clear_analyses()
        self.image_acquisition.destroy()
        self._do_exit()
예제 #10
0
class GenicamCamera(Camera):
    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)

    def capture(self) -> np.ndarray:
        with self._hacquirer.fetch_buffer() as buf:
            if not buf.payload.components:
                raise CameraCaptureError

            component = buf.payload.components[0]

            width, height, channels = component.width, component.height, int(
                component.num_components_per_pixel)
            x_padding = component.x_padding

            data = component.data
            data = np.lib.stride_tricks.as_strided(
                data,
                shape=(height, width * channels),
                strides=((width * channels + x_padding) * data.itemsize,
                         data.itemsize),
                writeable=False,
            )

            data_format = component.data_format

            if data_format == 'Mono8':
                image = cv2.cvtColor(data, cv2.COLOR_GRAY2RGB)
            elif data_format == 'Mono10':
                image = cv2.cvtColor(data, cv2.COLOR_GRAY2RGB)
                image = (image / 1023 * 255).astype(np.uint8)
            elif data_format == 'Mono12':
                image = cv2.cvtColor(data, cv2.COLOR_GRAY2RGB)
                image = (image / 4095 * 255).astype(np.uint8)
            elif data_format == 'RGB8':
                image = data.reshape(height, width, 3).copy()
            elif data_format == 'RGB10':
                image = data.reshape(height, width, 3)
                image = (image / 1023 * 255).astype(np.uint8)
            elif data_format == 'RGB12':
                image = data.reshape(height, width, 3)
                image = (image / 4095 * 255).astype(np.uint8)
            elif data_format == 'BGR8':
                image = cv2.cvtColor(
                    data.reshape(height, width, 3),
                    code=cv2.COLOR_BGR2RGB,
                )
            elif data_format == 'BGR10':
                image = cv2.cvtColor(
                    data.reshape(height, width, 3),
                    code=cv2.COLOR_BGR2RGB,
                )
                image = (image / 1023 * 255).astype(np.uint8)
            elif data_format == 'BGR12':
                image = cv2.cvtColor(
                    data.reshape(height, width, 3),
                    code=cv2.COLOR_BGR2RGB,
                )
                image = (image / 4095 * 255).astype(np.uint8)
            elif data_format in {
                    'BayerGR8', 'BayerRG8', 'BayerBG8', 'BayerGB8'
            }:
                image = cv2.cvtColor(
                    data,
                    # OpenCV has a different Bayer pattern naming convention.
                    code={
                        'BayerGR8': cv2.COLOR_BayerGB2RGB,
                        'BayerRG8': cv2.COLOR_BayerBG2RGB,
                        'BayerBG8': cv2.COLOR_BayerRG2RGB,
                        'BayerGB8': cv2.COLOR_BayerGR2RGB,
                    }[data_format])
            elif data_format in {
                    'BayerGR10', 'BayerRG10', 'BayerBG10', 'BayerGB10'
            }:
                image = cv2.cvtColor(
                    data,
                    # OpenCV has a different Bayer pattern naming convention.
                    code={
                        'BayerGR10': cv2.COLOR_BayerGB2RGB,
                        'BayerRG10': cv2.COLOR_BayerBG2RGB,
                        'BayerBG10': cv2.COLOR_BayerRG2RGB,
                        'BayerGB10': cv2.COLOR_BayerGR2RGB,
                    }[data_format])
                image = (image / 1023 * 255).astype(np.uint8)
            elif data_format in {
                    'BayerGR12', 'BayerRG12', 'BayerBG12', 'BayerGB12'
            }:
                image = cv2.cvtColor(
                    data,
                    # OpenCV has a different Bayer pattern naming convention.
                    code={
                        'BayerGR12': cv2.COLOR_BayerGB2RGB,
                        'BayerRG12': cv2.COLOR_BayerBG2RGB,
                        'BayerBG12': cv2.COLOR_BayerRG2RGB,
                        'BayerGB12': cv2.COLOR_BayerGR2RGB,
                    }[data_format])
                image = (image / 4095 * 255).astype(np.uint8)
            else:
                raise CameraCaptureError(
                    'Unsupported pixel format {}'.format(data_format))

            return image

    def get_image_size_hint(self) -> Optional[Tuple[int, int]]:
        if not hasattr(self, '_hacquirer'): return

        width = self._hacquirer.remote_device.node_map.Width.value
        height = self._hacquirer.remote_device.node_map.Height.value
        return width, height

    def destroy(self) -> None:
        if not hasattr(self, '_hacquirer'): return
        self._hacquirer.stop_acquisition()
        self._hacquirer.destroy()
        del self._hacquirer
        self.bn_alive.set(False)
예제 #11
0
class IFTPreviewPluginModel:
    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,
        )

    def watch(self) -> None:
        self._watchers += 1
        self._update_acquirer_controller()

    def unwatch(self) -> None:
        self._watchers -= 1
        self._update_acquirer_controller()

    def _update_acquirer_controller(self) -> None:
        self._destroy_acquirer_controller()

        if self._watchers <= 0:
            return

        new_acquirer = self._image_acquisition.bn_acquirer.get()

        if isinstance(new_acquirer, ImageSequenceAcquirer):
            new_acquirer_controller = IFTImageSequenceAcquirerController(
                acquirer=new_acquirer,
                features_params_factory=self._features_params_factory,
                features_service=self._features_service,
                out_image=self.bn_source_image,
                show_features=self._show_features,
            )
        elif isinstance(new_acquirer, CameraAcquirer):
            new_acquirer_controller = IFTCameraAcquirerController(
                acquirer=new_acquirer,
                features_params_factory=self._features_params_factory,
                features_service=self._features_service,
                out_image=self.bn_source_image,
                show_features=self._show_features,
            )
        elif new_acquirer is None:
            new_acquirer_controller = None
        else:
            raise ValueError(
                "Unknown acquirer '{}'"
                .format(new_acquirer)
            )

        self._acquirer_controller = new_acquirer_controller
        self.bn_acquirer_controller.poke()

    def _show_features(self, features: Optional[PendantFeatures]) -> None:
        if features is None:
            self.bn_labels.set(None)
            self.bn_drop_points.set(None)
            self.bn_needle_rect.set(None)
            return

        self.bn_labels.set(features.labels)
        self.bn_drop_points.set(features.drop_points)
        self.bn_needle_rect.set(features.needle_rect)

    def _destroy_acquirer_controller(self) -> None:
        acquirer_controller = self._acquirer_controller
        if acquirer_controller is None:
            return

        acquirer_controller.destroy()

        self._acquirer_controller = None
예제 #12
0
class DetailPresenter(Presenter['DetailView']):
    def _do_init(self,
                 in_analysis: Bindable[Optional[IFTDropAnalysis]]) -> None:
        self._bn_analysis = in_analysis
        self._analysis_unbind_tasks = []

        self.bn_interfacial_tension = VariableBindable(math.nan)
        self.bn_volume = VariableBindable(math.nan)
        self.bn_surface_area = VariableBindable(math.nan)
        self.bn_worthington = VariableBindable(math.nan)
        self.bn_bond_number = VariableBindable(math.nan)
        self.bn_apex_coords = VariableBindable((math.nan, math.nan))
        self.bn_image_angle = VariableBindable(math.nan)

        self.bn_drop_image = VariableBindable(None)
        self.bn_drop_profile_extract = VariableBindable(None)
        self.bn_drop_profile_fit = VariableBindable(None)

        self.bn_residuals = VariableBindable(None)

        self.bn_log_text = VariableBindable('')

        self.__event_connections = []

    def view_ready(self) -> None:
        self.__event_connections.extend(
            [self._bn_analysis.on_changed.connect(self._hdl_analysis_changed)])

        self._hdl_analysis_changed()

    def _hdl_analysis_changed(self) -> None:
        self._unbind_analysis()

        analysis = self._bn_analysis.get()
        if analysis is None:
            self.view.show_waiting_placeholder()
            return

        self.view.hide_waiting_placeholder()

        self._bind_analysis(analysis)

    def _bind_analysis(self, analysis: IFTDropAnalysis) -> None:
        assert len(self._analysis_unbind_tasks) == 0

        data_bindings = [
            analysis.bn_interfacial_tension.bind_to(
                self.bn_interfacial_tension),
            analysis.bn_volume.bind_to(self.bn_volume),
            analysis.bn_surface_area.bind_to(self.bn_surface_area),
            analysis.bn_worthington.bind_to(self.bn_worthington),
            analysis.bn_bond_number.bind_to(self.bn_bond_number),
            analysis.bn_apex_coords_px.bind_to(self.bn_apex_coords),
            analysis.bn_rotation.bind_to(self.bn_image_angle),
            analysis.bn_residuals.bind_to(self.bn_residuals),
            analysis.bn_log.bind_to(self.bn_log_text),
        ]

        self._analysis_unbind_tasks.extend(db.unbind for db in data_bindings)

        event_connections = [
            analysis.bn_image.on_changed.connect(
                self._hdl_analysis_image_changed),
            analysis.bn_drop_profile_extract.on_changed.connect(
                self._hdl_analysis_drop_profile_extract_changed),
            analysis.bn_drop_profile_fit.on_changed.connect(
                self._hdl_analysis_drop_profile_fit_changed),
        ]

        self._analysis_unbind_tasks.extend(ec.disconnect
                                           for ec in event_connections)

        self._hdl_analysis_image_changed()
        self._hdl_analysis_drop_profile_extract_changed()
        self._hdl_analysis_drop_profile_fit_changed()

    def _hdl_analysis_image_changed(self) -> None:
        analysis = self._bn_analysis.get()
        image = analysis.bn_image.get()
        if image is None:
            self.view.show_waiting_placeholder()
            return

        self.view.hide_waiting_placeholder()

        drop_region = analysis.bn_drop_region.get()
        assert drop_region is not None

        drop_image = image[drop_region.y0:drop_region.y1,
                           drop_region.x0:drop_region.x1]
        self.bn_drop_image.set(drop_image)

    def _hdl_analysis_drop_profile_extract_changed(self) -> None:
        analysis = self._bn_analysis.get()
        drop_profile_ext = analysis.bn_drop_profile_extract.get()
        if drop_profile_ext is None:
            self.bn_drop_profile_extract.set(None)
            return

        drop_region = analysis.bn_drop_region.get()
        assert drop_region is not None

        drop_profile_ext_rel_drop_image = drop_profile_ext - drop_region.position
        self.bn_drop_profile_extract.set(drop_profile_ext_rel_drop_image)

    def _hdl_analysis_drop_profile_fit_changed(self) -> None:
        analysis = self._bn_analysis.get()
        drop_profile_fit = analysis.bn_drop_profile_fit.get()
        if drop_profile_fit is None:
            self.bn_drop_profile_fit.set(None)
            return

        drop_region = analysis.bn_drop_region.get()
        assert drop_region is not None

        drop_profile_fit_rel_drop_image = drop_profile_fit - drop_region.position
        self.bn_drop_profile_fit.set(drop_profile_fit_rel_drop_image)

    def _unbind_analysis(self) -> None:
        for task in self._analysis_unbind_tasks:
            task()
        self._analysis_unbind_tasks.clear()

    def _do_destroy(self) -> None:
        for ec in self.__event_connections:
            ec.disconnect()

        self._unbind_analysis()
예제 #13
0
class ConanSession:
    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._conancalc_params = ContactAngleCalculatorParams()

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

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

        self.image_processing = ConanImageProcessingModel(
            image_acquisition=self.image_acquisition,
            feature_extractor_params=self._feature_extractor_params,
            conancalc_params=self._conancalc_params,
            do_extract_features=self.extract_features,
        )

        self.results = ConanResultsModel(
            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 start_analyses(self) -> None:
        assert len(self._bn_analyses.get()) == 0

        new_analyses = []

        input_images = self.image_acquisition.acquire_images()
        for input_image in input_images:
            new_analysis = ConanAnalysis(
                input_image=input_image,
                do_extract_features=self.extract_features,
                do_calculate_conan=self.calculate_contact_angle,
            )

            new_analyses.append(new_analysis)

        self._bn_analyses.set(new_analyses)
        self._analyses_saved = False

    def cancel_analyses(self) -> None:
        analyses = self._bn_analyses.get()

        for analysis in analyses:
            analysis.cancel()

    def clear_analyses(self) -> None:
        self.cancel_analyses()
        self._bn_analyses.set(tuple())

    def save_analyses(self, options: ConanAnalysisSaverOptions) -> None:
        analyses = self._bn_analyses.get()
        if len(analyses) == 0:
            return

        save_drops(analyses, options)
        self._analyses_saved = True

    def _create_save_options(self) -> ConanAnalysisSaverOptions:
        return ConanAnalysisSaverOptions()

    def check_if_safe_to_discard_analyses(self) -> bool:
        if self._analyses_saved:
            return True
        else:
            analyses = self._bn_analyses.get()
            if len(analyses) == 0:
                return True

            all_images_replicated = all(
                analysis.is_image_replicated
                for analysis in analyses
            )

            if not all_images_replicated:
                return False

            return True

    def extract_features(self, image: Bindable[np.ndarray]) -> FeatureExtractor:
        return FeatureExtractor(
            image=image,
            params=self._feature_extractor_params,
            loop=self._loop,
        )

    def calculate_contact_angle(self, extracted_features: FeatureExtractor) -> ContactAngleCalculator:
        return ContactAngleCalculator(
            features=extracted_features,
            params=self._conancalc_params,
        )

    def exit(self) -> None:
        self.clear_analyses()
        self.image_acquisition.destroy()
        self._do_exit()
예제 #14
0
class ConanResultsPresenter(Presenter['ConanResultsView']):
    UPDATE_TIME_INTERVAL = 1

    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()

    def view_ready(self) -> None:
        self.__event_connections.extend([
            self._model.bn_fitting_status.on_changed.connect(
                self._hdl_model_fitting_status_changed),
            self._model.bn_analyses.on_changed.connect(
                self._hdl_model_analyses_changed)
        ])

        self._hdl_model_fitting_status_changed()
        self._hdl_model_analyses_changed()

    def _hdl_model_analyses_changed(self) -> None:
        analyses = self._model.bn_analyses.get()

        if len(analyses) == 1:
            self.view.hide_graphs()
        else:
            self.view.show_graphs()

    def _hdl_model_fitting_status_changed(self) -> None:
        fitting_status = self._model.bn_fitting_status.get()

        if fitting_status is ConanResultsModel.Status.FITTING:
            footer_status = ResultsFooterStatus.IN_PROGRESS
        elif fitting_status is ConanResultsModel.Status.FINISHED:
            footer_status = ResultsFooterStatus.FINISHED
            self.view.hide_confirm_cancel_dialog()
        elif fitting_status is ConanResultsModel.Status.CANCELLED:
            footer_status = ResultsFooterStatus.CANCELLED
            self.view.hide_confirm_cancel_dialog()
        else:
            footer_status = ResultsFooterStatus.IN_PROGRESS

        self.bn_footer_status.set(footer_status)

    _update_times_handle = None

    def _update_times(self) -> None:
        if self._update_times_handle is not None:
            self._update_times_handle.cancel()
            self._update_times_handle = None

        time_elapsed = self._model.calculate_time_elapsed()
        self.bn_time_elapsed.set(time_elapsed)

        time_remaining = self._model.calculate_time_remaining()
        self.bn_time_remaining.set(time_remaining)

        fitting_status = self._model.bn_fitting_status.get()

        if fitting_status is ConanResultsModel.Status.FITTING:
            self._update_times_handle = self._loop.call_later(
                delay=self.UPDATE_TIME_INTERVAL,
                callback=self._update_times,
            )

    def back(self) -> None:
        if self._model.is_safe_to_discard:
            self._back()
        else:
            self.view.show_confirm_discard_dialog()

    def hdl_confirm_discard_response(self, accept: bool) -> None:
        if accept:
            self._back()

        self.view.hide_confirm_discard_dialog()

    def _back(self) -> None:
        self._page_controls.prev_page()

    def cancel(self) -> None:
        if self._model.is_safe_to_discard:
            self._cancel()
        else:
            self.view.show_confirm_cancel_dialog()

    def hdl_confirm_cancel_response(self, accept: bool) -> None:
        if accept:
            self._cancel()

        self.view.hide_confirm_cancel_dialog()

    def _cancel(self):
        self._model.cancel_analyses()

    def save(self) -> None:
        if self._active_save_options is not None:
            return

        self._active_save_options = self._model.create_save_options()
        self.view.show_save_dialog(self._active_save_options)

    def hdl_save_dialog_response(self, should_save: bool) -> None:
        if self._active_save_options is None:
            return

        self.view.hide_save_dialog()

        save_options = self._active_save_options
        self._active_save_options = None

        if should_save:
            self._model.save_analyses(save_options)

    def _do_destroy(self) -> None:
        for ec in self.__event_connections:
            ec.disconnect()

        if self._update_times_handle is not None:
            self._update_times_handle.cancel()
예제 #15
0
class ContactAngleCalculator:
    def __init__(self, features: FeatureExtractor, params: ContactAngleCalculatorParams) -> None:
        self._features = features
        self.params = params

        self.bn_left_tangent = VariableBindable(np.poly1d((math.nan, math.nan)))
        self.bn_left_angle = VariableBindable(math.nan)
        self.bn_left_point = VariableBindable(Vector2(math.nan, math.nan))
        self.bn_right_tangent = VariableBindable(np.poly1d((math.nan, math.nan)))
        self.bn_right_angle = VariableBindable(math.nan)
        self.bn_right_point = VariableBindable(Vector2(math.nan, math.nan))

        # Recalculate when inputs change
        features.bn_drop_profile_px.on_changed.connect(self._recalculate)
        params.bn_surface_line_px.on_changed.connect(self._recalculate)

        self._recalculate()

    def _recalculate(self) -> None:
        features = self._features
        params = self.params

        drop_profile = features.bn_drop_profile_px.get()
        if drop_profile is None:
            return

        surface = params.bn_surface_line_px.get()
        if surface is None:
            return

        drop_profile = drop_profile.copy()
        surface_poly1d = np.poly1d((surface.gradient, surface.eval(x=0).y))

        # ContactAngle expects the coordinates of drop profile to be such that the surface has a lower y-coordinate than
        # the drop, so mirror the drop in y-direction. (Remember drop profile is in 'image coordinates', where
        # increasing y-coordinate is 'downwards')
        drop_profile[:, 1] *= -1
        surface_poly1d = -surface_poly1d

        conancalc = ContactAngle(drop_profile, surface_poly1d)

        # Mirror the tangents and contact points back to original coordinate system as well.
        left_tangent = -conancalc.left_tangent
        left_point = Vector2(x=conancalc.left_point.x, y=-conancalc.left_point.y)
        right_tangent = -conancalc.right_tangent
        right_point = Vector2(x=conancalc.right_point.x, y=-conancalc.right_point.y)

        self.bn_left_tangent.set(left_tangent)
        self.bn_left_angle.set(conancalc.left_angle)
        self.bn_left_point.set(left_point)
        self.bn_right_tangent.set(right_tangent)
        self.bn_right_angle.set(conancalc.right_angle)
        self.bn_right_point.set(right_point)
예제 #16
0
class GraphsModel:
    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 _hdl_analyses_changed(self) -> None:
        analyses = self._bn_analyses.get()
        tracked_analyses = self._tracked_analyses

        to_track = set(analyses) - set(tracked_analyses)
        for analysis in to_track:
            self._track_analysis(analysis)

        to_untrack = set(tracked_analyses) - set(analyses)
        for analysis in to_untrack:
            self._untrack_analysis(analysis)

    def _track_analysis(self, analysis: ConanAnalysis) -> None:
        unbind_tasks = []

        event_connections = [
            analysis.bn_left_angle.on_changed.connect(
                self._hdl_tracked_analysis_data_changed
            ),
            analysis.bn_right_angle.on_changed.connect(
                self._hdl_tracked_analysis_data_changed
            ),
        ]

        unbind_tasks.extend(
            ec.disconnect for ec in event_connections
        )

        self._tracked_analyses.append(analysis)
        self._tracked_analysis_unbind_tasks[analysis] = unbind_tasks

        self._hdl_tracked_analysis_data_changed()

    def _untrack_analysis(self, analysis: ConanAnalysis) -> None:
        unbind_tasks = self._tracked_analysis_unbind_tasks[analysis]
        for task in unbind_tasks:
            task()

        del self._tracked_analysis_unbind_tasks[analysis]
        self._tracked_analyses.remove(analysis)

    def _hdl_tracked_analysis_data_changed(self) -> None:
        left_angle_data = []
        right_angle_data = []

        analyses = self._bn_analyses.get()

        for analysis in analyses:
            timestamp = analysis.bn_image_timestamp.get()
            if timestamp is None or not math.isfinite(timestamp):
                continue

            left_angle_value = analysis.bn_left_angle.get()
            right_angle_value = analysis.bn_right_angle.get()

            if left_angle_value is not None and math.isfinite(left_angle_value):
                left_angle_data.append((timestamp, left_angle_value))

            if right_angle_value is not None and math.isfinite(right_angle_value):
                right_angle_data.append((timestamp, right_angle_value))

        # Sort in ascending order of timestamp
        left_angle_data = sorted(left_angle_data, key=lambda x: x[0])
        right_angle_data = sorted(right_angle_data, key=lambda x: x[0])

        left_angle_data = tuple(zip(*left_angle_data))
        right_angle_data = tuple(zip(*right_angle_data))

        if len(left_angle_data) == 0:
            left_angle_data = (tuple(), tuple())

        if len(right_angle_data) == 0:
            right_angle_data = (tuple(), tuple())

        self.bn_left_angle_data.set(left_angle_data)
        self.bn_right_angle_data.set(right_angle_data)
예제 #17
0
class ResultsFooterPresenter(Presenter['ResultsFooterView']):
    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 = []

    def view_ready(self) -> None:
        self.__event_connections.extend([
            self._bn_status.on_changed.connect(self._hdl_status_changed),
            self._bn_time_remaining.on_changed.connect(
                self._hdl_time_remaining_changed),
        ])

        self._hdl_status_changed()
        self._hdl_time_remaining_changed()

    def _hdl_status_changed(self) -> None:
        status = self._bn_status.get()

        if status is ResultsFooterStatus.IN_PROGRESS:
            self.bn_progress_label.set('')
            self.view.show_cancel_btn()
            self.view.disable_save_btn()
        elif status is ResultsFooterStatus.FINISHED:
            self.bn_progress_label.set('Finished')
            self.view.show_back_btn()
            self.view.enable_save_btn()
        elif status is ResultsFooterStatus.CANCELLED:
            self.bn_progress_label.set('Cancelled')
            self.view.show_back_btn()
            self.view.enable_save_btn()

    def _hdl_time_remaining_changed(self) -> None:
        time_remaining = self._bn_time_remaining.get()
        self.bn_time_remaining.set(time_remaining)

    def back(self) -> None:
        self._do_back()

    def cancel(self) -> None:
        self._do_cancel()

    def save(self) -> None:
        self._do_save()

    def _do_destroy(self) -> None:
        for ec in self.__event_connections:
            ec.disconnect()
예제 #18
0
class ConanPreviewPluginModel:
    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, )

    def watch(self) -> None:
        self._watchers += 1
        self._update_acquirer_controller()

    def unwatch(self) -> None:
        self._watchers -= 1
        self._update_acquirer_controller()

    def _update_acquirer_controller(self) -> None:
        self._destroy_acquirer_controller()

        if self._watchers <= 0:
            return

        new_acquirer = self._image_acquisition.bn_acquirer.get()

        if isinstance(new_acquirer, ImageSequenceAcquirer):
            new_acquirer_controller = ConanImageSequenceAcquirerController(
                acquirer=new_acquirer,
                params_factory=self._params_factory,
                features_service=self._features_service,
                source_image_out=self.bn_source_image,
                show_features=self._show_features,
            )
        elif isinstance(new_acquirer, CameraAcquirer):
            new_acquirer_controller = ConanCameraAcquirerController(
                acquirer=new_acquirer,
                params_factory=self._params_factory,
                features_service=self._features_service,
                source_image_out=self.bn_source_image,
                show_features=self._show_features,
            )
        elif new_acquirer is None:
            new_acquirer_controller = None
        else:
            raise ValueError("Unknown acquirer '{}'".format(new_acquirer))

        self._acquirer_controller = new_acquirer_controller
        self.bn_acquirer_controller.poke()

    def _show_features(self, features: Optional[ConanFeatures]) -> None:
        if features is None:
            self.bn_features.set(None)
            return

        self.bn_features.set(features)

    def _destroy_acquirer_controller(self) -> None:
        acquirer_controller = self._acquirer_controller
        if acquirer_controller is None:
            return

        acquirer_controller.destroy()

        self._acquirer_controller = None
예제 #19
0
class PendantAnalysisJob:
    class Status(Enum):
        WAITING_FOR_IMAGE = ('Waiting for image', False)
        EXTRACTING_FEATURES = ('Extracting features', False)
        FITTING = ('Fitting', False)
        FINISHED = ('Finished', True)
        CANCELLED = ('Cancelled', True)

        def __init__(self, display_name: str, is_terminal: bool) -> None:
            self.display_name = display_name
            self.is_terminal = is_terminal

        def __str__(self) -> str:
            return self.display_name

    @inject
    def __init__(
            self,
            input_image: InputImage,
            *,
            physical_params_factory: PendantPhysicalParamsFactory,
            features_params_factory: PendantFeaturesParamsFactory,
            features_service: PendantFeaturesService,
            ylfit_service: YoungLaplaceFitService,
    ) -> None:
        self._loop = asyncio.get_event_loop()

        self._features_params_factory = features_params_factory
        self._physical_params_factory = physical_params_factory

        self._features_service = features_service
        self._ylfit_service = ylfit_service

        self._time_start = time.time()
        self._time_end = math.nan

        self._input_image = input_image

        self._status = self.Status.WAITING_FOR_IMAGE
        self.bn_status = AccessorBindable(
            getter=self._get_status,
            setter=self._set_status,
        )

        self._image = None  # type: Optional[np.ndarray]
        # The time (in Unix time) that the image was captured.
        self._image_timestamp = math.nan  # type: float

        self.bn_image = AccessorBindable(self._get_image)
        self.bn_image_timestamp = AccessorBindable(self._get_image_timestamp)

        # Attributes from YoungLaplaceFitter
        self.bn_bond_number = VariableBindable(math.nan)
        self.bn_apex_coords_px = VariableBindable(Vector2(math.nan, math.nan))
        self.bn_apex_radius_px = VariableBindable(math.nan)
        self.bn_rotation = VariableBindable(math.nan)
        self.bn_drop_profile_fit = VariableBindable(None)
        self.bn_residuals = VariableBindable(None)
        self.bn_arclengths = VariableBindable(None)

        # Attributes from PhysicalPropertiesCalculator
        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)

        # Attributes from FeatureExtractor
        self.bn_drop_region = VariableBindable(None)
        self.bn_needle_region = VariableBindable(None)
        self.bn_canny_min = VariableBindable(None)
        self.bn_canny_max = VariableBindable(None)
        self.bn_drop_profile_extract = VariableBindable(None)
        self.bn_needle_width_px = VariableBindable(math.nan)

        self.bn_is_done = AccessorBindable(getter=self._get_is_done)
        self.bn_is_cancelled = AccessorBindable(getter=self._get_is_cancelled)
        self.bn_progress = AccessorBindable(self._get_progress)
        self.bn_time_start = AccessorBindable(self._get_time_start)
        self.bn_time_est_complete = AccessorBindable(self._get_time_est_complete)

        self.bn_status.on_changed.connect(self.bn_is_done.poke)
        self.bn_status.on_changed.connect(self.bn_progress.poke)

        self._loop.create_task(self._input_image.read()).add_done_callback(self._input_image_read_done)

        self._features = None
        self._ylfit = None

    def _input_image_read_done(self, read_task: Future) -> None:
        if read_task.cancelled():
            self.cancel()
            return

        if self.bn_is_done.get():
            return

        image, image_timestamp = read_task.result()
        self._image_ready(image, image_timestamp)

    def _image_ready(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

        features_params = self._features_params_factory.create()
        self.bn_drop_region.set(features_params.drop_region)
        self.bn_needle_region.set(features_params.needle_region)
        self.bn_canny_max.set(features_params.thresh2)
        self.bn_canny_min.set(features_params.thresh1)

        self._features = self._features_service.extract(image, features_params)
        self._features.add_done_callback(self._features_done)

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

        self.bn_status.set(self.Status.EXTRACTING_FEATURES)

    def _features_done(self, fut: asyncio.Future) -> None:
        features: PendantFeatures

        if fut.cancelled():
            self.cancel()
            return
        try:
            features = fut.result()
        except Exception as e:
            raise e

        self.bn_drop_profile_extract.set(features.drop_points.T)
        self.bn_needle_width_px.set(features.needle_diameter)

        self._ylfit = self._ylfit_service.fit(features.drop_points)
        self._ylfit.add_done_callback(self._ylfit_done)

        self.bn_status.set(self.Status.FITTING)

    def _ylfit_done(self, fut: asyncio.Future) -> None:
        result: YoungLaplaceFitResult

        if fut.cancelled():
            self.cancel()
            return
        try:
            result = fut.result()
        except Exception as e:
            raise e

        physical_params = self._physical_params_factory.create()
        drop_density = physical_params.drop_density
        continuous_density = physical_params.continuous_density
        needle_diameter = physical_params.needle_diameter
        gravity = physical_params.gravity

        bond = result.bond
        apex_x = result.apex_x
        apex_y = result.apex_y
        rotation = result.rotation
        residuals = result.residuals
        closest = result.closest
        arclengths = result.arclengths
        radius_px = result.radius
        surface_area_px = result.surface_area
        volume_px = result.volume

        # Keep rotation angle between -90 to 90 degrees.
        rotation = (rotation + np.pi/2) % np.pi - np.pi/2

        needle_diameter_px = self.bn_needle_width_px.get()
        if needle_diameter_px is not None:
            px_size = needle_diameter/needle_diameter_px
            delta_density = abs(drop_density - continuous_density)

            radius = radius_px * px_size
            surface_area = surface_area_px * px_size**2
            volume = volume_px * px_size**3
            ift = delta_density * gravity * radius**2 / bond
            worthington = (delta_density * gravity * volume) / (PI * ift * needle_diameter)
        else:
            radius = math.nan
            surface_area = math.nan
            volume = math.nan
            ift = math.nan
            worthington = math.nan

        self.bn_bond_number.set(bond)
        self.bn_apex_coords_px.set(Vector2(apex_x, apex_y))
        self.bn_apex_radius_px.set(radius_px)
        self.bn_rotation.set(rotation)
        self.bn_residuals.set(residuals)
        self.bn_arclengths.set(arclengths)
        self.bn_drop_profile_fit.set(closest.T[np.argsort(arclengths)])

        self.bn_apex_radius.set(radius)
        self.bn_surface_area.set(surface_area)
        self.bn_volume.set(volume)
        self.bn_interfacial_tension.set(ift)
        self.bn_worthington.set(worthington)

        self.bn_status.set(self.Status.FINISHED)

    def cancel(self) -> None:
        if self.bn_status.get().is_terminal:
            # This is already at the end of its life.
            return

        if self.bn_status.get() is self.Status.WAITING_FOR_IMAGE:
            self._input_image.cancel()

        if self._features is not None:
            self._features.cancel()

        if self._ylfit is not None:
            self._ylfit.cancel()

        self.bn_status.set(self.Status.CANCELLED)

    def _get_status(self) -> Status:
        return self._status

    def _set_status(self, new_status: Status) -> None:
        self._status = new_status
        self.bn_is_cancelled.poke()

        if new_status.is_terminal:
            self._time_end = time.time()

    def _get_image(self) -> Optional[np.ndarray]:
        return self._image

    def _get_image_timestamp(self) -> float:
        return self._image_timestamp

    def _get_is_done(self) -> bool:
        return self.bn_status.get().is_terminal

    def _get_is_cancelled(self) -> bool:
        return self.bn_status.get() is self.Status.CANCELLED

    def _get_progress(self) -> float:
        if self.bn_is_done.get():
            return 1
        else:
            return 0

    def _get_time_start(self) -> float:
        return self._time_start

    def _get_time_est_complete(self) -> float:
        if self._input_image is None:
            return math.nan

        return self._input_image.est_ready

    def calculate_time_elapsed(self) -> float:
        time_start = self._time_start

        if math.isfinite(self._time_end):
            time_elapsed = self._time_end - time_start
        else:
            time_now = time.time()
            time_elapsed = time_now - time_start

        return time_elapsed

    def calculate_time_remaining(self) -> float:
        if self.bn_is_done.get():
            return 0

        time_est_complete = self.bn_time_est_complete.get()
        time_now = time.time()
        time_remaining = time_est_complete - time_now

        return time_remaining

    @property
    def is_image_replicated(self) -> bool:
        return self._input_image.is_replicated