-
Notifications
You must be signed in to change notification settings - Fork 2
/
scaling.js
312 lines (284 loc) · 13.1 KB
/
scaling.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
/**
* Wrapper for resizing functions that extracts an ImageInfo from the
* src/dest canvases and passes it them to the resize function.
*
* @param {Function} fn Resizing function. First 3 inputs are be src/dest ImageInfo and scaleFactor
* @param {HTMLCanvasElement} srcCanvas
* @param {HTMLCanvasElement} destCanvas
*/
function resizeWrapper(fn, srcCanvas, destCanvas) {
let srcImg = ImageInfo.fromCanvas(srcCanvas);
let destImg = ImageInfo.fromCanvas(destCanvas);
let xscale = srcImg.width / destImg.width;
let yscale = srcImg.height / destImg.height;
fn(srcImg, destImg, xscale, yscale);
let destContext = destCanvas.getContext("2d");
destContext.putImageData(destImg.imageData, 0, 0);
}
/**
* Reduce the resolution of the source image and render it into the destination image
* using a nearest-neighbor algorithm.
*
* @param {ImageInfo} srcImg
* @param {ImageInfo} destImg
* @param {Number} xscale 1 / scale factor. 2 = downsample in x by 50%, 4 = downsample by 75%...
* @param {Number} yscale 1 / scale factor. 2 = downsample in y by 50%, 4 = downsample by 75%...
*/
function resizeNearestNeighbor(srcImg, destImg, xscale, yscale) {
let nearestPixel = [0, 0, 0, 0];
for (let dy = 0; dy < destImg.height; dy++) {
nearestY = Math.floor((dy + 0.5) * yscale);
if (nearestY >= srcImg.height) { // Clamp source to edge of image
nearestY = srcImg.height - 1;
}
for (let dx = 0; dx < destImg.width; dx++) {
nearestX = Math.floor((dx + 0.5) * xscale);
if (nearestX >= srcImg.width) { // Clamp to edge of image
nearestX = srcImg.width - 1;
}
srcImg.getPixel(nearestX, nearestY, nearestPixel);
destImg.setPixel(dx, dy, nearestPixel);
}
}
}
/**
* Reduce the resolution of the source image and render it into the destination image.
*
* @param {ImageInfo} srcImg
* @param {ImageInfo} destImg
* @param {Number} xscale 1 / scale factor. 2 = downsample in x by 50%, 4 = downsample by 75%...
* @param {Number} yscale 1 / scale factor. 2 = downsample in y by 50%, 4 = downsample by 75%...
*/
function resizeBox(srcImg, destImg, xscale, yscale) {
let yradius = yscale / 2; // distance from center to edge of dest pixel, in pixels of the src img
let xradius = xscale / 2; // distance from center to edge of dest pixel, in pixels of the src img
let pixel = [0, 0, 0, 0];
let output = [0, 0, 0, 0];
for (let dy = 0; dy < destImg.height; dy++) {
let dcenterY = (dy + 0.5) * yscale;
let dtopY = dcenterY - yradius;
let dbottomY = dcenterY + yradius;
let rowTop = Math.max(Math.floor(dtopY), 0);
let fracTop = Math.min(1.0 - (dtopY - rowTop), 1.0); // Fraction of the top row to use
let rowBottom = Math.min(Math.ceil(dbottomY), srcImg.height);
let fracBottom = Math.min(1.0 - (rowBottom - dbottomY), 1.0); // Fraction of the bottom row to use
for (let dx = 0; dx < destImg.width; dx++) {
let dcenterX = (dx + 0.5) * xscale; // center of the dest pixel on the src img
let dleftX = dcenterX - xradius; // left edge of the dest pixel on the src img
let drightX = dcenterX + xradius; // right edge of the dest pixel on the src img
let boxSize = 0;
output[0] = 0;
output[1] = 0;
output[2] = 0;
output[3] = 0;
// upper left = dleftX, dtopY
// bottom right = drightX, dbottomY
let colLeft = Math.max(Math.floor(dleftX), 0);
let fracLeft = Math.min(1.0 - (dleftX - colLeft));
let colRight = Math.min(Math.ceil(drightX), srcImg.width);
let fracRight = Math.min(1.0 - (colRight - drightX));
for (let y = rowTop; y < rowBottom; y++) {
for (let x = colLeft; x < colRight; x++) {
srcImg.getPixel(x, y, pixel);
// Calculate fraction (weight) of the pixel to use in the box
let weight = 1.0;
if (y == rowTop) weight = weight * fracTop;
if (y == rowBottom - 1) weight = weight * fracBottom;
if (x == colLeft) weight = weight * fracLeft;
if (x == colRight - 1) weight = weight * fracRight;
output[0] += pixel[0] * weight;
output[1] += pixel[1] * weight;
output[2] += pixel[2] * weight;
output[3] += pixel[3] * weight;
boxSize += weight;
}
}
output[0] = output[0] / boxSize;
output[1] = output[1] / boxSize;
output[2] = output[2] / boxSize;
output[3] = 255; // Ignore alpha, the rest of the pipeline doesn't handle it well
destImg.setPixel(dx, dy, output);
}
}
}
/**
* Reduce the resolution of the source image and render it into the destination image
* using a bilinear interpolation algorithm.
*
* @param {ImageInfo} srcImg
* @param {ImageInfo} destImg
* @param {Number} xscale 1 / scale factor. 2 = downsample in x by 50%, 4 = downsample by 75%...
* @param {Number} yscale 1 / scale factor. 2 = downsample in y by 50%, 4 = downsample by 75%...
*/
function resizeBilinear(srcImg, destImg, xscale, yscale) {
let box = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
];
for (let dy = 0; dy < destImg.height; dy++) {
// dcenterX/Y are the center of the dest pixel on the src image
// x1,y1 is the upper-left of the origin pixel in the 2x2 box of source pixels
// we'll used for the filter. x2,y2 is the upper-left of the pixel at
// 1,1 in the 2x2 box of source pixels.
dcenterY = (dy + 0.5) * yscale;
y1 = Math.floor(dcenterY - 0.5);
for (let dx = 0; dx < destImg.width; dx++) {
dcenterX = (dx + 0.5) * xscale;
x1 = Math.floor(dcenterX - 0.5);
let x2 = x1 + 1;
let y2 = y1 + 1;
// Ensure we don't read past the edge of the image
if (x2 == srcImg.width) { x2 = x1; }
if (y2 == srcImg.height) { y2 = y1; }
// Interpolation:
// Find the distance from the dest pixel center to each of the edges
// of the 2x2 box. Weight the src pixels accordingly
let wx = 1.0 - (dcenterX - x1 - 0.5);
let wy = 1.0 - (dcenterY - y1 - 0.5);
let weights = [
wx * wy,
(1.0 - wx) * wy,
wx * (1.0 - wy),
(1.0 - wx) * (1.0 - wy),
];
srcImg.getPixel(x1, y1, box[0]);
srcImg.getPixel(x2, y1, box[1]);
srcImg.getPixel(x1, y2, box[2]);
srcImg.getPixel(x2, y2, box[3]);
// Sum the weighted values of each pixel to find the output pixel
let outputPixel = [0, 0, 0, 255]; // Ignore alpha for now
for (let i = 0; i <= 3; i++) {
let weighted = box[i].map(x => x * weights[i]);
outputPixel[0] += weighted[0];
outputPixel[1] += weighted[1];
outputPixel[2] += weighted[2];
}
destImg.setPixel(dx, dy, outputPixel);
}
}
}
/**
* Reduce the resolution of the source image and render it into the destination image
* using a detail-preserving algorithm adapted from Weber et al.:
* https://download.hrz.tu-darmstadt.de/media/FB20/GCC/dpid/Weber_2016_DPID.pdf
*
* @param {ImageInfo} srcImg
* @param {ImageInfo} destImg
* @param {Number} xscale 1 / scale factor. 2 = downsample in x by 50%, 4 = downsample by 75%...
* @param {Number} yscale 1 / scale factor. 2 = downsample in y by 50%, 4 = downsample by 75%...
*/
function resizeDetailPreserving(srcImg, destImg, xscale, yscale) {
// Max Euclidean distance; 4 = # of channels (RGBA)
const VMAX = Math.sqrt(4 * (255 ** 2));
// Discrete 3x3 gaussian kernel
const KERNEL = [
[1, 2, 1],
[2, 4, 2],
[1, 2, 1],
];
let yradius = yscale / 2; // distance from center to edge of dest pixel, in pixels of the src img
let xradius = xscale / 2; // distance from center to edge of dest pixel, in pixels of the src img
let pixel = [0, 0, 0, 0];
let avgPixel = [0, 0, 0, 0];
let output = [0, 0, 0, 0];
let lambda = 1.0;
// Calculate box-filtered image into an intermediate image
// Uses Array (not Uint8ClampedArray) for better storage of intermediate values
// TODO: Would Uint8ClampedArray work?
let avgImgData = { data: new Array(destImg.width * destImg.height * destImg.pixelStride) };
let avgImg = new ImageInfo(destImg.width, destImg.height, destImg.lineStride,
destImg.pixelStride, avgImgData);
resizeBox(srcImg, avgImg, xscale, yscale);
// Calculate guidance image
for (let dy = 0; dy < destImg.height; dy++) {
let dcenterY = (dy + 0.5) * yscale;
let dtopY = dcenterY - yradius;
let dbottomY = dcenterY + yradius;
let rowTop = Math.max(Math.floor(dtopY), 0);
let fracTop = Math.min(1.0 - (dtopY - rowTop), 1.0); // Fraction of the top row to use
let rowBottom = Math.min(Math.ceil(dbottomY), srcImg.height);
let fracBottom = Math.min(1.0 - (rowBottom - dbottomY), 1.0); // Fraction of the bottom row to use
for (let dx = 0; dx < destImg.width; dx++) {
let dcenterX = (dx + 0.5) * xscale; // center of the dest pixel on the src img
let dleftX = dcenterX - xradius; // left edge of the dest pixel on the src img
let drightX = dcenterX + xradius; // right edge of the dest pixel on the src img
// Apply the 3x3 convolution kernel, ignoring pixels that are off
// the edge of the image
let r=0, g=0, b=0, a=0; // Accumulator pixel
let accD = 0; // Accumulator for denominator of kernel
let xx = 0, yy = 0;
let kk = 0;
for (ky = 0; ky < 3; ky++) {
for (kx = 0; kx < 3; kx++) {
xx = dx + kx - 1; // Subtract one to center the kernel around dx/dy
yy = dy + ky - 1;
if (xx > 0 && yy > 0 && xx < avgImg.width && yy < avgImg.height) {
kk = KERNEL[ky][kx];
avgImg.getPixel(xx, yy, pixel);
r += kk * pixel[0];
g += kk * pixel[1];
b += kk * pixel[2];
a += kk * pixel[3];
accD += kk;
}
}
}
// Normalize
avgPixel = [r / accD, g / accD, b / accD, a / accD];
// Calculate the output image
let weight = 0;
let oF = 0;
output[0] = 0;
output[1] = 0;
output[2] = 0;
output[3] = 0;
// upper left = dleftX, dtopY
// bottom right = drightX, dbottomY
let colLeft = Math.max(Math.floor(dleftX), 0);
let fracLeft = Math.min(1.0 - (dleftX - colLeft));
let colRight = Math.min(Math.ceil(drightX), srcImg.width);
let fracRight = Math.min(1.0 - (colRight - drightX));
for (let y = rowTop; y < rowBottom; y++) {
for (let x = colLeft; x < colRight; x++) {
srcImg.getPixel(x, y, pixel);
if (lambda === 0) {
weight = 1;
} else {
// Find Euclidean distance from the average
// RGB only - ignore alpha
let vr = (avgPixel[0] - pixel[0]) ** 2;
let vg = (avgPixel[1] - pixel[1]) ** 2;
let vb = (avgPixel[2] - pixel[2]) ** 2;
let va = (avgPixel[3] - pixel[3]) ** 2;
weight = Math.sqrt(vr + vg + vb + va);
// Normalize to [0-1] and boost
weight = weight / VMAX;
weight = weight ** lambda;
}
// Calculate fraction (weight) of the pixel to use in the box
if (y == rowTop) weight = weight * fracTop;
if (y == rowBottom - 1) weight = weight * fracBottom;
if (x == colLeft) weight = weight * fracLeft;
if (x == colRight - 1) weight = weight * fracRight;
output[0] += pixel[0] * weight;
output[1] += pixel[1] * weight;
output[2] += pixel[2] * weight;
output[3] += pixel[3] * weight;
oF += weight;
}
}
if (oF === 0) {
// Result is same as box filter
destImg.setPixel(dx, dy, avgPixel);
} else {
output[0] = output[0] / oF;
output[1] = output[1] / oF;
output[2] = output[2] / oF;
output[3] = output[3] / oF;
destImg.setPixel(dx, dy, output);
}
}
}
}