Exemplo n.º 1
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))
Exemplo n.º 2
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
Exemplo n.º 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 = 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)
Exemplo n.º 4
0
class WizardController:
    def __init__(self, pages: Iterable) -> None:
        self._pages = tuple(pages)

        self.bn_current_page = BoxBindable(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)
Exemplo n.º 5
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
Exemplo n.º 6
0
class WizardModel:
    def __init__(self, pages: Iterable[Any]) -> None:
        self._pages = tuple(pages)

        self._interpage_actions = {}

        self.bn_current_page = BoxBindable(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
Exemplo n.º 7
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 = 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,
        )

    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()
Exemplo n.º 8
0
class ContactAngleCalculator:
    def __init__(self, features: FeatureExtractor,
                 params: ContactAngleCalculatorParams) -> None:
        self._features = features
        self.params = params

        self.bn_left_tangent = BoxBindable(np.poly1d((math.nan, math.nan)))
        self.bn_left_angle = BoxBindable(math.nan)
        self.bn_left_point = BoxBindable(Vector2(math.nan, math.nan))
        self.bn_right_tangent = BoxBindable(np.poly1d((math.nan, math.nan)))
        self.bn_right_angle = BoxBindable(math.nan)
        self.bn_right_point = BoxBindable(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_at(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)
Exemplo n.º 9
0
class GraphsModel:
    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()

    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: IFTDropAnalysis) -> None:
        unbind_tasks = []

        event_connections = [
            analysis.bn_interfacial_tension.on_changed.connect(
                self._hdl_tracked_analysis_data_changed),
            analysis.bn_volume.on_changed.connect(
                self._hdl_tracked_analysis_data_changed),
            analysis.bn_surface_area.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: IFTDropAnalysis) -> 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:
        ift_data = []
        vol_data = []
        sur_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

            ift_value = analysis.bn_interfacial_tension.get()
            vol_value = analysis.bn_volume.get()
            sur_value = analysis.bn_surface_area.get()

            if ift_value is not None and math.isfinite(ift_value):
                ift_data.append((timestamp, ift_value))

            if vol_value is not None and math.isfinite(vol_value):
                vol_data.append((timestamp, vol_value))

            if sur_value is not None and math.isfinite(sur_value):
                sur_data.append((timestamp, sur_value))

        # Sort in ascending order of timestamp
        ift_data = sorted(ift_data, key=lambda x: x[0])
        vol_data = sorted(vol_data, key=lambda x: x[0])
        sur_data = sorted(sur_data, key=lambda x: x[0])

        ift_data = tuple(zip(*ift_data))
        vol_data = tuple(zip(*vol_data))
        sur_data = tuple(zip(*sur_data))

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

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

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

        self.bn_ift_data.set(ift_data)
        self.bn_volume_data.set(vol_data)
        self.bn_surface_area_data.set(sur_data)
Exemplo n.º 10
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 = BoxBindable(
            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()
Exemplo n.º 11
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 = BoxBindable(math.nan)
        self.bn_volume = BoxBindable(math.nan)
        self.bn_surface_area = BoxBindable(math.nan)
        self.bn_worthington = BoxBindable(math.nan)
        self.bn_bond_number = BoxBindable(math.nan)
        self.bn_apex_coords = BoxBindable((math.nan, math.nan))
        self.bn_image_angle = BoxBindable(math.nan)

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

        self.bn_residuals = BoxBindable(None)

        self.bn_log_text = BoxBindable('')

        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(self.bn_interfacial_tension),
            analysis.bn_volume.bind(self.bn_volume),
            analysis.bn_surface_area.bind(self.bn_surface_area),
            analysis.bn_worthington.bind(self.bn_worthington),
            analysis.bn_bond_number.bind(self.bn_bond_number),
            analysis.bn_apex_coords_px.bind(self.bn_apex_coords),
            analysis.bn_rotation.bind(self.bn_image_angle),
            analysis.bn_residuals.bind(self.bn_residuals),
            analysis.bn_log.bind(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.pos
        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.pos
        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()
Exemplo n.º 12
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 = 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()

    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
Exemplo n.º 13
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 = 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()

    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()
Exemplo n.º 14
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 = BoxBindable('')
        self.bn_time_elapsed = in_time_elapsed
        self.bn_time_remaining = BoxBindable(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()
Exemplo n.º 15
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 = 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()

    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)