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

  1、打开和关闭相机

  2、实时显示图像

  3、拍照和录像

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



///<summary>/// 相机接口
///</summary>publicinterface 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; }
}



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



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



  相机的实现类比较长,直接上源码:;源码里面有官方SDK文档和Demo,强烈建议看完第六章的示例,因为Demo封装得太多,不易看懂;这里不重复SDK文档的内容,只是说一下新人容易踩到的坑。

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

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

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

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

  下面贴出显示实时图像的代码:




Androidstudio中Android拍照 android studio 拍照_Boo

Androidstudio中Android拍照 android studio 拍照_Boo_02

1privatevoid ReadEvf () {
  2// 等待实时图像传输开启  3     SpinWait.SpinUntil (() => (EvfOutputDevice & ) != 0, 5000);
  4  5     IntPtr stream = ;
  6     IntPtr evfImage = ;
  7     IntPtr evfStream = ;
  8     UInt64 length = 0, maxLength = 2 * 1024 * 1024;
  9var data = new Byte[maxLength];
 10var bmpStartPoint = new System.Drawing.Point (0, 0);
 11var startRecordTime = 0;
 12 13var err = ;
 14 15// 当实时图像传输开启时,不断地循环 16while ((EvfOutputDevice & ) != 0) {
 17lock (sdkLock) {
 18             err =  (maxLength, out stream); // 创建用于保存图像的流对象 19 20if (err == ) {
 21                 err =  (stream, out evfImage); // 创建evf图像对象 22 23if (err == )
 24                     err =  (camera, evfImage); // 从相机下载evf图像 25 26if (err == )
 27                     err =  (stream, out evfStream); // 获取流对象的流地址 28 29if (err == )
 30                     err =  (stream, out length); // 获取流的长度 31            }
 32        }
 33 34if (err == ) {
 35             Marshal.Copy (evfStream, data, 0, (Int32) length); // 从流地址拷贝一份到字节数组,再解码获取图像(如果可以写一个从指针解码图像,可以优化此步骤) 36 37using (var bmp = (Bitmap)  (data)) // 解码获取Bitmap 38            {
 39if (this.WriteableBitmap == null || this.width != bmp.Width || this.height != ) {
 40// 第一次或宽高不对应时创建WriteableBitmap对象 41this.width = bmp.Width;
 42this.height = ;
 43 44// 通过线程同步上下文切换到主线程 45                     context.Send (n => {
 46                         WriteableBitmap = new WriteableBitmap (this.width, this.height, 96, 96, PixelFormats.Bgr24, null);
 47                         backBuffer = WriteableBitmap.BackBuffer; // 保存后台缓冲区指针 48this.stride = WriteableBitmap.BackBufferStride; // 单行像素数据中的字节数 49this.length = this.stride * this.height; // 像素数据的总字节数 50                     }, null);
 51                }
 52 53// 获取Bitmap的像素数据指针 54var bmpData =  (new Rectangle (bmpStartPoint, ), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
 55 56// 将Bitmap的像素数据拷贝到WriteableBitmap 57if (this.stride == bmpData.Stride)
 58                     Memcpy (backBuffer, bmpData.Scan0, this.length);
 59else {
 60var s =  (this.stride, bmpData.Stride);
 61var tPtr = backBuffer;
 62var sPtr = bmpData.Scan0;
 63for (var i = 0; i < this.height; i++) {
 64                        Memcpy (tPtr, sPtr, s);
 65                         tPtr += this.stride;
 66                         sPtr += bmpData.Stride;
 67                    }
 68                }
 69 70                bmp.UnlockBits (bmpData);
 71 72                 Interlocked.Exchange (ref newFrame, 1);
 73 74if (videoFileWriter != null) {
 75lock (videoFileWriter) {
 76// 保存录像 77if (!) {
 78var folder = VideoFolder ?? ;
 79 80if (! (folder))
 81                                Directory.CreateDirectory (folder);
 82 83var fileName = NamingRulesFunc?.Invoke () ?? (DateTime.Now - new DateTime (1970, 1, 1)). ("0");
 84var filePath =  (folder, fileName + ".mp4");
 85 86                             videoFileWriter.Open (filePath, this.width, this.height);
 87                             startRecordTime = Environment.TickCount;
 88                        }
 89 90// 写入视频帧时传入时间戳,否则录像时长将对不上 91                         videoFileWriter.WriteVideoFrame (bmp,  (Environment.TickCount - startRecordTime));
 92                    }
 93                }
 94            }
 95        }
 96 97if (stream != ) {
 98            EDSDK.EdsRelease (stream);
 99             stream = ;
100        }
101102if (evfImage != ) {
103            EDSDK.EdsRelease (evfImage);
104             evfImage = ;
105        }
106107if (evfStream != ) {
108            EDSDK.EdsRelease (evfStream);
109             evfStream = ;
110        }
111    }
112113// 停止显示图像114     context.Send (n => { WriteableBitmap = null; }, null);
115 }


View Code


  利用WriteableBitmap的后台缓冲区,实现获取图像时不需要切换回UI线程,而是在事件(程序界面刷新)中刷新缓冲区。

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