< Back

BTF-UTIA lookup code for Python

This modules enables you loading BTF-UTIA BRDF in binary format using Python 3. It is not yet supporting OpenEXR and XYZ formats.

You can download BTF-UTIA materials from there: http://btf.utia.cas.cz.

Module source code

utia.py

"""
:mod: `utia` -- BTF-UTIA BRDF Python support
============================================

.. module:: utia 
    :synopsis: This module implements the support for UTIA BRDF material
               http://btf.utia.cas.cz/

.. moduleauthor:: Alban Fichet <alban.fichet@inria.fr>
"""

import struct
import math
from enum import Enum, unique

class Utia:
    @unique
    class FileType(Enum):
        binary = 1
        exr = 2
        png = 3

    @unique
    class ColorFormat(Enum):
        XYZ = 1
        sRGB = 2
        RGB = 3
        
    def __init__(self, utia_file,
                 nti = 6, ntv = 6, npi = 48, npv = 48,
                 file_type = FileType.binary,
                 color_format = ColorFormat.sRGB):
        """
        Initialize and load an UTIA BRDF file in binary format.
        Default parameters are suitable for the 150 materials in 
        'BRDF Database'. For 'BRDF Dense', these should be set as follow:
        - nti = 44
        - ntv = 44
        - npi = 180
        - npo = 180
        
        :param utia_file: The path of the file to load
        :param nti: number of theta_i values captured
        :param ntv: number of theta_v values captured
        :param npi: number of phi_i values captured
        :param npv: number of phi_v values captured
        :param file_type: Specifies the type of file to be read
        :param color_format: Specified the format of color used in the input 
        file
        """
        self.nti = nti
        self.ntv = ntv
        self.npi = npi
        self.npv = npv
        
        with open(utia_file, 'rb') as f:
            data = f.read()
            self.brdf = struct.unpack(str(3*nti*ntv*npi*npv) + 'd', data)

        if color_format is self.ColorFormat.sRGB:
            self.brdf = [self.sRGB_to_RGB(sRGB) for sRGB in self.brdf]
    
    def eval_raw(self, theta_i, phi_i, theta_v, phi_v):
        """
        Lookup the BRDF value for given incoming and outcoming angles.

        :param theta_i: Incoming elevation angle in radians
        :param phi_i: Incoming azimuthal angle in radians
        :param theta_v: Outgoing elevation angle in radians
        :param phi_v: Outgoing azimuthal angle in radians
        :return: A list of 3 elements giving the BRDF value for R, G, B in
        linear RGB D65)
        """
        theta_i_p, phi_i_p = self.positive_rad(theta_i, phi_i)
        theta_v_p, phi_v_p = self.positive_rad(theta_v, phi_v)

        if theta_i_p > math.pi/2 or theta_v_p > math.pi/2:
            return [0]*3
        
        p_max = 2*math.pi
        t_max = math.pi/2
        
        idx_ti = min(self.nti - 1, round(self.nti * theta_i_p / t_max))
        idx_pi = min(self.npi - 1, round(self.npi * phi_i_p / p_max))
        idx_tv = min(self.ntv - 1, round(self.ntv * theta_v_p / t_max))
        idx_pv = min(self.npv - 1, round(self.npv * phi_v_p / p_max))
        
        return self.__eval_idx(idx_ti, idx_pi, idx_tv, idx_pv)

    def eval_interpolated(self, theta_i, phi_i, theta_v, phi_v):
        """
        Lookup the BRDF value for given incoming and outcoming angles and
        perform an interpolation over theta_i, phi_i, theta_v, phi_v.

        :param theta_i: Incoming elevation angle in radians
        :param phi_i: Incoming azimuthal angle in radians
        :param theta_v: Outgoing elevation angle in radians
        :param phi_v: Outgoing azimuthal angle in radians
        :return: A list of 3 elements giving the BRDF value for R, G, B in
        linear RGB D65)
        """
        theta_i_p, phi_i_p = self.positive_rad(theta_i, phi_i)
        theta_v_p, phi_v_p = self.positive_rad(theta_v, phi_v)

        if theta_i_p > math.pi/2 or theta_v_p > math.pi/2:
            return [0]*3
        
        t_max = math.pi/2
        p_max = 2*math.pi

        idx_ti_b = self.__theta_i_idx(theta_i_p)
        idx_pi_b = self.__phi_i_idx(phi_i_p)
        idx_tv_b = self.__theta_v_idx(theta_v_p)
        idx_pv_b = self.__phi_v_idx(phi_v_p)

        # Calculate the indexes for interpolation
        idx_ti_b = idx_ti_b if idx_ti_b < self.nti - 1 else self.nti - 2
        idx_tv_b = idx_tv_b if idx_tv_b < self.ntv - 1 else self.ntv - 2
    
        idx_ti = [idx_ti_b, idx_ti_b + 1]
        idx_pi = [idx_pi_b, idx_pi_b + 1]
        idx_tv = [idx_tv_b, idx_tv_b + 1]
        idx_pv = [idx_pv_b, idx_pv_b + 1]

        # Calculate the weights
        weight_ti = [abs(x/self.nti - theta_i_p/t_max) for x in idx_ti]
        weight_pi = [abs(x/self.npi - phi_i_p/p_max) for x in idx_pi]
        weight_tv = [abs(x/self.ntv - theta_v_p/t_max) for x in idx_tv]
        weight_pv = [abs(x/self.npv - phi_v_p/p_max) for x in idx_pv]
        
        # Normalize the weigths
        weight_ti = [1 - x / sum(weight_ti) for x in weight_ti]
        weight_pi = [1 - x / sum(weight_pi) for x in weight_pi]
        weight_tv = [1 - x / sum(weight_tv) for x in weight_tv]
        weight_pv = [1 - x / sum(weight_pv) for x in weight_pv]

        idx_pi[1] = idx_pi[1] if idx_pi[1] < self.npi else 0
        idx_pv[1] = idx_pv[1] if idx_pv[1] < self.npv else 0
        
        ret_val = [0]*3
        
        for iti, wti in zip(idx_ti, weight_ti):
            for ipi, wpi in zip(idx_pi, weight_pi):
                for itv, wtv in zip(idx_tv, weight_tv):
                    for ipv, wpv in zip(idx_pv, weight_pv):
                        ret_val = [r + x * wti * wpi * wtv * wpv
                                   for r, x in
                                   zip(ret_val,
                                       self.__eval_idx(iti, ipi, itv, ipv))]

        return ret_val
    
    def __eval_idx(self, iti, ipi, itv, ipv):
        """
        Lookup a BRDF value knowing the index values

        :param iti: theta_i index
        :param ipi: phi_i index
        :param itv: theta_v index
        :param ipv: phi_v index
        :return: A list of 3 elements giving the BRDF value for R, G, B in
        linear RGB D65)
        """
        nc = self.npv * self.ntv
        nr = self.npi * self.nti

        idx = nc * (self.npi * iti + ipi) + self.npv * itv + ipv
        
        return [self.brdf[c*nc*nr + idx] for c in range(0,3)]

    def __theta_i_idx(self, theta_i):
        return max(0, min(self.nti-1, math.floor(self.nti * theta_i / (math.pi/2))))

    def __phi_i_idx(self, phi_i):
        return max(0, min(self.npi-1, math.floor(self.npi * phi_i / (2*math.pi))))
    
    def __theta_v_idx(self, theta_v):
        return max(0, min(self.ntv-1, math.floor(self.ntv * theta_v / (math.pi/2))))

    def __phi_v_idx(self, phi_v):
        return max(0, min(self.npv-1, math.floor(self.npv * phi_v / (2*math.pi))))
    
    
    def positive_rad(self, theta, phi):
        """
        Method to ensure a given set of angles in radians is returned as a positive 
        value.

        :param theta: The elevation angle in radian
        :param phi: The azimuthal angle in radian
        :return: The (theta, phi) angle set given in a range of [0:2*pi]
        """ 
        theta_bound = math.fmod(theta, 2*math.pi)
        phi_bound = phi
        
        if theta_bound < 0:
            theta_bound = abs(theta_bound)
            phi_bound += math.pi

        phi_bound = math.fmod(phi_bound, 2*math.pi)
        while phi_bound < 0:
            phi_bound += 2 * math.pi

        return (theta_bound, phi_bound)

    def sRGB_to_RGB(self, sRGB):
        """
        Convert sRGB color value to RGB
        
        :param sRGB: sRGB value
        :return: A linear RGB value corresponding to the sRGB input value
        """
        a = 0.055
        return (sRGB / 12.92 if sRGB < 0.04045
                else math.pow((sRGB + a) / (1 + a), 2.4))

Example

Example

utia_example.py

#!/usr/bin/env python3

import matplotlib.pyplot as plt
import math

from utia import Utia

def main():
    brdf = Utia('data/m072_fabric107.bin')

    theta_h = math.radians(45)
    samples = 1024
    phi_h = [2 * math.pi * x / samples for x in range(0, samples)]

    reflectance_raw = [brdf.eval_raw(theta_h, phi, theta_h, phi)
                       for phi in phi_h]
    reflectance_interp = [brdf.eval_interpolated(theta_h, phi, theta_h, phi)
                          for phi in phi_h]

    plot_raw = plt.subplot(121, projection='polar')
    plot_raw.plot(phi_h + [phi_h[0]],
                  reflectance_raw + [reflectance_raw[0]])
    plot_raw.grid(True)
    plot_raw.set_title('Raw values')
    
    plot_interp = plt.subplot(122, projection='polar')
    plot_interp.plot(phi_h + [phi_h[0]],
                     reflectance_interp + [reflectance_interp[0]])
    plot_interp.grid(True)
    plot_interp.set_title('Interpolated values')
    
    plt.show()

if __name__ == '__main__':
    main()