Skip to content

Commit

Permalink
Merge pull request #6 from instantink/multiple_areas
Browse files Browse the repository at this point in the history
Multiple areas
  • Loading branch information
cristianofmc authored Mar 18, 2024
2 parents 939b470 + c658fca commit 3a37184
Show file tree
Hide file tree
Showing 25 changed files with 419 additions and 426 deletions.
36 changes: 8 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<img alt="Gem total downloads" src="https://shields.io/gem/dt/image_compare" />

Compare PNG images in pure Ruby (uses [ChunkyPNG](https://github.com/wvanbergen/chunky_png)) using different algorithms.
This is an utility library for image regression testing.
This is a utility library for image regression testing.

## Installation

Expand Down Expand Up @@ -53,28 +53,6 @@ Resulting diff contains version of the first image with different pixels highlig

<img alt="color_diff" src="spec/fixtures/color_diff.png" />

### RGB mode (a.png X b.png)

Compare pixels by values, resulting score is a ratio of unequal pixels.
Resulting diff represents per-channel difference.

<img alt="rgb_diff.png" src="spec/fixtures/rgb_diff.png" />

### Grayscale mode (a.png X a1.png)

Compare pixels as grayscale (by brightness and alpha), resulting score is a ratio of unequal pixels (with respect to provided tolerance).

Resulting diff contains grayscale version of the first image with different pixels highlighted in red and red bounding box.

<img alt="grayscale_diff.png" src="spec/fixtures/grayscale_diff.png" />

### Delta (a.png X a1.png)

Compare pixels using [Delta E](https://en.wikipedia.org/wiki/Color_difference) distance.
Resulting diff contains grayscale version of the first image with different pixels highlighted in red (with respect to diff score).

<img alt="delta_diff.png" src="spec/fixtures/delta_diff.png" />

## Usage

```ruby
Expand Down Expand Up @@ -122,16 +100,16 @@ res.score #=> 0.0

## Excluding rectangle (a.png X a1.png)

<img alt="a1.png" src="spec/fixtures/rgb_exclude_rect.png" />
<img alt="a1.png" src="spec/fixtures/multiple_exclude_rects.png" />

You can exclude rectangle from comparing by passing `:exclude_rect` to `compare`.
You can exclude rectangle from comparing by passing `:exclude_rects` to `compare`.
E.g., if `path_1` and `path_2` contain images above
```ruby
ImageCompare.compare("path/image1.png", "path/image2.png", mode: :rgb, exclude_rect: [200, 150, 275, 200]).match? # => true
ImageCompare.compare("path/image1.png", "path/image2.png", mode: :rgb, exclude_rects: [[170, 221, 188, 246], [289, 221, 307, 246]]).match? # => true

# or

cmp = ImageCompare::Matcher.new mode: :rgb, exclude_rect: [200, 150, 275, 200]
cmp = ImageCompare::Matcher.new mode: :rgb, exclude_rect: [[170, 221, 188, 246], [289, 221, 307, 246]]
res = cmp.compare("path/image1.png", "path/image2.png")
res #=> ImageCompare::Result
res.match? #=> true
Expand All @@ -141,7 +119,9 @@ res.score #=> 0.0
res.difference_image #=> ImageCompare::Image
res.difference_image.save("path/diff.png")
```
`[200, 150, 275, 200]` is array of two vertices of rectangle -- (200, 150) is left-top vertex and (275, 200) is right-bottom.
`[[170, 221, 188, 246],[289, 221, 307, 246]]` is a set of multiple areas, containing area not to be considered in comparison, each area is an array of two vertices of rectangle -- (170, 121) is left-top vertex and (288, 246) is right-bottom.



### Cucumber + Capybara example
`support/env.rb`:
Expand Down
4 changes: 2 additions & 2 deletions lib/image_compare.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SizesMismatchError < StandardError
require "image_compare/color_methods"
require "image_compare/matcher"

def self.compare(path_a, path_b, **options)
Matcher.new(**options).compare(path_a, path_b)
def self.compare(path_a, path_b, **)
Matcher.new(**).compare(path_a, path_b)
end
end
20 changes: 13 additions & 7 deletions lib/image_compare/image.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,20 @@ def each_pixel
end
end

def compare_each_pixel(image, area: nil)
def find_different_pixel(image, area: nil, order: :top_to_bottom)
area = bounding_rect if area.nil?
(area.top..area.bot).each do |y|
current_row = row(y) || []
range = (area.left..area.right)
next if image.row(y).slice(range) == current_row.slice(range)
(area.left..area.right).each do |x|
yield(self[x, y], image[x, y], x, y)

rows = (area.top..area.bot).to_a
rows.reverse! if order == :bottom_to_top

cols = (area.left..area.right).to_a

rows.each do |y|
cols.each do |x|
pixel_self = self[x, y]
pixel_image = image[x, y]
next if pixel_self == pixel_image
yield(pixel_self, pixel_image, x, y)
end
end
end
Expand Down
5 changes: 3 additions & 2 deletions lib/image_compare/matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ def compare(a, b)
unless image_area.contains?(mode.include_rect)
raise ArgumentError, "Bounds must be in image"
end
unless mode.exclude_rect.nil?
unless mode.include_rect.contains?(mode.exclude_rect)

mode.exclude_rects&.each do |exclude_rect|
unless mode.include_rect.contains?(exclude_rect)
raise ArgumentError, "Included area must contain excluded"
end
end
Expand Down
3 changes: 0 additions & 3 deletions lib/image_compare/modes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,5 @@
module ImageCompare
module Modes
require "image_compare/modes/color"
require "image_compare/modes/delta"
require "image_compare/modes/grayscale"
require "image_compare/modes/rgb"
end
end
82 changes: 59 additions & 23 deletions lib/image_compare/modes/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,48 @@ class Base
require "image_compare/rectangle"
include ColorMethods

attr_reader :result, :threshold, :lower_threshold, :bounds, :exclude_rect, :include_rect
attr_reader :result, :threshold, :lower_threshold, :different_areas, :exclude_rects, :include_rect

def initialize(threshold: 0.0, lower_threshold: 0.0, exclude_rect: nil, include_rect: nil)
def initialize(threshold: 0.0, lower_threshold: 0.0, exclude_rects: [], include_rect: nil)
@include_rect = Rectangle.new(*include_rect) unless include_rect.nil?
@exclude_rect = Rectangle.new(*exclude_rect) unless exclude_rect.nil?
@exclude_rects = exclude_rects.empty? ? Set.new : Set.new(exclude_rects.map { |rect| Rectangle.new(*rect) })
@threshold = threshold
@lower_threshold = lower_threshold
@result = Result.new(self, threshold: threshold, lower_threshold: lower_threshold)
@different_areas = Set.new
end

def create_sections(session = [])
@sections.append(session)
end

def compare(a, b)
result.image = a
@include_rect ||= a.bounding_rect
@bounds = Rectangle.new(*include_rect.bounds)
@comparison_area = @include_rect

b.compare_each_pixel(a, area: include_rect) do |b_pixel, a_pixel, x, y|
next if !exclude_rect.nil? && exclude_rect.contains_point?(x, y)
excluded_points = Hash.new { |h, k| h[k] = Hash.new(false) }
@exclude_rects.each do |rect|
(rect.top..rect.bot).each do |y|
(rect.left..rect.right).each do |x|
if @comparison_area.contains_point?(x, y)
excluded_points[x][y] = true
end
end
end
end

b.find_different_pixel(a, area: @comparison_area) do |b_pixel, a_pixel, x, y|
next if excluded_points[x][y]
next if pixels_equal?(b_pixel, a_pixel)

update_result(b_pixel, a_pixel, x, y)
end

result.score = score
result
end

def diff(bg, diff)
diff_image = background(bg).highlight_rectangle(exclude_rect, :blue)
diff.each do |pixels_pair|
pixels_diff(diff_image, *pixels_pair)
end
create_diff_image(bg, diff_image)
.highlight_rectangle(bounds)
.highlight_rectangle(include_rect, :green)
end

def score
result.diff.length.to_f / area
end
Expand All @@ -49,17 +56,46 @@ def update_result(*_args, x, y)
update_bounds(x, y)
end

def connected_area_index(x, y)
@different_areas.each_with_index do |area, index|
if area.close_to_the_area?(x, y)
return index
end
end
nil
end

def areas_connected?(origin_area)
connected_areas, disconnected_areas = @different_areas.partition { |area| origin_area.rect_close_to_the_area?(area) }
merged_area = connected_areas.reduce(origin_area, :merge)
@different_areas = disconnected_areas.to_set
@different_areas.add(merged_area)
end

def create_area(x, y)
@different_areas.add(Rectangle.new(x, y, x, y))
end

def update_bounds(x, y)
bounds.left = [x, bounds.left].max
bounds.top = [y, bounds.top].max
bounds.right = [x, bounds.right].min
bounds.bot = [y, bounds.bot].min
current_area = @different_areas.find { |area| area.close_to_the_area?(x, y) }
if current_area.nil?
create_area(x, y)
current_area = @different_areas.to_a.last
else
current_area.left = [x, current_area.left].min
current_area.top = [y, current_area.top].min
current_area.right = [x, current_area.right].max
current_area.bot = [y, current_area.bot].max
end

areas_connected?(current_area)
current_area
end

def area
area = include_rect.area
return area if exclude_rect.nil?
area - exclude_rect.area
total_area = @include_rect.area
total_exclude_area = @exclude_rects.sum(&:area) || 0
total_area - total_exclude_area
end
end
end
Expand Down
45 changes: 26 additions & 19 deletions lib/image_compare/modes/color.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,43 @@ def initialize(**options)
end

def diff(bg, _diff)
diff_image = bg.highlight_rectangle(exclude_rect, :blue)
diff_image = bg
@exclude_rects.each do |rect|
diff_image = diff_image.highlight_rectangle(rect, :blue)
end

unless result.match? || area_in_exclude_rect?
return diff_image.highlight_rectangle(bounds, :red)
@different_areas.each do |area|
unless result.match? || area_in_exclude_rect?(area)
diff_image = diff_image.highlight_rectangle(area, :red)
end
end

diff_image
end

def area_in_exclude_rect?
return false if exclude_rect.nil?
def area_in_exclude_rect?(bound)
return false if exclude_rects.nil?

diff_area = {
left: bounds.bounds[0],
top: bounds.bounds[1],
right: bounds.bounds[2],
bot: bounds.bounds[3]
left: bound.bounds[0],
top: bound.bounds[1],
right: bound.bounds[2],
bot: bound.bounds[3]
}

exclude_area = {
left: exclude_rect.bounds[0],
top: exclude_rect.bounds[1],
right: exclude_rect.bounds[2],
bot: exclude_rect.bounds[3]
}
exclude_rects.any? do |exclude_rect|
exclude_area = {
left: exclude_rect.bounds[0],
top: exclude_rect.bounds[1],
right: exclude_rect.bounds[2],
bot: exclude_rect.bounds[3]
}

diff_area[:left] <= exclude_area[:left] &&
diff_area[:top] <= exclude_area[:top] &&
diff_area[:right] >= exclude_area[:right] &&
diff_area[:bot] >= exclude_area[:bot]
diff_area[:left] <= exclude_area[:left] &&
diff_area[:top] <= exclude_area[:top] &&
diff_area[:right] >= exclude_area[:right] &&
diff_area[:bot] >= exclude_area[:bot]
end
end

def pixels_equal?(a, b)
Expand Down
55 changes: 0 additions & 55 deletions lib/image_compare/modes/delta.rb

This file was deleted.

Loading

0 comments on commit 3a37184

Please sign in to comment.