# 3Dmodelling **Repository Path**: li-zhenyi/3-dmodelling ## Basic Information - **Project Name**: 3Dmodelling - **Description**: Modeler Master Center建模软件SDK二次开发 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-11-20 - **Last Updated**: 2026-01-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # iTwin Capture Modeler 自动化重建系统 **版本**: 1.0.0 **最后更新**: 2025-11-20 **开发周期**: 2025-11-14 ~ 2025-11-20 --- ## 📋 项目简介 这是一个基于 **iTwin Capture Modeler SDK** 开发的自动化3D重建系统,通过 MQTT 协议接收任务请求,自动完成从图片下载到3D模型生成的完整流程,并支持多种输出格式(OSGB、Cesium 3D Tiles、FBX 等)。 ### 核心特性 ✅ **全流程自动化**: 图片下载 → 空中三角测量 → 3D重建 → 结果上传 ✅ **多格式支持**: OSGB、Cesium 3D Tiles、OBJ、FBX、3SM 等 10+ 种格式 ✅ **任务恢复**: 程序崩溃后自动恢复未完成任务,支持断点续传 ✅ **精细化进度**: 每种格式独立追踪进度和状态 ✅ **并发处理**: 最多同时处理 3 个任务 ✅ **容错设计**: 下载中断重试、Job 失败检测、状态同步修复 --- ## 🏗️ 系统架构 ``` ┌─────────────────────────────────────────────────────────────┐ │ MQTT Broker │ │ (jiaoyujidi.work:1883) │ └────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ ReconstructionHandler │ │ • 接收 MQTT 任务请求 │ │ • 管理工作线程池 (3 workers) │ │ • 任务恢复管理 │ │ • 进度上报 │ └────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ ReconstructionService │ │ • 下载图片 (MinIO) │ │ • 调用 iTwin SDK 执行重建 │ │ • 上传结果 (MinIO) │ └────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ iTwin Capture Modeler SDK │ │ • 空中三角测量 (AT) │ │ • Production 生成 (多格式) │ │ • JobQueue 管理 │ └─────────────────────────────────────────────────────────────┘ ``` ### 数据流 ``` MQTT 请求 ↓ 创建 ReconstructionTask ↓ 加入任务队列 (queue.Queue) ↓ Worker 线程获取任务 ↓ ReconstructionService 执行 ├─ 下载图片 (0-10%) ├─ 空中三角测量 (10-30%) ├─ 生成 Production (30-80%, 每种格式独立进度) └─ 上传结果 (80-100%) ↓ 上报 MQTT 结果 ↓ 任务完成 ``` --- ## 📁 项目结构 ``` Reconstruction/ ├── 核心代码 (8 个文件) │ ├── ReconstructionHandler.py # MQTT 处理器和任务调度 │ ├── ReconstructionService.py # 重建业务逻辑 │ ├── ReconstructionTask.py # 任务模型和状态管理 │ ├── TaskPersistence.py # 数据库持久化 │ ├── TaskRecoveryManager.py # 任务恢复管理 │ ├── StorageManager.py # MinIO 存储管理 │ ├── config.py # 配置文件 │ └── utils.py # 工具函数 │ ├── 文档 (docs/) │ ├── 开发指南/ # 开发过程文档 │ ├── 八股知识/ # 技术原理和面试题 │ ├── 问题修复/ # Bug 修复记录 │ └── API文档/ # SDK 使用说明 │ ├── 工具脚本 (scripts/) │ ├── 诊断工具/ # 问题诊断脚本 │ ├── 测试脚本/ # 单元测试 │ ├── 部署脚本/ # 自动部署 │ └── 数据库迁移/ # Schema 升级 │ └── 日志 (logs/) └── *.log # 运行日志 ``` --- ## 🚀 快速开始 ### 1. 环境要求 - **Python**: 3.9+ - **iTwin Capture Modeler**: 24.1.7.4760+ - **依赖包**: paho-mqtt, minio, itwincapturemodeler ### 2. 安装 ```powershell # 1. 激活 Conda 环境 conda activate lzy-cmSDK # 2. 安装依赖 pip install paho-mqtt minio # 3. 配置文件 (config.py) # 编辑 MQTT_CONFIG、MINIO_CONFIG、RECONSTRUCTION_CONFIG ``` ### 3. 启动服务 ```powershell cd "C:\Program Files\Bentley\iTwin Capture Modeler Center\3D_modeling\Reconstruction" python ReconstructionHandler.py ``` ### 4. 发送测试任务 (MQTT) ```json { "task_id": "1001_test", "url": "http://192.168.1.100:9000/bucket/path/to/images", "model_type": "OSGB,Cesium 3D Tiles", "polygon_points": [ {"latitude": 35.771865, "longitude": 120.025036}, {"latitude": 35.772865, "longitude": 120.026036}, {"latitude": 35.773865, "longitude": 120.025036} ], "method": "start_oblique_photography_modeling" } ``` --- ## 🐛 从零开始:我们遇到的所有问题 ### 阶段 1: 项目初始化 (2025-11-14) #### 问题 1.1: SDK 路径配置错误 **现象**: ``` ImportError: No module named 'itwincapturemodeler' ``` **原因**: iTwin SDK 的 Python 绑定路径未添加到 `sys.path` **解决**: ```python import sys sys.path.append(r"C:\Program Files\Bentley\iTwin Capture Modeler Center\python") ``` **教训**: SDK 文档中未明确说明 Python 绑定的安装路径 --- #### 问题 1.2: 项目文件路径必须使用正斜杠 **现象**: ``` Failed to create project: Invalid path format ``` **原因**: iTwin SDK 内部使用 Unix 风格路径 (`/`) **解决**: ```python project_path = r"C:\output\task.ccm".replace('\\', '/') ``` **八股知识点**: 【跨平台路径处理】Windows `\` vs Unix `/` --- ### 阶段 2: MQTT 集成 (2025-11-15) #### 问题 2.1: MQTT 消息格式不符合接口文档 **现象**: 任务接收失败,缺少必需字段 **原因**: 实际 MQTT 消息格式与接口文档不一致 **解决**: - 添加字段验证和默认值 - 支持 `url` 为字符串或数组 ```python url = payload.get('url') if isinstance(url, str): task.image_urls = [url] elif isinstance(url, list): task.image_urls = url ``` **教训**: 永远不要假设输入格式,做好容错处理 --- #### 问题 2.2: MQTT QoS 级别选择 **现象**: 部分消息丢失 **解决**: 使用 QoS 1(至少一次),而非 QoS 0(最多一次) **八股知识点**: 【MQTT QoS】 - QoS 0: 最多一次(可能丢失) - QoS 1: 至少一次(推荐,可能重复) - QoS 2: 恰好一次(慢,少用) --- ### 阶段 3: 多边形 ROI 集成 (2025-11-16) #### 问题 3.1: `polygon_points` 传递丢失 **现象**: ```python polygon_points = task.metadata.get('polygon_points') # polygon_points 为 None! ``` **根本原因**: **对象引用陷阱** ```python # ❌ 错误代码 metadata = payload.get('metadata', {}) metadata['polygon_points'] = polygon_points task = ReconstructionTask(..., metadata=metadata) # 问题: metadata 是局部变量,task 构造函数中又创建了新的 {} ``` **解决**: ```python # ✅ 正确代码 metadata = payload.get('metadata', {}) if polygon_points: metadata['polygon_points'] = polygon_points task = ReconstructionTask(..., metadata=metadata) # 在构造函数中使用传入的 metadata,而非创建新的 ``` **八股知识点**: 【Python 引用 vs 拷贝】 - 字典是可变对象,传递的是引用 - 但如果在函数内重新赋值 `metadata = {}`,会创建新对象 - 解决: 使用 `metadata.update()` 或确保传入的对象被使用 --- #### 问题 3.2: Boost.Python 容器陷阱 **现象**: ```python external_ring.points.append(point) # ❌ 不生效! ``` **根本原因**: Boost.Python 的 `std::vector` 绑定返回的是**临时对象** **解决**: ```python # ❌ 错误: 修改临时对象 external_ring.points.append(point) # ✅ 正确: 先获取引用,再修改 points = external_ring.points points.append(point) external_ring.points = points ``` **八股知识点**: 【C++ 绑定陷阱】 - Boost.Python 的 getter 可能返回拷贝而非引用 - 修改拷贝不会影响原对象 - 解决: 获取 → 修改 → 设置回去 --- ### 阶段 4: 数据库持久化 (2025-11-17) #### 问题 4.1: SQLite 并发写入冲突 **现象**: ``` sqlite3.OperationalError: database is locked ``` **原因**: 多线程同时写入 SQLite **解决**: ```python # 使用上下文管理器和锁 with self._get_cursor() as cursor: cursor.execute("INSERT INTO ...") ``` **八股知识点**: 【数据库并发】 - SQLite 默认不支持并发写入 - WAL 模式可以改善但仍有限制 - 生产环境应该使用 PostgreSQL/MySQL --- #### 问题 4.2: `task_id` 唯一性冲突 **现象**: ``` UNIQUE constraint failed: tasks.task_id ``` **原因**: MQTT 重复发送相同 `task_id` **解决**: ```python # 使用 INSERT OR REPLACE(幂等性) cursor.execute(""" INSERT OR REPLACE INTO tasks (task_id, ...) VALUES (?, ...) """, ...) ``` **八股知识点**: 【幂等性设计】 - 同一个请求多次执行,结果一致 - 使用 `REPLACE` 或 `ON CONFLICT UPDATE` - 避免重复数据 --- ### 阶段 5: 任务恢复系统 (2025-11-18) #### 问题 5.1: Job 和 Task 映射失败 **现象**: 程序重启后,无法找到对应的 Job **根本原因**: - Job 的 `project_path` 是绝对路径 - Task 数据库中存储的可能是相对路径 - 路径大小写不一致 (Windows 不敏感,但字符串比较敏感) **解决**: ```python # 路径规范化 project_path_norm = os.path.normpath(project_path).lower() ``` **八股知识点**: 【跨平台路径处理】 - Windows 路径不区分大小写,但 Python 字符串比较区分 - 使用 `os.path.normpath()` 统一路径分隔符 - 使用 `.lower()` 统一大小写 --- #### 问题 5.2: AT 完成但 Production 未提交 **现象**: ``` 恢复任务: 1234_xxx AT 已完成 Production 未提交 → 任务卡住,永远无法完成 ``` **原因**: 程序在 AT 和 Production 之间崩溃 **解决**: ```python # 添加阶段完成标志 task.at_completed = True task.production_completed = False # 恢复时检查标志 if task.at_completed and not task.production_completed: # 提交 Production self._submit_production_after_at(task) ``` **八股知识点**: 【状态机设计】 - 明确的阶段标志 (downloaded, at_completed, production_completed, uploaded) - 每个阶段完成后立即持久化 - 恢复时根据标志决定从哪里继续 --- ### 阶段 6: 多格式支持 (2025-11-18) #### 问题 6.1: 多格式任务只生成部分格式 **现象**: ``` MQTT 请求: OSGB + Cesium 3D Tiles 实际输出: 只有 OSGB ``` **根本原因**: - 程序在 OSGB Production 运行时崩溃 - 重启后恢复任务,检测到 "Production Job 在运行" - 直接监控 OSGB Job,**没有检查 Cesium 是否提交** - Cesium 从未提交 **解决**: ```python # 检查所有请求的格式是否都有对应的 Job requested_formats = set(format_statuses.keys()) # {'OSGB', 'Cesium 3D Tiles'} submitted_formats = set() # 从 Job 名称推断 for job in production_jobs: if 'OSGB' in job['name']: submitted_formats.add('OSGB') if 'Cesium' in job['name']: submitted_formats.add('Cesium 3D Tiles') missing_formats = requested_formats - submitted_formats if missing_formats: # 提交缺失的格式 self._submit_production_after_at(task, only_formats=missing_formats) ``` **八股知识点**: 【集合运算 - 差集】 - `requested - submitted` = 缺失的格式 - 精细化检查,避免遗漏 - 增量恢复,只处理缺失部分 --- #### 问题 6.2: 格式状态 JSON 序列化 **现象**: ``` TypeError: Object of type datetime is not JSON serializable ``` **解决**: ```python format_statuses = { "OSGB": { "status": "running", "progress": 0.5, "updated_at": datetime.now().isoformat() # ✅ 转换为字符串 } } ``` **八股知识点**: 【JSON 序列化】 - `datetime` 对象不能直接序列化 - 使用 `.isoformat()` 转换为 ISO 8601 字符串 - 反序列化: `datetime.fromisoformat(string)` --- ### 阶段 7: 下载中断恢复 (2025-11-19) #### 问题 7.1: 下载中断后任务丢失 **现象**: ``` 恢复任务: 1255_xxx Project 文件不存在: None 任务可能太早期或已被删除,无法恢复 ❌ ``` **根本原因**: - 程序在**下载阶段**崩溃 - `project_path` 还未创建(下载完成后才创建项目) - 恢复时检测到 `project_path = None`,直接放弃 **解决**: ```python if not project_path or not os.path.exists(project_path): # ❌ 旧代码: return None # 放弃任务 # ✅ 新代码: 标记需要重新下载 task = self._rebuild_task_object(task_data) task._needs_redownload = True task._is_recovered = True return task ``` **八股知识点**: 【断点续传 - 多阶段策略】 - 下载阶段:重新下载(成本低) - AT 阶段:监控 Job(成本高) - Production 阶段:监控 Job 或增量提交 --- #### 问题 7.2: 'Project files' 目录已存在 **现象**: ``` Failed to create project: 'Project files' directory already exists ``` **根本原因**: - 重新下载时,output 目录中有旧的项目文件 - 只删除 `.ccm` 文件不够 - SDK 在调用 `setProjectFilePath()` 时会创建 `Project files` 目录 - 如果目录已存在,`writeToFile()` 会报错 **解决**: ```python # 在设置路径之前,清理整个项目目录 output_dir = os.path.dirname(project_path_abs) if os.path.exists(output_dir): logger.warning(f"⚠️ 项目目录已存在,清理: {output_dir}") shutil.rmtree(output_dir) logger.info(f" ✓ 已清理旧项目目录") # 重新创建 os.makedirs(output_dir, exist_ok=True) ``` **八股知识点**: 【幂等性 - 覆盖写入】 - 重复执行任务,结果一致 - 清理旧文件,重新创建 - 记录警告日志,便于追踪 --- ### 阶段 8: 进度和状态优化 (2025-11-19) #### 问题 8.1: 进度补偿不准确 **现象**: ``` 程序崩溃前: AT 完成 (30%) 重启后: 进度仍显示 0% 用户体验: 进度"倒退" ``` **解决**: ```python # 恢复时补发丢失的进度通知 if task.at_completed and task.progress < 0.3: self._report_progress(task.task_id, 0.30, "空中三角测量已完成") if task.production_completed and task.progress < 0.8: self._report_progress(task.task_id, 0.80, "3D 重建已完成") ``` **八股知识点**: 【状态补偿机制】 - 检查当前进度,避免重复上报 - 补发丢失的中间状态 - 保证用户体验连贯 --- #### 问题 8.2: RECONSTRUCTION_CONFIG 作用域错误 **现象**: ``` UnboundLocalError: local variable 'RECONSTRUCTION_CONFIG' referenced before assignment ``` **根本原因**: ```python # 文件顶部全局导入 from config import RECONSTRUCTION_CONFIG def func(): # Line 100: 使用全局变量 value = RECONSTRUCTION_CONFIG.get('key') # ← 报错! # Line 200: 局部导入 from config import RECONSTRUCTION_CONFIG # ← Python 认为这是局部变量 ``` **解决**: 删除局部导入,只使用全局导入 **八股知识点**: 【Python 作用域 - LEGB 规则】 - Local: 函数内部 - Enclosing: 外层函数 - Global: 模块全局 - Builtin: 内置 Python 在**编译时**确定变量作用域,如果函数内有赋值语句,就认为是局部变量。 --- ## 💡 核心技术原理 ### 1. 生产者-消费者模式 **问题**: 如何高效处理大量异步任务? **解决**: ```python # 生产者: MQTT 消息处理 task_queue = queue.Queue() task_queue.put(task) # 消费者: Worker 线程 while True: task = task_queue.get(timeout=1) self._execute_task(task) task_queue.task_done() ``` **八股知识点**: - `queue.Queue` 是线程安全的 - 自动阻塞和唤醒,避免忙等待 - FIFO 保证任务顺序 --- ### 2. 依赖注入模式 **问题**: 每个任务有不同的输出格式配置,如何避免全局状态污染? **❌ 错误做法**: ```python RECONSTRUCTION_CONFIG['output_formats'] = [...] # 全局修改 service = ReconstructionService(task) ``` **问题**: - 并发任务会互相覆盖配置 - 测试时需要恢复全局状态 **✅ 正确做法**: ```python # 配置存储在任务元数据中 task.metadata['output_formats'] = [...] # 通过参数传递 service = ReconstructionService( task, output_formats=task.metadata['output_formats'] ) ``` **八股知识点**: - 依赖注入: 外部传入依赖,而非内部创建 - 优点: 解耦、可测试、无副作用 - 类似: Spring 的 @Autowired --- ### 3. 状态机设计 **任务生命周期**: ``` PENDING → DOWNLOADING → PROCESSING → UPLOADING → COMPLETED ↓ FAILED ``` **阶段标志**: ```python task.downloaded_completed = False task.at_completed = False task.production_completed = False task.uploaded_completed = False ``` **恢复逻辑**: ```python if not downloaded_completed: 重新下载 elif at_completed and not production_completed: 提交 Production elif production_completed and not uploaded_completed: 执行上传 ``` **八股知识点**: - 明确的状态转换规则 - 幂等性: 重复进入同一状态,结果一致 - 可恢复性: 根据状态决定从哪里继续 --- ### 4. 精确进度计算 **传统方法**: ```python progress = job.percent / 100.0 # SDK 返回的百分比 ``` **问题**: - SDK 的百分比可能不准确 - 无法反映实际工作量 **优化方法**: ```python # 从 JobQueue 数据库查询 Task 完成情况 completed_tasks = SELECT COUNT(*) WHERE job_name = ? AND status = 'Completed' total_tasks = SELECT COUNT(*) WHERE job_name = ? job_progress = completed_tasks / total_tasks ``` **优点**: - 精确到每个瓦片的完成情况 - 更准确反映实际进度 --- ### 5. 幂等性设计 **核心原则**: 多次执行相同操作,结果一致 **应用场景**: 1. **数据库插入**: ```python INSERT OR REPLACE INTO tasks (task_id, ...) VALUES (?, ...) ``` 2. **文件下载**: ```python # 覆盖写入,支持重复下载 self.minio_client.fget_object(bucket, object_name, file_path) ``` 3. **项目创建**: ```python # 清理旧目录,重新创建 if os.path.exists(output_dir): shutil.rmtree(output_dir) os.makedirs(output_dir) ``` **八股知识点**: - 幂等性是分布式系统的核心要求 - HTTP 方法: GET、PUT、DELETE 是幂等的,POST 不是 - 实现: 唯一 ID + 状态检查 + 覆盖写入 --- ## 📊 性能优化 ### 1. 并发下载 **问题**: 顺序下载 171 张图片太慢 **优化**: ```python from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=10) as executor: futures = [ executor.submit(download_one_file, obj) for obj in objects ] for future in as_completed(futures): result = future.result() ``` **效果**: - 顺序: ~60 秒 - 并发: ~15 秒 - 提升: **4倍** --- ### 2. 数据库连接池 **问题**: 每次查询都创建新连接 **优化**: ```python class TaskPersistence: def __init__(self, db_path): self.db_path = db_path self._local = threading.local() # 线程局部存储 @contextmanager def _get_cursor(self): if not hasattr(self._local, 'conn'): self._local.conn = sqlite3.connect(self.db_path) yield self._local.conn.cursor() self._local.conn.commit() ``` **效果**: - 减少连接创建开销 - 避免 "database is locked" --- ### 3. 进度上报防抖 **问题**: 每秒上报几十次进度,MQTT 拥塞 **优化**: ```python prev_percent = -1 if percent != prev_percent: self._report_progress(...) prev_percent = percent ``` **效果**: - 只在进度变化时上报 - 减少 90% 的 MQTT 消息 --- ## 🧪 测试验证 ### 单元测试 ```powershell # 测试 URL 格式解析 python test_url_format.py # 测试多边形 ROI python test_send_polygon_mqtt.py # 测试任务恢复 python test_task_recovery.py # 测试格式转换 python test_format_conversion.py ``` ### 集成测试 ```powershell # 完整流程测试 python test_service.py # Production 测试 python test_production_minimal.py # 只测试重建部分(跳过下载) python test_reconstruction_only.py ``` --- ## 📚 八股知识点汇总 ### Python 核心 1. **GIL (Global Interpreter Lock)**: - 同一时刻只有一个线程执行 Python 字节码 - I/O 密集型任务可以用多线程(下载、数据库) - CPU 密集型任务用多进程 2. **装饰器**: ```python @contextmanager def _get_cursor(self): # 资源管理和异常处理 ``` 3. **生成器**: ```python for obj in objects: # MinIO 返回生成器,节省内存 yield obj ``` ### 面向对象 1. **SOLID 原则**: - Single Responsibility: 每个类只有一个职责 - ReconstructionHandler: 任务调度 - ReconstructionService: 业务逻辑 - TaskPersistence: 数据持久化 2. **设计模式**: - 生产者-消费者: 任务队列 - 观察者: 进度回调 - 策略: 恢复策略选择 - 单例: StorageManager ### 并发编程 1. **线程安全**: - `queue.Queue`: 内置锁 - `threading.Lock`: 保护共享资源 - `threading.local()`: 线程局部存储 2. **死锁**: - 避免嵌套锁 - 统一加锁顺序 ### 网络编程 1. **MQTT**: - 发布-订阅模式 - QoS 级别选择 - 保持连接(keepalive) 2. **TCP/IP**: - MinIO 使用 HTTP 协议 - 长连接 vs 短连接 - 连接池管理 ### 系统设计 1. **容错设计**: - 任务恢复 - 状态同步 - 幂等性 2. **可扩展性**: - 水平扩展(多个 Handler 实例) - 负载均衡(MQTT 订阅组) - 无状态设计(状态存储在数据库) --- ## 🔗 相关资源 - **SDK 文档**: `doc/` - **八股知识**: `docs/八股知识/` - **问题修复**: `docs/问题修复/` - **API 文档**: `docs/API文档/接口文档.md` --- ## 🙏 致谢 感谢 iTwin Capture Modeler SDK 团队提供强大的 3D 重建能力。 感谢在开发过程中提供帮助和测试的团队成员。 --- ## 📝 更新日志 ### v1.0.0 (2025-11-20) - ✅ 完整的任务恢复系统 - ✅ 多格式支持和精细化进度 - ✅ 下载中断重试 - ✅ 幂等性设计 - ✅ 完整的文档和测试 ### v0.9.0 (2025-11-18) - ✅ 格式状态追踪系统 - ✅ 任务持久化 - ✅ 基本恢复功能 ### v0.8.0 (2025-11-16) - ✅ 多边形 ROI 支持 - ✅ MQTT 集成 - ✅ 基本重建流程 ### v0.1.0 (2025-11-14) - 🎉 项目启动 --- **License**: Proprietary **Contact**: [Your Team] **Repository**: [Git URL if applicable]