免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Canny边缘检测、OTSU分割与Haar人脸检测:OpenCV经典算法详解

Canny边缘检测、OTSU分割与Haar人脸检测:OpenCV经典算法详解 简介一套数字图像处理大作业资料围绕图像分割、人脸检测与边缘检测三大经典任务展开面向计算机视觉入门者及需要完成课程设计的学生。内容完整覆盖Canny边缘检测的高斯滤波、梯度计算、非极大值抑制与双阈值流程OTSU自适应二值化的类间方差原理以及基于OpenCV Haar级联分类器的人脸检测方法可直接参考Python实现并进行结果可视化。压缩包共11个文件以7个Python脚本为主包含图像预处理、数据集划分、分类训练等代码模块另有2个Markdown说明文档和1个环境工具包整体大小97.64MB结构便于按步骤运行与二次修改。已有8347人学习浏览这份资料适合希望系统动手实践图像处理算法、提升OpenCV编程能力的读者。1. 作业里的三件套边缘检测、图像分割和人脸检测为什么仍然要手写现在语义分割和通用检测已经被深度学习刷榜UNet跑医学影像、RetinaFace跑人脸看起来经典算法没什么存在感。但这套 DIP-Project-master 大作业偏偏是反着来的用Canny做边缘检测用OTSU做图像分割用OpenCV的Haar级联做人脸检测。反直觉的地方在于这三者加起来不需要GPU不需要训练数据却能在一张图里完成「哪里是边界、哪里是前景、哪里有脸」三类任务并且每个环节都能被解释和手动调参。对做工程的人来说这类项目是练习图像预处理和调参手感的好素材对要交大作业的同学来说理解这三个算法能直接把preprocess.py之后的每个调用讲清楚。2. Canny边缘检测Prewitt与Sobel选型、梯度计算和双阈值调试2.1 为什么选Canny而不是Prewitt或Sobel边缘检测的核心问题是图像里灰度突变的位置怎么被提取成一条细且连续的线。Prewitt边缘检测原理最简单用两个3x3方向模板对图像做卷积一个响应水平梯度、一个响应垂直梯度然后取模。但它没有平滑项单个噪声点就会在输出里产生一个孤立的高响应而且边缘是几像素宽的发散带。Sobel给中心像素加了更高权重相当于内置了一点平滑但它输出的仍然是梯度强度图真正的边缘线需要自己再找极大值。Canny算法是John Canny在1986年提出的多阶段流程先高斯滤波去噪再用Sobel计算梯度幅度和方向随后做非极大值抑制最后用双阈值连接边缘。它同时满足检测完整、定位准确、单像素响应三条设计准则。四个算子的差异整理如下算子计算方式抗噪性边缘连续性OpenCV调用Prewitt3x3方向差分弱粗多断点filter2D自定义Sobel加权差分平滑中等粗存在重边cv2.SobelLaplacian二阶微分弱对孤立点敏感cv2.LaplacianCanny高斯滤波梯度NMS双阈值强细且连续cv2.Canny在实际的大作业里Sobel通常只用来计算梯度图Prewitt则很少单独出现。Canny的定位精度和连续性都更好唯一的代价是要调两个阈值这也是最容易卡住的地方。2.2 用cv2.Canny跑通最小实现并理解每个参数下面这段代码可以直接放到项目的analysis目录下跑把图片路径换成本机的即可import cv2 import numpy as np img cv2.imread(sample.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 1.0) edges cv2.Canny( blurred, threshold150, threshold2150, apertureSize3, L2gradientFalse ) cv2.imwrite(canny_output.png, edges)灰度化的目的是去掉颜色对梯度计算的干扰后续所有处理都在单通道上进行。GaussianBlur用5x5窗口、sigma1.0的高斯核做平滑把传感器噪声压下去否则这些噪声会直接参与梯度计算并在NMS阶段被当作假边缘保留。cv2.Canny内部会先计算x和y方向的梯度然后合成幅度和方向。非极大值抑制把梯度方向上的非最大像素置零让边缘从「一条带」收成「一条线」。双阈值阶段的工作方式是高于threshold2的像素判定为强边缘低于threshold1的直接丢弃介于两者之间的弱边缘只有在与某个强边缘像素连通时才被保留。threshold1尽量取threshold2的一半或三分之一例如50与150、30与90比值过大时边缘会断成碎片过小时背景纹理全部连进来。参数含义常用范围threshold1低阈值决定弱边缘保留条件30~60threshold2高阈值决定强边缘起点80~200apertureSizeSobel卷积核大小必须为奇数3或5L2gradient梯度模长算法False为L1近似True为欧氏距离如果检测结果里全是短线先检查是否把threshold1和threshold2写反了如果边缘明显偏粗则说明没有先做高斯滤波或者滤波核开得太大。调参时优先固定threshold2再单独动threshold1找边缘连续和噪声之间的平衡。2.3 预处理顺序resize、均衡化后Canny参数才可复现项目里preprocess.py的作用不只是读图。数字图像处理大作业通常要求对不同尺寸的输入保持同一套参数所以第一条是resize到固定尺寸常见做法是512x512。然后是灰度化、直方图均衡化、高斯滤波最后才进入Canny。下面这段是这种顺序的典型写法def edge_pipeline(path): img cv2.imread(path) img cv2.resize(img, (512, 512)) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) blur cv2.GaussianBlur(gray, (5, 5), 0.8) return cv2.Canny(blur, 30, 90)说明两点。第一直方图均衡化把灰度分布拉开低对比度图片的边缘响应会明显增强但噪声也会同步放大所以高斯滤波必须放在它后面且sigma不要开太大。第二如果预处理里做了归一化图像变成了float型cv2.Canny会直接报错或者输出空图正确做法是先乘以255再转回np.uint8阈值仍然按255尺度设置。这也是preprocess.py里最常见的错误来源。提示OpenCV的Canny在传入非8位单通道图像时行为不一致。统一在预处理阶段转成uint8比在调用处补救更可靠。3. OTSU图像分割类间方差、threshold参数组合与光照不均的补救3.1 OTSU的数学直觉与适用边界OTSU是1979年由大津展之提出的全局自适应阈值算法数字图像处理课程里常把它放在「分割」章节讲。它的思路是在0到255的灰度范围内逐一遍历阈值t把像素分成背景类ω0和前景类ω1然后计算类间方差σ²_b(t) ω0(t) · ω1(t) · (μ0(t) − μ1(t))²当某个t让这个值最大就认为此时背景和前景的灰度差被拉得最开t就是最优阈值。这个遍历在OpenCV里是内置的代价很低所以整张图算一遍阈值几乎是实时的。OTSU适用的条件是直方图呈现双峰也就是目标区域和背景区域各自占据一个相对集中的灰度范围且两者面积不要悬殊太大。如果目标只占画面5%背景占95%类间方差会被大类的统计主导阈值会偏向背景一侧。另外噪声会改变直方图峰的位置所以上一章的高斯滤波同样适用于OTSU的前置处理。3.2 cv2.threshold的OTSU组合与边界情况OpenCV里OTSU不是单独的函数而是cv2.threshold的一个标志位。标准写法如下ret, otsu_mask cv2.threshold( blurred, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU ) print(OTSU自动求出的阈值 , ret)这里传入的thresh0会被忽略阈值由OTSU算法自己计算并放在返回值ret里。type参数用加号把两个flag组合起来THRESH_BINARY表示大于阈值的像素置为255THRESH_OTSU表示「先自动求阈值再按BINARY规则处理」。实际使用中几种组合的差别如下type写法效果典型场景THRESH_BINARY固定阈值二值化已知灰度范围THRESH_BINARY THRESH_OTSU自动阈值二值化双峰直方图THRESH_BINARY_INV THRESH_OTSU自动阈值且前景置黑白底黑字转黑底白字THRESH_TOZERO THRESH_OTSU保留前景原灰度背景置0生成前景mask继续处理容易踩的坑有两个。第一传入的图像必须是8位单通道彩色图直接传给threshold会报错。第二THRESH_OTSU不能与cv2.adaptiveThreshold一起用后者自己决定每个像素的邻域阈值两者的flag互斥混用时OTSU会被静默忽略输出可能是一张全黑图。调试时先打印ret看看自动阈值是否落在合理范围内例如一张暗调图像算出ret220基本说明光照有问题或者传入通道搞错了。3.3 光照不均时OTSU失效的两种补救路径OTSU是全局阈值光照从左到右衰减时同一目标的灰度在画面左侧可能是180、右侧只有90全局阈值只能切下一半。两种常见补救第一种是形态学顶帽变换去除不均匀背景再跑OTSU第二种是改用局部自适应阈值。代码如下# 方案A顶帽变换 OTSU kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) tophat cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, kernel) ret2, mask_a cv2.threshold(tophat, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 方案B局部自适应阈值 mask_b cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize35, C5 )顶帽变换的定义是原图减去开运算结果开运算是先腐蚀再膨胀能去掉小于kernel结构的亮细节剩下的就是变化缓慢的背景光照。原图减去背景后目标与背景的灰度差恢复均匀OTSU就可以继续工作。kernel结构元尺寸一般是目标宽度的1.5倍以上太小会把目标也当成背景过滤掉。adaptiveThreshold对每个像素用邻域内的高斯加权均值作为参考阈值再减去常数C。blockSize必须是奇数而且要大于目标尺寸否则目标内部会被判定成背景C越大判定为前景的门槛越高输出越细碎。两张mask都生成后可以和人工标注做个对比指标部分放到第5章。4. Haar人脸检测从级联XML到detectMultiScale各参数的调优顺序4.1 Haar级联的机制和选择理由OpenCV提供的人脸检测器来自Viola-Jones框架核心是Haar特征与级联结构。Haar特征的取值是白色矩形区域像素和减黑色矩形区域像素和这个值对「眼睛比脸颊暗」「鼻梁比两侧亮」这类亮度差异很敏感。为了快速计算任意尺度下的矩形和算法引入了积分图任意矩形区域求和只需查表四次这是它能在CPU上实时运行的基础。级联结构把几百个弱分类器按难度分层组装。前几层用很少的特征快速排除大部分非人脸窗口越往后分类器越复杂只有真正像脸的窗口能走到最后。haarcascade_frontalface_default.xml这个文件只有不到1MB却能在笔记本上做到实时的正脸检测这是大作业选用它而不是深度学习模型的原因不用训练、不用GPU、单帧几十毫秒。补充一点OpenCV里还有lbpcascade_frontalface.xmlLBP特征基于局部纹理检测速度更快但精度略低在低分辨率监控画面里LBP有时反而比Haar表现好因为它对灰度绝对值的敏感度更低。可以根据实际输入尝试切换。4.2 加载模型与detectMultiScale参数表加载模型推荐使用cv2.data.haarcascades它是OpenCV自带模型目录免去拼接绝对路径。标准检测流程如下face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray_eq cv2.equalizeHist(gray) faces face_cascade.detectMultiScale( gray_eq, scaleFactor1.1, minNeighbors5, minSize(30, 30), maxSize(300, 300) ) for (x, y, w, h) in faces: cv2.rectangle(img, (x, y), (x w, y h), (0, 255, 0), 2)detectMultiScale返回的是numpy数组每一行是x、y、w、h四个值。先做灰度化和直方图均衡化是为了让局部对比度更突出检测本身只接受单通道图像在彩色图上直接调用也是先内部转灰度但显式转换可以保证我们控制均衡化这一步。参数的影响按下表来排查参数作用机制调大调小scaleFactor每轮检测窗口缩放比例漏检小脸、速度快检测更全、速度明显变慢minNeighbors候选框最少通过的相邻框数误检减少、正脸也丢误检增多minSize最小检测窗口忽略小脸、排除背景纹理小尺寸窗口误检增多maxSize最大检测窗口大脸丢失限制只保留特写scaleFactor是最需要解释的参数。窗口每次放大1.1倍意味着总共约log(原图尺寸/目标尺寸)/log(1.1)个尺度层级1.05会接近翻倍耗时1.3虽然快但小人脸通常在两次缩放之间被跳过。minNeighbors则更像一个投票约束潜在人脸位置会产生一堆重叠矩形少于minNeighbors个重叠的一律丢弃。4.3 从误检到漏检的调参顺序实际大作业里Haar最常被吐槽的就是框错。我的排查顺序是固定的先看是不是把minNeighbors设成了0或1这是初学者最常见的误检来源改成5左右误检会大幅下降。接着处理小脸漏检把scaleFactor从1.1降到1.05minSize从(30,30)放宽到(20,20)代价是耗时上升但对1080P以下的图仍可接受。如果整批图都有大量误检且调整minNeighbors无效优先怀疑输入分辨率过高。1920宽以上的图像里背景纹理也被放大到接近人脸尺寸误检区域往往集中在墙面纹路、树叶这类高频纹理上。处理方法是在检测前先对图像做一次高斯金字塔下采样或者把maxSize限制在人脸可能的最大尺寸。侧脸和低头场景属于模型能力边界此时换用haarcascade_frontalface_alt2.xml并用minNeighbors6通常比继续调默认模型更有效。5. 组合流水线三模块联动、ResNet扩展与IoU量化验证5.1 一个入口串联三件套单独跑两个算法看不出问题把三者串起来才能体现前置处理一致性的价值。下面的函数把一张输入图同时喂给Canny、OTSU和Haarimport cv2 import numpy as np def run_pipeline(path): img cv2.imread(path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blur cv2.GaussianBlur(gray, (5, 5), 1.0) edges cv2.Canny(blur, 50, 150) _, otsu cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml) faces cascade.detectMultiScale(blur, 1.1, 5, minSize(30, 30)) canvas cv2.cvtColor(np.hstack([edges, otsu]), cv2.COLOR_GRAY2BGR) for (x, y, w, h) in faces: cv2.rectangle(canvas, (x, y), (x w, y h), (0, 0, 255), 2) cv2.imwrite(pipeline_out.png, canvas)注意np.hstack要求两图高度一致这里edges和otsu都来自同一个blur尺寸天然相等。叠加人脸框前必须把拼接结果从灰度转BGR否则框的颜色参数会按单通道解释导致异常。这个流水线的价值在于分割出的前景mask可以作为Canny边缘的注意力范围反过来边缘密度也可以用来验证人脸框是否落在真正的轮廓密集区。5.2 结合项目脚本验证分割质量项目里metric.py的存在说明大作业要求量化评估而不只是贴图。常见做法是准备少量人工标注的mask和OTSU结果计算IoUimport numpy as np def iou(pred, gt): pred pred 0 gt gt 0 inter np.logical_and(pred, gt).sum() union np.logical_or(pred, gt).sum() return inter / (union 1e-6)IoU超过0.7通常认为分割可用低于0.4说明预处理或阈值选择有系统性问题。人脸检测没有标注时建议把检测框和Canny边缘叠在同一张图里存盘逐个框查看边缘密度人脸区域边缘密度明显高于纯色背景若某框内边缘占比低于全图平均的三成基本可以判定是误检。如果作业还要求跑classification.py和resnet.py做深度学习方法对比OTSU生成的mask可以直接作为ResNet输入的辅助通道train_3d.py对应的三维训练常见形式是类似3D UNet的滑窗方式和OTSU这种二维全局阈值属于两条路线前者要标注数据后者零训练。这两个验证技巧配合管线输出图比肉眼扫图更快暴露参数问题。本文还有配套的精品资源点击获取
返回列表