ruby_marks 0.0.5.dev → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,4 +1,4 @@
1
- = Ruby Marks
1
+ = Ruby Marks {<img src="https://secure.travis-ci.org/andrerpbts/ruby_marks.png" />}[http://travis-ci.org/andrerpbts/ruby_marks]
2
2
 
3
3
  A simple OMR ({Optical Mark Recognition}[http://en.wikipedia.org/wiki/Optical_mark_recognition]) gem.
4
4
 
@@ -3,80 +3,60 @@ module RubyMarks
3
3
 
4
4
  class ClockMark
5
5
 
6
- attr_accessor :document, :position, :coordinates
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 calc_coordinates
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
- break if color != "#000000" || coordinates[:x1] <= 0
29
- end
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
- coordinates[:x2] = x
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
- break if color != "#000000" || coordinates[:x2] >= self.document.file.page.width
38
- end
39
-
40
- coordinates[:y1] = y
41
- loop do
42
- coordinates[:y1] -= 1
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[:x2] - coordinates[:x1]
42
+ RubyMarks::ImageUtils.calc_width(@coordinates[:x1], @coordinates[:x2])
63
43
  end
64
44
 
65
45
  def height
66
- coordinates[:y2] - coordinates[:y1]
46
+ RubyMarks::ImageUtils.calc_height(@coordinates[:y1], @coordinates[:y2])
67
47
  end
68
48
 
69
49
  def horizontal_middle_position
70
- coordinates[:x1] + self.width / 2
50
+ RubyMarks::ImageUtils.calc_middle_horizontal(@coordinates[:x1], self.width)
71
51
  end
72
52
 
73
53
  def vertical_middle_position
74
- coordinates[:y1] + self.height / 2
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
@@ -3,21 +3,61 @@ module RubyMarks
3
3
 
4
4
  class Config
5
5
 
6
- attr_accessor :clock_marks_scan_x, :intensity_percentual, :recognition_colors, :default_marks_options,
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(document)
10
- @document = document
11
- @clock_marks_scan_x = RubyMarks.clock_marks_scan_x
11
+ def initialize(recognizer)
12
+ @recognizer = recognizer
13
+ @threshold_level = RubyMarks.threshold_level
14
+
12
15
  @intensity_percentual = RubyMarks.intensity_percentual
13
- @recognition_colors = RubyMarks.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, @document, &block)
20
- @document.add_group(group)
59
+ group = RubyMarks::Group.new(group_label, @recognizer, &block)
60
+ @recognizer.add_group(group)
21
61
  end
22
62
 
23
63
  def configure
@@ -2,15 +2,18 @@
2
2
  module RubyMarks
3
3
 
4
4
  class Group
5
+ attr_reader :label, :recognizer, :clocks_range
5
6
 
6
- attr_reader :label, :document, :clocks_range
7
- attr_accessor :marks_options, :x_distance_from_clock, :distance_between_marks
7
+ attr_accessor :mark_width, :mark_height, :marks_options, :x_distance_from_clock,
8
+ :distance_between_marks
8
9
 
9
- def initialize(label, document)
10
+ def initialize(label, recognizer)
10
11
  @label = label
11
- @document = document
12
- @marks_options = @document.config.default_marks_options
13
- @distance_between_marks = @document.config.default_distance_between_marks
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 RGB
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
@@ -1,3 +1,3 @@
1
1
  module RubyMarks
2
- VERSION = "0.0.5.dev".freeze
2
+ VERSION = "0.1.0".freeze
3
3
  end
@@ -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 :intensity_percentual
21
- @@intensity_percentual = 40
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/rgb'
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
- @document = RubyMarks::Document.new(@file)
7
+ @recognizer = RubyMarks::Recognizer.new
8
+ @recognizer.file = @file
8
9
  @positions = {}
9
- @positions[:first_clock_position] = {x: 62, y: 794}
10
- end
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(document: @document, position: @positions[:first_clock_position])
20
- assert_equal 27, clock.width
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(document: @document, position: @positions[:first_clock_position])
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(document: @document, position: @positions[:first_clock_position])
30
- assert_equal 61, clock.horizontal_middle_position
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(document: @document, position: @positions[:first_clock_position])
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
- @document = RubyMarks::Document.new(@file)
8
- @group = RubyMarks::Group.new(:test, @document)
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::RGBTest < Test::Unit::TestCase
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::RGB.to_hex(255, 255, 255)
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::RGB.to_hex(65535, 65535, 65535)
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
+
@@ -0,0 +1,11 @@
1
+ require "test_helper"
2
+
3
+ class RubyMarks::WatcherTest < Test::Unit::TestCase
4
+
5
+ def test_should_not_accept_invalid_watchers
6
+ assert_raise ArgumentError do
7
+ watcher = RubyMarks::Watcher.new(:some_invalid_watcher_name)
8
+ end
9
+ end
10
+
11
+ end
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.dev
5
- prerelease: 6
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/rgb.rb
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/rgb_test.rb
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: 1.3.1
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/rgb_test.rb
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
@@ -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
-