depixel 1.0.3 → 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 (2) hide show
  1. package/lib.js +62 -44
  2. package/package.json +1 -1
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,7 +90,7 @@ 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];
81
94
  }
82
95
 
83
96
  function fetchPixelRGBA8(src, x, y) {
@@ -90,6 +103,10 @@ function fetchPixelRGBA8(src, x, y) {
90
103
  return [d[idx], d[idx + 1], d[idx + 2], d[idx + 3]];
91
104
  }
92
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;
93
110
  function isSimilar(a, b, threshold) {
94
111
  const yA = 0.299 * a[0] + 0.587 * a[1] + 0.114 * a[2];
95
112
  const uA = 0.493 * (a[2] - yA);
@@ -97,16 +114,15 @@ function isSimilar(a, b, threshold) {
97
114
  const yB = 0.299 * b[0] + 0.587 * b[1] + 0.114 * b[2];
98
115
  const uB = 0.493 * (b[2] - yB);
99
116
  const vB = 0.877 * (b[0] - yB);
100
- const t = Math.max(0, Math.min(255, threshold | 0)) / 255.0;
101
- if (Math.abs(a[3] - b[3]) <= (32.0/255) * t) {
102
- 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) {
103
119
  // treat all alpha=0 pixels as similar, regardless of color
104
120
  // note: need an option for this if processing images with masks or premultiplied alpha
105
121
  return true;
106
122
  }
107
- if (Math.abs(yA - yB) <= (48.0 / 255.0) * t) {
108
- if (Math.abs(uA - uB) <= (7.0 / 255.0) * t) {
109
- 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) {
110
126
  return true;
111
127
  }
112
128
  }
@@ -115,6 +131,8 @@ function isSimilar(a, b, threshold) {
115
131
  return false;
116
132
  }
117
133
 
134
+ const THRESHOLD_CONTOUR = 100 / 255;
135
+ const THRESHOLD_CONTOUR_SQ = THRESHOLD_CONTOUR * THRESHOLD_CONTOUR;
118
136
  function isContour(src, pL, pR) {
119
137
  const a = fetchPixelRGBA(src, pL[0], pL[1]);
120
138
  const b = fetchPixelRGBA(src, pR[0], pR[1]);
@@ -127,8 +145,8 @@ function isContour(src, pL, pR) {
127
145
  const dy = yA - yB;
128
146
  const du = uA - uB;
129
147
  const dv = vA - vB;
130
- const dist = Math.sqrt(dy * dy + du * du + dv * dv);
131
- return dist > 100.0 / 255.0;
148
+ const dist_sq = dy * dy + du * du + dv * dv;
149
+ return dist_sq > THRESHOLD_CONTOUR_SQ;
132
150
  }
133
151
 
134
152
  function buildSimilarityGraph(src, similarityThreshold) {
@@ -146,7 +164,7 @@ function buildSimilarityGraph(src, similarityThreshold) {
146
164
  }
147
165
 
148
166
  function getPixelCoords(gx, gy) {
149
- return [Math.floor((gx - 1) / 2), Math.floor((gy - 1) / 2)];
167
+ return [floor((gx - 1) / 2), floor((gy - 1) / 2)];
150
168
  }
151
169
 
152
170
  for (let y = 0; y < sgH; y++) {
@@ -723,8 +741,8 @@ function computeCellGraph(src, sim) {
723
741
  }
724
742
 
725
743
  function checkForCorner(s1, s2) {
726
- const n1 = Math.hypot(s1[0], s1[1]);
727
- const n2 = Math.hypot(s2[0], s2[1]);
744
+ const n1 = hypot(s1[0], s1[1]);
745
+ const n2 = hypot(s2[0], s2[1]);
728
746
  if (n1 === 0 || n2 === 0) {
729
747
  return false;
730
748
  }
@@ -1057,8 +1075,8 @@ function optimizeCellGraph(cell, width, height) {
1057
1075
  function calcPositionalEnergy(pNew, pOld) {
1058
1076
  const dx = pNew[0] - pOld[0];
1059
1077
  const dy = pNew[1] - pOld[1];
1060
- const dist = POSITIONAL_ENERGY_SCALING * Math.hypot(dx, dy);
1061
- return dist * dist * dist * dist;
1078
+ const distSq = POSITIONAL_ENERGY_SCALING * POSITIONAL_ENERGY_SCALING * (dx * dx + dy * dy);
1079
+ return distSq * distSq;
1062
1080
  }
1063
1081
 
1064
1082
  function calcSegmentCurveEnergy(node1, node2, node3) {
@@ -1089,7 +1107,7 @@ function optimizeCellGraph(cell, width, height) {
1089
1107
  const r = (bx - ax) * (fb - fc);
1090
1108
  const q = (bx - cx) * (fb - fa);
1091
1109
  const qr = q - r;
1092
- 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));
1093
1111
  const ulim = bx + GLIMIT * (cx - bx);
1094
1112
  let fu;
1095
1113
  if ((bx - u) * (u - cx) > 0.0) {
@@ -1131,7 +1149,7 @@ function optimizeCellGraph(cell, width, height) {
1131
1149
 
1132
1150
  function searchOffset(pos, splineNeighbors) {
1133
1151
  let gradient = calcGradient(splineNeighbors[0], pos, splineNeighbors[1]);
1134
- const glen = Math.hypot(gradient[0], gradient[1]);
1152
+ const glen = hypot(gradient[0], gradient[1]);
1135
1153
  if (glen <= 0.0) {
1136
1154
  return [0, 0, 0];
1137
1155
  }
@@ -1141,7 +1159,7 @@ function optimizeCellGraph(cell, width, height) {
1141
1159
  let x1 = 0;
1142
1160
  let x2 = 0;
1143
1161
  let x3 = bracket[2];
1144
- 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])) {
1145
1163
  x1 = bracket[1];
1146
1164
  x2 = bracket[1] + C * (bracket[2] - bracket[1]);
1147
1165
  } else {
@@ -1154,7 +1172,7 @@ function optimizeCellGraph(cell, width, height) {
1154
1172
  let f2 = calcSegmentCurveEnergy(splineNeighbors[0], pOpt, splineNeighbors[1]) + calcPositionalEnergy(pOpt, pos);
1155
1173
  let counter = 0;
1156
1174
  let fx;
1157
- 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)) {
1158
1176
  counter++;
1159
1177
  if (f2 < f1) {
1160
1178
  x0 = x1; x1 = x2; x2 = R * x1 + C * x3;
@@ -1441,29 +1459,29 @@ function gaussRasterize(src, sim, cell, positions, outW, outH) {
1441
1459
  (w * (ox + 0.5) / outW) - 0.5 + 0.00001,
1442
1460
  (h * (oy + 0.5) / outH) - 0.5 + 0.00001,
1443
1461
  ];
1444
- 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;
1445
1463
  const node0flags = cell.flags[fragmentBaseKnotIndex] | 0;
1446
1464
  let hasCorrectedPosition = false;
1447
1465
 
1448
- const ULCoords = [Math.floor(cellSpaceCoords[0]), Math.ceil(cellSpaceCoords[1])];
1449
- const URCoords = [Math.ceil(cellSpaceCoords[0]), Math.ceil(cellSpaceCoords[1])];
1450
- const LLCoords = [Math.floor(cellSpaceCoords[0]), Math.floor(cellSpaceCoords[1])];
1451
- 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])];
1452
1470
 
1453
1471
  function findSegmentIntersections(p0, p1, p2) {
1454
1472
  let pointA = calcSplinePoint(p0, p1, p2, 0.0);
1455
1473
  for (let t = STEP; t < (1.0 + STEP); t += STEP) {
1456
1474
  const pointB = calcSplinePoint(p0, p1, p2, t);
1457
- if (intersects(cellSpaceCoords, ULCoords, pointA, pointB)) {
1475
+ if (influencingPixels[0] && intersects(cellSpaceCoords, ULCoords, pointA, pointB)) {
1458
1476
  influencingPixels[0] = false;
1459
1477
  }
1460
- if (intersects(cellSpaceCoords, URCoords, pointA, pointB)) {
1478
+ if (influencingPixels[1] && intersects(cellSpaceCoords, URCoords, pointA, pointB)) {
1461
1479
  influencingPixels[1] = false;
1462
1480
  }
1463
- if (intersects(cellSpaceCoords, LLCoords, pointA, pointB)) {
1481
+ if (influencingPixels[2] && intersects(cellSpaceCoords, LLCoords, pointA, pointB)) {
1464
1482
  influencingPixels[2] = false;
1465
1483
  }
1466
- if (intersects(cellSpaceCoords, LRCoords, pointA, pointB)) {
1484
+ if (influencingPixels[3] && intersects(cellSpaceCoords, LRCoords, pointA, pointB)) {
1467
1485
  influencingPixels[3] = false;
1468
1486
  }
1469
1487
  pointA = pointB;
@@ -1693,11 +1711,11 @@ function gaussRasterize(src, sim, cell, positions, outW, outH) {
1693
1711
  let weightSum = 0.0;
1694
1712
 
1695
1713
  function addWeightedColor(px, py) {
1696
- const col = fetchPixelRGBA(src, px, py);
1714
+ const col = fetchPixelRGBA8(src, px, py);
1697
1715
  const dx = cellSpaceCoords[0] - px;
1698
1716
  const dy = cellSpaceCoords[1] - py;
1699
- const dist = Math.hypot(dx, dy);
1700
- const weight = Math.exp(-(dist * dist) * GAUSS_MULTIPLIER);
1717
+ const distSq = dx * dx + dy * dy;
1718
+ const weight = exp(-distSq * GAUSS_MULTIPLIER);
1701
1719
  colorSum[0] += col[0] * weight;
1702
1720
  colorSum[1] += col[1] * weight;
1703
1721
  colorSum[2] += col[2] * weight;
@@ -1778,18 +1796,18 @@ function gaussRasterize(src, sim, cell, positions, outW, outH) {
1778
1796
 
1779
1797
  const outIdx = (oy * outW + ox) * 4;
1780
1798
  if (weightSum === 0) {
1781
- const nx = Math.round(cellSpaceCoords[0]);
1782
- const ny = Math.round(cellSpaceCoords[1]);
1799
+ const nx = round(cellSpaceCoords[0]);
1800
+ const ny = round(cellSpaceCoords[1]);
1783
1801
  const col = fetchPixelRGBA8(src, nx, ny);
1784
1802
  out[outIdx] = col[0];
1785
1803
  out[outIdx + 1] = col[1];
1786
1804
  out[outIdx + 2] = col[2];
1787
1805
  out[outIdx + 3] = col[3];
1788
1806
  } else {
1789
- out[outIdx] = clampInt(Math.round((colorSum[0] / weightSum) * 255), 0, 255);
1790
- out[outIdx + 1] = clampInt(Math.round((colorSum[1] / weightSum) * 255), 0, 255);
1791
- out[outIdx + 2] = clampInt(Math.round((colorSum[2] / weightSum) * 255), 0, 255);
1792
- 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);
1793
1811
  }
1794
1812
  }
1795
1813
  }
@@ -1801,9 +1819,9 @@ function runPipeline(src, outH, threshold) {
1801
1819
  const inW = src.width | 0;
1802
1820
  const inH = src.height | 0;
1803
1821
  const outHeight = outH | 0;
1804
- const outWidth = Math.max(1, Math.round((inW / inH) * outHeight));
1822
+ const outWidth = max(1, round((inW / inH) * outHeight));
1805
1823
 
1806
- const similarityThreshold = (typeof threshold === "number") ? threshold : 255;
1824
+ const similarityThreshold = ((typeof threshold === "number") ? threshold : 255) / 255;
1807
1825
 
1808
1826
  const sim0 = buildSimilarityGraph(src, similarityThreshold);
1809
1827
  const sim1 = valenceUpdate(sim0);
@@ -1831,14 +1849,14 @@ function scaleImage(src, opts) {
1831
1849
  const outH = opts.height | 0;
1832
1850
  const threshold = opts.threshold;
1833
1851
 
1834
- const borderPx = Math.max(0, Math.round(opts.borderPx || 0));
1852
+ const borderPx = max(0, round(opts.borderPx || 0));
1835
1853
  if (borderPx > 0) {
1836
1854
  const padW = inW + 2 * borderPx;
1837
1855
  const padH = inH + 2 * borderPx;
1838
1856
  const padData = new Uint8Array(padW * padH * 4);
1839
1857
  // copy src into center, expand into borders
1840
1858
  for (let y = 0; y < padH; y++) {
1841
- const srcRow = Math.min(inH - 1, Math.max(0, y - borderPx)) * inW * 4;
1859
+ const srcRow = min(inH - 1, max(0, y - borderPx)) * inW * 4;
1842
1860
  const dstRow = y * padW * 4 + borderPx * 4;
1843
1861
  padData.set(src.data.subarray(srcRow, srcRow + inW * 4), dstRow);
1844
1862
  for (let ii = 0; ii < borderPx * 4; ++ii) {
@@ -1847,11 +1865,11 @@ function scaleImage(src, opts) {
1847
1865
  }
1848
1866
  }
1849
1867
  const scale = outH / inH;
1850
- const outHpad = Math.max(1, Math.round(padH * scale));
1868
+ const outHpad = max(1, round(padH * scale));
1851
1869
  const padded = runPipeline({ data: padData, width: padW, height: padH }, outHpad, threshold);
1852
1870
 
1853
- const padOut = Math.max(0, Math.round(borderPx * scale));
1854
- 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));
1855
1873
  const cropped = new Uint8Array(outW * outH * 4);
1856
1874
 
1857
1875
  for (let y = 0; y < outH; y++) {
@@ -1861,7 +1879,7 @@ function scaleImage(src, opts) {
1861
1879
  }
1862
1880
  const srcRow = (srcY * padded.width + padOut) * 4;
1863
1881
  const dstRow = y * outW * 4;
1864
- const len = Math.min(outW, Math.max(0, padded.width - padOut)) * 4;
1882
+ const len = min(outW, max(0, padded.width - padOut)) * 4;
1865
1883
  cropped.set(padded.data.subarray(srcRow, srcRow + len), dstRow);
1866
1884
  }
1867
1885
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depixel",
3
- "version": "1.0.3",
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": [