mork 0.8.1 → 0.9.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 (150) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/README.md +107 -35
  4. data/lib/mork/coord.rb +1 -1
  5. data/lib/mork/extensions.rb +3 -0
  6. data/lib/mork/grid.rb +13 -6
  7. data/lib/mork/grid_const.rb +11 -9
  8. data/lib/mork/grid_omr.rb +15 -5
  9. data/lib/mork/grid_pdf.rb +1 -0
  10. data/lib/mork/magicko.rb +46 -28
  11. data/lib/mork/mimage.rb +113 -52
  12. data/lib/mork/mimage_list.rb +6 -5
  13. data/lib/mork/npatch.rb +16 -18
  14. data/lib/mork/sheet_omr.rb +210 -125
  15. data/lib/mork/sheet_pdf.rb +15 -8
  16. data/lib/mork/version.rb +1 -1
  17. data/mork.gemspec +2 -0
  18. data/mork.sublime-project +1 -1
  19. data/spec/mork/extensions_spec.rb +3 -3
  20. data/spec/mork/grid_omr_spec.rb +2 -1
  21. data/spec/mork/grid_spec.rb +34 -35
  22. data/spec/mork/magicko_spec.rb +2 -2
  23. data/spec/mork/mimage_spec.rb +22 -63
  24. data/spec/mork/sheet_omr_spec.rb +96 -201
  25. data/spec/mork/sheet_pdf_spec.rb +9 -10
  26. data/spec/out/{.gitignore → barcode/.gitignore} +0 -0
  27. data/spec/out/highlight/.gitignore +4 -0
  28. data/spec/out/mark/.gitignore +4 -0
  29. data/spec/out/outline/.gitignore +4 -0
  30. data/spec/out/pdf/.gitignore +4 -0
  31. data/spec/out/registration/.gitignore +4 -0
  32. data/spec/out/text/.gitignore +4 -0
  33. data/spec/samples/angolo.jpg +0 -0
  34. data/spec/samples/angolo2.jpg +0 -0
  35. data/spec/samples/angolo3.jpg +0 -0
  36. data/spec/samples/boxy.yml +0 -0
  37. data/spec/samples/grid.yml +0 -0
  38. data/spec/samples/grid160.yml +0 -0
  39. data/spec/samples/grid_omr_layout.yml +56 -0
  40. data/spec/samples/info.yml +15 -39
  41. data/spec/samples/jdoe/JohnDoe1.jpeg +0 -0
  42. data/spec/samples/jdoe/JohnDoe2.jpeg +0 -0
  43. data/spec/samples/jdoe/JohnDoe3.jpeg +0 -0
  44. data/spec/samples/jdoe/layout.yml +58 -0
  45. data/spec/samples/layout.yml +2 -2
  46. data/spec/samples/lucrezia/border1.pdf +0 -0
  47. data/spec/samples/lucrezia/border2.pdf +0 -0
  48. data/spec/samples/lucrezia/bw1.pdf +0 -0
  49. data/spec/samples/lucrezia/bw2.pdf +0 -0
  50. data/spec/samples/lucrezia/gray1.pdf +0 -0
  51. data/spec/samples/lucrezia/gray2.pdf +0 -0
  52. data/spec/samples/{syst/IMG_20150104_0004.jpg → marisol/marisol1.jpg} +0 -0
  53. data/spec/samples/{syst/IMG_20150104_0009.jpg → marisol/marisol2.jpg} +0 -0
  54. data/spec/samples/{syst/IMG_20150104_0011.jpg → marisol/marisol3.jpg} +0 -0
  55. data/spec/samples/reg_mark.jpg +0 -0
  56. data/spec/samples/rm00.jpeg +0 -0
  57. data/spec/samples/rm01.jpeg +0 -0
  58. data/spec/samples/rm02.jpeg +0 -0
  59. data/spec/samples/rm03.jpeg +0 -0
  60. data/spec/samples/rm04.jpeg +0 -0
  61. data/spec/samples/rm05.jpeg +0 -0
  62. data/spec/samples/syst/barr0.jpg +0 -0
  63. data/spec/samples/syst/barr1.jpg +0 -0
  64. data/spec/samples/syst/barr2.jpg +0 -0
  65. data/spec/samples/syst/bell0.jpg +0 -0
  66. data/spec/samples/syst/bell1.jpg +0 -0
  67. data/spec/samples/syst/bell2.jpg +0 -0
  68. data/spec/samples/syst/bila0.jpg +0 -0
  69. data/spec/samples/syst/bila1.jpg +0 -0
  70. data/spec/samples/syst/bila2.jpg +0 -0
  71. data/spec/samples/syst/bila3.jpg +0 -0
  72. data/spec/samples/syst/bila4.jpg +0 -0
  73. data/spec/samples/syst/bone0.jpg +0 -0
  74. data/spec/samples/syst/bone1.jpg +0 -0
  75. data/spec/samples/syst/bone2.jpg +0 -0
  76. data/spec/samples/syst/cost0.jpg +0 -0
  77. data/spec/samples/syst/cost1.jpg +0 -0
  78. data/spec/samples/syst/cost2.jpg +0 -0
  79. data/spec/samples/syst/cost3.jpg +0 -0
  80. data/spec/samples/syst/cost4.jpg +0 -0
  81. data/spec/samples/syst/dald0.jpg +0 -0
  82. data/spec/samples/syst/dald1.jpg +0 -0
  83. data/spec/samples/syst/dald2.jpg +0 -0
  84. data/spec/samples/syst/dald3.jpg +0 -0
  85. data/spec/samples/syst/dald4.jpg +0 -0
  86. data/spec/samples/syst/dign0.jpg +0 -0
  87. data/spec/samples/syst/dign1.jpg +0 -0
  88. data/spec/samples/syst/dign2.jpg +0 -0
  89. data/spec/samples/syst/dive0.jpg +0 -0
  90. data/spec/samples/syst/dive1.jpg +0 -0
  91. data/spec/samples/syst/dive2.jpg +0 -0
  92. data/spec/samples/syst/histo.m +0 -0
  93. data/spec/samples/{syst_grid.yml → syst/layout.yml} +0 -0
  94. data/spec/spec_helper.rb +29 -29
  95. metadata +49 -62
  96. data/auto.txt +0 -0
  97. data/spec/samples/out-1.jpg +0 -0
  98. data/spec/samples/qzc013.jpg +0 -0
  99. data/spec/samples/sample02.jpg +0 -0
  100. data/spec/samples/sample_gray.jpg +0 -0
  101. data/spec/samples/sheet.jpg +0 -0
  102. data/spec/samples/sheet666.jpg +0 -0
  103. data/spec/samples/slanted.jpg +0 -0
  104. data/spec/samples/slanted.yml +0 -54
  105. data/spec/samples/syst/IMG_20150104_0004.txt +0 -4955
  106. data/spec/samples/syst/IMG_20150104_0009.txt +0 -4955
  107. data/spec/samples/syst/IMG_20150104_0011.txt +0 -4955
  108. data/spec/samples/syst/SCN_0001.jpg +0 -0
  109. data/spec/samples/syst/SCN_0001.txt +0 -4955
  110. data/spec/samples/syst/barr0.txt +0 -4955
  111. data/spec/samples/syst/barr1.txt +0 -4955
  112. data/spec/samples/syst/barr2.txt +0 -4955
  113. data/spec/samples/syst/bell0.txt +0 -4955
  114. data/spec/samples/syst/bell1.txt +0 -4955
  115. data/spec/samples/syst/bell2.txt +0 -4955
  116. data/spec/samples/syst/bila0.txt +0 -4955
  117. data/spec/samples/syst/bila1.txt +0 -4955
  118. data/spec/samples/syst/bila2.txt +0 -4955
  119. data/spec/samples/syst/bila3.txt +0 -4955
  120. data/spec/samples/syst/bila4.txt +0 -4955
  121. data/spec/samples/syst/bone0.txt +0 -4955
  122. data/spec/samples/syst/bone1.txt +0 -4955
  123. data/spec/samples/syst/bone2.txt +0 -4955
  124. data/spec/samples/syst/cost0.txt +0 -4955
  125. data/spec/samples/syst/cost1.txt +0 -4955
  126. data/spec/samples/syst/cost2.txt +0 -4955
  127. data/spec/samples/syst/cost3.txt +0 -4955
  128. data/spec/samples/syst/cost4.txt +0 -4955
  129. data/spec/samples/syst/dald0.txt +0 -4955
  130. data/spec/samples/syst/dald1.txt +0 -4955
  131. data/spec/samples/syst/dald2.txt +0 -4955
  132. data/spec/samples/syst/dald3.txt +0 -4955
  133. data/spec/samples/syst/dald4.txt +0 -4955
  134. data/spec/samples/syst/dign0.txt +0 -4955
  135. data/spec/samples/syst/dign1.txt +0 -4955
  136. data/spec/samples/syst/dign2.txt +0 -4955
  137. data/spec/samples/syst/dive0.txt +0 -4955
  138. data/spec/samples/syst/dive1.txt +0 -4955
  139. data/spec/samples/syst/dive2.txt +0 -4955
  140. data/spec/samples/syst/out0000.jpg +0 -0
  141. data/spec/samples/syst/out0000.txt +0 -4955
  142. data/spec/samples/syst/out0001.jpg +0 -0
  143. data/spec/samples/syst/out0001.txt +0 -4955
  144. data/spec/samples/syst/out0002.jpg +0 -0
  145. data/spec/samples/syst/out0002.txt +0 -4955
  146. data/spec/samples/syst/qzc013.jpg +0 -0
  147. data/spec/samples/syst/qzc013.txt +0 -4955
  148. data/spec/samples/syst/sample_gray.jpg +0 -0
  149. data/spec/samples/syst/sample_gray.txt +0 -4955
  150. data/spec/samples/two_pages.pdf +0 -0
data/lib/mork/mimage.rb CHANGED
@@ -2,8 +2,7 @@ require 'mork/npatch'
2
2
  require 'mork/magicko'
3
3
 
4
4
  module Mork
5
- # The class Mimage processes the image.
6
- # Note that Mimage is NOT intended as public API, it should only be called by SheetOMR
5
+ # @private
7
6
  class Mimage
8
7
  attr_reader :rm
9
8
 
@@ -13,7 +12,6 @@ module Mork
13
12
  @grom = grom.set_page_size @mack.width, @mack.height
14
13
  @rm = {} # registration mark centers
15
14
  @valid = register
16
- # @writing = nil
17
15
  end
18
16
 
19
17
  def valid?
@@ -25,66 +23,91 @@ module Mork
25
23
  tl: @rm[:tl][:status],
26
24
  tr: @rm[:tr][:status],
27
25
  br: @rm[:br][:status],
28
- bl: @rm[:bl][:status],
29
- write: @writing
26
+ bl: @rm[:bl][:status]
30
27
  }
31
28
  end
32
29
 
33
- def marked?(q,c)
34
- shade_of(q,c) < choice_threshold
30
+ def marked
31
+ @logical_array_of_marked_cells ||= begin # memoization necessary?
32
+ itemator { |q, c| shade_of(q, c) < choice_threshold }
33
+ end
35
34
  end
36
35
 
37
- def barcode_bit?(i)
38
- reg_pixels.average(@grom.barcode_bit_area i+1) < barcode_threshold
36
+ def marked_int
37
+ marked.map do |q|
38
+ [].tap do |choices|
39
+ q.each_with_index do |choice, idx|
40
+ choices << idx if choice
41
+ end
42
+ end
43
+ end
39
44
  end
40
45
 
41
- def outline(cells)
42
- return if cells.empty?
43
- @mack.outline coordinates_of(cells)
46
+ def barcode_bits
47
+ @barcode_bits ||= begin
48
+ @grom.barcode_bits.times.collect do |b|
49
+ reg_pixels.average(@grom.barcode_bit_area b+1) < barcode_threshold
50
+ end
51
+ end
44
52
  end
45
53
 
46
- # highlight_cells(cells, roundedness)
47
- #
48
- # partially transparent yellow on top of choice cells
49
- def highlight_cells(cells)
50
- return if cells.empty?
51
- @mack.highlight_cells coordinates_of(cells)
54
+ def overlay(what, where)
55
+ areas = case where
56
+ when :barcode
57
+ @grom.barcode_areas barcode_bits
58
+ when :cal
59
+ @grom.calibration_cell_areas
60
+ when :marked
61
+ choice_cell_areas marked_int
62
+ when :all
63
+ all_choice_cell_areas
64
+ when :max
65
+ @grom.max_questions.times.map { |i| (0...@grom.max_choices_per_question).to_a }
66
+ when Array
67
+ choice_cell_areas where
68
+ else
69
+ raise ArgumentError, 'Invalid overlay argument “where”'
70
+ end
71
+ round = where != :barcode
72
+ @mack.send what, areas, round
52
73
  end
53
74
 
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
75
+ # write the underlying MiniMagick::Image to disk;
76
+ # if no file name is given, image is processed in-place;
77
+ # if the 2nd arg is false, then stretching is not applied
78
+ def save(fname=nil, reg=true)
79
+ pp = reg ? @rm : nil
80
+ @mack.save fname, pp
57
81
  end
58
82
 
59
- def highlight_barcode(bitstring)
60
- @mack.highlight_rect @grom.barcode_bit_areas bitstring
83
+ def save_registration(fname)
84
+ each_corner { |c| @mack.plus @rm[c][:x], @rm[c][:y], 30 }
85
+ each_corner { |c| @mack.outline [@grom.rm_crop_area(c)], false }
86
+ @mack.save fname, nil
61
87
  end
62
88
 
63
- def highlight_rm_centers
64
- each_corner { |c| @mack.plus @rm[c][:x], @rm[c][:y], 20 }
65
- end
89
+ # ============================================================#
90
+ private #
91
+ # ============================================================#
66
92
 
67
- def highlight_rm_areas
68
- each_corner { |c| @mack.highlight_area @grom.rm_crop_area(c) }
93
+ def itemator(items=@nitems)
94
+ items.map.with_index do |cho, q|
95
+ if cho.is_a? Fixnum
96
+ cho.times.map { |c| yield q, c }
97
+ else
98
+ cho.map { |c| yield q, c }
99
+ end
100
+ end
69
101
  end
70
102
 
71
- def cross(cells)
72
- return if cells.empty?
73
- cells = [cells] if cells.is_a? Hash
74
- @mack.cross coordinates_of(cells)
103
+ def choice_cell_areas(cells)
104
+ itemator(cells) { |q,c| @grom.choice_cell_area q, c }.flatten
75
105
  end
76
106
 
77
- # write the underlying MiniMagick::Image to disk;
78
- # if no file name is given, image is processed in-place;
79
- # if the 2nd arg is false, then stretching is not applied
80
- def write(fname=nil, reg=true)
81
- pp = reg ? @rm : nil
82
- @mack.write fname, pp
107
+ def all_choice_cell_areas
108
+ @all_choice_cell_areas ||= choice_cell_areas(@nitems)
83
109
  end
84
110
 
85
- # ============================================================#
86
- private #
87
- # ============================================================#
88
111
  def each_corner
89
112
  [:tl, :tr, :br, :bl].each { |c| yield c }
90
113
  end
@@ -95,17 +118,14 @@ module Mork
95
118
 
96
119
  def choice_cell_averages
97
120
  @choice_cell_averages ||= begin
98
- @nitems.each_with_index.collect do |cho, q|
99
- cho.times.collect do |c|
100
- reg_pixels.average @grom.choice_cell_area(q, c)
101
- end
102
- end
121
+ itemator { |q, c| reg_pixels.average @grom.choice_cell_area(q, c) }
103
122
  end
104
123
  end
105
124
 
106
125
  # TODO: 0.75 should be a parameter
107
126
  def choice_threshold
108
- @choice_threshold ||= (cal_cell_mean - darkest_cell_mean) * 0.75 + darkest_cell_mean
127
+ # puts "CT #{@grom.choice_threshold.inspect}"
128
+ @choice_threshold ||= (cal_cell_mean - darkest_cell_mean) * @grom.choice_threshold + darkest_cell_mean
109
129
  end
110
130
 
111
131
  def barcode_threshold
@@ -142,10 +162,6 @@ module Mork
142
162
  # plus the stdev of the search area as quality control
143
163
  def register
144
164
  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
165
  @rm.all? { |k,v| v[:status] == :ok }
150
166
  end
151
167
 
@@ -154,7 +170,6 @@ module Mork
154
170
  def rm_centroid_on(corner)
155
171
  c = @grom.rm_crop_area(corner)
156
172
  p = @mack.rm_patch(c, @grom.rm_blur, @grom.rm_dilate)
157
- # puts "REG #{@grom.rm_blur} - #{@grom.rm_dilate} - C #{c.inspect}"
158
173
  n = NPatch.new(p, c.w, c.h)
159
174
  cx, cy, sd = n.centroid
160
175
  st = (cx < 2) or (cy < 2) or (cy > c.h-2) or (cx > c.w-2)
@@ -197,3 +212,49 @@ end
197
212
  # @mack.raw_patch
198
213
  # end
199
214
 
215
+ # def outline(cells)
216
+ # return if cells.empty?
217
+ # @mack.outline coordinates_of(cells)
218
+ # end
219
+
220
+ # # highlight_cells(cells, roundedness)
221
+ # #
222
+ # # partially transparent yellow on top of choice cells
223
+ # def highlight_cells(cells)
224
+ # return if cells.empty?
225
+ # @mack.highlight_cells coordinates_of(cells)
226
+ # end
227
+
228
+ # def highlight_all_choices
229
+ # cells = (0...@grom.max_questions).collect { |i| (0...@grom.max_choices_per_question).to_a }
230
+ # highlight_cells cells
231
+ # end
232
+
233
+ # def highlight_barcode(bitstring)
234
+ # @mack.highlight_rect @grom.barcode_bit_areas bitstring
235
+ # end
236
+
237
+ # def highlight_rm_centers
238
+ # each_corner { |c| @mack.plus @rm[c][:x], @rm[c][:y], 20 }
239
+ # end
240
+
241
+ # def highlight_rm_areas
242
+ # each_corner { |c| @mack.highlight_area @grom.rm_crop_area(c) }
243
+ # end
244
+
245
+ # def cross(cells)
246
+ # return if cells.empty?
247
+ # cells = [cells] if cells.is_a? Hash
248
+ # @mack.cross coordinates_of(cells)
249
+ # end
250
+
251
+ # def barcode_bit?(i)
252
+ # reg_pixels.average(@grom.barcode_bit_area i+1) < barcode_threshold
253
+ # end
254
+
255
+ # puts "TL: #{@rm[:tl].inspect}"
256
+ # puts "TR: #{@rm[:tr].inspect}"
257
+ # puts "BR: #{@rm[:br].inspect}"
258
+ # puts "BL: #{@rm[:bl].inspect}"
259
+
260
+ # puts "REG #{@grom.rm_blur} - #{@grom.rm_dilate} - C #{c.inspect}"
@@ -1,7 +1,8 @@
1
1
  # require 'RMagick'
2
2
 
3
3
  module Mork
4
- # The class MimageList
4
+ # @private
5
+ # The class MimageList, currently abandoned
5
6
  class MimageList
6
7
  def initialize(fname)
7
8
  raise "Initializing a MimageList requires a string" unless fname.class == String
@@ -11,21 +12,21 @@ module Mork
11
12
  @images = Magick::ImageList.new(fname)
12
13
  end
13
14
  end
14
-
15
+
15
16
  def shift
16
17
  Mimage.new @images.shift
17
18
  end
18
-
19
+
19
20
  def [] (i)
20
21
  # puts "I: #{i}"
21
22
  # puts @images[i].inspect
22
23
  Mimage.new @images[i]
23
24
  end
24
-
25
+
25
26
  def each
26
27
  @images.each do |i|
27
28
  yield Mimage.new i
28
29
  end
29
30
  end
30
31
  end
31
- end
32
+ end
data/lib/mork/npatch.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'narray'
2
2
 
3
3
  module Mork
4
+ # @private
4
5
  # NPatch handles low-level computations on pixels by leveraging NArray
5
6
  class NPatch
6
7
  # NPatch.new(source, width, height) constructs an NPatch object
@@ -9,36 +10,28 @@ module Mork
9
10
  def initialize(source, width, height)
10
11
  @patch = NArray.float(width, height)
11
12
  @patch[true] = source
12
- @width = width
13
- @height = height
13
+ # @width = width
14
+ # @height = height
14
15
  end
15
16
 
16
- def average(c)
17
- @patch[c.x_rng, c.y_rng].mean
17
+ def average(coord)
18
+ @patch[coord.x_rng, coord.y_rng].mean
18
19
  end
19
20
 
20
- def stddev(c)
21
- @patch[c.x_rng, c.y_rng].stddev
21
+ def stddev(coord)
22
+ @patch[coord.x_rng, coord.y_rng].stddev
22
23
  end
23
24
 
24
- def length
25
- # is this only going to be used for testing purposes?
26
- @patch.length
27
- end
25
+ # def length
26
+ # # is this only going to be used for testing purposes?
27
+ # @patch.length
28
+ # end
28
29
 
29
30
  def centroid
30
31
  xp = @patch.sum(1).to_a
31
32
  yp = @patch.sum(0).to_a
32
33
  return xp.find_index(xp.min), yp.find_index(yp.min), @patch.stddev
33
34
  end
34
-
35
- private
36
-
37
- def sufficient_contrast?(p)
38
- # puts "Contrast: #{p.stddev}"
39
- # tested with the few examples: spec/samples/rm0x.jpeg
40
- p.stddev > 20
41
- end
42
35
  end
43
36
  end
44
37
 
@@ -61,3 +54,8 @@ end
61
54
  # p
62
55
  # end
63
56
 
57
+ # def sufficient_contrast?(p)
58
+ # # puts "Contrast: #{p.stddev}"
59
+ # # tested with the few examples: spec/samples/rm0x.jpeg
60
+ # p.stddev > 20
61
+ # end