最后更新:2026-06-17(V5.0 结构化重构:整合协议手册/真机实测/调试界面搬迁清单) 用途: 相机自动对焦/自动拍摄校正项目的需求、技术方案、代码结构、协议、当前状态的唯一权威记录。 配套:
自动对焦流程图.html(最新全流程图解)。
| 篇 | 章节 | 看这里如果你要… |
|---|---|---|
| 第一篇·项目 | 1 背景 / 2 硬件架构 | 了解设备、相机、运动机构、为什么做这个项目 |
| 第二篇·技术 | 3 对焦算法 / 4 踩坑教训 | 改算法、调参数、理解设计约束 |
| 第三篇·代码 | 5 代码结构 / 6 编译运行 / 7 GUI功能 / 8 业务闭环 | 上手改代码、跑程序 |
| 第四篇·协议 | 9 下位机串口协议手册 | 对接下位机、加新命令、写EEPROM |
| 第五篇·演进 | 10 修复与演进史 / 11 真机实测记录 | 了解改了什么、真机实际数值 |
| 第六篇·待办 | 12 调试界面搬迁清单 / 13 现状与下一步 | 搬迁老功能、接续工作 |
| 附录 | A 被删工具 / B 老代码考古 / C 诊断存图源码 | 备查 |
开发独立C#小软件,实现"拍出一张可用图"的全自动校正流程:
曝光校正 → XY皿孔居中 → 空皿检测 → Z对焦(选层/搜索)
"MVC2000",USB2.0,约200万像素,Bayer传感器)CapInfoStruct.Exposure);增益 RGB 三通道各 0-255(Gain[3],非单一全局gain)Init()(MV_Usb2Init "MVC2000") → SetOpMode(0)(0=拍照单帧/1=实时)→ 循环 GrabRgb()ZCoarseCenter=90000、区间60000~120000 与真机吻合。5E CMD 00 LTH 数据 累加和校验,无独立帧尾,回复长度按命令固定sumSq / 像素数 / mean(除以均值一次方)
1216bbc): 原为除以 mean²。实测随Z增大画面亮度从~53升到~185(光学效应),
mean² 把高Z清晰帧分数压死,导致选最暗最糊的低Z层。改÷mean后峰正确落在92000。这是"选错层"的真根因级修复。Imaging/Sharpness.cs:105①粗对焦(中央40% ROI,固定大窗口 90000±30000)
→ ②以EEPROM位置为中心小范围微调Y居中
→ ③曝光二分(well内ROI)
→ ④精对焦(围绕粗峰 ±6000,0.95r ROI + 3点平滑 + 抛物线插值)
代码:Calib/CalibrationEngine.cs · CalibrateWell()
关键技术点:
ZCoarseCenter=90000 ± ZCoarseHalf=30000,步距2000,约31层Thread.Sleep(CoarseSettleMs=2000) 停稳再开扫(见3.4)HFineRange=2000,9步),
绝不大范围扫描(well间距~9000,大扫会扫到相邻well);只优化Y偏移max(200,e/5)ms 并丢第1帧用第2帧(见4.6)FineZHalf=6000、步距 FineZStep=500(约25层),0.95r ROI保留边缘,
圆未检出降级中央40% ROI(绝不用全图);3点平滑 + 抛物线插值CenterTolPct)+ well完整Imaging/WellDetector.cs)aspect < 1.5 + boxFill > 0.6 + rConsist > 0.65 + rFrac ∈ [0.10, 0.28]ScanDelayMs=350516f02a) + 逐层分数落盘(007d9f5);最可疑"某层电机未真正到位/运动中采样形成伪峰"。CalibrationEngine.cs · CoarseFocus() 长注释。CalibrationEngine 保留 ZHalf=1500 / ZLayers=9 / CoarseFocusLayers=7 / HScanRange / HScanSteps / FineScanSteps
等字段,在当前 CalibrateWell 流程中已不被使用。改对焦范围认准正在用的参数:
ZCoarseCenter/ZCoarseHalf/ZCoarseStep(粗)、FineZHalf/FineZStep(精)、HFineRange/HFineSteps(居中)、CoarseSettleMs(停稳)。
| # | 坑 | 教训/方案 |
|---|---|---|
| 4.1 | 硬编码1600×1200,相机原生2592×1944 → well被裁出画面 | 所有尺寸取自 cam.Width/Height,绝不硬编码 |
| 4.2 | 曾优化X偏移,但只有1个水平旋转轴只能控Y | X的~5%固定偏移是硬件局限,只优化Y |
| 4.3 | well间斜纹反光被误判成圆 | aspect<1.5 + boxFill>0.6 + rConsist>0.65 + rFrac∈[0.10,0.28] |
| 4.4 | 曝光用全图均值被黑边拉低 → 过曝 | 只在well圆内ROI算亮度 |
| 4.5 | 焦不对→画面糊→检不出圆→无法居中 | 先粗对焦让well清晰,再居中 |
| 4.6 | 开环电机,回复≠停稳,移动后立刻抓图=运动拖影帧→伪峰 | 移动后等延时;大行程开扫前额外停稳2秒+丢残留帧 |
| 4.7 | 高Z层更亮(53→185),÷mean²把清晰亮帧分数压死→选错 | 归一化只÷mean一次方 |
C:\claudeFile\TL\AutoFocusTool\ (WPF/.NET 8/x64)
├── Camera/ 相机封装(Camera/MVCAPI P/Invoke/CapInfoStruct)
├── Serial/ 串口(Protocol协议 / SerialMotor底层 / HouseMotor语义层)
├── Imaging/ 图像(Sharpness / WellDetector / ExposureMeter / ImageConverter)
├── Calib/ 标定(CalibrationEngine / CalibrationFile / CalibrationManager)
├── Devices/ 设备扫描(DeviceScanner / HouseDevice)
├── Logging/ FileLogger静态文件日志器(落盘 Logs/)
├── MainWindow.*.cs 主窗口分部类(xaml/Camera/Motor/Scan/Calib)
├── CalibWindow.* 标定独立窗口(4x4=16格实时显示)
├── calibration.json 标定结果
└── 自动对焦流程图.html ★全流程图解(含每环节参数+异常分支)
⚠️ 旧文档列的
Survey/、Calibrate/、SelfTest/、SmokeTest/、HScan已全删(提交1e36b67)。 价值评估见附录A。Survey目录从来不存在(备份亦无)。
关键类:
CircleFound && PeakRatio>1.2)否则降级EEPROMLoad 必须 IncludeFields=true(见10.2·P0-7)VerticalMoveTo(pulse, delayMs) 自定义延时重载)cd "C:\claudeFile\TL\AutoFocusTool"
dotnet build -c Debug
./bin/Debug/net8.0-windows/AutoFocusTool.exe # 唯一可执行程序
calibration.json;终图:calib_result/;日志:Logs/;诊断数据:TestData/Calibrate.exe/SmokeTest.exe/HScan.exe 已失效。现所有标定/测试通过GUI。偏移<12% && 检到圆 && 峰比>1.2) → 存 calibration.json + 终图 calib_result/,存档后 RefreshCache()MainWindow.Calib.cs · BtnGoWell_Click): 接入 CalibrationManagerCircleFound && PeakRatio>1.2)才用JSON的水平/Z/曝光;否则降级EEPROMRefreshCache(),转well立即用最新结果[标定结果] 或 [EEPROM(未标定/不合格)]标定文件格式(当前真实数据,house8):
{
"tlSn": "house8", "date": "2026-06-16 23:32",
"houses": [{
"house": 8, "port": "COM5", "ccdIndex": 0, "ccdSn": "23120646",
"wells": [{ "well": 1, "horizontalPulse": 71200, "exposure": 33, "focusZ": 88434,
"centerOffsetPct": -2.181, "circleFound": true, "peakSharp": 4.284, "peakRatio": 1.239, "note": "" }]
}]
}
来源:
ivf_tl_control_2.0/ivf_tl_operate_2.0的 Commander/ComBin/Analysiser/Channel。两版逻辑基本一致,operate 是超集。
发送帧: [0]帧头0x5E [1]CMD命令码 [2]序号0x00 [3]整帧长LTH [4..N-2]辅助码+参数 [N-1]校验
CreateORC:for i<len-1: sum+=b[i])。无帧尾。[lenght-2]),0x00=成功,非0=下位机失败。末字节是校验。| 数据类型 | 字节序 |
|---|---|
| 电机脉冲(发0x05 / 收0x18) | 大端(高字节在前) |
| EEPROM数值(发0x12 / 收0x11) | 小端(低字节在前) |
| 温度/压力(收0x06 / 0x20) | 大端 int16 |
新代码 Protocol.MotorAbsolute 大端、ParseEepromInt 小端——已正确区分,务必沿用。
| CMD | 含义 | 回复长度 |
|---|---|---|
| 0x01 | 握手 | 6 |
| 0x02 | 自检 | 6 |
| 0x04 | 设目标温度 | 7 |
| 0x05 | 电机控制 | 6 |
| 0x06 | 读温度/压力 | 9 |
| 0x08 | 读传感器AD | 9 |
| 0x09 | 设IO(LED/气阀/补气/换气) | 6 |
| 0x10 | 获取IO(含舱门状态) | 7 |
| 0x11 | 读EEPROM | 10 |
| 0x12 | 写EEPROM | control=6 / operate=12 ⚠️需真机确认 |
| 0x16 | 自动换气(仅operate) | 6 |
| 0x18 | 读电机位置 | 10 |
| 0x19 | 缓冲瓶补气 | 6 |
| 0x20 | 读缓冲瓶数据(压力+双温度) | 12 |
布局:5E 05 00 0B [辅助码] [脉冲4字节大端] 00 [校验]
辅助码 = 高半字节(轴:水平1/垂直2) | 低半字节(动作:正转0/反转1/脱机2/复位3/绝对4)
| 命令 | 辅助码 | 帧 |
|---|---|---|
| 水平正转/反转 | 0x10 / 0x11 | 5E 05 00 0B 1x [4字节大端] 00 校验 |
| 水平绝对运动 | 0x14 | 同上 |
| 水平复位(硬编码,回退3000) | 0x13 | 5E 05 00 0B 13 00 00 00 0B B8 44 |
| 垂直正转/反转 | 0x20 / 0x21 | 5E 05 00 0B 2x [4字节大端] 00 校验 |
| 垂直绝对运动 | 0x24 | 同上 |
| 垂直复位(硬编码,回退2000) | 0x23 | 5E 05 00 0B 23 00 00 00 07 D0 68 |
脉冲是4字节int32大端(注释虽写"16位"但实现是32位,支持十几万,新旧一致,见11章)。
5E 18 00 06 01 00+校验;读垂直:5E 18 00 06 02 00+校验[4..7],4字节大端 int32布局:5E 09 00 07 [子设备] [开01/关00] [校验]
| 动作 | 子设备 | 帧 |
|------|--------|-----|
| 开/关 LED | 0x00 | 开5E 09 00 07 00 01 6F 关5E 09 00 07 00 00 6E(硬编码不走校验函数) |
| 进气阀 开/关 | 0x01 | 5E 09 00 07 01 01/00 校验 |
| 排气阀 开/关 | 0x02 | 5E 09 00 07 02 01/00 校验 |
| 换气标志 前/后 | 0x03 | 5E 09 00 07 03 01/00 校验 |
| 舱室补气 | 0x04 | 5E 09 00 07 04 01 校验 |
| 舱室排气(operate) | 0x05 | 5E 09 00 07 05 01 校验 |
阀类操作发完应
Thread.Sleep(valueDelay)等气路稳定。
布局:5E 11 00 09 [地址高] [地址中] [地址低] 04 00 [校验](倒数第二字节0x04=读取字节数)
解析:值在回复 [4..7],4字节小端 int32
EEPROM地址全表(高 中 低):
| 项 | 地址 |
|----|------|
| 仪器编号 TLNum | 00 00 08 |
| CCDSN | 00 00 10 |
| 下加热板目标温度(读出÷100) | 00 02 38 |
| 舱室排气阀时间 | 00 03 08 |
| 舱室进气阀时间 | 00 03 0C |
| 垂直焦准零点(写用,⚠️与读不一致) | 00 03 1C |
| 缓冲瓶进气阀时间 | 00 05 0C |
| 灯光亮度 | 00 05 34 |
| Z扫描间隔脉冲 | 00 04 48 |
| 16个well水平位置 | well1=00 04 00,well2~15=00 04 50起步进0x04,well16=00 04 04(特例) |
| 16个well Z焦准零点(读用) | well1=00 04 08,步进0x04,well16=00 04 44(连续) |
⚠️ 读写地址错配: Z焦准零点 读 00 04 08(按well表),写 00 03 1C(单地址)。
新项目逐well写零点应用well表 00 04 [08..44],但必须真机确认下位机真实写地址。
布局:5E 12 00 0C [地址高] [地址中] [地址低] [数值4字节小端] [校验]
数值小端(与电机脉冲大端相反!)。
0x12回复长度两版本不同(6/12),新项目须真机实测。
| 命令 | 帧 | 回复 | 解析 |
|---|---|---|---|
| 读温度(0x06) | 5E 06 00 06 [通道] 00,通道0下盖板/1上盖板/2玻璃片下 |
9字节 | [4..5]大端int16 ÷100 |
| 读压力(0x06) | 5E 06 00 06 03 00 |
9字节 | [4..5]大端int16 不除 |
| 读舱门(0x10) | 5E 10 00 06 02 00 |
7字节 | [4]==0x01为开 |
| 读缓冲瓶(0x20) | 5E 20 00 05 00 |
12字节 | 压力[4..5]/温度1[6..7]÷100/温度2[8..9]÷100 |
| 缓冲瓶补气(0x19) | 5E 19 00 05 00 |
6字节 | — |
| 握手(0x01) | 5E 01 00 05 00 |
6字节 | houseSn=[2] |
缓冲瓶是 houseSn=11 的独立串口。
曝光丢帧(7倍偏差→0)、居中阈值放宽+细扫800ms(62.5%→100%)、业务闭环接CalibrationManager、 异常处理、相机旧帧缓冲、粗对焦改中央40% ROI。
| 编号 | 问题 | 修复 |
|---|---|---|
| P0-1 | 标定"只存不用" | BtnGoWell接CalibrationManager,存档后RefreshCache |
| P0-2/3 | 手动路径运动模糊/旧帧 | Z扫描每层丢第1帧;设曝光后等max(200,e/5)ms并丢帧 |
| P0-4 | 精焦圆未检出用全图 | 降级中央40% ROI,绝不用全图 |
| P0-6 | 电机移动无重试 | RetryMove 重试3次 |
| P0-5 | 误导性死代码 | 删 GrabWithRollingExposure/FullMean |
| P0-7 | JSON反序列化静默失效 | public字段→System.Text.Json默认不反序列化→Load空对象。加 IncludeFields=true。否则标定永远读不到、闭环形同虚设 |
| 提交 | 改动 | 意义 |
|---|---|---|
ac9867c |
扫描范围参数 + 电机限位钳位 | 防越界 |
144f3db |
Z固定大窗口粗对焦 + 扩精焦半幅 | 不依赖EEPROM零点 |
8e6508b |
水平定位改小范围微调,废弃全行程扫描 | 防扫到相邻well |
1216bbc |
清晰度归一化 ÷mean²→÷mean + 峰落边界报警 | 修高Z清晰帧被压低 |
adea570 |
串口按命令类型动态读超时 | 修大行程误判超时 |
f92a71d/6446adb |
GetSourceBuffer加锁+空保护 | 修段错误/NPE |
007d9f5 |
粗对焦逐层分数落盘 | 排查74000伪峰 |
516f02a |
粗对焦开扫前停稳2秒 | 修运动帧伪峰 |
a354b79~18ef633 |
FileLogger + 按钮埋点 | 操作可追溯 |
1e36b67 |
清理测试子工程 | 见附录A |
仿真模式/单元测试/状态机重构等架构升级,经用户确认本轮不做("改动太大,非必要")。
工具:
C:\claudeFile\TL\测试代码\ReadEepromProbe(严格只读,只发握手/读EEPROM/读位置,不驱动任何电机)。
结论:培养舱Z脉冲是"万级(7~11万)",新代码参数完全正确,不存在10倍单位错误。
| 舱(houseSn) | COM | Z焦准零点范围 | 当前Z位置 | 每层步距 | 水平位置范围 |
|---|---|---|---|---|---|
| 8 | COM5 | 74000~74600 | 88434 | 128 | 70700~205650 |
| 7 | COM18 | 79700~80500 | 60000 | 128 | 70700~205700 |
| 9 | COM19 | 79300~80200 | 78456 | 128 | 70200~205300 |
| 6 | COM11 | 75760~76400 | 95740 | 96 | 70800~205800 |
| 4 | COM9 | 75372~76012 | 110000 | 96 | 71500~206650 |
| 2 | COM4 | 83272~84128 | 0 | 96 | 71000~205850 |
| 11(缓冲瓶) | COM3 | 7025~7325 | — | 48 | 7464~22088 |
三点定论:
ZCoarseCenter=90000、区间60000~120000、ZMaxPulse=125000 与真机吻合,无需改。真机6个培养舱Z焦准零点全部落在74000~84000,而74000伪峰恰在此区间下沿。 → 74000可能不是纯"运动拖影伪峰",而是EEPROM零点附近的真实次清晰峰与90000主焦面竞争。 下次复现74000问题时,应对比该well的EEPROM零点值,并开DebugSave存粗对焦每层图(附录C)。
真机显示同一舱16个well的Z焦准零点几乎相同(如house8全是74000~74600),但新代码精对焦实测最清晰层在88434(差~14000脉冲≈110层)。 → 印证:EEPROM零点严重偏离真实焦面,老系统从零点出发只扫40层×128≈5120脉冲的小窗口,焦面落在窗口外必然选错。新代码大窗口(±30000)正是对症。
老调试界面:
ivf_tl_operate_2.0 · HouseDebugPageViewModel / BufferDebugViewModel。 底层协议新程序已具备(第9章),多数缺失功能只需"加帧构造 + 加封装方法 + 上层循环"。
| 优先级 | 缺失功能 | 协议 | 备注 |
|---|---|---|---|
| P0 | 写EEPROM框架(新程序完全没有0x12写命令) | 0x12 | 是下面所有"写"的基础 |
| P0 | 标定结果回写下位机(well水平位置+Z焦准零点) | 0x12 | 现只写JSON,换文件即丢 |
| P0 | 读温度/压力/舱门 | 0x06/0x10 | 调试台基本监控 |
| P1 | 进/排气阀、舱室补气/排气、缓冲瓶补气与状态 | 0x09/0x19/0x20 | 固定帧,注意valueDelay |
| P1 | 写灯光亮度/扫描间隔/阀门时间 | 0x12 | 复用写框架换地址 |
| P2 | 逐well水平抓拍(ShuiPingZhuaPai) | 已有底层 | 上层循环1-16 |
| P2 | 多轮重复对焦(AutoFocusPic的xun轮次) | 已有底层 | 对焦稳定性复检 |
| P2 | 电机正转/反转/脱机、一键归位(MototReady) | 已有底层(0x05) | 组合命令 |
新程序 Protocol.cs 加(数值小端,与电机脉冲大端相反):
public static byte[] WriteEeprom(byte addrHi, byte addrMid, byte addrLo, int value)
{
byte[] p = BitConverter.GetBytes(value); // 小端: p[0]最低
return WithChecksum(new byte[] { ST, 0x12, 0x00, 0x0C, addrHi, addrMid, addrLo,
p[0], p[1], p[2], p[3], 0x00 });
}
注意事项: ① 数值小端(电机大端,别搞反);② 0x12回复长度老代码自相矛盾(6/12),先真机实测确认;
③ 写后回读校验;④ well地址表照抄第9.7(well16=0x04、well1=0x00非线性)。
在 MainWindow.Calib.cs:205 存JSON后,对每个合格well:
motor.WriteWellHorizontalPos(well, wc.HorizontalPulse); // 地址 00 04 [well表]
motor.WriteWellFocusZero(well, wc.FocusZ); // 地址 00 04 [08..44],⚠️写地址需真机确认
注意: Z焦准零点读写地址错配(9.7),写之前务必真机验证"写哪个地址 → 读回一致"。
| 功能 | 帧 | 解析 |
|---|---|---|
| 下盖板温度 | 5E 06 00 06 00 00+校验 |
[4..5]大端int16 ÷100 |
| 上盖板温度 | 5E 06 00 06 01 00+校验 |
同上 |
| 玻璃片下温度 | 5E 06 00 06 02 00+校验 |
同上 |
| 压力 | 5E 06 00 06 03 00+校验 |
[4..5]大端int16 不除 |
| 舱门 | 5E 10 00 06 02 00+校验 |
[4]==0x01为开 |
| 下加热板目标温度 | 5E 11 00 09 00 02 38 04 00+校验 |
[4..7]小端int32 ÷100 |
老代码 HouseDebugPageViewModel.ShuiPingZhuaPai:615:先水平复位 → for well=1..16:读该well水平位置→HorizontalAbsolute转过去→抓帧存盘。
注意: 每步用 motorDelay 延时;循环内检查中止标志;必须先打开实时图像否则抓不到。底层新程序全有。
老代码 AutoFocusPic(focalCount, xun):672:for k=0..xun(轮):每轮先垂直复位 → for i=0..focalCount:目标=起点+i×间隔脉冲→VerticalAbsolute→抓帧。
注意: 间隔脉冲默认128(真机实测96/128各异,应读EEPROM);超 verticalMotorPulseMax 立即终止;用于对焦稳定性复检。
[HandleProcessCorruptedStateExceptions][SecurityCritical] + static锁(防dll崩溃)XxxWait()feature/autofocus-scan-range。舱室约定: 有胚胎→9号舱;空皿/功能测试→1号舱(现场无1号则用2号等空舱)。
calib_result/ 终图)[标定结果] 且曝光自动设标定值开场:"继续相机自动对焦项目,读总方案文档第13章现状,代码在 C:\claudeFile\TL\AutoFocusTool"
进度:两轮P0清零+算法重写,真机已确认Z万级参数正确(第11章)。
开放:74000伪峰、标定回写EEPROM、真胚胎验证、调试界面搬迁(第12章)。
改算法:Calib/CalibrationEngine.cs(认准3.2参数,别动3.5死参数)
改协议:Serial/Protocol.cs(第9章手册)
测试代码:C:\claudeFile\TL\测试代码\(只读探针等,不混入主工程)
提交1e36b67删除了所有子工程。备份在 C:\Users\AIVFO\Desktop\AutoFocusTool-master\autofocustool。
| 工具 | 价值 | 建议 |
|------|------|------|
| SelfTest/Diag.cs | 高:纯C#裸写BMP+亮度统计,抓74000伪峰要靠它存图 | 建议恢复(源码见附录C) |
| SmokeTest/SmokeTest.cs | 中:无电机验证扫描/相机/曝光/JSON | 需快速回归时恢复 |
| Calibrate/Calibrate.cs | 中:命令行单well标定 | 需命令行调试时恢复,注意对齐当前参数 |
| ToPng/、CalibTest/、topng.csx | 低 | 不必恢复 |
新旧脉冲单位完全一致,无10倍差异。铁证:垂直复位命令帧两边逐字节相同(5E 05 00 0B 23 00 00 00 07 D0 68)。
老代码 verticalMotorPulseMax=125000、绝对运动用32位脉冲,与新代码、与真机(万级)全部吻合。
"1万"印象源于缓冲瓶舱(11号,千级,见11.1)。
mvcapi.dll 的方法加 [HandleProcessCorruptedStateExceptions][SecurityCritical] + static锁。dll抛AccessViolation时默认catch不住、进程挂。新代码近期 f92a71d/6446adb 修段错误属同域。GetRgbDataFun 关相机+重开,最多3次),不重试单帧。ReopenPort(丢缓冲+Close+Dispose+重建+重订阅);扫描类快速失败。SerialBin.GetHouseInfo):是"读设备全部标定值"现成模板。SourceBuffer 每次new数组 → 高频抓图GC压力;应复用缓冲。Clear()全清、一问一答容不得粘包 → 若下位机主动上报需改按长度清。autoFocus键名词不符义、返回码语义不统一 → 别学。来自备份
SelfTest/Diag.cs。纯C#不依赖WPF,把相机24bpp BGR裸写BMP(含FlipY翻正)+统计亮度。 用途: 排查3.4的74000伪峰时挂到CalibrationEngine.DebugSave逐层存图。需要时贴进项目临时用、用完即删。挂载:
> engine.DebugSave = (buf, name) => > Diag.SaveBmp(buf, cam.Width, cam.Height, $@"C:\claudeFile\TL\AutoFocusTool\TestData\{name}.bmp"); > ``` > 并在 `CoarseFocus` 逐层处加 `DebugSave?.Invoke(b, $"coarse_w{well}_z{z}")`。csharp using System; using System.IO;
namespace AutoFocusTool {
/// <summary>自检用轻量图像诊断(不依赖 WPF)。统计像素亮度;裸写 24bpp BMP 存盘。</summary>
internal static class Diag
{
/// <summary>返回 (最小, 最大, 平均) 灰度。</summary>
public static (int min, int max, double mean) Stats(byte[] bgr24)
{
int min = 255, max = 0; long sum = 0; int n = 0;
for (int i = 0; i + 2 < bgr24.Length; i += 27)
{
int g = (bgr24[i] * 29 + bgr24[i + 1] * 150 + bgr24[i + 2] * 77) >> 8;
if (g < min) min = g;
if (g > max) max = g;
sum += g; n++;
}
return (min, max, n > 0 ? (double)sum / n : 0);
}
/// <summary>裸写 24bpp BMP(含 FlipY 翻正)。</summary>
public static void SaveBmp(byte[] bgr24, int w, int h, string path)
{
int rowSize = ((w * 3 + 3) / 4) * 4;
int imgSize = rowSize * h;
int fileSize = 54 + imgSize;
using var fs = new FileStream(path, FileMode.Create);
using var bw = new BinaryWriter(fs);
bw.Write((byte)'B'); bw.Write((byte)'M');
bw.Write(fileSize); bw.Write(0); bw.Write(54);
bw.Write(40); bw.Write(w); bw.Write(h); // h>0 自底向上=翻正相机倒像
bw.Write((short)1); bw.Write((short)24);
bw.Write(0); bw.Write(imgSize);
bw.Write(2835); bw.Write(2835); bw.Write(0); bw.Write(0);
byte[] pad = new byte[rowSize - w * 3];
for (int y = 0; y < h; y++)
{
int row = y * w * 3;
bw.Write(bgr24, row, w * 3);
if (pad.Length > 0) bw.Write(pad);
}
}
}
} ```
文档版本: V5.0(2026-06-17 结构化重构:六篇+附录;整合协议手册/真机实测/调试界面搬迁清单)
状态: 唯一权威。配套 自动对焦流程图.html。
维护: 重大变更更新第10章演进史 + 第13章现状。