imatcher 0.1.3 → 0.1.4
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 +4 -4
- data/README.md +16 -0
- data/imatcher.gemspec +0 -1
- data/lib/imatcher/color_methods.rb +12 -0
- data/lib/imatcher/image.rb +18 -9
- data/lib/imatcher/matcher.rb +23 -5
- data/lib/imatcher/modes/base.rb +24 -11
- data/lib/imatcher/modes/delta.rb +1 -1
- data/lib/imatcher/modes/rgb.rb +1 -1
- data/lib/imatcher/rectangle.rb +31 -0
- data/lib/imatcher/version.rb +1 -1
- data/spec/fixtures/a1.png +0 -0
- data/spec/fixtures/delta_diff.png +0 -0
- data/spec/fixtures/exclude.png +0 -0
- data/spec/fixtures/grayscale_diff.png +0 -0
- data/spec/fixtures/include.png +0 -0
- data/spec/fixtures/rgb_diff.png +0 -0
- data/spec/image_spec.rb +11 -0
- data/spec/integrations/delta_spec.rb +2 -2
- data/spec/integrations/rgb_spec.rb +25 -0
- data/spec/matcher_spec.rb +34 -2
- data/spec/rectangle_spec.rb +37 -0
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c95e7ff691b3bd8a4c0225764cd6352e0b013cc3
|
4
|
+
data.tar.gz: 2ad7efe2e068f910d686d18b3f84ff35832354d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 14b97bfa6fc938c673aa126a56604bd51c1766b596e26302dd08416299219e058d8e294260f06c72099066e77df322ec393010541b2b57f3d46515eb96ece271
|
7
|
+
data.tar.gz: 0ccb28a3281c111a3a552f1a42c657113e868e8e2bc6cd6eab106ce281c1aba92a49f688ffa453b3d6fa141170839e9bf01758d570e3447f25bc15b62e30e689
|
data/README.md
CHANGED
@@ -88,6 +88,22 @@ res = Imatcher::Matcher.new(options).compare(path_1, path_2)
|
|
88
88
|
|
89
89
|
```
|
90
90
|
|
91
|
+
## Excluding area
|
92
|
+
|
93
|
+
<img src="https://raw.githubusercontent.com/teachbase/imatcher/master/spec/fixtures/a.png" width="300" />
|
94
|
+
<img src="https://raw.githubusercontent.com/teachbase/imatcher/master/spec/fixtures/a1.png" width="300" />
|
95
|
+
|
96
|
+
You can exclude area from comparing by passing `:exclude_area` to `compare`.
|
97
|
+
E.g., if `path_1` and `path_2` contain images above
|
98
|
+
```ruby
|
99
|
+
Imatcher.compare(path_1, path_2, exclude_area: [200, 150, 275, 200]).match? # => true
|
100
|
+
```
|
101
|
+
`[200, 150, 275, 200]` is array of two vertices of rectangle -- (200, 150) is left-top vertex and (275, 200) is right-bottom.
|
102
|
+
|
103
|
+
## Including area
|
104
|
+
|
105
|
+
You can set bounds of comparing by passing `:include_area` to `compare` with array similar to previous example
|
106
|
+
|
91
107
|
## Contributing
|
92
108
|
|
93
109
|
Bug reports and pull requests are welcome on GitHub at https://github.com/teachbase/imatcher.
|
data/imatcher.gemspec
CHANGED
@@ -6,7 +6,6 @@ Gem::Specification.new do |spec|
|
|
6
6
|
spec.version = Imatcher::VERSION
|
7
7
|
spec.authors = ["palkan"]
|
8
8
|
spec.email = ["dementiev.vm@gmail.com"]
|
9
|
-
|
10
9
|
spec.summary = "Image comparison lib"
|
11
10
|
spec.description = "Image comparison lib built on top of ChunkyPNG"
|
12
11
|
spec.homepage = "http://github.com/teachbase/imatcher"
|
data/lib/imatcher/image.rb
CHANGED
@@ -13,11 +13,13 @@ module Imatcher
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
-
def compare_each_pixel(image)
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
16
|
+
def compare_each_pixel(image, area: nil)
|
17
|
+
area = bounding_rect if area.nil?
|
18
|
+
(area.top..area.bot).each do |y|
|
19
|
+
range = (area.left..area.right)
|
20
|
+
next if image.row(y).slice(range) == row(y).slice(range)
|
21
|
+
(area.left..area.right).each do |x|
|
22
|
+
yield(self[x, y], image[x, y], x, y)
|
21
23
|
end
|
22
24
|
end
|
23
25
|
end
|
@@ -40,12 +42,19 @@ module Imatcher
|
|
40
42
|
[width, height] == [image.width, image.height]
|
41
43
|
end
|
42
44
|
|
43
|
-
def render_bounds(left, bot, right, top)
|
44
|
-
rect(left, bot, right, top, rgb(255, 0, 0))
|
45
|
-
end
|
46
|
-
|
47
45
|
def inspect
|
48
46
|
"Image:#{object_id}<#{width}x#{height}>"
|
49
47
|
end
|
48
|
+
|
49
|
+
def highlight_rectangle(rect, color = :red)
|
50
|
+
fail ArgumentError, "Undefined color: #{color}" unless respond_to?(color)
|
51
|
+
return self if rect.nil?
|
52
|
+
rect(*rect.bounds, send(color))
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def bounding_rect
|
57
|
+
Rectangle.new(0, 0, width - 1, height - 1)
|
58
|
+
end
|
50
59
|
end
|
51
60
|
end
|
data/lib/imatcher/matcher.rb
CHANGED
@@ -15,17 +15,35 @@ module Imatcher
|
|
15
15
|
|
16
16
|
def initialize(options = {})
|
17
17
|
mode_type = options.delete(:mode) || :rgb
|
18
|
+
fail ArgumentError, "Undefined mode: #{ mode_type }" unless MODES.keys.include?(mode_type)
|
18
19
|
@mode = Modes.const_get(MODES[mode_type]).new(options)
|
19
20
|
end
|
20
21
|
|
21
22
|
def compare(a, b)
|
22
23
|
a = Image.from_file(a) unless a.is_a?(Image)
|
23
24
|
b = Image.from_file(b) unless b.is_a?(Image)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
|
26
|
+
fail SizesMismatchError,
|
27
|
+
"Size mismatch: first image size: " \
|
28
|
+
"#{a.width}x#{a.height}, " \
|
29
|
+
"second image size: " \
|
30
|
+
"#{b.width}x#{b.height}" unless a.sizes_match?(b)
|
31
|
+
|
32
|
+
image_area = Rectangle.new(0, 0, a.width - 1, a.height - 1)
|
33
|
+
|
34
|
+
unless mode.exclude_rect.nil?
|
35
|
+
fail ArgumentError,
|
36
|
+
"Bounds must be in image" unless image_area.contains?(mode.exclude_rect)
|
37
|
+
end
|
38
|
+
|
39
|
+
unless mode.include_rect.nil?
|
40
|
+
fail ArgumentError,
|
41
|
+
"Bounds must be in image" unless image_area.contains?(mode.include_rect)
|
42
|
+
unless mode.exclude_rect.nil?
|
43
|
+
fail ArgumentError,
|
44
|
+
"Included area must contain excluded" unless mode.include_rect.contains?(mode.exclude_rect)
|
45
|
+
end
|
46
|
+
end
|
29
47
|
|
30
48
|
mode.compare(a, b)
|
31
49
|
end
|
data/lib/imatcher/modes/base.rb
CHANGED
@@ -1,21 +1,26 @@
|
|
1
1
|
module Imatcher
|
2
2
|
module Modes
|
3
3
|
class Base # :nodoc:
|
4
|
+
require 'imatcher/rectangle'
|
4
5
|
include ColorMethods
|
5
6
|
|
6
|
-
attr_reader :result, :threshold, :bounds
|
7
|
+
attr_reader :result, :threshold, :bounds, :exclude_rect, :include_rect
|
7
8
|
|
8
|
-
def initialize(threshold: 0.0)
|
9
|
+
def initialize(threshold: 0.0, exclude_rect: nil, include_rect: nil)
|
10
|
+
@include_rect = Rectangle.new(*include_rect) unless include_rect.nil?
|
11
|
+
@exclude_rect = Rectangle.new(*exclude_rect) unless exclude_rect.nil?
|
9
12
|
@threshold = threshold
|
10
13
|
@result = Result.new(self, threshold)
|
11
14
|
end
|
12
15
|
|
13
16
|
def compare(a, b)
|
14
17
|
result.image = a
|
15
|
-
@
|
18
|
+
@include_rect ||= a.bounding_rect
|
19
|
+
@bounds = Rectangle.new(*include_rect.bounds)
|
16
20
|
|
17
|
-
b.compare_each_pixel(a) do |b_pixel, a_pixel, x, y|
|
21
|
+
b.compare_each_pixel(a, area: include_rect) do |b_pixel, a_pixel, x, y|
|
18
22
|
next if pixels_equal?(b_pixel, a_pixel)
|
23
|
+
next if !exclude_rect.nil? && exclude_rect.contains_point?(x, y)
|
19
24
|
update_result(b_pixel, a_pixel, x, y)
|
20
25
|
end
|
21
26
|
|
@@ -24,15 +29,17 @@ module Imatcher
|
|
24
29
|
end
|
25
30
|
|
26
31
|
def diff(bg, diff)
|
27
|
-
diff_image = background(bg)
|
32
|
+
diff_image = background(bg).highlight_rectangle(exclude_rect, :blue)
|
28
33
|
diff.each do |pixels_pair|
|
29
34
|
pixels_diff(diff_image, *pixels_pair)
|
30
35
|
end
|
31
|
-
create_diff_image(bg, diff_image).
|
36
|
+
create_diff_image(bg, diff_image).
|
37
|
+
highlight_rectangle(bounds).
|
38
|
+
highlight_rectangle(include_rect, :green)
|
32
39
|
end
|
33
40
|
|
34
41
|
def score
|
35
|
-
|
42
|
+
result.diff.length.to_f / area
|
36
43
|
end
|
37
44
|
|
38
45
|
def update_result(*_args, x, y)
|
@@ -40,10 +47,16 @@ module Imatcher
|
|
40
47
|
end
|
41
48
|
|
42
49
|
def update_bounds(x, y)
|
43
|
-
bounds
|
44
|
-
bounds
|
45
|
-
bounds
|
46
|
-
bounds
|
50
|
+
bounds.left = [x, bounds.left].max
|
51
|
+
bounds.top = [y, bounds.top].max
|
52
|
+
bounds.right = [x, bounds.right].min
|
53
|
+
bounds.bot = [y, bounds.bot].min
|
54
|
+
end
|
55
|
+
|
56
|
+
def area
|
57
|
+
area = include_rect.area
|
58
|
+
return area if exclude_rect.nil?
|
59
|
+
area - exclude_rect.area
|
47
60
|
end
|
48
61
|
end
|
49
62
|
end
|
data/lib/imatcher/modes/delta.rb
CHANGED
data/lib/imatcher/modes/rgb.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Imatcher
|
2
|
+
class Rectangle
|
3
|
+
attr_accessor :left, :top, :right, :bot
|
4
|
+
|
5
|
+
def initialize(l, t, r, b)
|
6
|
+
@left = l
|
7
|
+
@top = t
|
8
|
+
@right = r
|
9
|
+
@bot = b
|
10
|
+
end
|
11
|
+
|
12
|
+
def area
|
13
|
+
(right - left + 1) * (bot - top + 1)
|
14
|
+
end
|
15
|
+
|
16
|
+
def contains?(rect)
|
17
|
+
(left <= rect.left) &&
|
18
|
+
(right >= rect.right) &&
|
19
|
+
(top <= rect.top) &&
|
20
|
+
(bot >= rect.bot)
|
21
|
+
end
|
22
|
+
|
23
|
+
def bounds
|
24
|
+
[left, top, right, bot]
|
25
|
+
end
|
26
|
+
|
27
|
+
def contains_point?(x, y)
|
28
|
+
x.between?(left, right) && y.between?(top, bot)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/imatcher/version.rb
CHANGED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/spec/fixtures/rgb_diff.png
CHANGED
Binary file
|
data/spec/image_spec.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Imatcher::Image do
|
4
|
+
describe "highlight_rectangle" do
|
5
|
+
let(:image) { described_class.new(10, 10, described_class::BLACK) }
|
6
|
+
let(:rect) { Imatcher::Rectangle.new(0, 0, 1, 1) }
|
7
|
+
subject { image.highlight_rectangle(rect, :deep_purple) }
|
8
|
+
|
9
|
+
it { expect { subject }.to raise_error(ArgumentError) }
|
10
|
+
end
|
11
|
+
end
|
@@ -18,13 +18,13 @@ describe Imatcher::Modes::Delta do
|
|
18
18
|
context "below score" do
|
19
19
|
let(:options) { { mode: :delta, threshold: 0.01 } }
|
20
20
|
|
21
|
-
it { expect(subject).to
|
21
|
+
it { expect(subject).to be_falsey }
|
22
22
|
end
|
23
23
|
|
24
24
|
context "above score" do
|
25
25
|
let(:options) { { mode: :delta, threshold: 0.1 } }
|
26
26
|
|
27
|
-
it { expect(subject).to
|
27
|
+
it { expect(subject).to be_truthy }
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
@@ -23,4 +23,29 @@ describe Imatcher::Modes::RGB do
|
|
23
23
|
expect(subject.difference_image).to eq(Imatcher::Image.from_file(image_path("rgb_diff")))
|
24
24
|
end
|
25
25
|
end
|
26
|
+
|
27
|
+
context "exclude rect" do
|
28
|
+
let(:options) { { exclude_rect: [200, 150, 275, 200] } }
|
29
|
+
let(:path_2) { image_path "a1" }
|
30
|
+
it { expect(subject.difference_image).to eq Imatcher::Image.from_file(image_path("exclude")) }
|
31
|
+
it { expect(subject.score).to eq 0 }
|
32
|
+
|
33
|
+
context "calculates score correctly" do
|
34
|
+
let(:path_2) { image_path "darker" }
|
35
|
+
|
36
|
+
it { expect(subject.score).to eq 1 }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "include rect" do
|
41
|
+
let(:options) { { include_rect: [0, 0, 100, 100] } }
|
42
|
+
let(:path_2) { image_path "a1" }
|
43
|
+
it { expect(subject.difference_image).to eq Imatcher::Image.from_file(image_path("include")) }
|
44
|
+
it { expect(subject.score).to eq 0 }
|
45
|
+
|
46
|
+
context "calculates score correctly" do
|
47
|
+
let(:path_2) { image_path "darker" }
|
48
|
+
it { expect(subject.score).to eq 1 }
|
49
|
+
end
|
50
|
+
end
|
26
51
|
end
|
data/spec/matcher_spec.rb
CHANGED
@@ -28,18 +28,50 @@ describe Imatcher::Matcher do
|
|
28
28
|
|
29
29
|
it { expect(subject.mode).to be_a Imatcher::Modes::Delta }
|
30
30
|
end
|
31
|
+
|
32
|
+
context "with undefined mode" do
|
33
|
+
let(:options) { { mode: :gamma } }
|
34
|
+
|
35
|
+
it { expect { subject }.to raise_error(ArgumentError) }
|
36
|
+
end
|
31
37
|
end
|
32
38
|
|
33
39
|
describe "compare" do
|
34
40
|
let(:path_1) { image_path "very_small" }
|
35
41
|
let(:path_2) { image_path "very_small" }
|
36
|
-
|
42
|
+
let(:options) { {} }
|
43
|
+
subject { Imatcher.compare(path_1, path_2, options) }
|
37
44
|
|
38
45
|
it { expect(subject).to be_a Imatcher::Result }
|
39
46
|
|
40
47
|
context "when sizes mismatch" do
|
41
48
|
let(:path_2) { image_path "small" }
|
42
|
-
it { expect { subject }.to raise_error
|
49
|
+
it { expect { subject }.to raise_error Imatcher::SizesMismatchError }
|
50
|
+
end
|
51
|
+
|
52
|
+
context "with negative exclude rect bounds" do
|
53
|
+
let(:options) { { exclude_rect: [-1, -1, -1, -1] } }
|
54
|
+
it { expect { subject }.to raise_error ArgumentError }
|
55
|
+
end
|
56
|
+
|
57
|
+
context "with big exclude rect bounds" do
|
58
|
+
let(:options) { { exclude_rect: [100, 100, 100, 100] } }
|
59
|
+
it { expect { subject }.to raise_error ArgumentError }
|
60
|
+
end
|
61
|
+
|
62
|
+
context "with negative include rect bounds" do
|
63
|
+
let(:options) { { include_rect: [-1, -1, -1, -1] } }
|
64
|
+
it { expect { subject }.to raise_error ArgumentError }
|
65
|
+
end
|
66
|
+
|
67
|
+
context "with big include rect bounds" do
|
68
|
+
let(:options) { { include_rect: [100, 100, 100, 100] } }
|
69
|
+
it { expect { subject }.to raise_error ArgumentError }
|
70
|
+
end
|
71
|
+
|
72
|
+
context "with wrong include and exclude rects combination" do
|
73
|
+
let(:options) { { include_rect: [1, 1, 2, 2], exclude_rect: [0, 0, 1, 1] } }
|
74
|
+
it { expect { subject }.to raise_error ArgumentError }
|
43
75
|
end
|
44
76
|
end
|
45
77
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Imatcher::Rectangle do
|
4
|
+
let(:rect) { described_class.new(0, 0, 9, 9) }
|
5
|
+
|
6
|
+
describe 'area' do
|
7
|
+
subject { rect.area }
|
8
|
+
|
9
|
+
it { expect(subject).to eq 100 }
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "contains?" do
|
13
|
+
let(:rect2) { described_class.new(1, 1, 8, 8) }
|
14
|
+
subject { rect.contains?(rect2) }
|
15
|
+
|
16
|
+
it { expect(subject).to be_truthy }
|
17
|
+
|
18
|
+
context "when does not contain" do
|
19
|
+
let(:rect2) { described_class.new(2, 2, 10, 10) }
|
20
|
+
|
21
|
+
it { expect(subject).to be_falsey }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "contains_point?" do
|
26
|
+
let(:point) { [5, 5] }
|
27
|
+
subject { rect.contains_point?(*point) }
|
28
|
+
|
29
|
+
it { expect(subject).to be_truthy }
|
30
|
+
|
31
|
+
context "when does not contain" do
|
32
|
+
let(:point) { [10, 10] }
|
33
|
+
|
34
|
+
it { expect(subject).to be_falsey }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: imatcher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- palkan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-03-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oily_png
|
@@ -94,21 +94,27 @@ files:
|
|
94
94
|
- lib/imatcher/modes/delta.rb
|
95
95
|
- lib/imatcher/modes/grayscale.rb
|
96
96
|
- lib/imatcher/modes/rgb.rb
|
97
|
+
- lib/imatcher/rectangle.rb
|
97
98
|
- lib/imatcher/result.rb
|
98
99
|
- lib/imatcher/version.rb
|
99
100
|
- spec/fixtures/a.png
|
101
|
+
- spec/fixtures/a1.png
|
100
102
|
- spec/fixtures/b.png
|
101
103
|
- spec/fixtures/darker.png
|
102
104
|
- spec/fixtures/delta_diff.png
|
105
|
+
- spec/fixtures/exclude.png
|
103
106
|
- spec/fixtures/grayscale_diff.png
|
107
|
+
- spec/fixtures/include.png
|
104
108
|
- spec/fixtures/rgb_diff.png
|
105
109
|
- spec/fixtures/small.png
|
106
110
|
- spec/fixtures/very_small.png
|
111
|
+
- spec/image_spec.rb
|
107
112
|
- spec/imatcher_spec.rb
|
108
113
|
- spec/integrations/delta_spec.rb
|
109
114
|
- spec/integrations/grayscale_spec.rb
|
110
115
|
- spec/integrations/rgb_spec.rb
|
111
116
|
- spec/matcher_spec.rb
|
117
|
+
- spec/rectangle_spec.rb
|
112
118
|
- spec/spec_helper.rb
|
113
119
|
homepage: http://github.com/teachbase/imatcher
|
114
120
|
licenses:
|
@@ -135,3 +141,4 @@ signing_key:
|
|
135
141
|
specification_version: 4
|
136
142
|
summary: Image comparison lib
|
137
143
|
test_files: []
|
144
|
+
has_rdoc:
|