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

http://jeffkreeftmeijer.com/2011/comparing-images-and-creating-image-diffs/
  • Перевод
Наверняка вы видели новые режимы просмотра изображений, которые 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) вместо одного цвета, то возвращается три значения. Это значит, что результирующая картинка цветная, но такое сравнение отдельно по каждому каналу может негативно отразиться на точности результата.
  • +29
  • 15,3k
  • 9
Поделиться публикацией
Комментарии 9
    +1
    Спасибо за перевод, отличный способ сравнения.
    А как быть с картинками которые изменились в масштабах (и соответственно в цветах отдельно взятых пикселей)?
    • НЛО прилетело и опубликовало эту надпись здесь
        0
        Такой коммент уже прозвучал в оригинальном посте, и было предложено разбить оригинал картинки на пиксели и обрамить каждый прозрачным «буфером». Не понял идею товарища, но она потолкнула на другую приблизительно похожую. В фотошопе есть пара типов масштабирования изображения типа bicubic.

        Так вот, если размер картинки увеличился и знать каким типом масштабирования пользовались, то можно примерно сравнить на схожесть одного пикселя оригинала на определённую зону изменённого изображения (пользуясь алгоритмом типа масштабирования).

        Проблема возникнет если картинку уменьшили.
        • НЛО прилетело и опубликовало эту надпись здесь
        0
        Первую тоже можно увеличить :)
      • НЛО прилетело и опубликовало эту надпись здесь
          0
          Если не секрет, сколько по времени работает этот алгоритм при сравнении изображений размером 1280х1024 на Вашей машине?
            0
            По вашему запросу на Маке Core i7 4GB RAM портрет Че заняло примерно:

            real 0m34.269s
            user 0m25.560s
            sys 0m8.231s

            0
            повторил на OpenCV — забавно, особенно на сдвиги :)

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое