从零开始用 OpenCV 在视频上实时刷新 FPS,这件事我一开始也觉得挺简单,不就是putText往画面上写几个字嘛。真上手做才发现,里面藏着不少坑:帧率算不准、画面卡顿、内存暴涨、中文显示乱码……一个接一个。翻遍了很多教程,大部分只给个两三行的代码片段,原理讲不明白,出了问题也不知道怎么排查。这次我把自己的完整实践过程整理出来,从环境搭建到实时显示,再到常见坑的排查,基本覆盖了视频处理入门阶段最常用到的知识,希望能让后面入坑的朋友少走些弯路。
核心关键词先交代清楚:FPS 是指每秒传输帧数,也就是视频每秒钟刷新多少帧画面。在视频分析、实时监控、运动检测这些场景里,FPS 是最直观的性能指标。你在屏幕上看到的那个数字,直接反映了当前处理链路能不能跟得上视频源的节奏,所以几乎每个有实时处理需求的项目都会把它显示出来,方便随时评估效果。
如果你正准备学习图像处理、做计算机视觉相关的课程设计或者入门项目,这篇文章应该能帮上忙。我会尽量把每一步的“为什么”也讲清楚,而不是只给一份能跑的代码,因为只有理解了底层逻辑,遇到问题才知道往哪个方向排查。
1. 动手前的准备工作:环境搭建与工具选择
先把最基础的环境准备好。我用的是 Windows 系统做日常调试,生产环境放在某台 Ubuntu 服务器上跑,两边的坑不太一样,等下我会分开讲。
1.1 Python 版本与 OpenCV 安装方式对比
OpenCV 的 Python 绑定有两个主要版本源,很多人一开始会困惑到底装哪个。一个是opencv-python,这是由社区维护的预编译包,包含核心模块和优化后的库,日常开发用它完全够;另一个是opencv-contrib-python,额外包含了 SIFT、SURF 等扩展模块,如果你要做特征匹配相关的实验,直接装这个版本能省去不少编译的麻烦。
安装命令很简单:
pip install opencv-python如果你需要扩展模块,就用:
pip install opencv-contrib-python我建议新手直接装opencv-python,理由有两个。第一,它的包体积更小,安装速度快,不容易出现依赖冲突;第二,我们这次视频处理用到的视频读取、图像绘制、文本显示等功能都在核心模块里,不需要额外的扩展库。
顺手把 numpy 也装上,因为 OpenCV 的图像数据本质上就是一个 numpy 数组,后续做数组运算、图像切片都要用到它。虽然 OpenCV 依赖里会自动带上 numpy,但显式安装可以确保版本足够新,避免有一些旧版本的 api 差异问题。
1.2 验证安装是否成功的小技巧
装完别急着写代码,先做一步快速验证。打开终端,输入 Python,然后执行下面这段代码:
import cv2 print(cv2.__version__)如果你能看到类似4.8.0这样的版本号输出,说明核心库没问题。再验证一下摄像头能不能正常打开:
import cv2 cap = cv2.VideoCapture(0) if not cap.isOpened(): print("camera open failed") else: print("camera open success") cap.release()VideoCapture(0)里的 0 表示默认摄像头,如果电脑同时插了多个摄像头,这个参数可以改成 1、2 等不同索引。这段代码的意义在于,把“环境问题”和“代码问题”提前切分开。后面如果视频显示不出来了,你能更快定位到是摄像头驱动问题,还是自己的处理逻辑写错了。
2. 核心原理解读:为什么 FPS 不能直接算
很多人第一次写 FPS 显示功能,用的方法是把 1 除以每帧处理时间,写出来大概是这样的代码:
fps = 1 / (end_time - start_time)这个思路方向上没问题,但在实际视频流里,这样算出来的数值会剧烈跳动。原因很好理解:视频帧的到达时间本身就不均匀,尤其是网络摄像头和视频文件,每一帧之间的间隔天然存在波动。你要是把这一帧的瞬时耗时当作 FPS 显示出来,画面上的数字就会在 20 到 40 之间疯狂横跳,看起来非常不稳定,也没法反映真实性能。
所以工程实践里的做法是“滑动窗口平均法”。简单说,就是统计过去 1 秒内处理了多少帧,用这个帧数作为当前 FPS 的估计值。你可以把它理解成计算汽车的平均速度:只看某一瞬间的速度表是不准的,要看这一段路程总耗时和总里程算出来的平均速度,才有参考意义。
2.1 基于时间窗口的平均帧率算法
最常用的实现方式是用一组固定的时间间隔,每过 1 秒统计一次帧数。核心逻辑是这样的:
- 记录上一秒的时间戳
- 统计这一秒内成功读取并处理了多少帧
- 下一秒更新 FPS 值
用伪代码表示就是:
start_time = time.time() frame_count = 0 fps = 0 while True: ret, frame = cap.read() if not ret: break frame_count += 1 current_time = time.time() if current_time - start_time >= 1.0: fps = frame_count / (current_time - start_time) frame_count = 0 start_time = current_time # 在画面上显示 fps这个方法的优点是逻辑简单、结果稳定,数字不会频繁跳动。它的代价是 FPS 的更新频率是每秒一次,实时性稍差,但对于大部分视频处理和监控场景来说完全够用了。
2.2 使用 OpenCV 内置的 FPS 工具类
其实 OpenCV 自己提供了一个 FPS 计算工具,在cv2的utils模块里,类名叫FPS。我一开始也不知道有这东西,后来翻了官方文档才发现,这个类封装的正是滑动窗口平均算法,代码封装得比我手写的更严谨。
使用方法非常简洁:
from cv2.utils import FPS fps = FPS().start() while True: ret, frame = cap.read() if not ret: break # 处理帧... fps.update() if cv2.waitKey(1) & 0xFF == ord('q'): break fps.stop() fps_avg = fps.fps()这个FPS类内部会自动记录开始时间、累计帧数,在调用fps()方法时返回平均帧率。它甚至自带elapsedTime()方法可以获取总耗时,用来做性能分析非常方便。
需要注意一点:这个类计算的是“从开始到现在”的全程平均帧率,而不是每秒更新的实时帧率。如果你希望看到的是实时波动情况,就得用前面说的手动窗口法。两者各有适用场景,没有绝对的好坏之分。
我个人的建议是:做性能基准测试时用 OpenCV 的FPS类,做实时显示时用手动窗口法。因为实时显示要的是“当前这一刻”的状态,全程平均值会掩盖掉短时间的性能抖动。
3. 从摄像头读取视频并实时显示 FPS
理论说得再多,不如直接跑一个能用的 Demo。下面我给出完整的代码,并且逐段解释每个关键函数的作用和参数含义。
3.1 完整可运行的代码示例
import time import cv2 def main(): # 打开摄像头,0 表示默认摄像头 cap = cv2.VideoCapture(0) if not cap.isOpened(): print("无法打开摄像头") return # 设置摄像头分辨率,降低分辨率可以提高处理速度 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) # 初始化 FPS 计算变量 start_time = time.time() frame_count = 0 fps = 0 while True: # 读取一帧画面 ret, frame = cap.read() if not ret: print("读取画面失败") break frame_count += 1 # 每隔 1 秒更新一次 FPS 值 current_time = time.time() if current_time - start_time >= 1.0: fps = frame_count / (current_time - start_time) frame_count = 0 start_time = current_time # 在画面的左上角显示 FPS 文本 text = f"FPS: {fps:.2f}" # 参数依次是:图像、文本、坐标、字体、字号、颜色、线宽、线条类型 cv2.putText(frame, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2, cv2.LINE_AA) # 显示画面 cv2.namedWindow("Video", cv2.WINDOW_NORMAL) cv2.imshow("Video", frame) # 按下 q 键退出循环 if cv2.waitKey(1) & 0xFF == ord('q'): break # 释放资源 cap.release() cv2.destroyAllWindows() if __name__ == "__main__": main()把这段代码保存为fps_demo.py,然后运行:
python fps_demo.py不出意外的话,你会看到摄像头画面,左上角有一个绿色的 FPS 数字,按字母 Q 可以退出程序。这个示例涵盖了视频处理的四个基本步骤:读取画面、处理画面、显示画面、释放资源。这四步几乎可以在所有 OpenCV 视频项目里复用。
3.2 关键函数和参数逐个说明
上面代码里cv2.putText()是重头戏,把它的每个参数说清楚,之后你想调整文字大小、颜色、位置都会很方便:
frame:要在哪张图像上绘制文本,直接传这一帧画面就行。text:要显示的内容,可以是纯文本,也可以像我们这样拼接一个变量。(10, 30):文本左下角的坐标,单位是像素。(10, 30)表示离左边 10 像素、离上边 30 像素的位置。想放在其他位置,调整这两个数字就行。cv2.FONT_HERSHEY_SIMPLEX:字体类型。OpenCV 内置了好几种字体,这个是最常用的基础字形,线条清晰,适合显示在画面上。1:字号缩放比例。这个值越大字越大,1.0 在 640x480 的分辨率下看起来比较协调,如果画面分辨率更高,可以适当加大到 1.5 甚至 2。(0, 255, 0):文字颜色,BGR 格式,不是平时听说的 RGB。(0, 255, 0)是纯绿色,(0, 0, 255)是红色,(255, 255, 255)是白色。2:线条粗细,单位是像素。线宽太大文字会看起来很粗,太小又可能看不清,建议范围在 1 到 3 之间。cv2.LINE_AA:线条类型,AA是抗锯齿的意思。加了它会显示更平滑,尤其是小字号文字,差别很明显。
还有一个值得专门提一下的细节,cv2.waitKey(1)这个函数有两个作用。第一个作用是等待 1 毫秒,让画面有机会刷新出来,如果没有这一行,窗口会直接无响应;第二个作用是接收键盘输入,返回值经过位运算& 0xFF处理后,可以跟 ASCII 码比对,用来判断是否按下了某个键。新手经常漏写这一行,结果发现窗口一闪而过或者直接卡死。
4. 从视频文件读取并显示 FPS
摄像头是实时视频源,而视频文件是另一个常见输入源。处理视频文件和处理摄像头,在代码结构上没有本质区别,无非是把VideoCapture(0)换成VideoCapture("video.mp4")。但真正跑起来你会发现,两者的表现差别很大,有必要单独说一下。
4.1 视频文件来源与摄像头输入的差异
视频文件的帧率通常是固定的,比如 30 FPS 的文件就是每秒 30 帧。OpenCV 读取文件时,会尽量按原始帧率逐帧读取。如果你的处理逻辑比较快,实际读取速度就跟原始帧率保持一致;但如果处理逻辑很慢,比如每帧要做耗时几十毫秒的计算,程序就会跟不上播放速度,表现出来就是画面变慢。
摄像头的情况则反过来了。摄像头是实时采集设备,它的输出帧率取决于硬件能力和环境亮度,整体相对稳定,但每帧之间的间隔会有波动。所以针对两种不同视频源,FPS 显示的含义也不同:
| 视频源 | FPS 显示的实际含义 | 决定因素 |
|---|---|---|
| 摄像头 | 实时采集和处理速度 | 硬件性能 + 光照 + 处理耗时 |
| 视频文件 | 程序的解码和处理耗时 | CPU 性能 + 算法复杂度 + 分辨率 |
理解这层区别之后,你就能明白为什么有时候读视频文件显示的 FPS 比原始文件高。我自己就遇到过这个现象,后来一查资料才明白,那是因为 OpenCV 读取文件时并不会主动等待,处理完一帧马上就去读下一帧了,在解码耗时低的情况下,单位时间内处理的帧数可能比文件的原始帧率还高,看起来 FPS 数值就虚高了。
4.2 控制播放速度与 FPS 显示的稳定性
如果希望视频文件按原始速度播放,一个通用的做法是在读取下一帧之前做延时等待。前置知识:视频文件的帧间隔是固定的,如果当前处理耗时比原始帧间隔短,就主动 sleep 一段时间去补上剩余时间。
代码可以这样改:
import time video_fps = cap.get(cv2.CAP_PROP_FPS) frame_interval = 1.0 / video_fps while True: loop_start = time.time() ret, frame = cap.read() if not ret: break process_frame(frame) elapsed = time.time() - loop_start if elapsed < frame_interval: time.sleep(frame_interval - elapsed)这一小段逻辑在真实项目里特别有用。比如你写了一个车辆检测算法,测试时希望看到和真实监控画面一致的播放节奏,没有这个等待逻辑,画面会像加速播放一样快速闪过,根本没法观察效果。
另外一个值得注意的坑是,很多视频文件编码方式不同,有的 OpenCV 默认解码器解不开,表现是cap.read()直接返回False。遇到这种情况,可以先试试用pip install opencv-contrib-python安装扩展包,里面带了解更多解码器,实在不行再用ffmpeg转格式,把视频转成 H.264 编码的 MP4 文件,兼容性最好。
5. 进阶优化与常见问题排查
基础功能跑通之后,下面这些优化和排查经验才是真正提升你效率的地方,也是我在实际项目中反复踩坑总结出来的。
5.1 让 FPS 显示更稳定的三种手段
前面提到手动窗口法会让 FPS 每秒更新一次,但如果你觉得即使在 1 秒窗口内,数值还是偶尔跳一下,可以试试这几个优化手段:
- 多重窗口取平均:维护最近 5 个 1 秒窗口的数据,取平均值展示。这个方法对像瞬间卡顿造成的掉帧非常有效,数字会平滑很多,但是会引入额外延迟,实时性要求极高的场景请慎用。
- 指数移动平均:这个思路是做瞬时帧率,同时让变化不会太突兀。公式是
fps_smooth = alpha * fps_current + (1 - alpha) * fps_smooth,alpha通常取 0.1 到 0.3 之间。它的特点是响应快,又不会像瞬时值那样剧烈抖动。我自己做运动检测的项目时用的是这个方法,效果相当不错。 - 显示取整:单纯把
fps取整显示成整数循环取整,也能减少视觉上的跳动感。
这三种方法各有取舍。简单场景用第 3 种就够了,需要平衡实时性和稳定性的用第 2 种,对稳定性要求很高的用第 1 种。
5.2 常见异常现象与解决思路速查表
把我在实践中常遇到的问题和对应的排查路径整理成一个表格,遇到问题时可以对照着看:
| 现象 | 可能原因 | 排查思路与解决 |
|---|---|---|
| 画面黑屏但不报错 | 摄像头被其他程序占用 | 关掉其他摄像头应用,或者换一个索引重新打开 |
| 画面很卡、FPS 很低 | 分辨率设置太高 | 降低分辨率到 640x480 或 320x240 |
| 打开视频文件时提示读取失败 | 视频编码格式不兼容 | 先转码成 H.264 MP4,或安装扩展包 |
| 画面窗口显示为灰色 | 忘了调用cv2.imshow | 检查循环里有没有imshow这一行 |
| FPS 数值疯狂跳动 | 使用了瞬时帧率计算 | 改成滑动窗口平均法 |
| 程序运行时 CPU 占用 100% | 处理逻辑太重或没有释放资源 | 压缩帧尺寸、减少不必要的计算、确认循环有退出条件 |
waitKey在保存视频时影响帧率 | 缺少正确的延时设置 | 每一个循环都调用waitKey(1)或waitKey(frame_interval) |
| 文字内容显示为乱码 | 使用了中文文本 | OpenCV 自带putText不支持中文,需要额外处理 |
关于中文乱码问题多说几句,这是所有中文开发者绕不开的一个坎。cv2.putText能正常显示英文和数字,但传入中文字符串时通常会显示成问号或乱码。一个可用的替代方案是使用 PIL(Pillow)库的ImageDraw绘制中文,再把结果转回 numpy 数组传给 OpenCV。虽然转换过程多了一步,但方案很稳定,建议需要显示中文标签的时候直接用它。
5.3 资源释放的细节不要忽视
代码写完了,最后一件事是把资源清理干净。运行结束后的cap.release()和cv2.destroyAllWindows()两行不能省。摄像头资源不释放,下一次运行程序时会发现摄像头一直打不开,或者提示被占用,排查起来非常抓狂。我记得有一次调试程序时,改了好几行代码都没找到摄像头打不开的原因,最后发现是上一个异常退出的进程没有释放摄像头资源。血的教训。
如果你经常用 jupyter notebook 调试,注意每次运行完都要手动执行资源释放,不然摄像头会被上一次运行的会话锁住。
6. 延伸实战:把 FPS 信息叠加到录制视频中
到这一步,实时显示 FPS 的核心内容已经完整覆盖了。最后再分享一个我在实际中很常用的延展方案:把处理后的画面连同 FPS 一起保存成新的视频文件。这个功能在做演示、项目答辩、效果汇报时非常有价值,评审人直接看视频就知道你系统的处理性能。
6.1 使用 VideoWriter 保存带 FPS 信息的视频
代码结构也不复杂,在原有逻辑上增加一个VideoWriter对象:
import cv2 cap = cv2.VideoCapture("input.mp4") if not cap.isOpened(): print("视频打开失败") exit() # 获取原始视频的尺寸和帧率,用于保持输出视频的一致性 width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = cap.get(cv2.CAP_PROP_FPS) fourcc = cv2.VideoWriter_fourcc(*'mp4v') out = cv2.VideoWriter("output.mp4", fourcc, fps, (width, height)) while True: ret, frame = cap.read() if not ret: break # 绘制 FPS 信息到画面 cv2.putText(frame, f"FPS: {fps:.1f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) out.write(frame) cv2.imshow("Processing", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() out.release() cv2.destroyAllWindows()这段代码里的关键是cv2.VideoWriter_fourcc(*'mp4v'),fourcc是视频编码格式的标识符,mp4v是 MP4 容器下兼容性较好的编码,基本所有播放器都能打开。如果你需要更小的文件体积,可以试试avc1编码,但部分老播放器不支持解码。
输出视频的帧率设置也值得注意,别想当然地认为写多大都行。如果输出帧率设置得比实际处理帧率高,视频播放时会显示为慢动作;设置低了则相反,视频看起来像快进。最稳妥的做法就是像示例代码里那样,直接从输入视频的参数中读取并复用。
我在一个运动检测项目里就这样做过,把每一帧检测结果、目标框、FPS 都绘制到画面里,然后录制成带标注的视频,后面的结果分析应回归复盘都是基于那段视频,比纸质记录直观太多了,也让报告的交付结果清晰了很多。
到了这里,整个主题就讲透了。动手试一遍,用你的摄像头跑一次实时 FPS 显示,再换一个视频文件练手,感受一下两种视频源的差异,你会对视频处理这一整套流程有更扎实的把握。后续想深入的话,可以尝试在每一帧里叠加目标检测框、画运动轨迹、做背景差分,这些技术在学会了基础显示之后,顺水推舟就都会了。