最近在入门计算机视觉,从 OpenCV 基础操作开始学起,今天把图像运算、边界填充、阈值处理、图像滤波这几块核心内容都跑了一遍,每个知识点都写了可直接运行的小 demo,最后还完成了两个综合小作业。整理成博客一方面自己复盘用,另一方面也给同样入门的朋友做参考,代码复制就能跑,记得把图片、视频路径换成自己本地的就行。
一、图像基础运算:直接相加与加权融合
图像运算本质就是对每个像素点的数值做计算,这里有个很容易踩坑的点:numpy 数组直接相加和OpenCV 自带的 add 函数,运算规则完全不一样。
- 直接用
+号:numpy 是取模运算,像素值超过 255 就对 256 取余,比如 250+20=270,最终结果是 14,画面反而会发黑。 - 用
cv2.add():饱和运算,超过 255 的数值直接截断为 255,符合我们 “加亮度、画面变亮” 的直观预期。
除此之外cv2.addWeighted()可以实现两张图的加权融合,也就是带透明度的叠加效果,日常用得非常多。
import cv2 # 读取两张测试图片 a = cv2.imread('image1.jpg') b = cv2.imread('image2.jpg') # 1. 单张图像整体加亮度(numpy直接相加,会取模) c = a + 10 cv2.imshow('原图a', a) cv2.imshow('a+10(numpy加法)', c) cv2.waitKey(0) # 2. 两张图局部区域直接相加(必须保证尺寸一致) # 先截取相同大小的区域再做加法 c = a[100:522, 150:700] + b[100:522, 150:700] cv2.imshow('a+b(numpy直接相加)', c) cv2.waitKey(0) # 统一两张图的尺寸,方便后续运算 a = cv2.resize(a, (1239, 622)) b = cv2.resize(b, (1239, 622)) # 3. OpenCV的add函数(饱和运算,超过255截断为255) c = cv2.add(a, b) cv2.imshow('a+b(cv2.add饱和运算)', c) cv2.waitKey(0) # 4. 加权融合:a占70%权重,b占30%权重,最后整体偏移10个亮度值 c = cv2.addWeighted(a, 0.7, b, 0.3, 10) cv2.imshow('加权融合addWeighted', c) cv2.waitKey(0) cv2.destroyAllWindows()跑的时候对比会很明显:直接相加的画面会出现很多异常的黑斑,就是像素溢出取模导致的;cv2.add 整体会更亮;加权融合则可以做出自然的半透明叠加效果。
二、图像边界填充的五种方式
做卷积、滤波操作的时候,经常需要给图像边缘填充像素,保证输出尺寸和原图一致。OpenCV 提供了 5 种不同的填充策略,用cv2.copyMakeBorder()实现,效果差别挺大的。
五种填充类型说明:
- BORDER_CONSTANT:用固定纯色填充边界,需要自己指定颜色
- BORDER_REFLECT:镜像反射填充,以边缘像素为轴向外镜像
- BORDER_REFLECT101:对称反射,不包含边缘像素本身,过渡更自然
- BORDER_REPLICATE:复制最边缘的像素,一直向外延伸
- BORDER_WRAP:平铺环绕,相当于把图像重复平铺,取外围部分
import cv2 # 读取测试图片 img = cv2.imread('test.jpg') # 定义上下左右四个方向的填充像素宽度 top, bottom, left, right = 50, 50, 50, 50 # 1. 纯色填充,value指定BGR颜色值 constant = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(229, 25, 80)) # 2. 镜像反射 reflect = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REFLECT) # 3. 对称反射(不含边缘像素) reflect101 = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REFLECT101) # 4. 复制边缘像素 replicate = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REPLICATE) # 5. 平铺环绕 wrap = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_WRAP) # 依次显示对比效果 cv2.imshow('原图', img) cv2.waitKey(0) cv2.imshow('CONSTANT纯色填充', constant) cv2.waitKey(0) cv2.imshow('REFLECT镜像反射', reflect) cv2.waitKey(0) cv2.imshow('REFLECT101对称反射', reflect101) cv2.waitKey(0) cv2.imshow('REPLICATE复制边缘', replicate) cv2.waitKey(0) cv2.imshow('WRAP平铺环绕', wrap) cv2.waitKey(0) cv2.destroyAllWindows()日常做滤波处理时,默认用得最多的是BORDER_REFLECT101,边界过渡最自然,不会出现明显的断层感。
三、五种阈值处理方式
阈值处理是图像二值化的基础,核心就是设定一个阈值,把像素分成两类,必须基于灰度图进行。OpenCV 的cv2.threshold()提供了 5 种常用模式。
以阈值 175、最大值 255 为例,五种模式的效果:
- THRESH_BINARY二值化:大于阈值变 255,小于阈值变 0
- THRESH_BINARY_INV反二值化:和上面相反,大于阈值变 0,小于变 255
- THRESH_TRUNC截断:大于阈值的都等于阈值,小于的保持原值
- THRESH_TOZERO置零:大于阈值的保持不变,小于的变成 0
- THRESH_TOZERO_INV反置零:大于阈值的变成 0,小于的保持不变
import cv2 # 以灰度模式读取图片(参数0表示灰度读取) image = cv2.imread('test.jpg', 0) # 五种阈值处理,阈值设为175,像素最大值255 ret1, binary = cv2.threshold(image, 175, 255, cv2.THRESH_BINARY) # 二值化 ret2, binaryinv = cv2.threshold(image, 175, 255, cv2.THRESH_BINARY_INV) # 反二值化 ret3, trunc = cv2.threshold(image, 175, 255, cv2.THRESH_TRUNC) # 截断 ret4, tozero = cv2.threshold(image, 175, 255, cv2.THRESH_TOZERO) # 置零 ret5, tozeroinv = cv2.threshold(image, 175, 255, cv2.THRESH_TOZERO_INV) # 反置零 # 依次查看每种效果 cv2.imshow('灰度原图', image) cv2.waitKey(0) cv2.imshow('二值化BINARY', binary) cv2.waitKey(0) cv2.imshow('反二值化BINARY_INV', binaryinv) cv2.waitKey(0) cv2.imshow('截断TRUNC', trunc) cv2.waitKey(0) cv2.imshow('置零TOZERO', tozero) cv2.waitKey(0) cv2.imshow('反置零TOZERO_INV', tozeroinv) cv2.waitKey(0) cv2.destroyAllWindows()固定阈值的缺点是对光照变化很敏感,后面学到自适应阈值会好用很多。阈值数值需要根据自己的图片亮度、对比度灵活调整。
四、图像降噪:四种滤波方法对比
实拍的图像经常会有噪声,最常见的就是椒盐噪声(随机分布的黑白点)。滤波就是用来消除噪声、平滑画面的,不同算法的适用场景和效果差异很大。
我们先手动给图片加上椒盐噪声,再分别用四种滤波方法处理,直观对比效果。
四种常用滤波:
- 均值滤波:取邻域像素平均值,实现最简单,但会把边缘也一起模糊
- 方框滤波:和均值原理类似,可选是否归一化;不归一化就是直接求和,容易过曝
- 高斯滤波:按高斯分布加权,离中心越近权重越高,保边效果比均值滤波好
- 中值滤波:取邻域像素的中位数,对椒盐噪声效果极佳,能较好保留边缘
import cv2 import numpy as np # 自定义函数:给图片添加椒盐噪声 # 参数n为噪声点的数量 def add_peppersalt_noise(img, n=1000): result = img.copy() h, w = img.shape[:2] for i in range(n): # 随机生成像素坐标 x = np.random.randint(h) y = np.random.randint(w) # 随机设置为黑点(0)或白点(255) if np.random.randint(2) == 0: result[x, y] = 0 else: result[x, y] = 255 return result # 读取原图 image = cv2.imread('test.png') cv2.imshow('原图', image) cv2.waitKey(0) # 添加椒盐噪声 noise_img = add_peppersalt_noise(image) cv2.imshow('加椒盐噪声后', noise_img) cv2.waitKey(0) # 1. 均值滤波 3x3卷积核 blur_1 = cv2.blur(noise_img, (3, 3)) cv2.imshow('均值滤波3x3', blur_1) cv2.waitKey(0) # 卷积核越大,模糊效果越明显 blur_2 = cv2.blur(noise_img, (25, 25)) cv2.imshow('均值滤波25x25', blur_2) cv2.waitKey(0) # 2. 方框滤波 # normalize=True 归一化,效果和均值滤波完全一致 boxFilter_1 = cv2.boxFilter(noise_img, -1, (3, 3), normalize=True) cv2.imshow('方框滤波(归一化)', boxFilter_1) cv2.waitKey(0) # normalize=False 不归一化,像素直接求和,很容易饱和到255 boxFilter_2 = cv2.boxFilter(noise_img, -1, (3, 3), normalize=False) cv2.imshow('方框滤波(不归一化)', boxFilter_2) cv2.waitKey(0) # 3. 高斯滤波,标准差设为1 GaussianB = cv2.GaussianBlur(noise_img, (3, 3), 1) cv2.imshow('高斯滤波', GaussianB) cv2.waitKey(0) # 4. 中值滤波 3x3 medianB = cv2.medianBlur(noise_img, 3) cv2.imshow('中值滤波', medianB) cv2.waitKey(0) cv2.destroyAllWindows()亲测对于椒盐噪声,中值滤波的效果是碾压级的,黑白点基本都能消掉,画面还不会特别糊。均值和高斯滤波会把整个画面都磨平,细节损失比较大。
五、实战作业 1:摄像头实时画面与背景图片融合
第一个综合练习,实现摄像头实时画面和背景图的加权融合,同时输出彩色和灰度两个版本的画面。
实现要求:
- 背景图缩放为 640x480 尺寸
- 分别生成彩色融合、灰度融合两路画面
- 两个窗口同时实时显示
import cv2 # 读取背景图并缩放到640x480,和摄像头默认尺寸匹配 bg = cv2.imread('background.jpg') bg = cv2.resize(bg, (640, 480)) # 生成灰度版背景图 bg_gray = cv2.cvtColor(bg, cv2.COLOR_BGR2GRAY) # 打开默认摄像头,设备号一般为0 cap = cv2.VideoCapture(0) while True: # 读取一帧画面 ret, frame = cap.read() if not ret: print("摄像头读取失败") break # 将摄像头当前帧转为灰度图 frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 彩色画面加权融合:背景和摄像头各占50%,亮度偏移10 result_color = cv2.addWeighted(bg, 0.5, frame, 0.5, 10) # 灰度画面加权融合 result_gray = cv2.addWeighted(bg_gray, 0.5, frame_gray, 0.5, 10) # 同时弹出两个窗口 cv2.imshow('彩色融合画面', result_color) cv2.imshow('灰度融合画面', result_gray) # 按ESC键退出循环(ESC的ASCII码为27) if cv2.waitKey(20) == 27: break # 释放摄像头资源,关闭所有窗口 cap.release() cv2.destroyAllWindows()这里要注意,如果你的摄像头默认分辨率不是 640x480,需要把 frame 也 resize 成和背景图一样的尺寸,不然 addWeighted 会报错。
六、实战作业 2:视频逐帧加噪声 + 实时降噪
第二个综合练习,处理本地视频文件,给每一帧添加椒盐噪声,再用滤波降噪,同时显示原视频、噪声视频、降噪后视频三个窗口。
import cv2 import numpy as np # 给单帧画面添加椒盐噪声,默认添加8000个噪声点 def add_salt_pepper_noise(frame, n=8000): result = frame.copy() h, w = frame.shape[:2] for i in range(n): x = np.random.randint(h) y = np.random.randint(w) # 随机设置为白点或黑点 if np.random.randint(2) == 1: result[x, y] = 255 else: result[x, y] = 0 return result # 读取本地视频文件 cap = cv2.VideoCapture('test.avi') while True: # 读取一帧画面 ret, frame = cap.read() if not ret: print("视频播放结束或读取失败") break # 1. 给当前帧添加椒盐噪声 noise_frame = add_salt_pepper_noise(frame) # 2. 中值滤波降噪(针对椒盐噪声效果最优) denoise_frame = cv2.medianBlur(noise_frame, 3) # 三个窗口同步播放 cv2.imshow('原视频', frame) cv2.imshow('加噪声后', noise_frame) cv2.imshow('中值滤波降噪后', denoise_frame) # 按ESC键退出 if cv2.waitKey(20) == 27: break # 释放视频资源 cap.release() cv2.destroyAllWindows()这里选择中值滤波就是因为它对椒盐噪声效果最好,3 的核大小既能消除大部分噪声,又不会让画面过度模糊。如果噪声特别密集,可以把核大小调到 5,但画面清晰度会有所下降。