Python OpenCV学习第四天:终于能自动框出物体了
昨天把几何变换和滤镜玩明白了,今天开始学更有意思的——边缘检测、角点检测和轮廓检测。一开始看这些概念觉得挺抽象的,什么梯度、非极大值抑制,头都大了,结果实际敲代码才发现,原来就是调几个参数的事,而且效果特别直观,能直接把图片里的物体边缘和角点标出来,最后写的自动框物体的小程序,跑出来的时候真的有点小激动。
今日目标完成情况
- Canny边缘检测和Sobel算子全搞懂
- Harris和Shi-Tomasi角点检测都会用
- 轮廓检测和轮廓特征计算(面积、周长、外接矩形)掌握
- 完成了实用小项目:自动筛选并框出图片中的物体
一、边缘检测:找到物体的轮廓线
边缘检测就是把图片中物体的边界找出来,是识别物体的第一步,常用的就是Canny和Sobel。
1. Canny边缘检测:最常用也最好用
Canny虽然步骤听起来多(高斯滤波→计算梯度→非极大值抑制→双阈值检测),但OpenCV都封装好了,一行代码就能搞定:
import cv2
import numpy as np
img = cv2.imread("test.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Canny边缘检测:(灰度图, 低阈值, 高阈值)
# 低阈值和高阈值比例一般是1:2或1:3,效果最好
edges = cv2.Canny(gray, 50, 150)
cv2.imshow("原图", img)
cv2.imshow("Canny边缘", edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
效果
这里要特别注意:
- 低阈值:低于这个值的边缘直接扔掉
- 高阈值:高于这个值的边缘保留
- 中间的:如果和高阈值边缘连在一起就保留,否则扔掉
- 我自己测试了一下,50和150这个组合对大多数图片都够用,要是边缘太多就把阈值调高点,太少就调低点。
2. Sobel算子:分方向计算梯度
Sobel是分别计算x方向(水平)和y方向(垂直)的梯度,然后合并起来:
# Sobel算子:(灰度图, 输出深度, x方向阶数, y方向阶数, 卷积核大小)
# 输出深度用cv2.CV_64F,不然负数会被截断,后面要转回来
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
# 转成uint8,不然显示出来是黑的
sobelx = cv2.convertScaleAbs(sobelx)
sobely = cv2.convertScaleAbs(sobely)
# 合并x和y方向的梯度
sobel = cv2.addWeighted(sobelx, 0.5, sobely, 0.5, 0)我对比了一下,Canny的效果比Sobel更干净,边缘更连续,所以平时用Canny更多,Sobel适合需要单独看某个方向边缘的情况。
二、角点检测:找到图片中“尖尖”(头发也行)的地方
角点就是两条边的交点,比如桌子的角、正方形的四个角,这些点在图像拼接、目标跟踪里特别有用。
1. Harris角点检测:经典但参数多
原理大概是:移动一个小窗口,如果窗口里的灰度变化很大,那就是角点。代码如下:
# Harris角点检测需要先把灰度图转成float32
gray_float = np.float32(gray)
# 参数:(灰度图, 窗口大小, Sobel卷积核大小, k值)
# k值一般取0.04-0.06,别改太大
dst = cv2.cornerHarris(gray_float, 2, 3, 0.04)
# 膨胀一下,让角点更明显
dst = cv2.dilate(dst, None)
# 标记角点:大于最大值1%的地方都标红
img[dst > 0.01 * dst.max()] = [0, 0, 255]踩坑:Harris的参数调起来有点麻烦,窗口大小和k值改一点,结果就差很多,而且标出来的角点有时候会连在一起。
2. Shi-Tomasi角点检测:改进版,更省心
这是Harris的优化版,效果更好,而且可以直接指定检测多少个角点,不用调那么多参数:
# 参数:(灰度图, 检测角点数量, 质量水平, 最小距离)
# 质量水平一般0.01-0.1,最小距离是角点之间的最小间隔
corners = cv2.goodFeaturesToTrack(gray, 100, 0.01, 10)
# 转成整数
corners = np.int0(corners)
# 画角点:画个小圆圈
for corner in corners:
x, y = corner.ravel()
cv2.circle(img, (x, y), 3, [0, 0, 255], -1)这个真的太好用了!指定100个角点就出来100个,而且分布很均匀,比Harris省心太多,平时用这个就行。
三、轮廓检测:终于能框物体了
轮廓检测是今天的重点!找到物体的轮廓后,就能计算它的面积、周长,还能画外接矩形把它框起来。
1. 找轮廓的步骤
注意:找轮廓前一定要先做二值化或者Canny边缘检测,不然找出来的轮廓会很乱:
# 步骤1:转灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 步骤2:高斯滤波去噪(可选,但推荐)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
# 步骤3:二值化(或者用Canny边缘检测)
ret, thresh = cv2.threshold(blur, 127, 255, cv2.THRESH_BINARY)
# 步骤4:找轮廓
# 参数:(二值图, 轮廓检索模式, 轮廓近似方法)
# 注意:OpenCV 4.x返回两个值(轮廓和层级),3.x返回三个,这里用4.x的写法
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 步骤5:画轮廓
# 参数:(原图, 轮廓列表, 要画的轮廓索引(-1表示画所有), 颜色, 线宽)
cv2.drawContours(img, contours, -1, (0, 255, 0), 2)2. 轮廓特征计算
找到轮廓后,就能算它的各种特征了,最常用的就是面积、周长和外接矩形:
# 遍历所有轮廓
for contour in contours:
# 1. 计算面积
area = cv2.contourArea(contour)
# 筛选:只保留面积大于500的轮廓,过滤掉小噪点
if area < 500:
continue
# 2. 计算周长(True表示闭合轮廓)
perimeter = cv2.arcLength(contour, True)
# 3. 画外接矩形(最常用!)
x, y, w, h = cv2.boundingRect(contour)
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 0, 255), 2)
# 4. 画最小外接圆(可选)
(x_circle, y_circle), radius = cv2.minEnclosingCircle(contour)
center = (int(x_circle), int(y_circle))
radius = int(radius)
cv2.circle(img, center, radius, (255, 0, 0), 2)筛选面积这步太关键了! 我一开始没加筛选,结果画出来一堆小框框,都是噪点,加了之后只保留大的物体,看起来清爽多了。
四、今日小项目:自动框出图片中的物体
把今天学的整合起来,写一个能自动找到图片中物体并画外接矩形的小程序:
import cv2
import numpy as np
def main():
img = cv2.imread("test.jpg")
if img is None:
print("图片读取失败!")
return
# 复制一份原图,用来画框
img_copy = img.copy()
# 转灰度、滤波、二值化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
ret, thresh = cv2.threshold(blur, 127, 255, cv2.THRESH_BINARY)
# 找轮廓
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 遍历轮廓,筛选并画框
for contour in contours:
area = cv2.contourArea(contour)
if area > 500: # 面积阈值根据自己的图片调整
x, y, w, h = cv2.boundingRect(contour)
cv2.rectangle(img_copy, (x, y), (x+w, y+h), (0, 0, 255), 2)
# 把面积写在框上面
cv2.putText(img_copy, f"Area:{int(area)}", (x, y-10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
# 显示结果
cv2.imshow("原图", img)
cv2.imshow("检测结果", img_copy)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == "__main__":
main()我用自己拍的一张桌上放了几个杯子的照片测试,居然真的把每个杯子都框出来了,还标了面积,太有成就感了!
五、今日学到的关键点
- Canny边缘检测的双阈值比例一般1:2或1:3,50和150是通用组合
- Shi-Tomasi角点检测比Harris好用,直接指定数量就行,不用调复杂参数
- 找轮廓前一定要先二值化或Canny,不然结果会很乱
- 轮廓筛选按面积来,能过滤掉大部分噪点,面积阈值根据图片实际情况调
- OpenCV 4.x的findContours返回两个值,3.x返回三个,别搞混了
六、明天要学的
- ORB特征点检测与匹配(用来找两张图里的同一个物体)
- 直方图均衡化(让图片更清晰)
- 模板匹配(在大图里找小图)
- 写一个简单的物体识别小程序
今天学了大概三个小时,主要是轮廓筛选那里花了点时间调阈值,不过看到最后框出来的结果,觉得一切都值了。现在终于能对图片做一些“有用”的处理了,不再是只能加加滤镜,越来越有意思了。