Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ba08a2e
Adding section on edge to User Guide.
robotrapta Oct 7, 2022
4b37837
Adding a partial `get_or_create_detector` method to SDK.
robotrapta Oct 7, 2022
6f0f7c5
Doesn't fail silently if pagination would be needed.
robotrapta Oct 7, 2022
a06183b
submit_image_query can accept a detector or a detector_id
robotrapta Oct 7, 2022
f0322d3
Consolidating the code samples into a single snippet.
robotrapta Oct 7, 2022
f221cd1
format udpates
robotrapta Oct 7, 2022
52e0517
Wordsmithing the user guide.
robotrapta Oct 7, 2022
e17496d
Accepts numpy arrays as images into imagequery
robotrapta Oct 7, 2022
ba9e69c
Improving interface and docs based on feedback.
robotrapta Oct 8, 2022
b49a13f
Applying black auto-format by hand.
robotrapta Nov 12, 2022
573a76a
Merge remote-tracking branch 'origin/main' into numpy-images
robotrapta Nov 12, 2022
9a1e744
Making PIL optional also.
robotrapta Nov 12, 2022
098d0a8
WIP fixing type hints
robotrapta Nov 12, 2022
d856739
Adding some tests on the optional_import functionality
robotrapta Nov 12, 2022
1f148df
Tests numpy submit image functionality when possible.
robotrapta Nov 12, 2022
3765348
Automatically reformatting code with black
Nov 12, 2022
080858d
See what happens if we try the numpy test all the time.
robotrapta Nov 12, 2022
f1895d1
Don't fail fast so we can see what's happening across the matrix.
robotrapta Nov 12, 2022
4f18886
black
robotrapta Nov 12, 2022
d775494
trying to get optional libraries to install properly in test matrix.
robotrapta Nov 12, 2022
4fe4ef4
fixing commented out error.
robotrapta Nov 12, 2022
bbb125e
Writing some unit tests for numpy/jpeg functionality
robotrapta Nov 12, 2022
5c2c547
Fixing import
robotrapta Nov 12, 2022
43e2f51
Fixes closed buffer problem
robotrapta Nov 12, 2022
f2fb3e4
black
robotrapta Nov 12, 2022
7a2c1f4
fixing bytesio / bytes messup
robotrapta Nov 12, 2022
2fca73b
Test verification
robotrapta Nov 12, 2022
c5956b3
Fixing import check
robotrapta Nov 12, 2022
1247262
Marking unpassable tests as skip
robotrapta Nov 12, 2022
eeb2500
I think hope maybe we're done?
robotrapta Nov 12, 2022
ff75013
black
robotrapta Nov 12, 2022
68440ce
just some comments on a failing test.
robotrapta Nov 12, 2022
32f824f
Bumping version to 0.6.1
robotrapta Nov 14, 2022
f45cae9
Merge remote-tracking branch 'origin' into numpy-images
robotrapta Nov 14, 2022
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
21 changes: 20 additions & 1 deletion .github/workflows/test-integ.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ jobs:
run-tests:
runs-on: ubuntu-20.04
strategy:
fail-fast: true
# It's totally debatable which is better here: fail-fast or not.
# Failing fast will use fewer cloud resources, in theory.
# But if the tests are slightly flaky (fail to pip install something)
# Then one flaky install kills lots of jobs that need to be redone.
# So the efficiency argument has its limits
# Failing slow is clearer about what's going on.
# This is pretty unambiguous, so we're going with it for now.
fail-fast: false
Copy link
Member

Choose a reason for hiding this comment

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

I agree! Diagnosing a failed run quickly is much more important than seeing a failed run quickly.

matrix:
python-version: [
#"3.6", # Default on Ubuntu18.04 but openapi-generator fails
Expand All @@ -15,6 +22,8 @@ jobs:
"3.10",
"3.11",
]
install_numpy: [ true, false ]
install_pillow: [ true, false ]
env:
# This is associated with the "sdk-integ-test" user, credentials on 1password
GROUNDLIGHT_API_TOKEN: ${{ secrets.GROUNDLIGHT_API_TOKEN }}
Expand All @@ -32,5 +41,15 @@ jobs:
pip install -U pip
pip install poetry
poetry install
- name: setup environment
run: make install
- name: install numpy
if: matrix.install_numpy
run: |
poetry run pip install numpy
- name: install pillow
if: matrix.install_pillow
run: |
poetry run pip install pillow
Comment on lines +46 to +53
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the poetry-native way to accomplish this would be "optional groups" (In v1.2+ https://python-poetry.org/docs/managing-dependencies/#optional-groups, or "extras" in v1.1: https://python-poetry.org/docs/1.1/pyproject#extras).

- name: run tests
run: make test-integ
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "groundlight"
version = "0.6.0"
version = "0.6.1"
license = "MIT"
readme = "UserGuide.md"
homepage = "https://groundlight.ai"
Expand Down
9 changes: 6 additions & 3 deletions src/groundlight/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from openapi_client.api.image_queries_api import ImageQueriesApi
from openapi_client.model.detector_creation_input import DetectorCreationInput

from groundlight.images import buffer_from_jpeg_file
from groundlight.images import buffer_from_jpeg_file, jpeg_from_numpy
from groundlight.optional_imports import np

API_TOKEN_WEB_URL = "https://app.groundlight.ai/reef/my-account/api-tokens"
API_TOKEN_VARIABLE_NAME = "GROUNDLIGHT_API_TOKEN"
Expand Down Expand Up @@ -113,7 +114,7 @@ def list_image_queries(self, page: int = 1, page_size: int = 10) -> PaginatedIma
def submit_image_query(
self,
detector: Union[Detector, str],
image: Union[str, bytes, BytesIO, BufferedReader],
image: Union[str, bytes, BytesIO, BufferedReader, np.ndarray],
wait: float = 0,
) -> ImageQuery:
"""Evaluates an image with Groundlight.
Expand All @@ -139,9 +140,11 @@ def submit_image_query(
elif isinstance(image, BytesIO) or isinstance(image, BufferedReader):
# Already in the right format
image_bytesio = image
elif isinstance(image, np.ndarray):
image_bytesio = BytesIO(jpeg_from_numpy(image))
else:
raise TypeError(
"Unsupported type for image. We only support JPEG images specified through a filename, bytes, BytesIO, or BufferedReader object."
"Unsupported type for image. We only support numpy arrays (3,W,H) or JPEG images specified through a filename, bytes, BytesIO, or BufferedReader object."
Copy link
Member

Choose a reason for hiding this comment

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

is there a world where we expose an error with "np" to the end user? It might be better to be explicit (numpy) everywhere. As someone might not know that np==numpy.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it's pretty universal to import numpy as np.

)

raw_img_query = self.image_queries_api.submit_image_query(detector_id=detector_id, body=image_bytesio)
Expand Down
12 changes: 12 additions & 0 deletions src/groundlight/images.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import imghdr
import io

from groundlight.optional_imports import np, Image


def buffer_from_jpeg_file(image_filename: str) -> io.BufferedReader:
"""
Expand All @@ -14,3 +16,13 @@ def buffer_from_jpeg_file(image_filename: str) -> io.BufferedReader:
return open(image_filename, "rb")
else:
raise ValueError("We only support JPEG files, for now.")


def jpeg_from_numpy(img: np.ndarray, jpeg_quality: int = 95) -> bytes:
"""Converts a numpy array to BytesIO"""
pilim = Image.fromarray(img.astype("uint8"), "RGB")
with io.BytesIO() as buf:
buf = io.BytesIO()
pilim.save(buf, "jpeg", quality=jpeg_quality)
out = buf.getvalue()
return out
51 changes: 51 additions & 0 deletions src/groundlight/optional_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""We use a trick to check if libraries like numpy are installed or not.
If they are, we make it available as normal.
If not, we set it up as a shim object which still lets type-hinting work properly,
but will fail at runtime if you try to use it.

This can be confusing, but hopefully the errors are explicit enough to be
clear about what's happening, and it makes the code which hopes numpy is installed
look readable.
"""


class UnavailableModule(type):
"""Represents a module that is not installed or otherwise unavailable at runtime.
Attempting to access anything in this object raises the original exception
(ImportError or similar) which happened when the optional library failed to import.

Needs to subclass type so that it works for type-hinting.
"""

def __new__(cls, exc):
out = type("UnavailableModule", (), {})
out.exc = exc
return out

def __getattr__(self, key):
# TODO: This isn't getting called for some reason.
raise RuntimeError("attempt to use module that failed to load") from self.exc


try:
import numpy as np

MISSING_NUMPY = False
except ImportError as e:
np = UnavailableModule(e)
# Expose np.ndarray so type-hinting looks normal
np.ndarray = np
MISSING_NUMPY = True

try:
import PIL
from PIL import Image

MISSING_PIL = False
except ImportError as e:
PIL = UnavailableModule(e)
Image = PIL
MISSING_PIL = True


__all__ = ["np", "PIL", "Image", "MISSING_NUMPY", "MISSING_PIL"]
9 changes: 9 additions & 0 deletions test/integration/test_groundlight.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest

from groundlight import Groundlight
from groundlight.optional_imports import *
from model import Detector, ImageQuery, PaginatedDetectorList, PaginatedImageQueryList


Expand Down Expand Up @@ -109,3 +110,11 @@ def test_get_image_query(gl: Groundlight, image_query: ImageQuery):
_image_query = gl.get_image_query(id=image_query.id)
assert str(_image_query)
assert isinstance(_image_query, ImageQuery)


@pytest.mark.skipif(MISSING_NUMPY or MISSING_PIL, reason="Needs numpy and pillow")
def test_submit_numpy_image(gl: Groundlight, detector: Detector):
np_img = np.random.uniform(0, 255, (600, 800, 3))
_image_query = gl.submit_image_query(detector=detector.id, image=np_img)
assert str(_image_query)
assert isinstance(_image_query, ImageQuery)
19 changes: 19 additions & 0 deletions test/unit/test_imagefuncs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest

from groundlight.images import *
from groundlight.optional_imports import *


@pytest.mark.skipif(MISSING_NUMPY or MISSING_PIL, reason="Needs numpy and pillow")
def test_jpeg_from_numpy():
np_img = np.random.uniform(0, 255, (480, 640, 3))
jpeg1 = jpeg_from_numpy(np_img)
assert len(jpeg1) > 500

np_img = np.random.uniform(0, 255, (768, 1024, 3))
jpeg2 = jpeg_from_numpy(np_img)
assert len(jpeg2) > len(jpeg1)

np_img = np.random.uniform(0, 255, (768, 1024, 3))
jpeg3 = jpeg_from_numpy(np_img, jpeg_quality=50)
assert len(jpeg2) > len(jpeg3)
29 changes: 29 additions & 0 deletions test/unit/test_optional_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Union

import pytest

from groundlight.optional_imports import UnavailableModule


@pytest.fixture
def failed_import() -> type:
e = ModuleNotFoundError("perfect_perception module does not exist")
return UnavailableModule(e)


def test_type_hints(failed_import):
# Check that the UnavailableModule class can be used in type hints.
def typed_method(foo: Union[failed_import, str]):
print(foo)

assert True, "Yay UnavailableModule can be used in a type hint"


@pytest.mark.skip("Would be nice if this works, but it doesn't")
def test_raises_exception(failed_import):
# We'd like the UnavailableModule object to raise an exception
# anytime you access it, where the exception is a RuntimeError
# but builds on the original ImportError so you can see what went wrong.
# The old version had this, but didn't work with modern type-hinting.
with pytest.raises(RuntimeError):
failed_import.foo