mork 0.9.3 → 0.10.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/.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
|