4

Edit: Quick Summary so far: I use the watershed algorithm but I have probably a problem with threshold. It didn't detect the brighter circles.

New: Fast radial symmetry transform approach which didn't quite work eiter (Edit 6).


I want to detect circles with different sizes. The use case is to detect coins on an image and to extract them solely. -> Get the single coins as single image files.

For this I used the Hough Circle Transform of open-cv: (https://docs.opencv.org/2.4/doc/tutorials/imgproc/imgtrans/hough_circle/hough_circle.html)

import sys
import cv2 as cv
import numpy as np


def main(argv):
    ## [load]
    default_file =  "data/newcommon_1euro.jpg"
    filename = argv[0] if len(argv) > 0 else default_file

    # Loads an image
    src = cv.imread(filename, cv.IMREAD_COLOR)

    # Check if image is loaded fine
    if src is None:
        print ('Error opening image!')
        print ('Usage: hough_circle.py [image_name -- default ' + default_file + '] \n')
        return -1
    ## [load]

    ## [convert_to_gray]
    # Convert it to gray
    gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
    ## [convert_to_gray]

    ## [reduce_noise]
    # Reduce the noise to avoid false circle detection
    gray = cv.medianBlur(gray, 5)
    ## [reduce_noise]

    ## [houghcircles]
    rows = gray.shape[0]
    circles = cv.HoughCircles(gray, cv.HOUGH_GRADIENT, 1, rows / 8,
                           param1=100, param2=30,
                           minRadius=0, maxRadius=120)
    ## [houghcircles]

    ## [draw]
    if circles is not None:
        circles = np.uint16(np.around(circles))
        for i in circles[0, :]:
            center = (i[0], i[1])
            # circle center
            cv.circle(src, center, 1, (0, 100, 100), 3)
            # circle outline
            radius = i[2]
            cv.circle(src, center, radius, (255, 0, 255), 3)
    ## [draw]

    ## [display]
    cv.imshow("detected circles", src)
    cv.waitKey(0)
    ## [display]

    return 0

if __name__ == "__main__":
    main(sys.argv[1:])

I tried all parameters (rows, param1, param2, minRadius, and maxRadius) to optimize the results. This worked very well for one specific image but other images with different sized coins didn't work.

Examples: Parameters circles = cv.HoughCircles(gray, cv.HOUGH_GRADIENT, 1, rows / 16, param1=100, param2=30, minRadius=0, maxRadius=120) enter image description here

With the same parameters: enter image description here

Changed to rows/8 enter image description here

I also tried two other approaches of this thread: writing robust (color and size invariant) circle detection with opencv (based on Hough transform or other features)

The approach of fireant leads to this result: enter image description here

The approach of fraxel didn't work either.

For the first approach: This happens with all different sizes and also the min and max radius. How could I change the code, so that the coin size is not important or that it finds the parameters itself?

Thank you in advance for any help!

Edit:

I tried the watershed algorithm of Open-cv, as suggested by Alexander Reynolds: https://docs.opencv.org/3.4/d3/db4/tutorial_py_watershed.html

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

img = cv.imread('data/P1190263.jpg')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)

# noise removal
kernel = np.ones((3,3),np.uint8)
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel, iterations = 2)

# sure background area
sure_bg = cv.dilate(opening,kernel,iterations=3)

# Finding sure foreground area
dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
ret, sure_fg = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)

# Finding unknown region
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg,sure_fg)

# Marker labelling
ret, markers = cv.connectedComponents(sure_fg)

# Add one to all labels so that sure background is not 0, but 1
markers = markers+1

# Now, mark the region of unknown with zero
markers[unknown==255] = 0

markers = cv.watershed(img,markers)
img[markers == -1] = [255,0,0]

#Display:
cv.imshow("detected circles", img)
cv.waitKey(0)

It works very well on the test image of the open-cv website:

enter image description here

But it performs very bad on my own images: enter image description here

I can't really think of a good reason why it's not working on my images?

Edit 2:

As suggested I looked at the intermediate images. The thresh looks not good in my opinion. Next, there is no difference between opening and dist_transform. The corresponding sure_fg shows the detected images.

thresh: thresh opening: opening dist_transform: dist_transform sure_bg: sure_bg sure_fg: sure_fg

Edit 3:

I tried all distanceTypes and maskSizes I could find, but the results were quite the same (https://www.tutorialspoint.com/opencv/opencv_distance_transformation.htm)

Edit 4:

Furthermore, I tried to change the (first) threshold function. I used different threshold values instead of the OTSU Function. The best one was with 160, but it was far from good:

enter image description here enter image description here

In the tutorial it looks like this: enter image description here

It seems like the coins are somehow too bright to be detected by this algorithm, but I don't know how to improve it?

Edit 5:

Changing the overall contrast and brightness of the image (with cv.convertScaleAbs) didn't improve the results. Increasing the contrast however should increase the "difference" between foreground and background, at least on the normal image. But it even got worse. The corresponding threshold image didn't improved (didn't get more white pixel).

Edit 6: I tried another approach, the fast radial symmetry transform (from here https://github.com/ceilab/frst_python)

import cv2
import numpy as np


def gradx(img):
    img = img.astype('int')
    rows, cols = img.shape
    # Use hstack to add back in the columns that were dropped as zeros
    return np.hstack((np.zeros((rows, 1)), (img[:, 2:] - img[:, :-2]) /     2.0, np.zeros((rows, 1))))


def grady(img):
    img = img.astype('int')
    rows, cols = img.shape
    # Use vstack to add back the rows that were dropped as zeros
    return np.vstack((np.zeros((1, cols)), (img[2:, :] - img[:-2, :]) / 2.0, np.zeros((1, cols))))


# Performs fast radial symmetry transform
# img: input image, grayscale
# radii: integer value for radius size in pixels (n in the original     paper); also used to size gaussian kernel
# alpha: Strictness of symmetry transform (higher=more strict; 2 is good place to start)
# beta: gradient threshold parameter, float in [0,1]
# stdFactor: Standard deviation factor for gaussian kernel
# mode: BRIGHT, DARK, or BOTH
def frst(img, radii, alpha, beta, stdFactor, mode='BOTH'):
    mode = mode.upper()
    assert mode in ['BRIGHT', 'DARK', 'BOTH']
    dark = (mode == 'DARK' or mode == 'BOTH')
    bright = (mode == 'BRIGHT' or mode == 'BOTH')

    workingDims = tuple((e + 2 * radii) for e in img.shape)

    # Set up output and M and O working matrices
    output = np.zeros(img.shape, np.uint8)
    O_n = np.zeros(workingDims, np.int16)
    M_n = np.zeros(workingDims, np.int16)

    # Calculate gradients
    gx = gradx(img)
    gy = grady(img)

    # Find gradient vector magnitude
    gnorms = np.sqrt(np.add(np.multiply(gx, gx), np.multiply(gy, gy)))

    # Use beta to set threshold - speeds up transform significantly
    gthresh = np.amax(gnorms) * beta

    # Find x/y distance to affected pixels
    gpx = np.multiply(np.divide(gx, gnorms, out=np.zeros(gx.shape), where=gnorms != 0),         
    radii).round().astype(int);
    gpy = np.multiply(np.divide(gy, gnorms, out=np.zeros(gy.shape), where=gnorms != 0),     
    radii).round().astype(int);

    # Iterate over all pixels (w/ gradient above threshold)
    for coords, gnorm in np.ndenumerate(gnorms):
        if gnorm > gthresh:
            i, j = coords
            # Positively affected pixel
            if bright:
                ppve = (i + gpx[i, j], j + gpy[i, j])
                O_n[ppve] += 1
                M_n[ppve] += gnorm
            # Negatively affected pixel
            if dark:
                pnve = (i - gpx[i, j], j - gpy[i, j])
                O_n[pnve] -= 1
                M_n[pnve] -= gnorm

    # Abs and normalize O matrix
    O_n = np.abs(O_n)
    O_n = O_n / float(np.amax(O_n))

    # Normalize M matrix
    M_max = float(np.amax(np.abs(M_n)))
    M_n = M_n / M_max

    # Elementwise multiplication
    F_n = np.multiply(np.power(O_n, alpha), M_n)

    # Gaussian blur
    kSize = int(np.ceil(radii / 2))
    kSize = kSize + 1 if kSize % 2 == 0 else kSize

    S = cv2.GaussianBlur(F_n, (kSize, kSize), int(radii * stdFactor))

    return S


img = cv2.imread('data/P1190263.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

result = frst(gray, 60, 2, 0, 1, mode='BOTH')

cv2.imshow("detected circles", result)
cv2.waitKey(0)

enter image description here I only get this nearly black output (it has some very dark grey shadows). I don't know what to change and would be thankful for help!

Cœur
  • 37,241
  • 25
  • 195
  • 267
chris
  • 155
  • 1
  • 10
  • 1
    Would not suggest Hough circle detection as it is not particularly robust and has too many hyper parameters. The OpenCV tutorial example of the watershed algorithm shows exactly this same task with far better results (and in general, it's a much more robust algorithm): https://docs.opencv.org/3.4/d3/db4/tutorial_py_watershed.html – alkasm Sep 11 '18 at 18:14
  • Also if all your images are like that, you could literally just do this with decent thresholding or probably some `floodFill()`ing with excellent results. – alkasm Sep 11 '18 at 18:16
  • Thank you very much for your answer! I tried the Open-cv watershed algorithm but it's not working very well on my own images (as you can see in the question). Do you have any idea why? – chris Sep 11 '18 at 20:00
  • 2
    Did you look at the image `thresh` and the other intermediate images? `thresh` should be a binary image where all coins are visible. I think that might not be the case for your image, as you're only finding the darker coins. – Cris Luengo Sep 11 '18 at 20:17
  • Now I looked at the intermediate images and added them in the question. Especially 'dist_transform' looks not as it's supposed to look like I think. – chris Sep 11 '18 at 20:58
  • As I tried to optimize the functions, it really looks like the threshold function isn't working good. But I have no idea to improve it? I already tried other threshold values and functions. Maybe if I increase the contrast of the images (so something like that) before I run it through the threshold function? As the coins seem to be looking too much like the background. – chris Sep 12 '18 at 17:07
  • I once tried to use opencv to detect cells in microscopic images. I ended up using it to create labeled datasets (by manually tweaking parameters for about 100 images) and used these images to train a convnet. Took some time and a GPU. Worked well in the end. The main benefit was that it was not necessary to do feature engineering. – Moritz Sep 19 '18 at 20:39
  • I'm trying pretty much the same. As you have seen I want to detect single coins in an image with multiple coins. They should be separated after that. I want to put each of the single coins in a Convnet. It would be awesome if you could share your experience for seperating the cells @Moritz . Maybe it works for coins as well. – chris Sep 19 '18 at 21:26
  • That was a tedious procedure and required a lot of manual work. I used ImageJ and played around with the threshold for every image to not have too much false positives. Afterwards I cleaned the results manually and used that as training set. At least for me (non expert) this was the most effective way. I do not know if it is a good idea but in your case you could try template matching together with a loop: http://scikit-image.org/docs/dev/auto_examples/features_detection/plot_template.html#sphx-glr-auto-examples-features-detection-plot-template-py and a treshold – Moritz Sep 20 '18 at 07:33
  • Wish I saw the updates to this, a very good effort in trying all these different approaches. I have some ideas, but you haven't provided some starting images. If you're still working on this, could you comment with a few images to start with? Your original `thresh` image from the watershed approach actually looks pretty good, barring that there's some little holes. Fill them up! Do some morphological closing first, which should give you (mostly) all white circles. Alternatively, use `floodFill()` to close the holes. See [here](https://stackoverflow.com/a/45971386/5087436). – alkasm Oct 01 '18 at 01:10

0 Answers0