PNG Capture

This adds an additional action to BaseCamera to capture and return a PNG Blob. It makes the PNG using Pillow, in a very similar way to `capture_jpeg`.

I have parametrized the test for `capture_jpeg` so it now tests both jpeg and png with the same code.
This commit is contained in:
Richard Bowman 2026-04-22 23:22:11 +01:00 committed by Julian Stirling
parent 59a93c673a
commit ee6911f4a7
2 changed files with 50 additions and 15 deletions

View file

@ -437,13 +437,44 @@ class BaseCamera(OFMThing):
capture_metadata = self._capture_metadata()
self._save_capture(
jpeg_path=jpeg_path,
path=jpeg_path,
image=img,
metadata=capture_metadata,
)
return JPEGBlob.from_temporary_directory(directory, fname)
@lt.action
def capture_png(
self,
stream_name: Literal["main", "lores", "full"] = "main",
wait: Optional[float] = None,
) -> PNGBlob:
"""Acquire one image from the camera as a PNG.
This will use the internal capture image functionally of capture_image of
the specific camera being used.
:param stream_name: A stream name supported by this camera.
:param wait: (Optional, float) Set a timeout in seconds. If None it will
use the default for the underlying camera.
"""
fname = datetime.now().strftime("%Y-%m-%d-%H%M%S.jpeg")
directory = tempfile.TemporaryDirectory()
png_path = os.path.join(directory.name, fname)
img = self.capture_image(stream_name, wait)
capture_metadata = self._capture_metadata()
self._save_capture(
path=png_path,
image=img,
metadata=capture_metadata,
)
return PNGBlob.from_temporary_directory(directory, fname)
@lt.action
def grab_jpeg(
self,
@ -656,7 +687,7 @@ class BaseCamera(OFMThing):
def _save_capture(
self,
jpeg_path: str,
path: str,
image: Image.Image,
metadata: Mapping[str, Any],
save_resolution: Optional[Tuple[int, int]] = None,
@ -672,22 +703,25 @@ class BaseCamera(OFMThing):
if save_resolution is not None and image.size != save_resolution:
image = image.resize(save_resolution, Image.Resampling.BOX)
try:
# Per PIL documentation,
# (https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#jpeg)
# there are two factors when saving a JPEG. Subsampling affects the colour,
# quality affects the pixels.
# subsampling = 0 disables subsampling of colour
# quality = 95 is the maximum recommended - above this, JPEG compression is
# disabled, file size increases and quality is barely or not affected
image.save(jpeg_path, quality=95, subsampling=0)
save_kwargs: dict[str, Any] = {}
if path.endswith("jpg"):
# Per PIL documentation,
# (https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#jpeg)
# there are two factors when saving a JPEG. Subsampling affects the colour,
# quality affects the pixels.
# subsampling = 0 disables subsampling of colour
# quality = 95 is the maximum recommended - above this, JPEG compression is
# disabled, file size increases and quality is barely or not affected
save_kwargs = {"quality": 95, "subsampling": 0}
image.save(path, **save_kwargs)
try:
self._add_metadata_to_capture(jpeg_path, dict(metadata))
self._add_metadata_to_capture(path, dict(metadata))
except Exception:
# We need to capture any exception as there are many reasons metadata
# might not be added. We warn rather than log the error.
self.logger.exception(f"Failed to add metadata to {jpeg_path}")
self.logger.exception(f"Failed to add metadata to {path}")
except Exception as e:
raise IOError(f"An error occurred while saving {jpeg_path}") from e
raise IOError(f"An error occurred while saving {path}") from e
settling_time: float = lt.setting(default=0.2, ge=0)
"""The settling time when calling the ``settle()`` method."""

View file

@ -40,10 +40,11 @@ def test_grab_jpeg(simulation_test_env):
assert image.size == (820, 616)
def test_capture_jpeg_metadata(simulation_test_env):
@pytest.mark.parametrize("fmt", ["jpeg", "png"])
def test_capture_and_metadata(simulation_test_env, fmt):
"""Check that the position is encoded into the image metadata."""
camera = simulation_test_env.get_thing_client("camera")
blob = camera.capture_jpeg()
blob = getattr(camera, f"capture_{fmt}")()
image = Image.open(blob.open())
exif_dict = piexif.load(image.info["exif"])
encoded_metadata = exif_dict["Exif"][piexif.ExifIFD.UserComment]