kvg_character_recognition 0.1.0 → 0.1.2

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: 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