Source code for luxpy.toolboxes.dispcal.rgbtraining_xyztest_set_generation

# -*- coding: utf-8 -*-
"""
Module for generation of RGB training and XYZ test sets for display colorimetric characterization
-------------------------------------------------------------------------------------------------

:generate_training_data(): Generate RGB training pairs by creating a cube of RGB values. 

:generate_test_data(): Generate XYZ test values by creating a cube of CIELAB L*a*b* values, then converting these to XYZ values. 

:plot_rgb_xyz_lab_of_set(): Make 3d-plots of the RGB, XYZ and L*a*b* cubes of the data in rgb_xyz_lab. 

:split_ramps_from_cube(): Split a cube data set in pure RGB (ramps) and non-pure (remainder of cube). 

:is_random_sampling_of_pure_rgbs(): Return boolean indicating if the RGB cube axes (=single channel ramps) are sampled (different increment) independently from the remainder of the cube.

Created on Fri Oct 28 13:16:50 2022

@author: u0032318
"""
import copy
import warnings
import itertools

import numpy as np 
import matplotlib.pyplot as plt

from luxpy import xyz_to_lab, lab_to_xyz, spd_to_xyz, _CIE_D65, _CIEOBS
from luxpy import math

from luxpy.toolboxes.dispcal import displaycalibration as dc

np.seterr(divide = 'raise', invalid = 'raise')


__all__ = ['_generate_training_data','generate_training_data','generate_test_data',
           'split_ramps_from_cube', 'is_random_sampling_of_pure_rgbs', 'plot_rgb_xyz_lab_of_set']

#------------------------------------------------------------------------------
# training and test data generation 
#------------------------------------------------------------------------------    
def _generate_training_data(inc = 10, inc_offset = 0, nbit = 8, include_max = True, include_min = True,
                            seed = 0, randomize_order = True):
    """
    Generate RGB training data by creating a cube of RGB values.
    
    Args:
        :inc:
            | 10, optional
            | Increment along each channel (=R,G,B) axes in the RGB cube.
        :inc_offset:
            | 0, optional
            | The offset along each channel axes from which to start incrementing.
        :nbit:
            | 8, optional
            | RGB values in nbit format (e.g. 8, 16, ...)
        :include_max:
            | True, optional
            | If True: ensure all combinations of max value (e.g. 255 for nbit = 8) are included in RGB cube. 
        :include_min:
            | True, optional
            | If True: ensure all combinations of min value 0 are included in RGB cube. 
        :seed:
            | 0, optional
            | Seed for setting the state of numpy's random number generator.
        :randomize_order:
            | True, optional
            | Randomize the order of the (xyz,rgb) pairs before output.
            
    Returns:
        :rgb:
            | ndarray with RGB values.
    """
    dv = np.arange(inc_offset, 2**nbit - 1, inc)
    if (dv.max() < (2**nbit-1)) & (include_max): dv = np.hstack((dv,[2**nbit-1])) # ensure max values are present
    if (dv.min() > 0) & (include_min): dv = np.hstack((0, dv)) # ensure 0 values are present
    rgb =  np.array(list(itertools.product(*[dv]*3)))
        
    # Randomize order of training pairs:
    if randomize_order:
        np.random.seed(seed)
        idx = np.arange(rgb.shape[0]) # get indices
        np.random.shuffle(idx) # random order for indices
        rgb = rgb[idx] # re-arrange rgb in random order
                     
    return rgb 

[docs] def split_ramps_from_cube(rgb, xyz = None, rgb_only = False): """ Split a cube data set in pure RGB (ramps) and non-pure (remainder of cube). """ # select pures from full re-sampled RGB cube: pure_rgb_bool = dc.find_pure_rgb(rgb) if rgb_only == False: xyz_pure = np.array([xyz[pure_rgb_bool[i],:] for i in range(3)]).reshape(-1,3) rgb_pure = np.array([rgb[pure_rgb_bool[i],:] for i in range(3)]).reshape(-1,3) # ensure monotonically increasing rgbs for pures (delete extra blacks): black_rgb_bool = dc.find_index_in_rgb(rgb_pure, k = [0,0,0], as_bool = True) rgb_black = rgb_pure[black_rgb_bool].mean(axis=0,keepdims=True) # find blacks and get average value rgb_pure = np.vstack((rgb_black, rgb_pure[~black_rgb_bool])) # only keep non-blacks, except one. if rgb_only == False: xyz_black = xyz_pure[black_rgb_bool].mean(axis=0,keepdims=True) xyz_pure = np.vstack((xyz_black, xyz_pure[~black_rgb_bool])) # select non-pure rgb: nonpure_rgb_bool = ~(pure_rgb_bool[0] | pure_rgb_bool[1] | pure_rgb_bool[2]) rgb_nonpure = rgb[nonpure_rgb_bool,:] rgb = np.vstack((rgb_pure,rgb_nonpure)) if rgb_only == False: xyz_nonpure = xyz[nonpure_rgb_bool,:] if rgb_only: xyz_pure, xyz_nonpure = None, None return (xyz_pure, rgb_pure), (xyz_nonpure, rgb_nonpure)
[docs] def is_random_sampling_of_pure_rgbs(inc): """ Return boolean indicating if the RGB cube axes (=single channel ramps) are sampled (different increment) independently from the remainder of the cube. Note: 1. Independent sampling is indicated when :inc: is a list with 2 different values. """ if len(inc) == 1: pure_rgb_sampled_separately = False else: if inc[0] == inc[1]: pure_rgb_sampled_separately = False else: pure_rgb_sampled_separately = True return pure_rgb_sampled_separately
[docs] def generate_training_data(inc = [10], inc_offset = 0, nbit = 8, seed = 0, randomize_order = True, verbosity = 0, fig = None): """ Generate RGB training pairs by creating a cube of RGB values. Args: :inc: | [10], optional | Increment along each channel (=R,G,B) axes in the RGB cube. | If inc is a list with 2 different values the RGB cube axes | are sampled independently from the remainder of the cube. | --> inc = [inc_remainder, inc_axes] :inc_offset: | 0, optional | The offset along each channel axes from which to start incrementing. :nbit: | 8, optional | RGB values in nbit format (e.g. 8, 16, ...) :include_max: | True, optional | If True: ensure all combinations of max value (e.g. 255 for nbit = 8) are included in RGB cube. :include_min: | True, optional | If True: ensure all combinations of min value 0 are included in RGB cube. :seed: | 0, optional | Seed for setting the state of numpy's random number generator. :randomize_order: | True, optional | Randomize the order of the (xyz,rgb) pairs before output. :verbosity: | 0, optional | Level of output. Returns: :rgb: | ndarray with RGB values. """ rgb_tr = _generate_training_data(inc = inc[0], inc_offset = inc_offset, nbit = nbit, randomize_order = True, seed = seed) pure_rgb_sampled_separately = is_random_sampling_of_pure_rgbs(inc) if pure_rgb_sampled_separately: # create extra training set to extract pure R,G,B data from: rgb_tr_pure = _generate_training_data(inc = inc[1], inc_offset = inc_offset, nbit = nbit, randomize_order = True, seed = seed) # select pures from full re-sampled RGB cube: pure_rgb_bool = dc.find_pure_rgb(rgb_tr_pure) rgb_tr_pure = np.array([rgb_tr_pure[pure_rgb_bool[i],:] for i in range(3)]).reshape(-1,3) # ensure monotonically increasing rgbs for pures (delete extra blacks): black_rgb_bool = dc.find_index_in_rgb(rgb_tr_pure, k = [0,0,0], as_bool = True) rgb_tr_black = rgb_tr_pure[black_rgb_bool].mean(axis=0,keepdims=True) # find blacks and get average value rgb_tr_pure = np.vstack((rgb_tr_black, rgb_tr_pure[~black_rgb_bool])) # only keep non-blacks, except one. # keep non-pure from original training data and add new pure rgb data: pure_rgb_bool = dc.find_pure_rgb(rgb_tr) nonpure_rgb_bool = ~(pure_rgb_bool[0] | pure_rgb_bool[1] | pure_rgb_bool[2]) rgb_tr_nonpure = rgb_tr[nonpure_rgb_bool,:] rgb_tr = np.vstack((rgb_tr_pure,rgb_tr_nonpure)) # Randomize order of training pairs: if randomize_order: np.random.seed(seed) idx = np.arange(rgb_tr.shape[0]) # get indices np.random.shuffle(idx) # random order for indices rgb_tr = rgb_tr[idx] # re-arrange rgb in random order if verbosity > 0: print('Number of training points for inc = {} and inc_offset = {:1.0f}: {:1.0f}.'.format(inc, inc_offset, rgb_tr.shape[0])) if verbosity > 1: data = rgb_tr data_contains = ['rgb'] fig, axs = plot_rgb_xyz_lab_of_set(data, data_contains = data_contains, subscript = '_train', nrows = 2, row = 1, fig = fig) if pure_rgb_sampled_separately: pure_rgb_bool = dc.find_pure_rgb(rgb_tr) pure_rgb_bool = (pure_rgb_bool[0] | pure_rgb_bool[1] | pure_rgb_bool[2]) data = rgb_tr[pure_rgb_bool] plot_rgb_xyz_lab_of_set(data, data_contains = data_contains, subscript = '_train', nrows = 2, row = 1, marker = '+', fig = fig, axs = axs) return rgb_tr.astype(dtype = np.int32)
[docs] def generate_test_data(dlab = [10,10,10], nbit = 8, seed = 0, xyzw = None, cieobs = _CIEOBS, xyzrgb_hull = None, randomize_order = True, verbosity = 0, fig = None): """ Generate XYZ test values by creating a cube of CIELAB L*a*b* values, then converting these to XYZ values. Args: :dlab: | [10,10,10], optional | Increment along each CIELAB (=L*,a*,b*) axes in the Lab cube. :nbit: | 8, optional | RGB values in nbit format (e.g. 8, 16, ...) :seed: | 0, optional | Seed for setting the state of numpy's random number generator. :xyzw: | None, optional | White point xyz to convert from lab to xyz | If None: use the white in xyzrgb_hull. If this is also None: use _CIE_D65 white. :cieobs: | _CIEOBS, optional | CIE standard observer used to convert _CIE_D65 to XYZ when xyzw | needs to be determined from the illuminant spectrum. :xyzrgb_hull: | None, optional | ndarray with (XYZ,RGB) pairs from which the hull (= display gamut) can be determined. | If None: test XYZ might fall outside of display gamut ! :randomize_order: | True, optional | Randomize the order of the test xyz before output. :verbosity: | 0, optional | Level of output. Returns: :xyz: | ndarray with XYZ values. """ if xyzrgb_hull is not None: xyz_hull, rgb_hull = xyzrgb_hull # if pre-calculated or measured: must be normalized to white Yw = 100 ! # find white xyz: if xyzw is None: xyzw = xyz_hull[dc.find_index_in_rgb(rgb_hull, k = [2**nbit - 1]*3),:].mean(axis = 0, keepdims = True) else: if xyzw is None: xyzw = spd_to_xyz(_CIE_D65, cieobs = cieobs, relative = True) # generate grid of points in lab space and convert to xyz: l = np.arange(0,100+dlab[1],dlab[0]) a = np.hstack((np.arange(-200,0,dlab[1]),np.arange(0,200+dlab[1],dlab[1]))) b = np.hstack((np.arange(-200,0,dlab[2]),np.arange(0,200+dlab[2],dlab[2]))) lab = np.array(list(itertools.product(l,a,b))) xyz = lab_to_xyz(lab, xyzw = xyzw) # get rid of those that result in negative xyz values: xyz = xyz[~((xyz<0).sum(-1)>0),:] if xyzrgb_hull is not None: # lab_hull = xyz_to_lab(xyz_hull, xyzw = xyzw) # fig = plt.figure() # ax = fig.add_subplot(projection ='3d') # ax.plot(lab_hull[:,1],lab_hull[:,2],lab_hull[:,0],'.') # lab_hull = xyz_to_lab(xyz_hull, xyzw = xyzw) # print(xyz_hull.shape, xyz_hull.max(),xyz_hull.min(),xyzw) # raise Exception('') # in_gamut = math.in_hull(lab, lab_hull) # Only keep those that are within the XYZ-gamut of the display (use XYZ gamut as lab-gamut has concave surfaces !): in_gamut = math.in_hull(xyz, xyz_hull) xyz = xyz[in_gamut,:] # Randomize order of training pairs: if randomize_order: np.random.seed(seed) idx = np.arange(xyz.shape[0]) # get indices np.random.shuffle(idx) # random order for indices xyz = xyz[idx] # re-arrange xyz in random order # Make plots: #convert xyz to lab for plots: lab = xyz_to_lab(xyz, xyzw = xyzw) if verbosity > 0: print('Number of in-gamut test points for dlab = [{:1.0f},{:1.0f},{:1.0f}]: {:1.0f}'.format(*dlab, xyz.shape[0])) data = np.dstack((xyz,lab)) data_contains = ['xyz','lab'] if verbosity > 1: plot_rgb_xyz_lab_of_set(data, data_contains = data_contains, subscript = '_test', nrows = 2, row = 2, fig = fig) return xyz
[docs] def plot_rgb_xyz_lab_of_set(rgb_xyz_lab, subscript = '', data_contains = ['rgb','xyz','lab'], nrows = 1, row = 1, fig = None, axs = None, figsize = (14,7), marker = '.'): """ Make 3d-plots of the RGB, XYZ and L*a*b* cubes of the data in rgb_xyz_lab. Args: :rgb_xyz_lab: | ndarray with RGB, XYZ, Lab data. :subscript: | '', optional | subscript to add to the axis labels. :data_contains: | ['rgb','xyz','lab'], optional | specifies what is in rgb_xyz_lab :nrows: | 1, optional | Number of rows in (nx3) figure. :row: | 1, optional | Current row number to plot to (when using the function to plot nx3 figures) :fig: | None, optional | Figure handle. | If None: generate new figure. :axs: | None, optional | Axes handles: (3,) or None | If None: add new axes for each of the RGB, XYZ, Lab subplots. :figsize: | (14,7), optional | Figure size. :marker: | '.', optional | Marker symbol used for plotting. Return: :fig, axs: | Handles to the figure and the three axes in that figure. """ if fig is None: fig = plt.figure(figsize = figsize) axis_labels = {'rgb' : ['G','B','R'], 'xyz' : ['Y','Z','X'], 'lab' : ['a*','b*','L*']} axs_ = [] if rgb_xyz_lab.ndim == 2: rgb_xyz_lab = rgb_xyz_lab[...,None] for i in range(rgb_xyz_lab.shape[-1]): ax_i = axs[i] if axs is not None else fig.add_subplot(nrows, len(data_contains), 1 + i + (row-1)*len(data_contains), projection='3d') ax_i.plot(rgb_xyz_lab[:,1,i], rgb_xyz_lab[:,2,i], rgb_xyz_lab[:,0,i], marker = marker, linestyle = 'none') ax_i.set_xlabel(axis_labels[data_contains[i]][0]+subscript) ax_i.set_ylabel(axis_labels[data_contains[i]][1]+subscript) ax_i.set_zlabel(axis_labels[data_contains[i]][2]+subscript) axs_.append(ax_i) return fig, axs_
if __name__ == '__main__': # generate some training RGB with axes spacing of 10, inner cube spacing 30: rgb_tr = generate_training_data(inc = [30, 10], verbosity = 2) print('rgb_tr.shape: ', rgb_tr.shape) # generate some XYZ test data: xyz_t = generate_test_data(dlab = [10,10,10], xyzrgb_hull = None, verbosity = 2, fig = None) print('xyz_t.shape: ', xyz_t.shape) # To ensure XYZ test data is within hull of display, provide xyzrgb_hull data: # simulate some measurements of the training data using a virtual display: from virtualdisplay import VirtualDisplay vd = VirtualDisplay(model = 'virtualdisplay_kwak2000_SII', channel_dependence = True) xyz_tr = vd.to_xyz(rgb_tr) # 'measure xyz for each of the displayed rgb_tr print('simulated xyz_tr.shape: ', xyz_tr.shape) # generate some XYZ test data within the display gamut # (make sure black and white are present in xyzrgb_hull!): xyz_t_ingamut = generate_test_data(dlab = [10,10,10], xyzrgb_hull = (xyz_tr,rgb_tr), verbosity = 2, fig = None) print('xyz_t_ingamut.shape: ', xyz_t_ingamut.shape)