kvg_character_recognition 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,37 +1,35 @@
1
- require 'matrix'
2
1
  module KvgCharacterRecognition
3
2
  #This class contains methods calculating similarity scores between input pattern and template patterns
4
3
  module Recognizer
4
+ extend KvgCharacterRecognition::Trainer
5
5
 
6
6
  #This method selects all templates from the database which should be further examined
7
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
8
+ def self.select_templates datastore, number_of_points, number_of_strokes
9
+ p_min = number_of_points - 100
10
+ p_max = number_of_points + 100
11
+ s_min = number_of_strokes - 12
12
+ s_max = number_of_strokes + 12
13
13
  datastore.characters_in_range(p_min..p_max, s_min..s_max)
14
14
  end
15
15
 
16
16
  #This method calculates similarity scores which is an average of the somehow weighted sum of the euclidean distance of
17
- #1. 20x20 smoothed heatmap
18
- #2. euclidean distance of directional feature densities in average
17
+ #1. 17x17 smoothed heatmap
18
+ #2. manhattan distance of directional feature densities in average
19
19
  #Params:
20
20
  #+strokes+:: strokes are not preprocessed
21
21
  #+datastore+:: JSONDatastore or custom datastore type having method characters_in_stroke_range(min..max)
22
22
  def self.scores strokes, datastore
23
- character = Trainer::Character.new(strokes, nil)
24
- templates = select_templates character, datastore
23
+ strokes = preprocess(strokes)
24
+ heatmaps = heatmaps(strokes)
25
+ templates = select_templates datastore, @number_of_points, strokes.count
25
26
 
27
+ #scores = datastore.data.map do |cand|
26
28
  scores = templates.map do |cand|
27
-
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)
32
-
33
-
34
- [[heatmap_bi_moment_score, heatmap_line_density_score, heatmap_bi_moment_slant_score, heatmap_line_density_slant_score].min, cand]
29
+ score = Math.manhattan_distance(heatmaps[0], cand[:heatmaps][0]) +
30
+ Math.manhattan_distance(heatmaps[1], cand[:heatmaps][1]) +
31
+ Math.manhattan_distance(heatmaps[2], cand[:heatmaps][2])
32
+ [score, cand]
35
33
  end
36
34
 
37
35
  scores.sort{ |a, b| a[0] <=> b[0] }
@@ -0,0 +1,43 @@
1
+ module KvgCharacterRecognition
2
+ class Template
3
+ extend KvgCharacterRecognition::Trainer
4
+ #This method populates the datastore with parsed template patterns from the kanjivg file in xml format
5
+ #Params:
6
+ #+xml+:: download the latest xml release from https://github.com/KanjiVG/kanjivg/releases
7
+ #+datastore+:: JSONDatastore or custom datastore type having methods store, persist!
8
+ def self.parse_from_xml xml, datastore, kanji_list=[]
9
+ file = File.open(xml) { |f| Nokogiri::XML(f) }
10
+
11
+ file.xpath("//kanji").each do |kanji|
12
+ #id has format: "kvg:kanji_CODEPOINT"
13
+ codepoint = kanji.attributes["id"].value.split("_")[1]
14
+ value = [codepoint.hex].pack("U")
15
+ if kanji_list.empty?
16
+ next unless codepoint.hex >= "04e00".hex && codepoint.hex <= "09faf".hex
17
+ else
18
+ next unless codepoint.hex >= "04e00".hex && codepoint.hex <= "09faf".hex && kanji_list.include?(value)
19
+ end
20
+ puts "#{codepoint} #{value}"
21
+
22
+ # parse strokes
23
+ strokes = kanji.xpath("g//path").map{|p| p.attributes["d"].value }.map{ |stroke| KvgCharacterRecognition::KvgParser::Stroke.new(stroke).to_a }
24
+
25
+ strokes = preprocess(strokes)
26
+
27
+ #Store to database
28
+ #--------------
29
+ character = {
30
+ value: value,
31
+ codepoint: codepoint.hex,
32
+ number_of_strokes: strokes.count,
33
+ number_of_points: @number_of_points,
34
+ heatmaps: heatmaps(strokes)
35
+ }
36
+
37
+ datastore.store character
38
+ end
39
+
40
+ datastore.persist!
41
+ end
42
+ end
43
+ end
@@ -1,81 +1,58 @@
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
3
+ @@config = { downsample_rate: 4,
4
+ interpolate_distance: 0.8,
5
+ size: 109,
6
+ smooth: true,
7
+ smooth_weights: [1,2,3,2,1],
8
+ smooth_filter_weights: [1/9.0, 1/9.0, 1/9.0, 1/9.0, 1/9.0, 1/9.0, 1/9.0, 1/9.0, 1/9.0],
9
+ heatmap_number_of_grids: 17
10
+ }
11
+ @@preprocessor = Preprocessor.new(@@config[:interpolate_distance],
12
+ @@config[:size],
13
+ @@config[:smooth],
14
+ @@config[:smooth_weights])
15
+
16
+ # this variable will be set in the method preprocess
17
+ @number_of_points = 0
18
+
19
+ # preprocess strokes and set the number_of_points variable
20
+ # !the preprocessed strokes are not downsampled
21
+ def preprocess strokes
22
+ strokes = @@preprocessor.preprocess(strokes)
23
+ @number_of_points = strokes.flatten(1).count
24
+ strokes
39
25
  end
40
26
 
41
- #This method populates the datastore with parsed template patterns from the kanjivg file in xml format
42
- #Params:
43
- #+xml+:: download the latest xml release from https://github.com/KanjiVG/kanjivg/releases
44
- #+datastore+:: JSONDatastore or custom datastore type having methods store, persist!
45
- def self.populate_from_xml xml, datastore
46
- file = File.open(xml) { |f| Nokogiri::XML(f) }
47
-
48
- file.xpath("//kanji").each do |kanji|
49
- #id has format: "kvg:kanji_CODEPOINT"
50
- codepoint = kanji.attributes["id"].value.split("_")[1]
51
- next unless codepoint.hex >= "04e00".hex && codepoint.hex <= "09faf".hex
52
- puts codepoint
53
- value = [codepoint.hex].pack("U")
54
-
55
- #Preprocessing
56
- #--------------
57
- #parse strokes
58
- strokes = kanji.xpath("g//path").map{|p| p.attributes["d"].value }.map{ |stroke| KvgParser::Stroke.new(stroke).to_a }
59
-
60
- chr = Character.new strokes, value
61
-
62
- #Store to database
63
- #--------------
64
- character = {
65
- value: value,
66
- codepoint: codepoint.hex,
67
- number_of_strokes: strokes.count,
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
73
- }
74
-
75
- datastore.store character
76
- end
27
+ # This method returns the 3x 17x17 direction feature vector
28
+ # strokes are preprocessed
29
+ def heatmaps strokes
30
+ weights = @@config[:smooth_filter_weights]
31
+ number_of_grids = @@config[:heatmap_number_of_grids]
32
+
33
+ bi_normed = strokes
34
+ ld_normed = @@preprocessor.line_density_normalize(bi_normed).map{ |stroke| downsample(stroke, @@config[:downsample_rate]) }
35
+ pd_normed = @@preprocessor.point_density_normalize(bi_normed).map{ |stroke| downsample(stroke, @@config[:downsample_rate]) }
36
+ bi_normed = bi_normed.map{ |stroke| downsample(stroke, @@config[:downsample_rate]) }
37
+
38
+ # feature extraction
39
+ heatmaps_map = HeatmapFeature.new(bi_normed,
40
+ ld_normed,
41
+ pd_normed,
42
+ @@config[:size],
43
+ number_of_grids,
44
+ weights).heatmaps
45
+
46
+ # convert to feature vector
47
+ Matrix.columns(heatmaps_map.to_a).to_a
48
+ end
77
49
 
78
- datastore.persist!
50
+ private
51
+ #This methods downsamples a stroke in given interval
52
+ #The number of points in the stroke will be reduced
53
+ def downsample stroke, interval
54
+ stroke.each_slice(interval).map(&:first)
79
55
  end
56
+
80
57
  end
81
58
  end
@@ -19,314 +19,3 @@ module Math
19
19
  sum
20
20
  end
21
21
  end
22
-
23
- module KvgCharacterRecognition
24
-
25
- #This class can be used for storing heatmap count and directional feature densities
26
- #basically it is a nxm matrix with an initial value in each cell
27
- class Map
28
- #Make a new map with
29
- #Params:
30
- #+n+:: row length
31
- #+m+:: column length
32
- #+initial_value+:: for heatmap initial_value = 0 and for directional feature densities initial_value = [0, 0, 0, 0] <= [weight in e1, weight in e2, ...]
33
- def initialize n, m, initial_value
34
- @array = Array.new(n * m, initial_value)
35
- @n = n
36
- @m = m
37
- end
38
-
39
- #Access value in the cell of i-th row and j-th column
40
- #e.g. map[i,j]
41
- def [](i, j)
42
- @array[j*@n + i]
43
- end
44
-
45
- #Store value in the cell of i-th row and j-th column
46
- #e.g. map[i,j] = value
47
- def []=(i, j, value)
48
- @array[j*@n + i] = value
49
- end
50
-
51
- def to_a
52
- @array
53
- end
54
-
55
- #Normaly n is the same as m
56
- def size
57
- @n
58
- end
59
- end
60
-
61
-
62
- #This module contains classes which can be used to parse a svg command
63
- #The code is copied from https://github.com/rogerbraun/KVG-Tools
64
- #Methods for generating sexp or xml outputs are removed
65
- module KvgParser
66
- #A Point
67
- class Point
68
- attr_accessor :x, :y, :color
69
-
70
- def initialize(x,y, color = :black)
71
- @x,@y, @color = x, y, color
72
- end
73
-
74
- #Basic point arithmetics
75
- def +(p2)
76
- return Point.new(@x + p2.x, @y + p2.y)
77
- end
78
-
79
- def -(p2)
80
- return Point.new(@x - p2.x, @y - p2.y)
81
- end
82
-
83
- def dist(p2)
84
- return Math.sqrt((p2.x - @x)**2 + (p2.y - @y)**2)
85
- end
86
-
87
- def *(number)
88
- return Point.new(@x * number, @y * number)
89
- end
90
-
91
- #to array
92
- def to_a
93
- [@x.round(2), @y.round(2)]
94
- end
95
-
96
- end
97
-
98
- # SVG_M represents the moveto command.
99
- # SVG Syntax is:
100
- # m x y
101
- # It sets the current cursor to the point (x,y).
102
- # As always, capitalization denotes absolute values.
103
- # Takes a Point as argument.
104
- # If given 2 Points, the second argument is treated as the current cursor.
105
- class SVG_M
106
-
107
- def initialize(p1, p2 = Point.new(0,0))
108
- @p = p1 + p2
109
- end
110
-
111
- def to_points
112
- return []
113
- end
114
-
115
- def current_cursor
116
- return @p
117
- end
118
-
119
- end
120
-
121
- # SVG_C represents the cubic Bézier curveto command.
122
- # Syntax is:
123
- # c x1 y1 x2 y2 x y
124
- # It sets the current cursor to the point (x,y).
125
- # As always, capitalization denotes absolute values.
126
- # Takes 4 Points as argument, the fourth being the current cursor
127
- # If constructed using SVG_C.relative, the current cursor is added to every
128
- # point.
129
- class SVG_C
130
-
131
- def initialize(c1,c2,p,current_cursor)
132
- @c1,@c2,@p,@current_cursor = c1,c2,p,current_cursor
133
- @@c_color = :green
134
- end
135
-
136
- def SVG_C.relative(c1,c2,p,current_cursor)
137
- SVG_C.new(c1 + current_cursor, c2 + current_cursor, p + current_cursor, current_cursor)
138
- end
139
-
140
- def second_point
141
- @c2
142
- end
143
-
144
- # This implements the algorithm found here:
145
- # http://www.cubic.org/docs/bezier.htm
146
- # Takes 2 Points and a factor between 0 and 1
147
- def linear_interpolation(a,b,factor)
148
-
149
- xr = a.x + ((b.x - a.x) * factor)
150
- yr = a.y + ((b.y - a.y) * factor)
151
-
152
- return Point.new(xr,yr);
153
-
154
- end
155
-
156
- def switch_color
157
- if @@c_color == :green
158
- @@c_color = :red
159
- elsif @@c_color == :red
160
- @@c_color = :purple
161
- else
162
- @@c_color = :green
163
- end
164
- end
165
-
166
- def make_curvepoint(factor)
167
- ab = linear_interpolation(@current_cursor,@c1,factor)
168
- bc = linear_interpolation(@c1,@c2,factor)
169
- cd = linear_interpolation(@c2,@p,factor)
170
-
171
- abbc = linear_interpolation(ab,bc,factor)
172
- bccd = linear_interpolation(bc,cd,factor)
173
- return linear_interpolation(abbc,bccd,factor)
174
- end
175
-
176
- def length(points)
177
- old_point = @current_cursor;
178
- length = 0.0
179
- factor = points.to_f
180
-
181
- (1..points).each {|point|
182
- new_point = make_curvepoint(point/(factor.to_f))
183
- length += old_point.dist(new_point)
184
- old_point = new_point
185
- }
186
- return length
187
- end
188
-
189
- # This gives back an array of points on the curve. The argument given
190
- # denotes how the distance between each point.
191
- def make_curvepoint_array(distance)
192
- result = Array.new
193
-
194
- l = length(20)
195
- points = l * distance
196
- factor = points.to_f
197
-
198
- (0..points).each {|point|
199
- result.push(make_curvepoint(point/(factor.to_f)))
200
- }
201
-
202
- return result
203
- end
204
-
205
-
206
- def to_points
207
- return make_curvepoint_array(0.3)
208
- end
209
-
210
- def current_cursor
211
- @p
212
- end
213
-
214
- end
215
-
216
- # SVG_S represents the smooth curveto command.
217
- # Syntax is:
218
- # s x2 y2 x y
219
- # It sets the current cursor to the point (x,y).
220
- # As always, capitalization denotes absolute values.
221
- # Takes 3 Points as argument, the third being the current cursor
222
- # If constructed using SVG_S.relative, the current cursor is added to every
223
- # point.
224
- class SVG_S < SVG_C
225
-
226
- def initialize(c2, p, current_cursor,previous_point)
227
- super(SVG_S.reflect(previous_point,current_cursor), c2, p, current_cursor)
228
- end
229
-
230
- # The reflection in this case is rather tricky. Using SVG_C.relative, the
231
- # offset of current_cursor is added to all the positions (except current_cursor).
232
- # The reflected point, however is already calculated in absolute values.
233
- # Because of this, we have to subtract the current_cursor from the reflected
234
- # point, as it is already added later. I think I got the classes somewhat wrong.
235
- # Maybe points should get a field whether they are absolute oder relative?
236
- # Don't know yet. It works now, though!
237
- def SVG_S.relative(c2, p, current_cursor, previous_point)
238
- SVG_C.relative(SVG_S.reflect(previous_point,current_cursor) - current_cursor, c2, p, current_cursor)
239
- end
240
-
241
- def SVG_S.reflect(p, mirror)
242
- return mirror + (mirror - p)
243
- end
244
-
245
- end
246
-
247
-
248
- # Stroke represent one stroke, which is a series of SVG commands.
249
- class Stroke
250
- COMMANDS = ["M", "C", "c", "s", "S"]
251
-
252
- def initialize(stroke_as_code)
253
- @command_list = parse(stroke_as_code)
254
- end
255
-
256
- def to_points
257
- return @command_list.map{|element| element.to_points}.flatten
258
- end
259
-
260
- #to array
261
- #TODO: better implementation using composite pattern
262
- def to_a
263
- to_points.map{|point| point.to_a}
264
- end
265
-
266
- def split_elements(line)
267
- # This is magic.
268
- return line.gsub("-",",-").gsub("s",",s,").gsub("S",",S,").gsub("c",",c,").gsub("C",",C,").gsub("m", "M").gsub("M","M,").gsub("[","").gsub(";",",;,").gsub(",,",",").gsub(" ,", ",").gsub(", ", ",").gsub(" ", ",").split(/,/);
269
- end
270
-
271
- def parse(stroke_as_code)
272
- elements = split_elements(stroke_as_code).delete_if{ |e| e == "" }
273
- command_list = Array.new
274
- current_cursor = Point.new(0,0);
275
-
276
- while elements != [] do
277
-
278
- case elements.slice!(0)
279
- when "M"
280
- x,y = elements.slice!(0..1)
281
- m = SVG_M.new(Point.new(x.to_f,y.to_f))
282
- current_cursor = m.current_cursor
283
- command_list.push(m)
284
-
285
- when "C"
286
- x1,y1,x2,y2,x,y = elements.slice!(0..5)
287
- c = SVG_C.new(Point.new(x1.to_f,y1.to_f), Point.new(x2.to_f,y2.to_f), Point.new(x.to_f,y.to_f), current_cursor)
288
- current_cursor = c.current_cursor
289
- command_list.push(c)
290
-
291
- #handle polybezier
292
- unless elements.empty? || COMMANDS.include?(elements.first)
293
- elements.unshift("C")
294
- end
295
- when "c"
296
- x1,y1,x2,y2,x,y = elements.slice!(0..5)
297
- c = SVG_C.relative(Point.new(x1.to_f,y1.to_f), Point.new(x2.to_f,y2.to_f), Point.new(x.to_f,y.to_f), current_cursor)
298
- current_cursor = c.current_cursor
299
- command_list.push(c)
300
-
301
- #handle polybezier
302
- unless elements.empty? || COMMANDS.include?(elements.first)
303
- elements.unshift("c")
304
- end
305
-
306
- when "s"
307
- x2,y2,x,y = elements.slice!(0..3)
308
- reflected_point = command_list[-1].second_point
309
- s = SVG_S.relative(Point.new(x2.to_f,y2.to_f), Point.new(x.to_f,y.to_f), current_cursor, reflected_point)
310
- current_cursor = s.current_cursor
311
- command_list.push(s)
312
-
313
- when "S"
314
- x2,y2,x,y = elements.slice!(0..3)
315
- reflected_point = command_list[-1].second_point
316
- s = SVG_S.new(Point.new(x2.to_f,y2.to_f), Point.new(x.to_f,y.to_f), current_cursor,reflected_point)
317
- current_cursor = s.current_cursor
318
- command_list.push(s)
319
-
320
- else
321
- #print "You should not be here\n"
322
-
323
- end
324
-
325
- end
326
-
327
- return command_list
328
- end
329
-
330
- end
331
- end
332
- end