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'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# 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 = Imagedef 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# 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


About

    Buildingsky.net is comprised of a group of computer science enthusiasts. We build, experiment and learn together.

    We are interested in HTML5, Ruby, Javascript, DSP, and finance/economics.

Contact

Projects

Categories