Coverage for colour/io/tabular.py: 100%

58 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-15 19:01 +1300

1""" 

2CSV Tabular Data Input / Output 

3=============================== 

4 

5Define input / output utilities for reading and writing spectral data and 

6spectral distributions from/to *CSV* tabular data files. 

7 

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""" 

12 

13from __future__ import annotations 

14 

15import csv 

16import os 

17import tempfile 

18import typing 

19 

20import numpy as np 

21 

22from colour.colorimetry import SpectralDistribution 

23from colour.constants import DTYPE_FLOAT_DEFAULT 

24 

25if typing.TYPE_CHECKING: 

26 from colour.hints import Any, Dict, NDArrayFloat, PathLike 

27 

28from colour.hints import cast 

29from colour.utilities import filter_kwargs 

30 

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" 

37 

38__all__ = [ 

39 "read_spectral_data_from_csv_file", 

40 "read_sds_from_csv_file", 

41 "write_sds_to_csv_file", 

42] 

43 

44 

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:: 

50 

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 

56 

57 and convert it to a *dict* as follows:: 

58 

59 { 

60 'wavelength': ndarray, 

61 'field 1': ndarray, 

62 'field 2': ndarray, 

63 ..., 

64 'field n': ndarray 

65 } 

66 

67 Parameters 

68 ---------- 

69 path 

70 *CSV* file path. 

71 

72 Other Parameters 

73 ---------------- 

74 kwargs 

75 Keywords arguments passed to :func:`numpy.recfromcsv` definition. 

76 

77 Returns 

78 ------- 

79 :class:`dict` 

80 *CSV* file content. 

81 

82 Raises 

83 ------ 

84 IOError 

85 If the file cannot be read. 

86 

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. 

92 

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 """ 

131 

132 path = str(path) 

133 

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) 

144 

145 transpose = settings.get("transpose") 

146 if transpose: 

147 delimiter = cast("str", settings.get("delimiter", ",")) 

148 

149 with open(path) as csv_file: 

150 content = zip(*csv.reader(csv_file, delimiter=delimiter), strict=True) 

151 

152 settings["delimiter"] = "," 

153 

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() 

158 

159 data = np.genfromtxt(path, **filter_kwargs(np.genfromtxt, **settings)) 

160 

161 if transpose: 

162 os.unlink(transposed_csv_file.name) 

163 

164 return {name: data[name] for name in data.dtype.names} # pyright: ignore 

165 

166 

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. 

173 

174 Parameters 

175 ---------- 

176 path 

177 *CSV* file path. 

178 

179 Other Parameters 

180 ---------------- 

181 kwargs 

182 Keywords arguments passed to :func:`numpy.genfromtxt` definition. 

183 

184 Returns 

185 ------- 

186 :class:`dict` 

187 *dict* of :class:`colour.SpectralDistribution` class instances. 

188 

189 Raises 

190 ------ 

191 IOError 

192 If the file cannot be read. 

193 

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 """ 

297 

298 path = str(path) 

299 

300 data = read_spectral_data_from_csv_file(path, **kwargs) 

301 

302 fields = list(data.keys()) 

303 wavelength_field, sd_fields = fields[0], fields[1:] 

304 

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 } 

311 

312 

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. 

318 

319 Parameters 

320 ---------- 

321 sds 

322 Spectral distributions to write to the specified CSV file. 

323 path 

324 CSV file path. 

325 

326 Returns 

327 ------- 

328 :class:`bool` 

329 Definition success. 

330 

331 Raises 

332 ------ 

333 ValueError 

334 If the specified spectral distributions have different shapes. 

335 """ 

336 

337 path = str(path) 

338 

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 ) 

346 

347 raise ValueError(error) 

348 

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 ) 

358 

359 writer.writeheader() 

360 

361 for wavelength in wavelengths: 

362 row = {"wavelength": wavelength} 

363 row.update({field: sds[field][wavelength] for field in fields}) 

364 writer.writerow(row) 

365 

366 return True