11

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.

Vlas Sokolov
  • 3,733
  • 3
  • 26
  • 43

2 Answers2

7

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.

Community
  • 1
  • 1
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Nice, I did not think of making contours on expanded grid. Why is there a mismatch between `imshow` and `contour` though? It can be better visualized for resolution increase of `10` instead of a `100`. – Vlas Sokolov Dec 01 '16 at 10:51
  • 3
    On the pixel edges definition - I would say that there's nothing to stop us from defining the pixel edges at `x` and `y` equal to `0.5, 1.0, 1.5, ...` grid lines. Those lines can then be drawn with a basic `Line2d`. – Vlas Sokolov Dec 01 '16 at 10:53
2

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..

Markus Dutschke
  • 9,341
  • 4
  • 63
  • 58
  • That works pretty well and is exactly what I had in mind. Can you maybe update the np.pad() line? In numpy <1.17 `mode` is a required argument and there's plenty of folks still running older versions. – Vlas Sokolov Mar 20 '20 at 16:16