croptoelie 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -6,8 +6,7 @@ Crops images based on entropy: leaving the most interesting part intact.
6
6
 
7
7
  Don't expect this to be a replacement for human cropping, it is an algorythm and not an extremely smart one at that :).
8
8
 
9
- Best results achieved in combination with scaling: the cropping is then only used to square the image, cutting off the least interesting part. It offers two methods, scanning and trimming: with scanning the whole image is placed in an array then evaluated: very slow and memory-gobbling.
10
-
9
+ Best results achieved in combination with scaling: the cropping is then only used to square the image, cutting off the least interesting part.
11
10
  The trimming simply chops off te edge that is least interesting, and continues doing so, untill it reached the requested size.
12
11
 
13
12
  ## Usage
@@ -44,6 +43,14 @@ File *uploaders/attachement_uploader.rb*:
44
43
  * Commit and push until you are happy with your contribution
45
44
  * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
46
45
 
46
+ ## Changelog
47
+ 2011-04-19: Replace crop with crop! avoids copying large chunks of images around.
48
+ 2011-04-18: Limit to N steps, instead of step_size.
49
+ 2011-04-16: Introduce tests and a profiler script, to profile performance.
50
+
51
+ ## Todo
52
+ Improved algorythm: first @image.scale by F, investigate the entropy on that, most-interesting square by factor F is to-be-cropped area.
53
+
47
54
  ## Copyright
48
55
 
49
56
  Copyright (c) 2011 Bèr Kessels. See LICENSE.txt for
data/Rakefile CHANGED
@@ -18,7 +18,7 @@ Jeweler::Tasks.new do |gem|
18
18
  gem.homepage = "http://github.com/berkes/croptoelie"
19
19
  gem.license = "MIT"
20
20
  gem.summary = %Q{Content aware cropper.}
21
- gem.description = %Q{Crops images based on entropy: leaving the most interesting part intact. Don't expect this to be a replacement for human cropping, it is an algorythm and not an extremely smart one at that :). Best results achieved in combination with scaling: the cropping is then only used to square the image, cutting off the least interesting part. It offers two methods, scanning and trimming: with scanning the whole image is placed in an array then evaluated: very slow and memory-gobbling. The trimming simply chops off te edge that is least interesting, and continues doing so, untill it reached the requested size.}
21
+ gem.description = %Q{Crops images based on entropy: leaving the most interesting part intact. Don't expect this to be a replacement for human cropping, it is an algorythm and not an extremely smart one at that :). Best results achieved in combination with scaling: the cropping is then only used to square the image, cutting off the least interesting part. The trimming simply chops off te edge that is least interesting, and continues doing so, untill it reached the requested size.}
22
22
  gem.email = "ber@webschuur.com"
23
23
  gem.authors = ["Bèr Kessels"]
24
24
  # Include your dependencies below. Runtime dependencies are required when using your gem,
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.4.0
@@ -5,12 +5,12 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{croptoelie}
8
- s.version = "0.3.1"
8
+ s.version = "0.4.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Bèr Kessels"]
12
- s.date = %q{2011-04-07}
13
- s.description = %q{Crops images based on entropy: leaving the most interesting part intact. Don't expect this to be a replacement for human cropping, it is an algorythm and not an extremely smart one at that :). Best results achieved in combination with scaling: the cropping is then only used to square the image, cutting off the least interesting part. It offers two methods, scanning and trimming: with scanning the whole image is placed in an array then evaluated: very slow and memory-gobbling. The trimming simply chops off te edge that is least interesting, and continues doing so, untill it reached the requested size.}
12
+ s.date = %q{2011-04-19}
13
+ s.description = %q{Crops images based on entropy: leaving the most interesting part intact. Don't expect this to be a replacement for human cropping, it is an algorythm and not an extremely smart one at that :). Best results achieved in combination with scaling: the cropping is then only used to square the image, cutting off the least interesting part. The trimming simply chops off te edge that is least interesting, and continues doing so, untill it reached the requested size.}
14
14
  s.email = %q{ber@webschuur.com}
15
15
  s.extra_rdoc_files = [
16
16
  "LICENSE.txt",
@@ -30,7 +30,10 @@ Gem::Specification.new do |s|
30
30
  "doc/croptoelie_test.rb",
31
31
  "doc/histogram.rb",
32
32
  "lib/croptoelie.rb",
33
+ "test/entropyish.png",
34
+ "test/entropyish.txt",
33
35
  "test/helper.rb",
36
+ "test/profiler.rb",
34
37
  "test/test_croptoelie.rb"
35
38
  ]
36
39
  s.homepage = %q{http://github.com/berkes/croptoelie}
@@ -40,6 +43,7 @@ Gem::Specification.new do |s|
40
43
  s.summary = %q{Content aware cropper.}
41
44
  s.test_files = [
42
45
  "test/helper.rb",
46
+ "test/profiler.rb",
43
47
  "test/test_croptoelie.rb"
44
48
  ]
45
49
 
@@ -3,16 +3,15 @@ class CropToelie
3
3
  include Magick
4
4
 
5
5
  attr_accessor :orig
6
- attr_accessor :step_size
6
+ attr_accessor :steps
7
7
 
8
8
  # Create a new CropToelie object from a ImageList single image object.
9
9
  # If you want to provide a file by its path use CropToelie.from_file('/path/to/image.png').
10
10
  def initialize(image)
11
11
  @image = image
12
- @orig = image
13
12
 
14
13
  # Hardcoded (but overridable) defaults.
15
- @step_size = 10
14
+ @steps = 10
16
15
 
17
16
  # Preprocess image.
18
17
  @image = @image.quantize
@@ -28,32 +27,21 @@ class CropToelie
28
27
  return CropToelie.new(image)
29
28
  end
30
29
 
31
- # Crops an image to width x height
32
- #
33
- # If you want speed, choose :by_trim. It will use smart_crop_by_trim().
34
- # If you have a very crowded image, an image with lots of color, or dark and light
35
- # spots, the much slower :by_search will give better results. It will use
36
- # smart_crop_by_search().
37
- def smart_crop(width, height, method = :by_trim)
38
- sq = square(width, height, method)
39
- return @orig.crop(sq[:left], sq[:top], width, height, true)
30
+ # Crops an image to width x height
31
+ def smart_crop(width, height)
32
+ sq = square(width, height)
33
+ return @image.crop!(sq[:left], sq[:top], width, height, true)
40
34
  end
41
35
 
42
36
  # Squares an image (with smart_square) and then scales that to width, heigh
43
- #
44
- # If you want speed, choose :by_trim. It will use smart_crop_by_trim().
45
- # If you have a very crowded image, an image with lots of color, or dark and light
46
- # spots, the much slower :by_search will give better results. It will use
47
- # smart_crop_by_search().
48
37
  def smart_crop_and_scale(width, height)
49
- cropped = smart_square
50
- return cropped.scale(width, height)
38
+ smart_square
39
+ return @image.scale!(width, height)
51
40
  end
52
41
 
53
42
  # Squares an image by slicing off the least interesting parts.
54
43
  # Usefull for squaring images such as thumbnails. Usefull before scaling.
55
44
  def smart_square
56
- cropped = @orig #square images can be returned as-is.
57
45
  if @rows != @columns #None-square images must be shaved off.
58
46
  if @rows < @columns #landscape
59
47
  crop_height = crop_width = @rows
@@ -61,23 +49,18 @@ class CropToelie
61
49
  crop_height = crop_width = @columns
62
50
  end
63
51
 
64
- sq = square(crop_width, crop_height, :by_trim)
65
- cropped = @orig.crop(sq[:left], sq[:top], crop_width, crop_height, true)
52
+ sq = square(crop_width, crop_height)
53
+ @image.crop!(sq[:left], sq[:top], crop_width, crop_height, true)
66
54
  end
67
55
 
68
- cropped
56
+ @image
69
57
  end
70
58
 
71
59
  # Finds the most interesting square with size width x height.
72
60
  #
73
- # See smart_crop documentation for explanation about the method
74
61
  # Returns a hash {:left => left, :top => top, :right => right, :bottom => bottom}
75
- def square(width, height, method = :by_trim)
76
- if method == :by_trim
77
- return smart_crop_by_trim(width, height)
78
- else
79
- return smart_crop_by_search(width, height)
80
- end
62
+ def square(width, height)
63
+ return smart_crop_by_trim(width, height)
81
64
  end
82
65
 
83
66
  private
@@ -88,49 +71,15 @@ class CropToelie
88
71
  return (@columns > @width) && (@rows < @height)
89
72
  end
90
73
 
91
- # Find Entropy by moving the "to be cropped" area over the image and
92
- # recording the entropy of each such square.
93
- # The square with the highest entropy is considered the most interesting and
94
- # cropped out of the original.
95
- # NOTE: this method is very slow compared to smart_crop_by_trim.
96
- def smart_crop_by_search(requested_x, requested_y)
97
- left, top = 0, 0
98
- right, bottom = requested_x, requested_y
99
-
100
- # Create a hash with all entropies
101
- entropies = {}
102
-
103
- # start in left-top corner, walk to right, with steps of 10 px.
104
- while (bottom <= @rows)
105
- while (right <= @columns)
106
- square = {:left => left, :top => top, :right => right, :bottom => bottom}
107
- entropies[square] = entropy_slice(@image, left, top, right - left, bottom - top)
108
-
109
- left += @step_size
110
- right += @step_size
111
- end
112
- # @TODO last item is the one that goes over the edge, or touches the edge.
113
- left = 0
114
- right = requested_x
115
- top += @step_size
116
- bottom += @step_size
117
- end
118
-
119
- # Find the square with highest entropy
120
- best = entropies.max_by{|s| s[1]}[0]
121
-
122
- # chop that one out
123
- best
124
- end
125
-
126
74
  def smart_crop_by_trim(requested_x, requested_y)
127
75
  left, top = 0, 0
128
76
  right, bottom = @columns, @rows
129
77
  width, height = right, bottom
78
+ step_size = step_size(requested_x, requested_y)
130
79
 
131
80
  # Slice from left and right edges until the correct width is reached.
132
81
  while (width > requested_x)
133
- slice_width = [(width - requested_x), @step_size].min
82
+ slice_width = [(width - requested_x), step_size].min
134
83
 
135
84
  left_entropy = entropy_slice(@image, left, 0, slice_width, bottom)
136
85
  right_entropy = entropy_slice(@image, (right - slice_width), 0, slice_width, bottom)
@@ -147,7 +96,7 @@ class CropToelie
147
96
 
148
97
  # Slice from top and bottom edges until the correct height is reached.
149
98
  while (height > requested_y)
150
- slice_height = [(height - @step_size), @step_size].min
99
+ slice_height = [(height - step_size), step_size].min
151
100
 
152
101
  top_entropy = entropy_slice(@image, 0, top, @columns, slice_height)
153
102
  bottom_entropy = entropy_slice(@image, 0, (bottom - slice_height), @columns, slice_height)
@@ -172,6 +121,8 @@ class CropToelie
172
121
  end
173
122
 
174
123
  # Compute the entropy of an image, defined as -sum(p.*log2(p)).
124
+ # Note: instead of log2, only available in ruby > 1.9, we use
125
+ # log(p)/log(2). which has the same effect.
175
126
  def entropy(image_slice)
176
127
  hist = image_slice.color_histogram
177
128
  hist_size = hist.values.inject{|sum,x| sum ? sum + x : x }.to_f
@@ -179,9 +130,12 @@ class CropToelie
179
130
  entropy = 0
180
131
  hist.values.each do |h|
181
132
  p = h.to_f / hist_size
182
- entropy += (p * Math.log2(p)) if p != 0
133
+ entropy += (p * (Math.log(p)/Math.log(2))) if p != 0
183
134
  end
184
-
185
135
  return entropy * -1
186
136
  end
137
+
138
+ def step_size(requested_x, requested_y)
139
+ ((([@rows - requested_x, @columns - requested_y].max)/2)/@steps).to_i
140
+ end
187
141
  end
Binary file
File without changes
@@ -1,5 +1,6 @@
1
1
  require 'rubygems'
2
2
  require 'bundler'
3
+
3
4
  begin
4
5
  Bundler.setup(:default, :development)
5
6
  rescue Bundler::BundlerError => e
@@ -14,5 +15,3 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
15
  $LOAD_PATH.unshift(File.dirname(__FILE__))
15
16
  require 'croptoelie'
16
17
 
17
- class Test::Unit::TestCase
18
- end
@@ -0,0 +1,37 @@
1
+ require 'ruby-prof'
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
4
+ require 'croptoelie'
5
+
6
+ tests = {
7
+ :smart_crop_by_trim => {:method => :smart_crop, :params => [100, 100]},
8
+ # :smart_crop_by_search => {:method => :smart_crop, :params => [100, 100, :by_search]},
9
+ :smart_crop_and_scale => {:method => :smart_crop_and_scale, :params => [100, 100]},
10
+ :smart_square => {:method => :smart_square, :params => []}
11
+ }
12
+ #result = RubyProf.profile do
13
+ tests.each do |id, test|
14
+ filename = File.join(File.expand_path(File.dirname(__FILE__)), "../doc/tyto.jpg")
15
+
16
+ 2.times do |i|
17
+ # result = RubyProf.profile do
18
+ img = CropToelie.from_file(filename)
19
+ img.send(test[:method], *test[:params])
20
+ img = nil
21
+ # end
22
+
23
+ # # Print a flat profile to text
24
+ # puts "Run #{i}:\t #{id} ------------------------"
25
+ # #file = File.new("./#{id}-#{i}.txt", "w")
26
+ # printer = RubyProf::FlatPrinter.new(result)
27
+ # printer.print(STDOUT, {:min_percent => 10})
28
+ # print = nil
29
+ end
30
+ end
31
+ #end
32
+ # # Print a flat profile to text
33
+ # puts "Run #{i}:\t #{id} ------------------------"
34
+ # #file = File.new("./#{id}-#{i}.txt", "w")
35
+ # printer = RubyProf::FlatPrinter.new(result)
36
+ # printer.print(STDOUT, {:min_percent => 10})
37
+ # print = nil
@@ -1,7 +1,42 @@
1
1
  require 'helper'
2
2
 
3
3
  class TestCroptoelie < Test::Unit::TestCase
4
- should "probably rename this file and start testing for real" do
5
- flunk "hey buddy, you should probably rename this file and start testing for real"
4
+ def setup
5
+ @filename = File.join(File.expand_path(File.dirname(__FILE__)), "entropyish.png")
6
+ @image = Magick::ImageList.new(@filename).last
7
+ end
8
+ should "initialize a croptoelie image from an ImageList item" do
9
+ img = CropToelie.new(@image)
10
+ assert_equal(img.class, CropToelie)
11
+ end
12
+ should "create a croptoelie from an imagefile" do
13
+ img = CropToelie.from_file(@filename)
14
+ assert_equal(img.class, CropToelie)
15
+ end
16
+
17
+ should "fail on creating a croptoelie image from a textfile" do
18
+ assert_raise Magick::ImageMagickError, NoMethodError do
19
+ CropToelie.new(File.join(File.expand_path(File.dirname(__FILE__)), "entropyish.txt"))
20
+ end
21
+ end
22
+
23
+ should "crop to 100x100 without scaling with smart_crop" do
24
+ img = CropToelie.new(@image)
25
+ img = img.smart_crop(100, 100)
26
+ size = [img.rows, img.columns]
27
+ assert_equal(size, [100, 100])
28
+ end
29
+
30
+ should "crop to 100x100 with scaling with smart_crop_and_scale" do
31
+ img = CropToelie.new(@image)
32
+ img = img.smart_crop_and_scale(100, 100)
33
+ size = [img.rows, img.columns]
34
+ assert_equal(size, [100, 100])
35
+ end
36
+
37
+ should "square image without scaling" do
38
+ img = CropToelie.new(@image)
39
+ img = img.smart_square
40
+ assert_equal(img.rows, img.columns)
6
41
  end
7
42
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: croptoelie
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.3.1
5
+ version: 0.4.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - "B\xC3\xA8r Kessels"
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-04-07 00:00:00 +02:00
13
+ date: 2011-04-19 00:00:00 +02:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -79,7 +79,7 @@ dependencies:
79
79
  type: :runtime
80
80
  prerelease: false
81
81
  version_requirements: *id006
82
- description: "Crops images based on entropy: leaving the most interesting part intact. Don't expect this to be a replacement for human cropping, it is an algorythm and not an extremely smart one at that :). Best results achieved in combination with scaling: the cropping is then only used to square the image, cutting off the least interesting part. It offers two methods, scanning and trimming: with scanning the whole image is placed in an array then evaluated: very slow and memory-gobbling. The trimming simply chops off te edge that is least interesting, and continues doing so, untill it reached the requested size."
82
+ description: "Crops images based on entropy: leaving the most interesting part intact. Don't expect this to be a replacement for human cropping, it is an algorythm and not an extremely smart one at that :). Best results achieved in combination with scaling: the cropping is then only used to square the image, cutting off the least interesting part. The trimming simply chops off te edge that is least interesting, and continues doing so, untill it reached the requested size."
83
83
  email: ber@webschuur.com
84
84
  executables: []
85
85
 
@@ -102,7 +102,10 @@ files:
102
102
  - doc/croptoelie_test.rb
103
103
  - doc/histogram.rb
104
104
  - lib/croptoelie.rb
105
+ - test/entropyish.png
106
+ - test/entropyish.txt
105
107
  - test/helper.rb
108
+ - test/profiler.rb
106
109
  - test/test_croptoelie.rb
107
110
  has_rdoc: true
108
111
  homepage: http://github.com/berkes/croptoelie
@@ -118,7 +121,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
118
121
  requirements:
119
122
  - - ">="
120
123
  - !ruby/object:Gem::Version
121
- hash: -1061105859
124
+ hash: -831961247
122
125
  segments:
123
126
  - 0
124
127
  version: "0"
@@ -137,4 +140,5 @@ specification_version: 3
137
140
  summary: Content aware cropper.
138
141
  test_files:
139
142
  - test/helper.rb
143
+ - test/profiler.rb
140
144
  - test/test_croptoelie.rb