mork 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -3
  3. data/lib/mork/coord.rb +58 -0
  4. data/lib/mork/grid.rb +44 -35
  5. data/lib/mork/grid_const.rb +11 -6
  6. data/lib/mork/grid_omr.rb +76 -62
  7. data/lib/mork/magicko.rb +162 -0
  8. data/lib/mork/mimage.rb +102 -177
  9. data/lib/mork/npatch.rb +38 -56
  10. data/lib/mork/sheet_omr.rb +45 -43
  11. data/lib/mork/sheet_pdf.rb +16 -16
  12. data/lib/mork/version.rb +1 -1
  13. data/mork.gemspec +2 -2
  14. data/mork.sublime-project +9 -0
  15. data/spec/mork/coord_spec.rb +55 -0
  16. data/spec/mork/grid_omr_spec.rb +62 -85
  17. data/spec/mork/grid_spec.rb +7 -7
  18. data/spec/mork/magicko_spec.rb +46 -0
  19. data/spec/mork/mimage_spec.rb +30 -20
  20. data/spec/mork/npatch_spec.rb +46 -39
  21. data/spec/mork/sheet_omr_spec.rb +82 -40
  22. data/spec/mork/sheet_pdf_spec.rb +8 -8
  23. data/spec/samples/angolo.jpg +0 -0
  24. data/spec/samples/grid.yml +53 -0
  25. data/spec/samples/info.yml +12 -11
  26. data/spec/samples/layout.yml +9 -5
  27. data/spec/samples/lucrezia/border1.pdf +0 -0
  28. data/spec/samples/lucrezia/border2.pdf +0 -0
  29. data/spec/samples/lucrezia/bw1.pdf +0 -0
  30. data/spec/samples/lucrezia/bw2.pdf +0 -0
  31. data/spec/samples/lucrezia/gray1.pdf +0 -0
  32. data/spec/samples/lucrezia/gray2.pdf +0 -0
  33. data/spec/samples/out-1.jpg +0 -0
  34. data/spec/samples/rm00.jpeg +0 -0
  35. data/spec/samples/slanted.jpg +0 -0
  36. data/spec/samples/slanted.yml +54 -0
  37. data/spec/samples/syst/IMG_20150104_0004.jpg +0 -0
  38. data/spec/samples/syst/IMG_20150104_0004.txt +4955 -0
  39. data/spec/samples/syst/IMG_20150104_0009.jpg +0 -0
  40. data/spec/samples/syst/IMG_20150104_0009.txt +4955 -0
  41. data/spec/samples/syst/IMG_20150104_0011.jpg +0 -0
  42. data/spec/samples/syst/IMG_20150104_0011.txt +4955 -0
  43. data/spec/samples/syst/SCN_0001.jpg +0 -0
  44. data/spec/samples/syst/SCN_0001.txt +4955 -0
  45. data/spec/samples/syst/barr0.jpg +0 -0
  46. data/spec/samples/syst/barr0.txt +4955 -0
  47. data/spec/samples/syst/barr1.jpg +0 -0
  48. data/spec/samples/syst/barr1.txt +4955 -0
  49. data/spec/samples/syst/barr2.jpg +0 -0
  50. data/spec/samples/syst/barr2.txt +4955 -0
  51. data/spec/samples/syst/bell0.jpg +0 -0
  52. data/spec/samples/syst/bell0.txt +4955 -0
  53. data/spec/samples/syst/bell1.jpg +0 -0
  54. data/spec/samples/syst/bell1.txt +4955 -0
  55. data/spec/samples/syst/bell2.jpg +0 -0
  56. data/spec/samples/syst/bell2.txt +4955 -0
  57. data/spec/samples/syst/bila0.jpg +0 -0
  58. data/spec/samples/syst/bila0.txt +4955 -0
  59. data/spec/samples/syst/bila1.jpg +0 -0
  60. data/spec/samples/syst/bila1.txt +4955 -0
  61. data/spec/samples/syst/bila2.jpg +0 -0
  62. data/spec/samples/syst/bila2.txt +4955 -0
  63. data/spec/samples/syst/bila3.jpg +0 -0
  64. data/spec/samples/syst/bila3.txt +4955 -0
  65. data/spec/samples/syst/bila4.jpg +0 -0
  66. data/spec/samples/syst/bila4.txt +4955 -0
  67. data/spec/samples/syst/bone0.jpg +0 -0
  68. data/spec/samples/syst/bone0.txt +4955 -0
  69. data/spec/samples/syst/bone1.jpg +0 -0
  70. data/spec/samples/syst/bone1.txt +4955 -0
  71. data/spec/samples/syst/bone2.jpg +0 -0
  72. data/spec/samples/syst/bone2.txt +4955 -0
  73. data/spec/samples/syst/cost0.jpg +0 -0
  74. data/spec/samples/syst/cost0.txt +4955 -0
  75. data/spec/samples/syst/cost1.jpg +0 -0
  76. data/spec/samples/syst/cost1.txt +4955 -0
  77. data/spec/samples/syst/cost2.jpg +0 -0
  78. data/spec/samples/syst/cost2.txt +4955 -0
  79. data/spec/samples/syst/cost3.jpg +0 -0
  80. data/spec/samples/syst/cost3.txt +4955 -0
  81. data/spec/samples/syst/cost4.jpg +0 -0
  82. data/spec/samples/syst/cost4.txt +4955 -0
  83. data/spec/samples/syst/dald0.jpg +0 -0
  84. data/spec/samples/syst/dald0.txt +4955 -0
  85. data/spec/samples/syst/dald1.jpg +0 -0
  86. data/spec/samples/syst/dald1.txt +4955 -0
  87. data/spec/samples/syst/dald2.jpg +0 -0
  88. data/spec/samples/syst/dald2.txt +4955 -0
  89. data/spec/samples/syst/dald3.jpg +0 -0
  90. data/spec/samples/syst/dald3.txt +4955 -0
  91. data/spec/samples/syst/dald4.jpg +0 -0
  92. data/spec/samples/syst/dald4.txt +4955 -0
  93. data/spec/samples/syst/dign0.jpg +0 -0
  94. data/spec/samples/syst/dign0.txt +4955 -0
  95. data/spec/samples/syst/dign1.jpg +0 -0
  96. data/spec/samples/syst/dign1.txt +4955 -0
  97. data/spec/samples/syst/dign2.jpg +0 -0
  98. data/spec/samples/syst/dign2.txt +4955 -0
  99. data/spec/samples/syst/dive0.jpg +0 -0
  100. data/spec/samples/syst/dive0.txt +4955 -0
  101. data/spec/samples/syst/dive1.jpg +0 -0
  102. data/spec/samples/syst/dive1.txt +4955 -0
  103. data/spec/samples/syst/dive2.jpg +0 -0
  104. data/spec/samples/syst/dive2.txt +4955 -0
  105. data/spec/samples/syst/histo.m +42 -0
  106. data/spec/samples/syst/out0000.jpg +0 -0
  107. data/spec/samples/syst/out0000.txt +4955 -0
  108. data/spec/samples/syst/out0001.jpg +0 -0
  109. data/spec/samples/syst/out0001.txt +4955 -0
  110. data/spec/samples/syst/out0002.jpg +0 -0
  111. data/spec/samples/syst/out0002.txt +4955 -0
  112. data/spec/samples/syst/qzc013.jpg +0 -0
  113. data/spec/samples/syst/qzc013.txt +4955 -0
  114. data/spec/samples/syst/sample_gray.jpg +0 -0
  115. data/spec/samples/syst/sample_gray.txt +4955 -0
  116. data/spec/samples/syst_grid.yml +53 -0
  117. data/spec/spec_helper.rb +18 -10
  118. data/test_reg.m +39 -0
  119. metadata +105 -8
  120. data/spec/samples/io.jpg +0 -0
data/lib/mork/mimage.rb CHANGED
@@ -1,27 +1,25 @@
1
- require 'mini_magick'
2
1
  require 'mork/npatch'
2
+ require 'mork/magicko'
3
3
 
4
4
  module Mork
5
- # The class Mimage manages the image. It is also a wrapper for the core image library
6
- # currently mini_magick. TODO: consider moving out the interaction with mini_magick.
5
+ # The class Mimage processes the image.
7
6
  # Note that Mimage is NOT intended as public API, it should only be called by SheetOMR
8
7
  class Mimage
8
+ attr_reader :rm
9
+
9
10
  def initialize(path, nitems, grom)
10
- @path = path
11
- @grom = grom
11
+ @mack = Magicko.new path
12
12
  @nitems = nitems
13
- @grom.set_page_size width, height
14
- @rm = {} # registration mark centers
15
- @rmsa = {} # registration mark search area
13
+ @grom = grom.set_page_size @mack.width, @mack.height
14
+ @rm = {} # registration mark centers
16
15
  @valid = register
17
- @writing = nil
18
- @cmd = []
16
+ # @writing = nil
19
17
  end
20
-
18
+
21
19
  def valid?
22
20
  @valid
23
21
  end
24
-
22
+
25
23
  def status
26
24
  {
27
25
  tl: @rm[:tl][:status],
@@ -31,132 +29,70 @@ module Mork
31
29
  write: @writing
32
30
  }
33
31
  end
34
-
32
+
35
33
  def marked?(q,c)
36
34
  shade_of(q,c) < choice_threshold
37
35
  end
38
-
36
+
39
37
  def barcode_bit?(i)
40
38
  reg_pixels.average(@grom.barcode_bit_area i+1) < barcode_threshold
41
39
  end
42
-
43
- def width
44
- img_size[0].to_i
45
- end
46
-
47
- def height
48
- img_size[1].to_i
49
- end
50
-
51
- # outline(cells, roundedness)
52
- #
53
- # draws on the Mimage a set of cell outlines
54
- # typically used to highlight the expected responses
55
- def outline(cells, roundedness=nil)
40
+
41
+ def outline(cells)
56
42
  return if cells.empty?
57
- @cmd << [:stroke, 'green']
58
- @cmd << [:strokewidth, '2']
59
- @cmd << [:fill, 'none']
60
- coordinates_of(cells).each do |c|
61
- roundedness ||= [c[:h], c[:w]].min / 2
62
- pts = [c[:x], c[:y], c[:x]+c[:w], c[:y]+c[:h], roundedness, roundedness].join ' '
63
- @cmd << [:draw, "roundrectangle #{pts}"]
64
- end
43
+ @mack.outline coordinates_of(cells)
65
44
  end
66
-
67
- def highlight_all_choices
68
- cells = (0...@grom.max_questions).collect { |i| (0...@grom.max_choices_per_question).to_a }
69
- highlight_cells cells
70
- end
71
-
45
+
72
46
  # highlight_cells(cells, roundedness)
73
- #
47
+ #
74
48
  # partially transparent yellow on top of choice cells
75
- def highlight_cells(cells, roundedness=nil)
49
+ def highlight_cells(cells)
76
50
  return if cells.empty?
77
- @cmd << [:stroke, 'none']
78
- @cmd << [:fill, 'rgba(255, 255, 0, 0.3)']
79
- coordinates_of(cells).each do |c|
80
- roundedness ||= [c[:h], c[:w]].min / 2
81
- pts = [c[:x], c[:y], c[:x]+c[:w], c[:y]+c[:h], roundedness, roundedness].join ' '
82
- @cmd << [:draw, "roundrectangle #{pts}"]
83
- end
51
+ @mack.highlight_cells coordinates_of(cells)
84
52
  end
85
-
86
- def highlight_reg_area
87
- highlight_rect [@rmsa[:tl], @rmsa[:tr], @rmsa[:br], @rmsa[:bl]]
88
- return unless valid?
89
- join [@rm[:tl], @rm[:tr], @rm[:br], @rm[:bl]]
53
+
54
+ def highlight_all_choices
55
+ cells = (0...@grom.max_questions).collect { |i| (0...@grom.max_choices_per_question).to_a }
56
+ highlight_cells cells
90
57
  end
91
-
58
+
92
59
  def highlight_barcode(bitstring)
93
- highlight_rect @grom.barcode_bit_areas bitstring
60
+ @mack.highlight_rect @grom.barcode_bit_areas bitstring
94
61
  end
95
-
96
- def highlight_rect(areas)
97
- return if areas.empty?
98
- @cmd << [:fill, 'none']
99
- @cmd << [:stroke, 'yellow']
100
- @cmd << [:strokewidth, 3]
101
- areas.each do |c|
102
- pts = [c[:x], c[:y], c[:x]+c[:w], c[:y]+c[:h]].join ' '
103
- @cmd << [:draw, "rectangle #{pts}"]
104
- end
62
+
63
+ def highlight_rm_centers
64
+ each_corner { |c| @mack.plus @rm[c][:x], @rm[c][:y], 20 }
65
+ end
66
+
67
+ def highlight_rm_areas
68
+ each_corner { |c| @mack.highlight_area @grom.rm_crop_area(c) }
105
69
  end
106
-
70
+
107
71
  def cross(cells)
108
72
  return if cells.empty?
109
73
  cells = [cells] if cells.is_a? Hash
110
- @cmd << [:stroke, 'red']
111
- @cmd << [:strokewidth, '3']
112
- coordinates_of(cells).each do |c|
113
- pts = [
114
- c[:x]+corner,
115
- c[:y]+corner,
116
- c[:x]+c[:w]-corner,
117
- c[:y]+c[:h]-corner
118
- ].join ' '
119
- @cmd << [:draw, "line #{pts}"]
120
- pts = [
121
- c[:x]+corner,
122
- c[:y]+c[:h]-corner,
123
- c[:x]+c[:w]-corner,
124
- c[:y]+corner
125
- ].join ' '
126
- @cmd << [:draw, "line #{pts}"]
127
- end
74
+ @mack.cross coordinates_of(cells)
128
75
  end
129
-
76
+
130
77
  # write the underlying MiniMagick::Image to disk;
131
78
  # if no file name is given, image is processed in-place;
132
79
  # if the 2nd arg is false, then stretching is not applied
133
80
  def write(fname=nil, reg=true)
134
- if fname
135
- MiniMagick::Tool::Convert.new(false) do |img|
136
- img << @path
137
- exec_mm_cmd img, reg
138
- img << fname
139
- end
140
- else
141
- MiniMagick::Tool::Mogrify.new(false) do |img|
142
- img << @path
143
- exec_mm_cmd img, reg
144
- end
145
- end
81
+ pp = reg ? @rm : nil
82
+ @mack.write fname, pp
146
83
  end
147
-
84
+
148
85
  # ============================================================#
149
86
  private #
150
87
  # ============================================================#
151
- def exec_mm_cmd(c, reg)
152
- c.distort(:perspective, perspective_points) if reg
153
- @cmd.each { |cmd| c.send *cmd }
88
+ def each_corner
89
+ [:tl, :tr, :br, :bl].each { |c| yield c }
154
90
  end
155
-
91
+
156
92
  def shade_of(q,c)
157
93
  choice_cell_averages[q][c]
158
94
  end
159
-
95
+
160
96
  def choice_cell_averages
161
97
  @choice_cell_averages ||= begin
162
98
  @nitems.each_with_index.collect do |cho, q|
@@ -166,109 +102,98 @@ module Mork
166
102
  end
167
103
  end
168
104
  end
169
-
105
+
106
+ # TODO: 0.75 should be a parameter
170
107
  def choice_threshold
171
108
  @choice_threshold ||= (cal_cell_mean - darkest_cell_mean) * 0.75 + darkest_cell_mean
172
109
  end
173
-
110
+
174
111
  def barcode_threshold
175
112
  @barcode_threshold ||= (paper_white + ink_black) / 2
176
- end
113
+ end
177
114
 
178
115
  def cal_cell_mean
179
116
  @grom.calibration_cell_areas.collect { |c| reg_pixels.average c }.mean
180
117
  end
181
118
 
182
119
  def darkest_cell_mean
183
- @choice_cell_averages.flatten.min
120
+ choice_cell_averages.flatten.min
184
121
  end
185
122
 
186
123
  def ink_black
187
124
  reg_pixels.average @grom.ink_black_area
188
125
  end
189
-
126
+
190
127
  def paper_white
191
128
  reg_pixels.average @grom.paper_white_area
192
129
  end
193
-
194
- def img_size
195
- @img_size ||= IO.read("|identify -format '%w,%h' #{@path}").split ','
196
- end
197
-
198
- def raw_pixels
199
- @raw_pixels ||= begin
200
- bytes = IO.read("|convert #{@path} gray:-").unpack 'C*'
201
- NPatch.new bytes, width, height
202
- end
203
- end
204
-
130
+
205
131
  def reg_pixels
206
- @reg_pixels ||= begin
207
- bytes = IO.read("|convert #{@path} -distort Perspective '#{perspective_points}' gray:-").unpack 'C*'
208
- NPatch.new bytes, width, height
209
- end
210
- end
211
-
212
- def perspective_points
213
- [
214
- @rm[:tl][:x], @rm[:tl][:y], 0, 0,
215
- @rm[:tr][:x], @rm[:tr][:y], width, 0,
216
- @rm[:br][:x], @rm[:br][:y], width, height,
217
- @rm[:bl][:x], @rm[:bl][:y], 0, height
218
- ].join ' '
132
+ @reg_pixels ||= NPatch.new @mack.registered_bytes(@rm), @mack.width, @mack.height
219
133
  end
220
134
 
221
- def join(p)
222
- @cmd << [:fill, 'none']
223
- @cmd << [:stroke, 'green']
224
- @cmd << [:strokewidth, 3]
225
- pts = [p[0][:x], p[0][:y], p[1][:x], p[1][:y], p[2][:x], p[2][:y], p[3][:x], p[3][:y]].join ' '
226
- @cmd << [:draw, "polygon #{pts}"]
227
- end
228
-
229
135
  def coordinates_of(cells)
230
136
  cells.collect.each_with_index do |q, i|
231
137
  q.collect { |c| @grom.choice_cell_area(i, c) }
232
138
  end.flatten
233
139
  end
234
-
235
- def corner
236
- @corner_size ||= @grom.cell_corner_size
237
- end
238
140
 
141
+ # find the XY coordinates of the 4 registration marks,
142
+ # plus the stdev of the search area as quality control
239
143
  def register
240
- # find the XY coordinates of the 4 registration marks
241
- @rm[:tl] = reg_centroid_on(:tl)
242
- # puts "TL: #{@rm[:tl][:status].inspect}"
243
- @rm[:tr] = reg_centroid_on(:tr)
244
- # puts "TR: #{@rm[:tr][:status].inspect}"
245
- @rm[:br] = reg_centroid_on(:br)
246
- # puts "BR: #{@rm[:br][:status].inspect}"
247
- @rm[:bl] = reg_centroid_on(:bl)
248
- # puts "BL: #{@rm[:bl][:status].inspect}"
249
- @rm.all? { |k,v| v[:status] == :ok }
144
+ each_corner { |c| @rm[c] = rm_centroid_on c }
145
+ # puts "TL: #{@rm[:tl].inspect}"
146
+ # puts "TR: #{@rm[:tr].inspect}"
147
+ # puts "BR: #{@rm[:br].inspect}"
148
+ # puts "BL: #{@rm[:bl].inspect}"
149
+ @rm.all? { |k,v| v[:status] == :ok }
250
150
  end
251
-
151
+
252
152
  # returns the centroid of the dark region within the given area
253
153
  # in the XY coordinates of the entire image
254
- def reg_centroid_on(corner)
255
- 1000.times do |i|
256
- @rmsa[corner] = @grom.rm_search_area(corner, i)
257
- # puts "================================================================"
258
- # puts "Corner #{corner} - Iteration #{i} - Coo #{@rmsa[corner].inspect}"
259
- cx, cy = raw_pixels.dark_centroid @rmsa[corner]
260
- if cx.nil?
261
- status = :no_contrast
262
- elsif (cx < @grom.rm_edgy_x) or
263
- (cy < @grom.rm_edgy_y) or
264
- (cy > @rmsa[corner][:h] - @grom.rm_edgy_y) or
265
- (cx > @rmsa[corner][:w] - @grom.rm_edgy_x)
266
- status = :edgy
267
- else
268
- return {status: :ok, x: cx + @rmsa[corner][:x], y: cy + @rmsa[corner][:y]}
269
- end
270
- return {status: status, x: nil, y: nil} if @rmsa[corner][:w] > @grom.rm_max_search_area_side
271
- end
154
+ def rm_centroid_on(corner)
155
+ c = @grom.rm_crop_area(corner)
156
+ p = @mack.rm_patch(c, @grom.rm_blur, @grom.rm_dilate)
157
+ # puts "REG #{@grom.rm_blur} - #{@grom.rm_dilate} - C #{c.inspect}"
158
+ n = NPatch.new(p, c.w, c.h)
159
+ cx, cy, sd = n.centroid
160
+ st = (cx < 2) or (cy < 2) or (cy > c.h-2) or (cx > c.w-2)
161
+ status = st ? :edgy : :ok
162
+ return {x: cx+c.x, y: cy+c.y, sd: sd, status: status}
272
163
  end
273
164
  end
274
165
  end
166
+
167
+ # def corner
168
+ # @corner_size ||= @grom.cell_corner_size
169
+ # end
170
+
171
+ # 1000.times do |i|
172
+ # @rmsa[corner] = @grom.rm_search_area(corner, i)
173
+ # # puts "================================================================"
174
+ # # puts "Corner #{corner} - Iteration #{i} - Coo #{@rmsa[corner].inspect}"
175
+ # cx, cy = raw_pixels.dark_centroid @rmsa[corner]
176
+ # if cx.nil?
177
+ # status = :no_contrast
178
+ # elsif (cx < @grom.rm_edgy_x) or
179
+ # (cy < @grom.rm_edgy_y) or
180
+ # (cy > @rmsa[corner][:h] - @grom.rm_edgy_y) or
181
+ # (cx > @rmsa[corner][:w] - @grom.rm_edgy_x)
182
+ # status = :edgy
183
+ # else
184
+ # return {status: :ok, x: cx + @rmsa[corner][:x], y: cy + @rmsa[corner][:y]}
185
+ # end
186
+ # return {status: status, x: nil, y: nil} if @rmsa[corner][:w] > @grom.rm_max_search_area_side
187
+ # end
188
+
189
+ # TAKE OUT
190
+ # def highlight_reg_area
191
+ # @mack.highlight_rect [@rmsa[:tl], @rmsa[:tr], @rmsa[:br], @rmsa[:bl]]
192
+ # return unless valid?
193
+ # @mack.join [@rm[:tl], @rm[:tr], @rm[:br], @rm[:bl]]
194
+ # end
195
+
196
+ # def raw_pixels
197
+ # @mack.raw_patch
198
+ # end
199
+
data/lib/mork/npatch.rb CHANGED
@@ -1,58 +1,39 @@
1
1
  require 'narray'
2
2
 
3
3
  module Mork
4
- # NPatch handles low-level computations on pixels
5
- # it is basically a wrapper around NArray
4
+ # NPatch handles low-level computations on pixels by leveraging NArray
6
5
  class NPatch
6
+ # NPatch.new(source, width, height) constructs an NPatch object
7
+ # from the `source` linear array of bytes, to be reshaped as a
8
+ # `width` by `height` matrix
7
9
  def initialize(source, width, height)
8
- @patch = NArray.byte(width, height)
9
- @patch[true] = case source
10
- when Array
11
- source
12
- when String
13
- IO.read("|convert #{source} gray:-").unpack 'C*'
14
- else
15
- raise 'Invalid NPatch init param'
16
- end
10
+ @patch = NArray.float(width, height)
11
+ @patch[true] = source
17
12
  @width = width
18
13
  @height = height
19
14
  end
20
-
21
- def average(c=nil)
22
- crop(c).mean
15
+
16
+ def average(c)
17
+ @patch[c.x_rng, c.y_rng].mean
23
18
  end
24
-
25
- def stddev(c=nil)
26
- crop(c).stddev
19
+
20
+ def stddev(c)
21
+ @patch[c.x_rng, c.y_rng].stddev
27
22
  end
28
-
23
+
29
24
  def length
25
+ # is this only going to be used for testing purposes?
30
26
  @patch.length
31
27
  end
32
-
33
- def dark_centroid(c = nil)
34
- p = crop c
35
- sufficient_contrast?(p) or return
36
- xp = p.sum(1).to_a
37
- yp = p.sum(0).to_a
38
- # find the intensity trough
39
- ctr_x = xp.find_index(xp.min)
40
- ctr_y = yp.find_index(yp.min)
41
- # puts "Centroid: #{ctr_x}, #{ctr_y} - MinX #{xp.min/xp.length}, MaxX #{xp.max/xp.length}, MinY #{yp.min/yp.length}, MaxY #{yp.max/yp.length}"
42
- return ctr_x, ctr_y
28
+
29
+ def centroid
30
+ xp = @patch.sum(1).to_a
31
+ yp = @patch.sum(0).to_a
32
+ return xp.find_index(xp.min), yp.find_index(yp.min), @patch.stddev
43
33
  end
44
-
34
+
45
35
  private
46
-
47
- def crop(c)
48
- c = {x: 0, y: 0, w: @width, h: @height} if c.nil?
49
- x = c[:x]...c[:x]+c[:w]
50
- y = c[:y]...c[:y]+c[:h]
51
- p = NArray.float c[:w], c[:h]
52
- p[true,true] = @patch[x, y]
53
- p
54
- end
55
-
36
+
56
37
  def sufficient_contrast?(p)
57
38
  # puts "Contrast: #{p.stddev}"
58
39
  # tested with the few examples: spec/samples/rm0x.jpeg
@@ -61,21 +42,22 @@ module Mork
61
42
  end
62
43
  end
63
44
 
64
-
65
- # def edgy?(x, y)
66
- # tol = 5
67
- # (x < tol) or (y < tol) or (y > @height - tol) or (x > @width - tol)
68
- # end
69
- #
70
- # def patch
71
- # @the_npatch ||= blurry_narr.reshape!(@width, @height)
45
+ # def dark_centroid(c = nil)
46
+ # p = crop c
47
+ # sufficient_contrast?(p) or return
48
+ # xp = p.sum(1).to_a
49
+ # yp = p.sum(0).to_a
50
+ # # find the intensity trough
51
+ # ctr_x = xp.find_index(xp.min)
52
+ # ctr_y = yp.find_index(yp.min)
53
+ # # puts "Centroid: #{ctr_x}, #{ctr_y} - MinX #{xp.min/xp.length}, MaxX #{xp.max/xp.length}, MinY #{yp.min/yp.length}, MaxY #{yp.max/yp.length}"
54
+ # return ctr_x, ctr_y
72
55
  # end
73
- #
74
- # def narr
75
- # NArray[@mim.pixels]
76
- # end
77
- #
78
- # def blurry_narr
79
- # @blurry_narr ||= NArray[@mim.blur!(10,5).pixels]
56
+
57
+ # def crop(c)
58
+ # raise "crop HELL" if c.nil?
59
+ # p = NArray.float c.w, c.h
60
+ # p[true,true] = @patch[c.x_rng, c.y_rng]
61
+ # p
80
62
  # end
81
- #
63
+
@@ -4,39 +4,39 @@ require 'mork/mimage_list'
4
4
 
5
5
  module Mork
6
6
  class SheetOMR
7
-
7
+
8
8
  def initialize(path, nitems=nil, grom=nil)
9
- raise "File '#{path}' not found" unless File.exists? path
9
+ raise IOError, "File '#{path}' not found" unless File.exists? path
10
10
  @grom = GridOMR.new grom
11
11
  @nitems = case nitems
12
12
  when nil
13
13
  [@grom.max_choices_per_question] * @grom.max_questions
14
14
  when Fixnum
15
- [@grom.max_choices_per_question] * nitems
15
+ [@grom.max_choices_per_question] * nitems
16
16
  when Array
17
17
  nitems
18
18
  end
19
19
  @mim = Mimage.new path, @nitems, @grom
20
20
  end
21
-
21
+
22
22
  def valid?
23
23
  @mim.valid?
24
24
  end
25
-
25
+
26
26
  def status
27
27
  @mim.status
28
28
  end
29
-
29
+
30
30
  # barcode
31
- #
31
+ #
32
32
  # returns the sheet barcode as an integer
33
33
  def barcode
34
34
  return if not_registered
35
35
  barcode_string.to_i(2)
36
36
  end
37
-
37
+
38
38
  # barcode_string
39
- #
39
+ #
40
40
  # returns the sheet barcode as a string of 0s and 1s. The string is barcode_bits
41
41
  # bits long, with most significant bits to the left
42
42
  def barcode_string
@@ -44,21 +44,21 @@ module Mork
44
44
  cs = @grom.barcode_bits.times.inject("") { |c, v| c << barcode_bit_string(v) }
45
45
  cs.reverse
46
46
  end
47
-
47
+
48
48
  # marked?(question, choice)
49
- #
49
+ #
50
50
  # returns true if the specified question/choice cell has been darkened
51
51
  # false otherwise
52
52
  def marked?(q, c)
53
53
  return if not_registered
54
54
  @mim.marked? q, c
55
55
  end
56
-
56
+
57
57
  # TODO: define method ‘mark’ to retrieve the choice array for a single item
58
-
59
-
58
+
59
+
60
60
  # mark_array(range)
61
- #
61
+ #
62
62
  # returns an array of arrays of marked choices.
63
63
  # takes either a range of questions, an array of questions, or a fixnum,
64
64
  # in which case the choices for the first n questions will be returned.
@@ -73,9 +73,9 @@ module Mork
73
73
  cho
74
74
  end
75
75
  end
76
-
76
+
77
77
  # mark_char_array(range)
78
- #
78
+ #
79
79
  # returns an array of arrays of the characters corresponding to marked choices.
80
80
  # WARNING: at this time, only the latin sequence 'A, B, C...' is supported.
81
81
  # takes either a range of questions, an array of questions, or a fixnum,
@@ -91,53 +91,49 @@ module Mork
91
91
  cho
92
92
  end
93
93
  end
94
-
94
+
95
95
  def mark_logical_array(r = nil)
96
96
  return if not_registered
97
97
  question_range(r).collect do |q|
98
98
  (0...@grom.max_choices_per_question).collect {|c| marked?(q, c)}
99
99
  end
100
100
  end
101
-
101
+
102
102
  # ================
103
103
  # = HIGHLIGHTING =
104
104
  # ================
105
-
105
+
106
106
  def outline(cells)
107
107
  return if not_registered
108
108
  raise "Invalid ‘cells’ argument" unless cells.kind_of? Array
109
109
  @mim.outline cells
110
110
  end
111
-
111
+
112
112
  def cross(cells)
113
113
  return if not_registered
114
114
  raise "Invalid ‘cells’ argument" unless cells.kind_of? Array
115
115
  @mim.cross cells
116
116
  end
117
-
117
+
118
118
  def cross_marked
119
119
  return if not_registered
120
120
  @mim.cross mark_array
121
121
  end
122
-
122
+
123
123
  def highlight_marked
124
124
  return if not_registered
125
125
  @mim.highlight_cells mark_array
126
126
  end
127
-
127
+
128
128
  def highlight_all_choices
129
129
  return if not_registered
130
130
  @mim.highlight_all_choices
131
131
  end
132
-
132
+
133
133
  def highlight_barcode
134
134
  return if not_registered
135
135
  @mim.highlight_barcode barcode_string
136
136
  end
137
-
138
- def highlight_registration
139
- @mim.highlight_reg_area
140
- end
141
137
 
142
138
  # write(output_path_file_name)
143
139
  #
@@ -149,27 +145,33 @@ module Mork
149
145
  return if not_registered
150
146
  @mim.write(fname)
151
147
  end
152
-
153
- # write_raw(output_path_file_name)
154
- #
155
- # writes out a copy of the source image before registration;
156
- # the output image will also contain any previously applied overlays
157
- # if the argument is omitted, the image is created in-place,
158
- # i.e. the original source image is overwritten.
159
- def write_raw(fname=nil)
160
- @mim.write(fname, false)
148
+
149
+ # # write_raw(output_path_file_name)
150
+ # #
151
+ # # writes out a copy of the source image before registration;
152
+ # # the output image will also contain any previously applied overlays
153
+ # # if the argument is omitted, the image is created in-place,
154
+ # # i.e. the original source image is overwritten.
155
+ # def write_raw(fname=nil)
156
+ # @mim.write(fname, false)
157
+ # end
158
+
159
+ def write_registration(fname)
160
+ @mim.highlight_rm_areas
161
+ @mim.highlight_rm_centers
162
+ @mim.write fname, false
161
163
  end
162
-
164
+
163
165
  # ============================================================#
164
166
  private #
165
167
  # ============================================================#
166
-
168
+
167
169
  def barcode_bit_string(i)
168
170
  @mim.barcode_bit?(i) ? "1" : "0"
169
171
  end
170
-
172
+
171
173
  def question_range(r)
172
- # TODO: help text: although not API, people need to know this!
174
+ # TODO: help text: although not API, people need to know this!
173
175
  if r.nil?
174
176
  (0...@nitems.length)
175
177
  elsif r.is_a? Fixnum
@@ -180,7 +182,7 @@ module Mork
180
182
  raise "Invalid argument"
181
183
  end
182
184
  end
183
-
185
+
184
186
  def not_registered
185
187
  unless valid?
186
188
  puts "---=={ Unregistered image. Reason: '#{@mim.status.inspect}' }==---"