先跑起来:用 Lena 串起 OpenCV 接口
这篇教程与《图像处理基础》使用同一张 Lena:前者帮助理解原理,这里重点练习 调用哪个接口、输入什么数组、参数怎样影响输出。配图均为代码实际运行结果。

下载 Lena.jpg · 下载本篇完整实验脚本 · 下载滤波、颜色与边缘效果脚本
图片来自 OpenCV 官方样例库,照片权利归原权利人;下载来源不等于公有领域。也可以改用自己拍摄的图像。
把原图和脚本放在同一目录:
python -m pip install numpy opencv-python matplotlib python lena_opencv.py --input Lena.jpg --output results
|
脚本不依赖 imshow 窗口,运行后直接保存 4 组对照图、annotated.png 和 mask.png,适用于 Notebook、服务器和桌面环境。为使坐标可复现,脚本先将输入统一为 512×512;对任意照片直接缩放可能改变宽高比,真实项目应按任务选择等比缩放或填充。
下面新增代码块默认已执行:
import cv2 import numpy as np img = cv2.imread("Lena.jpg") if img is None: raise FileNotFoundError("无法读取 Lena.jpg,请检查工作目录和大小写") img = cv2.resize(img, (512, 512), interpolation=cv2.INTER_AREA) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
一、什么是 OpenCV?
OpenCV 是开源计算机视觉库,提供图像处理、视频读写、特征分析和目标检测等功能。Python 通过 cv2 模块调用这些接口,图像通常以 NumPy 数组表示。
OpenCV 有什么用?
- 读取、保存、裁剪和缩放图片。
- 转换颜色空间,进行滤波、分割和边缘检测。
- 在图像中绘制标记,测量目标轮廓和位置。
- 读取摄像头与视频,逐帧处理并保存结果。
- 构建实验图像分析和简单视觉检测工具。
例如,读取图片并转换为灰度图:
import cv2 as cv
image = cv.imread("input.png") if image is None: raise FileNotFoundError("无法读取 input.png")
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY) if not cv.imwrite("gray.png", gray): raise OSError("灰度图保存失败")
|
说明: 本教程使用 Python 与 OpenCV 4 的常见接口,需要基本 Python 和 NumPy 数组知识。图像处理原理可结合《图像处理基础学习教程》阅读;这里重点介绍函数、参数、返回值和操作流程。
二、安装与检查环境
2.1 创建虚拟环境
在新的练习目录中执行:
Linux / macOS 的 Bash 或 Zsh:
source .venv/bin/activate
|
Windows PowerShell:
.venv\Scripts\Activate.ps1
|
若系统使用 python3 或 py,按实际解释器替换。编辑器也要选择同一虚拟环境。
2.2 选择安装包
| 安装包 |
常见用途 |
opencv-python |
常规桌面环境使用 |
opencv-contrib-python |
常规模块加额外 contrib 模块 |
opencv-python-headless |
不使用 OpenCV GUI 的服务器环境 |
opencv-contrib-python-headless |
无 GUI,且需要额外模块 |
同一环境只选择其中一个。 它们都提供 cv2,不应为增加功能而混装。
普通桌面练习可以安装:
python -m pip install numpy opencv-python
|
如果只保存图像、不打开窗口,可以在新环境选择 headless 版本。安装包名与导入名不同,不要使用 pip install cv2。
2.3 检查版本
import sys import cv2 as cv import numpy as np
print(sys.executable) print(cv.__version__) print(np.__version__)
|
预编译包的功能取决于构建配置,安装成功不等于已启用 CUDA、所有视频编码器或所有 GUI 后端。需要检查时可以查看 cv.getBuildInformation()。
三、图像数组与代码约定
3.1 shape、dtype 和坐标
| 内容 |
常见约定 |
| 彩色图数组 |
(高, 宽, 通道数) |
| 灰度图数组 |
(高, 宽) |
| 默认彩色通道 |
B、G、R |
| 像素访问 |
image[y, x] |
| 绘图坐标 |
(x, y) |
| 缩放目标尺寸 |
(宽, 高) |
| 常见图像类型 |
uint8,每通道 0~255 |
例如,image.shape == (480, 640, 3) 表示高 480、宽 640,而不是宽 480。
3.2 通用读取代码
第四至十三节等图像片段通常假设已经执行以下代码,得到 image 和 gray:
import cv2 as cv import numpy as np
image = cv.imread("input.png", cv.IMREAD_COLOR) if image is None: raise FileNotFoundError("无法读取 input.png,请检查路径和文件内容")
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
|
每个小节独立运行时,先加入导入和读取部分。可以使用自己的图片,也可以先运行第二十一节生成演示素材。
相对路径以当前工作目录为起点,不一定以 .py 文件位置为起点。保存前确认输出路径,避免覆盖原图。
四、读取、保存与显示图像
4.1 imread:读取图像
color = cv.imread("input.png", cv.IMREAD_COLOR) gray_image = cv.imread("input.png", cv.IMREAD_GRAYSCALE) unchanged = cv.imread("input.png", cv.IMREAD_UNCHANGED)
|
| 选项 |
用途 |
IMREAD_COLOR |
常规三通道 BGR 读取 |
IMREAD_GRAYSCALE |
单通道灰度读取 |
IMREAD_UNCHANGED |
尽可能保留原通道和位深,如 Alpha |
读取失败通常返回 None,先检查再调用处理函数。保留原始读取不等于返回图像一定为四通道或一定为 uint8。
4.2 imwrite:保存图像
from pathlib import Path
output = Path("output") output.mkdir(parents=True, exist_ok=True)
success = cv.imwrite(str(output / "result.png"), image) if not success: raise OSError("图像保存失败")
|
编码器通常根据文件扩展名选择。JPEG 质量参数示例:
success = cv.imwrite("preview.jpg", image, [cv.IMWRITE_JPEG_QUALITY, 90]) if not success: raise OSError("JPEG 保存失败")
|
质量数值不直接等于百分比相似度,JPEG 为有损压缩。二值掩码和类别标签优先使用无损格式。
4.3 imshow、waitKey 与窗口释放
以下需要桌面环境和可用 GUI 后端:
cv.namedWindow("Preview", cv.WINDOW_NORMAL) try: cv.imshow("Preview", image) cv.waitKey(0) finally: cv.destroyAllWindows()
|
waitKey(0) 等待按键,并让窗口处理事件。正数延时以毫秒为单位,但不是精准计时器。没有 GUI 时使用 imwrite() 保存结果,不直接调用窗口函数。
五、创建图像、像素访问与 ROI
5.1 创建画布
canvas = np.zeros((240, 320, 3), dtype=np.uint8) white = np.full((240, 320, 3), 255, dtype=np.uint8)
|
分别创建黑色与白色三通道图像。
5.2 读取和修改像素
canvas[20, 30] = (0, 0, 255) print(canvas[20, 30])
|
访问前确认索引没有超出尺寸。对大量像素操作时,优先使用数组切片和 OpenCV 函数,避免逐像素 Python 循环。
5.3 裁剪并复制
canvas[50:100, 60:140] = (0, 255, 0) roi = canvas[50:100, 60:140].copy() print(roi.shape)
|
结束索引不包含在内。.copy() 创建独立图像,避免后续改动影响原画布。
Lena 实验:把坐标画出来,再裁剪

roi = img[180:380, 200:400].copy() canvas = img.copy() cv2.rectangle(canvas, (200, 180), (399, 379), (0, 255, 255), 2) cv2.circle(canvas, (300, 280), 6, (0, 0, 255), -1) cv2.putText(canvas, "ROI: x=200 y=180", (18, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2, cv2.LINE_AA) assert roi.shape == (200, 200, 3)
|
黄色矩形的右下角用 (399,379),对应切片中最后包含的像素。红色点的 BGR 值是 (0,0,255)。右图虽然显示得较大,实际只有 200×200 像素;标准 putText 字体不直接支持中文。
练习:把 ROI 向左移 30 像素,同时修改切片和矩形,让两者继续对齐。
六、颜色转换与通道操作
6.1 cvtColor
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY) rgb = cv.cvtColor(image, cv.COLOR_BGR2RGB) hsv = cv.cvtColor(image, cv.COLOR_BGR2HSV)
|
转换标志要对应输入格式,不应把 BGR 图像当成 RGB 转换。灰度图没有三个颜色通道,不能直接使用 BGR 输入标志。
6.2 split 与 merge
blue, green, red = cv.split(image) restored = cv.merge([blue, green, red])
|
只需访问一个通道时可以用 image[:, :, 0],不必拆出所有通道。
6.3 HSV 范围
OpenCV 常规 8 位 COLOR_BGR2HSV 中,H 为 0~179,S、V 为 0~255。其他软件、浮点格式或 HSV_FULL 的数值约定可能不同。
Lena 实验:检查通道顺序

rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) b, g, r = cv2.split(img) recovered = cv2.merge([b, g, r]) assert np.array_equal(recovered, img)
|
上排右图是故意将 img 直接传入 Matplotlib 的错误效果。下排三个通道以灰度显示,亮处表示该通道值大。转成 RGB 是为了适配显示接口,不代表 OpenCV 所有函数都应接收 RGB。
七、绘制直线、矩形、圆和文字
绘图函数通常直接修改传入图像。需要保留原图时先复制。
marked = image.copy()
cv.line(marked, (10, 10), (100, 10), (255, 0, 0), 2) cv.rectangle(marked, (20, 30), (120, 100), (0, 255, 0), 2) cv.circle(marked, (160, 80), 25, (0, 0, 255), 2) cv.putText( marked, "Object 1", (20, 25), cv.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 1, cv.LINE_AA )
|
| 参数 |
含义 |
| 点坐标 |
通常为整数 (x, y) |
| 颜色 |
三通道图像通常用 BGR 元组 |
| thickness |
线宽;矩形、圆等闭合形状可用 -1 填充 |
| LINE_AA |
抗锯齿线型,适合展示标注 |
| putText 的位置 |
通常是文字基线左端点,不是文本框左上角 |
putText() 的内置 Hershey 字体不直接提供完整中文支持。先用英文数字标记;需要中文可后续结合 Pillow 或其他文字渲染方案。
八、缩放、翻转与几何变换
8.1 resize:缩放
height, width = image.shape[:2] small = cv.resize( image, (max(1, width // 2), max(1, height // 2)), interpolation=cv.INTER_AREA )
|
| 插值 |
常见使用场景 |
INTER_NEAREST |
二值掩码、类别标签 |
INTER_LINEAR |
普通图像常用缩放 |
INTER_AREA |
普通图像缩小 |
INTER_CUBIC |
放大时可尝试 |
目标尺寸为 (宽, 高),不保持原宽高比会造成形变。
8.2 flip 与 rotate
horizontal = cv.flip(image, 1) vertical = cv.flip(image, 0) rotated = cv.rotate(image, cv.ROTATE_90_CLOCKWISE)
|
flipCode=1 为水平翻转,0 为垂直翻转,负数为同时翻转。
8.3 仿射变换
height, width = image.shape[:2] matrix = cv.getRotationMatrix2D((width / 2, height / 2), 15, 1.0) rotated = cv.warpAffine(image, matrix, (width, height))
|
示例绕中心旋转 15 度,缩放系数为 1。固定画布可能裁掉部分内容,需要时调整变换和平移后的输出范围。
九、图像加权与数值运算
9.1 add 与 addWeighted
offset = np.full_like(image, 20) brighter = cv.add(image, offset)
overlay = image.copy() height, width = image.shape[:2] cv.rectangle(overlay, (0, 0), (width // 2, height // 2), (0, 255, 0), -1) blended = cv.addWeighted(image, 0.7, overlay, 0.3, 0)
|
addWeighted 按 α×图像1 + β×图像2 + γ 计算;输入尺寸、通道和类型需要兼容。8 位 cv.add() 使用饱和处理,而 NumPy 的固定宽度数组加法可能回绕。
9.2 浮点处理
adjusted = np.clip(image.astype(np.float32) * 1.1 + 10, 0, 255).astype(np.uint8) normalized = image.astype(np.float32) / 255.0
|
不要把 0~1 的浮点结果直接强制转为 uint8 后当作正常 8 位图像保存。恢复显示范围时需要相应乘以 255 并截断。
Lena 实验:旋转与混合分别改变什么?

matrix = cv2.getRotationMatrix2D((256, 256), 25, 0.85) rotated = cv2.warpAffine(img, matrix, (512, 512), borderValue=(255,255,255)) tint = np.full_like(img, (255, 80, 0)) blended = cv2.addWeighted(img, 0.75, tint, 0.25, 0) gray_bgr = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR) faded = cv2.addWeighted(img, 0.75, gray_bgr, 0.25, 0)
|
左图的空白是旋转后输入范围之外的像素,用白色填充;中图混合固定色块,右图混合三通道灰度图。addWeighted 要求输入尺寸与类型兼容,不能直接把三通道 img 与单通道 gray 混合。这里权重之和为 1,其他权重可能造成截断。
十、平滑滤波与边缘检测
10.1 常见滤波接口
mean_blur = cv.blur(image, (3, 3)) gaussian = cv.GaussianBlur(image, (5, 5), 0) median = cv.medianBlur(image, 3)
|
blur():均值滤波。
GaussianBlur():高斯平滑;示例让标准差由核尺寸推导。
medianBlur():中值滤波,核尺寸为正奇数且大于 1。
核尺寸和输入类型有具体限制。核越大通常平滑越明显,也可能损失目标细节。
10.2 Sobel 与 Canny
gradient_x = cv.Sobel(gray, cv.CV_32F, 1, 0, ksize=3) gradient_y = cv.Sobel(gray, cv.CV_32F, 0, 1, ksize=3) magnitude = cv.magnitude(gradient_x, gradient_y)
smoothed = cv.GaussianBlur(gray, (5, 5), 0) edges = cv.Canny(smoothed, 50, 150)
|
Sobel 选择浮点输出可以保留负梯度。Canny 前显式平滑,阈值按输入调整;边缘图与填充目标区域含义不同。
Lena 实验:把滤波和 Canny 放在同一条流水线上

高斯噪声与椒盐噪声是两种不同输入:上排右侧与下排左侧处理高斯噪声,下排右侧处理椒盐噪声。可以下载开头的 lena_demo.py 复现固定种子噪声,再比较核大小变化。

smooth = cv2.GaussianBlur(gray, (5, 5), 1.2) edges = cv2.Canny(smooth, 80, 160) overlay = img.copy() overlay[edges > 0] = (255, 255, 0) cv2.imwrite("edges.png", edges) cv2.imwrite("overlay.png", overlay)
|
配图中的三组阈值共用同一个平滑输入。降低阈值通常会增加弱边缘和杂线;提高阈值也可能漏掉眼睛或帽沿的结构。edges 是单通道 uint8 图,不是坐标列表,也不是人物识别结果。
十一、阈值、掩码与颜色范围
11.1 threshold 的两个返回值
threshold_value, binary = cv.threshold(gray, 127, 255, cv.THRESH_BINARY)
|
第一个返回值为实际使用的阈值,第二个为结果图。THRESH_BINARY 对超过阈值的像素赋 255,其余赋 0。
Otsu 自动阈值:
threshold_value, binary = cv.threshold( gray, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU )
|
对本教程常见 8 位单通道输入可直接使用上述形式。自动阈值并不保证在任意光照与背景下正确分割。
11.2 inRange 与 bitwise_and
hsv = cv.cvtColor(image, cv.COLOR_BGR2HSV) lower = np.array([35, 60, 50], dtype=np.uint8) upper = np.array([85, 255, 255], dtype=np.uint8)
mask = cv.inRange(hsv, lower, upper) selected = cv.bitwise_and(image, image, mask=mask)
|
上例演示绿色范围,实际参数需要采样验证。inRange 检查各通道闭区间,输出单通道 0/255 掩码。
bitwise_and 的掩码通常要求与输入宽高相同、8 位单通道;非零表示参与输出,不表示按灰度做透明度混合。
Lena 实验:颜色范围只是像素条件

hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, np.array([140, 40, 40]), np.array([179,255,255])) selected = cv2.bitwise_and(img, img, mask=mask)
|
掩码中白色表示命中、黑色表示排除,输出 mask 为单通道 uint8。它选中了一些紫红色区域,也可能选中背景或漏掉阴影中的目标,不能把它当作“皮肤检测”或“人像分割”。这里的 H 范围针对 OpenCV 常用的 8 位 HSV,H 为 0~179;浮点 HSV 的数值约定不同。
练习:只改变 H 的下界,再改变 S 的下界,分别记录哪类区域被排除,不要同时改所有参数。
十二、形态学操作
kernel = cv.getStructuringElement(cv.MORPH_RECT, (3, 3))
eroded = cv.erode(binary, kernel) dilated = cv.dilate(binary, kernel) opened = cv.morphologyEx(binary, cv.MORPH_OPEN, kernel) closed = cv.morphologyEx(binary, cv.MORPH_CLOSE, kernel)
|
| 接口 |
白色前景下的常见效果 |
erode() |
收缩前景 |
dilate() |
扩张前景 |
MORPH_OPEN |
先腐蚀后膨胀,可能去除小白点 |
MORPH_CLOSE |
先膨胀后腐蚀,可能填小缺口 |
结构元素还可以选择 MORPH_ELLIPSE、MORPH_CROSS。参数应根据目标尺度确定,过度处理可能删除小目标或连接多个目标。
Lena 实验:先确认前景,再做形态学

threshold_value, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) kernel = np.ones((3, 3), np.uint8) opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
|
注意:配图下排的开闭运算为了固定对照,输入是 threshold(gray,127,255,THRESH_BINARY) 的结果;上面的代码示范如何接到 Otsu 输出后,不能声称两者会得到完全相同图片。白色为前景,开运算倾向于去小白块,闭运算倾向于填小黑缝。Lena 的灰度前景与背景重叠,不适合靠这几步可靠统计“物体数量”。
十三、轮廓与连通区域
13.1 findContours
contours, hierarchy = cv.findContours( binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE )
|
示例输入为 8 位单通道二值图;RETR_EXTERNAL 只返回外部轮廓。OpenCV 4 通常返回轮廓与层级两个对象,不要直接套用早期版本的三返回值写法。
13.2 测量与标注
marked = image.copy() for contour in contours: area = cv.contourArea(contour) if area < 100: continue x, y, width, height = cv.boundingRect(contour) perimeter = cv.arcLength(contour, True) cv.rectangle(marked, (x, y), (x + width - 1, y + height - 1), (0, 0, 255), 2) print(area, perimeter)
|
boundingRect 返回 (x, y, 宽, 高),不是两个角的坐标。轮廓面积与像素数量不是同一定义。
13.3 connectedComponentsWithStats
label_count, labels, stats, centroids = cv.connectedComponentsWithStats( binary, connectivity=8 )
for label in range(1, label_count): x = stats[label, cv.CC_STAT_LEFT] y = stats[label, cv.CC_STAT_TOP] width = stats[label, cv.CC_STAT_WIDTH] height = stats[label, cv.CC_STAT_HEIGHT] pixel_area = stats[label, cv.CC_STAT_AREA] print(label, (x, y, width, height), pixel_area, centroids[label])
|
第 0 个标签为背景,目标通常从标签 1 开始。8 连通允许斜对角相邻像素相连,4 连通只考虑上下左右,可能得到不同数量。
十四、模板匹配
14.1 matchTemplate
需要另行准备 template.png,它应比待搜索图像小,具有明显且非恒定的纹理:
template = cv.imread("template.png", cv.IMREAD_GRAYSCALE) if template is None: raise FileNotFoundError("无法读取 template.png")
height, width = template.shape if height > gray.shape[0] or width > gray.shape[1]: raise ValueError("模板不能大于搜索图像") if np.std(template) == 0: raise ValueError("请选择有纹理的模板")
response = cv.matchTemplate(gray, template, cv.TM_CCOEFF_NORMED) min_value, max_value, min_location, max_location = cv.minMaxLoc(response)
x, y = max_location matched = image.copy() cv.rectangle(matched, (x, y), (x + width - 1, y + height - 1), (0, 0, 255), 2) print("最高响应:", max_value)
|
输入为宽 W、高 H,模板为宽 w、高 h 时,响应图形状为 (H-h+1, W-w+1)。
14.2 不同方法的判定方向
TM_CCOEFF_NORMED 等相关性方法通常取最大响应。
TM_SQDIFF、TM_SQDIFF_NORMED 取最小响应。
- 响应值不是通用概率,必须在样例上验证阈值。
单个最高响应不等于一定找到真实目标。常规模板匹配对尺度、旋转和外观变化敏感,多目标匹配还需要阈值与重复框处理。
Lena 实验:模板匹配的分数与坐标

template = gray[240:300, 240:320].copy() response = cv2.matchTemplate(gray, template, cv2.TM_CCOEFF_NORMED) min_score, max_score, min_loc, max_loc = cv2.minMaxLoc(response) x, y = max_loc result = img.copy() cv2.rectangle(result, (x,y), (x+79,y+59), (0,255,0), 2) print(response.shape, max_loc, max_score)
|
本图的响应数组为 (453,433),即 (H-h+1,W-w+1);最佳左上角是 (240,240),分数接近 1.0。热图归一化后只用于显示,不要拿显示颜色代替原始匹配分数。
这里刻意从原图自身裁出模板,是检查接口与坐标的自匹配实验,不能证明它能在新照片、旋转、缩放或光照变化下识别人脸。练习:把模板缩小后再匹配,观察定位和分数如何变化;换用 TM_SQDIFF_NORMED 时,应取 min_loc。
十五、窗口交互:鼠标与滑动条
本节需要桌面环境,可以作为选学内容。
15.1 鼠标回调
下面是一段独立可运行的示例:
import cv2 as cv import numpy as np
canvas = np.zeros((240, 320, 3), dtype=np.uint8)
def on_mouse(event, x, y, flags, userdata): if event == cv.EVENT_LBUTTONDOWN: print("点击坐标:", x, y) cv.circle(canvas, (x, y), 4, (0, 255, 0), -1)
cv.namedWindow("Mouse") cv.setMouseCallback("Mouse", on_mouse) try: while True: cv.imshow("Mouse", canvas) if cv.waitKey(20) & 0xFF == ord("q"): break finally: cv.destroyAllWindows()
|
点击窗口可以打印坐标并标点,按 q 退出。
15.2 滑动条调阈值
在已经读取 gray 的桌面脚本中运行:
def on_change(value): pass
cv.namedWindow("Threshold") cv.createTrackbar("T", "Threshold", 127, 255, on_change) try: while True: threshold = cv.getTrackbarPos("T", "Threshold") _, preview = cv.threshold(gray, threshold, 255, cv.THRESH_BINARY) cv.imshow("Threshold", preview) if cv.waitKey(20) & 0xFF == ord("q"): break finally: cv.destroyAllWindows()
|
调好后应记录具体参数,而不是只保留交互窗口截图。
十六、读取视频与摄像头
16.1 VideoCapture 与 read
使用本地视频文件的独立示例:
import cv2 as cv
capture = cv.VideoCapture("input.mp4") try: if not capture.isOpened(): raise OSError("无法打开视频")
frame_count = 0 while True: success, frame = capture.read() if not success: break gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY) frame_count += 1
print("成功读取帧数:", frame_count) finally: capture.release()
|
read() 返回成功标志与图像。文件读到末尾或解码失败时都可能返回失败,是否正常结束需要结合输入和日志判断。
16.2 读取摄像头
把输入改为 cv.VideoCapture(0),通常表示尝试打开编号为 0 的摄像头;实际编号、权限和可用后端取决于电脑环境。
实时预览可在成功读帧后使用 imshow()、waitKey(),退出时释放设备并关闭窗口。练习前确认摄像头可用、未被其他程序占用。
16.3 属性查询与设置
capture = cv.VideoCapture("input.mp4") try: if not capture.isOpened(): raise OSError("无法打开视频") print("宽:", capture.get(cv.CAP_PROP_FRAME_WIDTH)) print("高:", capture.get(cv.CAP_PROP_FRAME_HEIGHT)) print("帧率:", capture.get(cv.CAP_PROP_FPS)) finally: capture.release()
|
摄像头可以尝试通过 set() 请求宽高和帧率,但返回成功也不应代替实际读帧验证。属性是否支持、数值是否准确取决于驱动、文件和后端。
十七、视频保存与抽帧
17.1 VideoWriter
下面把视频帧标注后写入新文件,输出路径不要与输入相同:
import math import cv2 as cv
capture = cv.VideoCapture("input.mp4") writer = None try: if not capture.isOpened(): raise OSError("无法打开输入视频") success, frame = capture.read() if not success: raise OSError("无法读取首帧")
height, width = frame.shape[:2] fps = capture.get(cv.CAP_PROP_FPS) if not math.isfinite(fps) or fps <= 0: raise ValueError("无法确定帧率,请先明确所需输出帧率")
fourcc = cv.VideoWriter_fourcc(*"mp4v") writer = cv.VideoWriter("annotated.mp4", fourcc, fps, (width, height)) if not writer.isOpened(): raise OSError("无法创建输出视频,检查路径、编码器与后端")
index = 0 while success: marked = frame.copy() cv.putText(marked, f"Frame {index}", (10, 30), cv.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) writer.write(marked) index += 1 success, frame = capture.read() finally: capture.release() if writer is not None: writer.release()
|
编码器、容器和操作系统的支持情况不同,mp4v 只是示例。写入帧必须符合声明的宽高、类型和通道约定,完成后应重新打开输出核对。
该流程不保留输入音轨,也不自动保留可变帧率时间戳。声明的 FPS 决定播放速度,不等于处理程序实际每秒处理多少帧。
17.2 按帧号间隔抽帧
from pathlib import Path import cv2 as cv
output = Path("frames") output.mkdir(parents=True, exist_ok=True) capture = cv.VideoCapture("input.mp4") try: if not capture.isOpened(): raise OSError("无法打开视频") index = 0 while True: success, frame = capture.read() if not success: break if index % 30 == 0: path = output / f"frame_{index:06d}.png" if not cv.imwrite(str(path), frame): raise OSError(f"无法保存 {path}") index += 1 finally: capture.release()
|
每 30 帧抽一张,不等于任意视频中都每秒抽一张。按真实时间抽样需要另行考虑帧率和时间戳。
十八、批量处理图像
18.1 遍历目录
下面把 input-images 中可读取的普通图片转换为灰度 PNG,输出到另一个目录:
from pathlib import Path import cv2 as cv
source = Path("input-images") output = Path("gray-images") if not source.is_dir(): raise NotADirectoryError(source) output.mkdir(parents=True, exist_ok=True)
extensions = {".png", ".jpg", ".jpeg", ".bmp"} for path in sorted(source.iterdir()): if not path.is_file() or path.suffix.lower() not in extensions: continue image = cv.imread(str(path), cv.IMREAD_COLOR) if image is None: print("跳过无法读取的文件:", path) continue gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY) target = output / f"{path.name}.gray.png" if not cv.imwrite(str(target), gray): raise OSError(f"无法保存 {target}")
|
重复运行可能覆盖同名输出。批量任务建议同时记录成功、失败、输入版本和参数,不要静默忽略所有异常。
18.2 结果记录
检测结果可保存到 CSV 或 JSON,至少包含输入名称、目标数量、位置、面积和处理参数。NumPy 标量写入 JSON 前可能需要用 int()、float() 转为 Python 类型。
十九、调试输入、类型与参数
发生 OpenCV 断言错误时,先缩小到一张输入,打印:
print(type(image)) print(image.shape) print(image.dtype) print(image.min(), image.max())
|
这段检查本身也要求输入不是 None、数组非空。按下面顺序排查:
- 文件或视频是否成功读取?
- 图像是二维还是三维,通道顺序是否正确?
- 数据类型和数值范围是否满足接口要求?
- ROI 是否为空,掩码是否同尺寸?
- 参数是否适合当前图像尺度?
先保存中间结果,检查问题从哪一步出现。只调整最后一个阈值,可能掩盖上游读取或颜色转换错误。
二十、常见问题排查
| 问题 |
可能原因 |
解决方法 |
安装后仍无法导入 cv2 |
解释器不一致或包冲突 |
检查解释器与环境,只选择一种 OpenCV 包 |
imread 返回 None |
路径、权限、格式或文件内容问题 |
检查工作目录、文件存在和格式支持 |
| 图像红蓝互换 |
RGB / BGR 不一致 |
使用对应 cvtColor 转换 |
cvtColor 报空输入 |
读取失败或空 ROI |
在调用前检查输入 |
imshow 无法使用 |
headless 包或没有 GUI 环境 |
改为保存结果,或配置桌面环境 |
| 窗口无响应 |
没有持续处理窗口事件 |
正确调用 waitKey,在桌面主流程运行 |
| 掩码操作断言失败 |
掩码类型、通道、尺寸不符合要求 |
使用同宽高 8 位单通道掩码 |
| 轮廓数量很多 |
噪声、碎片或分割方向错误 |
先检查二值图,再验证去噪和筛选条件 |
| 模板匹配总能画出一个框 |
只取最大值,没有验证是否真实匹配 |
使用样例验证阈值和失败条件 |
| 摄像头打不开 |
权限、编号、占用或后端问题 |
核对设备并先用系统相机检查 |
| 视频写不出或播放异常 |
编码器、尺寸、通道或 FPS 不匹配 |
检查 isOpened、帧属性并重新读取输出 |
| 保存成功但颜色很暗 |
浮点范围或类型转换不正确 |
核对 0~1 与 0~255 的约定 |
二十一、一个综合示例
下面生成一张含两个绿色目标和一个蓝色干扰物的图像,通过 HSV 分割、形态学和连通区域统计完成检测,保存标注图和 JSON 报告。无需下载素材,也不需要摄像头或桌面窗口。
21.1 完整代码
保存为 opencv_demo.py:
from pathlib import Path import json
import cv2 as cv import numpy as np
def save_image(path, image): if not cv.imwrite(str(path), image): raise OSError(f"无法保存:{path}")
def main(): output = Path("opencv-output") output.mkdir(parents=True, exist_ok=True)
image = np.zeros((300, 400, 3), dtype=np.uint8) cv.rectangle(image, (30, 40), (120, 130), (0, 255, 0), -1) cv.circle(image, (280, 180), 40, (0, 255, 0), -1) cv.rectangle(image, (160, 30), (220, 80), (255, 0, 0), -1) image[10, 10] = (0, 255, 0)
hsv = cv.cvtColor(image, cv.COLOR_BGR2HSV) lower = np.array([35, 60, 50], dtype=np.uint8) upper = np.array([85, 255, 255], dtype=np.uint8) mask = cv.inRange(hsv, lower, upper) kernel = cv.getStructuringElement(cv.MORPH_RECT, (3, 3)) clean = cv.morphologyEx(mask, cv.MORPH_OPEN, kernel)
count, labels, stats, centroids = cv.connectedComponentsWithStats( clean, connectivity=8 ) regions = [] for label in range(1, count): area = int(stats[label, cv.CC_STAT_AREA]) if area < 100: continue regions.append({ "bbox": [ int(stats[label, cv.CC_STAT_LEFT]), int(stats[label, cv.CC_STAT_TOP]), int(stats[label, cv.CC_STAT_WIDTH]), int(stats[label, cv.CC_STAT_HEIGHT]), ], "pixel_area": area, "centroid": [float(value) for value in centroids[label]], }) regions.sort(key=lambda region: region["bbox"][0])
marked = image.copy() for index, region in enumerate(regions, start=1): x, y, width, height = region["bbox"] cv.rectangle(marked, (x, y), (x + width - 1, y + height - 1), (0, 0, 255), 2) cv.putText(marked, f"ID {index}", (x, max(15, y - 5)), cv.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv.LINE_AA)
save_image(output / "input.png", image) save_image(output / "mask.png", mask) save_image(output / "clean.png", clean) save_image(output / "marked.png", marked)
report = { "opencv_version": cv.__version__, "hsv_lower": lower.tolist(), "hsv_upper": upper.tolist(), "object_count": len(regions), "regions": regions, } (output / "report.json").write_text( json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8" ) print("绿色目标数量:", len(regions))
if __name__ == "__main__": main()
|
21.2 运行与检查
预期绿色目标数量为 2。检查:蓝色矩形是否未进入掩码,单个绿色噪点是否被去除,矩形和圆形是否都被标注。
需要使用前面小节的读取代码时,可将 opencv-output/input.png 复制为当前目录的 input.png。程序重复运行会覆盖同名输出。
21.3 使用范围
合成图颜色纯、目标分离明显。真实图像还受光照、反光、阴影、压缩与目标接触等影响;颜色区间、核尺寸和面积阈值都需要重新验证。
二十二、练习
22.1 基础操作
22.2 分割与测量
22.3 视频与交互(按环境选做)
提交材料建议包含代码、输入来源、关键参数、结果、运行说明和已知问题。摄像头与编码器不可用时,先完成不依赖设备的部分。
二十三、常用接口速查表
| 功能 |
示例 |
| 读图 |
cv.imread(path, cv.IMREAD_COLOR) |
| 写图 |
cv.imwrite(path, image),检查返回值 |
| 窗口显示 |
cv.imshow("Preview", image) |
| 窗口事件与按键 |
cv.waitKey(20) |
| 关闭窗口 |
cv.destroyAllWindows() |
| 灰度转换 |
cv.cvtColor(image, cv.COLOR_BGR2GRAY) |
| HSV 转换 |
cv.cvtColor(image, cv.COLOR_BGR2HSV) |
| 画框 |
cv.rectangle(image, (x1,y1), (x2,y2), color, 2) |
| 写文字 |
cv.putText(image, text, origin, font, scale, color) |
| 缩放 |
cv.resize(image, (width, height)) |
| 旋转 90 度 |
cv.rotate(image, cv.ROTATE_90_CLOCKWISE) |
| 加权混合 |
cv.addWeighted(a, alpha, b, beta, gamma) |
| 高斯滤波 |
cv.GaussianBlur(image, (5,5), 0) |
| 固定阈值 |
cv.threshold(gray, 127, 255, cv.THRESH_BINARY) |
| 范围掩码 |
cv.inRange(hsv, lower, upper) |
| 掩码选取 |
cv.bitwise_and(image, image, mask=mask) |
| 开运算 |
cv.morphologyEx(mask, cv.MORPH_OPEN, kernel) |
| 边缘 |
cv.Canny(smoothed, 50, 150) |
| 外部轮廓 |
cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) |
| 外接矩形 |
cv.boundingRect(contour) |
| 连通区域统计 |
cv.connectedComponentsWithStats(binary, connectivity=8) |
| 模板匹配 |
cv.matchTemplate(gray, template, cv.TM_CCOEFF_NORMED) |
| 响应极值 |
cv.minMaxLoc(response) |
| 读取视频或摄像头 |
cv.VideoCapture(path_or_index) |
| 读取一帧 |
success, frame = capture.read() |
| 创建视频输出 |
cv.VideoWriter(path, fourcc, fps, (width,height)) |
| 释放视频资源 |
capture.release() / writer.release() |
速查表中的变量需要先按正文定义,参数和类型检查不能省略。
二十四、相关学习资料
查阅时注意 OpenCV 版本与示例语言。C++ 文档中的函数签名与 Python 返回值形式可能不同,应同时查看对应 Python 接口。