Skip to content

Commit 280e4ac

Browse files
committed
utils: libtuning: modules: alsc: Add raspberrypi ALSC module
Add an ALSC module for Raspberry Pi. Signed-off-by: Paul Elder <[email protected]> Reviewed-by: Laurent Pinchart <[email protected]>
1 parent 288cfb9 commit 280e4ac

File tree

2 files changed

+247
-0
lines changed

2 files changed

+247
-0
lines changed

utils/tuning/libtuning/modules/lsc/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
# Copyright (C) 2022, Paul Elder <[email protected]>
44

55
from libtuning.modules.lsc.lsc import LSC
6+
from libtuning.modules.lsc.raspberrypi import ALSCRaspberryPi
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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

Comments
 (0)