mirror of
https://github.com/eddyem/eddys_snippets.git
synced 2026-03-20 08:41:02 +03:00
first commit
This commit is contained in:
523
erosion-dilation/README
Normal file
523
erosion-dilation/README
Normal file
@@ -0,0 +1,523 @@
|
||||
************************************
|
||||
СООБРАЖЕНИЯ ПО ПОВОДУ ЭРОЗИИ/ДИЛЯЦИИ
|
||||
************************************
|
||||
|
||||
|
||||
Эрозия
|
||||
|
||||
00000 00000
|
||||
01110 00000
|
||||
01110 => 00100
|
||||
01110 00000
|
||||
00000 00000
|
||||
|
||||
Бит равен единице лишь тогда, когда присутствуют все его 4 связи.
|
||||
Т.е. для срединных битов байта выполняем & текущего байта с байтами верхней и нижней строк. Для крайних битов необходимо проверить еще и 4-связные байты:
|
||||
|
||||
ABC
|
||||
DEF
|
||||
GHK
|
||||
|
||||
Младший бит E проверяется по старшему биту F, младшим битам B и H и второму биту E.
|
||||
Старший бит E проверяется по младшему биту D, старшим битам B и E и седьмому биту E.
|
||||
|
||||
Таким образом, алгоритм получается следующим:
|
||||
1. подменяем E маской[E] (эрозия внутренних битов)
|
||||
2. делаем &: E = E & B & H
|
||||
3. корректируем старший бит E по младшему D и младший бит E по старшему F.
|
||||
|
||||
ПРОВЕРИТЬ, ЧТО БЫСТРЕЙ: (A&0X80)>>8 ИЛИ РАЗЫМЕНОВАНИЕ УКАЗАТЕЛЯ (ПО ТАБЛИЦЕ)
|
||||
|
||||
|
||||
Диляция
|
||||
|
||||
00000 00110
|
||||
00110 01111
|
||||
01010 => 11111
|
||||
01100 11110
|
||||
00000 01100
|
||||
|
||||
Обратная ситуация: вместо & делаем | :
|
||||
1. подменяем E маской (диляция внутренних битов)
|
||||
2. делаем |: E = E | B | H
|
||||
3. корректируем старший и младший биты операцией |.
|
||||
|
||||
|
||||
|
||||
|
||||
**************************************************
|
||||
СООБРАЖЕНИЯ ПО ПОВОДУ ВЫДЕЛЕНИЯ СВЯЗАННЫХ ОБЛАСТЕЙ
|
||||
**************************************************
|
||||
|
||||
Вернемся к тому же общему случаю: искомый пиксель нахдится в байте 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-связанность.
|
||||
|
||||
Первое, что приходит на ум - двухпроходная маркировка: сначала мы пробегаемся
|
||||
по связанной области, постепенно расширяя границы заключающего ее прямоугольника.
|
||||
Затем мы выделяем память для этого кусочка и заполняем его, пробегаясь по нашей
|
||||
области еще раз (ну или делаем полную копию содержимого бокса, а затем выкидываем
|
||||
все, что не принадлежит искомой связанной области).
|
||||
|
||||
Еще вариант - сразу выделять область памяти (вспомогательную) размером со ВСЕ
|
||||
первоначальное изображение (гениально!). В этом случае мы сможем сразу же копировать
|
||||
в эту область памяти нашу х-связанную область, а затем, когда она целиком будет
|
||||
скопирована и будут ясны параметры описывающего ее прямоугольника, выделяем уже
|
||||
память именно под эту область и копируем в нее содержимое бокса.
|
||||
|
||||
Логично было бы преобразовать картинку до анализа в бинарный вид, поэтому сначала
|
||||
будем предполагать, что мы имеем дело с бинарным изображением (а не "сжатым"
|
||||
битовым).
|
||||
|
||||
Теперь каждый пиксель нашей матрицы соответствует одному пикселю изображения
|
||||
ABC
|
||||
DEF
|
||||
GHK
|
||||
|
||||
На ум приходит использовать метод шагающих квадратов, однако, он применяется
|
||||
для обхода границ -> мало чем поможет (разве что первоначальным определением
|
||||
размеров бокса, но я уже решил делать лишь один проход обнаружения связанных
|
||||
областей). Интернета под рукой нет, чтобы подсмотреть наиболее эффективный
|
||||
способ, поэтому буду изобретать велосипед.
|
||||
|
||||
|
||||
=== Нулевое приближение ===
|
||||
|
||||
/*
|
||||
Работаем с копией изображения, т.к. уже пройденные значения будем обнулять.
|
||||
*/
|
||||
|
||||
Итак, выделим память под промежуточный буфер (сразу обнулив ее) и начнем
|
||||
проходить изображение с левого верхнего угла. Как только мы натыкаемся
|
||||
на единицу в E, записываем в соответствующие координаты буфера 1.
|
||||
Далее смотрим, куда нам нужно двигаться. Считаем, что в A,B,C и D нули (т.к. там -
|
||||
уже пройденная область). Нам необходимо проверить пиксели F, G, H и K.
|
||||
Теперь каждый из них становится E. Если кто-то из них установлен в 1, проверяем
|
||||
его соседей и т.д. Однако, на всех шагах, отличных от первого, мы уже обязаны
|
||||
проверить ВСЕХ СОСЕДЕЙ, т.к. не факт, что наша область не представляет собою
|
||||
какой-нибудь извращенной фигуры с отростками и многочисленными полостями.
|
||||
|
||||
На ум приходит рекурсия, однако, учитывая то, что изображения могут быть довольно-таки
|
||||
огромными, с горем расстаюсь с этой идеей. А так было бы хорошо: рекурсия сделала
|
||||
бы очень простым этот алгоритм. Но регистры не бесконечные.
|
||||
|
||||
Кстати, если не преобразовывать изображение в бинарное, то вполне возможны случаи,
|
||||
когда один и тот же байт будет проходиться вплоть до четырех раз!
|
||||
|
||||
После копирования каждого пикселя с 1 в результирующий буфер, "стираем" его
|
||||
с оригинала (сбросом в 0). По окончанию операций обнуляем буфер.
|
||||
|
||||
Тут мне опять пришла в голову идея о шагающих квадратах: а что, если в первый
|
||||
проход пробежаться по границе области, определить описывающий ее прямоугольник,
|
||||
выделить буфер по размеру прямоугольника (это - как раз искомое возвращаемое
|
||||
функцией значение); после этого, проходя по объекту, пиксель за пикселем копировать
|
||||
его в буфер, "стирая" с оригинала. Правда, здесь опять та же самая проблема:
|
||||
нет гарантии, что за один проход все будет сделано.
|
||||
|
||||
|
||||
=== "Гюйгенс". ===
|
||||
Один из вариантов прохождения по изображению в поисках связанных областей (который,
|
||||
собственно, первым пришел мне в голову еще вчера, но лучше всего, конечно, он бы
|
||||
выглядел в рекурсивном исполнении).
|
||||
|
||||
== лирика по поводу рекурсии ==
|
||||
Еще раз, чтобы убедиться лишний раз в том, что рекурсия невозможна, я набросал
|
||||
элементарнейшую проверялку:
|
||||
|
||||
#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. пробегаемся по точкам изображения, как только находим 1, "закрашиваем" всю
|
||||
область нулями с "перенесением" ее во временную маску (равную размеру изображения)
|
||||
и параллельным обновлением границ бокса;
|
||||
2. выделяем для бокса память и переносим в него кусок из маски ("модифицированный"
|
||||
шаг 4 из предыдущих размышлений);
|
||||
3. сканируем изображение, начиная со следующей точки после точки из п.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 дает
|
||||
аж секунды! В общем, на втором этаже жизни нет.
|
||||
|
||||
Все-таки сделал более-менее реализацию. Работает, но нужно еще внимательно
|
||||
просмотреть функцию ремаркировки: эта собака неправильно считает количество
|
||||
найденных объектов!
|
||||
|
||||
Бульбец! Самолет задерживают на полтора часа! Буду дописывать...
|
||||
Reference in New Issue
Block a user