kvg_character_recognition 0.1.2 → 0.1.3

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
2
  SHA1:
3
- metadata.gz: 2f5bd00df050f48f3ee955daaa966fd04b10a5f3
4
- data.tar.gz: bb45a8db8023fb37f4f929a673f1f811756540fa
3
+ metadata.gz: d60d7f44902345773b5fff377783a490a41c7f06
4
+ data.tar.gz: 6b8e90de99d64c80ce576f969114cd02432fea58
5
5
  SHA512:
6
- metadata.gz: ae3a13b9370276c1714f19acebcf856cff091c27725514ae8f0a75f43ef754576d46bc69b7804d1203db5216524d0841e82b2deac7f29729ad5ebfe55441d080
7
- data.tar.gz: 057e9896548709b6fd4187ffcf8d576b2d49fa0fb19f6b1cc3909e75befde49149aa934d54ea9c6bbab48d394f4fdf708404287ec6eae6857dab3b53b7a09650
6
+ metadata.gz: 0ae3dda7c2114311a4613ed39b6e48e2e13a4b51619fb791c9476a0070a0dd49e60468fd63d7a7cb660ea52ea803e05cef48fbdd61f0c7a23c5c74141a54fc47
7
+ data.tar.gz: 6c43c6da5be2c25b6cf75ed364de496f8f87fb6f9bc0d8e2aaa0161e0e0ac159d2bdcf8805a36ff77f0ec52eab85fee697a093635a530dbfd779e7163f00f029
@@ -26,12 +26,11 @@ Gem::Specification.new do |spec|
26
26
  raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
27
27
  end
28
28
 
29
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f == 'kvg_character_recognition-0.1.1.gem' || f.match(%r{^(test|spec|features)/}) }
29
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f == 'kvg_character_recognition-0.1.2.gem' || f.match(%r{^(test|spec|features)/}) }
30
30
  spec.bindir = "exe"
31
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
32
  spec.require_paths = ["lib"]
33
33
 
34
- spec.add_dependency "parallel"
35
34
  spec.add_dependency "nokogiri"
36
35
  spec.add_dependency "bundler", "~> 1.10"
37
36
  spec.add_development_dependency "rake", "~> 10.0"
@@ -10,9 +10,8 @@ module KvgCharacterRecognition
10
10
  size: 109, #fixed canvas size of kanjivg data
11
11
  downsample_interval: 4,
12
12
  interpolate_distance: 0.8,
13
- direction_grid: 15,
14
- smoothed_heatmap_grid: 20,
15
- significant_points_heatmap_grid: 3
13
+ heatmap_coarse_grid: 17,
14
+ heatmap_granular_grid: 17,
16
15
  }
17
16
  VALID_KEYS = CONFIG.keys
18
17
 
@@ -16,8 +16,8 @@ module KvgCharacterRecognition
16
16
  end
17
17
  end
18
18
 
19
- def characters_in_stroke_range range
20
- @data.select { |character| range === character[:number_of_strokes] }
19
+ def characters_in_range point_range, stroke_range
20
+ @data.select { |character| point_range === character[:number_of_points] && stroke_range === character[:number_of_strokes] }
21
21
  end
22
22
 
23
23
  def store character
@@ -9,7 +9,7 @@ module KvgCharacterRecognition
9
9
  #Params:
10
10
  #+stroke+:: array of points i.e [[x1, y1], [x2, y2] ...]
11
11
  def self.smooth stroke
12
- weights = [1,3,1]
12
+ weights = [1,1,2,1,1]
13
13
  offset = weights.length / 2
14
14
  wsum = weights.inject{ |sum, x| sum + x}
15
15
 
@@ -31,22 +31,44 @@ module KvgCharacterRecognition
31
31
  end
32
32
 
33
33
  #This method executes different preprocessing steps
34
- #0.Normalize strokes to the size 109x109 and center the coordinates using bi moment normalization method
34
+ #strokes are normalized
35
35
  #1.Smooth strokes if set to true
36
36
  #2.Interpolate points by given distance, in order to equalize the sample rate of input and template
37
37
  #3.Downsample by given interval
38
38
  def self.preprocess strokes, interpolate_distance=0.8, downsample_interval=4, smooth=true
39
- means, diffs = means_and_diffs(strokes)
40
- #normalize strokes
41
- strokes = bi_moment_normalize(means, diffs, strokes)
42
-
43
39
  strokes.map do |stroke|
44
40
  stroke = smooth(stroke) if smooth
45
- interpolated = interpolate(stroke, interpolate_distance)
41
+ interpolated = smooth(interpolate(stroke, interpolate_distance))
46
42
  downsample(interpolated, downsample_interval)
47
43
  end
48
44
  end
49
45
 
46
+ # accumulated histogram needed by line density normalization
47
+ def self.accumulated_histogram points
48
+ grids = CONFIG[:size] + 1
49
+ h_x = []
50
+ h_y = []
51
+ (0..grids).each do |i|
52
+ h_x[i] = points.count{ |p| p[0].round == i }
53
+ h_y[i] = points.count{ |p| p[1].round == i }
54
+ h_x[i] = h_x[i] + h_x[i - 1] if i > 0
55
+ h_y[i] = h_y[i] + h_y[i - 1] if i > 0
56
+ end
57
+
58
+ [h_x, h_y]
59
+ end
60
+
61
+ # line density normalization
62
+ def self.line_density_normalize strokes
63
+ points = strokes.flatten(1)
64
+ h_x, h_y = accumulated_histogram points
65
+ strokes.map do |stroke|
66
+ stroke.map do |point|
67
+ [(CONFIG[:size] * h_x[point[0].round] / points.length.to_f).round(2), (CONFIG[:size] * h_y[point[1].round] / points.length.to_f).round(2)]
68
+ end
69
+ end
70
+ end
71
+
50
72
  #This method calculates means and diffs of x and y coordinates in the strokes
51
73
  #The return values are used in the normalization step
52
74
  #means, diffs = means_and_diffs strokes
@@ -59,16 +81,28 @@ module KvgCharacterRecognition
59
81
  #means = [x_c, y_c]
60
82
  means = sums.map{ |sum| (sum / points.length.to_f).round(2) }
61
83
 
62
- diffs = points.inject([[], []]){ |acc, point| acc = [acc[0] << point[0] - means[0], acc[1] << point[1] - means[1]] }
63
- [means, diffs]
84
+ #for slant correction
85
+ diff_x = []
86
+ diff_y = []
87
+ u11 = 0
88
+ u02 = 0
89
+ points.each do |point|
90
+ diff_x << point[0] - means[0]
91
+ diff_y << point[1] - means[1]
92
+
93
+ u11 += (point[0] - means[0]) * (point[1] - means[1])
94
+ u02 += (point[1] - means[1])**2
95
+ end
96
+ [means, [diff_x, diff_y], -1 * u11 / u02]
64
97
  end
65
98
 
66
99
  #This methods normalizes the strokes using bi moment
67
100
  #Params:
68
101
  #+strokes+:: [[[x1, y1], [x2, y2], ...], [[x1, y1], ...]]
69
- #+means+:: [x_c, y_c]
70
- #+diffs+:: [d_x, d_y]; d_x = [d1, d2, ...]
71
- def self.bi_moment_normalize means, diffs, strokes
102
+ #+slant_correction+:: boolean whether a slant correction should be performed
103
+ #returns normed_strokes, normed_strokes_with_slant_correction
104
+ def self.bi_moment_normalize strokes
105
+ means, diffs, slant_slope = means_and_diffs strokes
72
106
 
73
107
  #calculating delta values
74
108
  delta = Proc.new do |diff, operator|
@@ -87,65 +121,44 @@ module KvgCharacterRecognition
87
121
  end
88
122
 
89
123
  new_strokes = []
124
+ new_strokes_with_slant = []
125
+
90
126
  strokes.each do |stroke|
91
127
  new_stroke = []
128
+ new_stroke_slant = []
92
129
  stroke.each do |point|
93
- if point[0] - means[0] >= 0
94
- new_x = ( CONFIG[:size] * (point[0] - means[0]) / (4 * Math.sqrt(delta.call(diffs[0], :>=))).round(2) ) + CONFIG[:size]/2
130
+ x = point[0]
131
+ y = point[1]
132
+ x_slant = x + (y - means[1]) * slant_slope
133
+
134
+ if x - means[0] >= 0
135
+ new_x = ( CONFIG[:size] * (x - means[0]) / (4 * Math.sqrt(delta.call(diffs[0], :>=))).round(2) ) + CONFIG[:size]/2
95
136
  else
96
- new_x = ( CONFIG[:size] * (point[0] - means[0]) / (4 * Math.sqrt(delta.call(diffs[0], :<))).round(2) ) + CONFIG[:size]/2
137
+ new_x = ( CONFIG[:size] * (x - means[0]) / (4 * Math.sqrt(delta.call(diffs[0], :<))).round(2) ) + CONFIG[:size]/2
97
138
  end
98
- if point[1] - means[1] >= 0
99
- new_y = ( CONFIG[:size] * (point[1] - means[1]) / (4 * Math.sqrt(delta.call(diffs[1], :>=))).round(2) ) + CONFIG[:size]/2
139
+ if x_slant - means[0] >= 0
140
+ new_x_slant = ( CONFIG[:size] * (x_slant - means[0]) / (4 * Math.sqrt(delta.call(diffs[0], :>=))).round(2) ) + CONFIG[:size]/2
141
+ else
142
+ new_x_slant = ( CONFIG[:size] * (x_slant - means[0]) / (4 * Math.sqrt(delta.call(diffs[0], :<))).round(2) ) + CONFIG[:size]/2
143
+ end
144
+
145
+ if y - means[1] >= 0
146
+ new_y = ( CONFIG[:size] * (y - means[1]) / (4 * Math.sqrt(delta.call(diffs[1], :>=))).round(2) ) + CONFIG[:size]/2
100
147
  else
101
- new_y = ( CONFIG[:size] * (point[1] - means[1]) / (4 * Math.sqrt(delta.call(diffs[1], :<))).round(2) ) + CONFIG[:size]/2
148
+ new_y = ( CONFIG[:size] * (y - means[1]) / (4 * Math.sqrt(delta.call(diffs[1], :<))).round(2) ) + CONFIG[:size]/2
102
149
  end
103
150
 
104
151
  if new_x >= 0 && new_x <= CONFIG[:size] && new_y >= 0 && new_y <= CONFIG[:size]
105
152
  new_stroke << [new_x.round(3), new_y.round(3)]
106
153
  end
107
- end
108
- new_strokes << new_stroke unless new_stroke.empty?
109
- end
110
- new_strokes
111
- end
112
-
113
- #This method returns the significant points of a given character
114
- #Significant points are:
115
- #- Start and end point of a stroke
116
- #- Point on curve or edge
117
- #To determine whether a point is on curve or edge, we take the 2 adjacent points and calculate the angle between the 2 vectors
118
- #If the angle is smaller than 150 degree, then the point should be on curve or edge
119
- def self.significant_points strokes
120
- points = []
121
- strokes.each_with_index do |stroke, i|
122
- points << stroke[0]
123
-
124
- #collect edge points
125
- #determine whether a point is an edge point by the internal angle between vector P_i-1 - P_i and P_i+1 - P_i
126
- pre = stroke[0]
127
- (1..(stroke.length - 1)).each do |j|
128
- current = stroke[j]
129
- nex = stroke[j+1]
130
- if nex
131
- v1 = [pre[0] - current[0], pre[1] - current[1]]
132
- v2 = [nex[0] - current[0], nex[1] - current[1]]
133
- det = v1[0] * v2[1] - (v2[0] * v1[1])
134
- dot = v1[0] * v2[0] + (v2[1] * v1[1])
135
- angle = Math.atan2(det, dot) / (Math::PI / 180)
136
-
137
- if angle.abs < 150
138
- #current point is on a curve or an edge
139
- points << current
140
- end
154
+ if new_x_slant >= 0 && new_x_slant <= CONFIG[:size] && new_y >= 0 && new_y <= CONFIG[:size]
155
+ new_stroke_slant << [new_x_slant.round(3), new_y.round(3)]
141
156
  end
142
- pre = current
143
157
  end
144
-
145
- points << stroke[stroke.length - 1]
158
+ new_strokes << new_stroke unless new_stroke.empty?
159
+ new_strokes_with_slant << new_stroke_slant unless new_stroke_slant.empty?
146
160
  end
147
-
148
- points
161
+ [new_strokes, new_strokes_with_slant]
149
162
  end
150
163
 
151
164
  #This method interpolates points into a stroke with given distance
@@ -166,7 +179,7 @@ module KvgCharacterRecognition
166
179
 
167
180
  #calculate new point coordinate
168
181
  new_point = []
169
- if point[0] == current[0] # x2 == x1
182
+ if point[0].round(2) == current[0].round(2) # x2 == x1
170
183
  if point[1] > current[1] # y2 > y1
171
184
  new_point = [current[0], current[1] + d]
172
185
  else # y2 < y1
@@ -183,9 +196,11 @@ module KvgCharacterRecognition
183
196
  end
184
197
 
185
198
  new_point = new_point.map{ |num| num.round(2) }
186
- new_stroke << new_point
199
+ if current != new_point
200
+ new_stroke << new_point
187
201
 
188
- current = new_point
202
+ current = new_point
203
+ end
189
204
  last_index += ((index - last_index) / 2).floor
190
205
  index = last_index + 1
191
206
  end
@@ -199,5 +214,67 @@ module KvgCharacterRecognition
199
214
  def self.downsample stroke, interval=3
200
215
  stroke.each_slice(interval).map(&:first)
201
216
  end
217
+
218
+ #This methods generates a heatmap for the given character pattern
219
+ #A heatmap divides the input character pattern(image of the character) into nxn grids
220
+ #We count the points in each grid and store the number in a map
221
+ #The map array can be used as feature
222
+ #Params:
223
+ #+points+:: flattened strokes i.e. [[x1, y1], [x2, y2]...] because the seperation of points in strokes is irrelevant in this case
224
+ #+grid+:: number of grids
225
+ def self.heatmap points, grid, size
226
+
227
+ grid_size = size / grid.to_f
228
+
229
+ map = Map.new grid, grid, 0
230
+
231
+ #fill the heatmap
232
+ points.each do |point|
233
+ if point[0] < size && point[1] < size
234
+ x_i = (point[0] / grid_size).floor if point[0] < size
235
+ y_i = (point[1] / grid_size).floor if point[1] < size
236
+
237
+ map[y_i, x_i] += (1 / points.length.to_f).round(4)
238
+ end
239
+ end
240
+
241
+ map
242
+ end
243
+ #This method smooths a heatmap using spatial_weight_filter technique
244
+ #but instead of taking every 2nd grid, it processes every grid and stores the average of the weighted sum of adjacent grids
245
+ #Params:
246
+ #+map+:: a heatmap
247
+ def self.smooth_heatmap map
248
+ grid = map.size
249
+ #map is a heatmap
250
+ new_map = Map.new(grid, grid, 0)
251
+
252
+ (0..(grid - 1)).each do |i|
253
+ (0..(grid - 1)).each do |j|
254
+ #weights alternative
255
+ # = [1/16, 2/16, 1/16];
256
+ # [2/16, 4/16, 2/16];
257
+ # [1/16, 2/16, 1/16]
258
+ #
259
+ #weights = [1/9, 1/9, 1/9];
260
+ # [1/9, 1/9, 1/9];
261
+ # [1/9, 1/9, 1/9]
262
+ #
263
+ w11 = (0..(grid-1)).cover?(i+1) && (0..(grid-1)).cover?(j-1)? map[i+1,j-1] * 1 / 9.0 : 0
264
+ w12 = (0..(grid-1)).cover?(i+1) && (0..(grid-1)).cover?(j)? map[i+1,j] * 1 / 9.0 : 0
265
+ w13 = (0..(grid-1)).cover?(i+1) && (0..(grid-1)).cover?(j+1)? map[i+1,j+1] * 1 / 9.0 : 0
266
+ w21 = (0..(grid-1)).cover?(i) && (0..(grid-1)).cover?(j-1)? map[i,j-1] * 1 / 9.0 : 0
267
+ w22 = (0..(grid-1)).cover?(i) && (0..(grid-1)).cover?(j)? map[i,j] * 1 / 9.0 : 0
268
+ w23 = (0..(grid-1)).cover?(i) && (0..(grid-1)).cover?(j+1)? map[i,j+1] * 1 / 9.0 : 0
269
+ w31 = (0..(grid-1)).cover?(i-1) && (0..(grid-1)).cover?(j-1)? map[i-1,j-1] * 1 / 9.0 : 0
270
+ w32 = (0..(grid-1)).cover?(i-1) && (0..(grid-1)).cover?(j)? map[i-1,j] * 1 / 9.0 : 0
271
+ w33 = (0..(grid-1)).cover?(i-1) && (0..(grid-1)).cover?(j+1)? map[i-1,j+1] * 1 / 9.0 : 0
272
+
273
+ new_map[i,j] = (w11 + w12 + w13 + w21 + w22 + w23 + w31 + w32 + w33).round(4)
274
+ end
275
+ end
276
+
277
+ new_map
278
+ end
202
279
  end
203
280
  end
@@ -1,34 +1,16 @@
1
1
  require 'matrix'
2
- require 'parallel'
3
2
  module KvgCharacterRecognition
4
3
  #This class contains methods calculating similarity scores between input pattern and template patterns
5
4
  module Recognizer
6
- @thread_count = 10
7
5
 
8
6
  #This method selects all templates from the database which should be further examined
9
- #It filtered out those characters with a too great difference in number of strokes to the input character
10
- def self.select_templates strokes, datastore
11
- min = strokes.count <= 5 ? strokes.count : strokes.count - 5
12
- max = strokes.count + 10
13
- datastore.characters_in_stroke_range(min..max)
14
- end
15
-
16
- #This method uses heatmap of significant points to coarse recognize the input pattern
17
- #Params:
18
- #+strokes+:: strokes should be preprocessed
19
- #+datastore+:: JSONDatastore or custom datastore type having method characters_in_stroke_range(min..max)
20
- def self.coarse_recognize strokes, datastore
21
- heatmap = FeatureExtractor.heatmap(Preprocessor.significant_points(strokes), CONFIG[:significant_points_heatmap_grid], CONFIG[:size]).to_a
22
-
23
- templates = select_templates strokes, datastore
24
-
25
- # Use threads to accelerate the process
26
- Parallel.map(templates, in_threads: @thread_count) do |candidate|
27
- candidate_heatmap = candidate[:heatmap_significant_points].split(",").map(&:to_f)
28
-
29
- score = Math.euclidean_distance(heatmap, candidate_heatmap)
30
- [score.round(3), candidate]
31
- end
7
+ #It filtered out those characters with a too great difference in number of points and strokes to the input character
8
+ def self.select_templates character, datastore
9
+ p_min = character.number_of_points - 100
10
+ p_max = character.number_of_points + 100
11
+ s_min = character.number_of_strokes - 12
12
+ s_max = character.number_of_strokes + 12
13
+ datastore.characters_in_range(p_min..p_max, s_min..s_max)
32
14
  end
33
15
 
34
16
  #This method calculates similarity scores which is an average of the somehow weighted sum of the euclidean distance of
@@ -38,28 +20,18 @@ module KvgCharacterRecognition
38
20
  #+strokes+:: strokes are not preprocessed
39
21
  #+datastore+:: JSONDatastore or custom datastore type having method characters_in_stroke_range(min..max)
40
22
  def self.scores strokes, datastore
41
- #preprocess strokes
42
- #with smoothing
43
- strokes = Preprocessor.preprocess(strokes, CONFIG[:interpolate_distance], CONFIG[:downsample_interval], true)
44
-
45
- #feature extraction
46
- directions = Matrix.columns(FeatureExtractor.spatial_weight_filter(FeatureExtractor.directional_feature_densities(strokes, CONFIG[:direction_grid])).to_a).to_a
47
- heatmap_smoothed = FeatureExtractor.smooth_heatmap(FeatureExtractor.heatmap(strokes.flatten(1), CONFIG[:smoothed_heatmap_grid], CONFIG[:size])).to_a
23
+ character = Trainer::Character.new(strokes, nil)
24
+ templates = select_templates character, datastore
48
25
 
49
- #dump half of the templates after coarse recognition
50
- #collection is in the form [[score, c1], [score, c2] ...]
51
- collection = coarse_recognize(strokes, datastore).sort{ |a, b| a[0] <=> b[0] }
26
+ scores = templates.map do |cand|
52
27
 
53
- scores = Parallel.map(collection.take(collection.count / 2)) do |cand|
54
- direction_score = (Math.euclidean_distance(directions[0], cand[1][:direction_e1].split(",").map(&:to_f)) +
55
- Math.euclidean_distance(directions[1], cand[1][:direction_e2].split(",").map(&:to_f)) +
56
- Math.euclidean_distance(directions[2], cand[1][:direction_e3].split(",").map(&:to_f)) +
57
- Math.euclidean_distance(directions[3], cand[1][:direction_e4].split(",").map(&:to_f)) ) / 4
28
+ heatmap_bi_moment_score = Math.manhattan_distance(cand[:heatmap_smoothed_granular], character.heatmap_smoothed_granular)
29
+ heatmap_line_density_score = Math.manhattan_distance(cand[:heatmap_smoothed_coarse], character.heatmap_smoothed_coarse)
30
+ heatmap_bi_moment_slant_score = Math.manhattan_distance(cand[:heatmap_smoothed_granular_with_slant], character.heatmap_smoothed_granular_with_slant)
31
+ heatmap_line_density_slant_score = Math.manhattan_distance(cand[:heatmap_smoothed_coarse_with_slant], character.heatmap_smoothed_coarse_with_slant)
58
32
 
59
- heatmap_score = Math.euclidean_distance(heatmap_smoothed, cand[1][:heatmap_smoothed].split(",").map(&:to_f))
60
33
 
61
- mix = (direction_score / 100) + heatmap_score
62
- [mix/2, cand[1]]
34
+ [[heatmap_bi_moment_score, heatmap_line_density_score, heatmap_bi_moment_slant_score, heatmap_line_density_slant_score].min, cand]
63
35
  end
64
36
 
65
37
  scores.sort{ |a, b| a[0] <=> b[0] }
@@ -1,5 +1,43 @@
1
1
  module KvgCharacterRecognition
2
2
  module Trainer
3
+ class Character
4
+ attr_accessor :value,
5
+ :number_of_strokes,
6
+ :number_of_points,
7
+ :strokes,
8
+ :line_density_preprocessed_strokes,
9
+ :line_density_preprocessed_strokes_with_slant,
10
+ :bi_moment_preprocessed_strokes,
11
+ :bi_moment_preprocessed_strokes_with_slant,
12
+ :heatmap_smoothed_coarse,
13
+ :heatmap_smoothed_granular,
14
+ :heatmap_smoothed_coarse_with_slant,
15
+ :heatmap_smoothed_granular_with_slant
16
+ def initialize strokes, value
17
+ @value = value
18
+ @strokes = strokes
19
+ @number_of_strokes = @strokes.count
20
+ smooth = @value ? false : true
21
+ bi_moment_normalized_strokes, bi_moment_normalized_strokes_with_slant = Preprocessor.bi_moment_normalize(@strokes)
22
+ @bi_moment_preprocessed_strokes = Preprocessor.preprocess(bi_moment_normalized_strokes, CONFIG[:interpolate_distance], CONFIG[:downsample_interval], smooth)
23
+ @bi_moment_preprocessed_strokes_with_slant = Preprocessor.preprocess(bi_moment_normalized_strokes_with_slant, CONFIG[:interpolate_distance], CONFIG[:downsample_interval], smooth)
24
+
25
+ @number_of_points = @bi_moment_preprocessed_strokes.flatten(1).count
26
+
27
+ line_density_normalized_strokes = Preprocessor.line_density_normalize(@bi_moment_preprocessed_strokes)
28
+ @line_density_preprocessed_strokes = Preprocessor.preprocess(line_density_normalized_strokes, CONFIG[:interpolate_distance], CONFIG[:downsample_interval], true)
29
+ line_density_normalized_strokes_with_slant = Preprocessor.line_density_normalize(@bi_moment_preprocessed_strokes_with_slant)
30
+ @line_density_preprocessed_strokes_with_slant = Preprocessor.preprocess(line_density_normalized_strokes_with_slant, CONFIG[:interpolate_distance], CONFIG[:downsample_interval], true)
31
+
32
+ @heatmap_smoothed_coarse = Preprocessor.smooth_heatmap(Preprocessor.heatmap(@line_density_preprocessed_strokes.flatten(1), CONFIG[:heatmap_coarse_grid], CONFIG[:size])).to_a
33
+ @heatmap_smoothed_granular = Preprocessor.smooth_heatmap(Preprocessor.heatmap(@bi_moment_preprocessed_strokes.flatten(1), CONFIG[:heatmap_granular_grid], CONFIG[:size])).to_a
34
+
35
+ @heatmap_smoothed_coarse_with_slant = Preprocessor.smooth_heatmap(Preprocessor.heatmap(@line_density_preprocessed_strokes_with_slant.flatten(1), CONFIG[:heatmap_coarse_grid], CONFIG[:size])).to_a
36
+ @heatmap_smoothed_granular_with_slant = Preprocessor.smooth_heatmap(Preprocessor.heatmap(@bi_moment_preprocessed_strokes_with_slant.flatten(1), CONFIG[:heatmap_granular_grid], CONFIG[:size])).to_a
37
+
38
+ end
39
+ end
40
+
3
41
  #This method populates the datastore with parsed template patterns from the kanjivg file in xml format
4
42
  #Params:
5
43
  #+xml+:: download the latest xml release from https://github.com/KanjiVG/kanjivg/releases
@@ -18,31 +56,8 @@ module KvgCharacterRecognition
18
56
  #--------------
19
57
  #parse strokes
20
58
  strokes = kanji.xpath("g//path").map{|p| p.attributes["d"].value }.map{ |stroke| KvgParser::Stroke.new(stroke).to_a }
21
- #strokes in the format [[[x1, y1], [x2, y2] ...], [[x2, y2], [x3, y3] ...], ...]
22
- strokes = Preprocessor.preprocess(strokes, CONFIG[:interpolate_distance], CONFIG[:downsample_interval], false)
23
-
24
- #serialize strokes
25
- serialized = strokes.map.with_index do |stroke, i|
26
- stroke.map{ |p| [i, p[0], p[1]] }
27
- end
28
-
29
- points = strokes.flatten(1)
30
-
31
- #Feature Extraction
32
- #--------------
33
- #20x20 heatmap smoothed
34
- heatmap_smoothed = FeatureExtractor.smooth_heatmap(FeatureExtractor.heatmap(points, CONFIG[:smoothed_heatmap_grid], CONFIG[:size]))
35
-
36
- #directional feature densities
37
- #transposed from Mx4 to 4xM
38
- direction = Matrix.columns(FeatureExtractor.spatial_weight_filter(FeatureExtractor.directional_feature_densities(strokes, CONFIG[:direction_grid])).to_a).to_a
39
-
40
- #significant points
41
- significant_points = Preprocessor.significant_points(strokes)
42
-
43
- #3x3 heatmap of significant points for coarse recognition
44
- heatmap_significant_points = FeatureExtractor.heatmap(significant_points, CONFIG[:significant_points_heatmap_grid], CONFIG[:size])
45
59
 
60
+ chr = Character.new strokes, value
46
61
 
47
62
  #Store to database
48
63
  #--------------
@@ -50,13 +65,11 @@ module KvgCharacterRecognition
50
65
  value: value,
51
66
  codepoint: codepoint.hex,
52
67
  number_of_strokes: strokes.count,
53
- serialized_strokes: serialized.join(","),
54
- direction_e1: direction[0].join(","),
55
- direction_e2: direction[1].join(","),
56
- direction_e3: direction[2].join(","),
57
- direction_e4: direction[3].join(","),
58
- heatmap_smoothed: heatmap_smoothed.to_a.join(","),
59
- heatmap_significant_points: heatmap_significant_points.to_a.join(",")
68
+ number_of_points: chr.number_of_points,
69
+ heatmap_smoothed_coarse: chr.heatmap_smoothed_coarse,
70
+ heatmap_smoothed_granular: chr.heatmap_smoothed_granular,
71
+ heatmap_smoothed_coarse_with_slant: chr.heatmap_smoothed_coarse_with_slant,
72
+ heatmap_smoothed_granular_with_slant: chr.heatmap_smoothed_granular_with_slant
60
73
  }
61
74
 
62
75
  datastore.store character
@@ -10,6 +10,14 @@ module Math
10
10
  end
11
11
  Math.sqrt( sum_of_squares )
12
12
  end
13
+
14
+ def self.manhattan_distance(p1, p2)
15
+ sum = 0
16
+ p1.each_with_index do |p1_coord,index|
17
+ sum += (p1_coord - p2[index]).abs
18
+ end
19
+ sum
20
+ end
13
21
  end
14
22
 
15
23
  module KvgCharacterRecognition
@@ -1,3 +1,3 @@
1
1
  module KvgCharacterRecognition
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kvg_character_recognition
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jiayi Zheng
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-01-17 00:00:00.000000000 Z
11
+ date: 2016-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: parallel
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: nokogiri
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -118,7 +104,6 @@ files:
118
104
  - kvg_character_recognition.gemspec
119
105
  - lib/kvg_character_recognition.rb
120
106
  - lib/kvg_character_recognition/datastore.rb
121
- - lib/kvg_character_recognition/feature_extractor.rb
122
107
  - lib/kvg_character_recognition/preprocessor.rb
123
108
  - lib/kvg_character_recognition/recognizer.rb
124
109
  - lib/kvg_character_recognition/trainer.rb
@@ -1,168 +0,0 @@
1
- require 'matrix'
2
- module KvgCharacterRecognition
3
- #This class contains a collection of methods for extracting useful features
4
- class FeatureExtractor
5
-
6
- #This methods generates a heatmap for the given character pattern
7
- #A heatmap divides the input character pattern(image of the character) into nxn grids
8
- #We count the points in each grid and store the number in a map
9
- #The map array can be used as feature
10
- #Params:
11
- #+points+:: flattened strokes i.e. [[x1, y1], [x2, y2]...] because the seperation of points in strokes is irrelevant in this case
12
- #+grid+:: number of grids
13
- def self.heatmap points, grid, size
14
-
15
- grid_size = size / grid.to_f
16
-
17
- map = Map.new grid, grid, 0
18
-
19
- #fill the heatmap
20
- points.each do |point|
21
- if point[0] < size && point[1] < size
22
- x_i = (point[0] / grid_size).floor if point[0] < size
23
- y_i = (point[1] / grid_size).floor if point[1] < size
24
-
25
- map[y_i, x_i] = map[y_i, x_i] + 1
26
- end
27
- end
28
-
29
- map
30
- end
31
-
32
- #This method calculates the directional feature densities and stores them in a map
33
- #The process and algorithm is described in the paper "On-line Recognition of Freely Handwritten Japanese Characters Using Directional Feature Densities" by Akinori Kawamura and co.
34
- #Params:
35
- #+strokes+:: [[[x1, y1], [x2, y2] ...], [[x1, y1], ...]]]
36
- #+grid+:: number of grids in which the input character pattern should be seperated. Default is 15 as in the paper
37
- def self.directional_feature_densities strokes, grid
38
- #initialize a map for storing the weights in each directional space
39
- map = Map.new grid, grid, [0, 0, 0, 0]
40
-
41
- #step width
42
- step = CONFIG[:size] / grid.to_f
43
-
44
- strokes.each do |stroke|
45
- current_p = stroke[0]
46
- stroke.each do |point|
47
- next if point == current_p
48
- #map current point coordinate to map index
49
- #i_x = xth column
50
- #i_y = yth row
51
- i_x = (current_p[0] / step).floor
52
- i_y = (current_p[1] / step).floor
53
-
54
- #direction vector V_ij = P_ij+1 - P_ij
55
- v = [point[0] - current_p[0], point[1] - current_p[1]]
56
- #store the sum of decomposed direction vectors in the corresponding grid
57
- decomposed = decompose(v)
58
- map[i_y, i_x] = [map[i_y, i_x][0] + decomposed[0],
59
- map[i_y, i_x][1] + decomposed[1],
60
- map[i_y, i_x][2] + decomposed[2],
61
- map[i_y, i_x][3] + decomposed[3]]
62
- end
63
- end
64
- map
65
- end
66
-
67
- #This method is a helper method for calculating directional feature density
68
- #which decomposes the direction vector into predefined direction spaces
69
- #- e1: [1, 0]
70
- #- e2: [1/sqrt(2), 1/sqrt(2)]
71
- #- e3: [0, 1]
72
- #- e4: [-1/sqrt(2), 1/sqrt(2)]
73
- #Params:
74
- #+v+:: direction vector of 2 adjacent points V_ij = P_ij+1 - P_ij
75
- def self.decompose v
76
- e1 = [1, 0]
77
- e2 = [1/Math.sqrt(2), 1/Math.sqrt(2)]
78
- e3 = [0, 1]
79
- e4 = [-1/Math.sqrt(2), 1/Math.sqrt(2)]
80
- #angle between vector v and e1
81
- #det = x1*y2 - x2*y1
82
- #dot = x1*x2 + y1*y2
83
- #atan2(det, dot) in range 0..180 and 0..-180
84
- angle = (Math.atan2(v[1], v[0]) / (Math::PI / 180)).floor
85
- if (0..44).cover?(angle) || (-180..-136).cover?(angle)
86
- decomposed = [(Matrix.columns([e1, e2]).inverse * Vector.elements(v)).to_a, 0, 0].flatten
87
- elsif (45..89).cover?(angle) || (-135..-91).cover?(angle)
88
- decomposed = [0, (Matrix.columns([e2, e3]).inverse * Vector.elements(v)).to_a, 0].flatten
89
- elsif (90..134).cover?(angle) || (-90..-44).cover?(angle)
90
- decomposed = [0, 0, (Matrix.columns([e3, e4]).inverse * Vector.elements(v)).to_a].flatten
91
- elsif (135..179).cover?(angle) || (-45..-1).cover?(angle)
92
- tmp = (Matrix.columns([e4, e1]).inverse * Vector.elements(v)).to_a
93
- decomposed = [tmp[0], 0, 0, tmp[1]]
94
- end
95
-
96
- decomposed
97
- end
98
-
99
- #This methods reduces the dimension of directonal feature densities stored in the map
100
- #It takes every 2nd grid of directional_feature_densities map and stores the average of the weighted sum of adjacent grids around it
101
- #weights = [1/16, 2/16, 1/16];
102
- # [2/16, 4/16, 2/16];
103
- # [1/16, 2/16, 1/16]
104
- #Params:
105
- #+map+:: directional feature densities map i.e. [[e1, e2, e3, e4], [e1, e2, e3, e4] ...] for each grid of input character pattern
106
- def self.spatial_weight_filter map
107
- #default grid should be 15
108
- grid = map.size
109
- new_grid = (grid / 2.0).ceil
110
- new_map = Map.new(new_grid, new_grid, [0, 0, 0, 0])
111
-
112
- (0..(grid - 1)).each_slice(2) do |i, i2|
113
- (0..(grid - 1)).each_slice(2) do |j, j2|
114
- #weights = [1/16, 2/16, 1/16];
115
- # [2/16, 4/16, 2/16];
116
- # [1/16, 2/16, 1/16]
117
- w11 = (0..(grid-1)).cover?(i+1) && (0..(grid-1)).cover?(j-1)? map[i+1,j-1].map{|e| e * 1 / 16.0} : [0, 0, 0, 0]
118
- w12 = (0..(grid-1)).cover?(i+1) && (0..(grid-1)).cover?(j)? map[i+1,j].map{|e| e * 2 / 16.0} : [0, 0, 0, 0]
119
- w13 = (0..(grid-1)).cover?(i+1) && (0..(grid-1)).cover?(j+1)? map[i+1,j+1].map{|e| e * 1 / 16.0} : [0, 0, 0, 0]
120
- w21 = (0..(grid-1)).cover?(i) && (0..(grid-1)).cover?(j-1)? map[i,j-1].map{|e| e * 2 / 16.0} : [0, 0, 0, 0]
121
- w22 = (0..(grid-1)).cover?(i) && (0..(grid-1)).cover?(j)? map[i,j].map{|e| e * 4 / 16.0} : [0, 0, 0, 0]
122
- w23 = (0..(grid-1)).cover?(i) && (0..(grid-1)).cover?(j+1)? map[i,j+1].map{|e| e * 2 / 16.0} : [0, 0, 0, 0]
123
- w31 = (0..(grid-1)).cover?(i-1) && (0..(grid-1)).cover?(j-1)? map[i-1,j-1].map{|e| e * 1 / 16.0} : [0, 0, 0, 0]
124
- w32 = (0..(grid-1)).cover?(i-1) && (0..(grid-1)).cover?(j)? map[i-1,j].map{|e| e * 2 / 16.0} : [0, 0, 0, 0]
125
- w33 = (0..(grid-1)).cover?(i-1) && (0..(grid-1)).cover?(j+1)? map[i-1,j+1].map{|e| e * 1 / 16.0} : [0, 0, 0, 0]
126
-
127
- new_map[i/2,j/2] = [w11[0] + w12[0] + w13[0] + w21[0] + w22[0] + w23[0] + w31[0] + w32[0] + w33[0],
128
- w11[1] + w12[1] + w13[1] + w21[1] + w22[1] + w23[1] + w31[1] + w32[1] + w33[1],
129
- w11[2] + w12[2] + w13[2] + w21[2] + w22[2] + w23[2] + w31[2] + w32[2] + w33[2],
130
- w11[3] + w12[3] + w13[3] + w21[3] + w22[3] + w23[3] + w31[3] + w32[3] + w33[3]]
131
- end
132
- end
133
-
134
- new_map
135
- end
136
-
137
- #This method smooths a heatmap using spatial_weight_filter technique
138
- #but instead of taking every 2nd grid, it processes every grid and stores the average of the weighted sum of adjacent grids
139
- #Params:
140
- #+map+:: a heatmap
141
- def self.smooth_heatmap map
142
- grid = map.size
143
- #map is a heatmap
144
- new_map = Map.new(grid, grid, 0)
145
-
146
- (0..(grid - 1)).each do |i|
147
- (0..(grid - 1)).each do |j|
148
- #weights = [1/16, 2/16, 1/16];
149
- # [2/16, 4/16, 2/16];
150
- # [1/16, 2/16, 1/16]
151
- w11 = (0..(grid-1)).cover?(i+1) && (0..(grid-1)).cover?(j-1)? map[i+1,j-1] * 1 / 16.0 : 0
152
- w12 = (0..(grid-1)).cover?(i+1) && (0..(grid-1)).cover?(j)? map[i+1,j] * 2 / 16.0 : 0
153
- w13 = (0..(grid-1)).cover?(i+1) && (0..(grid-1)).cover?(j+1)? map[i+1,j+1] * 1 / 16.0 : 0
154
- w21 = (0..(grid-1)).cover?(i) && (0..(grid-1)).cover?(j-1)? map[i,j-1] * 2 / 16.0 : 0
155
- w22 = (0..(grid-1)).cover?(i) && (0..(grid-1)).cover?(j)? map[i,j] * 4 / 16.0 : 0
156
- w23 = (0..(grid-1)).cover?(i) && (0..(grid-1)).cover?(j+1)? map[i,j+1] * 2 / 16.0 : 0
157
- w31 = (0..(grid-1)).cover?(i-1) && (0..(grid-1)).cover?(j-1)? map[i-1,j-1] * 1 / 16.0 : 0
158
- w32 = (0..(grid-1)).cover?(i-1) && (0..(grid-1)).cover?(j)? map[i-1,j] * 2 / 16.0 : 0
159
- w33 = (0..(grid-1)).cover?(i-1) && (0..(grid-1)).cover?(j+1)? map[i-1,j+1] * 1 / 16.0 : 0
160
-
161
- new_map[i,j] = w11 + w12 + w13 + w21 + w22 + w23 + w31 + w32 + w33
162
- end
163
- end
164
-
165
- new_map
166
- end
167
- end
168
- end