
Поиск контуров

Поиск перебором

- Для простоты, удваиваем количество отрезков, добавляя для каждого отрезка его обратно направленного клона (это позволит нам ходить по отрезкам как по векторам, без необходимости ходить по отрезкам «назад»).
- Последовательно берём каждый отрезок за начальный для искомого контура, помещаем его в новый массив и вызываем рекурсивную процедуру поиска (ниже).
- Перебираем все отрезки ища такой, который своим началом стыкуется к последнему найденному и его конечная точка ещё не встречалась среди точек отрезков в массиве, и добавляем его в конец массива.
- Если конец этого отрезка совпадает с началом первого отрезка массива — значит мы нашли контур — добавляем массив отрезков в массив найденных контуров и выходим из рекурсии.
- Если нет — рекурсивно вызываем процедуру поиска следующего отрезка.

Направленный поиск выбором «крайнего» отрезка

Тут стоит упомянуть, что хотя мы идём по часовой стрелке, иногда мы будем находить и контуры с левым обходом (против часовой стрелки), если стартовый вектор-отрезок не принадлежит ни одному контуру с правым обходом. Причём эти левые контуры могут быть составными (На рисунке: вектор AB принадлежит правому контуру ABFDC, а вот вектор BA — составному левому контуру BACDGF.) А значит, нам потребуется исключить все левые контуры из результирующего массива.
Вот как определить, является ли контур правым (здесь (x1,y2) — начальная точка отрезка, (x3,y3) — конечная):
function isClockwise()
{
var sum = 0.0;
for (edge in edges)
{
sum += (edge.x3 - edge.x1) * (edge.y3 + edge.y1);
}
return sum >= 0;
}
Получившийся алгоритм работает уже с приемлемой скоростью (порядка O(n^2), на сколько я понимаю; поправьте, если ошибаюсь) и отлично себя показывает на реальных задачах. А мы идём дальше.
Выявление полигонов по найденным контурам

- Берём последовательно каждый контур за внешний контур будущего полигона.
- Среди оставшихся контуров ищем те, которые лежат внутри нашего внешнего контура.
- Берём точку, лежащую внутри внешнего контура, но за пределами его «дырок».
- На исходном изображении берём заливку в данной точке (т.е. ищем полигон, которому она принадлежит) и если такой полигон находится — добавляем внешний контур+«дырки» как новый полигон в массив найденных для исходного изображения.
- Аналогично для конечного изображения (ищем полигон, которому принадлежит точка и если находится — добавляем новый полигон в массив найденных для конечного изображения).
Таким образом мы «реконструируем» исходное и конечное изображение после пересечения всех отрезков. Причём теперь мы гарантированно имеем, что каждый из полигонов исходного изображения либо совпадает (в смысле координат) с таким же полигоном накладываемого изображения, либо выступает за пределы всех его полигонов.
Думаю, стоит обратить особое внимание на п.3. Как взять хорошую точку внутри контура за пределами его внутренних контуров? Для начала, нам нужно понимать, что точка тогда лежит внутри полигона, когда выпущенный из неё в любом направлении луч пересекается с отрезками полигона нечётное число раз. И всё бы хорошо, только вот из-за ограниченной точности вычислений луч может попасть в место соединения отрезков, что может вызвать казусы (детектирование пересечений с обоими отрезками, либо с одним, либо ни с одним вообще). Чаще всего, для упрощения вычислений, луч выпускают горизонтально. Также будем делать и мы. Вот что мы теперь знаем о хорошей точке:
- она не должна совпадать ни с одним из концов отрезков, составляющих полигон (и чем больше не совпадает — тем лучше);
- желательно, чтобы точка лежала подальше от отрезков полигона, чтобы уменьшить вероятность проблем, связанных с детектированием пересечений выпущенного из неё луча с этими самыми отрезками.

- Загоняем в массив все y-координаты концов отрезков, составляющих полигон.
Добавляем в него minY и maxY нашего внешнего контура (т.к. эти значения не всегда совпадают со значениями в концах отрезков — см. рис. справа).
- Сортируем массив по возрастанию.
- Ищем в массиве последовательную пару значений (y1,y2) для которых разность y2 — y1 максимальна.
- Принимаем среднее арифметическое этих значений за координату y искомой точки: y = (y1 + y2) / 2.
Осталось найти удобное нам значение координаты x. Для этого:

- Выпускаем горизонтальный луч из точки (-бесконечность; y) вправо.
- Находим все координаты x точек пересечения луча с отрезками полигона;
- Сортируем их по возрастанию.
- Выбираем среди пар (x[i], x[i+1]), где i=0,2,4... такую, для которой разница x[i+1] — x[i] максимальна.
- Берём среднее арифметическое этой пары за значение координаты x искомой точки: x = (x[i] + x[i+1]) / 2.
Полученный алгоритм не идеален, но достаточно прост и, как показывает практика, хорошо работает на реальных задачах.
Последние шаги
В рамках статьи мы не будем рассматривать подробно оставшиеся пункты алгоритма поиска объединения изображений:
- удаление всех рёбер исходного изображения, которые целиком попали внутрь закрашенных областей накладываемого изображения;
- удаление из исходного изображения всех полигонов, имеющихся в накладываемом изображении без учёта цвета;
- добавление к рёбрам и полигонам исходного изображения всех рёбер и полигонов накладываемого изображения (при этом совпадающие рёбра исходного изображения затираются рёбрами накладываемого);
- объединение на исходном изображении примыкающих друг к другу полигонов, имеющих одинаковую закраску и не имеющих явного разделения рёбрами.
Полагаю, что эти подзадачи достаточно тривиальны и читатели без труда (при необходимости) разберутся с этим самостоятельно.
Заключение
В статье я постарался показать ряд интересных моментов, связанных с поиском объединения векторных изображений. Некоторые особенности остались за кадром, т.к. они либо слишком мелкие, либо автор просто про них забыл. Буду рад любым уточнениям как по статье, так и по описанным алгоритмам.
sysprg
Для бесплатных редакторов в web такой подход работает. Но… Все алгоритмы, которые добавляют синтетические точки пересечений с рассчитанными в числах координатами (на промежуточных стадиях вычислений) по большому счету ламерские. Если в сложной закрашенной фигуре при таком подходе сделать очень сильный zoom вблизи рассчитанной точки пересечения ребер, то мы сможем увидеть не закрашенные щели или прорисовку узких фрагментов одной фигуры поверх другой. Это происходит из-за того, что многие решения уравнений (особенно с кривыми Безье) нельзя представить числами с фиксированной (и сравнительно небольшой) разрядностью без потери точности. И эта потеря точности вылезает наружу при сильных zoom-ах. Чтобы победить это, нужно откладывать все вычисления точных координат промежуточных точек до самого последнего этапа, когда будет делаться окончательная отрисовка (растеризация в пикселы). Но тогда промежуточные вычисления и анализ становятся намного более сложными — придется применять ряд подходов, которые используются в системах машинной математики. Например — рациональные числа вместо float, операции над векторами или кватернионы, и т.п. Просто решения через вычисления в float уравнений из школьной алгебры и геометрии будут давать артефакты при сильных zoom-ах (видимые даже в браузерах типа IE при загрузке в них SVG полученных через такие промежуточные вычисления).
yar3333 Автор
Хорошее замечание!

1) Думаю, что и для не бесплатных такой подход тоже работает (Flash Pro). Ибо так уж важны ли очень большие зумы для игровой 2D графики?
2) Подозреваю, что во многом поэтому во Flash существует gaps (т.е. округление всех координат до 1/20=0.05 пикселя). Аналогичное округление приходится делать и мне в NanoFL (до 1/100 = 0.01). А примитивы, схлопнувшиеся до точки — просто отбрасывать. Таким образом, сильно уменьшается вероятность того, что где-то окажется не закрашенная область — ибо мне гарантируются минимальные размеры любой области сильно больше точности вычислений (которая определяется длиной float в лучшем случае и заданной точностью вычислений в худшем (для итеративных численных методов)).
3) Буду рад, если вы приложите ссылку на такой SVG файл. Дело в том, что как раз сейчас я занимаюсь плагином SVG для NanoFL. Среди 200 протестированных файлов мне не удавалось обнаружить артефактов «на глаз». Есть, правда, файл accessible.svg, про который я думал, что там ошибка. Оказалось, что это так и задумано для целей тестирования. Вот картинка: