免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

CloudCompare二次开发实战:从源码编译到插件与命令行自动化

CloudCompare二次开发实战:从源码编译到插件与命令行自动化 研究点云的人大概都听过一句话CloudCompare功能不够用不如自己动手改。其实CloudCompare的二次开发并没有想象中那么高不可攀难的是很多人一开始就走了弯路——要么在源码编译这一关被各种依赖问题劝退要么面对上万行的开源代码仓库不知道从哪里下手。我最早做点云配准和重建算法验证时也在这个开源软件上踩了不少坑后来才慢慢摸清它的插件架构和命令行调用方式现在基本能做到一两天内把一个新算法封装成带GUI按钮的插件也能让CloudCompare在无人值守的批处理流水线里稳定跑完一批数据。这篇就把从编译环境搭建到第一个插件落地、再到命令行脚本自动化这件事讲透给每一个准备开始CloudCompare二次开发的人一条能直接照做的路线图。这篇文章适合两类人一类是有C/Qt基础、想把自定义算法集成进CloudCompare的开发者另一类是暂时不打算写C只希望把CloudCompare变成自动化批处理工具的工程师。基本概念我会解释到位但重点始终放在代码和实操上。1. 为什么非要对CloudCompare动手以及它能长出哪些“新器官”先给不熟的朋友交代一下背景。CloudCompare是一款开源免费的点云和三维网格处理软件平时大家用得最多的是它的可视化、点云配准、距离计算、分割、网格化这些功能。但工程场景里总有软件给不了的东西比如某个行业专用格式的导入导出、某种特定噪声的滤波算法、或者把点云批量按项目编号重新组织的内部工具。这时候就需要二次开发。CloudCompare之所以被很多开发者选为二次开发基座主要是三个原因第一它完全开源用的是C和Qt代码结构清晰没有黑盒第二它的核心库CCCoreLib和界面层分离算法逻辑可以相对独立地复用第三它天生就预留了插件系统官方自己也把大量功能写成插件这意味着我们可以照着官方插件的写法扩展新功能难度比直接改主程序低得多。按我的经验CloudCompare的二次开发可以拆成三条路线对应软件的不同扩展点扩展方式适用场景需要掌握的技术栈标准插件Standard Plugin给工具栏和菜单增加新功能比如自定义滤波、特征提取、批量处理C、Qt、CMakeGL滤镜GL Filter自定义可视化效果比如渲染点云时加特殊着色、显示自定义图标C、OpenGL、Qt门槛相对高IO插件IO Plugin支持新格式的点云/模型读写比如接自己公司的数据格式C、文件格式解析除了这三种图形界面上的扩展CloudCompare还提供了一条不需要碰GUI的路线——命令行模式。命令行模式不仅能调用主程序内置的处理命令还可以让标准插件主动注册自己的命令同一个算法既能出现在菜单里也能在Shell脚本里被调用。这一点相当实用后面我会专门开一章讲。所以在动手之前先想清楚你的二次开发到底要解决什么问题。如果只是给团队做一个内部批处理工具那命令行加脚本可能就够了如果是要交互式地检查点云、手动圈选区域再执行算法那就得走标准插件路线。不要一上来就想着大改界面和OpenGL渲染把需求拆成上面三类会轻松很多。2. 编译源码这场硬仗环境搭配与最小构建方案无论你走哪条开发路线第一步都是把CloudCompare源码编译成功。这个环节卡住了很多人但真说到底就是几个版本的匹配问题。先说我的推荐组合Windows上用Visual Studio 2019或2022配合CMakeQt选择5.15.2及以上Linux上用apt安装系统Qt库编译器用GCC 9以上。这套组合拿出来是比较稳的。2.1 获取源码时的注意事项源码直接去GitHub拉官方仓库就行。需要留意的是CloudCompare源码里包含了一些子模块依赖尤其像CCCoreLib这种核心库官方已经拆成独立仓库了。所以克隆的时候最好加上--recursive参数把子模块一起拉下来git clone --recursive https://github.com/CloudCompare/CloudCompare.git如果忘记加--recursive后面CMake配置时会发现找不到CCCoreLib的源码那时候再补还来得及在项目根目录执行git submodule update --init --recursive这一步很容易被忽略我在一台新机器上重新拉代码时就吃过亏白白排查了半天。2.2 Windows下的CMake配置要点源码拉下来之后我习惯在源码根目录建一个build文件夹用CMake图形界面指到源码路径然后做基础配置。这里有几个关键点需要逐一确认编译器架构必须和Qt库一致。如果Qt安装的是MSVC 2019 64位版本那Visual Studio也必须选用x64 Win64生成器不能选Win32。32位Qt和64位程序链接时会出现一大堆看不懂的链接错误。Qt版本尽量选官方预编译包。我在Windows上用的是Qt 5.15.2安装时勾选MSVC 2019 64-bit组件。CloudCompare支持Qt 6但我个人建议新入手先用Qt 5.15原因是社区资料和已有插件大多是Qt 5时代的写法遇到问题时能搜到的解决方案更多。不要全部插件一股脑开。CMake里有几十个PLUGIN_*开关对应官方自带的各种插件。第一次编译时把它们全部关掉只保留核心程序和几个必要插件能大幅缩短编译时间。等到主程序能跑起来了再逐个打开你需要的插件。我常用的CMake配置相当于这样cmake .. -G Visual Studio 17 2022 -A x64 \ -DCMAKE_PREFIX_PATHC:/Qt/5.15.2/msvc2019_64 \ -DPLUGIN_IO_QE57OFF \ -DPLUGIN_IO_QFBXOFF \ -DPLUGIN_STANDARD_QANIMATIONOFF实际的插件开关列表可以用cmake-gui搜索PLUGIN关键字看一下界面里勾选更直观。核心程序只有几百个源文件但加上所有插件后编译量会翻倍所以最小化构建的目的就是先把核心跑通。2.3 Linux环境下更快更省心Linux的编译其实比Windows简单很多因为依赖可以用系统的包管理器装。Ubuntu 20.04/22.04下你需要先安装sudo apt-get update sudo apt-get install build-essential cmake qtbase5-dev \ libqt5svg5-dev libqt5opengl5-dev libqt5xmlpatterns5-dev装完之后直接mkdir build cd build cmake .. cmake --build . -j8这里推荐用Ninja而非默认的Unix Makefiles会快不少cmake .. -G Ninja ninja编译完成后可执行文件会在build/qCC/CloudCompare插件会输出到build/plugins下对应的目录。首次运行可以在命令行里敲./CloudCompare控制台会打印插件加载信息如果看到类似Loaded 20 plugins这样的日志就说明编译和插件系统都正常了。2.4 拿到编译产物的验收标准成功编译后我习惯做三件事来确认环境没有隐患打开软件确认左上角标题栏没出现Debug字样之外的特殊标记看菜单栏是否出现插件下拉菜单里面能看到已启用的插件列表直接在命令行带--log参数启动一次数据加载确认日志能正常输出。只有这三件都顺畅才说明你的开发环境是可靠的。否则后面写插件时遇到报错你分不清是代码问题还是环境问题。3. 插件的骨架和血脉搞懂ccPluginInterface全家桶当你把环境搞定下一步就是理解CloudCompare插件系统是怎么运转的。CloudCompare的插件机制建立在Qt的插件框架之上本质上是一个带Q_PLUGIN_METADATA标识的动态库运行主程序通过QPluginLoader扫描插件目录把符合接口约定的库加载进来。理解这一点后面很多诡异问题就能迎刃而解。3.1 三种插件接口的分工上一章表格里提过三种插件类型对应的接口分别是ccStdPluginInterface、ccGLPluginInterface和ccIOPluginInterface。其中ccStdPluginInterface是最常用也是最适合入门的一种。它做的事情很直观告诉界面我在哪显示、我叫什么名字、我对应的操作按钮是什么。所有标准插件都必须继承QObject和ccStdPluginInterface这两个类。让我摘一段骨架代码#include ccStdPluginInterface.h class MyFirstPlugin : public QObject, public ccStdPluginInterface { Q_OBJECT Q_PLUGIN_METADATA(IID mycompany.cloudcompare.plugin.MyFirstPlugin FILE myfirst.json) Q_INTERFACES(ccStdPluginInterface) public: MyFirstPlugin() : m_action(nullptr) {} QString getName() const override { return MyFirstPlugin; } QString getDescription() const override { return A simple test plugin; } QString getVersion() const override { return 1.0; } QString getReference() const override { return ; } QString getIcon() const override { return ; } void doAction() override; FileArray getFileList() const override { return FileArray(); } };这里有个非常容易忽略的细节Q_PLUGIN_METADATA里的IID和FILE。IID是这个插件的唯一标识FILE指向一个JSON描述文件。CloudCompare加载插件时会核对这个标识如果IID拼写错误或者JSON文件缺失主程序会直接跳过这个插件而且界面上不一定有明显提示。我见过不少人在网上求助插件编译成功了但菜单里没有八成都是这个问题。3.2 界面动作和选择态的管理标准插件的核心方法里和用户体验最相关的是这几个getActions()返回QAction列表告诉界面这个插件能在菜单和工具栏上生成哪些按钮onNewSelection()选中对象变化时回调用来决定当前动作是否可用比如没选点云时置灰按钮doAction()用户点了你的按钮后真正执行任务的地方。开发插件时最常犯的错误是把onNewSelection当成事件循环来用在里面做耗时操作。这个函数的调用频率比你想象的高选中集合一变化就会触发你要是跑到一半去计算直方图界面会卡住。正确的做法是只更新动作的setEnabled状态真正的数据处理放到doAction里做。主程序还提供了一个指针m_app类型是ccMainAppInterface通过它你就能拿到当前打开的文档、选中实体列表、以及主窗口引用。m_app在插件初始化时由CloudCompare注入不需要自己创建。3.3 数据模型是理解一切的钥匙对点云做二次开发绕不开CloudCompare的数据抽象。它的所有数据对象都挂在ccHObject这棵树上ccPointCloud点云、ccMesh三角网格、ccOctree八叉树都是ccHObject的子类。插件拿到用户选中项时通常是一个ccHObject指针的容器你需要判断它实际是什么类型才能做后续处理ccHObject::Container selectedEntities getSelectedEntities(); for (ccHObject* obj : selectedEntities) { if (!obj-isA(CC_TYPES::POINT_CLOUD)) continue; // 跳过非点云对象 ccPointCloud* cloud static_castccPointCloud*(obj); // 这里才开始安全地处理点云 }我的经验是所有类型判断都用isA(CC_TYPES::XXX)不要直接用qobject_cast或裸的static_cast。因为CloudCompare的类型系统是在运行时判定的连ccHObject的继承关系有时候都比你想的绕比如ccMesh内部关联了多个子对象用isA最不会出错。4. 手写第一个插件从菜单项到数据处理全流程理论说再多不如亲手写一个能跑起来的插件。这一章我带你实现一个最简单的功能选中点云后点击插件按钮在控制台输出所有点的平均Z值。别小看这个例子它包含了插件开发的全部流程理解了它你就可以往里面填任意业务逻辑。4.1 创建一个标准插件的目录结构最简单的方式是从官方示例插件开始复制。CloudCompare源码的plugins/example目录下有一个ExamplePlugin结构非常清晰一个src文件夹、一个CMakeLists.txt、一个描述插件信息的JSON文件。把它整个复制一份重命名为MyAverageZPlugin然后修改三个内容CMakeLists里的项目名、头文件里类的名字、JSON描述文件。目录结构大概是这样MyAverageZPlugin/ ├── CMakeLists.txt └── src/ ├── MyAverageZPlugin.h ├── MyAverageZPlugin.cpp └── MyAverageZPlugin.jsonCMakeLists.txt里最关键的是最终生成的动态库名称和输出路径一般来说只要项目名对得上CMake会自动把生成的.dll或.so放到CloudCompare主程序的插件目录下。如果你的工程是手动从零建的记得在CMakeLists里添加qt5_add_resources(...) # 如果有资源文件 add_library(${PROJECT_NAME} SHARED ${SOURCES} ${HEADERS}) target_link_libraries(${PROJECT_NAME} PRIVATE CCAppInterface)4.2 核心接口实现头文件和前面骨架类似我直接把它补完整// MyAverageZPlugin.h #pragma once #include ccStdPluginInterface.h class MyAverageZPlugin : public QObject, public ccStdPluginInterface { Q_OBJECT Q_PLUGIN_METADATA(IID experiment.cc.plugin.MyAverageZ FILE myaveragez.json) Q_INTERFACES(ccStdPluginInterface) public: MyAverageZPlugin(); ~MyAverageZPlugin() override default; QString getName() const override { return AverageZ; } QString getDescription() const override { return Calculate the average Z of selected clouds; } QString getVersion() const override { return 1.0; } void doAction() override; };在doAction里第一步永远是检查选中项void MyAverageZPlugin::doAction() { if (!m_app) return; ccHObject::Container selectedEntities m_app-getSelectedEntities(); if (selectedEntities.empty()) { m_app-dispToConsole(No entity selected!, ccMainAppInterface::ERR_CONSOLE_MESSAGE); return; } double totalZ 0.0; unsigned pointCount 0; for (ccHObject* entity : selectedEntities) { if (!entity-isA(CC_TYPES::POINT_CLOUD)) continue; ccPointCloud* cloud static_castccPointCloud*(entity); for (unsigned i 0; i cloud-size(); i) { const CCVector3* P cloud-getPoint(i); totalZ P-z; pointCount; } } if (pointCount 0) { m_app-dispToConsole(No valid point cloud selected., ccMainAppInterface::ERR_CONSOLE_MESSAGE); return; } m_app-dispToConsole(QString(Average Z %1 (from %2 points)).arg(totalZ / pointCount).arg(pointCount), ccMainAppInterface::STD_CONSOLE_MESSAGE); }这段代码看起来简单但它体现了二次开发里最重要的几个规矩先判空、再判类型、最后用size()和getPoint()遍历点云。遇到超大点云时不要用std::vector自造拷贝getPoint(i)拿到的指针是只读的整个过程不会复制海量数据。4.3 JSON描述文件和菜单注册JSON文件myaveragez.json包含插件的元信息CloudCompare通过它在插件加载阶段确定显示的菜单名称。一个最小可用的JSON长这样{ name: AverageZ, type: Standard, icon: }其实即使没有这个文件插件也能被加载但很多信息会显示不完整所以最好还是写上。你还需要在主窗口菜单中注册动作这通常在插件构造函数里完成MyAverageZPlugin::MyAverageZPlugin() { m_action new QAction(getName(), this); connect(m_action, QAction::triggered, this, MyAverageZPlugin::doAction); }4.4 编译、部署和调试写完代码后重新走一遍CMake配置把插件开关打开。如果你是在官方源码树里加的目录CMakeLists里至少要有一行add_subdirectory(MyAverageZPlugin)。我这里更推荐的做法是单独建一个plugins/private目录管理自己的插件工程主程序通过CMake的add_subdirectory把它们带进来。编译成功后在构建目录里找到生成的动态库把它复制到CloudCompare可执行文件旁边的plugins目录Linux上通常是/usr/local/lib/cloudcompare/plugins。然后启动CloudCompare随便加载一个点云文件选中它在“插件”菜单里如果看到了AverageZ点击后控制台窗口就会打印平均Z值。调试的时候Windows用户可以直接用Visual Studio的“附加到进程”功能附加到正在运行的CloudCompare然后在doAction里下断点。Linux上用gdb或vscode远程调试都行。我个人的习惯是在关键路径上加ccLog::Print日志因为CloudCompare的日志系统会把输出同时发送到控制台和文件比单步调试在数据量大的时候更快定位问题。5. 不写C也能二次开发命令行批量改造与脚本自动化会写插件当然好但并不是所有二次开发需求都要上C。CloudCompare自带一套相当完整的命令行接口很多批量处理任务在命令行里组合一下就能完成。我后来做了两个比较大的自动化项目都是直接在服务器上跑命令行的省掉了编译打包的麻烦也方便做回归测试。5.1 命令行模式的基本套路命令行模式调用的基本格式是CloudCompare -SILENT -O input.las -SS SPATIAL 0.05 -SAVE_CLOUDS output.bin这里每个参数都有讲究-SILENT不启动图形界面在无头服务器上也能跑-O input.las加载输入文件-SS SPATIAL 0.05调用“空间采样”命令把点云按0.05米的间距下采样-SAVE_CLOUDS output.bin把处理后的点云保存为CloudCompare专用格式bin。关键点在于命令行参数是严格按顺序执行的。CloudCompare会从左到右读取参数遇到一个处理命令就执行一次所以-SS一定要放在-O后面否则它不知道要对谁采样。同样地-SAVE_CLOUDS如果想保存多个结果文件可以用文件列表方式但顺序还是遵守先处理再保存的原则。5.2 常用命令组合配准、裁剪、网格化一条龙如果你要做点云配准命令行也能胜任CloudCompare -SILENT -O reference.las -O source.las \ -ICP -GLOBAL_SHIFT -AUTO_SAVE AUTO \ -SAVE_CLOUDS source_aligned.las-ICP会计算两组点云之间的刚体变换并把source.las对齐到第一组。注意-GLOBAL_SHIFT这个参数处理大地坐标点云时经常用到它会在加载阶段自动平移坐标避免浮点精度问题。保存时建议用-AUTO_SAVE加-SAVE_CLOUDS组合让CloudCompare自动推导输出文件名方便批量脚本处理。裁剪分割类似比如你想按Z值范围切出一层地面点CloudCompare -SILENT -O terrain.las -CROP 2D \ -X_MIN 100 -X_MAX 200 -Y_MIN 50 -Y_MAX 80 \ -SAVE_CLOUDS cropped.bin实际命令语法要以你用的版本为准因为不同版本之间-CROP的参数格式有过调整建议在本地先跑一次CloudCompare -help查看关键命令支持的参数列表。5.3 用Python把CloudCompare包进你的服务命令行模式最大的好处就是能被任何语言调用。我通常用Python写一个批处理脚本遍历整个文件夹把CloudCompare当作一个子进程来跑import subprocess import glob import os CC_EXE CloudCompare for las_file in glob.glob(scans/*.las): out_name os.path.splitext(os.path.basename(las_file))[0] _sampled.bin cmd [ CC_EXE, -SILENT, -O, las_file, -SS, SPATIAL, 0.02, -SAVE_CLOUDS, out_name, ] result subprocess.run(cmd, capture_outputTrue, textTrue) if result.returncode ! 0: print(fFailed: {las_file}) print(result.stderr)这里有两个细节容易踩坑第一CloudCompare的日志输出非常丰富哪怕处理成功也会打印大段日志所以不要用result.stdout是否为空来判断成功要用returncode第二如果处理的是大文件建议给每个文件单独调用一次CloudCompare进程避免长时间占用内存导致系统崩溃反正命令行启动速度很快几百个文件也就几分钟的事。5.4 什么时候选CLI什么时候选插件以我的经验做数据预处理、批量格式转换、自动化质检这类任务优先用CLI加脚本做需要人工交互、视觉检查、区域手选的工作就用标准插件。不要为了单纯追求“高大上”去写GUI插件很多时候一个Python脚本加命令行半天就搞定了比编译一个插件快得多。6. 开发过程中的常见坑和我的解决习惯最后聊一聊这一年多折腾CloudCompare二次开发下来我觉得特别值得写下来的几个坑。这些内容在线文档里基本不会写但遇到一次就能让人难受好几天。6.1 插件加载失败菜单里怎么都看不到这个现象很典型代码编译通过、dll也放对了位置但CloudCompare里就是找不到你的插件。我的排查顺序是这样打开命令行直接输CloudCompare -d这个参数会让主程序打印非常详细的诊断信息包括每个插件的加载结果。如果看到Cannot load library ... is not a valid Qt plugin基本可以断定是Qt版本不匹配。确认你插件的编译Qt版本和主程序运行Qt版本完全一致包括MSVC编译器版本和Debug/Release模式。CloudCompare自己是用Release模式发布的你用Debug模式编译插件拿到Release主程序里大概率无法加载。检查Q_PLUGIN_METADATA里的IID和JSON文件是否都在正确位置。JSON文件最好放在和插件动态库同一个目录下。6.2 选中点云却不触发doAction很多新手写插件doAction里的逻辑明明没问题但就是按钮点了没反应。这时候先确认onNewSelection有没有被正确实现。如果插件没有在选中状态下把动作按钮的setEnabled(true)调用到那按钮永远是灰色的你点击自然无效。我习惯在onNewSelection里写void MyPlugin::onNewSelection(const ccHObject::Container selectedEntities) { if (m_action) { bool hasCloud false; for (ccHObject* obj : selectedEntities) { if (obj-isA(CC_TYPES::POINT_CLOUD)) { hasCloud true; break; } } m_action-setEnabled(hasCloud); } }只更新状态不干任何重活这是最稳的写法。6.3 内存管理和对象生命周期别踩雷CloudCompare的ccHObject有一套引用计数机制但也保留了不少裸露指针。我在开发时给自己定的规矩是对于通过接口拿到的对象只读不释放如果不小心把一个ccPointCloud从对象树里析构了后面必然崩溃。要给实体添加点云时用entity-addChild(cloud, ccHObject::DP_NONE)并明确指定是否希望由父对象管理内存比手动delete安全得多。6.4 大点云遍历性能的几个土办法点云上亿点的时候for (unsigned i 0; i cloud-size(); i)这种遍历可能慢到让人怀疑人生。写算法时优先利用CCCoreLib提供的并行API比如ccPointCloud::forEach配合OpenMP。其次处理之前先用cloud-computeOctree()给点云建八叉树很多空间查询算法会快几个数量级。最后就是能采样就先采样很多算法验证阶段根本不需要完整点云采样到几百万点足够跑通逻辑验证准确后再换回全量数据。CloudCompare二次开发这条路说难是开头那几步环境配置说容易是从插件架构理解清楚之后后面就是不断地复制官方插件改业务逻辑。我自己走了不少弯路最值钱的经验就是在动手以前先把命令行的能力摸一遍很多看似需要开发的功能其实已经被官方命令组合覆盖了。真正需要写插件的时候再从plugins/example这个起点出发几乎是一路坦途。希望这篇能帮你把第一步踩实。
返回列表