def __init__( self, name: Optional[str] = None, comments: Optional[Sequence[str]] = None, ): self._name = f"LUT Sequence Operator {id(self)}" self.name = optional(name, self._name) # TODO: Remove pragma when https://github.com/python/mypy/issues/3004 # is resolved. self._comments: List[str] = [] self.comments = optional(comments, self._comments) # type: ignore[arg-type]
def get_attribute(attribute: str) -> Any: """ Return given attribute value. Parameters ---------- attribute Attribute to retrieve, ``attribute`` must have a namespace module, e.g. *colour.models.eotf_BT2020*. Returns ------- :class:`object` Retrieved attribute value. Examples -------- >>> get_attribute('colour.models.eotf_BT2020') # doctest: +ELLIPSIS <function eotf_BT2020 at 0x...> """ attest("." in attribute, '"{0}" attribute has no namespace!') module_name, attribute = attribute.rsplit(".", 1) module = optional(sys.modules.get(module_name), import_module(module_name)) attest( module is not None, f'"{module_name}" module does not exists or cannot be imported!', ) return attrgetter(attribute)(module)
def _UCS_Luo2006_callable_to_UCS_Li2017_docstring(callable_: Callable) -> str: """ Convert given *Luo et al. (2006)* callable docstring to *Li et al. (2017)* docstring. Parameters ---------- callable_ Callable to use the docstring from. Returns ------- :class:`str` Docstring. """ docstring = callable_.__doc__ # NOTE: Required for optimised python launch. docstring = optional(docstring, "") docstring = docstring.replace("Luo et al. (2006)", "Li et al. (2017)") docstring = docstring.replace("CIECAM02", "CAM16") docstring = docstring.replace("CAM02", "CAM16") docstring = docstring.replace("Luo2006b", "Li2017") match = re.match("(.*)Examples", docstring, re.DOTALL) if match is not None: docstring = match.group(1) return docstring
def __init__( self, matrix: Optional[ArrayLike] = None, offset: Optional[ArrayLike] = None, *args: Any, **kwargs: Any, ): super().__init__(*args, **kwargs) # TODO: Remove pragma when https://github.com/python/mypy/issues/3004 # is resolved. self._matrix: NDArray = np.diag(ones(4)) self.matrix = cast(ArrayLike, optional(matrix, self._matrix)) # type: ignore[assignment] self._offset: NDArray = zeros(4) self.offset = cast(ArrayLike, optional(offset, self._offset)) # type: ignore[assignment]
def luminous_flux( sd: SpectralDistribution, lef: Optional[SpectralDistribution] = None, K_m: Floating = CONSTANT_K_M, ) -> Floating: """ Return the *luminous flux* for given spectral distribution using given luminous efficiency function. Parameters ---------- sd test spectral distribution lef :math:`V(\\lambda)` luminous efficiency function, default to the *CIE 1924 Photopic Standard Observer*. K_m :math:`lm\\cdot W^{-1}` maximum photopic luminous efficiency. Returns ------- :class:`numpy.floating` Luminous flux. References ---------- :cite:`Wikipedia2003b` Examples -------- >>> from colour import SDS_LIGHT_SOURCES >>> sd = SDS_LIGHT_SOURCES['Neodimium Incandescent'] >>> luminous_flux(sd) # doctest: +ELLIPSIS 23807.6555273... """ lef = cast( SpectralDistribution, optional(lef, SDS_LEFS_PHOTOPIC["CIE 1924 Photopic Standard Observer"]), ) lef = reshape_sd( lef, sd.shape, extrapolator_kwargs={ "method": "Constant", "left": 0, "right": 0 }, ) flux = K_m * np.trapz(lef.values * sd.values, sd.wavelengths) return as_float_scalar(flux)
def __init__( self, interpolator: Optional[TypeInterpolator] = None, method: Union[Literal["Linear", "Constant"], str] = "Linear", left: Optional[Number] = None, right: Optional[Number] = None, dtype: Optional[Type[DTypeNumber]] = None, ): dtype = cast(Type[DTypeNumber], optional(dtype, DEFAULT_FLOAT_DTYPE)) self._interpolator: TypeInterpolator = NullInterpolator( np.array([-np.inf, np.inf]), np.array([-np.inf, np.inf])) self.interpolator = optional(interpolator, self._interpolator) self._method: Union[Literal["Linear", "Constant"], str] = "Linear" self.method = cast( Union[Literal["Linear", "Constant"], str], optional(method, self._method), ) self._right: Optional[Number] = None self.right = right self._left: Optional[Number] = None self.left = left self._dtype: Type[DTypeNumber] = dtype
def sd_to_XYZ( sd: Union[ArrayLike, SpectralDistribution, MultiSpectralDistributions], cmfs: Optional[MultiSpectralDistributions] = None, illuminant: Optional[SpectralDistribution] = None, k: Optional[Number] = None, method: Union[Literal["ASTM E308", "Integration"], str] = "ASTM E308", **kwargs: Any, ) -> NDArray: """ Convert given spectral distribution to *CIE XYZ* tristimulus values using given colour matching functions, illuminant and method. This placeholder docstring is replaced with the modified :func:`colour.sd_to_XYZ` definition docstring. """ illuminant = cast( SpectralDistribution, optional(illuminant, SDS_ILLUMINANTS[_ILLUMINANT_DEFAULT]), ) return colour.sd_to_XYZ(sd, cmfs, illuminant, k, method, **kwargs)
def test_read(self, sd: Optional[SpectralDistribution] = None): """ Test :meth:`colour.io.tm2714.SpectralDistribution_IESTM2714.read` method. Parameters ---------- sd Optional *IES TM-27-14* spectral distribution for read tests. """ sd = cast( SpectralDistribution_IESTM2714, optional( sd, SpectralDistribution_IESTM2714( os.path.join(RESOURCES_DIRECTORY, "Fluorescent.spdx")).read(), ), ) sd_r = SpectralDistribution(FLUORESCENT_FILE_SPECTRAL_DATA) np.testing.assert_array_equal(sd_r.domain, sd.domain) np.testing.assert_almost_equal(sd_r.values, sd.values, decimal=7) test_read: List[Tuple[Dict, Union[Header_IESTM2714, SpectralDistribution_IESTM2714]]] = [ (FLUORESCENT_FILE_HEADER, sd.header), (FLUORESCENT_FILE_SPECTRAL_DESCRIPTION, sd), ] for test, read in test_read: for key, value in test.items(): for specification in read.mapping.elements: if key == specification.element: self.assertEqual( getattr(read, specification.attribute), value)
def XYZ_to_sd_Jakob2019( XYZ: ArrayLike, cmfs: Optional[MultiSpectralDistributions] = None, illuminant: Optional[SpectralDistribution] = None, optimisation_kwargs: Optional[Dict] = None, additional_data: Boolean = False, ) -> Union[Tuple[SpectralDistribution, Floating], SpectralDistribution]: """ Recover the spectral distribution of given RGB colourspace array using *Jakob and Hanika (2019)* method. Parameters ---------- XYZ *CIE XYZ* tristimulus values to recover the spectral distribution from. cmfs Standard observer colour matching functions, default to the *CIE 1931 2 Degree Standard Observer*. illuminant Illuminant spectral distribution, default to *CIE Standard Illuminant D65*. optimisation_kwargs Parameters for :func:`colour.recovery.find_coefficients_Jakob2019` definition. additional_data If *True*, ``error`` will be returned alongside the recovered spectral distribution. Returns ------- :class:`tuple` or :class:`colour.SpectralDistribution` Tuple of recovered spectral distribution and :math:`\\Delta E_{76}` between the target colour and the colour corresponding to the computed coefficients or recovered spectral distribution. References ---------- :cite:`Jakob2019` Examples -------- >>> from colour import ( ... CCS_ILLUMINANTS, MSDS_CMFS, SDS_ILLUMINANTS, XYZ_to_sRGB) >>> from colour.colorimetry import sd_to_XYZ_integration >>> from colour.utilities import numpy_print_options >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> cmfs = ( ... MSDS_CMFS['CIE 1931 2 Degree Standard Observer']. ... copy().align(SpectralShape(360, 780, 10)) ... ) >>> illuminant = SDS_ILLUMINANTS['D65'].copy().align(cmfs.shape) >>> sd = XYZ_to_sd_Jakob2019(XYZ, cmfs, illuminant) >>> with numpy_print_options(suppress=True): ... sd # doctest: +ELLIPSIS SpectralDistribution([[ 360. , 0.4893773...], [ 370. , 0.3258214...], [ 380. , 0.2147792...], [ 390. , 0.1482413...], [ 400. , 0.1086169...], [ 410. , 0.0841255...], [ 420. , 0.0683114...], [ 430. , 0.0577144...], [ 440. , 0.0504267...], [ 450. , 0.0453552...], [ 460. , 0.0418520...], [ 470. , 0.0395259...], [ 480. , 0.0381430...], [ 490. , 0.0375741...], [ 500. , 0.0377685...], [ 510. , 0.0387432...], [ 520. , 0.0405871...], [ 530. , 0.0434783...], [ 540. , 0.0477225...], [ 550. , 0.0538256...], [ 560. , 0.0626314...], [ 570. , 0.0755869...], [ 580. , 0.0952675...], [ 590. , 0.1264265...], [ 600. , 0.1779272...], [ 610. , 0.2649393...], [ 620. , 0.4039779...], [ 630. , 0.5832105...], [ 640. , 0.7445440...], [ 650. , 0.8499970...], [ 660. , 0.9094792...], [ 670. , 0.9425378...], [ 680. , 0.9616376...], [ 690. , 0.9732481...], [ 700. , 0.9806562...], [ 710. , 0.9855873...], [ 720. , 0.9889903...], [ 730. , 0.9914117...], [ 740. , 0.9931801...], [ 750. , 0.9945009...], [ 760. , 0.9955066...], [ 770. , 0.9962855...], [ 780. , 0.9968976...]], interpolator=SpragueInterpolator, interpolator_kwargs={}, extrapolator=Extrapolator, extrapolator_kwargs={...}) >>> sd_to_XYZ_integration(sd, cmfs, illuminant) / 100 # doctest: +ELLIPSIS array([ 0.2066217..., 0.1220128..., 0.0513958...]) """ XYZ = to_domain_1(XYZ) cmfs, illuminant = handle_spectral_arguments( cmfs, illuminant, shape_default=SPECTRAL_SHAPE_JAKOB2019 ) optimisation_kwargs = optional(optimisation_kwargs, {}) with domain_range_scale("ignore"): coefficients, error = find_coefficients_Jakob2019( XYZ, cmfs, illuminant, **optimisation_kwargs ) sd = sd_Jakob2019(coefficients, cmfs.shape) sd.name = f"{XYZ} (XYZ) - Jakob (2019)" if additional_data: return sd, error else: return sd
def __init__(self, name: Optional[str] = None): self._name: str = f"{self.__class__.__name__} ({id(self)})" self.name = optional(name, self._name)
def mesopic_weighting_function( wavelength: FloatingOrArrayLike, L_p: Floating, source: Union[Literal["Blue Heavy", "Red Heavy"], str] = "Blue Heavy", method: Union[Literal["MOVE", "LRC"], str] = "MOVE", photopic_lef: Optional[SpectralDistribution] = None, scotopic_lef: Optional[SpectralDistribution] = None, ) -> FloatingOrNDArray: """ Calculate the mesopic weighting function factor :math:`V_m` at given wavelength :math:`\\lambda` using the photopic luminance :math:`L_p`. Parameters ---------- wavelength Wavelength :math:`\\lambda` to calculate the mesopic weighting function factor. L_p Photopic luminance :math:`L_p`. source Light source colour temperature. method Method to calculate the weighting factor. photopic_lef :math:`V(\\lambda)` photopic luminous efficiency function, default to the *CIE 1924 Photopic Standard Observer*. scotopic_lef :math:`V^\\prime(\\lambda)` scotopic luminous efficiency function, default to the *CIE 1951 Scotopic Standard Observer*. Returns ------- :class:`numpy.floating` or :class:`numpy.ndarray` Mesopic weighting function factor :math:`V_m`. References ---------- :cite:`Wikipedia2005d` Examples -------- >>> mesopic_weighting_function(500, 0.2) # doctest: +ELLIPSIS 0.7052200... """ photopic_lef = cast( SpectralDistribution, optional( photopic_lef, SDS_LEFS_PHOTOPIC["CIE 1924 Photopic Standard Observer"], ), ) scotopic_lef = cast( SpectralDistribution, optional( scotopic_lef, SDS_LEFS_SCOTOPIC["CIE 1951 Scotopic Standard Observer"], ), ) source = validate_method( source, ["Blue Heavy", "Red Heavy"], '"{0}" light source colour temperature is invalid, ' "it must be one of {1}!", ) method = validate_method(method, ["MOVE", "LRC"]) mesopic_x_luminance_values = sorted(DATA_MESOPIC_X.keys()) index = mesopic_x_luminance_values.index( closest(mesopic_x_luminance_values, L_p)) x = DATA_MESOPIC_X[mesopic_x_luminance_values[index]][source][method] V_m = (1 - x) * scotopic_lef[wavelength] + x * photopic_lef[wavelength] return V_m
def sd_mesopic_luminous_efficiency_function( L_p: Floating, source: Union[Literal["Blue Heavy", "Red Heavy"], str] = "Blue Heavy", method: Union[Literal["MOVE", "LRC"], str] = "MOVE", photopic_lef: Optional[SpectralDistribution] = None, scotopic_lef: Optional[SpectralDistribution] = None, ) -> SpectralDistribution: """ Return the mesopic luminous efficiency function :math:`V_m(\\lambda)` for given photopic luminance :math:`L_p`. Parameters ---------- L_p Photopic luminance :math:`L_p`. source Light source colour temperature. method Method to calculate the weighting factor. photopic_lef :math:`V(\\lambda)` photopic luminous efficiency function, default to the *CIE 1924 Photopic Standard Observer*. scotopic_lef :math:`V^\\prime(\\lambda)` scotopic luminous efficiency function, default to the *CIE 1951 Scotopic Standard Observer*. Returns ------- :class:`colour.SpectralDistribution` Mesopic luminous efficiency function :math:`V_m(\\lambda)`. References ---------- :cite:`Wikipedia2005d` Examples -------- >>> from colour.utilities import numpy_print_options >>> with numpy_print_options(suppress=True): ... sd_mesopic_luminous_efficiency_function(0.2) # doctest: +ELLIPSIS SpectralDistribution([[ 380. , 0.000424 ...], [ 381. , 0.0004781...], [ 382. , 0.0005399...], [ 383. , 0.0006122...], [ 384. , 0.0006961...], [ 385. , 0.0007929...], [ 386. , 0.000907 ...], [ 387. , 0.0010389...], [ 388. , 0.0011923...], [ 389. , 0.0013703...], [ 390. , 0.0015771...], [ 391. , 0.0018167...], [ 392. , 0.0020942...], [ 393. , 0.0024160...], [ 394. , 0.0027888...], [ 395. , 0.0032196...], [ 396. , 0.0037222...], [ 397. , 0.0042957...], [ 398. , 0.0049531...], [ 399. , 0.0057143...], [ 400. , 0.0065784...], [ 401. , 0.0075658...], [ 402. , 0.0086912...], [ 403. , 0.0099638...], [ 404. , 0.0114058...], [ 405. , 0.0130401...], [ 406. , 0.0148750...], [ 407. , 0.0169310...], [ 408. , 0.0192211...], [ 409. , 0.0217511...], [ 410. , 0.0245342...], [ 411. , 0.0275773...], [ 412. , 0.0309172...], [ 413. , 0.0345149...], [ 414. , 0.0383998...], [ 415. , 0.0425744...], [ 416. , 0.0471074...], [ 417. , 0.0519322...], [ 418. , 0.0570541...], [ 419. , 0.0625466...], [ 420. , 0.0683463...], [ 421. , 0.0745255...], [ 422. , 0.0809440...], [ 423. , 0.0877344...], [ 424. , 0.0948915...], [ 425. , 0.1022731...], [ 426. , 0.109877 ...], [ 427. , 0.1178421...], [ 428. , 0.1260316...], [ 429. , 0.1343772...], [ 430. , 0.143017 ...], [ 431. , 0.1518128...], [ 432. , 0.1608328...], [ 433. , 0.1700088...], [ 434. , 0.1792726...], [ 435. , 0.1886934...], [ 436. , 0.1982041...], [ 437. , 0.2078032...], [ 438. , 0.2174184...], [ 439. , 0.2271147...], [ 440. , 0.2368196...], [ 441. , 0.2464623...], [ 442. , 0.2561153...], [ 443. , 0.2657160...], [ 444. , 0.2753387...], [ 445. , 0.2848520...], [ 446. , 0.2944648...], [ 447. , 0.3034902...], [ 448. , 0.3132347...], [ 449. , 0.3223257...], [ 450. , 0.3314513...], [ 451. , 0.3406129...], [ 452. , 0.3498117...], [ 453. , 0.3583617...], [ 454. , 0.3676377...], [ 455. , 0.3762670...], [ 456. , 0.3849392...], [ 457. , 0.3936540...], [ 458. , 0.4024077...], [ 459. , 0.4111965...], [ 460. , 0.4193298...], [ 461. , 0.4281803...], [ 462. , 0.4363804...], [ 463. , 0.4453117...], [ 464. , 0.4542949...], [ 465. , 0.4626509...], [ 466. , 0.4717570...], [ 467. , 0.4809300...], [ 468. , 0.4901776...], [ 469. , 0.4995075...], [ 470. , 0.5096145...], [ 471. , 0.5191293...], [ 472. , 0.5294259...], [ 473. , 0.5391316...], [ 474. , 0.5496217...], [ 475. , 0.5602103...], [ 476. , 0.5702197...], [ 477. , 0.5810207...], [ 478. , 0.5919093...], [ 479. , 0.6028683...], [ 480. , 0.6138806...], [ 481. , 0.6249373...], [ 482. , 0.6360619...], [ 483. , 0.6465989...], [ 484. , 0.6579538...], [ 485. , 0.6687841...], [ 486. , 0.6797939...], [ 487. , 0.6909887...], [ 488. , 0.7023827...], [ 489. , 0.7133032...], [ 490. , 0.7244513...], [ 491. , 0.7358470...], [ 492. , 0.7468118...], [ 493. , 0.7580294...], [ 494. , 0.7694964...], [ 495. , 0.7805225...], [ 496. , 0.7917805...], [ 497. , 0.8026123...], [ 498. , 0.8130793...], [ 499. , 0.8239297...], [ 500. , 0.8352251...], [ 501. , 0.8456342...], [ 502. , 0.8564818...], [ 503. , 0.8676921...], [ 504. , 0.8785021...], [ 505. , 0.8881489...], [ 506. , 0.8986405...], [ 507. , 0.9079322...], [ 508. , 0.9174255...], [ 509. , 0.9257739...], [ 510. , 0.9350656...], [ 511. , 0.9432365...], [ 512. , 0.9509063...], [ 513. , 0.9586931...], [ 514. , 0.9658413...], [ 515. , 0.9722825...], [ 516. , 0.9779924...], [ 517. , 0.9836106...], [ 518. , 0.9883465...], [ 519. , 0.9920964...], [ 520. , 0.9954436...], [ 521. , 0.9976202...], [ 522. , 0.9993457...], [ 523. , 1. ...], [ 524. , 0.9996498...], [ 525. , 0.9990487...], [ 526. , 0.9975356...], [ 527. , 0.9957615...], [ 528. , 0.9930143...], [ 529. , 0.9899559...], [ 530. , 0.9858741...], [ 531. , 0.9814453...], [ 532. , 0.9766885...], [ 533. , 0.9709363...], [ 534. , 0.9648947...], [ 535. , 0.9585832...], [ 536. , 0.952012 ...], [ 537. , 0.9444916...], [ 538. , 0.9367089...], [ 539. , 0.9293506...], [ 540. , 0.9210429...], [ 541. , 0.9124772...], [ 542. , 0.9036604...], [ 543. , 0.8945958...], [ 544. , 0.8845999...], [ 545. , 0.8750500...], [ 546. , 0.8659457...], [ 547. , 0.8559224...], [ 548. , 0.8456846...], [ 549. , 0.8352499...], [ 550. , 0.8253229...], [ 551. , 0.8152079...], [ 552. , 0.8042205...], [ 553. , 0.7944209...], [ 554. , 0.7837466...], [ 555. , 0.7735680...], [ 556. , 0.7627808...], [ 557. , 0.7522710...], [ 558. , 0.7417549...], [ 559. , 0.7312909...], [ 560. , 0.7207983...], [ 561. , 0.7101939...], [ 562. , 0.6996362...], [ 563. , 0.6890656...], [ 564. , 0.6785599...], [ 565. , 0.6680593...], [ 566. , 0.6575697...], [ 567. , 0.6471578...], [ 568. , 0.6368208...], [ 569. , 0.6264871...], [ 570. , 0.6161541...], [ 571. , 0.6058896...], [ 572. , 0.5957000...], [ 573. , 0.5855937...], [ 574. , 0.5754412...], [ 575. , 0.5653883...], [ 576. , 0.5553742...], [ 577. , 0.5454680...], [ 578. , 0.5355972...], [ 579. , 0.5258267...], [ 580. , 0.5160152...], [ 581. , 0.5062322...], [ 582. , 0.4965595...], [ 583. , 0.4868746...], [ 584. , 0.4773299...], [ 585. , 0.4678028...], [ 586. , 0.4583704...], [ 587. , 0.4489722...], [ 588. , 0.4397606...], [ 589. , 0.4306131...], [ 590. , 0.4215446...], [ 591. , 0.4125681...], [ 592. , 0.4037550...], [ 593. , 0.3950359...], [ 594. , 0.3864104...], [ 595. , 0.3778777...], [ 596. , 0.3694405...], [ 597. , 0.3611074...], [ 598. , 0.3528596...], [ 599. , 0.3447056...], [ 600. , 0.3366470...], [ 601. , 0.3286917...], [ 602. , 0.3208410...], [ 603. , 0.3130808...], [ 604. , 0.3054105...], [ 605. , 0.2978225...], [ 606. , 0.2903027...], [ 607. , 0.2828727...], [ 608. , 0.2755311...], [ 609. , 0.2682900...], [ 610. , 0.2611478...], [ 611. , 0.2541176...], [ 612. , 0.2471885...], [ 613. , 0.2403570...], [ 614. , 0.2336057...], [ 615. , 0.2269379...], [ 616. , 0.2203527...], [ 617. , 0.2138465...], [ 618. , 0.2073946...], [ 619. , 0.2009789...], [ 620. , 0.1945818...], [ 621. , 0.1881943...], [ 622. , 0.1818226...], [ 623. , 0.1754987...], [ 624. , 0.1692476...], [ 625. , 0.1630876...], [ 626. , 0.1570257...], [ 627. , 0.151071 ...], [ 628. , 0.1452469...], [ 629. , 0.1395845...], [ 630. , 0.1341087...], [ 631. , 0.1288408...], [ 632. , 0.1237666...], [ 633. , 0.1188631...], [ 634. , 0.1141075...], [ 635. , 0.1094766...], [ 636. , 0.1049613...], [ 637. , 0.1005679...], [ 638. , 0.0962924...], [ 639. , 0.0921296...], [ 640. , 0.0880778...], [ 641. , 0.0841306...], [ 642. , 0.0802887...], [ 643. , 0.0765559...], [ 644. , 0.0729367...], [ 645. , 0.0694345...], [ 646. , 0.0660491...], [ 647. , 0.0627792...], [ 648. , 0.0596278...], [ 649. , 0.0565970...], [ 650. , 0.0536896...], [ 651. , 0.0509068...], [ 652. , 0.0482444...], [ 653. , 0.0456951...], [ 654. , 0.0432510...], [ 655. , 0.0409052...], [ 656. , 0.0386537...], [ 657. , 0.0364955...], [ 658. , 0.0344285...], [ 659. , 0.0324501...], [ 660. , 0.0305579...], [ 661. , 0.0287496...], [ 662. , 0.0270233...], [ 663. , 0.0253776...], [ 664. , 0.0238113...], [ 665. , 0.0223226...], [ 666. , 0.0209086...], [ 667. , 0.0195688...], [ 668. , 0.0183056...], [ 669. , 0.0171216...], [ 670. , 0.0160192...], [ 671. , 0.0149986...], [ 672. , 0.0140537...], [ 673. , 0.0131784...], [ 674. , 0.0123662...], [ 675. , 0.0116107...], [ 676. , 0.0109098...], [ 677. , 0.0102587...], [ 678. , 0.0096476...], [ 679. , 0.0090665...], [ 680. , 0.0085053...], [ 681. , 0.0079567...], [ 682. , 0.0074229...], [ 683. , 0.0069094...], [ 684. , 0.0064213...], [ 685. , 0.0059637...], [ 686. , 0.0055377...], [ 687. , 0.0051402...], [ 688. , 0.00477 ...], [ 689. , 0.0044263...], [ 690. , 0.0041081...], [ 691. , 0.0038149...], [ 692. , 0.0035456...], [ 693. , 0.0032984...], [ 694. , 0.0030718...], [ 695. , 0.0028639...], [ 696. , 0.0026738...], [ 697. , 0.0025000...], [ 698. , 0.0023401...], [ 699. , 0.0021918...], [ 700. , 0.0020526...], [ 701. , 0.0019207...], [ 702. , 0.001796 ...], [ 703. , 0.0016784...], [ 704. , 0.0015683...], [ 705. , 0.0014657...], [ 706. , 0.0013702...], [ 707. , 0.001281 ...], [ 708. , 0.0011976...], [ 709. , 0.0011195...], [ 710. , 0.0010464...], [ 711. , 0.0009776...], [ 712. , 0.0009131...], [ 713. , 0.0008525...], [ 714. , 0.0007958...], [ 715. , 0.0007427...], [ 716. , 0.0006929...], [ 717. , 0.0006462...], [ 718. , 0.0006026...], [ 719. , 0.0005619...], [ 720. , 0.0005240...], [ 721. , 0.0004888...], [ 722. , 0.0004561...], [ 723. , 0.0004255...], [ 724. , 0.0003971...], [ 725. , 0.0003704...], [ 726. , 0.0003455...], [ 727. , 0.0003221...], [ 728. , 0.0003001...], [ 729. , 0.0002796...], [ 730. , 0.0002604...], [ 731. , 0.0002423...], [ 732. , 0.0002254...], [ 733. , 0.0002095...], [ 734. , 0.0001947...], [ 735. , 0.0001809...], [ 736. , 0.0001680...], [ 737. , 0.0001560...], [ 738. , 0.0001449...], [ 739. , 0.0001345...], [ 740. , 0.0001249...], [ 741. , 0.0001159...], [ 742. , 0.0001076...], [ 743. , 0.0000999...], [ 744. , 0.0000927...], [ 745. , 0.0000862...], [ 746. , 0.0000801...], [ 747. , 0.0000745...], [ 748. , 0.0000693...], [ 749. , 0.0000646...], [ 750. , 0.0000602...], [ 751. , 0.0000561...], [ 752. , 0.0000523...], [ 753. , 0.0000488...], [ 754. , 0.0000456...], [ 755. , 0.0000425...], [ 756. , 0.0000397...], [ 757. , 0.0000370...], [ 758. , 0.0000346...], [ 759. , 0.0000322...], [ 760. , 0.0000301...], [ 761. , 0.0000281...], [ 762. , 0.0000262...], [ 763. , 0.0000244...], [ 764. , 0.0000228...], [ 765. , 0.0000213...], [ 766. , 0.0000198...], [ 767. , 0.0000185...], [ 768. , 0.0000173...], [ 769. , 0.0000161...], [ 770. , 0.0000150...], [ 771. , 0.0000140...], [ 772. , 0.0000131...], [ 773. , 0.0000122...], [ 774. , 0.0000114...], [ 775. , 0.0000106...], [ 776. , 0.0000099...], [ 777. , 0.0000092...], [ 778. , 0.0000086...], [ 779. , 0.0000080...], [ 780. , 0.0000075...]], interpolator=SpragueInterpolator, interpolator_kwargs={}, extrapolator=Extrapolator, extrapolator_kwargs={...}) """ photopic_lef = cast( SpectralDistribution, optional( photopic_lef, SDS_LEFS_PHOTOPIC["CIE 1924 Photopic Standard Observer"], ), ) scotopic_lef = cast( SpectralDistribution, optional( scotopic_lef, SDS_LEFS_SCOTOPIC["CIE 1951 Scotopic Standard Observer"], ), ) shape = SpectralShape( max([photopic_lef.shape.start, scotopic_lef.shape.start]), min([photopic_lef.shape.end, scotopic_lef.shape.end]), max([photopic_lef.shape.interval, scotopic_lef.shape.interval]), ) wavelengths = shape.range() sd = SpectralDistribution( mesopic_weighting_function(wavelengths, L_p, source, method, photopic_lef, scotopic_lef), wavelengths, name=f"{L_p} Lp Mesopic Luminous Efficiency Function", ) return sd.normalise()
def label_rectangles( labels: Sequence[str], rectangles: Sequence[Patch], rotation: Union[Literal["horizontal", "vertical"], str] = "vertical", text_size: Floating = 10, offset: Optional[ArrayLike] = None, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Add labels above given rectangles. Parameters ---------- labels Labels to display. rectangles Rectangles to used to set the labels value and position. rotation Labels orientation. text_size Labels text size. offset Labels offset as percentages of the largest rectangle dimensions. Other Parameters ---------------- figure Figure to apply the render elements onto. axes Axes to apply the render elements onto. Returns ------- :class:`tuple` Current figure and axes. """ rotation = validate_method( rotation, ["horizontal", "vertical"], '"{0}" rotation is invalid, it must be one of {1}!', ) figure = kwargs.get("figure", plt.gcf()) axes = kwargs.get("axes", plt.gca()) offset = as_float_array(cast(ArrayLike, optional(offset, (0.0, 0.025)))) x_m, y_m = 0, 0 for rectangle in rectangles: x_m = max(x_m, rectangle.get_width()) y_m = max(y_m, rectangle.get_height()) for i, rectangle in enumerate(rectangles): x = rectangle.get_x() height = rectangle.get_height() width = rectangle.get_width() ha = "center" va = "bottom" axes.text( x + width / 2 + offset[0] * width, height + offset[1] * y_m, labels[i], ha=ha, va=va, rotation=rotation, fontsize=text_size, clip_on=True, zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_label, ) return figure, axes
def plot_planckian_locus( planckian_locus_colours: Optional[Union[ArrayLike, str]] = None, planckian_locus_opacity: Floating = 1, planckian_locus_labels: Optional[Sequence] = None, method: Union[Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"], str] = "CIE 1931", **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the *Planckian Locus* according to given method. Parameters ---------- planckian_locus_colours Colours of the *Planckian Locus*, if ``planckian_locus_colours`` is set to *RGB*, the colours will be computed according to the corresponding chromaticity coordinates. planckian_locus_opacity Opacity of the *Planckian Locus*. planckian_locus_labels Array of labels used to customise which iso-temperature lines will be drawn along the *Planckian Locus*. Passing an empty array will result in no iso-temperature lines being drawn. method *Chromaticity Diagram* method. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> plot_planckian_locus(planckian_locus_colours='RGB') ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Planckian_Locus.png :align: center :alt: plot_planckian_locus """ method = validate_method(method, ["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"]) planckian_locus_colours = optional(planckian_locus_colours, CONSTANTS_COLOUR_STYLE.colour.dark) labels = cast( Tuple, optional( planckian_locus_labels, (10**6 / 600, 2000, 2500, 3000, 4000, 6000, 10**6 / 100), ), ) D_uv = 0.05 settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) if method == "cie 1931": def uv_to_ij(uv: NDArray) -> NDArray: """ Convert given *uv* chromaticity coordinates to *ij* chromaticity coordinates. """ return UCS_uv_to_xy(uv) elif method == "cie 1960 ucs": def uv_to_ij(uv: NDArray) -> NDArray: """ Convert given *uv* chromaticity coordinates to *ij* chromaticity coordinates. """ return uv elif method == "cie 1976 ucs": def uv_to_ij(uv: NDArray) -> NDArray: """ Convert given *uv* chromaticity coordinates to *ij* chromaticity coordinates. """ return xy_to_Luv_uv(UCS_uv_to_xy(uv)) def CCT_D_uv_to_plotting_colourspace(CCT_D_uv): """ Convert given *uv* chromaticity coordinates to the default plotting colourspace. """ return normalise_maximum( XYZ_to_plotting_colourspace( xy_to_XYZ(UCS_uv_to_xy(CCT_to_uv(CCT_D_uv, "Robertson 1968")))), axis=-1, ) start, end = 10**6 / 600, 10**6 / 10 CCT = np.arange(start, end + 100, 100) CCT_D_uv = np.reshape(tstack([CCT, zeros(CCT.shape)]), (-1, 1, 2)) ij = uv_to_ij(CCT_to_uv(CCT_D_uv, "Robertson 1968")) use_RGB_planckian_locus_colours = ( str(planckian_locus_colours).upper() == "RGB") if use_RGB_planckian_locus_colours: pl_colours = CCT_D_uv_to_plotting_colourspace(CCT_D_uv) else: pl_colours = planckian_locus_colours line_collection = LineCollection( np.concatenate([ij[:-1], ij[1:]], axis=1), colors=pl_colours, alpha=planckian_locus_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_line, ) axes.add_collection(line_collection) for label in labels: CCT_D_uv = np.reshape( tstack([full(10, label), np.linspace(-D_uv, D_uv, 10)]), (-1, 1, 2)) if use_RGB_planckian_locus_colours: itl_colours = CCT_D_uv_to_plotting_colourspace(CCT_D_uv) else: itl_colours = planckian_locus_colours ij = uv_to_ij(CCT_to_uv(CCT_D_uv, "Robertson 1968")) line_collection = LineCollection( np.concatenate([ij[:-1], ij[1:]], axis=1), colors=itl_colours, alpha=planckian_locus_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_line, ) axes.add_collection(line_collection) axes.annotate( f"{as_int_scalar(label)}K", xy=(ij[-1, :, 0], ij[-1, :, 1]), xytext=(0, CONSTANTS_COLOUR_STYLE.geometry.long / 2), textcoords="offset points", size="x-small", zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_label, ) settings = {"axes": axes} settings.update(kwargs) return render(**settings)
def plot_RGB_colourspaces_gamuts( colourspaces: Union[RGB_Colourspace, str, Sequence[Union[RGB_Colourspace, str]]], reference_colourspace: Union[Literal["CAM02LCD", "CAM02SCD", "CAM02UCS", "CAM16LCD", "CAM16SCD", "CAM16UCS", "CIE XYZ", "CIE xyY", "CIE Lab", "CIE Luv", "CIE UCS", "CIE UVW", "DIN99", "Hunter Lab", "Hunter Rdab", "ICaCb", "ICtCp", "IPT", "IgPgTg", "Jzazbz", "OSA UCS", "Oklab", "hdr-CIELAB", "hdr-IPT", ], str, ] = "CIE xyY", segments: Integer = 8, show_grid: Boolean = True, grid_segments: Integer = 10, show_spectral_locus: Boolean = False, spectral_locus_colour: Optional[Union[ArrayLike, str]] = None, cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", chromatically_adapt: Boolean = False, convert_kwargs: Optional[Dict] = None, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot given *RGB* colourspaces gamuts in given reference colourspace. Parameters ---------- colourspaces *RGB* colourspaces to plot the gamuts. ``colourspaces`` elements can be of any type or form supported by the :func:`colour.plotting.filter_RGB_colourspaces` definition. reference_colourspace Reference colourspace model to plot the gamuts into, see :attr:`colour.COLOURSPACE_MODELS` attribute for the list of supported colourspace models. segments Edge segments count for each *RGB* colourspace cubes. show_grid Whether to show a grid at the bottom of the *RGB* colourspace cubes. grid_segments Edge segments count for the grid. show_spectral_locus Whether to show the spectral locus. spectral_locus_colour Spectral locus colour. cmfs Standard observer colour matching functions used for computing the spectral locus boundaries. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. chromatically_adapt Whether to chromatically adapt the *RGB* colourspaces given in ``colourspaces`` to the whitepoint of the default plotting colourspace. convert_kwargs Keyword arguments for the :func:`colour.convert` definition. Other Parameters ---------------- edge_colours Edge colours array such as `edge_colours = (None, (0.5, 0.5, 1.0))`. edge_alpha Edge opacity value such as `edge_alpha = (0.0, 1.0)`. face_alpha Face opacity value such as `face_alpha = (0.5, 1.0)`. face_colours Face colours array such as `face_colours = (None, (0.5, 0.5, 1.0))`. kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.volume.nadir_grid`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> plot_RGB_colourspaces_gamuts(['ITU-R BT.709', 'ACEScg', 'S-Gamut']) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...Axes3DSubplot...>) .. image:: ../_static/Plotting_Plot_RGB_Colourspaces_Gamuts.png :align: center :alt: plot_RGB_colourspaces_gamuts """ colourspaces = cast( List[RGB_Colourspace], list(filter_RGB_colourspaces(colourspaces).values()), ) convert_kwargs = optional(convert_kwargs, {}) count_c = len(colourspaces) title = ( f"{', '.join([colourspace.name for colourspace in colourspaces])} " f"- {reference_colourspace} Reference Colourspace") illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint convert_settings = {"illuminant": illuminant} convert_settings.update(convert_kwargs) settings = Structure( **{ "face_colours": [None] * count_c, "edge_colours": [None] * count_c, "face_alpha": [1] * count_c, "edge_alpha": [1] * count_c, "title": title, }) settings.update(kwargs) figure = plt.figure() axes = figure.add_subplot(111, projection="3d") points = zeros((4, 3)) if show_spectral_locus: cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) XYZ = cmfs.values points = colourspace_model_axis_reorder( convert(XYZ, "CIE XYZ", reference_colourspace, **convert_settings), reference_colourspace, ) points[np.isnan(points)] = 0 c = ((0.0, 0.0, 0.0, 0.5) if spectral_locus_colour is None else spectral_locus_colour) axes.plot( points[..., 0], points[..., 1], points[..., 2], color=c, zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line, ) axes.plot( (points[-1][0], points[0][0]), (points[-1][1], points[0][1]), (points[-1][2], points[0][2]), color=c, zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line, ) plotting_colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace quads_c: List = [] RGB_cf: List = [] RGB_ce: List = [] for i, colourspace in enumerate(colourspaces): if chromatically_adapt and not np.array_equal( colourspace.whitepoint, plotting_colourspace.whitepoint): colourspace = colourspace.chromatically_adapt( plotting_colourspace.whitepoint, plotting_colourspace.whitepoint_name, ) quads_cb, RGB = RGB_identity_cube( width_segments=segments, height_segments=segments, depth_segments=segments, ) XYZ = RGB_to_XYZ( quads_cb, colourspace.whitepoint, colourspace.whitepoint, colourspace.matrix_RGB_to_XYZ, ) convert_settings = {"illuminant": colourspace.whitepoint} convert_settings.update(convert_kwargs) quads_c.extend( colourspace_model_axis_reorder( convert(XYZ, "CIE XYZ", reference_colourspace, **convert_settings), reference_colourspace, )) if settings.face_colours[i] is not None: RGB = ones(RGB.shape) * settings.face_colours[i] RGB_cf.extend( np.hstack([RGB, full((RGB.shape[0], 1), settings.face_alpha[i])])) if settings.edge_colours[i] is not None: RGB = ones(RGB.shape) * settings.edge_colours[i] RGB_ce.extend( np.hstack([RGB, full((RGB.shape[0], 1), settings.edge_alpha[i])])) quads = as_float_array(quads_c) RGB_f = as_float_array(RGB_cf) RGB_e = as_float_array(RGB_ce) quads[np.isnan(quads)] = 0 if quads.size != 0: for i, axis in enumerate("xyz"): min_a = np.minimum(np.min(quads[..., i]), np.min(points[..., i])) max_a = np.maximum(np.max(quads[..., i]), np.max(points[..., i])) getattr(axes, f"set_{axis}lim")((min_a, max_a)) labels = np.array( COLOURSPACE_MODELS_AXIS_LABELS[reference_colourspace])[as_int_array( colourspace_model_axis_reorder([0, 1, 2], reference_colourspace))] for i, axis in enumerate("xyz"): getattr(axes, f"set_{axis}label")(labels[i]) if show_grid: limits = np.array([[-1.5, 1.5], [-1.5, 1.5]]) quads_g, RGB_gf, RGB_ge = nadir_grid(limits, grid_segments, labels, axes, **settings) quads = np.vstack([quads_g, quads]) RGB_f = np.vstack([RGB_gf, RGB_f]) RGB_e = np.vstack([RGB_ge, RGB_e]) collection = Poly3DCollection(quads) collection.set_facecolors(RGB_f) collection.set_edgecolors(RGB_e) axes.add_collection3d(collection) settings.update({ "axes": axes, "axes_visible": False, "camera_aspect": "equal" }) settings.update(kwargs) return render(**settings)
def Izazbz_to_XYZ( Izazbz: ArrayLike, constants: Optional[Structure] = None, method: Union[ Literal["Safdar 2017", "Safdar 2021", "ZCAM"], str ] = "Safdar 2017", ) -> NDArray: """ Convert from :math:`I_za_zb_z` colourspace to *CIE XYZ* tristimulus values. Parameters ---------- Izazbz :math:`I_za_zb_z` colourspace array where :math:`I_z` is the achromatic response, :math:`a_z` is redness-greenness and :math:`b_z` is yellowness-blueness. constants :math:`J_za_zb_z` colourspace constants. method Computation methods, *Safdar 2021* and *ZCAM* methods are equivalent. Returns ------- :class:`numpy.ndarray` *CIE XYZ* tristimulus values under *CIE Standard Illuminant D Series D65*. Warnings -------- The underlying *SMPTE ST 2084:2014* transfer function is an absolute transfer function. Notes ----- - The underlying *SMPTE ST 2084:2014* transfer function is an absolute transfer function, thus the domain and range values for the *Reference* and *1* scales are only indicative that the data is not affected by scale transformations. +------------+-----------------------+------------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+==================+ | ``Izazbz`` | ``Iz`` : [0, 1] | ``Iz`` : [0, 1] | | | | | | | ``az`` : [-1, 1] | ``az`` : [-1, 1] | | | | | | | ``bz`` : [-1, 1] | ``bz`` : [-1, 1] | +------------+-----------------------+------------------+ +------------+-----------------------+------------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+==================+ | ``XYZ`` | ``UN`` | ``UN`` | +------------+-----------------------+------------------+ References ---------- :cite:`Safdar2017`, :cite:`Safdar2021` Examples -------- >>> Izazbz = np.array([0.01207793, 0.00924302, 0.00526007]) >>> Izazbz_to_XYZ(Izazbz) # doctest: +ELLIPSIS array([ 0.2065401..., 0.1219723..., 0.0513696...]) """ Izazbz = as_float_array(Izazbz) method = validate_method(method, IZAZBZ_METHODS) constants = optional( constants, CONSTANTS_JZAZBZ_SAFDAR2017 if method == "safdar 2017" else CONSTANTS_JZAZBZ_SAFDAR2021, ) if method == "safdar 2017": LMS_p = vector_dot(MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2017, Izazbz) else: Izazbz[..., 0] += constants.d_0 LMS_p = vector_dot(MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2021, Izazbz) with domain_range_scale("ignore"): LMS = eotf_ST2084(LMS_p, 10000, constants) X_p_D65, Y_p_D65, Z_p_D65 = tsplit( vector_dot(MATRIX_JZAZBZ_LMS_TO_XYZ, LMS) ) X_D65 = (X_p_D65 + (constants.b - 1) * Z_p_D65) / constants.b Y_D65 = (Y_p_D65 + (constants.g - 1) * X_D65) / constants.g XYZ_D65 = tstack([X_D65, Y_D65, Z_p_D65]) return XYZ_D65
def plot_multi_colour_swatches( colour_swatches: Sequence[Union[ArrayLike, ColourSwatch]], width: Floating = 1, height: Floating = 1, spacing: Floating = 0, columns: Optional[Integer] = None, direction: Union[Literal["+y", "-y"], str] = "+y", text_kwargs: Optional[Dict] = None, background_colour: ArrayLike = (1.0, 1.0, 1.0), compare_swatches: Optional[Union[Literal["Diagonal", "Stacked"], str]] = None, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot given colours swatches. Parameters ---------- colour_swatches Colour swatch sequence, either a regular `ArrayLike` or a sequence of :class:`colour.plotting.ColourSwatch` class instances. width Colour swatch width. height Colour swatch height. spacing Colour swatches spacing. columns Colour swatches columns count, defaults to the colour swatch count or half of it if comparing. direction Row stacking direction. text_kwargs Keyword arguments for the :func:`matplotlib.pyplot.text` definition. The following special keywords can also be used: - ``offset``: Sets the text offset. - ``visible``: Sets the text visibility. background_colour Background colour. compare_swatches Whether to compare the swatches, in which case the colour swatch count must be an even number with alternating reference colour swatches and test colour swatches. *Stacked* will draw the test colour swatch in the center of the reference colour swatch, *Diagonal* will draw the reference colour swatch in the upper left diagonal area and the test colour swatch in the bottom right diagonal area. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> RGB_1 = ColourSwatch((0.45293517, 0.31732158, 0.26414773)) >>> RGB_2 = ColourSwatch((0.77875824, 0.57726450, 0.50453169)) >>> plot_multi_colour_swatches([RGB_1, RGB_2]) # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Multi_Colour_Swatches.png :align: center :alt: plot_multi_colour_swatches """ direction = validate_method( direction, ["+y", "-y"], '"{0}" direction is invalid, it must be one of {1}!', ) if compare_swatches is not None: compare_swatches = validate_method( compare_swatches, ["Diagonal", "Stacked"], '"{0}" compare swatches method is invalid, it must be one of {1}!', ) _figure, axes = artist(**kwargs) # Handling case where `colour_swatches` is a regular *ArrayLike*. colour_swatches = list(colour_swatches) colour_swatches_converted = [] if not isinstance(first_item(colour_swatches), ColourSwatch): for i, colour_swatch in enumerate( as_float_array(cast(ArrayLike, colour_swatches)).reshape([-1, 3])): colour_swatches_converted.append(ColourSwatch(colour_swatch)) else: colour_swatches_converted = cast(List[ColourSwatch], colour_swatches) colour_swatches = colour_swatches_converted if compare_swatches is not None: attest( len(colour_swatches) % 2 == 0, "Cannot compare an odd number of colour swatches!", ) colour_swatches_reference = colour_swatches[0::2] colour_swatches_test = colour_swatches[1::2] else: colour_swatches_reference = colour_swatches_test = colour_swatches columns = optional(columns, len(colour_swatches_reference)) text_settings = { "offset": 0.05, "visible": True, "zorder": CONSTANTS_COLOUR_STYLE.zorder.midground_label, } if text_kwargs is not None: text_settings.update(text_kwargs) text_offset = text_settings.pop("offset") offset_X: Floating = 0 offset_Y: Floating = 0 x_min, x_max, y_min, y_max = 0, width, 0, height y = 1 if direction == "+y" else -1 for i, colour_swatch in enumerate(colour_swatches_reference): if i % columns == 0 and i != 0: offset_X = 0 offset_Y += (height + spacing) * y x_0, x_1 = offset_X, offset_X + width y_0, y_1 = offset_Y, offset_Y + height * y axes.fill( (x_0, x_1, x_1, x_0), (y_0, y_0, y_1, y_1), color=np.clip(colour_swatches_reference[i].RGB, 0, 1), zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_polygon, ) if compare_swatches == "stacked": margin_X = width * 0.25 margin_Y = height * 0.25 axes.fill( ( x_0 + margin_X, x_1 - margin_X, x_1 - margin_X, x_0 + margin_X, ), ( y_0 + margin_Y * y, y_0 + margin_Y * y, y_1 - margin_Y * y, y_1 - margin_Y * y, ), color=np.clip(colour_swatches_test[i].RGB, 0, 1), zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_polygon, ) else: axes.fill( (x_0, x_1, x_1), (y_0, y_0, y_1), color=np.clip(colour_swatches_test[i].RGB, 0, 1), zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_polygon, ) if colour_swatch.name is not None and text_settings["visible"]: axes.text( x_0 + text_offset, y_0 + text_offset * y, colour_swatch.name, verticalalignment="bottom" if y == 1 else "top", clip_on=True, **text_settings, ) offset_X += width + spacing x_max = min(len(colour_swatches), int(columns)) x_max = x_max * width + x_max * spacing - spacing y_max = offset_Y axes.patch.set_facecolor(background_colour) if y == 1: bounding_box = [ x_min - spacing, x_max + spacing, y_min - spacing, y_max + spacing + height, ] else: bounding_box = [ x_min - spacing, x_max + spacing, y_max - spacing - height, y_min + spacing, ] settings: Dict[str, Any] = { "axes": axes, "bounding_box": bounding_box, "aspect": "equal", } settings.update(kwargs) return render(**settings)
def plot_chromaticity_diagram_colours( samples: Integer = 256, diagram_colours: Optional[Union[ArrayLike, str]] = None, diagram_opacity: Floating = 1, diagram_clipping_path: Optional[ArrayLike] = None, cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", method: Union[Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"], str] = "CIE 1931", **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the *Chromaticity Diagram* colours according to given method. Parameters ---------- samples Samples count on one axis when computing the *Chromaticity Diagram* colours. diagram_colours Colours of the *Chromaticity Diagram*, if ``diagram_colours`` is set to *RGB*, the colours will be computed according to the corresponding coordinates. diagram_opacity Opacity of the *Chromaticity Diagram*. diagram_clipping_path Path of points used to clip the *Chromaticity Diagram* colours. cmfs Standard observer colour matching functions used for computing the spectral locus boundaries. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. method *Chromaticity Diagram* method. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> plot_chromaticity_diagram_colours(diagram_colours='RGB') ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Chromaticity_Diagram_Colours.png :align: center :alt: plot_chromaticity_diagram_colours """ method = validate_method(method, ["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"]) settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) diagram_colours = cast( ArrayLike, optional(diagram_colours, HEX_to_RGB(CONSTANTS_COLOUR_STYLE.colour.average)), ) cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint if method == "cie 1931": spectral_locus = XYZ_to_xy(cmfs.values, illuminant) elif method == "cie 1960 ucs": spectral_locus = UCS_to_uv(XYZ_to_UCS(cmfs.values)) elif method == "cie 1976 ucs": spectral_locus = Luv_to_uv(XYZ_to_Luv(cmfs.values, illuminant), illuminant) use_RGB_diagram_colours = str(diagram_colours).upper() == "RGB" if use_RGB_diagram_colours: ii, jj = np.meshgrid(np.linspace(0, 1, samples), np.linspace(1, 0, samples)) ij = tstack([ii, jj]) # NOTE: Various values in the grid have potential to generate # zero-divisions, they could be avoided by perturbing the grid, e.g. # adding a small epsilon. It was decided instead to disable warnings. with suppress_warnings(python_warnings=True): if method == "cie 1931": XYZ = xy_to_XYZ(ij) elif method == "cie 1960 ucs": XYZ = xy_to_XYZ(UCS_uv_to_xy(ij)) elif method == "cie 1976 ucs": XYZ = xy_to_XYZ(Luv_uv_to_xy(ij)) diagram_colours = normalise_maximum(XYZ_to_plotting_colourspace( XYZ, illuminant), axis=-1) polygon = Polygon( spectral_locus if diagram_clipping_path is None else diagram_clipping_path, facecolor="none" if use_RGB_diagram_colours else np.hstack( [diagram_colours, diagram_opacity]), edgecolor="none" if use_RGB_diagram_colours else np.hstack( [diagram_colours, diagram_opacity]), zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, ) axes.add_patch(polygon) if use_RGB_diagram_colours: # Preventing bounding box related issues as per # https://github.com/matplotlib/matplotlib/issues/10529 image = axes.imshow( diagram_colours, interpolation="bilinear", extent=(0, 1, 0, 1), clip_path=None, alpha=diagram_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, ) image.set_clip_path(polygon) settings = {"axes": axes} settings.update(kwargs) return render(**kwargs)
def plot_spectral_locus( cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", spectral_locus_colours: Optional[Union[ArrayLike, str]] = None, spectral_locus_opacity: Floating = 1, spectral_locus_labels: Optional[Sequence] = None, method: Union[Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"], str] = "CIE 1931", **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the *Spectral Locus* according to given method. Parameters ---------- cmfs Standard observer colour matching functions used for computing the spectral locus boundaries. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. spectral_locus_colours Colours of the *Spectral Locus*, if ``spectral_locus_colours`` is set to *RGB*, the colours will be computed according to the corresponding chromaticity coordinates. spectral_locus_opacity Opacity of the *Spectral Locus*. spectral_locus_labels Array of wavelength labels used to customise which labels will be drawn around the spectral locus. Passing an empty array will result in no wavelength labels being drawn. method *Chromaticity Diagram* method. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> plot_spectral_locus(spectral_locus_colours='RGB') # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Spectral_Locus.png :align: center :alt: plot_spectral_locus """ method = validate_method(method, ["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"]) spectral_locus_colours = optional(spectral_locus_colours, CONSTANTS_COLOUR_STYLE.colour.dark) settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint wavelengths = list(cmfs.wavelengths) equal_energy = np.array([1 / 3] * 2) if method == "cie 1931": ij = XYZ_to_xy(cmfs.values, illuminant) labels = cast( Tuple, optional( spectral_locus_labels, ( 390, 460, 470, 480, 490, 500, 510, 520, 540, 560, 580, 600, 620, 700, ), ), ) elif method == "cie 1960 ucs": ij = UCS_to_uv(XYZ_to_UCS(cmfs.values)) labels = cast( Tuple, optional( spectral_locus_labels, ( 420, 440, 450, 460, 470, 480, 490, 500, 510, 520, 530, 540, 550, 560, 570, 580, 590, 600, 610, 620, 630, 645, 680, ), ), ) elif method == "cie 1976 ucs": ij = Luv_to_uv(XYZ_to_Luv(cmfs.values, illuminant), illuminant) labels = cast( Tuple, optional( spectral_locus_labels, ( 420, 440, 450, 460, 470, 480, 490, 500, 510, 520, 530, 540, 550, 560, 570, 580, 590, 600, 610, 620, 630, 645, 680, ), ), ) pl_ij = np.reshape( tstack([ np.linspace(ij[0][0], ij[-1][0], 20), np.linspace(ij[0][1], ij[-1][1], 20), ]), (-1, 1, 2), ) sl_ij = np.copy(ij).reshape(-1, 1, 2) purple_line_colours: Optional[Union[ArrayLike, str]] if str(spectral_locus_colours).upper() == "RGB": spectral_locus_colours = normalise_maximum(XYZ_to_plotting_colourspace( cmfs.values), axis=-1) if method == "cie 1931": XYZ = xy_to_XYZ(pl_ij) elif method == "cie 1960 ucs": XYZ = xy_to_XYZ(UCS_uv_to_xy(pl_ij)) elif method == "cie 1976 ucs": XYZ = xy_to_XYZ(Luv_uv_to_xy(pl_ij)) purple_line_colours = normalise_maximum(XYZ_to_plotting_colourspace( np.reshape(XYZ, (-1, 3))), axis=-1) else: purple_line_colours = spectral_locus_colours for slp_ij, slp_colours in ( (pl_ij, purple_line_colours), (sl_ij, spectral_locus_colours), ): line_collection = LineCollection( np.concatenate([slp_ij[:-1], slp_ij[1:]], axis=1), colors=slp_colours, alpha=spectral_locus_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_scatter, ) axes.add_collection(line_collection) wl_ij = dict(zip(wavelengths, ij)) for label in labels: ij_l = wl_ij.get(label) if ij_l is None: continue ij_l = as_float_array([ij_l]) i, j = tsplit(ij_l) index = bisect.bisect(wavelengths, label) left = wavelengths[index - 1] if index >= 0 else wavelengths[index] right = (wavelengths[index] if index < len(wavelengths) else wavelengths[-1]) dx = wl_ij[right][0] - wl_ij[left][0] dy = wl_ij[right][1] - wl_ij[left][1] direction = np.array([-dy, dx]) normal = (np.array([-dy, dx]) if np.dot( normalise_vector(ij_l - equal_energy), normalise_vector(direction), ) > 0 else np.array([dy, -dx])) normal = normalise_vector(normal) / 30 label_colour = ( spectral_locus_colours if is_string(spectral_locus_colours) else spectral_locus_colours[index] # type: ignore[index] ) axes.plot( (i, i + normal[0] * 0.75), (j, j + normal[1] * 0.75), color=label_colour, alpha=spectral_locus_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_line, ) axes.plot( i, j, "o", color=label_colour, alpha=spectral_locus_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_line, ) axes.text( i + normal[0], j + normal[1], label, clip_on=True, ha="left" if normal[0] >= 0 else "right", va="center", fontdict={"size": "small"}, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_label, ) settings = {"axes": axes} settings.update(kwargs) return render(**kwargs)
def __init__(self, module: ModuleType, changes: Optional[Dict] = None): self._module = module self._changes = optional(changes, {})
def XYZ_to_Izazbz( XYZ_D65: ArrayLike, constants: Optional[Structure] = None, method: Union[ Literal["Safdar 2017", "Safdar 2021", "ZCAM"], str ] = "Safdar 2017", ) -> NDArray: """ Convert from *CIE XYZ* tristimulus values to :math:`I_za_zb_z` colourspace. Parameters ---------- XYZ_D65 *CIE XYZ* tristimulus values under *CIE Standard Illuminant D Series D65*. constants :math:`J_za_zb_z` colourspace constants. method Computation methods, *Safdar 2021* and *ZCAM* methods are equivalent. Returns ------- :class:`numpy.ndarray` :math:`I_za_zb_z` colourspace array where :math:`I_z` is the achromatic response, :math:`a_z` is redness-greenness and :math:`b_z` is yellowness-blueness. Warnings -------- The underlying *SMPTE ST 2084:2014* transfer function is an absolute transfer function. Notes ----- - The underlying *SMPTE ST 2084:2014* transfer function is an absolute transfer function, thus the domain and range values for the *Reference* and *1* scales are only indicative that the data is not affected by scale transformations. The effective domain of *SMPTE ST 2084:2014* inverse electro-optical transfer function (EOTF) is [0.0001, 10000]. +------------+-----------------------+------------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+==================+ | ``XYZ`` | ``UN`` | ``UN`` | +------------+-----------------------+------------------+ +------------+-----------------------+------------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+==================+ | ``Izazbz`` | ``Iz`` : [0, 1] | ``Iz`` : [0, 1] | | | | | | | ``az`` : [-1, 1] | ``az`` : [-1, 1] | | | | | | | ``bz`` : [-1, 1] | ``bz`` : [-1, 1] | +------------+-----------------------+------------------+ References ---------- :cite:`Safdar2017`, :cite:`Safdar2021` Examples -------- >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> XYZ_to_Izazbz(XYZ) # doctest: +ELLIPSIS array([ 0.0120779..., 0.0092430..., 0.0052600...]) """ X_D65, Y_D65, Z_D65 = tsplit(as_float_array(XYZ_D65)) method = validate_method(method, IZAZBZ_METHODS) constants = optional( constants, CONSTANTS_JZAZBZ_SAFDAR2017 if method == "safdar 2017" else CONSTANTS_JZAZBZ_SAFDAR2021, ) X_p_D65 = constants.b * X_D65 - (constants.b - 1) * Z_D65 Y_p_D65 = constants.g * Y_D65 - (constants.g - 1) * X_D65 XYZ_p_D65 = tstack([X_p_D65, Y_p_D65, Z_D65]) LMS = vector_dot(MATRIX_JZAZBZ_XYZ_TO_LMS, XYZ_p_D65) with domain_range_scale("ignore"): LMS_p = eotf_inverse_ST2084(LMS, 10000, constants) if method == "safdar 2017": Izazbz = vector_dot(MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2017, LMS_p) else: Izazbz = vector_dot(MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2021, LMS_p) Izazbz[..., 0] -= constants.d_0 return Izazbz
def nadir_grid( limits: Optional[ArrayLike] = None, segments: Integer = 10, labels: Optional[Sequence[str]] = None, axes: Optional[plt.Axes] = None, **kwargs: Any, ) -> Tuple[NDArray, NDArray, NDArray]: """ Return a grid on *CIE xy* plane made of quad geometric elements and its associated faces and edges colours. Ticks and labels are added to the given axes according to the extended grid settings. Parameters ---------- limits Extended grid limits. segments Edge segments count for the extended grid. labels Axis labels. axes Axes to add the grid. Other Parameters ---------------- grid_edge_alpha Grid edge opacity value such as `grid_edge_alpha = 0.5`. grid_edge_colours Grid edge colours array such as `grid_edge_colours = (0.25, 0.25, 0.25)`. grid_face_alpha Grid face opacity value such as `grid_face_alpha = 0.1`. grid_face_colours Grid face colours array such as `grid_face_colours = (0.25, 0.25, 0.25)`. ticks_and_label_location Location of the *X* and *Y* axis ticks and labels such as `ticks_and_label_location = ('-x', '-y')`. x_axis_colour *X* axis colour array such as `x_axis_colour = (0.0, 0.0, 0.0, 1.0)`. x_label_colour *X* axis label colour array such as `x_label_colour = (0.0, 0.0, 0.0, 0.85)`. x_ticks_colour *X* axis ticks colour array such as `x_ticks_colour = (0.0, 0.0, 0.0, 0.85)`. y_axis_colour *Y* axis colour array such as `y_axis_colour = (0.0, 0.0, 0.0, 1.0)`. y_label_colour *Y* axis label colour array such as `y_label_colour = (0.0, 0.0, 0.0, 0.85)`. y_ticks_colour *Y* axis ticks colour array such as `y_ticks_colour = (0.0, 0.0, 0.0, 0.85)`. Returns ------- :class:`tuple` Grid quads, faces colours, edges colours. Examples -------- >>> nadir_grid(segments=1) (array([[[-1. , -1. , 0. ], [ 1. , -1. , 0. ], [ 1. , 1. , 0. ], [-1. , 1. , 0. ]], <BLANKLINE> [[-1. , -1. , 0. ], [ 0. , -1. , 0. ], [ 0. , 0. , 0. ], [-1. , 0. , 0. ]], <BLANKLINE> [[-1. , 0. , 0. ], [ 0. , 0. , 0. ], [ 0. , 1. , 0. ], [-1. , 1. , 0. ]], <BLANKLINE> [[ 0. , -1. , 0. ], [ 1. , -1. , 0. ], [ 1. , 0. , 0. ], [ 0. , 0. , 0. ]], <BLANKLINE> [[ 0. , 0. , 0. ], [ 1. , 0. , 0. ], [ 1. , 1. , 0. ], [ 0. , 1. , 0. ]], <BLANKLINE> [[-1. , -0.001, 0. ], [ 1. , -0.001, 0. ], [ 1. , 0.001, 0. ], [-1. , 0.001, 0. ]], <BLANKLINE> [[-0.001, -1. , 0. ], [ 0.001, -1. , 0. ], [ 0.001, 1. , 0. ], [-0.001, 1. , 0. ]]]), array([[ 0.25, 0.25, 0.25, 0.1 ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 1. ], [ 0. , 0. , 0. , 1. ]]), array([[ 0.5 , 0.5 , 0.5 , 0.5 ], [ 0.75, 0.75, 0.75, 0.25], [ 0.75, 0.75, 0.75, 0.25], [ 0.75, 0.75, 0.75, 0.25], [ 0.75, 0.75, 0.75, 0.25], [ 0. , 0. , 0. , 1. ], [ 0. , 0. , 0. , 1. ]])) """ limits = as_float_array( cast(ArrayLike, optional(limits, np.array([[-1, 1], [-1, 1]])))) labels = cast(Sequence, optional(labels, ("x", "y"))) extent = np.max(np.abs(limits[..., 1] - limits[..., 0])) settings = Structure( **{ "grid_face_colours": (0.25, 0.25, 0.25), "grid_edge_colours": (0.50, 0.50, 0.50), "grid_face_alpha": 0.1, "grid_edge_alpha": 0.5, "x_axis_colour": (0.0, 0.0, 0.0, 1.0), "y_axis_colour": (0.0, 0.0, 0.0, 1.0), "x_ticks_colour": (0.0, 0.0, 0.0, 0.85), "y_ticks_colour": (0.0, 0.0, 0.0, 0.85), "x_label_colour": (0.0, 0.0, 0.0, 0.85), "y_label_colour": (0.0, 0.0, 0.0, 0.85), "ticks_and_label_location": ("-x", "-y"), }) settings.update(**kwargs) # Outer grid. quads_g = primitive_vertices_grid_mpl( origin=(-extent / 2, -extent / 2), width=extent, height=extent, height_segments=segments, width_segments=segments, ) RGB_g = ones((quads_g.shape[0], quads_g.shape[-1])) RGB_gf = RGB_g * settings.grid_face_colours RGB_gf = np.hstack( [RGB_gf, full((RGB_gf.shape[0], 1), settings.grid_face_alpha)]) RGB_ge = RGB_g * settings.grid_edge_colours RGB_ge = np.hstack( [RGB_ge, full((RGB_ge.shape[0], 1), settings.grid_edge_alpha)]) # Inner grid. quads_gs = primitive_vertices_grid_mpl( origin=(-extent / 2, -extent / 2), width=extent, height=extent, height_segments=segments * 2, width_segments=segments * 2, ) RGB_gs = ones((quads_gs.shape[0], quads_gs.shape[-1])) RGB_gsf = RGB_gs * 0 RGB_gsf = np.hstack([RGB_gsf, full((RGB_gsf.shape[0], 1), 0)]) RGB_gse = np.clip(RGB_gs * settings.grid_edge_colours * 1.5, 0, 1) RGB_gse = np.hstack( (RGB_gse, full((RGB_gse.shape[0], 1), settings.grid_edge_alpha / 2))) # Axis. thickness = extent / 1000 quad_x = primitive_vertices_grid_mpl(origin=(limits[0, 0], -thickness / 2), width=extent, height=thickness) RGB_x = ones((quad_x.shape[0], quad_x.shape[-1] + 1)) RGB_x = RGB_x * settings.x_axis_colour quad_y = primitive_vertices_grid_mpl(origin=(-thickness / 2, limits[1, 0]), width=thickness, height=extent) RGB_y = ones((quad_y.shape[0], quad_y.shape[-1] + 1)) RGB_y = RGB_y * settings.y_axis_colour if axes is not None: # Ticks. x_s = 1 if "+x" in settings.ticks_and_label_location else -1 y_s = 1 if "+y" in settings.ticks_and_label_location else -1 for i, axis in enumerate("xy"): h_a = "center" if axis == "x" else "left" if x_s == 1 else "right" v_a = "center" ticks = list(sorted(set(quads_g[..., 0, i]))) ticks += [ticks[-1] + ticks[-1] - ticks[-2]] for tick in ticks: x = (limits[1, 1 if x_s == 1 else 0] + (x_s * extent / 25) if i else tick) y = (tick if i else limits[0, 1 if y_s == 1 else 0] + (y_s * extent / 25)) tick = as_int_scalar(tick) if is_integer(tick) else tick c = settings[f"{axis}_ticks_colour"] axes.text( x, y, 0, tick, "x", horizontalalignment=h_a, verticalalignment=v_a, color=c, clip_on=True, ) # Labels. for i, axis in enumerate("xy"): h_a = "center" if axis == "x" else "left" if x_s == 1 else "right" v_a = "center" x = (limits[1, 1 if x_s == 1 else 0] + (x_s * extent / 10) if i else 0) y = (0 if i else limits[0, 1 if y_s == 1 else 0] + (y_s * extent / 10)) c = settings[f"{axis}_label_colour"] axes.text( x, y, 0, labels[i], "x", horizontalalignment=h_a, verticalalignment=v_a, color=c, size=20, clip_on=True, ) quads = np.vstack([quads_g, quads_gs, quad_x, quad_y]) RGB_f = np.vstack([RGB_gf, RGB_gsf, RGB_x, RGB_y]) RGB_e = np.vstack([RGB_ge, RGB_gse, RGB_x, RGB_y]) return quads, RGB_f, RGB_e
def plot_hull_section_contour( hull: trimesh.Trimesh, # type: ignore[name-defined] # noqa model: Union[Literal["CAM02LCD", "CAM02SCD", "CAM02UCS", "CAM16LCD", "CAM16SCD", "CAM16UCS", "CIE XYZ", "CIE xyY", "CIE Lab", "CIE Luv", "CIE UCS", "CIE UVW", "DIN99", "Hunter Lab", "Hunter Rdab", "ICaCb", "ICtCp", "IPT", "IgPgTg", "Jzazbz", "OSA UCS", "Oklab", "hdr-CIELAB", "hdr-IPT", ], str, ] = "CIE xyY", axis: Union[Literal["+z", "+x", "+y"], str] = "+z", origin: Floating = 0.5, normalise: Boolean = True, contour_colours: Optional[Union[ArrayLike, str]] = None, contour_opacity: Floating = 1, convert_kwargs: Optional[Dict] = None, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the section contour of given *trimesh* hull along given axis and origin. Parameters ---------- hull *Trimesh* hull. model Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for the list of supported colourspace models. axis Axis the hull section will be normal to. origin Coordinate along ``axis`` at which to plot the hull section. normalise Whether to normalise ``axis`` to the extent of the hull along it. contour_colours Colours of the hull section contour, if ``contour_colours`` is set to *RGB*, the colours will be computed according to the corresponding coordinates. contour_opacity Opacity of the hull section contour. convert_kwargs Keyword arguments for the :func:`colour.convert` definition. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> from colour.models import RGB_COLOURSPACE_sRGB >>> from colour.utilities import is_trimesh_installed >>> vertices, faces, _outline = primitive_cube(1, 1, 1, 64, 64, 64) >>> XYZ_vertices = RGB_to_XYZ( ... vertices['position'] + 0.5, ... RGB_COLOURSPACE_sRGB.whitepoint, ... RGB_COLOURSPACE_sRGB.whitepoint, ... RGB_COLOURSPACE_sRGB.matrix_RGB_to_XYZ, ... ) >>> if is_trimesh_installed: ... import trimesh ... hull = trimesh.Trimesh(XYZ_vertices, faces, process=False) ... plot_hull_section_contour(hull, contour_colours='RGB') ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Hull_Section_Contour.png :align: center :alt: plot_hull_section_contour """ hull = hull.copy() contour_colours = cast( Union[ArrayLike, str], optional(contour_colours, CONSTANTS_COLOUR_STYLE.colour.dark), ) settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) convert_kwargs = optional(convert_kwargs, {}) # Luminance / Lightness is re-ordered along "z-up" axis. with suppress_warnings(python_warnings=True): ijk_vertices = colourspace_model_axis_reorder( convert(hull.vertices, "CIE XYZ", model, **convert_kwargs), model) ijk_vertices = np.nan_to_num(ijk_vertices) ijk_vertices *= COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[ model] hull.vertices = ijk_vertices plane = MAPPING_AXIS_TO_PLANE[axis] padding = 0.1 * np.mean( COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[model]) min_x = np.min(ijk_vertices[..., plane[0]]) - padding max_x = np.max(ijk_vertices[..., plane[0]]) + padding min_y = np.min(ijk_vertices[..., plane[1]]) - padding max_y = np.max(ijk_vertices[..., plane[1]]) + padding extent = (min_x, max_x, min_y, max_y) use_RGB_contour_colours = str(contour_colours).upper() == "RGB" section = hull_section(hull, axis, origin, normalise) if use_RGB_contour_colours: ijk_section = section / ( COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[model]) XYZ_section = convert( colourspace_model_axis_reorder(ijk_section, model, "Inverse"), model, "CIE XYZ", **convert_kwargs, ) contour_colours = np.clip(XYZ_to_plotting_colourspace(XYZ_section), 0, 1) section = np.reshape(section[..., plane], (-1, 1, 2)) line_collection = LineCollection( np.concatenate([section[:-1], section[1:]], axis=1), colors=contour_colours, alpha=contour_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_line, ) axes.add_collection(line_collection) settings = { "axes": axes, "bounding_box": extent, } settings.update(kwargs) return render(**settings)
def plot_multi_functions( functions: Dict[str, Callable], samples: Optional[ArrayLike] = None, log_x: Optional[Integer] = None, log_y: Optional[Integer] = None, plot_kwargs: Optional[Union[Dict, List[Dict]]] = None, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot given functions. Parameters ---------- functions Functions to plot. samples Samples to evaluate the functions with. log_x Log base to use for the *x* axis scale, if *None*, the *x* axis scale will be linear. log_y Log base to use for the *y* axis scale, if *None*, the *y* axis scale will be linear. plot_kwargs Keyword arguments for the :func:`matplotlib.pyplot.plot` definition, used to control the style of the plotted functions. ``plot_kwargs`` can be either a single dictionary applied to all the plotted functions with the same settings or a sequence of dictionaries with different settings for each plotted function. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> functions = { ... 'Gamma 2.2' : lambda x: x ** (1 / 2.2), ... 'Gamma 2.4' : lambda x: x ** (1 / 2.4), ... 'Gamma 2.6' : lambda x: x ** (1 / 2.6), ... } >>> plot_multi_functions(functions) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Multi_Functions.png :align: center :alt: plot_multi_functions """ settings: Dict[str, Any] = dict(kwargs) _figure, axes = artist(**settings) plot_settings_collection = [{ "label": f"{name}", "zorder": CONSTANTS_COLOUR_STYLE.zorder.midground_label, } for name in functions.keys()] if plot_kwargs is not None: update_settings_collection(plot_settings_collection, plot_kwargs, len(functions)) # TODO: Remove when "Matplotlib" minimum version can be set to 3.5.0. matplotlib_3_5 = tuple( int(token) for token in matplotlib.__version__.split(".")[:2]) >= (3, 5) if log_x is not None and log_y is not None: attest( log_x >= 2 and log_y >= 2, "Log base must be equal or greater than 2.", ) plotting_function = axes.loglog axes.set_xscale("log", base=log_x) axes.set_yscale("log", base=log_y) elif log_x is not None: attest(log_x >= 2, "Log base must be equal or greater than 2.") if matplotlib_3_5: # pragma: no cover plotting_function = partial(axes.semilogx, base=log_x) else: # pragma: no cover plotting_function = partial(axes.semilogx, basex=log_x) elif log_y is not None: attest(log_y >= 2, "Log base must be equal or greater than 2.") if matplotlib_3_5: # pragma: no cover plotting_function = partial(axes.semilogy, base=log_y) else: # pragma: no cover plotting_function = partial(axes.semilogy, basey=log_y) else: plotting_function = axes.plot samples = cast(ArrayLike, optional(samples, np.linspace(0, 1, 1000))) for i, (_name, function) in enumerate(functions.items()): plotting_function(samples, function(samples), **plot_settings_collection[i]) x_label = (f"x - Log Base {log_x} Scale" if log_x is not None else "x - Linear Scale") y_label = (f"y - Log Base {log_y} Scale" if log_y is not None else "y - Linear Scale") settings = { "axes": axes, "legend": True, "title": f"{', '.join(functions)} - Functions", "x_label": x_label, "y_label": y_label, } settings.update(kwargs) return render(**settings)
def plot_RGB_scatter( RGB: ArrayLike, colourspace: Union[RGB_Colourspace, str, Sequence[Union[RGB_Colourspace, str]]], reference_colourspace: Union[Literal["CAM02LCD", "CAM02SCD", "CAM02UCS", "CAM16LCD", "CAM16SCD", "CAM16UCS", "CIE XYZ", "CIE xyY", "CIE Lab", "CIE Luv", "CIE UCS", "CIE UVW", "DIN99", "Hunter Lab", "Hunter Rdab", "ICaCb", "ICtCp", "IPT", "IgPgTg", "Jzazbz", "OSA UCS", "Oklab", "hdr-CIELAB", "hdr-IPT", ], str, ] = "CIE xyY", colourspaces: Optional[Union[RGB_Colourspace, str, Sequence[Union[RGB_Colourspace, str]]]] = None, segments: Integer = 8, show_grid: Boolean = True, grid_segments: Integer = 10, show_spectral_locus: Boolean = False, spectral_locus_colour: Optional[Union[ArrayLike, str]] = None, points_size: Floating = 12, cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", chromatically_adapt: Boolean = False, convert_kwargs: Optional[Dict] = None, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot given *RGB* colourspace array in a scatter plot. Parameters ---------- RGB *RGB* colourspace array. colourspace *RGB* colourspace of the *RGB* array. ``colourspace`` can be of any type or form supported by the :func:`colour.plotting.filter_RGB_colourspaces` definition. reference_colourspace Reference colourspace model to plot the gamuts into, see :attr:`colour.COLOURSPACE_MODELS` attribute for the list of supported colourspace models. colourspaces *RGB* colourspaces to plot the gamuts. ``colourspaces`` elements can be of any type or form supported by the :func:`colour.plotting.filter_RGB_colourspaces` definition. segments Edge segments count for each *RGB* colourspace cubes. show_grid Whether to show a grid at the bottom of the *RGB* colourspace cubes. grid_segments Edge segments count for the grid. show_spectral_locus Whether to show the spectral locus. spectral_locus_colour Spectral locus colour. points_size Scatter points size. cmfs Standard observer colour matching functions used for computing the spectral locus boundaries. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. chromatically_adapt Whether to chromatically adapt the *RGB* colourspaces given in ``colourspaces`` to the whitepoint of the default plotting colourspace. convert_kwargs Keyword arguments for the :func:`colour.convert` definition. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_RGB_colourspaces_gamuts`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> RGB = np.random.random((128, 128, 3)) >>> plot_RGB_scatter(RGB, 'ITU-R BT.709') # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...Axes3DSubplot...>) .. image:: ../_static/Plotting_Plot_RGB_Scatter.png :align: center :alt: plot_RGB_scatter """ colourspace = cast( RGB_Colourspace, first_item(filter_RGB_colourspaces(colourspace).values()), ) colourspaces = cast(List[str], optional(colourspaces, [colourspace.name])) convert_kwargs = optional(convert_kwargs, {}) count_c = len(colourspaces) settings = Structure( **{ "face_colours": [None] * count_c, "edge_colours": [(0.25, 0.25, 0.25)] * count_c, "face_alpha": [0.0] * count_c, "edge_alpha": [0.1] * count_c, }) settings.update(kwargs) settings["standalone"] = False plot_RGB_colourspaces_gamuts( colourspaces=colourspaces, reference_colourspace=reference_colourspace, segments=segments, show_grid=show_grid, grid_segments=grid_segments, show_spectral_locus=show_spectral_locus, spectral_locus_colour=spectral_locus_colour, cmfs=cmfs, chromatically_adapt=chromatically_adapt, **settings, ) XYZ = RGB_to_XYZ( RGB, colourspace.whitepoint, colourspace.whitepoint, colourspace.matrix_RGB_to_XYZ, ) convert_settings = {"illuminant": colourspace.whitepoint} convert_settings.update(convert_kwargs) points = colourspace_model_axis_reorder( convert(XYZ, "CIE XYZ", reference_colourspace, **convert_settings), reference_colourspace, ) axes = plt.gca() axes.scatter( points[..., 0], points[..., 1], points[..., 2], color=np.reshape(RGB, (-1, 3)), s=points_size, zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_scatter, ) settings.update({"axes": axes, "standalone": True}) settings.update(kwargs) return render(**settings)
def logarithmic_function_camera( x: FloatingOrArrayLike, style: Union[Literal["cameraLinToLog", "cameraLogToLin"], str] = "cameraLinToLog", base: Integer = 2, log_side_slope: Floating = 1, lin_side_slope: Floating = 1, log_side_offset: Floating = 0, lin_side_offset: Floating = 0, lin_side_break: Floating = 0.005, linear_slope: Optional[Floating] = None, ) -> FloatingOrNDArray: """ Define the camera logarithmic function. Parameters ---------- x Linear/non-linear data to undergo encoding/decoding. style Defines the behaviour for the logarithmic function to operate: - *cameraLinToLog*: Applies a piece-wise function with logarithmic and linear segments on linear values, converting them to non-linear values. - *cameraLogToLin*: Applies a piece-wise function with logarithmic and linear segments on non-linear values, converting them to linear values. base Logarithmic base used for the conversion. log_side_slope Slope (or gain) applied to the log side of the logarithmic segment. The default value is 1. lin_side_slope Slope of the linear side of the logarithmic segment. The default value is 1. log_side_offset Offset applied to the log side of the logarithmic segment. The default value is 0. lin_side_offset Offset applied to the linear side of the logarithmic segment. The default value is 0. lin_side_break Break-point, defined in linear space, at which the piece-wise function transitions between the logarithmic and linear segments. linear_slope Slope of the linear portion of the curve. The default value is *None*. Returns ------- :class:`numpy.floating` or :class:`numpy.ndarray` Encoded/Decoded data. Examples -------- >>> logarithmic_function_camera( # doctest: +ELLIPSIS ... 0.18, 'cameraLinToLog') -2.4739311... >>> logarithmic_function_camera( # doctest: +ELLIPSIS ... -2.4739311883324122, 'cameraLogToLin') 0.1800000... """ x = as_float_array(x) style = validate_method( style, ["cameraLinToLog", "cameraLogToLin"], '"{0}" style is invalid, it must be one of {1}!', ) log_side_break = ( log_side_slope * (np.log(lin_side_slope * lin_side_break + lin_side_offset) / np.log(base)) + log_side_offset) linear_slope = cast( Floating, optional( linear_slope, (log_side_slope * (lin_side_slope / ((lin_side_slope * lin_side_break + lin_side_offset) * np.log(base)))), ), ) linear_offset = log_side_break - linear_slope * lin_side_break if style == "cameralintolog": return as_float( np.where( x <= lin_side_break, linear_slope * x + linear_offset, logarithmic_function_quasilog( x, "linToLog", base, log_side_slope, lin_side_slope, log_side_offset, lin_side_offset, ), )) else: # style == 'cameralogtolin' return as_float( np.where( x <= log_side_break, (x - linear_offset) / linear_slope, logarithmic_function_quasilog( x, "logToLin", base, log_side_slope, lin_side_slope, log_side_offset, lin_side_offset, ), ))
def signal_unpack_data( data=Optional[Union[ArrayLike, dict, Series, "Signal"]], domain: Optional[ArrayLike] = None, dtype: Optional[Type[DTypeFloating]] = None, ) -> Tuple: """ Unpack given data for continuous signal instantiation. Parameters ---------- data Data to unpack for continuous signal instantiation. domain Values to initialise the :attr:`colour.continuous.Signal.domain` attribute with. If both ``data`` and ``domain`` arguments are defined, the latter will be used to initialise the :attr:`colour.continuous.Signal.domain` property. dtype Floating point data type. Returns ------- :class:`tuple` Independent domain variable :math:`x` and corresponding range variable :math:`y` unpacked for continuous signal instantiation. Examples -------- Unpacking using implicit *domain*: >>> range_ = np.linspace(10, 100, 10) >>> domain, range_ = Signal.signal_unpack_data(range_) >>> print(domain) [ 0. 1. 2. 3. 4. 5. 6. 7. 8. 9.] >>> print(range_) [ 10. 20. 30. 40. 50. 60. 70. 80. 90. 100.] Unpacking using explicit *domain*: >>> domain = np.arange(100, 1100, 100) >>> domain, range = Signal.signal_unpack_data(range_, domain) >>> print(domain) [ 100. 200. 300. 400. 500. 600. 700. 800. 900. 1000.] >>> print(range_) [ 10. 20. 30. 40. 50. 60. 70. 80. 90. 100.] Unpacking using a *dict*: >>> domain, range_ = Signal.signal_unpack_data( ... dict(zip(domain, range_))) >>> print(domain) [ 100. 200. 300. 400. 500. 600. 700. 800. 900. 1000.] >>> print(range_) [ 10. 20. 30. 40. 50. 60. 70. 80. 90. 100.] Unpacking using a *Pandas* :class:`pandas.Series`: >>> if is_pandas_installed(): ... from pandas import Series ... domain, range = Signal.signal_unpack_data( ... Series(dict(zip(domain, range_)))) ... # doctest: +ELLIPSIS >>> print(domain) # doctest: +SKIP [ 100. 200. 300. 400. 500. 600. 700. 800. 900. 1000.] >>> print(range_) # doctest: +SKIP [ 10. 20. 30. 40. 50. 60. 70. 80. 90. 100.] Unpacking using a :class:`colour.continuous.Signal` class: >>> domain, range_ = Signal.signal_unpack_data( ... Signal(range_, domain)) >>> print(domain) [ 100. 200. 300. 400. 500. 600. 700. 800. 900. 1000.] >>> print(range_) [ 10. 20. 30. 40. 50. 60. 70. 80. 90. 100.] """ dtype = cast(Type[DTypeFloating], optional(dtype, DEFAULT_FLOAT_DTYPE)) domain_unpacked: NDArray = np.array([]) range_unpacked: NDArray = np.array([]) if isinstance(data, Signal): domain_unpacked = data.domain range_unpacked = data.range elif issubclass(type(data), Sequence) or isinstance( data, (tuple, list, np.ndarray, Iterator, ValuesView) ): data_array = tsplit(list(data)) attest(data_array.ndim == 1, 'User "data" must be 1-dimensional!') domain_unpacked, range_unpacked = ( np.arange(0, data_array.size, dtype=dtype), data_array, ) elif issubclass(type(data), Mapping) or isinstance(data, dict): domain_unpacked, range_unpacked = tsplit(sorted(data.items())) elif is_pandas_installed(): if isinstance(data, Series): domain_unpacked = data.index.values range_unpacked = data.values if domain is not None: domain_array = as_float_array(list(domain), dtype) # type: ignore[arg-type] attest( len(domain_array) == len(range_unpacked), 'User "domain" length is not compatible with unpacked "range"!', ) domain_unpacked = domain_array if range_unpacked is not None: range_unpacked = as_float_array(range_unpacked, dtype) return domain_unpacked, range_unpacked
def plot_single_sd_colour_rendition_report_full( sd: SpectralDistribution, source: Optional[str] = None, date: Optional[str] = None, manufacturer: Optional[str] = None, model: Optional[str] = None, notes: Optional[str] = None, report_size: Tuple = CONSTANT_REPORT_SIZE_FULL, report_row_height_ratios: Tuple = CONSTANT_REPORT_ROW_HEIGHT_RATIOS_FULL, report_box_padding: Optional[Dict] = None, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: # noqa: D405,D407,D410,D411 """ Generate the full *ANSI/IES TM-30-18 Colour Rendition Report* for given spectral distribution. Parameters ---------- sd Spectral distribution of the emission source to generate the report for. source Emission source name, defaults to `colour.SpectralDistribution_IESTM2714.header.description` or `colour.SpectralDistribution_IESTM2714.name` properties value. date Emission source measurement date, defaults to `colour.SpectralDistribution_IESTM2714.header.report_date` property value. manufacturer Emission source manufacturer, defaults to `colour.SpectralDistribution_IESTM2714.header.manufacturer` property value. model Emission source model, defaults to `colour.SpectralDistribution_IESTM2714.header.catalog_number` property value. notes Notes pertaining to the emission source, defaults to `colour.SpectralDistribution_IESTM2714.header.comments` property value. report_size Report size, default to A4 paper size in inches. report_row_height_ratios Report size row height ratios. report_box_padding Report box padding, tries to define the padding around the figure and in-between the axes. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> from colour import SDS_ILLUMINANTS >>> sd = SDS_ILLUMINANTS['FL2'] >>> plot_single_sd_colour_rendition_report_full(sd) ... # doctest: +ELLIPSIS (<Figure size ... with ... Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_\ Plot_Single_SD_Colour_Rendition_Report_Full.png :align: center :alt: plot_single_sd_colour_rendition_report_full """ report_box_padding = optional(report_box_padding, CONSTANT_REPORT_PADDING_FULL) specification: ColourQuality_Specification_ANSIIESTM3018 = cast( ColourQuality_Specification_ANSIIESTM3018, colour_fidelity_index_ANSIIESTM3018(sd, True), ) sd = (SpectralDistribution_IESTM2714(data=sd, name=sd.name) if not isinstance(sd, SpectralDistribution_IESTM2714) else sd) NA = _VALUE_NOT_APPLICABLE source = optional(optional(source, sd.header.description), sd.name) date = optional(optional(date, sd.header.report_date), NA) manufacturer = optional(optional(manufacturer, sd.header.manufacturer), NA) model = optional(optional(model, sd.header.catalog_number), NA) notes = optional(optional(notes, sd.header.comments), NA) figure = plt.figure(figsize=report_size, constrained_layout=True) settings: Dict[str, Any] = dict(kwargs) settings["standalone"] = False settings["tight_layout"] = False gridspec_report = figure.add_gridspec( 5, 1, height_ratios=report_row_height_ratios) # Title Row gridspec_title = gridspec_report[0].subgridspec(1, 1) axes_title = figure.add_subplot(gridspec_title[0]) _plot_report_header(axes_title) # Description Rows & Columns gridspec_description = gridspec_report[1].subgridspec(1, 2) # Source & Date Column axes_source_date = figure.add_subplot(gridspec_description[0]) axes_source_date.set_axis_off() axes_source_date.text( 0.25, 2 / 3, "Source: ", ha="right", va="center", size="medium", weight="bold", ) axes_source_date.text(0.25, 2 / 3, source, va="center", size="medium") axes_source_date.text( 0.25, 1 / 3, "Date: ", ha="right", va="center", size="medium", weight="bold", ) axes_source_date.text(0.25, 1 / 3, date, va="center", size="medium") # Manufacturer & Model Column axes_manufacturer_model = figure.add_subplot(gridspec_description[1]) axes_manufacturer_model.set_axis_off() axes_manufacturer_model.text( 0.25, 2 / 3, "Manufacturer: ", ha="right", va="center", size="medium", weight="bold", ) axes_manufacturer_model.text(0.25, 2 / 3, manufacturer, va="center", size="medium") axes_manufacturer_model.text( 0.25, 1 / 3, "Model: ", ha="right", va="center", size="medium", weight="bold", ) axes_manufacturer_model.text(0.25, 1 / 3, model, va="center", size="medium") # Main Figures Rows & Columns gridspec_figures = gridspec_report[2].subgridspec( 4, 2, height_ratios=[1, 1, 1, 1.5]) axes_spectra = figure.add_subplot(gridspec_figures[0, 0]) plot_spectra_ANSIIESTM3018(specification, axes=axes_spectra, **settings) axes_vector_graphics = figure.add_subplot(gridspec_figures[1:3, 0]) plot_colour_vector_graphic(specification, axes=axes_vector_graphics, **settings) axes_chroma_shifts = figure.add_subplot(gridspec_figures[0, 1]) plot_local_chroma_shifts(specification, axes=axes_chroma_shifts, **settings) axes_hue_shifts = figure.add_subplot(gridspec_figures[1, 1]) plot_local_hue_shifts(specification, axes=axes_hue_shifts, **settings) axes_colour_fidelities = figure.add_subplot(gridspec_figures[2, 1]) plot_local_colour_fidelities(specification, axes=axes_colour_fidelities, x_ticker=True, **settings) # Colour Fidelity Indexes Row axes_colour_fidelity_indexes = figure.add_subplot(gridspec_figures[3, :]) plot_colour_fidelity_indexes(specification, axes=axes_colour_fidelity_indexes, **settings) # Notes & Chromaticities / CRI Row and Columns gridspec_notes_chromaticities_CRI = gridspec_report[3].subgridspec(1, 2) axes_notes = figure.add_subplot(gridspec_notes_chromaticities_CRI[0]) axes_notes.set_axis_off() axes_notes.text( 0.25, 1, "Notes: ", ha="right", va="center", size="medium", weight="bold", ) axes_notes.text(0.25, 1, notes, va="center", size="medium") gridspec_chromaticities_CRI = gridspec_notes_chromaticities_CRI[ 1].subgridspec(1, 2) XYZ = sd_to_XYZ(specification.sd_test) xy = XYZ_to_xy(XYZ) Luv = XYZ_to_Luv(XYZ, xy) uv_p = Luv_to_uv(Luv, xy) gridspec_chromaticities = gridspec_chromaticities_CRI[0].subgridspec(1, 1) axes_chromaticities = figure.add_subplot(gridspec_chromaticities[0]) axes_chromaticities.set_axis_off() axes_chromaticities.text( 0.5, 4 / 5, f"$x$ {xy[0]:.4f}", ha="center", va="center", size="medium", weight="bold", ) axes_chromaticities.text( 0.5, 3 / 5, f"$y$ {xy[1]:.4f}", ha="center", va="center", size="medium", weight="bold", ) axes_chromaticities.text( 0.5, 2 / 5, f"$u'$ {uv_p[0]:.4f}", ha="center", va="center", size="medium", weight="bold", ) axes_chromaticities.text( 0.5, 1 / 5, f"$v'$ {uv_p[1]:.4f}", ha="center", va="center", size="medium", weight="bold", ) gridspec_CRI = gridspec_chromaticities_CRI[1].subgridspec(1, 1) CRI_spec: ColourRendering_Specification_CRI = cast( ColourRendering_Specification_CRI, colour_rendering_index(specification.sd_test, additional_data=True), ) axes_CRI = figure.add_subplot(gridspec_CRI[0]) axes_CRI.set_xticks([]) axes_CRI.set_yticks([]) axes_CRI.text( 0.5, 4 / 5, "CIE 13.31-1995", ha="center", va="center", size="medium", weight="bold", ) axes_CRI.text( 0.5, 3 / 5, "(CRI)", ha="center", va="center", size="medium", weight="bold", ) axes_CRI.text( 0.5, 2 / 5, f"$R_a$ {float(CRI_spec.Q_a):.0f}", ha="center", va="center", size="medium", weight="bold", ) axes_CRI.text( 0.5, 1 / 5, f"$R_9$ {float(CRI_spec.Q_as[8].Q_a):.0f}", ha="center", va="center", size="medium", weight="bold", ) gridspec_footer = gridspec_report[4].subgridspec(1, 1) axes_footer = figure.add_subplot(gridspec_footer[0]) _plot_report_footer(axes_footer) figure.set_constrained_layout_pads(**report_box_padding) settings = dict(kwargs) settings["tight_layout"] = False return render(**settings)
def plot_hull_section_colours( hull: trimesh.Trimesh, # type: ignore[name-defined] # noqa model: Union[Literal["CAM02LCD", "CAM02SCD", "CAM02UCS", "CAM16LCD", "CAM16SCD", "CAM16UCS", "CIE XYZ", "CIE xyY", "CIE Lab", "CIE Luv", "CIE UCS", "CIE UVW", "DIN99", "Hunter Lab", "Hunter Rdab", "ICaCb", "ICtCp", "IPT", "IgPgTg", "Jzazbz", "OSA UCS", "Oklab", "hdr-CIELAB", "hdr-IPT", ], str, ] = "CIE xyY", axis: Union[Literal["+z", "+x", "+y"], str] = "+z", origin: Floating = 0.5, normalise: Boolean = True, section_colours: Optional[Union[ArrayLike, str]] = None, section_opacity: Floating = 1, convert_kwargs: Optional[Dict] = None, samples: Integer = 256, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the section colours of given *trimesh* hull along given axis and origin. Parameters ---------- hull *Trimesh* hull. model Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for the list of supported colourspace models. axis Axis the hull section will be normal to. origin Coordinate along ``axis`` at which to plot the hull section. normalise Whether to normalise ``axis`` to the extent of the hull along it. section_colours Colours of the hull section, if ``section_colours`` is set to *RGB*, the colours will be computed according to the corresponding coordinates. section_opacity Opacity of the hull section colours. convert_kwargs Keyword arguments for the :func:`colour.convert` definition. samples Samples count on one axis when computing the hull section colours. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> from colour.models import RGB_COLOURSPACE_sRGB >>> from colour.utilities import is_trimesh_installed >>> vertices, faces, _outline = primitive_cube(1, 1, 1, 64, 64, 64) >>> XYZ_vertices = RGB_to_XYZ( ... vertices['position'] + 0.5, ... RGB_COLOURSPACE_sRGB.whitepoint, ... RGB_COLOURSPACE_sRGB.whitepoint, ... RGB_COLOURSPACE_sRGB.matrix_RGB_to_XYZ, ... ) >>> if is_trimesh_installed: ... import trimesh ... hull = trimesh.Trimesh(XYZ_vertices, faces, process=False) ... plot_hull_section_colours(hull, section_colours='RGB') ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Hull_Section_Colours.png :align: center :alt: plot_hull_section_colours """ axis = validate_method( axis, ["+z", "+x", "+y"], '"{0}" axis is invalid, it must be one of {1}!', ) hull = hull.copy() settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) section_colours = cast( ArrayLike, optional(section_colours, HEX_to_RGB(CONSTANTS_COLOUR_STYLE.colour.average)), ) convert_kwargs = optional(convert_kwargs, {}) # Luminance / Lightness reordered along "z" axis. with suppress_warnings(python_warnings=True): ijk_vertices = colourspace_model_axis_reorder( convert(hull.vertices, "CIE XYZ", model, **convert_kwargs), model) ijk_vertices = np.nan_to_num(ijk_vertices) ijk_vertices *= COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[ model] hull.vertices = ijk_vertices if axis == "+x": index_origin = 0 elif axis == "+y": index_origin = 1 elif axis == "+z": index_origin = 2 plane = MAPPING_AXIS_TO_PLANE[axis] section = hull_section(hull, axis, origin, normalise) padding = 0.1 * np.mean( COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[model]) min_x = np.min(ijk_vertices[..., plane[0]]) - padding max_x = np.max(ijk_vertices[..., plane[0]]) + padding min_y = np.min(ijk_vertices[..., plane[1]]) - padding max_y = np.max(ijk_vertices[..., plane[1]]) + padding extent = (min_x, max_x, min_y, max_y) use_RGB_section_colours = str(section_colours).upper() == "RGB" if use_RGB_section_colours: ii, jj = np.meshgrid( np.linspace(min_x, max_x, samples), np.linspace(max_y, min_y, samples), ) ij = tstack([ii, jj]) ijk_section = full((samples, samples, 3), np.median(section[..., index_origin])) ijk_section[..., plane] = ij ijk_section /= COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[ model] XYZ_section = convert( colourspace_model_axis_reorder(ijk_section, model, "Inverse"), model, "CIE XYZ", **convert_kwargs, ) RGB_section = XYZ_to_plotting_colourspace(XYZ_section) else: section_colours = np.hstack([section_colours, section_opacity]) facecolor = "none" if use_RGB_section_colours else section_colours polygon = Polygon( section[..., plane], facecolor=facecolor, edgecolor="none", zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, ) axes.add_patch(polygon) if use_RGB_section_colours: image = axes.imshow( np.clip(RGB_section, 0, 1), interpolation="bilinear", extent=extent, clip_path=None, alpha=section_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, ) image.set_clip_path(polygon) settings = { "axes": axes, "bounding_box": extent, } settings.update(kwargs) return render(**settings)
def plot_single_sd_colour_rendition_report_simple( sd: SpectralDistribution, report_size: Tuple = CONSTANT_REPORT_SIZE_SIMPLE, report_row_height_ratios: Tuple = CONSTANT_REPORT_ROW_HEIGHT_RATIOS_SIMPLE, report_box_padding: Optional[Dict] = None, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Generate the simple *ANSI/IES TM-30-18 Colour Rendition Report* for given spectral distribution. Parameters ---------- sd Spectral distribution of the emission source to generate the report for. report_size Report size, default to A4 paper size in inches. report_row_height_ratios Report size row height ratios. report_box_padding Report box padding, tries to define the padding around the figure and in-between the axes. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> from colour import SDS_ILLUMINANTS >>> sd = SDS_ILLUMINANTS['FL2'] >>> plot_single_sd_colour_rendition_report_simple(sd) ... # doctest: +ELLIPSIS (<Figure size ... with ... Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_\ Plot_Single_SD_Colour_Rendition_Report_Simple.png :align: center :alt: plot_single_sd_colour_rendition_report_simple """ report_box_padding = optional(report_box_padding, CONSTANT_REPORT_PADDING_SIMPLE) specification: ColourQuality_Specification_ANSIIESTM3018 = cast( ColourQuality_Specification_ANSIIESTM3018, colour_fidelity_index_ANSIIESTM3018(sd, True), ) figure = plt.figure(figsize=report_size, constrained_layout=True) settings: Dict[str, Any] = dict(kwargs) settings["standalone"] = False settings["tight_layout"] = False gridspec_report = figure.add_gridspec( 3, 1, height_ratios=report_row_height_ratios) # Title Row gridspec_title = gridspec_report[0].subgridspec(1, 1) axes_title = figure.add_subplot(gridspec_title[0]) _plot_report_header(axes_title) # Main Figures Rows & Columns gridspec_figures = gridspec_report[1].subgridspec(1, 1) axes_vector_graphics = figure.add_subplot(gridspec_figures[0, 0]) plot_colour_vector_graphic(specification, axes=axes_vector_graphics, **settings) gridspec_footer = gridspec_report[2].subgridspec(1, 1) axes_footer = figure.add_subplot(gridspec_footer[0]) _plot_report_footer(axes_footer) figure.set_constrained_layout_pads(**report_box_padding) settings = dict(kwargs) settings["tight_layout"] = False return render(**settings)