Skip to content

Commit

Permalink
use directory dependencies
Browse files Browse the repository at this point in the history
Add sample data file

create demo analysis file

remame color analysis classes

Update submodules

add example analysis

change default measurement notes and shortname

Updated submodule submodules/colour-science

add distributed pytest

ruff autofixes

some ruff fixes

update colour-version

quality fixes

update license and package properties

format toml

update license, contact info, and more

update specio
  • Loading branch information
tjdcs committed Oct 20, 2023
1 parent c8f9881 commit ea6d4d1
Show file tree
Hide file tree
Showing 20 changed files with 725 additions and 157 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -838,4 +838,5 @@ FodyWeavers.xsd
### VisualStudio Patch ###
# Additional files built by Visual Studio

# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,visualstudiocode,visualstudio,python,emacs,vim,pycharm
# End of
# https://www.toptal.com/developers/gitignore/api/windows,linux,macos,visualstudiocode,visualstudio,python,emacs,vim,pycharm
11 changes: 11 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Copyright 2013 Tucker Downs

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Color Workbench
# Colour Workbench

This is a useless README file. Thank you.
15 changes: 13 additions & 2 deletions colour_workbench/ETC_reports/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
from .analysis import FundamentalData, ReflectanceData, analyse_measurements_from_file
from .pdf import generate_report_page
from analysis import (
ColourPrecisionAnalysis,
ReflectanceData,
analyse_measurements_from_file,
)
from pdf import generate_report_page

__all__ = [
"generate_report_page",
"ReflectanceData",
"analyse_measurements_from_file",
"ColourPrecisionAnalysis",
]
105 changes: 68 additions & 37 deletions colour_workbench/ETC_reports/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@

import numpy as np
import numpy.typing as npt
import xxhash
from colour.colorimetry.spectrum import MultiSpectralDistributions, SpectralDistribution
from colour.colorimetry.spectrum import (
MultiSpectralDistributions,
SpectralDistribution,
)
from colour.colorimetry.tristimulus_values import sd_to_XYZ
from colour.difference.delta_e import delta_E_CIE2000
from colour.models.cie_lab import XYZ_to_Lab
Expand All @@ -21,8 +23,11 @@
from colour.models.rgb.transfer_functions import st_2084 as pq
from colour.plotting.common import XYZ_to_plotting_colourspace
from colour.temperature.ohno2013 import XYZ_to_CCT_Ohno2013
from matplotlib import pyplot as plt
from specio.fileio import MeasurementList, MeasurementList_Notes, load_measurements
from specio.fileio import (
MeasurementList,
MeasurementList_Notes,
load_measurements,
)


@dataclass
Expand All @@ -41,7 +46,7 @@ def glossiness_ratio(self) -> float:
return self.reflectance_45_45 / self.reflectance_45_0


class FundamentalData:
class ColourPrecisionAnalysis:
@property
def _snr_mask(self):
snr = 10 * np.log10(
Expand All @@ -58,13 +63,19 @@ def _analysis_mask(self):
t = np.all(
(
~np.any(
np.isinf([m.spd.values for m in self._data.measurements]), axis=1
np.isinf([m.spd.values for m in self._data.measurements]),
axis=1,
),
~np.any(
np.isnan([m.spd.values for m in self._data.measurements]), axis=1
np.isnan([m.spd.values for m in self._data.measurements]),
axis=1,
),
~np.any(
np.isnan([m.XYZ for m in self._data.measurements]), axis=1
),
~np.any(
np.isinf([m.XYZ for m in self._data.measurements]), axis=1
),
~np.any(np.isnan([m.XYZ for m in self._data.measurements]), axis=1),
~np.any(np.isinf([m.XYZ for m in self._data.measurements]), axis=1),
),
axis=0,
)
Expand All @@ -84,14 +95,18 @@ def black(self) -> dict:
tmp["measurements"] = measurements = self._data.measurements[mask]
spd_shape = measurements[0].spd.shape

tmp["values"] = np.transpose(np.array([m.spd.values for m in measurements]))
tmp["values"] = np.transpose(
np.array([m.spd.values for m in measurements])
)
tmp["spectral_deviation"] = np.std(tmp["values"], axis=1)

tmp["spd"] = np.mean(tmp["values"], axis=1)
tmp["spd"] = savgol_filter(tmp["spd"], 5, 2, mode="nearest")
tmp["spd"] = SpectralDistribution(tmp["spd"], domain=spd_shape)

tmp["XYZ"] = sd_to_XYZ(SpectralDistribution(tmp["spd"], spd_shape), k=683)
tmp["XYZ"] = sd_to_XYZ(
SpectralDistribution(tmp["spd"], spd_shape), k=683
)
tmp["power"] = np.sum(tmp["spd"].values)
return self._black

Expand All @@ -101,12 +116,19 @@ def primary_matrix(self) -> npt.NDArray:
return self._pm

color_masks = []
color_masks.append(np.all(self._data.test_colors[:, (1, 2)] == 0, axis=1))
color_masks.append(np.all(self._data.test_colors[:, (0, 2)] == 0, axis=1))
color_masks.append(np.all(self._data.test_colors[:, (0, 1)] == 0, axis=1))
color_masks.append(
np.all(self._data.test_colors[:, (1, 2)] == 0, axis=1)
)
color_masks.append(
np.all(self._data.test_colors[:, (0, 2)] == 0, axis=1)
)
color_masks.append(
np.all(self._data.test_colors[:, (0, 1)] == 0, axis=1)
)
color_masks.append(
np.all(
self._data.test_colors[:, (1, 2)].T == self._data.test_colors[:, (0)],
self._data.test_colors[:, (0)]
== self._data.test_colors[:, (1, 2)].T,
axis=0,
)
)
Expand All @@ -115,15 +137,17 @@ def primary_matrix(self) -> npt.NDArray:

xy = np.zeros((4, 2))
for idx, m in enumerate(color_masks):
color_measurements = self._data.measurements[m & self._analysis_mask]
color_measurements = self._data.measurements[
m & self._analysis_mask
]
color_XYZ = [t.XYZ for t in color_measurements] - self.black["XYZ"]
xys = XYZ_to_xy(color_XYZ)

try:
# Find mean chromaticity without being influenced by outliers
cov = EllipticEnvelope().fit(xys)
xy[idx, :] = cov.location_
except ValueError as e:
except ValueError:
# Covariance fit failed, probably because the data is well
# clustered, traditional mean can be used instead.
xy[idx, :] = np.mean(xys, axis=0)
Expand All @@ -139,7 +163,8 @@ def grey(self):

grey = self._grey = {}
grey_mask = np.all(
self._data.test_colors[:, (1, 2)].T == self._data.test_colors[:, (0)],
self._data.test_colors[:, (0)]
== self._data.test_colors[:, (1, 2)].T,
axis=0,
)
grey_mask = grey_mask & self._analysis_mask
Expand Down Expand Up @@ -182,10 +207,13 @@ def white(self):

white["xyz"] = self.primary_matrix.dot([1, 1, 1])

single_color_idx = np.all(self._data.test_colors == [1023, 1023, 1023], axis=1)
single_color_idx = np.all(
self._data.test_colors == [1023, 1023, 1023], axis=1
)
single_color_measurements = self._data.measurements[single_color_idx]
white["peak"] = np.mean(
[m.XYZ - self.black["XYZ"] for m in single_color_measurements], axis=0
[m.XYZ - self.black["XYZ"] for m in single_color_measurements],
axis=0,
)
white["nits_quantized"] = pq.eotf_ST2084(
np.round(pq.eotf_inverse_ST2084(white["peak"][1]) * 1023) / 1023
Expand All @@ -206,7 +234,9 @@ def test_colors_linear(self):
if hasattr(self, "_test_colors_linear"):
return self._test_colors_linear

tmp = self._test_colors_linear = pq.eotf_ST2084(self.test_colors.T / 1023)
tmp = self._test_colors_linear = pq.eotf_ST2084(
self.test_colors.T / 1023
)
clipping_mask = tmp > self.white["nits_quantized"]
tmp[clipping_mask] = self.white["nits_quantized"]
return self._test_colors_linear
Expand Down Expand Up @@ -247,14 +277,17 @@ def error(self):
return self._err
norm = partial(np.linalg.norm, axis=1)
err = {}
err["XYZ"] = norm(self.measured_colors["XYZ"] - self.expected_colors["XYZ"])
err["XYZ"] = norm(
self.measured_colors["XYZ"] - self.expected_colors["XYZ"]
)

err["ICtCp"] = 720 * norm(
(self.measured_colors["ICtCp"] - self.expected_colors["ICtCp"])
* (1, 0.5, 1)
)
err["dI"] = 720 * norm(
(self.measured_colors["ICtCp"] - self.expected_colors["ICtCp"]) * (1, 0, 0)
(self.measured_colors["ICtCp"] - self.expected_colors["ICtCp"])
* (1, 0, 0)
)
err["dChromatic"] = 720 * norm(
(self.measured_colors["ICtCp"] - self.expected_colors["ICtCp"])
Expand All @@ -272,14 +305,19 @@ def error(self):
def metadata(self) -> MeasurementList_Notes:
return self._data.metadata

@metadata.setter
def metadata(self, new_data: MeasurementList_Notes):
self._data.metadata = new_data

@property
def shortname(self) -> str:
if self._shortname is None:
if self.metadata.notes is None:
spds = MultiSpectralDistributions([m.spd for m in self.measurements])
return xxhash.xxh32_hexdigest(np.ascontiguousarray(spds.values).data)
return self.metadata.notes
return self._shortname
if self._shortname is not None:
return self._shortname

if self.metadata.notes is None or self.metadata.notes == "":
return self._data.shortname

return self.metadata.notes

@shortname.setter
def shortname(self, name: str | None):
Expand Down Expand Up @@ -317,12 +355,5 @@ def __init__(self, measurements: MeasurementList):
def analyse_measurements_from_file(file: str):
measurements = load_measurements(file)

fundamentalData = FundamentalData(measurements)
fundamentalData = ColourPrecisionAnalysis(measurements)
return fundamentalData
pass


if __name__ == "__main__":
file = "/Users/tucker/Dev/ETC_Jan/data/measurements/F_BP2v2_Brompton.csmf"
data = analyse_measurements_from_file(file)
print(data)
Loading

0 comments on commit ea6d4d1

Please sign in to comment.