Friday, 22 June 2012

ruby-vips launches

After a lot of fence-sitting by me, Stanislaw Pankevich went ahead and made a proper gem for ruby-vips:

http://rubygems.org/gems/ruby-vips

To celebrate, here's a tiny demo program in Ruby. It's yet another version of vipsthumbnail, though less complete.
#!/usr/bin/env ruby

require 'rubygems'
require 'vips'

include VIPS

# sample image shrinker for ruby-vips

# target size we shrink to ... the image will fit inside a size x size box
size = 100

# we apply a slight sharpen to thumbnails to make then "pop" a bit
mask = [
    [-1, -1,  -1],
    [-1,  32, -1,],
    [-1, -1,  -1]
]
m = Mask.new mask, 24, 0

# show what we do
# verbose = true
verbose = false

ARGV.each do |filename|
    puts "loop #{filename} ..."

    # get the image size ... ,new() only decompresses pixels on access, just
    # opening and getting properties is fast
    #
    # this will decode small images to a memory buffer, large images to a
    # temporary disc file which is then mapped
    #
    # vips also supports sequential mode access, where the image is directly
    # streamed from the source, through the decode library, to the destination,
    # but ruby-vips does not yet expose this, unfortunately
    #
    # see
    #
    #   http://libvips.blogspot.co.uk/2012/02/sequential-mode-read.html
    #
    # enabling this mode would help speed up tiff and png thumbnailing and
    # reduce disc traffic, must get around to adding it to ruby-vips
    a = Image.new(filename)

    # largest dimension
    d = [a.x_size, a.y_size].max

    shrink = d / size.to_f
    puts "shrink of #{shrink}" if verbose

    # jpeg images can be shrunk very quickly during load, by a factor of 2,
    # 4 or 8
    #
    # if this is a jpeg, turn on shrink-on-load
    #
    # a better file type test would be good here :-( vips has a nice one, but
    # it's not exposed in ruby-vips yet
    if filename.end_with? ".jpg"
        if shrink >= 8
            load_shrink = 8
        elsif shrink >= 4
            load_shrink = 4
        elsif shrink >= 2
            load_shrink = 4
        end

        puts "jpeg shrink on load of #{load_shrink}" if verbose

        a = Image.jpeg(filename, :shrink_factor => load_shrink)

        # and recalculate the shrink we need, since the dimensions have changed
        d = [a.x_size, a.y_size].max
        shrink = d / size.to_f
    end

    # we shrink in two stages: we use a box filter (each pixel in output is the
    # average of a m x n box of pixels in the input) to shrink by the largest
    # integer factor we can, then an affine transform to get down to the exact
    # size we need
    #
    # if you just shrink with the affine, you'll get bad aliasing for large
    # shrink factors (more than x2)

    ishrink = shrink.to_i

    # size after int shrink
    id = (d / ishrink).to_i

    # therefore residual float scale (note! not shrink)
    rscale = size.to_f / id

    puts "block shrink by #{ishrink}" if verbose
    puts "residual scale by #{rscale}" if verbose

    # vips has other interpolators, eg. :nohalo ... see the output of "vips list
    # classes" at the command-line
    #
    # :bicubic is well-known and mostly good enough
    a = a.shrink(ishrink).affinei_resize(:bicubic, rscale)

    # this will look a little "soft", apply a gentle sharpen
    a = a.conv(m)

    # finally ... write to the output
    #
    # this call will run the pipeline we have built above across all your CPUs,
    # though for a simple pipeline like this you'll be spending most of your
    # time in the file import / export libraries, which are generally
    # single-threaded
    a = JPEGWriter.new(a, {:quality => 50})
    a.write('test.jpg')

    # force the GC to run and free up any memory vips is hanging on to
    #
    # without this you'll see memuse slowly climb until ruby runs the GC
    # for you
    #
    # something to make ruby-vips drop refs to vips objects explicitly would
    # be nice
    GC.start
end

Speed and memory use

I tested the program like this:
$ for i in {1..100}; do cp ~/pics/wtc.jpg t_$i.jpg; done
$ soak.rb t_*.jpg
(where wtc.jpg is a 10,000 x 10,000 pixel RGB JPEG image)

On my laptop (6,1 macbook running ubuntu 12.04, ruby-vips 0.1.1, libvips 7.28.7) I see:
$ time ./soak.rb t_*.jpg
real  0m47.809s
user  0m33.626s
sys   0m1.516s
ie. about 0.5s real time to do a high-quality shrink of a 10k x 10k rgb jpg

I see a steady ~180mb of memuse as this program runs (watching RES in top). 100mb of this is the vips operation cache, we could disable this to get memuse down further if necessary.

Timing vs. ImageMagick

ImageMagick is also pretty quick on jpg images, since it does the shrink-on-load trick as well:
$ time convert -define jpeg:size=256x256 t_1.jpg -resize 100x100 test.jpg
real    0m0.424s
user    0m0.468s
sys    0m0.028s
However on formats that don't support shrink-on-load, you'll see a large speed difference:
$ time ./soak.rb wtc.tif
real    0m1.560s
user    0m1.096s
sys    0m0.876s
$ time convert wtc.tif -resize 100x100 test.jpg
real    0m11.416s
user    0m9.361s
sys    0m5.192s
ImageMagick has the -thumbnail option, which is quicker, but it does not do the block average and therefore will suffer from moire. I've used -resize since it's closer to what ruby-vips is doing.

3 comments:

  1. A new version of the gem is now done: 0.2.0.

    This disables the operation cache (saving around 100mb from the memuse) and adds support for sequential mode, speeding it up and reducing disc traffic.

    ReplyDelete
  2. ruby-vips looks really neat, but having very little experience with image processing, I am still struggling. I am wondering if you can help me out a little. I'm trying to create a ruby version of the daltonize algorithm for converting images for different forms of colour blindness. ruby-vips seems like a perfect candidate, but I'm stuck at the basics...

    I posted a question on stackoverflow with some links and a little bit about the things I'm stuck with. I guess at this point I'm not even sure which *questions* to ask :)

    http://stackoverflow.com/questions/13801073/first-steps-using-ruby-vips

    Any help would be much appreciated.

    ReplyDelete
  3. Hi Yoav, sorry, I didn't get a mail from blogger telling me that a comment needed moderation, I've only just seen this. I'll check my settings.

    I'll add some stuff to your stackoverflow question, thanks for pointing me to it.

    ReplyDelete