mork 0.0.9 → 0.0.10

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.
@@ -4,24 +4,24 @@ module Mork
4
4
  # default units are millimiters
5
5
  page_size: {
6
6
  # this is A4
7
- width: 210,
8
- height: 297
7
+ width: 210,
8
+ height: 297
9
9
  }, # page end
10
10
  regmarks: {
11
- margin: 10,
12
- radius: 2.5,
13
- search: 10,
14
- offset: 2
11
+ margin: 10,
12
+ radius: 2.5,
13
+ search: 10,
14
+ offset: 2
15
15
  }, # regmarks end
16
16
  header: {
17
17
  name: {
18
18
  top: 5,
19
- left: 7.5,
20
- width: 170,
21
- size: 14,
19
+ left: 7.5,
20
+ width: 170,
21
+ size: 14,
22
22
  },
23
23
  title: {
24
- top: 15,
24
+ top: 15,
25
25
  left: 7.5,
26
26
  width: 180,
27
27
  size: 12
@@ -34,7 +34,7 @@ module Mork
34
34
  },
35
35
  signature: {
36
36
  top: 30,
37
- left: 7.5,
37
+ left: 7.5,
38
38
  width: 120,
39
39
  height: 15,
40
40
  size: 7,
@@ -43,43 +43,41 @@ module Mork
43
43
  }, # header end
44
44
  items: {
45
45
  columns: 4,
46
- column_width: 49,
46
+ column_width: 44,
47
47
  rows: 30,
48
48
  # from the top-left registration mark
49
49
  # to the center of the first choice cell
50
50
  first_x: 10.5,
51
51
  first_y: 55.5,
52
52
  # between choices
53
- x_spacing: 7.0,
53
+ x_spacing: 7,
54
54
  # between rows
55
- y_spacing: 7.0,
55
+ y_spacing: 7,
56
56
  # darkened area
57
- cell_width: 6.0,
58
- cell_height: 5.0,
57
+ cell_width: 6,
58
+ cell_height: 5,
59
59
  # the maximum number of choices per question
60
- max_cells: 5,
61
- # font size for the question number
62
- number_size: 10,
60
+ max_cells: 5,
61
+ # font size for the question number and choice letters
62
+ font_size: 9,
63
63
  # distance between right side of q num and left side of first choice cell
64
- number_width: 8,
64
+ number_width: 8,
65
65
  # width of question number text box
66
- number_margin: 2,
67
- # font size for the choice letter
68
- letter_size: 8
66
+ number_margin: 2,
69
67
  }, # items end
70
- code: {
71
- bits: 40,
72
- left: 15,
73
- width: 3.0,
74
- height: 2.5,
75
- spacing: 4
76
- }, # code end
68
+ barcode: {
69
+ bits: 40,
70
+ left: 15,
71
+ width: 3,
72
+ height: 2.5,
73
+ spacing: 4
74
+ }, # barcode end
77
75
  control: {
78
- top: 40,
79
- left: 123,
80
- width: 50,
81
- size: 9,
82
- margin: 2.5
76
+ top: 40,
77
+ left: 123,
78
+ width: 50,
79
+ size: 9,
80
+ margin: 2.5
83
81
  } # control end
84
82
  }
85
83
  end
@@ -0,0 +1,94 @@
1
+ require 'mork/grid'
2
+
3
+ module Mork
4
+ class GridOMR < Grid
5
+ def initialize(page_width, page_height, options=nil)
6
+ super options
7
+ @px = page_width.to_f
8
+ @py = page_height.to_f
9
+ end
10
+
11
+ def barcode_bit_areas(code = 2**barcode_bits-1)
12
+ barcode_bits.times.collect { |b| barcode_bit_area b }
13
+ end
14
+
15
+ # ====================================================
16
+ # = Returning {x, y, w, h} hashes for area locations =
17
+ # ====================================================
18
+ def choice_cell_area(q, c)
19
+ {
20
+ x: (cx * cell_x(q,c)).round,
21
+ y: (cy * cell_y(q) ).round,
22
+ w: (cx * cell_width ).round,
23
+ h: (cy * cell_height).round
24
+ }
25
+ end
26
+
27
+ def calibration_cell_areas
28
+ rows.times.collect do |q|
29
+ {
30
+ x: (cx * cal_cell_x ).round,
31
+ y: (cy * cell_y(q) ).round,
32
+ w: (cx * cell_width ).round,
33
+ h: (cy * cell_height).round
34
+ }
35
+ end
36
+ end
37
+
38
+ def barcode_bit_area(bit)
39
+ {
40
+ x: (cx * barcode_bit_x(bit)).round,
41
+ y: (cy * barcode_y ).round,
42
+ w: (cx * barcode_width ).round,
43
+ h: (cy * barcode_height ).round
44
+ }
45
+ end
46
+
47
+ # the 4 values needed to locate a single registration mark
48
+ #
49
+ def rm_search_area(corner, i)
50
+ {
51
+ x: (ppu_x * rmx(corner, i)).round,
52
+ y: (ppu_y * rmy(corner, i)).round,
53
+ w: (ppu_x * (reg_search + reg_radius * i)).round,
54
+ h: (ppu_y * (reg_search + reg_radius * i)).round
55
+ }
56
+ end
57
+
58
+ # a safe distance to determine
59
+ def rm_edgy_x() (ppu_x * reg_radius).round + 5 end
60
+ def rm_edgy_y() (ppu_y * reg_radius).round + 5 end
61
+ # areas on the sheet that are certainly white/black
62
+ def paper_white_area() barcode_bit_area -1 end
63
+ def ink_black_area() barcode_bit_area 0 end
64
+ def max_choices_per_question() @params[:items][:max_cells].to_i end
65
+ def rm_max_search_area_side() (ppu_x * page_width / 4).round end
66
+
67
+ private
68
+
69
+ def cx() @px / reg_frame_width end
70
+ def cy() @py / reg_frame_height end
71
+ def ppu_x() @px / page_width end
72
+ def ppu_y() @py / page_height end
73
+
74
+ # finding the width of the registration area based on iteration
75
+ def rmx(corner, i)
76
+ case corner
77
+ when :tl; reg_off
78
+ when :tr; page_width - reg_search - reg_off - reg_radius * i
79
+ when :br; page_width - reg_search - reg_off - reg_radius * i
80
+ when :bl; reg_off
81
+ end
82
+ end
83
+
84
+ # finding the height of the registration area based on iteration
85
+ def rmy(corner, i)
86
+ case corner
87
+ when :tl; reg_off
88
+ when :tr; reg_off
89
+ when :br; page_height - reg_search - reg_off - reg_radius * i
90
+ when :bl; page_height - reg_search - reg_off - reg_radius * i
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,107 @@
1
+ require 'mork/grid'
2
+
3
+ module Mork
4
+ # GridPDF gets coordinates and measurements from a Grid and
5
+ # provides SheetPDF with the properly computed values
6
+ class GridPDF < Grid
7
+ def initialize(options=nil)
8
+ super options
9
+ end
10
+
11
+ def reg_marks
12
+ r = reg_radius.mm
13
+ [
14
+ { p: [0, 0 ], r: r },
15
+ { p: [0, reg_frame_height.mm], r: r },
16
+ { p: [reg_frame_width.mm, reg_frame_height.mm], r: r },
17
+ { p: [reg_frame_width.mm, 0 ], r: r }
18
+ ]
19
+ end
20
+
21
+ def barcode_bit_areas_for(code)
22
+ black = barcode_bits.times.reject { |x| (code>>x)[0]==0 }
23
+ black.collect { |x| barcode_area x+1 }
24
+ end
25
+
26
+ def calibration_cell_areas
27
+ rows.times.collect do |q|
28
+ {
29
+ p: [(reg_frame_width-cell_spacing).mm, (reg_frame_height - cell_y(q)).mm],
30
+ w: cell_width.mm,
31
+ h: cell_height.mm
32
+ }
33
+ end
34
+ end
35
+
36
+ # Coordinates at which to place calibration cell labels (usually an ‘X’)
37
+ def calibration_letter_xy(q)
38
+ [
39
+ (reg_frame_width-cell_spacing).mm + 2.mm,
40
+ item_text_y(q)
41
+ ]
42
+ end
43
+
44
+ # Coordinates at which to place item numbers
45
+ def qnum_xy(q)
46
+ [
47
+ cell_x(q, 0).mm - qnum_width - @params[:items][:number_margin].to_f.mm,
48
+ item_text_y(q)
49
+ ]
50
+ end
51
+
52
+ # Coordinates at which to place choice labels
53
+ def choice_letter_xy(q, c)
54
+ [
55
+ cell_x(q, c).mm + 2.mm,
56
+ item_text_y(q)
57
+ ]
58
+ end
59
+
60
+ def choice_cell_area(q, c)
61
+ {
62
+ p: [cell_x(q, c).mm, (reg_frame_height - cell_y(q)).mm],
63
+ w: cell_width.mm,
64
+ h: cell_height.mm
65
+ }
66
+ end
67
+
68
+ def page_size() [page_width.mm, page_height.mm] end
69
+ def margins() reg_margin.mm end
70
+ def ink_black_area() barcode_area(0) end
71
+ def qnum_width() @params[:items][:number_width].to_f.mm end
72
+ def item_font_size() @params[:items][:font_size].to_f end
73
+ def header_width(k) @params[:header][k][:width].to_f.mm end
74
+ def header_height(k) @params[:header][k][:height].to_f.mm end
75
+ def header_size(k) @params[:header][k][:size].to_f end
76
+ def header_boxed?(k) @params[:header][k][:box] == true end
77
+
78
+ def header_xy(k)
79
+ [
80
+ @params[:header][k][:left].to_f.mm,
81
+ (reg_frame_height - @params[:header][k][:top].to_f).mm
82
+ ]
83
+ end
84
+
85
+ def header_padding(k)
86
+ [
87
+ 1.mm,
88
+ header_height(k) - 1.mm
89
+ ]
90
+ end
91
+
92
+
93
+ private
94
+
95
+ def item_text_y(q)
96
+ (reg_frame_height - cell_y(q) - cell_height/4).mm
97
+ end
98
+
99
+ def barcode_area(i)
100
+ {
101
+ p: [barcode_bit_x(i).mm, (reg_frame_height - barcode_y).mm],
102
+ w: barcode_width.mm,
103
+ h: barcode_height.mm * 2
104
+ }
105
+ end
106
+ end
107
+ end
data/lib/mork/mimage.rb CHANGED
@@ -39,7 +39,7 @@ module Mork
39
39
  # =============
40
40
  # = Highlight =
41
41
  # =============
42
- def highlight!(cells, roundedness=nil)
42
+ def highlight_cells!(cells, roundedness=nil)
43
43
  cells = [cells] if cells.is_a? Hash
44
44
  roundedness ||= [cells[0][:h], cells[0][:w]].min / 2
45
45
  cells.each do |c|
@@ -51,12 +51,24 @@ module Mork
51
51
  end
52
52
  end
53
53
 
54
+ def highlight_rect!(areas)
55
+ areas = [areas] if areas.is_a? Hash
56
+ areas.each do |c|
57
+ out = Magick::Draw.new
58
+ out.fill_opacity 0
59
+ out.stroke 'yellow'
60
+ out.stroke_width 3
61
+ out.rectangle c[:x], c[:y], c[:x]+c[:w], c[:y]+c[:h]
62
+ out.draw @image
63
+ end
64
+ end
65
+
54
66
  def join!(p)
55
67
  poly = Magick::Draw.new
56
68
  poly.fill_opacity 0
57
69
  poly.stroke 'green'
58
70
  poly.stroke_width 3
59
- poly.polygon p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]
71
+ poly.polygon p[0][:x], p[0][:y], p[1][:x], p[1][:y], p[2][:x], p[2][:y], p[3][:x], p[3][:y]
60
72
  poly.draw @image
61
73
  end
62
74
 
data/lib/mork/npatch.rb CHANGED
@@ -44,7 +44,6 @@ module Mork
44
44
  end
45
45
 
46
46
  def edgy?(x, y)
47
- puts "PATCH: (#{@width},#{@height}) EDGY: x=#{x}, y=#{y}"
48
47
  tol = 5
49
48
  (x < tol) or (y < tol) or (y > @height - tol) or (x > @width - tol)
50
49
  end
@@ -1,19 +1,25 @@
1
+ require 'mork/grid_omr'
2
+ require 'mork/mimage'
3
+ require 'mork/mimage_list'
4
+ require 'mork/npatch'
5
+
1
6
  module Mork
2
- class Sheet
3
- def initialize(im, grid=Grid.new)
4
- @raw = case im.class.to_s
5
- when "String"
6
- Mimage.new im
7
- when "Mork::Mimage"
8
- im
9
- else
10
- raise "A new sheet requires either a Mimage or the name of the source image file, but it was a: #{im.class}"
11
- end
12
- @grid = grid
13
- # send page size to the grid, so that all later measurements can be done within the
14
- # grid itself; this method assumes a 'stretch' strategy, i.e. where the image
15
- # after registration has the same size in pixels as the original scanned file
16
- @grid.set_page_size @raw.width, @raw.height
7
+ class SheetOMR
8
+ def initialize(im, grom=nil)
9
+ @raw = case im
10
+ when String
11
+ Mimage.new im
12
+ when Mork::Mimage
13
+ im
14
+ else
15
+ raise "A new sheet requires either a Mimage or the name of the source image file, but it was a: #{im.class}"
16
+ end
17
+ @grom = case grom
18
+ when String, Hash, NilClass
19
+ GridOMR.new @raw.width, @raw.height, grom
20
+ else
21
+ raise 'Invalid argument in SheetOMR initialization'
22
+ end
17
23
  @rm = {}
18
24
  @rmsa = {}
19
25
  @ok_reg = register @raw
@@ -23,25 +29,21 @@ module Mork
23
29
  @ok_reg
24
30
  end
25
31
 
26
- def status
27
- @rm
28
- end
29
-
30
- # code
32
+ # barcode
31
33
  #
32
- # returns the sheet code as an integer
33
- def code
34
+ # returns the sheet barcode as an integer
35
+ def barcode
34
36
  return if not_registered
35
- code_string.to_i(2)
37
+ barcode_string.to_i(2)
36
38
  end
37
39
 
38
- # code_string
40
+ # barcode_string
39
41
  #
40
- # returns the sheet code as a string of 0s and 1s. The string is code_bits
42
+ # returns the sheet barcode as a string of 0s and 1s. The string is barcode_bits
41
43
  # bits long, with most significant bits to the left
42
- def code_string
44
+ def barcode_string
43
45
  return if not_registered
44
- cs = (0...@grid.code_bits).inject("") { |c, v| c << code_cell_value(v) }
46
+ cs = @grom.barcode_bits.times.inject("") { |c, v| c << barcode_bit_value(v) }
45
47
  cs.reverse
46
48
  end
47
49
 
@@ -51,7 +53,6 @@ module Mork
51
53
  # false otherwise
52
54
  def marked?(q, c)
53
55
  return if not_registered
54
- # puts "SHADE: #{shade_of(q, c)}, Q: #{q}, C: #{c}, THR: #{choice_threshold}"
55
56
  shade_of(q, c) < choice_threshold
56
57
  end
57
58
 
@@ -67,7 +68,7 @@ module Mork
67
68
  return if not_registered
68
69
  question_range(r).collect do |q|
69
70
  cho = []
70
- (0...@grid.max_choices_per_question).each do |c|
71
+ (0...@grom.max_choices_per_question).each do |c|
71
72
  cho << c if marked?(q, c)
72
73
  end
73
74
  cho
@@ -77,7 +78,7 @@ module Mork
77
78
  def mark_logical_array(r = nil)
78
79
  return if not_registered
79
80
  question_range(r).collect do |q|
80
- (0...@grid.max_choices_per_question).collect {|c| marked?(q, c)}
81
+ (0...@grom.max_choices_per_question).collect {|c| marked?(q, c)}
81
82
  end
82
83
  end
83
84
 
@@ -90,55 +91,36 @@ module Mork
90
91
  @crop.outline! array_of cells
91
92
  end
92
93
 
93
- def highlight_ctrl
94
- return if not_registered
95
- @crop.highlight! [@grid.ctrl_area_dark, @grid.ctrl_area_light]
96
- end
97
-
98
94
  def highlight_all
99
95
  return if not_registered
100
- cells = (0...@grid.max_questions).collect { |i| (0...@grid.max_choices_per_question).to_a }
101
- @crop.highlight! array_of cells
102
- end
103
-
104
- def highlight
105
- return if not_registered
106
- @crop.highlight! array_of mark_array
96
+ cells = (0...@grom.max_questions).collect { |i| (0...@grom.max_choices_per_question).to_a }
97
+ @crop.highlight_cells! array_of cells
98
+ @crop.highlight_cells! @grom.calibration_cell_areas
99
+ @crop.highlight_rect! [@grom.ink_black_area, @grom.paper_white_area]
100
+ @crop.highlight_rect! @grom.barcode_bit_areas
101
+ # @grom.barcode_bits.times do |bit|
102
+ # @crop.highlight_rect! @grom.barcode_bit_area bit
103
+ # end
107
104
  end
108
105
 
109
- def highlight_code_areas
106
+ def highlight_marked
110
107
  return if not_registered
111
- @grid.code_bits.times do |bit|
112
- @crop.highlight! @grid.code_bit_area bit
113
- end
108
+ @crop.highlight_cells! array_of mark_array
114
109
  end
115
110
 
116
- def highlight_code
111
+ def highlight_barcode
117
112
  return if not_registered
118
- @grid.code_bits.times do |bit|
119
- if code_string.reverse[bit] == '1'
120
- @crop.highlight! @grid.code_bit_area bit
113
+ @grom.barcode_bits.times do |bit|
114
+ if barcode_string.reverse[bit] == '1'
115
+ @crop.highlight_rect! @grom.barcode_bit_area bit+1
121
116
  end
122
117
  end
123
118
  end
124
119
 
125
- def highlight_dark_calibration_bit
126
- return if not_registered
127
- @crop.highlight!(@grid.cal_area_black)
128
- end
129
-
130
- def highlight_light_calibration_bit
131
- return if not_registered
132
- @crop.highlight!(@grid.cal_area_white)
133
- end
134
-
135
120
  def highlight_reg_area
136
- @raw.highlight! @rmsa[:tl]
137
- @raw.highlight! @rmsa[:tr]
138
- @raw.highlight! @rmsa[:br]
139
- @raw.highlight! @rmsa[:bl]
121
+ @raw.highlight_rect! [@rmsa[:tl], @rmsa[:tr], @rmsa[:br], @rmsa[:bl]]
140
122
  return if not_registered
141
- @raw.join!(@rm)
123
+ @raw.join! [@rm[:tl],@rm[:tr],@rm[:br],@rm[:bl]]
142
124
  end
143
125
 
144
126
  def write(fname)
@@ -150,13 +132,20 @@ module Mork
150
132
  @raw.write(fname)
151
133
  end
152
134
 
135
+ # =================================
136
+ # = compute shading with NPatches =
137
+ # =================================
138
+ def shade_of(q, c)
139
+ naverage @grom.choice_cell_area(q, c)
140
+ end
141
+
153
142
  private
154
143
 
155
144
  def array_of(cells)
156
145
  out = []
157
146
  cells.each_with_index do |q, i|
158
147
  q.each do |c|
159
- out << @grid.choice_cell_area(i, c)
148
+ out << @grom.choice_cell_area(i, c)
160
149
  end
161
150
  end
162
151
  out
@@ -164,7 +153,7 @@ module Mork
164
153
 
165
154
  def question_range(r)
166
155
  if r.nil?
167
- (0...@grid.max_questions)
156
+ (0...@grom.max_questions)
168
157
  elsif r.is_a? Fixnum
169
158
  (0...r)
170
159
  elsif r.is_a? Array
@@ -174,35 +163,44 @@ module Mork
174
163
  end
175
164
  end
176
165
 
177
- # =================================
178
- # = compute shading with NPatches =
179
- # =================================
180
- def shade_of(q, c)
181
- naverage @grid.choice_cell_area(q, c)
182
- end
183
-
184
- def shade_of_code_bit(i)
185
- naverage @grid.code_bit_area(i)
166
+ def barcode_bit_value(i)
167
+ shade_of_barcode_bit(i) < barcode_threshold ? "1" : "0"
186
168
  end
187
169
 
188
- def code_cell_value(i)
189
- shade_of_code_bit(i) < code_threshold ? "1" : "0"
170
+ def shade_of_barcode_bit(i)
171
+ naverage @grom.barcode_bit_area i+1
190
172
  end
191
173
 
174
+ def barcode_threshold
175
+ @barcode_threshold ||= (paper_white + ink_black) / 2
176
+ end
177
+
192
178
  def choice_threshold
193
- @choice_threshold ||= (naverage(@grid.ctrl_area_dark) +
194
- naverage(@grid.ctrl_area_light)) / 2
179
+ @choice_threshold ||= ccmeans.mean - ccmeans.stdev * 7
180
+ end
181
+
182
+ def ccmeans
183
+ @calcmeans ||= @grom.calibration_cell_areas.collect { |c| naverage c }
184
+ end
185
+
186
+ def paper_white
187
+ @paper_white ||= naverage @grom.paper_white_area
188
+ end
189
+
190
+ def ink_black
191
+ @ink_black ||= naverage @grom.ink_black_area
195
192
  end
196
193
 
197
- def code_threshold
198
- @code_threshold ||= (naverage(@grid.cal_area_black) +
199
- naverage(@grid.cal_area_white)) / 2
194
+ def shade_of_blank_cells
195
+ # @grom.
200
196
  end
201
197
 
202
198
  # ================
203
199
  # = Registration =
204
200
  # ================
205
201
 
202
+ # this method uses a 'stretch' strategy, i.e. where the image after
203
+ # registration has the same size in pixels as the original scanned file
206
204
  def register(img)
207
205
  # find the XY coordinates of the 4 registration marks
208
206
  @rm[:tl] = reg_centroid_on(img, :tl)
@@ -224,19 +222,19 @@ module Mork
224
222
  # in the XY coordinates of the entire image
225
223
  def reg_centroid_on(img, corner)
226
224
  1000.times do |i|
227
- @rmsa[corner] = @grid.rm_search_area(corner, i)
225
+ @rmsa[corner] = @grom.rm_search_area(corner, i)
228
226
  cx, cy = NPatch.new(img.crop(@rmsa[corner])).dark_centroid
229
227
  if cx.nil?
230
228
  status = :insufficient_contrast
231
- elsif (cx < @grid.rm_edgy_x) or
232
- (cy < @grid.rm_edgy_y) or
233
- (cy > @rmsa[corner][:h] - @grid.rm_edgy_y) or
234
- (cx > @rmsa[corner][:w] - @grid.rm_edgy_x)
229
+ elsif (cx < @grom.rm_edgy_x) or
230
+ (cy < @grom.rm_edgy_y) or
231
+ (cy > @rmsa[corner][:h] - @grom.rm_edgy_y) or
232
+ (cx > @rmsa[corner][:w] - @grom.rm_edgy_x)
235
233
  status = :edgy
236
234
  else
237
235
  return {status: :ok, x: cx + @rmsa[corner][:x], y: cy + @rmsa[corner][:y]}
238
236
  end
239
- return {status: status, x: nil, y: nil} if @rmsa[corner][:w] > @grid.rm_max_search_area_side
237
+ return {status: status, x: nil, y: nil} if @rmsa[corner][:w] > @grom.rm_max_search_area_side
240
238
  end
241
239
  end
242
240