Skip to content

Embed URL

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
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.

Requirements

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

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
        --dimensions

Examples

$ ruby cake.rb -s cat.png

input / output

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

input (medium 640) / output

Improvements

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)
end
def crop_and_scale(new_width = 100, new_height = 100)
width, height = @image.width, @image.height
if width > height
width = height
else
height = width
end
result = crop(width, height)
result.resample_bilinear!(new_width, new_height) unless debug
result
end
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
else
width -= slice_width
end
end
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
else
height -= slice_height
end
end
if debug
return @image.rect(x, y, x + crop_width, y + crop_height, ChunkyPNG::Color::WHITE)
end
@image.crop(x, y, crop_width, crop_height)
end
private
def histogram(image)
hist = Hash.new(0)
image.height.times do |y|
image.width.times do |x|
hist[image[x,y]] += 1
end
end
hist
end
# http://www.mathworks.com/help/toolbox/images/ref/entropy.html
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)
end
end
end
options = { :width => 100, :height => 100 }
option_parser = OptionParser.new do |opts|
opts.banner = "Usage: #{__FILE__} [options] <input ...>"
opts.on('-o', '--output <string>',
'Specify output file') do |filename|
options[:output] = filename
end
opts.on('-s', '--smart',
'Selects the largest, optimal square to crop and then resamples. Good for thumbnails.') do
options[:smart] = true
end
opts.on(nil, '--debug',
'Draws the bounding crop area instead of cropping the image') do
options[:debug] = true
end
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)
end
end
option_parser.parse!
if ARGV.empty?
puts option_parser.help
exit 1
end
ARGV.each_with_index do |file, i|
c = Cake.new(file)
c.debug = true if options[:debug]
output = if options[:output]
suffix = "_#{i}" if ARGV.size > 1
"#{File.basename(options[:output], '.png')}#{suffix}.png"
else
"#{File.basename(file, '.png')}_cropped.png"
end
if options[:smart]
c.crop_and_scale(options[:width], options[:height]).save(output)
else
c.crop(options[:width], options[:height]).save(output)
end
end
@mikewoodhouse

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.

@zaeleus
Owner

@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

@rogerbraun

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.

@jeffkreeftmeijer

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
Something went wrong with that request. Please try again.