From 9486740d21abce69fb90991722ee2e8769ce70e9 Mon Sep 17 00:00:00 2001 From: Leandro Ribeiro Date: Fri, 2 Jun 2023 14:39:25 -0300 Subject: [PATCH] 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 --- libweston/color-lcms/color-curve-segments.c | 2 +- libweston/color-lcms/color-curve-segments.h | 9 + libweston/color-lcms/color-lcms.h | 3 + libweston/color-lcms/color-transform.c | 12 +- tests/color-lcms-optimizer-test.c | 468 ++++++++++++++++++++ tests/meson.build | 5 + 6 files changed, 495 insertions(+), 4 deletions(-) create mode 100644 tests/color-lcms-optimizer-test.c diff --git a/libweston/color-lcms/color-curve-segments.c b/libweston/color-lcms/color-curve-segments.c index 5be707c6..1d997034 100644 --- a/libweston/color-lcms/color-curve-segments.c +++ b/libweston/color-lcms/color-curve-segments.c @@ -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; diff --git a/libweston/color-lcms/color-curve-segments.h b/libweston/color-lcms/color-curve-segments.h index 9c680688..595b0758 100644 --- a/libweston/color-lcms/color-curve-segments.h +++ b/libweston/color-lcms/color-curve-segments.h @@ -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) diff --git a/libweston/color-lcms/color-lcms.h b/libweston/color-lcms/color-lcms.h index 1f0e90e3..4cf149f4 100644 --- a/libweston/color-lcms/color-lcms.h +++ b/libweston/color-lcms/color-lcms.h @@ -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); diff --git a/libweston/color-lcms/color-transform.c b/libweston/color-lcms/color-transform.c index a95b2b52..25620e84 100644 --- a/libweston/color-lcms/color-transform.c +++ b/libweston/color-lcms/color-transform.c @@ -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; diff --git a/tests/color-lcms-optimizer-test.c b/tests/color-lcms-optimizer-test.c new file mode 100644 index 00000000..0393077b --- /dev/null +++ b/tests/color-lcms-optimizer-test.c @@ -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 + +#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 */ diff --git a/tests/meson.build b/tests/meson.build index 4f3cb6c1..9dd90f2b 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -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',