你的头怎么尖尖的:基于 OpenCV 的交互式角度测量

开头热梗:有一天,阿诺在直播中突然有人问他:“阿诺,你的头怎么尖尖的?”阿诺回答如下:
“那我问你,你是男的女的?如果你是女的你说这样的话,啊,那我问你,你你你是女孩子,那我问你,那那你那头顶是不是不是尖的?那我那我问你,你头顶是尖的呢?还是秃顶的,啊,还是,啊,染黄色染红色的,那我问你,啊,还是戴假发的,如果是如果你是男的那我问你,啊,你说我的头是尖的,那我问你那你是不是秃头?那你是不是光头?啊?你是光头还是有头发的,啊?那我问你,我头顶,我我头尖怎么了?我就尖怎么了?哎,我就尖怎么了?我就头顶尖怎么了?哎,我头顶尖你难道你看不惯么?我头顶就是尖的怎么了?我就是尖,我就是要尖怎么了,啊,你看不惯吗,啊。”
在这里插入图片描述
如果我们想知道头到底有多尖呢,这就不得不提基于 OpenCV 的交互式角度测量了。
一、工程背景
在机械制造中,零件加工的角度精度往往直接决定了产品的性能。例如刀具的刃口角度影响切削效率,焊缝的角度决定连接强度。如果工人在车间拍了一张零件照片,就能快速在图像上量取角度,无需专业测角工具,将大大提升效率。
在土木建筑领域,施工过程中梁柱的角度偏差可能影响整体结构稳定性。通过在工地拍摄的图片上进行快速角度测量,工程师能够即时发现问题。
在结构健康监测方面,无人机拍摄的桥梁或塔架照片,也可以通过图像测角发现变形趋势。这类基于图像的测量方法具有简便、直观、低成本的优势。
这里我们介绍一个简单的利用了 OpenCV 提供的鼠标事件和图像绘制功能,实现了“每 3 个点定义一个角度”的交互式测量方法。
二、主要功能
1、打开图像窗口;
2、鼠标点击三点(第 1 点为顶点,第 2 和第 3 点为两条边的端点),程序会在图像上绘制辅助线,并标注夹角值;
3、按 q 清空重新测量;按 ESC 或关闭窗口退出程序。
这个简短的 Python + OpenCV 脚本,展示了如何实现基于图像的交互式角度测量。这种方法实现简单、通用性强,适用于多种工程和科研场景。但是它依赖人工点击,效率和精度有限。
三、代码实现:
基于pycharm,先给出完整代码,然后给出细细致解释。
完整代码如下:

import cv2
import math

path = 'test2.jpg'
img = cv2.imread(path)
pointsList = []

def mousePoints(event,x,y,flags,params):
    if event == cv2.EVENT_LBUTTONDOWN:
        size = len(pointsList)
        if size != 0 and size % 3 != 0:
            cv2.line(img,tuple(pointsList[round((size-1)/3)*3]),(x,y),(0,0,255),2)
        cv2.circle(img,(x,y),5,(0,0,255),cv2.FILLED)
        pointsList.append([x,y])


def gradient(pt1,pt2):
    return (pt2[1]-pt1[1])/(pt2[0]-pt1[0])

def getAngle(pointsList):
    pt1, pt2, pt3 = pointsList[-3:]
    m1 = gradient(pt1,pt2)
    m2 = gradient(pt1,pt3)
    angR = math.atan((m2-m1)/(1+(m2*m1)))
    angD = round(math.degrees(angR))

    cv2.putText(img,str(angD),(pt1[0]-40,pt1[1]-20),cv2.FONT_HERSHEY_COMPLEX,
                1.5,(0,0,255),2)


cv2.namedWindow('Image')
cv2.setMouseCallback('Image', mousePoints)

while True:


    if len(pointsList) % 3 == 0 and len(pointsList) !=0:
        getAngle(pointsList)

    cv2.imshow('Image',img)

    if cv2.getWindowProperty('Image', cv2.WND_PROP_VISIBLE) < 1:
        break

    key = cv2.waitKey(1) & 0xFF
    if key == ord('q'):  # q:清空并重置
        pointsList = []
        img = cv2.imread(path)
    elif key == 27:  # ESC:退出程序
        break

cv2.destroyAllWindows()


运行结果如下:

在这里插入图片描述
看来是73度,也不算很尖嘛哈哈哈!!!!。
注意力强的小伙伴肯定发现了实际上这是个钝角。其实真实的角度是180-73=107度。当然这是算法的问题,修改也简单。
接下来是详细解释:
引用库与全局变量

import cv2

引入 OpenCV,用于读图、显示窗口、画线/画点、获取键盘/鼠标事件等。

import math

引入 math 数学库,用于反正切 atan、角度/弧度转换 degrees 等。

path = 'test.jpg'

指定待测的图片路径。后续 imread 会从这个路径载入图像。

img = cv2.imread(path)

读取图像到 NumPy 数组(BGR 通道)。若路径不对或文件损坏,这里会得到 None。

pointsList = []

全局列表,按点击顺序保存用户点击的像素坐标 [x, y]。每 3 个点构成一组角度的定义:第1个点=顶点,第2/3个点=两条边端点。
鼠标回调:点击取点 + 画辅助线/点

def mousePoints(event,x,y,flags,params):

定义鼠标事件回调函数。OpenCV 会把窗口中的鼠标事件(移动、按下、抬起等)传进来。

if event == cv2.EVENT_LBUTTONDOWN:

只处理 左键按下 事件。每次左键按下就“采点”。

size = len(pointsList)

当前已点击的点的数量,用于判断这一点击在“三点一组”里的位置(第1/2/3个)。

if size != 0 and size % 3 != 0:

若已经开始了当前这一组(size 不是 0)且还没满 3 个(size % 3 != 0,说明这次点击是第2或第3个点),就画一条辅助线方便可视化。

cv2.line(img,tuple(pointsList[round((size-1)/3)*3]),(x,y),(0,0,255),2)

画线:起点是“当前组的第一个点”(顶点),终点是本次点击位置 (x,y)。

round((size-1)/3)*3 算的是当前组首点在 pointsList 中的索引:

例如 size=1(正准备点第2个)、2(正准备点第3个),这个表达式都会回到本组三点的第一个点。

颜色 (0,0,255) 是 红色(BGR),线宽 2。

cv2.circle(img,(x,y),5,(0,0,255),cv2.FILLED)

在点击处画一个红色实心小圆,作“采点标记”,半径 5 像素。

pointsList.append([x,y])

把当前点击坐标加入列表,永久记录(直到你按 q 清空或退出)。

计算斜率(用于角度公式)

def gradient(pt1,pt2):
    return (pt2[1]-pt1[1])/(pt2[0]-pt1[0])

计算两点连线的斜率 :(y2-y1)/(x2-x1)

⚠️ 注意:当 x2 == x1(竖直线)会除零报错。生产环境建议改用向量法(见文末“稳健性建议”)。

计算并标注角度

def getAngle(pointsList):
    pt1, pt2, pt3 = pointsList[-3:]

取最近的 3 个点作为一个角:pt1 为顶点,pt2/pt3 为两条边的端点。

m1 = gradient(pt1,pt2)
m2 = gradient(pt1,pt3)

计算两条射线(顶点→端点)的斜率 m1、m2。

angR = math.atan((m2-m1)/(1+(m2*m1)))

用两直线夹角的斜率公式:
在这里插入图片描述

再 atan 得到弧度。
⚠️ 当 1 + m1*m2 == 0 时(两线垂直且斜率乘积为 -1),也会产生除零风险。

angD = round(math.degrees(angR))

把弧度转为角度(°),并四舍五入为整数,显示更友好。

cv2.putText(img,str(angD),(pt1[0]-40,pt1[1]-20),cv2.FONT_HERSHEY_COMPLEX,
            1.5,(0,0,255),2)

在顶点附近把角度值画到图上。字体 HERSHEY_COMPLEX,字号 1.5,红色,线宽 2。

文本位置 (pt1[0]-40, pt1[1]-20) 是顶点左上侧,避免挡住顶点。

创建窗口 + 绑定鼠标回调

cv2.namedWindow('Image')

先创建窗口(名字叫 ‘Image’),方便后面绑定鼠标事件。

cv2.setMouseCallback('Image', mousePoints)

给 ‘Image’ 窗口绑定一次鼠标回调。之后所有鼠标动作都会触发 mousePoints。

主循环:显示、计算、退出控制

while True:

程序主循环:不断刷新窗口、响应事件。

if len(pointsList) % 3 == 0 and len(pointsList) !=0:
    getAngle(pointsList)

每当点击数是 3 的倍数(且非 0)时,说明完整凑够一组三点,就计算并标注最新一组角度。

cv2.imshow('Image',img)

把当前图像(含画线、圆点、角度文字)显示到窗口。

if cv2.getWindowProperty('Image', cv2.WND_PROP_VISIBLE) < 1:
    break

如果用户点击了窗口右上角 X 关闭窗口,则退出循环(否则 imshow 下一帧会再次把窗口“弹回来”)。

key = cv2.waitKey(1) & 0xFF

监听键盘按键(等待 1ms)。返回值与 0xFF 取与,保证在不同平台/编码下稳定取到最低 8 位按键值。

if key == ord('q'):  # q:清空并重置
    pointsList = []
    img = cv2.imread(path)

按 q:清空所有已点击点,重新读取原图,于是所有画线、圆点、角度文字都被清除,便于重新开始一次或多次测量。

elif key == 27:  # ESC:退出程序
    break

按 ESC(27):退出主循环,准备收尾。

cv2.destroyAllWindows()

清理:销毁由 HighGUI 创建的所有窗口,释放资源。

这里使用斜率法可能产生除零与无穷斜率,可以试试把斜率法改为向量法更稳健:

import math
def angle_vec(p1, p2, p3):
    # 向量 u = p2 - p1, v = p3 - p1
    ux, uy = p2[0]-p1[0], p2[1]-p1[1]
    vx, vy = p3[0]-p1[0], p3[1]-p1[1]
    dot = ux*vx + uy*vy            # 点积
    det = ux*vy - uy*vx            # 2D 叉积等效(标量)
    ang = math.degrees(math.atan2(abs(det), dot))  # 0~180 的最小夹角
    return round(ang)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值