pax_global_header 0000666 0000000 0000000 00000000064 14177261103 0014514 g ustar 00root root 0000000 0000000 52 comment=2b53b0696800ee7776496740eee5f635092c3517 litophany-master/ 0000775 0000000 0000000 00000000000 14177261103 0014360 5 ustar 00root root 0000000 0000000 litophany-master/image2surface.py 0000664 0000000 0000000 00000010200 14177261103 0017440 0 ustar 00root root 0000000 0000000 #!/usr/bin/python # -*- coding: utf-8 -*- from argparse import ArgumentParser from PIL import Image import os import subprocess def get_args(): parser = ArgumentParser(description='Convert image to a surface for OpenSCAD and a .stl if desired.') parser.add_argument('imagefile', metavar='IMAGEFILE', type=str, help='The path to the image file you want to convert to a surface.') parser.add_argument('-i', dest='inverse', action='store_const', const=True, default=False, help='Defaults to white as a background, this option makes black the background.') parser.add_argument('-r', dest='removebase', action='store_const', const=True, default=False, help='Remove base layer from surface. Only applies if exporting to .scad and/or .stl.') parser.add_argument('-d', dest='maxdim', type=int, default=150, help='The maximum size in mm to make the x or y dimension. Only applies if exporting to .scad and/or .stl.') parser.add_argument('-z', dest='zheight', type=int, default=5, help='The max z-height of the text, defaults to 5') parser.add_argument('-o', dest='filename', type=str, default='image2surface.dat', help='By default, this script just outputs textsurface.dat, which can be imported into an OpenSCAD document. If you specify a .scad filename for this parameter, the script will also output a .scad file which imports the surface. If you specify a .stl filename, the script will go further and generate a .stl file.') return parser.parse_args() def get_image_data(imagefile): im = Image.open(imagefile) return [list(im.getdata()), im.size[0], im.size[1]] def create_dat(data, zheight, filename, inverse): white = 255*len(data[0]) textbuffer = '' line = [] lines = [] for i in range(len(data)): if i%width == 0 and i != 0: line.reverse() lines.append(line) line = [] line.append(data[i]) # To data for line in lines: textbuffer += '\n' for pixel in line: ratio = float(sum(pixel))/white if not inverse: ratio = 1-ratio # Numbers (with decimal places) must be reversed so # that when the entire textbuffer is reversed later, # numbers will be correct textbuffer += (' '+repr(ratio*zheight))[::-1] datfilename = filename if filename[-4:] == '.dat' else 'temp_image2surface.dat' f = open(datfilename, 'w') f.write(textbuffer[::-1]) f.close() print 'Surface is in '+datfilename return datfilename def create_scad(datfilename, filename, removebase, width, height, maxdim): if width > height: scale = [float(maxdim)/width, (maxdim*float(height)/width)/height, 1] else: scale = [(maxdim*float(width)/height)/width, float(maxdim)/height, 1] scadfilename = filename if filename[-5:] == '.scad' else 'temp_image2surface.scad' f = open(scadfilename, 'w') if removebase: f.write('translate([0, 0, -1]) difference() {\n\t') f.write('scale('+repr(scale)+') translate([0, 0, 1]) surface("'+datfilename+'", center=true, convexity=5);') if removebase: f.write('\n\tcube(['+repr(scale[0]*width)+', '+repr(scale[1]*height)+', 2], center=true);\n}') f.close() print 'SCAD file is '+scadfilename return scadfilename def create_stl(filename, scadfilename): openscadexec = 'openscad' windows = 'C:\Program Files\OpenSCAD\openscad.exe' mac = '/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD' if os.path.exists(windows): openscadexec = windows elif os.path.exists(mac): openscadexec = mac stlfilename = filename command = [openscadexec, '-m', 'make', '-s', filename, scadfilename] print 'Exporting to STL' subprocess.call(command) print 'STL file is '+stlfilename if __name__ == '__main__': args = get_args() # Generates an RGBA array, given an image file [data, width, height] = get_image_data(args.imagefile) # Outputs a .dat file that OpenSCAD can use with the surface command datfilename = create_dat(data, args.zheight, args.filename, args.inverse) # Generate .scad and/or .stl if args.filename[-5:] == '.scad' or args.filename[-4:] == '.stl': # Outputs a .scad file that can be used to create a .stl file scadfilename = create_scad(datfilename, args.filename, args.removebase, width, height, args.maxdim) if args.filename[-4:] == '.stl': # Outputs a printable .stl file create_stl(args.filename, scadfilename) litophany-master/lithocylinder2.scad 0000664 0000000 0000000 00000006252 14177261103 0020154 0 ustar 00root root 0000000 0000000 // preview[view:west, tilt:top] // Desired height of cylinder. Image will scaled to fit in the height, with the radius set so that the width takes up one half of it. cylinder_height = 60; // Desired thickness of embossed region. image_thickness = 2; // Select "outside" to get the image on the outside of the cylinder. direction = "inside"; // [inside, outside] // Use http://customizer.makerbot.com/image_surface?image_x=81&image_y=61 to generate. // Check invert colors if printing on the inside. Leave uninverted for the outside. surface_file = "surface.dat"; // [image_surface:81x61] // Select "light" if your (uninverted) image has a light background. background = "dark"; // [dark, light] // Need to leave one extrusion width all the way around so there will be no holes. extrusion_width = 0.4; // More gives higher resolution cylinder, but takes longer and may crash OpenSCAD. Use an odd number. n_facets = 21; /* [Hidden] */ // Change these if you are providing a larger than default surface file. image_width = 81; image_height = 61; slop = 0.1; surface_width = cylinder_height / image_height * image_width; cylinder_r = surface_width / PI; step_distance = surface_width / n_facets; step_degree = 180 / n_facets; $fn = n_facets * 2; module emboss(step = 0) { difference() { if (step < n_facets) rotate([0, 0, -step_degree]) emboss(step + 1) child(); else child(); image_surface(step * step_distance); } } // We want the non-image side to meld into the image, so for dark backgrounds // we cut in full black, and for light backgrounds cut in full white. module backside_cutter() { difference() { cylinder(h=cylinder_height + 4*slop, r=cylinder_r + slop, center=true); if (background == "dark" && direction == "inside" || background == "light" && direction == "outside") cylinder(h=cylinder_height + 6*slop, r=cylinder_r - image_thickness - extrusion_width, center=true); else cylinder(h=cylinder_height + 6*slop, r=cylinder_r - extrusion_width, center=true); translate([-cylinder_r - 2*slop, -cylinder_r - 2*slop, -cylinder_height/2 - 3*slop]) cube([cylinder_r + 2*slop, 2 * (cylinder_r + 2*slop), cylinder_height + 6*slop]); } } module blank() { difference() { cylinder(h=cylinder_height + 2*slop, r=cylinder_r, center=true); backside_cutter(); } } module scaled_surface() { rotate([90, 0, 0]) // Translate inward by extrusion width so there can be no holes for pure white. translate([0, 0, extrusion_width]) scale([surface_width/(image_width - 1), (cylinder_height + 2*slop)/(image_height - 1), image_thickness]) translate([image_width - 1, 0, 0]) mirror([1, 0, 0]) surface(file=surface_file, convexity = 5); } module image_surface(x) { translate([-x, cylinder_r * cos(step_degree/2), -cylinder_height/2 - slop]) scaled_surface(); } if (direction == "outside") { intersection() { difference() { emboss() blank(); cylinder(h=cylinder_height + 2*slop, r=cylinder_r - image_thickness - 2*extrusion_width, center=true); } cube([2*(cylinder_r + slop), 2*(cylinder_r + slop), cylinder_height], center=true); } } else { difference() { cylinder(h=cylinder_height, r=cylinder_r, center=true); emboss() blank(); } } litophany-master/lithophane.py 0000664 0000000 0000000 00000423315 14177261103 0017075 0 ustar 00root root 0000000 0000000 #!/usr/bin/python # -*- coding: utf-8 -*- import os import sys import argparse import platform import time import math import locale locale.setlocale(locale.LC_ALL, '') # platform namespace gets stomped by OpenGL currPlatform = platform.system() try: import PySide from PySide.QtCore import * from PySide.QtGui import * from PySide.QtOpenGL import * except ImportError: print "PySide is required for this program to work" sys.exit(1) __haveOpenGL__ = False try: from OpenGL.GL import * __haveOpenGL__ = True except ImportError: print "PyOpenGL must be installed to have mesh preview" __version__ = "v0.10 20140628" __prefsVersion__ = 1 __thisAppName__ = "HC Lithophane Creator" __companyName__ = "Hammurabi Concepts" __thisHelpFileName__ = "HCLithophaneCreator.html" __defaultFileName__ = "Unnamed-" # From Mark Summerfield (http://www.qtrac.eu/pyqtbook.html#pyside) # Replace def isAlive(qobj): ... # with def isAlive(qobj): return True. # (There is currently no PySide equivalent.) def isAlive(qobj): if qobj is None: return False return True """ makeBool(): Used to correct PySide bug wherein a boolean setting is stored as a text string of 'true' or 'false' and is loaded as a unicode, rather than boolean, value. """ def makeBool(val): if isinstance(val, bool): return val return 'true' == val """ makeCheckBox(): Used to correct PySide bug wherein a Qt.CheckState setting is stored as a text string of '0', '1', or '2' and is loaded as a unicode, rather than a Qt.CheckState, value. """ def makeCheckBox(val): val = int(val) if val == 2: return Qt.Checked elif val == 1: return Qt.PartiallyChecked return Qt.Unchecked class GLWidget(QGLWidget): xRotationChanged = Signal(int) yRotationChanged = Signal(int) zRotationChanged = Signal(int) def __init__(self, lithophane, parent=None): super(GLWidget, self).__init__(parent) self.object = None self.xRot = 0 self.yRot = 0 self.zRot = 0 self.width = 1 self.breadth = 1 self.maxDim = 1 self.minVal = [0, 0, 0] self.maxVal = [0, 0, 0] self.lastPos = QPoint() self.lithophane = lithophane def xRotation(self): return self.xRot def yRotation(self): return self.yRot def zRotation(self): return self.zRot def minimumSizeHint(self): return QSize(80, 45) def sizeHint(self): return QSize(640, 360) def setXRotation(self, angle): angle = self.normalizeAngle(angle) if angle != self.xRot: self.xRot = angle self.xRotationChanged.emit(angle) self.updateGL() def setYRotation(self, angle): angle = self.normalizeAngle(angle) if angle != self.yRot: self.yRot = angle self.yRotationChanged.emit(angle) self.updateGL() def setZRotation(self, angle): angle = self.normalizeAngle(angle) if angle != self.zRot: self.zRot = angle self.zRotationChanged.emit(angle) self.updateGL() def initializeGL(self): self.qglClearColor(QColor(0, 0, 0)) glEnable(GL_COLOR_MATERIAL) #glEnable(GL_STENCIL_TEST) #glClear(GL_STENCIL_BUFFER_BIT) #glStencilFunc(GL_ALWAYS, 0, 1) self.object = self.makeObject() glClearDepth(1.0) glDepthFunc(GL_LESS) glShadeModel(GL_SMOOTH) glEnable(GL_DEPTH_TEST) glEnable(GL_CULL_FACE) glMatrixMode(GL_PROJECTION) glLoadIdentity() glMatrixMode(GL_MODELVIEW) glLightfv( GL_LIGHT0, GL_AMBIENT, (0.5, 0.5, 0.5, 1.0)) glLightfv( GL_LIGHT0, GL_DIFFUSE, (1.0, 1.0, 1.0, 1.0)) glLightfv( GL_LIGHT0, GL_POSITION, (0.1, 0.2, 1.0, 0.0)) glEnable(GL_LIGHT0) def paintGL(self): glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glEnable(GL_LIGHTING) glLoadIdentity() #glTranslated(0.5 * self.maxDim, 0.5 * self.maxDim, -0.5 * self.maxDim) glRotated(self.xRot / 16.0, 1.0, 0.0, 0.0) glRotated(self.yRot / 16.0, 0.0, 1.0, 0.0) glRotated(self.zRot / 16.0, 0.0, 0.0, 1.0) glCallList(self.object) #glTranslated(-0.5 * self.maxDim, -0.5 * self.maxDim, 0.5 * self.maxDim) glTranslated(0.0, 0.0, 0 - self.maxDim / 4.0) def resizeGL(self, width, height): # ensure that a 16:9 view of the mesh is preserved if width / 16 > height / 9: width = 16 * height / 9 else: height = 9 * width / 16 glViewport(0, 0, width, height) glMatrixMode(GL_PROJECTION) glLoadIdentity() minVal = 0 - self.maxDim / 1.8 maxVal = self.maxDim / 1.8 glOrtho(minVal, maxVal, maxVal, minVal, minVal * 4, maxVal * 6) glMatrixMode(GL_MODELVIEW) def mousePressEvent(self, event): self.lastPos = QPoint(event.pos()) def mouseMoveEvent(self, event): dx = event.x() - self.lastPos.x() dy = event.y() - self.lastPos.y() if event.buttons() & Qt.LeftButton: self.setXRotation(self.xRot + 8 * dy) self.setYRotation(self.yRot + 8 * dx) elif event.buttons() & Qt.RightButton: self.setXRotation(self.xRot + 8 * dy) self.setZRotation(self.zRot + 8 * dx) self.lastPos = QPoint(event.pos()) def centerCoord(self, coord): (x, y, z) = coord x -= self.width / 2 y -= self.breadth / 2 return (x, y, z) def makeObject(self, method='default'): if self.object: glDeleteLists(self.object, 1) minVal = [0, 0, 0] maxVal = [0, 0, 0] for vertex in self.lithophane.vertices: for i in range(3): if vertex[i] < minVal[i]: minVal[i] = vertex[i] elif vertex[i] > maxVal[i]: maxVal[i] = vertex[i] self.width = maxVal[0] - minVal[0] self.breadth = maxVal[1] - minVal[1] self.maxDim = 1 for i in range(3): if maxVal[i] - minVal[i] > self.maxDim: self.maxDim = maxVal[i] - minVal[i] # glLightfv( GL_LIGHT0, GL_AMBIENT, (0.5, 0.5, 0.5, 1.0)) glLightfv( GL_LIGHT0, GL_DIFFUSE, (1.0, 1.0, 1.0, 1.0)) glLightfv( GL_LIGHT0, GL_POSITION, (0.1, 0.2, 1.0, 0)) glEnable(GL_LIGHT0) # genList = glGenLists(1) glNewList(genList, GL_COMPILE) #glTranslated(-0.5 * self.maxDim, -0.5 * self.maxDim, 0.5 * self.maxDim) # used to switch between back end makers print "method:", method if method == 'edges': self.__makeObjectEdges() elif method == 'stencil': self.__makeObjectStencil() else: self.__makeObject() glEndList() return genList def __makeObject(self): # creates a mesh, no stroking of edges so details are difficult without lighting # do faces glColor3f(1, 0, 1) glBegin(GL_TRIANGLES) for group in self.lithophane.tris: if self.lithophane.tris[group]: for face in self.lithophane.tris[group]: glNormal3fv(self.getNormal(face, 'First')) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[0] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[1] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[2] - 1 ])) glEnd() def __makeObjectEdges(self): # draws triangles then edges # problem is edges are not culled so it looks "see through" # do edges glLineWidth(1.5) glColor3f(0.25, 0.33, 0.3) glBegin(GL_LINES) for group in self.lithophane.tris: if self.lithophane.tris[group]: for face in self.lithophane.tris[group]: glNormal3fv(self.getNormal(face, 'First')) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[0] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[1] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[1] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[2] - 1 ])) glEnd() # do faces glColor3f(0.4, 0.4, 0.5) glBegin(GL_TRIANGLES) for group in self.lithophane.tris: if self.lithophane.tris[group]: for face in self.lithophane.tris[group]: glColor3f(1, 0, 0.5) glNormal3fv(self.getNormal(face, 'First')) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[0] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[1] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[2] - 1 ])) glEnd() def __makeObjectStencil(self): # attempt to draw faces and lines with lines being culled via stencil operations # http://glprogramming.com/red/chapter14.html#name16 glEnable(GL_STENCIL_TEST) glEnable(GL_DEPTH_TEST) glClear(GL_STENCIL_BUFFER_BIT) glStencilFunc(GL_ALWAYS, 0, 1) glStencilOp(GL_INVERT, GL_INVERT, GL_INVERT) # glColor3f(0, 1, 0) for group in self.lithophane.tris: if self.lithophane.tris[group]: for face in self.lithophane.tris[group]: glColor3f(0, 1, 0) # outline polygon glLineWidth(0.5) glBegin(GL_LINES) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[0] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[1] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[1] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[2] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[2] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[0] - 1 ])) glEnd() # glColor3f(0, 0, 1) glStencilFunc(GL_EQUAL, 0, 1) glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP) # fill polygon glBegin(GL_TRIANGLES) glNormal3fv(self.getNormal(face, 'First')) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[0] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[1] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[2] - 1 ])) glEnd() # glColor3f(0, 1, 0) glStencilFunc(GL_ALWAYS, 0, 1) glStencilOp(GL_INVERT, GL_INVERT, GL_INVERT) # outline polygon glLineWidth(0.5) glBegin(GL_LINES) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[0] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[1] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[1] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[2] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[2] - 1 ])) glVertex3dv(self.centerCoord(self.lithophane.vertices[ face[0] - 1 ])) glEnd() def normalizeAngle(self, angle): while angle < 0: angle += 360 * 16 while angle > 360 * 16: angle -= 360 * 16 return angle def getNormal(self, face, which): (p0x, p0y, p0z) = self.lithophane.vertices[ face[0] - 1] (p1x, p1y, p1z) = self.lithophane.vertices[ face[1] - 1] (p2x, p2y, p2z) = self.lithophane.vertices[ face[2] - 1] U = [p1x - p0x, p1y - p0y, p1z - p0z] V = [p2x - p0x, p2y - p0y, p2z - p0z] normal = [0, 0, 0] normal[0] = U[1] * V[2] - U[2] * V[1] normal[1] = U[2] * V[0] - U[0] * V[2] normal[2] = U[0] * V[1] - U[1] * V[0] return normal class MeshPreviewDlg(QDialog): def __init__(self, lithophane, parent=None): super(MeshPreviewDlg, self).__init__(parent) mainLayout = QVBoxLayout() if __haveOpenGL__: self.glWidget = GLWidget(lithophane) mainLayout.addWidget(self.glWidget) else: mainLayout.addWidget(QLabel('You must install PyOpenGL in order to preview the mesh')) self.setLayout(mainLayout) def makeMesh(self): if __haveOpenGL__: self.glWidget.makeObject() class ToolsPrefsDlg(QDialog): def __init__(self, updateFunc, parent=None): super(ToolsPrefsDlg, self).__init__(parent) self.app = parent tabWidget = QTabWidget() buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Close | QDialogButtonBox.RestoreDefaults) optionsLayout = QVBoxLayout() optionsLayout.addWidget(buttonBox) # generalWidget = QWidget() self.recentFilesSpinBox = QSpinBox() self.recentFilesSpinBox.setMinimum(0) self.recentFilesSpinBox.setMaximum(99) recentFilesLabel = QLabel("Maximum Recent Files") recentFilesLabel.setBuddy(self.recentFilesSpinBox) recentFilesLayout = QHBoxLayout() recentFilesLayout.addWidget(recentFilesLabel) recentFilesLayout.addWidget(self.recentFilesSpinBox) fileOptionLayout = QVBoxLayout() fileOptionLayout.addLayout(recentFilesLayout) fileOptionLayout.setAlignment( Qt.AlignLeft | Qt.AlignTop ) fileOptionGroupBox = QGroupBox("File Options") fileOptionGroupBox.setLayout(fileOptionLayout) self.useOptionsTabsCheckBox = QCheckBox() self.useOptionsTabsCheckBox.setText( 'Group lithophane parameters into tabs' ) useOptionsLabel = QLabel('only takes effect on application restart') useOptionsLabel.setTextFormat(Qt.RichText) useOptionsLabel.setAlignment(Qt.AlignRight) self.showLithophaneInfoCheckBox = QCheckBox() self.showLithophaneInfoCheckBox.setText( 'Show lithophane information' ) self.imageWidthSpinBox = QSpinBox() self.imageWidthSpinBox.setRange(100,1000) self.imageHeightSpinBox = QSpinBox() self.imageHeightSpinBox.setRange(100,1000) imagePreviewLabel = QLabel('only takes affect when OK button is pressed') imagePreviewLabel.setTextFormat(Qt.RichText) imagePreviewLabel.setAlignment(Qt.AlignRight) appearOptionLayout = QHBoxLayout() appearOptionLayout.addWidget(self.useOptionsTabsCheckBox) infoOptionLayout = QHBoxLayout() infoOptionLayout.addWidget(self.showLithophaneInfoCheckBox) imagePreviewLayout = QHBoxLayout() imagePreviewLayout.addWidget(QLabel('Image preview')) imagePreviewLayout.addWidget(self.imageWidthSpinBox) imagePreviewLayout.addWidget(QLabel(' x ')) imagePreviewLayout.addWidget(self.imageHeightSpinBox) layoutOptionLayout = QVBoxLayout() layoutOptionLayout.addLayout(appearOptionLayout) layoutOptionLayout.addWidget(useOptionsLabel) layoutOptionLayout.setAlignment( Qt.AlignLeft | Qt.AlignTop ) layoutOptionGroupBox = QGroupBox("Layout Options") layoutOptionGroupBox.setLayout(layoutOptionLayout) displayOptionLayout = QVBoxLayout() displayOptionLayout.addLayout(infoOptionLayout) displayOptionLayout.addLayout(imagePreviewLayout) displayOptionLayout.addWidget(imagePreviewLabel) displayOptionLayout.setAlignment( Qt.AlignLeft | Qt.AlignTop ) displayOptionGroupBox = QGroupBox("Display Options") displayOptionGroupBox.setLayout(displayOptionLayout) leftLayout = QVBoxLayout() leftLayout.addWidget(fileOptionGroupBox) leftLayout.addWidget(layoutOptionGroupBox) leftLayout.addWidget(displayOptionGroupBox) leftLayout.addStretch() generalLayout = QHBoxLayout() generalLayout.addLayout(leftLayout) generalWidget.setLayout(generalLayout) # tabWidget.addTab(generalWidget, "General") layout = QVBoxLayout() layout.addWidget(tabWidget) layout.addWidget(buttonBox) self.setLayout(layout) buttonBox.rejected.connect(self.close) buttonBox.button(QDialogButtonBox.Ok).clicked.connect(updateFunc) self.recentFilesSpinBox.valueChanged.connect(self.app.updateRecentFilesCount) def close(self): self.hide() def updateToolTip(self, index): self.dateFormatSpecifierComboBox.setToolTip(self.dateFormatSpecifierToolTipList[index]) def useTabs(self): return self.useOptionsTabsCheckBox.checkState() == Qt.Checked class HelpContentsDlg(QDialog): """ Displays a help dialog providing information about the application. """ def __init__(self, page, parent=None): super(HelpContentsDlg, self).__init__(parent) browser = QTextBrowser() browser.setSearchPaths([":/"]) browser.setSource(QUrl(page)) browser.setOpenExternalLinks(True) layout = QVBoxLayout() layout.addWidget( browser ) self.setLayout(layout) class Lithophane(QObject): #tick = QtCore.pyqtSignal(int, name='changed') # may need for class to derive from thread to avoid blocking? meshProgress = Signal(float) # current progress number meshTotal = Signal(int) # total number for completion stateChanged = Signal(str) # text messages def __init__(self, filename=None, parent=None): super(Lithophane, self).__init__(parent) self.doubleRes = False # should only set this when using method flat self.restrictToLayer = True # should expose this in interface self.thickness = 0.1 # layer thickness in mm self.layers = 10 # maximum number of layers, includes underlayer/back so minimum value of 2 self.border = 4 # width of border in pixels self.marginWidth = 1 # width of margin in pixels self.marginThickness = 1 # layer height of margin self.backThickness = 0 # extra layers of thickness for the back self.lithoType = None self.image = None self.image_orig = None self.vertices = None self.faces = None self.width = None # width in inches self.ppi = None # pixels per inch self.filename = filename self.tris = {'border': None, 'margin': None, 'image': None, 'back': None, 'edge': None} self.enhanceContrast = True self.scaleImage = 1.0 self.resolution = None self.useTiming = True self.timeLog = None self.refineMesh = True # do not allow single pixel protrusions self.logTime = True self.minExtrusionWidth = 0.4 # minimum diameter in mm of an extrusion in a layer self.contrastMult = None self.contrastSub = None self.scaleMethod = Qt.KeepAspectRatio self.filteredMap = {} self.filteredLayers = False if filename: self.load() def __setattr__(self, var, val): if val is None: pass elif var == 'border': val = int(val) if val < 0: val = 1 elif var == 'marginWidth': val = int(val) if val < 0: val = 1 elif var == 'marginThickness': val = int(val) if val < 1: val = 1 if val >= self.layers: val = self.layers - 1 elif var == 'thickness': val = float(val) if val < 0.01: val = 0.1 elif var == 'layers': val = int(val) if val < 2: val = 10 elif var == 'width': val = float(val) if val < 0.1: raise ValueError, "%s is less than minimum value of 0.1" % val elif var == 'ppi': val = float(val) if val < 0.01: raise ValueError, "%s is less than minimum value of 0.01" % val elif var == 'resolution': if isinstance(val, str): try: index = val.index('.') except ValueError: try: index = val.index('x') except ValueError: raise ValueError, "%s is not a valid resolution string" % val val = QSize( int(val[:index]), int(val[index+1:]) ) elif not isinstance(val, QSize): raise ValueError, "%s is not a valid resolution" % val elif var == 'scaleImage': val = float(val) elif var == 'backThickness': val = int(val) if val < 0: raise ValueError, "%s is not a valid backThickness" % val self.__dict__[var] = val if val is None: return elif var == 'width': if self.image: self.__dict__['ppi'] = float(self.width_px) / self.width elif var == 'ppi': if self.image: self.__dict__['width'] = float(self.width_px) / self.ppi elif var == 'scaleImage': if self.image: if self.image_orig is None: self.image_orig = self.image self.__dict__['resolution'] = self.image_orig.size() * self.scaleImage self.image = self.image_orig.scaled(self.resolution) self.__dict__['ppi'] = float(self.width_px) / self.width else: self.__dict__['resolution'] = None elif var == 'resolution': self.__dict__['scaleImage'] = None if self.image_orig is None: self.image_orig = self.image self.image = self.image_orig.scaled(self.resolution, self.scaleMethod) self.__dict__['ppi'] = float(self.width_px) / self.width def __getattr__(self, var): if var == 'ppmm': if self.ppi: return self.ppi / 25.4 return None if var == 'vpmm': imageWidth = self.width_mm if self.border: imageWidth -= 2 * self.breadth_mm return (self.width_image + 1) / imageWidth elif var == 'width_mm': if self.width: return self.width * 25.4 return None elif var == 'height': # implicitly in inches if self.image: return self.height_px / self.ppi elif var == 'height_mm': if self.image: return float(self.height_px) / self.width_px * self.width_mm return None elif var == 'depth': # implicitly in mm return self.layers * self.thickness elif var == 'marginDepth': return self.marginThickness * self.thickness elif var == 'border_mm': if self.image: return self.border / self.ppmm elif var == 'margin_mm': if self.border and self.image: return self.marginWidth / self.ppmm return 0 elif var == 'breadth_mm': if self.border and self.ppmm: return self.border_mm + self.margin_mm return 0 elif var == 'minDia_px': if self.image: # technically may be off slightly if there's a border return int(round(self.image.width() / self.width_mm * self.minExtrusionWidth + 0.5)) elif var == 'height_vert': if self.image: ht = self.height_image if self.border: ht += 6 return ht elif var == 'width_vert': if self.image: wd = self.width_image if self.border: wd += 6 return wd elif var == 'width_px': if self.image: wd = self.image.width() if self.border: wd += 2 * (self.border + self.marginWidth) return wd elif var == 'height_px': if self.image: ht = self.image.height() if self.border: ht += 2 * (self.border + self.marginWidth) return ht + 1 elif var == 'width_image': if self.image: wd = self.image.width() if self.doubleRes: wd *= 2 return wd return None elif var == 'height_image': if self.image: ht = self.image.height() if self.doubleRes: ht *= 2 return ht return None else: raise AttributeError def load(self, filename=None): if filename: self.filename = filename if self.filename is None: return False self.image = QImage(self.filename) self.image_orig = None if self.image.width() < 1: raise TypeError, "attempted to set invalid image %s having width %s" % (self.filename, self.image.width()) # ensure width and ppi are set and updated # setting either to a value when there is an image updates the other if self.width: self.width = self.width elif self.ppi: self.ppi = self.ppi else: # go with default of 7" width self.width = 7.0 def save(self, filename=None, lithoType=None): self.generateMesh(lithoType) # write file if not filename: # probably better to not assume three letter extensions fileId = '-%sx%s_%s_-%s' % (int(self.width*10)/10.0, int(self.height*10)/10.0, int(self.ppi*100)/100, self.lithoType) filename = self.filename[:-4] + fileId + '.obj' self.stateChanged.emit('Saving %s as %s' % (filename, lithoType)) with open(filename, 'w') as f: f.write('# Autogenerated from %s\n' % self.filename) f.write('# Written by %s (%s) copyright 2014 %s\n' % (__thisAppName__, __version__, __companyName__)) f.write('# Width: %s (inches)\n# Height: %s (inches)\n' % (int(self.width*10)/10.0, int(self.height*10)/10.0)) f.write('# Width: %s (pixels)\n# Height: %s (pixels)\n' % (self.width_px, self.height_px)) if lithoType: f.write('# Created with %s algorithm\n' % lithoType) f.write('o lithograph\n') for coord in self.vertices: f.write('v %F %F %F\n' % coord) if self.faces: for face in self.faces: f.write('f %d %d %d %d\n' % face) for group in self.tris: if self.tris[group]: f.write('g %s\n' % group) for face in self.tris[group]: (v1, v2, v3) = face f.write('f %d %d %d\n' % (v1, v2, v3)) def generateMesh(self, lithoType='simple'): lithoType = lithoType.lower() # clear old mesh data self.vertices = None self.faces = None self.tris = {'border': None, 'margin': None, 'image': None, 'back': None, 'edge': None, 'quints': None} self.lithoType = lithoType # self.stateChanged.emit('Generating %s mesh' % lithoType) if lithoType == 'simple' or lithoType == 'flat' or lithoType is None: self.lithoType = 'simple' self.generateMesh_Flat() elif lithoType == 'quint' or lithoType == 'quints': self.generateMesh_Quint() elif lithoType == 'cyl': self.generateMesh_Cyl() else: raise AttributeError, "mesh type %s not supported" % lithoType def getMeshStats(self, lithoType=None): if lithoType == 'simple' or lithoType == 'flat' or lithoType is None: return self.getMeshStats_Flat() elif lithoType == 'quint': return self.getMeshStats_Quint() else: raise AttributeError, "mush type %s statistics not supported" % lithoType def getMeshStats_Flat(self): # get vertices and faces wd = self.width_image ht = self.height_image # NOTE: these actually depend on the method employed vertices = { 'total': 0, 'image': 0, 'margin': 0, 'back': 0, 'border': 0 } faces = { 'total': 0, 'image': 0, 'margin': 0, 'back': 0, 'border': 0, 'edge': 0 } vertices['image'] = wd * ht vertices['back'] = wd * ht faces['image'] = (wd - 1) * (ht - 1) faces['back'] = (wd - 1) * (ht - 1) faces['edge'] = (wd + ht) * 2 - 4 if self.border > 0: vertices['image'] = wd * ht # image vertices['margin'] = (wd + ht + 4) * 4 # margin vertices['border'] = (wd + ht + 8) * 4 # border vertices['back'] = (wd + 6) * (ht + 6) # back faces['image'] = (wd + 1) * (ht + 1) # image faces['margin'] = (wd + ht + 4) * 4 + 4 # margin faces['border'] = (wd + ht + 8) * 2 # border faces['edge'] = (wd + ht +10) * 2 # edge faces['back'] = (wd + 5) * (ht + 5) # back for face in faces: faces[face] *= 2 for face in faces: faces['total'] += int(faces[face]) for vert in vertices: vertices['total'] += vertices[vert] return (vertices, faces) def getMeshStats_Quint(self): # get vertices and faces wd = self.width_image ht = self.height_image # NOTE: these actually depend on the method employed vertices = { 'total': 0, 'image': 0, 'margin': 0, 'back': 0, 'border': 0, 'quint': 0 } faces = { 'total': 0, 'image': 0, 'margin': 0, 'back': 0, 'border': 0, 'edge': 0 } vertices['image'] = wd * ht * 2 vertices['back'] = wd * ht vertices['quint'] = (wd - 1) * (ht - 1) faces['image'] = (wd - 1) * (ht - 1) faces['back'] = (wd - 1) * (ht - 1) faces['edge'] = (wd + ht) * 2 - 4 if self.border > 0: vertices['image'] = 4 * wd * ht + 2 * (wd + ht + 2) # image vertices['margin'] = (wd + ht + 4) * 4 # margin vertices['border'] = (wd + ht + 8) * 4 # border vertices['back'] = (wd + 6) * (ht + 6) # back faces['image'] = (wd + 1) * (ht + 1) # image faces['margin'] = (wd + ht + 4) * 4 + 4 # margin faces['border'] = (wd + ht + 8) * 2 # border faces['edge'] = (wd + ht +10) * 2 # edge faces['back'] = (wd + 5) * (ht + 5) # back for face in faces: faces[face] *= 2 faces['total'] += int(faces[face]) for vert in vertices: vertices['total'] += vertices[vert] return (vertices, faces) def generateMesh_Cyl(self): print "cylindrical mesh not implemented" def generateMesh_Flat(self): # generate four vertices per pixel in the *image* # effectively same algorithm for vertices as mesh_spiky, # except that image vertices require an extra row and column # per pixel changing the calculations totTime = self.estTime() (statsVert, statsFace) = self.getMeshStats() self.meshTotal.emit(totTime) curTime = 0 if self.useTiming: self.timeLog = [] self.timeLog.append( (time.time(), 'start') ) if not self.image: return False self.vertices = [] # row, column, height self.faces = [] numBorder = 0 numMargin = 0 tall = self.height_image wide = self.width_image totWidth = self.width_mm totHeight = self.height_mm numRows = self.height_vert numCols = self.width_vert if self.border > 0: if self.logTime: curTime = self.estTime('start') self.meshProgress.emit(curTime) if self.useTiming: self.timeLog.append( (time.time(), 'border vertices') ) # should not need to redefine totHeight, already accounts for border #totHeight = self.breadth_mm + (tall + 1) / self.vpmm + self.breadth_mm # do border vertices of top face in row order for row in range(numRows): i = row if row == 0: pass elif row == 1: i = self.border_mm elif row == 2: i = self.breadth_mm elif row == numRows - 3: i = self.breadth_mm + (tall + 1) / self.vpmm elif row == numRows - 2: i = self.breadth_mm + (tall + 1) / self.vpmm + self.margin_mm elif row == numRows - 1: i = self.breadth_mm + (tall + 1) / self.vpmm + self.breadth_mm else: i = self.breadth_mm + (i - 2) / self.vpmm self.vertices.append( (i, 0, self.depth) ) self.vertices.append( (i, self.border_mm, self.depth) ) if row < 2 or row > numRows - 3: self.vertices.append( (i, self.breadth_mm, self.depth) ) for j in range(self.width_image): self.vertices.append( (i, self.breadth_mm + (j + 1) / self.vpmm, self.depth) ) self.vertices.append( (i, self.breadth_mm + (wide + 1) / self.vpmm, self.depth) ) self.vertices.append( (i, self.breadth_mm + (wide + 1) / self.vpmm + self.margin_mm, self.depth) ) self.vertices.append( (i, self.breadth_mm + (wide + 1) / self.vpmm + self.breadth_mm, self.depth) ) numBorder = len(self.vertices) if numBorder != statsVert['border']: raise ValueError, "found %s border vertices when it should have been %s" % (numBorder, statsVert['border']) print "border vertices:", numBorder if self.logTime: curTime += self.estTime('borderV') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if self.useTiming: self.timeLog.append( (time.time(), 'border faces') ) # generate faces for border self.tris['border'] = [] # first two vertex rows are matched in pairs for i in range(numCols - 1): self.tris['border'].append( (i+2, i+1, i+numCols+1) ) self.tris['border'].append( (i+numCols+1, i+numCols+2, i+2) ) # next face row follows different rule self.tris['border'].append( (numCols+2, numCols+1, numCols * 2 + 1) ) self.tris['border'].append( (numCols * 2 + 1, numCols * 2+2, numCols+2) ) currVert = numCols * 2 - 1 self.tris['border'].append( (currVert+1, currVert, currVert+4) ) self.tris['border'].append( (currVert+4, currVert+5, currVert+1) ) # intermediate rows are paired currVert = numCols * 2 for i in range(tall+1): self.tris['border'].append( (currVert+2, currVert+1, currVert+5) ) self.tris['border'].append( (currVert+5, currVert+6, currVert+2) ) self.tris['border'].append( (currVert+4, currVert+3, currVert+7) ) self.tris['border'].append( (currVert+7, currVert+8, currVert+4) ) currVert += 4 # next face row follows different rule self.tris['border'].append( (currVert+2, currVert+1, currVert+5) ) self.tris['border'].append( (currVert+5, currVert+6, currVert+2) ) self.tris['border'].append( (currVert+4, currVert+3, currVert+3+numCols) ) self.tris['border'].append( (currVert+3+numCols, currVert+4+numCols, currVert+4) ) # last two rows are matched in pairs finInter = numBorder - numCols * 2 for i in range(finInter + 1, finInter + numCols): self.tris['border'].append( (i+1, i, i+numCols) ) self.tris['border'].append( (i+numCols, i+numCols+1, i+1) ) if self.logTime: curTime += self.estTime('borderF') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if len(self.tris['border']) != statsFace['border']: raise ValueError, "found %s border faces when it should have been %s" % (len(self.tris['border']), statsFace['border']) print "border faces:", len(self.tris['border']) if self.useTiming: self.timeLog.append( (time.time(), 'margin vertices') ) mrgRows = numRows - 2 mrgCols = numCols - 2 # do margin vertices of top face in row order # the first and last two rows include vertices to match the image width for row in range(mrgRows): i = self.breadth_mm + (row - 1) / self.vpmm if row == 0: i = self.border_mm elif row == mrgRows - 1: i = self.breadth_mm + (tall+1) / self.vpmm + self.margin_mm self.vertices.append( (i, self.border_mm, self.marginDepth) ) self.vertices.append( (i, self.breadth_mm, self.marginDepth) ) if row < 2 or row > mrgRows - 3: for j in range(1, wide + 1): self.vertices.append( (i, self.breadth_mm + j / self.vpmm, self.marginDepth) ) self.vertices.append( (i, totWidth - self.breadth_mm, self.marginDepth) ) self.vertices.append( (i, totWidth - self.border_mm, self.marginDepth) ) numMargin = len(self.vertices) - numBorder if numMargin != statsVert['margin']: raise ValueError, "found %s margin vertices when it should have been %s" % (numMargin, statsVert['margin']) print "margin vertices:", numMargin if self.logTime: curTime += self.estTime('marginV') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if self.useTiming: self.timeLog.append( (time.time(), 'margin faces') ) # generate faces for margin self.tris['margin'] = [] # first two vertex rows are matched in pairs for i in range(numBorder, numBorder + wide + 3): self.tris['margin'].append( (i+2, i+1, i+mrgCols+1) ) self.tris['margin'].append( (i+mrgCols+1, i+mrgCols+2, i+2) ) # next face row follows different rule currVert = numBorder+wide+5 self.tris['margin'].append( (currVert+1, currVert, currVert + mrgCols) ) self.tris['margin'].append( (currVert + mrgCols, currVert + mrgCols+1, currVert+1) ) currVert += wide+2 self.tris['margin'].append( (currVert+1, currVert, currVert+4) ) self.tris['margin'].append( (currVert+4, currVert+5, currVert+1) ) # intermediate rows are paired currVert = numBorder + mrgCols * 2 for i in range(tall-1): self.tris['margin'].append( (currVert+2, currVert+1, currVert+5) ) self.tris['margin'].append( (currVert+5, currVert+6, currVert+2) ) self.tris['margin'].append( (currVert+4, currVert+3, currVert+7) ) self.tris['margin'].append( (currVert+7, currVert+8, currVert+4) ) currVert += 4 # next face follows different rule self.tris['margin'].append( (currVert+2, currVert+1, currVert+5) ) self.tris['margin'].append( (currVert+5, currVert+6, currVert+2) ) self.tris['margin'].append( (currVert+4, currVert+3, currVert+3+mrgCols) ) self.tris['margin'].append( (currVert+3+mrgCols, currVert+4+mrgCols, currVert+4) ) # last two rows are matched in pairs finInter = numBorder + numMargin - mrgCols * 2 for i in range(finInter + 1, finInter + mrgCols): self.tris['margin'].append( (i+1, i, i+mrgCols) ) self.tris['margin'].append( (i+mrgCols, i+mrgCols+1, i+1) ) if self.logTime: curTime += self.estTime('marginF') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if self.useTiming: self.timeLog.append( (time.time(), 'border join') ) # join margin to border # top row currBorder = numCols + 2 currMargin = numBorder + 1 for i in range(wide + 3): self.tris['margin'].append( (currBorder+1, currBorder, currMargin) ) self.tris['margin'].append( (currMargin, currMargin+1, currBorder+1) ) currBorder += 1 currMargin += 1 # each side # first on left and right currBorder = numCols + 2 currMargin = numBorder + 1 nextBorder = currBorder + numCols nextMargin = currMargin + mrgCols self.tris['margin'].append( (currBorder, nextBorder, nextMargin) ) self.tris['margin'].append( (nextMargin, currMargin, currBorder) ) self.tris['margin'].append( (nextBorder+1, currBorder+mrgCols-1, currMargin+mrgCols-1) ) self.tris['margin'].append( (currMargin+mrgCols-1, nextMargin+mrgCols-1, nextBorder+1) ) # currBorder = nextBorder currMargin = nextMargin nextBorder += 4 nextMargin += mrgCols # second on left and right nextBorder = currBorder+4 nextMargin = currMargin+mrgCols self.tris['margin'].append( (currBorder, nextBorder, nextMargin) ) self.tris['margin'].append( (nextMargin, currMargin, currBorder) ) self.tris['margin'].append( (nextBorder+1, currBorder+1, currMargin+mrgCols-1) ) self.tris['margin'].append( (currMargin+mrgCols-1, nextMargin+3, nextBorder+1) ) currBorder = nextBorder currMargin = nextMargin nextBorder += 4 nextMargin += 4 for i in range(tall-1): self.tris['margin'].append( (currBorder, currBorder+4, currMargin+4) ) self.tris['margin'].append( (currMargin+4, currMargin, currBorder) ) self.tris['margin'].append( (currBorder+1, currMargin+3, currMargin+7) ) self.tris['margin'].append( (currMargin+7, currBorder+5, currBorder+1) ) currBorder += 4 currMargin += 4 # next to last on left and right self.tris['margin'].append( (currBorder, currBorder+4, currMargin+4) ) self.tris['margin'].append( (currMargin+4, currMargin, currBorder) ) self.tris['margin'].append( (currBorder+1, currMargin+3, currMargin+3+mrgCols) ) self.tris['margin'].append( (currMargin+3+mrgCols, currBorder+5, currBorder+1) ) # last on left and right currBorder += 4 currMargin += 4 self.tris['margin'].append( (currBorder, currBorder+4, currMargin+mrgCols) ) self.tris['margin'].append( (currMargin+mrgCols, currMargin, currBorder) ) self.tris['margin'].append( (currBorder+1, currMargin+mrgCols-1, currMargin+mrgCols*2-1) ) self.tris['margin'].append( (currMargin+mrgCols*2-1, currBorder+1+numCols, currBorder+1) ) # bottom row currBorder += 4 currMargin += mrgCols for i in range(wide + 3): self.tris['margin'].append( (currBorder+i+1, currMargin+i+1, currMargin+i) ) self.tris['margin'].append( (currMargin+i, currBorder+i, currBorder+i+1) ) if len(self.tris['margin']) != statsFace['margin']: raise ValueError, "found %s margin faces when it should have been %s" % (len(self.tris['margin']), statsFace['margin']) print "margin faces:", len(self.tris['margin']) if self.logTime: curTime = self.estTime('borderMargin') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if self.useTiming: self.timeLog.append( (time.time(), 'image') ) # now the main loop to get image data down currOffset = numBorder + numMargin self.tris['image'] = [] self.contrastSub = 0 self.contrastMult = 1.0 if self.enhanceContrast: if self.logTime: cTime = self.estTime('imageC') minVal = 255 maxVal = 0 for row in range(self.image.height()): for col in range(self.image.width()): light = QColor(self.image.pixel(col, row)).lightness() if light < minVal: minVal = light if light > maxVal: maxVal = light if self.logTime: curTime += cTime / tall print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) rngVal = maxVal - minVal self.contrastMult = 255 / float(maxVal - minVal) self.contrastSub = minVal imageTime = self.estTime('imageV') timeColor = 0 time lastTime = 0 # self.minDia_px is the minimum diameter for the top surface, # that is any feature smaller than that must be either lowered # a level or widened for y in range(tall): row = y if self.doubleRes: row /= 2 i = y + 1 for x in range(wide): col = x if self.doubleRes: col /= 2 j = x + 1 layer = self.getLayer(col, row) self.vertices.append( (self.breadth_mm + i / self.vpmm, self.breadth_mm + j / self.vpmm, layer * self.thickness) ) # do faces if y < tall - 1 and x < wide - 1: currVert = currOffset + y * wide + x + 1 lowV = 0 lowLayer = layer if self.getLayer(col + 1, row) < lowLayer: lowV = 1 lowLayer = self.getLayer(col + 1, row) if self.getLayer(col, row + 1) < lowLayer: lowV = 2 lowLayer = self.getLayer(col, row + 1) if self.getLayer(col + 1, row + 1) < lowLayer: lowV = 3 if lowV == 1 or lowV == 2: self.tris['image'].append( (currVert, currVert+wide, currVert+wide+1) ) self.tris['image'].append( (currVert+wide+1, currVert+1, currVert) ) else: self.tris['image'].append( (currVert, currVert+wide, currVert+1) ) self.tris['image'].append( (currVert+wide, currVert+wide+1, currVert+1) ) if self.logTime: curTime += imageTime / tall print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) numImage = len(self.vertices) - numBorder - numMargin if numImage != statsVert['image']: raise ValueError, "found %s image vertices when it should have been %s" % (numImage, statsVert['image']) print "image vertices:", numImage if self.logTime: #curTime += self.estTime('image') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) # join margin to main image if self.border > 0: if self.useTiming: self.timeLog.append( (time.time(), 'extend image') ) # top and bottom startMargin = numBorder + mrgCols + 1 startImage = numBorder + numMargin lastMargin = startImage - mrgCols * 2 lastImage = startImage + numImage - wide for j in range(wide - 1): currImageT = startImage + j currImageB = lastImage + j currMarginT = startMargin + j + 1 currMarginB = lastMargin + j self.tris['image'].append( (currImageT+1, currImageT+2, currMarginT+2) ) self.tris['image'].append( (currMarginT+2, currMarginT+1, currImageT+1) ) self.tris['image'].append( (currImageB+2, currImageB+1, currMarginB+3) ) self.tris['image'].append( (currMarginB+3, currMarginB+4, currImageB+2) ) # sides currMargin = startMargin + mrgCols + 1 currImage = startImage + 1 for i in range(tall - 1): self.tris['image'].append( (currMargin, currMargin+4, currImage+wide) ) self.tris['image'].append( (currImage+wide, currImage, currMargin) ) self.tris['image'].append( (currMargin+1, currImage+wide-1, currImage+wide*2-1) ) self.tris['image'].append( (currImage+wide*2-1, currMargin+5, currMargin+1) ) currMargin += 4 currImage += wide # corners self.tris['image'].append( (startMargin+2, startMargin+1, startMargin+mrgCols+1) ) self.tris['image'].append( (startMargin+mrgCols+1, startImage+1, startMargin+2) ) self.tris['image'].append( (startImage+wide, startMargin+mrgCols+2, startMargin+mrgCols-2) ) self.tris['image'].append( (startMargin+mrgCols-2, startMargin+mrgCols-3, startImage+wide) ) self.tris['image'].append( (lastMargin-2, lastMargin+2, lastMargin+3) ) self.tris['image'].append( (lastMargin+3, lastImage+1, lastMargin-2) ) self.tris['image'].append( (lastMargin+mrgCols-2, lastMargin+mrgCols-1, lastMargin-1) ) self.tris['image'].append( (lastMargin-1, startImage+numImage, lastMargin+mrgCols-2) ) if len(self.tris['image']) != statsFace['image']: raise ValueError, "found %s image faces when it should have been %s" % (len(self.tris['image']), statsFace['image']) print "image faces:", len(self.tris['image']) if self.logTime: curTime += self.estTime('extend') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) currOffset += numImage # finally generate back vertices if self.useTiming: self.timeLog.append( (time.time(), 'back vertices and faces') ) self.tris['back'] = [] print "height:", self.height_mm print "width:", self.width_mm backOffset = 0 - self.backThickness * self.thickness for i in range(numRows): row = float(i) / (numRows - 1) * self.height_mm for j in range(numCols): col = float(j) / (numCols - 1) * self.width_mm self.vertices.append( (row, col, backOffset) ) if i < numRows - 1 and j < numCols - 1: # record face of back currVert = currOffset + i * numCols + j + 1 self.tris['back'].append( (currVert+numCols, currVert, currVert+1) ) self.tris['back'].append( (currVert+1, currVert+numCols+1, currVert+numCols) ) numBack = len(self.vertices) - numBorder - numMargin - numImage if numBack != statsVert['back']: raise ValueError, "found %s back vertices when it should have been %s" % (numBack, statsVert['back']) print "back vertices:", numBack if len(self.tris['back']) != statsFace['back']: raise ValueError, "found %s image faces when it should have been %s" % (len(self.tris['back']), statsFace['back']) print "back faces:", len(self.tris['back']) if self.logTime: curTime += self.estTime('back') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) # ultimately, join back to either front or border if self.useTiming: self.timeLog.append( (time.time(), 'define edge') ) self.tris['edge'] = [] if self.border > 0: # top and bottom startBack = numBorder + numMargin + numImage lastBack = startBack + numBack - numCols lastBorder = numBorder - numCols currBorder = 0 for j in range(numCols - 1): currBorder = j + 1 currBack = startBack + currBorder lastBack += 1 lastBorder += 1 self.tris['edge'].append( (currBack+1, currBack, currBorder) ) self.tris['edge'].append( (currBorder, currBorder+1, currBack+1) ) self.tris['edge'].append( (lastBack, lastBack+1, lastBorder+1) ) self.tris['edge'].append( (lastBorder+1, lastBorder, lastBack) ) # sides currBorder = 1 for i in range(numRows - 1): currBack = startBack + i * numCols + 1 brdCols = numCols if i > 1 and i < numRows - 2: brdCols = 4 self.tris['edge'].append( (currBorder+brdCols, currBorder, currBack) ) self.tris['edge'].append( (currBack, currBack+numCols, currBorder+brdCols) ) currBack += numCols - 1 currBorder += brdCols - 1 if i == 1: brdCols = 4 elif i == numRows - 3: brdCols = numCols self.tris['edge'].append( (currBorder, currBorder+brdCols, currBack+numCols) ) self.tris['edge'].append( (currBack+numCols, currBack, currBorder) ) currBack += 1 currBorder += 1 elif numImage == numBack: # top and bottom startImage = numBorder + numMargin startBack = startImage + numImage lastImage = startBack - numCols lastBack = startBack + numBack - numCols for j in range(numCols - 1): currBack = startBack + j currImage = startImage + j lastBack += 1 lastImage += 1 self.tris['edge'].append( (currBack+2, currBack+1, currImage+1) ) self.tris['edge'].append( (currImage+1, currImage+2, currBack+2) ) self.tris['edge'].append( (lastBack, lastBack+1, lastImage+1) ) self.tris['edge'].append( (lastImage+1, lastImage, lastBack) ) # sides for i in range(numRows - 1): currImage = startImage + i * numCols + 1 currBack = currImage + numImage self.tris['edge'].append( (currImage+numCols, currImage, currBack) ) self.tris['edge'].append( (currBack, currBack+numCols, currImage+numCols) ) currImage += numCols - 1 currBack = currImage + numImage self.tris['edge'].append( (currImage, currImage+numCols, currBack+numCols) ) self.tris['edge'].append( (currBack+numCols, currBack, currImage) ) else: print "no border, but front and back have different number of vertices!" if len(self.tris['edge']) != statsFace['edge']: raise ValueError, "found %s edge faces when it should have been %s" % (len(self.tris['edge']), statsFace['edge']) print "edge faces:", len(self.tris['edge']) if self.logTime: curTime += self.estTime('edge') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if self.useTiming: self.timeLog.append( (time.time(), 'done!') ) self.meshProgress.emit(totTime) lastTime = 0 lastMsg = None for timeLog in self.timeLog: (currTime, msg) = timeLog tDelta = (currTime - lastTime) * 1000 if lastMsg: print "%s in \t%s milliseconds" % (lastMsg, tDelta) lastTime = currTime lastMsg = msg self.stateChanged.emit('mesh generated in %s milliseconds' % currTime) # timing for flat mesh generation (3264x1836) # start/prep: 0.03ms -> weight 0.030 # border vertices: 504ms -> weight 0.050 # border faces: 10ms -> weight 0.001 # margin vertices: 469ms -> weight 0.045 # margin faces: 8ms -> weight 0.001 # border join: 8ms -> weight 0.001 # image: 353s -> weight 0.060 # actually two components (estimated) # image contrast adjustment -> weight 0.006 # image vertices and faces -> weight 0.054 # extend image: 8ms -> weight 0.001 # back: 33s -> weight 0.006 # edge: 9ms -> weight 0.001 # so the cost estimate for an image is: # 0.030 + # 0.050 * (2x + 2y) + # 0.001 * (2x + 2y) + # 0.045 * (2x + 2y) + # 0.001 * (2x + 2y) + # 0.001 * (2x + 2y) + # 0.060 * (x * y) + # 0.001 * (2x + 2y) + # 0.006 * (x * y) + # 0.001 * (2x + 2y) # resulting in: # 0.030 + 0.1 * (2x + 2y) + 0.066 * (x * y) # without borders this is: # 0.030 + 0.001 * (2x + 2y) + 0.066 * (x * y) def generateMesh_Quint(self): # Each pixel in the image is pushed as a vertex, but between pixel rows insert # are inserted vertex rows such that each vertex's Z is the average of the # four neighboring pixels. Define these following the image vertices then each # triangle is comprised of two adjacent vertices plus one from the "quint" # group. Rows are considered in pairs for this as follow: # v0 = r0,0 # v1 = r0,1 # v2 = q0 # v3 = r1,0 # v4 = r1,1 # t0 = v2, v1, v0 # t1 = v3, v2, v0 # t2 = v2, v3, v4 # t4 = v1, v2, v4 totTime = self.estTime() (statsVert, statsFace) = self.getMeshStats() self.meshTotal.emit(totTime) curTime = 0 if self.useTiming: self.timeLog = [] self.timeLog.append( (time.time(), 'start') ) if not self.image: return False self.vertices = [] # row, column, height self.faces = [] numBorder = 0 numMargin = 0 tall = self.height_image wide = self.width_image totWidth = self.width_mm totHeight = self.height_mm numRows = self.height_vert numCols = self.width_vert if self.border > 0: if self.logTime: curTime = self.estTime('start') self.meshProgress.emit(curTime) if self.useTiming: self.timeLog.append( (time.time(), 'border vertices') ) # should not need to redefine totHeight, already accounts for border #totHeight = self.breadth_mm + (tall + 1) / self.vpmm + self.breadth_mm # do border vertices of top face in row order for row in range(numRows): i = row if row == 0: pass elif row == 1: i = self.border_mm elif row == 2: i = self.breadth_mm elif row == numRows - 3: i = self.breadth_mm + (tall + 1) / self.vpmm elif row == numRows - 2: i = self.breadth_mm + (tall + 1) / self.vpmm + self.margin_mm elif row == numRows - 1: i = self.breadth_mm + (tall + 1) / self.vpmm + self.breadth_mm else: i = self.breadth_mm + (i - 2) / self.vpmm self.vertices.append( (i, 0, self.depth) ) self.vertices.append( (i, self.border_mm, self.depth) ) if row < 2 or row > numRows - 3: self.vertices.append( (i, self.breadth_mm, self.depth) ) for j in range(self.width_image): self.vertices.append( (i, self.breadth_mm + (j + 1) / self.vpmm, self.depth) ) self.vertices.append( (i, self.breadth_mm + (wide + 1) / self.vpmm, self.depth) ) self.vertices.append( (i, self.breadth_mm + (wide + 1) / self.vpmm + self.margin_mm, self.depth) ) self.vertices.append( (i, self.breadth_mm + (wide + 1) / self.vpmm + self.breadth_mm, self.depth) ) numBorder = len(self.vertices) if numBorder != statsVert['border']: raise ValueError, "found %s border vertices when it should have been %s" % (numBorder, statsVert['border']) print "border vertices:", numBorder if self.logTime: curTime += self.estTime('borderV') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if self.useTiming: self.timeLog.append( (time.time(), 'border faces') ) # generate faces for border self.tris['border'] = [] # first two vertex rows are matched in pairs for i in range(numCols - 1): self.tris['border'].append( (i+2, i+1, i+numCols+1) ) self.tris['border'].append( (i+numCols+1, i+numCols+2, i+2) ) # next face row follows different rule self.tris['border'].append( (numCols+2, numCols+1, numCols * 2 + 1) ) self.tris['border'].append( (numCols * 2 + 1, numCols * 2+2, numCols+2) ) currVert = numCols * 2 - 1 self.tris['border'].append( (currVert+1, currVert, currVert+4) ) self.tris['border'].append( (currVert+4, currVert+5, currVert+1) ) # intermediate rows are paired currVert = numCols * 2 for i in range(tall+1): self.tris['border'].append( (currVert+2, currVert+1, currVert+5) ) self.tris['border'].append( (currVert+5, currVert+6, currVert+2) ) self.tris['border'].append( (currVert+4, currVert+3, currVert+7) ) self.tris['border'].append( (currVert+7, currVert+8, currVert+4) ) currVert += 4 # next face row follows different rule self.tris['border'].append( (currVert+2, currVert+1, currVert+5) ) self.tris['border'].append( (currVert+5, currVert+6, currVert+2) ) self.tris['border'].append( (currVert+4, currVert+3, currVert+3+numCols) ) self.tris['border'].append( (currVert+3+numCols, currVert+4+numCols, currVert+4) ) # last two rows are matched in pairs finInter = numBorder - numCols * 2 for i in range(finInter + 1, finInter + numCols): self.tris['border'].append( (i+1, i, i+numCols) ) self.tris['border'].append( (i+numCols, i+numCols+1, i+1) ) if self.logTime: curTime += self.estTime('borderF') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if len(self.tris['border']) != statsFace['border']: raise ValueError, "found %s border faces when it should have been %s" % (len(self.tris['border']), statsFace['border']) print "border faces:", len(self.tris['border']) if self.useTiming: self.timeLog.append( (time.time(), 'margin vertices') ) mrgRows = numRows - 2 mrgCols = numCols - 2 # do margin vertices of top face in row order # the first and last two rows include vertices to match the image width for row in range(mrgRows): i = self.breadth_mm + (row - 1) / self.vpmm if row == 0: i = self.border_mm elif row == mrgRows - 1: i = self.breadth_mm + (tall+1) / self.vpmm + self.margin_mm self.vertices.append( (i, self.border_mm, self.marginDepth) ) self.vertices.append( (i, self.breadth_mm, self.marginDepth) ) if row < 2 or row > mrgRows - 3: for j in range(1, wide + 1): self.vertices.append( (i, self.breadth_mm + j / self.vpmm, self.marginDepth) ) self.vertices.append( (i, totWidth - self.breadth_mm, self.marginDepth) ) self.vertices.append( (i, totWidth - self.border_mm, self.marginDepth) ) numMargin = len(self.vertices) - numBorder if numMargin != statsVert['margin']: raise ValueError, "found %s margin vertices when it should have been %s" % (numMargin, statsVert['margin']) print "margin vertices:", numMargin if self.logTime: curTime += self.estTime('marginV') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if self.useTiming: self.timeLog.append( (time.time(), 'margin faces') ) # generate faces for margin self.tris['margin'] = [] # first two vertex rows are matched in pairs for i in range(numBorder, numBorder + wide + 3): self.tris['margin'].append( (i+2, i+1, i+mrgCols+1) ) self.tris['margin'].append( (i+mrgCols+1, i+mrgCols+2, i+2) ) # next face row follows different rule currVert = numBorder+wide+5 self.tris['margin'].append( (currVert+1, currVert, currVert + mrgCols) ) self.tris['margin'].append( (currVert + mrgCols, currVert + mrgCols+1, currVert+1) ) currVert += wide+2 self.tris['margin'].append( (currVert+1, currVert, currVert+4) ) self.tris['margin'].append( (currVert+4, currVert+5, currVert+1) ) # intermediate rows are paired currVert = numBorder + mrgCols * 2 for i in range(tall-1): self.tris['margin'].append( (currVert+2, currVert+1, currVert+5) ) self.tris['margin'].append( (currVert+5, currVert+6, currVert+2) ) self.tris['margin'].append( (currVert+4, currVert+3, currVert+7) ) self.tris['margin'].append( (currVert+7, currVert+8, currVert+4) ) currVert += 4 # next face follows different rule self.tris['margin'].append( (currVert+2, currVert+1, currVert+5) ) self.tris['margin'].append( (currVert+5, currVert+6, currVert+2) ) self.tris['margin'].append( (currVert+4, currVert+3, currVert+3+mrgCols) ) self.tris['margin'].append( (currVert+3+mrgCols, currVert+4+mrgCols, currVert+4) ) # last two rows are matched in pairs finInter = numBorder + numMargin - mrgCols * 2 for i in range(finInter + 1, finInter + mrgCols): self.tris['margin'].append( (i+1, i, i+mrgCols) ) self.tris['margin'].append( (i+mrgCols, i+mrgCols+1, i+1) ) if self.logTime: curTime += self.estTime('marginF') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if self.useTiming: self.timeLog.append( (time.time(), 'border join') ) # join margin to border # top row currBorder = numCols + 2 currMargin = numBorder + 1 for i in range(wide + 3): self.tris['margin'].append( (currBorder+1, currBorder, currMargin) ) self.tris['margin'].append( (currMargin, currMargin+1, currBorder+1) ) currBorder += 1 currMargin += 1 # each side # first on left and right currBorder = numCols + 2 currMargin = numBorder + 1 nextBorder = currBorder + numCols nextMargin = currMargin + mrgCols self.tris['margin'].append( (currBorder, nextBorder, nextMargin) ) self.tris['margin'].append( (nextMargin, currMargin, currBorder) ) self.tris['margin'].append( (nextBorder+1, currBorder+mrgCols-1, currMargin+mrgCols-1) ) self.tris['margin'].append( (currMargin+mrgCols-1, nextMargin+mrgCols-1, nextBorder+1) ) # currBorder = nextBorder currMargin = nextMargin nextBorder += 4 nextMargin += mrgCols # second on left and right nextBorder = currBorder+4 nextMargin = currMargin+mrgCols self.tris['margin'].append( (currBorder, nextBorder, nextMargin) ) self.tris['margin'].append( (nextMargin, currMargin, currBorder) ) self.tris['margin'].append( (nextBorder+1, currBorder+1, currMargin+mrgCols-1) ) self.tris['margin'].append( (currMargin+mrgCols-1, nextMargin+3, nextBorder+1) ) currBorder = nextBorder currMargin = nextMargin nextBorder += 4 nextMargin += 4 for i in range(tall-1): self.tris['margin'].append( (currBorder, currBorder+4, currMargin+4) ) self.tris['margin'].append( (currMargin+4, currMargin, currBorder) ) self.tris['margin'].append( (currBorder+1, currMargin+3, currMargin+7) ) self.tris['margin'].append( (currMargin+7, currBorder+5, currBorder+1) ) currBorder += 4 currMargin += 4 # next to last on left and right self.tris['margin'].append( (currBorder, currBorder+4, currMargin+4) ) self.tris['margin'].append( (currMargin+4, currMargin, currBorder) ) self.tris['margin'].append( (currBorder+1, currMargin+3, currMargin+3+mrgCols) ) self.tris['margin'].append( (currMargin+3+mrgCols, currBorder+5, currBorder+1) ) # last on left and right currBorder += 4 currMargin += 4 self.tris['margin'].append( (currBorder, currBorder+4, currMargin+mrgCols) ) self.tris['margin'].append( (currMargin+mrgCols, currMargin, currBorder) ) self.tris['margin'].append( (currBorder+1, currMargin+mrgCols-1, currMargin+mrgCols*2-1) ) self.tris['margin'].append( (currMargin+mrgCols*2-1, currBorder+1+numCols, currBorder+1) ) # bottom row currBorder += 4 currMargin += mrgCols for i in range(wide + 3): self.tris['margin'].append( (currBorder+i+1, currMargin+i+1, currMargin+i) ) self.tris['margin'].append( (currMargin+i, currBorder+i, currBorder+i+1) ) if len(self.tris['margin']) != statsFace['margin']: raise ValueError, "found %s margin faces when it should have been %s" % (len(self.tris['margin']), statsFace['margin']) print "margin faces:", len(self.tris['margin']) if self.logTime: curTime = self.estTime('borderMargin') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if self.useTiming: self.timeLog.append( (time.time(), 'image') ) # now the main loop to get image data down currOffset = numBorder + numMargin self.tris['image'] = [] self.contrastSub = 0 self.contrastMult = 1.0 if self.enhanceContrast: if self.logTime: cTime = self.estTime('imageC') minVal = 255 maxVal = 0 for row in range(self.image.height()): for col in range(self.image.width()): light = QColor(self.image.pixel(col, row)).lightness() if light < minVal: minVal = light if light > maxVal: maxVal = light if self.logTime: curTime += cTime / tall print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) rngVal = maxVal - minVal self.contrastMult = 255 / float(maxVal - minVal) self.contrastSub = minVal imageTime = self.estTime('imageV') timeColor = 0 time lastTime = 0 for y in range(tall): row = y i = y + 1 for x in range(wide): col = x j = x + 1 layer = self.getLayer(col, row) self.vertices.append( (self.breadth_mm + i / self.vpmm, self.breadth_mm + j / self.vpmm, layer * self.thickness) ) if self.logTime: curTime += imageTime / tall print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) numImage = len(self.vertices) - numBorder - numMargin if numImage != statsVert['image']: print "found %s image vertices when it should have been %s" % (numImage, statsVert['image']) #raise ValueError, "found %s image vertices when it should have been %s" % (numImage, statsVert['image']) print "image vertices:", numImage if self.logTime: #curTime += self.estTime('image') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) # join margin to main image if self.border > 0: if self.useTiming: self.timeLog.append( (time.time(), 'extend image') ) # top and bottom startMargin = numBorder + mrgCols + 1 startImage = numBorder + numMargin lastMargin = startImage - mrgCols * 2 lastImage = startImage + numImage - wide for j in range(wide - 1): currImageT = startImage + j currImageB = lastImage + j currMarginT = startMargin + j + 1 currMarginB = lastMargin + j self.tris['image'].append( (currImageT+1, currImageT+2, currMarginT+2) ) self.tris['image'].append( (currMarginT+2, currMarginT+1, currImageT+1) ) self.tris['image'].append( (currImageB+2, currImageB+1, currMarginB+3) ) self.tris['image'].append( (currMarginB+3, currMarginB+4, currImageB+2) ) # sides currMargin = startMargin + mrgCols + 1 currImage = startImage + 1 for i in range(tall - 1): self.tris['image'].append( (currMargin, currMargin+4, currImage+wide) ) self.tris['image'].append( (currImage+wide, currImage, currMargin) ) self.tris['image'].append( (currMargin+1, currImage+wide-1, currImage+wide*2-1) ) self.tris['image'].append( (currImage+wide*2-1, currMargin+5, currMargin+1) ) currMargin += 4 currImage += wide # corners self.tris['image'].append( (startMargin+2, startMargin+1, startMargin+mrgCols+1) ) self.tris['image'].append( (startMargin+mrgCols+1, startImage+1, startMargin+2) ) self.tris['image'].append( (startImage+wide, startMargin+mrgCols+2, startMargin+mrgCols-2) ) self.tris['image'].append( (startMargin+mrgCols-2, startMargin+mrgCols-3, startImage+wide) ) self.tris['image'].append( (lastMargin-2, lastMargin+2, lastMargin+3) ) self.tris['image'].append( (lastMargin+3, lastImage+1, lastMargin-2) ) self.tris['image'].append( (lastMargin+mrgCols-2, lastMargin+mrgCols-1, lastMargin-1) ) self.tris['image'].append( (lastMargin-1, startImage+numImage, lastMargin+mrgCols-2) ) print "image faces (preliminary):", len(self.tris['image']) if self.logTime: curTime += self.estTime('extend') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) # add "quints" (central vertex) and image faces quintTime = self.estTime('imageV') # FIXME for y in range(tall - 1): col = y i = y + 1.5 for x in range(wide - 1): v0 = currOffset + y * wide + x + 1 v1 = v0 + 1 v2 = currOffset + y * (wide - 1) + x + numImage + 1 v3 = v0 + wide v4 = v0 + wide + 1 # row = x j = x + 1.5 #print self.vertices[v0] z = (self.vertices[v0][2] + self.vertices[v1][2] + self.vertices[v3][2] + self.vertices[v4][2]) / 4 if self.vertices[v0][2] == self.vertices[v1][2] or self.vertices[v0][2] == self.vertices[v3][2] or self.vertices[v1][2] == self.vertices[v4][2] or self.vertices[v3][2] == self.vertices[v4][2]: if (x+y)/2 == (x+y)/2.0: z += self.thickness / 2 else: z -= self.thickness / 2 # append quint self.vertices.append( (self.breadth_mm + i / self.vpmm, self.breadth_mm + j / self.vpmm, z) ) # define triangles... self.tris['image'].append((v2, v1, v0)) self.tris['image'].append((v3, v2, v0)) self.tris['image'].append((v2, v3, v4)) self.tris['image'].append((v1, v2, v4)) if self.logTime: curTime += quintTime / tall print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) numQuints = len(self.vertices) - numBorder - numMargin - numImage expQuints = (tall - 1) * (wide - 1) if numQuints != expQuints: raise ValueError, "found %s quint vertices when it should have been %s" % (numQuints, expQuints) print "quint vertices:", numQuints if self.logTime: #curTime += self.estTime('quints') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if len(self.tris['image']) != statsFace['image']: print "found %s image faces when it should have been %s" % (len(self.tris['image']), statsFace['image']) #raise ValueError, "found %s image faces when it should have been %s" % (len(self.tris['image']), statsFace['image']) print "image faces:", len(self.tris['image']) currOffset += numImage + numQuints # finally generate back vertices if self.useTiming: self.timeLog.append( (time.time(), 'back vertices and faces') ) self.tris['back'] = [] print "height:", self.height_mm print "width:", self.width_mm backOffset = 0 - self.backThickness * self.thickness for i in range(numRows): row = float(i) / (numRows - 1) * self.height_mm for j in range(numCols): col = float(j) / (numCols - 1) * self.width_mm self.vertices.append( (row, col, backOffset) ) if i < numRows - 1 and j < numCols - 1: # record face of back currVert = currOffset + i * numCols + j + 1 self.tris['back'].append( (currVert+numCols, currVert, currVert+1) ) self.tris['back'].append( (currVert+1, currVert+numCols+1, currVert+numCols) ) numBack = len(self.vertices) - numBorder - numMargin - numImage - numQuints if numBack != statsVert['back']: raise ValueError, "found %s back vertices when it should have been %s" % (numBack, statsVert['back']) print "back vertices:", numBack if len(self.tris['back']) != statsFace['back']: print "found %s image faces when it should have been %s" % (len(self.tris['back']), statsFace['back']) #raise ValueError, "found %s image faces when it should have been %s" % (len(self.tris['back']), statsFace['back']) print "back faces:", len(self.tris['back']) if self.logTime: curTime += self.estTime('back') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) # ultimately, join back to either front or border if self.useTiming: self.timeLog.append( (time.time(), 'define edge') ) self.tris['edge'] = [] if self.border > 0: # top and bottom startBack = numBorder + numMargin + numImage + numQuints lastBack = startBack + numBack - numCols lastBorder = numBorder - numCols currBorder = 0 for j in range(numCols - 1): currBorder = j + 1 currBack = startBack + currBorder lastBack += 1 lastBorder += 1 self.tris['edge'].append( (currBack+1, currBack, currBorder) ) self.tris['edge'].append( (currBorder, currBorder+1, currBack+1) ) self.tris['edge'].append( (lastBack, lastBack+1, lastBorder+1) ) self.tris['edge'].append( (lastBorder+1, lastBorder, lastBack) ) # sides currBorder = 1 for i in range(numRows - 1): currBack = startBack + i * numCols + 1 brdCols = numCols if i > 1 and i < numRows - 2: brdCols = 4 self.tris['edge'].append( (currBorder+brdCols, currBorder, currBack) ) self.tris['edge'].append( (currBack, currBack+numCols, currBorder+brdCols) ) currBack += numCols - 1 currBorder += brdCols - 1 if i == 1: brdCols = 4 elif i == numRows - 3: brdCols = numCols self.tris['edge'].append( (currBorder, currBorder+brdCols, currBack+numCols) ) self.tris['edge'].append( (currBack+numCols, currBack, currBorder) ) currBack += 1 currBorder += 1 elif numImage == numBack: # top and bottom startImage = numBorder + numMargin startBack = startImage + numImage lastImage = startBack - numCols lastBack = startBack + numBack - numCols for j in range(numCols - 1): currBack = startBack + j currImage = startImage + j lastBack += 1 lastImage += 1 self.tris['edge'].append( (currBack+2, currBack+1, currImage+1) ) self.tris['edge'].append( (currImage+1, currImage+2, currBack+2) ) self.tris['edge'].append( (lastBack, lastBack+1, lastImage+1) ) self.tris['edge'].append( (lastImage+1, lastImage, lastBack) ) # sides for i in range(numRows - 1): currImage = startImage + i * numCols + 1 currBack = currImage + numImage self.tris['edge'].append( (currImage+numCols, currImage, currBack) ) self.tris['edge'].append( (currBack, currBack+numCols, currImage+numCols) ) currImage += numCols - 1 currBack = currImage + numImage self.tris['edge'].append( (currImage, currImage+numCols, currBack+numCols) ) self.tris['edge'].append( (currBack+numCols, currBack, currImage) ) else: print "no border, but front and back have different number of vertices!" if len(self.tris['edge']) != statsFace['edge']: raise ValueError, "found %s edge faces when it should have been %s" % (len(self.tris['edge']), statsFace['edge']) print "edge faces:", len(self.tris['edge']) if self.logTime: curTime += self.estTime('edge') print "%2.2f%%" % (100 * curTime / totTime) self.meshProgress.emit(curTime) if self.useTiming: self.timeLog.append( (time.time(), 'done!') ) self.meshProgress.emit(totTime) lastTime = 0 lastMsg = None for timeLog in self.timeLog: (currTime, msg) = timeLog tDelta = (currTime - lastTime) * 1000 if lastMsg: print "%s in \t%s milliseconds" % (lastMsg, tDelta) lastTime = currTime lastMsg = msg self.stateChanged.emit('mesh generated in %s milliseconds' % currTime) # timing for flat mesh generation (3264x1836) # start/prep: 0.03ms -> weight 0.030 # border vertices: 504ms -> weight 0.050 # border faces: 10ms -> weight 0.001 # margin vertices: 469ms -> weight 0.045 # margin faces: 8ms -> weight 0.001 # border join: 8ms -> weight 0.001 # image: 353s -> weight 0.060 # actually two components (estimated) # image contrast adjustment -> weight 0.006 # image vertices and faces -> weight 0.054 # extend image: 8ms -> weight 0.001 # back: 33s -> weight 0.006 # edge: 9ms -> weight 0.001 # so the cost estimate for an image is: # 0.030 + # 0.050 * (2x + 2y) + # 0.001 * (2x + 2y) + # 0.045 * (2x + 2y) + # 0.001 * (2x + 2y) + # 0.001 * (2x + 2y) + # 0.060 * (x * y) + # 0.001 * (2x + 2y) + # 0.006 * (x * y) + # 0.001 * (2x + 2y) # resulting in: # 0.030 + 0.1 * (2x + 2y) + 0.066 * (x * y) # without borders this is: # 0.030 + 0.001 * (2x + 2y) + 0.066 * (x * y) def estTime(self, which='all'): if not self.image: return x = self.image.width() y = self.image.height() if which == 'start': return 0.03 elif which == 'borderV': return 0.05 * 2 * (x + y) elif which == 'borderF': return 0.001 * 2 * (x + y) elif which == 'border': return 0.051 * 2 * (x + y) elif which == 'marginV': return 0.045 * 2 * (x + y) elif which == 'marginF': return 0.001 * 2 * (x + y) elif which == 'margin': return 0.046 * 2 * (x + y) elif which == 'borderJ': return 0.001 * 2 * (x + y) elif which == 'borderMargin': if self.border > 0: return 0.03 + 0.098 * 2 * (x + y) return 0.03 elif which == 'imageC': return 0.006 * x * y elif which == 'imageV': return 0.054 * x * y elif which == 'image': return 0.06 * x * y elif which == 'extend': return 0.001 * 2 * (x + y) elif which == 'back': return 0.006 * x * y elif which == 'edge': return 0.001 * 2 * (x + y) a = 0.001 if self.border > 0: a = 0.1 return 0.03 + a * 2 * (x + y) + 0.066 * x * y def __getLayer(self, col, row): light = QColor(self.image.pixel(col, row)).lightness() # 0 - 255 if self.enhanceContrast: light = self.contrastMult * (light - self.contrastSub) if self.restrictToLayer: light = int(light) # integer math makes this work out: # the minimum layer will always be 2 with a maximum of self.layers # the possible lightness values are evenly distributed through the possible layers return self.layers - (self.layers - 1) * light / 256 def getLayer(self, col, row): #return self.old_getLayer(col, row) self.__filterLayers() return self.filteredMap[col,row] def __filterLayers(self): # only do this once... if self.filteredLayers: return # initialize height map for col in range(self.image.width()): for row in range(self.image.height()): self.filteredMap[col,row] = self.__getLayer(col,row) # go through height map from top to bottom # per __getLayer() comment, minimum layer is 2 for layer in range(self.layers, 2, -1): print "starting layer", layer layerCount = 0 changed = True while(changed): moveDown = [] for col in range(self.image.width()): for row in range(self.image.height()): if self.filteredMap[col,row] != layer: continue count = 0 for i in range(col - 1, col + 2): if i < 0: continue if i >= self.image.width(): continue for j in range(row - 1, row + 2): if i == col and j == row: continue if j < 0: continue if j >= self.image.height(): continue if self.filteredMap[i,j] >= self.filteredMap[col,row]: count += 1 if count < 3: moveDown.append((col,row)) print "%s pixels to lower" % len(moveDown) if moveDown: for cell in moveDown: self.filteredMap[cell] = layer - 1 else: changed = False layerCount += 1 if layerCount > 99: changed = False print "aborting this layer" print "took %s iterations" % layerCount self.filteredLayers = True def old_getLayer(self, col, row): layer = self.__getLayer(col, row) # look at neighbors and lower level as needed: countN = 0 maxLayer = 0 minLayer = 255 direction = [] for colN in range(col - 1, col + 2): if colN < 0: continue elif colN >= self.image.width(): continue for rowN in range(row - 1, row + 2): if rowN == row and colN == col: continue if rowN < 0: continue elif rowN >= self.image.height(): continue layerN = self.__getLayer(colN, rowN) if layerN > maxLayer: maxLayer = layerN if layerN < minLayer: minLayer = layerN if layerN >= layer: countN += 1 direction.append( (colN, rowN) ) if countN == 9: # if all neighbors are greater then raise to their minimum level # this is safe to "raise" because if it is actually raised then # only the four corners could possibly be lowered so it is still # guaranteed to have more than one pixel each axis return maxLayer elif countN >= 5: # guaranteed that at least one side is at least this level # might be lower than neighbors and preferrable to raise, but # by both raising and lowering could be working at odds and # causing problems so just stick with it as not creating a top # level feature of one pixel size return layer elif countN == 0: return minLayer elif countN == 1: # allow one pixel wide lines? no by default return minLayer elif countN == 2: # if adjacent forms a corner, potentially allowed return minLayer # but not at this time (X0, Y0) = direction[0] (X1, Y1) = direction[1] if (X0 == X1 and abs(Y0 - Y1) == 1) or (Y0 == Y1 and abs(X0 - X1) == 1): return layer else: return minLayer elif countN == 3: # if any two form a corner, potentially allowed (X0, Y0) = direction[0] (X1, Y1) = direction[1] (X2, Y2) = direction[2] if (X0 == X1 and abs(Y0 - Y1) == 1) or (Y0 == Y1 and abs(X0 - X1) == 1) or \ (X1 == X2 and abs(Y1 - Y2) == 1) or (Y1 == Y2 and abs(X1 - X2) == 1) or \ (X0 == X2 and abs(Y0 - Y2) == 1) or (Y0 == Y2 and abs(X0 - X2) == 1): return layer else: return minLayer elif countN == 4: # unless all neighbors are spaced (square or diamond) keep this one # (-1,-1), (-1, +1), (+1, -1), (+1, +1) # (0, -1), (0, +1), (-1, 0), (+1, 0) countZero = 0 for (x,y) in direction: if x == 0 or y == 0: countZero += 1 if countZero == 0 or countZero == 4: return minLayer else: return layer class LithophaneWidget(QWidget, QObject): statusUpdate = Signal(str, int) def __init__(self, filename=None, useTabs=True, parent=None): super(LithophaneWidget, self).__init__(parent) self.lockScale = False self.lockWidth = False self.lockHeight = False self.filename = filename self.shortName = None self.lithophane = Lithophane(filename) self.lithophane.stateChanged.connect(self.updateState) self.imagePreviewWidth = 300 self.imagePreviewHeight = 200 # visual elements self.pixmap = QPixmap() self.pixLabel = QLabel() self.pixLabel.setFrameShape(QFrame.Box) self.pixLabel.setFrameShadow(QFrame.Raised) self.pixLabel.setAlignment(Qt.AlignHCenter) self.pixLabel.setPixmap( self.pixmap ) self.pixLabel.setScaledContents(True) pixLayout = QHBoxLayout() pixLayout.addWidget(self.pixLabel) pixLayout.addStretch() visualLayout = QVBoxLayout() visualLayout.addLayout(pixLayout) visualLayout.addStretch() # informational elements self.imageSizeLabel = QLabel() self.imageSizeLabel.setFrameStyle(QFrame.Panel | QFrame.Sunken) self.imageThicknessLabel = QLabel() self.imageThicknessLabel.setFrameStyle(QFrame.Panel | QFrame.Sunken) self.imagePPILabel = QLabel() self.imagePPILabel.setFrameStyle(QFrame.Panel | QFrame.Sunken) self.imageVerticesLabel = QLabel() self.imageVerticesLabel.setFrameStyle(QFrame.Panel | QFrame.Sunken) self.imageFacesLabel = QLabel() self.imageFacesLabel.setFrameStyle(QFrame.Panel | QFrame.Sunken) infoLayout = QHBoxLayout() infoLayout.addWidget(QLabel('Size:')) infoLayout.addWidget(self.imageSizeLabel) infoLayout.addStretch() infoLayout.addWidget(QLabel('Thickness:')) infoLayout.addWidget(self.imageThicknessLabel) infoLayout.addStretch() infoLayout.addWidget(QLabel('Print Resolution:')) infoLayout.addWidget(self.imagePPILabel) infoLayout.addStretch() infoLayout.addWidget(QLabel('Vertices:')) infoLayout.addWidget(self.imageVerticesLabel) infoLayout.addStretch() infoLayout.addWidget(QLabel('Faces:')) infoLayout.addWidget(self.imageFacesLabel) # self.borderSpinBox = QSpinBox() self.borderSpinBox.setMinimum(1) self.borderSpinBox.setSuffix(' px') self.borderSpinBox.setValue( self.lithophane.border ) self.borderSpinBox.valueChanged.connect( self.updateInfo ) border = QHBoxLayout() border.addWidget(QLabel('Border Width')) border.addStretch() border.addWidget(self.borderSpinBox) self.marginWidthSpinBox = QSpinBox() self.marginWidthSpinBox.setMinimum(1) self.marginWidthSpinBox.setSuffix(' px') self.marginWidthSpinBox.setValue( self.lithophane.marginWidth ) self.marginWidthSpinBox.valueChanged.connect( self.updateInfo ) marginWidth = QHBoxLayout() marginWidth.addWidget(QLabel('Margin Width')) marginWidth.addStretch() marginWidth.addWidget(self.marginWidthSpinBox) self.marginThicknessSpinBox = QSpinBox() self.marginThicknessSpinBox.setMinimum(1) self.marginThicknessSpinBox.setSuffix(' layers') self.marginThicknessSpinBox.setValue( self.lithophane.marginThickness ) marginThickness = QHBoxLayout() marginThickness.addWidget(QLabel('Margin Thickness')) marginThickness.addStretch() marginThickness.addWidget(self.marginThicknessSpinBox) self.useBorderCheckBox = QCheckBox() self.useBorderCheckBox.stateChanged.connect(self.enableBorderGroup) useBorder = QHBoxLayout() useBorder.addWidget(QLabel('Use Border?')) useBorder.addStretch() useBorder.addWidget(self.useBorderCheckBox) borderLayout = QVBoxLayout() borderLayout.addLayout(useBorder) borderLayout.addLayout(border) borderLayout.addLayout(marginWidth) borderLayout.addLayout(marginThickness) # self.shapeComboBox = QComboBox() self.shapeComboBox.addItem('Rectangle') #self.shapeComboBox.addItem('Cylinder') #self.shapeComboBox.addItem('Disk') shapeLayout = QHBoxLayout() shapeLayout.addWidget(QLabel('Shape')) shapeLayout.addWidget(self.shapeComboBox) self.methodComboBox = QComboBox() self.methodComboBox.addItem('Simple') self.methodComboBox.addItem('Quints') methodLayout = QHBoxLayout() methodLayout.addWidget(QLabel('Method')) methodLayout.addWidget(self.methodComboBox) self.useDoubleResCheckBox = QCheckBox() self.useDoubleResCheckBox.stateChanged.connect(self.enableDoubleRes) useDoubleRes = QHBoxLayout() useDoubleRes.addWidget(QLabel('Use Double Resolution?')) useDoubleRes.addStretch() useDoubleRes.addWidget(self.useDoubleResCheckBox) meshLayout = QVBoxLayout() meshLayout.addLayout(shapeLayout) meshLayout.addLayout(methodLayout) meshLayout.addLayout(useDoubleRes) meshLayout.addStretch() # self.widthDoubleSpinBox = QDoubleSpinBox() self.widthDoubleSpinBox.setMinimum(0.1) self.widthDoubleSpinBox.setSuffix(' inches') currWidth = 7.0 if self.lithophane.width: currWidth = self.lithophane.width self.widthDoubleSpinBox.setValue( currWidth ) self.widthDoubleSpinBox.valueChanged.connect(self.setHeight) width = QHBoxLayout() width.addWidget(QLabel('Overall Width')) width.addStretch() width.addWidget(self.widthDoubleSpinBox) self.heightDoubleSpinBox = QDoubleSpinBox() self.heightDoubleSpinBox.setMinimum(0.1) self.heightDoubleSpinBox.setSuffix(' inches') currHeight = 5.0 if self.lithophane.width: currHeight = self.lithophane.height self.heightDoubleSpinBox.setValue( currHeight ) self.heightDoubleSpinBox.valueChanged.connect(self.setWidth) height = QHBoxLayout() height.addWidget(QLabel('Overall Height')) height.addStretch() height.addWidget(self.heightDoubleSpinBox) self.layerSpinBox = QSpinBox() self.layerSpinBox.setRange(2,255) self.layerSpinBox.setValue( self.lithophane.layers ) self.layerSpinBox.valueChanged.connect( self.setMaxMargin ) self.marginThicknessSpinBox.setMaximum(self.lithophane.layers) layers = QHBoxLayout() layers.addWidget(QLabel('Number of Layers')) layers.addStretch() layers.addWidget(self.layerSpinBox) self.layerThicknessSpinBox = QDoubleSpinBox() self.layerThicknessSpinBox.setMinimum(0.01) self.layerThicknessSpinBox.setSuffix(' mm') self.layerThicknessSpinBox.setValue( self.lithophane.thickness ) self.layerThicknessSpinBox.valueChanged.connect( self.updateInfo ) self.layerThicknessSpinBox.setSingleStep(0.01) layerThickness = QHBoxLayout() layerThickness.addWidget(QLabel('Layer Thickness')) layerThickness.addStretch() layerThickness.addWidget(self.layerThicknessSpinBox) self.backThicknessSpinBox = QSpinBox() self.backThicknessSpinBox.setMinimum(0) self.backThicknessSpinBox.setSuffix(' layers') self.backThicknessSpinBox.setValue( self.lithophane.backThickness ) self.backThicknessSpinBox.valueChanged.connect( self.updateInfo ) backThickness = QHBoxLayout() backThickness.addWidget(QLabel('Back extra thickness')) backThickness.addStretch() backThickness.addWidget(self.backThicknessSpinBox) sizeLayout = QVBoxLayout() sizeLayout.addLayout(width) sizeLayout.addLayout(height) sizeLayout.addLayout(layers) sizeLayout.addLayout(layerThickness) sizeLayout.addLayout(backThickness) # self.scaleSpinBox = QDoubleSpinBox() self.scaleSpinBox.setRange(0.001, 1000.0) self.scaleSpinBox.setValue(1.0) self.scaleSpinBox.setDecimals(3) self.scaleSpinBox.setSingleStep(0.1) self.scaleSpinBox.valueChanged.connect(self.updateImage) scaleRatio = QHBoxLayout() scaleRatio.addWidget(QLabel('Scale Factor')) scaleRatio.addStretch() scaleRatio.addWidget(self.scaleSpinBox) self.resWidthSpinBox = QSpinBox() self.resWidthSpinBox.setRange(1,214748364) self.resWidthSpinBox.setSingleStep(10) self.resWidthSpinBox.valueChanged.connect(self.updateScaleHeight) self.resHeightSpinBox = QSpinBox() self.resHeightSpinBox.setRange(1,214748364) self.resHeightSpinBox.setSingleStep(10) self.resHeightSpinBox.valueChanged.connect(self.updateScaleWidth) self.useContrastCheckBox = QCheckBox() scaleRes = QHBoxLayout() scaleRes.addWidget(QLabel('Resolution')) scaleRes.addStretch() scaleRes.addWidget(QLabel('Width')) scaleRes.addWidget(self.resWidthSpinBox) scaleRes.addWidget(QLabel('Height')) scaleRes.addWidget(self.resHeightSpinBox) if self.lithophane.enhanceContrast: self.useContrastCheckBox.setCheckState(Qt.Checked) useContrast = QHBoxLayout() useContrast.addWidget(QLabel('Auto Adjust Contrast?')) useContrast.addStretch() useContrast.addWidget(self.useContrastCheckBox) qualLayout = QVBoxLayout() qualLayout.addLayout(scaleRatio) qualLayout.addLayout(scaleRes) qualLayout.addLayout(useContrast) qualLayout.addStretch() # "belongs" higher up, but has interdependency issue if self.lithophane.border > 0: self.useBorderCheckBox.setCheckState(Qt.Checked) # just down here like the other if self.lithophane.doubleRes: self.useDoubleResCheckBox.setCheckState(Qt.Checked) # layout = QHBoxLayout() layout.addLayout(visualLayout) parmLayout = QVBoxLayout() if useTabs: borderGroup = QGroupBox() borderGroup.setLayout(borderLayout) sizeGroup = QGroupBox() sizeGroup.setLayout(sizeLayout) meshGroup = QGroupBox() meshGroup.setLayout(meshLayout) qualGroup = QGroupBox() qualGroup.setLayout(qualLayout) parmTabs = QTabWidget() parmTabs.addTab(sizeGroup, 'Size') parmTabs.addTab(borderGroup, 'Border') parmTabs.addTab(meshGroup, 'Mesh') parmTabs.addTab(qualGroup, 'Miscellaneous') parmLayout.addWidget(parmTabs) else: borderGroup = QGroupBox('Border') borderGroup.setLayout(borderLayout) sizeGroup = QGroupBox('Size') sizeGroup.setLayout(sizeLayout) meshGroup = QGroupBox('Mesh') meshGroup.setLayout(meshLayout) qualGroup = QGroupBox('Miscellaneous') qualGroup.setLayout(qualLayout) parmLayout.addWidget(sizeGroup) parmLayout.addWidget(borderGroup) parmLayout.addWidget(meshGroup) parmLayout.addWidget(qualGroup) parmLayout.addStretch() layout.addLayout(parmLayout) self.progress = QProgressBar() self.progress.setRange(0, 100) self.progressScale = 1.0 self.lithophane.meshTotal.connect(self.setProgressMaximum) self.lithophane.meshProgress.connect(self.setProgressValue) finalLayout = QVBoxLayout() finalLayout.addLayout(layout) finalLayout.addLayout(infoLayout) finalLayout.addWidget(self.progress) self.setLayout(finalLayout) # self.filename = filename self.shortName = None self.lithophane = Lithophane(filename) if self.filename: self.shortName = QFileInfo(filename).fileName() self.load() self.updateInfo() @Slot(int) def setProgressMaximum(self, max): print "setProgressMaximum(%s)" % max if int(max) < 100: self.progressScale = 100 / max max *= 100 self.progress.setMaximum(int(max)) @Slot(float) def setProgressValue(self, value): print "\n\n\t***** setProgressValue(%s)\n\n" % value value = int( value * self.progressScale ) self.progress.setValue(value) QtGui.QApplication.processEvents() def updateState(self, state): self.statusUpdate.emit(state) def load(self, filename=None): if filename: self.filename = filename self.lithophane.load(self.filename) if self.lithophane.image: #self.resizeImage() self.lockScale = True self.lockWidth = True self.resWidthSpinBox.setValue( int(self.lithophane.image.width()) ) self.lockScale = False self.lockWidth = False self.updateImage() self.setHeight(self.widthDoubleSpinBox.value()) def resizeImage(self, width=None, height=None): if width is None: width = self.imagePreviewWidth if height is None: height = self.imagePreviewHeight if self.lithophane.image: self.pixmap.convertFromImage( self.lithophane.image ) self.pixLabel.setPixmap( self.pixmap.scaled( QSize(width, height), Qt.KeepAspectRatio ) ) def setParameters(self): if self.useBorderCheckBox.checkState() == Qt.Checked: self.lithophane.border = self.borderSpinBox.value() self.lithophane.marginWidth = self.marginWidthSpinBox.value() self.lithophane.marginThickness = self.marginThicknessSpinBox.value() else: self.lithophane.border=0 if self.useContrastCheckBox.checkState() == Qt.Checked: self.lithophane.enhanceContrast = True else: self.lithophane.enhanceContrast = False self.lithophane.width = self.widthDoubleSpinBox.value() self.lithophane.layers = self.layerSpinBox.value() self.lithophane.thickness = self.layerThicknessSpinBox.value() self.lithophane.scaleImage = self.scaleSpinBox.value() self.lithophane.backThickness = self.backThicknessSpinBox.value() def save(self, filename=None, lithoType=None): self.setParameters() self.lithophane.save(filename, self.methodComboBox.currentText()) def enableBorderGroup(self, isEnabled): if isEnabled == Qt.Checked: self.borderSpinBox.setEnabled(True) self.marginWidthSpinBox.setEnabled(True) self.marginThicknessSpinBox.setEnabled(True) else: self.borderSpinBox.setEnabled(False) self.marginWidthSpinBox.setEnabled(False) self.marginThicknessSpinBox.setEnabled(False) # in addition to border group state need to update print resolution self.updateInfo() def enableDoubleRes(self, isEnabled): if isEnabled == Qt.Checked: self.lithophane.doubleRes = True else: self.lithophane.doubleRes = False self.updateInfo() def setHeight(self, width): imageHeight = width imageWidth = width border = 0 if self.lithophane.image: imageHeight = int(self.lithophane.image.height() * self.scaleSpinBox.value()) imageWidth = int(self.lithophane.image.width() * self.scaleSpinBox.value()) if self.useBorderCheckBox.checkState() == Qt.Checked: border = 2 * (self.borderSpinBox.value() + self.marginWidthSpinBox.value()) newHeight = width * (imageHeight + border) / (imageWidth + border) if abs(newHeight - self.heightDoubleSpinBox.value()) > 0.01: self.heightDoubleSpinBox.setValue( newHeight ) self.updateInfo() def setWidth(self, height): imageHeight = height imageWidth = height border = 0 if self.lithophane.image: imageHeight = int(self.lithophane.image.height() * self.scaleSpinBox.value()) imageWidth = int(self.lithophane.image.width() * self.scaleSpinBox.value()) if self.useBorderCheckBox.checkState() == Qt.Checked: border = 2 * (self.borderSpinBox.value() + self.marginWidthSpinBox.value()) newWidth = height * (imageWidth + border) / (imageHeight + border) if abs(newWidth - self.widthDoubleSpinBox.value()) > 0.01: self.widthDoubleSpinBox.setValue( newWidth ) self.updateInfo() def updateImage(self): if self.lithophane.image: width = int( self.lithophane.image.width() * self.scaleSpinBox.value() ) height = int( self.lithophane.image.height() * self.scaleSpinBox.value() ) self.lockScale = True if not self.lockWidth: self.resWidthSpinBox.setValue(width) if not self.lockHeight: self.resHeightSpinBox.setValue(height) self.lockScale = False self.pixmap.convertFromImage( self.lithophane.image.scaled(width, height) ) self.pixLabel.setPixmap( self.pixmap.scaled( self.imagePreviewWidth, self.imagePreviewHeight, Qt.KeepAspectRatio ) ) # also needs to update the info bar self.updateInfo() def updateScaleWidth(self): self.lockHeight = True scale = float(self.resHeightSpinBox.value()) / self.lithophane.image.height() if not self.lockWidth: self.resWidthSpinBox.setValue( int(self.lithophane.image.width() * scale) ) if not self.lockScale: self.scaleSpinBox.setValue(scale) self.lockHeight = False # get the height updated from width given new pixel size in relation to border... self.setHeight(self.widthDoubleSpinBox.value()) #self.updateInfo() def updateScaleHeight(self): self.lockWidth = True scale = float(self.resWidthSpinBox.value()) / self.lithophane.image.width() if not self.lockHeight: self.resHeightSpinBox.setValue( int(self.lithophane.image.height() * scale) ) if not self.lockScale: self.scaleSpinBox.setValue(scale) self.lockWidth = False # get the height updated from width given new pixel size in relation to border... self.setHeight( self.widthDoubleSpinBox.value() ) def updateInfo(self): self.setHeight( self.widthDoubleSpinBox.value() ) # get ppi ppi = 20 img = None if self.lithophane.image: img = self.lithophane.image if self.lithophane.image_orig: img = self.lithophane.image_orig border = 0 try: if self.useBorderCheckBox.checkState() == Qt.Checked: border = 2 * (self.borderSpinBox.value() + self.marginWidthSpinBox.value()) ppi = (border + img.width()) / self.widthDoubleSpinBox.value() except AttributeError: pass # self.widthDoubleSpinBox doesn't exist yet when opening a file into new tab # get image resolution width = 0 height = 0 if img: width = img.width() height = img.height() try: ppi *= self.scaleSpinBox.value() width *= self.scaleSpinBox.value() height *= self.scaleSpinBox.value() except AttributeError: pass # on startup this gets invoked before self.scaleSpinBox exists... # get vertices and faces wd = width ht = height if img: wd = self.lithophane.width_vert ht = self.lithophane.height_vert try: wd = int( wd * self.scaleSpinBox.value() ) ht = int( ht * self.scaleSpinBox.value() ) except AttributeError: pass # NOTE: these actually depend on the method employed vertices = wd * ht * 2 faces = (wd - 1) * (ht - 1) * 2 + (wd + ht) * 2 if self.useBorderCheckBox.checkState() == Qt.Checked: vertices = wd * ht # image vertices += (wd + ht + 4) * 4 # margin vertices += (wd + ht + 8) * 4 # border vertices += (wd + 6) * (ht + 6) # back faces = (wd + 1) * (ht + 1) # image faces += (wd + ht + 4) * 4 + 4 # margin faces += (wd + ht + 8) * 2 # border faces += (wd + ht +10) * 2 # edge faces += (wd + 5) * (ht + 5) # back faces *= 2 thickness = 1; try: thickness = (self.layerSpinBox.value() + self.backThicknessSpinBox.value()) * self.layerThicknessSpinBox.value() except AttributeError: pass self.imagePPILabel.setText('%.2f ppi' % ppi) self.imageSizeLabel.setText('%d x %d' % (width, height)) self.imageVerticesLabel.setText('%s' % format(vertices, 'n')) self.imageFacesLabel.setText('%s' % format(faces, 'n')) self.imageThicknessLabel.setText('%.2f mm' % thickness) def setMaxMargin(self, numLayers): numLayers = int(numLayers) self.marginThicknessSpinBox.setMaximum(numLayers) # also need to update information self.updateInfo() def paramLayout(self, useTabs): if useTabs: parmTabs = QTabWidget() parmTabs.addTab(self.sizeGroup, 'Size') parmTabs.addTab(self.borderGroup, 'Border') parmTabs.addTab(self.meshGroup, 'Mesh') parmTabs.addTab(self.qualGroup, 'Miscellaneous') layout = QHBoxLayout() layout.addLayout(self.visualLayout) layout.addWidget(parmTabs) self.setLayout(layout) else: parmLayout = QVBoxLayout() parmLayout.addWidget(self.sizeGroup) parmLayout.addWidget(self.borderGroup) parmLayout.addWidget(self.meshGroup) parmLayout.addWidget(self.qualGroup) layout = QHBoxLayout() layout.addLayout(self.visualLayout) layout.addLayout(parmLayout) self.setLayout(layout) class MainWindow(QMainWindow): Instances = set() def __init__(self, args=None, parent=None): super(MainWindow, self).__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose) self.debug = True self.ps = currPlatform MainWindow.Instances.add(self) self.helpContentsDlg = None self.meshPreviewDlg = None self.toolsPrefsDlg = ToolsPrefsDlg(self.updatePrefs, self) self.toolsPrefsDlg.setWindowTitle('Preferences') # use tabs for multiple images open at a time self.tabWidget = QTabWidget() self.tabWidget.setMovable(True) self.tabWidget.setDocumentMode(True) self.tabWidget.setElideMode(Qt.ElideLeft) self.tabWidget.setTabsClosable(True) self.setCentralWidget(self.tabWidget) # load stored configuration settings = QSettings(__companyName__, __thisAppName__) self.settings = settings version = settings.value("version") self.useOptionsTabs = makeCheckBox(settings.value('useOptionsTabs', 2)) print "useOptionsTabs:", self.useOptionsTabs self.showLithophaneInfo = makeCheckBox(settings.value('showLithophaneInfo', 2)) self.imagePreviewWidth = int(settings.value('imagePreviewWidth', 200)) self.imagePreviewHeight = int(settings.value('imagePreviewHeight', 200)) self.maxRecentFiles = int(settings.value("MaxRecentFiles", 9)) self.recentFiles = settings.value("RecentFiles") or [] if not isinstance(self.recentFiles, (list, tuple)): self.recentFiles = [self.recentFiles] # Apparently it is better to not use restoreGeometry() # http://www.pyside.org/docs/pyside/PySide/QtCore/QSettings.html self.restoreGeometry(settings.value("MainWindow/Geometry")) self.restoreState(settings.value("MainWindow/WindowState")) #self.resize(settings.value("size", QSize(400,400)).toSize()) #self.move(settings.value("pos", QPoint(200,200)).toPoint()) # application options self.toolsPrefsDlg.useOptionsTabsCheckBox.setCheckState( self.useOptionsTabs ) self.toolsPrefsDlg.showLithophaneInfoCheckBox.setCheckState( self.showLithophaneInfo ) self.toolsPrefsDlg.imageWidthSpinBox.setValue( self.imagePreviewWidth ) self.toolsPrefsDlg.imageHeightSpinBox.setValue( self.imagePreviewHeight ) self.status = self.statusBar() self.status.setSizeGripEnabled(False) self.status.showMessage("Ready", 5000) # define actions fileOpenAction = self.createAction("&Open...", self.fileOpen, QKeySequence.Open, "fileopen", "Open an image file") self.fileSaveAction = self.createAction("&Save", self.fileSave, QKeySequence.Save, "filesave", "Save lithophane mesh") self.fileSaveAsAction = self.createAction("Save &As...", self.fileSaveAs, "Ctrl+Shift+S", "filesaveas", "Save the lithophane mesh with a different name") self.filePreviewAction = self.createAction("Preview...", self.filePreview, None, "filepreview", "Preview lithophane mesh") fileCloseAction = self.createAction("Close Tab", self.closeTab, QKeySequence.Close, "fileclose", "Close the current image") # Windows does not have a Quit action, consider doing Ctrl+Q for this reason fileQuitAction = self.createAction("&Quit", self.closeApplication, QKeySequence.Quit, "filequit", "Close all windows") toolsPrefsAction = self.createAction("Preferences...", self.toolsPrefs, None, None, "Open preferences dialog") helpAboutAction = self.createAction("About...", self.helpAbout, None, None, "About " + __thisAppName__) helpContentsAction = self.createAction("Contents...", self.helpContents, QKeySequence.HelpContents, None, "View help file") QShortcut(QKeySequence.PreviousChild, self, self.prevTab) QShortcut(QKeySequence.NextChild, self, self.nextTab) self.fileMenu = self.menuBar().addMenu("&File") self.fileMenuActions = (fileOpenAction, self.fileSaveAction, self.fileSaveAsAction, None, fileCloseAction, fileQuitAction) toolsMenu = self.menuBar().addMenu("&Tools") self.addActions(toolsMenu, [toolsPrefsAction]) helpMenu = self.menuBar().addMenu("&Help") self.addActions(helpMenu, (helpAboutAction, helpContentsAction)) fileToolbar = self.addToolBar("File") fileToolbar.setObjectName("FileToolBar") self.addActions(fileToolbar, (fileOpenAction, self.fileSaveAction, self.filePreviewAction)) self.fileMenu.aboutToShow.connect( self.updateFileMenu ) self.setWindowTitle(__thisAppName__) # added for SDI self.destroyed[QObject].connect( MainWindow.updateInstances ) self.tabWidget.currentChanged[int].connect( self.updateUi ) self.tabWidget.tabCloseRequested[int].connect( self.closeTab ) # depends on self.fileMenu existing and triggers an update self.toolsPrefsDlg.recentFilesSpinBox.setValue( self.maxRecentFiles ) # need to have at least a single instance of Lithophane and it must follow the status bar creation if self.debug: print args if args.images: for image in args.images: lithophaneWidget = LithophaneWidget(filename=image, useTabs=self.toolsPrefsDlg.useTabs(), parent=self) lithophaneWidget.statusUpdate.connect(self.status.showMessage) lithophaneWidget.imagePreviewWidth = self.toolsPrefsDlg.imageWidthSpinBox.value() lithophaneWidget.imagePreviewHeight = self.toolsPrefsDlg.imageHeightSpinBox.value() lithophaneWidget.updateImage() self.tabWidget.addTab(lithophaneWidget, lithophaneWidget.shortName) self.tabWidget.setCurrentWidget(lithophaneWidget) lithophaneWidget.setFocus() else: lithophaneWidget = LithophaneWidget(filename=None, useTabs=self.toolsPrefsDlg.useTabs(), parent=self) self.tabWidget.addTab(lithophaneWidget, lithophaneWidget.shortName) self.tabWidget.setCurrentWidget(lithophaneWidget) lithophaneWidget.setFocus() self.updateUi() @staticmethod @Slot(QObject) def updateInstances(qobj): MainWindow.Instances = set([window for window in MainWindow.Instances if isAlive(window)]) def createAction(self, text, slot=None, shortcut=None, icon=None, tip=None, checkable=False, signal="triggered"): action = QAction(text, self) if shortcut: action.setShortcut(shortcut) if tip: action.setToolTip(tip) action.setStatusTip(tip) if slot: action.__dict__[signal].connect( slot ) if checkable: action.setCheckable(True) return action def addActions(self, target, actions): for action in actions: if action is None: target.addSeparator() else: target.addAction(action) def eventFilter(self, object, event): # VALIDATE: when isolating the editor components it was noted that this # function is exercised at the HCTextEdit object. It isn't # clear if it is serving any useful purpose here. return QFrame.eventFilter(object, event) def updateWindowTitle(self, updateUi=True): lithophaneWidget = self.currPane() self.setWindowTitle( __thisAppName__ + " -- %s" % lithophaneWidget.filename ) self.tabWidget.setTabText(self.tabWidget.currentIndex(), lithophaneWidget.shortName) self.tabWidget.setTabToolTip(self.tabWidget.currentIndex(), lithophaneWidget.filename) # probably don't this, if remove also remove the parameter including # where it is asserted in updateUi() if updateUi: self.updateUi() # Not supposed to call repaint() in Qt4 with update() being the # preferred method, but unless this is called the tab bar is frequently # blanked and not repainted. Probably means there's something # interrupting the painting and long term that needs to be fixed. self.tabWidget.repaint() def updateUi(self, index=None): lithophaneWidget = self.currPane() if lithophaneWidget is None: return self.updateWindowTitle(updateUi=False) #lithophaneWidget.updateStatusMessage() def updateRecentFilesCount(self): self.maxRecentFiles = int(self.toolsPrefsDlg.recentFilesSpinBox.value()) while len(self.recentFiles) > self.maxRecentFiles: self.recentFiles.pop() self.updateFileMenu() def updateFileMenu(self): self.fileMenu.clear() self.addActions(self.fileMenu, self.fileMenuActions[:-2]) current = None if self.tabWidget.count() > 0: currPane = self.currPane() if currPane is None: if self.debug: print "update File menu, self:", self else: current = currPane.filename recentFiles = [] if current and current[:len(__defaultFileName__)] != __defaultFileName__: recentFiles.append(current) for fname in self.recentFiles: if fname != current and QFile.exists(fname): recentFiles.append(fname) if recentFiles: self.fileMenu.addSeparator() for i, fname in enumerate(recentFiles): action = self.createAction("&%d %s" % (i+1, QFileInfo(fname).fileName()), self.loadFile, None, None, "Load %s" % fname) action.setData(fname) self.fileMenu.addAction(action) self.fileMenu.addSeparator() self.fileMenu.addActions(self.fileMenuActions[-2:]) def updateWindowMenu(self): self.windowMenu.clear() for window in MainWindow.Instances: if isAlive(window): self.windowMenu.addAction(window.windowTitle(), self.raiseWindow) def raiseWindow(self): action = self.sender() if not isinstance(action, QAction): return for window in MainWindow.Instances: if isAlive(window) and window.windowTitle() == action.text(): window.activateWindow() window.raise_() break def addRecentFile(self, fname): if fname == '' or fname[:len(__defaultFileName__)] == __defaultFileName__: return if fname not in self.recentFiles: self.recentFiles.insert(0, fname) while len(self.recentFiles) > self.maxRecentFiles: self.recentFiles.pop() self.updateFileMenu() def updatePrefs(self): for i in range(self.tabWidget.count()): currPane = self.tabWidget.widget(i) currPane.resizeImage( self.toolsPrefsDlg.imageWidthSpinBox.value(), self.toolsPrefsDlg.imageHeightSpinBox.value()) def closeApplication(self): QApplication.closeAllWindows() def closeTab(self, index=None, keepLast=True): if self.tabWidget.count() < 2 and keepLast: return False if index is None: index = self.tabWidget.currentIndex() self.tabWidget.widget(index).close() self.tabWidget.removeTab(index) return True def closeEvent(self, event): settings = self.settings settings.setValue("version", __version__) settings.setValue("prefsVersion", __prefsVersion__) files = [] for i in range(self.tabWidget.count()): currPane = self.tabWidget.widget(i) if currPane.filename: if currPane.filename[:len(__defaultFileName__)] != __defaultFileName__: files.append(currPane.filename) settings.setValue("CurrentFiles", files) settings.setValue("RecentFiles", self.recentFiles) settings.setValue("MainWindow/Geometry", self.saveGeometry()) #settings.setValue("size", self.size()) #settings.setValue("pos", self.pos()) settings.setValue("MainWindow/State", self.saveState()) settings.setValue("MaxRecentFiles", self.maxRecentFiles) settings.setValue("imagePreviewWidth", self.toolsPrefsDlg.imageWidthSpinBox.value()) settings.setValue("imagePreviewHeight", self.toolsPrefsDlg.imageHeightSpinBox.value()) settings.setValue("useOptionsTabs", self.toolsPrefsDlg.useOptionsTabsCheckBox.checkState()) settings.setValue("showLithophaneInfo", self.toolsPrefsDlg.showLithophaneInfoCheckBox.checkState()) currPane = self.currPane() if currPane: settings.setValue("LastFile", currPane.filename) failures = [] for i in range(self.tabWidget.count(), 0, -1): if self.debug: print "attempting to close tab", i if not self.closeTab(i - 1, keepLast=False): failures.append(str(self.tabWidget.widget(i - 1).filename)) if failures and QMessageBox.warning(self, __thisAppName__ + " -- Save Error", "Failed to close\n\t%s\nQuit anyway?" % "\n\t".join(failures), QMessageBox.Yes|QMessageBox.No) == QMessageBox.No: event.ignore() else: return True def prevTab(self): last = self.tabWidget.count() current = self.tabWidget.currentIndex() if last: last -= 1 current = last if current == 0 else current - 1 self.tabWidget.setCurrentIndex(current) def nextTab(self): last = self.tabWidget.count() current = self.tabWidget.currentIndex() if last: last -= 1 current = 0 if current == last else current + 1 self.tabWidget.setCurrentIndex(current) # both file open methods should check to see if the file is already open in a different window or tab # and switch to that window and tab (or prompt user to open new view?) def selectByFile(self, filename): for window in MainWindow.Instances: if isAlive(window): for index in range(window.tabWidget.count()): currPane = window.tabWidget.widget(index) if currPane.filename == filename: window.raise_() window.tabWidget.setCurrentIndex(index) currPane.setFocus() return True return False def fileOpen(self, newWindow=False): filename = self.currPane().filename dir = os.path.dirname(str(filename)) if filename else "." (filename, fileFilter) = QFileDialog.getOpenFileName(self, __thisAppName__ + " - Choose File", dir, "Images (*.png *.jpg *.bmp)") if filename and not self.selectByFile(filename): if newWindow: MainWindow(filename).show() else: self.loadFile(filename) # Loads file into the current tab (if unused), otherwise into a new tab def loadFile(self, filename=None, fileExists=True): # bug introduced since 1.0.4, causes segfault #QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) if filename is None: action = self.sender() if isinstance(action, QAction): filename = action.data() if self.debug: print "loadFile (recent file menu) filename:", filename # this happens when new tab is invoked # If the current tab is unused then load file into it currPane = self.currPane() newTab = False if currPane.filename and currPane.filename[:len(__defaultFileName__)] != __defaultFileName__ or filename is None: print "using new tab (%s, %s, %s)" % (currPane.filename, __defaultFileName__, filename) currPane = LithophaneWidget(filename=filename, parent=self) currPane.statusUpdate.connect(self.status.showMessage) newTab = True else: currPane.filename = filename currPane.shortName = QFileInfo(filename).fileName() try: if filename and fileExists: currPane.load() currPane.resizeImage(self.toolsPrefsDlg.imageWidthSpinBox.value(), self.toolsPrefsDlg.imageHeightSpinBox.value()) except (IOError, OSError), e: QMessageBox.warning(self, __thisAppName__ + " -- Load Error", "Failed to load %s: %s" % (filename, e)) if newTab: currPane.close() del currPane else: # no IO error occurred if newTab: index = self.tabWidget.addTab(currPane, currPane.shortName) self.tabWidget.setCurrentWidget(currPane) self.tabWidget.setTabText(index, currPane.shortName) self.tabWidget.setTabText(self.tabWidget.currentIndex(), currPane.windowTitle()) self.addRecentFile(currPane.filename) self.tabWidget.setTabToolTip(self.tabWidget.currentIndex(), currPane.filename) currPane.setFocus() finally: #QApplication.restoreOverrideCursor() self.updateUi() def fileSave(self): currPane = self.currPane() if currPane is None or currPane.filename is None: return currPane.save() self.updateUi() def fileSaveAs(self): currPane = self.currPane() if currPane is None or currPane.filename is None: return filename = currPane.filename (filename, fileFilter) = QFileDialog.getSaveFileName(self, __thisAppName__ + " -- Save File As", filename, "Images (*.png *.jpg *.bmp)") if filename: currPane.filename = filename currPane.shortName = QFileInfo(filename).fileName() self.updateWindowTitle() self.addRecentFile(currPane.filename) currPane.save() self.updateUi() def filePreview(self): if not __haveOpenGL__: QMessageBox.warning(None, __thisAppName__, "PyOpenGL must be installed to preview the mesh.") return currPane = self.currPane() currPane.setParameters() currPane.lithophane.generateMesh() if self.meshPreviewDlg: self.meshPreviewDlg.glWidget.lithophane = currPane.lithophane self.meshPreviewDlg.makeMesh() else: self.meshPreviewDlg = MeshPreviewDlg(currPane.lithophane) self.meshPreviewDlg.setWindowTitle('Mesh Preview') self.meshPreviewDlg.show() def toolsPrefs(self): self.toolsPrefsDlg.show() def helpAbout(self): QMessageBox.about(self, "About " + __thisAppName__, "" + __thisAppName__ + "" + """ v %s
Copyright © 2013 Tim Doty
Reads an image file as a height map and outputs a wavefront object file mesh using the specified number of layers, layer thickness, border settings and overall dimensions.
If PyOpenGL is installed can show a preview of the mesh.
Python %s - Qt %s - PySide %s on %s
""" % (
__version__, platform.python_version(), PySide.QtCore.__version__,
PySide.__version__, self.ps))
# consider using qVersion() instead of PySide.QtCore.__version__
def helpContents(self):
if not self.helpContentsDlg:
helpFile = os.path.join(os.path.dirname( os.path.realpath( __file__ ) ), __thisHelpFileName__)
self.helpContentsDlg = HelpContentsDlg( helpFile, self)
self.helpContentsDlg.setWindowTitle(__thisAppName__ + ' Help')
self.helpContentsDlg.show()
"""
Returns the current Lithophane object, or none.
"""
def currPane(self):
if isAlive(self):
if isAlive(self.tabWidget):
if isAlive(self.tabWidget.currentWidget()):
return self.tabWidget.currentWidget()
else: print "lithophaneWidget is dead"
else: print "tabWidget is dead"
else:
print "self is dead", self
return None
def main():
parser = argparse. ArgumentParser(description='Create a lithophane mesh from an image.')
parser.add_argument('images', type=str, nargs='*', help='an image file to make a lithophane from - specify more than one to create several lithophanes as a batch')
groupBorder = parser.add_argument_group('Border Parameters (optional)')
groupBorder.add_argument('-b', '--border', dest='border', metavar='