-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Image.js
392 lines (364 loc) · 15.7 KB
/
Image.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
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
(function() {
/**
* Image utility.
* @static
* @constructor
*/
tracking.Image = {};
/**
* Computes gaussian blur. Adapted from
* https://github.com/kig/canvasfilters.
* @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
* @param {number} width The image width.
* @param {number} height The image height.
* @param {number} diameter Gaussian blur diameter, must be greater than 1.
* @return {array} The edge pixels in a linear [r,g,b,a,...] array.
*/
tracking.Image.blur = function(pixels, width, height, diameter) {
diameter = Math.abs(diameter);
if (diameter <= 1) {
throw new Error('Diameter should be greater than 1.');
}
var radius = diameter / 2;
var len = Math.ceil(diameter) + (1 - (Math.ceil(diameter) % 2));
var weights = new Float32Array(len);
var rho = (radius + 0.5) / 3;
var rhoSq = rho * rho;
var gaussianFactor = 1 / Math.sqrt(2 * Math.PI * rhoSq);
var rhoFactor = -1 / (2 * rho * rho);
var wsum = 0;
var middle = Math.floor(len / 2);
for (var i = 0; i < len; i++) {
var x = i - middle;
var gx = gaussianFactor * Math.exp(x * x * rhoFactor);
weights[i] = gx;
wsum += gx;
}
for (var j = 0; j < weights.length; j++) {
weights[j] /= wsum;
}
return this.separableConvolve(pixels, width, height, weights, weights, false);
};
/**
* Computes the integral image for summed, squared, rotated and sobel pixels.
* @param {array} pixels The pixels in a linear [r,g,b,a,...] array to loop
* through.
* @param {number} width The image width.
* @param {number} height The image height.
* @param {array} opt_integralImage Empty array of size `width * height` to
* be filled with the integral image values. If not specified compute sum
* values will be skipped.
* @param {array} opt_integralImageSquare Empty array of size `width *
* height` to be filled with the integral image squared values. If not
* specified compute squared values will be skipped.
* @param {array} opt_tiltedIntegralImage Empty array of size `width *
* height` to be filled with the rotated integral image values. If not
* specified compute sum values will be skipped.
* @param {array} opt_integralImageSobel Empty array of size `width *
* height` to be filled with the integral image of sobel values. If not
* specified compute sobel filtering will be skipped.
* @static
*/
tracking.Image.computeIntegralImage = function(pixels, width, height, opt_integralImage, opt_integralImageSquare, opt_tiltedIntegralImage, opt_integralImageSobel) {
if (arguments.length < 4) {
throw new Error('You should specify at least one output array in the order: sum, square, tilted, sobel.');
}
var pixelsSobel;
if (opt_integralImageSobel) {
pixelsSobel = tracking.Image.sobel(pixels, width, height);
}
for (var i = 0; i < height; i++) {
for (var j = 0; j < width; j++) {
var w = i * width * 4 + j * 4;
var pixel = ~~(pixels[w] * 0.299 + pixels[w + 1] * 0.587 + pixels[w + 2] * 0.114);
if (opt_integralImage) {
this.computePixelValueSAT_(opt_integralImage, width, i, j, pixel);
}
if (opt_integralImageSquare) {
this.computePixelValueSAT_(opt_integralImageSquare, width, i, j, pixel * pixel);
}
if (opt_tiltedIntegralImage) {
var w1 = w - width * 4;
var pixelAbove = ~~(pixels[w1] * 0.299 + pixels[w1 + 1] * 0.587 + pixels[w1 + 2] * 0.114);
this.computePixelValueRSAT_(opt_tiltedIntegralImage, width, i, j, pixel, pixelAbove || 0);
}
if (opt_integralImageSobel) {
this.computePixelValueSAT_(opt_integralImageSobel, width, i, j, pixelsSobel[w]);
}
}
}
};
/**
* Helper method to compute the rotated summed area table (RSAT) by the
* formula:
*
* RSAT(x, y) = RSAT(x-1, y-1) + RSAT(x+1, y-1) - RSAT(x, y-2) + I(x, y) + I(x, y-1)
*
* @param {number} width The image width.
* @param {array} RSAT Empty array of size `width * height` to be filled with
* the integral image values. If not specified compute sum values will be
* skipped.
* @param {number} i Vertical position of the pixel to be evaluated.
* @param {number} j Horizontal position of the pixel to be evaluated.
* @param {number} pixel Pixel value to be added to the integral image.
* @static
* @private
*/
tracking.Image.computePixelValueRSAT_ = function(RSAT, width, i, j, pixel, pixelAbove) {
var w = i * width + j;
RSAT[w] = (RSAT[w - width - 1] || 0) + (RSAT[w - width + 1] || 0) - (RSAT[w - width - width] || 0) + pixel + pixelAbove;
};
/**
* Helper method to compute the summed area table (SAT) by the formula:
*
* SAT(x, y) = SAT(x, y-1) + SAT(x-1, y) + I(x, y) - SAT(x-1, y-1)
*
* @param {number} width The image width.
* @param {array} SAT Empty array of size `width * height` to be filled with
* the integral image values. If not specified compute sum values will be
* skipped.
* @param {number} i Vertical position of the pixel to be evaluated.
* @param {number} j Horizontal position of the pixel to be evaluated.
* @param {number} pixel Pixel value to be added to the integral image.
* @static
* @private
*/
tracking.Image.computePixelValueSAT_ = function(SAT, width, i, j, pixel) {
var w = i * width + j;
SAT[w] = (SAT[w - width] || 0) + (SAT[w - 1] || 0) + pixel - (SAT[w - width - 1] || 0);
};
/**
* Converts a color from a color-space based on an RGB color model to a
* grayscale representation of its luminance. The coefficients represent the
* measured intensity perception of typical trichromat humans, in
* particular, human vision is most sensitive to green and least sensitive
* to blue.
* @param {Uint8Array|Uint8ClampedArray|Array} pixels The pixels in a linear [r,g,b,a,...] array.
* @param {number} width The image width.
* @param {number} height The image height.
* @param {boolean} fillRGBA If the result should fill all RGBA values with the gray scale
* values, instead of returning a single value per pixel.
* @return {Uint8Array} The grayscale pixels in a linear array ([p,p,p,a,...] if fillRGBA
* is true and [p1, p2, p3, ...] if fillRGBA is false).
* @static
*/
tracking.Image.grayscale = function(pixels, width, height, fillRGBA) {
/*
Performance result (rough EST. - image size, CPU arch. will affect):
https://jsperf.com/tracking-new-image-to-grayscale
Firefox v.60b:
fillRGBA Gray only
Old 11 551 OPs/sec
New 3548 6487 OPs/sec
---------------------------------
322.5x 11.8x faster
Chrome v.67b:
fillRGBA Gray only
Old 291 489 OPs/sec
New 6975 6635 OPs/sec
---------------------------------
24.0x 13.6x faster
- Ken Nilsen / epistemex
*/
var len = pixels.length>>2;
var gray = fillRGBA ? new Uint32Array(len) : new Uint8Array(len);
var data32 = new Uint32Array(pixels.buffer || new Uint8Array(pixels).buffer);
var i = 0;
var c = 0;
var luma = 0;
// unrolled loops to not have to check fillRGBA each iteration
if (fillRGBA) {
while(i < len) {
// Entire pixel in little-endian order (ABGR)
c = data32[i];
// Using the more up-to-date REC/BT.709 approx. weights for luma instead: [0.2126, 0.7152, 0.0722].
// luma = ((c>>>16 & 0xff) * 0.2126 + (c>>>8 & 0xff) * 0.7152 + (c & 0xff) * 0.0722 + 0.5)|0;
// But I'm using scaled integers here for speed (x 0xffff). This can be improved more using 2^n
// close to the factors allowing for shift-ops (i.e. 4732 -> 4096 => .. (c&0xff) << 12 .. etc.)
// if "accuracy" is not important (luma is anyway an visual approx.):
luma = ((c>>>16&0xff) * 13933 + (c>>>8&0xff) * 46871 + (c&0xff) * 4732)>>>16;
gray[i++] = luma * 0x10101 | c & 0xff000000;
}
}
else {
while(i < len) {
c = data32[i];
luma = ((c>>>16&0xff) * 13933 + (c>>>8&0xff) * 46871 + (c&0xff) * 4732)>>>16;
// ideally, alpha should affect value here: value * (alpha/255) or with shift-ops for the above version
gray[i++] = luma;
}
}
// Consolidate array view to byte component format independent of source view
return new Uint8Array(gray.buffer);
};
/**
* Fast horizontal separable convolution. A point spread function (PSF) is
* said to be separable if it can be broken into two one-dimensional
* signals: a vertical and a horizontal projection. The convolution is
* performed by sliding the kernel over the image, generally starting at the
* top left corner, so as to move the kernel through all the positions where
* the kernel fits entirely within the boundaries of the image. Adapted from
* https://github.com/kig/canvasfilters.
* @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
* @param {number} width The image width.
* @param {number} height The image height.
* @param {array} weightsVector The weighting vector, e.g [-1,0,1].
* @param {number} opaque
* @return {array} The convoluted pixels in a linear [r,g,b,a,...] array.
*/
tracking.Image.horizontalConvolve = function(pixels, width, height, weightsVector, opaque) {
var side = weightsVector.length;
var halfSide = Math.floor(side / 2);
var output = new Float32Array(width * height * 4);
var alphaFac = opaque ? 1 : 0;
for (var y = 0; y < height; y++) {
for (var x = 0; x < width; x++) {
var sy = y;
var sx = x;
var offset = (y * width + x) * 4;
var r = 0;
var g = 0;
var b = 0;
var a = 0;
for (var cx = 0; cx < side; cx++) {
var scy = sy;
var scx = Math.min(width - 1, Math.max(0, sx + cx - halfSide));
var poffset = (scy * width + scx) * 4;
var wt = weightsVector[cx];
r += pixels[poffset] * wt;
g += pixels[poffset + 1] * wt;
b += pixels[poffset + 2] * wt;
a += pixels[poffset + 3] * wt;
}
output[offset] = r;
output[offset + 1] = g;
output[offset + 2] = b;
output[offset + 3] = a + alphaFac * (255 - a);
}
}
return output;
};
/**
* Fast vertical separable convolution. A point spread function (PSF) is
* said to be separable if it can be broken into two one-dimensional
* signals: a vertical and a horizontal projection. The convolution is
* performed by sliding the kernel over the image, generally starting at the
* top left corner, so as to move the kernel through all the positions where
* the kernel fits entirely within the boundaries of the image. Adapted from
* https://github.com/kig/canvasfilters.
* @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
* @param {number} width The image width.
* @param {number} height The image height.
* @param {array} weightsVector The weighting vector, e.g [-1,0,1].
* @param {number} opaque
* @return {array} The convoluted pixels in a linear [r,g,b,a,...] array.
*/
tracking.Image.verticalConvolve = function(pixels, width, height, weightsVector, opaque) {
var side = weightsVector.length;
var halfSide = Math.floor(side / 2);
var output = new Float32Array(width * height * 4);
var alphaFac = opaque ? 1 : 0;
for (var y = 0; y < height; y++) {
for (var x = 0; x < width; x++) {
var sy = y;
var sx = x;
var offset = (y * width + x) * 4;
var r = 0;
var g = 0;
var b = 0;
var a = 0;
for (var cy = 0; cy < side; cy++) {
var scy = Math.min(height - 1, Math.max(0, sy + cy - halfSide));
var scx = sx;
var poffset = (scy * width + scx) * 4;
var wt = weightsVector[cy];
r += pixels[poffset] * wt;
g += pixels[poffset + 1] * wt;
b += pixels[poffset + 2] * wt;
a += pixels[poffset + 3] * wt;
}
output[offset] = r;
output[offset + 1] = g;
output[offset + 2] = b;
output[offset + 3] = a + alphaFac * (255 - a);
}
}
return output;
};
/**
* Fast separable convolution. A point spread function (PSF) is said to be
* separable if it can be broken into two one-dimensional signals: a
* vertical and a horizontal projection. The convolution is performed by
* sliding the kernel over the image, generally starting at the top left
* corner, so as to move the kernel through all the positions where the
* kernel fits entirely within the boundaries of the image. Adapted from
* https://github.com/kig/canvasfilters.
* @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
* @param {number} width The image width.
* @param {number} height The image height.
* @param {array} horizWeights The horizontal weighting vector, e.g [-1,0,1].
* @param {array} vertWeights The vertical vector, e.g [-1,0,1].
* @param {number} opaque
* @return {array} The convoluted pixels in a linear [r,g,b,a,...] array.
*/
tracking.Image.separableConvolve = function(pixels, width, height, horizWeights, vertWeights, opaque) {
var vertical = this.verticalConvolve(pixels, width, height, vertWeights, opaque);
return this.horizontalConvolve(vertical, width, height, horizWeights, opaque);
};
/**
* Compute image edges using Sobel operator. Computes the vertical and
* horizontal gradients of the image and combines the computed images to
* find edges in the image. The way we implement the Sobel filter here is by
* first grayscaling the image, then taking the horizontal and vertical
* gradients and finally combining the gradient images to make up the final
* image. Adapted from https://github.com/kig/canvasfilters.
* @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
* @param {number} width The image width.
* @param {number} height The image height.
* @return {array} The edge pixels in a linear [r,g,b,a,...] array.
*/
tracking.Image.sobel = function(pixels, width, height) {
pixels = this.grayscale(pixels, width, height, true);
var output = new Float32Array(width * height * 4);
var sobelSignVector = new Float32Array([-1, 0, 1]);
var sobelScaleVector = new Float32Array([1, 2, 1]);
var vertical = this.separableConvolve(pixels, width, height, sobelSignVector, sobelScaleVector);
var horizontal = this.separableConvolve(pixels, width, height, sobelScaleVector, sobelSignVector);
for (var i = 0; i < output.length; i += 4) {
var v = vertical[i];
var h = horizontal[i];
var p = Math.sqrt(h * h + v * v);
output[i] = p;
output[i + 1] = p;
output[i + 2] = p;
output[i + 3] = 255;
}
return output;
};
/**
* Equalizes the histogram of a grayscale image, normalizing the
* brightness and increasing the contrast of the image.
* @param {pixels} pixels The grayscale pixels in a linear array.
* @param {number} width The image width.
* @param {number} height The image height.
* @return {array} The equalized grayscale pixels in a linear array.
*/
tracking.Image.equalizeHist = function(pixels, width, height){
var equalized = new Uint8ClampedArray(pixels.length);
var histogram = new Array(256);
for(var i=0; i < 256; i++) histogram[i] = 0;
for(var i=0; i < pixels.length; i++){
equalized[i] = pixels[i];
histogram[pixels[i]]++;
}
var prev = histogram[0];
for(var i=0; i < 256; i++){
histogram[i] += prev;
prev = histogram[i];
}
var norm = 255 / pixels.length;
for(var i=0; i < pixels.length; i++)
equalized[i] = (histogram[pixels[i]] * norm + 0.5) | 0;
return equalized;
}
}());