Mask by contour line in matplotlib - python

A user case: given a signed distance field phi, the contour phi = 0 marks the surface of a geometry, and regions inside the geometry have phi < 0. In some case, one wants to focus on values inside the geometry and only plot regions inside the geometry, i.e., regions masked by phi < 0.
Note: directly masking the array phi causes zig-zag boundary near the contour line phi = 0, i.e., bad visualization.
I was able to write the following code with the answer here: Fill OUTSIDE of polygon | Mask array where indicies are beyond a circular boundary? the function mask_outside_polygon below is from that post. My idea is to extract and use the coordinate of the contour line for creating a polygon mask.
The code works well when the contour line does not intersect the boundary of the figure. There is no zig-zag boundary so it's a good visualization.
But when the contour intersects with the figure boundary, the contour line is fragmented into pieces and the simple code no longer works. I wonder if there is some existing feature for masking the figure, or there is some simpler method I can use. Thanks!
import numpy as np
import matplotlib.pyplot as plt
def mask_outside_polygon(poly_verts, ax=None):
"""
Plots a mask on the specified axis ("ax", defaults to plt.gca()) such that
all areas outside of the polygon specified by "poly_verts" are masked.
"poly_verts" must be a list of tuples of the verticies in the polygon in
counter-clockwise order.
Returns the matplotlib.patches.PathPatch instance plotted on the figure.
"""
import matplotlib.patches as mpatches
import matplotlib.path as mpath
if ax is None:
ax = plt.gca()
# Get current plot limits
xlim = ax.get_xlim()
ylim = ax.get_ylim()
# Verticies of the plot boundaries in clockwise order
bound_verts = [(xlim[0], ylim[0]), (xlim[0], ylim[3]),
(xlim[3], ylim[3]), (xlim[3], ylim[0]),
(xlim[0], ylim[0])]
# A series of codes (1 and 2) to tell matplotlib whether to draw a line or
# move the "pen" (So that there's no connecting line)
bound_codes = [mpath.Path.MOVETO] + (len(bound_verts) - 1) * [mpath.Path.LINETO]
poly_codes = [mpath.Path.MOVETO] + (len(poly_verts) - 1) * [mpath.Path.LINETO]
# Plot the masking patch
path = mpath.Path(bound_verts + poly_verts, bound_codes + poly_codes)
patch = mpatches.PathPatch(path, facecolor='white', edgecolor='none')
patch = ax.add_patch(patch)
# Reset the plot limits to their original extents
ax.set_xlim(xlim)
ax.set_ylim(ylim)
return patch
def main():
x = np.linspace(-1.2, 1.2, 101)
y = np.linspace(-1.2, 1.2, 101)
xx, yy = np.meshgrid(x, y)
rr = np.sqrt(xx**2 + yy**2)
psi = xx*xx - yy*yy
plt.contourf(xx,yy,psi)
if 0: # change to 1 to see the working result
cs = plt.contour(xx,yy,rr,levels=[3]) # works
else:
cs = plt.contour(xx,yy,rr,levels=[1.3]) # does not work
path = cs.collections[0].get_paths()[0]
poly_verts = path.vertices
mask_outside_polygon(poly_verts.tolist()[::-1])
plt.show()
if __name__ == '__main__':
main()

Related

Skimage.draw.ellipse generates two undesired lines

I have a boolean image, where the zeros are the background, and I want to plot the ellipse that encloses the major and minor axis of an object retrieved from skimage.measure.regionprops. The module skimage.draw.ellipse_perimeter generates the expected ellipse but also two undesired lines.
Code (the input image is here):
import skimage
import skimage.draw
from skimage.measure import label
from skimage.measure import regionprops
import matplotlib.pyplot as plt
# load example image
TP_mask = plt.imread('https://i.stack.imgur.com/UYLE0.png')
# connect region with same integer value
region = label(TP_mask)
# obtain RegionProperties
props = regionprops(region)
props = props[0]
# define centroid
y0,x0 = props.centroid
# draw ellipse perimeter
rr,cc = skimage.draw.ellipse_perimeter(int(x0),int(y0),int(props.minor_axis_length*0.5),int(props.major_axis_length*0.5), orientation = props.orientation)
# plot
plt.plot(rr,cc, color = 'yellow')
plt.imshow(TP_mask, cmap = 'gray')
plt.show()
However, if I create a simplified example as follows, I obtain the expected ellipse. Could someone help me understand what am I doing wrong?
import numpy as np
img = np.zeros((1000,1000))
img[200:800,200:400] = 1
region = label(img)
props = regionprops(region)
props = props[0]
y0,x0 = props.centroid
rr,cc = skimage.draw.ellipse_perimeter(int(x0),int(y0),int(props.minor_axis_length*0.5),int(props.major_axis_length*0.5), orientation = props.orientation)
plt.plot(rr,cc, color = 'yellow')
plt.imshow(img, cmap = 'gray')
plt.show()
It turns out that the coordinates returned by the draw module are designed to index into an array, as shown in this example, rather than plot:
rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=math.pi / 4.)
img[rr, cc, :] = (1, 0, 1)
To use plt.plot and do a line plot, the coordinates need to be sorted as they go around the circle/ellipse. They are not properly sorted by default because the ellipse is actually drawn in four separate quadrants, which you can find by looking at the relevant part of the source code. (A clue: the lines hit exactly where the ellipse is vertical or horizontal.)
Since you have a convex surface, computing the angle between each point and the centre of the ellipse is enough to sort the points. Try the following:
fig, ax = plt.subplots()
_ = ax.imshow(TP_mask, cmap='gray')
angle = np.arctan2(rr - np.mean(rr), cc - np.mean(cc))
sorted_by_angle = np.argsort(angle)
rrs = rr[sorted_by_angle]
ccs = cc[sorted_by_angle]
_ = ax.plot(rrs, ccs, color='red')
Which gives:

How to fix limits after multiple use of ax.voxels method in python

I'm doing a research with 3D point clouds that I receive from Lidar. I split huge amount of points (up to 10 - 100 millions) into cubes, investigate their position and display results in a seperate voxels using Axes3D.voxels method. However, I face some problems while setting appropriate limits of Axes3D after multiple use of this method.
I define add_voxels function in order to display voxels immediately from np.array of positions of cubes inputted:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import itertools
def add_voxels(true_ids, ax):
shape_of_filled = true_ids.max(axis=0) + 1 # shape of building
filled = np.zeros(shape_of_filled)
for n in true_ids:
filled[n] = 1
x, y, z = np.indices(np.array(shape_of_filled) + 1)
return ax.voxels(x,y,z, filled)```
Then use it to plot my two clouds of cubes:
fig = plt.gcf() # get a reference to the current figure instance
ax = fig.gca(projection='3d') # get a reference to the current axes instance
cubecloud1 = np.array(list(itertools.product(range(2,4), range(2,4), range(2,4))))
cubecloud2 = np.array(list(itertools.product(range(4,7), range(4,7), range(4,7))))
add_voxels(cubecloud2, ax)
add_voxels(cubecloud1, ax)
plt.show()
It results in bad limits of display of voxel's position:
I'd like to have all the components displayed in a correct bounding box like this:
Or, at least, this (assuming bounding box includes invisible voxels too):
I could only make this work by setting the axis limits explicitly:
# [...]
faces2 = add_voxels(cubecloud2, ax)
faces1 = add_voxels(cubecloud1, ax)
points = list(faces1.keys()) + list(faces2.keys())
data = list(zip(*points))
xmin = min(data[0])
xmax = max(data[0])
ymin = min(data[1])
ymax = max(data[1])
zmin = min(data[2])
zmax = max(data[2])
ax.set_xlim3d(xmin, xmax)
ax.set_ylim3d(ymin, ymax)
ax.set_zlim3d(zmin, zmax)
plt.show()

How to draw "two directions widths line" in matplotlib

How to use matplotlib or pyqtgraph draw plot like this:
Line AB is a two-directions street, green part represents the direction from point A to point B, red part represents B to A, width of each part represents the traffic volume. Widths are measured in point, will not changed at different zoom levels or dpi settings.
This is only an example, in fact I have hunderds of streets. This kind of plot is very common in many traffic softwares. I tried to use matplotlib's patheffect but result is frustrated:
from matplotlib import pyplot as plt
import matplotlib.patheffects as path_effects
x=[0,1,2,3]
y=[1,0,0,-1]
ab_width=20
ba_width=30
fig, axes= plt.subplots(1,1)
center_line, = axes.plot(x,y,color='k',linewidth=2)
center_line.set_path_effects(
[path_effects.SimpleLineShadow(offset=(0, -ab_width/2),shadow_color='g', alpha=1, linewidth=ab_width),
path_effects.SimpleLineShadow(offset=(0, ba_width/2), shadow_color='r', alpha=1, linewidth=ba_width),
path_effects.SimpleLineShadow(offset=(0, -ab_width), shadow_color='k', alpha=1, linewidth=2),
path_effects.SimpleLineShadow(offset=(0, ba_width), shadow_color='k', alpha=1, linewidth=2),
path_effects.Normal()])
axes.set_xlim(-1,4)
axes.set_ylim(-1.5,1.5)
One idea came to me is to take each part of the line as a standalone line, and recalculate it's position when changing zoom level, but it's too complicated and slow.
If there any easy way to use matplotlib or pyqtgraph draw what I want? Any suggestion will be appreciated!
If you can have each independent line, this can be done easily with the fill_between function.
from matplotlib import pyplot as plt
import numpy as np
x=np.array([0,1,2,3])
y=np.array([1,0,0,-1])
y1width=-1
y2width=3
y1 = y + y1width
y2 = y + y2width
fig = plt.figure()
ax = fig.add_subplot(111)
plt.plot(x,y, 'k', x,y1, 'k',x,y2, 'k',linewidth=2)
ax.fill_between(x, y1, y, color='g')
ax.fill_between(x, y2, y, color='r')
plt.xlim(-1,4)
plt.ylim(-3,6)
plt.show()
Here I considered the center line as the reference (thus the negative y1width), but could be done differently. The result is then:
If the lines are 'complicated', eventually intersecting at some point, then the keyword argument interpolate=True must be used to fill the crossover regions properly. Another interesting argument probably useful for your use case is where, to condition the region, for instance, where=y1 < 0. For more information you can check out the documentation.
One way of solving your issue is using filled polygons, some linear algebra and some calculus. The main idea is to draw a polygon along your x and y coordinates and along shifted coordinates to close and fill the polygon.
These are my results:
And here is the code:
from __future__ import division
import numpy
from matplotlib import pyplot, patches
def road(x, y, w, scale=0.005, **kwargs):
# Makes sure input coordinates are arrays.
x, y = numpy.asarray(x, dtype=float), numpy.asarray(y, dtype=float)
# Calculate derivative.
dx = x[2:] - x[:-2]
dy = y[2:] - y[:-2]
dy_dx = numpy.concatenate([
[(y[1] - y[0]) / (x[1] - x[0])],
dy / dx,
[(y[-1] - y[-2]) / (x[-1] - x[-2])]
])
# Offsets the input coordinates according to the local derivative.
offset = -dy_dx + 1j
offset = w * scale * offset / abs(offset)
y_offset = y + w * scale
#
AB = zip(
numpy.concatenate([x + offset.real, x[::-1]]),
numpy.concatenate([y + offset.imag, y[::-1]]),
)
p = patches.Polygon(AB, **kwargs)
# Returns polygon.
return p
if __name__ == '__main__':
# Some plot initializations
pyplot.close('all')
pyplot.ion()
# This is the list of coordinates of each point
x = [0, 1, 2, 3, 4]
y = [1, 0, 0, -1, 0]
# Creates figure and axes.
fig, ax = pyplot.subplots(1,1)
ax.axis('equal')
center_line, = ax.plot(x, y, color='k', linewidth=2)
AB = road(x, y, 20, color='g')
BA = road(x, y, -30, color='r')
ax.add_patch(AB)
ax.add_patch(BA)
The first step in calculating how to offset each data point is by calculating the discrete derivative dy / dx. I like to use complex notation to handle vectors in Python, i.e. A = 1 - 1j. This makes life easier for some mathematical operations.
The next step is to remember that the derivative gives the tangent to the curve and from linear algebra that the normal to the tangent is n=-dy_dx + 1j, using complex notation.
The final step in determining the offset coordinates is to ensure that the normal vector has unity size n_norm = n / abs(n) and multiply by the desired width of the polygon.
Now that we have all the coordinates for the points in the polygon, the rest is quite straightforward. Use patches.Polygon and add them to the plot.
This code allows you also to define if you want the patch on top of your route or below it. Just give a positive or negative value for the width. If you want to change the width of the polygon depending on your zoom level and/or resolution, you adjust the scale parameter. It also gives you freedom to add additional parameters to the patches such as fill patterns, transparency, etc.

color matplotlib map using bicubic interpolation

I know that matplotlib and scipy can do bicubic interpolation:
http://matplotlib.org/examples/pylab_examples/image_interp.html
http://docs.scipy.org/doc/scipy/reference/tutorial/interpolate.html
http://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp2d.html
I also know that it is possible to draw a map of the world with matplotlib:
http://matplotlib.org/basemap/users/geography.html
http://matplotlib.org/basemap/users/examples.html
http://matplotlib.org/basemap/api/basemap_api.html
But can I do a bicubic interpolation based on 4 data points and only color the land mass?
For example using these for 4 data points (longitude and latitude) and colors:
Lagos: 6.453056, 3.395833; red HSV 0 100 100 (or z = 0)
Cairo: 30.05, 31.233333; green HSV 90 100 100 (or z = 90)
Johannesburg: -26.204444, 28.045556; cyan HSV 180 100 100 (or z = 180)
Mogadishu: 2.033333, 45.35; purple HSV 270 100 100 (or z = 270)
I am thinking that it must be possible to do the bicubic interpolation across the range of latitudes and longitudes and then add oceans, lakes and rivers on top of that layer? I can do this with drawmapboundary. Actually there is an option maskoceans for this:
http://matplotlib.org/basemap/api/basemap_api.html#mpl_toolkits.basemap.maskoceans
I can interpolate the data like this:
xnew, ynew = np.mgrid[-1:1:70j, -1:1:70j]
tck = interpolate.bisplrep(x, y, z, s=0)
znew = interpolate.bisplev(xnew[:,0], ynew[0,:], tck)
Or with scipy.interpolate.interp2d:
http://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp2d.html
Here it is explained how to convert to map projection coordinates:
http://matplotlib.org/basemap/users/mapcoords.html
But I need to figure out how to do this for a calculated surface instead of individual points. Actually there is an example of such a topographic map using external data, which I should be able to replicate:
http://matplotlib.org/basemap/users/examples.html
P.S. I am not looking for a complete solution. I would much prefer to solve this myself. Rather I am looking for suggestions and hints. I have been using gnuplot for more than 10 years and only switched to matplotlib within the past few weeks, so please don't assume I know even the simplest things about matplotlib.
I think this is what you are looking for (roughly). Note the crucial things are masking the data array before you plot the pcolor and passing in the hsv colormap (Docs: cmap parameter for pcolormesh and available colormaps).
I've kept the code for plotting the maps quite close to the examples so it should be easy to follow. I've kept your interpolation code for the same reason. Note that the interpolation is linear rather than cubic - kx=ky=1 - because you don't give enough points to do cubic interpolation (you'd need at least 16 - scipy will complain with less saying that "m must be >= (kx+1)(ky+1)", although the constraint is not mentioned in the documentation).
I've also extended the range of your meshgrid and kept in lat / lon for x and y throughout.
Code
from mpl_toolkits.basemap import Basemap,maskoceans
import matplotlib.pyplot as plt
import numpy as np
from scipy import interpolate
# set up orthographic map projection with
# perspective of satellite looking down at 0N, 20W (Africa in main focus)
# use low resolution coastlines.
map = Basemap(projection='ortho',lat_0=0,lon_0=20,resolution='l')
# draw coastlines, country boundaries
map.drawcoastlines(linewidth=0.25)
map.drawcountries(linewidth=0.25)
# Optionally (commented line below) give the map a fill colour - e.g. a blue sea
#map.drawmapboundary(fill_color='aqua')
# draw lat/lon grid lines every 30 degrees.
map.drawmeridians(np.arange(0,360,30))
map.drawparallels(np.arange(-90,90,30))
data = {'Lagos': (6.453056, 3.395833,0),
'Cairo': (30.05, 31.233333,90),
'Johannesburg': (-26.204444, 28.045556,180),
'Mogadishu': (2.033333, 45.35, 270)}
x,y,z = zip(*data.values())
xnew, ynew = np.mgrid[-30:60:0.1, -50:50:0.1]
tck = interpolate.bisplrep(x, y, z, s=0,kx=1,ky=1)
znew = interpolate.bisplev(xnew[:,0], ynew[0,:], tck)
znew = maskoceans(xnew, ynew, znew)
col_plot = map.pcolormesh(xnew, ynew, znew, latlon=True, cmap='hsv')
plt.show()
Output
Observe that doing the opposite, that is putting a raster on the sea and lay a mask over the continents, is easy as pie. Simply use map.fillcontinents(). So the basic idea of this solution is to modify the fillcontinents function so that it lays polygons over the oceans.
The steps are:
Create a large circle-like polygon that covers the entire globe.
Create a polygon for each shape in the map.coastpolygons array.
Cut the shape of the landmass polygon away from the circle using shapely and its difference method.
Add the remaining polygons, which have the shape of the oceans, on the top, with a high zorder.
The code:
from mpl_toolkits.basemap import Basemap
import numpy as np
from scipy import interpolate
from shapely.geometry import Polygon
from descartes.patch import PolygonPatch
def my_circle_polygon( (x0, y0), r, resolution = 50 ):
circle = []
for theta in np.linspace(0,2*np.pi, resolution):
x = r * np.cos(theta) + x0
y = r * np.sin(theta) + y0
circle.append( (x,y) )
return Polygon( circle[:-1] )
def filloceans(the_map, color='0.8', ax=None):
# get current axes instance (if none specified).
if not ax:
ax = the_map._check_ax()
# creates a circle that covers the world
r = 0.5*(map.xmax - map.xmin) # + 50000 # adds a little bit of margin
x0 = 0.5*(map.xmax + map.xmin)
y0 = 0.5*(map.ymax + map.ymin)
oceans = my_circle_polygon( (x0, y0) , r, resolution = 100 )
# for each coastline polygon, gouge it out of the circle
for x,y in the_map.coastpolygons:
xa = np.array(x,np.float32)
ya = np.array(y,np.float32)
xy = np.array(zip(xa.tolist(),ya.tolist()))
continent = Polygon(xy)
## catches error when difference with lakes
try:
oceans = oceans.difference(continent)
except:
patch = PolygonPatch(continent, color="white", zorder =150)
ax.add_patch( patch )
for ocean in oceans:
sea_patch = PolygonPatch(ocean, color="blue", zorder =100)
ax.add_patch( sea_patch )
########### DATA
x = [3.395833, 31.233333, 28.045556, 45.35 ]
y = [6.453056, 30.05, -26.204444, 2.033333]
z = [0, 90, 180, 270]
# set up orthographic map projection
map = Basemap(projection='ortho', lat_0=0, lon_0=20, resolution='l')
## Plot the cities on the map
map.plot(x,y,".", latlon=1)
# create a interpolated mesh and set it on the map
interpol_func = interpolate.interp2d(x, y, z, kind='linear')
newx = np.linspace( min(x), max(x) )
newy = np.linspace( min(y), max(y) )
X,Y = np.meshgrid(newx, newy)
Z = interpol_func(newx, newy)
map.pcolormesh( X, Y, Z, latlon=1, zorder=3)
filloceans(map, color="blue")
VoilĂ :

Drawing log-linear plot on a square plot area in matplotlib

I would like to draw a plot with a logarithmic y axis and a linear x axis on a square plot area in matplotlib. I can draw linear-linear as well as log-log plots on squares, but the method I use, Axes.set_aspect(...), is not implemented for log-linear plots. Is there a good workaround?
linear-linear plot on a square:
from pylab import *
x = linspace(1,10,1000)
y = sin(x)**2+0.5
plot (x,y)
ax = gca()
data_aspect = ax.get_data_ratio()
ax.set_aspect(1./data_aspect)
show()
log-log plot on a square:
from pylab import *
x = linspace(1,10,1000)
y = sin(x)**2+0.5
plot (x,y)
ax = gca()
ax.set_yscale("log")
ax.set_xscale("log")
xmin,xmax = ax.get_xbound()
ymin,ymax = ax.get_ybound()
data_aspect = (log(ymax)-log(ymin))/(log(xmax)-log(xmin))
ax.set_aspect(1./data_aspect)
show()
But when I try this with a log-linear plot, I do not get the square area, but a warning
from pylab import *
x = linspace(1,10,1000)
y = sin(x)**2+0.5
plot (x,y)
ax = gca()
ax.set_yscale("log")
xmin,xmax = ax.get_xbound()
ymin,ymax = ax.get_ybound()
data_aspect = (log(ymax)-log(ymin))/(xmax-xmin)
ax.set_aspect(1./data_aspect)
show()
yielding the warning:
axes.py:1173: UserWarning: aspect is not supported for Axes with xscale=linear, yscale=log
Is there a good way of achieving square log-linear plots despite the lack support in Axes.set_aspect?
Well, there is a sort of a workaround. The actual axis area (the area where the plot is, not including external ticks &c) can be resized to any size you want it to have.
You may use the ax.set_position to set the relative (to the figure) size and position of the plot. In order to use it in your case we need a bit of maths:
from pylab import *
x = linspace(1,10,1000)
y = sin(x)**2+0.5
plot (x,y)
ax = gca()
ax.set_yscale("log")
# now get the figure size in real coordinates:
fig = gcf()
fwidth = fig.get_figwidth()
fheight = fig.get_figheight()
# get the axis size and position in relative coordinates
# this gives a BBox object
bb = ax.get_position()
# calculate them into real world coordinates
axwidth = fwidth * (bb.x1 - bb.x0)
axheight = fheight * (bb.y1 - bb.y0)
# if the axis is wider than tall, then it has to be narrowe
if axwidth > axheight:
# calculate the narrowing relative to the figure
narrow_by = (axwidth - axheight) / fwidth
# move bounding box edges inwards the same amount to give the correct width
bb.x0 += narrow_by / 2
bb.x1 -= narrow_by / 2
# else if the axis is taller than wide, make it vertically smaller
# works the same as above
elif axheight > axwidth:
shrink_by = (axheight - axwidth) / fheight
bb.y0 += shrink_by / 2
bb.y1 -= shrink_by / 2
ax.set_position(bb)
show()
A slight stylistic comment is that import pylab is not usually used. The lore goes:
import matplotlib.pyplot as plt
pylab as an odd mixture of numpy and matplotlib imports created to make interactive IPython use easier. (I use it, too.)

Categories

Resources