Skip to content

Commit

Permalink
zoom: animate output using expressions
Browse files Browse the repository at this point in the history
Adds options to animate coordinates and scale factors using expressions
interpreted with FFmpeg's expression evaluator.

Expressions can be used in place of the existing options for coordinate
offsets and scale factors, but the latter may still be used to set
initial values or with still image output as previously.
  • Loading branch information
0x09 committed Aug 11, 2024
1 parent 0b9c8ce commit 44a97f1
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 15 deletions.
62 changes: 49 additions & 13 deletions zoom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This tool uses the continuous nature of the (i)DCT to perform interpolation on a
It's meant to offer a look at how these behave when sampled in between and outside of the integer grid.

# Usage
Usage: zoom [options] <input> <output>
Usage: zoom [options] <input> <output>

-h, --help This help text.
-s <scale> Rational or decimal scale factor. May be a single value or XxY to specify horizontal/veritcal scaling factors.
Expand All @@ -15,7 +15,7 @@ It's meant to offer a look at how these behave when sampled in between and outsi
-P Position coordinates with -p are relative to the input rather than the scaled output
-% Position coordinates with -p are a percent value rather than a number of samples
-g Scale in linear RGB
-q Don't output progress
-q Don't output progress

--showsamples[=<type>] Show where integer coordinates in the input are located in the scaled image when upscaling.
type: point (default), grid.
Expand All @@ -25,17 +25,53 @@ It's meant to offer a look at how these behave when sampled in between and outsi
interpolated: even around half of a sample of the scaled output
native: even around half of a sample of the input before scaling
centered: the first and last samples of the input correspond to the first and last samples of the output

animation options:
-n <frames> Number of output frames [default: 1]
-x <expr> Expression animating the x coordinate
-y <expr> Expression animating the y coordinate
-S <expr> Expression animating the overall scale factor
-X <expr> Expression animating the horizontal scale factor (if different from -S)
-Y <expr> Expression animating the vertical scale factor (if different from -S)

ffmpeg options:
--ff-format <avformat> output format
--ff-encoder <avcodec> output codec
--ff-rate <rate> output framerate
--ff-opts <optstring> output av options string (k=v:...)
--ff-loglevel <-8..64> av loglevel



## Animation
zoom produces still image output by default, but can animate output (e.g. pans, zooms) using expressions interpreted by [FFmpeg's expression evaluator](https://www.ffmpeg.org/ffmpeg-utils.html#Expression-Evaluation).

Expressions are evaluated for each frame of output and determine that frame's coordinate offsets and scale factors.

Values available for use in expressions:

* `i`: The current frame index.
* `n`: The total number of frames (defined by the `-n` option).
* `w`, `h`: The original input image dimensions.
* `vw`, `vh`: The output view dimensions.
* `x`, `y`: The current coordinates (scaled). The initial value of these is set by the `-p` option if provided.
* `xs`, `ys`: The current vertical and horizontal scale factors. The initial value of these is set by `s` option if provided.

Scaling expressions are evaluated first, so when evaluating coordinates with `-x` and `-y` the values of `xs` and `ys` reflect the current frame's scale factor.

Note that unlike coordinates specified by `-p` where units may be modified by other options, coordinate values for expressions are always in terms of scaled logical pixels.

# Examples
Animate a zoom from 1/4x to 4x keeping coordinates centered:

zoom -n 600 -S '0.25+3.75*(i/n)^2' -x '(w*xs-vw)/2' -y '(h*ys-vh)/2' flower.png zoom.avi

![Flower 1/4 to 4x](https://0x09.net/i/g/flower_zoom.mp4)

Generate an animated subpixel pan across a single logical pixel:

ffmpeg options:
--ff-format <avformat> output format
--ff-encoder <avcodec> output codec
--ff-rate <rate> output framerate
--ff-opts <optstring> output av options string (k=v:...)
--ff-loglevel <-8..64> av loglevel
zoom -n 600 -x 'i/n' image.png pan.avi

# Example
Interpolate [Kodak parrot's](https://r0k.us/graphics/kodak/kodak/kodim23.png) eye 20x
The same pan but iteratively referencing the current x coordinate:

zoom -s20x20 -p 10000x3200 -v 512x512 kodim23.png zoomed.png

![Zoom](https://0x09.net/i/g/zoom.png)
zoom -n 600 -x 'x+1/n' image.png pan.avi
57 changes: 55 additions & 2 deletions zoom/zoom.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <fftw3.h>
#include <libavutil/csp.h>
#include <libavutil/pixdesc.h>
#include <libavutil/eval.h>

#include "magickwand.h"
#include "longmath.h"
Expand Down Expand Up @@ -69,7 +70,7 @@ static size_t generate_scaled_basis(coeff** out_basis, enum scaling_type scaling
}

static void usage(const char* self) {
fprintf(stderr,"Usage: %s [(-s <scale> | -r <res>) -p <pos> -v <size> --basis <type> --showsamples[=<type>] -c -g -P -%%] <input> <output>\n", self);
fprintf(stderr,"Usage: %s [(-s <scale> | -r <res>) -p <pos> -v <size> --basis <type> --showsamples[=<type>] -c -g -P -%% -n -x -y -S -X -Y] <input> <output>\n", self);
exit(1);
}
static void help(const char* self) {
Expand All @@ -96,6 +97,14 @@ static void help(const char* self) {
" native: even around half of a sample of the input before scaling\n"
" centered: the first and last samples of the input correspond to the first and last samples of the output\n"
"\n"
"animation options:\n"
" -n <frames> Number of output frames [default: 1]\n"
" -x <expr> Expression animating the x coordinate\n"
" -y <expr> Expression animating the y coordinate\n"
" -S <expr> Expression animating the overall scale factor\n"
" -X <expr> Expression animating the horizontal scale factor (if different from -S)\n"
" -Y <expr> Expression animating the vertical scale factor (if different from -S)\n"
"\n"
"ffmpeg options:\n"
" --ff-format <avformat> output format\n"
" --ff-encoder <avcodec> output codec\n"
Expand All @@ -122,6 +131,8 @@ int main(int argc, char* argv[]) {
const char* oopt = NULL,* ofmt = NULL,* enc = NULL;
int loglevel = 0;

const char* exprstrs[7] = {0};

int c;
const struct option opts[] = {
{"help",no_argument,NULL,'h'},
Expand All @@ -137,7 +148,7 @@ int main(int argc, char* argv[]) {
{0}
};

while((c = getopt_long(argc,argv,"hs:v:p:cgaPr:%n:q",opts,NULL)) != -1) {
while((c = getopt_long(argc,argv,"hs:v:p:cgaPr:%n:qx:y:S:X:Y:",opts,NULL)) != -1) {
switch(c) {
case 0 : break;
case 'h': help(argv[0]);
Expand All @@ -164,6 +175,11 @@ int main(int argc, char* argv[]) {
case 'g': gamma = true; break;
case 'n': nframes = strtoull(optarg,NULL,10); break;
case 'q': quiet = true; break;
case 'x': exprstrs[0] = optarg; break;
case 'y': exprstrs[1] = optarg; break;
case 'S': exprstrs[2] = optarg; break;
case 'X': exprstrs[3] = optarg; break;
case 'Y': exprstrs[4] = optarg; break;
case 1: {
showsamples = POINT;
if(optarg) {
Expand All @@ -190,6 +206,7 @@ int main(int argc, char* argv[]) {
}
}

quiet |= nframes == 1;
argc -= optind;
if(argc < 1)
usage(argv[0]);
Expand All @@ -201,6 +218,13 @@ int main(int argc, char* argv[]) {

av_log_set_level(loglevel);

AVExpr* xexpr = NULL,* yexpr = NULL,* scaleexpr = NULL,* xscaleexpr = NULL,* yscaleexpr = NULL;
AVExpr** exprs[] = {&xexpr,&yexpr,&scaleexpr,&xscaleexpr,&yscaleexpr};
const char* exprnames[] = {"i","n","x","y","xs","ys","w","h","vw","vh",NULL};
for(int i = 0; i < sizeof(exprstrs)/sizeof(*exprstrs); i++)
if(exprstrs[i] && av_expr_parse(exprs[i],exprstrs[i],exprnames,NULL,NULL,NULL,NULL,0,NULL) < 0)
return 1;

MagickWandGenesis();
MagickWand* wand = NewMagickWand();
if(MagickReadImage(wand,infile) == MagickFalse) {
Expand Down Expand Up @@ -287,6 +311,32 @@ int main(int argc, char* argv[]) {
intermediate* tmp = NULL;

for(size_t d = 0; d < nframes; d++) {
double exprvars[] = {d,nframes,vx,vy,xscale_num/xscale_den,yscale_num/yscale_den,width,height,vw,vh,0};
if(scaleexpr) {
xscale_num = yscale_num = av_expr_eval(scaleexpr,exprvars,NULL);
xscale_den = yscale_den = 1;
}
if(xscaleexpr) {
xscale_num = av_expr_eval(xscaleexpr,exprvars,NULL);
xscale_den = 1;
}
if(yscaleexpr) {
yscale_num = av_expr_eval(yscaleexpr,exprvars,NULL);
yscale_den = 1;
}
// provide x/yexprs with the updated scale factor for this frame
exprvars[4] = xscale_num/xscale_den;
exprvars[5] = yscale_num/yscale_den;
if(xexpr)
vx = av_expr_eval(xexpr,exprvars,NULL);
if(yexpr)
vy = av_expr_eval(yexpr,exprvars,NULL);

if(!(isfinite(vx) && isfinite(vy) && isfinite(xscale_num/xscale_den) && isfinite(yscale_num/yscale_den))) {
fprintf(stderr,"Skipping non-finite expression result at frame %zu\n",d);
continue;
}

bool reuse_basis = width == height && vx == vy && xscale_num == yscale_num && xscale_den == yscale_den;
size_t cwidth = generate_scaled_basis(&xbasis,scaling_type,xscale_num,xscale_den,vx,(reuse_basis ? maxvectors : vw),width);
size_t cheight;
Expand Down Expand Up @@ -355,6 +405,9 @@ int main(int argc, char* argv[]) {
free(xbasis);
free(ybuf);

for(int i = 0; i < sizeof(exprstrs)/sizeof(*exprstrs); i++)
av_expr_free(*exprs[i]);

free(icoeffs);
ffapi_free_frame(frame);
ffapi_close(ffctx);
Expand Down

0 comments on commit 44a97f1

Please sign in to comment.