相机自动对焦项目-源码分析清单.md 14 KB

相机自动对焦项目 · 源码分析清单(控制方式 + 参数 + 安全阈值)

记录日期:2026-06-12 来源:阅读 C:\claudeFile\TL\源码 下两套工程后的只读分析结论。 用途:把"怎么控相机 / 怎么控马达 / 怎么控光源 + 物理参数 + 安全限位"一次说清,供你核对,确认后据此写第一步手动测试程序。 配套:主文档 相机自动对焦项目-总方案.md(需求与算法逻辑看那份,本份只讲源码能落地的硬接口)。 标注规则:【确证】=代码里直接读到;【推测】=据命名/引用推断,需真机或反编译确认。


0. 源码里有两套工程

工程 位置 作用 对我们的价值
ivf_tl_control_2.0 完整下位机控制系统(多项目解决方案) 现役:控温、拍照、换气、对焦上传 主参考:相机、串口马达、LED、对焦循环全在这
aivfo_ccd 一个小 WPF 工程,含独立 Camera.cs 像是相机调试/试验程序 相机操作更全(SetExpBitmapImage、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)
  • nMode0 = 拍照(单帧)模式,1 = 实时图像模式aivfo_ccd\Util\Camera.cs:282-285 注释明确)。
  • 句柄 IntPtr hImager 贯穿后续所有调用。
  • 卸载:UnInit() ⇒ MV_Usb2Uninit(ref hImager)

1.3 抓单帧 + 取像素

  • 抓图:GetRgbData() ⇒ MV_Usb2GetRgbData(hImager, ref capInfo, pDest)
  • 取数据:属性 SourceBufferbyte[],长度 width*height*324bpp 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.Gainbyte[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 停止位,无校验,读/写超时 3000msSystem.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) |

水平电机: 同结构,绝对运动 HorizontalMotorAbsoluteWaitComBin.cs:457),读位置 ReadHorizontalMotorWaitComBin.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.verticalMotorPulseMaxTLSetting.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)。
  4. 位置解析 Analysiser.ParseCurrentMotorAnalysiser.cs:110-117):回复字节[4..7]拼 int32,失败返回 -1。

对自动对焦的直接影响:Z 每移一层,必须等 motorDelay 稳定后再抓图,否则拍到运动模糊。motorDelay 的真机取值需标定。


三、光源 / LED 控制【确证】

(你补充的环节)曝光校正除了相机曝光+增益,还有第三个旋钮:补光 LED。

  • LED 开关(串口,CMD=0x09 设IO,7字节):
    • 开:CreateOpenLEDCommand = 5E 09 00 07 00 01 6FCommander.cs:107)→ OpenLEDWaitComBin.cs:655
    • 关:CreateCloseLEDCommand = 5E 09 00 07 00 00 6ECommander.cs:117)→ CloseLEDWaitComBin.cs:670
    • 流程:对焦/拍照前 OpenLEDWait,结束后 CloseLEDWaitHouseBin.cs:1070/1139/1407/1452)。
  • LED 亮度:⚠️ 代码里只能从 EEPROM 读亮度值CreateReadEEPROMLightNum Commander.cs:807ReadEEPROMLightNumWait 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.dllGetImageScoreAndSaveImageAivfoHelper.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 可在联调时标定。