How to remove/omit smaller contour lines using matplotlib

前端 未结 2 1027
暗喜
暗喜 2020-12-06 03:31

I am trying to plot contour lines of pressure level. I am using a netCDF file which contain the higher resolution data (ranges from 3 km to 27 km). Due to highe

相关标签:
2条回答
  • 2020-12-06 03:59

    General idea

    Your question seems to have 2 very different halves: one about omitting small contours, and another one about smoothing the contour lines. The latter is simpler, since I can't really think of anything else other than decreasing the resolution of your contour() call, just like you said.

    As for removing a few contour lines, here's a solution which is based on directly removing contour lines individually. You have to loop over the collections of the object returned by contour(), and for each element check each Path, and delete the ones you don't need. Redrawing the figure's canvas will get rid of the unnecessary lines:

    # dummy example based on matplotlib.pyplot.clabel example:
    import matplotlib
    import numpy as np
    import matplotlib.cm as cm
    import matplotlib.mlab as mlab
    import matplotlib.pyplot as plt
    
    delta = 0.025
    x = np.arange(-3.0, 3.0, delta)
    y = np.arange(-2.0, 2.0, delta)
    X, Y = np.meshgrid(x, y)
    Z1 = mlab.bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0)
    Z2 = mlab.bivariate_normal(X, Y, 1.5, 0.5, 1, 1)
    # difference of Gaussians
    Z = 10.0 * (Z2 - Z1)
    
    
    plt.figure()
    CS = plt.contour(X, Y, Z)
    
    for level in CS.collections:
        for kp,path in reversed(list(enumerate(level.get_paths()))):
            # go in reversed order due to deletions!
    
            # include test for "smallness" of your choice here:
            # I'm using a simple estimation for the diameter based on the
            #    x and y diameter...
            verts = path.vertices # (N,2)-shape array of contour line coordinates
            diameter = np.max(verts.max(axis=0) - verts.min(axis=0))
    
            if diameter<1: # threshold to be refined for your actual dimensions!
                del(level.get_paths()[kp])  # no remove() for Path objects:(
    
    # this might be necessary on interactive sessions: redraw figure
    plt.gcf().canvas.draw()
    

    Here's the original(left) and the removed version(right) for a diameter threshold of 1 (note the little piece of the 0 level at the top):

    Note that the top little line is removed while the huge cyan one in the middle doesn't, even though both correspond to the same collections element i.e. the same contour level. If we didn't want to allow this, we could've called CS.collections[k].remove(), which would probably be a much safer way of doing the same thing (but it wouldn't allow us to differentiate between multiple lines corresponding to the same contour level).

    To show that fiddling around with the cut-off diameter works as expected, here's the result for a threshold of 2:

    All in all it seems quite reasonable.


    Your actual case

    Since you've added your actual data, here's the application to your case. Note that you can directly generate the levels in a single line using np, which will almost give you the same result. The exact same can be achieved in 2 lines (generating an arange, then selecting those that fall between p1 and p2). Also, since you're setting levels in the call to contour, I believe the V=2 part of the function call has no effect.

    import numpy as np
    import matplotlib.pyplot as plt
    
    # insert actual data here...
    Z = np.loadtxt('mslp.txt',delimiter=',')
    X,Y = np.meshgrid(np.linspace(0,300000,Z.shape[1]),np.linspace(0,200000,Z.shape[0]))
    p1,p2 = 1006,1018
    
    # this is almost the same as the original, although it will produce
    # [p1, p1+2, ...] instead of `[Z.min()+n, Z.min()+n+2, ...]`
    levels = np.arange(np.maximum(Z.min(),p1),np.minimum(Z.max(),p2),2)
    
    
    #control
    plt.figure()
    CS = plt.contour(X, Y, Z, colors='b', linewidths=2, levels=levels)
    
    
    #modified
    plt.figure()
    CS = plt.contour(X, Y, Z, colors='b', linewidths=2, levels=levels)
    
    for level in CS.collections:
        for kp,path in reversed(list(enumerate(level.get_paths()))):
            # go in reversed order due to deletions!
    
            # include test for "smallness" of your choice here:
            # I'm using a simple estimation for the diameter based on the
            #    x and y diameter...
            verts = path.vertices # (N,2)-shape array of contour line coordinates
            diameter = np.max(verts.max(axis=0) - verts.min(axis=0))
    
            if diameter<15000: # threshold to be refined for your actual dimensions!
                del(level.get_paths()[kp])  # no remove() for Path objects:(
    
    # this might be necessary on interactive sessions: redraw figure
    plt.gcf().canvas.draw()
    plt.show()
    

    Results, original(left) vs new(right):


    Smoothing by resampling

    I've decided to tackle the smoothing problem as well. All I could come up with is downsampling your original data, then upsampling again using griddata (interpolation). The downsampling part could also be done with interpolation, although the small-scale variation in your input data might make this problem ill-posed. So here's the crude version:

    import scipy.interpolate as interp   #the new one
    
    # assume you have X,Y,Z,levels defined as before
    
    # start resampling stuff
    dN = 10 # use every dN'th element of the gridded input data
    my_slice = [slice(None,None,dN),slice(None,None,dN)]
    
    # downsampled data
    X2,Y2,Z2 = X[my_slice],Y[my_slice],Z[my_slice]
    # same as X2 = X[::dN,::dN] etc.
    
    # upsampling with griddata over original mesh
    Zsmooth = interp.griddata(np.array([X2.ravel(),Y2.ravel()]).T,Z2.ravel(),(X,Y),method='cubic')
    
    # plot
    plt.figure()
    CS = plt.contour(X, Y, Zsmooth, colors='b', linewidths=2, levels=levels)
    

    You can freely play around with the grids used for interpolation, in this case I just used the original mesh, as it was at hand. You can also play around with different kinds of interpolation: the default 'linear' one will be faster, but less smooth.

    Result after downsampling(left) and upsampling(right):

    Of course you should still apply the small-line-removal algorithm after this resampling business, and keep in mind that this heavily distorts your input data (since if it wasn't distorted, then it wouldn't be smooth). Also, note that due to the crude method used in the downsampling step, we introduce some missing values near the top/right edges of the region under consideraton. If this is a problem, you should consider doing the downsampling based on griddata as I've noted earlier.

    0 讨论(0)
  • 2020-12-06 04:00

    This is a pretty bad solution, but it's the only one that I've come up with. Use the get_contour_verts function in this solution you linked to, possibly with the matplotlib._cntr module so that nothing gets plotted initially. That gives you a list of contour lines, sections, vertices, etc. Then you have to go through that list and pop the contours you don't want. You could do this by calculating a minimum diameter, for example; if the max distance between points is less than some cutoff, throw it out.

    That leaves you with a list of LineCollection objects. Now if you make a Figure and Axes instance, you can use Axes.add_collection to add all of the LineCollections in the list.

    I checked this out really quick, but it seemed to work. I'll come back with a minimum working example if I get a chance. Hope it helps!


    Edit: Here's an MWE of the basic idea. I wasn't familiar with plt._cntr.Cntr, so I ended up using plt.contour to get the initial contour object. As a result, you end up making two figures; you just have to close the first one. You can replace checkDiameter with whatever function works. I think you could turn the line segments into a Polygon and calculate areas, but you'd have to figure that out on your own. Let me know if you run into problems with this code, but it at least works for me.

    import numpy as np
    import matplotlib as mpl
    import matplotlib.pyplot as plt
    
    def checkDiameter(seg, tol=.3):
        # Function for screening line segments. NB: Not actually a proper diameter.
        diam = (seg[:,0].max() - seg[:,0].min(),
                seg[:,1].max() - seg[:,1].min())
        return not (diam[0] < tol or diam[1] < tol)
    
    # Create testing data
    x = np.linspace(-1,1, 21)
    xx, yy = np.meshgrid(x,x)
    z = np.exp(-(xx**2 + .5*yy**2))
    
    # Original plot with plt.contour
    fig0, ax0 = plt.subplots()
    # Make sure this contour object actually has a tiny contour to remove
    cntrObj = ax0.contour(xx,yy,z, levels=[.2,.4,.6,.8,.9,.95,.99,.999])
    
    # Primary loop: Copy contours into a new LineCollection
    lineNew = list()
    for lineOriginal in cntrObj.collections:
        # Get properties of the original LineCollection
        segments = lineOriginal.get_segments()
        propDict = lineOriginal.properties()
        propDict = {key: value for (key,value) in propDict.items()
            if key in ['linewidth','color','linestyle']}  # Whatever parameters you want to carry over
        # Filter out the lines with small diameters
        segments = [seg for seg in segments if checkDiameter(seg)]
        # Create new LineCollection out of the OK segments
        if len(segments) > 0:
            lineNew.append(mpl.collections.LineCollection(segments, **propDict))
    
    # Make new plot with only these line collections; display results
    fig1, ax1 = plt.subplots()
    ax1.set_xlim(ax0.get_xlim())
    ax1.set_ylim(ax0.get_ylim())
    for line in lineNew:
        ax1.add_collection(line)
    plt.show()
    

    FYI: The bit with propDict is just to automate bringing over some of the line properties from the original plot. You can't use the whole dictionary at once, though. First, it contains the old plot's line segments, but you can just swap those for the new ones. But second, it appears to contain a number of parameters that are in conflict with each other: multiple linewidths, facecolors, etc. The {key for key in propDict if I want key} workaround is my way to bypass that, but I'm sure someone else can do it more cleanly.

    0 讨论(0)
提交回复
热议问题