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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7b023655f4f68f37b2d4a48c89dc11b367b2fa77
4
- data.tar.gz: 542127918bee9dbc4a0b849f852f36eb138d6a8f
3
+ metadata.gz: c95e7ff691b3bd8a4c0225764cd6352e0b013cc3
4
+ data.tar.gz: 2ad7efe2e068f910d686d18b3f84ff35832354d2
5
5
  SHA512:
6
- metadata.gz: 1bb1b4da86c334eaf59c7a33c5e95b85f1304b6af8859e5f6928af4c0dabdf4388cf8b8c1b7a2e79b1ab5e8ea7a2fb2661f74b1c4fd8e459ee44619ba1057207
7
- data.tar.gz: 7ba78053279d58196c07a29700caf8f1fcdc3366780a997e53f7100bb36438a71e0e39995a2cb279dbd0ce1ef44abe151aed6ecc9f441bd40007c1dbeaf22381
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.
@@ -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"
@@ -11,5 +11,17 @@ module Imatcher
11
11
  def brightness(a)
12
12
  0.3 * r(a) + 0.59 * g(a) + 0.11 * b(a)
13
13
  end
14
+
15
+ def red
16
+ rgb(255, 0, 0)
17
+ end
18
+
19
+ def green
20
+ rgb(0, 255, 0)
21
+ end
22
+
23
+ def blue
24
+ rgb(0, 0, 255)
25
+ end
14
26
  end
15
27
  end
@@ -13,11 +13,13 @@ module Imatcher
13
13
  end
14
14
  end
15
15
 
16
- def compare_each_pixel(image)
17
- height.times do |y|
18
- next if image.row(y) == row(y)
19
- row(y).each_with_index do |pixel, x|
20
- yield(pixel, image[x, y], x, y)
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
@@ -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
- raise SizesMismatchError,
25
- "Size mismatch: first image size: " \
26
- "#{a.width}x#{a.height}, " \
27
- "second image size: " \
28
- "#{b.width}x#{b.height}" unless a.sizes_match?(b)
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
@@ -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
- @bounds = [0, 0, result.image.width - 1, result.image.height - 1]
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).render_bounds(*bounds)
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
- @result.diff.length.to_f / @result.image.pixels.length
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[0] = [x, bounds[0]].max
44
- bounds[1] = [y, bounds[1]].max
45
- bounds[2] = [x, bounds[2]].min
46
- bounds[3] = [y, bounds[3]].min
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
@@ -47,7 +47,7 @@ module Imatcher
47
47
  end
48
48
 
49
49
  def score
50
- @delta_score / result.image.pixels.length
50
+ @delta_score / area
51
51
  end
52
52
  end
53
53
  end
@@ -18,7 +18,7 @@ module Imatcher
18
18
  Image.new(bg.width, bg.height, BLACK)
19
19
  end
20
20
 
21
- def create_diff_image(bg, diff_image)
21
+ def create_diff_image(_bg, diff_image)
22
22
  diff_image
23
23
  end
24
24
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Imatcher # :nodoc:
2
- VERSION = "0.1.3".freeze
2
+ VERSION = "0.1.4".freeze
3
3
  end
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -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 eq false }
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 eq true }
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
@@ -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
- subject { Imatcher::Matcher.new.compare(path_1, path_2) }
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(Imatcher::SizesMismatchError) }
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.3
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-02-18 00:00:00.000000000 Z
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: