ruby_marks 0.0.5.dev → 0.1.0
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.
- data/README.rdoc +1 -1
- data/lib/ruby_marks/clock_mark.rb +26 -46
- data/lib/ruby_marks/config.rb +48 -8
- data/lib/ruby_marks/group.rb +9 -6
- data/lib/ruby_marks/{rgb.rb → image_utils.rb} +18 -1
- data/lib/ruby_marks/recognizer.rb +435 -0
- data/lib/ruby_marks/version.rb +1 -1
- data/lib/ruby_marks/watcher.rb +18 -0
- data/lib/ruby_marks.rb +37 -5
- data/test/ruby_marks/clock_mark_test.rb +20 -14
- data/test/ruby_marks/group_test.rb +3 -2
- data/test/ruby_marks/{rgb_test.rb → image_utils_test.rb} +3 -3
- data/test/ruby_marks/recognizer_test.rb +153 -0
- data/test/ruby_marks/watcher_test.rb +11 -0
- metadata +13 -10
- data/lib/ruby_marks/document.rb +0 -161
- data/test/ruby_marks/document_test.rb +0 -141
data/README.rdoc
CHANGED
@@ -3,80 +3,60 @@ module RubyMarks
|
|
3
3
|
|
4
4
|
class ClockMark
|
5
5
|
|
6
|
-
attr_accessor :
|
6
|
+
attr_accessor :recognizer, :coordinates
|
7
7
|
|
8
8
|
def initialize(params={})
|
9
9
|
params.each do |k, v|
|
10
10
|
self.send("#{k}=", v) if self.respond_to?("#{k}=")
|
11
11
|
end
|
12
|
-
@coordinates = {x1: 0, x2: 0, y1: 0, y2: 0}
|
13
|
-
self.calc_coordinates
|
14
12
|
end
|
15
13
|
|
16
|
-
def
|
17
|
-
@coordinates.tap do |coordinates|
|
18
|
-
if self.document
|
19
|
-
x = position[:x]
|
20
|
-
y = position[:y]
|
21
|
-
|
22
|
-
coordinates[:x1] = x
|
23
|
-
loop do
|
24
|
-
coordinates[:x1] -= 1
|
25
|
-
color = self.document.file.pixel_color(coordinates[:x1], y)
|
26
|
-
color = RubyMarks::RGB.to_hex(color.red, color.green, color.blue)
|
14
|
+
def valid?
|
27
15
|
|
28
|
-
|
29
|
-
|
16
|
+
return false if !self.recognizer.config.clock_width_tolerance_range.include?(self.width) ||
|
17
|
+
!self.recognizer.config.clock_height_tolerance_range.include?(self.height)
|
18
|
+
|
19
|
+
x_pos = coordinates[:x1]..coordinates[:x2]
|
20
|
+
y_pos = coordinates[:y1]..coordinates[:y2]
|
30
21
|
|
31
|
-
|
32
|
-
loop do
|
33
|
-
coordinates[:x2] += 1
|
34
|
-
color = self.document.file.pixel_color(coordinates[:x2], y)
|
35
|
-
color = RubyMarks::RGB.to_hex(color.red, color.green, color.blue)
|
22
|
+
colors = []
|
36
23
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
color = self.document.file.pixel_color(x, coordinates[:y1])
|
44
|
-
color = RubyMarks::RGB.to_hex(color.red, color.green, color.blue)
|
45
|
-
|
46
|
-
break if color != "#000000" || coordinates[:y1] <= 0
|
47
|
-
end
|
48
|
-
|
49
|
-
coordinates[:y2] = y
|
50
|
-
loop do
|
51
|
-
coordinates[:y2] += 1
|
52
|
-
color = self.document.file.pixel_color(x, coordinates[:y2])
|
53
|
-
color = RubyMarks::RGB.to_hex(color.red, color.green, color.blue)
|
54
|
-
|
55
|
-
break if color != "#000000" || coordinates[:y2] >= self.document.file.page.height
|
56
|
-
end
|
24
|
+
y_pos.each do |y|
|
25
|
+
x_pos.each do |x|
|
26
|
+
color = self.recognizer.file.pixel_color(x, y)
|
27
|
+
color = RubyMarks::ImageUtils.to_hex(color.red, color.green, color.blue)
|
28
|
+
color = recognizer.config.recognition_colors.include?(color) ? "." : " "
|
29
|
+
colors << color
|
57
30
|
end
|
58
31
|
end
|
32
|
+
|
33
|
+
intensity = colors.count(".") * 100 / colors.size
|
34
|
+
return intensity >= 70 ? true : false
|
35
|
+
end
|
36
|
+
|
37
|
+
def invalid?
|
38
|
+
!valid?
|
59
39
|
end
|
60
40
|
|
61
41
|
def width
|
62
|
-
coordinates[:
|
42
|
+
RubyMarks::ImageUtils.calc_width(@coordinates[:x1], @coordinates[:x2])
|
63
43
|
end
|
64
44
|
|
65
45
|
def height
|
66
|
-
coordinates[:
|
46
|
+
RubyMarks::ImageUtils.calc_height(@coordinates[:y1], @coordinates[:y2])
|
67
47
|
end
|
68
48
|
|
69
49
|
def horizontal_middle_position
|
70
|
-
coordinates[:x1]
|
50
|
+
RubyMarks::ImageUtils.calc_middle_horizontal(@coordinates[:x1], self.width)
|
71
51
|
end
|
72
52
|
|
73
53
|
def vertical_middle_position
|
74
|
-
coordinates[:y1]
|
54
|
+
RubyMarks::ImageUtils.calc_middle_vertical(@coordinates[:y1], self.height)
|
75
55
|
end
|
76
56
|
|
77
57
|
def to_s
|
78
58
|
self.coordinates
|
79
59
|
end
|
80
|
-
end
|
81
60
|
|
61
|
+
end
|
82
62
|
end
|
data/lib/ruby_marks/config.rb
CHANGED
@@ -3,21 +3,61 @@ module RubyMarks
|
|
3
3
|
|
4
4
|
class Config
|
5
5
|
|
6
|
-
attr_accessor :clock_marks_scan_x, :
|
7
|
-
:default_distance_between_marks
|
6
|
+
attr_accessor :clock_marks_scan_x, :expected_clocks_count, :intensity_percentual, :recognition_colors,
|
7
|
+
:default_marks_options, :default_distance_between_marks,
|
8
|
+
:clock_width, :clock_height, :threshold_level, :clock_mark_size_tolerance,
|
9
|
+
:default_mark_width, :default_mark_height
|
8
10
|
|
9
|
-
def initialize(
|
10
|
-
@
|
11
|
-
@
|
11
|
+
def initialize(recognizer)
|
12
|
+
@recognizer = recognizer
|
13
|
+
@threshold_level = RubyMarks.threshold_level
|
14
|
+
|
12
15
|
@intensity_percentual = RubyMarks.intensity_percentual
|
13
|
-
@recognition_colors
|
16
|
+
@recognition_colors = RubyMarks.recognition_colors
|
17
|
+
|
18
|
+
@expected_clocks_count = RubyMarks.expected_clocks_count
|
19
|
+
@clock_marks_scan_x = RubyMarks.clock_marks_scan_x
|
20
|
+
@clock_width = RubyMarks.clock_width
|
21
|
+
@clock_height = RubyMarks.clock_height
|
22
|
+
@clock_mark_size_tolerance = RubyMarks.clock_mark_size_tolerance
|
23
|
+
|
24
|
+
@default_mark_width = RubyMarks.default_mark_width
|
25
|
+
@default_mark_height = RubyMarks.default_mark_height
|
14
26
|
@default_marks_options = RubyMarks.default_marks_options
|
15
27
|
@default_distance_between_marks = RubyMarks.default_distance_between_marks
|
16
28
|
end
|
17
29
|
|
30
|
+
def calculated_threshold_level
|
31
|
+
Magick::QuantumRange * (@threshold_level.to_f / 100)
|
32
|
+
end
|
33
|
+
|
34
|
+
def clock_width_with_down_tolerance
|
35
|
+
@clock_width - @clock_mark_size_tolerance
|
36
|
+
end
|
37
|
+
|
38
|
+
def clock_width_with_up_tolerance
|
39
|
+
@clock_width + @clock_mark_size_tolerance
|
40
|
+
end
|
41
|
+
|
42
|
+
def clock_height_with_down_tolerance
|
43
|
+
@clock_height - @clock_mark_size_tolerance
|
44
|
+
end
|
45
|
+
|
46
|
+
def clock_height_with_up_tolerance
|
47
|
+
@clock_height + @clock_mark_size_tolerance
|
48
|
+
end
|
49
|
+
|
50
|
+
def clock_width_tolerance_range
|
51
|
+
clock_width_with_down_tolerance..clock_width_with_up_tolerance
|
52
|
+
end
|
53
|
+
|
54
|
+
def clock_height_tolerance_range
|
55
|
+
clock_height_with_down_tolerance..clock_height_with_up_tolerance
|
56
|
+
end
|
57
|
+
|
18
58
|
def define_group(group_label, &block)
|
19
|
-
group = RubyMarks::Group.new(group_label, @
|
20
|
-
@
|
59
|
+
group = RubyMarks::Group.new(group_label, @recognizer, &block)
|
60
|
+
@recognizer.add_group(group)
|
21
61
|
end
|
22
62
|
|
23
63
|
def configure
|
data/lib/ruby_marks/group.rb
CHANGED
@@ -2,15 +2,18 @@
|
|
2
2
|
module RubyMarks
|
3
3
|
|
4
4
|
class Group
|
5
|
+
attr_reader :label, :recognizer, :clocks_range
|
5
6
|
|
6
|
-
|
7
|
-
|
7
|
+
attr_accessor :mark_width, :mark_height, :marks_options, :x_distance_from_clock,
|
8
|
+
:distance_between_marks
|
8
9
|
|
9
|
-
def initialize(label,
|
10
|
+
def initialize(label, recognizer)
|
10
11
|
@label = label
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@
|
12
|
+
@recognizer = recognizer
|
13
|
+
@mark_width = @recognizer.config.default_mark_width
|
14
|
+
@mark_height = @recognizer.config.default_mark_height
|
15
|
+
@marks_options = @recognizer.config.default_marks_options
|
16
|
+
@distance_between_marks = @recognizer.config.default_distance_between_marks
|
14
17
|
@x_distance_from_clock = 0
|
15
18
|
@clocks_range = 0..0
|
16
19
|
yield self if block_given?
|
@@ -1,7 +1,24 @@
|
|
1
1
|
#encoding: utf-8
|
2
2
|
module RubyMarks
|
3
3
|
|
4
|
-
class
|
4
|
+
class ImageUtils
|
5
|
+
|
6
|
+
def self.calc_width(x1, x2)
|
7
|
+
x2.to_i - x1.to_i + 1
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.calc_height(y1, y2)
|
11
|
+
y2.to_i - y1.to_i + 1
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.calc_middle_horizontal(x, width)
|
15
|
+
x.to_i + width.to_i / 2
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.calc_middle_vertical(y, height)
|
19
|
+
y.to_i + height.to_i / 2
|
20
|
+
end
|
21
|
+
|
5
22
|
|
6
23
|
def self.to_hex(red, green, blue)
|
7
24
|
red = get_hex_from_color(red)
|
@@ -0,0 +1,435 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
module RubyMarks
|
3
|
+
|
4
|
+
class Recognizer
|
5
|
+
|
6
|
+
attr_reader :file, :raised_watchers, :groups, :watchers
|
7
|
+
|
8
|
+
attr_accessor :current_position, :clock_marks, :config
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
self.reset_document
|
12
|
+
@groups = {}
|
13
|
+
self.create_config
|
14
|
+
end
|
15
|
+
|
16
|
+
def file=(file)
|
17
|
+
self.reset_document
|
18
|
+
@file = nil
|
19
|
+
@file = Magick::Image.read(file).first
|
20
|
+
@file = @file.threshold(@config.calculated_threshold_level)
|
21
|
+
end
|
22
|
+
|
23
|
+
def reset_document
|
24
|
+
@current_position = {x: 0, y: 0}
|
25
|
+
@clock_marks = []
|
26
|
+
@raised_watchers = []
|
27
|
+
@watchers = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_config
|
31
|
+
@config ||= RubyMarks::Config.new(self)
|
32
|
+
end
|
33
|
+
|
34
|
+
def filename
|
35
|
+
@file && @file.filename
|
36
|
+
end
|
37
|
+
|
38
|
+
def configure(&block)
|
39
|
+
self.create_config
|
40
|
+
@config.configure(&block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_group(group)
|
44
|
+
@groups[group.label] = group if group
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_watcher(watcher_name, &block)
|
48
|
+
watcher = RubyMarks::Watcher.new(watcher_name, self, &block)
|
49
|
+
@watchers[watcher.name] = watcher if watcher
|
50
|
+
end
|
51
|
+
|
52
|
+
def raise_watcher(name, *args)
|
53
|
+
watcher = @watchers[name]
|
54
|
+
if watcher
|
55
|
+
@raised_watchers << watcher.name unless @raised_watchers.include?(watcher.name)
|
56
|
+
watcher.run(*args)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def move_to(x, y)
|
61
|
+
@current_position = {x: @current_position[:x] + x, y: @current_position[:y] + y}
|
62
|
+
end
|
63
|
+
|
64
|
+
def marked?(expected_width, expected_height)
|
65
|
+
raise IOError, "There's a invalid or missing file" if @file.nil?
|
66
|
+
|
67
|
+
if self.current_position
|
68
|
+
|
69
|
+
neighborhood_x = current_position[:x]-1..current_position[:x]+1
|
70
|
+
neighborhood_y = current_position[:y]-1..current_position[:y]+1
|
71
|
+
|
72
|
+
neighborhood_y.each do |current_y|
|
73
|
+
neighborhood_x.each do |current_x|
|
74
|
+
|
75
|
+
color = @file.pixel_color(current_x, current_y)
|
76
|
+
color = RubyMarks::ImageUtils.to_hex(color.red, color.green, color.blue)
|
77
|
+
|
78
|
+
if @config.recognition_colors.include?(color)
|
79
|
+
stack = flood_scan(current_x, current_y)
|
80
|
+
|
81
|
+
x_elements = []
|
82
|
+
y_elements = []
|
83
|
+
stack.each do |k,v|
|
84
|
+
stack[k].inject(x_elements, :<<)
|
85
|
+
y_elements << k
|
86
|
+
end
|
87
|
+
|
88
|
+
x_elements.sort!.uniq!
|
89
|
+
y_elements.sort!.uniq!
|
90
|
+
|
91
|
+
x1 = x_elements.first || 0
|
92
|
+
x2 = x_elements.last || 0
|
93
|
+
y1 = y_elements.first || 0
|
94
|
+
y2 = y_elements.last || 0
|
95
|
+
|
96
|
+
current_width = RubyMarks::ImageUtils.calc_width(x1, x2)
|
97
|
+
current_height = RubyMarks::ImageUtils.calc_height(y1, y2)
|
98
|
+
|
99
|
+
if current_width >= expected_width + 2
|
100
|
+
distance_x1 = current_x - x1
|
101
|
+
distance_x2 = x2 - current_x
|
102
|
+
|
103
|
+
if distance_x1 <= distance_x2
|
104
|
+
x2 = x1 + expected_width
|
105
|
+
else
|
106
|
+
x1 = x2 - expected_width
|
107
|
+
end
|
108
|
+
current_width = RubyMarks::ImageUtils.calc_width(x1, x2)
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
if current_height >= expected_height + 2
|
113
|
+
distance_y1 = current_y - y1
|
114
|
+
distance_y2 = y2 - current_y
|
115
|
+
|
116
|
+
if distance_y1 <= distance_y2
|
117
|
+
y2 = y1 + expected_height
|
118
|
+
else
|
119
|
+
y1 = y2 - expected_height
|
120
|
+
end
|
121
|
+
current_height = RubyMarks::ImageUtils.calc_height(y1, y2)
|
122
|
+
end
|
123
|
+
|
124
|
+
if (current_width >= expected_width - 4 && current_width <= expected_width + 4) &&
|
125
|
+
(current_height >= expected_height - 4 && current_height <= expected_height + 4)
|
126
|
+
|
127
|
+
colors = []
|
128
|
+
|
129
|
+
x_pos = x1..x2
|
130
|
+
y_pos = y1..y2
|
131
|
+
|
132
|
+
y_pos.each do |y|
|
133
|
+
x_pos.each do |x|
|
134
|
+
color = @file.pixel_color(x, y)
|
135
|
+
color = RubyMarks::ImageUtils.to_hex(color.red, color.green, color.blue)
|
136
|
+
color = @config.recognition_colors.include?(color) ? "." : " "
|
137
|
+
colors << color
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
intensity = colors.count(".") * 100 / colors.size
|
142
|
+
return true if intensity >= @config.intensity_percentual
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
return false
|
151
|
+
end
|
152
|
+
|
153
|
+
def unmarked?(x_pos, y_pos)
|
154
|
+
!marked?(x_pos, y_pos)
|
155
|
+
end
|
156
|
+
|
157
|
+
def scan
|
158
|
+
raise IOError, "There's a invalid or missing file" if @file.nil?
|
159
|
+
|
160
|
+
unmarked_group_found = false
|
161
|
+
multiple_marked_found = false
|
162
|
+
|
163
|
+
result = {}
|
164
|
+
result.tap do |result|
|
165
|
+
position_before = @current_position
|
166
|
+
scan_clock_marks unless clock_marks.any?
|
167
|
+
|
168
|
+
return false if self.config.expected_clocks_count > 0 && @clock_marks.count != self.config.expected_clocks_count
|
169
|
+
clock_marks.each_with_index do |clock_mark, index|
|
170
|
+
group_hash = {}
|
171
|
+
@groups.each do |key, group|
|
172
|
+
if group.belongs_to_clock?(index + 1)
|
173
|
+
@current_position = {x: clock_mark.coordinates[:x2], y: clock_mark.vertical_middle_position}
|
174
|
+
move_to(group.x_distance_from_clock, 0)
|
175
|
+
markeds = []
|
176
|
+
group.marks_options.each do |mark|
|
177
|
+
markeds << mark if marked?(group.mark_width, group.mark_height)
|
178
|
+
move_to(group.distance_between_marks, 0)
|
179
|
+
end
|
180
|
+
if markeds.any?
|
181
|
+
group_hash["group_#{key}".to_sym] = markeds
|
182
|
+
multiple_marked_found = true if markeds.size > 1
|
183
|
+
else
|
184
|
+
unmarked_group_found = true
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
result["clock_#{index+1}".to_sym] = group_hash if group_hash.any?
|
189
|
+
end
|
190
|
+
@current_position = position_before
|
191
|
+
raise_watcher :scan_unmarked_watcher, result if unmarked_group_found
|
192
|
+
raise_watcher :scan_multiple_marked_watcher, result if multiple_marked_found
|
193
|
+
raise_watcher :scan_mark_watcher, result, unmarked_group_found, multiple_marked_found if unmarked_group_found || multiple_marked_found
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def flag_position
|
198
|
+
|
199
|
+
raise IOError, "There's a invalid or missing file" if @file.nil?
|
200
|
+
|
201
|
+
file = @file.dup
|
202
|
+
|
203
|
+
file.tap do |file|
|
204
|
+
if current_position
|
205
|
+
add_mark file
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def flag_all_marks
|
211
|
+
|
212
|
+
raise IOError, "There's a invalid or missing file" if @file.nil?
|
213
|
+
|
214
|
+
file = @file.dup
|
215
|
+
|
216
|
+
file.tap do |file|
|
217
|
+
position_before = @current_position
|
218
|
+
|
219
|
+
scan_clock_marks unless clock_marks.any?
|
220
|
+
|
221
|
+
clock_marks.each_with_index do |clock, index|
|
222
|
+
dr = Magick::Draw.new
|
223
|
+
dr.fill(RubyMarks::COLORS[5])
|
224
|
+
dr.rectangle(clock.coordinates[:x1], clock.coordinates[:y1], clock.coordinates[:x2], clock.coordinates[:y2])
|
225
|
+
dr.draw(file)
|
226
|
+
end
|
227
|
+
|
228
|
+
clock_marks.each_with_index do |clock_mark, index|
|
229
|
+
@groups.each do |key, group|
|
230
|
+
if group.belongs_to_clock?(index + 1)
|
231
|
+
@current_position = {x: clock_mark.coordinates[:x2], y: clock_mark.vertical_middle_position}
|
232
|
+
move_to(group.x_distance_from_clock, 0)
|
233
|
+
group.marks_options.each do |mark|
|
234
|
+
add_mark file
|
235
|
+
move_to(group.distance_between_marks, 0)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
@current_position = position_before
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def scan_clock_marks
|
246
|
+
|
247
|
+
raise IOError, "There's a invalid or missing file" if @file.nil?
|
248
|
+
|
249
|
+
@clock_marks = []
|
250
|
+
x = @config.clock_marks_scan_x
|
251
|
+
total_height = @file && @file.page.height || 0
|
252
|
+
@clock_marks.tap do |clock_marks|
|
253
|
+
current_y = 0
|
254
|
+
loop do
|
255
|
+
|
256
|
+
break if current_y > total_height
|
257
|
+
|
258
|
+
color = @file.pixel_color(x, current_y)
|
259
|
+
color = RubyMarks::ImageUtils.to_hex(color.red, color.green, color.blue)
|
260
|
+
|
261
|
+
if @config.recognition_colors.include?(color)
|
262
|
+
stack = flood_scan(x, current_y)
|
263
|
+
|
264
|
+
x_elements = []
|
265
|
+
y_elements = []
|
266
|
+
stack.each do |k,v|
|
267
|
+
stack[k].inject(x_elements, :<<)
|
268
|
+
y_elements << k
|
269
|
+
end
|
270
|
+
|
271
|
+
x_elements.sort!.uniq!
|
272
|
+
y_elements.sort!.uniq!
|
273
|
+
last_y = y_elements.last
|
274
|
+
|
275
|
+
loop do
|
276
|
+
stack_modified = false
|
277
|
+
|
278
|
+
|
279
|
+
x_elements.each do |col|
|
280
|
+
element_count = 0
|
281
|
+
y_elements.each do |row|
|
282
|
+
element_count += 1 if stack[row].include?(col)
|
283
|
+
end
|
284
|
+
|
285
|
+
if element_count > 0 && element_count < self.config.clock_height_with_down_tolerance
|
286
|
+
current_width = RubyMarks::ImageUtils.calc_width(x_elements.first, x_elements.last)
|
287
|
+
middle = RubyMarks::ImageUtils.calc_middle_horizontal(x_elements.first, current_width)
|
288
|
+
|
289
|
+
x_elements.delete_if do |el|
|
290
|
+
col <= middle && el <= col || col >= middle && el >= col
|
291
|
+
end
|
292
|
+
|
293
|
+
stack_modified = true
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
y_elements.each do |row|
|
298
|
+
if stack[row].count < self.config.clock_width_with_down_tolerance
|
299
|
+
current_height = RubyMarks::ImageUtils.calc_height(y_elements.first, y_elements.last)
|
300
|
+
middle = RubyMarks::ImageUtils.calc_middle_vertical(y_elements.first, current_height)
|
301
|
+
|
302
|
+
y_elements.delete_if do |ln|
|
303
|
+
row <= middle && ln <= row || row >= middle && ln >= row
|
304
|
+
end
|
305
|
+
|
306
|
+
stack_modified = true
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
break unless stack_modified
|
311
|
+
end
|
312
|
+
|
313
|
+
x1 = x_elements.first || 0
|
314
|
+
x2 = x_elements.last || 0
|
315
|
+
y1 = y_elements.first || 0
|
316
|
+
y2 = y_elements.last || 0
|
317
|
+
end
|
318
|
+
|
319
|
+
clock = RubyMarks::ClockMark.new(recognizer: self, coordinates: {x1: x1, x2: x2, y1: y1, y2: y2})
|
320
|
+
|
321
|
+
if clock.valid?
|
322
|
+
clock_marks << clock
|
323
|
+
current_y = last_y
|
324
|
+
end
|
325
|
+
|
326
|
+
current_y += 1
|
327
|
+
end
|
328
|
+
raise_watcher :clock_mark_difference_watcher if self.config.expected_clocks_count > 0 && @clock_marks.count != self.config.expected_clocks_count
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
def flood_scan(x, y)
|
333
|
+
result_mask = Hash.new { |hash, key| hash[key] = [] }
|
334
|
+
result_mask.tap do |result_mask|
|
335
|
+
process_queue = Hash.new { |hash, key| hash[key] = [] }
|
336
|
+
process_line = true
|
337
|
+
|
338
|
+
loop do
|
339
|
+
reset_process = false
|
340
|
+
|
341
|
+
if process_line
|
342
|
+
current_x = x.to_i
|
343
|
+
loop do
|
344
|
+
color = self.file.pixel_color(current_x, y)
|
345
|
+
color = RubyMarks::ImageUtils.to_hex(color.red, color.green, color.blue)
|
346
|
+
|
347
|
+
break if !self.config.recognition_colors.include?(color) || current_x - 1 <= 0
|
348
|
+
process_queue[y] << current_x unless process_queue[y].include?(current_x) || result_mask[y].include?(current_x)
|
349
|
+
result_mask[y] << current_x unless result_mask[y].include?(current_x)
|
350
|
+
current_x = current_x - 1
|
351
|
+
end
|
352
|
+
|
353
|
+
current_x = x.to_i
|
354
|
+
loop do
|
355
|
+
color = self.file.pixel_color(current_x, y)
|
356
|
+
color = RubyMarks::ImageUtils.to_hex(color.red, color.green, color.blue)
|
357
|
+
|
358
|
+
break if !self.config.recognition_colors.include?(color) || current_x + 1 >= self.file.page.width
|
359
|
+
process_queue[y] << current_x unless process_queue[y].include?(current_x) || result_mask[y].include?(current_x)
|
360
|
+
result_mask[y] << current_x unless result_mask[y].include?(current_x)
|
361
|
+
current_x = current_x + 1
|
362
|
+
end
|
363
|
+
|
364
|
+
result_mask[y] = result_mask[y].sort
|
365
|
+
process_queue[y] = process_queue[y].sort
|
366
|
+
end
|
367
|
+
|
368
|
+
process_line = true
|
369
|
+
|
370
|
+
process_queue[y].each do |element|
|
371
|
+
if y - 1 >= 0
|
372
|
+
color = self.file.pixel_color(element.to_i, y-1)
|
373
|
+
color = RubyMarks::ImageUtils.to_hex(color.red, color.green, color.blue)
|
374
|
+
if self.config.recognition_colors.include?(color) && !result_mask[y-1].include?(element)
|
375
|
+
x = element
|
376
|
+
y = y - 1
|
377
|
+
reset_process = true
|
378
|
+
break
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
next if reset_process
|
384
|
+
|
385
|
+
process_queue[y].each do |element|
|
386
|
+
if y + 1 <= self.file.page.height
|
387
|
+
color = self.file.pixel_color(element.to_i, y+1)
|
388
|
+
color = RubyMarks::ImageUtils.to_hex(color.red, color.green, color.blue)
|
389
|
+
if self.config.recognition_colors.include?(color) && !result_mask[y+1].include?(element)
|
390
|
+
x = element
|
391
|
+
y = y + 1
|
392
|
+
reset_process = true
|
393
|
+
break
|
394
|
+
else
|
395
|
+
process_queue[y].delete(element)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
next if reset_process
|
401
|
+
|
402
|
+
process_queue.each do |k,v|
|
403
|
+
process_queue.delete(k) if v.empty?
|
404
|
+
end
|
405
|
+
|
406
|
+
break if process_queue.empty?
|
407
|
+
|
408
|
+
process_line = false
|
409
|
+
y = process_queue.first[0] if process_queue.first.is_a?(Array)
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
private
|
415
|
+
def add_mark(file)
|
416
|
+
dr = Magick::Draw.new
|
417
|
+
dr.annotate(file, 0, 0, current_position[:x]-9, current_position[:y]+11, "+") do
|
418
|
+
self.pointsize = 30
|
419
|
+
self.fill = '#900000'
|
420
|
+
end
|
421
|
+
|
422
|
+
dr = Magick::Draw.new
|
423
|
+
dr.fill = '#FF0000'
|
424
|
+
dr.point(current_position[:x], current_position[:y])
|
425
|
+
dr.point(current_position[:x] + 1, current_position[:y])
|
426
|
+
dr.point(current_position[:x], current_position[:y] + 1)
|
427
|
+
dr.point(current_position[:x] + 1, current_position[:y] + 1)
|
428
|
+
dr.draw(file)
|
429
|
+
end
|
430
|
+
|
431
|
+
|
432
|
+
|
433
|
+
end
|
434
|
+
|
435
|
+
end
|
data/lib/ruby_marks/version.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
module RubyMarks
|
2
|
+
|
3
|
+
class Watcher
|
4
|
+
attr_reader :name, :recognizer
|
5
|
+
|
6
|
+
def initialize(name, recognizer, &block)
|
7
|
+
raise ArgumentError, "Invalid watcher name" unless RubyMarks::AVAILABLE_WATCHERS.include?(name)
|
8
|
+
@name = name
|
9
|
+
@recognizer = recognizer
|
10
|
+
@action = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(*args)
|
14
|
+
@action.call(@recognizer, *args) if @action
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
data/lib/ruby_marks.rb
CHANGED
@@ -14,24 +14,56 @@ if magick_version =~ /Q16/
|
|
14
14
|
end
|
15
15
|
|
16
16
|
module RubyMarks
|
17
|
+
mattr_accessor :threshold_level
|
18
|
+
@@threshold_level = 60
|
19
|
+
|
20
|
+
mattr_accessor :clock_mark_size_tolerance
|
21
|
+
@@clock_mark_size_tolerance = 2
|
22
|
+
|
23
|
+
mattr_accessor :expected_clocks_count
|
24
|
+
@@expected_clocks_count = 0
|
25
|
+
|
17
26
|
mattr_accessor :clock_marks_scan_x
|
18
27
|
@@clock_marks_scan_x = 62
|
19
28
|
|
20
|
-
mattr_accessor :
|
21
|
-
@@
|
29
|
+
mattr_accessor :clock_width
|
30
|
+
@@clock_width = 26
|
22
31
|
|
23
32
|
mattr_accessor :recognition_colors
|
24
33
|
@@recognition_colors = ["#000000"]
|
25
34
|
|
35
|
+
mattr_accessor :clock_height
|
36
|
+
@@clock_height = 12
|
37
|
+
|
38
|
+
mattr_accessor :default_mark_width
|
39
|
+
@@default_mark_width = 20
|
40
|
+
|
41
|
+
mattr_accessor :default_mark_height
|
42
|
+
@@default_mark_height = 20
|
43
|
+
|
44
|
+
mattr_accessor :intensity_percentual
|
45
|
+
@@intensity_percentual = 50
|
46
|
+
|
26
47
|
mattr_accessor :default_marks_options
|
27
48
|
@@default_marks_options = %w{A B C D E}
|
28
49
|
|
29
50
|
mattr_accessor :default_distance_between_marks
|
30
51
|
@@default_distance_between_marks = 25
|
52
|
+
|
53
|
+
COLORS = %w{ #d80000 #00d8d8 #d8006c #d86c00 #006cd8 #00d86c #d8d800 #00d86c #6c00d8 #a5a500
|
54
|
+
#a27b18 #18a236 #df4f27 }
|
55
|
+
|
56
|
+
AVAILABLE_WATCHERS = [
|
57
|
+
:scan_mark_watcher,
|
58
|
+
:scan_unmarked_watcher,
|
59
|
+
:scan_multiple_marked_watcher,
|
60
|
+
:clock_mark_difference_watcher
|
61
|
+
]
|
31
62
|
end
|
32
63
|
|
33
|
-
require 'ruby_marks/document'
|
34
|
-
require 'ruby_marks/config'
|
35
64
|
require 'ruby_marks/clock_mark'
|
65
|
+
require 'ruby_marks/config'
|
36
66
|
require 'ruby_marks/group'
|
37
|
-
require 'ruby_marks/
|
67
|
+
require 'ruby_marks/image_utils'
|
68
|
+
require 'ruby_marks/recognizer'
|
69
|
+
require 'ruby_marks/watcher'
|
@@ -4,35 +4,41 @@ class RubyMarks::ClockMarkTest < Test::Unit::TestCase
|
|
4
4
|
|
5
5
|
def setup
|
6
6
|
@file = 'assets/sheet_demo1.png'
|
7
|
-
@
|
7
|
+
@recognizer = RubyMarks::Recognizer.new
|
8
|
+
@recognizer.file = @file
|
8
9
|
@positions = {}
|
9
|
-
@positions[:first_clock_position] = {
|
10
|
-
|
11
|
-
|
12
|
-
def test_should_get_clock_coordinates_by_a_given_position
|
13
|
-
clock = RubyMarks::ClockMark.new(document: @document, position: @positions[:first_clock_position])
|
14
|
-
expected_coordinates = {:x1=>48, :x2=>75, :y1=>790, :y2=>802}
|
15
|
-
assert_equal expected_coordinates, clock.calc_coordinates
|
10
|
+
@positions[:first_clock_position] = {x1: 49, x2: 74, y1: 790, y2: 801}
|
11
|
+
@positions[:not_a_clock] = {x1: 62, x2:63, y1: 859, y2: 860}
|
16
12
|
end
|
17
13
|
|
18
14
|
def test_should_obtain_the_clock_mark_width
|
19
|
-
clock = RubyMarks::ClockMark.new(
|
20
|
-
assert_equal
|
15
|
+
clock = RubyMarks::ClockMark.new(recognizer: @recognizer, coordinates: @positions[:first_clock_position])
|
16
|
+
assert_equal 26, clock.width
|
21
17
|
end
|
22
18
|
|
23
19
|
def test_should_obtain_the_clock_mark_height
|
24
|
-
clock = RubyMarks::ClockMark.new(
|
20
|
+
clock = RubyMarks::ClockMark.new(recognizer: @recognizer, coordinates: @positions[:first_clock_position])
|
25
21
|
assert_equal 12, clock.height
|
26
22
|
end
|
27
23
|
|
28
24
|
def test_should_obtain_the_horizontal_middle_position
|
29
|
-
clock = RubyMarks::ClockMark.new(
|
30
|
-
assert_equal
|
25
|
+
clock = RubyMarks::ClockMark.new(recognizer: @recognizer, coordinates: @positions[:first_clock_position])
|
26
|
+
assert_equal 62, clock.horizontal_middle_position
|
31
27
|
end
|
32
28
|
|
33
29
|
def test_should_obtain_the_vertical_middle_position
|
34
|
-
clock = RubyMarks::ClockMark.new(
|
30
|
+
clock = RubyMarks::ClockMark.new(recognizer: @recognizer, coordinates: @positions[:first_clock_position])
|
35
31
|
assert_equal 796, clock.vertical_middle_position
|
36
32
|
end
|
37
33
|
|
34
|
+
def test_should_recognize_a_valid_clock
|
35
|
+
clock = RubyMarks::ClockMark.new(recognizer: @recognizer, coordinates: @positions[:first_clock_position])
|
36
|
+
assert clock.valid?, "Not recognized a valid clock"
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_should_recognize_a_invalid_clock
|
40
|
+
clock = RubyMarks::ClockMark.new(recognizer: @recognizer, coordinates: @positions[:not_a_clock])
|
41
|
+
assert clock.invalid?, "Recognized a invalid clock as a valid one"
|
42
|
+
end
|
43
|
+
|
38
44
|
end
|
@@ -4,8 +4,9 @@ class RubyMarks::GroupTest < Test::Unit::TestCase
|
|
4
4
|
|
5
5
|
def setup
|
6
6
|
@file = 'assets/sheet_demo1.png'
|
7
|
-
@
|
8
|
-
@
|
7
|
+
@recognizer = RubyMarks::Recognizer.new
|
8
|
+
@recognizer.file = @file
|
9
|
+
@group = RubyMarks::Group.new(:test, @recognizer)
|
9
10
|
end
|
10
11
|
|
11
12
|
def test_should_convert_fixnum_into_range_in_clocks_range
|
@@ -1,14 +1,14 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
|
-
class RubyMarks::
|
3
|
+
class RubyMarks::ImageUtilsTest < Test::Unit::TestCase
|
4
4
|
|
5
5
|
def test_should_return_the_white_color_in_hexa_receiving_8bits
|
6
|
-
color = RubyMarks::
|
6
|
+
color = RubyMarks::ImageUtils.to_hex(255, 255, 255)
|
7
7
|
assert_equal "#FFFFFF", color
|
8
8
|
end
|
9
9
|
|
10
10
|
def test_should_return_the_white_color_in_hexa_receiving_16bits
|
11
|
-
color = RubyMarks::
|
11
|
+
color = RubyMarks::ImageUtils.to_hex(65535, 65535, 65535)
|
12
12
|
assert_equal "#FFFFFF", color
|
13
13
|
end
|
14
14
|
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class RubyMarks::RecognizerTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@file = 'assets/sheet_demo1.png'
|
7
|
+
@recognizer = RubyMarks::Recognizer.new
|
8
|
+
@positions = {}
|
9
|
+
@positions[:marked_position] = {x: 161, y: 794}
|
10
|
+
@positions[:unmarked_position] = {x: 161, y: 994}
|
11
|
+
@positions[:first_clock_position] = {x: 62, y: 794}
|
12
|
+
@positions[:invalid_clock] = {x: 62, y: 1032}
|
13
|
+
|
14
|
+
@recognizer.configure do |config|
|
15
|
+
config.expected_clocks_count = 20
|
16
|
+
config.define_group :first do |group|
|
17
|
+
group.clocks_range = 1..20
|
18
|
+
group.x_distance_from_clock = 87
|
19
|
+
end
|
20
|
+
|
21
|
+
config.define_group :second do |group|
|
22
|
+
group.clocks_range = 1..20
|
23
|
+
group.x_distance_from_clock = 310
|
24
|
+
end
|
25
|
+
|
26
|
+
config.define_group :third do |group|
|
27
|
+
group.clocks_range = 1..20
|
28
|
+
group.x_distance_from_clock = 535
|
29
|
+
end
|
30
|
+
|
31
|
+
config.define_group :fourth do |group|
|
32
|
+
group.clocks_range = 1..20
|
33
|
+
group.x_distance_from_clock = 760
|
34
|
+
end
|
35
|
+
|
36
|
+
config.define_group :fifth do |group|
|
37
|
+
group.clocks_range = 1..20
|
38
|
+
group.x_distance_from_clock = 985
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
@recognizer.file = @file
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_should_initialize_a_recognizer_with_a_valid_file
|
46
|
+
assert_equal @file, @recognizer.filename
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_should_pass_the_configuration_to_recognizer_config
|
50
|
+
@recognizer.configure do |config|
|
51
|
+
config.clock_marks_scan_x = 30
|
52
|
+
end
|
53
|
+
assert_equal 30, @recognizer.config.clock_marks_scan_x
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_should_get_the_default_configuration_of_config_in_group
|
57
|
+
@recognizer.configure do |config|
|
58
|
+
config.default_marks_options = %w{1 2 3}
|
59
|
+
|
60
|
+
config.define_group :one
|
61
|
+
end
|
62
|
+
assert_equal %w{1 2 3}, @recognizer.groups[:one].marks_options
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_should_get_the_configuration_defined_in_group
|
66
|
+
@recognizer.configure do |config|
|
67
|
+
config.default_marks_options = %w{1 2 3}
|
68
|
+
config.define_group :one do |group|
|
69
|
+
group.marks_options = %w{X Y Z}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
assert_equal %w{X Y Z}, @recognizer.groups[:one].marks_options
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def test_should_return_a_file_with_a_position_flagged
|
77
|
+
@recognizer.current_position = @positions[:invalid_clock]
|
78
|
+
flagged_recognizer = @recognizer.flag_position
|
79
|
+
assert_equal Magick::Image, flagged_recognizer.class
|
80
|
+
|
81
|
+
# temp_filename = "temp_sheet_demo1.png"
|
82
|
+
# File.delete(temp_filename) if File.exist?(temp_filename)
|
83
|
+
# flagged_recognizer.write(temp_filename)
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_should_recognize_marked_position
|
87
|
+
@recognizer.current_position = @positions[:marked_position]
|
88
|
+
assert @recognizer.marked?(20, 20), "The position wasn't recognized as marked"
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_should_recognize_not_marked_position
|
92
|
+
@recognizer.current_position = @positions[:unmarked_position]
|
93
|
+
assert @recognizer.unmarked?(20, 20), "The position wasn't recognized as unmarked"
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_should_recognize_the_recognizer_clock_marks
|
97
|
+
@recognizer.scan_clock_marks
|
98
|
+
assert_equal 20, @recognizer.clock_marks.count
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_should_return_the_recognizer_with_all_marks_flagged
|
102
|
+
flagged_recognizer = @recognizer.flag_all_marks
|
103
|
+
assert_equal Magick::Image, flagged_recognizer.class
|
104
|
+
|
105
|
+
temp_filename = "temp_sheet_demo2.png"
|
106
|
+
File.delete(temp_filename) if File.exist?(temp_filename)
|
107
|
+
flagged_recognizer.write(temp_filename)
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_should_move_the_current_position_in_10_and_20_pixels
|
111
|
+
@recognizer.current_position = @positions[:marked_position]
|
112
|
+
expected_position = {x: 171, y: 814}
|
113
|
+
|
114
|
+
assert_equal expected_position, @recognizer.move_to(10, 20)
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_should_scan_the_recognizer_and_get_a_hash_of_marked_marks
|
118
|
+
expected_hash = {
|
119
|
+
clock_1: {
|
120
|
+
group_first: ['A'],
|
121
|
+
group_second: ['A']
|
122
|
+
},
|
123
|
+
clock_2: {
|
124
|
+
group_first: ['B'],
|
125
|
+
group_second: ['B'],
|
126
|
+
group_third: ['B']
|
127
|
+
},
|
128
|
+
clock_3: {
|
129
|
+
group_first: ['C'],
|
130
|
+
group_second: ['C'],
|
131
|
+
group_third: ['D']
|
132
|
+
},
|
133
|
+
clock_4: {
|
134
|
+
group_first: ['D'],
|
135
|
+
group_second: ['D'],
|
136
|
+
group_third: ['D']
|
137
|
+
},
|
138
|
+
clock_5: {
|
139
|
+
group_first: ['E'],
|
140
|
+
group_second: ['E']
|
141
|
+
}
|
142
|
+
}
|
143
|
+
assert_equal expected_hash, @recognizer.scan
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_should_make_scan_mark_watcher_raise_up
|
147
|
+
@recognizer.add_watcher :scan_mark_watcher
|
148
|
+
|
149
|
+
@recognizer.scan
|
150
|
+
assert @recognizer.raised_watchers.include?(:scan_mark_watcher)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
metadata
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_marks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
5
|
-
prerelease:
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- André Rodrigues
|
@@ -39,17 +39,19 @@ extra_rdoc_files:
|
|
39
39
|
files:
|
40
40
|
- lib/ruby_marks/clock_mark.rb
|
41
41
|
- lib/ruby_marks/config.rb
|
42
|
-
- lib/ruby_marks/document.rb
|
43
42
|
- lib/ruby_marks/group.rb
|
44
|
-
- lib/ruby_marks/
|
43
|
+
- lib/ruby_marks/image_utils.rb
|
44
|
+
- lib/ruby_marks/recognizer.rb
|
45
45
|
- lib/ruby_marks/support.rb
|
46
46
|
- lib/ruby_marks/version.rb
|
47
|
+
- lib/ruby_marks/watcher.rb
|
47
48
|
- lib/ruby_marks.rb
|
48
49
|
- README.rdoc
|
49
50
|
- test/ruby_marks/clock_mark_test.rb
|
50
|
-
- test/ruby_marks/document_test.rb
|
51
51
|
- test/ruby_marks/group_test.rb
|
52
|
-
- test/ruby_marks/
|
52
|
+
- test/ruby_marks/image_utils_test.rb
|
53
|
+
- test/ruby_marks/recognizer_test.rb
|
54
|
+
- test/ruby_marks/watcher_test.rb
|
53
55
|
- test/test_helper.rb
|
54
56
|
homepage: https://github.com/andrerpbts/ruby_marks.git
|
55
57
|
licenses:
|
@@ -67,9 +69,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
69
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
70
|
none: false
|
69
71
|
requirements:
|
70
|
-
- - ! '
|
72
|
+
- - ! '>='
|
71
73
|
- !ruby/object:Gem::Version
|
72
|
-
version:
|
74
|
+
version: '0'
|
73
75
|
requirements: []
|
74
76
|
rubyforge_project: ruby_marks
|
75
77
|
rubygems_version: 1.8.24
|
@@ -78,7 +80,8 @@ specification_version: 3
|
|
78
80
|
summary: A simple OMR tool
|
79
81
|
test_files:
|
80
82
|
- test/ruby_marks/clock_mark_test.rb
|
81
|
-
- test/ruby_marks/document_test.rb
|
82
83
|
- test/ruby_marks/group_test.rb
|
83
|
-
- test/ruby_marks/
|
84
|
+
- test/ruby_marks/image_utils_test.rb
|
85
|
+
- test/ruby_marks/recognizer_test.rb
|
86
|
+
- test/ruby_marks/watcher_test.rb
|
84
87
|
- test/test_helper.rb
|
data/lib/ruby_marks/document.rb
DELETED
@@ -1,161 +0,0 @@
|
|
1
|
-
#encoding: utf-8
|
2
|
-
module RubyMarks
|
3
|
-
|
4
|
-
# Represents a scanned document
|
5
|
-
class Document
|
6
|
-
|
7
|
-
attr_reader :file
|
8
|
-
|
9
|
-
attr_accessor :current_position, :clock_marks, :config, :groups
|
10
|
-
|
11
|
-
def initialize(file)
|
12
|
-
@file = Magick::Image.read(file).first
|
13
|
-
@current_position = {x: 0, y: 0}
|
14
|
-
@clock_marks = []
|
15
|
-
@groups = {}
|
16
|
-
self.create_config
|
17
|
-
end
|
18
|
-
|
19
|
-
def create_config
|
20
|
-
@config ||= RubyMarks::Config.new(self)
|
21
|
-
end
|
22
|
-
|
23
|
-
def filename
|
24
|
-
@file.filename
|
25
|
-
end
|
26
|
-
|
27
|
-
def configure(&block)
|
28
|
-
self.create_config
|
29
|
-
@config.configure(&block)
|
30
|
-
end
|
31
|
-
|
32
|
-
def add_group(group)
|
33
|
-
@groups[group.label] = group if group
|
34
|
-
end
|
35
|
-
|
36
|
-
def move_to(x, y)
|
37
|
-
@current_position = {x: @current_position[:x] + x, y: @current_position[:y] + y}
|
38
|
-
end
|
39
|
-
|
40
|
-
def marked?
|
41
|
-
if self.current_position
|
42
|
-
area_x = 8
|
43
|
-
area_y = 8
|
44
|
-
|
45
|
-
x_pos = current_position[:x]-area_x..current_position[:x]+area_x
|
46
|
-
y_pos = current_position[:y]-area_y..current_position[:y]+area_y
|
47
|
-
|
48
|
-
colors = []
|
49
|
-
|
50
|
-
y_pos.each do |y|
|
51
|
-
x_pos.each do |x|
|
52
|
-
color = @file.pixel_color(x, y)
|
53
|
-
color = RubyMarks::RGB.to_hex(color.red, color.green, color.blue)
|
54
|
-
color = @config.recognition_colors.include?(color) ? "." : " "
|
55
|
-
colors << color
|
56
|
-
end
|
57
|
-
end
|
58
|
-
intensity = colors.count(".") * 100 / colors.size
|
59
|
-
return intensity >= @config.intensity_percentual ? true : false
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def unmarked?
|
64
|
-
!marked?
|
65
|
-
end
|
66
|
-
|
67
|
-
def scan
|
68
|
-
result = {}
|
69
|
-
result.tap do |result|
|
70
|
-
position_before = @current_position
|
71
|
-
scan_clock_marks unless clock_marks.any?
|
72
|
-
|
73
|
-
clock_marks.each_with_index do |clock_mark, index|
|
74
|
-
group_hash = {}
|
75
|
-
@groups.each do |key, group|
|
76
|
-
if group.belongs_to_clock?(index + 1)
|
77
|
-
@current_position = {x: clock_mark.coordinates[:x2], y: clock_mark.vertical_middle_position}
|
78
|
-
move_to(group.x_distance_from_clock, 0)
|
79
|
-
markeds = []
|
80
|
-
group.marks_options.each do |mark|
|
81
|
-
markeds << mark if marked?
|
82
|
-
move_to(group.distance_between_marks, 0)
|
83
|
-
end
|
84
|
-
group_hash["group_#{key}".to_sym] = markeds if markeds.any?
|
85
|
-
end
|
86
|
-
end
|
87
|
-
result["clock_#{index+1}".to_sym] = group_hash if group_hash.any?
|
88
|
-
end
|
89
|
-
@current_position = position_before
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def flag_position
|
94
|
-
file = @file.dup
|
95
|
-
|
96
|
-
file.tap do |file|
|
97
|
-
if current_position
|
98
|
-
add_mark file
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def flag_all_marks
|
104
|
-
file = @file.dup
|
105
|
-
|
106
|
-
file.tap do |file|
|
107
|
-
position_before = @current_position
|
108
|
-
|
109
|
-
scan_clock_marks unless clock_marks.any?
|
110
|
-
|
111
|
-
clock_marks.each_with_index do |clock_mark, index|
|
112
|
-
@groups.each do |key, group|
|
113
|
-
if group.belongs_to_clock?(index + 1)
|
114
|
-
@current_position = {x: clock_mark.coordinates[:x2], y: clock_mark.vertical_middle_position}
|
115
|
-
move_to(group.x_distance_from_clock, 0)
|
116
|
-
group.marks_options.each do |mark|
|
117
|
-
add_mark file
|
118
|
-
move_to(group.distance_between_marks, 0)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
@current_position = position_before
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
def scan_clock_marks
|
129
|
-
@clock_marks = []
|
130
|
-
x = @config.clock_marks_scan_x
|
131
|
-
in_clock = false
|
132
|
-
total_height = @file && @file.page.height || 0
|
133
|
-
@clock_marks.tap do |clock_marks|
|
134
|
-
total_height.times do |y|
|
135
|
-
clock = {}
|
136
|
-
color = @file.pixel_color(x, y)
|
137
|
-
color = RubyMarks::RGB.to_hex(color.red, color.green, color.blue)
|
138
|
-
if !in_clock && color == "#000000"
|
139
|
-
in_clock = true
|
140
|
-
clock_marks << RubyMarks::ClockMark.new(document: self, position: {x: x, y: y+3})
|
141
|
-
elsif in_clock && color != "#000000"
|
142
|
-
in_clock = false
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
private
|
149
|
-
def add_mark(file)
|
150
|
-
flag = Magick::Draw.new
|
151
|
-
file.annotate(flag, 0, 0, current_position[:x] - 12, current_position[:y] + 15, "+") do
|
152
|
-
self.pointsize = 41
|
153
|
-
self.stroke = '#000000'
|
154
|
-
self.fill = '#C00000'
|
155
|
-
self.font_weight = Magick::BoldWeight
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
end
|
160
|
-
|
161
|
-
end
|
@@ -1,141 +0,0 @@
|
|
1
|
-
require "test_helper"
|
2
|
-
|
3
|
-
class RubyMarks::DocumentTest < Test::Unit::TestCase
|
4
|
-
|
5
|
-
def setup
|
6
|
-
@file = 'assets/sheet_demo1.png'
|
7
|
-
@document = RubyMarks::Document.new(@file)
|
8
|
-
@positions = {}
|
9
|
-
@positions[:marked_position] = {x: 161, y: 794}
|
10
|
-
@positions[:unmarked_position] = {x: 161, y: 994}
|
11
|
-
@positions[:first_clock_position] = {x: 62, y: 794}
|
12
|
-
|
13
|
-
@document.configure do |config|
|
14
|
-
config.define_group :first do |group|
|
15
|
-
group.clocks_range = 1..20
|
16
|
-
group.x_distance_from_clock = 87
|
17
|
-
end
|
18
|
-
|
19
|
-
config.define_group :second do |group|
|
20
|
-
group.clocks_range = 1..20
|
21
|
-
group.x_distance_from_clock = 310
|
22
|
-
end
|
23
|
-
|
24
|
-
config.define_group :third do |group|
|
25
|
-
group.clocks_range = 1..20
|
26
|
-
group.x_distance_from_clock = 535
|
27
|
-
end
|
28
|
-
|
29
|
-
config.define_group :fourth do |group|
|
30
|
-
group.clocks_range = 1..20
|
31
|
-
group.x_distance_from_clock = 760
|
32
|
-
end
|
33
|
-
|
34
|
-
config.define_group :fifth do |group|
|
35
|
-
group.clocks_range = 1..20
|
36
|
-
group.x_distance_from_clock = 985
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def test_should_initialize_a_document_with_a_valid_file
|
42
|
-
assert_equal @file, @document.filename
|
43
|
-
end
|
44
|
-
|
45
|
-
def test_should_pass_the_configuration_to_document_config
|
46
|
-
@document.configure do |config|
|
47
|
-
config.clock_marks_scan_x = 30
|
48
|
-
end
|
49
|
-
assert_equal 30, @document.config.clock_marks_scan_x
|
50
|
-
end
|
51
|
-
|
52
|
-
def test_should_get_the_default_configuration_of_config_in_group
|
53
|
-
@document.configure do |config|
|
54
|
-
config.default_marks_options = %w{1 2 3}
|
55
|
-
|
56
|
-
config.define_group :one
|
57
|
-
end
|
58
|
-
assert_equal %w{1 2 3}, @document.groups[:one].marks_options
|
59
|
-
end
|
60
|
-
|
61
|
-
def test_should_get_the_configuration_defined_in_group
|
62
|
-
@document.configure do |config|
|
63
|
-
config.default_marks_options = %w{1 2 3}
|
64
|
-
config.define_group :one do |group|
|
65
|
-
group.marks_options = %w{X Y Z}
|
66
|
-
end
|
67
|
-
end
|
68
|
-
assert_equal %w{X Y Z}, @document.groups[:one].marks_options
|
69
|
-
end
|
70
|
-
|
71
|
-
|
72
|
-
def test_should_return_a_file_with_a_position_flagged
|
73
|
-
@document.current_position = @positions[:first_clock_position]
|
74
|
-
flagged_document = @document.flag_position
|
75
|
-
assert_equal Magick::Image, flagged_document.class
|
76
|
-
|
77
|
-
# temp_filename = "temp_sheet_demo1.png"
|
78
|
-
# File.delete(temp_filename) if File.exist?(temp_filename)
|
79
|
-
# flagged_document.write(temp_filename)
|
80
|
-
end
|
81
|
-
|
82
|
-
def test_should_recognize_marked_position
|
83
|
-
@document.current_position = @positions[:marked_position]
|
84
|
-
assert @document.marked?, "The position wasn't recognized as marked"
|
85
|
-
end
|
86
|
-
|
87
|
-
def test_should_recognize_not_marked_position
|
88
|
-
@document.current_position = @positions[:unmarked_position]
|
89
|
-
assert @document.unmarked?, "The position wasn't recognized as unmarked"
|
90
|
-
end
|
91
|
-
|
92
|
-
def test_should_recognize_the_document_clock_marks
|
93
|
-
@document.scan_clock_marks
|
94
|
-
assert_equal 20, @document.clock_marks.count
|
95
|
-
end
|
96
|
-
|
97
|
-
def test_should_return_the_document_with_all_marks_flagged
|
98
|
-
flagged_document = @document.flag_all_marks
|
99
|
-
assert_equal Magick::Image, flagged_document.class
|
100
|
-
|
101
|
-
# temp_filename = "temp_sheet_demo2.png"
|
102
|
-
# File.delete(temp_filename) if File.exist?(temp_filename)
|
103
|
-
# flagged_document.write(temp_filename)
|
104
|
-
end
|
105
|
-
|
106
|
-
def test_should_move_the_current_position_in_10_and_20_pixels
|
107
|
-
@document.current_position = @positions[:marked_position]
|
108
|
-
expected_position = {x: 171, y: 814}
|
109
|
-
|
110
|
-
assert_equal expected_position, @document.move_to(10, 20)
|
111
|
-
end
|
112
|
-
|
113
|
-
def test_should_scan_the_document_and_get_a_hash_of_marked_marks
|
114
|
-
expected_hash = {
|
115
|
-
clock_1: {
|
116
|
-
group_first: ['A'],
|
117
|
-
group_second: ['A']
|
118
|
-
},
|
119
|
-
clock_2: {
|
120
|
-
group_first: ['B'],
|
121
|
-
group_second: ['B']
|
122
|
-
},
|
123
|
-
clock_3: {
|
124
|
-
group_first: ['C'],
|
125
|
-
group_second: ['C'],
|
126
|
-
group_third: ['D']
|
127
|
-
},
|
128
|
-
clock_4: {
|
129
|
-
group_first: ['D'],
|
130
|
-
group_second: ['D'],
|
131
|
-
group_third: ['D']
|
132
|
-
},
|
133
|
-
clock_5: {
|
134
|
-
group_first: ['E'],
|
135
|
-
group_second: ['E']
|
136
|
-
}
|
137
|
-
}
|
138
|
-
assert_equal expected_hash, @document.scan
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|