def _ylfit_incremental_update(self, ylfit: YoungLaplaceFit) -> None: if self._stop_flag: ylfit.cancel() return editor = self._data.edit(timeout=1) assert editor is not None try: apex_pos = Vector2(ylfit.apex_x, ylfit.apex_y) rotation = ylfit.rotation profile_fit = ylfit(np.linspace(0, 1, num=self.PROFILE_FIT_SAMPLES)) if not self._is_sessile: apex_pos = Vector2(apex_pos.x, -apex_pos.y) rotation *= -1 profile_fit[:, 1] *= -1 editor.set_value('apex_pos', apex_pos) editor.set_value('apex_radius', ylfit.apex_radius) editor.set_value('bond_number', ylfit.bond_number) editor.set_value('rotation', rotation) editor.set_value('profile_fit', profile_fit) editor.set_value('residuals', ylfit.residuals) editor.set_value('volume', ylfit.volume) editor.set_value('surface_area', ylfit.surface_area) except Exception as exc: # If any exceptions occur, discard changes and re-raise the exception. editor.discard() raise exc else: # Otherwise commit the changes. editor.commit()
def _recalculate(self) -> None: features = self._features params = self.params drop_profile = features.bn_drop_profile_px.get() if drop_profile is None: return surface = params.bn_surface_line_px.get() if surface is None: return drop_profile = drop_profile.copy() surface_poly1d = np.poly1d((surface.gradient, surface.eval(x=0).y)) # ContactAngle expects the coordinates of drop profile to be such that the surface has a lower y-coordinate than # the drop, so mirror the drop in y-direction. (Remember drop profile is in 'image coordinates', where # increasing y-coordinate is 'downwards') drop_profile[:, 1] *= -1 surface_poly1d = -surface_poly1d conancalc = ContactAngle(drop_profile, surface_poly1d) # Mirror the tangents and contact points back to original coordinate system as well. left_tangent = -conancalc.left_tangent left_point = Vector2(x=conancalc.left_point.x, y=-conancalc.left_point.y) right_tangent = -conancalc.right_tangent right_point = Vector2(x=conancalc.right_point.x, y=-conancalc.right_point.y) self.bn_left_tangent.set(left_tangent) self.bn_left_angle.set(conancalc.left_angle) self.bn_left_point.set(left_point) self.bn_right_tangent.set(right_tangent) self.bn_right_angle.set(conancalc.right_angle) self.bn_right_point.set(right_point)
def _calculate_right_contact_point( self, right_contact_tangent: np.poly1d) -> Vector2[float]: roots = right_contact_tangent.roots if not roots: return Vector2(math.nan, math.nan) return Vector2(roots[0], right_contact_tangent(roots[0]))
def __init__(self, drop_profile: np.ndarray, surface: np.poly1d) -> None: self._drop_profile = drop_profile self._surface = surface self.left_tangent, self.left_angle, self.left_point \ = (np.poly1d((math.nan, math.nan)), math.nan, Vector2(math.nan, math.nan)) self.right_tangent, self.right_angle, self.right_point \ = (np.poly1d((math.nan, math.nan)), math.nan, Vector2(math.nan, math.nan)) self._calculate()
def _widget_dist_from_canvas( self, dist_canvas: Vector2Like[float]) -> Vector2[float]: dist_canvas = Vector2(*dist_canvas) viewport_size = self.props.viewport_extents.size viewport_widget_size = self.props.viewport_widget_extents.size scale = Vector2(viewport_widget_size.x / viewport_size.x, viewport_widget_size.y / viewport_size.y) dist_widget = Vector2(scale.x * dist_canvas.x, scale.y * dist_canvas.y) return dist_widget
def _canvas_dist_from_widget( self, dist_widget: Tuple[float, float]) -> Vector2[float]: dist_widget = Vector2(*dist_widget) viewport_size = self.props.viewport_extents.size viewport_widget_size = self.props.viewport_widget_extents.size scale = Vector2(viewport_size.x / viewport_widget_size.x, viewport_size.y / viewport_widget_size.y) dist_canvas = Vector2(scale.x * dist_widget.x, scale.y * dist_widget.y) return dist_canvas
def _calculate(self) -> None: drop_profile = np.copy(self._drop_profile) surface = self._surface surface_angle = math.atan(surface.c[0]) if len(surface.c) > 1 else 0 rot_mtx = np.array( [[math.cos(surface_angle), -math.sin(surface_angle)], [math.sin(surface_angle), math.cos(surface_angle)]]) # Transform drop profile to coordinates where surface line is y=0 drop_profile = drop_profile.astype(float) drop_profile[:, 1] -= surface.c[-1] drop_profile = (rot_mtx.T @ drop_profile.T).T drop_profile = drop_profile.astype(int) self._right_segment, self.right_tangent, self.right_angle, self.right_point \ = self._calculate_right_params(drop_profile) # Mirror the contour left-to-right drop_profile = np.flipud(drop_profile) drop_profile[:, 0] *= -1 self._left_segment, self.left_tangent, self.left_angle, self.left_point \ = self._calculate_right_params(drop_profile) # Mirror back tangent and contact point. self.left_tangent = np.poly1d(self.left_tangent.coefficients * [-1, 1]) self.left_point = Vector2(-self.left_point.x, self.left_point.y) self._left_segment = self._left_segment * [-1, 1] # Transform back to given coordinates. self._left_segment = (rot_mtx @ self._left_segment.T).T self._left_segment[:, 1] += surface.c[-1] self._left_segment = self._left_segment.astype(int) self._right_segment = (rot_mtx @ self._right_segment.T).T self._right_segment[:, 1] += surface.c[-1] self._right_segment = self._right_segment.astype(int) self.left_tangent = _transform_line(self.left_tangent, rot_mtx) self.left_tangent += surface.c[-1] self.left_point = Vector2(*(rot_mtx @ self.left_point)) self.left_point += (0, surface.c[-1]) self.right_tangent = _transform_line(self.right_tangent, rot_mtx) self.right_tangent += surface.c[-1] self.right_point = Vector2(*(rot_mtx @ self.right_point)) self.right_point += (0, surface.c[-1])
def _do_init(self, in_analysis: Bindable[Optional[ConanAnalysis]]) -> None: self._bn_analysis = in_analysis self._analysis_unbind_tasks = [] self.bn_image = VariableBindable(None) self.bn_left_angle = VariableBindable(math.nan) self.bn_left_point = VariableBindable(Vector2(math.nan, math.nan)) self.bn_right_angle = VariableBindable(math.nan) self.bn_right_point = VariableBindable(Vector2(math.nan, math.nan)) self.bn_surface_line = VariableBindable(None) self.__event_connections = []
def _viewport_widget_size(self) -> Vector2[float]: widget_size = Vector2(self.get_allocated_width(), self.get_allocated_height()) return self._calculate_stretch_size( stretch=self._viewport_stretch, reference_size=widget_size, child_size=self._viewport_extents.size)
def __init__(self, features: FeatureExtractor, params: ContactAngleCalculatorParams) -> None: self._features = features self.params = params self.bn_left_tangent = VariableBindable(np.poly1d((math.nan, math.nan))) self.bn_left_angle = VariableBindable(math.nan) self.bn_left_point = VariableBindable(Vector2(math.nan, math.nan)) self.bn_right_tangent = VariableBindable(np.poly1d((math.nan, math.nan))) self.bn_right_angle = VariableBindable(math.nan) self.bn_right_point = VariableBindable(Vector2(math.nan, math.nan)) # Recalculate when inputs change features.bn_drop_profile_px.on_changed.connect(self._recalculate) params.bn_surface_line_px.on_changed.connect(self._recalculate) self._recalculate()
def _canvas_coord_from_widget( self, coord_widget: Tuple[float, float]) -> Vector2[float]: coord_widget = Vector2(*coord_widget) viewport_extents = self.props.viewport_extents viewport_widget_extents = self.props.viewport_widget_extents coord_viewport_widget = coord_widget - viewport_widget_extents.position coord_viewport_pct = Vector2( coord_viewport_widget.x / viewport_widget_extents.size.x, coord_viewport_widget.y / viewport_widget_extents.size.y) coord_viewport = Vector2( coord_viewport_pct.x * viewport_extents.size.x, coord_viewport_pct.y * viewport_extents.size.y) coord_canvas = viewport_extents.position + coord_viewport return coord_canvas
def _widget_coord_from_canvas( self, coord_canvas: Vector2Like[float]) -> Vector2[float]: coord_canvas = Vector2(*coord_canvas) viewport_extents = self.props.viewport_extents viewport_widget_extents = self.props.viewport_widget_extents coord_viewport = coord_canvas - viewport_extents.pos coord_viewport_pct = Vector2( coord_viewport.x / viewport_extents.size.x, coord_viewport.y / viewport_extents.size.y) coord_viewport_widget = Vector2( coord_viewport_pct.x * viewport_widget_extents.size.x, coord_viewport_pct.y * viewport_widget_extents.size.y) coord_widget = viewport_widget_extents.pos + coord_viewport_widget return coord_widget
def _calculate_right_params(self, drop_profile: np.ndarray) -> Tuple[np.ndarray, np.poly1d, float, Vector2[float]]: try: right_segment, right_tangent = self._calculate_right_contact_tangent(drop_profile) except self.NotEnoughDropPoints: return (np.empty((0, 2)), np.poly1d((math.nan, math.nan)), math.nan, Vector2(math.nan, math.nan)) return ( right_segment, right_tangent, self._calculate_right_contact_angle(right_tangent), self._calculate_right_contact_point(right_tangent) )
def __init__(self, features: FeatureExtractor, *, loop: Optional[asyncio.AbstractEventLoop] = None) -> None: self._loop = loop or asyncio.get_event_loop() self._features = features self._is_sessile = False self._data = self._Data( _loop=self._loop, apex_pos=Vector2(math.nan, math.nan), apex_radius=math.nan, bond_number=math.nan, rotation=math.nan, profile_fit=None, residuals=None, volume=math.nan, surface_area=math.nan, ) self._stop_flag = False self.bn_is_busy = AccessorBindable(getter=self.get_is_busy) self._updater_worker = UpdaterWorker(do_update=self._update, on_idle=self.bn_is_busy.poke, loop=self._loop) self.bn_apex_pos = self._data.apex_pos # type: Bindable[Vector2[float]] self.bn_apex_radius = self._data.apex_radius # type: Bindable[float] self.bn_bond_number = self._data.bond_number # type: Bindable[float] self.bn_rotation = self._data.rotation # type: Bindable[float] self.bn_profile_fit = self._data.profile_fit # type: Bindable[np.ndarray] self.bn_residuals = self._data.residuals # type: Bindable[np.ndarray] self.bn_volume = self._data.volume # type: Bindable[float] self.bn_surface_area = self._data.surface_area # type: Bindable[float] self._log = '' self._log_lock = threading.Lock() self.bn_log = AccessorBindable(getter=self.get_log) # Reanalyse when extracted drop profile changes features.bn_drop_profile_px.on_changed.connect( self._hdl_features_changed) # First update to initialise attributes. self._queue_update()
def draw_angle_marker(image: np.ndarray, vertex_pos: Vector2[float], start_angle: float, delta_angle: float, radius: float, color: Tuple[float, float, float]) -> None: if not Rect2(pos=(0, 0), size=image.shape[1::-1]).contains_point(vertex_pos): # Vertex is outside of the image, ignore. return end_angle = start_angle + delta_angle start_pos = vertex_pos delta_pos = radius * Vector2(math.cos(-end_angle), math.sin(-end_angle)) end_pos = start_pos + delta_pos cv2.line(image, pt1=tuple(start_pos.as_type(int)), pt2=tuple(end_pos.as_type(int)), color=color, thickness=1)
class Render(Gtk.DrawingArea, protocol.Render): _canvas_size = Vector2(1, 1) _viewport_extents = Rect2(position=(0, 0), size=_canvas_size) # type: Rect2[float] _viewport_stretch = protocol.Render.ViewportStretch.FIT STYLE = """ .render { border: 1px solid white; } """ _STYLE_PROV = Gtk.CssProvider() _STYLE_PROV.load_from_data(bytes(STYLE, 'utf-8')) class RenderObjectContainer: def __init__(self, render_object: protocol.RenderObject, handler_ids: Sequence[int]) -> None: self.render_object = render_object self.handler_ids = tuple(handler_ids) def __init__(self, *, can_focus=True, **options) -> None: super().__init__(focus_on_click=True, can_focus=can_focus, **options) self._ro_containers = [ ] # type: MutableSequence[Render.RenderObjectContainer] self.add_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.FOCUS_CHANGE_MASK | Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK | Gdk.EventMask.KEY_PRESS_MASK) self.get_style_context().add_class('render') self.get_style_context().add_provider(self._STYLE_PROV, Gtk.STYLE_PROVIDER_PRIORITY_USER) def do_draw(self, cr: cairo.Context) -> None: viewport_widget_extents = self.props.viewport_widget_extents with cairo_saved(cr): cr.rectangle(*viewport_widget_extents.position, *viewport_widget_extents.size) cr.clip() for ro in self._render_objects: ro.draw(cr) if self.has_focus(): # Draw focus indicator stroke_width = 1 rectangle_pos = viewport_widget_extents.position + ( stroke_width / 2, stroke_width / 2) rectangle_size = viewport_widget_extents.size - (stroke_width, stroke_width) cr.rectangle(*rectangle_pos, *rectangle_size) cr.set_source_rgb(70 / 255, 142 / 255, 220 / 255) cr.set_line_width(stroke_width) cr.stroke() @property def _render_objects(self) -> Sequence[protocol.RenderObject]: return sorted( (container.render_object for container in self._ro_containers), key=lambda ro: ro.props.z_index, ) def add_render_object(self, ro: protocol.RenderObject) -> None: handler_ids = (ro.connect('request-draw', lambda _: self.queue_draw()), ) self._ro_containers.append( self.RenderObjectContainer(render_object=ro, handler_ids=handler_ids)) ro.set_parent(self) self.queue_draw() def remove_render_object(self, ro: protocol.RenderObject) -> None: container = self._ro_container_from_ro(ro) for handler_id in container.handler_ids: ro.disconnect(handler_id) self._ro_containers.remove(container) self.queue_draw() def _ro_container_from_ro( self, ro: protocol.RenderObject) -> 'Render.RenderObjectContainer': for container in self._ro_containers: if container.render_object is ro: return container else: raise ValueError('No container found for {}.'.format(ro)) def do_button_press_event(self, event: Gdk.EventButton) -> None: self.emit('cursor-down-event', self._canvas_coord_from_widget(Vector2(event.x, event.y))) if self.props.can_focus: self.grab_focus() def do_button_release_event(self, event: Gdk.EventButton) -> None: self.emit('cursor-up-event', self._canvas_coord_from_widget(Vector2(event.x, event.y))) def do_motion_notify_event(self, event: Gdk.EventButton) -> None: self.emit('cursor-motion-event', self._canvas_coord_from_widget(Vector2(event.x, event.y))) def do_key_press_event(self, event: Gdk.EventKey) -> bool: if event.keyval == Gdk.KEY_Tab: # Allow user to use the tab key to cycle focus to another widget return False # Stop event propagation return True # Cursor signals @GObject.Signal(arg_types=(object, )) def cursor_down_event(self, pos: Vector2[float]) -> None: pass @GObject.Signal(arg_types=(object, )) def cursor_up_event(self, pos: Vector2[float]) -> None: pass @GObject.Signal(arg_types=(object, )) def cursor_motion_event(self, pos: Vector2[float]) -> None: pass # Coordinate transform functions def _widget_coord_from_canvas( self, coord_canvas: Tuple[float, float]) -> Vector2[float]: coord_canvas = Vector2(*coord_canvas) viewport_extents = self.props.viewport_extents viewport_widget_extents = self.props.viewport_widget_extents coord_viewport = coord_canvas - viewport_extents.position coord_viewport_pct = Vector2( coord_viewport.x / viewport_extents.size.x, coord_viewport.y / viewport_extents.size.y) coord_viewport_widget = Vector2( coord_viewport_pct.x * viewport_widget_extents.size.x, coord_viewport_pct.y * viewport_widget_extents.size.y) coord_widget = viewport_widget_extents.position + coord_viewport_widget return coord_widget def _canvas_coord_from_widget( self, coord_widget: Tuple[float, float]) -> Vector2[float]: coord_widget = Vector2(*coord_widget) viewport_extents = self.props.viewport_extents viewport_widget_extents = self.props.viewport_widget_extents coord_viewport_widget = coord_widget - viewport_widget_extents.position coord_viewport_pct = Vector2( coord_viewport_widget.x / viewport_widget_extents.size.x, coord_viewport_widget.y / viewport_widget_extents.size.y) coord_viewport = Vector2( coord_viewport_pct.x * viewport_extents.size.x, coord_viewport_pct.y * viewport_extents.size.y) coord_canvas = viewport_extents.position + coord_viewport return coord_canvas def _widget_dist_from_canvas( self, dist_canvas: Tuple[float, float]) -> Vector2[float]: dist_canvas = Vector2(*dist_canvas) viewport_size = self.props.viewport_extents.size viewport_widget_size = self.props.viewport_widget_extents.size scale = Vector2(viewport_widget_size.x / viewport_size.x, viewport_widget_size.y / viewport_size.y) dist_widget = Vector2(scale.x * dist_canvas.x, scale.y * dist_canvas.y) return dist_widget def _canvas_dist_from_widget( self, dist_widget: Tuple[float, float]) -> Vector2[float]: dist_widget = Vector2(*dist_widget) viewport_size = self.props.viewport_extents.size viewport_widget_size = self.props.viewport_widget_extents.size scale = Vector2(viewport_size.x / viewport_widget_size.x, viewport_size.y / viewport_widget_size.y) dist_canvas = Vector2(scale.x * dist_widget.x, scale.y * dist_widget.y) return dist_canvas # Canvas geometry properties @GObject.Property def canvas_size(self) -> Vector2[float]: return self._canvas_size @canvas_size.setter def canvas_size(self, new_size: Vector2[float]) -> None: self._canvas_size = new_size self.queue_draw() @GObject.Property def viewport_extents(self) -> Rect2[float]: return self._viewport_extents @viewport_extents.setter def viewport_extents(self, new_extents: Rect2[float]) -> None: self._viewport_extents = new_extents self.queue_draw() @GObject.Property def viewport_widget_extents(self) -> Rect2[float]: return Rect2(position=self._viewport_widget_pos, size=self._viewport_widget_size) @property def _viewport_widget_size(self) -> Vector2[float]: widget_size = Vector2(self.get_allocated_width(), self.get_allocated_height()) return self._calculate_stretch_size( stretch=self._viewport_stretch, reference_size=widget_size, child_size=self._viewport_extents.size) @property def _viewport_widget_pos(self) -> Vector2[float]: widget_size = Vector2(self.get_allocated_width(), self.get_allocated_height()) return self._calculate_offset_for_centred_rectangles( reference_size=widget_size, child_size=self._viewport_widget_size) @classmethod def _calculate_stretch_size(cls, stretch: protocol.Render.ViewportStretch, reference_size: Vector2[float], child_size: Vector2[float]) -> Vector2[float]: reference_aspect = reference_size[0] / reference_size[1] child_aspect = child_size[0] / child_size[1] if stretch is cls.ViewportStretch.FILL and (reference_aspect > child_aspect) \ or stretch is cls.ViewportStretch.FIT and (reference_aspect <= child_aspect): scale_factor = reference_size[0] / child_size[0] else: scale_factor = reference_size[1] / child_size[1] return child_size * scale_factor @staticmethod def _calculate_offset_for_centred_rectangles(reference_size: Vector2[float], child_size: Vector2[float]) \ -> Vector2[float]: offset = reference_size / 2 - child_size / 2 return offset
def _viewport_widget_pos(self) -> Vector2[float]: widget_size = Vector2(self.get_allocated_width(), self.get_allocated_height()) return self._calculate_offset_for_centred_rectangles( reference_size=widget_size, child_size=self._viewport_widget_size)
def do_motion_notify_event(self, event: Gdk.EventButton) -> None: self.emit('cursor-motion-event', self._canvas_coord_from_widget(Vector2(event.x, event.y)))
def do_button_release_event(self, event: Gdk.EventButton) -> None: self.emit('cursor-up-event', self._canvas_coord_from_widget(Vector2(event.x, event.y)))
def do_button_press_event(self, event: Gdk.EventButton) -> None: self.emit('cursor-down-event', self._canvas_coord_from_widget(Vector2(event.x, event.y))) if self.props.can_focus: self.grab_focus()
def __init__( self, input_image: InputImage, do_extract_features: Callable[[Bindable[np.ndarray]], FeatureExtractor], do_young_laplace_fit: Callable[[FeatureExtractor], YoungLaplaceFitter], do_calculate_physprops: Callable[[FeatureExtractor, YoungLaplaceFitter], PhysicalPropertiesCalculator] ) -> None: self._loop = asyncio.get_event_loop() self._time_start = time.time() self._time_end = math.nan self._input_image = input_image self._do_extract_features = do_extract_features self._do_young_laplace_fit = do_young_laplace_fit self._do_calculate_physprops = do_calculate_physprops self._status = self.Status.WAITING_FOR_IMAGE self.bn_status = AccessorBindable( getter=self._get_status, setter=self._set_status, ) self._image = None # type: Optional[np.ndarray] # The time (in Unix time) that the image was captured. self._image_timestamp = math.nan # type: float self._extracted_features = None # type: Optional[FeatureExtractor] self._physical_properties = None # type: Optional[PhysicalPropertiesCalculator] self._young_laplace_fit = None # type: Optional[YoungLaplaceFitter] self.bn_image = AccessorBindable(self._get_image) self.bn_image_timestamp = AccessorBindable(self._get_image_timestamp) # Attributes from YoungLaplaceFitter self.bn_bond_number = BoxBindable(math.nan) self.bn_apex_coords_px = BoxBindable(Vector2(math.nan, math.nan)) self.bn_apex_radius_px = BoxBindable(math.nan) self.bn_rotation = BoxBindable(math.nan) self.bn_drop_profile_fit = BoxBindable(None) self.bn_residuals = BoxBindable(None) # Attributes from PhysicalPropertiesCalculator 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) # Attributes from FeatureExtractor self.bn_drop_region = BoxBindable(None) self.bn_needle_region = BoxBindable(None) self.bn_drop_profile_extract = BoxBindable(None) self.bn_needle_profile_extract = BoxBindable(None) self.bn_needle_width_px = BoxBindable(math.nan) # Log self.bn_log = BoxBindable('') self.bn_is_done = AccessorBindable(getter=self._get_is_done) self.bn_is_cancelled = AccessorBindable(getter=self._get_is_cancelled) self.bn_progress = AccessorBindable(self._get_progress) self.bn_time_start = AccessorBindable(self._get_time_start) self.bn_time_est_complete = AccessorBindable(self._get_time_est_complete) self.bn_status.on_changed.connect(self.bn_is_done.poke) self.bn_status.on_changed.connect(self.bn_progress.poke) self._loop.create_task(self._input_image.read()).add_done_callback(self._hdl_input_image_read)
def __init__( self, input_image: InputImage, do_extract_features: Callable[[Bindable[np.ndarray]], FeatureExtractor], do_calculate_conan: Callable[[FeatureExtractor], ContactAngleCalculator], ) -> None: self._loop = asyncio.get_event_loop() self._time_start = time.time() self._time_end = math.nan self._input_image = input_image self._do_extract_features = do_extract_features self._do_calculate_conan = do_calculate_conan self._status = self.Status.WAITING_FOR_IMAGE self.bn_status = AccessorBindable( getter=self._get_status, setter=self._set_status, ) self._image = None # type: Optional[np.ndarray] # The time (in Unix time) that the image was captured. self._image_timestamp = math.nan # type: float self._extracted_features = None # type: Optional[FeatureExtractor] self._calculated_conan = None # type: Optional[ContactAngleCalculator] self.bn_image = AccessorBindable(self._get_image) self.bn_image_timestamp = AccessorBindable(self._get_image_timestamp) # Attributes from ContactAngleCalculator self.bn_left_angle = BoxBindable(math.nan) self.bn_left_tangent = BoxBindable(np.poly1d((math.nan, math.nan))) self.bn_left_point = BoxBindable(Vector2(math.nan, math.nan)) self.bn_right_angle = BoxBindable(math.nan) self.bn_right_tangent = BoxBindable(np.poly1d((math.nan, math.nan))) self.bn_right_point = BoxBindable(Vector2(math.nan, math.nan)) self.bn_surface_line = BoxBindable(None) # Attributes from FeatureExtractor self.bn_drop_region = BoxBindable(None) self.bn_drop_profile_extract = BoxBindable(None) # Log self.bn_is_done = AccessorBindable(getter=self._get_is_done) self.bn_is_cancelled = AccessorBindable(getter=self._get_is_cancelled) self.bn_progress = AccessorBindable(self._get_progress) self.bn_time_start = AccessorBindable(self._get_time_start) self.bn_time_est_complete = AccessorBindable( self._get_time_est_complete) self.bn_status.on_changed.connect(self.bn_is_done.poke) self.bn_status.on_changed.connect(self.bn_progress.poke) self._loop.create_task(self._input_image.read()).add_done_callback( self._hdl_input_image_read)