Problem:
The radius parameter in the function contains_point in matplotlib.path is defined inconsistently. This function checks if a specified point is inside or outside of a closed path. The radius parameter is used to make the path a little bit smaller/larger (dependent on the sign of radius). In this way, points can be taken into/out of account, which are close to the path. The problem is, that the sign of radius depends on the orientation of the path (clockwise or counterclockwise). The inconsistency (in my opinion) is there, because the orientation of path is ignored when checking if a point is inside or outside the path. In a mathematical strict sense one says: everything which is left along the path is included.
In short:
If the path is orientated counterclockwise, a positive radius takes more points into account. If the path is orientated clockwise, a positive radius takes less points into account.
Example:
In the following example there are 3 cases checked - each for a clockwise and a counterclockwise path:
- Is a point (close to path) contained with positive radius
- Is a point (close to path) contained with negative radius
- Is the origin contained (which is in the middle of both paths)
Code:
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'
)
Output:
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
Solution for convex paths:
The only idea I came up with, is to force the path into a counter-clockwise orientation and use radius according to this.
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[rot<0])==len(rot):
#clockwise
return False
else:
assert False, 'no yet implemented: This case applies if myPath is concave'
def forceCounterClockWise(myPath):
if not isCounterClockWise(myPath):
myPath.vertices=myPath.vertices[::-1]
forceCounterClockWise(cwPath)
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('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'
)
giving the following output:
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
An example, where this solution fails (for a concave path) is given in the comment of the code.
My questions:
- Does anyone know, why this inconsistency is there?
- Is there a more elegant way to circumvent this issue? Examples might be: using an other library for contains_point, using the radius parameter in a smarter/proper way or finding the orientation of a path with a predefined function.
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 toThis 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
)A particularity, which does not seem to be documented at all is that
radius
actually expands or shrinks the path byradius/2.
. This is seen above as with a radius of1
, points between-1.5
and1.5
are included instead of points between-2
and2
.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".
This would then allow to adjust the
radius
in case of "mostly clockwise" paths,such that a positive radius always expands the path and a negative radius always shrinks it.
Complete example: