#はじめに
なんとなくPyQtGraphのドキュメントを眺めていたら,APIの中に3D Graphicsの機能があることに気づきました.気になったので試しにPyQt5と組み合わせて3Dモデルを表示する簡単なGUIアプリケーションを作ってみました.
私が3Dプリンタをよく使う関係で,ここでいう3DモデルはSTLファイル形式のものを指しています.
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ファイルから頂点と面の情報を抽出します.points
,faces
のいずれもNumpy配列で,points
は(頂点の数, 3)
,faces
は(面の数,3)
の形をしています.
上のプログラムでは頂点と面の情報をMeshData
に渡してmeshdata
を作成し,さらにそれをもとにgl.GLMeshItem
を作成して描画方法(面や辺の色など)を決めるという二段階を踏んでいます.
そして作成したGLMeshItem
をGLViewWidget
である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つ分の大きさを指定しています.最後にGLViewWidget
のself.viewer
にaddItem
関数で追加します.