kvg_character_recognition 0.1.3 → 0.2.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.
@@ -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