Какой самый быстрый способ проверить, находится ли точка внутри многоугольника в python

Я нашел два основных метода, чтобы посмотреть, принадлежит ли точка внутри многоугольника. Один использует метод трассировки луча, используемый здесь, который является наиболее рекомендуемым ответом, а другой использует matplotlib path.contains_points (что кажется мне немного неясным). Мне придется постоянно проверять много очков. Кто-нибудь знает, если какой-либо из этих двух более рекомендуется, чем другой, или если есть еще лучшие варианты третьего?

ОБНОВИТЬ:

Я проверил два метода, и matplotlib выглядит намного быстрее.

from time import time
import numpy as np
import matplotlib.path as mpltPath

# regular polygon for testing
lenpoly = 100
polygon = [[np.sin(x)+0.5,np.cos(x)+0.5] for x in np.linspace(0,2*np.pi,lenpoly)[:-1]]

# random points set of points to test 
N = 10000
points = zip(np.random.random(N),np.random.random(N))


# Ray tracing
def ray_tracing_method(x,y,poly):

    n = len(poly)
    inside = False

    p1x,p1y = poly[0]
    for i in range(n+1):
        p2x,p2y = poly[i % n]
        if y > min(p1y,p2y):
            if y <= max(p1y,p2y):
                if x <= max(p1x,p2x):
                    if p1y != p2y:
                        xints = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
                    if p1x == p2x or x <= xints:
                        inside = not inside
        p1x,p1y = p2x,p2y

    return inside

start_time = time()
inside1 = [ray_tracing_method(point[0], point[1], polygon) for point in points]
print "Ray Tracing Elapsed time: " + str(time()-start_time)

# Matplotlib mplPath
start_time = time()
path = mpltPath.Path(polygon)
inside2 = path.contains_points(points)
print "Matplotlib contains_points Elapsed time: " + str(time()-start_time)

который дает,

Ray Tracing Elapsed time: 0.441395998001
Matplotlib contains_points Elapsed time: 0.00994491577148

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

Ответы

Ответ 1

Вы можете рассмотреть стройность:

from shapely.geometry import Point
from shapely.geometry.polygon import Polygon

point = Point(0.5, 0.5)
polygon = Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])
print(polygon.contains(point))

Из методов, о которых вы упоминали, я использовал только вторую, path.contains_points, и она отлично работает. В любом случае, в зависимости от точности, необходимой для вашего теста, я бы предложил создать сетку с множеством точек с нулевым значением, чтобы все узлы внутри полигона были истинными (False, если нет). Если вы собираетесь провести тест на множество точек, это может быть быстрее (хотя обратите внимание, что это полагает, что вы проводите тест в пределах "пиксельного" допуска):

from matplotlib import path
import matplotlib.pyplot as plt
import numpy as np

first = -3
size  = (3-first)/100
xv,yv = np.meshgrid(np.linspace(-3,3,100),np.linspace(-3,3,100))
p = path.Path([(0,0), (0, 1), (1, 1), (1, 0)])  # square with legs length 1 and bottom left corner at the origin
flags = p.contains_points(np.hstack((xv.flatten()[:,np.newaxis],yv.flatten()[:,np.newaxis])))
grid = np.zeros((101,101),dtype='bool')
grid[((xv.flatten()-first)/size).astype('int'),((yv.flatten()-first)/size).astype('int')] = flags

xi,yi = np.random.randint(-300,300,100)/100,np.random.randint(-300,300,100)/100
vflag = grid[((xi-first)/size).astype('int'),((yi-first)/size).astype('int')]
plt.imshow(grid.T,origin='lower',interpolation='nearest',cmap='binary')
plt.scatter(((xi-first)/size).astype('int'),((yi-first)/size).astype('int'),c=vflag,cmap='Greens',s=90)
plt.show()

результаты таковы:

point inside polygon within pixel tolerance

Ответ 2

Если вам нужна скорость и дополнительные зависимости не являются проблемой, вы можете найти numba весьма полезным (теперь его довольно просто установить на любой платформе). Предложенный вами классический подход ray_tracing можно легко перенести на numba, используя декоратор numba @jit и приведя многоangularьник к массиву пустышек. Код должен выглядеть примерно так:

@jit(nopython=True)
def ray_tracing(x,y,poly):
    n = len(poly)
    inside = False
    p2x = 0.0
    p2y = 0.0
    xints = 0.0
    p1x,p1y = poly[0]
    for i in range(n+1):
        p2x,p2y = poly[i % n]
        if y > min(p1y,p2y):
            if y <= max(p1y,p2y):
                if x <= max(p1x,p2x):
                    if p1y != p2y:
                        xints = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
                    if p1x == p2x or x <= xints:
                        inside = not inside
        p1x,p1y = p2x,p2y

    return inside

Первое выполнение займет немного больше времени, чем любой последующий вызов:

%%time
polygon=np.array(polygon)
inside1 = [numba_ray_tracing_method(point[0], point[1], polygon) for 
point in points]

CPU times: user 129 ms, sys: 4.08 ms, total: 133 ms
Wall time: 132 ms

Который после компиляции уменьшится до:

CPU times: user 18.7 ms, sys: 320 µs, total: 19.1 ms
Wall time: 18.4 ms

Если вам нужна скорость при первом вызове функции, вы можете предварительно скомпилировать код в модуле, используя pycc. Сохраните функцию в src.py как:

from numba import jit
from numba.pycc import CC
cc = CC('nbspatial')


@cc.export('ray_tracing',  'b1(f8, f8, f8[:,:])')
@jit(nopython=True)
def ray_tracing(x,y,poly):
    n = len(poly)
    inside = False
    p2x = 0.0
    p2y = 0.0
    xints = 0.0
    p1x,p1y = poly[0]
    for i in range(n+1):
        p2x,p2y = poly[i % n]
        if y > min(p1y,p2y):
            if y <= max(p1y,p2y):
                if x <= max(p1x,p2x):
                    if p1y != p2y:
                        xints = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
                    if p1x == p2x or x <= xints:
                        inside = not inside
        p1x,p1y = p2x,p2y

    return inside


if __name__ == "__main__":
    cc.compile()

Создайте его с помощью python src.py и запустите:

import nbspatial

import numpy as np
lenpoly = 100
polygon = [[np.sin(x)+0.5,np.cos(x)+0.5] for x in 
np.linspace(0,2*np.pi,lenpoly)[:-1]]

# random points set of points to test 
N = 10000
# making a list instead of a generator to help debug
points = zip(np.random.random(N),np.random.random(N))

polygon = np.array(polygon)

%%time
result = [nbspatial.ray_tracing(point[0], point[1], polygon) for point in points]

CPU times: user 20.7 ms, sys: 64 µs, total: 20.8 ms
Wall time: 19.9 ms

В коде numba я использовал:   'b1 (f8, f8, f8 [:,:])'

Для компиляции с nopython=True каждый var должен быть объявлен перед for loop.

В предварительно собранном коде src строка:

@cc.export('ray_tracing' , 'b1(f8, f8, f8[:,:])')

Используется для объявления имени функции и ее типов переменных ввода/вывода, логического выхода b1 и двух чисел с плавающей точкой f8 и двумерного массива чисел с плавающей точкой f8[:,:] в качестве входных данных.

Ответ 3

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

Более того, я полагаю, что вы измеряете метод matplotlib-внутри-polygon-метода vs ray, но matplotlib-somehow-optimized-iteration vs simple-list-итерация

Пусть N независимых сравнений (N пар точки и многоугольника)?

# ... your code...
lenpoly = 100
polygon = [[np.sin(x)+0.5,np.cos(x)+0.5] for x in np.linspace(0,2*np.pi,lenpoly)[:-1]]

M = 10000
start_time = time()
# Ray tracing
for i in range(M):
    x,y = np.random.random(), np.random.random()
    inside1 = ray_tracing_method(x,y, polygon)
print "Ray Tracing Elapsed time: " + str(time()-start_time)

# Matplotlib mplPath
start_time = time()
for i in range(M):
    x,y = np.random.random(), np.random.random()
    inside2 = path.contains_points([[x,y]])
print "Matplotlib contains_points Elapsed time: " + str(time()-start_time)

Результат:

Ray Tracing Elapsed time: 0.548588991165
Matplotlib contains_points Elapsed time: 0.103765010834

Matplotlib все еще намного лучше, но не в 100 раз лучше. Теперь попробуем гораздо более простой полигон...

lenpoly = 5
# ... same code

результат:

Ray Tracing Elapsed time: 0.0727779865265
Matplotlib contains_points Elapsed time: 0.105288982391

Ответ 4

Я просто оставлю это здесь, просто переписал приведенный выше код, используя numpy, может быть, кто-то найдет это полезным:

def ray_tracing_numpy(x,y,poly):
    n = len(poly)
    inside = np.zeros(len(x),np.bool_)
    p2x = 0.0
    p2y = 0.0
    xints = 0.0
    p1x,p1y = poly[0]
    for i in range(n+1):
        p2x,p2y = poly[i % n]
        idx = np.nonzero((y > min(p1y,p2y)) & (y <= max(p1y,p2y)) & (x <= max(p1x,p2x)))[0]
        if p1y != p2y:
            xints = (y[idx]-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
        if p1x == p2x:
            inside[idx] = ~inside[idx]
        else:
            idxx = idx[x[idx] <= xints]
            inside[idxx] = ~inside[idxx]    

        p1x,p1y = p2x,p2y
    return inside    

Обернутый ray_tracing в

def ray_tracing_mult(x,y,poly):
    return [ray_tracing(xi, yi, poly[:-1,:]) for xi,yi in zip(x,y)]

Проверено на 100000 баллов, результаты:

ray_tracing_mult 0:00:00.850656
ray_tracing_numpy 0:00:00.003769