Просмотр исходного кода

feat(d2-02-p5): CCD拍照按孔复用对焦标定曝光(修拍照偏亮)

对焦重构新增了每孔曝光二分优化,但只接到对焦照、没接到CCD采集照,
导致"对焦照曝光好、拍照偏亮"。本次让拍照时按孔读回标定曝光再设相机。

实现(读库回放,不塞采集内存结构,更解耦):
- HouseWellPhoto 加 exposure 字段(内存类)
- CalibrationStore 加 ExposureReader 回调(仿 BaselineReader,委托避免反向依赖SqlSugar)
- AppData 绑定 ExposureReader → DBService.GetCalibratedExposure
- DBService.GetCalibratedExposure: 查 house_autofocus_calibration,
  scene=1 日常最新优先(calibTime desc)、回退 scene=0 基准,无值返回 null
- HouseBin.ccdThreadFun: 每 well 拍照前读标定曝光,>0 用标定值否则兜底
  House.ccdExposure,Camera.SetPartOfCapInfo 设曝光+Sleep让连续采集流生效;
  try/catch 包裹,失败只记日志不崩采集线程

曝光值本就经 SaveCalibration→DbMirror 落库,无需新表/列,缺口仅"拍照读回"。

control.sln Release 编译0错,ControlHost输出已同步到 operate/bin/Release/control/。

排查发现(待真机验证前提):仓库内所有 aivfoTL.db 均为空模板无标定表,
ControlHost引擎当前未运行(38080无响应),需先起引擎跑对焦标定再验证拍照曝光。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 21 часов назад
Родитель
Сommit
06c72e3850

+ 8 - 0
ivf_tl_operate_2.0/control/IvfTl.AutoFocus/Storage/CalibrationStore.cs

@@ -38,6 +38,14 @@ namespace IvfTl.AutoFocus.Storage
         /// </summary>
         public Func<string, int, int, int?> BaselineReader { get; set; }
 
+        /// <summary>
+        /// 拍照按孔设曝光用:读该 well 标定曝光(×100µs)。
+        /// 参数:(tlSn, houseSn, wellSn);返回该 well 曝光,无则 null(调用方兜底 House.ccdExposure)。
+        /// 由 control 端绑定到 DBService.GetCalibratedExposure(scene=1 日常优先、回退 scene=0 基准)。
+        /// 本程序集不引用 SqlSugar/Entity,故以委托回调形式由 control 端实现,避免反向依赖(同 DbMirror/BaselineReader)。
+        /// </summary>
+        public Func<string, int, int, int?> ExposureReader { get; set; }
+
         /// <summary>日志回调(可空)。</summary>
         public Action<string> Log { get; set; }
 

+ 22 - 0
ivf_tl_operate_2.0/control/ivf_tl_Com/HouseBin.cs

@@ -2199,6 +2199,28 @@ namespace ivf_tl_Com
                     //垂直电机复位运动
                     ComBin.VerticalMotorResetWait(custom, TLSetting.motorDelay);
                     HouseState(CommandSource.ccdThread, custom, 0);
+                    // 按孔设曝光:读该 well 标定曝光(scene=1 日常优先/回退 scene=0),使采集照与对焦照曝光一致(修拍照偏亮)。
+                    // 取不到/无效 → 兜底用 House.ccdExposure(house 级默认,即原行为),不影响拍照。
+                    try
+                    {
+                        string tlSnCcd = TLSetting != null ? TLSetting.tlSn : (Dish != null ? Dish.tlSn : null);
+                        int? wellExp = (AutofocusStore != null && AutofocusStore.ExposureReader != null)
+                            ? AutofocusStore.ExposureReader(tlSnCcd, House.houseSn, item.wellSn)
+                            : null;
+                        int useExp = (wellExp.HasValue && wellExp.Value > 0) ? wellExp.Value : House.ccdExposure;
+                        if (Camera != null)
+                        {
+                            int setRet = Camera.SetPartOfCapInfo(useExp);
+                            HouseLogEvent?.Invoke(House.houseSn, DateTime.Now, $"[{House.houseSn}][{PortName}][{item.wellSn}号well拍照曝光={useExp}{(wellExp.HasValue && wellExp.Value > 0 ? "(标定)" : "(house默认兜底)")},设置结果={setRet}(0成功)]", LogEnum.HouseInfo);
+                            // 等新曝光生效再拍(对焦曝光二分同款保险:连续采集下让设置传导到传感器)。
+                            // 注:其后还有 VerticalMotorAbsoluteWait 电机到位阻塞(秒级),本睡眠仅兜底,故取小值。
+                            if (setRet == 0) Thread.Sleep(Math.Max(100, useExp / 5));
+                        }
+                    }
+                    catch (Exception expEx)
+                    {
+                        ExceptionLogEvent?.Invoke(expEx, $"[{House.houseSn}][{PortName}][{item.wellSn}号well设拍照曝光失败,沿用相机当前曝光]", null, LogEnum.RunException);
+                    }
                     currentPicNum = 1;
                     var newList = _ccdPhoto.Where(x => x.wellSn == item.wellSn && x.clearest <= (allPicNum - 1) / 2 && x.clearest >= (-(allPicNum - 1) / 2)).OrderBy(x => x.clearest).ToList();
                     DateTime SavePictrueTime = DateTime.Now;

+ 2 - 0
ivf_tl_operate_2.0/control/ivf_tl_Control/AppData.cs

@@ -112,6 +112,8 @@ namespace ivf_tl_Control
                     DBService.SaveAutofocusCalibration(tlSn, houseSn, wellSn, scene, focusZ, exposure, horizontalPulse, peakRatio, circleFound, centerOffsetPct, note, source),
                 // M2-06 安全门降级:关闭本地对焦时按 scene=0 基准位置拍照,由此回调读基准 FocusZ。
                 BaselineReader = (tlSn, houseSn, wellSn) => DBService.GetBaselineFocusZ(tlSn, houseSn, wellSn),
+                // 拍照按孔设曝光:读该 well 标定曝光(scene=1 优先/回退 scene=0),使采集照与对焦照曝光一致。
+                ExposureReader = (tlSn, houseSn, wellSn) => DBService.GetCalibratedExposure(tlSn, houseSn, wellSn),
             };
 
             KafkaService = new KafkaService(kafkaTopic, kfkaIP);

+ 7 - 0
ivf_tl_operate_2.0/control/ivf_tl_Entity/GlobalEntitys/HouseWellPhoto.cs

@@ -28,5 +28,12 @@ namespace IvfTl.Control.Entity.GlobalEntitys
         /// 0表示自动对焦 1表示清晰图层 其余均为拍照图层
         /// </summary>
         public int positionType { get; set; }
+
+        /// <summary>
+        /// 该 well 标定曝光(×100µs,来自 house_autofocus_calibration.exposure,对焦曝光二分算得)。
+        /// 拍照时按孔设此曝光,使采集照与对焦照曝光一致(修"拍照偏亮")。
+        /// 0=未标定/无值 → 拍照兜底用 House.ccdExposure(house 级默认)。
+        /// </summary>
+        public int exposure { get; set; }
     }
 }

+ 33 - 0
ivf_tl_operate_2.0/control/ivf_tl_Services/DBService.cs

@@ -875,6 +875,39 @@ namespace IvfTl.Control.Services
             }
         }
 
+        /// <summary>
+        /// 读该 well 标定曝光(×100µs):优先 scene=1 日常对焦最近一条(当次实测曝光),
+        /// 无则回退 scene=0 出厂基准。供 CCD 拍照按孔设曝光,使采集照与对焦照曝光一致(修拍照偏亮)。
+        /// 取到 0/负值视为无效返回 null(调用方兜底 House.ccdExposure)。读库异常吞掉记日志,绝不向上抛。
+        /// </summary>
+        public int? GetCalibratedExposure(string tlSn, int houseSn, int wellSn)
+        {
+            try
+            {
+                var row = Db.Queryable<HouseAutofocusCalibrationDB>()
+                    .Where(x => x.tlSn == tlSn && x.houseSn == houseSn && x.wellSn == wellSn
+                                && x.scene == 1 && x.deleted == null)
+                    .OrderBy(x => x.calibTime, OrderByType.Desc)
+                    .First();
+                if (row == null)
+                {
+                    // 日常对焦无记录(如安全门关闭未对焦),回退出厂基准 scene=0。
+                    row = Db.Queryable<HouseAutofocusCalibrationDB>()
+                        .Where(x => x.tlSn == tlSn && x.houseSn == houseSn && x.wellSn == wellSn
+                                    && x.scene == 0 && x.deleted == null)
+                        .OrderBy(x => x.calibTime, OrderByType.Desc)
+                        .First();
+                }
+                if (row == null || !row.exposure.HasValue || row.exposure.Value <= 0) return null;
+                return row.exposure.Value;
+            }
+            catch (Exception ex)
+            {
+                ExceptionLogEvent?.Invoke(ex, "DBServiceImpl.GetCalibratedExposure", null, LogEnum.DbException);
+                return null;
+            }
+        }
+
         /// <summary>
         /// G4-1 / 需求文档12 §2.7:对焦标定数据清理任务。
         /// 物理删除 house_autofocus_calibration 中 scene=1(日常对焦) 且 calibTime 早于 (now - keepDays) 的记录;

+ 28 - 0
项目文档/进度/D2-02-第三阶段-自动对焦重构-特殊情况记录.md

@@ -216,3 +216,31 @@
 - **修复**:`aivfo-gateway` local profile `routes[5].uri` 改为 `http://${server.ip}:8888`,重编译网关 + 重启。
 - **验证(闭环)**:登录拿真 token → `http://127.0.0.1:10010/group1/...?token=<真token>` → **HTTP 200,636786 字节(完整图)**。原图(source_image_url)和抠图(image_url)都走此 `/group1` 路由,**一并解决 operate 看原图 + 看抠图位置**。
 - 注:`/group1` 路由经网关需有效 token(operate 登录后自带,故能过;无/假 token 返回"请登陆后再操作")。网关重启后 operate 无需重启,下次看图请求自动走新路由。
+
+#### [Phase5·待修] CCD拍照曝光太亮 = 对焦重构"每孔曝光没接到拍照"(精确定位,待用户定方案)  — 2026-06-26 深夜
+- **现象(用户反馈)**:operate 图片显示已正常(网关修复后);但**拍照(CCD)曝光太亮,对焦照曝光好**。用户怀疑拍照没用对焦的曝光值。**经查属实**。
+- **需求依据**:需求文档 §53/§75 明确——采集时算「当次 FocusZ/水平/**曝光**」存 scene=1,**拍照复用**;每 well 四步算法找最清晰 FocusZ+居中水平位+**曝光**。即拍照本应用对焦标定的每孔曝光。
+- **精确根因(一处)**:`HouseBin.cs:1643` 填采集内存结构时
+  `_autoFocusPhoto.Add(new HouseWellPhoto { wellSn = well, verticalMotorPosition = wc.FocusZ });`
+  —— 对焦结果 `wc`(WellCalib)**同时有 FocusZ 和 Exposure**,但这里**只取了 FocusZ、丢了 Exposure**。拍照(:1916 取 _autoFocusPhoto)拿不到每孔曝光 → 只能用 house 级固定 `ccdExposure`(:549/:949 `GetCamera(...House.ccdExposure)`)→ 偏亮。
+- **对照老代码**:老代码对焦**无**每孔曝光二分,对焦/拍照都用 house `ccdExposure`(一致,不会"对焦好拍照亮")。是**新代码对焦重构新增了每孔曝光优化、但没接到拍照**(遗漏)。
+- **修复方案(部件齐全,待实施)**:① `HouseWellPhoto` 内存类加 `exposure` 字段;② 填充处带 `wc.Exposure`(:1643;scene=0 降级 :1535/:1544 用 house `ccdExposure` 或 eeprom 兜底);③ 拍照按孔 `Camera.SetPartOfCapInfo(exposure)` 设曝光再拍(Camera 已有此方法)。改后重编 control + 同步 operate\control\ 引擎 + 真机拍一轮看曝光观感。
+- **为何先不改**:改采集拍照核心逻辑、且需真机确认曝光观感(用户不在不好判断),scene=0 兜底取值也需用户定。**精确根因+方案已备,待用户拍板**。
+
+#### [Phase5·澄清] "结束培养后培养记录消失" = 非bug,数据在"历史/结束培养"列表  — 2026-06-26 深夜
+- **现象**:用户结束培养后,培养记录列表里看不到了。
+- **真相(DB实证)**:结束的两条记录(TEST-AF-CH2/TEST-AF-NORMAL)**都还在,deleted 是未删标记(2017-01-01)**,只是 `state` 从 1(培养中)→0(结束)。`endCultureRecord`=`stopCultivation`(**改状态,不删数据**)。
+- **原因**:培养记录列表默认查 `state=UNDER_CULTIVATION(1,正在培养)`(EmbryoCultureRecordManageImpl:376);查询枚举有「正在培养(2)/结束培养历史(3)/全部(1)」三维度。结束的记录转到「**结束培养/历史**」列表。
+- **结论**:**数据完全安全,非bug**,是"正在培养列表不显示已结束记录"的设计。已结束记录在"历史/结束培养"tab 可见。无需修复(除非产品要改默认显示)。
+
+#### [Phase5·已实施] CCD拍照按孔曝光 = 拍照时从标定库读回每孔曝光再设相机(方案落地+编译同步)  — 2026-06-27 凌晨
+- **用户拍板**:"拍照也用对焦标定的每孔曝光"。"今晚一起改,干就对了"。
+- **实现路径(比原文档①②③更稳:读库回放,而非把曝光塞进采集内存结构)**:
+  - ① `HouseWellPhoto` 加 `exposure` 字段(内存类,纯加字段无副作用)。
+  - ② `CalibrationStore` 加 `ExposureReader` 回调(仿 BaselineReader,委托避免反向依赖 SqlSugar);`AppData.cs` 绑定到 `DBService.GetCalibratedExposure`。
+  - ③ `DBService.GetCalibratedExposure(tlSn,houseSn,wellSn)`:查 `house_autofocus_calibration`,**scene=1 日常最新优先(calibTime desc),回退 scene=0 基准**,无值/exposure<=0 返回 null。
+  - ④ `HouseBin.ccdThreadFun` well 循环里(垂直复位后、逐层拍照前):读 `AutofocusStore.ExposureReader`,>0 用标定值否则兜底 `House.ccdExposure`,`Camera.SetPartOfCapInfo(useExp)` 设曝光 + 记日志;**设后 `Thread.Sleep(Max(100,useExp/5))`** 让连续采集流新曝光生效(对焦曝光二分同款保险),后续电机到位阻塞(秒级)进一步保证不取到旧曝光帧。try/catch 包裹,设曝光失败只记日志不崩采集线程,沿用相机当前曝光。
+  - **关键澄清**:曝光值 `wc.Exposure` 本就经既有 `SaveCalibration→DbMirror` 落 `house_autofocus_calibration.exposure`,**无需新表/新列**,缺口只在"拍照时读回"。原文档①②(塞 _autoFocusPhoto 内存结构)未采用——读库更解耦、且 scene 优先级/兜底集中在一处。
+- **编译同步**:`control/ivf_tl_Control.sln` Release 编译 **0 错**;`ivf_tl_ControlHost` 输出(80文件)`cp` 同步到 `ivf_tl_Operate/bin/Release/.../control/`,三关键 DLL(ivf_tl_Com/ivf_tl_Control/IvfTl.AutoFocus)时间戳更新、DependFile/DB 保留。
+- ★**排查中发现(待真机验证前提)**:仓库内**所有 `aivfoTL.db` 均为 8-28 空模板,无 `house_autofocus_calibration` 表;全盘无 `DependFile/AutoFocus/calibration.json`、无运行时写过的库**。但 `DBService.StartDbService` 启动会 `CodeFirst.InitTables` 自动建该表(CREATE IF NOT EXISTS)——**说明 ControlHost 引擎此刻没在跑**(进程列表证实只有 operate.exe,无 ControlHost/Watchdog),静态库自然没标定数据。**真机验证曝光的前提 = 先让对焦引擎跑起来、跑一轮对焦标定(写入 scene=1 exposure),再拍照对比观感**。
+- **待真机验证**:启动 control 引擎 → 跑对焦标定(确认 `house_autofocus_calibration` 有 exposure)→ 拍照 → 对比对焦照与采集照曝光是否一致(修"拍照偏亮")。