コード例 #1
0
class FeatureExtractor:
    _Data = thread_safe_bindable_collection(fields=[
        'bn_foreground_detection',
        'bn_drop_profile_px',
    ])

    def __init__(self,
                 image: Bindable[np.ndarray],
                 params: 'FeatureExtractorParams',
                 *,
                 loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
        self._loop = loop or asyncio.get_event_loop()

        self._bn_image = image

        self.params = params

        self._data = self._Data(
            _loop=self._loop,
            bn_foreground_detection=None,
            bn_drop_profile_px=None,
        )

        self.is_busy = AccessorBindable(getter=self.get_is_busy)
        self._updater_worker = UpdaterWorker(
            do_update=self._update,
            on_idle=self.is_busy.poke,
            loop=self._loop,
        )

        self.bn_foreground_detection = self._data.bn_foreground_detection  # type: Bindable[Optional[np.ndarray]]
        self.bn_drop_profile_px = self._data.bn_drop_profile_px  # type: Bindable[Optional[np.ndarray]]

        # Update extracted features whenever image or params change.
        self._bn_image.on_changed.connect(self._queue_update)
        self.params.bn_drop_region_px.on_changed.connect(self._queue_update)
        self.params.bn_thresh.on_changed.connect(self._queue_update)

        # First update to initialise features.
        self._queue_update()

    def _queue_update(self) -> None:
        was_busy = self._updater_worker.is_busy
        self._updater_worker.queue_update()
        if not was_busy:
            self.is_busy.poke()

    # This method will be run on different threads (could be called by UpdaterWorker), so make sure it stays
    # thread-safe.
    def _update(self) -> None:
        editor = self._data.edit(timeout=1)
        assert editor is not None

        try:
            new_foreground_detection = self._apply_foreground_detection()
            new_drop_profile_px = self._extract_drop_profile_px(
                new_foreground_detection)

            editor.set_value('bn_foreground_detection',
                             new_foreground_detection)
            editor.set_value('bn_drop_profile_px', new_drop_profile_px)
        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 _apply_foreground_detection(self) -> Optional[np.ndarray]:
        image = self._bn_image.get()
        if image is None:
            return None

        return apply_foreground_detection(
            image=image,
            thresh=self.params.bn_thresh.get(),
        )

    def _extract_drop_profile_px(
            self, binary_image: Optional[np.ndarray]) -> Optional[np.ndarray]:
        if binary_image is None:
            return None

        drop_region = self.params.bn_drop_region_px.get()
        if drop_region is None:
            return None

        drop_region = drop_region.as_type(int)

        drop_image = binary_image[drop_region.y0:drop_region.y1,
                                  drop_region.x0:drop_region.x1]

        drop_profile_px = extract_drop_profile(drop_image)
        drop_profile_px += drop_region.pos

        return drop_profile_px

    def get_is_busy(self) -> bool:
        return self._updater_worker.is_busy

    async def wait_until_not_busy(self) -> None:
        while self.is_busy.get():
            await self.is_busy.on_changed.wait()
コード例 #2
0
class FeatureExtractor:
    _Data = thread_safe_bindable_collection(fields=[
        'bn_edge_detection',
        'bn_drop_profile_px',
        'bn_needle_profile_px',
        'bn_needle_width_px',
    ])

    def __init__(self,
                 image: ReadBindable[np.ndarray],
                 params: 'FeatureExtractorParams',
                 *,
                 loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
        self._loop = loop or asyncio.get_event_loop()

        self._bn_image = image

        self.params = params

        self._data = self._Data(
            _loop=self._loop,
            bn_edge_detection=None,
            bn_drop_profile_px=None,
            bn_needle_profile_px=None,
            bn_needle_width_px=math.nan,
        )

        self.is_busy = AccessorBindable(getter=self.get_is_busy)
        self._updater_worker = UpdaterWorker(
            do_update=self._update,
            on_idle=self.is_busy.poke,
            loop=self._loop,
        )

        self.bn_edge_detection = self._data.bn_edge_detection  # type: ReadBindable[Optional[np.ndarray]]
        self.bn_drop_profile_px = self._data.bn_drop_profile_px  # type: ReadBindable[Optional[np.ndarray]]
        self.bn_needle_profile_px = self._data.bn_needle_profile_px  # type: ReadBindable[Optional[Tuple[np.ndarray, np.ndarray]]]
        self.bn_needle_width_px = self._data.bn_needle_width_px  # type: ReadBindable[float]

        # Update extracted features whenever image or params change.
        self._bn_image.on_changed.connect(self._queue_update)
        self.params.bn_drop_region_px.on_changed.connect(self._queue_update)
        self.params.bn_needle_region_px.on_changed.connect(self._queue_update)
        self.params.bn_canny_min.on_changed.connect(self._queue_update)
        self.params.bn_canny_max.on_changed.connect(self._queue_update)

        # First update to initialise features.
        self._queue_update()

    def _queue_update(self) -> None:
        was_busy = self._updater_worker.is_busy
        self._updater_worker.queue_update()
        if not was_busy:
            self.is_busy.poke()

    # This method will be run on different threads (could be called by UpdaterWorker), so make sure it stays
    # thread-safe.
    def _update(self) -> None:
        editor = self._data.edit(timeout=1)
        assert editor is not None

        try:
            new_edge_detection = self._apply_edge_detection()
            new_drop_profile_px = self._extract_drop_profile_px(
                new_edge_detection)
            new_needle_profile_px = self._extract_needle_profile_px(
                new_edge_detection)
            new_needle_width_px = self._extract_needle_width_px(
                new_needle_profile_px)

            editor.set_value('bn_edge_detection', new_edge_detection)
            editor.set_value('bn_drop_profile_px', new_drop_profile_px)
            editor.set_value('bn_needle_profile_px', new_needle_profile_px)
            editor.set_value('bn_needle_width_px', new_needle_width_px)
        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 _apply_edge_detection(self) -> Optional[np.ndarray]:
        image = self._bn_image.get()
        if image is None:
            return None

        return apply_edge_detection(
            image=image,
            canny_min=self.params.bn_canny_min.get(),
            canny_max=self.params.bn_canny_max.get(),
        )

    def _extract_drop_profile_px(
            self, binary_image: Optional[np.ndarray]) -> Optional[np.ndarray]:
        if binary_image is None:
            return None

        drop_region = self.params.bn_drop_region_px.get()
        if drop_region is None:
            return None

        drop_region = drop_region.map(int)

        drop_image = binary_image[drop_region.y0:drop_region.y1,
                                  drop_region.x0:drop_region.x1]

        drop_profile_px = extract_drop_profile(drop_image)
        drop_profile_px += drop_region.position

        return drop_profile_px

    def _extract_needle_profile_px(
        self, binary_image: Optional[np.ndarray]
    ) -> Optional[Tuple[np.ndarray, np.ndarray]]:
        if binary_image is None:
            return None

        needle_region = self.params.bn_needle_region_px.get()
        if needle_region is None:
            return None

        needle_region = needle_region.map(int)

        needle_image = binary_image[needle_region.y0:needle_region.y1,
                                    needle_region.x0:needle_region.x1]

        needle_profile_px = extract_needle_profile(needle_image)
        needle_profile_px = tuple(x + needle_region.position
                                  for x in needle_profile_px)

        return needle_profile_px

    def _extract_needle_width_px(
            self, needle_profile: Optional[Tuple[np.ndarray,
                                                 np.ndarray]]) -> float:
        if needle_profile is None:
            return math.nan

        return calculate_width_from_needle_profile(needle_profile)

    @property
    def is_sessile(self) -> bool:
        drop_region = self.params.bn_drop_region_px.get()
        needle_region = self.params.bn_needle_region_px.get()

        if needle_region is None or drop_region is None:
            # Can't determine if is sessile, just return False.
            return False

        if needle_region.pt0.y > drop_region.pt0.y:
            # Needle region is below drop region, probably sessile drop.
            return True

    def get_is_busy(self) -> bool:
        return self._updater_worker.is_busy

    async def wait_until_not_busy(self) -> None:
        while self.is_busy.get():
            await self.is_busy.on_changed.wait()
コード例 #3
0
class YoungLaplaceFitter:
    PROFILE_FIT_SAMPLES = 500

    _Data = thread_safe_bindable_collection(fields=[
        'apex_pos',
        'apex_radius',
        'bond_number',
        'rotation',
        'profile_fit',
        'residuals',
        'volume',
        'surface_area',
    ])

    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 _hdl_features_changed(self) -> None:
        self._queue_update()

    def _queue_update(self) -> None:
        was_busy = self._updater_worker.is_busy
        self._updater_worker.queue_update()

        if not was_busy:
            self.bn_is_busy.poke()

    # This method will be run on different threads (could be called by UpdaterWorker), so make sure it stays
    # thread-safe.
    def _update(self) -> None:
        if self._stop_flag:
            return

        drop_profile_px = self._features.bn_drop_profile_px.get()
        if drop_profile_px is None:
            return

        drop_profile_px = drop_profile_px.copy()

        self._is_sessile = self._features.is_sessile
        if not self._is_sessile:
            # YoungLaplaceFit takes in a drop profile where the drop is deformed in the negative y-direction.
            # (Remember that in 'image coordinates', positive y-direction is 'downwards')
            drop_profile_px[:, 1] *= -1

        self._clear_log()

        fit = YoungLaplaceFit(drop_profile=drop_profile_px,
                              on_update=self._ylfit_incremental_update,
                              logger=self._append_log)

    # This method will be run on different threads (could be called by UpdaterWorker), so make sure it stays
    # thread-safe.
    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 stop(self) -> None:
        self._stop_flag = True

    def get_is_busy(self) -> bool:
        return self._updater_worker.is_busy

    async def wait_until_not_busy(self) -> None:
        while self.bn_is_busy.get():
            await self.bn_is_busy.on_changed.wait()

    def _append_log(self, message: str) -> None:
        if message == '': return

        with self._log_lock:
            self._log += message

        self._loop.call_soon_threadsafe(self.bn_log.poke)

    def _clear_log(self) -> None:
        if self._log == '': return

        with self._log_lock:
            self._log = ''

        self._loop.call_soon_threadsafe(self.bn_log.poke)

    def get_log(self) -> str:
        with self._log_lock:
            return self._log