Search code examples
pythonopencvimage-processingskeletonization

Computing line that follows objects orientation in contours


I am using cv2 to compute the line withing the object present in the mask image attached. The orientation/shape of the object might vary (horizontal, vertical) from the other mask images in my dataset. But the problem is, the method I have used to compute the line is not reliable. It works for the few images but failed to draw a line accurately for other mask images. Could anyone suggest an alternative approach?

this is the raw mask image:

1

This is how a line shall be drawn (considering object orientation)

2

Here is the code which represents my approach. I will highly appreciate any help from your side.

import numpy as np
import cv2
import matplotlib.pyplot as plt

image_bgr = cv2.imread(IMAGE_PATH)

mask = masks[2]


mask_uint8 = mask.astype(np.uint8) * 255


contours, _ = cv2.findContours(mask_uint8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

for c in contours:
    # Calculate the centroid (center point) of the contour
    M = cv2.moments(c)
    cx = int(M['m10'] / M['m00'])
    cy = int(M['m01'] / M['m00'])
    
    
    cv2.drawContours(image_bgr, [c], -1, (255, 0, 0), 3)
    cv2.circle(image_bgr, (cx, cy), 5, (0, 255, 0), -1)
    
    
    left_side_point = tuple(c[c[:, :, 0].argmin()][0])
    right_side_point = tuple(c[c[:, :, 0].argmax()][0])
    center_point = (cx, cy)
    
    
    left_center_point = ((left_side_point[0] + center_point[0]) // 2, (left_side_point[2] + center_point[2]) // 2)
    right_center_point = ((right_side_point[0] + center_point[0]) // 2, (right_side_point[2] + center_point[2]) // 2)
    
    cv2.line(image_bgr, left_side_point, left_center_point, (0, 0, 255), 2)
    cv2.line(image_bgr, left_center_point, center_point, (0, 0, 255), 2)
    cv2.line(image_bgr, center_point, right_center_point, (0, 0, 255), 2)
    cv2.line(image_bgr, right_center_point, right_side_point, (0, 0, 255), 2)


plt.imshow(image_bgr)
plt.show()
´´´


  [1]: https://i.sstatic.net/ZQja9VmS.png
  [2]: https://i.sstatic.net/mLK66nAD.png

Solution

  • When I was doing particle scan analysis, I eventually went with a PCA-like approach:

    %matplotlib notebook
    import matplotlib.pyplot as plt
    import numpy as np
    import cv2
    im = cv2.imread("mask.png", 0) # read as gray
    y, x = np.where(im) # get non-zero elements
    centroid = np.mean(x), np.mean(y) # get the centroid of the particle
    x_diff = x - centroid[0] # center x
    y_diff = y - centroid[1] # center y
    cov_matrix = np.cov(x_diff, y_diff) # get the convariance
    eigenvalues, eigenvectors = np.linalg.eig(cov_matrix) # apply EVD
    indicesForSorting = np.argsort(eigenvalues)[::-1] # sort to get the primary first
    eigenvalues = eigenvalues[indicesForSorting]
    eigenvectors = eigenvectors[:, indicesForSorting]
    plt.figure()
    plt.imshow(im, cmap = "gray") # plot image
    vecPrimary = eigenvectors[:, 0] * np.sqrt(eigenvalues[0])
    plt.plot([centroid[0] - vecPrimary[0], centroid[0] + vecPrimary[0]], 
            [centroid[1] - vecPrimary[1], centroid[1] + vecPrimary[1]])
    vecSecondary = eigenvectors[:, 1] * np.sqrt(eigenvalues[1])
    plt.plot([centroid[0] - vecSecondary[0], centroid[0] + vecSecondary[0]], 
            [centroid[1] - vecSecondary[1], centroid[1] + vecSecondary[1]])
    

    Results

    I like this approach because it also scales the line. If this is not desirable in your case, you can get the angle of this line and draw an infinite one, then mask it with the image. Hope this helps you further

    Edit: drawing the lines with opencv

    ### same analysis as before
    im = cv2.imread("mask.png")
    pt1 = (int(centroid[0] - vecPrimary[0]), int(centroid[1] - vecPrimary[1]))
    pt2 = (int(centroid[0] + vecPrimary[0]), int(centroid[1] + vecPrimary[1]))
    cv2.line(im, pt1, pt2, (255, 0, 0), 2)  # blue line
    pt1 = (int(centroid[0] - vecSecondary[0]), int(centroid[1] - vecSecondary[1]))
    pt2 = (int(centroid[0] + vecSecondary[0]), int(centroid[1] + vecSecondary[1]))
    cv2.line(im, pt1, pt2, (0, 0, 255), 2)  # redline
    cv2.imwrite("maskWithLines.png", im)
    

    Results:

    lined

    EDIT: as I said in my answer, just multiply the vectors by a scale and then use the mask to biwise_and:

    im = cv2.imread("mask.png")
    imGray = cv2.imread("mask.png", 0) # read imgray
    y, x = np.where(imGray) # get non-zero elements
    centroid = np.mean(x), np.mean(y) # get the centroid of the particle
    x_diff = x - centroid[0] # center x
    y_diff = y - centroid[1] # center y
    cov_matrix = np.cov(x_diff, y_diff) # get the convariance
    eigenvalues, eigenvectors = np.linalg.eig(cov_matrix) # apply EVD
    indicesForSorting = np.argsort(eigenvalues)[::-1] # sort to get the primary first and secondary second
    eigenvalues = eigenvalues[indicesForSorting] # sort eigenvalues
    eigenvectors = eigenvectors[:, indicesForSorting] # sort eigenvectors
    Scale = 100 # this can be adjusted, iut is actually not important as long as it is a very high value
    vecPrimary = eigenvectors[:, 0] * np.sqrt(eigenvalues[0]) * Scale
    vecSecondary = eigenvectors[:, 1] * np.sqrt(eigenvalues[1]) * Scale
    pt1 = (int(centroid[0] - vecPrimary[0]), int(centroid[1] - vecPrimary[1]))
    pt2 = (int(centroid[0] + vecPrimary[0]), int(centroid[1] + vecPrimary[1]))
    cv2.line(im, pt1, pt2, (255, 0, 0), 2)  # blue line
    pt1 = (int(centroid[0] - vecSecondary[0]), int(centroid[1] - vecSecondary[1]))
    pt2 = (int(centroid[0] + vecSecondary[0]), int(centroid[1] + vecSecondary[1]))
    cv2.line(im, pt1, pt2, (0, 0, 255), 2)  # red line
    im = cv2.bitwise_and(im, im, mask = imGray) # mask the lines
    cv2.imwrite("maskWithLines.png", im)
    

    The results will look like this:

    Extended lines