2011-08-17 16:44:28 +08:00
\section { Python integration}
\label { sec:python}
2012-10-31 12:23:04 +08:00
A recent feature of Mitsuba is a Python interface to the renderer API.
2011-08-17 16:44:28 +08:00
While the interface is still limited at this point, it can already be
2012-10-21 07:27:10 +08:00
used for many useful purposes. To access the API, start your Python
2011-08-19 15:13:18 +08:00
interpreter and enter
2011-08-17 16:44:28 +08:00
\begin { python}
import mitsuba
\end { python}
2012-10-31 12:23:04 +08:00
\paragraph { Mac OS:}
2011-08-17 16:44:28 +08:00
For this to work on MacOS X, you will first have to run the ``\emph { Apple
2011-08-19 15:13:18 +08:00
Menu} $ \to $ \emph { Command-line access} '' menu item from within Mitsuba.
2012-10-31 12:23:04 +08:00
In the unlikely case that you run into shared library loading issues (this is
taken care of by default), you may have to set the \code { LD\_ LIBRARY\_ PATH}
environment variable before starting Python so that it points to where the
Mitsuba libraries are installed (e.g. the \code { Mitsuba.app/Contents/Frameworks}
directory).
2013-08-05 17:35:25 +08:00
When Python crashes directly after the \code { import mitsuba} statement,
make sure that Mitsuba is linked against the right Python distribution
(i.e. matching the \code { python} binary you are using). For e.g. Python
2.7, can be done by adjusting the \code { PYTHON27INCLUDE} and
\code { PYTHON27LIBDIR} variables in \code { config.py} . For other versions,
adjust the numbers accordingly.
2012-10-31 12:23:04 +08:00
\paragraph { Windows and Linux:}
2012-10-21 07:01:13 +08:00
On Windows and \emph { non-packaged} Linux builds, you may have to explicitly
specify the required extension search path before issuing the \code { import} command, e.g.:
2011-08-17 16:44:28 +08:00
\begin { python}
2012-10-21 07:01:13 +08:00
import os, sys
2011-08-17 16:44:28 +08:00
2012-10-21 07:01:13 +08:00
# Specify the extension search path on Linux/Windows (may vary depending on your
# setup. If you compiled from source, 'path-to-mitsuba-directory' should be the
# 'dist' subdirectory)
# NOTE: On Windows, specify these paths using FORWARD slashes (i.e. '/' instead of
# '\' to avoid pitfalls with string escaping)
# Configure the search path for the Python extension module
sys.path.append('path-to-mitsuba-directory/python/<python version, e.g. 2.7>')
# Ensure that Python will be able to find the Mitsuba core libraries
os.environ['PATH'] = 'path-to-mitsuba-directory' + os.pathsep + os.environ['PATH']
2011-08-17 16:44:28 +08:00
import mitsuba
\end { python}
2012-10-31 12:23:04 +08:00
In rare cases when running on Linux, it may also be necessary to set the
\code { LD\_ LIBRARY\_ PATH} environment variable before starting Python so that it
points to where the Mitsuba core libraries are installed.
2012-10-21 07:01:13 +08:00
2011-08-22 06:54:13 +08:00
For an overview of the currently exposed API subset, please refer
2011-08-19 15:13:18 +08:00
to the following page: \url { http://www.mitsuba-renderer.org/api/group_ _ libpython.html} .
2013-11-21 08:55:59 +08:00
\subsubsection * { Accessing signatures in an interactive Python shell}
2012-10-21 07:27:10 +08:00
The plugin exports comprehensive Python-style docstrings, hence
the following is an alternative and convenient way of getting information on
classes, function, or entire namespaces when running an interactive Python shell.
\begin { shell}
>>> help(mitsuba.core.Bitmap) # (can be applied to namespaces, classes, functions, etc.)
class Bitmap(Object)
| Method resolution order:
| Bitmap
| Object
| Boost.Python.instance
| _ _ builtin_ _ .object
|
| Methods defined here:
| _ _ init_ _ (...)
| _ _ init_ _ ( (object)arg1, (EPixelFormat)arg2, (EComponentFormat)arg3, (Vector2i)arg4) -> None :
| C++ signature :
| void _ _ init_ _ (_ object*,mitsuba::Bitmap::EPixelFormat,mitsuba::Bitmap::EComponentFormat,mitsuba::TVector2<int>)
|
| _ _ init_ _ ( (object)arg1, (EFileFormat)arg2, (Stream)arg3) -> None :
| C++ signature :
| void _ _ init_ _ (_ object*,mitsuba::Bitmap::EFileFormat,mitsuba::Stream*)
|
| clear(...)
| clear( (Bitmap)arg1) -> None :
| C++ signature :
| void clear(mitsuba::Bitmap { lvalue} )
...
\end { shell}
The docstrings list the currently exported functionality, as well as C++ and Python signatures, but they
2012-10-31 12:23:04 +08:00
don't document what these functions actually do. The web API documentation is
the preferred source of this information.
2012-10-21 07:27:10 +08:00
2011-08-22 06:54:13 +08:00
\subsection { Basics}
Generally, the Python API tries to mimic the C++ API as closely as possible.
Where applicable, the Python classes and methods replicate overloaded operators,
2012-10-21 07:27:10 +08:00
overridable virtual function calls, and default arguments. Under rare circumstances,
some features are inherently non-portable due to fundamental differences between the
2011-08-22 06:54:13 +08:00
two programming languages. In this case, the API documentation will contain further
information.
2011-08-19 15:13:18 +08:00
2011-08-22 06:54:13 +08:00
Mitsuba's linear algebra-related classes are usable with essentially the
2012-10-21 07:27:10 +08:00
same syntax as their C++ versions --- for example, the following snippet
2011-08-22 06:54:13 +08:00
creates and rotates a unit vector.
2011-08-19 15:13:18 +08:00
\begin { python}
import mitsuba
from mitsuba.core import *
2011-08-22 06:54:13 +08:00
# Create a normalized direction vector
2011-08-19 15:13:18 +08:00
myVector = normalize(Vector(1.0, 2.0, 3.0))
2011-08-22 06:54:13 +08:00
# 90 deg. rotation around the Y axis
trafo = Transform.rotate(Vector(0, 1, 0), 90)
2011-08-19 15:13:18 +08:00
2011-08-22 06:54:13 +08:00
# Apply the rotation and display the result
print(trafo * myVector)
2011-08-19 15:13:18 +08:00
\end { python}
2011-08-22 06:54:13 +08:00
\subsection { Recipes}
The following section contains a series of ``recipes'' on how to do
certain things with the help of the Python bindings.
\subsubsection { Loading a scene}
The following script demonstrates how to use the
2012-10-21 07:27:10 +08:00
\code { FileResolver} and \code { SceneHandler} classes to
2011-08-22 06:54:13 +08:00
load a Mitsuba scene from an XML file:
\begin { python}
import mitsuba
from mitsuba.core import *
from mitsuba.render import SceneHandler
# Get a reference to the thread's file resolver
fileResolver = Thread.getThread().getFileResolver()
2012-10-21 06:08:36 +08:00
# Register any searchs path needed to load scene resources (optional)
2012-09-28 00:43:51 +08:00
fileResolver.appendPath('<path to scene directory>')
2011-08-22 06:54:13 +08:00
2012-10-21 07:27:10 +08:00
# Optional: supply parameters that can be accessed
2011-08-22 06:54:13 +08:00
# by the scene (e.g. as $ \text { \color { lstcomment } \itshape \texttt { \$ } } $ myParameter)
paramMap = StringMap()
paramMap['myParameter'] = 'value'
# Load the scene from an XML file
2011-08-23 14:02:44 +08:00
scene = SceneHandler.loadScene(fileResolver.resolve("scene.xml"), paramMap)
2011-08-22 06:54:13 +08:00
# Display a textual summary of the scene's contents
print(scene)
\end { python}
\subsubsection { Rendering a loaded scene}
Once a scene has been loaded, it can be rendered as follows:
\begin { python}
from mitsuba.core import *
from mitsuba.render import RenderQueue, RenderJob
import multiprocessing
scheduler = Scheduler.getInstance()
# Start up the scheduling system with one worker per local core
for i in range(0, multiprocessing.cpu_ count()):
2013-10-18 15:29:28 +08:00
scheduler.registerWorker(LocalWorker(i, 'wrk%i' % i))
2011-08-22 06:54:13 +08:00
scheduler.start()
# Create a queue for tracking render jobs
queue = RenderQueue()
scene.setDestinationFile('renderedResult')
# Create a render job and insert it into the queue
job = RenderJob('myRenderJob', scene, queue)
job.start()
# Wait for all jobs to finish and release resources
queue.waitLeft(0)
queue.join()
# Print some statistics about the rendering process
print(Statistics.getInstance().getStats())
\end { python}
\subsubsection { Rendering over the network}
To render over the network, you must first set up one or
more machines that run the \code { mtssrv} server (see \secref { mtssrv} ).
A network node can then be registered with the scheduler as follows:
\begin { python}
# Connect to a socket on a named host or IP address
# 7554 is the default port of 'mtssrv'
stream = SocketStream('128.84.103.222', 7554)
# Create a remote worker instance that communicates over the stream
remoteWorker = RemoteWorker('netWorker', stream)
scheduler = Scheduler.getInstance()
# Register the remote worker (and any other potential workers)
scheduler.registerWorker(remoteWorker)
scheduler.start()
\end { python}
\subsubsection { Constructing custom scenes from Python}
2012-10-21 07:27:10 +08:00
Dynamically constructing Mitsuba scenes entails loading a series of external
plugins, instantiating them with custom parameters, and finally assembling
them into an object graph.
2011-08-22 06:54:13 +08:00
For instance, the following snippet shows how to create a basic
2012-09-28 00:43:51 +08:00
perspective sensor with a film that writes PNG images:
2011-08-22 06:54:13 +08:00
\begin { python}
from mitsuba.core import *
pmgr = PluginManager.getInstance()
# Encodes parameters on how to instantiate the 'perspective' plugin
2012-09-28 00:43:51 +08:00
sensorProps = Properties('perspective')
sensorProps['toWorld'] = Transform.lookAt(
2011-08-22 06:54:13 +08:00
Point(0, 0, -10), # Camera origin
Point(0, 0, 0), # Camera target
Vector(0, 1, 0) # 'up' vector
)
2012-09-28 00:43:51 +08:00
sensorProps['fov'] = 45.0
2011-08-22 06:54:13 +08:00
2012-09-28 00:43:51 +08:00
# Encodes parameters on how to instantiate the 'ldrfilm' plugin
filmProps = Properties('ldrfilm')
2011-08-22 06:54:13 +08:00
filmProps['width'] = 1920
filmProps['height'] = 1080
# Load and instantiate the plugins
2012-09-28 00:43:51 +08:00
sensor = pmgr.createObject(sensorProps)
2011-08-22 06:54:13 +08:00
film = pmgr.createObject(filmProps)
2012-09-28 00:43:51 +08:00
# First configure the film and then add it to the sensor
2011-08-22 06:54:13 +08:00
film.configure()
2012-09-28 00:43:51 +08:00
sensor.addChild('film', film)
2011-08-22 06:54:13 +08:00
2012-09-28 00:43:51 +08:00
# Now, the sensor can be configured
sensor.configure()
2011-08-22 06:54:13 +08:00
\end { python}
2012-10-21 07:27:10 +08:00
The above code fragment uses the plugin manager to construct a
\code { Sensor} instance from an external plugin named
2011-08-22 06:54:13 +08:00
\texttt { perspective.so/dll/dylib} and adds a child object
named \texttt { film} , which is a \texttt { Film} instance loaded from the
2012-09-28 00:43:51 +08:00
plugin \texttt { ldrfilm.so/dll/dylib} .
2011-08-22 06:54:13 +08:00
Each time after instantiating a plugin, all child objects are added, and
finally the plugin's \code { configure()} method must be called.
Creating scenes in this manner ends up being rather laborious.
Since Python comes with a powerful dynamically-typed dictionary
2012-10-21 07:27:10 +08:00
primitive, Mitsuba additionally provides a more ``pythonic''
2011-08-22 06:54:13 +08:00
alternative that makes use of this facility:
\begin { python}
from mitsuba.core import *
pmgr = PluginManager.getInstance()
2012-09-28 00:43:51 +08:00
sensor = pmgr.create({
2011-08-22 06:54:13 +08:00
'type' : 'perspective',
'toWorld' : Transform.lookAt(
Point(0, 0, -10),
Point(0, 0, 0),
Vector(0, 1, 0)
),
'film' : {
2012-09-28 00:43:51 +08:00
'type' : 'ldrfilm',
2011-08-22 06:54:13 +08:00
'width' : 1920,
'height' : 1080
}
} )
\end { python}
This code does exactly the same as the previous snippet.
By the time \code { PluginManager.create} returns, the object
2012-10-21 07:27:10 +08:00
hierarchy has already been assembled, and the
2011-08-22 06:54:13 +08:00
\code { configure()} method of every object
has been called.
Finally, here is an full example that creates a basic scene
2012-10-21 07:27:10 +08:00
which can be rendered. It describes a sphere lit by a point
2011-08-22 06:54:13 +08:00
light, rendered using the direct illumination integrator.
\begin { python}
from mitsuba.core import *
from mitsuba.render import Scene
scene = Scene()
2012-09-28 00:43:51 +08:00
# Create a sensor, film & sample generator
2011-08-22 06:54:13 +08:00
scene.addChild(pmgr.create({
'type' : 'perspective',
'toWorld' : Transform.lookAt(
Point(0, 0, -10),
Point(0, 0, 0),
Vector(0, 1, 0)
),
'film' : {
2012-09-28 00:43:51 +08:00
'type' : 'ldrfilm',
2011-08-22 06:54:13 +08:00
'width' : 1920,
'height' : 1080
} ,
'sampler' : {
'type' : 'ldsampler',
'sampleCount' : 2
}
} ))
# Set the integrator
scene.addChild(pmgr.create({
'type' : 'direct'
} ))
# Add a light source
scene.addChild(pmgr.create({
'type' : 'point',
'position' : Point(5, 0, -10),
'intensity' : Spectrum(100)
} ))
# Add a shape
scene.addChild(pmgr.create({
'type' : 'sphere',
'center' : Point(0, 0, 0),
'radius' : 1.0,
'bsdf' : {
'type' : 'diffuse',
'reflectance' : Spectrum(0.4)
}
} ))
2011-08-22 12:17:55 +08:00
scene.configure()
2011-08-22 06:54:13 +08:00
\end { python}
\subsubsection { Taking control of the logging system}
2011-08-20 15:36:40 +08:00
Many operations in Mitsuba will print one or more log messages
during their execution. By default, they will be printed to the console,
which may be undesirable. Similar to the C++ side, it is possible to define
custom \code { Formatter} and \code { Appender} classes to interpret and direct
2012-09-28 00:43:51 +08:00
the flow of these messages. This is also useful to keep track of the progress
of rendering jobs.
2011-08-17 16:44:28 +08:00
2011-08-20 15:36:40 +08:00
Roughly, a \code { Formatter} turns detailed
information about a logging event into a human-readable string, and a
\code { Appender} routes it to some destination (e.g. by appending it to
a file or a log viewer in a graphical user interface). Here is an example
of how to activate such extensions:
\begin { python}
import mitsuba
from mitsuba.core import *
class MyFormatter(Formatter):
def format(self, logLevel, sourceClass, sourceThread, message, filename, line):
2011-08-22 06:54:13 +08:00
return '%s (log level: %s, thread: %s, class %s, file %s, line %i)' % \
2012-10-21 07:27:10 +08:00
(message, str(logLevel), sourceThread.getName(), sourceClass,
2011-08-20 15:36:40 +08:00
filename, line)
class MyAppender(Appender):
def append(self, logLevel, message):
print(message)
def logProgress(self, progress, name, formatted, eta):
2011-08-22 06:54:13 +08:00
print('Progress message: ' + formatted)
2011-08-20 15:36:40 +08:00
# Get the logger associated with the current thread
logger = Thread.getThread().getLogger()
logger.setFormatter(MyFormatter())
logger.clearAppenders()
logger.addAppender(MyAppender())
logger.setLogLevel(EDebug)
2011-08-22 06:54:13 +08:00
Log(EInfo, 'Test message')
2011-08-20 15:36:40 +08:00
\end { python}
2013-02-18 05:58:39 +08:00
\subsubsection { Rendering a turntable animation with motion blur}
Rendering a turntable animation is a fairly common task that is
conveniently accomplished via the Python interface. In a turntable
video, the camera rotates around a completely static object or scene.
The following snippet does this for the material test ball scene downloadable
on the main website, complete with motion blur. It assumes that the
scene and scheduler have been set up approriately using one of the previous
snippets.
\begin { python}
sensor = scene.getSensor()
sensor.setShutterOpen(0)
sensor.setShutterOpenTime(1)
stepSize = 5
for i in range(0,360 / stepSize):
2013-02-18 07:28:53 +08:00
rotationCur = Transform.rotate(Vector(0, 0, 1), i*stepSize);
rotationNext = Transform.rotate(Vector(0, 0, 1), (i+1)*stepSize);
2013-02-18 05:58:39 +08:00
trafoCur = Transform.lookAt(rotationCur * Point(0,-6,4),
Point(0, 0, .5), rotationCur * Vector(0, 1, 0))
trafoNext = Transform.lookAt(rotationNext * Point(0,-6,4),
Point(0, 0, .5), rotationNext * Vector(0, 1, 0))
atrafo = AnimatedTransform()
atrafo.appendTransform(0, trafoCur)
atrafo.appendTransform(1, trafoNext)
atrafo.sortAndSimplify()
sensor.setWorldTransform(atrafo)
scene.setDestinationFile('frame_ %03i.png' % i)
job = RenderJob('job_ %i' % i, scene, queue)
job.start()
queue.waitLeft(0)
queue.join()
\end { python}
2013-02-18 08:03:01 +08:00
A useful property of this approach is that scene loading and initialization
must only take place once. Performance-wise, this compares favourably with
running many separate rendering jobs, e.g. using the \code { mitsuba}
command-line executable.
2013-11-18 23:46:42 +08:00
2013-11-21 18:52:29 +08:00
\subsubsection { Creating triangle-based shapes}
2013-11-18 23:46:42 +08:00
It is possible to create new triangle-based shapes directly in Python, though
doing so is discouraged: because Python is an interpreted programming language,
the construction of large meshes will run very slowly. The builtin shapes
and shape loaders are to be preferred when this is an option. That said, the
following snippet shows how to create \code { TriMesh} objects from within Python:
\begin { python}
# Create a new mesh with 1 triangle, 3 vertices,
# and allocate buffers for normals and texture coordinates
mesh = TriMesh('Name of this mesh', 1, 3, True, True)
v = mesh.getVertexPositions()
v[0] = Point3(0, 0, 0)
v[1] = Point3(1, 0, 0)
v[2] = Point3(0, 1, 0)
n = mesh.getVertexNormals()
n[0] = Normal(0, 0, 1)
n[1] = Normal(0, 0, 1)
n[2] = Normal(0, 0, 1)
t = mesh.getTriangles() # Indexed triangle list: tri 1 references vertices 0,1,2
t[0] = 0
t[1] = 1
t[2] = 2
uv = mesh.getTexcoords()
uv[0] = Point2(0, 0)
uv[1] = Point2(1, 0)
uv[2] = Point2(0, 1)
mesh.configure()
# Add to a scene (assumes 'scene' is available)
sensor.addChild(mesh)
\end { python}
2013-11-21 17:31:51 +08:00
2013-11-25 23:55:44 +08:00
\subsubsection { Calling Mitsuba functions from a multithread Python program}
By default, Mitsuba assumes that threads accessing Mitsuba-internal
data structures were created by (or at least registered with) Mitsuba. This is the
case for the main thread and subclasses of \code { mitsuba.core.Thread} . When a
Mitsuba function is called from an event dispatch thread of a multithreaded
Python application that is not known to Mitsuba, an exception or crash will result.
To avoid this, get a reference to the main thread right after loading the Mitsuba plugin
and save some related state (the attached \code { FileResolver} and \code { Logger} instances).
\begin { python}
mainThread = Thread.getThead()
saved_ fresolver = mainThread.getFileResolver()
saved_ logger = mainThread.getLogger()
\end { python}
Later when accessed from an unregister thread, execute the following:
\begin { python}
# This rendering thread was not created by Mitsuba -- register it
newThread = Thread.registerUnmanagedThread('render')
newThread.setFileResolver(saved_ fresolver)
newThread.setLogger(saved_ logger)
\end { python}
It is fine to execute this several times (\code { registerUnmanagedThread} just returns
a reference to the associated \code { Thread} instance if it was already registered).
2013-12-02 01:22:01 +08:00
\subsubsection { Mitsuba interaction with PyQt/PySide (simple version)}
2013-11-21 17:31:51 +08:00
The following listing contains a complete program that
renders a sphere and efficiently displays it in a PyQt window
2013-11-21 18:52:29 +08:00
(to make this work in PySide, change all occurrences of \code { PyQt4} to \code { PySide} in the
import declarations and rename the function call to \code { getNativeBuffer()} to \code { toByteArray()} ,
which is a tiny bit less efficient).
2013-11-21 17:31:51 +08:00
\begin { python}
import mitsuba, multiprocessing, sys
from mitsuba.core import Scheduler, PluginManager, \
LocalWorker, Properties, Bitmap, Point2i, FileStream
from mitsuba.render import RenderQueue, RenderJob, Scene
from PyQt4.QtCore import QPoint
from PyQt4.QtGui import QApplication, QMainWindow, QPainter, QImage
class MitsubaView(QMainWindow):
def _ _ init_ _ (self):
super(MitsubaView, self)._ _ init_ _ ()
2013-11-27 20:25:33 +08:00
self.setWindowTitle('Mitsuba/PyQt demo')
2013-11-21 17:31:51 +08:00
self.initializeMitsuba()
self.image = self.render(self.createScene())
self.resize(self.image.width(), self.image.height())
def initializeMitsuba(self):
# Start up the scheduling system with one worker per local core
self.scheduler = Scheduler.getInstance()
for i in range(0, multiprocessing.cpu_ count()):
self.scheduler.registerWorker(LocalWorker(i, 'wrk%i' % i))
self.scheduler.start()
# Create a queue for tracking render jobs
self.queue = RenderQueue()
# Get a reference to the plugin manager
self.pmgr = PluginManager.getInstance()
def shutdownMitsuba(self):
self.queue.join()
self.scheduler.stop()
def createScene(self):
# Create a simple scene containing a sphere
sphere = self.pmgr.createObject(Properties("sphere"))
sphere.configure()
scene = Scene()
scene.addChild(sphere)
scene.configure()
# Don't automatically write an output bitmap file when the
# rendering process finishes (want to control this from Python)
scene.setDestinationFile('')
return scene
def render(self, scene):
# Create a render job and insert it into the queue
job = RenderJob('myRenderJob', scene, self.queue)
job.start()
# Wait for the job to finish
self.queue.waitLeft(0)
# Develop the camera's film into an 8 bit sRGB bitmap
film = scene.getFilm()
size = film.getSize()
bitmap = Bitmap(Bitmap.ERGB, Bitmap.EUInt8, size)
film.develop(Point2i(0, 0), size, Point2i(0, 0), bitmap)
# Write to a PNG bitmap file
outFile = FileStream("rendering.png", FileStream.ETruncReadWrite)
bitmap.write(Bitmap.EPNG, outFile)
outFile.close()
# Also create a QImage (using a fast memory copy in C++)
2013-11-25 23:55:44 +08:00
return QImage(bitmap.getNativeBuffer(),
2013-11-21 17:31:51 +08:00
size.x, size.y, QImage.Format_ RGB888)
def paintEvent(self, event):
painter = QPainter(self)
painter.drawImage(QPoint(0, 0), self.image)
2013-11-26 21:30:49 +08:00
painter.end()
2013-11-21 17:31:51 +08:00
def main():
app = QApplication(sys.argv)
view = MitsubaView()
view.show()
2013-11-25 19:44:21 +08:00
view.raise_ ()
2013-11-21 17:31:51 +08:00
retval = app.exec_ ()
view.shutdownMitsuba()
sys.exit(retval)
if _ _ name_ _ == '_ _ main_ _ ':
main()
\end { python}
2013-11-21 18:52:29 +08:00
2013-12-02 01:22:01 +08:00
\subsubsection { Mitsuba interaction with PyQt/PySide (fancy)}
2013-11-25 23:55:44 +08:00
The following snippet is a much fancier version of the previous PyQt/PySide example.
Instead of waiting for the rendering to finish and then displaying it, this example launches the
rendering in the background and uses Mitsuba's \code { RenderListener} interface to update the
view and show image blocks as they are being rendered.
As before, some changes will be necessary to get this to run on PySide.
\begin { center}
\includegraphics [width=10cm] { images/python_ demo.jpg}
\end { center}
When using this snippet, please be wary of threading-related issues; the key thing to remember is that
in Qt, only the main thread is allowed to modify Qt widgets. On the other hand, rendering and logging-related
callbacks will be invoked from different Mitsuba-internal threads---this means that it's not possible to e.g.
directly update the status bar message from the callback \code { finishJobEvent} . To do this, we must use
use Qt's \code { QueuedConnection} to communicate this event to the main thread via signals and slots. See the
code that updates the status and progress bar for more detail.
2013-11-21 18:52:29 +08:00
\begin { python}
2013-11-25 23:55:44 +08:00
import mitsuba, multiprocessing, sys, time
2013-11-21 18:52:29 +08:00
2013-11-25 23:55:44 +08:00
from mitsuba.core import Scheduler, PluginManager, Thread, Vector, Point2i, \
2013-11-27 05:54:26 +08:00
Vector2i, LocalWorker, Properties, Bitmap, Spectrum, Appender, EWarn, \
Transform, FileStream
2013-11-25 23:55:44 +08:00
from mitsuba.render import RenderQueue, RenderJob, Scene, RenderListener
2013-11-27 05:54:26 +08:00
from PyQt4.QtCore import Qt, QPoint, QSize, QRect, pyqtSignal
from PyQt4.QtGui import QApplication, QMainWindow, QPainter, QImage, \
QProgressBar, QWidget, QSizePolicy
Signal = pyqtSignal
class MitsubaRenderBuffer(RenderListener):
"""
Implements the Mitsuba callback interface to capture notifications about
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
2014-02-05 04:05:13 +08:00
GEOMETRY_ CHANGED = 3
2013-11-27 05:54:26 +08:00
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)
2013-11-27 20:25:33 +08:00
self.bitmap.drawWorkUnit(wu.getOffset(), wu.getSize(), thr)
2013-11-27 05:54:26 +08:00
self._ potentially_ send_ update()
2013-12-03 18:50:20 +08:00
def workEndEvent(self, job, wr, cancelled):
2013-11-27 05:54:26 +08:00
""" 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)
2014-02-05 04:05:13 +08:00
self._ potentially_ send_ update()
2013-11-27 05:54:26 +08:00
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()
2013-11-25 23:55:44 +08:00
2013-11-27 05:54:26 +08:00
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)
2014-02-05 04:05:13 +08:00
self.callback(MitsubaRenderBuffer.GEOMETRY_ CHANGED)
2013-11-27 05:54:26 +08:00
return film
2014-02-05 04:05:13 +08:00
def _ potentially_ send_ update(self):
2013-11-27 05:54:26 +08:00
""" Send an update request to any attached widgets, but not too often """
now = time.time()
2014-02-05 04:05:13 +08:00
if now - self.time > .25:
2013-11-27 05:54:26 +08:00
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)
2014-02-05 04:05:13 +08:00
def _ _ init_ _ (self, parent, queue, default_ size = Vector2i(0, 0)):
2013-11-27 05:54:26 +08:00
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)
2014-02-05 04:05:13 +08:00
self.default_ size = default_ size
2013-11-27 05:54:26 +08:00
def sizeHint(self):
2014-02-05 04:05:13 +08:00
size = self.buffer.size if not self.buffer.size.isZero() else self.default_ size
return QSize(size.x, size.y)
2013-11-27 05:54:26 +08:00
def _ handle_ update(self, event):
image = self.buffer.qimage
# Detect when an image of different resolution is being rendered
2014-02-05 04:05:13 +08:00
if image.width() > self.width() or image.height() > self.height():
2013-11-27 05:54:26 +08:00
self.updateGeometry()
self.repaint()
2013-11-25 23:55:44 +08:00
2013-11-27 05:54:26 +08:00
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)
2013-11-25 23:55:44 +08:00
def _ _ init_ _ (self):
2013-11-27 05:54:26 +08:00
super(MitsubaDemo, self)._ _ init_ _ ()
2013-11-26 01:59:12 +08:00
# Initialize Mitsuba
2013-11-25 23:55:44 +08:00
self.initializeMitsuba()
2013-11-26 01:59:12 +08:00
self.job = self.createRenderJob()
2014-02-05 04:05:13 +08:00
self.job.setInteractive(True)
2013-11-27 05:54:26 +08:00
2013-11-26 01:59:12 +08:00
# Initialize the user interface
2013-11-25 23:55:44 +08:00
status = self.statusBar()
2014-02-05 04:05:13 +08:00
self.rwidget = RenderWidget(self, self.queue, self.scene.getFilm().getSize())
2013-11-27 05:54:26 +08:00
progress = QProgressBar(status)
2013-11-25 23:55:44 +08:00
status.setContentsMargins(0,0,5,0)
2013-11-27 05:54:26 +08:00
status.addPermanentWidget(progress)
2013-11-25 23:55:44 +08:00
status.setSizeGripEnabled(False)
2013-11-27 20:25:33 +08:00
self.setWindowTitle('Mitsuba/PyQt demo')
2013-11-27 05:54:26 +08:00
self.setCentralWidget(self.rwidget)
2013-11-25 23:55:44 +08:00
2013-11-27 05:54:26 +08:00
# Hide the scroll bar once the rendering is done
def renderingUpdated(event):
if event == MitsubaRenderBuffer.RENDERING_ FINISHED:
status.showMessage("Done.")
progress.hide()
2013-11-25 23:55:44 +08:00
2013-11-27 05:54:26 +08:00
self.renderProgress.connect(progress.setValue, Qt.QueuedConnection)
self.rwidget.renderingUpdated.connect(renderingUpdated,
2013-11-25 23:55:44 +08:00
Qt.QueuedConnection)
2013-11-27 05:54:26 +08:00
2013-11-26 01:59:12 +08:00
# Start the rendering process
2013-11-25 23:55:44 +08:00
status.showMessage("Rendering ..")
2013-11-26 01:59:12 +08:00
self.job.start()
2013-11-25 23:55:44 +08:00
def initializeMitsuba(self):
# Start up the scheduling system with one worker per local core
self.scheduler = Scheduler.getInstance()
for i in range(0, multiprocessing.cpu_ count()):
self.scheduler.registerWorker(LocalWorker(i, 'wrk%i' % i))
self.scheduler.start()
# Create a queue for tracking render jobs
self.queue = RenderQueue()
# Get a reference to the plugin manager
self.pmgr = PluginManager.getInstance()
2013-11-27 05:54:26 +08:00
# Process Mitsuba log and progress messages within Python
2013-11-25 23:55:44 +08:00
class CustomAppender(Appender):
def append(self2, logLevel, message):
print(message)
def logProgress(self2, progress, name, formatted, eta):
2013-11-27 05:54:26 +08:00
# Asynchronously notify the main thread
2013-11-25 23:55:44 +08:00
self.renderProgress.emit(progress)
logger = Thread.getThread().getLogger()
2013-11-27 05:54:26 +08:00
logger.setLogLevel(EWarn) # Display warning & error messages
2013-11-25 23:55:44 +08:00
logger.clearAppenders()
logger.addAppender(CustomAppender())
def closeEvent(self, e):
self.job.cancel()
self.queue.join()
self.scheduler.stop()
2013-11-26 01:59:12 +08:00
def createRenderJob(self):
self.scene = self.pmgr.create({
2013-11-25 23:55:44 +08:00
'type' : 'scene',
'sphere' : {
'type' : 'sphere',
} ,
'envmap' : {
'type' : 'sunsky'
} ,
'sensor' : {
'type' : 'perspective',
'toWorld' : Transform.translate(Vector(0, 0, -5)),
'sampler' : {
'type' : 'halton',
'sampleCount' : 64
}
}
} )
2013-11-26 01:59:12 +08:00
return RenderJob('rjob', self.scene, self.queue)
2013-11-25 23:55:44 +08:00
def keyPressEvent(self, e):
if e.key() == Qt.Key_ Escape:
self.close()
def main():
import signal
# Stop the program upon Ctrl-C (SIGINT)
signal.signal(signal.SIGINT, signal.SIG_ DFL)
app = QApplication(sys.argv)
2013-11-27 05:54:26 +08:00
demo = MitsubaDemo()
demo.show()
demo.raise_ ()
2013-11-25 23:55:44 +08:00
retval = app.exec_ ()
sys.exit(retval)
if _ _ name_ _ == '_ _ main_ _ ':
main()
2013-11-21 18:52:29 +08:00
\end { python}
2013-11-25 23:55:44 +08:00
2013-12-02 01:22:01 +08:00
\subsubsection { Mitsuba interaction with NumPy}
Suppose that \code { bitmap} contains a \code { mitsuba.core.Bitmap} instance (e.g. a rendering). Then the following snippet efficiently turns the image into a NumPy array:
\begin { python}
import numpy as np
array = np.array(bitmap.getNativeBuffer())
\end { python}