Skip to content

Instantly share code, notes, and snippets.

@zaeleus
Created November 26, 2011 22:13
Show Gist options
  • Star 37 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save zaeleus/a54cd41137b678935c91 to your computer and use it in GitHub Desktop.
Save zaeleus/a54cd41137b678935c91 to your computer and use it in GitHub Desktop.
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
@jeffkreeftmeijer
Copy link

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