2026.05.20征图日记18(说好的open house呢?)

今天先改了App的名字,之前拼错了是Focusight不是Foucsight,之后就和mentor在进行最后的调整

改完了之后就回去自己位置刷力扣了

感觉这个App不是要用来展示的,现在也没有说去展示现场调试,或者通知我们啥的,玩我呢!?

还有一件事就是公司在马来西亚有办事处,所以打算用马来的同事订阅claudecode,再想办法搭一个中转站到国内,听说实习生好像也能用,酥胡~

LiDAR理论上是依靠飞行时间差来计算距离的,所以距离越近误差越大,所以在10-20cm这个距离的误差是很大的,但恰巧因为缺陷很小,app的拍摄距离也落到了这个区间,所以用iphone来测量微小缺陷面积本来就不合适,硬件层面的误差就能把你耗死

和Ai合作的时候要特别注意Ai的谄媚性,是指你在修改方案的时候,无论你说什么,Ai都会找到你这个方案的亮点,从而对你进行附和。从而让你忽略掉一些方案设计上的缺陷,这些缺陷在Ai的上下文不全的情况下,Ai是无法判断的,这个时候是需要你自己注意并进行判断

字符串解码

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
class Solution {
public:
string decodeString(string s) {
int n = s.size();
// <左括号前的倍数, 左括号前的字符串>
stack<pair<int, string>> stk;
int time = 0; // 记录当前的倍数(注意初始化为 0)
string res = ""; // 记录当前的字符串
for (int i = 0; i < n; ++i) {
char c = s[i];
if (c >= '0' && c <= '9') {
// 处理多位数字
time = time * 10 + (c - '0');
} else if (c >= 'a' && c <= 'z') {
// 如果是字母,直接加到当前 result 里
res += c;
} else if (c == '[') {
// 遇到 '[',开始存档,并清空当前状态给括号内使用
stk.push({time, res});
res = "";
time = 0;
} else if (c == ']') {
// 遇到 ']',提取存档
auto Pair = stk.top();
stk.pop();
// 将括号里的内容 (也就是现在的 result) 翻倍
string tmp = "";
for (int j = 0; j < Pair.first; j++)
tmp += res;
// 拼接到进括号前的历史字符串后面,成为新的 result
res = Pair.second + tmp;
}
}
return res;
}
};

今日工作内容

  1. 将 App 内所有品牌文案从 征图/ZhengtuVision 改为 Focusight Vision,修改 app.json、Info.plist、LaunchScreen.storyboard、AppDelegate.mm。

  2. 修复 Swift 原生相机模块距离显示硬编码中文,改为通过 React props 传入 i18n 字符串。修复历史记录和结果页缺陷数量摘要未国际化问题。

  3. 从设置页移除标定法和距离法选项,开启缺陷面积测量后直接走 LiDAR。核心计算代码和 CalibrationOverlay 组件保留不动。

  4. ImageCropPicker 默认 width/height 为 200×200,导致所有裁切/未裁切图片都被缩放到 200×150,缺陷 mask 像素数偏差 10 倍,面积测量严重不准。修复:openCropper 和 openPicker 中显式设置 width: 400, height: 300。同时将推理提示文案统一为’正在推理中…’。(这个修改之后还是不准后续还需修改)

  5. 将缺陷测量方法从根据掩膜像素积分改为根据深度图像素积分,具体方案在 “上下文摘要:缺陷面积测量深度像素积分方案” 中

下阶段计划

  1. 继续优化App交互与测量

上下文摘要:缺陷面积测量深度像素积分方案

生成时间:2026-05-20


一、涉及的关键文件

文件 职责
Swift mobile/ios/UnifiedCameraModule.swift 统一相机:单 AVCaptureSession 双输出(RGB+LiDAR)
Swift mobile/ios/DepthCaptureModule.swift 独立 LiDAR 模块:预热、单帧快照、连续流(legacy)
ObjC mobile/ios/UnifiedCameraModule.m RN bridge
ObjC mobile/ios/DepthCaptureModule.m RN bridge
TS 封装 mobile/src/native/UnifiedCamera.ts capturePhotoWithDepth() 零时差采集
TS 封装 mobile/src/native/DepthCapture.ts 独立 LiDAR TS 封装(legacy)
TS 工具 mobile/src/utils/depthTransform.ts 深度图裁剪:RGB cropRect → 深度坐标系映射
TS 工具 mobile/src/utils/image.ts 图片压缩(仅长边 >4096 时生效)
TS 核心 mobile/src/utils/measurement.ts 面积计算引擎:三种测量方法 + 逐缺陷面积(已改动
TS 类型 mobile/src/types/measurement.ts AreaResult、CalculationContext、MeasurementSession 等
页面 mobile/src/screens/CaptureScreen.tsx 拍照→裁剪→压缩→存图+深度到 store
页面 mobile/src/screens/ResultScreen.tsx 调用面积计算,传给 AreaDisplay 和 DetectionList
页面 mobile/src/screens/ConfigScreen.tsx downscale 参数(1-4)设置界面
组件 mobile/src/components/result/AreaDisplay.tsx 总面积展示面板
组件 mobile/src/components/result/DetectionList.tsx 逐缺陷面积列表
Store mobile/src/store/inferenceStore.ts fx/fy、depthSnapshot、measurementSession 存储
Store mobile/src/store/settingsStore.ts measureAreaEnabled 开关
后端 backend/app/core/pipeline.py downscale 参数生效位置(width//=downscale)
后端 backend/app/utils/image_io.py downscale_if_needed(仅长边 >4096 时自动缩放)
后端 backend/app/services/inference_service.py 推理服务编排,返回 image_size(已 downscale 后)

二、完整数据流

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
1. 拍照                 unifiedCamera.capturePhotoWithDepth()
↓ ↓
RGB 4032×3024 LiDAR 深度 256×192
↓ ↓
2. 用户裁剪 ImageCropPicker: 400×300 deriveDepthCropFromCropRect
↓ ↓
3. 压缩/裁切 compressImage(空操作) cropDepthData(深度同步裁切)
↓ ↓
4. 存 Store setImage + setDepthSnapshot + setFx/setFy

5. 送后端推理 POST /detect (image + downscale + ...)

6. 后端处理 bytes_to_ndarray → downscale_if_needed(空操作,400远小于4096)
pipeline.py: width=400//downscale, height=300//downscale

7. 模型推理 GroundingDINO + TRT → 掩膜 + bboxes

8. 返回前端 DetectResponse { image_size: (w_downscaled, h_downscaled), segmentation.data, detections }

9. 面积计算 用户点击「测定缺陷面积」

perPixelIntegrate(掩膜, 深度图, fx_depth, fy_depth)

掩膜像素 → 双线性投票 → 深度像素桶 → 积分面积

AreaDisplay + DetectionList 展示

三、之前的方案是什么

3.1 积分框架:掩膜像素为单位

perPixelIntegrate 的核心逻辑(measurement.ts 旧版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (let my = 0; my < maskH; my++) {
for (let mx = 0; mx < maskW; mx++) {
if (mask.rgba[...] <= 128) continue;
totalCount++;

const dx = Math.round(mx * (depthW / maskW)); // 最近邻映射
const dy = Math.round(my * (depthH / maskH));

const z = depthLookupWithFallback(depthData, dx, dy); // 回退搜索
if (z > 0) {
areaSum += z * z / (fx * fy); // ← 深度内参直接代入
depthSum += z;
validCount++;
}
}
}

循环以掩膜像素为粒度,每个缺陷掩膜像素累加一次 Z²/(fx_depth × fy_depth)

3.2 像素映射:最近邻 + 回退搜索

depthLookupWithFallback:先查目标深度像素的 Z 值,无效时向外扩展最多 3 像素搜索有效深度值(曼哈顿环状搜索),跨过深度不连续面时可能取到错误表面的深度。

3.3 内参:深度摄像头的 fx_depth/fy_depth

fx/fy 来自 DepthFrameProcessor(Swift),从 depthData.cameraCalibrationData.intrinsicMatrix 提取并按深度图实际分辨率缩放。


四、旧方案有什么缺点

4.1 核心 Bug:漏乘分辨率缩放因子

Z²/(fx_depth × fy_depth) 是一个深度像素的物理面积,而循环遍历的是掩膜像素。两者覆盖相同 FOV 但分辨率不同:

场景 掩膜分辨率 深度图分辨率 每个掩膜像素面积是深度像素面积的
downscale=1 400×300 152×114 0.144 倍
downscale=2 200×150 152×114 0.578 倍
downscale=4 100×75 152×114 2.31 倍

每个掩膜像素应该累加的是 Z²/(fx_mask × fy_mask),而非 Z²/(fx_depth × fy_depth)。两者关系:fx_mask = fx_depth × (maskW/depthW)

缺失的修正因子(depthW × depthH) / (maskW × maskH)。当前漏乘,导致面积结果随 downscale 参数变化——同一块物理缺陷,换个参数面积就变。

downscale 面积偏差
1 高估约 6.9 倍
2 高估约 1.7 倍
4 低估约 57%

4.2 回退搜索可能取错深度

depthLookupWithFallback 在目标深度像素无效时向外扩展 3 像素。缺陷位于物体边缘时,回退可能跨越深度不连续面,取到背景的深度而非缺陷所在表面的深度。

4.3 深度噪声被平方放大但无抵消机制

逐像素 Z² 累加,每个像素的独立测量噪声被平方放大后累加(而不是通过平均来抵消)。改用深度像素方案后,多个掩膜像素共享一个 Z 值,等效于对噪声做了局部分组平均。

4.4 其他误差源(方案改进不涉及的)

以下误差源属于 LiDAR 硬件的根本性限制,本文讨论的方案改动不直接解决,但深度像素方案的覆盖率诚实反映了数据质量:

因素 影响程度 说明
深度图分辨率低(~256×192) 空间精度受限于 LiDAR 传感器
LiDAR 深度噪声 ~1cm 精度,Z² 放大误差
Float32→Uint16 量化 中低 亚毫米精度丢失
表面倾斜未补偿 中低 公式假设表面垂直于光轴
RGB-LiDAR 基线视差 中低 两个传感器有物理位置差

五、如何改进(两种可选方案)

方案 A:掩膜像素方案(加缩放因子)

保持现有循环结构,在面积累加后乘修正因子:

1
area = Σ z²/(fx_depth × fy_depth) × (depthW × depthH) / (maskW × maskH)

优点:改动量最小,一行代码。

缺点

  • 公式不直观,修正因子容易在后续维护中被遗漏
  • 仍需推导 fx_mask(多一环变换)
  • 回退搜索问题依旧
  • 广角主摄的 RGB 相机没有工厂标定内参可用(见第九节)

方案 B:深度像素方案(推荐,已采用)

改变积分框架:以深度像素为积分单位,掩膜像素”投票”到深度像素桶中。

优点

  • 内参直接用深度摄像头的工厂标定值,无需推导
  • 分辨率比例内嵌在 coverage 中,不会遗漏
  • 无效深度直接跳过,覆盖率真实,不猜测
  • 推导链短,维护风险低

缺点:改动量较大(需重写积分循环),需要额外的深度像素桶数组(~17KB 内存)。

两种方案数学等价,差异在工程属性而非计算结果。


六、改进后的方案是什么

6.1 核心算法:双线性投票 + 深度像素积分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
第一步:初始化深度像素权重桶
bucket = new Float64Array(depthW × depthH)

第二步:遍历掩膜缺陷像素,按双线性权重投票
for each mask pixel (mx, my) where mask > 128:
fx_coord = mx × (depthW / maskW) // 深度图浮点坐标
fy_coord = my × (depthH / maskH)

ix0 = floor(fx_coord), iy0 = floor(fy_coord)
ix1 = min(ix0+1, depthW-1), iy1 = min(iy0+1, depthH-1)
wx = fx_coord - ix0, wy = fy_coord - iy0

bucket[iy0][ix0] += (1-wx)(1-wy) // 左上
bucket[iy0][ix1] += wx(1-wy) // 右上
bucket[iy1][ix0] += (1-wx)wy // 左下
bucket[iy1][ix1] += wx·wy // 右下

第三步:以深度像素为单位积分面积
for each depth pixel (di, dj):
weight = bucket[dj][di]
if weight == 0 || Z(di, dj) <= 0: continue

area += weight × Z² / (fx_depth × fy_depth)

6.2 双线性权重的作用

掩膜像素在深度图坐标系中是一个 sub-pixel 矩形(宽 scaleX,高 scaleY),该矩形可能与最多 4 个深度像素相交。双线性分配按几何占地比例划分权重,权重之和恒为 1。相比最近邻(整颗分配给一个深度像素),双线性在边缘处平滑过渡,不同 downscale 下面积更稳定。

1
2
3
4
5
6
7
8
9
     dx=76     dx=77
┌────────┬────────┐
dy=57 │ A │ B │
│ ┌────┼──┐ │ ← 掩膜像素对应的 sub-pixel 矩形
│ │ │ │ │ 按与 A/B/C/D 相交面积比例分配权重
│ └────┼──┘ │
├────────┼────────┤
dy=58 │ C │ D │
└────────┴────────┘

6.3 为什么面积不随 downscale 变化

假设某个深度像素对应物理区域内有 10 处缺陷:

downscale maskW×maskH 该区域掩膜像素数 N N/总掩膜像素/pixelPerDepth fraction
1 120000 10 10/6.93 1.44
2 30000 ~2.5 2.5/1.73 1.44
4 7500 ~0.625 0.625/0.43 1.44

N 和 pixelsPerDepthPixel 随 downscale 同比例缩放,fraction 恒定,面积不变。


七、为什么要这么改(深度像素 > 掩膜像素的七个理由)

# 理由 掩膜像素方案 深度像素方案
1 内参来源 需推导 fx_mask = fx_depth × (maskW/depthW) 直接用工厂标定 fx_depth
2 公式自洽性 z²/(fx×fy) × 修正因子 不直观 coverage × Z²/(fx×fy) 匹配物理直觉
3 分辨率比例 显式乘因子,容易遗漏 内嵌在 bucket 的 fraction 中
4 回退搜索 需要外扩 3 像素,可能取错深度 无效深度直接跳过,不猜测
5 覆盖率语义 被回退搜索高估 真实反映深度数据可用性
6 推导链 fx_depth → fx_mask → 修正因子 → 积分(3 步) fx_depth → 积分(1 步)
7 维护性 高(当前 bug 就是证据)

八、具体是如何改动的

改动文件:mobile/src/utils/measurement.ts,三处变更,调用方接口不变。

8.1 删除:depthLookupWithFallback 函数(原 128-152 行)

不再需要回退搜索,深度无效像素直接跳过。该函数无其他调用方。

8.2 重写:perPixelIntegrate(原 159-228 行)

循环单位 掩膜像素 先掩膜像素(投票),再深度像素(积分)
像素映射 最近邻 Math.round(mx*scaleX) 双线性分配到四个邻近深度像素
Z 获取 depthLookupWithFallback 回退搜索 直接从深度数组索引
面积累加 z²/(fx×fy) 裸值 weight × z²/(fx×fy),weight 隐含分辨率修正
深度无效处理 回退搜索取邻近值 跳过不累加,覆盖率降低反映真实数据质量

8.3 重写:perPixelIntegrateBbox(原 233-273 行)

同理改动,但 bucket 范围限定在 bbox 对应的深度像素区域(局部桶,减少内存分配)。

8.4 不改的部分

  • calculateArea — 调用接口不变,内部仍调用 perPixelIntegrate
  • calculatePerDetectionAreasLidar — 调用接口不变,内部仍调用 perPixelIntegrateBbox
  • 标定法(calibrateAreacalculateCalibratedArea)、距离法(distanceArea
  • 掩膜解码(countDefectPixelsgetMaskDatacountPixelsInBbox
  • 深度解码(decodeDepthUint16
  • ResultScreen、AreaDisplay、DetectionList 等 TSX/组件

8.5 验证

1
2
cd mobile && npm run lint  # 通过,零错误
npm run typecheck # 通过,零错误

九、硬件背景

9.1 使用的摄像头

builtInLiDARDepthCamera,对应后置广角主摄(Wide),超广角和长焦完全不参与。

9.2 分辨率

广角主摄照片:4032×3024(约 12MP)。LiDAR 深度图:约 256×192。

9.3 时空对齐

RGB 和 LiDAR 封装在同一逻辑设备中(builtInLiDARDepthCamera),共享时钟源。单个 AVCaptureSession 同时输出 AVCapturePhotoOutputAVCaptureDepthDataOutput,硬件层面同步触发,出厂做过 RGB-LiDAR 联合几何标定。深度图返回时已 warp 对齐到 RGB 坐标系。

9.4 RGB 内参为什么拿不到

Apple 只为深度数据流提供 cameraCalibrationDataAVDepthData 属性),内含 intrinsicMatrix(工厂逐台标定的深度摄像头内参)。广角主摄的 RGB 照片流没有等价接口,只能反推(从 sensor 尺寸或 FOV 计算设计值,精度低于工厂标定)或硬编码(按机型维护型号表)。深度像素方案绕开了这个限制,直接使用深度摄像头的出厂标定内参。


十、相关文档

  • .trellis/tasks/05-20-depth-pixel-integration/prd.md — PRD 需求文档
  • .trellis/tasks/05-20-depth-pixel-integration/research/why-depth-pixel.md — 深度像素方案详解