matplotlib.Path.contains_points: parámetro “radius” definido de manera inconsistente

Problema:

El parámetro de radio en la función includes_point en matplotlib.path se define de manera inconsistente. Esta función comprueba si un punto especificado está dentro o fuera de una ruta cerrada. El parámetro de radio se usa para hacer que la ruta sea un poco más pequeña / más grande (depende del signo del radio). De esta manera, los puntos pueden ser tomados dentro o fuera de cuenta, que están cerca de la ruta. El problema es que el signo de radio depende de la orientación de la ruta (en sentido horario o antihorario). La inconsistencia (en mi opinión) está ahí, porque la orientación de la ruta se ignora cuando se comprueba si un punto está dentro o fuera de la ruta. En un sentido matemático estricto se dice: todo lo que queda a lo largo del camino está incluido.

En breve:

Si la trayectoria está orientada en sentido contrario a las agujas del reloj, un radio positivo tiene más puntos en cuenta. Si la trayectoria se orienta en el sentido de las agujas del reloj, un radio positivo tiene en cuenta menos puntos.

Ejemplo:

En el siguiente ejemplo, se verifican 3 casos, cada uno para un recorrido en sentido horario y antihorario:

  1. Es un punto (cerca de la trayectoria) contenido con un radio positivo
  2. Es un punto (cerca del camino) contenido con un radio negativo
  3. Es el origen contenido (que está en el medio de ambos caminos)

Código:

import matplotlib.path as path import numpy as np verts=np.array([[-11.5, 16. ],[-11.5, -16. ],[ 11.5, -16. ],[ 11.5, 16. ],[-11.5, 16. ]]) ccwPath=path.Path(verts, closed=True) cwPath=path.Path(verts[::-1,:], closed=True) testPoint=[12,0] print('contains: ','|\t', '[12,0], radius=3','|\t', '[12,0], radius=-3','|\t', '[0,0]|') print('counterclockwise: ','|\t' ,'{0:>16s}'.format(str(ccwPath.contains_point(testPoint,radius=3) )),'|\t' ,'{0:>17s}'.format(str(ccwPath.contains_point(testPoint,radius=-3) )),'|\t' ,ccwPath.contains_point([0,0],radius=0) ,'|\t' ,'=> radius increases tolerance \t' ) print('clockwise: ','|\t' ,'{0:>16s}'.format(str(cwPath.contains_point(testPoint,radius=3) )),'|\t' ,'{0:>17s}'.format(str(cwPath.contains_point(testPoint,radius=-3) )),'|\t' ,cwPath.contains_point([0,0],radius=0) ,'|\t' ,'=> radius decreases tolerance \t' ) 

Salida:

 contains: | [12,0], radius=3 | [12,0], radius=-3 | [0,0]| counterclockwise: | True | False | True | => radius increases tolerance clockwise: | False | True | True | => radius decreases tolerance 

Solución para trayectos convexos:

La única idea que se me ocurrió es forzar el camino en una orientación contraria a las agujas del reloj y usar el radio de acuerdo con esto.

 import matplotlib.path as path import numpy as np verts=np.array([[-11.5, 16. ],[-11.5, -16. ],[ 11.5, -16. ],[ 11.5, 16. ],[-11.5, 16. ]]) #comment following line out to make isCounterClockWise crash #verts=np.array([[-11.5, 16. ],[-10,0],[-11.5, -16. ],[ 11.5, -16. ],[ 11.5, 16. ],[-11.5, 16. ]]) ccwPath=path.Path(verts, closed=True) cwPath=path.Path(verts[::-1,:], closed=True) testPoint=[12,0] def isCounterClockWise(myPath): #directions from on vertex to the other dirs=myPath.vertices[1:]-myPath.vertices[0:-1] #rot: array of rotations at ech edge rot=np.cross(dirs[:-1],dirs[1:]) if len(rot[rot>0])==len(rot): #counterclockwise return True elif len(rot[rot16s}'.format(str(ccwPath.contains_point(testPoint,radius=3) )),'|\t' ,'{0:>17s}'.format(str(ccwPath.contains_point(testPoint,radius=-3) )),'|\t' ,ccwPath.contains_point([0,0],radius=0) ,'|\t' ,'=> radius increases tolerance \t' ) print('forced ccw: ','|\t' ,'{0:>16s}'.format(str(cwPath.contains_point(testPoint,radius=3) )),'|\t' ,'{0:>17s}'.format(str(cwPath.contains_point(testPoint,radius=-3) )),'|\t' ,cwPath.contains_point([0,0],radius=0) ,'|\t' ,'=> radius increases tolerance \t' ) 

dando la siguiente salida:

 contains: | [12,0], radius=3 | [12,0], radius=-3 | [0,0]| counterclockwise: | True | False | True | => radius increases tolerance forced ccw: | True | False | True | => radius increases tolerance 

Un ejemplo, donde esta solución falla (para una ruta cóncava) se da en el comentario del código.

Mis preguntas:

  1. ¿Alguien sabe, por qué esta inconsistencia está ahí?
  2. ¿Hay una manera más elegante de evitar este problema? Los ejemplos pueden ser: usar otra biblioteca para canjea_contenida, usar el parámetro de radio de una manera más inteligente / adecuada o encontrar la orientación de una ruta con una función predefinida.

Creo que el único supuesto erróneo aquí es que “todo lo que queda a lo largo del camino está incluido”. . En su lugar, includes_point significa literalmente si una ruta cerrada incluye o no un punto.

El radius se define entonces para

  • ampliar el camino cuando el camino va en sentido contrario a las agujas del reloj y para
  • reducir el camino cuando el camino va hacia la derecha

Esto se muestra en el siguiente ejemplo, donde para un trazado (en sentido contrario a las agujas del reloj) se trazan los puntos incluidos en el área expandida / shunk. (rojo = not contains_point , azul = contains_point )

introduzca la descripción de la imagen aquí

 import matplotlib.pyplot as plt import matplotlib.path as path import matplotlib.patches as patches import numpy as np verts=np.array([[-1, 1 ],[-1, -1 ],[ 1, -1 ],[ 1, 0 ],[ 1, 1],[-1, 1 ]]) ccwPath=path.Path(verts, closed=True) cwPath=path.Path(verts[::-1,:], closed=True) paths = [ccwPath, cwPath] pathstitle = ["ccwPath", "cwPath"] radii = [1,-1] testPoint=(np.random.rand(400,2)-.5)*4 c = lambda p,x,r: p.contains_point(x,radius=r) fig, axes = plt.subplots(nrows=len(paths),ncols=len(radii)) for j in range(len(paths)): for i in range(len(radii)): ax = axes[i,j] r = radii[i] patch = patches.PathPatch(paths[j], fill=False, lw=2) ax.add_patch(patch) col = [c(paths[j], point[0], r) for point in zip(testPoint)] ax.scatter(testPoint[:,0], testPoint[:,1], c=col, s=8, vmin=0,vmax=1, cmap="bwr_r") ax.set_title("{}, r={}".format(pathstitle[j],radii[i]) ) plt.tight_layout() plt.show() 

Una particularidad, que no parece estar documentada en absoluto, es que el radius realidad expande o reduce la trayectoria en radius/2. . Esto se ve arriba ya que con un radio de 1 , se incluyen los puntos entre -1.5 y 1.5 lugar de los puntos entre -2 y 2 .

Con respecto a la orientación de un camino, puede que no haya una orientación fija. Si tiene 3 puntos, se puede determinar sin ambigüedad que la orientación es hacia la derecha, hacia la izquierda (o colineal). Una vez que tienes más puntos, el concepto de orientación no está bien definido.

Una opción puede ser verificar si la ruta es “mayormente en sentido contrario a las agujas del reloj”.

 def is_ccw(p): v = p.vertices-p.vertices[0,:] a = np.arctan2(v[1:,1],v[1:,0]) return (a[1:] >= a[:-1]).astype(int).mean() >= 0.5 

Esto permitiría entonces ajustar el radius en el caso de rutas “mayormente en el sentido de las agujas del reloj”,

 r = r*is_ccw(p) - r*(1-is_ccw(p)) 

de tal manera que un radio positivo siempre expande el camino y un radio negativo siempre lo reduce.

Ejemplo completo:

 import matplotlib.pyplot as plt import matplotlib.path as path import matplotlib.patches as patches import numpy as np verts=np.array([[-1, 1 ],[-1, -1 ],[ 1, -1 ],[ 1, 0 ],[ 1, 1],[-1, 1 ]]) ccwPath=path.Path(verts, closed=True) cwPath=path.Path(verts[::-1,:], closed=True) paths = [ccwPath, cwPath] pathstitle = ["ccwPath", "cwPath"] radii = [1,-1] testPoint=(np.random.rand(400,2)-.5)*4 c = lambda p,x,r: p.contains_point(x,radius=r) def is_ccw(p): v = p.vertices-p.vertices[0,:] a = np.arctan2(v[1:,1],v[1:,0]) return (a[1:] >= a[:-1]).astype(int).mean() >= 0.5 fig, axes = plt.subplots(nrows=len(radii),ncols=len(paths)) for j in range(len(paths)): for i in range(len(radii)): ax = axes[i,j] r = radii[i] isccw = is_ccw(paths[j]) r = r*isccw - r*(1-isccw) patch = patches.PathPatch(paths[j], fill=False, lw=2) ax.add_patch(patch) col = [c(paths[j], point[0], r) for point in zip(testPoint)] ax.scatter(testPoint[:,0], testPoint[:,1], c=col, s=8, vmin=0,vmax=1, cmap="bwr_r") ax.set_title("{}, r={} (isccw={})".format(pathstitle[j],radii[i], isccw) ) plt.tight_layout() plt.show() 

introduzca la descripción de la imagen aquí