
简介这份源码资源面向使用 C# 与 WinForms 开发相机控制程序的开发者重点解决佳能相机拍照、实时图像传输等功能的实现问题内含最新版 EOS SDK可直接对接 EDSDK.Dll 完成设备通信与图像回传。资源兼容 EOS-1D Mark III、40D、5D Mark II、7D、60D、550D 等多款佳能单反机型适合需要构建拍摄采集、远程控制或图像传输类桌面应用的中级开发者参考。压缩包共 1792 个文件约 134.97MB其中 1518 个 bin 与 104 个 dll 构成 SDK 运行库与依赖组件18 个 cs 源码文件展示核心调用逻辑另有 icc、icm 色彩配置文件及 csproj、sln 工程文件便于直接编译调试。目前已有 1330 人学习下载。读者可从中获取完整的相机连接、拍照触发与实时传输实现思路结合工程结构与 SDK 依赖快速搭建自己的采集程序减少从零摸索 SDK 接口的时间成本。1. 从一台吃灰的单反说起这套 C# 源码到底能帮你省掉多少事手里有台佳能单反想用程序控制它拍照、实时取景、拍完立刻把照片传到电脑上这事儿听起来简单真动手就知道坑有多深。官方给的 EOS SDK 是一堆 C 头文件和动态库文档写得像天书示例代码还是 MFC 时代的风格。你要在 C# 项目里调它得自己写 P/Invoke 封装处理回调、内存释放、错误码映射稍不留神就是内存泄漏或者相机卡死。这套源码干的就是这件事把佳能官方 EOS SDK 用 C# 完整封装了一遍提供相机连接、实时取景、拍照、文件传输、参数设置这些核心功能的可运行示例。它适合两类人一是做机器视觉、影楼联机拍摄、证件照采集这类需要程序控制相机的开发者二是想学 P/Invoke 和硬件 SDK 封装套路的 C# 工程师。源码里带了最新版 EOS SDK 的引用不用自己满世界找库文件。2. 技术选型与工程结构为什么是 C# 封装 EDSDK.Dll2.1 托管代码调非托管 SDK 的必然选择佳能 EOS SDK 本质是一组 C 风格的动态链接库核心是EdsInitializeSDK、EdsOpenSession、EdsSendCommand这些导出函数。C# 要调它们只有 P/Invoke 一条路。这套源码的做法是把所有DllImport声明集中在一个静态类里对外暴露符合 C# 习惯的托管接口。常见做法是建一个EDSDK静态类放原生方法声明再建一个Camera实例类包装会话生命周期。这样业务代码里看不到一个IntPtr全是camera.TakePhoto()这种调用。// EDSDK.cs - 原生方法声明集中管理 internal static class EDSDK { // 相机列表变化、属性变化等事件都通过这个回调进来 internal delegate uint EdsCameraEventCallback(uint event, IntPtr objectHandle, IntPtr context); [DllImport(EDSDK.dll, CallingConvention CallingConvention.StdCall)] internal static extern uint EdsInitializeSDK(); [DllImport(EDSDK.dll, CallingConvention CallingConvention.StdCall)] internal static extern uint EdsGetCameraList(out IntPtr cameraList); [DllImport(EDSDK.dll, CallingConvention CallingConvention.StdCall)] internal static extern uint EdsOpenSession(IntPtr camera); [DllImport(EDSDK.dll, CallingConvention CallingConvention.StdCall)] internal static extern uint EdsSendCommand(IntPtr camera, uint command, int param); [DllImport(EDSDK.dll, CallingConvention CallingConvention.StdCall)] internal static extern uint EdsRelease(IntPtr obj); }这里有几个关键点。CallingConvention.StdCall必须写对佳能的库用的是标准调用约定写成Cdecl会在某些机器上直接崩。返回值uint是 EDSDK 的错误码EDS_ERR_OK等于 0其他值都要查表翻译成可读信息。EdsRelease用来释放任何 EDSDK 返回的对象句柄漏调就是内存泄漏。2.2 工程目录与模块划分源码的目录结构大致是这样的CanonEDSDKWrapper/ ├── EDSDK/ │ ├── EDSDK.cs # 原生方法声明 │ ├── EDSDKErrors.cs # 错误码枚举与翻译 │ ├── EDSDKStructs.cs # 结构体定义 │ └── EDSDKConstants.cs # 常量属性ID、命令ID ├── Camera/ │ ├── Camera.cs # 相机会话管理 │ ├── CameraEvent.cs # 事件回调处理 │ └── CameraProperty.cs # 参数读写 ├── LiveView/ │ └── LiveViewStream.cs # 实时取景流 ├── Transfer/ │ └── ImageDownloader.cs # 图片下载 └── Demo/ └── MainForm.cs # WinForms 示例界面这个划分的逻辑是EDSDK目录只做底层映射不掺业务Camera目录管会话和状态LiveView和Transfer是两个独立的功能模块因为实时取景和文件传输在 EDSDK 里走的是完全不同的 API 路径。示例用 WinForms 是因为实时取景需要把相机画面渲染到控件上WinForms 的PictureBox改起来最直接。2.3 会话生命周期从连接相机到安全释放相机操作的第一步是初始化 SDK、拿到相机列表、打开会话。这套流程必须严格按顺序来顺序错了就是各种EDS_ERR_DEVICE_NOT_FOUND或者EDS_ERR_SESSION_ALREADY_OPEN。public bool Connect() { // 1. 初始化 SDK整个进程只需调一次 uint err EDSDK.EdsInitializeSDK(); if (err ! EDSDKErrors.EDS_ERR_OK) return false; // 2. 获取相机列表 IntPtr cameraList; err EDSDK.EdsGetCameraList(out cameraList); if (err ! EDSDKErrors.EDS_ERR_OK) return false; // 3. 取列表里第一台相机的句柄 int count; EDSDK.EdsGetChildCount(cameraList, out count); if (count 0) return false; IntPtr camera; EDSDK.EdsGetChildAtIndex(cameraList, 0, out camera); // 4. 打开会话之后所有操作都基于这个会话 err EDSDK.EdsOpenSession(camera); if (err ! EDSDKErrors.EDS_ERR_OK) return false; _cameraHandle camera; // 5. 注册事件回调相机状态变化会通知到这里 EDSDK.EdsSetCameraStateEventHandler(camera, EDSDKConstants.StateEvent_All, _eventCallback, IntPtr.Zero); return true; }参数说明EdsGetChildCount拿到的数量通常是 1除非你同时插了多台佳能相机。EdsSetCameraStateEventHandler的第二个参数StateEvent_All表示订阅所有状态事件包括快门按下、属性变化、对象添加等。回调函数里不能做耗时操作否则会阻塞相机通信线程常见做法是往一个队列里丢消息主线程轮询处理。释放的顺序和连接完全相反先关会话EdsCloseSession再释放相机句柄EdsRelease最后EdsTerminateSDK。漏掉任何一步下次连接就可能失败这是血泪经验。3. 核心功能拆解实时取景、拍照与文件传输的完整链路3.1 实时取景从相机传感器到 PictureBox实时取景是这套源码里最有价值的部分。EDSDK 的实时取景不是推流而是你主动去“拉”每一帧。流程是先设置Evf_OutputDevice属性把取景画面输出到电脑然后设置Evf_Mode为 1 开启实时取景之后循环调用EdsCreateEvfImageRef和EdsGetPointer拿到 JPEG 数据。public void StartLiveView() { // 开启实时取景模式 EDSDK.EdsSetPropertyData(_cameraHandle, EDSDKConstants.PropID_Evf_Mode, 0, 4, BitConverter.GetBytes(1)); // 指定输出到电脑 EDSDK.EdsSetPropertyData(_cameraHandle, EDSDKConstants.PropID_Evf_OutputDevice, 0, 4, BitConverter.GetBytes(EDSDKConstants.EvfOutputDevice_PC)); // 定时器每 33ms 拉一帧约 30fps _timer new Timer(PullFrame, null, 0, 33); } private void PullFrame(object state) { IntPtr evfImage; // 从相机取当前取景帧 uint err EDSDK.EdsCreateEvfImageRef(_cameraHandle, out evfImage); if (err ! EDSDKErrors.EDS_ERR_OK) return; // 下载帧数据到本地内存 err EDSDK.EdsDownloadEvfImage(_cameraHandle, evfImage); if (err ! EDSDKErrors.EDS_ERR_OK) { EDSDK.EdsRelease(evfImage); return; } // 拿到 JPEG 数据指针和长度 IntPtr stream; EDSDK.EdsGetPointer(evfImage, out stream); // 这里把 stream 转成 Bitmap 显示到 PictureBox // 注意用完必须 EdsRelease(evfImage) EDSDK.EdsRelease(evfImage); }关键参数PropID_Evf_OutputDevice的值EvfOutputDevice_PC是 2表示输出到电脑。EdsDownloadEvfImage是阻塞调用相机没准备好会返回EDS_ERR_OBJECT_NOTREADY这时候直接跳过这一帧就行不要当错误处理。帧率由定时器间隔决定33ms 对应约 30fps设太小相机会跟不上设太大画面卡顿。注意实时取景开启时相机会发热长时间使用建议加个散热风扇。另外不是所有佳能机型都支持实时取景老款入门机可能没有这个功能。3.2 拍照与文件传输两种模式的选择拍照有两种方式一种是EdsSendCommand发CameraCommand_TakePicture相机拍完存到卡里你再通过EdsDownload下载另一种是设置SaveTo为电脑拍完直接回调通知你下载。源码里两种都实现了用哪种看场景。public void TakePicture(bool saveToHost) { if (saveToHost) { // 拍完存电脑通过对象事件回调通知 EDSDK.EdsSetPropertyData(_cameraHandle, EDSDKConstants.PropID_SaveTo, 0, 4, BitConverter.GetBytes(EDSDKConstants.SaveTo_Host)); } // 发送拍照命令 uint err EDSDK.EdsSendCommand(_cameraHandle, EDSDKConstants.CameraCommand_TakePicture, 0); if (err ! EDSDKErrors.EDS_ERR_OK) { // 常见错误对焦失败、卡满、相机忙 throw new Exception(EDSDKErrors.GetErrorText(err)); } }SaveTo_Host模式下相机会在拍照完成后触发StateEvent_ObjectAdded事件事件参数里带着新对象的句柄。你在回调里拿到这个句柄调EdsGetDirectoryItemInfo获取文件名和大小再调EdsDownload下载到本地。private uint OnCameraEvent(uint eventType, IntPtr objectHandle, IntPtr context) { if (eventType EDSDKConstants.StateEvent_ObjectAdded) { // 新照片对象来了取信息 EDSDK.EdsDirectoryItemInfo info; EDSDK.EdsGetDirectoryItemInfo(objectHandle, out info); // 下载到本地 IntPtr stream; EDSDK.EdsCreateFileStream( Path.Combine(_saveDir, info.szFileName), EDSDKConstants.FileCreate_ForWrite, out stream); EDSDK.EdsDownload(objectHandle, info.size, stream); EDSDK.EdsDownloadComplete(objectHandle); // 释放资源 EDSDK.EdsRelease(stream); EDSDK.EdsRelease(objectHandle); } return EDSDKErrors.EDS_ERR_OK; }这里有个容易翻车的地方EdsDownload之后必须调EdsDownloadComplete否则相机会认为文件还在传输中后续操作全部阻塞。另外回调函数运行在 EDSDK 的内部线程上直接在里面更新 UI 会抛跨线程异常标准做法是用Control.Invoke切回主线程。3.3 参数读写光圈、快门、ISO 的远程控制相机参数通过EdsSetPropertyData和EdsGetPropertyData读写每个参数有对应的 PropID。光圈是PropID_Av快门是PropID_TvISO 是PropID_ISOSpeed。值不是直接的 f 值或秒数而是 EDSDK 定义的编码值需要转换。public void SetAperture(int avValue) { // avValue 是 EDSDK 编码值比如 f/2.8 对应 0x50 byte[] data BitConverter.GetBytes(avValue); uint err EDSDK.EdsSetPropertyData(_cameraHandle, EDSDKConstants.PropID_Av, 0, 4, data); } public int GetAperture() { byte[] data new byte[4]; uint err EDSDK.EdsGetPropertyData(_cameraHandle, EDSDKConstants.PropID_Av, 0, 4, data); return BitConverter.ToInt32(data, 0); }实际项目里一般会先调EdsGetPropertyDesc拿到该参数所有可用值的列表然后在 UI 上做成下拉框让用户选而不是硬编码。不同机型的可用光圈范围不一样硬编码必然出问题。4. 避坑与排查相机连不上、取景黑屏、传输卡死的真实原因4.1 相机连不上错误码 0x20 或 0x81现象是EdsOpenSession返回非零相机列表能拿到但会话打不开。原因通常是相机被其他程序占用了比如佳能官方 Utility 还开着或者上一次程序异常退出没释放会话。解决办法是先关掉所有可能占用相机的软件然后重启相机。如果还不行在代码里加一个EdsCloseSession的兜底调用程序退出时强制释放。4.2 实时取景黑屏但没报错现象是EdsDownloadEvfImage返回 OK但画面全黑。原因多半是相机模式不对——实时取景只在拍照模式M/Av/Tv/P下有效如果相机拨盘在视频模式或者 AUTO 模式取景数据就是黑的。解决方法是先通过EdsSetPropertyData把PropID_AEMode设成AEMode_Manual再开实时取景。4.3 拍照后文件传输卡死现象是EdsDownload一直不返回程序无响应。原因是EdsDownload是同步阻塞的大文件比如 RAW传输时间很长如果在 UI 线程调用就会卡界面。解决办法是把下载放到后台线程并且设置EdsSetCapacity确保电脑端有足够空间。另外传输过程中不要调其他 EDSDK 函数相机会忙不过来。4.4 程序退出后相机假死现象是程序关了但相机屏幕还显示“忙碌”重新插拔 USB 才能恢复。原因是会话没正常关闭相机还在等待电脑的指令。解决办法是在FormClosing事件里按顺序调EdsCloseSession、EdsRelease、EdsTerminateSDK一个都不能少。我一般还会加个try-finally确保异常时也能释放。4.5 多台相机同时连接时句柄混乱现象是插了两台相机程序只认到一台或者操作 A 相机影响了 B 相机。原因是EdsGetChildAtIndex的索引没管理好而且事件回调里没区分是哪个相机发来的。解决办法是给每个相机句柄建一个字典映射回调里通过context参数区分来源。这套源码的示例只处理单相机多相机场景需要自己扩展。5. 二次开发改造从示例到生产项目的几个关键调整拿到这套源码跑通之后真正用到项目里还得做几处改造。第一处是错误处理。示例里很多地方直接return false或者抛异常生产环境需要把 EDSDK 错误码翻译成用户能看懂的中文提示并且记录日志。我一般会建一个EdsErrorTranslator类把常见错误码映射成“相机未连接”“存储卡已满”“对焦失败”这类提示。第二处是实时取景的性能优化。示例用System.Windows.Forms.Timer拉帧在低配电脑上会掉帧。改成独立的后台线程加AutoResetEvent控制节奏帧率能稳定不少。另外每帧创建的Bitmap对象必须及时Dispose否则内存涨得飞快这是最容易翻车的地方。// 优化后的取景循环后台线程 对象复用 private void LiveViewLoop() { while (_isRunning) { _frameReady.WaitOne(33); IntPtr evfImage; if (EDSDK.EdsCreateEvfImageRef(_cameraHandle, out evfImage) ! EDSDKErrors.EDS_ERR_OK) continue; if (EDSDK.EdsDownloadEvfImage(_cameraHandle, evfImage) EDSDKErrors.EDS_ERR_OK) { // 复用同一个 Bitmap 对象避免频繁分配 UpdateFrame(evfImage); } EDSDK.EdsRelease(evfImage); } }第三处是断线重连。USB 线松动或者相机休眠都会导致会话断开示例里没有处理。生产项目需要监听StateEvent_WillSoonShutDown和StateEvent_ShutDown事件在断开时自动尝试重连并且把当前参数设置恢复到重连后的会话上。改造点示例做法生产建议错误处理返回 bool错误码翻译 日志取景线程WinForms Timer后台线程 对象复用断线处理无事件监听 自动重连多相机只支持单台句柄字典 上下文区分参数设置硬编码值动态获取可用值列表最后说个我自己的习惯每次改完 EDSDK 相关的代码我都会先用一台相机做完整的“连接-取景-拍照-传输-断开”流程测试确认没有资源泄漏再提交。因为 EDSDK 的问题往往不是立刻暴露的而是跑了几十次之后才出现句柄耗尽。从那以后我每次动相机相关的代码都强制走一遍完整流程希望帮到你。本文还有配套的精品资源点击获取