Created
February 11, 2011 09:50
-
-
Save berkes/822154 to your computer and use it in GitHub Desktop.
SmartCropper testbed. Use in irb as: load 'croptoelie.rb'; CropToelie.new('not_too_big_image.jpg')
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'RMagick' | |
class CropToelie | |
include Magick | |
attr_accessor :image | |
attr_accessor :width | |
attr_accessor :height | |
attr_accessor :step_size | |
def initialize image_path | |
@image = ImageList.new(image_path).last | |
@orig = ImageList.new(image_path).last | |
@width = 200 | |
@height = 200 | |
@step_size = 30 | |
@image = @image.quantize | |
# Debug by preprocessing the image. See if this improves the result. | |
# @image = @image.posterize(5) | |
# @image = @image.edge(1) #aggressive egdes | |
# @image.display | |
# @image = @image.modulate(100,0,100) # grayscale | |
# @image.display | |
# @image = @image.black_threshold(15) # higher contrast | |
# @image.display | |
smart_crop(@width, @height) | |
smart_crop_by_search(@width, @height) | |
end | |
private | |
def smart_crop_by_search(requested_x, requested_y) | |
left, top = 0, 0 | |
right, bottom = requested_x, requested_y | |
# @TODO falltrough when requested is larger or equal to original | |
# Create a hash with all entropies | |
entropies = {} | |
# start in left-top corner, walk to right, with steps of 10 px. | |
while (bottom <= @image.rows) | |
while (right <= @image.columns) | |
square = {:left => left, :top => top, :right => right, :bottom => bottom} | |
entropies[square] = entropy_slice(@image, left, top, right - left, bottom - top) | |
draw_entropy(@orig, left, top, right, bottom, entropies[square]) | |
left += @step_size | |
right += @step_size | |
end | |
# @TODO last item is the one that goes over the edge, or touches the edge. | |
left = 0 | |
right = requested_x | |
top += @step_size | |
bottom += @step_size | |
end | |
# Find the one with highest entropy | |
best = entropies.max_by{|s| s[1]}[0] | |
# chop that one out | |
pp best | |
draw_rect(@orig, best[:left], best[:top], best[:right], best[:bottom]) | |
@orig.display | |
end | |
def smart_crop(requested_x, requested_y) | |
left, top = 0, 0 | |
right, bottom = @image.columns, @image.rows | |
width, height = right, bottom | |
# Slice from left and right edges until the correct width is reached. | |
while (width > requested_x) do | |
slice_width = [(width - requested_x), @step_size].min | |
left_entropy = entropy_slice(@image, left, 0, slice_width, bottom) | |
right_entropy = entropy_slice(@image, (right - slice_width), 0, slice_width, bottom) | |
#remove the slice with the least entropy | |
if left_entropy < right_entropy | |
#draw_entropy(@orig, left, 0, left + slice_width, bottom, left_entropy) | |
left += slice_width | |
else | |
#draw_entropy(@orig, (right - slice_width), 0, right + slice_width, bottom, right_entropy) | |
right -= slice_width | |
end | |
width = (right - left) | |
end | |
# Slice from top and bottom edges until the correct height is reached. | |
while (height > requested_y) do | |
slice_height = [(height - requested_y), @step_size].min | |
top_entropy = entropy_slice(@image, 0, top, @image.columns, slice_height) | |
bottom_entropy = entropy_slice(@image, 0, (bottom - slice_height), @image.columns, slice_height) | |
#remove the slice with the least entropy | |
if top_entropy < bottom_entropy | |
draw_entropy(@orig, 0, top, @image.columns, top + slice_height, top_entropy) | |
top += slice_height | |
else | |
draw_entropy(@orig, 0, bottom - slice_height, @image.columns, bottom, bottom_entropy) | |
bottom -= slice_height | |
end | |
height = (bottom - top) | |
end | |
draw_rect(@orig, left, top, right, bottom) | |
@orig.display | |
end | |
# Compute the entropy of an image slice. | |
def entropy_slice(image_data, x, y, width, height) | |
slice = image_data.crop(x, y, width, height) | |
entropy = entropy(slice) | |
end | |
# Compute the entropy of an image, defined as -sum(p.*log2(p)). | |
def entropy(image_slice) | |
hist = image_slice.color_histogram | |
hist_size = hist.values.inject{|sum,x| sum ? sum + x : x }.to_f | |
entropy = 0 | |
hist.values.each do |h| | |
p = h.to_f / hist_size | |
entropy += (p * Math.log2(p)) if p != 0 | |
end | |
return entropy * -1 | |
end | |
def draw_entropy(img, x1, y1, x2, y2, entropy) | |
gc = Magick::Draw.new | |
gc.stroke('white') | |
gc.fill('white') | |
opacity = (0.07) | |
gc.fill_opacity(opacity) | |
gc.stroke_opacity(entropy / 6.0) | |
gc.rectangle(x1, y1, x2, y2) | |
gc.font_weight(Magick::NormalWeight) | |
gc.font_style(Magick::NormalStyle) | |
gc.fill('black') | |
gc.stroke('transparent') | |
gc.text(x1+10,y1+15, "'#{entropy.round(1)}'") | |
gc.draw(img) | |
end | |
def draw_rect(img, x1, y1, x2, y2) | |
gc = Magick::Draw.new | |
gc.stroke('transparent') | |
gc.fill('white') | |
gc.fill_opacity(0.65) | |
gc.rectangle(x1, y1, x2, y2) | |
# Outline corners | |
gc.stroke_width(1) | |
gc.stroke('gray50') | |
gc.circle(x1,y1, x1+3,y1+3) | |
gc.circle(x2,y2, x2+3,y2+3) | |
# Annotate | |
gc.font_weight(Magick::NormalWeight) | |
gc.font_style(Magick::NormalStyle) | |
gc.fill('black') | |
gc.stroke('transparent') | |
gc.text(x1+10,y1+15, "'#{x1},#{y1}'") | |
gc.text(x2-50,y2-5, "'#{x2},#{y2}'") | |
gc.draw(img) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment