Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the plumbing and documentation for some of the ask* methods #125

Merged
merged 9 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/docs/building-applications/5-async-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ from time import sleep

detector = gl.get_or_create_detector(name="your_detector_name", query="your_query")

cam = cv2.VideoCapture(0) # Initialize camera (0 is the default index)
cam = cv2.VideoCapture(0) # Initialize camera (0 is the default index)

while True:
_, image = cam.read() # Capture one frame from the camera
Expand All @@ -35,7 +35,7 @@ from groundlight import Groundlight

detector = gl.get_or_create_detector(name="your_detector_name", query="your_query")

image_query_id = db.get_next_image_query_id()
image_query_id = db.get_next_image_query_id()

while image_query_id is not None:
image_query = gl.get_image_query(id=image_query_id) # retrieve the image query from Groundlight
Expand Down Expand Up @@ -67,4 +67,7 @@ result = image_query.result # This will always be 'None' as you asked asynchron

image_query = gl.get_image_query(id=image_query.id) # Immediately retrieve the image query from Groundlight
result = image_query.result # This will likely be 'UNCLEAR' as Groundlight is still processing your query

image_query = gl.wait_for_confident_result(id=image_query.id) # Poll for a confident result from Groundlight
result = image_query.result
Comment on lines +71 to +72
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like showing this as the default use case.

```
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ packages = [
{include = "**/*.py", from = "src"},
]
readme = "README.md"
version = "0.12.0"
version = "0.12.1"

[tool.poetry.dependencies]
# For certifi, use ">=" instead of "^" since it upgrades its "major version" every year, not really following semver
Expand Down
79 changes: 59 additions & 20 deletions src/groundlight/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import os
import time
from functools import partial
from io import BufferedReader, BytesIO
from typing import Callable, Optional, Union

Expand Down Expand Up @@ -304,9 +305,20 @@ def submit_image_query( # noqa: PLR0913 # pylint: disable=too-many-arguments, t
Any pixel format will get converted to JPEG at high quality before sending to service.
:type image: str or bytes or Image.Image or BytesIO or BufferedReader or np.ndarray

:param wait: How long to wait (in seconds) for a confident answer.
:param wait: How long to poll (in seconds) for a confident answer. This is a client-side timeout.
:type wait: float

:param patience_time: How long to wait (in seconds) for a confident answer for this image query.
The longer the patience_time, the more likely Groundlight will arrive at a confident answer.
Within patience_time, Groundlight will update ML predictions based on stronger findings,
and, additionally, Groundlight will prioritize human review of the image query if necessary.
This is a soft server-side timeout. If not set, use the detector's patience_time.
:type patience_time: float

:param confidence_threshold: The confidence threshold to wait for.
If not set, use the detector's confidence threshold.
:type confidence_threshold: float

:param human_review: If `None` or `DEFAULT`, send the image query for human review
only if the ML prediction is not confident.
If set to `ALWAYS`, always send the image query for human review.
Expand Down Expand Up @@ -375,8 +387,10 @@ def ask_confident(
confidence_threshold: Optional[float] = None,
wait: Optional[float] = None,
) -> ImageQuery:
"""Evaluates an image with Groundlight waiting until an answer above the confidence threshold
of the detector is reached or the wait period has passed.
"""
Evaluates an image with Groundlight waiting until an answer above the confidence threshold
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of the detector is reached or the wait period has passed.

:param detector: the Detector object, or string id of a detector like `det_12345`
:type detector: Detector or str

Expand Down Expand Up @@ -405,6 +419,8 @@ def ask_confident(
image,
confidence_threshold=confidence_threshold,
wait=wait,
patience_time=wait,
human_review=None,
)

def ask_ml(
Expand All @@ -413,7 +429,9 @@ def ask_ml(
image: Union[str, bytes, Image.Image, BytesIO, BufferedReader, np.ndarray],
wait: Optional[float] = None,
) -> ImageQuery:
"""Evaluates an image with Groundlight, getting the first answer Groundlight can provide.
"""
Evaluates an image with Groundlight, getting the first answer Groundlight can provide.

:param detector: the Detector object, or string id of a detector like `det_12345`
:type detector: Detector or str

Expand Down Expand Up @@ -443,12 +461,13 @@ def ask_ml(
wait = self.DEFAULT_WAIT if wait is None else wait
return self.wait_for_ml_result(iq, timeout_sec=wait)

def ask_async(
def ask_async( # noqa: PLR0913 # pylint: disable=too-many-arguments
self,
detector: Union[Detector, str],
image: Union[str, bytes, Image.Image, BytesIO, BufferedReader, np.ndarray],
patience_time: Optional[float] = None,
confidence_threshold: Optional[float] = None,
human_review: Optional[str] = None,
inspection_id: Optional[str] = None,
) -> ImageQuery:
"""
Convenience method for submitting an `ImageQuery` asynchronously. This is equivalent to calling
Expand All @@ -469,6 +488,17 @@ def ask_async(

:type image: str or bytes or Image.Image or BytesIO or BufferedReader or np.ndarray

:param patience_time: How long to wait (in seconds) for a confident answer for this image query.
The longer the patience_time, the more likely Groundlight will arrive at a confident answer.
Within patience_time, Groundlight will update ML predictions based on stronger findings,
and, additionally, Groundlight will prioritize human review of the image query if necessary.
This is a soft server-side timeout. If not set, use the detector's patience_time.
:type patience_time: float

:param confidence_threshold: The confidence threshold to wait for.
If not set, use the detector's confidence threshold.
:type confidence_threshold: float

Comment on lines +491 to +501
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I absolutely love this description of patience time

:param human_review: If `None` or `DEFAULT`, send the image query for human review
only if the ML prediction is not confident.
If set to `ALWAYS`, always send the image query for human review.
Expand Down Expand Up @@ -500,26 +530,34 @@ def ask_async(
assert image_query.id is not None

# Do not attempt to access the result of this query as the result for all async queries
# will be None. Your result is being computed asynchronously and will be available
# later
# will be None. Your result is being computed asynchronously and will be available later
assert image_query.result is None

# retrieve the result later or on another machine by calling gl.get_image_query()
# with the id of the image_query above
image_query = gl.get_image_query(image_query.id)
# retrieve the result later or on another machine by calling gl.wait_for_confident_result()
# with the id of the image_query above. This will block until the result is available.
image_query = gl.wait_for_confident_result(image_query.id)

# now the result will be available for your use
assert image_query.result is not None

# alternatively, you can check if the result is available (without blocking) by calling
# gl.get_image_query() with the id of the image_query above.
image_query = gl.get_image_query(image_query.id)
"""
return self.submit_image_query(
detector, image, wait=0, human_review=human_review, want_async=True, inspection_id=inspection_id
detector,
image,
wait=0,
patience_time=patience_time,
confidence_threshold=confidence_threshold,
human_review=human_review,
want_async=True,
)

def wait_for_confident_result(
self,
image_query: Union[ImageQuery, str],
confidence_threshold: float,
confidence_threshold: Optional[float] = None,
timeout_sec: float = 30.0,
) -> ImageQuery:
"""
Expand All @@ -529,7 +567,8 @@ def wait_for_confident_result(
:param image_query: An ImageQuery object to poll
:type image_query: ImageQuery or str

:param confidence_threshold: The minimum confidence level required to return before the timeout.
:param confidence_threshold: The confidence threshold to wait for.
If not set, use the detector's confidence threshold.
:type confidence_threshold: float

:param timeout_sec: The maximum number of seconds to wait.
Expand All @@ -538,10 +577,12 @@ def wait_for_confident_result(
:return: ImageQuery
:rtype: ImageQuery
"""
if confidence_threshold is None:
if isinstance(image_query, str):
image_query = self.get_image_query(image_query)
confidence_threshold = self.get_detector(image_query.detector_id).confidence_threshold
Comment on lines +580 to +583
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my only concern here is that this introduces another request from the server. I recall @robotrapta suggesting that we should try to minimize this to minimize overall latency, but maybe I am mistaken. I am guessing that the alternative would be to force the user to provide the detector as an argument to the function which is kinda ugly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually call get_image_query if image_query is a string as the very first step in _wait_for_result, so this just brings the call forwards a bit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah ok nice


def confidence_above_thresh(iq):
return iq_is_confident(iq, confidence_threshold=confidence_threshold)

confidence_above_thresh = partial(iq_is_confident, confidence_threshold=confidence_threshold)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for teaching me about partial - this is very haskell-y.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha its honestly really nice sometimes. I love it because its a bit of a functional holdover

return self._wait_for_result(image_query, condition=confidence_above_thresh, timeout_sec=timeout_sec)

def wait_for_ml_result(self, image_query: Union[ImageQuery, str], timeout_sec: float = 30.0) -> ImageQuery:
Expand All @@ -551,9 +592,6 @@ def wait_for_ml_result(self, image_query: Union[ImageQuery, str], timeout_sec: f
:param image_query: An ImageQuery object to poll
:type image_query: ImageQuery or str

:param confidence_threshold: The minimum confidence level required to return before the timeout.
:type confidence_threshold: float

:param timeout_sec: The maximum number of seconds to wait.
:type timeout_sec: float

Expand Down Expand Up @@ -623,6 +661,7 @@ def add_label(self, image_query: Union[ImageQuery, str], label: Union[Label, str
else:
image_query_id = str(image_query)
# Some old imagequery id's started with "chk_"
# TODO: handle iqe_ for image_queries returned from edge endpoints
if not image_query_id.startswith(("chk_", "iq_")):
raise ValueError(f"Invalid image query id {image_query_id}")
api_label = convert_display_label_to_internal(image_query_id, label)
Expand Down
Loading