2026.05.11征图日记11(调试LiDAR部分的代码)
上来依旧先打开电脑看之前剩下的需求,等待ai的过程依旧刷linuxdo
为什么有些计算的部分,比如计算缺陷面积,要由前端app来完成?因为如果用后端计算的话,需要把前端拿到的深度图数据传到后端,后端计算完再返回前端。如果在前端计算:在拍照的时候同时调用激光雷达,获取深度图并保存,把照片上传,后端返回bbox和mask,前端拿到mask里面的缺陷像素,对应深度图进行计算。
用户还有可能会对照片进行裁剪,思考如何在兼容之前的代码的情况下实现深度图与剪裁后的照片的像素映射
核心矛盾: 照片被裁剪后只覆盖部分 FOV,但深度图仍然覆盖完整 FOV。measurement.ts 中的 scaleX = depthWidth / maskW 假设 mask 覆盖完整 FOV,这在裁剪后不再成立。
但是有没有可能,被检测平面是与手机是平行的情况,并且被检测平面的深度差异很小,无法对齐的情况只会在手机斜放拍照,也就是图片具有透视效果的时候会对检测结果有影响,拍照时只需注意即可
可以根据这个文档实现,但是还可以在设置界面保留选择,如果手机平行拍照就直接使用平均深度计算了,如果有透视情况再对每个像素进行积分
今日工作内容
1、代码实现调用激光雷达,获取深度图并计算缺陷面积
2、app实现平均深度和像素级积分两种测量方案
3、解决app的模型展示界面在实机上不显示图片的问题
下阶段计划
1、解决app实装在手机上调用摄像头报错的问题
2、测试app缺陷面积计算逻辑正确性
今日遗留问题:
用户点击拍照之后弹出“深度采集超时”日志如下:
1 | 错误 17:51:03.610496+0800 thermalmonitord <Error> could not find `cpu-avg-limiter-input-w2r property |
1 | Illegal callback invocation from nativemodule. This callback type only permits : |
用户在实机上点击拍照的时候有机率出现这个报错,分析一下出现这个报错是什么原因,另外用户点击拍照后会等一会才弹出拍照界面,这个可能是后台在调用激光雷达,但是检测完成之后又显示没有获取到深度图
近期vibecoding的一些小经验:做一个项目、需求的时候,先和ai讨论,生成技术文档。注意对比多个方案的优缺点,一定要选好方向,做好调研分析,让ai查询多个方案的优缺点。之后拆分成小需求。在CLAUDE.md里面做好编码规范。如果是多端项目,先让ai做web端,让ai参考web做mobile
深度图与裁剪照片像素映射对齐方案
Context
用户拍照后可通过 ImageCropPicker 裁剪照片,但当前 CaptureScreen.handleResult 丢弃了 cropRect 字段,导致深度图(覆盖完整 FOV)与裁剪后的照片(覆盖部分 FOV)空间不对齐。measurement.ts 中 scaleX = depthWidth / maskW 的映射假设 mask 覆盖完整 FOV,裁剪后该假设不再成立。
核心策略:预裁剪(Store + Apply)
在 CaptureScreen 拿到 cropRect 后立即用 cropDepthData 裁剪深度图,使其与裁剪后照片覆盖相同的 FOV。后续所有代码零改动。
需要修改的文件
1. mobile/ios/ZhengtuVision/DepthCaptureModule.swift
- 新增 native 方法获取 RGB 相机拍照原始分辨率(4032x3024,通过型号查找表)
2. mobile/ios/ZhengtuVision/DepthCaptureModule.m
- 导出新增的 native 方法
3. mobile/src/native/DepthCapture.ts
- 新增
getPhotoResolution()方法封装
4. mobile/src/types/models.ts
CapturedImage接口新增cropRect、origWidth、origHeight可选字段
5. mobile/src/utils/depthTransform.ts
- 新增
deriveDepthCropFromCropRect函数(基于实际 cropRect 而非居中假设) - 保留旧的
deriveDepthCrop作为无 cropRect 时的回退
6. mobile/src/utils/image.ts
compressImage返回时保留cropRect、origWidth、origHeight字段
7. mobile/src/screens/CaptureScreen.tsx
handleResult: 提取img.cropRect,获取原始分辨率,调用深度图裁剪pick: 拍照时加includeExif: true
不需要修改的文件
measurement.ts— 零改动(预裁剪后 scaleX/scaleY 自动正确)inferenceStore.ts— CapturedImage 类型扩展,zustand 自动序列化ResultScreen.tsx— 零改动useInference.ts— 零改动
获取原始照片分辨率的三层回退策略
- Native helper:iPhone 型号查找表返回 4032x3024(所有 LiDAR 机型宽摄像头一致)
- cropRect + 深度图宽高比估算:利用
cropRect.x + cropRect.width作为最小宽度约束 - 兜底:不裁剪深度图,等价于当前行为
边界情况
| 场景 | 处理 |
|---|---|
| 用户不裁剪 | cropRect 为 null,跳过深度裁剪,与当前行为一致 |
| 相册选图 | 无 pendingDepth,不影响 |
| cropRect 越界 | try/catch 捕获,回退全幅深度图 |
| 历史回放 | 深度图在采集时已被裁剪,面积计算自然正确 |
验证方式
npx tsc -p tsconfig.json --noEmit确认类型通过- 真机测试场景:拍照不裁剪 → 拍照居中裁剪 → 拍照偏角裁剪 → 相册选图 → 历史回放
- 对比裁剪前后面积计算结果,确认像素映射正确
实机 Bug 修复记录
日期:2026-05-11
问题 1:模型信息展示界面图片无法显示
现象:安装在实机上的时候,模型信息展示界面(ModelInfoScreen)的图片无法显示。
根因:content.ts 使用 Image.resolveAssetSource().uri 获取图片路径,在 iOS 实机 (release build) 上返回 file:// 协议的绝对路径。这些路径嵌入到 WebView 的 HTML <img src> 中后,底层 WKWebView 因沙盒安全限制禁止加载 file:// 资源,导致图片空白。
修复方案:将所有图片预转为 base64 data URI,直接嵌入 HTML,绕过文件系统访问限制。
修改文件:
- 新建
mobile/scripts/generate-image-base64.ts— 图片转 base64 的构建脚本 - 新建
mobile/src/assets/model-info/imageBase64Map.ts— 自动生成的 base64 映射文件(22 张图片,原始 1.2MB) - 修改
mobile/src/assets/model-info/content.ts— 改为从 base64 映射文件导入
后续维护:当 assets/model-info/images/ 下新增或替换图片时,在 mobile/ 目录执行:
1 | npx tsx scripts/generate-image-base64.ts |
问题 2:摄像头 & 激光雷达代码二次回调缺陷
现象:原生 LiDAR 采集模块存在 5 个关键问题,可能导致二次 Promise resolve、摄像头资源泄漏、线程安全问题。
发现的问题清单:
| 序号 | 问题 | 严重性 | 影响 |
|---|---|---|---|
| 1️⃣ | stopCapture() 方法完全未实现 |
🔴 关键 | TypeScript 端无法停止采集,资源泄漏 |
| 2️⃣ | completion 缺防重复调用标志 |
🔴 关键 | 快速连续拍照时可能二次 resolve |
| 3️⃣ | Delegate 回调未切换主线程 | 🟠 高 | Promise 在后台线程 resolve,线程竞态 |
| 4️⃣ | Session 替换时旧资源未清理 | 🟠 高 | AVCaptureSession 泄漏,旧 delegate 持续回调 |
| 5️⃣ | TypeScript 端缺重试机制 | 🟡 中 | 超时后无恢复手段,用户体验差 |
根因分析:
DepthCaptureModule.m声明了stopCapture()但 Swift 代码中只有private func stop(),无@objc public实现LiDARCaptureSession的completion闭包缺乏状态保护,在高并发或网络延迟时可能被多次调用- React Native Bridge 要求 Promise resolve/reject 必须在主线程,但 delegate 回调在后台串行队列中
- 每次创建新 session 时未显式清理旧 session,导致资源泄漏和二次回调
修复方案(详见 camera-lidar-audit-report.md & DepthCaptureModule.swift.fixed):
实现
stopCapture()@objc 方法1
2
3
4
5
6@objc func stopCapture() {
if #available(iOS 15.4, *) {
self.session?.stop()
self.session = nil
}
}添加完成标志与锁保护
1
2
3
4
5private var hasCompleted = false
private let completionLock = NSLock()
guard !hasCompleted else { return }
hasCompleted = trueDelegate 回调切换主线程
1
2
3DispatchQueue.main.async {
self.completion?(resultDict)
}Session 替换时显式清理
1
2
3
4if let oldSession = self.session {
oldSession.stop()
}
self.session = nilTypeScript 端添加重试机制
1
async captureWithRetry(maxRetries: number = 1, timeoutMs: number = 3000)
修改文件:
mobile/ios/DepthCaptureModule.swift— 核心修复(5 处)mobile/src/native/DepthCapture.ts— 添加重试、改进日志(可选但推荐)mobile/ios/DepthCaptureModule.m— 无需改动
验证方法:
1 | // 测试二次回调 |
预期改进:
- ✅
stopCapture()正常工作,超时时立即释放摄像头 - ✅ 二次回调风险消除,Promise 语义保证
- ✅ 线程安全性提升,消除竞态
- ✅ 资源泄漏大幅降低
- ✅ 超时自动重试,用户体验改善
---
## 问题 2:点击开始检测后再点拍照没有反应
**现象**:安装在实机上的时候,用户点击"开始检测"进入拍照页面,再点击"拍照"按钮没有反应。
**根因**:默认测量方法为 LiDAR 模式时,拍照前调用 `DepthCapture.capture()` 进行深度预采集。其 Swift 实现通过 `AVCaptureSession` 打开 LiDAR 后置摄像头,用 `dataOutputSynchronizer` 代理异步回调获取深度数据。两个故障模式:
1. **Promise 永久挂起**:代理回调中 `depthDataWasDropped == true` 时直接 return 不调用 completion,导致 JS 端 `await` 永不解锁,阻塞后续 `ImageCropPicker.openCamera()`
2. **摄像头硬件竞争**:`AVCaptureSession.stop()` 后硬件释放有延迟,立即打开系统相机 UI 可能因资源未释放而失败
**修复方案**:在 JS 端添加超时保护和缓冲延时。
- `withTimeout(promise, 3000ms)` — 3 秒超时保护,超时后直接跳过深度采集继续拍照
- 300ms 缓冲延时 — 深度采集成功后在打开相机前等待硬件释放
**修改文件**:
- `mobile/src/screens/CaptureScreen.tsx` — 添加超时工具函数和深度采集保护逻辑
- 修改 `mobile/ios/ZhengtuVision/DepthCaptureModule.swift` — 添加 `stopCapture()` 方法
- 修改 `mobile/ios/ZhengtuVision/DepthCaptureModule.m` — 暴露 `stopCapture` 到 RN bridge
---
## 问题 3:拍照时偶发 Illegal callback invocation 崩溃,且无深度图
**现象**:实机上点击拍照按钮有机率出现 `RCTFatal: Illegal callback invocation from native module` 崩溃,堆栈指向 `ImageCropPicker imagePickerControllerDidCancel:`。同时拍照会延迟一段时间才弹出相机界面,检测完成后显示未获取到深度图。
**根因**:问题 2 的修复中,`withTimeout` 超时返回 `undefined` 后**未停止原生 LiDAR 会话**,直接调用 `ImageCropPicker.openCamera()`。此时原生端 `AVCaptureSession` 仍在运行并占用后置摄像头,导致:
1. **崩溃**:`UIImagePickerController` 因摄像头被占用而异常关闭,iOS 向 `imagePickerControllerDidCancel:` 发送多次回调,RN 检测到同一 callback 被二次 invoke 后触发 `RCTFatal`
2. **延迟**:LiDAR 会话启动到超时(3 秒)加上相机界面异常处理的时间
3. **无深度图**:超时后没有存储深度数据,后续推理流程无深度可用
**修复方案**:三步修复——
1. 原生端新增 `stopCapture()` 方法,主动停止 LiDAR 会话并释放摄像头硬件
2. TS 封装层暴露该方法
3. CaptureScreen 超时/异常路径中调用 `stopCapture()`,且缓冲延时对所有路径(成功/超时/异常)生效,确保相机硬件完全释放后再打开拍照界面
**修改文件**:
- `mobile/ios/ZhengtuVision/DepthCaptureModule.swift` — 添加 `stopCapture()` 到 `DepthCaptureModule`;将 `LiDARCaptureSession.stop()` 从 `private` 改为 `internal`
- `mobile/ios/ZhengtuVision/DepthCaptureModule.m` — bridge 文件暴露 `stopCapture` 方法
- `mobile/ios/DepthCaptureModule.m` — 同步更新
- `mobile/src/native/DepthCapture.ts` — 添加 `stopCapture()` 类型声明与封装
- `mobile/src/screens/CaptureScreen.tsx` — 超时/异常路径调用 `DepthCapture.stopCapture()`,缓冲延时移到所有路径共用
### 代码审查发现的额外问题(同次修复)
**Bug A — RN Promise 回调泄漏**:`stop()` 只停止 AVCaptureSession,不调用 `resolve`/`reject`。JS 超时后调用 `stopCapture()` → `stop()`,导致 `captureDepthSnapshot` 的 Promise 回调永不被调用,RN bridge 中的回调对无法释放。
**Bug B — 跨线程竞争**:`stop()` 在主线程写 `depthOutput = nil`,delegate 在后台队列 `queue` 上读 `self.depthOutput`。无同步机制,存在竞争窗口。
**Bug C — 双重 rejection**:delegate 错误路径(`baseAddr` 为空)调用 `rejection()` 但未标记 `finished`,`cancel()` 随后也会调用 `rejection()`,触发 RN 双重 invoke 检测。
**修复**(均在 `DepthCaptureModule.swift`):
- 新增 `cancel()` 方法:dispatch 到 delegate 所在串行队列执行,通过 `finished` 标志与 delegate 互斥,确保 `resolve`/`reject` 仅调用一次
- 新增 `finished` 布尔标志,delegate 的两种出口(成功/失败)均设置 `finished = true`
- `stopCapture()` 改为调用 `s.cancel()` 而非 `s.stop()`
- `captureDepthSnapshot` 创建新 session 前取消旧 session,`start()` 失败时清理并 return(防御性改进)
### 审计对照说明
外部审计报告(`.claude/CHECK-SUMMARY.md`)提出的 5 个问题中,发现 1/2 在审查前已修复,发现 3(主线程 dispatch)对 RN bridge 架构不必要,发现 4 已补充,发现 5(重试机制)为可选优化。注意 `.fixed` 文件删除了相机内参提取(fx/fy/cx/cy),会导致面积测量失效,不应直接替换。
---
## 问题 4:Xcode 编译的是旧版 DepthCaptureModule,所有之前的修复未实际生效
**发现日期**:2026-05-11
**现象**:问题 2/3 的修复代码虽然写入了 `ZhengtuVision/DepthCaptureModule.swift`,但实机上 LiDAR 仍然存在二次回调、无法停止、缺少内参等问题。`getCameraPhotoResolution()` 从 JS 调用始终返回 `null`。
**根因**:Xcode 项目文件(`project.pbxproj`)中 "ZhengtuVision" 组是**虚拟组**(有 `name` 但无 `path` 属性),因此组内文件路径相对于父组(项目根目录 `mobile/ios/`)。标准 RN 模板文件(AppDelegate.h 等)的 `path` 包含 `ZhengtuVision/` 前缀,正确指向子目录;但 `DepthCaptureModule.swift` 和 `DepthCaptureModule.m` 的 `path` **没有**这个前缀,导致 Xcode 编译的是 `mobile/ios/DepthCaptureModule.*`(根目录旧版),而非 `mobile/ios/ZhengtuVision/DepthCaptureModule.*`(修复版)。
**根目录旧版 vs ZhengtuVision 修复版差异对照**:
| 特性 | 根目录旧版 (5972B) | ZhengtuVision 修复版 (8507B) |
|------|-------------------|------------------------------|
| `getCameraPhotoResolution()` | ❌ 缺失 | ✅ 有 |
| `stopCapture()` | ❌ 缺失 | ✅ 有(调用 cancel) |
| `finished` 标志 | ❌ 无 | ✅ 有(防止双重 resolve/reject) |
| `cancel()` 方法 | ❌ 无 | ✅ 有(串行队列互斥) |
| 旧 session 清理 | ❌ 无 | ✅ 有 |
| 相机内参提取 (fx/fy/cx/cy) | ❌ 缺失,completion 无这些 key | ✅ 有,从 calibrationData 提取 |
| `start()` 失败时清理 | ❌ 无 | ✅ 调用 s.stop() |
**对应的 `.m` 桥接文件差异**:
| 导出方法 | 根目录 (518B) | ZhengtuVision (652B) |
|----------|--------------|---------------------|
| `isLiDARAvailable` | ✅ | ✅ |
| `captureDepthSnapshot` | ✅ | ✅ |
| `getCameraPhotoResolution` | ❌ 缺失 | ✅ |
| `stopCapture` | ✅ | ✅ |
**修复**(共 4 步):
1. 将 ZhengtuVision 修复版 `.swift` 内容覆盖到根目录 `mobile/ios/DepthCaptureModule.swift`,同时新增 `validCount == 0` 保护(原版无此保护)
2. 将完整的 `.m` 桥接声明覆盖到根目录 `mobile/ios/DepthCaptureModule.m`
3. 删除 ZhengtuVision 目录下的孤儿副本文件
4. `mobile/ios/DepthCaptureModule.swift` 需 `git add` (之前为 untracked)
---
## 问题 5:`cropDepthData` 展开运算符导致真机栈溢出
**现象**:LiDAR 拍照后深度裁剪时,Hermes JS 引擎抛出 `RangeError: Maximum call stack size exceeded`,App 崩溃。
**根因**:`depthTransform.ts` 的 `cropDepthData` 函数中两处使用展开运算符传递大数组:
- 第 166 行:`String.fromCharCode(...bytes)` — `bytes` 为 `Uint8Array(width * height * 2)`,LiDAR 分辨率 256×192 时约 98,304 个参数
- 第 174-175 行:`Math.min(...dstData.filter(v => v > 0))` — 过滤后的数组可达 49,152 个参数
Hermes 引擎对函数参数数量有限制,超过约 60K 参数会导致栈溢出。
**修复**(`mobile/src/utils/depthTransform.ts`):
- `String.fromCharCode` 改为分块拼接,每块 4096 字节
- `Math.min/max` 改为在已有的拷贝循环中直接比较,消除二次遍历
---
## 问题 6:双击拍照按钮导致深度数据错配的竞态条件
**现象**:用户快速双击拍照按钮后,第一张照片关联的可能是第二次采集的深度图,导致面积计算使用错误的深度值。
**根因**:`CaptureScreen` 使用 `useRef<DepthSnapshot>` 作为所有 `pick()` 调用共享的可变引用。`pick()` 是 async 函数——用户在第一次调用的 `await ImageCropPicker.openCamera()` 等待期间可触发第二次 `pick()`,后者重置并覆盖 `pendingDepth.current`。
**修复**(`mobile/src/screens/CaptureScreen.tsx`):
- 移除 `useRef`,改用 `pick()` 内的局部变量 `depthForThisPick`
- `handleResult` 通过参数接收深度快照,而非从共享 ref 读取
- 每次 `pick()` 调用的深度数据完全隔离
---
## 完整修改文件清单(2026-05-11 第二轮)
| 文件 | 修改内容 |
|------|----------|
| `mobile/ios/DepthCaptureModule.swift` | 覆盖为修复版:+getCameraPhotoResolution、+stopCapture、+finished/cancel、+内参提取、+validCount==0 保护 |
| `mobile/ios/DepthCaptureModule.m` | +getCameraPhotoResolution 桥接声明 |
| `mobile/ios/ZhengtuVision/DepthCaptureModule.swift` | 已删除(孤儿文件) |
| `mobile/ios/ZhengtuVision/DepthCaptureModule.m` | 已删除(孤儿文件) |
| `mobile/src/utils/depthTransform.ts` | 展开运算符改分块编码和循环比较 |
| `mobile/src/screens/CaptureScreen.tsx` | useRef 改为局部变量,handleResult 改为参数接收 |