简介:这份资源是面向 Metashape Professional 用户的 Python 脚本开发参考手册,对应 1.8.2 版本,适合已具备一定 Python 基础、希望将摄影测量处理流程自动化的测绘、三维重建与遥感从业者。内容围绕 Metashape 模块展开,涵盖 Document 类与项目读写、块与相机及标记管理、相机校准与地面控制数据配置、照片对齐与密集云建模纹理化等处理步骤调用、模型与 DEM 及正射影像导出,以及网络处理任务的启动与控制,并附有模块索引与 API 变更日志便于查阅。资源包共 1 个 PDF 文件,约 1.16MB,为官方英文参考文档,结构按概述、应用模块、变更记录与索引组织,适合作为案头速查工具。目前已有 311 人学习,可帮助读者快速定位类与函数用法,减少脚本调试中的试错成本。
1. 脚本化摄影测量:为什么一个chunk对象能顶半支建模团队
很多人第一次看到 Metashape 的 Python 脚本,会觉得它不过是把界面上的按钮换成了函数调用——点一下「对齐照片」和写一行chunk.alignCameras(),能有多大差别?真正反直觉的地方在于:一旦处理流程被表达成 Python 对象树,摄影测量就从「一次性的手工操作」变成了「可复现、可参数化、可批量分发的计算管线」。你面对的不再是菜单,而是一个以Document → Chunk → Camera/Calibration为主干的活数据结构,照片对齐、深度图、密集云、网格、UV、纹理这些步骤,全部是对同一棵对象树施加的变换。搞懂这套 API 的底层机制,等于搞懂了摄影测量软件如何把「像素」翻译成「三维几何」的中间状态管理逻辑,这也是为什么值得把它的模块结构、坐标系约定和任务调度方式拆开看。
2. Document 与 Chunk:项目状态是怎么被组织成一棵对象树的
2.1 从Metashape.app.document说起:单例入口与多文档并存
这套 API 里最容易被忽略、却决定脚本骨架的设计,是「应用级单例」和「文档级实例」的分离。Metashape.app是一个Application对象,它在整个进程里只有一个,承载的是软件全局状态:版本号、GPU 设备枚举、界面语言、设置项、菜单注册、文件对话框。而Metashape.app.document指向的是「当前在界面里打开的那个项目」,它只是众多Document实例中的一个。
这个区分很关键。文档里明确写了「Multiple Document instances can be created simultaneously if needed」,意思是你可以Metashape.Document()新建一个游离于界面之外的项目对象,在后台跑批处理,完全不干扰用户正在看的那个。常见的实现思路是:app.document是一个弱引用式的「当前活动文档」指针,界面切换项目时它跟着变;而你自己 new 出来的Document是独立生命周期,需要自己open()、自己save()。
import Metashape # 界面当前文档:跟随用户操作切换 current = Metashape.app.document # 独立文档:后台批处理用,不占用界面 doc = Metashape.Document() doc.open("/data/site_a/project.psz") # 此时 current 和 doc 可以是两个完全不同的项目 print(Metashape.app.version) # 全局属性,与文档无关参数上要注意open()接受的是项目文件路径,.psz是压缩工程格式。这里有个血泪经验:app.document在没有任何项目打开时访问chunk会直接抛异常,所以批处理脚本一律用独立Document,别去赌界面状态。
2.2 Chunk 是处理阶段的容器,不是「一块数据」
Document下面挂的是chunk。很多人把 chunk 理解成「一次建模任务」,但更准确的说法是:chunk 是一个处理阶段的命名空间,它内部同时持有相机集合、标记点、对齐结果、深度图、密集云、网格、纹理、坐标系、地面控制点。也就是说,同一个 chunk 里,原始照片和最终网格是并存的,处理步骤只是在不断往这个容器里填新层。
这解释了为什么 API 的调用顺序天然是「先matchPhotos再alignCameras,先buildDepthMaps再buildDenseCloud」——每一步读取上一层产物、写入下一层产物,chunk 就是那个承载中间态的账本。doc.chunks[-1]取最后一个 chunk 是常见写法,但更稳的是按名字找:
chunk = doc.chunks[-1] # 或者按 label 定位,避免多 chunk 时取错 chunk = next(c for c in doc.chunks if c.label == "facade_south")chunk.label是字符串,doc.chunks支持add()、remove()。多 chunk 的典型用途是把一个大场景按区域切开分别对齐,最后再合并——因为对齐是全局优化,chunk 越大内存和误差累积越难控。
2.3 相机与标定:Calibration里那些系数到底在算什么
Calibration类是理解「为什么照片能变成三维」的钥匙。它存的不是像素,而是成像模型的参数:焦距f、主点cx/cy、径向畸变k1~k4、偏心畸变p1~p4、仿射项b1/b2、图像宽高。这些参数共同定义了一个从相机坐标系到像素坐标的投影函数,API 直接把它暴露成两个方法:
calib = chunk.sensors[0].calibration # 三维点 -> 像素坐标 pixel = calib.project(Metashape.Vector([1.0, 2.0, 5.0])) # 已知三维点和它的像素观测,算重投影误差 err = calib.error(Metashape.Vector([1.0, 2.0, 5.0]), Metashape.Vector([512.0, 384.0]))project()内部走的就是那套畸变模型:先把归一化坐标乘焦距加主点,再叠加径向和偏心畸变项。error()则是把投影结果和实际观测像素做差,返回二维误差向量。光束法平差(bundle adjustment)优化的目标函数,本质上就是所有error()的平方和最小化——所以这个方法是理解整个对齐质量指标的入口。
covariance_matrix和covariance_params则是对标定参数不确定度的描述,参数名列表告诉你哪些系数参与了协方差估计。实际项目里如果标定是从外部文件load()进来的,协方差可能是空的,这时候平差会把这些参数当固定值处理,不再优化——这是个容易翻车的点,后面会细说。
3. 处理管线:matchPhotos → alignCameras → buildDenseCloud的依赖链
3.1 特征匹配与对齐:downscale和预筛选参数在控制什么
官方示例里那串调用不是随便排的,它精确对应了摄影测量的标准流水线。先看前两步:
chunk.matchPhotos( downscale=1, # 特征提取的图像金字塔层级,1=全分辨率 generic_preselection=True, # 通用图像对预筛选 reference_preselection=False # 不依赖 GPS/IMU 参考坐标预筛选 ) chunk.alignCameras()matchPhotos做的是特征提取 + 图像对匹配,产出的是稀疏的连接点(tie points)。downscale控制特征检测在图像金字塔的哪一层做:值越大,用的图越小,速度快但特征少、精度低。generic_preselection让软件先按图像内容相似度筛出可能重叠的图对,避免 O(n²) 全量匹配;reference_preselection则是用相机位置先验(GPS 坐标)来筛图对,适合有定位数据的航拍。两者都关掉的话,匹配量会爆炸。
alignCameras()才是真正做光束法平差的一步:它拿匹配出的连接点,联合优化所有相机的位姿(位置 + 朝向)和场景稀疏点坐标。这一步的输出是每台相机的transform矩阵和稀疏点云。注意matchPhotos和alignCameras是分开的,这个拆分有实际意义——你可以先匹配、检查连接点数量,再决定要不要调参重跑对齐,而不用重复昂贵的特征提取。
3.2 深度图与密集云:filter_mode为什么影响几何质量
chunk.buildDepthMaps( downscale=4, filter_mode=Metashape.AggressiveFiltering ) chunk.buildDenseCloud()buildDepthMaps对每张(或每对)图像计算深度图,downscale=4表示在 1/4 分辨率上算深度,这是速度与精度的直接权衡。filter_mode是这里最值得说的参数:它控制深度图的后处理滤波强度,AggressiveFiltering会激进地剔除低置信度的深度估计,代价是薄结构、植被边缘可能被削掉;MildFiltering保留更多细节但噪声多。这个参数没有「正确值」,取决于场景——建筑立面适合激进,植被或雕塑适合温和。
buildDenseCloud()把多张深度图融合成统一坐标系下的密集点云。它依赖上一步的深度图,所以顺序不能反。密集云是后续网格的输入,也是很多导出需求(点云交付)的终点。
3.3 网格、UV 与纹理:surface_type和mapping_mode的取舍
chunk.buildModel( surface_type=Metashape.Arbitrary, interpolation=Metashape.EnabledInterpolation ) chunk.buildUV(mapping_mode=Metashape.GenericMapping) chunk.buildTexture(blending_mode=Metashape.MosaicBlending, texture_size=4096)buildModel的surface_type是分水岭:Arbitrary生成任意拓扑的封闭/半封闭网格,适合雕塑、复杂物体;另一类(如高度场)适合地形。interpolation=EnabledInterpolation允许在点云空洞处插值补面,关闭则保留孔洞。buildUV的GenericMapping是通用参数化,把三维表面摊平到二维纹理空间;buildTexture的MosaicBlending表示多张照片按视角拼接成一张大纹理,texture_size=4096是纹理图边长。
这条链的机制本质是:每一步都在降低数据维度、提高抽象层级——从百万级像素到稀疏点,到密集点,到三角面,到 UV 坐标,到一张贴图。理解这个降维链条,才能判断哪一步的参数值得调、哪一步调了也白调。
4. Application 与 Settings:全局状态、GPU 掩码和界面回调的底层约定
4.1gpu_mask的位运算:为什么是 5 而不是「第 5 块卡」
Application.gpu_mask是这套 API 里最容易被误读的字段。文档写得很清楚:「1 - use device, 0 - do not use(即值 5 启用 0 号和 2 号设备)」。也就是说它是一个位掩码,第 n 位对应第 n 块 GPU,不是设备编号本身。
devices = Metashape.app.enumGPUDevices() for i, d in enumerate(devices): print(i, d) # 只启用 0 号和 2 号设备:二进制 101 = 5 Metashape.app.gpu_mask = 0b101enumGPUDevices()返回设备列表,索引就是掩码里的位序。cpu_enable则控制 GPU 激活时是否仍用 CPU 协同。批处理服务器上多卡分配任务时,这个掩码是隔离资源的主要手段——但要注意它改的是全局状态,同一进程里所有文档共享,别指望按文档隔离。
4.2Settings的键值模型与addMenuItem的注册机制
Settings类是一个扁平的键值存储,value(key)/setValue(key, value)读写,load()/save()落盘。暴露出来的键包括language、log_enable、log_path、network_enable、project_compression、project_absolute_paths等。project_absolute_paths这个开关很实用:存绝对路径方便迁移,存相对路径方便整体搬目录,二选一。
addMenuItem(label, func, shortcut, icon)则是把 Python 函数挂到软件菜单上,func是回调,shortcut是快捷键字符串。这背后的机制是:应用维护一张「标签 → 回调」的注册表,菜单点击时按标签查表调用。removeMenuItem(label)按标签移除,且「若有多条同标签会全部移除」——所以自定义菜单项要用足够独特的标签,否则可能误删别人的。
4.3update()与长任务:界面为什么需要手动泵消息
Application.update()的文档只有一句「Update user interface during long operations」。这暴露了一个底层事实:Python 脚本执行时占用主线程,界面事件循环被阻塞,进度条不刷新、窗口不重绘。在长循环里周期性调用update(),相当于手动泵一次消息队列,让界面有机会响应。这不是可选项,是写带界面的批处理脚本时的必需品——否则用户会以为程序卡死。
5. 避坑与排查:脚本跑通之前最容易翻车的几件事
5.1 现象:chunk为None或直接抛异常
原因:用了Metashape.app.document但界面里没打开项目,或者项目打开了但chunks为空。解决:批处理一律用独立Metashape.Document()并显式open();取 chunk 前先判len(doc.chunks),按label定位而不是盲取[-1]。
5.2 现象:对齐后重投影误差巨大,模型飘
原因:Calibration是从外部文件load()进来的,covariance_matrix为空,平差把内参当固定值,实际镜头畸变和标定不符。解决:要么让软件在对齐时联合优化内参(不加载外部标定),要么确认外部标定的畸变模型和实际一致;用calib.error()抽查几个点看误差量级。
5.3 现象:多卡机器上只有一张卡在跑
原因:gpu_mask默认只置了某一位,或者被前一个脚本改过没恢复。解决:脚本开头显式Metashape.app.gpu_mask = (1 << n) - 1启用前 n 块,或按enumGPUDevices()的索引精确置位;注意这是全局状态,脚本结束前考虑恢复。
5.4 现象:长循环里界面假死、日志不刷新
原因:Python 占用主线程,事件循环没机会跑。解决:在循环体内周期性调用Metashape.app.update(),频率别太高(每几十次迭代一次即可),否则反而拖慢计算。
5.5 现象:buildTexture报内存不足或纹理错乱
原因:texture_size设得过大,或buildUV的映射模式与网格拓扑不匹配。解决:先降texture_size验证流程通不通,再逐步上调;GenericMapping对非流形网格可能出问题,检查buildModel的surface_type是否选错。
6. 设计取舍:为什么这套 API 选择「对象树 + 显式步骤」而不是「一键管线」
把 Metashape 的 Python API 和那些「一行代码出模型」的封装库对比,会发现它刻意不做端到端封装:matchPhotos、alignCameras、buildDepthMaps、buildDenseCloud、buildModel、buildUV、buildTexture每一步都独立暴露,中间产物(稀疏点、深度图、密集云)都可访问、可检查、可干预。这个设计取舍的代价是脚本更长、学习曲线更陡;收益是可诊断性和可复现性——当结果不对时,你能定位到是哪一步、哪个参数出的问题,而不是面对一个黑匣子。
另一个取舍是「全局单例 + 文档实例」的双层结构。app承载全局状态(GPU、设置、菜单),Document承载项目状态,两者解耦让后台批处理和前台交互能共存,但也带来了gpu_mask这类全局状态无法按文档隔离的副作用。这是典型的「简单优先于隔离」的选择。
验证这套机制是否被正确理解,我一般会这么做:写一个最小脚本,只做open → matchPhotos → alignCameras → 打印每台相机的 transform 和连接点数 → save,跑通后再逐步加步骤,每加一步就检查中间产物的规模(点数、面数、纹理尺寸)是否符合预期。这样任何一步的参数错误都会在下一步放大之前暴露出来。
从那以后我每次写这类摄影测量脚本,都强制先跑一遍「只对齐、不建模」的探针版本,确认相机位姿和连接点数量正常,再放开后续重计算步骤——这个习惯省下的返工时间,远比多写几十行检查代码多。希望帮到你。
本文还有配套的精品资源,点击获取