佳能数码单反相机是众多相机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兆的样子,再传回电脑特别慢,再加上上面加锁的关系,卡住其他功能操作;还有录像结束后会自动停止实时图像传输,因此在停止录像后需要等待几秒再打开实时图像传输;并且打开录像模式之后,实时图像传输明显变卡;综合以上原因,我决定不打开录像模式,而是在实时图像传输时保存视频帧;
下面贴出显示实时图像的代码:
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方法,即使初始化失败,当相机重新连接时,会自动初始化并打开实时图像传输;