Last active
January 27, 2016 06:54
-
-
Save biovisualize/abeeff354c04abe9219c to your computer and use it in GitHub Desktop.
Phosphenes visualization with image loader and using hermite downsampling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
onmessage = function (event) { | |
var img = event.data[0]; | |
var data = img.data; | |
var W = event.data[1]; | |
var H = event.data[2]; | |
var W2 = event.data[3]; | |
var H2 = event.data[4]; | |
var img2 = event.data[5] | |
var data2 = img2.data; | |
var ratio_w = W / W2; | |
var ratio_h = H / H2; | |
var ratio_w_half = Math.ceil(ratio_w/2); | |
var ratio_h_half = Math.ceil(ratio_h/2); | |
for(var j = 0; j < H2; j++) { | |
for(var i = 0; i < W2; i++) { | |
var x2 = (i + j*W2) * 4; | |
var weight = 0; | |
var weights = 0; | |
var gx_r = gx_g = gx_b = gx_a = 0; | |
var center_y = (j + 0.5) * ratio_h; | |
for(var yy = Math.floor(j * ratio_h); yy < (j + 1) * ratio_h; yy++) { | |
var dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half; | |
var center_x = (i + 0.5) * ratio_w; | |
var w0 = dy*dy //pre-calc part of w | |
for(var xx = Math.floor(i * ratio_w); xx < (i + 1) * ratio_w; xx++) { | |
var dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half; | |
var w = Math.sqrt(w0 + dx*dx); | |
if(w >= -1 && w <= 1){ | |
//hermite filter | |
weight = 2 * w*w*w - 3*w*w + 1; | |
if(weight > 0) { | |
dx = 4*(xx + yy*W); | |
gx_r += weight * data[dx]; | |
gx_g += weight * data[dx + 1]; | |
gx_b += weight * data[dx + 2]; | |
gx_a += weight * data[dx + 3]; | |
weights += weight; | |
} | |
} | |
} | |
} | |
data2[x2] = gx_r / weights; | |
data2[x2 + 1] = gx_g / weights; | |
data2[x2 + 2] = gx_b / weights; | |
data2[x2 + 3] = gx_a / weights; | |
} | |
} | |
postMessage({data: img2}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//name: Hermite resize | |
//about: Fast image resize/resample using Hermite filter with JavaScript. | |
//author: ViliusL | |
//forkedby: calvintwr | |
var Hermite = { | |
init: function(workerPath) { | |
var copy = Object.create(this); | |
if (!workerPath) throw new Error('Fail to provide worker\'s path when initializing.'); | |
copy._workerPath = workerPath; | |
copy.init = undefined; | |
return copy; | |
}, | |
resize: function(obj, callback) { | |
var self = this; | |
var mandatory = ['source', 'width', 'height']; | |
for(var i=0; i<mandatory.length; i++) { | |
var param = mandatory[i]; | |
if(typeof obj[param] === 'undefined') { | |
throw new Error('#resize() is expecting mandatory param `'+ param +'`.'); | |
} | |
} | |
if(typeof callback !== 'function') { | |
throw new Error('#resize() is expecting `callback` to be a function.'); | |
} | |
var sourceElement = this._extract(obj.source); | |
var mimeType = this._mimeConverter(obj.format); | |
var canvas = (sourceElement[1] === 'CANVAS') ? sourceElement[0] : this._imageToCanvas(sourceElement[0]); | |
var startTime = Date.now(); | |
var originalWidth = canvas.width; | |
var originalHeight = canvas.height; | |
var resizeToWidth = Math.round(obj.width); | |
var resizeToHeight = Math.round(obj.height); | |
var original = canvas.getContext('2d').getImageData(0, 0, originalWidth, originalHeight); | |
var resizedImage = canvas.getContext('2d').getImageData(0, 0, resizeToWidth, resizeToHeight); | |
canvas.getContext('2d').clearRect(0, 0, originalWidth, originalHeight); | |
var worker = new Worker(self._workerPath); | |
worker.onmessage = function(event){ | |
resizedImage = event.data.data; | |
console.log('Hermite resize completed in ' + (Math.round(Date.now() - startTime)/1000) + 's'); | |
canvas.getContext('2d').clearRect(0, 0, resizeToWidth, resizeToHeight); | |
canvas.height = resizeToHeight; | |
canvas.width = resizeToWidth; | |
canvas.getContext('2d').putImageData(resizedImage, 0, 0); | |
if (obj.output === 'canvas') { | |
return callback(canvas); | |
} else if (obj.output === 'image') { | |
_makeIntoImage(self._canvas, self._mimeType, self._outputImageQuality, callback); | |
} else if (typeof obj.output === 'undefined') { | |
// when not defined, assume whatever element type is the input, is the desired output | |
if(sourceElement[1] === 'IMG') return callback( self._canvasToImage(canvas, obj.mimeType, obj.quality) ); | |
return callback(canvas); | |
} else { | |
throw new Error('`output` is not valid.'); | |
} | |
}; | |
worker.postMessage([original, originalWidth, originalHeight, resizeToWidth, resizeToHeight, resizedImage]); | |
}, | |
_extract: function(source) { | |
var results; | |
if (source.tagName) { | |
// getElementById sources will pass this | |
results = [source, source.tagName]; | |
} else if (source[0].tagName) { | |
results = [source[0], source[0].tagName]; | |
} else { | |
throw new Error('#resize() found `source` element to be invalid.'); | |
} | |
if (['CANVAS','IMG'].indexOf(results[1]) === -1) throw new Error('#resize() expects element type of `img` or `canvas`.'); | |
return results; | |
}, | |
_imageToCanvas: function(image) { | |
// create a off-screen canvas | |
var c = document.createElement('canvas'); | |
var context = c.getContext('2d'); | |
c.height = image.height; | |
c.width = image.width; | |
context.drawImage(image, 0, 0, image.width, image.height); | |
return c; | |
}, | |
_canvasToImage: function(canvas, mimeType, quality, callback) { | |
var image = new Image(); | |
image.src = canvas.toDataURL(mimeType, quality); | |
return image; | |
}, | |
_mimeConverter: function(format) { | |
// if undefined, assume no compression. | |
if (typeof format === 'undefined') return 'image/png'; | |
var formats = ['raw', 'png', 'jpg', 'gif']; | |
var index = formats.indexOf(format); | |
if (index === -1) throw new Error('#inputImage() outputType can only be `raw`, `png`, `jpg` or `gif`'); | |
if (index === 0 || index === 1) return 'image/png'; | |
if (index === 2) return 'image/jpg'; | |
if (index === 3) return 'image/gif'; | |
throw new Error('#_mimeConverter fell through all cases!'); | |
} | |
}; | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<!--<script src="../lib/d3.js"></script>--> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="hermite.js"></script> | |
<style> | |
.panel { | |
fill: black; | |
} | |
.controls { | |
position: absolute; | |
} | |
canvas { | |
margin-top: 40px; | |
float: left; | |
} | |
#imageLoader, .slider, .count { | |
float: left; | |
} | |
.slider { | |
width: 400px; | |
} | |
#base-image, #phosphene-image { | |
width: 400px; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="controls"> | |
<input type="file" id="imageLoader" name="imageLoader"></input> | |
<input class="slider" type="range" min="8" max="32" value="8" step="1"> | |
<div class="count"></div> | |
</div> | |
<canvas id="base-image"></canvas> | |
<canvas id="phosphene-image"></canvas> | |
<div class="container"></div> | |
<script> | |
var outputW = 400; | |
var hermite = Hermite.init('hermite-worker.js'); | |
var imgLoader = document.getElementById('imageLoader'); | |
imgLoader.addEventListener('change', handleImage, false); | |
var baseCanvas = document.getElementById('base-image'); | |
var baseCtx = baseCanvas.getContext('2d'); | |
var phospheneCanvas = document.getElementById('phosphene-image'); | |
var phospheneCtx = phospheneCanvas.getContext('2d'); | |
var root = d3.select('.container').append('svg'); | |
var panel = root.append('rect').classed('panel', true); | |
root.append('defs').append('filter') | |
.attr({ | |
id: 'f1', | |
x: 0, | |
y: 0 | |
}) | |
.append('feGaussianBlur') | |
.attr({ | |
in: 'SourceGraphic', | |
stdDeviation: 2 | |
}); | |
var imgData, img; | |
d3.select('.slider').on('input', function(d){ | |
buildGrid(this.value); | |
}); | |
function handleImage(e){ | |
var reader = new FileReader(); | |
reader.onload = function(event){ | |
img = new Image; | |
img.onload = function(){ | |
buildBaseImg(); | |
buildGrid(8); | |
}; | |
img.src = event.target.result; | |
}; | |
reader.readAsDataURL(e.target.files[0]); | |
} | |
function buildBaseImg(){ | |
var imgW = img.width; | |
var imgH = img.height; | |
baseCanvas.width = imgW; | |
baseCanvas.height = imgH; | |
baseCtx.clearRect(0, 0, imgW, imgH); | |
baseCtx.drawImage(img, 0, 0, imgW, imgH); | |
imgData = baseCtx.getImageData(0, 0, imgW, imgH).data; | |
} | |
function buildGrid(outputResolution){ | |
d3.select('.count').text(outputResolution * outputResolution + ' phosphenes'); | |
var imgW = outputResolution; | |
var imgH = imgW * img.height / img.width; | |
phospheneCanvas.width = img.width; | |
phospheneCanvas.height = img.height ; | |
phospheneCtx.drawImage(img, 0, 0, img.width, img.height); | |
hermite.resize({ | |
source: phospheneCanvas, | |
width: imgW, | |
height: imgH | |
}, function(output) { | |
imgData = phospheneCtx.getImageData(0, 0, imgW, imgH).data; | |
var cellSize = outputW / imgW; | |
var data = []; | |
var dataIdx = -1; | |
var r, g, b, gray; | |
for(var i = 0; i < imgData.length / 4; i++){ | |
if(i % imgW === 0){ | |
dataIdx++; | |
data.push([]); | |
} | |
r = imgData[i*4]; | |
g = imgData[i*4+1]; | |
b = imgData[i*4+2]; | |
gray = r*0.2126 + g*0.7152 + b*0.0722; | |
data[dataIdx].push(gray); | |
} | |
var outputH = outputW * imgH / imgW; | |
root.attr({width: outputW, height: outputH}); | |
panel.attr({width: outputW, height: outputH}); | |
var rowGroup = root.selectAll('g.row') | |
.data(data); | |
rowGroup.enter().append('g') | |
.classed('row', true) | |
.attr({ | |
transform: function(d, i){ | |
var y = cellSize * i; | |
var x = 0; | |
if(i % 2 === 0){ | |
x += cellSize / 2; | |
} | |
return 'translate(' + [x, y] + ')'; | |
} | |
}); | |
rowGroup | |
.attr({ | |
transform: function(d, i){ | |
var y = cellSize * i; | |
var x = 0; | |
if(i % 2 === 0){ | |
x += cellSize / 4; | |
} | |
else{ | |
x -= cellSize / 4; | |
} | |
return 'translate(' + [x, y] + ')'; | |
} | |
}); | |
rowGroup.exit().remove(); | |
var cells = rowGroup.selectAll('circle.cell') | |
.data(function(d, i){ | |
return d; | |
}); | |
cells.enter().append('circle') | |
.classed('cell', true); | |
cells.attr({ | |
r: (cellSize / 2) * 0.9, | |
fill: function(d){ | |
return d3.rgb(d, d, d).toString(); | |
}, | |
filter: 'url(#f1)', | |
cy: cellSize / 2, | |
cx: function(d, i){ | |
return cellSize * i + cellSize / 2; | |
} | |
}); | |
cells.exit().remove(); | |
}); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment