summaryrefslogtreecommitdiff
path: root/utils/tuning
diff options
context:
space:
mode:
Diffstat (limited to 'utils/tuning')
-rw-r--r--utils/tuning/config-example.yaml44
-rw-r--r--utils/tuning/libtuning/ctt_awb.py58
-rw-r--r--utils/tuning/libtuning/image.py2
-rw-r--r--utils/tuning/libtuning/modules/awb/__init__.py6
-rw-r--r--utils/tuning/libtuning/modules/awb/awb.py40
-rw-r--r--utils/tuning/libtuning/modules/awb/rkisp1.py36
-rw-r--r--utils/tuning/libtuning/modules/lux/__init__.py6
-rw-r--r--utils/tuning/libtuning/modules/lux/lux.py70
-rw-r--r--utils/tuning/libtuning/modules/lux/rkisp1.py22
-rwxr-xr-xutils/tuning/rkisp1.py18
10 files changed, 265 insertions, 37 deletions
diff --git a/utils/tuning/config-example.yaml b/utils/tuning/config-example.yaml
index 1b7f52cd..5593eaef 100644
--- a/utils/tuning/config-example.yaml
+++ b/utils/tuning/config-example.yaml
@@ -5,7 +5,49 @@ general:
do_alsc_colour: 1
luminance_strength: 0.5
awb:
- greyworld: 0
+ # Algorithm can either be 'grey' or 'bayes'
+ algorithm: bayes
+ # Priors is only used for the bayes algorithm. They are defined in linear
+ # space. A good staring point is:
+ # - lux: 0
+ # ct: [ 2000, 3000, 13000 ]
+ # probability: [ 1.005, 1.0, 1.0 ]
+ # - lux: 800
+ # ct: [ 2000, 6000, 13000 ]
+ # probability: [ 1.0, 1.01, 1.01 ]
+ # - lux: 1500
+ # ct: [ 2000, 4000, 6000, 6500, 7000, 13000 ]
+ # probability: [ 1.0, 1.005, 1.032, 1.037, 1.01, 1.01 ]
+ priors:
+ - lux: 0
+ ct: [ 2000, 13000 ]
+ probability: [ 1.0, 1.0 ]
+ AwbMode:
+ AwbAuto:
+ lo: 2500
+ hi: 8000
+ AwbIncandescent:
+ lo: 2500
+ hi: 3000
+ AwbTungsten:
+ lo: 3000
+ hi: 3500
+ AwbFluorescent:
+ lo: 4000
+ hi: 4700
+ AwbIndoor:
+ lo: 3000
+ hi: 5000
+ AwbDaylight:
+ lo: 5500
+ hi: 6500
+ AwbCloudy:
+ lo: 6500
+ hi: 8000
+ # One custom mode can be defined if needed
+ #AwbCustom:
+ # lo: 2000
+ # hi: 1300
macbeth:
small: 1
show: 0
diff --git a/utils/tuning/libtuning/ctt_awb.py b/utils/tuning/libtuning/ctt_awb.py
index abf22321..240f37e6 100644
--- a/utils/tuning/libtuning/ctt_awb.py
+++ b/utils/tuning/libtuning/ctt_awb.py
@@ -4,6 +4,8 @@
#
# camera tuning tool for AWB
+import logging
+
import matplotlib.pyplot as plt
from bisect import bisect_left
from scipy.optimize import fmin
@@ -11,12 +13,12 @@ import numpy as np
from .image import Image
+logger = logging.getLogger(__name__)
"""
obtain piecewise linear approximation for colour curve
"""
-def awb(Cam, cal_cr_list, cal_cb_list, plot):
- imgs = Cam.imgs
+def awb(imgs, cal_cr_list, cal_cb_list, plot):
"""
condense alsc calibration tables into one dictionary
"""
@@ -39,7 +41,7 @@ def awb(Cam, cal_cr_list, cal_cb_list, plot):
rb_raw = []
rbs_hat = []
for Img in imgs:
- Cam.log += '\nProcessing '+Img.name
+ logger.info(f'Processing {Img.name}')
"""
get greyscale patches with alsc applied if alsc enabled.
Note: if alsc is disabled then colour_cals will be set to None and the
@@ -51,7 +53,7 @@ def awb(Cam, cal_cr_list, cal_cb_list, plot):
"""
r_g = np.mean(r_patchs/g_patchs)
b_g = np.mean(b_patchs/g_patchs)
- Cam.log += '\n r : {:.4f} b : {:.4f}'.format(r_g, b_g)
+ logger.info(f' r : {r_g:.4f} b : {b_g:.4f}')
"""
The curve tends to be better behaved in so-called hatspace.
R, B, G represent the individual channels. The colour curve is plotted in
@@ -74,12 +76,11 @@ def awb(Cam, cal_cr_list, cal_cb_list, plot):
"""
r_g_hat = r_g/(1+r_g+b_g)
b_g_hat = b_g/(1+r_g+b_g)
- Cam.log += '\n r_hat : {:.4f} b_hat : {:.4f}'.format(r_g_hat, b_g_hat)
- rbs_hat.append((r_g_hat, b_g_hat, Img.col))
+ logger.info(f' r_hat : {r_g_hat:.4f} b_hat : {b_g_hat:.4f}')
+ rbs_hat.append((r_g_hat, b_g_hat, Img.color))
rb_raw.append((r_g, b_g))
- Cam.log += '\n'
- Cam.log += '\nFinished processing images'
+ logger.info('Finished processing images')
"""
sort all lits simultaneously by r_hat
"""
@@ -95,7 +96,7 @@ def awb(Cam, cal_cr_list, cal_cb_list, plot):
fit quadratic fit to r_g hat and b_g_hat
"""
a, b, c = np.polyfit(rbs_hat[0], rbs_hat[1], 2)
- Cam.log += '\nFit quadratic curve in hatspace'
+ logger.info('Fit quadratic curve in hatspace')
"""
the algorithm now approximates the shortest distance from each point to the
curve in dehatspace. Since the fit is done in hatspace, it is easier to
@@ -151,14 +152,14 @@ def awb(Cam, cal_cr_list, cal_cb_list, plot):
if (x+y) > (rr+bb):
dist *= -1
dists.append(dist)
- Cam.log += '\nFound closest point on fit line to each point in dehatspace'
+ logger.info('Found closest point on fit line to each point in dehatspace')
"""
calculate wiggle factors in awb. 10% added since this is an upper bound
"""
transverse_neg = - np.min(dists) * 1.1
transverse_pos = np.max(dists) * 1.1
- Cam.log += '\nTransverse pos : {:.5f}'.format(transverse_pos)
- Cam.log += '\nTransverse neg : {:.5f}'.format(transverse_neg)
+ logger.info(f'Transverse pos : {transverse_pos:.5f}')
+ logger.info(f'Transverse neg : {transverse_neg:.5f}')
"""
set minimum transverse wiggles to 0.1 .
Wiggle factors dictate how far off of the curve the algorithm searches. 0.1
@@ -167,10 +168,10 @@ def awb(Cam, cal_cr_list, cal_cb_list, plot):
"""
if transverse_pos < 0.01:
transverse_pos = 0.01
- Cam.log += '\nForced transverse pos to 0.01'
+ logger.info('Forced transverse pos to 0.01')
if transverse_neg < 0.01:
transverse_neg = 0.01
- Cam.log += '\nForced transverse neg to 0.01'
+ logger.info('Forced transverse neg to 0.01')
"""
generate new b_hat values at each r_hat according to fit
@@ -202,25 +203,25 @@ def awb(Cam, cal_cr_list, cal_cb_list, plot):
i = len(c_fit) - 1
while i > 0:
if c_fit[i] > c_fit[i-1]:
- Cam.log += '\nColour temperature increase found\n'
- Cam.log += '{} K at r = {} to '.format(c_fit[i-1], r_fit[i-1])
- Cam.log += '{} K at r = {}'.format(c_fit[i], r_fit[i])
+ logger.info('Colour temperature increase found')
+ logger.info(f'{c_fit[i - 1]} K at r = {r_fit[i - 1]} to ')
+ logger.info(f'{c_fit[i]} K at r = {r_fit[i]}')
"""
if colour temperature increases then discard point furthest from
the transformed fit (dehatspace)
"""
error_1 = abs(dists[i-1])
error_2 = abs(dists[i])
- Cam.log += '\nDistances from fit:\n'
- Cam.log += '{} K : {:.5f} , '.format(c_fit[i], error_1)
- Cam.log += '{} K : {:.5f}'.format(c_fit[i-1], error_2)
+ logger.info('Distances from fit:')
+ logger.info(f'{c_fit[i]} K : {error_1:.5f}')
+ logger.info(f'{c_fit[i - 1]} K : {error_2:.5f}')
"""
find bad index
note that in python false = 0 and true = 1
"""
bad = i - (error_1 < error_2)
- Cam.log += '\nPoint at {} K deleted as '.format(c_fit[bad])
- Cam.log += 'it is furthest from fit'
+ logger.info(f'Point at {c_fit[bad]} K deleted as ')
+ logger.info('it is furthest from fit')
"""
delete bad point
"""
@@ -239,12 +240,12 @@ def awb(Cam, cal_cr_list, cal_cb_list, plot):
return formatted ct curve, ordered by increasing colour temperature
"""
ct_curve = list(np.array(list(zip(b_fit, r_fit, c_fit))).flatten())[::-1]
- Cam.log += '\nFinal CT curve:'
+ logger.info('Final CT curve:')
for i in range(len(ct_curve)//3):
j = 3*i
- Cam.log += '\n ct: {} '.format(ct_curve[j])
- Cam.log += ' r: {} '.format(ct_curve[j+1])
- Cam.log += ' b: {} '.format(ct_curve[j+2])
+ logger.info(f' ct: {ct_curve[j]} ')
+ logger.info(f' r: {ct_curve[j + 1]} ')
+ logger.info(f' b: {ct_curve[j + 2]} ')
"""
plotting code for debug
@@ -301,10 +302,10 @@ def get_alsc_patches(Img, colour_cals, grey=True):
patches for each channel, remembering to subtract blacklevel
If grey then only greyscale patches considered
"""
+ patches = Img.patches
if grey:
cen_coords = Img.cen_coords[3::4]
- col = Img.col
- patches = [np.array(Img.patches[i]) for i in Img.order]
+ col = Img.color
r_patchs = patches[0][3::4] - Img.blacklevel_16
b_patchs = patches[3][3::4] - Img.blacklevel_16
"""
@@ -314,7 +315,6 @@ def get_alsc_patches(Img, colour_cals, grey=True):
else:
cen_coords = Img.cen_coords
col = Img.color
- patches = [np.array(Img.patches[i]) for i in Img.order]
r_patchs = patches[0] - Img.blacklevel_16
b_patchs = patches[3] - Img.blacklevel_16
g_patchs = (patches[1]+patches[2])/2 - Img.blacklevel_16
diff --git a/utils/tuning/libtuning/image.py b/utils/tuning/libtuning/image.py
index c8911a0f..ecd334bd 100644
--- a/utils/tuning/libtuning/image.py
+++ b/utils/tuning/libtuning/image.py
@@ -135,6 +135,6 @@ class Image:
all_patches.append(ch_patches)
- self.patches = all_patches
+ self.patches = np.array(all_patches)
return not saturated
diff --git a/utils/tuning/libtuning/modules/awb/__init__.py b/utils/tuning/libtuning/modules/awb/__init__.py
new file mode 100644
index 00000000..2d67f10c
--- /dev/null
+++ b/utils/tuning/libtuning/modules/awb/__init__.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2024, Ideas On Board
+
+from libtuning.modules.awb.awb import AWB
+from libtuning.modules.awb.rkisp1 import AWBRkISP1
diff --git a/utils/tuning/libtuning/modules/awb/awb.py b/utils/tuning/libtuning/modules/awb/awb.py
new file mode 100644
index 00000000..0dc4f59d
--- /dev/null
+++ b/utils/tuning/libtuning/modules/awb/awb.py
@@ -0,0 +1,40 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2024, Ideas On Board
+
+import logging
+
+from ..module import Module
+
+from libtuning.ctt_awb import awb
+import numpy as np
+
+logger = logging.getLogger(__name__)
+
+
+class AWB(Module):
+ type = 'awb'
+ hr_name = 'AWB (Base)'
+ out_name = 'GenericAWB'
+
+ def __init__(self, *, debug: list):
+ super().__init__()
+
+ self.debug = debug
+
+ def do_calculation(self, images):
+ logger.info('Starting AWB calculation')
+
+ imgs = [img for img in images if img.macbeth is not None]
+
+ ct_curve, transverse_pos, transverse_neg = awb(imgs, None, None, False)
+ ct_curve = np.reshape(ct_curve, (-1, 3))
+ gains = [{
+ 'ct': int(v[0]),
+ 'gains': [float(1.0 / v[1]), float(1.0 / v[2])]
+ } for v in ct_curve]
+
+ return {'colourGains': gains,
+ 'transversePos': transverse_pos,
+ 'transverseNeg': transverse_neg}
+
diff --git a/utils/tuning/libtuning/modules/awb/rkisp1.py b/utils/tuning/libtuning/modules/awb/rkisp1.py
new file mode 100644
index 00000000..d562d26e
--- /dev/null
+++ b/utils/tuning/libtuning/modules/awb/rkisp1.py
@@ -0,0 +1,36 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2024, Ideas On Board
+#
+# AWB module for tuning rkisp1
+
+from .awb import AWB
+
+class AWBRkISP1(AWB):
+ hr_name = 'AWB (RkISP1)'
+ out_name = 'Awb'
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+
+ def validate_config(self, config: dict) -> bool:
+ return True
+
+ def process(self, config: dict, images: list, outputs: dict) -> dict:
+ if not 'awb' in config['general']:
+ raise ValueError('AWB configuration missing')
+ awb_config = config['general']['awb']
+ algorithm = awb_config['algorithm']
+
+ output = {'algorithm': algorithm}
+ data = self.do_calculation(images)
+ if algorithm == 'grey':
+ output['colourGains'] = data['colourGains']
+ elif algorithm == 'bayes':
+ output['AwbMode'] = awb_config['AwbMode']
+ output['priors'] = awb_config['priors']
+ output.update(data)
+ else:
+ raise ValueError(f"Unknown AWB algorithm {output['algorithm']}")
+
+ return output
diff --git a/utils/tuning/libtuning/modules/lux/__init__.py b/utils/tuning/libtuning/modules/lux/__init__.py
new file mode 100644
index 00000000..af9d4e08
--- /dev/null
+++ b/utils/tuning/libtuning/modules/lux/__init__.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2025, Ideas on Board
+
+from libtuning.modules.lux.lux import Lux
+from libtuning.modules.lux.rkisp1 import LuxRkISP1
diff --git a/utils/tuning/libtuning/modules/lux/lux.py b/utils/tuning/libtuning/modules/lux/lux.py
new file mode 100644
index 00000000..4bad429a
--- /dev/null
+++ b/utils/tuning/libtuning/modules/lux/lux.py
@@ -0,0 +1,70 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+# Copyright (C) 2025, Ideas on Board
+#
+# Base Lux tuning module
+
+from ..module import Module
+
+import logging
+import numpy as np
+
+logger = logging.getLogger(__name__)
+
+
+class Lux(Module):
+ type = 'lux'
+ hr_name = 'Lux (Base)'
+ out_name = 'GenericLux'
+
+ def __init__(self, debug: list):
+ super().__init__()
+
+ self.debug = debug
+
+ def calculate_lux_reference_values(self, images):
+ # The lux calibration is done on a single image. For best effects, the
+ # image with lux level closest to 1000 is chosen.
+ imgs = [img for img in images if img.macbeth is not None]
+ lux_values = [img.lux for img in imgs]
+ index = lux_values.index(min(lux_values, key=lambda l: abs(1000 - l)))
+ img = imgs[index]
+ logger.info(f'Selected image {img.name} for lux calibration')
+
+ if img.lux < 50:
+ logger.warning(f'A Lux level of {img.lux} is very low for proper lux calibration')
+
+ ref_y = self.calculate_y(img)
+ exposure_time = img.exposure
+ gain = img.againQ8_norm
+ aperture = 1
+ logger.info(f'RefY:{ref_y} Exposure time:{exposure_time}µs Gain:{gain} Aperture:{aperture}')
+ return {'referenceY': ref_y,
+ 'referenceExposureTime': exposure_time,
+ 'referenceAnalogueGain': gain,
+ 'referenceDigitalGain': 1.0,
+ 'referenceLux': img.lux}
+
+ def calculate_y(self, img):
+ max16Bit = 0xffff
+ # Average over all grey patches.
+ ap_r = np.mean(img.patches[0][3::4]) / max16Bit
+ ap_g = (np.mean(img.patches[1][3::4]) + np.mean(img.patches[2][3::4])) / 2 / max16Bit
+ ap_b = np.mean(img.patches[3][3::4]) / max16Bit
+ logger.debug(f'Averaged grey patches: Red: {ap_r}, Green: {ap_g}, Blue: {ap_b}')
+
+ # Calculate white balance gains.
+ gr = ap_g / ap_r
+ gb = ap_g / ap_b
+ logger.debug(f'WB gains: Red: {gr} Blue: {gb}')
+
+ # Calculate the mean Y value of the whole image
+ a_r = np.mean(img.channels[0]) * gr
+ a_g = (np.mean(img.channels[1]) + np.mean(img.channels[2])) / 2
+ a_b = np.mean(img.channels[3]) * gb
+ y = 0.299 * a_r + 0.587 * a_g + 0.114 * a_b
+ y /= max16Bit
+
+ return y
+
diff --git a/utils/tuning/libtuning/modules/lux/rkisp1.py b/utils/tuning/libtuning/modules/lux/rkisp1.py
new file mode 100644
index 00000000..62d3f94c
--- /dev/null
+++ b/utils/tuning/libtuning/modules/lux/rkisp1.py
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2024, Ideas on Board
+#
+# Lux module for tuning rkisp1
+
+from .lux import Lux
+
+
+class LuxRkISP1(Lux):
+ hr_name = 'Lux (RkISP1)'
+ out_name = 'Lux'
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+
+ # We don't need anything from the config file.
+ def validate_config(self, config: dict) -> bool:
+ return True
+
+ def process(self, config: dict, images: list, outputs: dict) -> dict:
+ return self.calculate_lux_reference_values(images)
diff --git a/utils/tuning/rkisp1.py b/utils/tuning/rkisp1.py
index f5c42a61..207b717a 100755
--- a/utils/tuning/rkisp1.py
+++ b/utils/tuning/rkisp1.py
@@ -2,25 +2,28 @@
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
+# Copyright (C) 2024, Ideas On Board
#
# Tuning script for rkisp1
-import coloredlogs
import logging
import sys
+import coloredlogs
import libtuning as lt
-from libtuning.parsers import YamlParser
from libtuning.generators import YamlOutput
-from libtuning.modules.lsc import LSCRkISP1
from libtuning.modules.agc import AGCRkISP1
+from libtuning.modules.awb import AWBRkISP1
from libtuning.modules.ccm import CCMRkISP1
+from libtuning.modules.lsc import LSCRkISP1
+from libtuning.modules.lux import LuxRkISP1
from libtuning.modules.static import StaticModule
+from libtuning.parsers import YamlParser
coloredlogs.install(level=logging.INFO, fmt='%(name)s %(levelname)s %(message)s')
agc = AGCRkISP1(debug=[lt.Debug.Plot])
-awb = StaticModule('Awb')
+awb = AWBRkISP1(debug=[lt.Debug.Plot])
blc = StaticModule('BlackLevelCorrection')
ccm = CCMRkISP1(debug=[lt.Debug.Plot])
color_processing = StaticModule('ColorProcessing')
@@ -43,12 +46,15 @@ lsc = LSCRkISP1(debug=[lt.Debug.Plot],
# This is the function that will be used to smooth the color ratio
# values. This can also be a custom function.
smoothing_function=lt.smoothing.MedianBlur(3),)
+lux = LuxRkISP1(debug=[lt.Debug.Plot])
tuner = lt.Tuner('RkISP1')
-tuner.add([agc, awb, blc, ccm, color_processing, filter, gamma_out, lsc])
+tuner.add([agc, awb, blc, ccm, color_processing, filter, gamma_out, lsc, lux])
tuner.set_input_parser(YamlParser())
tuner.set_output_formatter(YamlOutput())
-tuner.set_output_order([agc, awb, blc, ccm, color_processing,
+
+# Bayesian AWB uses the lux value, so insert the lux algorithm before AWB.
+tuner.set_output_order([agc, lux, awb, blc, ccm, color_processing,
filter, gamma_out, lsc])
if __name__ == '__main__':