mork 0.6.0 → 0.7.0

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