目录
四、Laplacian拉普拉斯算子(二阶微分,同时捕捉横竖边缘)
[六、全局阈值处理 threshold](#六、全局阈值处理 threshold)
[七、自适应阈值 adaptiveThreshold(光照不均图片专用)](#七、自适应阈值 adaptiveThreshold(光照不均图片专用))
[九、综合案例:标准预处理流水线(降噪→灰度→Canny 边缘)](#九、综合案例:标准预处理流水线(降噪→灰度→Canny 边缘))
[1. 梯度算子类(Sobel / Scharr / Laplacian)](#1. 梯度算子类(Sobel / Scharr / Laplacian))
[2. 阈值处理类(threshold / adaptiveThreshold)](#2. 阈值处理类(threshold / adaptiveThreshold))
[3. Canny 边缘检测类](#3. Canny 边缘检测类)
[4. 通用细节类](#4. 通用细节类)
上一章我们学习了图像各类滤波算法,掌握卷积、高斯、中值、双边滤波,主要用于图像降噪预处理。降噪之后,视觉任务里两个非常关键的操作就是提取图像边缘 和阈值分割。
边缘可以理解为图片里面物体轮廓,阈值分割把灰度图划分为前景、背景。物体识别、轮廓查找、目标计数都离不开这两套技术。
本章结合全部示例代码,依次学习 Sobel 索贝尔算子、Scharr 沙尔算子、Laplacian 拉普拉斯算子、Canny 边缘检测,以及全局阈值、自适应阈值处理。
一、前置知识回顾
- 边缘:图像像素灰度发生剧烈变化的位置,就是物体轮廓;
- 边缘检测前置建议:一般先做滤波降噪,再做边缘提取,减少噪点产生的伪边缘;
- 阈值处理只接受单通道灰度图,彩色图需要先转灰度;
- 梯度:像素明暗变化程度,算子本质就是计算图像梯度。
二、Sobel索贝尔算子(基础梯度边缘检测)
Sobel 算子分别计算 x 方向、y 方向梯度。
- dx=1, dy=0:计算 X 方向梯度,提取垂直边缘(图像左右明暗变化)。通俗理解:X 方向梯度看左右像素差异,突出物体竖线轮廓。
- dx=0, dy=1:计算 Y 方向梯度,提取水平边缘(图像上下明暗变化)。通俗理解:Y 方向梯度看上下像素差异,突出物体横线轮廓。
- 重要提示:官方建议分开计算 dx、dy,不要直接 dx=1, dy=1 ,直接合并输出效果差。
ddepth 是输出图像深度,不要填-1。Sobel 做微分会产生正负梯度,8 位图像 0-255 存不下负数,填-1会直接截断负数,丢掉一半边缘信息(半边边缘消失)。工程标准写法:先用cv2.CV_64F保存正负梯度,再用cv2.convertScaleAbs取绝对值转回正数。
python
# opencv索贝尔算子.py
import cv2
img = cv2.imread(r'D:\PYTHON\your name.jpg')
if img is None:
print("图片读取失败,请检查路径")
exit()
#x方向,提取垂直边缘
dx = cv2.Sobel(img, cv2.CV_64F, dx=1, dy=0, ksize=5)
dx = cv2.convertScaleAbs(dx)
#y方向,提取水平边缘
dy = cv2.Sobel(img, cv2.CV_64F, dx=0, dy=1, ksize=5)
dy = cv2.convertScaleAbs(dy)
#不推荐直接同时dx=1 dy=1,效果较差
dxy = cv2.Sobel(img, cv2.CV_64F, dx=1, dy=1, ksize=5)
dxy = cv2.convertScaleAbs(dxy)
#三种方式合并两个方向边缘
res1 = dx + dy
res2 = cv2.add(dx, dy)
res3 = cv2.addWeighted(dx, 0.5, dy, 0.5, 0)
cv2.imshow('原图', img)
cv2.imshow('res1', res1)
cv2.imshow('res2', res2)
cv2.imshow('res3', res3)
while 1:
key = cv2.waitKey(10)
if key == ord('q') or key == 27:
break
cv2.destroyAllWindows()
三种合并方式的差异:
res1 = dx + dy:numpy 数组直接相加,数值溢出会截断(超过 255 归 255);
res2 = cv2.add(dx, dy):OpenCV 饱和加法,超过 255 限制为 255;
res3 = cv2.addWeighted(dx, 0.5, dy, 0.5, 0):加权融合,可以手动控制两个方向权重。
拓展:梯度幅值的工程标准写法
上面用 addWeighted 是简化教学写法。真正数学上的梯度幅值是开平方:grad = sqrt(dx² + dy²),很多竞赛代码会这么写:
python
import numpy as np
grad = np.sqrt(dx**2 + dy**2)
grad = cv2.convertScaleAbs(grad)
说明:学习阶段用加权融合已经够用;竞赛或追求更精确梯度幅值的项目,可以用上面这种开平方写法。
适用场景:噪声较多的图片,Sobel自带一定平滑效果;对边缘定位精度要求一般的场合。
缺点:边缘线条偏粗,会产生部分伪边缘。
三、Scharr沙尔算子(Sobel优化版,捕捉细小边缘)
Scharr 本质是一组精度更高的 3×3 Sobel 卷积核,专门用来弥补 ksize=3 时 Sobel 算子精度不足的问题。它并不是独立的新算法,而是 Sobel 的升级版本,卷积核固定 3×3,不需要设置 ksize,对微弱细小边缘更敏感。同样需要分开计算 x、y 方向,再合并结果。
python
# opencv沙尔算子.py
import cv2
img = cv2.imread(r'D:\PYTHON\your name.jpg')
if img is None:
print("图片读取失败,请检查路径")
exit()
#x方向垂直边缘
dx = cv2.Scharr(img, cv2.CV_64F, dx=1, dy=0)
dx = cv2.convertScaleAbs(dx)
#y方向水平边缘
dy = cv2.Scharr(img, cv2.CV_64F, dx=0, dy=1)
dy = cv2.convertScaleAbs(dy)
#三种合并方式
res1 = dx + dy
res2 = cv2.add(dx, dy)
res3 = cv2.addWeighted(dx, 0.5, dy, 0.5, 0)
cv2.imshow('res1', res1)
cv2.imshow('res2', res2)
cv2.imshow('res3', res3)
while 1:
key = cv2.waitKey(10)
if key == ord('q'):
break
cv2.destroyAllWindows()
适用:希望捕捉图片细微轮廓、纹理边缘。
四、Laplacian拉普拉斯算子(二阶微分,同时捕捉横竖边缘)
Sobel、Scharr属于一阶微分;Laplacian是二阶微分算子,可以同时捕捉水平+垂直两个方向边缘,也可以用来做图像锐化。通俗类比:一阶微分看像素变化快慢,二阶微分看变化的快慢程度,因此能同时抓取两个方向轮廓。
- ksize为卷积核大小,取奇数;ksize越大轮廓越明显。
python
# opencv拉普拉斯算子.py
import cv2
img = cv2.imread(r'D:\PYTHON\your name.jpg')
if img is None:
print("图片读取失败,请检查路径")
exit()
#先用CV_64F保存正负梯度,再用convertScaleAbs取绝对值
res1 = cv2.Laplacian(img, cv2.CV_64F, ksize=3)
res1 = cv2.convertScaleAbs(res1)
res2 = cv2.Laplacian(img, cv2.CV_64F, ksize=9)
res2 = cv2.convertScaleAbs(res2)
cv2.imshow("res1", res1)
cv2.imshow("res2", res2)
while 1:
key = cv2.waitKey(10)
if key == ord('q'):
break
cv2.destroyAllWindows()
注意:拉普拉斯算子对噪声比较敏感,建议图片先做降噪滤波,再调用Laplacian 。
另外 ddepth 同样不要填-1 。Laplacian 是二阶微分,也会产生正负梯度,8 位图像 0-255 存不下负数,填-1会直接截断负数,丢失一半边缘信息。工程标准写法:先用cv2.CV_64F保存正负梯度,再用cv2.convertScaleAbs取绝对值转回正数。
五、Canny边缘检测(工业最常用双阈值边缘算法)
Canny是一套完整边缘检测算法,内部包含降噪、梯度计算、非极大值抑制、双阈值筛选。通俗理解四步:
高斯降噪 :提前平滑图像,抑制噪点;
Sobel 梯度计算 :求取像素梯度大小与方向;
非极大值抑制 :把粗边缘"削细",只保留边缘峰值;
双阈值筛选 :区分强边缘、弱边缘,过滤虚假边缘。
函数:cv2.Canny(图像, 低阈值, 高阈值)
规则:
- 像素高于高阈值 → 确定为强边缘,保留;
- 像素低于低阈值 → 直接丢弃;
- 介于高低阈值之间:只有和强边缘相连才保留,否则丢弃。
阈值调小,检测出来边缘细节变多;阈值调大,只保留明显的大轮廓。
python
# opencv Canny边缘检测.py
import cv2
#直接读取为灰度图(工程惯例:Canny 输入灰度图)
img = cv2.imread(r'D:\PYTHON\your name.jpg', cv2.IMREAD_GRAYSCALE)
if img is None:
print("图片读取失败,请检查路径")
exit()
#三组不同阈值对比效果
res1 = cv2.Canny(img,60,130)
res2 = cv2.Canny(img,100,200)
res3 = cv2.Canny(img,80,150)
cv2.imshow("res1", res1)
cv2.imshow("res2", res2)
cv2.imshow("res3", res3)
while 1:
key = cv2.waitKey(10)
if key == ord('q'):
break
cv2.destroyAllWindows()
调参经验:高阈值一般取低阈值的2~3倍。例如低阈值 60、高阈值 130,或低阈值 100、高阈值 200,都是比较常用的比例。这样既能保留主要轮廓,又能过滤掉大量弱边缘和噪点。如果边缘断裂严重,可以整体调低两个阈值;如果伪边缘太多,就整体调高。
Canny优势:输出的边缘线条细,干扰少,机器视觉项目高频使用。
实操小技巧:Canny 虽然可以直接接收彩色图,但工程惯例是输入灰度图。建议流程:先转灰度,再传入Canny之前用GaussianBlur做一次高斯模糊,抑制噪点带来的多余边缘。
六、全局阈值处理 threshold
全局阈值:整张图片使用同一个分割阈值,把像素划分为黑白两类,实现前景背景分离。
retval, dst = cv2.threshold(灰度图, thresh, maxval, type)
- retval:实际使用的阈值;
- dst:输出二值图像;
type常用模式:
- THRESH_BINARY:大于阈值取maxval,其余0;
- THRESH_BINARY_INV:反向二值,小于等于阈值取maxval,其余0。适合物体颜色暗、背景亮的场景,把前景目标变成白色,方便后续轮廓查找;
- THRESH_TOZERO、THRESH_TRUNC等截断模式。
OTSU 大津阈值(自动计算最优阈值) :
上面手动填 thresh 的方式需要自己试值。OTSU 大津法可以自动计算最优全局阈值,不用手动填写 thresh,用法如下:
python
retval, dst = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
这里 thresh 填 0 即可,OTSU 会根据灰度直方图自动算出最佳分割阈值并返回给 retval。光照均匀的场景非常好用,是工程里非常高频的写法。
python
# OTSU大津阈值自动求阈值示例
import cv2
gray = cv2.imread(r'D:\PYTHON\your name.jpg', cv2.IMREAD_GRAYSCALE)
ret, dst = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
cv2.imshow("otsu", dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
python
# opencv阈值处理.py
import cv2
#直接读取为灰度图
img = cv2.imread(r'D:\PYTHON\your name.jpg',cv2.IMREAD_GRAYSCALE)
if img is None:
print("图片读取失败,请检查路径")
exit()
threshold_value = 100
max_value = 255
#二进制全局阈值
retval, thresholded_image = cv2.threshold(img, threshold_value, max_value, cv2.THRESH_BINARY)
cv2.imshow("thresholded_image", thresholded_image)
while 1:
key = cv2.waitKey(10)
if key == ord('q'):
break
cv2.destroyAllWindows()
短板:如果图片光照不均匀,整张图用固定一个阈值效果很差,这时就要用自适应阈值。
七、自适应阈值 adaptiveThreshold(光照不均图片专用)
自适应阈值不会整张图片用固定数值;每个像素根据周边一小块区域自动计算本地阈值。适合明暗分布不均、局部光照变化大的照片。
cv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C)
- blockSize:局部窗口大小,必须奇数;
- C:减去的微调常数;
- 两种算法:ADAPTIVE_THRESH_MEAN_C均值、ADAPTIVE_THRESH_GAUSSIAN_C高斯加权均值。
python
# opencv阈值自适应.py
import cv2
img = cv2.imread(r'D:\PYTHON\your name.jpg',cv2.IMREAD_GRAYSCALE)
if img is None:
print("图片读取失败,请检查路径")
exit()
max_value = 255
adaptive_method = cv2.ADAPTIVE_THRESH_GAUSSIAN_C
threshold_type = cv2.THRESH_BINARY
block_size = 11
C = 2
new_img = cv2.adaptiveThreshold(img, max_value, adaptive_method, threshold_type, block_size, C)
cv2.imshow("new_img", new_img)
while 1:
key = cv2.waitKey(10)
if key == ord('q'):
break
cv2.destroyAllWindows()
适用场景:书本扫描件、逆光拍摄图片、光照明暗变化大的图像。
快速选型口诀 :
光线均匀、光照稳定 → 优先全局阈值 + OTSU,速度快;
逆光、扫描文档、局部明暗差异大 → 直接自适应阈值;
自适应阈值速度慢,实时摄像头项目谨慎高频使用。
八、算子与阈值工具对比简表
| 工具 | 类型 | 核心作用 | 适合场景 |
|---|---|---|---|
| Sobel | 一阶微分 | 分别提取水平、垂直边缘 | 带噪声图像,一般精度边缘 |
| Scharr | 一阶微分 | 捕捉细小微弱边缘 | 需要提取细微纹理 |
| Laplacian | 二阶微分 | 同时提取双向边缘、锐化 | 图片提前降噪,追求轮廓强化 |
| Canny | 完整边缘算法 | 精细干净的轮廓边缘 | 机器视觉、物体轮廓提取(推荐首选) |
| threshold 全局阈值 | 阈值分割 | 整张图固定阈值二值化 | 光照均匀的图片 |
| adaptiveThreshold 自适应阈值 | 阈值分割 | 局部动态计算阈值 | 光照不均匀、扫描文档类图片 |
九、综合案例:标准预处理流水线(降噪→灰度→Canny 边缘)
工程中标准流程:原图 → 高斯降噪 → 转灰度 → Canny 边缘检测
python
import cv2
img = cv2.imread(r'D:\PYTHON\your name.jpg')
if img is None:
print("图片读取失败,请检查路径")
exit()
#1.高斯降噪
blur = cv2.GaussianBlur(img,(3,3),0)
#2.转为灰度图
gray = cv2.cvtColor(blur,cv2.COLOR_BGR2GRAY)
#3.Canny边缘提取
edge = cv2.Canny(gray,70,160)
cv2.imshow('原图',img)
cv2.imshow('Canny输出边缘',edge)
cv2.waitKey(0)
cv2.destroyAllWindows()
十、本章高频踩坑清单
下面把本章常见坑点按「梯度算子」「阈值处理」「Canny 边缘检测」「通用细节」四类整理,并为每一条配上错误写法与正确写法对比,方便对照排查。
1. 梯度算子类(Sobel / Scharr / Laplacian)
坑点 1:Sobel / Scharr 不要同时开启 dx=1, dy=1
错误写法:
python
# 直接同时计算两个方向,边缘效果差
dxy = cv2.Sobel(img, cv2.CV_64F, dx=1, dy=1, ksize=5)
正确写法:
python
# 分开计算 x、y 方向,再合并
dx = cv2.Sobel(img, cv2.CV_64F, dx=1, dy=0, ksize=5)
dy = cv2.Sobel(img, cv2.CV_64F, dx=0, dy=1, ksize=5)
dx = cv2.convertScaleAbs(dx)
dy = cv2.convertScaleAbs(dy)
res = cv2.addWeighted(dx, 0.5, dy, 0.5, 0)
坑点 2:Sobel / Scharr 的 ddepth 不要填 -1
错误写法:
python
# 填 -1 会截断负数梯度,丢失半边边缘
dx = cv2.Sobel(img, -1, dx=1, dy=0, ksize=5)
正确写法:
python
# 先用 CV_64F 保存正负梯度,再取绝对值
dx = cv2.Sobel(img, cv2.CV_64F, dx=1, dy=0, ksize=5)
dx = cv2.convertScaleAbs(dx)
坑点 3:Laplacian 的 ddepth 同样不要填 -1
错误写法:
python
# 二阶微分也会产生正负梯度,填 -1 会截断负数
res = cv2.Laplacian(img, -1, ksize=3)
正确写法:
python
# 同样用 CV_64F + convertScaleAbs
res = cv2.Laplacian(img, cv2.CV_64F, ksize=3)
res = cv2.convertScaleAbs(res)
坑点 4:Laplacian 对噪声敏感,使用前务必先降噪
错误写法:
python
# 直接对含噪原图做拉普拉斯,会放大噪点
res = cv2.Laplacian(img, cv2.CV_64F, ksize=3)
正确写法:
python
# 先高斯降噪,再做拉普拉斯
blur = cv2.GaussianBlur(img, (3, 3), 0)
res = cv2.Laplacian(blur, cv2.CV_64F, ksize=3)
res = cv2.convertScaleAbs(res)
坑点 5:Scharr 算子没有 ksize 参数
错误写法:
python
# Scharr 卷积核固定 3×3,传 ksize 会报错
dx = cv2.Scharr(img, cv2.CV_64F, dx=1, dy=0, ksize=3)
正确写法:
python
# 不需要设置 ksize
dx = cv2.Scharr(img, cv2.CV_64F, dx=1, dy=0)
2. 阈值处理类(threshold / adaptiveThreshold)
坑点 6:threshold、adaptiveThreshold 只能接收灰度图
错误写法:
python
# 直接对彩色图做阈值处理会报错
ret, dst = cv2.threshold(img, 100, 255, cv2.THRESH_BINARY)
正确写法:
python
# 先转灰度,再做阈值处理
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, dst = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY)
坑点 7:adaptiveThreshold 的 blockSize 必须是奇数
错误写法:
python
# blockSize 为偶数会报错
new_img = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 10, 2)
正确写法:
python
# blockSize 取奇数,如 11
new_img = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
3. Canny 边缘检测类
坑点 8:Canny 工程惯例输入灰度图,标准流程是降噪→灰度→Canny
错误写法:
python
# 直接对彩色原图做 Canny,不符合工程惯例
edge = cv2.Canny(img, 70, 160)
正确写法:
python
# 标准流程:降噪 → 灰度 → Canny
blur = cv2.GaussianBlur(img, (3, 3), 0)
gray = cv2.cvtColor(blur, cv2.COLOR_BGR2GRAY)
edge = cv2.Canny(gray, 70, 160)
坑点 9:Canny 建议前置高斯模糊,否则会检测大量噪点伪边缘
错误写法:
python
# 跳过降噪直接 Canny,噪点会被当成边缘
edge = cv2.Canny(gray, 70, 160)
正确写法:
python
# 先高斯模糊抑制噪点,再 Canny
blur = cv2.GaussianBlur(gray, (3, 3), 0)
edge = cv2.Canny(blur, 70, 160)
4. 通用细节类
坑点 10:图片路径记得加 r 原始字符串,防止转义字符导致读取返回 None
错误写法:
python
# 反斜杠被当成转义字符,路径错误
img = cv2.imread('D:\PYTHON\your name.jpg')
正确写法:
python
# 加 r 原始字符串,路径原样解析
img = cv2.imread(r'D:\PYTHON\your name.jpg')
十一、本章知识小结
承接上一章滤波:本章的边缘检测与阈值分割,必须建立在良好降噪预处理之上。噪声不去除,后续边缘、二值结果全部不可用------噪点会被当成伪边缘,二值图也会出现大量黑白斑点。所以工程上始终遵循「先降噪、再提取」的顺序,这也是上一章滤波知识在本章反复被调用的原因。
- Sobel、Scharr 属于一阶微分算子,分开获取水平、垂直方向边缘再合并;
- Scharr 对微弱纹理更灵敏;
- Laplacian 二阶微分算子,同时捕获两个方向轮廓,可用于锐化,但对噪声敏感;
- Canny 是成熟完整边缘检测方案,双阈值筛选,输出干净细致物体轮廓,项目优先选用;
- 全局阈值threshold整张图片使用固定分割值,适合光照均匀图像;
- 自适应阈值adaptiveThreshold局部区域动态算阈值,解决光照不均匀图像二值化难题;
- 工程通用流程:原图→滤波降噪→灰度转换→边缘检测 / 阈值分割。
下一章开始我们进入形态学操作,学习腐蚀、膨胀、开运算、闭运算,用来清理二值图里面的噪点小毛刺,为后续轮廓查找做准备。