2

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.
Yair M
  • 450
  • 4
  • 12
  • Have you got sample images where the angle you want to measure is easier to see please? – Mark Setchell Feb 27 '23 at 13:35
  • theoretically there is no angle. so in all images it will be hard to see. what i wish is to smoothen the line, as you can see, it jitters. – Yair M Feb 27 '23 at 14:30
  • Use morphology open (or close) on your binary (first) image above to smooth it. – fmw42 Feb 27 '23 at 17:20
  • 1
    don't use Hough transform. don't use Canny. just use `findContours()`. then calculate the local orientation of each segment of the contour. based on that, you can split the contour into sections going horizontally or vertically. remember, contours are "circular" things, so the "beginning" is arbitrary. you may need to roll/rotate the list (numpy has a `roll()` function) – Christoph Rackwitz Feb 27 '23 at 21:51
  • `Sobel` and `cartToPolar` don't help you? – fana Feb 28 '23 at 03:18
  • @fmw42 the binary image you see is after closing. Is there a point in repetitive closing? – Yair M Feb 28 '23 at 11:32
  • @ChristophRackwitz can you think of a way of filtering / smoothing the result? – Yair M Feb 28 '23 at 12:52
  • right, find the contour with the largest area. `max(contours, key=cv.contourArea)`. ***NOT*** on the canny'd image, on the source/thresholded one. – Christoph Rackwitz Feb 28 '23 at 15:41
  • @ChristophRackwitz, that doesn't work well for me. I should have said it before, but what you see is only a part of the image. It is the whole width, but it is 25 times higher, and I show here a subset for visualization purposes. It pretty much repeats itself. If only plot the largest one, I get only one contour, but as you can see above, I have 4 contours. Also, apparently taking the one with the largest area, considers only *closed* contours, while the ones I'm interested in are open. Lastly, when applying over the non-Cannied image, I get "noise" contours too, not sure what is the gain. – Yair M Feb 28 '23 at 17:21
  • 1
    You could get contours and then reduce the number of vertices with approxPolyDP(). Then you can find the edges between the vertices. – fmw42 Feb 28 '23 at 17:23
  • I second the use of appoxPolyDP. It's a straightforward way to reduce the number of lines and it's adjustable. – Ian Chu Mar 01 '23 at 15:06

2 Answers2

3

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

Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
  • 1
    This should be a good approach. You can do the same with OpenCV by finding the white rectangles using `findContours()` - or summing across the rows with `np.sum(..., axis=1)` - and then discard the top and bottom 10% to ignore the rounded corners and fit a line through the remaining 80% of the points on the vertical edge. – Mark Setchell Feb 28 '23 at 19:54
  • Eventually I ended up using morphological operations + segmentation by sums as suggested. Turns out I don't the lines to get what I actually needed. – Yair M May 16 '23 at 09:21
1

Are you looking for something like the Hough transform? You should be able to use it on your detected edges directly.

  1. Horizontal lines doesn't affect the result. (You can filter lines by angles)
  2. Lines being slightly sloped doesn't affect the result.
  3. Hough transform tells you where lines are and what angles they are -- it fits an infinitely long line onto your image based on your edge pixels. Since you know the line slope and the shortest distance from the line to the origin, you can check every pixel that line intersects.

However, this may not answer your question if you are looking for the specific (x,y) coordinates for where each vertical line segment begins and ends.


I copied the code sample from the OpenCV tutorial and tested it on your image to find the two vertical lines:

>>> import cv2 as cv
>>> import numpy as np
>>> src = cv.imread("test.png")
>>> edge = cv.Canny(src, 50, 200, None, 3)
>>> cv.HoughLines(edge, 1, np.pi / 180, 150, None, 0, 0)
array([[[ 81.,   0.]],

       [[144.,   0.]]], dtype=float32)
mimocha
  • 1,041
  • 8
  • 18
  • Thanks for the suggestion. I edited the question per your suggestion. As you can see, I still need some filtering/clustering of lines. Do you have an idea? – Yair M Feb 28 '23 at 12:53
  • 1
    For using Hough Transform you could write code to select for specific lines / filter one line out of the many lines it detected (e.g. if there are many lines near around a location, pick the one with the most votes). But overall, I think Cris Luengo's answer might be a better approach, so I suggest you try out that one first. – mimocha Mar 01 '23 at 07:08