@ Dave war der erste, der eine Antwort gepostet hat darauf (mit ) veröffentlichte, und seine Antwort war eine unschätzbare Quelle für schamloses Kopieren und Einfügen für mich Inspiration. Dieser Beitrag begann als Versuch, die Antwort von @ Dave zu erklären und zu verfeinern, hat sich aber inzwischen zu einer eigenen Antwort entwickelt.
Meine Methode ist deutlich schneller. Laut einem jsPerf-Benchmark für zufällig generierte RGB-Farben läuft der @ Dave-Algorithmus in 600 ms , während meiner in 30 ms läuft . Dies kann definitiv von Bedeutung sein, beispielsweise in der Ladezeit, in der die Geschwindigkeit kritisch ist.
Außerdem ist mein Algorithmus für einige Farben besser:
- Denn
rgb(0,255,0)
@ Dave's produziert rgb(29,218,34)
und produziertrgb(1,255,0)
- Denn
rgb(0,0,255)
@ Dave's produziert rgb(37,39,255)
und meins produziertrgb(5,6,255)
- Denn
rgb(19,11,118)
@ Dave's produziert rgb(36,27,102)
und meins produziertrgb(20,11,112)
Demo
"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>
Verwendung
let color = new Color(0, 255, 0);
let solver = new Solver(color);
let result = solver.solve();
let filterCSS = result.css;
Erläuterung
Wir beginnen mit dem Schreiben von 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)}%);`;
}
}
Erläuterung:
- Die
Color
Klasse repräsentiert eine RGB-Farbe.
- Seine
toString()
Funktion gibt die Farbe in einer CSS- rgb(...)
Farbzeichenfolge zurück.
- Seine
hsl()
Funktion gibt die in HSL konvertierte Farbe zurück .
- Seine
clamp()
Funktion stellt sicher, dass ein bestimmter Farbwert innerhalb der Grenzen (0-255) liegt.
- Die
Solver
Klasse wird versuchen, nach einer Zielfarbe zu suchen.
- Seine
css()
Funktion gibt einen bestimmten Filter in einer CSS-Filterzeichenfolge zurück.
Die Implementierung grayscale()
, sepia()
undsaturate()
Das Herzstück von CSS / SVG-Filtern sind Filterprimitive , die geringfügige Änderungen an einem Bild darstellen.
Die Filter grayscale()
, sepia()
und saturate()
werden durch die Filter primitiven implementiert <feColorMatrix>
, das führt Matrizenmultiplikation zwischen einer Matrix , die durch die Filter festgelegt (oft dynamisch erzeugt) und eine Matrix von der Farbe erzeugt. Diagramm:
Hier können wir einige Optimierungen vornehmen:
- Das letzte Element der Farbmatrix ist und bleibt
1
. Es macht keinen Sinn, es zu berechnen oder zu speichern.
- Es macht auch keinen Sinn, den Alpha / Transparenz-Wert (
A
) zu berechnen oder zu speichern , da es sich um RGB handelt, nicht um RGBA.
- Daher können wir die Filtermatrizen von 5x5 auf 3x5 und die Farbmatrix von 1x5 auf 1x3 zuschneiden . Das spart ein bisschen Arbeit.
- Alle
<feColorMatrix>
Filter lassen die Spalten 4 und 5 als Nullen. Daher können wir die Filtermatrix weiter auf 3x3 reduzieren .
- Da die Multiplikation relativ einfach ist, müssen hierfür keine komplexen mathematischen Bibliotheken gezogen werden . Wir können den Matrixmultiplikationsalgorithmus selbst implementieren.
Implementierung:
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;
}
(Wir verwenden temporäre Variablen, um die Ergebnisse jeder Zeilenmultiplikation zu speichern, da wir keine Änderungen this.r
usw. an nachfolgenden Berechnungen wünschen .)
Nun , da wir implementiert haben <feColorMatrix>
, können wir umsetzen grayscale()
, sepia()
und saturate()
, die einfach rufen Sie es mit einem bestimmten Filtermatrix:
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
]);
}
Implementierung hue-rotate()
Der hue-rotate()
Filter wird implementiert von <feColorMatrix type="hueRotate" />
.
Die Filtermatrix wird wie folgt berechnet:
Zum Beispiel würde das Element a 00 folgendermaßen berechnet:
Einige Notizen:
- Der Drehwinkel wird in Grad angegeben. Es muss in Bogenmaß umgerechnet werden, bevor es an
Math.sin()
oder übergeben wird Math.cos()
.
Math.sin(angle)
und Math.cos(angle)
sollte einmal berechnet und dann zwischengespeichert werden.
Implementierung:
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
]);
}
Implementierung brightness()
undcontrast()
Die brightness()
und contrast()
Filter werden von <feComponentTransfer>
mit implementiert <feFuncX type="linear" />
.
Jedes <feFuncX type="linear" />
Element akzeptiert ein Steigungs- und Intercept- Attribut. Anschließend wird jeder neue Farbwert anhand einer einfachen Formel berechnet:
value = slope * value + intercept
Dies ist einfach zu implementieren:
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);
}
Sobald dies implementiert ist brightness()
und contrast()
auch implementiert werden kann:
function brightness(value = 1) { this.linear(value); }
function contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
Implementierung invert()
Der invert()
Filter wird von <feComponentTransfer>
mit implementiert <feFuncX type="table" />
.
Die Spezifikation besagt:
Im Folgenden ist C die Anfangskomponente und C ' die neu zugeordnete Komponente; beide im geschlossenen Intervall [0,1].
Für "Tabelle" wird die Funktion durch lineare Interpolation zwischen den im Attribut tableValues angegebenen Werten definiert . Die Tabelle enthält n + 1 Werte (dh v 0 bis v n ), die die Start- und Endwerte für n gleichmäßig große Interpolationsbereiche angeben. Interpolationen verwenden die folgende Formel:
Für einen Wert C finde k so, dass:
k / n ≤ C <(k + 1) / n
Das Ergebnis C ' ist gegeben durch:
C '= vk + (C - k / n) · n · ( vk + 1 - vk )
Eine Erklärung dieser Formel:
- Der
invert()
Filter definiert diese Tabelle: [Wert, 1 - Wert]. Dies ist tableValues oder v .
- Die Formel definiert n so, dass n + 1 die Länge der Tabelle ist. Da die Länge der Tabelle 2 beträgt, ist n = 1.
- Die Formel definiert k , wobei k und k + 1 Indizes der Tabelle sind. Da die Tabelle 2 Elemente enthält, ist k = 0.
Somit können wir die Formel vereinfachen, um:
C '= v 0 + C * (v 1 - v 0 )
Wenn wir die Werte der Tabelle einfügen, bleibt uns Folgendes übrig:
C '= Wert + C * (1 - Wert - Wert)
Noch eine Vereinfachung:
C '= Wert + C * (1 - 2 * Wert)
Die Spezifikation definiert C und C ' als RGB-Werte innerhalb der Grenzen 0-1 (im Gegensatz zu 0-255). Daher müssen wir die Werte vor der Berechnung verkleinern und danach wieder hochskalieren.
So kommen wir zu unserer Umsetzung:
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);
}
Zwischenspiel: @ Daves Brute-Force-Algorithmus
@ Daves Code generiert 176.660 Filterkombinationen, einschließlich:
- 11
invert()
Filter (0%, 10%, 20%, ..., 100%)
- 11
sepia()
Filter (0%, 10%, 20%, ..., 100%)
- 20
saturate()
Filter (5%, 10%, 15%, ..., 100%)
- 73
hue-rotate()
Filter (0 Grad, 5 Grad, 10 Grad, ..., 360 Grad)
Es berechnet Filter in der folgenden Reihenfolge:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);
Anschließend werden alle berechneten Farben durchlaufen. Es stoppt, sobald eine erzeugte Farbe innerhalb der Toleranz gefunden wurde (alle RGB-Werte liegen innerhalb von 5 Einheiten von der Zielfarbe).
Dies ist jedoch langsam und ineffizient. Daher präsentiere ich meine eigene Antwort.
Implementierung von SPSA
Zunächst müssen wir eine Verlustfunktion definieren , die die Differenz zwischen der durch eine Filterkombination erzeugten Farbe und der Zielfarbe zurückgibt. Wenn die Filter perfekt sind, sollte die Verlustfunktion 0 zurückgeben.
Wir werden den Farbunterschied als die Summe von zwei Metriken messen:
- RGB-Unterschied, weil das Ziel darin besteht, den nächstgelegenen RGB-Wert zu erzeugen.
- HSL-Unterschied, da viele HSL-Werte Filtern entsprechen (z. B. Farbton korreliert grob mit
hue-rotate()
, Sättigung korreliert mit saturate()
usw.) Dies führt den Algorithmus.
Die Verlustfunktion akzeptiert ein Argument - ein Array von Filterprozentsätzen.
Wir werden die folgende Filterreihenfolge verwenden:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg) brightness(e%) contrast(f%);
Implementierung:
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);
}
Wir werden versuchen, die Verlustfunktion so zu minimieren, dass:
loss([a, b, c, d, e, f]) = 0
Der SPSA- Algorithmus ( Website , weitere Informationen , Papier , Implementierungspapier , Referenzcode ) ist hier sehr gut. Es wurde entwickelt, um komplexe Systeme mit lokalen Minima, verrauschten / nichtlinearen / multivariaten Verlustfunktionen usw. zu optimieren. Es wurde verwendet, um Schach-Engines abzustimmen . Und im Gegensatz zu vielen anderen Algorithmen sind die Beschreibungen tatsächlich verständlich (wenn auch mit großem Aufwand).
Implementierung:
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;
}
}
Ich habe einige Änderungen / Optimierungen an SPSA vorgenommen:
- Verwenden Sie das beste Ergebnis anstelle des letzten.
- Wiederverwenden alle Arrays (
deltas
, highArgs
, lowArgs
), anstatt sie mit jeder Iteration neu zu erstellen.
- Verwenden eines Array von Werten für a anstelle eines einzelnen Werts. Dies liegt daran, dass alle Filter unterschiedlich sind und sich daher mit unterschiedlichen Geschwindigkeiten bewegen / konvergieren sollten.
- Ausführen einer
fix
Funktion nach jeder Iteration. Es klemmt alle Werte auf zwischen 0% und 100%, außer saturate
(wo das Maximum 7500% beträgt) brightness
und contrast
(wo das Maximum 200% beträgt) und hueRotate
(wo die Werte umwickelt anstatt geklemmt werden).
Ich benutze SPSA in einem zweistufigen Prozess:
- Die "breite" Bühne, die versucht, den Suchraum zu "erkunden". SPSA wird nur begrenzt wiederholt, wenn die Ergebnisse nicht zufriedenstellend sind.
- Die "schmale" Bühne, die das beste Ergebnis von der breiten Bühne nimmt und versucht, es zu "verfeinern". Es werden dynamische Werte für A und a verwendet .
Implementierung:
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);
}
SPSA einstellen
Warnung: Spielen Sie nicht mit dem SPSA-Code, insbesondere nicht mit seinen Konstanten, es sei denn, Sie sind sicher , dass Sie wissen, was Sie tun.
Die wichtigen Konstanten sind A , a , c , die Anfangswerte, die Wiederholungsschwellenwerte, die Werte von max
in fix()
und die Anzahl der Iterationen jeder Stufe. Alle diese Werte wurden sorgfältig abgestimmt, um gute Ergebnisse zu erzielen, und ein zufälliges Durchschrauben verringert mit ziemlicher Sicherheit die Nützlichkeit des Algorithmus.
Wenn Sie darauf bestehen, es zu ändern, müssen Sie messen, bevor Sie "optimieren".
Wenden Sie zuerst diesen Patch an .
Führen Sie dann den Code in Node.js aus. Nach einiger Zeit sollte das Ergebnis ungefähr so aussehen:
Average loss: 3.4768521401985275
Average time: 11.4915ms
Stellen Sie nun die Konstanten nach Herzenslust ein.
Einige Hinweise:
- Der durchschnittliche Verlust sollte bei 4 liegen. Wenn er größer als 4 ist, werden zu weit entfernte Ergebnisse erzielt, und Sie sollten die Genauigkeit einstellen. Wenn es weniger als 4 ist, wird Zeit verschwendet, und Sie sollten die Anzahl der Iterationen reduzieren.
- Wenn Sie die Anzahl der Iterationen erhöhen / verringern, passen Sie A entsprechend an.
- Wenn Sie erhöhen / verringern A , stellen ein entsprechend.
- Verwenden Sie das
--debug
Flag, wenn Sie das Ergebnis jeder Iteration sehen möchten.
TL; DR