文章目录
前言
3D打印切片控制一体化流程中涉及到的文件格式主要有stl以及gcode文件。stl文件为三维实体模型,其预览功能的实现较为成熟,对于java和python而言,通常可以运用openGL库实现预览,而gcode文件为切片处理后生成的文件,本质上与txt等文本文件类似,不包含stl文件中的面片信息,因此实现gcode文件的预览需要对其文件进行解析,实现过程较为复杂,相关资料较少,本文在实现gcode基本预览的基础上,基于marlin固件的M165指令完成gcode模型的全彩预览,主要涉及到的编程语言为python,涉及到的第三方库主要有pyqt5和vtk。
一、pyqt5和vtk是什么?
在进行开发时,后台实现业务逻辑,前台负责界面展示,网络编程通常使用html+css+JavaScript的方式完成前端界面的展示,利用AS进行过Android开发的人通常使用xml文件完成前端界面。如果不想使用类似flask这样的web框架进行网络编程,只希望做一个简单的exe文件方便自己的日常使用,当然也可以用纯python进行界面制作,PyQt5 就是这样一种具有强大功能的GUI库之一。
vtk是一种图形库,主要用于三维计算机图形学、图像处理和可视化。Vtk是在面向对象原理的基础上设计和实现的,它的内核采用C++构建,但同样可以运用在python以及Java中,stl预览以及gcode预览都可以基于此库实现。
二、基本原理
三、实现步骤
1.在pycharm上安装所需库
python解释器安装库(画圈为预览所必须的库):
2.随意写一个pyqt5界面类并定义预览控件的初始化方法
代码如下(记得先import相关库):
def init3dWidget(self):
widget3d = QVTKRenderWindowInteractor()
widget3d.Initialize()
widget3d.Start()
self.render = vtk.vtkRenderer()
self.render.SetBackground(params.BackgroundColor)#设置背景颜色
widget3d.GetRenderWindow().AddRenderer(self.render)
self.interactor = widget3d.GetRenderWindow().GetInteractor()
self.interactor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()
self.axesWidget = gui_utils.createAxes(self.interactor)#创建坐标轴
return widget3d
初始化方法的意义在于规定预览窗口的背景颜色以及坐标轴等基础元素,至于预览窗口的大小可以先利用pyqt5的相关布局确定大小,再利用addWidget方法将上述初始化方法传入完成基础预览窗口的创建。
self.main_grid.addWidget(self.init3dWidget())
3.在窗口中加入预览台
代码如下(示例):
self.planeActor = gui_utils.createPlaneActorCircle(params.PlaneCenter)#以圆柱体为例
self.planeTransform = vtk.vtkTransform()
self.render.AddActor(self.planeActor)
self.render.ResetCamera()
下面两个函数在gui_utils.py中,此文件主要定义共有函数:
def createPlaneActorCircle(x):
return createPlaneActorCircleByCenter(x)
def createPlaneActorCircleByCenter(center):
cylinder = vtk.vtkCylinderSource()#形状
cylinder.SetResolution(50)
cylinder.SetRadius(params.PlaneDiameter / 2)#大小
cylinder.SetHeight(0.1)#高度
cylinder.SetCenter(center[0], center[2] - 0.1, center[1])#中心
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(cylinder.GetOutputPort())
actor = vtk.vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetColor(params.PlaneColor)#颜色
actor.RotateX(90)
return actor
完成以上两步就能看到初始化界面窗口和预览台,如下图所示
4.解析gcode文件
完成显示窗口之后,可以进一步完善gcode的全彩预览后台,为此首先需要设置一个按钮控件并利用pyqt5提供的QFileDialog.getOpenFileName方法创建一个gcode文件的选择窗口,将gcode路径传入解析函数。
解析代码如下(示例):
def readGCode(filename):
with open(filename) as f:
lines = [line.strip() for line in f]
return parseGCode(lines)
def parseGCode(lines):
path = []
layer = []
layers = []#位置信息和旋转信息
rotations = []
lays2rots = []
color = []#颜色信息
floor = 0#层数
divide = []#分层信息
center = None#模型中心位置
plane = []#只有xyz坐标值
stop = False
rotations.append(Rotation(0, 0))
x, y, z = 0, 0, 0
w,e,r = 0,0,0
abs_pos = True # absolute positioning
def finishLayer():
nonlocal path, layer
if len(path) > 1:
layer.append(path)
path = [[x, y, z]]
if len(layer) > 0:
layers.append(layer)
lays2rots.append(len(rotations) - 1)
layer = []
for line in lines:
if len(line) == 0:
continue
if line.startswith(";LAYER:1"):
stop = True
if line.startswith(';'):
if line.startswith(";LAYER:"):
finishLayer()
floor += 1
# elif line.startswith(";End"):
# break
# if line.startswith(";LAYER:"):
if stop is True:
for i in range(len(plane)):
w += plane[i][0]
e += plane[i][1]
r += plane[i][2]
w = w / len(plane)
e = e / len(plane)
r = r / len(plane)
stop = False #归位
if center is None:#保证center只有一个值
center = [w, e, r]
else:
if "G1" in line or "G0" in line:
a = float(getValue(line, "X", -1))
b = float(getValue(line, "Y", -1))
c = float(getValue(line, "Z", -1))
if a != -1 and b != -1 and c != -1:
plane.append([a, -b,-c])
else:
plane.append([a, -b, 0])
args = line.split(" ")
if args[0] == "G0":
if len(path) > 1: # finish path and start new
layer.append(path)
x, y, z, z_rot = parseArgs(args[1:], x, y, z, abs_pos)
path = [[x, y, z]]
if z_rot is not None:
finishLayer()
rotations.append(Rotation(rotations[-1].x_rot, z_rot))
elif args[0] == "M165":#全彩解析
divide.append(floor)
a = float(getValue(line, "A", -1))
b = float(getValue(line, "B", -1))
c = float(getValue(line, "C", -1))
color.append(material_chose(a,b,c))
print(color)
elif args[0] == "G1" : # draw to
x, y, z, _ = parseArgs(args[1:], x, y, z, abs_pos)
path.append([x, y, z])
elif args[0] == "G90": # absolute positioning
abs_pos = True
elif args[0] == "G91": # relative positioning
abs_pos = False
else:
pass # skip
# finishLayer() # not forget about last layer
divide.append(floor)
print(len(divide),floor,len(color),center)
layers.append(layer)
lays2rots.append(len(rotations) - 1)
# print(layers)
return GCode(layers, rotations, lays2rots,color,divide,center)#每一层单独存储
def material_chose(user_a,user_b,user_c):
"""挤料比例模块"""
color = []
info = [{'C': 98, 'M': 1, 'Y': 1, 'R': 128, 'G': 199, 'B': 217},
{'C': 89, 'M': 10, 'Y': 1, 'R': 153, 'G': 185, 'B': 215},
{'C': 79, 'M': 20, 'Y': 1, 'R': 160, 'G': 144, 'B': 191},
{'C': 69, 'M': 30, 'Y': 1, 'R': 172, 'G': 136, 'B': 188},
{'C': 59, 'M': 40, 'Y': 1, 'R': 175, 'G': 112, 'B': 172},
{'C': 49, 'M': 50, 'Y': 1, 'R': 179, 'G': 94, 'B': 161},
{'C': 39, 'M': 60, 'Y': 1, 'R': 190, 'G': 92, 'B': 165},
{'C': 29, 'M': 70, 'Y': 1, 'R': 195, 'G': 93, 'B': 167},
{'C': 19, 'M': 80, 'Y': 1, 'R': 210, 'G': 95, 'B': 170},
{'C': 9, 'M': 90, 'Y': 1, 'R': 225, 'G': 104, 'B': 181},
{'C': 1, 'M': 98, 'Y': 1, 'R': 244, 'G': 104, 'B': 193},
{'C': 1, 'M': 89, 'Y': 10, 'R': 232, 'G': 112, 'B': 163},
{'C': 1, 'M': 79, 'Y': 20, 'R': 227, 'G': 107, 'B': 142},
{'C': 1, 'M': 69, 'Y': 30, 'R': 223, 'G': 101, 'B': 129},
{'C': 1, 'M': 59, 'Y': 40, 'R': 225, 'G': 109, 'B': 128},
{'C': 1, 'M': 49, 'Y': 50, 'R': 224, 'G': 118, 'B': 127},
{'C': 1, 'M': 39, 'Y': 60, 'R': 223, 'G': 124, 'B': 120},
{'C': 1, 'M': 29, 'Y': 70, 'R': 223, 'G': 137, 'B': 116},
{'C': 1, 'M': 19, 'Y': 80, 'R': 225, 'G': 159, 'B': 113},
{'C': 1, 'M': 9, 'Y': 90, 'R': 226, 'G': 179, 'B': 96},
{'C':1, 'M':1, 'Y':98, 'R':223, 'G':215, 'B':89},
{'C':10, 'M':1, 'Y':89, 'R':214, 'G':213, 'B':88},
{'C':20, 'M':1, 'Y':79, 'R':194, 'G':211, 'B':105},
{'C':30, 'M':1, 'Y':69, 'R':177, 'G':202, 'B':108},
{'C':40, 'M':1, 'Y':59, 'R':169, 'G':203, 'B':126},
{'C':50, 'M':1, 'Y':49, 'R':161, 'G':200, 'B':138},
{'C':60, 'M':1, 'Y':39, 'R':153, 'G':195, 'B':145},
{'C':70, 'M':1, 'Y':29, 'R':140, 'G':191, 'B':154},
{'C':80, 'M':1, 'Y':19, 'R':141, 'G':197, 'B':168},
{'C':90, 'M':1, 'Y':9, 'R':137, 'G':199, 'B':190},
{'C':80, 'M':10, 'Y':10, 'R':138, 'G':145, 'B':129},
{'C':70, 'M':20, 'Y':10, 'R':141, 'G':111, 'B':113},
{'C':60, 'M':30, 'Y':10, 'R':149, 'G':91, 'B':106},
{'C':50, 'M':40, 'Y':10, 'R':154, 'G':71, 'B':94},
{'C':40, 'M':50, 'Y':10, 'R':163, 'G':63, 'B':89},
{'C':30, 'M':60, 'Y':10, 'R':168, 'G':54, 'B':80},
{'C':20, 'M':70, 'Y':10, 'R':178, 'G':50, 'B':77},
{'C':10, 'M':80, 'Y':10, 'R':187, 'G':44, 'B':77},
{'C':70, 'M':10, 'Y':20, 'R':152, 'G':150, 'B':110},
{'C': 60, 'M': 20, 'Y': 20, 'R': 158, 'G': 131, 'B': 117},
{'C': 50, 'M': 30, 'Y': 20, 'R': 173, 'G': 120, 'B': 118},
{'C': 40, 'M': 40, 'Y': 20, 'R': 181, 'G': 110, 'B': 116},
{'C': 30, 'M': 50, 'Y': 20, 'R': 177, 'G': 88, 'B': 98},
{'C': 20, 'M': 60, 'Y': 20, 'R': 187, 'G': 76, 'B': 92},
{'C': 10, 'M': 70, 'Y': 20, 'R': 193, 'G': 69, 'B': 90},
{'C': 60, 'M': 10, 'Y': 30, 'R': 160, 'G': 161, 'B': 114},
{'C': 50, 'M': 20, 'Y': 30, 'R': 171, 'G': 131, 'B': 106},
{'C': 40, 'M': 30, 'Y': 30, 'R': 183, 'G': 122, 'B': 101},
{'C': 30, 'M': 40, 'Y': 30, 'R': 173, 'G': 88, 'B': 80},
{'C': 20, 'M': 50, 'Y': 30, 'R': 184, 'G': 84, 'B': 80},
{'C': 10, 'M': 60, 'Y': 30, 'R': 182, 'G': 74, 'B': 80},
{'C': 50, 'M': 10, 'Y': 40, 'R': 158, 'G': 155, 'B': 86},
{'C': 40, 'M': 20, 'Y': 40, 'R': 171, 'G': 129, 'B': 83},
{'C': 30, 'M': 30, 'Y': 40, 'R': 178, 'G': 104, 'B': 78},
{'C': 20, 'M': 40, 'Y': 40, 'R': 182, 'G': 93, 'B': 70},
{'C': 10, 'M': 50, 'Y': 40, 'R': 192, 'G': 82, 'B': 72},
{'C': 40, 'M': 10, 'Y': 50, 'R': 172, 'G': 152, 'B': 65},
{'C': 30, 'M': 20, 'Y': 50, 'R': 174, 'G': 126, 'B': 85},
{'C': 20, 'M': 30, 'Y': 50, 'R': 179, 'G': 108, 'B': 86},
{'C': 10, 'M': 40, 'Y': 50, 'R': 188, 'G': 91, 'B': 78},
{'C': 30, 'M': 10, 'Y': 60, 'R': 168, 'G': 146, 'B': 78},
{'C': 20, 'M': 20, 'Y': 60, 'R': 179, 'G': 120, 'B': 76},
{'C': 10, 'M': 30, 'Y': 60, 'R': 190, 'G': 104, 'B': 74},
{'C': 20, 'M': 10, 'Y': 70, 'R': 187, 'G': 149, 'B': 65},
{'C': 10, 'M': 20, 'Y': 70, 'R': 197, 'G': 128, 'B': 63},
{'C': 10, 'M': 10, 'Y': 80, 'R': 206, 'G': 162, 'B': 59}]
for i in info:
if user_a == i['C'] and user_b == i['M'] and user_c == i['Y']:
color = [i['R'],i['G'],i['B']]
if len(color) == 0:#如果固定色卡中没有则利用公式进行转换
total = user_a + user_b + user_c
color = [255-255*user_a/total,255-255*user_b/total,255-255*user_c/total]
return color
5.将分层位置信息转化为分层面片信息
def makeBlocks(layers): # 将离散点变成面片信息
blocks = []
for layer in layers:
points = vtk.vtkPoints() # 点云
lines = vtk.vtkCellArray()
block = vtk.vtkPolyData()
points_count = 0
for path in layer:
line = vtk.vtkLine()
for k in range(len(path) - 1):
points.InsertNextPoint(path[k])
line.GetPointIds().SetId(0, points_count + k)
line.GetPointIds().SetId(1, points_count + k + 1)
lines.InsertNextCell(line)
points.InsertNextPoint(path[-1]) # not forget to add last point
points_count += len(path)
block.SetPoints(points)
block.SetLines(lines)
blocks.append(block)
return blocks
此函数传入的layers对应着解析函数中layers,此函数主要将点信息转化为线信息再转化为面片信息,接着就可以这些分层面片信息赋予颜色并转化分层实体模型。
6.将分层面片信息根据颜色信息进行渲染
def wrapWithActors(blocks, rotations, lays2rots,color = None,divide = None):
actors = []
for i in range(len(blocks)):
block = blocks[i]
actor = build_actor(block, True)
transform = vtk.vtkTransform()
# rotate to abs coords firstly and then apply last rotation
transform.PostMultiply()
transform.RotateX(-rotations[lays2rots[i]].x_rot)
transform.PostMultiply()
transform.RotateZ(-rotations[lays2rots[i]].z_rot)
transform.PostMultiply()
transform.RotateZ(rotations[-1].z_rot)
transform.PostMultiply()
transform.RotateX(rotations[-1].x_rot)
actor.SetUserTransform(transform)
if len(color) == 0:
actor.GetProperty().SetColor(params.LastLayerColor)
elif len(color) == 1:
actor.GetProperty().SetColor(color[0][0]/255,color[0][1]/255,color[0][2]/255)
actors.append(actor)
if len(color) >1:
if len(color) == len(actors)-2:
for i in range(len(color)):
actors[i].GetProperty().SetColor(color[i][0]/255,color[i][1]/255,color[i][2]/255)
else:
for i in range(len(color)):
for layer in range(divide[i], divide[i + 1]):
if len(actors) - 1>=divide[i+1]:
actors[layer + 1].GetProperty().SetColor(color[i][0]/255,color[i][1]/255,color[i][2]/255)
else:
for layer in range(divide[i+1],len(actors)-1):
actors[layer + 1].GetProperty().SetColor(color[i][0] / 255, color[i][1] / 255,color[i][2] / 255)
print(len(actors))
actors[-1].GetProperty().SetColor(params.LayerColor)
return actors
def build_actor(source, as_is=False):#source就是面片信息,gcode需要得到面片信息
mapper = vtk.vtkPolyDataMapper()# 2. 建图(将点拼接成立方体)
if as_is:
mapper.SetInputData(source)#gcode信息需要用另外的方法提取
else:
mapper.SetInputData(source.GetOutput())#提取解析stl结果的面片信息
actor = vtk.vtkActor()
actor.SetMapper(mapper)# 3. 根据2创建执行单元
return actor
7.清空界面,根据解析出的模型中心位置重新添加预览台,分层叠加渲染结果
self.clearScene()#清空预览台
self.planeActor = gui_utils.createPlaneActorCircle(self.gode.center)
self.render.AddActor(self.planeActor)#自适应生成符合模型位置的预览台
for actor in self.actors:#分层叠加渲染结果
self.render.AddActor(actor)
四、结果展示
渐变模型:
分层模型:
单色模型:
五、部分说明
- 上述解析函数只适用于cura切出的相关模型,如果想兼容预览3r切出的gcode文件,还需在文件导入后进行相关预处理,不仅需要添加类似layer;这样的标识符便于分层解析,也需要将3r文件中跳转点的G1指令转化为与cura相同的G0指令。
- 本文的颜色信息的解析主要依赖于marlin固件提供的M165指令,此指令类似M165 A0.8733 B0.0188 C0.1079,其中ABC对应的是挤出机对应的挤料比例,本人使用的三喷头采用的是cmy三原色,即青色(Cyan)、品红(Magenta)和黄色(Yellow)。
- 使用vtk库同样可以完成stl模型的三维预览,预览形式如下图所示,代码实现在本文就不多赘述。
- 由于现在主流的cura以及3r切片软件无法切出含有M165指令的gcode,因此需要进行相关的gcode后处理实现M165指令的插入,单色、分层以及渐变的gcode后处理方法会再写一篇博客展示。
PS:
第一次写博客,水平有限,有什么问题可以联系本尊,扣扣:408536802,谢谢!