def camera(**kwargs: Union[KwargsCamera, Any]) -> Tuple[plt.Figure, plt.Axes]: """ Set the camera settings. Other Parameters ---------------- kwargs {:func:`colour.plotting.common.KwargsCamera`}, See the documentation of the previously listed class. Returns ------- :class:`tuple` Current figure and axes. """ figure = cast(plt.Figure, kwargs.get("figure", plt.gcf())) axes = cast(plt.Axes, kwargs.get("axes", plt.gca())) settings = Structure(**{ "camera_aspect": "equal", "elevation": None, "azimuth": None }) settings.update(kwargs) if settings.camera_aspect == "equal": uniform_axes3d(axes=axes) axes.view_init(elev=settings.elevation, azim=settings.azimuth) return figure, axes
def contour_centroid(contour: ArrayLike) -> Tuple[Floating, Floating]: """ Return the centroid of given contour. Parameters ---------- contour Contour to return the centroid of. Returns ------- :class:`tuple` Contour centroid. Notes ----- - A :class:`tuple` class is returned instead of a :class:`ndarray` class for convenience with *OpenCV*. Examples -------- >>> contour = np.array([[0, 0], [1, 0], [1, 1], [0, 1]]) >>> contour_centroid(contour) (0.5, 0.5) """ moments = cv2.moments(contour) centroid = ( moments["m10"] / moments["m00"], moments["m01"] / moments["m00"], ) return cast(Tuple[Floating, Floating], centroid)
def plot_single_illuminant_sd( illuminant: Union[SpectralDistribution, str], cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot given single illuminant spectral distribution. Parameters ---------- illuminant Illuminant to plot. ``illuminant`` can be of any type or form supported by the :func:`colour.plotting.filter_illuminants` definition. cmfs Standard observer colour matching functions used for computing the spectrum domain and colours. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_single_sd`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. References ---------- :cite:`Spiker2015a` Examples -------- >>> plot_single_illuminant_sd('A') # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Single_Illuminant_SD.png :align: center :alt: plot_single_illuminant_sd """ cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) title = f"Illuminant {illuminant} - {cmfs.strict_name}" illuminant = first_item(filter_illuminants(illuminant).values()) settings: Dict[str, Any] = {"title": title, "y_label": "Relative Power"} settings.update(kwargs) return plot_single_sd(illuminant, **settings)
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 plot_single_cmfs( cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot given colour matching functions. Parameters ---------- cmfs Colour matching functions to plot. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_multi_cmfs`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> plot_single_cmfs('CIE 1931 2 Degree Standard Observer') ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Single_CMFS.png :align: center :alt: plot_single_cmfs """ cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) settings: Dict[str, Any] = { "title": f"{cmfs.strict_name} - Colour Matching Functions" } settings.update(kwargs) return plot_multi_cmfs((cmfs, ), **settings)
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 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 chromatic_strength_function( theta: FloatingOrArrayLike, ) -> FloatingOrNDArray: """ Define the chromatic strength function :math:`E_s(\\theta)` used to correct saturation scale as function of hue angle :math:`\\theta` in degrees. Parameters ---------- theta Hue angle :math:`\\theta` in degrees. Returns ------- :class:`numpy.floating` or :class:`numpy.ndarray` Corrected saturation scale. Examples -------- >>> h = 257.52322689806243 >>> chromatic_strength_function(h) # doctest: +ELLIPSIS 1.2267869... """ theta = np.radians(theta) E_s = cast(NDArray, 0.9394) E_s += -0.2478 * np.sin(1 * theta) E_s += -0.0743 * np.sin(2 * theta) E_s += +0.0666 * np.sin(3 * theta) E_s += -0.0186 * np.sin(4 * theta) E_s += -0.0055 * np.cos(1 * theta) E_s += -0.0521 * np.cos(2 * theta) E_s += -0.0573 * np.cos(3 * theta) E_s += -0.0061 * np.cos(4 * theta) return as_float(E_s)
def plot_colour_vector_graphic( specification: ColourQuality_Specification_ANSIIESTM3018, **kwargs: Any) -> Tuple[plt.Figure, plt.Axes]: """ Plot *Color Vector Graphic* according to *ANSI/IES TM-30-18 Colour Rendition Report*. Parameters ---------- specification *ANSI/IES TM-30-18 Colour Rendition Report* specification. 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 >>> from colour.quality import colour_fidelity_index_ANSIIESTM3018 >>> sd = SDS_ILLUMINANTS['FL2'] >>> specification = colour_fidelity_index_ANSIIESTM3018(sd, True) >>> plot_colour_vector_graphic(specification) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) """ settings: Dict[str, Any] = dict(kwargs) settings["standalone"] = False # Background background_image = read_image( os.path.join(RESOURCES_DIRECTORY_ANSIIESTM3018, "CVG_Background.jpg")) _figure, axes = plot_image( background_image, imshow_kwargs={"extent": [-1.5, 1.5, -1.5, 1.5]}, **settings, ) # Lines dividing the hues in 16 equal parts along with bin numbers. axes.plot(0, 0, "+", color="#A6A6A6") for i in range(16): angle = 2 * np.pi * i / 16 dx = np.cos(angle) dy = np.sin(angle) axes.plot( (0.15 * dx, 1.5 * dx), (0.15 * dy, 1.5 * dy), "--", color="#A6A6A6", lw=0.75, zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line, ) angle = 2 * np.pi * (i + 0.5) / 16 axes.annotate( str(i + 1), color="#A6A6A6", ha="center", va="center", xy=(1.41 * np.cos(angle), 1.41 * np.sin(angle)), weight="bold", size=9, zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_annotation, ) # Circles. circle = plt.Circle( (0, 0), 1, color="black", lw=1.25, fill=False, zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_polygon, ) axes.add_artist(circle) for radius in [0.8, 0.9, 1.1, 1.2]: circle = plt.Circle( (0, 0), radius, color="white", lw=0.75, fill=False, zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_polygon, ) axes.add_artist(circle) # -/+20% marks near the white circles. props = dict(ha="right", color="white", size=7) axes.annotate( "-20%", xy=(0, -0.8), va="bottom", zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_annotation, **props, ) axes.annotate( "+20%", xy=(0, -1.2), va="top", zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_annotation, **props, ) # Average "CAM02" h correlate for each bin, in radians. average_hues = np.radians([ np.mean( as_float_array([ cast( Floating, specification.colorimetry_data[1][i].CAM.h, ) for i in specification.bins[j] ])) for j in range(16) ]) xy_reference = np.transpose( np.vstack([np.cos(average_hues), np.sin(average_hues)])) # Arrow offsets as defined by the standard. offsets = (specification.averages_test - specification.averages_reference ) / specification.average_norms[:, np.newaxis] xy_test = xy_reference + offsets # Arrows. for i in range(16): axes.arrow( xy_reference[i, 0], xy_reference[i, 1], offsets[i, 0], offsets[i, 1], length_includes_head=True, width=0.005, head_width=0.04, linewidth=None, color=_COLOURS_BIN_ARROW[i], zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_annotation, ) # Red (test) gamut shape. loop = np.append(xy_test, xy_test[0, np.newaxis], axis=0) axes.plot( loop[:, 0], loop[:, 1], "-", color="#F05046", lw=2, zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line, ) def corner_label_and_text(label: str, text: str, ha: str, va: str): """Draw a label and text in given corner.""" x = -1.45 if ha == "left" else 1.45 y = 1.45 if va == "top" else -1.45 y_text = -15 if va == "top" else 15 axes.annotate( text, xy=(x, y), color="black", ha=ha, va=va, weight="bold", size="larger", zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_label, ) axes.annotate( label, xy=(x, y), color="black", xytext=(0, y_text), textcoords="offset points", ha=ha, va=va, size="small", zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_label, ) corner_label_and_text("$R_f$", f"{specification.R_f:.0f}", "left", "top") corner_label_and_text("$R_g$", f"{specification.R_g:.0f}", "right", "top") corner_label_and_text("CCT", f"{specification.CCT:.0f} K", "left", "bottom") corner_label_and_text("$D_{uv}$", f"{specification.D_uv:.4f}", "right", "bottom") settings = {"standalone": True} settings.update(kwargs) return render(**settings)
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 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_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 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 extract_colour_checkers_segmentation(image: ArrayLike, **kwargs: Any) -> Tuple[NDArray, ...]: """ Extract the colour checkers sub-images in given image using segmentation. Parameters ---------- image Image to extract the colours checkers sub-images from. Other Parameters ---------------- aspect_ratio Colour checker aspect ratio, e.g. 1.5. aspect_ratio_minimum Minimum colour checker aspect ratio for detection: projective geometry might reduce the colour checker aspect ratio. aspect_ratio_maximum Maximum colour checker aspect ratio for detection: projective geometry might increase the colour checker aspect ratio. swatches Colour checker swatches total count. swatches_horizontal Colour checker swatches horizontal columns count. swatches_vertical Colour checker swatches vertical row count. swatches_count_minimum Minimum swatches count to be considered for the detection. swatches_count_maximum Maximum swatches count to be considered for the detection. swatches_chromatic_slice A `slice` instance defining chromatic swatches used to detect if the colour checker is upside down. swatches_achromatic_slice A `slice` instance defining achromatic swatches used to detect if the colour checker is upside down. swatch_minimum_area_factor Swatch minimum area factor :math:`f` with the minimum area :math:`m_a` expressed as follows: :math:`m_a = image_w * image_h / s_c / f` where :math:`image_w`, :math:`image_h` and :math:`s_c` are respectively the image width, height and the swatches count. swatch_contour_scale As the image is filtered, the swatches area will tend to shrink, the generated contours can thus be scaled. cluster_contour_scale As the swatches are clustered, it might be necessary to adjust the cluster scale so that the masks are centred better on the swatches. working_width Size the input image is resized to for detection. fast_non_local_means_denoising_kwargs Keyword arguments for :func:`cv2.fastNlMeansDenoising` definition. adaptive_threshold_kwargs Keyword arguments for :func:`cv2.adaptiveThreshold` definition. interpolation_method Interpolation method used when resizing the images, `cv2.INTER_CUBIC` and `cv2.INTER_LINEAR` methods are recommended. Returns ------- :class:`tuple` Tuple of colour checkers sub-images. Examples -------- >>> import os >>> from colour import read_image >>> from colour_checker_detection import TESTS_RESOURCES_DIRECTORY >>> path = os.path.join(TESTS_RESOURCES_DIRECTORY, ... 'colour_checker_detection', 'detection', ... 'IMG_1967.png') >>> image = read_image(path) >>> extract_colour_checkers_segmentation(image) ... # doctest: +SKIP (array([[[ 0.17908671, 0.14010708, 0.09243158], [ 0.17805016, 0.13058874, 0.09513047], [ 0.17175764, 0.13128328, 0.08811688], ..., [ 0.15934898, 0.13436384, 0.07479276], [ 0.17178158, 0.13138185, 0.07703256], [ 0.15082785, 0.11866678, 0.07680314]], <BLANKLINE> [[ 0.16597673, 0.13563241, 0.08780421], [ 0.16490564, 0.13110894, 0.08601525], [ 0.16939694, 0.12963502, 0.08783565], ..., [ 0.14708202, 0.12856133, 0.0814603 ], [ 0.16883563, 0.12862256, 0.08452422], [ 0.16781917, 0.12363558, 0.07361614]], <BLANKLINE> [[ 0.16326806, 0.13720085, 0.08925959], [ 0.16014062, 0.13585283, 0.08104862], [ 0.16657823, 0.12889633, 0.08870038], ..., [ 0.14619341, 0.13086307, 0.07367594], [ 0.16302426, 0.13062705, 0.07938427], [ 0.16618022, 0.1266259 , 0.07200021]], <BLANKLINE> ..., [[ 0.1928642 , 0.14578913, 0.11224515], [ 0.18931177, 0.14416392, 0.10288388], [ 0.17707473, 0.1436448 , 0.09188452], ..., [ 0.16879168, 0.12867133, 0.09001681], [ 0.1699731 , 0.1287041 , 0.07616285], [ 0.17137891, 0.129711 , 0.07517841]], <BLANKLINE> [[ 0.19514292, 0.1532704 , 0.10375113], [ 0.18217109, 0.14982903, 0.10452617], [ 0.18830594, 0.1469499 , 0.10896181], ..., [ 0.18234864, 0.12642328, 0.08047272], [ 0.17617388, 0.13000189, 0.06874527], [ 0.17108543, 0.13264084, 0.06309374]], <BLANKLINE> [[ 0.16243187, 0.14983535, 0.08954653], [ 0.155507 , 0.14899652, 0.10273992], [ 0.17993385, 0.1498394 , 0.1099571 ], ..., [ 0.18079454, 0.1253967 , 0.07739887], [ 0.17239226, 0.13181566, 0.07806754], [ 0.17422497, 0.13277327, 0.07513551]]], dtype=float32),) """ image = as_float_array(image, FLOAT_DTYPE_DEFAULT)[..., :3] settings = Structure(**SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC) settings.update(**kwargs) image = adjust_image(image, settings.working_width, settings.interpolation_method) colour_checkers = [] for rectangle in cast( List[NDArray], colour_checkers_coordinates_segmentation(image, **settings), ): colour_checker = crop_and_level_image_with_rectangle( image, cv2.minAreaRect(rectangle), settings.interpolation_method) width, height = (colour_checker.shape[1], colour_checker.shape[0]) if width < height: colour_checker = cv2.rotate(colour_checker, cv2.ROTATE_90_CLOCKWISE) colour_checkers.append(colour_checker) return tuple(colour_checkers)
def XYZ_to_Hunt( XYZ: ArrayLike, XYZ_w: ArrayLike, XYZ_b: ArrayLike, L_A: FloatingOrArrayLike, surround: InductionFactors_Hunt = VIEWING_CONDITIONS_HUNT["Normal Scenes"], L_AS: Optional[FloatingOrArrayLike] = None, CCT_w: Optional[FloatingOrArrayLike] = None, XYZ_p: Optional[ArrayLike] = None, p: Optional[FloatingOrArrayLike] = None, S: Optional[FloatingOrArrayLike] = None, S_w: Optional[FloatingOrArrayLike] = None, helson_judd_effect: Boolean = False, discount_illuminant: Boolean = True, ) -> CAM_Specification_Hunt: """ Compute the *Hunt* colour appearance model correlates. Parameters ---------- XYZ *CIE XYZ* tristimulus values of test sample / stimulus. XYZ_w *CIE XYZ* tristimulus values of reference white. XYZ_b *CIE XYZ* tristimulus values of background. L_A Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`. surround Surround viewing conditions induction factors. L_AS Scotopic luminance :math:`L_{AS}` of the illuminant, approximated if not specified. CCT_w Correlated color temperature :math:`T_{cp}`: of the illuminant, needed to approximate :math:`L_{AS}`. XYZ_p *CIE XYZ* tristimulus values of proximal field, assumed to be equal to background if not specified. p Simultaneous contrast / assimilation factor :math:`p` with value normalised to domain [-1, 0] when simultaneous contrast occurs and normalised to domain [0, 1] when assimilation occurs. S Scotopic response :math:`S` to the stimulus, approximated using tristimulus values :math:`Y` of the stimulus if not specified. S_w Scotopic response :math:`S_w` for the reference white, approximated using the tristimulus values :math:`Y_w` of the reference white if not specified. helson_judd_effect Truth value indicating whether the *Helson-Judd* effect should be accounted for. discount_illuminant Truth value indicating if the illuminant should be discounted. Returns ------- :class:`colour.CAM_Specification_Hunt` *Hunt* colour appearance model specification. Raises ------ ValueError If an illegal argument combination is specified. Notes ----- +------------+-----------------------+---------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +------------+-----------------------+---------------+ | ``XYZ_w`` | [0, 100] | [0, 1] | +------------+-----------------------+---------------+ | ``XYZ_b`` | [0, 100] | [0, 1] | +------------+-----------------------+---------------+ | ``XYZ_p`` | [0, 100] | [0, 1] | +------------+-----------------------+---------------+ +------------------------------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +==============================+=======================+===============+ | ``CAM_Specification_Hunt.h`` | [0, 360] | [0, 1] | +------------------------------+-----------------------+---------------+ References ---------- :cite:`Fairchild2013u`, :cite:`Hunt2004b` Examples -------- >>> XYZ = np.array([19.01, 20.00, 21.78]) >>> XYZ_w = np.array([95.05, 100.00, 108.88]) >>> XYZ_b = np.array([95.05, 100.00, 108.88]) >>> L_A = 318.31 >>> surround = VIEWING_CONDITIONS_HUNT['Normal Scenes'] >>> CCT_w = 6504 >>> XYZ_to_Hunt(XYZ, XYZ_w, XYZ_b, L_A, surround, CCT_w=CCT_w) ... # doctest: +ELLIPSIS CAM_Specification_Hunt(J=30.0462678..., C=0.1210508..., h=269.2737594..., \ s=0.0199093..., Q=22.2097654..., M=0.1238964..., H=None, HC=None) """ XYZ = to_domain_100(XYZ) XYZ_w = to_domain_100(XYZ_w) XYZ_b = to_domain_100(XYZ_b) _X, Y, _Z = tsplit(XYZ) _X_w, Y_w, _Z_w = tsplit(XYZ_w) X_b, Y_b, _Z_b = tsplit(XYZ_b) # Arguments handling. if XYZ_p is not None: X_p, Y_p, Z_p = tsplit(to_domain_100(XYZ_p)) else: X_p = X_b Y_p = Y_b Z_p = Y_b usage_warning('Unspecified proximal field "XYZ_p" argument, using ' 'background "XYZ_b" as approximation!') if surround.N_cb is None: N_cb = 0.725 * spow(Y_w / Y_b, 0.2) usage_warning( f'Unspecified "N_cb" argument, using approximation: "{N_cb}"') if surround.N_bb is None: N_bb = 0.725 * spow(Y_w / Y_b, 0.2) usage_warning( f'Unspecified "N_bb" argument, using approximation: "{N_bb}"') if L_AS is None and CCT_w is None: raise ValueError('Either the scotopic luminance "L_AS" of the ' "illuminant or its correlated colour temperature " '"CCT_w" must be specified!') elif L_AS is None and CCT_w is not None: L_AS = illuminant_scotopic_luminance(L_A, CCT_w) usage_warning( f'Unspecified "L_AS" argument, using approximation from "CCT": ' f'"{L_AS}"') if (S is None and S_w is not None) or (S is not None and S_w is None): raise ValueError('Either both stimulus scotopic response "S" and ' 'reference white scotopic response "S_w" arguments ' "need to be specified or none of them!") elif S is None and S_w is None: S_p = Y S_w_p = Y_w usage_warning( f'Unspecified stimulus scotopic response "S" and reference white ' f'scotopic response "S_w" arguments, using approximation: ' f'"{S}", "{S_w}"') if p is None: usage_warning( 'Unspecified simultaneous contrast / assimilation "p" ' "argument, model will not account for simultaneous chromatic " "contrast!") XYZ_p = tstack([X_p, Y_p, Z_p]) # Computing luminance level adaptation factor :math:`F_L`. F_L = luminance_level_adaptation_factor(L_A) # Computing test sample chromatic adaptation. rgb_a = chromatic_adaptation( XYZ, XYZ_w, XYZ_b, L_A, F_L, XYZ_p, p, helson_judd_effect, discount_illuminant, ) # Computing reference white chromatic adaptation. rgb_aw = chromatic_adaptation( XYZ_w, XYZ_w, XYZ_b, L_A, F_L, XYZ_p, p, helson_judd_effect, discount_illuminant, ) # Computing opponent colour dimensions. # Computing achromatic post adaptation signals. A_a = achromatic_post_adaptation_signal(rgb_a) A_aw = achromatic_post_adaptation_signal(rgb_aw) # Computing colour difference signals. C = colour_difference_signals(rgb_a) C_w = colour_difference_signals(rgb_aw) # ------------------------------------------------------------------------- # Computing the *hue* angle :math:`h_s`. # ------------------------------------------------------------------------- h = hue_angle(C) # hue_w = hue_angle(C_w) # TODO: Implement hue quadrature & composition computation. # ------------------------------------------------------------------------- # Computing the correlate of *saturation* :math:`s`. # ------------------------------------------------------------------------- # Computing eccentricity factors. e_s = eccentricity_factor(h) # Computing low luminance tritanopia factor :math:`F_t`. F_t = low_luminance_tritanopia_factor(L_A) M_yb = yellowness_blueness_response(C, e_s, surround.N_c, N_cb, F_t) M_rg = redness_greenness_response(C, e_s, surround.N_c, N_cb) M_yb_w = yellowness_blueness_response(C_w, e_s, surround.N_c, N_cb, F_t) M_rg_w = redness_greenness_response(C_w, e_s, surround.N_c, N_cb) # Computing overall chromatic response. M = overall_chromatic_response(M_yb, M_rg) M_w = overall_chromatic_response(M_yb_w, M_rg_w) s = saturation_correlate(M, rgb_a) # ------------------------------------------------------------------------- # Computing the correlate of *brightness* :math:`Q`. # ------------------------------------------------------------------------- # Computing achromatic signal :math:`A`. A = achromatic_signal(cast(FloatingOrNDArray, L_AS), S_p, S_w_p, N_bb, A_a) A_w = achromatic_signal(cast(FloatingOrNDArray, L_AS), S_w_p, S_w_p, N_bb, A_aw) Q = brightness_correlate(A, A_w, M, surround.N_b) brightness_w = brightness_correlate(A_w, A_w, M_w, surround.N_b) # TODO: Implement whiteness-blackness :math:`Q_{wb}` computation. # ------------------------------------------------------------------------- # Computing the correlate of *Lightness* :math:`J`. # ------------------------------------------------------------------------- J = lightness_correlate(Y_b, Y_w, Q, brightness_w) # ------------------------------------------------------------------------- # Computing the correlate of *chroma* :math:`C_{94}`. # ------------------------------------------------------------------------- C_94 = chroma_correlate(s, Y_b, Y_w, Q, brightness_w) # ------------------------------------------------------------------------- # Computing the correlate of *colourfulness* :math:`M_{94}`. # ------------------------------------------------------------------------- M_94 = colourfulness_correlate(F_L, C_94) return CAM_Specification_Hunt( J, C_94, as_float(from_range_degrees(h)), s, Q, M_94, None, None, )
def plot_the_blue_sky( cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the blue sky. Parameters ---------- cmfs Standard observer colour matching functions used for computing the spectrum domain and colours. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_single_sd`, :func:`colour.plotting.plot_multi_colour_swatches`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> plot_the_blue_sky() # doctest: +ELLIPSIS (<Figure size ... with 2 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_The_Blue_Sky.png :align: center :alt: plot_the_blue_sky """ figure = plt.figure() figure.subplots_adjust(hspace=CONSTANTS_COLOUR_STYLE.geometry.short / 2) cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) ASTMG173_sd = cast(SpectralDistribution, SD_ASTMG173_ETR.copy()) rayleigh_sd = sd_rayleigh_scattering() ASTMG173_sd.align(rayleigh_sd.shape) sd = rayleigh_sd * ASTMG173_sd axes = figure.add_subplot(211) settings: Dict[str, Any] = { "axes": axes, "title": "The Blue Sky - Synthetic Spectral Distribution", "y_label": "W / m-2 / nm-1", } settings.update(kwargs) settings["standalone"] = False plot_single_sd(sd, cmfs, **settings) axes = figure.add_subplot(212) x_label = ("The sky is blue because molecules in the atmosphere " "scatter shorter wavelengths more than longer ones.\n" "The synthetic spectral distribution is computed as " "follows: " "(ASTM G-173 ETR * Standard Air Rayleigh Scattering).") settings = { "axes": axes, "aspect": None, "title": "The Blue Sky - Colour", "x_label": x_label, "y_label": "", "x_ticker": False, "y_ticker": False, } settings.update(kwargs) settings["standalone"] = False blue_sky_color = XYZ_to_plotting_colourspace( sd_to_XYZ(cast(SpectralDistribution, sd))) figure, axes = plot_single_colour_swatch( ColourSwatch(normalise_maximum(blue_sky_color)), **settings) settings = {"axes": axes, "standalone": True} settings.update(kwargs) return render(**settings)
__maintainer__ = "Colour Developers" __email__ = "*****@*****.**" __status__ = "Production" __all__ = [ "TestPlotSpectraANSIIESTM3018", "TestPlotColourVectorGraphic", "TestPlot16BinBars", "TestPlotLocalChromaShifts", "TestPlotLocalHueShifts", "TestPlotLocalColourFidelities", "TestPlotColourFidelityIndexes", ] SPECIFICATION_ANSIIESTM3018: ColourQuality_Specification_ANSIIESTM3018 = cast( ColourQuality_Specification_ANSIIESTM3018, colour_fidelity_index_ANSIIESTM3018(SDS_ILLUMINANTS["FL2"], True), ) class TestPlotSpectraANSIIESTM3018(unittest.TestCase): """ Define :func:`colour.plotting.tm3018.components. plot_spectra_ANSIIESTM3018` definition unit tests methods. """ def test_plot_spectra_ANSIIESTM3018(self): """ Test :func:`colour.plotting.tm3018.components.\ plot_spectra_ANSIIESTM3018` definition. """
def plot_RGB_colourspace_section( colourspace: Union[RGB_Colourspace, str, Sequence[Union[RGB_Colourspace, str]]], 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, show_section_colours: Boolean = True, show_section_contour: Boolean = True, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot given *RGB* colourspace section colours along given axis and origin. Parameters ---------- 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. 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. show_section_colours Whether to show the hull section colours. show_section_contour Whether to show the hull section contour. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`, :func:`colour.plotting.section.plot_hull_section_colours` :func:`colour.plotting.section.plot_hull_section_contour`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> from colour.utilities import is_trimesh_installed >>> if is_trimesh_installed: ... plot_RGB_colourspace_section( ... 'sRGB', section_colours='RGB', section_opacity=0.15) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_RGB_Colourspace_Section.png :align: center :alt: plot_RGB_colourspace_section """ import trimesh settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) colourspace = cast( RGB_Colourspace, first_item(filter_RGB_colourspaces(colourspace).values()), ) vertices, faces, _outline = primitive_cube(1, 1, 1, 64, 64, 64) XYZ_vertices = RGB_to_XYZ( vertices["position"] + 0.5, colourspace.whitepoint, colourspace.whitepoint, colourspace.matrix_RGB_to_XYZ, ) hull = trimesh.Trimesh(XYZ_vertices, faces, process=False) if show_section_colours: settings = {"axes": axes} settings.update(kwargs) settings["standalone"] = False plot_hull_section_colours(hull, model, axis, origin, normalise, **settings) if show_section_contour: settings = {"axes": axes} settings.update(kwargs) settings["standalone"] = False plot_hull_section_contour(hull, model, axis, origin, normalise, **settings) title = (f"{colourspace.name} Section - " f"{f'{origin * 100}%' if normalise else origin} - " f"{model}") plane = MAPPING_AXIS_TO_PLANE[axis] labels = np.array(COLOURSPACE_MODELS_AXIS_LABELS[model])[as_int_array( colourspace_model_axis_reorder([0, 1, 2], model))] x_label, y_label = labels[plane[0]], labels[plane[1]] settings.update({ "axes": axes, "standalone": True, "title": title, "x_label": x_label, "y_label": y_label, }) settings.update(kwargs) return render(**settings)
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_sds_colour_rendering_indexes_bars( sds: Union[ Sequence[Union[SpectralDistribution, MultiSpectralDistributions]], MultiSpectralDistributions, ], **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the *Colour Rendering Index* (CRI) of given illuminants or light sources spectral distributions. Parameters ---------- sds Spectral distributions or multi-spectral distributions to plot. `sds` can be a single :class:`colour.MultiSpectralDistributions` class instance, a list of :class:`colour.MultiSpectralDistributions` class instances or a list of :class:`colour.SpectralDistribution` class instances. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.quality.plot_colour_quality_bars`, :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, ... SDS_LIGHT_SOURCES) >>> illuminant = SDS_ILLUMINANTS['FL2'] >>> light_source = SDS_LIGHT_SOURCES['Kinoton 75P'] >>> plot_multi_sds_colour_rendering_indexes_bars( ... [illuminant, light_source]) # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_\ Plot_Multi_SDS_Colour_Rendering_Indexes_Bars.png :align: center :alt: plot_multi_sds_colour_rendering_indexes_bars """ sds_converted = sds_and_msds_to_sds(sds) settings: Dict[str, Any] = dict(kwargs) settings.update({"standalone": False}) specifications = cast( List[ColourRendering_Specification_CRI], [ colour_rendering_index(sd, additional_data=True) for sd in sds_converted ], ) # *colour rendering index* colorimetry data tristimulus values are # computed in [0, 100] domain however `plot_colour_quality_bars` expects # [0, 1] domain. As we want to keep `plot_colour_quality_bars` definition # agnostic from the colour quality data, we update the test sd # colorimetry data tristimulus values domain. for specification in specifications: colorimetry_data = specification.colorimetry_data for i in range(len(colorimetry_data[0])): colorimetry_data[0][i].XYZ /= 100 _figure, axes = plot_colour_quality_bars(specifications, **settings) title = ( f"Colour Rendering Index - " f"{', '.join([sd.strict_name for sd in sds_converted])}" ) settings = {"axes": axes, "title": title} settings.update(kwargs) return render(**settings)
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_single_sd_rayleigh_scattering( CO2_concentration: FloatingOrArrayLike = CONSTANT_STANDARD_CO2_CONCENTRATION, temperature: FloatingOrArrayLike = CONSTANT_STANDARD_AIR_TEMPERATURE, pressure: FloatingOrArrayLike = CONSTANT_AVERAGE_PRESSURE_MEAN_SEA_LEVEL, latitude: FloatingOrArrayLike = CONSTANT_DEFAULT_LATITUDE, altitude: FloatingOrArrayLike = CONSTANT_DEFAULT_ALTITUDE, cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot a single *Rayleigh* scattering spectral distribution. Parameters ---------- CO2_concentration :math:`CO_2` concentration in parts per million (ppm). temperature Air temperature :math:`T[K]` in kelvin degrees. pressure Surface pressure :math:`P` of the measurement site. latitude Latitude of the site in degrees. altitude Altitude of the site in meters. cmfs Standard observer colour matching functions used for computing the spectrum domain and colours. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_single_sd`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> plot_single_sd_rayleigh_scattering() # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Single_SD_Rayleigh_Scattering.png :align: center :alt: plot_single_sd_rayleigh_scattering """ title = "Rayleigh Scattering" cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) settings: Dict[str, Any] = {"title": title, "y_label": "Optical Depth"} settings.update(kwargs) sd = sd_rayleigh_scattering( cmfs.shape, CO2_concentration, temperature, pressure, latitude, altitude, ) return plot_single_sd(sd, **settings)
def plot_multi_sds_colour_quality_scales_bars( sds: Union[ Sequence[Union[SpectralDistribution, MultiSpectralDistributions]], MultiSpectralDistributions, ], method: Union[ Literal["NIST CQS 7.4", "NIST CQS 9.0"], str ] = "NIST CQS 9.0", **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the *Colour Quality Scale* (CQS) of given illuminants or light sources spectral distributions. Parameters ---------- sds Spectral distributions or multi-spectral distributions to plot. `sds` can be a single :class:`colour.MultiSpectralDistributions` class instance, a list of :class:`colour.MultiSpectralDistributions` class instances or a list of :class:`colour.SpectralDistribution` class instances. method *Colour Quality Scale* (CQS) computation method. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.quality.plot_colour_quality_bars`, :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, ... SDS_LIGHT_SOURCES) >>> illuminant = SDS_ILLUMINANTS['FL2'] >>> light_source = SDS_LIGHT_SOURCES['Kinoton 75P'] >>> plot_multi_sds_colour_quality_scales_bars([illuminant, light_source]) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_\ Plot_Multi_SDS_Colour_Quality_Scales_Bars.png :align: center :alt: plot_multi_sds_colour_quality_scales_bars """ method = validate_method(method, COLOUR_QUALITY_SCALE_METHODS) sds_converted = sds_and_msds_to_sds(sds) settings: Dict[str, Any] = dict(kwargs) settings.update({"standalone": False}) specifications = cast( List[ColourRendering_Specification_CQS], [colour_quality_scale(sd, True, method) for sd in sds_converted], ) _figure, axes = plot_colour_quality_bars(specifications, **settings) title = ( f"Colour Quality Scale - " f"{', '.join([sd.strict_name for sd in sds_converted])}" ) settings = {"axes": axes, "title": title} settings.update(kwargs) return render(**settings)
def colour_fidelity_index_ANSIIESTM3018( sd_test: SpectralDistribution, additional_data: Boolean = False ) -> Union[Floating, ColourQuality_Specification_ANSIIESTM3018, ColourRendering_Specification_CIE2017, ]: """ Return the *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI) :math:`R_f` of given spectral distribution. Parameters ---------- sd_test Test spectral distribution. additional_data Whether to output additional data. Returns ------- :class:`numpy.floating` or \ :class:`colour.quality.ColourQuality_Specification_ANSIIESTM3018` *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI). References ---------- :cite:`ANSI2018` Examples -------- >>> from colour import SDS_ILLUMINANTS >>> sd = SDS_ILLUMINANTS['FL2'] >>> colour_fidelity_index_ANSIIESTM3018(sd) # doctest: +ELLIPSIS 70.1208254... """ if not additional_data: return colour_fidelity_index_CIE2017(sd_test, False) specification: ( ColourRendering_Specification_CIE2017) = colour_fidelity_index_CIE2017( sd_test, True) # type: ignore[assignment] # Setup bins based on where the reference a'b' points are located. bins: List[List[int]] = [[] for _i in range(16)] for i, sample in enumerate(specification.colorimetry_data[1]): bin_index = as_int_scalar(np.floor( cast(Floating, sample.CAM.h) / 22.5)) bins[bin_index].append(i) # Per-bin a'b' averages. averages_test = np.empty([16, 2]) averages_reference = np.empty([16, 2]) for i in range(16): apbp_s = [ specification.colorimetry_data[0][j].Jpapbp[[1, 2]] for j in bins[i] ] averages_test[i, :] = np.mean(apbp_s, axis=0) apbp_s = [ specification.colorimetry_data[1][j].Jpapbp[[1, 2]] for j in bins[i] ] averages_reference[i, :] = np.mean(apbp_s, axis=0) # Gamut Index. R_g = 100 * (averages_area(averages_test) / averages_area(averages_reference)) # Local colour fidelity indexes, i.e. 16 CFIs for each bin. bin_delta_E_s = [ np.mean([specification.delta_E_s[bins[i]]]) for i in range(16) ] R_fs = as_float_array(delta_E_to_R_f(bin_delta_E_s)) # Angles bisecting the hue bins. angles = (22.5 * np.arange(16) + 11.25) / 180 * np.pi cosines = np.cos(angles) sines = np.sin(angles) average_norms = np.linalg.norm(averages_reference, axis=1) a_deltas = averages_test[:, 0] - averages_reference[:, 0] b_deltas = averages_test[:, 1] - averages_reference[:, 1] # Local chromaticity shifts, multiplied by 100 to obtain percentages. R_cs = 100 * (a_deltas * cosines + b_deltas * sines) / average_norms # Local hue shifts. R_hs = (-a_deltas * sines + b_deltas * cosines) / average_norms return ColourQuality_Specification_ANSIIESTM3018( specification.name, sd_test, specification.sd_reference, specification.R_f, specification.R_s, specification.CCT, specification.D_uv, specification.colorimetry_data, R_g, bins, averages_test, averages_reference, average_norms, R_fs, R_cs, R_hs, )
def plot_visible_spectrum_section( cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", illuminant: Union[SpectralDistribution, str] = "D65", 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, show_section_colours: Boolean = True, show_section_contour: Boolean = True, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the visible spectrum volume, i.e. *Rösch-MacAdam* colour solid, section colours along given axis and origin. Parameters ---------- cmfs Standard observer colour matching functions, default to the *CIE 1931 2 Degree Standard Observer*. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. illuminant Illuminant spectral distribution, default to *CIE Illuminant D65*. ``illuminant`` can be of any type or form supported by the :func:`colour.plotting.filter_illuminants` definition. 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. show_section_colours Whether to show the hull section colours. show_section_contour Whether to show the hull section contour. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.render`, :func:`colour.plotting.section.plot_hull_section_colours` :func:`colour.plotting.section.plot_hull_section_contour`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> from colour.utilities import is_trimesh_installed >>> if is_trimesh_installed: ... plot_visible_spectrum_section( ... section_colours='RGB', section_opacity=0.15) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Visible_Spectrum_Section.png :align: center :alt: plot_visible_spectrum_section """ import trimesh settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) # pylint: disable=E1102 cmfs = reshape_msds(first_item(filter_cmfs(cmfs).values()), SpectralShape(360, 780, 1)) illuminant = cast( SpectralDistribution, first_item(filter_illuminants(illuminant).values()), ) vertices = solid_RoschMacAdam( cmfs, illuminant, point_order="Pulse Wave Width", filter_jagged_points=True, ) mesh = trimesh.Trimesh(vertices) hull = trimesh.convex.convex_hull(mesh) if show_section_colours: settings = {"axes": axes} settings.update(kwargs) settings["standalone"] = False plot_hull_section_colours(hull, model, axis, origin, normalise, **settings) if show_section_contour: settings = {"axes": axes} settings.update(kwargs) settings["standalone"] = False plot_hull_section_contour(hull, model, axis, origin, normalise, **settings) title = (f"Visible Spectrum Section - " f"{f'{origin * 100}%' if normalise else origin} - " f"{model} - " f"{cmfs.strict_name}") plane = MAPPING_AXIS_TO_PLANE[axis] labels = np.array(COLOURSPACE_MODELS_AXIS_LABELS[model])[as_int_array( colourspace_model_axis_reorder([0, 1, 2], model))] x_label, y_label = labels[plane[0]], labels[plane[1]] settings.update({ "axes": axes, "standalone": True, "title": title, "x_label": x_label, "y_label": y_label, }) settings.update(kwargs) return render(**settings)
def as_8_bit_BGR_image(image: ArrayLike) -> NDArray: """ Convert and encodes given linear float *RGB* image to 8-bit *BGR* with *sRGB* reverse OETF. Parameters ---------- image Image to convert. Returns ------- :class:`numpy.ndarray` Converted image. Notes ----- - In the eventuality where the image is already an integer array, the conversion is by-passed. Examples -------- >>> from colour.algebra import random_triplet_generator >>> prng = np.random.RandomState(4) >>> image = list(random_triplet_generator(8, random_state=prng)) >>> image = np.reshape(image, [4, 2, 3]) >>> print(image) [[[ 0.96702984 0.25298236 0.0089861 ] [ 0.54723225 0.43479153 0.38657128]] <BLANKLINE> [[ 0.97268436 0.77938292 0.04416006] [ 0.71481599 0.19768507 0.95665297]] <BLANKLINE> [[ 0.69772882 0.86299324 0.43614665] [ 0.2160895 0.98340068 0.94897731]] <BLANKLINE> [[ 0.97627445 0.16384224 0.78630599] [ 0.00623026 0.59733394 0.8662893 ]]] >>> image = as_8_bit_BGR_image(image) >>> print(image) [[[ 23 137 251] [167 176 195]] <BLANKLINE> [[ 59 228 251] [250 122 219]] <BLANKLINE> [[176 238 217] [249 253 128]] <BLANKLINE> [[229 112 252] [239 203 18]]] >>> as_8_bit_BGR_image(image) array([[[ 23, 137, 251], [167, 176, 195]], <BLANKLINE> [[ 59, 228, 251], [250, 122, 219]], <BLANKLINE> [[176, 238, 217], [249, 253, 128]], <BLANKLINE> [[229, 112, 252], [239, 203, 18]]], dtype=uint8) """ image = np.asarray(image)[..., :3] if image.dtype == np.uint8: return image return cv2.cvtColor( cast(NDArray, cctf_encoding(image) * 255).astype(np.uint8), cv2.COLOR_RGB2BGR, )
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 adjust_image( image: ArrayLike, target_width: Integer, interpolation_method: Literal[ # type: ignore[misc] cv2.INTER_AREA, cv2.INTER_BITS, cv2.INTER_BITS2, cv2.INTER_CUBIC, cv2.INTER_LANCZOS4, cv2.INTER_LINEAR, ] = cv2.INTER_CUBIC, ) -> NDArray: """ Adjust given image so that it is horizontal and resizes it to given target width. Parameters ---------- image Image to adjust. target_width Width the image is resized to. interpolation_method Interpolation method. Returns ------- :class:`numpy.ndarray` Resized image. Examples -------- >>> from colour.algebra import random_triplet_generator >>> prng = np.random.RandomState(4) >>> image = list(random_triplet_generator(8, random_state=prng)) >>> image = np.reshape(image, [2, 4, 3]) >>> adjust_image(image, 5) # doctest: +ELLIPSIS array([[[ 0.9925325..., 0.2419374..., -0.0139522...], [ 0.6174497..., 0.3460756..., 0.3189758...], [ 0.7447774..., 0.678666 ..., 0.1652180...], [ 0.9476452..., 0.6550805..., 0.2609945...], [ 0.6991505..., 0.1623470..., 1.0120867...]], <BLANKLINE> [[ 0.7269885..., 0.8556784..., 0.4049920...], [ 0.2666565..., 1.0401633..., 0.8238320...], [ 0.6419699..., 0.5442698..., 0.9082211...], [ 0.7894426..., 0.1944301..., 0.7906868...], [-0.0526997..., 0.6236685..., 0.8711483...]]], dtype=float32) """ image = as_float_array(image, FLOAT_DTYPE_DEFAULT)[..., :3] width, height = image.shape[1], image.shape[0] if width < height: image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) height, width = width, height ratio = width / target_width if np.allclose(ratio, 1): return cast(NDArray, image) else: return cv2.resize( image, (as_int(target_width), as_int(height / ratio)), interpolation=interpolation_method, )