正射模块-odm_orthophoto

OdmOrthoPhoto 是一个正射影像生成器,用于从带纹理的 3D 网格模型(OBJ 格式)生成正射校正的航拍图像。这是无人机摄影测量后端处理的关键组件,属于 ODM项目的一部分。

1、正射影像生成器相关

1.1 关键技术栈

技术 用途
Eigen 3D 仿射变换、向量运算
OpenCV 纹理读取、Inpaint 修复、图像处理
GDAL GeoTIFF 输出、空间参考、地理编码
OpenMP Inpaint 并行加速

1.2 输入输出

输入:OBJ 网格文件 + 纹理图片 (PNG)

输出:GeoTIFF 正射影像 + Alpha 通道

1.3 工作流程

python 复制代码
1. 参数解析  → 2. 网格加载  →  3. 边界计算   →  4. 纹理渲染  (光栅化)  
 →    5. 边缘修复(Inpaint)    →   6. GeoTIFF(输出 )

1.3.1 OBJ/MTL 文件解析 (loadObjFile)

支持解析的内容:

python 复制代码
v - 顶点坐标 (x, y, z)
vt - 纹理坐标 (u, v)
f - 面(顶点/纹理索引)
usemtl - 材质引用
mtllib - MTL 材质库文件

数据结构 TextureMesh

python 复制代码
struct TextureMesh {
    std::vector<PointXYZ> vertices;           // 3D 顶点
    std::vector<Tex2D> uvs;                   // UV 坐标
    std::unordered_map<std::string, cv::Mat> materials;  // 纹理图
    std::unordered_map<std::string, std::vector<Face>> faces; // 面按材质分组
    std::vector<std::string> materials_idx;   // 材质名称列表
};

1.3.2 边界计算与坐标变换 (computeBoundsForModel, getROITransform)

边界计算: 遍历所有顶点,找出 X-Y 平面的最小/最大范围

pyhon 复制代码
Point 1: (xMin, yMin)    
Point 2: (xMin, yMax)
Point 3: (xMax, yMax)    
Point 4: (xMax, yMin)

仿射变换矩阵:

T=[r00−xmin⁡⋅r0−r0−ymin⁡⋅r00100001],where r=resolution T = \begin{bmatrix} r & 0 & 0 & -x_{\min} \cdot r \\ 0 & -r & 0 & -y_{\min} \cdot r \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}, \quad \text{where } r = \text{resolution} T= r0000−r000010−xmin⋅r−ymin⋅r01 ,where r=resolution

其中,Y 轴镜像(便于从下到上渲染)

这个4×4 矩阵是3D 空间到 2D 图像空间的坐标变换,用于将世界坐标系中的网格顶点映射到正射影像的像素坐标。本质上就是对Y的值做了一个翻转,然后归一化坐标,并进行缩放,用这个仿射变换只是一种实现的形式。

1.3.3 纹理三角形光栅化 (drawTexturedTriangle)

核心问题

输入: 一个三角形,三个顶点各带一个纹理坐标 (UV)

python 复制代码
v1 ──→ 贴图上的点 (0.2, 0.8)
v2 ──→ 贴图上的点 (0.5, 0.1)  
v3 ──→ 贴图上的点 (0.9, 0.6)

任务: 把这个三角形"贴"到屏幕上,三角形内部每个像素应该显示贴图上的什么颜色?

屏幕上的三角形内部如果有几千个像素,每个像素该取贴图的哪个位置?

解决思路

通俗理解,想象三角形三个顶点是三个"灯",每个灯照亮三角形内部:

靠近 v1 的地方,v1 的"影响力"最大

靠近 v2 的地方,v2 的"影响力"最大

三个影响力的总和永远是 1

重心坐标 (l1, l2, l3) 就是这个"影响力权重",例如P 点的重心坐标 = (0.5, 0.3, 0.2),含义:v1 占 50%, v2 占 30%, v3 占 20%。

用重心坐标找纹理颜色

关键思路

如果 P 点在三角形中的权重是 (0.5, 0.3, 0.2),那么 P 点的纹理坐标也是三个顶点 UV 的同样比例混合。

python 复制代码
P 的 U 坐标 = 0.5×u1 + 0.3×u2 + 0.2×u3
P 的 V 坐标 = 0.5×v1 + 0.3×v2 + 0.2×v3

然后拿着 (U, V) 去纹理图上采样颜色!

重心公式:

python 复制代码
l1 = h1 / H1  
l2 = h2 / H2
l3 = h3 / H3
其中, H1 是 v1 到对边的总高度, h1是P点到对边的高度。

注意在这个阶段的三角形是2D像素平面上的三角形,顶点是像素坐标(列,行),不同阶段的三角形是变化的:

阶段 三角形类型 坐标含义 示例
OBJ 文件读取后 3D 空间三角形 顶点是世界坐标 (米) v1: (17.05, -31.02, 340.06) , v2: (17.01, -30.09, 340.23),v3: (16.66, -30.87, 340.13)
仿射变换后 2D 像素平面三角形 顶点是像素坐标 (列,行) v1': (列=512, 行=1024, z=340.06),v2': (列=510, 行=998, z=340.23),v3': (列=498, 行=1020, z=340.13)
光栅化时 2D 像素平面三角形 用像素坐标扫描,深度值 (z) 用于 Z-buffer v1t: (0.123, 0.456),v2t: (0.234, 0.567),v3t: (0.345, 0.678)

三角形的三个顶点有 UV 坐标,三角形覆盖区域内的所有像素,都要通过重心坐标插值,去纹理图上找对应的颜色。

如果纹理图不止一张怎么办

这正是 mtllib/usemtl 材质系统要解决的问题,当有多张纹理图时,数据结构如下:

python 复制代码
mesh.faces = {
    "material_0": [face1, face2, face3...],   ← 用 texture0 的三角形
    "material_1": [face4, face5, face6...],   ← 用 texture1 的三角形
    "material_2": [face7, face8, face9...],   ← 用 texture2 的三角形
}

mesh.materials = {
    "material_0": cv::Mat(texture0),
    "material_1": cv::Mat(texture1),
    "material_2": cv::Mat(texture2),
}

同一个三角型只能对应一个纹理图,当两个纹理图对应的三角形有重叠会根据视角远近覆盖,覆盖是像素级的,核心原则如下:

规则 说明
1. 一个三角形只能属于一个材质 OBJ 文件中每个 f 面只能有一个 usemtl 引用
2. 重叠像素按深度决定谁显示 Z-buffer 深度测试,近的覆盖远的

1.3.4 双线性纹理采样 (renderPixel)

这个算法发生在通过坐标去纹理图取颜色的阶段,出现的原因是计算出来坐标值不是整数,这个时候当然可以直接取整拿到坐标最接近的像素点颜色,但是好像这种做法有点糙。所以引入了双线性纹理采样。

python 复制代码
计算出来的采样点坐标:(2.7, 3.4)
真实像素坐标:(2,3), (3,3)...

双线性纹理采样的核心思想就是让周围的4个像素投票,距离近的票权重越大。

例如:

python 复制代码
最终颜色 = A 的颜色×0.42 + B 的颜色×0.28 + C 的颜色×0.18 + D 的颜色×0.12

距离越近权重越大,距离越远权重越小,有点像前面三角形求重心的方法。

1.3.5 多波段图像合成 (saveTIFF)

所谓的多波段实际就是把一幅图像分别差分成R、G、B、A4个单独的通道分开管理,每个通道可以单独分析处理。在渲染时当渲染了某个像素,填充了RGB后对应的R、G、B胶片的值就进行一次更新,同时A对应的位置加3,所有三角都渲染完成后,判断A中的值,数值<3设置成0(不透明),否则设置成255(透明)。最后把所有通道打包成一个 GeoTIFF 文件。

1.3.6 边缘修复 (inpaint)

边缘修复的场景是当有多个纹理需要拼接时,有时会在接缝出产生明显的边界,主要原因是不同纹理图的色调不一致。比如纹理1是在阳光充足时拍的,纹理2是在有云遮挡时拍的,这会导致最终的地图上有明显的分块。

出现接缝的主要原因如下:

python 复制代码
不同纹理图色调不一致:光照变化
3D 模型投影变形:投影变形导致边缘像素不连续
深度突变:建筑物和地面这类深度突然发生变化的区域

核心思想:用周围的颜色"智能填充"有问题的区域

方法流程:

使用 OpenCV 的 Telea 算法修复接缝:

分块处理 - 将大图分为 1024x1024 的块(支持 OpenMP 并行)

计算深度梯度 - 检测深度突变区域

生成掩膜 - 梯度超过阈值的区域标记为需要修复

执行 Inpaint - 使用周围像素填充接缝

python 复制代码
cv::inpaint(input, inpaintMask, output, 5, cv::INPAINT_TELEA);

注意这个修复方法也是有局限性的:

Inpaint 能修复 Inpaint 不能修复
有颜色但颜色不连续(接缝) 根本没有颜色的区域(孔洞)
纹理拼接处的色差 3D 重建缺失导致的空白
深度突变处的边缘 Alpha 通道为 0 的区域

2、编译

python 复制代码
WORKDIR /root/odm_orthophoto
COPY lib/odm_orthophoto /root/odm_orthophoto
RUN rm -rf build && mkdir -p build && cd build \
    && cmake -DCMAKE_BUILD_TYPE=Release .. \
    && make -j$(nproc) \
    && make install 

3、报段错误

3.1 错误原因

python 复制代码
 /usr/local/bin/odm_orthophoto -inputFiles ./odm_textured_model_geo.obj -resolution 25 -inpaintThreshold 0.05 2>&1

执行上面命令直接报了段错误,只能根据源码解决,经过调试发现/root/odm_orthophoto/src/OdmOrthoPhoto.cpp 文件中的 renderPixel 函数,缺少纹理边界检查,原代码在进行双线性插值时,需要访问 4 个像素:

  • (top, left) - 左上
  • (top, left + 1) - 右上
  • (top + 1, left) - 左下
  • (top + 1, left + 1) - 右下

但当 UV 坐标接近 1.0 时,top 可能等于 texture.rows - 1,此时 top + 1 就会越界访问,导致段错误。

3.2 修复方法

修复逻辑

将 left 和 top 钳制在有效范围内,确保 top + 1 和 left + 1 不会超出纹理尺寸:

如果 top = texture.rows - 2,则 top + 1 = texture.rows - 1(最后一个有效像素)

如果 top 原本超出范围,会被钳制到 texture.rows - 2

修改位置
OdmOrthoPhoto.cpp961-965 行(在计算 lefttop 之后)

修改内容

添加了 4 行边界钳制代码:

c++ 复制代码
// Boundary check: clamp to valid texture range to prevent segfault
// We need 2x2 pixels for bilinear interpolation, so check bounds accordingly
if (left < 0) left = 0;
if (top < 0) top = 0;
if (left + 1 >= texture.cols) left = texture.cols - 2;
if (top + 1 >= texture.rows) top = texture.rows - 2;

修改后已重新编译并测试通过,正射影像生成成功。

相关推荐
Hali_Botebie2 小时前
【3D 生成技术】3D Gaussian Splatting 的基础理解
3d
3DVisionary4 小时前
【深度实测】从三坐标到全场扫描:汽车精密锻铸件 3D 质检的数字化进阶
3d·汽车·质量检测·精密制造·三维扫描仪·汽车工业·xtop3d
GIS数据转换器6 小时前
基于AI+无人机的城市巡检系统
人工智能·3d·无人机·知识图谱·旅游
CG_MAGIC7 小时前
零基础学 UV:简单解决贴图拉伸
3d·贴图·uv·效果图·建模教程·渲云渲染
HashTang7 小时前
用 AI 对话式驱动的开源 3D 建筑设计编辑器-Aedifex
人工智能·3d·编辑器
m0_743106467 小时前
【浙大&南洋理工最新综述】Feed-Forward 3D Scene Modeling(五)
人工智能·算法·计算机视觉·3d·几何学
ZC跨境爬虫1 天前
3D地球卫星轨道可视化平台开发 Day15(添加卫星系列模糊搜索功能)
前端·数据库·3d·交互·数据可视化
kobesdu1 天前
开源3D激光SLAM算法的异同点、优劣势与适配场景总结
算法·3d·机器人·ros
ZC跨境爬虫1 天前
3D 地球卫星轨道可视化平台开发 Day13(卫星可视化交互优化+丝滑悬停聚焦)
前端·算法·3d·json·交互