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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
错误	17:51:03.610496+0800	thermalmonitord	<Error> could not find `cpu-avg-limiter-input-w2r property
错误 17:51:03.610512+0800 thermalmonitord <Error> Failed to read `cpu-avg-limiter-input-w2r
错误 17:51:03.733865+0800 appleh16camerad getProperty: Incoming query from client (pid <private>), num pending requests = 1
错误 17:51:03.733896+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Scheduling the lux query on the serial queue
错误 17:51:03.733906+0800 appleh16camerad GetLuxInfo_block_invoke - GetLuxInfo_block_invoke: Waiting for lux
错误 17:51:03.866056+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Sending lux, ret = 0x0
错误 17:51:03.935842+0800 maild nw_flow_add_read_request [C2620 IPv4#c9a641fd:993 ready parent-flow (satisfied (Path is satisfied), interface: pdp_ip0[nr_sa_sub6], ipv4, ipv6, dns, expensive, uses cell, estimated upload: 8388608Bps, estimated download: 268435456Bps, LQM: moderate)] already delivered final read, cannot accept read requests
错误 17:51:03.936415+0800 maild nw_read_request_report [C2620] Receive failed with error "No message available on STREAM"
错误 17:51:03.936610+0800 maild [01-BK] [C2620] Connection did fail (1 running command(s)): posix [96: No message available on STREAM]
错误 17:51:03.944883+0800 maild [01-BK] Connection failed. Failed commands: 0. Network errors: 1. Error Domain=Network.NWError Code=96 UserInfo={NSDescription=<private>}
错误 17:51:03.945139+0800 maild [C2620 Hostname#31a7a89e:993 tcp, bundle id: com.apple.mobilemail, account id: c197b060, tls, attribution: website] is already cancelled, ignoring cancel
错误 17:51:03.946214+0800 maild [C2620 Hostname#31a7a89e:993 tcp, bundle id: com.apple.mobilemail, account id: c197b060, tls, attribution: website] is already cancelled, ignoring cancel
错误 17:51:03.946288+0800 maild [C2620 Hostname#31a7a89e:993 tcp, bundle id: com.apple.mobilemail, account id: c197b060, tls, attribution: website] is already cancelled, ignoring forced cancel
错误 17:51:04.040905+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d4f10
错误 17:51:04.070194+0800 maild tcp_input [C2620.1.1:3] flags=[R] seq=3990733518, ack=0, win=0 state=LAST_ACK rcv_nxt=3990733518, snd_una=1419413117
错误 17:51:04.120498+0800 kernel netagent_get_agent_domain_and_type: Type requested for invalid netagent
错误 17:51:04.120678+0800 kernel netagent_get_agent_domain_and_type: Type requested for invalid netagent
错误 17:51:04.144383+0800 symptomsd 0x288d36070 unexpected radio technology from baseband: 0, defaulting to unknown
错误 17:51:04.145809+0800 symptomsd 0x288d36070 unexpected radio technology from baseband: 0, defaulting to unknown
错误 17:51:04.147017+0800 symptomsd 0x288d36070 unexpected radio technology from baseband: 0, defaulting to unknown
错误 17:51:04.147226+0800 financed CoreData: warning: cache_handle_memory_pressure invoked for core 0xb03070000 / 9B23E3A6-DD6F-4B96-A8E8-C6936B3627A0
错误 17:51:04.150465+0800 financed CoreData: warning: cache_handle_memory_pressure invoked for core 0xb031c9040 / 2B8B5D37-91BE-4131-99D1-F0BF91E8EBCF
错误 17:51:04.151716+0800 symptomsd 0x288d36070 unexpected radio technology from baseband: 0, defaulting to unknown
错误 17:51:04.151726+0800 financed CoreData: warning: cache_handle_memory_pressure invoked for core 0xb0323cc80 / 9B23E3A6-DD6F-4B96-A8E8-C6936B3627A0
错误 17:51:04.152970+0800 symptomsd 0x288d36070 unexpected radio technology from baseband: 0, defaulting to unknown
错误 17:51:04.155085+0800 symptomsd 0x288d36070 unexpected radio technology from baseband: 0, defaulting to unknown
错误 17:51:04.158899+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d448e
错误 17:51:04.158915+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d4485
错误 17:51:04.158941+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d448e
错误 17:51:04.158945+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d447c
错误 17:51:04.158948+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d448e
错误 17:51:04.158957+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d4473
错误 17:51:04.158967+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d448e
错误 17:51:04.158970+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d446a
错误 17:51:04.158978+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d4f90
错误 17:51:04.158982+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d4f30
错误 17:51:04.165190+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d4f90
错误 17:51:04.166188+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d42a0
错误 17:51:04.166198+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d4280
错误 17:51:04.166207+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d4260
错误 17:51:04.166215+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d4220
错误 17:51:04.166266+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d4f90
错误 17:51:04.166277+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d36f0
错误 17:51:04.166301+0800 kernel log,F4AC6F35-8B2E-3A78-B12F-F61A24633E1A,10d5c10
错误 17:51:04.773025+0800 appleh16camerad getProperty: Incoming query from client (pid <private>), num pending requests = 1
错误 17:51:04.773062+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Scheduling the lux query on the serial queue
错误 17:51:04.773075+0800 appleh16camerad GetLuxInfo_block_invoke - GetLuxInfo_block_invoke: Waiting for lux
错误 17:51:04.794569+0800 dasd Error obtaining RBS process handle: Error Domain=RBSRequestErrorDomain Code=3 UserInfo={NSLocalizedFailureReason=<private>}
错误 17:51:04.795521+0800 dasd Error obtaining RBS process handle: Error Domain=RBSRequestErrorDomain Code=3 UserInfo={NSLocalizedFailureReason=<private>}
错误 17:51:04.796458+0800 dasd Error obtaining RBS process handle: Error Domain=RBSRequestErrorDomain Code=3 UserInfo={NSLocalizedFailureReason=<private>}
错误 17:51:04.798269+0800 dasd Error obtaining RBS process handle: Error Domain=RBSRequestErrorDomain Code=3 UserInfo={NSLocalizedFailureReason=<private>}
错误 17:51:04.889710+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Sending lux, ret = 0x0
错误 17:51:05.269556+0800 contextstored Dispatching call to deprecated registration callback for <private>, this may lead to priority problems. Switch to non-deprecated _CDContextualChangeRegistration APIs.
错误 17:51:05.823961+0800 appleh16camerad getProperty: Incoming query from client (pid <private>), num pending requests = 1
错误 17:51:05.823999+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Scheduling the lux query on the serial queue
错误 17:51:05.824012+0800 appleh16camerad GetLuxInfo_block_invoke - GetLuxInfo_block_invoke: Waiting for lux
错误 17:51:05.914149+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Sending lux, ret = 0x0
错误 17:51:06.851176+0800 appleh16camerad getProperty: Incoming query from client (pid <private>), num pending requests = 1
错误 17:51:06.851238+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Scheduling the lux query on the serial queue
错误 17:51:06.851269+0800 appleh16camerad GetLuxInfo_block_invoke - GetLuxInfo_block_invoke: Waiting for lux
错误 17:51:06.938025+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Sending lux, ret = 0x0
错误 17:51:07.578825+0800 cameracaptured <<<< BWFigCaptureDeviceVendor >>>> -[BWFigCaptureDeviceVendor _setupDeviceCloseTimerForDevice:reason:]_block_invoke: Unknown timer cancelled: <private>. Known timer: <private>
错误 17:51:07.588824+0800 kernel Error setting grimaldi power state
错误 17:51:07.599198+0800 appleh16camerad Failed to send the com.apple.applecamerad.S2RPowerStatus event into the diagnostics system 00000000
错误 17:51:07.599205+0800 appleh16camerad Failed to send report to analyticsd: E00002BC
错误 17:51:07.599220+0800 appleh16camerad Failed to send the com.apple.applecamerad.SifErrorDetection event into the diagnostics system 0
错误 17:51:07.599323+0800 appleh16camerad Failed to report the Sensor Interface Error Statistics to analyticsd: E00002BC
错误 17:51:07.599529+0800 appleh16camerad Failed to send the com.apple.applecamerad.FaultManagementStatus event into the diagnostics system 0
错误 17:51:07.599649+0800 appleh16camerad Failed to send the com.apple.applecamerad.FlickerDetection event into the diagnostics system 00000000
错误 17:51:07.599747+0800 appleh16camerad Failed to report the ISP Flicker Detection to analyticsd: E00002BC
错误 17:51:07.599884+0800 appleh16camerad Failed to send the com.apple.tof.clocking event into the diagnostics system - 00000000
错误 17:51:07.643049+0800 maild [01] Did not receive any push registration info.
错误 17:51:07.656447+0800 kernel [ERROR] : enableFWIPCEP_gated: fISPEndpoints[1] is not ready
错误 17:51:07.892169+0800 appleh16camerad getProperty: Incoming query from client (pid <private>), num pending requests = 1
错误 17:51:07.892198+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Scheduling the lux query on the serial queue
错误 17:51:07.956442+0800 appleh16camerad GetLuxInfo_block_invoke - GetLuxInfo_block_invoke: Waiting for lux
错误 17:51:08.061184+0800 geod Canceling 0 in-flight resource loaders
错误 17:51:08.107270+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Sending lux, ret = 0x0
错误 17:51:08.109246+0800 kernel Error setting grimaldi power state
错误 17:51:08.612384+0800 thermalmonitord <Error> could not find `cpu-avg-limiter-input-w2r property
错误 17:51:08.612479+0800 thermalmonitord <Error> Failed to read `cpu-avg-limiter-input-w2r
错误 17:51:08.870527+0800 bluetoothd sendSmartRoutingInformation: Failed to send SR info message with status = 122
错误 17:51:08.870588+0800 bluetoothd ### SendSmartRoutingInformation failed: BT_ERROR_NOT_CONNECTED 'SendSmartRoutingInformation failed'
错误 17:51:08.870856+0800 bluetoothd sendSmartRoutingInformation: Failed to send SR info message with status = 122
错误 17:51:08.870923+0800 bluetoothd ### SendSmartRoutingInformation failed: BT_ERROR_NOT_CONNECTED 'SendSmartRoutingInformation failed'
错误 17:51:08.928951+0800 appleh16camerad getProperty: Incoming query from client (pid <private>), num pending requests = 1
错误 17:51:08.928971+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Scheduling the lux query on the serial queue
错误 17:51:08.990744+0800 appleh16camerad GetLuxInfo_block_invoke - GetLuxInfo_block_invoke: Waiting for lux
错误 17:51:09.142312+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Sending lux, ret = 0x0
错误 17:51:09.143969+0800 kernel Error setting grimaldi power state
错误 17:51:09.943757+0800 appleh16camerad getProperty: Incoming query from client (pid <private>), num pending requests = 1
错误 17:51:09.943793+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Scheduling the lux query on the serial queue
错误 17:51:10.008831+0800 appleh16camerad GetLuxInfo_block_invoke - GetLuxInfo_block_invoke: Waiting for lux
错误 17:51:10.159636+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Sending lux, ret = 0x0
错误 17:51:10.161330+0800 kernel Error setting grimaldi power state
错误 17:51:10.981204+0800 appleh16camerad getProperty: Incoming query from client (pid <private>), num pending requests = 1
错误 17:51:10.981234+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Scheduling the lux query on the serial queue
错误 17:51:11.045135+0800 appleh16camerad GetLuxInfo_block_invoke - GetLuxInfo_block_invoke: Waiting for lux
错误 17:51:11.195972+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Sending lux, ret = 0x0
错误 17:51:11.197160+0800 kernel Error setting grimaldi power state
错误 17:51:11.920481+0800 timed 7BAEB9D8-C980-454A-B2A3-427536E82809 (12) send failure (Error Domain=com.apple.identityservices.error Code=26 "Bluetooth required" UserInfo={NSLocalizedDescription=Bluetooth required, NSUnderlyingError=0x837032490 {Error Domain=com.apple.ids.idssenderrordomain Code=15 "(null)"}}), will retry
错误 17:51:11.923027+0800 timed 785D29B1-941A-456E-AAC4-8DD2C4BC1E29 (7) send failure (Error Domain=com.apple.identityservices.error Code=26 "Bluetooth required" UserInfo={NSLocalizedDescription=Bluetooth required, NSUnderlyingError=0x837033120 {Error Domain=com.apple.ids.idssenderrordomain Code=15 "(null)"}}), will retry
错误 17:51:11.923536+0800 timed D4037A39-AB86-4A89-94FF-2A3F35CC3469 (4-TMLSSourceDevice) send failure (Error Domain=com.apple.identityservices.error Code=26 "Bluetooth required" UserInfo={NSLocalizedDescription=Bluetooth required, NSUnderlyingError=0x837032fd0 {Error Domain=com.apple.ids.idssenderrordomain Code=15 "(null)"}}), will retry
错误 17:51:12.029233+0800 appleh16camerad getProperty: Incoming query from client (pid <private>), num pending requests = 1
错误 17:51:12.029263+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Scheduling the lux query on the serial queue
错误 17:51:12.092974+0800 appleh16camerad GetLuxInfo_block_invoke - GetLuxInfo_block_invoke: Waiting for lux
错误 17:51:12.243484+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Sending lux, ret = 0x0
错误 17:51:12.245410+0800 kernel Error setting grimaldi power state
错误 17:51:13.035588+0800 appleh16camerad getProperty: Incoming query from client (pid <private>), num pending requests = 1
错误 17:51:13.035609+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Scheduling the lux query on the serial queue
错误 17:51:13.099509+0800 appleh16camerad GetLuxInfo_block_invoke - GetLuxInfo_block_invoke: Waiting for lux
错误 17:51:13.250062+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Sending lux, ret = 0x0
错误 17:51:13.251630+0800 kernel Error setting grimaldi power state
错误 17:51:13.612082+0800 thermalmonitord <Error> could not find `cpu-avg-limiter-input-w2r property
错误 17:51:13.612128+0800 thermalmonitord <Error> Failed to read `cpu-avg-limiter-input-w2r
错误 17:51:14.073999+0800 appleh16camerad getProperty: Incoming query from client (pid <private>), num pending requests = 1
错误 17:51:14.074028+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Scheduling the lux query on the serial queue
错误 17:51:14.137693+0800 appleh16camerad GetLuxInfo_block_invoke - GetLuxInfo_block_invoke: Waiting for lux
错误 17:51:14.288675+0800 appleh16camerad GetLuxInfo - GetLuxInfo: Sending lux, ret = 0x0
错误 17:51:14.290009+0800 kernel Error setting grimaldi power state
错误 17:51:14.812155+0800 financed CoreData: warning: cache_handle_memory_pressure invoked for core 0xb0323cc80 / 9B23E3A6-DD6F-4B96-A8E8-C6936B3627A0
错误 17:51:14.812273+0800 financed CoreData: warning: cache_handle_memory_pressure invoked for core 0xb03070000 / 9B23E3A6-DD6F-4B96-A8E8-C6936B3627A0
错误 17:51:14.812294+0800 financed CoreData: warning: cache_handle_memory_pressure invoked for core 0xb031c9040 / 2B8B5D37-91BE-4131-99D1-F0BF91E8EBCF
1
2
3
4
5
6
Illegal callback invocation from nativemodule. This callback type only permits :
single invocation from native code.
RCTFatal
checkCallbackMultipleInvocations(bool*)
_41-[RcTModuleMethod processMethodSignature]_b1ock_invoke_2.73
50-[ImageCropPicker imagePickerControllerDidCancel:]_block_invoke

用户在实机上点击拍照的时候有机率出现这个报错,分析一下出现这个报错是什么原因,另外用户点击拍照后会等一会才弹出拍照界面,这个可能是后台在调用激光雷达,但是检测完成之后又显示没有获取到深度图

近期vibecoding的一些小经验:做一个项目、需求的时候,先和ai讨论,生成技术文档。注意对比多个方案的优缺点,一定要选好方向,做好调研分析,让ai查询多个方案的优缺点。之后拆分成小需求。在CLAUDE.md里面做好编码规范。如果是多端项目,先让ai做web端,让ai参考web做mobile

深度图与裁剪照片像素映射对齐方案

Context

用户拍照后可通过 ImageCropPicker 裁剪照片,但当前 CaptureScreen.handleResult 丢弃了 cropRect 字段,导致深度图(覆盖完整 FOV)与裁剪后的照片(覆盖部分 FOV)空间不对齐。measurement.tsscaleX = 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 接口新增 cropRectorigWidthorigHeight 可选字段

5. mobile/src/utils/depthTransform.ts

  • 新增 deriveDepthCropFromCropRect 函数(基于实际 cropRect 而非居中假设)
  • 保留旧的 deriveDepthCrop 作为无 cropRect 时的回退

6. mobile/src/utils/image.ts

  • compressImage 返回时保留 cropRectorigWidthorigHeight 字段

7. mobile/src/screens/CaptureScreen.tsx

  • handleResult: 提取 img.cropRect,获取原始分辨率,调用深度图裁剪
  • pick: 拍照时加 includeExif: true

不需要修改的文件

  • measurement.ts — 零改动(预裁剪后 scaleX/scaleY 自动正确)
  • inferenceStore.ts — CapturedImage 类型扩展,zustand 自动序列化
  • ResultScreen.tsx — 零改动
  • useInference.ts — 零改动

获取原始照片分辨率的三层回退策略

  1. Native helper:iPhone 型号查找表返回 4032x3024(所有 LiDAR 机型宽摄像头一致)
  2. cropRect + 深度图宽高比估算:利用 cropRect.x + cropRect.width 作为最小宽度约束
  3. 兜底:不裁剪深度图,等价于当前行为

边界情况

场景 处理
用户不裁剪 cropRect 为 null,跳过深度裁剪,与当前行为一致
相册选图 无 pendingDepth,不影响
cropRect 越界 try/catch 捕获,回退全幅深度图
历史回放 深度图在采集时已被裁剪,面积计算自然正确

验证方式

  1. npx tsc -p tsconfig.json --noEmit 确认类型通过
  2. 真机测试场景:拍照不裁剪 → 拍照居中裁剪 → 拍照偏角裁剪 → 相册选图 → 历史回放
  3. 对比裁剪前后面积计算结果,确认像素映射正确

实机 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 实现
  • LiDARCaptureSessioncompletion 闭包缺乏状态保护,在高并发或网络延迟时可能被多次调用
  • React Native Bridge 要求 Promise resolve/reject 必须在主线程,但 delegate 回调在后台串行队列中
  • 每次创建新 session 时未显式清理旧 session,导致资源泄漏和二次回调

修复方案(详见 camera-lidar-audit-report.md & DepthCaptureModule.swift.fixed):

  1. 实现 stopCapture() @objc 方法

    1
    2
    3
    4
    5
    6
    @objc func stopCapture() {
    if #available(iOS 15.4, *) {
    self.session?.stop()
    self.session = nil
    }
    }
  2. 添加完成标志与锁保护

    1
    2
    3
    4
    5
    private var hasCompleted = false
    private let completionLock = NSLock()

    guard !hasCompleted else { return }
    hasCompleted = true
  3. Delegate 回调切换主线程

    1
    2
    3
    DispatchQueue.main.async {
    self.completion?(resultDict)
    }
  4. Session 替换时显式清理

    1
    2
    3
    4
    if let oldSession = self.session {
    oldSession.stop()
    }
    self.session = nil
  5. TypeScript 端添加重试机制

    1
    async captureWithRetry(maxRetries: number = 1, timeoutMs: number = 3000)

修改文件

  • mobile/ios/DepthCaptureModule.swift — 核心修复(5 处)
  • mobile/src/native/DepthCapture.ts — 添加重试、改进日志(可选但推荐)
  • mobile/ios/DepthCaptureModule.m — 无需改动

验证方法

1
2
3
4
5
// 测试二次回调
const p1 = DepthCapture.capture();
const p2 = DepthCapture.capture();
const results = await Promise.allSettled([p1, p2]);
// 期望:一个成功,一个失败(或都成功但不重复 resolve)

预期改进

  • 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 改为参数接收 |