Wednesday, December 26, 2012

A tiny language for mechanical animations using ImageMagick

My Movimentum project is currently fast asleep—but in a discussion about railroad barriers, I felt that I needed to explain a certain mechanical linkage with animations. Christmas season is a good time to experiment with new ideas (at least new to me), so I created three "mechanical explanations" for an old Austrian gear that drove (and drives) railroad barriers. You can see the results in that posting (in German). If you are interested in what it explains, just ask here—then I'll give you a quick overview about what this is all about. However, this posting here is really about the tool chain I used to create these animations.

Here is the second animation I made:


Here is an overview of my process. Below, I will share my experiences for each step:
  1. I create the parts of the animation with a CAD tool (I use an older version of Caddy++).
  2. The result is exported ("printed") into a single PNG file with PDFCreator.
  3. The PNG file is split into multiple PNGs, each of which gets background transparency (I use an old version of Paint Shop Pro here).
  4. Next, the images need "location calibration": In step 3., they do not come out exactly overlapping. The small corrections to move them to the right position are done here, using ImageMagick.
  5. Next, I write the animation script. For this, I defined a very simple language that is interpreted by a 100-line AWK program (I use Gnu's Gawk for this). The description of this language is the major part of this posting.
  6. The result of this is a batch file that calls ImageMagick's convert program to assemble the images into frames.
  7. At last, ffmpeg is used to create an animation file from the frames.
For the record, I work with Windows. However, I think most of this also applies e.g. to Linux.

1. Creating the parts


Most probably, the parts have to be aligned in the animation. For this, it is very useful to add cross hairs as anchor points at critical locations, e.g., at the center of rotating items.

Moreover, to place the anchors at a predictable point, I put each part into a box of equal size, where the anchors are at the same position relative to the left upper box corner. In addition, I create an empty box that contains only the cross hairs of an anchor.

Here is a part of such a drawing. The lower left box is the empty one.



2. Creating PNG files


Instead of snipping the parts images from the CAD program or a PDF rendering, PDFCreator can be installed as a virtual printer that directly creates PNG files (despite its name). This avoids color and other distortions. Currently, I create a single PNG file with all the parts—this is faster than creating a single PNG file per part by printing from the CAD program, because I have to adjust each parts file anyway in the next step.

Already here, I select the correct scaling of the PNG—at least with Caddy++, this is possible in the print dialog. This avoids any later scalings which will destroy the cross hairs of the anchor points.

PDFCreator creates the PNG in portrait orientation even if you "print" in landscape—I do not know whether this is a fault of PDFCreator or Caddy++ or whatever else. In Windows, the created PNG is immediately opened in the Photo Viewer, so it is only a single click to rotate it to the intended orientation.

3. Splitting the PNG image and adding global transparency


Using Paint Shop Pro, I select the content of each box in the result and paste each part into a new PNG file. Unfortunately, box selection uses screen coordinates in Paint Shop Pro. Therefore, in the new files, the pasted parts may not lie at perfectly the same relative spot—I consistently got offsets of ±1 for both coordinates. In step 4 I explain how I correct this—but maybe there is a better way ... Because of this correction, I save the parts in files called ...Approx.PNG; the corrected files will have names without Approx.

While I have the new part file open, I make the background transparent, because this is what is needed for stacking the parts in the animation. Also the empty box gets its own "part file" with transparent background.

4. "Location calibration"


As explained above, the anchor points of the parts files will not be exactly at the same relative position to the upper left corner. This is a nuisance, as ImageMagick uses the upper left corner as a reference point for its computations, whereas we want to use our anchors. I repair this problem manually as follows:
  • With ImageMagick, I overlay the the empty box over each part.
  • In the resulting image, I check the distance of the part anchors vs. the empty box anchors.
  • Again using ImageMagick, I correct the location of the part.
Here are the concrete steps for this on the command line, using ImageMagick's convert:
  • Overlay and view with convert Part1Approx.PNG Empty.PNG -geometry +0+0 -composite temp.png
  • Viewing temp.png reveals that the anchors can e.g. be superimposed precisely with +1+-2 instead of +0+0. This is a trial-and-error process. Nicely enough, Windows's Photo Viewer will refresh the image immediately, so this is a very quick process.
  • Knowing the size of the empty box (e.g. use ImageMagick's identify—here I use 565x612 as example), invert the correction from +1+-2 to +-1+2, because we now want to shift the part's image, not the empty box. Then create the perfect part file via convert -size 565x612 xc:none Part1Approx.PNG -geometry +-1+2 -composite Part1.PNG.

5. The animation script


Now we come to the central piece: The animation script. The basic idea is that we create a separate ImageMagick convert command for each frame. Each such command will place the parts at specific places. But of course, instead of writing single commands for thousands of frames, I write patterns that contain variables that are expanded many times. The script is then run through an Gnu AWK program that emits the convert commands that compute the frames. Here is the call I use:

  gawk -v rem=REM -f animate.awk script.txt > script.bat

I will explain the "script compiler" animate.awk in a separate posting.

Update: During documenting the small compiler, I found I could make the language a little bit terser: The = step definition has been merged into the $ and + commands.

5.1. Command patterns and variables - overview


Here is such a command pattern that overlays a Part1 at a fixed place and a Part2 after a rotation and a shift (you can look up convert's options in ImageMagick's documentation):

... convert -size 900x800 xc:white \
    Part1.PNG -geometry +50+600 -composite \

    ( Part2.PNG -distort SRT "333,329,-!A2" ) \
      -geometry +%2:x+%2:y -composite \
  out\f_!T.png

The variables in this pattern are the strings starting with ! or %:
  • !... is a scalar variable, Very often this is used for angles (like !A2 above), but sometimes also for numbering of files (like the !T at the end of the command).
  • %... is a vector variable. Its components are accessed with %...:x and %...:y. In the command above, a vector variable %2 is used to shift the rotated Part2.

    5.2. Script segments


    A complete script consists of script segments where each segment describes a homogeneous movement. Each segment consists of two parts:
    • A (possibly empty) sequence of variable assignments.
    • A command pattern (convert patterns usually become very long, therefore they can be broken over multiple lines with trailing backslashes, as shown above).
    Here is an example of such a segment:

      !T 10000 1 .
      $ 4 convert -size 900x800 xc:white \

          Text1.PNG -geometry +50+600 -composite out\f_!T.png
    • !T 10000 1 . is an assignment to the variable !T: Its initial value is 10000, and it is incremented by 1 for each emitted convert. The last parameter (which is a single dot here) will be explained later.
    • $ 4 convert ... means that the convert command is to be emitted 4 times, with variables substituted in each command.
    This script segment will emit four lines where the variable !T is replaced with 10000, 10001, 10002 and 10003 in turn:

      convert -size .... out\f_10000.png
      convert -size .... out\f_10001.png
      convert -size .... out\f_10002.png
      convert -size .... out\f_10003.png  

    5.3. The details of scalar variables


    The assignment to a scalar variable has the following form:

      !name  initial  increment  modulus

    name can consist of any characters you like that are not special in regexps (reason: In the AWK script, regexp replacement is used to interpolate the variables).

    The three parameters can either be numbers (decimals are ok) or single dots (.). The dot means that the value remains as it was before. This is a central concept that helps to write short scripts. E.g., here is how you move something linearly, then let it stop:

      # 15 frames that reduce !X from 200 to some value.
      !X 200 -10 . 
      $ 15 convert ...

      # 10 frames without movement
      !X . 0 .
      $ 10 convert ...

    In the second assignment, we left !X at whatever value it had after the first segment completed by specifying initial as . (dot). However, we set the increment to 0, so that this variable does not change its value in the 10 frames of this segment.

    The modulus is sometimes useful for angle variables: The replacement value for a pattern is the variable's value modulo the modulus. I needed this because I precomputed rotated images of a part into 360 files called something like Part1_000.PNG, Part1_001.PNG,..., Part1_359.PNG. Each file contained the part rotated by so-and-so many degrees. For rotating the part, I could now write something like

      !A 350 2 360
      $ 20 convert ... Part_!A.PNG -geometry +0+0 -composite ...

    Starting at 350 (degrees), the variable !A will be incremented by 2 in each of 20 frames. After 5 frames, !A will be 360 and continue with 362, 364 etc. up to 390. Without 360, the calls using Part_360.PNG etc. will fail, as these files do not exist. However, with modules 360, the emitted commands will contain Part_000.PNG, Part_002.PNG etc. The modulus, if defined, also determines the number of digits in the output number: Because modulus is 360 here, we get three-digit numbers.

    5.4. The details of vector variables


    A vector variable consists of two components, x and y. There are two possible assignments to vector variables. The first form does what you expect:

      %name  initialX  initialY  incrementX  incrementY

    The variable is set and/or incremented as specified. Instead of a number, each argument can again be a single dot, which keeps the current value.

    The other form is necessary for rotational movements:

      %name  initialX  initialY  %other  degrees

    This means that the change to %name is a rotation around %other by the angle specified as the last argument. %other cannot be a dot here (the % is the marker that this form is intended), but the other three values can be numbers or dots. If  %other itself changes, the results are somewhat unpredictable, as the order of variable computations is not defined. If someone has the idea to try this—well, it's on your own risk ...

    5.5. The details of commands


    There are two possible forms for commands. One is

      $ count command-text

    This creates count instances of command-text. The variables are replaced in each instance, and the result is written to the output. As explained above, long commands can be wrapped with \ at the lines' ends (the \ must actually be the last character on the line—no trailing white space!).

    text can be anything your operating systems understands. I have, up to now, only called ImageMagick's convert here. However, one could e.g. write intermediary files and use them later in the frame sequence, e.g. to improve the performance of frame generation (thousands of calls to convert are not that fast ...).

    It makes much sense to write the output of commands—especially the created frame images—to a specific output directory, to keep things cleanly separated.

    The other command form omits the command-text:

      $ count

    This means that the previous command is to be repeated in this segment. This is a very important concept: It allows one to specify the "model" in a single convert command that places all the parts at various places; and then have this assembly move simply by maniulating variables. One could even argue that a good animation script sets up a model once at the beginning; and then only modifies variables.

    In addition to the $ lines, it is possible to switch off the output with

      ~ -

    and switch it on at a specific point with

      ~ +

    This is very helpful for debugging: It creates a script that will start "later in the animation", so that the script will only compute certain frames that are to be fine-tuned. This is preferable to (and much quicker than) editing the script each time it has been written before running it.

    5.6. Comments


    There are two possible forms of comments. One is

      # text

    The text of such a comment is copied into the output, with a leading comment marker that must be passed to the compiler. This is helpful for debugging, because you can line up the script with the output.

    The other form is everything else which is not understood by animation.awk, i.e., every line that does not start with !, %, ~, $, or #. Such lines are simply ignored. I use this feature to embed the script into a text that explains the animations, which will later become a web page. However, one could call this an unfortunate idea (even though it is along the lines of Donald Knuth's Literate Programming).

    6. Running the batch file


    This is as easy as it gets. You just run it.

    7. Creating the animation


    At last, ffmpeg is used to create an animation file from the frames. Here is the command line I use:

    ffmpeg -f image2 -r 12 -i f_1%04d.png 
                 -vcodec libx264 -pix_fmt yuv420p -y out.mp4

    Of course, you can use other formats than mp4 for your animation (especially if you do not like proprietory formats). For mp4, the -pix_fmt yuv420p is important. The default pixel format, whose name I forgot, is not understood by the majority of programs or web sites out there.

    If you want to create multiple animations from a single script (which happens if you embed your script code into running text, as I do), it makes sense to start the frame files with new numbers for each script, using !T 10000 . ., !T 20000 . ., !T 30000 . . etc. Then, you can run ffmpeg with file patterns f_1%04d, f_2%04d, f_3%04d etc.

    In my next posting, I explain the small compiler (which you can download here).
    In a subsequent posting, I will give a few animation examples and some background on why cranks are difficult.

    No comments:

    Post a Comment