@Dave a été le premier à poster une réponse à cela (avec un code de travail), et sa réponse a été une source inestimable d' inspiration pour copier et coller sans vergogne . Cet article a commencé comme une tentative d'expliquer et d'affiner la réponse de @ Dave, mais il a depuis évolué pour devenir une réponse à part entière.
Ma méthode est nettement plus rapide. Selon un benchmark jsPerf sur les couleurs RVB générées aléatoirement, l'algorithme de @ Dave s'exécute en 600 ms , tandis que le mien s'exécute en 30 ms . Cela peut certainement avoir une importance, par exemple en temps de chargement, où la vitesse est essentielle.
De plus, pour certaines couleurs, mon algorithme fonctionne mieux:
- Car
rgb(0,255,0)
, @ Dave's produit rgb(29,218,34)
et produitrgb(1,255,0)
- Car
rgb(0,0,255)
, @ Dave's produit rgb(37,39,255)
et le mien produitrgb(5,6,255)
- Car
rgb(19,11,118)
, @ Dave's produit rgb(36,27,102)
et le mien produitrgb(20,11,112)
Démo
"use strict";
class Color {
constructor(r, g, b) { this.set(r, g, b); }
toString() { return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; }
set(r, g, b) {
this.r = this.clamp(r);
this.g = this.clamp(g);
this.b = this.clamp(b);
}
hueRotate(angle = 0) {
angle = angle / 180 * Math.PI;
let sin = Math.sin(angle);
let cos = Math.cos(angle);
this.multiply([
0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
]);
}
grayscale(value = 1) {
this.multiply([
0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value)
]);
}
sepia(value = 1) {
this.multiply([
0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value),
0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value),
0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value)
]);
}
saturate(value = 1) {
this.multiply([
0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value
]);
}
multiply(matrix) {
let newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
let newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
let newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
this.r = newR; this.g = newG; this.b = newB;
}
brightness(value = 1) { this.linear(value); }
contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
linear(slope = 1, intercept = 0) {
this.r = this.clamp(this.r * slope + intercept * 255);
this.g = this.clamp(this.g * slope + intercept * 255);
this.b = this.clamp(this.b * slope + intercept * 255);
}
invert(value = 1) {
this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
}
hsl() { // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
let r = this.r / 255;
let g = this.g / 255;
let b = this.b / 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if(max === min) {
h = s = 0;
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
} h /= 6;
}
return {
h: h * 100,
s: s * 100,
l: l * 100
};
}
clamp(value) {
if(value > 255) { value = 255; }
else if(value < 0) { value = 0; }
return value;
}
}
class Solver {
constructor(target) {
this.target = target;
this.targetHSL = target.hsl();
this.reusedColor = new Color(0, 0, 0); // Object pool
}
solve() {
let result = this.solveNarrow(this.solveWide());
return {
values: result.values,
loss: result.loss,
filter: this.css(result.values)
};
}
solveWide() {
const A = 5;
const c = 15;
const a = [60, 180, 18000, 600, 1.2, 1.2];
let best = { loss: Infinity };
for(let i = 0; best.loss > 25 && i < 3; i++) {
let initial = [50, 20, 3750, 50, 100, 100];
let result = this.spsa(A, a, c, initial, 1000);
if(result.loss < best.loss) { best = result; }
} return best;
}
solveNarrow(wide) {
const A = wide.loss;
const c = 2;
const A1 = A + 1;
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
return this.spsa(A, a, c, wide.values, 500);
}
spsa(A, a, c, values, iters) {
const alpha = 1;
const gamma = 0.16666666666666666;
let best = null;
let bestLoss = Infinity;
let deltas = new Array(6);
let highArgs = new Array(6);
let lowArgs = new Array(6);
for(let k = 0; k < iters; k++) {
let ck = c / Math.pow(k + 1, gamma);
for(let i = 0; i < 6; i++) {
deltas[i] = Math.random() > 0.5 ? 1 : -1;
highArgs[i] = values[i] + ck * deltas[i];
lowArgs[i] = values[i] - ck * deltas[i];
}
let lossDiff = this.loss(highArgs) - this.loss(lowArgs);
for(let i = 0; i < 6; i++) {
let g = lossDiff / (2 * ck) * deltas[i];
let ak = a[i] / Math.pow(A + k + 1, alpha);
values[i] = fix(values[i] - ak * g, i);
}
let loss = this.loss(values);
if(loss < bestLoss) { best = values.slice(0); bestLoss = loss; }
} return { values: best, loss: bestLoss };
function fix(value, idx) {
let max = 100;
if(idx === 2 /* saturate */) { max = 7500; }
else if(idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; }
if(idx === 3 /* hue-rotate */) {
if(value > max) { value = value % max; }
else if(value < 0) { value = max + value % max; }
} else if(value < 0) { value = 0; }
else if(value > max) { value = max; }
return value;
}
}
loss(filters) { // Argument is array of percentages.
let color = this.reusedColor;
color.set(0, 0, 0);
color.invert(filters[0] / 100);
color.sepia(filters[1] / 100);
color.saturate(filters[2] / 100);
color.hueRotate(filters[3] * 3.6);
color.brightness(filters[4] / 100);
color.contrast(filters[5] / 100);
let colorHSL = color.hsl();
return Math.abs(color.r - this.target.r)
+ Math.abs(color.g - this.target.g)
+ Math.abs(color.b - this.target.b)
+ Math.abs(colorHSL.h - this.targetHSL.h)
+ Math.abs(colorHSL.s - this.targetHSL.s)
+ Math.abs(colorHSL.l - this.targetHSL.l);
}
css(filters) {
function fmt(idx, multiplier = 1) { return Math.round(filters[idx] * multiplier); }
return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`;
}
}
$("button.execute").click(() => {
let rgb = $("input.target").val().split(",");
if (rgb.length !== 3) { alert("Invalid format!"); return; }
let color = new Color(rgb[0], rgb[1], rgb[2]);
let solver = new Solver(color);
let result = solver.solve();
let lossMsg;
if (result.loss < 1) {
lossMsg = "This is a perfect result.";
} else if (result.loss < 5) {
lossMsg = "The is close enough.";
} else if(result.loss < 15) {
lossMsg = "The color is somewhat off. Consider running it again.";
} else {
lossMsg = "The color is extremely off. Run it again!";
}
$(".realPixel").css("background-color", color.toString());
$(".filterPixel").attr("style", result.filter);
$(".filterDetail").text(result.filter);
$(".lossDetail").html(`Loss: ${result.loss.toFixed(1)}. <b>${lossMsg}</b>`);
});
.pixel {
display: inline-block;
background-color: #000;
width: 50px;
height: 50px;
}
.filterDetail {
font-family: "Consolas", "Menlo", "Ubuntu Mono", monospace;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input class="target" type="text" placeholder="r, g, b" value="250, 150, 50" />
<button class="execute">Compute Filters</button>
<p>Real pixel, color applied through CSS <code>background-color</code>:</p>
<div class="pixel realPixel"></div>
<p>Filtered pixel, color applied through CSS <code>filter</code>:</p>
<div class="pixel filterPixel"></div>
<p class="filterDetail"></p>
<p class="lossDetail"></p>
Usage
let color = new Color(0, 255, 0);
let solver = new Solver(color);
let result = solver.solve();
let filterCSS = result.css;
Explication
Nous allons commencer par écrire du Javascript.
"use strict";
class Color {
constructor(r, g, b) {
this.r = this.clamp(r);
this.g = this.clamp(g);
this.b = this.clamp(b);
} toString() { return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; }
hsl() { // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
let r = this.r / 255;
let g = this.g / 255;
let b = this.b / 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if(max === min) {
h = s = 0;
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
} h /= 6;
}
return {
h: h * 100,
s: s * 100,
l: l * 100
};
}
clamp(value) {
if(value > 255) { value = 255; }
else if(value < 0) { value = 0; }
return value;
}
}
class Solver {
constructor(target) {
this.target = target;
this.targetHSL = target.hsl();
}
css(filters) {
function fmt(idx, multiplier = 1) { return Math.round(filters[idx] * multiplier); }
return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`;
}
}
Explication:
- La
Color
classe représente une couleur RVB.
- Sa
toString()
fonction renvoie la couleur dans une rgb(...)
chaîne de couleurs CSS .
- Sa
hsl()
fonction renvoie la couleur, convertie en TSL .
- Sa
clamp()
fonction garantit qu'une valeur de couleur donnée est dans les limites (0-255).
- La
Solver
classe tentera de trouver une couleur cible.
- Sa
css()
fonction renvoie un filtre donné dans une chaîne de filtre CSS.
La mise en œuvre grayscale()
, sepia()
etsaturate()
Le cœur des filtres CSS / SVG sont les primitives de filtre , qui représentent des modifications de bas niveau d'une image.
Les filtres grayscale()
, sepia()
et saturate()
sont implémentés par la primative de filtre <feColorMatrix>
, qui effectue une multiplication de matrice entre une matrice spécifiée par le filtre (souvent générée dynamiquement) et une matrice créée à partir de la couleur. Diagramme:
Il y a quelques optimisations que nous pouvons faire ici:
- Le dernier élément de la matrice de couleurs est et sera toujours
1
. Il ne sert à rien de le calculer ou de le stocker.
- Il est inutile de calculer ou de stocker la valeur alpha / transparence (
A
) non plus, puisque nous avons affaire à RVB, pas à RVBA.
- Par conséquent, nous pouvons couper les matrices de filtre de 5x5 à 3x5 et la matrice de couleur de 1x5 à 1x3 . Cela économise un peu de travail.
- Tous les
<feColorMatrix>
filtres laissent les colonnes 4 et 5 sous forme de zéros. Par conséquent, nous pouvons réduire davantage la matrice de filtre à 3x3 .
- Comme la multiplication est relativement simple, il n'est pas nécessaire de faire glisser des bibliothèques mathématiques complexes pour cela. Nous pouvons implémenter nous-mêmes l'algorithme de multiplication matricielle.
La mise en oeuvre:
function multiply(matrix) {
let newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
let newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
let newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
this.r = newR; this.g = newG; this.b = newB;
}
(Nous utilisons des variables temporaires pour conserver les résultats de chaque multiplication de lignes, car nous ne voulons pas que des modifications this.r
, etc. affectent les calculs ultérieurs.)
Maintenant que nous avons mis en place <feColorMatrix>
, nous pouvons mettre en œuvre grayscale()
, sepia()
et saturate()
qui tout simplement l' invoquons avec une matrice de filtre donné:
function grayscale(value = 1) {
this.multiply([
0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value)
]);
}
function sepia(value = 1) {
this.multiply([
0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value),
0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value),
0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value)
]);
}
function saturate(value = 1) {
this.multiply([
0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value,
0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value
]);
}
Exécution hue-rotate()
Le hue-rotate()
filtre est implémenté par <feColorMatrix type="hueRotate" />
.
La matrice de filtre est calculée comme indiqué ci-dessous:
Par exemple, l'élément a 00 serait calculé comme ceci:
Quelques notes:
- L'angle de rotation est exprimé en degrés. Il doit être converti en radians avant d'être passé à
Math.sin()
ou Math.cos()
.
Math.sin(angle)
et Math.cos(angle)
doit être calculé une fois, puis mis en cache.
La mise en oeuvre:
function hueRotate(angle = 0) {
angle = angle / 180 * Math.PI;
let sin = Math.sin(angle);
let cos = Math.cos(angle);
this.multiply([
0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
]);
}
Mettre en œuvre brightness()
etcontrast()
Les filtres brightness()
et contrast()
sont implémentés par <feComponentTransfer>
avec <feFuncX type="linear" />
.
Chaque <feFuncX type="linear" />
élément accepte un attribut de pente et d' interception . Il calcule ensuite chaque nouvelle valeur de couleur via une formule simple:
value = slope * value + intercept
C'est facile à mettre en œuvre:
function linear(slope = 1, intercept = 0) {
this.r = this.clamp(this.r * slope + intercept * 255);
this.g = this.clamp(this.g * slope + intercept * 255);
this.b = this.clamp(this.b * slope + intercept * 255);
}
Une fois que cela est implémenté, brightness()
et contrast()
peut également être implémenté:
function brightness(value = 1) { this.linear(value); }
function contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
Exécution invert()
Le invert()
filtre est implémenté par <feComponentTransfer>
avec <feFuncX type="table" />
.
La spécification stipule:
Dans ce qui suit, C est le composant initial et C ' est le composant remappé; les deux dans l'intervalle fermé [0,1].
Pour "table", la fonction est définie par interpolation linéaire entre les valeurs données dans l'attribut tableValues . Le tableau a n + 1 valeurs (c'est-à-dire, v 0 à v n ) spécifiant les valeurs de début et de fin pour n régions d'interpolation de taille égale. Les interpolations utilisent la formule suivante:
Pour une valeur C trouver k tel que:
k / n ≤ C <(k + 1) / n
Le résultat C ' est donné par:
C '= v k + (C - k / n) * n * (v k + 1 - v k )
Une explication de cette formule:
- Le
invert()
filtre définit cette table: [valeur, 1 - valeur]. C'est tableValues ou v .
- La formule définit n , tel que n + 1 est la longueur de la table. Puisque la longueur de la table est 2, n = 1.
- La formule définit k , avec k et k + 1 étant des index de la table. Puisque la table a 2 éléments, k = 0.
Ainsi, nous pouvons simplifier la formule pour:
C '= v 0 + C * (v 1 - v 0 )
En incorporant les valeurs de la table, nous nous retrouvons avec:
C '= valeur + C * (1 - valeur - valeur)
Encore une simplification:
C '= valeur + C * (1 - 2 * valeur)
La spécification définit C et C ' comme étant des valeurs RVB, dans les limites 0-1 (par opposition à 0-255). En conséquence, nous devons réduire les valeurs avant le calcul et les redimensionner après.
Nous arrivons ainsi à notre implémentation:
function invert(value = 1) {
this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
}
Interlude: l'algorithme de force brute de @ Dave
@ Le code de Dave génère 176 660 combinaisons de filtres, dont:
- 11
invert()
filtres (0%, 10%, 20%, ..., 100%)
- 11
sepia()
filtres (0%, 10%, 20%, ..., 100%)
- 20
saturate()
filtres (5%, 10%, 15%, ..., 100%)
- 73
hue-rotate()
filtres (0deg, 5deg, 10deg, ..., 360deg)
Il calcule les filtres dans l'ordre suivant:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);
Il parcourt ensuite toutes les couleurs calculées. Il s'arrête une fois qu'il a trouvé une couleur générée dans la tolérance (toutes les valeurs RVB sont à moins de 5 unités de la couleur cible).
Cependant, cela est lent et inefficace. Ainsi, je présente ma propre réponse.
Mise en œuvre de SPSA
Tout d'abord, nous devons définir une fonction de perte , qui renvoie la différence entre la couleur produite par une combinaison de filtres et la couleur cible. Si les filtres sont parfaits, la fonction de perte doit renvoyer 0.
Nous mesurerons la différence de couleur comme la somme de deux métriques:
- Différence RVB, car l'objectif est de produire la valeur RVB la plus proche.
- Différence HSL, car de nombreuses valeurs HSL correspondent à des filtres (par exemple, la teinte est en corrélation avec
hue-rotate()
, la saturation est corrélée avec saturate()
, etc.) Ceci guide l'algorithme.
La fonction de perte prendra un argument - un tableau de pourcentages de filtre.
Nous utiliserons l'ordre de filtrage suivant:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg) brightness(e%) contrast(f%);
La mise en oeuvre:
function loss(filters) {
let color = new Color(0, 0, 0);
color.invert(filters[0] / 100);
color.sepia(filters[1] / 100);
color.saturate(filters[2] / 100);
color.hueRotate(filters[3] * 3.6);
color.brightness(filters[4] / 100);
color.contrast(filters[5] / 100);
let colorHSL = color.hsl();
return Math.abs(color.r - this.target.r)
+ Math.abs(color.g - this.target.g)
+ Math.abs(color.b - this.target.b)
+ Math.abs(colorHSL.h - this.targetHSL.h)
+ Math.abs(colorHSL.s - this.targetHSL.s)
+ Math.abs(colorHSL.l - this.targetHSL.l);
}
Nous allons essayer de minimiser la fonction de perte, de sorte que:
loss([a, b, c, d, e, f]) = 0
L' algorithme SPSA ( site Web , plus d'infos , papier , document de mise en œuvre , code de référence ) est très bon à cela. Il a été conçu pour optimiser des systèmes complexes avec des minima locaux, des fonctions de perte bruyante / non linéaire / multivariée, etc. Il a été utilisé pour régler les moteurs d'échecs . Et contrairement à de nombreux autres algorithmes, les articles qui le décrivent sont en fait compréhensibles (bien qu'avec beaucoup d'efforts).
La mise en oeuvre:
function spsa(A, a, c, values, iters) {
const alpha = 1;
const gamma = 0.16666666666666666;
let best = null;
let bestLoss = Infinity;
let deltas = new Array(6);
let highArgs = new Array(6);
let lowArgs = new Array(6);
for(let k = 0; k < iters; k++) {
let ck = c / Math.pow(k + 1, gamma);
for(let i = 0; i < 6; i++) {
deltas[i] = Math.random() > 0.5 ? 1 : -1;
highArgs[i] = values[i] + ck * deltas[i];
lowArgs[i] = values[i] - ck * deltas[i];
}
let lossDiff = this.loss(highArgs) - this.loss(lowArgs);
for(let i = 0; i < 6; i++) {
let g = lossDiff / (2 * ck) * deltas[i];
let ak = a[i] / Math.pow(A + k + 1, alpha);
values[i] = fix(values[i] - ak * g, i);
}
let loss = this.loss(values);
if(loss < bestLoss) { best = values.slice(0); bestLoss = loss; }
} return { values: best, loss: bestLoss };
function fix(value, idx) {
let max = 100;
if(idx === 2 /* saturate */) { max = 7500; }
else if(idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; }
if(idx === 3 /* hue-rotate */) {
if(value > max) { value = value % max; }
else if(value < 0) { value = max + value % max; }
} else if(value < 0) { value = 0; }
else if(value > max) { value = max; }
return value;
}
}
J'ai apporté quelques modifications / optimisations à SPSA:
- Utiliser le meilleur résultat obtenu, au lieu du dernier.
- Réutilisant tous les tableaux (
deltas
, highArgs
, lowArgs
), au lieu de les recréer à chaque itération.
- Utilisation d'un tableau de valeurs pour a , au lieu d'une seule valeur. En effet, tous les filtres sont différents et doivent donc se déplacer / converger à des vitesses différentes.
- Exécution d'une
fix
fonction après chaque itération. Il fixe toutes les valeurs entre 0% et 100%, sauf saturate
(où le maximum est 7500%), brightness
et contrast
(où le maximum est 200%), et hueRotate
(où les valeurs sont enroulées au lieu de serrées).
J'utilise SPSA dans un processus en deux étapes:
- La scène "large", qui tente "d'explorer" l'espace de recherche. Il fera des tentatives limitées de SPSA si les résultats ne sont pas satisfaisants.
- La scène "étroite", qui prend le meilleur résultat de la scène large et tente de la "raffiner". Il utilise des valeurs dynamiques pour A et a .
La mise en oeuvre:
function solve() {
let result = this.solveNarrow(this.solveWide());
return {
values: result.values,
loss: result.loss,
filter: this.css(result.values)
};
}
function solveWide() {
const A = 5;
const c = 15;
const a = [60, 180, 18000, 600, 1.2, 1.2];
let best = { loss: Infinity };
for(let i = 0; best.loss > 25 && i < 3; i++) {
let initial = [50, 20, 3750, 50, 100, 100];
let result = this.spsa(A, a, c, initial, 1000);
if(result.loss < best.loss) { best = result; }
} return best;
}
function solveNarrow(wide) {
const A = wide.loss;
const c = 2;
const A1 = A + 1;
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
return this.spsa(A, a, c, wide.values, 500);
}
Réglage SPSA
Attention: ne jouez pas avec le code SPSA, en particulier avec ses constantes, sauf si vous êtes sûr de savoir ce que vous faites.
Les constantes importantes sont A , a , c , les valeurs initiales, les seuils de nouvelle tentative, les valeurs de max
in fix()
et le nombre d'itérations de chaque étape. Toutes ces valeurs ont été soigneusement ajustées pour produire de bons résultats, et les visser aléatoirement avec elles réduira presque certainement l'utilité de l'algorithme.
Si vous insistez pour le modifier, vous devez mesurer avant de «l'optimiser».
Tout d'abord, appliquez ce patch .
Ensuite, exécutez le code dans Node.js. Après un certain temps, le résultat devrait ressembler à ceci:
Average loss: 3.4768521401985275
Average time: 11.4915ms
Réglez maintenant les constantes à votre guise.
Quelques conseils:
- La perte moyenne doit être d'environ 4. Si elle est supérieure à 4, cela produit des résultats trop éloignés et vous devez régler pour plus de précision. S'il est inférieur à 4, cela fait perdre du temps et vous devez réduire le nombre d'itérations.
- Si vous augmentez / diminuez le nombre d'itérations, ajustez A de manière appropriée.
- Si vous augmentez / diminuez A , ajustez a de manière appropriée.
- Utilisez l'
--debug
indicateur si vous souhaitez voir le résultat de chaque itération.
TL; DR