简介:这套源码面向C#桌面开发者,基于WinForms与最新版EOS SDK实现佳能相机联机拍照、实时传输和参数控制,适合将EOS相机接入Windows业务系统、机器视觉采集或摄影工作流的项目参考。压缩包共1792个文件、约134.97MB,包含18个cs源码、104个dll运行库与SDK组件、1518个bin固件及资源文件,并配有icc/icm色彩配置、csproj/sln工程文件和说明文本,目录保留了完整示例与依赖结构。包内附相机兼容列表,覆盖EOS-1D Mark III、40D、5D Mark II、7D、60D、550D等多代机型,便于核对硬件与接口调用。已有1332人学习下载,可研究EDSDK在C#中的封装方式、会话连接、拍照回调与实时取景传输流程,也能作为WinForms相机控制界面的二次开发基础。
1. 一个 0x8007007E 的开场
上门调试一套产线拍照归档工具,插上相机,EdsInitializeSDK返回 0,我以为成了,结果EdsGetChildCount拿到的相机数是 0。换线、换口、重启机器,折腾到下午才发现进程是 AnyCPU 勾了首选 32 位,实际跑在 x86 下,而输出目录里那份是 64 位的EDSDK.dll。C# 调佳能相机这条链路就是这样:官方只给 C 头文件和原生库,没有托管封装,得自己在 C# 里对 EDSDK.dll 做 P/Invoke,位数、搜索路径、回调生命周期、相机占用,任何一环对不上,报出来的都不是「你配错了」,而是「没找到设备」或者一个十六进制错误码。下面按我实际落地的顺序,从选型、装库、开会话,一路写到能连续跑两百张不掉线。
2. 环境选型:EDSDK 位数、.NET 运行时和相机侧三件事
装之前先把三件事定死,能省掉后面一大半的返工。顺序是:先定进程位数,再定运行时,最后确认相机那一侧没被别的东西占着。这三件事任何一件回头改,代码基本都要重调一遍。
2.1 位数一次定死,别留 AnyCPU
EDSDK 的 Windows 包解压后是两份原生库,32 位一份、64 位一份,文件名一样,靠目录区分。不同版本的目录命名略有差别,但找EDSDK.dll就够了,看它躺在一层还是两层子目录里就知道哪份是 64 位。我的做法是两份都留在仓库里,按平台分目录放,构建时只把对应那份拷进输出目录。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <!-- 关键:平台锁死。AnyCPU 在 32 位宿主里会直接翻车 --> <PlatformTarget>x64</PlatformTarget> <Prefer32Bit>false</Prefer32Bit> <Platforms>x64;x86</Platforms> <TargetFramework>net8.0-windows</TargetFramework> </PropertyGroup> <ItemGroup> <!-- Link 把 DLL 平铺到输出根目录,DllImport("EDSDK.dll") 才找得到 --> <None Include="..\lib\EDSDK\x64\*.dll"> <Link>%(Filename)%(Extension)</Link> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup> </Project>PlatformTarget写死 x64 之后,即使开发机是 64 位系统也不会被 AnyCPU 的自动决策带偏,尤其是这套代码将来被塞进 Office 加载项、IIS 应用池或者某个 32 位宿主进程里的时候,AnyCPU 的坑会以「本机好好的,客户现场一装就崩」的形式出现。Link这个元数据容易被忽略:不写它,文件会按原相对路径落进bin\Debug\net8.0-windows\..\lib\EDSDK\x64\这种奇怪的层级,P/Invoke 的默认探测只认输出根目录和系统路径,照样找不到。
改完编译,拿dumpbin /headers或者随便一个 PE 查看工具确认:进程是 x64,EDSDK.dll的 machine 也是 x64,两边对齐再往下走。这一步花两分钟,比后面看三小时错误码划算。
2.2 运行时版本:Framework 4.8 还是 .NET 8
两套都能跑,取舍点不在 API,而在交付环境和原生库加载方式。
| 运行时 | 适合的场景 | 要注意的地方 |
|---|---|---|
| .NET Framework 4.7.2 / 4.8 | 交付到离线产线机、Windows 自带无需装运行时 | 委托保活靠静态字段或GCHandle,没有NativeLibrary |
| .NET 6 / 8(-windows) | 自托管采集服务、需要 async 与后台任务 | 单文件发布要解包原生库,默认探测路径不含子目录 |
| .NET 8 AOT | 不建议用于这套 | 非托管回调与委托封送在 AOT 下限制较多 |
我给产线机器交付时一般选 .NET Framework 4.8,理由很土:那批机器装不了新运行时,而 4.8 是系统组件,开机就有。自己写采集服务、要跑后台队列和异步下载的,用 .NET 8 更顺手,但要补一段原生库解析:
// .NET 6+:DllImport 默认不去 runtimes/native 子目录找,自己接管解析 static Eds() { NativeLibrary.SetDllImportResolver(typeof(Eds).Assembly, (name, asm, path) => { if (!name.Equals("EDSDK.dll", StringComparison.OrdinalIgnoreCase)) return IntPtr.Zero; // 交回默认逻辑 var full = Path.Combine(AppContext.BaseDirectory, "native", "x64", name); return NativeLibrary.Load(full); // 找不到会抛,别吞异常 }); }这段只能设一次,放静态构造函数里刚好。另外单文件发布时要把原生库设成不压缩、运行时解包到临时目录,否则进程启动瞬间解包没完成,第一次调用就报「找不到指定模块」。
2.3 相机那一侧的前置条件
代码没错但拿不到设备,十次里有六次是相机侧的问题。我按顺序排查:先确认走的是 USB 数据线而不是只能充电的线,再确认相机没有休眠(机身省电设置里把自动关机调长或者关掉),然后关掉随相机附带的那套遥控/导入工具,它们会抢先建立会话,表现就是EdsOpenSession返回设备忙。Windows 有时会把相机当普通便携设备挂载,系统驱动先接管,这种时候换一个 USB 口、让它重新枚举一次通常就恢复了。
还有两个容易忘的:相机的模式拨盘别停在全自动或场景模式,SDK 改曝光参数会直接返回不支持;供电不足的扩展坞会让相机在连续拍摄时掉线,长任务尽量插主板后置口。这些都不算配置项,但每一条都能让一套本来正确的代码跑不起来。
3. SDK 落地:从 DLL 目录到第一次 EdsOpenSession
环境定完,进到真正把库摆进工程、把会话开起来的环节。这一章的四步是有顺序依赖的:目录不对后面全废,句柄不释放跑不长,保存路径设晚了图会落到卡里。
3.1 目录先摆正,DLL 整目录拷
我不去挑哪几个 DLL 是必需的,整目录拷是最省事的做法,因为不同版本的 EDSDK 拆分的模块不完全一样,漏一个依赖的表现往往是初始化成功、某个功能一调就崩。
repo/ lib/ EDSDK/ x64/ <- 64 位原生库,整目录 x86/ <- 32 位原生库,整目录 src/ Interop/ Eds.cs <- P/Invoke 声明 EdsTypes.cs <- 常量与结构体 Camera/ CameraSession.cs托管侧自己写一层Eds静态类,把DllImport集中在一个文件里,后面换 SDK 版本只改这一处。结构体和常量单独一个文件,从 SDK 附带的头文件里原样抄,别凭记忆敲数值——SDK 每个版本都会补常量,抄错一个属性 ID,返回的就是「不支持」。
3.2 P/Invoke 声明与调用约定
internal static class Eds { private const string Dll = "EDSDK.dll"; // 与输出目录文件名完全一致,含大小写 [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 EdsGetChildCount(IntPtr @ref, out int count); [DllImport(Dll, CallingConvention = CallingConvention.StdCall)] internal static extern uint EdsGetChildAtIndex(IntPtr @ref, int index, out IntPtr item); [DllImport(Dll, CallingConvention = CallingConvention.StdCall)] internal static extern uint EdsRelease(IntPtr @ref); [DllImport(Dll, CallingConvention = CallingConvention.StdCall)] internal static extern uint EdsOpenSession(IntPtr camera); [DllImport(Dll, CallingConvention = CallingConvention.StdCall)] internal static extern uint EdsCloseSession(IntPtr camera); }调用约定在 Windows 上就是StdCall,和头文件里的导出宏一致。网上有些封装抄成了Cdecl,症状是调用返回后栈失衡、进程随机退出,很多人会怀疑相机固件,其实是这一行。返回值统一是uint,0 表示成功,非 0 就是错误码,靠一个Check方法集中处理:
private static void Check(uint err) { if (err != 0) throw new InvalidOperationException($"EDSDK 调用失败,错误码 0x{err:X8}"); }错误码的十六进制值对照 SDK 头文件里EDS_ERR_开头的常量去查,不要靠猜。
3.3 初始化、枚举、开会话
Check(Eds.EdsInitializeSDK()); IntPtr list; Check(Eds.EdsGetCameraList(out list)); Eds.EdsGetChildCount(list, out int count); if (count == 0) { Eds.EdsRelease(list); throw new InvalidOperationException("没有枚举到相机,先查连接和占用"); } IntPtr camera; Check(Eds.EdsGetChildAtIndex(list, 0, out camera)); Eds.EdsRelease(list); // 列表用完立刻释放,别留到会话关闭时 Check(Eds.EdsOpenSession(camera));注意EdsGetChildAtIndex拿到的camera是一次引用计数加一,整个生命周期里都得留着,直到EdsCloseSession之后再EdsRelease。list是临时对象,拿到相机就放掉。所有out IntPtr的返回值都是这个规律,漏一个就是长期运行后的句柄泄漏,短测看不出来,跑两小时就开始莫名报错,属于典型的黑匣子故障。
3.4 保存路径:先把图收到主机,再谈落盘
默认情况下相机拍完是往存储卡里写的,程序这边只能事后读卡。要实时拿到图,必须把保存目标改成主机:
const uint PropIDSaveTo = 0x0000000B; // kEdsPropID_SaveTo,数值照 EDSDKTypes.h 抄 const uint SaveToHost = 0x00000002; // kEdsSaveTo_Host uint saveTo = SaveToHost; Check(Eds.EdsSetPropertyData(camera, PropIDSaveTo, 0, 4, ref saveTo)); var cap = new EdsCapacity { NumberOfFreeClusters = 0x7FFFFFFF, // 给足空间,避免相机判定无容量而拒绝拍 BytesPerCluster = 0x1000, Reset = 1 // 1 表示重置剩余容量统计 }; Check(Eds.EdsSetCapacity(camera, cap));EdsSetPropertyData官方签名最后一个是void*,我这里用ref uint固定四字节,属性值都是整数时够用;改成字符串或结构体时换成IntPtr配Marshal.AllocHGlobal,用完必须FreeHGlobal。顺序不能反:先设SaveTo再设Capacity,反了之后某些机型仍然往卡里写,程序这边等半天等不到图。EdsSetCapacity传的是结构体本身而不是指针,封送时字段顺序和类型必须和头文件严格一致,差一个ulong→`
本文还有配套的精品资源,点击获取