Эффективная память для строчки в очень больших изображениях - PullRequest
12 голосов
/ 29 января 2011

Фон

Я работаю с очень большими наборами данных со спутников радаров с синтезированной апертурой.Их можно рассматривать как полутоновые изображения с высоким динамическим диапазоном порядка 10 тыс. Пикселей на стороне.

В последнее время я разрабатываю приложения одномасштабного варианта гребня пространства масштаба Линдеберга.алгоритм обнаружения для обнаружения линейных элементов в изображении SAR.Это улучшение использования направленных фильтров или использования преобразования Хафа, методов, которые использовались ранее, поскольку он менее затратен в вычислительном отношении, чем любой из них.(Я представлю некоторые недавние результаты на JURSE 2011 в апреле, и я могу загрузить препринт, если это будет полезно).

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

struct ridge_t { unsigned char top, left, bottom, right };
int rows, cols;
struct ridge_t *ridges;  /* An array of rows*cols ridge entries */

Запись в ridges содержит сегмент ребра, если ровно два из top, left, right и bottom имеют значения в диапазоне 0 - 128. Предположим, у меня есть:

ridge_t entry;
entry.top = 25; entry.left = 255; entry.bottom = 255; entry.right = 76;

Затем я могу найти начало сегмента гребня (x1, y1) иend (x2, y2):

float x1, y1, x2, y2;
x1 = (float) col + (float) entry.top / 128.0;
y1 = (float) row;
x2 = (float) col + 1;
y2 = (float) row + (float) entry.right / 128.0;

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

Rendered ridge segments

Каждая из этих длинных кривых визуализируется из серии крошечных сегментов хребта.

Тривиально определить, связаны ли два соседних местоположения, которые содержат сегменты хребта.Если у меня ridge1 в (x, y) и ridge2 в (x + 1, y), то они являются частями одной строки, если 0 <= <code>ridge1.right <= 128 <em>и ridge2.left = ridge1.right.

Задача

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

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

Что читатели предлагают в качестве возможного подхода?Похоже, что-то такое, что кто-то мог бы найти эффективный способ сделать в прошлом ...

Ответы [ 4 ]

4 голосов
/ 30 января 2011

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

Алгоритм непересекающегося множества без блокировки

Я предполагаю наличие сравнения размером с два указателя-и-своп операции на ваш выбор архитектуры процессора.Это доступно как минимум для архитектур x86 и x64.

Алгоритм в основном такой же, как описан на странице Википедии для однопоточного регистра , с некоторыми изменениями для безопасной работы без блокировки.Во-первых, мы требуем, чтобы элементы rank и родительские элементы имели размер указателя и были выровнены в 2 * sizeof (указатель) в памяти, для атомарного CAS позже.

Find () не нужно изменять;в худшем случае оптимизация сжатия пути не будет иметь полного эффекта в присутствии одновременно пишущих.

Union (), однако, должен измениться:

function Union(x, y)
  redo:
    x = Find(x)
    y = Find(y)
    if x == y
        return
    xSnap = AtomicRead(x) -- read both rank and pointer atomically
    ySnap = AtomicRead(y) -- this operation may be done using a CAS
    if (xSnap.parent != x || ySnap.parent != y)
        goto redo
    -- Ensure x has lower rank (meaning y will be the new root)
    if (xSnap.rank > ySnap.rank)
        swap(xSnap, ySnap)
        swap(x, y)
    -- if same rank, use pointer value as a fallback sort
    else if (xSnap.rank == ySnap.rank && x > y)
        swap(xSnap, ySnap)
        swap(x, y)
    yNew = ySnap
    yNew.rank = max(yNew.rank, xSnap.rank + 1)
    xNew = xSnap
    xNew.parent = y
    if (!CAS(y, ySnap, yNew))
      goto redo
    if (!CAS(x, xSnap, xNew))
      goto redo
    return

Это должно быть безопасно вчто он никогда не будет образовывать петли и всегда приведет к правильному объединению.Мы можем подтвердить это, наблюдая, что:

  • Во-первых, до завершения один из двух корней всегда будет заканчиваться родительским указателем, указывающим на другого.Следовательно, до тех пор, пока петли нет, слияние будет успешным.
  • Во-вторых, ранг всегда увеличивается.После сравнения порядка x и y мы знаем, что x имеет более низкий ранг, чем y во время моментального снимка.Для формирования цикла другой поток должен был бы сначала увеличить ранг x, а затем объединить x и y.Однако в CAS, который записывает родительский указатель x, мы проверяем, что ранг не изменился;следовательно, ранг y должен оставаться больше x.

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

Следовательно, не должно быть никаких шансов на формирование петель, и этот алгоритм без блокировок дизъюнкт-множества должен быть безопасным.

А теперь перейдем к приложению к вашей проблеме...

Допущения

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

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

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

Этап 1: Идентификация локальной линии

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

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

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

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

Обратите внимание, что поскольку непересекающиеся множества еще не разделены между потоками, нет необходимости использовать операции сравнения и замены; просто используйте обычный однопоточный алгоритм объединения-слияния. Поскольку мы не освобождаем ни одну из структур непересекающихся множеств до тех пор, пока алгоритм не завершится, выделение может быть также выполнено с помощью бампера-распределителя для каждого потока, что делает выделение памяти (практически) без блокировки и O (1).

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

Поскольку перебор всего изображения равен O (x * y), а disjoint-merge фактически равен O (1), эта операция - O (x * y) и требует рабочей памяти O (m + 2 * x * y). / k + k ^ 2) = O (x * y / k + k ^ 2), где t - количество секторов, k - ширина сектора, а m - количество частичных отрезков в секторе ( в зависимости от того, как часто линии пересекают границы, m может значительно отличаться, но оно никогда не будет превышать количество отрезков линии). Память перенесена на следующую операцию: O (m + 2 * x * y / k) = O (x * y / k)

Фаза 2: Межсекторное слияние

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

Эта операция имеет время выполнения O (x + y) и использует O (1) памяти (однако мы должны сохранить память из фазы 1). По окончании краевые массивы могут быть отброшены.

Фаза 3: Сбор линий

Теперь мы выполняем операцию многопоточного отображения над всеми выделенными объектами структуры с непересекающимся множеством. Сначала мы пропускаем любой объект, который не является корнем (т. Е. Где obj.parent! = Obj). Затем, начиная с репрезентативного отрезка, мы оттуда выезжаем, собираем и записываем любую необходимую информацию о рассматриваемой линии. Мы уверены, что только один поток просматривает любую строку за раз, поскольку пересекающиеся линии оказались бы в одной структуре с непересекающимися множествами.

Это время выполнения O (m) и использование памяти в зависимости от того, какую информацию вам нужно собрать об этих отрезках.

Резюме

В целом, этот алгоритм должен иметь время работы O (x * y) и использование памяти O (x * y / k + k ^ 2). Регулировка k дает компромисс между использованием переходной памяти на процессах фазы 1 и долгосрочным использованием памяти для массивов смежности и структур с непересекающимся множеством, перенесенных на фазу 2.

Обратите внимание, что я на самом деле не тестировал производительность этого алгоритма в реальном мире; также возможно, что я упустил из виду проблемы параллелизма в алгоритме объединения-объединения без блокирования дизъюнкт-множества выше. Комментарии приветствуются:)

2 голосов
/ 29 января 2011

Вы можете использовать не обобщенную форму преобразования Hough .Похоже, что он достигает впечатляющей O (N) временной сложности на N x N массивах мешей (если у вас есть доступ к ~ 10000x10000 SIMD массивам, и ваш меш равен N x N - примечание: в вашем случае N будетструктура гребня, или группа A x B гребней, НЕ пиксель). Нажмите для источника .Более консервативные (неядерные) решения перечисляют сложность как O (kN ^ 2), где k = [-π/2, π]. Источник .

Тем не менее, преобразование Хафа действительно имеет некоторые требования к объему памяти, и сложность пространства будет O (кН), но если вы предварительно вычислите sin() и cos() ипредоставьте подходящие таблицы поиска, все сводится к O (k + N), который может все еще быть слишком большим, в зависимости от того, насколько велик ваш N ... но я не вижу, чтобы вы понизили его.

Редактировать : Проблема элементов cross-thread / kernel / SIMD / line line нетривиальна.Мой первый импульс подсказывает мне подразделить сетку на рекурсивные квадр-деревья (в зависимости от определенного допуска), проверять непосредственные ребра и игнорировать все структуры ребер ребер (вы можете пометить их как «потенциальные длинные линии» и использовать их во всей распределенной системе).);просто делайте работу над всем, что внутри этого конкретного четырехугольника, и постепенно продвигайтесь наружу.Вот графическое представление (зеленый - первый проход, красный - второй и т. Д.).Тем не менее, моя интуиция говорит мне, что это вычислительно дорого.

Passes

0 голосов
/ 29 января 2011

Хорошо, подумав об этом немного дольше, я получил предложение, которое кажется слишком простым, чтобы быть эффективным ... Буду признателен за отзыв о том, кажется ли это разумным!

1) Поскольку я могу легко определить, подключен ли каждый ridge_t сегмент гребня к нулю, к одному или двум соседним сегментам, я мог бы покрасить каждый из них соответствующим образом (LINE_NONE, LINE_END или LINE_MID).Это можно легко сделать параллельно, так как нет шансов на состояние гонки.

2) После завершения окраски:

for each `LINE_END` ridge segment X found:
    traverse line until another `LINE_END` ridge segment Y found
    if X is earlier in memory than Y:
        change X to `LINE_START`
    else:
        change Y to `LINE_START`

Это также не содержит условий гонки, так как дажеесли два потока одновременно пересекают одну и ту же строку, они внесут одно и то же изменение.

3) Теперь у каждой строки на изображении будет ровно один конец, помеченный как LINE_START.Строки могут быть расположены и упакованы в более удобную структуру в одном потоке, без необходимости каких-либо поисков, чтобы увидеть, была ли линия уже посещена.

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

Есть ли какие-либо подводные камни, которые я пропустил?

Редактировать: Очевидная проблема заключается в том, что я в конечном итоге хожу по строкам дважды, один раз, чтобы найти RIDGE_START s, а затем - окончательную переупаковку, что приводит к неэффективности вычислений.Тем не менее, он по-прежнему равен O (N) с точки зрения времени хранения и вычислений, что является хорошим признаком ...

0 голосов
/ 29 января 2011

Если гребни разрешены достаточно, чтобы разрывы составляли всего несколько пикселей, тогда стандартное расширение - поиск соседей - стирает шаги, которые вы бы сделали для нахождения линий / OCR должен работать.

Объединение более длинных контуров из многих сегментов и знание, когда создать шею или когда сделать отдельный остров, намного сложнее

...