Coverage for colour/models/rgb/cylindrical.py: 100%

120 statements  

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

1""" 

2Cylindrical & Spherical Colour Models 

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

4 

5Define various cylindrical and spherical colour models: 

6 

7- :func:`colour.RGB_to_HSV` 

8- :func:`colour.HSV_to_RGB` 

9- :func:`colour.RGB_to_HSL` 

10- :func:`colour.HSL_to_RGB` 

11- :func:`colour.RGB_to_HCL` 

12- :func:`colour.HCL_to_RGB` 

13 

14These colour models prioritise computational efficiency over perceptual 

15uniformity. While unsuitable for rigorous colour science applications, they 

16serve effectively in image analysis workflows and provide intuitive colour 

17selection interfaces for end-user applications. 

18 

19These transformations are included for practical utility and comprehensive 

20coverage of colour space conversions. 

21 

22References 

23---------- 

24- :cite:`EasyRGBj` : EasyRGB. (n.d.). RGB --> HSV. Retrieved May 18, 2014, 

25 from http://www.easyrgb.com/index.php?X=MATH&H=20#text20 

26- :cite:`EasyRGBk` : EasyRGB. (n.d.). HSL --> RGB. Retrieved May 18, 2014, 

27 from http://www.easyrgb.com/index.php?X=MATH&H=19#text19 

28- :cite:`EasyRGBl` : EasyRGB. (n.d.). RGB --> HSL. Retrieved May 18, 2014, 

29 from http://www.easyrgb.com/index.php?X=MATH&H=18#text18 

30- :cite:`EasyRGBn` : EasyRGB. (n.d.). HSV --> RGB. Retrieved May 18, 2014, 

31 from http://www.easyrgb.com/index.php?X=MATH&H=21#text21 

32- :cite:`Smith1978b` : Smith, A. R. (1978). Color gamut transform pairs. 

33 Proceedings of the 5th Annual Conference on Computer Graphics and 

34 Interactive Techniques - SIGGRAPH "78, 12-19. doi:10.1145/800248.807361 

35- :cite:`Wikipedia2003` : Wikipedia. (2003). HSL and HSV. Retrieved 

36 September 10, 2014, from http://en.wikipedia.org/wiki/HSL_and_HSV 

37- :cite:`Sarifuddin2005` : Sarifuddin, M., & Missaoui, R. (2005). A New 

38 Perceptually Uniform Color Space with Associated Color Similarity Measure 

39 for ContentBased Image and Video Retrieval. 

40- :cite:`Sarifuddin2005a` : Sarifuddin, M., & Missaoui, R. (2005). HCL: a new 

41 Color Space for a more Effective Content-based Image Retrieval. 

42 http://w3.uqo.ca/missaoui/Publications/TRColorSpace.zip 

43- :cite:`Sarifuddin2021` : Sarifuddin, M. (2021). RGB to HCL and HCL to RGB 

44 color conversion (1.0.0). https://www.mathworks.com/matlabcentral/\ 

45fileexchange/100878-rgb-to-hcl-and-hcl-to-rgb-color-conversion 

46- :cite:`Wikipedia2015` : Wikipedia. (2015). HCL color space. Retrieved 

47 April 4, 2021, from https://en.wikipedia.org/wiki/HCL_color_space 

48""" 

49 

50from __future__ import annotations 

51 

52import typing 

53 

54import numpy as np 

55 

56from colour.algebra import sdiv, sdiv_mode 

57 

58if typing.TYPE_CHECKING: 

59 from colour.hints import Domain1, NDArrayFloat, Range1 

60 

61from colour.hints import ArrayLike, cast 

62from colour.utilities import as_float_array, from_range_1, to_domain_1, tsplit, tstack 

63 

64__author__ = "Colour Developers" 

65__copyright__ = "Copyright 2013 Colour Developers" 

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

67__maintainer__ = "Colour Developers" 

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

69__status__ = "Production" 

70 

71__all__ = [ 

72 "RGB_to_HSV", 

73 "HSV_to_RGB", 

74 "RGB_to_HSL", 

75 "HSL_to_RGB", 

76 "RGB_to_HCL", 

77 "HCL_to_RGB", 

78] 

79 

80 

81def RGB_to_HSV(RGB: Domain1) -> Range1: 

82 """ 

83 Convert from *RGB* colourspace to *HSV* colourspace. 

84 

85 Parameters 

86 ---------- 

87 RGB 

88 *RGB* colourspace array. 

89 

90 Returns 

91 ------- 

92 :class:`numpy.ndarray` 

93 *HSV* colourspace array. 

94 

95 Notes 

96 ----- 

97 +------------+-----------------------+---------------+ 

98 | **Domain** | **Scale - Reference** | **Scale - 1** | 

99 +============+=======================+===============+ 

100 | ``RGB`` | 1 | 1 | 

101 +------------+-----------------------+---------------+ 

102 

103 +------------+-----------------------+---------------+ 

104 | **Range** | **Scale - Reference** | **Scale - 1** | 

105 +============+=======================+===============+ 

106 | ``HSV`` | 1 | 1 | 

107 +------------+-----------------------+---------------+ 

108 

109 References 

110 ---------- 

111 :cite:`EasyRGBj`, :cite:`Smith1978b`, :cite:`Wikipedia2003` 

112 

113 Examples 

114 -------- 

115 >>> RGB = np.array([0.45620519, 0.03081071, 0.04091952]) 

116 >>> RGB_to_HSV(RGB) # doctest: +ELLIPSIS 

117 array([ 0.9960394..., 0.9324630..., 0.4562051...]) 

118 """ 

119 

120 RGB = to_domain_1(RGB) 

121 

122 maximum = np.amax(RGB, -1) 

123 delta = np.ptp(RGB, -1) 

124 

125 V = maximum 

126 

127 R, G, B = tsplit(RGB) 

128 

129 with sdiv_mode(): 

130 S = sdiv(delta, maximum) 

131 

132 delta_R = sdiv(((maximum - R) / 6) + (delta / 2), delta) 

133 delta_G = sdiv(((maximum - G) / 6) + (delta / 2), delta) 

134 delta_B = sdiv(((maximum - B) / 6) + (delta / 2), delta) 

135 

136 H = delta_B - delta_G 

137 H = np.where(maximum == G, (1 / 3) + delta_R - delta_B, H) 

138 H = np.where(maximum == B, (2 / 3) + delta_G - delta_R, H) 

139 H[np.asarray(H < 0)] += 1 

140 H[np.asarray(H > 1)] -= 1 

141 H[np.asarray(delta == 0)] = 0 

142 

143 HSV = tstack([H, S, V]) 

144 

145 return from_range_1(HSV) 

146 

147 

148def HSV_to_RGB(HSV: Domain1) -> Range1: 

149 """ 

150 Convert from *HSV* colourspace to *RGB* colourspace. 

151 

152 Parameters 

153 ---------- 

154 HSV 

155 *HSV* colourspace array. 

156 

157 Returns 

158 ------- 

159 :class:`numpy.ndarray` 

160 *RGB* colourspace array. 

161 

162 Notes 

163 ----- 

164 +------------+-----------------------+---------------+ 

165 | **Domain** | **Scale - Reference** | **Scale - 1** | 

166 +============+=======================+===============+ 

167 | ``HSV`` | 1 | 1 | 

168 +------------+-----------------------+---------------+ 

169 

170 +------------+-----------------------+---------------+ 

171 | **Range** | **Scale - Reference** | **Scale - 1** | 

172 +============+=======================+===============+ 

173 | ``RGB`` | 1 | 1 | 

174 +------------+-----------------------+---------------+ 

175 

176 References 

177 ---------- 

178 :cite:`EasyRGBn`, :cite:`Smith1978b`, :cite:`Wikipedia2003` 

179 

180 Examples 

181 -------- 

182 >>> HSV = np.array([0.99603944, 0.93246304, 0.45620519]) 

183 >>> HSV_to_RGB(HSV) # doctest: +ELLIPSIS 

184 array([ 0.4562051..., 0.0308107..., 0.0409195...]) 

185 """ 

186 

187 H, S, V = tsplit(to_domain_1(HSV)) 

188 

189 h = as_float_array(H * 6) 

190 h[np.asarray(h == 6)] = 0 

191 

192 i = np.floor(h) 

193 j = V * (1 - S) 

194 k = V * (1 - S * (h - i)) 

195 l = V * (1 - S * (1 - (h - i))) # noqa: E741 

196 

197 i = tstack([i, i, i]).astype(np.uint8) 

198 

199 RGB = np.choose( 

200 i, 

201 [ 

202 tstack([V, l, j]), 

203 tstack([k, V, j]), 

204 tstack([j, V, l]), 

205 tstack([j, k, V]), 

206 tstack([l, j, V]), 

207 tstack([V, j, k]), 

208 ], 

209 mode="clip", 

210 ) 

211 

212 return from_range_1(RGB) 

213 

214 

215def RGB_to_HSL(RGB: Domain1) -> Range1: 

216 """ 

217 Convert from *RGB* colourspace to *HSL* colourspace. 

218 

219 Parameters 

220 ---------- 

221 RGB 

222 *RGB* colourspace array. 

223 

224 Returns 

225 ------- 

226 :class:`numpy.ndarray` 

227 *HSL* array. 

228 

229 Notes 

230 ----- 

231 +------------+-----------------------+---------------+ 

232 | **Domain** | **Scale - Reference** | **Scale - 1** | 

233 +============+=======================+===============+ 

234 | ``RGB`` | 1 | 1 | 

235 +------------+-----------------------+---------------+ 

236 

237 +------------+-----------------------+---------------+ 

238 | **Range** | **Scale - Reference** | **Scale - 1** | 

239 +============+=======================+===============+ 

240 | ``HSL`` | 1 | 1 | 

241 +------------+-----------------------+---------------+ 

242 

243 References 

244 ---------- 

245 :cite:`EasyRGBl`, :cite:`Smith1978b`, :cite:`Wikipedia2003` 

246 

247 Examples 

248 -------- 

249 >>> RGB = np.array([0.45620519, 0.03081071, 0.04091952]) 

250 >>> RGB_to_HSL(RGB) # doctest: +ELLIPSIS 

251 array([ 0.9960394..., 0.8734714..., 0.2435079...]) 

252 """ 

253 

254 RGB = to_domain_1(RGB) 

255 

256 minimum = np.amin(RGB, -1) 

257 maximum = np.amax(RGB, -1) 

258 delta = np.ptp(RGB, -1) 

259 

260 R, G, B = tsplit(RGB) 

261 

262 L = (maximum + minimum) / 2 

263 

264 with sdiv_mode(): 

265 S = np.where( 

266 L < 0.5, 

267 sdiv(delta, maximum + minimum), 

268 sdiv(delta, 2 - maximum - minimum), 

269 ) 

270 

271 delta_R = sdiv(((maximum - R) / 6) + (delta / 2), delta) 

272 delta_G = sdiv(((maximum - G) / 6) + (delta / 2), delta) 

273 delta_B = sdiv(((maximum - B) / 6) + (delta / 2), delta) 

274 

275 H = delta_B - delta_G 

276 H = np.where(maximum == G, (1 / 3) + delta_R - delta_B, H) 

277 H = np.where(maximum == B, (2 / 3) + delta_G - delta_R, H) 

278 H[np.asarray(H < 0)] += 1 

279 H[np.asarray(H > 1)] -= 1 

280 H[np.asarray(delta == 0)] = 0 

281 

282 HSL = tstack([H, S, L]) 

283 

284 return from_range_1(HSL) 

285 

286 

287def HSL_to_RGB(HSL: Domain1) -> Range1: 

288 """ 

289 Convert from *HSL* colourspace to *RGB* colourspace. 

290 

291 Parameters 

292 ---------- 

293 HSL 

294 *HSL* colourspace array. 

295 

296 Returns 

297 ------- 

298 :class:`numpy.ndarray` 

299 *RGB* colourspace array. 

300 

301 Notes 

302 ----- 

303 +------------+-----------------------+---------------+ 

304 | **Domain** | **Scale - Reference** | **Scale - 1** | 

305 +============+=======================+===============+ 

306 | ``HSL`` | 1 | 1 | 

307 +------------+-----------------------+---------------+ 

308 

309 +------------+-----------------------+---------------+ 

310 | **Range** | **Scale - Reference** | **Scale - 1** | 

311 +============+=======================+===============+ 

312 | ``RGB`` | 1 | 1 | 

313 +------------+-----------------------+---------------+ 

314 

315 References 

316 ---------- 

317 :cite:`EasyRGBk`, :cite:`Smith1978b`, :cite:`Wikipedia2003` 

318 

319 Examples 

320 -------- 

321 >>> HSL = np.array([0.99603944, 0.87347144, 0.24350795]) 

322 >>> HSL_to_RGB(HSL) # doctest: +ELLIPSIS 

323 array([ 0.4562051..., 0.0308107..., 0.0409195...]) 

324 """ 

325 

326 H, S, L = tsplit(to_domain_1(HSL)) 

327 

328 def H_to_RGB(vi: NDArrayFloat, vj: NDArrayFloat, vH: NDArrayFloat) -> NDArrayFloat: 

329 """Convert *hue* value to *RGB* colourspace.""" 

330 

331 vH = as_float_array(vH) 

332 

333 vH[np.asarray(vH < 0)] += 1 

334 vH[np.asarray(vH > 1)] -= 1 

335 

336 v = np.where( 

337 6 * vH < 1, 

338 vi + (vj - vi) * 6 * vH, 

339 np.nan, 

340 ) 

341 v = np.where(np.logical_and(2 * vH < 1, np.isnan(v)), vj, v) 

342 v = np.where( 

343 np.logical_and(3 * vH < 2, np.isnan(v)), 

344 vi + (vj - vi) * ((2 / 3) - vH) * 6, 

345 v, 

346 ) 

347 return np.where(np.isnan(v), vi, v) 

348 

349 j = np.where(L < 0.5, L * (1 + S), (L + S) - (S * L)) 

350 i = 2 * L - j 

351 

352 R = H_to_RGB(i, j, H + (1 / 3)) 

353 G = H_to_RGB(i, j, H) 

354 B = H_to_RGB(i, j, H - (1 / 3)) 

355 

356 R = np.where(S == 0, L, R) 

357 G = np.where(S == 0, L, G) 

358 B = np.where(S == 0, L, B) 

359 

360 RGB = tstack([R, G, B]) 

361 

362 return from_range_1(RGB) 

363 

364 

365def RGB_to_HCL(RGB: Domain1, gamma: float = 3, Y_0: float = 100) -> Range1: 

366 """ 

367 Convert from *RGB* colourspace to *HCL* colourspace according to 

368 *Sarifuddin and Missaoui (2005)* method. 

369 

370 Parameters 

371 ---------- 

372 RGB 

373 *RGB* colourspace array. 

374 gamma 

375 Non-linear lightness exponent matching *Lightness* :math:`L^*`. 

376 Y_0 

377 White reference luminance :math:`Y_0`. 

378 

379 Returns 

380 ------- 

381 :class:`numpy.ndarray` 

382 *HCL* array. 

383 

384 Notes 

385 ----- 

386 +------------+-----------------------+---------------+ 

387 | **Domain** | **Scale - Reference** | **Scale - 1** | 

388 +============+=======================+===============+ 

389 | ``RGB`` | 1 | 1 | 

390 +------------+-----------------------+---------------+ 

391 

392 +------------+-----------------------+---------------+ 

393 | **Range** | **Scale - Reference** | **Scale - 1** | 

394 +============+=======================+===============+ 

395 | ``HCL`` | 1 | 1 | 

396 +------------+-----------------------+---------------+ 

397 

398 - This implementation uses the equations specified in 

399 :cite:`Sarifuddin2005a` with the corrections from 

400 :cite:`Sarifuddin2021`. 

401 

402 References 

403 ---------- 

404 :cite:`Sarifuddin2005`, :cite:`Sarifuddin2005a`, :cite:`Wikipedia2015` 

405 

406 Examples 

407 -------- 

408 >>> RGB = np.array([0.45620519, 0.03081071, 0.04091952]) 

409 >>> RGB_to_HCL(RGB) # doctest: +ELLIPSIS 

410 array([-0.0316785..., 0.2841715..., 0.2285964...]) 

411 """ 

412 

413 R, G, B = tsplit(to_domain_1(RGB)) 

414 

415 Min = np.minimum(np.minimum(R, G), B) 

416 Max = np.maximum(np.maximum(R, G), B) 

417 

418 with sdiv_mode(): 

419 Q = np.exp(sdiv(Min * gamma, Max * Y_0)) 

420 

421 L = (Q * Max + (Q - 1) * Min) / 2 

422 

423 R_G = R - G 

424 G_B = G - B 

425 B_R = B - R 

426 

427 C = Q * (np.abs(R_G) + np.abs(G_B) + np.abs(B_R)) / 3 

428 

429 with sdiv_mode("Ignore"): 

430 H = np.arctan(sdiv(G_B, R_G)) 

431 

432 _2_H_3 = 2 * H / 3 

433 _4_H_3 = 4 * H / 3 

434 

435 H = np.select( 

436 [ 

437 C == 0, 

438 np.logical_and(R_G >= 0, G_B >= 0), 

439 np.logical_and(R_G >= 0, G_B < 0), 

440 np.logical_and(R_G < 0, G_B >= 0), 

441 np.logical_and(R_G < 0, G_B < 0), 

442 ], 

443 [ 

444 0, 

445 _2_H_3, 

446 _4_H_3, 

447 np.pi + _4_H_3, 

448 _2_H_3 - np.pi, 

449 ], 

450 ) 

451 

452 HCL = tstack([H, C, L]) 

453 

454 return from_range_1(HCL) 

455 

456 

457def HCL_to_RGB(HCL: Domain1, gamma: float = 3, Y_0: float = 100) -> Range1: 

458 """ 

459 Convert from *HCL* colourspace to *RGB* colourspace according to 

460 *Sarifuddin and Missaoui (2005)* method. 

461 

462 Parameters 

463 ---------- 

464 HCL 

465 *HCL* colourspace array. 

466 gamma 

467 Non-linear lightness exponent matching *Lightness* :math:`L^*`. 

468 Y_0 

469 White reference luminance :math:`Y_0`. 

470 

471 Returns 

472 ------- 

473 :class:`numpy.ndarray` 

474 *RGB* colourspace array. 

475 

476 Notes 

477 ----- 

478 +------------+-----------------------+---------------+ 

479 | **Domain** | **Scale - Reference** | **Scale - 1** | 

480 +============+=======================+===============+ 

481 | ``HCL`` | 1 | 1 | 

482 +------------+-----------------------+---------------+ 

483 

484 +------------+-----------------------+---------------+ 

485 | **Range** | **Scale - Reference** | **Scale - 1** | 

486 +============+=======================+===============+ 

487 | ``RGB`` | 1 | 1 | 

488 +------------+-----------------------+---------------+ 

489 

490 - This implementation uses the equations specified in 

491 :cite:`Sarifuddin2005a` with the corrections from 

492 :cite:`Sarifuddin2021`. 

493 

494 References 

495 ---------- 

496 :cite:`Sarifuddin2005`, :cite:`Sarifuddin2005a`, :cite:`Wikipedia2015` 

497 

498 Examples 

499 -------- 

500 >>> HCL = np.array([-0.03167854, 0.28417150, 0.22859647]) 

501 >>> HCL_to_RGB(HCL) # doctest: +ELLIPSIS 

502 array([ 0.4562033..., 0.0308104..., 0.0409192...]) 

503 """ 

504 

505 H, C, L = tsplit(to_domain_1(HCL)) 

506 

507 with sdiv_mode(): 

508 Q = np.exp((1 - sdiv(3 * C, 4 * L)) * gamma / Y_0) 

509 

510 Min = sdiv(4 * L - 3 * C, 4 * Q - 2) 

511 Max = Min + sdiv(3 * C, 2 * Q) 

512 

513 tan_3_2_H = np.tan(3 / 2 * H) 

514 tan_3_4_H_MP = np.tan(3 / 4 * (H - np.pi)) 

515 tan_3_4_H = np.tan(3 / 4 * H) 

516 tan_3_2_H_PP = np.tan(3 / 2 * (H + np.pi)) 

517 

518 r_p60 = np.radians(60) 

519 r_p120 = np.radians(120) 

520 r_n60 = np.radians(-60) 

521 r_n120 = np.radians(-120) 

522 

523 def _1_2_3(a: ArrayLike) -> NDArrayFloat: 

524 """Tail-stack specified :math:`a` array as a *bool* dtype.""" 

525 

526 return tstack(cast("ArrayLike", [a, a, a]), dtype=np.bool_) 

527 

528 with sdiv_mode(): 

529 RGB = np.select( 

530 [ 

531 _1_2_3(np.logical_and(H >= 0, r_p60 >= H)), 

532 _1_2_3(np.logical_and(r_p60 < H, r_p120 >= H)), 

533 _1_2_3(np.logical_and(r_p120 < H, np.pi >= H)), 

534 _1_2_3(np.logical_and(r_n60 <= H, H < 0)), 

535 _1_2_3(np.logical_and(r_n120 <= H, r_n60 > H)), 

536 _1_2_3(np.logical_and(-np.pi < H, r_n120 > H)), 

537 ], 

538 [ 

539 tstack( 

540 [ 

541 Max, 

542 (Max * tan_3_2_H + Min) / (1 + tan_3_2_H), 

543 Min, 

544 ] 

545 ), 

546 tstack( 

547 [ 

548 sdiv(Max * (1 + tan_3_4_H_MP) - Min, tan_3_4_H_MP), 

549 Max, 

550 Min, 

551 ] 

552 ), 

553 tstack( 

554 [ 

555 Min, 

556 Max, 

557 Max * (1 + tan_3_4_H_MP) - Min * tan_3_4_H_MP, 

558 ] 

559 ), 

560 tstack( 

561 [ 

562 Max, 

563 Min, 

564 Min * (1 + tan_3_4_H) - Max * tan_3_4_H, 

565 ] 

566 ), 

567 tstack( 

568 [ 

569 sdiv(Min * (1 + tan_3_4_H) - Max, tan_3_4_H), 

570 Min, 

571 Max, 

572 ] 

573 ), 

574 tstack( 

575 [ 

576 Min, 

577 (Min * tan_3_2_H_PP + Max) / (1 + tan_3_2_H_PP), 

578 Max, 

579 ] 

580 ), 

581 ], 

582 ) 

583 

584 return from_range_1(RGB)