From 501988417531f2f9f6c7f379df6acfe159fb9dd2 Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Tue, 3 Mar 2026 13:23:30 +0000 Subject: [PATCH 1/7] Camera metadata includes subclass name, and test --- .../things/camera/__init__.py | 8 +- .../things/camera/picamera.py | 5 + tests/conftest.py | 24 +++ tests/unit_tests/test_cameras.py | 24 --- tests/unit_tests/test_metadata.py | 180 ++++++++++++++++++ 5 files changed, 215 insertions(+), 26 deletions(-) create mode 100644 tests/unit_tests/test_metadata.py diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index f656ed1b..802a1718 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -643,8 +643,12 @@ class BaseCamera(lt.Thing): @property def thing_state(self) -> Mapping[str, Any]: - """Empty metadata dict for subclasses to populate.""" - return {} + """Return camera-specific metadata. + + By default, this just adds the subclass name as the camera type. + Subclasses can extend by overriding this property and calling `super()`. + """ + return {"camera": self.__class__.__name__} def downsample(factor: int, image: np.ndarray) -> np.ndarray: diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 901a1c60..2e845558 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -927,4 +927,9 @@ class StreamingPiCamera2(BaseCamera): """Update generic camera metadata with Picamera-specific data.""" state = dict(super().thing_state) state["camera_board"] = self._camera_board + state["tuning"] = { + "exposure_time": self.exposure_time, + "colour_gains": self.colour_gains, + "analogue_gain": self.analogue_gain, + } return state diff --git a/tests/conftest.py b/tests/conftest.py index a7d77bfe..68d6a483 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,8 @@ from typing import Optional import pytest +from labthings_fastapi.testing import create_thing_without_server + @pytest.fixture def check_side_effect(caplog): @@ -61,3 +63,25 @@ def check_side_effect(caplog): assert re.match(match, record.message) is not None return _checker + + +@pytest.fixture +def mock_picam_thing(mocker): + """Import PiCamera without hardware well enough to get a ThingDescription.""" + dummy_cam = mocker.Mock() + mock_picamera2 = mocker.MagicMock() + mock_picamera2.return_value.__enter__.return_value = dummy_cam + mock_picamera2.return_value.__exit__.return_value = None + + mocker.patch.dict( + "sys.modules", + { + "picamera2": mock_picamera2, + "picamera2.encoders": mocker.Mock(), + "picamera2.outputs": mocker.Mock(), + }, + ) + + from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2 + + return create_thing_without_server(StreamingPiCamera2) diff --git a/tests/unit_tests/test_cameras.py b/tests/unit_tests/test_cameras.py index 15d42dc0..40d0bf88 100644 --- a/tests/unit_tests/test_cameras.py +++ b/tests/unit_tests/test_cameras.py @@ -4,8 +4,6 @@ Tests for specific camera hardware are in the hardware_specific_tests directory. on camera functionality using the simulation camera are in "test_camera". """ -import pytest - from labthings_fastapi.testing import create_thing_without_server from openflexure_microscope_server.things.camera import BaseCamera @@ -13,28 +11,6 @@ from openflexure_microscope_server.things.camera.opencv import OpenCVCamera from openflexure_microscope_server.things.camera.simulation import SimulatedCamera -@pytest.fixture -def mock_picam_thing(mocker): - """Import PiCamera without hardware well enough to get a ThingDescription.""" - dummy_cam = mocker.Mock() - mock_picamera2 = mocker.MagicMock() - mock_picamera2.return_value.__enter__.return_value = dummy_cam - mock_picamera2.return_value.__exit__.return_value = None - - mocker.patch.dict( - "sys.modules", - { - "picamera2": mock_picamera2, - "picamera2.encoders": mocker.Mock(), - "picamera2.outputs": mocker.Mock(), - }, - ) - - from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2 - - return create_thing_without_server(StreamingPiCamera2) - - def _get_clean_camera_description(camera_thing): """Return actions and properties for a camera Thing, separating those exposed to the UI. diff --git a/tests/unit_tests/test_metadata.py b/tests/unit_tests/test_metadata.py new file mode 100644 index 00000000..02edfcbb --- /dev/null +++ b/tests/unit_tests/test_metadata.py @@ -0,0 +1,180 @@ +"""Tests that captures have the expected metadata.""" + +import json +from datetime import datetime +from pathlib import Path + +import piexif +import pytest +from PIL import Image + +import labthings_fastapi as lt +from labthings_fastapi.testing import create_thing_without_server + +from openflexure_microscope_server.things.background_detect import ChannelDeviationLUV +from openflexure_microscope_server.things.camera import BaseCamera +from openflexure_microscope_server.things.camera.simulation import SimulatedCamera +from openflexure_microscope_server.things.stage.dummy import DummyStage + +from ..shared_utils.lt_test_utils import LabThingsTestEnv + + +@pytest.fixture +def temp_jpeg(tmp_path: Path) -> Path: + """Create a temporary blank JPEG image.""" + jpeg_path = tmp_path / "test.jpg" + image = Image.new("RGB", (10, 10), color="white") + image.save(jpeg_path, "jpeg") + return jpeg_path + + +def test_add_metadata_to_capture(temp_jpeg): + """Use a BaseCamera to add metadata to a tmp capture and test fields.""" + base_cam = create_thing_without_server(BaseCamera) + metadata = { + "Dummy1": 1, + "Dummy2": "two", + } + + current_time = datetime.now() + capture_metadata = { + "capture_time": current_time.timestamp(), + "timezone": current_time.astimezone().utcoffset(), + "make": "OpenFlexure", + "model": "OpenFlexure Microscope", + "things_states": metadata, + } + + base_cam._add_metadata_to_capture(str(temp_jpeg), capture_metadata) + + # Reload EXIF + exif_dict = piexif.load(str(temp_jpeg)) + + # Assert UserComment + user_comment_raw = exif_dict["Exif"][piexif.ExifIFD.UserComment] + assert json.loads(user_comment_raw.decode("utf-8")) == metadata + + # Assert timestamps + expected_time_str = datetime.fromtimestamp( + capture_metadata["capture_time"] + ).strftime("%Y:%m:%d %H:%M:%S") + + assert ( + exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal].decode() == expected_time_str + ) + + assert ( + exif_dict["Exif"][piexif.ExifIFD.DateTimeDigitized].decode() + == expected_time_str + ) + + assert exif_dict["0th"][piexif.ImageIFD.DateTime].decode() == expected_time_str + + # Assert timezone offset + offset_original = exif_dict["Exif"][piexif.ExifIFD.OffsetTimeOriginal].decode() + offset_digitized = exif_dict["Exif"][piexif.ExifIFD.OffsetTimeDigitized].decode() + + tz = capture_metadata["timezone"] + hours = int(tz.total_seconds() // 3600) + minutes = int((abs(tz.total_seconds()) % 3600) // 60) + sign = "+" if hours >= 0 else "-" + expected_offset = f"{sign}{abs(hours):02d}:{minutes:02d}" + + assert offset_original == expected_offset + assert offset_digitized == expected_offset + + # Assert Make and Model + assert exif_dict["0th"][piexif.ImageIFD.Make].decode() == capture_metadata["make"] + assert exif_dict["0th"][piexif.ImageIFD.Model].decode() == capture_metadata["model"] + + +@pytest.fixture +def test_env() -> LabThingsTestEnv: + """Yield a test environment with the Simulated Camera and Dummy Stage.""" + thing_conf = { + "camera": SimulatedCamera, + "stage": DummyStage, + "bg_channel_deviations_luv": ChannelDeviationLUV, + } + with LabThingsTestEnv(things=thing_conf) as env: + yield env + + +@pytest.fixture +def camera(test_env) -> lt.Thing: + """Return the SimulatedCamera Thing set up in the test environment.""" + return test_env.get_thing_by_type(SimulatedCamera) + + +def test_add_metadata_to_simulated_capture(camera, temp_jpeg): + """Test metadata from simulated camera includes expected camera_board: simulator.""" + metadata = camera.thing_state + current_time = datetime.now() + capture_metadata = { + "capture_time": current_time.timestamp(), + "timezone": current_time.astimezone().utcoffset(), + "make": "OpenFlexure", + "model": "OpenFlexure Microscope", + "things_states": metadata, + } + + camera._add_metadata_to_capture(str(temp_jpeg), capture_metadata) + + # Reload EXIF + exif_dict = piexif.load(str(temp_jpeg)) + + # Assert UserComment + user_comment_raw = exif_dict["Exif"][piexif.ExifIFD.UserComment] + assert json.loads(user_comment_raw.decode("utf-8")) == metadata + assert json.loads(user_comment_raw.decode("utf-8"))["camera"] == "SimulatedCamera" + + +def test_picamera_adds_metadata(mock_picam_thing): + """Test PiCamera adds camera_board and tuning metadata.""" + camera = mock_picam_thing + + # Inject controlled values + camera._camera_board = "imx219" + camera.exposure_time = 1234 + camera.colour_gains = (1.1, 1.2) + camera.analogue_gain = 2.5 + + state = camera.thing_state + + # Assert metadata has been set + assert state["camera_board"] == "imx219" + assert state["tuning"] == { + "exposure_time": 1234, + "colour_gains": (1.1, 1.2), + "analogue_gain": 2.5, + } + + +def test_picamera_metadata_written_to_exif(mock_picam_thing, temp_jpeg, mocker): + """Ensure PiCamera metadata is written into JPEG EXIF.""" + camera = mock_picam_thing + + camera._camera_board = "imx219" + camera.exposure_time = 1234 + camera.colour_gains = (1.1, 1.2) + camera.analogue_gain = 2.5 + + # Mock the server interface to return the camera's own state + mock_interface = mocker.Mock() + mock_interface.get_thing_states.return_value = camera.thing_state + camera._thing_server_interface = mock_interface + + capture_metadata = camera._capture_metadata() + + camera._add_metadata_to_capture(str(temp_jpeg), capture_metadata) + + exif_dict = piexif.load(str(temp_jpeg)) + user_comment = json.loads(exif_dict["Exif"][piexif.ExifIFD.UserComment].decode()) + + assert user_comment["camera"] == "StreamingPiCamera2" + assert user_comment["camera_board"] == "imx219" + assert user_comment["tuning"] == { + "exposure_time": 1234, + "colour_gains": [1.1, 1.2], + "analogue_gain": 2.5, + } From 17f77e36c4a962a9ad2afe158805433d610662c5 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 3 Mar 2026 13:33:34 +0000 Subject: [PATCH 2/7] Picamera test --- picamera_coverage.zip | Bin 54038 -> 54038 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/picamera_coverage.zip b/picamera_coverage.zip index bdf5503ecd885e733ccf8e901643820229d1de0f..b30b7cb0a3e6ef76b7de0fb2124fd00f75f6fdd0 100644 GIT binary patch delta 493 zcmbQXjCtBJW}yIYW)=|!5O|ZF9CL4Nuhd2%B?o3%PTtAp4hGzAT+EzDIlDOBIC(ca z3Y_L(HJ4>(D4l%JQ*-hLPaZ~@$$LGW8KoyPdRehZF*DRpX7n?dobScOD#^mo$PVVM z^WtT5mSACMa=sTQtCSNXf3eFIxk*UNf(wzZny}Fb0q9p8W|zZ ziCSvHxPS7r{s<;BkIf(Yy&O0|!SmRFYsF-z3uzh_21bd7=7tu@hK6Q_NtUJ-rWQs9 z#%3udCPqdnmZlcwW@bj_W+_RNFI;d|H%hfIF*P-{FiuQKHZV^yF)}hXPE1TSOfs`f zHcYihGc-3aF*Zt>eEx#%GeiAkMn8keT3%eN z5-bdj>|kD|7cZ-wI15ALWXEWO%^$p6Sk+`WgBkeG^Umh^&YQv+%z1&chu4sQ8h00e zFyAvSZoVQudEPTzo||I=9&t>(;6B+d(t=gSnWd3)vSXyy)rhDoWWsb*#triO{;#zw}8rp87lMk&VT1_r6g zsisMWrpA*mTyWN~G%!pwOg1yHG_bHtG&VCbGBh=_G)T3yG)he|F-}WNOSMciwlFiC zeBpx4 Date: Tue, 3 Mar 2026 13:59:43 +0000 Subject: [PATCH 3/7] Add gamma_curve to metadata --- .../things/camera/__init__.py | 2 +- .../things/camera/picamera.py | 6 ++++++ .../camera/picamera_tuning_file_utils.py | 21 +++++++++++++++++++ tests/unit_tests/test_metadata.py | 8 +++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 802a1718..55f2107a 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -646,7 +646,7 @@ class BaseCamera(lt.Thing): """Return camera-specific metadata. By default, this just adds the subclass name as the camera type. - Subclasses can extend by overriding this property and calling `super()`. + Subclasses can extend by overriding this property and calling `super().thing_state`. """ return {"camera": self.__class__.__name__} diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 2e845558..a759ab02 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -718,6 +718,11 @@ class StreamingPiCamera2(BaseCamera): copy_from=self.default_tuning, ) + @lt.property + def gamma_correction(self) -> float: + """Return the gamma correction from the tuning file.""" + return tf_utils.get_gamma_curve(self.tuning) + @lt.action def set_static_green_equalisation(self, offset: int = 65535) -> None: """Set the green equalisation to a static value. @@ -931,5 +936,6 @@ class StreamingPiCamera2(BaseCamera): "exposure_time": self.exposure_time, "colour_gains": self.colour_gains, "analogue_gain": self.analogue_gain, + "gamma_correction": self.gamma_correction, } return state diff --git a/src/openflexure_microscope_server/things/camera/picamera_tuning_file_utils.py b/src/openflexure_microscope_server/things/camera/picamera_tuning_file_utils.py index c6b4f77d..d2d139d9 100644 --- a/src/openflexure_microscope_server/things/camera/picamera_tuning_file_utils.py +++ b/src/openflexure_microscope_server/things/camera/picamera_tuning_file_utils.py @@ -186,6 +186,27 @@ def get_lst(tuning: dict) -> LensShadingModel: ) +def get_gamma_curve(tuning: dict) -> dict[int, float]: + """Return the gamma curve from the rpi.contrast section of the tuning file. + + Returns a dictionary mapping input levels to output levels. + Defaults to {} if gamma curve is missing. + """ + contrast = find_tuning_algo(tuning, "rpi.contrast") + return contrast.get("gamma", {}) + + +def set_gamma_curve(tuning: dict, gamma_curve: dict[int, float]) -> dict: + """Set the gamma curve in the rpi.contrast section of the tuning file. + + Returns a new tuning dictionary with the updated gamma curve. + """ + output_tuning = deepcopy(tuning) + contrast = find_tuning_algo(output_tuning, "rpi.contrast") + contrast["gamma"] = {int(k): float(v) for k, v in gamma_curve.items()} + return output_tuning + + def get_colour_gains_from_lst(tuning: dict) -> tuple[float, float]: """Get the colour gains that are needed from the lens shading tables. diff --git a/tests/unit_tests/test_metadata.py b/tests/unit_tests/test_metadata.py index 02edfcbb..915a5866 100644 --- a/tests/unit_tests/test_metadata.py +++ b/tests/unit_tests/test_metadata.py @@ -13,6 +13,9 @@ from labthings_fastapi.testing import create_thing_without_server from openflexure_microscope_server.things.background_detect import ChannelDeviationLUV from openflexure_microscope_server.things.camera import BaseCamera +from openflexure_microscope_server.things.camera import ( + picamera_tuning_file_utils as tf_utils, +) from openflexure_microscope_server.things.camera.simulation import SimulatedCamera from openflexure_microscope_server.things.stage.dummy import DummyStage @@ -138,6 +141,7 @@ def test_picamera_adds_metadata(mock_picam_thing): camera.exposure_time = 1234 camera.colour_gains = (1.1, 1.2) camera.analogue_gain = 2.5 + camera.tuning = tf_utils.set_gamma_curve(camera.tuning, {1: 5, 2: 10}) state = camera.thing_state @@ -147,6 +151,7 @@ def test_picamera_adds_metadata(mock_picam_thing): "exposure_time": 1234, "colour_gains": (1.1, 1.2), "analogue_gain": 2.5, + "gamma_correction": {1: 5, 2: 10}, } @@ -158,6 +163,7 @@ def test_picamera_metadata_written_to_exif(mock_picam_thing, temp_jpeg, mocker): camera.exposure_time = 1234 camera.colour_gains = (1.1, 1.2) camera.analogue_gain = 2.5 + camera.tuning = tf_utils.set_gamma_curve(camera.tuning, {1: 5, 2: 10}) # Mock the server interface to return the camera's own state mock_interface = mocker.Mock() @@ -173,8 +179,10 @@ def test_picamera_metadata_written_to_exif(mock_picam_thing, temp_jpeg, mocker): assert user_comment["camera"] == "StreamingPiCamera2" assert user_comment["camera_board"] == "imx219" + # gamma_correction keys are cast to strings assert user_comment["tuning"] == { "exposure_time": 1234, "colour_gains": [1.1, 1.2], "analogue_gain": 2.5, + "gamma_correction": {"1": 5, "2": 10}, } From b716013ef2199d7f2b8bab2c56f3203f5f53f35f Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 3 Mar 2026 14:03:17 +0000 Subject: [PATCH 4/7] Fixed tuning file gamma key to gamma_curve --- .../things/camera/picamera_tuning_file_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openflexure_microscope_server/things/camera/picamera_tuning_file_utils.py b/src/openflexure_microscope_server/things/camera/picamera_tuning_file_utils.py index d2d139d9..29729741 100644 --- a/src/openflexure_microscope_server/things/camera/picamera_tuning_file_utils.py +++ b/src/openflexure_microscope_server/things/camera/picamera_tuning_file_utils.py @@ -193,7 +193,7 @@ def get_gamma_curve(tuning: dict) -> dict[int, float]: Defaults to {} if gamma curve is missing. """ contrast = find_tuning_algo(tuning, "rpi.contrast") - return contrast.get("gamma", {}) + return contrast.get("gamma_curve", {}) def set_gamma_curve(tuning: dict, gamma_curve: dict[int, float]) -> dict: @@ -203,7 +203,7 @@ def set_gamma_curve(tuning: dict, gamma_curve: dict[int, float]) -> dict: """ output_tuning = deepcopy(tuning) contrast = find_tuning_algo(output_tuning, "rpi.contrast") - contrast["gamma"] = {int(k): float(v) for k, v in gamma_curve.items()} + contrast["gamma_curve"] = {int(k): float(v) for k, v in gamma_curve.items()} return output_tuning From 3020ab59f4b81d3f268d94aea84114449e3d2c7c Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 3 Mar 2026 14:21:28 +0000 Subject: [PATCH 5/7] Fix typing for gamma_curve --- picamera_coverage.zip | Bin 54038 -> 54038 bytes .../things/camera/picamera.py | 4 ++-- .../camera/picamera_tuning_file_utils.py | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/picamera_coverage.zip b/picamera_coverage.zip index b30b7cb0a3e6ef76b7de0fb2124fd00f75f6fdd0..524b404436499df9f8babe06b1c9d1f54c243aa3 100644 GIT binary patch delta 418 zcmbQXjCtBJW}yIYW)=|!5SUby9K)f*bYr8CN`r_J1OHF{kNgMt)A(NSt>vrcOXXAA z>?pv&H#w{?(#U|Fg^@Ff^HB68#t%Lo?F=db%D)*HE=+yCljR==gTf33mU#(s3>c zX=G|aK5O=BR#ryN#!lBKDIsfCe&u~~|ViIGu?rKyFvnVFHfSxVC63m2R=XsR? diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index a759ab02..40ad6bda 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -719,8 +719,8 @@ class StreamingPiCamera2(BaseCamera): ) @lt.property - def gamma_correction(self) -> float: - """Return the gamma correction from the tuning file.""" + def gamma_correction(self) -> list[int]: + """Return the gamma correction curve from the tuning file.""" return tf_utils.get_gamma_curve(self.tuning) @lt.action diff --git a/src/openflexure_microscope_server/things/camera/picamera_tuning_file_utils.py b/src/openflexure_microscope_server/things/camera/picamera_tuning_file_utils.py index 29729741..f83a82c4 100644 --- a/src/openflexure_microscope_server/things/camera/picamera_tuning_file_utils.py +++ b/src/openflexure_microscope_server/things/camera/picamera_tuning_file_utils.py @@ -186,24 +186,24 @@ def get_lst(tuning: dict) -> LensShadingModel: ) -def get_gamma_curve(tuning: dict) -> dict[int, float]: +def get_gamma_curve(tuning: dict) -> list[int]: """Return the gamma curve from the rpi.contrast section of the tuning file. - Returns a dictionary mapping input levels to output levels. - Defaults to {} if gamma curve is missing. + Returns a list where each two elements are the input and output level. + Defaults to [] if gamma curve is missing. """ contrast = find_tuning_algo(tuning, "rpi.contrast") - return contrast.get("gamma_curve", {}) + return contrast.get("gamma_curve", []) -def set_gamma_curve(tuning: dict, gamma_curve: dict[int, float]) -> dict: +def set_gamma_curve(tuning: dict, gamma_curve: list[int]) -> dict: """Set the gamma curve in the rpi.contrast section of the tuning file. Returns a new tuning dictionary with the updated gamma curve. """ output_tuning = deepcopy(tuning) contrast = find_tuning_algo(output_tuning, "rpi.contrast") - contrast["gamma_curve"] = {int(k): float(v) for k, v in gamma_curve.items()} + contrast["gamma_curve"] = gamma_curve return output_tuning From 9a6f80edf4832aa895b31523e2841a5653c8c56b Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 3 Mar 2026 14:45:32 +0000 Subject: [PATCH 6/7] Update camera_test with extra key --- tests/unit_tests/test_cameras.py | 1 + tests/unit_tests/test_metadata.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/test_cameras.py b/tests/unit_tests/test_cameras.py index 40d0bf88..0fcec5ca 100644 --- a/tests/unit_tests/test_cameras.py +++ b/tests/unit_tests/test_cameras.py @@ -98,6 +98,7 @@ def test_thing_description_equivalence(mock_picam_thing): picamera_extra_props = { "mjpeg_bitrate", "colour_correction_matrix", + "gamma_correction", "lens_shading_tables", "sensor_resolution", "capture_metadata", diff --git a/tests/unit_tests/test_metadata.py b/tests/unit_tests/test_metadata.py index 915a5866..7c5f87b6 100644 --- a/tests/unit_tests/test_metadata.py +++ b/tests/unit_tests/test_metadata.py @@ -141,7 +141,7 @@ def test_picamera_adds_metadata(mock_picam_thing): camera.exposure_time = 1234 camera.colour_gains = (1.1, 1.2) camera.analogue_gain = 2.5 - camera.tuning = tf_utils.set_gamma_curve(camera.tuning, {1: 5, 2: 10}) + camera.tuning = tf_utils.set_gamma_curve(camera.tuning, [0, 0, 5, 50]) state = camera.thing_state @@ -151,7 +151,7 @@ def test_picamera_adds_metadata(mock_picam_thing): "exposure_time": 1234, "colour_gains": (1.1, 1.2), "analogue_gain": 2.5, - "gamma_correction": {1: 5, 2: 10}, + "gamma_correction": [0, 0, 5, 50], } @@ -163,7 +163,7 @@ def test_picamera_metadata_written_to_exif(mock_picam_thing, temp_jpeg, mocker): camera.exposure_time = 1234 camera.colour_gains = (1.1, 1.2) camera.analogue_gain = 2.5 - camera.tuning = tf_utils.set_gamma_curve(camera.tuning, {1: 5, 2: 10}) + camera.tuning = tf_utils.set_gamma_curve(camera.tuning, [0, 0, 5, 50]) # Mock the server interface to return the camera's own state mock_interface = mocker.Mock() @@ -184,5 +184,5 @@ def test_picamera_metadata_written_to_exif(mock_picam_thing, temp_jpeg, mocker): "exposure_time": 1234, "colour_gains": [1.1, 1.2], "analogue_gain": 2.5, - "gamma_correction": {"1": 5, "2": 10}, + "gamma_correction": [0, 0, 5, 50], } From 7ca94497c79bf1547a6f27932e971e419418a18d Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 3 Mar 2026 15:00:25 +0000 Subject: [PATCH 7/7] Remove link from basecamera docstring --- picamera_coverage.zip | Bin 54038 -> 54038 bytes .../things/camera/__init__.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/picamera_coverage.zip b/picamera_coverage.zip index 524b404436499df9f8babe06b1c9d1f54c243aa3..72bb9e27685ee0c14575b50a6a75c282ac815f6f 100644 GIT binary patch delta 102 zcmbQXjCtBJW}X0VW)=|!5J;-p$fI|GBdIDmCRBdHgUL=8(lm@s4Gk?!Q&W>IER4-f x4Na5MQc{ePjSUPE4b08W6AcrMl9P;042(BlxUiLxDXD65`z1Ss31=^P0styHA|L<& delta 102 zcmbQXjCtBJW}X0VW)=|!5SUc7kw@(~Jxa%o7t6O$`i< xk`2r)P0|vLOihhb(vlNXj4dsVOwA09O$;_)xUiLxX;RVT_DgmM6V6`p1OOHyAyWVV diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 55f2107a..351185ee 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -646,7 +646,7 @@ class BaseCamera(lt.Thing): """Return camera-specific metadata. By default, this just adds the subclass name as the camera type. - Subclasses can extend by overriding this property and calling `super().thing_state`. + Subclasses can extend by overriding this property and calling super().thing_state. """ return {"camera": self.__class__.__name__}