Coverage for io/tabular.py: 66%
58 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2CSV Tabular Data Input / Output
3===============================
5Define input / output utilities for reading and writing spectral data and
6spectral distributions from/to *CSV* tabular data files.
8- :func:`colour.read_spectral_data_from_csv_file`
9- :func:`colour.read_sds_from_csv_file`
10- :func:`colour.write_sds_to_csv_file`
11"""
13from __future__ import annotations
15import csv
16import os
17import tempfile
18import typing
20import numpy as np
22from colour.colorimetry import SpectralDistribution
23from colour.constants import DTYPE_FLOAT_DEFAULT
25if typing.TYPE_CHECKING:
26 from colour.hints import Any, Dict, NDArrayFloat, PathLike
28from colour.hints import cast
29from colour.utilities import filter_kwargs
31__author__ = "Colour Developers"
32__copyright__ = "Copyright 2013 Colour Developers"
33__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
34__maintainer__ = "Colour Developers"
35__email__ = "colour-developers@colour-science.org"
36__status__ = "Production"
38__all__ = [
39 "read_spectral_data_from_csv_file",
40 "read_sds_from_csv_file",
41 "write_sds_to_csv_file",
42]
45def read_spectral_data_from_csv_file(
46 path: str | PathLike, **kwargs: Any
47) -> Dict[str, NDArrayFloat]:
48 """
49 Read spectral data from the specified *CSV* file in the following form::
51 390, 4.15003e-04, 3.68349e-04, 9.54729e-03
52 395, 1.05192e-03, 9.58658e-04, 2.38250e-02
53 400, 2.40836e-03, 2.26991e-03, 5.66498e-02
54 ...
55 830, 9.74306e-07, 9.53411e-08, 0.00000
57 and convert it to a *dict* as follows::
59 {
60 'wavelength': ndarray,
61 'field 1': ndarray,
62 'field 2': ndarray,
63 ...,
64 'field n': ndarray
65 }
67 Parameters
68 ----------
69 path
70 *CSV* file path.
72 Other Parameters
73 ----------------
74 kwargs
75 Keywords arguments passed to :func:`numpy.recfromcsv` definition.
77 Returns
78 -------
79 :class:`dict`
80 *CSV* file content.
82 Raises
83 ------
84 IOError
85 If the file cannot be read.
87 Notes
88 -----
89 - A *CSV* spectral data file should at least define two fields: one
90 for the wavelengths and one for the associated values of one
91 spectral distribution.
93 Examples
94 --------
95 >>> import os
96 >>> from pprint import pprint
97 >>> csv_file = os.path.join(
98 ... os.path.dirname(__file__),
99 ... "tests",
100 ... "resources",
101 ... "colorchecker_n_ohta.csv",
102 ... )
103 >>> sds_data = read_spectral_data_from_csv_file(csv_file)
104 >>> pprint(list(sds_data.keys()))
105 ['wavelength',
106 '1',
107 '2',
108 '3',
109 '4',
110 '5',
111 '6',
112 '7',
113 '8',
114 '9',
115 '10',
116 '11',
117 '12',
118 '13',
119 '14',
120 '15',
121 '16',
122 '17',
123 '18',
124 '19',
125 '20',
126 '21',
127 '22',
128 '23',
129 '24']
130 """
132 path = str(path)
134 settings = {
135 "names": True,
136 "delimiter": ",",
137 "case_sensitive": True,
138 # "case_sensitive": "lower",
139 "deletechars": "",
140 "replace_space": " ",
141 "dtype": DTYPE_FLOAT_DEFAULT,
142 }
143 settings.update(**kwargs)
145 transpose = settings.get("transpose")
146 if transpose:
147 delimiter = cast("str", settings.get("delimiter", ","))
149 with open(path) as csv_file:
150 content = zip(*csv.reader(csv_file, delimiter=delimiter), strict=True)
152 settings["delimiter"] = ","
154 with tempfile.NamedTemporaryFile(mode="w", delete=False) as transposed_csv_file:
155 path = transposed_csv_file.name
156 csv.writer(transposed_csv_file).writerows(content)
157 transposed_csv_file.close()
159 data = np.genfromtxt(path, **filter_kwargs(np.genfromtxt, **settings))
161 if transpose:
162 os.unlink(transposed_csv_file.name)
164 return {name: data[name] for name in data.dtype.names} # pyright: ignore
167def read_sds_from_csv_file(
168 path: str | PathLike, **kwargs: Any
169) -> Dict[str, SpectralDistribution]:
170 """
171 Read spectral data from the specified *CSV* file and convert its content
172 to a *dict* of :class:`colour.SpectralDistribution` class instances.
174 Parameters
175 ----------
176 path
177 *CSV* file path.
179 Other Parameters
180 ----------------
181 kwargs
182 Keywords arguments passed to :func:`numpy.genfromtxt` definition.
184 Returns
185 -------
186 :class:`dict`
187 *dict* of :class:`colour.SpectralDistribution` class instances.
189 Raises
190 ------
191 IOError
192 If the file cannot be read.
194 Examples
195 --------
196 >>> from colour.utilities import numpy_print_options
197 >>> import os
198 >>> csv_file = os.path.join(
199 ... os.path.dirname(__file__),
200 ... "tests",
201 ... "resources",
202 ... "colorchecker_n_ohta.csv",
203 ... )
204 >>> sds = read_sds_from_csv_file(csv_file)
205 >>> print(tuple(sds.keys()))
206 ('1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', \
207'14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24')
208 >>> with numpy_print_options(suppress=True):
209 ... sds["1"] # doctest: +ELLIPSIS
210 ...
211 SpectralDistribution([[ 380. , 0.048],
212 [ 385. , 0.051],
213 [ 390. , 0.055],
214 [ 395. , 0.06 ],
215 [ 400. , 0.065],
216 [ 405. , 0.068],
217 [ 410. , 0.068],
218 [ 415. , 0.067],
219 [ 420. , 0.064],
220 [ 425. , 0.062],
221 [ 430. , 0.059],
222 [ 435. , 0.057],
223 [ 440. , 0.055],
224 [ 445. , 0.054],
225 [ 450. , 0.053],
226 [ 455. , 0.053],
227 [ 460. , 0.052],
228 [ 465. , 0.052],
229 [ 470. , 0.052],
230 [ 475. , 0.053],
231 [ 480. , 0.054],
232 [ 485. , 0.055],
233 [ 490. , 0.057],
234 [ 495. , 0.059],
235 [ 500. , 0.061],
236 [ 505. , 0.062],
237 [ 510. , 0.065],
238 [ 515. , 0.067],
239 [ 520. , 0.07 ],
240 [ 525. , 0.072],
241 [ 530. , 0.074],
242 [ 535. , 0.075],
243 [ 540. , 0.076],
244 [ 545. , 0.078],
245 [ 550. , 0.079],
246 [ 555. , 0.082],
247 [ 560. , 0.087],
248 [ 565. , 0.092],
249 [ 570. , 0.1 ],
250 [ 575. , 0.107],
251 [ 580. , 0.115],
252 [ 585. , 0.122],
253 [ 590. , 0.129],
254 [ 595. , 0.134],
255 [ 600. , 0.138],
256 [ 605. , 0.142],
257 [ 610. , 0.146],
258 [ 615. , 0.15 ],
259 [ 620. , 0.154],
260 [ 625. , 0.158],
261 [ 630. , 0.163],
262 [ 635. , 0.167],
263 [ 640. , 0.173],
264 [ 645. , 0.18 ],
265 [ 650. , 0.188],
266 [ 655. , 0.196],
267 [ 660. , 0.204],
268 [ 665. , 0.213],
269 [ 670. , 0.222],
270 [ 675. , 0.231],
271 [ 680. , 0.242],
272 [ 685. , 0.251],
273 [ 690. , 0.261],
274 [ 695. , 0.271],
275 [ 700. , 0.282],
276 [ 705. , 0.294],
277 [ 710. , 0.305],
278 [ 715. , 0.318],
279 [ 720. , 0.334],
280 [ 725. , 0.354],
281 [ 730. , 0.372],
282 [ 735. , 0.392],
283 [ 740. , 0.409],
284 [ 745. , 0.42 ],
285 [ 750. , 0.436],
286 [ 755. , 0.45 ],
287 [ 760. , 0.462],
288 [ 765. , 0.465],
289 [ 770. , 0.448],
290 [ 775. , 0.432],
291 [ 780. , 0.421]],
292 SpragueInterpolator,
293 {},
294 Extrapolator,
295 {'method': 'Constant', 'left': None, 'right': None})
296 """
298 path = str(path)
300 data = read_spectral_data_from_csv_file(path, **kwargs)
302 fields = list(data.keys())
303 wavelength_field, sd_fields = fields[0], fields[1:]
305 return {
306 sd_field: SpectralDistribution(
307 data[sd_field], data[wavelength_field], name=sd_field
308 )
309 for sd_field in sd_fields
310 }
313def write_sds_to_csv_file(
314 sds: Dict[str, SpectralDistribution], path: str | PathLike
315) -> bool:
316 """
317 Write spectral distributions to a CSV file.
319 Parameters
320 ----------
321 sds
322 Spectral distributions to write to the specified CSV file.
323 path
324 CSV file path.
326 Returns
327 -------
328 :class:`bool`
329 Definition success.
331 Raises
332 ------
333 ValueError
334 If the specified spectral distributions have different shapes.
335 """
337 path = str(path)
339 if len(sds) != 1:
340 shapes = [sd.shape for sd in sds.values()]
341 if not all(shape == shapes[0] for shape in shapes):
342 error = (
343 "Cannot write spectral distributions "
344 'with different shapes to "CSV" file!'
345 )
347 raise ValueError(error)
349 wavelengths = next(iter(sds.values())).wavelengths
350 with open(path, "w") as csv_file:
351 fields = sorted(sds.keys())
352 writer = csv.DictWriter(
353 csv_file,
354 delimiter=",",
355 fieldnames=["wavelength", *fields],
356 lineterminator="\n",
357 )
359 writer.writeheader()
361 for wavelength in wavelengths:
362 row = {"wavelength": wavelength}
363 row.update({field: sds[field][wavelength] for field in fields})
364 writer.writerow(row)
366 return True