kvg_character_recognition 0.1.0 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a14886233c4152851248456913d18768d9c8472a
4
- data.tar.gz: f29d6c47b40ce1eb68a4db14c3b03961c275197c
3
+ metadata.gz: 2f5bd00df050f48f3ee955daaa966fd04b10a5f3
4
+ data.tar.gz: bb45a8db8023fb37f4f929a673f1f811756540fa
5
5
  SHA512:
6
- metadata.gz: 339db4a0b7e01108b9d105f688dfb4bba6ea81420bb4255b70f1ff8ef51c5ae04cab3424464e453632f2ea17727f2e1629a95148d06379720f5e1cae448fa4a8
7
- data.tar.gz: 2f57226b5dac0f9311bf0c5dec2750354aa6c40c5348e2f1df858b52a470aed242aa380be1d4aadfd2d9a1753e5fbad0a1a621a232d28d5e81b3dde4b2c929d9
6
+ metadata.gz: ae3a13b9370276c1714f19acebcf856cff091c27725514ae8f0a75f43ef754576d46bc69b7804d1203db5216524d0841e82b2deac7f29729ad5ebfe55441d080
7
+ data.tar.gz: 057e9896548709b6fd4187ffcf8d576b2d49fa0fb19f6b1cc3909e75befde49149aa934d54ea9c6bbab48d394f4fdf708404287ec6eae6857dab3b53b7a09650
data/Gemfile CHANGED
@@ -3,6 +3,3 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in kvg_character_recognition.gemspec
4
4
  gemspec
5
5
 
6
- gem 'sequel'
7
- gem 'sqlite3'
8
- gem 'nokogiri'
data/README.md CHANGED
@@ -4,21 +4,27 @@ A stroke is an array of points in the format [[x1, y1], [x2, y2], ...].
4
4
  For templates, we use svg data from the [KanjiVG project](http://kanjivg.tagaini.net/)
5
5
 
6
6
  The engine takes 3 steps to perform the recognition of an input pattern.
7
+
7
8
  1. Preprocessing
9
+
8
10
  The preprocessing step consists of smoothing, normalizing, interpolating and downsampling of the data points.
11
+
9
12
  2. Feature Extraction
13
+
10
14
  Smoothed heatmap, significant points and directional feature densities are used as features.
11
15
  A heatmap divides the input pattern in small grids and stores the number of data points in each grid.
12
16
  Significant points are defined as start and end point of a stroke, points on curve or edge.
13
17
  Directional feature densities are introduced in the paper "On-line Recognition of Freely Handwritten Japanese Character Using Directional Feature Density"
18
+
14
19
  3. Matching
20
+
15
21
  We use the significant points to perform a coarse recognition of the input pattern, that filters out template patterns with great distance to the input pattern. Next, a mixed distance score of directional feature density and smoothed heatmap is calculated.
16
22
  ## Installation
17
23
 
18
24
  Add this line to your application's Gemfile:
19
25
 
20
26
  ```ruby
21
- gem 'kvg_character_recognition'
27
+ gem 'kvg_character_recognition', '~>0.1.1'
22
28
  ```
23
29
 
24
30
  And then execute:
@@ -31,15 +37,14 @@ Or install it yourself as:
31
37
 
32
38
  ## Usage
33
39
 
34
- 1. Create a database(e.g. using sqlite3 data.db)
35
-
36
- 2. Setup the characters table in the database and populate it with kanjivg templates from the [xml release](https://github.com/KanjiVG/kanjivg/releases)
40
+ 1. Setup a json datastore and populate it with kanjivg templates from the [xml release](https://github.com/KanjiVG/kanjivg/releases)
37
41
  ```ruby
38
42
  require 'kvg_character_recognition'
39
43
 
40
- KvgCharacterRecognition::Database.setup
44
+ datastore = KvgCharacterRecognition::JSONDatastore.new("characters.json")
45
+ #ignore the warning
41
46
 
42
- KvgCharacterRecognition::Database.populate_from_xml "kanjivg-20150615-2.xml"
47
+ KvgCharacterRecognition::Trainer.populate_from_xml "kanjivg-20150615-2.xml", datastore
43
48
  ```
44
49
 
45
50
  3. Recognition
@@ -48,7 +53,8 @@ Use an input field of size 300x300 for the best recognition accuracy. The input
48
53
  ```ruby
49
54
  strokes = [[[99.0, 108.0], [100.0, 108.0], [101.0, 108.0], [101.0, 108.0], [103.0, 108.0], [105.0, 107.0], [107.0, 107.0], [108.0, 107.0], [111.0, 106.0], [111.0, 106.0], [112.0, 106.0], [113.0, 106.0], [114.0, 106.0], [115.0, 105.0], [116.0, 105.0], [118.0, 105.0], [120.0, 105.0], [121.0, 104.0], [122.0, 104.0], [122.0, 104.0], [123.0, 104.0], [124.0, 103.0], [125.0, 103.0], [126.0, 103.0], [127.0, 103.0], [129.0, 102.0], [130.0, 102.0], [132.0, 102.0], [132.0, 101.0], [133.0, 101.0], [135.0, 101.0], [136.0, 101.0], [137.0, 101.0], [138.0, 101.0], [140.0, 101.0], [141.0, 100.0], [142.0, 100.0], [143.0, 100.0], [144.0, 100.0], [145.0, 99.0], [148.0, 99.0], [150.0, 99.0], [151.0, 98.0], [152.0, 98.0], [153.0, 98.0], [154.0, 98.0], [156.0, 97.0], [157.0, 97.0], [158.0, 97.0], [159.0, 97.0], [161.0, 97.0], [162.0, 96.0], [162.0, 96.0], [164.0, 96.0], [165.0, 96.0], [166.0, 96.0], [167.0, 96.0], [169.0, 95.0], [170.0, 95.0], [171.0, 95.0], [172.0, 95.0], [173.0, 95.0], [174.0, 95.0]], [[53.0, 190.0], [54.0, 190.0], [56.0, 190.0], [57.0, 190.0], [59.0, 190.0], [61.0, 190.0], [63.0, 189.0], [66.0, 189.0], [67.0, 189.0], [68.0, 189.0], [69.0, 189.0], [71.0, 189.0], [72.0, 188.0], [72.0, 188.0], [74.0, 188.0], [76.0, 187.0], [78.0, 187.0], [80.0, 187.0], [81.0, 187.0], [82.0, 186.0], [84.0, 186.0], [87.0, 186.0], [89.0, 185.0], [91.0, 185.0], [93.0, 185.0], [95.0, 184.0], [98.0, 184.0], [100.0, 183.0], [102.0, 183.0], [104.0, 183.0], [106.0, 183.0], [110.0, 182.0], [111.0, 182.0], [112.0, 182.0], [115.0, 182.0], [118.0, 182.0], [120.0, 182.0], [122.0, 182.0], [125.0, 182.0], [128.0, 181.0], [130.0, 181.0], [133.0, 180.0], [136.0, 180.0], [141.0, 180.0], [143.0, 179.0], [146.0, 179.0], [150.0, 179.0], [152.0, 178.0], [155.0, 178.0], [158.0, 178.0], [159.0, 178.0], [162.0, 177.0], [164.0, 177.0], [167.0, 177.0], [170.0, 177.0], [173.0, 176.0], [176.0, 176.0], [179.0, 176.0], [182.0, 175.0], [187.0, 175.0], [189.0, 174.0], [192.0, 174.0], [194.0, 174.0], [196.0, 173.0], [199.0, 173.0], [202.0, 173.0], [204.0, 172.0], [206.0, 172.0], [209.0, 172.0], [211.0, 172.0], [212.0, 172.0], [215.0, 172.0], [217.0, 172.0], [219.0, 171.0], [221.0, 171.0], [221.0, 172.0]]]
50
55
 
51
- scores = KvgCharacterRecognition::Recognizer.scores strokes
56
+ datastore = KvgCharacterRecognition::JSONDatastore.new("characters.json") #this will initialize datastore once for the entire usage
57
+ scores = KvgCharacterRecognition::Recognizer.scores strokes, datastore
52
58
 
53
59
  irb(main):004:0> scores.take 10
54
60
  => [[1.524079282599697, 60, "二"], [2.8346163809971143, 1373, "工"], [3.0987422100694757, 7, "上"], [3.127346308294038, 365, "冫"], [3.439293212191952, 6, "三"], [3.4890481845638304, 3770, "立"], [3.541524904953307, 2721, "江"], [3.641178875851016, 569, "厂"], [3.6447144433336294, 72, "亠"], [3.7498483818966353, 2706, "氵"]]
@@ -73,9 +79,6 @@ Don't forget to redo the whole database step after changing the configuration.
73
79
  #from yaml file
74
80
  Kvgcharacterrecognition.configure_with(path_to_yml)
75
81
 
76
- #configure database with yml
77
- #TODO why is postgres slower than sqlite?
78
- Kvgcharacterrecognition.configure_database(path_to_yml)
79
82
  ```
80
83
 
81
84
 
@@ -26,15 +26,15 @@ 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.match(%r{^(test|spec|features)/}) }
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)/}) }
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"
34
35
  spec.add_dependency "nokogiri"
35
- spec.add_dependency "sequel"
36
- spec.add_dependency "sqlite3"
37
36
  spec.add_dependency "bundler", "~> 1.10"
38
37
  spec.add_development_dependency "rake", "~> 10.0"
39
38
  spec.add_development_dependency "rspec"
39
+ spec.add_development_dependency "pry"
40
40
  end
@@ -6,7 +6,6 @@ Dir[File.join(File.dirname(__FILE__), '/kvg_character_recognition/*.rb')].each {
6
6
 
7
7
  module KvgCharacterRecognition
8
8
 
9
- @db = Sequel.connect('sqlite://characters.db')
10
9
  CONFIG = {
11
10
  size: 109, #fixed canvas size of kanjivg data
12
11
  downsample_interval: 4,
@@ -34,22 +33,4 @@ module KvgCharacterRecognition
34
33
 
35
34
  configure(config)
36
35
  end
37
-
38
- #Configure database
39
- def self.configure_database(yml)
40
- begin
41
- db_config = YAML::load(IO.read(yml))
42
- rescue Errno::ENOENT
43
- log(:warning, "YAML configuration file couldn't be found. Using defaults."); return
44
- rescue Psych::SyntaxError
45
- log(:warning, "YAML configuration file contains invalid syntax. Using defaults."); return
46
- end
47
- @db = Sequel.connect(yml)
48
- end
49
-
50
- #getter
51
- def self.db
52
- @db
53
- end
54
-
55
36
  end
@@ -0,0 +1,35 @@
1
+ require 'json'
2
+
3
+ module KvgCharacterRecognition
4
+ class JSONDatastore
5
+ def initialize filename = 'characters.json'
6
+ @data = load_file(filename)
7
+ @filename = filename
8
+ end
9
+
10
+ def load_file filename
11
+ begin
12
+ JSON.parse(File.read(filename, encoding: 'utf-8'), symbolize_names: true)
13
+ rescue
14
+ puts "WARNING: Can't load file, returning empty character collection."
15
+ []
16
+ end
17
+ end
18
+
19
+ def characters_in_stroke_range range
20
+ @data.select { |character| range === character[:number_of_strokes] }
21
+ end
22
+
23
+ def store character
24
+ @data.push character
25
+ end
26
+
27
+ def persist!
28
+ dump @filename
29
+ end
30
+
31
+ def dump filename
32
+ File.write(filename, @data.to_json)
33
+ end
34
+ end
35
+ end
@@ -148,17 +148,6 @@ module KvgCharacterRecognition
148
148
  points
149
149
  end
150
150
 
151
- #This methods calculates the euclidean distance between 2 points
152
- #Params:
153
- #- p1, p2: [x, y]
154
- def self.euclidean_distance(p1, p2)
155
- sum_of_squares = 0
156
- p1.each_with_index do |p1_coord,index|
157
- sum_of_squares += (p1_coord - p2[index]) ** 2
158
- end
159
- Math.sqrt( sum_of_squares )
160
- end
161
-
162
151
  #This method interpolates points into a stroke with given distance
163
152
  #The algorithm is taken from the paper preprocessing techniques for online character recognition
164
153
  def self.interpolate stroke, d=0.5
@@ -171,7 +160,7 @@ module KvgCharacterRecognition
171
160
  point = stroke[index]
172
161
 
173
162
  #only consider point with greater than d distance to current point
174
- if euclidean_distance(current, point) < d
163
+ if Math.euclidean_distance(current, point) < d
175
164
  index += 1
176
165
  else
177
166
 
@@ -1,27 +1,32 @@
1
1
  require 'matrix'
2
+ require 'parallel'
2
3
  module KvgCharacterRecognition
3
4
  #This class contains methods calculating similarity scores between input pattern and template patterns
4
- class Recognizer
5
+ module Recognizer
6
+ @thread_count = 10
5
7
 
6
8
  #This method selects all templates from the database which should be further examined
7
9
  #It filtered out those characters with a too great difference in number of strokes to the input character
8
- def self.select_templates strokes
10
+ def self.select_templates strokes, datastore
9
11
  min = strokes.count <= 5 ? strokes.count : strokes.count - 5
10
12
  max = strokes.count + 10
11
- KvgCharacterRecognition.db[:characters].where(:number_of_strokes => (min..max))
13
+ datastore.characters_in_stroke_range(min..max)
12
14
  end
13
15
 
14
16
  #This method uses heatmap of significant points to coarse recognize the input pattern
15
17
  #Params:
16
18
  #+strokes+:: strokes should be preprocessed
17
- def self.coarse_recognize strokes
19
+ #+datastore+:: JSONDatastore or custom datastore type having method characters_in_stroke_range(min..max)
20
+ def self.coarse_recognize strokes, datastore
18
21
  heatmap = FeatureExtractor.heatmap(Preprocessor.significant_points(strokes), CONFIG[:significant_points_heatmap_grid], CONFIG[:size]).to_a
19
22
 
20
- templates = select_templates strokes
21
- templates.map do |candidate|
23
+ templates = select_templates strokes, datastore
24
+
25
+ # Use threads to accelerate the process
26
+ Parallel.map(templates, in_threads: @thread_count) do |candidate|
22
27
  candidate_heatmap = candidate[:heatmap_significant_points].split(",").map(&:to_f)
23
28
 
24
- score = Preprocessor.euclidean_distance(heatmap, candidate_heatmap)
29
+ score = Math.euclidean_distance(heatmap, candidate_heatmap)
25
30
  [score.round(3), candidate]
26
31
  end
27
32
  end
@@ -31,7 +36,8 @@ module KvgCharacterRecognition
31
36
  #2. euclidean distance of directional feature densities in average
32
37
  #Params:
33
38
  #+strokes+:: strokes are not preprocessed
34
- def self.scores strokes
39
+ #+datastore+:: JSONDatastore or custom datastore type having method characters_in_stroke_range(min..max)
40
+ def self.scores strokes, datastore
35
41
  #preprocess strokes
36
42
  #with smoothing
37
43
  strokes = Preprocessor.preprocess(strokes, CONFIG[:interpolate_distance], CONFIG[:downsample_interval], true)
@@ -42,18 +48,18 @@ module KvgCharacterRecognition
42
48
 
43
49
  #dump half of the templates after coarse recognition
44
50
  #collection is in the form [[score, c1], [score, c2] ...]
45
- collection = coarse_recognize(strokes).sort{ |a, b| a[0] <=> b[0] }
51
+ collection = coarse_recognize(strokes, datastore).sort{ |a, b| a[0] <=> b[0] }
46
52
 
47
- scores = collection.take(collection.count / 2).map do |cand|
48
- direction_score = (Preprocessor.euclidean_distance(directions[0], cand[1][:direction_e1].split(",").map(&:to_f)) +
49
- Preprocessor.euclidean_distance(directions[1], cand[1][:direction_e2].split(",").map(&:to_f)) +
50
- Preprocessor.euclidean_distance(directions[2], cand[1][:direction_e3].split(",").map(&:to_f)) +
51
- Preprocessor.euclidean_distance(directions[3], cand[1][:direction_e4].split(",").map(&:to_f)) ) / 4
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
52
58
 
53
- heatmap_score = Preprocessor.euclidean_distance(heatmap_smoothed, cand[1][:heatmap_smoothed].split(",").map(&:to_f))
59
+ heatmap_score = Math.euclidean_distance(heatmap_smoothed, cand[1][:heatmap_smoothed].split(",").map(&:to_f))
54
60
 
55
61
  mix = (direction_score / 100) + heatmap_score
56
- [mix/2, cand[1][:id], cand[1][:value]]
62
+ [mix/2, cand[1]]
57
63
  end
58
64
 
59
65
  scores.sort{ |a, b| a[0] <=> b[0] }
@@ -1,49 +1,10 @@
1
- require 'matrix'
2
- require 'nokogiri'
3
-
4
1
  module KvgCharacterRecognition
5
- #This class contains methods for database interactions
6
- class Database
7
-
8
- #This method creates a database table for storing the extracted features of the templates
9
- #Arrays of points will be serialized and stored as string
10
- #Following fields are created:
11
- # - primary_key :id
12
- # - String :value
13
- # - Integer :codepoint
14
- # - String :serialized_strokes i.e. [stroke, x, y]
15
- # - String :direction_e1
16
- # - String :direction_e2
17
- # - String :direction_e3
18
- # - String :direction_e4
19
- # - String :heatmap_smoothed
20
- # - String :heatmap_significant_points
21
- def self.setup
22
- KvgCharacterRecognition.db.create_table :characters do
23
- primary_key :id
24
- String :value
25
- Integer :codepoint
26
- Integer :number_of_strokes
27
- String :serialized_strokes
28
- String :direction_e1
29
- String :direction_e2
30
- String :direction_e3
31
- String :direction_e4
32
- String :heatmap_smoothed
33
- String :heatmap_significant_points
34
- end
35
-
36
- end
37
-
38
- #Drop created table
39
- def self.drop
40
- KvgCharacterRecognition.db.drop_table(:characters) if KvgCharacterRecognition.db.table_exists?(:characters)
41
- end
42
-
43
- #This method populates the database table with parsed template patterns from the kanjivg file in xml format
2
+ module Trainer
3
+ #This method populates the datastore with parsed template patterns from the kanjivg file in xml format
44
4
  #Params:
45
5
  #+xml+:: download the latest xml release from https://github.com/KanjiVG/kanjivg/releases
46
- def self.populate_from_xml xml
6
+ #+datastore+:: JSONDatastore or custom datastore type having methods store, persist!
7
+ def self.populate_from_xml xml, datastore
47
8
  file = File.open(xml) { |f| Nokogiri::XML(f) }
48
9
 
49
10
  file.xpath("//kanji").each do |kanji|
@@ -85,7 +46,8 @@ module KvgCharacterRecognition
85
46
 
86
47
  #Store to database
87
48
  #--------------
88
- KvgCharacterRecognition.db[:characters].insert value: value,
49
+ character = {
50
+ value: value,
89
51
  codepoint: codepoint.hex,
90
52
  number_of_strokes: strokes.count,
91
53
  serialized_strokes: serialized.join(","),
@@ -95,9 +57,12 @@ module KvgCharacterRecognition
95
57
  direction_e4: direction[3].join(","),
96
58
  heatmap_smoothed: heatmap_smoothed.to_a.join(","),
97
59
  heatmap_significant_points: heatmap_significant_points.to_a.join(",")
60
+ }
61
+
62
+ datastore.store character
98
63
  end
99
64
 
65
+ datastore.persist!
100
66
  end
101
-
102
67
  end
103
68
  end
@@ -1,3 +1,17 @@
1
+ module Math
2
+ #Add euclidean distance method to ruby Math module
3
+ #This methods calculates the euclidean distance between 2 points
4
+ #Params:
5
+ #- p1, p2: [x, y]
6
+ def self.euclidean_distance(p1, p2)
7
+ sum_of_squares = 0
8
+ p1.each_with_index do |p1_coord,index|
9
+ sum_of_squares += (p1_coord - p2[index]) ** 2
10
+ end
11
+ Math.sqrt( sum_of_squares )
12
+ end
13
+ end
14
+
1
15
  module KvgCharacterRecognition
2
16
 
3
17
  #This class can be used for storing heatmap count and directional feature densities
@@ -1,3 +1,3 @@
1
1
  module KvgCharacterRecognition
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.2"
3
3
  end
metadata CHANGED
@@ -1,31 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kvg_character_recognition
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
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-12 00:00:00.000000000 Z
11
+ date: 2016-01-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: nokogiri
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
- - !ruby/object:Gem::Dependency
28
- name: sequel
14
+ name: parallel
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - ">="
@@ -39,7 +25,7 @@ dependencies:
39
25
  - !ruby/object:Gem::Version
40
26
  version: '0'
41
27
  - !ruby/object:Gem::Dependency
42
- name: sqlite3
28
+ name: nokogiri
43
29
  requirement: !ruby/object:Gem::Requirement
44
30
  requirements:
45
31
  - - ">="
@@ -94,6 +80,20 @@ dependencies:
94
80
  - - ">="
95
81
  - !ruby/object:Gem::Version
96
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
97
  description: "This gem contains a CJK-character recognition engine using pattern/template
98
98
  matching techniques.\n It can recognize stroke-order and stroke-number free handwritten
99
99
  character patterns in the format [stroke1, stroke2 ...].\n A stroke is an array
@@ -117,10 +117,11 @@ files:
117
117
  - bin/setup
118
118
  - kvg_character_recognition.gemspec
119
119
  - lib/kvg_character_recognition.rb
120
- - lib/kvg_character_recognition/database.rb
120
+ - lib/kvg_character_recognition/datastore.rb
121
121
  - lib/kvg_character_recognition/feature_extractor.rb
122
122
  - lib/kvg_character_recognition/preprocessor.rb
123
123
  - lib/kvg_character_recognition/recognizer.rb
124
+ - lib/kvg_character_recognition/trainer.rb
124
125
  - lib/kvg_character_recognition/utils.rb
125
126
  - lib/kvg_character_recognition/version.rb
126
127
  homepage: https://github.com/thebluber/kvg_character_recognition