Dear Johannes,

Am 31.07.2017 um 04:18 schrieb johannes hanika:
oh, nice, thanks for looking into this!

i can't parse your diff, it has so many white space changes. could you
maybe fix it or explain what it does? is it just a special case for
the zero-patch case?

yeah the solver sucks. unfortunately eigen is written in c++, so i'm
hesitant to pull it in as a dependency.

in the color look up module we have to solve a linear system of size (N+4)x(N+4) where N is the number of color patches. The problem is that for N<=4 the coefficient matrix A has rank N, not the full rank N+4. This means that the system has non-unique solutions. Thus we have to pick a reasonable solution among the infinite set of solutions for N<=4. I think the most reasonable solution is to set as many as possible coefficients to zero. For the remaining coefficients, one can derive a smaller full-rank linear system. This approach is numerically very stable and explained in more detail in the python prototype attached to this e-mail.

Assuming that all source colors are different, the (N+4)x(N+4) coefficient matrix has full rank and can be solved by various methods. LU-decomposition with partial pivoting but also numerical solution via singular value decomposition as implemented in the python script work quite well. I also tried various variants of Newton iteration, which often also gives accurate results. In the Newton iteration approach, one minimizes |A*x - b|, where x is the vector of unknowns and b the right-hand-side of the linear system. For the Newton iteration, one has to find an approximate solution to a linear system of the form 2*A^2 * y = c. I tried various methods for solving the latter equation.

The modified version of colochecker.c in my github repository deals only with the N=0 case. The diffs may show an unreasonable large change set because my editor has automatically removed white spaces at the end of lines.


        Heiko


--
-- Number Crunch Blog @ https://www.numbercrunch.de
--  Cluster Computing @ http://www.clustercomputing.de
--       Professional @ https://www.mpi-hd.mpg.de/personalhomes/bauke
--  Social Networking @ https://www.researchgate.net/profile/Heiko_Bauke

___________________________________________________________________________
darktable developer mailing list
to unsubscribe send a mail to [email protected]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# calculate interpolation coefficients as needed in
# the darktable color look up module 

# import math and plotting modules
import numpy as np
from pylab import *
from scipy.optimize import minimize, curve_fit
from scipy.linalg import *

# the source colors
S=array([37.99, 13.56,  14.06,  # dark skin
         65.71, 18.13,  17.81,  # light skin
         49.93, -4.88,  -21.93, # blue sky
         43.14, -13.10, 21.91,  # foliage
         55.11, 8.84,   -25.40, # blue flower
         70.72, -33.40, -0.20,  # bluish green
         62.66, 36.07,  57.10,  # orange
         40.02, 10.41,  -45.96, # purple red
         51.12, 48.24,  16.25,  # moderate red
         30.33, 22.98,  -21.59, # purple
         72.53, -23.71, 57.26,  # yellow green
         71.94, 19.36,  67.86,  # orange yellow
         28.78, 14.18,  -50.30, # blue
         55.26, -38.34, 31.37,  # green
         42.10, 53.38,  28.19,  # red
         81.73, 4.04,   79.82,  # yellow
         51.94, 49.99,  -14.57, # magenta
         51.04, -28.63, -28.64, # cyan
         96.54, -0.43,  1.19,   # white
         81.26, -0.64,  -0.34,  # neutral 8
         66.77, -0.73,  -0.50,  # neutral 65
         50.87, -0.15,  -0.27,  # neutral 5
         35.66, -0.42,  -1.23,  # neutral 35
         20.46, -0.08,  -0.97   # black
])
S=reshape(S, (24, 3))

channel=1 # select a channel, (0, 1, 2) for (L, a, b)
# reduce number of color mappings for testing
# try N>4 for the nummerically instable cases
N=5
q=array(range(0, 24))
np.random.shuffle(q)
S=S[q[0:N], :] # random selection of original colors

# construct coefficient matrix

# the radial basis function
def kernel(a, b):
    r2=dot(a-b, a-b)
    return r2*log(r2) if r2>1e-12 else 0

A=zeros((N+4, N+4))
for i in range(0, N):
    for j in range(0, N):
        A[i, j]=kernel(S[i, :], S[j, :])
    A[i, N+0]=A[N+0, i]=1
    A[i, N+1]=A[N+1, i]=S[i, 0]
    A[i, N+2]=A[N+2, i]=S[i, 1]
    A[i, N+3]=A[N+3, i]=S[i, 2]

T=zeros((N+4))
T[0:N]=S[:, channel] + 10*randn(N)  # target color channel, start from original color and modify

# quadratic cost function to be minimized (A must be symetric)
def N2(x):
    return norm(dot(A, x)-T)

# gradient of cost function
def dN2(x):
    return 2*dot(A, dot(A, x)-T)

# jacobian of cost function
def ddN2():
    return 2*dot(A, A)

# solve the linear system, treat special cases N=0, N=1, N=2, N=3, N=4
# extra, solve cases N>4 by different methods

# for N<=4 the linear system has rank N, i.e. less than the dimension
# of the coefficient matrix the linear system, which is N+4

# we have to pick a reasonable solution among the infinite set of solutions for N<=4
if N==0:
    # set all coefficents of the interpolating function to zero
    x=zeros((N+4))
    # set the coefficient of the linear function correponding to the target channel to one
    x[N+1+channel]=1
    print('residual: {0}\nsolution: {1}\n'.format(N2(x), x))
elif N==1:
    # set all coefficents of the interpolating function to zero
    x=zeros((N+4))
    # set the coefficient of the linear function correponding to the target channel to the ratio of target and source
    x[N+1+channel]=T[0]/S[0, channel]
    print('residual: {0}\nsolution: {1}\n'.format(N2(x), x))
elif N==2:
    # set all coefficents of the interpolating function to zero
    x=zeros((N+4))
    # determine the coefficient of the linear function correponding to the target channel and the coefficinet of the constant offset
    x[N]=(T[0]*S[1, channel]-T[1]*S[0, channel])/(S[1, channel]-S[0, channel])
    x[N+1+channel]=(T[1]-T[0])/(S[1, channel]-S[0, channel])
    print('residual: {0}\nsolution: {1}\n'.format(N2(x), x))
elif N==3:
    # set all coefficents of the interpolating function to zero
    x=zeros((N+4))
    # determine the coefficient of the linear function correponding to
    # the target channel, the coefficinet of the constant offset and
    # the coefficients of the linear functions correponding to the
    # other two color channels, the latter coefficinets are forced to
    # be equal to render the solution unique
    c1, c2={ 0, 1, 2 } - { channel }
    M=zeros((3, 3))
    M[0, 0]=1
    M[1, 0]=1
    M[2, 0]=1
    M[0, 1]=S[0, channel]
    M[1, 1]=S[1, channel]
    M[2, 1]=S[2, channel]
    M[0, 2]=S[0, c1]+S[0, c2]
    M[1, 2]=S[1, c1]+S[1, c2]
    M[2, 2]=S[2, c1]+S[2, c2]
    a=solve(M, T[0:3])
    x[3]=a[0]
    x[4+channel]=a[1]
    x[4+c1]=a[2]
    x[4+c2]=a[2]
    print('residual: {0}\nsolution: {1}\n'.format(N2(x), x))
elif N==4:
    # set all coefficents of the interpolating function to zero
    x=zeros((N+4))
    # determine the coefficient of the linear function correponding to
    # the target channel, the coefficinet of the constant offset and
    # the coefficients of the linear functions correponding to the
    # other two color channels, the latter coefficinets may be different
    M=zeros((4, 4))
    M[0, 0]=1
    M[1, 0]=1
    M[2, 0]=1
    M[3, 0]=1
    M[:, 1:]=S[:, :]
    a=solve(M, T[0:4])
    x[4:]=a
    print('residual: {0}\nsolution: {1}\n'.format(N2(x), x))
else:
    # solve directly
    x=zeros((N+4))
    try:
        print("Direct solution via LU decomposition and partial pivoting:")
        x=solve(A, T)
        print('residual: {0}\nsolution: {1}\n'.format(N2(x), x))
    except:
        print("Direct solution via LU cdecomposition failed.\n")
    X=x
    # solve by diagonalization of A
    # is surprisingly inaccurate
    x=zeros((N+4))
    try:
        print("Solution via diagonalization of A:")
        Lambda, U=eigh(A)
        x=dot(U, (1/Lambda)*dot(U.T, T))
        print('residual: {0}\nsolution: {1}\n'.format(N2(x), x))
    except:
        print("Solution via diagonalization of A failed.\n")
    X=vstack((X, x))
    # solve by sigular value decomposition of A
    x=zeros((N+4))
    try:
        print("Solution via sigular value decomposition of A:")
        u, s, v=svd(A, full_matrices=True)
        x=dot(v.T, (1/s)*dot(u.T, T))
        print('residual: {0}\nsolution: {1}\n'.format(N2(x), x))
    except:
        print("Solution via sigular value decomposition of A failed.\n")
    X=vstack((X, x))
    # solve by miminizing quadratic cost function via Newton iteration
    # solve 2*A^2 * y = b via LU factorization of 2*A^2 in each Newton iteration step
    # in rare cases, this method gives results that differ from those as obtained
    # by the other methods
    x=zeros((N+4))
    try:
        print("Solution via Newton iteration and LU factorization of 2*A^2:")
        a=polyfit(S[:, channel], T[0:N], 1)
        x[N]=a[1]
        x[N+1+channel]=a[0]
        x[N]=1
        x[N+1+channel]=0
        p_L_U=lu_factor(ddN2())
        for i in range(0, 128):
            if N2(x)<1e-9:
                break
            y=dN2(x)
            x-=lu_solve(p_L_U, y)
        print('residual: {0}\nsolution: {1}\n'.format(N2(x), x))
    except:
        print("Solution via Newton iteration and LU factorization of 2*A^2 failed.\n")
    X=vstack((X, x))
    # solve by miminizing quadratic cost function via Newton iteration
    # solve 2*A^2 * y = b via diagonalization of 2*A^2 in each Newton iteration step
    # in rare cases, this method gives results that differ from those as obtained
    # by the other methods
    x=zeros((N+4))
    try:
        print("Solution via Newton iteration and diagonalization 2*A^2:")
        a=polyfit(S[:, channel], T[0:N], 1)
        x[N]=a[1]
        x[N+1+channel]=a[0]
        x[N]=1
        x[N+1+channel]=0
        Lambda, U=eigh(ddN2())
        for i in range(0, 128):
            if N2(x)<1e-9:
                break
            y=dN2(x)
            x-=dot(U, (1/Lambda)*dot(U.T, y))
        print('residual: {0}\nsolution: {1}\n'.format(N2(x), x))
    except:
        print("Solution via Newton iteration and diagonalization 2*A^2 failed.\n")
    X=vstack((X, x))
    # solve by miminizing quadratic cost function via Newton iteration
    # solve 2*A^2 * y = b via singular value decomposition of 2*A^2 in each Newton iteration step
    x=zeros((N+4))
    try:
        print("Solution via Newton iteration and singular value decomposition 2*A^2:")
        a=polyfit(S[:, channel], T[0:N], 1)
        x[N]=a[1]
        x[N+1+channel]=a[0]
        x[N]=1
        x[N+1+channel]=0
        u, s, v=svd(ddN2(), full_matrices=True)
        for i in range(0, 128):
            if N2(x)<1e-9:
                break
            y=dN2(x)
            x-=dot(v.T, (1/s)*dot(u.T, y))
        print('residual: {0}\nsolution: {1}\n'.format(N2(x), x))
    except:
        print("Solution via Newton iteration and singular value decomposition 2*A^2 failed.\n")
    X=vstack((X, x))
    # solve by miminizing quadratic cost function via Newton iteration
    # solve 2*A^2 * y = b via QR factorization of 2*A^2 in each Newton iteration step
    x=zeros((N+4))
    try:
        print("Solution via Newton iteration and QR factorization of 2*A^2:")
        a=polyfit(S[:, channel], T[0:N], 1)
        x[N]=a[1]
        x[N+1+channel]=a[0]
        x[N]=1
        x[N+1+channel]=0
        q, r=qr(ddN2())
        for i in range(0, 128):
            if N2(x)<1e-9:
                break
            y=dN2(x)
            x-=solve_triangular(r, dot(q.T, y))
        print('residual: {0}\nsolution: {1}\n'.format(N2(x), x))
    except:
        print("Solution via Newton iteration and QR factorization of 2*A^2 failed.\n")
    X=vstack((X, x))

    # compare results as obtained by the differen methods
    for j in range(0, X.shape[0]):
        for i in range(0, j):
            print("{} {} {}".format(i, j, norm(X[i, :]-X[j, :])))

Reply via email to