Сравните изображения построчно, используя vips

Предыстория: у меня есть изображения, которые мне нужно сравнить на предмет различий. Изображения большие (порядка 1400x9000 пикселей), сгенерированные машиной и сильно ограниченные (скриншоты конкретной части линейного пользовательского интерфейса), и ожидается, что они будут почти идентичными, а различия заключаются в одной из следующих трех возможностей:

  • На изображении 1 есть разрез, на изображении 2 отсутствует
  • На изображении 1 отсутствует изображение раздела 2.
  • Оба изображения имеют данный раздел, но его содержимое отличается

Я пытаюсь создать инструмент, который выделяет различия для человека-рецензента, по сути, графическую версию линейно-ориентированного diff. С этой целью я пытаюсь сканировать изображения построчно и сравнивать их, чтобы решить, идентичны ли строки. Моя конечная цель — фактический вывод, похожий на diff, где он может определить, что разделы отсутствуют/добавлены/отличны, и как можно скорее синхронизировать изображения для оставшихся частей идентичного контента, но для первого разреза я используя более простой подход, при котором два изображения накладываются друг на друга (альфа-смешение), а линии, которые были разными, выделяются определенным цветом (т.е. альфа-смешение с третьей строкой сплошного цвета). Сначала я пытался использовать Python Imaging Library, но это было на несколько порядков медленнее, поэтому я решил попробовать использовать vips, который должен быть намного быстрее. Однако я совершенно не представляю, как выразить то, чем я являюсь после использования операций vips. Псевдокод для более простой версии будет по существу:

out = []
# image1 and image2 are expected, but not guaranteed to have the same height
# they are likely to have different heights if different
# most lines are entirely white pixels
for line1, line2 in zip(image1, image2):
    if line1 == line2:
        out.append(line1)
    else:
        # ALL_RED is a line composed of solid red pixels
        out.append(line1.blend(line2, 0.5).blend(ALL_RED, 0.5))

Я использую pyvips в своем проекте, но меня также интересует код с использованием простого vips или любых других привязок, поскольку операции являются общими и легко переводятся для разных диалектов.

Изменить: добавление образцов изображений по запросуimage1 image2

Редактировать 2: полноразмерные изображения с отсутствующими/добавленными/измененными разделами:


person mathrick    schedule 22.10.2019    source источник
comment
Некоторые репрезентативные изображения были бы полезны, пожалуйста.   -  person Mark Setchell    schedule 22.10.2019
comment
Добавлено несколько репрезентативных образцов   -  person mathrick    schedule 22.10.2019
comment
Вы действительно сравниваете настройки заданий Jenkins? Если это так, вы должны знать, что они хранятся в XML-файлах, и существуют инструменты для сравнения XML-файлов. Также я считаю, что плагин ThinBackup может выполнять дифференциальное резервное копирование, что также дает представление о том, что действительно изменилось.   -  person Nakilon    schedule 23.10.2019
comment
Я, и я делаю это как часть разработки набора тестов для анализатора заданий Jenkins, поэтому я хорошо знаю, какие задания хранятся как:)   -  person mathrick    schedule 23.10.2019


Ответы (2)


Как насчет того, чтобы просто использовать diff? Это довольно быстро. Все, что вам нужно сделать, это превратить ваши PNG в текст по одной строке сканирования, а затем проанализировать вывод diff.

Например:

#!/usr/bin/env python3

import sys
import os
import re
import pyvips

# calculate a checksum for each scanline and write to name_out    
def scanline_checksum(name_in, name_out):
    a = pyvips.Image.new_from_file(name_in, access="sequential")
    # unfold colour channels to make a wider 1-band image
    a = a.bandunfold()
    # xyz makes an index image, where the value of each pixel is its coordinate
    b = pyvips.Image.xyz(a.width, a.height)
    # make a pow gradient image ... each pixel is some power of the x coordinate
    b = b[0] ** 0.5
    # now multiply and sum to make a checksum for each scanline
    # "project" returns sum of columns, sum of rows
    sum_of_columns, sum_of_rows = (a * b).project()
    sum_of_rows.write_to_file(name_out)

to_csv(sys.argv[1], "1.csv")
to_csv(sys.argv[2], "2.csv")

os.system("diff 1.csv 2.csv > diff.csv")

for line in open("diff.csv", "r"):
    match = re.match("(\\d+),(\\d+)c(\\d+),(\\d+)", line)
    if not match:
        continue
    print(line)

Для ваших двух тестовых изображений я вижу:

$ time ./diff.py 1.png 2.png 
264,272c264,272
351,359c351,359
real    0m0.346s
user    0m0.445s
sys 0m0.033s

На этом пожилом ноутбуке. Все, что вам нужно сделать, это использовать эти команды «изменить», чтобы разметить ваши изображения.

person jcupitt    schedule 22.10.2019
comment
Это действительно интересный подход. Я заглянул в .project(), но не понял, как использовать его для чего-то большего, чем простая сумма. Знаете ли вы, как, скажем, получить представление строки в base64? Или что-нибудь еще с достаточной структурой, чтобы быть биекцией, чтобы я мог использовать difflib на нем, а затем превратить его обратно в данные изображения? - person mathrick; 22.10.2019
comment
Вы можете удалить .project -- тогда каждая строка будет просто списком значений пикселей RGBRGBRGB. Хотя это было бы намного медленнее. - person jcupitt; 22.10.2019
comment
Как бы я сделал что-то более красивое? Мне нравится ваша идея сначала вычислить наценку, поэтому большинство строк можно пропустить, но есть ли способ выразить reduce(lambda x, y: x / 2 + y, pixels_in_line, init=0) как операцию Vips? Т.е. закодируйте их как своего рода двоичный файл с плавающей запятой, где каждый пиксель имеет вдвое больший вес, чем предыдущий. Я думаю, что это всегда будет улавливать строки, которые отличаются. - person mathrick; 23.10.2019
comment
Хорошо, у меня был удар по более причудливой контрольной сумме. Это должно определять элементы, перемещаемые влево-вправо. - person jcupitt; 23.10.2019
comment
Это выглядит здорово! К сожалению, я только что понял, что CSV - это выходной формат, который может создавать Vips, хотя он немного хромает, он может записывать его только в файл на диске. Было бы здорово, если бы он поддерживал генерацию в буфере (или даже лучше, если бы я мог просто получить числа в буфере). Как бы то ни было, bytes(scanline_checksum.write_to_memory()) намного медленнее, чем запись CSV в файл и обратное чтение, что неоптимально, тем более что байты все еще находятся в паре медленных операций от преобразования в числа, которые фактически запустил Vips. с участием. Но я могу использовать CSV, спасибо! - person mathrick; 23.10.2019
comment
Надеюсь, в следующей версии CSV будет читать/записывать в память. Вы можете превратить изображения libvips в массивы Python следующим образом: libvips.github.io /pyvips/intro.html#numpy-and-pil - person jcupitt; 23.10.2019
comment
Ах, это действительно приятно на самом деле. Я закончил тем, что сделал это и использовал SequenceMatcher для функциональности diff, и это работает очень хорошо. - person mathrick; 24.10.2019
comment
Однако, как ни странно, иногда идентичные строки имеют разные контрольные суммы (т.е. я могу убедиться, что они идентичны, сравнивая их после .extract_area()), что сбивает сопоставитель. У меня действительно не было времени на отладку, есть идеи, что может быть причиной? ``` (Pdb++) a[i] 46140707.35610962 (Pdb++) b[j] 46120071.029815674 (Pdb++) img1.extract_area(0,i,img1.width,1) ‹pyvips.Image 1400x1 uchar, 3 полосы, srgb› (Pdb++ ) mem1 = img1.extract_area(0,i,img1.width,1).write_to_memory() (Pdb++) mem2 = img2.extract_area(0,j,img2.width,1).write_to_memory() (Pdb++) mem1 == mem2 Истинно ``` - person mathrick; 24.10.2019
comment
Ха, это очень странно. Пожалуйста, откройте проблему на трекере libvips с кодом и изображениями, чтобы воспроизвести это, и я это исправлю. github.com/libvips/libvips/issues - person jcupitt; 24.10.2019

Если вам подходят OpenCV и NumPy, то будет довольно простое решение, по крайней мере, для поиска и окрашивания разных строк.

В моем подходе я просто вычисляю различия в пикселях, используя np.abs< /a> и найти ненулевые индексы строк с помощью np.nonzero< /а>. С этими найденными индексами строк я устанавливаю дополнительное черное изображение и рисую красные линии для каждой строки. Окончательное смешивание — это просто линейное смешивание:

0.5 * image1 + 0.5 * image2

для всех равных строк или

0.333 * image1 + 0.333 * image2 + 0.333 * red

для всех разных строк.

Вот окончательный код:

import cv2
import numpy as np

# Load images
first = cv2.imread('9gOlq.png', cv2.IMREAD_COLOR)
second = cv2.imread('1Hdx4.png', cv2.IMREAD_COLOR)

# Calcluate absolute differences between images
diff = np.abs(np.float32(first) - np.float32(second))

# Find all non-zero rows
nz_rows = np.unique(np.nonzero(diff)[0])

# Set up image with red lines
red = np.zeros(first.shape, np.uint8)
red[nz_rows, :, :] = [0, 0, 255]

# Set up output image
output = np.uint8(0.5 * first + 0.5 * second)
output[nz_rows, :, :] = 0.333 * first[nz_rows, :, :] + 0.333 * second[nz_rows, :, :] + 0.333 * red[nz_rows, :, :]

# Show results
cv2.imshow("diff", np.array(diff, dtype=np.uint8))
cv2.imshow("output", output)
cv2.waitKey()
cv2.destroyAllWindows()

Разностное изображение diff выглядит так:

Различное изображение

Окончательный output выглядит так:

Вывод

Было бы интересно увидеть два входных изображения с пропущенными разделами, как вы описали в своем вопросе. Кроме того, необходимо протестировать этот подход с использованием изображений исходного размера, поскольку вы упомянули, что время имеет решающее значение.

В любом случае - надеюсь, что это поможет!

person HansHirse    schedule 22.10.2019
comment
Спасибо, это действительно многообещающе. Я связал полноразмерные изображения с более обширными изменениями в вопросе. - person mathrick; 22.10.2019