两百行代码实现简易点云标注工具

夏天来了非常热,LZ周末不想出去玩,于是乎继之前的图片标注工具利用两个晚上写了一个简单的点云标注工具。该工具基于Qt5.14.2-msvc2017(其实LZ的VS版本是2019,似乎兼容)平台C++语言开发,用到的第三方库为PCL1.12.1+VTK9.1.0。之前了解到VTK9之前需要编译集成Qt的QVTKWidget插件,而之后改为将QWidget提升为QVTKOpenGLNativeWidget(同样需要自己编译VTK,编译和配置方法可参考:VTK笔记-Qt5.12.11编译VTK9.0.3-QVTKOpenGLNativeWidgetQT5+VTK9.1最新配置方法)。
实现ui界面:其实就是拖拽各种控件啦~ LZ特别喜欢弄这个,感觉挺有成就感的。包括信号和槽也可以用designer可视化编辑实现,就懒得写代码了。在这里插入图片描述

受于篇幅限制,只贴出部分核心实现代码,一共也就两百多行:
mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H


#include "label_settings.h"
#include "scale_settings.h"
#include "help_settings.h"

#include <QMainWindow>
#include <QFileDialog>
#include <QMessageBox>

#include <iostream>

#include <pcl/io/pcd_io.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <pcl/features/moment_of_inertia_estimation.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkOutputWindow.h>


QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE


class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);

    ~MainWindow();

private slots:
    void on_action_open_cloud_triggered();

    void on_action_close_cloud_triggered();

    void on_action_save_label_triggered();

    void on_action_delete_label_triggered();

    void update_viewer();

    void on_action_add_box_triggered();

    void on_action_x_bigger_triggered();

    void on_action_x_smaller_triggered();

    void on_action_y_bigger_triggered();

    void on_action_y_smaller_triggered();

    void on_action_z_bigger_triggered();

    void on_action_z_smaller_triggered();

    void on_action_l_bigger_triggered();

    void on_action_l_smaller_triggered();

    void on_action_w_bigger_triggered();

    void on_action_w_smaller_triggered();

    void on_action_h_bigger_triggered();

    void on_action_h_smaller_triggered();

    void on_action_set_scale_triggered();

    void on_action_show_help_triggered();

private:
    Ui::MainWindow *ui;

    Label_Settings *label_settings;

    Scale_Settings *scale_settings;

    Help_Settings *help_settings;

    QString cloud_path, label_path;

    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud;

    pcl::visualization::PCLVisualizer::Ptr viewer;

    struct BoundingBox
    {
        Eigen::Vector3f position;
        Eigen::Quaternionf quat;
        float l;
        float w;
        float h;
        int label;
    } box;

    std::vector<BoundingBox> boxes;

    static int box_id;
};


#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "helpers.h"


int MainWindow::box_id = 0;


MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    label_settings = new Label_Settings;
    scale_settings = new Scale_Settings;
    help_settings = new Help_Settings;

    cloud.reset(new pcl::PointCloud<pcl::PointXYZ>);

    auto renderer = vtkSmartPointer<vtkRenderer>::New();
    auto renderWindow = vtkSmartPointer<vtkGenericOpenGLRenderWindow>::New();
    viewer.reset(new pcl::visualization::PCLVisualizer(renderer, renderWindow, "viewer", false));

    vtkOutputWindow::SetGlobalWarningDisplay(0);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_action_open_cloud_triggered()
{
    cloud_path = QFileDialog::getOpenFileName(this, QString("打开.pcd文件"), "", "*.pcd");
    if(cloud_path.isEmpty())
    {
        warningbox(QString("请选择有效的点云路径!"));
        return;
    }

    pcl::io::loadPCDFile(cloud_path.toStdString(), *cloud);
    boxes.clear();

    viewer->removeAllPointClouds();
    viewer->removeAllShapes();
    viewer->addPointCloud(cloud, "cloud");
    viewer->setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "cloud");
    viewer->setupInteractor(ui->qvtkWidget->interactor(), ui->qvtkWidget->renderWindow());
    ui->qvtkWidget->setRenderWindow(viewer->getRenderWindow());
    ui->qvtkWidget->renderWindow()->Render();
    ui->statusBar->showMessage(cloud_path);
}

void MainWindow::on_action_close_cloud_triggered()
{
    cloud->clear();
    boxes.clear();
    viewer->removeAllPointClouds();
    viewer->removeAllShapes();
    ui->qvtkWidget->renderWindow()->Render();
    ui->statusBar->clearMessage();
}

void MainWindow::on_action_save_label_triggered()
{
    if(cloud_path.isEmpty())  return;

    QString cloud_path_temp = cloud_path;
    label_path = cloud_path_temp.replace(".pcd", ".txt");

    std::fstream txt(label_path.toStdString(), 'w');
    for(auto box : boxes)
    {
        txt << box.label << " "
            << box.position.x() << " " << box.position.y() << " " << box.position.z() << " "
            << box.l << " " << box.w << " " << box.h << std::endl;
    }
    txt.close();
}

void MainWindow::on_action_delete_label_triggered()
{
    boxes.pop_back();
    viewer->removeShape(std::to_string(box_id));
    ui->qvtkWidget->renderWindow()->Render();
}

void MainWindow::update_viewer()
{
    viewer->removeShape(std::to_string(box_id));
    viewer->addCube(box.position, box.quat, box.l, box.w, box.h, std::to_string(box_id));
    viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR, 1, 0, 0, std::to_string(box_id));
    viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_OPACITY, 0.1, std::to_string(box_id));
    viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_LINE_WIDTH, 1, std::to_string(box_id));
    ui->qvtkWidget->renderWindow()->Render();
}

void MainWindow::on_action_add_box_triggered()
{
    if(cloud->size() == 0)  return;

    label_settings->exec();

    pcl::PointXYZ min_point_AABB, max_point_AABB;
    pcl::MomentOfInertiaEstimation<pcl::PointXYZ> feature_extractor;
    feature_extractor.setInputCloud(cloud);
    feature_extractor.compute();
    feature_extractor.getAABB(min_point_AABB, max_point_AABB);

    box.position.x() = (min_point_AABB.x + max_point_AABB.x) / 2;
    box.position.y() = (min_point_AABB.y + max_point_AABB.y) / 2;
    box.position.z() = (min_point_AABB.z + max_point_AABB.z) / 2;
    box.quat = Eigen::Quaternionf(1, 0, 0, 0);
    box.l = max_point_AABB.x - min_point_AABB.x;
    box.w = max_point_AABB.y - min_point_AABB.y;
    box.h = max_point_AABB.z - min_point_AABB.z;
    box.label = Label_Settings::label;

    boxes.push_back(box);
    box_id++;

    update_viewer();
    viewer->addCoordinateSystem(std::max(box.l, std::max(box.w, box.h)) / 10);
}

void MainWindow::on_action_x_bigger_triggered()
{
    if(cloud->size() == 0)  return;

    box.position.x() += Scale_Settings::xyz_scale * box.l;
    boxes.pop_back();
    boxes.push_back(box);
    update_viewer();
}

void MainWindow::on_action_x_smaller_triggered()
{
    if(cloud->size() == 0)  return;

    box.position.x() -= Scale_Settings::xyz_scale * box.l;
    boxes.pop_back();
    boxes.push_back(box);
    update_viewer();
}

void MainWindow::on_action_y_bigger_triggered()
{
    if(cloud->size() == 0)  return;

    box.position.y() += Scale_Settings::xyz_scale * box.w;
    boxes.pop_back();
    boxes.push_back(box);
    update_viewer();
}

void MainWindow::on_action_y_smaller_triggered()
{
    if(cloud->size() == 0)  return;

    box.position.y() -= Scale_Settings::xyz_scale * box.w;
    boxes.pop_back();
    boxes.push_back(box);
    update_viewer();
}

void MainWindow::on_action_z_bigger_triggered()
{
    if(cloud->size() == 0)  return;

    box.position.z() += Scale_Settings::xyz_scale * box.h;
    boxes.pop_back();
    boxes.push_back(box);
    update_viewer();
}

void MainWindow::on_action_z_smaller_triggered()
{
    if(cloud->size() == 0)  return;

    box.position.z() -= Scale_Settings::xyz_scale * box.h;
    boxes.pop_back();
    boxes.push_back(box);
    update_viewer();
}

void MainWindow::on_action_l_bigger_triggered()
{
    if(cloud->size() == 0)  return;

    box.l += Scale_Settings::lwh_scale * box.l;
    boxes.pop_back();
    boxes.push_back(box);
    update_viewer();
}

void MainWindow::on_action_l_smaller_triggered()
{
    if(cloud->size() == 0)  return;

    box.l -= Scale_Settings::lwh_scale * box.l;
    boxes.pop_back();
    boxes.push_back(box);
    update_viewer();
}

void MainWindow::on_action_w_bigger_triggered()
{
    if(cloud->size() == 0)  return;

    box.w += Scale_Settings::lwh_scale * box.w;
    boxes.pop_back();
    boxes.push_back(box);
    update_viewer();
}

void MainWindow::on_action_w_smaller_triggered()
{
    if(cloud->size() == 0)  return;

    box.w -= Scale_Settings::lwh_scale * box.w;
    boxes.pop_back();
    boxes.push_back(box);
    update_viewer();
}

void MainWindow::on_action_h_bigger_triggered()
{
    if(cloud->size() == 0)  return;

    box.h += Scale_Settings::lwh_scale * box.h;
    boxes.pop_back();
    boxes.push_back(box);
    update_viewer();
}

void MainWindow::on_action_h_smaller_triggered()
{
    if(cloud->size() == 0)  return;

    box.h -= Scale_Settings::lwh_scale * box.h;
    boxes.pop_back();
    boxes.push_back(box);
    update_viewer();
}

void MainWindow::on_action_set_scale_triggered()
{
    scale_settings->exec();
}

void MainWindow::on_action_show_help_triggered()
{
    help_settings->exec();
}

代码很容易看懂。需要解释的地方:boxes是用于储存所有BoundingBox的容器,box_id是用于定义每个box的唯一id。类的构造函数中的

vtkOutputWindow::SetGlobalWarningDisplay(0);

用来消除VTK的warning窗口。
另外,旧版本Qt+VTK刷新窗口方式为

ui->qvtkWidget->update();

新版本对应语句为

ui->qvtkWidget->renderWindow()->Render();

本工具实现了打开点云、关闭点云,新建点云3d boundingbox(初始化为点云的AABB包围盒)并调整包围盒的位置、大小,以及保存标注、删除标注的功能。不过代码量很小,就别指望有什么高级功能了hh~ LZ感觉用是能用,就是调整包围盒太位置和大小的时候麻烦了,可能是没有实现鼠标拖动相应的功能吧。其他功能待感兴趣的小伙伴发掘和完善~
最后贴上一张界面效果图和保存的标注(格式:每行为box的cls x y z w h l)
在这里插入图片描述
在这里插入图片描述
工程下载链接:https://download.csdn.net/download/taifyang/87998855

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

给算法爸爸上香

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值