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}, }