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.
- checksums.yaml +4 -4
- data/README.md +5 -3
- data/lib/mork/coord.rb +58 -0
- data/lib/mork/grid.rb +44 -35
- data/lib/mork/grid_const.rb +11 -6
- data/lib/mork/grid_omr.rb +76 -62
- data/lib/mork/magicko.rb +162 -0
- data/lib/mork/mimage.rb +102 -177
- data/lib/mork/npatch.rb +38 -56
- data/lib/mork/sheet_omr.rb +45 -43
- data/lib/mork/sheet_pdf.rb +16 -16
- data/lib/mork/version.rb +1 -1
- data/mork.gemspec +2 -2
- data/mork.sublime-project +9 -0
- data/spec/mork/coord_spec.rb +55 -0
- data/spec/mork/grid_omr_spec.rb +62 -85
- data/spec/mork/grid_spec.rb +7 -7
- data/spec/mork/magicko_spec.rb +46 -0
- data/spec/mork/mimage_spec.rb +30 -20
- data/spec/mork/npatch_spec.rb +46 -39
- data/spec/mork/sheet_omr_spec.rb +82 -40
- data/spec/mork/sheet_pdf_spec.rb +8 -8
- data/spec/samples/angolo.jpg +0 -0
- data/spec/samples/grid.yml +53 -0
- data/spec/samples/info.yml +12 -11
- data/spec/samples/layout.yml +9 -5
- data/spec/samples/lucrezia/border1.pdf +0 -0
- data/spec/samples/lucrezia/border2.pdf +0 -0
- data/spec/samples/lucrezia/bw1.pdf +0 -0
- data/spec/samples/lucrezia/bw2.pdf +0 -0
- data/spec/samples/lucrezia/gray1.pdf +0 -0
- data/spec/samples/lucrezia/gray2.pdf +0 -0
- data/spec/samples/out-1.jpg +0 -0
- data/spec/samples/rm00.jpeg +0 -0
- data/spec/samples/slanted.jpg +0 -0
- data/spec/samples/slanted.yml +54 -0
- data/spec/samples/syst/IMG_20150104_0004.jpg +0 -0
- data/spec/samples/syst/IMG_20150104_0004.txt +4955 -0
- data/spec/samples/syst/IMG_20150104_0009.jpg +0 -0
- data/spec/samples/syst/IMG_20150104_0009.txt +4955 -0
- data/spec/samples/syst/IMG_20150104_0011.jpg +0 -0
- data/spec/samples/syst/IMG_20150104_0011.txt +4955 -0
- data/spec/samples/syst/SCN_0001.jpg +0 -0
- data/spec/samples/syst/SCN_0001.txt +4955 -0
- data/spec/samples/syst/barr0.jpg +0 -0
- data/spec/samples/syst/barr0.txt +4955 -0
- data/spec/samples/syst/barr1.jpg +0 -0
- data/spec/samples/syst/barr1.txt +4955 -0
- data/spec/samples/syst/barr2.jpg +0 -0
- data/spec/samples/syst/barr2.txt +4955 -0
- data/spec/samples/syst/bell0.jpg +0 -0
- data/spec/samples/syst/bell0.txt +4955 -0
- data/spec/samples/syst/bell1.jpg +0 -0
- data/spec/samples/syst/bell1.txt +4955 -0
- data/spec/samples/syst/bell2.jpg +0 -0
- data/spec/samples/syst/bell2.txt +4955 -0
- data/spec/samples/syst/bila0.jpg +0 -0
- data/spec/samples/syst/bila0.txt +4955 -0
- data/spec/samples/syst/bila1.jpg +0 -0
- data/spec/samples/syst/bila1.txt +4955 -0
- data/spec/samples/syst/bila2.jpg +0 -0
- data/spec/samples/syst/bila2.txt +4955 -0
- data/spec/samples/syst/bila3.jpg +0 -0
- data/spec/samples/syst/bila3.txt +4955 -0
- data/spec/samples/syst/bila4.jpg +0 -0
- data/spec/samples/syst/bila4.txt +4955 -0
- data/spec/samples/syst/bone0.jpg +0 -0
- data/spec/samples/syst/bone0.txt +4955 -0
- data/spec/samples/syst/bone1.jpg +0 -0
- data/spec/samples/syst/bone1.txt +4955 -0
- data/spec/samples/syst/bone2.jpg +0 -0
- data/spec/samples/syst/bone2.txt +4955 -0
- data/spec/samples/syst/cost0.jpg +0 -0
- data/spec/samples/syst/cost0.txt +4955 -0
- data/spec/samples/syst/cost1.jpg +0 -0
- data/spec/samples/syst/cost1.txt +4955 -0
- data/spec/samples/syst/cost2.jpg +0 -0
- data/spec/samples/syst/cost2.txt +4955 -0
- data/spec/samples/syst/cost3.jpg +0 -0
- data/spec/samples/syst/cost3.txt +4955 -0
- data/spec/samples/syst/cost4.jpg +0 -0
- data/spec/samples/syst/cost4.txt +4955 -0
- data/spec/samples/syst/dald0.jpg +0 -0
- data/spec/samples/syst/dald0.txt +4955 -0
- data/spec/samples/syst/dald1.jpg +0 -0
- data/spec/samples/syst/dald1.txt +4955 -0
- data/spec/samples/syst/dald2.jpg +0 -0
- data/spec/samples/syst/dald2.txt +4955 -0
- data/spec/samples/syst/dald3.jpg +0 -0
- data/spec/samples/syst/dald3.txt +4955 -0
- data/spec/samples/syst/dald4.jpg +0 -0
- data/spec/samples/syst/dald4.txt +4955 -0
- data/spec/samples/syst/dign0.jpg +0 -0
- data/spec/samples/syst/dign0.txt +4955 -0
- data/spec/samples/syst/dign1.jpg +0 -0
- data/spec/samples/syst/dign1.txt +4955 -0
- data/spec/samples/syst/dign2.jpg +0 -0
- data/spec/samples/syst/dign2.txt +4955 -0
- data/spec/samples/syst/dive0.jpg +0 -0
- data/spec/samples/syst/dive0.txt +4955 -0
- data/spec/samples/syst/dive1.jpg +0 -0
- data/spec/samples/syst/dive1.txt +4955 -0
- data/spec/samples/syst/dive2.jpg +0 -0
- data/spec/samples/syst/dive2.txt +4955 -0
- data/spec/samples/syst/histo.m +42 -0
- data/spec/samples/syst/out0000.jpg +0 -0
- data/spec/samples/syst/out0000.txt +4955 -0
- data/spec/samples/syst/out0001.jpg +0 -0
- data/spec/samples/syst/out0001.txt +4955 -0
- data/spec/samples/syst/out0002.jpg +0 -0
- data/spec/samples/syst/out0002.txt +4955 -0
- data/spec/samples/syst/qzc013.jpg +0 -0
- data/spec/samples/syst/qzc013.txt +4955 -0
- data/spec/samples/syst/sample_gray.jpg +0 -0
- data/spec/samples/syst/sample_gray.txt +4955 -0
- data/spec/samples/syst_grid.yml +53 -0
- data/spec/spec_helper.rb +18 -10
- data/test_reg.m +39 -0
- metadata +105 -8
- 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
|
|
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
|
-
@
|
|
11
|
-
@grom = grom
|
|
11
|
+
@mack = Magicko.new path
|
|
12
12
|
@nitems = nitems
|
|
13
|
-
@grom.set_page_size width, height
|
|
14
|
-
@rm
|
|
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
|
|
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
|
-
@
|
|
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
|
|
49
|
+
def highlight_cells(cells)
|
|
76
50
|
return if cells.empty?
|
|
77
|
-
@
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
135
|
-
|
|
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
|
|
152
|
-
|
|
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
|
-
|
|
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 ||=
|
|
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
|
-
|
|
241
|
-
@rm[:tl]
|
|
242
|
-
# puts "
|
|
243
|
-
@rm[:
|
|
244
|
-
# puts "
|
|
245
|
-
@rm[:
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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.
|
|
9
|
-
@patch[true] =
|
|
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
|
|
22
|
-
|
|
15
|
+
|
|
16
|
+
def average(c)
|
|
17
|
+
@patch[c.x_rng, c.y_rng].mean
|
|
23
18
|
end
|
|
24
|
-
|
|
25
|
-
def stddev(c
|
|
26
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
xp
|
|
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
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
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
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
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
|
+
|
data/lib/mork/sheet_omr.rb
CHANGED
|
@@ -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
|
-
|
|
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}' }==---"
|