5

I'd like to do a matplotlib contour or contourf plot of non-gridded 3D data (x, y, z) which is somehow C-shaped in x and y (see sketch) -- therefore part of the enclosing hull around the data is concave in x and y.

Usually I do plots of non-gridded 3D data by first interpolating it with

from matplotlib.mlab import griddata
griddata...

but this generates artifacts in the concave part of the data such that the concave part is filled by the interpolation.

Is it possible to do the interpolating or contourf/contour plots such that the concave part of the data is respected?

enter image description here

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
think nice things
  • 4,315
  • 3
  • 14
  • 12

2 Answers2

10

Masking by condition

Below is an example on how to use tricontourf with masking to obtain a concave shape without interpolated portions outside the data. It relies on the ability to mask the data depending on a condition.

import matplotlib.pyplot as plt
import matplotlib.tri as tri
import numpy as np

# create some data
rawx = np.random.rand(500)
rawy = np.random.rand(len(rawx))
cond01 = (rawx-1)**2 + rawy**2 <=1
cond02 = (rawx-0.7)**2 + rawy**2 >0.3
x = rawx[cond01 & cond02]
y = rawy[cond01 & cond02]
f = lambda x,y: np.sin(x*4)+np.cos(y)
z = f(x,y)
# now, x,y are points within a partially concave shape

triang0 = tri.Triangulation(x, y)
triang = tri.Triangulation(x, y)
x2 = x[triang.triangles].mean(axis=1) 
y2 = y[triang.triangles].mean(axis=1)
#note the very obscure mean command, which, if not present causes an error.
#now we need some masking condition.
# this is easy in this case where we generated the data according to the same condition
cond1 = (x2-1)**2 + y2**2 <=1
cond2 = (x2-0.7)**2 + (y2)**2 >0.3
mask = np.where(cond1 & cond2,0,1)
# apply masking
triang.set_mask(mask)


fig, (ax, ax2) = plt.subplots(ncols=2, figsize=(6,3))
ax.set_aspect("equal")
ax2.set_aspect("equal")

ax.tricontourf(triang0, z,  cmap="Oranges")
ax.scatter(x,y, s=3, color="k")

ax2.tricontourf(triang, z,  cmap="Oranges")
ax2.scatter(x,y, s=3, color="k")

ax.set_title("tricontourf without mask")
ax2.set_title("tricontourf with mask")
ax.set_xlim(0,1)
ax.set_ylim(0,1)
ax2.set_xlim(0,1)
ax2.set_ylim(0,1)

plt.show()

enter image description here

Masking by maximum side-length

If you do not have access to the exact condition, but have a maximum side-length (distance) between points, the following would be a solution. It would mask out all triangles for which at least one side is longer than some maximum distance. This can be well applied if the point density is rather high.

import matplotlib.pyplot as plt
import matplotlib.tri as tri
import numpy as np

# create some data
rawx = np.random.rand(500)
rawy = np.random.rand(len(rawx))
cond01 = (rawx-1)**2 + rawy**2 <=1
cond02 = (rawx-0.7)**2 + rawy**2 >0.3
x = rawx[cond01 & cond02]
y = rawy[cond01 & cond02]
f = lambda x,y: np.sin(x*4)+np.cos(y)
z = f(x,y)
# now, x,y are points within a partially concave shape

triang1 = tri.Triangulation(x, y)
triang2 = tri.Triangulation(x, y)
triang3 = tri.Triangulation(x, y)

def apply_mask(triang, alpha=0.4):
    # Mask triangles with sidelength bigger some alpha
    triangles = triang.triangles
    # Mask off unwanted triangles.
    xtri = x[triangles] - np.roll(x[triangles], 1, axis=1)
    ytri = y[triangles] - np.roll(y[triangles], 1, axis=1)
    maxi = np.max(np.sqrt(xtri**2 + ytri**2), axis=1)
    # apply masking
    triang.set_mask(maxi > alpha)

apply_mask(triang2, alpha=0.1)
apply_mask(triang3, alpha=0.3)

fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, figsize=(9,3))

ax1.tricontourf(triang1, z,  cmap="Oranges")
ax1.scatter(x,y, s=3, color="k")

ax2.tricontourf(triang2, z,  cmap="Oranges")
ax2.scatter(x,y, s=3, color="k")

ax3.tricontourf(triang3, z,  cmap="Oranges")
ax3.scatter(x,y, s=3, color="k")

ax1.set_title("tricontourf without mask")
ax2.set_title("with mask (alpha=0.1)")
ax3.set_title("with mask (alpha=0.3)")

for ax in (ax1, ax2, ax3):
    ax.set(xlim=(0,1), ylim=(0,1), aspect="equal")

plt.show()

enter image description here

As can be seen, finding the correct parameter (alpha) here is may need some tweaking.

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • 1
    thanks, this perfectly answers my question and solves my problem! It took a while for my case to generate an appropriate mask though. For my case as well as for your example it seems to be quite obvious for the eye how the mask should look like. Nevertheless you generated the mask with knowing the generating function. I wonder if it would be possible to generate a reasonable mask just from the data points w/o knowing about a function. – think nice things Mar 03 '17 at 20:15
  • Correct. I'm not sure if there is some generic algorithms to find the masking condition. Depending on the case, doing the masking might be easier or harder. Feel free not to accept this answer and see if someone else can come up with a better solution. – ImportanceOfBeingErnest Mar 04 '17 at 07:31
  • "tricontourf without mask" = peppered salmon fillet – Biggsy Jun 03 '19 at 14:46
1

The answer provided by TheImportanceOfBeingErnest gave me the perfect starting point and I have manipulated the code above to provide a general solution to the contour plot of a concave hull/ alpha shape. I use the Python package shapely to create a polygon of the concave hull. This is not a built-in function it needs to be added, which I have taken from this post. Once we have the polygon we simply check if the mean of the triangulated points are within the polygon and use this as our condition to form the mask.

import matplotlib.pyplot as plt
import matplotlib.tri as tri
import numpy as np
# Start Using SHAPELY
import shapely.geometry as geometry
from shapely.geometry import Polygon, MultiPoint, Point
from shapely.ops import triangulate
from shapely.ops import cascaded_union, polygonize
from scipy.spatial import Delaunay

from descartes.patch import PolygonPatch
import math

# create some data
rawx = np.random.rand(500)
rawy = np.random.rand(len(rawx))
cond01 = (rawx-1)**2 + rawy**2 <=1
cond02 = (rawx-0.7)**2 + rawy**2 >0.3
x = rawx[cond01 & cond02]
y = rawy[cond01 & cond02]
f = lambda x,y: np.sin(x*4)+np.cos(y)
z = f(x,y)
# now, x,y are points within a partially concave shape

triang0 = tri.Triangulation(x, y)
triang = tri.Triangulation(x, y)
# Function for finding an alpha function
def alpha_shape(points, alpha):
  """
  Compute the alpha shape (concave hull) of a set
  of points.
  @param points: Iterable container of points.
  @param alpha: alpha value to influence the
      gooeyness of the border. Smaller numbers
      don't fall inward as much as larger numbers.
      Too large, and you lose everything!
  """
  if len(points) < 4:
    # When you have a triangle, there is no sense
    # in computing an alpha shape.
    return geometry.MultiPoint(list(points)).convex_hull
  def add_edge(edges, edge_points, coords, i, j):
    """
    Add a line between the i-th and j-th points,
    if not in the list already
    """
    if (i, j) in edges or (j, i) in edges:
       # already added
       return
    edges.add( (i, j) )
    edge_points.append(coords[ [i, j] ])

  coords = np.array([point.coords[0]
                     for point in points])

tri = Delaunay(coords)
  edges = set()
  edge_points = []
  # loop over triangles:
  # ia, ib, ic = indices of corner points of the
  # triangle
  for ia, ib, ic in tri.vertices:
      pa = coords[ia]
      pb = coords[ib]
      pc = coords[ic]
      # Lengths of sides of triangle
      a = math.sqrt((pa[0]-pb[0])**2 + (pa[1]-pb[1])**2)
      b = math.sqrt((pb[0]-pc[0])**2 + (pb[1]-pc[1])**2)
      c = math.sqrt((pc[0]-pa[0])**2 + (pc[1]-pa[1])**2)
      # Semiperimeter of triangle
      s = (a + b + c)/2.0
      # Area of triangle by Heron's formula
      area = math.sqrt(s*(s-a)*(s-b)*(s-c))
      circum_r = a*b*c/(4.0*area)
      # Here's the radius filter.
      #print circum_r
      if circum_r < 1.0/alpha:
          add_edge(edges, edge_points, coords, ia, ib)
          add_edge(edges, edge_points, coords, ib, ic)
          add_edge(edges, edge_points, coords, ic, ia)
  m = geometry.MultiLineString(edge_points)
  triangles = list(polygonize(m))
  #import ipdb; ipdb.set_trace()
  return cascaded_union(triangles), edge_points

# create array of points from reduced exp data to convert to Polygon
crds=np.array( [x,y]).transpose()
# Adjust the length of acceptable sides by adjusting the alpha parameter
concave_hull, edge_points = alpha_shape(MultiPoint(crds), alpha=2.3)

# View the polygon and adjust alpha if needed
def plot_polygon(polygon):
    fig = plt.figure(figsize=(10,10))
    ax = fig.add_subplot(111)
    margin = .3
    x_min, y_min, x_max, y_max = polygon.bounds
    ax.set_xlim([x_min-margin, x_max+margin])
    ax.set_ylim([y_min-margin, y_max+margin])
    patch = PolygonPatch(polygon, fc='#999999',
                         ec='#000000', fill=True,
                         zorder=-1)
    ax.add_patch(patch)
    return fig
plot_polygon(concave_hull); plt.plot(x,y,'.'); #plt.show()


# Use the mean distance between the triangulated x & y poitns
x2 = x[triang.triangles].mean(axis=1)
y2 = y[triang.triangles].mean(axis=1)
##note the very obscure mean command, which, if not present causes an error.
##now we need some masking condition.

# Create an empty set to fill with zeros and ones
cond = np.empty(len(x2))
# iterate through points checking if the point lies within the polygon
for i in range(len(x2)):
  cond[i] = concave_hull.contains(Point(x2[i],y2[i]))

mask = np.where(cond,0,1)
# apply masking
triang.set_mask(mask)

fig, (ax, ax2) = plt.subplots(ncols=2, figsize=(6,3))
ax.set_aspect("equal")
ax2.set_aspect("equal")

ax.tricontourf(triang0, z,  cmap="Oranges")
ax.scatter(x,y, s=3, color="k")

ax2.tricontourf(triang, z,  cmap="Oranges")
ax2.scatter(x,y, s=3, color="k")

ax.set_title("tricontourf without mask")
ax2.set_title("tricontourf with mask")
ax.set_xlim(0,1)
ax.set_ylim(0,1)
ax2.set_xlim(0,1)
ax2.set_ylim(0,1)

plt.show()

tricontour with alpha shape mask

Mika Sundland
  • 18,120
  • 16
  • 38
  • 50