C#实现录屏功能时如何对比前后截图检测屏幕画面变动
实现思路
你的核心逻辑方向是对的,但要注意两个优化点:一是帧比对全程在内存中完成,不要先存盘再读盘比对,平白增加IO开销;二是不要直接比对全尺寸原始帧,高分辨率下逐像素比对的CPU占用会很高,完全没必要。
实际生产环境用的屏幕变动检测通用方案是缩略图灰度比对法,性能开销极低,检测灵敏度完全满足录屏需求:
- 每次截到全屏帧后,先将其缩放到极小尺寸(常用64*36,总像素仅2304个),转成单通道灰度图
- 维护一个上一帧的灰度缩略图缓存,每次计算当前帧和上一帧的像素差异占比
- 预设一个差异阈值(推荐0.2%~1%,可根据需要的灵敏度调整),只有差异占比超过阈值时,才执行存盘、写入帧序列的操作,同时更新上一帧缓存;否则直接丢弃当前帧,不做任何IO操作
- 第一帧没有历史缓存时直接保存即可
注意避坑
- 取位图像素不要用
GetPixel/SetPixel方法,这俩是GDI+的包装方法,性能极差,要通过LockBits锁定位图内存直接读取,速度能提升几十上百倍;如果项目不允许开启unsafe编译,用Marshal.Copy拷贝内存数据读取即可,性能差异很小 - 阈值不要设为0,否则任务栏时间跳动、光标闪烁、托盘图标微小动画都会触发帧保存,会产生大量无意义的冗余帧
- 原代码中
bitmap已经包裹在using块中,块结束时会自动释放资源,不需要手动调用Dispose() - 路径拼接建议用
Path.Combine,自动适配不同系统的路径分隔符,避免硬编码//导致的路径异常
改造后的代码
首先添加类级别常量和缓存字段:
// 缩略图尺寸,越小比对速度越快,不要小于32*18 private const int ThumbWidth = 64; private const int ThumbHeight = 36; // 差异阈值,0.005代表0.5%的像素存在差异时判定为画面变动 private const double DifferenceThreshold = 0.005; // 灰度差判定阈值,过滤缩放插值产生的微小噪点 private const int GrayDiffThreshold = 20; // 上一帧的灰度缩略图缓存 private byte[] _lastFrameGrayThumb = null;
添加两个辅助方法:
// 将位图转为灰度缩略图字节数组(unsafe版本,性能最优) private unsafe byte[] GetGrayThumbnail(Bitmap sourceBmp) { using (Bitmap thumbBmp = new Bitmap(ThumbWidth, ThumbHeight, PixelFormat.Format8bppIndexed)) { // 先把原图缩放到缩略图尺寸 using (Graphics g = Graphics.FromImage(thumbBmp)) { g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Bilinear; g.DrawImage(sourceBmp, 0, 0, ThumbWidth, ThumbHeight); } // 锁内存直接读像素 Rectangle rect = new Rectangle(0, 0, ThumbWidth, ThumbHeight); BitmapData bmpData = thumbBmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed); byte[] grayValues = new byte[ThumbWidth * ThumbHeight]; byte* ptr = (byte*)bmpData.Scan0; for (int i = 0; i < grayValues.Length; i++) { grayValues[i] = ptr[i]; } thumbBmp.UnlockBits(bmpData); return grayValues; } } // 计算两个灰度帧的差异占比 private double GetFrameDifferenceRatio(byte[] currentGray, byte[] lastGray) { if (currentGray.Length != lastGray.Length) return 1.0; int diffPixelCount = 0; for (int i = 0; i < currentGray.Length; i++) { if (Math.Abs(currentGray[i] - lastGray[i]) > GrayDiffThreshold) { diffPixelCount++; } } return (double)diffPixelCount / currentGray.Length; }
改造原有RecordVideo方法:
public void RecordVideo() { watch.Start(); using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height)) { using (Graphics g = Graphics.FromImage(bitmap)) { g.CopyFromScreen(new Point(bounds.Left, bounds.Top), Point.Empty, bounds.Size); } // 先计算当前帧的灰度缩略图 byte[] currentGrayThumb = GetGrayThumbnail(bitmap); bool needSave = false; // 第一帧直接保存 if (_lastFrameGrayThumb == null) { needSave = true; } else { double diffRatio = GetFrameDifferenceRatio(currentGrayThumb, _lastFrameGrayThumb); // 差异超过阈值才保存 if (diffRatio >= DifferenceThreshold) { needSave = true; } } if (needSave) { string name = Path.Combine(tempPath, $"screenshot-{fileCount}.png"); bitmap.Save(name, ImageFormat.Png); inputImageSequence.Add(name); fileCount++; // 更新上一帧缓存 _lastFrameGrayThumb = currentGrayThumb; } } }
可调参数说明
- 如果需要更高的检测灵敏度(比如要捕捉小范围的光标移动、小字变化),可以把
ThumbWidth/ThumbHeight调到128*72,DifferenceThreshold调到0.002(0.2%) - 如果更在意存储空间,只需要捕捉大范围画面变动,可以把
DifferenceThreshold调到0.01(1%),过滤掉更多微小变动 GrayDiffThreshold可以根据实际显示效果调整,值越大越能过滤噪点,但可能漏掉低对比度的画面变动
内容的提问来源于stack exchange,提问作者Kostas G
相关产品推荐
相关产品推荐

