如何过滤掉边缘检测到的非常嘈杂的圆点?

How can I filter out points of an edge-detected circle that are extremely noisy?

我正在研究检测被激光束照射的圆形孔径的中心和半径。该算法将从我无法物理控制的系统中提供图像(即调暗光源或调整激光位置。)我需要使用 C++ 执行此操作,并选择使用 openCV。

在某些区域,孔径的边缘很清晰,但在其他区域则非常嘈杂。我目前正在尝试隔离 "good" 点以进行 RANSAC 拟合,但我在此过程中采取了其他步骤。以下两张原图供参考:

我首先尝试做霍夫拟合。我执行了中值模糊以去除椒盐噪声,然后是高斯模糊,然后将图像馈送到 openCV 中的 HoughCircle 函数,滑块控制定义的 Hough 参数 1 和 2 here。结果是灾难性的:

然后我决定在将图像发送到 HoughCircle 之前尝试对图像进行更多处理。我从原始图像开始,进行中值模糊、高斯模糊、阈值化、扩张,进行 Canny 边缘检测,然后将 Canny 图像输入函数。

我最终能够对我的圆得到一个合理的估计,但是当手动减小 Hough 参数时,它大约出现了第 15 个圆。我手动绘制了紫色轮廓,绿色圆圈代表接近我手动估计的 Hough 输出。以下图片为:

  1. 没有膨胀的精明输出
  2. 带扩张的 Canny 输出
  3. 在原始图像上绘制的扩张 Canny 图像的 Hough 输出。

如您所见,无效圆的数量远远超过正确的圆,考虑到 Hough 变换 returns 这么多其他无效圆更严格的参数。

我目前有一些我实现的代码,它适用于我收到的所有测试图像,但代码是一个复杂的混乱,有许多看起来非常脆弱的可调参数。我所做的背后的驱动逻辑是注意到被激光良好照明的孔径边缘区域在多个阈值水平上相对恒定(下图所示)。

我在两个阈值级别进行了边缘检测,并存储了两个图像中重叠的点。目前,结果还存在一些不准确,因为孔径边缘仍然会随着不同的阈值水平而略有偏移。如有必要,我可以 post 很长的代码,但它背后的伪代码是:

1. Perform a median blur, followed by a Gaussian blur. Kernels are 9x9.
2. Threshold the image until 35% of the image is white. (~intensities > 30)
3. Take the Canny edges of this thresholded image and store (Canny1)
4. Take the original image, perform the same median and Gaussian blurs, but threshold with a 50% larger value, giving a smaller spot (~intensities > 45)
5. Perform the "Closing" morphology operation to further erode the spot and remove any smaller contours.
6. Perform another Canny to get the edges, and store this image (Canny2)
7. Blur both the Canny images with a 7x7 Gaussian blur.
8. Take the regions where the two Canny images overlap and say that these points are likely to be good points.
9. Do a RANSAC circle fit with these points.

我注意到检测到的边缘圆的某些区域可以被人眼很好地区分为最佳圆的一部分。有没有办法隔离这些区域以进行 RANSAC 拟合?

霍夫代码:

int houghParam1 = 100;
int houghParam2 = 100;
int dp = 10; //divided by 10 later
int x=616;
int y=444;
int radius = 398;
int iterations = 0;

int main()
{
namedWindow("Circled Orig");
namedWindow("Processed", 1);
namedWindow("Circles");
namedWindow("Parameters");
namedWindow("Canny");
createTrackbar("Param1", "Parameters", &houghParam1, 200);
createTrackbar("Param2", "Parameters", &houghParam2, 200);
createTrackbar("dp", "Parameters", &dp, 20);
createTrackbar("x", "Parameters", &x, 1200);
createTrackbar("y", "Parameters", &y, 1200);
createTrackbar("radius", "Parameters", &radius, 900);
createTrackbar("dilate #", "Parameters", &iterations, 20);
std::string directory = "Secret";
std::string suffix = ".pgm";
Mat processedImage;
Mat origImg;
for (int fileCounter = 2; fileCounter < 3; fileCounter++) //1, 12
{
    std::string numString = std::to_string(static_cast<long long>(fileCounter));
    std::string imageFile = directory + numString + suffix;
    testImage = imread(imageFile);
    Mat bwImage;
    cvtColor(testImage, bwImage, CV_BGR2GRAY);
    GaussianBlur(bwImage, processedImage, Size(9, 9), 9);
    threshold(processedImage, processedImage, 25, 255, THRESH_BINARY); //THRESH_OTSU
    int numberContours = -1;
    int iterations = 1;
    imshow("Processed", processedImage);
}

vector<Vec3f> circles;
Mat element = getStructuringElement(MORPH_ELLIPSE, Size(5, 5));
float dp2 = dp;
while (true)
{
    float dp2 = dp;
    Mat circleImage = processedImage.clone();
    origImg = testImage.clone();
    if (iterations > 0) dilate(circleImage, circleImage, element, Point(-1, -1), iterations);
    Mat cannyImage;
    Canny(circleImage, cannyImage, 100, 20);
    imshow("Canny", cannyImage);
    HoughCircles(circleImage, circles, HOUGH_GRADIENT, dp2/10, 5, houghParam1, houghParam2, 300, 5000);
    cvtColor(circleImage, circleImage, CV_GRAY2BGR);
    for (size_t i = 0; i < circles.size(); i++)
    {
        Scalar color = Scalar(0, 0, 255);
        Point center2(cvRound(circles[i][0]), cvRound(circles[i][1]));
        int radius2 = cvRound(circles[i][2]);
        if (abs(center2.x - x) < 10 && abs((center2.y - y) < 10) && abs(radius - radius2) < 20)  color = Scalar(0, 255, 0);
        circle(circleImage, center2, 3, color, -1, 8, 0);
        circle(circleImage, center2, radius2, color, 3, 8, 0);
        circle(origImg, center2, 3, color, -1, 8, 0);
        circle(origImg, center2, radius2,color, 3, 8, 0);
    }
    //Manual circles
    circle(circleImage, Point(x, y), 3, Scalar(128, 0, 128), -1, 8, 0);
    circle(circleImage, Point(x, y), radius, Scalar(128, 0, 128), 3, 8, 0);
    circle(origImg, Point(x, y), 3, Scalar(128, 0, 128), -1, 8, 0);
    circle(origImg, Point(x, y), radius, Scalar(128, 0, 128), 3, 8, 0);
    imshow("Circles", circleImage);
    imshow("Circled Orig", origImg);
    int x = waitKey(50);
}
Mat drawnImage;
cvtColor(processedImage, drawnImage, CV_GRAY2BGR);
return 1;
}

谢谢@jalconvolvon - 这是一个有趣的问题。这是我的结果: 我发现重要的是在原型制作时使用动态参数调整,因此我包括了我用来调整 Canny 检测的功能。该代码还对 Ransac 部分使用 答案。

import cv2
import numpy as np
import auxcv as aux
from skimage import measure, draw

def empty_function(*arg):
    pass

# tune canny edge detection. accept with pressing "C"
def CannyTrackbar(img, win_name):
    trackbar_name = win_name + "Trackbar"

    cv2.namedWindow(win_name)
    cv2.resizeWindow(win_name, 500,100)
    cv2.createTrackbar("canny_th1", win_name, 0, 255, empty_function)
    cv2.createTrackbar("canny_th2", win_name, 0, 255, empty_function)
    cv2.createTrackbar("blur_size", win_name, 0, 255, empty_function)
    cv2.createTrackbar("blur_amp", win_name, 0, 255, empty_function)

    while True:
        trackbar_pos1 = cv2.getTrackbarPos("canny_th1", win_name)
        trackbar_pos2 = cv2.getTrackbarPos("canny_th2", win_name)
        trackbar_pos3 = cv2.getTrackbarPos("blur_size", win_name)
        trackbar_pos4 = cv2.getTrackbarPos("blur_amp", win_name)
        img_blurred = cv2.GaussianBlur(img.copy(), (trackbar_pos3 * 2 + 1, trackbar_pos3 * 2 + 1), trackbar_pos4)
        canny = cv2.Canny(img_blurred, trackbar_pos1, trackbar_pos2)
        cv2.imshow(win_name, canny)

        key = cv2.waitKey(1) & 0xFF
        if key == ord("c"):
            break

    cv2.destroyAllWindows()
    return canny

img = cv2.imread("sphere.jpg")

#resize for convenience
img = cv2.resize(img, None, fx = 0.2, fy = 0.2)

#closing
kernel = np.ones((11,11), np.uint8)
img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

#sharpening
kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]])
img = cv2.filter2D(img, -1, kernel)

#test if you use different scale img than 0.2 of the original that I used
#remember that the actual kernel size for GaussianBlur is trackbar_pos3*2+1
#you want to get as full circle as possible here
#canny = CannyTrackbar(img, "canny_trakbar")

#additional blurring to reduce the offset toward brighter region
img_blurred = cv2.GaussianBlur(img.copy(), (8*2+1,8*2+1), 1)

#detect edge. important: make sure this works well with CannyTrackbar()
canny = cv2.Canny(img_blurred, 160, 78)

coords = np.column_stack(np.nonzero(canny))

model, inliers = measure.ransac(coords, measure.CircleModel,
                                min_samples=3, residual_threshold=1,
                                max_trials=1000)

rr, cc = draw.circle_perimeter(int(model.params[0]),
                               int(model.params[1]),
                               int(model.params[2]),
                               shape=img.shape)

img[rr, cc] = 1

import matplotlib.pyplot as plt
plt.imshow(img, cmap='gray')
plt.scatter(model.params[1], model.params[0], s=50, c='red')
plt.axis('off')
plt.savefig('sphere_center.png', bbox_inches='tight')
plt.show()

现在我可能会尝试计算哪些像素在统计上更亮,哪些像素更暗以调整激光位置(如果我理解正确的话)

如果Ransac还是不够。我会尝试调整 Canny 以仅检测圆顶上的完美弧线(轮廓清晰的地方),而不是尝试使用以下依赖项(我怀疑这应该是可能的):