Skip to content

Instantly share code, notes, and snippets.

@jeffkreeftmeijer
Last active November 3, 2023 00:56
Show Gist options
  • Select an option

  • Save jeffkreeftmeijer/923894 to your computer and use it in GitHub Desktop.

Select an option

Save jeffkreeftmeijer/923894 to your computer and use it in GitHub Desktop.
Ruby image diff
require 'chunky_png'
images = [
ChunkyPNG::Image.from_file('1.png'),
ChunkyPNG::Image.from_file('2.png')
]
diff = []
images.first.height.times do |y|
images.first.row(y).each_with_index do |pixel, x|
diff << [x,y] unless pixel == images.last[x,y]
end
end
puts "pixels (total): #{images.first.pixels.length}"
puts "pixels changed: #{diff.length}"
puts "pixels changed (%): #{(diff.length.to_f / images.first.pixels.length) * 100}%"
x, y = diff.map{ |xy| xy[0] }, diff.map{ |xy| xy[1] }
images.last.rect(x.min, y.min, x.max, y.max, ChunkyPNG::Color.rgb(0,255,0))
images.last.save('diff.png')
# frozen_string_literal: true
source "https://rubygems.org"
gem "chunky_png"
GEM
remote: https://rubygems.org/
specs:
chunky_png (1.4.0)
PLATFORMS
ruby
DEPENDENCIES
chunky_png
BUNDLED WITH
2.1.4

Comparing images and creating image diffs

I’m sure you’ve seen the image view modes Github released last month. It’s a really nice way to see the differences between two versions of an image. In this article, I’ll try to explain how a simple image diff could be built using pure Ruby and ChunkyPNG.

If you need a more basic introduction to working with pixel data in ChunkyPNG, check out last week’s article, which I did some simple blob detection.

In its simplest form, finding differences in images works by looping over each pixel in the first image and checking if it’s the same as the pixel in the same spot in the second image. An implementation might look like this:

require 'chunky_png'

images = [ ChunkyPNG::Image.from_file('1.png'), ChunkyPNG::Image.from_file('2.png') ]

diff = []

images.first.height.times do |y| images.first.row(y).each_with_index do |pixel, x| diff << [x,y] unless pixel == images.last[x,y] end end

puts "pixels (total): #{images.first.pixels.length}" puts "pixels changed: #{diff.length}" puts "pixels changed (%): #{(diff.length.to_f / images.first.pixels.length) * 100}%"

x, y = diff.map{ |xy| xy[0] }, diff.map{ |xy| xy[1] }

images.last.rect(x.min, y.min, x.max, y.max, ChunkyPNG::Color.rgb(0,255,0)) images.last.save('diff.png')

Want the code? Here’s a Gist.

After loading in the two images, we’ll loop over the pixels of the first one. If the pixel is the same as the one in the second image, we’ll add it to the diff array. When we’re done, we’ll draw a bounding box around the area that contains the changes:

It worked! The result image has a bounding box around the hat we added to the image and the output tells us that almost 9% of the pixels in the image changed, which seems about right.

pixels (total):     16900
pixels changed:     1502
pixels changed (%): 8.887573964497042%

A problem with this approach is that it only detects change, without measuring it. It doesn’t care if the pixel it’s looking at is just a bit darker or a completely different color. If we use this code to compare one image to a slightly darker version of itself, the result will look like this:

pixels (total):     16900
pixels changed:     16900
pixels changed (%): 100.0%

This would mean that the two images are completely different, while (from a human eye’s perspective) they’re almost the same. To get a more accurate result, we’ll need to measure the difference in the pixels’ colors.

Calculating color difference

To calculate the color difference, we’ll use the the ΔE* (“Delta E”) distance metric. There are a couple of different versions of this metric, but we’ll take the first one (CIE76), since it’s the simplest and we don’t need anything too fancy. The ΔE* metric was created for the LAB color space, which was designed to approximate human vision. In this example, we’re not going to worry about converting to LAB, so we’ll just use the RGB color space (note that this will mean our results will be less accurate). If you want to know more about the difference, check out this demo.

Again, we loop over every pixel in the images. If they’re different, we calculate how different they are using the ΔE* metric and store that in the diff array. We also use that score to calculate a grayscale color value we use on the result image:

require 'chunky_png'
include ChunkyPNG::Color

images = [ ChunkyPNG::Image.from_file('1.png'), ChunkyPNG::Image.from_file('2.png') ]

output = ChunkyPNG::Image.new(images.first.width, images.last.width, WHITE)

diff = []

images.first.height.times do |y| images.first.row(y).each_with_index do |pixel, x| unless pixel == images.last[x,y] score = Math.sqrt( (r(images.last[x,y]) - r(pixel)) 2 + (g(images.last[x,y]) - g(pixel)) 2 + (b(images.last[x,y]) - b(pixel)) 2 ) / Math.sqrt(MAX 2 * 3)

  <span class="n">output</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span> <span class="o">=</span> <span class="n">grayscale</span><span class="p">(</span><span class="no">MAX</span> <span class="o">-</span> <span class="p">(</span><span class="n">score</span> <span class="o">*</span> <span class="no">MAX</span><span class="p">)</span><span class="o">.</span><span class="n">round</span><span class="p">)</span>
  <span class="n">diff</span> <span class="o">&lt;&lt;</span> <span class="n">score</span>
<span class="k">end</span>

end end

puts "pixels (total): #{images.first.pixels.length}" puts "pixels changed: #{diff.length}" puts "image changed (%): #{(diff.inject {|sum, value| sum + value} / images.first.pixels.length) * 100}%"

output.save('diff.png')

Want the code? Here’s a Gist.

Now we have a more accurate difference score. If we look at the output, we can see that less than 3% of the image was changed:

pixels (total):    16900
pixels changed:    1502
image changed (%): 2.882157784948056%

Again, a diff image is saved. This time, it shows the differences using shades of gray. Bigger changes are darker:

Now, let’s try the two images where the second one is slightly darker:

pixels (total):    16900
pixels changed:    16900
image changed (%): 5.4418255392228945%

Great. Now our code knows that the images are only darker, not completely different. If you look closely, you can see the difference in the result image.

What about Github?

Github uses a difference blend, which might be familiar if you’ve worked with image-editing software like Photoshop before. Doing something like that is quite simple. We loop over every pixel in the two images and calculate their difference per RGB channel:

require 'chunky_png'
include ChunkyPNG::Color

images = [ ChunkyPNG::Image.from_file('1.png'), ChunkyPNG::Image.from_file('2.png') ]

images.first.height.times do |y| images.first.row(y).each_with_index do |pixel, x|

<span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span> <span class="o">=</span> <span class="n">rgb</span><span class="p">(</span>
  <span class="n">r</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span> <span class="o">+</span> <span class="n">r</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span> <span class="o">-</span> <span class="mi">2</span> <span class="o">*</span> <span class="o">[</span><span class="n">r</span><span class="p">(</span><span class="n">pixel</span><span class="p">),</span> <span class="n">r</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span><span class="o">].</span><span class="n">min</span><span class="p">,</span>
  <span class="n">g</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span> <span class="o">+</span> <span class="n">g</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span> <span class="o">-</span> <span class="mi">2</span> <span class="o">*</span> <span class="o">[</span><span class="n">g</span><span class="p">(</span><span class="n">pixel</span><span class="p">),</span> <span class="n">g</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span><span class="o">].</span><span class="n">min</span><span class="p">,</span>
  <span class="n">b</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span> <span class="o">+</span> <span class="n">b</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span> <span class="o">-</span> <span class="mi">2</span> <span class="o">*</span> <span class="o">[</span><span class="n">b</span><span class="p">(</span><span class="n">pixel</span><span class="p">),</span> <span class="n">b</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span><span class="o">].</span><span class="n">min</span>
<span class="p">)</span>

end

end

images.last.save('diff.png')

Want the code? Here’s a Gist.

Using that, comparing the two images to the left would result in the diff-image on the right, nicely showing what changed:

Because the colors are compared by channel (R,G and B) instead of as one color, three scores are returned. This means the output image is in color, but comparing the channels separately can make the result less accurate.

As always, if you used this idea to build something yourself, know of a way to improve the code or have some questions or tips, be sure to let me know. If you want to know more about something I talked about, be sure to suggest it as a next article.

#+title: Comparing images and creating image diffs in Ruby
To measure the difference between two images, ChunkyPNG
[[https://github.com/wvanbergen/chunky_png][ChunkyPNG]] is a pure Ruby library for reading and writing PNG files.
#+caption: The input images
| =original.png= | =hat.png= |
|----------------+-----------|
| [[./original.png]] | [[./hat.png]] |
* Detecting differences
To detect differences between the two input images, this program loops over each pixel in both images and compares their pixel values:
#+name: box
#+headers: :exports both
#+headers: :results output
#+headers: :cache yes
#+begin_src ruby
require 'chunky_png'
one = ChunkyPNG::Image.from_file('original.png')
two = ChunkyPNG::Image.from_file('hat.png')
diff = []
one.height.times.map do |y|
one.row(y).each_with_index do |pixel, x|
diff << [x,y] unless pixel == two[x,y]
end
end
puts "pixels (total): #{one.pixels.length}"
puts "pixels changed: #{diff.length}"
puts "image changed (%): #{(diff.length.to_f / one.pixels.length) * 100}%"
diff.map(&:first).max
two.rect(
diff.map(&:first).min,
diff.map(&:last).min,
diff.map(&:first).max,
diff.map(&:last).max,
ChunkyPNG::Color.rgb(0,255,0)
)
two.save('diff-1.png')
#+end_src
This example program loads the two input images (=original.png= and =hat.png=).
It loops over each pixel in the first image, and compares it to the pixel in the same position in the second image.
The loop adds the pixel's =x= and =y= location to the =diff= array unless the two pixels are identical.
The program draws a box around the changed pixels by taking the lowest and highest =x= and =y= from the =diff= array and using them as control points for =ChunkyPNG::Canvas#rect=.
The result is saved to =diff-1.png=, and clearly shows the difference between the two input files:
#+caption: The difference between the two input images
| =original.png= | =hat.png= | =diff-1.png= |
|----------------+-----------+--------------|
| [[./original.png]] | [[./hat.png]] | [[./diff-1.png]] |
The program prints the amount of pixels that are different between the two input images, which is almost nine percent in this case:
#+RESULTS[e46bd1ae246646b24cf548ac8941b7f61370e929]: box
: pixels (total): 16900
: pixels changed: 1502
: image changed (%): 8.887573964497042%
* Measuring differences
A problem with the previous implementation is that it only /detects/ differences, without /measuring/ how different the images are.
For example, comparing one image to a slightly version produces the following result:
#+name: 100%
#+headers: :exports results
#+headers: :results output
#+headers: :cache yes
#+begin_src ruby
require 'chunky_png'
one = ChunkyPNG::Image.from_file('original.png')
two = ChunkyPNG::Image.from_file('dark.png')
diff = []
one.height.times.map do |y|
one.row(y).each_with_index do |pixel, x|
diff << [x,y] unless pixel == two[x,y]
end
end
puts "pixels (total): #{one.pixels.length}"
puts "pixels changed: #{diff.length}"
puts "image changed (%): #{(diff.length.to_f / one.pixels.length) * 100}%"
diff.map(&:first).max
two.rect(
diff.map(&:first).min,
diff.map(&:last).min,
diff.map(&:first).max,
diff.map(&:last).max,
ChunkyPNG::Color.rgb(0,255,0)
)
two.save('diff-2.png')
#+end_src
#+caption: The difference between the two input images
| =hat.png= | =dark.png= | =diff-2.png= |
|----------------+-----------+--------------|
| [[./hat.png]] | [[./dark.png]] | [[./diff-2.png]] |
The box now surrounds all pixels in the output image (=diff-2.png=), and the output reports that 100 percent of the image has changed:
#+RESULTS[3eb30efe4d38027da412f53ecb82a1c2b46d4c8c]: 100%
: pixels (total): 16900
: pixels changed: 16900
: image changed (%): 100.0%
** Measuring color difference
To calculate the color difference, we'll use the the ΔE/ ("Delta E") distance metric. There are a couple of different versions of this metric, but we'll take the first one (CIE76), since it's the simplest and we don't need anything too fancy. The ΔE/ metric was created for the LAB color space, which was designed to approximate human vision. In this example, we're not going to worry about converting to LAB, so we'll just use the RGB color space (note that this will mean our results will be less accurate). If you want to know more about the difference, check out this demo.
#+headers: :exports both
#+headers: :results output
#+begin_src ruby
require 'chunky_png'
one = ChunkyPNG::Image.from_file('original.png')
two = ChunkyPNG::Image.from_file('hat.png')
output = ChunkyPNG::Image.new(one.width, one.width, ChunkyPNG::Color::WHITE)
diff = []
one.height.times do |y|
one.row(y).each_with_index do |pixel, x|
unless pixel == two[x,y]
score = Math.sqrt(
(ChunkyPNG::Color.r(two[x,y]) - ChunkyPNG::Color.r(pixel)) ** 2 +
(ChunkyPNG::Color.g(two[x,y]) - ChunkyPNG::Color.g(pixel)) ** 2 +
(ChunkyPNG::Color.b(two[x,y]) - ChunkyPNG::Color.b(pixel)) ** 2
) / Math.sqrt(255 ** 2 * 3)
output[x,y] = ChunkyPNG::Color.grayscale(255 - (score * 255).round)
diff << score
end
end
end
puts "pixels (total): #{one.pixels.length}"
puts "pixels changed: #{diff.length}"
puts "image changed (%): #{(diff.inject {|sum, value| sum + value} / one.pixels.length) * 100}%"
output.save('diff-3.png')
#+end_src
#+RESULTS:
: pixels (total): 16900
: pixels changed: 1502
: image changed (%): 2.882157784948056%
[[./diff-3.png]]
#+headers: :exports both
#+headers: :results output
#+begin_src ruby
require 'chunky_png'
one = ChunkyPNG::Image.from_file('original.png')
two = ChunkyPNG::Image.from_file('dark.png')
output = ChunkyPNG::Image.new(one.width, one.width, ChunkyPNG::Color::WHITE)
diff = []
one.height.times do |y|
one.row(y).each_with_index do |pixel, x|
unless pixel == two[x,y]
score = Math.sqrt(
(ChunkyPNG::Color.r(two[x,y]) - ChunkyPNG::Color.r(pixel)) ** 2 +
(ChunkyPNG::Color.g(two[x,y]) - ChunkyPNG::Color.g(pixel)) ** 2 +
(ChunkyPNG::Color.b(two[x,y]) - ChunkyPNG::Color.b(pixel)) ** 2
) / Math.sqrt(255 ** 2 * 3)
output[x,y] = ChunkyPNG::Color.grayscale(255 - (score * 255).round)
diff << score
end
end
end
puts "pixels (total): #{one.pixels.length}"
puts "pixels changed: #{diff.length}"
puts "image changed (%): #{(diff.inject {|sum, value| sum + value} / one.pixels.length) * 100}%"
output.save('diff-4.png')
#+end_src
#+RESULTS:
: pixels (total): 16900
: pixels changed: 16900
: image changed (%): 7.99418157777341%
[[./diff-4.png]]
#+headers: :exports both
#+headers: :results output
#+begin_src ruby
require 'chunky_png'
one = ChunkyPNG::Image.from_file('original.png')
two = ChunkyPNG::Image.from_file('dark.png')
diff = []
one.height.times do |y|
one.row(y).each_with_index do |pixel, x|
two[x,y] = ChunkyPNG::Color.rgb(
ChunkyPNG::Color.r(pixel) + ChunkyPNG::Color.r(two[x,y]) - 2 * [ChunkyPNG::Color.r(pixel), ChunkyPNG::Color.r(two[x,y])].min,
ChunkyPNG::Color.g(pixel) + ChunkyPNG::Color.g(two[x,y]) - 2 * [ChunkyPNG::Color.g(pixel), ChunkyPNG::Color.g(two[x,y])].min,
ChunkyPNG::Color.b(pixel) + ChunkyPNG::Color.b(two[x,y]) - 2 * [ChunkyPNG::Color.b(pixel), ChunkyPNG::Color.b(two[x,y])].min
)
end
end
two.save('diff-5.png')
#+end_src
#+RESULTS:
[[./diff-5.png]]
-----
#+begin_html
<h1>
#+end_html
Comparing images and creating image diffs
#+begin_html
</h1>
#+end_html
#+begin_html
<p>
#+end_html
I'm sure you've seen the image view modes Github released last month. It's a really nice way to see the differences between two versions of an image. In this article, I'll try to explain how a simple image diff could be built using pure Ruby and ChunkyPNG.
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
If you need a more basic introduction to working with pixel data in ChunkyPNG, check out last week's article, which I did some simple blob detection.
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
In its simplest form, finding differences in images works by looping over each pixel in the first image and checking if it's the same as the pixel in the same spot in the second image. An implementation might look like this:
#+begin_html
</p>
#+end_html
#+begin_html
<pre><code class="ruby"><span class="nb">require</span> <span class="s1">'chunky_png'</span>
<span class="n">images</span> <span class="o">=</span> <span class="o">[</span>
<span class="ss">ChunkyPNG</span><span class="p">:</span><span class="ss">:Image</span><span class="o">.</span><span class="n">from_file</span><span class="p">(</span><span class="s1">'1.png'</span><span class="p">),</span>
<span class="ss">ChunkyPNG</span><span class="p">:</span><span class="ss">:Image</span><span class="o">.</span><span class="n">from_file</span><span class="p">(</span><span class="s1">'2.png'</span><span class="p">)</span>
<span class="o">]</span>
<span class="n">diff</span> <span class="o">=</span> <span class="o">[]</span>
<span class="n">images</span><span class="o">.</span><span class="n">first</span><span class="o">.</span><span class="n">height</span><span class="o">.</span><span class="n">times</span> <span class="k">do</span> <span class="o">|</span><span class="n">y</span><span class="o">|</span>
<span class="n">images</span><span class="o">.</span><span class="n">first</span><span class="o">.</span><span class="n">row</span><span class="p">(</span><span class="n">y</span><span class="p">)</span><span class="o">.</span><span class="n">each_with_index</span> <span class="k">do</span> <span class="o">|</span><span class="n">pixel</span><span class="p">,</span> <span class="n">x</span><span class="o">|</span>
<span class="n">diff</span> <span class="o">&lt;&lt;</span> <span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span> <span class="k">unless</span> <span class="n">pixel</span> <span class="o">==</span> <span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="nb">puts</span> <span class="s2">"pixels (total): </span><span class="si">#{</span><span class="n">images</span><span class="o">.</span><span class="n">first</span><span class="o">.</span><span class="n">pixels</span><span class="o">.</span><span class="n">length</span><span class="si">}</span><span class="s2">"</span>
<span class="nb">puts</span> <span class="s2">"pixels changed: </span><span class="si">#{</span><span class="n">diff</span><span class="o">.</span><span class="n">length</span><span class="si">}</span><span class="s2">"</span>
<span class="nb">puts</span> <span class="s2">"pixels changed (%): </span><span class="si">#{</span><span class="p">(</span><span class="n">diff</span><span class="o">.</span><span class="n">length</span><span class="o">.</span><span class="n">to_f</span> <span class="o">/</span> <span class="n">images</span><span class="o">.</span><span class="n">first</span><span class="o">.</span><span class="n">pixels</span><span class="o">.</span><span class="n">length</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span><span class="si">}</span><span class="s2">%"</span>
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">diff</span><span class="o">.</span><span class="n">map</span><span class="p">{</span> <span class="o">|</span><span class="n">xy</span><span class="o">|</span> <span class="n">xy</span><span class="o">[</span><span class="mi">0</span><span class="o">]</span> <span class="p">},</span> <span class="n">diff</span><span class="o">.</span><span class="n">map</span><span class="p">{</span> <span class="o">|</span><span class="n">xy</span><span class="o">|</span> <span class="n">xy</span><span class="o">[</span><span class="mi">1</span><span class="o">]</span> <span class="p">}</span>
<span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">.</span><span class="n">rect</span><span class="p">(</span><span class="n">x</span><span class="o">.</span><span class="n">min</span><span class="p">,</span> <span class="n">y</span><span class="o">.</span><span class="n">min</span><span class="p">,</span> <span class="n">x</span><span class="o">.</span><span class="n">max</span><span class="p">,</span> <span class="n">y</span><span class="o">.</span><span class="n">max</span><span class="p">,</span> <span class="ss">ChunkyPNG</span><span class="p">:</span><span class="ss">:Color</span><span class="o">.</span><span class="n">rgb</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">255</span><span class="p">,</span><span class="mi">0</span><span class="p">))</span>
<span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="s1">'diff.png'</span><span class="p">)</span>
</code></pre>
#+end_html
#+begin_html
<p>
#+end_html
Want the code? Here's a Gist.
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
After loading in the two images, we'll loop over the pixels of the first one. If the pixel is the same as the one in the second image, we'll add it to the diff array. When we're done, we'll draw a bounding box around the area that contains the changes:
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
It worked! The result image has a bounding box around the hat we added to the image and the output tells us that almost 9% of the pixels in the image changed, which seems about right.
#+begin_html
</p>
#+end_html
#+begin_html
<pre><code>pixels (total): 16900
pixels changed: 1502
pixels changed (%): 8.887573964497042%</code></pre>
#+end_html
#+begin_html
<p>
#+end_html
A problem with this approach is that it only detects change, without measuring it. It doesn't care if the pixel it's looking at is just a bit darker or a completely different color. If we use this code to compare one image to a slightly darker version of itself, the result will look like this:
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
#+begin_html
</p>
#+end_html
#+begin_html
<pre><code>pixels (total): 16900
pixels changed: 16900
pixels changed (%): 100.0%</code></pre>
#+end_html
#+begin_html
<p>
#+end_html
This would mean that the two images are completely different, while (from a human eye's perspective) they're almost the same. To get a more accurate result, we'll need to measure the difference in the pixels' colors.
#+begin_html
</p>
#+end_html
#+begin_html
<h3>
#+end_html
Calculating color difference
#+begin_html
</h3>
#+end_html
#+begin_html
<p>
#+end_html
To calculate the color difference, we'll use the the ΔE/ ("Delta E") distance metric. There are a couple of different versions of this metric, but we'll take the first one (CIE76), since it's the simplest and we don't need anything too fancy. The ΔE/ metric was created for the LAB color space, which was designed to approximate human vision. In this example, we're not going to worry about converting to LAB, so we'll just use the RGB color space (note that this will mean our results will be less accurate). If you want to know more about the difference, check out this demo.
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
Again, we loop over every pixel in the images. If they're different, we calculate how different they are using the ΔE* metric and store that in the diff array. We also use that score to calculate a grayscale color value we use on the result image:
#+begin_html
</p>
#+end_html
#+begin_html
<pre><code class="ruby"><span class="nb">require</span> <span class="s1">'chunky_png'</span>
<span class="kp">include</span> <span class="ss">ChunkyPNG</span><span class="p">:</span><span class="ss">:Color</span>
<span class="n">images</span> <span class="o">=</span> <span class="o">[</span>
<span class="ss">ChunkyPNG</span><span class="p">:</span><span class="ss">:Image</span><span class="o">.</span><span class="n">from_file</span><span class="p">(</span><span class="s1">'1.png'</span><span class="p">),</span>
<span class="ss">ChunkyPNG</span><span class="p">:</span><span class="ss">:Image</span><span class="o">.</span><span class="n">from_file</span><span class="p">(</span><span class="s1">'2.png'</span><span class="p">)</span>
<span class="o">]</span>
<span class="n">output</span> <span class="o">=</span> <span class="ss">ChunkyPNG</span><span class="p">:</span><span class="ss">:Image</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">first</span><span class="o">.</span><span class="n">width</span><span class="p">,</span> <span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">.</span><span class="n">width</span><span class="p">,</span> <span class="no">WHITE</span><span class="p">)</span>
<span class="n">diff</span> <span class="o">=</span> <span class="o">[]</span>
<span class="n">images</span><span class="o">.</span><span class="n">first</span><span class="o">.</span><span class="n">height</span><span class="o">.</span><span class="n">times</span> <span class="k">do</span> <span class="o">|</span><span class="n">y</span><span class="o">|</span>
<span class="n">images</span><span class="o">.</span><span class="n">first</span><span class="o">.</span><span class="n">row</span><span class="p">(</span><span class="n">y</span><span class="p">)</span><span class="o">.</span><span class="n">each_with_index</span> <span class="k">do</span> <span class="o">|</span><span class="n">pixel</span><span class="p">,</span> <span class="n">x</span><span class="o">|</span>
<span class="k">unless</span> <span class="n">pixel</span> <span class="o">==</span> <span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span>
<span class="n">score</span> <span class="o">=</span> <span class="no">Math</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span>
<span class="p">(</span><span class="n">r</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span> <span class="o">-</span> <span class="n">r</span><span class="p">(</span><span class="n">pixel</span><span class="p">))</span> <span class="o">**</span> <span class="mi">2</span> <span class="o">+</span>
<span class="p">(</span><span class="n">g</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span> <span class="o">-</span> <span class="n">g</span><span class="p">(</span><span class="n">pixel</span><span class="p">))</span> <span class="o">**</span> <span class="mi">2</span> <span class="o">+</span>
<span class="p">(</span><span class="n">b</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span> <span class="o">-</span> <span class="n">b</span><span class="p">(</span><span class="n">pixel</span><span class="p">))</span> <span class="o">**</span> <span class="mi">2</span>
<span class="p">)</span> <span class="o">/</span> <span class="no">Math</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="no">MAX</span> <span class="o">**</span> <span class="mi">2</span> <span class="o">*</span> <span class="mi">3</span><span class="p">)</span>
<span class="n">output</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span> <span class="o">=</span> <span class="n">grayscale</span><span class="p">(</span><span class="no">MAX</span> <span class="o">-</span> <span class="p">(</span><span class="n">score</span> <span class="o">*</span> <span class="no">MAX</span><span class="p">)</span><span class="o">.</span><span class="n">round</span><span class="p">)</span>
<span class="n">diff</span> <span class="o">&lt;&lt;</span> <span class="n">score</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="nb">puts</span> <span class="s2">"pixels (total): </span><span class="si">#{</span><span class="n">images</span><span class="o">.</span><span class="n">first</span><span class="o">.</span><span class="n">pixels</span><span class="o">.</span><span class="n">length</span><span class="si">}</span><span class="s2">"</span>
<span class="nb">puts</span> <span class="s2">"pixels changed: </span><span class="si">#{</span><span class="n">diff</span><span class="o">.</span><span class="n">length</span><span class="si">}</span><span class="s2">"</span>
<span class="nb">puts</span> <span class="s2">"image changed (%): </span><span class="si">#{</span><span class="p">(</span><span class="n">diff</span><span class="o">.</span><span class="n">inject</span> <span class="p">{</span><span class="o">|</span><span class="n">sum</span><span class="p">,</span> <span class="n">value</span><span class="o">|</span> <span class="n">sum</span> <span class="o">+</span> <span class="n">value</span><span class="si">}</span><span class="s2"> / images.first.pixels.length) * 100}%"</span>
<span class="n">output</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="s1">'diff.png'</span><span class="p">)</span>
</code></pre>
#+end_html
#+begin_html
<p>
#+end_html
Want the code? Here's a Gist.
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
Now we have a more accurate difference score. If we look at the output, we can see that less than 3% of the image was changed:
#+begin_html
</p>
#+end_html
#+begin_html
<pre><code>pixels (total): 16900
pixels changed: 1502
image changed (%): 2.882157784948056%</code></pre>
#+end_html
#+begin_html
<p>
#+end_html
Again, a diff image is saved. This time, it shows the differences using shades of gray. Bigger changes are darker:
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
Now, let's try the two images where the second one is slightly darker:
#+begin_html
</p>
#+end_html
#+begin_html
<pre><code>pixels (total): 16900
pixels changed: 16900
image changed (%): 5.4418255392228945%</code></pre>
#+end_html
#+begin_html
<p>
#+end_html
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
Great. Now our code knows that the images are only darker, not completely different. If you look closely, you can see the difference in the result image.
#+begin_html
</p>
#+end_html
#+begin_html
<h3>
#+end_html
What about Github?
#+begin_html
</h3>
#+end_html
#+begin_html
<p>
#+end_html
Github uses a difference blend, which might be familiar if you've worked with image-editing software like Photoshop before. Doing something like that is quite simple. We loop over every pixel in the two images and calculate their difference per RGB channel:
#+begin_html
</p>
#+end_html
#+begin_html
<pre><code class="ruby"><span class="nb">require</span> <span class="s1">'chunky_png'</span>
<span class="kp">include</span> <span class="ss">ChunkyPNG</span><span class="p">:</span><span class="ss">:Color</span>
<span class="n">images</span> <span class="o">=</span> <span class="o">[</span>
<span class="ss">ChunkyPNG</span><span class="p">:</span><span class="ss">:Image</span><span class="o">.</span><span class="n">from_file</span><span class="p">(</span><span class="s1">'1.png'</span><span class="p">),</span>
<span class="ss">ChunkyPNG</span><span class="p">:</span><span class="ss">:Image</span><span class="o">.</span><span class="n">from_file</span><span class="p">(</span><span class="s1">'2.png'</span><span class="p">)</span>
<span class="o">]</span>
<span class="n">images</span><span class="o">.</span><span class="n">first</span><span class="o">.</span><span class="n">height</span><span class="o">.</span><span class="n">times</span> <span class="k">do</span> <span class="o">|</span><span class="n">y</span><span class="o">|</span>
<span class="n">images</span><span class="o">.</span><span class="n">first</span><span class="o">.</span><span class="n">row</span><span class="p">(</span><span class="n">y</span><span class="p">)</span><span class="o">.</span><span class="n">each_with_index</span> <span class="k">do</span> <span class="o">|</span><span class="n">pixel</span><span class="p">,</span> <span class="n">x</span><span class="o">|</span>
<span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span> <span class="o">=</span> <span class="n">rgb</span><span class="p">(</span>
<span class="n">r</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span> <span class="o">+</span> <span class="n">r</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span> <span class="o">-</span> <span class="mi">2</span> <span class="o">*</span> <span class="o">[</span><span class="n">r</span><span class="p">(</span><span class="n">pixel</span><span class="p">),</span> <span class="n">r</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span><span class="o">].</span><span class="n">min</span><span class="p">,</span>
<span class="n">g</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span> <span class="o">+</span> <span class="n">g</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span> <span class="o">-</span> <span class="mi">2</span> <span class="o">*</span> <span class="o">[</span><span class="n">g</span><span class="p">(</span><span class="n">pixel</span><span class="p">),</span> <span class="n">g</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span><span class="o">].</span><span class="n">min</span><span class="p">,</span>
<span class="n">b</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span> <span class="o">+</span> <span class="n">b</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span> <span class="o">-</span> <span class="mi">2</span> <span class="o">*</span> <span class="o">[</span><span class="n">b</span><span class="p">(</span><span class="n">pixel</span><span class="p">),</span> <span class="n">b</span><span class="p">(</span><span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">[</span><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="o">]</span><span class="p">)</span><span class="o">].</span><span class="n">min</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">images</span><span class="o">.</span><span class="n">last</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="s1">'diff.png'</span><span class="p">)</span>
</code></pre>
#+end_html
#+begin_html
<p>
#+end_html
Want the code? Here's a Gist.
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
Using that, comparing the two images to the left would result in the diff-image on the right, nicely showing what changed:
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
Because the colors are compared by channel (R,G and B) instead of as one color, three scores are returned. This means the output image is in color, but comparing the channels separately can make the result less accurate.
#+begin_html
</p>
#+end_html
#+begin_html
<p>
#+end_html
As always, if you used this idea to build something yourself, know of a way to improve the code or have some questions or tips, be sure to let me know. If you want to know more about something I talked about, be sure to suggest it as a next article.
#+begin_html
</p>
#+end_html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment