RMagick The Polaroid Effect

Do you remember how the old Polaroid* cameras worked? You snapped the picture and then tugged the not-yet-developed print out of the back of the camera. After a minute or so, you carefully peeled the top layer off the print to reveal your "instant" picture. A fresh print always had a little curl in it, so you had to gently bend it back to get it to lay flat. This tutorial shows you how to use RMagick to frame an image in a way that evokes an old Polaroid print. I saw a version of this effect in Scott Kelby's The Photoshop Elements 4 Book for Digital Photographers. Mr. Kelby calls it "The Polaroid Effect," and so shall I.

Here's an example of the effect. On the left is our original image and on the right is the same image after applying the effect.

before polaroid effect after polaroid effect
Before After

This is not an introductory tutorial to RMagick. Like my previous articles, I'm assuming that you are already somewhat familar with routine uses of RMagick, such as how to read and write image files.

To save you some bandwidth I reduced the size of the example images on this page by 50%. The actual images I used are twice the size of the images you see here.

The complete script is here. I'll go through the script a step at a time and explain the important methods and how they're used. After each step, I'll show you what the intermediate image looks like.

Add the border

require "RMagick"

if !ARGV[0]
    puts "Usage: photo.rb path-to-image"
    exit
end

image = Magick::Image.read(ARGV[0]).first

image.border!(18, 18, "#f0f0ff")

The script expects the name of an image file as its only argument.

Polaroid prints always had a narrow white border. Adding the border to the input image is a job for the border method. The border method takes 3 arguments: the width of the left and right borders, the height of the top and bottom borders, and the border color. The width and height are measured in pixels. Here I've specified 18 pixels for both sizes. Why 18 pixels? I'm assuming that the image will be displayed on a 72DPI device, so 18 pixels is 1/4 inch. If you want to render the image on another kind of device (such as a printer) adjust accordingly. For the border color I picked a very light bluish gray. This color looks okay on my Dell monitor but it might be a little too light for viewing on a Mac. If you're reading this with a Powerbook, for example, you may be asking "where's the border?"

Add the curl

image.background_color = "none"

amplitude = image.columns * 0.01
wavelength = image.rows  * 2

image.rotate!(90)
image = image.wave(amplitude, wavelength)
image.rotate!(-90)

Of course, the "curl" in the image is a bit of fool-the-eye trickery. We start by putting a very slight curve in the left and right sides. Curving an image like this is a task for wave. The wave method curves an image along a sine wave.

Since the wave method moves image pixels around it must add new pixels to replace the ones it moves. By default wave uses the background color as the color of the new pixels. Since (as you'll see) we need to create a shadow from the curved image, we need wave to replace the moved pixels with transparent pixels. We do this by setting the background color to "none" (transparent black).

The wave method only alters the image in the y dimension (top-to-bottom) and we need to curve our image in the x dimension (left-to-right). This complicates things a bit, but we can solve it by rotating the image 90°, calling wave, and then rotating the image back to its original position. Since both rotations are exactly 90° this process does not degrade the image.

The wave method takes two arguments, the amplitude (the height of the wave) and the wavelength (the distance from crest to crest). After some experimentation I decided that setting the amplitude to 1% of the image width produced a curve that looked "right" to me. It's an artistic decision. If you want more of a curve try 2% or even 2.5%. The choice of a wavelength is fixed, though. To make a single smooth curve the wavelength must be exactly twice the height of the image.

Make the shadow

shadow = image.flop
shadow = shadow.colorize(1, 1, 1, "gray75")
shadow.background_color = "white"
shadow.border!(10, 10, "white")
shadow = shadow.blur_image(0, 3)

It takes 4 method calls to make the shadow. To complete our "curl" trickery we want the shadow to be the same shape as the image, but curl in the opposite direction, that is, the right side bends inward instead of the left side. We start by calling flop to rotate the image about its vertical axis.

Then we use colorize to replace all the colors in the image with light gray. The colorize method takes 3 or 4 numeric arguments followed by a color name or Magick::Pixel object. The numeric arguments are values between 0 and 1. Each argument represents a percentage of the red, green, blue, and (optionally) alpha channels in the target image. For each channel, the argument represents the percentage of that channel to be replaced by the corresponding channel in the color argument. In this case, since the first three arguments are all 1, colorize replaces 100% of the red, green, and blue channels in the image with the red, green, and blue channels in "gray75". Since the alpha (opacity) channel is not changed the pixels that are transparent in the original image are transparent in the shadow.

"Gray75" is one of the named grays in ImageMagick's palette of named colors. There are 100 such colors, ranging from "gray0" (black) to "gray100" (white). "Gray50" is medium gray, therefore "gray75" is halfway between medium gray and white. Choosing the shadow color is an artistic decision. You may decide you like a darker or lighter shadow.

Finally, we use blur_image to blur the shadow. This method takes two arguments, radius and sigma. I'm sure that radius is vitally important for some applications but for making a shadow I always set it to 0. Since radius is fixed the amount of blurriness is determined by the value of sigma. The higher the value of sigma the blurrier, but as sigma increases so does the amount of time required to run blur_image.

As with so many things in this script, the amount of blurriness is a judgement call. This script uses 3 as the value of sigma to introduce a moderate amount of blurring. Blurring the shadow adds pixels around the edges so we need to make some room for them. Before we call blur_image, we'll use border again to add a small white border around the shadow. Here's the completed shadow.

Composite the image over its shadow

image = shadow.composite(image, -amplitude/2, 5, Magick::OverCompositeOp)

If you've read my earlier article about compositing then there's no surprises here. The only things worth remarking on are the 2nd and 3rd arguments. To make a realistic-looking composition, we placed the image slightly to the left of the shadow and 5 pixels down from the top. (To be honest, I'm not entirely happy with these two arguments. The actual position of the image relative to its shadow seems to depend on the aspect ratio of the image. These two values produce an adequate result for all the images I tried. It seems likely there's a better way of computing the x- and y-offsets but I can't figure it out.)

Add a little rotation

image.rotate!(-5)
image.trim!

out = ARGV[0].sub(/\./, "-print.")
puts "Writing #{out}"
image.write(out)

All that's left is to tip the image back a jaunty -5° and call trim! to eliminate the excess border. Again, the exact amount is an artistic decision. You might like -7.5° better or, if you're applying the effect to a number of images, you might try using a random amount between 4° and 8°. Although in general I took the "less is more" approach with the preceding changes, here it's important to be bold. Using too little rotation causes the image to look annoyingly misaligned instead of casually displayed.

Finally, the script appends "-print" to the image filename and writes it to disk in the same format as the original image. If you need the output image in a different format, here's the place to covert it by specifying a different extension The result image is opaque so you can save it in a format that doesn't support transparency. such as JPEG.

Here's the final image at full-size.

Conclusion

Not bad for about a dozen methods, eh? Try varying some of the calls to see how the result changes. Even with a script this short you can get quite a lot of variation in the output.

I'd like to thank Anthony Thyssen for his help with the Polaroid Effect. Anthony is responsible for the premier ImageMagick examples web site, Examples of ImageMagick Usage. Check it out!

One more thing...

January 31, 2007 - News flash! After reading this tutorial, the ImageMagick developers decided to build support for this effect into ImageMagick itself. This support is available with ImageMagick 6.3.2 and later releases. So, starting with RMagick 1.15.0 you can use the Image#polaroid method to add this effect to your image. The polariod method also allows you to add a caption to your Polaroid-effect image. For more information see the polaroid documentation.

A few more examples

Here's a couple of other images with the Polaroid Effect:

polaroid.rb

require "RMagick"

if !ARGV[0]
    puts "Usage: polaroid.rb path-to-image"
    exit
end

image = Magick::Image.read(ARGV[0]).first

image.border!(18, 18, "#f0f0ff")

# Bend the image
image.background_color = "none"

amplitude = image.columns * 0.01        # vary according to taste
wavelength = image.rows  * 2

image.rotate!(90)
image = image.wave(amplitude, wavelength)
image.rotate!(-90)

# Make the shadow
shadow = image.flop
shadow = shadow.colorize(1, 1, 1, "gray75")     # shadow color can vary to taste
shadow.background_color = "white"       # was "none"
shadow.border!(10, 10, "white")
shadow = shadow.blur_image(0, 3)        # shadow blurriness can vary according to taste

# Composite image over shadow. The y-axis adjustment can vary according to taste.
image = shadow.composite(image, -amplitude/2, 5, Magick::OverCompositeOp)

image.rotate!(-5)                       # vary according to taste
image.trim!

# Add -print to image basename, write to file.
out = ARGV[0].sub(/\./, "-print.")
puts "Writing #{out}"
image.write(out)

Tim - Sep 22, 2006 (updated Jan 31, 2007)


*Polaroid and the other names of products of Polaroid Corporation are trademarks of Polaroid Corporation.