Элегантный / чистый (особый случай) алгоритм обхода прямой сетки?

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

Особым случаем здесь является то, что все начальная и конечная точки ограничены точным центром квадратов / ячеек.

Вот несколько примеров - с парами начальных и конечных точек выборки. Заштрихованные квадраты - это те, которые должны быть возвращены соответствующим вызовом функции.

удалена неработающая ссылка на ImageShack - пример

Начальная и конечная точки обозначаются квадратами, в которых они находятся. На приведенном выше рисунке, если предположить, что нижний левый угол - это [1,1], линия справа внизу будет идентифицирована как от [6,2] до [9,5].

То есть от (центра) квадрата в шестом столбце слева, во втором ряду снизу до (центра) квадрата на девятый столбец слева, в пятом ряду снизу

Что действительно не кажется таким сложным. Однако каким-то образом мне показалось, что я нашел в сети какой-то сложный алгоритм и реализовал его.

Я помню, что это было очень и очень быстро. Мол, оптимизировано для сотен или тысяч раз на кадр быстро.

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

Это вроде нормально в концепции, но фактическая реализация оказалась довольно не очень красивой, и я боюсь, что уровень оптимизации может быть слишком высоким для того, что мне практически нужно (я называю этот обход алгоритм может быть пять или шесть раз в минуту).

Есть ли простой, понятный и прозрачный алгоритм обхода прямой сетки?

С точки зрения программирования:

def traverse(start_point,end_point)
  # returns a list of all squares that this line would pass through
end

где заданные координаты определяют сами квадраты.

Некоторые примеры:

traverse([0,0],[0,4])
# => [0,0], [0,1], [0,2], [0,3], [0,4]
traverse([0,0],[3,2])
# => [0,0], [0,1], [1,1], [2,1], [2,2], [3,2]
traverse([0,0],[3,3])
# => [0,0], [1,1], [2,2], [3,3]

Обратите внимание, что линии, проходящие непосредственно через углы, не должны включать квадраты на «крыле» линии.

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

(Я пересматриваю алгоритмы Брезенхэма и Брезенхэма из-за моего недопонимания)


Для пояснения, одно из возможных применений этого было бы, если бы я хранил все свои объекты в игре внутри зон (сетка), и у меня есть луч, и я хочу видеть, каких объектов он касается. Используя этот алгоритм, я мог тестировать луч только для объектов, находящихся в заданных зонах, а не для каждого объекта на карте.

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

Обратите внимание, что на данный момент текущая реализация, которая у меня есть, действительно работает. Этот вопрос в основном из любопытства. Должен быть более простой способ ... как-нибудь ... для такой простой проблемы.


Что именно я ищу? Что-то концептуально / аккуратное и чистое. Кроме того, я понял, что из-за того, что я точно указываю, все начальные и конечные точки всегда будут в центре квадратов / ячеек; так что, возможно, что-то, что использует это в своих интересах, тоже было бы изящным.


person Justin L.    schedule 13.07.2010    source источник
comment
Где должны быть концы линии - углы квадратов (если да, то какие) или в центрах?   -  person a_m0d    schedule 13.07.2010
comment
@ a_m0d спасибо за указание на двусмысленность; Я добавил картинку для пояснения. Я имею в виду центры.   -  person Justin L.    schedule 13.07.2010
comment
спасибо за разъяснения и изображение - делает его намного яснее   -  person a_m0d    schedule 13.07.2010


Ответы (4)


Вам нужен частный случай суперпокрытия, которое представляет собой все пиксели, пересекаемые геометрическим объектом. Объект может быть линией или треугольником, и есть обобщения на более высокие измерения.

В любом случае, вот одна реализация для сегментов линии. На этой странице также сравнивается суперпокрытие с результатом алгоритма Брезенхема - они разные. alt text
(источник: free.fr)

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

Кстати, из вашего вопроса следует, что алгоритм Брезенхема неэффективен для больших сеток. Это неправда - он генерирует только пиксели на линии. Вам не нужно проводить проверку истинности / ложности для каждого пикселя сетки.

Обновление 1: я заметил, что на картинке есть два «лишних» синих квадрата, через которые, как мне кажется, линия не проходит. Один из них находится рядом с буквой «h» в «этом алгоритме». Я не знаю, отражает ли это ошибку в алгоритме или диаграмме (но см. Комментарий @kikito ниже).

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

Обновление 2: другая реализация .

person brainjam    schedule 13.07.2010
comment
+1 особенно за наблюдение о Брезенхэме. Bresenham - подходящий инструмент для этой работы (возможно, включая версию с антиалиасингом). - person Drew Hall; 13.07.2010
comment
Спасибо, что указали на это насчет Брезенхэма. Мое понимание этого было ошибочным (я думал, что это способ проверить, является ли пиксель частью данной линии). Но сейчас я ищу альтернативный алгоритм :) - person Justin L.; 13.07.2010
comment
Я просмотрел это; Честно говоря, я не верю, что алгоритм в статье имеет большую элегантность по сравнению с тем, который у меня уже был (см. ответ a_m0d), но я рассмотрю алгоритмы суперпокрытия. - person Justin L.; 13.07.2010
comment
@Justin L: Я согласен с тем, что алгоритм выглядит немного запутанным и не имеет простого объяснения. Но он избегает операций с плавающей запятой (что может испортить случаи, когда линия проходит точно через точку сетки). - person brainjam; 13.07.2010
comment
Я ожидал, что в случаях, когда линия проходит точно через точку сетки, должна образоваться пифагорова тройка. Тогда должно быть легко определить, имеет ли линия шанс пересечь точку сетки, определив, соответствует ли она известной тройке Пифагора, где значение гипотенузы не больше, чем длина линии. Я думаю, что эта проблема усугубляется тем фактом, что линии начинаются между точками сетки, а не между точками одной сетки. - person Kirk Broadhurst; 14.07.2010
comment
Я заметил, что на картинке есть два «лишних» синих квадрата, через которые, как мне кажется, линия не проходит, я думаю, что автор обращается к этому в конце страницы, где он говорит. Здесь мы предполагаем, что если линия проходит через угол прорисовываются оба квадрата. Если вы хотите удалить это, вы можете просто удалить часть else, относящуюся к углу. - person kikito; 15.11.2012
comment
Как уже указывал @DrewHall: также может использоваться модифицированная версия версии с антиалиасингом: en.wikipedia .org / wiki / Xiaolin_Wu% 27s_line_algorithm - person Karussell; 07.07.2014

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

Есть еще один документ, здесь, посвященный чему-то похожему.

Ссылки на оба этих документа приведены в части 4 Jakko Bikker превосходный учебные пособия по трассировке лучей (которые также включают его исходный код, чтобы вы могли просматривать / изучать его реализации).

person a_m0d    schedule 13.07.2010
comment
К сожалению, похоже, что первая статья - это в точности алгоритм, который я уже использую. Он все делает нормально; Я просто хотел бы найти, может быть, что-то более элегантное. Во второй статье я не нашел ничего подходящего = / но спасибо за предложения - person Justin L.; 13.07.2010
comment
@Justin - извините за это :( - каковы были шансы получить точно такую ​​же бумагу? - person a_m0d; 13.07.2010
comment
хаха наверное маленький. но это означает только то, что мы с тобой думали в одном направлении. - person Justin L.; 13.07.2010

There's a very easy algorithm for Your problem that runs in linear time:

  1. Даны две точки A и B, определите точки пересечения линии (A, B) с каждой вертикальной линией Вашей сетки, лежащей в этом интервале.
  2. Вставьте две специальные точки пересечения внутри ячеек, содержащих A и B, в начале / конце списка из точки 1.
  3. Интерпретируйте каждые две последовательные точки пересечения как минимальные и максимальные векторы выровненного по оси прямоугольника и отметьте все ячейки сетки, которые лежат внутри этого прямоугольника (это очень просто (пересечение двух выровненных по оси прямоугольников), особенно с учетом того, что прямоугольник имеет ширину из 1 и поэтому занимает только 1 столбец Вашей сетки)
Example:
+------+------+------+------+
|      |      |      |      |
|      |      | B    *      |
|      |      |/     |      |
+------+------*------+------+
|      |     /|      |      |
|      |    / |      |      |
|      |   /  |      |      |
+------+--/---+------+------+
|      | /    |      |      |
|      |/     |      |      |
|      *      |      |      |
+-----/+------+------+------+
|    / |      |      |      |
*   A  |      |      |      |
|      |      |      |      |
+------+------+------+------+ 

«A» и «B» - это точки, завершающие линию, обозначенную «/». «*» отмечает точки пересечения линии с сеткой. Две специальные точки пересечения необходимы для обозначения ячеек, содержащих A и B, и для обработки особых случаев, таких как A.x == B.x.

Оптимизированная реализация требует (| B.x - A.x | + | B.y - A.y |) времени для строки (A, B). Кроме того, можно написать этот алгоритм для определения точек пересечения с горизонтальными линиями сетки, если это будет проще для разработчика.

Обновление: пограничные дела

Как правильно указывает brainjam в своем ответе, трудными случаями являются те, когда линия проходит точно через точку сетки. Предположим, что такой случай имеет место, и арифметические операции с плавающей запятой правильно возвращают точку пересечения с целыми координатами. В этом случае предлагаемый алгоритм отмечает только правильные ячейки (как указано на изображении, предоставленном OP).

Однако ошибки с плавающей запятой рано или поздно возникнут и приведут к неверным результатам. На мой взгляд, даже использования double будет недостаточно и нужно переключиться на тип Decimal. Оптимизированная реализация будет выполнять (| max.x - min.x |) добавления к этому типу данных, каждое из которых занимает (log max.y) время. Это означает, что в худшем случае (строка ((0, 0), (N, N)) с огромным N (> 10 6) алгоритм деградирует до наихудшего случая O (N log N) Даже переключение между обнаружением пересечения вертикальной / горизонтальной линии сетки в зависимости от наклона линии (A, B) не помогает в этом худшем случае, но точно помогает в среднем случае - я бы только подумал о реализации такого переключитесь, если профилировщик считает, что Decimal операции являются узким местом.

Наконец, я могу представить, что некоторые умные люди могли бы придумать решение O (N), которое правильно работает с этими пограничными случаями. Все Ваши предложения приветствуются!

Исправление

brainjam указал, что десятичный тип данных неудовлетворителен, даже если он может представлять числа с плавающей запятой произвольной точности, поскольку, например, 1 / 3 не может быть представлен правильно. Поэтому следует использовать дробный тип данных, который должен правильно обрабатывать граничные случаи. Thx brainjam! :)

person Dave O.    schedule 13.07.2010
comment
+1. Хороший! Я думаю, что все эти алгоритмы, возможно, должны быть немного осторожны с ошибками с плавающей запятой, чтобы избежать проблем, когда пересечение находится точно в точке сетки (но ошибки делают его немного за точку сетки). - person brainjam; 13.07.2010
comment
@Justin L. LOL, я только что заметил, что алгоритм, который я предложил, тот же, на который ссылается a_m0d в своем ответе (я пропустил первый здесь), и что это тот, который Вы уже используете. Что вам не нравится в вашем алгоритме? Я могу представить себе красивую реализацию этого - к сожалению, не все может быть однострочным. Может, поможет какая-то абстракция ^^ или запуск кода golf xD. - person Dave O.; 13.07.2010
comment
На самом деле это концептуально отличается от статьи в ответе a_m0d; он проходит через квадраты, обращая внимание на пересечения горизонтальных и вертикальных осей. Но мне нравится обобщение, что он должен пересекать все квадраты между пересечениями y. - person Justin L.; 14.07.2010
comment
@ Дэйв, если вы имеете в виду тип Python Decimal, я не думаю, что он вам очень выгоден. 1/3 не может быть представлена ​​точно. Однако тип Python Fraction должен помочь. - person brainjam; 14.07.2010
comment
@brainjam Я имею в виду тип псевдодесятичного числа, который может представлять числа с плавающей запятой произвольной точности, например BigDecimal в Java. Но да, вы правы, дробный тип намного лучше подходит для этой задачи, так как он должен уметь обрабатывать пограничные случаи. - person Dave O.; 14.07.2010

Вот простая реализация на Python с использованием numpy. Однако здесь используются только 2D-векторы и покомпонентные операции, что довольно часто. Результат мне кажется довольно элегантным (~ 20 мест без комментариев).

Это не является общим, поскольку предполагается, что плитки центрируются в целочисленных координатах, а разделительные линии появляются на каждом целом числе плюс половина (0,5, 1,5, 2,5 и т. Д.). Это позволяет использовать округление для получения целочисленных координат тайла из мировой координаты (что на самом деле не требуется в вашем особом случае) и дает магическое число 0.5, чтобы определить, когда мы достигли последнего тайла.

Наконец, обратите внимание, что этот алгоритм не работает с точкой, точно пересекающей сетку на пересечении.

import numpy as np

def raytrace(v0, v1):
    # The equation of the ray is v = v0 + t*d
    d = v1 - v0
    inc = np.sign(d)  # Gives the quadrant in which the ray progress

    # Rounding coordinates give us the current tile
    tile = np.array(np.round(v0), dtype=int)
    tiles = [tile]
    v = v0
    endtile = np.array(np.round(v1), dtype=int)

    # Continue as long as we are not in the last tile
    while np.max(np.abs(endtile - v)) > 0.5:
        # L = (Lx, Ly) where Lx is the x coordinate of the next vertical
        # line and Ly the y coordinate of the next horizontal line
        L = tile + 0.5*inc

        # Solve the equation v + d*t == L for t, simultaneously for the next
        # horizontal line and vertical line
        t = (L - v)/d

        if t[0] < t[1]:  # The vertical line is intersected first
            tile[0] += inc[0]
            v += t[0]*d
        else:  # The horizontal line is intersected first
            tile[1] += inc[1]
            v += t[1]*d

        tiles.append(tile)

    return tiles
person Kolaru    schedule 09.07.2019