Skimage.draw.ellipse generates two undesired lines - python

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:

Related

Mask by contour line in matplotlib

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()

Is there a way to set the background color of a specific subplot in python matplotlib?

I would like to make a multiple plot using subplot, such that one or more specific subplots have a different background color than the rest, like in this example:
Note that I'm interested in setting the background color of the exterior patch of the subplots not the background color inside the plot (which can be done with facecolor='gray'). This is because I want to plot density plots and I want to distinguish some of them from the rest.
I have found similar questions like this for example where each row of subplots has a different background color, but I wasn't able to modify the code so that the color can be applied on specific subplots (say (1,2), (1,3), (2,1) and (2,2) as in the attached figure above).
This is an example code:
import numpy as np
import matplotlib.pyplot as plt
fig, subs = plt.subplots(3,3,figsize=(10,10))
images = []
for i in range(3):
for j in range(3):
data = np.random.rand(20,20)
images.append(subs[i, j].imshow(data))
subs[i, j].label_outer()
plt.show()
Any help would be greatly appreciated.
According [this post] you can use fig.patches.extend to draw a rectangle on the figure. With a high zorder the rectangle will be on top of the subplots, with a low zorder it can be behind.
Now, the exact area belonging to the surroundings of a subplot isn't well-defined.
A simple approach would be to give equal space to each subplot, but that doesn't work out well with shared axes nor with the white space near the figure edges.
The example code uses a different number of columns and rows to be sure horizontal and vertical calculations aren't flipped.
import numpy as np
import matplotlib.pyplot as plt
fig, subs = plt.subplots(3, 4, figsize=(10, 8))
images = []
for i in range(3):
for j in range(4):
data = np.random.rand(20, 20)
images.append(subs[i, j].imshow(data))
subs[i, j].label_outer()
m, n = subs.shape
for _ in range(50):
i = np.random.randint(m)
j = np.random.randint(n)
color = ['r', 'b', 'g'][np.random.randint(3)]
fig.patches.extend([plt.Rectangle((j / n, (m - 1 - i) / m), 1 / n, 1 / m,
fill=True, color=color, alpha=0.2, zorder=-1,
transform=fig.transFigure, figure=fig)])
plt.show()
Another approach would be to use subs[i, j].get_tightbbox(fig.canvas.get_renderer()), but that bounding box just includes the texts belonging to the subplot and nothing more.
A more involved approach calculates the difference between neighboring subplots and uses that to enlarge the area occupied by the axes of the subplots:
m, n = subs.shape
bbox00 = subs[0, 0].get_window_extent()
bbox01 = subs[0, 1].get_window_extent()
bbox10 = subs[1, 0].get_window_extent()
pad_h = 0 if n == 1 else bbox01.x0 - bbox00.x0 - bbox00.width
pad_v = 0 if m == 1 else bbox00.y0 - bbox10.y0 - bbox10.height
for _ in range(20):
i = np.random.randint(m)
j = np.random.randint(n)
color = ['r', 'b', 'g'][np.random.randint(3)]
bbox = subs[i, j].get_window_extent()
fig.patches.extend([plt.Rectangle((bbox.x0 - pad_h / 2, bbox.y0 - pad_v / 2),
bbox.width + pad_h, bbox.height + pad_v,
fill=True, color=color, alpha=0.2, zorder=-1,
transform=None, figure=fig)])
Depending on the layout of the plots, it still isn't perfect. The approach can be refined further, such as special treatment for the first column and lowest row. If overlapping isn't a problem, the bounding box can also be extended by the result of get_tightbbox(), using a lighter color and alpha=1.
This is how it looks like with plots that have tick labels at the four sides:
Set the color of the fig:
fig, subs = plt.subplots(3,3,figsize=(10,10))
images = []
for i in range(3):
for j in range(3):
data = np.random.rand(20,20)
images.append(subs[i, j].imshow(data))
subs[i, j].label_outer()
# this
fig.set_facecolor('red')
plt.show()
Output:
I was faced with the same problem and I think the nicest solution is using subfigures. Here is a example changing the color of the diagonal subplots:
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(10,10))
subfigs = fig.subfigures(ncols=3,nrows=3)
images = []
for i in range(3):
for j in range(3):
axs = subfigs[i][j].subplots()
data = np.random.rand(20,20)
images.append(axs.imshow(data))
axs.label_outer()
if i==j:
subfigs[i][j].set_facecolor("red")
plt.show()
Output:
The alternative solution I used was to pad the images with a certain color using:
img_padded = cv2.copyMakeBorder(img, border_size, border_size, border_size, border_size, cv2.BORDER_CONSTANT, value=[0, 255, 0]);

Modify matplotlib colormap

I'm trying to produce a similar version of this image using Python:
I'm close but can't quite figure out how to modify a matplotlib colormap to make values <0.4 go to white. I tried masking those values and using set_bad but I ended up with a real blocky appearance, losing the nice smooth contours seen in the original image.
Result with continuous colormap (problem: no white):
Result with set_bad (problem: no smooth transition to white):
Code so far:
from netCDF4 import Dataset as NetCDFFile
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.basemap import Basemap
nc = NetCDFFile('C:/myfile1.nc')
nc1 = NetCDFFile('C:/myfile2.nc')
lat = nc.variables['lat'][:]
lon = nc.variables['lon'][:]
time = nc.variables['time'][:]
uwnd = nc.variables['uwnd'][:]
vwnd = nc1.variables['vwnd'][:]
map = Basemap(llcrnrlon=180.,llcrnrlat=0.,urcrnrlon=340.,urcrnrlat=80.)
lons,lats = np.meshgrid(lon,lat)
x,y = map(lons,lats)
speed = np.sqrt(uwnd*uwnd+vwnd*vwnd)
#speed = np.ma.masked_where(speed < 0.4, speed)
#cmap = plt.cm.jet
#cmap.set_bad(color='white')
levels = np.arange(0.0,3.0,0.1)
ticks = np.arange(0.0,3.0,0.2)
cs = map.contourf(x,y,speed[0],levels, cmap='jet')
vw = plt.quiver(x,y,speed)
cbar = plt.colorbar(cs, orientation='horizontal', cmap='jet', spacing='proportional',ticks=ticks)
cbar.set_label('850 mb Vector Wind Anomalies (m/s)')
map.drawcoastlines()
map.drawparallels(np.arange(20,80,20),labels=[1,1,0,0], linewidth=0.5)
map.drawmeridians(np.arange(200,340,20),labels=[0,0,0,1], linewidth=0.5)
#plt.show()
plt.savefig('phase8_850wind_anom.png',dpi=600)
The answer to get the result smooth lies in constructing your own colormap. To do this one has to create an RGBA-matrix: a matrix with on each row the amount (between 0 and 1) of Red, Green, Blue, and Alpha (transparency; 0 means that the pixel does not have any coverage information and is transparent).
As an example the distance to some point is plotted in two dimensions. Then:
For any distance higher than some critical value, the colors will be taken from a standard colormap.
For any distance lower than some critical value, the colors will linearly go from white to the first color of the previously mentioned map.
The choices depend fully on what you want to show. The colormaps and their sizes depend on your problem. For example, you can choose different types of interpolation: linear, exponential, ...; single- or multi-color colormaps; etc..
The code:
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
# create colormap
# ---------------
# create a colormap that consists of
# - 1/5 : custom colormap, ranging from white to the first color of the colormap
# - 4/5 : existing colormap
# set upper part: 4 * 256/4 entries
upper = mpl.cm.jet(np.arange(256))
# set lower part: 1 * 256/4 entries
# - initialize all entries to 1 to make sure that the alpha channel (4th column) is 1
lower = np.ones((int(256/4),4))
# - modify the first three columns (RGB):
# range linearly between white (1,1,1) and the first color of the upper colormap
for i in range(3):
lower[:,i] = np.linspace(1, upper[0,i], lower.shape[0])
# combine parts of colormap
cmap = np.vstack(( lower, upper ))
# convert to matplotlib colormap
cmap = mpl.colors.ListedColormap(cmap, name='myColorMap', N=cmap.shape[0])
# show some example
# -----------------
# open a new figure
fig, ax = plt.subplots()
# some data to plot: distance to point at (50,50)
x,y = np.meshgrid(np.linspace(0,99,100),np.linspace(0,99,100))
z = (x-50)**2. + (y-50)**2.
# plot data, apply colormap, set limit such that our interpretation is correct
im = ax.imshow(z, interpolation='nearest', cmap=cmap, clim=(0,5000))
# add a colorbar to the bottom of the image
div = make_axes_locatable(ax)
cax = div.append_axes('bottom', size='5%', pad=0.4)
cbar = plt.colorbar(im, cax=cax, orientation='horizontal')
# save/show the image
plt.savefig('so.png')
plt.show()
The result:

Overlap area of 2 ellipses using matplotlib

Does anyone know if it is possible to calculate the overlapping area of two ellipses using matplotlib.patches.Ellipse.
I have to ellipses like this:
And i would like to calculate the ratio between the overlap area and the are of the individual ellipses.
Is this possible using only the Ellipse from matplotlib.patches
You cannot compute the area of the intersect with matplotlib (at least not to my knowledge), but you can use shapely to do so and then use matplotlib to visualise the result. Here a quick demo:
from matplotlib import pyplot as plt
from shapely.geometry.point import Point
from shapely import affinity
from matplotlib.patches import Polygon
import numpy as np
def create_ellipse(center, lengths, angle=0):
"""
create a shapely ellipse. adapted from
https://gis.stackexchange.com/a/243462
"""
circ = Point(center).buffer(1)
ell = affinity.scale(circ, int(lengths[0]), int(lengths[1]))
ellr = affinity.rotate(ell, angle)
return ellr
fig,ax = plt.subplots()
##these next few lines are pretty important because
##otherwise your ellipses might only be displayed partly
##or may be distorted
ax.set_xlim([-5,5])
ax.set_ylim([-5,5])
ax.set_aspect('equal')
##first ellipse in blue
ellipse1 = create_ellipse((0,0),(2,4),10)
verts1 = np.array(ellipse1.exterior.coords.xy)
patch1 = Polygon(verts1.T, color = 'blue', alpha = 0.5)
ax.add_patch(patch1)
##second ellipse in red
ellipse2 = create_ellipse((1,-1),(3,2),50)
verts2 = np.array(ellipse2.exterior.coords.xy)
patch2 = Polygon(verts2.T,color = 'red', alpha = 0.5)
ax.add_patch(patch2)
##the intersect will be outlined in black
intersect = ellipse1.intersection(ellipse2)
verts3 = np.array(intersect.exterior.coords.xy)
patch3 = Polygon(verts3.T, facecolor = 'none', edgecolor = 'black')
ax.add_patch(patch3)
##compute areas and ratios
print('area of ellipse 1:',ellipse1.area)
print('area of ellipse 2:',ellipse2.area)
print('area of intersect:',intersect.area)
print('intersect/ellipse1:', intersect.area/ellipse1.area)
print('intersect/ellipse2:', intersect.area/ellipse2.area)
plt.show()
The resulting plot looks like this:
And the computed areas (printed out to the terminal) are:
area of ellipse 1: 25.09238792436751
area of ellipse 2: 18.81929094327563
area of intersect: 13.656608779925698
intersect/ellipse1: 0.5442530547945023
intersect/ellipse2: 0.7256707397260032
Note that I adapted the code to generate the ellipse-shaped polygon from this post. Hope this helps.

Plot semi transparent contour plot over image file using matplotlib

I'd like to plot a transparent contour plot over an image file in matplotlib/pyplot.
Here's what I got so far...
I have a 600x600 pixel square image file test.png that looks like so:
I would like to plot a contour plot over this image (having the image file be 'below' and a semi-transparent version of the contour plot overlaid) using matplotlib and pyplot. As a bonus, the image would be automatically scaled to fit within the current plotting boundaries. My example plotting script is as follows:
from matplotlib import pyplot
from matplotlib.ticker import MultipleLocator, FormatStrFormatter
from matplotlib.colors import BoundaryNorm
from matplotlib.ticker import MaxNLocator
from pylab import *
import numpy as np
import random
# ----------------------------- #
dx, dy = 500.0, 500.0
y, x = np.mgrid[slice(-2500.0, 2500.0 + dy, dy),slice(-2500.0, 2500.0 + dx, dx)]
z = []
for i in x:
z.append([])
for j in y:
z[-1].append(random.uniform(80.0,100.0))
# ----------------------------- #
plot_aspect = 1.2
plot_height = 10.0
plot_width = int(plot_height*plot_aspect)
# ----------------------------- #
pyplot.figure(figsize=(plot_width, plot_height), dpi=100)
pyplot.subplots_adjust(left=0.10, right=1.00, top=0.90, bottom=0.06, hspace=0.30)
subplot1 = pyplot.subplot(111)
# ----------------------------- #
cbar_max = 100.0
cbar_min = 80.0
cbar_step = 1.0
cbar_num_colors = 200
cbar_num_format = "%d"
# ----------
levels = MaxNLocator(nbins=cbar_num_colors).tick_values(cbar_min, cbar_max)
cmap = pyplot.get_cmap('jet')
norm = BoundaryNorm(levels, ncolors=cmap.N, clip=True)
pp = pyplot.contourf(x,y,z,levels=levels,cmap=cmap)
cbar = pyplot.colorbar(pp, orientation='vertical', ticks=np.arange(cbar_min, cbar_max+cbar_step, cbar_step), format=cbar_num_format)
cbar.ax.set_ylabel('Color Scale [unit]', fontsize = 16, weight="bold")
# ----------
CS = pyplot.contour(x,y,z, alpha=0.5)
# ----------
majorLocator1 = MultipleLocator(500)
majorFormatter1 = FormatStrFormatter('%d')
minorLocator1 = MultipleLocator(250)
subplot1.xaxis.set_major_locator(majorLocator1)
subplot1.xaxis.set_major_formatter(majorFormatter1)
subplot1.xaxis.set_minor_locator(minorLocator1)
pyplot.xticks(fontsize = 16)
pyplot.xlim(-2500.0,2500.0)
# ----------
majorLocator2 = MultipleLocator(500)
majorFormatter2 = FormatStrFormatter('%d')
minorLocator2 = MultipleLocator(250)
subplot1.yaxis.set_major_locator(majorLocator2)
subplot1.yaxis.set_major_formatter(majorFormatter2)
subplot1.yaxis.set_minor_locator(minorLocator2)
pyplot.yticks(fontsize = 16)
pyplot.ylim(-2500.0,2500.0)
# ----------
subplot1.xaxis.grid()
subplot1.yaxis.grid()
# ----------
subplot1.axes.set_aspect('equal')
# ----------
pyplot.suptitle('Main Title', fontsize = 24, weight="bold")
# ----------
pyplot.xlabel('X [m]', fontsize=16, weight="bold")
pyplot.ylabel('Y [m]', fontsize=16, weight="bold")
# ----------
implot = subplot1.imshow( pyplot.imread('test.png') , interpolation='nearest', alpha=0.5)
# ----------
pyplot.show()
#pyplot.savefig("tmp.png", dpi=100)
pyplot.close()
...but I'm not getting the result I want... instead I just see the contour plot part. Something like:
What should I do in my code to get what I want?
You basically need to do two things, set the extent of the image you want in the background. If you dont, the coordinates are assumed to be pixel coordinates, in this case 0 till 600 for both x and y. So adjust you imshow command to:
implot = subplot1.imshow(pyplot.imread(r'test.png'), interpolation='nearest',
alpha=0.5, extent=[-2500.0,2500.0,-2500.0,2500.0])
If you want to stretch the image to the limits of the plot automatically, you can grab the extent with:
extent = subplot1.get_xlim()+ subplot1.get_ylim()
And pass it to imshow as extent=extent.
Since its the background image, setting the alpha to 0.5 makes it very faint, i would set it to 1.0.
Secondly, you set the alpha of the contour lines, but you probably also (or especially) want to set the alpha of the filled contours. And when you use alpha with filled contours, enabling anti-aliasing reduces artifacts. So change your contourf command to:
pp = pyplot.contourf(x,y,z,levels=levels,cmap=cmap, alpha=.5, antialiased=True)
And since you already create the subplot object yourself, i would advice also using it to do the plotting instead of the pyplot interface, which operates on the currently active axes.
So:
subplot1.contourf()
etc
Instead of:
pyplot.contourf()
With the two changes mentioned above, my result looks like:
I personally used the multiple contour plot answer for a while with great results. However, I had to output my figures to PostScript, which does not support opacity (alpha option). I found this answer useful since it does not require the use of opacity.
The reason these lines show up is due to the edge color of the faces that make up the contour plot. The linked solution avoids this by changing the edge color to the face color.
cf = plt.contourf(x, y, z, levels=100)
# This is the fix for the white lines between contour levels
for c in cf.collections:
c.set_edgecolor("face")

Categories

Resources