morandi 0.12.0 → 0.99.03
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -1
- data/README.md +6 -1
- data/ext/gdk_pixbuf_cairo/extconf.rb +124 -0
- data/ext/gdk_pixbuf_cairo/gdk_pixbuf_cairo.c +260 -0
- data/ext/morandi_native/extconf.rb +121 -0
- data/ext/morandi_native/filter.h +229 -0
- data/ext/morandi_native/gamma.h +48 -0
- data/ext/morandi_native/mask.h +75 -0
- data/ext/morandi_native/morandi_native.c +1126 -0
- data/ext/morandi_native/rotate.h +79 -0
- data/ext/morandi_native/tint.h +72 -0
- data/lib/gdk_pixbuf_cairo.so +0 -0
- data/lib/morandi/cairo_ext.rb +56 -0
- data/lib/morandi/crop_utils.rb +95 -0
- data/lib/morandi/image_operation.rb +205 -0
- data/lib/morandi/image_processor.rb +180 -180
- data/lib/morandi/pixbuf_ext.rb +19 -0
- data/lib/morandi/profiled_pixbuf.rb +40 -61
- data/lib/morandi/redeye.rb +35 -47
- data/lib/morandi/version.rb +3 -1
- data/lib/morandi.rb +33 -11
- data/lib/morandi_native.so +0 -0
- metadata +52 -117
- data/.gitignore +0 -18
- data/.rspec +0 -3
- data/.ruby-version +0 -1
- data/Gemfile +0 -4
- data/Rakefile +0 -1
- data/lib/morandi/image-ops.rb +0 -307
- data/lib/morandi/utils.rb +0 -136
- data/morandi.gemspec +0 -33
- data/sample/100_mb_image.jpg +0 -0
- data/sample/sample.jpg +0 -0
- data/spec/morandi_spec.rb +0 -208
- data/spec/spec_helper.rb +0 -19
@@ -1,245 +1,245 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'morandi/profiled_pixbuf'
|
2
4
|
require 'morandi/redeye'
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
-
attr_accessor :config
|
7
|
-
|
8
|
-
def self.default_icc_path(path)
|
9
|
-
"#{path}.icc.jpg"
|
10
|
-
end
|
6
|
+
module Morandi
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
11
8
|
|
12
|
-
|
13
|
-
|
9
|
+
# ImageProcessor transforms an image.
|
10
|
+
class ImageProcessor
|
11
|
+
attr_reader :options, :pb
|
12
|
+
attr_accessor :config
|
14
13
|
|
15
|
-
|
14
|
+
def initialize(file, user_options, local_options = {})
|
15
|
+
@file = file
|
16
16
|
|
17
|
-
|
18
|
-
@options = (local_options || {}).merge(user_options || {})
|
19
|
-
@local_options = local_options
|
17
|
+
user_options.keys.grep(/^path/).each { |k| user_options.delete(k) }
|
20
18
|
|
21
|
-
|
22
|
-
|
19
|
+
# Give priority to user_options
|
20
|
+
@options = (local_options || {}).merge(user_options || {})
|
21
|
+
@local_options = local_options
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@pb = @file
|
28
|
-
@scale = 1.0
|
23
|
+
@scale_to = @options['output.max']
|
24
|
+
@width = @options['output.width']
|
25
|
+
@height = @options['output.height']
|
29
26
|
end
|
30
|
-
end
|
31
27
|
|
32
|
-
|
33
|
-
|
34
|
-
|
28
|
+
def process!
|
29
|
+
case @file
|
30
|
+
when String
|
31
|
+
get_pixbuf
|
32
|
+
when GdkPixbuf::Pixbuf, Morandi::ProfiledPixbuf
|
33
|
+
@pb = @file
|
34
|
+
@scale = 1.0
|
35
|
+
end
|
35
36
|
|
36
|
-
|
37
|
-
|
37
|
+
# Apply Red-Eye corrections
|
38
|
+
apply_redeye!
|
38
39
|
|
39
|
-
|
40
|
-
|
40
|
+
# Apply contrast, brightness etc
|
41
|
+
apply_colour_manipulations!
|
41
42
|
|
42
|
-
|
43
|
-
|
43
|
+
# apply rotation
|
44
|
+
apply_rotate!
|
44
45
|
|
45
|
-
|
46
|
-
|
46
|
+
# apply crop
|
47
|
+
apply_crop!
|
47
48
|
|
48
|
-
|
49
|
-
|
49
|
+
# apply filter
|
50
|
+
apply_filters!
|
50
51
|
|
51
|
-
|
52
|
-
|
53
|
-
end
|
52
|
+
# add border
|
53
|
+
apply_decorations!
|
54
54
|
|
55
|
-
|
56
|
-
end
|
55
|
+
@pb = @pb.scale_max([@width, @height].max) if @options['output.limit'] && @width && @height
|
57
56
|
|
58
|
-
|
59
|
-
|
60
|
-
end
|
57
|
+
@pb
|
58
|
+
end
|
61
59
|
|
62
|
-
|
63
|
-
|
60
|
+
# Returns generated pixbuf
|
61
|
+
def result
|
62
|
+
process! unless @pb
|
63
|
+
@pb
|
64
|
+
end
|
65
|
+
|
66
|
+
def write_to_png(write_to, orientation = :any)
|
67
|
+
pb = @pb
|
64
68
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
69
|
+
case orientation
|
70
|
+
when :landscape
|
71
|
+
pb = @pb.rotate(90) if @pb.width < @pb.height
|
72
|
+
when :portrait
|
73
|
+
pb = @pb.rotate(90) if @pb.width > @pb.height
|
74
|
+
end
|
75
|
+
pb.save(write_to, 'png')
|
70
76
|
end
|
71
|
-
pb.save(fn, 'png')
|
72
|
-
end
|
73
77
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
+
def write_to_jpeg(write_to, quality = nil)
|
79
|
+
quality ||= options.fetch('quality', '97')
|
80
|
+
@pb.save(write_to, 'jpeg', quality: quality.to_s)
|
81
|
+
end
|
78
82
|
|
79
|
-
protected
|
80
|
-
def get_pixbuf
|
81
|
-
_, width, height = GdkPixbuf::Pixbuf.get_file_info(@file)
|
83
|
+
protected
|
82
84
|
|
83
|
-
|
84
|
-
|
85
|
-
@
|
85
|
+
def get_pixbuf
|
86
|
+
_, width, height = GdkPixbuf::Pixbuf.get_file_info(@file)
|
87
|
+
@pb = Morandi::ProfiledPixbuf.new(@file, @local_options, @scale_to)
|
86
88
|
@actual_max = [@pb.width, @pb.height].max
|
87
|
-
else
|
88
|
-
@pb = Morandi::ProfiledPixbuf.new(@file, @local_options)
|
89
|
-
@src_max = [@pb.width, @pb.height].max
|
90
|
-
@actual_max = [@pb.width, @pb.height].max
|
91
|
-
end
|
92
89
|
|
93
|
-
|
94
|
-
|
90
|
+
@src_max = if @scale_to
|
91
|
+
[width, height].max
|
92
|
+
else
|
93
|
+
[@pb.width, @pb.height].max
|
94
|
+
end
|
95
|
+
|
96
|
+
@scale = @actual_max / @src_max.to_f
|
97
|
+
end
|
98
|
+
|
99
|
+
SHARPEN = [
|
100
|
+
-1, -1, -1, -1, -1,
|
101
|
+
-1, 2, 2, 2, -1,
|
102
|
+
-1, 2, 8, 2, -1,
|
103
|
+
-1, 2, 2, 2, -1,
|
104
|
+
-1, -1, -1, -1, -1
|
105
|
+
].freeze
|
106
|
+
|
107
|
+
BLUR = [
|
108
|
+
0, 1, 1, 1, 0,
|
109
|
+
1, 1, 1, 1, 1,
|
110
|
+
1, 1, 1, 1, 1,
|
111
|
+
1, 1, 1, 1, 1,
|
112
|
+
0, 1, 1, 1, 0
|
113
|
+
].freeze
|
114
|
+
|
115
|
+
def apply_colour_manipulations!
|
116
|
+
if options['brighten'].to_i.nonzero?
|
117
|
+
brighten = (5 * options['brighten']).clamp(-100, 100)
|
118
|
+
@pb = MorandiNative::PixbufUtils.brightness(@pb, brighten)
|
119
|
+
end
|
95
120
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
-1, 2, 2, 2, -1,
|
101
|
-
-1, -1, -1, -1, -1,
|
102
|
-
]
|
103
|
-
BLUR = [
|
104
|
-
0, 1, 1, 1, 0,
|
105
|
-
1, 1, 1, 1, 1,
|
106
|
-
1, 1, 1, 1, 1,
|
107
|
-
1, 1, 1, 1, 1,
|
108
|
-
0, 1, 1, 1, 0,
|
109
|
-
]
|
110
|
-
|
111
|
-
def apply_colour_manipulations!
|
112
|
-
if options['brighten'].to_i.nonzero?
|
113
|
-
brighten = [ [ 5 * options['brighten'], -100 ].max, 100 ].min
|
114
|
-
@pb = PixbufUtils.brightness(@pb, brighten)
|
115
|
-
end
|
121
|
+
if options['gamma'] && not_equal_to_one(options['gamma'])
|
122
|
+
@pb = MorandiNative::PixbufUtils.gamma(@pb,
|
123
|
+
options['gamma'])
|
124
|
+
end
|
116
125
|
|
117
|
-
|
118
|
-
|
119
|
-
|
126
|
+
if options['contrast'].to_i.nonzero?
|
127
|
+
contrast = (5 * options['contrast']).clamp(-100, 100)
|
128
|
+
@pb = MorandiNative::PixbufUtils.contrast(@pb, contrast)
|
129
|
+
end
|
120
130
|
|
121
|
-
|
122
|
-
@pb = PixbufUtils.contrast(@pb, [ [ 5 * options['contrast'], -100 ].max, 100 ].min)
|
123
|
-
end
|
131
|
+
return unless options['sharpen'].to_i.nonzero?
|
124
132
|
|
125
|
-
|
126
|
-
if options['sharpen'] > 0
|
133
|
+
if options['sharpen'].positive?
|
127
134
|
[options['sharpen'], 5].min.times do
|
128
|
-
@pb = PixbufUtils.filter(@pb, SHARPEN, SHARPEN.inject(0, &:+))
|
135
|
+
@pb = MorandiNative::PixbufUtils.filter(@pb, SHARPEN, SHARPEN.inject(0, &:+))
|
129
136
|
end
|
130
|
-
elsif options['sharpen']
|
131
|
-
[
|
132
|
-
@pb = PixbufUtils.filter(@pb, BLUR, BLUR.inject(0, &:+))
|
137
|
+
elsif options['sharpen'].negative?
|
138
|
+
[(options['sharpen'] * -1), 5].min.times do
|
139
|
+
@pb = MorandiNative::PixbufUtils.filter(@pb, BLUR, BLUR.inject(0, &:+))
|
133
140
|
end
|
134
141
|
end
|
135
142
|
end
|
136
|
-
end
|
137
143
|
|
138
|
-
|
139
|
-
|
140
|
-
|
144
|
+
def apply_redeye!
|
145
|
+
(options['redeye'] || []).each do |eye|
|
146
|
+
@pb = Morandi::RedEye::TapRedEye.tap_on(@pb, eye[0] * @scale, eye[1] * @scale)
|
147
|
+
end
|
141
148
|
end
|
142
|
-
end
|
143
149
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
(360-a)%360
|
148
|
-
else
|
149
|
-
nil
|
150
|
+
def angle
|
151
|
+
a = options['angle'].to_i
|
152
|
+
(360 - a) % 360 if a
|
150
153
|
end
|
151
|
-
end
|
152
154
|
|
153
|
-
|
154
|
-
|
155
|
-
|
155
|
+
# modifies @pb with any applied rotation
|
156
|
+
def apply_rotate!
|
157
|
+
a = angle
|
158
|
+
|
159
|
+
@pb = @pb.rotate(a) unless (a % 360).zero?
|
156
160
|
|
157
|
-
|
158
|
-
|
161
|
+
unless options['straighten'].to_f.zero?
|
162
|
+
@pb = Morandi::Straighten.new_from_hash(angle: options['straighten'].to_f).call(nil,
|
163
|
+
@pb)
|
164
|
+
end
|
165
|
+
|
166
|
+
@image_width = @pb.width
|
167
|
+
@image_height = @pb.height
|
159
168
|
end
|
160
169
|
|
161
|
-
|
162
|
-
|
170
|
+
DEFAULT_CONFIG = {
|
171
|
+
'border-size-mm' => 5
|
172
|
+
}.freeze
|
173
|
+
def config_for(key)
|
174
|
+
return options[key] if options&.key?(key)
|
175
|
+
return @config[key] if @config&.key?(key)
|
176
|
+
|
177
|
+
DEFAULT_CONFIG[key]
|
163
178
|
end
|
164
179
|
|
165
|
-
|
166
|
-
|
167
|
-
end
|
180
|
+
def apply_crop!
|
181
|
+
crop = options['crop']
|
168
182
|
|
169
|
-
|
170
|
-
'border-size-mm' => 5
|
171
|
-
}
|
172
|
-
def config_for(key)
|
173
|
-
return options[key] if options && options.has_key?(key)
|
174
|
-
return @config[key] if @config && @config.has_key?(key)
|
175
|
-
DEFAULT_CONFIG[key]
|
176
|
-
end
|
183
|
+
return if crop.nil? && config_for('image.auto-crop').eql?(false)
|
177
184
|
|
178
|
-
|
179
|
-
def apply_crop!
|
180
|
-
crop = options['crop']
|
185
|
+
crop = crop.split(',').map(&:to_i) if crop.is_a?(String) && crop =~ /^\d+,\d+,\d+,\d+/
|
181
186
|
|
182
|
-
|
183
|
-
|
184
|
-
|
187
|
+
crop = nil unless crop.is_a?(Array) && crop.size.eql?(4) && crop.all? do |i|
|
188
|
+
i.is_a?(Numeric)
|
189
|
+
end
|
185
190
|
|
186
|
-
|
187
|
-
|
188
|
-
end
|
191
|
+
# can't crop, won't crop
|
192
|
+
return if @width.nil? && @height.nil? && crop.nil?
|
189
193
|
|
190
|
-
|
191
|
-
i.kind_of?(Numeric)
|
192
|
-
}
|
194
|
+
crop = crop.map { |s| (s.to_f * @scale).floor } if crop && not_equal_to_one(@scale)
|
193
195
|
|
194
|
-
|
195
|
-
return if @width.nil? && @height.nil? && crop.nil?
|
196
|
+
crop ||= Morandi::CropUtils.autocrop_coords(@pb.width, @pb.height, @width, @height)
|
196
197
|
|
197
|
-
|
198
|
-
crop = crop.map { |s| (s.to_f * @scale).floor }
|
198
|
+
@pb = Morandi::CropUtils.apply_crop(@pb, crop[0], crop[1], crop[2], crop[3])
|
199
199
|
end
|
200
200
|
|
201
|
-
|
201
|
+
def apply_filters!
|
202
|
+
filter = options['fx']
|
202
203
|
|
203
|
-
|
204
|
-
|
204
|
+
case filter
|
205
|
+
when 'greyscale', 'sepia', 'bluetone'
|
206
|
+
op = Morandi::Colourify.new_from_hash('filter' => filter)
|
207
|
+
else
|
208
|
+
return
|
209
|
+
end
|
210
|
+
@pb = op.call(nil, @pb)
|
211
|
+
end
|
205
212
|
|
213
|
+
def apply_decorations!
|
214
|
+
style = options['border-style']
|
215
|
+
colour = options['background-style']
|
206
216
|
|
207
|
-
|
208
|
-
|
217
|
+
return if style.nil? || style.eql?('none')
|
218
|
+
return if colour.eql?('none')
|
209
219
|
|
210
|
-
|
211
|
-
when 'greyscale'
|
212
|
-
op = Morandi::Colourify.new_from_hash('op' => filter)
|
213
|
-
when 'sepia', 'bluetone'
|
214
|
-
# could also set 'alpha' => (0.85 * 255).to_i
|
215
|
-
op = Morandi::Colourify.new_from_hash('op' => filter)
|
216
|
-
else
|
217
|
-
return
|
218
|
-
end
|
219
|
-
@pb = op.call(nil, @pb)
|
220
|
-
end
|
220
|
+
colour ||= 'black'
|
221
221
|
|
222
|
-
|
223
|
-
|
222
|
+
crop = options['crop']
|
223
|
+
crop = crop.map { |s| (s.to_f * @scale).floor } if crop && not_equal_to_one(@scale)
|
224
224
|
|
225
|
-
|
226
|
-
|
227
|
-
|
225
|
+
op = Morandi::ImageBorder.new_from_hash(
|
226
|
+
'style' => style,
|
227
|
+
'colour' => colour || '#000000',
|
228
|
+
'crop' => crop,
|
229
|
+
'size' => [@image_width, @image_height],
|
230
|
+
'print_size' => [@width, @height],
|
231
|
+
'shrink' => true,
|
232
|
+
'border_size' => @scale * config_for('border-size-mm').to_i * 300 / 25.4 # 5mm at 300dpi
|
233
|
+
)
|
228
234
|
|
229
|
-
|
230
|
-
|
235
|
+
@pb = op.call(nil, @pb)
|
236
|
+
end
|
231
237
|
|
232
|
-
|
233
|
-
'style' => style,
|
234
|
-
'colour' => colour || '#000000',
|
235
|
-
'crop' => crop,
|
236
|
-
'size' => [@image_width, @image_height],
|
237
|
-
'print_size' => [@width, @height],
|
238
|
-
'shrink' => true,
|
239
|
-
'border_size' => @scale * config_for('border-size-mm').to_i * 300 / 25.4 # 5mm at 300dpi
|
240
|
-
})
|
238
|
+
private
|
241
239
|
|
242
|
-
|
240
|
+
def not_equal_to_one(float)
|
241
|
+
(float - 1.0).abs >= Float::EPSILON
|
242
|
+
end
|
243
243
|
end
|
244
|
-
|
244
|
+
# rubocop:enable Metrics/ClassLength
|
245
245
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gdk_pixbuf2'
|
4
|
+
|
5
|
+
# GdkPixbuf module / hierachy
|
6
|
+
module GdkPixbuf
|
7
|
+
# Add #to_cairo_image_surface for converting pixels to Cairo::ImageSurface format (RGBA->ARGB)
|
8
|
+
class Pixbuf
|
9
|
+
def to_cairo_image_surface
|
10
|
+
GdkPixbufCairo.pixbuf_to_surface(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def scale_max(max_size, interp = GdkPixbuf::InterpType::BILINEAR, _max_scale = 1.0)
|
14
|
+
mul = (max_size / [width, height].max.to_f)
|
15
|
+
mul = [1.0, mul].min
|
16
|
+
scale(width * mul, height * mul, interp)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,82 +1,61 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
def valid_jpeg?(filename)
|
5
|
-
return false unless File.exist?(filename)
|
6
|
-
return false unless File.size(filename) > 0
|
3
|
+
require 'gdk_pixbuf2'
|
7
4
|
|
8
|
-
|
5
|
+
module Morandi
|
6
|
+
# ProfiledPixbuf is a descendent of GdkPixbuf::Pixbuf with ICC support.
|
7
|
+
# It attempts to load an image using jpegicc/littlecms to ensure that it is sRGB.
|
8
|
+
class ProfiledPixbuf < GdkPixbuf::Pixbuf
|
9
|
+
def valid_jpeg?(filename)
|
10
|
+
return false unless File.exist?(filename)
|
11
|
+
return false unless File.size(filename).positive?
|
9
12
|
|
10
|
-
|
11
|
-
rescue
|
12
|
-
false
|
13
|
-
end
|
13
|
+
type, = GdkPixbuf::Pixbuf.get_file_info(filename)
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
loader.write(string.byteslice(i * chunk_size, chunk_size))
|
15
|
+
type && type.name.eql?('jpeg')
|
16
|
+
rescue StandardError
|
17
|
+
false
|
19
18
|
end
|
20
|
-
loader.close
|
21
|
-
loader.pixbuf
|
22
|
-
end
|
23
19
|
|
24
|
-
|
25
|
-
|
26
|
-
|
20
|
+
# TODO: this doesn't use lcms
|
21
|
+
def self.from_string(string, loader: nil, chunk_size: 4096)
|
22
|
+
loader ||= GdkPixbuf::PixbufLoader.new
|
23
|
+
((string.bytesize + chunk_size - 1) / chunk_size).times do |i|
|
24
|
+
loader.write(string.byteslice(i * chunk_size, chunk_size))
|
25
|
+
end
|
26
|
+
loader.close
|
27
|
+
loader.pixbuf
|
28
|
+
end
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
+
def self.default_icc_path(path)
|
31
|
+
"#{path}.icc.jpg"
|
32
|
+
end
|
30
33
|
|
31
|
-
|
32
|
-
@
|
34
|
+
def initialize(file, local_options, scale_to = nil)
|
35
|
+
@local_options = local_options
|
36
|
+
@file = file
|
33
37
|
|
34
38
|
if suitable_for_jpegicc?
|
35
39
|
icc_file = icc_cache_path
|
36
|
-
|
37
|
-
|
40
|
+
valid_jpeg?(icc_file) || system('jpgicc', '-q97', @file, icc_file)
|
41
|
+
file = icc_file if valid_jpeg?(icc_file)
|
38
42
|
end
|
39
|
-
end
|
40
|
-
|
41
|
-
# TODO: This is to fix some deprecation warnings. This needs refactoring.
|
42
|
-
# All can be implemented without having to hack on the PixBuff gem.
|
43
|
-
case args.size
|
44
|
-
when 1
|
45
|
-
super(file: args.first)
|
46
|
-
when 3
|
47
|
-
super(path: args[0], width: args[1], height: args[2])
|
48
|
-
else
|
49
|
-
super(*args)
|
50
|
-
end
|
51
|
-
rescue GdkPixbuf::PixbufError::CorruptImage => e
|
52
|
-
if args[0].is_a?(String) && defined? Tempfile
|
53
|
-
temp = Tempfile.new
|
54
|
-
pixbuf = self.class.from_string(File.read(args[0]))
|
55
|
-
pixbuf.save(temp.path, 'jpeg')
|
56
|
-
args[0] = temp.path
|
57
43
|
|
58
|
-
if
|
59
|
-
super file:
|
44
|
+
if scale_to
|
45
|
+
super(file: file, width: scale_to, height: scale_to)
|
60
46
|
else
|
61
|
-
super(
|
47
|
+
super(file: file)
|
62
48
|
end
|
63
|
-
|
64
|
-
temp.close
|
65
|
-
temp.unlink
|
66
|
-
else
|
67
|
-
throw e
|
68
49
|
end
|
69
|
-
end
|
70
50
|
|
51
|
+
private
|
71
52
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
type && type.name.eql?('jpeg')
|
77
|
-
end
|
53
|
+
def suitable_for_jpegicc?
|
54
|
+
valid_jpeg?(@file)
|
55
|
+
end
|
78
56
|
|
79
|
-
|
80
|
-
|
57
|
+
def icc_cache_path
|
58
|
+
@local_options['path.icc'] || Morandi::ProfiledPixbuf.default_icc_path(@file)
|
59
|
+
end
|
81
60
|
end
|
82
61
|
end
|
data/lib/morandi/redeye.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Morandi
|
4
4
|
module RedEye
|
@@ -6,57 +6,45 @@ module Morandi
|
|
6
6
|
# The reason for its existence is to prevent the situations when the bigger red area causes an excessive correction
|
7
7
|
# e.g. continuous red eyeglasses frame or sunburnt person's skin around eyes forming an area
|
8
8
|
RED_AREA_DENSITY_THRESHOLD = 0.3
|
9
|
-
end
|
10
|
-
end
|
11
9
|
|
12
|
-
|
13
|
-
module
|
14
|
-
|
15
|
-
module_function
|
16
|
-
def tap_on(pb, x, y)
|
17
|
-
n = ([pb.height,pb.width].max / 10)
|
18
|
-
x1 = [x - n, 0].max
|
19
|
-
x2 = [x + n, pb.width].min
|
20
|
-
y1 = [y - n, 0].max
|
21
|
-
y2 = [y + n, pb.height].min
|
22
|
-
return pb unless (x1 >= 0) && (x2 > x1) && (y1 >= 0) && (y2 > y1)
|
23
|
-
redeye = ::RedEye.new(pb, x1, y1, x2, y2)
|
24
|
-
|
25
|
-
sensitivity = 2
|
26
|
-
blobs = redeye.identify_blobs(sensitivity).reject { |region|
|
27
|
-
region.noPixels < 4 || !region.squareish?(0.5, RED_AREA_DENSITY_THRESHOLD)
|
28
|
-
}.sort_by { |region|
|
29
|
-
region.area_min_x = x1
|
30
|
-
region.area_min_y = y1
|
31
|
-
|
32
|
-
# Higher is better
|
33
|
-
score = (region.noPixels) / (region.distance_from(x, y) ** 2)
|
34
|
-
}
|
35
|
-
|
36
|
-
blob = blobs.last
|
37
|
-
redeye.correct_blob(blob.id) if blob
|
38
|
-
pb = redeye.pixbuf
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
10
|
+
# RedEye finder that looks for "eye" closest to a point
|
11
|
+
module TapRedEye
|
12
|
+
module_function
|
43
13
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
14
|
+
def tap_on(pixbuf, x_coord, y_coord)
|
15
|
+
n = ([pixbuf.height, pixbuf.width].max / 10)
|
16
|
+
x1 = [x_coord - n, 0].max
|
17
|
+
x2 = [x_coord + n, pixbuf.width].min
|
18
|
+
y1 = [y_coord - n, 0].max
|
19
|
+
y2 = [y_coord + n, pixbuf.height].min
|
20
|
+
|
21
|
+
return pixbuf unless (x1 >= 0) && (x2 > x1) && (y1 >= 0) && (y2 > y1)
|
51
22
|
|
52
|
-
|
53
|
-
def distance_from(x,y)
|
54
|
-
cx,cy = centre()
|
23
|
+
red_eye = MorandiNative::RedEye.new(pixbuf, x1, y1, x2, y2)
|
55
24
|
|
56
|
-
|
57
|
-
|
25
|
+
sensitivity = 2
|
26
|
+
blobs = red_eye.identify_blobs(sensitivity).reject do |region|
|
27
|
+
region.noPixels < 4 || !region.squareish?(0.5, RED_AREA_DENSITY_THRESHOLD)
|
28
|
+
end
|
58
29
|
|
59
|
-
|
30
|
+
sorted_blobs = blobs.sort_by do |region|
|
31
|
+
region.area_min_x = x1
|
32
|
+
region.area_min_y = y1
|
33
|
+
end
|
34
|
+
|
35
|
+
blob = sorted_blobs.last
|
36
|
+
red_eye.correct_blob(blob.id) if blob
|
37
|
+
red_eye.pixbuf
|
38
|
+
end
|
39
|
+
end
|
60
40
|
end
|
61
41
|
end
|
62
42
|
|
43
|
+
module MorandiNative
|
44
|
+
class RedEye
|
45
|
+
# Represents an area with a suspected red eye
|
46
|
+
class Region
|
47
|
+
attr_accessor :area_min_x, :area_min_y
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/morandi/version.rb
CHANGED