mork 0.12.0 → 0.13.2

Sign up to get free protection for your applications and to get access to all the features.
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