利用python的pyqt5和vtk库实现对gcode模型的全彩预览


前言

 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)

四、结果展示

渐变模型:
渐变模型
分层模型:
在这里插入图片描述
单色模型:
在这里插入图片描述

五、部分说明

  1. 上述解析函数只适用于cura切出的相关模型,如果想兼容预览3r切出的gcode文件,还需在文件导入后进行相关预处理,不仅需要添加类似layer;这样的标识符便于分层解析,也需要将3r文件中跳转点的G1指令转化为与cura相同的G0指令。
  2. 本文的颜色信息的解析主要依赖于marlin固件提供的M165指令,此指令类似M165 A0.8733 B0.0188 C0.1079,其中ABC对应的是挤出机对应的挤料比例,本人使用的三喷头采用的是cmy三原色,即青色(Cyan)、品红(Magenta)和黄色(Yellow)。
  3. 使用vtk库同样可以完成stl模型的三维预览,预览形式如下图所示,代码实现在本文就不多赘述。
    在这里插入图片描述
  4. 由于现在主流的cura以及3r切片软件无法切出含有M165指令的gcode,因此需要进行相关的gcode后处理实现M165指令的插入,单色、分层以及渐变的gcode后处理方法会再写一篇博客展示。

PS:

第一次写博客,水平有限,有什么问题可以联系本尊,扣扣:408536802,谢谢!

  • 12
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Python是一种高级编程语言,具有简洁、易读、易学的特点。它广泛应用于各个领域,包括科学计算、机器学习、数据分析等。而PyQtPython的一个GUI,它是对Qt框架的封装,使得开发者可以使用Python语言来开发跨平台的图形界面应用程序。 而VTK(Visualization Toolkit)是一个用于可视化的开源软件系统,它提供了丰富的可视化算法和工具,可以用于生成、浏览和处理2D、3D图形。VTK是使用C++编写的,但也提供了Python的接口。通过使用PyQt,我们可以结合VTK的功能来创建交互式的3D可视化应用程序。 在使用这三个工具的过程中,Python提供了简洁而强大的编程语言特性,使得开发过程更加高效。PyQt提供了丰富的GUI组件和工具,可以轻松地创建用户友好的界面。而VTK则提供了丰富的可视化算法和工具,可以将数据转换为具有可视化效果的图形。 使用Python+PyQt+VTK的组合可以方便地开发各种可视化应用程序,例如医学图像处理、工程领域的模拟与分析、科学计算等。我们可以使用PyQt创建用户界面,然后通过VTK来呈现和处理图形数据。 总而言之,PythonPyQtVTK是一组强大的工具,它们的结合可以帮助我们快速开发高效、交互式的可视化应用程序。无论是进行科研工作还是进行工程应用,这个组合都能提供丰富的功能和便捷的开发体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值