Speed and Memory Use

From VipsWiki

Revision as of 08:09, 4 April 2012; view current revision
←Older revision | Newer revision→
Jump to: navigation, search

We've written programs to load a TIFF image, crop 100 pixels off every edge, shrink by 10% with bilinear interpolation, sharpen with a 3x3 convolution and save again using a number of different image processing systems. It's a trivial test but it does give some idea of the speed and memory behaviour of these libraries (and it's also quite fun to compare the code).

See also our main Benchmarks page for a more complex benchmark and timings on a variety of machines.

Contents

Results

2 x Opteron 254 workstation, 2.7 GHz

Software Run time (secs real) Memory (peak RSS MB)
VIPS C++ 7.28 1.0 10
VIPS Python 7.28 1.0 12
ruby-vips 7.28 1.0 12
OpenCV 2.1 1.2 185
VIPS command-line 7.28 2.0 12
VIPS nip2 7.28 2.0 39
NetPBM 10 3.6 70
GraphicsMagick 1.3.12 3.8 240
PIL 1.1.7 4.3 188
RMagick 2.13.1 (ImageMagick 6.5.7) 5.3 670
ImageMagick 6.6.0 5.9 480
FreeImage 3.10 (incomplete) 7.0 180
ImageScience 1.2.1 (based on FreeImage 3.10, incomplete) 8.0 260
Octave 3.0.1 64 (est.) 8500 (est.)

Notes

All timings are for a 5,000 by 5,000 pixel 8-bit RGB image in uncompressed tiled TIFF format, 128 by 128 pixel tiles. Each test was run with something like:

time ./vips.sh wtc_tiled_small.tif wtc2.tif

On a quiet system with the quickest real time of three runs recorded. I didn't try to clear the disc cache so the disc speed should not be a factor.

OpenCV is extremely fast and vips only scrapes a win because it can use more than one CPU. OpenCV would win on a single-cpu machine, though conversely I suppose vips would win by a larger margin on a four-cpu machine.

Some systems, like ImageScience and nip2, have relatively long start-up times and this hurts their position in the table.

ruby-vips is still experimental and the API is not yet frozen.

Some systems, such as the VIPS command-line and NetPBM, generate huge amounts of disc traffic which make them unsuitable in certain applications. This is not really considered in this table.

Octave aims to be a very high-level prototyping language and is not primarily targeting speed. I timed a 2,000 by 2,000 pixel monochrome JPEG and extrapolated from that.

Both ImageMagick and GraphicsMagick were compiled with Q16, ie. 16 bits per pixel.

FreeImage does not have a sharpening or convolution operation so I skipped that part of the benchmark.

NetPBM will not read tiled tiff so we used a striped tiff file for this one.

ImageScience is based on FreeImage and therefore does not support sharpening, so I've skipped that part of the test. The resize() method is always bicubic which is a little unfair as the other benchmarks here use bilinear.

Implementations

Here it is in a variety of image processing systems.

VIPS Python

#!/usr/bin/python

import sys
from vipsCC import *

im = VImage.VImage (sys.argv[1])
im = im.extract_area (100, 100, im.Xsize () - 200, im.Ysize () - 200)
im = im.affine (0.9, 0, 0, 0.9, 0, 0, 0, 0,
        int (im.Xsize() * 0.9), int (im.Ysize() * 0.9))
mask = VMask.VIMask (3, 3, 8, 0, 
		  [-1, -1, -1, 
		   -1,  16, -1, 
		   -1, -1, -1])
im = im.conv (mask)
im.write (sys.argv[2])

ruby-vips

#!/usr/bin/ruby

require 'rubygems'
require 'vips'
include VIPS

im = Image.new(ARGV[0])

im = im.extract_area(100, 100, im.x_size - 200, im.y_size - 200)
im = im.affinei(:bilinear, 0.9, 0, 0, 0.9, 0, 0)
mask = [
    [-1, -1,  -1],
    [-1,  16, -1,],
    [-1, -1,  -1]
]
m = Mask.new mask, 8, 0 
im = im.conv(m)

im.write(ARGV[1])

VIPS nip2

#!/home/john/vips/bin/nip2 -s

main
  = error "usage: infile -o outfile", argc != 2
  = (sharpen @ shrink @ crop) (Image_file argv?1)
{
  crop x = extract_area 100 100 (x.width - 200) (x.height - 200) x;
  shrink = resize Interpolate_bilinear 0.9 0.9;
  sharpen = conv (Matrix_con 8 0 [[-1, -1, -1], [-1, 16, -1], [-1, -1, -1]]);
}

VIPS command-line

#!/bin/bash

width=`header -f Xsize $1`
height=`header -f Ysize $1`

width=$((width - 200))
height=$((height - 200))

set -x

vips im_extract_area $1 t1.v 100 100 $width $height
vips im_affinei_all t1.v t2.v bilinear 0.9 0 0 0.9 0 0
cat > mask.con <<EOF
3 3 8 0
-1 -1 -1
-1 16 -1
-1 -1 -1
EOF
vips im_conv t2.v $2 mask.con
rm t1.v t2.v mask.con

VIPS C++

#include <vips/vips>

int main (int argc, char **argv)
{
        vips::VImage in (argv[1]);
        vips::VIMask mask (3, 3, 8, 0,
                -1, -1, -1, -1, 16,-1, -1, -1, -1);

        in.
                extract_area (100, 100, in.Xsize () - 200, in.Ysize () - 200).
                affine (0.9, 0, 0, 0.9, 0, 0,
                        0, 0, in.Xsize () * 0.9, in.Ysize () * 0.9).
                conv (mask).
                write (argv[2]);

        return 0;
}

PIL

#!/usr/bin/python 

import Image, sys
import ImageFilter 

im = Image.open (sys.argv[1])
im = im.crop ((100, 100, im.size[0] - 100, im.size[1] - 100))
im = im.resize ((int (im.size[0] * 0.9), int (im.size[1] * 0.9)),
        Image.BILINEAR) 
filter = ImageFilter.Kernel ((3, 3),
              (-1, -1, -1,
               -1, 16, -1,
               -1, -1, -1))
im = im.filter (filter)
im.save (sys.argv[2])

Octave

#!/usr/bin/octave -qf

pkg load image

im = imread(argv(){1});
im = im(101:end-100, 101:end-100);        % Crop
im = imresize(im, 0.9, 'linear');         % Shrink    
myFilter = [-1 -1 -1
        -1 16 -1
        -1 -1 -1]; 
im = conv2(double(im), myFilter);         % Sharpen
im = max(0, im ./ (max(max(im)) / 255));  % Renormalize
imwrite(argv(){2}, uint8(im));           % Write back again

ImageMagick

#!/bin/bash

# we crop on load, it's a bit quicker and saves some memory
# we can't crop 100 pixels with the crop-on-load syntax, so we have to
# find the width and height ourselves
width=`header -f Xsize $1`
height=`header -f Ysize $1`

width=$((width - 200))
height=$((height - 200))

set -x

convert "$1[${width}x${height}+100+100]" \
        -resize 90x90% \
        -convolve "-1, -1, -1, -1, 16, -1, -1, -1, -1" \
        $2

GraphicsMagick

#!/bin/bash

set -x

# GraphicsMagick does not have crop-on-load so we use -shave instead
gm convert $1 \
        -shave 100x100 \
        -resize 90x90% \
        -convolve "-1, -1, -1, -1, 16, -1, -1, -1, -1" \
        $2

FreeImage

/* Compile with:

   gcc freeimage.c -lfreeimage

 */

#include <FreeImage.h>

int
main (int argc, char **argv)
{       
  FIBITMAP *t1;
  FIBITMAP *t2;
  int width;
  int height;

  FreeImage_Initialise (FALSE);

  t1 = FreeImage_Load (FIF_TIFF, argv[1], TIFF_DEFAULT);

  width = FreeImage_GetWidth (t1); 
  height = FreeImage_GetHeight (t1); 

  t2 = FreeImage_Copy (t1, 100, 100, width - 100, height - 100); 
  FreeImage_Unload (t1); 

  t1 = FreeImage_Rescale (t2, (width - 200) * 0.9, (height - 200) * 0.9,
                          FILTER_BILINEAR);
  FreeImage_Unload (t2); 

  /* FreeImage does not have a sharpen operation, so we skip that.
   */

  FreeImage_Save (FIF_TIFF, t1, argv[2], TIFF_DEFAULT);
  FreeImage_Unload (t1); 

  FreeImage_DeInitialise ();

  return 0;
}      

NetPBM

#!/bin/bash

cat > mask <<EOF
P2
3 3
32
14 14 14 
14 48 14
14 14 14
EOF

tifftopnm $1 | \
  pnmcut -left 100 -right -100 -top 100 -bottom -100 | \
  pnmscale 0.9 | \
  pnmconvol mask | \
  pnmtotiff -truecolor -color > $2

ImageScience

#!/usr/bin/ruby

require 'rubygems'
require 'image_science'

ImageScience.with_image(ARGV[0]) do |img|
    img.with_crop(100, 100, img.width() - 100, img.height() - 100) do |crop|
        crop.resize(crop.width() * 0.9, crop.height() * 0.9) do |small|
            small.save(ARGV[1])
        end
    end
end

RMagick

#!/usr/bin/ruby

require 'rubygems'
require 'RMagick'
include Magick

im = ImageList.new(ARGV[0])

im = im.shave(100, 100)
im = im.scale(0.9)
kernel = [-1, -1, -1, -1, 16, -1, -1, -1, -1]
im = im.convolve(3, kernel)
                   
im.write(ARGV[1])

OpenCV

/*
   g++ -g -Wall opencv.cc `pkg-config opencv --cflags --libs`
 */     

#include <cv.h> 
#include <highgui.h>
                  
using namespace cv;
                   
int  
main (int argc, char **argv)
{
  Ptr < IplImage > t1;

  if (!(t1 = cvLoadImage (argv[1])))
    return 1;   
  Mat img (t1);

  Mat crop (img, Rect (100, 100, img.cols - 200, img.rows - 200));

  Mat shrunk;
  resize (crop, shrunk, Size (0, 0), 0.9, 0.9);

  float m[3][3] = { {-1, -1, -1}, {-1, 16, -1}, {-1, -1, -1} };
  Mat kernel = Mat (3, 3, CV_32F, m) / 8.0; 

  Mat sharp;
  filter2D (shrunk, sharp, -1, kernel, Point (-1, -1), 0, BORDER_REPLICATE);

  CvMat cvimg = sharp;
  cvSaveImage (argv[2], &cvimg);
        
  return 0;
}     

GEGL

I've not included a timing for gegl in the table at the top since this sort of batch processing is not what gegl was designed for and gegl has not yet had much optimisation. It's interesting to see the code though. Gegl doesn't seem to have a general convolve operator yet, so I used usharp with a very small radius instead.

For reference, gegl-0.1.6 with default settings takes about 50s and needs about 300mb of memory. You can get this down to 40s by tuning a few environment variables. Bear in mind that gegl uses PNG (rather slow), writes 16-bits by default (again, rather slow), maintains an alpha channel (including a lot of work in pre-multiplying), and caches calculated tiles on disc for reuse (unnecessary in batch processing) making the speed comparison very unclear.

/* compile with
 
   gcc -g -Wall gegl.c `pkg-config gegl --cflags --libs`

 */

#include <stdio.h>
#include <stdlib.h>

#include <gegl.h>

int
main (int argc, char **argv)
{
  GeglNode *gegl, *load, *crop, *scale, *sharp, *save;

  g_thread_init (NULL);
  gegl_init (&argc, &argv);

  if (argc != 3) 
    {           
      fprintf (stderr, "usage: %s file-in file-out\n", argv[0]);
      exit (1);
    }
        
  gegl = gegl_node_new ();
        
  load = gegl_node_new_child (gegl,
                              "operation", "gegl:load",
                              "path", argv[1], 
                              NULL);

  crop = gegl_node_new_child (gegl, 
                              "operation", "gegl:crop",
                              "x", 100.0,
                              "y", 100.0,
                              "width", 4800.0, 
                              "height", 4800.0, 
                              NULL);
                
  scale = gegl_node_new_child (gegl,
                               "operation", "gegl:scale",
                               "x", 0.9,
                               "y", 0.9,
                               "filter", "linear", 
                               "hard-edges", FALSE, 
                               NULL);
                
  sharp = gegl_node_new_child (gegl,
                               "operation", "gegl:unsharp-mask",
                               "std-dev", 0.1, 
                               NULL);

  save = gegl_node_new_child (gegl,
                              "operation", "gegl:png-save",
                              "path", argv[2], 
                              NULL);
                
  gegl_node_link_many (load, crop, scale, sharp, save, NULL);
        
  gegl_node_process (save);
                
  g_object_unref (gegl);

  gegl_exit ();

  return (0);
}
Personal tools