mork 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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}' }==---"
|