libtuning: Copy files from raspberrypi
Copy ctt_{awb,ccm,colors,ransac} from the raspberrypi tuning scripts as
basis for the libcamera implementation. color.py was renamed to
ctt_colors.py to better express the origin.
The files were taken from commit 66479605ba
("utils: raspberrypi: ctt:
Improve the Macbeth Chart search reliability").
Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>
Acked-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Acked-by: Paul Elder <paul.elder@ideasonboard.com>
Acked-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
This commit is contained in:
parent
6fb8f5cbf9
commit
9af5948cac
4 changed files with 883 additions and 0 deletions
376
utils/tuning/libtuning/ctt_awb.py
Normal file
376
utils/tuning/libtuning/ctt_awb.py
Normal file
|
@ -0,0 +1,376 @@
|
|||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# Copyright (C) 2019, Raspberry Pi Ltd
|
||||
#
|
||||
# camera tuning tool for AWB
|
||||
|
||||
from ctt_image_load import *
|
||||
import matplotlib.pyplot as plt
|
||||
from bisect import bisect_left
|
||||
from scipy.optimize import fmin
|
||||
|
||||
|
||||
"""
|
||||
obtain piecewise linear approximation for colour curve
|
||||
"""
|
||||
def awb(Cam, cal_cr_list, cal_cb_list, plot):
|
||||
imgs = Cam.imgs
|
||||
"""
|
||||
condense alsc calibration tables into one dictionary
|
||||
"""
|
||||
if cal_cr_list is None:
|
||||
colour_cals = None
|
||||
else:
|
||||
colour_cals = {}
|
||||
for cr, cb in zip(cal_cr_list, cal_cb_list):
|
||||
cr_tab = cr['table']
|
||||
cb_tab = cb['table']
|
||||
"""
|
||||
normalise tables so min value is 1
|
||||
"""
|
||||
cr_tab = cr_tab/np.min(cr_tab)
|
||||
cb_tab = cb_tab/np.min(cb_tab)
|
||||
colour_cals[cr['ct']] = [cr_tab, cb_tab]
|
||||
"""
|
||||
obtain data from greyscale macbeth patches
|
||||
"""
|
||||
rb_raw = []
|
||||
rbs_hat = []
|
||||
for Img in imgs:
|
||||
Cam.log += '\nProcessing '+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
|
||||
function will just return the greyscale patches
|
||||
"""
|
||||
r_patchs, b_patchs, g_patchs = get_alsc_patches(Img, colour_cals)
|
||||
"""
|
||||
calculate ratio of r, b to g
|
||||
"""
|
||||
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)
|
||||
"""
|
||||
The curve tends to be better behaved in so-called hatspace.
|
||||
R, B, G represent the individual channels. The colour curve is plotted in
|
||||
r, b space, where:
|
||||
r = R/G
|
||||
b = B/G
|
||||
This will be referred to as dehatspace... (sorry)
|
||||
Hatspace is defined as:
|
||||
r_hat = R/(R+B+G)
|
||||
b_hat = B/(R+B+G)
|
||||
To convert from dehatspace to hastpace (hat operation):
|
||||
r_hat = r/(1+r+b)
|
||||
b_hat = b/(1+r+b)
|
||||
To convert from hatspace to dehatspace (dehat operation):
|
||||
r = r_hat/(1-r_hat-b_hat)
|
||||
b = b_hat/(1-r_hat-b_hat)
|
||||
Proof is left as an excercise to the reader...
|
||||
Throughout the code, r and b are sometimes referred to as r_g and b_g
|
||||
as a reminder that they are ratios
|
||||
"""
|
||||
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))
|
||||
rb_raw.append((r_g, b_g))
|
||||
Cam.log += '\n'
|
||||
|
||||
Cam.log += '\nFinished processing images'
|
||||
"""
|
||||
sort all lits simultaneously by r_hat
|
||||
"""
|
||||
rbs_zip = list(zip(rbs_hat, rb_raw))
|
||||
rbs_zip.sort(key=lambda x: x[0][0])
|
||||
rbs_hat, rb_raw = list(zip(*rbs_zip))
|
||||
"""
|
||||
unzip tuples ready for processing
|
||||
"""
|
||||
rbs_hat = list(zip(*rbs_hat))
|
||||
rb_raw = list(zip(*rb_raw))
|
||||
"""
|
||||
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'
|
||||
"""
|
||||
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
|
||||
find the actual shortest distance in hatspace and use the projection back
|
||||
into dehatspace as an overestimate.
|
||||
The distance will be used for two things:
|
||||
1) In the case that colour temperature does not strictly decrease with
|
||||
increasing r/g, the closest point to the line will be chosen out of an
|
||||
increasing pair of colours.
|
||||
|
||||
2) To calculate transverse negative an dpositive, the maximum positive
|
||||
and negative distance from the line are chosen. This benefits from the
|
||||
overestimate as the transverse pos/neg are upper bound values.
|
||||
"""
|
||||
"""
|
||||
define fit function
|
||||
"""
|
||||
def f(x):
|
||||
return a*x**2 + b*x + c
|
||||
"""
|
||||
iterate over points (R, B are x and y coordinates of points) and calculate
|
||||
distance to line in dehatspace
|
||||
"""
|
||||
dists = []
|
||||
for i, (R, B) in enumerate(zip(rbs_hat[0], rbs_hat[1])):
|
||||
"""
|
||||
define function to minimise as square distance between datapoint and
|
||||
point on curve. Squaring is monotonic so minimising radius squared is
|
||||
equivalent to minimising radius
|
||||
"""
|
||||
def f_min(x):
|
||||
y = f(x)
|
||||
return((x-R)**2+(y-B)**2)
|
||||
"""
|
||||
perform optimisation with scipy.optmisie.fmin
|
||||
"""
|
||||
x_hat = fmin(f_min, R, disp=0)[0]
|
||||
y_hat = f(x_hat)
|
||||
"""
|
||||
dehat
|
||||
"""
|
||||
x = x_hat/(1-x_hat-y_hat)
|
||||
y = y_hat/(1-x_hat-y_hat)
|
||||
rr = R/(1-R-B)
|
||||
bb = B/(1-R-B)
|
||||
"""
|
||||
calculate euclidean distance in dehatspace
|
||||
"""
|
||||
dist = ((x-rr)**2+(y-bb)**2)**0.5
|
||||
"""
|
||||
return negative if point is below the fit curve
|
||||
"""
|
||||
if (x+y) > (rr+bb):
|
||||
dist *= -1
|
||||
dists.append(dist)
|
||||
Cam.log += '\nFound 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)
|
||||
"""
|
||||
set minimum transverse wiggles to 0.1 .
|
||||
Wiggle factors dictate how far off of the curve the algorithm searches. 0.1
|
||||
is a suitable minimum that gives better results for lighting conditions not
|
||||
within calibration dataset. Anything less will generalise poorly.
|
||||
"""
|
||||
if transverse_pos < 0.01:
|
||||
transverse_pos = 0.01
|
||||
Cam.log += '\nForced transverse pos to 0.01'
|
||||
if transverse_neg < 0.01:
|
||||
transverse_neg = 0.01
|
||||
Cam.log += '\nForced transverse neg to 0.01'
|
||||
|
||||
"""
|
||||
generate new b_hat values at each r_hat according to fit
|
||||
"""
|
||||
r_hat_fit = np.array(rbs_hat[0])
|
||||
b_hat_fit = a*r_hat_fit**2 + b*r_hat_fit + c
|
||||
"""
|
||||
transform from hatspace to dehatspace
|
||||
"""
|
||||
r_fit = r_hat_fit/(1-r_hat_fit-b_hat_fit)
|
||||
b_fit = b_hat_fit/(1-r_hat_fit-b_hat_fit)
|
||||
c_fit = np.round(rbs_hat[2], 0)
|
||||
"""
|
||||
round to 4dp
|
||||
"""
|
||||
r_fit = np.where((1000*r_fit) % 1 <= 0.05, r_fit+0.0001, r_fit)
|
||||
r_fit = np.where((1000*r_fit) % 1 >= 0.95, r_fit-0.0001, r_fit)
|
||||
b_fit = np.where((1000*b_fit) % 1 <= 0.05, b_fit+0.0001, b_fit)
|
||||
b_fit = np.where((1000*b_fit) % 1 >= 0.95, b_fit-0.0001, b_fit)
|
||||
r_fit = np.round(r_fit, 4)
|
||||
b_fit = np.round(b_fit, 4)
|
||||
"""
|
||||
The following code ensures that colour temperature decreases with
|
||||
increasing r/g
|
||||
"""
|
||||
"""
|
||||
iterate backwards over list for easier indexing
|
||||
"""
|
||||
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])
|
||||
"""
|
||||
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)
|
||||
"""
|
||||
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'
|
||||
"""
|
||||
delete bad point
|
||||
"""
|
||||
r_fit = np.delete(r_fit, bad)
|
||||
b_fit = np.delete(b_fit, bad)
|
||||
c_fit = np.delete(c_fit, bad).astype(np.uint16)
|
||||
"""
|
||||
note that if a point has been discarded then the length has decreased
|
||||
by one, meaning that decreasing the index by one will reassess the kept
|
||||
point against the next point. It is therefore possible, in theory, for
|
||||
two adjacent points to be discarded, although probably rare
|
||||
"""
|
||||
i -= 1
|
||||
|
||||
"""
|
||||
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:'
|
||||
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])
|
||||
|
||||
"""
|
||||
plotting code for debug
|
||||
"""
|
||||
if plot:
|
||||
x = np.linspace(np.min(rbs_hat[0]), np.max(rbs_hat[0]), 100)
|
||||
y = a*x**2 + b*x + c
|
||||
plt.subplot(2, 1, 1)
|
||||
plt.title('hatspace')
|
||||
plt.plot(rbs_hat[0], rbs_hat[1], ls='--', color='blue')
|
||||
plt.plot(x, y, color='green', ls='-')
|
||||
plt.scatter(rbs_hat[0], rbs_hat[1], color='red')
|
||||
for i, ct in enumerate(rbs_hat[2]):
|
||||
plt.annotate(str(ct), (rbs_hat[0][i], rbs_hat[1][i]))
|
||||
plt.xlabel('$\\hat{r}$')
|
||||
plt.ylabel('$\\hat{b}$')
|
||||
"""
|
||||
optional set axes equal to shortest distance so line really does
|
||||
looks perpendicular and everybody is happy
|
||||
"""
|
||||
# ax = plt.gca()
|
||||
# ax.set_aspect('equal')
|
||||
plt.grid()
|
||||
plt.subplot(2, 1, 2)
|
||||
plt.title('dehatspace - indoors?')
|
||||
plt.plot(r_fit, b_fit, color='blue')
|
||||
plt.scatter(rb_raw[0], rb_raw[1], color='green')
|
||||
plt.scatter(r_fit, b_fit, color='red')
|
||||
for i, ct in enumerate(c_fit):
|
||||
plt.annotate(str(ct), (r_fit[i], b_fit[i]))
|
||||
plt.xlabel('$r$')
|
||||
plt.ylabel('$b$')
|
||||
"""
|
||||
optional set axes equal to shortest distance so line really does
|
||||
looks perpendicular and everybody is happy
|
||||
"""
|
||||
# ax = plt.gca()
|
||||
# ax.set_aspect('equal')
|
||||
plt.subplots_adjust(hspace=0.5)
|
||||
plt.grid()
|
||||
plt.show()
|
||||
"""
|
||||
end of plotting code
|
||||
"""
|
||||
return(ct_curve, np.round(transverse_pos, 5), np.round(transverse_neg, 5))
|
||||
|
||||
|
||||
"""
|
||||
obtain greyscale patches and perform alsc colour correction
|
||||
"""
|
||||
def get_alsc_patches(Img, colour_cals, grey=True):
|
||||
"""
|
||||
get patch centre coordinates, image colour and the actual
|
||||
patches for each channel, remembering to subtract blacklevel
|
||||
If grey then only greyscale patches considered
|
||||
"""
|
||||
if grey:
|
||||
cen_coords = Img.cen_coords[3::4]
|
||||
col = Img.col
|
||||
patches = [np.array(Img.patches[i]) for i in Img.order]
|
||||
r_patchs = patches[0][3::4] - Img.blacklevel_16
|
||||
b_patchs = patches[3][3::4] - Img.blacklevel_16
|
||||
"""
|
||||
note two green channels are averages
|
||||
"""
|
||||
g_patchs = (patches[1][3::4]+patches[2][3::4])/2 - Img.blacklevel_16
|
||||
else:
|
||||
cen_coords = Img.cen_coords
|
||||
col = Img.col
|
||||
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
|
||||
|
||||
if colour_cals is None:
|
||||
return r_patchs, b_patchs, g_patchs
|
||||
"""
|
||||
find where image colour fits in alsc colour calibration tables
|
||||
"""
|
||||
cts = list(colour_cals.keys())
|
||||
pos = bisect_left(cts, col)
|
||||
"""
|
||||
if img colour is below minimum or above maximum alsc calibration colour, simply
|
||||
pick extreme closest to img colour
|
||||
"""
|
||||
if pos % len(cts) == 0:
|
||||
"""
|
||||
this works because -0 = 0 = first and -1 = last index
|
||||
"""
|
||||
col_tabs = np.array(colour_cals[cts[-pos//len(cts)]])
|
||||
"""
|
||||
else, perform linear interpolation between existing alsc colour
|
||||
calibration tables
|
||||
"""
|
||||
else:
|
||||
bef = cts[pos-1]
|
||||
aft = cts[pos]
|
||||
da = col-bef
|
||||
db = aft-col
|
||||
bef_tabs = np.array(colour_cals[bef])
|
||||
aft_tabs = np.array(colour_cals[aft])
|
||||
col_tabs = (bef_tabs*db + aft_tabs*da)/(da+db)
|
||||
col_tabs = np.reshape(col_tabs, (2, 12, 16))
|
||||
"""
|
||||
calculate dx, dy used to calculate alsc table
|
||||
"""
|
||||
w, h = Img.w/2, Img.h/2
|
||||
dx, dy = int(-(-(w-1)//16)), int(-(-(h-1)//12))
|
||||
"""
|
||||
make list of pairs of gains for each patch by selecting the correct value
|
||||
in alsc colour calibration table
|
||||
"""
|
||||
patch_gains = []
|
||||
for cen in cen_coords:
|
||||
x, y = cen[0]//dx, cen[1]//dy
|
||||
# We could probably do with some better spatial interpolation here?
|
||||
col_gains = (col_tabs[0][y][x], col_tabs[1][y][x])
|
||||
patch_gains.append(col_gains)
|
||||
|
||||
"""
|
||||
multiply the r and b channels in each patch by the respective gain, finally
|
||||
performing the alsc colour correction
|
||||
"""
|
||||
for i, gains in enumerate(patch_gains):
|
||||
r_patchs[i] = r_patchs[i] * gains[0]
|
||||
b_patchs[i] = b_patchs[i] * gains[1]
|
||||
|
||||
"""
|
||||
return greyscale patches, g channel and correct r, b channels
|
||||
"""
|
||||
return r_patchs, b_patchs, g_patchs
|
406
utils/tuning/libtuning/ctt_ccm.py
Normal file
406
utils/tuning/libtuning/ctt_ccm.py
Normal file
|
@ -0,0 +1,406 @@
|
|||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# Copyright (C) 2019, Raspberry Pi Ltd
|
||||
#
|
||||
# camera tuning tool for CCM (colour correction matrix)
|
||||
|
||||
from ctt_image_load import *
|
||||
from ctt_awb import get_alsc_patches
|
||||
import colors
|
||||
from scipy.optimize import minimize
|
||||
from ctt_visualise import visualise_macbeth_chart
|
||||
import numpy as np
|
||||
"""
|
||||
takes 8-bit macbeth chart values, degammas and returns 16 bit
|
||||
"""
|
||||
|
||||
'''
|
||||
This program has many options from which to derive the color matrix from.
|
||||
The first is average. This minimises the average delta E across all patches of
|
||||
the macbeth chart. Testing across all cameras yeilded this as the most color
|
||||
accurate and vivid. Other options are avalible however.
|
||||
Maximum minimises the maximum Delta E of the patches. It iterates through till
|
||||
a minimum maximum is found (so that there is
|
||||
not one patch that deviates wildly.)
|
||||
This yields generally good results but overall the colors are less accurate
|
||||
Have a fiddle with maximum and see what you think.
|
||||
The final option allows you to select the patches for which to average across.
|
||||
This means that you can bias certain patches, for instance if you want the
|
||||
reds to be more accurate.
|
||||
'''
|
||||
|
||||
matrix_selection_types = ["average", "maximum", "patches"]
|
||||
typenum = 0 # select from array above, 0 = average, 1 = maximum, 2 = patches
|
||||
test_patches = [1, 2, 5, 8, 9, 12, 14]
|
||||
|
||||
'''
|
||||
Enter patches to test for. Can also be entered twice if you
|
||||
would like twice as much bias on one patch.
|
||||
'''
|
||||
|
||||
|
||||
def degamma(x):
|
||||
x = x / ((2 ** 8) - 1) # takes 255 and scales it down to one
|
||||
x = np.where(x < 0.04045, x / 12.92, ((x + 0.055) / 1.055) ** 2.4)
|
||||
x = x * ((2 ** 16) - 1) # takes one and scales up to 65535, 16 bit color
|
||||
return x
|
||||
|
||||
|
||||
def gamma(x):
|
||||
# Take 3 long array of color values and gamma them
|
||||
return [((colour / 255) ** (1 / 2.4) * 1.055 - 0.055) * 255 for colour in x]
|
||||
|
||||
|
||||
"""
|
||||
FInds colour correction matrices for list of images
|
||||
"""
|
||||
|
||||
|
||||
def ccm(Cam, cal_cr_list, cal_cb_list):
|
||||
global matrix_selection_types, typenum
|
||||
imgs = Cam.imgs
|
||||
"""
|
||||
standard macbeth chart colour values
|
||||
"""
|
||||
m_rgb = np.array([ # these are in RGB
|
||||
[116, 81, 67], # dark skin
|
||||
[199, 147, 129], # light skin
|
||||
[91, 122, 156], # blue sky
|
||||
[90, 108, 64], # foliage
|
||||
[130, 128, 176], # blue flower
|
||||
[92, 190, 172], # bluish green
|
||||
[224, 124, 47], # orange
|
||||
[68, 91, 170], # purplish blue
|
||||
[198, 82, 97], # moderate red
|
||||
[94, 58, 106], # purple
|
||||
[159, 189, 63], # yellow green
|
||||
[230, 162, 39], # orange yellow
|
||||
[35, 63, 147], # blue
|
||||
[67, 149, 74], # green
|
||||
[180, 49, 57], # red
|
||||
[238, 198, 20], # yellow
|
||||
[193, 84, 151], # magenta
|
||||
[0, 136, 170], # cyan (goes out of gamut)
|
||||
[245, 245, 243], # white 9.5
|
||||
[200, 202, 202], # neutral 8
|
||||
[161, 163, 163], # neutral 6.5
|
||||
[121, 121, 122], # neutral 5
|
||||
[82, 84, 86], # neutral 3.5
|
||||
[49, 49, 51] # black 2
|
||||
])
|
||||
"""
|
||||
convert reference colours from srgb to rgb
|
||||
"""
|
||||
m_srgb = degamma(m_rgb) # now in 16 bit color.
|
||||
|
||||
# Produce array of LAB values for ideal color chart
|
||||
m_lab = [colors.RGB_to_LAB(color / 256) for color in m_srgb]
|
||||
|
||||
"""
|
||||
reorder reference values to match how patches are ordered
|
||||
"""
|
||||
m_srgb = np.array([m_srgb[i::6] for i in range(6)]).reshape((24, 3))
|
||||
m_lab = np.array([m_lab[i::6] for i in range(6)]).reshape((24, 3))
|
||||
m_rgb = np.array([m_rgb[i::6] for i in range(6)]).reshape((24, 3))
|
||||
"""
|
||||
reformat alsc correction tables or set colour_cals to None if alsc is
|
||||
deactivated
|
||||
"""
|
||||
if cal_cr_list is None:
|
||||
colour_cals = None
|
||||
else:
|
||||
colour_cals = {}
|
||||
for cr, cb in zip(cal_cr_list, cal_cb_list):
|
||||
cr_tab = cr['table']
|
||||
cb_tab = cb['table']
|
||||
"""
|
||||
normalise tables so min value is 1
|
||||
"""
|
||||
cr_tab = cr_tab / np.min(cr_tab)
|
||||
cb_tab = cb_tab / np.min(cb_tab)
|
||||
colour_cals[cr['ct']] = [cr_tab, cb_tab]
|
||||
|
||||
"""
|
||||
for each image, perform awb and alsc corrections.
|
||||
Then calculate the colour correction matrix for that image, recording the
|
||||
ccm and the colour tempertaure.
|
||||
"""
|
||||
ccm_tab = {}
|
||||
for Img in imgs:
|
||||
Cam.log += '\nProcessing image: ' + Img.name
|
||||
"""
|
||||
get macbeth patches with alsc applied if alsc enabled.
|
||||
Note: if alsc is disabled then colour_cals will be set to None and no
|
||||
the function will simply return the macbeth patches
|
||||
"""
|
||||
r, b, g = get_alsc_patches(Img, colour_cals, grey=False)
|
||||
# 256 values for each patch of sRGB values
|
||||
|
||||
"""
|
||||
do awb
|
||||
Note: awb is done by measuring the macbeth chart in the image, rather
|
||||
than from the awb calibration. This is done so the awb will be perfect
|
||||
and the ccm matrices will be more accurate.
|
||||
"""
|
||||
r_greys, b_greys, g_greys = r[3::4], b[3::4], g[3::4]
|
||||
r_g = np.mean(r_greys / g_greys)
|
||||
b_g = np.mean(b_greys / g_greys)
|
||||
r = r / r_g
|
||||
b = b / b_g
|
||||
"""
|
||||
normalise brightness wrt reference macbeth colours and then average
|
||||
each channel for each patch
|
||||
"""
|
||||
gain = np.mean(m_srgb) / np.mean((r, g, b))
|
||||
Cam.log += '\nGain with respect to standard colours: {:.3f}'.format(gain)
|
||||
r = np.mean(gain * r, axis=1)
|
||||
b = np.mean(gain * b, axis=1)
|
||||
g = np.mean(gain * g, axis=1)
|
||||
"""
|
||||
calculate ccm matrix
|
||||
"""
|
||||
# ==== All of below should in sRGB ===##
|
||||
sumde = 0
|
||||
ccm = do_ccm(r, g, b, m_srgb)
|
||||
# This is the initial guess that our optimisation code works with.
|
||||
original_ccm = ccm
|
||||
r1 = ccm[0]
|
||||
r2 = ccm[1]
|
||||
g1 = ccm[3]
|
||||
g2 = ccm[4]
|
||||
b1 = ccm[6]
|
||||
b2 = ccm[7]
|
||||
'''
|
||||
COLOR MATRIX LOOKS AS BELOW
|
||||
R1 R2 R3 Rval Outr
|
||||
G1 G2 G3 * Gval = G
|
||||
B1 B2 B3 Bval B
|
||||
Will be optimising 6 elements and working out the third element using 1-r1-r2 = r3
|
||||
'''
|
||||
|
||||
x0 = [r1, r2, g1, g2, b1, b2]
|
||||
'''
|
||||
We use our old CCM as the initial guess for the program to find the
|
||||
optimised matrix
|
||||
'''
|
||||
result = minimize(guess, x0, args=(r, g, b, m_lab), tol=0.01)
|
||||
'''
|
||||
This produces a color matrix which has the lowest delta E possible,
|
||||
based off the input data. Note it is impossible for this to reach
|
||||
zero since the input data is imperfect
|
||||
'''
|
||||
|
||||
Cam.log += ("\n \n Optimised Matrix Below: \n \n")
|
||||
[r1, r2, g1, g2, b1, b2] = result.x
|
||||
# The new, optimised color correction matrix values
|
||||
optimised_ccm = [r1, r2, (1 - r1 - r2), g1, g2, (1 - g1 - g2), b1, b2, (1 - b1 - b2)]
|
||||
|
||||
# This is the optimised Color Matrix (preserving greys by summing rows up to 1)
|
||||
Cam.log += str(optimised_ccm)
|
||||
Cam.log += "\n Old Color Correction Matrix Below \n"
|
||||
Cam.log += str(ccm)
|
||||
|
||||
formatted_ccm = np.array(original_ccm).reshape((3, 3))
|
||||
|
||||
'''
|
||||
below is a whole load of code that then applies the latest color
|
||||
matrix, and returns LAB values for color. This can then be used
|
||||
to calculate the final delta E
|
||||
'''
|
||||
optimised_ccm_rgb = [] # Original Color Corrected Matrix RGB / LAB
|
||||
optimised_ccm_lab = []
|
||||
|
||||
formatted_optimised_ccm = np.array(optimised_ccm).reshape((3, 3))
|
||||
after_gamma_rgb = []
|
||||
after_gamma_lab = []
|
||||
|
||||
for RGB in zip(r, g, b):
|
||||
ccm_applied_rgb = np.dot(formatted_ccm, (np.array(RGB) / 256))
|
||||
optimised_ccm_rgb.append(gamma(ccm_applied_rgb))
|
||||
optimised_ccm_lab.append(colors.RGB_to_LAB(ccm_applied_rgb))
|
||||
|
||||
optimised_ccm_applied_rgb = np.dot(formatted_optimised_ccm, np.array(RGB) / 256)
|
||||
after_gamma_rgb.append(gamma(optimised_ccm_applied_rgb))
|
||||
after_gamma_lab.append(colors.RGB_to_LAB(optimised_ccm_applied_rgb))
|
||||
'''
|
||||
Gamma After RGB / LAB - not used in calculations, only used for visualisation
|
||||
We now want to spit out some data that shows
|
||||
how the optimisation has improved the color matrices
|
||||
'''
|
||||
Cam.log += "Here are the Improvements"
|
||||
|
||||
# CALCULATE WORST CASE delta e
|
||||
old_worst_delta_e = 0
|
||||
before_average = transform_and_evaluate(formatted_ccm, r, g, b, m_lab)
|
||||
new_worst_delta_e = 0
|
||||
after_average = transform_and_evaluate(formatted_optimised_ccm, r, g, b, m_lab)
|
||||
for i in range(24):
|
||||
old_delta_e = deltae(optimised_ccm_lab[i], m_lab[i]) # Current Old Delta E
|
||||
new_delta_e = deltae(after_gamma_lab[i], m_lab[i]) # Current New Delta E
|
||||
if old_delta_e > old_worst_delta_e:
|
||||
old_worst_delta_e = old_delta_e
|
||||
if new_delta_e > new_worst_delta_e:
|
||||
new_worst_delta_e = new_delta_e
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
visualise_macbeth_chart(m_rgb, optimised_ccm_rgb, after_gamma_rgb, str(Img.col) + str(matrix_selection_types[typenum]))
|
||||
'''
|
||||
The program will also save some visualisations of improvements.
|
||||
Very pretty to look at. Top rectangle is ideal, Left square is
|
||||
before optimisation, right square is after.
|
||||
'''
|
||||
|
||||
"""
|
||||
if a ccm has already been calculated for that temperature then don't
|
||||
overwrite but save both. They will then be averaged later on
|
||||
""" # Now going to use optimised color matrix, optimised_ccm
|
||||
if Img.col in ccm_tab.keys():
|
||||
ccm_tab[Img.col].append(optimised_ccm)
|
||||
else:
|
||||
ccm_tab[Img.col] = [optimised_ccm]
|
||||
Cam.log += '\n'
|
||||
|
||||
Cam.log += '\nFinished processing images'
|
||||
"""
|
||||
average any ccms that share a colour temperature
|
||||
"""
|
||||
for k, v in ccm_tab.items():
|
||||
tab = np.mean(v, axis=0)
|
||||
tab = np.where((10000 * tab) % 1 <= 0.05, tab + 0.00001, tab)
|
||||
tab = np.where((10000 * tab) % 1 >= 0.95, tab - 0.00001, tab)
|
||||
ccm_tab[k] = list(np.round(tab, 5))
|
||||
Cam.log += '\nMatrix calculated for colour temperature of {} K'.format(k)
|
||||
|
||||
"""
|
||||
return all ccms with respective colour temperature in the correct format,
|
||||
sorted by their colour temperature
|
||||
"""
|
||||
sorted_ccms = sorted(ccm_tab.items(), key=lambda kv: kv[0])
|
||||
ccms = []
|
||||
for i in sorted_ccms:
|
||||
ccms.append({
|
||||
'ct': i[0],
|
||||
'ccm': i[1]
|
||||
})
|
||||
return ccms
|
||||
|
||||
|
||||
def guess(x0, r, g, b, m_lab): # provides a method of numerical feedback for the optimisation code
|
||||
[r1, r2, g1, g2, b1, b2] = x0
|
||||
ccm = np.array([r1, r2, (1 - r1 - r2),
|
||||
g1, g2, (1 - g1 - g2),
|
||||
b1, b2, (1 - b1 - b2)]).reshape((3, 3)) # format the matrix correctly
|
||||
return transform_and_evaluate(ccm, r, g, b, m_lab)
|
||||
|
||||
|
||||
def transform_and_evaluate(ccm, r, g, b, m_lab): # Transforms colors to LAB and applies the correction matrix
|
||||
# create list of matrix changed colors
|
||||
realrgb = []
|
||||
for RGB in zip(r, g, b):
|
||||
rgb_post_ccm = np.dot(ccm, np.array(RGB) / 256) # This is RGB values after the color correction matrix has been applied
|
||||
realrgb.append(colors.RGB_to_LAB(rgb_post_ccm))
|
||||
# now compare that with m_lab and return numeric result, averaged for each patch
|
||||
return (sumde(realrgb, m_lab) / 24) # returns an average result of delta E
|
||||
|
||||
|
||||
def sumde(listA, listB):
|
||||
global typenum, test_patches
|
||||
sumde = 0
|
||||
maxde = 0
|
||||
patchde = [] # Create array of the delta E values for each patch. useful for optimisation of certain patches
|
||||
for listA_item, listB_item in zip(listA, listB):
|
||||
if maxde < (deltae(listA_item, listB_item)):
|
||||
maxde = deltae(listA_item, listB_item)
|
||||
patchde.append(deltae(listA_item, listB_item))
|
||||
sumde += deltae(listA_item, listB_item)
|
||||
'''
|
||||
The different options specified at the start allow for
|
||||
the maximum to be returned, average or specific patches
|
||||
'''
|
||||
if typenum == 0:
|
||||
return sumde
|
||||
if typenum == 1:
|
||||
return maxde
|
||||
if typenum == 2:
|
||||
output = sum([patchde[test_patch] for test_patch in test_patches])
|
||||
# Selects only certain patches and returns the output for them
|
||||
return output
|
||||
|
||||
|
||||
"""
|
||||
calculates the ccm for an individual image.
|
||||
ccms are calculated in rgb space, and are fit by hand. Although it is a 3x3
|
||||
matrix, each row must add up to 1 in order to conserve greyness, simplifying
|
||||
calculation.
|
||||
The initial CCM is calculated in RGB, and then optimised in LAB color space
|
||||
This simplifies the initial calculation but then gets us the accuracy of
|
||||
using LAB color space.
|
||||
"""
|
||||
|
||||
|
||||
def do_ccm(r, g, b, m_srgb):
|
||||
rb = r-b
|
||||
gb = g-b
|
||||
rb_2s = (rb * rb)
|
||||
rb_gbs = (rb * gb)
|
||||
gb_2s = (gb * gb)
|
||||
|
||||
r_rbs = rb * (m_srgb[..., 0] - b)
|
||||
r_gbs = gb * (m_srgb[..., 0] - b)
|
||||
g_rbs = rb * (m_srgb[..., 1] - b)
|
||||
g_gbs = gb * (m_srgb[..., 1] - b)
|
||||
b_rbs = rb * (m_srgb[..., 2] - b)
|
||||
b_gbs = gb * (m_srgb[..., 2] - b)
|
||||
|
||||
"""
|
||||
Obtain least squares fit
|
||||
"""
|
||||
rb_2 = np.sum(rb_2s)
|
||||
gb_2 = np.sum(gb_2s)
|
||||
rb_gb = np.sum(rb_gbs)
|
||||
r_rb = np.sum(r_rbs)
|
||||
r_gb = np.sum(r_gbs)
|
||||
g_rb = np.sum(g_rbs)
|
||||
g_gb = np.sum(g_gbs)
|
||||
b_rb = np.sum(b_rbs)
|
||||
b_gb = np.sum(b_gbs)
|
||||
|
||||
det = rb_2 * gb_2 - rb_gb * rb_gb
|
||||
|
||||
"""
|
||||
Raise error if matrix is singular...
|
||||
This shouldn't really happen with real data but if it does just take new
|
||||
pictures and try again, not much else to be done unfortunately...
|
||||
"""
|
||||
if det < 0.001:
|
||||
raise ArithmeticError
|
||||
|
||||
r_a = (gb_2 * r_rb - rb_gb * r_gb) / det
|
||||
r_b = (rb_2 * r_gb - rb_gb * r_rb) / det
|
||||
"""
|
||||
Last row can be calculated by knowing the sum must be 1
|
||||
"""
|
||||
r_c = 1 - r_a - r_b
|
||||
|
||||
g_a = (gb_2 * g_rb - rb_gb * g_gb) / det
|
||||
g_b = (rb_2 * g_gb - rb_gb * g_rb) / det
|
||||
g_c = 1 - g_a - g_b
|
||||
|
||||
b_a = (gb_2 * b_rb - rb_gb * b_gb) / det
|
||||
b_b = (rb_2 * b_gb - rb_gb * b_rb) / det
|
||||
b_c = 1 - b_a - b_b
|
||||
|
||||
"""
|
||||
format ccm
|
||||
"""
|
||||
ccm = [r_a, r_b, r_c, g_a, g_b, g_c, b_a, b_b, b_c]
|
||||
|
||||
return ccm
|
||||
|
||||
|
||||
def deltae(colorA, colorB):
|
||||
return ((colorA[0] - colorB[0]) ** 2 + (colorA[1] - colorB[1]) ** 2 + (colorA[2] - colorB[2]) ** 2) ** 0.5
|
||||
# return ((colorA[1]-colorB[1]) * * 2 + (colorA[2]-colorB[2]) * * 2) * * 0.5
|
||||
# UNCOMMENT IF YOU WANT TO NEGLECT LUMINANCE FROM CALCULATION OF DELTA E
|
30
utils/tuning/libtuning/ctt_colors.py
Normal file
30
utils/tuning/libtuning/ctt_colors.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Program to convert from RGB to LAB color space
|
||||
def RGB_to_LAB(RGB): # where RGB is a 1x3 array. e.g RGB = [100, 255, 230]
|
||||
num = 0
|
||||
XYZ = [0, 0, 0]
|
||||
# converted all the three R, G, B to X, Y, Z
|
||||
X = RGB[0] * 0.4124 + RGB[1] * 0.3576 + RGB[2] * 0.1805
|
||||
Y = RGB[0] * 0.2126 + RGB[1] * 0.7152 + RGB[2] * 0.0722
|
||||
Z = RGB[0] * 0.0193 + RGB[1] * 0.1192 + RGB[2] * 0.9505
|
||||
|
||||
XYZ[0] = X / 255 * 100
|
||||
XYZ[1] = Y / 255 * 100 # XYZ Must be in range 0 -> 100, so scale down from 255
|
||||
XYZ[2] = Z / 255 * 100
|
||||
XYZ[0] = XYZ[0] / 95.047 # ref_X = 95.047 Observer= 2°, Illuminant= D65
|
||||
XYZ[1] = XYZ[1] / 100.0 # ref_Y = 100.000
|
||||
XYZ[2] = XYZ[2] / 108.883 # ref_Z = 108.883
|
||||
num = 0
|
||||
for value in XYZ:
|
||||
if value > 0.008856:
|
||||
value = value ** (0.3333333333333333)
|
||||
else:
|
||||
value = (7.787 * value) + (16 / 116)
|
||||
XYZ[num] = value
|
||||
num = num + 1
|
||||
|
||||
# L, A, B, values calculated below
|
||||
L = (116 * XYZ[1]) - 16
|
||||
a = 500 * (XYZ[0] - XYZ[1])
|
||||
b = 200 * (XYZ[1] - XYZ[2])
|
||||
|
||||
return [L, a, b]
|
71
utils/tuning/libtuning/ctt_ransac.py
Normal file
71
utils/tuning/libtuning/ctt_ransac.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# Copyright (C) 2019, Raspberry Pi Ltd
|
||||
#
|
||||
# camera tuning tool RANSAC selector for Macbeth chart locator
|
||||
|
||||
import numpy as np
|
||||
|
||||
scale = 2
|
||||
|
||||
|
||||
"""
|
||||
constructs normalised macbeth chart corners for ransac algorithm
|
||||
"""
|
||||
def get_square_verts(c_err=0.05, scale=scale):
|
||||
"""
|
||||
define macbeth chart corners
|
||||
"""
|
||||
b_bord_x, b_bord_y = scale*8.5, scale*13
|
||||
s_bord = 6*scale
|
||||
side = 41*scale
|
||||
x_max = side*6 + 5*s_bord + 2*b_bord_x
|
||||
y_max = side*4 + 3*s_bord + 2*b_bord_y
|
||||
c1 = (0, 0)
|
||||
c2 = (0, y_max)
|
||||
c3 = (x_max, y_max)
|
||||
c4 = (x_max, 0)
|
||||
mac_norm = np.array((c1, c2, c3, c4), np.float32)
|
||||
mac_norm = np.array([mac_norm])
|
||||
|
||||
square_verts = []
|
||||
square_0 = np.array(((0, 0), (0, side),
|
||||
(side, side), (side, 0)), np.float32)
|
||||
offset_0 = np.array((b_bord_x, b_bord_y), np.float32)
|
||||
c_off = side * c_err
|
||||
offset_cont = np.array(((c_off, c_off), (c_off, -c_off),
|
||||
(-c_off, -c_off), (-c_off, c_off)), np.float32)
|
||||
square_0 += offset_0
|
||||
square_0 += offset_cont
|
||||
"""
|
||||
define macbeth square corners
|
||||
"""
|
||||
for i in range(6):
|
||||
shift_i = np.array(((i*side, 0), (i*side, 0),
|
||||
(i*side, 0), (i*side, 0)), np.float32)
|
||||
shift_bord = np.array(((i*s_bord, 0), (i*s_bord, 0),
|
||||
(i*s_bord, 0), (i*s_bord, 0)), np.float32)
|
||||
square_i = square_0 + shift_i + shift_bord
|
||||
for j in range(4):
|
||||
shift_j = np.array(((0, j*side), (0, j*side),
|
||||
(0, j*side), (0, j*side)), np.float32)
|
||||
shift_bord = np.array(((0, j*s_bord),
|
||||
(0, j*s_bord), (0, j*s_bord),
|
||||
(0, j*s_bord)), np.float32)
|
||||
square_j = square_i + shift_j + shift_bord
|
||||
square_verts.append(square_j)
|
||||
# print('square_verts')
|
||||
# print(square_verts)
|
||||
return np.array(square_verts, np.float32), mac_norm
|
||||
|
||||
|
||||
def get_square_centres(c_err=0.05, scale=scale):
|
||||
"""
|
||||
define macbeth square centres
|
||||
"""
|
||||
verts, mac_norm = get_square_verts(c_err, scale=scale)
|
||||
|
||||
centres = np.mean(verts, axis=1)
|
||||
# print('centres')
|
||||
# print(centres)
|
||||
return np.array(centres, np.float32)
|
Loading…
Add table
Add a link
Reference in a new issue