187 lines
5.5 KiB
Python
187 lines
5.5 KiB
Python
# SPDX-License-Identifier: BSD-2-Clause
|
|
#
|
|
# Copyright (C) 2019, Raspberry Pi Ltd
|
|
# Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
|
|
#
|
|
# Utilities for libtuning
|
|
|
|
import cv2
|
|
import decimal
|
|
import math
|
|
import numpy as np
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
import logging
|
|
|
|
import libtuning as lt
|
|
from libtuning.image import Image
|
|
from .macbeth import locate_macbeth
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Utility functions
|
|
|
|
|
|
def get_module_by_type_name(modules, name):
|
|
for module in modules:
|
|
if module.type == name:
|
|
return module
|
|
return None
|
|
|
|
|
|
# Private utility functions
|
|
|
|
|
|
def _list_image_files(directory):
|
|
d = Path(directory)
|
|
files = [d.joinpath(f) for f in os.listdir(d)
|
|
if re.search(r'\.(jp[e]g$)|(dng$)', f)]
|
|
files.sort()
|
|
return files
|
|
|
|
|
|
def _parse_image_filename(fn: Path):
|
|
lsc_only = False
|
|
color_temperature = None
|
|
lux = None
|
|
|
|
parts = fn.stem.split('_')
|
|
for part in parts:
|
|
if part == 'alsc':
|
|
lsc_only = True
|
|
continue
|
|
r = re.match(r'(\d+)[kK]', part)
|
|
if r:
|
|
color_temperature = int(r.group(1))
|
|
continue
|
|
r = re.match(r'(\d+)[lLuU]', part)
|
|
if r:
|
|
lux = int(r.group(1))
|
|
|
|
if color_temperature is None:
|
|
logger.error(f'The file name of "{fn.name}" does not contain a color temperature')
|
|
|
|
if lux is None and lsc_only is False:
|
|
logger.error(f'The file name of "{fn.name}" must either contain alsc or a lux level')
|
|
|
|
return color_temperature, lux, lsc_only
|
|
|
|
|
|
# \todo Implement this from check_imgs() in ctt.py
|
|
def _validate_images(images):
|
|
return True
|
|
|
|
|
|
# Public utility functions
|
|
|
|
|
|
# @brief Load images into a single list of Image instances
|
|
# @param input_dir Directory from which to load image files
|
|
# @param config Configuration dictionary
|
|
# @param load_nonlsc Whether or not to load non-lsc images
|
|
# @param load_lsc Whether or not to load lsc-only images
|
|
# @return A list of Image instances
|
|
def load_images(input_dir: str, config: dict, load_nonlsc: bool, load_lsc: bool) -> list:
|
|
files = _list_image_files(input_dir)
|
|
if len(files) == 0:
|
|
logger.error(f'No images found in {input_dir}')
|
|
return None
|
|
|
|
images = []
|
|
for f in files:
|
|
color, lux, lsc_only = _parse_image_filename(f)
|
|
|
|
if color is None:
|
|
logger.warning(f'Ignoring "{f.name}" as it has no associated color temperature')
|
|
continue
|
|
|
|
logger.info(f'Process image "{f.name}" (color={color}, lux={lux}, lsc_only={lsc_only})')
|
|
|
|
# Skip lsc image if we don't need it
|
|
if lsc_only and not load_lsc:
|
|
logger.warning(f'Skipping {f.name} as this tuner has no LSC module')
|
|
continue
|
|
|
|
# Skip non-lsc image if we don't need it
|
|
if not lsc_only and not load_nonlsc:
|
|
logger.warning(f'Skipping {f.name} as this tuner only has an LSC module')
|
|
continue
|
|
|
|
# Load image
|
|
try:
|
|
image = Image(f)
|
|
except Exception as e:
|
|
logger.error(f'Failed to load image {f.name}: {e}')
|
|
continue
|
|
|
|
# Populate simple fields
|
|
image.lsc_only = lsc_only
|
|
image.color = color
|
|
image.lux = lux
|
|
|
|
# Black level comes from the TIFF tags, but they are overridable by the
|
|
# config file.
|
|
if 'blacklevel' in config['general']:
|
|
image.blacklevel_16 = config['general']['blacklevel']
|
|
|
|
if lsc_only:
|
|
images.append(image)
|
|
continue
|
|
|
|
# Handle macbeth
|
|
macbeth = locate_macbeth(image, config)
|
|
if macbeth is None:
|
|
continue
|
|
|
|
images.append(image)
|
|
|
|
if not _validate_images(images):
|
|
return None
|
|
|
|
return images
|
|
|
|
|
|
|
|
"""
|
|
Some code that will save virtual macbeth charts that show the difference between optimised matrices and non optimised matrices
|
|
|
|
The function creates an image that is 1550 by 1050 pixels wide, and fills it with patches which are 200x200 pixels in size
|
|
Each patch contains the ideal color, the color from the original matrix, and the color from the final matrix
|
|
_________________
|
|
| |
|
|
| Ideal Color |
|
|
|_______________|
|
|
| Old | new |
|
|
| Color | Color |
|
|
|_______|_______|
|
|
|
|
Nice way of showing how the optimisation helps change the colors and the color matricies
|
|
"""
|
|
def visualise_macbeth_chart(macbeth_rgb, original_rgb, new_rgb, output_filename):
|
|
image = np.zeros((1050, 1550, 3), dtype=np.uint8)
|
|
colorindex = -1
|
|
for y in range(6):
|
|
for x in range(4): # Creates 6 x 4 grid of macbeth chart
|
|
colorindex += 1
|
|
xlocation = 50 + 250 * x # Means there is 50px of black gap between each square, more like the real macbeth chart.
|
|
ylocation = 50 + 250 * y
|
|
for g in range(200):
|
|
for i in range(100):
|
|
image[xlocation + i, ylocation + g] = macbeth_rgb[colorindex]
|
|
xlocation = 150 + 250 * x
|
|
ylocation = 50 + 250 * y
|
|
for i in range(100):
|
|
for g in range(100):
|
|
image[xlocation + i, ylocation + g] = original_rgb[colorindex] # Smaller squares below to compare the old colors with the new ones
|
|
xlocation = 150 + 250 * x
|
|
ylocation = 150 + 250 * y
|
|
for i in range(100):
|
|
for g in range(100):
|
|
image[xlocation + i, ylocation + g] = new_rgb[colorindex]
|
|
|
|
im_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
|
cv2.imwrite(f'{output_filename} Generated Macbeth Chart.png', im_bgr)
|
|
|