"""Tests for interactions with tuning files and dictionaries.""" from copy import deepcopy import pytest import numpy as np from openflexure_microscope_server.things.camera import ( picamera_tuning_file_utils as tf_utils, ) RNG = np.random.default_rng() @pytest.fixture def imx219_tuning(): """Load the default tuning file for the PiCamera v2.""" return tf_utils.load_default_tuning("imx219") @pytest.fixture def imx477_tuning(): """Load the default tuning file for the PiCamera HQ.""" return tf_utils.load_default_tuning("imx477") def test_load_tuning(imx219_tuning, imx477_tuning): """Check the sctandard tuning files load and contain the correct information.""" assert imx219_tuning != imx477_tuning for tuning in [imx219_tuning, imx477_tuning]: assert tuning["version"] == 2.0 assert tuning["target"] == "bcm2835" assert isinstance(tuning["algorithms"], list) # The algorithms in order, and whether we expect them to be the same for the cameras. algos = [ ("rpi.black_level", True), ("rpi.dpc", True), ("rpi.lux", True), ("rpi.noise", True), ("rpi.geq", True), ("rpi.sdn", True), ("rpi.awb", True), ("rpi.agc", True), ("rpi.alsc", False), ("rpi.contrast", True), ("rpi.ccm", False), ("rpi.sharpen", True), ("rpi.hdr", True), ("rpi.sync", True), ] # Strict= true so this will error if the tuning files are not the same length as # the algorithms zipped_algos = zip( algos, imx219_tuning["algorithms"], imx477_tuning["algorithms"], strict=True ) # Split up the algorithm name, either we expect them to be the same in both files # and the algorithm for the two tuning files. for (algo_name, algo_same), algo219, algo477 in zipped_algos: # Check this is the correct algorithm assert algo_name in algo219 assert algo_name in algo477 # Check they are they are the same if expected to be. if algo_same: assert algo219 == algo477 else: assert algo219 != algo477 def test_find_tuning_algo_v1(): """Check tuning algorithms are read directly from dict if no version is set.""" # Version 1 onf the tuning file was just a dictionary of algorithms mock_tuning = {"mock": {"param": 1}, "foo": {"param": "bar"}} assert tf_utils.find_tuning_algo(mock_tuning, "mock") == {"param": 1} assert tf_utils.find_tuning_algo(mock_tuning, "foo") == {"param": "bar"} with pytest.raises(KeyError, match="No algorithm who in tuning."): assert tf_utils.find_tuning_algo(mock_tuning, "who") # Also check that if version is not 1 it is read differently. mock_tuning["version"] = 2 with pytest.raises(KeyError, match="A v2 tuning file must specify"): assert tf_utils.find_tuning_algo(mock_tuning, "mock") def test_find_tuning_algo_v2(): """Check reading version 2 dictionary structure.""" mock_tuning = { "version": 2.0, "algorithms": [{"mock": {"param": 1}}, {"foo": {"param": "bar"}}], } assert tf_utils.find_tuning_algo(mock_tuning, "mock") == {"param": 1} assert tf_utils.find_tuning_algo(mock_tuning, "foo") == {"param": "bar"} with pytest.raises(KeyError, match="No algorithm who in tuning."): assert tf_utils.find_tuning_algo(mock_tuning, "who") def _assert_lst_table_equivalence(array, table): """Check equivalence between input numpy array and output table.""" for i in range(12): for j in range(16): assert table[i][j] == round(float(array[i, j]), 3) assert table[i][j] == round(float(array[i, j]), 3) def test_tuning_lst_tools(imx219_tuning, imx477_tuning): """Check reading the default lens shading tables.""" for tuning in (imx219_tuning, imx477_tuning): assert not tf_utils.lst_calibrated(tuning) lst_model = tf_utils.get_lst(tuning) assert isinstance(lst_model, tf_utils.LensShading) # Check the tables are lists of lists assert isinstance(lst_model.luminance, list) assert isinstance(lst_model.luminance[0], list) assert isinstance(lst_model.Cr, list) assert isinstance(lst_model.Cr[0], list) assert isinstance(lst_model.Cb, list) assert isinstance(lst_model.Cb[0], list) # Loaded table has default lens tuning, assert lst_model.colour_temp == tf_utils.DEFAULT_COLOUR_TEMP def test_tuning_initial_colour_gains(imx219_tuning, imx477_tuning): """The initial colour gains are as expected.""" assert tf_utils.get_colour_gains_from_lst(imx219_tuning) == (1.068, 1.259) assert tf_utils.get_colour_gains_from_lst(imx477_tuning) == (1.0, 2.0) def test_setting_lst(imx219_tuning): """Check that the LST can be set.""" lum = RNG.normal(size=(12, 16)) cr = RNG.normal(size=(12, 16)) cb = RNG.normal(size=(12, 16)) updated_tuning = tf_utils.set_lst( imx219_tuning, luminance=lum, cr=cr, cb=cb, colour_temp=tf_utils.CALIBRATED_COLOUR_TEMP, ) # Check it wasn't modified in place. assert updated_tuning != imx219_tuning # Check it now reports as calibrated assert tf_utils.lst_calibrated(updated_tuning) lst_model = tf_utils.get_lst(updated_tuning) assert lst_model.colour_temp == tf_utils.CALIBRATED_COLOUR_TEMP # Check the numbers are equivalent from the input array and the output table # except for formatting and rounding. _assert_lst_table_equivalence(lum, lst_model.luminance) _assert_lst_table_equivalence(cr, lst_model.Cr) _assert_lst_table_equivalence(cb, lst_model.Cb) # Flatten the lens shading tables flat_tuning = tf_utils.flatten_lst(updated_tuning) # In this case only the colour channels flat_chroma_tuning = tf_utils.flatten_lst(updated_tuning, keep_luminance=True) # Check each was changed assert updated_tuning != flat_tuning assert updated_tuning != flat_chroma_tuning assert flat_tuning != flat_chroma_tuning # Check the flattened tunings report as not calibrated assert not tf_utils.lst_calibrated(flat_tuning) assert not tf_utils.lst_calibrated(flat_chroma_tuning) flat_lst_model = tf_utils.get_lst(flat_tuning) flat_chroma_lst_model = tf_utils.get_lst(flat_chroma_tuning) flat_array = np.ones((12, 16)) # Check the that it really was flattened for the flat tuning _assert_lst_table_equivalence(flat_array, flat_lst_model.luminance) _assert_lst_table_equivalence(flat_array, flat_lst_model.Cr) _assert_lst_table_equivalence(flat_array, flat_lst_model.Cb) # Check only colour channels flattened when keep_luminance=True _assert_lst_table_equivalence(lum, flat_chroma_lst_model.luminance) _assert_lst_table_equivalence(flat_array, flat_chroma_lst_model.Cr) _assert_lst_table_equivalence(flat_array, flat_chroma_lst_model.Cb) def test_setting_ccm(imx219_tuning): """Check the colour correction matrix can be set.""" # CCM is set as a flat list. Create a mock ccm new_ccm = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] updated_tuning = tf_utils.set_ccm(imx219_tuning, new_ccm) # Check it wasn't modified in place. assert updated_tuning != imx219_tuning # Check the value was set assert tf_utils.get_ccm(updated_tuning) == new_ccm # Check that our dictionary doesn't update if the input list is updated. new_ccm[0] = 0.0 assert tf_utils.get_ccm(updated_tuning) != new_ccm new_ccm.append(1.0) with pytest.raises( ValueError, match="col_corr_matrix should be a list of 9 floats" ): tf_utils.set_ccm(imx219_tuning, new_ccm) def test_green_equalisation(imx219_tuning): """Test setting the offset parameter of green equalisation.""" assert tf_utils.geq_is_static(imx219_tuning) bad_geq_tuning = tf_utils.set_static_geq(imx219_tuning, offset=1234) # Check it wasn't modified in place. assert bad_geq_tuning != imx219_tuning # No longer static assert not tf_utils.geq_is_static(bad_geq_tuning) fixed_geq_tuning = tf_utils.set_static_geq(bad_geq_tuning) assert tf_utils.geq_is_static(fixed_geq_tuning) def test_ce_enable(imx219_tuning): """Check the setting of ce enable in the contrast algorithm.""" assert tf_utils.ce_enable_is_static(imx219_tuning) # No way to enable it so do it manually bad_tuning = deepcopy(imx219_tuning) contrast = tf_utils.find_tuning_algo(bad_tuning, "rpi.contrast") contrast["ce_enable"] = 1 assert not tf_utils.ce_enable_is_static(bad_tuning) fixed_tuning = tf_utils.set_ce_to_disabled(bad_tuning) assert tf_utils.ce_enable_is_static(fixed_tuning) def test_copying_algorithms(imx219_tuning): """Check an algorithm can be copied from one file to another.""" # Update the CCM new_ccm = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] updated_ccm_tuning = tf_utils.set_ccm(imx219_tuning, new_ccm) # It has changed assert updated_ccm_tuning != imx219_tuning # Create another tuning file with the GEQ updated. updated_geq_tuning = tf_utils.set_static_geq(imx219_tuning, offset=1234) # It has changed assert updated_geq_tuning != imx219_tuning # Copy the CCM from the dict with the bad GEQ value into the one with the updated # CCM original_tuning = tf_utils.copy_algo_from_other_tuning( "rpi.ccm", base_tuning_file=updated_ccm_tuning, copy_from=updated_geq_tuning ) # This should now be equivalent to the original. assert original_tuning == imx219_tuning