You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
weston/tests/color-shaper-matrix-test.c

469 lines
12 KiB

/*
* Copyright 2021 Advanced Micro Devices, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice (including the
* next paragraph) shall be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "config.h"
#include <math.h>
#include "weston-test-client-helper.h"
#include "weston-test-fixture-compositor.h"
#include "color_util.h"
#include <string.h>
#include <lcms2.h>
#include <linux/limits.h>
struct lcms_pipeline {
/**
* Color space name
*/
const char *color_space;
/**
* Chromaticities for output profile
*/
cmsCIExyYTRIPLE prim_output;
/**
* tone curve enum
*/
enum transfer_fn pre_fn;
/**
* Transform matrix from sRGB to target chromaticities in prim_output
*/
struct lcmsMAT3 mat;
/**
* tone curve enum
*/
enum transfer_fn post_fn;
/**
* 2/255 or 3/255 maximum possible error, where 255 is 8 bit max value
*/
int tolerance;
};
static const int WINDOW_WIDTH = 256;
static const int WINDOW_HEIGHT = 24;
static cmsCIExyY wp_d65 = { 0.31271, 0.32902, 1.0 };
struct setup_args {
struct fixture_metadata meta;
struct lcms_pipeline pipeline;
};
/*
* Using currently destination gamut bigger than source.
* Using https://www.colour-science.org/ we can extract conversion matrix:
* import colour
* colour.matrix_RGB_to_RGB(colour.RGB_COLOURSPACES['sRGB'], colour.RGB_COLOURSPACES['Adobe RGB (1998)'], None)
* colour.matrix_RGB_to_RGB(colour.RGB_COLOURSPACES['sRGB'], colour.RGB_COLOURSPACES['ITU-R BT.2020'], None)
*/
const struct setup_args arr_setup[] = {
{
.meta.name = "sRGB->sRGB unity",
.pipeline = {
.color_space = "sRGB",
.prim_output = {
.Red = { 0.640, 0.330, 1.0 },
.Green = { 0.300, 0.600, 1.0 },
.Blue = { 0.150, 0.060, 1.0 }
},
.pre_fn = TRANSFER_FN_SRGB_EOTF,
.mat = LCMSMAT3(1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0),
.post_fn = TRANSFER_FN_SRGB_EOTF_INVERSE,
.tolerance = 0
}
},
{
.meta.name = "sRGB->adobeRGB",
.pipeline = {
.color_space = "adobeRGB",
.prim_output = {
.Red = { 0.640, 0.330, 1.0 },
.Green = { 0.210, 0.710, 1.0 },
.Blue = { 0.150, 0.060, 1.0 }
},
.pre_fn = TRANSFER_FN_SRGB_EOTF,
.mat = LCMSMAT3(0.715119, 0.284881, 0.0,
0.0, 1.0, 0.0,
0.0, 0.041169, 0.958831),
.post_fn = TRANSFER_FN_ADOBE_RGB_EOTF_INVERSE,
.tolerance = 1
/*
* Tolerance depends more on the 1D LUT used for the
* inv EOTF than the tested 3D LUT size:
* 9x9x9, 17x17x17, 33x33x33, 127x127x127
*/
}
},
{
.meta.name = "sRGB->bt2020",
.pipeline = {
.color_space = "bt2020",
.prim_output = {
.Red = { 0.708, 0.292, 1.0 },
.Green = { 0.170, 0.797, 1.0 },
.Blue = { 0.131, 0.046, 1.0 }
},
.pre_fn = TRANSFER_FN_SRGB_EOTF,
.mat = LCMSMAT3(0.627402, 0.329292, 0.043306,
0.069095, 0.919544, 0.011360,
0.016394, 0.088028, 0.895578),
/* this is equivalent to BT.1886 with zero black level */
.post_fn = TRANSFER_FN_POWER2_4_EOTF_INVERSE,
.tolerance = 5
/*
* TODO: when we add power-law in the curve enumeration
* in GL-renderer, then we should fix the tolerance
* as the error should reduce a lot.
*/
}
}
};
struct image_header {
int width;
int height;
int stride;
int depth;
pixman_format_code_t pix_format;
uint32_t *data;
};
static void
get_image_prop(struct buffer *buf, struct image_header *header)
{
header->width = pixman_image_get_width(buf->image);
header->height = pixman_image_get_height(buf->image);
header->stride = pixman_image_get_stride(buf->image);
header->depth = pixman_image_get_depth(buf->image);
header->pix_format = pixman_image_get_format (buf->image);
header->data = pixman_image_get_data(buf->image);
}
static void
gen_ramp_rgb(const struct image_header *header, int bitwidth, int width_bar)
{
static const int hue[][COLOR_CHAN_NUM] = {
{ 1, 1, 1 }, /* White */
{ 1, 1, 0 }, /* Yellow */
{ 0, 1, 1 }, /* Cyan */
{ 0, 1, 0 }, /* Green */
{ 1, 0, 1 }, /* Magenta */
{ 1, 0, 0 }, /* Red */
{ 0, 0, 1 }, /* Blue */
};
const int num_hues = ARRAY_LENGTH(hue);
float val_max;
int x, y;
int hue_index;
int chan;
float value;
unsigned char r, g, b;
uint32_t *pixel;
float n_steps = width_bar - 1;
val_max = (1 << bitwidth) - 1;
for (y = 0; y < header->height; y++) {
hue_index = (y * num_hues) / (header->height - 1);
hue_index = MIN(hue_index, num_hues - 1);
for (x = 0; x < header->width; x++) {
struct color_float rgb = { .rgb = { 0, 0, 0 } };
value = (float)x / (float)(header->width - 1);
if (width_bar > 1)
value = floor(value * n_steps) / n_steps;
for (chan = 0; chan < COLOR_CHAN_NUM; chan++) {
if (hue[hue_index][chan])
rgb.rgb[chan] = value;
}
sRGB_delinearize(&rgb);
r = round(rgb.r * val_max);
g = round(rgb.g * val_max);
b = round(rgb.b * val_max);
pixel = header->data + (y * header->stride / 4) + x;
*pixel = (255U << 24) | (r << 16) | (g << 8) | b;
}
}
}
static cmsHPROFILE
build_lcms_profile_output(const struct lcms_pipeline *pipeline)
{
cmsToneCurve *arr_curves[3];
cmsHPROFILE hRGB;
int type_inverse_tone_curve;
double inverse_tone_curve_param[5];
assert(find_tone_curve_type(pipeline->post_fn, &type_inverse_tone_curve,
inverse_tone_curve_param));
/*
* We are creating output profile and therefore we can use the following:
* calling semantics:
* cmsBuildParametricToneCurve(type_inverse_tone_curve, inverse_tone_curve_param)
* The function find_tone_curve_type sets the type of curve positive if it
* is tone curve and negative if it is inverse. When we create an ICC
* profile we should use a tone curve, the inversion is done by LCMS
* when the profile is used for output.
*/
arr_curves[0] = arr_curves[1] = arr_curves[2] =
cmsBuildParametricToneCurve(NULL,
(-1) * type_inverse_tone_curve,
inverse_tone_curve_param);
assert(arr_curves[0]);
hRGB = cmsCreateRGBProfileTHR(NULL, &wp_d65,
&pipeline->prim_output, arr_curves);
assert(hRGB);
cmsFreeToneCurve(arr_curves[0]);
return hRGB;
}
static char *
build_output_icc_profile(const struct lcms_pipeline *pipe)
{
char *profile_name = NULL;
cmsHPROFILE profile = NULL;
char *wd;
int ret;
bool saved;
wd = realpath(".", NULL);
assert(wd);
ret = asprintf(&profile_name, "%s/matrix-shaper-test-%s.icm", wd,
pipe->color_space);
assert(ret > 0);
profile = build_lcms_profile_output(pipe);
assert(profile);
saved = cmsSaveProfileToFile(profile, profile_name);
assert(saved);
cmsCloseProfile(profile);
return profile_name;
}
static enum test_result_code
fixture_setup(struct weston_test_harness *harness, const struct setup_args *arg)
{
struct compositor_setup setup;
char *file_name;
compositor_setup_defaults(&setup);
setup.renderer = RENDERER_GL;
setup.backend = WESTON_BACKEND_HEADLESS;
setup.width = WINDOW_WIDTH;
setup.height = WINDOW_HEIGHT;
setup.shell = SHELL_TEST_DESKTOP;
file_name = build_output_icc_profile(&arg->pipeline);
if (!file_name)
return RESULT_HARD_ERROR;
weston_ini_setup(&setup,
cfgln("[core]"),
cfgln("color-management=true"),
cfgln("[output]"),
cfgln("name=headless"),
cfgln("icc_profile=%s", file_name));
free(file_name);
return weston_test_harness_execute_as_client(harness, &setup);
}
DECLARE_FIXTURE_SETUP_WITH_ARG(fixture_setup, arr_setup, meta);
static bool
compare_float(float ref, float dst, int x, const char *chan,
float *max_diff, float max_allow_diff)
{
#if 0
/*
* This file can be loaded in Octave for visualization.
*
* S = load('compare_float_dump.txt');
*
* rvec = S(S(:,1)==114, 2:3);
* gvec = S(S(:,1)==103, 2:3);
* bvec = S(S(:,1)==98, 2:3);
*
* figure
* subplot(3, 1, 1);
* plot(rvec(:,1), rvec(:,2) .* 255, 'r');
* subplot(3, 1, 2);
* plot(gvec(:,1), gvec(:,2) .* 255, 'g');
* subplot(3, 1, 3);
* plot(bvec(:,1), bvec(:,2) .* 255, 'b');
*/
static FILE *fp = NULL;
if (!fp)
fp = fopen("compare_float_dump.txt", "w");
fprintf(fp, "%d %d %f\n", chan[0], x, dst - ref);
fflush(fp);
#endif
float diff = fabsf(ref - dst);
if (diff > *max_diff)
*max_diff = diff;
if (diff <= max_allow_diff)
return true;
testlog("x=%d %s: ref %f != dst %f, delta %f\n",
x, chan, ref, dst, dst - ref);
return false;
}
static bool
process_pipeline_comparison(const struct image_header *src,
const struct image_header *shot,
const struct setup_args * arg)
{
const char *const chan_name[COLOR_CHAN_NUM] = { "r", "g", "b" };
const float max_pixel_value = 255.0;
struct color_float max_diff_pipeline = { .rgb = { 0.0f, 0.0f, 0.0f } };
float max_allow_diff = arg->pipeline.tolerance / max_pixel_value;
float max_err = 0.0f;
bool ok = true;
uint32_t *row_ptr, *row_ptr_shot;
int y, x;
int chan;
struct color_float pix_src;
struct color_float pix_src_pipeline;
struct color_float pix_shot;
for (y = 0; y < src->height; y++) {
row_ptr = (uint32_t*)((uint8_t*)src->data + (src->stride * y));
row_ptr_shot = (uint32_t*)((uint8_t*)shot->data + (shot->stride * y));
for (x = 0; x < src->width; x++) {
pix_src = a8r8g8b8_to_float(row_ptr[x]);
pix_shot = a8r8g8b8_to_float(row_ptr_shot[x]);
/* do pipeline processing */
process_pixel_using_pipeline(arg->pipeline.pre_fn,
&arg->pipeline.mat,
arg->pipeline.post_fn,
&pix_src, &pix_src_pipeline);
/* check if pipeline matches to shader variant */
for (chan = 0; chan < COLOR_CHAN_NUM; chan++) {
ok &= compare_float(pix_src_pipeline.rgb[chan],
pix_shot.rgb[chan],
x, chan_name[chan],
&max_diff_pipeline.rgb[chan],
max_allow_diff);
}
}
}
for (chan = 0; chan < COLOR_CHAN_NUM; chan++)
max_err = MAX(max_err, max_diff_pipeline.rgb[chan]);
testlog("%s %s %s tol_req %d, tol_cal %f, max diff: r=%f, g=%f, b=%f\n",
__func__, ok == true? "SUCCESS":"FAILURE",
arg->meta.name, arg->pipeline.tolerance,
max_err * max_pixel_value,
max_diff_pipeline.r, max_diff_pipeline.g, max_diff_pipeline.b);
return ok;
}
static bool
check_process_pattern_ex(struct buffer *src, struct buffer *shot,
const struct setup_args * arg)
{
struct image_header header_src;
struct image_header header_shot;
bool ok;
get_image_prop(src, &header_src);
get_image_prop(shot, &header_shot);
/* no point to compare different images */
assert(header_src.width == header_shot.width);
assert(header_src.height == header_shot.height);
ok = process_pipeline_comparison(&header_src, &header_shot, arg);
return ok;
}
/*
* Test that matrix-shaper profile does CM correctly, it is used color ramp pattern
*/
TEST(shaper_matrix)
{
const int width = WINDOW_WIDTH;
const int height = WINDOW_HEIGHT;
const int bitwidth = 8;
const int width_bar = 32;
struct client *client;
struct buffer *buf;
struct buffer *shot;
struct wl_surface *surface;
struct image_header image;
bool match;
int seq_no = get_test_fixture_index();
client = create_client_and_test_surface(0, 0, width, height);
assert(client);
surface = client->surface->wl_surface;
buf = create_shm_buffer_a8r8g8b8(client, width, height);
get_image_prop(buf, &image);
gen_ramp_rgb(&image, bitwidth, width_bar);
wl_surface_attach(surface, buf->proxy, 0, 0);
wl_surface_damage(surface, 0, 0, width, height);
wl_surface_commit(surface);
shot = capture_screenshot_of_output(client);
assert(shot);
match = verify_image(shot, "shaper_matrix", seq_no, NULL, seq_no);
assert(check_process_pattern_ex(buf, shot, &arr_setup[seq_no]));
assert(match);
buffer_destroy(shot);
buffer_destroy(buf);
client_destroy(client);
}