VISION · OPENCV · 计算机视觉第一课

OpenCV:让程序看懂图像

图像在程序眼里只是一个 NumPy 数组。这页带你从读写一张图开始,把滤波、边缘、轮廓这些经典操作跑起来,最后做一个批量加水印的小工具——所有代码都能直接复制运行。

zsh — ~/vision-lab
$ pip install opencv-python
Successfully installed opencv-python-5.0.0 numpy-2.x
$ python edge_demo.py photo.jpg
读入 photo.jpg → shape=(1080, 1920, 3) dtype=uint8
灰度 → Canny 边缘 → 完成
已保存 photo_edges.png
本页目录

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 包
和深度学习框架的分工:PyTorch、TensorFlow 负责「训练神经网络」,OpenCV 负责「把图像变成好用的输入」和「跑经典视觉算法」。两者不是竞争关系——真实项目里经常是 OpenCV 读图做预处理,再喂给深度模型;OpenCV 自带的 cv2.dnn 模块也能直接加载训练好的模型做推理(见第 04 节)。

02

安装:四个包,永远只装一个

pip 上的 OpenCV 有四个包,功能不同但共用同一个 cv2 命名空间。官方明确要求同一环境只装其中一个,装多了会互相覆盖,报一些莫名其妙的错。

Terminal
# 主模块包,桌面环境(Windows / macOS / 常见 Linux)装这个
pip install opencv-python

# 会自动带上 numpy 依赖;想顺手升级 numpy:
pip install -U opencv-python numpy
新手直接装 opencv-python 即可。它不含 contrib 扩展模块,但本页全部功能都覆盖。
装错了解法:如果曾经装过多个包,全部卸干净再装一个:pip uninstall opencv-python opencv-contrib-python opencv-python-headless opencv-contrib-python-headless,然后只装你要的那一个。报错像「cv2 has no attribute」或 DLL 冲突,多半是这个原因。

03

快速上手:读图 → 灰度 → 边缘 → 保存

找一张 JPG 放到当前目录(下面假设叫 photo.jpg),八行代码走完视觉处理最经典的流水线:

Python · edge_demo.py
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 收尾。

在服务器、Docker、Jupyter 远程内核上没有桌面环境,cv2.imshow 会直接报错——那是包没装错、环境没显示器。用 imwrite 存文件查看,或装 opencv-python-headless 并改用 matplotlib 显示。

没有桌面环境时,把上面八行里最后三行(imshow / waitKey / destroyAllWindows)删掉,就是纯文件流程——读图、处理、保存,全程不弹窗,在任何服务器上都能跑:

Python · headless 变体
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)不是宽——这是和坐标写法最容易被搞混的地方。

Python · 数组操作
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]——写反了画到图外去,什么也看不见。

Python · 缩放与绘制
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 通常是默认摄像头)。每一帧就是普通图像,前面所有操作全部适用。

Python · 摄像头灰度预览
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, 帧率, (宽, 高)),注意宽高顺序
Python · 把摄像头画面录成 MP4
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 文件搞定。

Python · Haar 人脸检测
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 相反

06

实战:批量水印 + 三联对比图生成器

把前面的操作串成一个能用的命令行小工具:扫一个目录里的所有 JPG,给每张图打上水印,再生成「原图 | 灰度 | 边缘」的三联拼图方便检查。约 30 行,直接可跑:

Python · watermark.py
# 用法: 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} → 水印图 + 三联对比图")

跑起来看效果

Terminal
$ 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
往里加功能的顺序:换水印文字/位置只动 putText 两行;想做半透明水印就用 cv2.addWeighted 把文字层和原图按权重叠加(见下);想让 Canny 阈值自适应,可以把 100/200 换成中位数的倍数。改一处跑一次,肉眼对比 out 目录里的三联图。

变体:半透明水印

实心文字太重?用 addWeighted 把「画了字的白底层」和原图按权重融合,透明度由权重控制:

Python · 半透明水印(替换 putText 两行)
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) 绕开。

Python · 防御式读图
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 输出文件。

远程 Jupyter 用户:notebook 内核在服务器上,imshow 无法弹窗到你本机。用 matplotlib.pyplot.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) 在单元格内显示,记得先转 RGB。

坑三:findContours 解包报错

老教程写 _, contours, _ = cv2.findContours(...)(三个返回值),在 OpenCV 4.x 上直接报 too many values to unpack——4.0 起只返回两个值:

Python · 4.x 正确写法
# 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 是 (高, 宽)。拿不准就在纸上画个小坐标轴,比反复试错快。

Python · matplotlib 里正确显示 OpenCV 图
import matplotlib.pyplot as plt

plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))   # 先转 RGB 再显示
plt.axis("off")
plt.show()

08

进阶方向与资源

学完本页去哪:先把 NumPy 切片练熟(去 machine-learning.html 打底),然后两条路:往经典视觉深入——轮廓分析、相机标定、光流;往深度学习走——用 cv2.dnn 部署 ONNX 模型,或者进 pytorch.html 学训练自己的视觉模型。

09

收尾:照着勾一遍

0 / 5

已复制