CloudCompare插件编写三(算法实现)

唠叨

本文分三篇来介绍一个完整的CloudComapre插件的编写教程,分别是插件框架篇数据结构篇算法实现篇

这是第三篇,算法实现篇,你可以根据本文改成自己的插件,待卿临幸。

qSAF源码:Github . qSAF

前文概要

在上回中,我们知道了点云中扫描角度的存储结构,下面我们来讲qSAF的具体实现。

UI界面

新建QT设计器界面类,命名为ccSAFDlg,在ccSAFDlg.ui文件设计简单的界面。

因为我们只需要一个范围,一个确认取消键,所以我把它弄成这样子:

doubleSpinBox要设置范围:0.0090.00,默认值分别设为20.0070.00

ccSAFDlg.h

#ifndef CCSAFDLG_H
#define CCSAFDLG_H

#include "ui_SAFDlg.h"
#include <QDialog>

namespace Ui {
class ccSAFDlg;
}

class ccSAFDlg : public QDialog, public Ui::ccSAFDlg
{
    Q_OBJECT

public:
    explicit ccSAFDlg(QWidget *parent = 0);

protected slots:

    //! Saves (temporarily) the dialog paramters on acceptation
    void saveSettings();
};

#endif // CCSAFDLG_H

ccSAFDlg.cpp

#include "ccSAFDlg.h"

//定义两个静态阈值,并初始化
static double threshold_1 = 20;
static double threshold_2 = 70;

ccSAFDlg::ccSAFDlg(QWidget *parent) : QDialog(parent), Ui::ccSAFDlg()
{
    setupUi(this);

    //关联信号槽
    connect(buttonBox, SIGNAL(accepted()), this, SLOT(saveSettings()));

    //初始化设置阈值
    doubleSpinBox_1->setValue(threshold_1);
    doubleSpinBox_2->setValue(threshold_2);
}

void ccSAFDlg::saveSettings()
{
    //OK后重新赋值
    threshold_1 = doubleSpinBox_1->value();
    threshold_2 = doubleSpinBox_2->value();
}

现在界面就做好了。

插件doAction实现

至于doAction的实现,点云其中的数据结构,可以参考第二篇,数据结构篇

简单地说,我们需要:

  1. Scan Angle Rank,通过getScalarFieldIndexByName()获得扫描角度在标量域中的索引
  2. 用索引,通过getScalarField()获得扫描角度标量域指针
  3. 用指针,通过getValue()获得每个点的值
  4. 比较扫描角度值与用户输入区间的大小,把合适的值存储起来
  5. 把合适值封装成点云实体
  6. 显示在界面上

大体的算法思路上是没有问题的,但是有个纠结的地方,就是是否使用进度条。

实测SAF处理一个雷达文件,

  • 使用进度条耗时:129.1s
  • 不用进度条耗时:3.5s

这种压倒性的差距让我果断砍掉真·进度条,没错!我使用假·进度条,就是不会动的进度条。

这样短时间的处理使用假·进度条,既不会降低处理速度,也不会降低用户体验~

下面就是完整代码,注释中有真·进度条的实现([进度条]),但不推荐使用

void qSAF::doAction()
{
    //当插件加载时,m_app应该已经被CC初始化了
    assert(m_app);
    if (!m_app)
        return;

    //获取选择的实体
    const ccHObject::Container& selectedEntities = m_app->getSelectedEntities();
    //获取选择的实体数量
    size_t selNum = selectedEntities.size();
    //确保只选择一个实体
    if (selNum != 1)
    {
        m_app->dispToConsole("[SAF] Select only one cloud!", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
        return;
    }

    ccHObject* ent = selectedEntities[0];
    assert(ent);
    //确保选择的实体是POINT_CLOUD类型
    if (!ent || !ent->isA(CC_TYPES::POINT_CLOUD))
    {
        m_app->dispToConsole("[SAF] Select a real point cloud!", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
        return;
    }

    //从选择的实体中转换成ccPointCloud*类型
    ccPointCloud* pc = static_cast<ccPointCloud*>(ent);

    //获取点云的数量m_count
    unsigned count = pc->size();

    //初始化阈值变量
    static double threshold_1 = 20;
    static double threshold_2 = 70;
    double threshold_temp = 0;

    //显示插件ui窗体
    {
        ccSAFDlg safDlg(m_app->getMainWindow());
        safDlg.doubleSpinBox_1->setValue(threshold_1);
        safDlg.doubleSpinBox_2->setValue(threshold_2);

        if(!safDlg.exec())
        {
            return;
        }

        //存储阈值
        threshold_1 = safDlg.doubleSpinBox_1->value();
        threshold_2 = safDlg.doubleSpinBox_2->value();
    }

    //显示进度条窗体
    QProgressDialog pDlg;
    pDlg.setWindowTitle("SAF");
    pDlg.setLabelText(QString("Scan Angle Filter\nfrom %1 to %2").arg(threshold_1).arg(threshold_2));
    //[进度条]设置进度条总范围
    //pDlg.setRange(0, count);
    pDlg.setCancelButton(0);
    pDlg.show();
    QApplication::processEvents();
    
    QElapsedTimer timer;
    //计时开始
    timer.start();

    ScalarType scanAngle;

    CCLib::ReferenceCloud rangeAnglerc(pc);

    //确保 threshold_1 小于 threshold_2
    if(threshold_1 > threshold_2)
    {
        threshold_temp = threshold_1;
        threshold_1 = threshold_2;
        threshold_2 = threshold_temp;
    }

    //[进度条]进度条的取消SAF按钮
    //bool wasCancelled = false;

    //获取 Scan Angle Rank 的索引
    int scanAngleSFIndex = pc->getScalarFieldIndexByName("Scan Angle Rank");

    //[重点]遍历每个点的操作
    for(unsigned i = 0; i < count; ++i)
    {
        //获取每个点的扫描角度
        scanAngle = pc->getScalarField(scanAngleSFIndex)->getValue(i);

        //取扫描角度的绝对值
        if(scanAngle < 0)
        {
            scanAngle = -scanAngle;
        }

        //如果扫描角度在给定的阈值范围,则添加它的索引到参考云
        if(threshold_1 <= scanAngle && scanAngle <= threshold_2)
        {
            rangeAnglerc.addPointIndex(i);
        }

//        //[进度条]重置进度条
//        pDlg.setValue(i);
//        QCoreApplication::processEvents();

//        //[进度条]取消SAF处理
//        if (pDlg.wasCanceled())
//        {
//            wasCancelled = true;
//            break;
//        }
    }

    //把 ReferenceCloud 类型克隆成 ccPointCloud 类型
    ccPointCloud* rangeAnglepc = pc->partialClone(&rangeAnglerc);

    //判断rangeAnglepc是否为空,即所选范围内是否有点
    if(!rangeAnglepc)
    {
        m_app->dispToConsole("[SAF] Failed to extract the range angle subset.", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
        return;
    }
    //计算SAF后点数所占的百分比和SAF过程所花的时间
    m_app->dispToConsole(QString("[SAF] %1% of scan angle points are filtered").arg((rangeAnglerc.size() * 100.0) / count, 0, 'f', 2), ccMainAppInterface::STD_CONSOLE_MESSAGE);
    m_app->dispToConsole(QString("[SAF] Timing: %1 s.").arg(timer.elapsed() / 1000.0, 0, 'f', 1), ccMainAppInterface::STD_CONSOLE_MESSAGE);

    //关闭进度条
    pDlg.close();
    QApplication::processEvents();

//      //[进度条]取消SAF    
//    if (wasCancelled)
//    {
//        m_app->dispToConsole("[SAF] SAF was cancelled", ccMainAppInterface::STD_CONSOLE_MESSAGE);
//        return;
//    }

    //隐藏原始点云
    pc->setEnabled(false);

    //添加新的一组DB实体
    ccHObject* cloudContainer = new ccHObject(pc->getName() + QString("_saf"));

    //设置新点云并添加到实体
    rangeAnglepc->setVisible(true);
    rangeAnglepc->setName("SAF Point Cloud");
    cloudContainer->addChild(rangeAnglepc);

    //添加实体到DB树
    m_app->addToDB(cloudContainer);

    //刷新
    m_app->refreshAll();
}

效果

结语

经过了三篇的学习,终于实现了个完整的插件。

回顾我们学习的路线:插件框架 -> 数据结构 -> 算法实现

我们不仅从中学会了CC插件的编写,也学到了QT的pro文件编写、QT界面设计、CC运作流程、点云数据结构等。

而我在学习这个插件编写的过程收获更多,因为我是看代码两个月,写代码两小时,Debug两天(差不多啦不要纠结为什么222

看代码的过程是非常痛苦的,CC里面大量的模板编程思想,接口设计思想,还有去他继承谁爸爸的爸爸……

但是期间确实学到很多,以此作为分享,望共勉!


我的博客:https://blog.huihut.com/
转载请注明出处:http://blog.huihut.com/2017/04/27/CloudCompareSAFPlugin_3_Algorithm/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容