1
0
mirror of https://github.com/FFmpeg/FFmpeg.git synced 2025-10-06 05:47:18 +02:00

avfilter/vf_libplacebo: introduce fit_mode option

The semantics of these keywords are well-defined by the CSS 'object-fit'
property. This is arguably more user-friendly and less obtuse than the
existing `normalize_sar` and `pad_crop_ratio` options. Additionally, this
comes with two new (useful) behaviors, `none` and `scale_down`, neither of
which map elegantly to the existing options.

One additional benefit of this option is that, unlike `normalize_sar`, it
does *not* also imply `reset_sar`; meaning that users can now choose to
have an anamorphic base layer and still have the overlay images scaled to fit
on top of it according to the chosen strategy.

See-Also: https://drafts.csswg.org/css-images/#the-object-fit
This commit is contained in:
Niklas Haas
2025-09-08 18:28:19 +02:00
committed by Niklas Haas
parent 6ad839ff2e
commit 12d696cff4
2 changed files with 86 additions and 6 deletions

View File

@@ -16459,7 +16459,7 @@ e.g. anamorphic video sources, are forwarded to the output pixel aspect ratio.
@item normalize_sar @item normalize_sar
Like @option{reset_sar}, but instead of stretching the video content to fill Like @option{reset_sar}, but instead of stretching the video content to fill
the new output aspect ratio, the content is instead padded or cropped as the new output aspect ratio, the content is instead padded or cropped as
necessary. necessary. Mutually exclusive with @option{fit_mode}. Disabled by default.
@item pad_crop_ratio @item pad_crop_ratio
Specifies a ratio (between @code{0.0} and @code{1.0}) between padding and Specifies a ratio (between @code{0.0} and @code{1.0}) between padding and
@@ -16469,6 +16469,40 @@ content with black borders, while a value of @code{1.0} always crops off parts
of the content. Intermediate values are possible, leading to a mix of the two of the content. Intermediate values are possible, leading to a mix of the two
approaches. approaches.
@item fit_mode
Specify the content fit strategy according to a list of predefined modes.
Determines how the input image is to be placed inside the destination crop
rectangle (as defined by @code{pos_x/y} and @code{pos_w/h}). The names and
their implementations are taken from the CSS 'object-fit' property. Note that
this option is mutually exclusive with @option{normalize_sar}. Defaults to
@code{fill}. Valid values are:
@table @samp
@item fill
Stretch the input to the output rectangle, ignoring aspect ratio mismatches.
Note that unless @option{reset_sar} is also enabled, the output will still
have the correct pixel aspect ratio tagged.
@item contain
Scale the input to fit inside the output, preserving aspect ratio by padding.
Equivalent to @option{normalize_sar} with @option{pad_crop_ratio} set to
@code{0.0}.
@item cover
Scale the input to fill the output, preserving aspect ratio by cropping.
Equivalent to @option{normalize_sar} with @option{pad_crop_ratio} set to
@code{1.0}.
@item none, place
Don't scale the input. The input will be placed inside the output rectangle at
its natural size; which may result in additional padding or cropping.
@item scale_down
Scale the input down as much as needed to fit inside the output. Equivalent
to either @code{contain} or @code{none}, depending on whether the input is
larger than the output or not.
@end table
@item fillcolor @item fillcolor
Set the color used to fill the output area not covered by the output image, for Set the color used to fill the output area not covered by the output image, for
example as a result of @option{normalize_sar}. For the general syntax of this example as a result of @option{normalize_sar}. For the general syntax of this

View File

@@ -152,6 +152,15 @@ typedef struct LibplaceboInput {
int status; int status;
} LibplaceboInput; } LibplaceboInput;
enum fit_mode {
FIT_FILL,
FIT_CONTAIN,
FIT_COVER,
FIT_NONE,
FIT_SCALE_DOWN,
FIT_MODE_NB,
};
typedef struct LibplaceboContext { typedef struct LibplaceboContext {
/* lavfi vulkan*/ /* lavfi vulkan*/
FFVulkanContext vkctx; FFVulkanContext vkctx;
@@ -196,6 +205,7 @@ typedef struct LibplaceboContext {
int force_divisible_by; int force_divisible_by;
int reset_sar; int reset_sar;
int normalize_sar; int normalize_sar;
int fit_mode;
int apply_filmgrain; int apply_filmgrain;
int apply_dovi; int apply_dovi;
int colorspace; int colorspace;
@@ -543,6 +553,11 @@ static int libplacebo_init(AVFilterContext *avctx)
LibplaceboContext *s = avctx->priv; LibplaceboContext *s = avctx->priv;
const AVVulkanDeviceContext *vkhwctx = NULL; const AVVulkanDeviceContext *vkhwctx = NULL;
if (s->normalize_sar && s->fit_mode != FIT_FILL) {
av_log(avctx, AV_LOG_WARNING, "normalize_sar has no effect when using "
"a fit mode other than 'fill'\n");
}
/* Create libplacebo log context */ /* Create libplacebo log context */
s->log = pl_log_create(PL_API_VER, pl_log_params( s->log = pl_log_create(PL_API_VER, pl_log_params(
.log_level = get_log_level(), .log_level = get_log_level(),
@@ -841,6 +856,7 @@ static void update_crops(AVFilterContext *ctx, LibplaceboInput *in,
{ {
FilterLink *outl = ff_filter_link(ctx->outputs[0]); FilterLink *outl = ff_filter_link(ctx->outputs[0]);
LibplaceboContext *s = ctx->priv; LibplaceboContext *s = ctx->priv;
const AVFilterLink *outlink = ctx->outputs[0];
const AVFilterLink *inlink = ctx->inputs[in->idx]; const AVFilterLink *inlink = ctx->inputs[in->idx];
const AVFrame *ref = ref_frame(&in->mix); const AVFrame *ref = ref_frame(&in->mix);
@@ -900,10 +916,33 @@ static void update_crops(AVFilterContext *ctx, LibplaceboInput *in,
target->crop.y0 = av_expr_eval(s->pos_y_pexpr, s->var_values, NULL); target->crop.y0 = av_expr_eval(s->pos_y_pexpr, s->var_values, NULL);
target->crop.x1 = target->crop.x0 + s->var_values[VAR_POS_W]; target->crop.x1 = target->crop.x0 + s->var_values[VAR_POS_W];
target->crop.y1 = target->crop.y0 + s->var_values[VAR_POS_H]; target->crop.y1 = target->crop.y0 + s->var_values[VAR_POS_H];
if (s->normalize_sar) {
float aspect = pl_rect2df_aspect(&image->crop); /* Effective visual crop */
aspect *= av_q2d(inlink->sample_aspect_ratio); const float w_adj = av_q2d(inlink->sample_aspect_ratio) /
pl_rect2df_aspect_set(&target->crop, aspect, s->pad_crop_ratio); av_q2d(outlink->sample_aspect_ratio);
pl_rect2df fixed = image->crop;
pl_rect2df_stretch(&fixed, w_adj, 1.0);
switch (s->fit_mode) {
case FIT_FILL:
if (s->normalize_sar)
pl_rect2df_aspect_copy(&target->crop, &fixed, s->pad_crop_ratio);
break;
case FIT_CONTAIN:
pl_rect2df_aspect_copy(&target->crop, &fixed, 0.0);
break;
case FIT_COVER:
pl_rect2df_aspect_copy(&target->crop, &fixed, 1.0);
break;
case FIT_NONE: {
const float sx = fabsf(pl_rect_w(fixed)) / pl_rect_w(target->crop);
const float sy = fabsf(pl_rect_h(fixed)) / pl_rect_h(target->crop);
pl_rect2df_stretch(&target->crop, sx, sy);
break;
}
case FIT_SCALE_DOWN:
pl_rect2df_aspect_fit(&target->crop, &fixed, 0.0);
} }
} }
} }
@@ -1446,7 +1485,7 @@ static int libplacebo_config_output(AVFilterLink *outlink)
if (s->reset_sar) { if (s->reset_sar) {
/* SAR is normalized, or we have multiple inputs, set out to 1:1 */ /* SAR is normalized, or we have multiple inputs, set out to 1:1 */
outlink->sample_aspect_ratio = (AVRational){ 1, 1 }; outlink->sample_aspect_ratio = (AVRational){ 1, 1 };
} else if (inlink->sample_aspect_ratio.num) { } else if (inlink->sample_aspect_ratio.num && s->fit_mode == FIT_FILL) {
/* This is consistent with other scale_* filters, which only /* This is consistent with other scale_* filters, which only
* set the outlink SAR to be equal to the scale SAR iff the input SAR * set the outlink SAR to be equal to the scale SAR iff the input SAR
* was set to something nonzero */ * was set to something nonzero */
@@ -1540,6 +1579,13 @@ static const AVOption libplacebo_options[] = {
{ "reset_sar", "force SAR normalization to 1:1 by adjusting pos_x/y/w/h", OFFSET(reset_sar), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, STATIC }, { "reset_sar", "force SAR normalization to 1:1 by adjusting pos_x/y/w/h", OFFSET(reset_sar), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, STATIC },
{ "normalize_sar", "like reset_sar, but pad/crop instead of stretching the video", OFFSET(normalize_sar), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, STATIC }, { "normalize_sar", "like reset_sar, but pad/crop instead of stretching the video", OFFSET(normalize_sar), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, STATIC },
{ "pad_crop_ratio", "ratio between padding and cropping when normalizing SAR (0=pad, 1=crop)", OFFSET(pad_crop_ratio), AV_OPT_TYPE_FLOAT, {.dbl=0.0}, 0.0, 1.0, DYNAMIC }, { "pad_crop_ratio", "ratio between padding and cropping when normalizing SAR (0=pad, 1=crop)", OFFSET(pad_crop_ratio), AV_OPT_TYPE_FLOAT, {.dbl=0.0}, 0.0, 1.0, DYNAMIC },
{ "fit_mode", "Content fit strategy for placing input layers in the output", OFFSET(fit_mode), AV_OPT_TYPE_INT, {.i64 = FIT_FILL }, 0, FIT_MODE_NB - 1, STATIC, .unit = "fit_mode" },
{ "fill", "Stretch content, ignoring aspect ratio", 0, AV_OPT_TYPE_CONST, {.i64 = FIT_FILL }, 0, 0, STATIC, .unit = "fit_mode" },
{ "contain", "Stretch content, padding to preserve aspect", 0, AV_OPT_TYPE_CONST, {.i64 = FIT_CONTAIN }, 0, 0, STATIC, .unit = "fit_mode" },
{ "cover", "Stretch content, cropping to preserve aspect", 0, AV_OPT_TYPE_CONST, {.i64 = FIT_COVER }, 0, 0, STATIC, .unit = "fit_mode" },
{ "none", "Keep input unscaled, padding and cropping as needed", 0, AV_OPT_TYPE_CONST, {.i64 = FIT_NONE }, 0, 0, STATIC, .unit = "fit_mode" },
{ "place", "Keep input unscaled, padding and cropping as needed", 0, AV_OPT_TYPE_CONST, {.i64 = FIT_NONE }, 0, 0, STATIC, .unit = "fit_mode" },
{ "scale_down", "Downscale only if larger, padding to preserve aspect", 0, AV_OPT_TYPE_CONST, {.i64 = FIT_SCALE_DOWN }, 0, 0, STATIC, .unit = "fit_mode" },
{ "fillcolor", "Background fill color", OFFSET(fillcolor), AV_OPT_TYPE_COLOR, {.str = "black@0"}, .flags = DYNAMIC }, { "fillcolor", "Background fill color", OFFSET(fillcolor), AV_OPT_TYPE_COLOR, {.str = "black@0"}, .flags = DYNAMIC },
{ "corner_rounding", "Corner rounding radius", OFFSET(corner_rounding), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, 0.0, 1.0, .flags = DYNAMIC }, { "corner_rounding", "Corner rounding radius", OFFSET(corner_rounding), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, 0.0, 1.0, .flags = DYNAMIC },
{ "lut", "Path to custom LUT file to apply", OFFSET(lut_filename), AV_OPT_TYPE_STRING, { .str = NULL }, .flags = STATIC }, { "lut", "Path to custom LUT file to apply", OFFSET(lut_filename), AV_OPT_TYPE_STRING, { .str = NULL }, .flags = STATIC },