
1. 先搞清楚FPS显示这个项目到底让你练什么很多人学OpenCV的第一件事就是跑一个人脸识别demo代码一粘贴框倒是画出来了但细问起来完全不知道每行在干什么。换个视频文件就报错换台电脑摄像头就打不开最后除了截图发个朋友圈实际上什么也没学会遇到新需求还是无从下手。我自己带过不少新人也踩过一模一样的弯路。后来发现拿“在视频画面上实时显示FPS”这种小项目当作OpenCV的第一课效果远比一上来就人脸检测好。这个项目看着小却把OpenCV日常开发最常用的六大核心环节全串起来了视频采集、逐帧读取、时间计算、图像绘制、窗口刷新、按键退出。全部代码不超过三十行但每一步都能拆开讲透而且跑起来特别有成就感——你自己写的程序能实时算一个性能指标并绘制在画面上这种正反馈对新手非常重要。这篇文章就围绕“python-opencv在视频中显示fps”这个主题展开把这个快速入门小项目的原理、代码、坑位全部过一遍。读完你可以直接照抄代码跑起来更关键的是你会明白每一行代码背后的逻辑以后再遇到任何视频处理需求都知道从哪个环节下手。1.1 OpenCV处理视频的基本套路抛开FPS不谈OpenCV处理视频的流程其实非常固定所有视频类项目都逃不出这个骨架用cv2.VideoCapture创建视频对象参数可以填摄像头编号也可以填视频文件路径在一个循环里反复调用cap.read()拿当前帧返回两个值一个是布尔标志ret另一个是图像矩阵frame对frame做你要做的处理画框、画线条、转灰度、做检测都发生在这个位置用cv2.imshow把处理后的画面推向窗口用cv2.waitKey给窗口事件循环一点时间顺便判断有没有按键盘循环结束后释放资源、关闭窗口。这个套路理解以后你跟任何人聊OpenCV视频处理心里都能有个框架。别人说“我要做运动检测”你脑子里立刻浮现read → 处理 → imshow → waitKey这条流水线再复杂的项目也只是在中间那个“处理”环节里加料。FPS显示这个项目恰恰把这条流水线完整走了一遍而且每一步都有肉眼可见的效果。1.2 为什么选FPS显示当入门题目人脸识别、目标跟踪这些看起来炫酷的项目其实把大量复杂度藏在了一个黑盒模型里。新手根本分不清哪些工作由模型完成、哪些由OpenCV完成出了问题也无从排查。而FPS显示这个题目具备几个优势第一它不依赖任何第三方模型纯粹用OpenCV自带函数就能实现天然适合入门。第二它的反馈是即时且可量化的。你改一个参数FPS数字立刻跳动这种直接对应关系能帮你快速建立起“代码改动→程序行为变化”的直觉。第三它的计算逻辑非常清晰又涉及时间戳操作这是所有视频处理项目都绕不开的基本功。第四它自带性能监测属性。以后你做任何视频处理只要在代码里留一个FPS计数就能客观评估你的处理开销是否可控这个习惯做项目时非常受用。所以别小看这个“显示FPS”的小功能。它既是入门起点又是一个可以一直保留在你的工具库里的调试利器。学会它你学的不只是一个函数而是一条完整的视频处理思维链。2. 环境准备与OpenCV安装2.1 虚拟环境、Python版本和安装命令动手写代码前环境要先弄干净。我个人强烈建议所有Python项目都创建独立虚拟环境尤其OpenCV这种带大量二进制依赖的库一旦和系统全局环境里的版本冲突排查起来非常折磨人。python -m venv venvWindows下激活环境venv\Scripts\activatemacOS或Linux下激活环境source venv/bin/activate激活后能看到命令行前缀变化说明环境已生效。接着安装OpenCVpip install opencv-python如果后面要用人脸检测或深度学习模块还需要装opencv-contrib-python——注意这个包和opencv-python是替代关系不要两个都装否则两个包里的同名模块文件会互相覆盖导致cv2导入报错或者某些函数突然找不到了。这是新手最常见的环境坑。pip install opencv-contrib-python至于Python版本OpenCV对版本兼容性做得比较好Python 3.8到3.12之间的主流版本基本都可以直接安装。我这里建议直接用3.10或3.11遇到一些老的教程代码时兼容性更好第三方库的支持也最全面。2.2 验证安装是否正常装好以后先别急着写视频代码先用一行命令验证安装结果import cv2 print(cv2.__version__)如果输出类似4.9.0的版本号说明安装成功。注意这里可能会遇到一个奇怪问题某些环境中import cv2已经成功但输入cv2.__version__却提示找不到版本号。遇到这种情况多半是装了某个不完整的发行包解决方案是干净卸载后重装标准版pip uninstall opencv-python opencv-contrib-python -y pip install opencv-python另外在Linux服务器上偶尔会遇到缺少libgl1报错的情况。这是OpenCV的GUI模块依赖系统图形库所致按对应系统的包管理器安装缺失依赖即可解决。很多人在这一步被劝退其实只是缺一个底层库。3. FPS计算的三种办法3.1 逐帧时间差法最直观但最不稳定计算FPS最朴素的办法就是算相邻两帧之间的时间间隔然后取倒数。假设上一帧是t_prev当前帧是t_now那么这一瞬间的帧率就是fps 1 / (t_now - t_prev)这个逻辑非常容易理解。你可以用time.time()来获取时间戳也可以直接用OpenCV自带的cv2.getTickCount()和cv2.getTickFrequency()。后者的精度更高而且它是OpenCV内部统一的高精度计时器做性能分析时结果更可靠。t_prev cv2.getTickCount() # 在循环里 t_now cv2.getTickCount() fps cv2.getTickFrequency() / (t_now - t_prev) t_prev t_now但这种逐帧计算方式有个明显缺陷每一帧的耗时波动很大。摄像头采集本身有抖动系统调度也有随机性算出来的FPS数字会跳来跳去肉眼看着非常难受。如果你只是自己调试用问题不大但如果要把FPS显示给别人看最好用下面的平滑方法。3.2 帧数计数法稳定平滑的FPS更稳的做法是统计一段时间内的总帧数。思路是设定一个统计窗口比如累积0.5秒的帧数然后用帧数/耗时得到平均帧率。这样即使在单帧上有波动平均值也能比较平滑地反映真实性能。start_time time.time() frame_count 0 fps 0.0 while True: ret, frame cap.read() if not ret: break frame_count 1 elapsed time.time() - start_time if elapsed 0.5: fps frame_count / elapsed frame_count 0 start_time time.time()这里还可以进一步用滑动平均或低通滤波让FPS变化更顺滑。比如维护一个队列存最近N次计算结果每次取平均值显示。这种方式在视觉上非常自然也是实际工程项目里更常见的做法。另外要注意这种“统计式FPS”和你画面实际卡不卡之间存在微妙的滞后画面僵住了FPS数字不会立刻掉下去要等统计窗口结束才更新。而第一种逐帧方式虽然抖动厉害但反映瓶颈的速度更快。严格来说这两种方式用途不同调试性能瓶颈建议用第一种做展示建议用第二种。3.3 视频文件的固有FPS和实时FPS如果你是打开一个视频文件cv2.VideoCapture里有一个非常容易混淆的属性cv2.CAP_PROP_FPS。这里必须说清楚CAP_PROP_FPS返回的是视频文件里记录的“标称帧率”是文件封装元数据里写好的数字比如30.0。它跟你的程序实际能跑多快没有半点关系。说直白点视频文件标称30fps但你每一帧都要做五秒的耗时的图像处理那么你的实时FPS就是0.2只是CAP_PROP_FPS照样返回30。反过来对于摄像头CAP_PROP_FPS返回的常常是驱动的默认值或请求值也不代表真实采集帧率。所以永远不要拿CAP_PROP_FPS去当真实性能指标它只是“文件属性”或“请求参数”不是“运行事实”。nominal_fps cap.get(cv2.CAP_PROP_FPS)如果要按视频的标称帧率来控制播放速度可以用cap.get(cv2.CAP_PROP_POS_MSEC)或者配合time.sleep来控制循环节奏。下面讲代码时我会展示这种区分有多重要。我把三种方法整理成一个表方便对照方法核心思想优点缺点适用场景逐帧时间差相邻帧间隔的倒数反应快、无延迟数值抖动明显性能瓶颈定位帧数计数窗口内帧数/耗时平滑稳定有滞后感显示给用户看CAP_PROP_FPS直接读取文件属性零计算成本不代表实时性能视频文件信息展示4. 完整代码实现与逐行解读4.1 基础版实时显示FPS环境准备好、原理也清楚了直接上代码。下面这个版本我故意保留了最简单的写法方便你把每个环节和前面的流程图对应起来import cv2 import time cap cv2.VideoCapture(0) if not cap.isOpened(): print(无法打开摄像头请检查设备编号和占用情况) exit() prev_time time.time() while True: ret, frame cap.read() if not ret: print(读取视频帧失败可能视频已结束或设备异常) break current_time time.time() fps 1 / (current_time - prev_time) prev_time current_time fps_text fFPS: {fps:.1f} cv2.putText(frame, fps_text, (20, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) cv2.imshow(FPS Demo, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逐行拆开讲几个关键点cv2.VideoCapture(0)里的0是摄像头索引号。电脑只接一个摄像头时通常填0有多个摄像头时可以试1、2、3。也可以直接传视频文件路径比如cv2.VideoCapture(test.mp4)除此之外的代码基本不用改。cap.read()会返回一个元组。ret是布尔值告诉你这次读取是否成功。如果视频播放到最后一帧read()会返回(False, None)所以要加上对ret的判断否则后面的frame是None任何处理都会直接崩溃。cv2.putText是本次项目的另一个核心函数。它的参数分别是图像、文字内容、左下角坐标、字体、字号缩放、颜色、线条粗细。颜色注意OpenCV默认是三通道BGR顺序不是RGB。所以(0, 255, 0)是纯绿色也就是绿色通道拉满、其他通道为0。这个顺序问题是新手必踩的坑你在网上找颜色数值时一定要确认对方给的是RGB还是BGR。cv2.waitKey(1)里的1是毫秒。这个参数经常被人忽略但它非常重要。如果填0窗口会一直阻塞等待按键视频画面会卡死如果填太大比如100视频会明显掉帧。对于摄像头实时流一般填1对于视频文件可以填int(1000 / nominal_fps)来贴近原始播放速度。另外 0xFF是为了从返回值中提取低8位按键码其实在大多数平台上直接比较waitKey(1) ord(q)也能工作但加上 0xFF是更稳妥的跨平台写法。4.2 进阶版平滑FPS和多功能信息面板基础版能跑以后我们可以把它升级成更像样的“信息面板”。这里我会额外显示分辨率、帧计数、耗时这三项同时把FPS改成平滑统计用户体验会好很多。import cv2 import time from collections import deque cap cv2.VideoCapture(0) if not cap.isOpened(): print(摄像头打开失败) exit() fps_window deque(maxlen30) start_time time.time() frame_count 0 display_fps 0.0 while True: ret, frame cap.read() if not ret: break frame_count 1 current_time time.time() if current_time - start_time 0.5: current_fps frame_count / (current_time - start_time) fps_window.append(current_fps) display_fps sum(fps_window) / len(fps_window) frame_count 0 start_time current_time fps_text fFPS: {display_fps:.1f} cv2.putText(frame, fps_text, (20, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) w cap.get(cv2.CAP_PROP_FRAME_WIDTH) h cap.get(cv2.CAP_PROP_FRAME_HEIGHT) res_text fResolution: {int(w)}x{int(h)} cv2.putText(frame, res_text, (20, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 0, 0), 2) frame_id_text fFrame: {frame_count} cv2.putText(frame, frame_id_text, (20, 130), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imshow(Info Panel, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这里用deque(maxlen30)来做滑动窗口最多保存最近30次FPS采样每次都取平均数。这样一来就算某一瞬间系统卡了一下显示的数字也不会剧烈跳动。这种“取平均来平滑指标”的思路在很多场景都能复用比如网络延迟统计、内存占用监控底层逻辑完全一样。我还特意在画面左上角叠了三行信息实时FPS、分辨率、帧号。这个布局本身就是一套很实用的“调试信息模板”。以后你写任何视频程序都可以在这个基础上追加自己的业务数据比如检测框数量、处理耗时、模型置信度画面上的信息面板就是最好的临时日志。4.3 在画面上绘制中文的正确姿势这里必须单独拿出一节来强调cv2.putText不支持中文。你如果直接往里传中文字符串轻则显示成乱码重则直接抛异常。原因是OpenCV内置的cv2.FONT_HERSHEY_*系列字体都是英文和数字的字形根本没有中文字库。如果你确实需要在画面上显示中文一个通用的办法是把文本先用Pillow画到透明图层上再把图层跟OpenCV图像合成from PIL import Image, ImageDraw, ImageFont import numpy as np def draw_chinese_text(frame, text, position, font_size24, color(0, 255, 0)): # 把OpenCV的BGR图像转成Pillow的RGB格式 pil_img Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) overlay Image.new(RGBA, pil_img.size, (0, 0, 0, 0)) draw ImageDraw.Draw(overlay) # 这里换成你自己系统里的中文字体路径 font ImageFont.truetype(../fonts/simhei.ttf, font_size) draw.text(position, text, fontfont, fill(*color, 255)) result Image.alpha_composite(pil_img.convert(RGBA), overlay) return cv2.cvtColor(np.array(result), cv2.COLOR_RGB2BGR)这个方法虽然多了几步转换但本质只是走了一遍“RGB转来转去”的过程性能开销在实时视频里可以接受。如果追求极致性能也可以预先绘制好静态文字图层循环里只做一次图像合成会比每次重新渲染字体快很多。我个人的建议是除非确实有中文显示需求否则排查信息一律用英文干净、省事、跨平台不出问题。5. 实操中容易踩的坑5.1 摄像头打不开或者打开后黑屏最典型的现象是cap.isOpened()返回False。这个时候首先要确认摄像头是否被别的程序占用比如微信视频会议、调试工具等都占着摄像头OpenCV就拿不到设备。把占用程序关掉再试基本能解决一半问题。如果确认没被占用还打不开检查索引号。笔记本自带摄像头一般是0外接USB摄像头可能是1。你可以写个小循环把0到4都试一遍for idx in range(5): cap cv2.VideoCapture(idx) if cap.isOpened(): print(f发现可用摄像头索引: {idx}) break另外在Linux系统上如果用的是普通用户可能会因为设备权限不足导致打不开摄像头。这种情况光靠Python代码解决不了需要把自己的账号加入视频用户组或者做相应设备权限配置。5.2 视频文件播放时快时慢打开视频文件时处理循环跑得飞快一秒钟可能读完几十帧于是视频像在放快进而如果你在循环里加了耗时操作又可能变得非常卡。要按视频本来的节奏播放可以利用CAP_PROP_FPS读取标称帧率然后用waitKey控制帧间隔fps cap.get(cv2.CAP_PROP_FPS) if fps 0: fps 30 frame_delay int(1000 / fps) # 循环里使用 if cv2.waitKey(frame_delay) 0xFF ord(q): break但这个方法在帧处理耗时超过帧间隔时依然会变慢。更准确的做法是用时间戳做目标控制计算当前帧应该展示的时间点和实际时间的差距动态调整等待时长。原理并不复杂但对大多数工具型项目来说用waitKey结合标称FPS已经足够。5.3 窗口不刷新、界面无响应新手最容易困惑的一个问题是明明写了cv2.imshow运行时窗口却是灰色的或者一点都不动。原因几乎永远是忘了写cv2.waitKey(1)。imshow只是把图像数据塞进窗口缓冲窗口实际刷新依赖waitKey驱动的事件循环。没有waitKey整个GUI就停留在“未处理”状态画面上什么都看不到。反过来如果waitKey的参数过大画面倒不会灰了但会变得一顿一顿。另外还有一种少见的情况在Jupyter Notebook等交互式环境里跑这类循环imshow表现会异常因为Notebook的事件循环和OpenCV的窗口循环冲突。遇到这种问题把代码保存成.py文件用终端跑通常一切正常。5.4 画面上的FPS波动异常如果你发现FPS低到离谱比如个位数先别急着怀疑代码写得烂。检查一下是不是后台开了太多程序抢CPU或者运行的是虚拟机里的摄像头直通。还有一点容易被忽略如果frame是4000x3000分辨率的大图光是转灰度、缩放、绘制这些操作就会吃掉不少性能。你可以先用cv2.resize把帧缩小到640x480再处理流畅度会立刻上来frame cv2.resize(frame, (640, 480))这个优化思路极其常见。很多实时项目根本不需要处理全分辨率画面把图像缩到合适大小性能可以有几倍的提升。6. 往后的扩展玩法6.1 从显示到处理叠加Canny边缘检测FPS显示跑通后你其实已经具备了所有视频处理所需的组织能力。做一个简单的扩展在每一帧上运行cv2.Canny边缘检测再把检测结果叠加到画面上的另一个小窗口同时观测处理后的FPS变化。这一步能让你直观感受到“算法复杂度与帧率”的权衡关系。gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) edges cv2.Canny(gray, 100, 200) cv2.imshow(Edges, edges)你会发现加上这一层计算后FPS明显下降。这时你可以试着调整Canny的两个阈值、改变图像尺寸观察FPS怎么跟着变化。这种“亲手调参、亲手看指标”的体验比看任何教程都更能帮你建立性能敏感度。我在实际做项目时也经常用这种办法快速验证一个算法是否适合放在实时链路里。6.2 用Trackbar做实时参数调节如果还想更进一步可以用cv2.createTrackbar创建一个滑动条在运行时动态调节Canny的低阈值而不用每次改代码重启程序。这看起来只多了一个滑条但它意味着你理解了“GUI事件回调与图像处理循环如何共存”。很多人做图像处理时习惯了“改参数、重启、看结果”的低效循环而Trackbar能让你在同一个画面里实时试参数效率完全不一样。再往后你可以把这个信息面板扩展成更完整的调试工具叠加处理耗时直方图、记录一段时间内的FPS曲线、把平均帧率写入日志文件甚至在画面实时显示当前正在处理的算法名称和参数。这些能力都是从“在视频中显示FPS”这个小项目里长出来的。我自己做视频处理项目时第一件事永远是先在画面角落放一个FPS数字。没有这个数字所有优化都是凭感觉有了它哪一行代码拖慢了性能、哪个分辨率性价比最高全都一目了然。所以别看这个入门小项目不起眼它其实是你以后所有OpenCV实战项目的一块重要基石。把今天这些代码亲手敲一遍把每个参数都改一改、测一测你对视频处理这条流水线的理解会比看十篇教程都扎实。