「選択的ガウスぼかし」がえらい気に入ったので、アルゴリズムの学習も兼ねてJavaScriptでやってみたら思いの他使い物になりそうということで。
Demo:
File APIを実装しているブラウザーで動きます。IEの方ごめんなさい。IEだと10以降になります。小さめのファイルを読み込ませて下さい。1024*1024ピクセルを一応の上限に設定してあります。(追記2021.11.29:上限を16Mピクセルまで上げました。その他CSS周り修正)
- Info:
- Source:
- Radius:
- Threshold:(0-255)
- 32
-
実装
ざっとこんな感じです。img中の画像にradiusとthresholdで指定されたとおりに bilateral filter をかけ、結果をData URIとして帰します。画像操作には使い捨てのcanvasを使っています。canvasに関しては「HTML5 Canvas」が参考になりました。この場を借りて献本御礼。
要点と感想
- アルゴリズムの実装そのものは簡単でした。以下のページのおかげです。
- 同ページの分類で行くと、"Better Burute-Force"な実装ということになります。計算量は処理対象の画素数と、ぼかし半径の二乗(つまりカーネルの面積)に比例します。
- 実は一番悩ましかったのが、ガウスぼかしをGausissanたらしめる、σの設定をどうするかでした。ソース中に2.04045 というマジックナンバーが出てきますが、これは solve x for erf(x) = 255/256 の xです。ぼかし半径の外では計算結果が必ず最低輝度以下、つまりゼロになる、と。
- これに最大距離、つまり空間方向にはぼかし半径、色差方向にはRGBのユークリッド距離をかけたものがσの基本になります。
- それにさらに(閾値/255)をかけてものを、色差のσとして使っています。こうすることで、閾値外のものは確実にゼロなるというわけです。
- 前述のとおり、色差にはRGBのユークリッド距離を使っています。マンハッタン距離だと、可能な状態が768通りしかないので、空間方向の係数と同様ルックアップテーブルを使って高速化しやすいのですが(そういう実装もちらほら見受けられた)、採用しなかったのは、たとえば rgb(80,80,80) とrgb(240,0,0)の距離が同じになってしまうから。ユークリッド距離だと後者は√3倍つまり1.7倍以上離れています。
- あるいはYCbCrにしてからYだけ見るか。これも一応試してみたのですが、YCbCr変換のコストが以外と大きくて、手元の環境では素直に計算した方が高速でした。今日日のブラウザーの
Mathはあなどれません。
Enjoy!
Dan the JavaScripter
Demo Source
HTML
JavaScript
(function(global){
if (!global.FileReader) return; /* throw new Error('FileReader not supported'); */
var $ = function(id){ return document.getElementById(id) };
var stubIcon = '//dankogai.livedoor.blog/img/1x1.gif';
var clearImg = function(){
$('theImage').src = $('busy').src = stubIcon;
};
var readImg = function (file, img, onload) {
var reader = new FileReader();
reader.onload = function (ev) {
img.src = ev.target.result;
img.style.width = '100%';
img.style.height = 'auto';
setTimeout(function(){
$('dimension').innerHTML = img.naturalWidth + '*' + img.naturalHeight;
var theFiltered = $('theFiltered');
theFiltered.src = stubIcon;
theFiltered.width = '1';
theFiltered.height = '1';
$('elapsed').innerHTML = '';
}, 20)
};
reader.readAsDataURL(file);
};
var filterit = function() {
var img = $('theImage'),
radius = 1 * $('theRadius').value,
threshold = 1 * $('theThreshold').value;
/* safety valve */
if (img.width * img.height > 4096 * 4096) {
throw 'image too large (Max = 16M Pixels)';
}
$('filterIt').disabled = true;
$('busy').src = '//dankogai.livedoor.blog/img/ajax-loader.gif';
setTimeout(function(){
var started = Date.now();
var theFiltered = $('theFiltered');
theFiltered.src = bilateralFilter(img, radius, threshold);
theFiltered.style.width = '100%';
theFiltered.style.height = 'auto';
$('elapsed').innerHTML = (Date.now() - started) + 'ms';
$('filterIt').disabled = false;
$('busy').src = stubIcon;
}, 20);
};
var readit = function () {
var file = $('theFile').files[0];
var info = {
name: file.name,
lastModifiedDate: file.lastModifiedDate,
size: file.size,
type: file.type
};
$('fileInfo').textContent = JSON.stringify(info, null, ' ');
if (file.type.indexOf('image') === 0) {
readImg(file, $('theImage'));
} else {
clearImg();
}
};
$('theFile').addEventListener('change', readit, false);
$('readIt').addEventListener('click', readit, false);
$('filterIt').addEventListener('click', filterit, false);
clearImg();
})(this);

このブログにコメントするにはログインが必要です。
さんログアウト
この記事には許可ユーザしかコメントができません。