:: Re: [Frei0r] [PATCH] Add "colgate",…
Top Page
Delete this message
Reply to this message
Author: Marko Cebokli
Date:  
To: Minimalistic plugin API for video effects
Subject: Re: [Frei0r] [PATCH] Add "colgate", a new color correction plugin.
Just a (theoretical) comment...

If we are talking about simple white balancing (=compensating for non-white
light on the recorded scene), any color space conversion is not just
unnecessary, but positively harmful!

Also, we DO NOT want to keep neither saturation nor luminance constant. With
saturation that is obvious: if a grey object has a color cast, it's saturation
is nonzero, but we want it to be zero after compensation. It is similar with
luminance: if we recorded a blue object in blue deficient light (like
tungsten), it will be rendered too dark, so we want our white point
compensation to lighten it up.

Let's look at the physics of this.

For the beginning let's assume there is no gamma correction, and the video is
linear to light.
The signal in each camera channel (R,G,B) will be proportional to the product
of illumination and reflectivity of the object. If we assume that white light
is (100,100,100), and we record with tungsten light, say (100,80,30), then the
signal in the G and B channels will simply be proportionally smaller, for all
colors by the same factor.
No interchannel crosstalk, that would require a "full matrix" transform to
compensate, is caused by non-white light. All we need to do, to simulate white
light, is a simple scaling (gain adjustment), done in each channel
independently. For the above example, pixel by pixel multiplying the RGB
channel values by (1.0, 1/0.8, 1/0.3) will do.

All that changes when we bring gamma correction into the picture is, that
instead of a simple scaling, we just need a more complex curve adjustment, but
still independently in each of the R,G and B channels. It is normally enough
to specify each curve at three points, hence "three point color balance".

One could also convert video to linear (de-gamma), apply the simple scaling
described above, and apply gamma again (re-gamma). The problem with this
method is, that the exact gamma used in the camera is often unknown.

As noted in the beginning, the above holds for removing eventual color casts
caused by the light source, "white balancing".

In general, creative "color grading" is a much wider subject than just white
point setting, and the author might want to change the colors in any possible
way, including introducing artificial color casts. In that case, of course,
any kind of color manipulation, done in any type of color space is fair game.

In the end, in the visual arts, the most important is the eye of the beholder.
So if the results of this plugin are more visually pleasing than the "correct"
solution, it should be an welcome addition.

Marko Cebokli



On Wednesday 12 September 2012 23:52:07 Steinar H. Gunderson wrote:
> Hi,
>
> This is a new white balancing filter; the rationale should be quite well
> explained in the commit message and the comments. I'm including the patch
> verbatim here for easier review.
>
> I intend to add Kdenlive support when/if the patch is accepted into Frei0r
> (the Kdenlive patch is simple).
>
> /* Steinar */
>
> commit fc992af6a2870a87fe5a9269da42982902459cf7
> Author: Steinar H. Gunderson <sgunderson@???>
> Date: Mon Sep 10 22:34:01 2012 +0200
>
>     Add "colgate", a new color correction plugin.

>
>     colgate works in the LMS color space, and generally seems to give much
>     better results then balanc0r (generally it corrects color casts much
>     more precisely, and without changing the overall saturation or
> brightness like balanc0r sometimes does).

>
>     I consider it to obsolete balanc0r, although balanc0r is kept for
>     compatibility reasons with older projects etc.

>
> diff --git a/src/Makefile.am b/src/Makefile.am
> index 42eea28..8bcf96d 100644
> --- a/src/Makefile.am
> +++ b/src/Makefile.am
> @@ -32,6 +32,7 @@ plugin_LTLIBRARIES = \
>      c0rners.la \
>      cartoon.la \
>      cluster.la \
> +    colgate.la \
>      coloradj_RGB.la \
>      colordistance.la \
>      color_only.la \
> @@ -165,6 +166,7 @@ bw0r_la_SOURCES = filter/bw0r/bw0r.c
>  c0rners_la_SOURCES = filter/c0rners/c0rners.c filter/c0rners/interp.h
>  cartoon_la_SOURCES = filter/cartoon/cartoon.cpp
>  cluster_la_SOURCES = filter/cluster/cluster.c
> +colgate_la_SOURCES = filter/colgate/colgate.c
>  coloradj_RGB_la_SOURCES = filter/coloradj/coloradj_RGB.c
>  colordistance_la_SOURCES = filter/colordistance/colordistance.c
>  contrast0r_la_SOURCES = filter/contrast0r/contrast0r.c
> diff --git a/src/filter/CMakeLists.txt b/src/filter/CMakeLists.txt
> index 55e59c2..2de4a6d 100644
> --- a/src/filter/CMakeLists.txt
> +++ b/src/filter/CMakeLists.txt
> @@ -19,6 +19,7 @@ add_subdirectory (brightness)
>  add_subdirectory (bw0r)
>  add_subdirectory (cartoon)
>  add_subdirectory (cluster)
> +add_subdirectory (colgate)
>  add_subdirectory (coloradj)
>  add_subdirectory (colordistance)
>  add_subdirectory (contrast0r)
> diff --git a/src/filter/colgate/CMakeLists.txt
> b/src/filter/colgate/CMakeLists.txt new file mode 100644
> index 0000000..f7b0183
> --- /dev/null
> +++ b/src/filter/colgate/CMakeLists.txt
> @@ -0,0 +1,12 @@
> +set (SOURCES colgate.c)
> +set (TARGET colgate)
> +
> +if (MSVC)
> +  set_source_files_properties (colgate.c PROPERTIES LANGUAGE CXX)
> +  set (SOURCES ${SOURCES} ${FREI0R_DEF})
> +endif (MSVC)
> +
> +add_library (${TARGET}  MODULE ${SOURCES})
> +set_target_properties (${TARGET} PROPERTIES PREFIX "")
> +
> +install (TARGETS ${TARGET} LIBRARY DESTINATION ${LIBDIR})
> diff --git a/src/filter/colgate/colgate.c b/src/filter/colgate/colgate.c
> new file mode 100644
> index 0000000..9291b55
> --- /dev/null
> +++ b/src/filter/colgate/colgate.c
> @@ -0,0 +1,493 @@
> +/* colgate.c
> + * Copyright (C) 2012 Steinar H. Gunderson <sgunderson@???>
> + *
> + * Color correction in LMS color space.
> + *
> + * This plugin is intended for the same uses as the balanc0r plugin,
> + * but differs from balanc0r in two important aspects:
> + *
> + *   1. It operates in the LMS color space, which is a better
> approximation + *      to the human color sensation than RGB is. This
> allows for more + *      natural color changes. (Note that this inevitably
> requires that we + *      we work in linear color space, not the nonlinear
> space pixels are + *      typically stored in.)
> + *
> + *   2. Its choice of neutral color is not limited by the Planckian locus
> + *      (typically expressed in Kelvins) like balanc0r is; you can choose
> + *      any color, even those that are not typically regarded as “white”.
> + *      If you want a color cast (e.g. a warmer scene), this can be
> + *      adjusted with a separate control for color temperature.
> + *
> + * Color is a very complex topic, and this plugin makes no claims to
> + * being 100% correct in any way; however, the results appear visually
> + * much more meaningful to me than the balanc0r plugin does, although it
> + * also uses significantly more CPU time.
> + *
> + * frei0r plugins are not given any meaningful information about input or
> + * output color space. We assume sRGB, since that typically matches
> people's + * viewing devices pretty well. sRGB in turn very closely
> matches ITU-R Rec. + * BT.709, the standard color space for HDTV; they
> share the same RGB + * primaries, and have a similar gamma (2.35 or 2.4,
> versus sRGB's curve that + * is approximately a gamma curve at 2.2). SDTV
> uses a different color space, + * ITU-R Rec. BT.601, which has somewhat
> different primaries and a gamma of + * 2.2, but in practice, sRGB should
> be an okay approximation for this as well. + *
> + * The color matrices used are typically from Wikipedia, and the inverses
> + * are computed by Octave if nothing else is mentioned.
> + *
> + *
> + * This program is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License as published by
> + * the Free Software Foundation; either version 2 of the License, or
> + * (at your option) any later version.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> + * GNU General Public License for more details.
> + *
> + * You should have received a copy of the GNU General Public License
> + * along with this program; if not, write to the Free Software
> + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
> + */
> +
> +/*
> + * We use fixed point, since conversion back and forth to floating-point
> is + * slow. (This also enables us to use LUTs in an efficient way for
> lookup + * to and from sRGB, as opposed to a pow()-based solution, which
> is very slow.) + *
> + * We need to think a bit about the range and precision of the different
> + * elements to get the best results here, since linear RGB can span quite
> + * some range. So:
> + *
> + *  - Input pixels (which are in linear RGB, always 0..1) are stored as
> 1.15. + *  - Matrix elements are s4.10.
> + *
> + * Matrix elements up to +/- 16 should give us some headroom; extreme
> color + * adjustments typically have elements of 5-6.
> + *
> + * Our standard operation is input_pixel * matrix_element, which becomes
> s5.25. + * We add three of them, which would seem to be able to overflow,
> but doesn't, + * since the largest pixel is 1.0 (represented as 2^15):
> + *
> + *   3 * 2^15 * (2^14 - 1) ~= 3 * 2^29 < 2^31.
> + *
> + * It _is_ larger than 2^30, though, so we don't have any bits to spare
> here. + */
> +#define INPUT_PIXEL_BITS 15
> +#define MATRIX_ELEMENT_FRAC_BITS 10
> +#define MATRIX_ELEMENT_BITS (4 + MATRIX_ELEMENT_FRAC_BITS)
> +
> +#include <stdlib.h>
> +#include <assert.h>
> +#include <math.h>
> +#include <stdio.h>
> +
> +#include "frei0r.h"
> +#include "frei0r_math.h"
> +
> +enum ParamIndex {
> +    NEUTRAL_COLOR,
> +    COLOR_TEMPERATURE,
> +};
> +
> +// Row major (opposite of OpenGL).
> +typedef float Matrix3x3[9];
> +
> +// Same, but with elements in s4.10 (see above).
> +typedef int Matrix3x3fp[9];
> +
> +typedef struct colgate_instance
> +{
> +    unsigned width;
> +    unsigned height;
> +    f0r_param_color_t neutral_color;
> +    double color_temperature;
> +    Matrix3x3fp corr_matrix;
> +} colgate_instance_t;
> +
> +// Assumes input value in [0..255]; output value is normalized.
> +static float convert_srgb_to_linear_rgb(float x)
> +{
> +    if (x < 255.0f * 0.04045f) {
> +        return x * (1.0f / (255.0f * 12.92f));
> +    } else {
> +        return pow((x + 255.0f * 0.055) * (1.0 / (255.0f * 1.055f)), 2.4);
> +    }
> +}
> +
> +// Assumes normalized input value; output value in [0..255].
> +static float convert_linear_rgb_to_srgb(float x)
> +{
> +    if (x < 0.0031308f) {
> +        return (255.0f * 12.92f) * x;
> +    } else {
> +        return ((255.0f * 1.055f) * pow(x, 1.0f / 2.4f)) - (0.055 * 255.0f);
> +    }
> +}
> +
> +/*
> + * For sRGB -> linear, we always have exact values when we look up,
> + * so we can do with a 8-bit LUT. For the other way, we need at least
> + * 13 bits to be able to distinguish all input values; we go for 14
> + * to get some extra accuracy. This results in an 16 kB LUT, but we
> + * generally don't need the L1 cache for a lot of other things anyway,
> + * so hopefully the LUT can mostly stay in L1 cache.
> + */
> +#define REVERSE_LUT_BITS 14
> +#define REVERSE_LUT_SIZE (1 << REVERSE_LUT_BITS)
> +
> +static int srgb_to_linear_rgb_lut[256];
> +static uint8_t linear_rgb_to_srgb_lut[REVERSE_LUT_SIZE];
> +
> +static void fill_srgb_luts()
> +{
> +    int i;
> +    for (i = 0; i < 256; ++i) {
> +        srgb_to_linear_rgb_lut[i] = lrintf(convert_srgb_to_linear_rgb(i) *
> (float)(1 << INPUT_PIXEL_BITS)); +    }
> +    for (i = 0; i < REVERSE_LUT_SIZE; ++i) {
> +        // Subtract 0.5 to compensate for the fact that we don't round
> +        // (which, for our purposes, would entail _adding_ 0.5) at lookup 

time.
> +        float x = (i - 0.5) / (float)(REVERSE_LUT_SIZE);
> +        int srgb = lrintf(convert_linear_rgb_to_srgb(x));
> +        assert(srgb >= 0 && srgb <= 255);
> +        linear_rgb_to_srgb_lut[i] = srgb;
> +    }
> +}
> +
> +// Multiply two 3x3 matrices.
> +static void multiply_3x3_matrices(const Matrix3x3 a, const Matrix3x3 b,
> Matrix3x3 result) +{
> +    result[0] = a[0] * b[0] + a[1] * b[3] + a[2] * b[6];
> +    result[3] = a[3] * b[0] + a[4] * b[3] + a[5] * b[6];
> +    result[6] = a[6] * b[0] + a[7] * b[3] + a[8] * b[6];
> +
> +    result[1] = a[0] * b[1] + a[1] * b[4] + a[2] * b[7];
> +    result[4] = a[3] * b[1] + a[4] * b[4] + a[5] * b[7];
> +    result[7] = a[6] * b[1] + a[7] * b[4] + a[8] * b[7];
> +
> +    result[2] = a[0] * b[2] + a[1] * b[5] + a[2] * b[8];
> +    result[5] = a[3] * b[2] + a[4] * b[5] + a[5] * b[8];
> +    result[8] = a[6] * b[2] + a[7] * b[5] + a[8] * b[8];
> +}
> +
> +// Multiply a 3x3 matrix with a three-element float vector.
> +static void multiply_3x3_matrix_float3(const Matrix3x3 M, float x0, float
> x1, float x2, float *y0, float *y1, float *y2) +{
> +    *y0 = M[0] * x0 + M[1] * x1 + M[2] * x2;
> +    *y1 = M[3] * x0 + M[4] * x1 + M[5] * x2;
> +    *y2 = M[6] * x0 + M[7] * x1 + M[8] * x2;
> +}
> +
> +// Same as above, but for fixed-point. Results come back unscaled.
> +static inline void multiply_3x3_matrix_fp3(const Matrix3x3fp M, int x0,
> int x1, int x2, int *y0, int *y1, int *y2) +{
> +    *y0 = M[0] * x0 + M[1] * x1 + M[2] * x2;
> +    *y1 = M[3] * x0 + M[4] * x1 + M[5] * x2;
> +    *y2 = M[6] * x0 + M[7] * x1 + M[8] * x2;
> +}
> +
> +// Convert a linear RGB value in s6.25 fixed-point to sRGB (between 0 to
> 255, inclusive). +static inline uint8_t convert_linear_rgb_to_srgb_fp(int
> x)
> +{
> +    if (x < 0) {
> +        return 0;
> +    }
> +    if (x >= (REVERSE_LUT_SIZE << (INPUT_PIXEL_BITS +
> MATRIX_ELEMENT_FRAC_BITS - REVERSE_LUT_BITS))) { +        return 255;
> +    }
> +    return linear_rgb_to_srgb_lut[((unsigned)x) >> (INPUT_PIXEL_BITS +
> MATRIX_ELEMENT_FRAC_BITS - REVERSE_LUT_BITS)]; +}
> +
> +// Temperature is in Kelvin. Formula from
> http://en.wikipedia.org/wiki/Planckian_locus#Approximation . +void
> convert_color_temperature_to_xyz(float T, float *x, float *y, float *z) +{
> +    double invT = 1.0 / T;
> +    double xc, yc;
> +
> +    if (T <= 4000.0f) {
> +        xc = ((-0.2661239e9 * invT - 0.2343580e6) * invT + 0.8776956e3) * 

invT +
> 0.179910; +    } else {
> +        xc = ((-3.0258469e9 * invT + 2.1070379e6) * invT + 0.2226347e3) * 

invT +
> 0.240390; +    }
> +
> +    if (T <= 2222.0f) {
> +        yc = ((-1.1063814 * xc - 1.34811020) * xc + 2.18555832) * xc -
> 0.20219683; +    } else if (T <= 4000.0f) {
> +        yc = ((-0.9549476 * xc - 1.37418593) * xc + 2.09137015) * xc -
> 0.16748867; +    } else {
> +        yc = (( 3.0817580 * xc - 5.87338670) * xc + 3.75112997) * xc -
> 0.37001483; +    }
> +
> +    *x = xc;
> +    *y = yc;
> +    *z = 1.0 - xc - yc;
> +}
> +
> +// sRGB primaries.
> +static const Matrix3x3 rgb_to_xyz_matrix = {
> +    0.4124, 0.3576, 0.1805,
> +    0.2126, 0.7152, 0.0722,
> +    0.0193, 0.1192, 0.9505,
> +};
> +static const Matrix3x3 xyz_to_rgb_matrix = {
> +     3.240625, -1.537208, -0.498629,
> +    -0.968931,  1.875756,  0.041518,
> +     0.055710, -0.204021,  1.056996,
> +};
> +
> +static void convert_linear_rgb_to_linear_xyz(float r, float g, float b,
> float *x, float *y, float *z) +{
> +    multiply_3x3_matrix_float3(rgb_to_xyz_matrix, r, g, b, x, y, z);
> +}
> +
> +static void convert_linear_xyz_to_linear_rgb(float x, float y, float z,
> float *r, float *g, float *b) +{
> +    multiply_3x3_matrix_float3(xyz_to_rgb_matrix, x, y, z, r, g, b);
> +}
> +
> +/*
> + * There are several different LMS spaces, at least according to
> Wikipedia. + * Through practical testing, I've found most of them (like
> the CIECAM02 model) + * to yield a result that is too reddish in practice.
> This is the RLAB space, + * normalized to D65, which means that the
> standard D65 illuminant + * (x=0.31271, y=0.32902, z=1-y-x) gives L=M=S
> under this transformation. + * This makes sense because sRGB (which is
> used to derive those XYZ values + * in the first place) assumes the D65
> illuminant, and so the D65 illuminant + * also gives R=G=B in sRGB.
> + */
> +static const Matrix3x3 xyz_to_lms_matrix = {
> +     0.4002, 0.7076, -0.0808,
> +    -0.2263, 1.1653,  0.0457,
> +        0.0,    0.0,  0.9182,
> +};
> +static const Matrix3x3 lms_to_xyz_matrix = {
> +    1.86007, -1.12948,  0.21990,
> +    0.36122,  0.63880, -0.00001,
> +    0.00000,  0.00000,  1.08909,
> +};
> +
> +static void convert_linear_xyz_to_linear_lms(float x, float y, float z,
> float *l, float *m, float *s) +{
> +    multiply_3x3_matrix_float3(xyz_to_lms_matrix, x, y, z, l, m, s);
> +}
> +
> +static void convert_linear_lms_to_linear_xyz(float l, float m, float s,
> float *x, float *y, float *z) +{
> +    multiply_3x3_matrix_float3(lms_to_xyz_matrix, l, m, s, x, y, z);
> +}
> +
> +/*
> + * For a given reference color (given in XYZ space),
> + * compute scaling factors for L, M and S. What we want at the output is
> equal L, M and S + * for the reference color (making it a neutral
> illuminant), or sL ref_L = sM ref_M = sS ref_S. + * This removes two
> degrees of freedom for our system, and we only need to find fL. + *
> + * A reasonable last constraint would be to preserve Y, approximately the
> brightness, + * for the reference color. Since L'=M'=S' and the Y row of
> the LMS-to-XYZ matrix + * sums to unity, we know that Y'=L', and it's easy
> to find the fL that sets Y'=Y. + */
> +static void compute_lms_scaling_factors(float x, float y, float z, float
> *scale_l, float *scale_m, float *scale_s) +{
> +    float l, m, s;
> +    convert_linear_xyz_to_linear_lms(x, y, z, &l, &m, &s);
> +
> +    *scale_l = y / l;
> +    *scale_m = *scale_l * (l / m);
> +    *scale_s = *scale_l * (l / s);
> +}
> +
> +static void compute_correction_matrix(colgate_instance_t *o)
> +{
> +    int i;
> +
> +    /*
> +     * Find out what the given neutral color would be in LMS space,
> +     * and use that value to build a correction factor for each component
> +     * so that the neutral color really becomes gray (in LMS).
> +     */
> +    float ref_r = o->neutral_color.r * 255.0f;
> +    float ref_g = o->neutral_color.g * 255.0f;
> +    float ref_b = o->neutral_color.b * 255.0f;
> +
> +    float linear_r = convert_srgb_to_linear_rgb(ref_r);
> +    float linear_g = convert_srgb_to_linear_rgb(ref_g);
> +    float linear_b = convert_srgb_to_linear_rgb(ref_b);
> +
> +    float x, y, z;
> +    convert_linear_rgb_to_linear_xyz(linear_r, linear_g, linear_b, &x, &y,
> &z); +
> +    float l, m, s;
> +    convert_linear_xyz_to_linear_lms(x, y, z, &l, &m, &s);
> +
> +    float l_scale, m_scale, s_scale;
> +    compute_lms_scaling_factors(x, y, z, &l_scale, &m_scale, &s_scale);
> +
> +    /*
> +     * Now apply the color balance. Simply put, we find the chromacity point
> +     * for the desired white temperature, see what LMS scaling factors they
> +     * would have given us, and then reverse that transform. For T=6500K,
> +     * the default, this gives us nearly an identity transform (but only
> nearly, +     * since the D65 illuminant does not exactly match the results
> of T=6500K); +     * we normalize so that T=6500K really is a no-op.
> +     */
> +    float white_x, white_y, white_z, l_scale_white, m_scale_white,
> s_scale_white; +    convert_color_temperature_to_xyz(o->color_temperature,
> &white_x, &white_y, &white_z); +    compute_lms_scaling_factors(white_x,
> white_y, white_z, &l_scale_white, &m_scale_white, &s_scale_white); +
> +    float ref_x, ref_y, ref_z, l_scale_ref, m_scale_ref, s_scale_ref;
> +    convert_color_temperature_to_xyz(6500.0f, &ref_x, &ref_y, &ref_z);
> +    compute_lms_scaling_factors(ref_x, ref_y, ref_z, &l_scale_ref,
> &m_scale_ref, &s_scale_ref); +
> +    l_scale *= l_scale_ref / l_scale_white;
> +    m_scale *= m_scale_ref / m_scale_white;
> +    s_scale *= s_scale_ref / s_scale_white;
> +
> +    /*
> +     * Concatenate all the different linear operations into a single 3x3
> matrix, +     * which is then converted to fixed point and stored. Note 

that
> since we +     * postmultiply our vectors, the order of the matrices has to
> be the opposite +     * of the execution order.
> +     */
> +    Matrix3x3 temp, temp2, corr_matrix;
> +    Matrix3x3 lms_scale_matrix = {
> +        l_scale,    0.0f,    0.0f,
> +           0.0f, m_scale,    0.0f,
> +           0.0f,    0.0f, s_scale,
> +    };
> +    multiply_3x3_matrices(xyz_to_rgb_matrix, lms_to_xyz_matrix, temp);
> +    multiply_3x3_matrices(temp, lms_scale_matrix, temp2);
> +    multiply_3x3_matrices(temp2, xyz_to_lms_matrix, temp);
> +    multiply_3x3_matrices(temp, rgb_to_xyz_matrix, corr_matrix);
> +
> +    for (i = 0; i < 9; ++i) {
> +        o->corr_matrix[i] = lrintf(corr_matrix[i] * (float)(1 <<
> MATRIX_ELEMENT_FRAC_BITS)); +        if (o->corr_matrix[i] < -(1 <<
> MATRIX_ELEMENT_BITS)) {
> +            o->corr_matrix[i] = -(1 << MATRIX_ELEMENT_BITS);
> +        }
> +        if (o->corr_matrix[i] > (1 << MATRIX_ELEMENT_BITS) - 1) {
> +            o->corr_matrix[i] = (1 << MATRIX_ELEMENT_BITS) - 1;
> +        }
> +    }
> +}
> +
> +int f0r_init()
> +{
> +    fill_srgb_luts();
> +    return 1;
> +}
> +
> +void f0r_deinit()
> +{
> +}
> +
> +void f0r_get_plugin_info(f0r_plugin_info_t *colordistance_info)
> +{
> +    colordistance_info->name = "White Balance (LMS space)";
> +    colordistance_info->author = "Steinar H. Gunderson";
> +    colordistance_info->plugin_type = F0R_PLUGIN_TYPE_FILTER;
> +    colordistance_info->color_model = F0R_COLOR_MODEL_RGBA8888;
> +    colordistance_info->frei0r_version = FREI0R_MAJOR_VERSION;
> +    colordistance_info->major_version = 0;
> +    colordistance_info->minor_version = 1;
> +    colordistance_info->num_params = 2;
> +    colordistance_info->explanation = "Do simple color correction, in a
> physically meaningful way"; +}
> +
> +void f0r_get_param_info(f0r_param_info_t *info, int param_index)
> +{
> +    switch (param_index) {
> +    case NEUTRAL_COLOR:
> +        info->name = "Neutral Color";
> +        info->type = F0R_PARAM_COLOR;
> +        info->explanation = "Choose a color from the source image that 

should be
> white."; +        break;
> +
> +    case COLOR_TEMPERATURE:
> +        info->name = "Color Temperature";
> +        info->type = F0R_PARAM_DOUBLE;
> +        info->explanation = "Choose an output color temperature, if 

different
> from 6500 K."; +        break;
> +    }
> +
> +}
> +
> +f0r_instance_t f0r_construct(unsigned width, unsigned height)
> +{
> +    colgate_instance_t *inst = (colgate_instance_t *)calloc(1,
> sizeof(*inst)); +    inst->width = width;
> +    inst->height = height;
> +    inst->neutral_color.r = 0.5;
> +    inst->neutral_color.g = 0.5;
> +    inst->neutral_color.b = 0.5;
> +    inst->color_temperature = 6500.0;
> +    compute_correction_matrix(inst);
> +    return (f0r_instance_t)inst;
> +}
> +
> +void f0r_destruct(f0r_instance_t instance)
> +{
> +    free(instance);
> +}
> +
> +void f0r_set_param_value(f0r_instance_t instance, f0r_param_t param, int
> param_index) +{
> +    assert(instance);
> +    colgate_instance_t *inst = (colgate_instance_t *)instance;
> +
> +    switch (param_index) {
> +    case NEUTRAL_COLOR:
> +        inst->neutral_color = *((f0r_param_color_t *)param);
> +        compute_correction_matrix(inst);
> +        break;
> +
> +    case COLOR_TEMPERATURE:
> +        inst->color_temperature = *((double *)param);
> +        if (inst->color_temperature < 1000.0 || inst->color_temperature >
> 15000.0) { +            inst->color_temperature = 6500.0;
> +        }
> +        compute_correction_matrix(inst);
> +        break;
> +    }
> +}
> +
> +void f0r_get_param_value(f0r_instance_t instance, f0r_param_t param, int
> param_index) +{
> +    assert(instance);
> +    colgate_instance_t *inst = (colgate_instance_t*)instance;
> +
> +    switch (param_index) {
> +    case NEUTRAL_COLOR:
> +        *((f0r_param_color_t *)param) = inst->neutral_color;
> +        break;
> +
> +    case COLOR_TEMPERATURE:
> +        *((double *)param) = inst->color_temperature;
> +        break;
> +    }
> +}
> +
> +void f0r_update(f0r_instance_t instance, double time, const uint32_t
> *inframe, uint32_t *outframe) +{
> +    assert(instance);
> +    colgate_instance_t *inst = (colgate_instance_t *)instance;
> +    unsigned len = inst->width * inst->height;
> +    unsigned char *dst = (unsigned char *)outframe;
> +    const unsigned char *src = (unsigned char *)inframe;
> +    unsigned i;
> +
> +    for (i = 0; i < len; ++i) {
> +        int r = srgb_to_linear_rgb_lut[*src++];
> +        int g = srgb_to_linear_rgb_lut[*src++];
> +        int b = srgb_to_linear_rgb_lut[*src++];
> +
> +        int new_r, new_g, new_b;
> +        multiply_3x3_matrix_fp3(inst->corr_matrix, r, g, b, &new_r, &new_g,
> &new_b); +
> +        *dst++ = convert_linear_rgb_to_srgb_fp(new_r);
> +        *dst++ = convert_linear_rgb_to_srgb_fp(new_g);
> +        *dst++ = convert_linear_rgb_to_srgb_fp(new_b);
> +        *dst++ = *src++;  // Copy alpha.
> +    }
> +}
> diff --git a/src/filter/coloradj/readme b/src/filter/coloradj/readme
> index 76f80f5..d60dd80 100755
> --- a/src/filter/coloradj/readme
> +++ b/src/filter/coloradj/readme
> @@ -1,7 +1,7 @@
>  coloradj*

>
> These plugins are for manual color adjustment.
> -For (semi)automatic color correction, use "balanc0r" and/or
> +For (semi)automatic color correction, use "balanc0r", "colgate" and/or
> "three_point_balance" plugins.