Render Great-looking Collages with Ruby and RMagick

Yesterday I was glancing Douglas Bowman’s Photo gallery and a fun idea came to me. Inspired by using photo slides on a light table, Douglas employs a unified theme and feel across his gallery. Kindly he released the theme free for use and is available for download on his website.

Douglas is an incredible graphic artist and prepares lovely photo collages for each gallery in his album painstakingly by hand.

What if we wanted to to generate such a collage on the fly from random images in our gallery? What follows is some insight into using RMagick to accomplish just such a task.


Preface

In this tutorial we will learn how to use some of RMagicks compositing features to compose a collage of images randomly picked from a psuedo photo gallery.

The collage is made up of 2 parts, the main image and the slides. In a gallery application you would allow a user to select the main image which showcases the gallery category. The slides could be chosen too or just randomly picked.

What you will need

In this tutorial I am using Douglas Bowman’s templates from his gallery theme. Here are the two images you will need, in PNG-24 format.

Here is the finished collage.rb file you may use to follow along.

Why PNG?

PNG is a graphic format which supports alpha channel opacities. With PNG we can make varying levels of transparency, a drop shadow for instance 20% opaque, whereas GIF only gives us 1 level of transparency, 100% transparent or 100% opaque and although that might have been cool in 1998 this is the year 2006.

Now semi transparency in PNG is not really used much on the web since IE doesnt support it, but since we are using PNG for compositing purposes and then rendering the finished output to JPG we can really take advantage of it and create shortcuts which would otherwise cost CPU time creating alpha masks in RMagick.

Nitty Gritty

Step 1. Resize and Composite Main image

First, we will add a main image to the template. The chosen image will need to be resized to fit within the border of our template (710×200). We will use the crop_resized method to do this as it will crop and resize our image while maintaining the aspect ratio of the original image. crop_resized is the perfect method to use for generating thumbnails as well.

# load the template
template = Image.read('template.png').first

# load an image as our 'Main Image'
photo = Image.read("main_image.png").first

# resize photo to fit within template
photo.crop_resized!(710,200)

# composite photo over template offsetting 10x10 for the template border
template.composite!(photo, 10, 10, OverCompositeOp)

# save composite image to JPG
template.write("collage.jpg")

In the above code we offset the photo 10×10 pixels to make up for the 10 pixel width border of our template.

We used the OperCompositeOp constant because we simply wanted to place one layer over the other. If you are familiar with Adobe Photoshop layer modes you will be happy to know that most, if not all, of the layer modes are supported (MultiplyCompositeOp, ScreenCompositeOp, etc). A full list of composite operation constants is available in the RMagick API documentation

The main image cropped and resized and composited over our template.

Step 2. Layout Slides

Next we will layout the empty slides and composite them over our main image. We will use a custom method called backandforth to rotate the slide +/- 45 degrees and then apply a drop shadow effect with the shadow method. The shadow method generates an image which contains only the shadow of your original image so you must recomposite your original image over top of the generated shadow image. It is useful to create a new workspace layer which is big enough to encompass both the original image and the extra offset of the shadow image.

backandforth method
def backandforth(degree)
  polarity = rand(2) * -1
  return rand(degree) * polarity if polarity < 0
  return rand(degree)
end
create_slide method
def create_slide
  # load slide border image
  slide = Image.read('slide.png').first

  # rotate slide +/- 45 degrees
  rotation = backandforth(45)
  slide.rotate!(rotation)

  # create workspace to apply shadow
  workspace = Image.new(slide.columns+5, slide.rows+5) { self.background_color = 'transparent' }
  shadow = slide.shadow(0, 0, 0.0, '20%')

  # offset shadow by 3 pixels
  workspace.composite!(shadow, 3, 3, OverCompositeOp)
  workspace.composite!(slide, NorthWestGravity, OverCompositeOp)

  return workspace
end

Now we will layout our empty slides across our template . I will lay them out from right-to-left overlapping and with a slight vertical and horizontal random jitter. Since we are multiplying the loop position by 100, which is less then the width of the slide, they will be slightly overlapped.

lay out slides across template
# iterate backwards from 2..0
2.downto(0) do |i|
  slide = create_slide

  # multiply the loop position i by 100 to get the x position.
  # looping backwards from 2..0 will layout the slides from right-to-left
  template.composite!(slide, i * 100 + rand(15), 150 + rand(15), OverCompositeOp)
end

# save the collage progress so far
template.write('collage.jpg')
Starting to come together. The slide borders layed out and composited over our template.

Step 3. Add Slide images and Composite Alpha Channel

Now that we have the slides positioned correctly, it is time to add the slide image to the empty slide. We will start by loading the image filename passed into the updated create_slide method and store it in the photo variable. Next we will crop and resize the image with the crop_resized method we used before for the main image. The resized image is a thumbnail of the original and will fit nicely within the slide border.

Real slides are made out of semi transparent film and we want to achieve that effect in our digital slides. Before we composite the photo layer on top of the slide border we need to make the photo slightly transparent and we do this by compositing a grey alpha mask over it with the CopyOpacityCompositeOp mode. Pay special attention to the matte method. The photo image will need its matte set to true so that it will respect the alpha levels of its pixels. The mask however will need its matte set to false. This forces the CopyOpacityCompositeOp to copy the masks grey scale information and map it to the photo’s pixel alpha intensities. If the mask’s matte is not set it false it would attempt to copy its alpha levels instead, which in this case, we do not want.

After applying the mask we simply composite the photo and slide onto the slide_background and continue with the rotate and shadow effect.

updated create_slide method
def create_slide(image)
  slide = Image.read('slide.png').first
  slide_background = Image.new(slide.columns, slide.rows) { self.background_color = 'transparent' }
  photo = Image.read(image).first

  mask_fill = GradientFill.new(0, 0, 0, 88, '#FFFFFF', '#F0F0F0')
  mask = Image.new(photo.columns, photo.rows, mask_fill)
  # create thumbnail sized square image of photo
  photo.crop_resized!(88,88)

  # apply alpha mask to slide
  photo.matte = true
  mask.matte = false
  photo.composite!(mask, 0, 0, CopyOpacityCompositeOp)

  # composite photo and slide on transparent background
  slide_background.composite!(photo, 16, 16, OverCompositeOp)
  slide_background.composite!(slide, 0, 0, OverCompositeOp)

  # rotate slide +/- 45 degrees
  rotation = backandforth(45)
  slide_background.rotate!(rotation)

  # create workspace to apply shadow
  workspace = Image.new(slide_background.columns+5, slide_background.rows+5) { self.background_color = 'transparent' }
  shadow = slide_background.shadow(0, 0, 0.0, '20%')
  workspace.composite!(shadow, 3, 3, OverCompositeOp)
  workspace.composite!(slide_background, NorthWestGravity, OverCompositeOp)

  return workspace
end

Now that our we have a working slide function we need to lay them out across our template . I will assume that we have random selected 3 images and stored their filenames in an images array. Looping through the images array I will lay them out from right-to-left overlapping and with a slight vertical and horizontal random jitter. Since we are multiplying the loop position by 100, which is less then the width of the slide, they will be slightly overlapped.

lay out slides across template
# images array holds 3 image filenames

# iterate backwards from 2..0 and load images into create_slide method
2.downto(0) do |i|
  slide = create_slide(images[i])
  template.composite!(slide, i * 100 + rand(15), 150 + rand(15), OverCompositeOp)
end

# save the finished collage
template.write('collage.jpg')
Finished. Semi transparent slide images added to our template.

Step 4: Experiment!

Now that we know how to composite layers and add alpha masks we should be able to accomplish a great deal. Just think in layers. Think of the steps you would take in Photoshop to achieve a certain effect. RMagick isn’t just for converting images to different formats and creating thumbnails.

Experiment with different styles and layouts.

Continued Reading

Want to generate random collages from your Flickr Album? Check out Part 2 of this series.

Part 2: Using RMagick with Flickr



About this entry


Comments

  1. Avatar

    Mislav

    Posted: 1 day later:

    Unbelievable! This is great! Corban, thank you.

    I would like to see more stuff like this in the future. Subscribing already

  2. Avatar

    Corban Brook

    Posted: 1 day later:

    Thanks Mislav,

    I plan on doing more RMagick articles as I learn more. It is quickly becoming apparent the most everything you can do in Adobe Photoshop can be generated using the above techiques. There is sure to be some fun ideas to try out in the future.

  3. Avatar

    Garth

    Posted: 1 day later:

    Great article. Couldn’t get it working tho’. Could you include some stock images to download?

  4. Avatar

    Sarah

    Posted: 1 day later:

    Great tutorial; I can’t wait to try this out for my site!

  5. Avatar

    John Munsch

    Posted: 1 day later:

    stock.xchng will happily give you all the images you want Garth. Here’s a link to my own little mini-gallery of images but you can search for whatever appeals to you to play with:

    http://www.sxc.hu/browse.phtml?f=profile&;l=muir_woods

  6. Avatar

    Sarah

    Posted: 2 days later:

    It’s me again. I encountered a problem:

    collage.rb:38:in `shadow’: not enough memory to continue (NoMemoryError) from /home/sarah/bin/collage.rb:38:in `create_slide’ from /home/sarah/bin/collage.rb:62 from /home/sarah/bin/collage.rb:61

    I looked around online and people were having this trouble with RMagick used in Gruff Graphs on Ubuntu systems. The solution listed was to make sure all number values passed in were numbers, and not numbers in strings. However, that didn’t seem to be the problem. When I checked RMagick’s docs for the shadow method, here was its description:

    image.shadow(x_offset=4, y_offset=4, sigma=4.0, opacity=1.0) -> anImage “sigma – The standard deviation of the Gaussian operator used to produce the shadow. The higher the number, the “blurrier” the shadow, but the longer it takes to produce the shadow. Must be > 0.0.”

    When I looked at line 38 in collage.rb, however, the sigma was 0.0:

    shadow = slide_background.shadow(0, 0, 0.0, ‘20%’)

    I changed it to 0.1 and everything’s fine. :)

  7. Avatar

    Tim Hunter

    Posted: 2 days later:

    Beautiful! This is a wonderful example of using RMagick. Thanks for all the work you put into this tutorial. Encore!

  8. Avatar

    Corban Brook

    Posted: 2 days later:

    Sarah:

    Thank you for this fix. I think there must be some small inconsistencies between different versions of ImageMagick or GraphicsMagick on different platforms.

    I would love to see your results, and perhaps post them at the end of the article.

  9. Avatar

    Chris

    Posted: 3 days later:

    For another source of images, there’s a great recipe in the “Ruby Cookbook” about pulling images with a certain keyword using the flickr API .

  10. Avatar

    Corban Brook

    Posted: 4 days later:

    Currently I am experimenting with rflickr and will be releasing another article this week about using rflickr with rmagick.

    I already have some working code which reads in random images from a flickr photoset of your choosing and then generates a collage.

  11. Avatar

    ben

    Posted: 5 days later:

    you got me the easy way to start with RMagick

  12. Avatar

    Alex Wayne

    Posted: 9 days later:

    Just wanted to let you know that you inspired me. After stumbling on this article, I decided to extend my FlexImage plugin with a handful of image processing functions.

    It still doesn’t do anything quite as complex as your example, but it handles most other things ok.

    Thanks for the kick in the pants.

  13. Avatar

    Matte

    Posted: 10 days later:

    Thanks for your useful tutorial. I use RMagick in my project, but this is a great way to show slide in web site! Add creativity to it! It’s also very useful the RMagick command crop_resized! to make thumbnails!

  14. Avatar

    topfunky

    Posted: 12 days later:

    Great tutorial!

    I love to see people doing creative things with dynamic graphics generation. Hopefully the web will be much more graphical, customizable, and dynamic in the future!

About

    Buildingsky.net is comprised of Corban Brook and Maciek Adwent. We build experimental web applications.

    We are interested in computer science, ruby-lang, javascript, web technologies, audio synthesis, finance/economics.

Contact

Projects

Categories