mork 0.12.0 → 0.13.2
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 +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +3 -0
- data/README.md +8 -10
- data/Rakefile +4 -1
- data/lib/mork/extensions.rb +1 -1
- data/lib/mork/grid.rb +1 -1
- data/lib/mork/grid_const.rb +8 -8
- data/lib/mork/grid_omr.rb +1 -1
- data/lib/mork/grid_pdf.rb +2 -2
- data/lib/mork/magicko.rb +67 -21
- data/lib/mork/mimage.rb +15 -3
- data/lib/mork/sheet_omr.rb +29 -26
- data/lib/mork/sheet_pdf.rb +12 -8
- data/lib/mork/version.rb +1 -1
- data/mork.gemspec +11 -12
- data/mork.sublime-workspace +2566 -0
- data/spec/mork/grid_omr_spec.rb +3 -3
- data/spec/mork/magicko_spec.rb +0 -4
- data/spec/mork/sheet_omr_spec.rb +142 -103
- data/spec/mork/sheet_pdf_spec.rb +1 -1
- data/spec/samples/corrupt.pdf +0 -0
- data/spec/samples/info.yml +3 -1
- data/spec/samples/raffa-ige.jpg +0 -0
- metadata +28 -32
- data/spec/out/barcode/.gitignore +0 -4
- data/spec/out/highlight/.gitignore +0 -4
- data/spec/out/mark/.gitignore +0 -4
- data/spec/out/outline/.gitignore +0 -4
- data/spec/out/registration/.gitignore +0 -4
- data/spec/out/text/.gitignore +0 -4
- data/test_reg.m +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2d3d6ed3d87702211f4671ed0897c00b47104502865c71c7880583b81a872db3
|
4
|
+
data.tar.gz: 8106f2dfc61c23142bc215421686d3033960cab7831be8906051bf33c35a5894
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab2fb5a733b621dc3ca6c1097644190eaf0b613acb91aa79e7b0910f684a68c0b88170e904ee4516c90548186184ee748af3001ecb5feb96850af0b97b4fc184
|
7
|
+
data.tar.gz: 6121d3469a8128c696db1c9c4be71a6b99608040b8e1cc9f25216234b60f3f1a687dcd324ab964baee6989c2b009ba89cbbf480edd849eb5e6bf69b84363f54b
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -15,7 +15,7 @@ Mork is a low-level library, and very much work in progress. It is not, and will
|
|
15
15
|
|
16
16
|
- the PDF files generated by Mork are intended to be printed on regular printer paper
|
17
17
|
- the entire response sheet must fit into a single page
|
18
|
-
- after
|
18
|
+
- after marking the responses with a blue or black pen, a filled-out form should be acquired as a JPEG, PNG, or PDF image by a normal optical scanner or camera (i.e., no specialized equipment is necessary)
|
19
19
|
- independent of how the sheet is printed and the image is acquired, all internal processing is done on a grayscale version of the bitmap
|
20
20
|
- the response sheet always contains the following items:
|
21
21
|
- registration marks at each page corner
|
@@ -29,15 +29,13 @@ 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 a “command not found”-like error, you will need to install ImageMagick.
|
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
|
-
In
|
34
|
+
In macOS, `brew` is an excellent package manager:
|
35
35
|
|
36
36
|
brew install imagemagick
|
37
37
|
|
38
|
-
|
39
|
-
|
40
|
-
sudo apt-get install imagemagick
|
38
|
+
Please visit [ImageMagick’s home page](http://www.imagemagick.org/script/index.php) for instructions on how to install the software on other platforms.
|
41
39
|
|
42
40
|
To create a small ruby project that uses Mork, `cd` into a directory of choice, then execute the following shell commands:
|
43
41
|
|
@@ -215,7 +213,7 @@ layout = {
|
|
215
213
|
|
216
214
|
sheet = SheetPDF.new content, layout
|
217
215
|
s.save 'sheet.pdf'
|
218
|
-
system 'open sheet.pdf' # this works in
|
216
|
+
system 'open sheet.pdf' # this works in macOS
|
219
217
|
```
|
220
218
|
|
221
219
|
## Analyzing response sheets with `SheetOMR`
|
@@ -279,7 +277,7 @@ It is also possible to show the scoring graphically by applying an overlay on to
|
|
279
277
|
```ruby
|
280
278
|
s.overlay :check, :marked
|
281
279
|
s.save 'marked_choices.jpg'
|
282
|
-
system 'open marked_choices.jpg' # this works in
|
280
|
+
system 'open marked_choices.jpg' # this works in macOS
|
283
281
|
```
|
284
282
|
|
285
283
|
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:
|
@@ -289,7 +287,7 @@ correct = [[3], [0], [2], [1], [2]] # and so on...
|
|
289
287
|
s.overlay :outline, correct
|
290
288
|
s.overlay :check, :marked
|
291
289
|
s.save 'marked_choices_and_outlines.jpg'
|
292
|
-
system 'open marked_choices_and_outlines.jpg' # this works in
|
290
|
+
system 'open marked_choices_and_outlines.jpg' # this works in macOS
|
293
291
|
```
|
294
292
|
|
295
293
|
Scoring can only be performed if the sheet gets properly registered, which in turn depends on the quality of the scanned image.
|
@@ -312,7 +310,7 @@ end
|
|
312
310
|
Experiment with the following layout settings to improve sheet registration:
|
313
311
|
|
314
312
|
```yaml
|
315
|
-
reg_marks:
|
313
|
+
reg_marks:
|
316
314
|
radius: 3 # a bigger registration mark can sometimes help
|
317
315
|
crop: 20 # make sure the registration mark lies comfortably
|
318
316
|
# inside the crop area
|
data/Rakefile
CHANGED
data/lib/mork/extensions.rb
CHANGED
data/lib/mork/grid.rb
CHANGED
@@ -21,7 +21,7 @@ module Mork
|
|
21
21
|
when String
|
22
22
|
@params.deeper_merge! symbolize YAML.load_file(options)
|
23
23
|
else
|
24
|
-
|
24
|
+
fail ArgumentError, "Invalid parameter in the Grid constructor: #{options.class.inspect}"
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
data/lib/mork/grid_const.rb
CHANGED
@@ -58,14 +58,14 @@ module Mork
|
|
58
58
|
},
|
59
59
|
# student's unique id
|
60
60
|
uid: {
|
61
|
-
digits:
|
62
|
-
left:
|
63
|
-
top:
|
64
|
-
width:
|
65
|
-
height:
|
66
|
-
cell_width:
|
67
|
-
cell_height:
|
68
|
-
box:
|
61
|
+
digits: 6,
|
62
|
+
left: 150,
|
63
|
+
top: 30,
|
64
|
+
width: 50,
|
65
|
+
height: 40,
|
66
|
+
cell_width: 4,
|
67
|
+
cell_height: 3,
|
68
|
+
box: true
|
69
69
|
}
|
70
70
|
}
|
71
71
|
end
|
data/lib/mork/grid_omr.rb
CHANGED
data/lib/mork/grid_pdf.rb
CHANGED
@@ -29,7 +29,7 @@ module Mork
|
|
29
29
|
|
30
30
|
def barcode_xy_for(code)
|
31
31
|
black = barcode_bits.times.reject { |x| (code>>x)[0]==0 }
|
32
|
-
black.
|
32
|
+
black.map { |x| barcode_xy x+1 }
|
33
33
|
end
|
34
34
|
|
35
35
|
def ink_black_xy
|
@@ -37,7 +37,7 @@ module Mork
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def calibration_cells_xy
|
40
|
-
rows.times.
|
40
|
+
rows.times.map do |q|
|
41
41
|
[(reg_frame_width-cell_spacing).mm, item_y(q).mm]
|
42
42
|
end
|
43
43
|
end
|
data/lib/mork/magicko.rb
CHANGED
@@ -1,21 +1,46 @@
|
|
1
1
|
require 'mini_magick'
|
2
|
+
require 'open3'
|
2
3
|
|
3
4
|
module Mork
|
4
5
|
# @private
|
5
|
-
# Magicko: image management, done in two ways: 1) direct system calls to
|
6
|
+
# Magicko: low-level image management, done in two ways: 1) direct system calls to
|
6
7
|
# imagemagick tools; 2) via the MiniMagick gem
|
7
8
|
class Magicko
|
9
|
+
attr_reader :width
|
10
|
+
attr_reader :height
|
11
|
+
|
8
12
|
def initialize(path)
|
9
13
|
@path = path
|
10
14
|
@cmd = []
|
15
|
+
# a density is required for processing PDF or other vector-based images;
|
16
|
+
# a default of 150 dpi seems sensible. It should not affect bitmaps.
|
17
|
+
density = 150
|
18
|
+
# inspect the source file
|
19
|
+
s1, s2, s3 = Open3.capture3 "identify -density #{density} -format '%w %h %m' #{path}"
|
20
|
+
if s3.success?
|
21
|
+
# parse the identify command output
|
22
|
+
w, h, @type = s1.split(' ')
|
23
|
+
@width = w.to_i
|
24
|
+
@height = h.to_i
|
25
|
+
if @type.downcase == 'pdf'
|
26
|
+
# remember density for later use
|
27
|
+
@density = density
|
28
|
+
end
|
29
|
+
else
|
30
|
+
# Inspect the stderr and raise appropriate errors
|
31
|
+
case s2
|
32
|
+
when /No such file/
|
33
|
+
fail Errno::ENOENT
|
34
|
+
when /The file has been damaged/
|
35
|
+
fail IOError, 'Invalid image. File may have been damaged'
|
36
|
+
else
|
37
|
+
fail IOError, 'Unknown problem with image file'
|
38
|
+
end
|
39
|
+
end
|
11
40
|
end
|
12
41
|
|
13
|
-
def
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
def height
|
18
|
-
img_size[1]
|
42
|
+
def valid?
|
43
|
+
@valid
|
19
44
|
end
|
20
45
|
|
21
46
|
# registered_bytes returns an array of the same size as the original image,
|
@@ -26,7 +51,9 @@ module Mork
|
|
26
51
|
read_bytes "-distort Perspective '#{pps pp}'"
|
27
52
|
end
|
28
53
|
|
29
|
-
#
|
54
|
+
# Reading from the image file the bytes from one of the four corner
|
55
|
+
# squares encompassing each registration mark; the blur and dilation
|
56
|
+
# manipulations may prevent registration misalignments due to stray dark pixels
|
30
57
|
def rm_patch(c, blr=0, dlt=0)
|
31
58
|
b = blr==0 ? '' : " -blur #{blr*3}x#{blr}"
|
32
59
|
d = dlt==0 ? '' : " -morphology Dilate Octagon:#{dlt}"
|
@@ -37,10 +64,6 @@ module Mork
|
|
37
64
|
# Constructing MiniMagick commands
|
38
65
|
##################################
|
39
66
|
|
40
|
-
def write_registration(fname)
|
41
|
-
|
42
|
-
end
|
43
|
-
|
44
67
|
def highlight(coords, rounded)
|
45
68
|
@cmd << [:stroke, 'none']
|
46
69
|
@cmd << [:fill, 'rgba(255, 255, 0, 0.3)']
|
@@ -49,7 +72,7 @@ module Mork
|
|
49
72
|
|
50
73
|
def outline(coords, rounded)
|
51
74
|
@cmd << [:stroke, 'green']
|
52
|
-
@cmd << [:strokewidth, '
|
75
|
+
@cmd << [:strokewidth, '3']
|
53
76
|
@cmd << [:fill, 'none']
|
54
77
|
coords.each { |c| @cmd << [:draw, shape(c, rounded)] }
|
55
78
|
end
|
@@ -104,6 +127,7 @@ module Mork
|
|
104
127
|
|
105
128
|
def save(fname, reg)
|
106
129
|
MiniMagick::Tool::Convert.new(whiny: false) do |img|
|
130
|
+
img << '-density' << @density if @density
|
107
131
|
img << @path
|
108
132
|
img.distort(:perspective, pps(reg)) if reg
|
109
133
|
@cmd.each { |cmd| img.send(*cmd) }
|
@@ -120,7 +144,8 @@ module Mork
|
|
120
144
|
# calling imagemagick and capturing the converted image
|
121
145
|
# into an array of bytes
|
122
146
|
def read_bytes(params=nil)
|
123
|
-
|
147
|
+
d = @density ? "-density #{@density}" : nil
|
148
|
+
s = "|convert -depth 8 #{d} #{@path} #{params} gray:-"
|
124
149
|
IO.read(s).unpack 'C*'
|
125
150
|
end
|
126
151
|
|
@@ -135,12 +160,33 @@ module Mork
|
|
135
160
|
pp[:bl][:x], pp[:bl][:y], 0, height
|
136
161
|
].join ' '
|
137
162
|
end
|
138
|
-
|
139
|
-
def img_size
|
140
|
-
@img_size ||= begin
|
141
|
-
s = "|identify -format '%w,%h' #{@path}"
|
142
|
-
IO.read(s).split(',').map(&:to_i)
|
143
|
-
end
|
144
|
-
end
|
145
163
|
end
|
146
164
|
end
|
165
|
+
|
166
|
+
# @pdf = File.extname(path).strip.downcase[1..-1] == 'pdf'
|
167
|
+
# @pdf_den = 150 # dpi
|
168
|
+
# get_info_and_test_sanity
|
169
|
+
|
170
|
+
# def width
|
171
|
+
# img_size[0]
|
172
|
+
# end
|
173
|
+
|
174
|
+
# def height
|
175
|
+
# img_size[1]
|
176
|
+
# end
|
177
|
+
# if s1.downcase=='pdf'
|
178
|
+
# @density = 150
|
179
|
+
# @den_str = "-density #{@density}"
|
180
|
+
# end
|
181
|
+
|
182
|
+
# def img_size
|
183
|
+
# @img_size ||= begin
|
184
|
+
# s = "|identify -format '%w,%h' #{@density} #{@path}"
|
185
|
+
# IO.read(s).split(',').map(&:to_i)
|
186
|
+
# end
|
187
|
+
# end
|
188
|
+
|
189
|
+
# def parse_from_stdout(s1)
|
190
|
+
# w, h, t = s1.split ','
|
191
|
+
# return w.to_i, h.to_i, t
|
192
|
+
# end
|
data/lib/mork/mimage.rb
CHANGED
@@ -8,11 +8,12 @@ module Mork
|
|
8
8
|
|
9
9
|
attr_reader :rm
|
10
10
|
attr_reader :choxq # choices per question
|
11
|
+
attr_reader :status
|
11
12
|
|
12
13
|
def initialize(path, grom)
|
13
14
|
@mack = Magicko.new path
|
15
|
+
|
14
16
|
@grom = grom.set_page_size @mack.width, @mack.height
|
15
|
-
# @choxq = [grom.max_choices_per_question] * grom.max_questions
|
16
17
|
@choxq = [(0...@grom.max_choices_per_question).to_a] * grom.max_questions
|
17
18
|
@rm = {} # registration mark centers
|
18
19
|
@valid = register
|
@@ -22,6 +23,10 @@ module Mork
|
|
22
23
|
@valid
|
23
24
|
end
|
24
25
|
|
26
|
+
def low_contrast?
|
27
|
+
@rm.any? { |k,v| v < @grom.reg_min_contrast }
|
28
|
+
end
|
29
|
+
|
25
30
|
def status
|
26
31
|
{
|
27
32
|
tl: @rm[:tl][:status],
|
@@ -78,7 +83,7 @@ module Mork
|
|
78
83
|
when Array
|
79
84
|
choice_cell_areas where
|
80
85
|
else
|
81
|
-
|
86
|
+
fail ArgumentError, 'Invalid overlay argument “where”'
|
82
87
|
end
|
83
88
|
round = where != :barcode
|
84
89
|
@mack.send what, areas, round
|
@@ -109,6 +114,12 @@ module Mork
|
|
109
114
|
end
|
110
115
|
|
111
116
|
def choice_cell_areas(cells)
|
117
|
+
if cells.length > @grom.max_questions
|
118
|
+
fail ArgumentError, 'Maximum number of responses exceeded'
|
119
|
+
end
|
120
|
+
if cells.any? { |q| q.any? { |c| c >= @grom.max_choices_per_question } }
|
121
|
+
fail ArgumentError, 'Maximum number of choices exceeded'
|
122
|
+
end
|
112
123
|
itemator(cells) { |q,c| @grom.choice_cell_area q, c }.flatten
|
113
124
|
end
|
114
125
|
|
@@ -124,7 +135,7 @@ module Mork
|
|
124
135
|
end
|
125
136
|
|
126
137
|
def cal_cell_mean
|
127
|
-
m = @grom.calibration_cell_areas.
|
138
|
+
m = @grom.calibration_cell_areas.map { |c| reg_pixels.average c }
|
128
139
|
m.inject(:+) / m.length.to_f
|
129
140
|
end
|
130
141
|
|
@@ -156,6 +167,7 @@ module Mork
|
|
156
167
|
def rm_centroid_on(corner)
|
157
168
|
c = @grom.rm_crop_area(corner)
|
158
169
|
p = @mack.rm_patch(c, @grom.rm_blur, @grom.rm_dilate)
|
170
|
+
# byebug
|
159
171
|
n = NPatch.new(p, c.w, c.h)
|
160
172
|
cx, cy, sd = n.centroid
|
161
173
|
st = (cx < 2) or (cy < 2) or (cy > c.h-2) or (cx > c.w-2)
|
data/lib/mork/sheet_omr.rb
CHANGED
@@ -18,7 +18,6 @@ module Mork
|
|
18
18
|
# containing the parameters. See the README file for a full listing
|
19
19
|
# of the available parameters.
|
20
20
|
def initialize(path, layout=nil)
|
21
|
-
raise IOError, "File '#{path}' not found" unless File.exists? path
|
22
21
|
grom = GridOMR.new layout
|
23
22
|
@mim = Mimage.new path, grom
|
24
23
|
end
|
@@ -30,28 +29,8 @@ module Mork
|
|
30
29
|
@mim.valid?
|
31
30
|
end
|
32
31
|
|
33
|
-
|
34
|
-
|
35
|
-
# evaluated.
|
36
|
-
#
|
37
|
-
# @param choices [Fixnum, Array] the questions/choices we want subsequent
|
38
|
-
# scoring/overlaying to apply to. Normally, `choices` should be an array
|
39
|
-
# of integers, with each element indicating the number of available
|
40
|
-
# choices for the corresponding question (i.e. `choices.length` is the
|
41
|
-
# number of questions). As a shortcut, `choices` can also be a single
|
42
|
-
# integer value, indicating the number of questions; in such case, the
|
43
|
-
# maximum number of choices allowed by the layout will be considered.
|
44
|
-
#
|
45
|
-
# @return [Boolean] True if the sheet is properly registered and ready to
|
46
|
-
# be marked; false otherwise.
|
47
|
-
def set_choices(choices)
|
48
|
-
return false unless valid?
|
49
|
-
@mim.set_ch case choices
|
50
|
-
when Fixnum; @mim.choxq[0...choices]
|
51
|
-
when Array; choices
|
52
|
-
else raise ArgumentError, 'Invalid choice set'
|
53
|
-
end
|
54
|
-
true
|
32
|
+
def low_contrast?
|
33
|
+
@mim.low_contrast?
|
55
34
|
end
|
56
35
|
|
57
36
|
# Registration status for each of the four corners
|
@@ -65,7 +44,7 @@ module Mork
|
|
65
44
|
|
66
45
|
# Sheet barcode as an integer
|
67
46
|
#
|
68
|
-
# @return [
|
47
|
+
# @return [Integer]
|
69
48
|
def barcode
|
70
49
|
return if not_registered
|
71
50
|
barcode_string.to_i(2)
|
@@ -82,10 +61,34 @@ module Mork
|
|
82
61
|
end.join.reverse
|
83
62
|
end
|
84
63
|
|
64
|
+
# Setting the choices/questions to analyze. If this function is not called,
|
65
|
+
# the maximum number of choices/questions allowed by the layout will be
|
66
|
+
# evaluated.
|
67
|
+
#
|
68
|
+
# @param choices [Integer, Array] the questions/choices we want subsequent
|
69
|
+
# scoring/overlaying to apply to. Normally, `choices` should be an array
|
70
|
+
# of integers, with each element indicating the number of available
|
71
|
+
# choices for the corresponding question (i.e. `choices.length` is the
|
72
|
+
# number of questions). As a shortcut, `choices` can also be a single
|
73
|
+
# integer value, indicating the number of questions; in such case, the
|
74
|
+
# maximum number of choices allowed by the layout will be considered.
|
75
|
+
#
|
76
|
+
# @return [Boolean] True if the sheet is properly registered and ready to
|
77
|
+
# be marked; false otherwise.
|
78
|
+
def set_choices(choices)
|
79
|
+
return false unless valid?
|
80
|
+
@mim.set_ch case choices
|
81
|
+
when Integer; @mim.choxq[0...choices]
|
82
|
+
when Array; choices
|
83
|
+
else fail ArgumentError, 'Invalid choice set'
|
84
|
+
end
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
85
88
|
# True if the specified question/choice cell has been marked
|
86
89
|
#
|
87
|
-
# @param question [
|
88
|
-
# @param choice [
|
90
|
+
# @param question [Integer] the question number, zero-based
|
91
|
+
# @param choice [Integer] the choice number, zero-based
|
89
92
|
# @return [Boolean]
|
90
93
|
def marked?(question, choice)
|
91
94
|
return if not_registered
|