简介:一套针对佳能EOS系列相机的C#控制源码,基于官方EDSDK开发,使用WinForms界面,可实现相机连接、实时取景传输、拍照控制等功能。包内内置最新版EOS SDK及相应DLL,适配EOS 5D Mark II、7D、60D、450D等多款单反,适合做相机采集、自动化拍摄或相机控制软件二次开发的.NET工程师。压缩包共1792个文件,约134.97MB,包含18个C#源文件及工程文件、EDSDK运行所需的DLL/EXE/PDB组件、1518个相机固件BIN、102个ICC色彩配置文件,另有VS解决方案和辅助文件,结构完整便于调试。发布至今已有1329人学习下载,读者可从中了解EDSDK调用流程、相机事件处理、实时图像显示与保存等实现,并可直接基于现有工程修改扩展,节省从零对接SDK的时间,适合作为相机控制项目的起步框架。
1. 用 C# 控制某相机大厂的 EOS 机型:拍照、实时取景与图像传输的落地思路
在自动检测工位上,我接过一个需求:上位机要控制两台相机轮流拍照、画面实时显示、拍完的原始图 1 秒内落盘,还要能远程改曝光参数。第一反应是用常见相机厂商 SDK 做二次开发,结果发现通用方案在快门同步、RAW 数据支持和参数写入上都跟不上,最后只能切到相机大厂官方提供的 EOS SDK 上。这份资源就是一套 C# 工程源码,核心是针对 EDSDK.Dll 做托管封装,覆盖初始化连接、拍照触发、图像传输、实时取景这条完整链路,并且已内置最新版 SDK 运行文件,省去自己去官网核对新旧版本差异的功夫。适合正在做视觉检测、自动化拍摄台、延时摄影采集的开发者,也适合第一次碰原生相机 SDK 的人作为能跑起来的最小示例。
2. EDSDK 初始化与通信机制:为什么直接拍一张图却拿不到文件
2.1 为什么选官方 EOS SDK:从线程模型和底层协议说起
很多 C# 开发者第一反应是找免驱的通用摄像头方案。普通 UVC 摄像头走的是标准视频流,拿到的是压缩或未压缩的彩色图像,既控制不了机身快门、感光度、白平衡,也拿不到 RAW 原始格式,更没有快门同步信号,在自动检测场景里很难和工位上的气动夹具做精确联动。那类方案只适合做简单监控,不适合做工业拍摄。
某相机大厂的 EOS 系列则完全不一样。它通过 EDSDK.Dll 暴露的是一套面向底层硬件的控制 API,交互链路里有一个独立的传输协议层。调用方发送命令,相机固件反馈状态和图像数据,响应通过事件回调异步通知。这个不确定的异步过程恰恰是新手最容易翻车的地方,如果像调用普通函数一样去拿返回值,十次里有九次拿不到图。
官方推荐的事件模式是:应用注册回调,SDK 内部在它的工作线程上触发事件,你的代码再把事件内容转发到 UI 线程去更新界面,或者存入队列交给后台任务去消费。问题就在这里——不少人在回调线程里直接执行文件写入、日志输出甚至 UI 刷新,结果就是同一台相机上同时跑着取景、拍照、下载三个任务时,整个界面卡死。
我一般会在封装层的最外层套一个事件聚合器,把所有来自 SDK 的回调统一挂到一个并发队列上,再由独立线程去消费。这个结构和后面要讲的避坑内容直接相关,如果你打算直接在 UI 刷新逻辑里执行相机命令,就会遇到回调阻塞的问题。说白了,用 EDSDK 之前先把线程模型搞清楚,后面的功能才不会重复踩同一个坑。
2.2 SDK 目录结构:EDSDK.Dll 与依赖文件必须一起带
拿到这份资源后,先别急着编译,看一眼 SDK 目录的完整结构。官方 SDK 包里通常包含头文件、库文件、运行库和 API 文档,而 C# 工程里真正要引用的不一定只有 EDSDK.Dll 一个。在 64 位系统上,你经常要同时保留 32 位和 64 位两套运行库,因为有些老机型驱动只带了 32 位版本,而新一代相机已经全面切到 64 位调用链。
资源里的目录结构一般是这样的:
SDK/ ├── x64/ │ ├── EDSDK.Dll │ ├── EdsImage.dll │ └── DPPRSC.Dll ├── x86/ │ ├── EDSDK.Dll │ ├── EdsImage.dll │ └── DPPRSC.Dll └── EDSDK_CSharp_Header.cs这段目录结构解决了两件事。第一,DLL 的位数必须和进程平台一致,否则加载时会抛 BadImageFormatException。第二,EDSDK.Dll 依赖另外两个运行库,单独拷贝主 DLL 到 bin 目录是不行的。我见过有人只复制了 EDSDK.Dll 一个文件到输出目录,初始化方法返回设备列表为空的错误码,排查半天才发现是依赖库缺失。
在工程引用层面,右键项目点添加引用,直接浏览选中对应平台的 EDSDK.Dll。如果你用的是 .NET 6 或更高版本的项目文件,建议把 DLL 放在工程根目录的 native 文件夹下,在 csproj 里用 Content 节点标记为复制到输出目录,方便部署时随程序一起带走。换机器部署不用重新配全局搜索路径,这是实际项目里最省事的方式。
2.3 初始化与会话建立:一套不会漏资源的标准写法
EDSDK 的使用流程可以拆成四个阶段:初始化 SDK、枚举相机、建立会话、最后释放。几乎所有的资源泄漏都出在建立会话后忘了释放。下面给出一个小项目里常用的最小模板:
public class CameraSession : IDisposable { private IntPtr _cameraHandle; private bool _disposed; public bool Initialize() { // 1. 初始化 SDK 运行环境 uint err = Edsdk.EdsInitializeSDK(); if (err != EdsError.EDS_ERR_OK) { Console.WriteLine($"初始化失败,错误码:0x{err:X8}"); return false; } try { // 2. 枚举已连接的相机列表 IntPtr cameraList; err = Edsdk.EdsGetCameraList(out cameraList); if (err != EdsError.EDS_ERR_OK || cameraList == IntPtr.Zero) return false; // 3. 取第一台相机,拿到相机句柄 err = Edsdk.EdsGetChildAtIndex(cameraList, 0, out _cameraHandle); if (err != EdsError.EDS_ERR_OK) return false; // 4. 建立会话 err = Edsdk.EdsOpenSession(_cameraHandle); return err == EdsError.EDS_ERR_OK; } catch (Exception ex) { Console.WriteLine($"建立会话异常:{ex.Message}"); return false; } } public void Dispose() { if (_disposed) return; // 释放顺序:先关会话,再释放相机对象,最后反初始化 SDK if (_cameraHandle != IntPtr.Zero) { Edsdk.EdsCloseSession(_cameraHandle); Edsdk.EdsRelease(_cameraHandle); _cameraHandle = IntPtr.Zero; } Edsdk.EdsTerminateSDK(); _disposed = true; } }这段代码的动作顺序是有讲究的。EdsInitializeSDK 是全局入口,正常只能调用一次,多线程环境下也不要重复初始化。EdsGetCameraList 拿到的是一个相机列表句柄,本质上是 COM 风格的对象引用,使用完必须释放,否则会累积句柄。所以在会话关闭时统一走完释放流程,把相机列表、相机对象和 SDK 环境全部清干净。
补充一个细节:错误码不要只看中文翻译,建议都转成十六进制打印。EDSDK 的错误码每位段都有含义,比如 0x0000A100 这类段位里的数值直接对应官方文档里的错误类型表。调试阶段把错误码记下来,再去查文档能省不少时间。
2.4 事件回调注册:委托生命周期和线程切换是重灾区
事件注册是 EDSDK 使用中最容易搞混的环节。相机的拍照完成、取景画面的每一帧到来、设备移除,全部通过回调触发。官方提供的注册函数是 EdsSetObjectEventHandler 和 EdsSetPropertyEventHandler,一个负责对象事件(比如图像文件生成),一个负责属性变化(比如感光度、光圈变化)。
按照官方推荐写法,注册回调需要传入委托实例。注意,这个委托必须保存在类的成员字段里,不能写成方法内局部变量,否则会被垃圾回收器在回调还没触发时回收掉,然后出现一个莫名其妙的空引用异常。我封装时就专门留了一个字段长期引用委托,避免被 GC 回收。
// 保存委托引用,防止被 GC 回收 private EdsObjectEventHandler _objectEventHandler; public void RegisterEvents(IntPtr cameraHandle) { _objectEventHandler = (inEvent, inRef, inContext) => { if (inEvent == EdsObjectEvent.EdsObjectEvent_ImageCaptured) { // 图像已拍摄完成,相机内部已生成临时文件 // 将相机对象引用排队,交给后台线程去下载 _downloadQueue.Enqueue(inRef); } }; uint err = Edsdk.EdsSetObjectEventHandler( cameraHandle, EdsObjectEvent.EdsObjectEvent_All, _objectEventHandler, IntPtr.Zero); }这段代码最关键的一行是_downloadQueue.Enqueue(inRef)。事件回调发生在 SDK 自己的工作线程上,下载图像涉及文件操作和存储写入,耗时可能在几百毫秒到几秒,如果在回调里直接同步执行,会让 SDK 内部线程阻塞,后续的实时取景帧、按钮事件全都会被卡住。常见做法是只做入队动作,让后台任务慢慢处理。
还有一点,inRef 是相机传出来的文件引用,用完后要调用 EdsRelease 释放,如果漏了,每拍一张图就泄漏一个句柄,拍几百张之后系统会明显变卡。上面这段代码里没有释放,实际项目中我会在下载完成后立刻调 EdsRelease。这个点后面避坑章还会再说。
3. 从按下快门到图像落地:拍照、实时取景与数据回传的实现
3.1 拍照指令只是半程指令:TakePicture 之后还得等事件
很多人拿到 EDSDK 后的第一个动作就是找拍照 API,然后发现调用起来异常简单,但拍完却找不到文件。原因在于 EdsSendCommand 只是一个单向命令,发出后立刻返回,相机在完成拍摄后通过事件通知你,你收到通知之后才能真正把图下载到本地。中间隔了少则几百毫秒,多则一两秒。
public bool TakePhoto() { // 拍照前先确认相机处于可拍摄状态 if (_cameraHandle == IntPtr.Zero) return false; // 清空下载队列,防止旧事件干扰本次判断 _downloadQueue.Clear(); uint err = Edsdk.EdsSendCommand( _cameraHandle, EdsCameraCommand.EdsCameraCommand_TakePicture, 0); if (err != EdsError.EDS_ERR_OK) { Console.WriteLine($"拍照指令失败:0x{err:X8}"); return false; } Console.WriteLine("拍照指令已发送,等待相机返回事件..."); return true; }拍照指令发出后,相机会在内部生成一个临时文件,然后在 ImageCaptured 事件里把文件引用传出来。整个链路是:触发命令 → 相机固件响应 → 文件生成 → 事件回调 → 下载文件。任何一步出问题都会导致你拍完看不到内容。
这里面值得注意的参数是第三位的 0,对应命令参数,通常拍照时填 0 就行,但如果是驱动快门线之类的特殊功能,这里需要填对应的属性值。在封装层里我会把拍照、下载拆成两个独立方法,拍照只管发命令,下载挂到事件回调里,这样职责清楚,后面做批量队列调度时会轻松很多。
3.2 图像下载与落盘:分块读取避免长时间阻塞
当 ImageCaptured 事件触发时,inRef 指向的是一个目录项对象,需要先读取它的文件信息,再创建目标流,然后执行下载。实测发现,大分辨率 RAW 文件可能有几十 MB,如果一次性读进内存会占太多空间,我一般会分块处理。
private void DownloadImage(IntPtr dirItemRef, string saveDir) { // 1. 获取文件信息,拿到文件名和大小 var info = new EdsDirectoryItemInfo(); uint err = Edsdk.EdsGetDirectoryItemInfo(dirItemRef, out info); if (err != EdsError.EDS_ERR_OK) { Console.WriteLine($"读取文件信息失败:0x{err:X8}"); Edsdk.EdsRelease(dirItemRef); return; } string savePath = Path.Combine(saveDir, $"{DateTime.Now:yyyyMMdd_HHmmss_fff}_{info.szFileName}"); // 2. 创建目标文件流 IntPtr streamRef; err = Edsdk.EdsCreateFileStream( savePath, EdsFileCreateDisposition.EdsFileCreate_Always, out streamRef); // 3. 执行下载,把相机文件写入流 err = Edsdk.EdsDownload(dirItemRef, (uint)info.uSize, streamRef); // 4. 完成下载并关闭流 If (err == EdsError.EDS_ERR_OK) { Edsdk.EdsDownloadComplete(dirItemRef); Console.WriteLine($"图像已保存:{savePath}"); } else { Console.WriteLine($"下载失败:0x{err:X8}"); } Edsdk.EdsClose(streamRef); Edsdk.EdsRelease(dirItemRef); }这段代码里 EdsDownload 是同步方法,会阻塞当前线程直到传输完成。如果你放在事件回调线程里执行,就会遇到前面说的卡顿问题,所以在我的封装里是把它丢到后台任务队列里执行。EdsDownloadComplete 是必需的一步,它告诉相机缓存可以释放了,漏掉的话相机的内部缓冲区会被占满,拍个几十张就再也不出图了。
还有一个容易忽略的点:保存路径的目录要提前创建好。我自己就翻车过一次,相机拍完回车电脑下载时才发现目录不存在,下载步骤直接报错,相机端的事件已经被消费掉,图也没了。后来统一在会话初始化时先把保存目录检查一遍,省了很多麻烦。
3.3 实时取景:从开启到每一帧的回传
实时取景和拍照是两套独立链路。拍照走的是“命令 + 文件事件”模式,而实时取景走的是“属性设置 + 帧事件回调”模式。开启实时取景前需要先设置 EVF 模式,把相机切到“电脑实时显示”状态,再发送 Evf Start 命令。
public bool StartLiveView() { // 设置相机取景模式为电脑实时显示 uint evfMode = (uint)EdsEvfMode.EdsEvfMode_Desktop; uint err = Edsdk.EdsSetPropertyData( _cameraHandle, EdsPropertyID.EdsPropID_Evf_Mode, 0, sizeof(uint), ref evfMode); // 启动实时取景 err = Edsdk.EdsSendCommand( _cameraHandle, EdsCameraCommand.EdsCameraCommand_Evf_Start, 0); return err == EdsError.EDS_ERR_OK; }实时取景帧通过另一个事件回调送出来。每一帧是一张 JPEG 压缩图,分辨率看你设置的取景尺寸,常见的是 1024×680 左右。拿到帧数据后,转换成 Bitmap 显示到界面上。
private void OnEvfFrame(IntPtr evfImageRef) { // 1. 获取这一帧的 JPEG 数据地址和大小 IntPtr framePtr = IntPtr.Zero; uint frameSize = 0; uint err = Edsdk.EdsGetPointer(evfImageRef, out framePtr, out frameSize); if (err != EdsError.EDS_ERR_OK || framePtr == IntPtr.Zero) return; // 2. 拷贝到托管数组,避免原生内存被后续帧覆盖 byte[] buffer = new byte[frameSize]; Marshal.Copy(framePtr, buffer, 0, (int)frameSize); // 3. 转成 Bitmap 后丢给界面显示 using (var ms = new MemoryStream(buffer)) { var bitmap = new Bitmap(ms); // 这里只存引用,UI 线程负责释放和刷新 _latestFrame = (Bitmap)bitmap.Clone(); } }帧数据拷贝出来后要立刻转成托管对象。原生缓冲区是 SDK 内部复用的,下一帧到来时上一帧的数据会被覆盖,如果你直接把 IntPtr 交给 UI 线程去读,画面会出现闪烁和撕裂。我的习惯是每一帧都拷贝到托管数组,再交给一个正在等待新帧的显示控件去刷新。实测在 30 分钟长时间运行时,这种方式能保持稳定,GC 压力也不会太大。
如果用 DLP 或高速拍摄,实时取景的帧率其实有限制,通常在 10 到 30 帧之间,具体取决于机身的视频能力。做连续自动对焦或者手动物体跟踪是够的,但测量类项目最好别依赖取景帧测尺寸,畸变和延迟都不可控。
3.4 停止实时取景与资源释放
实时取景不止要发 Evf Stop 命令,还要把前面注册的回调注销掉。我见过一个项目运行时内存一直涨,最后定位到是实时取景停掉后回调注册没取消,SDK 还在往托管侧推帧,每一帧都产生一次装箱和引用,时间长了内存分代直接被打爆。
public void StopLiveView() { // 先停止实时取景 Edsdk.EdsSendCommand( _cameraHandle, EdsCameraCommand.EdsCameraCommand_Evf_Stop, 0); // 再注销取景回调,阻止后续帧事件继续派发 Edsdk.EdsSetObjectEventHandler( _cameraHandle, EdsObjectEvent.EdsObjectEvent_All, null, IntPtr.Zero); _latestFrame?.Dispose(); _latestFrame = null; }这段代码里把对象事件回调全部注销,是因为实时取景帧和拍照文件事件共用同一个事件注册入口。如果你只停掉取景不注销回调,下一次重新开启时事件处理函数会被覆盖注册,旧委托不会被清理,长期反复开关就堆积了大量无用委托。
4. 避坑指南:EDSDK 调试中常见的几个坑与排查路径
4.1 相机列表为空,枚举不到任何设备
现象:调用 EdsGetCameraList 后取到列表,EdsGetChildAtIndex 返回错误码,程序里拿不到相机句柄。换个电脑测试又正常,非常随机。
原因:排第一的是相机端的 USB 连接模式没有切到“PC 连接”。EOS 相机在机身菜单里默认是“自动”或“打印与上传”,这两种模式下 PC 无法枚举设备。排第二的是 USB 线只支持充电不支持数据传输。排第三的是 SDK 位数和系统位数不匹配。
解决:先到相机菜单里把 USB 模式设置为 PC 连接,再换一根完整的数据线,最后确认你的进程平台和 SDK 目录一致。这个顺序不能颠倒,因为线材问题最容易迷惑人,看似电脑识别到了设备,但 SDK 就是返回空列表。我从那以后在初始化方法里加了一个提示,枚举不到相机时直接把这三个可能性打印出来,省得每次都要重新猜。
4.2 拍几百张之后界面明显卡顿,内存一路涨
现象:前 50 张完全正常,到 150 张之后程序响应开始变慢,任务管理器里句柄数和内存持续上升,时间再长一点直接未响应。
原因:事件回调里拿到的相机文件引用没有释放。每拍一张图,SDK 就在内存里保留一个文件对象的引用,不释放就堆积。另一个容易忽略的地方是下载过程中创建的流没有关闭,尤其是下载报错时提前 return 的路径,经常忘记关流。
解决:所有 inRef 在结束使用后统一调用 EdsRelease。下载方法写成一个独立的私有函数,保证不管是成功还是失败,都走到资源清理的代码行。我当时写完这段逻辑后,把 500 张批量拍摄的测试跑了一遍,用 Process Explorer 看句柄数,拍完前后基本持平,才放心把这个逻辑收到公共代码里。
4.3 实时取景开启成功,但一帧都不回调
现象:Evf Start 命令返回成功,界面上的画面区域一直是黑的,回调函数完全没有被触发。
原因:最常见的是相机的取景模式设置不对。有些机身如果不先把内部设置切到“电脑/远程取景”,即使发送了 Evf Start 命令,相机也不会真正输出流。还有一个低频原因是 HDMI 线接到了相机上,视频输出被引导到了外部显示设备上。
解决:设置 Evf_Mode 为桌面实时显示后,发送 Evf Start 命令,再确认视频输出口没有接任何设备。如果仍然无帧,检查机身的自动关机设置,把自动关机时间改成长一些。我在实际项目中把这几项做成了一个检查清单函数,每次打开取景前跑一遍,问题率降了大半。
4.4 在回调里写日志,日志写到一半程序死锁
现象:程序运行到某一次拍照时整个进程假死,断点看是卡在日志框架的写入方法里,后面所有任务都排不上。
原因:日志框架内部用了一个同步锁,而回调线程是 SDK 的工作线程,调用栈里可能在持有某个锁的情况下又触发了新的回调。两边互相等待就死锁了。还有一个常见原因是回调里直接去刷新 UI 控件,也和界面线程的锁产生竞争。
解决:回调线程里只做入队操作,日志也好、UI 刷新也好,都放到消费队列的独立线程上去。我在封装层里加了一个专门的日志队列,SDK 回调里把日志文本丢进队列,后台线程再异步写文件,从机制上杜绝了死锁的可能。
4.5 下载文件速度很慢,长曝光后尤其明显
现象:同一批相机做延时摄影,光圈快门不变的快捷键下载拍摄只需几百毫秒,但长曝光 30 秒后,下载过程要等十几秒甚至更久,好像被卡住了一样。
原因:长曝光时相机内部正在进行降噪处理,相机会在快门结束后继续占用内部处理器做暗场扣除,这段时间内文件系统是锁住的,EddsDownload 只能等待。
解决:在事件回调里拿到文件引用后,先判断相机的当前状态,如果是“正在处理”的状态,就等几秒再尝试取文件信息。更稳的方案是开启机内降噪自动模式时,把命令发送和文件下载之间的等待时间放宽。我习惯给下载任务设置一个最长超时时间,超过时间就把文件引用释放掉,提示用户检查相机状态,而不是让程序无限等下去。
5. 进阶技巧:相机参数写入、批量队列调度与断线自动重连
5.1 曝光参数写入:光圈、快门、感光度的顺序有讲究
手动控制曝光时,三个参数的写入顺序会影响相机行为。官方 SDK 的属性设置方法本身不区分顺序,但如果你把感光度设为一个当前档位不支持的值,相机会返回错误码。我的习惯是先设感光度,再设光圈,最后设快门,因为快门速度在部分机身上可以设置小数档,而 ISO 和光圈是步进式的,先设步进值能减少中间的冲突。
public bool SetExposure(uint isoSpeed, uint aperture, uint shutterSpeed) { // 1. 设置感光度 uint err = Edsdk.EdsSetPropertyData( _cameraHandle, EdsPropertyID.EdsPropID_ISOSpeed, 0, sizeof(uint), ref isoSpeed); // 2. 设置光圈值 err = Edsdk.EdsSetPropertyData( _cameraHandle, EdsPropertyID.EdsPropID_Aperture, 0, sizeof(uint), ref aperture); // 3. 设置快门速度 err = Edsdk.EdsSetPropertyData( _cameraHandle, EdsPropertyID.EdsPropID_Tv, 0, sizeof(uint), ref shutterSpeed); return err == EdsError.EDS_ERR_OK; }这三个属性对应的值都是内部编码值,不是我们平时熟悉的 f/2.8、1/125s 这种直观数字。比如快门速度的编码值 0x1C 对应的可能是 1/125s,具体映射需要查 SDK 文档里的属性值表。我前期在这个地方耗费了不少时间,后来直接把常用档位的编码维护成一个静态字典,需要时按字符串查找,代码可读性也变好了。
5.2 批量拍摄队列:超时判断与失败重试
工业拍照场景里,单张拍完就收工的情况很少见,更多时候是拍一批连续的照片。如果不做队列控制,指令发得太多,相机的内部缓冲和 USB 传输会互相挤兑,轻则丢事件,重则相机死机。我设计的批量任务队列逻辑是:队列里存拍摄参数,后台线程取一个参数发送拍照命令,然后等待 ImageCaptured 事件,收到后在主队列里把这项任务标记为完成。如果 5 秒内没收到事件,就判定为超时,释放相关引用并记录一次失败,再进入下一项。
这个超时值怎么定?我用的是读取当前快门速度再乘 3 加 2 秒的算法。比如快门是 1/125s,超时就是约 2 秒。快门是 1s,超时就是 5 秒。长短曝光任务混在一起时,统一用最大任务的经验值做兜底,再对单个任务单独判断。这套逻辑跑下来,批量 500 张的延时序列能稳定完成,不会因为单张卡住拖死整个任务。
5.3 断线检测与自动重连:被拔掉 USB 后怎么恢复
相机在长时间无人值守的拍摄任务里,随时可能因为 USB 线接触不良、主机休眠策略或者相机自动关机而断开。SDK 有没有断线通知?有,但不是所有固件版本都会触发。我在封装层里做的是双保险:注册机身属性变化事件监听设备状态,同时加一个定时器每 5 秒检查一次相机状态。
public bool IsCameraReady() { if (_cameraHandle == IntPtr.Zero) return false; uint err = Edsdk.EdsSendCommand( _cameraHandle, EdsCameraCommand.EdsCameraCommand_ExtendShutDownTimer, 0); return err == EdsError.EDS_ERR_OK; }这段代码的本质是给相机发送一个“延长关机计时器”的命令,如果相机还在线上,命令会正常返回;如果相机已经断开,错误码会变成一个无效句柄的错误。我用这个返回值判断连接状态,然后触发重连流程。重连不能直接重发 EdsOpenSession,得先把之前的句柄和 SDK 环境全部清理干净,再走一遍初始化流程。当时验证这个方案的时候,我故意在拍摄中途拔掉 USB 线再插回去,确认程序能在两个周期内自动恢复并继续完成剩余队列任务。
从那以后我每次部署一键启动脚本时都会强制跑一遍断线重连测试,再批量拍 100 张看句柄和内存曲线有没有异常,确认没问题了才交出去。EDSDK 这套体系平时用着顺手,但坑都在细节里,初始化、回调、事件、资源释放,每个环节都值得较真一次。希望帮到你。
本文还有配套的精品资源,点击获取