percept 0.0.1 → 3.0.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.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +34 -0
- data/.gitignore +3 -1
- data/.rspec +3 -0
- data/.rubocop.yml +110 -0
- data/.rubocop_todo.yml +383 -0
- data/.tool-versions +2 -0
- data/Gemfile +12 -1
- data/Gemfile.lock +138 -0
- data/Guardfile +10 -0
- data/README.md +20 -0
- data/Rakefile +18 -0
- data/bin/percept +8 -1
- data/lib/ext/rmagick_pixel.rb +19 -0
- data/lib/percept/checkbox_collection.rb +19 -0
- data/lib/percept/checkbox_detector.rb +19 -0
- data/lib/percept/checkbox_field.rb +24 -0
- data/lib/percept/color_utils.rb +17 -0
- data/lib/percept/config.rb +35 -0
- data/lib/percept/field.rb +74 -0
- data/lib/percept/field_collection.rb +29 -0
- data/lib/percept/field_detector.rb +56 -0
- data/lib/percept/image.rb +60 -0
- data/lib/percept/line.rb +116 -4
- data/lib/percept/line_collection.rb +88 -0
- data/lib/percept/line_detector.rb +102 -0
- data/lib/percept/line_filter.rb +95 -0
- data/lib/percept/pixel.rb +28 -0
- data/lib/percept/pixel_utils.rb +73 -0
- data/lib/percept/rmagick_image.rb +49 -0
- data/lib/percept/runner.rb +15 -96
- data/lib/percept/utils.rb +15 -0
- data/lib/percept/version.rb +3 -1
- data/lib/percept.rb +40 -1
- data/percept.gemspec +6 -7
- data/spec/fixtures/black_lines.png +0 -0
- data/spec/fixtures/bottom_line.png +0 -0
- data/spec/fixtures/bumpy_no_line.png +0 -0
- data/spec/fixtures/corners.png +0 -0
- data/spec/fixtures/divots.png +0 -0
- data/spec/fixtures/field_next_to_text.png +0 -0
- data/spec/fixtures/gray_cell_bottom.png +0 -0
- data/spec/fixtures/gray_image_section.png +0 -0
- data/spec/fixtures/grey_lines.png +0 -0
- data/spec/fixtures/high_line.png +0 -0
- data/spec/fixtures/large_checkbox.png +0 -0
- data/spec/fixtures/large_checkboxes.png +0 -0
- data/spec/fixtures/light_gray.png +0 -0
- data/spec/fixtures/low_line.png +0 -0
- data/spec/fixtures/low_lines.png +0 -0
- data/spec/fixtures/mid_lines.png +0 -0
- data/spec/fixtures/more_corners.png +0 -0
- data/spec/fixtures/more_text_on_line.png +0 -0
- data/spec/fixtures/nub_both_sides.png +0 -0
- data/spec/fixtures/nub_bottom_left.png +0 -0
- data/spec/fixtures/nub_top_right.png +0 -0
- data/spec/fixtures/nubs.png +0 -0
- data/spec/fixtures/slanted_left.png +0 -0
- data/spec/fixtures/slanted_right.png +0 -0
- data/spec/fixtures/small_checkbox.png +0 -0
- data/spec/fixtures/text_no_line.png +0 -0
- data/spec/fixtures/text_no_line_2.png +0 -0
- data/spec/fixtures/text_on_line.png +0 -0
- data/spec/fixtures/thick_line.png +0 -0
- data/spec/fixtures/thin_field.png +0 -0
- data/spec/percept/checkbox_field/height_spec.rb +15 -0
- data/spec/percept/checkbox_field/width_spec.rb +15 -0
- data/spec/percept/field/contains_spec.rb +50 -0
- data/spec/percept/field_detector/detect_field_spec.rb +90 -0
- data/spec/percept/field_detector/detect_fields_spec.rb +42 -0
- data/spec/percept/fields_collection/shovel_spec.rb +53 -0
- data/spec/percept/line/approxequals_spec.rb +60 -0
- data/spec/percept/line/initialize_spec.rb +29 -0
- data/spec/percept/line/length_spec.rb +22 -0
- data/spec/percept/line/merge_bang_spec.rb +58 -0
- data/spec/percept/line/pixels_spec.rb +15 -0
- data/spec/percept/line/split_spec.rb +28 -0
- data/spec/percept/line/to_s_spec.rb +13 -0
- data/spec/percept/line_collection/merge_line_spec.rb +50 -0
- data/spec/percept/line_collection/remove_unwanted_lines_bang_spec.rb +17 -0
- data/spec/percept/line_collection/split_spec.rb +20 -0
- data/spec/percept/line_collection/to_s_spec.rb +11 -0
- data/spec/percept/line_detector/detect_lines_spec.rb +93 -0
- data/spec/percept/line_detector/scaled_color_spec.rb +14 -0
- data/spec/percept/line_filter/filter_line_predicate_spec.rb +29 -0
- data/spec/percept/line_filter/left_bottom_corner_predicate_spec.rb +49 -0
- data/spec/percept/line_filter/left_top_corner_predicate_spec.rb +50 -0
- data/spec/percept/line_filter/right_bottom_corner_predicate_spec.rb +49 -0
- data/spec/percept/line_filter/right_top_corner_predicate_spec.rb +49 -0
- data/spec/percept/line_filter/table_bottom_predicate_spec.rb +48 -0
- data/spec/percept/line_filter/table_top_predicate_spec.rb +48 -0
- data/spec/percept/pixel/blackish_predicate_spec.rb +36 -0
- data/spec/percept/rmagick_image/interface_spec.rb +11 -0
- data/spec/percept/runner/color_fields_spec.rb +28 -0
- data/spec/percept/runner/color_lines_spec.rb +27 -0
- data/spec/shared_examples/image_interface_shared_examples.rb +162 -0
- data/spec/spec_helper.rb +52 -0
- data/tddium.yml +12 -0
- data/test_images/funky_image.png +0 -0
- data/test_images/interaction_test.png +0 -0
- data/test_images/jagged_lines.png +0 -0
- data/test_images/permission.png +0 -0
- data/test_images/rough.png +0 -0
- data/test_images/slanted.png +0 -0
- data/test_images/time_sheet.png +0 -0
- data/test_images/travel_reimbursement.png +0 -0
- data/test_images/warehouse_form.png +0 -0
- metadata +171 -34
- data/new_permission.png +0 -0
- data/permission.png +0 -0
data/Gemfile.lock
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
percept (3.0.0)
|
5
|
+
rmagick (~> 6.1.1)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
ast (2.4.3)
|
11
|
+
coderay (1.1.3)
|
12
|
+
colorize (1.1.0)
|
13
|
+
diff-lcs (1.6.2)
|
14
|
+
docile (1.4.1)
|
15
|
+
ffi (1.17.2-x86_64-darwin)
|
16
|
+
ffi (1.17.2-x86_64-linux-gnu)
|
17
|
+
formatador (1.1.0)
|
18
|
+
guard (2.19.1)
|
19
|
+
formatador (>= 0.2.4)
|
20
|
+
listen (>= 2.7, < 4.0)
|
21
|
+
logger (~> 1.6)
|
22
|
+
lumberjack (>= 1.0.12, < 2.0)
|
23
|
+
nenv (~> 0.1)
|
24
|
+
notiffany (~> 0.0)
|
25
|
+
ostruct (~> 0.6)
|
26
|
+
pry (>= 0.13.0)
|
27
|
+
shellany (~> 0.0)
|
28
|
+
thor (>= 0.18.1)
|
29
|
+
guard-compat (1.2.1)
|
30
|
+
guard-rspec (4.7.3)
|
31
|
+
guard (~> 2.1)
|
32
|
+
guard-compat (~> 1.1)
|
33
|
+
rspec (>= 2.99.0, < 4.0)
|
34
|
+
guard-rubocop (1.5.0)
|
35
|
+
guard (~> 2.0)
|
36
|
+
rubocop (< 2.0)
|
37
|
+
json (2.12.0)
|
38
|
+
language_server-protocol (3.17.0.5)
|
39
|
+
lint_roller (1.1.0)
|
40
|
+
listen (3.9.0)
|
41
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
42
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
43
|
+
logger (1.7.0)
|
44
|
+
lumberjack (1.2.10)
|
45
|
+
method_source (1.1.0)
|
46
|
+
nenv (0.3.0)
|
47
|
+
notiffany (0.1.3)
|
48
|
+
nenv (~> 0.1)
|
49
|
+
shellany (~> 0.0)
|
50
|
+
observer (0.1.2)
|
51
|
+
ostruct (0.6.1)
|
52
|
+
parallel (1.27.0)
|
53
|
+
parser (3.3.8.0)
|
54
|
+
ast (~> 2.4.1)
|
55
|
+
racc
|
56
|
+
pkg-config (1.6.2)
|
57
|
+
prism (1.4.0)
|
58
|
+
pry (0.15.2)
|
59
|
+
coderay (~> 1.1)
|
60
|
+
method_source (~> 1.0)
|
61
|
+
racc (1.8.1)
|
62
|
+
rainbow (3.1.1)
|
63
|
+
rake (13.2.1)
|
64
|
+
rb-fsevent (0.11.2)
|
65
|
+
rb-inotify (0.11.1)
|
66
|
+
ffi (~> 1.0)
|
67
|
+
regexp_parser (2.10.0)
|
68
|
+
rmagick (6.1.1)
|
69
|
+
observer (~> 0.1)
|
70
|
+
pkg-config (~> 1.4)
|
71
|
+
rspec (3.13.0)
|
72
|
+
rspec-core (~> 3.13.0)
|
73
|
+
rspec-expectations (~> 3.13.0)
|
74
|
+
rspec-mocks (~> 3.13.0)
|
75
|
+
rspec-core (3.13.3)
|
76
|
+
rspec-support (~> 3.13.0)
|
77
|
+
rspec-expectations (3.13.4)
|
78
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
79
|
+
rspec-support (~> 3.13.0)
|
80
|
+
rspec-mocks (3.13.4)
|
81
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
82
|
+
rspec-support (~> 3.13.0)
|
83
|
+
rspec-support (3.13.3)
|
84
|
+
rtesseract (3.1.4)
|
85
|
+
rubocop (1.75.6)
|
86
|
+
json (~> 2.3)
|
87
|
+
language_server-protocol (~> 3.17.0.2)
|
88
|
+
lint_roller (~> 1.1.0)
|
89
|
+
parallel (~> 1.10)
|
90
|
+
parser (>= 3.3.0.2)
|
91
|
+
rainbow (>= 2.2.2, < 4.0)
|
92
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
93
|
+
rubocop-ast (>= 1.44.0, < 2.0)
|
94
|
+
ruby-progressbar (~> 1.7)
|
95
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
96
|
+
rubocop-ast (1.44.1)
|
97
|
+
parser (>= 3.3.7.2)
|
98
|
+
prism (~> 1.4)
|
99
|
+
rubocop-rake (0.7.1)
|
100
|
+
lint_roller (~> 1.1)
|
101
|
+
rubocop (>= 1.72.1)
|
102
|
+
rubocop-rspec (3.6.0)
|
103
|
+
lint_roller (~> 1.1)
|
104
|
+
rubocop (~> 1.72, >= 1.72.1)
|
105
|
+
ruby-progressbar (1.13.0)
|
106
|
+
shellany (0.0.1)
|
107
|
+
simplecov (0.22.0)
|
108
|
+
docile (~> 1.1)
|
109
|
+
simplecov-html (~> 0.11)
|
110
|
+
simplecov_json_formatter (~> 0.1)
|
111
|
+
simplecov-html (0.13.1)
|
112
|
+
simplecov_json_formatter (0.1.4)
|
113
|
+
thor (1.3.2)
|
114
|
+
unicode-display_width (3.1.4)
|
115
|
+
unicode-emoji (~> 4.0, >= 4.0.4)
|
116
|
+
unicode-emoji (4.0.4)
|
117
|
+
|
118
|
+
PLATFORMS
|
119
|
+
x86_64-darwin-23
|
120
|
+
x86_64-darwin-24
|
121
|
+
x86_64-linux
|
122
|
+
|
123
|
+
DEPENDENCIES
|
124
|
+
colorize
|
125
|
+
guard
|
126
|
+
guard-rspec
|
127
|
+
guard-rubocop
|
128
|
+
percept!
|
129
|
+
rake
|
130
|
+
rspec
|
131
|
+
rtesseract
|
132
|
+
rubocop
|
133
|
+
rubocop-rake
|
134
|
+
rubocop-rspec
|
135
|
+
simplecov
|
136
|
+
|
137
|
+
BUNDLED WITH
|
138
|
+
2.6.8
|
data/Guardfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
guard :rspec, failed_mode: :keep, cmd: 'rspec' do
|
2
|
+
watch(%r{^spec/.+_spec\.rb$})
|
3
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}/" }
|
4
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
5
|
+
end
|
6
|
+
|
7
|
+
guard :rubocop, all_on_start: false, cli: ['-D'] do
|
8
|
+
watch(/.+\.rb$/)
|
9
|
+
watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
|
10
|
+
end
|
data/README.md
CHANGED
@@ -27,3 +27,23 @@ TODO: Write usage instructions here
|
|
27
27
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
28
|
4. Push to the branch (`git push origin my-new-feature`)
|
29
29
|
5. Create new Pull Request
|
30
|
+
|
31
|
+
## Publishing
|
32
|
+
|
33
|
+
1. Set up a personal access token with the `write:packages` scope and add it to
|
34
|
+
your `~/.gem/credentials` file as described in [GitHub's
|
35
|
+
documentation][github-rubygems-registry-doc]. Set your `credentials` file to
|
36
|
+
be only readable by you, if you haven't already:
|
37
|
+
```
|
38
|
+
chmod 0600 ~/.gem/credentials
|
39
|
+
```
|
40
|
+
2. Build the gem:
|
41
|
+
```
|
42
|
+
gem build percept.gemspec
|
43
|
+
```
|
44
|
+
3. Publish to Github Packages:
|
45
|
+
```
|
46
|
+
gem push --key github --host https://rubygems.pkg.github.com/informedk12 percept-<version>.gem`
|
47
|
+
```
|
48
|
+
|
49
|
+
[github-rubygems-registry-doc]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-rubygems-registry#authenticating-with-a-personal-access-token
|
data/Rakefile
CHANGED
@@ -1 +1,19 @@
|
|
1
1
|
require 'bundler/gem_tasks'
|
2
|
+
require 'rubocop/rake_task'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
task default: [:rubocop, :spec]
|
6
|
+
|
7
|
+
RuboCop::RakeTask.new
|
8
|
+
|
9
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
10
|
+
t.rspec_opts = '--force-color'
|
11
|
+
end
|
12
|
+
|
13
|
+
task :console do
|
14
|
+
require 'irb'
|
15
|
+
require 'irb/completion'
|
16
|
+
require_relative 'lib/percept'
|
17
|
+
ARGV.clear
|
18
|
+
IRB.start
|
19
|
+
end
|
data/bin/percept
CHANGED
@@ -2,4 +2,11 @@
|
|
2
2
|
|
3
3
|
require_relative '../lib/percept'
|
4
4
|
|
5
|
-
|
5
|
+
if ARGV.length < 2
|
6
|
+
puts 'need an option and file name as arguments'
|
7
|
+
exit
|
8
|
+
elsif ARGV.first == '--lines'
|
9
|
+
Percept::Runner.new.color_lines(ARGV.last)
|
10
|
+
elsif ARGV.first == '--fields'
|
11
|
+
Percept::Runner.new.color_fields(ARGV.last)
|
12
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Magick
|
2
|
+
|
3
|
+
class Pixel
|
4
|
+
|
5
|
+
include Percept::PixelUtils
|
6
|
+
|
7
|
+
def scale_down(color_value)
|
8
|
+
return 0 if color_value.zero?
|
9
|
+
(color_value + 1) / 256 - 1
|
10
|
+
end
|
11
|
+
|
12
|
+
def scale_up(color_value)
|
13
|
+
return 0 if color_value.zero?
|
14
|
+
(color_value + 1) * 256 - 1
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Percept
|
2
|
+
|
3
|
+
class CheckboxDetector
|
4
|
+
|
5
|
+
include Percept::Utils
|
6
|
+
|
7
|
+
attr_accessor :image
|
8
|
+
|
9
|
+
def detect_checkboxes(image)
|
10
|
+
self.image = image
|
11
|
+
# checkboxes = checkbox_collection.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def find_checkboxes
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Percept
|
2
|
+
|
3
|
+
class CheckboxField
|
4
|
+
|
5
|
+
attr_accessor :start_x, :end_x, :start_y, :end_y
|
6
|
+
|
7
|
+
def initialize(params)
|
8
|
+
self.start_x = params.fetch(:start_x)
|
9
|
+
self.start_y = params.fetch(:start_y)
|
10
|
+
self.end_x = params.fetch(:end_x)
|
11
|
+
self.end_y = params.fetch(:end_y)
|
12
|
+
end
|
13
|
+
|
14
|
+
def width
|
15
|
+
end_x - start_x
|
16
|
+
end
|
17
|
+
|
18
|
+
def height
|
19
|
+
end_y - start_y
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Percept
|
2
|
+
|
3
|
+
class Config
|
4
|
+
|
5
|
+
ATTRIBUTES = %i[
|
6
|
+
line_length color_tolerance field_padding min_field_height
|
7
|
+
max_field_height image_class output_path max_color_tolerance
|
8
|
+
min_color_tolerance field_edge_buffer max_line_height
|
9
|
+
].freeze
|
10
|
+
|
11
|
+
attr_accessor(*ATTRIBUTES)
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
self.line_length = options.fetch(:line_length) { 30 }
|
15
|
+
self.max_line_height = options.fetch(:max_line_height) { 10 }
|
16
|
+
self.min_color_tolerance = options.fetch(:min_color_tolerance) { 200 }
|
17
|
+
self.max_color_tolerance = options.fetch(:min_color_tolerance) { 230 }
|
18
|
+
self.color_tolerance = min_color_tolerance
|
19
|
+
self.field_padding = options.fetch(:field_padding) { 1 }
|
20
|
+
self.min_field_height = options.fetch(:min_field_height) { 20 }
|
21
|
+
self.max_field_height = options.fetch(:max_field_height) { 60 }
|
22
|
+
self.field_edge_buffer = options.fetch(:field_edge_buffer) { 2 }
|
23
|
+
self.image_class = options.fetch(:image_class) { Percept::RMagickImage }
|
24
|
+
self.output_path = options.fetch(:output_path) { 'output.png' }
|
25
|
+
invalid_keys = options.keys - Config::ATTRIBUTES
|
26
|
+
raise "invalid keys: #{invalid_keys}" if invalid_keys.any?
|
27
|
+
end
|
28
|
+
|
29
|
+
def maximize_color_tolerance!
|
30
|
+
self.color_tolerance = max_color_tolerance
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Percept
|
2
|
+
|
3
|
+
class Field
|
4
|
+
|
5
|
+
include Percept::ColorUtils
|
6
|
+
|
7
|
+
attr_accessor :start_x, :end_x, :start_y, :end_y, :line, :image
|
8
|
+
|
9
|
+
def initialize(params)
|
10
|
+
self.start_x = params.fetch(:start_x)
|
11
|
+
self.end_x = params.fetch(:end_x)
|
12
|
+
self.start_y = params.fetch(:start_y)
|
13
|
+
self.end_y = params.fetch(:end_y)
|
14
|
+
self.line = params.fetch(:line)
|
15
|
+
self.image = params.fetch(:image)
|
16
|
+
end
|
17
|
+
|
18
|
+
def pixels
|
19
|
+
@pixels ||= top_pixels + bottom_pixels + left_pixels + right_pixels
|
20
|
+
end
|
21
|
+
|
22
|
+
def height
|
23
|
+
end_y - start_y
|
24
|
+
end
|
25
|
+
|
26
|
+
def width
|
27
|
+
end_x - start_x
|
28
|
+
end
|
29
|
+
|
30
|
+
def enclosed_pixels
|
31
|
+
image.get_pixels(start_x, start_y, width, height)
|
32
|
+
end
|
33
|
+
|
34
|
+
def blackish_pixels
|
35
|
+
enclosed_pixels.select(&:blackish?)
|
36
|
+
end
|
37
|
+
|
38
|
+
def contains_text?
|
39
|
+
blackish_pixels.size > 100
|
40
|
+
end
|
41
|
+
|
42
|
+
def overlaps?(other)
|
43
|
+
!(start_x > other.end_x || end_x < other.start_x ||
|
44
|
+
start_y > other.end_y || end_y < other.start_y)
|
45
|
+
end
|
46
|
+
|
47
|
+
def store_pixels!
|
48
|
+
image.store_pixels(start_x, start_y, width, 1, top_pixels)
|
49
|
+
image.store_pixels(start_x, end_y - 1, width, 1, bottom_pixels)
|
50
|
+
image.store_pixels(start_x, start_y + 1, 1, height - 2, left_pixels)
|
51
|
+
image.store_pixels(end_x - 1, start_y + 1, 1, height - 2, right_pixels)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def top_pixels
|
57
|
+
@top_pixels ||= image.get_pixels(start_x, start_y, width, 1)
|
58
|
+
end
|
59
|
+
|
60
|
+
def bottom_pixels
|
61
|
+
@bottom_pixels ||= image.get_pixels(start_x, end_y - 1, width, 1)
|
62
|
+
end
|
63
|
+
|
64
|
+
def left_pixels
|
65
|
+
@left_pixels ||= image.get_pixels(start_x, start_y + 1, 1, height - 2)
|
66
|
+
end
|
67
|
+
|
68
|
+
def right_pixels
|
69
|
+
@right_pixels ||= image.get_pixels(end_x - 1, start_y + 1, 1, height - 2)
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Percept
|
2
|
+
|
3
|
+
class FieldCollection
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_accessor :fields
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
self.fields = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def <<(new_field)
|
14
|
+
return if fields.any? { |field| field.overlaps?(new_field) }
|
15
|
+
|
16
|
+
fields << new_field
|
17
|
+
end
|
18
|
+
|
19
|
+
def each
|
20
|
+
fields.each { |field| yield(field) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def size
|
24
|
+
fields.size
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Percept
|
2
|
+
|
3
|
+
class FieldDetector
|
4
|
+
|
5
|
+
def detect_fields(image)
|
6
|
+
field_collection = FieldCollection.new
|
7
|
+
|
8
|
+
image.filtered_lines.map do |line|
|
9
|
+
field = detect_field(line, image)
|
10
|
+
field_collection << field if field
|
11
|
+
end
|
12
|
+
|
13
|
+
field_collection
|
14
|
+
end
|
15
|
+
|
16
|
+
def detect_field(line, image)
|
17
|
+
stopping_point = [0, line.start_y - Percept.config.max_field_height].max
|
18
|
+
starting_point = line.start_y - 2
|
19
|
+
min_index = line.start_y - Percept.config.min_field_height
|
20
|
+
|
21
|
+
return nil unless min_index >= 0
|
22
|
+
|
23
|
+
buffered_start = line.start_x + Percept.config.field_edge_buffer
|
24
|
+
buffered_length = line.length - 2 * Percept.config.field_edge_buffer
|
25
|
+
|
26
|
+
starting_point.downto(stopping_point) do |index|
|
27
|
+
pixels = image.get_pixels(buffered_start, index, buffered_length, 1)
|
28
|
+
blackish_pixel = pixels.any?(&:blackish?)
|
29
|
+
|
30
|
+
if index >= min_index && blackish_pixel
|
31
|
+
return nil
|
32
|
+
elsif blackish_pixel
|
33
|
+
return Percept::Field.new(
|
34
|
+
start_x: line.start_x,
|
35
|
+
end_x: line.end_x,
|
36
|
+
start_y: index + 1,
|
37
|
+
end_y: line.start_y,
|
38
|
+
line: line,
|
39
|
+
image: image,
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Percept::Field.new(
|
45
|
+
start_x: line.start_x,
|
46
|
+
end_x: line.end_x,
|
47
|
+
start_y: stopping_point,
|
48
|
+
end_y: line.start_y,
|
49
|
+
line: line,
|
50
|
+
image: image,
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Percept
|
2
|
+
|
3
|
+
class Image
|
4
|
+
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_accessor :file_path
|
8
|
+
|
9
|
+
def initialize(file_path)
|
10
|
+
self.file_path = file_path
|
11
|
+
end
|
12
|
+
|
13
|
+
def config
|
14
|
+
Percept.config
|
15
|
+
end
|
16
|
+
|
17
|
+
def filtered_lines
|
18
|
+
lines.filtered.split
|
19
|
+
end
|
20
|
+
|
21
|
+
def fields
|
22
|
+
@fields ||= FieldDetector.new.detect_fields(self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def checkbox_fields
|
26
|
+
[CheckboxField.new(start_x: 10, end_x: 27, start_y: 5, end_y: 22)]
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_pixels(x, y, columns, rows)
|
30
|
+
retrieve_pixels(x, y, columns, rows)
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_rows(x, y, columns, rows)
|
34
|
+
retrieve_pixels(x, y, columns, rows).each_slice(rows).to_a
|
35
|
+
end
|
36
|
+
|
37
|
+
def retrieve_row(row_number)
|
38
|
+
get_pixels(0, row_number, column_count, 1)
|
39
|
+
end
|
40
|
+
|
41
|
+
def lines
|
42
|
+
@lines ||= LineDetector.new.detect_lines(self)
|
43
|
+
end
|
44
|
+
|
45
|
+
def magick_column(column_number)
|
46
|
+
get_pixels(column_number, 0, 1, row_count)
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
"#<#{self.class}:#{object_id} " \
|
51
|
+
"columns: #{column_count}, rows: #{row_count}>"
|
52
|
+
end
|
53
|
+
|
54
|
+
def inspect
|
55
|
+
to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
data/lib/percept/line.rb
CHANGED
@@ -2,17 +2,129 @@ module Percept
|
|
2
2
|
|
3
3
|
class Line
|
4
4
|
|
5
|
-
|
5
|
+
include Percept::Utils
|
6
|
+
include Percept::ColorUtils
|
7
|
+
|
8
|
+
attr_accessor :start_x, :end_x, :start_y, :rows, :image
|
9
|
+
attr_reader :end_y
|
10
|
+
|
6
11
|
def initialize(params)
|
7
12
|
self.start_x = params.fetch(:start_x)
|
8
13
|
self.end_x = params.fetch(:end_x)
|
9
14
|
self.start_y = params.fetch(:start_y)
|
10
|
-
self.end_y = params.fetch(:end_y) { start_y }
|
11
|
-
self.
|
15
|
+
self.end_y = params.fetch(:end_y) { start_y + 1 }
|
16
|
+
self.rows = params.fetch(:rows) { [params.fetch(:row)] }
|
17
|
+
self.image = params.fetch(:image)
|
12
18
|
end
|
13
19
|
|
14
20
|
def pixels
|
15
|
-
row
|
21
|
+
@pixels ||= rows.flat_map { |row| row[start_x...end_x] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def =~(other)
|
25
|
+
overlaps_x?(other) && overlaps_y?(other)
|
26
|
+
end
|
27
|
+
|
28
|
+
def overlaps_x?(other)
|
29
|
+
!(end_x < other.start_x || start_x > other.end_x)
|
30
|
+
end
|
31
|
+
|
32
|
+
def overlaps_y?(other)
|
33
|
+
!(end_y < other.start_y || start_y > other.end_y)
|
34
|
+
end
|
35
|
+
|
36
|
+
def end_y=(new_end_y)
|
37
|
+
raise 'end_y must be greater than start_y' unless new_end_y > start_y
|
38
|
+
@end_y = new_end_y
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
attributes.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
def inspect
|
46
|
+
to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def attributes
|
50
|
+
{
|
51
|
+
start_x: start_x,
|
52
|
+
end_x: end_x,
|
53
|
+
start_y: start_y,
|
54
|
+
end_y: end_y,
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def full_attributes
|
59
|
+
attributes.merge(rows: rows, image: image)
|
60
|
+
end
|
61
|
+
|
62
|
+
def length
|
63
|
+
end_x - start_x
|
64
|
+
end
|
65
|
+
|
66
|
+
def height
|
67
|
+
end_y - start_y
|
68
|
+
end
|
69
|
+
|
70
|
+
def split
|
71
|
+
lines = LineCollection.new
|
72
|
+
next_x = start_x
|
73
|
+
intersections.each do |x_coordinate|
|
74
|
+
if x_coordinate == next_x
|
75
|
+
next_x += 1
|
76
|
+
next
|
77
|
+
end
|
78
|
+
line = copy_line(start_x: next_x, end_x: x_coordinate)
|
79
|
+
next_x = x_coordinate + 1
|
80
|
+
lines << line unless line.short?
|
81
|
+
end
|
82
|
+
if next_x < end_x
|
83
|
+
line = copy_line(start_x: next_x)
|
84
|
+
lines << line unless line.short?
|
85
|
+
end
|
86
|
+
lines
|
87
|
+
end
|
88
|
+
|
89
|
+
def copy_line(params)
|
90
|
+
Line.new(full_attributes.merge(params))
|
91
|
+
end
|
92
|
+
|
93
|
+
def columns_for(x_range, start_y, row_count = 2)
|
94
|
+
x_range.map do |x|
|
95
|
+
next if x > image.column_count || start_y + row_count > image.row_count
|
96
|
+
image.get_pixels(x, start_y, 1, row_count)
|
97
|
+
end.compact
|
98
|
+
end
|
99
|
+
|
100
|
+
def intersections
|
101
|
+
above_columns = columns_for(start_x..end_x, start_y - 4, 4)
|
102
|
+
vertical_dividers = []
|
103
|
+
above_columns.each_with_index do |column, index|
|
104
|
+
vertical_dividers << index if column.any?(&:blackish?)
|
105
|
+
end
|
106
|
+
vertical_dividers.map { |x| x + start_x }
|
107
|
+
end
|
108
|
+
|
109
|
+
def merge!(other)
|
110
|
+
if start_y > other.start_y
|
111
|
+
self.start_y = other.start_y
|
112
|
+
elsif end_y < other.end_y
|
113
|
+
self.end_y = other.end_y
|
114
|
+
end
|
115
|
+
|
116
|
+
self.start_x = other.start_x if start_x > other.start_x
|
117
|
+
self.end_x = other.end_x if end_x < other.end_x
|
118
|
+
|
119
|
+
self.rows += other.rows
|
120
|
+
end
|
121
|
+
|
122
|
+
def short?
|
123
|
+
length < Percept.config.line_length
|
124
|
+
end
|
125
|
+
|
126
|
+
def store_pixels!
|
127
|
+
image.store_pixels(start_x, start_y, length, height, pixels)
|
16
128
|
end
|
17
129
|
|
18
130
|
end
|