Search code examples
pythonopenglpyqt5pyopengl

Point Cloud Distortion when Drawing with modern OpenGL


I found this nice tutorial of drawing and rotating a cube with PyQt and modern OpenGL. My objective was to adapt the script for point clouds, by doing the following (see also code below):

  1. Load point cloud using Open3D and extract coordinates & colors as numpy arrays
  2. Create Vertex Buffer Objects (VBOs) from the arrays
  3. Change the drawing function to gl.glDrawElements(gl.GL_POINTS, ...)

Unfortunately then the point cloud is very distorted and thin (see screenshot). It should actually be a room with chairs and walls.

Do you see if I made a mistake with the VBOs or drawing? Or is there a better way of loading a point cloud?

Example of distorted point cloud with modern approach

I tested the example with the old fixed pipeline (glBegin(GL_POINTS) ... glEnd()) and there the point cloud is correctly drawn (but also the performance really bad!).

from PyQt5 import QtCore      # core Qt functionality
from PyQt5 import QtGui       # extends QtCore with GUI functionality
from PyQt5 import QtOpenGL    # provides QGLWidget, a special OpenGL QWidget
from PyQt5 import QtWidgets

import OpenGL.GL as gl        # python wrapping of OpenGL
from OpenGL import GLU        # OpenGL Utility Library, extends OpenGL functionality
from OpenGL.arrays import vbo
import numpy as np
import open3d as o3d
import sys

# Loading the point cloud from file
def load_pointcloud():
    pcd = o3d.io.read_point_cloud("../pointclouds/0004.ply")
    print(pcd)
    print("Pointcloud Center: " + str(pcd.get_center()))
    points = np.asarray(pcd.points)
    colors = np.asarray(pcd.colors)

    return points, colors

#### here was only the GUI code (slider, ...) , which works fine! ####
        
class GLWidget(QtOpenGL.QGLWidget):
    def __init__(self, parent=None):
        self.parent = parent
        QtOpenGL.QGLWidget.__init__(self, parent)

    def initializeGL(self):
        self.qglClearColor(QtGui.QColor(250, 250, 250))     # initialize the screen to blue
        gl.glEnable(gl.GL_DEPTH_TEST)                   # enable depth testing

        self.initGeometryPC()

        self.rotX = 0.0
        self.rotY = 0.0
        self.rotZ = 0.0

    def setRotX(self, val):
        self.rotX = val

    def setRotY(self, val):
        self.rotY = val

    def setRotZ(self, val):
        self.rotZ = val
        
    def resizeGL(self, width, height):
        gl.glViewport(0, 0, width, height)
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glLoadIdentity()
        aspect = width / float(height)

        #GLU.gluPerspective(45.0, aspect, 1.0, 100.0)   #GLU.gluPerspective(45.0, aspect, 1.0, 100.0)
        gl.glOrtho(-2.0, 2.0, -2.0, 2.0, 1.0, 100.0)
 
        gl.glMatrixMode(gl.GL_MODELVIEW)
        
    def paintGL(self):
        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)

        gl.glPushMatrix()                       # push the current matrix to the current stack

        gl.glTranslate(0.0, 0.0, -5.0)          # third, translate cube to specified depth
        #gl.glScale(.5, .5, .5)                 # second, scale point cloud
        gl.glRotate(self.rotX, 1.0, 0.0, 0.0)
        gl.glRotate(self.rotY, 0.0, 1.0, 0.0)
        gl.glRotate(self.rotZ, 0.0, 0.0, 1.0)
        gl.glTranslate(-0.5, -0.5, -0.5)        # first, translate point cloud center to origin

        gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
        gl.glEnableClientState(gl.GL_COLOR_ARRAY)

        gl.glVertexPointer(3, gl.GL_FLOAT, 0, self.vertVBO)
        gl.glColorPointer(3, gl.GL_FLOAT, 0, self.colorVBO)

        gl.glPointSize(2)
        gl.glDrawElements(gl.GL_POINTS, len(self.pointsIdxArray), gl.GL_UNSIGNED_INT, self.pointsIdxArray)
        
        gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
        gl.glDisableClientState(gl.GL_COLOR_ARRAY)

        gl.glPopMatrix()    # restore the previous modelview matrix
        
    # Push geometric data to GPU
    def initGeometryPC(self):
        points, colors = load_pointcloud()
    
        self.pointsVtxArray = points
        self.vertVBO = vbo.VBO(np.reshape(self.pointsVtxArray, (1, -1)).astype(np.float32))
        self.vertVBO.bind()
        
        self.pointsClrArray = colors
        self.colorVBO = vbo.VBO(np.reshape(self.pointsClrArray, (1, -1)).astype(np.float32))
        self.colorVBO.bind()
        
        self.pointsIdxArray = np.arange(len(points))


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    win = MainWindow()
    win.show()
    sys.exit(app.exec_())

Solution

  • After a long search I came upon this stackoverflow-post. I adapted my code to that answer by storing point coordinates and colors together in one vbo-object (gl.glGenBuffers(1)). Then I define the vertex and color pointer with the specific stride and offset:

    • gl.glVertexPointer(3, gl.GL_FLOAT, 6*4, None)
      • Stride= 24 bytes: [x, y, z, r, g, b] * sizeof(float)
    • gl.glColorPointer(3, gl.GL_FLOAT, 6*4, ctypes.c_void_p(3*4))
      • Offset= 12 bytes: the rgb color starts after the 3 coordinates x, y, z

    And finally I use gl.glDrawArrays(gl.GL_POINTS, 0, noOfVertices) for drawing the point cloud.

    The full code can be seen below (marked with ### NEW ### comments):

    from PyQt5 import QtCore      # core Qt functionality
    from PyQt5 import QtGui       # extends QtCore with GUI functionality
    from PyQt5 import QtOpenGL    # provides QGLWidget, a special OpenGL QWidget
    from PyQt5 import QtWidgets
    
    import OpenGL.GL as gl        # python wrapping of OpenGL
    from OpenGL import GLU        # OpenGL Utility Library, extends OpenGL functionality
    from OpenGL.arrays import vbo
    import numpy as np
    import open3d as o3d
    import ctypes 
    import sys                    # we'll need this later to run our Qt application
    
    class MainWindow(QtWidgets.QMainWindow):
    
        def __init__(self):
            QtWidgets.QMainWindow.__init__(self)    # call the init for the parent class
    
            self.resize(300, 300)
            self.setWindowTitle('Hello OpenGL App')
            
            self.glWidget = GLWidget(self)
            self.initGUI()
            
            timer = QtCore.QTimer(self)
            timer.setInterval(20)   # period, in milliseconds
            timer.timeout.connect(self.glWidget.updateGL)
            timer.start()
            
        def initGUI(self):
            central_widget = QtWidgets.QWidget()
            gui_layout = QtWidgets.QVBoxLayout()
            central_widget.setLayout(gui_layout)
    
            self.setCentralWidget(central_widget)
    
            gui_layout.addWidget(self.glWidget)
    
            sliderX = QtWidgets.QSlider(QtCore.Qt.Horizontal)
            sliderX.valueChanged.connect(lambda val: self.glWidget.setRotX(val))
    
            sliderY = QtWidgets.QSlider(QtCore.Qt.Horizontal)
            sliderY.valueChanged.connect(lambda val: self.glWidget.setRotY(val))
    
            sliderZ = QtWidgets.QSlider(QtCore.Qt.Horizontal)
            sliderZ.valueChanged.connect(lambda val: self.glWidget.setRotZ(val))
    
            gui_layout.addWidget(sliderX)
            gui_layout.addWidget(sliderY)
            gui_layout.addWidget(sliderZ)
            
    class GLWidget(QtOpenGL.QGLWidget):
        def __init__(self, parent=None):
            self.parent = parent
            QtOpenGL.QGLWidget.__init__(self, parent)
    
        def initializeGL(self):
            self.qglClearColor(QtGui.QColor(100, 100, 100))     # initialize the screen to blue
            gl.glEnable(gl.GL_DEPTH_TEST)                       # enable depth testing
    
            self.initGeometry()
    
            self.rotX = 0.0
            self.rotY = 0.0
            self.rotZ = 0.0
    
        def setRotX(self, val):
            self.rotX = val
    
        def setRotY(self, val):
            self.rotY = val
    
        def setRotZ(self, val):
            self.rotZ = val
            
        def resizeGL(self, width, height):
            gl.glViewport(0, 0, width, height)
            gl.glMatrixMode(gl.GL_PROJECTION)
            gl.glLoadIdentity()
            aspect = width / float(height)
    
            GLU.gluPerspective(45.0, aspect, 1.0, 100.0)
            gl.glMatrixMode(gl.GL_MODELVIEW)
            
        def paintGL(self):
            gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
    
            gl.glPushMatrix()    # push the current matrix to the current stack
    
            gl.glTranslate(0.0, 0.0, -3.0)    # third, translate cube to specified depth
            #gl.glScale(20.0, 20.0, 20.0)       # second, scale cube
            gl.glRotate(self.rotX, 1.0, 0.0, 0.0)
            gl.glRotate(self.rotY, 0.0, 1.0, 0.0)
            gl.glRotate(self.rotZ, 0.0, 0.0, 1.0)
            gl.glTranslate(-0.5, -0.5, -0.5)   # first, translate cube center to origin
    
            # Point size
            gl.glPointSize(3)
            
            ### NEW ###
            gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vbo)
    
            stride = 6*4 # (24 bates) : [x, y, z, r, g, b] * sizeof(float)
    
            gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
            gl.glVertexPointer(3, gl.GL_FLOAT, stride, None)
    
            gl.glEnableClientState(gl.GL_COLOR_ARRAY)
            offset = 3*4 # (12 bytes) : the rgb color starts after the 3 coordinates x, y, z 
            gl.glColorPointer(3, gl.GL_FLOAT, stride, ctypes.c_void_p(offset))
            
            noOfVertices = self.noPoints
            gl.glDrawArrays(gl.GL_POINTS, 0, noOfVertices)
    
            gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
            gl.glDisableClientState(gl.GL_COLOR_ARRAY)
            gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
            ### NEW ###
    
            gl.glPopMatrix()    # restore the previous modelview matrix
            
    
        def initGeometry(self):
        
            vArray = self.LoadVertices()
            self.noPoints  = len(vArray) // 6
            print("No. of Points: %s" % self.noPoints)
            
            self.vbo = self.CreateBuffer(vArray)
    
            
        ### NEW ###
    
        def LoadVertices(self):
            
            pcd = o3d.io.read_point_cloud("../pointclouds/0004.ply")
            print(pcd)
            print("Pointcloud Center: " + str(pcd.get_center()))
        
            points = np.asarray(pcd.points).astype('float32')
            colors = np.asarray(pcd.colors).astype('float32')
            
            attributes = np.concatenate((points, colors),axis=1)
            print("Attributes shape: " + str(attributes.shape))
            
            return attributes.flatten()
    
        def CreateBuffer(self, attributes):
            bufferdata = (ctypes.c_float*len(attributes))(*attributes) # float buffer
            buffersize = len(attributes)*4                             # buffer size in bytes 
    
            vbo = gl.glGenBuffers(1)
            gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo)
            gl.glBufferData(gl.GL_ARRAY_BUFFER, buffersize, bufferdata, gl.GL_STATIC_DRAW) 
            gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
            return vbo
            
        ### NEW ###
    
    
    if __name__ == '__main__':
    
        app = QtWidgets.QApplication(sys.argv)
    
        win = MainWindow()
        win.show()
    
        sys.exit(app.exec_())
    

    However, I still did not find the correct parameters for initial approach above with two separate VBOs for coordinate and color. So I am happy for further comments.