Маркировка связанных областей, поиск
Apr. 21st, 2013 06:56 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Итак, алгоритм китайцев заработал и показал вполне неплохие результаты (но об этом — в следующей записи). Здесь же я просто скопирую то, что написал за время пребывания в НН и внуковском аэропорту.
Вернемся к тому же общему случаю: искомый пиксель нахдится в байте E.
Фильтрация 4-связанных областей схожа с операцией диляции за исключением собственно логики - в отсечении пикселей, не являющихся 4-связанными, необходимо провести операцию:
Соответственно, для крайних пикселей (крайние столбцы/строки) выражение для E4 будет видоизменяться (для крайних строк - наличием/отсутствием B,H; для крайних столбцов - наличием/отсутствием выражений с D, F).
Для упрощения записи можно было бы сделать макрос при помощи boost, но что-то лень мне в дебри буста погружаться, поэтому проблему решаю простым копипастом.
Кстати, здесь нет проверки A,C,G и K, иначе пришлось бы еще больше кода наворачивать (добавились бы особые случаи для углов изображения).
Общее выражение для поиска 8-связанных областей несколько видоизменится:
/* (сомнительно) Аналогично, заранее подготовив матрицы (x<<7) и (x>>7) можно
* попытаться ускорить поиск E4 */
Когда мы уже имеем отфильтрованное изображение, нужно "всего лишь" пробежаться по нему в поисках областей, со всех сторон ограниченных нулями. Грубо говоря, мы маркируем 8-связанные области без проверки на 8-связанность.
Первое, что приходит на ум - двухпроходная маркировка: сначала мы пробегаемся по связанной области, постепенно расширяя границы заключающего ее прямоугольника. Затем мы выделяем память для этого кусочка и заполняем его, пробегаясь по нашей области еще раз (ну или делаем полную копию содержимого бокса, а затем выкидываем все, что не принадлежит искомой связанной области).
Еще вариант - сразу выделять область памяти (вспомогательную) размером со ВСЕ первоначальное изображение (гениально!). В этом случае мы сможем сразу же копировать в эту область памяти нашу х-связанную область, а затем, когда она целиком будет скопирована и будут ясны параметры описывающего ее прямоугольника, выделяем уже память именно под эту область и копируем в нее содержимое бокса.
Логично было бы преобразовать картинку до анализа в бинарный вид, поэтому сначала будем предполагать, что мы имеем дело с бинарным изображением (а не "сжатым" битовым).
Теперь каждый пиксель нашей матрицы соответствует одному пикселю изображения.
На ум приходит использовать метод шагающих квадратов, однако, он применяется для обхода границ -> мало чем поможет (разве что первоначальным определением размеров бокса, но я уже решил делать лишь один проход обнаружения связанных областей). Интернета под рукой нет, чтобы подсмотреть наиболее эффективный способ, поэтому буду изобретать велосипед.
/*
Работаем с копией изображения, т.к. уже пройденные значения будем обнулять.
*/
Итак, выделим память под промежуточный буфер (сразу обнулив ее) и начнем проходить изображение с левого верхнего угла. Как только мы натыкаемся на единицу в E, записываем в соответствующие координаты буфера 1. Далее смотрим, куда нам нужно двигаться. Считаем, что в A,B,C и D нули (т.к. там - уже пройденная область). Нам необходимо проверить пиксели F, G, H и K. Теперь каждый из них становится E. Если кто-то из них установлен в 1, проверяем его соседей и т.д. Однако, на всех шагах, отличных от первого, мы уже обязаны проверить ВСЕХ СОСЕДЕЙ, т.к. не факт, что наша область не представляет собою какой-нибудь извращенной фигуры с отростками и многочисленными полостями.
На ум приходит рекурсия, однако, учитывая то, что изображения могут быть довольно-таки огромными, с горем расстаюсь с этой идеей. А так было бы хорошо: рекурсия сделала бы очень простым этот алгоритм. Но регистры не бесконечные.
Кстати, если не преобразовывать изображение в бинарное, то вполне возможны случаи, когда один и тот же байт будет проходиться вплоть до четырех раз!
После копирования каждого пикселя с 1 в результирующий буфер, "стираем" его с оригинала (сбросом в 0). По окончанию операций обнуляем буфер.
Тут мне опять пришла в голову идея о шагающих квадратах: а что, если в первый проход пробежаться по границе области, определить описывающий ее прямоугольник, выделить буфер по размеру прямоугольника (это - как раз искомое возвращаемое функцией значение); после этого, проходя по объекту, пиксель за пикселем копировать его в буфер, "стирая" с оригинала. Правда, здесь опять та же самая проблема: нет гарантии, что за один проход все будет сделано.
Принцип Гюйгенса прост: каждая точка волнового фронта (в нашем случае - точка внутри искомой области) является источником нового волнового фронта (в нашем случае - центром, от которого "расходится" поисковая волна).
Пробегаясь такой "волной" от первого обнаруженного пикселя, принадлежащего искомой фигуре, мы пройдем все ее закоулки. Однако, проблема в том, как реализовать это без бешеного количества разыменования указателей и проверок.
Долго кумекая, я так и не придумал, как реализовать "Гюйгенса", не вводя уйму промежуточных матриц.
Метод получился опять каким-то уж очень извращенным. Глаза уже слипаются, поэтому пока - отбой.
Процедура перемаркировки такова: проходя матрицу с маркерами, проверяем каждый пиксель. Если пиксель нулевой, устанавливаем флаг обнаружения в false и продолжаем. Если флаг == true, это значит, что данный номер мы уже перемаркировали - продолжаем. Если же пиксель - "свеженайденный", надо проверить все 6 пикселей (для 8-связных областей) в предыдущем и последующем рядах (снизу - т.к. возможен вариант коротенького кусочка, а то и единичного пикселя над длинной линией - из-за флага обнаружения мы пролетим под ним, а вот на этапе сканирования его, изучая то, что под ним, сделаем правильную ремаркировку). Вполне возможно, что нет необходимости проверять сразу и пиксель над, и под нашим: пиксель точно под ним по-любому будет проверен (он - либо часть более длинной линии, которую мы обнаружим в DL или DR углах, либо часть вертикальной, которая обнаружится при сканировании нижней линии).
А вообще, полезно рассмотреть, какие варианты расположения соседей могут быть, чтобы не пропустить мелочей (нумерация не важна, поэтому просто 1/0; кроме того, т.к вариантов аж 256, рассмотрим лишь некоторые): (X - что угодно, U - единица в одном из пикселей, V - единица в одном и более пикселей)
Итак, общие тенденции:
При организации процедуры переименования я натолкнулся на следующее: если ряд X переименовывается в нижележащий Y, а потом Y переименовывается в Z, то ряд X так и остается переименованным в Y. Следовательно, при переименовании в массиве ассоциаций на стадии перенумерования надо проверять, выполняется ли равенство assoc[assoc[X]] == assoc[X], т.е. зафиксировать сложные связи.
Оказалось не все так просто с переименованием: надо будет нарисовать на бумажке, что-то засыпаю уже. На сегодняшний день ничего не планирую (спать надо лечь рано, т.к. завтра в 5 утра вставать), так что, пожалуй, закруглюсь и буду спать. Сегодня еще вопросы с криостатом утрясти надо будет.
Пока засыпал, придумал, как правильно скорректировать алгоритм переименования: скажем, переименовываем мы пиксель с изначальной меткой "X", имеющий новую метку "Y" (т.е. assoc[X] != X) в "Z". В этом случае надо наоборот переименовать Z в Y, а величины, больше Z, но <= Ncur декрементировать, затем декрементировать и Ncur. Все довольно просто, а очевидным плюсом является то, что не надо многократно сканировать изображение для переименования "по месту": это будет сделано на шаге 3.
Конечно, все познается в сравнении, поэтому сразу обливать дерьмом этот метод нельзя: стоит попытаться его реализовать. А учитывая то, что все шаги, кроме второго, превосходно параллелизуются (1, 3 - хоть по строкам, хоть по прямоугольным областям, 4 - по номерам объектов), на куде, пожалуй, он будет работать просто превосходно!
Итак, если выбрать эффективный метод закрашивания областей, то обнаружение сведется к следующему:
Вернемся к тому же общему случаю: искомый пиксель нахдится в байте E.
ABC DEF GHK
Фильтрация 4-связанных областей схожа с операцией диляции за исключением собственно логики - в отсечении пикселей, не являющихся 4-связанными, необходимо провести операцию:
E4 = E & ( B|H|(E<<1)|(E>>1)|(D<<7)|(F>>7) )Т.е. мы оставляем пиксель в E лишь в том случае, если хотя бы один из его 4-связанных соседей присутствует.
Соответственно, для крайних пикселей (крайние столбцы/строки) выражение для E4 будет видоизменяться (для крайних строк - наличием/отсутствием B,H; для крайних столбцов - наличием/отсутствием выражений с D, F).
Для упрощения записи можно было бы сделать макрос при помощи boost, но что-то лень мне в дебри буста погружаться, поэтому проблему решаю простым копипастом.
Кстати, здесь нет проверки A,C,G и K, иначе пришлось бы еще больше кода наворачивать (добавились бы особые случаи для углов изображения).
Общее выражение для поиска 8-связанных областей несколько видоизменится:
пусть {x} = (x|(x<<1)|(x>>1)), тогда E8 = E & ( {B|H} | ((E<<1)|(E>>1)) | ((C|F|K)>>7) | ((A|D|G)<<7) )Вычислительная сложность сильно возрастает, причем многие операции вычисляются по два раза. Пожалуй, можно попытаться немного ускорить вычисления, если заранее подготовить матрицу {x} (и матрицы (x<<7) и (x>>7) ???).
/* (сомнительно) Аналогично, заранее подготовив матрицы (x<<7) и (x>>7) можно
* попытаться ускорить поиск E4 */
Когда мы уже имеем отфильтрованное изображение, нужно "всего лишь" пробежаться по нему в поисках областей, со всех сторон ограниченных нулями. Грубо говоря, мы маркируем 8-связанные области без проверки на 8-связанность.
Первое, что приходит на ум - двухпроходная маркировка: сначала мы пробегаемся по связанной области, постепенно расширяя границы заключающего ее прямоугольника. Затем мы выделяем память для этого кусочка и заполняем его, пробегаясь по нашей области еще раз (ну или делаем полную копию содержимого бокса, а затем выкидываем все, что не принадлежит искомой связанной области).
Еще вариант - сразу выделять область памяти (вспомогательную) размером со ВСЕ первоначальное изображение (гениально!). В этом случае мы сможем сразу же копировать в эту область памяти нашу х-связанную область, а затем, когда она целиком будет скопирована и будут ясны параметры описывающего ее прямоугольника, выделяем уже память именно под эту область и копируем в нее содержимое бокса.
Логично было бы преобразовать картинку до анализа в бинарный вид, поэтому сначала будем предполагать, что мы имеем дело с бинарным изображением (а не "сжатым" битовым).
Теперь каждый пиксель нашей матрицы соответствует одному пикселю изображения.
На ум приходит использовать метод шагающих квадратов, однако, он применяется для обхода границ -> мало чем поможет (разве что первоначальным определением размеров бокса, но я уже решил делать лишь один проход обнаружения связанных областей). Интернета под рукой нет, чтобы подсмотреть наиболее эффективный способ, поэтому буду изобретать велосипед.
Нулевое приближение
/*
Работаем с копией изображения, т.к. уже пройденные значения будем обнулять.
*/
Итак, выделим память под промежуточный буфер (сразу обнулив ее) и начнем проходить изображение с левого верхнего угла. Как только мы натыкаемся на единицу в E, записываем в соответствующие координаты буфера 1. Далее смотрим, куда нам нужно двигаться. Считаем, что в A,B,C и D нули (т.к. там - уже пройденная область). Нам необходимо проверить пиксели F, G, H и K. Теперь каждый из них становится E. Если кто-то из них установлен в 1, проверяем его соседей и т.д. Однако, на всех шагах, отличных от первого, мы уже обязаны проверить ВСЕХ СОСЕДЕЙ, т.к. не факт, что наша область не представляет собою какой-нибудь извращенной фигуры с отростками и многочисленными полостями.
На ум приходит рекурсия, однако, учитывая то, что изображения могут быть довольно-таки огромными, с горем расстаюсь с этой идеей. А так было бы хорошо: рекурсия сделала бы очень простым этот алгоритм. Но регистры не бесконечные.
Кстати, если не преобразовывать изображение в бинарное, то вполне возможны случаи, когда один и тот же байт будет проходиться вплоть до четырех раз!
После копирования каждого пикселя с 1 в результирующий буфер, "стираем" его с оригинала (сбросом в 0). По окончанию операций обнуляем буфер.
Тут мне опять пришла в голову идея о шагающих квадратах: а что, если в первый проход пробежаться по границе области, определить описывающий ее прямоугольник, выделить буфер по размеру прямоугольника (это - как раз искомое возвращаемое функцией значение); после этого, проходя по объекту, пиксель за пикселем копировать его в буфер, "стирая" с оригинала. Правда, здесь опять та же самая проблема: нет гарантии, что за один проход все будет сделано.
"Гюйгенс".
Один из вариантов прохождения по изображению в поисках связанных областей (который, собственно, первым пришел мне в голову еще вчера, но лучше всего, конечно, он бы выглядел в рекурсивном исполнении).
лирика по поводу рекурсии
Еще раз, чтобы убедиться лишний раз в том, что рекурсия невозможна, я набросал элементарнейшую проверялку:
Проверялочка эта должна вылетать с сегфолтом как только скончаются все ресурсы на рекурсию. Получилось вот что:
... 261504 Ошибка сегментирования (core dumped)
... 261350 Ошибка сегментирования (core dumped)
... 261333 Ошибка сегментирования (core dumped)
ЕМНИП, что-то подобное было на ЛОРе. И, возможно, предел этот даже как-то регулируется. Число это подозрительно близко к 256к (262144): возможно, оно так и есть (просто еще кучу места в стеке занимают всякие вспомогательные буферы из stdio).
Я немного видоизменил программку: она теперь еще и по 1к выделяет:
... 174258 Ошибка сегментирования (core dumped)
... 174372 Ошибка сегментирования (core dumped)
... 174325 Ошибка сегментирования (core dumped)
Кто сожрал 85к? Почему именно 85к? Заменяю выделение памяти на char x = 2; - получаю то же самое. Добавление переменных ничего не меняет. В любом случае, все-таки про рекурсию придется забыть: если искомая область будет иметь площадь, превышающую эдак 170 тысяч пикселей (а это не так уж и много), то я получу большой фигвам.
Еще раз, чтобы убедиться лишний раз в том, что рекурсия невозможна, я набросал элементарнейшую проверялку:
#include <stdio.h>
int incr(int a){
fprintf(stderr, "%d ", a++);
incr(a);
return a;
}
main(){
incr(1);
return 0;
}
Проверялочка эта должна вылетать с сегфолтом как только скончаются все ресурсы на рекурсию. Получилось вот что:
... 261504 Ошибка сегментирования (core dumped)
... 261350 Ошибка сегментирования (core dumped)
... 261333 Ошибка сегментирования (core dumped)
ЕМНИП, что-то подобное было на ЛОРе. И, возможно, предел этот даже как-то регулируется. Число это подозрительно близко к 256к (262144): возможно, оно так и есть (просто еще кучу места в стеке занимают всякие вспомогательные буферы из stdio).
Я немного видоизменил программку: она теперь еще и по 1к выделяет:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int incr(int a){
char *x = malloc(1024);
memset(x, 1, 1024);
fprintf(stderr, "%d ", a++);
incr(a);
return a;
}
main(){
incr(1);
return 0;
}
Теперь цифры "более другие":
... 174258 Ошибка сегментирования (core dumped)
... 174372 Ошибка сегментирования (core dumped)
... 174325 Ошибка сегментирования (core dumped)
Кто сожрал 85к? Почему именно 85к? Заменяю выделение памяти на char x = 2; - получаю то же самое. Добавление переменных ничего не меняет. В любом случае, все-таки про рекурсию придется забыть: если искомая область будет иметь площадь, превышающую эдак 170 тысяч пикселей (а это не так уж и много), то я получу большой фигвам.
Принцип Гюйгенса прост: каждая точка волнового фронта (в нашем случае - точка внутри искомой области) является источником нового волнового фронта (в нашем случае - центром, от которого "расходится" поисковая волна).
Пробегаясь такой "волной" от первого обнаруженного пикселя, принадлежащего искомой фигуре, мы пройдем все ее закоулки. Однако, проблема в том, как реализовать это без бешеного количества разыменования указателей и проверок.
Долго кумекая, я так и не придумал, как реализовать "Гюйгенса", не вводя уйму промежуточных матриц.
"Гюйгенс модифицированный"
Для ограничения одной-единственной вспомогательной матрицей, можно попробовать оставить одну "волну": от первоначальной точки проводим расходящуюся во все стороны "волну". Пусть для простоты она будет "квадратной". Пробегаясь по всем точкам очередного фронта "волны", кроме особых (о них - позже, их тоже надо контролировать), "стираем" единицы с изображения, перенося их в маску. Как только натыкаемся на нуль, если до этого тоже был нуль, заносим единичку в матрицу спец.маски в соответствующий пиксель, а также в пиксель, соседний с текущим, но расположеный на "волне" с радиусом на 1 больше (т.е. для левой поверхности переносим пиксель влево на 1, для правой - вправо на 1 и т.п.). Если натыкаемся на 1, делаем обнуление соответствующего пикселя на "волне" с радиусом на 1 больше + его предыдущего соседа в спец.маске. Таким образом, "тени" получаются постепенно сходящимися. Но все равно, остается возможность того, что эти "тени" что-нибудь да сокроют, не говоря уже о том, что эта "волна" только в очень редких случаях сразу выявит хотя бы внешний контур искомой фигуры. Именно для этого и нужна наша спец.маска: как только "кончится" искомая фигура по "большой волне", нужно будет просмотреть все пиксели маски (внутри уже получившегося прямоугольника + 1 по радиусу, т.к. снаружи все равно нули), пробегая по их внешней границе: как только обнаруживаем 1 в изображении, "выпускаем" новую "волну". Пока бежим первый раз, пропускаем все единички в маске, копируя их на "волну" маски с большим радиусом.Метод получился опять каким-то уж очень извращенным. Глаза уже слипаются, поэтому пока - отбой.
первое приближение к решению
Итак, сегодня я накачал всяких статеек. Почитал. Обычный алгоритм выделения связанных областей (как я мог о нем не вспомнить?) жутко тормозной: он требует как минимум четыре прохода (1 - маркировка соседних областей, 2 - построение списка эквивалентности номеров, 3 - переименование областей, 4 - выделение объектов). При этом на первом шаге мы должны проверять, были ли до пикселя нули, на втором - верхнего соседа (и его боковых, если нужно заполнить 8-связанную область), на третьем - сверяться с таблицей, на четвертом - многократно сканировать изображение, пока все объекты не будут выделены. Конечно, четвертый шаг можно укоротить: на третьем же шаге выделить массив прямоугольников и обновлять размеры соответствующего прямоугольника. Тогда четвертый шаг сведется к сканированию внутренностей каждого прямоугольника с заполнением его маски единицами там, где в массиве номеров пиксель == N.Процедура перемаркировки такова: проходя матрицу с маркерами, проверяем каждый пиксель. Если пиксель нулевой, устанавливаем флаг обнаружения в false и продолжаем. Если флаг == true, это значит, что данный номер мы уже перемаркировали - продолжаем. Если же пиксель - "свеженайденный", надо проверить все 6 пикселей (для 8-связных областей) в предыдущем и последующем рядах (снизу - т.к. возможен вариант коротенького кусочка, а то и единичного пикселя над длинной линией - из-за флага обнаружения мы пролетим под ним, а вот на этапе сканирования его, изучая то, что под ним, сделаем правильную ремаркировку). Вполне возможно, что нет необходимости проверять сразу и пиксель над, и под нашим: пиксель точно под ним по-любому будет проверен (он - либо часть более длинной линии, которую мы обнаружим в DL или DR углах, либо часть вертикальной, которая обнаружится при сканировании нижней линии).
А вообще, полезно рассмотреть, какие варианты расположения соседей могут быть, чтобы не пропустить мелочей (нумерация не важна, поэтому просто 1/0; кроме того, т.к вариантов аж 256, рассмотрим лишь некоторые): (X - что угодно, U - единица в одном из пикселей, V - единица в одном и более пикселей)
A B C D E F G H I J K L M N O P Q R XXX 000 UUU 000 101 101 11X 000 010 000 000 000 000 000 000 000 000 000 11X 01X 01X 01X 01X 01X 01X 01X 01X 01X 01X 01X 01X 01X 01X 01X 01X 01X XXX 000 000 VVV 000 VVV VVV 010 000 000 000 000 000 000 000 000 000 000 A - просто проходим мимо B - инкрементируем счетчик уникальных объектов и перемаркируем текущий номер C - ремаркируем текущее значение на то, которое содержится в массиве с индексом, имеющим значение счетчика в пикселе с единичкой D - то же, что в B + ремаркировка значений в нижней строке E - ремаркировка текущего + правого верхнего счетчика по массиву с индексом, равным значению счетчика в левом верхнем пикселе F - ремаркировка всех по левому верхнему G - для упрощения можно сделать эквивалентом F: нет нужды усложнять алгоритм ради пары лишних операций H - нет нужды проверять этот вариант, т.к. на стадии I он ремаркируется I - заменяет H
Итак, общие тенденции:
- Если в верхней строке ничего нет, проверяем, не является ли наше текущее значение счетчика уже перемаркированным: если является, оставляем как есть, иначе - счетчик уникальных объектов инкрементируется и текущее значение перемаркируется им; объекты внизу (если есть) тоже перемаркируются им;
- если в левом верхнем углу !=0, все номера перемаркируются им, а пиксель над нами проверять нет нужды;
- если обнаружен !=0 ровно над нами, правый верхний проверять нет нужды, а оставшиеся перемаркируются им.
При организации процедуры переименования я натолкнулся на следующее: если ряд X переименовывается в нижележащий Y, а потом Y переименовывается в Z, то ряд X так и остается переименованным в Y. Следовательно, при переименовании в массиве ассоциаций на стадии перенумерования надо проверять, выполняется ли равенство assoc[assoc[X]] == assoc[X], т.е. зафиксировать сложные связи.
Оказалось не все так просто с переименованием: надо будет нарисовать на бумажке, что-то засыпаю уже. На сегодняшний день ничего не планирую (спать надо лечь рано, т.к. завтра в 5 утра вставать), так что, пожалуй, закруглюсь и буду спать. Сегодня еще вопросы с криостатом утрясти надо будет.
Пока засыпал, придумал, как правильно скорректировать алгоритм переименования: скажем, переименовываем мы пиксель с изначальной меткой "X", имеющий новую метку "Y" (т.е. assoc[X] != X) в "Z". В этом случае надо наоборот переименовать Z в Y, а величины, больше Z, но <= Ncur декрементировать, затем декрементировать и Ncur. Все довольно просто, а очевидным плюсом является то, что не надо многократно сканировать изображение для переименования "по месту": это будет сделано на шаге 3.
Конечно, все познается в сравнении, поэтому сразу обливать дерьмом этот метод нельзя: стоит попытаться его реализовать. А учитывая то, что все шаги, кроме второго, превосходно параллелизуются (1, 3 - хоть по строкам, хоть по прямоугольным областям, 4 - по номерам объектов), на куде, пожалуй, он будет работать просто превосходно!
еще мысли
Однако, меня все еще грызут сомнения: ведь по сути обнаружение связанных областей подобно закрашиванию области, ограниченной контуром (в данном случае - "закрашивание" нулями этой самой связанной области с "перенесением" соответствующих пикселей во временное хранилище (а оттуда уже, по завершению закрашивания, - в соответствующее подызображение).Итак, если выбрать эффективный метод закрашивания областей, то обнаружение сведется к следующему:
- пробегаемся по точкам изображения, как только находим 1, "закрашиваем" всю область нулями с "перенесением" ее во временную маску (равную размеру изображения) и параллельным обновлением границ бокса;
- выделяем для бокса память и переносим в него кусок из маски ("модифицированный" шаг 4 из предыдущих размышлений);
- сканируем изображение, начиная со следующей точки после точки из п.1.
Если опустить "волшебную" фразу "закрашиваем" из п.1, то изображение сканируется не "три с половиной", а "полтора" раза. Т.о., если "закрашивание" будет сканировать пиксели изображения лишь один раз, то получим весомый выигрыш. Однако, тут же всплывает и минус: параллелится эта задача только разбиением изображения на фреймы с последующим объединением оных. В принципе, если "закрашивание" будет достаточно быстрым, то проблем не будет: все равно алгоритм будет работать быстрее классического.
Итак, теперь остается прикинуть: что же за алгоритмы закрашивания есть. Если верить википедии, то самый быстрый алгоритм основывается на закрашивании области линиями с параллельной проверкой соседних сверху и снизу пикселей и занесением в список координат пикселей, которые появляются после 0. По окончанию сканирования линии, мы переходим к следующему пикселю в списке. И так до тех пор, пока список не кончится. Причем, сканирование идет в двух направлениях!
Еще я наткнулся на какой-то "очень быстрый метод" маркирования связанных областей от братьев-китайцев, но что-то даже после трехкратного прочтения я так и не понял: то ли братья-китайцы что-то темнят, то ли я - идиот (конечно, возможны оба варианта, но мне больше первый нравится).
Итак, хватит растекаться мысью по древу - пора переключаться в "медленный режим" и реализовывать методы. В любом случае, закрашивание областей реализовать надо (для начала - хотя бы 4-связанных, а о восьмисвязанных подумать, т.к. я что-то с трудом себе представляю реальные объекты, ограниченные четким 4-связанным контуром), поэтому стоит для начала на CPU с помощью openMP реализовать названные два способа маркировки, да и сравнить их: даже если окажется, что второй способ медленнее, он все равно понадобится для закрашивания.
// что-то у меня промежуточные всякие выкладки много времени занимают: сейчас вот начал организовывать FIFO и подумал, что неплохо было бы туда и LIFO добавить...
реализация перемаркировки
Реализуя перемаркировку, я наткнулся на кое-какие "косяки". В итоге "малюсенькая" inline-функция для перемаркировки превратилась в громоздкого монстра. Но надо еще протестировать на разных "картинках" ее поведение. Если проблем с перемаркировкой не будет, можно будет переходить к завершающему этапу - заполнению.Так как в процессе перемаркировки счетчик количества объектов скачет туда-сюда, для завершающего - четвертого - этапа придется-таки использовать "полтора" прохода: сначала пробежаться по изображению с заполнением величин соответствующих боксов, а затем уже пробежаться по внутренностям каждого бокса, заполняя их маски.
Пожалуй, цикл по ремаркировке стоит вынести в отдельную функцию: тогда можно будет искать и 4-связанные области без предварительной 4-фильтрации.
Проверка на случайно сгенерированных "картинках" показала, что алгоритм вроде бы вполне рабочий. Надо бы добавить отбрасывание одиночных точек. Но что-то не хочется мне утяжелять и без того уже тормозной и раздувшийся алгоритм. Проще все-таки, наверное, сделать предварительную фильтрацию (по октетам) "сжатого" изображения (как в FCfilter), а потом уже обрабатывать ее результат.
Кстати, сейчас прогнал поиск 8-связанных областей по изображению, отфильтрованному функцией FCfilter, и до меня дошло, что все равно нужно писать отдельную функцию поиска 4-связанных областей, т.к. вполне может быть так, что две 4-связанные области соприкасаются по диагонали. При этом они являются разными объектами, но поиск 8-связанных областей обзывает их одинаково (что понятно).
Для упрощения громоздких выкладок я сделал такой велосипед: внутренности некоторых функций были вынесены в отдельные (многократно подключаемые) файлы. Перед подключением файла для модификации устанавливается тот или иной флаг. В результате некоторое количество условных операций отпадает. Правда, реализация перемаркировки получилась у меня такой страшнючей, что мне лень ее оптимизировать подобным образом.
Перед тем, как выполнить поиск 4-связанных областей, я выполняю фильтрацию. Она заодно отсекает и одиночные пиксели. А вот фильтрация для функции выделения 8-связанных областей заключается в том, что как раз только одиночные пиксели и отсекаются. Жалею сейчас, что пока был в ИПФ, не нагуглил, как наиболее оптимально их фильтровать, поэтому опять буду изобретать велосипед. Фильтрация одиночных точек на "упакованном" изображении при помощи логических операций, как я говорил выше, использует огромное количество промежуточных вычислений, поэтому я даже и браться не буду за нее: выигрыш производительности получится сомнительным. Поэтому, пожалуй, проще всего будет воткнуть эту проверку в функцию поиска связанных областей.
Красотищща! Оно работает!!!
Рано радовался: запустил тест (случайная картинка 2000х2000): получилось, что cc4 работает почти в 2 раза медленней, чем cc8!!! Как? А ведь у cc8 значительно больше проверок... Черт знает что! Да и вообще не понравилось мне время выполнения: 13.3 секунд у cc4 и 7.8 у cc8. Кстати, я еще не реализовал шаг 4! При этом если из cc4 выкинуть функцию `FC_filter`, производительность возрастает еще на пару секунд (ну, это, наверное, из-за того, что приходится лишние пиксели проверять).
Я подозреваю, что виновата перемаркировка, занимающая тем больше времени, чем больше областей обнаружено на изображении. Сейчас воткну тайминги в cclabel. Тайминги показали, что действительно шаг 2 занимает на 4 порядка больше времени, чем остальные шаги. А ведь сканирование-то идет единожды! Отключив функцию ремаркировки, я получил крайне высокую производительность. Глянул количество первично обнаруженных областей - их больше 200 тысяч! Понятное дело, что если каждый раз при перемаркировке пробегаться по 200000 значений, а ремаркировок получаем почти столько же, производительность выйдет вообще никакая!
В общем, надо думать, как оптимизировать это узкое место. Параллелизация дала прирост производительности всего лишь в полтора раза. Я, правда, еще заметил, что у меня количество итераций постоянно - по всем элементам, хотя стоит проверять лишь те, которые уже пройдены. Скорректировал, получил прирост производительности всего-то в полтора раза у cc4 и никакого прироста у cc8!
Уменьшив содержание единиц и нулей на изображении от 50/50 до 10/90, я получил поразительно низкие цифры для cc4: полторы сотни миллисекунд! Время cc8 получилось в районе 1с. Даже не знаю, нормально ли это: надо будет сравнить с другими алгоритмами (хотя бы с лептоникой). Уже сравнение с октавой показало, что у меня полная лажа: для тех же 50/50 октава дает 20мс! Зато для 10/90 у меня быстрей, чем в октаве (но октава считает и одиночные точки :( ).
Проверка на очень большом количестве единиц показала, что считать все-таки надо не до текущего значения (я про цикл с перемаркировкой) ведь нижняя строчкае уже может быть ремаркирована -> получаем косячок-с. Чтобы его не было, добавлю "про запас" половину ширины строки (больше объектов все равно быть не может).
Я долго думал, как бы это на графах реализовать, но что-то так и не придумал. Посмотрел на время - уже начало 11-го. Пора баиньки, а то завтра в 4:30 вставать.
другой метод
Пока сидел в аэропорту НН, было время почитать статью китайцев. Действительно, интересный способ: мой метод (теоретически) будет хорош на небольшом количестве больших объектов, а их метод - на большом количестве малых. Принцип почти тот же, что и в моем случае, но: - шаги 1 и 2 объединены: на стадии именования меток происходит и ремаркировка; - второй проход (который у меня - третий) такой же, как у меня - переименование участков по скорректированным меткам. Правда, китайцы умолчали, как они создавали список меток: если его создавать динамически (как список, динамический массив или даже дерево), то все операции над списком будут довольно-таки дорогими, поэтому есть смысл просто выделить заранее достаточное количество памяти. Учитывая то, что объектов на изображении ну никак не может быть больше, чем 1/6 размера изображения, можно "на авось" выделить 1/4 размера изображения под массив ремаркировок.Еще одним отличием является метод сканирования: сканируются все пиксели, но в зависимости от того, был ли ненулевой пиксель до текущего, или его не было, производятся разные действия: если точка новая, производится проверка ТРЕХ точек над ней (точки снизу не проверяются, понятное дело, т.к. происходит сканирование всех точек) с соответствующим именованием текущей точки и ремаркировкой, если мы натыкаемся на ситуацию, когда над точкой ничего нет, а вверху справа и слева - разные метки. Если же до текущей точки уже была точка, нам нужно лишь проверить, что находится в правом верхнем углу. Если там ничего нет, мы просто даем точке номер соседней слева, если же есть - все равно даем тот же номер, но еще и производим процедуру ремаркировки.
Благодаря такому методу сканирования (теоретически) повышается скорость: ремаркировки необходимо производить значительно реже. В общем, надо бы проверить этот метод.
Описанная выше процедура - для маркировки 8-связанных областей, если же нам нужно маркировать 4-связанные области, мы просто проверяем лишь точку НАД нашей.
Да уж, попытался я было что-нибудь "покодить", сидя в аэропорту. Оказалось, что это не так-то и просто: без мыши и нормальной клавиатуры ну совсем невозможно работать! Да еще и экран ноутбука перевешивает - бук так и норовит кувыркнуться с колен. В общем, откладываю до дома.
Черт бы подрал этот аэропорт: wifi как бы номинально есть, а реально - не работает, собака! Вроде бы соединяет, но скорость очень близка к нулю: tracepath дает аж секунды! В общем, на втором этаже жизни нет.
Все-таки сделал более-менее реализацию. Работает, но нужно еще внимательно просмотреть функцию ремаркировки: эта собака неправильно считает количество найденных объектов!
Бульбец! Самолет задерживают на полтора часа! Буду дописывать...
Oh, shi! It seems like I'll never fly from this fucking airport!!!