简介:这份源码资源面向使用 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_Tv,ISO 是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 的问题往往不是立刻暴露的,而是跑了几十次之后才出现句柄耗尽。从那以后我每次动相机相关的代码都强制走一遍完整流程,希望帮到你。本文还有配套的精品资源,点击获取
简介:这份源码资源面向使用 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_Tv,ISO 是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 的问题往往不是立刻暴露的,而是跑了几十次之后才出现句柄耗尽。从那以后我每次动相机相关的代码都强制走一遍完整流程,希望帮到你。本文还有配套的精品资源,点击获取