C#调用佳能EDSDK实现相机控制、拍照下载与实时取景

发布时间:2026/10/28 2:17:27
C#调用佳能EDSDK实现相机控制、拍照下载与实时取景 简介这份 C# 源码包面向需要用上位机软件控制佳能单反完成拍照与实时图像传输的开发者基于 WinForms 构建界面并内置最新版 EOS SDKEDSDK可直接用 Visual Studio 打开解决方案调试运行也便于在现有项目中二次封装。适配 EOS-1D Mark III、40D、5D Mark II、7D、60D 等经典型号适用于联机拍摄、工业检测等需程序化触发快门的场景。压缩包共 1792 个文件、约 134.97MB其中 18 个 .cs 承载拍照与传输的业务逻辑104 个 dll 与 1518 个 bin 提供 SDK 运行库与固件资源102 个 icc、6 个 icm 为相机色彩特性配置另含 sln、csproj 工程文件及 exe、pdb、txt 说明目录结构贴近真实 SDK 分发包便于按模块定位依赖。已有 1332 人学习下载。读者可获得相机枚举与连接、曝光参数设置、快门触发、图像实时回传保存的完整调用链示例以及 WinForms 界面与 SDK 事件回调的整合写法遇到设备识别或传输异常时也能对照代码与依赖配置排查。1. 一套能直接跑的 C# 相机控制源码去年帮一个做检测设备的团队改上位机需求很朴素按钮一按相机对焦、拍照图片立刻出现在界面上同时落盘归档整个动作不能超过两秒。他们一开始用的是通用的相机控制库结果发现部分机型只能拿到低分辨率预览拍完还得等相机自己写卡再想办法把文件捞出来。最后换成佳能官方的 EOS SDK用 C# 通过 P/Invoke 调 EDSDK.dll问题才算解决。这套源码就是把这条链路完整走通的一份实现相机枚举、会话管理、事件回调、快门触发、原图下载、实时取景预览都在里面压缩包还带了官方 SDK 本体。适合正在用 WinForm / WPF 做上位机、需要批量拍摄或实时预览的 C# 开发者也适合想搞清楚 EDSDK 到底怎么在托管环境里落地的人。2. 架构拆解原生 EDSDK 与托管封装的分层2.1 原生 DLL 与托管代码的边界在哪EDSDK 本身是一套纯 C 接口的动态库导出的是几百个EdsXxx形式的函数参数全是EdsCameraRef、EdsBaseRef这种不透明句柄和 C 结构体指针。它没有 COM 接口也没有 .NET 绑定所以 C# 想用只有一条路DllImport手工声明每一个要调用的函数再把句柄统一映射成IntPtr。这条边界决定了整个项目的组织方式——所有结构体定义、枚举常量、函数签名集中在一个静态类里业务逻辑层不直接出现DllImport。先看最基础的一层导入声明和错误码约定internal static class NativeMethods { // 库名不带路径靠输出目录下的 DLL 被系统加载器找到 private const string Dll EDSDK; [DllImport(Dll, CallingConvention CallingConvention.StdCall)] internal static extern uint EdsInitializeSDK(); [DllImport(Dll, CallingConvention CallingConvention.StdCall)] internal static extern uint EdsTerminateSDK(); [DllImport(Dll, CallingConvention CallingConvention.StdCall)] internal static extern uint EdsGetCameraList(out IntPtr cameraList); [DllImport(Dll, CallingConvention CallingConvention.StdCall)] internal static extern uint EdsOpenSession(IntPtr camera); [DllImport(Dll, CallingConvention CallingConvention.StdCall)] internal static extern uint EdsRelease(IntPtr obj); }CallingConvention.StdCall必须写死EDSDK 的导出约定是标准调用漏掉在某些情况下只是偶发栈失衡排查起来非常痛苦。所有函数返回值都是uint类型的EdsErrorEDS_ERR_OK为 0其余都是错误码绝不能忽略不管。提示DllImport的库名建议只写EDSDK而不要带.dll后缀和相对路径让运行时按标准搜索顺序去找这样 x86 和 x64 两个目录切换时不用改代码。2.2 源码目录与各类的职责划分按常见做法这套代码会拆成五块原生声明、SDK 单例封装、相机对象、取景器、界面。结构大致如下。文件职责是否可直接复用NativeMethods.cs全部 DllImport 声明、结构体、常量枚举是基本不用改Edsdk.csSDK 初始化/释放、错误码转异常、句柄包装是Camera.cs会话、属性读写、拍照、文件下载是业务钩子在这里加LiveView.cs取景开关、帧采集、位图转换是MainForm.cs界面、事件订阅、日志否按项目重写这样分的好处是界面层完全不知道IntPtr的存在。Camera类对外只暴露Connect()、TakePhoto()、SetIso(uint)这类方法内部把句柄、错误码、回调都吞掉。二次开发时你只需要关心Camera上挂了哪些事件。2.3 句柄生命周期谁创建、谁释放EDSDK 是手动引用计数模型凡是EdsGetXxx拿到的对象都要EdsRelease。相机列表、相机对象、目录项、流、取景器图像每一个都是独立计数。血泪经验是漏掉一个EdsRelease短时间看不出问题跑上一两个小时内存曲线就上去了相机端还可能因为对象堆积拒绝新请求。IntPtr list IntPtr.Zero; try { Check(NativeMethods.EdsGetCameraList(out list)); uint count; Check(NativeMethods.EdsGetChildCount(list, out count)); // ... 拿第一台相机 } finally { // 无论中间哪一步失败列表句柄都要还回去 if (list ! IntPtr.Zero) NativeMethods.EdsRelease(list); }Check是一个把错误码翻译成异常的小工具非 0 就抛EdsException带上错误码和出错函数名。这套写法覆盖了绝大多数失败分支比每步写if (err ! 0) return;清晰得多也让finally里的释放成为唯一出口。3. 会话管理从枚举相机到事件回调3.1 打开会话的完整调用链连接一台相机要按固定顺序走初始化 SDK、取相机列表、取列表里的子项、打开会话。顺序错了拿不到句柄比如没EdsInitializeSDK直接EdsGetCameraList会返回EDS_ERR_NOT_INITIALIZED。public void Connect() { Check(NativeMethods.EdsInitializeSDK()); IntPtr list; Check(NativeMethods.EdsGetCameraList(out list)); try { uint count; Check(NativeMethods.EdsGetChildCount(list, out count)); if (count 0) throw new EdsException(未检测到相机请检查 USB 连接与机身电源); // 取第 0 台多机场景改成按序列号匹配 Check(NativeMethods.EdsGetChildAtIndex(list, 0, out _camera)); Check(NativeMethods.EdsOpenSession(_camera)); // 会话建立后才能设置属性、注册回调 RegisterCallbacks(); SetSaveToHost(); // 关键让照片直接回传而不是只写相机存储卡 } finally { NativeMethods.EdsRelease(list); } }EdsOpenSession是分水岭之前的操作都是设备级的之后才能读写属性和触发拍摄。SetSaveToHost里做的是把kEdsPropID_SaveTo设成kEdsSaveTo_Host并调用EdsSetCapacity告诉相机主机侧有多少可用空间——这一步不做某些机型会一直往卡里写对象事件根本不会触发。3.2 回调的线程模型与委托保活EDSDK 的回调是在 SDK 内部的线程上触发的不是 UI 线程也不是你调用EdsSendCommand的那条线程。注册时传的是函数指针托管代码里就是委托。这里有个经典翻车点委托对象如果只作为局部变量传给EdsSetObjectEventHandler方法返回后没有任何强引用GC 一回收SDK 再回调就是野指针程序直接闪退日志里什么都留不下。// 字段级引用保证委托活到会话关闭 private NativeMethods.EdsObjectEventHandler _objectCb; // 拍照、文件创建都走这个 private NativeMethods.EdsPropertyEventHandler _propertyCb; private GCHandle _objectCbHandle; private void RegisterCallbacks() { _objectCb OnObjectEvent; _objectCbHandle GCHandle.Alloc(_objectCb); // 再钉一层双保险 Check(NativeMethods.EdsSetObjectEventHandler( _camera, NativeMethods.kEdsObjectEvent_All, Marshal.GetFunctionPointerForDelegate(_objectCb), IntPtr.Zero)); _propertyCb OnPropertyEvent; Check(NativeMethods.EdsSetPropertyEventHandler( _camera, NativeMethods.kEdsPropertyEvent_All, Marshal.GetFunctionPointerForDelegate(_propertyCb), IntPtr.Zero)); }kEdsObjectEvent_All表示订阅全部对象事件包括DirItemCreated、DirItemRequestTransfer、VolumeUpdate等。第三个参数是上下文指针这里传IntPtr.Zero因为单相机场景用不到多相机时可以把相机序号装箱成GCHandle传进去回调里再取回来避免全局变量。3.3 断线、关机与释放顺序相机拔线或者自动关机时EdsSetCameraStateEventHandler会收到kEdsCameraState_WillSoonShutDown之类的通知。如果不处理界面会一直停在「已连接」状态用户再点拍照就抛超时。释放顺序必须严格反着来先关会话再释放相机对象最后终止 SDK。少一步下次运行EdsInitializeSDK都可能返回EDS_ERR_DEVICE_BUSY只能拔插 USB 才好——这个坑我在调试阶段踩过不止一次。public void Disconnect() { try { if (_camera ! IntPtr.Zero) { NativeMethods.EdsCloseSession(_camera); NativeMethods.EdsRelease(_camera); _camera IntPtr.Zero; } } finally { if (_objectCbHandle.IsAllocated) _objectCbHandle.Free(); NativeMethods.EdsTerminateSDK(); // 放最后且必须执行 } }把释放写在窗体的FormClosing里还不够保险进程被强杀时照样残留。稳妥做法是同时挂AppDomain.CurrentDomain.ProcessExit两条路都走一遍Disconnect()。4. 拍照与下载快门触发到文件落盘4.1 两种快门下发方式的区别EDSDK 提供两种触发方式。kEdsCameraCommand_TakePicture是「直接拍」相机自己决定对焦适合快速抓拍kEdsCameraCommand_PressShutterButton带三个状态值Completely、Halfway、Off可以模拟半按对焦、确认合焦后再全按适合需要精确控制的场景比如拍静态样品。// 方式一一步到位 Check(NativeMethods.EdsSendCommand(_camera, NativeMethods.kEdsCameraCommand_TakePicture, 0)); // 方式二半按对焦 - 等待 - 全按 Check(NativeMethods.EdsSendCommand(_camera, NativeMethods.kEdsCameraCommand_PressShutterButton, NativeMethods.kEdsCameraCommand_ShutterButton_Halfway)); await Task.Delay(300); // 给对焦留时间具体值按镜头和光照调 Check(NativeMethods.EdsSendCommand(_camera, NativeMethods.kEdsCameraCommand_PressShutterButton, NativeMethods.kEdsCameraCommand_ShutterButton_Completely));EdsSendCommand的第三个参数是命令附加数据只有快门命令用得到。注意TakePicture是异步的——函数返回只代表命令发出去了真正拍完是对象事件通知你。4.2 对象事件里完成下载照片生成后SDK 在内部线程回调OnObjectEvent事件类型为kEdsObjectEvent_DirItemCreated或DirItemRequestTransfer第二个参数是目录项句柄。下载流程固定取信息、建流、下载、标记完成、释放、删相机端文件。private uint OnObjectEvent(uint inEvent, IntPtr inRef, IntPtr inContext) { if (inEvent ! NativeMethods.kEdsObjectEvent_DirItemCreated inEvent ! NativeMethods.kEdsObjectEvent_DirItemRequestTransfer) return 0; IntPtr item inRef; try { var info new NativeMethods.EdsDirectoryItemInfo(); Check(NativeMethods.EdsGetDirectoryItemInfo(item, out info)); string path Path.Combine(_saveDir, DateTime.Now.ToString(yyyyMMdd_HHmmss_fff) .jpg); IntPtr stream; Check(NativeMethods.EdsCreateFileStream(path, NativeMethods.kEdsFileCreateDisposition_CreateAlways, NativeMethods.kEdsAccess_ReadWrite, out stream)); try { Check(NativeMethods.EdsDownload(item, info.size, stream)); Check(NativeMethods.EdsDownloadComplete(item)); } finally { NativeMethods.EdsRelease(stream); } // 确认落盘成功后再删相机端否则卡很快写满 Check(NativeMethods.EdsDeleteDirectoryItem(item)); PhotoSaved?.Invoke(this, path); // 通知界面刷新注意这是 SDK 线程 } finally { NativeMethods.EdsRelease(item); } return 0; }EdsDirectoryItemInfo里的size是字节数必须原样传给EdsDownload传 0 拿到的就是空文件。EdsDownloadComplete不能省它是告诉相机「这个文件传完了可以清理」漏掉的话相机端会一直占着这个条目。PhotoSaved是在 SDK 线程上触发的界面上刷新缩略图必须BeginInvoke回 UI 线程直接赋值控件会抛跨线程异常。4.3 实时取景内存流加 JPEG 解码实时取景的本质是让相机把画面持续以 JPEG 帧推给主机主机每帧从内存流里读出来转成Bitmap。开启前要先设置输出设备关闭时要还原否则相机屏幕会黑掉。public void StartLiveView() { uint device NativeMethods.kEdsEvfOutputDevice_PC; Check(NativeMethods.EdsSetPropertyData(_camera, NativeMethods.kEdsPropID_Evf_OutputDevice, 0, 4, ref device)); _lvCts new CancellationTokenSource(); Task.Run(() LiveViewLoop(_lvCts.Token)); } private void LiveViewLoop(CancellationToken token) { while (!token.IsCancellationRequested) { IntPtr stream, evfImage; Check(NativeMethods.EdsCreateMemoryStream(0, out stream)); try { Check(NativeMethods.EdsCreateEvfImageRef(stream, out evfImage)); try { if (NativeMethods.EdsDownloadEvfImage(_camera, evfImage) ! 0) { Thread.Sleep(30); // 相机还没准备好跳过本帧 continue; } IntPtr ptr; Check(NativeMethods.EdsGetPointer(stream, out ptr)); uint len; Check(NativeMethods.EdsGetLength(stream, out len)); byte[] buf new byte[len]; Marshal.Copy(ptr, buf, 0, (int)len); using var ms new MemoryStream(buf); FrameReady?.Invoke(this, new Bitmap(ms)); // 界面侧负责 Dispose } finally { NativeMethods.EdsRelease(evfImage); } } finally { NativeMethods.EdsRelease(stream); } } }EdsCreateMemoryStream(0, ...)里的 0 表示长度由 SDK 自己增长。EdsDownloadEvfImage返回非 0 通常只是这一帧还没准备好不要当致命错误抛异常跳过继续循环即可。帧率取决于机型和解码耗时普通 USB 连接下 2030 fps 是正常水平Bitmap的构造和释放放在 UI 线程做SDK 线程只负责搬运字节。注意LiveView 期间不要频繁切换属性ISO、光圈部分机型会返回忙状态导致取景中断切换后重新走一次EdsDownloadEvfImage就能恢复。5. 避坑与排查五个真实翻车现场5.1 提示找不到 DLL 或位数不匹配现象是启动即抛DllNotFoundException或BadImageFormatException后者尤其容易被误判成「DLL 损坏」。原因是 EDSDK 的原生库分 32 位和 64 位两套和 C# 项目的PlatformTarget必须一致另外默认情况下 DLL 不会随生成自动复制到bin目录。解决办法有两个一是把对应位数的EDSDK.dll、EdsImage.dll拷到输出目录并在 csproj 里配置NoneCopyToOutputDirectory二是项目固定x64绝不用AnyCPU避免在 64 位系统上被 JIT 成 32 位进程。5.2 跑十几分钟后无征兆闪退现象是程序稳定运行一段时间后直接退出Windows 事件日志里只有一句「应用程序错误」没有任何 .NET 异常。原因是事件回调的委托没有强引用GC 回收后 SDK 内部线程调到已释放的地址。解决办法就是前面说的把委托声明成字段再用GCHandle.Alloc钉住Disconnect时统一Free。这条经验适用于所有把托管委托传给原生回调的场景。5.3 拍照成功但文件夹是空的现象是相机快门响了界面没图目录里也没有文件。三个原因按概率排序一是没注册kEdsObjectEvent_All事件压根没进来二是kEdsPropID_SaveTo没设成kEdsSaveTo_Host照片只写了相机存储卡三是EdsSetCapacity没调用相机认为主机没有空间。排查方法是打开 SDK 的日志开关看事件回调有没有被触发再逐项核对属性值。5.4 界面卡死或抛跨线程异常现象是拍照瞬间界面冻结或者弹「线程间操作无效」。原因是PhotoSaved、FrameReady这些事件都在 SDK 内部线程上触发直接操作控件就会出问题。解决办法统一走Control.BeginInvoke不要用Invoke——Invoke会同步阻塞 SDK 线程一旦 UI 线程正在等相机响应双方互等就是死锁。5.5 程序退出后再也连不上相机现象是关掉程序重新打开EdsInitializeSDK返回EDS_ERR_DEVICE_BUSY必须拔插数据线。原因是上一次进程没有走EdsCloseSession和EdsTerminateSDK相机端会话还没断。解决办法是把释放逻辑放进try/finally同时在ProcessExit里再兜一次调试期间频繁「停止调试」最容易触发建议调试时也走正常关闭流程。6. 二次开发把回调改造成可测试的流水线拿到源码后最该动的不是界面而是把 SDK 回调从「直接干活」改成「发事件、排队处理」。原始代码里OnObjectEvent直接写文件、直接通知界面逻辑耦合在一起加一个上传云端的动作就得改回调体风险大。我的做法是回调只做两件事把文件下载到本地临时目录然后往Channelstring里塞一个路径。消费端独立成Task负责重命名、归档、上传、刷新 UI任何一环失败都不影响拍摄主流程。// 回调里只投递不做耗时操作 private readonly Channelstring _photoQueue Channel.CreateBoundedstring(new BoundedChannelOptions(64) { FullMode BoundedChannelFullMode.DropOldest }); // 消费端独立线程失败可重试可替换为上传逻辑 private async Task ConsumeAsync(CancellationToken ct) { await foreach (var path in _photoQueue.Reader.ReadAllAsync(ct)) { try { await _archive.MoveAsync(path); OnPhotoReady(path); } catch (Exception ex) { _log.Error($归档失败 {path}, ex); } } }第二个改造点是相机参数。ISO、光圈、快门速度都通过EdsSetPropertyData写值的类型是uint写之前建议先EdsGetPropertyData读一次确认范围越界写入会返回EDS_ERR_INVALID_PARAMETER而不是被自动截断。做多档位拍摄时把参数组合抽成一个ShotProfile类循环下发再拍比在界面上手点可靠得多。第三个点是可测试性。相机不在手边时没法联调所以我把Camera抽象成接口TakePhoto、StartLiveView这些方法用 Mock 实现返回一张本地图片。界面层的代码在无设备状态下也能跑通、能验证布局和异常处理等真机接上只换一行注册代码。这套做法省下的时间比我花在它上面的时间多得多。从那以后我每次接这类原生 SDK 的项目都强制先做三件事把句柄释放路径写完整、把委托保活写成字段、把回调线程和 UI 线程的边界画清楚。这三步做完剩下基本就是业务堆砌了。希望帮到你。本文还有配套的精品资源点击获取