[{"id":30364,"web_url":"https://patchwork.libcamera.org/comment/30364/","msgid":"<327187c8-b259-4bd8-9cdc-a2511f692a83@ideasonboard.com>","date":"2024-07-05T15:59:53","subject":"Re: [PATCH v4 07/23] libtuning: Migrate prints to python logging\n\tframework","submitter":{"id":156,"url":"https://patchwork.libcamera.org/api/people/156/","name":"Dan Scally","email":"dan.scally@ideasonboard.com"},"content":"Hi Stefan, thanks for the patch\n\nOn 05/07/2024 15:41, Stefan Klug wrote:\n> In ctt_ccm.py the logging functionality of the Cam object was used. As\n> we don't want to port over that class, it needs to be replaced anyways.\n> While at it, also replace the eprint function as it doesn't add any\n> value over the logging framework and misses the ability for easy log\n> formatting.\n>\n> For nice output formatting add the coloredlogs library.\n>\n> Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>\n> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>\n> ---\n\n\nLooks ok to me; only one comment below but either way: Reviewed-by: Daniel Scally \n<dan.scally@ideasonboard.com>\n\n>   utils/tuning/libtuning/ctt_ccm.py             | 27 ++++++++++---------\n>   .../libtuning/generators/yaml_output.py       |  5 ++--\n>   utils/tuning/libtuning/image.py               |  7 +++--\n>   utils/tuning/libtuning/libtuning.py           | 21 ++++++++-------\n>   utils/tuning/libtuning/macbeth.py             | 13 +++++----\n>   .../libtuning/modules/lsc/raspberrypi.py      | 12 +++++----\n>   utils/tuning/libtuning/utils.py               | 17 ++++++------\n>   utils/tuning/requirements.txt                 |  1 +\n>   utils/tuning/rkisp1.py                        |  5 ++++\n>   9 files changed, 62 insertions(+), 46 deletions(-)\n>\n> diff --git a/utils/tuning/libtuning/ctt_ccm.py b/utils/tuning/libtuning/ctt_ccm.py\n> index f37adaf45538..c4362756c3c0 100644\n> --- a/utils/tuning/libtuning/ctt_ccm.py\n> +++ b/utils/tuning/libtuning/ctt_ccm.py\n> @@ -4,6 +4,8 @@\n>   #\n>   # camera tuning tool for CCM (colour correction matrix)\n>   \n> +import logging\n> +\n>   import numpy as np\n>   from scipy.optimize import minimize\n>   \n> @@ -12,6 +14,8 @@ from .image import Image\n>   from .ctt_awb import get_alsc_patches\n>   from .utils import visualise_macbeth_chart\n>   \n> +logger = logging.getLogger(__name__)\n> +\n>   \"\"\"\n>   takes 8-bit macbeth chart values, degammas and returns 16 bit\n>   \"\"\"\n> @@ -129,7 +133,7 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n>       \"\"\"\n>       ccm_tab = {}\n>       for Img in imgs:\n> -        Cam.log += '\\nProcessing image: ' + Img.name\n> +        logger.info('Processing image: ' + Img.name)\n>           \"\"\"\n>           get macbeth patches with alsc applied if alsc enabled.\n>           Note: if alsc is disabled then colour_cals will be set to None and no\n> @@ -154,7 +158,7 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n>           each channel for each patch\n>           \"\"\"\n>           gain = np.mean(m_srgb) / np.mean((r, g, b))\n> -        Cam.log += '\\nGain with respect to standard colours: {:.3f}'.format(gain)\n> +        logger.info(f'Gain with respect to standard colours: {gain:.3f}')\n>           r = np.mean(gain * r, axis=1)\n>           b = np.mean(gain * b, axis=1)\n>           g = np.mean(gain * g, axis=1)\n> @@ -192,15 +196,13 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n>           zero since the input data is imperfect\n>           '''\n>   \n> -        Cam.log += (\"\\n \\n Optimised Matrix Below: \\n \\n\")\n>           [r1, r2, g1, g2, b1, b2] = result.x\n>           # The new, optimised color correction matrix values\n> +        # This is the optimised Color Matrix (preserving greys by summing rows up to 1)\n>           optimised_ccm = [r1, r2, (1 - r1 - r2), g1, g2, (1 - g1 - g2), b1, b2, (1 - b1 - b2)]\n>   \n> -        # This is the optimised Color Matrix (preserving greys by summing rows up to 1)\n> -        Cam.log += str(optimised_ccm)\n> -        Cam.log += \"\\n Old Color Correction Matrix Below \\n\"\n> -        Cam.log += str(ccm)\n> +        logger.info(f'Optimized Matrix: {np.round(optimised_ccm, 4)}')\n> +        logger.info(f'Old Matrix:       {np.round(ccm, 4)}')\n>   \n>           formatted_ccm = np.array(original_ccm).reshape((3, 3))\n>   \n> @@ -229,7 +231,7 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n>           We now want to spit out some data that shows\n>           how the optimisation has improved the color matrices\n>           '''\n> -        Cam.log += \"Here are the Improvements\"\n> +        logger.info(\"Here are the Improvements\")\n>   \n>           # CALCULATE WORST CASE delta e\n>           old_worst_delta_e = 0\n> @@ -244,8 +246,8 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n>               if new_delta_e > new_worst_delta_e:\n>                   new_worst_delta_e = new_delta_e\n>   \n> -        Cam.log += \"Before color correction matrix was optimised, we got an average delta E of \" + str(before_average) + \" and a maximum delta E of \" + str(old_worst_delta_e)\n> -        Cam.log += \"After color correction matrix was optimised, we got an average delta E of \" + str(after_average) + \" and a maximum delta E of \" + str(new_worst_delta_e)\n> +        logger.info(f'delta E optimized: average: {after_average:.2f}  max:{new_worst_delta_e:.2f}')\n> +        logger.info(f'delta E old:       average: {before_average:.2f}  max:{old_worst_delta_e:.2f}')\n>   \n>           visualise_macbeth_chart(m_rgb, optimised_ccm_rgb, after_gamma_rgb, str(Img.col) + str(matrix_selection_types[typenum]))\n>           '''\n> @@ -262,9 +264,8 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n>               ccm_tab[Img.col].append(optimised_ccm)\n>           else:\n>               ccm_tab[Img.col] = [optimised_ccm]\n> -        Cam.log += '\\n'\n>   \n> -    Cam.log += '\\nFinished processing images'\n> +    logger.info('Finished processing images')\n>       \"\"\"\n>       average any ccms that share a colour temperature\n>       \"\"\"\n> @@ -273,7 +274,7 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n>           tab = np.where((10000 * tab) % 1 <= 0.05, tab + 0.00001, tab)\n>           tab = np.where((10000 * tab) % 1 >= 0.95, tab - 0.00001, tab)\n>           ccm_tab[k] = list(np.round(tab, 5))\n> -        Cam.log += '\\nMatrix calculated for colour temperature of {} K'.format(k)\n> +        logger.info(f'Matrix calculated for colour temperature of {k} K')\n>   \n>       \"\"\"\n>       return all ccms with respective colour temperature in the correct format,\n> diff --git a/utils/tuning/libtuning/generators/yaml_output.py b/utils/tuning/libtuning/generators/yaml_output.py\n> index 8f22d386f1b3..31e265df4ea7 100644\n> --- a/utils/tuning/libtuning/generators/yaml_output.py\n> +++ b/utils/tuning/libtuning/generators/yaml_output.py\n> @@ -9,8 +9,9 @@ from .generator import Generator\n>   from numbers import Number\n>   from pathlib import Path\n>   \n> -import libtuning.utils as utils\n> +import logging\n>   \n> +logger = logging.getLogger(__name__)\n>   \n>   class YamlOutput(Generator):\n>       def __init__(self):\n> @@ -112,7 +113,7 @@ class YamlOutput(Generator):\n>                   continue\n>   \n>               if not isinstance(output_dict[module], dict):\n> -                utils.eprint(f'Error: Output of {module.type} is not a dictionary')\n> +                logger.error(f'Error: Output of {module.type} is not a dictionary')\n>                   continue\n>   \n>               lines = self._stringify_dict(output_dict[module])\n> diff --git a/utils/tuning/libtuning/image.py b/utils/tuning/libtuning/image.py\n> index 6ff60ec17ec4..2c4d774f11e2 100644\n> --- a/utils/tuning/libtuning/image.py\n> +++ b/utils/tuning/libtuning/image.py\n> @@ -13,6 +13,9 @@ import re\n>   \n>   import libtuning as lt\n>   import libtuning.utils as utils\n> +import logging\n> +\n> +logger = logging.getLogger(__name__)\n>   \n>   \n>   class Image:\n> @@ -25,13 +28,13 @@ class Image:\n>           try:\n>               self._load_metadata_exif()\n>           except Exception as e:\n> -            utils.eprint(f'Failed to load metadata from {self.path}: {e}')\n> +            logger.error(f'Failed to load metadata from {self.path}: {e}')\n>               raise e\n>   \n>           try:\n>               self._read_image_dng()\n>           except Exception as e:\n> -            utils.eprint(f'Failed to load image data from {self.path}: {e}')\n> +            logger.error(f'Failed to load image data from {self.path}: {e}')\n>               raise e\n>   \n>       @property\n> diff --git a/utils/tuning/libtuning/libtuning.py b/utils/tuning/libtuning/libtuning.py\n> index 5e22288df49b..5342e5d6daaa 100644\n> --- a/utils/tuning/libtuning/libtuning.py\n> +++ b/utils/tuning/libtuning/libtuning.py\n> @@ -5,13 +5,14 @@\n>   # An infrastructure for camera tuning tools\n>   \n>   import argparse\n> +import logging\n>   \n>   import libtuning as lt\n>   import libtuning.utils as utils\n> -from libtuning.utils import eprint\n>   \n>   from enum import Enum, IntEnum\n>   \n> +logger = logging.getLogger(__name__)\n>   \n>   class Color(IntEnum):\n>       R = 0\n> @@ -112,10 +113,10 @@ class Tuner(object):\n>           for module_type in output_order:\n>               modules = [module for module in self.modules if module.type == module_type.type]\n>               if len(modules) > 1:\n> -                eprint(f'Multiple modules found for module type \"{module_type.type}\"')\n> +                logger.error(f'Multiple modules found for module type \"{module_type.type}\"')\n>                   return False\n>               if len(modules) < 1:\n> -                eprint(f'No module found for module type \"{module_type.type}\"')\n> +                logger.error(f'No module found for module type \"{module_type.type}\"')\n>                   return False\n>               self.output_order.append(modules[0])\n>   \n> @@ -124,19 +125,19 @@ class Tuner(object):\n>       # \\todo Validate parser and generator at Tuner construction time?\n>       def _validate_settings(self):\n>           if self.parser is None:\n> -            eprint('Missing parser')\n> +            logger.error('Missing parser')\n>               return False\n>   \n>           if self.generator is None:\n> -            eprint('Missing generator')\n> +            logger.error('Missing generator')\n>               return False\n>   \n>           if len(self.modules) == 0:\n> -            eprint('No modules added')\n> +            logger.error('No modules added')\n>               return False\n>   \n>           if len(self.output_order) != len(self.modules):\n> -            eprint('Number of outputs does not match number of modules')\n> +            logger.error('Number of outputs does not match number of modules')\n>               return False\n>   \n>           return True\n> @@ -183,7 +184,7 @@ class Tuner(object):\n>   \n>           for module in self.modules:\n>               if not module.validate_config(self.config):\n> -                eprint(f'Config is invalid for module {module.type}')\n> +                logger.error(f'Config is invalid for module {module.type}')\n>                   return -1\n>   \n>           has_lsc = any(isinstance(m, lt.modules.lsc.LSC) for m in self.modules)\n> @@ -192,14 +193,14 @@ class Tuner(object):\n>   \n>           images = utils.load_images(args.input, self.config, not has_only_lsc, has_lsc)\n>           if images is None or len(images) == 0:\n> -            eprint(f'No images were found, or able to load')\n> +            logger.error(f'No images were found, or able to load')\n>               return -1\n>   \n>           # Do the tuning\n>           for module in self.modules:\n>               out = module.process(self.config, images, self.output)\n>               if out is None:\n> -                eprint(f'Module {module.name} failed to process, aborting')\n> +                logger.error(f'Module {module.hr_name} failed to process...')\n>                   break\n>               self.output[module] = out\n>   \n> diff --git a/utils/tuning/libtuning/macbeth.py b/utils/tuning/libtuning/macbeth.py\n> index 265a33d68378..28051de8155c 100644\n> --- a/utils/tuning/libtuning/macbeth.py\n> +++ b/utils/tuning/libtuning/macbeth.py\n> @@ -13,12 +13,15 @@ import os\n>   from pathlib import Path\n>   import numpy as np\n>   import warnings\n> +import logging\n>   from sklearn import cluster as cluster\n>   \n>   from .ctt_ransac import get_square_verts, get_square_centres\n>   \n>   from libtuning.image import Image\n>   \n> +logger = logging.getLogger(__name__)\n> +\n>   \n>   # Reshape image to fixed width without distorting returns image and scale\n>   # factor\n> @@ -374,7 +377,7 @@ def get_macbeth_chart(img, ref_data):\n>   \n>       # Catch macbeth errors and continue with code\n>       except MacbethError as error:\n> -        eprint(error)\n> +        logger.warning(error)\n>           return (0, None, None, False)\n>   \n>   \n> @@ -497,7 +500,7 @@ def find_macbeth(img, mac_config):\n>   \n>       coords_fit = coords\n>       if cor < 0.75:\n> -        eprint(f'Warning: Low confidence {cor:.3f} for macbeth chart in {img.path.name}')\n> +        logger.warning(f'Low confidence {cor:.3f} for macbeth chart')\n\n\nI probably would have kept the image file name here, just in case it's hard to figure out which was \nthe duff image without it\n\n>   \n>       if show:\n>           draw_macbeth_results(img, coords_fit)\n> @@ -510,18 +513,18 @@ def locate_macbeth(image: Image, config: dict):\n>       av_chan = (np.mean(np.array(image.channels), axis=0) / (2**16))\n>       av_val = np.mean(av_chan)\n>       if av_val < image.blacklevel_16 / (2**16) + 1 / 64:\n> -        eprint(f'Image {image.path.name} too dark')\n> +        logger.warning(f'Image {image.path.name} too dark')\n>           return None\n>   \n>       macbeth = find_macbeth(av_chan, config['general']['macbeth'])\n>   \n>       if macbeth is None:\n> -        eprint(f'No macbeth chart found in {image.path.name}')\n> +        logger.warning(f'No macbeth chart found in {image.path.name}')\n>           return None\n>   \n>       mac_cen_coords = macbeth[1]\n>       if not image.get_patches(mac_cen_coords):\n> -        eprint(f'Macbeth patches have saturated in {image.path.name}')\n> +        logger.warning(f'Macbeth patches have saturated in {image.path.name}')\n>           return None\n>   \n>       return macbeth\n> diff --git a/utils/tuning/libtuning/modules/lsc/raspberrypi.py b/utils/tuning/libtuning/modules/lsc/raspberrypi.py\n> index f19c71637b89..99bc4fe6e07f 100644\n> --- a/utils/tuning/libtuning/modules/lsc/raspberrypi.py\n> +++ b/utils/tuning/libtuning/modules/lsc/raspberrypi.py\n> @@ -12,7 +12,9 @@ import libtuning.utils as utils\n>   \n>   from numbers import Number\n>   import numpy as np\n> +import logging\n>   \n> +logger = logging.getLogger(__name__)\n>   \n>   class ALSCRaspberryPi(LSC):\n>       # Override the type name so that the parser can match the entry in the\n> @@ -35,7 +37,7 @@ class ALSCRaspberryPi(LSC):\n>   \n>       def validate_config(self, config: dict) -> bool:\n>           if self not in config:\n> -            utils.eprint(f'{self.type} not in config')\n> +            logger.error(f'{self.type} not in config')\n>               return False\n>   \n>           valid = True\n> @@ -46,14 +48,14 @@ class ALSCRaspberryPi(LSC):\n>           color_key = self.do_color.name\n>   \n>           if lum_key not in conf and self.luminance_strength.required:\n> -            utils.eprint(f'{lum_key} is not in config')\n> +            logger.error(f'{lum_key} is not in config')\n>               valid = False\n>   \n>           if lum_key in conf and (conf[lum_key] < 0 or conf[lum_key] > 1):\n> -            utils.eprint(f'Warning: {lum_key} is not in range [0, 1]; defaulting to 0.5')\n> +            logger.warning(f'{lum_key} is not in range [0, 1]; defaulting to 0.5')\n>   \n>           if color_key not in conf and self.do_color.required:\n> -            utils.eprint(f'{color_key} is not in config')\n> +            logger.error(f'{color_key} is not in config')\n>               valid = False\n>   \n>           return valid\n> @@ -235,7 +237,7 @@ class ALSCRaspberryPi(LSC):\n>           if count == 1:\n>               output['sigma'] = 0.005\n>               output['sigma_Cb'] = 0.005\n> -            utils.eprint('Warning: Only one alsc calibration found; standard sigmas used for adaptive algorithm.')\n> +            logger.warning('Only one alsc calibration found; standard sigmas used for adaptive algorithm.')\n>               return output\n>   \n>           # Obtain worst-case scenario residual sigmas\n> diff --git a/utils/tuning/libtuning/utils.py b/utils/tuning/libtuning/utils.py\n> index f099c0ed685c..872341407b7b 100644\n> --- a/utils/tuning/libtuning/utils.py\n> +++ b/utils/tuning/libtuning/utils.py\n> @@ -12,16 +12,15 @@ import os\n>   from pathlib import Path\n>   import re\n>   import sys\n> +import logging\n>   \n>   import libtuning as lt\n>   from libtuning.image import Image\n>   from libtuning.macbeth import locate_macbeth\n>   \n> -# Utility functions\n> -\n> +logger = logging.getLogger(__name__)\n>   \n> -def eprint(*args, **kwargs):\n> -    print(*args, file=sys.stderr, **kwargs)\n> +# Utility functions\n>   \n>   \n>   def get_module_by_type_name(modules, name):\n> @@ -45,7 +44,7 @@ def _list_image_files(directory):\n>   def _parse_image_filename(fn: Path):\n>       result = re.search(r'^(alsc_)?(\\d+)[kK]_(\\d+)?[lLuU]?.\\w{3,4}$', fn.name)\n>       if result is None:\n> -        eprint(f'The file name of {fn.name} is incorrectly formatted')\n> +        logger.error(f'The file name of {fn.name} is incorrectly formatted')\n>           return None, None, None\n>   \n>       color = int(result.group(2))\n> @@ -72,7 +71,7 @@ def _validate_images(images):\n>   def load_images(input_dir: str, config: dict, load_nonlsc: bool, load_lsc: bool) -> list:\n>       files = _list_image_files(input_dir)\n>       if len(files) == 0:\n> -        eprint(f'No images found in {input_dir}')\n> +        logger.error(f'No images found in {input_dir}')\n>           return None\n>   \n>       images = []\n> @@ -83,19 +82,19 @@ def load_images(input_dir: str, config: dict, load_nonlsc: bool, load_lsc: bool)\n>   \n>           # Skip lsc image if we don't need it\n>           if lsc_only and not load_lsc:\n> -            eprint(f'Skipping {f.name} as this tuner has no LSC module')\n> +            logger.warning(f'Skipping {f.name} as this tuner has no LSC module')\n>               continue\n>   \n>           # Skip non-lsc image if we don't need it\n>           if not lsc_only and not load_nonlsc:\n> -            eprint(f'Skipping {f.name} as this tuner only has an LSC module')\n> +            logger.warning(f'Skipping {f.name} as this tuner only has an LSC module')\n>               continue\n>   \n>           # Load image\n>           try:\n>               image = Image(f)\n>           except Exception as e:\n> -            eprint(f'Failed to load image {f.name}: {e}')\n> +            logger.error(f'Failed to load image {f.name}: {e}')\n>               continue\n>   \n>           # Populate simple fields\n> diff --git a/utils/tuning/requirements.txt b/utils/tuning/requirements.txt\n> index d1dc589d0329..c3c20cee1263 100644\n> --- a/utils/tuning/requirements.txt\n> +++ b/utils/tuning/requirements.txt\n> @@ -1,3 +1,4 @@\n> +coloredlogs\n>   numpy\n>   opencv-python\n>   py3exiv2\n> diff --git a/utils/tuning/rkisp1.py b/utils/tuning/rkisp1.py\n> index d0ce15d5ed7a..2606e07a93f7 100755\n> --- a/utils/tuning/rkisp1.py\n> +++ b/utils/tuning/rkisp1.py\n> @@ -5,6 +5,8 @@\n>   #\n>   # Tuning script for rkisp1\n>   \n> +import coloredlogs\n> +import logging\n>   import sys\n>   \n>   import libtuning as lt\n> @@ -13,6 +15,9 @@ from libtuning.generators import YamlOutput\n>   from libtuning.modules.lsc import LSCRkISP1\n>   from libtuning.modules.agc import AGCRkISP1\n>   \n> +\n> +coloredlogs.install(level=logging.INFO, fmt='%(name)s %(levelname)s %(message)s')\n> +\n>   tuner = lt.Tuner('RkISP1')\n>   tuner.add(LSCRkISP1(\n>             debug=[lt.Debug.Plot],","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 215BBBEFBE\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  5 Jul 2024 16:00:00 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id D23CF619C7;\n\tFri,  5 Jul 2024 17:59:58 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 6FD48619BF\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  5 Jul 2024 17:59:56 +0200 (CEST)","from [192.168.0.43]\n\t(cpc141996-chfd3-2-0-cust928.12-3.cable.virginm.net [86.13.91.161])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 8860C541\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  5 Jul 2024 17:59:26 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"hPR0bqan\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1720195166;\n\tbh=Zroy4JdeO8RWzKB7lOfaUNh/V9ZFZqqsZIjrhu92K+Q=;\n\th=Date:Subject:To:References:From:In-Reply-To:From;\n\tb=hPR0bqanONK2UTrYFB/rWqSNX3DyBZZaK3VwbGJcTadBHAgzjNWFLB9RFE00/iWN1\n\tpCg2yo+3XJt2mriJ1Lk+4SElO4uJv4iqg4FpR2zYJRp79ruZoyAqRrWPQbGl80CS8H\n\tJA7OR7mxSsLZxQsbeA7OiLn0nGy40GqXx/pk+9UM=","Message-ID":"<327187c8-b259-4bd8-9cdc-a2511f692a83@ideasonboard.com>","Date":"Fri, 5 Jul 2024 16:59:53 +0100","MIME-Version":"1.0","User-Agent":"Mozilla Thunderbird","Subject":"Re: [PATCH v4 07/23] libtuning: Migrate prints to python logging\n\tframework","To":"libcamera-devel@lists.libcamera.org","References":"<20240705144209.418906-1-stefan.klug@ideasonboard.com>\n\t<20240705144209.418906-8-stefan.klug@ideasonboard.com>","Content-Language":"en-US","From":"Dan Scally <dan.scally@ideasonboard.com>","Autocrypt":"addr=dan.scally@ideasonboard.com; keydata=\n\t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","In-Reply-To":"<20240705144209.418906-8-stefan.klug@ideasonboard.com>","Content-Type":"text/plain; charset=UTF-8; format=flowed","Content-Transfer-Encoding":"7bit","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":30367,"web_url":"https://patchwork.libcamera.org/comment/30367/","msgid":"<tl6rjaprte6mvgkquxizcn64i2hh72bsoenmw54h26lhyn7fmg@n23ujh726m4v>","date":"2024-07-05T19:05:10","subject":"Re: [PATCH v4 07/23] libtuning: Migrate prints to python logging\n\tframework","submitter":{"id":184,"url":"https://patchwork.libcamera.org/api/people/184/","name":"Stefan Klug","email":"stefan.klug@ideasonboard.com"},"content":"Hi Dan,\n\nThank you for the review.\n\nOn Fri, Jul 05, 2024 at 04:59:53PM +0100, Dan Scally wrote:\n> Hi Stefan, thanks for the patch\n> \n> On 05/07/2024 15:41, Stefan Klug wrote:\n> > In ctt_ccm.py the logging functionality of the Cam object was used. As\n> > we don't want to port over that class, it needs to be replaced anyways.\n> > While at it, also replace the eprint function as it doesn't add any\n> > value over the logging framework and misses the ability for easy log\n> > formatting.\n> > \n> > For nice output formatting add the coloredlogs library.\n> > \n> > Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>\n> > Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>\n> > ---\n> \n> \n> Looks ok to me; only one comment below but either way: Reviewed-by: Daniel\n> Scally <dan.scally@ideasonboard.com>\n> \n> >   utils/tuning/libtuning/ctt_ccm.py             | 27 ++++++++++---------\n> >   .../libtuning/generators/yaml_output.py       |  5 ++--\n> >   utils/tuning/libtuning/image.py               |  7 +++--\n> >   utils/tuning/libtuning/libtuning.py           | 21 ++++++++-------\n> >   utils/tuning/libtuning/macbeth.py             | 13 +++++----\n> >   .../libtuning/modules/lsc/raspberrypi.py      | 12 +++++----\n> >   utils/tuning/libtuning/utils.py               | 17 ++++++------\n> >   utils/tuning/requirements.txt                 |  1 +\n> >   utils/tuning/rkisp1.py                        |  5 ++++\n> >   9 files changed, 62 insertions(+), 46 deletions(-)\n> > \n> > diff --git a/utils/tuning/libtuning/ctt_ccm.py b/utils/tuning/libtuning/ctt_ccm.py\n> > index f37adaf45538..c4362756c3c0 100644\n> > --- a/utils/tuning/libtuning/ctt_ccm.py\n> > +++ b/utils/tuning/libtuning/ctt_ccm.py\n> > @@ -4,6 +4,8 @@\n> >   #\n> >   # camera tuning tool for CCM (colour correction matrix)\n> > +import logging\n> > +\n> >   import numpy as np\n> >   from scipy.optimize import minimize\n> > @@ -12,6 +14,8 @@ from .image import Image\n> >   from .ctt_awb import get_alsc_patches\n> >   from .utils import visualise_macbeth_chart\n> > +logger = logging.getLogger(__name__)\n> > +\n> >   \"\"\"\n> >   takes 8-bit macbeth chart values, degammas and returns 16 bit\n> >   \"\"\"\n> > @@ -129,7 +133,7 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n> >       \"\"\"\n> >       ccm_tab = {}\n> >       for Img in imgs:\n> > -        Cam.log += '\\nProcessing image: ' + Img.name\n> > +        logger.info('Processing image: ' + Img.name)\n> >           \"\"\"\n> >           get macbeth patches with alsc applied if alsc enabled.\n> >           Note: if alsc is disabled then colour_cals will be set to None and no\n> > @@ -154,7 +158,7 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n> >           each channel for each patch\n> >           \"\"\"\n> >           gain = np.mean(m_srgb) / np.mean((r, g, b))\n> > -        Cam.log += '\\nGain with respect to standard colours: {:.3f}'.format(gain)\n> > +        logger.info(f'Gain with respect to standard colours: {gain:.3f}')\n> >           r = np.mean(gain * r, axis=1)\n> >           b = np.mean(gain * b, axis=1)\n> >           g = np.mean(gain * g, axis=1)\n> > @@ -192,15 +196,13 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n> >           zero since the input data is imperfect\n> >           '''\n> > -        Cam.log += (\"\\n \\n Optimised Matrix Below: \\n \\n\")\n> >           [r1, r2, g1, g2, b1, b2] = result.x\n> >           # The new, optimised color correction matrix values\n> > +        # This is the optimised Color Matrix (preserving greys by summing rows up to 1)\n> >           optimised_ccm = [r1, r2, (1 - r1 - r2), g1, g2, (1 - g1 - g2), b1, b2, (1 - b1 - b2)]\n> > -        # This is the optimised Color Matrix (preserving greys by summing rows up to 1)\n> > -        Cam.log += str(optimised_ccm)\n> > -        Cam.log += \"\\n Old Color Correction Matrix Below \\n\"\n> > -        Cam.log += str(ccm)\n> > +        logger.info(f'Optimized Matrix: {np.round(optimised_ccm, 4)}')\n> > +        logger.info(f'Old Matrix:       {np.round(ccm, 4)}')\n> >           formatted_ccm = np.array(original_ccm).reshape((3, 3))\n> > @@ -229,7 +231,7 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n> >           We now want to spit out some data that shows\n> >           how the optimisation has improved the color matrices\n> >           '''\n> > -        Cam.log += \"Here are the Improvements\"\n> > +        logger.info(\"Here are the Improvements\")\n> >           # CALCULATE WORST CASE delta e\n> >           old_worst_delta_e = 0\n> > @@ -244,8 +246,8 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n> >               if new_delta_e > new_worst_delta_e:\n> >                   new_worst_delta_e = new_delta_e\n> > -        Cam.log += \"Before color correction matrix was optimised, we got an average delta E of \" + str(before_average) + \" and a maximum delta E of \" + str(old_worst_delta_e)\n> > -        Cam.log += \"After color correction matrix was optimised, we got an average delta E of \" + str(after_average) + \" and a maximum delta E of \" + str(new_worst_delta_e)\n> > +        logger.info(f'delta E optimized: average: {after_average:.2f}  max:{new_worst_delta_e:.2f}')\n> > +        logger.info(f'delta E old:       average: {before_average:.2f}  max:{old_worst_delta_e:.2f}')\n> >           visualise_macbeth_chart(m_rgb, optimised_ccm_rgb, after_gamma_rgb, str(Img.col) + str(matrix_selection_types[typenum]))\n> >           '''\n> > @@ -262,9 +264,8 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n> >               ccm_tab[Img.col].append(optimised_ccm)\n> >           else:\n> >               ccm_tab[Img.col] = [optimised_ccm]\n> > -        Cam.log += '\\n'\n> > -    Cam.log += '\\nFinished processing images'\n> > +    logger.info('Finished processing images')\n> >       \"\"\"\n> >       average any ccms that share a colour temperature\n> >       \"\"\"\n> > @@ -273,7 +274,7 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n> >           tab = np.where((10000 * tab) % 1 <= 0.05, tab + 0.00001, tab)\n> >           tab = np.where((10000 * tab) % 1 >= 0.95, tab - 0.00001, tab)\n> >           ccm_tab[k] = list(np.round(tab, 5))\n> > -        Cam.log += '\\nMatrix calculated for colour temperature of {} K'.format(k)\n> > +        logger.info(f'Matrix calculated for colour temperature of {k} K')\n> >       \"\"\"\n> >       return all ccms with respective colour temperature in the correct format,\n> > diff --git a/utils/tuning/libtuning/generators/yaml_output.py b/utils/tuning/libtuning/generators/yaml_output.py\n> > index 8f22d386f1b3..31e265df4ea7 100644\n> > --- a/utils/tuning/libtuning/generators/yaml_output.py\n> > +++ b/utils/tuning/libtuning/generators/yaml_output.py\n> > @@ -9,8 +9,9 @@ from .generator import Generator\n> >   from numbers import Number\n> >   from pathlib import Path\n> > -import libtuning.utils as utils\n> > +import logging\n> > +logger = logging.getLogger(__name__)\n> >   class YamlOutput(Generator):\n> >       def __init__(self):\n> > @@ -112,7 +113,7 @@ class YamlOutput(Generator):\n> >                   continue\n> >               if not isinstance(output_dict[module], dict):\n> > -                utils.eprint(f'Error: Output of {module.type} is not a dictionary')\n> > +                logger.error(f'Error: Output of {module.type} is not a dictionary')\n> >                   continue\n> >               lines = self._stringify_dict(output_dict[module])\n> > diff --git a/utils/tuning/libtuning/image.py b/utils/tuning/libtuning/image.py\n> > index 6ff60ec17ec4..2c4d774f11e2 100644\n> > --- a/utils/tuning/libtuning/image.py\n> > +++ b/utils/tuning/libtuning/image.py\n> > @@ -13,6 +13,9 @@ import re\n> >   import libtuning as lt\n> >   import libtuning.utils as utils\n> > +import logging\n> > +\n> > +logger = logging.getLogger(__name__)\n> >   class Image:\n> > @@ -25,13 +28,13 @@ class Image:\n> >           try:\n> >               self._load_metadata_exif()\n> >           except Exception as e:\n> > -            utils.eprint(f'Failed to load metadata from {self.path}: {e}')\n> > +            logger.error(f'Failed to load metadata from {self.path}: {e}')\n> >               raise e\n> >           try:\n> >               self._read_image_dng()\n> >           except Exception as e:\n> > -            utils.eprint(f'Failed to load image data from {self.path}: {e}')\n> > +            logger.error(f'Failed to load image data from {self.path}: {e}')\n> >               raise e\n> >       @property\n> > diff --git a/utils/tuning/libtuning/libtuning.py b/utils/tuning/libtuning/libtuning.py\n> > index 5e22288df49b..5342e5d6daaa 100644\n> > --- a/utils/tuning/libtuning/libtuning.py\n> > +++ b/utils/tuning/libtuning/libtuning.py\n> > @@ -5,13 +5,14 @@\n> >   # An infrastructure for camera tuning tools\n> >   import argparse\n> > +import logging\n> >   import libtuning as lt\n> >   import libtuning.utils as utils\n> > -from libtuning.utils import eprint\n> >   from enum import Enum, IntEnum\n> > +logger = logging.getLogger(__name__)\n> >   class Color(IntEnum):\n> >       R = 0\n> > @@ -112,10 +113,10 @@ class Tuner(object):\n> >           for module_type in output_order:\n> >               modules = [module for module in self.modules if module.type == module_type.type]\n> >               if len(modules) > 1:\n> > -                eprint(f'Multiple modules found for module type \"{module_type.type}\"')\n> > +                logger.error(f'Multiple modules found for module type \"{module_type.type}\"')\n> >                   return False\n> >               if len(modules) < 1:\n> > -                eprint(f'No module found for module type \"{module_type.type}\"')\n> > +                logger.error(f'No module found for module type \"{module_type.type}\"')\n> >                   return False\n> >               self.output_order.append(modules[0])\n> > @@ -124,19 +125,19 @@ class Tuner(object):\n> >       # \\todo Validate parser and generator at Tuner construction time?\n> >       def _validate_settings(self):\n> >           if self.parser is None:\n> > -            eprint('Missing parser')\n> > +            logger.error('Missing parser')\n> >               return False\n> >           if self.generator is None:\n> > -            eprint('Missing generator')\n> > +            logger.error('Missing generator')\n> >               return False\n> >           if len(self.modules) == 0:\n> > -            eprint('No modules added')\n> > +            logger.error('No modules added')\n> >               return False\n> >           if len(self.output_order) != len(self.modules):\n> > -            eprint('Number of outputs does not match number of modules')\n> > +            logger.error('Number of outputs does not match number of modules')\n> >               return False\n> >           return True\n> > @@ -183,7 +184,7 @@ class Tuner(object):\n> >           for module in self.modules:\n> >               if not module.validate_config(self.config):\n> > -                eprint(f'Config is invalid for module {module.type}')\n> > +                logger.error(f'Config is invalid for module {module.type}')\n> >                   return -1\n> >           has_lsc = any(isinstance(m, lt.modules.lsc.LSC) for m in self.modules)\n> > @@ -192,14 +193,14 @@ class Tuner(object):\n> >           images = utils.load_images(args.input, self.config, not has_only_lsc, has_lsc)\n> >           if images is None or len(images) == 0:\n> > -            eprint(f'No images were found, or able to load')\n> > +            logger.error(f'No images were found, or able to load')\n> >               return -1\n> >           # Do the tuning\n> >           for module in self.modules:\n> >               out = module.process(self.config, images, self.output)\n> >               if out is None:\n> > -                eprint(f'Module {module.name} failed to process, aborting')\n> > +                logger.error(f'Module {module.hr_name} failed to process...')\n> >                   break\n> >               self.output[module] = out\n> > diff --git a/utils/tuning/libtuning/macbeth.py b/utils/tuning/libtuning/macbeth.py\n> > index 265a33d68378..28051de8155c 100644\n> > --- a/utils/tuning/libtuning/macbeth.py\n> > +++ b/utils/tuning/libtuning/macbeth.py\n> > @@ -13,12 +13,15 @@ import os\n> >   from pathlib import Path\n> >   import numpy as np\n> >   import warnings\n> > +import logging\n> >   from sklearn import cluster as cluster\n> >   from .ctt_ransac import get_square_verts, get_square_centres\n> >   from libtuning.image import Image\n> > +logger = logging.getLogger(__name__)\n> > +\n> >   # Reshape image to fixed width without distorting returns image and scale\n> >   # factor\n> > @@ -374,7 +377,7 @@ def get_macbeth_chart(img, ref_data):\n> >       # Catch macbeth errors and continue with code\n> >       except MacbethError as error:\n> > -        eprint(error)\n> > +        logger.warning(error)\n> >           return (0, None, None, False)\n> > @@ -497,7 +500,7 @@ def find_macbeth(img, mac_config):\n> >       coords_fit = coords\n> >       if cor < 0.75:\n> > -        eprint(f'Warning: Low confidence {cor:.3f} for macbeth chart in {img.path.name}')\n> > +        logger.warning(f'Low confidence {cor:.3f} for macbeth chart')\n> \n> \n> I probably would have kept the image file name here, just in case it's hard\n> to figure out which was the duff image without it\n\nThe fun thing is that in this function img is a numpy array without a\npath property :-). Should definitely be cleaned up.\n\nCheers,\nStefan\n\n\n> \n> >       if show:\n> >           draw_macbeth_results(img, coords_fit)\n> > @@ -510,18 +513,18 @@ def locate_macbeth(image: Image, config: dict):\n> >       av_chan = (np.mean(np.array(image.channels), axis=0) / (2**16))\n> >       av_val = np.mean(av_chan)\n> >       if av_val < image.blacklevel_16 / (2**16) + 1 / 64:\n> > -        eprint(f'Image {image.path.name} too dark')\n> > +        logger.warning(f'Image {image.path.name} too dark')\n> >           return None\n> >       macbeth = find_macbeth(av_chan, config['general']['macbeth'])\n> >       if macbeth is None:\n> > -        eprint(f'No macbeth chart found in {image.path.name}')\n> > +        logger.warning(f'No macbeth chart found in {image.path.name}')\n> >           return None\n> >       mac_cen_coords = macbeth[1]\n> >       if not image.get_patches(mac_cen_coords):\n> > -        eprint(f'Macbeth patches have saturated in {image.path.name}')\n> > +        logger.warning(f'Macbeth patches have saturated in {image.path.name}')\n> >           return None\n> >       return macbeth\n> > diff --git a/utils/tuning/libtuning/modules/lsc/raspberrypi.py b/utils/tuning/libtuning/modules/lsc/raspberrypi.py\n> > index f19c71637b89..99bc4fe6e07f 100644\n> > --- a/utils/tuning/libtuning/modules/lsc/raspberrypi.py\n> > +++ b/utils/tuning/libtuning/modules/lsc/raspberrypi.py\n> > @@ -12,7 +12,9 @@ import libtuning.utils as utils\n> >   from numbers import Number\n> >   import numpy as np\n> > +import logging\n> > +logger = logging.getLogger(__name__)\n> >   class ALSCRaspberryPi(LSC):\n> >       # Override the type name so that the parser can match the entry in the\n> > @@ -35,7 +37,7 @@ class ALSCRaspberryPi(LSC):\n> >       def validate_config(self, config: dict) -> bool:\n> >           if self not in config:\n> > -            utils.eprint(f'{self.type} not in config')\n> > +            logger.error(f'{self.type} not in config')\n> >               return False\n> >           valid = True\n> > @@ -46,14 +48,14 @@ class ALSCRaspberryPi(LSC):\n> >           color_key = self.do_color.name\n> >           if lum_key not in conf and self.luminance_strength.required:\n> > -            utils.eprint(f'{lum_key} is not in config')\n> > +            logger.error(f'{lum_key} is not in config')\n> >               valid = False\n> >           if lum_key in conf and (conf[lum_key] < 0 or conf[lum_key] > 1):\n> > -            utils.eprint(f'Warning: {lum_key} is not in range [0, 1]; defaulting to 0.5')\n> > +            logger.warning(f'{lum_key} is not in range [0, 1]; defaulting to 0.5')\n> >           if color_key not in conf and self.do_color.required:\n> > -            utils.eprint(f'{color_key} is not in config')\n> > +            logger.error(f'{color_key} is not in config')\n> >               valid = False\n> >           return valid\n> > @@ -235,7 +237,7 @@ class ALSCRaspberryPi(LSC):\n> >           if count == 1:\n> >               output['sigma'] = 0.005\n> >               output['sigma_Cb'] = 0.005\n> > -            utils.eprint('Warning: Only one alsc calibration found; standard sigmas used for adaptive algorithm.')\n> > +            logger.warning('Only one alsc calibration found; standard sigmas used for adaptive algorithm.')\n> >               return output\n> >           # Obtain worst-case scenario residual sigmas\n> > diff --git a/utils/tuning/libtuning/utils.py b/utils/tuning/libtuning/utils.py\n> > index f099c0ed685c..872341407b7b 100644\n> > --- a/utils/tuning/libtuning/utils.py\n> > +++ b/utils/tuning/libtuning/utils.py\n> > @@ -12,16 +12,15 @@ import os\n> >   from pathlib import Path\n> >   import re\n> >   import sys\n> > +import logging\n> >   import libtuning as lt\n> >   from libtuning.image import Image\n> >   from libtuning.macbeth import locate_macbeth\n> > -# Utility functions\n> > -\n> > +logger = logging.getLogger(__name__)\n> > -def eprint(*args, **kwargs):\n> > -    print(*args, file=sys.stderr, **kwargs)\n> > +# Utility functions\n> >   def get_module_by_type_name(modules, name):\n> > @@ -45,7 +44,7 @@ def _list_image_files(directory):\n> >   def _parse_image_filename(fn: Path):\n> >       result = re.search(r'^(alsc_)?(\\d+)[kK]_(\\d+)?[lLuU]?.\\w{3,4}$', fn.name)\n> >       if result is None:\n> > -        eprint(f'The file name of {fn.name} is incorrectly formatted')\n> > +        logger.error(f'The file name of {fn.name} is incorrectly formatted')\n> >           return None, None, None\n> >       color = int(result.group(2))\n> > @@ -72,7 +71,7 @@ def _validate_images(images):\n> >   def load_images(input_dir: str, config: dict, load_nonlsc: bool, load_lsc: bool) -> list:\n> >       files = _list_image_files(input_dir)\n> >       if len(files) == 0:\n> > -        eprint(f'No images found in {input_dir}')\n> > +        logger.error(f'No images found in {input_dir}')\n> >           return None\n> >       images = []\n> > @@ -83,19 +82,19 @@ def load_images(input_dir: str, config: dict, load_nonlsc: bool, load_lsc: bool)\n> >           # Skip lsc image if we don't need it\n> >           if lsc_only and not load_lsc:\n> > -            eprint(f'Skipping {f.name} as this tuner has no LSC module')\n> > +            logger.warning(f'Skipping {f.name} as this tuner has no LSC module')\n> >               continue\n> >           # Skip non-lsc image if we don't need it\n> >           if not lsc_only and not load_nonlsc:\n> > -            eprint(f'Skipping {f.name} as this tuner only has an LSC module')\n> > +            logger.warning(f'Skipping {f.name} as this tuner only has an LSC module')\n> >               continue\n> >           # Load image\n> >           try:\n> >               image = Image(f)\n> >           except Exception as e:\n> > -            eprint(f'Failed to load image {f.name}: {e}')\n> > +            logger.error(f'Failed to load image {f.name}: {e}')\n> >               continue\n> >           # Populate simple fields\n> > diff --git a/utils/tuning/requirements.txt b/utils/tuning/requirements.txt\n> > index d1dc589d0329..c3c20cee1263 100644\n> > --- a/utils/tuning/requirements.txt\n> > +++ b/utils/tuning/requirements.txt\n> > @@ -1,3 +1,4 @@\n> > +coloredlogs\n> >   numpy\n> >   opencv-python\n> >   py3exiv2\n> > diff --git a/utils/tuning/rkisp1.py b/utils/tuning/rkisp1.py\n> > index d0ce15d5ed7a..2606e07a93f7 100755\n> > --- a/utils/tuning/rkisp1.py\n> > +++ b/utils/tuning/rkisp1.py\n> > @@ -5,6 +5,8 @@\n> >   #\n> >   # Tuning script for rkisp1\n> > +import coloredlogs\n> > +import logging\n> >   import sys\n> >   import libtuning as lt\n> > @@ -13,6 +15,9 @@ from libtuning.generators import YamlOutput\n> >   from libtuning.modules.lsc import LSCRkISP1\n> >   from libtuning.modules.agc import AGCRkISP1\n> > +\n> > +coloredlogs.install(level=logging.INFO, fmt='%(name)s %(levelname)s %(message)s')\n> > +\n> >   tuner = lt.Tuner('RkISP1')\n> >   tuner.add(LSCRkISP1(\n> >             debug=[lt.Debug.Plot],","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 0DFAEBD87C\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  5 Jul 2024 19:05:17 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id CEF7862E22;\n\tFri,  5 Jul 2024 21:05:15 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id BD1F1619BF\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  5 Jul 2024 21:05:13 +0200 (CEST)","from ideasonboard.com (unknown\n\t[IPv6:2a00:6020:448c:6c00:60b6:33a3:3a20:6030])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id CE4424CC;\n\tFri,  5 Jul 2024 21:04:43 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"pl3tnqSJ\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1720206283;\n\tbh=cnPG1CoaQzvXFenPWZShYbvN/i8wSwr/unyqhIAUjMI=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=pl3tnqSJOkQGGkrOu6Igj+LGP3nKbDr58EBtDB5slQOS1yJC5nBULdy/+dGFI8lA4\n\t4AezekvRXs63M6tif1zzDUCTF6AnOY9jC7avv6cvI+ux1lCLwFJv47tJrDsEY59q/g\n\tLfjMm1D10owyf4nI51+gm4LJYMtOhC+LcCMGolrA=","Date":"Fri, 5 Jul 2024 21:05:10 +0200","From":"Stefan Klug <stefan.klug@ideasonboard.com>","To":"Dan Scally <dan.scally@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Subject":"Re: [PATCH v4 07/23] libtuning: Migrate prints to python logging\n\tframework","Message-ID":"<tl6rjaprte6mvgkquxizcn64i2hh72bsoenmw54h26lhyn7fmg@n23ujh726m4v>","References":"<20240705144209.418906-1-stefan.klug@ideasonboard.com>\n\t<20240705144209.418906-8-stefan.klug@ideasonboard.com>\n\t<327187c8-b259-4bd8-9cdc-a2511f692a83@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<327187c8-b259-4bd8-9cdc-a2511f692a83@ideasonboard.com>","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]