Python OpenCV学习第四天:Canny边缘检测+角点检测+轮廓检测

Python OpenCV学习第四天:Canny边缘检测+角点检测+轮廓检测

寒烟似雪
10小时前发布

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()

canny边缘

效果

这里要特别注意

  • 低阈值:低于这个值的边缘直接扔掉
  • 高阈值:高于这个值的边缘保留
  • 中间的:如果和高阈值边缘连在一起就保留,否则扔掉
  • 我自己测试了一下,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()

我用自己拍的一张桌上放了几个杯子的照片测试,居然真的把每个杯子都框出来了,还标了面积,太有成就感了!

五、今日学到的关键点

  1. Canny边缘检测的双阈值比例一般1:2或1:3,50和150是通用组合
  2. Shi-Tomasi角点检测比Harris好用,直接指定数量就行,不用调复杂参数
  3. 找轮廓前一定要先二值化或Canny,不然结果会很乱
  4. 轮廓筛选按面积来,能过滤掉大部分噪点,面积阈值根据图片实际情况调
  5. OpenCV 4.x的findContours返回两个值,3.x返回三个,别搞混了

六、明天要学的

  1. ORB特征点检测与匹配(用来找两张图里的同一个物体)
  2. 直方图均衡化(让图片更清晰)
  3. 模板匹配(在大图里找小图)
  4. 写一个简单的物体识别小程序

今天学了大概三个小时,主要是轮廓筛选那里花了点时间调阈值,不过看到最后框出来的结果,觉得一切都值了。现在终于能对图片做一些“有用”的处理了,不再是只能加加滤镜,越来越有意思了。

© 版权声明
THE END
喜欢就支持一下吧
点赞 0 分享 收藏
评论 抢沙发
OωO
取消