Create a 3D model viewer with PyQt5 and PyQtGraph

Introduction

Somehow, when I was looking at the PyQtGraph documentation, I noticed that there was a 3D Graphics function in the API. I was curious, so I tried to make a simple GUI application that displays a 3D model in combination with PyQt5.

Since I often use 3D printers, the 3D model here refers to the STL file format.

What I made

test2.gif

You can wireframe the STL file by selecting or dragging and dropping the STL file. It is a simple program that displays only one STL file at a time. The code is also on GitHub. GitHub:https://github.com/Be4rR/STLViewer

What is PyQtGraph?

PyQtGraph is a library for drawing graphs and can be used alone, but you can easily embed the created graph in the GUI made by PyQt. Although its function is weaker than the standard Matplotlib, it is very light and suitable for plotting data in real time. It's a little-known library, but I personally find it useful. Official page: http://www.pyqtgraph.org/ Official documentation: https://pyqtgraph.readthedocs.io/en/latest/index.html

environment

I am using Python3.8, PyQt5, PyQtGraph, PyOpenGL, Numpy, Numpy-STL. PyOpenGL is required to use 3D Graphics functions with PyQtGraph. Also, read the STL file with Numpy-STL.

conda create -n stlviewer python=3.8 pyqt pyqtgraph numpy numpy-stl pyopengl 

program

It's a little long.

stl-viewer.py
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
import pyqtgraph.opengl as gl

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *  

import numpy as np
from stl import mesh

from pathlib import Path
        
class MyWindow(QMainWindow):
    def __init__(self):
        super(MyWindow, self).__init__()
        self.setGeometry(0, 0, 700, 900) 
        self.setAcceptDrops(True)
        
        self.initUI()
        
        self.currentSTL = None
        self.lastDir = None
        
        self.droppedFilename = None
    
    def initUI(self):
        centerWidget = QWidget()
        self.setCentralWidget(centerWidget)
        
        layout = QVBoxLayout()
        centerWidget.setLayout(layout)
        
        self.viewer = gl.GLViewWidget()
        layout.addWidget(self.viewer, 1)
        
        self.viewer.setWindowTitle('STL Viewer')
        self.viewer.setCameraPosition(distance=40)
        
        g = gl.GLGridItem()
        g.setSize(200, 200)
        g.setSpacing(5, 5)
        self.viewer.addItem(g)

        btn = QPushButton(text="Load STL")
        btn.clicked.connect(self.showDialog)
        btn.setFont(QFont("Ricty Diminished", 14))
        layout.addWidget(btn)
            
    def showDialog(self):
        directory = Path("")
        if self.lastDir:
            directory = self.lastDir
        fname = QFileDialog.getOpenFileName(self, "Open file", str(directory), "STL (*.stl)")
        if fname[0]:
            self.showSTL(fname[0])
            self.lastDir = Path(fname[0]).parent
            
    def showSTL(self, filename):
        if self.currentSTL:
            self.viewer.removeItem(self.currentSTL)

        points, faces = self.loadSTL(filename)
        meshdata = gl.MeshData(vertexes=points, faces=faces)
        mesh = gl.GLMeshItem(meshdata=meshdata, smooth=True, drawFaces=False, drawEdges=True, edgeColor=(0, 1, 0, 1))
        self.viewer.addItem(mesh)
        
        self.currentSTL = mesh
        
    def loadSTL(self, filename):
        m = mesh.Mesh.from_file(filename)
        shape = m.points.shape
        points = m.points.reshape(-1, 3)
        faces = np.arange(points.shape[0]).reshape(-1, 3)
        return points, faces

    def dragEnterEvent(self, e):
        print("enter")
        mimeData = e.mimeData()
        mimeList = mimeData.formats()
        filename = None
        
        if "text/uri-list" in mimeList:
            filename = mimeData.data("text/uri-list")
            filename = str(filename, encoding="utf-8")
            filename = filename.replace("file:///", "").replace("\r\n", "").replace("%20", " ")
            filename = Path(filename)
            
        if filename.exists() and filename.suffix == ".stl":
            e.accept()
            self.droppedFilename = filename
        else:
            e.ignore()
            self.droppedFilename = None
        
    def dropEvent(self, e):
        if self.droppedFilename:
            self.showSTL(self.droppedFilename)

if __name__ == '__main__':
    app = QtGui.QApplication([])
    window = MyWindow()
    window.show()
    app.exec_()

Commentary

It's not too complicated, but I'll explain some key points.

Widget for 3D display GLViewWidget

Various Graphics Items are listed in the 3D Graphics System of the PyQtGraph documentation.

The first GLViewWidget is a widget for displaying 3D models and so on. We will add the second and subsequent Graphics Items to this widget. For example, GLGridItem can be used to add a grid plane, and GLMeshItem can be used to add mesh data such as STL files. See the official documentation for details.

Since GLViewWidget can be handled in exactly the same way as a PyQt widget, it can be embedded in the PyQt GUI as it is.

View 3D model with GLMeshItem

    def showSTL(self, filename):
        #If another 3D model is already displayed, remove that 3D model.
        if self.currentSTL:
            self.viewer.removeItem(self.currentSTL)

        #Extract vertex points and face faces from the STL file.
        points, faces = self.loadSTL(filename)

        #A widget that creates a mesh and displays a 3D model(self.viewer)Add to.
        meshdata = gl.MeshData(vertexes=points, faces=faces)
        mesh = gl.GLMeshItem(meshdata=meshdata, smooth=True, drawFaces=False, drawEdges=True, edgeColor=(0, 1, 0, 1))
        self.viewer.addItem(mesh)
        
        self.currentSTL = mesh

The loadSTL function extracts vertex and face information from the STL file. Both points and faces are Numpy arrays, where points is in the form of(number of vertices, 3)and faces is in the form of (number of faces, 3).

In the above program, the vertex and face information is passed to MeshData to create meshdata, and based on that, gl.GLMeshItem is created to determine the drawing method (face and side colors, etc.). There are two steps.

Then add the created GLMeshItem to the GLViewWidget self.viewer.

self.viewer.addItem(mesh)

Show grid

The grid is also the same Graphics Item as GLMeshItem, so it can be displayed in the same way.

ʻInitUI` function part.

        g = gl.GLGridItem()
        g.setSize(200, 200)
        g.setSpacing(5, 5)
        self.viewer.addItem(g)

After creating with GLGridItem (), the size is decided with the setSize function, and the size of one grid is specified with the setSpacing function. Finally, add it to self.viewer of GLViewWidget with the ʻaddItem` function.

Recommended Posts

Create a 3D model viewer with PyQt5 and PyQtGraph
Create a heatmap with pyqtgraph
Implement a model with state and behavior
Create a native GUI app with Py2app and Tkinter
Create a batch of images and inflate with ImageDataGenerator
I came up with a way to create a 3D model from a photo Part 02 Image loading and vertex drawing
[Python] How to create a 2D histogram with Matplotlib
Create a 2d CAD file ".dxf" with python [ezdxf]
[Linux] Create a self-signed certificate with Docker and apache
Create 3d gif with python3
Create a homepage with django
Create Image Viewer with Tkinter
Create a directory with python
Create a web surveillance camera with Raspberry Pi and OpenCV
[Azure] Create, deploy, and relearn a model [ML Studio classic]
Create applications, register data, and share with a single email
Let's create a PRML diagram with Python, Numpy and matplotlib.
Make a 2D RPG with Ren'Py (3) -Items and Tool Shop
Create a deploy script with fabric and cuisine and reuse it
Create a python machine learning model relearning mechanism with mlflow
I came up with a way to create a 3D model from a photo Part 04 Polygon generation
Draw a graph with Julia + PyQtGraph (2)
Let's create a tic-tac-toe AI with Pylearn 2-Save and load models-
Create a temporary file with django as a zip file and return it
Solve ABC166 A ~ D with Python
Create a striped illusion with gamma correction for Python3 and openCV3
Create a virtual environment with Python!
Draw a graph with Julia + PyQtGraph (1)
I came up with a way to create a 3D model from a photo Part 01 Creating an environment
Draw a graph with Julia + PyQtGraph (3)
Create a private DMP with zero initial cost and zero development with BigQuery
I tried to create Bulls and Cows with a shell program
Make a model iterator with PySide
Create a poisson stepper with numpy.random
Create a C ++ and Python execution environment with WSL2 + Docker + VSCode
Create a simple Python development environment with VS Code and Docker
Create a file uploader with Django
Create and return a CP932 CSV file for Excel with Chalice
[AWS] Create a Python Lambda environment with CodeStar and do Hello World
I came up with a way to make a 3D model from a photo.
Create a stack with a queue and a queue with a stack (from LetCode / Implement Stack using Queues, Implement Queue using Stacks)
Create a Todo app with Django ④ Implement folder and task creation functions
Create a Python3 environment with pyenv on Mac and display a NetworkX graph
Implement a model with state and behavior (3) --Example of implementation by decorator
Create a decision tree from 0 with Python and understand it (5. Information Entropy)
Create a Python function decorator with Class
Build a blockchain with Python ① Create a class
Create a dummy image with Python + PIL.
Create a model for your Django schedule
[Python] Create a virtual environment with Anaconda
Let's create a free group with Python
Create a GUI app with Python's Tkinter
A memo with Python2.7 and Python3 on CentOS
Create a large text file with shellscript
Create and decrypt Caesar cipher with python
Create a star system with Blender 2.80 script
Create a virtual environment with Python_Mac version
Create a VM with a YAML file (KVM)
Create a simple web app with flask
Solve AtCoder ABC168 with python (A ~ D)
Create a word frequency counter with Python 3.4