开头热梗:有一天,阿诺在直播中突然有人问他:“阿诺,你的头怎么尖尖的?”阿诺回答如下:
“那我问你,你是男的女的?如果你是女的你说这样的话,啊,那我问你,你你你是女孩子,那我问你,那那你那头顶是不是不是尖的?那我那我问你,你头顶是尖的呢?还是秃顶的,啊,还是,啊,染黄色染红色的,那我问你,啊,还是戴假发的,如果是如果你是男的那我问你,啊,你说我的头是尖的,那我问你那你是不是秃头?那你是不是光头?啊?你是光头还是有头发的,啊?那我问你,我头顶,我我头尖怎么了?我就尖怎么了?哎,我就尖怎么了?我就头顶尖怎么了?哎,我头顶尖你难道你看不惯么?我头顶就是尖的怎么了?我就是尖,我就是要尖怎么了,啊,你看不惯吗,啊。”
如果我们想知道头到底有多尖呢,这就不得不提基于 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)