예제 #1
0
class FigureOptions:
    def __init__(self, should_save: bool, dpi: int, size_w: float, size_h: float) -> None:
        self.bn_should_save = BoxBindable(should_save)
        self.bn_dpi = BoxBindable(dpi)
        self.bn_size_w = BoxBindable(size_w)
        self.bn_size_h = BoxBindable(size_h)

        # Validation

        self.dpi_err = validate(
            value=self.bn_dpi,
            checks=(check_is_not_empty,
                    check_is_positive),
            enabled=self.bn_should_save)
        self.size_w_err = validate(
            value=self.bn_size_w,
            checks=(check_is_not_empty,
                    check_is_positive),
            enabled=self.bn_should_save)
        self.size_h_err = validate(
            value=self.bn_size_h,
            checks=(check_is_not_empty,
                    check_is_positive),
            enabled=self.bn_should_save)

        self.errors = bn_apply(lambda *args: any(args), self.dpi_err, self.size_w_err, self.size_h_err)

    @property
    def size(self) -> Tuple[float, float]:
        return self.bn_size_w.get(), self.bn_size_h.get()
예제 #2
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._conancalc_params = ContactAngleCalculatorParams()

        self._bn_analyses = BoxBindable(
            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,
        )
예제 #3
0
    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 = BoxBindable(math.nan)
        self.bn_volume = BoxBindable(math.nan)
        self.bn_surface_area = BoxBindable(math.nan)
        self.bn_apex_radius = BoxBindable(math.nan)
        self.bn_worthington = BoxBindable(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()
예제 #4
0
class ErrorsPresenter(Generic[ErrorType]):
    def __init__(self, errors_in: Bindable[Set[ErrorType]],
                 errors_out: Bindable[Set[ErrorType]]) -> None:
        self._errors_in = errors_in

        self.__destroyed = False
        self.__cleanup_tasks = []

        self._is_showing_errors = BoxBindable(False)
        self.__cleanup_tasks.append(lambda: self._is_showing_errors.set(False))

        data_bindings = [
            errors_out.bind_from(
                bn_apply(lambda x, y: x if y else set(), self._errors_in,
                         self._is_showing_errors))
        ]
        self.__cleanup_tasks.extend(db.unbind for db in data_bindings)

    def show_errors(self) -> None:
        self._is_showing_errors.set(bool(self._errors_in.get()))

    def destroy(self) -> None:
        assert not self.__destroyed
        for f in self.__cleanup_tasks:
            f()
        self.__destroyed = True
예제 #5
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 = BoxBindable(
            None)  # type: Bindable[Optional[np.ndarray]]
        self.bn_foreground_detection = BoxBindable(
            None)  # type: Bindable[Optional[np.ndarray]]
        self.bn_drop_profile = BoxBindable(
            None)  # type: Bindable[Optional[np.ndarray]]

        self._image_acquisition.bn_acquirer.on_changed.connect(
            self._update_acquirer_controller, )
예제 #6
0
class LocalStorageAcquirer(ImageSequenceAcquirer):
    IS_REPLICATED = True

    def __init__(self) -> None:
        super().__init__()
        self.bn_last_loaded_paths = BoxBindable(
            tuple())  # type: BoxBindable[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))
예제 #7
0
    def __init__(self) -> None:
        self.bn_save_dir_parent = BoxBindable(None)  # type: Bindable[Optional[Path]]
        self.bn_save_dir_name = BoxBindable('')

        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
        )
예제 #8
0
 def __init__(self) -> None:
     self.bn_inner_density = BoxBindable(
         math.nan)  # type: Bindable[Optional[float]]
     self.bn_outer_density = BoxBindable(
         math.nan)  # type: Bindable[Optional[float]]
     self.bn_needle_width = BoxBindable(
         math.nan)  # type: Bindable[Optional[float]]
     self.bn_gravity = BoxBindable(
         math.nan)  # type: Bindable[Optional[float]]
예제 #9
0
    def __init__(self) -> None:
        self.bn_save_dir_parent = BoxBindable(None)  # type: Bindable[Optional[Path]]
        self.bn_save_dir_name = BoxBindable('')

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

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

        self.bn_num_frames = BoxBindable(1)
        self.bn_frame_interval = BoxBindable(
            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()
예제 #11
0
    def __init__(self, camera_index: int) -> None:
        self._vc = cv2.VideoCapture(camera_index)

        self.bn_alive = BoxBindable(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()
예제 #12
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 = BoxBindable((tuple(), tuple()))
        self.bn_right_angle_data = BoxBindable((tuple(), tuple()))

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

        self._hdl_analyses_changed()
예제 #13
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 = BoxBindable(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)
예제 #14
0
class ConanAnalysisSaverOptions:
    def __init__(self) -> None:
        self.bn_save_dir_parent = BoxBindable(None)  # type: Bindable[Optional[Path]]
        self.bn_save_dir_name = BoxBindable('')

        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()
예제 #15
0
    def __init__(self, *, loop: asyncio.AbstractEventLoop) -> None:
        self._loop = loop

        self.bn_mode = BoxBindable(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)
            ),
        )
예제 #16
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 = BoxBindable(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,
        )
예제 #17
0
    def __init__(self, errors_in: Bindable[Set[ErrorType]],
                 errors_out: Bindable[Set[ErrorType]]) -> None:
        self._errors_in = errors_in

        self.__destroyed = False
        self.__cleanup_tasks = []

        self._is_showing_errors = BoxBindable(False)
        self.__cleanup_tasks.append(lambda: self._is_showing_errors.set(False))

        data_bindings = [
            errors_out.bind_from(
                bn_apply(lambda x, y: x if y else set(), self._errors_in,
                         self._is_showing_errors))
        ]
        self.__cleanup_tasks.extend(db.unbind for db in data_bindings)
예제 #18
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 = BoxBindable(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,
        )
예제 #19
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(BoxBindable(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)
예제 #20
0
class ImageSequenceAcquirer(ImageAcquirer):
    IS_REPLICATED = False

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

        self.bn_frame_interval = BoxBindable(
            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]
예제 #21
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 = BoxBindable(ResultsFooterStatus.IN_PROGRESS)
        self.bn_completion_progress = model.bn_analyses_completion_progress
        self.bn_time_elapsed = BoxBindable(math.nan)
        self.bn_time_remaining = BoxBindable(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()
예제 #22
0
    def __init__(self, should_save: bool, dpi: int, size_w: float, size_h: float) -> None:
        self.bn_should_save = BoxBindable(should_save)
        self.bn_dpi = BoxBindable(dpi)
        self.bn_size_w = BoxBindable(size_w)
        self.bn_size_h = BoxBindable(size_h)

        # Validation

        self.dpi_err = validate(
            value=self.bn_dpi,
            checks=(check_is_not_empty,
                    check_is_positive),
            enabled=self.bn_should_save)
        self.size_w_err = validate(
            value=self.bn_size_w,
            checks=(check_is_not_empty,
                    check_is_positive),
            enabled=self.bn_should_save)
        self.size_h_err = validate(
            value=self.bn_size_h,
            checks=(check_is_not_empty,
                    check_is_positive),
            enabled=self.bn_should_save)

        self.errors = bn_apply(lambda *args: any(args), self.dpi_err, self.size_w_err, self.size_h_err)
예제 #23
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 = BoxBindable('')
        self.bn_time_elapsed = in_time_elapsed
        self.bn_time_remaining = BoxBindable(math.nan)

        self.__event_connections = []
예제 #24
0
    def __init__(self) -> None:
        self._loop = asyncio.get_event_loop()

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

        self.bn_num_frames = BoxBindable(1)
        self.bn_frame_interval = BoxBindable(
            None)  # type: Bindable[Optional[float]]
예제 #25
0
    def __init__(self,
                 value: ValueType,
                 *,
                 errors_out: Optional[Bindable[Set[ErrorType]]] = None,
                 on_user_finished_editing: Optional[Event] = None) -> None:
        if errors_out is None:
            errors_out = BoxBindable(set())  # type: Bindable[Set[ErrorType]]

        if on_user_finished_editing is None:
            on_user_finished_editing = Event()

        self.value = value
        self.errors_out = errors_out
        self.on_user_finished_editing = on_user_finished_editing
예제 #26
0
class AppRootModel:
    def __init__(self, *, loop: asyncio.AbstractEventLoop) -> None:
        self._loop = loop

        self.bn_mode = BoxBindable(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
예제 #27
0
class IFTAnalysisSaverOptions:
    def __init__(self) -> None:
        self.bn_save_dir_parent = BoxBindable(None)  # type: Bindable[Optional[Path]]
        self.bn_save_dir_name = BoxBindable('')

        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()
예제 #28
0
    def __init__(self,
                 in_analyses: Bindable[Sequence[IFTDropAnalysis]]) -> None:
        self._bn_analyses = in_analyses

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

        self.bn_ift_data = BoxBindable((tuple(), tuple()))
        self.bn_volume_data = BoxBindable((tuple(), tuple()))
        self.bn_surface_area_data = BoxBindable((tuple(), tuple()))

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

        self._hdl_analyses_changed()
예제 #29
0
    def _do_init(self, in_analysis: Bindable[Optional[ConanAnalysis]]) -> None:
        self._bn_analysis = in_analysis
        self._analysis_unbind_tasks = []

        self.bn_image = BoxBindable(None)

        self.bn_left_angle = BoxBindable(math.nan)
        self.bn_left_point = BoxBindable(Vector2(math.nan, math.nan))
        self.bn_right_angle = BoxBindable(math.nan)
        self.bn_right_point = BoxBindable(Vector2(math.nan, math.nan))

        self.bn_surface_line = BoxBindable(None)

        self.__event_connections = []
예제 #30
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(BoxBindable(
            self._image))
        calculated_conan = self._do_calculate_conan(extracted_features)

        self._extracted_features = extracted_features
        self._calculated_conan = calculated_conan

        self._bind_fit()

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

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