@Dave adalah orang pertama yang memposting jawaban untuk ini (dengan kode yang berfungsi), dan jawabannya telah menjadi sumber inspirasi tempel dan salinan tak tahu malu bagi saya. Postingan ini dimulai sebagai upaya untuk menjelaskan dan menyempurnakan jawaban @ Dave, tetapi sejak itu berkembang menjadi jawabannya sendiri.
Metode saya jauh lebih cepat. Menurut tolok ukur jsPerf pada warna RGB yang dihasilkan secara acak, algoritme @ Dave berjalan dalam 600 ms , sedangkan milikku berjalan dalam 30 ms . Ini pasti bisa menjadi masalah, misalnya dalam waktu muat, di mana kecepatan sangat penting.
Selain itu, untuk beberapa warna, algoritme saya bekerja lebih baik:
- Untuk
rgb(0,255,0)
, produksi rgb(29,218,34)
dan produksi @ Davergb(1,255,0)
- Untuk
rgb(0,0,255)
, produksi @ Dave rgb(37,39,255)
dan produksi sayargb(5,6,255)
- Untuk
rgb(19,11,118)
, produksi @ Dave rgb(36,27,102)
dan produksi sayargb(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>
Pemakaian
let color = new Color(0, 255, 0);
let solver = new Solver(color);
let result = solver.solve();
let filterCSS = result.css;
Penjelasan
Kami akan mulai dengan menulis beberapa 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)}%);`;
}
}
Penjelasan:
- The
Color
kelas merupakan warna RGB.
- Fungsinya
toString()
mengembalikan warna dalam rgb(...)
string warna CSS .
- Fungsinya
hsl()
mengembalikan warna, diubah menjadi HSL .
- Fungsinya
clamp()
memastikan bahwa nilai warna tertentu berada dalam batas (0-255).
- The
Solver
kelas akan mencoba untuk memecahkan warna sasaran.
- Fungsinya
css()
mengembalikan filter yang diberikan dalam string filter CSS.
Menerapkan grayscale()
, sepia()
dansaturate()
Inti dari filter CSS / SVG adalah filter primitif , yang mewakili modifikasi tingkat rendah pada gambar.
Filter grayscale()
,, sepia()
dan saturate()
diimplementasikan oleh filter primatif <feColorMatrix>
, yang melakukan perkalian matriks antara matriks yang ditentukan oleh filter (sering dibuat secara dinamis), dan matriks yang dibuat dari warna. Diagram:
Ada beberapa pengoptimalan yang bisa kami lakukan di sini:
- Elemen terakhir dari matriks warna adalah dan akan selalu
1
. Tidak ada gunanya menghitung atau menyimpannya.
- Tidak ada gunanya menghitung atau menyimpan nilai alpha / transparansi (
A
), karena kita berurusan dengan RGB, bukan RGBA.
- Oleh karena itu, kita dapat memangkas matriks filter dari 5x5 menjadi 3x5, dan matriks warna dari 1x5 menjadi 1x3 . Ini menghemat sedikit pekerjaan.
- Semua
<feColorMatrix>
filter meninggalkan kolom 4 dan 5 sebagai nol. Oleh karena itu, kami selanjutnya dapat mengurangi matriks filter menjadi 3x3 .
- Karena perkaliannya relatif sederhana, tidak perlu menyeret perpustakaan matematika yang rumit untuk ini. Algoritma perkalian matriks dapat kita implementasikan sendiri.
Penerapan:
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;
}
(Kami menggunakan variabel sementara untuk menampung hasil perkalian setiap baris, karena kami tidak ingin perubahan this.r
, dll. Mempengaruhi perhitungan selanjutnya.)
Sekarang kita telah menerapkan <feColorMatrix>
, kita dapat menerapkan grayscale()
, sepia()
, dan saturate()
, yang hanya meminta dengan matriks filter yang diberikan:
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
]);
}
Menerapkan hue-rotate()
The hue-rotate()
filter dilaksanakan oleh <feColorMatrix type="hueRotate" />
.
Matriks filter dihitung seperti yang ditunjukkan di bawah ini:
Misalnya, elemen yang 00 akan dihitung seperti:
Beberapa catatan:
- Sudut rotasi diberikan dalam derajat. Ini harus dikonversi ke radian sebelum diteruskan ke
Math.sin()
atau Math.cos()
.
Math.sin(angle)
dan Math.cos(angle)
harus dihitung sekali dan kemudian disimpan dalam cache.
Penerapan:
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
]);
}
Menerapkan brightness()
dancontrast()
The brightness()
dan contrast()
filter dilaksanakan oleh <feComponentTransfer>
dengan <feFuncX type="linear" />
.
Setiap <feFuncX type="linear" />
elemen menerima atribut kemiringan dan intersep . Kemudian menghitung setiap nilai warna baru melalui rumus sederhana:
value = slope * value + intercept
Ini mudah diterapkan:
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);
}
Setelah ini diterapkan, brightness()
dan contrast()
dapat diterapkan juga:
function brightness(value = 1) { this.linear(value); }
function contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
Menerapkan invert()
The invert()
filter dilaksanakan oleh <feComponentTransfer>
dengan <feFuncX type="table" />
.
Spesifikasi menyatakan:
Berikut ini, C adalah komponen awal dan C ' adalah komponen yang dipetakan ulang; keduanya dalam interval tertutup [0,1].
Untuk "tabel", fungsinya ditentukan dengan interpolasi linier antara nilai yang diberikan dalam atribut tableValues . Tabel tersebut memiliki nilai n + 1 (yaitu, v 0 hingga v n ) yang menentukan nilai awal dan akhir untuk n wilayah interpolasi berukuran sama. Interpolasi menggunakan rumus berikut:
Untuk nilai C temukan k seperti itu:
k / n ≤ C <(k + 1) / n
Hasil C ' diberikan oleh:
C '= v k + (C - k / n) * n * (v k + 1 - v k )
Penjelasan rumus ini:
- The
invert()
Filter mendefinisikan tabel ini: [value, 1 - value]. Ini adalah tableValues atau v .
- Rumus tersebut mendefinisikan n , sehingga n + 1 adalah panjang tabel. Karena panjang meja adalah 2, n = 1.
- Rumusnya mendefinisikan k , dengan k dan k + 1 menjadi indeks tabel. Karena tabel memiliki 2 elemen, k = 0.
Dengan demikian, kita dapat menyederhanakan rumusnya menjadi:
C '= v 0 + C * (v 1 - v 0 )
Menyebariskan nilai tabel, kita mendapatkan:
C '= nilai + C * (1 - nilai - nilai)
Satu lagi penyederhanaan:
C '= nilai + C * (1 - 2 * nilai)
Spesifikasi mendefinisikan C dan C ' menjadi nilai RGB, dalam batas 0-1 (sebagai lawan 0-255). Akibatnya, kita harus menurunkan nilai sebelum komputasi, dan menskalakannya kembali setelahnya.
Jadi kami sampai pada implementasi kami:
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);
}
Selingan: Algoritma kekerasan @ Dave
Kode @ Dave menghasilkan 176.660 kombinasi filter, termasuk:
- 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 (0deg, 5deg, 10deg, ..., 360deg)
Ini menghitung filter dalam urutan berikut:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);
Kemudian iterasi melalui semua warna yang dihitung. Ini berhenti setelah menemukan warna yang dihasilkan dalam toleransi (semua nilai RGB berada dalam 5 unit dari warna target).
Namun, ini lambat dan tidak efisien. Demikian jawaban saya sendiri.
Menerapkan SPSA
Pertama, kita harus mendefinisikan fungsi kerugian , yang mengembalikan perbedaan antara warna yang dihasilkan oleh kombinasi filter, dan warna target. Jika filternya sempurna, fungsi kerugian harus mengembalikan 0.
Kami akan mengukur perbedaan warna sebagai jumlah dari dua metrik:
- Perbedaan RGB, karena tujuannya adalah menghasilkan nilai RGB yang paling mendekati.
- Perbedaan HSL, karena banyak nilai HSL sesuai dengan filter (mis. Hue secara kasar berkorelasi dengan
hue-rotate()
, saturasi berkorelasi dengan saturate()
, dll.) Ini memandu algoritme.
Fungsi kerugian akan mengambil satu argumen - larik persentase filter.
Kami akan menggunakan urutan filter berikut:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg) brightness(e%) contrast(f%);
Penerapan:
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);
}
Kami akan mencoba meminimalkan fungsi kerugian, sehingga:
loss([a, b, c, d, e, f]) = 0
The SPSA algoritma ( situs , info lebih lanjut , kertas , kertas implementasi , kode referensi ) sangat pandai dalam hal ini. Ini dirancang untuk mengoptimalkan sistem yang kompleks dengan fungsi minima lokal, noise / nonlinear / multivariate loss, dll. Telah digunakan untuk menyetel mesin catur . Dan tidak seperti banyak algoritme lainnya, makalah yang mendeskripsikannya sebenarnya dapat dipahami (meskipun dengan usaha keras).
Penerapan:
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;
}
}
Saya membuat beberapa modifikasi / pengoptimalan pada SPSA:
- Menggunakan hasil terbaik yang dihasilkan, bukan yang terakhir.
- Menggunakan kembali semua array (
deltas
, highArgs
, lowArgs
), bukannya menciptakan mereka dengan setiap iterasi.
- Menggunakan larik nilai untuk a , bukan satu nilai. Ini karena semua filter berbeda, dan karenanya harus bergerak / menyatu pada kecepatan yang berbeda.
- Menjalankan
fix
fungsi setelah setiap iterasi. Ini menjepit semua nilai antara 0% dan 100%, kecuali saturate
(di mana maksimum adalah 7500%), brightness
dan contrast
(di mana maksimum adalah 200%), dan hueRotate
(di mana nilai-nilai dibungkus bukan dijepit).
Saya menggunakan SPSA dalam proses dua tahap:
- Panggung "lebar", yang mencoba "menjelajahi" ruang pencarian. Ini akan membatasi pengulangan SPSA jika hasilnya tidak memuaskan.
- Tahap "sempit", yang mengambil hasil terbaik dari panggung lebar dan mencoba "menyempurnakannya". Ini menggunakan nilai dinamis untuk A dan a .
Penerapan:
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);
}
Menyetel SPSA
Peringatan: Jangan main-main dengan kode SPSA, terutama dengan konstanta, kecuali Anda yakin tahu apa yang Anda lakukan.
Konstanta yang penting adalah A , a , c , nilai awal, retry threshold, nilai max
in fix()
, dan jumlah iterasi setiap tahapan. Semua nilai ini disetel dengan cermat untuk menghasilkan hasil yang baik, dan mengacaukannya secara acak hampir pasti akan mengurangi kegunaan algoritme.
Jika Anda bersikeras untuk mengubahnya, Anda harus mengukurnya sebelum Anda "mengoptimalkan".
Pertama, terapkan tambalan ini .
Kemudian jalankan kode di Node.js. Setelah beberapa lama, hasilnya akan seperti ini:
Average loss: 3.4768521401985275
Average time: 11.4915ms
Sekarang setel konstanta sesuka hati Anda.
Beberapa tips:
- Kerugian rata-rata harus sekitar 4. Jika lebih besar dari 4, itu menghasilkan hasil yang terlalu jauh, dan Anda harus menyesuaikan keakuratannya. Jika kurang dari 4, ini membuang-buang waktu, dan Anda harus mengurangi jumlah iterasi.
- Jika Anda menambah / mengurangi jumlah iterasi, sesuaikan A dengan tepat.
- Jika Anda menaikkan / menurunkan A , sesuaikan a dengan benar.
- Gunakan
--debug
bendera jika Anda ingin melihat hasil dari setiap iterasi.
TL; DR