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.
.contains_point
correctly. The documentation statesradius
allows the path to be made slightly larger or smaller. So it would be the path that is offset byradius
not the point. – Leftwardsccw + +r -> path expanded
,cw + -r -> path expanded
,ccw + -r -> path shrunk
,cw + +r -> path shrunk
are working as expected. However, I do see a problem in the case where you chose a radius of+0.9
. In this case, the point at (12,0) should be inside the (ccw) path; as 11.5+0.9=12.4 > 12), butcontains_point
returnsFalse
. So while something is indeed strange, the example from the question is not well suited to see this problem. – Leftwards