简介:这份C#开发示例面向需要以编程方式控制佳能相机的.NET开发者,系统演示了EDSDK在设备管理、实时预览、参数调整、远程快门及图像下载等环节的调用方法。压缩包仅73KB,包含18个文件,其中8个cs源码为主要内容,覆盖相机枚举、会话开启、属性读取、拍照命令与图像传输等模块;另有3个txt说明文档以及工程配置、资源文件,便于直接打开VS项目学习。示例对EdsGetChildCount、EdsOpenSession、EdsSendCommand、EdsShoot等关键API都有实际用法展示,还涉及事件回调与资源释放等易被忽略的细节。目前已有1012人学习下载,适合刚接触佳能SDK的C#开发者作为入门参考,也可作为自动化拍摄或远程监控项目的代码基底。
1. 项目概述与核心需求解析
做工业视觉和自动化设备这些年,相机控制一直是绕不开的环节。工业相机有工业相机的玩法,但很多项目现场客户拿来的就是一台佳能单反——他们可能已经有用惯了的机身和镜头,或者项目预算有限,不想再掏几万块买面阵工业相机。这时候佳能提供的官方开发套件(EOS Digital SDK,简称EDSDK)就派上用场了。
这套SDK是佳能官方发布的原生C语言接口,核心封装在Edsdk.dll(Windows平台)或者macOS的框架里,能让开发者控制佳能EOS系列相机的几乎所有功能:拍照、录像、实时取景、参数调节(光圈、快门、ISO、白平衡)、文件传输等。说白了,它就是佳能给第三方开发者开的一扇门,让你不用逆向协议,也不用操作物理按键,就能用代码指挥相机干活。
但问题来了:EDSDK本身是C接口,而现在的上位机软件绝大多数是用C#写的,尤其是做工业视觉、医疗影像、证件照系统、远程拍照控制这类项目的朋友,几乎都在C#环境下开发。C#调原生DLL不是不行,P/Invoke(平台调用)本身就是Windows上很成熟的方案,但EDSDK的接口设计相对古老,涉及大量指针、回调函数、结构体,直接P/Invoke写起来不仅繁琐,而且坑特别多。
这篇博文就是围绕“用C#完整调通佳能EDSDK”这件事来写的。我会从环境准备、SDK架构、核心功能实现、典型坑点排查这几个角度展开,把我实际调试过、踩过坑、最终跑通的经验完整分享出来。适合正在做C#上位机开发、刚接触EDSDK、或者想评估“用单反替代工业相机来开发视觉项目”是否可行的朋友参考。
2. EDSDK的基础认知与环境准备
2.1 EDSDK到底是什么,以及C#为什么需要P/Invoke
EDSDK全称是EOS Digital SDK,是佳能官方发布的原生开发包,主要面向Windows和macOS两个平台。它的本质是一组C语言接口,通过EdsInitializeSDK初始化、EdsEnumCameras枚举相机、EdsOpenSession打开会话等一套API来控制相机。
这里要理解一个关键点:C#运行在.NET托管环境中,默认是不能直接调用原生DLL导出函数的,需要通过P/Invoke机制——用DllImport特性告诉CLR“我要调哪个DLL里的哪个函数”,再由运行时负责参数封送(Marshal)。EDSDK涉及的数据结构并不复杂,核心就是EdsCamera、EdsObject这些句柄(IntPtr),以及EdsCameraInfo、EdsEvfInfo等结构体。所以理论上一对一映射并不难,难的是怎么把佳能官方的回调模型、异步事件机制和C#的事件/委托模型对接起来。
2.2 官方SDK的获取与版本选择
先从最基础的开始:到佳能官方开发者网站注册账号,申请下载EDSDK。佳能对开发者有审核机制,但一般个人开发者提交申请都能通过。下载下来的是一个压缩包,里面包含:
Windows/目录:含DLL、头文件(.h)、示例代码(C/C++)Documents/目录:官方API参考文档(很有用,建议通读)Samples/目录:官方示例,有C++和C#两个版本
关于版本选择,我个人建议优先使用你手头相机支持的最新的EDSDK版本。EDSDK有向后兼容性,新版SDK一般能兼容老机型,但反过来老版SDK驱动不了新机身。比如你用的是R5、R6这类新机,旧版SDK枚举不到或者功能缺失是很常见的。
2.3 C#项目里的DLL引用方案
拿到SDK后,在C#项目里引用EDSDK一般有两种做法:
第一种:直接用DllImport手工封送
这种方式最灵活,也没有额外依赖,适合接口调用较少的轻量项目。需要自己把EDSDK的几十个核心接口和结构体逐一翻译成C#版本,工作量大一些,但对于理解EDSDK的工作机制特别有帮助。
第二种:使用封装好的NuGet包
社区有一些现成的封装库,比如EDSDK.CSharp(由Edawrd和社区维护)。这个包把大部分P/Invoke和基本功能都封装好了,开箱即用,适合快速出Demo。但要注意这个包的维护活跃度和版本兼容性。
我实际项目中采用的是折中方案:引用社区封装包跑通整体流程,再根据项目需求用DllImport补充封装包覆盖不到的自定义功能。对于初学者,我建议先用DllImport把官方Samples里C#示例跑一遍,理解原理后再考虑封装库。
2.4 DllImport基础封装代码
无论你最终用哪种方案,下面这些核心接口的P/Invoke声明是绕不开的。先看骨架代码:
using System; using System.Runtime.InteropServices; namespace CanonEDSDKDemo { internal static class EdsdkNative { private const string DllName = "EDSDK.dll"; [DllImport(DllName, EntryPoint = "EdsInitializeSDK")] public static extern uint EdsInitializeSDK(); [DllImport(DllName, EntryPoint = "EdsTerminateSDK")] public static extern uint EdsTerminateSDK(); [DllImport(DllName, EntryPoint = "EdsGetCameraList")] public static extern uint EdsGetCameraList(out IntPtr cameraListRef); [DllImport(DllName, EntryPoint = "EdsGetChildCount")] public static extern uint EdsGetChildCount(IntPtr listRef, out int count); [DllImport(DllName, EntryPoint = "EdsGetChildAtIndex")] public static extern uint EdsGetChildAtIndex(IntPtr listRef, int index, out IntPtr outRef); [DllImport(DllName, EntryPoint = "EdsOpenSession")] public static extern uint EdsOpenSession(IntPtr cameraRef); [DllImport(DllName, EntryPoint = "EdsCloseSession")] public static extern uint EdsCloseSession(IntPtr cameraRef); } }其中的uint对应EDSDK里的EdsError类型,返回0(即EDS_ERR_OK)表示调用成功。这一层封装好之后,后续的功能开发就都是在这个基础上扩展了。
3. 核心功能设计与实现思路
3.1 相机管理模块:枚举、注册、打开会话
一个完整的EDSDK程序生命周期是这样的:初始化SDK → 枚举相机列表 → 获取相机对象 → 注册相机事件回调 → 打开会话 → 执行控制操作 → 关闭会话 → 释放相机对象 → 终止SDK。
这里有几个容易出错的操作细节需要特别留意。
第一,初始化与释放必须成对调用。EdsInitializeSDK和EdsTerminateSDK必须严格配对,而且建议整个程序生命周期只初始化一次,不要多次初始化。
第二,相机对象必须从SDK的CameraList里取,不要自己new或持有过期的指针。每次重新枚举时,之前拿到的相机句柄可能已失效。
第三,注册回调要早于打开会话。官方推荐先注册事件回调,再打开会话,这样拍照完成、文件传输等事件就不会丢。
来看一段完整的相机初始化代码:
public class CanonCameraManager : IDisposable { private IntPtr _cameraList = IntPtr.Zero; private IntPtr _camera = IntPtr.Zero; private bool _sessionOpen = false; public event EventHandler<PhotoCapturedEventArgs> PhotoCaptured; public int Initialize() { // 初始化SDK uint err = EdsdkNative.EdsInitializeSDK(); if (err != Edserror.EDS_ERR_OK) { throw new InvalidOperationException($"SDK初始化失败,错误码:{err}"); } // 枚举相机 err = EdsdkNative.EdsGetCameraList(out _cameraList); if (err != Edserror.EDS_ERR_OK) { throw new InvalidOperationException($"获取相机列表失败,错误码:{err}"); } int cameraCount = 0; err = EdsdkNative.EdsGetChildCount(_cameraList, out cameraCount); if (cameraCount == 0) { throw new InvalidOperationException("未检测到任何佳能相机,请检查USB连接和相机设置"); } // 取第一台相机 err = EdsdkNative.EdsGetChildAtIndex(_cameraList, 0, out _camera); if (err != Edserror.EDS_ERR_OK) { throw new InvalidOperationException($"获取相机对象失败,错误码:{err}"); } // 注册对象事件回调(拍照完成/文件传输等) err = EdsdkNative.EdsSetObjectEventHandler( _camera, EdObjectEventType.All, _objectEventHandler, IntPtr.Zero); // 打开会话 err = EdsdkNative.EdsOpenSession(_camera); if (err != Edserror.EDS_ERR_OK) { throw new InvalidOperationException($"打开相机会话失败,错误码:{err}"); } _sessionOpen = true; return 0; } // ... 其他方法 }这里特别注意EdsSetObjectEventHandler这个函数,它是接收相机异步事件(拍照完成、文件保存完毕等)的入口。C#里通过委托把原生回调转换为托管事件,后续UI或者业务流程直接订阅PhotoCaptured事件即可。
3.2 拍照控制的完整流程
拍一张照片涉及的动作不仅仅是“按快门”,还包括:设置拍摄参数、通知相机准备拍摄、执行拍照、等待事件回调、下载照片文件。EDSDK的拍照流程比工业相机走GigE Vision要复杂一点,因为它遵循“相机端控制”的模型。
核心代码逻辑如下:
public async Task CapturePhotoAsync(string savePath) { if (!_sessionOpen) throw new InvalidOperationException("会话未打开"); // 1. 确保相机处于可以拍照的状态 uint err = EdsdkNative.EdsSendCommand(_camera, EdCameraCommand.TakePicture, 0); if (err != Edserror.EDS_ERR_OK) { throw new Exception($"拍照指令发送失败,错误码:{err}"); } // 2. 等待拍照完成事件(由ObjectEventHandler异步回调触发) // 事件回调中会把照片临时保存到相机内存/存储卡 // 3. 在回调中通过DirectoryItem相关API下载图片到PC await Task.Delay(2000); // 实际项目中改为事件驱动的等待 }实际做产品时,我不建议用Task.Delay等待,而是用SemaphoreSlim或TaskCompletionSource结合事件来精确同步。拍照命令发出后,相机把照片写入存储介质,然后触发kEdsObjectEvent_DirItemCreated事件——你在这个事件里拿到DirectoryItem句柄,调用EdsDownload把文件传输到PC。这样的流程才能保证文件完整,不丢图。
3.3 实时取景与参数调节
EDSDK的实时取景(LiveView)功能在自动对焦调试、视觉定位项目里非常有用。它的实现逻辑是:开启取景 → 循环获取取景图像数据 → 显示到WPF/WinForms的Image控件。
实时取景的核心思路并不复杂:调用EdsSendCommand命令通知相机进入取景状态,然后创建一个EdsEvfImage对象,循环调用EdsDownloadEvfImage获取最新画面,然后转换成Bitmap显示。但有几个参数细节很关键:取景图像的尺寸、画质模式、刷新频率。尺寸越大,传输压力越大,UI刷新就越容易卡顿。
关于UI卡顿的问题,很多做C#上位机的朋友都会遇到。我的经验是:实时取景图像的获取和UI更新必须分离到不同线程,并且取景循环用独立的后台线程跑。UI线程只负责接收已经处理好的Bitmap并显示,同时要控制刷新率,一般每秒15~30帧就足够流畅了,不需要无脑刷。
3.4 文件下载与存储卡读取
拍摄完成后,照片保存在相机的CF/SD卡里。EDSDK提供了两种方式获取照片:
- 事件驱动式:注册
DirItemCreated事件,拍照完成后事件回调会传递新创建的文件项句柄,你用这个句柄去下载。 - 主动查询式:枚举存储卡上的目录项,找到目标文件后下载。
实践中,事件驱动方式更自然。伪代码逻辑如下:
// 在ObjectEventHandler中 if (inEvent == EdObjectEventType.DirItemCreated) { IntPtr dirItemRef = (IntPtr)inRef; // 获取文件信息 EdsDirectoryItemInfo itemInfo; EdsdkNative.EdsGetDirectoryItemInfo(dirItemRef, out itemInfo); // 创建本地文件流并下载 string localPath = Path.Combine(_saveDirectory, itemInfo.szFileName); IntPtr streamRef = IntPtr.Zero; EdsdkNative.EdsCreateFileStream(localPath, EdFileCreateDisposition.CreateAlways, EdAccess.ReadWrite, out streamRef); EdsdkNative.EdsDownload(dirItemRef, streamRef, itemInfo.size); EdsdkNative.EdsDownloadComplete(dirItemRef); // 释放流和目录项 EdsdkNative.EdsRelease(streamRef); EdsdkNative.EdsRelease(dirItemRef); }这里特别提醒一点:EdsDownload是同步阻塞的,当文件较大(Raw格式一张几十MB)时,会阻塞当前调用线程。如果放在事件回调线程里,会影响后续事件的响应。文件下载操作建议放到线程池或专门的异步逻辑中处理。另外别忘了EdsDownloadComplete。如果不调用这个函数,存储卡里的文件可能不会被正确标记为已读取,下次枚举时还会出现,而且相机内部可能产生异常状态。
4. 实操指南:完整示例的搭建
4.1 创建一个简单的控制台或WinForms/WPF应用
EDSDK项目对UI框架没有限制。我建议先做一个WinForms的快速原型,界面布局直观,调试方便。如果要做成正式产品,再迁移到WPF或者其他框架也不难。
开发前先做好这几步准备工作:
- Windows上先安装佳能相机的官方驱动(一般装个EOS Utility就行),目的不是用它的软件,而是让系统能正确识别相机USB设备。
- 确认相机的USB连接模式设置为“PC连接”(部分机型叫“电脑连接”),有些机身还要求把自动关机时间调长或者关闭,不然拍几张就休眠了。
- 在C#工程里引用EDSDK.dll——直接
DllImport无需在项目中添加引用,但记得把EDSDK.dll和DPPDLL.dll(一个依赖库)拷贝到输出目录。
4.2 完整可运行的最小示例代码
下面这个示例我会尽量精简但完整,覆盖了:初始化、枚举相机、回调事件、拍照、下载、释放的完整闭环。代码基于我实际项目的简化版本,关键位置都加了注释。
using System; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; namespace CanonSdkDemo { // 错误码常量(只列出常用部分) internal static class Edserror { public const uint EDS_ERR_OK = 0; public const uint EDS_ERR_DEVICE_NOT_FOUND = 0x80; public const uint EDS_ERR_DEVICE_BUSY = 0x81; public const uint EDS_ERR_TAKE_PICTURE_AF_NG = 0x8D01; } // 事件类型常量 internal static class EdObjectEventType { public const uint All = 0x00000200; public const uint DirItemCreated = 0x00000202; } // 相机命令 internal static class EdCameraCommand { public const uint TakePicture = 0x00000000; } public class CameraController : IDisposable { private IntPtr _camera = IntPtr.Zero; private IntPtr _cameraList = IntPtr.Zero; private string _saveDir = AppDomain.CurrentDomain.BaseDirectory; // 委托定义 public delegate uint EdsObjectEventHandler(uint inEvent, IntPtr inRef, IntPtr inContext); // DllImport部分 [DllImport("EDSDK.dll", EntryPoint = "EdsInitializeSDK")] private static extern uint EdsInitializeSDK(); [DllImport("EDSDK.dll", EntryPoint = "EdsTerminateSDK")] private static extern uint EdsTerminateSDK(); [DllImport("EDSDK.dll", EntryPoint = "EdsGetCameraList")] private static extern uint EdsGetCameraList(out IntPtr cameraListRef); [DllImport("EDSDK.dll", EntryPoint = "EdsGetChildCount")] private static extern uint EdsGetChildCount(IntPtr listRef, out int count); [DllImport("EDSDK.dll", EntryPoint = "EdsGetChildAtIndex")] private static extern uint EdsGetChildAtIndex(IntPtr listRef, int index, out IntPtr outRef); [DllImport("EDSDK.dll", EntryPoint = "EdsOpenSession")] private static extern uint EdsOpenSession(IntPtr cameraRef); [DllImport("EDSDK.dll", EntryPoint = "EdsCloseSession")] private static extern uint EdsCloseSession(IntPtr cameraRef); [DllImport("EDSDK.dll", EntryPoint = "EdsSendCommand")] private static extern uint EdsSendCommand(IntPtr cameraRef, uint command, int param); [DllImport("EDSDK.dll", EntryPoint = "EdsSetObjectEventHandler")] private static extern uint EdsSetObjectEventHandler(IntPtr cameraRef, uint eventType, EdsObjectEventHandler eventHandler, IntPtr context); [DllImport("EDSDK.dll", EntryPoint = "EdsGetDirectoryItemInfo", CharSet = CharSet.Ansi)] private static extern uint EdsGetDirectoryItemInfo(IntPtr dirItemRef, out EdsDirectoryItemInfo dirItemInfo); [DllImport("EDSDK.dll", EntryPoint = "EdsCreateFileStream", CharSet = CharSet.Ansi)] private static extern uint EdsCreateFileStream(string fileName, uint createDisposition, uint access, out IntPtr streamRef); [DllImport("EDSDK.dll", EntryPoint = "EdsDownload")] private static extern uint EdsDownload(IntPtr dirItemRef, IntPtr streamRef, ulong size); [DllImport("EDSDK.dll", EntryPoint = "EdsDownloadComplete")] private static extern uint EdsDownloadComplete(IntPtr dirItemRef); [DllImport("EDSDK.dll", EntryPoint = "EdsRelease")] private static extern uint EdsRelease(IntPtr objectRef); // 目录项信息结构体(简化版) [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] internal struct EdsDirectoryItemInfo { public uint size; public int isFolder; public int groupID; public int option; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string szFileName; public uint format; public int dateTime; } private EdsObjectEventHandler _objectEventHandler; public CameraController() { _objectEventHandler = OnObjectEvent; } public int Connect() { uint err = EdsInitializeSDK(); if (err != Edserror.EDS_ERR_OK) return -1; err = EdsGetCameraList(out _cameraList); if (err != Edserror.EDS_ERR_OK) return -2; int count = 0; EdsGetChildCount(_cameraList, out count); if (count == 0) return -3; err = EdsGetChildAtIndex(_cameraList, 0, out _camera); if (err != Edserror.EDS_ERR_OK) return -4; // 注册事件回调 err = EdsSetObjectEventHandler(_camera, EdObjectEventType.All, _objectEventHandler, IntPtr.Zero); if (err != Edserror.EDS_ERR_OK) return -5; err = EdsOpenSession(_camera); if (err != Edserror.EDS_ERR_OK) return -6; return 0; } public void Capture() { if (_camera == IntPtr.Zero) return; uint err = EdsSendCommand(_camera, EdCameraCommand.TakePicture, 0); if (err != Edserror.EDS_ERR_OK) { Console.WriteLine($"拍照失败,错误码:0x{err:X8}"); } } private uint OnObjectEvent(uint inEvent, IntPtr inRef, IntPtr inContext) { if (inEvent == EdObjectEventType.DirItemCreated) { // 新文件已创建,执行下载 try { EdsDirectoryItemInfo itemInfo; EdsGetDirectoryItemInfo(inRef, out itemInfo); Console.WriteLine($"发现新文件:{itemInfo.szFileName},大小:{itemInfo.size} 字节"); string filePath = Path.Combine(_saveDir, itemInfo.szFileName); IntPtr stream = IntPtr.Zero; uint err = EdsCreateFileStream(filePath, 2 /* CreateAlways */, 1 /* ReadWrite */, out stream); if (err == Edserror.EDS_ERR_OK) { err = EdsDownload(inRef, stream, itemInfo.size); if (err != Edserror.EDS_ERR_OK) { Console.WriteLine($"下载文件失败:0x{err:X8}"); } EdsDownloadComplete(inRef); EdsRelease(stream); } } catch (Exception ex) { Console.WriteLine($"处理文件事件异常:{ex.Message}"); } // 注意:这里延时释放inRef,确保下载完成 // 官方文档要求在文件事件处理完后调用 EdsRelease(inRef) // 实际项目中可以稍后再释放,避免下载未完成 Task.Delay(1000).ContinueWith(_ => { EdsRelease(inRef); }); } return Edserror.EDS_ERR_OK; } public void Dispose() { if (_camera != IntPtr.Zero) { EdsCloseSession(_camera); } if (_cameraList != IntPtr.Zero) { EdsRelease(_cameraList); } EdsTerminateSDK(); } } class Program { static void Main(string[] args) { var controller = new CameraController(); int result = controller.Connect(); if (result != 0) { Console.WriteLine($"连接失败,错误码:{result}"); return; } Console.WriteLine("相机连接成功。按回车键拍照,输入q退出。"); while (true) { var key = Console.ReadLine(); if (key?.ToLower() == "q") break; controller.Capture(); } controller.Dispose(); Console.WriteLine("程序退出。"); } } }这段代码已经能跑通“连接相机→拍照→自动下载照片到程序目录”的完整流程了。
4.3 拍照参数设置的两种方式
实际项目中,控制光圈、快门、ISO是经常需要的。EDSDK提供了两套控制属性机制:
- 简单方式:
EdsSetPropertyData+ 属性ID。比如把kEdsPropID_Tv(快门速度)或者kEdsPropID_ISOSpeed(ISO)设为特定值。这种方式简单,但很多属性需要相机处于特定曝光模式下才能生效。 - 复杂方式:使用
EdsSetProperty加自定义数据类型。适合白平衡偏移、色彩矩阵这类复杂设置,日常项目用到的不多。
经验之谈是:**先把相机的拍摄模式切到M档(手动曝光)或Av/Tv档,再设置参数。**相机如果是Auto档,SDK对参数的控制力会很弱,甚至直接拒绝修改属性。这个坑我见过很多次。
5. 常见问题与排查技巧实录
5.1 核心报错速查表
| 错误码/现象 | 可能原因 | 解决建议 |
|---|---|---|
| 枚举不到相机 | USB线连接不稳定 / 相机未设置为PC连接模式 / 驱动未安装 | 换原装或短线缆;菜单里检查“USB连接”或“PC连接”设置;重装EOS Utility |
| EdsOpenSession返回0x81(Device Busy) | 相机正被EOS Utility或其他软件占用 | 彻底关闭EOS Utility;拔掉USB重新连接;检查是否有后台进程占用 |
| 拍照命令超时/失败 | 相机正在处理上一张照片 / 对焦失败 / 存储卡已满 | 延长命令间时间间隔;把对焦模式切到MF;检查SD卡剩余空间 |
| 实时取景黑屏 | 取景模式不对 / 未设置输出设备 | 确认调用EdsSendCommand(kEdsCameraCommand_EvfStart);设置kEdsPropID_Evf_OutputDevice为PC端输出 |
| 下载文件不完整 | 下载过程中释放了DirectoryItem引用 | 确保EdsDownload和EdsDownloadComplete都成功后再释放引用 |
| 程序崩溃在回调中 | 回调线程中直接操作了UI | 回调线程是SDK的内部线程,UI操作一律用Invoke/BeginInvoke封送到UI线程 |
5.2 关于“拍一张卡顿一下”的优化
很多朋友做了拍照功能后发现,相机每拍一张,程序就卡一下,拍完还要等好几秒才能恢复。这个多半是文件下载阻塞了主流程。相机端设置到“RAW+JPEG”双格式时尤其明显——一张照片可能要写入20~50MB数据。
我的优化思路是:
- 拍照命令和文件下载完全异步化。拍照指令发出去后,UI立刻恢复,文件下载放到后台处理。
- 下载时设置传输进度回调,避免文件过大时界面长时间无响应。
- 必要时关闭相机的“自动回放”功能(
kEdsPropID_AutoPowerOff和回放相关属性),减少相机内部的处理步骤。 - 如果不需要RAW,就只用JPEG格式,传输速度会快很多。
5.3 UI卡顿问题的分析
热词里有一条“C#循环数据采集和UI刷新卡顿”,在EDSDK中也经常遇到。实时取景时如果UI线程直接去转Bitmap,肯定卡成PPT。解决思路很明确:
- 用独立的取景线程,循环从SDK拿
EvfImage数据,转Bitmap; - 把Bitmap通过
BeginInvoke发到UI线程,UI线程只做显示; - 控制帧率,12~20帧/秒足够。
5.4 多相机同时控制的注意点
有些项目需要用一台PC同时控制多台佳能相机(比如商品拍摄的多角度同步)。EDSDK是支持多相机枚举的,它的CameraList里可以有多台相机。但要注意:
- 每台相机要分别注册事件回调,事件的上下文参数(
inContext)要区分开; - 每个相机的会话要独立打开关闭,不要共享句柄;
- USB带宽是共享的,多台相机同时实时取景时,帧率会明显下降。
如果要实现多相机同步拍照,单纯靠SDK的循环发送命令是不够的,因为每台相机的响应延迟不同。更可靠的方案是用硬件触发(如果相机支持MC协议),或者通过时间戳校准。
5.5 其他实战避坑总结
- 调试时始终开着相机日志:EDSDK有
EdsSetLogLevel和日志文件输出功能,遇到诡异问题打开日志,事半功倍。 - 不要跨线程调用EDSDK接口:SDK内部不是线程安全的,所有API调用尽量在一个专门的工作线程里完成,用
ConcurrentQueue来串行化指令。这一点非常重要。 - 程序退出时严格按顺序释放:先关Session,再释放相机列表,最后TerminateSDK。顺序反了容易出现进程无法退出或USB驱动异常。
- 如果调用
EdsGetPropertyData获取属性失败,先检查相机是不是处于某个特殊状态(比如正在实时取景、正在写入存储卡),再检查属性ID和数据类型是否匹配。
6. 扩展思路:EDSDK在实际项目中的更多玩法
EDSDK的能力边界远不止拍照和取景。我参与过的项目里,这些功能被用在完全不同的场景中:
- 证件照/自助拍照机:用户面对屏幕,程序控制相机实时取景,叠加人脸框,自动对焦,拍完自动裁切打印。这类设备在政务大厅、办证中心很常见,核心就是EDSDK + C# + 打印SDK的组合。
- 电商产品拍摄自动化:转盘转一下拍一张,程序控制相机、灯光、转盘联动,自动输出不同角度的素材。
- 视觉检测:虽然工业相机更常用,但单反的大靶面、高像素、丰富镜头群在某些精密检测(如纺织品纹理、大尺寸平面缺陷)中也有独特优势。
- 远程遥控拍摄:通过EDSDK把相机实时画面传到远程客户端,操作人员远程控制相机拍摄,常用于危险区域或野外监测。
- 延时摄影/定格动画:程序定时触发快门,自动生成序列帧,再合成视频。
如果你做的是长期运行的生产设备,我建议在你的C#程序里额外加一个看门狗机制,定期轮询相机是否还在线(比如每10秒调用一次EdsGetCameraInfo),如果掉线就尝试重连。USB连接在长时间高负载运行下确实可能出现偶发断开,不做自动恢复的话,产线一停就是损失。
还有一个容易被忽略的点:EDSDK虽然叫“EOS Digital SDK”,但部分PowerShot系列卡片机(比如早期的G系列)也支持一部分SDK功能,不过功能裁剪得比较多。如果你打算用佳能非EOS系列的机型做开发,一定要先查SDK的兼容性列表,不然枚举不到或者功能不可用就白忙了。
7. 最后再分享几点经验
根据我自己前后踩坑调通的经验,有几个建议想送给准备上手EDSDK的朋友。
第一,先跑通官方C#示例再动你自己的业务代码。EDSDK的坑有它的“性格”,很多问题在你对API还不熟悉时会觉得莫名其妙,但跑通官方Demo之后你会对它的工作方式有体感,后续再排查问题会快很多。
第二,不要直接在一个线程里乱调SDK。给SDK单独开一个工作线程,把程序里的控制指令通过队列发给它,SDK的回调在内部线程触发后,用SynchronizationContext或BeginInvoke切到UI层。一开始就这么设计,后期维护会轻松非常多。
第三,用日志记录每一次SDK调用的入参、出参和错误码。EDSDK的报错码是十六进制,不同版本的SDK对某个错误码的解释可能略有差异。把日志做好,现场出问题时你能快速定位是SDK层问题还是业务逻辑问题。我见过太多人卡在一个错误码上好几天,最后发现只是相机没开或者USB线松了。
第四点也是我的切身之痛:处理完DirectoryItem和Stream后,一定要记得释放引用。EDSDK是COM风格的对象管理,所有通过EdsGetXxx拿到的对象都要在不使用时调用EdsRelease。忘了释放短期看不出来问题,但跑一个长任务后内存涨得飞快,再排查就浪费时间了。
如果你之前只写过纯托管代码,第一次接触COM风格对象管理确实会有点不适应,但其实规则不复杂:谁拿到,谁释放。把这条规则贯彻好,EDSDK用起来会顺畅很多。
本文还有配套的精品资源,点击获取