mork 0.9.3 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/README.md +60 -19
- data/lib/mork/grid.rb +1 -1
- data/lib/mork/grid_const.rb +57 -83
- data/lib/mork/grid_pdf.rb +4 -0
- data/lib/mork/mimage.rb +35 -21
- data/lib/mork/npatch.rb +5 -7
- data/lib/mork/sheet_omr.rb +79 -33
- data/lib/mork/sheet_pdf.rb +5 -2
- data/lib/mork/version.rb +1 -1
- data/spec/mork/mimage_spec.rb +1 -1
- data/spec/mork/sheet_omr_spec.rb +23 -15
- data/spec/mork/sheet_pdf_spec.rb +45 -37
- data/spec/samples/boxy.yml +1 -31
- data/spec/samples/info.yml +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4aab0200b0c68dc3f3720fef8b8ae239d1c3d521
|
4
|
+
data.tar.gz: 7b1dcdfea624a8d99f98230ee3899c51ce5f90ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4002e21d4604887c06c75dedfd6689316364766276f3dff453cd7dd774ae752a9293098d18a72a236ce607eb81ac8b53d65d985b83b67b157f844e2f1706de5a
|
7
|
+
data.tar.gz: d9720227584eef5a59d842cd4b713b6c021b7eac3d6398124c6affbf65f21e2d0d44751ba436e0aee5d305721fb49d27441e5b2aa48a5d1539b5d12c780dc108
|
data/.yardopts
CHANGED
@@ -1 +1 @@
|
|
1
|
-
--no-private
|
1
|
+
--no-private --markup markdown
|
data/README.md
CHANGED
@@ -5,6 +5,10 @@ A ruby [optical mark recognition](http://en.wikipedia.org/wiki/Optical_mark_reco
|
|
5
5
|
1. generating [response sheets](/spec/samples/sheet.jpg) in PDF format
|
6
6
|
2. capturing the responses provided on the [printed sheet](/spec/samples/sample_gray.jpg) by a human with a pen or a pencil.
|
7
7
|
|
8
|
+
## First, a word of caution
|
9
|
+
|
10
|
+
__Please note that this library is under active development. Until v.1.0 is reached, the API should be regarded as unstable and bound to change without notice.__
|
11
|
+
|
8
12
|
## Assumptions and limitations
|
9
13
|
|
10
14
|
Mork is a low-level library, and very much work in progress. It is not, and will likely never be a complete OMR solution. While suggestions and contributions are more than welcome, for the time being several assumptions and restrictions apply.
|
@@ -84,13 +88,13 @@ content = {
|
|
84
88
|
|
85
89
|
These are the key-value pairs that the `content` hash may contain:
|
86
90
|
|
87
|
-
- **choices**: an array of integers, specifiying the number of items in the test (the length of the array) and the number of choices available for each item (the array values); if omitted, the maximum number of items and choices per item are printed
|
91
|
+
- **choices**: an array of integers, specifiying the number of items in the test (the length of the array) and the number of choices available for each item (the array values); if omitted, the maximum number of items and choices per item allowed by the layout (see below) are printed
|
88
92
|
- **header**: optional; a (sub)hash of key-value pairs defining the content of named header elements; in each pair, the key is the name of one header element, while the value is the rendered content; the actually available elements are defined in the `layout` (see below)
|
89
93
|
- **barcode**: optional; an integer number, the sheet's unique identifier to be printed as a binary barcode along the bottom edge of the sheet
|
90
94
|
|
91
95
|
### layout
|
92
96
|
|
93
|
-
The layout
|
97
|
+
The layout is also defined as a hash, but because of its length and since the layout often stays identical across many response sheets, it is usually more convenient to write the information in a YAML file and pass its path/filename to the `SheetPDF` constructor instead. Here is the YAML version of a standard layout hash. Please note that the parameters with an (*) in the comment have no effect on PDF production, but are relevant to OMR scan (see further below).
|
94
98
|
|
95
99
|
```yaml
|
96
100
|
page_size: # all measurements in mm
|
@@ -161,31 +165,69 @@ s.save 'sheet.pdf'
|
|
161
165
|
system 'open sheet.pdf' # this works in OSX
|
162
166
|
```
|
163
167
|
|
164
|
-
It's easy to see that by iterating over a series of `content` hashes you can produce any number of sheets, all based on the same layout but each containing unique information (notably the barcode, but also names, dates, etc.)
|
165
|
-
|
166
168
|
If the `layout` argument is omitted, Mork will search for a file named `layout.yml` and load it. If such file cannot be found, Mork will fall back to a default, boilerplate layout (incidentally, this is the layout shown above).
|
167
169
|
|
168
|
-
|
170
|
+
Importantly, if `content` is an array of hashes, Mork will produce one sheet for each hash, all based on the same layout but each containing unique information (the barcode, names, dates, a specific number of questions/choices, etc.)
|
171
|
+
|
172
|
+
## Analyzing response sheets with `SheetOMR`
|
169
173
|
|
170
|
-
|
174
|
+
### Preparing a `SheetOMR` object
|
175
|
+
|
176
|
+
Assuming that a person has filled out a response sheet by darkening with a pen the selected choices, and that the sheet has been acquired as an image file, response scoring is performed by the `Mork::SheetOMR` class. Two pieces of information must be provided to the object constructor:
|
171
177
|
|
172
178
|
- **path**: mandatory path/filename of the bitmap image (accepts JPG, JPEG, PNG, PDF extensions; a resolution of 150-200 dpi is usually more than sufficient to obtain accurate readings)
|
173
|
-
- **choices**: a named argument (ruby-2 style) equivalent to the `choices` array of integers passed to the `SheetPDF` constructor as part of the `content` parameter (see above). If omitted, the `choices` parameter is inferred from the layout
|
174
179
|
- **layout_file**: same as for the `SheetPDF` class
|
175
180
|
|
176
|
-
The following code shows how to create
|
181
|
+
The following code shows how to create a SheetOMR based on a bitmap file named `image.jpg`:
|
177
182
|
|
178
183
|
```ruby
|
179
184
|
# instantiating the object
|
180
|
-
s = SheetOMR.new 'image.jpg',
|
181
|
-
|
182
|
-
|
185
|
+
s = SheetOMR.new 'image.jpg', 'layout.yml'
|
186
|
+
```
|
187
|
+
|
188
|
+
When the object is initialized, Mork attempts to register the image, a necessary step before marking can be performed. To find out if registration succeeded:
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
s.valid?
|
192
|
+
```
|
193
|
+
|
194
|
+
Next, we need to indicate the questions/choices of interest for subsequent operations. For example, the following instructs Mork to evaluate 5 choices for each of the first 50 questions:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
choices = [5] * 50
|
198
|
+
s.set_choices choices
|
183
199
|
```
|
184
200
|
|
185
|
-
|
201
|
+
`choices` is equivalent to the array of integers passed to the `SheetPDF` constructor as part of the `content` parameter (see above).
|
202
|
+
|
203
|
+
### Marking the response sheet
|
204
|
+
|
205
|
+
We are now ready to mark the sheet:
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
mc = s.marked_choices
|
209
|
+
```
|
210
|
+
|
211
|
+
Since the function `set_choices` returns true only if the sheet is properly registered, it can replace the call to `valid?`, allowing you to write something like:
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
s = SheetOMR.new 'image.jpg', 'layout.yml'
|
215
|
+
if s.set_choices [5] * 50
|
216
|
+
marked = s.marked_choices
|
217
|
+
else
|
218
|
+
puts "The sheet is not registered!"
|
219
|
+
end
|
220
|
+
```
|
221
|
+
|
222
|
+
If all goes well, the `marked` array will contain 100 sub-arrays, each containing the list of marked choices for that item, where the first cell is indicated by a 0, the second by a 1, etc.
|
223
|
+
|
224
|
+
Read the [API documentation](http://www.rubydoc.info/gems/mork) to learn about additional methods to extract marked responses.
|
225
|
+
|
226
|
+
### Applying overlays on the original image
|
227
|
+
|
228
|
+
It is also possible to show the scoring graphically by applying an overlay on top of the scanned image.
|
186
229
|
|
187
230
|
```ruby
|
188
|
-
s = SheetOMR.new 'image.jpg', choices: [5]*100, layout_file: 'layout.yml'
|
189
231
|
s.overlay :check, :marked
|
190
232
|
s.save 'marked_choices.jpg'
|
191
233
|
system 'open marked_choices.jpg' # this works in OSX
|
@@ -194,14 +236,13 @@ system 'open marked_choices.jpg' # this works in OSX
|
|
194
236
|
More than one overlay can be applied on a sheet. For example, in addition to checking the marked choice cells, the expected choices can be outlined to show correct vs. wrong responses:
|
195
237
|
|
196
238
|
```ruby
|
197
|
-
...
|
198
239
|
correct = [[3], [0], [2], [1], [2]] # and so on...
|
199
|
-
s.overlay :check, :marked
|
200
240
|
s.overlay :outline, correct
|
201
|
-
|
241
|
+
s.overlay :check, :marked
|
242
|
+
s.save 'marked_choices_and_outlines.jpg'
|
243
|
+
system 'open marked_choices_and_outlines.jpg' # this works in OSX
|
202
244
|
```
|
203
245
|
|
204
|
-
|
205
246
|
Scoring can only be performed if the sheet gets properly registered, which in turn depends on the quality of the scanned image.
|
206
247
|
|
207
248
|
### Improving sheet registration and marking
|
@@ -213,14 +254,14 @@ Mork tries to be tolerant of variations in the above parameters, but you should
|
|
213
254
|
Check for object “validity” to make sure that registration succeeded:
|
214
255
|
|
215
256
|
```ruby
|
216
|
-
s = SheetOMR.new 'image.jpg',
|
257
|
+
s = SheetOMR.new 'image.jpg', 'layout.yml'
|
217
258
|
s.valid?
|
218
259
|
```
|
219
260
|
|
220
261
|
When registration fails, it is possible to get some information by displaying the status and by applying a dedicated overlay on the original image:
|
221
262
|
|
222
263
|
```ruby
|
223
|
-
s = SheetOMR.new 'image.jpg',
|
264
|
+
s = SheetOMR.new 'image.jpg', 'layout.yml'
|
224
265
|
unless s.valid?
|
225
266
|
s.save_registration 'unregistered.jpg'
|
226
267
|
end
|
data/lib/mork/grid.rb
CHANGED
@@ -9,7 +9,7 @@ module Mork
|
|
9
9
|
class Grid
|
10
10
|
# Calling Grid.new without arguments creates the default boilerplate Grid
|
11
11
|
def initialize(options=nil)
|
12
|
-
@params =
|
12
|
+
@params = default_grid
|
13
13
|
if File.exists?('layout.yml')
|
14
14
|
@params.deeper_merge! symbolize YAML.load_file('layout.yml')
|
15
15
|
end
|
data/lib/mork/grid_const.rb
CHANGED
@@ -1,86 +1,60 @@
|
|
1
1
|
module Mork
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
2
|
+
class Grid
|
3
|
+
# this is the default grid!
|
4
|
+
# default units are millimiters
|
5
|
+
def default_grid
|
6
|
+
{
|
7
|
+
# size of the paper sheet
|
8
|
+
page_size: {
|
9
|
+
width: 210, # A4
|
10
|
+
height: 297
|
11
|
+
},
|
12
|
+
# size, location, search parameters of registration marks
|
13
|
+
reg_marks: {
|
14
|
+
margin: 10, # from sheet edge to registration mark center
|
15
|
+
radius: 3, # of the registration circle
|
16
|
+
offset: 2, # distance between page edge and registraton mark search area
|
17
|
+
crop: 20, # size of square where the regmark should be located
|
18
|
+
dilate: 5, # set to >0 to apply a dilate IM operation
|
19
|
+
blur: 2, # set to >0 to apply a blur IM operation
|
20
|
+
contrast: 20 # minimum contrast between registration mark circles and the white paper
|
21
|
+
},
|
22
|
+
# you can place multiple elements in the header; title is the only default
|
23
|
+
header: {
|
24
|
+
title: {
|
25
|
+
top: 15,
|
26
|
+
left: 15,
|
27
|
+
width: 160,
|
28
|
+
height: 12,
|
29
|
+
size: 12
|
30
|
+
}
|
31
|
+
},
|
32
|
+
# questions and answers
|
33
|
+
items: {
|
34
|
+
threshold: 0.75, # how much darker a marked cell should be compared to cal cells
|
35
|
+
columns: 4,
|
36
|
+
column_width: 44,
|
37
|
+
rows: 30,
|
38
|
+
left: 11, # distance from the top-left registration mark...
|
39
|
+
top: 55, # ...to the center of the first choice cell
|
40
|
+
x_spacing: 7, # between choices
|
41
|
+
y_spacing: 7, # between rows
|
42
|
+
cell_width: 6, # choice cell size
|
43
|
+
cell_height: 5, # choice cell size
|
44
|
+
max_cells: 5, # the maximum number of choices per question
|
45
|
+
font_size: 9, # for the question number and choice letters
|
46
|
+
number_width: 8, # distance between right side of q num and left side of first choice cell
|
47
|
+
number_margin: 2 # width of question number text box
|
48
|
+
},
|
49
|
+
# unique sheet ID as a binary barcode
|
50
|
+
barcode: {
|
51
|
+
bits: 38,
|
52
|
+
left: 15,
|
53
|
+
width: 3,
|
54
|
+
height: 3,
|
55
|
+
spacing: 4
|
56
|
+
}
|
51
57
|
}
|
52
|
-
|
53
|
-
|
54
|
-
threshold: 0.75,
|
55
|
-
columns: 4,
|
56
|
-
column_width: 44,
|
57
|
-
rows: 30,
|
58
|
-
# from the top-left registration mark
|
59
|
-
# to the center of the first choice cell
|
60
|
-
left: 11,
|
61
|
-
top: 55,
|
62
|
-
# between choices
|
63
|
-
x_spacing: 7,
|
64
|
-
# between rows
|
65
|
-
y_spacing: 7,
|
66
|
-
# darkened area
|
67
|
-
cell_width: 6,
|
68
|
-
cell_height: 5,
|
69
|
-
# the maximum number of choices per question
|
70
|
-
max_cells: 5,
|
71
|
-
# font size for the question number and choice letters
|
72
|
-
font_size: 9,
|
73
|
-
# distance between right side of q num and left side of first choice cell
|
74
|
-
number_width: 8,
|
75
|
-
# width of question number text box
|
76
|
-
number_margin: 2
|
77
|
-
}, # items end
|
78
|
-
barcode: {
|
79
|
-
bits: 40,
|
80
|
-
left: 15,
|
81
|
-
width: 3,
|
82
|
-
height: 3,
|
83
|
-
spacing: 4
|
84
|
-
} # barcode end
|
85
|
-
}
|
58
|
+
end
|
59
|
+
end
|
86
60
|
end
|
data/lib/mork/grid_pdf.rb
CHANGED
@@ -70,6 +70,10 @@ module Mork
|
|
70
70
|
@cround ||= [width_of_cell, height_of_cell].min / 2
|
71
71
|
end
|
72
72
|
|
73
|
+
def missing_header?(k)
|
74
|
+
@params[:header][k].nil?
|
75
|
+
end
|
76
|
+
|
73
77
|
def page_size() [page_width.mm, page_height.mm] end
|
74
78
|
def margins() reg_margin.mm end
|
75
79
|
def qnum_margin() @params[:items][:number_margin].to_f.mm end
|
data/lib/mork/mimage.rb
CHANGED
@@ -5,12 +5,13 @@ module Mork
|
|
5
5
|
# @private
|
6
6
|
class Mimage
|
7
7
|
attr_reader :rm
|
8
|
+
attr_reader :choxq # choices per question
|
8
9
|
|
9
|
-
def initialize(path,
|
10
|
-
@mack
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@rm
|
10
|
+
def initialize(path, grom)
|
11
|
+
@mack = Magicko.new path
|
12
|
+
@grom = grom.set_page_size @mack.width, @mack.height
|
13
|
+
@choxq = [grom.max_choices_per_question] * grom.max_questions
|
14
|
+
@rm = {} # registration mark centers
|
14
15
|
@valid = register
|
15
16
|
end
|
16
17
|
|
@@ -27,17 +28,19 @@ module Mork
|
|
27
28
|
}
|
28
29
|
end
|
29
30
|
|
30
|
-
def
|
31
|
-
@
|
32
|
-
|
33
|
-
|
31
|
+
def set_ch(cho)
|
32
|
+
@choxq = cho
|
33
|
+
# if set_ch is called more than once, discard memoization
|
34
|
+
@marked_choices = nil
|
34
35
|
end
|
35
36
|
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
def marked
|
38
|
+
@marked_choices ||= begin
|
39
|
+
@choxq.map.with_index do |ncho, q|
|
40
|
+
[].tap do |choices|
|
41
|
+
ncho.times do |c|
|
42
|
+
choices << c if shade_of(q, c) < choice_threshold
|
43
|
+
end
|
41
44
|
end
|
42
45
|
end
|
43
46
|
end
|
@@ -58,11 +61,12 @@ module Mork
|
|
58
61
|
when :cal
|
59
62
|
@grom.calibration_cell_areas
|
60
63
|
when :marked
|
61
|
-
choice_cell_areas
|
64
|
+
choice_cell_areas marked
|
62
65
|
when :all
|
63
66
|
all_choice_cell_areas
|
64
67
|
when :max
|
65
|
-
@grom.
|
68
|
+
choice_cell_areas [@grom.max_choices_per_question] * @grom.max_questions
|
69
|
+
# @grom.max_questions.times.map { |i| (0...@grom.max_choices_per_question).to_a }
|
66
70
|
when Array
|
67
71
|
choice_cell_areas where
|
68
72
|
else
|
@@ -90,7 +94,7 @@ module Mork
|
|
90
94
|
private #
|
91
95
|
# ============================================================#
|
92
96
|
|
93
|
-
def itemator(items=@
|
97
|
+
def itemator(items=@choxq)
|
94
98
|
items.map.with_index do |cho, q|
|
95
99
|
if cho.is_a? Fixnum
|
96
100
|
cho.times.map { |c| yield q, c }
|
@@ -105,7 +109,7 @@ module Mork
|
|
105
109
|
end
|
106
110
|
|
107
111
|
def all_choice_cell_areas
|
108
|
-
@all_choice_cell_areas ||= choice_cell_areas(@
|
112
|
+
@all_choice_cell_areas ||= choice_cell_areas(@choxq)
|
109
113
|
end
|
110
114
|
|
111
115
|
def each_corner
|
@@ -122,10 +126,10 @@ module Mork
|
|
122
126
|
end
|
123
127
|
end
|
124
128
|
|
125
|
-
# TODO: 0.75 should be a parameter
|
126
129
|
def choice_threshold
|
127
|
-
|
128
|
-
|
130
|
+
@choice_threshold ||= begin
|
131
|
+
(cal_cell_mean-darkest_cell_mean) * @grom.choice_threshold + darkest_cell_mean
|
132
|
+
end
|
129
133
|
end
|
130
134
|
|
131
135
|
def barcode_threshold
|
@@ -258,3 +262,13 @@ end
|
|
258
262
|
# puts "BL: #{@rm[:bl].inspect}"
|
259
263
|
|
260
264
|
# puts "REG #{@grom.rm_blur} - #{@grom.rm_dilate} - C #{c.inspect}"
|
265
|
+
|
266
|
+
# def marked_int
|
267
|
+
# marked.map do |q|
|
268
|
+
# [].tap do |choices|
|
269
|
+
# q.each_with_index do |choice, idx|
|
270
|
+
# choices << idx if choice
|
271
|
+
# end
|
272
|
+
# end
|
273
|
+
# end
|
274
|
+
# end
|
data/lib/mork/npatch.rb
CHANGED
@@ -10,8 +10,6 @@ module Mork
|
|
10
10
|
def initialize(source, width, height)
|
11
11
|
@patch = NArray.float(width, height)
|
12
12
|
@patch[true] = source
|
13
|
-
# @width = width
|
14
|
-
# @height = height
|
15
13
|
end
|
16
14
|
|
17
15
|
def average(coord)
|
@@ -22,11 +20,6 @@ module Mork
|
|
22
20
|
@patch[coord.x_rng, coord.y_rng].stddev
|
23
21
|
end
|
24
22
|
|
25
|
-
# def length
|
26
|
-
# # is this only going to be used for testing purposes?
|
27
|
-
# @patch.length
|
28
|
-
# end
|
29
|
-
|
30
23
|
def centroid
|
31
24
|
xp = @patch.sum(1).to_a
|
32
25
|
yp = @patch.sum(0).to_a
|
@@ -59,3 +52,8 @@ end
|
|
59
52
|
# # tested with the few examples: spec/samples/rm0x.jpeg
|
60
53
|
# p.stddev > 20
|
61
54
|
# end
|
55
|
+
|
56
|
+
# def length
|
57
|
+
# # is this only going to be used for testing purposes?
|
58
|
+
# @patch.length
|
59
|
+
# end
|
data/lib/mork/sheet_omr.rb
CHANGED
@@ -14,26 +14,14 @@ module Mork
|
|
14
14
|
class SheetOMR
|
15
15
|
# @param path [String] the required path/filename to the saved image
|
16
16
|
# (.jpg, .jpeg, .png, or .pdf)
|
17
|
-
# @param
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# is passed, or on the indicated questions, if the argument is an array.
|
21
|
-
# @param layout [String, Hash] the sheet description. Use a string to
|
22
|
-
# specify the path/filename of a YAML file containing the parameters,
|
23
|
-
# or directly a hash of parameters. See the README file for a full listing
|
17
|
+
# @param layout [String, Hash] the sheet description. Send a hash of
|
18
|
+
# parameters or a string to specify the path/filename of a YAML file
|
19
|
+
# containing the parameters. See the README file for a full listing
|
24
20
|
# of the available parameters.
|
25
|
-
def initialize(path,
|
21
|
+
def initialize(path, layout=nil)
|
26
22
|
raise IOError, "File '#{path}' not found" unless File.exists? path
|
27
|
-
grom
|
28
|
-
|
29
|
-
when NilClass
|
30
|
-
[grom.max_choices_per_question] * grom.max_questions
|
31
|
-
when Fixnum
|
32
|
-
[grom.max_choices_per_question] * choices
|
33
|
-
when Array
|
34
|
-
choices
|
35
|
-
end
|
36
|
-
@mim = Mimage.new path, nitems, grom
|
23
|
+
grom = GridOMR.new layout
|
24
|
+
@mim = Mimage.new path, grom
|
37
25
|
end
|
38
26
|
|
39
27
|
# True if sheet registration completed successfully
|
@@ -43,6 +31,30 @@ module Mork
|
|
43
31
|
@mim.valid?
|
44
32
|
end
|
45
33
|
|
34
|
+
# Setting the choices/questions to analyze. If this function is not called,
|
35
|
+
# the maximum number of choices/questions allowed by the layout will be
|
36
|
+
# evaluated.
|
37
|
+
#
|
38
|
+
# @param choices [Fixnum, Array] the questions/choices we want subsequent
|
39
|
+
# scoring/overlaying to apply to. Normally, `choices` should be an array
|
40
|
+
# of integers, with each element indicating the number of available
|
41
|
+
# choices for the corresponding question (i.e. `choices.length` is the
|
42
|
+
# number of questions). As a shortcut, `choices` can also be a single
|
43
|
+
# integer value, indicating the number of questions; in such case, the
|
44
|
+
# maximum number of choices allowed by the layout will be considered.
|
45
|
+
#
|
46
|
+
# @return [Boolean] True if the sheet is properly registered and ready to
|
47
|
+
# be marked; false otherwise.
|
48
|
+
def set_choices(cho)
|
49
|
+
return false unless valid?
|
50
|
+
@mim.set_ch case cho
|
51
|
+
when Fixnum; @mim.choxq[0...cho]
|
52
|
+
when Array; cho
|
53
|
+
else raise ArgumentError, 'Invalid choice set'
|
54
|
+
end
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
46
58
|
# Registration status for each of the four corners
|
47
59
|
#
|
48
60
|
# @return [Hash] { tl: Symbol, tr: Symbol, br: Symbol, bl: Symbol } where
|
@@ -78,7 +90,7 @@ module Mork
|
|
78
90
|
# @return [Boolean]
|
79
91
|
def marked?(question, choice)
|
80
92
|
return if not_registered
|
81
|
-
|
93
|
+
marked_choices[question].find {|x| x==choice} ? true : false
|
82
94
|
end
|
83
95
|
|
84
96
|
# The set of choice indices marked on the response sheet
|
@@ -89,12 +101,22 @@ module Mork
|
|
89
101
|
# indicates that the responder has marked the first choice for the first
|
90
102
|
# question, none for the second, and the fourth and fifth choices for the
|
91
103
|
# third question.
|
92
|
-
#
|
93
|
-
# Note that only the questions/choices indicated via the `choices` argument
|
94
|
-
# during object creation are evaluated.
|
95
104
|
def marked_choices
|
96
105
|
return if not_registered
|
97
|
-
@mim.
|
106
|
+
@mim.marked
|
107
|
+
end
|
108
|
+
|
109
|
+
# The set of choice indices marked on the response sheet. If more than one
|
110
|
+
# choice was marked for a question, the response is regarded as invalid and
|
111
|
+
# treated as if it had been left blank.
|
112
|
+
#
|
113
|
+
# @return [Array] an array of integers; each element contains
|
114
|
+
# the (zero-based) marked choice for the corresponding question.
|
115
|
+
def marked_choices_unique
|
116
|
+
return if not_registered
|
117
|
+
marked_choices.map do |c|
|
118
|
+
c.length == 1 ? c.first : nil
|
119
|
+
end
|
98
120
|
end
|
99
121
|
|
100
122
|
# The set of letters marked on the response sheet. At this time, only the
|
@@ -102,9 +124,6 @@ module Mork
|
|
102
124
|
#
|
103
125
|
# @return [Array] an array of arrays of 1-character strings; each element
|
104
126
|
# contains the list of letters marked for the corresponding question.
|
105
|
-
#
|
106
|
-
# Note that only the questions/choices indicated via the `choices` argument
|
107
|
-
# during object creation are evaluated.
|
108
127
|
def marked_letters
|
109
128
|
return if not_registered
|
110
129
|
marked_choices.map do |q|
|
@@ -112,13 +131,17 @@ module Mork
|
|
112
131
|
end
|
113
132
|
end
|
114
133
|
|
115
|
-
#
|
134
|
+
# The set of letters marked on the response sheet. At this time, only the
|
135
|
+
# latin sequence 'A, B, C...' is supported. If more than one choice was
|
136
|
+
# marked for an item, the response is regarded as invalid and treated as if
|
137
|
+
# it had been left blank.
|
116
138
|
#
|
117
|
-
# @return [Array] an array of
|
118
|
-
|
119
|
-
def marked_logicals
|
139
|
+
# @return [Array] an array of 1-character strings
|
140
|
+
def marked_letters_unique
|
120
141
|
return if not_registered
|
121
|
-
|
142
|
+
marked_choices_unique.map do |c|
|
143
|
+
c.nil?? '' : (65+c).chr
|
144
|
+
end
|
122
145
|
end
|
123
146
|
|
124
147
|
# Apply an overlay on the image
|
@@ -131,8 +154,8 @@ module Mork
|
|
131
154
|
# specified by the `choices` argument during object creation
|
132
155
|
# (this is the default); `:all`: all cells in `choices`;
|
133
156
|
# `:max`: maximum number of cells allowed by the layout (can be larger
|
134
|
-
#
|
135
|
-
#
|
157
|
+
# than `:all`); `:barcode`: the dark barcode elements; `:cal` the
|
158
|
+
# calibration cells
|
136
159
|
def overlay(what, where=:marked)
|
137
160
|
return if not_registered
|
138
161
|
@mim.overlay what, where
|
@@ -276,3 +299,26 @@ end
|
|
276
299
|
# def barcode_bit_string(i)
|
277
300
|
# @mim.barcode_bit?(i) ? "1" : "0"
|
278
301
|
# end
|
302
|
+
|
303
|
+
# def validate_choices(ch=nil)
|
304
|
+
# return false unless valid?
|
305
|
+
# cho = case ch
|
306
|
+
# when NilClass; [@mc] * @mq
|
307
|
+
# when Fixnum; [@mc] * [[ch, @mq].min, 1].max
|
308
|
+
# when Array; ch
|
309
|
+
# else raise ArgumentError, 'Invalid choice set'
|
310
|
+
# end
|
311
|
+
# @marked_choices = @mim.marked cho
|
312
|
+
# true
|
313
|
+
# end
|
314
|
+
|
315
|
+
# # Marked choices as boolean values
|
316
|
+
# #
|
317
|
+
# # @return [Array] an array of arrays of true/false values corresponding to
|
318
|
+
# # marked vs unmarked choice cells.
|
319
|
+
# def marked_logicals
|
320
|
+
# return if not_registered
|
321
|
+
# # this is the only marking function calling the mimage object
|
322
|
+
# @marked_choices ||= @mim.marked
|
323
|
+
# end
|
324
|
+
|
data/lib/mork/sheet_pdf.rb
CHANGED
@@ -90,8 +90,11 @@ module Mork
|
|
90
90
|
@grip.barcode_xy_for(code).each { |c| stamp_at 'barcode', c }
|
91
91
|
end
|
92
92
|
|
93
|
-
def header(
|
94
|
-
|
93
|
+
def header(elements)
|
94
|
+
elements.each do |k,v|
|
95
|
+
if @grip.missing_header? k
|
96
|
+
raise ArgumentError, "The header element '#{k}' is not described in the layout"
|
97
|
+
end
|
95
98
|
font_size @grip.header_size(k) do
|
96
99
|
align = @grip.header_align(k).nil?? :left : @grip.header_align(k).to_sym
|
97
100
|
if @grip.header_boxed?(k)
|
data/lib/mork/version.rb
CHANGED
data/spec/mork/mimage_spec.rb
CHANGED
@@ -7,7 +7,7 @@ module Mork
|
|
7
7
|
context 'John Doe' do
|
8
8
|
let(:img) { sample_img 'jdoe1' }
|
9
9
|
let(:fn) { File.basename(img.image_path) }
|
10
|
-
let(:mim) { Mimage.new img.image_path,
|
10
|
+
let(:mim) { Mimage.new img.image_path, GridOMR.new(img.grid_path) }
|
11
11
|
describe 'basics' do
|
12
12
|
it 'should be valid' do
|
13
13
|
expect(mim.valid?).to be_truthy
|
data/spec/mork/sheet_omr_spec.rb
CHANGED
@@ -6,7 +6,7 @@ module Mork
|
|
6
6
|
context 'with a valid response sheet' do
|
7
7
|
let(:img) { sample_img 'jdoe1' }
|
8
8
|
let(:fn) { File.basename(img.image_path) }
|
9
|
-
let(:omr) { SheetOMR.new img.image_path,
|
9
|
+
let(:omr) { SheetOMR.new img.image_path, layout: img.grid_path }
|
10
10
|
describe '#new' do
|
11
11
|
it 'creates a SheetOMR object' do
|
12
12
|
expect(omr).to be_a SheetOMR
|
@@ -17,6 +17,12 @@ module Mork
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
+
describe '#set_choices' do
|
21
|
+
it 'returns true if all goes well' do
|
22
|
+
expect(omr.set_choices(10)).to be_truthy
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
20
26
|
describe '#valid?' do
|
21
27
|
it 'registers correctly' do
|
22
28
|
expect(omr.valid?).to be_truthy
|
@@ -50,29 +56,18 @@ module Mork
|
|
50
56
|
end
|
51
57
|
end
|
52
58
|
|
53
|
-
describe '#marked_choices' do
|
54
|
-
it 'returns an array of marked choices as position indices' do
|
55
|
-
expect(omr.marked_choices).to eq standard_mark_array(24)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
59
|
describe '#marked_choices' do
|
60
60
|
it 'returns an array of marked choices as position indexes' do
|
61
61
|
expect(omr.marked_choices ).to eq standard_mark_array(24)
|
62
62
|
end
|
63
63
|
|
64
|
-
let(:om2) { SheetOMR.new img.image_path,
|
64
|
+
let(:om2) { SheetOMR.new img.image_path, layout: img.grid_path }
|
65
65
|
it 'returns marked choices only for existing choice cells' do
|
66
|
+
om2.set_choices [5, 4, 3, 2, 1]
|
66
67
|
expect(om2.marked_choices).to eq [[0], [1], [2], [], []]
|
67
68
|
end
|
68
69
|
end
|
69
70
|
|
70
|
-
describe '#marked_logicals' do
|
71
|
-
it 'returns an array of logicals for the marked choices' do
|
72
|
-
expect(omr.marked_logicals).to eq standard_mark_logical_array(24)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
71
|
describe '#marked_letters' do
|
77
72
|
it 'returns an array of characters for the marked choices' do
|
78
73
|
expect(omr.marked_letters).to eq standard_mark_char_array(24)
|
@@ -96,8 +91,15 @@ module Mork
|
|
96
91
|
end
|
97
92
|
|
98
93
|
it 'highlights all choice cells' do
|
94
|
+
omr.set_choices [5] * 32
|
99
95
|
omr.overlay :highlight, :all
|
100
|
-
omr.save "spec/out/highlight
|
96
|
+
omr.save "spec/out/highlight/all-#{fn}"
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'highlights all possible choice cells' do
|
100
|
+
omr.set_choices [5] * 30
|
101
|
+
omr.overlay :highlight, :max
|
102
|
+
omr.save "spec/out/highlight/max-#{fn}"
|
101
103
|
end
|
102
104
|
|
103
105
|
it 'highlights marked cells' do
|
@@ -110,6 +112,12 @@ module Mork
|
|
110
112
|
omr.save "spec/out/mark/#{fn}"
|
111
113
|
end
|
112
114
|
|
115
|
+
it 'checks the first 32 marked cells' do
|
116
|
+
omr.set_choices [5] * 32
|
117
|
+
omr.overlay :check, :marked
|
118
|
+
omr.save "spec/out/mark/part-#{fn}"
|
119
|
+
end
|
120
|
+
|
113
121
|
it 'outlines and crosses marked cells' do
|
114
122
|
omr.overlay :outline, standard_mark_array(24)
|
115
123
|
omr.overlay :check
|
data/spec/mork/sheet_pdf_spec.rb
CHANGED
@@ -4,13 +4,10 @@ module Mork
|
|
4
4
|
describe SheetPDF do
|
5
5
|
let(:content) {
|
6
6
|
{
|
7
|
-
barcode:
|
7
|
+
barcode: 183251937962,
|
8
8
|
choices: [5] * 120,
|
9
9
|
header: {
|
10
|
-
|
11
|
-
title: 'A really serious and difficult test - 18 January 2013',
|
12
|
-
code: '201.48',
|
13
|
-
signature: 'Signature'
|
10
|
+
title: 'A serious, difficult test - 31 December 1999',
|
14
11
|
}
|
15
12
|
}
|
16
13
|
}
|
@@ -29,6 +26,12 @@ module Mork
|
|
29
26
|
lambda { SheetPDF.new(content, 2) }.should raise_error ArgumentError
|
30
27
|
end
|
31
28
|
|
29
|
+
it 'raises an error if a header part is not described in the layout' do
|
30
|
+
lambda {
|
31
|
+
SheetPDF.new({header: {dummy: 'yes I am'}})
|
32
|
+
}.should raise_error ArgumentError
|
33
|
+
end
|
34
|
+
|
32
35
|
it 'assigns an array to @content' do
|
33
36
|
s = SheetPDF.new(content)
|
34
37
|
s.instance_variable_get('@content').should be_an Array
|
@@ -39,62 +42,67 @@ module Mork
|
|
39
42
|
s.instance_variable_get('@content').first.should be_a Hash
|
40
43
|
end
|
41
44
|
|
42
|
-
it 'creates a
|
43
|
-
s = SheetPDF.new(
|
44
|
-
s.save
|
45
|
+
it 'creates a minimal PDF sheet' do
|
46
|
+
s = SheetPDF.new({})
|
47
|
+
s.save dest 'minimal'
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'creates a PDF sheet with a big barcode' do
|
51
|
+
s = SheetPDF.new({barcode: 183251937962})
|
52
|
+
s.save dest 'bigbarcode'
|
45
53
|
end
|
46
54
|
|
47
|
-
it 'creates a boxed
|
55
|
+
it 'creates a PDF sheet with several boxed header elements' do
|
48
56
|
h = {
|
49
57
|
name: lorem,
|
50
58
|
title: lorem,
|
51
59
|
code: '1000.10.100',
|
52
60
|
signature: 'Signature'
|
53
61
|
}
|
54
|
-
s = SheetPDF.new(
|
55
|
-
s.save
|
62
|
+
s = SheetPDF.new({header: h}, 'spec/samples/boxy.yml')
|
63
|
+
s.save dest 'boxy'
|
56
64
|
end
|
57
65
|
|
58
|
-
it 'creates a
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
s.
|
66
|
-
|
67
|
-
|
68
|
-
it 'creates a PDF sheet with the maximum possible barcode' do
|
69
|
-
s = SheetPDF.new(content.merge({barcode: 1099511627775}))
|
70
|
-
s.save('spec/out/pdf/maxcode.pdf')
|
66
|
+
it 'creates a PDF sheet with the maximum possible barcode for 38 bits' do
|
67
|
+
c = {
|
68
|
+
barcode: 274877906943,
|
69
|
+
header: {
|
70
|
+
title: 'The maximum barcode for 38 bits is 274877906943'
|
71
|
+
}
|
72
|
+
}
|
73
|
+
s = SheetPDF.new(c)
|
74
|
+
s.save dest 'maxcode'
|
71
75
|
end
|
72
76
|
|
73
77
|
it 'creates a PDF sheet with 160 items' do
|
74
78
|
s = SheetPDF.new(content.merge({choices: [5] * 160}), 'spec/samples/grid160.yml')
|
75
|
-
s.save
|
79
|
+
s.save dest 'i160'
|
76
80
|
end
|
77
81
|
|
78
82
|
it 'creates a PDF sheet with unequal choices per item' do
|
79
|
-
|
80
|
-
|
83
|
+
c = {
|
84
|
+
choices: [5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1],
|
85
|
+
header: {
|
86
|
+
title: 'Each question can have an arbitrary number of choices'
|
87
|
+
}
|
88
|
+
}
|
89
|
+
SheetPDF.new(c).save dest('uneq')
|
81
90
|
end
|
82
91
|
|
83
92
|
it 'creates 20 PDF sheets' do
|
84
|
-
c =
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
it 'creates a pdf with minimal content' do
|
90
|
-
s = SheetPDF.new({choices: [4] * 30})
|
91
|
-
s.save 'spec/out/pdf/mincont.pdf'
|
93
|
+
c = 20.times.collect do |x|
|
94
|
+
content.merge({ header: {title: "Test #{x+1}"}, barcode: x})
|
95
|
+
end
|
96
|
+
SheetPDF.new(c).save dest('p20')
|
92
97
|
end
|
93
98
|
|
94
99
|
it 'creates a PDF with the maximum possible choice cells if none are specified' do
|
95
100
|
ct = [{}, {choices: [3]*20}]
|
96
|
-
|
97
|
-
|
101
|
+
SheetPDF.new(ct).save dest('nocontent')
|
102
|
+
end
|
103
|
+
|
104
|
+
def dest(fname)
|
105
|
+
"spec/out/pdf/#{fname}.pdf"
|
98
106
|
end
|
99
107
|
end
|
100
108
|
end
|
data/spec/samples/boxy.yml
CHANGED
@@ -1,13 +1,3 @@
|
|
1
|
-
page_size: # all measurements in mm
|
2
|
-
width: 210 # width of the paper sheet
|
3
|
-
height: 297 # height of the paper sheet
|
4
|
-
reg_marks:
|
5
|
-
margin: 10 # distance from each page border to registration mark center
|
6
|
-
radius: 3 # registration mark radius
|
7
|
-
offset: 2 # distance between the search area and each page border (*)
|
8
|
-
crop: 20
|
9
|
-
blur: 2
|
10
|
-
dilate: 5
|
11
1
|
header:
|
12
2
|
name: # ‘name’ is just a label; you can add arbitrary header elements
|
13
3
|
top: 5 # margin relative to registration frame top side
|
@@ -15,7 +5,7 @@ header:
|
|
15
5
|
width: 160 # text will be fitted to this width
|
16
6
|
height: 7 # text will be fitted to this height
|
17
7
|
size: 14 # font size
|
18
|
-
|
8
|
+
box: true
|
19
9
|
title:
|
20
10
|
top: 15
|
21
11
|
left: 15
|
@@ -38,23 +28,3 @@ header:
|
|
38
28
|
height: 15
|
39
29
|
size: 7
|
40
30
|
box: true # header element will be enclosed in a box
|
41
|
-
items:
|
42
|
-
top: 55.5 # response area margin, relative to reg frame
|
43
|
-
left: 10.5 # response area margin, relative to reg frame
|
44
|
-
rows: 30 # number of items per column
|
45
|
-
columns: 4 # number of columns
|
46
|
-
column_width: 44 #
|
47
|
-
x_spacing: 7 # horizontal distance between ajacent cell centers
|
48
|
-
y_spacing: 7 # vertical distance between ajacent cell centers
|
49
|
-
cell_width: 6 # width of each choice and calibration cell
|
50
|
-
cell_height: 5 # height of each choice and calibration cell
|
51
|
-
max_cells: 5 # maximum number of choices per item
|
52
|
-
font_size: 9 # size of both the item number and choice cell letter
|
53
|
-
number_width: 8 #
|
54
|
-
number_margin: 2 # margin between
|
55
|
-
barcode:
|
56
|
-
bits: 40 # the maximum sheet identifier is 2 to the power or bits
|
57
|
-
left: 15 # distance between registration frame side and the first barcode bit
|
58
|
-
width: 3 # width of each barcode bit
|
59
|
-
height: 2.5 # height of each barcode bit from the registration frame bottom side
|
60
|
-
spacing: 4 # horizontal distance between adjacent barcode bit centers
|
data/spec/samples/info.yml
CHANGED
@@ -4,7 +4,7 @@ jdoe1:
|
|
4
4
|
nchoices: 5
|
5
5
|
nitems: 120
|
6
6
|
barcode_int: 1234566
|
7
|
-
barcode_str: '
|
7
|
+
barcode_str: '00000000000000000100101101011010000110'
|
8
8
|
width: 1240
|
9
9
|
height: 1754
|
10
10
|
mark_array: [[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4]]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mork
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Giuseppe Bertini
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-06-
|
11
|
+
date: 2016-06-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: narray
|