You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用OpenCV Python提取单轮廓中的多段圆弧半径?

多半径圆弧轮廓拟合问题及解决方案

问题描述

我希望用不同半径的圆弧段拟合轮廓。当轮廓仅包含单一半径的圆弧段时,使用cf.least_squares_circle()函数可以正常拟合出对应半径;但当轮廓包含多段不同半径的圆弧段时,该函数会得到平均化的半径结果。请问除了cf.least_squares_circle()外,还有其他可行方法吗?

现有代码

import cv2
import numpy as np
import circle_fit as cf

# load image as color to draw
img = cv2.imread(r'image.png')

##img Height:
print((img.shape[0]))
##Bildbreite:
print((img.shape[2]))

PY = []
PX = []

for j in range (img.shape[2]):
    for i in range (img.shape[0]):
        if img[i,j,0] != 0:
            PY.append(i)
            PX.append(j)

PXY = np.zeros((len(PX), 2))
for n in range(len(PX)):
    PXY[(n, 1)] = PX[n]
    PXY[(n, 0)] = PY[n]

xc, yc, r, _ = cf.least_squares_circle(PXY)
cv2.circle(img, (int(yc), int(xc)), int(r), (0, 0, 255), thickness=1, lineType=8, shift=0)
cv2.imshow("output", img)
cv2.waitKey(0)

相关示意图

  • 单一半径轮廓图
  • 多半径轮廓图
  • 当前结果图

可行解决方案

1. RANSAC随机抽样一致性拟合

RANSAC能从混合数据中筛选出符合单一圆弧模型的点集,反复迭代即可拟合出多段不同半径的圆弧,避免平均化问题。

示例代码:

import cv2
import numpy as np

def ransac_fit_circle(points, iterations=100, threshold=2.0):
    best_inliers = []
    best_params = None
    n_points = len(points)
    if n_points < 3:
        return None, []
    
    for _ in range(iterations):
        # 随机选取3个点用于拟合圆
        sample_indices = np.random.choice(n_points, 3, replace=False)
        sample = points[sample_indices]
        (x1,y1), (x2,y2), (x3,y3) = sample
        
        # 解方程组求圆心
        A = np.array([
            [2*(x2-x1), 2*(y2-y1)],
            [2*(x3-x1), 2*(y3-y1)]
        ])
        B = np.array([
            x2**2 + y2**2 - x1**2 - y1**2,
            x3**2 + y3**2 - x1**2 - y1**2
        ])
        try:
            xc, yc = np.linalg.solve(A, B)
            r = np.sqrt((x1 - xc)**2 + (y1 - yc)**2)
        except np.linalg.LinAlgError:
            continue
        
        # 计算所有点到圆的距离,筛选内点
        distances = np.abs(np.sqrt((points[:,0]-xc)**2 + (points[:,1]-yc)**2) - r)
        inliers = points[distances < threshold]
        
        if len(inliers) > len(best_inliers):
            best_inliers = inliers
            best_params = (xc, yc, r)
    return best_params, best_inliers

# 优化轮廓点提取逻辑
img = cv2.imread('image.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
points = np.vstack(contours).squeeze()

remaining_points = points.copy()
all_circles = []

# 迭代拟合多段圆弧,剩余点过少时停止
while len(remaining_points) > 10:
    circle_params, inliers = ransac_fit_circle(remaining_points)
    if not circle_params:
        break
    all_circles.append(circle_params)
    
    # 移除已拟合的内点
    mask = np.ones(len(remaining_points), dtype=bool)
    for idx, p in enumerate(remaining_points):
        if any((p == inlier).all() for inlier in inliers):
            mask[idx] = False
    remaining_points = remaining_points[mask]

# 绘制所有拟合结果
for xc, yc, r in all_circles:
    cv2.circle(img, (int(yc), int(xc)), int(r), (0,0,255), 1)
cv2.imshow('output', img)
cv2.waitKey(0)

2. 曲率分段后单独拟合

通过计算轮廓点的曲率变化找到分段边界,将轮廓拆分为多个圆弧段后,再分别用最小二乘法拟合。

示例代码:

import cv2
import numpy as np
import circle_fit as cf

# 计算轮廓点的曲率
def calculate_curvature(contour):
    curvatures = []
    n = len(contour)
    for i in range(n):
        p_prev = contour[(i-1)%n][0]
        p_curr = contour[i][0]
        p_next = contour[(i+1)%n][0]
        
        vec1 = p_curr - p_prev
        vec2 = p_next - p_curr
        cross = np.cross(vec1, vec2)
        norm1 = np.linalg.norm(vec1)
        norm2 = np.linalg.norm(vec2)
        
        if norm1 == 0 or norm2 == 0:
            curvatures.append(0)
        else:
            curvature = 2 * cross / (norm1 * norm2 * (norm1 + norm2))
            curvatures.append(np.abs(curvature))
    return curvatures

# 加载图像并提取轮廓
img = cv2.imread('image.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contour = contours[0]

# 计算曲率并找到分段点
curvatures = calculate_curvature(contour)
threshold = np.mean(curvatures) * 2
split_indices = [i for i, c in enumerate(curvatures) if c > threshold]
split_indices.append(0)  # 闭合轮廓,补全首尾
split_indices = sorted(list(set(split_indices)))

# 对每个分段单独拟合圆弧
for i in range(len(split_indices)-1):
    start = split_indices[i]
    end = split_indices[i+1]
    segment = contour[start:end]
    if len(segment) < 3:
        continue
    segment_points = np.array([p[0] for p in segment])
    xc, yc, r, _ = cf.least_squares_circle(segment_points)
    cv2.circle(img, (int(yc), int(xc)), int(r), (0,255,0), 1)

cv2.imshow('output', img)
cv2.waitKey(0)

3. 利用专业几何库分割拟合

使用shapely库将轮廓转换为几何线段,通过近似分割得到圆弧段后再拟合:

import cv2
import numpy as np
import circle_fit as cf
from shapely.geometry import LineString

# 加载图像提取轮廓点
img = cv2.imread('image.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
points = np.vstack(contours).squeeze()

# 转换为LineString并分割近似
line = LineString(points)
# tolerance参数控制近似精度,值越大分段越少
simplified = line.simplify(tolerance=1.0, preserve_topology=False)

# 对每个子段拟合圆弧
for geom in simplified.geoms:
    seg_points = np.array(list(geom.coords))
    if len(seg_points) >=3:
        xc, yc, r, _ = cf.least_squares_circle(seg_points)
        cv2.circle(img, (int(yc), int(xc)), int(r), (255,0,0),1)

cv2.imshow('output', img)
cv2.waitKey(0)

内容的提问来源于stack exchange,提问作者Michael

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 04:37:04