# 相机自动对焦项目 · 源码分析清单(控制方式 + 参数 + 安全阈值) > 记录日期:2026-06-12 > 来源:阅读 `C:\claudeFile\TL\源码` 下两套工程后的只读分析结论。 > 用途:把"怎么控相机 / 怎么控马达 / 怎么控光源 + 物理参数 + 安全限位"一次说清,供你核对,确认后据此写第一步手动测试程序。 > 配套:主文档 `相机自动对焦项目-总方案.md`(需求与算法逻辑看那份,本份只讲源码能落地的硬接口)。 > 标注规则:**【确证】**=代码里直接读到;**【推测】**=据命名/引用推断,需真机或反编译确认。 --- ## 0. 源码里有两套工程 | 工程 | 位置 | 作用 | 对我们的价值 | |---|---|---|---| | `ivf_tl_control_2.0` | 完整下位机控制系统(多项目解决方案) | 现役:控温、拍照、换气、对焦上传 | **主参考**:相机、串口马达、LED、对焦循环全在这 | | `aivfo_ccd` | 一个小 WPF 工程,含独立 `Camera.cs` | 像是相机调试/试验程序 | 相机操作更全(`SetExp`、`BitmapImage`、Raw 访问),**适合直接抄来做测试程序的相机层** | 结论:测试程序以 `ivf_tl_control_2.0` 的健壮相机框架为骨架,补 `aivfo_ccd` 的便捷方法。 --- ## 一、相机控制【确证为主】 ### 1.1 是什么相机 - 厂商 **Microview(微视)**,型号字符串硬编码 `"MVC2000"`,USB2.0 工业相机,约 200 万像素。 - 命名空间 `using Microview;`(见 `aivfo_ccd\Util\Camera.cs:1`)。 - **Bayer 传感器**(取 Raw 后需去马赛克转 RGB)。 - SDK = `mvcapi.dll`(核心)+ `mvavi.dll`(录像),P/Invoke 封装在 `ivf_tl_CameraHelper\MVCAPI.cs`。 ### 1.2 连接 / 初始化顺序 ``` new Camera(index, 1600, 1200, exposure) → Init() ⇒ MV_Usb2Init("MVC2000", out index, ref capInfo, out hImager) → SetOpMode() ⇒ MV_Usb2SetOpMode(hImager, nMode, false) ``` - `nMode`:**0 = 拍照(单帧)模式,1 = 实时图像模式**(`aivfo_ccd\Util\Camera.cs:282-285` 注释明确)。 - 句柄 `IntPtr hImager` 贯穿后续所有调用。 - 卸载:`UnInit() ⇒ MV_Usb2Uninit(ref hImager)`。 ### 1.3 抓单帧 + 取像素 - 抓图:`GetRgbData() ⇒ MV_Usb2GetRgbData(hImager, ref capInfo, pDest)`。 - 取数据:属性 `SourceBuffer` → `byte[]`,长度 `width*height*3`,**24bpp RGB(BGR 排列)**。 - ⚠️ **相机原始数据上下颠倒**:`aivfo_ccd` 版取图后做了 `RotateNoneFlipY` 垂直翻转(`Camera.cs:185-208`)。显示/保存/算清晰度时要统一处理,否则 ROI 位置会上下错。 - 另一条路径:`GetRawData()` → `RawToRgb()`(`MV_Usb2ConvertRawToRgb`),用于需要 Raw 的场合。 - 默认分辨率 **1600 × 1200**。 ### 1.4 曝光与增益(曝光校正这一环靠它) - **曝光时间**:字段 `CapInfoStruct.Exposure`(int),**单位 100µs**(默认 400 → 40ms)。 - 设置入口:`SetPartOfCapInfo(int Exposure)`(`ivf_tl` 版)/ `SetExp(int newExp)`(`aivfo_ccd` 版,最简洁)⇒ `MV_Usb2SetPartOfCapInfo`。 - **增益**:字段 `CapInfoStruct.Gain` 是 **byte[3],分别是 红 / 绿 / 蓝 三通道增益**(不是单一全局 gain),取值 0–255,默认约 R25 G14 B25。 - 含义:**"曝光校正"在相机侧的可调量 = 曝光时间(100µs步进) + RGB 三通道增益**。 ### 1.5 预览 / 取流(可视化界面要用) - 贴窗口预览:`Usb2Start(控件句柄, x, y, w, h) ⇒ MV_Usb2Start(...)`;停 `Usb2Stop`;暂停 `MV_Usb2PausePreview`。 - 帧回调(软件取流,不贴窗):`MV_Usb2SetFrameCallBack(hImager, 委托, lpUser)`。 - 帧率:`MV_Usb2GetFrameRate`。 - **对焦算分不需要预览**:拍照模式(nMode=0) + 循环 `GetRgbData()` 抓帧即可;要给人看实时画面再开 `Usb2Start` 或回调。 ### 1.6 存图 - `SaveBmpPic(path)` 存 BMP;`SaveJPG` 实为原始字节直写(非真 JPEG,注意)。 --- ## 二、马达控制(串口)【确证为主】 ### 2.1 串口与协议 - 串口参数:波特率 **9600**,8 数据位,1 停止位,无校验,读/写超时 **3000ms**。`System.IO.Ports.SerialPort` 事件驱动收数(`Channel.cs:84-109`)。 - 帧格式(`Commander.cs:873-884` 注释): | 字节 | [0] | [1] | [2] | [3] | [4..n-2] | [n-1] | |---|---|---|---|---|---|---| | 含义 | 帧头 `0x5E` | 命令码 CMD | 序号 `0x00` | 整帧长 LTH | 辅助码+参数 | 校验 CRC | - **无独立帧尾**,末字节即校验。 - **校验 = 累加和**:前 n-1 字节逐字节相加(byte 溢出截断)放末字节(`Commander.CreateORC` `Commander.cs:15-41`)。 - **每条命令的回复长度写死**(`Commander.CustomProtocolLength` `Commander.cs:43-89`),接收端按固定长度从环形缓冲区取,不靠帧尾分包。 - 回复 [n-2] = 下位机结果位,非 0 视为失败(`Channel.cs:186-188`)。 ### 2.2 有几个马达 **只有 2 个步进马达**(不是 X+Y+Z 三轴): - **水平电机(Horizontal)**:辅助码高半字节 `1`。单轴,把样品/well 定位到水平位置。→ 对应方案里的"XY 皿孔定位",但实际是**单轴水平**。 - **垂直电机(Vertical)= Z 轴 = 对焦轴**:辅助码高半字节 `2`。 > ⚠️ 与方案文档的差异:方案里写"XY 两轴居中",**源码里水平只有一个轴**。皿孔居中是否真的只有单轴、还是另有机构,**需你真机确认**。 ### 2.3 电机命令(CMD=0x05,11 字节帧) 辅助码 = 高半字节(轴) | 低半字节(动作):正转0 / 反转1 / 脱机2 / 复位3 / 绝对运动4。 **Z 轴(垂直/对焦)——写程序重点:** | 动作 | 辅助码 | Commander 方法 | ComBin 封装(阻塞等待) | |---|---|---|---| | 复位(回零) | 0x23 | `CreateVerticalMotorResetCommand` (:998) | `VerticalMotorResetWait` (`ComBin.cs:509`) | | 正转(相对) | 0x20 | `CreateVerticalMotorForwardCommand` (:1009) | — | | 反转(相对) | 0x21 | `CreateVerticalMotorBackwardCommand` (:1080) | — | | **绝对运动** | 0x24 | `CreateVerticalMotorAbsoluteMovementCommand` (:1045) | **`VerticalMotorAbsoluteWait`** (`ComBin.cs:535`) | | 读位置 | — | `CreateReadVerticalMotorCommand`(CMD=0x18,辅助0x02,:187) | `ReadVerticalMotorWait` (`ComBin.cs:493`) | **水平电机:** 同结构,绝对运动 `HorizontalMotorAbsoluteWait`(`ComBin.cs:457`),读位置 `ReadHorizontalMotorWait`(`ComBin.cs:477`)。 ### 2.4 位置单位、行程、限位 - **单位 = 步进电机脉冲数(步),不是 µm。**【确证】全代码无 µm 换算,字段一律 `Pulse`/`Position`。 - 绝对运动用 32 位 int 脉冲值,**大端写入帧**(高字节在前,`Commander.cs:1056-1069`)。 - **Z 层间距**:`House.verticalMotorSpacePulse`(每层脉冲间隔,`House.cs:86`);未配置时**硬编码默认 128 脉冲/层**(`HouseBin.cs:1125`)。 - **Z 行程上限**:`TLSetting.verticalMotorPulseMax`(`TLSetting.cs:259`,测试值见 `StartMain.cs:106` = 125000)。对焦循环里多处 `if (currentVer > verticalMotorPulseMax) break;` 做**软限位**(`HouseBin.cs:1128/1266/1442`)。 - **零点**:复位命令回机械零点;每个 well 的"焦准零点脉冲"存 EEPROM(`CreateReadEEPROMvertMtStartPulse` `Commander.cs:530`)。 - **硬限位开关**:源码不可见,**【推测】由下位机固件处理**。需你确认行程下限/上限的物理保护。 ### 2.5 怎么知道移动到位了 —— 关键 **开环 + 固定延时 + 事后回读**,没有闭环到位反馈: 1. 指令阻塞等串口回复(`WaitOne`),但**回复只代表"下位机收到指令",不代表机械停稳**。超时 30s 重发,最多 3 次,失败重开串口。 2. 回复后再 `Thread.Sleep(motorDelay)` 等运动完成。`motorDelay` = `TLSetting.motorDelay`(电机到位延时 ms,`TLSetting.cs:253`)。 3. 移动后 `ReadVerticalMotorWait` 回读实际位置与目标比对,不一致记错误日志(`HouseBin.cs:1537/1610`)。 - 位置解析 `Analysiser.ParseCurrentMotor`(`Analysiser.cs:110-117`):回复字节[4..7]拼 int32,失败返回 -1。 > 对自动对焦的直接影响:**Z 每移一层,必须等 `motorDelay` 稳定后再抓图**,否则拍到运动模糊。`motorDelay` 的真机取值需标定。 --- ## 三、光源 / LED 控制【确证】 > (你补充的环节)曝光校正除了相机曝光+增益,还有第三个旋钮:补光 LED。 - **LED 开关(串口,CMD=0x09 设IO,7字节):** - 开:`CreateOpenLEDCommand` = `5E 09 00 07 00 01 6F`(`Commander.cs:107`)→ `OpenLEDWait`(`ComBin.cs:655`) - 关:`CreateCloseLEDCommand` = `5E 09 00 07 00 00 6E`(`Commander.cs:117`)→ `CloseLEDWait`(`ComBin.cs:670`) - 流程:对焦/拍照前 `OpenLEDWait`,结束后 `CloseLEDWait`(`HouseBin.cs:1070/1139/1407/1452`)。 - **LED 亮度**:⚠️ 代码里**只能从 EEPROM 读亮度值**(`CreateReadEEPROMLightNum` `Commander.cs:807` → `ReadEEPROMLightNumWait` `ComBin.cs:822`),**没找到动态"设置亮度"的串口命令**。【确证:只读未写】 - 含义:现役系统的光源亮度是**出厂/EEPROM 固定值**,运行时只开关、不调亮度。 - **待确认(重要)**:自动曝光校正若想调光源亮度,需要下位机支持"写亮度"命令。现有协议里没有 → 要么走相机曝光+增益这条路(已可调),要么请硬件方加一条写亮度指令。 **结论:曝光校正三个可调量的现状** | 旋钮 | 能否程序调 | 接口 | |---|---|---| | 相机曝光时间 | ✅ 可调 | `SetExp/SetPartOfCapInfo`,单位100µs | | 相机 RGB 增益 | ✅ 可调 | `CapInfoStruct.Gain[3]`,0–255 | | LED 亮度 | ❌ 现仅开关+读取 | 需硬件加"写亮度"命令才可调 | | LED 开关 | ✅ 可调 | `OpenLEDWait/CloseLEDWait` | --- ## 四、现有"对焦/选层"流程 —— 重大发现【确证】 ### 4.1 客户端不做选层,决策在服务器 现役 C# 客户端**本身不计算"哪一层最清晰"**。它只做:沿 Z 逐层移动 → 每层抓图 → 存盘 → **HTTP 上传服务器** → 轮询向服务器**索要**"最清晰层"的相对位置。 - 选层算法跑在**服务器端**,客户端只是上传+取结果。 - **直接推论**:方案文档说的"7 组全选错",**问题大概率在服务器算法,或在传给服务器的几何参数**(Z 起点 / 层间距 / 抠图偏移),不在这个仓库的 C# 里。 ### 4.2 Z 序列拍摄循环(现成可复用) - 全孔:`HouseBin.cs:1106-1137`;单孔:`HouseBin.cs:1244-1281`。逻辑相同: - 层数 = `House.autoFocusNumber`; - 每层 Z = `对焦起点 verticalMotorPosition + 层间距 * i`(层间距默认 128 脉冲); - 超 `verticalMotorPulseMax` 则 break; - `VerticalMotorAbsoluteWait` 移动 → `Autofocus()` 抓图存盘上传。 ### 4.3 本地其实有一条"算分"通路,但没被用 - `HouseBin.GetScore(...)`(`HouseBin.cs:1734`)→ 外部 DLL `Project2.dll` 的 `GetImageScoreAndSaveImage`(`AivfoHelper.cs:34`,导出序号 #5)。 - **全仓库搜索:`GetScore` 除定义外零调用** —— 是死代码 / 调试遗留。 - 即:本地具备"给一张图打清晰度分"的能力(在 `Project2.dll` 里,底层应是 OpenCV),但现役流程没拿它选层。 - 具体用的是 Laplacian / Tenengrad / 方差中的哪种,**C# 源码里看不到,在 DLL/服务器里**,需反编译 `Project2.dll`(依赖 `opencv_world3416.dll`)或查服务器才能确证。 ### 4.4 抠图 ROI 偏移(选错的高嫌疑因素) - 每孔存 `leftOffset` / `bottomOffset`(抠图裁剪偏移,`HouseWellSettingDB.cs`)。 - 这俩配错 → ROI 偏离胚胎 → 任何清晰度算法都会选错层。**【推测】值得优先排查。** ### 4.5 DLL 职责一览 | DLL | 作用 | 依据 | |---|---|---| | `mvcapi.dll` | 相机 SDK | 【确证】MVCAPI.cs 全部 P/Invoke | | `mvavi.dll` | 录像 | 【确证】 | | `Project2.dll`(newccd) | 当前图像处理:抠图#1 / 打分#5 / 存图#7 | 【确证】AivfoHelper.cs | | `opencv_world3416.dll` | Project2 的底层 CV | 【推测】 | | `CellProcessorDll.dll`/`CellCultureDllMulti64.dll`/`opencv_world342.dll`(ccd) | 旧图像处理链路 | 【推测】已被 newccd 取代,无 C# 引用 | | `MVBayerDec.dll`/`MVParm.dll` | Bayer 解码 / 相机参数 | 【推测】仅打包 | --- ## 五、写"第一步手动测试程序"的落点(拿到你确认后开工) 新程序是**独立小工程**,不动现役系统。各能力的现成接口: | 能力 | 直接复用 | 备注 | |---|---|---| | 连相机 | `Camera.Init()` + `SetOpMode(0)` | 抄 `aivfo_ccd\Util\Camera.cs` | | 调曝光 | `SetExp(int)` 单位100µs | | | 调增益 | `CapInfoStruct.Gain[0..2]` 0–255 | RGB 三通道 | | 抓帧 | `GetRgbData()` + `SourceBuffer` | 注意 FlipY 翻转 | | 实时预览 | `Usb2Start(句柄,...)` 或帧回调 | 可视化界面用 | | 开串口/电机 | `ComBin` + `Commander`(COM口、9600) | | | Z 绝对移动 | `VerticalMotorAbsoluteWait(pulse, motorDelay)` | 移完等 motorDelay 再抓图 | | 读 Z 位置 | `ReadVerticalMotorWait` | 事后校验 | | 水平移动 | `HorizontalMotorAbsoluteWait` | 皿孔定位 | | LED 开关 | `OpenLEDWait` / `CloseLEDWait` | 对焦前开、后关 | | 清晰度算分 | **自己写**(Tenengrad/Laplacian,OpenCvSharp 或纯 C#) | 不依赖服务器、不依赖 Project2.dll,方案要求本地可解释 | **最小闭环(手动):** 连相机 → 开 LED → 实时预览 + 显示当前帧清晰度分 → 手动按钮调曝光/增益、手动 Z±/水平± → 移动后回读位置 → 关 LED。打通这条就完成"第一步"。 --- ## 六、需要你真机/拍板确认的问题 1. **水平轴 vs XY**:源码只有一个水平电机。皿孔居中真的只有单轴?还是另有未在此代码里的 XY 机构?(影响"居中"怎么做) 2. **串口号**:现场相机串口是哪个 COM 口?(程序要选口) 3. **motorDelay 真机值**:Z 移一层后等多少 ms 才稳?(决定抓图时机、防运动模糊) 4. **Z 行程**:对焦零点、层间距(默认128脉冲)、上限(测试值125000)在你这台真机的实际值?固定范围拍几层? 5. **光源亮度**:要不要程序调 LED 亮度?现协议只能开关+读取。若要调,得请硬件方加"写亮度"命令;否则曝光校正只用相机曝光+增益。 6. **相机翻转**:保存/算分按 FlipY 后的图,对吗? 7. **选层搬本地**:确认旧选层在服务器→我们新程序在本地自己算分选层(绕开服务器),对吗? > 确认 1/2/3/5/7 即可让我开始写第一步手动测试程序;4/6 可在联调时标定。