MrChristo
MrChristo

Reputation: 21

ImageMagick: combine multiple 'montage' commands into one to gain performance

I have a script that takes 4 pictures and duplicates them to produce 8 small pictures in one. The script also adds a background to the output.

Here is what I expect:

http://img11.hostingpics.net/pics/831624stack.png

I have a code that works well. But it needs to save multiple temporary images.

So I was looking if I could merge the commands of my script to get only one image saving operation. The goal is to make the script complete faster.

//Make image smaller:
    convert /home/pi/images/*.png -resize 487x296 -flop \
      -set filename:f "/home/pi/imagesResized/%t%p" '%[filename:f]'.png

//Make one column from 4 images:
    montage /home/pi/imagesResized/*.png -tile 1x4 \
      -geometry +0+15 -background none  /home/pi/line.png

//Duplicate to two columns:
    montage /home/pi/line.png /home/pi/line.png -tile 2x1 \
      -geometry +45+0 -background none /home/pi/photos.png

//Add the background image:
    suffix=$(date +%H%M%S)
    montage /home/pi/photos.png -geometry +20+247 \
         -texture /home/pi/data/background_photo.png \
          /home/pi/photo_${suffix}.jpg

Upvotes: 1

Views: 2972

Answers (3)

Kurt Pfeifle
Kurt Pfeifle

Reputation: 90253

Here is another answer. It took inspiration from Mark Setchell's approach. Here I come up with a new command pipeline. The difference is, that I...

  • ...use exclusively 3 montage commands in a pipeline (Mark used 1 montage and 2 converts in his pipeline), and I
  • ...reproduce exactly the output of the OP's 3 different commands.

As input I used the four images created by the Ghostscript command in my other answer, t{1,2,3,4}.png.

To create a suitable background image I used the command from Mark, though modified: I made the image a tad bit smaller, so that the -texture operator from the OP could by meaningfully used:

convert -size 200x200 xc:gray +noise gaussian background.png

Then I benchmarked all three commands/scripts,...

  1. ...the OP commands as a shell script,
  2. ...the approach of Mark, albeit modified as outlined above,
  3. ...my initial approach, amended with adding the background image.

For benchmarking, I did run 100 repetitions each. When doing so, I started each script/command in a separate terminal window, roughly at the same time. The CPU on the test machine has 4 cores. So when the commands were running in parallel, they had to deal with the same CPU- and I/O-load each, competing with each other for resources. This is the most "fair" ad-hoc performance testing setup I could come up with.

I also confirmed that the output files created by the 3 tests (photo_test1.jpg, ms+kp_test2.jpg and kp_test3.jpg) are pixelwise (almost) identical. If the output is switched to PNG (instead of JPEG, how the OP asked for), then these small differences also disappear between the 3 approaches.

Here are the results:

Script using 4 commands from the original post (OP):

mkdir ./imagesResized
time for i in {1..100}; do
    convert t*.png -resize 487x296\! -flop                   \
      -set filename:f "./imagesResized/%t%p" '%[filename:f]'.png    
    montage ./imagesResized/*.png -tile 1x4 -geometry +0+15  \
            -background none line.png    
    montage line.png line.png -tile 2x1 -geometry +45+0      \
            -background none photos.png

    montage photos.png -geometry +20+247                     \
            -texture background.png                          \
             photo_test1.jpg    
done

Results:

real 2m13.471s
user 1m54.306s
sys 0m14.340s

Roughly 133 seconds in real time.

Take this as 100% time consumption.

Modified command pipeline inspired by Mark's answer:

time for i in {1..100}; do
  montage t[1234].png -resize 487x296\! -flop -background none \
         -tile 1x4 -geometry +0+15 miff:-                      \
    | montage :- -background none -clone 0 -tile 2x1 -geometry +45+0 miff:- \
    | montage :- -geometry +20+247 -texture background.png ms+kp_test2.jpg ;
done

Results:

real 1m50.125s
user 1m32.453s
sys 0m16.578s

Roughly 110 seconds.

About 83% time consumption compared to original commands.

My original command, now completed with missing background compositing:

time for i in {1..100}; do
    convert t*.png -flop -resize 487x296\! \
          -background none     \    
          -size 0x15           \    
           xc:white            \    
          -duplicate 7         \    
          -insert 0            \    
          -insert 2            \    
          -insert 3            \    
          -insert 5            \    
          -insert 6            \    
          -insert 8            \    
          -insert 9            \    
          -append              \    
          \( +clone -size 90x0 xc:white +swap \) \ 
          +append              \    
          -transparent white   \
           miff:-              \    
    | montage :- -geometry +65+247 -texture background.png kp_test3.png
done 

Results:

real 1m34.786s
user 1m20.595s
sys 0m13.026s

Roughly 95 seconds.

About 72% time consumption compared to original commands.

Upvotes: 0

Kurt Pfeifle
Kurt Pfeifle

Reputation: 90253

First, let me say this: you'll save only significant processing time by putting the separate commands into one single ImageMagick command chain if your input images are quite large. You may however save disk I/O time by skipping the need to write out and read in the intermediate result images.

Your code uses two different montage commands plus one convert command in order to achieve a first montage. Finally you use one montage to place the previous result on the background.

From the top of my head, I can quickly come up with a way to combine the first three commands into a single one. The last montage step to place the intermediate results onto the background is not so easy to get right, and very likely will not save much time either. Hence, I will leave that open for now.

Unfortunately you did not provide any link to your source images. I had to create my own ones in order to answer this question. They can also serve to demo the validity of my answer.

To create four 800x600 pixel sized PNGs, I used Ghostscript with a little bit of PostScript code on the command line:

for i in 1 2 3 4 ; do 
   gs -o t${i}.png                  \
      -g800x600                     \
      -sDEVICE=pngalpha             \
      -c "0.5 setgray"              \
      -c "0 0 800 600 rectfill"     \
      -c "1 0 0 setrgbcolor"        \
      -c "3 setlinewidth"           \
      -c "10 10 780 580 rectstroke" \
      -c "0 setgray"                \
      -c "/Helvetica-Bold findfont" \
      -c "560 scalefont setfont"    \
      -c "230 60 moveto"            \
      -c "(${i}) show "             \
      -c "showpage" ; 
done

Then I first tested your code with my images. Here is the result from the OP's commands. The result is complete, including montage on a background image from my own stock (updated), created with a command inspired by Mark Setchell's answer:

convert -size 200x200 xc:gray +noise gaussian background.png

Merging the first two commands:

The following was my first shot in order to come up with a single command. It should achieve the same result as your first two commands outputting line.png. I already knew it wouldn't work exactly as expected in some aspects, but I still tried. I tried it in order to see if there are other places of the command that would show problems that I didn't expect. No worries, the explanation of the complete, final code will be at the end of the answer. You can try to figure out how the following command works once you read the complete answer:

_col1=blue ;
_col2=red  ;
convert t*.png -flop -resize 487x296\! \
   \( -size 15x15                      \
      -clone 0 xc:${_col1}             \
      -clone 1 xc:${_col1}             \
      -clone 2 xc:${_col1}             \
      -clone 3                         \
      -append                          \
      +write f.png                     \
   \) null:

Here is the result of my command (right) compared to the intermediate result after your second command (left):

 

So, one thing I had expected happened: there is a blue spacer between each image. I colorized it for debugging reasons. This can be fixed by setting the color variable to none (transparent).

Things I hadn't expected and which I only discovered after opening the resulting image f.png:

  • My background was white instead of transparent. This can be fixed by adding -background none at the right place.

  • My spacing of the individual images in the columns is too narraw, being 15 pixels only. This is because in the intermediate file line.png of the OP the spacing is not 15 pixels, but 30 pixels. His parameter -geometry +0+15 for the montage creating the columns does add the 15 pixels on top as well as on bottom of each image. My command (using convert ... -append instead of montage) does not allow for additional -geometry settings which would have the same effect. But his can be fixed by adding more xc:{_col1} spacers into my command.

Merging the first three commands:

So here is the next iteration. It integrates the effect of the third command from the OP. This is achieved by adding +duplicate for duplicating the first created column. Then it adds +append to append the duplicate column horizontally (-append would do so vertically):

_col1=blue ;
_col2=red  ;
convert t*.png -flop -resize 487x296\! \
   \( -size 15x15                      \
      -background none                 \
       xc:${_col1}                     \
      -clone 0 xc:${_col1} xc:${_col1} \
      -clone 1 xc:${_col1} xc:${_col1} \
      -clone 2 xc:${_col1} xc:${_col1} \
      -clone 3                         \
       xc:${_col1}                     \
      -append                          \
      +duplicate                       \
      -size 45x45 xc:${_col2}          \
      +append                          \
      +write f2.png                    \
   \) null:

Again one thing I had expected happened:

  • The red spacer between the two columns was on the right instead of sitting in between the columns. We can fix that by swapping the last two images that get +append-ed. This can be done by adding the +swap operator at the right place.

  • Also, the same thing as with the inter-image spacing in the first column will apply to the spacing in bewteen the columns: I have to double it.

  • I will not care at the moment that the same space (45 pixels) is not added to the +append-ed columns left and right.

So here is one more iteration:

_col1=red ;
_col2=blue ; 
convert t*.png -flop -resize 487x296\! \
   \( -background none                 \
      -size 15x15                      \
       xc:${_col1}                     \
      -clone 0 xc:${_col1} xc:${_col1} \
      -clone 1 xc:${_col1} xc:${_col1} \
      -clone 2 xc:${_col1} xc:${_col1} \
      -clone 3                         \
       xc:${_col1}                     \
      -append                          \
      +duplicate                       \
      -size 90x90 xc:${_col2}          \
      +swap                            \
      +append                          \
      +write f3.png                    \
   \) null:

Here is the result:

  • On the right is the intermediate photos.png created by the OP code after the third command.
  • On the left is the image montage created by my command.

 

What's missing now is an explanation with a breakdown of the individual operations I packed into a single command:

  • \( :
    This opens a "sideways" processing of some of the images. The result of this sideway processing is then again inserted into the main ImageMagick process. The backslashes are required so the shell does not try to interpret them.
  • \) :
    This closes the sideways processing.
  • -size 15x15 :
    This sets the size of a canvas to be filled next.
  • xc:${_col1} :
    This fills the canvas with color specified. xc: is just an alias to canvas:, but it is faster to type.
  • -clone 0 :
    This creates a copy of the first image that is currently in the loaded image stack. In this case it is a copy of t1.png. -clone 1 copies t2.png, -clone 2 copies t3.png, etc. -clone or +clone work best inside sideway processing chains, hence the previously explained use of \( and \).
  • -append :
    This operator appends all currently loaded images vertically. In this case these are the 4 copies of t1.png, ... t4.png.
  • +duplicate :
    This operator similar to +clone. It copies the last image in the currently loaded image stack. In this case the last image (and only remaining one inside the sideways pipeline) is the result of the previous -append operation. This operation had created the first column of 4 images, spaced apart by the red spacers.
  • +append :
    This operator appends all currently loaded images horizontally. There are currently three images: the result of the -append operation, its copy created by +duplicate, and the 90x90 sized xc:-canvas.
  • +swap :
    This operator swaps the last two images on the currently loaded stack.
  • +write :
    This operator writes out all of the images from the currently loaded stack. When there are multiple images, it will write these images with the given name, but with a number appended. It's a great tool for debugging complex ImageMagick commands. It is great because after the +write operation is finished, the previously loaded images remain all on the stack. These images remain unchanged, and processing can continue. However, we are finished now and won't continue in this case. Hence we close the side-way process with a \).
  • null :
    Now that we closed the sideway process, ImageMagick puts the resulting image from the sideway into the main pipeline again. Remember, +write didn't finish the processing, it wrote an file to disk that is meant to be an intermediate result. In the main pipeline, there are now still the original t1.png ... t4.png plus the result from the sideway processing. However we will not do anything with them. We will take the intermediate result from +write as our final one. But the convert command expects to now see an output filename. If it doesn't see one, it will complain and show us an error message. Hence we tell it to write off all it has loaded and discard all images from the stack. To achieve this, we use null: as output filename.

    (If you feel adventurous, use out.png as a filename instead of null:. You will see that ImageMagick actually creates multiple out-0.png, out-1.png,...out-3.png filenames. You will find that out-4.png is the same as f.png, and out-{0,1,2,3}.png are the same as the input images. -- You could also replace null: by -append output.jpg and see what happens then...)

Update

Now for the speed comparison...

For a first rough benchmark, I did run the OP's first three commands in a loop with 100 iterations. Then I did run my own command 100 times as well.

Here are the results:

  • OP first three commands, 100 times: 61.3 seconds
  • My single command replacing these, 100 times: 48.9 seconds

So my single command saved roughly 20% time over the original commands from the OP.

Given that my disk I/O performance can be assumed to be pretty fast (the test system has an SSD) in comparison with a spinning harddisk, the speed gain from the merged command (which avoids too many temporary file write/reads) may be more distinct on a system with a slower disk.

To check if a little re-architecture of the command (where not so many loaded images are simply discarded at the end, as can be seen by the null: output file name) would gain more improvements, I also tried this:

convert t*.png -flop -resize 487x296\! \
      -background none   \
      -size 0x15         \
       xc:red            \
      -duplicate 7 \
      -insert 0    \
      -insert 2    \
      -insert 3    \
      -insert 5    \
      -insert 6    \
      -insert 8    \
      -insert 9    \
      -append      \
      \( +clone -size 45x0 xc:blue +swap \) \
       +append \
       f4.png

The architecture of this command is a bit different.

  1. First, it loads all the images. It -flops them and it -resizes them.

  2. Second, it creates a single 15x15 pixels canvas, which then is also placed on the image stack.

  3. Third, it creates 7 additional copies of that canvas. Now there are 12 images on the stack: the 4 input files, 1 xc:-canvas, and 7 copies of the canvas.

  4. Then, it uses a series of -insert N operations. The -insert N operation manipulates the order of the image stack. It removes the last image from the stack and inserts it into image index position N. When the -insert series of operations starts, there are 4 images (t1, t2, t3, t4) on the stack, plus 8 "spacers" (s). This is their original order:

    index:   0   1   2   3   4   5   6   7   8   9  10  11
    image:  t1  t2  t3  t4   s   s   s   s   s   s   s   s  
    

    I've picked the index numbers in a way so that from above original order the changed new order after all -insert N operations are finished is:

    index:   0   1   2   3   4   5   6   7   8   9  10  11
    image:   s  t1   s   s  t2   s   s  t3   s   s  t4   s
    

    Since all spacers s are 15 pixels wide, this achieves the same spacing as my initial command.

  5. Next, a similar sideway processing happens: this time to +clone the result of the previous -append operation and to create the horizontal spacer.

  6. Last, the +append (which operates on the result from the previous -append and from the sideway-process) creates the final result, f4.png.

When I benchmarked this last command, I got the following results for 100 repetitions of each command:

  • My first command before the update, 100 times: 48.3 seconds
  • My last command explained after the update, 100 times: 46.6 seconds

So the speed gain is not quite so notable, roughly 3% better if you want to trust these numbers.

(It should be noted, that I did run both loops in parallel on the same machine to create more fairness in the benchmarking competition. By running in parallel, they both have to deal with the same CPU and I/O load which may be caused by themselves as well as by other processes happening on the machine concurrently.)

Upvotes: 6

Mark Setchell
Mark Setchell

Reputation: 207465

As I don't have your images or your texture or their sizes, I will show you something similar for you to adapt...

Make some input images:

convert -size 500x400 xc:black 1.png
convert -size 500x400 xc:red 2.png
convert -size 500x400 xc:green 3.png
convert -size 500x400 xc:blue 4.png

and a background texture:

convert -size 2000x2000 xc:gray +noise gaussian background.png

Now do what you asked, but without intermediate files to disk:

montage [1234].png -background none -tile 1x4 -geometry +0+15 miff:- |
  convert -background none :- -size 15 xc:none -clone 0 +append png: | 
  convert -gravity center background.png :- -composite z.png

enter image description here

The first line lays out the 4 images one above the other, and sends the combined result in a MIFF (Mutiple Image File Format) through a pipe to the next command. The second command reads an image from the pipe and appends a 15 pixel wider "spacer" and then duplicates the first column of images (with clone) and writes as a PNG to the next command through another pipe. The final command reads the 8 small images combined and puts them on a background, centred.

If the right hand column of images is supposed to be the left column reflected - it's hard to tell from your poor example which shows flat black boxes - you may need to change the second line of the command from

  convert -background none :- -size 15 xc:none -clone 0 +append png: | 

to

  convert -background none :- -size 15 xc:none \( -clone 0 -flop \) +append png: | 

Upvotes: 1

Related Questions