tests: add color pipeline optimizer tests

This will help us to debug our color pipeline optimizer without the
need to craft special ICC profiles for that. In this initial patch,
we are able to add matrices and curve sets to the pipeline and assure
that the optimizer is doing the right thing.

Signed-off-by: Leandro Ribeiro <leandro.ribeiro@collabora.com>
This commit is contained in:
Leandro Ribeiro 2023-06-02 14:39:25 -03:00 committed by Pekka Paalanen
parent 884579bc3c
commit 9486740d21
6 changed files with 495 additions and 4 deletions

View File

@ -306,7 +306,7 @@ are_segments_equal(const cmsCurveSegment *seg_A, const cmsCurveSegment *seg_B)
return true;
}
static bool
WESTON_EXPORT_FOR_TESTS bool
are_curves_equal(cmsToneCurve *curve_A, cmsToneCurve *curve_B)
{
unsigned int i;

View File

@ -38,6 +38,9 @@ curve_set_print(cmsStage *stage, struct weston_log_scope *scope);
bool
are_curvesets_inverse(cmsStage *set_A, cmsStage *set_B);
bool
are_curves_equal(cmsToneCurve *curve_A, cmsToneCurve *curve_B);
cmsStage *
join_powerlaw_curvesets(cmsContext context_id,
cmsToneCurve **set_A, cmsToneCurve **set_B);
@ -57,6 +60,12 @@ are_curvesets_inverse(cmsStage *set_A, cmsStage *set_B)
return false;
}
static inline bool
are_curves_equal(cmsToneCurve *curve_A, cmsToneCurve *curve_B)
{
return false;
}
static inline cmsStage *
join_powerlaw_curvesets(cmsContext context_id,
cmsToneCurve **set_A, cmsToneCurve **set_B)

View File

@ -238,6 +238,9 @@ retrieve_eotf_and_output_inv_eotf(cmsContext lcms_ctx,
unsigned int
cmlcms_reasonable_1D_points(void);
void
lcms_optimize_pipeline(cmsPipeline **lut, cmsContext context_id);
cmsToneCurve *
lcmsJoinToneCurve(cmsContext context_id, const cmsToneCurve *X,
const cmsToneCurve *Y, unsigned int resulting_points);

View File

@ -581,9 +581,8 @@ translate_pipeline(struct cmlcms_color_transform *xform, const cmsPipeline *lut)
return false;
}
static cmsBool
optimize_float_pipeline(cmsPipeline **lut, cmsContext context_id,
struct cmlcms_color_transform *xform)
WESTON_EXPORT_FOR_TESTS void
lcms_optimize_pipeline(cmsPipeline **lut, cmsContext context_id)
{
bool cont_opt;
@ -597,6 +596,13 @@ optimize_float_pipeline(cmsPipeline **lut, cmsContext context_id,
cont_opt = merge_matrices(lut, context_id);
cont_opt |= merge_curvesets(lut, context_id);
} while (cont_opt);
}
static cmsBool
optimize_float_pipeline(cmsPipeline **lut, cmsContext context_id,
struct cmlcms_color_transform *xform)
{
lcms_optimize_pipeline(lut, context_id);
if (translate_pipeline(xform, *lut)) {
xform->status = CMLCMS_TRANSFORM_OPTIMIZED;

View File

@ -0,0 +1,468 @@
/*
* 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 */

View File

@ -301,6 +301,11 @@ if get_option('color-management-lcms')
'name': 'color-icc-output',
'dep_objs': [ dep_libm, dep_lcms_util ]
},
{
'name': 'color-lcms-optimizer',
'link_with': plugin_color_lcms,
'dep_objs': [ dep_lcms_util ]
},
{ 'name': 'color-metadata-parsing' },
{
'name': 'lcms-util',