@ डेव इस (वर्किंग कोड के साथ) का उत्तर पोस्ट करने वाले पहले व्यक्ति थे , और उनका जवाब बेशर्म कॉपी और मेरे लिए प्रेरणा चिपकाने का एक अमूल्य स्रोत रहा है। यह पोस्ट @ डेव के उत्तर को समझाने और परिष्कृत करने के प्रयास के रूप में शुरू हुई, लेकिन तब से यह स्वयं के उत्तर में विकसित हुई है।
मेरा तरीका काफी तेज है। यादृच्छिक रूप से उत्पन्न RGB रंगों पर एक jsPerf बेंचमार्क के अनुसार , @ डेव का एल्गोरिथ्म 600 ms में चलता है , जबकि मेरा 30 ms में चलता है । यह निश्चित रूप से मायने रखता है, उदाहरण के लिए लोड समय में, जहां गति महत्वपूर्ण है।
इसके अलावा, कुछ रंगों के लिए, मेरा एल्गोरिथ्म बेहतर प्रदर्शन करता है:
- के लिए
rgb(0,255,0)
, @ डेव का उत्पादन rgb(29,218,34)
और उत्पादन होता हैrgb(1,255,0)
- के लिए
rgb(0,0,255)
, @ डेव का उत्पादन rgb(37,39,255)
और मेरा उत्पादन होता हैrgb(5,6,255)
- के लिए
rgb(19,11,118)
, @ डेव का उत्पादन rgb(36,27,102)
और मेरा उत्पादन होता हैrgb(20,11,112)
डेमो
"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>
प्रयोग
let color = new Color(0, 255, 0);
let solver = new Solver(color);
let result = solver.solve();
let filterCSS = result.css;
व्याख्या
हम कुछ जावास्क्रिप्ट लिखना शुरू करेंगे।
"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)}%);`;
}
}
स्पष्टीकरण:
Color
वर्ग एक आरजीबी रंग का प्रतिनिधित्व करता है।
- आईटी इस
toString()
कार्य सीएसएस rgb(...)
रंग स्ट्रिंग में रंग लौटाता है ।
- आईटी इस
hsl()
कार्य एचएसएल में परिवर्तित रंग देता है ।
- आईटी इस
clamp()
कार्य यह सुनिश्चित करता है कि किसी दिए गए रंग का मूल्य सीमा (0-255) के भीतर है।
Solver
वर्ग एक लक्ष्य रंग के लिए हल करने के लिए प्रयास करेंगे।
- इसका
css()
कार्य सीएसएस फिल्टर स्ट्रिंग में दिए गए फिल्टर देता है।
लागू करना grayscale()
,sepia()
औरsaturate()
सीएसएस / एसवीजी फिल्टर का दिल हैं फिल्टर प्राइमिटिव हैं , जो एक छवि के निम्न-स्तरीय संशोधनों का प्रतिनिधित्व करते हैं।
फ़िल्टर grayscale()
, sepia()
और saturate()
फ़िल्टर प्राइमरी द्वारा कार्यान्वित किए जाते हैं <feColorMatrix>
, जो फ़िल्टर द्वारा निर्दिष्ट मैट्रिक्स (अक्सर गतिशील रूप से उत्पन्न), और रंग से निर्मित मैट्रिक्स के बीच मैट्रिक्स गुणन करता है । चित्र:
कुछ अनुकूलन हैं जो हम यहां कर सकते हैं:
- रंग मैट्रिक्स का अंतिम तत्व है और हमेशा रहेगा
1
। इसकी गणना या भंडारण का कोई मतलब नहीं है।
- अल्फा / ट्रांसपेरेंसी वैल्यू (
A
) की गणना करने या संग्रहीत करने का कोई मतलब नहीं है , क्योंकि हम RGB के साथ काम कर रहे हैं, RGBA के साथ नहीं।
- इसलिए, हम फ़िल्टर मैट्रिक्स को 5x5 से 3x5 तक और कलर मैट्रिक्स को 1x5 से 1x3 तक ट्रिम कर सकते हैं । इससे थोड़ा बहुत काम बच जाता है।
- सभी
<feColorMatrix>
फ़िल्टर कॉलम 4 और 5 को शून्य के रूप में छोड़ देते हैं। इसलिए, हम फ़िल्टर मैट्रिक्स को 3x3 तक कम कर सकते हैं ।
- चूंकि गुणा अपेक्षाकृत सरल है, इसके लिए जटिल गणित पुस्तकालयों में खींचने की कोई आवश्यकता नहीं है । हम स्वयं मैट्रिक्स गुणा एल्गोरिथ्म को लागू कर सकते हैं।
कार्यान्वयन:
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;
}
(हम प्रत्येक पंक्ति गुणन के परिणामों को धारण करने के लिए अस्थायी चर का उपयोग करते हैं, क्योंकि हम बदलाव नहीं चाहते हैं this.r
बाद की गणनाओं को प्रभावित करने वाले आदि को ।)
अब जब हम लागू कर दिया है <feColorMatrix>
, हम लागू कर सकते हैं grayscale()
, sepia()
और saturate()
, जो केवल यह आह्वान किसी दिए गए फिल्टर मैट्रिक्स के साथ:
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
]);
}
क्रियान्वयन hue-rotate()
hue-rotate()
फिल्टर द्वारा कार्यान्वित किया जाता <feColorMatrix type="hueRotate" />
।
फ़िल्टर मैट्रिक्स की गणना नीचे दिखाए अनुसार की गई है:
उदाहरण के लिए, तत्व एक 00 तो जैसे गणना की जाएगी:
कुछ नोट:
- रोटेशन के कोण को डिग्री में दिया गया है। यह रेडियंस में परिवर्तित किया जाना चाहिए करने के लिए पारित कर दिया इससे पहले कि
Math.sin()
या Math.cos()
।
Math.sin(angle)
और Math.cos(angle)
एक बार संकलित किया जाना चाहिए।
कार्यान्वयन:
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
]);
}
लागू करने brightness()
औरcontrast()
brightness()
और contrast()
फिल्टर द्वारा कार्यान्वित किया जाता <feComponentTransfer>
के साथ <feFuncX type="linear" />
।
प्रत्येक <feFuncX type="linear" />
तत्व एक ढलान और अवरोधन विशेषता को स्वीकार करता है । इसके बाद एक सरल सूत्र के माध्यम से प्रत्येक नए रंग मान की गणना की जाती है:
value = slope * value + intercept
इसे लागू करना आसान है:
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);
}
एक बार यह लागू हो जाए, brightness()
और contrast()
इसे लागू किया जा सके:
function brightness(value = 1) { this.linear(value); }
function contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
क्रियान्वयन invert()
invert()
फिल्टर द्वारा कार्यान्वित किया जाता <feComponentTransfer>
के साथ <feFuncX type="table" />
।
युक्ति बताती है:
निम्नलिखित में, C प्रारंभिक घटक है और C ' रीमैप्ड घटक है; बंद अंतराल में दोनों [0,1]।
"तालिका" के लिए, समारोह विशेषता में दिए गए मूल्यों के बीच रैखिक प्रक्षेप द्वारा परिभाषित किया गया है tableValues । तालिका में n समान रूप से प्रक्षेप क्षेत्रों के लिए प्रारंभ और अंत मूल्यों को निर्दिष्ट करते हुए तालिका में n + 1 मान (जैसे, v 0 से v n ) है । प्रक्षेप निम्नलिखित सूत्र का उपयोग करते हैं:
एक मूल्य के लिए सी खोज कश्मीर ऐसी है कि:
k / n / C <(k + 1) / n
परिणाम C ' द्वारा दिया गया है:
C '= v k + (C - k / n) * n * (v k + 1 - v k )
इस सूत्र का स्पष्टीकरण:
invert()
[- मूल्य मूल्य, 1]: फिल्टर इस तालिका को परिभाषित करता है। यह टेबलवैल्यू या वी है ।
- सूत्र n को परिभाषित करता है , जैसे कि n + 1 तालिका की लंबाई है। चूंकि तालिका की लंबाई 2, n = 1 है।
- सूत्र को परिभाषित करता है कश्मीर , साथ कश्मीर और कश्मीर + 1 तालिका के अनुक्रमित किया जा रहा है। चूंकि तालिका में 2 तत्व हैं, k = 0।
इस प्रकार, हम सूत्र को सरल बना सकते हैं:
C '= v 0 + C * (v 1 - v 0 )
तालिका के मूल्यों को सम्मिलित करते हुए, हम साथ रह गए हैं:
C '= मान + C * (1 - मान - मान)
एक और सरलीकरण:
C '= मान + C * (1 - 2 * मान)
सी ने सी और सी ' को परिभाषित किया आरजीबी मान, सीमा 0-1 के भीतर (जैसा कि 0-255 के विपरीत)। परिणामस्वरूप, हमें गणना करने से पहले मूल्यों को मापना चाहिए, और बाद में उन्हें वापस स्केल करना चाहिए।
इस प्रकार हम अपने कार्यान्वयन पर पहुँचते हैं:
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);
}
इंटरल्यूड: @ डेव्स ब्रूट-फोर्स एल्गोरिथम
@ डेव का कोड 176,660 फ़िल्टर संयोजन बनाता है , जिसमें शामिल हैं:
- 1 1
invert()
फ़िल्टर (0%, 10%, 20%, ..., 100%)
- 1 1
sepia()
फ़िल्टर (0%, 10%, 20%, ..., 100%)
- 20
saturate()
फ़िल्टर (5%, 10%, 15%, ..., 100%)
- 73
hue-rotate()
फ़िल्टर (0deg, 5deg, 10deg, ..., 360deg)
यह निम्नलिखित क्रम में फिल्टर की गणना करता है:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg);
यह तब सभी गणना रंगों के माध्यम से पुनरावृत्त करता है। एक बार यह सहन करने के भीतर उत्पन्न रंग पा लेता है (सभी RGB मान लक्ष्य रंग से 5 इकाइयों के भीतर हैं) बंद हो जाता है।
हालांकि, यह धीमा और अक्षम है। इस प्रकार, मैं अपना जवाब प्रस्तुत करता हूं।
SPSA को लागू करना
सबसे पहले, हमें एक नुकसान फ़ंक्शन को परिभाषित करना चाहिए , जो फ़िल्टर संयोजन द्वारा उत्पादित रंग और लक्ष्य रंग के बीच का अंतर लौटाता है। यदि फ़िल्टर सही हैं, तो नुकसान फ़ंक्शन 0 पर लौटना चाहिए।
हम दो मीट्रिक के योग के रूप में रंग अंतर को मापेंगे:
- RGB अंतर, क्योंकि लक्ष्य निकटतम RGB मान का उत्पादन करना है।
- एचएसएल अंतर, क्योंकि कई एचएसएल मान फ़िल्टर के अनुरूप हैं (जैसे कि लगभग मोटे तौर पर सहसंबद्ध
hue-rotate()
, संतृप्ति सहसंबंध के साथ saturate()
, आदि) यह एल्गोरिथ्म को निर्देशित करता है।
हानि फ़ंक्शन एक तर्क लेगा - फ़िल्टर प्रतिशत की एक सरणी।
हम निम्नलिखित फ़िल्टर क्रम का उपयोग करेंगे:
filter: invert(a%) sepia(b%) saturate(c%) hue-rotate(θdeg) brightness(e%) contrast(f%);
कार्यान्वयन:
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);
}
हम नुकसान फ़ंक्शन को कम करने की कोशिश करेंगे, जैसे:
loss([a, b, c, d, e, f]) = 0
SPSA एल्गोरिथ्म ( वेबसाइट , अधिक जानकारी , कागज , कार्यान्वयन कागज , संदर्भ कोड ) इस पर बहुत अच्छा है। इसे स्थानीय मिनिमा, शोर / नॉनलाइनर / मल्टीवीरेट लॉस फ़ंक्शंस आदि के साथ जटिल प्रणालियों को अनुकूलित करने के लिए डिज़ाइन किया गया था। इसका उपयोग शतरंज इंजनों को ट्यून करने के लिए किया गया है । और कई अन्य एल्गोरिदम के विपरीत, यह वर्णन करने वाले कागजात वास्तव में बोधगम्य हैं (यद्यपि बड़े प्रयास के साथ)।
कार्यान्वयन:
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;
}
}
मैंने एसपीएएसए में कुछ संशोधन / अनुकूलन किए:
- अंतिम के बजाय, उत्पादित सर्वोत्तम परिणाम का उपयोग करना।
- सभी सरणियों पुनर्प्रयोग (
deltas
, highArgs
, lowArgs
), बजाय उन्हें एक यात्रा के साथ पुनः बनाने की।
- के लिए मूल्यों की एक सरणी का उपयोग करते हुए एक , एक भी मान के बजाय। ऐसा इसलिए है क्योंकि सभी फ़िल्टर अलग-अलग हैं, और इस तरह उन्हें अलग-अलग गति से आगे बढ़ना / जुटना चाहिए।
fix
प्रत्येक पुनरावृत्ति के बाद एक समारोह चल रहा है । यह सभी मानों को 0% से 100% के बीच रखता है, saturate
(जहां अधिकतम 7500% है) को छोड़कर , brightness
और contrast
(जहां अधिकतम 200% है), और hueRotate
(जहां मानों को क्लैंप के बजाय चारों ओर लपेटा जाता है)।
मैं दो चरणों की प्रक्रिया में SPSA का उपयोग करता हूं:
- "विस्तृत" चरण, जो खोज स्थान को "एक्सप्लोर" करने का प्रयास करता है। यदि परिणाम संतोषजनक नहीं हैं तो यह SPSA की सीमित वापसी कर देगा।
- "संकीर्ण" चरण, जो विस्तृत चरण से सबसे अच्छा परिणाम लेता है और इसे "परिष्कृत" करने का प्रयास करता है। यह ए और ए के लिए गतिशील मूल्यों का उपयोग करता है ।
कार्यान्वयन:
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
चेतावनी: SPSA कोड के साथ गड़बड़ न करें, विशेष रूप से इसके स्थिरांक के साथ, जब तक कि आप सुनिश्चित नहीं हैं कि आप जानते हैं कि आप क्या कर रहे हैं।
महत्वपूर्ण स्थिरांक हैं एक , एक , ग , प्रारंभिक मान, पुन: प्रयास करें थ्रेसहोल्ड, के मूल्यों max
में fix()
, और प्रत्येक चरण के पुनरावृत्तियों की संख्या। इन सभी मूल्यों को अच्छे परिणाम देने के लिए सावधानीपूर्वक तैयार किया गया था, और उनके साथ बेतरतीब ढंग से पेंच करना निश्चित रूप से एल्गोरिथ्म की उपयोगिता को कम कर देगा।
यदि आप इसे बदलने पर जोर देते हैं, तो आपको "अनुकूलन" करने से पहले मापना होगा।
सबसे पहले, इस पैच को लागू करें ।
फिर कोड को Node.js. में चलाएं काफी समय के बाद, परिणाम कुछ इस तरह होना चाहिए:
Average loss: 3.4768521401985275
Average time: 11.4915ms
अब स्थिरांक को अपने दिल की सामग्री पर ट्यून करें।
कुछ सुझाव:
- औसत नुकसान लगभग 4 होना चाहिए। यदि यह 4 से अधिक है, तो यह ऐसे परिणाम पैदा कर रहा है जो बहुत दूर हैं, और आपको सटीकता के लिए ट्यून करना चाहिए। यदि यह 4 से कम है, तो यह समय बर्बाद कर रहा है, और आपको पुनरावृत्तियों की संख्या को कम करना चाहिए।
- आप को बढ़ाते हैं /, पुनरावृत्तियों की संख्या में कमी को समायोजित एक उचित रूप से।
- आप को बढ़ाते हैं / कमी एक , समायोजित एक उचित रूप से।
--debug
यदि आप प्रत्येक पुनरावृत्ति का परिणाम देखना चाहते हैं तो ध्वज का उपयोग करें ।
टी एल; डॉ