diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 99463d06..ef912e3e 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -112,7 +112,7 @@ class SensorModeSelector(BaseModel): These values are the output size and the bit depth. - This is a Pydantic model so that it can be saved to disk. + This is a Pydantic model so that it can sent by FastAPI """ output_size: tuple[int, int] @@ -126,12 +126,13 @@ class LensShading(BaseModel): (12, 16) in size. The arrays are luminance, red-difference chroma (Cr), and blue-difference chroma (Cb). - This is a Pydantic model so that it can be saved to the disk. + This is a Pydantic model so that it can sent by FastAPI """ luminance: list[list[float]] Cr: list[list[float]] Cb: list[list[float]] + colour_temp: int class StreamingPiCamera2(BaseCamera): @@ -216,7 +217,8 @@ class StreamingPiCamera2(BaseCamera): @lt.thing_property def calibration_required(self) -> bool: """Whether the camera needs calibrating.""" - return not self.lens_shading_is_static + # Check if the lens shading table is calibrated. + return not tf_utils.lst_calibrated(self.tuning) ## Persistent controls! These are settings @@ -730,7 +732,12 @@ class StreamingPiCamera2(BaseCamera): # luminance (L), red-difference chroma (Cr), and blue-difference chroma # (Cb). L, Cr, Cb = recalibrate_utils.lst_from_camera(cam, self._sensor_info) # noqa: N806 - self.tuning = tf_utils.set_static_lst(self.tuning, L, Cr, Cb) + self.tuning = tf_utils.set_lst( + self.tuning, + luminance=L, + cr=Cr, + cb=Cb, + ) # Re-initialise the picamera to reload the tuning file. self._initialise_picamera() @@ -750,14 +757,14 @@ class StreamingPiCamera2(BaseCamera): It is a 9 value tuple used to specify the 3x3 matrix that the GPU pipeline uses to convert from the camera R,G,B vector to the standard R,G,B. """ - return tuple(tf_utils.get_static_ccm(self.tuning)[0]["ccm"]) + return tuple(tf_utils.get_ccm(self.tuning)[0]["ccm"]) @colour_correction_matrix.setter # type: ignore def colour_correction_matrix( self, value: tuple[float, float, float, float, float, float, float, float, float], ) -> None: - self.tuning = tf_utils.set_static_ccm(self.tuning, value) + self.tuning = tf_utils.set_ccm(self.tuning, value) if self._picamera is not None: with self._streaming_picamera(pause_stream=True): @@ -833,8 +840,11 @@ class StreamingPiCamera2(BaseCamera): with self._streaming_picamera(pause_stream=True): # Generate and array of ones of the correct size for each channel flat_array = np.ones((12, 16)) - self.tuning = tf_utils.set_static_lst( - self.tuning, flat_array, flat_array, flat_array + self.tuning = tf_utils.set_lst( + self.tuning, + luminance=flat_array, + cr=flat_array, + cb=flat_array, ) self._initialise_picamera() @@ -930,9 +940,6 @@ class StreamingPiCamera2(BaseCamera): - adaptive control is enabled - multiple LSTs in use (for different colour temperatures), """ - if not self.lens_shading_is_static: - return None - # Note "alsc" is the Picamera2 term for "Automatic Lens Shading Correction" alsc = self.get_tuning_algo("rpi.alsc") @@ -951,23 +958,25 @@ class StreamingPiCamera2(BaseCamera): luminance=reshape_lst(alsc["luminance_lut"]), Cr=reshape_lst(alsc["calibrations_Cr"][0]["table"]), Cb=reshape_lst(alsc["calibrations_Cb"][0]["table"]), + colour_temp=alsc["calibrations_Cb"][0]["ct"], ) @lens_shading_tables.setter def lens_shading_tables(self, lst: LensShading) -> None: """Set the lens shading tables.""" with self._streaming_picamera(pause_stream=True): - self.tuning = tf_utils.set_static_lst( + self.tuning = tf_utils.set_lst( self.tuning, luminance=lst.luminance, cr=lst.Cr, cb=lst.Cb, + colour_temp=lst.colour_temp, ) self._initialise_picamera() @lt.thing_action def flat_lens_shading_chrominance(self) -> None: - """Disable flat-field correction. + """Disable flat-field correction for colour only. This method will set the chrominance of the lens shading table to be flat, i.e. we'll correct vignetting of intensity, but not any change in @@ -977,7 +986,13 @@ class StreamingPiCamera2(BaseCamera): alsc = self.get_tuning_algo("rpi.alsc") luminance = alsc["luminance_lut"] flat = np.ones((12, 16)) - self.tuning = tf_utils.set_static_lst(self.tuning, luminance, flat, flat) + self.tuning = tf_utils.set_lst( + self.tuning, + luminance=luminance, + cr=flat, + cb=flat, + colour_temp=1234, + ) self._initialise_picamera() @lt.thing_action @@ -995,17 +1010,6 @@ class StreamingPiCamera2(BaseCamera): ) self._initialise_picamera() - @lt.thing_property - def lens_shading_is_static(self) -> bool: - """Whether the lens shading is static. - - This property is true if the lens shading correction has been set to use - a static table (i.e. the number of automatic correction iterations is zero). - The default LST is not static, but all the calibration controls will set it - to be static (except "reset") - """ - return tf_utils.lst_is_static(self.tuning) - @property def thing_state(self) -> Mapping[str, Any]: """Update generic camera metadata with Picamera-specific data.""" 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 14b51e3e..1c349896 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 @@ -1,7 +1,6 @@ """Functions for loading, adjusting, or reading from the Picamera2 tuning file. -The functions that edit the tuning files edit them in place. This will change in -the future. +The functions that edit the tuning files return a new dictionary that is updated. """ from typing import Any @@ -60,44 +59,65 @@ def find_tuning_algo(tuning: dict[str, dict], name: str) -> dict[str, Any]: return algo_dict[name] -def set_static_lst( +def set_lst( tuning: dict, + *, luminance: np.ndarray, cr: np.ndarray, cb: np.ndarray, + colour_temp: int = 5000, ) -> dict: - """Update the ``rpi.alsc`` section of a camera tuning dict to use a static correction. + """Update the ``rpi.alsc`` section of with new lens shading tables. - ``tuning`` will be updated in-place to set its shading to static, and disable any - adaptive tweaking by the algorithm. + Only one set of tables is set so no adaptive lens shading will be used. + + :param tuning: The current tuning file. + :param luminance: The table of luminance values, as (12, 16) numpy array + :param cr: The table of cr values, as (12, 16) numpy array + :param cb: The table of cb values, as (12, 16) numpy array + :param colour_temp: The colour temperature to set. By default this is 5000. Set a + different value for the PiCamera Thing to report that the lens shading is not + calibrated. + :return: an updated tuning dict with the new lens shading tables. """ output_tuning = deepcopy(tuning) for table in luminance, cr, cb: if np.array(table).shape != (12, 16): raise ValueError("Lens shading tables must be 12x16!") alsc = find_tuning_algo(output_tuning, "rpi.alsc") - alsc["n_iter"] = 0 # disable the adaptive part + alsc["n_iter"] = 0 # disable the adaptive part. alsc["luminance_strength"] = 1.0 alsc["calibrations_Cr"] = [ - {"ct": 4500, "table": _as_flat_rounded_list(cr, round_to=3)} + {"ct": colour_temp, "table": _as_flat_rounded_list(cr, round_to=3)} ] alsc["calibrations_Cb"] = [ - {"ct": 4500, "table": _as_flat_rounded_list(cb, round_to=3)} + {"ct": colour_temp, "table": _as_flat_rounded_list(cb, round_to=3)} ] alsc["luminance_lut"] = _as_flat_rounded_list(luminance, round_to=3) return output_tuning -def set_static_ccm( +def lst_calibrated(tuning: dict) -> bool: + """Whether the lens shading table is calibrated. + + This checks whether the lens shading table is has a colour temperature of 5000. As + this is what we set on calibration. Our tuning file sets a temperature of 1234. + """ + alsc = find_tuning_algo(tuning, "rpi.alsc") + return alsc["calibrations_Cr"][0]["ct"] == 5000 + + +def set_ccm( tuning: dict, col_corr_matrix: tuple[ float, float, float, float, float, float, float, float, float ], ) -> dict: - """Update the ``rpi.alsc`` section of a camera tuning dict to use a static correction. + """Update the ``rpi.alsc`` section of a camera tuning dict set the colour correction matrix. - ``tuning`` will be updated in-place to set its shading to static, and disable any - adaptive tweaking by the algorithm. + :param tuning: The current tuning dict + :param col_corr_matrix: The colour correction matrix to set + :return: an updated tuning dict with the new colour correction matrix. """ output_tuning = deepcopy(tuning) ccm = find_tuning_algo(output_tuning, "rpi.ccm") @@ -105,32 +125,26 @@ def set_static_ccm( return output_tuning -def get_static_ccm(tuning: dict) -> None: +def get_ccm(tuning: dict) -> None: """Get a copy of the the ``rpi.ccm`` section of a camera tuning dict.""" ccm = find_tuning_algo(tuning, "rpi.ccm") return deepcopy(ccm["ccms"]) -def lst_is_static(tuning: dict) -> bool: - """Whether the lens shading table is set to static.""" - alsc = find_tuning_algo(tuning, "rpi.alsc") - return alsc["n_iter"] == 0 - - def set_static_geq( tuning: dict, offset: int = 65535, ) -> dict: """Update the ``rpi.geq`` section of a camera tuning dict. - :param tuning: the raspberry pi tuning file. This will be updated in-place to - set the geq offset to the given value. + :param tuning: the raspberry pi tuning dictionary :param offset: The desired green equalisation offset. Default 65535. The default is the maximum allowed value. This means the brightness will always be below the threshold where averaging is used. This is default as we always need the green equalisation to averages the green pixels in the red and blue rows due to the chief ray angle compensation issue when the the stock lens is replaced by an objective. + :param return: An updated tuning dictionary """ output_tuning = deepcopy(tuning) geq = find_tuning_algo(output_tuning, "rpi.geq") @@ -182,7 +196,7 @@ def copy_algo_from_other_tuning( # Find the relevant sub-dict for each tuning file from_i = _index_of_algorithm(copy_from["algorithms"], algo) to_i = _index_of_algorithm(base_tuning_file["algorithms"], algo) - # Updating the dictionary in place. + # Updating the output_tuning copy. output_tuning["algorithms"][to_i] = deepcopy(copy_from["algorithms"][from_i]) return output_tuning diff --git a/tests/test_cameras.py b/tests/test_cameras.py index ffb65df1..307df610 100644 --- a/tests/test_cameras.py +++ b/tests/test_cameras.py @@ -123,7 +123,6 @@ def test_thing_description_equivalence(mock_picam_thing): "colour_correction_matrix", "lens_shading_tables", "sensor_resolution", - "lens_shading_is_static", "capture_metadata", "camera_configuration", "stream_resolution", @@ -131,5 +130,9 @@ def test_thing_description_equivalence(mock_picam_thing): "sensor_modes", "sensor_mode", } + for action in picamera_extra_actions: + assert action in picamera_actions + for props in picamera_extra_props: + assert props in picamera_props assert picamera_actions - base_actions == picamera_extra_actions assert picamera_props - base_props == picamera_extra_props