Speed and memory use

John Cupitt edited this page Oct 12, 2017 · 20 revisions

We've written programs using number of different image processing system 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. 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. The Why is libvips quick page tries to explain why libvips does well on this test.

Results

E5-1650 @ 3.20GHz (HP workstation), Ubuntu 17.04

Software Run time
(secs real)
Memory
(peak RSS MB)
Times slower
tiffcp -s 0.08 72
VIPS C/C++ 8.6 0.20 59 1.0
VIPS C/C++ 8.5, JPEG images 0.33 56 1.6
Python VIPS 8.5 0.39 74 1.9
Pillow-SIMD 4.3 see 1 0.40 230 1.9
VIPS command-line 8.5 0.43 42 2.1
GraphicsMagick 1.3.25 0.57 493 2.9
VIPS C/C++ 8.5, one thread 0.64 38 3.1
ymagine git master 15/12/15 see 2 1.06 2.9 3.2 compared to vips-c JPEG
NetPBM 10.0-15.3 0.82 74 3.9
sips 10.4.4 see 6 0.7 est. 268 4.1
ImageMagick 6.9.7-4 0.86 485 4.1
RMagick 2.16.0 ImageMagick 6.9.7-4 0.85 720 4.1
VIPS nip2 8.5 0.89 2821 4.2
OpenCV 2.4.9.1 1.18 215 5.5
ExactImage 0.9.1 see 3 1.47 115 7
Imlib2 1.4.7 see 9 1.53 220 7.3
FreeImage 3.17 see 7 1.59 185 7.5
libgd 2.2.4-2 see 2 2.53 186 8.3 compared to vips-c JPEG
ImageScience 1.3.0 based on FreeImage 3.17, see 7, 8 1.79 435 8.5
OpenImageIO 1.6.17 see 10 1.92 847 9.1
ImageMagick 7.0.2-5 see 11 1.57 733 9.2
gmic 1.7.9 1.9 727 9.24
pike 8.0.388 2.23 329 10.7
ImageJ 1.51 3.08 850 14.7
GEGL 0.3.8 / BABL 0.1.18 see 4 11.9 676 57
Octave 4.0.3-3 see 5 15.2 6146 72

Graphically

This graph was made by running ps very quickly and piping the output to a simple script that calculated total RSS for all processes associated with a task.

This is a fancier one generated by vipsprofile showing the memory behaviour of libvips. The bottom graph shows total memory, the upper traces show threads calculating useful results (green), threads blocked on synchronisation (red) and memory allocations (white ticks). There's a blog post with some more detail on how this was made.

Notes

The benchmarks plus a simple driver program are in a github repository. See the README for details.

Except where noted, all timings are for a 5,000 by 5,000 pixel 8-bit RGB image in uncompressed strip TIFF format. Each test was run with something like:

time ./vips.sh tmp/x.tif tmp/x2.tif

On a quiet system with the quickest real time of five runs recorded. There's no attempt to clear the disc cache, so disc speed should not be a factor. The peak memory column was found by sampling RES with ps using this script. I used the systems as packaged for Ubuntu unless otherwise indicated. I last ran these tests on 12 October 2017 and used the current stable version of every package except where otherwise noted. Tracker was disabled.

The benchmark hardware has six hyperthreaded cores, so systems like Pillow, ymagine, OpenCV and ImageScience, which do not thread automatically, are lower in the table than they should be. On a single-core machine the table would look quite different. There's a separate entry for libvips with a single worker thread for comparison, although even running with just a single worker vips will still use a separate write-behind thread.

This test does a lot of file IO and relatively little processing, which flatters libvips.

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

The VIPS command-line version generates a huge amount of disc traffic which makes it unsuitable for certain applications. This is not really considered in this table.

  1. Pillow is single-threaded, so the fairest comparison for raw processing speed would be against vips-1thread.

  2. libgd and ymagine will not read tiff, so I used jpeg. Their "times slower" column is against vips with a jpeg source. A lot of time is therefore being spent in libjpeg, which is slightly unfair to libvips.

  3. ExactImage will not read tiled tiff, so the benchmark uses a strip tiff for this test.

  4. GEGL does not really focus on batch-style processing -- it targets interactive applications, like paint programs.

  5. Octave aims to be a very high-level prototyping language and is not primarily targeting speed.

  6. sips was run on a different OS X machine. On that machine, vips-c took 0.28s and sips 1.03s, so I scaled the vips time up by the ratio of 0.28 / 1.03. Not very realistic, unfortunately. sips only supports crop and resize, so I didn't time sharpen. The sips resize algorithm is unknown and is probably much fancier than the simple bilinear interpolation used in the other tests.

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

  8. 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.

  9. Imlib2 is spending almost all its time in image input and output.

  10. The OpenImageIO test uses oiiotool, which may not be the best way to test the library.

  11. ImageMagick 7 is rather new and no doubt speed will improve. I built it with the default configuration and -O3.

Implementations

VIPS Python

import sys
import pyvips

im = pyvips.Image.new_from_file(sys.argv[1], access='sequential')
im = im.crop(100, 100, im.width - 200, im.height - 200)
im = im.reduce(1.0 / 0.9, 1.0 / 0.9, kernel='linear')
mask = pyvips.Image.new_from_array([[-1, -1,  -1], 
                                    [-1,  16, -1], 
                                    [-1, -1,  -1]], scale=8)
im = im.conv(mask, precision='integer')
im.write_to_file(sys.argv[2])

ruby-vips

require 'vips'

im = Vips::Image.new_from_file ARGV[0]
im = im.crop 100, 100, im.width - 200, im.height - 200
im = im.similarity scale: 0.9
mask = Vips::Image.new_from_array [
	[-1, -1,  -1], 
	[-1,  16, -1],
	[-1, -1,  -1]], 8
im = im.conv mask, precision: "integer"
im.write_to_file ARGV[1]

php-vips

$im = Vips\Image::newFromFile($argv[1], ["access" => "sequential"]);
$im = $im->crop(100, 100, $im->width - 200, $im->height - 200);
$im = $im->reduce(1.0 / 0.9, 1.0 / 0.9, ["kernel" => "linear"]);
$mask = Vips\Image::newFromArray(
		  [[-1,  -1, -1], 
		   [-1,  16, -1], 
		   [-1,  -1, -1]], 8);
$im = $im->conv($mask, ["precision" => "integer"]);
$im->writeToFile($argv[2]);

lua-vips

local vips = require("vips")

image = vips.Image.new_from_file(arg[1], {access = "sequential"})
image = image:crop(100, 100, image:width() - 200, image:height() - 200)
image = image:reduce(1.0 / 0.9, 1.0 / 0.9, {kernel = "linear"})
mask = vips.Image.new_from_array(
    {{-1,  -1, -1}, 
     {-1,  16, -1}, 
     {-1,  -1, -1}}, 8)
image = image:conv(mask, {precision = "integer"})
image:write_to_file(arg[2]);

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=$(vipsheader -f Xsize $1)
height=$(vipsheader -f Ysize $1)

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

vips crop $1 t1.v 100 100 $width $height
vips reduce t1.v t2.v 1.111 1.111 --kernel linear

cat > mask.con <<EOF
3 3 8 0
-1 -1 -1
-1 16 -1
-1 -1 -1
EOF
vips conv t2.v $2 mask.con

rm t1.v t2.v

VIPS C++

#include <vips/vips8>

using namespace vips;

int
main( int argc, char **argv )
{
        if( VIPS_INIT( argv[0] ) )
                return( -1 );

        VImage in = VImage::new_from_file( argv[1], 
		VImage::option()-> set( "access", VIPS_ACCESS_SEQUENTIAL ) );

        VImage mask = VImage::new_matrixv( 3, 3, 
                -1, -1, -1, -1, 16, -1, -1, -1, -1 );
	mask.set( "scale", 8 ); 

        in.
                extract_area( 100, 100, in.width() - 200, in.height() - 200 ).
		reduce( 1.0 / 0.9, 1.0 / 0.9, VImage::option()->
			set( "kernel", VIPS_KERNEL_LINEAR ) ).
                conv( mask, VImage::option()->
			set( "precision", VIPS_PRECISION_INTEGER ) ). 
                write_to_file( argv[2] );

        return( 0 );
}

VIPS C

// compile with
// gcc -Wall vips.c `pkg-config vips --cflags --libs` -o vips-c

#include <vips/vips.h>

int 
main( int argc, char **argv )
{
        VipsImage *global;
        VipsImage **t;

        if( VIPS_INIT( argv[0] ) )
                return( -1 );

        global = vips_image_new();
        t = (VipsImage **) vips_object_local_array( VIPS_OBJECT( global ), 5 );

        if( !(t[0] = vips_image_new_from_file( argv[1], 
		"access", VIPS_ACCESS_SEQUENTIAL, NULL )) )
                vips_error_exit( NULL );

        t[1] = vips_image_new_matrixv( 3, 3, 
                -1.0, -1.0, -1.0, 
                -1.0, 16.0, -1.0,
                -1.0, -1.0, -1.0 );
        vips_image_set_double( t[1], "scale", 8 );

        if( vips_crop( t[0], &t[2], 
                100, 100, t[0]->Xsize - 200, t[0]->Ysize - 200, NULL ) ||
		vips_reduce( t[2], &t[3], 1.0 / 0.9, 1.0 / 0.9, 
			"kernel", VIPS_KERNEL_LINEAR,
			NULL ) ||
                vips_conv( t[3], &t[4], t[1], 
			"precision", VIPS_PRECISION_INTEGER,
			NULL ) ||
                vips_image_write_to_file( t[4], argv[2], NULL ) )
                vips_error_exit( NULL ); 

        g_object_unref( global );

        return( 0 );
}

PIL (and Pillow)

import sys
from PIL import Image, ImageFilter

im = Image.open(sys.argv[1])
im = im.crop((100, 100, im.width - 100, im.height - 100))
im = im.resize((int(im.width * 0.9), int(im.height * 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

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(uint8(im), argv(){2}); 		  % write back

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=$(vipsheader -f Xsize $1)
height=$(vipsheader -f Ysize $1)

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

set -x

convert "$1[${width}x${height}+100+100]" \
        -filter triangle -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 \
        -filter triangle -resize 90x90% \
        -convolve "-1, -1, -1, -1, 16, -1, -1, -1, -1" \
        $2

ExactImage

#!/bin/bash

width=$(vipsheader -f Xsize $1)
height=$(vipsheader -f Ysize $1)

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

# set -x

econvert -i $1 \
	--crop "100,100,$width,$height" \
	--bilinear-scale 0.9 \
	--convolve "-1, -1, -1, -1, 16, -1, -1, -1, -1" \
	-o $2

GMIC

#!/bin/bash

width=$(vipsheader -f Xsize $1)
height=$(vipsheader -f Ysize $1)
crop_width=$((width - 200))
crop_height=$((height - 200))

gmic \
        -verbose - \
        -input $1 \
        -crop 100,100,$crop_width,$crop_height \
        -resize 90%,90%,1,3,3,1 \
        "(-1,-1,-1;-1,9,-1;-1,-1,-1)" -convolve[-2] [-1] -keep[-2] \
        -type uchar \
        -output $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

OpenImageIO

#!/bin/bash

width=$(vipsheader -f Xsize $1)
height=$(vipsheader -f Ysize $1)

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

# resize with triangle is bilinear

# this will blur rather than sharpen, but the speed should be the same

oiiotool $1 \
	--crop $widthx$height+100+100 --origin +0+0 --fullpixels \
	--resize:filter=triangle 90% \
	--kernel gaussian 3x3 --convolve \
	-o $2

RMagick

#!/usr/bin/ruby

require 'rubygems'
require 'RMagick'
include Magick

im = ImageList.new(ARGV[0])

im = im.shave(100, 100)
im = im.resize(im.columns * 0.9, im.rows * 0.9, filter = TriangleFilter)
kernel = [-1, -1, -1, -1, 16, -1, -1, -1, -1]
im = im.convolve(3, kernel)
                   
im.write(ARGV[1])

ImageJ

makeRectangle(100, 100, 4800, 4800);
run("Crop");
run("Size...", "width=4320 height=4271 constrain average interpolation=Bilinear");
run("Convolve...", "text1=[-1 -1 -1\n-1 16 -1\n-1 -1 -1\n] normalize");
saveAs("tiff", "tmp/x2.tif");

OpenCV

/* compile with:

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

   code from Amadan@shacknews, thank you very much!

 */

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace cv;

int
main (int argc, char **argv)
{
  Mat img = imread (argv[1]);

  if (img.empty ())
    return 1;

  Mat crop = Mat (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);

  imwrite (argv[2], sharp);

  return 0;
}

sips

#!/bin/bash

width=$(vipsheader -f Xsize $1)
height=$(vipsheader -f Ysize $1)

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

resize_width=$((crop_width * 9 / 10))

# set -x

sips \
        --cropToHeightWidth $crop_height $crop_width \
        --resampleWidth $resize_width \
        $1 --out $2 &> /dev/null

pike

#!/usr/bin/pike

int main(int argc, array(string) argv)
{
        object image = Image.load(argv[1]);

        image = image->copy(100, 100,  
                image->xsize() - 101, image->ysize() - 101);

        image = image->scale(0.9);

        image = image->apply_matrix(
                ({({-1,-1,-1}),
                  ({-1,16,-1}),
                  ({-1,-1,-1})}));

        Stdio.write_file(argv[2], Image.TIFF.encode(image));

        return 0;
}

imlib2

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

#include <string.h>
#include <stdlib.h>

#include <Imlib2.h>

int
main( int argc, char **argv )
{
	Imlib_Image image;
	int width;
	int height;
	char *tmp;
		    
	if( argc != 3 ) 
		exit( 1 );

	if( !(image = imlib_load_image( argv[1] )) )
		exit( 1 );

	/* set the image we loaded as the current context image to work on 
	 */
	imlib_context_set_image( image );
	width = imlib_image_get_width();
	height = imlib_image_get_height();
	if( !(image = imlib_create_cropped_scaled_image( 100, 100, 
		width - 200, height - 200,
		(width - 200) * 0.9,
		(height - 200) * 0.9 )) )
		exit( 1 );
	imlib_free_image();
	imlib_context_set_image( image );

	imlib_image_sharpen( 1 );

	if( (tmp = strrchr( argv[2], '.' )) )
		imlib_image_set_format( tmp + 1 );

	/* save the image 
	 */
	imlib_save_image( argv[2] );
	imlib_free_image();

	return( 0 );
}

gd

// compile with
// gcc -Wall gd.c `pkg-config gdlib --cflags --libs` -o gd

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

#include <gd.h>

int
main( int argc, char **argv )
{
	FILE *fp;
	gdImagePtr original, cropped,resized;
	gdRect crop;

	if( argc != 3 ) {
		printf( "usage: %s in-jpeg out-jpeg\n", argv[0] );
		exit( 1 );
	}

	if( !(fp = fopen( argv[1], "r" )) ) {
		printf( "unable to open \"%s\"\n", argv[1] );
		exit( 1 );
	}
	if( !(original = gdImageCreateFromJpeg( fp )) ) {
		printf( "unable to load \"%s\"\n", argv[1] );
		exit( 1 );
	}
	fclose( fp );

	crop.x = 100;
	crop.y = 100;
	crop.width = original->sx - 200;
	crop.height = original->sy - 200;
	cropped = gdImageCrop( original, &crop );
	gdImageDestroy( original );
	original = 0;
	if( !cropped ) {
		printf( "unable to crop image\n" ); 
		exit( 1 );
	}

	resized = gdImageScale( cropped, crop.width * 0.9, crop.height * 0.9 );
	gdImageDestroy( cropped );
	cropped = 0;
	if( !resized ) {
		printf( "unable to resize image\n" ); 
		exit( 1 );
	}

	//gdImageSharpen is extremely slow
	gdImageSharpen( resized, 75 );

	if( !(fp = fopen( argv[2], "w" )) ) {
		printf( "unable to open \"%s\"\n", argv[2] );
		exit( 1 );
	}
	gdImageJpeg( resized, fp, -1 );
	fclose( fp );

	gdImageDestroy( resized );

	return( 0 ); 
}

GEGL

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

 */

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

#include <gegl.h>

static void 
null_log_handler (const gchar *log_domain, 
		  GLogLevelFlags log_level, 
		  const gchar *message, 
		  gpointer user_data)
{
}

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

  gegl_init (&argc, &argv);

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

  g_log_set_handler ("GEGL-load.c", 
    G_LOG_LEVEL_WARNING | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION, 
    null_log_handler, NULL);
  g_log_set_handler ("GEGL-gegl-tile-handler-cache.c", 
    G_LOG_LEVEL_WARNING | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION, 
    null_log_handler, NULL);

  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-ratio",
                               "x", 0.9,
                               "y", 0.9,
                               "sampler", GEGL_SAMPLER_LINEAR,
                               NULL);
                
  sharp = gegl_node_new_child (gegl,
                               "operation", "gegl:unsharp-mask",
                               "std-dev", 1.0, // diameter 7 mask in gegl
                               NULL);

  save = gegl_node_new_child (gegl,
                              "operation", "gegl:save",
                              //"operation", "gegl:png-save",
                              //"bitdepth", 8,
                              "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);
}
Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.