一、任务需求

计算出二值图中圆弧图形与圆心之间的夹角,找出圆弧的起点和终点角度,并进行可视化。假设圆心已知(在前期任务中通过霍夫圆检测得来,具体参考)

原图如下所示:为二值图

opencv 二维点位集合寻找转折点 opencv 两点距离_opencv

二、问题分析

在二值图中计算圆弧图形的起点角度与终点角度存在一定困难(opencv没有直接提供相关api接口),需要自行找出圆弧图形的所有像素点,并计算其与圆心的夹角。然后找出夹角的最大值与最小值。

三、基本实现步骤

1、读取图像为灰度图并进二值化 【imread(“filename”,0),0:灰度图模式】
2、对图像的连通域进行优化处理【肉眼看到可能是一个连通域,实际上可能是多个连通域】
3、点集优化,对圆弧图形连通域进行拟合,只计算拐点的角度【圆弧图形上的点太多,可以削减大部分计算量】
4、计算所有拐点与圆心的夹角,找出角度的最大值与最小值

四、关键知识

4.1 将圆弧图形转化为点集

查找圆弧轮廓

查找轮廓需要用到findContours函数,但我们发现查找出来的连通域除了目标区域,还有一些小的干扰区域存在,我们可以在查找轮廓前先使用方框滤波(cv2.blur)进行降噪。如果降噪后的连通域个数还是不理想,需要调节findContours函数的参数。参数列表详情:

opencv 二维点位集合寻找转折点 opencv 两点距离_人工智能_02

找出轮廓的多边形拟合线(点集)

上述步骤找到的轮廓线上的点太多,需要对其进行拟合,只保留轮廓线上的拐点,这样可以 减少轮廓线上的点,减少运行时间。找出轮廓的多边形拟合线可使用approxPolyDP函数。
approxPolyDP()函数是opencv中对指定的点集进行多边形逼近的函数,其逼近的精度可通过参数设置。
对应的函数为:
void approxPolyDP(InputArray curve, OutputArray approxCurve, double epsilon, bool closed);

例如:approxPolyDP(contourMat, approxCurve, 10, true);//找出轮廓的多边形拟合曲线
第一个参数 InputArray curve:输入的点集
第二个参数OutputArray approxCurve:输出的点集,当前点集是能最小包容指定点集的。画出来即是一个多边形。
第三个参数double epsilon:指定的精度,也即是原始曲线与近似曲线之间的最大距离。
第四个参数bool closed:若为true,则说明近似曲线是闭合的;反之,若为false,则断开。
画出拟合后的轮廓线

用drawContours函数,将拟合后的轮廓线可视化,可以检测轮廓线的拟合是否准确。
cv2.drawContours()函数的功能是绘制轮廓,输入变量如下:

cv2.drawContours(image, contours, contourIdx, color, thickness=None, lineType=None, hierarchy=None, maxLevel=None, offset=None)

第一个参数image表示目标图像,
第二个参数contours表示输入的轮廓组,每一组轮廓由点vector构成,
第三个参数contourIdx指明画第几个轮廓,如果该参数为负值,则画全部轮廓,
第四个参数color为轮廓的颜色,
第五个参数thickness为轮廓的线宽,如果为负值或CV_FILLED表示填充轮廓内部,
第六个参数lineType为线型,
第七个参数为轮廓结构信息,
第八个参数为maxLevel

绘制结果如下:灰色的线条就是拟合后的轮廓线,可以看出少了很多的点。

opencv 二维点位集合寻找转折点 opencv 两点距离_ci_03

4.2 计算坐标角度

理论部分

根据圆上一点的坐标和圆心坐标,求点与圆心之间的夹角的计算公式如下图所示:根据圆心建立坐标系,并以x轴的正方向为起点。

opencv 二维点位集合寻找转折点 opencv 两点距离_opencv 二维点位集合寻找转折点_04

代码部分

参考
具体代码如下所示,math.atan2() 方法用于将矩形坐标 (x, y) 转换成极坐标 (r, theta),返回所得角 theta。该方法通过计算 y/x 的反正切值来计算相角 theta,范围为从 -pi 到 pi

def get_angle(cx, cy, x, y):
    '''
    :param cx: 圆心x
    :param cy: 圆心y
    :param x: 点x坐标
    :param y: 点y坐标
    :return: 
    '''
    dx = x - cx
    dy = y - cy
    print((dx,dy))
    # 计算两点之间的切线值,其返回值为弧度
    rads = math.atan2(dy, dx)
    # 将弧度值转换为0-2π的范围
    rads %= 2*math.pi
    # 将弧度转换为角度后
    degs = math.degrees(rads)
    return round(degs,2)
实际应用

给定多个点的坐标(既多边形拟合线的点)及圆心坐标,绘制的图形如下所示。

opencv 二维点位集合寻找转折点 opencv 两点距离_计算机视觉_05


代码如下所示,get_angle函数计算出的0度起点是x轴正方向,这里将其沿顺时针旋转90度,将6点钟方向作为0度起点。

for p in approx:
            x,y=p[0]
            angle360=get_angle(cx,cy,x,y)
            angle360-=90 #将起点方向由x轴正方向旋转到6点钟方向
            if angle360<0:
                angle360=angle360+360
            #每隔10个点,画一条线
            if p_i%28==0:
                cv2.line(circle_img,(cx,cy),(x,y),(255,0,0))
                cv2.putText(circle_img,'%.2f'%angle360,(x,y),1,1,(255,128,0))

五、具体实现

5.1 基本实现

根据关键知识4.1可以得出圆弧上的所有拐点,根据关键知识4.2可以计算出圆上点与圆心的夹角。要得到圆弧的起始点与终点,则需要遍历所有拐点,计算其与圆心的夹角,找出其中最大角度(圆弧的终点)与最小角度(圆弧的起点)。

其中需要注意,当圆弧横跨0度起点时,所得到的最大角度与最小角度是错误的,因为横跨0度起点时,最小角度通常为0~10度,最大角度通常为30~359.9度。

opencv 二维点位集合寻找转折点 opencv 两点距离_人工智能_06

5.2 实现代码

针对于5.1所描述的情况,我们将0度起点暂时定义为x轴正方向,此时计算的的最大角度与最小角度均与0度起点没有任何关系,故可以得到正确的最大最小角度。然后在将其旋转90°,即可得到正确的角度值。
完整代码如下:

import cv2,os
import numpy as np
import random,math
def get_angle(cx, cy, x, y):
    '''
    :param cx: 圆心x
    :param cy: 圆心y
    :param x: 点x坐标
    :param y: 点y坐标
    :return: 
    '''
    dx = x - cx
    dy = y - cy
    print((dx,dy))
    # 计算两点之间的切线值,其返回值为弧度
    rads = math.atan2(dy, dx)
    # 将弧度值转换为0-2π的范围
    rads %= 2*math.pi
    # 将弧度转换为角度后
    degs = math.degrees(rads)
    return round(degs,2)


def get_coil_head(img,circle_img,cx,cy):
    if True:
        #减少联通域的个数(肉眼看到是一个连通域,实际上是多个)
        img = cv2.blur(img, (3, 3)) #方框滤波,去除噪声
        #找连通域的轮廓(联通域个数不理想时,要调节参数)  参数见上图 
        contours, hierarchy = cv2.findContours(img,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

        #联通域上的点太多,对其进行拟合,只保留轮廓的拐点
        approx = cv2.approxPolyDP(contours[0], 0.5, True)
        h,w=img.shape                        
        #draw_img = np.ones((h,w,3),np.uint8)*255
        circle_img = cv2.cvtColor(circle_img,cv2.COLOR_GRAY2BGR)
        circle_img = cv2.drawContours(circle_img, [approx], -1, (0,0,255), 2)

        yd=np.pi/180
        p_i=0
        max_angle=-1
        max_ax,max_ay=0,0
        min_angle=361
        min_ax,min_ay=0,0


        #所有的点,都不经过原点时生效
        for p in approx:
            x,y=p[0]
            angle360=get_angle(cx,cy,x,y)
            angle360-=90 #将起点方向由x轴正方向旋转到6点钟方向
            if angle360<0:
                angle360=angle360+360
            
            if angle360>max_angle:
                max_angle=angle360
                max_ax,max_ay=x,y
            
            if angle360<min_angle:
                min_angle=angle360
                min_ax,min_ay=x,y

            #每隔10个点,画一条线
            # if p_i%28==0:
            #     cv2.line(circle_img,(cx,cy),(x,y),(255,0,0))
            #     cv2.putText(circle_img,'%.2f'%angle360,(x,y),1,1,(255,128,0))
            p_i+=1

            #按照随机概率划线
            # rad= random.random() #0~1之间的随机数
            # if rad<0.05:
            #     cv2.line(ret,(cx,cy),(x,y),(255,0,0))
        
        #当弧线上的点经过0度时,上述方法找出来的最大值与最小值不通用了
        if max_angle+min_angle>340 and max_angle+min_angle<380 and min_angle<30 and max_angle>340 :
            max_angle=-1
            max_ax,max_ay=0,0
            min_angle=361
            min_ax,min_ay=0,0
             
            for p in approx:
                x,y=p[0]
                angle360=get_angle(cx,cy,x,y)
                #print(x,y,angle,angle360)
                angle360-=90
                if angle360>max_angle:
                    max_angle=angle360
                    max_ax,max_ay=x,y
                
                if angle360<min_angle:
                    min_angle=angle360
                    min_ax,min_ay=x,y
            #print("这张图找出来的结果不对,已经进行矫正")



        cv2.line(circle_img,(cx,cy),(min_ax,min_ay),(255,0,0))
        cv2.putText(circle_img,'%.2f'%min_angle,(min_ax,min_ay),1,1,(128,128,0))


        cv2.line(circle_img,(cx,cy),(max_ax,max_ay),(255,0,0))
        cv2.putText(circle_img,'%.2f'%max_angle,(max_ax,max_ay),1,1,(128,128,0))


        cv2.circle(circle_img,(cx,cy),3,(255,0,255),-1)
        return circle_img

img=cv2.imread('txqy/a6.jpg',0)
#图像resize,减少轮廓的面积
img = cv2.resize(img,None,fx=0.5,fy=0.5)
h,w=img.shape
cx,cy=w//2,h//2 #假定的圆心(在实际代码中采用霍夫圆的圆心)

ret=get_coil_head(img,img,cx,cy)
cv2.imshow('ret',ret)
cv2.waitKey()

5.3 运行效果

opencv 二维点位集合寻找转折点 opencv 两点距离_opencv_07


opencv 二维点位集合寻找转折点 opencv 两点距离_opencv 二维点位集合寻找转折点_08


opencv 二维点位集合寻找转折点 opencv 两点距离_opencv_09