mork 0.0.9 → 0.0.10

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