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()
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 __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()
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
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, )
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))
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 )
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]]
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 )
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()
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 __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()
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)
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()
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 __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, )
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 __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, )
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)
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]
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 __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)
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 __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 __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
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
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()
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 _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 = []
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)