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

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

2

3 from __future__ import annotations

4

5 import os

6 import shutil

7 import tempfile

8 import textwrap

9

10 import numpy as np

11

12 from colour.constants import TOLERANCE_ABSOLUTE_TESTS

13 from colour.io import (

14 ctl_render,

15 process_image_ctl,

16 read_image,

17 template_ctl_transform_float,

18 template_ctl_transform_float3,

19)

20 from 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

37 ROOT_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

43 class 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

120 class 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

198 class 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

274 class 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