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 +9 -2
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/croptoelie.gemspec +7 -3
- data/lib/croptoelie.rb +23 -69
- data/test/entropyish.png +0 -0
- data/test/entropyish.txt +0 -0
- data/test/helper.rb +1 -2
- data/test/profiler.rb +37 -0
- data/test/test_croptoelie.rb +37 -2
- metadata +8 -4
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.
|
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.
|
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.
|
1
|
+
0.4.0
|
data/croptoelie.gemspec
CHANGED
@@ -5,12 +5,12 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{croptoelie}
|
8
|
-
s.version = "0.
|
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-
|
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.
|
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
|
|
data/lib/croptoelie.rb
CHANGED
@@ -3,16 +3,15 @@ class CropToelie
|
|
3
3
|
include Magick
|
4
4
|
|
5
5
|
attr_accessor :orig
|
6
|
-
attr_accessor :
|
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
|
-
@
|
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
|
-
|
34
|
-
|
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
|
-
|
50
|
-
return
|
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
|
65
|
-
|
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
|
-
|
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
|
76
|
-
|
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),
|
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 -
|
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.
|
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
|
data/test/entropyish.png
ADDED
Binary file
|
data/test/entropyish.txt
ADDED
File without changes
|
data/test/helper.rb
CHANGED
@@ -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
|
data/test/profiler.rb
ADDED
@@ -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
|
data/test/test_croptoelie.rb
CHANGED
@@ -1,7 +1,42 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
class TestCroptoelie < Test::Unit::TestCase
|
4
|
-
|
5
|
-
|
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.
|
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-
|
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.
|
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: -
|
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
|