LoginSignup
7
7

More than 3 years have passed since last update.

PyQt5とPyQtGraphで3Dモデルビューワーを作る

Last updated at Posted at 2020-09-07

はじめに

なんとなくPyQtGraphのドキュメントを眺めていたら,APIの中に3D Graphicsの機能があることに気づきました.気になったので試しにPyQt5と組み合わせて3Dモデルを表示する簡単なGUIアプリケーションを作ってみました.

私が3Dプリンタをよく使う関係で,ここでいう3DモデルはSTLファイル形式のものを指しています.

作ったもの

test2.gif

STLファイルを選択するかドラッグアンドドロップすることで,STLファイルをワイヤフレーム表示できます.1度に1つのSTLファイルのみを表示するシンプルなプログラムです.コードはGitHubにもあります.
GitHub:https://github.com/Be4rR/STLViewer

PyQtGraphとは?

PyQtGraphはグラフ描画用のライブラリで,単体でも使えますが,作成したグラフをPyQt製のGUIに埋め込むことも簡単にできます.定番のMatplotlibと比べると機能は弱いですが,非常に軽いためリアルタイムにデータをプロットするような場合に適しています.あまり知られていないライブラリではありますが,個人的に重宝しています.
公式ページ:http://www.pyqtgraph.org/
公式ドキュメント:https://pyqtgraph.readthedocs.io/en/latest/index.html

環境

Python3.8,PyQt5,PyQtGraph,PyOpenGL,Numpy,Numpy-STLを使用しています.
PyOpenGLはPyQtGraphで3D Graphicsの機能を使う際に必要になります.またNumpy-STLでSTLファイルを読み込みます.

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

プログラム

少し長いです.

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_()

解説

あまり複雑なことはしていませんが,いくつかポイントとなる部分を説明します.

3D表示用のウィジェットGLViewWidget

PyQtGraphのドキュメントの3D Graphics Systemに様々なGraphics Itemが挙げられています.

  • GLViewWidget
  • GLGridItem
  • GLSurfacePlotItem
  • GLVolumeItem
  • GLImageItem
  • GLMeshItem
  • GLLinePlotItem
  • GLAxisItem
  • GLGraphicsItem
  • GLScatterPlotItem
  • MeshData

1番目のGLViewWidgetは3Dモデルなどを表示するためのウィジェットです.このウィジェットに2番目以降のGraphics Itemを追加していきます.たとえばGLGridItemでグリッド平面を追加したり,GLMeshItemでSTLファイルなどのメッシュデータを追加できます.詳しくは公式のドキュメントを見てください.

GLViewWidgetはPyQtのウィジェットと全く同じように扱えるので,PyQtのGUIにそのまま埋め込むことができます.

GLMeshItemで3Dモデルを表示

    def showSTL(self, filename):
        # 既に他の3Dモデルを表示している場合,その3Dモデルを取り除く.
        if self.currentSTL:
            self.viewer.removeItem(self.currentSTL)

        # STLファイルから頂点points,面facesを抽出する.
        points, faces = self.loadSTL(filename)

        # メッシュを作成し,3Dモデルを表示するウィジェット(self.viewer)に追加する.
        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

loadSTL関数はSTLファイルから頂点と面の情報を抽出します.pointsfacesのいずれもNumpy配列で,points(頂点の数, 3)faces(面の数,3)の形をしています.

上のプログラムでは頂点と面の情報をMeshDataに渡してmeshdataを作成し,さらにそれをもとにgl.GLMeshItemを作成して描画方法(面や辺の色など)を決めるという二段階を踏んでいます.

そして作成したGLMeshItemGLViewWidgetであるself.viewerに追加します.

self.viewer.addItem(mesh)

グリッドを表示する

グリッドもGLMeshItemと同じGraphics Itemなので,同じようにして表示できます.

initUI関数の部分です.

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

GLGridItem()で作成後,setSize関数でサイズを決め,setSpacing関数でグリッド1つ分の大きさを指定しています.最後にGLViewWidgetself.vieweraddItem関数で追加します.

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7