3
$\begingroup$

I rendered and exported a scene with cycles as MultiChannel OpenEXR, then read it back in with OpenEXR binding for Python. The header looks like below. As it indicates, most of the field are floats (32 bits as I've saved with FULL option). I expect them to be in the range of [0, 1] which I can reconstruct to RGB value by multiplying with 255. However, it turns out that the values don't belong to any specific range which is confusing me. For example, the min and max values of each channels of the Composite.Combined are:

[(0.0, 270.6739501953125), (0.0, 221.4493865966797), (0.0, 106.66129302978516)]

So what do these values mean? And is there a way to reconstruct the RGB image that Blender renders? My intention is to simulate the way Blender render with all the passes.

PS: simply normalize these values to the range [0, 255] does not work. Somehow most of the pixels are around 1. or 2., but some get very large.

My code to extract image data is as follows:

exrFile = OpenEXR.InputFile('fallroad_0001.exr')
header = exrFile.header()
dw = header['dataWindow']
pt = Imath.PixelType(Imath.PixelType.FLOAT)
size = (dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1)

cc_r = np.fromstring(exrFile.channel('RenderLayer.Combined.R', pt), dtype=np.float32)
cc_g = np.fromstring(exrFile.channel('RenderLayer.Combined.G', pt), dtype=np.float32)
cc_b = np.fromstring(exrFile.channel('RenderLayer.Combined.B', pt), dtype=np.float32)
cc_r.shape = cc_g.shape = cc_b.shape = (size[1], size[0])
cc = np.dstack((cc_r, cc_g, cc_b))

The header information is as follows

{'BlenderMultiChannel': 'Blender V2.55.1 and newer',
'Camera': 'Camera',
'Date': '2016/10/17 10:23:17',
'File': '/home/FallRoad/FallRoad_render1.blend',
'Frame': '015',
'RenderTime': '11:30.66',
'Scene': 'Scene',
'Time': '00:00:00:15',
'channels': {'Composite.Combined.A': FLOAT (1, 1),
'Composite.Combined.B': FLOAT (1, 1),
'Composite.Combined.G': FLOAT (1, 1),
'Composite.Combined.R': FLOAT (1, 1),
'RenderLayer.Combined.A': FLOAT (1, 1),
'RenderLayer.Combined.B': FLOAT (1, 1),
'RenderLayer.Combined.G': FLOAT (1, 1),
'RenderLayer.Combined.R': FLOAT (1, 1),
'RenderLayer.Depth.Z': FLOAT (1, 1),
'RenderLayer.DiffCol.B': FLOAT (1, 1),
'RenderLayer.DiffCol.G': FLOAT (1, 1),
'RenderLayer.DiffCol.R': FLOAT (1, 1),
'RenderLayer.DiffDir.B': FLOAT (1, 1),
'RenderLayer.DiffDir.G': FLOAT (1, 1),
'RenderLayer.DiffDir.R': FLOAT (1, 1),
'RenderLayer.DiffInd.B': FLOAT (1, 1),
'RenderLayer.DiffInd.G': FLOAT (1, 1),
'RenderLayer.DiffInd.R': FLOAT (1, 1),
'RenderLayer.Emit.B': FLOAT (1, 1),
'RenderLayer.Emit.G': FLOAT (1, 1),
'RenderLayer.Emit.R': FLOAT (1, 1),
'RenderLayer.GlossCol.B': FLOAT (1, 1),
'RenderLayer.GlossCol.G': FLOAT (1, 1),
'RenderLayer.GlossCol.R': FLOAT (1, 1),
'RenderLayer.GlossDir.B': FLOAT (1, 1),
'RenderLayer.GlossDir.G': FLOAT (1, 1),
'RenderLayer.GlossDir.R': FLOAT (1, 1),
'RenderLayer.GlossInd.B': FLOAT (1, 1),
'RenderLayer.GlossInd.G': FLOAT (1, 1),
'RenderLayer.GlossInd.R': FLOAT (1, 1),
'RenderLayer.IndexMA.X': FLOAT (1, 1),
'RenderLayer.IndexOB.X': FLOAT (1, 1),
'RenderLayer.Shadow.B': FLOAT (1, 1),
'RenderLayer.Shadow.G': FLOAT (1, 1),
'RenderLayer.Shadow.R': FLOAT (1, 1),
'RenderLayer.SubsurfaceCol.B': FLOAT (1, 1),
'RenderLayer.SubsurfaceCol.G': FLOAT (1, 1),
'RenderLayer.SubsurfaceCol.R': FLOAT (1, 1),
'RenderLayer.SubsurfaceDir.B': FLOAT (1, 1),
'RenderLayer.SubsurfaceDir.G': FLOAT (1, 1),
'RenderLayer.SubsurfaceDir.R': FLOAT (1, 1),
'RenderLayer.SubsurfaceInd.B': FLOAT (1, 1),
'RenderLayer.SubsurfaceInd.G': FLOAT (1, 1),
'RenderLayer.SubsurfaceInd.R': FLOAT (1, 1),
'RenderLayer.TransCol.B': FLOAT (1, 1),
'RenderLayer.TransCol.G': FLOAT (1, 1),
'RenderLayer.TransCol.R': FLOAT (1, 1),
'RenderLayer.TransDir.B': FLOAT (1, 1),
'RenderLayer.TransDir.G': FLOAT (1, 1),
'RenderLayer.TransDir.R': FLOAT (1, 1),
'RenderLayer.TransInd.B': FLOAT (1, 1),
'RenderLayer.TransInd.G': FLOAT (1, 1),
'RenderLayer.TransInd.R': FLOAT (1, 1),
'RenderLayer.Vector.W': FLOAT (1, 1),
'RenderLayer.Vector.X': FLOAT (1, 1),
'RenderLayer.Vector.Y': FLOAT (1, 1),
'RenderLayer.Vector.Z': FLOAT (1, 1)},
'compression': NO_COMPRESSION,
'dataWindow': (0, 0) - (1919, 1079),
'displayWindow': (0, 0) - (1919, 1079),
'lineOrder': INCREASING_Y,
'pixelAspectRatio': 1.0,
'screenWindowCenter': (0.0, 0.0),
'screenWindowWidth': 1.0}

Edited: I include here the images that I have hand-on. From left to right: the image display with original float value, the image with float cut-off at 1 (all > 1 become 1) and image rendered by Blender enter image description here

$\endgroup$

3 Answers 3

4
$\begingroup$

I will use nodes rather than code, since they are more visual, hence easier to understand.

I will use the following example scene.

  • Diffuse Walls (1, 1, 1)
  • Emission (1, 1, 1) with 0.9 strength
  • Emission (red / blue) with 32 strength
  • Cube consiting equally of Diffuse, Glossy, Tranmissive and Scattered shaders (all with different colors)

enter image description here

Rendering and saving the scene as a .exr file will have the main advantage of color correcting all channels individually.

The 32 bit float values ensure, that even extremely dark values will have proper colors and show no banding when multiplying with numbers larger than 1.
enter image description here

Some areas of the image appear very bright. They have RGB values above (1, 1, 1). Higher values can only be displayed as (1, 1, 1), since monitors can't get brighter than white. These areas will still display the correct colors after multiplying them with numbers smaller than one.
enter image description here

A pixel with numbers larger than 1 like (4, 1, 1) will be display as (1, 1, 1) [white]. But after doing a color operation like dividing by half the floating bit pixel will still have the correct relative color (2, 0.5, 0.5) [red]. If the pixel would have been clamped to (1, 1, 1), the operation would produce a gray pixels (0.5, 0.5, 0.5).
enter image description here

The RGB channels should be mapped correct already. (0, 0, 0) producing black and (1, 1, 1) producing white. All lower values than 0 will show as black, all higher values than 1 will show as white.

An .exr can contain more RGB information than monitors can display.


Mapping Blenders output channels.

The beauty pass (combined) is made up from:
Emit
DiffDir, DiffInd, DiffCol
GlossDir, GlossInd, GlossCol
TransDir, TransInd, TransCol
SubsurfaceDir, SubsurfaceInd, SubsurfaceCol

All other channels have additional information (e.g. shadow), which is not used/needed to construct the rendered image.

For each of the pass types (Diffuse, Glossy, Transmission, Subsurface), the procedure will be to add the Direct and the Indirect, then multiply them with the Color. Direct and Indirect passes are unpremultiplied, Color passes are premultiplied. Hence the product of the three will be premultiplied.

(Dir + Ind) * Col

dir ind col

The resulting five light pass types are Emission, Diffuse, Glossy, Transmission, Subsurface. Since the example cubes shader has different colors for each pass type, we can easily identify then. enter image description here

Each pass type will add to the final image.

Emit + [(DiffDir + DiffInd) * DiffCol]
  + [(GlossDir + GlossInd) * GlossCol]
  + [(TransDir + TransInd) * TransCol]
  + [(SubsurfaceDir + SubsurfaceInd) * SubsurfaceCol]

The exr can be loaded into the compositor and the passes combined as explained. The final Add node holds the same information as the Combined pass.
enter image description here

$\endgroup$
5
  • $\begingroup$ to confirm, the formula to derive the final image is done on plain float values, i.e. the results could be stacked up to very high values? In that case, how can I get the RBG values that Blender uses in its final images? What is the good way to scale down the high-range value to the normal monitor-friendly values since the usual min-max to 0-1 wouldn't work? $\endgroup$
    – AugLe
    Commented Oct 18, 2016 at 8:27
  • $\begingroup$ After adding the channels together, no scaling needs to be done. You can simply ignore all higher values. If values higher than 1 cause a problem, you can clamp them to one. It should not look any different $\endgroup$
    – Leander
    Commented Oct 18, 2016 at 9:41
  • $\begingroup$ I've edited my question with sample images following your idea, but it look better now but still so dark. Any idea why? $\endgroup$
    – AugLe
    Commented Oct 18, 2016 at 10:10
  • $\begingroup$ Here's one thing that I missed, that is the color display in Blender's render or png image is in sRGB space (generally display space), which is low-range, while the openexr (and what Blender stores internally) is in linear space (much higher range). So just simply flooring down everything higher than 1 back to 1 wouldn't work. I found an answer for my problem, here $\endgroup$
    – AugLe
    Commented Oct 19, 2016 at 8:15
  • 2
    $\begingroup$ Use a proper view transform. This overcomplicates and muddles up the simplicity of a proper view transform. OpenColorIO's ociolutimage works fine for this in post, and can perfectly apply view transforms that you see live in the viewport. $\endgroup$
    – troy_s
    Commented Oct 19, 2016 at 18:30
0
$\begingroup$

The code and everything was fine, the answer of Leander also gives good general view of the problem and shows how the final image of Blender is constructed. Yet, I think the concrete answer to the original question could be: these values contained in an openexr file is in linear space (high-range) while images exported as png is in display space (sRGB, low-range). To convert from linear-space to sRGB, a simple formula is described here convert OpenEXR float to color value

$\endgroup$
0
$\begingroup$

Bumped by the same question (the need to easily/automatically convert multi-layer EXR to sRGB PNG images), i wrote another py-script for this task. Why? OpenEXR python bindings are really hard to setup in Windows environment. I tried a lot. So may be someone find this solution useful too.

This script automatically exports layers from multi-layered EXR into set of PNG images. With proper Linear->sRGB conversion (and keeping alpha where appropriate) and layer naming. It does not use OpenEXR python bindings, it just utilizes oiiotool.exe from OpenImageIO suite

https://github.com/IPv6/kristallum/blob/master/blender/scripts/_exr_extractlayers.py

$\endgroup$
2
  • $\begingroup$ wow, it sounds like a very useful tool. yet, I'm wondering if the similar thing can be done on linux? I have to use Python's OpenEXR package, and to install it is somehow a nightmare to me when moving to a new machine $\endgroup$
    – AugLe
    Commented Jul 17, 2017 at 15:07
  • $\begingroup$ Yes, the same script can used everywhere where oiiotool can be run, so on linux too. I am using it on Windows and MacOs, the only change - path to oiiotool inside script $\endgroup$
    – IPv6
    Commented Jul 18, 2017 at 6:14

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .