Circle detection in a micro bearing

Hello all.
Trying again to bother you a bit with your experience :slight_smile:

I’m trying to detect the inner ring of my micro bearing to get a positionnal offset for my CNC.
In another post Mr. Crackwitz advised not to use hough transformation, but rather look for local maximas (Detecting balls in a bearing),
But here I’m trying to find the one and only circle that is the inner ring (a bit hidden by a cover though :thinking: )
I have a better camera for the focus and I’d say (but only I) that my lightning is all right, but so far the detection is so-so.
In another post (still with Mr. Crackwitz), I’ve read apparently HoughCircles detection is not so good and a newbie trap (Circle Detection Issues - #4 by crackwitz) but what makes me afraid in this post in the “fixing the position”. As it’s to calculate an offset for my cnc, the position will not be the exat same, even if normally very small position variations (few mm max) will happen.

Any way, here is my orginal image :thinking :

Here is the code I’m trying so far :

#!/usr/bin/env python
#pylint:disable=no-member

# BEC - 2024
# Python GUI (Tkinter) for the "Bench assembly for the micro bearing of Rheon"
#-------------------------------------------------------------------------------
import cv2
import numpy as np
#-------------------------------------------------------------------------------
# Simulate win_info for standalone usage
class ConsoleLogger:
    """A simple logger to print messages to the console."""
    @staticmethod
    def add(message, level="info"):
        levels = {"debug": "[DEBUG]", "info": "[INFO]", "warning": "[WARNING]", "error": "[ERROR]"}
        print(f"{levels.get(level.lower(), '[DEBUG]')} {message}")

#-------------------------------------------------------------------------------
def detectInnerRing(image_path, win_info):
    '''Detect the inner ring in image_path, return the processed image and offsets'''

    offset_x, offset_y = None, None

    image = cv2.imread(image_path)
    if image is None:
        win_info.add(f"Failed to read image at {image_path}", level="error")
        return None, None, None
    image_copy = cv2.imread(image_path)

    # Conversion en niveaux de gris
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    # Appliquer un flou léger pour réduire le bruit
    blurred = cv2.GaussianBlur(gray, (9, 9), 0)
    # Détection des contours
    edges = cv2.Canny(blurred, 100, 180) # 50 et 150 sont les seuils min et max

    # Détection des contours
    circles = cv2.HoughCircles(edges, cv2.HOUGH_GRADIENT, dp=1, minDist=1000, param1=180, param2=90, minRadius=80, maxRadius=150)

    # Si des cercles sont détectés
    if circles is not None:
        circles = np.round(circles[0, :]).astype("int")
        for (x, y, r) in circles:
            # Dessiner le cercle détecté
            cv2.circle(image, (x, y), r, (0, 255, 0), 4)
            # Ajouter un point au centre
            cv2.circle(image, (x, y), 5, (0, 0, 255), -1)
            win_info.add(f"Circle center detected at position: ({x}, {y})")
    else:
        win_info.add("No circles detected.", level="warning")
        return None, None, None

    detected_center_x, detected_center_y = circles[0][0], circles[0][1]

    # Centre du crosshair
    crosshair_center_x, crosshair_center_y = image.shape[1] // 2, image.shape[0] // 2
    win_info.add(f"Image size: ({image.shape[1]}, {image.shape[0]})", level="debug")
    win_info.add(f"Crosshair center position: ({crosshair_center_x}, {crosshair_center_y}", level="debug")

    # Draw crosshairs on the image
    height, width = image.shape[:2]
    # Calculate center of the image
    center_x, center_y = width // 2, height // 2
    # Draw vertical line
    cv2.line(image, (center_x, 0), (center_x, height), (0, 0, 255), 1)
    # Draw horizontal line
    cv2.line(image, (0, center_y), (width, center_y), (0, 0, 255), 1)
    cv2.circle(image, (center_x, center_y), 111, (0, 0, 255), 1)

    # Calcul de l'offset
    offset_x = detected_center_x - crosshair_center_x
    offset_y = detected_center_y - crosshair_center_y

    win_info.add(f"Offset in X: {offset_x} pixels")
    win_info.add(f"Offset in Y: {offset_y} pixels")

    # Affichage de l'offset sur l'image
    cv2.line(image, (crosshair_center_x, crosshair_center_y), (detected_center_x, detected_center_y), (255, 255, 0), 2)

    # Affichage des valeurs de l'offset sur l'image. Positionner le texte près du centre détecté
    text_position = (detected_center_x + 10, detected_center_y - 10)

    # Préparer le texte à afficher
    text_offset_x = f"Offset X: {offset_x} px"
    text_offset_y = f"Offset Y: {offset_y} px"

    # Afficher les offsets sur l'image
    cv2.putText(image, text_offset_x, text_position, cv2.FONT_HERSHEY_SIMPLEX,
                0.5, (0, 0, 0), 2)
    cv2.putText(image, text_offset_y, (text_position[0], text_position[1] + 25),
                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 2)

    return image, offset_x, offset_y

#-------------------------------------------------------------------------------
# Standalone execution
if __name__ == "__main__":
    # Path to the image for standalone testing
    image_path = "/home/stephane/Documents/ABBA/Soft/Images/InnerRingImages/inner_ring_image.jpg"

    # Use the console logger when running standalone
    logger = ConsoleLogger()

    # Run detection
    processed_image, offset_x, offset_y = detectInnerRing(image_path, logger)

    # Display intermediate and final images
    if processed_image is not None:
        # Reload images to show intermediate steps
        image = cv2.imread(image_path)
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        blurred = cv2.GaussianBlur(gray, (9, 9), 0)
        edges = cv2.Canny(blurred, 100, 180)

        cv2.imshow("Image originale", image)
        cv2.imshow("Image en niveaux de gris", gray)
        cv2.imshow("Image floue", blurred)
        cv2.imshow("Contours detectes", edges)
        cv2.imshow("Detected Inner Ring", processed_image)

        # Save the processed image
        output_path = "/home/stephane/Documents/ABBA/Soft/Images/InnerRingImages/processed_inner_ring.jpg"
        cv2.imwrite(output_path, processed_image)
        logger.add(f"Processed image saved to {output_path}")

        cv2.waitKey(0)
        cv2.destroyAllWindows()

And here is a result I’m having most of the time :

And rarely, but just to keep me motivated :slight_smile: :

Does anybody have a suggestion ?
The resolution and lightning seems alright (to me only :angel: ),
But am I missing something with this approach ?
Thanks a lot for your reading time and any advices,
Take care in all cases.

you can try cv.ximgproc_EdgeDrawing to find cirlcles

see edge_drawing.py