• C# 佳能相机SDK对接,采集并保存视频,使用WriteableBitmap高效渲染


      佳能数码单反相机是众多相机SDK里面最难对接的一个,应该说数码相机要比普通工业相机难对接,因为工业相机仅仅只是采集图像,而数码单反相机SDK意味着操作一部相机,有时我们需要像普通相机一样使用数码单反相机,本文就是实现这样的需求,需要实现的功能包括:

      1、打开和关闭相机

      2、实时显示图像

      3、拍照和录像

      由于佳能相机拍照和录像的特殊性(通过回调的方式),因此我们定义的相机功能接口如下(适合大部分相机):

    /// <summary>
    /// 相机接口
    /// </summary>
    public interface ICamera : IDisposable {
        /// <summary>
        /// 初始化
        /// </summary>
        /// <returns></returns>
        Boolean Init (out String errMsg);
        /// <summary>
        /// 开始运行
        /// </summary>
        /// <returns></returns>
        Boolean Play (out String errMsg);
        /// <summary>
        /// 停止运行
        /// </summary>
        /// <returns></returns>
        Boolean Stop (out String errMsg);
        /// <summary>
        /// 开始录像
        /// </summary>
        /// <returns></returns>
        Boolean BeginRecord (out String errMsg);
        /// <summary>
        /// 停止录像
        /// </summary>
        /// <returns></returns>
        Boolean EndRecord (out String errMsg);
        /// <summary>
        /// 拍照
        /// </summary>
        /// <returns></returns>
        Boolean TakePicture (out String errMsg);
        /// <summary>
        /// 图像源改变事件回调通知
        /// </summary>
        Action<ImageSource> ImageSourceChanged { get; set; }
        /// <summary>
        /// 相机名称
        /// </summary>
        String CameraName { get; }
        /// <summary>
        /// 新照片回调通知
        /// </summary>
        Action<String> NewImage { get; set; }
        /// <summary>
        /// 新录像回调通知
        /// </summary>
        Action<String> NewVideo { get; set; }
        /// <summary>
        /// 储存图像文件夹
        /// </summary>
        String ImageFolder { get; set; }
        /// <summary>
        /// 储存录像文件夹
        /// </summary>
        String VideoFolder { get; set; }
        /// <summary>
        /// 命名规则
        /// </summary>
        Func<String> NamingRulesFunc { get; set; }
    }
    View Code

      创建相机对象时,类似于这样:

    var camera = new Camera {
        ImageSourceChanged = n => { this.img.Source = n; }, // 更新图像源
        ImageFolder = Path.Combine (Environment.CurrentDirectory, "Images"), // 图像保存路径
        VideoFolder = Path.Combine (Environment.CurrentDirectory, "Videos"), // 录像保存路径
        NamingRulesFunc = () => (DateTime.Now - new DateTime (1970, 1, 1)).TotalMilliseconds.ToString ("0") // 新文件命名方式
    };

      相机的实现类比较长,代码已上传至Github:https://github.com/LowPlayer/CanonCamera;源码里面有官方SDK文档和Demo,强烈建议看完第六章的示例,因为Demo封装得太多,不易看懂;

      相机的连接:

    public Boolean Init (out String errMsg) {
        errMsg = null;
    
        lock (sdkLock) {
            var err = InitCamera (); // 初始化相机
            var ret = err == EDSDK.EDS_ERR_OK;
    
            if (!ret) {
                errMsg = "未检测到相机,错误代码:" + err;
                Close (); // 关闭相机
            }
    
            return ret;
        }
    }
    
    private UInt32 InitCamera () {
        var err = EDSDK.EDS_ERR_OK;
    
        if (!isSDKLoaded) {
            err = EDSDK.EdsInitializeSDK (); // 初始化SDK
    
            if (err != EDSDK.EDS_ERR_OK)
                return err;
    
            isSDKLoaded = true;
        }
    
        err = GetFirstCamera (out camera); // 获取相机对象
    
        if (err == EDSDK.EDS_ERR_OK) {
            // 注册回调函数
            err = EDSDK.EdsSetObjectEventHandler (camera, EDSDK.ObjectEvent_All, objectEventHandler, handle);
    
            if (err == EDSDK.EDS_ERR_OK)
                err = EDSDK.EdsSetPropertyEventHandler (camera, EDSDK.PropertyEvent_All, propertyEventHandler, handle);
    
            if (err == EDSDK.EDS_ERR_OK)
                err = EDSDK.EdsSetCameraStateEventHandler (camera, EDSDK.StateEvent_All, stateEventHandler, handle);
    
            // 打开会话
            if (err == EDSDK.EDS_ERR_OK)
                err = EDSDK.EdsOpenSession (camera);
    
            if (err == EDSDK.EDS_ERR_OK)
                isSessionOpened = true;
        }
    
        return err;
    }
    View Code

      相机的退出:

    private void Close (Boolean isDisposed = false) {
        // 关闭实时取景
        if ((EvfOutputDevice & EDSDK.EvfOutputDevice_PC) != 0)
            Stop (out _);
    
        // 停止录像
        if (videoFileWriter != null)
            EndRecord (out _);
    
        // 结束会话
        if (isSessionOpened) {
            lock (sdkLock) {
                if (EDSDK.EdsCloseSession (camera) == EDSDK.EDS_ERR_OK)
                    isSessionOpened = false;
            }
        }
    
        // 释放相机对象
        if (camera != IntPtr.Zero) {
            EDSDK.EdsRelease (camera);
            camera = IntPtr.Zero;
        }
    
        if (isDisposed) {
            GCHandle.FromIntPtr (handle).Free (); // 释放当前对象
            this.ImageSourceChanged = null;
            this.NewImage = null;
            this.NewVideo = null;
            this.NamingRulesFunc = null;
        } else
            EDSDK.EdsSetCameraAddedHandler (cameraAddedHandler, handle); // 监听相机连接
    }
    View Code

      获取相机对象:

    private UInt32 GetFirstCamera (out IntPtr camera) {
        camera = IntPtr.Zero;
    
        // 获取相机列表对象
        var err = EDSDK.EdsGetCameraList (out IntPtr cameraList);
    
        if (err == EDSDK.EDS_ERR_OK) {
            err = EDSDK.EdsGetChildCount (cameraList, out Int32 count);
    
            if (err == EDSDK.EDS_ERR_OK && count > 0) {
                err = EDSDK.EdsGetChildAtIndex (cameraList, 0, out camera);
    
                // 释放相机列表对象
                EDSDK.EdsRelease (cameraList);
                cameraList = IntPtr.Zero;
    
                return err;
            }
        }
    
        if (cameraList != IntPtr.Zero)
            EDSDK.EdsRelease (cameraList);
    
        return EDSDK.EDS_ERR_DEVICE_NOT_FOUND;
    }
    View Code

      相机连接之后的相机设置:

    // 获取相机名称
    if (err == EDSDK.EDS_ERR_OK)
        err = EDSDK.EdsGetPropertyData (camera, EDSDK.PropID_ProductName, 0, out cameraName);
    
    if (err == EDSDK.EDS_ERR_OK)
        err = EDSDK.EdsGetPropertySize (camera, EDSDK.PropID_Evf_OutputDevice, 0, out _, out deviceSize);
    
    // 保存到计算机
    if (err == EDSDK.EDS_ERR_OK)
        err = SaveToHost ();
    
    if (err == EDSDK.EDS_ERR_OK) {
        // 设置自动曝光
        if (ISOSpeed != 0)
            EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_ISOSpeed, 0, sizeof (UInt32), 0);
    
        // 设置拍摄图片质量
        if (ImageQualityDesc != null)
            SetImageQualityJpegOnly ();
    
        // 设置曝光补偿+3
        if (ExposureCompensation != 0x18)
            EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_ExposureCompensation, 0, sizeof (UInt32), 0x18);
    
        // 设置白平衡;自动:环境优先
        if (ExposureCompensation != 0)
            EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_WhiteBalance, 0, sizeof (UInt32), 0);
    
        // 设置测光模式:点测光
        if (MeteringMode != 0)
            EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_MeteringMode, 0, sizeof (UInt32), 0);
    
        // 设置单拍模式
        if (DriveMode != 0)
            EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_DriveMode, 0, sizeof (UInt32), 0);
    
        // 设置快门速度
        if (Tv != 0x60)
            EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_Tv, 0, sizeof (UInt32), 0x60);
    }
    View Code

      开始实时取景,将画面传输到PC:

    public Boolean Play (out String errMsg) {
        errMsg = null;
    
        if (camera == IntPtr.Zero) {
            if (!Init (out errMsg))
                return false;
            else
                Thread.Sleep (500);
        }
    
        if ((EvfOutputDevice & EDSDK.EvfOutputDevice_PC) != 0)
            return true;
    
        UInt32 err = EDSDK.EDS_ERR_OK;
    
        lock (sdkLock) {
            // 不允许设置AE模式转盘
            //if (AEMode != EDSDK.AEMode_Tv)
            //    err = EDSDK.EdsSetPropertyData(camera, EDSDK.PropID_Evf_Mode, 0, sizeof(UInt32), EDSDK.AEMode_Tv);
    
            // 开启实时取景
            if (err == EDSDK.EDS_ERR_OK && (EvfOutputDevice & EDSDK.EvfOutputDevice_PC) == 0)
                err = EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_Evf_OutputDevice, 0, deviceSize, EvfOutputDevice | EDSDK.EvfOutputDevice_PC);
        }
    
        var ret = err == EDSDK.EDS_ERR_OK;
    
        if (ret) {
            thread_evf = new Thread (ReadEvf) { IsBackground = true };
            thread_evf.SetApartmentState (ApartmentState.STA);
            thread_evf.Start ();
        } else
            errMsg = "开启实时图像模式失败,错误代码:" + err;
    
        return ret;
    }
    View Code

      关闭实时取景:

    public Boolean Stop (out String errMsg) {
        errMsg = null;
    
        if (camera == IntPtr.Zero || (EvfOutputDevice & EDSDK.EvfOutputDevice_PC) == 0)
            return true;
    
        var err = EDSDK.EDS_ERR_OK;
    
        // 停止实时取景
        lock (sdkLock) {
            if (DepthOfFieldPreview != EDSDK.EvfDepthOfFieldPreview_OFF)
                err = EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_Evf_DepthOfFieldPreview, 0, sizeof (UInt32), EDSDK.EvfDepthOfFieldPreview_OFF);
    
            if (err == EDSDK.EDS_ERR_OK && (EvfOutputDevice & EDSDK.EvfOutputDevice_PC) != 0)
                err = EDSDK.EdsSetPropertyData (camera, EDSDK.PropID_Evf_OutputDevice, 0, deviceSize, EvfOutputDevice & ~EDSDK.EvfOutputDevice_PC);
        }
    
        if (err != EDSDK.EDS_ERR_OK)
            errMsg = "关闭实时图像模式失败,错误代码:" + err;
    
        return err == EDSDK.EDS_ERR_OK;
    }
    View Code

      获取实时取景画面:

    private void ReadEvf () {
        // 等待实时图像传输开启
        SpinWait.SpinUntil (() => (EvfOutputDevice & EDSDK.EvfOutputDevice_PC) != 0, 5000);
    
        IntPtr stream = IntPtr.Zero;
        IntPtr evfImage = IntPtr.Zero;
        IntPtr evfStream = IntPtr.Zero;
        UInt64 length = 0, maxLength = 2 * 1024 * 1024;
    
        var err = EDSDK.EDS_ERR_OK;
    
        // 当实时图像传输开启时,不断地循环
        while (isSessionOpened && (EvfOutputDevice & EDSDK.EvfOutputDevice_PC) != 0) {
            lock (sdkLock) {
                err = EDSDK.EdsCreateMemoryStream (maxLength, out stream); // 创建用于保存图像的流对象
    
                if (err == EDSDK.EDS_ERR_OK) {
                    err = EDSDK.EdsCreateEvfImageRef (stream, out evfImage); // 创建evf图像对象
    
                    if (err == EDSDK.EDS_ERR_OK)
                        err = EDSDK.EdsDownloadEvfImage (camera, evfImage); // 从相机下载evf图像
    
                    if (err == EDSDK.EDS_ERR_OK)
                        err = EDSDK.EdsGetPointer (stream, out evfStream); // 获取流对象的流地址
    
                    if (err == EDSDK.EDS_ERR_OK)
                        err = EDSDK.EdsGetLength (stream, out length); // 获取流的长度
                }
            }
    
            if (err == EDSDK.EDS_ERR_OK)
                RenderBitmap (evfStream, length); // 渲染图像
    
            if (stream != IntPtr.Zero) {
                EDSDK.EdsRelease (stream);
                stream = IntPtr.Zero;
            }
    
            if (evfImage != IntPtr.Zero) {
                EDSDK.EdsRelease (evfImage);
                evfImage = IntPtr.Zero;
            }
    
            if (evfStream != IntPtr.Zero) {
                EDSDK.EdsRelease (evfStream);
                evfStream = IntPtr.Zero;
            }
        }
    
        // 停止显示图像
        context.Send (n => { WriteableBitmap = null; }, null);
    }
    View Code

      拍摄:

     public Boolean TakePicture (out String errMsg) {
         errMsg = null;
    
         if (camera == IntPtr.Zero) {
             errMsg = "未检测到相机";
             return false;
         }
    
         lock (sdkLock) {
             // 存储到计算机
             var err = SaveToHost ();
    
             if (err == EDSDK.EDS_ERR_OK) {
                 err = EDSDK.EdsSendCommand (camera, EDSDK.CameraCommand_PressShutterButton, (Int32) EDSDK.EdsShutterButton.CameraCommand_ShutterButton_Completely); // 按下拍摄按钮
    
                 if (err == EDSDK.EDS_ERR_OK)
                     err = EDSDK.EdsSendCommand (camera, EDSDK.CameraCommand_PressShutterButton, (Int32) EDSDK.EdsShutterButton.CameraCommand_ShutterButton_OFF); // 弹起拍摄按钮
             }
    
             if (err != EDSDK.EDS_ERR_OK)
                 errMsg = "拍照失败,错误代码:" + err;
    
             return err == EDSDK.EDS_ERR_OK;
         }
     }
    View Code

      开始录像:

    public Boolean BeginRecord (out String errMsg) {
        errMsg = null;
    
        if (camera == IntPtr.Zero) {
            errMsg = "未检测到相机";
            return false;
        }
    
        if (videoFileWriter != null)
            return true;
    
        if ((EvfOutputDevice & EDSDK.EvfOutputDevice_PC) == 0 && !Play (out errMsg))
            return false;
    
        videoFileWriter = new VideoFileWriter ();
        stopwatch = new Stopwatch ();
    
        return true;
    }
    View Code

      停止录像:

    public Boolean EndRecord (out String errMsg) {
        errMsg = null;
    
        if (camera == IntPtr.Zero) {
            errMsg = "未检测到相机";
            return false;
        }
    
        if (videoFileWriter == null)
            return true;
    
        lock (videoFileWriter) {
            videoFileWriter.Close ();
            videoFileWriter = null;
            stopwatch.Stop ();
            stopwatch = null;
        }
    
        return true;
    }
    View Code

      录像使用Accord.Video.FFMPEG.VideoFileWriter类,佳能相机的帧率不稳定,这里使用固定帧率16PFS,这会导致录像文件时长不对,因此需要使用计时器StopWatch计算当前帧的时间戳;

    using (var bmp = (Bitmap) imageConverter.ConvertFrom (data)) // 解码获取Bitmap
    {
        // 获取Bitmap的像素数据指针
        var bmpData = bmp.LockBits (new Rectangle (bmpStartPoint, bmp.Size), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
    
        if (videoFileWriter != null) {
            lock (videoFileWriter) {
                // 保存录像
                if (!videoFileWriter.IsOpen) {
                    var folder = VideoFolder ?? Environment.CurrentDirectory;
    
                    if (!Directory.Exists (folder))
                        Directory.CreateDirectory (folder);
    
                    var fileName = NamingRulesFunc?.Invoke () ?? (DateTime.Now - new DateTime (1970, 1, 1)).TotalMilliseconds.ToString ("0");
                    var filePath = Path.Combine (folder, fileName + ".mp4");
    
                    videoFileWriter.Open (filePath, this.width, this.height, 16, VideoCodec.MPEG4); // 使用16FPS,MP4文件保存
                    spf = 1000 / 16; // 计算一帧毫秒数
                    stopwatch.Restart ();
                    frameIndex = 0;
                    videoFileWriter.WriteVideoFrame (bmpData);
                } else {
                    // 写入视频帧时传入时间戳,否则录像时长将对不上
                    var frame_index = (UInt32) (stopwatch.ElapsedMilliseconds / spf);
    
                    if (frameIndex != frame_index) {
                        frameIndex = frame_index;
                        videoFileWriter.WriteVideoFrame (bmpData, frameIndex);
                    }
                }
            }
        }
    
        bmp.UnlockBits (bmpData);
    }
    View Code

      如果是winfrom,可以使用PictureBox直接渲染Bitmap,本项目使用wpf技术,使用WriteableBitmap高效渲染,在第一帧时创建WriteableBitmap对象,之后将Bitmap数据写入WriteableBitmap的后台缓冲区,监听程序渲染事件CompositionTarget.Rendering不断更新画面;

    private WriteableBitmap writeableBitmap;
    /// <summary>
    /// WPF的一个高性能渲染图像,利用后台缓冲区,渲染图像时不必每次都切换线程
    /// </summary>
    private WriteableBitmap WriteableBitmap {
        get => this.writeableBitmap;
        set {
            if (this.writeableBitmap == value)
                return;
    
            if (this.writeableBitmap == null)
                CompositionTarget.Rendering += OnRender;
            else if (value == null)
                CompositionTarget.Rendering -= OnRender;
    
            this.writeableBitmap = value;
            this.ImageSourceChanged?.Invoke (value);
        }
    }
    
    private void RenderBitmap (IntPtr evfStream, UInt64 length) {
        var data = new Byte[length];
        var bmpStartPoint = new System.Drawing.Point (0, 0);
    
        Marshal.Copy (evfStream, data, 0, (Int32) length); // 从流地址拷贝一份到字节数组,再解码获取图像(如果可以写一个从指针解码图像,可以优化此步骤)
    
        using (var bmp = (Bitmap) imageConverter.ConvertFrom (data)) // 解码获取Bitmap
        {
            if (this.WriteableBitmap == null || this.width != bmp.Width || this.height != bmp.Height) {
                // 第一次或宽高不对应时创建WriteableBitmap对象
                this.width = bmp.Width;
                this.height = bmp.Height;
    
                // 通过线程同步上下文切换到主线程
                context.Send (n => {
                    WriteableBitmap = new WriteableBitmap (this.width, this.height, 96, 96, PixelFormats.Bgr24, null);
                    backBuffer = WriteableBitmap.BackBuffer; // 保存后台缓冲区指针
                    this.stride = WriteableBitmap.BackBufferStride; // 单行像素数据中的字节数
                    this.length = this.stride * this.height; // 像素数据的总字节数
                }, null);
            }
    
            // 获取Bitmap的像素数据指针
            var bmpData = bmp.LockBits (new Rectangle (bmpStartPoint, bmp.Size), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
    
            // 将Bitmap的像素数据拷贝到WriteableBitmap
            if (this.stride == bmpData.Stride)
                Memcpy (backBuffer, bmpData.Scan0, this.length);
            else {
                var s = Math.Min (this.stride, bmpData.Stride);
                var tPtr = backBuffer;
                var sPtr = bmpData.Scan0;
                for (var i = 0; i < this.height; i++) {
                    Memcpy (tPtr, sPtr, s);
                    tPtr += this.stride;
                    sPtr += bmpData.Stride;
                }
            }
    
            bmp.UnlockBits (bmpData);
            Interlocked.Exchange (ref newFrame, 1);
        }
    }
    
    private void OnRender (Object sender, EventArgs e) {
        var curRenderingTime = ((RenderingEventArgs) e).RenderingTime;
    
        if (curRenderingTime == lastRenderingTime)
            return;
    
        lastRenderingTime = curRenderingTime;
    
        if (Interlocked.CompareExchange (ref newFrame, 0, 1) != 1)
            return;
    
        var bmp = this.WriteableBitmap;
        bmp.Lock ();
        bmp.AddDirtyRect (new Int32Rect (0, 0, bmp.PixelWidth, bmp.PixelHeight));
        bmp.Unlock ();
    }
    View Code

      下面说一下新人容易踩到的坑:

      1、EDSDK的API不能同时调用,否则会卡死;为了解决这个问题,加了一个锁,保证多条线程不能同时调API;

      2、同时执行多条API期间可能需要等待500ms,真是坑;

      3、图像回调还需要下载,而且下载的是Jpeg文件流而不是BGR24或YUV等RAW数据;因此还需要解码获取BGR24数据;

      4、录像必须保存到相机,因此需要存储卡,并且录像文件未编码,因此特别大,1秒1兆的样子,再传回电脑特别慢,再加上上面加锁的关系,卡住其他功能操作;还有录像结束后会自动停止实时图像传输,因此在停止录像后需要等待几秒再打开实时图像传输;并且打开录像模式之后,实时图像传输明显变卡;综合以上原因,我决定不打开录像模式,而是在实时图像传输时保存视频帧;

      佳能相机在30分钟未操作后,会自动进入休眠模式,需要通电(或关闭再打开相机)才能调用,这里的解决方案是,创建了相机对象,只要不调用Dispose方法,即使初始化失败,当相机重新连接时,会自动初始化并打开实时图像传输;

  • 相关阅读:
    Python中Random随机数返回值方式
    SQL跨库查询
    正则表达式基本语法
    excel VBA使用教程
    使用某些Widows API时,明明包含了该头文件,却报错“error C2065: undeclared identifier”
    电脑开机后数字键盘为关闭状态
    编译Boost 详细步骤 适用 VC6 VS2003 VS2005 VS2008 VS2010
    变量作用域,不能理解,先记下
    解决MySQL 在 Java 检索遇到timestamp空值时报异常的问题
    Annotation
  • 原文地址:https://www.cnblogs.com/pumbaa/p/13969954.html
Copyright © 2020-2023  润新知