Detecting balls in a bearing

many_balls_in_bearing_with_light_good_side

Hello all,
I’m trying to detect (and count) the number of balls (I know the diameter 0,28mm) in a bearing.
So far I tried using opencv with HoughCircles, but the results are quite bad

I tried something like this :

import numpy as np
import cv2

# img = cv2.imread('/home/stephane/Documents/Bastien_Bearing/2_balls_sticked.jpg')
img = cv2.imread('/home/stephane/Documents/Bastien_Bearing/many_balls_in_bearing_with_light_good_side.jpg')

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.medianBlur(gray, 25) #cv2.bilateralFilter(gray,10,50,50)

# For 2 balls
minDist = 10
param1 = 9 
param2 = 20 
minRadius = 3
maxRadius = 15

# # For many balls
# minDist = 1
# param1 = 9
# param2 = 20
# minRadius = 1
# maxRadius = 15

circles = cv2.HoughCircles(blurred, cv2.HOUGH_GRADIENT, 1, minDist, param1=param1, param2=param2, minRadius=minRadius, maxRadius=maxRadius)

if circles is not None:
    circles = np.uint16(np.around(circles))
    print("Nombre de cercles détectés:", len(circles[0]))
    for i in circles[0,:]:
        cv2.circle(img, (i[0], i[1]), i[2], (0, 255, 0), 2)
else:
    print("Aucun cercle détecté.")

# Show result for testing:
cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Somebody told me that I was maybe going iin the wrong dirction,
That I should have source images (unfiltered, no median blur, no filtering, no adjusting). Getting a decent point light source and shut out all other light. you’ll get that light reflected in points on all the balls. those are local maxima. easily found.
So, first I’m not sure on how to find local maximas so far (but trying :innocent:),
Second I’m wondering If I should “simply” use a “filter” to look for RED areas :thinking:

If anybody has an idea to put mee on the right track it would be awesome :slight_smile:
Otherwise I’ll keep in touch if I found anything in the meantime.

related:

Sorry Crackwitz,
I hope it’s not a problem if it’s on 2 different forums,
I thought it might’ve been better to ask around a forum dedicated to opencv,
But if it seems to be a problem I can delete one and never do it again :innocent:

The point being that a crosspost should at least be disclosed. The debates and reasoning can be found in that link.

1 Like

Still trying,
The results are a bit better (with SimpleBlobDetector class, and the same parameters as before to isolate red color) but still false positives and missing spheres that should be counted.
It seems I cannot manage to use local maximas so far.

import cv2
import numpy as np

image = cv2.imread('/home/stephane/Documents/Bastien_Bearing/many_balls_in_bearing_with_light_good_side.jpg')

mask1 = cv2.inRange(image, (130, 50, 70), (200, 220, 235))
cv2.imshow("mask red color", mask1)

params = cv2.SimpleBlobDetector_Params()

params.filterByArea = True
# params.minArea = 30
# params.maxArea = 200


detector = cv2.SimpleBlobDetector_create(params)
keypoints = detector.detect(mask1)

blank = np.zeros((1, 1))
blobs = cv2.drawKeypoints(
    image, keypoints, blank, (0, 0, 255), cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
)

cv2.imshow("Blobs Using Area", blobs)
cv2.waitKey(0)
cv2.destroyAllWindows()

image

Hello all,

After playing for hours with my mask’s parameters for the “SimpleBlobDetector” (seemed promising), no better results (maybe I played wrongly).
If I’m trying local maximas as i was told it’s the simplest way. I think I’m doing something wrong (again) : not even a good positive (only hundreds of false positives).
I looked at this post to understand a bit, but it may not be the proper way for my case.

import cv2
import numpy as np

img = cv2.imread('many_balls_in_bearing_with_light_good_side.jpg')
img1 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 

localmax = cv2.dilate(img1, None, iterations=40)
localmax = (img1 == localmax)

mask = (img1 >= 0.3*255)

result = levelmask & localmax
(nlabels, labels, stats, centroids) = cv2.connectedComponentsWithStats(result.astype(np.uint8), connectivity=8)
print(nlabels-1, "found")

cv2.imshow("Original Image", img)
for i in range(1, nlabels): 
    centroid = centroids[i]
    x, y = int(centroid[0]), int(centroid[1])
    cv2.circle(img, (x, y), 5, (0, 255, 0), -1) # Rayon de 5, couleur verte

cv2.imshow("Composants detectes", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

But the results are horrible so far :slight_smile:
image
And I thought image processing would be fun :smiling_face_with_tear:

Still trying,
So I’m first applying a mask on my original image, just to focus on the micro spheres area,

Then I’m trying to find the local maximas or blob detection,
But I’m still stuck.

import cv2
import numpy as np

# Lire l'image
img = cv2.imread('/home/stephane/Documents/Bastien_Bearing/Many_Balls_In_Implant_1_Light.jpg')

# Obtenir les dimensions de l'image
hauteur, largeur = img.shape[:2]

# Spécifier les paramètres des cercles à dessiner
# Coordonnées du centre du premier cercle (x, y)
center1 = (373, 215)
# Coordonnées du centre du second cercle (x, y)
center2 = (373, 215)
# Rayon des cercles
radius1 = 165
radius2 = 143
# Couleur des cercles en BGR (Vert ici)
color = (0, 255, 0) # Vert
# # Épaisseur du contour du cercle (-1 pour un cercle plein)
# thickness = -1

# Créer un masque noir
masque = np.zeros((hauteur, largeur), dtype="uint8")
# Dessiner les cercles sur le masque
cv2.circle(masque, center1, radius1, 255, -1)  # Grand cercle blanc
cv2.circle(masque, center2, radius2, 0, -1)   # Petit cercle noir

# Appliquer le masque sur l'image
image_finale = cv2.bitwise_and(img, img, mask=masque)

# Afficher l'image résultante
cv2.imshow("Resultat", image_finale)

img1 = cv2.cvtColor(image_finale, cv2.COLOR_BGR2GRAY) 

localmax = cv2.dilate(img1, None, iterations=10)
localmax = (img1 == localmax)

levelmask = (img1 >= 0.3*255)

result = levelmask & localmax
(nlabels, labels, stats, centroids) = cv2.connectedComponentsWithStats(result.astype(np.uint8), connectivity=8)
print(nlabels-1, "found")

cv2.imshow("Original Image", img)
for i in range(1, nlabels): 
    centroid = centroids[i]
    x, y = int(centroid[0]), int(centroid[1])
    cv2.circle(img, (x, y), 5, (0, 255, 0), -1) # Rayon de 5, couleur verte
cv2.imshow("Composants detectes", img)

# params = cv2.SimpleBlobDetector_Params()
# # params.filterByArea = True
# # params.minArea = 10
# # params.maxArea = 200
# detector = cv2.SimpleBlobDetector_create(params)
# keypoints = detector.detect(image_finale)
# blank = np.zeros((1, 1))
# blobs = cv2.drawKeypoints(
#     image_finale, keypoints, blank, (0, 0, 255), cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
# )
# cv2.imshow("Blobs Using Area", blobs)

cv2.waitKey(0)
cv2.destroyAllWindows()

Just a small bit better for the local maximas method :
image

Still nothing with the blobs.

Still trying.
captured_image

I tried to :

  • Apply mask to check the right area (picture will be taken in same conditions), by drawing a black circle in a white circle in a black image (might not be the best solution)
    Then

bitwise_and

  • Afterward, gray filter and

bitwise_not

The result looks almost nice.

  • Then I’m wondering if threshold would be nice or necessary before using

cv2.findContours

But nothing seems to be working :frowning:


import cv2
import numpy as np

# load image to draw contours
image_color = cv2.imread(r'C:\Users\lhtc\Documents\Bastien\opencv\Bastien_Bearing\captured_image.jpg')

# Obtain dimensions
hauteur, largeur = image_color.shape[:2]

# Where to draw circle for the mask
center = (273, 225)
radius1 = 225
radius2 = 183
color = (0, 255, 0)

# # Draw to double check before masking
# cv2.circle(image_color, center, radius1, color, 2)
# cv2.circle(image_color, center, radius2, color, 2)


# Black mask
masque = np.zeros((hauteur, largeur), dtype="uint8")
# Dessiner les cercles sur le masque
cv2.circle(masque, center, radius1, 255, -1)  # big white circle
cv2.circle(masque, center, radius2, 0, -1)   # small black circle

# Apply mask
image_finale = cv2.bitwise_and(image_color, image_color, mask=masque)

# load gray image
image_gray = cv2.cvtColor(image_finale, cv2.COLOR_BGR2GRAY)
cv2.imshow('Image en niveaux de gris', image_gray)

# Invert image if necessary ?
image_gray_inv = cv2.bitwise_not(image_gray)
cv2.imshow('Image en niveaux de gris inversee', image_gray_inv)

# Appliquer un seuillage pour convertir l'image en binaire
_, thresh = cv2.threshold(image_gray_inv, 127, 255, cv2.THRESH_BINARY)
cv2.imshow('Image seuillée', thresh)

# Find contours
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

circle_count = 0

for contour in contours:
    ((x, y), radius) = cv2.minEnclosingCircle(contour)
    diameter = radius * 2
    if 5 <= diameter <= 10:
        area = cv2.contourArea(contour)
        if area > 0:
            circularity = (4 * np.pi * area) / (cv2.arcLength(contour, True) ** 2)
            if 0.7 < circularity <= 1.0:
                circle_count += 1
                # Draw contours
                cv2.drawContours(image_color, [contour], -1, (0,255,0), 2)

print(f"Number of detected circles : {circle_count}")

cv2.imshow('Result with circles', image_color)
cv2.waitKey(0)
cv2.destroyAllWindows()

I know that Mr @crackwitz told me to search for local maximas, but it seems I’m not managing :frowning:

In those pictures, that suggestion will never work.

You need better pictures.

The camera sensor and camera’s postprocessing are awful. There is so much noise suppression that the picture is nearly ruined.

The lighting… I hope I said something about the lighting.