Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can matplotlib contours match pixel edges?

How to outline pixel boundaries in matplotlib? For instance, for a semi-random dataset like the one below,

# the code block that follows is irrelevant
import numpy as np
k = []
for s in [2103, 1936, 2247, 2987]:
    np.random.seed(s)
    k.append(np.random.randint(0, 2, size=(2,6)))
arr = np.hstack([np.vstack(k)[:, :-1], np.vstack(k).T[::-1].T ])
image = np.zeros(shape=(arr.shape[0]+2, arr.shape[1]+2))
image[1:-1, 1:-1] = arr

it is quite clear that a contour matching the pixel edges of image would be preferred to the default behavior of the contour function, where the contour lines are effectively drawn across the diagonals of edge pixels.

import matplotlib.pyplot as plt
plt.contour(image[::-1], [0.5], colors='r')

binary_invader

How to make the contours align with the pixels? I'm looking for a solution within numpy and matplotlib libraries.

like image 555
Vlas Sokolov Avatar asked Nov 30 '16 15:11

Vlas Sokolov


2 Answers

enter image description here

contour_rect_slow draws slingle lines at the boundaries between pixels with values 0 and 1. contour_rect is a more compact version, connecting longer lines to a single line.

Code:

import numpy as np
k = []
for s in [2103, 1936, 2247, 2987]:
    np.random.seed(s)
    k.append(np.random.randint(0, 2, size=(2,6)))
arr = np.hstack([np.vstack(k)[:, :-1], np.vstack(k).T[::-1].T ])
image = np.zeros(shape=(arr.shape[0]+2, arr.shape[1]+2))
image[1:-1, 1:-1] = arr[::1]

#    image[1, 1] = 1

import matplotlib.pyplot as plt
plt.imshow(image, interpolation="none", cmap="Blues")

def contour_rect_slow(im):
    """Clear version"""

    pad = np.pad(im, [(1, 1), (1, 1)])  # zero padding

    im0 = np.abs(np.diff(pad, n=1, axis=0))[:, 1:]
    im1 = np.abs(np.diff(pad, n=1, axis=1))[1:, :]

    lines = []

    for ii, jj in np.ndindex(im0.shape):
        if im0[ii, jj] == 1:
            lines += [([ii-.5, ii-.5], [jj-.5, jj+.5])]
        if im1[ii, jj] == 1:
            lines += [([ii-.5, ii+.5], [jj-.5, jj-.5])]

    return lines



def contour_rect(im):
    """Fast version"""

    lines = []
    pad = np.pad(im, [(1, 1), (1, 1)])  # zero padding

    im0 = np.abs(np.diff(pad, n=1, axis=0))[:, 1:]
    im1 = np.abs(np.diff(pad, n=1, axis=1))[1:, :]

    im0 = np.diff(im0, n=1, axis=1)
    starts = np.argwhere(im0 == 1)
    ends = np.argwhere(im0 == -1)
    lines += [([s[0]-.5, s[0]-.5], [s[1]+.5, e[1]+.5]) for s, e
              in zip(starts, ends)]

    im1 = np.diff(im1, n=1, axis=0).T
    starts = np.argwhere(im1 == 1)
    ends = np.argwhere(im1 == -1)
    lines += [([s[1]+.5, e[1]+.5], [s[0]-.5, s[0]-.5]) for s, e
              in zip(starts, ends)]

    return lines

lines = contour_rect(image)
for line in lines:
    plt.plot(line[1], line[0], color='r', alpha=1)

Warning: This is significantly slower then mpl.contour for large images..

like image 65
Markus Dutschke Avatar answered Nov 05 '22 11:11

Markus Dutschke


If the image has a resolution of 1 pixel per unit, how would you define the "edge" of a pixel? The notion of "edge" only makes sense in a frame of increased resolution compared to the pixel itself and contour cannot draw any edges if it is working with the same resoltion as the image itself.

On the other hand, it is of course possible to increase the resolution such that the notion "edge" carries a meaning. So let's say we increase the resolution by a factor of 100 we can easily draw the edges using a contour plot.

import matplotlib.pyplot as plt
import numpy as np

k = []
for s in [2103, 1936, 2247, 2987]:
    np.random.seed(s)
    k.append(np.random.randint(0, 2, size=(2,6)))
arr = np.hstack([np.vstack(k)[:, :-1], np.vstack(k).T[::-1].T ])
image = np.zeros(shape=(arr.shape[0]+2, arr.shape[1]+2))
image[1:-1, 1:-1] = arr


f = lambda x,y: image[int(y),int(x) ]
g = np.vectorize(f)

x = np.linspace(0,image.shape[1], image.shape[1]*100)
y = np.linspace(0,image.shape[0], image.shape[0]*100)
X, Y= np.meshgrid(x[:-1],y[:-1])
Z = g(X[:-1],Y[:-1])

plt.imshow(image[::-1], origin="lower", interpolation="none", cmap="Blues")

plt.contour(Z[::-1], [0.5], colors='r', linewidths=[3], 
            extent=[0-0.5, x[:-1].max()-0.5,0-0.5, y[:-1].max()-0.5])

plt.show()

enter image description here

For comparison, we can also draw the image itself in the same plot using imshow.

like image 23
ImportanceOfBeingErnest Avatar answered Nov 05 '22 12:11

ImportanceOfBeingErnest