Bootstrap

QT 使用OpenGL显示并查看点云图

效果图

在这里插入图片描述

概述

  • OpenglWidget继承自QOpenGLWidgetQOpenGLFunctions,它具有OpenGL的功能,并且可以绘制OpenGL图形
  • MinimumBoundBox类用于计算点云的最小包围盒(轴对齐包围盒,AABB),可以帮助确定视图的缩放级别,或者在用户进行平移和旋转操作时保持点云在视图内
  • 这两个类结合起来,前者提供了渲染和交互的框架,后者用于理解和限定点云的空间范围

功能点

  1. 不依赖其他库和插件
  2. 计算点云的最小包围盒
  3. 可旋转(左键),平移(右键),复位(空格)查看点云
  4. 根据Z轴深度进行伪颜色处理

代码分析

读取点云文件
  • 从文件如pcd文件中获取点云数据,一般来说点云文件是如下格式,但是若是文件有rgb颜色信息,那么后续也不需要使用z轴深度去进行伪颜色处理,这里就使用QVector4D保存好颜色信息,传递给后续使用。
    在这里插入图片描述
void PointCloudPage::onStart()
{
    std::vector<QVector3D> cloud;
    QString qfile = "文件路径";
    cloud = ReadVec3PointCloudASC(qfile);
    m_openglWidget->showPointCloud(cloud);
}

std::vector<QVector3D> PointCloudPage::ReadVec3PointCloudASC(QString path)
{
    std::vector<QVector3D> cloud;
    QFile file(path);
    if (!file.open(QFile::ReadOnly | QIODevice::Text))
    {
        qDebug() << "There is no asc file";
        return cloud;
    }
    QTextStream in(&file);
    QString ramData = in.readAll();
    QStringList list = ramData.split("\n");
    QStringList listline;
    cloud.resize(list.count() - 1);
    for (int i = 0; i < list.count() - 1; i++)
    {
        listline = list.at(i).split(" ");
        if (listline.size() >= 3)
        {
            cloud[i].setX((listline.at(0).toFloat()));
            cloud[i].setY((listline.at(1).toFloat()));
            cloud[i].setZ((listline.at(2).toFloat()));
        }
    }
    return cloud;
}

着色器
  • 着色器是现代图形编程的基础,它们提供了对渲染过程的精细控制,使得开发者能够创建更加丰富和高效的视觉效果
/// @brief 顶点着色器
static const char *vertexShaderSource =
    "attribute highp vec3 posAttr;\n"
    "attribute lowp vec4 colAttr;\n"
    "varying lowp vec4 col;\n"
    "uniform highp mat4 matrix;\n"
    "void main() {\n"
    "  col=colAttr;\n"
    "  gl_Position=matrix * vec4(posAttr,1.0f);\n"
    "}\n";

/// @brief 片段着色器
static const char *fragmentShaderSource =
    "varying lowp vec4 col;\n"
    "void main() {\n"
    "   gl_FragColor = col;\n"
    "}\n";
 bool OpenglWidget::InitShader()
{
    bool success = true;
    success &= m_Program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource);
    success &= m_Program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource);
    success &= m_Program->link();
    GetShaderUniformPara();
    return success;
}
图形绘制
  • makeCurrent进行任何OpenGL操作之前必须调用的,glDrawElements绘制坐标轴,在计算正交投影矩阵时考虑当前窗口的宽高比,防止点云图跟随窗口大小而产生形变。
void OpenglWidget::paintGL()
{
    makeCurrent();
    m_Program->bind();
    glClearColor(m_backgroundColor.x(), m_backgroundColor.y(), m_backgroundColor.z(), m_backgroundColor.w());
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BITS);

    m_VAO->bind();
    setMatrixUniform();
    glDrawArrays(GL_POINTS, 6, (GLsizei)m_PointsVertex.size() - 6);
    if (m_bShowAxis)
    {
        glDrawElements(GL_LINES, 6, GL_UNSIGNED_INT, 0);
    }
    m_VAO->release();
    m_Program->release();
}

void OpenglWidget::setMatrixUniform()
{
    QMatrix4x4 matrix = QMatrix4x4();
    QMatrix4x4 matrixPerspect = QMatrix4x4();
    QMatrix4x4 matrixView = QMatrix4x4();
    QMatrix4x4 matrixModel = QMatrix4x4();

    QVector3D minPos = (m_box.getMinPoint() - m_box.getCenterPoint());
    QVector3D maxPos = (m_box.getMaxPoint() - m_box.getCenterPoint());
    float maxAxis;
    maxAxis = qAbs(qMax(qMax(m_box.depth(), m_box.width()), m_box.height()));

    float aspectRatio = width() * 1.0f / height();
    // TODO 计算正交投影矩阵时考虑当前窗口的宽高比
    matrixPerspect.ortho(-aspectRatio * maxAxis, aspectRatio * maxAxis, -maxAxis, maxAxis, -2 * maxAxis, 2 * maxAxis);

    matrixView.lookAt(QVector3D(0, 0, maxAxis), QVector3D(0.0, 0.0, -1), QVector3D(0.0, 1.0, 0.0));
    matrixView.translate(m_lineMove.x(), m_lineMove.y(), m_lineMove.z());

    matrixModel.rotate(m_rotate);
    matrixModel.scale(m_scale);

    matrix = matrixPerspect * matrixView * matrixModel;
    m_Program->setUniformValue(m_matrixUniform, matrix);
}
图形变换
  • 通过各种事件控制,旋转(左键),平移(右键),复位(空格)
void OpenglWidget::mousePressEvent(QMouseEvent *e)
{
    if (e->buttons() & Qt::LeftButton || e->buttons() & Qt::MidButton)
    {
        setMouseTracking(true);
        m_lastPoint = QVector2D(e->localPos());
        // 开始拖拽时显示坐标系
        m_bShowAxis = true;
        repaint();
    }
    if (e->button() == Qt::RightButton)
    {
        // 记录右键按下的初始位置
        m_rightButtonInitialPos = QVector2D(e->localPos());
    }
}

void OpenglWidget::mouseMoveEvent(QMouseEvent *e)
{
    if (e->buttons() & Qt::LeftButton)
    {
        Rotate(QVector2D(m_lastPoint), QVector2D(e->localPos()));
    }
    if (e->buttons() & Qt::RightButton)
    {
        // 使用右键按下的初始位置和当前位置来计算移动
        LineMove(m_rightButtonInitialPos, QVector2D(e->localPos()));
        // 更新初始位置为当前位置,以便下一次移动计算
        m_rightButtonInitialPos = QVector2D(e->localPos());
    }
    m_lastPoint = QVector2D(e->localPos());
    repaint();
}

void OpenglWidget::mouseReleaseEvent(QMouseEvent *e)
{
    setMouseTracking(false);
    // 松开鼠标时隐藏坐标系
    m_bShowAxis = false;
    repaint();
}

void OpenglWidget::wheelEvent(QWheelEvent *e)
{
    if (e->delta() > 0)
    {
        modelZoomInOrOut(true);
    }
    else
    {
        modelZoomInOrOut(false);
    }
}

void OpenglWidget::keyPressEvent(QKeyEvent *e)
{
    if (e->key() == Qt::Key_Space)
    {
        ResetView();
    }
    QWidget::keyPressEvent(e);
}
最小包围盒
  • 这里使用的AABB轴对齐包围盒,这是最简单的一种包围盒,,计算速度快,但可能不是最紧凑的。初始化时calculateMinBoundingBox函数接收点云数据,并计算出这些点构成的最小包围盒。
bool MinimumBoundBox::calculateMinBoundingBox(const std::vector<QVector3D> &cloud)
{
	zerolize();
	int size = (int)cloud.size();
	if (size == 0)
	{
		return false;
	}
	else if (size == 1)
	{
		firstPoint(cloud[0]);
		return false;
	}
	else
	{
		bool bfirst = false;
		for (int i = 0; i < size; i++)
		{
			if (!bfirst)
			{
				if (isValid(cloud[i]))
				{
					firstPoint(cloud[i]);
					bfirst = true;
				}
			}
			else
			{
				nextPoint(cloud[i]);
			}
		}
		m_center = QVector3D(midX(), midY(), midZ());
	}
	return true;
}
伪颜色
  • 一般来说点云是无色彩的,为了更加直观的展示,我们可以使用根据z轴深度进行颜色处理。
void OpenglWidget::gray2Pseudocolor(const QVector3D pos, float color[4])
{
    float fmin = m_box.getMinPoint().z();
    float fmax = m_box.getMaxPoint().z();
    int colortemp = (int)(((fmax - pos.z()) / (fmax - fmin)) * 255);
    int r, g, b;
    if (colortemp >= 0 && colortemp < 64)
    {
        r = 0;
        g = 254 - 4 * colortemp;
        b = 255;
    }
    else if (colortemp >= 64 && colortemp < 128)
    {
        r = 0;
        g = 4 * colortemp - 254;
        b = 510 - 4 * colortemp;
    }
    else if (colortemp >= 128 && colortemp < 192)
    {
        r = 4 * colortemp - 510;
        g = 255;
        b = 0;
    }
    else if (colortemp >= 192 && colortemp <= 255)
    {
        r = 255;
        g = 1022 - 4 * colortemp;
        b = 0;
    }
    else
    {
        r = 255;
        g = 255;
        b = 255;
    }
    color[0] = r * 1.0f / 255;
    color[1] = g * 1.0f / 255;
    color[2] = b * 1.0f / 255;
    color[3] = 1.0f;
}
;