210 lines
7.2 KiB
Python
210 lines
7.2 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
#
|
|
# Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
|
|
#
|
|
# An infrastructure for camera tuning tools
|
|
|
|
import argparse
|
|
import logging
|
|
|
|
import libtuning as lt
|
|
import libtuning.utils as utils
|
|
|
|
from enum import Enum, IntEnum
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class Color(IntEnum):
|
|
R = 0
|
|
GR = 1
|
|
GB = 2
|
|
B = 3
|
|
|
|
|
|
class Debug(Enum):
|
|
Plot = 1
|
|
|
|
|
|
# @brief What to do with the leftover pixels after dividing them into ALSC
|
|
# sectors, when the division gradient is uniform
|
|
# @var Float Force floating point division so all sectors divide equally
|
|
# @var DistributeFront Divide the remainder equally (until running out,
|
|
# obviously) into the existing sectors, starting from the front
|
|
# @var DistributeBack Same as DistributeFront but starting from the back
|
|
class Remainder(Enum):
|
|
Float = 0
|
|
DistributeFront = 1
|
|
DistributeBack = 2
|
|
|
|
|
|
# @brief A helper class to contain a default value for a module configuration
|
|
# parameter
|
|
class Param(object):
|
|
# @var Required The value contained in this instance is irrelevant, and the
|
|
# value must be provided by the tuning configuration file.
|
|
# @var Optional If the value is not provided by the tuning configuration
|
|
# file, then the value contained in this instance will be used instead.
|
|
# @var Hardcode The value contained in this instance will be used
|
|
class Mode(Enum):
|
|
Required = 0
|
|
Optional = 1
|
|
Hardcode = 2
|
|
|
|
# @param name Name of the parameter. Shall match the name used in the
|
|
# configuration file for the parameter
|
|
# @param required Whether or not a value is required in the config
|
|
# parameter of get_value()
|
|
# @param val Default value (only relevant if mode is Optional)
|
|
def __init__(self, name: str, required: Mode, val=None):
|
|
self.name = name
|
|
self.__required = required
|
|
self.val = val
|
|
|
|
def get_value(self, config: dict):
|
|
if self.__required is self.Mode.Hardcode:
|
|
return self.val
|
|
|
|
if self.__required is self.Mode.Required and self.name not in config:
|
|
raise ValueError(f'Parameter {self.name} is required but not provided in the configuration')
|
|
|
|
return config[self.name] if self.required else self.val
|
|
|
|
@property
|
|
def required(self):
|
|
return self.__required is self.Mode.Required
|
|
|
|
# @brief Used by libtuning to auto-generate help information for the tuning
|
|
# script on the available parameters for the configuration file
|
|
# \todo Implement this
|
|
@property
|
|
def info(self):
|
|
raise NotImplementedError
|
|
|
|
|
|
class Tuner(object):
|
|
|
|
# External functions
|
|
|
|
def __init__(self, platform_name):
|
|
self.name = platform_name
|
|
self.modules = []
|
|
self.parser = None
|
|
self.generator = None
|
|
self.output_order = []
|
|
self.config = {}
|
|
self.output = {}
|
|
|
|
def add(self, module):
|
|
self.modules.append(module)
|
|
|
|
def set_input_parser(self, parser):
|
|
self.parser = parser
|
|
|
|
def set_output_formatter(self, output):
|
|
self.generator = output
|
|
|
|
def set_output_order(self, modules):
|
|
self.output_order = modules
|
|
|
|
# @brief Convert classes in self.output_order to the instances in self.modules
|
|
def _prepare_output_order(self):
|
|
output_order = self.output_order
|
|
self.output_order = []
|
|
for module_type in output_order:
|
|
modules = [module for module in self.modules if module.type == module_type.type]
|
|
if len(modules) > 1:
|
|
logger.error(f'Multiple modules found for module type "{module_type.type}"')
|
|
return False
|
|
if len(modules) < 1:
|
|
logger.error(f'No module found for module type "{module_type.type}"')
|
|
return False
|
|
self.output_order.append(modules[0])
|
|
|
|
return True
|
|
|
|
# \todo Validate parser and generator at Tuner construction time?
|
|
def _validate_settings(self):
|
|
if self.parser is None:
|
|
logger.error('Missing parser')
|
|
return False
|
|
|
|
if self.generator is None:
|
|
logger.error('Missing generator')
|
|
return False
|
|
|
|
if len(self.modules) == 0:
|
|
logger.error('No modules added')
|
|
return False
|
|
|
|
if len(self.output_order) != len(self.modules):
|
|
logger.error('Number of outputs does not match number of modules')
|
|
return False
|
|
|
|
return True
|
|
|
|
def _process_args(self, argv, platform_name):
|
|
parser = argparse.ArgumentParser(description=f'Camera Tuning for {platform_name}')
|
|
parser.add_argument('-i', '--input', type=str, required=True,
|
|
help='''Directory containing calibration images (required).
|
|
Images for ALSC must be named "alsc_{Color Temperature}k_1[u].dng",
|
|
and all other images must be named "{Color Temperature}k_{Lux Level}l.dng"''')
|
|
parser.add_argument('-o', '--output', type=str, required=True,
|
|
help='Output file (required)')
|
|
# It is not our duty to scan all modules to figure out their default
|
|
# options, so simply return an empty configuration if none is provided.
|
|
parser.add_argument('-c', '--config', type=str, default='',
|
|
help='Config file (optional)')
|
|
# \todo Check if we really need this or if stderr is good enough, or if
|
|
# we want a better logging infrastructure with log levels
|
|
parser.add_argument('-l', '--log', type=str, default=None,
|
|
help='Output log file (optional)')
|
|
return parser.parse_args(argv[1:])
|
|
|
|
def run(self, argv):
|
|
args = self._process_args(argv, self.name)
|
|
if args is None:
|
|
return -1
|
|
|
|
if not self._validate_settings():
|
|
return -1
|
|
|
|
if not self._prepare_output_order():
|
|
return -1
|
|
|
|
if len(args.config) > 0:
|
|
self.config, disable = self.parser.parse(args.config, self.modules)
|
|
else:
|
|
self.config = {'general': {}}
|
|
disable = []
|
|
|
|
# Remove disabled modules
|
|
for module in disable:
|
|
if module in self.modules:
|
|
self.modules.remove(module)
|
|
|
|
for module in self.modules:
|
|
if not module.validate_config(self.config):
|
|
logger.error(f'Config is invalid for module {module.type}')
|
|
return -1
|
|
|
|
has_lsc = any(isinstance(m, lt.modules.lsc.LSC) for m in self.modules)
|
|
# Only one LSC module allowed
|
|
has_only_lsc = has_lsc and len(self.modules) == 1
|
|
|
|
images = utils.load_images(args.input, self.config, not has_only_lsc, has_lsc)
|
|
if images is None or len(images) == 0:
|
|
logger.error(f'No images were found, or able to load')
|
|
return -1
|
|
|
|
# Do the tuning
|
|
for module in self.modules:
|
|
out = module.process(self.config, images, self.output)
|
|
if out is None:
|
|
logger.warning(f'Module {module.hr_name} failed to process...')
|
|
continue
|
|
self.output[module] = out
|
|
|
|
self.generator.write(args.output, self.output, self.output_order)
|
|
|
|
return 0
|