# -*- coding: utf-8 -*- #---Standard library imports import math from collections import namedtuple import time import threading import sys import os import subprocess import logging; logging.basicConfig(level=logging.DEBUG) log = logging.getLogger() #---Third-party imports import PyQt4 from PyQt4 import QtGui, QtCore from scipy.misc import fromimage, imfilter, pilutil import numpy as np import scipy.ndimage.filters as image_filters from PIL import Image, ImageFilter, ImageDraw sys.path.append(os.path.abspath('../')) from utils.path import path from utils.xml import NS_MAP from utils import extract as np_extract, getPyPlot, cython_annotate from utils.qt import NumpyModel, QTLoggingHandler, MaterialModel from utils.objparser import Mesh from utils.materials import loadMaterials from intersect import Mesh2D import pyximport; pyximport.install(setup_args={'include_dirs': [np.get_include()]}) from _gpu import dostuff np.set_printoptions(linewidth=200, threshold=1000) #---Framework imports cython_annotate('_simulator.pyx') from _simulator import Simulation REBUILD_UI = True if REBUILD_UI: # rebuild ui file f = path(PyQt4.__file__).parent.joinpath('uic/pyuic.py') try: subprocess.check_call([sys.executable, f, 'simray.ui', '-o', 'ui_simray.py']) except subprocess.CalledProcessError: log.exception('Error during pyqt compiling') # import from build ui file from ui_simray import Ui_MainWindow #---Globals INTENSITY = 0.0000002 OFFSET_X = OFFSET_Y = 0 tmpDir = path('tmp') #---Functions class Source(object): def __init__(self, x, y, power, idx): self.x = x self.y = y self.power = power self.idx = idx class Config(object): # [rays] rayangle = 38 raycount = 2000 max_generations = 6 # [map] pixel_per_meter = 16 # auto calculated during loadmesh width = None height = None alphachannel = 150 bgmap = True reflections = True refractions = True gausssigma = 5 # files meshfile = path('../../maps/umic/umic.obj') measurementsfile = path(r'd:\loco-dev\maps\umic\experiment\measure.txt') mesh_xlim = (-1, 58) mesh_ylim = (-1, 18) config = Config() def smoothMap(Z, maxpower): #~ Z = Z / maxpower # normalize to max=1.0 Z = 10 * np.log10(Z) # filter values that are -inf'ed by np.log(0) #~ Z[np.isinf(Z)] = -200 Z[np.isinf(Z)] = -130 #-np.min(np.isfinite(Z)) Z = image_filters.gaussian_filter(Z, config.gausssigma) return Z class MeshLoader(object): def __init__(self): self.objfile = None self.m2d = None self.mesh = None def load(self, objfile): self.objfile = path(objfile) self.mesh = Mesh() self.mesh.parseObjFile(self.objfile) self.m2d = Mesh2D(self.mesh, config.mesh_xlim, config.mesh_ylim, config.pixel_per_meter) config.width = self.m2d.width config.height = self.m2d.height meshLoader = MeshLoader() meshLoader.load(config.meshfile) class Renderer(object): def __init__(self): source1 = Source(580, 55, INTENSITY, 0) source2 = Source(500, 80, INTENSITY, 1) source3 = Source(810, 160, INTENSITY, 2) source4 = Source(60, 200, INTENSITY, 3) #~ self.sources = [source1, source2, source3, source4] self.sources = [source1] self.source_signals = [] # key: source id, value: smothed signal map self.needReplot = False self.needSimUpdate = threading.Event() self.renderLoop = threading.Thread(target=self._render) self.renderLoop.daemon = True self.renderLoop.start() self.plotLock = threading.Lock() self.imageBufferPtr = None self.envmap = np.zeros([config.width, config.height], dtype=np.int16) self.materials = [] # holds material data for each object def _runsim(self, verbose=True): t = time.time() self.sim = Simulation(config.width, config.height, config.max_generations, config.reflections, config.refractions, self.sources, self.materials, config.pixel_per_meter) self.sim.initRays(config.raycount, config.rayangle) init_time = time.time() -t t = time.time() #~ rays = self.sim.rays[0].toNumpy() #~ maparray = dostuff(rays, self.sim.map) #~ print 'gpu: %.3f sec' % (time.time() - t) self.sim.build(self.envmap) render_time = time.time() -t t = time.time() self.sim.map.writeBGRA(self.imageBufferPtr, self.envmap_, config.alphachannel) draw_time = time.time() - t if verbose: s = '%.3f init, %.3f render, %.3f draw [%.2f frames/sec]' % (init_time, render_time, draw_time, 1 / (render_time + draw_time + 1e-9)) log.info(s) #~ s = ', '.join('g%s:%sr/%sms' % (i, rays.count, int(duration*1000)) #~ for i, rays, duration in self.sim.generations[:20]) #~ log.debug(s) def _render(self): try: while True: self.needSimUpdate.wait() if self.imageBufferPtr is None: raise ValueError('imagebuffer not connected') self._runsim() signals.newSimulationData.emit() self.needSimUpdate.clear() if self.needReplot: self._plot() self.needReplot = False except Exception: log.exception('error during rendering') def _plot(self): plt, mlab, dates, font_manager, ticker = getPyPlot() map = self.sim.map def _work(): with self.plotLock: log.debug('plotting...') self.source_signals = [] t = time.time() dpi = 96 fig = plt.figure(figsize=(float(config.width) / dpi, float(config.height) / dpi)) try: #~ ax = plt.axes([0.10, 0.10, 0.89, 0.89]) ax = plt.axes([0.00, 0.00, 1.0, 1.0], axisbg='black') x = np.arange(0, map.width, 1) y = np.arange(0, map.height, 1) X, Y = np.meshgrid(x, y) gauss_duration = 0 for i in range(len(map.sources)): _t = time.time() Z = map.asArray(i, invertY=True) Z = smoothMap(Z, map.sources[i].power) gauss_duration += (time.time() - _t) #~ Z = image_filters.median_filter(Z, size=(5,5)) try: levels = range(-110, -50, 5) #~ levels = range(-110, 10, 10) ax.contour(X, Y, Z, levels) except ValueError: pass self.source_signals.append(Z) fig.savefig(tmpDir.joinpath('analysis.png'), format='png', dpi=dpi) log.info('plotted map in %.3f sec [gauss: %.3f sec]' % (time.time() - t, gauss_duration)) finally: plt.close(fig) signals.newPlotAvailable.emit() threading.Thread(target=_work).start() def setImage(self, image): '''the a QImage instance that should be used for drawing''' self.imageBufferPtr = image.bits() def update(self, withPlot=False): if withPlot: self.needReplot = True self.needSimUpdate.set() def saveEnvMap(self, fname): self.envmap_ = self.envmap.transpose().ravel() envmap = self.envmap.transpose() colors = [(255, 0, 0, 255), (0, 255, 0, 255), (0, 0, 255, 255), (255, 255, 255, 255), (255, 0, 255, 255), (128, 128, 128, 255)] img = Image.new('RGBA', (config.width, config.height), color=0) pixels = img.load() for x in range(config.width): for y in range(config.height): if envmap[y][x] > 0: idx = envmap[y][x] % len(colors) pixels[x, y] = colors[idx] log.debug('saving envmap to %s' % path(fname).abspath()) img.save(fname) renderer = Renderer() class Signals(QtCore.QObject): ''' signals have to be classlevel attributes of a QObject so we need to define them here ''' logMsg = QtCore.pyqtSignal(str) newPlotAvailable = QtCore.pyqtSignal() newAnalysisMap = QtCore.pyqtSignal(str) newSimulationData = QtCore.pyqtSignal() renderMapMouseMove = QtCore.pyqtSignal(int, int, tuple) renderMapSelectRect = QtCore.pyqtSignal(tuple) signals = Signals() class RenderedMap(QtGui.QLabel): ''' acts as drawing pane for the renderer data and as mouse controller for moving the sources ''' def __init__(self, parent, mainform): QtGui.QLabel.__init__(self, parent) self.setGeometry(0, 0, config.width, config.height) self.image = QtGui.QImage(config.width, config.height, QtGui.QImage.Format_ARGB32) # tell the renderer where to store the image data renderer.setImage(self.image) self.setMouseTracking(True) self.dragged_source = None signals.newSimulationData.connect(self.onNewSimulationData) self.mainform = mainform self.mouseMode = 'dragSources' self.selected_rect = None self.inSelect = False def onNewSimulationData(self): if (self.selected_rect is not None and self.dragged_source is None): self.selectRect(self.selected_rect) else: self.reloadImageBuffer() def reloadImageBuffer(self): self.setPixmap(QtGui.QPixmap.fromImage(self.image)) def mousePressEvent(self, event): x = event.pos().x() y = event.pos().y() if self.mouseMode == 'dragSources': min_dist, min_dist_source = 99999, None for source in renderer.sources: d = math.sqrt((source.x - x)**2 + (source.y - y)**2) if d < min_dist: min_dist, min_dist_source = d, source self.dragged_source = min_dist_source elif self.mouseMode == 'zoomRect': if self.inSelect: self._zoomRectEndSelect(x, y) else: self.inSelect = True self.selected_rect = [x, y, None, None] #~ elif self.mouseMode == 'hover': #~ self._inspectPosition(x, y) def _normalizeRect(self, r): # ensure x1 < x2 and y1 < y2 r = list(r) if r[2] < r[0]: r[0], r[2] = r[2], r[0] if r[3] < r[1]: r[1], r[3] = r[3], r[1] return r #~ def _inspectPosition(self, x, y): #~ w = h = 100 #~ r = [x-int(w/2), y-int(h/2), x+int(w/2), y+int(h/2)] #~ self.selectRect(r) def _inspectSection(self, r): map = renderer.sim.map w = r[2] - r[0] h = r[3] - r[1] x = r[0] + w / 2 y = r[1] + h / 2 #~ mode = 'smoothed' #~ mode = 'raw' mode = 'decibel_from_raw' if mode == 'smoothed': a = smoothMap(map.asArray(0), map.sources[0].power) elif mode in ('raw', 'decibel_from_raw'): a = map.asArray(0) a = np_extract(a, (h, w ), (y, x), fill=np.nan) if map.width / float(w) > map.height / float(h): size = (map.height, int(float(w) * map.height / h)) else: size = (int(float(h) * map.width / w), map.width) offset = x - int(w / 2), y - int(h / 2) mainform.ui.tableViewArray.setModel(NumpyModel(a, offset, mode)) a = pilutil.imresize(a, size, interp='nearest') pilutil.imsave('tmp/zoomrect.png', a) signals.newAnalysisMap.emit('tmp/zoomrect.png') def _zoomRectEndSelect(self, x, y): r = self.selected_rect self.selected_rect = r = self._normalizeRect([r[0], r[1], x, y]) signals.renderMapSelectRect.emit(tuple(r)) log.info('selected:%s,%s,%s,%s' % (r[0], r[2], r[1], r[3])) self._inspectSection(r) self.inSelect = False def mouseReleaseEvent(self, event): x = event.pos().x() y = event.pos().y() if self.mouseMode == 'dragSources': log.info('moved source to %s,%s' % (x, y)) self.dragged_source = None renderer.update(withPlot=True) elif self.mouseMode == 'zoomRect': self._zoomRectEndSelect(x, y) def selectRect(self, r): self.selected_rect = r renderer.sim.map.drawRect(r[0], r[1], r[2], r[3]) self.reloadImageBuffer() self._inspectSection(r) def mouseMoveEvent(self, event): x = event.pos().x() y = event.pos().y() if self.mouseMode == 'dragSources': if self.dragged_source: self.dragged_source.x = x self.dragged_source.y = y if self.dragged_source is not None: renderer.update() elif self.mouseMode == 'zoomRect': r = self.selected_rect if self.inSelect: nr = self._normalizeRect([r[0], r[1], x, y]) signals.renderMapSelectRect.emit(tuple(nr)) renderer.sim.map.drawRect(nr[0], nr[1], nr[2], nr[3]) self.reloadImageBuffer() elif self.mouseMode == 'measure': # TODO: interface slowdown? sourceVector = tuple(sig[config.height-y][x] for sig in renderer.source_signals) signals.renderMapMouseMove.emit(x, y, sourceVector) class MainForm(QtGui.QMainWindow): def __init__(self, parent=None): QtGui.QWidget.__init__(self, parent) self.ui = Ui_MainWindow() self.ui.setupUi(self) self.setupFeedbackTree() self.loadConfig() self.ui.actionExit.triggered.connect(lambda: self.close()) self.ui.actionBenchmark.triggered.connect(self.benchmark) self.ui.actionMoveSources.setChecked(True) self.ui.actionMoveSources.triggered.connect(self.toggleMouseMode('dragSources')) self.ui.actionZoomRect.triggered.connect(self.toggleMouseMode('zoomRect')) self.ui.actionHover.triggered.connect(self.toggleMouseMode('measure')) self.loadMaterials() self.loadEnvMap() self.setupLogger() if not tmpDir.exists(): tmpDir.mkdir() self.renderedMap = RenderedMap(self.ui.previewImage, self) self.loadBackgroundForRenderMap() signals.newPlotAvailable.connect(self.onNewPlotAvailable) signals.newAnalysisMap.connect(self.onNewAnalysisImage) signals.renderMapMouseMove.connect(self.onRenderMapMouseMove) signals.renderMapSelectRect.connect(self.onRenderMapRectSelect) renderer.update(withPlot=True) def toggleMouseMode(self, mode): def _inner(): self.renderedMap.mouseMode = mode if mode != 'dragSources': self.ui.actionMoveSources.setChecked(False) if mode != 'zoomRect': self.ui.actionZoomRect.setChecked(False) if mode != 'measure': self.ui.actionHover.setChecked(False) if mode in ('zoomRect', 'measure'): self.renderedMap.selected_rect = None return _inner def benchmark(self): log.info('starting benchmark...') total = 150 t = time.time() for i in range(total): renderer._runsim(verbose=False) avgdur = (time.time() - t) / total log.info('%s runs - avg duration: %.3f/sec [%.2f frames/sec]' % (total, avgdur, avgdur**-1)) def setupFeedbackTree(self): p = QtGui.QTreeWidgetItem(self.ui.treeFeedback) p.setText(0, 'Position') e = self.feedback_posx = QtGui.QTreeWidgetItem() p.addChild(e) e.setText(0, 'x') e = self.feedback_posy = QtGui.QTreeWidgetItem() p.addChild(e) e.setText(0, 'y') self.ui.treeFeedback.expandItem(p) p = QtGui.QTreeWidgetItem(self.ui.treeFeedback) p.setText(0, 'Sources') self.feedback_sources = {} for i, source in enumerate(renderer.sources): e = QtGui.QTreeWidgetItem() p.addChild(e) e.setText(0, str(i)) self.feedback_sources[i] = e self.ui.treeFeedback.expandItem(p) self.feedback_rect_parent = p = QtGui.QTreeWidgetItem(self.ui.treeFeedback) p.setText(0, 'SelectedRect') self.feedback_rect = [] for s in ('x1', 'y1', 'x2', 'y2'): e = QtGui.QTreeWidgetItem() p.addChild(e) e.setText(0, s) self.feedback_rect.append(e) def loadEnvMap(self): ''' load map from mesh file and sets self.envmap''' #~ zz = np.zeros([config.width, config.height], dtype=np.int16) t = time.time() CUT_AT_Z = 1.0 for i, objname in enumerate(meshLoader.m2d.objnames, 1): #~ if objname != 'Light_Walls_M4': #~ continue log.debug('loading object %s: %s' % (i, objname)) fn = 'cut_%s_%s_%s.png' % (objname, ','.join(str(e) for e in config.mesh_xlim + config.mesh_ylim), config.pixel_per_meter) cachefile = tmpDir.joinpath(fn) if not cachefile.exists() or cachefile.mtime < meshLoader.objfile.mtime: img = meshLoader.m2d.cut(objname, CUT_AT_Z) log.debug('cache file to %s' % cachefile.abspath()) img.save(cachefile) else: log.debug('from cached %s' % cachefile.abspath()) img = Image.open(cachefile) #~ if img is None: #~ log.debug('cutplane for object %s is empty' % objname) #~ continue edges = fromimage(img) e = edges.astype(np.int16).transpose() shapediff = renderer.envmap.shape[0] - e.shape[0] both = (e, np.zeros((shapediff, e.shape[1]))) e = np.vstack(both) shapediff = renderer.envmap.shape[1] - e.shape[1] both = (e, np.zeros((e.shape[0], shapediff))) e = np.hstack(both) e = np.clip(e, 0, 1) * i # set every entry that is greater 0 to layerindex # user first array as condition - if entry of forst array is greater 0 use that entry - else the stored value # effect: copy layer over layer renderer.envmap = np.where(e, e, renderer.envmap).astype(np.int16) log.info('loaded layer in %.3f sec' % (time.time() -t)) renderer.saveEnvMap('tmp/envmap_edges.png') def loadMaterials(self): materials = loadMaterials('../../maps/umic/materials.xml') def reloadMaterials(): renderer.materials[:] = [] for objname in meshLoader.m2d.objnames: matname = meshLoader.mesh.materials[objname] log.debug('using material %s' % matname) reflection, refraction = materials[matname].reflect, materials[matname].alpha renderer.materials.append((reflection, refraction)) reloadMaterials() def newMaterialParams(): reloadMaterials() renderer.update(withPlot=True) model = MaterialModel(materials) self.ui.tableMaterials.setModel(model) model.onChange = newMaterialParams def loadConfig(self): def onChange(cfgname, translate, check=None): def _inner(value): try: value = translate(value) except ValueError: pass else: if check is None or check(value): setattr(config, cfgname, value) renderer.update(withPlot=True) return _inner cfg = [('spinBoxRayAngle', 'rayangle'), ('spinBoxRayCount', 'raycount'), ('spinBoxGenerations', 'max_generations'), ('spinBoxAlphaChannel', 'alphachannel'), ('spinBoxGaussSigma', 'gausssigma'), ('checkBoxBGMap', 'bgmap'), ('checkBoxReflections', 'reflections'), ('checkBoxRefractions', 'refractions')] for ui_name, cfg_name in cfg: e = getattr(self.ui, ui_name) if isinstance(e, PyQt4.QtGui.QSpinBox): e.setValue(getattr(config, cfg_name)) e.valueChanged.connect(onChange(cfg_name, int, lambda v: 0 < v < 10000)) elif isinstance(e, PyQt4.QtGui.QCheckBox): e.setCheckState(2 if getattr(config, cfg_name) else 0) e.stateChanged.connect(onChange(cfg_name, lambda v: True if v else False)) for qtimg in (self.ui.previewImage, self.ui.analysisImage): qtimg.setMaximumSize(config.width, config.height) qtimg.setMinimumSize(config.width, config.height) # load measurement file self.ui.lineEditMeasurements.setText(config.measurementsfile) if config.measurementsfile.exists(): self.ui.textMeasurements.setPlainText(config.measurementsfile.text()) def loadBackgroundForRenderMap(self, baseImgFile=None): if baseImgFile is None or not config.bgmap: img = Image.new('RGBA', (config.width, config.height), (0,0,0, 255)) else: img = Image.open(baseImgFile) draw = ImageDraw.Draw(img) edgeimg = Image.open('tmp/envmap_edges.png') img.paste(edgeimg, (0, 0), edgeimg) # draw access points for source in renderer.sources: draw.ellipse([(source.x - 3, source.y - 3), (source.x + 2, source.y + 3)], fill='white') # draw measurements for line in config.measurementsfile.lines(): x, y, z, _, sigstrength = line.split() x, y = meshLoader.m2d.meter2pixel(float(x), float(y)) draw.ellipse([(x - 3, y - 3), (x + 2, y + 3)], fill='red') img.save('tmp/renderMapBackground.png') self.ui.previewImage.setStyleSheet("background-image: url(tmp/renderMapBackground.png)") log.info('loaded bgimage') def setupLogger(self): from PyQt4.Qsci import QsciScintilla _fromUtf8 = QtCore.QString.fromUtf8 self.ui.textLogger = QsciScintilla(self.ui.mainWidget) self.ui.textLogger.setToolTip(_fromUtf8("")) self.ui.textLogger.setWhatsThis(_fromUtf8("")) self.ui.textLogger.setObjectName(_fromUtf8("textLogger")) #~ print dir(self.ui.tabLogger) self.ui.tabLogger.layout().addWidget(self.ui.textLogger) self.ui.textLogger.setFont(QtGui.QFont('Lucida Console', pointSize=9)) self.ui.textLogger.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0) def _log(msg): self.ui.textLogger.append(msg + '\n') self.ui.textLogger.SendScintilla(QsciScintilla.SCI_GOTOPOS, len(self.ui.textLogger.text()), 0) log.handlers = [] log.addHandler(QTLoggingHandler(signals.logMsg)) signals.logMsg.connect(_log) self.ui.actionClearLog.triggered.connect(lambda: self.ui.textLogger.setText('')) #~ @signals.newPlotAvailable.connect def onNewPlotAvailable(self): self.loadBackgroundForRenderMap('tmp/analysis.png') #~ self.ui.analysisImage.setStyleSheet("background-image: url(tmp/analysis.png)") #~ @signals.newAnalysisMap.connect def onNewAnalysisImage(self, fname): css = "background-image: url(%s); background-repeat: no-repeat" % fname self.ui.analysisImage.setStyleSheet(css) #~ @signals.renderMapMouseMove.connect def onRenderMapMouseMove(self, x, y, sourceVector): _x, _y = meshLoader.m2d.pixel2meter(x, y) self.feedback_posx.setText(1, '%.2f [%s]' % (_x, x)) self.feedback_posy.setText(1, '%.2f [%s]' % (_y, y)) for i, value in enumerate(sourceVector): self.feedback_sources[i].setText(1, '%.3f' % value) #~ @signals.renderMapSelectRect.connect def onRenderMapRectSelect(self, r): for item, value in zip(self.feedback_rect, r): item.setText(1, '%s' % value) self.ui.treeFeedback.expandItem(self.feedback_rect_parent) def closeEvent(self, event): pass def eventFilter(self, object, event): print event.type() return True class App(QtGui.QApplication): pass if __name__ == '__main__': app = App(sys.argv) mainform = MainForm() mainform.show() sys.exit(app.exec_())