@Dave는 이에 대한 답변 (작업 코드 포함)을 최초로 게시 했으며 그의 답변은 뻔뻔한 복사 및 붙여 넣기 의 귀중한 소스였습니다. 영감 . 이 게시물은 @Dave의 답변을 설명하고 수정하려는 시도로 시작되었지만 이후 자체 답변으로 발전했습니다.
내 방법이 훨씬 빠릅니다. A에 따라 jsPerf 벤치 마크 데이브의 알고리즘 @ 무작위로 생성 된 RGB 색상에에서 실행 (600) MS 광산에서 실행되는 동안, 30 MS . 예를 들어 속도가 중요한로드 시간에서 이는 확실히 중요 할 수 있습니다.
또한 일부 색상의 경우 알고리즘이 더 잘 수행됩니다.
- 의 경우
rgb(0,255,0)
@ Dave 's rgb(29,218,34)
는rgb(1,255,0)
- 의 경우
rgb(0,0,255)
@Dave의 생산 rgb(37,39,255)
및 광산은rgb(5,6,255)
- 의 경우
rgb(19,11,118)
@Dave의 생산 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;
설명
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)}%);`;
}
}
설명:
Color
클래스는 RGB 색상을 나타냅니다.
- 이
toString()
함수는 CSS rgb(...)
색상 문자열 의 색상을 반환 합니다.
- 이
hsl()
함수는 HSL 로 변환 된 색상을 반환합니다 .
- 이
clamp()
기능은 주어진 색상 값이 범위 (0-255) 내에 있는지 확인합니다.
Solver
클래스는 대상 색상으로 문제 해결을 시도합니다.
- 이
css()
함수는 CSS 필터 문자열에 지정된 필터를 반환합니다.
구현 grayscale()
, sepia()
및saturate()
CSS / SVG 필터의 핵심은 필터 기본 요소입니다. 이미지에 대한 저수준 수정을 나타내는 입니다.
필터는 grayscale()
, sepia()
및 saturate()
필터에 의해 구현되는 프리미티브 <feColorMatrix>
행한다는 매트릭스 승산에 필터 (종종 동적으로 생성)에 의해 특정되는 행렬과 색상이 만든 매트릭스 사이. 도표:
여기에서 몇 가지 최적화 할 수 있습니다.
- 색상 매트릭스의 마지막 요소는 다음과 같습니다.
1
. 그것을 계산하거나 저장할 필요가 없습니다.
- 알파 / 투명도 값을 계산하거나 저장할 필요가 없습니다 (
A
RGBA가 아닌 RGB를 다루기 때문에 ) 없습니다.
- 따라서 필터 행렬을 5x5에서 3x5로, 색상 행렬을 1x5에서 1x3으로 트리밍 할 수 있습니다. . 이것은 약간의 작업을 절약합니다.
- 모든
<feColorMatrix>
필터는 4 열과 5 열을 0으로 남겨 둡니다. 따라서 필터 행렬을 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]에서.
"table"의 경우 함수는 tableValues 속성에 지정된 값 간의 선형 보간에 의해 정의됩니다 . 테이블에는 n 개의 균등 한 크기의 보간 영역에 대한 시작 및 끝 값을 지정하는 n + 1 값 (즉, v 0 ~ v n )이 있습니다. 보간은 다음 공식을 사용합니다.
값 C에 대해 k를 찾으십시오. 과 같은 .
k / n ≤ C <(k + 1) / n
결과 C ' 는 다음과 같이 제공됩니다.
C '= v k + (C-k / n) * n * (v k + 1 -v k )
이 공식에 대한 설명 :
invert()
[- 값이 값 1] : 필터는이 테이블을 정의하고있다. 이것은 tableValues 또는 v 입니다.
- 공식은 n을 정의 하므로 n + 1이 테이블의 길이가됩니다. 테이블의 길이가 2이므로 n = 1입니다.
- 공식은 k를 정의 하며 k 및 k + 1은 테이블의 인덱스입니다. 테이블에 2 개의 요소가 있으므로 k = 0입니다.
따라서 공식을 단순화하여 다음을 수행 할 수 있습니다.
C '= v 0 + C * (v 1 -v 0 )
테이블의 값을 인라인하면 다음이 남습니다.
C '= 값 + C * (1-값-값)
한 가지 더 단순화 :
C '= 값 + C * (1-2 * 값)
사양은 C 와 C ' 를 범위 0-1 (0-255와 반대) 내에서 RGB 값으로 . 결과적으로 계산 전에 값을 축소하고 나중에 다시 확장해야합니다.
따라서 우리는 구현에 도달합니다.
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);
}
막간 : @Dave의 무차별 대입 알고리즘
@Dave 의 코드는 다음을 포함하여 176,660 개의 필터 조합을 생성합니다 .
- 11
invert()
필터 (0 %, 10 %, 20 %, ..., 100 %)
- 11
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 차이입니다.
- 많은 HSL 값이 필터에 해당하기 때문에 HSL 차이 (예 : 색조는 대략적으로 상관
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;
}
}
SPSA를 수정 / 최적화했습니다.
- 마지막 대신 생성 된 최상의 결과를 사용합니다.
- 모든 배열을 인용 (
deltas
, highArgs
, lowArgs
), 대신 각각의 반복으로 그들을 재 작성.
- 값의 어레이를 사용 대신 하나의 값. 이는 모든 필터가 다르기 때문에 서로 다른 속도로 이동 / 수렴해야하기 때문입니다.
fix
각 반복 후 함수 실행 . saturate
(최대 값이 7500 % 인 경우) brightness
및 contrast
(최대 값이 200 % 인 경우) 및 hueRotate
(값이 고정되는 대신 래핑되는 경우 )를 제외하고 모든 값을 0 %에서 100 % 사이로 고정합니다.
2 단계 프로세스에서 SPSA를 사용합니다.
- 검색 공간을 "탐색"하려는 "넓은"단계. 결과가 만족스럽지 않으면 SPSA를 제한적으로 재 시도합니다.
- 넓은 스테이지에서 최상의 결과를 가져 와서 "조정"하려는 "좁은"스테이지입니다. A 및 a에 동적 값을 사용 합니다 .
이행:
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 코드, 특히 해당 상수를 엉망으로 만들지 마십시오 .
중요한 상수는 A , a , c , 초기 값, 재시도 임계 값, max
in 값 fix()
및 각 단계의 반복 횟수입니다. 이 모든 값은 좋은 결과를 내기 위해 신중하게 조정되었으며, 무작위로 조이면 알고리즘의 유용성이 거의 감소합니다.
변경을 고집하는 경우 "최적화"하기 전에 측정해야합니다.
먼저이 패치를 적용 합니다 .
그런 다음 Node.js에서 코드를 실행합니다. 꽤 오랜 시간이 지나면 결과는 다음과 같습니다.
Average loss: 3.4768521401985275
Average time: 11.4915ms
이제 상수를 마음의 내용으로 조정하십시오.
몇 가지 팁 :
- 평균 손실은 약 4 여야합니다. 4보다 크면 너무 멀리 떨어져있는 결과를 생성하는 것이므로 정확도를 조정해야합니다. 4보다 작 으면 시간 낭비이므로 반복 횟수를 줄여야합니다.
- 반복 횟수를 늘리거나 줄이면 A를 적절하게 조정하십시오 .
- A 를 늘리거나 줄이면 적절하게 조정 하십시오 .
--debug
각 반복의 결과를 보려면 플래그를 사용하십시오 .
TL; DR