I am writing a PyQt5 application that uses OpenGL to slice an STL into images and displays them as it goes. The application works fine when invoked from the command line (Windows 10, Python 3.7.1, PyInstaller 3.5), but when I try packaging it with pyinstaller
it crashes with an error:
Traceback (most recent call last):
File "app_qt.py", line 45, in initializeGL
self.gl = self.context().versionFunctions()
ModuleNotFoundError: No module named 'PyQt5._QOpenGLFunctions_4_1_Core'`
The call comes from a QtGui.QOpenGLWindow
object. I have followed the suggestions in this answer to no avail. I have tried importing PyQt5 directly, and adding it to the hidden imports in the .spec
file as well for pyinstaller.
Since the application runs fine when invoked normally (i.e. python slicer_gui.py
) I am inclined to believe pyinstaller
is neglecting to add an import to the package somewhere.
Here is the full code. The GUI is simple (pun intended):
import PySimpleGUI as sg
import app_pyopengl
# define GUI layout
layout = [
[sg.Text('STL to Slice', size=(16, 1)), sg.Input(), sg.FileBrowse()],
[sg.Text('Layer Thickness (um) ', size=(16, 1)), sg.InputText('10')],
[sg.Submit(button_text="Slice"), sg.Cancel(button_text='Quit')]
]
# name the window
window = sg.Window('PyQT STL Slicer').Layout(layout)
# loop until user quits
while True:
button, values = window.Read() # read all values in the window
if button == "Quit":
exit()
if button == "Slice":
thickness = float(values[1]) / 1000 # convert um to mm
filename = values[0]
app_pyopengl.main(filename, thickness)
Here is the slicing application:
import sys
import os
import shutil
from ctypes import c_float, c_uint, sizeof
from PyQt5 import QtGui, QtCore, QtWidgets
from stl import mesh
import numpy as np
from printer import printer
GLfloat = c_float
GLuint = c_uint
EPSILON = 0.00001
SCR_WIDTH = 640
SCR_HEIGHT = int(SCR_WIDTH * printer.height / printer.width)
class Window(QtGui.QOpenGLWindow):
def __init__(self,
stlFilename,
layerThickness,
sliceSavePath,
*args,
**kwargs):
super().__init__(*args, **kwargs)
self.setTitle('STL Slicer')
self.vertVAO, self.vertVBO = 0, 0
self.maskVAO, self.maskVBO = 0, 0
self.numOfVerts = 0
self.bounds = dict()
self.totalThickness = 0.
self.currentLayer = 0
self.height = 0
self.stlFilename = stlFilename
self.layerThickness = layerThickness
self.sliceSavePath = sliceSavePath
def initializeGL(self):
self.gl = self.context().versionFunctions()
self.shaderProg = QtGui.QOpenGLShaderProgram()
self.shaderProg.create()
self.shaderProg.addShaderFromSourceFile(
QtGui.QOpenGLShader.Vertex, 'shaders/slice.vert')
self.shaderProg.addShaderFromSourceFile(
QtGui.QOpenGLShader.Fragment, 'shaders/slice.frag')
self.shaderProg.link()
self.loadMesh()
self.proj = QtGui.QMatrix4x4()
self.proj.setToIdentity()
self.proj.ortho(0, printer.width*printer.pixel,
0, printer.height*printer.pixel,
-self.totalThickness, self.totalThickness)
self.model = QtGui.QMatrix4x4()
self.model.setToIdentity()
self.model.translate(0, 0, self.totalThickness+EPSILON)
self.sliceFbo = QtGui.QOpenGLFramebufferObject(
printer.width,
printer.height
)
self.sliceFbo.setAttachment(
QtGui.QOpenGLFramebufferObject.CombinedDepthStencil
)
def loadMesh(self):
# Get information about our mesh
ourMesh = mesh.Mesh.from_file(self.stlFilename)
self.numOfVerts = ourMesh.vectors.shape[0] * 3
self.bounds = {
'xmin': np.min(ourMesh.vectors[:,:,0]),
'xmax': np.max(ourMesh.vectors[:,:,0]),
'ymin': np.min(ourMesh.vectors[:,:,1]),
'ymax': np.max(ourMesh.vectors[:,:,1]),
'zmin': np.min(ourMesh.vectors[:,:,2]),
'zmax': np.max(ourMesh.vectors[:,:,2])
}
self.totalThickness = self.bounds['zmax'] - self.bounds['zmin']
#######################################
# make VAO for drawing our mesh
self.vertVAO = QtGui.QOpenGLVertexArrayObject()
self.vertVAO.create()
self.vertVAO.bind()
self.vertVBO = QtGui.QOpenGLBuffer(QtGui.QOpenGLBuffer.VertexBuffer)
self.vertVBO.create()
self.vertVBO.bind()
self.vertVBO.setUsagePattern(QtGui.QOpenGLBuffer.StaticDraw)
data = ourMesh.vectors.astype(GLfloat).tostring()
self.vertVBO.allocate(data, len(data))
self.gl.glVertexAttribPointer(0, 3, self.gl.GL_FLOAT,
self.gl.GL_FALSE, 3*sizeof(GLfloat), 0)
self.gl.glEnableVertexAttribArray(0)
self.vertVBO.release()
self.vertVAO.release()
#######################################
# a mask vertex array for stencil buffer to subtract
maskVert = np.array(
[[0, 0, 0],
[printer.width*printer.pixel, 0, 0],
[printer.width*printer.pixel, printer.height*printer.pixel, 0],
[0, 0, 0],
[printer.width*printer.pixel, printer.height*printer.pixel, 0],
[0, printer.height*printer.pixel, 0]], dtype=GLfloat
)
#######################################
# make VAO for drawing mask
self.maskVAO = QtGui.QOpenGLVertexArrayObject()
self.maskVAO.create()
self.maskVAO.bind()
self.maskVBO = QtGui.QOpenGLBuffer(QtGui.QOpenGLBuffer.VertexBuffer)
self.maskVBO.create()
self.maskVBO.bind()
self.maskVBO.setUsagePattern(QtGui.QOpenGLBuffer.StaticDraw)
data = maskVert.tostring()
self.maskVBO.allocate(data, len(data))
self.gl.glVertexAttribPointer(0, 3, self.gl.GL_FLOAT,
self.gl.GL_FALSE, 3*sizeof(GLfloat), 0)
self.gl.glEnableVertexAttribArray(0)
self.maskVBO.release()
self.maskVAO.release()
#######################################
def paintGL(self):
if self.height >= self.totalThickness-EPSILON:
sys.exit()
else:
self.height += self.layerThickness
self.currentLayer += 1
self.draw()
self.renderSlice()
self.update()
def draw(self):
self.gl.glViewport(0, 0, self.size().width(), self.size().height())
self.gl.glEnable(self.gl.GL_STENCIL_TEST)
self.gl.glClearColor(0., 0., 0., 1.)
self.gl.glClear(self.gl.GL_COLOR_BUFFER_BIT | self.gl.GL_STENCIL_BUFFER_BIT)
self.vertVAO.bind()
self.shaderProg.bind()
self.model.translate(0, 0, -self.layerThickness)
self.shaderProg.setUniformValue('proj', self.proj)
self.shaderProg.setUniformValue('model', self.model)
self.gl.glEnable(self.gl.GL_CULL_FACE)
self.gl.glCullFace(self.gl.GL_FRONT)
self.gl.glStencilFunc(self.gl.GL_ALWAYS, 0, 0xFF)
self.gl.glStencilOp(self.gl.GL_KEEP, self.gl.GL_KEEP, self.gl.GL_INCR)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, self.numOfVerts)
self.gl.glCullFace(self.gl.GL_BACK)
self.gl.glStencilOp(self.gl.GL_KEEP, self.gl.GL_KEEP, self.gl.GL_DECR)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, self.numOfVerts)
self.gl.glDisable(self.gl.GL_CULL_FACE)
self.gl.glClear(self.gl.GL_COLOR_BUFFER_BIT)
self.maskVAO.bind()
self.gl.glStencilFunc(self.gl.GL_NOTEQUAL, 0, 0xFF)
self.gl.glStencilOp(self.gl.GL_KEEP, self.gl.GL_KEEP, self.gl.GL_KEEP)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, 6)
self.gl.glDisable(self.gl.GL_STENCIL_TEST)
self.shaderProg.release()
def renderSlice(self):
self.sliceFbo.bind()
self.gl.glViewport(0, 0, printer.width, printer.height)
self.gl.glEnable(self.gl.GL_STENCIL_TEST)
self.gl.glClearColor(0., 0., 0., 1.)
self.gl.glClear(self.gl.GL_COLOR_BUFFER_BIT | self.gl.GL_STENCIL_BUFFER_BIT)
self.vertVAO.bind()
self.shaderProg.bind()
self.shaderProg.setUniformValue('proj', self.proj)
self.shaderProg.setUniformValue('model', self.model)
self.gl.glEnable(self.gl.GL_CULL_FACE)
self.gl.glCullFace(self.gl.GL_FRONT)
self.gl.glStencilFunc(self.gl.GL_ALWAYS, 0, 0xFF)
self.gl.glStencilOp(self.gl.GL_KEEP, self.gl.GL_KEEP, self.gl.GL_INCR)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, self.numOfVerts)
self.gl.glCullFace(self.gl.GL_BACK)
self.gl.glStencilOp(self.gl.GL_KEEP, self.gl.GL_KEEP, self.gl.GL_DECR)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, self.numOfVerts)
self.gl.glDisable(self.gl.GL_CULL_FACE)
self.gl.glClear(self.gl.GL_COLOR_BUFFER_BIT)
self.maskVAO.bind()
self.gl.glStencilFunc(self.gl.GL_NOTEQUAL, 0, 0xFF)
self.gl.glStencilOp(self.gl.GL_KEEP, self.gl.GL_KEEP, self.gl.GL_KEEP)
self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, 6)
self.gl.glDisable(self.gl.GL_STENCIL_TEST)
image = self.sliceFbo.toImage()
# makes a QComboBox for different Image Format,
# namely Format_Mono, Format_MonoLSB, and Format_Grayscale8
image = image.convertToFormat(QtGui.QImage.Format_Grayscale8)
image.save(os.path.join(self.sliceSavePath,
'out{:04d}.png'.format(self.currentLayer)))
self.sliceFbo.release()
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Escape:
sys.exit()
event.accept()
def main(stlFilename, layerThickness):
temp = os.path.dirname(stlFilename)
sliceSavePath = os.path.join(temp, 'slices')
# remove old slices folder if there is one, and make a new empty one
if os.path.exists(sliceSavePath):
shutil.rmtree(sliceSavePath) # using shutil here avoids permissions errors
if not os.path.exists(sliceSavePath):
os.mkdir(sliceSavePath)
# Set format here, otherwise it throws error
# `QCocoaGLContext: Falling back to unshared context.`
# on Mac when use QOpenGLWidgets
# https://doc.qt.io/qt-5/qopenglwidget.html#details last paragraph
format = QtGui.QSurfaceFormat()
format.setRenderableType(QtGui.QSurfaceFormat.OpenGL)
format.setProfile(QtGui.QSurfaceFormat.CoreProfile)
format.setVersion(4, 1)
format.setStencilBufferSize(8)
QtGui.QSurfaceFormat.setDefaultFormat(format)
app = QtWidgets.QApplication(sys.argv)
window = Window(stlFilename, layerThickness, sliceSavePath)
window.resize(SCR_WIDTH, SCR_HEIGHT)
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main(sys.argv[1], float(sys.argv[2]))