secret
Last active

Codebrawl: Content-aware image cropping with ChunkyPNG

  • Download Gist
README.md
Markdown

Content-aware image cropping with ChunkyPNG

cake (**c**ontent-**a**ware **k**ropping [sic] by **e**ntropy) 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.

cake.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
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

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.

@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

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.

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. ;)

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.