Problématique de luminosité apparente des montages électroniques

Dans plusieurs montages électroniques, j’ai pu constaté que la luminosité apparente des LEDs n’est pas linéaire. Je m’explique.

Introduction

Quand on fait varier la puissance électrique transmise à une LED, entre 0 à 100%, elle montre une rapide variation d’éclairement au départ puis une relative stabilisation au-delà de 50%. Il s’agit d’un phénomène de perception. L’œil humain n’a pas une sensibilité linéaire à la lumière blanche du sombre au clair. La Compagnie Internationale de l’Éclairage (CIE) a établi la formule suivante entre la luminosité et la luminance :

L* Luminosité, Y/Yn Luminance relative
d’après Wikipédia

La formule est découpée en deux parties, l’une linéaire à partir de 0 et l’autre exponentielle.

Comment utiliser la formule de la CIE ?

La formule est “à l’envers” pour notre besoin, puisqu’il faut au contraire obtenir une luminescence linéaire (augmentation progressive de l’éclairage apparent) et ainsi compenser l’effet de l’œil. Il faut donc calculer la valeur de Y/Yn (entre 0 et 1), lorsque L* varie de 0 à 100%.

En prenant la partie linéaire de la formule initiale de L* :

$$
\begin{align}
L^* & = 116 \cdot {1\over3} \cdot \left({29\over6}\right)^2 \cdot {Y \over Y_n} \quad \text{pour} \quad {Y \over Y_n} = \left({6\over29}\right)^3 \\
 & = 116 \cdot {1\over3} \cdot \left({29\over6}\right)^2 \cdot \left({6\over29}\right)^3 \\
 & = 116 \cdot {1\over3} \cdot {6\over29} = \frac{116 \cdot 6}{3 \cdot 29} = 8
\end{align}
$$

On aura donc la formule linéaire entre 0 et 8%, puis l’autre entre 8 et 100%. Il ne reste donc plus qu’à les inverser toutes les deux.

$$
L^* = 116 \cdot {1\over3} \cdot \left({29\over6}\right)^2 \cdot {Y \over Y_n} \\
\iff {Y \over Y_n} = L^* \cdot {1\over116} \cdot 3 \cdot \left({6\over29}\right)^2 = L^* \cdot {27\over24389} \\
\iff {Y \over Y_n} \approx L^* \cdot 1.1070565\cdot10^{-3}
$$

Pour la partie linéaire, le résultat est un coefficient assez faible, sachant donc que L* ne peut varier qu’entre 0 et 8. Il ne faut pas oublier que résultat est un ratio entre [0..1].

$$
L^* = 116 \cdot \left( {Y \over Y_n} \right)^{1\over3} – 16 \\
\iff \left({Y \over Y_n}\right)^{1\over3} = { {L^* + 16} \over 116} \\
\iff {Y \over Y_n} = {\left( { {L^* + 16} \over 116} \right)}^3 = {{(L^* + 16)^3} \over 1560896}
$$

Je ne vais pas plus loin dans le développement de (L*+16)3 car ça ne pas faire gagner grand chose dans les calculs, au contraire. Pour finir, vous pourrez vérifier que les 2 formules donnent bien le même résultat quand L* = 8.

Le code

Extrait 1

Le résultat de nos précédents calculs nous donne le code suivant :

float Y(const float& l) {
  if (l <= 8) {
    return l * 27 / 24389.0 ;
  } else {
    return pow(l + 16, 3) / 1560896.0;
  }
}

Le code ci-dessus fonctionne et donnera bien les résultats attendus, pour autant que l’on donne à l une valeur décimale entre 1 et 100 et que l’on attende un résultat sous la forme d’une valeur décimale comprise entre 0 et 1.

Extrait 2

Imaginons maintenant que l’on n’ait besoin que d’une valeur entière dans un intervalle défini entre 0 et… MAX_Y, il suffira de multiplier par le résultat précédent par MAX_Y et d’en prendre l’arrondi. Si en plus, cette valeur est constante, on peut alors la passer sous la forme d’un paramètre de modèle (template).

template <int MAX_Y>
unsigned Y(const float& l) {
  if (l <= 8) {
    return round(l * MAX_Y * 27 / 24389.0) ;
  } else {
    return round(pow(l + 16, 3) * MAX_Y / 1560896.0);
  }
}

Mais le programme doit encore traiter des nombres décimaux qui restent difficiles à manipuler pour certains processeurs qui ne sont pas équipés d’opérandes pour effectuer ces calculs.

Extrait 3

Pour continuer dans le sens de l’optimisation, imaginons maintenant que la luminosité demandée n’est pas un nombre décimal entre 1 à 100, mais un entier dans un intervalle constant de 0 à MAX_L. On pourra modifier le code dans ce sens :

template <int MAX_Y, int MAX_L>
unsigned Y(const unsigned l) {
  if (l * MAX_L <= 800) {
    return (2700UL * MAX_Y) * l / (24389UL * MAX_L) ;
  } else {
    const unsigned long long a = l * 100UL + (16 * MAX_L);
    return MAX_Y * a * a * a / (1560896ULL * MAX_L * MAX_L * MAX_L);
  }
}

Voilà, le code ne contient plus que des entiers, long, voir très long même (64 bits), mais les opérations qui restent ne sont plus que des additions, des multiplications et une seule division et majoritairement avec des constantes. Il n’est pas extrêmement plus court en terme de taille du code, mais il l’air beaucoup plus rapide sur un processeur sans capacité de calcul en virgule flottante.

Référence

  • https://fr.wikipedia.org/wiki/Luminosité_(colorimétrie)