Skip to content

Commit 4e94f99

Browse files
author
Tim Huff
committed
addressing PR feedback
1 parent ed61b1e commit 4e94f99

5 files changed

Lines changed: 26 additions & 29 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ packages = [
99
{include = "**/*.py", from = "src"},
1010
]
1111
readme = "README.md"
12-
version = "0.28.0"
12+
version = "0.29.0"
1313

1414
[tool.poetry.dependencies]
1515
# For certifi, use ">=" instead of "^" since it upgrades its "major version" every year, not really following semver

src/groundlight/client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from groundlight.binary_labels import Label, convert_internal_label_to_display
4444
from groundlight.config import API_TOKEN_MISSING_HELP_MESSAGE, API_TOKEN_VARIABLE_NAME, DISABLE_TLS_VARIABLE_NAME
4545
from groundlight.encodings import url_encode_dict
46-
from groundlight.images import ByteStreamWrapper, parse_supported_image_types, recompress_shrink_image
46+
from groundlight.images import ByteStreamWrapper, parse_supported_image_types, shrink_image_if_needed
4747
from groundlight.internalapi import (
4848
GroundlightApiClient,
4949
NotFoundError,
@@ -799,10 +799,10 @@ def submit_image_query( # noqa: PLR0913 # pylint: disable=too-many-arguments, t
799799
detector_id = detector.id if isinstance(detector, Detector) else detector
800800

801801
image_bytesio: ByteStreamWrapper = parse_supported_image_types(image)
802-
# Match the cloud's ingest pipeline locally. Saves bandwidth and ensures Edge
803-
# Endpoints, which do not run this step, see the same input distribution
804-
# cloud-trained models were trained on.
805-
image_bytesio = ByteStreamWrapper(data=recompress_shrink_image(image_bytesio.read()))
802+
# Match the Groundlight cloud service's ingest pipeline locally. Saves bandwidth
803+
# and ensures Edge Endpoints, which do not run this step, see the same input
804+
# distribution cloud-trained models were trained on.
805+
image_bytesio = ByteStreamWrapper(data=shrink_image_if_needed(image_bytesio.read()))
806806

807807
params = {
808808
"detector_id": detector_id,

src/groundlight/images.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,14 @@
77

88
DEFAULT_JPEG_QUALITY = 95
99

10-
# The Groundlight cloud applies a recompress/shrink step on ingest. Doing the same
11-
# work client-side saves bandwidth and ensures Edge Endpoints, which do not run
12-
# this step, see the same input distribution that cloud-trained models expect.
13-
#
14-
# The constants and algorithm below mirror zuuul's implementation. Source of truth:
15-
# - zuuul/janzu/apparati/imgtools.py::recompress_shrink_image
16-
# - zuuul/janzu/reef_api/utils.py::_save_image (gate)
17-
# - zuuul/janzu/authz/user-settings-defaults.yaml (default values)
18-
# If the cloud's behavior changes, update these together.
10+
# The Groundlight cloud service applies the same shrink-and-re-encode step on
11+
# ingest. Doing the same work client-side saves bandwidth and ensures Edge
12+
# Endpoints, which do not run this step, see the same input distribution that
13+
# cloud-trained models expect. Keep these constants in sync with the cloud
14+
# service if it ever changes its defaults.
1915
MAX_BYTES_IMAGE_SIZE = 256_000
2016
MAX_IMAGE_RESOLUTION_LONGSIDE = 1024
21-
RECOMPRESS_SHRINK_IMAGE_JPEG_QUALITY = 85
17+
SHRINK_JPEG_QUALITY = 85
2218

2319

2420
class ByteStreamWrapper(IOBase):
@@ -91,8 +87,8 @@ def bytestream_from_pil(pil_image: Image.Image, jpeg_quality: int = DEFAULT_JPEG
9187
return ByteStreamWrapper(data=bytesio)
9288

9389

94-
def recompress_shrink_image(jpeg: bytes) -> bytes:
95-
"""Shrink and re-encode an oversized JPEG to match the cloud's ingest pipeline.
90+
def shrink_image_if_needed(jpeg: bytes) -> bytes:
91+
"""Shrink an oversized JPEG to match the Groundlight cloud service's ingest pipeline.
9692
9793
If the input is already at or below MAX_BYTES_IMAGE_SIZE, returns it unchanged.
9894
Otherwise, decodes the image, scales it (BICUBIC, aspect-ratio preserved) so the
@@ -109,7 +105,7 @@ def recompress_shrink_image(jpeg: bytes) -> bytes:
109105
new_size = (int(img.width * ratio), int(img.height * ratio))
110106
img = img.resize(new_size, resample=Image.Resampling.BICUBIC)
111107
buf = BytesIO()
112-
img.save(buf, "jpeg", quality=RECOMPRESS_SHRINK_IMAGE_JPEG_QUALITY)
108+
img.save(buf, "jpeg", quality=SHRINK_JPEG_QUALITY)
113109
return buf.getvalue()
114110

115111

test/integration/test_groundlight.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,10 +374,11 @@ def test_submit_image_query_png(gl: Groundlight, detector: Detector):
374374
def test_submit_image_query_shrinks_oversized_image(gl: Groundlight, detector: Detector):
375375
"""Verifies the SDK shrinks oversized images client-side and the cloud stores the shrunken version.
376376
377-
Detects drift between the SDK and zuuul: if either side changes its algorithm such that
378-
the cloud-stored dimensions differ from what the SDK produces locally, this test fails.
379-
Does not catch zuuul becoming more permissive (the SDK would still shrink to a smaller
380-
image that zuuul accepts as-is); that direction is benign and intentionally not covered.
377+
Detects drift between the SDK and the cloud service: if either side changes its
378+
algorithm such that the cloud-stored dimensions differ from what the SDK produces
379+
locally, this test fails. Does not catch the cloud service becoming more permissive
380+
(the SDK would still shrink to a smaller image that the cloud accepts as-is); that
381+
direction is benign and intentionally not covered.
381382
"""
382383
np.random.seed(0)
383384
# Random noise compresses poorly, so 3000x4000 is well above the 256 KB threshold.

test/unit/test_imagefuncs.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,35 +91,35 @@ def test_pil_support_ref():
9191

9292

9393
@pytest.mark.skipif(MISSING_NUMPY or MISSING_PIL, reason="Needs numpy and pillow") # type: ignore
94-
def test_recompress_shrink_image_small_returns_unchanged():
94+
def test_shrink_image_if_needed_small_returns_unchanged():
9595
"""Images at or below the byte threshold are passed through untouched."""
9696
np.random.seed(0)
9797
small = jpeg_from_numpy(np.random.uniform(0, 255, (200, 200, 3)))
9898
assert len(small) <= MAX_BYTES_IMAGE_SIZE
99-
assert recompress_shrink_image(small) is small
99+
assert shrink_image_if_needed(small) is small
100100

101101

102102
@pytest.mark.skipif(MISSING_NUMPY or MISSING_PIL, reason="Needs numpy and pillow") # type: ignore
103-
def test_recompress_shrink_image_oversized_dimensions_get_resized():
103+
def test_shrink_image_if_needed_oversized_dimensions_get_resized():
104104
"""Images above the byte threshold with longest side > 1024 are downscaled."""
105105
np.random.seed(0)
106106
# Random noise compresses poorly, so 3000x4000 easily exceeds the 256 KB threshold.
107107
big = jpeg_from_numpy(np.random.uniform(0, 255, (3000, 4000, 3)))
108108
assert len(big) > MAX_BYTES_IMAGE_SIZE
109-
out = recompress_shrink_image(big)
109+
out = shrink_image_if_needed(big)
110110
out_img = Image.open(BytesIO(out))
111111
# 3000x4000 scaled so longest side == 1024 preserves the 3:4 aspect ratio.
112112
assert out_img.size == (1024, 768)
113113

114114

115115
@pytest.mark.skipif(MISSING_NUMPY or MISSING_PIL, reason="Needs numpy and pillow") # type: ignore
116-
def test_recompress_shrink_image_oversized_bytes_only_gets_reencoded():
116+
def test_shrink_image_if_needed_oversized_bytes_only_gets_reencoded():
117117
"""Images above the byte threshold but with longest side <= 1024 are re-encoded only."""
118118
np.random.seed(0)
119119
arr = np.random.uniform(0, 255, (768, 1024, 3))
120120
high_q = jpeg_from_numpy(arr, jpeg_quality=99)
121121
assert len(high_q) > MAX_BYTES_IMAGE_SIZE
122-
out = recompress_shrink_image(high_q)
122+
out = shrink_image_if_needed(high_q)
123123
out_img = Image.open(BytesIO(out))
124124
assert out_img.size == (1024, 768)
125125
# Bytes changed (proves re-encode happened) and got smaller (Q85 vs Q99).

0 commit comments

Comments
 (0)