yet more refinements to the fancy PyQt integration example + necessary code adaptations

metadata
Wenzel Jakob 2013-11-26 22:54:26 +01:00
parent fd7400593a
commit 96832ab70d
6 changed files with 160 additions and 80 deletions

View File

@ -567,46 +567,162 @@ code that updates the status and progress bar for more detail.
import mitsuba, multiprocessing, sys, time import mitsuba, multiprocessing, sys, time
from mitsuba.core import Scheduler, PluginManager, Thread, Vector, Point2i, \ from mitsuba.core import Scheduler, PluginManager, Thread, Vector, Point2i, \
LocalWorker, Properties, Bitmap, Spectrum, Appender, EWarn, Transform, FileStream Vector2i, LocalWorker, Properties, Bitmap, Spectrum, Appender, EWarn, \
Transform, FileStream
from mitsuba.render import RenderQueue, RenderJob, Scene, RenderListener from mitsuba.render import RenderQueue, RenderJob, Scene, RenderListener
from PyQt4.QtCore import Qt, QPoint, QSize, QRect, pyqtSignal
from PyQt4.QtGui import QApplication, QMainWindow, QPainter, QImage, \
QProgressBar, QWidget, QSizePolicy
from PyQt4.QtCore import Qt, QPoint, pyqtSignal Signal = pyqtSignal
from PyQt4.QtGui import QApplication, QMainWindow, QPainter, QImage, QProgressBar
class MitsubaView(QMainWindow): class MitsubaRenderBuffer(RenderListener):
viewUpdated = pyqtSignal() """
renderProgress = pyqtSignal(int) Implements the Mitsuba callback interface to capture notifications about
renderingCompleted = pyqtSignal(bool) rendering progress. Partially completed image blocks are efficiently
tonemapped into a local 8-bit Mitsuba Bitmap instance and exposed as a QImage.
"""
RENDERING_FINISHED = 0
RENDERING_CANCELLED = 1
RENDERING_UPDATED = 2
def __init__(self, queue, callback):
super(MitsubaRenderBuffer, self).__init__()
self.bitmap = self.qimage = None
self.callback = callback
self.time = 0
self.size = Vector2i(0, 0)
queue.registerListener(self)
def workBeginEvent(self, job, wu, thr):
""" Callback: a worker thread started rendering an image block.
Draw a rectangle to highlight this """
_ = self._get_film_ensure_initialized(job)
self.bitmap.drawRect(wu.getOffset(), wu.getSize(), Spectrum(0.8))
self._potentially_send_update()
def workEndEvent(self, job, wr):
""" Callback: a worker thread finished rendering an image block.
Tonemap the associated pixels and store them in 'self.bitmap' """
film = self._get_film_ensure_initialized(job)
film.develop(wr.getOffset(), wr.getSize(), wr.getOffset(), self.bitmap)
self._potentially_send_update()
def refreshEvent(self, job):
""" Callback: the entire image changed (some rendering techniques
do this occasionally). Hence, tonemap the full film. """
film = self._get_film_ensure_initialized(job)
film.develop(Point2i(0), self.size, Point2i(0), self.bitmap)
self._potentially_send_update(force=True)
def finishJobEvent(self, job, cancelled):
""" Callback: the rendering job has finished or was cancelled.
Re-develop the image once more for the final result. """
film = self._get_film_ensure_initialized(job)
film.develop(Point2i(0), self.size, Point2i(0), self.bitmap)
self.callback(MitsubaRenderBuffer.RENDERING_CANCELLED if cancelled
else MitsubaRenderBuffer.RENDERING_FINISHED)
def _get_film_ensure_initialized(self, job):
""" Ensure that all internal data structure are set up to deal
with the given rendering job """
film = job.getScene().getFilm()
size = film.getSize()
if self.size != size:
self.size = size
# Round the buffer size to the next power of 4 to ensure 32-bit
# aligned scanlines in the underlying buffer. This is needed so
# that QtGui.QImage and mitsuba.Bitmap have exactly the same
# in-memory representation.
bufsize = Vector2i((size.x + 3) // 4 * 4, (size.y + 3) // 4 * 4)
# Create an 8-bit Mitsuba bitmap that will store tonemapped pixels
self.bitmap = Bitmap(Bitmap.ERGB, Bitmap.EUInt8, bufsize)
self.bitmap.clear()
# Create a QImage that is backed by the Mitsuba Bitmap instance
# (i.e. without doing unnecessary bitmap copy operations)
self.qimage = QImage(self.bitmap.getNativeBuffer(), self.size.x,
self.size.y, QImage.Format_RGB888)
return film
def _potentially_send_update(self, force = False):
""" Send an update request to any attached widgets, but not too often """
now = time.time()
if now - self.time > .25 or force:
self.time = now
self.callback(MitsubaRenderBuffer.RENDERING_UPDATED)
class RenderWidget(QWidget):
""" This simple widget attaches itself to a Mitsuba RenderQueue instance
and displays the progress of everything that's being rendered """
renderingUpdated = Signal(int)
def __init__(self, parent, queue):
QWidget.__init__(self, parent)
self.buffer = MitsubaRenderBuffer(queue, self.renderingUpdated.emit)
# Need a queued conn. to avoid threading issues between Qt and Mitsuba
self.renderingUpdated.connect(self._handle_update, Qt.QueuedConnection)
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
def sizeHint(self):
return QSize(self.buffer.size.x, self.buffer.size.y)
def _handle_update(self, event):
image = self.buffer.qimage
# Detect when an image of different resolution is being rendered
if image.width() < self.width() or image.height() < self.height():
self.updateGeometry()
self.repaint()
def paintEvent(self, event):
""" When there is more space then necessary, display the image centered
on a black background, surrounded by a light gray border """
QWidget.paintEvent(self, event)
qp = QPainter(self)
qp.fillRect(self.rect(), Qt.black)
image = self.buffer.qimage
if image is not None:
offset = QPoint((self.width() - image.width()) / 2,
(self.height() - image.height()) / 2)
qp.setPen(Qt.lightGray)
qp.drawRect(QRect(offset - QPoint(1, 1), image.size() + QSize(1, 1)))
qp.drawImage(offset, image)
qp.end()
class MitsubaDemo(QMainWindow):
renderProgress = Signal(int)
def __init__(self): def __init__(self):
super(MitsubaView, self).__init__() super(MitsubaDemo, self).__init__()
# Initialize Mitsuba # Initialize Mitsuba
self.initializeMitsuba() self.initializeMitsuba()
self.job = self.createRenderJob() self.job = self.createRenderJob()
# Initialize the user interface # Initialize the user interface
self.setWindowTitle('Mitsuba/Qt demo')
status = self.statusBar() status = self.statusBar()
self.rwidget = RenderWidget(self, self.queue)
progress = QProgressBar(status)
status.setContentsMargins(0,0,5,0) status.setContentsMargins(0,0,5,0)
self.progress = QProgressBar(status) status.addPermanentWidget(progress)
status.addPermanentWidget(self.progress)
status.setSizeGripEnabled(False) status.setSizeGripEnabled(False)
self.setFixedSize(self.qimage.width(), self.qimage.height() + self.setWindowTitle('Mitsuba/Qt demo')
self.progress.height()*1.5) self.setCentralWidget(self.rwidget)
def handleRenderingCompleted(cancelled): # Hide the scroll bar once the rendering is done
status.showMessage("Rendering finished.") def renderingUpdated(event):
self.progress.setVisible(False) if event == MitsubaRenderBuffer.RENDERING_FINISHED:
status.showMessage("Done.")
progress.hide()
if not cancelled: self.renderProgress.connect(progress.setValue, Qt.QueuedConnection)
outFile = FileStream("rendering.png", FileStream.ETruncReadWrite) self.rwidget.renderingUpdated.connect(renderingUpdated,
self.bitmap.write(Bitmap.EPNG, outFile)
outFile.close()
self.viewUpdated.connect(self.repaint, Qt.QueuedConnection)
self.renderProgress.connect(self.progress.setValue, Qt.QueuedConnection)
self.renderingCompleted.connect(handleRenderingCompleted,
Qt.QueuedConnection) Qt.QueuedConnection)
# Start the rendering process # Start the rendering process
status.showMessage("Rendering ..") status.showMessage("Rendering ..")
self.job.start() self.job.start()
@ -621,47 +737,20 @@ class MitsubaView(QMainWindow):
self.queue = RenderQueue() self.queue = RenderQueue()
# Get a reference to the plugin manager # Get a reference to the plugin manager
self.pmgr = PluginManager.getInstance() self.pmgr = PluginManager.getInstance()
# Appender to process log and progress messages within Python
# Process Mitsuba log and progress messages within Python
class CustomAppender(Appender): class CustomAppender(Appender):
def append(self2, logLevel, message): def append(self2, logLevel, message):
print(message) print(message)
def logProgress(self2, progress, name, formatted, eta): def logProgress(self2, progress, name, formatted, eta):
# Asynchronously notify the main thread
self.renderProgress.emit(progress) self.renderProgress.emit(progress)
logger = Thread.getThread().getLogger() logger = Thread.getThread().getLogger()
logger.setLogLevel(EWarn) logger.setLogLevel(EWarn) # Display warning & error messages
logger.clearAppenders() logger.clearAppenders()
logger.addAppender(CustomAppender()) logger.addAppender(CustomAppender())
# Listener to update bitmap subregions when blocks finish rendering
class CustomListener(RenderListener):
def __init__(self):
super(CustomListener, self).__init__()
self.time = 0
def workBeginEvent(self2, job, wu, thr):
self.bitmap.drawRect(wu.getOffset(), wu.getSize(), Spectrum(1.0))
now = time.time()
if now - self2.time > .25:
self.viewUpdated.emit()
self2.time = now
def workEndEvent(self2, job, wr):
self.film.develop(wr.getOffset(), wr.getSize(),
wr.getOffset(), self.bitmap)
now = time.time()
if now - self2.time > .25:
self.viewUpdated.emit()
self2.time = now
def refreshEvent(self2, job):
self.film.develop(Point2i(0), self.bitmap.getSize(),
Point2i(0), self.bitmap)
self.viewUpdated.emit()
def finishJobEvent(self2, job, cancelled):
self2.refreshEvent(job)
self.renderingCompleted.emit(cancelled)
# Create a custom listener and register it with the image queue
self.queue.registerListener(CustomListener())
def closeEvent(self, e): def closeEvent(self, e):
self.job.cancel() self.job.cancel()
self.queue.join() self.queue.join()
@ -686,38 +775,21 @@ class MitsubaView(QMainWindow):
} }
}) })
self.film = self.scene.getFilm()
size = self.film.getSize()
# Mitsuba Bitmap that will store pixels of the developed film
self.bitmap = Bitmap(Bitmap.ERGB, Bitmap.EUInt8, size)
self.bitmap.clear()
# Create a Qt Image that directly points into the contents of self.bitmap
self.qimage = QImage(self.bitmap.getNativeBuffer(),
size.x, size.y, QImage.Format_RGB888)
return RenderJob('rjob', self.scene, self.queue) return RenderJob('rjob', self.scene, self.queue)
def keyPressEvent(self, e): def keyPressEvent(self, e):
if e.key() == Qt.Key_Escape: if e.key() == Qt.Key_Escape:
self.close() self.close()
def paintEvent(self, event):
super(MitsubaView, self).paintEvent(event)
painter = QPainter(self)
painter.drawImage(QPoint(0, 0), self.qimage)
painter.end()
def main(): def main():
import signal import signal
# Stop the program upon Ctrl-C (SIGINT) # Stop the program upon Ctrl-C (SIGINT)
signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGINT, signal.SIG_DFL)
app = QApplication(sys.argv) app = QApplication(sys.argv)
view = MitsubaView() demo = MitsubaDemo()
view.show() demo.show()
view.raise_() demo.raise_()
retval = app.exec_() retval = app.exec_()
sys.exit(retval) sys.exit(retval)

View File

@ -92,6 +92,12 @@ public:
*/ */
inline bool isInteractive() const { return m_interactive; } inline bool isInteractive() const { return m_interactive; }
/// Get a pointer to the underlying scene
inline Scene *getScene() { return m_scene.get(); }
/// Get a pointer to the underlying scene (const version)
inline const Scene *getScene() const { return m_scene.get(); }
MTS_DECLARE_CLASS() MTS_DECLARE_CLASS()
protected: protected:
/// Virtual destructor /// Virtual destructor

View File

@ -373,7 +373,7 @@ public:
uint8_t *targetData = target->getUInt8Data() uint8_t *targetData = target->getUInt8Data()
+ (targetOffset.x + targetOffset.y * target->getWidth()) * targetBpp; + (targetOffset.x + targetOffset.y * target->getWidth()) * targetBpp;
if (size.x == m_cropSize.x) { if (size.x == m_cropSize.x && target->getWidth() == m_storage->getWidth()) {
/* Develop a connected part of the underlying buffer */ /* Develop a connected part of the underlying buffer */
cvt->convert(source->getPixelFormat(), 1.0f, sourceData, cvt->convert(source->getPixelFormat(), 1.0f, sourceData,
target->getPixelFormat(), target->getGamma(), targetData, target->getPixelFormat(), target->getGamma(), targetData,

View File

@ -273,7 +273,7 @@ public:
uint8_t *targetData = target->getUInt8Data() uint8_t *targetData = target->getUInt8Data()
+ (targetOffset.x + targetOffset.y * target->getWidth()) * targetBpp; + (targetOffset.x + targetOffset.y * target->getWidth()) * targetBpp;
if (size.x == m_cropSize.x) { if (size.x == m_cropSize.x && target->getWidth() == m_storage->getWidth()) {
/* Develop a connected part of the underlying buffer */ /* Develop a connected part of the underlying buffer */
cvt->convert(source->getPixelFormat(), 1.0f, sourceData, cvt->convert(source->getPixelFormat(), 1.0f, sourceData,
target->getPixelFormat(), target->getGamma(), targetData, target->getPixelFormat(), target->getGamma(), targetData,

View File

@ -214,7 +214,7 @@ public:
uint8_t *targetData = target->getUInt8Data() uint8_t *targetData = target->getUInt8Data()
+ (targetOffset.x + targetOffset.y * target->getWidth()) * targetBpp; + (targetOffset.x + targetOffset.y * target->getWidth()) * targetBpp;
if (size.x == m_cropSize.x) { if (size.x == m_cropSize.x && target->getWidth() == m_storage->getWidth()) {
/* Develop a connected part of the underlying buffer */ /* Develop a connected part of the underlying buffer */
cvt->convert(source->getPixelFormat(), 1.0f, sourceData, cvt->convert(source->getPixelFormat(), 1.0f, sourceData,
target->getPixelFormat(), target->getGamma(), targetData, target->getPixelFormat(), target->getGamma(), targetData,

View File

@ -334,11 +334,13 @@ void export_render() {
.def("loadScene", &loadScene2, BP_RETURN_VALUE) .def("loadScene", &loadScene2, BP_RETURN_VALUE)
.staticmethod("loadScene"); .staticmethod("loadScene");
Scene *(RenderJob::*renderJob_getScene)(void) = &RenderJob::getScene;
BP_CLASS(RenderJob, Thread, (bp::init<const std::string &, Scene *, RenderQueue *>())) BP_CLASS(RenderJob, Thread, (bp::init<const std::string &, Scene *, RenderQueue *>()))
.def(bp::init<const std::string &, Scene *, RenderQueue *, int, bp::optional<int, int> >()) .def(bp::init<const std::string &, Scene *, RenderQueue *, int, bp::optional<int, int> >())
.def("flush", &RenderJob::flush) .def("flush", &RenderJob::flush)
.def("cancel", renderJob_cancel) .def("cancel", renderJob_cancel)
.def("wait", &RenderJob::wait); .def("wait", &RenderJob::wait)
.def("getScene", renderJob_getScene, BP_RETURN_VALUE);
BP_CLASS(RenderQueue, Object, bp::init<>()) BP_CLASS(RenderQueue, Object, bp::init<>())
.def("getJobCount", &RenderQueue::getJobCount) .def("getJobCount", &RenderQueue::getJobCount)