Problem:
The radius parameter in the function contains_point in matplotlib.path is defined inconsistently. This function checks if a specified point
I think the only wrong assumption here is "everything which is left along the path is included.". Instead, contains_point
literally means whether or not a closed path includes a point.
The radius
is then defined to
This is shown in the following example, where for a (counter)clockwise path the points included in the expanded/shunk area are plotted. (red = not contains_point
, blue = contains_point
)
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()
A particularity, which does not seem to be documented at all is that radius
actually expands or shrinks the path by radius/2.
. This is seen above as with a radius of 1
, points between -1.5
and 1.5
are included instead of points between -2
and 2
.
Concerning the orientation of a path, there may not be one fix orientation. If you have 3 points, orientation can be unambiguously determined to be clockwise, counterclockwise (or colinear). Once you have more points, the concept of orientation is not well defined.
An option may be to check if the path is "mostly counterclockwise".
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
This would then allow to adjust the radius
in case of "mostly clockwise" paths,
r = r*is_ccw(p) - r*(1-is_ccw(p))
such that a positive radius always expands the path and a negative radius always shrinks it.
Complete example:
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()