OpenCV 开发案例
1. OpenCV 简介
OpenCV 是开源的计算机视觉库,常用于读取图片和视频、转换色彩、去噪、提取轮廓和分析画面变化。在 Python 中,一张图片或一帧视频通常以 NumPy 数组表示。可以先处理单张图片,再把同样的操作应用到视频的每一帧。
本篇从图片和视频两个场景入手。图片案例先用高斯滤波减少噪声,再用 Canny 检测轮廓。视频案例可以切换灰度预览、边缘预览和运动区域预览。运动区域通过比较相邻帧的亮度来标出画面变化,适合观察固定机位下的活动。
在 Rockchip 板卡上,系统 GStreamer 调用 MPP 硬件解码本地视频,Python 端的 OpenCV 负责处理解码后的帧并在 HDMI 桌面显示。后文先演示读取灰度帧,再介绍如何直接读取 NV12 的亮度平面,并对比两种路径的板端预览速率。
2. 环境准备
2.1 适用版型与本次实测环境
本次使用 Python OpenCV 5.0.0 实测了以下两套环境:
| 板卡 | 系统与桌面 | 内核 | Python |
|---|---|---|---|
| ArmSoM CM5 + CM5-IO(RK3576) | Debian 12 arm64 XFCE | 6.1.118-rk3576 | 3.11 |
| ArmSoM AIM7(RK3588) | Ubuntu 22.04.5 LTS arm64 GNOME/X11 | 6.1.118-rk3588 | 3.10.12 |
下表列出 SDK 提供的板型与系统配置。其余板型和镜像组合需按第 2.2 节确认依赖,并在目标板端验证。
ArmSoM SDK 的多个板型共用 Debian 和 Ubuntu rootfs 构建目录。SDK 中可找到以下 ArmSoM 产品配置。
| 芯片 | 板型 | SDK 系统配置 |
|---|---|---|
| RK3528 | Sige1 | Debian |
| RK3576 | AIM5、CM5(CM5-IO 和 CM4 Nano B IO)、Sige5 | Debian、Ubuntu |
| RK3588 / RK3588S | AIM7、CM7S、LM7、Sige7、Sige7S | Debian、Ubuntu |
SDK 的 Debian 12、Ubuntu 22.04 和 Ubuntu 24.04 rootfs 目录均包含 ARM64 的 gst-rkmpp 软件包。表格列出的是 SDK 配置。运行 HDMI 案例时,请使用桌面镜像,确认 gst-inspect-1.0 mppvideodec 能找到插件且本地显示可用。静态图片案例只需要 Python OpenCV 和输入图片。
2.2 安装与检查依赖
- 使用对应板型的桌面镜像,已验证的系统版本见第 2.1 节。
- 将显示器接到 HDMI 并启动图形桌面。串口运行时需指定可访问的桌面显示,实测为
:0。 - 准备 Python venv、FFmpeg(含
ffprobe和libx264)及系统 GStreamer。镜像需提供mppvideodec、qtdemux、h264parse和videoconvert。如果无法找到mppvideodec,请先检查镜像是否包含 Rockchip MPP 插件。 - 在独立环境安装带 GUI 的 OpenCV 包。若已有相同版本的环境,直接复用。SDK 源码和系统 OpenCV 无需调整。
首次启动后,接好 HDMI,登录桌面,再接网线或在桌面连接 Wi-Fi。以下命令在板端终端执行。串口可用于输入命令,预览窗口仍显示在板端桌面。APT 命令以 root 身份执行,普通用户请在 APT 命令前加 sudo。
apt update
apt install -y python3-venv curl ffmpeg x11-xserver-utils gstreamer1.0-tools python3-gi \
gir1.2-gstreamer-1.0 gir1.2-gst-plugins-base-1.0
mkdir -p ~/cvproj
cd ~/cvproj
python3 -m venv venv
./venv/bin/python -m pip install --only-binary=:all: opencv-python==5.0.0.93
./venv/bin/python -c 'import cv2; print(cv2.__version__)'
# 检查 GStreamer 能否找到 Rockchip MPP 硬件解码器。
gst-inspect-1.0 mppvideodec
for element in qtdemux h264parse videoconvert appsink; do
gst-inspect-1.0 "$element" >/dev/null || break
done
DISPLAY=:0 xrandr --current
确认 OpenCV 输出 5.0.0,gst-inspect-1.0 mppvideodec 能显示 Factory Details 或 Plugin Details(此命令只查询插件信息),其他元件检查无报错,且 xrandr 显示 HDMI 已连接。若缺少 MPP 或其他 GStreamer 元件,请检查当前板型的官方桌面镜像和对应软件包,再继续测试。串口遇到无法连接显示器的错误时,请在 HDMI 桌面的终端运行,或使用当前桌面会话的 DISPLAY 值和登录用户。
2.3 刚刷镜像时的 APT 排查
软件源查看、备份和更新的通用步骤见更新 APT 软件源。AIM7 的 Ubuntu 镜像实测遇到了以下两种情况:
- 提示
Release file ... is not valid yet。先执行timedatectl status对照当前日期、时间和时区。实测板端时钟落后约一个月。通过桌面设置启用自动校时,或在无时间同步服务时手动设为当前时间,再重新运行apt update。串口手动校时时,可以用date -s '当前日期 时间 时区偏移',执行前请替换引号内的全部内容。 - 更新卡在镜像附带的第三方源。确认
/etc/apt/sources.list包含与当前系统匹配的官方 Ubuntu 源后,本次可临时跳过/etc/apt/sources.list.d/。下面的命令以 root 身份执行,分别刷新索引和安装缺失的python3-venv。其他依赖有缺失时,可用同样的参数执行第 2.2 节的安装命令。
apt-get -o Dir::Etc::sourceparts=- -o Acquire::Retries=0 -o Acquire::http::Timeout=20 update
apt-get -o Dir::Etc::sourceparts=- install -y python3-venv
这些选项只对本次命令生效,软件源文件无需修改。
3. 完整 Python 示例
将 mpp_preview.py 保存到板端的 ~/cvproj/,或把下方完整代码复制到同名文件。执行 cd ~/cvproj && ./venv/bin/python mpp_preview.py --help,确认脚本和 Python 环境均可用。
#!/usr/bin/env python3
"""Preview local H.264/H.265 MP4 video via Rockchip MPP and OpenCV."""
import argparse
import json
import os
from pathlib import Path
import subprocess
import time
os.environ.setdefault("DISPLAY", ":0")
os.environ.setdefault("QT_XCB_GL_INTEGRATION", "none")
import cv2
import numpy as np
def preview(path):
probe = subprocess.check_output([
"ffprobe", "-v", "error", "-select_streams", "v:0",
"-show_entries", "stream=codec_name,width,height,avg_frame_rate",
"-of", "json", str(path)
])
stream = json.loads(probe)["streams"][0]
parser = {"h264": "h264parse", "hevc": "h265parse"}.get(stream["codec_name"])
if parser is None:
raise ValueError("Expected H.264 or H.265 video in an MP4/MOV container")
w, h = stream["width"], stream["height"]
num, den = map(int, stream["avg_frame_rate"].split("/"))
delay = max(1, round(1000 * den / num)) if num and den else 33
stride = (w + 3) & ~3
cmd = [
"gst-launch-1.0", "-q", "filesrc", f"location={path}", "!",
"qtdemux", "!", parser, "!", "mppvideodec", "!", "videoconvert",
"!", f"video/x-raw,format=GRAY8,width={w},height={h}",
"!", "fdsink", "fd=1", "sync=false"
]
title = "RK3576 MPP - grayscale"
frames = 0
start = time.monotonic()
stopped = False
with subprocess.Popen(cmd, stdout=subprocess.PIPE) as decoder:
try:
while True:
raw = decoder.stdout.read(stride * h)
if not raw:
break
if len(raw) != stride * h:
raise RuntimeError("Incomplete grayscale frame")
gray = np.frombuffer(raw, np.uint8).reshape(h, stride)[:, :w]
cv2.imshow(title, gray)
frames += 1
if cv2.waitKey(delay) & 255 in (27, ord("q")):
stopped = True
break
if not stopped and decoder.wait(timeout=5):
raise RuntimeError("MPP pipeline failed")
finally:
if decoder.poll() is None:
decoder.kill()
cv2.destroyAllWindows()
if not frames:
raise RuntimeError("MPP produced no video frames")
elapsed = time.monotonic() - start
print(f"elapsed={elapsed:.3f}s preview_fps={frames / elapsed:.2f}")
print(f"OpenCV {cv2.__version__} | mppvideodec | {w}x{h} | frames={frames}")
if __name__ == "__main__":
args = argparse.ArgumentParser(description=__doc__)
args.add_argument("video", type=Path, help="Local H.264/H.265 MP4 or MOV path")
path = args.parse_args().video.resolve()
if not path.is_file():
args.error(f"File not found: {path}")
try:
preview(path)
except (OSError, ValueError, IndexError, RuntimeError, subprocess.SubprocessError, cv2.error) as exc:
args.exit(1, f"Preview failed: {exc}\n")
4. 使用 1080p 短片测试
下载 30 秒的全高清样片,从第 12 秒截取 8 秒。已有 H.264 MP4 时,可直接把本地路径传给脚本。按 q 或 Esc 可提前退出。
cd ~/cvproj
mkdir -p samples
curl -fL https://raw.githubusercontent.com/renesas-rz/media/main/Big_Buck_Bunny/big_buck_bunny_1080p_30fps_30s.mp4 \
-o samples/bbb-source.mp4
ffmpeg -nostdin -ss 12 -i samples/bbb-source.mp4 -t 8 -an \
-c:v libx264 -preset ultrafast -pix_fmt yuv420p samples/bbb-1080p.mp4
DISPLAY=:0 ./venv/bin/python mpp_preview.py samples/bbb-1080p.mp4
echo "TEST_EXIT=$?"
素材为 Blender Foundation 的 Big Buck Bunny(© 2008,CC BY 3.0)。截图使用测试样片的节选,并转换为灰度。
保持窗口打开直到视频播完。使用本节样片时,终端应打印 frames=240 和 TEST_EXIT=0。确认基础预览可用后,按第 7.2 节下载优化脚本,再用第 7.3 节命令验证三种模式。
下载的原视频保存在 ~/cvproj/samples/bbb-source.mp4,截取的 1080p 测试短片保存在 ~/cvproj/samples/bbb-1080p.mp4。在板端桌面终端运行 xdg-open ~/cvproj/samples/bbb-1080p.mp4 可查看彩色短片。灰度、边缘和运动区域效果由示例窗口实时显示,脚本不另存处理后的视频。
5. 图片与视频边缘检测
5.1 静态图片边缘检测
从视频截取一帧,先用高斯滤波减少噪声,再用 Canny 提取边缘。输出图可用于查看物体轮廓和纹理边界。
ffmpeg -nostdin -ss 2 -i samples/bbb-1080p.mp4 -frames:v 1 samples/bbb-frame.jpg
./venv/bin/python - <<'PY'
import cv2
image = cv2.imread("samples/bbb-frame.jpg")
if image is None:
raise RuntimeError("Cannot read the sample frame")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(cv2.GaussianBlur(gray, (5, 5), 0), 60, 160)
if not cv2.imwrite("samples/bbb-edges.png", edges):
raise RuntimeError("Cannot save the edge image")
print("IMAGE_OK", edges.shape, cv2.countNonZero(edges))
PY
截取的原图保存在 ~/cvproj/samples/bbb-frame.jpg,生成的边缘图保存在 ~/cvproj/samples/bbb-edges.png。看到 IMAGE_OK 后,可以用 OpenCV 直接在 HDMI 桌面打开结果。将焦点切到图片窗口后,按任意键关闭。
cd ~/cvproj
DISPLAY=:0 QT_XCB_GL_INTEGRATION=none ./venv/bin/python - <<'PY'
import cv2
image = cv2.imread("samples/bbb-edges.png", cv2.IMREAD_GRAYSCALE)
if image is None:
raise SystemExit("Cannot read samples/bbb-edges.png")
cv2.namedWindow("Canny edges", cv2.WINDOW_NORMAL)
cv2.resizeWindow("Canny edges", 960, 540)
cv2.imshow("Canny edges", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
PY

5.2 视频边缘预览
下方是写入 mpp_preview.py 的 Python 代码片段。若只想运行视频边缘预览,先按第 7.2 节保存 NV12 脚本,然后在终端运行 cd ~/cvproj && ./venv/bin/python mpp_nv12_preview.py samples/bbb-1080p.mp4 --mode edges。
也可以逐帧预览边缘。把完整示例中的 cv2.imshow(title, gray) 替换为下面两行,保持原缩进。
edges = cv2.Canny(cv2.GaussianBlur(gray, (5, 5), 0), 60, 160)
cv2.imshow(title, edges)
5.3 视频运动区域
在 frames = 0 后初始化 previous = None,再用下列代码替换原来的 cv2.imshow(title, gray)。白色表示相邻两帧中亮度变化超过阈值的区域。固定机位更容易观察运动物体,镜头移动时背景也会被标亮。
smooth = cv2.GaussianBlur(gray, (5, 5), 0)
if previous is None:
mask = np.zeros_like(smooth)
else:
diff = cv2.absdiff(smooth, previous)
_, mask = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)
previous = smooth
cv2.imshow(title, mask)
6. 板端实测结果
6.1 板端复测步骤
复测基础灰度预览,请按第 4 章操作。复测 NV12 灰度、边缘和运动区域预览,请按第 7 章操作。
6.2 实测截图与结果
在搭载 CM5-IO 的 ArmSoM CM5(RK3576)上,使用 Debian 12 arm64 XFCE 和 OpenCV 5.0.0 实测。1920×1080 H.264 短片在灰度预览和边缘预览中均处理完 240 帧。图片案例成功保存了 1920×1080 边缘图。下图是从板端 HDMI 桌面正在运行的灰度预览窗口抓取的截图。

素材为 Blender Foundation 的 Big Buck Bunny(© 2008,CC BY 3.0)。截图使用测试样片的节选,并转换为灰度。
OpenCV 5.0.0 | mppvideodec | 1920x1080 | frames=240
TEST_EXIT=0
6.3 预览速率实测
HDMI-1 当前输出模式为 1920×1080、60 Hz,测试短片的输入帧率为 30 fps。下表用提交到 OpenCV 窗口的帧数除以程序耗时,包含解码、图像处理和窗口事件处理。
| 模式 | 帧数 | 耗时 | 平均预览送帧速率 |
|---|---|---|---|
| 灰度预览 | 240 | 20.045 秒 | 11.97 fps |
| 运动区域预览 | 240 | 18.518 秒 | 12.96 fps |
两次测试均正常退出。表中是单次运行结果,不能据此判断两种模式的性能高低。60 Hz 是 HDMI 输出刷新率,程序送帧速率不等于显示器实际呈现的不同画面数。
7. 优化 1080p 预览到接近 30 fps
7.1 定位瓶颈
在 CM5 上通过 COM4 对同一段 1080p30 H.264 短片分段测试。每次只运行一条管线,输入均为 240 帧。分段测试以 fakesink sync=false 结束,不开窗口、不按播放时钟限速。
| 测试路径 | 耗时 | 平均速率 |
|---|---|---|
| MPP 解码 → fakesink | 0.694 秒 | 345.82 fps |
MPP 解码 → videoconvert 转 GRAY8 → fakesink | 19.043 秒 | 12.60 fps |
| 原 GRAY8 管线 → Python(关闭窗口) | 20.076 秒 | 11.95 fps |
| 原 GRAY8 管线 → OpenCV 窗口 | 20.045 秒 | 11.97 fps |
| NV12 亮度平面 → OpenCV 窗口 | 8.018 秒 | 29.93 fps |
主要耗时出现在本次镜像上 videoconvert 转 GRAY8 的路径。只把 waitKey 改成 1 毫秒,原尺寸窗口也只有 12.50 fps。把缩放移到管道前、以 960×540 预览,实测为 14.31 fps。
7.2 直接读取 NV12 亮度平面
保留 MPP 的 NV12 输出,用进程内的 GStreamer appsink 取帧。NV12 的 Y 平面已经提供亮度数据,可以用 NumPy 按实际行步长读取,再交给 OpenCV 显示。板上测到有效宽度为 1920、步长为 1984 字节,直接按 1920 字节重排行会读错位置。脚本还根据视频帧率和单调时钟计算剩余等待时间。
将 mpp_nv12_preview.py 保存到 ~/cvproj/。该版用于 H.264 MP4,复用板端 OpenCV 5.0.0 虚拟环境,以及与其 Python 版本匹配的系统 PyGObject 和 GStreamer introspection 绑定。可先用以下命令检查绑定并运行:
cd ~/cvproj
python3 -c 'import gi; gi.require_version("Gst", "1.0"); gi.require_version("GstVideo", "1.0"); from gi.repository import Gst, GstVideo'
DISPLAY=:0 ./venv/bin/python mpp_nv12_preview.py samples/bbb-1080p.mp4
NV12 luma 1920 1080 stride 1984
frames=240 elapsed=8.018s fps=29.93
优化版保持 1920×1080 预览,完整处理 240 帧,退出码为 0。29.93 fps 是程序向窗口送帧的平均速率,HDMI 输出模式仍是 60 Hz。这个数值也包含播放节奏控制。改动只涉及 Python 和 GStreamer 管线。
7.3 边缘检测与运动区域实测
同一份脚本可用 --mode gray、--mode edges 或 --mode motion 切换预览。边缘模式在 Y 平面上做高斯滤波和 Canny 检测。运动模式对相邻的滤波帧求绝对差,再用阈值 25 生成运动掩膜。三种模式共用 MPP 硬解和按源视频帧率控制的预览循环。
下载本节的更新版脚本后,在 CM5 的 HDMI 桌面运行以下命令。测试使用第 4 节生成的同一段 1080p30 短片。
cd ~/cvproj
for mode in gray edges motion; do
DISPLAY=:0 ./venv/bin/python mpp_nv12_preview.py \
samples/bbb-1080p.mp4 --mode "$mode"
done
| 模式 | 原 GRAY8 管线 | NV12 管线 | 帧数 |
|---|---|---|---|
| 灰度预览 | 11.97 fps | 29.94 fps | 240 |
| 边缘检测 | 13.29 fps | 29.93 fps | 240 |
| 运动区域 | 12.59 fps | 29.94 fps | 240 |
三种 NV12 模式均正常退出。边缘检测和运动区域模式的耗时均为 8.017 秒(保留三位小数)。
表中是向 OpenCV 窗口提交的平均速率。NV12 版本按输入的 30 fps 节奏运行,因此这些数字反映预览表现,不能用于推算算法的最大吞吐量。灰度的 GRAY8 数据来自第 7.1 节。
7.4 CM5 与 AIM7 实测对比
两块板卡均使用同一段 1920×1080、30 fps、240 帧的 H.264 视频和手册下载版脚本。AIM7 在 Ubuntu 22.04.5 GNOME/X11 上也成功生成并打开了 1080p Canny 边缘图。两块板卡的 HDMI 输出模式均为 1080p、60 Hz。
| 预览模式 | CM5 · Debian 12 XFCE | AIM7 · Ubuntu 22.04 GNOME/X11 |
|---|---|---|
| 基础 GRAY8 灰度 | 11.97 fps | 5.68 fps |
| NV12 灰度 | 29.94 fps | 29.95 fps |
| NV12 边缘检测 | 29.93 fps | 29.95 fps |
| NV12 运动区域 | 29.94 fps | 29.96 fps |
AIM7 的四种预览均处理完 240 帧,退出码为 0。数值为程序向窗口送帧的平均速率。NV12 版本按源视频节奏播放,两块板卡使用的系统和桌面也不同,因此这组数据用于说明各自实测配置下的预览效果。
