Pull to refresh

Сравнение изображений и генерация картинки отличий на Ruby

Reading time 4 min
Views 28K
Original author: Jeff Kreeftmeijer
Наверняка вы видели новые режимы просмотра изображений, которые Github выкатил в прошлом месяце. Это действительно изящный способ показать разницу между двумя версиями картинки. В этой статье я попробую объяснить, как можно просто сравнивать изображения с помощью только Ruby и ChunkyPNG.

В самом простом варианте поиск отличий сводится к обходу каждого пикселя в первой картинке и проверке, есть ли этот пиксель во второй. Реализация может выглядеть примерно так:

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') 
Код: Gist.

После загрузки двух изображений мы обходим каждый пиксель в первом, и если он присутствует во втором, добавляем его в массив diff. После этого рисуем рамку вокруг области, в которой есть отличия:



Работает! В итоговом изображении присутствует рамка вокруг шляпы, которую мы добавили на фотографию и в итоге мы видим, что почти 9% пикселей на фотографии изменили своё значение.

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


Проблема такого подхода в том, что он только определяет изменения, не измеряя их. Нет разницы, стал пиксель лишь чуточку темнее, либо у него полностью другой цвет. Если мы применим этот код к чуть затемнённой версии фотографии, то результат будет выглядеть так:



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


Выходит, что две фотографии полностью отличаются, хотя на глаз они почти одинаковы. Чтобы получить более точный результат, нам нужно измерить разницу между значениями пикселей.

Вычисление цветового отличия

Чтобы вычислить отличия в цвете, мы будем использовать метрику ΔE* (“Delta E”). Существует три разные формулы для этой метрики, но мы возьмём первую из них (CIE76), потому что она самая простая, а нам не нужно что-то заумное. Метрика ΔE* была создана для цветового пространства LAB, которое максимально соответствует человеческому зрению. В этом примере мы не будет конвертировать цвета в LAB, а просто будем работать в цветовом пространстве RGB (заметьте, это значит, что наши результаты не будут такими точными). Если вам интересно, чем отличаются различные цветовые пространства, посмотрите это демо.

Как и раньше, мы пройдёмся по всем пикселям в изображениях. Если они отличаются, то мы применим метрику ΔE* и сохраним результат в массиве diff. Мы также применим этот результат для вычисления значения серого цвета, которое будет использоваться в конечной сравнительной картинке.

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)

      output[x,y] = grayscale(MAX - (score * MAX).round)
      diff << score
    end
  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') 
Код: Gist.

Теперь мы получили более точную картину отличий. Если посмотреть на результат, то увидим, что изменилось менее 3% фотографии.

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


Снова, мы сохраняем результат — и в этот раз он уже показывает отличия в оттенках серого. Более сильные изменения имеют более тёмный цвет.



А теперь давайте попробуем те два изображения, в которых второе было чуть темнее.

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




Отлично. Теперь наша программа знает, что фотографии только немножко отличаются, а не совсем разные. Если посмотреть внимательно, то можно даже увидеть кокретные участки, где изображение отличается.

Что делает Github?

Github использует режим тональной разницы, который знаком по фоторедакторам, таким как Photoshop. Это довольно простой метод. Мы обходим каждый пиксель в двух изображениях и вычисляем их разницу по каналам RGB:

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|

    images.last[x,y] = rgb(
      r(pixel) + r(images.last[x,y]) - 2 * [r(pixel), r(images.last[x,y])].min,
      g(pixel) + g(images.last[x,y]) - 2 * [g(pixel), g(images.last[x,y])].min,
      b(pixel) + b(images.last[x,y]) - 2 * [b(pixel), b(images.last[x,y])].min
    )
  end

end

images.last.save('diff.png') 
Код: Gist.

Используя этот метод, сравнение двух фотографий слева выдаёт картину отличий на изображении справа, чётко показывая изменения:



Поскольку цвета сравниваются по каналам (R,G и B) вместо одного цвета, то возвращается три значения. Это значит, что результирующая картинка цветная, но такое сравнение отдельно по каждому каналу может негативно отразиться на точности результата.
Tags:
Hubs:
+29
Comments 9
Comments Comments 9

Articles