image_compare 1.0.0.pre.dev
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 +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +119 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/lib/image_compare/color_methods.rb +43 -0
- data/lib/image_compare/image.rb +63 -0
- data/lib/image_compare/matcher.rb +55 -0
- data/lib/image_compare/modes/base.rb +66 -0
- data/lib/image_compare/modes/color.rb +69 -0
- data/lib/image_compare/modes/delta.rb +55 -0
- data/lib/image_compare/modes/grayscale.rb +46 -0
- data/lib/image_compare/modes/rgb.rb +34 -0
- data/lib/image_compare/modes.rb +10 -0
- data/lib/image_compare/rectangle.rb +33 -0
- data/lib/image_compare/result.rb +24 -0
- data/lib/image_compare/version.rb +5 -0
- data/lib/image_compare.rb +15 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a67b91873ee2ca3ec939803ef3ff7c7e116ce75e48d4643340aae67f48c59225
|
4
|
+
data.tar.gz: 2795771468140989921e1ec9afffa2f9616f5363df7106e050ebcb36871659b0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 39a0d7d0bc330345bc4a9624bbbd721f7206db5d5c0d5900bfaa78bb0eec90dd9c03598d683c7f165a78549a8d311edaa7e206b5773abe96672479b25a322851
|
7
|
+
data.tar.gz: 3fc93372828b8354867e63ac3e80cd2998fdd025ef9e3601a239d952f7ec0bfbdff3cd148bd03c4feb1a2c9cddf0be73c39da65ad55276639f3ac392886af1fa
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 instantink
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
[](https://rubygems.org/gems/image_compare)
|
2
|
+
[](https://github.com/instantink/image_compare/actions)
|
3
|
+
|
4
|
+
# ImageCompare
|
5
|
+
|
6
|
+
Compare PNG images in pure Ruby (uses [ChunkyPNG](https://github.com/wvanbergen/chunky_png)) using different algorithms.
|
7
|
+
This is an utility library for image regression testing.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'image_compare'
|
15
|
+
```
|
16
|
+
|
17
|
+
Or adding to your project:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
# my-cool-gem.gemspec
|
21
|
+
Gem::Specification.new do |spec|
|
22
|
+
# ...
|
23
|
+
spec.add_dependency 'image_compare'
|
24
|
+
# ...
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
Additionally, you may want to install [oily_png](https://github.com/wvanbergen/oily_png) to improve performance when using MRI. Just install it globally or add to your Gemfile.
|
29
|
+
|
30
|
+
## Modes
|
31
|
+
|
32
|
+
ImageCompare supports different ways (_modes_) of comparing images.
|
33
|
+
|
34
|
+
Source images used in examples:
|
35
|
+
|
36
|
+
<img src='https://raw.githubusercontent.com/instantink/image_compare/master/spec/fixtures/a.png' width='300' />
|
37
|
+
<img src='https://raw.githubusercontent.com/instantink/image_compare/master/spec/fixtures/b.png' width='300' />
|
38
|
+
|
39
|
+
### Base (RGB) mode
|
40
|
+
|
41
|
+
Compare pixels by values, resulting score is a ratio of unequal pixels.
|
42
|
+
Resulting diff represents per-channel difference.
|
43
|
+
|
44
|
+
<img src='https://raw.githubusercontent.com/instantink/image_compare/master/spec/fixtures/rgb_diff.png' width='300' />
|
45
|
+
|
46
|
+
### Grayscale mode
|
47
|
+
|
48
|
+
Compare pixels as grayscale (by brightness and alpha), resulting score is a ratio of unequal pixels (with respect to provided tolerance).
|
49
|
+
|
50
|
+
Resulting diff contains grayscale version of the first image with different pixels highlighted in red and red bounding box.
|
51
|
+
|
52
|
+
<img src='https://raw.githubusercontent.com/instantink/image_compare/master/spec/fixtures/grayscale_diff.png' width='300' />
|
53
|
+
|
54
|
+
### Delta
|
55
|
+
|
56
|
+
Compare pixels using [Delta E](https://en.wikipedia.org/wiki/Color_difference) distance.
|
57
|
+
Resulting diff contains grayscale version of the first image with different pixels highlighted in red (with respect to diff score).
|
58
|
+
|
59
|
+
<img src='https://raw.githubusercontent.com/instantink/image_compare/master/spec/fixtures/delta_diff.png' width='300' />
|
60
|
+
|
61
|
+
## Usage
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
# create new matcher with default threshold equals to 0
|
65
|
+
# and base (RGB) mode
|
66
|
+
cmp = ImageCompare::Matcher.new
|
67
|
+
cmp.mode #=> ImageCompare::Modes::RGB
|
68
|
+
|
69
|
+
# create matcher with specific threshold
|
70
|
+
cmp = ImageCompare::Matcher.new threshold: 0.05
|
71
|
+
cmp.threshold #=> 0.05
|
72
|
+
|
73
|
+
# or with a lower threshold (in case you want to test that there is some difference)
|
74
|
+
cmp = ImageCompare::Matcher.new lower_threshold: 0.01
|
75
|
+
cmp.lower_threshold #=> 0.01
|
76
|
+
|
77
|
+
# create zero-tolerance grayscale matcher
|
78
|
+
cmp = ImageCompare::Matcher.new mode: :grayscale, tolerance: 0
|
79
|
+
cmp.mode #=> ImageCompare::Modes::Grayscale
|
80
|
+
|
81
|
+
res = cmp.compare(path_1, path_2)
|
82
|
+
res #=> ImageCompare::Result
|
83
|
+
res.match? #=> true
|
84
|
+
res.score #=> 0.0
|
85
|
+
|
86
|
+
# Return diff image object
|
87
|
+
res.difference_image #=> ImageCompare::Image
|
88
|
+
res.difference_image.save(new_path)
|
89
|
+
|
90
|
+
# without explicit matcher
|
91
|
+
res = ImageCompare.compare(path_1, path_2, options)
|
92
|
+
|
93
|
+
# equals to
|
94
|
+
res = ImageCompare::Matcher.new(options).compare(path_1, path_2)
|
95
|
+
```
|
96
|
+
|
97
|
+
## Excluding rectangle
|
98
|
+
|
99
|
+
<img src='https://raw.githubusercontent.com/instantink/image_compare/master/spec/fixtures/a.png' width='300' />
|
100
|
+
<img src='https://raw.githubusercontent.com/instantink/image_compare/master/spec/fixtures/a1.png' width='300' />
|
101
|
+
|
102
|
+
You can exclude rectangle from comparing by passing `:exclude_rect` to `compare`.
|
103
|
+
E.g., if `path_1` and `path_2` contain images above
|
104
|
+
```ruby
|
105
|
+
ImageCompare.compare(path_1, path_2, exclude_rect: [200, 150, 275, 200]).match? # => true
|
106
|
+
```
|
107
|
+
`[200, 150, 275, 200]` is array of two vertices of rectangle -- (200, 150) is left-top vertex and (275, 200) is right-bottom.
|
108
|
+
|
109
|
+
## Including rectangle
|
110
|
+
|
111
|
+
You can set bounds of comparing by passing `:include_rect` to `compare` with array similar to previous example
|
112
|
+
|
113
|
+
## Contributing
|
114
|
+
|
115
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/instantink/image_compare.
|
116
|
+
|
117
|
+
## License
|
118
|
+
|
119
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/bin/console
ADDED
data/bin/setup
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'chunky_png'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'oily_png' unless RUBY_PLATFORM == 'java'
|
7
|
+
rescue LoadError => _e
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
module ImageCompare
|
12
|
+
module ColorMethods
|
13
|
+
include ChunkyPNG::Color
|
14
|
+
|
15
|
+
def brightness(a)
|
16
|
+
0.3 * r(a) + 0.59 * g(a) + 0.11 * b(a)
|
17
|
+
end
|
18
|
+
|
19
|
+
def red
|
20
|
+
rgb(255, 0, 0)
|
21
|
+
end
|
22
|
+
|
23
|
+
def green
|
24
|
+
rgb(0, 255, 0)
|
25
|
+
end
|
26
|
+
|
27
|
+
def blue
|
28
|
+
rgb(0, 0, 255)
|
29
|
+
end
|
30
|
+
|
31
|
+
def yellow
|
32
|
+
rgb(255, 255, 51)
|
33
|
+
end
|
34
|
+
|
35
|
+
def orange
|
36
|
+
rgb(255, 128, 0)
|
37
|
+
end
|
38
|
+
|
39
|
+
def transparent
|
40
|
+
rgba(255, 255, 255, 255)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'image_compare/color_methods'
|
4
|
+
|
5
|
+
module ImageCompare
|
6
|
+
class Image < ChunkyPNG::Image
|
7
|
+
include ColorMethods
|
8
|
+
|
9
|
+
def each_pixel
|
10
|
+
height.times do |y|
|
11
|
+
current_row = row(y) || []
|
12
|
+
current_row.each_with_index do |pixel, x|
|
13
|
+
yield(pixel, x, y)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def compare_each_pixel(image, area: nil)
|
19
|
+
area = bounding_rect if area.nil?
|
20
|
+
(area.top..area.bot).each do |y|
|
21
|
+
current_row = row(y) || []
|
22
|
+
range = (area.left..area.right)
|
23
|
+
next if image.row(y).slice(range) == current_row.slice(range)
|
24
|
+
(area.left..area.right).each do |x|
|
25
|
+
yield(self[x, y], image[x, y], x, y)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_grayscale
|
31
|
+
each_pixel do |pixel, x, y|
|
32
|
+
self[x, y] = grayscale(brightness(pixel).round)
|
33
|
+
end
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def with_alpha(value)
|
38
|
+
each_pixel do |pixel, x, y|
|
39
|
+
self[x, y] = rgba(r(pixel), g(pixel), b(pixel), value)
|
40
|
+
end
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def sizes_match?(image)
|
45
|
+
[width, height] == [image.width, image.height]
|
46
|
+
end
|
47
|
+
|
48
|
+
def inspect
|
49
|
+
"Image:#{object_id}<#{width}x#{height}>"
|
50
|
+
end
|
51
|
+
|
52
|
+
def highlight_rectangle(rect, color = :red)
|
53
|
+
raise ArgumentError, "Undefined color: #{color}" unless respond_to?(color)
|
54
|
+
return self if rect.nil?
|
55
|
+
rect(*rect.bounds, send(color))
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
def bounding_rect
|
60
|
+
Rectangle.new(0, 0, width - 1, height - 1)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageCompare
|
4
|
+
class Matcher
|
5
|
+
require 'image_compare/image'
|
6
|
+
require 'image_compare/result'
|
7
|
+
require 'image_compare/modes'
|
8
|
+
|
9
|
+
MODES = {
|
10
|
+
rgb: 'RGB',
|
11
|
+
delta: 'Delta',
|
12
|
+
grayscale: 'Grayscale',
|
13
|
+
color: 'Color'
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
attr_reader :mode
|
17
|
+
|
18
|
+
def initialize(**options)
|
19
|
+
mode_type = options.delete(:mode) || :rgb
|
20
|
+
raise ArgumentError, "Undefined mode: #{mode_type}" unless MODES.key?(mode_type)
|
21
|
+
@mode = Modes.const_get(MODES[mode_type]).new(**options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def compare(a, b)
|
25
|
+
a = Image.from_file(a) unless a.is_a?(Image)
|
26
|
+
b = Image.from_file(b) unless b.is_a?(Image)
|
27
|
+
|
28
|
+
unless a.sizes_match?(b)
|
29
|
+
raise SizesMismatchError,
|
30
|
+
"Size mismatch: first image size: #{a.width}x#{a.height}, second image size: #{b.width}x#{b.height}"
|
31
|
+
end
|
32
|
+
|
33
|
+
image_area = Rectangle.new(0, 0, a.width - 1, a.height - 1)
|
34
|
+
|
35
|
+
unless mode.exclude_rect.nil?
|
36
|
+
unless image_area.contains?(mode.exclude_rect)
|
37
|
+
raise ArgumentError, 'Bounds must be in image'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
unless mode.include_rect.nil?
|
42
|
+
unless image_area.contains?(mode.include_rect)
|
43
|
+
raise ArgumentError, 'Bounds must be in image'
|
44
|
+
end
|
45
|
+
unless mode.exclude_rect.nil?
|
46
|
+
unless mode.include_rect.contains?(mode.exclude_rect)
|
47
|
+
raise ArgumentError, 'Included area must contain excluded'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
mode.compare(a, b)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageCompare
|
4
|
+
module Modes
|
5
|
+
class Base
|
6
|
+
require 'image_compare/rectangle'
|
7
|
+
include ColorMethods
|
8
|
+
|
9
|
+
attr_reader :result, :threshold, :lower_threshold, :bounds, :exclude_rect, :include_rect
|
10
|
+
|
11
|
+
def initialize(threshold: 0.0, lower_threshold: 0.0, exclude_rect: nil, include_rect: nil)
|
12
|
+
@include_rect = Rectangle.new(*include_rect) unless include_rect.nil?
|
13
|
+
@exclude_rect = Rectangle.new(*exclude_rect) unless exclude_rect.nil?
|
14
|
+
@threshold = threshold
|
15
|
+
@lower_threshold = lower_threshold
|
16
|
+
@result = Result.new(self, threshold: threshold, lower_threshold: lower_threshold)
|
17
|
+
end
|
18
|
+
|
19
|
+
def compare(a, b)
|
20
|
+
result.image = a
|
21
|
+
@include_rect ||= a.bounding_rect
|
22
|
+
@bounds = Rectangle.new(*include_rect.bounds)
|
23
|
+
|
24
|
+
b.compare_each_pixel(a, area: include_rect) do |b_pixel, a_pixel, x, y|
|
25
|
+
next if pixels_equal?(b_pixel, a_pixel)
|
26
|
+
next if !exclude_rect.nil? && exclude_rect.contains_point?(x, y)
|
27
|
+
update_result(b_pixel, a_pixel, x, y)
|
28
|
+
end
|
29
|
+
|
30
|
+
result.score = score
|
31
|
+
result
|
32
|
+
end
|
33
|
+
|
34
|
+
def diff(bg, diff)
|
35
|
+
diff_image = background(bg).highlight_rectangle(exclude_rect, :blue)
|
36
|
+
diff.each do |pixels_pair|
|
37
|
+
pixels_diff(diff_image, *pixels_pair)
|
38
|
+
end
|
39
|
+
create_diff_image(bg, diff_image)
|
40
|
+
.highlight_rectangle(bounds)
|
41
|
+
.highlight_rectangle(include_rect, :green)
|
42
|
+
end
|
43
|
+
|
44
|
+
def score
|
45
|
+
result.diff.length.to_f / area
|
46
|
+
end
|
47
|
+
|
48
|
+
def update_result(*_args, x, y)
|
49
|
+
update_bounds(x, y)
|
50
|
+
end
|
51
|
+
|
52
|
+
def update_bounds(x, y)
|
53
|
+
bounds.left = [x, bounds.left].max
|
54
|
+
bounds.top = [y, bounds.top].max
|
55
|
+
bounds.right = [x, bounds.right].min
|
56
|
+
bounds.bot = [y, bounds.bot].min
|
57
|
+
end
|
58
|
+
|
59
|
+
def area
|
60
|
+
area = include_rect.area
|
61
|
+
return area if exclude_rect.nil?
|
62
|
+
area - exclude_rect.area
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageCompare
|
4
|
+
module Modes
|
5
|
+
require 'image_compare/modes/base'
|
6
|
+
|
7
|
+
class Color < Base
|
8
|
+
include ColorMethods
|
9
|
+
|
10
|
+
DEFAULT_TOLERANCE = 16
|
11
|
+
|
12
|
+
attr_reader :tolerance
|
13
|
+
|
14
|
+
def initialize(**options)
|
15
|
+
@tolerance = options.delete(:tolerance) || DEFAULT_TOLERANCE
|
16
|
+
super(**options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def diff(bg, diff)
|
20
|
+
diff_image = bg.highlight_rectangle(exclude_rect, :blue)
|
21
|
+
|
22
|
+
if area_in_exclude_rect?
|
23
|
+
diff_image
|
24
|
+
else
|
25
|
+
diff_image.highlight_rectangle(bounds)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def area_in_exclude_rect?
|
30
|
+
return false if exclude_rect.nil?
|
31
|
+
|
32
|
+
diff_area = {
|
33
|
+
left: bounds.bounds[0],
|
34
|
+
top: bounds.bounds[1],
|
35
|
+
right: bounds.bounds[2],
|
36
|
+
bot: bounds.bounds[3]
|
37
|
+
}
|
38
|
+
|
39
|
+
exclude_area = {
|
40
|
+
left: exclude_rect.bounds[0],
|
41
|
+
top: exclude_rect.bounds[1],
|
42
|
+
right: exclude_rect.bounds[2],
|
43
|
+
bot: exclude_rect.bounds[3]
|
44
|
+
}
|
45
|
+
|
46
|
+
diff_area[:left] <= exclude_area[:left] &&
|
47
|
+
diff_area[:top] <= exclude_area[:top] &&
|
48
|
+
diff_area[:right] >= exclude_area[:right] &&
|
49
|
+
diff_area[:bot] >= exclude_area[:bot]
|
50
|
+
end
|
51
|
+
|
52
|
+
def pixels_equal?(a, b)
|
53
|
+
alpha = color_similar?(a(a), a(b))
|
54
|
+
brightness = color_similar?(brightness(a), brightness(b))
|
55
|
+
brightness && alpha
|
56
|
+
end
|
57
|
+
|
58
|
+
def update_result(a, b, x, y)
|
59
|
+
super
|
60
|
+
@result.diff << [a, b, x, y]
|
61
|
+
end
|
62
|
+
|
63
|
+
def color_similar?(a, b)
|
64
|
+
d = (a - b).abs
|
65
|
+
d <= tolerance
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageCompare
|
4
|
+
module Modes
|
5
|
+
require 'image_compare/modes/base'
|
6
|
+
|
7
|
+
class Delta < Base
|
8
|
+
attr_reader :tolerance
|
9
|
+
|
10
|
+
def initialize(**options)
|
11
|
+
@tolerance = options.delete(:tolerance) || 0.01
|
12
|
+
@delta_score = 0.0
|
13
|
+
super(**options)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def pixels_equal?(a, b)
|
19
|
+
a == b
|
20
|
+
end
|
21
|
+
|
22
|
+
def update_result(a, b, x, y)
|
23
|
+
d = euclid(a, b) / (MAX * Math.sqrt(3))
|
24
|
+
return if d <= tolerance
|
25
|
+
@result.diff << [a, b, x, y, d]
|
26
|
+
@delta_score += d
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def background(bg)
|
31
|
+
Image.new(bg.width, bg.height, WHITE).with_alpha(0)
|
32
|
+
end
|
33
|
+
|
34
|
+
def euclid(a, b)
|
35
|
+
Math.sqrt(
|
36
|
+
(r(a) - r(b))**2 +
|
37
|
+
(g(a) - g(b))**2 +
|
38
|
+
(b(a) - b(b))**2
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_diff_image(bg, diff_image)
|
43
|
+
bg.to_grayscale.compose!(diff_image, 0, 0)
|
44
|
+
end
|
45
|
+
|
46
|
+
def pixels_diff(d, *_args, x, y, a)
|
47
|
+
d[x, y] = rgba(MAX, 0, 0, (a * MAX).round)
|
48
|
+
end
|
49
|
+
|
50
|
+
def score
|
51
|
+
@delta_score / area
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageCompare
|
4
|
+
module Modes
|
5
|
+
require 'image_compare/modes/base'
|
6
|
+
|
7
|
+
class Grayscale < Base
|
8
|
+
DEFAULT_TOLERANCE = 16
|
9
|
+
|
10
|
+
attr_reader :tolerance
|
11
|
+
|
12
|
+
def initialize(**options)
|
13
|
+
@tolerance = options.delete(:tolerance) || DEFAULT_TOLERANCE
|
14
|
+
super(**options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def pixels_equal?(a, b)
|
18
|
+
alpha = color_similar?(a(a), a(b))
|
19
|
+
brightness = color_similar?(brightness(a), brightness(b))
|
20
|
+
brightness && alpha
|
21
|
+
end
|
22
|
+
|
23
|
+
def update_result(a, b, x, y)
|
24
|
+
super
|
25
|
+
@result.diff << [a, b, x, y]
|
26
|
+
end
|
27
|
+
|
28
|
+
def background(bg)
|
29
|
+
bg.to_grayscale
|
30
|
+
end
|
31
|
+
|
32
|
+
def pixels_diff(d, _a, _b, x, y)
|
33
|
+
d[x, y] = rgb(255, 0, 0)
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_diff_image(_bg, diff_image)
|
37
|
+
diff_image
|
38
|
+
end
|
39
|
+
|
40
|
+
def color_similar?(a, b)
|
41
|
+
d = (a - b).abs
|
42
|
+
d <= tolerance
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageCompare
|
4
|
+
module Modes
|
5
|
+
require 'image_compare/modes/base'
|
6
|
+
|
7
|
+
class RGB < Base
|
8
|
+
def pixels_equal?(a, b)
|
9
|
+
a == b
|
10
|
+
end
|
11
|
+
|
12
|
+
def update_result(a, b, x, y)
|
13
|
+
super
|
14
|
+
@result.diff << [a, b, x, y]
|
15
|
+
end
|
16
|
+
|
17
|
+
def background(bg)
|
18
|
+
Image.new(bg.width, bg.height, BLACK)
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_diff_image(_bg, diff_image)
|
22
|
+
diff_image
|
23
|
+
end
|
24
|
+
|
25
|
+
def pixels_diff(d, a, b, x, y)
|
26
|
+
d[x, y] = rgb(
|
27
|
+
(r(a) - r(b)).abs,
|
28
|
+
(g(a) - g(b)).abs,
|
29
|
+
(b(a) - b(b)).abs
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageCompare
|
4
|
+
class Rectangle
|
5
|
+
attr_accessor :left, :top, :right, :bot
|
6
|
+
|
7
|
+
def initialize(l, t, r, b)
|
8
|
+
@left = l
|
9
|
+
@top = t
|
10
|
+
@right = r
|
11
|
+
@bot = b
|
12
|
+
end
|
13
|
+
|
14
|
+
def area
|
15
|
+
(right - left + 1) * (bot - top + 1)
|
16
|
+
end
|
17
|
+
|
18
|
+
def contains?(rect)
|
19
|
+
(left <= rect.left) &&
|
20
|
+
(right >= rect.right) &&
|
21
|
+
(top <= rect.top) &&
|
22
|
+
(bot >= rect.bot)
|
23
|
+
end
|
24
|
+
|
25
|
+
def bounds
|
26
|
+
[left, top, right, bot]
|
27
|
+
end
|
28
|
+
|
29
|
+
def contains_point?(x, y)
|
30
|
+
x.between?(left, right) && y.between?(top, bot)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageCompare
|
4
|
+
class Result
|
5
|
+
attr_accessor :score, :image
|
6
|
+
attr_reader :diff, :mode, :threshold, :lower_threshold
|
7
|
+
|
8
|
+
def initialize(mode, threshold:, lower_threshold:)
|
9
|
+
@score = 0.0
|
10
|
+
@diff = []
|
11
|
+
@threshold = threshold
|
12
|
+
@lower_threshold = lower_threshold
|
13
|
+
@mode = mode
|
14
|
+
end
|
15
|
+
|
16
|
+
def difference_image
|
17
|
+
@diff_image ||= mode.diff(image, diff)
|
18
|
+
end
|
19
|
+
|
20
|
+
def match?
|
21
|
+
score <= threshold && score >= lower_threshold
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'image_compare/version'
|
4
|
+
|
5
|
+
module ImageCompare
|
6
|
+
class SizesMismatchError < StandardError
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'image_compare/matcher'
|
10
|
+
require 'image_compare/color_methods'
|
11
|
+
|
12
|
+
def self.compare(path_a, path_b, **options)
|
13
|
+
Matcher.new(**options).compare(path_a, path_b)
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: image_compare
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.pre.dev
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- cristianofmc
|
8
|
+
- gsguma
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2023-03-24 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: chunky_png
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rake
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '13.0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '13.0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rspec
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '3.9'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '3.9'
|
56
|
+
description: Image comparison lib built on top of ChunkyPNG
|
57
|
+
email:
|
58
|
+
- cristiano.fmc@hotmail.com
|
59
|
+
- giltonguma@gmail.com
|
60
|
+
executables: []
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- CHANGELOG.md
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- bin/console
|
68
|
+
- bin/setup
|
69
|
+
- lib/image_compare.rb
|
70
|
+
- lib/image_compare/color_methods.rb
|
71
|
+
- lib/image_compare/image.rb
|
72
|
+
- lib/image_compare/matcher.rb
|
73
|
+
- lib/image_compare/modes.rb
|
74
|
+
- lib/image_compare/modes/base.rb
|
75
|
+
- lib/image_compare/modes/color.rb
|
76
|
+
- lib/image_compare/modes/delta.rb
|
77
|
+
- lib/image_compare/modes/grayscale.rb
|
78
|
+
- lib/image_compare/modes/rgb.rb
|
79
|
+
- lib/image_compare/rectangle.rb
|
80
|
+
- lib/image_compare/result.rb
|
81
|
+
- lib/image_compare/version.rb
|
82
|
+
homepage: https://github.com/instantink/image_compare
|
83
|
+
licenses:
|
84
|
+
- MIT
|
85
|
+
metadata:
|
86
|
+
bug_tracker_uri: https://github.com/instantink/image_compare/issues
|
87
|
+
changelog_uri: https://github.com/instantink/image_compare/blob/master/CHANGELOG.md
|
88
|
+
documentation_uri: https://github.com/instantink/image_compare
|
89
|
+
homepage_uri: https://github.com/instantink/image_compare
|
90
|
+
source_code_uri: https://github.com/instantink/image_compare
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: 3.2.0
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: 1.3.1
|
105
|
+
requirements: []
|
106
|
+
rubygems_version: 3.4.1
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Image comparison lib
|
110
|
+
test_files: []
|