Page 1 of 4

Алгоритмы заливки

Posted: Fri Jan 21, 2011 2:25 pm
by SoUrcerer
Алгоритмы заливки, ясное дело, бывают разные. Пытаюсь составить алгоритм, который был бы нетребователен к памяти и довольно быстр.
Итак, я попробовал сделать алгоритм, закрашивающий замкнутые области, в том числе с отверстиями внутри, при условии, что отверстия имеют размер хотя бы 1x1 пиксел и окружены границей минимум в 1 пиксел.
Изображение сканируется построчно, причем в некоторых случаях проверяется, будет ли иметься возможность закрашивания других точек в этой строке. Пространства для оптимизации море (например, выходить из цикла для строки, когда в строке достоверно больше не остается точек для закрашивания).

На JS это выглядит так:

Code: Select all

function flood(){            //заливка
   for (y=0;y<maxy;y++){ //выполняем для каждой строки
      fill=0;  //строка не может начинаться с заливки
      for (x=0;x<maxx;x++){    //для каждой точки в строке 
         if ((m[x][y]==1)&&(m[x+1][y]==1)) {fill=0;} else { //если текущая точка - граничная, и следующая - тоже граничная, то закрашивать следующую точку не нужно
             if (m[x][y]==1&&(scanend(x,y)==1)) {fill==1 ? fill=0 : fill=1} //если текущая точка граничная, но за ней незакрашенная точка, а впереди есть граничные точки, то изменим положение пера (опущено/поднято) на противоположное. Таким образом, если мы встречаем первую граничную точку в строке, за которой можно закрашивать, то мы опускаем перо (т.е. мы во внутреннем контуре). Следующая за ней граничная точка, очевидно, показывает на окончание внутреннего контура, и мы поднимаем перо.
             if (m[x][y]==1&&(scanend(x,y)==0)) {fill=0;} //если текущая точка граничная, но за ней нет граничных точек в строке, то заливать ничего не нужно
             if ((m[x][y]==0)&&(fill==1)) {m[x][y]=2;}} //если текущая точка свободна, и перо опущено, то закрасим её
        draw(x,y);}}} //отрисовка для пошаговой отладки, можно убрать

function scanend(x,y){
    var xx=0; //сбросим значение функции
    for (i=x+1;i<maxx;i++){ //для всех оставшихся точек в строке
       if (m[i][y]==1) {xx=1;}} //если хоть одна граничная точка есть, то еще что-то можно закрасить
   return xx;}
Посмотреть на работающий пример!

Если хватит упертости, попробую сделать это на fasm, и посмотреть, насколько быстро работает

Re: Алгоритмы заливки

Posted: Fri Jan 21, 2011 3:57 pm
by art_zh
Sorcerer

Растровой графике - растровую заливку.
1) построчный прогон реализовать проще, чем полноценный обход контура.
2) циклы записи в память оптимизированы именно для горизонтальной растровой заливки.

вот только что делать с таким контуром:

Code: Select all

     BBB ? BBB ?
   BB...BBB.?.B
   B.....B..?.B
   BBB......BB
      BBBBBB ?

Re: Алгоритмы заливки

Posted: Fri Jan 21, 2011 4:20 pm
by SoUrcerer
Хм, не самый худший результат. Эти точки (?) просто опускаются и на заливку не влияют. Для монохромных изображений сработает, для цветных пара дополнительных условий, и всё будет работать...

Почему векторные изображения нужно как-то иначе заливать? Ведь на экране все равно мы будем иметь растр? Можно ведь растрировать векторную картинку, и залить... Или есть более правильные решения?

Re: Алгоритмы заливки

Posted: Fri Jan 21, 2011 7:11 pm
by art_zh
Sorcerer wrote:Почему векторные изображения нужно как-то иначе заливать? Ведь на экране все равно мы будем иметь растр? Можно ведь растрировать векторную картинку, и залить... Или есть более правильные решения?
Дык и я про то же: перед заливкой картинка так или иначе все равно будет растеризована, так что предложенный подход представляется наиболее оптимальным.

А вопросительными знаками я обозначил фрагменты, на которых твой алгоритм может сбоить.
Надо как-то проверять контур на выпуклость/вогнутость.
И проще всего это делать в векторном режиме :?

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

Re: Алгоритмы заливки

Posted: Fri Jan 21, 2011 8:02 pm
by SoUrcerer
Так контур и проверяется ;) Здесь обводка (т.е. пиксели типа 1) - это действительно обводка, а не просто закрашенные пиксели. Она как раз учитывает изгибы, выпуклые и вогнутые части фигуры, и может закрашивать, например, контуры с волнистым краем.
Сейчас выложу версию, где можно править карты вручную (правда, сделал это на скорую руку, не судите строго).
А вот и ссылка для экспериментов. Если еще не работает, то скоро будет
UPD: хотя, некоторые спорные места в алгоритме все же есть.

Re: Алгоритмы заливки

Posted: Fri Jan 21, 2011 8:18 pm
by SoUrcerer
Хм. А вот если предложенный контур повернуть.... Что будет, интересно.
Портится вся малина :) Пошел думать над алгоритмом дальше

(А портится она из-за того, что слежение "внутри контура"/"снаружи контура" ведется только по одной оси)

Может быть, у кого-нибудь есть идеи лучше?

Re: Алгоритмы заливки

Posted: Fri Jan 21, 2011 9:01 pm
by SoUrcerer
А что, если так?

1. Обвести заливаемый контур снаружи - так можно определить, где внутренняя часть контура, а где внешняя.
2. Обвести внутренние полости заливаемого контура
3. Идти по изображению сверху вниз слева направо и искать точки обводки, справа от которых имеется граница, вдоль которой мы делали обводку. Искать следующую в строке точку обводки. Закрасить строку от первой до второй точек обводки. Повторять пункт 3 до последней строки

При таком раскладе вроде бы однозначно будет закрашена только внутренняя часть изображения...
Правильно мыслю?

Re: Алгоритмы заливки

Posted: Fri Jan 21, 2011 10:40 pm
by lev
Сделать триангуляцию, закрасить треугольники

Re: Алгоритмы заливки

Posted: Fri Jan 21, 2011 11:08 pm
by SoUrcerer
Для кривых Безье на разрешении 1200x1200 это будет трудно и ресурсоемко.

Re: Алгоритмы заливки

Posted: Sat Jan 22, 2011 12:31 am
by Foldl
А вот этот алгоритм не подойдет?

Re: Алгоритмы заливки

Posted: Sat Jan 22, 2011 1:19 am
by SoUrcerer
Удивлен. Проверяю различные алгоритмы (на javascript - потому что понятнее, нагляднее и систему перезагружать постоянно не требуется), решил попробовать сделать "ленивый" вариант нахождения обводки для заливаемого контура. По-хорошему нужно обходить контур снаружи, проверяя, есть ли рядом пиксели контура. Но голова не варила, и я решил поступить так:

1. Для всех свободных точек рядом с границей контура в памяти устанавливаю метки (условно зову их "синие") - получаю обводку по обе стороны от контура
2. Далее, иду от края холста к контуру до тех пор, пока не встречу метку. Отмечаю эту точку "зеленой" меткой. Запоминаю ее координаты
3. Из построения "синих" меток имею, что каждая точка имеет ровно двух соседей по горизонтали и/или вертикали для выпуклого контура (вогнутые пока что не рассматриваем для упрощения жизни).
Делаю цикл while, в котором перехожу на соседнюю "синюю" точку, одновременно перекрашивая ее в "зеленую" до тех пор, пока не попаду в точку, соседнюю с начальной.
4. Делаю заливку

Тесты на участках 8x8 мне порядком надоели, и я растеризовал часть глифа от буквы "о" в массив 300 на 300.
Получаю примерно ожидаемые результаты: Chromium 8 рендерит изображение за две-три секунды, Firefox 3.6.14 - около десяти (при этом возмущается, что скрипт не отвечает или завис).

Код участка:

Code: Select all

fin=0;x=k;y=100;l=0;
while (fin!=1) {
if (m[x+1][y]==2) {m[x+1][y]=3;x=x+1;} //2 - синий, 3 - зеленый
if (m[x-1][y]==2) {m[x-1][y]=3;x=x-1;} //Можно было бы не проверять прошлую клетку, но это потребовало бы еще один if, так какая разница?
if (m[x][y+1]==2) {m[x][y+1]=3;y=y+1;}
if (m[x][y-1]==2) {m[x][y-1]=3;y=y-1;}
if ((x==k)&&(y==99)) {fin=1;}}

А теперь начинается шаманство. Расставляю контрольные точки, и выясняю, что самый красный тормоз коммунизма - не перебор массива 300 на 300 в 1м или 4м шагах алгоритма (причем в первом шаге на каждую из 90000 итераций от 1й до 8ми проверок - не меньше 90000 операций, а во втором на каждую из 90000 итераций - ровно 3 проверки, то есть минимум 270000 операций), а "безобидный" третий шаг с его примерно 5 проверок*(2*Pi*150 пикселей радиуса)= 5000 операциями.

Включаю индусский режим, и заменяю шаг 3 на вот такой бред:
3. Перебрать все "зеленые" точки в памяти , и раскрасить их "синих" соседей в "зеленые". Повторять до просветления, число итераций определено экспериментально и находится между 140 и 160.

Итого 300*300*160 итераций, в каждой от 1 до 5 проверок - никак не меньше 14400000 операций. Безумная и идиотская затея.

Code: Select all

for (z=0;z<160;++z){ //повторять до просветления
for (y=0;y<299;++y){
for (x=0;x<299;++x){
if (m[x][y]==3) {if (m[x-1][y]==2) {m[x-1][y]=3;}; if (m[x+1][y]==2) {m[x+1][y]=3;}; if (m[x][y-1]==2) {m[x][y-1]=3;}; if (m[x][y+1]==2) {m[x][y+1]=3;}}}}}
Chromium 8 - 2 секунды, Firefox 3.6.14 - 2 секунды.

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

У меня тут вопрос... А в Колибри какой вариант быстрее работать будет-то? Обход контура, или "перекрашивание" многократными проходами? И насколько?

UPD: Первый вариант в Firefox 4a9 -4 секунды, в Opera 10.60 - 2 секунды. Второй вариант в Firefox 4a9 - 3 секунды, Opera 10.60 - 4 секунды

Re: Алгоритмы заливки

Posted: Sat Jan 22, 2011 1:33 am
by SoUrcerer
Foldl wrote:А вот этот алгоритм не подойдет?
Наверное, его используют, но насколько я знаю, в Колибри его реализации не прижились - кушает много памяти и/или не очень быстро работает.
В принципе, алгоритм, который я использую, это один из представленных в wiki - scanline fill.

Re: Алгоритмы заливки

Posted: Thu Apr 07, 2011 9:19 am
by SoUrcerer
Не дает покоя мне идея реализации собственного формата шрифтов. Контур символа отобразить легко, а залить - трудно.
FreeType использует следующий алгоритм: выбирается произвольная точка на контуре, и мы по ней идем (вверх, допустим). Пока мы идем вверх - помещаем в ячейку памяти 1, как только начали идти вниз - помещаем 2. Возвращаемся в начальную точку, и заливаем все между 1 и 2:

Code: Select all

____1_____
___1*2____
__1***2___
__1****2__
___1111*2_
_______12_
Алгоритм отработает легко и быстро для прямых, но как только встречается кривая Безье, начинаются проблемы. Потому что для прямой определить, идет она вверх или вниз, можно, сравнив координаты Y начальной и конечной точек. С кривыми Безье так нельзя - потому что наивысшая точка кривой может не совпадать ни с начальной, ни с конечной, и, хуже того, может быть даже не посередине кривой.

Алгоритм растеризатора FreeType (грубо говоря) разбивает кривые Безье на десятки-сотни меньших "прямых", и работает только с "прямыми".

Мне такой подход не очень нравится, и вот что я придумал.

Кривая Безье задается уравнениями:
x=(1-t)^2 A + 2t(1-t) B + t^2 C
y=(1-t)^2 A + 2t(1-t) B + t^2 C
Если я закрашиваю буквы горизонтальными прямыми, я всегда знаю номер текущей строки (то есть y). Кроме того, я знаю A, B и С для всех кривых Безье.

Из уравнения y=(1-t)^2 A + 2t(1-t) B + t^2 C я могу найти t для данной кривой (это займет примерно 50-60 команд ассемблера), и, зная t, могу найти координаты x для текущей строки этой кривой (еще штук 20-30 команд). Эту операцию я провожу для всех кривых (да и прямых), и нахожу все точки контура в закрашиваемой строке. В обычных шрифтах в одном глифе примерно от 10 до 100 кривых.

Затем расставляю их по величине x, и заливаю между 1 и 2, между 3 и 4, между 5 и 6, и так далее. (Не знаю, сколько памяти и процессорного времени займет сортировка).

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

Что скажете? Будет ли работать быстро, или нет? Я прикидываю, что на моем компе с 4200 bogomips этот алгоритм сможет отрисовывать в памяти около 10-20 тысяч глифов высотой 24 пиксела в секунду.

Re: Алгоритмы заливки

Posted: Thu Apr 07, 2011 11:54 am
by popovpa
Здравствуйте!

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

Re: Алгоритмы заливки

Posted: Thu Apr 07, 2011 2:16 pm
by Foldl
Sorcerer
Что-то я не до конца понимаю. Если работать с растром -- то только с растром, а иначе все примитивы должны быть заданы аналитически. Что имеется? Кривые Безье и отрезки? Из них составляется замкнутый контур (фигура), необходимо этот контур залить. Тогда остается только научиться определять, лежит ли данная точка внутри фигуры или нет.

Если все так, то почти наверняка эта задача уже решена, и описана в литературе.

Прошу прощения, не знаю как организована информация в шрифтах.
Но вот допустим, разбили мы все это хозяйство на фигуры. Умеем определять, лежит ли точка внутри фигуры, тогда для буквы "А" вот такой:

Code: Select all

    _____
   /   __  \
  /   /_1\  \
 /    _ _     \
/_2/      \ __\

нужно закрасить все точки принадлежащие области 2 и одновременно не принадлежащие области 1.