"""
Observer Function Database - Asano (2015)
=========================================
Defines the objects implementing support for *Asano (2015)* *Observer Function
Database* dataset loading:
- :class:`colour_datasets.loaders.DatasetLoader_Asano2015`
- :func:`colour_datasets.loaders.build_Asano2015`
References
----------
- :cite:`Asano2015` : Asano, Y. (2015). Individual Colorimetric Observers for
Personalized Color Imaging. R.I.T.
"""
from __future__ import annotations
import os
from collections import namedtuple
import numpy as np
import xlrd
from colour import SpectralShape
from colour.colorimetry import (
LMS_ConeFundamentals,
XYZ_ColourMatchingFunctions,
)
from colour.hints import Dict, NDArrayFloat
from colour.utilities import as_float_array, tstack
from colour_datasets.loaders import AbstractDatasetLoader
from colour_datasets.records import datasets
from colour_datasets.utilities import cell_range_values, index_to_column
__author__ = "Colour Developers"
__copyright__ = "Copyright 2019 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"
__all__ = [
"Specification_Asano2015",
"DatasetLoader_Asano2015",
"build_Asano2015",
]
class Specification_Asano2015(
namedtuple(
"Specification_Asano2015",
("XYZ_2", "XYZ_10", "LMS_2", "LMS_10", "parameters", "others"),
)
):
"""
Define the *Asano (2015)* specification for an observer.
Parameters
----------
XYZ_2
*CIE XYZ* 2 degree colour matching functions.
XYZ_10
*CIE XYZ* 10 degree colour matching functions.
LMS_2
*LMS* 2 degree cone fundamentals.
LMS_10
*LMS* 10 degree cone fundamentals.
parameters
Observer parameters.
others
Other information.
References
----------
:cite:`Asano2015`
""" # noqa: D405, D407, D410, D411
def __new__(
cls,
XYZ_2: XYZ_ColourMatchingFunctions,
XYZ_10: XYZ_ColourMatchingFunctions,
LMS_2: LMS_ConeFundamentals,
LMS_10: LMS_ConeFundamentals,
parameters: NDArrayFloat,
others: Dict | None = None,
):
"""
Return a new instance of the
:class:`colour_datasets.loaders.asano2015.Specification_Asano2015`
class.
"""
return super().__new__(cls, XYZ_2, XYZ_10, LMS_2, LMS_10, parameters, others)
[docs]
class DatasetLoader_Asano2015(AbstractDatasetLoader):
"""
Define the *Asano (2015)* *Observer Function Database* dataset loader.
Attributes
----------
- :attr:`colour_datasets.loaders.DatasetLoader_Asano2015.ID`
Methods
-------
- :meth:`colour_datasets.loaders.DatasetLoader_Asano2015.__init__`
- :meth:`colour_datasets.loaders.DatasetLoader_Asano2015.load`
- :meth:`colour_datasets.loaders.DatasetLoader_Asano2015.\
parse_workbook_Asano2015`
References
----------
:cite:`Asano2015`
"""
ID: str = "3252742"
"""Dataset record id, i.e. the *Zenodo* record number."""
def __init__(self) -> None:
super().__init__(datasets()[DatasetLoader_Asano2015.ID])
[docs]
def load(self) -> Dict[str, Dict[int, Specification_Asano2015]]:
"""
Sync, parse, convert and return the *Asano (2015)*
*Observer Function Database* dataset content.
Returns
-------
:class:`dict`
*Asano (2015)* *Observer Function Database* dataset content.
Examples
--------
>>> from colour_datasets.utilities import suppress_stdout
>>> dataset = DatasetLoader_Asano2015()
>>> with suppress_stdout():
... dataset.load()
>>> len(dataset.content.keys())
2
"""
super().sync()
self._content = {
"Categorical Observers": {},
"Colour Normal Observers": {},
}
# Categorical Observers
workbook_path = os.path.join(
self.record.repository, "dataset", "Data_10CatObs.xls"
)
observers = (1, 10)
template = "Asano 2015 {0} Categorical Observer No. {1} {2}"
for index, observer in self.parse_workbook_Asano2015(
workbook_path, template, observers
).items():
self._content["Categorical Observers"][index] = Specification_Asano2015(
observer["XYZ_2"],
observer["XYZ_10"],
observer["LMS_2"],
observer["LMS_10"],
observer["parameters"],
)
# Colour Normal Observers
workbook_path = os.path.join(
self.record.repository, "dataset", "Data_151Obs.xls"
)
observers = (1, 151)
# Other Information
column_in, column_out = (
index_to_column(observers[0] - 1),
index_to_column(observers[1]),
)
workbook = xlrd.open_workbook(workbook_path)
values_data = cell_range_values(
workbook.sheet_by_index(5), f"{column_in}2:{column_out}9"
)
values_data.extend(
cell_range_values(
workbook.sheet_by_index(5), f"{column_in}12:{column_out}16"
)
)
values_transposed = np.transpose(values_data)
header, values = values_transposed[0], values_transposed[1:]
template = "Asano 2015 {0} Colour Normal Observer No. {1} {2}"
for i, (index, observer) in enumerate(
self.parse_workbook_Asano2015(workbook_path, template, observers).items()
):
self._content["Colour Normal Observers"][index] = Specification_Asano2015(
observer["XYZ_2"],
observer["XYZ_10"],
observer["LMS_2"],
observer["LMS_10"],
observer["parameters"],
dict(zip(header, values[i])),
)
return self._content
[docs]
@staticmethod
def parse_workbook_Asano2015(
workbook: str, template: str, observers: tuple = (1, 10)
) -> Dict[str, Dict]:
"""
Parse given *Asano (2015)* *Observer Function Database* workbook.
Parameters
----------
workbook
*Asano (2015)* *Observer Function Database* workbook path.
template
Template used to create the *CMFS* names.
observers
Observers range.
Returns
-------
:class:`dict`
*Asano (2015)* *Observer Function Database* workbook observer data.
"""
book = xlrd.open_workbook(workbook)
# "CIE XYZ" and "LMS" CMFS.
column_in, column_out = (
index_to_column(observers[0] + 1),
index_to_column(observers[1] + 1),
)
shape = SpectralShape(390, 780, 5)
wavelengths = shape.range()
data: Dict = {}
for i, cmfs in enumerate(
[
(XYZ_ColourMatchingFunctions, "XYZ"),
(LMS_ConeFundamentals, "LMS"),
]
):
for j, degree in enumerate([(2, "2$^\\circ$"), (10, "10$^\\circ$")]):
sheet = book.sheet_by_index(j + (i * 2))
x = np.transpose(
cell_range_values(sheet, f"{column_in}3:{column_out}81")
)
y = np.transpose(
cell_range_values(sheet, f"{column_in}82:{column_out}160")
)
z = np.transpose(
cell_range_values(sheet, f"{column_in}161:{column_out}239")
)
for k in range(observers[1]):
observer = k + 1
rgb = tstack([x[k], y[k], z[k]])
if data.get(observer) is None:
data[observer] = {}
key = f"{cmfs[1]}_{degree[0]}"
data[observer][key] = cmfs[0](
rgb,
domain=wavelengths,
name=template.format(degree[0], observer, cmfs[1]),
display_name=template.format(degree[0], observer, cmfs[1]),
)
# Parameters
column_in, column_out = (
index_to_column(observers[0] - 1),
index_to_column(observers[1]),
)
values = np.transpose(
cell_range_values(book.sheet_by_index(4), f"{column_in}2:{column_out}10")
)
header, values = values[0], values[1:]
for i in range(observers[1]):
observer = i + 1
data[observer]["parameters"] = dict(zip(header, as_float_array(values[i])))
return data
_DATASET_LOADER_ASANO2015: DatasetLoader_Asano2015 | None = None
"""
Singleton instance of the *Asano (2015)* *Observer Function Database* dataset
loader.
"""
[docs]
def build_Asano2015(load: bool = True) -> DatasetLoader_Asano2015:
"""
Singleton factory that the builds *Asano (2015)*
*Observer Function Database* dataset loader.
Parameters
----------
load
Whether to load the dataset upon instantiation.
Returns
-------
:class:`colour_datasets.loaders.DatasetLoader_Asano2015`
Singleton instance of the *Asano (2015)* *Observer Function Database*
dataset loader.
References
----------
:cite:`Asano2015`
"""
global _DATASET_LOADER_ASANO2015 # noqa: PLW0603
if _DATASET_LOADER_ASANO2015 is None:
_DATASET_LOADER_ASANO2015 = DatasetLoader_Asano2015()
if load:
_DATASET_LOADER_ASANO2015.load()
return _DATASET_LOADER_ASANO2015
if __name__ == "__main__":
import colour_datasets
colour_datasets.load("3252742")