depixel 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/lib.js +85 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,9 +7,10 @@ Based on the following paper:
|
|
|
7
7
|
And the [source code](https://github.com/falichs/Depixelizing-Pixel-Art-on-GPUs) included with this paper:
|
|
8
8
|
* [Depixelizing Pixel Art on GPUs](https://www.cg.tuwien.ac.at/research/publications/2014/KREUZER-2014-DPA/) by Felix Kreuzer
|
|
9
9
|
|
|
10
|
-
Improvements upon original
|
|
10
|
+
Improvements upon original GPU version:
|
|
11
11
|
* Add a threshold parameter to adjust how close two colors need to be to be consider similar
|
|
12
12
|
* Better handle alpha channels (treat as dissimilar from non-transparent pixels)
|
|
13
|
+
* Fix stretching/scaling due to texel misalignment
|
|
13
14
|
|
|
14
15
|
Notes
|
|
15
16
|
* Original GPU code is MIT licensed, the same license may apply here, all original code in this project is additionally released under the MIT License
|
package/lib.js
CHANGED
|
@@ -14,6 +14,18 @@ type Opts = {
|
|
|
14
14
|
function scaleImage(src: Image, opts: Opts): Image;
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
const {
|
|
18
|
+
abs,
|
|
19
|
+
ceil,
|
|
20
|
+
exp,
|
|
21
|
+
floor,
|
|
22
|
+
hypot,
|
|
23
|
+
max,
|
|
24
|
+
min,
|
|
25
|
+
round,
|
|
26
|
+
sign,
|
|
27
|
+
} = Math;
|
|
28
|
+
|
|
17
29
|
const EDGE_HORVERT = 16;
|
|
18
30
|
const EDGE_DIAGONAL_ULLR = 32;
|
|
19
31
|
const EDGE_DIAGONAL_LLUR = 64;
|
|
@@ -55,6 +67,7 @@ const BRACKET_SEARCH_B = -0.1;
|
|
|
55
67
|
const GOLD = 1.618034;
|
|
56
68
|
const GLIMIT = 10.0;
|
|
57
69
|
const TINY = 0.000000001;
|
|
70
|
+
const ONEo255 = 1/255;
|
|
58
71
|
|
|
59
72
|
function clampInt(v, lo, hi) {
|
|
60
73
|
if (v < lo) {
|
|
@@ -77,9 +90,23 @@ function fetchPixelRGBA(src, x, y) {
|
|
|
77
90
|
const cy = clampInt(y, 0, h - 1);
|
|
78
91
|
const idx = pixelIndex(cx, cy, w);
|
|
79
92
|
const d = src.data;
|
|
80
|
-
return [d[idx]
|
|
93
|
+
return [d[idx] * ONEo255, d[idx + 1] * ONEo255, d[idx + 2] * ONEo255, d[idx + 3] * ONEo255];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function fetchPixelRGBA8(src, x, y) {
|
|
97
|
+
const w = src.width;
|
|
98
|
+
const h = src.height;
|
|
99
|
+
const cx = clampInt(x, 0, w - 1);
|
|
100
|
+
const cy = clampInt(y, 0, h - 1);
|
|
101
|
+
const idx = pixelIndex(cx, cy, w);
|
|
102
|
+
const d = src.data;
|
|
103
|
+
return [d[idx], d[idx + 1], d[idx + 2], d[idx + 3]];
|
|
81
104
|
}
|
|
82
105
|
|
|
106
|
+
const THRESHOLD_a = 32 / 255;
|
|
107
|
+
const THRESHOLD_y = 48 / 255;
|
|
108
|
+
const THRESHOLD_u = 7 / 255;
|
|
109
|
+
const THRESHOLD_v = 6 / 255;
|
|
83
110
|
function isSimilar(a, b, threshold) {
|
|
84
111
|
const yA = 0.299 * a[0] + 0.587 * a[1] + 0.114 * a[2];
|
|
85
112
|
const uA = 0.493 * (a[2] - yA);
|
|
@@ -87,16 +114,15 @@ function isSimilar(a, b, threshold) {
|
|
|
87
114
|
const yB = 0.299 * b[0] + 0.587 * b[1] + 0.114 * b[2];
|
|
88
115
|
const uB = 0.493 * (b[2] - yB);
|
|
89
116
|
const vB = 0.877 * (b[0] - yB);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (a[3] + b[3] <= (32.0/255) * t) {
|
|
117
|
+
if (abs(a[3] - b[3]) <= THRESHOLD_a * threshold) {
|
|
118
|
+
if (a[3] + b[3] <= THRESHOLD_a * threshold) {
|
|
93
119
|
// treat all alpha=0 pixels as similar, regardless of color
|
|
94
120
|
// note: need an option for this if processing images with masks or premultiplied alpha
|
|
95
121
|
return true;
|
|
96
122
|
}
|
|
97
|
-
if (
|
|
98
|
-
if (
|
|
99
|
-
if (
|
|
123
|
+
if (abs(yA - yB) <= THRESHOLD_y * threshold) {
|
|
124
|
+
if (abs(uA - uB) <= THRESHOLD_u * threshold) {
|
|
125
|
+
if (abs(vA - vB) <= THRESHOLD_v * threshold) {
|
|
100
126
|
return true;
|
|
101
127
|
}
|
|
102
128
|
}
|
|
@@ -105,6 +131,8 @@ function isSimilar(a, b, threshold) {
|
|
|
105
131
|
return false;
|
|
106
132
|
}
|
|
107
133
|
|
|
134
|
+
const THRESHOLD_CONTOUR = 100 / 255;
|
|
135
|
+
const THRESHOLD_CONTOUR_SQ = THRESHOLD_CONTOUR * THRESHOLD_CONTOUR;
|
|
108
136
|
function isContour(src, pL, pR) {
|
|
109
137
|
const a = fetchPixelRGBA(src, pL[0], pL[1]);
|
|
110
138
|
const b = fetchPixelRGBA(src, pR[0], pR[1]);
|
|
@@ -117,8 +145,8 @@ function isContour(src, pL, pR) {
|
|
|
117
145
|
const dy = yA - yB;
|
|
118
146
|
const du = uA - uB;
|
|
119
147
|
const dv = vA - vB;
|
|
120
|
-
const
|
|
121
|
-
return
|
|
148
|
+
const dist_sq = dy * dy + du * du + dv * dv;
|
|
149
|
+
return dist_sq > THRESHOLD_CONTOUR_SQ;
|
|
122
150
|
}
|
|
123
151
|
|
|
124
152
|
function buildSimilarityGraph(src, similarityThreshold) {
|
|
@@ -136,7 +164,7 @@ function buildSimilarityGraph(src, similarityThreshold) {
|
|
|
136
164
|
}
|
|
137
165
|
|
|
138
166
|
function getPixelCoords(gx, gy) {
|
|
139
|
-
return [
|
|
167
|
+
return [floor((gx - 1) / 2), floor((gy - 1) / 2)];
|
|
140
168
|
}
|
|
141
169
|
|
|
142
170
|
for (let y = 0; y < sgH; y++) {
|
|
@@ -713,8 +741,8 @@ function computeCellGraph(src, sim) {
|
|
|
713
741
|
}
|
|
714
742
|
|
|
715
743
|
function checkForCorner(s1, s2) {
|
|
716
|
-
const n1 =
|
|
717
|
-
const n2 =
|
|
744
|
+
const n1 = hypot(s1[0], s1[1]);
|
|
745
|
+
const n2 = hypot(s2[0], s2[1]);
|
|
718
746
|
if (n1 === 0 || n2 === 0) {
|
|
719
747
|
return false;
|
|
720
748
|
}
|
|
@@ -1047,8 +1075,8 @@ function optimizeCellGraph(cell, width, height) {
|
|
|
1047
1075
|
function calcPositionalEnergy(pNew, pOld) {
|
|
1048
1076
|
const dx = pNew[0] - pOld[0];
|
|
1049
1077
|
const dy = pNew[1] - pOld[1];
|
|
1050
|
-
const
|
|
1051
|
-
return
|
|
1078
|
+
const distSq = POSITIONAL_ENERGY_SCALING * POSITIONAL_ENERGY_SCALING * (dx * dx + dy * dy);
|
|
1079
|
+
return distSq * distSq;
|
|
1052
1080
|
}
|
|
1053
1081
|
|
|
1054
1082
|
function calcSegmentCurveEnergy(node1, node2, node3) {
|
|
@@ -1079,7 +1107,7 @@ function optimizeCellGraph(cell, width, height) {
|
|
|
1079
1107
|
const r = (bx - ax) * (fb - fc);
|
|
1080
1108
|
const q = (bx - cx) * (fb - fa);
|
|
1081
1109
|
const qr = q - r;
|
|
1082
|
-
let u = bx - ((bx - cx) * q - (bx - ax) * r) / (2.0 *
|
|
1110
|
+
let u = bx - ((bx - cx) * q - (bx - ax) * r) / (2.0 * sign(qr) * max(abs(qr), TINY));
|
|
1083
1111
|
const ulim = bx + GLIMIT * (cx - bx);
|
|
1084
1112
|
let fu;
|
|
1085
1113
|
if ((bx - u) * (u - cx) > 0.0) {
|
|
@@ -1121,7 +1149,7 @@ function optimizeCellGraph(cell, width, height) {
|
|
|
1121
1149
|
|
|
1122
1150
|
function searchOffset(pos, splineNeighbors) {
|
|
1123
1151
|
let gradient = calcGradient(splineNeighbors[0], pos, splineNeighbors[1]);
|
|
1124
|
-
const glen =
|
|
1152
|
+
const glen = hypot(gradient[0], gradient[1]);
|
|
1125
1153
|
if (glen <= 0.0) {
|
|
1126
1154
|
return [0, 0, 0];
|
|
1127
1155
|
}
|
|
@@ -1131,7 +1159,7 @@ function optimizeCellGraph(cell, width, height) {
|
|
|
1131
1159
|
let x1 = 0;
|
|
1132
1160
|
let x2 = 0;
|
|
1133
1161
|
let x3 = bracket[2];
|
|
1134
|
-
if (
|
|
1162
|
+
if (abs(bracket[2] - bracket[1]) > abs(bracket[1] - bracket[0])) {
|
|
1135
1163
|
x1 = bracket[1];
|
|
1136
1164
|
x2 = bracket[1] + C * (bracket[2] - bracket[1]);
|
|
1137
1165
|
} else {
|
|
@@ -1144,7 +1172,7 @@ function optimizeCellGraph(cell, width, height) {
|
|
|
1144
1172
|
let f2 = calcSegmentCurveEnergy(splineNeighbors[0], pOpt, splineNeighbors[1]) + calcPositionalEnergy(pOpt, pos);
|
|
1145
1173
|
let counter = 0;
|
|
1146
1174
|
let fx;
|
|
1147
|
-
while (
|
|
1175
|
+
while (abs(x3 - x0) > TOL * (abs(x1) + abs(x2)) && (counter < LIMIT_SEARCH_ITERATIONS)) {
|
|
1148
1176
|
counter++;
|
|
1149
1177
|
if (f2 < f1) {
|
|
1150
1178
|
x0 = x1; x1 = x2; x2 = R * x1 + C * x3;
|
|
@@ -1428,32 +1456,32 @@ function gaussRasterize(src, sim, cell, positions, outW, outH) {
|
|
|
1428
1456
|
for (let ox = 0; ox < outW; ox++) {
|
|
1429
1457
|
let influencingPixels = [true, true, true, true];
|
|
1430
1458
|
const cellSpaceCoords = [
|
|
1431
|
-
(w
|
|
1432
|
-
(h
|
|
1459
|
+
(w * (ox + 0.5) / outW) - 0.5 + 0.00001,
|
|
1460
|
+
(h * (oy + 0.5) / outH) - 0.5 + 0.00001,
|
|
1433
1461
|
];
|
|
1434
|
-
const fragmentBaseKnotIndex = (2 *
|
|
1462
|
+
const fragmentBaseKnotIndex = (2 * floor(cellSpaceCoords[0]) + floor(cellSpaceCoords[1]) * 2 * (w - 1)) | 0;
|
|
1435
1463
|
const node0flags = cell.flags[fragmentBaseKnotIndex] | 0;
|
|
1436
1464
|
let hasCorrectedPosition = false;
|
|
1437
1465
|
|
|
1438
|
-
const ULCoords = [
|
|
1439
|
-
const URCoords = [
|
|
1440
|
-
const LLCoords = [
|
|
1441
|
-
const LRCoords = [
|
|
1466
|
+
const ULCoords = [floor(cellSpaceCoords[0]), ceil(cellSpaceCoords[1])];
|
|
1467
|
+
const URCoords = [ceil(cellSpaceCoords[0]), ceil(cellSpaceCoords[1])];
|
|
1468
|
+
const LLCoords = [floor(cellSpaceCoords[0]), floor(cellSpaceCoords[1])];
|
|
1469
|
+
const LRCoords = [ceil(cellSpaceCoords[0]), floor(cellSpaceCoords[1])];
|
|
1442
1470
|
|
|
1443
1471
|
function findSegmentIntersections(p0, p1, p2) {
|
|
1444
1472
|
let pointA = calcSplinePoint(p0, p1, p2, 0.0);
|
|
1445
1473
|
for (let t = STEP; t < (1.0 + STEP); t += STEP) {
|
|
1446
1474
|
const pointB = calcSplinePoint(p0, p1, p2, t);
|
|
1447
|
-
if (intersects(cellSpaceCoords, ULCoords, pointA, pointB)) {
|
|
1475
|
+
if (influencingPixels[0] && intersects(cellSpaceCoords, ULCoords, pointA, pointB)) {
|
|
1448
1476
|
influencingPixels[0] = false;
|
|
1449
1477
|
}
|
|
1450
|
-
if (intersects(cellSpaceCoords, URCoords, pointA, pointB)) {
|
|
1478
|
+
if (influencingPixels[1] && intersects(cellSpaceCoords, URCoords, pointA, pointB)) {
|
|
1451
1479
|
influencingPixels[1] = false;
|
|
1452
1480
|
}
|
|
1453
|
-
if (intersects(cellSpaceCoords, LLCoords, pointA, pointB)) {
|
|
1481
|
+
if (influencingPixels[2] && intersects(cellSpaceCoords, LLCoords, pointA, pointB)) {
|
|
1454
1482
|
influencingPixels[2] = false;
|
|
1455
1483
|
}
|
|
1456
|
-
if (intersects(cellSpaceCoords, LRCoords, pointA, pointB)) {
|
|
1484
|
+
if (influencingPixels[3] && intersects(cellSpaceCoords, LRCoords, pointA, pointB)) {
|
|
1457
1485
|
influencingPixels[3] = false;
|
|
1458
1486
|
}
|
|
1459
1487
|
pointA = pointB;
|
|
@@ -1683,11 +1711,11 @@ function gaussRasterize(src, sim, cell, positions, outW, outH) {
|
|
|
1683
1711
|
let weightSum = 0.0;
|
|
1684
1712
|
|
|
1685
1713
|
function addWeightedColor(px, py) {
|
|
1686
|
-
const col =
|
|
1714
|
+
const col = fetchPixelRGBA8(src, px, py);
|
|
1687
1715
|
const dx = cellSpaceCoords[0] - px;
|
|
1688
1716
|
const dy = cellSpaceCoords[1] - py;
|
|
1689
|
-
const
|
|
1690
|
-
const weight =
|
|
1717
|
+
const distSq = dx * dx + dy * dy;
|
|
1718
|
+
const weight = exp(-distSq * GAUSS_MULTIPLIER);
|
|
1691
1719
|
colorSum[0] += col[0] * weight;
|
|
1692
1720
|
colorSum[1] += col[1] * weight;
|
|
1693
1721
|
colorSum[2] += col[2] * weight;
|
|
@@ -1768,15 +1796,18 @@ function gaussRasterize(src, sim, cell, positions, outW, outH) {
|
|
|
1768
1796
|
|
|
1769
1797
|
const outIdx = (oy * outW + ox) * 4;
|
|
1770
1798
|
if (weightSum === 0) {
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
out[outIdx
|
|
1799
|
+
const nx = round(cellSpaceCoords[0]);
|
|
1800
|
+
const ny = round(cellSpaceCoords[1]);
|
|
1801
|
+
const col = fetchPixelRGBA8(src, nx, ny);
|
|
1802
|
+
out[outIdx] = col[0];
|
|
1803
|
+
out[outIdx + 1] = col[1];
|
|
1804
|
+
out[outIdx + 2] = col[2];
|
|
1805
|
+
out[outIdx + 3] = col[3];
|
|
1775
1806
|
} else {
|
|
1776
|
-
out[outIdx] = clampInt(
|
|
1777
|
-
out[outIdx + 1] = clampInt(
|
|
1778
|
-
out[outIdx + 2] = clampInt(
|
|
1779
|
-
out[outIdx + 3] = clampInt(
|
|
1807
|
+
out[outIdx] = clampInt(round((colorSum[0] / weightSum)), 0, 255);
|
|
1808
|
+
out[outIdx + 1] = clampInt(round((colorSum[1] / weightSum)), 0, 255);
|
|
1809
|
+
out[outIdx + 2] = clampInt(round((colorSum[2] / weightSum)), 0, 255);
|
|
1810
|
+
out[outIdx + 3] = clampInt(round((colorSum[3] / weightSum)), 0, 255);
|
|
1780
1811
|
}
|
|
1781
1812
|
}
|
|
1782
1813
|
}
|
|
@@ -1788,9 +1819,9 @@ function runPipeline(src, outH, threshold) {
|
|
|
1788
1819
|
const inW = src.width | 0;
|
|
1789
1820
|
const inH = src.height | 0;
|
|
1790
1821
|
const outHeight = outH | 0;
|
|
1791
|
-
const outWidth =
|
|
1822
|
+
const outWidth = max(1, round((inW / inH) * outHeight));
|
|
1792
1823
|
|
|
1793
|
-
const similarityThreshold = (typeof threshold === "number") ? threshold : 255;
|
|
1824
|
+
const similarityThreshold = ((typeof threshold === "number") ? threshold : 255) / 255;
|
|
1794
1825
|
|
|
1795
1826
|
const sim0 = buildSimilarityGraph(src, similarityThreshold);
|
|
1796
1827
|
const sim1 = valenceUpdate(sim0);
|
|
@@ -1818,39 +1849,27 @@ function scaleImage(src, opts) {
|
|
|
1818
1849
|
const outH = opts.height | 0;
|
|
1819
1850
|
const threshold = opts.threshold;
|
|
1820
1851
|
|
|
1821
|
-
const borderPx =
|
|
1852
|
+
const borderPx = max(0, round(opts.borderPx || 0));
|
|
1822
1853
|
if (borderPx > 0) {
|
|
1823
|
-
const doInvisBorder = Math.min(
|
|
1824
|
-
src.data[3],
|
|
1825
|
-
src.data[(inW - 1) * 4 + 3],
|
|
1826
|
-
src.data[(inH - 1) * inW * 4 + 3],
|
|
1827
|
-
src.data[((inH - 1) * inW + (inW - 1)) * 4 + 3],
|
|
1828
|
-
) < 255;
|
|
1829
1854
|
const padW = inW + 2 * borderPx;
|
|
1830
1855
|
const padH = inH + 2 * borderPx;
|
|
1831
1856
|
const padData = new Uint8Array(padW * padH * 4);
|
|
1832
|
-
//
|
|
1857
|
+
// copy src into center, expand into borders
|
|
1833
1858
|
for (let y = 0; y < padH; y++) {
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
padData[idx] = doInvisBorder ? 0 : 255;
|
|
1837
|
-
padData[idx + 1] = 0;
|
|
1838
|
-
padData[idx + 2] = doInvisBorder ? 0 : 255;
|
|
1839
|
-
padData[idx + 3] = doInvisBorder ? 0 : 255;
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
// copy src into center
|
|
1843
|
-
for (let y = 0; y < inH; y++) {
|
|
1844
|
-
const srcRow = y * inW * 4;
|
|
1845
|
-
const dstRow = (y + borderPx) * padW * 4 + borderPx * 4;
|
|
1859
|
+
const srcRow = min(inH - 1, max(0, y - borderPx)) * inW * 4;
|
|
1860
|
+
const dstRow = y * padW * 4 + borderPx * 4;
|
|
1846
1861
|
padData.set(src.data.subarray(srcRow, srcRow + inW * 4), dstRow);
|
|
1862
|
+
for (let ii = 0; ii < borderPx * 4; ++ii) {
|
|
1863
|
+
padData[dstRow - borderPx * 4 + ii] = src.data[srcRow + ii % 4];
|
|
1864
|
+
padData[dstRow + inW * 4 + ii] = src.data[srcRow + ii % 4];
|
|
1865
|
+
}
|
|
1847
1866
|
}
|
|
1848
1867
|
const scale = outH / inH;
|
|
1849
|
-
const outHpad =
|
|
1868
|
+
const outHpad = max(1, round(padH * scale));
|
|
1850
1869
|
const padded = runPipeline({ data: padData, width: padW, height: padH }, outHpad, threshold);
|
|
1851
1870
|
|
|
1852
|
-
const padOut =
|
|
1853
|
-
const outW =
|
|
1871
|
+
const padOut = max(0, round(borderPx * scale));
|
|
1872
|
+
const outW = max(1, round((inW / inH) * outH));
|
|
1854
1873
|
const cropped = new Uint8Array(outW * outH * 4);
|
|
1855
1874
|
|
|
1856
1875
|
for (let y = 0; y < outH; y++) {
|
|
@@ -1860,7 +1879,7 @@ function scaleImage(src, opts) {
|
|
|
1860
1879
|
}
|
|
1861
1880
|
const srcRow = (srcY * padded.width + padOut) * 4;
|
|
1862
1881
|
const dstRow = y * outW * 4;
|
|
1863
|
-
const len =
|
|
1882
|
+
const len = min(outW, max(0, padded.width - padOut)) * 4;
|
|
1864
1883
|
cropped.set(padded.data.subarray(srcRow, srcRow + len), dstRow);
|
|
1865
1884
|
}
|
|
1866
1885
|
|