Skip to content

Instantly share code, notes, and snippets.

@zaeleus zaeleus/ Secret
Created Nov 26, 2011

What would you like to do?
Codebrawl: Content-aware image cropping with ChunkyPNG

Content-aware image cropping with ChunkyPNG

cake (content-aware kropping [sic] by entropy) is a greedy image cropper inspired by Reddit's scraper library. It works by moving a sliding window over a given image that is continually reduced to the size of a specified crop width and height. Two copies of the current window boundary are made, and for each, 16px are cropped on opposite sides (left-right on the first pass and top-bottom on the second). The entropies of the cropped windows are compared, and the smaller of the two will be discarded, moving the window toward the larger.

The current "smart" implementation takes the smaller dimension of the input image as the crop width and height. The optimal crop position is found, and the image is then scaled to 100x100 px.


Make sure that you have chunky_png installed, and you're good to go. This has also only been tested on Ruby 1.9.3p0.


Usage: cake.rb [options] <input ...>
    -o, --output <string>            Specify output file
    -s, --smart                      Selects the largest, optimal square to crop and rescales. Good for thumbnails.
        --debug                      Draws the bounding crop area instead of cropping the image
    -d <integer>x<integer>,          Specify output dimensions (width x height) in pixels


$ ruby cake.rb -s cat.png

input / output

$ ruby cake.rb --debug -d 240x240 soundlab.png

input (medium 640) / output


Because of the large number of colors in an image, it would probably be more ideal to do color quantization before processing.

require 'chunky_png'
require 'optparse'
class Cake
attr_accessor :debug
def initialize(file)
@image = ChunkyPNG::Image.from_file(file)
def crop_and_scale(new_width = 100, new_height = 100)
width, height = @image.width, @image.height
if width > height
width = height
height = width
result = crop(width, height)
result.resample_bilinear!(new_width, new_height) unless debug
def crop(crop_width = 100, crop_height = 100)
x, y, width, height = 0, 0, @image.width, @image.height
slice_length = 16
while (width - x) > crop_width
slice_width = [width - x - crop_width, slice_length].min
left = @image.crop(x, 0, slice_width, @image.height)
right = @image.crop(width - slice_width, 0, slice_width, @image.height)
if entropy(left) < entropy(right)
x += slice_width
width -= slice_width
while (height - y) > crop_height
slice_height = [height - y - crop_height, slice_length].min
top = @image.crop(0, y, @image.width, slice_height)
bottom = @image.crop(0, height - slice_height, @image.width, slice_height)
if entropy(top) < entropy(bottom)
y += slice_height
height -= slice_height
if debug
return @image.rect(x, y, x + crop_width, y + crop_height, ChunkyPNG::Color::WHITE)
@image.crop(x, y, crop_width, crop_height)
def histogram(image)
hist =
image.height.times do |y|
image.width.times do |x|
hist[image[x,y]] += 1
def entropy(image)
hist = histogram(image.grayscale)
area = image.area.to_f
-hist.values.reduce(0.0) do |e, freq|
p = freq / area
e + p * Math.log2(p)
options = { :width => 100, :height => 100 }
option_parser = do |opts|
opts.banner = "Usage: #{__FILE__} [options] <input ...>"
opts.on('-o', '--output <string>',
'Specify output file') do |filename|
options[:output] = filename
opts.on('-s', '--smart',
'Selects the largest, optimal square to crop and then resamples. Good for thumbnails.') do
options[:smart] = true
opts.on(nil, '--debug',
'Draws the bounding crop area instead of cropping the image') do
options[:debug] = true
opts.on('-d', '--dimensions <integer>x<integer>',
'Specify output dimensions (width x height) in pixels') do |dim|
options[:width], options[:height] = dim.split('x').map(&:to_i)
if ARGV.empty?
exit 1
ARGV.each_with_index do |file, i|
c =
c.debug = true if options[:debug]
output = if options[:output]
suffix = "_#{i}" if ARGV.size > 1
"#{File.basename(options[:output], '.png')}#{suffix}.png"
"#{File.basename(file, '.png')}_cropped.png"
if options[:smart]
c.crop_and_scale(options[:width], options[:height]).save(output)
c.crop(options[:width], options[:height]).save(output)

This comment has been minimized.

Copy link

mikewoodhouse commented Nov 28, 2011

This really annoying - it uses entropy (in a way that I wanted to but couldn't figure out) but then crops to a proportion and then resizes, which isn't how I read the challenge. So plus point for the math, minus point for the resize.


This comment has been minimized.

Copy link
Owner Author

zaeleus commented Nov 28, 2011

@mikewoodhouse, I do the resize because the problem says not to build the solution around the sample images (or maybe it meant not to hardcode crop areas). But if I had a 1600x1600 image, a 100x100 crop would be rather useless.

My solution does support cropping without resizing (actually, by default, when you don't specify the --smart option), but I found what I submitted to produce better results. Here are the default outputs without resizing.

cat.png: output
dog.png: output
duck.png: output


This comment has been minimized.

Copy link

rogerbraun commented Nov 28, 2011

Hey, you cheated ;-) The results for the cat and the ducks are great, but the dog not so much. I like the idea of using entropy. The code is easy to read and I can imagine using the smart mode in a real app. You might want to make a gem from it.


This comment has been minimized.

Copy link

jeffkreeftmeijer commented Dec 5, 2011

The idea of the contest was that you could do anything (including a bit of resizing) to get a nice crop from the input images. I realize the contest rules were a bit unclear, as the rest of the entries focussed on cropping only. I'm sorry about that.

That said, it was about finding the point of interest in images rather than just cropping and Michael did just that, his implementation just had some extra fancy sauce.

Again, sorry for the confusing contest description, I'll do better next year. ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.