mirror of
https://github.com/eddyem/eddys_snippets.git
synced 2025-12-06 02:35:12 +03:00
524 lines
25 KiB
Plaintext
524 lines
25 KiB
Plaintext
************************************
|
||
СООБРАЖЕНИЯ ПО ПОВОДУ ЭРОЗИИ/ДИЛЯЦИИ
|
||
************************************
|
||
|
||
|
||
Эрозия
|
||
|
||
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 дает
|
||
аж секунды! В общем, на втором этаже жизни нет.
|
||
|
||
Все-таки сделал более-менее реализацию. Работает, но нужно еще внимательно
|
||
просмотреть функцию ремаркировки: эта собака неправильно считает количество
|
||
найденных объектов!
|
||
|
||
Бульбец! Самолет задерживают на полтора часа! Буду дописывать...
|