Content-aware image cropping with ChunkyPNG

Are you ready for another ChunkyPNG brawl? This week, the challenge is to build a cropping tool that knows which parts to cut off by looking at the contents of the image and deciding what the point of interest is.

Here are the three images we’ll use. A cat, a dog and some ducks. You’ll have to crop all three of them and the resulting images should be 100 by 100 pixels. Your solution should use ChunkyPNG, but of course you can use additional gems if you think you’ll need them.

How you solve this is entirely up to you, but solutions that need human input or are built only to support these images will get disqualified. Please be creative.

When you’re done, throw your implementation, a README and all three resulting images (please don’t include the input images) in a Gist. You can’t add images in the Gist web interface, so you’ll have to clone your Gist and add your output image using plain git. If you don’t know how, just send me a message and I’ll help you out.

You have one week to get your entry in, but we’ll add more time if necessary. I can’t wait to see how you solve this one. Good luck!

Prize

This week’s winner gets one month of free Github Micro!

This contest is finished

Congratulations to this week's winners! The entries and the contestant names are shown below.

  • README.md

    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.

    duck_cropped.png
    dog_cropped.png
    cat_cropped.png
    cake.rb
    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
    
    View full entry
    Finished in 1st place with a final score of 4.1/5. (View the Gist)
  • README.md

    Content-aware image cropping

    How to use

    Syntax is as follows:

    ruby cropper.rb input output width height noise

    How it works

    The idea is to calculate the energy of each pixel (something like how important this pixel is for the whole image) and then crop the part that has the highes energy.

    The energy function in this case is edge detection via the Sobel operator. You can swap in any other energy function, as long as it returns a grayscale canvas.

    After getting the grayscale image, the total energy of each column and row are calculated. Then, it takes lines from the left or right (or top and bottom) of the image, depending on which has the smaller total energy. What is left is a high energy image.

    This works pretty good if there is one large edgy thing (like the cat or dog), but breaks down if there are a lot of small edges (like the dirt in the duck image). For this cases, the cacrop method takes an additional noise operator, which makes it ignore values below a certain threshold. All images have been generated with a noise value of 70.

    The three images are all very hard to crop to 100x100. You may want to try with 150x150, which gives much better results

    duck_cropped.png
    cat_cropped.png
    dog_cropped.png
    sobel.rb
    module EdgeDetection
      def edge_detection
        res = ChunkyPNG::Image.new(width, height)
        (1...width - 1).each do |x|
          (1...height - 1).each do |y|
            x_edge = 
              (-1 * ChunkyPNG::Color.grayscale_teint(self[x - 1, y - 1])) +
              (-2 * ChunkyPNG::Color.grayscale_teint(self[x - 1, y    ])) +
              (-1 * ChunkyPNG::Color.grayscale_teint(self[x - 1, y + 1])) +
              ( 1 * ChunkyPNG::Color.grayscale_teint(self[x + 1, y - 1])) +
              ( 2 * ChunkyPNG::Color.grayscale_teint(self[x + 1, y    ])) +
              ( 1 * ChunkyPNG::Color.grayscale_teint(self[x + 1, y + 1]))
    
            y_edge = 
              (-1 * ChunkyPNG::Color.grayscale_teint(self[x - 1, y - 1])) +
              (-2 * ChunkyPNG::Color.grayscale_teint(self[x    , y - 1])) +
              (-1 * ChunkyPNG::Color.grayscale_teint(self[x + 1, y - 1])) +
              ( 1 * ChunkyPNG::Color.grayscale_teint(self[x - 1, y + 1])) +
              ( 2 * ChunkyPNG::Color.grayscale_teint(self[x    , y + 1])) +
              ( 1 * ChunkyPNG::Color.grayscale_teint(self[x + 1, y + 1]))
    
            edge = ((x_edge.abs / 4) + (y_edge.abs / 4)) / 2
            res[x,y] = ChunkyPNG::Color.grayscale(edge)
          end
        end
        res
      end
    end
    
    cropper.rb
    require "rubygems"
    require "chunky_png"
    require "pry"
    require_relative "sobel"
    require_relative "cacrop"
    
    input = ARGV[0]
    output = ARGV[1]
    width = (ARGV[2] || 100).to_i
    height = (ARGV[3] || 100).to_i
    noise = (ARGV[4] || 0).to_i
    
    image = ChunkyPNG::Image.from_file(input)
    image.extend(EdgeDetection)
    image.extend(CACrop)
    
    image = image.cacrop width, height, noise
    image.save(output)
    
    cacrop.rb
    module CACrop
    
      # This tries to crop the most energy dense parts of the image as determined
      # by the Sobel operator.
      def cacrop width, height, noise = 0
        
        # Get the edge detected image
        edges = edge_detection
    
        # This calculates arrays of the sums of each pixel in each row / column.
        # This is a simple measure of the activity in each row / column
        columns = (0...self.width).map{|i| edges.column(i).map{|c| ChunkyPNG::Color.r(c)}.map{|c| c > noise ? c : 0}.inject(&:+)}
        rows = (0...self.height).map{|i| edges.row(i).map{|c| ChunkyPNG::Color.r(c)}.map{|c| c > noise ? c : 0 }.inject(&:+)} 
        
        
        x = take_smallest_until columns, width
        y = take_smallest_until rows, height
    
        crop(x, y, width, height)
    
      end
    
      # Takes the smalles value from the end or the beginning of the array.
      # This continues until the size of the array is right.
      # It returns the numbers of dropped values from the left.
      def take_smallest_until arr, goal
        res = []
        arr.first < arr.last ? res << arr.shift : arr.pop while arr.size > goal
        res.size
      end
    end
    
    View full entry
    Finished in 2nd place with a final score of 3.6/5. (View the Gist)
  • README.md

    Robocrop: attempt at content-aware image cropping

    Basic idea is to find some area on the image that stands out the most from the rest of the image. There are quite a few ways of doing this, and I decided to use only difference of color and cotrast levels, for each block vs entire image.

    I use stddev of colors of 3x3 pixel block with the center on the pixel as a pixel's 'contrast' here.

    robocrop.rb - as simple as possible

    The first version of robocrop is simple and only supports 100x100 blocks, you can't control how block weight is calculated etc.

    There are two weights used here to determine what block to use in the end.

    First is simply an average contrast level of block's pixels. This promotes blocks with high contrast, such as cat's fur and folds on dog's skin.

    Using only this gives passable results for cat/dog, but fails on the ducks, since ground have higher contrast there.

    Second is diversity of block's pixels contrast. This weight promotes blocks that has areas of both low and high contrast, making algorithm to more likely capture the 'edges' of objects.

    In the case of 'cat' and 'dog', adding diversity to weight will make cropped image to contain some of the blurry background behind the cat/dog, producing better results. It also helps on ducks, since the duck itself has a low contrast, while the ground has high, algorithm will try to fit both duck and the ground into a block, resulting in a block centered on duck's body.

    robocrop2.rb - as a library

    Same algorithm, packed as a new method of Image class, :robocrop ( and :robocrop! ), that allow to override some parameters. Accepts hash of arguments.

    :width, :height - dimensions of resulting canvas.

    :contrast, :diversity - additional weights that specify how much average contrast and diversity contributes to block weight.

    :precision - specifies how many blocks are actually checked. Higher precision means more accurate results, but worse performance. For :precision => X, at most X*X blocks will be tested.

    Random thoughts

    Despite being very slow, this algorithm is simple and usually produce ok results. Interesting is that there is little to no point in comparing block's color versus image color, using only contrast is good enough, if not better.

    Also, I like how algorithm captures the edges. Similar result could be accomplished by using some edge detection library ( imagemagick for example ) first, but it would add complexity to the solution.

    I use NArray for calclulations, since its fast and does have nice api.

    dog_cropped.png
    duck_cropped.png
    cat_cropped.png
    robocrop2.rb
    require 'rubygems'
    require 'chunky_png'
    require 'narray'
    
    class ChunkyPNG::Image
    
      def robocrop(options = {})
        dup.robocrop(options)
      end
    
      def robocrop!(options = {})
        options = {
            # dimensions
            :width => 100,
            :height => 100,
            # weights
            :contrast => 1,
            :diversity => 1,
            # complexity
            :precision => 100,
        }.merge!(options)
    
        # create 3 width*height matrixes, that stores red, green or blue channel of each pixel
        image_rgb = NArray.byte(3, width, height)
        # fill the colors matrix
        width.times do |x|
          image_rgb[true, x, true] = column(x).collect do |color|
            ChunkyPNG::Color.to_truecolor_bytes(color)
          end
        end
    
        # calculate 'contrast' for every pixel of the image
        # i use stddev of colors of 3x3 pixel block with the center on the pixel as a pixel's 'contrast' here
        # stddev is calculated separately for every color channel
        image_contrast = NArray.float(width, height)
        width.times do |x|
          height.times do |y|
            image_contrast[x, y] = image_rgb[
                true,
                ([0, x-1].max)..([width-1, x+1].min),
                ([0, y-1].max)..([height-1, y+1].min)
            ].stddev(1..2).sum
          end
        end
    
        # for every crop_width*crop_height block possible, find its top-left most pixel
        x_step = [(width-options[:width]).to_f/options[:precision], 1].max
        y_step = [(height-options[:height]).to_f/options[:precision], 1].max
        northwest_pixels = (0..(width-options[:width])).step(x_step).to_a
        northwest_pixels = northwest_pixels.product((0..(height-options[:height])).step(y_step).to_a)
    
        # iterate over all possible blocks
        crop_x, crop_y, block_weight = northwest_pixels.collect {|x, y|
          # contrast levels for pixels of this block
          block_contrast = image_contrast[x...(x+options[:width]), y...(y+options[:height])]
    
          contrast = block_contrast.mean  ** options[:contrast]
          contrast_diversity = block_contrast.stddev ** options[:diversity]
          # capture block coords with its weight
          [x, y, contrast * contrast_diversity]
        }.max_by{|x, y, weight| weight}
    
        crop!(crop_x, crop_y, options[:width], options[:height])
      end
    end
    
    robocrop.rb
    require 'rubygems'
    require 'chunky_png'
    require 'narray'
    
    def robocrop(original_filename, cropped_filename)
      image = ChunkyPNG::Image.from_file(original_filename)
    
      # create 3 width*height matrixes, that stores red, green or blue channel of each pixel
      image_rgb = NArray.byte(3, image.width, image.height)
      # fill the colors matrix
      image.width.times do |x|
        image_rgb[true, x, true] = image.column(x).collect do |color|
          ChunkyPNG::Color.to_truecolor_bytes(color)
        end
      end
    
      # calculate 'contrast' for every pixel of the image
      # i use stddev of colors of 3x3 pixel block with the center on the pixel as a pixel's 'contrast' here
      # stddev is calculated separately for every color channel
      image_contrast = NArray.float(image.width, image.height)
      image.width.times do |x|
        image.height.times do |y|
          image_contrast[x, y] = image_rgb[
              true,
              ([0, x-1].max)..([image.width-1, x+1].min),
              ([0, y-1].max)..([image.height-1, y+1].min)
          ].stddev(1..2).sum
        end
      end
    
      # for every 100x100 block possible, find its top-left most pixel
      x_step = 1
      y_step = 1
      northwest_pixels = (0..(image.width-100)).step(x_step).to_a
      northwest_pixels = northwest_pixels.product((0..(image.height-100)).step(y_step).to_a)
    
      # iterate over all possible blocks
      crop_x, crop_y, block_weight = northwest_pixels.collect {|x, y|
        # contrast levels for pixels of this block
        block_contrast = image_contrast[x...(x+100), y...(y+100)]
    
        contrast = block_contrast.mean
        contrast_diversity = block_contrast.stddev
        # capture block coords with its weight
        [x, y, contrast * contrast_diversity]
      }.max_by{|x, y, weight| weight}
    
      # crop the image and save it with a new filename
      image.crop!(crop_x, crop_y, 100, 100)
      image.save(cropped_filename)
    end
    
    robocrop('cat.png', 'cat_cropped.png')
    robocrop('dog.png', 'dog_cropped.png')
    robocrop('duck.png', 'duck_cropped.png')
    
    View full entry
    Finished in 3rd place with a final score of 3.2/5. (View the Gist)
  • README.md

    Simplest possible solution to image cropping problem:

    • grab the most "exciting" part of the image
      • "exciting" => least average according to unweighed color
    • checks every single 100 X 100 splotch, so it's very inefficient. Not checking every single subimage would be the most obvious improvement, but frankly, doing image processing in native ruby is probably not the best idea anyhow.
    • special bonus! also finds the least interesting 100X100 subimage.
    • usage: ruby chunky.rb <inputfn.png>
    • most "exciting" crop is saved as <inputfn.png>.max.png
    • most "boring" crop is saved as <inputfn.png>.min.png

    This works:

    • extremely well for the duck
    • well for the cat
    • so-so for the dog

    Actually in the case of the dog, the bit identified as "most boring" is a pretty good crop. Reason being that the dog picture has little variation: half big black dog, half green. All said, though, the dog crop is acceptable and by far not the worst possible choice.

    Possible improvements:

    • tons of performance optimizations
    • calculating the averages differently, especially perception weighting the individual color channels or hsv components.
    • doing this properly would require more sophisticated algorithms, I doubt it would be feasible with ChunkyPNG
    dog.png.min.png
    cat.png.min.png
    duck.png.min.png
    dog.png.max.png
    duck.png.max.png
    cat.png.max.png
    chunky.rb
    require 'chunky_png'
    
    
    include ChunkyPNG
    
    
    def usage
      STDERR.puts "usage: [bla] filename.png"
      exit 1
    end
    
    usage if 1 != ARGV.length 
    
    
    # determine "average" color of a splotch.
    def avg img
      sum = img.pixels.inject(0) {|sum, pixel|
        sum += pixel
      }  
      sum/img.pixels.length
    end
    
    # iterate over every possible 100 X 100 
    # subimage
    def each img
      # replace `upto` with `step` here to 
      # speed things up
      0.upto(img.width-100) {|x|
        0.upto(img.height-100) {|y|
          yield img.crop(x,y,100,100)
        }
      }
    end
    
    
    
    img     = Image.from_file(ARGV[0])
    img_avg = avg(img)
    
    
    
    max              = 0
    min              = 0xffffffffffff
    img_max, img_min = nil, nil
    
    each(img) { |i|
      d = (avg(i)-img_avg).abs
      max, img_max = d, i if d > max
      min, img_min = d, i if d < min
    }
    
    
    
    img_max.to_image.save(ARGV[0]+".max.png")
    img_min.to_image.save(ARGV[0]+".min.png")
    
    View full entry
    Finished in 4th place with a final score of 3.2/5. (View the Gist)
  • readme.md

    Crop

    This solution crops to the center 100 pixel square in the image.

    Show me the Output!

    Why this is a great solution

    • It's wicked fast
    • It's easily maintainable
    • It gets the right crop at least as often as more complicated solutions
    • The most interesting part of an image is frequently at the center

    Seriously?

    I spent a lot of time thinking about this and tried other solutions. The solution I came to is that there aren't a lot of great solutions. An algorithm gets it wrong as much as it gets it right. Centering the crop was the first solution I wrote up as the stupid simple case. It was meant to be the solution against which I compared all other solutions. What I found is that I was happier with it for cropping than other solutions.

    Let me try and convince you a bit more. Let's talk about the duck image:

    The duck image has no true focal point. There are no lines leading to a central point in the photo. There is no strong use of contrasting color. However if we're talking human focal points then we're probably talking about the heads. We would like to see the ducks cropped to the ducks' heads:

    In this image I've highlighted the focal points in pink. I've also highlighted 'ideal' crops in orange. Notice anything interesting? Even if the algorithm could perfectly determine the focal point the crop wouldn't be right. Another thing that's interesting? This is completely a matter of opinion. Cropping is something that's pretty subjective.

    Another interesting thing- this center crop picked a solution that was very similar to another I had toyed with that looked for standout colors. This is because the duck's legs are one of the brightest parts of the image. That algorithm was considerably slower.

    Now let's talk about the dog and the cat- there is a much clearer center point. This centering method grabs the right picture.

    Another good thing about this method is that it's fast. There's no deep calculation that needs to go on so this solution is able to churn through images very quickly. It's also small so it's very maintainable.

    If this brawl was a challenge to ship code, this would definitely be the solution I'd ship.

    Other solutions

    As I said I explored other solutions and I'd like to share some quick notes:

    Blob Detection

    I explored blob detection by flipping the image grayscale and upping the brightness and contrast until there were black blobs on the page. I then looked for the box that best fit the most number of black pixels. This worked surprisingly well for the ducks, not good for the cat, and awful for the dog. The algorithm falls apart on any images that are dark.

    Interest Hue Detection

    This algorithm looked for interesting hues. I was really disappointed with the results.

    dog-output.png
    cat-output.png
    duck-output.png
    crop.rb
    require 'rubygems'
    require 'chunky_png'
    
    module ChunkyPNG::Canvas::Operations
      def crop_square_at_point_of_interest(x,y,size)
        x = x - ( size / 2)
        x = 0 if x<0
        y = y - ( size / 2 )
        y = 0 if y<0
        self.crop(x,y,size,size)
      end
    end
    
    def center_point_of_interest(image)
      [image.dimension.width/2, image.dimension.height/2]
    end
    
    ['cat', 'duck', 'dog'].each do |test_image|
      input = ChunkyPNG::Image.from_file("#{test_image}.png")
      point_of_interest = center_point_of_interest(input)
      input.crop_square_at_point_of_interest(point_of_interest[0], 
            point_of_interest[1], 100).save("#{test_image}-output.png")
    end
    
    View full entry
    Finished in 5th place with a final score of 2.8/5. (View the Gist)
  • README
    Program is run by typing "ruby main-single.rb input.png output.png". 
    The output contains the cropped image in png format.
    dog.png
    cat.png
    duck.png
    main.rb
    require 'rubygems'
    require 'chunky_png'
    
    WIDTH = 100
    HEIGHT = 100
    
    class TwodimensionalArray
    	def initialize(width, height)
    		@width = width
    		@height = height
    		@array = Array.new(width)
    		@array.map! {Array.new(height)}
    	end
    	
    	def [](x, y)
    		@array[x][y]
    	end
    	
    	def []=(x, y, value)
    		@array[x][y] = value
    	end
    	
    	def width 
    		@width
    	end
    	
    	def height
    		@height
    	end
    	
    	def average
    		sum = 0
    		0.upto(@width-1) do |x|
    			0.upto(@height-1) do |y|
    				sum += @array[x][y]
    			end
    		end
    		sum / (@width * @height)
    	end
    	
    	def key_values
    		max = min = @array[0][0]	
    		sum = 0
    		0.upto(@width-1) do |x|
    			0.upto(@height-1) do |y|
    				sum += @array[x][y]
    				max = @array[x][y] if max < @array[x][y] 
    				min = @array[x][y] if min > @array[x][y] 
    			end
    		end
    		[min, sum / (@width * @height), max]
    	end
    	
    	def neighbours(x, y, distance = 1)
    		result = []
    		(-distance).upto(distance) do |dx|
    			(-distance).upto(distance) do |dy|				
    				result << @array[x + dx][y + dy] unless ((dx == 0 && dy == 0) || (x + dx < 0) || (x + dx >= width) || (y + dy < 0) || (y + dy >= height))
    			end
    		end		
    		result
    	end
    	
    	def self.from_canvas(source)
    		result = TwodimensionalArray.new(source.width, source.height)
    		0.upto(source.width-1) do |x|
    			0.upto(source.height-1) do |y|
    				result[x,y] = Pixel.from_int(source.get_pixel(x, y))
    			end
    		end
    		result
    	end
    end
    class SumArray
    	def initialize(source_array, sum_width, sum_height)
    		@sum_width = sum_width
    		@sum_height = sum_height
    		@source_array = source_array
    		cumulative_array = cumulative(source_array)
    		@array = TwodimensionalArray.new(source_array.width - sum_width, source_array.height - sum_height)
    		0.upto(@array.width-1) do |x|
    			0.upto(@array.height-1) do |y|
    				@array[x,y] = cumulative_array[x+sum_width-1,y+sum_height-1]
    				@array[x,y] -= cumulative_array[x-1,y+sum_height-1] if (x-1 >= 0)
    				@array[x,y] -= cumulative_array[x+sum_width-1,y-1] if (y-1 >= 0)
    				@array[x,y] += cumulative_array[x-1,y-1] if ((x-1 >= 0) && (y-1 >= 0))
    				@array[x,y] /= (sum_width*sum_height)						
    			end
    		end
    	end
    	
    	def [](x, y)
    		@array[x, y]
    	end
    	
    	def sum_width
    		@sum_width
    	end
    	
    	def sum_height
    		@sum_height
    	end
    	
    	def max_difference
    		average_contrast = @source_array.average		
    		max_x = max_y = max_difference = 0;
    		
    		0.upto(@array.width-1) do |x|
    			0.upto(@array.height-1) do |y|
    				if ((@array[x,y] - average_contrast).abs > max_difference)
    					max_difference = (@array[x,y] - average_contrast).abs
    					max_x = x
    					max_y = y
    				end
    			end
    		end
    		[max_x, max_y]
    	end
    	
    	private
    	def cumulative(source_array)
    		result = TwodimensionalArray.new(source_array.width, source_array.height)
    		0.upto(source_array.width-1) do |x|		
    			0.upto(source_array.height-1) do |y|				
    				result[x,y] = source_array[x,y]
    				result[x,y] += result[x-1,y] if (x-1 >= 0)
    				result[x,y] += result[x,y-1] if (y-1 >= 0)
    				result[x,y] -= result[x-1,y-1] if ((x-1 >= 0) && (y-1 >= 0))				
    			end
    		end
    		result
    	end
    end
    class AreaOfInterest 	
    	def initialize(width, height)
    		@width, @height = width, height		
    	end
    	
    	def process(source)		
    		img = TwodimensionalArray.from_canvas(source)
    		contrast_field = get_contrast_field(img)
    		avg = contrast_field.average
    		min, avg, max = contrast_field.key_values
    		
    		0.upto(contrast_field.width-1) do |x|
    			0.upto(contrast_field.height-1) do |y|
    					contrast_field[x,y] = ((contrast_field[x,y] < avg) ? min : max)				
    					#contrast_field[x, y] = 0 if contrast_field[x,y] < avg
    			end
    		end
    		area_of_interest = get_area_of_interest(contrast_field, @width, @height)				
    		source.crop(area_of_interest[0], area_of_interest[1], @width, @height)
    		#to_png(contrast_field)
    	end	
    	
    	private		
    	def get_area_of_interest(contrast_field, width, height)		
    		SumArray.new(contrast_field, width, height).max_difference			
    	end
    	
    	def get_contrast_field(pixel_field) 
    		result = TwodimensionalArray.new(pixel_field.width, pixel_field.height)
    		0.upto(pixel_field.width-1) do |x|
    			0.upto(pixel_field.height-1) do |y|
    				neighbours = pixel_field.neighbours(x, y)
    				result[x,y] = neighbours.inject(0) {|sum, neighbour| sum += pixel_field[x, y].absolute_difference(neighbour)} / neighbours.count				
    			end
    		end		
    		result
    	end
    	
    	def to_png(input)
    		result = ChunkyPNG::Canvas.new(input.width, input.height)
    		0.upto(input.width-1) do |x|
    			0.upto(input.height-1) do |y|
    				result.set_pixel(x, y, input[x,y])
    			end
    		end
    		result
    	end
    end
    class Pixel 	
    	def self.from_int(value)
    		Pixel.new((value >> 24) % 0xFF, (value >> 16) % 0xFF, (value >> 8) % 0xFF, value % 0xFF)
    	end
    
    	def initialize(alpha, red, green, blue)
    		@blue = blue
    		@green = green
    		@red = red
    		@alpha = alpha
    	end
    	
    	def red
    		@red
    	end
    
    	def red=(value)
    		@red = value
    	end
    	
    	def green
    		@green
    	end
    	
    	def green=(value)
    		@green = value
    	end	
    	
    	def blue
    		@blue
    	end
    
    	def blue=(value)
    		@blue = value
    	end
    
    	
    	def alpha
    		@alpha
    	end
    	
    	def alpha=(value)
    		@alpha = value
    	end
    	
    	def absolute_difference(another_pixel)
    		(red - another_pixel.red).abs + (green - another_pixel.green).abs + (blue - another_pixel.blue).abs + (alpha - another_pixel.alpha).abs
    	end
    end
    
    source = ChunkyPNG::Image.from_file(ARGV[0])
    processor = AreaOfInterest.new(WIDTH, HEIGHT)
    result = processor.process(source)
    result.save(ARGV[1] || "output.png")
    
    View full entry
    Finished in 6th place with a final score of 2.4/5. (View the Gist)