From d4d1aa04dda17cd3d980c8a4031b9114ef4cf268 Mon Sep 17 00:00:00 2001 From: Nard Janssens Date: Sun, 2 Dec 2018 21:02:25 +0100 Subject: [PATCH] SVG-conversion, possible filedialogs, GPU slicing Added conversion of Slic3r multi svg-file to photon Added arguments to show dialogs for selecting input file and output file/directory Ported hackathon-slicer to python (GPU slicing) which can be used with argument -f False --- GL_Stl2Slices.py | 244 +++++++++++++++++ GL_Viewport.py | 698 +++++++++++++++++++++++++++++++++++++++++++++++ PhotonSlicer.py | 218 ++++++++++----- Stl2Slices.py | 27 +- Svg2Slices.py | 182 ++++++++++++ base.frag | 10 + base.vert | 13 + consetup.py | 46 +--- guisetup.py | 9 +- mesh.frag | 13 + mesh.vert | 13 + profiling.txt | 14 + quad.frag | 12 + quad.vert | 17 ++ slice.frag | 4 + slice.vert | 17 ++ 16 files changed, 1415 insertions(+), 122 deletions(-) create mode 100644 GL_Stl2Slices.py create mode 100644 GL_Viewport.py create mode 100644 Svg2Slices.py create mode 100644 base.frag create mode 100644 base.vert create mode 100644 mesh.frag create mode 100644 mesh.vert create mode 100644 quad.frag create mode 100644 quad.vert create mode 100644 slice.frag create mode 100644 slice.vert diff --git a/GL_Stl2Slices.py b/GL_Stl2Slices.py new file mode 100644 index 0000000..d452b97 --- /dev/null +++ b/GL_Stl2Slices.py @@ -0,0 +1,244 @@ +# Port of https:#github.com/Formlabs/hackathon-slicer/blob/master/app/js/slicer.js +# +# + +# internal +import struct +import math +import time +import os +import sys # for stdout + +# external +import cv2 +import numpy + +# user +import GL_Viewport +import rleEncode +from PhotonFile import * + + +class GL_Stl2Slices: + gui=False + viewport = None + + def clearModel(self): + self.points = [] + self.normals = [] + self.cmin = [] + self.cmax = [] + + def load_binary_stl(self,filename, scale=1): + print("Reading binary") + # filebytes = os.path.getsize(filename) + + #scale=scale*0.1 + fp = open(filename, 'rb') + + h = fp.read(80) + l = struct.unpack('I', fp.read(4))[0] + count = 0 + + t0 = time.time() + + self.clearModel() + points = [] + normals = [] + filepos = 0 + while True: + try: + p = fp.read(12) + if len(p) == 12: + n = struct.unpack('f', p[0:4])[0], struct.unpack('f', p[4:8])[0], struct.unpack('f', p[8:12])[0] + + p = fp.read(12) + if len(p) == 12: + p1 = struct.unpack('f', p[0:4])[0], struct.unpack('f', p[4:8])[0], struct.unpack('f', p[8:12])[0] + + p = fp.read(12) + if len(p) == 12: + p2 = struct.unpack('f', p[0:4])[0], struct.unpack('f', p[4:8])[0], struct.unpack('f', p[8:12])[0] + + p = fp.read(12) + if len(p) == 12: + p3 = struct.unpack('f', p[0:4])[0], struct.unpack('f', p[4:8])[0], struct.unpack('f', p[8:12])[0] + + if len(p) == 12: + # switch coordinates to OpenGL + a = 0 + b = 1 + c = 2 + n = [n[a], n[b], n[c]] + p1 = [p1[a], p1[b], p1[c]] + p2 = [p2[a], p2[b], p2[c]] + p3 = [p3[a], p3[b], p3[c]] + + # add points to array + points.append(p1) + points.append(p2) + points.append(p3) + normals.append(n) + + count += 1 + fp.read(2) + + # Check if we reached end of file + if len(p) == 0: + break + except EOFError: + break + fp.close() + + # t1=time.time() + # print ("t1-t0",t1-t0) + + # use numpy for easy and fast center and scale model + np_points = numpy.array(points) + np_normals = numpy.array(normals) + + # scale model, 1mm should be 1/0,047 pixels + #scale=scale/0.047 + np_points = np_points * scale + + # find max and min of x, y and z + x = np_points[:, 0] + y = np_points[:, 1] + z = np_points[:, 2] + self.cmin = (x.min(), y.min(), z.min()) + self.cmax = (x.max(), y.max(), z.max()) + self.modelheight = self.cmax[2] - self.cmin[2] + #print ("min: ",self.cmin) + #print ("max: ",self.cmax) + + # Center model and put on base + #trans = [0, 0, 0] + #trans[0] = -(self.cmax[0] - self.cmin[0]) / 2 - self.cmin[0] + #trans[1] = -(self.cmax[2] - self.cmin[2]) / 2 - self.cmin[2] + #trans[2] = -self.cmin[1] + + # We want the model centered in 2560x1440 + # 2560x1440 pixels equals 120x67 + #trans[0] = trans[0] +1440 / 2 + #trans[2] = trans[2] +2560 / 2 + + # Center numpy array of points which is returned for fast OGL model loading + #np_points = np_points + trans + + # Find bounding box again + x = np_points[:, 0] + y = np_points[:, 1] + z = np_points[:, 2] + self.cmin = (x.min(), y.min(), z.min()) + self.cmax = (x.max(), y.max(), z.max()) + + # align coordinates on grid + # this will reduce number of points and speed up loading + # with benchy grid-screenres/1: total time 28 sec, nr points remain 63k , but large artifacts + # with benchy grid-screenres/50: total time 39 sec, nr points remain 112k, no artifacts + # w/o benchy : total time 40 sec, nr points remain 113k, no artifacts + #screenres = 0.047 + #grid = screenres / 50 # we do not want artifacts but reduce rounding errors in the file to cause misconnected triangles + #np_points = grid * (np_points // grid) + + + # return points and normal for OGLEngine to display + return np_points, np_normals + + def __init__(self, stlfilename, scale=1, + outputpath=None, # should end with '/' + layerheight=0.05, + photonfilename=None, # keep outputpath=None if output to photonfilename + normalexposure=8.0, + bottomexposure=90, + bottomlayers=8, + offtime=6.5, + ): + self.viewport = GL_Viewport.Viewport() + + # Get path of script/exe for local resources like iconpath and newfile.photon + if getattr(sys, 'frozen', False):# frozen + self.installpath = os.path.dirname(sys.executable) + else: # unfrozen + self.installpath = os.path.dirname(os.path.realpath(__file__)) + + # Measure how long it takes + t1 = time.time() + + # Setup output path + if outputpath==None and photonfilename==None:return + + #create path if not exists + if not outputpath==None: + if not os.path.exists(outputpath): + os.makedirs(outputpath) + + # if we output to PhotonFile we need a place to store RunLengthEncoded images + if not photonfilename==None: + rlestack=[] + + # Load 3d Model in memory + points, normals = self.load_binary_stl(stlfilename, scale=scale) + + # Check if inside build area + size=(self.cmax[0]-self.cmin[0],self.cmax[1]-self.cmin[1],self.cmax[2]-self.cmin[2]) + if size[0]>65 or size[1]>115: + sizestr="("+str(int(size[0]))+"x"+str(int(size[2]))+")" + areastr="(65x115)" + errmsg="Model is too big "+sizestr+" for build area "+areastr+". Maybe try another orientation, use the scale argument (-s or --scale) or cut up the model." + if not self.gui: + print (errmsg) + else: + sys.tracebacklimit = None + raise Exception(errmsg) + sys.tracebacklimit = 0 + sys.exit() # quit() does not work if we make this an exe with cx_Freeze + + + # Load mesh + #print ("loading mesh") + self.viewport.loadMesh(points,normals,self.cmin,self.cmax); + #self.viewport.display() # this will loop until window is closed + self.viewport.draw() + + microns = layerheight*1000 #document.getElementById("height").value; + bounds = self.viewport.getBounds() + #print ((bounds['zmax']-bounds['zmin']) , self.viewport.printer.getGLscale()) + #quit() + zrange_mm=(bounds['zmax']-bounds['zmin']) / self.viewport.printer.getGLscale() + count=math.ceil(zrange_mm * 1000 / microns); + #print ("b",bounds) + #print ("z",zrange_mm) + #print ("m",microns) + #print ("c",count) + + if not photonfilename==None: + rlestack=[] + + for i in range(0,count): + data = self.viewport.getSliceAt(i / count) + img=data.reshape(2560,1440,4) + imgarr8=img[:,:,1] + if photonfilename==None: + Sstr = "%04d" % i + filename = outputpath+Sstr + ".png" + print (i,"/",count,filename) + cv2.imwrite(filename, imgarr8) + else: + img1D=imgarr8.flatten(0) + rlestack.append(rleEncode.encodedBitmap_Bytes_numpy1DBlock(img1D)) + + + if not photonfilename==None: + tempfilename=os.path.join(self.installpath,"newfile.photon") + photonfile=PhotonFile(tempfilename) + photonfile.readFile() + photonfile.Header["Layer height (mm)"]= PhotonFile.float_to_bytes(layerheight) + photonfile.Header["Exp. time (s)"] = PhotonFile.float_to_bytes(normalexposure) + photonfile.Header["Exp. bottom (s)"] = PhotonFile.float_to_bytes(bottomexposure) + photonfile.Header["# Bottom Layers"] = PhotonFile.int_to_bytes(bottomlayers) + photonfile.Header["Off time (s)"] = PhotonFile.float_to_bytes(offtime) + photonfile.replaceBitmaps(rlestack) + photonfile.writeFile(photonfilename) + + print("Elapsed: ", "%.2f" % (time.time() - t1), "secs") \ No newline at end of file diff --git a/GL_Viewport.py b/GL_Viewport.py new file mode 100644 index 0000000..75c8c9e --- /dev/null +++ b/GL_Viewport.py @@ -0,0 +1,698 @@ +# Port of https:#github.com/Formlabs/hackathon-slicer/blob/master/app/js/viewport.js +# +# + +#external +import OpenGL +from OpenGL.GL import * +from OpenGL.GLU import * +from OpenGL.GLUT import * +from OpenGL.GL.shaders import * +'''Import the PyOpenGL convenience wrappers for the FrameBufferObject +extension(s) we're going to use. (Requires PyOpenGL 3.0.1b2 or above).''' +from OpenGL.GL.framebufferobjects import * +import numpy +import math + +######################################## + +class Printer: + resolution = {"x": 1440, "y": 2560}; + width_mm = 1440*0.047;#25.4 * 3; + + def aspectRatio(self): + return self.resolution['x'] / self.resolution['y'] + + def pixels(self): + return self.resolution['x'] * self.resolution['y'] + + # Returns a scale ratio of OpenGL units per mm + def getGLscale(self): + return 2 * self.aspectRatio() / self.width_mm; + +class Mat4: + #https://open.gl/transformations + #http://headerphile.com/uncategorized/opengl-matrix-operations/ + @staticmethod + def Create(): + R4x4I=numpy.identity(4,dtype=numpy.float32) + R4x4=R4x4I.transpose() + R=R4x4.flatten() + return R + + @staticmethod + def Mul(M,N): + # M is 4x4 matrix as 1 dimensional 16 long vector row after row + # N is 4x4 matrix as 1 dimensional 16 long vector row after row + M4x4=M.reshape([4,4]) + N4x4=N.reshape([4,4]) + M4x4I=M4x4.transpose() + N4x4I=N4x4.transpose() + R4x4I=numpy.dot(M4x4I,N4x4I) + R4x4=R4x4I.transpose() + R=R4x4.flatten() + return R + + @staticmethod + def MulV4(M,V): + # M is 4x4 matrix as 1 dimensional 16 long vector row after row + # V is 1-dim vector of length 4 + M4x4=M.reshape([4,4]) + M4x4I=M4x4.transpose() + R4x4I=numpy.dot(M4x4I,V) + return R4x4I + + @staticmethod + def MulV3(M,V): + V4=numpy.append(V,1) + #print ("V4",V4) + return (Mat4.MulV4(M,V4)) + + @staticmethod + def MulV3s(M,Vs): + # M is 4x4 matrix as 1 dimensional 16 long vector row after row + # Vs is nx3 matrix of 1-dim vectors of length 3 + # points3n=self.mesh['verts'] + + nrVs=Vs.shape[0] + Vnx4=numpy.append(Vs,numpy.ones((nrVs,1)),axis=-1) + + Vnx4I=Vnx4.transpose() + M4x4=M.reshape([4,4]) + M4x4I=M4x4.transpose() + R4xnI=numpy.dot(M4x4I,Vnx4I) + R4xn=R4xnI.transpose() + R3xn=R4xn[:,:-1] + return (R3xn) + + @staticmethod + def Scale(M,V): + # M is 4x4 matrix as 1 dimensional 16 long vector row after row + # V should be 3 vector + M4x4=M.reshape(4,4) + M4x4I=M4x4.transpose() + V4=numpy.append(V,1) + R4x4I=M4x4I*V4 + R4x4=R4x4I.transpose() + R=R4x4.flatten() + return R + + @staticmethod + def Translate(M,V): + # M is 4x4 matrix as 1 dimensional 16 long vector row after row + # V should be 3 vector + N=M.copy() + x,y,z=V[0],V[1],V[2] + N[12] = M[0] * x + M[4] * y + M[8] * z + M[12] + N[13] = M[1] * x + M[5] * y + M[9] * z + M[13] + N[14] = M[2] * x + M[6] * y + M[10] * z + M[14] + N[15] = M[3] * x + M[7] * y + M[11] * z + M[15] + return N + + @staticmethod + def Rotate_old(M,theta,axis): + # This one does not to seem work correctly with axis=2 (Z) + # axis: 0: x, 1: y, 2: z + # theta in radians + s=math.sin(theta) + c=math.cos(theta) + if axis==0: + O=numpy.array([1,0,0,0,0,c,s,0,0,-s,c,0,0,0,0,1]) + elif axis==1: + O=numpy.array([c,0,-s,0,0,1,0,0,s,0,c,0,0,0,0,1]) + elif axis==2: + O=numpy.array([c,-s,0,0,s,c,0,0,0,0,1,0,0,0,0,1]) + else: + return + O4x4=O.reshape([4,4]) + O4x4I=O4x4.transpose() + + M4x4=M.reshape([4,4]) + M4x4I=M4x4.transpose() + + + R4x4I=numpy.dot(M4x4I,O4x4I) + R4x4=R4x4I.transpose() + R=R4x4.flatten() + return R + @staticmethod + def Rotate(M,theta,axis): + # See hackathon-slicer-master/node_modules/gl-matrix/src/gl-matrix/mat4.js + # axis: 0: x, 1: y, 2: z + # theta in radians + s=math.sin(theta) + c=math.cos(theta) + out=M.copy().astype(numpy.float32) + if axis==0: + a10 = M[4]; + a11 = M[5]; + a12 = M[6]; + a13 = M[7]; + a20 = M[8]; + a21 = M[9]; + a22 = M[10]; + a23 = M[11]; + out[4] = a10 * c + a20 * s; + out[5] = a11 * c + a21 * s; + out[6] = a12 * c + a22 * s; + out[7] = a13 * c + a23 * s; + out[8] = a20 * c - a10 * s; + out[9] = a21 * c - a11 * s; + out[10] = a22 * c - a12 * s; + out[11] = a23 * c - a13 * s; + return out + elif axis==1: + a00 = M[0]; + a01 = M[1]; + a02 = M[2]; + a03 = M[3]; + a20 = M[8]; + a21 = M[9]; + a22 = M[10]; + a23 = M[11]; + out[0] = a00 * c - a20 * s; + out[1] = a01 * c - a21 * s; + out[2] = a02 * c - a22 * s; + out[3] = a03 * c - a23 * s; + out[8] = a00 * s + a20 * c; + out[9] = a01 * s + a21 * c; + out[10] = a02 * s + a22 * c; + out[11] = a03 * s + a23 * c; + return out; + elif axis==2: + a00 = M[0] + a01 = M[1] + a02 = M[2] + a03 = M[3] + a10 = M[4] + a11 = M[5] + a12 = M[6] + a13 = M[7] + out[0] = a00 * c + a10 * s + out[1] = a01 * c + a11 * s + out[2] = a02 * c + a12 * s + out[3] = a03 * c + a13 * s + out[4] = a10 * c - a00 * s + out[5] = a11 * c - a01 * s + out[6] = a12 * c - a02 * s + out[7] = a13 * c - a03 * s + return out + + @staticmethod + def RotateX(M,theta): + return Mat4.Rotate(M,theta,0) + @staticmethod + def RotateY(M,theta): + return Mat4.Rotate(M,theta,1) + @staticmethod + def RotateZ(M,theta): + return Mat4.Rotate(M,theta,2) + + @staticmethod + def test(): + M=numpy.array([0,1,2,3, 4,5,6,7, 8,9,10,11, 12,13,14,15]) + V=numpy.array([1,2,3]) + V4=numpy.array([4,5,6,1]) + VW=numpy.array([[1,2,3],[4,5,6]]) + N=numpy.array([0,10,20,30, 40,50,60,70, 80,90,100,110, 120,130,140,150]) + print("Mat4.Create",Mat4.Create()) + print("Mat4.Translate",Mat4.Translate(M,V)) + print("Mat4.Scale",Mat4.Scale(M,V)) + print("Mat4.MulV3",Mat4.MulV3(M,V)) + print("Mat4.MulV4",Mat4.MulV4(M,V4)) + print("VW",VW) + print("Mat4.MulV3s",Mat4.MulV3s(M,VW)) + print("Mat4.RotateX",Mat4.RotateX(M,10)) + print("Mat4.RotateY",Mat4.RotateY(M,10)) + print("Mat4.RotateZ",Mat4.RotateZ(M,10)) + quit() +#Mat4.test() + +class Viewport: + printer=None + quad = None + base = None + slice=None + # Model object + mesh = {"loaded": False} + window=None + windowsize=(500,500)#(256*3,144*3) + + def __init__(self): + # init printer specs + self.printer=Printer() + + glutInit(sys.argv) + #glutInitContextVersion( 3, 2 ) + + # Create a double-buffer RGBA window. (Single-buffering is possible. + # So is creating an index-mode window.) + glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH) + + # Create a window, setting its title + # https://noobtuts.com/python/opengl-introduction + glutInitWindowSize(self.windowsize[0],self.windowsize[1]) + glutInitWindowPosition(0,0) + self.window=glutCreateWindow('python port of hackathon-slicer') + + # Set the display callback. You can set other callbacks for keyboard and + # mouse events. + glutDisplayFunc(self.draw) + #glutIdleFunc(self.draw) + + # Some things that where global in npm javascript version + self.quad=self.makeQuad() + self.base=self.makeBase() + self.scene={"roll": 45+math.pi/2, "pitch": 45} + self.slice=self.makeSlice() + glEnable(GL_DEPTH_TEST) + self.draw() + + + def display(self): + glutMainLoop() + return + + + def buildShader(self,txt, type): + #print ("buildShader",txt,type) + s = glCreateShader(type) + glShaderSource(s, txt) + glCompileShader(s) + + #if (not glGetShaderParameter(s, GL_COMPILE_STATUS)): + if (not glGetShaderiv(s, GL_COMPILE_STATUS)): + raise RuntimeError ("Could not compile shader:" , glGetShaderInfoLog(s)) + return s + + def setUniforms(self,prog,modelvar,names): + modelvar['uniform'] = {} + for u in names: + modelvar['uniform'][u] = glGetUniformLocation(prog, u) + return modelvar + + def setAttribs(self,prog, modelvar,names): + modelvar['attrib'] = {} + for attrib in names: + modelvar['attrib'][attrib]=glGetAttribLocation(prog,attrib) + return modelvar + + def makeProgram(self,modelvar,vert, frag, uniforms, attribs): + v = self.buildShader(vert, GL_VERTEX_SHADER) + f = self.buildShader(frag, GL_FRAGMENT_SHADER) + + prog = glCreateProgram() + glAttachShader(prog, v) + glAttachShader(prog, f) + glLinkProgram(prog) + + #if (not glGetProgramParameter(prog, GL_LINK_STATUS)): + if (not glGetProgramiv(prog, GL_LINK_STATUS)): + raise RuntimeError("Could not link program:" + glGetProgramInfoLog(prog)) + + self.setUniforms(prog, modelvar, uniforms) + self.setAttribs(prog, modelvar, attribs) + + return prog + + + def viewMatrix(self): + # We assume there is no rotation, because this port has no userinterface + v = Mat4.Create() + v = Mat4.Scale(v,numpy.array([1, 1, 0.5])) + v = Mat4.RotateX(v, self.scene['pitch']) + v = Mat4.RotateZ(v, self.scene['roll']) + v = Mat4.Scale(v, numpy.array([1, 1, -1])) + #v = Mat4.Scale(v, numpy.array([0.5, 0.5, -0.5])) + return v + + + def modelMatrix(self): + # We assume there is no rotation, because this port has no userinterface + m = Mat4.Create() #Creates a new identity mat4 + m = Mat4.RotateZ(m, self.mesh['roll']) + m = Mat4.RotateX(m, self.mesh['pitch']) + m = Mat4.RotateY(m, self.mesh['yaw']) + + out = Mat4.Create() + out = Mat4.Mul(m, self.mesh['M']) + return out + + + def drawMesh(self,mesh): + glUseProgram(self.mesh['prog']) + + glUniformMatrix4fv(self.mesh['uniform']['view'], 1, False, self.viewMatrix()) + glUniformMatrix4fv(self.mesh['uniform']['model'], 1, False, self.modelMatrix()) + + glBindBuffer(GL_ARRAY_BUFFER, self.mesh['vert']) + glEnableVertexAttribArray(self.mesh['attrib']['v']) + glVertexAttribPointer(self.mesh['attrib']['v'], 3, GL_FLOAT, False, 0, None) + + glBindBuffer(GL_ARRAY_BUFFER, self.mesh['norm']) + glEnableVertexAttribArray(self.mesh['attrib']['n']) + glVertexAttribPointer(mesh['attrib']['n'], 3, GL_FLOAT, False, 0, None) + + glDrawArrays(GL_TRIANGLES, 0, self.mesh['triangles']) + #print ("drawMesh") + + def drawBase(self,base): + glEnable(GL_CULL_FACE) + glCullFace(GL_FRONT) + glUseProgram(self.base['prog']) + glUniformMatrix4fv(self.base['uniform']['view'], 1, False, self.viewMatrix()) + if (self.mesh['loaded']): + glUniform1f(self.base['uniform']['zmin'], self.mesh['bounds']['zmin']) + else: + glUniform1f(self.base['uniform']['zmin'], 0) + glUniform1f(self.base['uniform']['aspect'], self.printer.aspectRatio()) + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glBindBuffer(GL_ARRAY_BUFFER, self.base['vert']) + glEnableVertexAttribArray(self.base['attrib']['v']) + glVertexAttribPointer(self.base['attrib']['v'], 2, GL_FLOAT, False, 0, None) + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) + glBindTexture(GL_TEXTURE_2D, 0) + glDisable(GL_CULL_FACE) + + + def drawQuad(self,quad): + # Draws slice + glUseProgram(quad['prog']) + + glDisable(GL_DEPTH_TEST) + glUniformMatrix4fv(self.quad['uniform']['view'], 1, False, self.viewMatrix()) + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glActiveTexture(GL_TEXTURE0) + glBindTexture(GL_TEXTURE_2D,self.slice['tex']) + glUniform1i(self.quad['uniform']['tex'], 0) + + glBindBuffer(GL_ARRAY_BUFFER, self.quad['vert']) + glEnableVertexAttribArray(self.quad['attrib']['v']) + glVertexAttribPointer(self.quad['attrib']['v'], 2, GL_FLOAT, False, 0, None) + + glUniform1f(self.quad['uniform']['frac'], self.quad['frac']) + glUniform1f(self.quad['uniform']['aspect'], self.printer.aspectRatio()) + glUniform2f(self.quad['uniform']['bounds'], self.mesh['bounds']['zmin'], self.mesh['bounds']['zmax']) + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) + glBindTexture(GL_TEXTURE_2D, 0) + glEnable(GL_DEPTH_TEST) + + + def draw(self): + glClearColor(1, 1, 1, 1) + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # clear the screen + + self.drawBase(self.base) + + if (self.mesh['loaded']): + self.drawMesh(self.mesh) + self.drawQuad(self.quad) + pass + glutSwapBuffers() + + + def makeQuad(self): + quad = {} + quad['prog'] = self.makeProgram( + quad, + open('quad.vert','r').read(), + open('quad.frag','r').read(), + ['view','tex','frac','aspect','bounds'], ['v']) + + quad['vert'] = glGenBuffers(1) + glBindBuffer(GL_ARRAY_BUFFER, quad['vert']) + glBufferData( + GL_ARRAY_BUFFER, + numpy.array([-1, -1, + -1, 1, + 1, -1, + 1, 1],dtype=numpy.float32), + GL_STATIC_DRAW) + + quad['frac'] = 0.5 + return quad + + + def makeBase(self): + base = {} + base['prog'] = self.makeProgram( + base, + open('base.vert').read(), + open('base.frag').read(), + ['view', 'zmin', 'aspect'], ['v']) + + base['vert'] = glGenBuffers(1) #createBuffer() + glBindBuffer(GL_ARRAY_BUFFER, base['vert']) + glBufferData( + GL_ARRAY_BUFFER, + numpy.array([-1, -1, + -1, 1, + 1, -1, + 1, 1],dtype=numpy.float32), + GL_STATIC_DRAW) + + base['frac'] = 0.5 + return base + + + def makeSlice(self): + # all these create methods are WebGL specific + slice ={"fbo": glGenFramebuffers(1), + "tex": glGenTextures(1), + "buf": glGenRenderbuffers(1)} + + slice['prog'] = self.makeProgram( + slice, + open('slice.vert','r').read(), + open('slice.frag','r').read(), + ['model','bounds','frac','aspect'], ['v']) + + glBindTexture(GL_TEXTURE_2D, slice['tex']) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, + self.printer.resolution['x'],self.printer.resolution['y'], + 0, GL_RGBA, GL_UNSIGNED_BYTE, None) #null + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + + glBindTexture(GL_TEXTURE_2D, 0)#null + return slice + + + def getMeshBounds(self): + M = self.modelMatrix() + + points=self.mesh['verts'] + out=Mat4.MulV3s(M,points) + x = out[:, 0] + y = out[:, 1] + z = out[:, 2] + cmin = (x.min(), y.min(), z.min()) + cmax = (x.max(), y.max(), z.max()) + + self.mesh['bounds'] = {} + self.mesh['bounds']['xmin'] = cmin[0] + self.mesh['bounds']['xmax'] = cmax[0] + + self.mesh['bounds']['ymin'] = cmin[1] + self.mesh['bounds']['ymax'] = cmax[1] + + self.mesh['bounds']['zmin'] = cmin[2] + self.mesh['bounds']['zmax'] = cmax[2] + + + def updateScale(self): + # Create identity transform matrix + self.mesh['M'] = Mat4.Create() + + # Find bounds and center, then store them in matrix M + self.getMeshBounds() + + scale = self.printer.getGLscale() + + # Store mesh transform matrix + self.mesh['M'] = Mat4.Create() + self.mesh['M'] = Mat4.Scale(self.mesh['M'], numpy.array([scale, scale, scale])) + + Mat4.Translate(self.mesh['M'], numpy.array([ + -(self.mesh['bounds']['xmin'] + self.mesh['bounds']['xmax']) / 2, + -(self.mesh['bounds']['ymin'] + self.mesh['bounds']['ymax']) / 2, + -(self.mesh['bounds']['zmin'] + self.mesh['bounds']['zmax']) / 2])) + + #print ("mesh['M']",self.mesh["M"]) + # Recalculate mesh bounds with the transform matrix + self.getMeshBounds() + + + def loadMesh(self,points,normals,cmin,cmax): + # https://www.opengl.org/discussion_boards/showthread.php/183305-How-to-use-glDrawArrays%28%29-with-VBO-Vertex-Buffer-Object-to-display-stl-geometry + # points = [ [x,y,z], [a,b,c],....] + + # Store model min and max bounds + self.mesh['bounds']={} + #print ("cmin",cmin) + #print ("cmax",cmax) + self.mesh['bounds']['xmin_orig']=cmin[0] + self.mesh['bounds']['ymin_orig']=cmin[1] + self.mesh['bounds']['zmin_orig']=cmin[2] + self.mesh['bounds']['xmax_orig']=cmax[0] + self.mesh['bounds']['ymax_orig']=cmax[1] + self.mesh['bounds']['zmax_orig']=cmax[2] + + + # Reset pitch and roll + self.mesh['roll'] = 0 + self.mesh['pitch'] = 0 + self.mesh['yaw'] = 0 + # Compile shader program for mesh + self.mesh['prog'] = self.makeProgram( + self.mesh, + open('mesh.vert','r').read(), + open('mesh.frag','r').read(), + ['view', 'model'], ['v', 'n']) + + # Store unique vertices + self.mesh['verts'] = points + + # Store mesh's convex hull (as indices into vertex list) + # only used to calc mesh bounds + + # Work out mesh scale + self.updateScale() + + # Load vertex positions into a buffer + #oints zijn nog niet goed... + points_flattened = numpy.array(points.flatten(),dtype=numpy.float32)#_.flatten(stl.positions); + self.mesh['vert'] = glGenBuffers(1)#gl.createBuffer(); + glBindBuffer(GL_ARRAY_BUFFER, self.mesh['vert']) + glBufferData( + GL_ARRAY_BUFFER, + points_flattened.nbytes, + points_flattened, + GL_STATIC_DRAW) + + # Load normals into a second buffer + # normals has a vector for each tri and not for each point! + normalsPerPoint=numpy.repeat(normals,3,0) + normals_flattened = numpy.array(normalsPerPoint.flatten(),dtype=numpy.float32) + self.mesh['norm'] = glGenBuffers(1); + glBindBuffer(GL_ARRAY_BUFFER, self.mesh['norm']); + glBufferData( + GL_ARRAY_BUFFER, + normals_flattened.nbytes, + numpy.array(normals_flattened,dtype=numpy.float32), + GL_STATIC_DRAW) + + # Store the number of triangles + self.mesh['triangles'] = len(points)#stl.positions.length; + + # Get bounds with new transform matrix applied + self.mesh['loaded'] = True + + #print ("loaded mesh") + self.renderSlice() + + + def renderSlice(self): + glDisable(GL_DEPTH_TEST) + glEnable(GL_STENCIL_TEST) + glViewport(0, 0, self.printer.resolution['x'],self.printer.resolution['y'])#printer.resolution.x, printer.resolution.y) + + # Bind the target framebuffer + sliceFbo = self.slice['fbo'] #glGenFramebuffers(1) + glBindFramebuffer(GL_FRAMEBUFFER, sliceFbo) + #glBindBuffer(GL_ARRAY_BUFFER, self.modelPtBufferIdx) + + # Attach our output texture + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, self.slice['tex'], 0) + + # Bind the renderbuffer to get a stencil buffer + glBindRenderbuffer(GL_RENDERBUFFER, self.slice['buf']) + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_STENCIL, + self.printer.resolution['x'],self.printer.resolution['y']) + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, + GL_RENDERBUFFER, self.slice['buf']) + + # Clear texture + glClearColor(0, 0, 0, 0) + glClearStencil(0) + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) + + glUseProgram(self.slice['prog']) + + # Load model matrix + glUniformMatrix4fv(self.slice['uniform']['model'],1, False, self.modelMatrix()) + + # Load slice position and mesh bounds + glUniform1f(self.slice['uniform']['frac'], + self.quad['frac']) + glUniform1f(self.slice['uniform']['aspect'], + self.printer.aspectRatio()) + glUniform2f(self.slice['uniform']['bounds'], + self.mesh['bounds']['zmin'], + self.mesh['bounds']['zmax']) + + # Load mesh vertices + glBindBuffer(GL_ARRAY_BUFFER, self.mesh['vert']) + glEnableVertexAttribArray(self.mesh['attrib']['v']) + glVertexAttribPointer(self.mesh['attrib']['v'], 3, GL_FLOAT, False, 0, None) + + # Draw twice, adding and subtracting values in the stencil buffer + # based on the handedness of faces that we encounter + glStencilFunc(GL_ALWAYS, 0, 0xFF) + glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_INCR) + glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_KEEP) + glDrawArrays(GL_TRIANGLES, 0, self.mesh['triangles']) + + glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_KEEP) + glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_DECR) + glDrawArrays(GL_TRIANGLES, 0, self.mesh['triangles']) + + # Clear the color bit in preparation for a redraw + glClear(GL_COLOR_BUFFER_BIT) + + # Draw again, discarding samples if the stencil buffer != 0 + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP, GL_KEEP) + glStencilFunc(GL_NOTEQUAL, 0, 0xFF) + glDrawArrays(GL_TRIANGLES, 0, self.mesh['triangles']) + + # Load the data from the framebuffer + data=numpy.empty([self.printer.pixels()*4],dtype=numpy.uint8) #data = new Uint8Array(printer.pixels() * 4) + glReadPixels(0, 0, + self.printer.resolution['x'], self.printer.resolution['y'], + GL_RGBA, + GL_UNSIGNED_BYTE, data) + + # Restore the default framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, 0) + glEnable(GL_DEPTH_TEST) + glDisable(GL_STENCIL_TEST) + + glViewport(0, 0, self.windowsize[0],self.windowsize[1]) + + return data + + def getSliceAt(self,frac): + self.quad['frac'] = frac + #print ("getSliceAt:", frac) + #document.getElementById("slider").valueAsNumber = frac * 100 + self.draw() + return self.renderSlice() + + def getBounds(self): + return self.mesh['bounds'] + + def hasModel(self): + return self.mesh['loaded'] diff --git a/PhotonSlicer.py b/PhotonSlicer.py index a7f49a7..e73794f 100644 --- a/PhotonSlicer.py +++ b/PhotonSlicer.py @@ -1,32 +1,23 @@ +# Todo/Bugs +# if force GPU is default....why than console mode? +# keep opengl center in viewport.updateScale? + """ Needed (external) packages by other modules cython numpy opencv-python + PyOpenGL + PyOpenGL-accelerate -Usage -1) PhotonSlicer bunny.stl photon 0.05 -Slices ./bunny.stl to ./bunny.photon with sliceheight 0.05 - -2) PhotonSlicer STLs/bunny.stl hare.photon 0.05 - Slices ./STLs/bunny.stl to ./hare.photon with sliceheight 0.05 - PhotonSlicer STLs/bunny.stl subdir/hare.photon 0.05 - Slices ./STLs/bunny.stl to ./subdir/hare.photon with sliceheight 0.05 - PhotonSlicer STLs/bunny.stl /subdir/hare.photon 0.05 - Slices ./STLs/bunny.stl to /subdir/hare.photon with sliceheight 0.05 - -3) PhotonSlicer bunny.stl ./animals/photon 0.05 -Slices ./bunny.stl to ./animals/bunny.photon with sliceheight 0.05 - -4) PhotonSlicer bunny.stl images 0.05 -Slices ./bunny.stl to ./bunny/0001.png , ./bunny/0002.png , ... with sliceheight 0.05 - -5) PhotonSlicer bunny.stl userdir/ 0.05 -Slices ./bunny.stl to ./userdir/001.png , ./userdir/0002.png , ... with sliceheight 0.05 - -For 1,2,3 you can add optional arguments for the resin exposure type etc. -For 5 only last directory level may be new +Usage: use --help argument + + safe mode - command line only, CPU slicing, sliceheight 0.05mm: + PhotonSlicer.py -s STLs/3dBenchy.stl + gui mode - dialog to select stl file and photon file, GPU slicing, sliceheight 0.05mm: + PhotonSlicer.py -s dialog -p dialog -f False -g True + """ # import the necessary packages @@ -37,17 +28,15 @@ import re # needed for case insentive replace from Stl2Slices import * +from Svg2Slices import * +from GL_Stl2Slices import * -stlfilename=None +filename=None outputpath=None outputfile=None gui=False # If cx_Freeze, check if we are in console or gui model -if sys.stdout==None: - gui=True -else: - gui=False """ try: gui=True @@ -56,7 +45,7 @@ except IOError: gui=False """ -def is_bool(arg): +def is_bool_gui(arg): global gui if arg.lower() in ('yes', 'true', 't', 'y', '1'): gui = True @@ -67,24 +56,83 @@ def is_bool(arg): else: raise argparse.ArgumentTypeError('boolean value expected.') +def is_bool(arg): + if arg.lower() in ('yes', 'true', 't', 'y', '1'): + return True + elif arg.lower() in ('no', 'false', 'f', 'n', '0'): + return False + else: + raise argparse.ArgumentTypeError('boolean value expected.') + def is_valid_file(arg): - global stlfilename + global filename + global gui + global args + if arg=="dialog": + if gui: + import tkinter + from tkinter.filedialog import askopenfilename + root=tkinter.Tk() # define root (which is opened by askopenfilename anyway) so we can destroy it afterwards + root.withdraw() # hide root + filename=askopenfilename(initialdir = ".",title = "Open file",filetypes = (("stl files","*.stl"),("svg files","*.svg"))) + root.destroy() # destroy root + if not (filename): + print ("Abort, no file selected.") + sys.exit() + args["filename"]=filename + return filename + else: + raise argparse.ArgumentTypeError('filedialog only available in GUI mode.') + arg=os.path.normpath(arg) # convert all / to \ for windows and vv for linux if not os.path.isfile(arg): - raise argparse.ArgumentTypeError("stlfilename argument ('"+arg+"') does not point to valid STL file") - elif not arg[-4:].lower()==".stl": - raise argparse.ArgumentTypeError("stlfilename argument ('"+arg+"') does not point to valid STL file") + raise argparse.ArgumentTypeError("filename argument ('"+arg+"') does not point to valid STL/SVG file") + elif not (arg[-4:].lower()==".stl" or arg[-4:].lower()==".svg"): + raise argparse.ArgumentTypeError("filename argument ('"+arg+"') does not point to valid STL/SVG file") else: - stlfilename = arg + filename = arg return arg def is_valid_output(arg): - global stlfilename + global filename global outputpath global outputfile + if arg=="dialog": + if gui: + import tkinter + from tkinter.filedialog import asksaveasfilename + root=tkinter.Tk() # define root (which is opened by askopenfilename anyway) so we can destroy it afterwards + root.withdraw() # hide root + outputfile=asksaveasfilename(initialdir = ".",title = "Save to file",filetypes = (("photon files","*.photon"),)) + root.destroy() # destroy root + if not (outputfile): + print ("Abort, no file selected.") + sys.exit() + args["photonfilename"]=outputfile + return outputfile + else: + raise argparse.ArgumentTypeError('filedialog only available in GUI mode.') + + if arg=="dialogdir": + if gui: + import tkinter + from tkinter.filedialog import askdirectory + root=tkinter.Tk() # define root (which is opened by askopenfilename anyway) so we can destroy it afterwards + root.withdraw() # hide root + outputpath=askdirectory(initialdir = ".",title = "Save to directory") + root.destroy() # destroy root + if not (outputpath): + print ("Abort, no file selected.") + sys.exit() + outputpath=outputpath+os.path.sep + args["photonfilename"]=outputpath + return outputpath + else: + raise argparse.ArgumentTypeError('filedialog only available in GUI mode.') + if arg=="photon": #1 output to same dir and use same name but end with .photon - # stlfilename is checked to end with '.stl' so replace last 4 with '.photon' - outputfile=stlfilename[:-4]+'.photon' + # filename is checked to end with '.stl' so replace last 4 with '.photon' + outputfile=filename[:-4]+'.photon' return outputfile elif arg.endswith(".photon"): #2 output to current working dir but use same name but end with .photon arg=os.path.normpath(arg) # make sure the slashes are correct for os @@ -105,9 +153,9 @@ def is_valid_output(arg): # if not starts with slash we have relative path so we append current path if not arg.startswith('/') and not arg.startswith('\\'): arg=os.path.join(os.getcwd(),arg) - # stlfilename is checked to end with '.stl' so remove last 6 to get new dir - bare_stlfilename=os.path.basename(stlfilename)[:-4] - outputfile=os.path.join(arg[:-6],bare_stlfilename+".photon") + # filename is checked to end with '.stl' so remove last 6 to get new dir + bare_filename=os.path.basename(filename)[:-4] + outputfile=os.path.join(arg[:-6],bare_filename+".photon") #check if parent directory exists pardir=os.path.dirname(arg) if os.path.isdir(pardir): @@ -116,8 +164,8 @@ def is_valid_output(arg): raise argparse.ArgumentTypeError("photonfilename path does not exist") return outputfile elif arg=="images": #4 output to same dir under new subdir with name of stl - # stlfilename is checked to end with '.stl' - outputpath=stlfilename[:-4]+os.path.sep + # filename is checked to end with '.stl' + outputpath=filename[:-4]+os.path.sep return outputpath elif arg.endswith("/") or arg.endswith("\\") : #5 output to user defined path # make sure the slashes are correct for os @@ -139,10 +187,10 @@ def is_valid_output(arg): # if not starts with slash we have relative path so we append current path if not arg.startswith('/') and not arg.startswith('\\'): arg=os.path.join(os.getcwd(),arg) - # stlfilename is checked to end with '.stl' - bare_stlfilename=os.path.basename(stlfilename)[:-4] + # filename is checked to end with '.stl' + bare_filename=os.path.basename(filename)[:-4] # make new path - outputpath=os.path.join(arg[:-6],bare_stlfilename+os.path.sep) + outputpath=os.path.join(arg[:-6],bare_filename+os.path.sep) #check if parent directory exists pardir=os.path.dirname(outputpath) #just removes last '/' pardir=os.path.dirname(pardir) @@ -187,6 +235,7 @@ def _print_message(self,message,stderr): "description: Slices a STL (binary file) to images or a photon file.\n" "\n"+ "examples: PhotonSlicer.cmd -s ./STLs/Cube.stl -> ./STLs/Cube.photon\n" + " PhotonSlicer.cmd -s ./STLs/Cube.svg -> ./STLs/Cube.photon\n" " PhotonSlicer.cmd -s ./STLs/Cube.stl -p photon -l 0.05 -> ./STLs/Cube.photon\n" " PhotonSlicer.cmd -s ./STLs/Cube.stl -p /home/photon -l 0.05 -> /home/Cube.photon\n" " PhotonSlicer.cmd -s ./STLs/Cube.stl -p /Sqrs.photon -l 0.05 -> /Sqrs.photon\n" @@ -194,13 +243,16 @@ def _print_message(self,message,stderr): " PhotonSlicer.cmd -s ./STLs/Cube.stl -p ./sliced/ -l 0.05 -> ./sliced/0001.png,..\n" ,formatter_class=argparse.RawTextHelpFormatter) -ap.add_argument("-s","--stlfilename", +ap.add_argument("-s","--filename", required=True, - help="name of (binary) stl file to import") + help="name of (binary) stl or svg file to import\n"+ + "'dialog' for dialog to select stl file (only in GUI mode) OR\n") ap.add_argument("-p","--photonfilename", #type=str, help="photon file name (ends with '.photon') OR \n"+ "output directory (ends with '/') for images OR \n"+ + "'dialog' to select photon file (only in GUI mode) OR\n"+ + "'dialogdir' to select dir to save images (only in GUI mode) OR\n"+ "'photon' as argument to generate photon file with same name OR \n"+ "'images' to generate images in directory with same name as stl\n"+ "these can be combined e.g. './subdir/photon'") @@ -224,8 +276,11 @@ def _print_message(self,message,stderr): default=6.5,type=float, help="off time between layers (sec)") ap.add_argument("-g", "--gui", required=False, - default=6.5,type=is_bool, + default=True,type=is_bool_gui, help="show progress in popup window") +ap.add_argument("-f", "--forceCPU", required=False, + default=True,type=is_bool, + help="force slicing with CPU instead of GPU/OpenGL") ap.add_argument("-e", "--execute", required=False, help="execute command when done \n"+ "'photon' will be replace with output filename \n"+ @@ -233,10 +288,10 @@ def _print_message(self,message,stderr): args = vars(ap.parse_args()) -# Check photonfilename is valid only now (that we have stlfilename) -sf=(args["stlfilename"]) -type = is_valid_file(sf) -#print ("sf",sf, stlfilename) +# Check photonfilename is valid only now (that we have filename) +sf=(args["filename"]) +is_valid_file(sf) +filetype = args["filename"][-4:].lower() pf=(args["photonfilename"]) if pf==None: pf="photon" @@ -255,24 +310,51 @@ def _print_message(self,message,stderr): bottomlayers = int(args["bottomlayers"]) offtime = float(args["offtime"]) linkedcmd = args["execute"] +forceCPU = args["forceCPU"] -S2I=Stl2Slices(stlfilename=stlfilename, - outputpath=outputpath, - photonfilename=outputfile, - layerheight=layerheight, - scale=scale, - normalexposure=normalexposure, - bottomexposure=bottomexposure, - bottomlayers=bottomlayers, - offtime=offtime, - gui=gui - ) +# Some arguments do not work together +if not forceCPU and not gui: #default is false, so user explicitly set it to True + print ("You cannot use opengl without gui.") + sys.exit() + +if filetype == ".svg": + S2I=Svg2Slices(svgfilename=filename, + outputpath=outputpath, + photonfilename=outputfile, + layerheight=layerheight, + scale=scale, + normalexposure=normalexposure, + bottomexposure=bottomexposure, + bottomlayers=bottomlayers, + offtime=offtime, + gui=gui + ) +elif filetype == ".stl": + if forceCPU: + S2I=Stl2Slices(stlfilename=filename, + outputpath=outputpath, + photonfilename=outputfile, + layerheight=layerheight, + scale=scale, + normalexposure=normalexposure, + bottomexposure=bottomexposure, + bottomlayers=bottomlayers, + offtime=offtime, + gui=gui + ) + + else: #use GPU/OpenGL + S2I=GL_Stl2Slices(stlfilename=filename, + outputpath=outputpath, + photonfilename=outputfile, + layerheight=layerheight, + scale=scale, + normalexposure=normalexposure, + bottomexposure=bottomexposure, + bottomlayers=bottomlayers, + offtime=offtime + ) -""" -Test on linux using: -python3 PhotonSlicer.py -s STLs/bunny.stl -r 0.5 -l 0.1 - -e "wine ~/.wine/drive_c/Program\ Files/ANYCUBIC\ Photon\ Slicer64/ANYCUBIC\ Photon\ Slicer.exe /home/nard/PhotonSlicer/photon" -""" import subprocess import platform import os @@ -283,7 +365,7 @@ def open_folder(path): elif platform.system() == 'Linux': subprocess.Popen(['xdg-open', path]) #os.startfile(path) - elif platform.system() == 'Windows': + else: #platform.system() == 'Windows': os.startfile(path) #os.startfile(path) diff --git a/Stl2Slices.py b/Stl2Slices.py index 2ced3f6..aae7e14 100644 --- a/Stl2Slices.py +++ b/Stl2Slices.py @@ -387,7 +387,7 @@ def __init__(self, stlfilename, scale=1, """ # Traverse all triangles - for pidx in slicepointindices[sliceNr]: + for pidx in slicepointindices[sliceNr]: p0 = points[pidx + 0] p1 = points[pidx + 1] p2 = points[pidx + 2] @@ -396,14 +396,14 @@ def __init__(self, stlfilename, scale=1, polypoints = triInSlice.triInSlice(p0, p1, p2, sliceBottom, sliceTop) # Draw filled poly, fillConvexPoly is much faster than fillPoly, but poly should be convex... if polypoints: - cv2.fillConvexPoly(img, numpy.array([polypoints], dtype='int32'), color=contourColor) - + # if we do fill on all lines, do we need fillConvexPoly? + cv2.fillConvexPoly(img, numpy.array([polypoints], dtype='int32'), color=contourColor) # Add points for which to floodfillpoints using normal but only if normal not along y if not (n2d[0] == 0 and n2d[1] == 0): nrpoints = len(polypoints) for idx in range(nrpoints): - pfrom = polypoints[idx % nrpoints] - pto = polypoints[(idx + 1) % nrpoints] + pfrom = polypoints[idx] + pto = polypoints[(idx + 1) % nrpoints] pmid = ((pfrom[0] + pto[0]) / 2, (pfrom[1] + pto[1]) / 2) pfill = (int((pmid[0] - n2d[0])), int((pmid[1] - n2d[1]))) # Check if point inside triangle(s) - https://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle @@ -436,7 +436,7 @@ def __init__(self, stlfilename, scale=1, nors.append(n2d) ps.append([p0,p1,p2]) """ - + # Floodfill all points tester = img.copy() nrTests = 0 @@ -452,12 +452,25 @@ def __init__(self, stlfilename, scale=1, #quit() #for idx,fillPoint in enumerate(fillpoints): + """ + for y in range(0,2560): + oldcol=0 + inShape=False + for x in range (0,1440): + col=1#img[y,x] + if oldcol!=col: + inShape!=inShape + col=oldcol + if inShape: + img[y,x]=innerColor + """ + for fillPoint in fillpoints: # Check if fill is necessary at fillpoint (if fillpoint still has background color = 0,0,0)) and not fill color (=innerColor) pxColor = (img[fillPoint[1], fillPoint[0], 0]#, #img[fillPoint[1], fillPoint[0], 1], #img[fillPoint[1], fillPoint[0], 2] - ) + ) #if pxColor == (0, 0, 0): if pxColor == (0): # Do a testfill on tester diff --git a/Svg2Slices.py b/Svg2Slices.py new file mode 100644 index 0000000..fcba2b4 --- /dev/null +++ b/Svg2Slices.py @@ -0,0 +1,182 @@ +import time +import os +import sys # for stdout + +from xml.dom import minidom +import cv2 + +from PhotonFile import * +import rleEncode + + +class Svg2Slices: + cmin = [100, 100] + cmax = [-100, -100] + modelheight = 0 + + def __init__(self, svgfilename, scale=1, + outputpath=None, # should end with '/' + layerheight=0.05, + photonfilename=None, # keep outputpath=None if output to photonfilename + normalexposure=8.0, + bottomexposure=90, + bottomlayers=8, + offtime=6.5, + gui=False + ): + + # Set Gui + self.gui=gui + + # Get path of script/exe for local resources like iconpath and newfile.photon + if getattr(sys, 'frozen', False):# frozen + self.installpath = os.path.dirname(sys.executable) + else: # unfrozen + self.installpath = os.path.dirname(os.path.realpath(__file__)) + if gui: + import tkinter as tk + from tkinter import ttk + # Construct window + self.popup = tk.Tk()#tk.Toplevel() + self.popup.geometry('240x32') + # Set window icon + img=tk.PhotoImage(file=os.path.join(self.installpath,'PhotonSlicer.gif')) + self.popup.tk.call('wm','iconphoto',self.popup._w,img) + #tk.Label(self.popup, text="Slicing...").grid(row=0, column=0) + self.popup.title("Slicing...") + self.progress_var = tk.DoubleVar() + progress_bar = ttk.Progressbar(self.popup, variable=self.progress_var, maximum=100, length=240) + progress_bar.pack(fill=tk.Y, expand=1, side=tk.BOTTOM) + + # Measure how long it takes + t1 = time.time() + + # Setup output path + if outputpath==None and photonfilename==None:return + + #create path if not exists + if not outputpath==None: + if not os.path.exists(outputpath): + os.makedirs(outputpath) + + # if we output to PhotonFile we need a place to store RunLengthEncoded images + if not photonfilename==None: + rlestack=[] + + # read and parse svg file + xmldoc = minidom.parse(svgfilename) + layers = xmldoc.getElementsByTagName('g') + + #contourColor = (255, 255, 255) # alpha 255 is NOT transparent + #contourColor = (255) # alpha 255 is NOT transparent + # innerColor = (0, 0, 255) + innerColor = (128) + + scale=scale/0.047 + + # draw layer for layer + sliceNr=0 + nrSlices=len(layers) + pLayers=[] + for layer in layers: + layer_id = layer.attributes['id'].value + layer_z = layer.attributes['slic3r:z'].value + layer_polygons =layer.getElementsByTagName('polygon') + img = numpy.zeros((2560, 1440, 1), numpy.uint8) + pPolys=[] + for layer_polygon in layer_polygons: + layer_polygon_points = layer_polygon.attributes['points'].value + pointString=layer_polygon_points.replace(',',' ') + np_points = numpy.fromstring(pointString,dtype=float,sep=' ') + np_points = np_points * scale + nr_points = np_points.size + nr_coords = nr_points//2 + np_coords = np_points.reshape(nr_coords,2) + pPolys.append(np_coords) + + x = np_coords[:, 0] + z = np_coords[:, 1] + self.cmin = (min(self.cmin[0],x.min()), min(self.cmin[1],z.min())) + self.cmax = (max(self.cmax[0],x.max()), max(self.cmax[1],z.max())) + + pLayers.append(pPolys) + sliceNr=sliceNr+1 + + # Show progress in terminal + if not self.gui: + msg="Reading ... "+str(sliceNr)+" / " + str(nrSlices) + sys.stdout.write (msg) + sys.stdout.write('\r') + sys.stdout.flush() + + print("") + + # Center model and put on base + trans = [0, 0] + trans[0] = -(self.cmax[0] - self.cmin[0]) / 2 - self.cmin[0] + trans[1] = -(self.cmax[1] - self.cmin[1]) / 2 - self.cmin[1] + + # We want the model centered in 2560x1440 + # 2560x1440 pixels equals 120x67 + trans[0] = trans[0] + 1440 / 2 + trans[1] = trans[1] + 2560 / 2 + + sliceNr=0 + nrSlices=len(layers) + for pLayer in pLayers: + img = numpy.zeros((2560, 1440, 1), numpy.uint8) + + for pPoly in pLayer: + # Center numpy array of points which is returned for fast OGL model loading + pPoly = pPoly + trans + # Fill poly + cv2.fillPoly(img, numpy.array([pPoly],dtype='int32'), color=innerColor) + + if photonfilename==None: + cv2.imwrite(filename, img) + else: + imgarr8 = img + img1D=imgarr8.flatten(0) + rlestack.append(rleEncode.encodedBitmap_Bytes_numpy1DBlock(img1D)) + + # Show progress in terminal + if not self.gui: + msg="Saving ... "+str(sliceNr)+" / " + str(nrSlices) + sys.stdout.write (msg) + sys.stdout.write('\r') + sys.stdout.flush() + + # Update GUI progress bar if gui active + if self.gui: + try: # Check if user aborted/closed window + self.popup.update() + progress=100*sliceNr/nrSlices + self.progress_var.set(progress) + except Exception: + sys.exit() # quit() does not work if we make this an exe with cx_Freeze + + sliceNr += 1 + + + if not self.gui: print () # close progress stdout and go to new line + + if not photonfilename==None: + tempfilename=os.path.join(self.installpath,"newfile.photon") + photonfile=PhotonFile(tempfilename) + photonfile.readFile() + photonfile.Header["Layer height (mm)"]= PhotonFile.float_to_bytes(layerheight) + photonfile.Header["Exp. time (s)"] = PhotonFile.float_to_bytes(normalexposure) + photonfile.Header["Exp. bottom (s)"] = PhotonFile.float_to_bytes(bottomexposure) + photonfile.Header["# Bottom Layers"] = PhotonFile.int_to_bytes(bottomlayers) + photonfile.Header["Off time (s)"] = PhotonFile.float_to_bytes(offtime) + photonfile.replaceBitmaps(rlestack) + photonfile.writeFile(photonfilename) + + if not self.gui: print("Elapsed: ", "%.2f" % (time.time() - t1), "secs") + + +#test = Svg2Slices( +# svgfilename='STLs/pikachu_repaired.svg', +# photonfilename="STLs/pikachu_svg.photon", +# gui=False +# ) \ No newline at end of file diff --git a/base.frag b/base.frag new file mode 100644 index 0000000..07ca160 --- /dev/null +++ b/base.frag @@ -0,0 +1,10 @@ +varying /*mediump*/ vec2 uv; + +void main() { + /*mediump*/ float u = mod(uv.x * 10.0, 1.0); + /*mediump*/ float v = mod(uv.y * 10.0, 1.0); + + /*mediump*/ float t = (u > 0.1 && u < 0.9 && + v > 0.1 && v < 0.9) ? 0.3 : 0.5; + gl_FragColor = vec4(t, t, t, 1); +} diff --git a/base.vert b/base.vert new file mode 100644 index 0000000..f963bce --- /dev/null +++ b/base.vert @@ -0,0 +1,13 @@ +attribute vec2 v; // Vertex position + +uniform mat4 view; // Model transform matrix +uniform /*mediump*/ float zmin; // Z position of plane +uniform /*mediump*/ float aspect; // Aspect ratio + +varying /*mediump*/ vec2 uv; + +void main() { + gl_Position = view * vec4(v.x * aspect, v.y, zmin - 0.01, 1); + gl_Position.w = (gl_Position.z + 1.0); + uv = (vec2(v.x * aspect, v.y) + 1.0) / 2.0; +} diff --git a/consetup.py b/consetup.py index 38a2885..5b3ceb1 100644 --- a/consetup.py +++ b/consetup.py @@ -4,45 +4,10 @@ # ################################ # -# just build python consetup.py build -b ..\ #Builds on the folder above -# build installer python consetup.py build -b ..\ bdist_msi -d ..\ #Builds on the folder above -# -# OR make exe directory to zip +#just build python consetup.py build -b ..\ #Builds on the folder above +#build installer python consetup.py build -b ..\ bdist_msi -d ..\ #Builds on the folder above # -# 1) python consetup.py build -b ..\PhotonSlicer.buildcon install_exe -d ..\PhotonSlicer.installcon -# -# 2) in .build move all dlls (tcl86t.dll,tk86t.dll,VCRUNTIME140.dll,python36.dll) from install rootdir to libs folder -# -# 3) following files (in order of size) are large and not necessary: -# [ -# "numpy.core.mkl_avx512_mic.dll", -# "numpy.core.mkl_avx512.dll", -# "numpy.core.mkl_avx2.dll", -# "numpy/core/mkl_avx.dll", -# "numpy.core.libopenblas.dll", -# "numpy.core.mkl_mc3.dll", -# "numpy.core.mkl_mc.dll", -# -# "numpy.core.svml_dispmd.dll" -# "numpy.core.mkl_sequential.dll", -# -# "numpy.core.vml_avx512.dll", -# "numpy.core.vml_avx.dll", -# "numpy.core.vml_avx2.dll", -# "numpy.core.vml_avx512_mic.dll", -# "numpy.core.mkl_vml_mc.dll", -# "numpy.core.mkl_vml_mc3.dll", -# "numpy.core.mkl_vml_mc2.dll", -# "numpy.core.mkl_vml_def.dll" -# ] -# -# 4) Remove all stl's except bunny.stl and cog.stl -# -# 5) 7-Zip file - Resulting .7Z is 53MB -# -# Diff between Con and GUI is PhotonSlicer.exe and lib/library.zip -# -################################# +################################ import os import sys @@ -62,7 +27,7 @@ "", # Icon 0, # IconIndex None, # ShowCmd - "TARGETDIR", # WkDir + "TARGETDIR", # WkDir ) ] @@ -80,7 +45,8 @@ build_exe_options = { "packages": ["os", "numpy"], "include_msvcr" : True, - "excludes":["email","html","https","json","urllib","xml","xmlrpc","setuptools","pydoc_data"], + "excludes":[], + #"include_files": [""], "include_files":[ #os.path.join(PYTHON_INSTALL_DIR,'DLLs','tcl86t.dll'), #os.path.join(PYTHON_INSTALL_DIR,'DLLs','tk86t.dll'), diff --git a/guisetup.py b/guisetup.py index 387156f..90efbaa 100644 --- a/guisetup.py +++ b/guisetup.py @@ -9,7 +9,7 @@ # # OR make exe directory to zip # -# 1) python guisetup.py build -b ..\PhotonSlicer.buildgui install_exe -d ..\PhotonSlicer.installgui +# 1) python guisetup.py build -b ..\PhotonSlicer.build install_exe -d ..\PhotonSlicer.install # # 2) in .build move all dlls (tcl86t.dll,tk86t.dll,VCRUNTIME140.dll,python36.dll) from install rootdir to libs folder # @@ -35,13 +35,8 @@ # "numpy.core.mkl_vml_mc2.dll", # "numpy.core.mkl_vml_def.dll" # ] -# -# 4) Remove all stl's except bunny.stl and cog.stl # -# 5) 7-Zip file - Resulting .7Z is 53MB -# -# Diff between Con and GUI is PhotonSlicer.exe and lib/library.zip -# But only exchanging PhotonSlicer.Exe for Console version is enough to make it work. +# 4) 7-Zip file - Resulting .7Z is 64MB # ################################ diff --git a/mesh.frag b/mesh.frag new file mode 100644 index 0000000..1bab805 --- /dev/null +++ b/mesh.frag @@ -0,0 +1,13 @@ +varying /*mediump*/ vec3 norm; + +void main() { + /*mediump*/ vec3 base3 = vec3(0.99, 0.96, 0.89); + /*mediump*/ vec3 base2 = vec3(0.92, 0.91, 0.83); + /*mediump*/ vec3 base00 = vec3(0.40, 0.48, 0.51); + + /*mediump*/ float a = dot(norm, vec3(0.0, 0.0, -1.0)); + /*mediump*/ float b = dot(norm, vec3(-0.57, 0.57, -0.57)); + + gl_FragColor = vec4((a*base2 + (1.0 - a)*base00)*0.5 + + (b*base3 + (1.0 - b)*base00)*0.5, 1.0); +} diff --git a/mesh.vert b/mesh.vert new file mode 100644 index 0000000..dbceab8 --- /dev/null +++ b/mesh.vert @@ -0,0 +1,13 @@ +attribute vec3 v; // Vertex position +attribute vec3 n; // Vertex normal + +uniform mat4 model; +uniform mat4 view; + +varying /*mediump*/ vec3 norm; + +void main() { + gl_Position = view * model * vec4(v, 1); + gl_Position.w = (gl_Position.z + 1.0); + norm = normalize((view * vec4(n, 1)).xyz); +} diff --git a/profiling.txt b/profiling.txt index 579cefb..dd9aab4 100644 --- a/profiling.txt +++ b/profiling.txt @@ -25,8 +25,22 @@ Cython triInSlice 74 sec (after making tri lists per slice) 8 bit img 56 sec No fills if normal is up/down 56 sec +Slice 62 sec: + if polypoints = 10 sec + ConvexFill = 14 sec + pfrom,pto,pmid = 8 sec + fillpoints.append(pfill) = 1 sec + fill = 22 sec + img[fillPoint[1], fillPoint[0]) = 12 sec + Floodfill = 10 sec + +Slicing CPU (Stl2Slices) vs GPU (GL_Stl2Slices) + GL_Stl2Slices: 18 sec + Stl2Slices: 58 sec + + ============================================================================================== Encoding Test Case: linux diff --git a/quad.frag b/quad.frag new file mode 100644 index 0000000..429b963 --- /dev/null +++ b/quad.frag @@ -0,0 +1,12 @@ +// mediump(recision) supported in webGL but not in pyOpenGL +varying /*mediump*/ vec2 uv; +uniform sampler2D tex; + +void main() +{ + if (texture2D(tex, uv).r == 0.0) + { + discard; + } + gl_FragColor = vec4(texture2D(tex, uv).rgb, 0.5); +} diff --git a/quad.vert b/quad.vert new file mode 100644 index 0000000..c938d0f --- /dev/null +++ b/quad.vert @@ -0,0 +1,17 @@ +attribute vec2 v; // Vertex position + +//mediump(recision) supported in webGL but not in pyOpenGL +uniform mat4 view; +uniform /*mediump*/ vec2 bounds; // Z bounds +uniform /*mediump*/ float frac; // Z fraction (0 to 1) +uniform /*mediump*/ float aspect; // Aspect ratio + +varying /*mediump*/ vec2 uv; + +void main() { + gl_Position = view * vec4( + v.x * aspect, v.y, + (1.0 - frac) * bounds[0] + frac * bounds[1], 1); + gl_Position.w = (gl_Position.z + 1.0); + uv = (v + 1.0) / 2.0; +} diff --git a/slice.frag b/slice.frag new file mode 100644 index 0000000..09edc64 --- /dev/null +++ b/slice.frag @@ -0,0 +1,4 @@ +void main() +{ + gl_FragColor = vec4(1); +} \ No newline at end of file diff --git a/slice.vert b/slice.vert new file mode 100644 index 0000000..2cc8737 --- /dev/null +++ b/slice.vert @@ -0,0 +1,17 @@ +#define EPSILON 0.001 + +attribute vec3 v; // Vertex position + +//mediump(recision) supported in webGL but not in pyOpenGL +uniform mat4 model; // Model transform matrix +uniform /*mediump*/ vec2 bounds; // Z bounds +uniform /*mediump*/ float frac; // Z fraction (0 to 1) +uniform /*mediump*/ float aspect; // Aspect ratio + +void main() { + float fz = (1.0 - frac) * (bounds[0] + EPSILON) + frac * (bounds[1] - EPSILON); + gl_Position = model * vec4(v, 1); + gl_Position.z += (1.0 - fz); + gl_Position.x /= aspect; +} +