mork 0.6.0 → 0.7.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.
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}' }==---"