gstreamer/gst/dvbsubenc/libimagequant/mediancut.c
Jan Schmidt 1cf3cae5e1 dvbsubenc: Add DVB Subtitle encoder
Add an element that converts AYUV video frames to a DVB
subpicture stream.

It's fairly simple for now. Later it would be good to support
input via a stream that contains only GstVideoOverlayComposition
meta.

The element searches each input video frame for the largest
sub-region containing non-transparent pixels and encodes that
as a single DVB subpicture region. It can also do palette
reduction of the input frames using code taken from
libimagequant.

There are various FIXME for potential improvements for now, but
it works.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/merge_requests/1227>
2020-06-17 12:50:13 +10:00

598 lines
17 KiB
C

/*
** Copyright (C) 1989, 1991 by Jef Poskanzer.
** Copyright (C) 1997, 2000, 2002 by Greg Roelofs; based on an idea by
** Stefan Schneider.
** © 2009-2013 by Kornel Lesinski.
**
** Permission to use, copy, modify, and distribute this software and its
** documentation for any purpose and without fee is hereby granted, provided
** that the above copyright notice appear in all copies and that both that
** copyright notice and this permission notice appear in supporting
** documentation. This software is provided "as is" without express or
** implied warranty.
*/
#include <stdlib.h>
#include <stddef.h>
#include "libimagequant.h"
#include "pam.h"
#include "mediancut.h"
#define index_of_channel(ch) (offsetof(f_pixel,ch)/sizeof(float))
static f_pixel averagepixels (unsigned int clrs, const hist_item achv[],
float min_opaque_val, const f_pixel center);
struct box
{
f_pixel color;
f_pixel variance;
double sum, total_error, max_error;
unsigned int ind;
unsigned int colors;
};
ALWAYS_INLINE static double variance_diff (double val,
const double good_enough);
inline static double
variance_diff (double val, const double good_enough)
{
val *= val;
if (val < good_enough * good_enough)
return val * 0.25;
return val;
}
/** Weighted per-channel variance of the box. It's used to decide which channel to split by */
static f_pixel
box_variance (const hist_item achv[], const struct box *box)
{
f_pixel mean = box->color;
double variancea = 0, variancer = 0, varianceg = 0, varianceb = 0;
for (unsigned int i = 0; i < box->colors; ++i) {
f_pixel px = achv[box->ind + i].acolor;
double weight = achv[box->ind + i].adjusted_weight;
variancea += variance_diff (mean.a - px.a, 2.0 / 256.0) * weight;
variancer += variance_diff (mean.r - px.r, 1.0 / 256.0) * weight;
varianceg += variance_diff (mean.g - px.g, 1.0 / 256.0) * weight;
varianceb += variance_diff (mean.b - px.b, 1.0 / 256.0) * weight;
}
return (f_pixel) {
.a = variancea * (4.0 / 16.0),.r = variancer * (7.0 / 16.0),.g =
varianceg * (9.0 / 16.0),.b = varianceb * (5.0 / 16.0),};
}
static double
box_max_error (const hist_item achv[], const struct box *box)
{
f_pixel mean = box->color;
double max_error = 0;
unsigned int i;
for (i = 0; i < box->colors; ++i) {
const double diff = colordifference (mean, achv[box->ind + i].acolor);
if (diff > max_error) {
max_error = diff;
}
}
return max_error;
}
ALWAYS_INLINE static double color_weight (f_pixel median, hist_item h);
static inline void
hist_item_swap (hist_item * l, hist_item * r)
{
if (l != r) {
hist_item t = *l;
*l = *r;
*r = t;
}
}
ALWAYS_INLINE static unsigned int qsort_pivot (const hist_item * const base,
const unsigned int len);
inline static unsigned int
qsort_pivot (const hist_item * const base, const unsigned int len)
{
if (len < 32) {
return len / 2;
}
{
const unsigned int aidx = 8, bidx = len / 2, cidx = len - 1;
const unsigned int a = base[aidx].tmp.sort_value, b =
base[bidx].tmp.sort_value, c = base[cidx].tmp.sort_value;
return (a < b) ? ((b < c) ? bidx : ((a < c) ? cidx : aidx))
: ((b > c) ? bidx : ((a < c) ? aidx : cidx));
}
}
ALWAYS_INLINE static unsigned int qsort_partition (hist_item * const base,
const unsigned int len);
inline static unsigned int
qsort_partition (hist_item * const base, const unsigned int len)
{
unsigned int l = 1, r = len;
if (len >= 8) {
hist_item_swap (&base[0], &base[qsort_pivot (base, len)]);
}
{
const unsigned int pivot_value = base[0].tmp.sort_value;
while (l < r) {
if (base[l].tmp.sort_value >= pivot_value) {
l++;
} else {
while (l < --r && base[r].tmp.sort_value <= pivot_value) {
}
hist_item_swap (&base[l], &base[r]);
}
}
l--;
hist_item_swap (&base[0], &base[l]);
}
return l;
}
/** quick select algorithm */
static void
hist_item_sort_range (hist_item * base, unsigned int len,
unsigned int sort_start)
{
for (;;) {
const unsigned int l = qsort_partition (base, len), r = l + 1;
if (l > 0 && sort_start < l) {
len = l;
} else if (r < len && sort_start > r) {
base += r;
len -= r;
sort_start -= r;
} else
break;
}
}
/** sorts array to make sum of weights lower than halfvar one side, returns edge between <halfvar and >halfvar parts of the set */
static hist_item *
hist_item_sort_halfvar (hist_item * base, unsigned int len,
double *const lowervar, const double halfvar)
{
do {
const unsigned int l = qsort_partition (base, len), r = l + 1;
// check if sum of left side is smaller than half,
// if it is, then it doesn't need to be sorted
unsigned int t = 0;
double tmpsum = *lowervar;
while (t <= l && tmpsum < halfvar)
tmpsum += base[t++].color_weight;
if (tmpsum < halfvar) {
*lowervar = tmpsum;
} else {
if (l > 0) {
hist_item *res = hist_item_sort_halfvar (base, l, lowervar, halfvar);
if (res)
return res;
} else {
// End of left recursion. This will be executed in order from the first element.
*lowervar += base[0].color_weight;
if (*lowervar > halfvar)
return &base[0];
}
}
if (len > r) {
base += r;
len -= r; // tail-recursive "call"
} else {
*lowervar += base[r].color_weight;
return (*lowervar > halfvar) ? &base[r] : NULL;
}
} while (1);
}
static f_pixel get_median (const struct box *b, hist_item achv[]);
typedef struct
{
unsigned int chan;
float variance;
} channelvariance;
static int
comparevariance (const void *ch1, const void *ch2)
{
return ((const channelvariance *) ch1)->variance >
((const channelvariance *) ch2)->variance ? -1 : (((const channelvariance
*) ch1)->variance <
((const channelvariance *) ch2)->variance ? 1 : 0);
}
/** Finds which channels need to be sorted first and preproceses achv for fast sort */
static double
prepare_sort (struct box *b, hist_item achv[])
{
/*
** Sort dimensions by their variance, and then sort colors first by dimension with highest variance
*/
double totalvar = 0;
channelvariance channels[4] = {
{index_of_channel (r), b->variance.r},
{index_of_channel (g), b->variance.g},
{index_of_channel (b), b->variance.b},
{index_of_channel (a), b->variance.a},
};
qsort (channels, 4, sizeof (channels[0]), comparevariance);
for (unsigned int i = 0; i < b->colors; i++) {
const float *chans = (const float *) &achv[b->ind + i].acolor;
// Only the first channel really matters. When trying median cut many times
// with different histogram weights, I don't want sort randomness to influence outcome.
achv[b->ind + i].tmp.sort_value =
((unsigned int) (chans[channels[0].chan] *
65535.0) << 16) | (unsigned int) ((chans[channels[2].chan] +
chans[channels[1].chan] / 2.0 +
chans[channels[3].chan] / 4.0) * 65535.0);
}
{
const f_pixel median = get_median (b, achv);
// box will be split to make color_weight of each side even
const unsigned int ind = b->ind, end = ind + b->colors;
for (unsigned int j = ind; j < end; j++)
totalvar += (achv[j].color_weight = color_weight (median, achv[j]));
}
return totalvar / 2.0;
}
/** finds median in unsorted set by sorting only minimum required */
static f_pixel
get_median (const struct box *b, hist_item achv[])
{
const unsigned int median_start = (b->colors - 1) / 2;
hist_item_sort_range (&(achv[b->ind]), b->colors, median_start);
if (b->colors & 1)
return achv[b->ind + median_start].acolor;
// technically the second color is not guaranteed to be sorted correctly
// but most of the time it is good enough to be useful
return averagepixels (2, &achv[b->ind + median_start], 1.0, (f_pixel) {
0.5, 0.5, 0.5, 0.5}
);
}
/*
** Find the best splittable box. -1 if no boxes are splittable.
*/
static int
best_splittable_box (struct box *bv, unsigned int boxes, const double max_mse)
{
int bi = -1;
double maxsum = 0;
unsigned int i;
for (i = 0; i < boxes; i++) {
if (bv[i].colors < 2) {
continue;
}
// looks only at max variance, because it's only going to split by it
{
const double cv =
MAX (bv[i].variance.r, MAX (bv[i].variance.g, bv[i].variance.b));
double thissum = bv[i].sum * MAX (bv[i].variance.a, cv);
if (bv[i].max_error > max_mse) {
thissum = thissum * bv[i].max_error / max_mse;
}
if (thissum > maxsum) {
maxsum = thissum;
bi = i;
}
}
}
return bi;
}
inline static double
color_weight (f_pixel median, hist_item h)
{
float diff = colordifference (median, h.acolor);
// if color is "good enough", don't split further
if (diff < 2.f / 256.f / 256.f)
diff /= 2.f;
return sqrt (diff) * (sqrt (1.0 + h.adjusted_weight) - 1.0);
}
static void set_colormap_from_boxes (colormap * map, struct box *bv,
unsigned int boxes, hist_item * achv);
static void adjust_histogram (hist_item * achv, const colormap * map,
const struct box *bv, unsigned int boxes);
static double
box_error (const struct box *box, const hist_item achv[])
{
f_pixel avg = box->color;
unsigned int i;
double total_error = 0;
for (i = 0; i < box->colors; ++i) {
total_error +=
colordifference (avg,
achv[box->ind + i].acolor) * achv[box->ind + i].perceptual_weight;
}
return total_error;
}
static bool
total_box_error_below_target (double target_mse, struct box bv[],
unsigned int boxes, const histogram * hist)
{
double total_error = 0;
unsigned int i;
target_mse *= hist->total_perceptual_weight;
for (i = 0; i < boxes; i++) {
// error is (re)calculated lazily
if (bv[i].total_error >= 0) {
total_error += bv[i].total_error;
}
if (total_error > target_mse)
return false;
}
for (i = 0; i < boxes; i++) {
if (bv[i].total_error < 0) {
bv[i].total_error = box_error (&bv[i], hist->achv);
total_error += bv[i].total_error;
}
if (total_error > target_mse)
return false;
}
return true;
}
/*
** Here is the fun part, the median-cut colormap generator. This is based
** on Paul Heckbert's paper, "Color Image Quantization for Frame Buffer
** Display," SIGGRAPH 1982 Proceedings, page 297.
*/
LIQ_PRIVATE colormap *
mediancut (histogram * hist, const float min_opaque_val, unsigned int newcolors,
const double target_mse, const double max_mse, void *(*malloc) (size_t),
void (*free) (void *))
{
hist_item *achv = hist->achv;
struct box *bv = g_alloca (sizeof (struct box) * newcolors);
unsigned int i, boxes, subset_size;
colormap *representative_subset = NULL;
colormap *map;
/*
** Set up the initial box.
*/
bv[0].ind = 0;
bv[0].colors = hist->size;
bv[0].color =
averagepixels (bv[0].colors, &achv[bv[0].ind], min_opaque_val, (f_pixel) {
0.5, 0.5, 0.5, 0.5});
bv[0].variance = box_variance (achv, &bv[0]);
bv[0].max_error = box_max_error (achv, &bv[0]);
bv[0].sum = 0;
bv[0].total_error = -1;
for (i = 0; i < bv[0].colors; i++)
bv[0].sum += achv[i].adjusted_weight;
boxes = 1;
// remember smaller palette for fast searching
subset_size = ceilf (powf (newcolors, 0.7f));
/*
** Main loop: split boxes until we have enough.
*/
while (boxes < newcolors) {
unsigned int indx, clrs;
unsigned int break_at, i;
double lowervar = 0, halfvar, current_max_mse;
hist_item *break_p;
double sm, lowersum;
int bi;
f_pixel previous_center;
if (boxes == subset_size) {
representative_subset = pam_colormap (boxes, malloc, free);
set_colormap_from_boxes (representative_subset, bv, boxes, achv);
}
// first splits boxes that exceed quality limit (to have colors for things like odd green pixel),
// later raises the limit to allow large smooth areas/gradients get colors.
current_max_mse = max_mse + (boxes / (double) newcolors) * 16.0 * max_mse;
bi = best_splittable_box (bv, boxes, current_max_mse);
if (bi < 0)
break; /* ran out of colors! */
indx = bv[bi].ind;
clrs = bv[bi].colors;
/*
Classic implementation tries to get even number of colors or pixels in each subdivision.
Here, instead of popularity I use (sqrt(popularity)*variance) metric.
Each subdivision balances number of pixels (popular colors) and low variance -
boxes can be large if they have similar colors. Later boxes with high variance
will be more likely to be split.
Median used as expected value gives much better results than mean.
*/
halfvar = prepare_sort (&bv[bi], achv);
// hist_item_sort_halfvar sorts and sums lowervar at the same time
// returns item to break at …minus one, which does smell like an off-by-one error.
break_p = hist_item_sort_halfvar (&achv[indx], clrs, &lowervar, halfvar);
break_at = MIN (clrs - 1, break_p - &achv[indx] + 1);
/*
** Split the box.
*/
sm = bv[bi].sum;
lowersum = 0;
for (i = 0; i < break_at; i++)
lowersum += achv[indx + i].adjusted_weight;
previous_center = bv[bi].color;
bv[bi].colors = break_at;
bv[bi].sum = lowersum;
bv[bi].color =
averagepixels (bv[bi].colors, &achv[bv[bi].ind], min_opaque_val,
previous_center);
bv[bi].total_error = -1;
bv[bi].variance = box_variance (achv, &bv[bi]);
bv[bi].max_error = box_max_error (achv, &bv[bi]);
bv[boxes].ind = indx + break_at;
bv[boxes].colors = clrs - break_at;
bv[boxes].sum = sm - lowersum;
bv[boxes].color =
averagepixels (bv[boxes].colors, &achv[bv[boxes].ind], min_opaque_val,
previous_center);
bv[boxes].total_error = -1;
bv[boxes].variance = box_variance (achv, &bv[boxes]);
bv[boxes].max_error = box_max_error (achv, &bv[boxes]);
++boxes;
if (total_box_error_below_target (target_mse, bv, boxes, hist)) {
break;
}
}
map = pam_colormap (boxes, malloc, free);
set_colormap_from_boxes (map, bv, boxes, achv);
map->subset_palette = representative_subset;
adjust_histogram (achv, map, bv, boxes);
return map;
}
static void
set_colormap_from_boxes (colormap * map, struct box *bv, unsigned int boxes,
hist_item * achv)
{
/*
** Ok, we've got enough boxes. Now choose a representative color for
** each box. There are a number of possible ways to make this choice.
** One would be to choose the center of the box; this ignores any structure
** within the boxes. Another method would be to average all the colors in
** the box - this is the method specified in Heckbert's paper.
*/
for (unsigned int bi = 0; bi < boxes; ++bi) {
map->palette[bi].acolor = bv[bi].color;
/* store total color popularity (perceptual_weight is approximation of it) */
map->palette[bi].popularity = 0;
for (unsigned int i = bv[bi].ind; i < bv[bi].ind + bv[bi].colors; i++) {
map->palette[bi].popularity += achv[i].perceptual_weight;
}
}
}
/* increase histogram popularity by difference from the final color (this is used as part of feedback loop) */
static void
adjust_histogram (hist_item * achv, const colormap * map, const struct box *bv,
unsigned int boxes)
{
for (unsigned int bi = 0; bi < boxes; ++bi) {
for (unsigned int i = bv[bi].ind; i < bv[bi].ind + bv[bi].colors; i++) {
achv[i].adjusted_weight *=
sqrt (1.0 + colordifference (map->palette[bi].acolor,
achv[i].acolor) / 4.0);
achv[i].tmp.likely_colormap_index = bi;
}
}
}
static f_pixel
averagepixels (unsigned int clrs, const hist_item achv[],
const float min_opaque_val, const f_pixel center)
{
double r = 0, g = 0, b = 0, a = 0, new_a = 0, sum = 0;
float maxa = 0;
// first find final opacity in order to blend colors at that opacity
for (unsigned int i = 0; i < clrs; ++i) {
const f_pixel px = achv[i].acolor;
new_a += px.a * achv[i].adjusted_weight;
sum += achv[i].adjusted_weight;
/* find if there are opaque colors, in case we're supposed to preserve opacity exactly (ie_bug) */
if (px.a > maxa)
maxa = px.a;
}
if (sum)
new_a /= sum;
/** if there was at least one completely opaque color, "round" final color to opaque */
if (new_a >= min_opaque_val && maxa >= (255.0 / 256.0))
new_a = 1;
sum = 0;
// reverse iteration for cache locality with previous loop
for (int i = clrs - 1; i >= 0; i--) {
double tmp, weight = 1.0f;
f_pixel px = achv[i].acolor;
/* give more weight to colors that are further away from average
this is intended to prevent desaturation of images and fading of whites
*/
tmp = (center.r - px.r);
weight += tmp * tmp;
tmp = (center.g - px.g);
weight += tmp * tmp;
tmp = (center.b - px.b);
weight += tmp * tmp;
weight *= achv[i].adjusted_weight;
sum += weight;
if (px.a) {
px.r /= px.a;
px.g /= px.a;
px.b /= px.a;
}
r += px.r * new_a * weight;
g += px.g * new_a * weight;
b += px.b * new_a * weight;
a += new_a * weight;
}
if (sum) {
a /= sum;
r /= sum;
g /= sum;
b /= sum;
}
assert (!isnan (r) && !isnan (g) && !isnan (b) && !isnan (a));
return (f_pixel) {
.r = r,.g = g,.b = b,.a = a};
}