ImageCompare

Gem total downloads

Compare PNG images in pure Ruby (uses ChunkyPNG) using different algorithms. This is an utility library for image regression testing.

Installation

Add this line to your application's Gemfile:

gem "image_compare"

Or adding to your project:

# my-cool-gem.gemspec
Gem::Specification.new do |spec|
  # ...
  spec.add_dependency "image_compare"
  # ...
end

Additionally, you may want to install oily_png to improve performance when using MRI. Just install it globally or add to your Gemfile.

Modes

ImageCompare supports different ways (modes) of comparing images.

Source images used in examples:

a.png

a.png

b.png

b.png

a1.png

a1.png

Base (Color) mode (a.png X a1.png)

Compare pixels, resulting score is a ratio of unequal pixels (with respect to provided tolerance).

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

color_diff

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.

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.

grayscale_diff.png

Delta (a.png X a1.png)

Compare pixels using Delta E distance. Resulting diff contains grayscale version of the first image with different pixels highlighted in red (with respect to diff score).

delta_diff.png

Usage

# Create new matcher with default threshold equals to 0
# and base (Color) mode
cmp = ImageCompare::Matcher.new
cmp.mode #=> ImageCompare::Modes::Color

# Create new matcher with default threshold equals to 0
# and base (RGB) mode
cmp = ImageCompare::Matcher.new mode: :rgb
cmp.mode #=> ImageCompare::Modes::RGB

# Create matcher with specific threshold
cmp = ImageCompare::Matcher.new threshold: 0.05
cmp.threshold #=> 0.05

# or with a lower threshold (in case you want to test that there is some difference)
cmp = ImageCompare::Matcher.new lower_threshold: 0.01
cmp.lower_threshold #=> 0.01

# Create zero-tolerance grayscale matcher
cmp = ImageCompare::Matcher.new mode: :grayscale, tolerance: 0
cmp.mode #=> ImageCompare::Modes::Grayscale

res = cmp.compare("path/image1.png", "path/image2.png")
res #=> ImageCompare::Result
res.match? #=> true
res.score #=> 0.0

# Return diff image object
res.difference_image #=> ImageCompare::Image
res.difference_image.save(result)

# without explicit matcher
res = ImageCompare.compare("path/image1.png", "path/image2.png", options)
res.match? #=> true
res.score #=> 0.0

# equals to
res = ImageCompare::Matcher.new(options).compare("my_images_path/image1.png", "my_images_path/image2.png")
res.match? #=> true
res.score #=> 0.0

Excluding rectangle (a.png X a1.png)

a1.png

You can exclude rectangle from comparing by passing :exclude_rect to compare. E.g., if path_1 and path_2 contain images above

ImageCompare.compare("path/image1.png", "path/image2.png", mode: :rgb, exclude_rect: [200, 150, 275, 200]).match? # => true

# or

cmp = ImageCompare::Matcher.new mode: :rgb, exclude_rect: [200, 150, 275, 200]
res = cmp.compare("path/image1.png", "path/image2.png")
res #=> ImageCompare::Result
res.match? #=> true
res.score #=> 0.0

# Return diff image object
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.

Cucumber + Capybara example

support/env.rb:

require "image_compare"

support/capybara_extensions.rb:

# frozen_string_literal: true

Capybara::Node::Element.class_eval do
  def rect_area
    rect_area = self.evaluate_script("this.getBoundingClientRect()") # rubocop:disable Style/RedundantSelf

    [
      rect_area["left"].to_f.round,
      rect_area["top"].to_f.round,
      rect_area["right"].to_f.round,
      rect_area["bottom"].to_f.round
    ]
  rescue StandardError => e # rubocop:disable Style/RescueStandardError
    raise "Error getting element rect area: #{e.inspect}!"
  end
end

steps/my_step.rb:

my_element = page.find("#my_element").rect_area

cmp = ImageCompare::Matcher.new exclude_rect: my_element.rect_area
res = cmp.compare("path/image1.png", "path/image2.png")
res.difference_image.save("path/diff.png")

expect(res.match?).to eq(true)

Including rectangle

You can set bounds of comparing by passing :include_rect to compare with array similar to previous example

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/instantink/image_compare.

Running tests and code inspections

  • rake rubocop
  • rake rubocop:md
  • rake spec
  • ruby examples/performance.rb Create the "gemfile.local" file with the content below to run the performance tests: ```ruby # frozen_string_literal: true

source "https://rubygems.org" gem "benchmark-ips", "~> 2.7", ">= 2.7.2"



## License

The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).