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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/lib.js +85 -66
  3. 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] / 255, d[idx + 1] / 255, d[idx + 2] / 255, d[idx + 3] / 255];
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
- const t = Math.max(0, Math.min(255, threshold | 0)) / 255.0;
91
- if (Math.abs(a[3] - b[3]) <= (32.0/255) * t) {
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 (Math.abs(yA - yB) <= (48.0 / 255.0) * t) {
98
- if (Math.abs(uA - uB) <= (7.0 / 255.0) * t) {
99
- if (Math.abs(vA - vB) <= (6.0 / 255.0) * t) {
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 dist = Math.sqrt(dy * dy + du * du + dv * dv);
121
- return dist > 100.0 / 255.0;
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 [Math.floor((gx - 1) / 2), Math.floor((gy - 1) / 2)];
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 = Math.hypot(s1[0], s1[1]);
717
- const n2 = Math.hypot(s2[0], s2[1]);
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 dist = POSITIONAL_ENERGY_SCALING * Math.hypot(dx, dy);
1051
- return dist * dist * dist * dist;
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 * Math.sign(qr) * Math.max(Math.abs(qr), TINY));
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 = Math.hypot(gradient[0], gradient[1]);
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 (Math.abs(bracket[2] - bracket[1]) > Math.abs(bracket[1] - bracket[0])) {
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 (Math.abs(x3 - x0) > TOL * (Math.abs(x1) + Math.abs(x2)) && (counter < LIMIT_SEARCH_ITERATIONS)) {
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 - 1) * (ox / (outW - 1)),
1432
- (h - 1) * (oy / (outH - 1)),
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 * Math.floor(cellSpaceCoords[0]) + Math.floor(cellSpaceCoords[1]) * 2 * (w - 1)) | 0;
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 = [Math.floor(cellSpaceCoords[0]), Math.ceil(cellSpaceCoords[1])];
1439
- const URCoords = [Math.ceil(cellSpaceCoords[0]), Math.ceil(cellSpaceCoords[1])];
1440
- const LLCoords = [Math.floor(cellSpaceCoords[0]), Math.floor(cellSpaceCoords[1])];
1441
- const LRCoords = [Math.ceil(cellSpaceCoords[0]), Math.floor(cellSpaceCoords[1])];
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 = fetchPixelRGBA(src, px, py);
1714
+ const col = fetchPixelRGBA8(src, px, py);
1687
1715
  const dx = cellSpaceCoords[0] - px;
1688
1716
  const dy = cellSpaceCoords[1] - py;
1689
- const dist = Math.hypot(dx, dy);
1690
- const weight = Math.exp(-(dist * dist) * GAUSS_MULTIPLIER);
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
- out[outIdx] = 0;
1772
- out[outIdx + 1] = 0;
1773
- out[outIdx + 2] = 0;
1774
- out[outIdx + 3] = 255;
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(Math.round((colorSum[0] / weightSum) * 255), 0, 255);
1777
- out[outIdx + 1] = clampInt(Math.round((colorSum[1] / weightSum) * 255), 0, 255);
1778
- out[outIdx + 2] = clampInt(Math.round((colorSum[2] / weightSum) * 255), 0, 255);
1779
- out[outIdx + 3] = clampInt(Math.round((colorSum[3] / weightSum) * 255), 0, 255);
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 = Math.max(1, Math.round((inW / inH) * outHeight));
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 = Math.max(0, Math.round(opts.borderPx || 0));
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
- // fill magenta border (invisible if original image appears to have invisible edges)
1857
+ // copy src into center, expand into borders
1833
1858
  for (let y = 0; y < padH; y++) {
1834
- for (let x = 0; x < padW; x++) {
1835
- const idx = (y * padW + x) * 4;
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 = Math.max(1, Math.round(padH * scale));
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 = Math.max(0, Math.round(borderPx * scale));
1853
- const outW = Math.max(1, Math.round((inW / inH) * outH));
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 = Math.min(outW, Math.max(0, padded.width - padOut)) * 4;
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depixel",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Depixelizing Pixel Art by Kopf-Lischinski and Felix Kreuzer for Node.js",
5
5
  "main": "lib.js",
6
6
  "keywords": [