diff --git a/src/modules/plus/CMakeLists.txt b/src/modules/plus/CMakeLists.txt index 58d746c1c..6b7d12e66 100644 --- a/src/modules/plus/CMakeLists.txt +++ b/src/modules/plus/CMakeLists.txt @@ -8,6 +8,8 @@ add_library(mltplus MODULE filter_chroma.c filter_dynamictext.c filter_dynamic_loudness.c + filter_hslprimaries.c + filter_hslrange.c filter_invert.c filter_lift_gamma_gain.c filter_loudness.c @@ -67,6 +69,8 @@ install(FILES filter_chroma.yml filter_dynamic_loudness.yml filter_dynamictext.yml + filter_hslprimaries.yml + filter_hslrange.yml filter_invert.yml filter_lift_gamma_gain.yml filter_loudness_meter.yml diff --git a/src/modules/plus/factory.c b/src/modules/plus/factory.c index c12902214..ab63fc3ba 100644 --- a/src/modules/plus/factory.c +++ b/src/modules/plus/factory.c @@ -49,6 +49,14 @@ extern mlt_filter filter_dynamic_loudness_init(mlt_profile profile, mlt_service_type type, const char *id, char *arg); +extern mlt_filter filter_hslprimaries_init(mlt_profile profile, + mlt_service_type type, + const char *id, + char *arg); +extern mlt_filter filter_hslrange_init(mlt_profile profile, + mlt_service_type type, + const char *id, + char *arg); extern mlt_filter filter_invert_init(mlt_profile profile, mlt_service_type type, const char *id, @@ -157,6 +165,8 @@ MLT_REPOSITORY MLT_REGISTER(mlt_service_filter_type, "chroma_hold", filter_chroma_hold_init); MLT_REGISTER(mlt_service_filter_type, "dynamictext", filter_dynamictext_init); MLT_REGISTER(mlt_service_filter_type, "dynamic_loudness", filter_dynamic_loudness_init); + MLT_REGISTER(mlt_service_filter_type, "hslprimaries", filter_hslprimaries_init); + MLT_REGISTER(mlt_service_filter_type, "hslrange", filter_hslrange_init); MLT_REGISTER(mlt_service_filter_type, "invert", filter_invert_init); MLT_REGISTER(mlt_service_filter_type, "lift_gamma_gain", filter_lift_gamma_gain_init); MLT_REGISTER(mlt_service_filter_type, "loudness", filter_loudness_init); @@ -201,6 +211,14 @@ MLT_REPOSITORY "dynamic_loudness", metadata, "filter_dynamic_loudness.yml"); + MLT_REGISTER_METADATA(mlt_service_filter_type, + "hslprimaries", + metadata, + "filter_hslprimaries.yml"); + MLT_REGISTER_METADATA(mlt_service_filter_type, + "hslrange", + metadata, + "filter_hslrange.yml"); MLT_REGISTER_METADATA(mlt_service_filter_type, "invert", metadata, "filter_invert.yml"); MLT_REGISTER_METADATA(mlt_service_filter_type, "lift_gamma_gain", diff --git a/src/modules/plus/filter_hslprimaries.c b/src/modules/plus/filter_hslprimaries.c new file mode 100644 index 000000000..8652323a4 --- /dev/null +++ b/src/modules/plus/filter_hslprimaries.c @@ -0,0 +1,304 @@ +/* + * filter_hslprimaries.cpp + * Copyright (C) 2024 Meltytech, LLC + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "hsl.h" + +#include + +#include +#include +#include + +static const float MAX_OVERLAP_RANGE = 29.9; + +enum { + HSL_REGION_RED, + HSL_REGION_YELLOW, + HSL_REGION_GREEN, + HSL_REGION_CYAN, + HSL_REGION_BLUE, + HSL_REGION_MAGENTA, + HSL_REGION_COUNT, +}; + +struct +{ + float beg; + float mid; + float end; +} hueRegions[HSL_REGION_COUNT] = { + [HSL_REGION_RED] = {330.0 / 360.0, 0.0 / 360.0, 30.0 / 360.0}, + [HSL_REGION_YELLOW] = {30.0 / 360.0, 60.0 / 360.0, 90.0 / 360.0}, + [HSL_REGION_GREEN] = {90.0 / 360.0, 120.0 / 360.0, 150.0 / 360.0}, + [HSL_REGION_CYAN] = {150.0 / 360.0, 180.0 / 360.0, 210.0 / 360.0}, + [HSL_REGION_BLUE] = {210.0 / 360.0, 240.0 / 360.0, 270.0 / 360.0}, + [HSL_REGION_MAGENTA] = {270.0 / 360.0, 300.0 / 360.0, 330.0 / 360.0}, +}; + +typedef struct +{ + mlt_filter filter; + uint8_t *image; + mlt_image_format format; + int width; + int height; + float h_shift[HSL_REGION_COUNT]; + float s_scale[HSL_REGION_COUNT]; + float l_scale[HSL_REGION_COUNT]; + float overlap; + float overlap_range; +} sliced_desc; + +static int hueToRegion(float h) +{ + for (int i = 0; i < HSL_REGION_COUNT; i++) { + if (h < hueRegions[i].end) { + return i; + } + } + return HSL_REGION_RED; +} + +static void adjust_pixel(uint8_t *sample, sliced_desc *desc) +{ + float h, s, l; + rgbToHsl(sample[0] / 255.0, sample[1] / 255.0, sample[2] / 255.0, &h, &s, &l); + if (s == 0) { + // No color. Do not adjust. + return; + } + + int region = hueToRegion(h); + float h_shift = desc->h_shift[region]; + float s_scale = desc->s_scale[region]; + float l_scale = desc->l_scale[region]; + + if (desc->overlap != 0.0) { + int o_region = region; + float o_strength = 0.0; + if (region == HSL_REGION_RED) { + // Special case to handle red wrap around + if (h > hueRegions[HSL_REGION_RED].beg + && h < (hueRegions[HSL_REGION_RED].beg + desc->overlap_range)) { + o_region = HSL_REGION_MAGENTA; + o_strength = (h - hueRegions[region].beg) / desc->overlap_range; + } else if (h < hueRegions[HSL_REGION_RED].end + && h > (hueRegions[HSL_REGION_RED].end - desc->overlap_range)) { + o_region = HSL_REGION_YELLOW; + o_strength = (hueRegions[region].end - h) / desc->overlap_range; + } + } else if (h < (hueRegions[region].beg + desc->overlap_range)) { + o_region = (region - 1 + HSL_REGION_COUNT) % HSL_REGION_COUNT; + o_strength = (h - hueRegions[region].beg) / desc->overlap_range; + } else if (h > (hueRegions[region].end - desc->overlap_range)) { + o_region = (region + 1) % HSL_REGION_COUNT; + o_strength = (hueRegions[region].end - h) / desc->overlap_range; + } + if (o_region != region) { + float edge_hshift = (desc->h_shift[region] + desc->h_shift[o_region]) / 2.0; + h_shift = (desc->h_shift[region] * o_strength) + (edge_hshift * (1.0 - o_strength)); + float edge_sscale = (desc->s_scale[region] + desc->s_scale[o_region]) / 2.0; + s_scale = (desc->s_scale[region] * o_strength) + (edge_sscale * (1.0 - o_strength)); + float edge_lscale = (desc->l_scale[region] + desc->l_scale[o_region]) / 2.0; + l_scale = (desc->l_scale[region] * o_strength) + (edge_lscale * (1.0 - o_strength)); + } + } + + if (h_shift == 0.0 && s_scale == 1.0 && l_scale == 1.0) { + // No adjustment for this pixel + return; + } + + // Apply the adjustment + h = h + h_shift; + h = fmod(h, 1.0); + s = s * s_scale; + s = s < 0.0 ? 0.0 : s > 1.0 ? 1.0 : s; + l = l * l_scale; + l = l < 0.0 ? 0.0 : l > 1.0 ? 1.0 : l; + float r, g, b; + hslToRgb(h, s, l, &r, &g, &b); + sample[0] = lrint(r * 255.0); + sample[1] = lrint(g * 255.0); + sample[2] = lrint(b * 255.0); +} + +static int sliced_proc(int id, int index, int jobs, void *data) +{ + (void) id; // unused + sliced_desc *desc = ((sliced_desc *) data); + int slice_line_start, + slice_height = mlt_slices_size_slice(jobs, index, desc->height, &slice_line_start); + int total = desc->width * slice_height + 1; + uint8_t *sample = desc->image + + slice_line_start + * mlt_image_format_size(desc->format, desc->width, 1, NULL); + + switch (desc->format) { + case mlt_image_rgb: + while (--total) { + adjust_pixel(sample, desc); + sample += 3; + } + break; + case mlt_image_rgba: + while (--total) { + adjust_pixel(sample, desc); + sample += 4; + } + break; + default: + mlt_log_error(MLT_FILTER_SERVICE(desc->filter), + "Invalid image format: %s\n", + mlt_image_format_name(desc->format)); + break; + } + + return 0; +} + +static int filter_get_image(mlt_frame frame, + uint8_t **image, + mlt_image_format *format, + int *width, + int *height, + int writable) +{ + mlt_filter filter = (mlt_filter) mlt_frame_pop_service(frame); + mlt_properties properties = MLT_FILTER_PROPERTIES(filter); + mlt_position position = mlt_filter_get_position(filter, frame); + mlt_position length = mlt_filter_get_length2(filter, frame); + sliced_desc desc; + desc.h_shift[HSL_REGION_RED] + = mlt_properties_anim_get_double(properties, "h_shift_red", position, length); + desc.s_scale[HSL_REGION_RED] + = mlt_properties_anim_get_double(properties, "s_scale_red", position, length); + desc.l_scale[HSL_REGION_RED] + = mlt_properties_anim_get_double(properties, "l_scale_red", position, length); + desc.h_shift[HSL_REGION_YELLOW] + = mlt_properties_anim_get_double(properties, "h_shift_yellow", position, length); + desc.s_scale[HSL_REGION_YELLOW] + = mlt_properties_anim_get_double(properties, "s_scale_yellow", position, length); + desc.l_scale[HSL_REGION_YELLOW] + = mlt_properties_anim_get_double(properties, "l_scale_yellow", position, length); + desc.h_shift[HSL_REGION_GREEN] + = mlt_properties_anim_get_double(properties, "h_shift_green", position, length); + desc.s_scale[HSL_REGION_GREEN] + = mlt_properties_anim_get_double(properties, "s_scale_green", position, length); + desc.l_scale[HSL_REGION_GREEN] + = mlt_properties_anim_get_double(properties, "l_scale_green", position, length); + desc.h_shift[HSL_REGION_CYAN] + = mlt_properties_anim_get_double(properties, "h_shift_cyan", position, length); + desc.s_scale[HSL_REGION_CYAN] + = mlt_properties_anim_get_double(properties, "s_scale_cyan", position, length); + desc.l_scale[HSL_REGION_CYAN] + = mlt_properties_anim_get_double(properties, "l_scale_cyan", position, length); + desc.h_shift[HSL_REGION_BLUE] + = mlt_properties_anim_get_double(properties, "h_shift_blue", position, length); + desc.s_scale[HSL_REGION_BLUE] + = mlt_properties_anim_get_double(properties, "s_scale_blue", position, length); + desc.l_scale[HSL_REGION_BLUE] + = mlt_properties_anim_get_double(properties, "l_scale_blue", position, length); + desc.h_shift[HSL_REGION_MAGENTA] + = mlt_properties_anim_get_double(properties, "h_shift_magenta", position, length); + desc.s_scale[HSL_REGION_MAGENTA] + = mlt_properties_anim_get_double(properties, "s_scale_magenta", position, length); + desc.l_scale[HSL_REGION_MAGENTA] + = mlt_properties_anim_get_double(properties, "l_scale_magenta", position, length); + desc.overlap = mlt_properties_anim_get_double(properties, "overlap", position, length); + + // Check if there is any processing to do + int no_change = 1; + for (int i = 0; i < HSL_REGION_COUNT; i++) { + if (desc.h_shift[i] != 0.0 || desc.s_scale[i] != 100.0 || desc.l_scale[i] != 100.0) { + no_change = 0; + break; + } + } + if (no_change) { + return mlt_frame_get_image(frame, image, format, width, height, writable); + } + int error = 0; + + // Make sure the format is acceptable + if (*format != mlt_image_rgb && *format != mlt_image_rgba) { + *format = mlt_image_rgb; + } + + // Get the image + writable = 1; + error = mlt_frame_get_image(frame, image, format, width, height, writable); + if (!error) { + // Scale the parameters down to [0-1] + for (int i = 0; i < HSL_REGION_COUNT; i++) { + desc.h_shift[i] /= 360.0; + desc.s_scale[i] /= 100.0; + desc.l_scale[i] /= 100.0; + } + desc.overlap /= 100.0; + desc.overlap_range = desc.overlap * MAX_OVERLAP_RANGE / 360.0; + // Perform the processing + desc.format = *format; + desc.height = *height; + desc.width = *width; + desc.image = *image; + mlt_slices_run_normal(0, sliced_proc, &desc); + } + + return error; +} + +static mlt_frame filter_process(mlt_filter filter, mlt_frame frame) +{ + mlt_frame_push_service(frame, filter); + mlt_frame_push_get_image(frame, filter_get_image); + return frame; +} + +mlt_filter filter_hslprimaries_init(mlt_profile profile, + mlt_service_type type, + const char *id, + char *arg) +{ + mlt_filter filter = mlt_filter_new(); + if (filter != NULL) { + mlt_properties properties = MLT_FILTER_PROPERTIES(filter); + mlt_properties_set_double(properties, "h_shift_red", 0); + mlt_properties_set_double(properties, "s_scale_red", 100); + mlt_properties_set_double(properties, "l_scale_red", 100); + mlt_properties_set_double(properties, "h_shift_yellow", 0); + mlt_properties_set_double(properties, "s_scale_yellow", 100); + mlt_properties_set_double(properties, "l_scale_yellow", 100); + mlt_properties_set_double(properties, "h_shift_green", 0); + mlt_properties_set_double(properties, "s_scale_green", 100); + mlt_properties_set_double(properties, "l_scale_green", 100); + mlt_properties_set_double(properties, "h_shift_cyan", 0); + mlt_properties_set_double(properties, "s_scale_cyan", 100); + mlt_properties_set_double(properties, "l_scale_cyan", 100); + mlt_properties_set_double(properties, "h_shift_blue", 0); + mlt_properties_set_double(properties, "s_scale_blue", 100); + mlt_properties_set_double(properties, "l_scale_blue", 100); + mlt_properties_set_double(properties, "h_shift_magenta", 0); + mlt_properties_set_double(properties, "s_scale_magenta", 100); + mlt_properties_set_double(properties, "l_scale_magenta", 100); + mlt_properties_set_double(properties, "overlap", 0); + filter->process = filter_process; + } + return filter; +} diff --git a/src/modules/plus/filter_hslprimaries.yml b/src/modules/plus/filter_hslprimaries.yml new file mode 100644 index 000000000..1e72052b9 --- /dev/null +++ b/src/modules/plus/filter_hslprimaries.yml @@ -0,0 +1,205 @@ +schema_version: 7.0 +type: filter +identifier: hslprimaries +title: HSL Primaries +version: 1 +copyright: Meltytech, LLC +license: LGPLv2.1 +language: en +tags: + - Video +description: > + Adjust Hue, Saturation and Lightness for each of the 6 primary/secondary + colors. + +parameters: + - identifier: h_shift_red + title: Hue Shift Red + type: float + minimum: 0.0 + maximum: 360.0 + default: 0.0 + unit: degrees + mutable: yes + animation: yes + + - identifier: s_scale_red + title: Saturation Scale Red + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: l_scale_red + title: Lightness Scale Red + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: h_shift_yellow + title: Hue Shift Yellow + type: float + minimum: 0.0 + maximum: 360.0 + default: 0.0 + unit: degrees + mutable: yes + animation: yes + + - identifier: s_scale_yellow + title: Saturation Scale Yellow + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: l_scale_yellow + title: Lightness Scale Yellow + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: h_shift_green + title: Hue Shift Green + type: float + minimum: 0.0 + maximum: 360.0 + default: 0.0 + unit: degrees + mutable: yes + animation: yes + + - identifier: s_scale_green + title: Saturation Scale Green + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: l_scale_green + title: Lightness Scale Green + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: h_shift_cyan + title: Hue Shift Cyan + type: float + minimum: 0.0 + maximum: 360.0 + default: 0.0 + unit: degrees + mutable: yes + animation: yes + + - identifier: s_scale_cyan + title: Saturation Scale Cyan + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: l_scale_cyan + title: Lightness Scale Cyan + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: h_shift_blue + title: Hue Shift Blue + type: float + minimum: 0.0 + maximum: 360.0 + default: 0.0 + unit: degrees + mutable: yes + animation: yes + + - identifier: s_scale_blue + title: Saturation Scale Blue + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: l_scale_blue + title: Lightness Scale Blue + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: h_shift_magenta + title: Hue Shift Magenta + type: float + minimum: 0.0 + maximum: 360.0 + default: 0.0 + unit: degrees + mutable: yes + animation: yes + + - identifier: s_scale_magenta + title: Saturation Scale Magenta + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: l_scale_magenta + title: Lightness Scale Magenta + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: overlap + title: Overlap + description: The amount of overlap or blending between primaries + type: float + minimum: 0.0 + maximum: 100.0 + default: 0.0 + unit: percent + mutable: yes + animation: yes diff --git a/src/modules/plus/filter_hslrange.c b/src/modules/plus/filter_hslrange.c new file mode 100644 index 000000000..228d3888f --- /dev/null +++ b/src/modules/plus/filter_hslrange.c @@ -0,0 +1,222 @@ +/* + * filter_hslrange.cpp + * Copyright (C) 2024 Meltytech, LLC + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "hsl.h" + +#include + +#include +#include +#include + +typedef struct +{ + mlt_filter filter; + uint8_t *image; + mlt_image_format format; + int width; + int height; + float hue_center; + float hue_range; + float hue_range_max; + float hue_range_min; + float blend; + float blend_range; + float blend_threshold; + float h_shift; + float s_scale; + float l_scale; +} sliced_desc; + +static void adjust_pixel(uint8_t *sample, sliced_desc *desc) +{ + float h, s, l; + rgbToHsl(sample[0] / 255.0, sample[1] / 255.0, sample[2] / 255.0, &h, &s, &l); + if (s == 0) { + // No color. Do not adjust. + return; + } + + float hue_distance = 1.0; + if (desc->hue_range_max > desc->hue_range_min) { + if (h < desc->hue_range_max && h > desc->hue_range_min) { + hue_distance = fabs(desc->hue_center - h); + } + } else { + // Handle the case were the range wraps around 0 + if (h < desc->hue_range_max) { + hue_distance = fabs(desc->hue_range - (desc->hue_range_max - h)); + } else if (h > desc->hue_range_min) { + hue_distance = fabs(desc->hue_range - (h - desc->hue_range_min)); + } + } + if (hue_distance >= 1.0) { + // This sample is outside the range. Do not adjust + return; + } + + float h_shift = desc->h_shift; + float s_scale = desc->s_scale; + float l_scale = desc->l_scale; + + if (hue_distance > desc->blend_threshold) { + float blend_strength = 1.0 - (hue_distance - desc->blend_threshold) / desc->blend_range; + h_shift = h_shift * blend_strength; + s_scale = (desc->s_scale * blend_strength) + (1.0 - blend_strength); + l_scale = (desc->l_scale * blend_strength) + (1.0 - blend_strength); + } + + if (h_shift == 0.0 && s_scale == 1.0 && l_scale == 1.0) { + // No adjustment for this pixel + return; + } + + // Apply the adjustment + h = h + h_shift; + h = fmod(h, 1.0); + s = s * s_scale; + s = s < 0.0 ? 0.0 : s > 1.0 ? 1.0 : s; + l = l * l_scale; + l = l < 0.0 ? 0.0 : l > 1.0 ? 1.0 : l; + float r, g, b; + hslToRgb(h, s, l, &r, &g, &b); + sample[0] = lrint(r * 255.0); + sample[1] = lrint(g * 255.0); + sample[2] = lrint(b * 255.0); +} + +static int sliced_proc(int id, int index, int jobs, void *data) +{ + (void) id; // unused + sliced_desc *desc = ((sliced_desc *) data); + int slice_line_start, + slice_height = mlt_slices_size_slice(jobs, index, desc->height, &slice_line_start); + int total = desc->width * slice_height + 1; + uint8_t *sample = desc->image + + slice_line_start + * mlt_image_format_size(desc->format, desc->width, 1, NULL); + + switch (desc->format) { + case mlt_image_rgb: + while (--total) { + adjust_pixel(sample, desc); + sample += 3; + } + break; + case mlt_image_rgba: + while (--total) { + adjust_pixel(sample, desc); + sample += 4; + } + break; + default: + mlt_log_error(MLT_FILTER_SERVICE(desc->filter), + "Invalid image format: %s\n", + mlt_image_format_name(desc->format)); + break; + } + + return 0; +} + +static int filter_get_image(mlt_frame frame, + uint8_t **image, + mlt_image_format *format, + int *width, + int *height, + int writable) +{ + mlt_filter filter = (mlt_filter) mlt_frame_pop_service(frame); + mlt_properties properties = MLT_FILTER_PROPERTIES(filter); + mlt_position position = mlt_filter_get_position(filter, frame); + mlt_position length = mlt_filter_get_length2(filter, frame); + sliced_desc desc; + desc.hue_center = mlt_properties_anim_get_double(properties, "hue_center", position, length); + desc.hue_range = mlt_properties_anim_get_double(properties, "hue_range", position, length); + desc.blend = mlt_properties_anim_get_double(properties, "blend", position, length); + desc.h_shift = mlt_properties_anim_get_double(properties, "h_shift", position, length); + desc.s_scale = mlt_properties_anim_get_double(properties, "s_scale", position, length); + desc.l_scale = mlt_properties_anim_get_double(properties, "l_scale", position, length); + + // Check if there is any processing to do + if (desc.hue_range == 0.0 + || (desc.h_shift == 0.0 && desc.s_scale == 100.0 && desc.l_scale == 100.0)) { + return mlt_frame_get_image(frame, image, format, width, height, writable); + } + int error = 0; + + // Make sure the format is acceptable + if (*format != mlt_image_rgb && *format != mlt_image_rgba) { + *format = mlt_image_rgb; + } + + // Get the image + writable = 1; + error = mlt_frame_get_image(frame, image, format, width, height, writable); + if (!error) { + // Scale the parameters down to [0-1] + desc.hue_center /= 360.0; + desc.hue_range /= 360.0; + desc.blend /= 100.0; + desc.h_shift /= 360.0; + desc.s_scale /= 100.0; + desc.l_scale /= 100.0; + // Precompute some variables + desc.hue_range /= 2; // Range from center, not whole range + desc.hue_range_min = desc.hue_center - desc.hue_range; + if (desc.hue_range_min < 0.0) + desc.hue_range_min += 1.0; + desc.hue_range_max = fmod(desc.hue_center + desc.hue_range, 1.0); + desc.blend_range = desc.hue_range * desc.blend; + desc.blend_threshold = desc.hue_range - desc.blend_range; + desc.format = *format; + desc.height = *height; + desc.width = *width; + desc.image = *image; + mlt_slices_run_normal(0, sliced_proc, &desc); + } + + return error; +} + +static mlt_frame filter_process(mlt_filter filter, mlt_frame frame) +{ + mlt_frame_push_service(frame, filter); + mlt_frame_push_get_image(frame, filter_get_image); + return frame; +} + +mlt_filter filter_hslrange_init(mlt_profile profile, + mlt_service_type type, + const char *id, + char *arg) +{ + mlt_filter filter = mlt_filter_new(); + if (filter != NULL) { + mlt_properties properties = MLT_FILTER_PROPERTIES(filter); + mlt_properties_set_double(properties, "hue_center", 180); + mlt_properties_set_double(properties, "hue_range", 0); + mlt_properties_set_double(properties, "blend", 0); + mlt_properties_set_double(properties, "h_shift", 0); + mlt_properties_set_double(properties, "s_scale", 100); + mlt_properties_set_double(properties, "l_scale", 100); + filter->process = filter_process; + } + return filter; +} diff --git a/src/modules/plus/filter_hslrange.yml b/src/modules/plus/filter_hslrange.yml new file mode 100644 index 000000000..a1865215c --- /dev/null +++ b/src/modules/plus/filter_hslrange.yml @@ -0,0 +1,80 @@ +schema_version: 7.0 +type: filter +identifier: hslrange +title: HSL Range +version: 1 +copyright: Meltytech, LLC +license: LGPLv2.1 +language: en +tags: + - Video +description: > + Adjust Hue, Saturation and Lightness for a range of hue values. + The user can specify the range of the hue values to be adjusted. + +parameters: + - identifier: hue_center + title: Hue Center + description: The center value for the hue range + type: float + minimum: 0.0 + maximum: 360.0 + default: 180.0 + unit: degrees + mutable: yes + animation: yes + + - identifier: hue_range + title: Hue Range + description: The width of the hue range + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: blend + title: Blend + description: The amount to blend the edges of the hue range + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: h_shift + title: Hue Shift + description: The amount to shift the hue in the hue range + type: float + minimum: 0.0 + maximum: 360.0 + default: 0.0 + unit: degrees + mutable: yes + animation: yes + + - identifier: s_scale + title: Saturation Scale + description: The amount to scale the saturation in the hue range + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes + + - identifier: l_scale + title: Lightness Scale + description: The amount to scale the lightness in the hue range + type: float + minimum: 0.0 + maximum: 500.0 + default: 100.0 + unit: percent + mutable: yes + animation: yes diff --git a/src/modules/plus/hsl.h b/src/modules/plus/hsl.h new file mode 100644 index 000000000..d1c793a5e --- /dev/null +++ b/src/modules/plus/hsl.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 Meltytech, LLC + * + * 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. + */ + +#include + +static void rgbToHsl(float r, float g, float b, float *h, float *s, float *l) +{ + float max = MAX(MAX(r, g), b); + float min = MIN(MIN(r, g), b); + *l = (max + min) / 2; + if (max == min) { + // No color + *h = *s = 0; + } else { + float d = max - min; + *s = (*l > 0.5) ? d / (2 - max - min) : d / (max + min); + if (max == r) + *h = (g - b) / d + (g < b ? 6 : 0); + else if (max == g) + *h = (b - r) / d + 2; + else // if (max == b) + *h = (r - g) / d + 4; + *h /= 6; + } +} + +static float hueToRgb(float p, float q, float t) +{ + if (t < 0) + t += 1; + if (t > 1) + t -= 1; + if (t < 1. / 6) + return p + (q - p) * 6 * t; + if (t < 1. / 2) + return q; + if (t < 2. / 3) + return p + (q - p) * (2. / 3 - t) * 6; + return p; +} + +static void hslToRgb(float h, float s, float l, float *r, float *g, float *b) +{ + if (0 == s) { + // No color + *r = *g = *b = l; + } else { + float q = l < 0.5 ? l * (1 + s) : l + s - l * s; + float p = 2 * l - q; + *r = hueToRgb(p, q, h + 1.0 / 3.0); + *g = hueToRgb(p, q, h); + *b = hueToRgb(p, q, h - 1.0 / 3.0); + } +}