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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: b30b33a147b248a73592bcea2dd9ff4d7952a4fb
4
- data.tar.gz: e85ca2b34c0b579ec8bf7a3885331ac9ef74c62b
2
+ SHA256:
3
+ metadata.gz: 2d3d6ed3d87702211f4671ed0897c00b47104502865c71c7880583b81a872db3
4
+ data.tar.gz: 8106f2dfc61c23142bc215421686d3033960cab7831be8906051bf33c35a5894
5
5
  SHA512:
6
- metadata.gz: 0649c313f2db3af8d358c03787c0c773c4962cd40ea245758b1bfcb40bec331568c05523371410e81dc4e845ab370d42028331bb685241538856c4875c213e57
7
- data.tar.gz: 283fe8d57acd7451bca93920e1049218c1b6574c2944e5c8711dd43a2dbc25a096d802bf2929bcd91719c86efbfb44fe603043e1af60ac33a6375e9be54c2efa
6
+ metadata.gz: ab2fb5a733b621dc3ca6c1097644190eaf0b613acb91aa79e7b0910f684a68c0b88170e904ee4516c90548186184ee748af3001ecb5feb96850af0b97b4fc184
7
+ data.tar.gz: 6121d3469a8128c696db1c9c4be71a6b99608040b8e1cc9f25216234b60f3f1a687dcd324ab964baee6989c2b009ba89cbbf480edd849eb5e6bf69b84363f54b
data/.gitignore CHANGED
@@ -11,3 +11,4 @@ tmp/*
11
11
  .rspec
12
12
  .byebug_history
13
13
  doc
14
+ spec/out
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.5.1
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 collecting the responses, 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)
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 OS X, `brew` is an excellent package manager:
34
+ In macOS, `brew` is an excellent package manager:
35
35
 
36
36
  brew install imagemagick
37
37
 
38
- In a Debian-based Linux distro (e.g., Ubuntu) you would do:
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 OSX
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 OSX
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 OSX
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
@@ -1 +1,4 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ task default: :spec
4
+ RSpec::Core::RakeTask.new
@@ -1,5 +1,5 @@
1
1
  # @private
2
- class Fixnum
2
+ class Integer
3
3
  def mm
4
4
  self * 2.83464566929134
5
5
  end
@@ -21,7 +21,7 @@ module Mork
21
21
  when String
22
22
  @params.deeper_merge! symbolize YAML.load_file(options)
23
23
  else
24
- raise ArgumentError, "Invalid parameter in the Grid constructor: #{options.class.inspect}"
24
+ fail ArgumentError, "Invalid parameter in the Grid constructor: #{options.class.inspect}"
25
25
  end
26
26
  end
27
27
 
@@ -58,14 +58,14 @@ module Mork
58
58
  },
59
59
  # student's unique id
60
60
  uid: {
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
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
@@ -41,7 +41,7 @@ module Mork
41
41
  end
42
42
 
43
43
  def calibration_cell_areas
44
- rows.times.collect do |q|
44
+ rows.times.map do |q|
45
45
  coord cal_cell_x, cell_y(q), cell_width, cell_height
46
46
  end
47
47
  end
@@ -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.collect { |x| barcode_xy x+1 }
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.collect do |q|
40
+ rows.times.map do |q|
41
41
  [(reg_frame_width-cell_spacing).mm, item_y(q).mm]
42
42
  end
43
43
  end
@@ -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 width
14
- img_size[0]
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
- # def rm_patch(coord, blur_factor, dilate_factor)
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, '2']
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
- s = "|convert #{@path} #{params} gray:-"
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
@@ -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
- raise ArgumentError, 'Invalid overlay argument “where”'
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.collect { |c| reg_pixels.average c }
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)
@@ -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
- # Setting the choices/questions to analyze. If this function is not called,
34
- # the maximum number of choices/questions allowed by the layout will be
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 [Fixnum]
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 [Fixnum] the question number, zero-based
88
- # @param choice [Fixnum] the choice number, zero-based
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