Coverage for io/tests/test_ctl.py: 100%

39 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-16 22:49 +1300

1"""Define the unit tests for the :mod:`colour.io.ctl` module.""" 

2 

3from __future__ import annotations 

4 

5import os 

6import shutil 

7import tempfile 

8import textwrap 

9 

10import numpy as np 

11 

12from colour.constants import TOLERANCE_ABSOLUTE_TESTS 

13from colour.io import ( 

14 ctl_render, 

15 process_image_ctl, 

16 read_image, 

17 template_ctl_transform_float, 

18 template_ctl_transform_float3, 

19) 

20from colour.utilities import full, is_ctlrender_installed 

21 

22__author__ = "Colour Developers" 

23__copyright__ = "Copyright 2013 Colour Developers" 

24__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" 

25__maintainer__ = "Colour Developers" 

26__email__ = "colour-developers@colour-science.org" 

27__status__ = "Production" 

28 

29__all__ = [ 

30 "ROOT_RESOURCES", 

31 "TestCtlRender", 

32 "TestProcessImageCtl", 

33 "TestTemplateCtlTransformFloat", 

34 "TestTemplateCtlTransformFloat3", 

35] 

36 

37ROOT_RESOURCES: str = os.path.join(os.path.dirname(__file__), "resources") 

38 

39# TODO: Reinstate coverage when "ctlrender" is tivially available 

40# cross-platform. 

41 

42 

43class TestCtlRender: 

44 """Define :func:`colour.io.ctl.ctl_render` definition unit tests methods.""" 

45 

46 def setup_method(self) -> None: 

47 """Initialise the common tests attributes.""" 

48 

49 self._temporary_directory = tempfile.mkdtemp() 

50 

51 def teardown_method(self) -> None: 

52 """After tests actions.""" 

53 

54 shutil.rmtree(self._temporary_directory) 

55 

56 def test_ctl_render(self) -> None: # pragma: no cover 

57 """Test :func:`colour.io.ctl.ctl_render` definition.""" 

58 

59 if not is_ctlrender_installed(): 

60 return 

61 

62 ctl_adjust_gain_float = template_ctl_transform_float( 

63 "rIn * gain[0]", 

64 "gIn * gain[1]", 

65 "bIn * gain[2]", 

66 description="Adjust Gain", 

67 parameters=["input float gain[3] = {1.0, 1.0, 1.0}"], 

68 ) 

69 

70 ctl_adjust_exposure_float = template_ctl_transform_float( 

71 "rIn * pow(2, exposure)", 

72 "gIn * pow(2, exposure)", 

73 "bIn * pow(2, exposure)", 

74 description="Adjust Exposure", 

75 parameters=["input float exposure = 0.0"], 

76 ) 

77 

78 path_input = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr") 

79 path_output = os.path.join( 

80 self._temporary_directory, "CMS_Test_Pattern_Float.exr" 

81 ) 

82 

83 ctl_render( 

84 path_input, 

85 path_output, 

86 { 

87 ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"], 

88 ctl_adjust_exposure_float: ["-param1 exposure 1.0"], 

89 }, 

90 "-verbose", 

91 "-force", 

92 ) 

93 

94 np.testing.assert_allclose( 

95 read_image(path_output)[..., 0:3], 

96 read_image(path_input) * [1, 2, 4], 

97 atol=TOLERANCE_ABSOLUTE_TESTS, 

98 ) 

99 

100 ctl_render( 

101 path_input, 

102 path_output, 

103 { 

104 os.path.join(ROOT_RESOURCES, "Adjust_Exposure_Float3.ctl"): [ 

105 "-param1 exposure 1.0" 

106 ], 

107 }, 

108 "-verbose", 

109 "-force", 

110 env=dict(os.environ, CTL_MODULE_PATH=ROOT_RESOURCES), 

111 ) 

112 

113 np.testing.assert_allclose( 

114 read_image(path_output)[..., 0:3], 

115 read_image(path_input) * 2, 

116 atol=TOLERANCE_ABSOLUTE_TESTS, 

117 ) 

118 

119 

120class TestProcessImageCtl: 

121 """ 

122 Define :func:`colour.io.ctl.process_image_ctl` definition unit tests 

123 methods. 

124 """ 

125 

126 def test_process_image_ctl(self) -> None: # pragma: no cover 

127 """Test :func:`colour.io.ctl.process_image_ctl` definition.""" 

128 

129 if not is_ctlrender_installed(): 

130 return 

131 

132 ctl_adjust_gain_float = template_ctl_transform_float( 

133 "rIn * gain[0]", 

134 "gIn * gain[1]", 

135 "bIn * gain[2]", 

136 description="Adjust Gain", 

137 parameters=["input float gain[3] = {1.0, 1.0, 1.0}"], 

138 ) 

139 

140 np.testing.assert_allclose( 

141 process_image_ctl( 

142 0.18, 

143 { 

144 ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"], 

145 }, 

146 "-verbose", 

147 "-force", 

148 ), 

149 0.18 / 2, 

150 atol=0.0001, 

151 ) 

152 

153 np.testing.assert_allclose( 

154 process_image_ctl( 

155 np.array([0.18, 0.18, 0.18]), 

156 { 

157 ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"], 

158 }, 

159 ), 

160 np.array([0.18 / 2, 0.18, 0.18 * 2]), 

161 atol=0.0001, 

162 ) 

163 

164 np.testing.assert_allclose( 

165 process_image_ctl( 

166 np.array([[0.18, 0.18, 0.18]]), 

167 { 

168 ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"], 

169 }, 

170 ), 

171 np.array([[0.18 / 2, 0.18, 0.18 * 2]]), 

172 atol=0.0001, 

173 ) 

174 

175 np.testing.assert_allclose( 

176 process_image_ctl( 

177 np.array([[[0.18, 0.18, 0.18]]]), 

178 { 

179 ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"], 

180 }, 

181 ), 

182 np.array([[[0.18 / 2, 0.18, 0.18 * 2]]]), 

183 atol=0.0001, 

184 ) 

185 

186 np.testing.assert_allclose( 

187 process_image_ctl( 

188 full([4, 2, 3], 0.18), 

189 { 

190 ctl_adjust_gain_float: ["-param3 gain 0.5 1.0 2.0"], 

191 }, 

192 ), 

193 full([4, 2, 3], 0.18) * [0.5, 1.0, 2.0], 

194 atol=0.0001, 

195 ) 

196 

197 

198class TestTemplateCtlTransformFloat: 

199 """ 

200 Define :func:`colour.io.ctl.template_ctl_transform_float` definition unit 

201 tests methods. 

202 """ 

203 

204 def test_template_ctl_transform_float(self) -> None: 

205 """Test :func:`colour.io.ctl.template_ctl_transform_float` definition.""" 

206 

207 ctl_foo_bar_float = template_ctl_transform_float( 

208 "rIn + foo[0]", 

209 "gIn + foo[1]", 

210 "bIn + foo[2]", 

211 description="Foo & Bar", 

212 imports=['import "Foo.ctl";', 'import "Bar.ctl";'], 

213 parameters=[ 

214 "input float foo[3] = {1.0, 1.0, 1.0}", 

215 "input float bar = 1.0", 

216 ], 

217 header="// Custom Header", 

218 ) 

219 

220 target = textwrap.dedent( 

221 """ 

222 // Foo & Bar 

223 

224 import "Foo.ctl"; 

225 import "Bar.ctl"; 

226 

227 // Custom Header 

228 void main 

229 ( 

230 output varying float rOut, 

231 output varying float gOut, 

232 output varying float bOut, 

233 output varying float aOut, 

234 input varying float rIn, 

235 input varying float gIn, 

236 input varying float bIn, 

237 input varying float aIn = 1.0, 

238 input float foo[3] = {1.0, 1.0, 1.0}, 

239 input float bar = 1.0 

240 ) 

241 { 

242 rOut = rIn + foo[0]; 

243 gOut = gIn + foo[1]; 

244 bOut = bIn + foo[2]; 

245 aOut = aIn; 

246 }"""[1:] 

247 ) 

248 assert ctl_foo_bar_float == target 

249 

250 ctl_no_description_float = template_ctl_transform_float("rIn * 2.0") 

251 

252 target_no_description = ( 

253 '// "float" Processing Function\n\n' 

254 "void main\n" 

255 "(\n" 

256 " output varying float rOut,\n" 

257 " output varying float gOut,\n" 

258 " output varying float bOut,\n" 

259 " output varying float aOut,\n" 

260 " input varying float rIn,\n" 

261 " input varying float gIn,\n" 

262 " input varying float bIn,\n" 

263 " input varying float aIn = 1.0)\n" 

264 "{\n" 

265 " rOut = rIn * 2.0;\n" 

266 " gOut = rIn * 2.0;\n" 

267 " bOut = rIn * 2.0;\n" 

268 " aOut = aIn;\n" 

269 "}" 

270 ) 

271 assert ctl_no_description_float == target_no_description 

272 

273 

274class TestTemplateCtlTransformFloat3: 

275 """ 

276 Define :func:`colour.io.ctl.template_ctl_transform_float3` definition unit 

277 tests methods. 

278 """ 

279 

280 def test_template_ctl_transform_float3(self) -> None: 

281 """Test :func:`colour.io.ctl.template_ctl_transform_float3` definition.""" 

282 

283 ctl_foo_bar_float3 = template_ctl_transform_float3( 

284 "baz(rgbIn, foo, bar)", 

285 description="Foo, Bar & Baz", 

286 imports=[ 

287 '// import "Foo.ctl";', 

288 '// import "Bar.ctl";', 

289 '// import "Baz.ctl";', 

290 ], 

291 parameters=[ 

292 "input float foo[3] = {1.0, 1.0, 1.0}", 

293 "input float bar = 1.0", 

294 ], 

295 header=textwrap.dedent( 

296 """ 

297 float[3] baz(float rgbIn[3], float foo[3], float qux) 

298 { 

299 float rgbOut[3]; 

300 

301 rgbOut[0] = rgbIn[0] * foo[0]* qux; 

302 rgbOut[1] = rgbIn[1] * foo[1]* qux; 

303 rgbOut[2] = rgbIn[2] * foo[2]* qux; 

304 

305 return rgbOut; 

306 } 

307"""[1:] 

308 ), 

309 ) 

310 # fmt: off 

311 target = textwrap.dedent( 

312 """ 

313 // Foo, Bar & Baz 

314 

315 // import "Foo.ctl"; 

316 // import "Bar.ctl"; 

317 // import "Baz.ctl"; 

318 

319 float[3] baz(float rgbIn[3], float foo[3], float qux) 

320 { 

321 float rgbOut[3]; 

322 

323 rgbOut[0] = rgbIn[0] * foo[0]* qux; 

324 rgbOut[1] = rgbIn[1] * foo[1]* qux; 

325 rgbOut[2] = rgbIn[2] * foo[2]* qux; 

326 

327 return rgbOut; 

328 } 

329 

330 void main 

331 ( 

332 output varying float rOut, 

333 output varying float gOut, 

334 output varying float bOut, 

335 output varying float aOut, 

336 input varying float rIn, 

337 input varying float gIn, 

338 input varying float bIn, 

339 input varying float aIn = 1.0, 

340 input float foo[3] = {1.0, 1.0, 1.0}, 

341 input float bar = 1.0 

342 ) 

343 { 

344 float rgbIn[3] = {rIn, gIn, bIn}; 

345 

346 float rgbOut[3] = baz(rgbIn, foo, bar); 

347 

348 rOut = rgbOut[0]; 

349 gOut = rgbOut[1]; 

350 bOut = rgbOut[2]; 

351 aOut = aIn; 

352 }"""[ 

353 1: 

354 ] 

355 ) 

356 assert ctl_foo_bar_float3 == target 

357 

358 ctl_no_description_float3 = template_ctl_transform_float3("rgbIn * 2.0") 

359 

360 target_no_description = ( 

361 '// "float3" Processing Function\n\n' 

362 "void main\n" 

363 "(\n" 

364 " output varying float rOut,\n" 

365 " output varying float gOut,\n" 

366 " output varying float bOut,\n" 

367 " output varying float aOut,\n" 

368 " input varying float rIn,\n" 

369 " input varying float gIn,\n" 

370 " input varying float bIn,\n" 

371 " input varying float aIn = 1.0)\n" 

372 "{\n" 

373 " float rgbIn[3] = {rIn, gIn, bIn};\n\n" 

374 " float rgbOut[3] = rgbIn * 2.0;\n\n" 

375 " rOut = rgbOut[0];\n" 

376 " gOut = rgbOut[1];\n" 

377 " bOut = rgbOut[2];\n" 

378 " aOut = aIn;\n" 

379 "}" 

380 ) 

381 assert ctl_no_description_float3 == target_no_description