weston/tests/color-lcms-optimizer-test.c

469 lines
12 KiB
C

/*
* Copyright 2023 Collabora, Ltd.
*
* 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 <lcms2_plugin.h>
#include "weston-test-client-helper.h"
#include "libweston/color-lcms/color-lcms.h"
#include "libweston/color-lcms/color-curve-segments.h"
#define N_CHANNELS 3
#define PRECISION 1e-4
struct pipeline_context {
cmsContext context_id;
cmsPipeline *pipeline;
};
struct curve {
cmsInt32Number type;
cmsFloat64Number params[10];
};
/* Parametric power-law curve with nothing special. */
const struct curve power_law_curve_A = {
.type = 1,
.params = {2.0},
};
/* Inverse power-law curve with another gamma value. */
const struct curve power_law_curve_B = {
.type = -1,
.params = {8.0},
};
/* Parametric type 4 comes from IEC 61966-2.1 (sRGB). */
const struct curve srgb_curve = {
.type = 4,
.params = {2.4, 1.0/1.055, 0.055/1.055, 1.0/12.92, 0.04045},
};
/* Identity matrix. */
const cmsFloat64Number identity_matrix[N_CHANNELS * N_CHANNELS] = {
1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0,
};
/* Matrix with no special characteristics. */
const cmsFloat64Number regular_matrix[N_CHANNELS * N_CHANNELS] = {
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0,
};
/* Inverse matrices (matrix_A * matrix_A_inverse == identity). */
const cmsFloat64Number matrix_A[N_CHANNELS * N_CHANNELS] = {
0.218024, 0.192559, 0.071524,
0.111244, 0.358458, 0.030306,
0.006960, 0.048534, 0.356962,
};
const cmsFloat64Number matrix_A_inverse[N_CHANNELS * N_CHANNELS] = {
6.268277, -3.234369, -0.981373,
-1.957467, 3.832202, 0.066866,
0.143926, -0.457981, 2.811465,
};
static struct pipeline_context
pipeline_context_new(void)
{
struct pipeline_context ret;
ret.context_id = cmsCreateContext(NULL, NULL);
assert(ret.context_id);
ret.pipeline = cmsPipelineAlloc(ret.context_id, N_CHANNELS, N_CHANNELS);
assert(ret.pipeline);
return ret;
}
static void
pipeline_context_release(struct pipeline_context *pc)
{
cmsPipelineFree(pc->pipeline);
cmsDeleteContext(pc->context_id);
}
static void
add_curve(struct pipeline_context *pc, cmsInt32Number type,
const cmsFloat64Number params[])
{
cmsToneCurve *curveset[N_CHANNELS];
cmsToneCurve *curve;
cmsStage *stage;
unsigned int i;
curve = cmsBuildParametricToneCurve(pc->context_id, type, params);
assert(curve);
for (i = 0; i < N_CHANNELS; i++)
curveset[i] = curve;
stage = cmsStageAllocToneCurves(pc->context_id, ARRAY_LENGTH(curveset), curveset);
assert(stage);
assert(cmsPipelineInsertStage(pc->pipeline, cmsAT_END, stage));
cmsFreeToneCurve(curve);
}
static void
add_identity_curve(struct pipeline_context *pc)
{
cmsStage *stage;
stage = cmsStageAllocToneCurves(pc->context_id, N_CHANNELS, NULL);
assert(stage);
assert(cmsPipelineInsertStage(pc->pipeline, cmsAT_END, stage));
}
static void
add_matrix(struct pipeline_context *pc,
const cmsFloat64Number matrix[] /* row-major matrix */)
{
cmsStage *stage;
stage = cmsStageAllocMatrix(pc->context_id, N_CHANNELS, N_CHANNELS, matrix, NULL);
assert(stage);
assert(cmsPipelineInsertStage(pc->pipeline, cmsAT_END, stage));
}
static bool
are_matrices_equal(const cmsFloat64Number matrix_A[N_CHANNELS * N_CHANNELS],
const cmsFloat64Number matrix_B[N_CHANNELS * N_CHANNELS])
{
unsigned int i;
for (i = 0; i < N_CHANNELS * N_CHANNELS; i++)
if (fabs(matrix_A[i] - matrix_B[i]) > PRECISION)
return false;
return true;
}
/* Pipeline with a regular matrix. Nothing should change. */
TEST(keep_regular_matrix)
{
struct pipeline_context pc = pipeline_context_new();
const _cmsStageMatrixData *data;
cmsStage *elem;
add_matrix(&pc, regular_matrix);
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
elem = cmsPipelineGetPtrToFirstStage(pc.pipeline);
assert(elem);
data = cmsStageData(elem);
assert(are_matrices_equal(regular_matrix, data->Double));
assert(!cmsStageNext(elem));
pipeline_context_release(&pc);
}
/* Pipeline with a identity matrix, which should be removed. */
TEST(drop_identity_matrix)
{
struct pipeline_context pc = pipeline_context_new();
add_matrix(&pc, identity_matrix);
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
assert(cmsPipelineStageCount(pc.pipeline) == 0);
pipeline_context_release(&pc);
}
/* Pipeline with two inverse matrices. When merged they become identity, which
* should be removed. */
TEST(drop_inverse_matrices)
{
struct pipeline_context pc = pipeline_context_new();
add_matrix(&pc, matrix_A);
add_matrix(&pc, matrix_A_inverse);
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
assert(cmsPipelineStageCount(pc.pipeline) == 0);
pipeline_context_release(&pc);
}
/* Pipeline with an identity and two inverse matrices. Pipeline must be empty
* afterwards. */
TEST(drop_identity_and_inverse_matrices)
{
struct pipeline_context pc = pipeline_context_new();
add_matrix(&pc, identity_matrix);
add_matrix(&pc, matrix_A);
add_matrix(&pc, matrix_A_inverse);
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
assert(cmsPipelineStageCount(pc.pipeline) == 0);
pipeline_context_release(&pc);
}
/* Pipeline has a regular matrix followed by two inverses, which should be
* removed. Pipeline must contain only the regular matrix afterwards. */
TEST(only_drop_inverse_matrices)
{
struct pipeline_context pc = pipeline_context_new();
const _cmsStageMatrixData *data;
cmsStage *elem;
add_matrix(&pc, regular_matrix);
add_matrix(&pc, matrix_A);
add_matrix(&pc, matrix_A_inverse);
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
elem = cmsPipelineGetPtrToFirstStage(pc.pipeline);
assert(elem);
data = cmsStageData(elem);
assert(are_matrices_equal(regular_matrix, data->Double));
assert(!cmsStageNext(elem));
pipeline_context_release(&pc);
}
/* Same as above, but the regular matrix is the last element. */
TEST(only_drop_inverse_matrices_another_order)
{
struct pipeline_context pc = pipeline_context_new();
const _cmsStageMatrixData *data;
cmsStage *elem;
add_matrix(&pc, matrix_A);
add_matrix(&pc, matrix_A_inverse);
add_matrix(&pc, regular_matrix);
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
elem = cmsPipelineGetPtrToFirstStage(pc.pipeline);
assert(elem);
data = cmsStageData(elem);
assert(are_matrices_equal(regular_matrix, data->Double));
assert(!cmsStageNext(elem));
pipeline_context_release(&pc);
}
/* Pipeline has an identity curve, which should be removed. */
TEST(drop_identity_curve)
{
struct pipeline_context pc = pipeline_context_new();
add_identity_curve(&pc);
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
assert(cmsPipelineStageCount(pc.pipeline) == 0);
pipeline_context_release(&pc);
}
/* Pipeline has two parametric curves that are inverse. So they should be
* removed from the pipeline. */
TEST(drop_inverse_curves)
{
struct pipeline_context pc = pipeline_context_new();
add_curve(&pc, srgb_curve.type, srgb_curve.params);
add_curve(&pc, -srgb_curve.type, srgb_curve.params);
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
assert(cmsPipelineStageCount(pc.pipeline) == 0);
pipeline_context_release(&pc);
}
/* Pipeline has two parametric inverse curves followed by an identity. Pipeline
* should be empty afterwards. */
TEST(drop_identity_and_inverse_curves)
{
struct pipeline_context pc = pipeline_context_new();
add_identity_curve(&pc);
add_curve(&pc, srgb_curve.type, srgb_curve.params);
add_curve(&pc, -srgb_curve.type, srgb_curve.params);
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
assert(cmsPipelineStageCount(pc.pipeline) == 0);
pipeline_context_release(&pc);
}
/* Same as above, but the identity is the last element. */
TEST(drop_identity_and_inverse_curves_another_order)
{
struct pipeline_context pc = pipeline_context_new();
add_curve(&pc, srgb_curve.type, srgb_curve.params);
add_curve(&pc, -srgb_curve.type, srgb_curve.params);
add_identity_curve(&pc);
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
assert(cmsPipelineStageCount(pc.pipeline) == 0);
pipeline_context_release(&pc);
}
#if HAVE_CMS_GET_TONE_CURVE_SEGMENT
static bool
are_curveset_curves_equal_to_curve(struct pipeline_context *pc,
const _cmsStageToneCurvesData *curveset_data,
const struct curve *curve)
{
unsigned int i;
bool ret = true;
cmsToneCurve *cms_curve =
cmsBuildParametricToneCurve(pc->context_id, curve->type,
curve->params);
assert(curveset_data->nCurves == N_CHANNELS);
for (i = 0; i < N_CHANNELS; i++) {
if (!are_curves_equal(curveset_data->TheCurves[i], cms_curve)) {
ret = false;
break;
}
}
cmsFreeToneCurve(cms_curve);
return ret;
}
/* Pipeline with a regular curve. Nothing should change. */
TEST(keep_regular_curve)
{
struct pipeline_context pc = pipeline_context_new();
cmsStage *stage;
add_curve(&pc, power_law_curve_A.type, power_law_curve_A.params);
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
stage = cmsPipelineGetPtrToFirstStage(pc.pipeline);
assert(stage);
assert(are_curveset_curves_equal_to_curve(&pc, cmsStageData(stage),
&power_law_curve_A));
assert(!cmsStageNext(stage));
pipeline_context_release(&pc);
}
/* Inverse curves followed by a parametric curve. Inverse curves are dropped (as
* they would become identity if merged), and parametric curve should not be
* changed. */
TEST(do_not_merge_identity_with_parametric)
{
struct pipeline_context pc = pipeline_context_new();
cmsStage *stage;
add_curve(&pc, srgb_curve.type, srgb_curve.params);
add_curve(&pc, -srgb_curve.type, srgb_curve.params);
add_curve(&pc, srgb_curve.type, srgb_curve.params);
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
stage = cmsPipelineGetPtrToFirstStage(pc.pipeline);
assert(stage);
assert(are_curveset_curves_equal_to_curve(&pc, cmsStageData(stage),
&srgb_curve));
assert(!cmsStageNext(stage));
pipeline_context_release(&pc);
}
/* Merge power-law function with itself. */
TEST(merge_power_law_curves_with_itself)
{
struct pipeline_context pc = pipeline_context_new();
cmsStage *stage;
struct curve result_curve;
add_curve(&pc, power_law_curve_A.type, power_law_curve_A.params);
add_curve(&pc, power_law_curve_A.type, power_law_curve_A.params);
memset(&result_curve, 0, sizeof(result_curve));
result_curve.type = power_law_curve_A.type;
result_curve.params[0] = power_law_curve_A.params[0] * power_law_curve_A.params[0];
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
stage = cmsPipelineGetPtrToFirstStage(pc.pipeline);
assert(stage);
assert(are_curveset_curves_equal_to_curve(&pc, cmsStageData(stage),
&result_curve));
assert(!cmsStageNext(stage));
pipeline_context_release(&pc);
}
/* Merge power-law functions into a single parametric one of the same type. */
TEST(merge_power_law_curves_with_another)
{
struct pipeline_context pc = pipeline_context_new();
cmsStage *stage;
struct curve result_curve;
add_curve(&pc, power_law_curve_A.type, power_law_curve_A.params);
add_curve(&pc, power_law_curve_B.type, power_law_curve_B.params);
memset(&result_curve, 0, sizeof(result_curve));
result_curve.type = power_law_curve_A.type;
result_curve.params[0] = power_law_curve_A.params[0] / power_law_curve_B.params[0];
lcms_optimize_pipeline(&pc.pipeline, pc.context_id);
stage = cmsPipelineGetPtrToFirstStage(pc.pipeline);
assert(stage);
assert(are_curveset_curves_equal_to_curve(&pc, cmsStageData(stage),
&result_curve));
assert(!cmsStageNext(stage));
pipeline_context_release(&pc);
}
#endif /* HAVE_CMS_GET_TONE_CURVE_SEGMENT */