6

I am trying to find accurate locations for the corners on ink blotches as seen below:

My idea is to fit lines to the edges and then find where they intersect. As of now, I've tried using cv2.approxPolyDP() with various values of epsilon to approximate the edges, however this doesn't look like the way to go. My cv2.approxPolyDP code gives the following result:

Ideally, this is what I want to produce (drawn on paint):

Are there CV functions in place for this sort of problem? I've considered using Gaussian blurring before the threshold step although that method does not seem like it would be very accurate for corner finding. Additionally, I would like this to be robust to rotated images, so filtering for vertical and horizontal lines won't necessarily work without other considerations.

Code:*

import numpy as np
from PIL import ImageGrab
import cv2


def process_image4(original_image):  # Douglas-peucker approximation
    # Convert to black and white threshold map
    gray = cv2.cvtColor(original_image, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (5, 5), 0)
    (thresh, bw) = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    # Convert bw image back to colored so that red, green and blue contour lines are visible, draw contours
    modified_image = cv2.cvtColor(bw, cv2.COLOR_GRAY2BGR)
    contours, hierarchy = cv2.findContours(bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    cv2.drawContours(modified_image, contours, -1, (255, 0, 0), 3)

    # Contour approximation
    try:  # Just to be sure it doesn't crash while testing!
        for cnt in contours:
            epsilon = 0.005 * cv2.arcLength(cnt, True)
            approx = cv2.approxPolyDP(cnt, epsilon, True)
            # cv2.drawContours(modified_image, [approx], -1, (0, 0, 255), 3)
    except:
        pass
    return modified_image


def screen_record():
    while(True):
        screen = np.array(ImageGrab.grab(bbox=(100, 240, 750, 600)))
        image = process_image4(screen)
        cv2.imshow('window', image)
        if cv2.waitKey(25) & 0xFF == ord('q'):
            cv2.destroyAllWindows()
            break

screen_record()
  • A note about my code: I'm using screen capture so that I can process these images live. I have a digital microscope that can display live feed on a screen, so the constant screen recording will allow me to sample from the video feed and locate the corners live on the other half of my screen.
nathancy
  • 42,661
  • 14
  • 115
  • 137
Sam O
  • 73
  • 1
  • 5
  • 1
    What about getting two rectangles that will fit inside and outside of your area, and taking the rectangle which is the "average" of them? – alkasm Dec 18 '19 at 00:13
  • If what you need also must work on angled rectangles, consider adding another example image with that case. – alkasm Dec 18 '19 at 00:22
  • Thank you - I've added an image of a rotated sample – Sam O Dec 18 '19 at 00:36
  • Are the ink blotches always a rectangle shape? If its not how many corners are possible on the blotch? – coffeewin Dec 18 '19 at 10:56
  • Before any sensible answer can be given, you need to tell us where precisely the corners are. Because this is subjective. –  Dec 18 '19 at 11:27
  • The ink blotches will always be rectangular. Also, to answer Yves: yes that is true, it is quite subjective. I've spent much time wondering how to decide that myself. The way I settled on is this: the corners are defined by the intersection of two lines which pass through the edges of the blotch such that there is an equal amount of white and black overlap on either side. If the edge were a sine wave, the line would be the x axis. It's really not critical though, just a way to mitigate sensitivity to jagged edges (as opposed to bounding boxes which are very sensitive to outward extremes). – Sam O Dec 18 '19 at 23:38

2 Answers2

7

Here's a potential solution using thresholding + morphological operations:

  1. Obtain binary image. We load the image, blur with cv2.bilateralFilter(), grayscale, then Otsu's threshold

  2. Morphological operations. We perform a series of morphological open and close to smooth the image and remove noise

  3. Find distorted approximated mask. We find the bounding rectangle coordinates of the object with cv2.arcLength() and cv2.approxPolyDP() then draw this onto a mask

  4. Find corners. We use the Shi-Tomasi Corner Detector already implemented as cv2.goodFeaturesToTrack() for corner detection. Take a look at this for an explanation of each parameter


Here's a visualization of each step:

Binary image -> Morphological operations -> Approximated mask -> Detected corners

Here are the corner coordinates:

(103, 550)
(1241, 536)

Here's the result for the other images

(558, 949)
(558, 347)

Finally for the rotated image

(201, 99)
(619, 168)

Code

import cv2
import numpy as np

# Load image, bilaterial blur, and Otsu's threshold
image = cv2.imread('1.png')
mask = np.zeros(image.shape, dtype=np.uint8)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.bilateralFilter(gray,9,75,75)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

# Perform morpholgical operations
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (10,10))
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1)
close = cv2.morphologyEx(opening, cv2.MORPH_CLOSE, kernel, iterations=1)

# Find distorted rectangle contour and draw onto a mask
cnts = cv2.findContours(close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
rect = cv2.minAreaRect(cnts[0])
box = cv2.boxPoints(rect)
box = np.int0(box)
cv2.drawContours(image,[box],0,(36,255,12),4)
cv2.fillPoly(mask, [box], (255,255,255))

# Find corners
mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
corners = cv2.goodFeaturesToTrack(mask,4,.8,100)
offset = 25
for corner in corners:
    x,y = corner.ravel()
    cv2.circle(image,(x,y),5,(36,255,12),-1)
    x, y = int(x), int(y)
    cv2.rectangle(image, (x - offset, y - offset), (x + offset, y + offset), (36,255,12), 3)
    print("({}, {})".format(x,y))
    
cv2.imshow('image', image)
cv2.imshow('thresh', thresh)
cv2.imshow('close', close)
cv2.imshow('mask', mask)
cv2.waitKey()

Note: The idea for the distorted bounding box came from a previous answer in How to find accurate corner positions of a distorted rectangle from blurry image

nathancy
  • 42,661
  • 14
  • 115
  • 137
  • Unfortunately, this won't really fit the requirements for being able to approximate the rectangle when it's rotated---the morphological operations are doing a lot of work here to straighten the mask, which will only work with an upright rectangle. – alkasm Dec 18 '19 at 00:11
  • Thank you, this is greatly helpful. One question I have about this approach: wouldn't the bounded rectangle method using cv2.boundingRect be rather sensitive to extremes in deviations in the ink? Since the rectangle bounds the data, it seems like this method would likely report the corners to be further outward than they are. In fact, in your 4th image, the corner appears to be shifted outwards slightly. Also, would this method be robust to rotated samples? – Sam O Dec 18 '19 at 00:14
  • It shouldn't be sensitive to deviations in the ink since it uses Otsu's threshold to automatically calculate the threshold value. It seems to report the outwards corners since it draws a straight mask as alkasm mentioned. The corner will be shifted if your image is not straight. Unfortunately this method will probably not be robust to rotated samples – nathancy Dec 18 '19 at 02:29
  • 2
    @SamO, check the update. I found a way to get your desired result :) The corners should not be outward anymore since the bounding box should conform to the actual object better – nathancy Dec 18 '19 at 03:18
  • This is superb. Thank you for your excellent understanding and solution! – Sam O Dec 18 '19 at 23:39
1

After seeing the description of the corners, here is what I would recommend:

  • by any method, find the gross location of the corners (for instance by looking for the extreme values of (±X+±Y, ±X+±Y) or (±X, ±Y)).

  • consider a strip than joins two corners, with a certain width. Extract the pixels in that strip, on a portion close to the corner, rotate to make it horizontal and average the values along horizontals.

  • you will obtain a gray profile that tells you the accurate position of the edge, at the mean between the background and foreground intensities.

  • repeat on all four edges and at both ends. This will give you four accurate corners, by intersection.

enter image description here