Delete script/openseadragon/openseadragon-filtering.js
This commit is contained in:
parent
5f98e66651
commit
4f9f9411cc
|
@ -1,493 +0,0 @@
|
||||||
/*
|
|
||||||
* 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();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}());
|
|
Loading…
Reference in New Issue