mork 0.6.0 → 0.7.0

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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -3
  3. data/lib/mork/coord.rb +58 -0
  4. data/lib/mork/grid.rb +44 -35
  5. data/lib/mork/grid_const.rb +11 -6
  6. data/lib/mork/grid_omr.rb +76 -62
  7. data/lib/mork/magicko.rb +162 -0
  8. data/lib/mork/mimage.rb +102 -177
  9. data/lib/mork/npatch.rb +38 -56
  10. data/lib/mork/sheet_omr.rb +45 -43
  11. data/lib/mork/sheet_pdf.rb +16 -16
  12. data/lib/mork/version.rb +1 -1
  13. data/mork.gemspec +2 -2
  14. data/mork.sublime-project +9 -0
  15. data/spec/mork/coord_spec.rb +55 -0
  16. data/spec/mork/grid_omr_spec.rb +62 -85
  17. data/spec/mork/grid_spec.rb +7 -7
  18. data/spec/mork/magicko_spec.rb +46 -0
  19. data/spec/mork/mimage_spec.rb +30 -20
  20. data/spec/mork/npatch_spec.rb +46 -39
  21. data/spec/mork/sheet_omr_spec.rb +82 -40
  22. data/spec/mork/sheet_pdf_spec.rb +8 -8
  23. data/spec/samples/angolo.jpg +0 -0
  24. data/spec/samples/grid.yml +53 -0
  25. data/spec/samples/info.yml +12 -11
  26. data/spec/samples/layout.yml +9 -5
  27. data/spec/samples/lucrezia/border1.pdf +0 -0
  28. data/spec/samples/lucrezia/border2.pdf +0 -0
  29. data/spec/samples/lucrezia/bw1.pdf +0 -0
  30. data/spec/samples/lucrezia/bw2.pdf +0 -0
  31. data/spec/samples/lucrezia/gray1.pdf +0 -0
  32. data/spec/samples/lucrezia/gray2.pdf +0 -0
  33. data/spec/samples/out-1.jpg +0 -0
  34. data/spec/samples/rm00.jpeg +0 -0
  35. data/spec/samples/slanted.jpg +0 -0
  36. data/spec/samples/slanted.yml +54 -0
  37. data/spec/samples/syst/IMG_20150104_0004.jpg +0 -0
  38. data/spec/samples/syst/IMG_20150104_0004.txt +4955 -0
  39. data/spec/samples/syst/IMG_20150104_0009.jpg +0 -0
  40. data/spec/samples/syst/IMG_20150104_0009.txt +4955 -0
  41. data/spec/samples/syst/IMG_20150104_0011.jpg +0 -0
  42. data/spec/samples/syst/IMG_20150104_0011.txt +4955 -0
  43. data/spec/samples/syst/SCN_0001.jpg +0 -0
  44. data/spec/samples/syst/SCN_0001.txt +4955 -0
  45. data/spec/samples/syst/barr0.jpg +0 -0
  46. data/spec/samples/syst/barr0.txt +4955 -0
  47. data/spec/samples/syst/barr1.jpg +0 -0
  48. data/spec/samples/syst/barr1.txt +4955 -0
  49. data/spec/samples/syst/barr2.jpg +0 -0
  50. data/spec/samples/syst/barr2.txt +4955 -0
  51. data/spec/samples/syst/bell0.jpg +0 -0
  52. data/spec/samples/syst/bell0.txt +4955 -0
  53. data/spec/samples/syst/bell1.jpg +0 -0
  54. data/spec/samples/syst/bell1.txt +4955 -0
  55. data/spec/samples/syst/bell2.jpg +0 -0
  56. data/spec/samples/syst/bell2.txt +4955 -0
  57. data/spec/samples/syst/bila0.jpg +0 -0
  58. data/spec/samples/syst/bila0.txt +4955 -0
  59. data/spec/samples/syst/bila1.jpg +0 -0
  60. data/spec/samples/syst/bila1.txt +4955 -0
  61. data/spec/samples/syst/bila2.jpg +0 -0
  62. data/spec/samples/syst/bila2.txt +4955 -0
  63. data/spec/samples/syst/bila3.jpg +0 -0
  64. data/spec/samples/syst/bila3.txt +4955 -0
  65. data/spec/samples/syst/bila4.jpg +0 -0
  66. data/spec/samples/syst/bila4.txt +4955 -0
  67. data/spec/samples/syst/bone0.jpg +0 -0
  68. data/spec/samples/syst/bone0.txt +4955 -0
  69. data/spec/samples/syst/bone1.jpg +0 -0
  70. data/spec/samples/syst/bone1.txt +4955 -0
  71. data/spec/samples/syst/bone2.jpg +0 -0
  72. data/spec/samples/syst/bone2.txt +4955 -0
  73. data/spec/samples/syst/cost0.jpg +0 -0
  74. data/spec/samples/syst/cost0.txt +4955 -0
  75. data/spec/samples/syst/cost1.jpg +0 -0
  76. data/spec/samples/syst/cost1.txt +4955 -0
  77. data/spec/samples/syst/cost2.jpg +0 -0
  78. data/spec/samples/syst/cost2.txt +4955 -0
  79. data/spec/samples/syst/cost3.jpg +0 -0
  80. data/spec/samples/syst/cost3.txt +4955 -0
  81. data/spec/samples/syst/cost4.jpg +0 -0
  82. data/spec/samples/syst/cost4.txt +4955 -0
  83. data/spec/samples/syst/dald0.jpg +0 -0
  84. data/spec/samples/syst/dald0.txt +4955 -0
  85. data/spec/samples/syst/dald1.jpg +0 -0
  86. data/spec/samples/syst/dald1.txt +4955 -0
  87. data/spec/samples/syst/dald2.jpg +0 -0
  88. data/spec/samples/syst/dald2.txt +4955 -0
  89. data/spec/samples/syst/dald3.jpg +0 -0
  90. data/spec/samples/syst/dald3.txt +4955 -0
  91. data/spec/samples/syst/dald4.jpg +0 -0
  92. data/spec/samples/syst/dald4.txt +4955 -0
  93. data/spec/samples/syst/dign0.jpg +0 -0
  94. data/spec/samples/syst/dign0.txt +4955 -0
  95. data/spec/samples/syst/dign1.jpg +0 -0
  96. data/spec/samples/syst/dign1.txt +4955 -0
  97. data/spec/samples/syst/dign2.jpg +0 -0
  98. data/spec/samples/syst/dign2.txt +4955 -0
  99. data/spec/samples/syst/dive0.jpg +0 -0
  100. data/spec/samples/syst/dive0.txt +4955 -0
  101. data/spec/samples/syst/dive1.jpg +0 -0
  102. data/spec/samples/syst/dive1.txt +4955 -0
  103. data/spec/samples/syst/dive2.jpg +0 -0
  104. data/spec/samples/syst/dive2.txt +4955 -0
  105. data/spec/samples/syst/histo.m +42 -0
  106. data/spec/samples/syst/out0000.jpg +0 -0
  107. data/spec/samples/syst/out0000.txt +4955 -0
  108. data/spec/samples/syst/out0001.jpg +0 -0
  109. data/spec/samples/syst/out0001.txt +4955 -0
  110. data/spec/samples/syst/out0002.jpg +0 -0
  111. data/spec/samples/syst/out0002.txt +4955 -0
  112. data/spec/samples/syst/qzc013.jpg +0 -0
  113. data/spec/samples/syst/qzc013.txt +4955 -0
  114. data/spec/samples/syst/sample_gray.jpg +0 -0
  115. data/spec/samples/syst/sample_gray.txt +4955 -0
  116. data/spec/samples/syst_grid.yml +53 -0
  117. data/spec/spec_helper.rb +18 -10
  118. data/test_reg.m +39 -0
  119. metadata +105 -8
  120. data/spec/samples/io.jpg +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 59b94f585fff925a78dc0fc848c267503cfdc8c0
4
- data.tar.gz: 0a71421a1f6a1fd9790047ea4b913397572799ea
3
+ metadata.gz: 805582a3e749af1de651277c4a3f2f4cd6507989
4
+ data.tar.gz: 5d552b668eedbea2261c4d722620f2b3534859e7
5
5
  SHA512:
6
- metadata.gz: 2d8863ac600a282190b719b433db6ac761422f4ad8485fe979230b6e1ee609685e1a611f996ef2fe33785e2185dfde23ac9f8253c5c4351d0f937a262ef487df
7
- data.tar.gz: ec876a3c40eb586f462d9b7127537b5562a0402a7ad6f96063ca5434dd5ad5d6151e96de92913b6ee8845fb677c0133766c63986f5c475dd271705b641d0013f
6
+ metadata.gz: a37d3e1a738d0593efa9e8c2bb1f8599e4954ae34140344bbc60e03957cd49ad5837e45b0617b724830b62b3a582e43f6fa9a91dc8c1fb5177ce01d9bdbaba30
7
+ data.tar.gz: fb52e0048d8068804fdb78e1530d96e9312ecd353e1fde01a8d2d0f7d0fb4a74ba728cb3b6d08b015acc079996d12aa8eced2e759ad9bd6a0bc3aa1c0fc15d30
data/README.md CHANGED
@@ -91,8 +91,10 @@ page_size: # all measurements in mm
91
91
  reg_marks:
92
92
  margin: 10 # distance from each page border to registration mark center
93
93
  radius: 2.5 # registration mark radius
94
- search: 12 # initial size of the registration mark search area (*)
94
+ crop: 12 # size of the registration mark search area (*)
95
95
  offset: 2 # distance between the search area and each page border (*)
96
+ blur: 2 # size of a gaussian blur filter to smooth overly pixelated registration marks (0 to skip)
97
+ dilate: 5 # size of a “dilate” filter to get rid of stray noise (0 to skip)
96
98
  header:
97
99
  name: # ‘name’ is just a label; you can add arbitrary header elements
98
100
  top: 5 # margin relative to registration frame top side
@@ -152,7 +154,7 @@ If the `layout` argument is omitted, Mork will search for a file named `layout.y
152
154
 
153
155
  ## Scoring response sheets with `SheetOMR`
154
156
 
155
- Assuming that a person has filled out a response sheet by darkening with a pen the selected choices, and that sheet has been acquired as an image file, response scoring is performed by the `Mork::SheetOMR` class. Three pieces of information must be provided to the class constructor:
157
+ Assuming that a person has filled out a response sheet by darkening with a pen the selected choices, and that the sheet has been acquired as an image file, response scoring is performed by the `Mork::SheetOMR` class. Three pieces of information must be provided to the object constructor:
156
158
 
157
159
  - **image**: the path/filename of the bitmap image (currently accepts JPG, JPEG, PNG, PDF extensions; a resolution of 150-200 dpi is usually more than sufficient to obtain accurate readings)
158
160
  - **choices**: equivalent to the `choices` array of integers passed to the `SheetPDF` constructor as part of the `content` parameter (see above)
@@ -178,4 +180,4 @@ system 'open highlights.jpg' # OSX only
178
180
 
179
181
  #### Wait, why Mork?
180
182
 
181
- Because I used to have a crush on Mindy
183
+ Because I used to have a crush on Mindy
data/lib/mork/coord.rb ADDED
@@ -0,0 +1,58 @@
1
+ module Mork
2
+ # The Coord class takes coordinates in the standard unit (e.g. mm)
3
+ # and provides pixel-based coordinates useful for image manipulation
4
+ class Coord
5
+ attr_reader :x, :y, :w, :h
6
+
7
+ def initialize(w, h: w, x: 0, y: 0, cx: 1, cy: cx)
8
+ @x = (cx*x).round
9
+ @y = (cy*y).round
10
+ @w = (cx*w).round
11
+ @h = (cy*h).round
12
+ end
13
+
14
+ def to_hash
15
+ { w: @w, h: @h, x: @x, y: @y }
16
+ 5
17
+ end
18
+
19
+ def print
20
+ puts "X: #{@x}, Y: #{@y}, W: #{@w}, H: #{@h}"
21
+ end
22
+
23
+ def rect_points
24
+ [@x, @y, @x+@w, @y+@h].join ' '
25
+ end
26
+
27
+ def choice_cell
28
+ rness = [@h, @w].min / 2
29
+ rect_points + " #{rness} #{rness}"
30
+ end
31
+
32
+ def cross1
33
+ [@x+corner, @y+corner, @x+@w-corner, @y+@h-corner].join ' '
34
+ end
35
+
36
+ def cross2
37
+ [@x+corner, @y+@h-corner, @x+@w-corner, @y+corner].join ' '
38
+ end
39
+
40
+ def cropper
41
+ "#{@w}x#{@h}+#{@x}+#{@y}"
42
+ end
43
+
44
+ def x_rng
45
+ @x...@x+@w
46
+ end
47
+
48
+ def y_rng
49
+ @y...@y+@h
50
+ end
51
+
52
+ private
53
+
54
+ def corner
55
+ (@h - @w).abs
56
+ end
57
+ end
58
+ end
data/lib/mork/grid.rb CHANGED
@@ -22,7 +22,7 @@ module Mork
22
22
  raise "Invalid parameter in the Grid constructor: #{options.class.inspect}"
23
23
  end
24
24
  end
25
-
25
+
26
26
  # Puts out the Grid parameters in YAML format; the entire hash is displayed
27
27
  # if no arguments are given; you can specify what to show by passing one of:
28
28
  # :page_size, :reg_marks, :header, :items, :barcode, :control
@@ -30,27 +30,35 @@ module Mork
30
30
  out = subset ? @params[subset] : @params
31
31
  puts out.to_yaml
32
32
  end
33
-
33
+
34
34
  def options
35
35
  @params
36
36
  end
37
-
37
+
38
38
  def max_questions
39
39
  columns * rows
40
40
  end
41
-
41
+
42
42
  def max_choices_per_question
43
43
  @params[:items][:max_cells].to_i
44
44
  end
45
-
45
+
46
46
  def barcode_bits
47
47
  @params[:barcode][:bits].to_i
48
48
  end
49
-
49
+
50
+ def rm_dilate
51
+ @params[:reg_marks][:dilate].to_i
52
+ end
53
+
54
+ def rm_blur
55
+ @params[:reg_marks][:blur].to_i
56
+ end
57
+
50
58
  #====================#
51
59
  private
52
60
  #====================#
53
-
61
+
54
62
  # recursively turn hash keys into symbols. pasted from
55
63
  # http://stackoverflow.com/questions/800122/best-way-to-convert-strings-to-symbols-in-hash
56
64
  def symbolize(obj)
@@ -58,60 +66,61 @@ module Mork
58
66
  return obj.inject([]){|memo,v | memo << symbolize(v); memo} if obj.is_a? Array
59
67
  return obj
60
68
  end
61
-
69
+
62
70
  # cell_y(q)
63
- #
71
+ #
64
72
  # the distance from the registration frame to the top edge
65
73
  # of all choice cells in the q-th question
66
74
  def cell_y(q)
67
75
  first_y + item_spacing * (q % rows) - cell_height / 2
68
76
  end
69
-
77
+
70
78
  # cell_x(q,c)
71
- #
79
+ #
72
80
  # the distance from the registration frame to the left edge
73
81
  # of the c-th choice cell of the q-th question
74
82
  def cell_x(q,c)
75
83
  item_x(q) + cell_spacing * c
76
84
  end
77
-
85
+
78
86
  def item_x(q)
79
87
  first_x + column_width * (q / rows) - cell_width / 2
80
88
  end
81
-
89
+
82
90
  def cal_cell_x
83
91
  reg_frame_width - cell_spacing
84
92
  end
85
-
93
+
86
94
  # ===========
87
95
  # = barcode =
88
96
  # ===========
89
97
  def barcode_bit_x(i)
90
98
  @params[:barcode][:left] + @params[:barcode][:spacing] * i
91
99
  end
92
-
100
+
93
101
  # ===============================
94
102
  # = Simple parameter extraction =
95
103
  # ===============================
96
- def barcode_y() reg_frame_height - barcode_height end
97
- def barcode_height() @params[:barcode][:height].to_f end
98
- def barcode_width() @params[:barcode][:width].to_f end
99
- def cell_width() @params[:items][:cell_width].to_f end
100
- def cell_height() @params[:items][:cell_height].to_f end
101
- def cell_spacing() @params[:items][:x_spacing].to_f end
102
- def item_spacing() @params[:items][:y_spacing].to_f end
103
- def column_width() @params[:items][:column_width].to_f end
104
- def first_x() @params[:items][:left].to_f end
105
- def first_y() @params[:items][:top].to_f end
106
- def rows() @params[:items][:rows] end
107
- def columns() @params[:items][:columns] end
108
- def reg_search() @params[:reg_marks][:search].to_f end
109
- def reg_off() @params[:reg_marks][:offset].to_f end
110
- def reg_frame_width() page_width - reg_margin * 2 end
111
- def reg_frame_height() page_height - reg_margin * 2 end
112
- def page_width() @params[:page_size][:width].to_f end
113
- def page_height() @params[:page_size][:height].to_f end
114
- def reg_margin() @params[:reg_marks][:margin].to_f end
115
- def reg_radius() @params[:reg_marks][:radius].to_f end
104
+ def barcode_y() reg_frame_height - barcode_height end
105
+ def barcode_height() @params[:barcode][:height].to_f end
106
+ def barcode_width() @params[:barcode][:width].to_f end
107
+ def cell_width() @params[:items][:cell_width].to_f end
108
+ def cell_height() @params[:items][:cell_height].to_f end
109
+ def cell_spacing() @params[:items][:x_spacing].to_f end
110
+ def item_spacing() @params[:items][:y_spacing].to_f end
111
+ def column_width() @params[:items][:column_width].to_f end
112
+ def first_x() @params[:items][:left].to_f end
113
+ def first_y() @params[:items][:top].to_f end
114
+ def rows() @params[:items][:rows] end
115
+ def columns() @params[:items][:columns] end
116
+ def reg_search() @params[:reg_marks][:search].to_f end
117
+ def reg_crop() @params[:reg_marks][:crop].to_f end
118
+ def reg_off() @params[:reg_marks][:offset].to_f end
119
+ def reg_frame_width() page_width - reg_margin * 2 end
120
+ def reg_frame_height() page_height - reg_margin * 2 end
121
+ def page_width() @params[:page_size][:width].to_f end
122
+ def page_height() @params[:page_size][:height].to_f end
123
+ def reg_margin() @params[:reg_marks][:margin].to_f end
124
+ def reg_radius() @params[:reg_marks][:radius].to_f end
116
125
  end
117
126
  end
@@ -1,18 +1,23 @@
1
1
  module Mork
2
2
  # this is the default grid!
3
+ # default units are millimiters
3
4
  DGRID = {
4
- # default units are millimiters
5
+ # size of the paper sheet
5
6
  page_size: {
6
7
  # this is A4
7
8
  width: 210,
8
9
  height: 297
9
- }, # page end
10
+ },
11
+ # size, location, search parameters of registration marks
10
12
  reg_marks: {
11
13
  margin: 10,
12
14
  radius: 2.5,
13
- search: 10,
14
- offset: 2
15
- }, # reg_marks end
15
+ search: 10, # remove this?
16
+ offset: 2,
17
+ crop: 20, # size of square where the regmark should be located
18
+ dilate: 0, # set to >0 to apply a dilate IM operation
19
+ blur: 0 # set to >0 to apply a blur IM operation
20
+ },
16
21
  header: {
17
22
  name: {
18
23
  top: 5,
@@ -42,7 +47,7 @@ module Mork
42
47
  }
43
48
  }, # header end
44
49
  items: {
45
- columns: 4,
50
+ columns: 4,
46
51
  column_width: 44,
47
52
  rows: 30,
48
53
  # from the top-left registration mark
data/lib/mork/grid_omr.rb CHANGED
@@ -1,16 +1,18 @@
1
1
  require 'mork/grid'
2
+ require 'mork/coord'
2
3
 
3
4
  module Mork
4
5
  class GridOMR < Grid
5
6
  def initialize(options=nil)
6
7
  super options
7
8
  end
8
-
9
+
9
10
  def set_page_size(width, height)
10
11
  @px = width.to_f
11
12
  @py = height.to_f
13
+ self
12
14
  end
13
-
15
+
14
16
  def barcode_bit_areas(bitstring = '1' * barcode_bits)
15
17
  areas = []
16
18
  bitstring.reverse.each_char.with_index do |c, i|
@@ -18,88 +20,100 @@ module Mork
18
20
  end
19
21
  areas
20
22
  end
21
-
22
- # ====================================================
23
- # = Returning {x, y, w, h} hashes for area locations =
24
- # ====================================================
23
+
24
+ # ===========================================
25
+ # = Returning Coord sets for area locations =
26
+ # ===========================================
25
27
  def choice_cell_area(q, c)
26
- {
27
- x: (cx * cell_x(q,c)).round,
28
- y: (cy * cell_y(q) ).round,
29
- w: (cx * cell_width ).round,
30
- h: (cy * cell_height).round
31
- }
28
+ coord cell_x(q,c), cell_y(q), cell_width, cell_height
32
29
  end
33
-
30
+
34
31
  def calibration_cell_areas
35
32
  rows.times.collect do |q|
36
- {
37
- x: (cx * cal_cell_x ).round,
38
- y: (cy * cell_y(q) ).round,
39
- w: (cx * cell_width ).round,
40
- h: (cy * cell_height).round
41
- }
33
+ coord cal_cell_x, cell_y(q), cell_width, cell_height
42
34
  end
43
35
  end
44
-
45
- def cell_corner_size
46
- d = choice_cell_area(0,0)
47
- (d[:w]-d[:h]).abs
48
- end
49
-
36
+
50
37
  def barcode_bit_area(bit)
51
- {
52
- x: (cx * barcode_bit_x(bit)).round,
53
- y: (cy * barcode_y ).round,
54
- w: (cx * barcode_width ).round,
55
- h: (cy * barcode_height ).round
56
- }
38
+ coord barcode_bit_x(bit), barcode_y, barcode_width, barcode_height
57
39
  end
58
-
59
- # the 4 values needed to locate a single registration mark
60
- #
61
- def rm_search_area(corner, i)
62
- {
63
- x: (ppu_x * rmx(corner, i)).round,
64
- y: (ppu_y * rmy(corner, i)).round,
65
- w: (ppu_x * (reg_search + reg_radius * i)).round,
66
- h: (ppu_y * (reg_search + reg_radius * i)).round
67
- }
40
+
41
+ def rm_crop_area(corner)
42
+ coord rx(corner), ry(corner), reg_crop, reg_crop, ppu_x, ppu_y
68
43
  end
69
-
70
- # a safe distance to determine
71
- def rm_edgy_x() (ppu_x * reg_radius).round + 5 end
72
- def rm_edgy_y() (ppu_y * reg_radius).round + 5 end
73
- # areas on the sheet that are certainly white/black
74
- def paper_white_area() barcode_bit_area -1 end
75
- def ink_black_area() barcode_bit_area 0 end
76
- def rm_max_search_area_side() (ppu_x * page_width / 4).round end
77
-
44
+
45
+ def paper_white_area() barcode_bit_area(-1) end
46
+ def ink_black_area() barcode_bit_area( 0) end
47
+
78
48
  private
79
-
49
+
80
50
  def cx() @px / reg_frame_width end
81
51
  def cy() @py / reg_frame_height end
82
52
  def ppu_x() @px / page_width end
83
53
  def ppu_y() @py / page_height end
84
-
85
- # finding the x position of the registration area based on iteration
86
- def rmx(corner, i)
54
+
55
+ def coord(x, y, w, h, cX=cx, cY=cy)
56
+ Coord.new w, h: h, x: x, y: y, cx: cX, cy: cY
57
+ end
58
+
59
+ # iterationless x registration
60
+ def rx(corner)
87
61
  case corner
88
62
  when :tl; reg_off
89
- when :tr; page_width - reg_search - reg_off - reg_radius * i
90
- when :br; page_width - reg_search - reg_off - reg_radius * i
63
+ when :tr; page_width - reg_crop - reg_off
64
+ when :br; page_width - reg_crop - reg_off
91
65
  when :bl; reg_off
92
66
  end
93
67
  end
94
-
95
- # finding the y position of the registration area based on iteration
96
- def rmy(corner, i)
68
+
69
+ def ry(corner)
97
70
  case corner
98
71
  when :tl; reg_off
99
72
  when :tr; reg_off
100
- when :br; page_height - reg_search - reg_off - reg_radius * i
101
- when :bl; page_height - reg_search - reg_off - reg_radius * i
73
+ when :br; page_height - reg_crop - reg_off
74
+ when :bl; page_height - reg_crop - reg_off
102
75
  end
103
76
  end
104
77
  end
105
- end
78
+ end
79
+
80
+ # # the 4 values needed to locate a single registration mark
81
+ #
82
+ # def rm_search_area(corner, i)
83
+ # {
84
+ # x: (ppu_x * rmx(corner, i)).round,
85
+ # y: (ppu_y * rmy(corner, i)).round,
86
+ # w: (ppu_x * (reg_search + reg_radius * i)).round,
87
+ # h: (ppu_y * (reg_search + reg_radius * i)).round
88
+ # }
89
+ # end
90
+
91
+ # # finding the x position of the registration area based on iteration
92
+ # def rmx(corner, i)
93
+ # case corner
94
+ # when :tl; reg_off
95
+ # when :tr; page_width - reg_search - reg_off - reg_radius * i
96
+ # when :br; page_width - reg_search - reg_off - reg_radius * i
97
+ # when :bl; reg_off
98
+ # end
99
+ # end
100
+
101
+ # # finding the y position of the registration area based on iteration
102
+ # def rmy(corner, i)
103
+ # case corner
104
+ # when :tl; reg_off
105
+ # when :tr; reg_off
106
+ # when :br; page_height - reg_search - reg_off - reg_radius * i
107
+ # when :bl; page_height - reg_search - reg_off - reg_radius * i
108
+ # end
109
+ # end
110
+
111
+ # def rm_edgy_x() (ppu_x * reg_radius).round + 5 end
112
+ # def rm_edgy_y() (ppu_y * reg_radius).round + 5 end
113
+ # def rm_max_search_area_side() (ppu_x * page_width / 4).round end
114
+
115
+ # def cell_corner_size
116
+ # d = choice_cell_area(0,0)
117
+ # (d[:w]-d[:h]).abs
118
+ # end
119
+
@@ -0,0 +1,162 @@
1
+ require 'mini_magick'
2
+
3
+ module Mork
4
+ # Magicko: image management, done in two ways: 1) direct system calls to
5
+ # imagemagick tools; 2) via the MiniMagick gem
6
+ class Magicko
7
+ def initialize(path)
8
+ @path = path
9
+ @cmd = []
10
+ end
11
+
12
+ def width
13
+ img_size[0]
14
+ end
15
+
16
+ def height
17
+ img_size[1]
18
+ end
19
+
20
+ # registered_bytes returns an array of the same size as the original image,
21
+ # but with pixels stretched out based on the passed perspective points
22
+ # (i.e. the centers of the four registration marks)
23
+ # pp: a hash in the form of pp[:tl][:x], pp[:tl][:y], etc.
24
+ def registered_bytes(pp)
25
+ read_bytes "-distort Perspective '#{pps pp}'"
26
+ end
27
+
28
+ # def rm_patch(coord, blur_factor, dilate_factor)
29
+ def rm_patch(c, blr=0, dlt=0)
30
+ b = blr==0 ? '' : " -blur #{blr*3}x#{blr}"
31
+ d = dlt==0 ? '' : " -morphology Dilate Octagon:#{dlt}"
32
+ read_bytes "-crop #{c.cropper}#{b}#{d}"
33
+ end
34
+
35
+ # MiniMagick stuff
36
+
37
+ def highlight_cells(coords)
38
+ @cmd << [:stroke, 'none']
39
+ @cmd << [:fill, 'rgba(255, 255, 0, 0.3)']
40
+ coords.each do |c|
41
+ @cmd << [:draw, "roundrectangle #{c.choice_cell}"]
42
+ end
43
+ end
44
+
45
+ def outline(coords)
46
+ @cmd << [:stroke, 'green']
47
+ @cmd << [:strokewidth, '2']
48
+ @cmd << [:fill, 'none']
49
+ coords.each do |c|
50
+ @cmd << [:draw, "roundrectangle #{c.choice_cell}"]
51
+ end
52
+ end
53
+
54
+ def cross(coords)
55
+ @cmd << [:stroke, 'red']
56
+ @cmd << [:strokewidth, '3']
57
+ coords.each do |c|
58
+ @cmd << [:draw, "line #{c.cross1}"]
59
+ @cmd << [:draw, "line #{c.cross2}"]
60
+ end
61
+ end
62
+
63
+ def plus(x, y, l)
64
+ @cmd << [:stroke, 'red']
65
+ @cmd << [:strokewidth, 1]
66
+ pts = [ x-l, y, x+l, y ].join ' '
67
+ @cmd << [:draw, "line #{pts}"]
68
+ pts = [ x, y-l, x, y+l ].join ' '
69
+ @cmd << [:draw, "line #{pts}"]
70
+ end
71
+
72
+ def highlight_area(c)
73
+ @cmd << [:fill, 'none']
74
+ @cmd << [:stroke, 'yellow']
75
+ @cmd << [:strokewidth, 3]
76
+ @cmd << [:draw, "rectangle #{c.rect_points}"]
77
+ end
78
+
79
+ def highlight_rect(areas)
80
+ return if areas.empty?
81
+ @cmd << [:fill, 'none']
82
+ @cmd << [:stroke, 'yellow']
83
+ @cmd << [:strokewidth, 3]
84
+ areas.each do |c|
85
+ @cmd << [:draw, "rectangle #{c.rect_points}"]
86
+ end
87
+ end
88
+
89
+ def join(p)
90
+ @cmd << [:fill, 'none']
91
+ @cmd << [:stroke, 'green']
92
+ @cmd << [:strokewidth, 3]
93
+ pts = [
94
+ p[0][:x], p[0][:y],
95
+ p[1][:x], p[1][:y],
96
+ p[2][:x], p[2][:y],
97
+ p[3][:x], p[3][:y]
98
+ ].join ' '
99
+ @cmd << [:draw, "polygon #{pts}"]
100
+ end
101
+
102
+ def write(fname, reg)
103
+ if fname
104
+ MiniMagick::Tool::Convert.new(whiny: false) do |img|
105
+ img << @path
106
+ exec_mm_cmd img, reg
107
+ img << fname
108
+ end
109
+ else
110
+ MiniMagick::Tool::Mogrify.new(whiny: false) do |img|
111
+ img << @path
112
+ exec_mm_cmd img, reg
113
+ end
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ # calling imagemagick and capturing the converted image
120
+ # into an array of bytes
121
+ def read_bytes(params=nil)
122
+ s = "|convert #{@path} #{params} gray:-"
123
+ IO.read(s).unpack 'C*'
124
+ end
125
+
126
+ def exec_mm_cmd(c, pp)
127
+ c.distort(:perspective, pps(pp)) if pp
128
+ @cmd.each { |cmd| c.send(*cmd) }
129
+ end
130
+
131
+
132
+ # perspective points: brings the found registration area centers to the
133
+ # original image boundaries; the result is that the registered image is
134
+ # somewhat stretched, which should be okay
135
+ def pps(pp)
136
+ [
137
+ pp[:tl][:x], pp[:tl][:y], 0, 0,
138
+ pp[:tr][:x], pp[:tr][:y], width, 0,
139
+ pp[:br][:x], pp[:br][:y], width, height,
140
+ pp[:bl][:x], pp[:bl][:y], 0, height
141
+ ].join ' '
142
+ end
143
+
144
+ def img_size
145
+ @img_size ||= begin
146
+ s = "|identify -format '%w,%h' #{@path}"
147
+ IO.read(s).split(',').map(&:to_i)
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ # def patch(shape: nil, wid: width, hei: height)
154
+ # s = "|convert #{@path} #{shape} gray:-"
155
+ # bytes = IO.read(s).unpack 'C*'
156
+ # NPatch.new bytes, wid, hei
157
+ # end
158
+
159
+ # # raw_patch returns an array containing the pixels of the original image
160
+ # def raw_patch
161
+ # @raw_pixels ||= patch
162
+ # end