使用OpenCV类库,从视频中截取视频帧
一、用途
用于构建日志报告,报告中中某软件运行的截图,有问题时,日志与视频当时的帧对应。
二、代码实现
using System; using System.Reflection; using OpenCvSharp; using OpenCvSharp.Extensions; namespace PicInVideo { /// <summary> /// 视频帧截取工具类(支持指定时间点截取) /// </summary> public static class VideoFrameCaptureTool { /// <summary> /// 从视频中截取指定时间点的帧 /// </summary> /// <param name="videoPath">视频文件路径(支持FFmpeg录制的MP4/MKV/AVI等格式)</param> /// <param name="outputImagePath">输出图片路径(含文件名,如:D:\frame.jpg)</param> /// <param name="videoStartTime">视频开始的绝对时间(格式:yyyy-MM-dd HH:mm:ss)</param> /// <param name="targetCaptureTime">要截取的绝对时间(格式:yyyy-MM-dd HH:mm:ss)</param> /// <returns>是否截取成功</returns> public static bool CaptureFrameByAbsoluteTime( string videoPath, string outputImagePath, string videoStartTime, string targetCaptureTime) { // 验证输入参数 if (!File.Exists(videoPath)) { throw new Exception($"错误:视频文件不存在 - {videoPath}"); } // 解析时间戳 if (!DateTime.TryParseExact(videoStartTime, "yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime startDt)) { //Console.WriteLine($"错误:视频开始时间格式无效(应为 yyyy-MM-dd HH:mm:ss) - {videoStartTime}"); return false; } if (!DateTime.TryParseExact(targetCaptureTime, "yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime targetDt)) { //Console.WriteLine($"错误:目标截取时间格式无效(应为 yyyy-MM-dd HH:mm:ss) - {targetCaptureTime}"); return false; } // 计算目标时间相对于视频开始的偏移秒数 TimeSpan timeOffset = targetDt - startDt; if (timeOffset.TotalSeconds < 0) { //Console.WriteLine("错误:目标截取时间早于视频开始时间"); return false; } // 调用核心截取方法(按相对时间偏移) return CaptureFrameByRelativeTime(videoPath, outputImagePath, timeOffset.TotalSeconds); } /// <summary> /// 从视频中截取指定相对时间点的帧(相对于视频开始) /// </summary> /// <param name="videoPath">视频文件路径</param> /// <param name="outputImagePath">输出图片路径</param> /// <param name="relativeSeconds">相对于视频开始的偏移秒数(如:10.5 表示第10.5秒)</param> /// <returns>是否截取成功</returns> public static bool CaptureFrameByRelativeTime( string videoPath, string outputImagePath, double relativeSeconds) { // 验证参数 if (!File.Exists(videoPath)) { throw new Exception($"错误:视频文件不存在 - {videoPath}"); } if (relativeSeconds < 0) { return false; } // 创建输出目录(如果需要) string outputDir = Path.GetDirectoryName(outputImagePath); if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir)) { Directory.CreateDirectory(outputDir); } // 打开视频并定位到目标帧 using (var capture = new VideoCapture(videoPath)) { if (!capture.IsOpened()) { //Console.WriteLine($"错误:无法打开视频文件 - {videoPath}"); return false; } // 获取视频关键信息 double fps = capture.Fps; int totalFrames = (int)capture.FrameCount; double totalSeconds = totalFrames / fps; // 视频总时长(秒) //Console.WriteLine($"视频信息:帧率={fps:F2} FPS,总帧数={totalFrames},总时长={totalSeconds:F2}秒"); //Console.WriteLine($"目标截取时间:相对视频开始 {relativeSeconds:F2} 秒"); // 检查目标时间是否超出视频时长 if (relativeSeconds > totalSeconds + 0.1) // 允许0.1秒误差 { //Console.WriteLine($"错误:目标时间超出视频时长(视频仅 {totalSeconds:F2} 秒)"); return false; } // 计算目标帧序号(四舍五入取最近帧) int targetFrameIndex = (int)Math.Round(relativeSeconds * fps); targetFrameIndex = Math.Clamp(targetFrameIndex, 0, totalFrames - 1); // 确保不越界 //Console.WriteLine($"定位到帧序号:{targetFrameIndex}"); // 直接跳转到目标帧(高效,无需遍历所有帧) capture.PosFrames = targetFrameIndex; // 读取目标帧 using (Mat frame = new Mat()) { bool readSuccess = capture.Read(frame); if (!readSuccess) { //Console.WriteLine("错误:读取目标帧失败"); return false; } // 保存图片(支持JPG/PNG等格式,根据输出后缀自动识别) bool saveSuccess = Cv2.ImWrite(outputImagePath, frame); if (saveSuccess) { //Console.WriteLine($"成功保存帧到:{outputImagePath}"); return true; } else { //Console.WriteLine($"错误:保存图片失败 - {outputImagePath}"); return false; } } } } /// <summary> /// 将视频前部截取成帧 /// </summary> /// <param name="videoPath"></param> /// <param name="outputDir"></param> /// <param name="interval"></param> /// <param name="startDateTime"></param> public static void ExtractFramesWithTimestamp( string videoPath, string outputDir, int interval = 1, string startDateTime = null) { // 创建输出目录 if (!Directory.Exists(outputDir)) { Directory.CreateDirectory(outputDir); } // 打开视频文件 using (var capture = new VideoCapture(videoPath)) { if (!capture.IsOpened()) { throw new Exception($"无法打开视频文件: {videoPath}"); } // 获取视频属性 double fps = capture.Fps; int totalFrames = (int)capture.FrameCount; //Console.WriteLine($"视频帧率: {fps:F2} FPS,总帧数: {totalFrames}"); // 解析视频开始时间 DateTime? startDt = null; if (!string.IsNullOrEmpty(startDateTime)) { try { startDt = DateTime.ParseExact( startDateTime, "yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture); //Console.WriteLine($"视频开始时间: {startDt}"); } catch (FormatException) { throw new Exception("startDateTime格式错误(应为yyyy-MM-dd HH:mm:ss),将仅使用相对时间"); } } int frameCount = 0; int savedCount = 0; Mat frame = new Mat(); // 循环读取帧 while (true) { bool readSuccess = capture.Read(frame); if (!readSuccess) break; // 视频结束 // 按间隔截取帧 if (frameCount % interval == 0) { // 计算相对时间(秒) double relativeTime = frameCount / fps; // 计算绝对时间 string absoluteTimeStr = ""; if (startDt.HasValue) { DateTime absoluteTime = startDt.Value.AddSeconds(relativeTime); absoluteTimeStr = absoluteTime.ToString("yyyyMMdd_HHmmss_fff"); } // 生成文件名 string fileName; if (!string.IsNullOrEmpty(absoluteTimeStr)) { fileName = $"{absoluteTimeStr}_frame_{frameCount:D6}.jpg"; } else { fileName = $"t_{relativeTime:F3}_frame_{frameCount:D6}.jpg"; } // 保存图片 string savePath = Path.Combine(outputDir, fileName); Cv2.ImWrite(savePath, frame); savedCount++; //Console.WriteLine($"保存: {fileName} | 相对时间: {relativeTime:F3}秒"); } frameCount++; } frame.Release(); } } } internal class Program { static void Main(string[] args) { /* // 配置参数----从视频中每隔n帧截取帧 string videoPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "test.mp4");// 视频文件路径 string outputDir = "frames_with_timestamp"; // 输出目录 int interval = 30; // 截取间隔(每30帧取一次) string videoStartTime = "2025-11-05 13:35:00"; // 视频开始时间(可选) // 调用截取函数 VideoFrameCaptureTool.ExtractFramesWithTimestamp( videoPath, outputDir, interval, videoStartTime); */ // 配置参数 string videoPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "test.mp4"); // FFmpeg录制的视频路径 string outputImagePath = "captured_frame.jpg"; // 输出图片路径 string videoStartTime = "2025-05-20 14:30:00"; // 视频开始的绝对时间 string targetCaptureTime = "2025-05-20 14:30:15";// 要截取的绝对时间(视频开始后15秒) // 方式1:按绝对时间截取(推荐,与外部时间戳对应) bool result1 = VideoFrameCaptureTool.CaptureFrameByAbsoluteTime( videoPath, outputImagePath, videoStartTime, targetCaptureTime); } } }
依赖类库
OpenCvSharp4
OpenCvSharp4.Extensions
OpenCvSharp4.Windows
浙公网安备 33010602011771号