Source code for colour_hdri.distortion.vignette

"""
Lens Vignette Characterisation & Correction
===========================================

Defines various objects to correct camera lens vignette:

-   :func:`colour_hdri.distortion.apply_radial_gradient`
-   :func:`colour_hdri.distortion.parabolic_2D_function`
-   :func:`colour_hdri.distortion.hyperbolic_cosine_2D_function`
-   :func:`colour_hdri.distortion.DataVignetteCharacterisation`
-   :func:`colour_hdri.distortion.characterise_vignette_2D_function`
-   :func:`colour_hdri.distortion.correct_vignette_2D_function`
-   :func:`colour_hdri.distortion.characterise_vignette_bivariate_spline`
-   :func:`colour_hdri.distortion.correct_vignette_bivariate_spline`
-   :func:`colour_hdri.distortion.characterise_vignette_RBF`
-   :func:`colour_hdri.distortion.correct_vignette_RBF`
-   :func:`colour_hdri.VIGNETTE_CHARACTERISATION_METHODS`
-   :func:`colour_hdri.characterise_vignette`
-   :func:`colour_hdri.VIGNETTE_CORRECTION_METHODS`
-   :func:`colour_hdri.correct_vignette`

References
----------
-   :cite:`Kordecki2016` : Kordecki, A., Palus, H., & Bal, A. (2016). Practical
    vignetting correction function for digital camera with measurement of surface
    luminance distribution. Signal, Image and Video Processing, 10(8),
    1417-1424. doi:10.1007/s11760-016-0941-2
-   :cite:`WonpilYu2004` : Wonpil Yu. (2004). Practical anti-vignetting methods
    for digital cameras. IEEE Transactions on Consumer Electronics, 50(4),
    975-983. doi:10.1109/TCE.2004.1362487
"""

from __future__ import annotations

from dataclasses import dataclass

import numpy as np
from colour.algebra import (
    LinearInterpolator,
    linear_conversion,
    polar_to_cartesian,
)
from colour.hints import (
    ArrayLike,
    Callable,
    Literal,
    NDArrayFloat,
    Tuple,
    cast,
)
from colour.utilities import (
    CanonicalMapping,
    MixinDataclassIterable,
    as_float_array,
    as_int_array,
    ones,
    tsplit,
    tstack,
    validate_method,
    zeros,
)
from scipy.interpolate import RBFInterpolator, RectBivariateSpline
from scipy.ndimage import center_of_mass, gaussian_filter
from scipy.optimize import curve_fit

__author__ = "Colour Developers"
__copyright__ = "Copyright 2015 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
    "apply_radial_gradient",
    "vignette_principal_point",
    "parabolic_2D_function",
    "hyperbolic_cosine_2D_function",
    "FunctionVignetteCharacterisation",
    "VIGNETTE_CHARACTERISATION_2D_FUNCTIONS",
    "DataVignetteCharacterisation",
    "characterise_vignette_2D_function",
    "correct_vignette_2D_function",
    "characterise_vignette_bivariate_spline",
    "correct_vignette_bivariate_spline",
    "radial_sampling_function",
    "vignette_sampling_coordinates",
    "characterise_vignette_RBF",
    "correct_vignette_RBF",
    "VIGNETTE_CHARACTERISATION_METHODS",
    "characterise_vignette",
    "VIGNETTE_CORRECTION_METHODS",
    "correct_vignette",
]


[docs] def apply_radial_gradient( image: ArrayLike, scale: ArrayLike = (1, 1), offset: ArrayLike = (0.5, 0.5), intensity: float = 1, bias: float = 1, noise: float = 0, ) -> NDArrayFloat: """ Apply a radial gradient on given image. Parameters ---------- image Image to apply the radial gradient onto. scale Radial gradient scale as a ratio of the image height. offset Radial gradient offset from the image center and as a ratio of image dimensions. intensity Radial gradient intensity where a value of 1 produces black at the top and bottom corners. bias Power function applied on the gradient. noise Noise factor. Returns ------- :class:`numpy.ndarray` Image with radial gradient applied. Examples -------- >>> np.around(apply_radial_gradient(np.ones([5, 7])), 3) array([[ 0. , 0.023, 0.212, 0.286, 0.212, 0.023, 0. ], [ 0. , 0.244, 0.511, 0.643, 0.511, 0.244, 0. ], [ 0. , 0.333, 0.667, 1. , 0.667, 0.333, 0. ], [ 0. , 0.244, 0.511, 0.643, 0.511, 0.244, 0. ], [ 0. , 0.023, 0.212, 0.286, 0.212, 0.023, 0. ]]) """ image = as_float_array(np.atleast_3d(image)) scale_x, scale_y = tsplit(scale) offset_x, offset_y = tsplit(offset) height, width = cast(Tuple, image.shape)[0:2] ratio = height / width samples_x = np.linspace(-1, 1, height) samples_x *= (1 / scale_x) * ratio samples_x += offset_x - 0.5 samples_y = np.linspace(-1, 1, width) samples_y *= 1 / scale_y samples_y += offset_y - 0.5 distance = cast( NDArrayFloat, np.sqrt((samples_x**2)[..., None] + (samples_y**2)[None, ...]), ) image *= 1 - distance[..., None] * intensity image **= bias image += np.random.random(image.shape) * noise return np.squeeze(np.nan_to_num(np.clip(image, 0, 1)))
def vignette_principal_point(image: ArrayLike, threshold: float = 0.99) -> NDArrayFloat: """ Return the vignette principal point for given image. Parameters ---------- image Vignette image to return the principal point of. threshold Pixels threshold before finding the vignette principal point. Returns ------- :class:`numpy.ndarray` Vignette principal point. Examples -------- >>> vignette_principal_point( # doctest: +ELLIPSIS ... apply_radial_gradient(np.ones([5, 7, 3])) ... ) array([ 0.4 , 0.4285714...]) """ image = as_float_array(image) shape_x, shape_y, _ = image.shape M = np.median(image, axis=-1) thresholded = zeros(M.shape) thresholded[np.max(M) * threshold < M] = 1 return center_of_mass(thresholded) / as_float_array([shape_x, shape_y])
[docs] def parabolic_2D_function( x_y: Tuple, a_x2: float, a_x1: float, a_x0: float, a_y2: float, a_y1: float, a_y0: float, ): """ Evaluate a parabolic 2D function on given coordinate matrices from coordinate vectors. The parabolic 2D function adopts the following form as given by :cite:`Kordecki2016`: :math:`I_v(x, y) = \\cfrac{1}{2}(a_{x2}x^2 + a_{x1}x + a_{x0}) + \ \\cfrac{1}{2}(a_{y2}y^2 + a_{y1}y + a_{y0})` Parameters ---------- x_y Coordinate matrices from coordinate vectors to evaluate the parabolic 2d function on. The coordinate matrices can be generated with the :func:`numpy.meshgrid` definition. a_x2 Coefficient :math:`a_{x2}` for the parabolic equation. a_x1 Coefficient :math:`a_{x1}` for the parabolic equation. a_x0 Coefficient :math:`a_{x0}` for the parabolic equation. a_y2 Coefficient :math:`a_{y2}` for the parabolic equation. a_y1 Coefficient :math:`a_{y1}` for the parabolic equation. a_y0 Coefficient :math:`a_{y0}` for the parabolic equation. Returns ------- :class:`numpy.ndarray` Coordinate matrices with evaluated parabolic 2D function. References ---------- :cite:`Kordecki2016` Examples -------- >>> x_1, y_1 = np.meshgrid(np.linspace(0, 1, 4), np.linspace(0, 1, 3)) >>> parabolic_2D_function( # doctest: +ELLIPSIS ... (x_1, y_1), -0.5, 0, 1, -0.5, 0, 1 ... ) array([[ 1. , 0.9722222..., 0.8888888..., 0.75 ], [ 0.9375 , 0.9097222..., 0.8263888..., 0.6875 ], [ 0.75 , 0.7222222..., 0.6388888..., 0.5 ]]) """ x, y = x_y I_v = (a_x2 * x**2 + a_x1 * x + a_x0) / 2 I_v += (a_y2 * y**2 + a_y1 * y + a_y0) / 2 return I_v
[docs] def hyperbolic_cosine_2D_function( x_y: Tuple, r_x: float, x_0: float, r_y: float, y_0: float, c: float, ): """ Evaluate a hyperbolic cosine 2D function on given coordinate matrices from coordinate vectors. The hyperbolic cosine 2D function adopts the following form: :math:`I_v(x, y) = 1 - (cosh(r_x * (x - x_0)) * cosh(r_y * (y - y_0))) + c` Parameters ---------- x_y Coordinate matrices from coordinate vectors to evaluate the parabolic 2d function on. The coordinate matrices can be generated with the :func:`numpy.meshgrid` definition. r_x Coefficient :math:`r_x` for the hyperbolic cosine equation. x_0 Coefficient :math:`x_0` for the hyperbolic cosine equation. r_y Coefficient :math:`r_y` for the hyperbolic cosine equation. y_0 Coefficient :math:`y_0` for the hyperbolic cosine equation. c_y Coefficient :math:`c_y` for the hyperbolic cosine equation. c Coefficient :math:`c` for the hyperbolic cosine equation. Returns ------- :class:`numpy.ndarray` Coordinate matrices with evaluated hyperbolic cosine 2D function. References ---------- :cite:`WonpilYu2004` Examples -------- >>> x_1, y_1 = np.meshgrid(np.linspace(0, 1, 4), np.linspace(0, 1, 3)) >>> hyperbolic_cosine_2D_function( # doctest: +ELLIPSIS ... (x_1, y_1), 1, -0.5, 1, -0.5, 1 ... ) array([[ 1. ..., 0.9439281..., 0.7694244..., 0.4569193...], [ 0.8723740..., 0.8091459..., 0.6123710..., 0.2599822...], [ 0.4569193..., 0.3703959..., 0.1011226..., -0.3810978...]]) """ x, y = x_y x = linear_conversion(x, (0, 1), (-0.5, 0.5)) y = linear_conversion(y, (0, 1), (-0.5, 0.5)) I_v = 1 - (np.cosh(r_x * (x - x_0)) * np.cosh(r_y * (y - y_0))) + c return I_v
@dataclass class FunctionVignetteCharacterisation(MixinDataclassIterable): """ Define a vignette characterisation function and the required data for fitting it to an image. Parameters ---------- function Vignette characterisation function. p0 Initial guess for the function fitting, passed to :func:`scipy.optimize.curve_fit` definition. bounds Lower and upper bounds for the function fitting, passed to :func:`scipy.optimize.curve_fit` definition. """ function: Callable p0: NDArrayFloat bounds: NDArrayFloat VIGNETTE_CHARACTERISATION_2D_FUNCTIONS: CanonicalMapping = CanonicalMapping( { "Parabolic": FunctionVignetteCharacterisation( parabolic_2D_function, np.array([0, 0, 1, 0, 0, 1]), np.array( [ (-5.0, -0.5, 0.9, -5.0, -0.5, 0.9), (+0.0, +0.5, 1.1, +0.0, +0.5, 1.1), ] ), ), "Hyperbolic Cosine": FunctionVignetteCharacterisation( hyperbolic_cosine_2D_function, np.array([1, 0, 1, 0, 0]), np.array( [ (0.5, -1.0, 0.5, -1.0, 0.0), (5.0, +0.0, 5.0, +0.0, 1.5), ] ), ), } ) VIGNETTE_CHARACTERISATION_2D_FUNCTIONS.__doc__ = """ Supported vignette characterisation 2D functions. References ---------- :cite:`Kordecki2016`, :cite:`WonpilYu2004` """
[docs] @dataclass class DataVignetteCharacterisation(MixinDataclassIterable): """ Define the data of a vignette characterisation process. Parameters ---------- parameters Vignette characterisation parameters. principal_point Vignette principal point. """ # noqa: D405, D407, D410, D411, D414 parameters: ArrayLike principal_point: ArrayLike
[docs] def characterise_vignette_2D_function( image: ArrayLike, function: Literal["Parabolic", "Hyperbolic Cosine"] | str = "Parabolic", ) -> DataVignetteCharacterisation: """ Characterise the vignette of given image using a given 2D function. Parameters ---------- image Image to characterise the vignette of. function Characterisation function. Returns ------- :class:`DataVignetteCharacterisation` Vignette characterisation. Examples -------- >>> characterise_vignette_2D_function( # doctest: +ELLIPSIS ... apply_radial_gradient(np.ones([5, 7])) ... ) DataVignetteCharacterisation(parameters=array([[-5. , 0.5 , \ 0.9 , -4.4699758..., 0.5 , 0.9 ]]), principal_point=array([ 0.4 , 0.4285714...])) """ image = np.atleast_3d(image) function = validate_method( function, tuple(VIGNETTE_CHARACTERISATION_2D_FUNCTIONS.keys()), '"{0}" function is invalid, it must be one of {1}!', ) ( vignette_characterisation_function, p0, bounds, ) = VIGNETTE_CHARACTERISATION_2D_FUNCTIONS[function].values height, width, channels = image.shape x_1, y_1 = np.meshgrid( np.linspace(0, 1, width), np.linspace(0, 1, height), ) principal_point = vignette_principal_point(image) parameters = [] for i in range(channels): parameters.append( curve_fit( vignette_characterisation_function, ( np.ravel(x_1 - principal_point[0]), np.ravel(y_1 - principal_point[1]), ), np.ravel(np.nan_to_num(image[..., i])), p0=p0, bounds=bounds, )[0] ) return DataVignetteCharacterisation(as_float_array(parameters), principal_point)
[docs] def correct_vignette_2D_function( image: ArrayLike, characterisation_data: DataVignetteCharacterisation, function: Literal["Parabolic", "Hyperbolic Cosine"] | str = "Parabolic", ) -> NDArrayFloat: """ Correct the vignette of given image using given characterisation for a 2D function. Parameters ---------- image Image to correct the vignette of. characterisation_data Vignette characterisation data for given function. function Correction function. Returns ------- :class:`numpy.ndarray` Vignette corrected image. Examples -------- >>> image = apply_radial_gradient(np.ones([5, 7])) >>> characterisation_data = characterise_vignette_2D_function(image) >>> np.around(correct_vignette_2D_function(image, characterisation_data), 3) array([[-0. , 0.122, 0.597, 0.747, 0.781, 1.08 , -0. ], [ 0. , 0.413, 0.676, 0.82 , 0.76 , 0.576, 0. ], [ 0. , 0.468, 0.759, 1.103, 0.838, 0.611, 0. ], [ 0. , 0.439, 0.709, 0.858, 0.801, 0.628, -0. ], [-0. , 0.193, 0.742, 0.913, 1.049, -0.477, -0. ]]) """ image = np.copy(np.atleast_3d(image)) function = validate_method( function, tuple(VIGNETTE_CHARACTERISATION_2D_FUNCTIONS.keys()), '"{0}" function is invalid, it must be one of {1}!', ) vignette_characterisation_function = VIGNETTE_CHARACTERISATION_2D_FUNCTIONS[ function ] parameters, principal_point = characterisation_data.values height, width, channels = image.shape x_1, y_1 = np.meshgrid( np.linspace(0, 1, width), np.linspace(0, 1, height), ) for i in range(channels): image[..., i] /= vignette_characterisation_function.function( (x_1 - principal_point[0], y_1 - principal_point[1]), *parameters[i], ) return np.squeeze(image)
[docs] def characterise_vignette_bivariate_spline( image: ArrayLike, pre_denoise_sigma: float = 6, post_denoise_sigma: float = 1, samples: int = 50, degree: int = 3, ) -> DataVignetteCharacterisation: """ Characterise the vignette of given image using a bivariate spline. Parameters ---------- image Image to characterise the vignette of. pre_denoise_sigma Standard deviation of the gaussian filtering kernel applied on the image. post_denoise_sigma Standard deviation of the gaussian filtering kernel applied on the resampled image at given ``samples`` count. samples Sample count of the resampled image on the long edge. degree Degree of the bivariate spline. Returns ------- :class:`DataVignetteCharacterisation` Vignette characterisation. Examples -------- >>> parameters, principal_point = characterise_vignette_bivariate_spline( ... apply_radial_gradient(np.ones([300, 400])) ... ).values >>> parameters.shape (37, 50, 1) >>> principal_point # doctest: +ELLIPSIS array([ 0.4983333..., 0.49875 ]) """ image = np.copy(np.atleast_3d(image)) principal_point = vignette_principal_point(image) height, width, channels = image.shape ratio = samples / max(height, width) height_n, width_n = int(height * ratio), int(width * ratio) x_1, y_1 = np.linspace(0, 1, height), np.linspace(0, 1, width) x_1_n, y_1_n = np.linspace(0, 1, height_n), np.linspace(0, 1, width_n) # NOTE: Here "parameters" represent a lower resolution version of the # image, i.e. the "I_v" function directly. parameters = zeros((height_n, width_n, channels)) for i in range(channels): image[..., i] = gaussian_filter( image[..., i], pre_denoise_sigma, truncate=pre_denoise_sigma, mode="nearest", ) interpolator = RectBivariateSpline( x_1, y_1, image[..., i], kx=degree, ky=degree ) parameters[..., i] = interpolator(x_1_n, y_1_n) parameters[..., i] = gaussian_filter( parameters[..., i], post_denoise_sigma, truncate=pre_denoise_sigma, mode="nearest", ) return DataVignetteCharacterisation(parameters, principal_point)
[docs] def correct_vignette_bivariate_spline( image: ArrayLike, characterisation_data: DataVignetteCharacterisation, degree: int = 3, ) -> NDArrayFloat: """ Correct the vignette of given image using given characterisation for a bivariate spline. Parameters ---------- image Image to correct the vignette of. characterisation_data Vignette characterisation data for given function. degree Degree of the bivariate spline. Returns ------- :class:`numpy.ndarray` Vignette corrected image. Examples -------- >>> image = apply_radial_gradient(np.ones([5, 7])) >>> characterisation_data = characterise_vignette_bivariate_spline(image) >>> np.around(correct_vignette_bivariate_spline(image, characterisation_data), 3) array([[ 0. , 0.345, 3.059, 4.072, 3.059, 0.345, 0. ], [ 0. , 3.624, 7.304, 9.058, 7.304, 3.624, 0. ], [ 0. , 4.936, 9.481, 14.032, 9.481, 4.936, 0. ], [ 0. , 3.624, 7.304, 9.058, 7.304, 3.624, 0. ], [ 0. , 0.345, 3.059, 4.072, 3.059, 0.345, 0. ]]) """ image = np.copy(np.atleast_3d(image)) parameters, principal_point = characterisation_data.values height, width, channels = image.shape height_I_v, width_I_v, channels_I_v = parameters.shape x_1, y_1 = np.linspace(0, 1, height), np.linspace(0, 1, width) x_I_v, y_I_v = np.linspace(0, 1, height_I_v), np.linspace(0, 1, width_I_v) for i in range(channels): interpolator = RectBivariateSpline( x_I_v, y_I_v, parameters[..., i], kx=degree, ky=degree ) image[..., i] /= interpolator(x_1, y_1) return np.squeeze(image)
def radial_sampling_function( samples_rho: int = 7, samples_phi: int = 21, radius: float = 1, radial_bias: float = 1, ) -> NDArrayFloat: """ Return a series of radial samples. Parameters ---------- samples_rho Sample count along the radial coordinate. samples_phi Sample count along the angular coordinate. radius Sample distribution radius. radial_bias Sample distribution bias, i.e. an exponent affecting the radial distribution. Returns ------- :class:`numpy.ndarray` Radial samples. Examples -------- >>> radial_sampling_function().shape (21, 7, 2) """ rho, phi = np.meshgrid( np.linspace(0, radius, samples_rho) ** radial_bias, np.linspace(-np.pi, np.pi, samples_phi), ) return polar_to_cartesian(tstack([rho, phi])) def vignette_sampling_coordinates( principal_point: ArrayLike = np.array([0.5, 0.5]), aspect_ratio: float = 1, diagonal_samples: int = 10, diagonal_selection: int = 2, edge_samples: int = 10, samples_rho: int = 7, samples_phi: int = 21, radius: float = 0.9, radial_bias: float = 1, ) -> NDArrayFloat: """ Return a series of sampling coordinates appropriate for radial basis function (RBF) interpolation of a vignette function. Parameters ---------- principal_point Principal point of the vignette function to sample. aspect_ratio Aspect ratio of the image storing the vignette function to sample. diagonal_samples Sample count along the diagonals. diagonal_selection Sample count to retain along the diagonals ends. Given a series of 6 ``diagonal_samples`` as follows: `[0, 1, 2, 3, 4, 5]`, a ``diagonal_selection`` of 2 would retain the following samples: `[0, 1, 4, 5]`. edge_samples Sample count along the edges. samples_rho Sample count along the radial coordinate. samples_phi Sample count along the angular coordinate. radius Sample distribution radius. radial_bias Sample distribution bias, i.e. an exponent affecting the radial distribution. Returns ------- :class:`numpy.ndarray` Radial samples. Examples -------- >>> vignette_sampling_coordinates().shape (187, 2) """ principal_point = as_float_array(principal_point) samples = [] diagonal = np.linspace(0, 1, diagonal_samples) diagonal = np.hstack( [diagonal[1:diagonal_selection], diagonal[-diagonal_selection:-1]] ) samples.append(tstack([diagonal, diagonal])) samples.append(tstack([diagonal, 1 - diagonal])) edge = np.linspace(0, 1, edge_samples) samples.append(tstack([edge, zeros(edge_samples)])) samples.append(tstack([edge, ones(edge_samples)])) samples.append(tstack([zeros(edge_samples), edge])[1:-1]) samples.append(tstack([ones(edge_samples), edge])[1:-1]) coordinates = np.vstack(samples) coordinates[..., 0] = LinearInterpolator([0, 0.5, 1], [0, principal_point[0], 1])( coordinates[..., 0] ) coordinates[..., 1] = LinearInterpolator([0, 0.5, 1], [0, principal_point[1], 1])( coordinates[..., 1] ) radial_samples = radial_sampling_function( samples_rho, samples_phi, cast(float, 1 + (np.max(principal_point - 0.5) * 2)), radial_bias, ) # NOTE: Some randomisation is required to avoid a # "LinAlgError: Singular matrix" exception raised by # "scipy.interpolate.RBFInterpolator" definition. radial_samples += ( np.random.default_rng(8).random(radial_samples.shape) - 0.5 ) / 1000 radial_samples = np.reshape(radial_samples / (2 * 1 / radius), [-1, 2]) radial_samples[..., 1] *= aspect_ratio radial_samples += principal_point coordinates = np.vstack([coordinates, radial_samples]) coordinates = coordinates[ np.logical_and( np.all(coordinates >= 0, axis=-1), np.all(coordinates <= 1, axis=-1), ) ] return coordinates
[docs] def characterise_vignette_RBF( image: ArrayLike, denoise_sigma: float = 6 ) -> DataVignetteCharacterisation: """ Characterise the vignette of given image using a series of sampling coordinates appropriate for radial basis function (RBF) interpolation of a vignette function. Parameters ---------- image Image to characterise the vignette of. denoise_sigma Standard deviation of the gaussian filtering kernel applied on the image. Returns ------- :class:`DataVignetteCharacterisation` Vignette characterisation. Examples -------- >>> parameters, principal_point = characterise_vignette_RBF( ... apply_radial_gradient(np.ones([300, 400])) ... ).values >>> parameters.shape (180, 1) >>> principal_point # doctest: +ELLIPSIS array([ 0.4983333..., 0.49875 ]) """ image = np.copy(np.atleast_3d(image)) height, width, channels = image.shape principal_point = vignette_principal_point(image) sampling_coordinates = vignette_sampling_coordinates( principal_point, width / height ) x_indices = as_int_array(sampling_coordinates[..., 0] * (height - 1)) y_indices = as_int_array(sampling_coordinates[..., 1] * (width - 1)) parameters = [] for i in range(channels): filtered = gaussian_filter(image[..., i], denoise_sigma, truncate=denoise_sigma) parameters.append(filtered[x_indices, y_indices]) return DataVignetteCharacterisation(np.transpose(parameters), principal_point)
[docs] def correct_vignette_RBF( image: ArrayLike, characterisation_data: DataVignetteCharacterisation, smoothing: float = 0.001, kernel: Literal[ "linear", "thin_plate_spline", "cubic", "quintic", "multiquadric", "inverse_multiquadric", "inverse_quadratic", "gaussian", ] = "cubic", epsilon: float = 1, ) -> NDArrayFloat: """ Correct the vignette of given image using given characterisation for radial basis function (RBF) interpolation. Parameters ---------- image Image to correct the vignette of. characterisation_data Vignette characterisation data for given function. smoothing Smoothing parameter, see :class:`scipy.interpolate.RBFInterpolator` class. kernel Type of RBF, see :class:`scipy.interpolate.RBFInterpolator` class. epsilon Shape parameter that scales the input to the RBF, see :class:`scipy.interpolate.RBFInterpolator` class. Returns ------- :class:`numpy.ndarray` Vignette corrected image. Examples -------- >>> image = apply_radial_gradient(np.ones([5, 7])) >>> characterisation_data = characterise_vignette_RBF(image) >>> np.around(correct_vignette_RBF(image, characterisation_data), 3) array([[ 0. , 0.091, 0.841, 1.134, 0.841, 0.091, 0. ], [ 0. , 0.967, 2.03 , 2.552, 2.03 , 0.967, 0. ], [ 0. , 1.323, 2.647, 3.97 , 2.647, 1.323, 0. ], [ 0. , 0.967, 2.03 , 2.552, 2.03 , 0.967, 0. ], [ 0. , 0.091, 0.841, 1.134, 0.841, 0.091, 0. ]]) """ image = np.copy(np.atleast_3d(image)) height, width, channels = image.shape parameters, principal_point = characterisation_data.values sampling_coordinates = vignette_sampling_coordinates( principal_point, width / height ) x_1, y_1 = np.meshgrid( np.linspace(0, 1, width), np.linspace(0, 1, height), ) for i in range(channels): interpolator = RBFInterpolator( sampling_coordinates, parameters[..., i], kernel=kernel, smoothing=smoothing, epsilon=epsilon, ) I_v = interpolator(tstack([y_1, x_1]).reshape([-1, 2])).reshape(height, width) image[..., i] /= I_v return np.squeeze(image)
VIGNETTE_CHARACTERISATION_METHODS: CanonicalMapping = CanonicalMapping( { "2D Function": characterise_vignette_2D_function, "Bivariate Spline": characterise_vignette_bivariate_spline, "RBF": characterise_vignette_RBF, } ) VIGNETTE_CHARACTERISATION_METHODS.__doc__ = """ Supported vignette characterisation methods. """
[docs] def characterise_vignette( image: ArrayLike, method: Literal["2D Function", "Bivariate Spline", "RBF"] | str = "RBF", **kwargs, ) -> DataVignetteCharacterisation: """ Characterise the vignette of given image using given method. Parameters ---------- image Image to characterise the vignette of. method Vignette characterisation method. Other Parameters ---------------- function {:func:`colour_hdri.distortion.characterise_vignette_2D_function`}, Characterisation function. pre_denoise_sigma {:func:`colour_hdri.distortion.characterise_vignette_bivariate_spline`}, Standard deviation of the gaussian filtering kernel applied on the image. post_denoise_sigma {:func:`colour_hdri.distortion.characterise_vignette_bivariate_spline`}, Standard deviation of the gaussian filtering kernel applied on the resampled image at given ``samples`` count. samples {:func:`colour_hdri.distortion.characterise_vignette_bivariate_spline`}, Sample count of the resampled image on the long edge. degree {:func:`colour_hdri.distortion.characterise_vignette_bivariate_spline`}, Degree of the bivariate spline. denoise_sigma {:func:`colour_hdri.distortion.characterise_vignette_RBF`}, Standard deviation of the gaussian filtering kernel applied on the image. Returns ------- :class:`DataVignetteCharacterisation` Vignette characterisation. Examples -------- >>> image = apply_radial_gradient(np.ones([300, 400])) >>> parameters, principal_point = characterise_vignette(image).values >>> parameters.shape (180, 1) >>> principal_point # doctest: +ELLIPSIS array([ 0.4983333..., 0.49875 ]) >>> parameters, principal_point = characterise_vignette(image, method="RBF").values >>> parameters.shape (180, 1) >>> principal_point # doctest: +ELLIPSIS array([ 0.4983333..., 0.49875 ]) >>> parameters, principal_point = characterise_vignette( ... image, method="2D Function" ... ).values >>> parameters.shape (1, 6) >>> principal_point # doctest: +ELLIPSIS array([ 0.4983333..., 0.49875 ]) >>> parameters, principal_point = characterise_vignette( ... image, method="Bivariate Spline" ... ).values >>> parameters.shape (37, 50, 1) >>> principal_point # doctest: +ELLIPSIS array([ 0.4983333..., 0.49875 ]) """ method = validate_method(method, tuple(VIGNETTE_CHARACTERISATION_METHODS.keys())) return VIGNETTE_CHARACTERISATION_METHODS[method](image, **kwargs)
VIGNETTE_CORRECTION_METHODS: CanonicalMapping = CanonicalMapping( { "2D Function": correct_vignette_2D_function, "Bivariate Spline": correct_vignette_bivariate_spline, "RBF": correct_vignette_RBF, } ) VIGNETTE_CHARACTERISATION_METHODS.__doc__ = """ Supported vignette correction methods. """
[docs] def correct_vignette( image: ArrayLike, characterisation_data: DataVignetteCharacterisation, method: Literal["2D Function", "Bivariate Spline", "RBF"] | str = "RBF", **kwargs, ) -> NDArrayFloat: """ Correct the vignette of given image using given method. Parameters ---------- image Image to correct the vignette of. characterisation_data Vignette characterisation data for given function. method Vignette characterisation method. Other Parameters ---------------- function {:func:`colour_hdri.distortion.correct_vignette_2D_function`}, Characterisation function. degree {:func:`colour_hdri.distortion.correct_vignette_bivariate_spline`}, Degree of the bivariate spline. smoothing {:func:`colour_hdri.distortion.correct_vignette_RBF`}, Smoothing parameter, see :class:`scipy.interpolate.RBFInterpolator` class. kernel {:func:`colour_hdri.distortion.correct_vignette_RBF`}, Type of RBF, see :class:`scipy.interpolate.RBFInterpolator` class. epsilon {:func:`colour_hdri.distortion.correct_vignette_RBF`}, Shape parameter that scales the input to the RBF, see :class:`scipy.interpolate.RBFInterpolator` class. Returns ------- :class:`numpy.ndarray` Vignette corrected image. Examples -------- >>> image = apply_radial_gradient(np.ones([5, 7])) >>> characterisation_data = characterise_vignette(image) >>> np.around(correct_vignette_RBF(image, characterisation_data), 3) array([[ 0. , 0.091, 0.841, 1.134, 0.841, 0.091, 0. ], [ 0. , 0.967, 2.03 , 2.552, 2.03 , 0.967, 0. ], [ 0. , 1.323, 2.647, 3.97 , 2.647, 1.323, 0. ], [ 0. , 0.967, 2.03 , 2.552, 2.03 , 0.967, 0. ], [ 0. , 0.091, 0.841, 1.134, 0.841, 0.091, 0. ]]) >>> characterisation_data = characterise_vignette(image, method="RBF") >>> np.around(correct_vignette(image, characterisation_data, method="RBF"), 3) array([[ 0. , 0.091, 0.841, 1.134, 0.841, 0.091, 0. ], [ 0. , 0.967, 2.03 , 2.552, 2.03 , 0.967, 0. ], [ 0. , 1.323, 2.647, 3.97 , 2.647, 1.323, 0. ], [ 0. , 0.967, 2.03 , 2.552, 2.03 , 0.967, 0. ], [ 0. , 0.091, 0.841, 1.134, 0.841, 0.091, 0. ]]) >>> characterisation_data = characterise_vignette(image, method="2D Function") >>> np.around( ... correct_vignette(image, characterisation_data, method="2D Function"), ... 3, ... ) array([[-0. , 0.122, 0.597, 0.747, 0.781, 1.08 , -0. ], [ 0. , 0.413, 0.676, 0.82 , 0.76 , 0.576, 0. ], [ 0. , 0.468, 0.759, 1.103, 0.838, 0.611, 0. ], [ 0. , 0.439, 0.709, 0.858, 0.801, 0.628, -0. ], [-0. , 0.193, 0.742, 0.913, 1.049, -0.477, -0. ]]) >>> characterisation_data = characterise_vignette(image, method="Bivariate Spline") >>> np.around( ... correct_vignette(image, characterisation_data, method="Bivariate Spline"), ... 3, ... ) array([[ 0. , 0.345, 3.059, 4.072, 3.059, 0.345, 0. ], [ 0. , 3.624, 7.304, 9.058, 7.304, 3.624, 0. ], [ 0. , 4.936, 9.481, 14.032, 9.481, 4.936, 0. ], [ 0. , 3.624, 7.304, 9.058, 7.304, 3.624, 0. ], [ 0. , 0.345, 3.059, 4.072, 3.059, 0.345, 0. ]]) """ method = validate_method(method, tuple(VIGNETTE_CORRECTION_METHODS.keys())) return VIGNETTE_CORRECTION_METHODS[method](image, characterisation_data, **kwargs)