Mentions légales du service

Skip to content

New file format: Spectral EXR (support for polarisation, emissive, reflective and bispectral spectrum types)

Alban Fichet requested to merge af_spectral_exr into master

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 respectively Hz and m.

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 with ImfStringVectorAttribute (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 a ImfMatrixAttribute (assumed perfect filters if not present).

In addition, for RGB representation, optional custom attributes are:

  • A response curve for each X, Y, Z values ImfStringVectorAttribute (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 (assumed W.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;
		}		
	}
}
Edited by Alban Fichet

Merge request reports