Skip to content

Commit

Permalink
Add gradient map filter
Browse files Browse the repository at this point in the history
  • Loading branch information
YakoYakoYokuYoku committed Nov 3, 2024
1 parent 335d009 commit 995a0c3
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/modules/plus/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ add_library(mltplus MODULE
filter_chroma.c
filter_dynamictext.c
filter_dynamic_loudness.c
filter_gradientmap.cpp
filter_invert.c
filter_lift_gamma_gain.c
filter_loudness.c
Expand Down
9 changes: 9 additions & 0 deletions src/modules/plus/factory.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ extern mlt_filter filter_dynamic_loudness_init(mlt_profile profile,
mlt_service_type type,
const char *id,
char *arg);
extern mlt_filter filter_gradientmap_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,
Expand Down Expand Up @@ -157,6 +161,7 @@ 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, "gradientmap", filter_gradientmap_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);
Expand Down Expand Up @@ -201,6 +206,10 @@ MLT_REPOSITORY
"dynamic_loudness",
metadata,
"filter_dynamic_loudness.yml");
MLT_REGISTER_METADATA(mlt_service_filter_type,
"gradientmap",
metadata,
"filter_gradientmap.yml");
MLT_REGISTER_METADATA(mlt_service_filter_type, "invert", metadata, "filter_invert.yml");
MLT_REGISTER_METADATA(mlt_service_filter_type,
"lift_gamma_gain",
Expand Down
264 changes: 264 additions & 0 deletions src/modules/plus/filter_gradientmap.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
extern "C" {
#include <framework/mlt_filter.h>
#include <framework/mlt_frame.h>
}

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <map>
#include <string>
#include <vector>

#define MAX_STOPS 32
#define MAX_COLORS 256

struct mlt_gradient
{
mlt_color color;
double pos;
};

bool operator<(const mlt_gradient &lhs, const mlt_gradient &rhs)
{
return lhs.pos < rhs.pos;
}

bool operator==(const mlt_gradient &lhs, const mlt_gradient &rhs)
{
return lhs.pos == rhs.pos;
}

struct gradient_cache
{
std::vector<mlt_gradient> gradient;
std::vector<mlt_color> colors;
};

typedef std::map<std::string, gradient_cache> gradientmap_cache;

static std::string gradient_key(mlt_properties props)
{
size_t count = mlt_properties_count(props);
size_t p = 1;
std::string res;

// This function will ignore tailing color.* values if one of the indices
// is missing.
for (size_t i = 1; i <= MIN(count, MAX_STOPS); ++i) {
char name[9] = "";
sprintf(name, "color.%d", i);
char *value = mlt_properties_get(props, name);
if (!value)
break;
res += value;
res += ";";
}

// Gradient cache keys are stops joined by semicolons.
return res;
}

static int parse_color(const char *value, char **end)
{
// Parse a hex color value as #RRGGBB or #AARRGGBB.
if (value[0] == '#') {
unsigned int rgb = strtoul(value + 1, end, 16);
unsigned int alpha = (value - *end > 7) ? (rgb >> 24) : 0xff;
return (rgb << 8) | alpha;
}
// Do hex and decimal explicitly to avoid decimal value with leading zeros
// interpreted as octal.
else if (value[0] == '0' && value[1] == 'x') {
return strtoul(value + 2, end, 16);
} else {
return strtol(value, end, 10);
}
}

static void deserialize_gradient(mlt_properties props, std::vector<mlt_gradient> &gradient)
{
size_t count = mlt_properties_count(props);
size_t p = 1;

for (size_t i = 1; i <= MIN(count, MAX_STOPS); ++i) {
char name[9];
sprintf(name, "color.%d", i);
char *value = mlt_properties_get(props, name);
if (!value)
break;
char *p = nullptr;
int c = parse_color(value, &p);
value = p;
double pos = strtod(value, &p);
if (p == value)
continue;
if (p[0] == '%')
pos /= 100.0;

mlt_color color = {uint8_t((c >> 24) & 0xff),
uint8_t((c >> 16) & 0xff),
uint8_t((c >> 8) & 0xff),
uint8_t(c & 0xff)};
gradient.push_back({color, pos});
}

if (gradient.size() == 0) {
// Use a black to white range by default.
gradient.push_back({{0x00, 0x00, 0x00, 0xff}, 0.0});
gradient.push_back({{0xff, 0xff, 0xff, 0xff}, 1.0});
} else {
// Sort the gradient vector using a stable sort.
std::stable_sort(gradient.begin(), gradient.end());
// Deduplicate stops by their position in the gradient, first come,
// only served.
auto last = std::unique(gradient.begin(), gradient.end());
gradient.erase(last, gradient.end());
}
}

static void compute_colors(const std::vector<mlt_gradient> &gradient, std::vector<mlt_color> &colors)
{
for (size_t c = 0; c < colors.size(); ++c) {
// Intensity is proportional to its order in the cache.
float intensity = float(c) / float(colors.size());
const mlt_gradient &front = gradient.front();
const mlt_gradient &back = gradient.back();
// Any value positioned before or after the extremes maps to those.
if (intensity <= front.pos) {
colors[c] = front.color;
continue;
} else if (intensity >= back.pos) {
colors[c] = back.color;
continue;
}
for (size_t i = 0; i < gradient.size() - 1; ++i) {
const mlt_gradient &s = gradient[i];
const mlt_gradient &e = gradient[i + 1];
// Only care if the intensity is bounded by start and end.
if (intensity < s.pos) {
continue;
} else if (intensity == s.pos) {
colors[c] = s.color;
continue;
} else if (intensity == e.pos) {
colors[c] = e.color;
continue;
}
// This interpolation can result in values outside the 0 to 1 range
// due to roundoff errors.
double pos = (intensity - s.pos) / (e.pos - s.pos);
// Calculated color values can be above 255 or below due to above,
// so clamp it to avoid incorrect values in the color cache.
colors[c].r = CLAMP(pos * e.color.r + (1 - pos) * s.color.r, 0, 255);
colors[c].g = CLAMP(pos * e.color.g + (1 - pos) * s.color.g, 0, 255);
colors[c].b = CLAMP(pos * e.color.b + (1 - pos) * s.color.b, 0, 255);
colors[c].a = CLAMP(pos * e.color.a + (1 - pos) * s.color.a, 0, 255);
break;
}
}
}

static mlt_color gradient_map(
const std::vector<mlt_color> &colors, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
{
// This is what Krita does, although other methods could be explored.
float intensity = (r * 0.30 + g * 0.59 + b * 0.11) / 255;
uint32_t t = intensity * colors.size() + 0.5;
if (colors.size() > t) {
mlt_color c = colors[t];
c.a = a;
return c;
} else {
// Because the colors are sorted by position this is the last of the
// gradient stops.
return colors.back();
}
}

static int filter_get_image(mlt_frame frame,
uint8_t **image,
mlt_image_format *format,
int *width,
int *height,
int writable)
{
mlt_filter filter = reinterpret_cast<mlt_filter>(mlt_frame_pop_service(frame));
gradientmap_cache *cache = reinterpret_cast<gradientmap_cache *>(filter->child);

*format = mlt_image_rgba;
int error = mlt_frame_get_image(frame, image, format, width, height, 0);

if (error == 0) {
// As odd as it sounds, a nested cache is used to speed things up. A
// parent holds as many children identified by a key computed from the
// color stops and each one of those contains cached results calculated
// from the gradient. A color cache contains width plus height entries,
// so this might result in a loss of color resolution. Else, the process
// is really slow (O(n^3)) if each pixel has to be computed.
std::string k = gradient_key(MLT_FILTER_PROPERTIES(filter));
auto it = cache->find(k);
if (it == cache->end()) {
it = cache->insert(it, {k, {}});
gradient_cache &e = it->second;
e.gradient.reserve(MAX_STOPS);
e.colors.resize(*width + *height, {0x00, 0x00, 0x00, 0xff});
deserialize_gradient(MLT_FILTER_PROPERTIES(filter), e.gradient);
compute_colors(e.gradient, e.colors);
}
const std::vector<mlt_color> &colors = it->second.colors;

int i = *width * *height + 1;
uint8_t *p = *image;
uint8_t *b = *image;
while (--i) {
mlt_color c = gradient_map(colors, *b++, *b++, *b++, *b++);
*p++ = c.r;
*p++ = c.g;
*p++ = c.b;
*p++ = c.a;
}
}

return error;
}

static mlt_frame filter_process(mlt_filter filter, mlt_frame frame)
{
// Push the frame filter
mlt_frame_push_service(frame, filter);
mlt_frame_push_get_image(frame, filter_get_image);
return frame;
}

static void filter_close(mlt_filter filter)
{
gradientmap_cache *cache = reinterpret_cast<gradientmap_cache *>(filter->child);

delete cache;
filter->child = nullptr;
filter->close = nullptr;
filter->parent.close = nullptr;
mlt_service_close(&filter->parent);
}

extern "C" {

mlt_filter filter_gradientmap_init(mlt_profile profile,
mlt_service_type type,
const char *id,
char *arg)
{
mlt_filter filter = mlt_filter_new();
gradientmap_cache *cache = new gradientmap_cache;
if (filter && cache) {
mlt_properties properties = MLT_FILTER_PROPERTIES(filter);
mlt_properties_set_int(properties, "_filter_private", 1);
filter->process = filter_process;
filter->close = filter_close;
filter->child = cache;
}
return filter;
}

}
32 changes: 32 additions & 0 deletions src/modules/plus/filter_gradientmap.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
schema_version: 0.1
type: filter
identifier: gradientmap
title: Gradient Map
version: 1
copyright: Martin Rodriguez Reboredo
creator: Martin Rodriguez Reboredo
license: LGPLv2.1
language: en
tags:
- Video
description:
Maps the colors of an image to a gradient according to their intensity.

parameters:
- identifier: color.*
title: Color Stop
type: string
description: |
The gradient color stops.
A set of pairs that each describe a point in the gradient. A pair consists
on a color and a position with a separator in between them.
By default, the filter has a gradient that goes from black to white.
color.1='0xff000000 0.0' color.2='0xffffffff 1.0'
This results in the image turned into black and white.
The gradient can hold up to 32 colors. On repeated stop positions only the
first one is taken.

0 comments on commit 995a0c3

Please sign in to comment.