|
| 1 | +# SPDX-License-Identifier: BSD-2-Clause |
| 2 | +# |
| 3 | +# Copyright (C) 2019, Raspberry Pi Ltd |
| 4 | +# Copyright (C) 2022, Paul Elder <[email protected]> |
| 5 | +# |
| 6 | +# raspberrypi.py - ALSC module for tuning Raspberry Pi |
| 7 | + |
| 8 | +from .lsc import LSC |
| 9 | + |
| 10 | +import libtuning as lt |
| 11 | +import libtuning.utils as utils |
| 12 | + |
| 13 | +from numbers import Number |
| 14 | +import numpy as np |
| 15 | + |
| 16 | + |
| 17 | +class ALSCRaspberryPi(LSC): |
| 18 | + # Override the type name so that the parser can match the entry in the |
| 19 | + # config file. |
| 20 | + type = 'alsc' |
| 21 | + hr_name = 'ALSC (Raspberry Pi)' |
| 22 | + out_name = 'rpi.alsc' |
| 23 | + compatible = ['raspberrypi'] |
| 24 | + |
| 25 | + def __init__(self, *, |
| 26 | + do_color: lt.Param, |
| 27 | + luminance_strength: lt.Param, |
| 28 | + **kwargs): |
| 29 | + super().__init__(**kwargs) |
| 30 | + |
| 31 | + self.do_color = do_color |
| 32 | + self.luminance_strength = luminance_strength |
| 33 | + |
| 34 | + self.output_range = (0, 3.999) |
| 35 | + |
| 36 | + def validate_config(self, config: dict) -> bool: |
| 37 | + if self not in config: |
| 38 | + utils.eprint(f'{self.type} not in config') |
| 39 | + return False |
| 40 | + |
| 41 | + valid = True |
| 42 | + |
| 43 | + conf = config[self] |
| 44 | + |
| 45 | + lum_key = self.luminance_strength.name |
| 46 | + color_key = self.do_color.name |
| 47 | + |
| 48 | + if lum_key not in conf and self.luminance_strength.required: |
| 49 | + utils.eprint(f'{lum_key} is not in config') |
| 50 | + valid = False |
| 51 | + |
| 52 | + if lum_key in conf and (conf[lum_key] < 0 or conf[lum_key] > 1): |
| 53 | + utils.eprint(f'Warning: {lum_key} is not in range [0, 1]; defaulting to 0.5') |
| 54 | + |
| 55 | + if color_key not in conf and self.do_color.required: |
| 56 | + utils.eprint(f'{color_key} is not in config') |
| 57 | + valid = False |
| 58 | + |
| 59 | + return valid |
| 60 | + |
| 61 | + # @return Image color temperature, flattened array of red calibration table |
| 62 | + # (containing {sector size} elements), flattened array of blue |
| 63 | + # calibration table, flattened array of green calibration |
| 64 | + # table |
| 65 | + |
| 66 | + def _do_single_alsc(self, image: lt.Image, do_alsc_colour: bool): |
| 67 | + average_green = np.mean((image.channels[lt.Color.GR:lt.Color.GB + 1]), axis=0) |
| 68 | + |
| 69 | + cg, g = self._lsc_single_channel(average_green, image) |
| 70 | + |
| 71 | + if not do_alsc_colour: |
| 72 | + return image.color, None, None, cg.flatten() |
| 73 | + |
| 74 | + cr, _ = self._lsc_single_channel(image.channels[lt.Color.R], image, g) |
| 75 | + cb, _ = self._lsc_single_channel(image.channels[lt.Color.B], image, g) |
| 76 | + |
| 77 | + # \todo implement debug |
| 78 | + |
| 79 | + return image.color, cr.flatten(), cb.flatten(), cg.flatten() |
| 80 | + |
| 81 | + # @return Red shading table, Blue shading table, Green shading table, |
| 82 | + # number of images processed |
| 83 | + |
| 84 | + def _do_all_alsc(self, images: list, do_alsc_colour: bool, general_conf: dict) -> (list, list, list, Number, int): |
| 85 | + # List of colour temperatures |
| 86 | + list_col = [] |
| 87 | + # Associated calibration tables |
| 88 | + list_cr = [] |
| 89 | + list_cb = [] |
| 90 | + list_cg = [] |
| 91 | + count = 0 |
| 92 | + for image in self._enumerate_lsc_images(images): |
| 93 | + col, cr, cb, cg = self._do_single_alsc(image, do_alsc_colour) |
| 94 | + list_col.append(col) |
| 95 | + list_cr.append(cr) |
| 96 | + list_cb.append(cb) |
| 97 | + list_cg.append(cg) |
| 98 | + count += 1 |
| 99 | + |
| 100 | + # Convert to numpy array for data manipulation |
| 101 | + list_col = np.array(list_col) |
| 102 | + list_cr = np.array(list_cr) |
| 103 | + list_cb = np.array(list_cb) |
| 104 | + list_cg = np.array(list_cg) |
| 105 | + |
| 106 | + cal_cr_list = [] |
| 107 | + cal_cb_list = [] |
| 108 | + |
| 109 | + # Note: Calculation of average corners and center of the shading tables |
| 110 | + # has been removed (which ctt had, as it was unused) |
| 111 | + |
| 112 | + # Average all values for luminance shading and return one table for all temperatures |
| 113 | + lum_lut = list(np.round(np.mean(list_cg, axis=0), 3)) |
| 114 | + |
| 115 | + if not do_alsc_colour: |
| 116 | + return None, None, lum_lut, count |
| 117 | + |
| 118 | + for ct in sorted(set(list_col)): |
| 119 | + # Average tables for the same colour temperature |
| 120 | + indices = np.where(list_col == ct) |
| 121 | + ct = int(ct) |
| 122 | + t_r = np.round(np.mean(list_cr[indices], axis=0), 3) |
| 123 | + t_b = np.round(np.mean(list_cb[indices], axis=0), 3) |
| 124 | + |
| 125 | + cr_dict = { |
| 126 | + 'ct': ct, |
| 127 | + 'table': list(t_r) |
| 128 | + } |
| 129 | + cb_dict = { |
| 130 | + 'ct': ct, |
| 131 | + 'table': list(t_b) |
| 132 | + } |
| 133 | + cal_cr_list.append(cr_dict) |
| 134 | + cal_cb_list.append(cb_dict) |
| 135 | + |
| 136 | + return cal_cr_list, cal_cb_list, lum_lut, count |
| 137 | + |
| 138 | + # @brief Calculate sigma from two adjacent gain tables |
| 139 | + def _calcSigma(self, g1, g2): |
| 140 | + g1 = np.reshape(g1, self.sector_shape[::-1]) |
| 141 | + g2 = np.reshape(g2, self.sector_shape[::-1]) |
| 142 | + |
| 143 | + # Apply gains to gain table |
| 144 | + gg = g1 / g2 |
| 145 | + if np.mean(gg) < 1: |
| 146 | + gg = 1 / gg |
| 147 | + |
| 148 | + # For each internal patch, compute average difference between it and |
| 149 | + # its 4 neighbours, then append to list |
| 150 | + diffs = [] |
| 151 | + for i in range(self.sector_shape[1] - 2): |
| 152 | + for j in range(self.sector_shape[0] - 2): |
| 153 | + # Indexing is incremented by 1 since all patches on borders are |
| 154 | + # not counted |
| 155 | + diff = np.abs(gg[i + 1][j + 1] - gg[i][j + 1]) |
| 156 | + diff += np.abs(gg[i + 1][j + 1] - gg[i + 2][j + 1]) |
| 157 | + diff += np.abs(gg[i + 1][j + 1] - gg[i + 1][j]) |
| 158 | + diff += np.abs(gg[i + 1][j + 1] - gg[i + 1][j + 2]) |
| 159 | + diffs.append(diff / 4) |
| 160 | + |
| 161 | + mean_diff = np.mean(diffs) |
| 162 | + return np.round(mean_diff, 5) |
| 163 | + |
| 164 | + # @brief Obtains sigmas for red and blue, effectively a measure of the |
| 165 | + # 'error' |
| 166 | + def _get_sigma(self, cal_cr_list, cal_cb_list): |
| 167 | + # Provided colour alsc tables were generated for two different colour |
| 168 | + # temperatures sigma is calculated by comparing two calibration temperatures |
| 169 | + # adjacent in colour space |
| 170 | + |
| 171 | + color_temps = [cal['ct'] for cal in cal_cr_list] |
| 172 | + |
| 173 | + # Calculate sigmas for each adjacent color_temps and return worst one |
| 174 | + sigma_rs = [] |
| 175 | + sigma_bs = [] |
| 176 | + for i in range(len(color_temps) - 1): |
| 177 | + sigma_rs.append(self._calcSigma(cal_cr_list[i]['table'], cal_cr_list[i + 1]['table'])) |
| 178 | + sigma_bs.append(self._calcSigma(cal_cb_list[i]['table'], cal_cb_list[i + 1]['table'])) |
| 179 | + |
| 180 | + # Return maximum sigmas, not necessarily from the same colour |
| 181 | + # temperature interval |
| 182 | + sigma_r = max(sigma_rs) if sigma_rs else 0.005 |
| 183 | + sigma_b = max(sigma_bs) if sigma_bs else 0.005 |
| 184 | + |
| 185 | + return sigma_r, sigma_b |
| 186 | + |
| 187 | + def process(self, config: dict, images: list, outputs: dict) -> dict: |
| 188 | + output = { |
| 189 | + 'omega': 1.3, |
| 190 | + 'n_iter': 100, |
| 191 | + 'luminance_strength': 0.7 |
| 192 | + } |
| 193 | + |
| 194 | + conf = config[self] |
| 195 | + general_conf = config['general'] |
| 196 | + |
| 197 | + do_alsc_colour = self.do_color.get_value(conf) |
| 198 | + |
| 199 | + # \todo I have no idea where this input parameter is used |
| 200 | + luminance_strength = self.luminance_strength.get_value(conf) |
| 201 | + if luminance_strength < 0 or luminance_strength > 1: |
| 202 | + luminance_strength = 0.5 |
| 203 | + |
| 204 | + output['luminance_strength'] = luminance_strength |
| 205 | + |
| 206 | + # \todo Validate images from greyscale camera and force grescale mode |
| 207 | + # \todo Debug functionality |
| 208 | + |
| 209 | + alsc_out = self._do_all_alsc(images, do_alsc_colour, general_conf) |
| 210 | + # \todo Handle the second green lut |
| 211 | + cal_cr_list, cal_cb_list, luminance_lut, count = alsc_out |
| 212 | + |
| 213 | + if not do_alsc_colour: |
| 214 | + output['luminance_lut'] = luminance_lut |
| 215 | + output['n_iter'] = 0 |
| 216 | + return output |
| 217 | + |
| 218 | + output['calibrations_Cr'] = cal_cr_list |
| 219 | + output['calibrations_Cb'] = cal_cb_list |
| 220 | + output['luminance_lut'] = luminance_lut |
| 221 | + |
| 222 | + # The sigmas determine the strength of the adaptive algorithm, that |
| 223 | + # cleans up any lens shading that has slipped through the alsc. These |
| 224 | + # are determined by measuring a 'worst-case' difference between two |
| 225 | + # alsc tables that are adjacent in colour space. If, however, only one |
| 226 | + # colour temperature has been provided, then this difference can not be |
| 227 | + # computed as only one table is available. |
| 228 | + # To determine the sigmas you would have to estimate the error of an |
| 229 | + # alsc table with only the image it was taken on as a check. To avoid |
| 230 | + # circularity, dfault exaggerated sigmas are used, which can result in |
| 231 | + # too much alsc and is therefore not advised. |
| 232 | + # In general, just take another alsc picture at another colour |
| 233 | + # temperature! |
| 234 | + |
| 235 | + if count == 1: |
| 236 | + output['sigma'] = 0.005 |
| 237 | + output['sigma_Cb'] = 0.005 |
| 238 | + utils.eprint('Warning: Only one alsc calibration found; standard sigmas used for adaptive algorithm.') |
| 239 | + return output |
| 240 | + |
| 241 | + # Obtain worst-case scenario residual sigmas |
| 242 | + sigma_r, sigma_b = self._get_sigma(cal_cr_list, cal_cb_list) |
| 243 | + output['sigma'] = np.round(sigma_r, 5) |
| 244 | + output['sigma_Cb'] = np.round(sigma_b, 5) |
| 245 | + |
| 246 | + return output |
0 commit comments