1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
8 * A namespace for image filter utilities.
13 * Create a filter from name and options.
15 * @param {string} name Maps to a filter method name.
16 * @param {Object} options A map of filter-specific options.
17 * @return {function(ImageData,ImageData,number,number)} created function.
19 filter.create = function(name, options) {
20 var filterFunc = filter[name](options);
22 var time = Date.now();
23 filterFunc.apply(null, arguments);
24 var dst = arguments[0];
25 var mPixPerSec = dst.width * dst.height / 1000 / (Date.now() - time);
26 ImageUtil.trace.report(name, Math.round(mPixPerSec * 10) / 10 + 'Mps');
31 * Apply a filter to a image by splitting it into strips.
33 * To be used with large images to avoid freezing up the UI.
35 * @param {HTMLCanvasElement} dstCanvas Destination canvas.
36 * @param {HTMLCanvasElement} srcCanvas Source canvas.
37 * @param {function(ImageData,ImageData,number,number)} filterFunc Filter.
38 * @param {function(number, number)} progressCallback Progress callback.
39 * @param {number} maxPixelsPerStrip Pixel number to process at once.
41 filter.applyByStrips = function(
42 dstCanvas, srcCanvas, filterFunc, progressCallback, maxPixelsPerStrip) {
43 var dstContext = dstCanvas.getContext('2d');
44 var srcContext = srcCanvas.getContext('2d');
45 var source = srcContext.getImageData(0, 0, srcCanvas.width, srcCanvas.height);
47 var stripCount = Math.ceil(srcCanvas.width * srcCanvas.height /
48 (maxPixelsPerStrip || 1000000)); // 1 Mpix is a reasonable default.
50 var strip = srcContext.getImageData(0, 0,
51 srcCanvas.width, Math.ceil(srcCanvas.height / stripCount));
55 function filterStrip() {
56 // If the strip overlaps the bottom of the source image we cannot shrink it
57 // and we cannot fill it partially (since canvas.putImageData always draws
58 // the entire buffer).
59 // Instead we move the strip up several lines (converting those lines
60 // twice is a small price to pay).
61 if (offset > source.height - strip.height) {
62 offset = source.height - strip.height;
65 filterFunc(strip, source, 0, offset);
66 dstContext.putImageData(strip, 0, offset);
68 offset += strip.height;
70 if (offset < source.height) {
71 setTimeout(filterStrip, 0);
73 ImageUtil.trace.reportTimer('filter-commit');
76 progressCallback(offset, source.height);
79 ImageUtil.trace.resetTimer('filter-commit');
84 * Return a color histogram for an image.
86 * @param {HTMLCanvasElement|ImageData} source Image data to analyze.
87 * @return {{r: Array.<number>, g: Array.<number>, b: Array.<number>}}
90 filter.getHistogram = function(source) {
92 if (source.constructor.name == 'HTMLCanvasElement') {
93 imageData = source.getContext('2d').
94 getImageData(0, 0, source.width, source.height);
103 for (var i = 0; i != 256; i++) {
109 var data = imageData.data;
110 var maxIndex = 4 * imageData.width * imageData.height;
111 for (var index = 0; index != maxIndex;) {
118 return { r: r, g: g, b: b };
122 * Compute the function for every integer value from 0 up to maxArg.
124 * Rounds and clips the results to fit the [0..255] range.
125 * Useful to speed up pixel manipulations.
127 * @param {number} maxArg Maximum argument value (inclusive).
128 * @param {function(number): number} func Function to precompute.
129 * @return {Uint8Array} Computed results.
131 filter.precompute = function(maxArg, func) {
132 var results = new Uint8Array(maxArg + 1);
133 for (var arg = 0; arg <= maxArg; arg++) {
134 results[arg] = Math.max(0, Math.min(0xFF, Math.round(func(arg))));
140 * Convert pixels by applying conversion tables to each channel individually.
142 * @param {Array.<number>} rMap Red channel conversion table.
143 * @param {Array.<number>} gMap Green channel conversion table.
144 * @param {Array.<number>} bMap Blue channel conversion table.
145 * @param {ImageData} dst Destination image data. Can be smaller than the
146 * source, must completely fit inside the source.
147 * @param {ImageData} src Source image data.
148 * @param {number} offsetX Horizontal offset of dst relative to src.
149 * @param {number} offsetY Vertical offset of dst relative to src.
151 filter.mapPixels = function(rMap, gMap, bMap, dst, src, offsetX, offsetY) {
152 var dstData = dst.data;
153 var dstWidth = dst.width;
154 var dstHeight = dst.height;
156 var srcData = src.data;
157 var srcWidth = src.width;
158 var srcHeight = src.height;
160 if (offsetX < 0 || offsetX + dstWidth > srcWidth ||
161 offsetY < 0 || offsetY + dstHeight > srcHeight)
162 throw new Error('Invalid offset');
165 for (var y = 0; y != dstHeight; y++) {
166 var srcIndex = (offsetX + (offsetY + y) * srcWidth) * 4;
167 for (var x = 0; x != dstWidth; x++) {
168 dstData[dstIndex++] = rMap[srcData[srcIndex++]];
169 dstData[dstIndex++] = gMap[srcData[srcIndex++]];
170 dstData[dstIndex++] = bMap[srcData[srcIndex++]];
178 * Number of digits after period(in binary form) to preserve.
181 filter.FIXED_POINT_SHIFT = 16;
184 * Maximum value that can be represented in fixed point without overflow.
187 filter.MAX_FLOAT_VALUE = 0x7FFFFFFF >> filter.FIXED_POINT_SHIFT;
190 * Converts floating point to fixed.
191 * @param {number} x Number to convert.
192 * @return {number} Converted number.
194 filter.floatToFixedPoint = function(x) {
195 // Math.round on negative arguments causes V8 to deoptimize the calling
196 // function, so we are using >> 0 instead.
197 return (x * (1 << filter.FIXED_POINT_SHIFT)) >> 0;
201 * Perform an image convolution with a symmetrical 5x5 matrix:
209 * @param {Array.<number>} weights See the picture above.
210 * @param {ImageData} dst Destination image data. Can be smaller than the
211 * source, must completely fit inside the source.
212 * @param {ImageData} src Source image data.
213 * @param {number} offsetX Horizontal offset of dst relative to src.
214 * @param {number} offsetY Vertical offset of dst relative to src.
216 filter.convolve5x5 = function(weights, dst, src, offsetX, offsetY) {
217 var w0 = filter.floatToFixedPoint(weights[0]);
218 var w1 = filter.floatToFixedPoint(weights[1]);
219 var w2 = filter.floatToFixedPoint(weights[2]);
220 var w3 = filter.floatToFixedPoint(weights[3]);
222 var dstData = dst.data;
223 var dstWidth = dst.width;
224 var dstHeight = dst.height;
225 var dstStride = dstWidth * 4;
227 var srcData = src.data;
228 var srcWidth = src.width;
229 var srcHeight = src.height;
230 var srcStride = srcWidth * 4;
231 var srcStride2 = srcStride * 2;
233 if (offsetX < 0 || offsetX + dstWidth > srcWidth ||
234 offsetY < 0 || offsetY + dstHeight > srcHeight)
235 throw new Error('Invalid offset');
237 // Javascript is not very good at inlining constants.
238 // We inline manually and assert that the constant is equal to the variable.
239 if (filter.FIXED_POINT_SHIFT != 16)
240 throw new Error('Wrong fixed point shift');
244 var startX = Math.max(0, margin - offsetX);
245 var endX = Math.min(dstWidth, srcWidth - margin - offsetX);
247 var startY = Math.max(0, margin - offsetY);
248 var endY = Math.min(dstHeight, srcHeight - margin - offsetY);
250 for (var y = startY; y != endY; y++) {
251 var dstIndex = y * dstStride + startX * 4;
252 var srcIndex = (y + offsetY) * srcStride + (startX + offsetX) * 4;
254 for (var x = startX; x != endX; x++) {
255 for (var c = 0; c != 3; c++) {
256 var sum = w0 * srcData[srcIndex] +
257 w1 * (srcData[srcIndex - 4] +
258 srcData[srcIndex + 4] +
259 srcData[srcIndex - srcStride] +
260 srcData[srcIndex + srcStride]) +
261 w2 * (srcData[srcIndex - srcStride - 4] +
262 srcData[srcIndex + srcStride - 4] +
263 srcData[srcIndex - srcStride + 4] +
264 srcData[srcIndex + srcStride + 4]) +
265 w3 * (srcData[srcIndex - 8] +
266 srcData[srcIndex + 8] +
267 srcData[srcIndex - srcStride2] +
268 srcData[srcIndex + srcStride2]);
270 dstData[dstIndex++] = 0;
271 else if (sum > 0xFF0000)
272 dstData[dstIndex++] = 0xFF;
274 dstData[dstIndex++] = sum >> 16;
284 * Compute the average color for the image.
286 * @param {ImageData} imageData Image data to analyze.
287 * @return {{r: number, g: number, b: number}} average color.
289 filter.getAverageColor = function(imageData) {
290 var data = imageData.data;
291 var width = imageData.width;
292 var height = imageData.height;
299 var maxIndex = 4 * width * height;
300 for (var i = 0; i != maxIndex;) {
307 if (total == 0) return { r: 0, g: 0, b: 0 };
308 return { r: r / total, g: g / total, b: b / total };
312 * Compute the average color with more weight given to pixes at the center.
314 * @param {ImageData} imageData Image data to analyze.
315 * @return {{r: number, g: number, b: number}} weighted average color.
317 filter.getWeightedAverageColor = function(imageData) {
318 var data = imageData.data;
319 var width = imageData.width;
320 var height = imageData.height;
327 var center = Math.floor(width / 2);
328 var maxDist = center * Math.sqrt(2);
329 maxDist *= 2; // Weaken the effect of distance
332 for (var x = 0; x != width; x++) {
333 for (var y = 0; y != height; y++) {
334 var dist = Math.sqrt(
335 (x - center) * (x - center) + (y - center) * (y - center));
336 var weight = (maxDist - dist) / maxDist;
339 r += data[i++] * weight;
340 g += data[i++] * weight;
341 b += data[i++] * weight;
345 if (total == 0) return { r: 0, g: 0, b: 0 };
346 return { r: r / total, g: g / total, b: b / total };
350 * Copy part of src image to dst, applying matrix color filter on-the-fly.
352 * The copied part of src should completely fit into dst (there is no clipping
355 * @param {Array.<number>} matrix 3x3 color matrix.
356 * @param {ImageData} dst Destination image data.
357 * @param {ImageData} src Source image data.
358 * @param {number} offsetX X offset in source to start processing.
359 * @param {number} offsetY Y offset in source to start processing.
361 filter.colorMatrix3x3 = function(matrix, dst, src, offsetX, offsetY) {
362 var c11 = filter.floatToFixedPoint(matrix[0]);
363 var c12 = filter.floatToFixedPoint(matrix[1]);
364 var c13 = filter.floatToFixedPoint(matrix[2]);
365 var c21 = filter.floatToFixedPoint(matrix[3]);
366 var c22 = filter.floatToFixedPoint(matrix[4]);
367 var c23 = filter.floatToFixedPoint(matrix[5]);
368 var c31 = filter.floatToFixedPoint(matrix[6]);
369 var c32 = filter.floatToFixedPoint(matrix[7]);
370 var c33 = filter.floatToFixedPoint(matrix[8]);
372 var dstData = dst.data;
373 var dstWidth = dst.width;
374 var dstHeight = dst.height;
376 var srcData = src.data;
377 var srcWidth = src.width;
378 var srcHeight = src.height;
380 if (offsetX < 0 || offsetX + dstWidth > srcWidth ||
381 offsetY < 0 || offsetY + dstHeight > srcHeight)
382 throw new Error('Invalid offset');
384 // Javascript is not very good at inlining constants.
385 // We inline manually and assert that the constant is equal to the variable.
386 if (filter.FIXED_POINT_SHIFT != 16)
387 throw new Error('Wrong fixed point shift');
390 for (var y = 0; y != dstHeight; y++) {
391 var srcIndex = (offsetX + (offsetY + y) * srcWidth) * 4;
392 for (var x = 0; x != dstWidth; x++) {
393 var r = srcData[srcIndex++];
394 var g = srcData[srcIndex++];
395 var b = srcData[srcIndex++];
398 var rNew = r * c11 + g * c12 + b * c13;
399 var gNew = r * c21 + g * c22 + b * c23;
400 var bNew = r * c31 + g * c32 + b * c33;
403 dstData[dstIndex++] = 0;
404 } else if (rNew > 0xFF0000) {
405 dstData[dstIndex++] = 0xFF;
407 dstData[dstIndex++] = rNew >> 16;
411 dstData[dstIndex++] = 0;
412 } else if (gNew > 0xFF0000) {
413 dstData[dstIndex++] = 0xFF;
415 dstData[dstIndex++] = gNew >> 16;
419 dstData[dstIndex++] = 0;
420 } else if (bNew > 0xFF0000) {
421 dstData[dstIndex++] = 0xFF;
423 dstData[dstIndex++] = bNew >> 16;
432 * Return a convolution filter function bound to specific weights.
434 * @param {Array.<number>} weights Weights for the convolution matrix
436 * @return {function(ImageData,ImageData,number,number)} Convolution filter.
438 filter.createConvolutionFilter = function(weights) {
439 // Normalize the weights to sum to 1.
441 for (var i = 0; i != weights.length; i++) {
442 total += weights[i] * (i ? 4 : 1);
446 for (i = 0; i != weights.length; i++) {
447 normalized.push(weights[i] / total);
453 var maxWeightedSum = 0xFF *
454 Math.abs(normalized[0]) +
455 Math.abs(normalized[1]) * 4 +
456 Math.abs(normalized[2]) * 4 +
457 Math.abs(normalized[3]) * 4;
458 if (maxWeightedSum > filter.MAX_FLOAT_VALUE)
459 throw new Error('convolve5x5 cannot convert the weights to fixed point');
461 return filter.convolve5x5.bind(null, normalized);
465 * Creates matrix filter.
466 * @param {Array.<number>} matrix Color transformation matrix.
467 * @return {function(ImageData,ImageData,number,number)} Matrix filter.
469 filter.createColorMatrixFilter = function(matrix) {
470 for (var r = 0; r != 3; r++) {
472 for (var c = 0; c != 3; c++) {
473 maxRowSum += 0xFF * Math.abs(matrix[r * 3 + c]);
475 if (maxRowSum > filter.MAX_FLOAT_VALUE)
477 'colorMatrix3x3 cannot convert the matrix to fixed point');
479 return filter.colorMatrix3x3.bind(null, matrix);
483 * Return a blur filter.
484 * @param {Object} options Blur options.
485 * @return {function(ImageData,ImageData,number,number)} Blur filter.
487 filter.blur = function(options) {
488 if (options.radius == 1)
489 return filter.createConvolutionFilter(
490 [1, options.strength]);
491 else if (options.radius == 2)
492 return filter.createConvolutionFilter(
493 [1, options.strength, options.strength]);
495 return filter.createConvolutionFilter(
496 [1, options.strength, options.strength, options.strength]);
500 * Return a sharpen filter.
501 * @param {Object} options Sharpen options.
502 * @return {function(ImageData,ImageData,number,number)} Sharpen filter.
504 filter.sharpen = function(options) {
505 if (options.radius == 1)
506 return filter.createConvolutionFilter(
507 [5, -options.strength]);
508 else if (options.radius == 2)
509 return filter.createConvolutionFilter(
510 [10, -options.strength, -options.strength]);
512 return filter.createConvolutionFilter(
513 [15, -options.strength, -options.strength, -options.strength]);
517 * Return an exposure filter.
518 * @param {Object} options exposure options.
519 * @return {function(ImageData,ImageData,number,number)} Exposure filter.
521 filter.exposure = function(options) {
522 var pixelMap = filter.precompute(
525 if (options.brightness > 0) {
526 value *= (1 + options.brightness);
528 value += (0xFF - value) * options.brightness;
531 (value - 0x80) * Math.tan((options.contrast + 1) * Math.PI / 4);
534 return filter.mapPixels.bind(null, pixelMap, pixelMap, pixelMap);
538 * Return a color autofix filter.
539 * @param {Object} options Histogram for autofix.
540 * @return {function(ImageData,ImageData,number,number)} Autofix filter.
542 filter.autofix = function(options) {
543 return filter.mapPixels.bind(null,
544 filter.autofix.stretchColors(options.histogram.r),
545 filter.autofix.stretchColors(options.histogram.g),
546 filter.autofix.stretchColors(options.histogram.b));
550 * Return a conversion table that stretches the range of colors used
551 * in the image to 0..255.
552 * @param {Array.<number>} channelHistogram Histogram to calculate range.
553 * @return {Uint8Array} Color mapping array.
555 filter.autofix.stretchColors = function(channelHistogram) {
556 var range = filter.autofix.getRange(channelHistogram);
557 return filter.precompute(
560 return (x - range.first) / (range.last - range.first) * 255;
566 * Return a range that encloses non-zero elements values in a histogram array.
567 * @param {Array.<number>} channelHistogram Histogram to analyze.
568 * @return {{first: number, last: number}} Channel range in histogram.
570 filter.autofix.getRange = function(channelHistogram) {
572 while (first < channelHistogram.length && channelHistogram[first] == 0)
575 var last = channelHistogram.length - 1;
576 while (last >= 0 && channelHistogram[last] == 0)
579 if (first >= last) // Stretching does not make sense
580 return {first: 0, last: channelHistogram.length - 1};
582 return {first: first, last: last};
586 * Minimum channel offset that makes visual difference. If autofix calculated
587 * offset is less than SENSITIVITY, probably autofix is not needed.
588 * Reasonable empirical value.
591 filter.autofix.SENSITIVITY = 8;
594 * @param {Array.<number>} channelHistogram Histogram to analyze.
595 * @return {boolean} True if stretching this range to 0..255 would make
596 * a visible difference.
598 filter.autofix.needsStretching = function(channelHistogram) {
599 var range = filter.autofix.getRange(channelHistogram);
600 return (range.first >= filter.autofix.SENSITIVITY ||
601 range.last <= 255 - filter.autofix.SENSITIVITY);
605 * @param {{r: Array.<number>, g: Array.<number>, b: Array.<number>}} histogram
606 * @return {boolean} True if the autofix would make a visible difference.
608 filter.autofix.isApplicable = function(histogram) {
609 return filter.autofix.needsStretching(histogram.r) ||
610 filter.autofix.needsStretching(histogram.g) ||
611 filter.autofix.needsStretching(histogram.b);