01
OpenCV 是什么:二十多年的视觉瑞士军刀
OpenCV(Open Source Computer Vision Library)1999 年在 Intel 内部启动,2006 年首次对外发布,现由 OpenCV 基金会维护,是计算机视觉领域事实上的基础库。人脸检测、目标跟踪、镜头矫正、二维码识别——你在手机相册和工业相机软件里见到的很多功能,底层都有它。
Python 里用它只有一条心智路径:图像就是 NumPy 数组。读进来是数组,切片是数组,滤波、边缘、轮廓处理的还是数组。所以 NumPy 熟练度直接决定你的 OpenCV 上限;反过来,它也是练 NumPy 切片最好的场景。
典型战场:把扫描件摆正去噪、给照片批量打标、工业流水线上检测缺陷、扫码识别、给深度模型做预处理。如果你要解决的问题能一句话概括成「让程序看图做事」,OpenCV 通常就是第一行 import cv2。
- 起源
- Intel,1999 年启动,2006 年首次开源发布
- 维护方
- OpenCV 基金会(2012 年起)
- 核心语言
- C++(提供 Python / Java 等绑定)
- 协议
- Apache 2.0(4.5.0 起,此前为 BSD)
- pip 包
- opencv-python(2026-11 查:5.0 系列最新,4.x 仍在维护)
- 图像本质
- NumPy 数组(高 × 宽 × 通道)
- 默认通道顺序
- BGR,不是 RGB
- GUI
- 依赖桌面环境;服务器用 headless 包
cv2.dnn 模块也能直接加载训练好的模型做推理(见第 04 节)。02
安装:四个包,永远只装一个
pip 上的 OpenCV 有四个包,功能不同但共用同一个 cv2 命名空间。官方明确要求同一环境只装其中一个,装多了会互相覆盖,报一些莫名其妙的错。
# 主模块包,桌面环境(Windows / macOS / 常见 Linux)装这个 pip install opencv-python # 会自动带上 numpy 依赖;想顺手升级 numpy: pip install -U opencv-python numpy
# 全量包:主模块 + contrib 扩展(额外的特征匹配、二维码、跟踪算法等) pip install opencv-contrib-python # 无 GUI 包:服务器 / Docker / CI,没有显示器时用这个,体积更小 pip install opencv-python-headless
# 打印出版本号就装好了 python -c "import cv2; print(cv2.__version__)" # 顺手确认 numpy 也正常(OpenCV 离不开它) python -c "import cv2, numpy; print(cv2.__version__, numpy.__version__)"
pip uninstall opencv-python opencv-contrib-python opencv-python-headless opencv-contrib-python-headless,然后只装你要的那一个。报错像「cv2 has no attribute」或 DLL 冲突,多半是这个原因。03
快速上手:读图 → 灰度 → 边缘 → 保存
找一张 JPG 放到当前目录(下面假设叫 photo.jpg),八行代码走完视觉处理最经典的流水线:
import cv2 img = cv2.imread("photo.jpg") # 读图 → NumPy 数组 print(img.shape) # (高, 宽, 3),BGR 三通道 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转 8 位灰度图 edges = cv2.Canny(gray, 100, 200) # Canny 边缘检测 cv2.imwrite("photo_edges.png", edges) # 按扩展名自动选编码器 cv2.imshow("edges", edges) # 弹窗显示(仅本地桌面) cv2.waitKey(0) # 等任意按键,0 = 一直等 cv2.destroyAllWindows()
-
01
先确认读到了
imread读失败不报错,只返回 None。下一步就报AttributeError: 'NoneType' object。养成习惯:读图后立刻assert img is not None或打印 shape。 -
02
彩色转灰度
边缘、阈值、直方图这类算法只要单通道。cvtColor 是通道与色彩空间转换的总入口,第二个参数是转换码,敲
cv2.COLOR_BGR2会有几十个可选。 -
03
调 Canny 的两个阈值
100/200 是常用起点:阈值差越大保留的边缘越连贯。图像发虚就降阈值,噪点多就先 GaussianBlur 再 Canny。
-
04
保存与显示
imwrite 按扩展名决定格式(.png / .jpg)。imshow 必须配 waitKey,否则窗口一闪而过或直接无响应;脚本结束前 destroyAllWindows 收尾。
cv2.imshow 会直接报错——那是包没装错、环境没显示器。用 imwrite 存文件查看,或装 opencv-python-headless 并改用 matplotlib 显示。没有桌面环境时,把上面八行里最后三行(imshow / waitKey / destroyAllWindows)删掉,就是纯文件流程——读图、处理、保存,全程不弹窗,在任何服务器上都能跑:
import cv2 img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE) # 直接按灰度读 edges = cv2.Canny(img, 100, 200) print("边缘像素数:", int((edges > 0).sum())) # 用数字确认处理有效 cv2.imwrite("photo_edges.png", edges)
04
核心概念:数组、BGR、绘制与视频
图像就是 NumPy 数组
彩色图 shape 是 (高, 宽, 3),每个元素 uint8、取值 0–255。于是所有 NumPy 技巧都能用:切片裁剪、通道拆分、逐像素运算。注意第一维是高(y)不是宽——这是和坐标写法最容易被搞混的地方。
h, w = img.shape[:2] roi = img[100:300, 200:500] # 裁剪:先行(y)后列(x) b, g, r = cv2.split(img) # 拆通道;cv2.merge([b, g, r]) 合回 img[50, 80] # 第 50 行第 80 列那个像素,返回 [B, G, R] img[:] = 0 # 整图涂黑——数组是引用,改了就是真改
BGR:历史遗留的经典坑
OpenCV 出于早年相机厂商字节序的原因,通道顺序是 蓝-绿-红。你自己用 np.zeros 手搓图像没问题,但要把 OpenCV 的图交给其他库(matplotlib、PIL、深度模型)时,必须先 cv2.cvtColor(img, cv2.COLOR_BGR2RGB),否则红蓝互换,人脸发蓝。
缩放、绘制与标注
resize 负责缩放,rectangle / circle / putText 负责把检测结果画出来。关键区别:绘制函数的坐标是 (x, y),而数组切片是 [y, x]——写反了画到图外去,什么也看不见。
small = cv2.resize(img, None, fx=0.5, fy=0.5) # 缩到一半 small = cv2.resize(img, (640, 480), interpolation=cv2.INTER_AREA) cv2.rectangle(img, (60, 50), (220, 200), (0, 255, 0), 2) # 绿框,线宽 2;-1 为实心 cv2.circle(img, (150, 120), 30, (0, 0, 255), -1) # 红色实心圆 cv2.putText(img, "cat: 0.98", (60, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) # putText 只支持 ASCII——中文标注得先画到 PIL/Pillow 再转回 ndarray
视频与摄像头:一帧一帧的图像
VideoCapture 把视频和摄像头统一成「循环读帧」:视频是文件路径,摄像头是设备编号(0 通常是默认摄像头)。每一帧就是普通图像,前面所有操作全部适用。
cap = cv2.VideoCapture(0) # 或 VideoCapture("demo.mp4") while cap.isOpened(): ok, frame = cap.read() # ok=False 表示读完了 if not ok: break cv2.imshow("gray", cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)) if cv2.waitKey(1) & 0xFF == ord("q"): # 按 q 退出 break cap.release() cv2.destroyAllWindows() # 写视频用 cv2.VideoWriter(路径, fourcc, 帧率, (宽, 高)),注意宽高顺序
fourcc = cv2.VideoWriter_fourcc(*"mp4v") # 编码器要与扩展名匹配 writer = cv2.VideoWriter("out.mp4", fourcc, 30.0, (1280, 720)) cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) # 尺寸要和 Writer 声明一致 cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) while cap.isOpened(): ok, frame = cap.read() if not ok: break writer.write(frame) cap.release() writer.release() # 不 release 文件会损坏
与深度学习结合:cv2.dnn 与人脸检测
OpenCV 不负责训练,但自带 cv2.dnn 推理模块,能加载 ONNX、Caffe、TensorFlow 等格式的预训练模型直接前向推理,适合部署端轻量使用。而人脸检测不用神经网络也有百年老配方:内置的 Haar 级联分类器,一个 XML 文件搞定。
detector = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = detector.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5)
for (x, y, w2, h2) in faces: # 返回 [(x, y, w, h), ...]
cv2.rectangle(img, (x, y), (x + w2, y + h2), (255, 0, 0), 2)
05
速查表:最常用的 cv2 函数
下面这块表格能搜。忘了某个函数怎么写,直接搜 缩放、边缘、轮廓 之类的词。
| 函数 | 作用 |
|---|---|
| cv2.imread("a.jpg") | 读图;失败返回 None 不报错,务必检查 |
| cv2.imread(path, cv2.IMREAD_GRAYSCALE) | 直接按灰度读,省一次 cvtColor |
| cv2.imwrite("b.png", img) | 按扩展名选编码器保存,返回是否成功 |
| cv2.imwrite("a.jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 90]) | 带参数保存:控制 JPG 压缩质量 |
| cv2.imread(p, cv2.IMREAD_UNCHANGED) | 保留 alpha 通道读 PNG(4 通道) |
| cv2.imshow("w", img) + cv2.waitKey(0) | 弹窗显示;waitKey 必须调,0 表示无限等 |
| cv2.destroyAllWindows() | 关掉所有窗口,脚本收尾标配 |
| cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | 色彩空间转换总入口;BGR2RGB 同理 |
| cv2.imdecode(np.fromfile(p, np.uint8), 1) | 绕开 imread 的中文路径问题(Windows) |
| cv2.imencode(".png", img)[1].tofile(p) | 中文路径保存的对应写法 |
| img.shape / img.dtype | (高, 宽, 通道);uint8,取值 0–255 |
| cv2.split(img) / cv2.merge([...]) | 拆 / 合 BGR 通道 |
| cv2.VideoCapture(0) | 开摄像头;传文件路径则读视频 |
| cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) | 设置采集宽高、帧率等属性 |
| cap.read() | 返回 (是否成功, 帧数组);失败即读完 |
| cv2.VideoWriter(p, fourcc, fps, (w, h)) | 写视频;尺寸是 (宽, 高),与 shape 相反 |
| 函数 | 作用 |
|---|---|
| cv2.resize(img, (640, 480)) | 指定目标 (宽, 高);缩小用 INTER_AREA 更平滑 |
| cv2.resize(img, None, fx=0.5, fy=0.5) | 按比例缩放 |
| img[y1:y2, x1:x2] | 裁剪 ROI:先行后列,和绘制坐标相反 |
| cv2.flip(img, 1) | 镜像:1 水平、0 垂直、-1 双向 |
| cv2.getRotationMatrix2D + cv2.warpAffine | 绕指定点旋转/仿射变换 |
| cv2.rectangle(img, (x1,y1), (x2,y2), color, 2) | 画框;坐标是 (x, y),线宽 -1 为实心 |
| cv2.circle(img, (x, y), r, color, -1) | 画圆,-1 实心 |
| cv2.putText(img, "txt", (x, y), font, 0.8, color, 2) | 写字,仅 ASCII;中文走 Pillow |
| cv2.add / cv2.subtract | 图像相加相减(饱和截断,与 NumPy 直接加减不同) |
| cv2.addWeighted(a, 0.7, b, 0.3, 0) | 按权重叠加两张图,做半透明水印 |
| cv2.cvtColor(img, cv2.COLOR_BGR2HSV) | 转 HSV:按颜色定位物体比 BGR 稳定 |
| cv2.inRange(hsv, lower, upper) | 颜色阈值分割,输出掩膜图 |
| cv2.getPerspectiveTransform + cv2.warpPerspective | 四点透视校正,把拍歪的文档摆正 |
| cv2.copyMakeBorder(img, 20, 20, 20, 20, cv2.BORDER_CONSTANT) | 给图加边框/补边 |
| 函数 | 作用 |
|---|---|
| cv2.GaussianBlur(img, (5, 5), 0) | 高斯模糊去噪;核必须是正奇数 |
| cv2.medianBlur(img, 5) | 中值滤波,专治椒盐噪点 |
| cv2.bilateralFilter(img, 9, 75, 75) | 双边滤波:磨皮不糊边,速度慢 |
| cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) | 全局二值化;返回 (阈值, 结果图) |
| cv2.threshold(..., cv2.THRESH_OTSU) | 加 Otsu 标志自动选阈值,阈值参数随便填 |
| cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) | 局部自适应阈值,光照不均时用 |
| cv2.Canny(gray, 100, 200) | Canny 边缘检测,先模糊效果更稳 |
| cv2.Sobel / cv2.Laplacian | 梯度与二阶导算子,Canny 的底层原理 |
| cv2.calcHist([img], [0], None, [256], [0, 256]) | 直方图统计,看曝光分布 |
| cv2.equalizeHist(gray) | 直方图均衡化,提升暗图对比度 |
| cv2.morphologyEx(bin, cv2.MORPH_OPEN, kernel) | 开/闭运算:去掉边缘图里的碎点 |
| np.getStructuringElement / np.ones((5,5), np.uint8) | 形态学的结构元 kernel |
| np.hstack([...]) / np.vstack([...]) | NumPy 拼接多张图(通道数须一致) |
| 函数 | 作用 |
|---|---|
| cv2.findContours(bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | 找轮廓;4.x 起返回 2 个值(3.x 是 3 个) |
| cv2.drawContours(img, cs, -1, (0, 0, 255), 2) | 画轮廓;-1 表示全部 |
| cv2.boundingRect(c) / cv2.minAreaRect(c) | 轮廓的外接正矩形 / 最小面积旋转矩形 |
| cv2.contourArea(c) / cv2.arcLength(c, True) | 轮廓面积 / 周长 |
| cv2.approxPolyDP(c, eps, True) | 多边形逼近,数顶点识别形状 |
| cv2.moments(c) / cv2.convexHull(c) | 矩(算质心)/ 凸包 |
| cv2.HoughLinesP(edge_img, ...) | 概率霍夫直线检测 |
| cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, ...) | 霍夫圆检测 |
| cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml") | Haar 级联人脸检测器 |
| detector.detectMultiScale(gray, 1.1, 5) | 人脸检测执行;返回 (x, y, w, h) 列表 |
| cv2.dnn.readNetFromONNX("model.onnx") | 加载预训练模型做推理(也有 Caffe/TF 变体) |
| cv2.matchTemplate(img, tpl, cv2.TM_CCOEFF_NORMED) | 模板匹配,找小图在大图中的位置 |
| cv2.QRCodeDetector() | 二维码检测与解码:detectAndDecode(img) |
没有匹配的函数。换个词试试,比如「缩放」「边缘」「人脸」。
06
实战:批量水印 + 三联对比图生成器
把前面的操作串成一个能用的命令行小工具:扫一个目录里的所有 JPG,给每张图打上水印,再生成「原图 | 灰度 | 边缘」的三联拼图方便检查。约 30 行,直接可跑:
# 用法: python watermark.py ./photos ./out # 依赖: pip install opencv-python numpy import sys from pathlib import Path import cv2 import numpy as np src, out = Path(sys.argv[1]), Path(sys.argv[2]) out.mkdir(parents=True, exist_ok=True) for path in sorted(src.glob("*.jpg")): img = cv2.imread(str(path)) if img is None: # 坏文件/乱路径:imread 静默返回 None print(f"跳过(读不了): {path.name}") continue # 1. 右下角水印:先画黑色描边再画白字,任何背景都看得清 h, w = img.shape[:2] cv2.putText(img, "AI Code Study", (w - 262, h - 26), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 0), 3) cv2.putText(img, "AI Code Study", (w - 262, h - 26), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 1) # 2. 灰度 + Canny,统一转回三通道再横向拼接 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) edges = cv2.Canny(gray, 100, 200) trio = np.hstack([ img, cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR), # 通道数必须一致才能拼 cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR), ]) trio = cv2.resize(trio, None, fx=0.4, fy=0.4) # 三张图并排,缩到合理大小 cv2.imwrite(str(out / f"{path.stem}_wm.jpg"), img) cv2.imwrite(str(out / f"{path.stem}_trio.jpg"), trio) print(f"完成: {path.name} → 水印图 + 三联对比图")
跑起来看效果
$ mkdir -p photos && cp ~/Desktop/*.jpg photos/ $ python watermark.py photos out 完成: cat.jpg → 水印图 + 三联对比图 完成: street.jpg → 水印图 + 三联对比图 $ ls out/ cat_trio.jpg cat_wm.jpg street_trio.jpg street_wm.jpg
cv2.addWeighted 把文字层和原图按权重叠加(见下);想让 Canny 阈值自适应,可以把 100/200 换成中位数的倍数。改一处跑一次,肉眼对比 out 目录里的三联图。变体:半透明水印
实心文字太重?用 addWeighted 把「画了字的白底层」和原图按权重融合,透明度由权重控制:
layer = img.copy() cv2.putText(layer, "AI Code Study", (w - 262, h - 26), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 3) img = cv2.addWeighted(layer, 0.35, img, 0.65, 0) # 字 35%,原图 65%
原理:两张同尺寸图按权重线性混合。想让水印只出现在局部,把 layer 里水印区域之外涂黑(乘一个掩膜)再混合。
这个项目练到的点,正好是前三节的主线:防御式读图、cvtColor 通道转换、putText 绘制、NumPy 拼接、resize——改任何一个参数跑一遍,比多看三篇教程有用。
07
避坑:四个新手必踩的坑
坑一:imread 静默返回 None
路径错了、文件损坏、扩展名骗人(jpg 实际是 webp),imread 都不报错,返回 None,然后下一行才炸出莫名其妙的 NoneType 错误。Windows 上还有中文路径这层:imread 内部用 fopen,遇到非 ASCII 路径可能直接失败,用 cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR) 绕开。
img = cv2.imread("photo.jpg") if img is None: raise FileNotFoundError(f"读不到: {path}(检查相对路径与工作目录)") print(img.shape) # 到这里才安全
坑二:imshow 窗口一闪而过或卡死
imshow 后不调 cv2.waitKey(0),窗口立刻消失;waitKey 参数给错(如填 0 又放在循环里),程序假死。规矩就三条:imshow 必配 waitKey;循环读帧用 waitKey(1);结束前 destroyAllWindows。服务器/Docker 上干脆没有 GUI,imshow 直接报错——装 headless 包并改用 imwrite 输出文件。
matplotlib.pyplot.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) 在单元格内显示,记得先转 RGB。坑三:findContours 解包报错
老教程写 _, contours, _ = cv2.findContours(...)(三个返回值),在 OpenCV 4.x 上直接报 too many values to unpack——4.0 起只返回两个值:
# OpenCV 4.x:返回 (contours, hierarchy) contours, _ = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 注意:findContours 会修改输入图,喂给它的二值图要先 copy 或现场生成 # 入参必须是二值图:先 threshold / Canny,再 findContours
坑四:颜色发蓝、坐标画偏
两个「错觉级」bug 都源于坐标系约定:颜色——matplotlib 里直接显示 OpenCV 图像,红蓝互换,转一次 RGB 就好;坐标——绘制函数收 (x, y),数组切片用 [y, x],resize 的尺寸参数也是 (宽, 高) 而 shape 是 (高, 宽)。拿不准就在纸上画个小坐标轴,比反复试错快。
import matplotlib.pyplot as plt plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) # 先转 RGB 再显示 plt.axis("off") plt.show()
08
进阶方向与资源
- OpenCV 官方文档(4.x)——API 参考 + 官方教程,查函数参数的第一站。
- 官方 Tutorials——从图像处理到深度学习推理的系统教程。
- opencv/opencv——源码仓库;扩展模块在 opencv_contrib 仓库。
- opencv-python(PyPI)——四个 pip 包的区别与「只装一个」的官方说明都在这。
- OpenCV University——官方出品的免费课程(OpenCV Bootcamp 等)。
- LearnOpenCV——OpenCV 团队成员主理的教学站,进阶案例极多。
09
收尾:照着勾一遍
0 / 5