From b47413100ed2e8c1245e57086aa994a0928d3a5f Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 16 Oct 2025 18:45:07 +0100 Subject: [PATCH 01/18] README walking through the settings in the tuning file, and initial thoughts --- .../things/camera/tuning_files/README.md | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/openflexure_microscope_server/things/camera/tuning_files/README.md diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/README.md b/src/openflexure_microscope_server/things/camera/tuning_files/README.md new file mode 100644 index 00000000..6e8e2aa4 --- /dev/null +++ b/src/openflexure_microscope_server/things/camera/tuning_files/README.md @@ -0,0 +1,125 @@ +# A list of all the settings in a IMXxx.json: + +## version: + +I assume this needs to be 2.0 + +## target + +Needs to be bcm2835 + +## Algorithms + +### rpi.black_level + +contains one key-value pair. `black_level: 4096` + +### rpi.dpc + +Defective pixel correction, default off(?) + +### rpi.lux + +For light sensors, not used. Can be emptied? + +### rpi.noise + +``` +"reference_constant": 0, +"reference_slope": 3.67 +``` + +### rpi.geq + +Two elements. offset should be 65535 in all cases we've found. Slope unknown + +### rpi.sdn + +Spatial denoise. Warning on startup asks us to move it into rpi.denoise, even if empty + +### rpi.awb + +Auto white balance. Even though we don't use it, camera can't start without it being minimally populated, which is quite a lot + +### rpi.alsc + +Auto lens shading correction is calibrated by our microscope, and one suggestion is to ship this as a flat table (all 1s) and use that to check if the microscope is calibrated. + +`n_iter: 0` (not adaptive) + +_If we provide a flat one, then reset and disable flatfield correction will do the same thing. I'm also not sure these are persistent if you run one of them, then `ofm restart`_ + +### rpi.contrast + +`ce_enable` should be 0 (disabled). Gamma curve linear or not is an open question + +### rpi.ccm + +We only want to ship one temperature. Currently it's 5000K, the temperature of the LED. However, the diffuser might affect this. + +Different for HQ and version 2 cameras: currently we take the temperatures either side of 5000K from `/usr/share/libcamera/ipa/rpi/pisp/imx219.json` or `/usr/share/libcamera/ipa/rpi/pisp/imx477.json` + +Python code: + +``` +import numpy as np + +temps = {4650: + np.array([ + 2.18174, -0.70887, -0.47287, + -0.70196, 2.76426, -1.06231, + -0.25157, -0.71978, 1.97135 + ]), + 5858: + np.array( + [ + 2.32392, -0.88421, -0.43971, + -0.63821, 2.58348, -0.94527, + -0.28541, -0.54112, 1.82653 + ]) + } +target_temp = 5000 +temp_low, temp_high = sorted(temps.keys()) +alpha = (target_temp - temp_low) / (temp_high - temp_low) + +print(alpha) + +ccm_interp = (1 - alpha) * temps[temp_low] + alpha * temps[temp_high] +print(np.round(ccm_interp, 6)) + +temps = {4559: np.array( + [ + 2.15423, -0.98143, -0.17279, + -0.38131, 2.14763, -0.76632, + -0.10069, -0.54383, 1.64452 + ]), + 5881:np.array( + [ + 2.18464, -0.95493, -0.22971, + -0.36826, 2.00298, -0.63471, + -0.15219, -0.38055, 1.53274 + ]) + } +target_temp = 5000 +temp_low, temp_high = sorted(temps.keys()) +alpha = (target_temp - temp_low) / (temp_high - temp_low) + +print(alpha) + +ccm_interp = (1 - alpha) * temps[temp_low] + alpha * temps[temp_high] +print(np.round(ccm_interp, 6)) +``` + +This will be superseced by using a MacBeth target. + +### rpi.sharpen + +Currently empty. Sharpening might be something to add in post processing, but not here I expect. + +### rpi.hdr + +High dynamic range. Not relevant for us. Currently a short snippet. + +### rpi.sync + +Syncronising between cameras. Not something we're looking at anytime soon. \ No newline at end of file From e3a7252d78d4aeb37b7d0d1d7650774bb2944a96 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 16 Oct 2025 18:45:34 +0100 Subject: [PATCH 02/18] Example custom tuning files imx219 has been tested briefly imx477 untested, based on imx219 with custom ccm --- .../things/camera/tuning_files/imx219.json | 1356 +++++++++++++++++ .../things/camera/tuning_files/imx477.json | 1356 +++++++++++++++++ 2 files changed, 2712 insertions(+) create mode 100644 src/openflexure_microscope_server/things/camera/tuning_files/imx219.json create mode 100644 src/openflexure_microscope_server/things/camera/tuning_files/imx477.json diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/imx219.json b/src/openflexure_microscope_server/things/camera/tuning_files/imx219.json new file mode 100644 index 00000000..e6fbb9ab --- /dev/null +++ b/src/openflexure_microscope_server/things/camera/tuning_files/imx219.json @@ -0,0 +1,1356 @@ +{ + "version": 2.0, + "target": "bcm2835", + "algorithms": [ + { + "rpi.black_level": { + "black_level": 4096 + } + }, + { + "rpi.dpc": {} + }, + { + "rpi.lux": { + "reference_shutter_speed": 27685, + "reference_gain": 1.0, + "reference_aperture": 1.0, + "reference_lux": 998, + "reference_Y": 12744 + } + }, + { + "rpi.noise": { + "reference_constant": 0, + "reference_slope": 3.67 + } + }, + { + "rpi.geq": { + "offset": 65535, + "slope": 0.01633 + } + }, + { + "rpi.sdn": {} + }, + { + "rpi.awb": + { + "priors": [ + { + "lux": 0, + "prior": + [ + 2000, 1.0, + 3000, 0.0, + 13000, 0.0 + ] + }, + { + "lux": 800, + "prior": + [ + 2000, 0.0, + 6000, 2.0, + 13000, 2.0 + ] + }, + { + "lux": 1500, + "prior": + [ + 2000, 0.0, + 4000, 1.0, + 6000, 6.0, + 6500, 7.0, + 7000, 1.0, + 13000, 1.0 + ] + } + ], + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "incandescent": + { + "lo": 2500, + "hi": 3000 + }, + "tungsten": + { + "lo": 3000, + "hi": 3500 + }, + "fluorescent": + { + "lo": 4000, + "hi": 4700 + }, + "indoor": + { + "lo": 3000, + "hi": 5000 + }, + "daylight": + { + "lo": 5500, + "hi": 6500 + }, + "cloudy": + { + "lo": 7000, + "hi": 8000 + } + }, + "bayes": 1, + "ct_curve": + [ + 2860.0, 0.9514, 0.4156, + 2960.0, 0.9289, 0.4372, + 3603.0, 0.8305, 0.5251, + 4650.0, 0.6756, 0.6433, + 5858.0, 0.6193, 0.6807, + 7580.0, 0.5019, 0.7495 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.03392, + "transverse_neg": 0.034 + } + }, + { + "rpi.agc": { + "channels": [ + { + "metering_modes": { + "centre-weighted": { + "weights": [ + 3, + 3, + 3, + 2, + 2, + 2, + 2, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0 + ] + }, + "spot": { + "weights": [ + 2, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "matrix": { + "weights": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + }, + "exposure_modes": { + "normal": { + "shutter": [ + 100, + 10000, + 30000, + 60000, + 66666 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 8.0 + ] + }, + "short": { + "shutter": [ + 100, + 5000, + 10000, + 20000, + 33333 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 8.0 + ] + }, + "long": { + "shutter": [ + 100, + 10000, + 30000, + 60000, + 120000 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 12.0 + ] + } + }, + "constraint_modes": { + "normal": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.5, + 1000, + 0.5 + ] + } + ], + "highlight": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.5, + 1000, + 0.5 + ] + }, + { + "bound": "UPPER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.8, + 1000, + 0.8 + ] + } + ], + "shadows": [ + { + "bound": "LOWER", + "q_lo": 0.0, + "q_hi": 0.5, + "y_target": [ + 0, + 0.17, + 1000, + 0.17 + ] + } + ] + }, + "y_target": [ + 0, + 0.16, + 1000, + 0.165, + 10000, + 0.17 + ] + }, + { + "base_ev": 0.125, + "metering_modes": { + "centre-weighted": { + "weights": [ + 3, + 3, + 3, + 2, + 2, + 2, + 2, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0 + ] + }, + "spot": { + "weights": [ + 2, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "matrix": { + "weights": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + }, + "exposure_modes": { + "normal": { + "shutter": [ + 100, + 10000, + 30000, + 60000, + 66666 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 8.0 + ] + }, + "short": { + "shutter": [ + 100, + 5000, + 10000, + 20000, + 33333 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 8.0 + ] + }, + "long": { + "shutter": [ + 100, + 10000, + 30000, + 60000, + 120000 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 12.0 + ] + } + }, + "constraint_modes": { + "normal": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.5, + 1000, + 0.5 + ] + } + ], + "highlight": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.5, + 1000, + 0.5 + ] + }, + { + "bound": "UPPER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.8, + 1000, + 0.8 + ] + } + ], + "shadows": [ + { + "bound": "LOWER", + "q_lo": 0.0, + "q_hi": 0.5, + "y_target": [ + 0, + 0.17, + 1000, + 0.17 + ] + } + ] + }, + "y_target": [ + 0, + 0.16, + 1000, + 0.165, + 10000, + 0.17 + ] + }, + { + "base_ev": 1.5, + "metering_modes": { + "centre-weighted": { + "weights": [ + 3, + 3, + 3, + 2, + 2, + 2, + 2, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0 + ] + }, + "spot": { + "weights": [ + 2, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "matrix": { + "weights": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + }, + "exposure_modes": { + "normal": { + "shutter": [ + 100, + 10000, + 30000, + 60000, + 66666 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 8.0 + ] + }, + "short": { + "shutter": [ + 100, + 5000, + 10000, + 20000, + 33333 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 8.0 + ] + }, + "long": { + "shutter": [ + 100, + 10000, + 30000, + 60000, + 120000 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 12.0 + ] + } + }, + "constraint_modes": { + "normal": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.5, + 1000, + 0.5 + ] + } + ], + "highlight": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.5, + 1000, + 0.5 + ] + }, + { + "bound": "UPPER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.8, + 1000, + 0.8 + ] + } + ], + "shadows": [ + { + "bound": "LOWER", + "q_lo": 0.0, + "q_hi": 0.5, + "y_target": [ + 0, + 0.17, + 1000, + 0.17 + ] + } + ] + }, + "y_target": [ + 0, + 0.16, + 1000, + 0.165, + 10000, + 0.17 + ] + } + ] + } + }, + { + "rpi.alsc": { + "omega": 1.3, + "n_iter": 0, + "luminance_strength": 1.0, + "calibrations_Cr": [ + { + "ct": 4500, + "table": [ + 0.941, + 0.953, + 0.962, + 0.969, + 0.971, + 0.966, + 0.961, + 0.958, + 0.96, + 0.967, + 0.977, + 0.984, + 0.981, + 0.97, + 0.958, + 0.95, + 0.924, + 0.934, + 0.953, + 0.974, + 0.986, + 0.992, + 0.993, + 0.994, + 0.996, + 0.999, + 1.003, + 0.998, + 0.983, + 0.959, + 0.937, + 0.926, + 0.9, + 0.914, + 0.938, + 0.967, + 0.994, + 1.011, + 1.021, + 1.026, + 1.027, + 1.027, + 1.023, + 1.007, + 0.977, + 0.941, + 0.915, + 0.902, + 0.879, + 0.892, + 0.921, + 0.96, + 0.997, + 1.024, + 1.039, + 1.045, + 1.047, + 1.045, + 1.037, + 1.012, + 0.967, + 0.923, + 0.893, + 0.878, + 0.863, + 0.877, + 0.908, + 0.953, + 0.999, + 1.032, + 1.047, + 1.053, + 1.056, + 1.055, + 1.043, + 1.01, + 0.958, + 0.908, + 0.875, + 0.862, + 0.854, + 0.868, + 0.898, + 0.946, + 0.997, + 1.033, + 1.048, + 1.055, + 1.057, + 1.057, + 1.045, + 1.009, + 0.952, + 0.898, + 0.866, + 0.852, + 0.852, + 0.865, + 0.896, + 0.942, + 0.993, + 1.029, + 1.046, + 1.053, + 1.054, + 1.054, + 1.042, + 1.007, + 0.951, + 0.897, + 0.865, + 0.852, + 0.857, + 0.871, + 0.9, + 0.943, + 0.987, + 1.019, + 1.035, + 1.043, + 1.045, + 1.044, + 1.034, + 1.003, + 0.953, + 0.904, + 0.873, + 0.86, + 0.869, + 0.881, + 0.906, + 0.941, + 0.974, + 0.996, + 1.007, + 1.014, + 1.017, + 1.017, + 1.012, + 0.993, + 0.956, + 0.916, + 0.888, + 0.874, + 0.884, + 0.89, + 0.905, + 0.926, + 0.943, + 0.95, + 0.952, + 0.955, + 0.96, + 0.964, + 0.971, + 0.968, + 0.949, + 0.923, + 0.901, + 0.892, + 0.897, + 0.895, + 0.897, + 0.9, + 0.899, + 0.892, + 0.885, + 0.884, + 0.888, + 0.9, + 0.917, + 0.929, + 0.93, + 0.922, + 0.912, + 0.91, + 0.905, + 0.894, + 0.885, + 0.874, + 0.86, + 0.847, + 0.837, + 0.834, + 0.839, + 0.852, + 0.872, + 0.893, + 0.908, + 0.916, + 0.92, + 0.925 + ] + } + ], + "calibrations_Cb": [ + { + "ct": 4500, + "table": [ + 1.492, + 1.478, + 1.482, + 1.487, + 1.49, + 1.495, + 1.505, + 1.51, + 1.507, + 1.492, + 1.484, + 1.48, + 1.471, + 1.464, + 1.457, + 1.467, + 1.477, + 1.479, + 1.492, + 1.508, + 1.527, + 1.545, + 1.568, + 1.585, + 1.582, + 1.557, + 1.537, + 1.513, + 1.486, + 1.456, + 1.432, + 1.429, + 1.464, + 1.472, + 1.498, + 1.533, + 1.567, + 1.603, + 1.644, + 1.674, + 1.669, + 1.632, + 1.591, + 1.55, + 1.5, + 1.449, + 1.408, + 1.398, + 1.456, + 1.474, + 1.513, + 1.563, + 1.62, + 1.677, + 1.735, + 1.78, + 1.767, + 1.711, + 1.653, + 1.594, + 1.517, + 1.443, + 1.389, + 1.37, + 1.458, + 1.48, + 1.532, + 1.601, + 1.677, + 1.757, + 1.838, + 1.893, + 1.876, + 1.803, + 1.724, + 1.64, + 1.54, + 1.445, + 1.375, + 1.353, + 1.457, + 1.484, + 1.544, + 1.624, + 1.715, + 1.809, + 1.901, + 1.957, + 1.943, + 1.867, + 1.774, + 1.676, + 1.557, + 1.448, + 1.373, + 1.344, + 1.459, + 1.484, + 1.543, + 1.623, + 1.713, + 1.802, + 1.891, + 1.948, + 1.934, + 1.858, + 1.771, + 1.677, + 1.561, + 1.45, + 1.376, + 1.348, + 1.463, + 1.488, + 1.54, + 1.608, + 1.682, + 1.759, + 1.835, + 1.887, + 1.871, + 1.805, + 1.732, + 1.656, + 1.557, + 1.46, + 1.391, + 1.367, + 1.475, + 1.497, + 1.536, + 1.588, + 1.645, + 1.703, + 1.76, + 1.798, + 1.787, + 1.739, + 1.683, + 1.627, + 1.554, + 1.479, + 1.422, + 1.399, + 1.495, + 1.505, + 1.531, + 1.564, + 1.598, + 1.631, + 1.667, + 1.691, + 1.685, + 1.652, + 1.619, + 1.587, + 1.543, + 1.492, + 1.452, + 1.44, + 1.514, + 1.512, + 1.519, + 1.527, + 1.536, + 1.548, + 1.561, + 1.572, + 1.564, + 1.551, + 1.541, + 1.532, + 1.52, + 1.497, + 1.479, + 1.478, + 1.53, + 1.515, + 1.504, + 1.494, + 1.484, + 1.479, + 1.478, + 1.478, + 1.475, + 1.47, + 1.473, + 1.483, + 1.494, + 1.496, + 1.502, + 1.513 + ] + } + ], + "luminance_lut": [ + 1.891, + 2.158, + 1.778, + 1.606, + 1.57, + 1.543, + 1.533, + 1.555, + 1.617, + 1.723, + 1.873, + 2.054, + 2.249, + 2.419, + 2.543, + 2.585, + 1.574, + 1.675, + 1.749, + 1.543, + 1.467, + 1.403, + 1.369, + 1.377, + 1.435, + 1.544, + 1.71, + 1.928, + 2.18, + 2.418, + 2.593, + 2.679, + 1.587, + 1.589, + 1.547, + 1.458, + 1.352, + 1.26, + 1.209, + 1.205, + 1.255, + 1.362, + 1.536, + 1.781, + 2.08, + 2.368, + 2.597, + 2.718, + 1.605, + 1.587, + 1.515, + 1.389, + 1.257, + 1.149, + 1.088, + 1.079, + 1.124, + 1.222, + 1.392, + 1.651, + 1.982, + 2.308, + 2.567, + 2.721, + 1.621, + 1.585, + 1.491, + 1.348, + 1.2, + 1.085, + 1.024, + 1.014, + 1.053, + 1.146, + 1.314, + 1.58, + 1.922, + 2.268, + 2.55, + 2.728, + 1.652, + 1.609, + 1.506, + 1.352, + 1.187, + 1.07, + 1.01, + 1.0, + 1.036, + 1.122, + 1.293, + 1.569, + 1.925, + 2.287, + 2.575, + 2.77, + 1.708, + 1.67, + 1.558, + 1.402, + 1.233, + 1.105, + 1.042, + 1.032, + 1.066, + 1.157, + 1.335, + 1.621, + 1.992, + 2.367, + 2.654, + 2.85, + 1.794, + 1.765, + 1.657, + 1.501, + 1.335, + 1.202, + 1.13, + 1.116, + 1.156, + 1.261, + 1.455, + 1.756, + 2.132, + 2.507, + 2.792, + 2.972, + 1.894, + 1.892, + 1.81, + 1.67, + 1.507, + 1.375, + 1.297, + 1.282, + 1.335, + 1.455, + 1.666, + 1.972, + 2.343, + 2.699, + 2.968, + 3.121, + 1.986, + 2.033, + 2.0, + 1.894, + 1.754, + 1.633, + 1.563, + 1.556, + 1.615, + 1.756, + 1.975, + 2.276, + 2.623, + 2.944, + 3.172, + 3.283, + 2.053, + 2.151, + 2.175, + 2.121, + 2.027, + 1.929, + 1.879, + 1.879, + 1.953, + 2.098, + 2.316, + 2.609, + 2.92, + 3.183, + 3.35, + 3.39, + 2.072, + 2.216, + 2.283, + 2.284, + 2.235, + 2.181, + 2.146, + 2.156, + 2.236, + 2.378, + 2.584, + 2.846, + 3.11, + 3.324, + 3.416, + 3.395 + ], + "sigma": 0.00381, + "sigma_Cb": 0.00216 + } + }, + { + "rpi.contrast": { + "ce_enable": 0, + "gamma_curve": [ + 0, + 0, + 1024, + 5040, + 2048, + 9338, + 3072, + 12356, + 4096, + 15312, + 5120, + 18051, + 6144, + 20790, + 7168, + 23193, + 8192, + 25744, + 9216, + 27942, + 10240, + 30035, + 11264, + 32005, + 12288, + 33975, + 13312, + 35815, + 14336, + 37600, + 15360, + 39168, + 16384, + 40642, + 18432, + 43379, + 20480, + 45749, + 22528, + 47753, + 24576, + 49621, + 26624, + 51253, + 28672, + 52698, + 30720, + 53796, + 32768, + 54876, + 36864, + 57012, + 40960, + 58656, + 45056, + 59954, + 49152, + 61183, + 53248, + 62355, + 57344, + 63419, + 61440, + 64476, + 65535, + 65535 + ] + } + }, + { + "rpi.ccm": { + "ccms": [ + { + "ct": 5000, + "ccm": [ + 2.222935, -0.759672, -0.463262, + -0.683489, 2.711882, -1.028399, + -0.261375, -0.668016, 1.929391 + ] + } + ] + } + }, + { + "rpi.sharpen": {} + }, + { + "rpi.hdr": { + "MultiExposureUnmerged": { + "cadence": [ + 1, + 2 + ], + "channel_map": { + "short": 1, + "long": 2 + } + } + } + }, + { + "rpi.sync": {} + } + ] +} \ No newline at end of file diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/imx477.json b/src/openflexure_microscope_server/things/camera/tuning_files/imx477.json new file mode 100644 index 00000000..93923d5a --- /dev/null +++ b/src/openflexure_microscope_server/things/camera/tuning_files/imx477.json @@ -0,0 +1,1356 @@ +{ + "version": 2.0, + "target": "bcm2835", + "algorithms": [ + { + "rpi.black_level": { + "black_level": 4096 + } + }, + { + "rpi.dpc": {} + }, + { + "rpi.lux": { + "reference_shutter_speed": 27685, + "reference_gain": 1.0, + "reference_aperture": 1.0, + "reference_lux": 998, + "reference_Y": 12744 + } + }, + { + "rpi.noise": { + "reference_constant": 0, + "reference_slope": 3.67 + } + }, + { + "rpi.geq": { + "offset": 65535, + "slope": 0.01633 + } + }, + { + "rpi.sdn": {} + }, + { + "rpi.awb": + { + "priors": [ + { + "lux": 0, + "prior": + [ + 2000, 1.0, + 3000, 0.0, + 13000, 0.0 + ] + }, + { + "lux": 800, + "prior": + [ + 2000, 0.0, + 6000, 2.0, + 13000, 2.0 + ] + }, + { + "lux": 1500, + "prior": + [ + 2000, 0.0, + 4000, 1.0, + 6000, 6.0, + 6500, 7.0, + 7000, 1.0, + 13000, 1.0 + ] + } + ], + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "incandescent": + { + "lo": 2500, + "hi": 3000 + }, + "tungsten": + { + "lo": 3000, + "hi": 3500 + }, + "fluorescent": + { + "lo": 4000, + "hi": 4700 + }, + "indoor": + { + "lo": 3000, + "hi": 5000 + }, + "daylight": + { + "lo": 5500, + "hi": 6500 + }, + "cloudy": + { + "lo": 7000, + "hi": 8000 + } + }, + "bayes": 1, + "ct_curve": + [ + 2860.0, 0.9514, 0.4156, + 2960.0, 0.9289, 0.4372, + 3603.0, 0.8305, 0.5251, + 4650.0, 0.6756, 0.6433, + 5858.0, 0.6193, 0.6807, + 7580.0, 0.5019, 0.7495 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.03392, + "transverse_neg": 0.034 + } + }, + { + "rpi.agc": { + "channels": [ + { + "metering_modes": { + "centre-weighted": { + "weights": [ + 3, + 3, + 3, + 2, + 2, + 2, + 2, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0 + ] + }, + "spot": { + "weights": [ + 2, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "matrix": { + "weights": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + }, + "exposure_modes": { + "normal": { + "shutter": [ + 100, + 10000, + 30000, + 60000, + 66666 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 8.0 + ] + }, + "short": { + "shutter": [ + 100, + 5000, + 10000, + 20000, + 33333 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 8.0 + ] + }, + "long": { + "shutter": [ + 100, + 10000, + 30000, + 60000, + 120000 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 12.0 + ] + } + }, + "constraint_modes": { + "normal": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.5, + 1000, + 0.5 + ] + } + ], + "highlight": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.5, + 1000, + 0.5 + ] + }, + { + "bound": "UPPER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.8, + 1000, + 0.8 + ] + } + ], + "shadows": [ + { + "bound": "LOWER", + "q_lo": 0.0, + "q_hi": 0.5, + "y_target": [ + 0, + 0.17, + 1000, + 0.17 + ] + } + ] + }, + "y_target": [ + 0, + 0.16, + 1000, + 0.165, + 10000, + 0.17 + ] + }, + { + "base_ev": 0.125, + "metering_modes": { + "centre-weighted": { + "weights": [ + 3, + 3, + 3, + 2, + 2, + 2, + 2, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0 + ] + }, + "spot": { + "weights": [ + 2, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "matrix": { + "weights": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + }, + "exposure_modes": { + "normal": { + "shutter": [ + 100, + 10000, + 30000, + 60000, + 66666 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 8.0 + ] + }, + "short": { + "shutter": [ + 100, + 5000, + 10000, + 20000, + 33333 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 8.0 + ] + }, + "long": { + "shutter": [ + 100, + 10000, + 30000, + 60000, + 120000 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 12.0 + ] + } + }, + "constraint_modes": { + "normal": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.5, + 1000, + 0.5 + ] + } + ], + "highlight": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.5, + 1000, + 0.5 + ] + }, + { + "bound": "UPPER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.8, + 1000, + 0.8 + ] + } + ], + "shadows": [ + { + "bound": "LOWER", + "q_lo": 0.0, + "q_hi": 0.5, + "y_target": [ + 0, + 0.17, + 1000, + 0.17 + ] + } + ] + }, + "y_target": [ + 0, + 0.16, + 1000, + 0.165, + 10000, + 0.17 + ] + }, + { + "base_ev": 1.5, + "metering_modes": { + "centre-weighted": { + "weights": [ + 3, + 3, + 3, + 2, + 2, + 2, + 2, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0 + ] + }, + "spot": { + "weights": [ + 2, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "matrix": { + "weights": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + }, + "exposure_modes": { + "normal": { + "shutter": [ + 100, + 10000, + 30000, + 60000, + 66666 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 8.0 + ] + }, + "short": { + "shutter": [ + 100, + 5000, + 10000, + 20000, + 33333 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 8.0 + ] + }, + "long": { + "shutter": [ + 100, + 10000, + 30000, + 60000, + 120000 + ], + "gain": [ + 1.0, + 2.0, + 4.0, + 6.0, + 12.0 + ] + } + }, + "constraint_modes": { + "normal": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.5, + 1000, + 0.5 + ] + } + ], + "highlight": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.5, + 1000, + 0.5 + ] + }, + { + "bound": "UPPER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": [ + 0, + 0.8, + 1000, + 0.8 + ] + } + ], + "shadows": [ + { + "bound": "LOWER", + "q_lo": 0.0, + "q_hi": 0.5, + "y_target": [ + 0, + 0.17, + 1000, + 0.17 + ] + } + ] + }, + "y_target": [ + 0, + 0.16, + 1000, + 0.165, + 10000, + 0.17 + ] + } + ] + } + }, + { + "rpi.alsc": { + "omega": 1.3, + "n_iter": 0, + "luminance_strength": 1.0, + "calibrations_Cr": [ + { + "ct": 4500, + "table": [ + 0.941, + 0.953, + 0.962, + 0.969, + 0.971, + 0.966, + 0.961, + 0.958, + 0.96, + 0.967, + 0.977, + 0.984, + 0.981, + 0.97, + 0.958, + 0.95, + 0.924, + 0.934, + 0.953, + 0.974, + 0.986, + 0.992, + 0.993, + 0.994, + 0.996, + 0.999, + 1.003, + 0.998, + 0.983, + 0.959, + 0.937, + 0.926, + 0.9, + 0.914, + 0.938, + 0.967, + 0.994, + 1.011, + 1.021, + 1.026, + 1.027, + 1.027, + 1.023, + 1.007, + 0.977, + 0.941, + 0.915, + 0.902, + 0.879, + 0.892, + 0.921, + 0.96, + 0.997, + 1.024, + 1.039, + 1.045, + 1.047, + 1.045, + 1.037, + 1.012, + 0.967, + 0.923, + 0.893, + 0.878, + 0.863, + 0.877, + 0.908, + 0.953, + 0.999, + 1.032, + 1.047, + 1.053, + 1.056, + 1.055, + 1.043, + 1.01, + 0.958, + 0.908, + 0.875, + 0.862, + 0.854, + 0.868, + 0.898, + 0.946, + 0.997, + 1.033, + 1.048, + 1.055, + 1.057, + 1.057, + 1.045, + 1.009, + 0.952, + 0.898, + 0.866, + 0.852, + 0.852, + 0.865, + 0.896, + 0.942, + 0.993, + 1.029, + 1.046, + 1.053, + 1.054, + 1.054, + 1.042, + 1.007, + 0.951, + 0.897, + 0.865, + 0.852, + 0.857, + 0.871, + 0.9, + 0.943, + 0.987, + 1.019, + 1.035, + 1.043, + 1.045, + 1.044, + 1.034, + 1.003, + 0.953, + 0.904, + 0.873, + 0.86, + 0.869, + 0.881, + 0.906, + 0.941, + 0.974, + 0.996, + 1.007, + 1.014, + 1.017, + 1.017, + 1.012, + 0.993, + 0.956, + 0.916, + 0.888, + 0.874, + 0.884, + 0.89, + 0.905, + 0.926, + 0.943, + 0.95, + 0.952, + 0.955, + 0.96, + 0.964, + 0.971, + 0.968, + 0.949, + 0.923, + 0.901, + 0.892, + 0.897, + 0.895, + 0.897, + 0.9, + 0.899, + 0.892, + 0.885, + 0.884, + 0.888, + 0.9, + 0.917, + 0.929, + 0.93, + 0.922, + 0.912, + 0.91, + 0.905, + 0.894, + 0.885, + 0.874, + 0.86, + 0.847, + 0.837, + 0.834, + 0.839, + 0.852, + 0.872, + 0.893, + 0.908, + 0.916, + 0.92, + 0.925 + ] + } + ], + "calibrations_Cb": [ + { + "ct": 4500, + "table": [ + 1.492, + 1.478, + 1.482, + 1.487, + 1.49, + 1.495, + 1.505, + 1.51, + 1.507, + 1.492, + 1.484, + 1.48, + 1.471, + 1.464, + 1.457, + 1.467, + 1.477, + 1.479, + 1.492, + 1.508, + 1.527, + 1.545, + 1.568, + 1.585, + 1.582, + 1.557, + 1.537, + 1.513, + 1.486, + 1.456, + 1.432, + 1.429, + 1.464, + 1.472, + 1.498, + 1.533, + 1.567, + 1.603, + 1.644, + 1.674, + 1.669, + 1.632, + 1.591, + 1.55, + 1.5, + 1.449, + 1.408, + 1.398, + 1.456, + 1.474, + 1.513, + 1.563, + 1.62, + 1.677, + 1.735, + 1.78, + 1.767, + 1.711, + 1.653, + 1.594, + 1.517, + 1.443, + 1.389, + 1.37, + 1.458, + 1.48, + 1.532, + 1.601, + 1.677, + 1.757, + 1.838, + 1.893, + 1.876, + 1.803, + 1.724, + 1.64, + 1.54, + 1.445, + 1.375, + 1.353, + 1.457, + 1.484, + 1.544, + 1.624, + 1.715, + 1.809, + 1.901, + 1.957, + 1.943, + 1.867, + 1.774, + 1.676, + 1.557, + 1.448, + 1.373, + 1.344, + 1.459, + 1.484, + 1.543, + 1.623, + 1.713, + 1.802, + 1.891, + 1.948, + 1.934, + 1.858, + 1.771, + 1.677, + 1.561, + 1.45, + 1.376, + 1.348, + 1.463, + 1.488, + 1.54, + 1.608, + 1.682, + 1.759, + 1.835, + 1.887, + 1.871, + 1.805, + 1.732, + 1.656, + 1.557, + 1.46, + 1.391, + 1.367, + 1.475, + 1.497, + 1.536, + 1.588, + 1.645, + 1.703, + 1.76, + 1.798, + 1.787, + 1.739, + 1.683, + 1.627, + 1.554, + 1.479, + 1.422, + 1.399, + 1.495, + 1.505, + 1.531, + 1.564, + 1.598, + 1.631, + 1.667, + 1.691, + 1.685, + 1.652, + 1.619, + 1.587, + 1.543, + 1.492, + 1.452, + 1.44, + 1.514, + 1.512, + 1.519, + 1.527, + 1.536, + 1.548, + 1.561, + 1.572, + 1.564, + 1.551, + 1.541, + 1.532, + 1.52, + 1.497, + 1.479, + 1.478, + 1.53, + 1.515, + 1.504, + 1.494, + 1.484, + 1.479, + 1.478, + 1.478, + 1.475, + 1.47, + 1.473, + 1.483, + 1.494, + 1.496, + 1.502, + 1.513 + ] + } + ], + "luminance_lut": [ + 1.891, + 2.158, + 1.778, + 1.606, + 1.57, + 1.543, + 1.533, + 1.555, + 1.617, + 1.723, + 1.873, + 2.054, + 2.249, + 2.419, + 2.543, + 2.585, + 1.574, + 1.675, + 1.749, + 1.543, + 1.467, + 1.403, + 1.369, + 1.377, + 1.435, + 1.544, + 1.71, + 1.928, + 2.18, + 2.418, + 2.593, + 2.679, + 1.587, + 1.589, + 1.547, + 1.458, + 1.352, + 1.26, + 1.209, + 1.205, + 1.255, + 1.362, + 1.536, + 1.781, + 2.08, + 2.368, + 2.597, + 2.718, + 1.605, + 1.587, + 1.515, + 1.389, + 1.257, + 1.149, + 1.088, + 1.079, + 1.124, + 1.222, + 1.392, + 1.651, + 1.982, + 2.308, + 2.567, + 2.721, + 1.621, + 1.585, + 1.491, + 1.348, + 1.2, + 1.085, + 1.024, + 1.014, + 1.053, + 1.146, + 1.314, + 1.58, + 1.922, + 2.268, + 2.55, + 2.728, + 1.652, + 1.609, + 1.506, + 1.352, + 1.187, + 1.07, + 1.01, + 1.0, + 1.036, + 1.122, + 1.293, + 1.569, + 1.925, + 2.287, + 2.575, + 2.77, + 1.708, + 1.67, + 1.558, + 1.402, + 1.233, + 1.105, + 1.042, + 1.032, + 1.066, + 1.157, + 1.335, + 1.621, + 1.992, + 2.367, + 2.654, + 2.85, + 1.794, + 1.765, + 1.657, + 1.501, + 1.335, + 1.202, + 1.13, + 1.116, + 1.156, + 1.261, + 1.455, + 1.756, + 2.132, + 2.507, + 2.792, + 2.972, + 1.894, + 1.892, + 1.81, + 1.67, + 1.507, + 1.375, + 1.297, + 1.282, + 1.335, + 1.455, + 1.666, + 1.972, + 2.343, + 2.699, + 2.968, + 3.121, + 1.986, + 2.033, + 2.0, + 1.894, + 1.754, + 1.633, + 1.563, + 1.556, + 1.615, + 1.756, + 1.975, + 2.276, + 2.623, + 2.944, + 3.172, + 3.283, + 2.053, + 2.151, + 2.175, + 2.121, + 2.027, + 1.929, + 1.879, + 1.879, + 1.953, + 2.098, + 2.316, + 2.609, + 2.92, + 3.183, + 3.35, + 3.39, + 2.072, + 2.216, + 2.283, + 2.284, + 2.235, + 2.181, + 2.146, + 2.156, + 2.236, + 2.378, + 2.584, + 2.846, + 3.11, + 3.324, + 3.416, + 3.395 + ], + "sigma": 0.00381, + "sigma_Cb": 0.00216 + } + }, + { + "rpi.contrast": { + "ce_enable": 0, + "gamma_curve": [ + 0, + 0, + 1024, + 5040, + 2048, + 9338, + 3072, + 12356, + 4096, + 15312, + 5120, + 18051, + 6144, + 20790, + 7168, + 23193, + 8192, + 25744, + 9216, + 27942, + 10240, + 30035, + 11264, + 32005, + 12288, + 33975, + 13312, + 35815, + 14336, + 37600, + 15360, + 39168, + 16384, + 40642, + 18432, + 43379, + 20480, + 45749, + 22528, + 47753, + 24576, + 49621, + 26624, + 51253, + 28672, + 52698, + 30720, + 53796, + 32768, + 54876, + 36864, + 57012, + 40960, + 58656, + 45056, + 59954, + 49152, + 61183, + 53248, + 62355, + 57344, + 63419, + 61440, + 64476, + 65535, + 65535 + ] + } + }, + { + "rpi.ccm": { + "ccms": [ + { + "ct": 5000, + "ccm": [ + 2.164374, -0.97259, -0.191778, + -0.376957, 2.099377, -0.722417, + -0.11787, -0.489362, 1.607232 + ] + } + ] + } + }, + { + "rpi.sharpen": {} + }, + { + "rpi.hdr": { + "MultiExposureUnmerged": { + "cadence": [ + 1, + 2 + ], + "channel_map": { + "short": 1, + "long": 2 + } + } + } + }, + { + "rpi.sync": {} + } + ] +} \ No newline at end of file From ac08462405a8dce44823a1128f3b560a2986aeb5 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 16 Oct 2025 18:46:21 +0100 Subject: [PATCH 03/18] Attempt to load custom tuning file first Most likely want to be in try except --- .../things/camera/picamera_tuning_file_utils.py | 7 +++++++ 1 file changed, 7 insertions(+) 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 e68b7681..b6f12450 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 @@ -9,7 +9,9 @@ from copy import deepcopy from picamera2 import Picamera2 import numpy as np +import os +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) def load_default_tuning(sensor_model: str) -> dict: """Load the default tuning file for the camera. @@ -17,6 +19,11 @@ def load_default_tuning(sensor_model: str) -> dict: This will load the tuning file based on the specified sensor model. """ fname = f"{sensor_model}.json" + custom_path = os.path.join(THIS_DIR, 'tuning_files', fname) + + # Attempt to load custom tuning file from OpenFlexure settings + if os.path.isfile(custom_path): + return Picamera2.load_tuning_file(fname, dir=os.path.join(THIS_DIR, 'tuning_files')) try: return Picamera2.load_tuning_file(fname) except RuntimeError: From 1b14990b76a04b59977e813fa1d3f398448e6029 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 16 Oct 2025 18:48:45 +0100 Subject: [PATCH 04/18] Stop overwriting CCM in full recalibrate Fix typo --- src/openflexure_microscope_server/things/camera/picamera.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 1c646be5..2143b138 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -170,7 +170,7 @@ class StreamingPiCamera2(BaseCamera): ) # Set tuning to default tuning. This will be overwritten when the Thing is - # connects to the server if tuning is saved to disk. + # connected to the server if tuning is saved to disk. try: self.tuning = copy.deepcopy(self.default_tuning) except NotConnectedToServerError: @@ -837,7 +837,7 @@ class StreamingPiCamera2(BaseCamera): * ``auto_expose_from_minimum`` * ``set_static_green_equalisation`` to set geq offset to max * ``calibrate_lens_shading`` (also sets colour gains for white balance) - * ``reset_ccm`` + * ``set_background`` """ self.flat_lens_shading() @@ -845,7 +845,6 @@ class StreamingPiCamera2(BaseCamera): self.set_static_green_equalisation() self.set_ce_enable_to_off() self.calibrate_lens_shading() - self.reset_ccm() time.sleep(0.5) self.set_background(portal) From a2900b0c1634a507df0e4b3aa4e7dbeed8792d69 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 30 Oct 2025 12:35:54 +0000 Subject: [PATCH 05/18] Move tuning files into vc4 folder in case we support pisp (Pi5) later and update README --- docs/images/ChiefRayAngle.png | 3 + docs/images/GreenEqualisation.png | 3 + .../things/camera/picamera.py | 1 + .../things/camera/tuning_files/README.md | 125 ------------- .../things/camera/tuning_files/vc4/README.md | 172 ++++++++++++++++++ .../camera/tuning_files/{ => vc4}/imx219.json | 0 .../camera/tuning_files/{ => vc4}/imx477.json | 0 7 files changed, 179 insertions(+), 125 deletions(-) create mode 100644 docs/images/ChiefRayAngle.png create mode 100644 docs/images/GreenEqualisation.png delete mode 100644 src/openflexure_microscope_server/things/camera/tuning_files/README.md create mode 100644 src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md rename src/openflexure_microscope_server/things/camera/tuning_files/{ => vc4}/imx219.json (100%) rename src/openflexure_microscope_server/things/camera/tuning_files/{ => vc4}/imx477.json (100%) diff --git a/docs/images/ChiefRayAngle.png b/docs/images/ChiefRayAngle.png new file mode 100644 index 00000000..6fc03089 --- /dev/null +++ b/docs/images/ChiefRayAngle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36ca9e6e8ee0af71a03dee4a6e2d4405c63d54ea9b123046d39c0187402504da +size 96857 diff --git a/docs/images/GreenEqualisation.png b/docs/images/GreenEqualisation.png new file mode 100644 index 00000000..71cbcd58 --- /dev/null +++ b/docs/images/GreenEqualisation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f65ef3712cb9986cb28f40000ddb283e89fe93671b55ad12d8b6fc057afeac9 +size 111804 diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 2143b138..83e2f147 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -766,6 +766,7 @@ class StreamingPiCamera2(BaseCamera): with self._streaming_picamera(pause_stream=True): self._initialise_picamera() + # TODO make reset CCM reset from default tuning. @lt.thing_action def reset_ccm(self) -> None: """Overwrite the colour correction matrix in camera tuning with default values. diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/README.md b/src/openflexure_microscope_server/things/camera/tuning_files/README.md deleted file mode 100644 index 6e8e2aa4..00000000 --- a/src/openflexure_microscope_server/things/camera/tuning_files/README.md +++ /dev/null @@ -1,125 +0,0 @@ -# A list of all the settings in a IMXxx.json: - -## version: - -I assume this needs to be 2.0 - -## target - -Needs to be bcm2835 - -## Algorithms - -### rpi.black_level - -contains one key-value pair. `black_level: 4096` - -### rpi.dpc - -Defective pixel correction, default off(?) - -### rpi.lux - -For light sensors, not used. Can be emptied? - -### rpi.noise - -``` -"reference_constant": 0, -"reference_slope": 3.67 -``` - -### rpi.geq - -Two elements. offset should be 65535 in all cases we've found. Slope unknown - -### rpi.sdn - -Spatial denoise. Warning on startup asks us to move it into rpi.denoise, even if empty - -### rpi.awb - -Auto white balance. Even though we don't use it, camera can't start without it being minimally populated, which is quite a lot - -### rpi.alsc - -Auto lens shading correction is calibrated by our microscope, and one suggestion is to ship this as a flat table (all 1s) and use that to check if the microscope is calibrated. - -`n_iter: 0` (not adaptive) - -_If we provide a flat one, then reset and disable flatfield correction will do the same thing. I'm also not sure these are persistent if you run one of them, then `ofm restart`_ - -### rpi.contrast - -`ce_enable` should be 0 (disabled). Gamma curve linear or not is an open question - -### rpi.ccm - -We only want to ship one temperature. Currently it's 5000K, the temperature of the LED. However, the diffuser might affect this. - -Different for HQ and version 2 cameras: currently we take the temperatures either side of 5000K from `/usr/share/libcamera/ipa/rpi/pisp/imx219.json` or `/usr/share/libcamera/ipa/rpi/pisp/imx477.json` - -Python code: - -``` -import numpy as np - -temps = {4650: - np.array([ - 2.18174, -0.70887, -0.47287, - -0.70196, 2.76426, -1.06231, - -0.25157, -0.71978, 1.97135 - ]), - 5858: - np.array( - [ - 2.32392, -0.88421, -0.43971, - -0.63821, 2.58348, -0.94527, - -0.28541, -0.54112, 1.82653 - ]) - } -target_temp = 5000 -temp_low, temp_high = sorted(temps.keys()) -alpha = (target_temp - temp_low) / (temp_high - temp_low) - -print(alpha) - -ccm_interp = (1 - alpha) * temps[temp_low] + alpha * temps[temp_high] -print(np.round(ccm_interp, 6)) - -temps = {4559: np.array( - [ - 2.15423, -0.98143, -0.17279, - -0.38131, 2.14763, -0.76632, - -0.10069, -0.54383, 1.64452 - ]), - 5881:np.array( - [ - 2.18464, -0.95493, -0.22971, - -0.36826, 2.00298, -0.63471, - -0.15219, -0.38055, 1.53274 - ]) - } -target_temp = 5000 -temp_low, temp_high = sorted(temps.keys()) -alpha = (target_temp - temp_low) / (temp_high - temp_low) - -print(alpha) - -ccm_interp = (1 - alpha) * temps[temp_low] + alpha * temps[temp_high] -print(np.round(ccm_interp, 6)) -``` - -This will be superseced by using a MacBeth target. - -### rpi.sharpen - -Currently empty. Sharpening might be something to add in post processing, but not here I expect. - -### rpi.hdr - -High dynamic range. Not relevant for us. Currently a short snippet. - -### rpi.sync - -Syncronising between cameras. Not something we're looking at anytime soon. \ No newline at end of file diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md new file mode 100644 index 00000000..a94b53e5 --- /dev/null +++ b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md @@ -0,0 +1,172 @@ +# A list of all the settings in the Raspberry Pi Camera (vc4) tuning files. + +Note that VC4 is the patform used by the Pi for with the broadcom ISP. From Pi 5 onwards Raspberry Pi uses their own ISP (PiSP). These have their own tuning file structure. + +More data is available in this datasheet: https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf + +## version: + +This needs to be 2.0, version one was a normal dictionary of algorithms, tather than this current structure with the algorithms as a list. + +## target + +Needs to be bcm2835. This is the broadcom system on a chip used by vc4. + +## Algorithms + +### rpi.black_level - Sensor Black Level + +Contains one key-value pair. i.e.: `black_level: 4096` + +Note that this is not the actual black level of the sensor, but the black level once the image has been scaled up to 16 bit. So for a 10-bit sensor this number would need to be divided by 2^6. + +### rpi.dpc - Defective Pixel Correction + +This has three strength parameters: + +* 0 - None +* 1 - Normal (default) +* 2 - Strong + +### rpi.lux - Lux Calculation + +This is used to create the lux value. This lux value is not used in the ISP but is sent to the metadata for the image for downstream control algorithms. The camera may crash if this is removed even though we turn off the adaptive algorithms. + +### rpi.noise - Noise Profile + +This calculates the noise profile for the current images. + +Values from the default tuning files for imx219 and imx477 are: + +``` +"reference_constant": 0, +"reference_slope": 3.67 +``` + +These can be optimised using the methods in the camera tuning tool shipped with libcamera. Though most of the camera tuning tool methods make assumptions that are invalid for a microscope. As such any tuning would further consideration. + +### rpi.geq - Green Equalisation + +#### The problems with green equalisation + +This tried to calculate whether adjacent grren pixels should be equal. This interacts very porly with the chief ray angle compesation when using the camera. The imx219 has a lenslet array: + +![](../../../../../../docs/images/ChiefRayAngle.png) + +Normally as shown in (a) the light comes in at an angle as we get to the edge of the sensor due to how close the lens is to the camera. The lenslet array is designed for this and so if you are in the centre (b), or at the edge (d) the light is focussed onto the correct pixel. + +For a microscope the light comes in perpendicular to the sensor, this makes no difference in the centre but at the edge of the image (e) there is a lot of cross talk between pixels. + +But it makes sense that this also strongly affects the green imbalance (which is something I didn't know about, but should, and will need to read about in more detail so we can improve tuning). If we consider the case of red light, we expect that the red pixels are the only illuminated pixels. If we consider what happens on the sensor in the case of the microscope, I think the light will be focussing on the positions shown: + +![](../../../../../../docs/images/GreenEqualisation.png) + +So in the case of the diagonals this is illuminating the green in both the red and the blue row. Where as in the areas we were seeing issues the red light will be strongly illuminating the the greens in one row but not the other. + +The result is that for strongly red on the edges every other geen pixel is illuminated causing an overflow. + +#### Turning off the adaptive green equalisation + +The parameters for geeen equalisation are a `slope` and `offset` + +The "green equalisation" algorithm averages Gr and Gb when they "don't look that different". The threshold below which things get averaged is given by + +`threshold = offset + pixel_value * slope` + +As we always want the green pixels to be averaged to remove the effect above. We can set `offset` the maximum 16-bit value (65535). In this case it will always average green pixels, no matter the pixel value or the value of `slope`. + +### rpi.sdn - Spatial Denoise + +This algorithm is used on the VC4 (Pi 4) but not on PiSP (Pi 5) which uses `rpi.denoise`. A warning on startup asks us to move it into `rpi.denoise` even though we are on VC4 that does not support `rpi.denoise`. + +### rpi.awb - Automatic White Balance + +Even though we don't use it, camera can't start without it being minimally populated. + +### rpi.alsc - Automatic Lens Shading Correction + +Auto lens shading correction is calibrated by our microscope. This contains the tables for luninance, and colour shading. It can contain multiple tables depending on estimated colour temperature (`ct`). As the microscope has fixed lighting we only provide one set of tables as we don't want the correction to be adaptive. + +We also set `n_iter: 0` this stops it adding iterative adaptuion. + + +### rpi.contrast - Contrast + +This does the gamma correction. If `ce_enable` is 1 then the gamma correction is somewhat adaptive. We set this to zero to always use the specified gamma table. + +The table of gamma values is a 1d list of alternating x,y values in the gamma curve. + +We may want to adjust the gamma curve in future. + +### rpi.ccm - Colour Correction Matrix + +It is possible tp ship different CCMs to be used based on the estimated colour temperatire. We only ship one CCM as our illumination colour temperature is fixed to 5000K, the colour temperature of the LED board. However, the diffuser might affect this. + +The CCMs are different for PiCamera HQ and PiCamera version 2. The default uning files for these cameras (`/usr/share/libcamera/ipa/rpi/vc4/imx219.json` or `/usr/share/libcamera/ipa/rpi/vc4/imx477.json`) specify CCMs for a number of temperatures but not for 5000k. As such we linearly interpolate between temperatures below 5k with the following code: + +``` +import numpy as np + +temps = {4650: + np.array([ + 2.18174, -0.70887, -0.47287, + -0.70196, 2.76426, -1.06231, + -0.25157, -0.71978, 1.97135 + ]), + 5858: + np.array( + [ + 2.32392, -0.88421, -0.43971, + -0.63821, 2.58348, -0.94527, + -0.28541, -0.54112, 1.82653 + ]) + } +target_temp = 5000 +temp_low, temp_high = sorted(temps.keys()) +alpha = (target_temp - temp_low) / (temp_high - temp_low) + +print(alpha) + +ccm_interp = (1 - alpha) * temps[temp_low] + alpha * temps[temp_high] +print(np.round(ccm_interp, 6)) + +temps = {4559: np.array( + [ + 2.15423, -0.98143, -0.17279, + -0.38131, 2.14763, -0.76632, + -0.10069, -0.54383, 1.64452 + ]), + 5881:np.array( + [ + 2.18464, -0.95493, -0.22971, + -0.36826, 2.00298, -0.63471, + -0.15219, -0.38055, 1.53274 + ]) + } +target_temp = 5000 +temp_low, temp_high = sorted(temps.keys()) +alpha = (target_temp - temp_low) / (temp_high - temp_low) + +print(alpha) + +ccm_interp = (1 - alpha) * temps[temp_low] + alpha * temps[temp_high] +print(np.round(ccm_interp, 6)) +``` + +#### Calculating our own CCMs + +The CCM calculation is done using a MacBeth target. Lib camera provides a method for this using the Colour Tuning Tool. We will need to adapt this algorithm as they image the target at once and pick out the areas. We have fixed lighting but do not have colour uniformity. + +In the future with a colour calibration slide we can take images of each colour with fixed camera settings to create a CCM using the same calculations as the Camera Tuning Tool. + +### rpi.sharpen - Sharpening + +This is empty. We do not want to be artificially sharpening images as this can add in artifacts. + +### rpi.hdr - High dynamic range + +Not relevant for us, we don't use high dynamic range capture. + +### rpi.sync - Software Camera Synchronisation + +Not relevant for us, we only have 1 camera. diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/imx219.json b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx219.json similarity index 100% rename from src/openflexure_microscope_server/things/camera/tuning_files/imx219.json rename to src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx219.json diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/imx477.json b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx477.json similarity index 100% rename from src/openflexure_microscope_server/things/camera/tuning_files/imx477.json rename to src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx477.json From cec6f4af737a12ae628355ad128cdb8090ecef24 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 30 Oct 2025 13:15:50 +0000 Subject: [PATCH 06/18] Load tuning files in without using Picamera2 --- .../camera/picamera_tuning_file_utils.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 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 b6f12450..399043e2 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 @@ -6,34 +6,34 @@ the future. from typing import Any from copy import deepcopy - -from picamera2 import Picamera2 -import numpy as np import os +import json + +import numpy as np + THIS_DIR = os.path.dirname(os.path.abspath(__file__)) + +class TuningFileError(RuntimeError): + """Raised if the tuning file cannot be loaded for any reason.""" + + def load_default_tuning(sensor_model: str) -> dict: """Load the default tuning file for the camera. This will load the tuning file based on the specified sensor model. """ fname = f"{sensor_model}.json" - custom_path = os.path.join(THIS_DIR, 'tuning_files', fname) + # Note the vc4 here. This locks us to Pi4. We will need to update this to support + # the Raspberry Pi 5. + tuning_path = os.path.join(THIS_DIR, "tuning_files", "vc4", fname) - # Attempt to load custom tuning file from OpenFlexure settings - if os.path.isfile(custom_path): - return Picamera2.load_tuning_file(fname, dir=os.path.join(THIS_DIR, 'tuning_files')) try: - return Picamera2.load_tuning_file(fname) - except RuntimeError: - tuning_dir = "/usr/share/libcamera/ipa/raspberrypi" - # from picamera2 v0.3.9 - # The directory above has been removed from the search path seems - # odd - as that's where the files currently are on a default - # Raspbian image. This may need updating if the files have moved - # in future updates to the system libcamera package - return Picamera2.load_tuning_file(fname, dir=tuning_dir) + with open(tuning_path, "r") as fp: + return json.load(fp) + except (json.decoder.JSONDecodeError, IOError) as e: + raise TuningFileError(f"Could not load tuning from {tuning_path}.") from e def find_tuning_algo(tuning: dict[str, dict], name: str) -> dict[str, Any]: From 01a883e95c31baa89b7729f55b4b020f48fcdd75 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 30 Oct 2025 13:54:23 +0000 Subject: [PATCH 07/18] Reset CCM resets to the CCM in the default tuning file. --- .../things/camera/picamera.py | 48 ++++--------------- .../camera/picamera_tuning_file_utils.py | 19 ++++---- 2 files changed, 20 insertions(+), 47 deletions(-) diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 83e2f147..99463d06 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -749,9 +749,6 @@ 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. - - The value here is interpolated from the IMX219 defaults for the colour temperatures - above and below our LED temperature of 5000K. """ return tuple(tf_utils.get_static_ccm(self.tuning)[0]["ccm"]) @@ -766,41 +763,14 @@ class StreamingPiCamera2(BaseCamera): with self._streaming_picamera(pause_stream=True): self._initialise_picamera() - # TODO make reset CCM reset from default tuning. @lt.thing_action def reset_ccm(self) -> None: - """Overwrite the colour correction matrix in camera tuning with default values. - - These values are from the Raspberry Pi Camera Algorithm and Tuning Guide, page - 45. - """ - # This is flattened 3x3 matrix. See `colour_correction_matrix` - if self._camera_board == "picamera_v2": - col_corr_matrix = [ - 2.222935, - -0.759672, - -0.463262, - -0.683489, - 2.711882, - -1.028399, - -0.261375, - -0.668016, - 1.929391, - ] - else: - # Note the only other option is the HQ - col_corr_matrix = [ - 2.164374, - -0.97259, - -0.191778, - -0.376957, - 2.099377, - -0.722417, - -0.11787, - -0.489362, - 1.607232, - ] - self.colour_correction_matrix = col_corr_matrix + """Overwrite the colour correction matrix in camera tuning with default values.""" + self.tuning = tf_utils.copy_algo_from_other_tuning( + algo="rpi.ccm", + base_tuning_file=self.tuning, + copy_from=self.default_tuning, + ) @lt.thing_action def set_static_green_equalisation(self, offset: int = 65535) -> None: @@ -1018,8 +988,10 @@ class StreamingPiCamera2(BaseCamera): by the Raspberry Pi camera. """ with self._streaming_picamera(pause_stream=True): - self.tuning = tf_utils.copy_tuning_with_alsc_section_from_other( - base_tuning_file=self.tuning, copy_alsc_from=self.default_tuning + self.tuning = tf_utils.copy_algo_from_other_tuning( + algo="rpi.alsc", + base_tuning_file=self.tuning, + copy_from=self.default_tuning, ) self._initialise_picamera() 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 399043e2..14b51e3e 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 @@ -30,8 +30,8 @@ def load_default_tuning(sensor_model: str) -> dict: tuning_path = os.path.join(THIS_DIR, "tuning_files", "vc4", fname) try: - with open(tuning_path, "r") as fp: - return json.load(fp) + with open(tuning_path, "r", encoding="utf-8") as file_obj: + return json.load(file_obj) except (json.decoder.JSONDecodeError, IOError) as e: raise TuningFileError(f"Could not load tuning from {tuning_path}.") from e @@ -165,13 +165,14 @@ def ce_enable_is_static(tuning: dict) -> bool: return contrast["ce_enable"] == 0 -def copy_tuning_with_alsc_section_from_other( - *, base_tuning_file: dict, copy_alsc_from: dict +def copy_algo_from_other_tuning( + algo: str, *, base_tuning_file: dict, copy_from: dict ) -> dict: - """Return a copy of tuning_file with the lens shading correction from another file. + """Return a copy of tuning_file with an algorithm copied from another file. - All parameters are keyword only for clarity. + Tuning dict arguments are keyword only for clarity. + :param algo: The algorithm to copy. Eg ``rpi.alsc`` for lens shading correction. :param base_tuning_file: The tuning file to copy. :param copy_alsc_from: The tuning file to take the alsc section from. :return: A deep copy of base_tuning_file with the alsc section copied in from the @@ -179,10 +180,10 @@ def copy_tuning_with_alsc_section_from_other( """ output_tuning = deepcopy(base_tuning_file) # Find the relevant sub-dict for each tuning file - from_i = _index_of_algorithm(copy_alsc_from["algorithms"], "rpi.alsc") - to_i = _index_of_algorithm(base_tuning_file["algorithms"], "rpi.alsc") + from_i = _index_of_algorithm(copy_from["algorithms"], algo) + to_i = _index_of_algorithm(base_tuning_file["algorithms"], algo) # Updating the dictionary in place. - output_tuning["algorithms"][to_i] = deepcopy(copy_alsc_from["algorithms"][from_i]) + output_tuning["algorithms"][to_i] = deepcopy(copy_from["algorithms"][from_i]) return output_tuning From f74a51866a78e6f9b6eb758f32d91880459c4252 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 30 Oct 2025 14:52:59 +0000 Subject: [PATCH 08/18] Adjust tuning files to have adaptive algorithms turned off. --- .../things/camera/tuning_files/vc4/README.md | 6 + .../camera/tuning_files/vc4/imx219.json | 1294 ++--------------- .../camera/tuning_files/vc4/imx477.json | 1294 ++--------------- 3 files changed, 170 insertions(+), 2424 deletions(-) diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md index a94b53e5..dabaafec 100644 --- a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md +++ b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md @@ -83,12 +83,18 @@ This algorithm is used on the VC4 (Pi 4) but not on PiSP (Pi 5) which uses `rpi. Even though we don't use it, camera can't start without it being minimally populated. +### rpi.agc - Automatic Gain Control / Automatic Exposure Control + +For calculating the shutter time of the sensor if AeEnable is on. Only the required fields are populated with minimal data as AeEnable is always off. + ### rpi.alsc - Automatic Lens Shading Correction Auto lens shading correction is calibrated by our microscope. This contains the tables for luninance, and colour shading. It can contain multiple tables depending on estimated colour temperature (`ct`). As the microscope has fixed lighting we only provide one set of tables as we don't want the correction to be adaptive. We also set `n_iter: 0` this stops it adding iterative adaptuion. +As there is only one table the colour temperature is ignored. We set the colour temperature to 1234 in the tuning file to make it clear that we have not calibrated it on the microscope. We set the colour temperature to 5000 (our actual illumination colour temperature) once recalibrated. + ### rpi.contrast - Contrast diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx219.json b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx219.json index e6fbb9ab..d92c2b24 100644 --- a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx219.json +++ b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx219.json @@ -37,90 +37,17 @@ { "rpi.awb": { - "priors": [ - { - "lux": 0, - "prior": - [ - 2000, 1.0, - 3000, 0.0, - 13000, 0.0 - ] - }, - { - "lux": 800, - "prior": - [ - 2000, 0.0, - 6000, 2.0, - 13000, 2.0 - ] - }, - { - "lux": 1500, - "prior": - [ - 2000, 0.0, - 4000, 1.0, - 6000, 6.0, - 6500, 7.0, - 7000, 1.0, - 13000, 1.0 - ] - } - ], "modes": { "auto": { - "lo": 2500, - "hi": 7700 - }, - "incandescent": - { - "lo": 2500, - "hi": 3000 - }, - "tungsten": - { - "lo": 3000, - "hi": 3500 - }, - "fluorescent": - { - "lo": 4000, - "hi": 4700 - }, - "indoor": - { - "lo": 3000, - "hi": 5000 - }, - "daylight": - { - "lo": 5500, - "hi": 6500 - }, - "cloudy": - { - "lo": 7000, + "lo": 1000, "hi": 8000 } }, "bayes": 1, - "ct_curve": - [ - 2860.0, 0.9514, 0.4156, - 2960.0, 0.9289, 0.4372, - 3603.0, 0.8305, 0.5251, - 4650.0, 0.6756, 0.6433, - 5858.0, 0.6193, 0.6807, - 7580.0, 0.5019, 0.7495 - ], "sensitivity_r": 1.0, - "sensitivity_b": 1.0, - "transverse_pos": 0.03392, - "transverse_neg": 0.034 + "sensitivity_b": 1.0 } }, { @@ -129,111 +56,13 @@ { "metering_modes": { "centre-weighted": { - "weights": [ - 3, - 3, - 3, - 2, - 2, - 2, - 2, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - "spot": { - "weights": [ - 2, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - }, - "matrix": { - "weights": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ] + "weights": [3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0] } }, "exposure_modes": { "normal": { - "shutter": [ - 100, - 10000, - 30000, - 60000, - 66666 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 8.0 - ] - }, - "short": { - "shutter": [ - 100, - 5000, - 10000, - 20000, - 33333 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 8.0 - ] - }, - "long": { - "shutter": [ - 100, - 10000, - 30000, - 60000, - 120000 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 12.0 - ] + "shutter": [100, 10000, 30000, 60000, 66666], + "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0] } }, "constraint_modes": { @@ -243,402 +72,16 @@ "q_lo": 0.98, "q_hi": 1.0, "y_target": [ - 0, - 0.5, - 1000, - 0.5 - ] - } - ], - "highlight": [ - { - "bound": "LOWER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.5, - 1000, - 0.5 - ] - }, - { - "bound": "UPPER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.8, - 1000, - 0.8 - ] - } - ], - "shadows": [ - { - "bound": "LOWER", - "q_lo": 0.0, - "q_hi": 0.5, - "y_target": [ - 0, - 0.17, - 1000, - 0.17 + 0, 0.5, + 1000, 0.5 ] } ] }, "y_target": [ - 0, - 0.16, - 1000, - 0.165, - 10000, - 0.17 - ] - }, - { - "base_ev": 0.125, - "metering_modes": { - "centre-weighted": { - "weights": [ - 3, - 3, - 3, - 2, - 2, - 2, - 2, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - "spot": { - "weights": [ - 2, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - }, - "matrix": { - "weights": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ] - } - }, - "exposure_modes": { - "normal": { - "shutter": [ - 100, - 10000, - 30000, - 60000, - 66666 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 8.0 - ] - }, - "short": { - "shutter": [ - 100, - 5000, - 10000, - 20000, - 33333 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 8.0 - ] - }, - "long": { - "shutter": [ - 100, - 10000, - 30000, - 60000, - 120000 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 12.0 - ] - } - }, - "constraint_modes": { - "normal": [ - { - "bound": "LOWER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.5, - 1000, - 0.5 - ] - } - ], - "highlight": [ - { - "bound": "LOWER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.5, - 1000, - 0.5 - ] - }, - { - "bound": "UPPER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.8, - 1000, - 0.8 - ] - } - ], - "shadows": [ - { - "bound": "LOWER", - "q_lo": 0.0, - "q_hi": 0.5, - "y_target": [ - 0, - 0.17, - 1000, - 0.17 - ] - } - ] - }, - "y_target": [ - 0, - 0.16, - 1000, - 0.165, - 10000, - 0.17 - ] - }, - { - "base_ev": 1.5, - "metering_modes": { - "centre-weighted": { - "weights": [ - 3, - 3, - 3, - 2, - 2, - 2, - 2, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - "spot": { - "weights": [ - 2, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - }, - "matrix": { - "weights": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ] - } - }, - "exposure_modes": { - "normal": { - "shutter": [ - 100, - 10000, - 30000, - 60000, - 66666 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 8.0 - ] - }, - "short": { - "shutter": [ - 100, - 5000, - 10000, - 20000, - 33333 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 8.0 - ] - }, - "long": { - "shutter": [ - 100, - 10000, - 30000, - 60000, - 120000 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 12.0 - ] - } - }, - "constraint_modes": { - "normal": [ - { - "bound": "LOWER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.5, - 1000, - 0.5 - ] - } - ], - "highlight": [ - { - "bound": "LOWER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.5, - 1000, - 0.5 - ] - }, - { - "bound": "UPPER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.8, - 1000, - 0.8 - ] - } - ], - "shadows": [ - { - "bound": "LOWER", - "q_lo": 0.0, - "q_hi": 0.5, - "y_target": [ - 0, - 0.17, - 1000, - 0.17 - ] - } - ] - }, - "y_target": [ - 0, - 0.16, - 1000, - 0.165, - 10000, - 0.17 + 0, 0.16, + 1000, 0.165, + 10000, 0.17 ] } ] @@ -651,595 +94,55 @@ "luminance_strength": 1.0, "calibrations_Cr": [ { - "ct": 4500, + "ct": 1234, "table": [ - 0.941, - 0.953, - 0.962, - 0.969, - 0.971, - 0.966, - 0.961, - 0.958, - 0.96, - 0.967, - 0.977, - 0.984, - 0.981, - 0.97, - 0.958, - 0.95, - 0.924, - 0.934, - 0.953, - 0.974, - 0.986, - 0.992, - 0.993, - 0.994, - 0.996, - 0.999, - 1.003, - 0.998, - 0.983, - 0.959, - 0.937, - 0.926, - 0.9, - 0.914, - 0.938, - 0.967, - 0.994, - 1.011, - 1.021, - 1.026, - 1.027, - 1.027, - 1.023, - 1.007, - 0.977, - 0.941, - 0.915, - 0.902, - 0.879, - 0.892, - 0.921, - 0.96, - 0.997, - 1.024, - 1.039, - 1.045, - 1.047, - 1.045, - 1.037, - 1.012, - 0.967, - 0.923, - 0.893, - 0.878, - 0.863, - 0.877, - 0.908, - 0.953, - 0.999, - 1.032, - 1.047, - 1.053, - 1.056, - 1.055, - 1.043, - 1.01, - 0.958, - 0.908, - 0.875, - 0.862, - 0.854, - 0.868, - 0.898, - 0.946, - 0.997, - 1.033, - 1.048, - 1.055, - 1.057, - 1.057, - 1.045, - 1.009, - 0.952, - 0.898, - 0.866, - 0.852, - 0.852, - 0.865, - 0.896, - 0.942, - 0.993, - 1.029, - 1.046, - 1.053, - 1.054, - 1.054, - 1.042, - 1.007, - 0.951, - 0.897, - 0.865, - 0.852, - 0.857, - 0.871, - 0.9, - 0.943, - 0.987, - 1.019, - 1.035, - 1.043, - 1.045, - 1.044, - 1.034, - 1.003, - 0.953, - 0.904, - 0.873, - 0.86, - 0.869, - 0.881, - 0.906, - 0.941, - 0.974, - 0.996, - 1.007, - 1.014, - 1.017, - 1.017, - 1.012, - 0.993, - 0.956, - 0.916, - 0.888, - 0.874, - 0.884, - 0.89, - 0.905, - 0.926, - 0.943, - 0.95, - 0.952, - 0.955, - 0.96, - 0.964, - 0.971, - 0.968, - 0.949, - 0.923, - 0.901, - 0.892, - 0.897, - 0.895, - 0.897, - 0.9, - 0.899, - 0.892, - 0.885, - 0.884, - 0.888, - 0.9, - 0.917, - 0.929, - 0.93, - 0.922, - 0.912, - 0.91, - 0.905, - 0.894, - 0.885, - 0.874, - 0.86, - 0.847, - 0.837, - 0.834, - 0.839, - 0.852, - 0.872, - 0.893, - 0.908, - 0.916, - 0.92, - 0.925 + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ] } ], "calibrations_Cb": [ { - "ct": 4500, + "ct": 1234, "table": [ - 1.492, - 1.478, - 1.482, - 1.487, - 1.49, - 1.495, - 1.505, - 1.51, - 1.507, - 1.492, - 1.484, - 1.48, - 1.471, - 1.464, - 1.457, - 1.467, - 1.477, - 1.479, - 1.492, - 1.508, - 1.527, - 1.545, - 1.568, - 1.585, - 1.582, - 1.557, - 1.537, - 1.513, - 1.486, - 1.456, - 1.432, - 1.429, - 1.464, - 1.472, - 1.498, - 1.533, - 1.567, - 1.603, - 1.644, - 1.674, - 1.669, - 1.632, - 1.591, - 1.55, - 1.5, - 1.449, - 1.408, - 1.398, - 1.456, - 1.474, - 1.513, - 1.563, - 1.62, - 1.677, - 1.735, - 1.78, - 1.767, - 1.711, - 1.653, - 1.594, - 1.517, - 1.443, - 1.389, - 1.37, - 1.458, - 1.48, - 1.532, - 1.601, - 1.677, - 1.757, - 1.838, - 1.893, - 1.876, - 1.803, - 1.724, - 1.64, - 1.54, - 1.445, - 1.375, - 1.353, - 1.457, - 1.484, - 1.544, - 1.624, - 1.715, - 1.809, - 1.901, - 1.957, - 1.943, - 1.867, - 1.774, - 1.676, - 1.557, - 1.448, - 1.373, - 1.344, - 1.459, - 1.484, - 1.543, - 1.623, - 1.713, - 1.802, - 1.891, - 1.948, - 1.934, - 1.858, - 1.771, - 1.677, - 1.561, - 1.45, - 1.376, - 1.348, - 1.463, - 1.488, - 1.54, - 1.608, - 1.682, - 1.759, - 1.835, - 1.887, - 1.871, - 1.805, - 1.732, - 1.656, - 1.557, - 1.46, - 1.391, - 1.367, - 1.475, - 1.497, - 1.536, - 1.588, - 1.645, - 1.703, - 1.76, - 1.798, - 1.787, - 1.739, - 1.683, - 1.627, - 1.554, - 1.479, - 1.422, - 1.399, - 1.495, - 1.505, - 1.531, - 1.564, - 1.598, - 1.631, - 1.667, - 1.691, - 1.685, - 1.652, - 1.619, - 1.587, - 1.543, - 1.492, - 1.452, - 1.44, - 1.514, - 1.512, - 1.519, - 1.527, - 1.536, - 1.548, - 1.561, - 1.572, - 1.564, - 1.551, - 1.541, - 1.532, - 1.52, - 1.497, - 1.479, - 1.478, - 1.53, - 1.515, - 1.504, - 1.494, - 1.484, - 1.479, - 1.478, - 1.478, - 1.475, - 1.47, - 1.473, - 1.483, - 1.494, - 1.496, - 1.502, - 1.513 + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ] } ], - "luminance_lut": [ - 1.891, - 2.158, - 1.778, - 1.606, - 1.57, - 1.543, - 1.533, - 1.555, - 1.617, - 1.723, - 1.873, - 2.054, - 2.249, - 2.419, - 2.543, - 2.585, - 1.574, - 1.675, - 1.749, - 1.543, - 1.467, - 1.403, - 1.369, - 1.377, - 1.435, - 1.544, - 1.71, - 1.928, - 2.18, - 2.418, - 2.593, - 2.679, - 1.587, - 1.589, - 1.547, - 1.458, - 1.352, - 1.26, - 1.209, - 1.205, - 1.255, - 1.362, - 1.536, - 1.781, - 2.08, - 2.368, - 2.597, - 2.718, - 1.605, - 1.587, - 1.515, - 1.389, - 1.257, - 1.149, - 1.088, - 1.079, - 1.124, - 1.222, - 1.392, - 1.651, - 1.982, - 2.308, - 2.567, - 2.721, - 1.621, - 1.585, - 1.491, - 1.348, - 1.2, - 1.085, - 1.024, - 1.014, - 1.053, - 1.146, - 1.314, - 1.58, - 1.922, - 2.268, - 2.55, - 2.728, - 1.652, - 1.609, - 1.506, - 1.352, - 1.187, - 1.07, - 1.01, - 1.0, - 1.036, - 1.122, - 1.293, - 1.569, - 1.925, - 2.287, - 2.575, - 2.77, - 1.708, - 1.67, - 1.558, - 1.402, - 1.233, - 1.105, - 1.042, - 1.032, - 1.066, - 1.157, - 1.335, - 1.621, - 1.992, - 2.367, - 2.654, - 2.85, - 1.794, - 1.765, - 1.657, - 1.501, - 1.335, - 1.202, - 1.13, - 1.116, - 1.156, - 1.261, - 1.455, - 1.756, - 2.132, - 2.507, - 2.792, - 2.972, - 1.894, - 1.892, - 1.81, - 1.67, - 1.507, - 1.375, - 1.297, - 1.282, - 1.335, - 1.455, - 1.666, - 1.972, - 2.343, - 2.699, - 2.968, - 3.121, - 1.986, - 2.033, - 2.0, - 1.894, - 1.754, - 1.633, - 1.563, - 1.556, - 1.615, - 1.756, - 1.975, - 2.276, - 2.623, - 2.944, - 3.172, - 3.283, - 2.053, - 2.151, - 2.175, - 2.121, - 2.027, - 1.929, - 1.879, - 1.879, - 1.953, - 2.098, - 2.316, - 2.609, - 2.92, - 3.183, - 3.35, - 3.39, - 2.072, - 2.216, - 2.283, - 2.284, - 2.235, - 2.181, - 2.146, - 2.156, - 2.236, - 2.378, - 2.584, - 2.846, - 3.11, - 3.324, - 3.416, - 3.395 + "luminance_lut":[ + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "sigma": 0.00381, "sigma_Cb": 0.00216 @@ -1249,72 +152,39 @@ "rpi.contrast": { "ce_enable": 0, "gamma_curve": [ - 0, - 0, - 1024, - 5040, - 2048, - 9338, - 3072, - 12356, - 4096, - 15312, - 5120, - 18051, - 6144, - 20790, - 7168, - 23193, - 8192, - 25744, - 9216, - 27942, - 10240, - 30035, - 11264, - 32005, - 12288, - 33975, - 13312, - 35815, - 14336, - 37600, - 15360, - 39168, - 16384, - 40642, - 18432, - 43379, - 20480, - 45749, - 22528, - 47753, - 24576, - 49621, - 26624, - 51253, - 28672, - 52698, - 30720, - 53796, - 32768, - 54876, - 36864, - 57012, - 40960, - 58656, - 45056, - 59954, - 49152, - 61183, - 53248, - 62355, - 57344, - 63419, - 61440, - 64476, - 65535, - 65535 + 0, 0, + 1024, 5040, + 2048, 9338, + 3072, 12356, + 4096, 15312, + 5120, 18051, + 6144, 20790, + 7168, 23193, + 8192, 25744, + 9216, 27942, + 10240, 30035, + 11264, 32005, + 12288, 33975, + 13312, 35815, + 14336, 37600, + 15360, 39168, + 16384, 40642, + 18432, 43379, + 20480, 45749, + 22528, 47753, + 24576, 49621, + 26624, 51253, + 28672, 52698, + 30720, 53796, + 32768, 54876, + 36864, 57012, + 40960, 58656, + 45056, 59954, + 49152, 61183, + 53248, 62355, + 57344, 63419, + 61440, 64476, + 65535, 65535 ] } }, diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx477.json b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx477.json index 93923d5a..e518cf07 100644 --- a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx477.json +++ b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx477.json @@ -37,90 +37,17 @@ { "rpi.awb": { - "priors": [ - { - "lux": 0, - "prior": - [ - 2000, 1.0, - 3000, 0.0, - 13000, 0.0 - ] - }, - { - "lux": 800, - "prior": - [ - 2000, 0.0, - 6000, 2.0, - 13000, 2.0 - ] - }, - { - "lux": 1500, - "prior": - [ - 2000, 0.0, - 4000, 1.0, - 6000, 6.0, - 6500, 7.0, - 7000, 1.0, - 13000, 1.0 - ] - } - ], "modes": { "auto": { - "lo": 2500, - "hi": 7700 - }, - "incandescent": - { - "lo": 2500, - "hi": 3000 - }, - "tungsten": - { - "lo": 3000, - "hi": 3500 - }, - "fluorescent": - { - "lo": 4000, - "hi": 4700 - }, - "indoor": - { - "lo": 3000, - "hi": 5000 - }, - "daylight": - { - "lo": 5500, - "hi": 6500 - }, - "cloudy": - { - "lo": 7000, + "lo": 1000, "hi": 8000 } }, "bayes": 1, - "ct_curve": - [ - 2860.0, 0.9514, 0.4156, - 2960.0, 0.9289, 0.4372, - 3603.0, 0.8305, 0.5251, - 4650.0, 0.6756, 0.6433, - 5858.0, 0.6193, 0.6807, - 7580.0, 0.5019, 0.7495 - ], "sensitivity_r": 1.0, - "sensitivity_b": 1.0, - "transverse_pos": 0.03392, - "transverse_neg": 0.034 + "sensitivity_b": 1.0 } }, { @@ -129,111 +56,13 @@ { "metering_modes": { "centre-weighted": { - "weights": [ - 3, - 3, - 3, - 2, - 2, - 2, - 2, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - "spot": { - "weights": [ - 2, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - }, - "matrix": { - "weights": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ] + "weights": [3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0] } }, "exposure_modes": { "normal": { - "shutter": [ - 100, - 10000, - 30000, - 60000, - 66666 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 8.0 - ] - }, - "short": { - "shutter": [ - 100, - 5000, - 10000, - 20000, - 33333 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 8.0 - ] - }, - "long": { - "shutter": [ - 100, - 10000, - 30000, - 60000, - 120000 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 12.0 - ] + "shutter": [100, 10000, 30000, 60000, 66666], + "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0] } }, "constraint_modes": { @@ -243,402 +72,16 @@ "q_lo": 0.98, "q_hi": 1.0, "y_target": [ - 0, - 0.5, - 1000, - 0.5 - ] - } - ], - "highlight": [ - { - "bound": "LOWER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.5, - 1000, - 0.5 - ] - }, - { - "bound": "UPPER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.8, - 1000, - 0.8 - ] - } - ], - "shadows": [ - { - "bound": "LOWER", - "q_lo": 0.0, - "q_hi": 0.5, - "y_target": [ - 0, - 0.17, - 1000, - 0.17 + 0, 0.5, + 1000, 0.5 ] } ] }, "y_target": [ - 0, - 0.16, - 1000, - 0.165, - 10000, - 0.17 - ] - }, - { - "base_ev": 0.125, - "metering_modes": { - "centre-weighted": { - "weights": [ - 3, - 3, - 3, - 2, - 2, - 2, - 2, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - "spot": { - "weights": [ - 2, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - }, - "matrix": { - "weights": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ] - } - }, - "exposure_modes": { - "normal": { - "shutter": [ - 100, - 10000, - 30000, - 60000, - 66666 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 8.0 - ] - }, - "short": { - "shutter": [ - 100, - 5000, - 10000, - 20000, - 33333 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 8.0 - ] - }, - "long": { - "shutter": [ - 100, - 10000, - 30000, - 60000, - 120000 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 12.0 - ] - } - }, - "constraint_modes": { - "normal": [ - { - "bound": "LOWER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.5, - 1000, - 0.5 - ] - } - ], - "highlight": [ - { - "bound": "LOWER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.5, - 1000, - 0.5 - ] - }, - { - "bound": "UPPER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.8, - 1000, - 0.8 - ] - } - ], - "shadows": [ - { - "bound": "LOWER", - "q_lo": 0.0, - "q_hi": 0.5, - "y_target": [ - 0, - 0.17, - 1000, - 0.17 - ] - } - ] - }, - "y_target": [ - 0, - 0.16, - 1000, - 0.165, - 10000, - 0.17 - ] - }, - { - "base_ev": 1.5, - "metering_modes": { - "centre-weighted": { - "weights": [ - 3, - 3, - 3, - 2, - 2, - 2, - 2, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0 - ] - }, - "spot": { - "weights": [ - 2, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - }, - "matrix": { - "weights": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ] - } - }, - "exposure_modes": { - "normal": { - "shutter": [ - 100, - 10000, - 30000, - 60000, - 66666 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 8.0 - ] - }, - "short": { - "shutter": [ - 100, - 5000, - 10000, - 20000, - 33333 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 8.0 - ] - }, - "long": { - "shutter": [ - 100, - 10000, - 30000, - 60000, - 120000 - ], - "gain": [ - 1.0, - 2.0, - 4.0, - 6.0, - 12.0 - ] - } - }, - "constraint_modes": { - "normal": [ - { - "bound": "LOWER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.5, - 1000, - 0.5 - ] - } - ], - "highlight": [ - { - "bound": "LOWER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.5, - 1000, - 0.5 - ] - }, - { - "bound": "UPPER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": [ - 0, - 0.8, - 1000, - 0.8 - ] - } - ], - "shadows": [ - { - "bound": "LOWER", - "q_lo": 0.0, - "q_hi": 0.5, - "y_target": [ - 0, - 0.17, - 1000, - 0.17 - ] - } - ] - }, - "y_target": [ - 0, - 0.16, - 1000, - 0.165, - 10000, - 0.17 + 0, 0.16, + 1000, 0.165, + 10000, 0.17 ] } ] @@ -651,595 +94,55 @@ "luminance_strength": 1.0, "calibrations_Cr": [ { - "ct": 4500, + "ct": 1234, "table": [ - 0.941, - 0.953, - 0.962, - 0.969, - 0.971, - 0.966, - 0.961, - 0.958, - 0.96, - 0.967, - 0.977, - 0.984, - 0.981, - 0.97, - 0.958, - 0.95, - 0.924, - 0.934, - 0.953, - 0.974, - 0.986, - 0.992, - 0.993, - 0.994, - 0.996, - 0.999, - 1.003, - 0.998, - 0.983, - 0.959, - 0.937, - 0.926, - 0.9, - 0.914, - 0.938, - 0.967, - 0.994, - 1.011, - 1.021, - 1.026, - 1.027, - 1.027, - 1.023, - 1.007, - 0.977, - 0.941, - 0.915, - 0.902, - 0.879, - 0.892, - 0.921, - 0.96, - 0.997, - 1.024, - 1.039, - 1.045, - 1.047, - 1.045, - 1.037, - 1.012, - 0.967, - 0.923, - 0.893, - 0.878, - 0.863, - 0.877, - 0.908, - 0.953, - 0.999, - 1.032, - 1.047, - 1.053, - 1.056, - 1.055, - 1.043, - 1.01, - 0.958, - 0.908, - 0.875, - 0.862, - 0.854, - 0.868, - 0.898, - 0.946, - 0.997, - 1.033, - 1.048, - 1.055, - 1.057, - 1.057, - 1.045, - 1.009, - 0.952, - 0.898, - 0.866, - 0.852, - 0.852, - 0.865, - 0.896, - 0.942, - 0.993, - 1.029, - 1.046, - 1.053, - 1.054, - 1.054, - 1.042, - 1.007, - 0.951, - 0.897, - 0.865, - 0.852, - 0.857, - 0.871, - 0.9, - 0.943, - 0.987, - 1.019, - 1.035, - 1.043, - 1.045, - 1.044, - 1.034, - 1.003, - 0.953, - 0.904, - 0.873, - 0.86, - 0.869, - 0.881, - 0.906, - 0.941, - 0.974, - 0.996, - 1.007, - 1.014, - 1.017, - 1.017, - 1.012, - 0.993, - 0.956, - 0.916, - 0.888, - 0.874, - 0.884, - 0.89, - 0.905, - 0.926, - 0.943, - 0.95, - 0.952, - 0.955, - 0.96, - 0.964, - 0.971, - 0.968, - 0.949, - 0.923, - 0.901, - 0.892, - 0.897, - 0.895, - 0.897, - 0.9, - 0.899, - 0.892, - 0.885, - 0.884, - 0.888, - 0.9, - 0.917, - 0.929, - 0.93, - 0.922, - 0.912, - 0.91, - 0.905, - 0.894, - 0.885, - 0.874, - 0.86, - 0.847, - 0.837, - 0.834, - 0.839, - 0.852, - 0.872, - 0.893, - 0.908, - 0.916, - 0.92, - 0.925 + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ] } ], "calibrations_Cb": [ { - "ct": 4500, + "ct": 1234, "table": [ - 1.492, - 1.478, - 1.482, - 1.487, - 1.49, - 1.495, - 1.505, - 1.51, - 1.507, - 1.492, - 1.484, - 1.48, - 1.471, - 1.464, - 1.457, - 1.467, - 1.477, - 1.479, - 1.492, - 1.508, - 1.527, - 1.545, - 1.568, - 1.585, - 1.582, - 1.557, - 1.537, - 1.513, - 1.486, - 1.456, - 1.432, - 1.429, - 1.464, - 1.472, - 1.498, - 1.533, - 1.567, - 1.603, - 1.644, - 1.674, - 1.669, - 1.632, - 1.591, - 1.55, - 1.5, - 1.449, - 1.408, - 1.398, - 1.456, - 1.474, - 1.513, - 1.563, - 1.62, - 1.677, - 1.735, - 1.78, - 1.767, - 1.711, - 1.653, - 1.594, - 1.517, - 1.443, - 1.389, - 1.37, - 1.458, - 1.48, - 1.532, - 1.601, - 1.677, - 1.757, - 1.838, - 1.893, - 1.876, - 1.803, - 1.724, - 1.64, - 1.54, - 1.445, - 1.375, - 1.353, - 1.457, - 1.484, - 1.544, - 1.624, - 1.715, - 1.809, - 1.901, - 1.957, - 1.943, - 1.867, - 1.774, - 1.676, - 1.557, - 1.448, - 1.373, - 1.344, - 1.459, - 1.484, - 1.543, - 1.623, - 1.713, - 1.802, - 1.891, - 1.948, - 1.934, - 1.858, - 1.771, - 1.677, - 1.561, - 1.45, - 1.376, - 1.348, - 1.463, - 1.488, - 1.54, - 1.608, - 1.682, - 1.759, - 1.835, - 1.887, - 1.871, - 1.805, - 1.732, - 1.656, - 1.557, - 1.46, - 1.391, - 1.367, - 1.475, - 1.497, - 1.536, - 1.588, - 1.645, - 1.703, - 1.76, - 1.798, - 1.787, - 1.739, - 1.683, - 1.627, - 1.554, - 1.479, - 1.422, - 1.399, - 1.495, - 1.505, - 1.531, - 1.564, - 1.598, - 1.631, - 1.667, - 1.691, - 1.685, - 1.652, - 1.619, - 1.587, - 1.543, - 1.492, - 1.452, - 1.44, - 1.514, - 1.512, - 1.519, - 1.527, - 1.536, - 1.548, - 1.561, - 1.572, - 1.564, - 1.551, - 1.541, - 1.532, - 1.52, - 1.497, - 1.479, - 1.478, - 1.53, - 1.515, - 1.504, - 1.494, - 1.484, - 1.479, - 1.478, - 1.478, - 1.475, - 1.47, - 1.473, - 1.483, - 1.494, - 1.496, - 1.502, - 1.513 + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ] } ], - "luminance_lut": [ - 1.891, - 2.158, - 1.778, - 1.606, - 1.57, - 1.543, - 1.533, - 1.555, - 1.617, - 1.723, - 1.873, - 2.054, - 2.249, - 2.419, - 2.543, - 2.585, - 1.574, - 1.675, - 1.749, - 1.543, - 1.467, - 1.403, - 1.369, - 1.377, - 1.435, - 1.544, - 1.71, - 1.928, - 2.18, - 2.418, - 2.593, - 2.679, - 1.587, - 1.589, - 1.547, - 1.458, - 1.352, - 1.26, - 1.209, - 1.205, - 1.255, - 1.362, - 1.536, - 1.781, - 2.08, - 2.368, - 2.597, - 2.718, - 1.605, - 1.587, - 1.515, - 1.389, - 1.257, - 1.149, - 1.088, - 1.079, - 1.124, - 1.222, - 1.392, - 1.651, - 1.982, - 2.308, - 2.567, - 2.721, - 1.621, - 1.585, - 1.491, - 1.348, - 1.2, - 1.085, - 1.024, - 1.014, - 1.053, - 1.146, - 1.314, - 1.58, - 1.922, - 2.268, - 2.55, - 2.728, - 1.652, - 1.609, - 1.506, - 1.352, - 1.187, - 1.07, - 1.01, - 1.0, - 1.036, - 1.122, - 1.293, - 1.569, - 1.925, - 2.287, - 2.575, - 2.77, - 1.708, - 1.67, - 1.558, - 1.402, - 1.233, - 1.105, - 1.042, - 1.032, - 1.066, - 1.157, - 1.335, - 1.621, - 1.992, - 2.367, - 2.654, - 2.85, - 1.794, - 1.765, - 1.657, - 1.501, - 1.335, - 1.202, - 1.13, - 1.116, - 1.156, - 1.261, - 1.455, - 1.756, - 2.132, - 2.507, - 2.792, - 2.972, - 1.894, - 1.892, - 1.81, - 1.67, - 1.507, - 1.375, - 1.297, - 1.282, - 1.335, - 1.455, - 1.666, - 1.972, - 2.343, - 2.699, - 2.968, - 3.121, - 1.986, - 2.033, - 2.0, - 1.894, - 1.754, - 1.633, - 1.563, - 1.556, - 1.615, - 1.756, - 1.975, - 2.276, - 2.623, - 2.944, - 3.172, - 3.283, - 2.053, - 2.151, - 2.175, - 2.121, - 2.027, - 1.929, - 1.879, - 1.879, - 1.953, - 2.098, - 2.316, - 2.609, - 2.92, - 3.183, - 3.35, - 3.39, - 2.072, - 2.216, - 2.283, - 2.284, - 2.235, - 2.181, - 2.146, - 2.156, - 2.236, - 2.378, - 2.584, - 2.846, - 3.11, - 3.324, - 3.416, - 3.395 + "luminance_lut":[ + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "sigma": 0.00381, "sigma_Cb": 0.00216 @@ -1249,72 +152,39 @@ "rpi.contrast": { "ce_enable": 0, "gamma_curve": [ - 0, - 0, - 1024, - 5040, - 2048, - 9338, - 3072, - 12356, - 4096, - 15312, - 5120, - 18051, - 6144, - 20790, - 7168, - 23193, - 8192, - 25744, - 9216, - 27942, - 10240, - 30035, - 11264, - 32005, - 12288, - 33975, - 13312, - 35815, - 14336, - 37600, - 15360, - 39168, - 16384, - 40642, - 18432, - 43379, - 20480, - 45749, - 22528, - 47753, - 24576, - 49621, - 26624, - 51253, - 28672, - 52698, - 30720, - 53796, - 32768, - 54876, - 36864, - 57012, - 40960, - 58656, - 45056, - 59954, - 49152, - 61183, - 53248, - 62355, - 57344, - 63419, - 61440, - 64476, - 65535, - 65535 + 0, 0, + 1024, 5040, + 2048, 9338, + 3072, 12356, + 4096, 15312, + 5120, 18051, + 6144, 20790, + 7168, 23193, + 8192, 25744, + 9216, 27942, + 10240, 30035, + 11264, 32005, + 12288, 33975, + 13312, 35815, + 14336, 37600, + 15360, 39168, + 16384, 40642, + 18432, 43379, + 20480, 45749, + 22528, 47753, + 24576, 49621, + 26624, 51253, + 28672, 52698, + 30720, 53796, + 32768, 54876, + 36864, 57012, + 40960, 58656, + 45056, 59954, + 49152, 61183, + 53248, 62355, + 57344, 63419, + 61440, 64476, + 65535, 65535 ] } }, From ec16cc17f8221c378bb7a153c6082543fda2a6b4 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 30 Oct 2025 15:27:13 +0000 Subject: [PATCH 09/18] Update tuning file utils for our own tuning files. --- .../things/camera/picamera.py | 54 +++++++++-------- .../camera/picamera_tuning_file_utils.py | 60 ++++++++++++------- tests/test_cameras.py | 5 +- 3 files changed, 70 insertions(+), 49 deletions(-) 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 From 6763845e386a6689551edfa17f52b2d8d1ec0e6f Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 30 Oct 2025 16:12:50 +0000 Subject: [PATCH 10/18] Let the tuning file utils handle lens shading table manipulation. --- .../things/camera/picamera.py | 143 ++++-------------- .../camera/picamera_tuning_file_utils.py | 120 ++++++++++++--- 2 files changed, 129 insertions(+), 134 deletions(-) diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index ef912e3e..9cee3856 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -15,7 +15,7 @@ https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf """ from __future__ import annotations -from typing import Annotated, Iterator, Literal, Mapping, Optional, overload, Any +from typing import Annotated, Iterator, Literal, Mapping, Optional, Any from types import TracebackType import json import logging @@ -119,22 +119,6 @@ class SensorModeSelector(BaseModel): bit_depth: int -class LensShading(BaseModel): - """A Pydantic model holding the lens shading tables. - - PiCamera needs three numpy arrays for lens shading correction. Each array is - (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 sent by FastAPI - """ - - luminance: list[list[float]] - Cr: list[list[float]] - Cb: list[list[float]] - colour_temp: int - - class StreamingPiCamera2(BaseCamera): """A Thing that provides and interface to the Raspberry Pi Camera. @@ -348,42 +332,6 @@ class StreamingPiCamera2(BaseCamera): tuning = lt.ThingSetting(Optional[dict], None, readonly=True) """The Raspberry PiCamera Tuning File JSON.""" - # Use overload to clarify that only a dictionary is returned if `raise_if_missing` - # is True - @overload - def get_tuning_algo( - self, algorithm_name: str, raise_if_missing: Literal[True] - ) -> dict: ... - # Otherwise may also be None - @overload - def get_tuning_algo( - self, algorithm_name: str, raise_if_missing: bool - ) -> Optional[dict]: ... - - def get_tuning_algo( - self, algorithm_name: str, raise_if_missing: bool = True - ) -> Optional[dict]: - """Return the active tuning algorithm settings for the given algorithm. - - :returns: The algorithm dictionary if found, returns None if no tuning data - is loaded or if the tuning algorithm is not found. - - :raises MissingCalibrationError: If raise_if_missing is true and there is no - tuning file is available, or the requested algorithm is not present. - """ - if self.tuning is None: - if raise_if_missing: - raise MissingCalibrationError("No tuning data is set.") - return None - try: - return tf_utils.find_tuning_algo(self.tuning, algorithm_name) - except StopIteration as e: - if raise_if_missing: - raise MissingCalibrationError( - f"No tuning algorithm with name {algorithm_name}." - ) from e - return None - def _initialise_picamera(self, check_sensor_model: bool = False) -> None: """Acquire the picamera device and store it as ``self._picamera``. @@ -737,6 +685,7 @@ class StreamingPiCamera2(BaseCamera): luminance=L, cr=Cr, cb=Cb, + colour_temp=tf_utils.CALIBRATED_COLOUR_TEMP, ) # Re-initialise the picamera to reload the tuning file. @@ -826,28 +775,6 @@ class StreamingPiCamera2(BaseCamera): time.sleep(0.5) self.set_background(portal) - @lt.thing_action - def flat_lens_shading(self) -> None: - """Disable flat-field correction. - - This method will set a completely flat lens shading table. It is not the - same as the default behaviour, which is to use an adaptive lens shading - table. - - This flat table is used to take an image with no lens shading so that the - correct lens shading table can be calibrated. - """ - 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_lst( - self.tuning, - luminance=flat_array, - cr=flat_array, - cb=flat_array, - ) - self._initialise_picamera() - @lt.thing_property def primary_calibration_actions(self) -> list[ActionButton]: """The calibration actions for both calibration wizard and settings panel.""" @@ -894,6 +821,12 @@ class StreamingPiCamera2(BaseCamera): can_terminate=False, button_primary=False, ), + action_button_for( + self.flat_lens_shading_chrominance, + submit_label="Disable Flat Field Chrominance", + can_terminate=False, + button_primary=False, + ), action_button_for( self.reset_lens_shading, submit_label="Reset Flat Field Correction", @@ -930,39 +863,21 @@ class StreamingPiCamera2(BaseCamera): ] @lt.thing_property - def lens_shading_tables(self) -> Optional[LensShading]: + def lens_shading_tables(self) -> Optional[tf_utils.LensShading]: """The current lens shading (i.e. flat-field correction). Return the current lens shading correction, as three 2D lists each with - dimensions 16x12, if a static lens shading table is in use. + dimensions 16x12. - Return None if: - - adaptive control is enabled - - multiple LSTs in use (for different colour temperatures), + The colour temperature is returned. If the colour temperature us 5000 then this + means the lens shading tables have been calibrated (with our illumination which + has a 5000k colour temperature). Other numbers are set when flatening or + resetting the table. """ - # Note "alsc" is the Picamera2 term for "Automatic Lens Shading Correction" - alsc = self.get_tuning_algo("rpi.alsc") - - # Check there is exactly 1 correction table for red-difference chroma (Cr) - # and blue-difference chroma (Cb) - if len(alsc["calibrations_Cr"]) != 1 or len(alsc["calibrations_Cb"]) != 1: - # If there is not exactly one table, then lens shading isn't static. - return None - - def reshape_lst(lin: list[float]) -> list[list[float]]: - """Reshape the 192 element list into a 2D 16x12 list.""" - w, h = 16, 12 - return [lin[w * i : w * (i + 1)] for i in range(h)] - - return LensShading( - 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"], - ) + return tf_utils.get_lst(self.tuning) @lens_shading_tables.setter - def lens_shading_tables(self, lst: LensShading) -> None: + def lens_shading_tables(self, lst: tf_utils.LensShading) -> None: """Set the lens shading tables.""" with self._streaming_picamera(pause_stream=True): self.tuning = tf_utils.set_lst( @@ -974,6 +889,21 @@ class StreamingPiCamera2(BaseCamera): ) self._initialise_picamera() + @lt.thing_action + def flat_lens_shading(self) -> None: + """Disable flat-field correction. + + This method will set a completely flat lens shading table. It is not the + same as the default behaviour, which is to use an adaptive lens shading + table. + + This flat table is used to take an image with no lens shading so that the + correct lens shading table can be calibrated. + """ + with self._streaming_picamera(pause_stream=True): + self.tuning = tf_utils.flatten_lst(self.tuning) + self._initialise_picamera() + @lt.thing_action def flat_lens_shading_chrominance(self) -> None: """Disable flat-field correction for colour only. @@ -983,16 +913,7 @@ class StreamingPiCamera2(BaseCamera): colour across the image. """ with self._streaming_picamera(pause_stream=True): - alsc = self.get_tuning_algo("rpi.alsc") - luminance = alsc["luminance_lut"] - flat = np.ones((12, 16)) - self.tuning = tf_utils.set_lst( - self.tuning, - luminance=luminance, - cr=flat, - cb=flat, - colour_temp=1234, - ) + self.tuning = tf_utils.flatten_lst(self.tuning, keep_luminance=True) self._initialise_picamera() @lt.thing_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 1c349896..20f2d7a1 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 @@ -3,16 +3,39 @@ The functions that edit the tuning files return a new dictionary that is updated. """ -from typing import Any +from typing import Any, Optional from copy import deepcopy import os import json +from pydantic import BaseModel import numpy as np THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +# The colour temperature to use when setting a value +CALIBRATED_COLOUR_TEMP = 5000 + +# The colour temperature to use for default uncalibrated values +DEFAULT_COLOUR_TEMP = 1234 + + +class LensShading(BaseModel): + """A Pydantic model holding the lens shading tables. + + PiCamera needs three numpy arrays for lens shading correction. Each array is + (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 sent by FastAPI + """ + + luminance: list[list[float]] + Cr: list[list[float]] + Cb: list[list[float]] + colour_temp: int + class TuningFileError(RuntimeError): """Raised if the tuning file cannot be loaded for any reason.""" @@ -62,41 +85,92 @@ def find_tuning_algo(tuning: dict[str, dict], name: str) -> dict[str, Any]: def set_lst( tuning: dict, *, - luminance: np.ndarray, - cr: np.ndarray, - cb: np.ndarray, - colour_temp: int = 5000, + luminance: Optional[np.ndarray], + cr: Optional[np.ndarray], + cb: Optional[np.ndarray], + colour_temp: int, ) -> dict: """Update the ``rpi.alsc`` section of with new lens shading tables. 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. + :param luminance: The table of luminance values, as (12, 16) numpy array. Or None + to leave unchanged. + :param cr: The table of cr values, as (12, 16) numpy array. Or None to leave + unchanged. + :param cb: The table of cb values, as (12, 16) numpy array. Or None to leave + unchanged. + :param colour_temp: The colour temperature to set. On calibration this should be + set to 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["luminance_strength"] = 1.0 - alsc["calibrations_Cr"] = [ - {"ct": colour_temp, "table": _as_flat_rounded_list(cr, round_to=3)} - ] - alsc["calibrations_Cb"] = [ - {"ct": colour_temp, "table": _as_flat_rounded_list(cb, round_to=3)} - ] - alsc["luminance_lut"] = _as_flat_rounded_list(luminance, round_to=3) + + def check_shape(table: np.ndarray) -> None: + """Throw error if the lens shading table is the wrong shape.""" + if np.array(table).shape != (12, 16): + raise ValueError("Lens shading tables must be 12x16!") + + if cr is not None: + check_shape(cr) + alsc["calibrations_Cr"] = [ + {"ct": colour_temp, "table": _as_flat_rounded_list(cr, round_to=3)} + ] + + if cr is not None: + check_shape(cb) + alsc["calibrations_Cb"] = [ + {"ct": colour_temp, "table": _as_flat_rounded_list(cb, round_to=3)} + ] + + if luminance is not None: + check_shape(luminance) + alsc["luminance_lut"] = _as_flat_rounded_list(luminance, round_to=3) + return output_tuning +def flatten_lst(tuning: dict, keep_luminance: bool = False) -> dict: + """Flaten the len shading table ro an array of ones. + + :param tuning: The current tuning dictionary. + :param keep_luminance: Set to True to only flatten the cr and cb tables. + :return: An updated tuning dict. + """ + flat = np.ones((12, 16)) + return set_lst( + tuning, + luminance=None if keep_luminance else flat, + cr=flat, + cb=flat, + colour_temp=DEFAULT_COLOUR_TEMP, + ) + + +def get_lst(tuning: dict) -> LensShading: + """Return the lens shading as a LenSading Base Model.""" + # Note "alsc" is the Picamera2 term for "Automatic Lens Shading Correction" + alsc = find_tuning_algo(tuning, "rpi.alsc") + + def reshape_lst(lin: list[float]) -> list[list[float]]: + """Reshape the 192 element list into a 2D 16x12 list.""" + w, h = 16, 12 + return [lin[w * i : w * (i + 1)] for i in range(h)] + + return LensShading( + 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"], + ) + + def lst_calibrated(tuning: dict) -> bool: """Whether the lens shading table is calibrated. @@ -104,7 +178,7 @@ def lst_calibrated(tuning: dict) -> bool: 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 + return alsc["calibrations_Cr"][0]["ct"] == CALIBRATED_COLOUR_TEMP def set_ccm( @@ -121,7 +195,7 @@ def set_ccm( """ output_tuning = deepcopy(tuning) ccm = find_tuning_algo(output_tuning, "rpi.ccm") - ccm["ccms"] = [{"ct": 5000, "ccm": col_corr_matrix}] + ccm["ccms"] = [{"ct": CALIBRATED_COLOUR_TEMP, "ccm": col_corr_matrix}] return output_tuning From 13d387105937aea2a2de6ea82d88525a458e8f0e Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 30 Oct 2025 19:11:13 +0000 Subject: [PATCH 11/18] Add testing for loading the tuning files in the normal test suite now not tied to picamera --- .../picamera2/test_tuning.py | 22 -- .../things/camera/picamera.py | 2 +- .../camera/picamera_tuning_file_utils.py | 23 +- tests/test_cameras.py | 2 +- tests/test_picamera_tuning_files.py | 258 ++++++++++++++++++ 5 files changed, 276 insertions(+), 31 deletions(-) create mode 100644 tests/test_picamera_tuning_files.py diff --git a/hardware-specific-tests/picamera2/test_tuning.py b/hardware-specific-tests/picamera2/test_tuning.py index d2733b8f..6d58d8e6 100644 --- a/hardware-specific-tests/picamera2/test_tuning.py +++ b/hardware-specific-tests/picamera2/test_tuning.py @@ -1,7 +1,5 @@ """Tests that check that a tuning file can be reloaded.""" -import os - import pytest from picamera2 import Picamera2 @@ -24,24 +22,6 @@ def generate_bad_tuning(): return bad_tuning -def print_tuning(read_file: bool = False): - """Print the path of the default tuning file from the the environment variable. - - :param read_file: Boolean, set true to also print the file contents. - - This is useful for debugging. As pytest suppresses the printing by default the - -s option is needed when running pytest to see this. - """ - key = "LIBCAMERA_RPI_TUNING_FILE" - if key in os.environ: - print(f"Tuning file environment variable: {os.environ[key]}") - if read_file: - with open(os.environ[key], "r") as f: - print(f.read()) - else: - print("Tuning file environment variable not set") - - def _test_bad_tuning_after_good_tuning(configure: bool = False): """Test loading good, bad, then another good tuning files in sequence. @@ -56,10 +36,8 @@ def _test_bad_tuning_after_good_tuning(configure: bool = False): """ bad_tuning = generate_bad_tuning() default_tuning = tf_utils.load_default_tuning("imx219") - print_tuning() print("opening camera with default tuning") with Picamera2(tuning=default_tuning) as cam: - print_tuning() if configure: cam.configure(cam.create_preview_configuration()) del cam diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 9cee3856..95eb06e5 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -706,7 +706,7 @@ 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_ccm(self.tuning)[0]["ccm"]) + return tuple(tf_utils.get_ccm(self.tuning)) @colour_correction_matrix.setter # type: ignore def colour_correction_matrix( 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 20f2d7a1..991296dc 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 @@ -73,11 +73,20 @@ def find_tuning_algo(tuning: dict[str, dict], name: str) -> dict[str, Any]: # Version 1 of the tuning files was simply a dictionary of algorithms. Later # versions have an "algorithms" key, the value of which is a list of algorithms. if version == 1: - return tuning[name] + try: + return tuning[name] + except KeyError as e: + raise KeyError(f"No algorithm {name} in tuning.") from e + # The tuning file "algorithms" is a list of dictionaries + if "algorithms" not in tuning: + raise KeyError("A v2 tuning file must specify an algorithms key") algorithms = tuning["algorithms"] # The list is a list of dictionaries that have 1 key: the algorithm name - algo_dict = next(algo for algo in algorithms if name in algo) + try: + algo_dict = next(algo for algo in algorithms if name in algo) + except StopIteration as e: + raise KeyError(f"No algorithm {name} in tuning.") from e # We want the value for that key, which is a dictionary of algorithm parameters return algo_dict[name] @@ -183,9 +192,7 @@ def lst_calibrated(tuning: dict) -> bool: def set_ccm( tuning: dict, - col_corr_matrix: tuple[ - float, float, float, float, float, float, float, float, float - ], + col_corr_matrix: list, ) -> dict: """Update the ``rpi.alsc`` section of a camera tuning dict set the colour correction matrix. @@ -194,15 +201,17 @@ def set_ccm( :return: an updated tuning dict with the new colour correction matrix. """ output_tuning = deepcopy(tuning) + if len(col_corr_matrix) != 9: + raise ValueError("col_corr_matrix should be a list of 9 floats") ccm = find_tuning_algo(output_tuning, "rpi.ccm") - ccm["ccms"] = [{"ct": CALIBRATED_COLOUR_TEMP, "ccm": col_corr_matrix}] + ccm["ccms"] = [{"ct": CALIBRATED_COLOUR_TEMP, "ccm": list(col_corr_matrix)}] return output_tuning 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"]) + return deepcopy(ccm["ccms"][0]["ccm"]) def set_static_geq( diff --git a/tests/test_cameras.py b/tests/test_cameras.py index 307df610..fc10370b 100644 --- a/tests/test_cameras.py +++ b/tests/test_cameras.py @@ -112,7 +112,6 @@ def test_thing_description_equivalence(mock_picam_thing): # defined as PiCamera specific, or by replicating the functionality for other # cameras. picamera_extra_actions = { - "flat_lens_shading_chrominance", "set_static_green_equalisation", "set_ce_enable_to_off", "stop_streaming", @@ -130,6 +129,7 @@ def test_thing_description_equivalence(mock_picam_thing): "sensor_modes", "sensor_mode", } + # Note these are only the action not exposed as calibration actions. for action in picamera_extra_actions: assert action in picamera_actions for props in picamera_extra_props: diff --git a/tests/test_picamera_tuning_files.py b/tests/test_picamera_tuning_files.py new file mode 100644 index 00000000..cb2ea8a6 --- /dev/null +++ b/tests/test_picamera_tuning_files.py @@ -0,0 +1,258 @@ +"""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", True), + ("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_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 From c0362ee8dd04263ccbb03ebfa2fd2c15b026e046 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 30 Oct 2025 19:26:24 +0000 Subject: [PATCH 12/18] Make picamera tests compatible with updates --- .../picamera2/test_calibration.py | 50 +------------------ 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/hardware-specific-tests/picamera2/test_calibration.py b/hardware-specific-tests/picamera2/test_calibration.py index 622405e7..eb0d127b 100644 --- a/hardware-specific-tests/picamera2/test_calibration.py +++ b/hardware-specific-tests/picamera2/test_calibration.py @@ -3,48 +3,9 @@ from copy import deepcopy import tempfile -import pytest - -import labthings_fastapi as lt - -from openflexure_microscope_server.things.camera.picamera import ( - MissingCalibrationError, -) - from .cam_test_utils import camera_test_client -def test_get_tuning_algo(picamera_thing): - """Test that get_tuning algorithm retrieves tuning algorithms.""" - # Missing algorithm raises an error. - with pytest.raises(MissingCalibrationError): - picamera_thing.get_tuning_algo("foo") - # Or returns None if explicitly told not to error. - assert picamera_thing.get_tuning_algo("foo", raise_if_missing=False) is None - - # Real algorithm is a dict and contains expected keys - assert isinstance(picamera_thing.get_tuning_algo("rpi.geq"), dict) - assert "offset" in picamera_thing.get_tuning_algo("rpi.geq") - - # Set the tuning to None as it is technically optional. And check this - # is handled gracefully. This needs to be done in a try block as LabThings - # tries and fails to emit an event. - try: - picamera_thing.tuning = None - except lt.exceptions.NotConnectedToServerError: - # Labthings will complain that it is not connected to a server - pass - - # Check it is set to None despite the above LabThings issue. - assert picamera_thing.tuning is None - - # Raises an error as there is no tuning file set. - with pytest.raises(MissingCalibrationError): - picamera_thing.get_tuning_algo("rpi.geq") - # Or returns None if explicitly told not to error. - assert picamera_thing.get_tuning_algo("rpi.geq", raise_if_missing=False) is None - - def test_calibration(picamera_thing, picamera_client): """Check that full auto calibrate completes and set the expected values.""" # Check the calibration_required property used by the calibration wizard @@ -53,9 +14,6 @@ def test_calibration(picamera_thing, picamera_client): original_default = deepcopy(picamera_thing.default_tuning) # Tuning should start the same as the server is loading with no settings. assert picamera_thing.default_tuning == picamera_thing.tuning - # lens_shading tables should be None when loaded on default tuning as it is - # adaptive - assert picamera_thing.lens_shading_tables is None # The background detector isn't ready as there is no background image. assert not picamera_thing.background_detector_status.ready @@ -68,13 +26,7 @@ def test_calibration(picamera_thing, picamera_client): assert picamera_thing.default_tuning != picamera_thing.tuning # The default should be unchanged assert picamera_thing.default_tuning == original_default - # Lens shading tables should no longer be None - assert picamera_thing.lens_shading_tables is not None - # There should be exactly one colour correction matrix - # (no variations by colour temp) - assert len(picamera_thing.get_tuning_algo("rpi.ccm")["ccms"]) == 1 - # Green equalisation offset should be set to maximum. - assert picamera_thing.get_tuning_algo("rpi.geq")["offset"] == 65535 + assert picamera_thing.background_detector_status.ready From 982c55905cbc8b86a18c2aef446aa10b46424d1d Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 30 Oct 2025 20:02:51 +0000 Subject: [PATCH 13/18] Update default exposure in picam and defaul LST values in tuning files, and set default colour gains from LST --- .../things/camera/picamera.py | 9 ++-- .../camera/picamera_tuning_file_utils.py | 13 +++++ .../things/camera/tuning_files/vc4/README.md | 5 ++ .../camera/tuning_files/vc4/imx219.json | 48 +++++++++---------- .../camera/tuning_files/vc4/imx477.json | 24 +++++----- tests/test_picamera_tuning_files.py | 8 +++- 6 files changed, 67 insertions(+), 40 deletions(-) diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 95eb06e5..ff7c8867 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -163,6 +163,10 @@ class StreamingPiCamera2(BaseCamera): # a server. But we know this, so we ignore the error. pass + # Also set the colour gains based on the tuning. Set to _colour_gains to not + # trigger a NotConnectedToServerError + self._colour_gains = tf_utils.get_colour_gains_from_lst(self.tuning) + stream_resolution = lt.ThingProperty( tuple[int, int], initial_value=(820, 616), @@ -246,7 +250,7 @@ class StreamingPiCamera2(BaseCamera): with self._streaming_picamera() as cam: cam.set_controls({"ColourGains": value}) - _exposure_time: int = 0 + _exposure_time: int = 500 @lt.thing_setting def exposure_time(self) -> int: @@ -691,8 +695,7 @@ class StreamingPiCamera2(BaseCamera): # Re-initialise the picamera to reload the tuning file. self._initialise_picamera() - # Set colour gains based on the LST results - self.colour_gains = (float(np.min(Cr)), float(np.min(Cb))) + self.colour_gains = tf_utils.get_colour_gains_from_lst(self.tuning) @lt.thing_property def colour_correction_matrix( 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 991296dc..6d80acd0 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 @@ -180,6 +180,19 @@ def get_lst(tuning: dict) -> LensShading: ) +def get_colour_gains_from_lst(tuning: dict) -> tuple[float, float]: + """Get the colour gains that are needed from the lens shading tables. + + The lens shading tables are calculated to create a white balanced image, but the + ISP normalises by the minimum Cr and Cb value. So these need to be set as colour + gains. + """ + alsc = find_tuning_algo(tuning, "rpi.alsc") + min_cr = float(min(alsc["calibrations_Cr"][0]["table"])) + min_cb = float(min(alsc["calibrations_Cb"][0]["table"])) + return (min_cr, min_cb) + + def lst_calibrated(tuning: dict) -> bool: """Whether the lens shading table is calibrated. diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md index dabaafec..8554e440 100644 --- a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md +++ b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md @@ -95,6 +95,11 @@ We also set `n_iter: 0` this stops it adding iterative adaptuion. As there is only one table the colour temperature is ignored. We set the colour temperature to 1234 in the tuning file to make it clear that we have not calibrated it on the microscope. We set the colour temperature to 5000 (our actual illumination colour temperature) once recalibrated. +The tables are flat 1s for luninance on for both sensors. + +For the IMX477 (PiCamera HQ) the Cr and Cb tables are flat, but Cb is 2.0 not 1.0 for all values. This is so that the initial colour gains are set to (1.0, 2.0) + +For the IMX219 (PiCamera v2) the Cr and Cb tables are symmetric tables taken from the top quadrant of a calibrated microscope. They are used to create a good enough correction that the microscope is usable for alignment. ### rpi.contrast - Contrast diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx219.json b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx219.json index d92c2b24..30ddfb24 100644 --- a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx219.json +++ b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx219.json @@ -96,18 +96,18 @@ { "ct": 1234, "table": [ - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 + 1.209, 1.200, 1.197, 1.189, 1.169, 1.150, 1.135, 1.128, 1.128, 1.135, 1.150, 1.169, 1.189, 1.197, 1.200, 1.209, + 1.180, 1.195, 1.216, 1.233, 1.240, 1.238, 1.235, 1.233, 1.233, 1.235, 1.238, 1.240, 1.233, 1.216, 1.195, 1.180, + 1.147, 1.175, 1.217, 1.264, 1.304, 1.324, 1.338, 1.345, 1.345, 1.338, 1.324, 1.304, 1.264, 1.217, 1.175, 1.147, + 1.110, 1.144, 1.207, 1.279, 1.346, 1.391, 1.414, 1.425, 1.425, 1.414, 1.391, 1.346, 1.279, 1.207, 1.144, 1.110, + 1.083, 1.116, 1.184, 1.278, 1.363, 1.426, 1.464, 1.473, 1.473, 1.464, 1.426, 1.363, 1.278, 1.184, 1.116, 1.083, + 1.068, 1.101, 1.170, 1.267, 1.365, 1.441, 1.485, 1.497, 1.497, 1.485, 1.441, 1.365, 1.267, 1.170, 1.101, 1.068, + 1.068, 1.101, 1.170, 1.267, 1.365, 1.441, 1.485, 1.497, 1.497, 1.485, 1.441, 1.365, 1.267, 1.170, 1.101, 1.068, + 1.083, 1.116, 1.184, 1.278, 1.363, 1.426, 1.464, 1.473, 1.473, 1.464, 1.426, 1.363, 1.278, 1.184, 1.116, 1.083, + 1.110, 1.144, 1.207, 1.279, 1.346, 1.391, 1.414, 1.425, 1.425, 1.414, 1.391, 1.346, 1.279, 1.207, 1.144, 1.110, + 1.147, 1.175, 1.217, 1.264, 1.304, 1.324, 1.338, 1.345, 1.345, 1.338, 1.324, 1.304, 1.264, 1.217, 1.175, 1.147, + 1.180, 1.195, 1.216, 1.233, 1.240, 1.238, 1.235, 1.233, 1.233, 1.235, 1.238, 1.240, 1.233, 1.216, 1.195, 1.180, + 1.209, 1.200, 1.197, 1.189, 1.169, 1.150, 1.135, 1.128, 1.128, 1.135, 1.150, 1.169, 1.189, 1.197, 1.200, 1.209 ] } ], @@ -115,18 +115,18 @@ { "ct": 1234, "table": [ - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 + 1.318, 1.320, 1.322, 1.329, 1.330, 1.327, 1.337, 1.343, 1.343, 1.337, 1.327, 1.330, 1.329, 1.322, 1.320, 1.318, + 1.296, 1.311, 1.332, 1.357, 1.374, 1.382, 1.400, 1.421, 1.421, 1.400, 1.382, 1.374, 1.357, 1.332, 1.311, 1.296, + 1.279, 1.296, 1.334, 1.376, 1.410, 1.435, 1.462, 1.490, 1.490, 1.462, 1.435, 1.410, 1.376, 1.334, 1.296, 1.279, + 1.268, 1.284, 1.325, 1.387, 1.439, 1.471, 1.508, 1.541, 1.541, 1.508, 1.471, 1.439, 1.387, 1.325, 1.284, 1.268, + 1.259, 1.286, 1.324, 1.392, 1.457, 1.506, 1.550, 1.586, 1.586, 1.550, 1.506, 1.457, 1.392, 1.324, 1.286, 1.259, + 1.262, 1.291, 1.337, 1.398, 1.461, 1.522, 1.576, 1.617, 1.617, 1.576, 1.522, 1.461, 1.398, 1.337, 1.291, 1.262, + 1.262, 1.291, 1.337, 1.398, 1.461, 1.522, 1.576, 1.617, 1.617, 1.576, 1.522, 1.461, 1.398, 1.337, 1.291, 1.262, + 1.259, 1.286, 1.324, 1.392, 1.457, 1.506, 1.550, 1.586, 1.586, 1.550, 1.506, 1.457, 1.392, 1.324, 1.286, 1.259, + 1.268, 1.284, 1.325, 1.387, 1.439, 1.471, 1.508, 1.541, 1.541, 1.508, 1.471, 1.439, 1.387, 1.325, 1.284, 1.268, + 1.279, 1.296, 1.334, 1.376, 1.410, 1.435, 1.462, 1.490, 1.490, 1.462, 1.435, 1.410, 1.376, 1.334, 1.296, 1.279, + 1.296, 1.311, 1.332, 1.357, 1.374, 1.382, 1.400, 1.421, 1.421, 1.400, 1.382, 1.374, 1.357, 1.332, 1.311, 1.296, + 1.318, 1.320, 1.322, 1.329, 1.330, 1.327, 1.337, 1.343, 1.343, 1.337, 1.327, 1.330, 1.329, 1.322, 1.320, 1.318 ] } ], diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx477.json b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx477.json index e518cf07..fc85b715 100644 --- a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx477.json +++ b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/imx477.json @@ -115,18 +115,18 @@ { "ct": 1234, "table": [ - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 + 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0 ] } ], diff --git a/tests/test_picamera_tuning_files.py b/tests/test_picamera_tuning_files.py index cb2ea8a6..c48b2380 100644 --- a/tests/test_picamera_tuning_files.py +++ b/tests/test_picamera_tuning_files.py @@ -44,7 +44,7 @@ def test_load_tuning(imx219_tuning, imx477_tuning): ("rpi.sdn", True), ("rpi.awb", True), ("rpi.agc", True), - ("rpi.alsc", True), + ("rpi.alsc", False), ("rpi.contrast", True), ("rpi.ccm", False), ("rpi.sharpen", True), @@ -124,6 +124,12 @@ def test_tuning_lst_tools(imx219_tuning, imx477_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)) From f432247446d4ae705d9830610e7017b33ca31fec Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 30 Oct 2025 21:06:17 +0000 Subject: [PATCH 14/18] Fix typos during refactor caught by CI --- .../things/camera/picamera_tuning_file_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 6d80acd0..495f55b5 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 @@ -132,7 +132,7 @@ def set_lst( {"ct": colour_temp, "table": _as_flat_rounded_list(cr, round_to=3)} ] - if cr is not None: + if cb is not None: check_shape(cb) alsc["calibrations_Cb"] = [ {"ct": colour_temp, "table": _as_flat_rounded_list(cb, round_to=3)} @@ -240,7 +240,7 @@ def set_static_geq( 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 + :return: An updated tuning dictionary """ output_tuning = deepcopy(tuning) geq = find_tuning_algo(output_tuning, "rpi.geq") @@ -284,9 +284,9 @@ def copy_algo_from_other_tuning( :param algo: The algorithm to copy. Eg ``rpi.alsc`` for lens shading correction. :param base_tuning_file: The tuning file to copy. - :param copy_alsc_from: The tuning file to take the alsc section from. - :return: A deep copy of base_tuning_file with the alsc section copied in from the - other tuning file. + :param copy_from: The tuning file to copy the algorithm from section from. + :return: A deep copy of base_tuning_file with the specified algorithm copied in + from the other tuning file. """ output_tuning = deepcopy(base_tuning_file) # Find the relevant sub-dict for each tuning file From 5c56c1e85114a2b8750054c5ee71405fc8e00eb1 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Fri, 7 Nov 2025 11:32:32 +0000 Subject: [PATCH 15/18] Apply suggestions from code review of branch temperature-ccm Co-authored-by: Richard Bowman --- .../things/camera/picamera_tuning_file_utils.py | 8 ++++++-- .../things/camera/tuning_files/vc4/README.md | 6 +++--- 2 files changed, 9 insertions(+), 5 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 495f55b5..71960441 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 @@ -101,7 +101,11 @@ def set_lst( ) -> dict: """Update the ``rpi.alsc`` section of with new lens shading tables. - Only one set of tables is set so no adaptive lens shading will be used. + Only one set of tables is set so that the camera pipeline does not adaptively + switch between lens shading tables based on its estimation of colour temperature. + Also ``n_iter`` is set to 0 so that the pipeline doesn't perform an iterative + adaption of our table to try to "correct" if interprets the image as having + different types of lighting. :param tuning: The current tuning file. :param luminance: The table of luminance values, as (12, 16) numpy array. Or None @@ -207,7 +211,7 @@ def set_ccm( tuning: dict, col_corr_matrix: list, ) -> dict: - """Update the ``rpi.alsc`` section of a camera tuning dict set the colour correction matrix. + """Update the ``rpi.ccm`` section of a camera tuning dict set the colour correction matrix. :param tuning: The current tuning dict :param col_corr_matrix: The colour correction matrix to set diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md index 8554e440..9f02aeec 100644 --- a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md +++ b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md @@ -6,7 +6,7 @@ More data is available in this datasheet: https://datasheets.raspberrypi.com/cam ## version: -This needs to be 2.0, version one was a normal dictionary of algorithms, tather than this current structure with the algorithms as a list. +This needs to be 2.0, version one was a normal dictionary of algorithms, rather than this current structure with the algorithms as a list. ## target @@ -43,13 +43,13 @@ Values from the default tuning files for imx219 and imx477 are: "reference_slope": 3.67 ``` -These can be optimised using the methods in the camera tuning tool shipped with libcamera. Though most of the camera tuning tool methods make assumptions that are invalid for a microscope. As such any tuning would further consideration. +These can be optimised using the methods in the camera tuning tool shipped with libcamera. Though most of the camera tuning tool methods make assumptions that are invalid for a microscope. As such any tuning would need further consideration. ### rpi.geq - Green Equalisation #### The problems with green equalisation -This tried to calculate whether adjacent grren pixels should be equal. This interacts very porly with the chief ray angle compesation when using the camera. The imx219 has a lenslet array: +This tried to calculate whether adjacent grren pixels should be equal. This interacts very porly with the chief ray angle compensation when using the camera. The imx219 has a lenslet array: ![](../../../../../../docs/images/ChiefRayAngle.png) From d0d30222b4d8463de29a07dd226a40d8ff0e9cf1 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Fri, 7 Nov 2025 12:10:30 +0000 Subject: [PATCH 16/18] Update sensor mode picamera test to ensure that camera tests don't use saved settings --- .../picamera2/test_sensor_mode.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/hardware-specific-tests/picamera2/test_sensor_mode.py b/hardware-specific-tests/picamera2/test_sensor_mode.py index af875d9c..87c16d7c 100644 --- a/hardware-specific-tests/picamera2/test_sensor_mode.py +++ b/hardware-specific-tests/picamera2/test_sensor_mode.py @@ -2,25 +2,16 @@ import logging -from fastapi.testclient import TestClient import numpy as np -from labthings_fastapi.server import ThingServer -from labthings_fastapi.client import ThingClient - -from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2 +from .cam_test_utils import camera_test_client logging.basicConfig(level=logging.DEBUG) def test_sensor_mode(): """Test capturing raw arrays in two different sensor modes.""" - cam = StreamingPiCamera2() - server = ThingServer() - server.add_thing(cam, "/camera/") - - with TestClient(server.app) as test_client: - client = ThingClient.from_url("/camera/", client=test_client) + with camera_test_client() as client: for size in [(3280, 2464), (1640, 1232)]: client.sensor_mode = {"output_size": size, "bit_depth": 10} arr = np.array(client.capture_array(stream_name="raw")) From 8a4e6390c7afefdcf5058437935de70ef51652d6 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Tue, 11 Nov 2025 11:16:51 +0000 Subject: [PATCH 17/18] Apply suggestions from code review of branch temperature-ccm Co-authored-by: Joe Knapper --- .../things/camera/picamera_tuning_file_utils.py | 6 +++--- .../things/camera/tuning_files/vc4/README.md | 14 +++++++------- 2 files changed, 10 insertions(+), 10 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 71960441..2a327289 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 @@ -110,9 +110,9 @@ def set_lst( :param tuning: The current tuning file. :param luminance: The table of luminance values, as (12, 16) numpy array. Or None to leave unchanged. - :param cr: The table of cr values, as (12, 16) numpy array. Or None to leave + :param cr: The table of Cr values, as (12, 16) numpy array. Or None to leave unchanged. - :param cb: The table of cb values, as (12, 16) numpy array. Or None to leave + :param cb: The table of Cb values, as (12, 16) numpy array. Or None to leave unchanged. :param colour_temp: The colour temperature to set. On calibration this should be set to 5000. Set a different value for the PiCamera Thing to report that the @@ -237,7 +237,7 @@ def set_static_geq( ) -> dict: """Update the ``rpi.geq`` section of a camera tuning dict. - :param tuning: the raspberry pi tuning dictionary + :param tuning: the Raspberry Pi camera 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 diff --git a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md index 9f02aeec..7881ab22 100644 --- a/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md +++ b/src/openflexure_microscope_server/things/camera/tuning_files/vc4/README.md @@ -91,15 +91,15 @@ For calculating the shutter time of the sensor if AeEnable is on. Only the requi Auto lens shading correction is calibrated by our microscope. This contains the tables for luninance, and colour shading. It can contain multiple tables depending on estimated colour temperature (`ct`). As the microscope has fixed lighting we only provide one set of tables as we don't want the correction to be adaptive. -We also set `n_iter: 0` this stops it adding iterative adaptuion. +We also set `n_iter: 0`. This stops it adding iterative adaptuion. -As there is only one table the colour temperature is ignored. We set the colour temperature to 1234 in the tuning file to make it clear that we have not calibrated it on the microscope. We set the colour temperature to 5000 (our actual illumination colour temperature) once recalibrated. +As there is only one table, the colour temperature is ignored. We set the colour temperature to 1234 in the tuning file to make it clear that we have not calibrated it on the microscope. We set the colour temperature to 5000 (our actual illumination colour temperature) once recalibrated. -The tables are flat 1s for luninance on for both sensors. +The tables are flat 1s for luminance on for both sensors. -For the IMX477 (PiCamera HQ) the Cr and Cb tables are flat, but Cb is 2.0 not 1.0 for all values. This is so that the initial colour gains are set to (1.0, 2.0) +For the IMX477 (PiCamera HQ), the Cr and Cb tables are flat, but Cb is 2.0 not 1.0 for all values. This is so that the initial colour gains are set to (1.0, 2.0) -For the IMX219 (PiCamera v2) the Cr and Cb tables are symmetric tables taken from the top quadrant of a calibrated microscope. They are used to create a good enough correction that the microscope is usable for alignment. +For the IMX219 (PiCamera v2), the Cr and Cb tables are symmetric tables taken from the top quadrant of a calibrated microscope. They are used to create a good enough correction that the microscope is initially usable for alignment. ### rpi.contrast - Contrast @@ -113,7 +113,7 @@ We may want to adjust the gamma curve in future. It is possible tp ship different CCMs to be used based on the estimated colour temperatire. We only ship one CCM as our illumination colour temperature is fixed to 5000K, the colour temperature of the LED board. However, the diffuser might affect this. -The CCMs are different for PiCamera HQ and PiCamera version 2. The default uning files for these cameras (`/usr/share/libcamera/ipa/rpi/vc4/imx219.json` or `/usr/share/libcamera/ipa/rpi/vc4/imx477.json`) specify CCMs for a number of temperatures but not for 5000k. As such we linearly interpolate between temperatures below 5k with the following code: +The CCMs are different for PiCamera HQ and PiCamera version 2. The default tuning files for these cameras (`/usr/share/libcamera/ipa/rpi/vc4/imx219.json` or `/usr/share/libcamera/ipa/rpi/vc4/imx477.json`) specify CCMs for a number of temperatures but not for 5000K. As such we linearly interpolate between temperatures around 5000K with the following code: ``` import numpy as np @@ -168,7 +168,7 @@ print(np.round(ccm_interp, 6)) The CCM calculation is done using a MacBeth target. Lib camera provides a method for this using the Colour Tuning Tool. We will need to adapt this algorithm as they image the target at once and pick out the areas. We have fixed lighting but do not have colour uniformity. -In the future with a colour calibration slide we can take images of each colour with fixed camera settings to create a CCM using the same calculations as the Camera Tuning Tool. +In the future, with a colour calibration slide, we can take images of each colour with fixed camera settings to create a CCM using the same calculations as the Camera Tuning Tool. ### rpi.sharpen - Sharpening From 7354f093197a6120cbaaae9daed2447242b55fa4 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Tue, 11 Nov 2025 11:29:49 +0000 Subject: [PATCH 18/18] Update picamera coverage info --- 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 169800f57debd57a45ed7796ade51ef433dc2e82..293a8ee634115b9352b3704494af0ee628677d85 100644 GIT binary patch delta 451 zcmbQXjCtBJW}yIYW)=|!5D<&bj&6MZs&%7~N`p`)1OHF{i~J|~Q~93rt>&xY%iQcJ z5Xd)qR-drGE;|b&XAH-o*(%I7!j}{oIIo;6Y)T z1~XaK$!Gg)>T6h8895s(*q9jpz2BZtdPb+=^yG)<5AZPkC}y~x(~*8TMd z?=Sy9_nXw3MqX})+V`dHO?kWw`&sWXG6Wp>|L-;TIX?T`!|NFt82)W!WXQ^3Fvwv@ zuxCCbZ_2=vaDXANfcZf_LjxnoH*5?H1`Iq6jCUA#_LU`Vp4Pv_fdk~j=rzaGCp%rZ zsF9Xto@`-ZW}cF2U|?inmXc^}VQ89~nwn^uW@&7mXl|KgXqak{m^Asq1)Is`7pK)5 zB^n!<8(CPIrKOrD8JMOSnVF@SB_>%UCMTsOCnZ{%8YCK-8m1X4=_n}aCFhr=7A2;q z>J?-rC+4OWB`R4dC?zJF8yHw7Tc%kW8CjYq8zz}28C#gAm?j#fn5QIKrX`str6d`r cB`ehicr!AIFr&l^W9#JJOBM(}oW0};0PgOW!2kdN delta 450 zcmbQXjCtBJW}yIYW)=|!5IB<=8$F|W`qqs?Dh)ze4E#U&FY=${PvyJGHdz7`CO9$NN0@;^7JXUH+ke(->kZ-K8n)_ z;XjkZ{p!057eq53$ZsrXU=aDR|NpP1&4>MOf5>KFXsFL&XpoX(;E`syalXMIli|P% zHZ}$ZlLqNK3_r{p)-f`$F)%PBFfi~kFwf}t#&dunp@27oee=}*r4Ae*4>rFzG-0yS zg^L=7$z~>INvWo0$p#k5=EjyrmgYv5NtUK5Mi!Rl#)e78$*Jav7HNr-FI=#hTz+v{ zeX5y7lDV0Mp{21ws%cu1NwSf#nYoFvMXIG`Qlh1qQIesdnYoc^ijt0kl3sFtS!z*Y zda7PQW^!U~YEhz+m4cFqMT(I{N}^e!WnyBoQL1H{S+a3jl7WS#d1_j6vXOz2sgaqb dQL2ejZGbl;lL#|Pv@mX++