Search code examples
pythonopencvimage-processingcontouredge-detection

getting edges / lines of vertical lines in an image


Original Post

I have a batch of similar images which I need to analyze.

After

  • thresholding
  • hole filling using skimage.morphology.reconstruction (example of usage here)
  • image closing

I get something like this image:

enter image description here

I'm interested in the edges of shape, specifically the steepness of the slope along vertical lines in the original grayscale image. I thought I could use Canny to get the contours which indeed gave me:

enter image description here

My questions:

  1. How would I go about separating the approximately vertical and horizontal edges? The have very different meaning for me, as I'm not at all interested in what happens between adjacent horizontal lines (I would rather just cut those parts from the image), and I'm interested in what happens around vertical lines (white surrounding in the BW image).
  2. I'm would assume my lines should be straight (there could be a slope, but the noise is too high). How would I smoothen the contour lines?
  3. Eventually I wish to go back to the gray scale image and look at the pixel statistics in the vicinity of vertical lines. How can extract the location info of lines from the edges found by Canny?

My code:

im_th= cv2.inRange(img, 0, 500, cv2.THRESH_BINARY)
seed = np.copy(im_th)
seed[1:-1, 1:-1] = im_th.max()
mask = im_th
filled = reconstruction(seed, mask, method='erosion').astype(np.uint8)
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS,(4,4))
closed = cv2.morphologyEx(filled,cv2.MORPH_CLOSE, kernel=kernel)
edges = cv2.Canny(cv2.medianBlur(closed, 5), 50, 150)

Edit

Per suggestions of Christoph Rackwitz and mimocha, I attempted using findContours and HoughLinesP. Both results look promising, but require further work to be of use.

findContours approach

Code:

contours, hierarchy = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contour_img = cv2.drawContours(cv2.cvtColor(closed, cv2.COLOR_GRAY2BGR), contours, -1, (255,0,0), 3)

Resulting image (overlay over the closed image):

enter image description here

The contours are found nicely, I still 151 contour lines. I would want to smoothen the result and get less lines.

HoughLinesP approach

Code:

threshold = 15
min_line_length = 30
max_line_gap = 200
line_image = np.copy(cv2.cvtColor(closed, cv2.COLOR_GRAY2BGR)) * 0
lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold, None, min_line_length, max_line_gap)
for line in lines:
    for x1,y1,x2,y2 in line:
        cv2.line(line_image,(x1,y1),(x2,y2),(255,0,0),5)
lines_edges = cv2.addWeighted(cv2.cvtColor(closed, cv2.COLOR_GRAY2BGR), 0.2, line_image, 1, 0)

Resulting image:

enter image description here

HoughLinesP indeed guarantees straight lines, and it find the lines nicely, but I still have some lines which are too "thick" (you can see in the image some thick lines which are actually parallel lines). Thus I need some method for refining this results.

Summary so far:

  1. Both the contours approach and the Hough transform approach have gain.
  2. Hough transform may have an advantage as it returns straight lines, and also allows to separate horizontal in vertical lines as shown here.
  3. I still need a method to merge/cluster lines.

Solution

  • I would find the two relevant sections of the image by finding the wider bars, and cutting there. This is a bit of code using DIPlib (disclaimer: I'm an author), but it's quite easy to do this with just about any other package too.

    import diplib as dip
    import math
    
    
    img = dip.ImageRead('5oC9n.png')
    
    # Colapse the x axis by summation
    y = dip.Sum(img, process=[True, False]).Squeeze()
    
    # In our 1D result, find the transition points
    y = dip.Abs(dip.Dx(y))
    y = dip.Shrinkage(y, 5)  # ignore any local maxima below 5
    maxima = dip.SubpixelMaxima(y)
    
    # We want segments between 1st and 2nd, and between 3rd and 4th.
    # We'll add a bit of margin too
    assert(len(maxima) == 4)
    
    # First segment
    top = math.ceil(maxima[0].coordinates[0]) + 2
    bottom = math.floor(maxima[1].coordinates[0]) - 2
    img1 = img[:, top:bottom]
    
    # Second segment
    top = math.ceil(maxima[2].coordinates[0]) + 2
    bottom = math.floor(maxima[3].coordinates[0]) - 2
    img2 = img[:, top:bottom]
    

    Next, finding the slopes of the lines, could be done by fitting a straight line to each of the edges. This other answer of mine does exactly that (the answer computes the distance between two parallel edges, but in doing so it fits a straight line and finds its normal (i.e. the slope).