New file format: Spectral EXR (support for polarisation, emissive, reflective and bispectral spectrum types)
Spectral EXR
Specification proposal
RGB
We encourage to provide R
, G
and B
channels for compatibility purpose. This follows the OpenEXR recommendation: MultiViewOpenEXR section "Default channel naming". It also allows to get a quick preview of the file with "standard" imaging software.
Spectral data
The spectral data shall be stored as follows:
- A layer for each Stokes component:
S0
,S1
,S2
,S3
. - A channel for each wavelength bin with the following naming convension: '<central_value>'
-
<central_value>
is a positive real number with a comma for separating the floating part. A integer power of ten exponent can be also provided with 'E' or 'e' followed by sign (optional) and integer value. -
<units>
is made of an optional multiplier (n
,m
,p
...) and the units, in Hertz or meters denoted respectivelyHz
andm
.
-
If the image is not polarised, only the S0
layer is populated.
An regex corresponding to this description is: ^(S[0-3])\.(\d*,?\d*([Ee][+-]?\d+)?)(Y|Z|E|P|T|G|M|k|h|da|d|c|m|u|n|p)?(m|Hz)$
. Test there: https://regex101.com/
Bi-spectral data
We can add bi-spectral data later on by adding the non diagonal reradiation part as child layers of the main diagonal.
For instance, the non fluorescent part would be:
S0.520nm
The reradiation at 600nm would be:
S0.520nm.600nm
Custom attributes
OpenEXR allows storage of custom attributes in the header. They recommend using types provided from ImfStandardAtrributes.h
We use optional custom attributes to store:
- A response curve for each
S0
channel withImfStringVectorAttribute
(assumed box function between consecutive bands (middle of two central bands) if not present).^((\d*\.?\d*([eE][-+]?\d+)?)(Y|Z|E|P|T|G|M|k|h|da|d|c|m|u|n|p)?(m|Hz):(\d*\.?\d*([eE][-+]?\d+)?);)*$
Better?
^((\d*\.?\d*([eE][-+]?\d+)?)(Y|Z|E|P|T|G|M|k|h|da|d|c|m|u|n|p)?(m|Hz):(\d*\.?\d*([eE][-+]?\d+)?))(;(\d*\.?\d*([eE][-+]?\d+)?)(Y|Z|E|P|T|G|M|k|h|da|d|c|m|u|n|p)?(m|Hz):(\d*\.?\d*([eE][-+]?\d+)?))*$
- A Mueller matrix for each
S[1-3]
view and channel with aImfMatrixAttribute
(assumed perfect filters if not present).
In addition, for RGB representation, optional custom attributes are:
- A response curve for each
X
,Y
,Z
valuesImfStringVectorAttribute
(assumed CIE-XYZ 1931 2degress if not present). - A matrix for XYZ to RGB conversion
ImfMatrixAttribute
(assumed XYZ to sRGB D65 if not present). - The units for the stored values
ImfStringVectorAttribute
(assumedW.m^2.sr-1
if not present)
Examples
Layers of a non polarised image with 8 bins on [400nm:800nm]
R
G
B
S0.425nm
S0.475nm
S0.525nm
S0.575nm
S0.625nm
S0.675nm
S0.725nm
S0.775nm
Layers of a polarised image with 8 bins on [400nm:800nm]
R
G
B
S0.425nm
S0.475nm
S0.525nm
S0.575nm
S0.625nm
S0.675nm
S0.725nm
S0.775nm
S1.425nm
S1.475nm
S1.525nm
S1.575nm
S1.625nm
S1.675nm
S1.725nm
S1.775nm
S2.425nm
S2.475nm
S2.525nm
S2.575nm
S2.625nm
S2.675nm
S2.725nm
S2.775nm
S3.425nm
S3.475nm
S3.525nm
S3.575nm
S3.625nm
S3.675nm
S3.725nm
S3.775nm
Various accepted layer names:
S0.512,13E-9m
S0.512,13e-9m
S0.512,E-0m
S0.12MHz
S0.,12E2MHz
Non accepted layer names:
S0.512.13E-9m
S0.12
S0.-2MHz
S0.1E-12.2m
S0.1E-12,2m
S4.500nm
Code snippet
#include <iostream>
#include <string>
#include <regex>
#include <algorithm>
#include <map>
bool isSpectralChannel(
const std::string& s,
int & stokes_component,
float & wavelength_nm)
{
const std::map<std::string, float> unit_prefix =
{{"Y", 1e24}, {"Z", 1e21}, {"E", 1e18}, {"P", 1e15}, {"T", 1e12},
{"G", 1e9}, {"M", 1e6}, {"k", 1e3}, {"h", 1e2}, {"da", 1e1},
{"d", 1e-1}, {"c", 1e-2}, {"m", 1e-3}, {"u", 1e-6}, {"n", 1e-9},
{"p", 1e-12}};
const std::regex expr
("^S([0-3])\\.(\\d*,?\\d*([Ee][+-]?\\d+)?)(Y|Z|E|P|T|G|M|k|h|da|d|c|m|u|n|p)?(m|Hz)$");
std::smatch matches;
const bool matched = std::regex_search(s, matches, expr);
if (matched) {
if (matches.size() != 6) {
// Something went wrong with the parsing. This shall not occur.
goto error_parsing;
}
stokes_component = std::stoi(matches[1].str());
// Get value
std::string central_value_str(matches[2].str());
std::replace(central_value_str.begin(), central_value_str.end(), ',', '.');
float value = std::stof(central_value_str);
// Apply multiplier
const std::string prefix = matches[4].str();
if (prefix.size() > 0) {
try {
value *= unit_prefix.at(prefix);
} catch (std::out_of_range& exception) {
// Unknown unit multiplier
// Something went wrong with the parsing. This shall not occur.
goto error_parsing;
}
}
// Apply units
const std::string units = matches[5].str();
if (units == "Hz") {
wavelength_nm = 299792458.F/value * 1e9;
} else if (units == "m") {
wavelength_nm = value * 1e9;
} else {
// Unknown unit
// Something went wrong with the parsing. This shall not occur.
goto error_parsing;
}
}
return matched;
error_parsing:
return false;
}
int main(int argc, char* argv[]) {
int stokes_component;
float wavelength_nm;
for (int i = 1; i < argc; i++) {
std::string str = argv[i];
if (isSpectralChannel(str, stokes_component, wavelength_nm)) {
std::cout << "Component: " << stokes_component
<< " Wavelenght: " << wavelength_nm << "nm"
<< std::endl;
}
}
}