使用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

posted @ 2025-11-05 15:13  卖雨伞的小男孩  阅读(8)  评论(0)    收藏  举报