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