Search code examples
pythonopencvimage-processingedge-detection

The goal is to find the line that represents the distance between the "hole" and the outer edge. Is Canny edge detection the best approach?


The goal is to find the line that represents the distance between the "hole" and the outer edge (transition from black to white). I was able to successfully binarize this photo and get a very clean black and white image. The next step would be to find the (almost) vertical line on it and calculate the perpendicular distance to the midpoint of this vertical line and the hole.

original picture with details of the task hole - zoomed in

ps: what I call "hole" is a shadow. I am shooting a laser into a hole. So the lines we can see is a steel and the black part without a line is a hole. The 2 white lines serve as a reference to measure the distance.

Is Canny edge detection the best approach? If so, what are good values for the A, B and C parameters? I can't tune it. I'm getting too much noise.


Solution

  • This is not complete; You have to take the time to reach the final result. But this idea might help you.

    Preprocessors:

    import os
    import cv2
    import numpy as np
    

    Main code:

    # Read original image
    dir = os.path.abspath(os.path.dirname(__file__))
    im = cv2.imread(dir+'/'+'im.jpg')
    h, w = im.shape[:2]
    print(w, h)
    
    # Convert image to Grayscale
    imGray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
    cv2.imwrite(dir+'/im_1_grayscale.jpg', imGray)
    
    # Eliminate noise and display laser light better
    imHLine = imGray.copy()
    imHLine = cv2.GaussianBlur(imHLine, (0, 9), 21)  # 5, 51
    cv2.imwrite(dir+'/im_2_h_line.jpg', imHLine)
    
    # Make a BW mask to find the ROI of laser array
    imHLineBW = cv2.threshold(imHLine, 22, 255, cv2.THRESH_BINARY)[1]
    cv2.imwrite(dir+'/im_3_h_line_bw.jpg', imHLineBW)
    
    # Remove noise with mask and extract just needed area
    imHLineROI = imGray.copy()
    imHLineROI[np.where(imHLineBW == 0)] = 0
    imHLineROI = cv2.GaussianBlur(imHLineROI, (0, 3), 6)
    imHLineROI = cv2.threshold(imHLineROI, 25, 255, cv2.THRESH_BINARY)[1]  # 22
    cv2.imwrite(dir+'/im_4_ROI.jpg', imHLineROI)
    
    # Found laser array and draw box around it
    cnts, _ = cv2.findContours(
        imHLineROI, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts.sort(key=lambda x: cv2.boundingRect(x)[0])
    pts = []
    for cnt in cnts:
        x2, y2, w2, h2 = cv2.boundingRect(cnt)
        if h2 < h/10:
            cv2.rectangle(im, (x2, y2), (x2+w2, y2+h2), (0, 255, 0), 1)
            pts.append({'x': x2, 'y': y2, 'w': w2, 'h': h2})
    
    circle = {
        'left': (pts[0]['x']+pts[0]['w'], pts[0]['y']+pts[0]['h']/2),
        'right': (pts[1]['x'], pts[1]['y']+pts[1]['h']/2),
    }
    circle['center'] = calculateMiddlePint(circle['left'], circle['right'])
    circle['radius'] = (circle['right'][0]-circle['left'][0])//2
    
    # Draw line and Circle inside it
    im = drawLine(im, circle['left'], circle['right'], color=(27, 50, 120))
    im = cv2.circle(im, circle['center'], circle['radius'], (255, 25, 25), 3)
    
    # Remove pepper/salt noise to find metal edge
    imVLine = imGray.copy()
    imVLine = cv2.medianBlur(imVLine, 17)
    cv2.imwrite(dir+'/im_6_v_line.jpg', imVLine)
    
    # Remove remove the shadows to find metal edge
    imVLineBW = cv2.threshold(imVLine, 50, 255, cv2.THRESH_BINARY)[1]
    cv2.imwrite(dir+'/im_7_v_bw.jpg', imVLineBW)
    
    # Finding the right vertical edge of metal
    y1, y2 = h/5, h-h/5
    x1 = horizantalDistance(imVLineBW, y1)
    x2 = horizantalDistance(imVLineBW, y2)
    pt1, pt2 = (x1, y1), (x2, y2)
    imVLineBW = drawLine(imVLineBW, pt1, pt2)
    cv2.imwrite(dir+'/im_8_v_bw.jpg', imVLineBW)
    
    # Draw lines
    im = drawLine(im, pt1, pt2)
    im = drawLine(im, calculateMiddlePint(pt1, pt2), circle['center'])
    
    # Draw final image
    cv2.imwrite(dir+'/im_8_output.jpg', im)
    

    Extra functions:
    Find the first white pixel in one line of picture:

    # This function only processes on a horizontal line of the image
    # Its job is to examine the pixels one by one from the right and
    # report the distance of the first white pixel from the right of
    # the image.
    def horizantalDistance(im, y):
        y = int(y)
        h, w = im.shape[:2]
        for i in range(0, w):
            x = w-i-1
            if im[y][x] == 255:
                return x
        return -1
    

    To draw a line in opencv:

    def drawLine(im, pt1, pt2, color=(128, 0, 200), thickness=2):
        return cv2.line(
            im,
            pt1=(int(pt1[0]), int(pt1[1])),
            pt2=(int(pt2[0]), int(pt2[1])),
            color=color,
            thickness=thickness,
            lineType=cv2.LINE_AA  # Anti-Aliased
        )
    

    To calculate middle point of two 2d points:

    def calculateMiddlePint(p1, p2):
        return (int((p1[0]+p2[0])/2), int((p1[1]+p2[1])/2))
    

    Output:
    Original image:
    enter image description here

    Eliminate noise and process to see the laser array better:
    enter image description here

    Find the laser area to extract the hole:
    enter image description here

    Work on another copy of image to find the right side of metal object:
    enter image description here

    Remove the shadows to better see the right edge:
    enter image description here

    The final output:
    enter image description here


    I first defined an ROI area. I changed the code later but did not change the names of the variables. If you were asked.