[v2,12/17] libtuning: Add module for lux calibration
diff mbox series

Message ID 20250123114204.79321-13-stefan.klug@ideasonboard.com
State New
Headers show
Series
  • Add Bayesian AWB algorithm to libipa and rkisp1
Related show

Commit Message

Stefan Klug Jan. 23, 2025, 11:41 a.m. UTC
For the lux algorithm, reference values get calculated based on a tuning
image taken at a known lux level. The reference data contains the mean Y
of the image, lux level, exposure time, gain and aperture. This module
calculates these values for insertion into the tuning file.

Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>

---

Changes in v2:
- Added this patch
---
 .../tuning/libtuning/modules/lux/__init__.py  |  6 ++
 utils/tuning/libtuning/modules/lux/lux.py     | 70 +++++++++++++++++++
 utils/tuning/libtuning/modules/lux/rkisp1.py  | 22 ++++++
 3 files changed, 98 insertions(+)
 create mode 100644 utils/tuning/libtuning/modules/lux/__init__.py
 create mode 100644 utils/tuning/libtuning/modules/lux/lux.py
 create mode 100644 utils/tuning/libtuning/modules/lux/rkisp1.py

Comments

Paul Elder Jan. 27, 2025, 11:31 a.m. UTC | #1
On Thu, Jan 23, 2025 at 12:41:02PM +0100, Stefan Klug wrote:
> For the lux algorithm, reference values get calculated based on a tuning
> image taken at a known lux level. The reference data contains the mean Y
> of the image, lux level, exposure time, gain and aperture. This module
> calculates these values for insertion into the tuning file.
> 
> Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>
> 
> ---
> 
> Changes in v2:
> - Added this patch
> ---
>  .../tuning/libtuning/modules/lux/__init__.py  |  6 ++
>  utils/tuning/libtuning/modules/lux/lux.py     | 70 +++++++++++++++++++
>  utils/tuning/libtuning/modules/lux/rkisp1.py  | 22 ++++++
>  3 files changed, 98 insertions(+)
>  create mode 100644 utils/tuning/libtuning/modules/lux/__init__.py
>  create mode 100644 utils/tuning/libtuning/modules/lux/lux.py
>  create mode 100644 utils/tuning/libtuning/modules/lux/rkisp1.py
> 
> diff --git a/utils/tuning/libtuning/modules/lux/__init__.py b/utils/tuning/libtuning/modules/lux/__init__.py
> new file mode 100644
> index 000000000000..af9d4e08e64f
> --- /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 000000000000..5cf7d3baeb5d
> --- /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)'

s/LUX/Lux/

The rest looks sound to me.

Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>

> +    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 000000000000..62d3f94cabd8
> --- /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)
> -- 
> 2.43.0
>

Patch
diff mbox series

diff --git a/utils/tuning/libtuning/modules/lux/__init__.py b/utils/tuning/libtuning/modules/lux/__init__.py
new file mode 100644
index 000000000000..af9d4e08e64f
--- /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 000000000000..5cf7d3baeb5d
--- /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 000000000000..62d3f94cabd8
--- /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)