BBM has been designed with BSDF fitting in mind. BSDF fitting is the process where the parameters of a BSDF model is optimized such that some error metric between the fitted model and a reference model are minimized.
Basic Usage
Consider the following example that fits a Cook-Torrance BSDF model to the
MERL-MIT measured Alum-Bronze:
#include <iostream>
#include "bbm.h"
using namespace bbm;
#include "optimizer/compass.h"
#include "loss/cosine_weighted_log.h"
int main(int argc, char** argv)
{
BBM_IMPORT_CONFIG( floatRGB );
size_t maxItr = 100;
// create model to fit and reference to fit to.
auto fitted = aggregate( lambertian<Config>(), cooktorrance<Config>() );
merl<Config> reference("alum-bronze.binary");
// Use a standard Log loss
standardLog loss(fitted, reference, {90, 30}, {1, 9});
auto param = parameter_values(fitted);
std::cout << "Initial param: " << param << std::endl;
std::cout << "Initial loss : " << loss() << std::endl;
// Use the bsdfmodel defined box constraints on the parameters
auto low = parameter_lower_bound(fitted);
auto up = parameter_upper_bound(fitted);
std::cout << "Lower: " << low << std::endl;
std::cout << "Upper: " << up << std::endl;
// Use a compass search for optimization
compass opt(loss, param, low, up);
// optimize for 100 steps, or until convergence
for(size_t t=0; t < maxItr && !bbm::all(opt.is_converged()); ++t)
{
auto err = opt.step();
std::cout << err << ": " << param << ", " << opt.is_converged() << std::endl;
}
return 0;
}
This example demonstrates the three key components in BSDF fitting in BBM:
the ability to enumerate the BSDF parameters in a vector-like structure using the
parameter_valuesmethod, as well as the ability to query the upper and lower limits of the parameters. The enumerated parameter values have been decoupled from the BSDF model making generic interfacing with optimization algorithms easier.Decoupling of the fitting from the optimization algorithm. Many non-linear optimization implementations assume a L2 error metric. Decoupling the loss from the optimization procedure allows for experimentation.
Separation of the optimization algorithm from the BSDF fitting. The above code employs a
compasssearch. However, future developments can include other algorithms such as Adam.
Parameter Enumeration
Parameters can be enumerated by:
-
template<typename MODEL>
inline auto bbm::parameter_values(MODEL &&model, bsdf_attr flag = bsdf_attr::All) Enumerate the parameters of a BSDF model in a vector.
- Parameters:
model – = bsdf model to extract parameters from
flag – = bsdf_attr flag to select a subset of the parameters
- Returns:
vector of references to the bsdf parameter values
Note a version of parameter_values exists for both bsdfmodel and
bsdf. By changing the flags, a subset of the parameters can be
enumerated, for example to separate the fitting of albedo and the non-albedo
parameters:
-
enum class bbm::bsdf_attr
Attribute Property Flags.
Values:
-
enumerator None
-
enumerator DiffuseScale
-
enumerator DiffuseParameter
-
enumerator SpecularScale
-
enumerator SpecularParameter
-
enumerator Dependent
-
enumerator Diffuse
-
enumerator Specular
-
enumerator Scale
-
enumerator Parameter
-
enumerator All
-
enumerator None
The ‘’DiffuseScale’’ and SpecularScale refer to albedo parameters. E.g.,
the albedo added by wrapping a bsdfmodel in a scaledmodel is a
scale. Scalar = DiffuseScale | SpecularScale. The DiffuseParameter
and SpecularParameter represent all non-albedo parameters, with
Parameter = DiffuseParameter | SpecularParameter. Diffuse and
Specular is the union of the corresponding scale and parameter. Finally,
a parameter marked as Dependent is not directly optimizable; it is assumed
that its value dependents directly or indirectly on the values of other
parameters.
A similar interface exists for enumerating parameter_default_values,
parameter_lower_bound, and parameter_upper_bound.
Fitting Metric
Similar to the relation between bsdfmodel and bsdf, BBM splits a
fitting metric in a lossfunction and a loss (with corresponding
loss_base and loss_ptr). The latter wraps a lossfunction and
provides an interface with virtual function calls.
A lossfunction must meet bbm::concepts::lossfunction:
-
template<typename LOSSFUNC>
concept lossfunction - #include <lossfunction.h>
loss function concept
Each loss function requires:
concepts::has_config
update(void) method that initializes the loss. This method should be called at the beginning of each optimization step()
Value_t operator()(Mask=true) const method that returns the loss.
The returned loss should support addition.
The loss function constructor should precompute any values that do not change
during the whole optimization process. The init method should precompute
any values that remain constant within an optimization step. Finally,
operator() should return the loss. Note that init is only called once
per optimization step, whereas operator() might be called multiple times.
A (fictional) example of how this could be used: during construction the loss
might precompute cosines of angles that are evaluated as part of the loss; the
init function could preselect a batch of directions to compute the loss over;
and the operator() might sum the differences over the batch and scale it
by the precomputed cosine.
Many fitting metrics compute a loss over a number of sampled (in,out)
directions. So simplify creation, BBM introduces a sampledlossfunction
(which also meets bbm::concepts::sampledlossfunction):
-
template<typename BSDF, typename REFERENCE, typename SAMPLELOSSFUNC, typename LINEARIZER, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct sampledlossfunction sampledlossfunction
Computes the loss over BSDFs sampled by a linearizer.
Satisfied: concepts::sampledlossfunction
Subclassed by bieronL2< BSDF, REFERENCE, COMPONENT, UNIT >, bieronLog< BSDF, REFERENCE, COMPONENT, UNIT >, lowL2< BSDF, REFERENCE, COMPONENT, UNIT >, lowLog< BSDF, REFERENCE, COMPONENT, UNIT >, nganL2< BSDF, REFERENCE, COMPONENT, UNIT >, standardLog< BSDF, REFERENCE, COMPONENT, UNIT >
Public Functions
-
inline sampledlossfunction(const BSDF &bsdf, const REFERENCE &reference, const SAMPLELOSSFUNC &samplelossfunc, const LINEARIZER &linearizer)
Constructor.
- Parameters:
bsdf – = bsdf model to be optimized
reference – = goal bsdf model
samplelossfunc – = loss over a single loss sample
linearizer – = linearizer to sample the bsdf models.
-
inline void update(void)
Init (does nothing)
-
inline Size_t samples(void) const
Returns the number of samples.
-
inline Value operator()(Size_t idx, Mask mask = true) const
Compute the loss over the idx-th sample.
-
inline Value operator()(Mask mask = true) const
Compute loss over all samples.
-
inline sampledlossfunction(const BSDF &bsdf, const REFERENCE &reference, const SAMPLELOSSFUNC &samplelossfunc, const LINEARIZER &linearizer)
Sampled Loss Function
The behavior of a sampledlossfunction is determined by a
sample loss function that computes the loss over a single sample and which
must meet bbm::concepts::samplelossfunction:
-
template<typename SAMPLELOSS>
concept samplelossfunction - #include <samplelossfunction.h>
A number of sampled loss functions are predefined, that will be used in conjunction with a linearizer to create different loss functions.
Cosine weighted L2
-
template<typename CONF>
struct nganL2_error Standard cosine weighted l2 error.
A cosine weighted l2 error for an (in,out) BSDF sample. Each sample is weighted by sin(theta_in) * sin(theta_out)
Implements: concepts::samplelossfunction
-
template<typename CONF>
struct lowL2_error Low et al. cosine weighted l2 error.
Low et al. “BRDF models for accurate and efficient rendering of glossy
surfaces”:
https://doi.org/10.1145/2077341.2077350A cosine weighted l2 error on (in,out) BSDF sample. Each sample is weighted by sin(theta_in)
Implements: concepts::samplelossfunction
-
template<typename CONF>
struct bieronL2_error Bieron and Peers cosine weighted l2 error.
Bieron and Peers “An Adaptive BRDF Fitting Metric”: https://doi.org/10.1111/cgf.14054
A cosine weighted l2 error on an (in,out) BSDF sample. Each sample is weighted by sin(theta_in) * sin(theta_out) * cos(theta_out)
Implements: concepts::samplelossfunction
Cosine weighted Log
-
template<typename CONF>
struct standardLog_error Cosine weighted log error weighted by sin theta of in and out.
A cosine weighted log error for an (in,out) BSDF sample. Each sample is weighted by sin(theta_in) * sin(theta_out). While this error is the re-interpretation of Low et al.’s log error with ‘standard’ sin theta in.out weighting of the error.
Implements: concepts::samplelossfunction
-
template<typename CONF>
struct lowLog_error Low et al.’s cosine weighted log error.
Low et al. “BRDF models for accurate and efficient rendering of glossy
surfaces”:
https://doi.org/10.1145/2077341.2077350A cosine weighted log error on (in,out) BSDF sample. Each sample is weighted by sin(theta_in)
Implements: concepts::samplelossfunction
-
template<typename CONF>
struct bieronLog_error Bieron and Peers cosine weighted log error.
Bieron and Peers “An Adaptive BRDF Fitting Metric”: https://doi.org/10.1111/cgf.14054
A cosine weighted log error on an (in,out) BSDF sample. Each sample is weighted by sin(theta_in) * sin(theta_out) * cos(theta_out)
Implements: concepts::samplelossfunction
Linearizer
The behavior of a sampledlossfunction is also determined by a linearizer
(bbm::concepts::inout_linearizer) that determines how samples are
generated:
-
template<typename T>
concept inout_linearizer - #include <inout_linearizer.h>
inout_linearizer concept
An inout_linearizer enumerates a discrete set of (in,out) directions on the joint incident and outgoing direction sphere.
Each inout_linearizer contains the following:
concept::has_config
Size_t size(void) const: the number of discrete direction-pairs
Vec3dPair opeator()(Size_t index, Mask mask=true) const: returns the index-th direction pair
Size_t operator()(const Vec3d& in, const Vec3d& out, Mask mask=true) const: the inverse operation
An example of a linearizer is spherical_linearizer that samples the
incident and outgoing hemisphere of directions:
-
template<typename CONF>
struct spherical_linearizer Public Functions
-
inline spherical_linearizer(const vec2d<Size_t> &samplesIn, const vec2d<Size_t> &samplesOut, const Vec2d &startIn = 0, const Vec2d &endIn = Constants::Hemisphere(), const Vec2d &startOut = 0, const Vec2d &endOut = Constants::Hemisphere())
Constructor.
- Parameters:
samplesIn – = number vec2d<size_t>(phi, theta) samples for incident directions
samplesOut – = number vec2d<size_t>(phi, theta) samples for outgoing directions
startIn – = start Vec2d(phi,theta) for incident directions (default 0)
endIn – = end Vec2d(phi,theta) for incident directions (default HEMISPHERE)
startOut – = start Vec2d(phi,theta) for outgoing directions (default 0)
endOut – = end Vec2d(phi,theta) for outgoing directions (default HEMISPHERE)
-
inline spherical_linearizer(const spherical_linearizer &src)
Copy Constructor.
-
inline Size_t size(void) const
Size of the linearized ‘array’ of direction pairs.
-
inline Vec3dPair operator()(Size_t idx, Mask mask = true) const
The in-out coordinate of the idx-th sample.
Note: the sample direction is placed at the center of the index’s bin, except for the top and bottom, where it is placed at the center of the top and bottom edge respectively.
- Parameters:
idx – = index of the sample (in [0, samples()-1]
- Returns:
a Ve3dPair of the in and out direction of the sample. Will return {0,0} if the index is not valid.
-
inline spherical_linearizer(const vec2d<Size_t> &samplesIn, const vec2d<Size_t> &samplesOut, const Vec2d &startIn = 0, const Vec2d &endIn = Constants::Hemisphere(), const Vec2d &startOut = 0, const Vec2d &endOut = Constants::Hemisphere())
Note
A inout_linearizer is also a useful tool for creating a
staticmodel. The merl static BSDF model utilizes a
merl_linearizer.
Loss functions
The spherical_linearizer is used to predefine a number of sampled loss
functions (stored in include/loss.
Sampled Squared Loss
-
template<typename BSDF, typename REFERENCE, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct nganL2 : public bbm::sampledlossfunction<BSDF, REFERENCE, nganL2_error<get_config<BSDF>>, spherical_linearizer<get_config<BSDF>>, bsdf_flag::All, unit_t::Radiance>
-
template<typename BSDF, typename REFERENCE, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct lowL2 : public bbm::sampledlossfunction<BSDF, REFERENCE, lowL2_error<get_config<BSDF>>, spherical_linearizer<get_config<BSDF>>, bsdf_flag::All, unit_t::Radiance>
-
template<typename BSDF, typename REFERENCE, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct bieronL2 : public bbm::sampledlossfunction<BSDF, REFERENCE, bieronL2_error<get_config<BSDF>>, spherical_linearizer<get_config<BSDF>>, bsdf_flag::All, unit_t::Radiance>
Sampled Log Loss
-
template<typename BSDF, typename REFERENCE, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct standardLog : public bbm::sampledlossfunction<BSDF, REFERENCE, standardLog_error<get_config<BSDF>>, spherical_linearizer<get_config<BSDF>>, bsdf_flag::All, unit_t::Radiance>
-
template<typename BSDF, typename REFERENCE, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct lowLog : public bbm::sampledlossfunction<BSDF, REFERENCE, lowLog_error<get_config<BSDF>>, spherical_linearizer<get_config<BSDF>>, bsdf_flag::All, unit_t::Radiance>
-
template<typename BSDF, typename REFERENCE, bsdf_flag COMPONENT = bsdf_flag::All, unit_t UNIT = unit_t::Radiance>
struct bieronLog : public bbm::sampledlossfunction<BSDF, REFERENCE, bieronLog_error<get_config<BSDF>>, spherical_linearizer<get_config<BSDF>>, bsdf_flag::All, unit_t::Radiance>
Optimizer
The optimization algorithms interface is defined by
bbm::concepts::optimization_algorithm
-
template<typename OPT>
concept optimization_algorithm - #include <optimization_algorithm.h>
optimization_algorithm concept
Each optimization algorithm has the following:
concepts::has_config
Value step(void) : take one optimization step
void reset(void) : reset the internal state of the algorithm
Mask is_converged(void) const : true if the algorithm has converged
Similar to bsdfmodel and bsdf, a virtual interface is provided through
optimizer (and similarly optimizer_base and optimizer_ptr).
An example of an optimization algorithm is bbm::compass:
-
template<typename LOSSFUNC, typename PARAM, typename BOX = PARAM>
struct compass Compass Search.
Performs a pattern search by probing each cardinal direction in the parameter space.
When the Value type is a packet, this compass search will perform N parallel searches (where N == size of the packet).
Satisfies: concepts::optimization algorithm
- Template Parameters:
LOSSFUNC – = loss function
PARAM – = parameters to optimize
BOX – = box constraint type (default=PARAM)
Public Functions
-
inline compass(LOSSFUNC &lossfunc, PARAM ¶m, const BOX &lower = BOX(), const BOX &upper = BOX(), Scalar tolerance = Constants::Epsilon(), Scalar stepSize = 1.0, Scalar contraction = 0.5, Scalar expansion = 1.0, Mask mask = true)
Constructor: compass search.
- Parameters:
lossfunc – = loss function to minimize
param – = set of parameters to optimize
lower – = lower bound of box constraint
upper – = upper bound of box constraint
tolerance – = threshold on step size to determine convergence (default=Epsilon)
stepSize – = initial step size (default=1.0)
contraction – = reduction factor for step size if no probe in the cardinal directions improves the loss (default=0.5)
expansion – = expansion factor for step size if a good candinal direction was found (default=1.0)
-
inline Value step(void)
probe each cardinal direction, and update the parameters to the one with the lowest loss.
- Returns:
loss after update
-
inline void reset(void)
reset the step size
-
inline Mask is_converged(void) const
is_converged when _step < _tolerance
Note
optimization algorithms are defined directly in the bbm namespace and
stored in include/optimizer
When implementing a new optimization algorithm, care must be taken to ensure that the implementation is compatible with packet types.
Fit files
To store and exhange BSDF fits, BBM uses a simple text based format. In this
format lines with comments start with #. Each material fit is stored as:
name = model(parameters, where name is a unique identifier (e.g., the
material name), model(parameters) is what you would see when the str
method in Python is called on the model or the output of bbm::toString.
We refer the fits subdirectory for examples. To import the BSDF fits in
python:
import bbm_floatRGB as bbm
from import_fits import *
fits = import_fits('fits/ngan_ward.fit', bbm)
str(fits['alum-bronze'])
The 3rd line, reads the fits/ngan_ward.fit file, and imports the fits into
a dictionary (called fits). This dictionary contains the BSDF fits with
the key the name of the MERL material, and the value the corresponding BSDF
(defined in the Python bbm namespace).
In C++ we can use the bbm::io::importFIT method:
-
template<typename CONF>
void bbm::io::importFIT(const std::string &filename, std::map<std::string, bsdf_ptr<CONF>> &data) Import a ‘fit’ file.
- Parameters:
filename – = FIT filename to read from
data – = std::map<std::string, bsdf_ptr> to store the fit data to
For example:
#include "io/fit.h"
// ....
std::map<std::string, bsdf_ptr<Config>> fits;
bbm::io::importFIT("filename.fit", fits);
std::cout << fits["alum-bronze"] << std::endl;
Similarly, one can export an std::map of fits using the bbm::exportFIT
method:
-
template<typename CONF>
void bbm::io::exportFIT(const std::string &filename, const std::map<std::string, bsdf_ptr<CONF>> &data, const std::string &comment) Export a ‘fit’ file.
- Parameters:
filename – = FIT filename to write to
data – = std::map<std::string, bsdf_ptr> of fitted data
comment – = string of comment to add to the start of the fit file (ignored if empty)