249 lines
8.8 KiB
Python
249 lines
8.8 KiB
Python
# SPDX-License-Identifier: BSD-2-Clause
|
|
#
|
|
# Copyright (C) 2019, Raspberry Pi Ltd
|
|
# Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
|
|
#
|
|
# ALSC module for tuning Raspberry Pi
|
|
|
|
from .lsc import LSC
|
|
|
|
import libtuning as lt
|
|
import libtuning.utils as utils
|
|
|
|
from numbers import Number
|
|
import numpy as np
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ALSCRaspberryPi(LSC):
|
|
# Override the type name so that the parser can match the entry in the
|
|
# config file.
|
|
type = 'alsc'
|
|
hr_name = 'ALSC (Raspberry Pi)'
|
|
out_name = 'rpi.alsc'
|
|
compatible = ['raspberrypi']
|
|
|
|
def __init__(self, *,
|
|
do_color: lt.Param,
|
|
luminance_strength: lt.Param,
|
|
**kwargs):
|
|
super().__init__(**kwargs)
|
|
|
|
self.do_color = do_color
|
|
self.luminance_strength = luminance_strength
|
|
|
|
self.output_range = (0, 3.999)
|
|
|
|
def validate_config(self, config: dict) -> bool:
|
|
if self not in config:
|
|
logger.error(f'{self.type} not in config')
|
|
return False
|
|
|
|
valid = True
|
|
|
|
conf = config[self]
|
|
|
|
lum_key = self.luminance_strength.name
|
|
color_key = self.do_color.name
|
|
|
|
if lum_key not in conf and self.luminance_strength.required:
|
|
logger.error(f'{lum_key} is not in config')
|
|
valid = False
|
|
|
|
if lum_key in conf and (conf[lum_key] < 0 or conf[lum_key] > 1):
|
|
logger.warning(f'{lum_key} is not in range [0, 1]; defaulting to 0.5')
|
|
|
|
if color_key not in conf and self.do_color.required:
|
|
logger.error(f'{color_key} is not in config')
|
|
valid = False
|
|
|
|
return valid
|
|
|
|
# @return Image color temperature, flattened array of red calibration table
|
|
# (containing {sector size} elements), flattened array of blue
|
|
# calibration table, flattened array of green calibration
|
|
# table
|
|
|
|
def _do_single_alsc(self, image: lt.Image, do_alsc_colour: bool):
|
|
average_green = np.mean((image.channels[lt.Color.GR:lt.Color.GB + 1]), axis=0)
|
|
|
|
cg, g = self._lsc_single_channel(average_green, image)
|
|
|
|
if not do_alsc_colour:
|
|
return image.color, None, None, cg.flatten()
|
|
|
|
cr, _ = self._lsc_single_channel(image.channels[lt.Color.R], image, g)
|
|
cb, _ = self._lsc_single_channel(image.channels[lt.Color.B], image, g)
|
|
|
|
# \todo implement debug
|
|
|
|
return image.color, cr.flatten(), cb.flatten(), cg.flatten()
|
|
|
|
# @return Red shading table, Blue shading table, Green shading table,
|
|
# number of images processed
|
|
|
|
def _do_all_alsc(self, images: list, do_alsc_colour: bool, general_conf: dict) -> (list, list, list, Number, int):
|
|
# List of colour temperatures
|
|
list_col = []
|
|
# Associated calibration tables
|
|
list_cr = []
|
|
list_cb = []
|
|
list_cg = []
|
|
count = 0
|
|
for image in self._enumerate_lsc_images(images):
|
|
col, cr, cb, cg = self._do_single_alsc(image, do_alsc_colour)
|
|
list_col.append(col)
|
|
list_cr.append(cr)
|
|
list_cb.append(cb)
|
|
list_cg.append(cg)
|
|
count += 1
|
|
|
|
# Convert to numpy array for data manipulation
|
|
list_col = np.array(list_col)
|
|
list_cr = np.array(list_cr)
|
|
list_cb = np.array(list_cb)
|
|
list_cg = np.array(list_cg)
|
|
|
|
cal_cr_list = []
|
|
cal_cb_list = []
|
|
|
|
# Note: Calculation of average corners and center of the shading tables
|
|
# has been removed (which ctt had, as it was unused)
|
|
|
|
# Average all values for luminance shading and return one table for all temperatures
|
|
lum_lut = list(np.round(np.mean(list_cg, axis=0), 3))
|
|
|
|
if not do_alsc_colour:
|
|
return None, None, lum_lut, count
|
|
|
|
for ct in sorted(set(list_col)):
|
|
# Average tables for the same colour temperature
|
|
indices = np.where(list_col == ct)
|
|
ct = int(ct)
|
|
t_r = np.round(np.mean(list_cr[indices], axis=0), 3)
|
|
t_b = np.round(np.mean(list_cb[indices], axis=0), 3)
|
|
|
|
cr_dict = {
|
|
'ct': ct,
|
|
'table': list(t_r)
|
|
}
|
|
cb_dict = {
|
|
'ct': ct,
|
|
'table': list(t_b)
|
|
}
|
|
cal_cr_list.append(cr_dict)
|
|
cal_cb_list.append(cb_dict)
|
|
|
|
return cal_cr_list, cal_cb_list, lum_lut, count
|
|
|
|
# @brief Calculate sigma from two adjacent gain tables
|
|
def _calcSigma(self, g1, g2):
|
|
g1 = np.reshape(g1, self.sector_shape[::-1])
|
|
g2 = np.reshape(g2, self.sector_shape[::-1])
|
|
|
|
# Apply gains to gain table
|
|
gg = g1 / g2
|
|
if np.mean(gg) < 1:
|
|
gg = 1 / gg
|
|
|
|
# For each internal patch, compute average difference between it and
|
|
# its 4 neighbours, then append to list
|
|
diffs = []
|
|
for i in range(self.sector_shape[1] - 2):
|
|
for j in range(self.sector_shape[0] - 2):
|
|
# Indexing is incremented by 1 since all patches on borders are
|
|
# not counted
|
|
diff = np.abs(gg[i + 1][j + 1] - gg[i][j + 1])
|
|
diff += np.abs(gg[i + 1][j + 1] - gg[i + 2][j + 1])
|
|
diff += np.abs(gg[i + 1][j + 1] - gg[i + 1][j])
|
|
diff += np.abs(gg[i + 1][j + 1] - gg[i + 1][j + 2])
|
|
diffs.append(diff / 4)
|
|
|
|
mean_diff = np.mean(diffs)
|
|
return np.round(mean_diff, 5)
|
|
|
|
# @brief Obtains sigmas for red and blue, effectively a measure of the
|
|
# 'error'
|
|
def _get_sigma(self, cal_cr_list, cal_cb_list):
|
|
# Provided colour alsc tables were generated for two different colour
|
|
# temperatures sigma is calculated by comparing two calibration temperatures
|
|
# adjacent in colour space
|
|
|
|
color_temps = [cal['ct'] for cal in cal_cr_list]
|
|
|
|
# Calculate sigmas for each adjacent color_temps and return worst one
|
|
sigma_rs = []
|
|
sigma_bs = []
|
|
for i in range(len(color_temps) - 1):
|
|
sigma_rs.append(self._calcSigma(cal_cr_list[i]['table'], cal_cr_list[i + 1]['table']))
|
|
sigma_bs.append(self._calcSigma(cal_cb_list[i]['table'], cal_cb_list[i + 1]['table']))
|
|
|
|
# Return maximum sigmas, not necessarily from the same colour
|
|
# temperature interval
|
|
sigma_r = max(sigma_rs) if sigma_rs else 0.005
|
|
sigma_b = max(sigma_bs) if sigma_bs else 0.005
|
|
|
|
return sigma_r, sigma_b
|
|
|
|
def process(self, config: dict, images: list, outputs: dict) -> dict:
|
|
output = {
|
|
'omega': 1.3,
|
|
'n_iter': 100,
|
|
'luminance_strength': 0.7
|
|
}
|
|
|
|
conf = config[self]
|
|
general_conf = config['general']
|
|
|
|
do_alsc_colour = self.do_color.get_value(conf)
|
|
|
|
# \todo I have no idea where this input parameter is used
|
|
luminance_strength = self.luminance_strength.get_value(conf)
|
|
if luminance_strength < 0 or luminance_strength > 1:
|
|
luminance_strength = 0.5
|
|
|
|
output['luminance_strength'] = luminance_strength
|
|
|
|
# \todo Validate images from greyscale camera and force grescale mode
|
|
# \todo Debug functionality
|
|
|
|
alsc_out = self._do_all_alsc(images, do_alsc_colour, general_conf)
|
|
# \todo Handle the second green lut
|
|
cal_cr_list, cal_cb_list, luminance_lut, count = alsc_out
|
|
|
|
if not do_alsc_colour:
|
|
output['luminance_lut'] = luminance_lut
|
|
output['n_iter'] = 0
|
|
return output
|
|
|
|
output['calibrations_Cr'] = cal_cr_list
|
|
output['calibrations_Cb'] = cal_cb_list
|
|
output['luminance_lut'] = luminance_lut
|
|
|
|
# The sigmas determine the strength of the adaptive algorithm, that
|
|
# cleans up any lens shading that has slipped through the alsc. These
|
|
# are determined by measuring a 'worst-case' difference between two
|
|
# alsc tables that are adjacent in colour space. If, however, only one
|
|
# colour temperature has been provided, then this difference can not be
|
|
# computed as only one table is available.
|
|
# To determine the sigmas you would have to estimate the error of an
|
|
# alsc table with only the image it was taken on as a check. To avoid
|
|
# circularity, dfault exaggerated sigmas are used, which can result in
|
|
# too much alsc and is therefore not advised.
|
|
# In general, just take another alsc picture at another colour
|
|
# temperature!
|
|
|
|
if count == 1:
|
|
output['sigma'] = 0.005
|
|
output['sigma_Cb'] = 0.005
|
|
logger.warning('Only one alsc calibration found; standard sigmas used for adaptive algorithm.')
|
|
return output
|
|
|
|
# Obtain worst-case scenario residual sigmas
|
|
sigma_r, sigma_b = self._get_sigma(cal_cr_list, cal_cb_list)
|
|
output['sigma'] = np.round(sigma_r, 5)
|
|
output['sigma_Cb'] = np.round(sigma_b, 5)
|
|
|
|
return output
|