4. OpenCV基础(三):图像形态学操作、轮廓检测与直方图

各位好,欢迎来到第四章。这一章的内容,我个人觉得是OpenCV里最“出活”的部分。你想想看,前面我们学了怎么读图、调颜色、做滤波,但真正要把图像里的“东西”提取出来,靠的就是形态学和轮廓检测。说白了,就是让计算机学会“看”形状。

我记得刚入行那会儿,做一个零件检测的项目。图像里全是噪点,阈值分割完还是一团糟。后来用了开运算和闭运算,效果立竿见影。嗯,今天我们就来把这些“手术刀”一把一把讲清楚。

4.1 图像形态学操作:腐蚀与膨胀

形态学操作,核心就两个动作:腐蚀和膨胀。你可以把它们想象成“侵蚀”和“扩张”。

  • 腐蚀(Erosion):让白色区域“瘦”一圈。适合去掉细小的白色噪点。
  • 膨胀(Dilation):让白色区域“胖”一圈。适合填补物体内部的小空洞。

它们俩通常配合一个“核”(Kernel)来工作。核就是一个小的结构元素,比如3x3的方形或圆形。

import cv2
import numpy as np

# 读取二值图像(或先做阈值处理)
img = cv2.imread('shape.png', 0)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

# 定义核
kernel = np.ones((5,5), np.uint8)

# 腐蚀
erosion = cv2.erode(binary, kernel, iterations=1)

# 膨胀
dilation = cv2.dilate(binary, kernel, iterations=1)

cv2.imshow('Erosion', erosion)
cv2.imshow('Dilation', dilation)
我的习惯:核的大小不要一上来就用大的。从3x3开始试,iterations参数可以叠加。我曾经因为核设太大,直接把目标物体给“吃”没了,调试了半天才发现。

4.2 开运算与闭运算

腐蚀和膨胀单独用,效果有限。真正好用的是它们的组合:

  • 开运算(Opening):先腐蚀,再膨胀。用来去除小白点噪点,同时保持主体大小不变。
  • 闭运算(Closing):先膨胀,再腐蚀。用来填充小黑洞,同时保持主体大小不变。

你想想看,如果直接腐蚀,物体变小了;直接膨胀,物体变大了。但开闭运算做完,大小基本不变,只改变细节。

# 开运算
opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)

# 闭运算
closing = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
实战场景:做文档OCR预处理时,文字笔画里经常有断点。用闭运算一填,笔画就连起来了。反之,扫描件上的墨点,用开运算一吹就没了。

4.3 轮廓检测与绘制

形态学做完,图像干净了。下一步就是找轮廓。OpenCV里最常用的就是findContours函数。这个函数会返回图像中所有连通域的边界点集。

这里有个坑,我必须要说:输入图像必须是二值图,且背景是黑色,物体是白色。否则结果会完全反掉。

# 找轮廓
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 在原图上绘制轮廓
img_color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
cv2.drawContours(img_color, contours, -1, (0, 255, 0), 2)

cv2.imshow('Contours', img_color)

参数说明:

参数 含义 常用值
mode 轮廓检索模式 RETR_EXTERNAL(只找最外层)、RETR_TREE(全层级)
method 轮廓近似方法 CHAIN_APPROX_SIMPLE(压缩水平/垂直/对角段)
我曾经踩过的坑:用RETR_TREE模式时,hierarchy层级关系没处理好,结果把内轮廓和外轮廓都画出来了,导致一个物体被框了两次。如果你只需要物体个数,用RETR_EXTERNAL最稳。

4.4 直方图均衡化

直方图均衡化,说白了就是“拉对比度”。当一张图太暗或太亮,细节都挤在一起时,均衡化可以把灰度值均匀摊开,让细节显现出来。

OpenCV里直接调用equalizeHist就行。注意:它只接受单通道灰度图。

img = cv2.imread('dark.jpg', 0)
equ = cv2.equalizeHist(img)

# 并排显示
result = np.hstack((img, equ))
cv2.imshow('Equalization', result)

效果非常直观。左边灰蒙蒙,右边层次分明。但有个问题:它会放大噪声。如果原图噪点多,均衡化后噪点会更明显。

我的建议:先做高斯模糊降噪,再做均衡化。顺序别搞反。我在做夜间车牌识别时,这个组合拳救了很多次场。

4.5 模板匹配

模板匹配,就是拿一个小图(模板)在大图里滑动,找最像的位置。OpenCV提供了matchTemplate函数,返回一个匹配结果图,然后用minMaxLoc找到最佳匹配点。

img = cv2.imread('big.jpg', 0)
template = cv2.imread('small.jpg', 0)
w, h = template.shape[::-1]

# 匹配
res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)

# 画矩形
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
cv2.rectangle(img, top_left, bottom_right, 255, 2)

匹配方法有几种:

方法 特点
TM_CCOEFF_NORMED 归一化相关系数,对光照变化不敏感,推荐
TM_CCORR_NORMED 归一化相关,速度快但精度一般
TM_SQDIFF_NORMED 归一化平方差,值越小越匹配
注意:模板匹配不处理旋转和缩放。如果目标在图像里旋转了30度,匹配就会失败。这时候需要用到特征匹配(比如SIFT、ORB),那是后面章节的内容。

4.6 本章知识体系图

下面这张图,帮你把本章的核心逻辑串起来:

第4章:图像形态学与轮廓检测知识体系 输入图像(灰度/二值) 形态学操作 直方图均衡化 模板匹配 腐蚀 / 膨胀 开运算 / 闭运算 轮廓检测 findContours 输出:轮廓 / 匹配位置 避坑提醒 输入必须是二值图 背景黑,物体白

4.7 小结与避坑清单

这一章的内容,在实际项目中几乎天天用。我把几个容易出问题的地方列出来,你记一下:

  • 形态学核大小:从3x3开始试,别贪大。iterations可以叠加,效果更可控。
  • 轮廓检测输入:一定是二值图,且背景为黑。如果结果反了,用cv2.bitwise_not取反。
  • 直方图均衡化:先降噪再做,否则噪声会被放大。
  • 模板匹配:不抗旋转和缩放。如果目标有角度变化,换特征匹配。
一句话总结:形态学负责“清洁”图像,轮廓检测负责“提取”形状,直方图负责“增强”细节,模板匹配负责“定位”目标。四者配合,能解决大部分图像预处理问题。