Как я могу быстрее всего сравнить два растровых изображения одинакового размера, чтобы определить, идентичны ли они?

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

Хотя это работает и хорошо работает для небольших растровых изображений, в производстве я собираюсь использовать это в узком цикле и на больших изображениях, поэтому мне нужен лучший способ. Есть ли у кого-нибудь рекомендации?

Между прочим, я использую язык C # - и да, я уже использую метод .LockBits. знак равно

Изменить: я закодировал реализации некоторых из предложенных предложений, и вот контрольные показатели. Настройка: два идентичных (наихудший случай) растрового изображения размером 100x100 с 10 000 итераций каждое. Вот результаты:

CompareByInts (Marc Gravell) :   1107ms
CompareByMD5  (Skilldrick)   :   4222ms
CompareByMask (GrayWizardX)  :    949ms

В CompareByInts и CompareByMask я использую указатели для прямого доступа к памяти; в методе MD5 я использую Marshal.Copy для получения массива байтов и передачи его в качестве аргумента в MD5.ComputeHash. CompareByMask только немного быстрее, но, учитывая контекст, я думаю, что любое улучшение полезно.

Всем спасибо. знак равно

Редактировать 2: забыл включить оптимизацию - это дает ответ GrayWizardX еще больше:

CompareByInts   (Marc Gravell) :    944ms
CompareByMD5    (Skilldrick)   :   4275ms
CompareByMask   (GrayWizardX)  :    630ms
CompareByMemCmp (Erik)         :    105ms

Интересно, что метод MD5 вообще не улучшился.

Edit 3: опубликовал мой ответ (MemCmp), который поразил другие методы. o.O


person Erik Forbes    schedule 08.01.2010    source источник
comment
Вы тестировали это с полноразмерными растровыми изображениями, которые собираетесь использовать в продакшене? Это оказалось медленным? Вы профилировали свой код на рабочих растровых изображениях, чтобы определить, где он замедляется?   -  person    schedule 09.01.2010
comment
Да, проблема в том, что наихудший случай - сканирование обоих растровых изображений и определение их идентичности - также является наиболее частым случаем.   -  person Erik Forbes    schedule 09.01.2010
comment
Определите идентичные в контексте растровых изображений. Вы имеете в виду идентичные двоичные файлы на основе файлов (оба файла .png, и их содержимое идентично)? Вы имеете в виду идентичные пиксели (могут быть разные форматы файлов, но пиксели одинаковые)? Вы имеете в виду идентичность восприятия (немного разные красные каналы - это нормально, поскольку глаза людей в любом случае не так хороши, чтобы различать различия в красном канале?   -  person Lasse V. Karlsen    schedule 09.01.2010
comment
просто небольшая подсказка. Мы (в колледже) использовали одномерное распределение для изображения с использованием указателей (в C) с одним malloc вместо нескольких распределений, когда изображение было представлено как 2D.   -  person ram    schedule 09.01.2010
comment
Я имею в виду абсолютно идентичные пиксели.   -  person Erik Forbes    schedule 09.01.2010
comment
Спасибо за научные исследования с вашими тестовыми примерами.   -  person Factor Mystic    schedule 12.10.2015
comment
См. Мой ответ в stackoverflow.com/questions/ 43289 / немного превосходит memcmp.   -  person Mr Anderson    schedule 24.06.2016


Ответы (9)


Отредактируйте 8-31-12: согласно приведенному ниже комментарию Джои, помните о формате сравниваемых растровых изображений. Они могут содержать отступы на шагах, которые делают растровые изображения неравными, несмотря на то, что они эквивалентны по пикселям. Дополнительные сведения см. В этом вопросе.


Чтение этого ответа на вопрос о сравнении байтовых массивов дало НАМНОГО БЫСТРЕЕ метод: использование P / Invoke и вызова API memcmp в msvcrt. Вот код:

[DllImport("msvcrt.dll")]
private static extern int memcmp(IntPtr b1, IntPtr b2, long count);

public static bool CompareMemCmp(Bitmap b1, Bitmap b2)
{
    if ((b1 == null) != (b2 == null)) return false;
    if (b1.Size != b2.Size) return false;

    var bd1 = b1.LockBits(new Rectangle(new Point(0, 0), b1.Size), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
    var bd2 = b2.LockBits(new Rectangle(new Point(0, 0), b2.Size), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

    try
    {
        IntPtr bd1scan0 = bd1.Scan0;
        IntPtr bd2scan0 = bd2.Scan0;

        int stride = bd1.Stride;
        int len = stride * b1.Height;

        return memcmp(bd1scan0, bd2scan0, len) == 0;
    }
    finally
    {
        b1.UnlockBits(bd1);
        b2.UnlockBits(bd2);
    }
}
person Erik Forbes    schedule 10.01.2010
comment
Ах, очень интересно. Я не рассматривал эту возможность - хотя на практике для моего приложения это не проблема, поскольку рассматриваемые растровые изображения отформатированы Format32bppRgb. Спасибо за подсказку! знак равно - person Erik Forbes; 31.08.2012
comment
Мне пришло в голову, что еще один способ справиться с проблемой заполнения шага - это вызвать memcmp построчно и установить «count» равным количеству байтов в строке за вычетом заполнения. Это может занять немного больше времени, но уменьшит количество ложных промахов из-за несоответствий заполнения. - person Erik Forbes; 29.07.2013
comment
Вопрос в том, какие накладные расходы несет в этом случае вызов P / Invoke на линию. - person Joey; 29.07.2013
comment
Я провел несколько тестов времени и обнаружил, что при сравнении растровых изображений приведенный выше пример медленнее в 100 раз по сравнению с прямым сравнением каждого пикселя с использованием цикла for и b1.GetPixel(x, y) == b2.GetPixel(x, y). Я делаю небольшие сравнения растровых изображений (около 100 x 10 пикселей), поэтому результат может измениться при сравнении с более крупными растровыми изображениями. - person BenR; 05.09.2014
comment
Обратите внимание, что последний параметр memcmp - это IntPtr, а не long, потому что, если программа работает на 32-битном уровне, это 32-битный, а при 64-битном - 64-битный. Очевидно, тогда вам нужно return memcmp(bd1scan0, bd2scan0, (IntPtr)len) == 0; - person xanatos; 23.07.2015

Если вы пытаетесь определить, равны ли они на 100%, вы можете инвертировать один и добавить его к другому, если они равны нулю. Расширяя это с помощью небезопасного кода, возьмите 64 бита за раз и выполняйте математические вычисления таким образом, любые различия могут вызвать немедленный сбой.

Если изображения не идентичны на 100% (сравнивая png и jpeg), или если вы не ищете 100% совпадения, вам предстоит еще поработать.

Удачи.

person GrayWizardx    schedule 08.01.2010
comment
Я ищу 100% идентичный, чтобы этот метод мог работать. Спасибо =) - person Erik Forbes; 09.01.2010
comment
Разве не добавление одного пикселя к инверсии другого и не проверка, является ли результат нулевым эквивалентом или медленнее, чем сравнение двух пикселей и проверка, совпадают ли они? Или я что-то упускаю? - person Skilldrick; 09.01.2010
comment
Он использует арифметику указателей (т. Е. Небезопасный код), поэтому он может рассматривать указатели как массив типов фиксированного размера (т. Е. Long) и выполнять чисто аппаратную математику. - person GrayWizardx; 09.01.2010
comment
Кроме того, насколько я помню, проверка на ноль работает лучше, чем проверка на любую другую константу. - person Erik Forbes; 09.01.2010
comment
Вы не можете перевернуть одно и вычесть его из другого. Это было бы как - (-b). Если a == b, то это будет равно a * 2. У меня нет проблем с пониманием того, что вы имеете в виду, но вы должны понять метод правильно. - person Lasse V. Karlsen; 09.01.2010
comment
@Lasse, да! Спасибо что подметил это. Я поправил. - person GrayWizardx; 09.01.2010
comment
Я думаю, что инверсия все еще в порядке, при условии, что все сделано с 8 битами. - person ram; 09.01.2010
comment
32-битная версия работает и является самой быстрой в этом контексте, поэтому я отметил это как ответ. Я скоро опубликую результаты тестов. - person Erik Forbes; 09.01.2010
comment
Между прочим - соответствующее сравнение: if ((~cursor1[0] & cursor2[0]) != 0x00) return false; - инвертировать одно, И его в другое, и проверить результат. В качестве альтернативы я мог бы выполнить XOR их вместе без инвертирования и сравнить этот результат с нулем. В любом случае кажется, что преимущество этого метода перед сравнением на равенство состоит в том, что сравнение с нулем происходит быстрее, чем сравнение с любой другой константой. - person Erik Forbes; 09.01.2010

Что ж, вы используете .LockBits, поэтому, по-видимому, вы используете небезопасный код. Вместо того, чтобы рассматривать начало каждой строки (Scan0 + y * Stride) как byte*, подумайте о том, чтобы рассматривать его как int*; int арифметика выполняется довольно быстро, и вам нужно выполнить только 1/4 объема работы. А для изображений в ARGB вы все равно можете использовать пиксели, упрощая математику.

person Marc Gravell    schedule 08.01.2010
comment
Я буду использовать изображения ARGB, так что похоже, что это может быть победителем. Я попробую и сделаю профилирование. Ну, еще немного профилирования. - person Erik Forbes; 09.01.2010

Не могли бы вы взять хэш каждого и сравнить? Это было бы немного вероятностным, но практически нет.

Благодаря Раму, вот образец реализации этого метода.

person Skilldrick    schedule 08.01.2010
comment
Он не подведет быстро, но он работает лучше, если вам нужно сравнить одно изображение с несколькими кандидатами ... - person EricLaw; 09.01.2010
comment
Вы можете сделать гибрид и протестировать случайную выборку, скажем, из 2% пикселей, чтобы убедиться, что они одинаковы, и если да, перейти к хешированию ... - person Skilldrick; 09.01.2010
comment
+1 за упоминание хеша. Вот пример реализации codeproject.com/KB/GDI-plus/comparingimages.aspx - person ram; 09.01.2010
comment
@Ram - Спасибо, отличный пример! - person Skilldrick; 09.01.2010

Если исходная проблема состоит в том, чтобы просто найти точные дубликаты среди двух растровых изображений, тогда нужно будет просто сравнить битовый уровень. Я не знаю C #, но в C я бы использовал следующую функцию:

int areEqual (long size, long *a, long *b)
{
    long start = size / 2;
    long i;
    for (i = start; i != size; i++) { if (a[i] != b[i]) return 0 }
    for (i = 0; i != start; i++) { if (a[i] != b[i]) return 0 }
    return 1;
}

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

Если вы пытаетесь найти точные дубликаты среди сотен изображений, сравнивать все пары из них нет необходимости. Сначала вычислите хэш MD5 каждого изображения и поместите его в список пар (md5Hash, imageId); затем отсортируйте список по m5Hash. Затем выполняйте попарные сравнения только тех изображений, которые имеют одинаковый md5Hash.

person Jeff Kubina    schedule 09.01.2010

Если эти растровые изображения уже есть на вашей видеокарте, вы можете распараллелить такую ​​проверку, выполнив ее на видеокарте, используя такой язык, как CUDA или OpenCL.

Я объясню в терминах CUDA, так как это тот, который я знаю. По сути, CUDA позволяет вам писать код общего назначения для параллельной работы на каждом узле вашей видеокарты. Вы можете получить доступ к растровым изображениям, находящимся в общей памяти. Каждому вызову функции также присваивается индекс в наборе параллельных запусков. Итак, для такой проблемы вы просто запустите одну из приведенных выше функций сравнения для некоторого подмножества растрового изображения, используя распараллеливание для покрытия всего растрового изображения. Затем просто запишите 1 в определенную ячейку памяти, если сравнение не удастся (и ничего не запишите, если оно удастся).

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

Вот некоторый (довольно плохой) пример кода (прошло немного времени с тех пор, как я программировал CUDA). Есть более эффективные способы доступа к растровым изображениям, которые уже загружены как текстуры, но я не стал здесь беспокоиться.

// kernel to run on GPU, once per thread
__global__ void compare_bitmaps(long const * const A, long const * const B, char * const retValue, size_t const len)
{
 // divide the work equally among the threads (each thread is in a block, each block is in a grid)
 size_t const threads_per_block = blockDim.x * blockDim.y * blockDim.z;
 size_t const len_to_compare = len / (gridDim.x * gridDim.y * gridDim.z * threads_per_block);
# define offset3(idx3,dim3)  (idx3.x + dim3.x * (idx3.y + dim3.y * idx3.z))
 size_t const start_offset = len_to_compare * (offset3(threadIdx,blockDim) + threads_per_block * offset3(blockIdx,gridDim));
 size_t const stop_offset = start_offset + len_to_compare;
# undef offset3

 size_t i;
 for (i = start_offset; i < stop_offset; i++)
 {
  if (A[i] != B[i]) 
  {
   *retValue = 1;
   break;
  }
 }
 return;
}
person rampion    schedule 10.01.2010

Если вы можете реализовать что-то вроде устройства Даффа на своем языке, это может дать вам значительный увеличение скорости по простому циклу. Обычно он используется для копирования данных, но нет причин, по которым его нельзя использовать для сравнения данных.

Или, если на то пошло, вы можете просто захотеть использовать какой-нибудь эквивалент memcmp ().

person rmeador    schedule 08.01.2010
comment
Вы можете развернуть цикл практически на любом языке (где это применимо). Вы можете не получить такого красивого и компактного синтаксиса, как у устройства Даффа, но производительность будет аналогичной. - person R. Martinho Fernandes; 09.01.2010

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

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

person Drew    schedule 08.01.2010

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

public static class Utils
{
    public static byte[] ShaHash(this Image image)
    {
        var bytes = new byte[1];
        bytes = (byte[])(new ImageConverter()).ConvertTo(image, bytes.GetType());

        return (new SHA256Managed()).ComputeHash(bytes);
    }

    public static bool AreEqual(Image imageA, Image imageB)
    {
        if (imageA.Width != imageB.Width) return false;
        if (imageA.Height != imageB.Height) return false;

        var hashA = imageA.ShaHash();
        var hashB = imageB.ShaHash();

        return !hashA
            .Where((nextByte, index) => nextByte != hashB[index])
            .Any();
    }
]

Использование простое:

bool isMatch = Utils.AreEqual(bitmapOne, bitmapTwo);
person nathanchere    schedule 01.03.2014
comment
Ваше сравнение всегда будет верным для изображений одного размера. hashB = imageA.ShaHash () должно быть imageB. Опечатка? - person Wbmstrmjb; 21.11.2014