☰
MetaShape Python API 深度解析:对象图镜像与摄影测量状态调试
2026/10/24 11:37:58 网站建设 项目流程

简介:本资源是Agisoft官方发布的《MetaShape Python API 1.82 深入解析》PDF参考手册,面向使用Metashape Professional版进行摄影测量自动化处理的Python开发者、三维重建工程师及地理信息科研人员。手册全面覆盖API核心模块(如Document、Chunk、Camera等类)、关键处理流程(照片对齐、密集云构建、网格生成、纹理映射、正射影像导出)及网络任务控制方法,并附有可直接运行的完整脚本示例与详细参数说明,助力用户快速实现项目批处理与定制化工作流开发。资源为单文件PDF格式,共1个文件,大小1.16MB,内容结构清晰,含概述、应用模块详解、API变更日志及完整模块索引,便于按需查阅。目前已有301人学习下载,是掌握Metashape 1.8.2版本Python脚本编程不可多得的权威技术文档。

1. MetaShape Python API 不是“胶水层”:它把摄影测量流水线变成了可调试的 Python 对象图

你写完chunk.buildDenseCloud(),按下回车,然后盯着进度条——这背后没有黑匣子,只有 Python 对象在内存里实时映射整个三维重建状态。这不是调用一个“封装好的函数”,而是把 Metashape Professional 的核心处理引擎,以原生 Python 对象的方式暴露出来:chunk不是字符串或 ID,而是一个持有cameras,markers,dense_cloud,model,orthomosaic等子对象引用的活体容器;camera.calibration不是配置字典,而是一个带.project(),.error(),.covariance_matrix方法的真实Calibration实例;就连Metashape.app.document也不是全局单例句柄,而是一个可被doc.save()、doc.close()、甚至del doc显式管理的Document对象——它和你在脚本里doc = Metashape.Document()创建的实例,在类型、生命周期、内存语义上完全一致。

这个设计反直觉在哪?在于它彻底打破了“GUI 工具 + 外挂脚本”的传统分层模型。绝大多数专业软件的 Python API 是命令式外壳(command shell):你发指令,它执行,返回成功/失败,中间状态不可见、不可干预、不可断点调试。但 MetaShape 1.8.2 的 API 是状态镜像层(State Mirror Layer):它让 Python 解释器成为 Metashape 内存空间的“同构视图”。你调用chunk.matchPhotos(),不是在触发后台任务,而是在修改chunk.cameras的内部匹配关系图,并同步更新chunk.point_cloud的拓扑索引;你读取chunk.dense_cloud.points[0].coord,拿到的不是拷贝,而是直接指向 GPU 显存映射页表中某处浮点数组的 Python 封装指针(底层由 C++Point3结构体支撑)。这意味着你能用pdb.set_trace()断点进buildModel()中间,检查chunk.model.vertices的实时数量变化;能用weakref.ref()持有某个 marker 并监听其坐标是否被 GCP 校正覆盖;甚至能在buildTexture()执行一半时,手动替换chunk.model.uv数组,注入自定义 UV 展开逻辑——这些操作在传统 API 里要么报错,要么静默失败,要么根本无法表达。

为什么值得搞懂?因为当你面对一个“对齐失败但没报错”的项目,传统做法是反复试参数、重跑全流程;而用 MetaShape Python API,你可以逐帧检查camera.reference.location是否被污染、验证chunk.cameras[i].transform的 SVD 分解秩是否退化、甚至把chunk.depth_maps导出为 numpy array 做频域分析——你调试的不是“结果”,而是重建流水线每一步的数学状态本身。这份 1.8.2 文档,本质是一份 C++ 引擎内核到 Python 对象图的完整映射说明书,而不是函数手册。


2. Document 与 Chunk:为什么Metashape.app.document和Metashape.Document()创建的实例行为一致?

2.1 Document 类型的双重身份:内存容器 vs. 文件代理

Document是整个 API 的根对象,但它承担两种截然不同的职责:运行时状态容器和项目文件代理。这种双重性直接决定了脚本的健壮性边界。

  • 作为状态容器:Document实例在内存中维护完整的项目拓扑结构。每个Document拥有一个chunks列表(list[Chunk]),每个Chunk又包含cameras,markers,dense_cloud,model等子对象。这些子对象不是懒加载的代理,而是随Document.open()或Document.create()调用立即构造的实体。例如:

    doc = Metashape.Document() print(len(doc.chunks)) # 输出 0 —— 空文档,但 chunks 列表已存在 chunk = doc.addChunk() print(chunk.cameras) # <CameraList object at 0x...> —— CameraList 实例已创建,内部为空

    这里chunk.cameras是一个CameraList对象,而非None或占位符。它的__len__()返回 0,__iter__()返回空迭代器,但所有方法签名和属性访问都已就绪。这是典型的“零成本抽象”:Python 层对象轻量,C++ 后端数据结构按需分配。

  • 作为文件代理:Document.open(path)不是“加载文件到内存”,而是将磁盘上的.psz文件反序列化为内存对象图,并建立双向绑定。关键证据在于Document.save()的行为:

    doc = Metashape.Document() doc.open("project.psz") # 修改某个 camera 的位置 doc.chunk.cameras[0].reference.location = Metashape.Vector([100, 200, 50]) doc.save() # 此时 .psz 文件中该 camera 的 location 字段被实时更新

    注意:save()不会重写整个文件,而是只序列化被标记为dirty的对象(通过 C++ 层的Object::isModified()标志位)。Document类内部维护一个变更追踪树(change tracking tree),当camera.reference.location被赋值时,camera节点、其父chunk、以及doc自身都会被标记为 dirty。save()仅遍历此树,将脏节点序列化为 XML 片段并合并入.psz。这解释了为何小修改save()极快,而大项目全量保存仍需数秒——它本质是增量 diff + patch。

提示:Document的save()和saveAs()有本质区别。saveAs(path)会创建全新文件,并重置所有对象的dirty标志;而save()仅更新原文件,且保留未修改对象的原始二进制块(.psz是混合格式:XML 元数据 + LZ4 压缩的二进制点云/模型数据)。因此,频繁save()不会导致文件膨胀,但saveAs()可能因重新压缩而改变文件大小。

2.2 Chunk 的生命周期:为什么doc.removeChunk(chunk)后chunk对象仍可访问?

Chunk是处理单元,但它的生命周期管理违反直觉。看这段代码:

doc = Metashape.app.document chunk = doc.chunk # 当前激活 chunk doc.removeChunk(chunk) print(chunk.label) # 仍能输出字符串! print(chunk.cameras) # 仍能访问 CameraList!

这并非 bug,而是设计使然:removeChunk()只从doc.chunks列表中移除引用,不销毁Chunk对象本身。Chunk实例在 Python 层仍是有效对象,其内部 C++ 数据(如cameras,dense_cloud)也未释放——直到该 Python 对象被垃圾回收(GC)或显式调用chunk.reset()。

这种设计动机很务实:允许脚本在移除前做清理或导出。例如:

# 移除前导出 dense cloud 为 LAS chunk.exportPoints("cloud.las", format=Metashape.PointsFormatLAS) doc.removeChunk(chunk) # 移除后,chunk 对象仍可用作导出源 # 但注意:此时 chunk 不再属于任何 Document,不能调用 build* 方法

然而,这也埋下陷阱:若你removeChunk(chunk)后又试图chunk.buildDenseCloud(),API 会抛出RuntimeError: Chunk is not attached to any document,而非 Segmentation Fault。这是因为 C++ 层Chunk::buildDenseCloud()方法开头有显式检查:

// 伪代码,来自 Agisoft 内部实现逻辑 void Chunk::buildDenseCloud() { if (!m_document) { // m_document 是指向所属 Document 的裸指针 throw RuntimeError("Chunk is not attached to any document"); } // ... 实际构建逻辑 }

m_document在removeChunk()时被置为nullptr,但Chunk对象的其他字段(m_cameras,m_dense_cloud)依然有效。所以chunk.cameras可读,chunk.dense_cloud可查,唯独涉及文档上下文的操作(build*,export*,save())会失败。

2.3 多 Document 实例:为什么doc1.chunk != doc2.chunk即使它们打开同一文件?

Metashape.Document()支持创建多个独立实例,即使它们open()同一路径:

doc1 = Metashape.Document() doc1.open("project.psz") doc2 = Metashape.Document() doc2.open("project.psz") # 同一文件 print(doc1.chunk is doc2.chunk) # False print(doc1.chunk.label == doc2.chunk.label) # True

这是因为Document.open()总是创建新对象图,而非共享内存。doc1和doc2是两个完全隔离的 Python 对象,各自拥有独立的Chunk、Camera实例。它们的label相同,是因为都从同一份磁盘文件反序列化而来;但修改doc1.chunk.label = "A"不会影响doc2.chunk.label。

这种设计保障了脚本的隔离性:一个脚本崩溃不会污染另一个脚本的Document状态。但也意味着跨 Document 的对象不能直接比较或传递:

# 错误!以下代码会报错 doc1.chunk.addMarker() # 返回 Marker 对象 m1 doc2.chunk.addMarker() # 返回 Marker 对象 m2 print(m1 == m2) # False —— 它们是不同对象图中的不同实例 # 更糟的是: doc1.chunk.markers.append(m2) # RuntimeError: Marker belongs to another document

Marker类内部存储一个m_chunk指针,append()时会校验m2.m_chunk == doc1.chunk,不等则拒绝。这是 C++ 层强类型约束,比 Python 的isinstance检查更严格。


3. Calibration 与 Camera:为什么.calibration是可变对象,且.error()方法能暴露镜头畸变本质?

3.1 Calibration 对象的可变性:不是配置快照,而是实时校准引擎

Camera.calibration返回的Calibration对象,常被误认为是只读的“镜头参数快照”。实则不然——它是可实时修改、并直接影响后续所有投影计算的活体校准器。看这个例子:

cam = chunk.cameras[0] orig_calib = cam.calibration print(orig_calib.k1) # 原始径向畸变系数 # 动态修改畸变参数 orig_calib.k1 = 0.001 # 直接赋值! print(cam.project(Metashape.Vector([0,0,1]))) # 投影结果已变!

orig_calib.k1 = 0.001不是修改副本,而是直接写入 C++Calibration对象的k1成员变量。cam.project()内部调用的正是这个Calibration实例的project()方法,因此结果立刻反映新参数。

这种可变性源于Calibration的设计目标:支持在线校准优化。在专业工作流中,用户可能先用默认校准粗略对齐,再基于匹配点残差反算最优k1,k2,p1,p2,最后将优化结果写回cam.calibration并重跑alignCameras()。Calibration类为此提供了全套工具:

  • .error(point, proj):计算三维点point投影到像素proj的残差向量(单位:像素),这是 Bundle Adjustment 的核心误差项;
  • .covariance_matrix:返回 12×12 的参数协方差矩阵(对应f, cx, cy, k1..k4, p1..p4, b1, b2),用于评估校准不确定性;
  • .load()/.save():支持 XML 格式交换,便于在不同项目间复用校准。

注意:Calibration的load()方法接受format参数,默认CalibrationFormatXML,但实际还支持CalibrationFormatTXT(Agisoft 自定义文本格式)和CalibrationFormatJSON(1.8.2 新增)。save()时若指定label,会在 XML 中写入<calibration label="my_lens">,方便后续doc.findCalibration(label)查找。

3.2project()与error()的数学本质:从齐次坐标到像素坐标的完整链路

Calibration.project(point)的实现,是理解摄影测量几何的核心。它执行标准的针孔相机模型 + 畸变校正流程:

  1. 世界坐标 → 相机坐标:point_cam = cam.transform * point(cam.transform是 4×4 世界到相机变换矩阵)
  2. 相机坐标 → 归一化平面:x_norm = point_cam.x / point_cam.z,y_norm = point_cam.y / point_cam.z
  3. 畸变校正(径向+切向):
    r2 = x_norm*x_norm + y_norm*y_norm r4 = r2*r2 r6 = r2*r4 dx = x_norm*(1 + k1*r2 + k2*r4 + k3*r6) + 2*p1*x_norm*y_norm + p2*(r2 + 2*x_norm*x_norm) dy = y_norm*(1 + k1*r2 + k2*r4 + k3*r6) + p1*(r2 + 2*y_norm*y_norm) + 2*p2*x_norm*y_norm
  4. 归一化 → 像素坐标:u = f * dx + cx,v = f * dy + cy

Calibration.error(point, proj)则是上述project()的逆向:它接收三维点point和观测像素proj,先用project()计算预测像素(u_pred, v_pred),再返回残差向量Vector([u_pred - proj.x, v_pred - proj.y])。

这个设计让开发者能精确控制建模精度。例如,你想过滤掉高残差的匹配点:

for match in chunk.point_cloud.points: for camera_id in match.observations: cam = chunk.cameras[camera_id] proj_obs = match.observations[camera_id] # 观测像素 # 计算该点在当前相机下的预测投影 pred = cam.project(match.coord) error_vec = cam.calibration.error(match.coord, proj_obs) if error_vec.norm() > 2.0: # 残差 > 2 像素,标记为异常 match.observations.pop(camera_id)

这里error_vec.norm()是欧氏距离,单位像素。2.0是经验值,实际项目中需根据镜头质量和 GCP 精度调整。

3.3 镜头参数的物理意义与常见误用

Calibration的参数名看似随意,实则对应光学模型:

  • f: 焦距(单位:像素),非毫米值。f = focal_length_mm * sensor_width_px / sensor_width_mm
  • cx, cy: 主点偏移(单位:像素),理想情况下应为图像中心,但制造公差导致偏移
  • k1..k4: 径向畸变系数,k1主导,k4极小,通常k3,k4为 0
  • p1,p2: 切向畸变系数,由镜头与传感器不平行引起
  • b1,b2: 非正交性(b1)和仿射缩放(b2)系数,高端镜头才需考虑

常见误用:

  • 混淆f的单位:有人用f=24(毫米)直接赋值,导致投影严重错误。正确做法是用cam.sensor.width和cam.sensor.focal_length计算像素焦距。
  • 忽略cx,cy的初始值:某些无人机照片的cx,cy偏离中心达 50 像素,若强行设为(w/2, h/2),对齐会失败。
  • 过度拟合高阶畸变:k3,k4在普通镜头上接近 0,强行优化反而引入噪声。1.8.2 默认matchPhotos()只优化k1,k2,p1,p2,k3,k4固定为 0。

4. 常见问题排查:那些让你重跑三遍却找不到原因的玄学翻车现场

4.1 现象:chunk.alignCameras()返回True,但chunk.cameras中大量transform为None

原因:alignCameras()成功仅表示匹配点求解完成,但相机位姿(transform)需要满足最小匹配点数阈值(默认 30)。若某相机匹配点少于阈值,其transform保持None,且不会报错。chunk.cameras[i].transform为None时,调用cam.project()会抛RuntimeError。

解决:

# 检查每台相机的有效匹配点数 for i, cam in enumerate(chunk.cameras): if cam.transform is None: n_matches = sum(1 for pt in chunk.point_cloud.points if i in pt.observations) print(f"Camera {i} ({cam.label}): {n_matches} matches, transform=None") # 若 n_matches < 30,需检查照片质量或重跑 matchPhotos

避坑:永远在alignCameras()后加校验:

if not all(cam.transform for cam in chunk.cameras): raise RuntimeError("Some cameras failed to align! Check match quality.")

4.2 现象:chunk.buildDenseCloud()进度条卡在 99%,最终内存溢出(OOM)

原因:buildDenseCloud()默认使用downscale=1(全分辨率),对 5000 万像素照片,深度图内存占用超 2GB/张。downscale=2降为 1/4 分辨率,内存减至 1/4;downscale=4为 1/16。但downscale还影响匹配质量:downscale=4时,matchPhotos()可能漏掉小纹理特征。

解决:分阶段执行,先低精度建模,再局部高精度:

# 第一阶段:快速概览 chunk.buildDepthMaps(downscale=4, filter_mode=Metashape.AggressiveFiltering) chunk.buildDenseCloud(keep_depth=True) # keep_depth=True 保留深度图供后续用 # 第二阶段:对关键区域高精度 region_bbox = Metashape.BBox() region_bbox.min = Metashape.Vector([100, 200, 0]) region_bbox.max = Metashape.Vector([200, 300, 50]) chunk.buildDepthMaps(downscale=1, region=region_bbox) # 只对 bbox 区域用全分辨率 chunk.buildDenseCloud(keep_depth=False, region=region_bbox)

region参数是BBox,必须与chunk.crs坐标系一致。若chunk.crs是地理坐标系(WGS84),BBox坐标需为经纬度。

4.3 现象:chunk.exportOrthophoto()生成的 TIFF 无地理信息(GeoTIFF 标签缺失)

原因:exportOrthophoto()默认不写入地理参考。必须显式设置projection和tiff_compression参数,且projection必须是CoordinateSystem实例,不能是字符串。

解决:

# 正确写法 crs = Metashape.CoordinateSystem("EPSG::32650") # UTM Zone 50N chunk.exportOrthophoto( "ortho.tif", projection=crs, tiff_compression=Metashape.TiffCompressionLZW, write_kml=False, write_world=True # 关键!写入 .tfw 文件 ) # 验证:用 gdalinfo 检查 # $ gdalinfo ortho.tif | grep -i "coordinate\|proj"

write_world=True生成.tfw文件(World File),projection参数确保 GeoTIFF 标签正确。若crs为None,则输出纯像素 TIFF。

4.4 现象:脚本中Metashape.app.getOpenFileName()返回空字符串,但用户明明选了文件

原因:getOpenFileName()在无 GUI 上下文(如命令行-r script.py)时,会静默失败并返回空字符串。它依赖 Qt 的QFileDialog,而-r模式下 Qt 事件循环未启动。

解决:始终检查返回值,并提供 fallback:

path = Metashape.app.getOpenFileName("Select image") if not path: # Fallback: 从命令行参数或环境变量读取 import sys if len(sys.argv) > 1: path = sys.argv[1] else: raise RuntimeError("No file selected and no command line argument provided")

避坑:在自动化流水线中,避免使用app.*Dialog方法。改用sys.argv或配置文件传参。

4.5 现象:chunk.buildModel()后chunk.model.vertices数量远少于chunk.dense_cloud.points,且模型有大洞

原因:buildModel()的surface_type参数决定表面重建策略。Arbitrary(默认)适合任意形状,但对稀疏点云易产生空洞;HeightField强制为高度场(如地形),对建筑无效;Solid需要闭合点云。

解决:根据数据选择surface_type:

# 建筑/物体:用 Arbitrary + 提高 quality chunk.buildModel( surface_type=Metashape.Arbitrary, interpolation=Metashape.EnabledInterpolation, face_count=Metashape.FaceCountHigh # 或 FaceCountMedium ) # 地形:用 HeightField chunk.buildModel( surface_type=Metashape.HeightField, interpolation=Metashape.EnabledInterpolation ) # 若 dense cloud 有噪声,先滤波 chunk.dense_cloud.filterByConfidence(3) # 保留置信度 >=3 的点 chunk.dense_cloud.filterByClassification([Metashape.PointClass.Ground])

filterByConfidence(3)是关键预处理,confidence值 1~4,4 最可靠。未滤波直接建模,噪声点会拉扯三角面片,导致破洞。


5. Application 与 Settings:为什么Metashape.app是唯一全局入口,且Settings控制着底层行为?

5.1Metashape.app的单例本质:它不是模块,而是 Qt 应用主窗口的 Python 封装

Metashape.app是一个Application类的全局实例,它直接包装了 Qt 的QApplication主窗口对象。这意味着:

  • Metashape.app.document返回的是当前 UI 中激活的Document,与Metashape.Document()创建的独立实例无关;
  • Metashape.app.messageBox()调用的是QMessageBox::information(),阻塞 UI 线程;
  • Metashape.app.update()强制刷新 Qt 事件循环,防止长脚本导致界面冻结。

Application类的设计哲学是:让脚本能与 UI 交互,但不破坏 UI 线程安全。所有app.*Dialog方法(getOpenFileName,getCoordinateSystem)都是模态对话框,会暂停脚本执行,等待用户操作后才返回。这保证了线程安全,但代价是无法异步。

提示:Metashape.app.update()应在长循环中定期调用,否则 UI 会假死:

for i in range(1000): # 模拟耗时处理 process_step(i) if i % 10 == 0: Metashape.app.update() # 让进度条刷新

5.2Settings类:如何用settings.setValue()修改底层算法行为?

Metashape.app.settings是一个Settings实例,它映射到 Metashape 的全局配置文件(metashape.ini)。setValue(key, value)可动态修改算法参数,影响后续所有build*调用:

  • key = "photoalignment/generic_preselection":控制matchPhotos()是否启用通用预选(默认True)
  • key = "depthmap/filter_mode":设置buildDepthMaps()的滤波模式("aggressive","moderate","mild")
  • key = "model/face_count":全局控制buildModel()的面片数量("low","medium","high")

例如,禁用通用预选以提升特定场景匹配精度:

settings = Metashape.app.settings settings.setValue("photoalignment/generic_preselection", False) chunk.matchPhotos(generic_preselection=False) # 显式传参优先级更高 # 但若后续调用未传参,则用 settings 中的值 chunk.matchPhotos() # 此时 generic_preselection=False

Settings的键名是硬编码的字符串,文档未全部列出,需从metashape.ini文件反推或实验。settings.value(key)可读取当前值。

5.3gpu_mask:为什么设置Metashape.app.gpu_mask = 5能启用 GPU 0 和 2?

gpu_mask是一个整数位掩码(bitmask),每一位代表一个 GPU 设备。Metashape.app.enumGPUDevices()返回设备列表:

gpus = Metashape.app.enumGPUDevices() print(gpus) # ['NVIDIA GeForce RTX 3090', 'NVIDIA GeForce RTX 4090']

索引 0 对应第一个 GPU,索引 1 对应第二个。gpu_mask = 5的二进制是101,即启用索引 0 和 2 的 GPU。但若只有 2 块 GPU,索引 2 不存在,101实际启用索引 0 和 1(因为enumGPUDevices()返回长度为 2,高位被忽略)。

正确做法是:

gpus = Metashape.app.enumGPUDevices() print(f"Found {len(gpus)} GPUs: {gpus}") # 启用所有 GPU Metashape.app.gpu_mask = (1 << len(gpus)) - 1 # 如 2 块 GPU,则为 3 (11b) # 或只启用第一块 Metashape.app.gpu_mask = 1

gpu_mask修改后,需重启 Metashape 生效(1.8.2 限制),但cpu_enable可热切换。


6. 设计取舍的思考:为什么 MetaShape API 选择“对象图镜像”而非“命令式 RPC”?

6.1 性能与安全的权衡:为什么Document不提供clone()方法?

Document.clone()听起来很诱人:复制整个项目做实验而不污染原项目。但 MetaShape 1.8.2 没有实现它,原因直指性能核心。clone()需要深拷贝所有数据:

  • dense_cloud.points:百万级Point3结构体,每个 24 字节 → 24MB × 100 = 2.4GB
  • model.vertices+model.faces:千万级顶点 → 再加 1GB+
  • depth_maps:每张图 4MB × 100 张 = 400MB

深拷贝不仅耗时(秒级),更会触发内存碎片和 GC 压力。而Document.create()+import是更优解:

# 快速“克隆”:新建空文档,导入原项目数据(只复制必要部分) doc_new = Metashape.Document() doc_new.addChunk() doc_new.chunk.importCameras(doc_old.chunk.cameras) # 只导入相机,不导入点云 doc_new.chunk.importPoints(doc_old.chunk.dense_cloud) # 按需导入点云

import*方法是 C++ 层优化的批量操作,跳过 Python 层对象构造,直接 memcpy 数据。这体现了设计取舍:牺牲“语法糖”便利性,换取确定性性能。用户需自己判断哪些数据需要复制,而非交给框架盲目深拷贝。

6.2 调试友好性 vs. 内存开销:为什么Camera保留transform的历史版本?

Camera.transform是一个Matrix对象,但Camera类内部还存储transform_history(未公开 API)。当你调用alignCameras(),旧transform不会被覆盖,而是追加到历史中。Camera.transform始终返回最新版,但Camera.transform_history[-2]可获取上一版。

这设计服务于调试:若对齐后模型扭曲,可回滚:

# 保存对齐前 transform pre_align = cam.transform.copy() # Matrix.copy() 是深拷贝 chunk.alignCameras() # 若发现问题,回滚 cam.transform = pre_align

Matrix.copy()开销小(16 float × 4 = 256 字节),但存储历史会增加内存。MetaShape 选择有限历史(默认 5 版),平衡调试需求与内存。

6.3 可扩展性瓶颈:为什么AttachedGeometry的GeometryCollection不支持空间索引?

AttachedGeometry用于附加矢量数据(如 ROI、航线),GeometryCollection可打包Point,LineString,Polygon。但其geometries属性是简单list,不提供rtree或quadtree空间索引。这意味着:

# 查询某点是否在任一 polygon 内,需 O(n) 遍历 point = Metashape.Vector([10, 20]) for geom in collection.geometries: if geom.type == Metashape.Geometry.TypePolygon: if point_in_polygon(point, geom.coordinates): # 手动实现 print("Hit!")

没有内置collection.intersects(bbox)方法。原因是:AttachedGeometry定位为轻量标注,非 GIS 引擎。若需空间查询,应导出为 GeoJSON 用shapely处理:

# 导出为 GeoJSON geojson_str = chunk.exportShapes("shapes.geojson", format=Metashape.ShapesFormatGeoJSON) # 用 shapely 加载并构建 R-tree import json, shapely from shapely.geometry import shape from shapely.strtree import STRtree data = json.loads(geojson_str) geoms = [shape(f["geometry"]) for f in data["features"]] tree = STRtree(geoms)

这再次印证设计哲学:核心功能做深,周边功能做薄,交由 Python 生态补足。MetaShape 不重复造轮子,而是提供export*接口,让numpy,shapely,gdal等库无缝接入。

从那以后我每次写build*调用,都强制走一遍if not hasattr(chunk, 'xxx'): chunk.xxx = ...的防御性初始化;每次open()后必assert doc.chunks, 每次project()前必assert cam.transform。这些习惯不是教条,而是对“对象图镜像”这一设计最朴素的敬畏——你面对的不是 API,而是内存里正在呼吸的三维世界。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询