mork 0.10.0 → 0.11.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +133 -91
- data/lib/mork.rb +1 -0
- data/lib/mork/extensions.rb +26 -16
- data/lib/mork/grid.rb +6 -6
- data/lib/mork/grid_const.rb +5 -3
- data/lib/mork/mimage.rb +4 -1
- data/lib/mork/sheet_omr.rb +4 -4
- data/lib/mork/sheet_pdf.rb +16 -9
- data/lib/mork/version.rb +1 -1
- data/spec/mork/grid_omr_spec.rb +1 -1
- data/spec/mork/grid_spec.rb +11 -82
- data/spec/mork/sheet_pdf_spec.rb +32 -15
- data/spec/samples/base_layout.yml +40 -0
- data/spec/samples/content.yml +9 -0
- data/spec/samples/standard.png +0 -0
- data/spec/samples/standard.yml +27 -0
- metadata +6 -12
- data/spec/mork/extensions_spec.rb +0 -10
- 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/marisol/marisol1.jpg +0 -0
- data/spec/samples/marisol/marisol2.jpg +0 -0
- data/spec/samples/marisol/marisol3.jpg +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23ab6d5cb32c6c9474e304f619305b1841afb6a7
|
4
|
+
data.tar.gz: 021b401aeb958368de5bdaaf64908bbc42dc1e14
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be6f8857ff7c2559204d40a5d7ac743a1b64cc2f6b9a84ee7c4b54956db55754180d07d3ecf0a36fc77e838f4758e55d99b710740328e36e76b1b58b2e1834d3
|
7
|
+
data.tar.gz: 3bef5ff687f155a1094ff2a384e7b0cb4b504b1cdc7c47e68b5766cdb714b4214752a24154e7f9170ba4b3a96c2a1bc6b544a6e1adc319d33498935c5fef3664
|
data/README.md
CHANGED
@@ -3,11 +3,11 @@
|
|
3
3
|
A ruby [optical mark recognition](http://en.wikipedia.org/wiki/Optical_mark_recognition) (OMR) library aimed at accomplishing two tasks in the context of paper-based, multiple-choice tests and surveys:
|
4
4
|
|
5
5
|
1. generating [response sheets](/spec/samples/sheet.jpg) in PDF format
|
6
|
-
2.
|
6
|
+
2. detecting the responses provided on the [printed sheet](/spec/samples/sample_gray.jpg) by a human with a pen or a pencil.
|
7
7
|
|
8
|
-
##
|
8
|
+
## Roadmap
|
9
9
|
|
10
|
-
|
10
|
+
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
11
|
|
12
12
|
## Assumptions and limitations
|
13
13
|
|
@@ -29,7 +29,7 @@ Mork is a low-level library, and very much work in progress. It is not, and will
|
|
29
29
|
|
30
30
|
## Getting started
|
31
31
|
|
32
|
-
First, make sure that ImageMagick is installed in your system. Typing `convert in a terminal shell should print out a long help message. If instead you get
|
32
|
+
First, make sure that ImageMagick is installed in your system. Typing `convert in a terminal shell should print out a long help message. If instead you get a “command not found”-like error, you will need to install ImageMagick.
|
33
33
|
|
34
34
|
In OS X, `brew` is an excellent package manager:
|
35
35
|
|
@@ -57,10 +57,14 @@ Edit the file `mork_test.rb` with the code snippets below, then execute the foll
|
|
57
57
|
|
58
58
|
## Generating response sheets with `SheetPDF`
|
59
59
|
|
60
|
-
Response sheets are created through the `Mork::SheetPDF` class. Two pieces of information must be provided to the class constructor to produce a meaningful sheet:
|
60
|
+
Response sheets are created through the `Mork::SheetPDF` class. Two pieces of information must be provided to the class constructor to produce a meaningful sheet or, more commonly, a set of similar sheets:
|
61
61
|
|
62
|
-
- **content**: what to place on
|
63
|
-
- **layout**: sizes, margins, spacing, fonts, etc
|
62
|
+
- **content**: what to place specifically on each sheet, such as how many response items and choices, what to write in the header, the number to print as a barcode
|
63
|
+
- **layout**: the sheet description in terms of element positioning, sizes, margins, spacing, fonts, etc. Layout settings apply equally to all generated sheets
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
s = SheetPDF.new content, layout
|
67
|
+
```
|
64
68
|
|
65
69
|
Let’s look at each argument in turn.
|
66
70
|
|
@@ -70,16 +74,16 @@ The `content` argument should be a hash like the following:
|
|
70
74
|
|
71
75
|
```ruby
|
72
76
|
content = {
|
73
|
-
# the response sheet's unique identifier; it is printed
|
74
|
-
# in binary form as a barcode at the bottom of the sheet
|
75
|
-
barcode: 123456,
|
76
77
|
# number of items and number of choices per item
|
77
78
|
# in this case: 100 items with 5 choices each
|
78
79
|
choices: [5] * 100,
|
80
|
+
# the response sheet's unique identifier; it is printed
|
81
|
+
# in binary form as a barcode at the bottom of the sheet
|
82
|
+
barcode: 123456,
|
79
83
|
# stuff to print in the header
|
80
84
|
header: {
|
81
85
|
name: 'John Doe UI354320',
|
82
|
-
title: '
|
86
|
+
title: 'A serious, difficult test - 31 December 1999',
|
83
87
|
code: '201.48',
|
84
88
|
signature: 'Signature'
|
85
89
|
}
|
@@ -89,86 +93,131 @@ content = {
|
|
89
93
|
These are the key-value pairs that the `content` hash may contain:
|
90
94
|
|
91
95
|
- **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
|
92
|
-
- **
|
93
|
-
- **
|
96
|
+
- **barcode**: an integer number, the sheet's unique identifier to be printed as a binary barcode along the bottom edge of the sheet; if omitted, no barcode is printed
|
97
|
+
- **header**: a (sub)hash defining header elements, where each key is the element’s name and each value is the content to be rendered; you can place an arbitrary number of elements in the header, as long as the same element names are also defined in the `layout` (see below); if omitted, no header elements are printed
|
98
|
+
|
99
|
+
In most situations, you will want to generate several sheets at once. To do that, simply pass an array of content hashes to the constructor.
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
content = [
|
103
|
+
{
|
104
|
+
barcode: 1001,
|
105
|
+
header: { name: 'John Doe UI354320', code: '1001'}
|
106
|
+
},
|
107
|
+
{
|
108
|
+
barcode: 1002,
|
109
|
+
header: { name: 'Jane Roe UI354321', code: '1002'}
|
110
|
+
}
|
111
|
+
]
|
112
|
+
```
|
113
|
+
|
114
|
+
The `content` can also be specified by passing a string with the path/name of a YAML file like the following:
|
115
|
+
|
116
|
+
```yaml
|
117
|
+
- barcode: 1001
|
118
|
+
header:
|
119
|
+
name: 'John Doe UI354320'
|
120
|
+
code: '1001'
|
121
|
+
- barcode: 1002
|
122
|
+
header:
|
123
|
+
name: 'Jane Roe UI354321'
|
124
|
+
code: '1002'
|
125
|
+
```
|
94
126
|
|
95
127
|
### layout
|
96
128
|
|
97
|
-
The layout
|
129
|
+
The layout specifies exactly the size and location of choice cells, header elements, and barcode bits; it also specifies parameters to fine tune detection of marked cells and of registration marks (see below).
|
130
|
+
|
131
|
+
The layout hash is put together in 3 steps during `SheetPDF` initialization:
|
132
|
+
|
133
|
+
1. at first, the layout is generated based on a set of built-in options
|
134
|
+
2. next, if a file named `layout.yml` exists in the current path, Mork automatically parses it; any values found override the built-in ones
|
135
|
+
3. finally, values specified in the constructor argument override the existing ones
|
136
|
+
|
137
|
+
This is the list of built-in values that the layout is initially based on. 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).
|
98
138
|
|
99
139
|
```yaml
|
100
|
-
page_size:
|
101
|
-
width:
|
102
|
-
height:
|
103
|
-
reg_marks:
|
104
|
-
margin:
|
105
|
-
radius:
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
contrast:
|
111
|
-
header:
|
112
|
-
|
113
|
-
top:
|
114
|
-
left:
|
115
|
-
width:
|
116
|
-
height:
|
117
|
-
size:
|
118
|
-
|
119
|
-
top: 15
|
120
|
-
left: 15
|
121
|
-
width: 160
|
122
|
-
height: 12
|
123
|
-
size: 12
|
124
|
-
code:
|
125
|
-
top: 35
|
126
|
-
left: 130
|
127
|
-
width: 57
|
128
|
-
height: 10
|
129
|
-
size: 14
|
130
|
-
signature:
|
131
|
-
top: 30
|
132
|
-
left: 15
|
133
|
-
width: 120
|
134
|
-
height: 15
|
135
|
-
size: 7
|
136
|
-
box: true # header element will be enclosed in a box
|
140
|
+
page_size: # all measurements in mm
|
141
|
+
width: 210 # width of the paper sheet
|
142
|
+
height: 297 # height of the paper sheet
|
143
|
+
reg_marks:
|
144
|
+
margin: 10 # distance from each page border to registration mark center
|
145
|
+
radius: 3 # registration mark radius
|
146
|
+
offset: 2 # distance between the search area and each page border (*)
|
147
|
+
crop: 20 # size of the registration mark search area (*)
|
148
|
+
dilate: 5 # size of a “dilate” filter to get rid of stray noise (0 to skip) (*)
|
149
|
+
blur: 2 # size of a gaussian blur filter to smooth overly pixelated registration marks (0 to skip) (*)
|
150
|
+
contrast: 20 # minimum contrast between registration mark circles and the surrounding white paper (*)
|
151
|
+
header:
|
152
|
+
title: # ‘title’ is a label of your choosing; you can add arbitrary header elements
|
153
|
+
top: 15 # margin relative to registration frame top side
|
154
|
+
left: 15 # margin relative to registration frame left side
|
155
|
+
width: 160 # text will be fitted to this width
|
156
|
+
height: 12 # text will be fitted to this height
|
157
|
+
size: 12 # font size
|
158
|
+
box: false # if true, header element will be enclosed in a frame
|
137
159
|
items:
|
138
|
-
threshold:
|
139
|
-
|
140
|
-
|
141
|
-
rows:
|
142
|
-
|
143
|
-
|
144
|
-
x_spacing:
|
145
|
-
y_spacing:
|
146
|
-
cell_width:
|
147
|
-
cell_height:
|
148
|
-
max_cells:
|
149
|
-
font_size:
|
150
|
-
number_width:
|
151
|
-
number_margin:
|
160
|
+
threshold: 0.75 # mark detection threshold (*)
|
161
|
+
columns: 4 # number of columns
|
162
|
+
column_width: 44 #
|
163
|
+
rows: 30 # number of items per column
|
164
|
+
left: 11 # response area margin, relative to reg frame
|
165
|
+
top: 55 # response area margin, relative to reg frame
|
166
|
+
x_spacing: 7 # horizontal distance between ajacent cell centers
|
167
|
+
y_spacing: 7 # vertical distance between ajacent cell centers
|
168
|
+
cell_width: 6 # width of each choice and calibration cell
|
169
|
+
cell_height: 5 # height of each choice and calibration cell
|
170
|
+
max_cells: 5 # the maximum number of choices per question
|
171
|
+
font_size: 9 # for the question number and choice letters
|
172
|
+
number_width: 8 # width of question number text box
|
173
|
+
number_margin: 2 # distance between right side of q num and left side of first choice cell
|
152
174
|
barcode:
|
153
|
-
bits:
|
154
|
-
left:
|
155
|
-
width:
|
156
|
-
height:
|
157
|
-
spacing:
|
175
|
+
bits: 38 # the maximum sheet identifier is 2 to the power or bits
|
176
|
+
left: 15 # distance between registration frame side and the first barcode bit
|
177
|
+
width: 3 # width of each barcode bit
|
178
|
+
height: 3 # height of each barcode bit from the registration frame bottom side
|
179
|
+
spacing: 4 # horizontal distance between adjacent barcode bit centers
|
180
|
+
```
|
181
|
+
|
182
|
+
A `layout.yml` file may be used on top of the above to always apply settings that you would consider your own defaults. A good example might be to override the built-in A4 paper size with Letter paper width and height. For example, if everthing in the built-in layout fits your needs except for paper size, the `layout.yml` file should contain just the following:
|
183
|
+
|
184
|
+
```yaml
|
185
|
+
page_size:
|
186
|
+
width: 215.9
|
187
|
+
height: 279.4
|
158
188
|
```
|
159
189
|
|
160
|
-
|
190
|
+
Finally, the `layout` argument into the `SheetPDF` constructor can be either a hash or a string indicating the path/name of a YAML file.
|
191
|
+
|
192
|
+
The specification of header elements is slightly different than all other settings, in that you are free to add any number of elements with arbitrary names, as long as each element is given the following properties: `top`, `left`, `width`, `height`, and `size` (see the built-in title element above). The `box` property is optional. As already noted, header elements can be used in the `content` only if their properties are defined in the `layout`.
|
193
|
+
|
194
|
+
In summary, the following sample code shows how to create a 2-page PDF document:
|
161
195
|
|
162
196
|
```ruby
|
163
|
-
|
197
|
+
content = [
|
198
|
+
{
|
199
|
+
barcode: 1001,
|
200
|
+
header: { name: 'John Doe UI354320', title: 'Final exam', code: '1001'}
|
201
|
+
},
|
202
|
+
{
|
203
|
+
barcode: 1002,
|
204
|
+
header: { name: 'Jane Roe UI354321', title: 'Final exam', code: '1002'}
|
205
|
+
}
|
206
|
+
]
|
207
|
+
|
208
|
+
layout = {
|
209
|
+
header: {
|
210
|
+
name: { top: 5, left: 15, width: 160, height: 7, size: 14 },
|
211
|
+
title: { top: 15, left: 15, width: 160, height: 12, size: 12 },
|
212
|
+
code: { top: 15, left: 155, width: 35, height: 10, size: 14 }
|
213
|
+
}
|
214
|
+
}
|
215
|
+
|
216
|
+
sheet = SheetPDF.new content, layout
|
164
217
|
s.save 'sheet.pdf'
|
165
218
|
system 'open sheet.pdf' # this works in OSX
|
166
219
|
```
|
167
220
|
|
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).
|
169
|
-
|
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
221
|
## Analyzing response sheets with `SheetOMR`
|
173
222
|
|
174
223
|
### Preparing a `SheetOMR` object
|
@@ -176,13 +225,13 @@ Importantly, if `content` is an array of hashes, Mork will produce one sheet for
|
|
176
225
|
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:
|
177
226
|
|
178
227
|
- **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)
|
179
|
-
- **
|
228
|
+
- **layout**: same as for the `SheetPDF` class
|
180
229
|
|
181
230
|
The following code shows how to create a SheetOMR based on a bitmap file named `image.jpg`:
|
182
231
|
|
183
232
|
```ruby
|
184
233
|
# instantiating the object
|
185
|
-
s = SheetOMR.new 'image.jpg', '
|
234
|
+
s = SheetOMR.new 'image.jpg', 'mylayout.yml'
|
186
235
|
```
|
187
236
|
|
188
237
|
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:
|
@@ -208,18 +257,18 @@ We are now ready to mark the sheet:
|
|
208
257
|
mc = s.marked_choices
|
209
258
|
```
|
210
259
|
|
211
|
-
Since the function `set_choices` returns true only if the sheet is properly registered,
|
260
|
+
Since the function `set_choices` returns true only if the sheet is properly registered, you can write something like the following (instead of calling `valid?`):
|
212
261
|
|
213
262
|
```ruby
|
214
|
-
s = SheetOMR.new 'image.jpg'
|
263
|
+
s = SheetOMR.new 'image.jpg'
|
215
264
|
if s.set_choices [5] * 50
|
216
|
-
|
265
|
+
puts s.marked_choices
|
217
266
|
else
|
218
267
|
puts "The sheet is not registered!"
|
219
268
|
end
|
220
269
|
```
|
221
270
|
|
222
|
-
If all goes well, the `marked` array will contain
|
271
|
+
If all goes well, the `marked` array will contain 50 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
272
|
|
224
273
|
Read the [API documentation](http://www.rubydoc.info/gems/mork) to learn about additional methods to extract marked responses.
|
225
274
|
|
@@ -247,21 +296,14 @@ Scoring can only be performed if the sheet gets properly registered, which in tu
|
|
247
296
|
|
248
297
|
### Improving sheet registration and marking
|
249
298
|
|
250
|
-
For Mork to be able to register the response sheet, the acquired image
|
299
|
+
For Mork to be able to register the response sheet, the acquired image must be of _sufficient_ resolution and contrast, and it should be _reasonably_ straight, with some white margin around the 4 registration circles.
|
251
300
|
|
252
301
|
Mork tries to be tolerant of variations in the above parameters, but you should experiment with your own layout, actual printout, and actual scans.
|
253
302
|
|
254
|
-
Check for object “validity” to make sure that registration succeeded:
|
255
|
-
|
256
|
-
```ruby
|
257
|
-
s = SheetOMR.new 'image.jpg', 'layout.yml'
|
258
|
-
s.valid?
|
259
|
-
```
|
260
|
-
|
261
303
|
When registration fails, it is possible to get some information by displaying the status and by applying a dedicated overlay on the original image:
|
262
304
|
|
263
305
|
```ruby
|
264
|
-
s = SheetOMR.new 'image.jpg', '
|
306
|
+
s = SheetOMR.new 'image.jpg', 'mylayout.yml'
|
265
307
|
unless s.valid?
|
266
308
|
s.save_registration 'unregistered.jpg'
|
267
309
|
end
|
data/lib/mork.rb
CHANGED
data/lib/mork/extensions.rb
CHANGED
@@ -1,19 +1,3 @@
|
|
1
|
-
# @private
|
2
|
-
class Array
|
3
|
-
def mean
|
4
|
-
@the_sample_mean ||= inject(:+)/length.to_f
|
5
|
-
end
|
6
|
-
|
7
|
-
def sample_variance
|
8
|
-
sum = inject(0){|accum, i| accum + (i-mean)**2 }
|
9
|
-
sum/(length - 1).to_f
|
10
|
-
end
|
11
|
-
|
12
|
-
def stdev
|
13
|
-
Math.sqrt sample_variance
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
1
|
# @private
|
18
2
|
class Fixnum
|
19
3
|
def mm
|
@@ -27,3 +11,29 @@ class Float
|
|
27
11
|
self * 2.83464566929134
|
28
12
|
end
|
29
13
|
end
|
14
|
+
|
15
|
+
module Mork
|
16
|
+
# @private
|
17
|
+
module Extensions
|
18
|
+
def symbolize(obj)
|
19
|
+
return obj.inject({}){|memo,(k,v)| memo[k.to_sym] = symbolize(v); memo} if obj.is_a? Hash
|
20
|
+
return obj.inject([]){|memo,v | memo << symbolize(v); memo} if obj.is_a? Array
|
21
|
+
return obj
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# # @private
|
27
|
+
# class Array
|
28
|
+
# def mean
|
29
|
+
# @the_sample_mean ||= inject(:+)/length.to_f
|
30
|
+
# end
|
31
|
+
# def sample_variance
|
32
|
+
# sum = inject(0){|accum, i| accum + (i-mean)**2 }
|
33
|
+
# sum/(length - 1).to_f
|
34
|
+
# end
|
35
|
+
# def stdev
|
36
|
+
# Math.sqrt sample_variance
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
|
data/lib/mork/grid.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'yaml'
|
2
1
|
require 'mork/grid_const'
|
3
2
|
|
4
3
|
module Mork
|
@@ -7,6 +6,7 @@ module Mork
|
|
7
6
|
# It knows nothing about the actual scanned image.
|
8
7
|
# All returned values are in the arbitrary units given in the configuration file
|
9
8
|
class Grid
|
9
|
+
include Extensions
|
10
10
|
# Calling Grid.new without arguments creates the default boilerplate Grid
|
11
11
|
def initialize(options=nil)
|
12
12
|
@params = default_grid
|
@@ -67,11 +67,11 @@ module Mork
|
|
67
67
|
|
68
68
|
# recursively turn hash keys into symbols. pasted from
|
69
69
|
# http://stackoverflow.com/questions/800122/best-way-to-convert-strings-to-symbols-in-hash
|
70
|
-
def symbolize(obj)
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
end
|
70
|
+
# def symbolize(obj)
|
71
|
+
# return obj.inject({}){|memo,(k,v)| memo[k.to_sym] = symbolize(v); memo} if obj.is_a? Hash
|
72
|
+
# return obj.inject([]){|memo,v | memo << symbolize(v); memo} if obj.is_a? Array
|
73
|
+
# return obj
|
74
|
+
# end
|
75
75
|
|
76
76
|
# cell_y(q)
|
77
77
|
#
|
data/lib/mork/grid_const.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module Mork
|
2
2
|
class Grid
|
3
|
+
# @private
|
3
4
|
# this is the default grid!
|
4
5
|
# default units are millimiters
|
5
6
|
def default_grid
|
@@ -26,7 +27,8 @@ module Mork
|
|
26
27
|
left: 15,
|
27
28
|
width: 160,
|
28
29
|
height: 12,
|
29
|
-
size: 12
|
30
|
+
size: 12,
|
31
|
+
box: false
|
30
32
|
}
|
31
33
|
},
|
32
34
|
# questions and answers
|
@@ -43,8 +45,8 @@ module Mork
|
|
43
45
|
cell_height: 5, # choice cell size
|
44
46
|
max_cells: 5, # the maximum number of choices per question
|
45
47
|
font_size: 9, # for the question number and choice letters
|
46
|
-
number_width: 8, #
|
47
|
-
number_margin: 2 #
|
48
|
+
number_width: 8, # width of question number text box
|
49
|
+
number_margin: 2 # distance between right side of q num and left side of first choice cell
|
48
50
|
},
|
49
51
|
# unique sheet ID as a binary barcode
|
50
52
|
barcode: {
|
data/lib/mork/mimage.rb
CHANGED
@@ -4,6 +4,8 @@ require 'mork/magicko'
|
|
4
4
|
module Mork
|
5
5
|
# @private
|
6
6
|
class Mimage
|
7
|
+
include Extensions
|
8
|
+
|
7
9
|
attr_reader :rm
|
8
10
|
attr_reader :choxq # choices per question
|
9
11
|
|
@@ -137,7 +139,8 @@ module Mork
|
|
137
139
|
end
|
138
140
|
|
139
141
|
def cal_cell_mean
|
140
|
-
@grom.calibration_cell_areas.collect { |c| reg_pixels.average c }
|
142
|
+
m = @grom.calibration_cell_areas.collect { |c| reg_pixels.average c }
|
143
|
+
m.inject(:+) / m.length.to_f
|
141
144
|
end
|
142
145
|
|
143
146
|
def darkest_cell_mean
|
data/lib/mork/sheet_omr.rb
CHANGED
@@ -45,11 +45,11 @@ module Mork
|
|
45
45
|
#
|
46
46
|
# @return [Boolean] True if the sheet is properly registered and ready to
|
47
47
|
# be marked; false otherwise.
|
48
|
-
def set_choices(
|
48
|
+
def set_choices(choices)
|
49
49
|
return false unless valid?
|
50
|
-
@mim.set_ch case
|
51
|
-
when Fixnum; @mim.choxq[0...
|
52
|
-
when Array;
|
50
|
+
@mim.set_ch case choices
|
51
|
+
when Fixnum; @mim.choxq[0...choices]
|
52
|
+
when Array; choices
|
53
53
|
else raise ArgumentError, 'Invalid choice set'
|
54
54
|
end
|
55
55
|
true
|
data/lib/mork/sheet_pdf.rb
CHANGED
@@ -6,17 +6,24 @@ module Mork
|
|
6
6
|
# Generating response sheets as PDF files.
|
7
7
|
# See the README file for usage
|
8
8
|
class SheetPDF < Prawn::Document
|
9
|
+
include Extensions
|
9
10
|
def initialize(content, layout=nil)
|
10
|
-
@
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
11
|
+
@content =
|
12
|
+
case content
|
13
|
+
when Array; content
|
14
|
+
when Hash; [content]
|
15
|
+
when String
|
16
|
+
raise IOError, "File '#{content}' not found" unless File.exists? content
|
17
|
+
symbolize YAML.load_file(content)
|
18
|
+
end
|
19
|
+
@grip =
|
20
|
+
case layout
|
21
|
+
when NilClass; GridPDF.new
|
22
|
+
when String, Hash; GridPDF.new layout
|
23
|
+
when Mork::GridPDF; layout
|
24
|
+
else raise ArgumentError, 'Invalid initialization parameter'
|
25
|
+
end
|
16
26
|
super my_page_params
|
17
|
-
# @content should be an array of hashes, one per page;
|
18
|
-
# convert to array if a single hash was passed
|
19
|
-
@content = content.class == Hash ? [content] : content
|
20
27
|
process
|
21
28
|
end
|
22
29
|
|
data/lib/mork/version.rb
CHANGED
data/spec/mork/grid_omr_spec.rb
CHANGED
data/spec/mork/grid_spec.rb
CHANGED
@@ -1,7 +1,16 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
include Mork::Extensions
|
2
3
|
|
3
4
|
module Mork
|
4
5
|
describe Grid do
|
6
|
+
let(:base) { symbolize YAML.load_file('spec/samples/base_layout.yml') }
|
7
|
+
|
8
|
+
describe 'hash vs yaml' do
|
9
|
+
it 'makes sure that the default grid and the base_layout.yml are equivalent' do
|
10
|
+
expect(base).to eq(Grid.new.default_grid)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
5
14
|
context 'init params' do
|
6
15
|
it 'does not work with an integer' do
|
7
16
|
expect {Grid.new 1}.to raise_error ArgumentError
|
@@ -9,97 +18,17 @@ module Mork
|
|
9
18
|
end
|
10
19
|
|
11
20
|
context 'default grid' do
|
12
|
-
before(:all) do
|
13
|
-
@grid = Grid.new 'spec/samples/layout.yml'
|
14
|
-
end
|
15
|
-
|
16
21
|
describe '#max_questions' do
|
17
22
|
it 'returns the maximum number of questions in a sheet' do
|
18
|
-
|
23
|
+
expect(Grid.new.max_questions).to eq base[:items][:columns]*base[:items][:rows]
|
19
24
|
end
|
20
25
|
end
|
21
26
|
|
22
27
|
describe '#barcode_bits' do
|
23
28
|
it 'returns the number of bits used to define the form barcode' do
|
24
|
-
|
29
|
+
expect(Grid.new.send(:barcode_bits)).to eq base[:barcode][:bits]
|
25
30
|
end
|
26
31
|
end
|
27
32
|
end
|
28
33
|
end
|
29
34
|
end
|
30
|
-
|
31
|
-
# describe "#question_area" do
|
32
|
-
# before(:each) do
|
33
|
-
# grid.reg_marks(@image)
|
34
|
-
# end
|
35
|
-
# it "returns a hash" do
|
36
|
-
# grid.question_area(1).should be_an_instance_of(Hash)
|
37
|
-
# end
|
38
|
-
#
|
39
|
-
# it "returns the location in pixels of the first question patch" do
|
40
|
-
# c = grid.question_area(1)
|
41
|
-
# c[:x].should be_within(4).of(90)
|
42
|
-
# c[:y].should be_within(4).of(388)
|
43
|
-
# end
|
44
|
-
# it "returns the location in pixels of the 40th question patch" do
|
45
|
-
# c = grid.question_area(40)
|
46
|
-
# c[:x].should be_within(4).of(90)
|
47
|
-
# c[:y].should be_within(4).of(3120)
|
48
|
-
# end
|
49
|
-
#
|
50
|
-
# it "returns the location in pixels of the 121th question patch" do
|
51
|
-
# c = grid.question_area(121)
|
52
|
-
# c[:x].should be_within(4).of(1887)
|
53
|
-
# c[:y].should be_within(4).of(388)
|
54
|
-
# end
|
55
|
-
#
|
56
|
-
# it "returns the location in pixels of the last question patch" do
|
57
|
-
# c = grid.question_area(160)
|
58
|
-
# c[:x].should be_within(4).of(1887)
|
59
|
-
# c[:y].should be_within(4).of(3120)
|
60
|
-
# end
|
61
|
-
# end
|
62
|
-
|
63
|
-
# describe '#ctrl_area_dark' do
|
64
|
-
# it 'returns the coordinates of the control cell used to set the darkened threshold' do
|
65
|
-
# @grid.ctrl_area_dark.should == {:x=>1479, :y=>329, :w=>51, :h=>41}
|
66
|
-
# end
|
67
|
-
# end
|
68
|
-
|
69
|
-
# describe '#ctrl_area_light' do
|
70
|
-
# it 'returns the coordinates of the control cell used to set the darkened threshold' do
|
71
|
-
# @grid.ctrl_area_light.should == {:x=>1538, :y=>329, :w=>51, :h=>41}
|
72
|
-
# end
|
73
|
-
# end
|
74
|
-
|
75
|
-
# describe "#cell_x" do
|
76
|
-
# context "for 1st-column questions" do
|
77
|
-
# it "returns the distance from the registration frame of the left edge of the 1st choice" do
|
78
|
-
# grid.send(:cell_x,0,0).should == 7.5
|
79
|
-
# end
|
80
|
-
#
|
81
|
-
# it "returns the distance from the registration frame of the left edge of the 2nd choice" do
|
82
|
-
# grid.send(:cell_x,0,1).should == 14.5
|
83
|
-
# end
|
84
|
-
# end
|
85
|
-
#
|
86
|
-
# context "for 4th-column questions" do
|
87
|
-
# it "returns the distance from the registration frame of the left edge of the 1st choice" do
|
88
|
-
# grid.send(:cell_x,120,0).should == 157.5
|
89
|
-
# end
|
90
|
-
#
|
91
|
-
# it "returns the distance from the registration frame of the left edge of the 2nd choice" do
|
92
|
-
# grid.send(:cell_x,120,1).should == 164.5
|
93
|
-
# end
|
94
|
-
# end
|
95
|
-
# end
|
96
|
-
#
|
97
|
-
# describe "#cell_y" do
|
98
|
-
# it "returns the distance from the registration frame of the top edge of the 1st row of cells" do
|
99
|
-
# grid.send(:cell_y,0).should == 33.5
|
100
|
-
# end
|
101
|
-
#
|
102
|
-
# it "returns the distance from the registration frame of the 40th row of cells" do
|
103
|
-
# grid.send(:cell_y,39).should == 267.5
|
104
|
-
# end
|
105
|
-
# end
|
data/spec/mork/sheet_pdf_spec.rb
CHANGED
@@ -7,39 +7,45 @@ module Mork
|
|
7
7
|
barcode: 183251937962,
|
8
8
|
choices: [5] * 120,
|
9
9
|
header: {
|
10
|
-
title: 'A serious, difficult test - 31 December 1999'
|
10
|
+
title: 'A serious, difficult test - 31 December 1999'
|
11
11
|
}
|
12
12
|
}
|
13
13
|
}
|
14
|
+
def sp(cnt: {title: 'Hello world'}, grip: nil)
|
15
|
+
SheetPDF.new cnt, grip
|
16
|
+
end
|
14
17
|
|
15
18
|
it 'assigns the grid to @grid' do
|
16
|
-
|
17
|
-
|
19
|
+
expect(sp.instance_variable_get('@grip')).to be_a GridPDF
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'uses a yaml file as content' do
|
23
|
+
s = 'spec/samples/content.yml'
|
24
|
+
c = sp(cnt: s).instance_variable_get('@content')[0][:choices]
|
25
|
+
expect(c).to eq [5,5,5,4,5,5,5]
|
18
26
|
end
|
19
27
|
|
20
28
|
it 'creates a grid by loading the specified file' do
|
21
|
-
s =
|
22
|
-
s.instance_variable_get('@grip').
|
29
|
+
s = 'spec/samples/layout.yml'
|
30
|
+
expect(sp(grip: s).instance_variable_get('@grip')).to be_a GridPDF
|
23
31
|
end
|
24
32
|
|
25
33
|
it 'raises an error with an invalid init parameter' do
|
26
|
-
|
34
|
+
expect { SheetPDF.new(content, 2) }.to raise_error ArgumentError
|
27
35
|
end
|
28
36
|
|
29
37
|
it 'raises an error if a header part is not described in the layout' do
|
30
|
-
|
31
|
-
|
32
|
-
}.
|
38
|
+
expect {
|
39
|
+
sp(cnt: {header: {dummy: 'yes I am'}})
|
40
|
+
}.to raise_error ArgumentError
|
33
41
|
end
|
34
42
|
|
35
43
|
it 'assigns an array to @content' do
|
36
|
-
|
37
|
-
s.instance_variable_get('@content').should be_an Array
|
44
|
+
expect(sp.instance_variable_get('@content')).to be_an Array
|
38
45
|
end
|
39
46
|
|
40
47
|
it 'assigns an array of hashes to @content' do
|
41
|
-
|
42
|
-
s.instance_variable_get('@content').first.should be_a Hash
|
48
|
+
expect(sp.instance_variable_get('@content').first).to be_a Hash
|
43
49
|
end
|
44
50
|
|
45
51
|
it 'creates a minimal PDF sheet' do
|
@@ -53,6 +59,17 @@ module Mork
|
|
53
59
|
end
|
54
60
|
|
55
61
|
it 'creates a PDF sheet with several boxed header elements' do
|
62
|
+
h = {
|
63
|
+
name: 'John Doe UI354320',
|
64
|
+
title: 'A serious, difficult test - 31 December 1999',
|
65
|
+
code: '201.48',
|
66
|
+
signature: 'Signature'
|
67
|
+
}
|
68
|
+
s = SheetPDF.new({header: h, barcode: 2384685871}, 'spec/samples/standard.yml')
|
69
|
+
s.save dest 'standard'
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'creates a PDF sheet with several header elements' do
|
56
73
|
h = {
|
57
74
|
name: lorem,
|
58
75
|
title: lorem,
|
@@ -75,8 +92,8 @@ module Mork
|
|
75
92
|
end
|
76
93
|
|
77
94
|
it 'creates a PDF sheet with 160 items' do
|
78
|
-
|
79
|
-
|
95
|
+
c = { header: {title: '160 items, tighter layout'}, choices: [5]*160}
|
96
|
+
sp(cnt: c, grip: 'spec/samples/grid160.yml').save dest 'i160'
|
80
97
|
end
|
81
98
|
|
82
99
|
it 'creates a PDF sheet with unequal choices per item' do
|
@@ -0,0 +1,40 @@
|
|
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 # size of the registration mark search area (*)
|
9
|
+
dilate: 5 # size of a “dilate” filter to get rid of stray noise (0 to skip) (*)
|
10
|
+
blur: 2 # size of a gaussian blur filter to smooth overly pixelated registration marks (0 to skip) (*)
|
11
|
+
contrast: 20 # minimum contrast between registration mark circles and the surrounding white paper (*)
|
12
|
+
header:
|
13
|
+
title: # ‘title’ is a label of your choosing; you can add arbitrary header elements
|
14
|
+
top: 15 # margin relative to registration frame top side
|
15
|
+
left: 15 # margin relative to registration frame left side
|
16
|
+
width: 160 # text will be fitted to this width
|
17
|
+
height: 12 # text will be fitted to this height
|
18
|
+
size: 12 # font size
|
19
|
+
box: false # if true, header element will be enclosed in a box
|
20
|
+
items:
|
21
|
+
threshold: 0.75 # mark detection threshold (*)
|
22
|
+
columns: 4 # number of columns
|
23
|
+
column_width: 44 #
|
24
|
+
rows: 30 # number of items per column
|
25
|
+
left: 11 # response area margin, relative to reg frame
|
26
|
+
top: 55 # response area margin, relative to reg frame
|
27
|
+
x_spacing: 7 # horizontal distance between ajacent cell centers
|
28
|
+
y_spacing: 7 # vertical distance between ajacent cell centers
|
29
|
+
cell_width: 6 # width of each choice and calibration cell
|
30
|
+
cell_height: 5 # height of each choice and calibration cell
|
31
|
+
max_cells: 5 # the maximum number of choices per question
|
32
|
+
font_size: 9 # for the question number and choice letters
|
33
|
+
number_width: 8 # width of question number text box
|
34
|
+
number_margin: 2 # distance between right side of q num and left side of first choice cell
|
35
|
+
barcode:
|
36
|
+
bits: 38 # the maximum sheet identifier is 2 to the power or bits
|
37
|
+
left: 15 # distance between registration frame side and the first barcode bit
|
38
|
+
width: 3 # width of each barcode bit
|
39
|
+
height: 3 # height of each barcode bit from the registration frame bottom side
|
40
|
+
spacing: 4 # horizontal distance between adjacent barcode bit centers
|
Binary file
|
@@ -0,0 +1,27 @@
|
|
1
|
+
header:
|
2
|
+
name: # ‘name’ is just a label; you can add arbitrary header elements
|
3
|
+
top: 5 # margin relative to registration frame top side
|
4
|
+
left: 15 # margin relative to registration frame left side
|
5
|
+
width: 160 # text will be fitted to this width
|
6
|
+
height: 7 # text will be fitted to this height
|
7
|
+
size: 14 # font size
|
8
|
+
title:
|
9
|
+
top: 15
|
10
|
+
left: 15
|
11
|
+
width: 160
|
12
|
+
height: 12
|
13
|
+
size: 12
|
14
|
+
code:
|
15
|
+
top: 30
|
16
|
+
left: 152
|
17
|
+
width: 35
|
18
|
+
height: 10
|
19
|
+
size: 14
|
20
|
+
align: right
|
21
|
+
signature:
|
22
|
+
top: 30
|
23
|
+
left: 15
|
24
|
+
width: 120
|
25
|
+
height: 15
|
26
|
+
size: 7
|
27
|
+
box: true
|
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.11.1
|
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-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: narray
|
@@ -197,7 +197,6 @@ files:
|
|
197
197
|
- mork.gemspec
|
198
198
|
- mork.sublime-project
|
199
199
|
- spec/mork/coord_spec.rb
|
200
|
-
- spec/mork/extensions_spec.rb
|
201
200
|
- spec/mork/grid_omr_spec.rb
|
202
201
|
- spec/mork/grid_spec.rb
|
203
202
|
- spec/mork/magicko_spec.rb
|
@@ -216,7 +215,9 @@ files:
|
|
216
215
|
- spec/samples/angolo.jpg
|
217
216
|
- spec/samples/angolo2.jpg
|
218
217
|
- spec/samples/angolo3.jpg
|
218
|
+
- spec/samples/base_layout.yml
|
219
219
|
- spec/samples/boxy.yml
|
220
|
+
- spec/samples/content.yml
|
220
221
|
- spec/samples/grid.yml
|
221
222
|
- spec/samples/grid160.yml
|
222
223
|
- spec/samples/grid_omr_layout.yml
|
@@ -226,15 +227,6 @@ files:
|
|
226
227
|
- spec/samples/jdoe/JohnDoe3.jpeg
|
227
228
|
- spec/samples/jdoe/layout.yml
|
228
229
|
- spec/samples/layout.yml
|
229
|
-
- spec/samples/lucrezia/border1.pdf
|
230
|
-
- spec/samples/lucrezia/border2.pdf
|
231
|
-
- spec/samples/lucrezia/bw1.pdf
|
232
|
-
- spec/samples/lucrezia/bw2.pdf
|
233
|
-
- spec/samples/lucrezia/gray1.pdf
|
234
|
-
- spec/samples/lucrezia/gray2.pdf
|
235
|
-
- spec/samples/marisol/marisol1.jpg
|
236
|
-
- spec/samples/marisol/marisol2.jpg
|
237
|
-
- spec/samples/marisol/marisol3.jpg
|
238
230
|
- spec/samples/reg_mark.jpg
|
239
231
|
- spec/samples/rm00.jpeg
|
240
232
|
- spec/samples/rm01.jpeg
|
@@ -242,6 +234,8 @@ files:
|
|
242
234
|
- spec/samples/rm03.jpeg
|
243
235
|
- spec/samples/rm04.jpeg
|
244
236
|
- spec/samples/rm05.jpeg
|
237
|
+
- spec/samples/standard.png
|
238
|
+
- spec/samples/standard.yml
|
245
239
|
- spec/samples/syst/barr0.jpg
|
246
240
|
- spec/samples/syst/barr1.jpg
|
247
241
|
- spec/samples/syst/barr2.jpg
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|