对照市面上目前阶段大部分的找圆算法都是采用卡尺测量法进行测量,其原理大概为选择一个大的roi区域(这里我使用的圆形roi)进行初定位,在这个大的roi区域内选择首尾相连的卡尺(沿着法线方向绘制同等大小的矩形),每个卡尺(小矩形)就是一个小roi,对这个小roi区域内的像素求解其梯度(又分正极性和负极性),分为由白(亮)到黑(暗)和由黑(暗)向白(亮)两种,那么在计算梯度时,求解一阶导数就要区分其是内侧还是外侧,最后对每个小roi内求解到的梯度变化的点进行拟合(很多方式拟合圆,我这里使用的最小二乘法),这样就可以防止出现其余的点提取错误但是影响整个圆的查找,具有一定的鲁棒性,通过AI和一些理解写了一个python的代码,在此记录一下,有问题的话希望大家一起交流学习,下面附代码(python实现)
import cv2
import numpy as np
# 全局变量初始化
drawing = False
center = (0, 0)
radius = 0
polarity = 'positive'
num_calipers = 100
roi_length = 40
roi_width = 8
min_gray_diff = 50
edge_threshold = 30
def fit_circle_least_squares(points):
"""使用最小二乘法拟合圆"""
x = points[:, 0]
y = points[:, 1]
A = np.vstack([2*x, 2*y, np.ones_like(x)]).T
b = x**2 + y**2
c = np.linalg.lstsq(A, b, rcond=None)[0]
center_x, center_y = c[0], c[1]
radius = np.sqrt(c[2] + center_x**2 + center_y**2)
return (center_x, center_y), radius
def find_strongest_edge(bin_means, bins, polarity, edge_threshold, min_gray_diff):
"""寻找最显著的有效边缘点"""
gradients = np.gradient(bin_means)
# 找出所有候选边缘点
if polarity == 'positive':
candidates = np.where(gradients < -edge_threshold)[0]
else:
candidates = np.where(gradients > edge_threshold)[0]
# 如果没有候选点则返回None
if len(candidates) == 0:
return None
# 计算每个候选点的边缘强度
edge_strengths = []
for idx in candidates:
prev_idx = max(0, idx-1)
next_idx = min(len(bin_means)-1, idx+1)
if polarity == 'positive':
strength = bin_means[prev_idx] - bin_means[next_idx]
else:
strength = bin_means[next_idx] - bin_means[prev_idx]
# 只保留满足最小灰度差的边缘
if strength > min_gray_diff:
edge_strengths.append((abs(gradients[idx]), idx))
# 选择强度最大的边缘点
if len(edge_strengths) > 0:
edge_strengths.sort(reverse=True)
strongest_idx = edge_strengths[0][1]
return bins[strongest_idx] + (bins[1]-bins[0])/2
return None
def detect_edges_in_rois(gray_img, center, radius, num_calipers, roi_length, roi_width,
polarity='positive', edge_threshold=30, min_gray_diff=50):
"""改进的卡尺测量找圆主函数"""
vis_img = cv2.cvtColor(gray_img, cv2.COLOR_GRAY2BGR) if len(gray_img.shape) == 2 else gray_img.copy()
cv2.circle(vis_img, (int(center[0]), int(center[1])), int(radius), (0, 255, 0), 2)
points = []
angles = np.linspace(0, 2*np.pi, num_calipers, endpoint=False)
for angle in angles:
# 计算ROI位置和方向
x0 = center[0] + radius * np.cos(angle)
y0 = center[1] + radius * np.sin(angle)
dx, dy = center[0] - x0, center[1] - y0
norm = np.sqrt(dx**2 + dy**2)
# nx, ny = dx/norm, dy/norm
if polarity == 'positive':
nx, ny = -dx/norm, -dy/norm # 正极性指向圆外
arrow_color = (0, 255, 255) # 黄色箭头表示正极性
else:
nx, ny = dx/norm, dy/norm # 负极性指向圆内
arrow_color = (255, 255, 0) # 青色箭头表示负极性
tx, ty = -ny, nx
# 构建矩形ROI
half_len, half_wid = roi_length/2, roi_width/2
pts = np.array([
[x0 - half_len*nx - half_wid*tx, y0 - half_len*ny - half_wid*ty],
[x0 - half_len*nx + half_wid*tx, y0 - half_len*ny + half_wid*ty],
[x0 + half_len*nx + half_wid*tx, y0 + half_len*ny + half_wid*ty],
[x0 + half_len*nx - half_wid*tx, y0 + half_len*ny - half_wid*ty]
])
# 可视化ROI
pts_int = pts.astype(np.int32).reshape((-1,1,2))
cv2.polylines(vis_img, [pts_int], True, (255,0,0), 1)
# 在卡尺中心绘制极性方向箭头
arrow_length = 15
arrow_end = (int(x0 + arrow_length*nx), int(y0 + arrow_length*ny))
cv2.arrowedLine(vis_img, (int(x0), int(y0)), arrow_end, arrow_color, 2, tipLength=0.3)
# 提取ROI数据
mask = np.zeros_like(gray_img)
cv2.fillPoly(mask, [pts_int], 255)
roi = cv2.bitwise_and(gray_img, gray_img, mask=mask)
y_vals, x_vals = np.where(roi > 0)
if len(x_vals) == 0: continue
# 沿法线方向分析
vectors = np.vstack((x_vals-x0, y_vals-y0)).T
distances = vectors[:,0]*nx + vectors[:,1]*ny
intensities = gray_img[y_vals, x_vals]
# 分组统计
bins = np.linspace(-half_len, half_len, roi_length*2)
digitized = np.digitize(distances, bins)
bin_means = [np.mean(intensities[digitized==i]) if np.sum(digitized==i)>0 else 0
for i in range(1, len(bins))]
# 寻找最强边缘
edge_dist = find_strongest_edge(bin_means, bins, polarity, edge_threshold, min_gray_diff)
if edge_dist is not None:
edge_x = x0 + edge_dist * nx
edge_y = y0 + edge_dist * ny
points.append([edge_x, edge_y])
cv2.circle(vis_img, (int(edge_x), int(edge_y)), 3, (255,0,255), -1)
# 拟合圆
if len(points) >= 3:
points = np.array(points)
fitted_center, fitted_radius = fit_circle_least_squares(points)
# 打印检测成功信息
print("\n=== 圆检测成功 ===")
print(f"圆心坐标: ({fitted_center[0]:.2f}, {fitted_center[1]:.2f})")
print(f"半径: {fitted_radius:.2f} 像素")
print(f"使用的边缘点数量: {len(points)}")
# 在图像上绘制结果
cv2.circle(vis_img, (int(fitted_center[0]), int(fitted_center[1])),
int(fitted_radius), (0,0,255), 3)
cv2.circle(vis_img, (int(fitted_center[0]), int(fitted_center[1])),
7, (0,0,255), -1)
label = f"Center: ({fitted_center[0]:.1f}, {fitted_center[1]:.1f}) Radius: {fitted_radius:.1f}"
cv2.putText(vis_img, label, (10,30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,255), 2)
return points, fitted_center, fitted_radius, vis_img
else:
# 打印检测失败信息
print("\n=== 圆检测失败 ===")
print(f"原因: 只检测到 {len(points)} 个有效边缘点 (需要至少3个)")
print("建议调整:")
print("1. 检查初始圆是否接近真实圆边缘")
print("2. 尝试调整极性(按'd'键切换)")
print("3. 降低梯度阈值或最小灰度差要求")
# 在图像上绘制失败信息
cv2.putText(vis_img, "Detection failed: Not enough valid edges", (10,30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,255), 2)
return [], None, None, vis_img
def draw_circle(event, x, y, flags, param):
"""鼠标回调函数"""
global center, radius, drawing
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
center = (x, y)
elif event == cv2.EVENT_MOUSEMOVE:
if drawing:
radius = int(np.sqrt((x - center[0])**2 + (y - center[1])**2))
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
radius = int(np.sqrt((x - center[0])**2 + (y - center[1])**2))
print(f"Initial circle: center={center}, radius={radius}")
def set_parameters():
"""参数设置"""
global num_calipers, roi_length, roi_width, min_gray_diff, edge_threshold
print("\n=== 参数设置 ===")
try:
num_calipers = int(input(f"卡尺数量 (当前{num_calipers}): ") or num_calipers)
roi_length = int(input(f"矩形长度 (当前{roi_length}): ") or roi_length)
roi_width = int(input(f"矩形宽度 (当前{roi_width}): ") or roi_width)
min_gray_diff = int(input(f"最小灰度差 (当前{min_gray_diff}): ") or min_gray_diff)
edge_threshold = int(input(f"梯度阈值 (当前{edge_threshold}): ") or edge_threshold)
except ValueError:
print("输入无效,保持原参数")
def main():
global center, radius, polarity
# 读取图像
img = cv2.imread("10.jpg")
if img is None:
print("错误: 无法读取图像 '10.jpg'")
return
# 转换为灰度图
if len(img.shape) == 3:
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
else:
gray_img = img.copy()
# 创建窗口
cv2.namedWindow("Circle Detection", cv2.WINDOW_NORMAL)
cv2.setMouseCallback("Circle Detection", draw_circle)
print("操作指南:")
print("1. 鼠标左键绘制初始圆")
print("2. 按's'设置参数")
print("3. 按'd'切换极性")
print("4. 空格键执行检测")
print("5. ESC退出")
while True:
display_img = img.copy()
if radius > 0: # 只有当半径有效时才绘制
cv2.circle(display_img, center, radius, (0,255,0), 2)
# 显示状态
status = [
f"极性: {'正(亮→暗)' if polarity=='positive' else '负(暗→亮)'}",
f"卡尺: {num_calipers} 尺寸: {roi_length}x{roi_width}",
f"阈值: 梯度{edge_threshold} 灰度差{min_gray_diff}"
]
for i, text in enumerate(status):
cv2.putText(display_img, text, (10, 30+i*30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,255,255), 2)
cv2.imshow("Circle Detection", display_img)
key = cv2.waitKey(1) & 0xFF
if key == ord('s'):
set_parameters()
elif key == ord('d'):
polarity = 'negative' if polarity == 'positive' else 'positive'
print(f"极性切换为: {'正(亮→暗)' if polarity=='positive' else '负(暗→亮)'}")
elif key == 32 and radius > 0: # 空格键且半径有效
points, fitted_center, fitted_radius, result = detect_edges_in_rois(
gray_img, center, radius, num_calipers, roi_length, roi_width,
polarity, edge_threshold, min_gray_diff)
# 无论成功与否,都显示结果图像
cv2.namedWindow("Result", cv2.WINDOW_NORMAL)
cv2.imshow("Result", result)
elif key == 27: # ESC退出
break
cv2.destroyAllWindows()
if __name__ == "__main__":
main()