
简介这是一份EVM欧拉视频放大率的Python实现代码包面向计算机视觉、医学图像分析及运动检测方向的研究者与开发者用于揭示视频中肉眼难以察觉的时间变化例如面部血流引起的细微颜色波动、肢体微小运动等。实现时先对视频帧做空间分解再在时间维度滤波将目标频段差异信号放大并叠加到原画面使隐藏信息直观显现算法设计上具备实时处理能力。资源共16个文件压缩包约139.1MB除Python核心算法脚本外还提供婴儿面部血流、吉他弹奏等多个avi/mp4演示视频以及XML工程配置文件、JPG示意图和说明文档便于对照效果、理解流程并开展二次开发。目前已有2085人学习下载适合希望复现经典EVM实验、深入掌握时空域视频处理技术或将其应用到体征检测、微振动测量等实时场景的人群。 我第一次在笔记本上跑通PyEVMEulerian Video Magnification欧拉视频放大的时候素材是一段对着手机摄像头拍的脸部静止画面——视频里的人没有任何表情但经过放大处理后脸颊肤色像潮水一样有节奏地“泛红”。整个过程只用了不到一百行Python代码那一刻的感受是原来肉眼看不到的东西是真真切切存在的数据信号。PyEVM解决的核心问题很简单肉眼看不出的微小颜色变化和微小运动能不能通过纯软件算法放大到肉眼可辨的程度答案是可以的。这套算法最早由学术界在2012年前后提出中文社区经常翻译成“欧拉视频放大率”英文全称是Eulerian Video Magnification。它适用的场景包括心率检测、新生儿呼吸监护、微小振动分析、微表情研究等。这篇文章我就按自己的踩坑经历把原理、环境搭建、代码实现、参数调优一路讲清楚希望能帮你少走点弯路。1. 静止画面里的隐藏信号EVM到底在放大什么1.1 人眼的分辨极限和视频里的“隐形信号”先想一个问题一段帧率为30fps的视频画面中某个区域每秒钟出现几次灰度值1到2的波动你能看出来吗答案是几乎不可能。普通数字视频用8位量化每个通道只有0到255共256个等级灰度值变化1到2对应颜色差异大约是0.4%到0.8%。对视觉系统来说这种变化被直接忽略了。但忽略不等于不存在。心脏每跳动一次泵出血液使面部血管充盈度变化皮肤反射率会有周期性的微小波动。头部肌肉的极微小抖动、呼吸引起的胸廓起伏、机械设备表面的震动都会在视频像素的时间序列里留下痕迹。EVM做的事情就是把这些处于“量化噪声之下的微弱信号”提取出来再人为放大到一个肉眼可见的水平。这里要注意一个关键点它不是在放大视频分辨率也不是在增强清晰度它放大的是时间维度上被隐藏的变化。空间上画面越平缓越好时间上信号越规律越容易被放大出来。1.2 拉格朗日视角和欧拉视角的思考切换理解EVM绕不开两个名词拉格朗日视角和欧拉视角。传统的运动分析大多采用拉格朗日视角也就是追踪物体上的特征点在哪里。光流法、特征点匹配、目标跟踪都是这个思路。它的前提是“有特征可追踪”。但皮肤颜色变化这个信号没有边缘、没有角点、没有可追踪的纹理它就是一团像素亮度在上下浮动你根本没办法锁定某个特征点说“它动了”。欧拉视角则完全换了一个思路我不追踪任何物体只盯着视频画面中每个固定位置的像素看它们在时间轴上怎么变化。打个比方拉格朗日视角是跟着一片叶子在河里漂流观察叶子怎么运动欧拉视角是站在岸边不动观察河面上某个固定位置的水面波高怎么波动。对于血液充盈这类“原地波动型”信号欧拉视角是天然匹配的。EVM这个名字里的“欧拉”恰恰来自这个观察方式的切换。很多初学者第一眼看到算法框图会觉得复杂一旦看懂了这一层思维方式后面的金字塔、滤波、放大其实都是常规图像处理手段的组合。1.3 EVM能落地的几个典型场景心率检测通过放大面部肤色随心跳的周期性变化再用信号处理算出每分钟心跳次数新生儿/婴儿呼吸监护不需要接触皮肤用普通摄像头就能捕捉胸腹部起伏工业设备振动分析观察电机、管道表面肉眼不可见的震颤频率微表情研究放大面部肌肉的微小位移视觉麦克风通过纱窗等物体上极细微的振动还原声音信号我实际跑过的案例里心率检测的效果最直观也最适合用来验证PyEVM是否调通了。后文我会给出完整的实测流程。2. 原理先行的算法管线空间金字塔、时域滤波与欧拉放大2.1 算法整体三步走PyEVM的整体管线可以拆成三个环节空间分解把视频每一帧分解成不同空间尺度的频带常用高斯金字塔或拉普拉斯金字塔。时域滤波对每个空间频带、每个像素位置上的时间序列做带通滤波只保留目标频率范围比如心率的0.8到2.0Hz。放大重建把滤波后得到的“变化量”乘以放大系数加回到原始视频帧中输出新的视频。如果你只用一句话向别人介绍EVM可以说“把视频当成一个三维时空信号先按空间尺度分层再按时间频率筛选最后把选中的微小波动放大。”画成流程图也就这四个框视频输入 → 金字塔分解 → 时域带通滤波 → 放大叠加 → 输出视频。不要被论文里的复杂公式吓到核心思路就是这么简单。2.2 为什么必须先做金字塔分解很多人第一次实验时会想我直接在原始像素上做时间滤波再放大不行吗我最初也是这么干的结果是画面上全是块状噪点完全没法看。原因有三个第一微小运动信号的幅度在不同空间尺度上表现不同。一个只有0.1像素的位移在高分辨率原始图像上几乎体现不出来但下采样到很小尺寸后相对位移比例变大信号幅度更容易被分离和放大。这就是金字塔的“粗尺度优势”。第二直接对原始像素做时间滤波再放大会放大高频空间噪声。金字塔将图像按空间频率分开我们可以只放大中间那几个频率带而放弃极高频的噪声层这样视觉效果干净很多。第三运动放大本质上需要借用空间梯度信息。在拉普拉斯金字塔上处理滤波后的差值信号天然带有边缘和轮廓结构重建出来的放大效果才符合“物体真的动了”的视觉预期。单纯把像素值放大得到的是对比度异常不是运动感。2.3 时间滤波器的选择和频率设置空间上分完层之后每个像素位置拿到了一条时间序列。比如一段10秒、30fps的视频序列长度是300个采样点。对这个序列做带通滤波就是EVM时域滤波的核心操作。滤波器我推荐用Butterworth带通滤波用scipy.signal.butter设计再用filtfilt做零相位滤波。相比普通IIR滤波filtfilt会让波形不产生相位偏移这对最终放大出来的信号形状至关重要。滤波器阶数选择4阶比较均衡阶数太高容易出现数值不稳定太低则频率选择性不足。频率范围怎么定取决于你想放大什么信号目标信号频率范围说明人脸心率0.8 ~ 2.0 Hz对应48 ~ 120 bpm覆盖绝大多数成人静息心率婴儿呼吸0.2 ~ 1.0 Hz呼吸频率较快需根据实际调整机械振动5 ~ 30 Hz取决于设备类型和工况微表情动作0.1 ~ 1.5 Hz表情变化属于低频运动有一个很实用的技巧如果不知道目标信号频率可以先对ROI区域的时间序列做一次FFT看看功率谱峰值出现在哪个频率附近再据此设置带通上下界。不要盲目的把范围设得过宽范围越宽放大的无关噪声就越多。2.4 放大叠加的数学直觉颜色放大和运动放大在数学本质上可以统一起来。设原始视频信号为I(x, t) ≈ f(x δ(t))其中δ(t)表示某时刻的微小位移。对时间做一阶泰勒展开得到I(x, t) ≈ f(x) δ(t) * ∂f/∂x也就是“当前时刻信号”约等于“静态基础信号”加上“运动位移乘以空间梯度”。时间带通滤波可以把第一项静态部分滤掉留下第二项变化部分再乘上放大系数α之后加回去I(x, t) I(x, t) α * B(x, t)这里B(x, t)就是带通滤波后的变化量。α就是放大倍数推荐值通常在20到100之间。α不是越大越好后面调参部分我会详细说。3. 跑通PyEVM的环境准备依赖、安装与最容易翻车的地方3.1 依赖清单和用途PyEVM的核心依赖不多我推荐直接用Python 3.8以上的版本配合以下四个库就够了库用途说明OpenCV视频读写、金字塔、图像缩放安装opencv-python即可NumPy数组运算、FFT几乎所有步骤都依赖它SciPyButtterworth滤波器核心是signal模块Matplotlib画心率信号的频谱图用于调试和分析有人会用scikit-video或imageio读视频我的经验是多引入一个库就多一个坑OpenCV的VideoCapture虽然简陋但最稳定。最终输出视频用OpenCV的VideoWriter写编解码器用mp4v兼容性最好。3.2 安装命令和虚拟环境推荐先建一个虚拟环境避免污染系统Python也避免不同项目之间依赖版本冲突python -m venv evm_env # Windows激活 evm_env\Scripts\activate # Linux/macOS激活 source evm_env/bin/activate然后安装依赖pip install numpy opencv-python scipy matplotlib这里插一句题外话我在群里看过不少人卡在“python环境变量”和“装numpy报错”这类问题上。如果你电脑上装了多个Python版本建议在命令里指定版本号比如py -3.11 -m venv evm_env确保虚拟环境用的是你想要的那个解释器。如果你用的是Visual Studio Code记得在左下角选择解释器时选中你创建好的evm_env否则代码能跑但import报错很容易让人误以为是numpy没装好。3.3 测试视频的准备建议代码跑通前先别急着拿真实摄像头拍的主观视频来测试。EVM对视频质量要求比普通图像处理高得多测试素材需要满足几个条件帧率稳定最好30fps及以上相机固定画面没有全局运动拍摄对象有足够亮度和均匀光照编码压缩率尽量低码率尽量高手机默认的H.264编码会严重破坏微小的颜色信号我遇到过好几次找半天找不到心跳频率最后发现是视频压缩把信号抹掉了。建议先用电脑摄像头录制一段不压缩的avi测试或者用手机拍摄时选择较高码率的专业模式。分辨率不需要高640x480或者720p足够分辨率太高只会拖慢处理速度。4. 核心代码实现从金字塔到放大重建的完整流程4.1 金字塔构建与重建先写金字塔部分。我这里以拉普拉斯金字塔为例它是后续放大的基础import cv2 import numpy as np from scipy import signal def gaussian_pyramid(img, levels): gp [img] tmp img.copy() for _ in range(levels - 1): tmp cv2.pyrDown(tmp) gp.append(tmp) return gp def laplacian_pyramid(img, levels): gp gaussian_pyramid(img, levels) lp [] for i in range(levels - 1): expanded cv2.pyrUp(gp[i 1]) h, w gp[i].shape[:2] expanded cv2.resize(expanded, (w, h)) lp.append(gp[i] - expanded) lp.append(gp[-1]) return lp def reconstruct_laplacian(lp): img lp[-1] for i in range(len(lp) - 2, -1, -1): img cv2.pyrUp(img) h, w lp[i].shape[:2] img cv2.resize(img, (w, h)) img img lp[i] return img这里有个容易踩坑的细节cv2.pyrUp出来的尺寸不一定和上一层严格一致尤其是奇数尺寸的视频帧。所以我在每一次上采样后都加了cv2.resize把尺寸强制对齐。少了这一步减法运算时维度会报错。4.2 时域带通滤波滤波函数直接封装成一个小工具def butter_bandpass(data, low_freq, high_freq, fps, order4): nyq 0.5 * fps low low_freq / nyq high high_freq / nyq b, a signal.butter(order, [low, high], btypeband) return signal.filtfilt(b, a, data, axis0)这里的axis0非常关键。我们的金字塔数据形状是(帧数T, 高度H, 宽度W)每一层每一帧都是一个二维数组。把时间维放在第一个轴上filtfilt就能沿着帧方向对每个像素位置的时间序列做滤波相当于一次性处理了H*W条时间序列。这种批量处理的写法比循环每个像素快几个数量级。4.3 主处理循环准备好金字塔和滤波函数之后主流程就清晰了def evm_process(video_path, out_path, levels4, alpha40, low_freq0.8, high_freq2.0): cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) if fps 0: fps 30.0 frames [] while True: ok, frame cap.read() if not ok: break frame cv2.cvtColor(frame, cv2.COLOR_BGR2YUV) frames.append(frame.astype(np.float32) / 255.0) cap.release() data np.stack(frames) # 形状: T, H, W, C T, H, W, C data.shape all_pyramids [] for c in range(C): channel data[:, :, :, c] level_data [] for l in range(levels): level_data.append(np.zeros((T, H // (2 ** l), W // (2 ** l)), dtypenp.float32)) for t in range(T): lp laplacian_pyramid(channel[t], levels) for l in range(levels): h_l, w_l lp[l].shape[:2] level_data[l][t, :h_l, :w_l] lp[l] filtered [] for l in range(levels): filtered.append(butter_bandpass(level_data[l], low_freq, high_freq, fps)) all_pyramids.append(filtered) writer cv2.VideoWriter(out_path, cv2.VideoWriter_fourcc(*mp4v), fps, (W, H)) for t in range(T): channel_list [] for c in range(C): lp_reconstructed [] for l in range(levels): h_l H // (2 ** l) w_l W // (2 ** l) lp_reconstructed.append( all_pyramids[c][l][t, :h_l, :w_l] * alpha ) small reconstruct_laplacian(lp_reconstructed) channel_list.append(small) # 把放大的变化量叠加回原始帧 result data[t].copy() for c in range(C): h_l H // (2 ** (levels - 1)) w_l W // (2 ** (levels - 1)) resized cv2.resize(channel_list[c], (W, H)) result[:, :, c] np.clip(result[:, :, c] alpha * resized, 0.0, 1.0) frame_bgr cv2.cvtColor((result * 255.0).astype(np.uint8), cv2.COLOR_YUV2BGR) writer.write(frame_bgr) writer.release()这个实现是一个偏教学向的版本每一步都对应前面讲的原理读起来很直观。如果追求效率可以把金字塔时间序列改为滑窗批次处理避免一次性把所有帧都放进内存。但作为跑通原理的Demo这个版本完全够用。5. 实测参数调优与踩坑记录放大、层级与频率边界的平衡5.1 三个关键参数的调参经验PyEVM跑通只是第一步真正决定效果的是参数。我整理了一张参数影响表都是实测得来的经验值参数推荐范围太小会怎样太大对结果的影响金字塔层数levels3 ~ 5空间分频不充分噪声被放大底层尺寸太小边缘细节全丢放大倍数alpha颜色20~50运动50~100变化不明显颜色过曝、运动失真、四周出现黑色晕边带通频率范围见前文场景表信号被滤掉一半幅度小混入大量噪声画面出现随机闪烁我一般优先固定levels 4先把alpha从20开始往上加每次加10肉眼观察输出视频直到效果满意为止。alpha调完之后再微调频率范围用FFT辅助确定信号峰这样三个参数一次就能定下来。5.2 最容易翻车的三类画面问题问题一画面出现“果冻状”晕影。这通常是alpha过大导致的。放大后的变化量在空间上有强烈的明暗过渡叠加回原图时形成类似日晕的边界。解决办法是降低alpha或者把金字塔最底层最大尺度层的放大系数减半因为大尺度层变化幅度最大最容易过冲。问题二放大后全是彩色噪点看不到目标信号。这多半是频率范围设置不对。我曾有一次把心率检测的带通设成0.5到5.0Hz结果放大了大量肌电噪声和相机自动增益的晃动面部看起来像雪花屏。先用FFT定位信号峰再把带通上下界收敛到0.5Hz以内效果立刻干净。问题三运动放大后画面有水平条纹。这要分两种情况一种是相机滚动快门带来的果冻效应处理办法是避免用卷帘快门拍摄高频率振动目标另一种是金字塔重建时尺寸没对齐导致每一帧的叠加位置出现半像素错位。我在4.1节里强调的resize对齐就是专门防这个的。5.3 性能优化和内存踩坑我用一个1080p、10秒的视频跑实验原始写法差点把16G内存吃满。原因是最上面代码里三个通道、四层金字塔、每个层都保存了全部帧内存占用约等于T * H * W * C * levels * 4字节粗略算一下300帧x1080x1920x3x4层x4字节大约是27GB这肯定扛不住。所以实际工程中我做了两个调整把所有帧缩放成长边640以后再处理信息量足够内存直接降到原来的1/9。分块处理内存比如每次读入10秒的视频切片处理完再写下一段。性能优化思路总结成一句话空间分辨率是性能性价比最高的调节杠杆降分辨率比降帧率对结果影响小得多。6. 落地场景与扩展心率检测实测和后续可以做的事6.1 用PyEVM做心率检测的完整流程我实际跑过不止一次心率检测把完整步骤列在这里供你复现录制一段30秒以上的面部视频让被测者尽量静止面部光照均匀避免阳光直射。用dlib或mediapipe检测人脸取脸颊偏下、嘴角附近的ROI区域。这个区域受表情影响小且毛细血管丰富。对ROI视频跑一遍PyEVMlevels4alpha30带通频率0.8~2.0Hz。对放大后的视频取ROI内所有像素的绿色通道平均值得到一条随时间变化的信号曲线。对信号去均值、加窗Hamming窗做FFT在0.8~2.0Hz范围找最大峰值对应的频率。把峰值频率乘以60就得到每分钟心跳次数。为什么取绿色通道因为血红蛋白对绿光的吸收率比红光和蓝光都高血液容积变化时绿色通道的反射率波动最显著。很多现有的心率检测方案甚至不需要EVM直接在原始视频上也能算出微弱信号但用EVM放大之后信号能量更强抗噪声能力更好。6.2 和原始信号直接处理的对比我把同一段视频分别做了“原始视频直接FFT”和“EVM放大后FFT”的对比实验。结果很有意思原始视频的心率峰值很矮和背景噪声只有大约1.5倍的差距偶尔还会被环境光的工频干扰盖过去。EVM放大后的心率峰值是噪声的3到4倍特征明显更容易被自动算法锁定。代价是处理时间变长10秒视频大约需要十几秒的处理时间。这就是EVM在信号处理链路上的价值它不是必需环节但能让下游分析稳定不少。6.3 后续扩展打包成exe和实时化思路文章最后分享两个方向。第一个是打包分发把PyEVM封装成一个小工具用PyInstaller打包成exe给不懂Python的人用。打包命令很简单pip install pyinstaller pyinstaller -F -n PyEVM_tool --add-data opencv_videoio_ffmpeg*.dll;. evm_gui.py注意OpenCV在打包时偶尔会丢FFmpeg插件导致输出的视频文件无法打开。解决办法是把本机cv2安装目录下的FFmpeg DLL手动加到打包参数里或者直接改用--collect-all cv2省心一点。第二个方向是实时化。把视频流切成长度5~10秒的滑窗每隔几帧滑动一次窗口对每个窗口做金字塔和滤波。由于EVM本身计算量大实时化的关键是降低分辨率和金字塔层数。我的经验是320x240分辨率、3层金字塔在普通笔记本上可以实现约10fps的准实时处理用于固定场景监测足够。对我个人来说PyEVM最大的冲击不在于算法本身多复杂而在于它把“看不见的信号”变成了“看得见的视频”。你明明知道这段录像是静止的但处理完之后生命活动的节奏就清清楚楚地浮现在屏幕上。强烈建议你动手跑一遍哪怕只是对着自己的脸拍10秒那种直观的震撼远胜过读十篇论文。最后提醒一句心率检测这类应用目前只适合做教学实验和娱乐参考不要当医疗诊断工具使用。本文还有配套的精品资源点击获取