Skip to content

Commit c8961b4

Browse files
Add gradient map filter (#1046)
1 parent 8d48e87 commit c8961b4

4 files changed

Lines changed: 324 additions & 0 deletions

File tree

src/modules/plus/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ add_library(mltplus MODULE
88
filter_chroma.c
99
filter_dynamictext.c
1010
filter_dynamic_loudness.c
11+
filter_gradientmap.cpp
1112
filter_hslprimaries.c
1213
filter_hslrange.c
1314
filter_invert.c
@@ -69,6 +70,7 @@ install(FILES
6970
filter_chroma.yml
7071
filter_dynamic_loudness.yml
7172
filter_dynamictext.yml
73+
filter_gradientmap.yml
7274
filter_hslprimaries.yml
7375
filter_hslrange.yml
7476
filter_invert.yml

src/modules/plus/factory.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ extern mlt_filter filter_dynamic_loudness_init(mlt_profile profile,
4949
mlt_service_type type,
5050
const char *id,
5151
char *arg);
52+
extern mlt_filter filter_gradientmap_init(mlt_profile profile,
53+
mlt_service_type type,
54+
const char *id,
55+
char *arg);
5256
extern mlt_filter filter_hslprimaries_init(mlt_profile profile,
5357
mlt_service_type type,
5458
const char *id,
@@ -165,6 +169,7 @@ MLT_REPOSITORY
165169
MLT_REGISTER(mlt_service_filter_type, "chroma_hold", filter_chroma_hold_init);
166170
MLT_REGISTER(mlt_service_filter_type, "dynamictext", filter_dynamictext_init);
167171
MLT_REGISTER(mlt_service_filter_type, "dynamic_loudness", filter_dynamic_loudness_init);
172+
MLT_REGISTER(mlt_service_filter_type, "gradientmap", filter_gradientmap_init);
168173
MLT_REGISTER(mlt_service_filter_type, "hslprimaries", filter_hslprimaries_init);
169174
MLT_REGISTER(mlt_service_filter_type, "hslrange", filter_hslrange_init);
170175
MLT_REGISTER(mlt_service_filter_type, "invert", filter_invert_init);
@@ -211,6 +216,10 @@ MLT_REPOSITORY
211216
"dynamic_loudness",
212217
metadata,
213218
"filter_dynamic_loudness.yml");
219+
MLT_REGISTER_METADATA(mlt_service_filter_type,
220+
"gradientmap",
221+
metadata,
222+
"filter_gradientmap.yml");
214223
MLT_REGISTER_METADATA(mlt_service_filter_type,
215224
"hslprimaries",
216225
metadata,
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
extern "C" {
2+
#include <framework/mlt_filter.h>
3+
#include <framework/mlt_frame.h>
4+
#include <framework/mlt_slices.h>
5+
}
6+
7+
#include <algorithm>
8+
#include <cstdio>
9+
#include <cstdlib>
10+
#include <map>
11+
#include <string>
12+
#include <vector>
13+
14+
#define MAX_STOPS 32
15+
16+
struct stop
17+
{
18+
mlt_color color;
19+
double pos;
20+
};
21+
22+
bool operator<(const stop &lhs, const stop &rhs)
23+
{
24+
return lhs.pos < rhs.pos;
25+
}
26+
27+
bool operator==(const stop &lhs, const stop &rhs)
28+
{
29+
return lhs.pos == rhs.pos;
30+
}
31+
32+
struct gradient_cache
33+
{
34+
std::vector<stop> gradient;
35+
std::vector<mlt_color> colors;
36+
};
37+
38+
typedef std::map<std::string, gradient_cache> gradientmap_cache;
39+
40+
struct slice_desc
41+
{
42+
mlt_image_s image;
43+
const std::vector<mlt_color> *colors;
44+
};
45+
46+
static std::string gradient_key(mlt_properties props)
47+
{
48+
int count = mlt_properties_count(props);
49+
std::string res;
50+
51+
// This function will ignore tailing color.* values if one of the indices
52+
// is missing.
53+
for (int i = 1; i <= MIN(count, MAX_STOPS); ++i) {
54+
char name[9] = "";
55+
sprintf(name, "stop.%d", i);
56+
char *value = mlt_properties_get(props, name);
57+
if (!value)
58+
break;
59+
res += value;
60+
res += ";";
61+
}
62+
63+
// Gradient cache keys are stops joined by semicolons.
64+
return res;
65+
}
66+
67+
static int parse_color(const char *value, char **end)
68+
{
69+
// Parse a hex color value as #RRGGBB or #AARRGGBB.
70+
if (value[0] == '#') {
71+
unsigned int rgb = strtoul(value + 1, end, 16);
72+
unsigned int alpha = (value - *end > 7) ? (rgb >> 24) : 0xff;
73+
return (rgb << 8) | alpha;
74+
}
75+
// Do hex and decimal explicitly to avoid decimal value with leading zeros
76+
// interpreted as octal.
77+
else if (value[0] == '0' && value[1] == 'x') {
78+
return strtoul(value + 2, end, 16);
79+
} else {
80+
return strtol(value, end, 10);
81+
}
82+
}
83+
84+
static void deserialize_gradient(mlt_properties props, std::vector<stop> &gradient)
85+
{
86+
int count = mlt_properties_count(props);
87+
88+
for (int i = 1; i <= MIN(count, MAX_STOPS); ++i) {
89+
char name[9];
90+
sprintf(name, "stop.%d", i);
91+
char *value = mlt_properties_get(props, name);
92+
if (!value)
93+
break;
94+
char *p = nullptr;
95+
int c = parse_color(value, &p);
96+
value = p;
97+
double pos = strtod(value, &p);
98+
if (p == value)
99+
continue;
100+
if (p[0] == '%')
101+
pos /= 100.0;
102+
103+
mlt_color color = {uint8_t((c >> 24) & 0xff),
104+
uint8_t((c >> 16) & 0xff),
105+
uint8_t((c >> 8) & 0xff),
106+
uint8_t(c & 0xff)};
107+
gradient.push_back({color, pos});
108+
}
109+
110+
if (gradient.size() == 0) {
111+
// Use a black to white range by default.
112+
gradient.push_back({{0x00, 0x00, 0x00, 0xff}, 0.0});
113+
gradient.push_back({{0xff, 0xff, 0xff, 0xff}, 1.0});
114+
} else {
115+
// Sort the gradient vector using a stable sort.
116+
std::stable_sort(gradient.begin(), gradient.end());
117+
// Deduplicate stops by their position in the gradient, first come,
118+
// only served.
119+
auto last = std::unique(gradient.begin(), gradient.end());
120+
gradient.erase(last, gradient.end());
121+
}
122+
}
123+
124+
static void compute_colors(const std::vector<stop> &gradient, std::vector<mlt_color> &colors)
125+
{
126+
for (size_t c = 0; c < colors.size(); ++c) {
127+
// Intensity is proportional to its order in the cache.
128+
float intensity = float(c) / float(colors.size());
129+
const stop &front = gradient.front();
130+
const stop &back = gradient.back();
131+
// Any value positioned before or after the extremes maps to those.
132+
if (intensity <= front.pos) {
133+
colors[c] = front.color;
134+
continue;
135+
} else if (intensity >= back.pos) {
136+
colors[c] = back.color;
137+
continue;
138+
}
139+
for (size_t i = 0; i < gradient.size() - 1; ++i) {
140+
const stop &s = gradient[i];
141+
const stop &e = gradient[i + 1];
142+
// Only care if the intensity is bounded by start and end.
143+
if (intensity > e.pos) {
144+
continue;
145+
} else if (intensity == s.pos) {
146+
colors[c] = s.color;
147+
break;
148+
} else if (intensity == e.pos) {
149+
colors[c] = e.color;
150+
break;
151+
}
152+
// This interpolation can result in values outside the 0 to 1 range
153+
// due to roundoff errors.
154+
double pos = (intensity - s.pos) / (e.pos - s.pos);
155+
// Calculated color values can be above 255 or below 0 due to above,
156+
// so clamp it to avoid incorrect values in the color cache.
157+
colors[c].r = CLAMP(pos * e.color.r + (1 - pos) * s.color.r, 0, 255);
158+
colors[c].g = CLAMP(pos * e.color.g + (1 - pos) * s.color.g, 0, 255);
159+
colors[c].b = CLAMP(pos * e.color.b + (1 - pos) * s.color.b, 0, 255);
160+
colors[c].a = CLAMP(pos * e.color.a + (1 - pos) * s.color.a, 0, 255);
161+
break;
162+
}
163+
}
164+
}
165+
166+
static mlt_color gradient_map(
167+
const std::vector<mlt_color> &colors, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
168+
{
169+
// This is what Krita does, although other methods could be explored.
170+
float intensity = (r * 0.30 + g * 0.59 + b * 0.11) / 255;
171+
uint32_t t = intensity * colors.size() + 0.5;
172+
if (colors.size() > t) {
173+
mlt_color c = colors[t];
174+
c.a = a;
175+
return c;
176+
} else {
177+
// Because the colors are sorted by position this is the last of the
178+
// gradient stops.
179+
return colors.back();
180+
}
181+
}
182+
183+
static int sliced_proc(int id, int index, int jobs, void *data)
184+
{
185+
(void) id;
186+
slice_desc *desc = reinterpret_cast<slice_desc *>(data);
187+
int slice_line_start,
188+
slice_height = mlt_slices_size_slice(jobs, index, desc->image.height, &slice_line_start);
189+
int slice_line_end = slice_line_start + slice_height;
190+
int line_size = desc->image.strides[0];
191+
for (int j = slice_line_start; j < slice_line_end; ++j) {
192+
uint8_t *p = desc->image.planes[0] + j * line_size;
193+
for (int i = 0; i < line_size; i += 4) {
194+
mlt_color c = gradient_map(*desc->colors, p[i], p[i + 1], p[i + 2], p[i + 3]);
195+
p[i] = c.r;
196+
p[i + 1] = c.g;
197+
p[i + 2] = c.b;
198+
p[i + 3] = c.a;
199+
}
200+
}
201+
return 0;
202+
}
203+
204+
static int filter_get_image(mlt_frame frame,
205+
uint8_t **image,
206+
mlt_image_format *format,
207+
int *width,
208+
int *height,
209+
int writable)
210+
{
211+
mlt_filter filter = reinterpret_cast<mlt_filter>(mlt_frame_pop_service(frame));
212+
gradientmap_cache *cache = reinterpret_cast<gradientmap_cache *>(filter->child);
213+
214+
*format = mlt_image_rgba;
215+
int error = mlt_frame_get_image(frame, image, format, width, height, 1);
216+
217+
if (error == 0) {
218+
// As odd as it sounds, a nested cache is used to speed things up. A
219+
// parent holds as many children identified by a key computed from the
220+
// color stops and each one of those contains cached results calculated
221+
// from the gradient. A color cache contains width plus height entries,
222+
// so this might result in a loss of color resolution. Else, the process
223+
// is really slow (O(n^3)) if each pixel has to be computed.
224+
std::string k = gradient_key(MLT_FILTER_PROPERTIES(filter));
225+
auto it = cache->find(k);
226+
if (it == cache->end()) {
227+
it = cache->insert(it, {k, {}});
228+
gradient_cache &e = it->second;
229+
e.gradient.reserve(MAX_STOPS);
230+
e.colors.resize(*width + *height, {0x00, 0x00, 0x00, 0xff});
231+
deserialize_gradient(MLT_FILTER_PROPERTIES(filter), e.gradient);
232+
compute_colors(e.gradient, e.colors);
233+
}
234+
const std::vector<mlt_color> &colors = it->second.colors;
235+
236+
slice_desc desc;
237+
desc.colors = &colors;
238+
mlt_image_set_values(&desc.image, *image, *format, *width, *height);
239+
mlt_slices_run_normal(0, sliced_proc, &desc);
240+
}
241+
242+
return error;
243+
}
244+
245+
static mlt_frame filter_process(mlt_filter filter, mlt_frame frame)
246+
{
247+
// Push the frame filter
248+
mlt_frame_push_service(frame, filter);
249+
mlt_frame_push_get_image(frame, filter_get_image);
250+
return frame;
251+
}
252+
253+
static void filter_close(mlt_filter filter)
254+
{
255+
gradientmap_cache *cache = reinterpret_cast<gradientmap_cache *>(filter->child);
256+
257+
delete cache;
258+
filter->child = nullptr;
259+
filter->close = nullptr;
260+
filter->parent.close = nullptr;
261+
mlt_service_close(&filter->parent);
262+
}
263+
264+
extern "C" {
265+
266+
mlt_filter filter_gradientmap_init(mlt_profile profile,
267+
mlt_service_type type,
268+
const char *id,
269+
char *arg)
270+
{
271+
mlt_filter filter = mlt_filter_new();
272+
gradientmap_cache *cache = new gradientmap_cache;
273+
if (filter && cache) {
274+
filter->process = filter_process;
275+
filter->close = filter_close;
276+
filter->child = cache;
277+
}
278+
return filter;
279+
}
280+
281+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
schema_version: 0.1
2+
type: filter
3+
identifier: gradientmap
4+
title: Gradient Map
5+
version: 1
6+
copyright: Martin Rodriguez Reboredo
7+
creator: Martin Rodriguez Reboredo
8+
license: LGPLv2.1
9+
language: en
10+
tags:
11+
- Video
12+
description:
13+
Maps the colors of an image to a gradient according to their intensity.
14+
15+
parameters:
16+
- identifier: stop.*
17+
title: Color Stop
18+
type: string
19+
description: |
20+
The gradient color stops.
21+
22+
A set of pairs that each describe a point in the gradient. A pair consists
23+
on a color and a position with a separator in between them.
24+
25+
By default, the filter has a gradient that goes from black to white.
26+
27+
stop.1='#ff000000 0.0' stop.2='#ffffffff 1.0'
28+
29+
This results in the image turned into black and white.
30+
31+
The gradient can hold up to 32 colors. On repeated stop positions only the
32+
first one is taken.

0 commit comments

Comments
 (0)