Более быстрая обработка изображений, чем биты блокировки

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

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

Вот базовый код, который определяет, является ли пиксель краем. Он принимает Color[] из девяти цветов, первый из которых — это проверяемый пиксель.

public Boolean isEdgeOptimized(Color[] colors)
{
    //colors[0] should be the checking pixel
    Boolean returnBool = true;
    float percentage = percentageInt; //the percentage used is set
    //equal to the global variable percentageInt

    if (isMatching(colors[0], colors[1], percentage) &&
            isMatching(colors[0], colors[2], percentage) &&
            isMatching(colors[0], colors[3], percentage) &&
            isMatching(colors[0], colors[4], percentage) &&
            isMatching(colors[0], colors[5], percentage) &&
            isMatching(colors[0], colors[6], percentage) &&
            isMatching(colors[0], colors[7], percentage) &&
            isMatching(colors[0], colors[8], percentage))
    {
        returnBool = false;
    }
    return returnBool;
}

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

Итак, в основном, вопрос в том, как я могу заставить свою программу работать быстрее? Это мой алгоритм, или я могу использовать что-то более быстрое, чем lockBits?

Кстати, проект есть на gitHub, здесь


person vkoves    schedule 18.04.2013    source источник
comment
И... в чем вопрос?   -  person LightStriker    schedule 18.04.2013
comment
Только что отредактировал, извините за неясность.   -  person vkoves    schedule 18.04.2013
comment
Пожалуйста, опубликуйте свой код. Мы не хотим перемещаться по вашему проекту на Github, чтобы найти соответствующую часть.   -  person leonbloy    schedule 18.04.2013
comment
Вопрос не в том, есть ли что-то быстрее, чем lockBits? LockBits практически необходим для получения низкоуровневого доступа к изображению и управления пикселями как элементами массива.   -  person leonbloy    schedule 18.04.2013
comment
LockBits дает вам массив байтов, к которому вы можете получить прямой доступ. Маловероятно, что вы найдете более быстрый способ доступа к данным. Если вы хотите, чтобы кто-нибудь посмотрел на ваш алгоритм, либо опубликуйте соответствующие части здесь, либо, по крайней мере, сообщите нам, какой файл в этом проекте GitHub вы хотите, чтобы мы просмотрели. Не заставляйте нас просматривать десятки ненужных файлов, чтобы найти то, о чем вы спрашиваете.   -  person Jim Mischel    schedule 18.04.2013
comment
Небольшая вещь, вы делаете много переоценки индексов пикселей в своем коде, которые можно улучшить, отслеживая фокусный (центральный) пиксель и используя предварительно рассчитанные смещения для других 8 вокруг него. (см. DualImageForm.cs, строка 330) — Роберт Рухани, однако, делает гораздо более важный вывод, который следует рассмотреть в первую очередь (вы не используете заблокированные данные для редактирования растрового изображения, что должно быть).   -  person VisualMelon    schedule 18.04.2013
comment
Образец кода добавлен, и спасибо за отзыв.   -  person vkoves    schedule 18.04.2013
comment
Вы должны использовать профилировщик, чтобы выяснить, какая часть вашего алгоритма медленная, а затем оптимизировать эту часть.   -  person Pete Baughman    schedule 18.04.2013


Ответы (4)


Вы действительно передаете число с плавающей запятой в процентах isMatching?

Я посмотрел ваш код для isMatching на GitHub и, ну, да. Вы портировали это с Java, верно? В C# используется bool, а не Boolean, и хотя я не уверен в этом, мне не нравится внешний вид кода, который так много упаковывает и распаковывает. Кроме того, вы выполняете массу умножения и сравнения с плавающей запятой, когда вам это не нужно:

public static bool IsMatching(Color a, Color b, int percent)
{
    //this method is used to identify whether two pixels, 
    //of color a and b match, as in they can be considered
    //a solid color based on the acceptance value (percent)

    int thresh = (int)(percent * 255);

    return Math.Abs(a.R - b.R) < thresh &&
           Math.Abs(a.G - b.G) < thresh &&
           Math.Abs(a.B - b.B) < thresh;
}

Это сократит объем работы, которую вы выполняете на пиксель. Мне все еще это не нравится, потому что я стараюсь избегать вызовов методов в середине цикла для каждого пикселя, особенно в цикле 8x для каждого пикселя. Я сделал метод статическим, чтобы сократить количество передаваемых экземпляров, которые не используются. Одни только эти изменения, вероятно, удвоят вашу производительность, так как мы делаем только 1 умножение, без упаковки, и теперь используем присущее && короткое замыкание, чтобы сократить работу.

Если бы я делал это, я бы, скорее всего, сделал что-то вроде этого:

// assert: bitmap.Height > 2 && bitmap.Width > 2
BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height),
                      ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

int scaledPercent = percent * 255;
unsafe {
    byte* prevLine = (byte*)data.Scan0;
    byte* currLine = prevLine + data.Stride;
    byte* nextLine = currLine + data.Stride;

    for (int y=1; y < bitmap.Height - 1; y++) {

       byte* pp = prevLine + 3;
       byte* cp = currLine + 3;
       byte* np = nextLine + 3;
       for (int x = 1; x < bitmap.Width - 1; x++) {
           if (IsEdgeOptimized(pp, cp, np, scaledPercent))
           {
               // do what you need to do
           }
           pp += 3; cp += 3; np += 3;
       }
       prevLine = currLine;
       currLine = nextLine;
       nextLine += data.Stride;
    }
}

private unsafe static bool IsEdgeOptimized(byte* pp, byte* cp, byte* np, int scaledPecent)
{
    return IsMatching(cp, pp - 3, scaledPercent) &&
           IsMatching(cp, pp, scaledPercent) &&
           IsMatching(cp, pp + 3, scaledPercent) &&
           IsMatching(cp, cp - 3, scaledPercent) &&
           IsMatching(cp, cp + 3, scaledPercent) &&
           IsMatching(cp, np - 3, scaledPercent) &&
           IsMatching(cp, np, scaledPercent) &&
           IsMatching(cp, np + 3, scaledPercent);
}

private unsafe static bool IsMatching(byte* p1, byte* p2, int thresh)
{
    return Math.Abs(p1++ - p2++) < thresh &&
           Math.Abs(p1++ - p2++) < thresh &&
           Math.Abs(p1 - p2) < thresh;
}

Что теперь делает все виды ужасных искажений указателя, чтобы сократить доступ к массиву и так далее. Если вся эта работа с указателями вызывает у вас дискомфорт, вы можете выделить массивы байтов для prevLine, currLine и nextLine и выполнить Marshal.Copy для каждой строки по мере продвижения.

Алгоритм таков: начните с одного пикселя сверху и слева и перебирайте каждый пиксель изображения, кроме внешнего края (без граничных условий! Ура!). Я сохраняю указатели на начало каждой строки, prevLine, currLine и nextLine. Затем, когда я запускаю цикл x, я составляю pp, cp, np, которые являются предыдущим пикселем, текущим пикселем и следующим пикселем. текущий пиксель действительно тот, о котором мы заботимся. pp — пиксель прямо над ним, np — прямо под ним. Я передаю их в IsEdgeOptimized, который просматривает cp, вызывая IsMatching для каждого.

Теперь все это предполагает 24 бита на пиксель. Если вы смотрите на 32 бита на пиксель, все эти магические 3 должны быть 4, но в остальном код не меняется. Вы можете параметризовать количество байтов на пиксель, если хотите, чтобы он мог обрабатывать и то, и другое.

К вашему сведению, каналы в пикселях обычно имеют вид b, g, r, (a).

Цвета хранятся в виде байтов в памяти. Ваше фактическое растровое изображение, если это 24-битное изображение, хранится в виде блока байтов. Строки развертки имеют ширину data.Stride байт, что как минимум равно 3 * количеству пикселей в строке (может быть больше, поскольку строки развертки часто дополняются).

Когда я объявляю переменную типа byte * в C#, я делаю несколько вещей. Во-первых, я говорю, что эта переменная содержит адрес расположения байта в памяти. Во-вторых, я говорю, что собираюсь нарушить все меры безопасности в .NET, потому что теперь я могу читать и записывать любой байт в памяти, что может быть опасно.

Итак, когда у меня есть что-то вроде:

Math.Abs(*p1++ - *p2++) < thresh

Что он говорит (и это будет долго):

  1. Возьмите байт, на который указывает p1, и удерживайте его.
  2. Добавьте 1 к p1 (это ++ - указатель указывает на следующий байт)
  3. Возьмите байт, на который указывает p2, и удерживайте его.
  4. Добавить 1 к p2
  5. Вычесть шаг 3 из шага 1
  6. Передайте это Math.Abs.

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

Когда мы вводим IsMatching, p1 указывает на пиксель 1, p2 указывает на пиксель 2 и в памяти они располагаются так:

p1    : B
p1 + 1: G
p1 + 2: R

p2    : B
p2 + 1: G
p2 + 2: R

Таким образом, IsMatching просто вычисляет абсолютную разницу при обходе памяти.

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

Например, вы заметите, что я смотрю с первой строки на предпоследнюю строку и с первого столбца на предпоследний столбец. Это делается намеренно, чтобы избежать обработки случая «Я не могу прочитать выше 0-й строки», что устраняет большой класс потенциальных ошибок, которые могут включать чтение за пределы допустимого блока памяти, что может быть безопасным во многих условиях выполнения.

person plinth    schedule 18.04.2013
comment
Можете ли вы объяснить, что означает каждая из ваших переменных, а также использование звездочек? Вы каким-то образом сохраняете цвета в виде байтов? - person vkoves; 19.04.2013
comment
указатели - это определенно способ обработки изображений в С#, однако я бы не стал вызывать функции во вложенном цикле. это неэффективно в С#. в целом я обнаружил, что обработка изображений в С# занимает как минимум вдвое больше времени, чем в эквивалентной функции С++. если вам действительно нужна эффективность, я бы посоветовал либо вызвать ваши собственные функции С++ из С#, либо использовать одну из оболочек С# для opencv - это очень быстро. - person morishuz; 19.04.2013
comment
Ваше заявление isMatching не имеет для меня смысла. Math.Abs(*p1++ - *p2++) ‹ thresh и Math.Abs(*p1++ - *p2++) ‹ thresh — это одно и то же, так почему вы используете его дважды? Должно ли это быть Math.Abs(*p1+2 - *p2+2) ‹ thresh? - person vkoves; 19.04.2013
comment
потому что ++ побочный эффект указателей. Фактические значения указателя отличаются после выполнения ++. Это как Math.Abs(arr1[i++] - arr[j++]) ‹ thresh. - person plinth; 20.04.2013

Вместо того, чтобы копировать каждое изображение в byte[], затем копировать в Color[], создавать еще один временный Color[9] для каждого пикселя, а затем использовать SetPixel для установки цвета, скомпилировать с использованием флага /unsafe, пометить метод как небезопасный, заменить копирование в byte[] на Marshal.Copy кому:

using (byte* bytePtr = ptr)
{
    //code goes here
}

Убедитесь, что вы заменили вызов SetPixel установкой правильных байтов. Это не проблема с LockBits, вам нужны LockBits, проблема в том, что вы неэффективны во всем остальном, связанном с обработкой изображения.

person Robert Rouhani    schedule 18.04.2013
comment
Можете ли вы объяснить свою реализацию более подробно? Я точно не знаю, как я могу изменить свой код, чтобы использовать то, что вы показываете. - person vkoves; 19.04.2013
comment
Вы используете Marshal.Copy для копирования всего изображения в отдельно выделенный byte[], затем копируете byte[] в другой, отдельно выделенный Color[], используя в 3 раза больше памяти исходного изображения и занимая много времени копирование. Вместо этого вы должны преобразовать IntPtr в byte* и напрямую манипулировать указателем. Это оставляет вас без копий изображения и является самым быстрым способом доступа к данным изображения. - person Robert Rouhani; 19.04.2013
comment
Это имеет смысл, но не могли бы вы объяснить, что такое байт*? Является ли звездочка просто специальным модификатором? - person vkoves; 19.04.2013
comment
@vkoves: SetPixel — это огромная трата времени. Если вы делаете их много, это, вероятно, ваша самая большая проблема с производительностью. byte* — это указатель. Вам необходимо понимать небезопасный код и указатели: msdn.microsoft.com/en -us/library/vstudio/t2yzs44b.aspx - person Jim Mischel; 19.04.2013
comment
Спасибо, это многое объясняет. - person vkoves; 19.04.2013

Если вы хотите использовать параллельное выполнение задач, вы можете использовать класс Parallel в пространстве имен System.Threading.Tasks. По следующей ссылке есть несколько примеров и пояснений.

person turgay    schedule 03.09.2014

Вы можете разделить изображение на 10 растровых изображений и обработать каждое из них, а затем, наконец, объединить их (просто идея).

person Ayman Benyahia    schedule 08.04.2018
comment
Когда вы говорите «разделить изображение», как бы вы это сделали? - person d219; 08.04.2018