494 lines
20 KiB
JavaScript
494 lines
20 KiB
JavaScript
/*
|
|
* This software was developed at the National Institute of Standards and
|
|
* Technology by employees of the Federal Government in the course of
|
|
* their official duties. Pursuant to title 17 Section 105 of the United
|
|
* States Code this software is not subject to copyright protection and is
|
|
* in the public domain. This software is an experimental system. NIST assumes
|
|
* no responsibility whatsoever for its use by other parties, and makes no
|
|
* guarantees, expressed or implied, about its quality, reliability, or
|
|
* any other characteristic. We would appreciate acknowledgement if the
|
|
* software is used.
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @author Antoine Vandecreme <antoine.vandecreme@nist.gov>
|
|
*/
|
|
(function() {
|
|
|
|
'use strict';
|
|
|
|
var $ = window.OpenSeadragon;
|
|
if (!$) {
|
|
$ = require('openseadragon');
|
|
if (!$) {
|
|
throw new Error('OpenSeadragon is missing.');
|
|
}
|
|
}
|
|
// Requires OpenSeadragon >=2.1
|
|
if (!$.version || $.version.major < 2 ||
|
|
$.version.major === 2 && $.version.minor < 1) {
|
|
throw new Error(
|
|
'Filtering plugin requires OpenSeadragon version >= 2.1');
|
|
}
|
|
|
|
$.Viewer.prototype.setFilterOptions = function(options) {
|
|
if (!this.filterPluginInstance) {
|
|
options = options || {};
|
|
options.viewer = this;
|
|
this.filterPluginInstance = new $.FilterPlugin(options);
|
|
} else {
|
|
setOptions(this.filterPluginInstance, options);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @class FilterPlugin
|
|
* @param {Object} options The options
|
|
* @param {OpenSeadragon.Viewer} options.viewer The viewer to attach this
|
|
* plugin to.
|
|
* @param {String} [options.loadMode='async'] Set to sync to have the filters
|
|
* applied synchronously. It will only work if the filters are all synchronous.
|
|
* Note that depending on how complex the filters are, it may also hang the browser.
|
|
* @param {Object[]} options.filters The filters to apply to the images.
|
|
* @param {OpenSeadragon.TiledImage[]} options.filters[x].items The tiled images
|
|
* on which to apply the filter.
|
|
* @param {function|function[]} options.filters[x].processors The processing
|
|
* function(s) to apply to the images. The parameters of this function are
|
|
* the context to modify and a callback to call upon completion.
|
|
*/
|
|
$.FilterPlugin = function(options) {
|
|
options = options || {};
|
|
if (!options.viewer) {
|
|
throw new Error('A viewer must be specified.');
|
|
}
|
|
var self = this;
|
|
this.viewer = options.viewer;
|
|
|
|
this.viewer.addHandler('tile-loaded', tileLoadedHandler);
|
|
this.viewer.addHandler('tile-drawing', tileDrawingHandler);
|
|
|
|
// filterIncrement allows to determine whether a tile contains the
|
|
// latest filters results.
|
|
this.filterIncrement = 0;
|
|
|
|
setOptions(this, options);
|
|
|
|
|
|
function tileLoadedHandler(event) {
|
|
var processors = getFiltersProcessors(self, event.tiledImage);
|
|
if (processors.length === 0) {
|
|
return;
|
|
}
|
|
var tile = event.tile;
|
|
var image = event.data;
|
|
if (image !== null && image !== undefined) {
|
|
var canvas = window.document.createElement('canvas');
|
|
canvas.width = image.width;
|
|
canvas.height = image.height;
|
|
var context = canvas.getContext('2d');
|
|
context.drawImage(image, 0, 0);
|
|
tile._renderedContext = context;
|
|
var callback = event.getCompletionCallback();
|
|
applyFilters(context, processors, callback);
|
|
tile._filterIncrement = self.filterIncrement;
|
|
}
|
|
}
|
|
|
|
|
|
function applyFilters(context, filtersProcessors, callback) {
|
|
if (callback) {
|
|
var currentIncrement = self.filterIncrement;
|
|
var callbacks = [];
|
|
for (var i = 0; i < filtersProcessors.length - 1; i++) {
|
|
(function(i) {
|
|
callbacks[i] = function() {
|
|
// If the increment has changed, stop the computation
|
|
// chain immediately.
|
|
if (self.filterIncrement !== currentIncrement) {
|
|
return;
|
|
}
|
|
filtersProcessors[i + 1](context, callbacks[i + 1]);
|
|
};
|
|
})(i);
|
|
}
|
|
callbacks[filtersProcessors.length - 1] = function() {
|
|
// If the increment has changed, do not call the callback.
|
|
// (We don't want OSD to draw an outdated tile in the canvas).
|
|
if (self.filterIncrement !== currentIncrement) {
|
|
return;
|
|
}
|
|
callback();
|
|
};
|
|
filtersProcessors[0](context, callbacks[0]);
|
|
} else {
|
|
for (var i = 0; i < filtersProcessors.length; i++) {
|
|
filtersProcessors[i](context, function() {
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function tileDrawingHandler(event) {
|
|
var tile = event.tile;
|
|
var rendered = event.rendered;
|
|
if (rendered._filterIncrement === self.filterIncrement) {
|
|
return;
|
|
}
|
|
var processors = getFiltersProcessors(self, event.tiledImage);
|
|
if (processors.length === 0) {
|
|
if (rendered._originalImageData) {
|
|
// Restore initial data.
|
|
rendered.putImageData(rendered._originalImageData, 0, 0);
|
|
delete rendered._originalImageData;
|
|
}
|
|
rendered._filterIncrement = self.filterIncrement;
|
|
return;
|
|
}
|
|
|
|
if (rendered._originalImageData) {
|
|
// The tile has been previously filtered (by another filter),
|
|
// restore it first.
|
|
rendered.putImageData(rendered._originalImageData, 0, 0);
|
|
} else {
|
|
rendered._originalImageData = rendered.getImageData(
|
|
0, 0, rendered.canvas.width, rendered.canvas.height);
|
|
}
|
|
|
|
if (tile._renderedContext) {
|
|
if (tile._filterIncrement === self.filterIncrement) {
|
|
var imgData = tile._renderedContext.getImageData(0, 0,
|
|
tile._renderedContext.canvas.width,
|
|
tile._renderedContext.canvas.height);
|
|
rendered.putImageData(imgData, 0, 0);
|
|
delete tile._renderedContext;
|
|
delete tile._filterIncrement;
|
|
rendered._filterIncrement = self.filterIncrement;
|
|
return;
|
|
}
|
|
delete tile._renderedContext;
|
|
delete tile._filterIncrement;
|
|
}
|
|
applyFilters(rendered, processors);
|
|
rendered._filterIncrement = self.filterIncrement;
|
|
}
|
|
};
|
|
|
|
function setOptions(instance, options) {
|
|
options = options || {};
|
|
var filters = options.filters;
|
|
instance.filters = !filters ? [] :
|
|
$.isArray(filters) ? filters : [filters];
|
|
for (var i = 0; i < instance.filters.length; i++) {
|
|
var filter = instance.filters[i];
|
|
if (!filter.processors) {
|
|
throw new Error('Filter processors must be specified.');
|
|
}
|
|
filter.processors = $.isArray(filter.processors) ?
|
|
filter.processors : [filter.processors];
|
|
}
|
|
instance.filterIncrement++;
|
|
|
|
if (options.loadMode === 'sync') {
|
|
instance.viewer.forceRedraw();
|
|
} else {
|
|
var itemsToReset = [];
|
|
for (var i = 0; i < instance.filters.length; i++) {
|
|
var filter = instance.filters[i];
|
|
if (!filter.items) {
|
|
itemsToReset = getAllItems(instance.viewer.world);
|
|
break;
|
|
}
|
|
if ($.isArray(filter.items)) {
|
|
for (var j = 0; j < filter.items.length; j++) {
|
|
addItemToReset(filter.items[j], itemsToReset);
|
|
}
|
|
} else {
|
|
addItemToReset(filter.items, itemsToReset);
|
|
}
|
|
}
|
|
for (var i = 0; i < itemsToReset.length; i++) {
|
|
itemsToReset[i].reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
function addItemToReset(item, itemsToReset) {
|
|
if (itemsToReset.indexOf(item) >= 0) {
|
|
throw new Error('An item can not have filters ' +
|
|
'assigned multiple times.');
|
|
}
|
|
itemsToReset.push(item);
|
|
}
|
|
|
|
function getAllItems(world) {
|
|
var result = [];
|
|
for (var i = 0; i < world.getItemCount(); i++) {
|
|
result.push(world.getItemAt(i));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function getFiltersProcessors(instance, item) {
|
|
if (instance.filters.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
var globalProcessors = null;
|
|
for (var i = 0; i < instance.filters.length; i++) {
|
|
var filter = instance.filters[i];
|
|
if (!filter.items) {
|
|
globalProcessors = filter.processors;
|
|
} else if (filter.items === item ||
|
|
$.isArray(filter.items) && filter.items.indexOf(item) >= 0) {
|
|
return filter.processors;
|
|
}
|
|
}
|
|
return globalProcessors ? globalProcessors : [];
|
|
}
|
|
|
|
$.Filters = {
|
|
THRESHOLDING: function(threshold) {
|
|
if (threshold < 0 || threshold > 255) {
|
|
throw new Error('Threshold must be between 0 and 255.');
|
|
}
|
|
return function(context, callback) {
|
|
var imgData = context.getImageData(
|
|
0, 0, context.canvas.width, context.canvas.height);
|
|
var pixels = imgData.data;
|
|
for (var i = 0; i < pixels.length; i += 4) {
|
|
var r = pixels[i];
|
|
var g = pixels[i + 1];
|
|
var b = pixels[i + 2];
|
|
var v = (r + g + b) / 3;
|
|
pixels[i] = pixels[i + 1] = pixels[i + 2] =
|
|
v < threshold ? 0 : 255;
|
|
}
|
|
context.putImageData(imgData, 0, 0);
|
|
callback();
|
|
};
|
|
},
|
|
BRIGHTNESS: function(adjustment) {
|
|
if (adjustment < -255 || adjustment > 255) {
|
|
throw new Error(
|
|
'Brightness adjustment must be between -255 and 255.');
|
|
}
|
|
var precomputedBrightness = [];
|
|
for (var i = 0; i < 256; i++) {
|
|
precomputedBrightness[i] = i + adjustment;
|
|
}
|
|
return function(context, callback) {
|
|
var imgData = context.getImageData(
|
|
0, 0, context.canvas.width, context.canvas.height);
|
|
var pixels = imgData.data;
|
|
for (var i = 0; i < pixels.length; i += 4) {
|
|
pixels[i] = precomputedBrightness[pixels[i]];
|
|
pixels[i + 1] = precomputedBrightness[pixels[i + 1]];
|
|
pixels[i + 2] = precomputedBrightness[pixels[i + 2]];
|
|
}
|
|
context.putImageData(imgData, 0, 0);
|
|
callback();
|
|
};
|
|
},
|
|
CONTRAST: function(adjustment) {
|
|
if (adjustment < 0) {
|
|
throw new Error('Contrast adjustment must be positive.');
|
|
}
|
|
var precomputedContrast = [];
|
|
for (var i = 0; i < 256; i++) {
|
|
precomputedContrast[i] = i * adjustment;
|
|
}
|
|
return function(context, callback) {
|
|
var imgData = context.getImageData(
|
|
0, 0, context.canvas.width, context.canvas.height);
|
|
var pixels = imgData.data;
|
|
for (var i = 0; i < pixels.length; i += 4) {
|
|
pixels[i] = precomputedContrast[pixels[i]];
|
|
pixels[i + 1] = precomputedContrast[pixels[i + 1]];
|
|
pixels[i + 2] = precomputedContrast[pixels[i + 2]];
|
|
}
|
|
context.putImageData(imgData, 0, 0);
|
|
callback();
|
|
};
|
|
},
|
|
GAMMA: function(adjustment) {
|
|
if (adjustment < 0) {
|
|
throw new Error('Gamma adjustment must be positive.');
|
|
}
|
|
var precomputedGamma = [];
|
|
for (var i = 0; i < 256; i++) {
|
|
precomputedGamma[i] = Math.pow(i / 255, adjustment) * 255;
|
|
}
|
|
return function(context, callback) {
|
|
var imgData = context.getImageData(
|
|
0, 0, context.canvas.width, context.canvas.height);
|
|
var pixels = imgData.data;
|
|
for (var i = 0; i < pixels.length; i += 4) {
|
|
pixels[i] = precomputedGamma[pixels[i]];
|
|
pixels[i + 1] = precomputedGamma[pixels[i + 1]];
|
|
pixels[i + 2] = precomputedGamma[pixels[i + 2]];
|
|
}
|
|
context.putImageData(imgData, 0, 0);
|
|
callback();
|
|
};
|
|
},
|
|
GREYSCALE: function() {
|
|
return function(context, callback) {
|
|
var imgData = context.getImageData(
|
|
0, 0, context.canvas.width, context.canvas.height);
|
|
var pixels = imgData.data;
|
|
for (var i = 0; i < pixels.length; i += 4) {
|
|
var val = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
|
|
pixels[i] = val;
|
|
pixels[i + 1] = val;
|
|
pixels[i + 2] = val;
|
|
}
|
|
context.putImageData(imgData, 0, 0);
|
|
callback();
|
|
};
|
|
},
|
|
INVERT: function() {
|
|
var precomputedInvert = [];
|
|
for (var i = 0; i < 256; i++) {
|
|
precomputedInvert[i] = 255 - i;
|
|
}
|
|
return function(context, callback) {
|
|
var imgData = context.getImageData(
|
|
0, 0, context.canvas.width, context.canvas.height);
|
|
var pixels = imgData.data;
|
|
for (var i = 0; i < pixels.length; i += 4) {
|
|
pixels[i] = precomputedInvert[pixels[i]];
|
|
pixels[i + 1] = precomputedInvert[pixels[i + 1]];
|
|
pixels[i + 2] = precomputedInvert[pixels[i + 2]];
|
|
}
|
|
context.putImageData(imgData, 0, 0);
|
|
callback();
|
|
};
|
|
},
|
|
MORPHOLOGICAL_OPERATION: function(kernelSize, comparator) {
|
|
if (kernelSize % 2 === 0) {
|
|
throw new Error('The kernel size must be an odd number.');
|
|
}
|
|
var kernelHalfSize = Math.floor(kernelSize / 2);
|
|
|
|
if (!comparator) {
|
|
throw new Error('A comparator must be defined.');
|
|
}
|
|
|
|
return function(context, callback) {
|
|
var width = context.canvas.width;
|
|
var height = context.canvas.height;
|
|
var imgData = context.getImageData(0, 0, width, height);
|
|
var originalPixels = context.getImageData(0, 0, width, height)
|
|
.data;
|
|
var offset;
|
|
|
|
for (var y = 0; y < height; y++) {
|
|
for (var x = 0; x < width; x++) {
|
|
offset = (y * width + x) * 4;
|
|
var r = originalPixels[offset];
|
|
var g = originalPixels[offset + 1];
|
|
var b = originalPixels[offset + 2];
|
|
for (var j = 0; j < kernelSize; j++) {
|
|
for (var i = 0; i < kernelSize; i++) {
|
|
var pixelX = x + i - kernelHalfSize;
|
|
var pixelY = y + j - kernelHalfSize;
|
|
if (pixelX >= 0 && pixelX < width &&
|
|
pixelY >= 0 && pixelY < height) {
|
|
offset = (pixelY * width + pixelX) * 4;
|
|
r = comparator(originalPixels[offset], r);
|
|
g = comparator(
|
|
originalPixels[offset + 1], g);
|
|
b = comparator(
|
|
originalPixels[offset + 2], b);
|
|
}
|
|
}
|
|
}
|
|
imgData.data[offset] = r;
|
|
imgData.data[offset + 1] = g;
|
|
imgData.data[offset + 2] = b;
|
|
}
|
|
}
|
|
context.putImageData(imgData, 0, 0);
|
|
callback();
|
|
};
|
|
},
|
|
CONVOLUTION: function(kernel) {
|
|
if (!$.isArray(kernel)) {
|
|
throw new Error('The kernel must be an array.');
|
|
}
|
|
var kernelSize = Math.sqrt(kernel.length);
|
|
if ((kernelSize + 1) % 2 !== 0) {
|
|
throw new Error('The kernel must be a square matrix with odd' +
|
|
'width and height.');
|
|
}
|
|
var kernelHalfSize = (kernelSize - 1) / 2;
|
|
|
|
return function(context, callback) {
|
|
var width = context.canvas.width;
|
|
var height = context.canvas.height;
|
|
var imgData = context.getImageData(0, 0, width, height);
|
|
var originalPixels = context.getImageData(0, 0, width, height)
|
|
.data;
|
|
var offset;
|
|
|
|
for (var y = 0; y < height; y++) {
|
|
for (var x = 0; x < width; x++) {
|
|
var r = 0;
|
|
var g = 0;
|
|
var b = 0;
|
|
for (var j = 0; j < kernelSize; j++) {
|
|
for (var i = 0; i < kernelSize; i++) {
|
|
var pixelX = x + i - kernelHalfSize;
|
|
var pixelY = y + j - kernelHalfSize;
|
|
if (pixelX >= 0 && pixelX < width &&
|
|
pixelY >= 0 && pixelY < height) {
|
|
offset = (pixelY * width + pixelX) * 4;
|
|
var weight = kernel[j * kernelSize + i];
|
|
r += originalPixels[offset] * weight;
|
|
g += originalPixels[offset + 1] * weight;
|
|
b += originalPixels[offset + 2] * weight;
|
|
}
|
|
}
|
|
}
|
|
offset = (y * width + x) * 4;
|
|
imgData.data[offset] = r;
|
|
imgData.data[offset + 1] = g;
|
|
imgData.data[offset + 2] = b;
|
|
}
|
|
}
|
|
context.putImageData(imgData, 0, 0);
|
|
callback();
|
|
};
|
|
},
|
|
COLORMAP: function(cmap, ctr) {
|
|
var resampledCmap = cmap.slice(0);
|
|
var diff = 255 - ctr;
|
|
for(var i = 0; i < 256; i++) {
|
|
var position = 0;
|
|
if(i > ctr) {
|
|
position = Math.min((i - ctr) / diff * 128 + 128,255) | 0;
|
|
}else{
|
|
position = Math.max(0, i / (ctr / 128)) | 0;
|
|
}
|
|
resampledCmap[i] = cmap[position];
|
|
}
|
|
return function(context, callback) {
|
|
var imgData = context.getImageData(
|
|
0, 0, context.canvas.width, context.canvas.height);
|
|
var pxl = imgData.data;
|
|
for (var i = 0; i < pxl.length; i += 4) {
|
|
var v = (pxl[i] + pxl[i + 1] + pxl[i + 2]) / 3 | 0;
|
|
var c = resampledCmap[v];
|
|
pxl[i] = c[0];
|
|
pxl[i + 1] = c[1];
|
|
pxl[i + 2] = c[2];
|
|
}
|
|
context.putImageData(imgData, 0, 0);
|
|
callback();
|
|
};
|
|
}
|
|
};
|
|
|
|
}());
|