文字の輪郭抽出。
2015/5/21
独自の方法で文字の輪郭を抽出してみる。まず輪郭の画素を抽出し、次に画素集合をカーブとして抽出する。
プログラム。
function program1() {
var F_BORDER = false; // ボーダーの表示。
var Z_SIZE = 30; // 拡大矩形のサイズ。(pixel)
var D_LEFT = 0, D_DOWN = 1, D_RIGHT= 2, D_UP = 3, D_COUNT = 4, D_NA = -1; // ボーダーの方向。
function turnLeft(dir) {return (dir + 1) % 4;} // 左回り。
/**
* テストプログラムのmain()。
*/
(function main() {
// サンプル画像を読み込む。
var img = new Image();
img.src = document.getElementById('photoUrl').value;
img.onload = function() {
// キャンバスの初期化。
var ctx0 = CanvasRenderingContext2D.initById('canvas0', 1);
var ctx1 = CanvasRenderingContext2D.initById('canvas1', 1);
// 画素データの取得。
var w = ctx0.canvas.width;
var h = ctx0.canvas.height;
ctx0.drawImage(img, 0, 0, w, h); // カラーで表示。
var text = '吾輩わがはいは猫である。名前はまだ無い。どこで生れたかとんと' +
'見当けんとうがつかぬ。何でも薄暗いじめじめした所でニャーニャ' +
'ー泣いていた事だけは記憶している。';
for (var size = 14, y = 0; 0 < text.length; y += size, size += 2) {
ctx0.text(0, y, text.substr(0, 12), size, 'black', 'white');
text = text.substr(12);
}
var mono = ctx0.getMonoImage(0, 0, w, h); // モノクロで取得。
// canvasにマウスイベントのハンドラを設定。
var clamp = false;
document.getElementById('canvas0').onmousedown = function(ev) {clamp = !clamp;}
document.getElementById('canvas0').onmousemove = function(ev) {
if (!clamp) {
// ポインター位置を拡大表示。
var pos = localPos(ev);
var x = Math.min(Math.max(pos.x - Z_SIZE, 0), mono.width - Z_SIZE);
var y = Math.min(Math.max(pos.y - Z_SIZE, 0), mono.height - Z_SIZE);
drawGuide(ctx0, mono, x, y);
drawZoom(ctx1, mono, x, y);
}
}
drawGuide(ctx0, mono, 0, 0);
drawZoom(ctx1, mono, 0, 0);
};
})();
/**
* サブルーチン。
*/
// 元画像と拡大枠を表示。
function drawGuide(ctx, mono, x, y) {
ctx.putMonoImage(mono, 0, 0);
ctx.rect(x, y, Z_SIZE, Z_SIZE, 'blue');
}
// 拡大表示。
function drawZoom(ctx, mono, x, y) {
// 拡大矩形の画素データを作成。
var crop = [];
for (var iy = 0; iy < Z_SIZE; ++iy) {
for (var ix = 0; ix < Z_SIZE; ++ix) {
crop[iy * Z_SIZE + ix] = mono.data[(y + iy) * mono.width + (x + ix)];
}
}
// 拡大矩形の画像を表示。
regularize(crop);
var A = calcAverage(crop);
var scale = mono.width / Z_SIZE;
for (var iy = 0; iy < Z_SIZE; ++iy) {
for (var ix = 0; ix < Z_SIZE; ++ix) {
var val = crop[iy * Z_SIZE + ix];
var color = 'rgb(%d,%d,%d)'.replace(/%d/g, val);
ctx.rect(ix * scale, iy * scale, scale, scale, color, color);
}
}
// ボーダーマップを作成。
var bmap = [];
for (var iy = 0; iy <= Z_SIZE; ++iy) {
bmap[iy] = [];
for (var ix = 0; ix <= Z_SIZE; ++ix) {
bmap[iy][ix] = getBorders(crop, A, ix, iy);
}
}
// ボーダーマップを描画。
var pos = new Pos(0, 0);
while (pos.scanNext()) {
for (var dir = 0; dir < D_COUNT; ++dir) {
if (bmap[pos.y][pos.x][dir]) {
if (F_BORDER)
drawBorder(pos, dir, 'yellow');
}
}
}
// ストロークを抽出。
var strokes = [];
// 1.はじめに辺縁から始まるストローク群を抽出。
// 横ストローク部分が左→右のラスタスキャンで細切れになるのを回避。
var pos = new Pos(0, 0);
while ((pos = warpEdge(pos)) != null) {
readStrokes(pos, strokes, 'red');
}
// 2.グリッドをラスタスキャンして残りのストローク群を抽出。
var pos = new Pos(0, 0);
while (pos.scanNext()) {
if (pos.hasBorder(bmap)) {
readStrokes(pos, strokes, 'blue');
}
}
// ストロークを描画。
for (var i = 0; i < strokes.length; ++i) {
var s = strokes[i];
// 各点の座標を求める。
var poses = [];
var pos = new Pos(s.pos);
for (var j = 0; j < s.dirs.length; ++j) {
poses.push(new Pos(pos));
pos.move(s.dirs[j]);
}
var sx = s.pos.x * scale;
var sy = s.pos.y * scale;
var ex = pos.x * scale;
var ey = pos.y * scale;
// 始点(sx,sy)と終点(ex,ey)を結ぶ直線 y=ax+b を求める。
var a = (ey-sy) / (ex-sx);
var b = sy - a * sx;
// 直線 y=ax+b とストロークの各点の距離dを求める。
// 公式:直線 y=ax+b と点(x0,y0)の距離dは d=abs(a*x0-y0+b)/sqr(a^2+1)
var ds = [];
for (var j = 0; j < s.dirs.length; ++j) {
var x = poses[j].x * scale;
var y = poses[j].y * scale;
var d = Math.abs(a * x - y + b) / Math.sqrt(a * a + 1);
ds.push({d:d, idx:j});
}
// 最も遠い点を制御点としたカーブを描く。
ds.sort(function(a, b) {
return (a.d < b.d) ? 1 : (a.d> b.d) ? -1 : 0;
});
var farIdx = ds[0].idx;
var cpx = poses[farIdx].x * scale;
var cpy = poses[farIdx].y * scale;
ctx.curve(sx, sy, cpx, cpy, ex, ey, 'green', 2);
}
/**
* 辺縁上をたどって次のボーダー開始位置を得る。
*
* @return pos=開始位置, null=ない
*/
function warpEdge(org) {
var pos = new Pos(org);
while (true) {
// 辺縁を左回りに移動。
if (pos.isLeftEdge() && !pos.isBottomEdge()) pos.move(D_DOWN);
else if (pos.isBottomEdge() && !pos.isRightEdge() ) pos.move(D_RIGHT);
else if (pos.isRightEdge() && !pos.isTopEdge() ) pos.move(D_UP);
else pos.move(D_LEFT);
// ボーダー開始しているか?
if (pos.hasBorder(bmap)) {
return pos;
}
//最初の位置に戻った → 辺縁上にボーダーはない。
if (org.x == pos.x && org.y == pos.y) {
return null;
}
}
}
/**
* ストローク群の抽出。(途切れるまで。辺縁では必ず途切れる。)
*/
function readStrokes(org, strokes, color) {
var pos = new Pos(org);
while (true) {
var s = readStroke(pos);
if (s.dirs.length == 0) { // ボーダーが途切れた。このストロークは終わり。
break;
}
strokes.push(s);
var bold = strokes.length % 2;
pos = clearStroke(s, color, bold);
}
}
/**
* ストローク抽出。
* ・ストロークには2つの方向(主、副)のボーダーを含むことができる。
* ・最初に2連続したら主方向とする。
* ・副方向に2連続したら停止。
* ・ボーダーが無くなったら停止。
*
* @param org 始点。
* @return {pos:pos, dirs:[dir0, dir1, ..]}
*/
function readStroke(org) {
var s = {pos:new Pos(org), dirs:[]};
var pos = new Pos(org);
var prevDir, dir = D_NA, dir1 = D_NA, dir2 = D_NA, mainDir = D_NA;
while (true) {
// ボーダーが続く方向を得る。
prevDir = dir;
dir = nextDir(pos, dir);
if (dir == D_NA) { // ボーダーが無くなったら停止。
return s;
}
// ストロークの連続性を判定。
if (dir1 == D_NA) {
dir1 = dir;
} else if (dir1 == dir) {
if (dir1 == prevDir) { // 2連続なら、
if (mainDir == D_NA) { // 主方向に設定。
mainDir = dir1;
} else if (mainDir == dir2) { // 副横行なら停止。
s.dirs.pop();
return s;
}
}
} else if (dir2 == D_NA) {
dir2 = dir;
} else if (dir2 == dir) {
if (dir2 == prevDir) {
if (mainDir == D_NA) {
mainDir = dir2;
} else if (mainDir == dir1) {
s.dirs.pop();
return s;
}
}
} else {
return s;
}
s.dirs.push(dir); // ボーダーをストロークに追加。
// 検査点を移動。
pos.move(dir);
if (pos.isEdge()) {
return s; // 辺縁に達した。
}
}
}
/**
* ストロークに含まれるボーダーをボーダーマップから消去。
*/
function clearStroke(s, color, bold) {
var pos = new Pos(s.pos);
for (var i = 0; i < s.dirs.length; ++i) {
var dir = s.dirs[i];
if (F_BORDER)
drawBorder(pos, dir, color, bold);
bmap[pos.y][pos.x][dir] = false;
pos.move(dir);
}
return pos;
}
/**
* ボーダーが続く方向を得る。
* 1.前回と同じ方向にボーダーがあれば優先。
* 2.なければ左回り順に探す。
*
* @param pos 現在の位置。
* @param dir 前回の方向。D_NAならD_LEFTから探す。
* @return 続く方向。
*/
function nextDir(pos, dir) {
if (dir == D_NA) {
dir = D_LEFT;
}
for (var i = 0; i < D_COUNT; ++i) {
if (bmap[pos.y][pos.x][dir]) {
return dir;
}
dir = ++dir % D_COUNT;
}
return D_NA;
}
/**
* ボーダーを描画。
*/
function drawBorder(pos, dir, color, bold) {
var ex = pos.x;
var ey = pos.y;
if (dir == D_LEFT ) ex -= 0.7;
else if (dir == D_DOWN ) ey += 0.7;
else if (dir == D_RIGHT) ex += 0.7;
else if (dir == D_UP ) ey -= 0.7;
var width = bold ? 4 : 2;
ctx.line(pos.x * scale, pos.y * scale, ex * scale, ey * scale, color, width);
}
}
/**
* 格子点から四方のボーダー取得。
* 例えば辺(1)の下辺と辺(2)の上辺はつながるべきでない。
* これを区別するため辺周囲を「左回り」方向にたどるボーダーのみ採用する。
* (1)■■■□□□
* □□□■■■(2)
*
* @param data Z_SIZE×Z_SIZEのモノクロ画素データ。(0~255)
* @param A 平均輝度。(0~255)
* @param x, y n×n矩形から(1,1)~(n-1,n-1)の格子点を指定。
* (0,0)┌┬┬┐
* ├┼┼┤
* ├┼┼┤
* └┴┴┘(n,n)
* @return [D_UP, D_RIGHT, D_DOWN, D_LEFT] 各方向のボーダー有無。
* 黒点周囲の左回りボーダーならtrue。
* □□□
* □■□ (1,1) -> DOWNがtrue
* □□□ (1,2) -> RIGHTがtrue
*/
function getBorders(data, A, x, y) {
// (x,y)を中心とする田型の4pixelを取得。
// ┌─┬─┐
// │v1│v2│
// ├─┼─┤
// │v3│v4│
// └─┴─┘
var v1 = (x == 0 || y == 0 ) ? -1 : data[(y - 1) * Z_SIZE + x - 1];
var v2 = (x == Z_SIZE || y == 0 ) ? -1 : data[(y - 1) * Z_SIZE + x ];
var v3 = (x == 0 || y == Z_SIZE) ? -1 : data[ y * Z_SIZE + x - 1];
var v4 = (x == Z_SIZE || y == Z_SIZE) ? -1 : data[ y * Z_SIZE + x ];
return [
v1 != -1 && v3 != -1 && (v3 < A && A <= v1), // D_LEFT
v3 != -1 && v4 != -1 && (v4 < A && A <= v3), // D_DOWN
v2 != -1 && v4 != -1 && (v2 < A && A <= v4), // D_RIGHT
v1 != -1 && v2 != -1 && (v1 < A && A <= v2) // D_UP
];
}
// 輝度の正規化。
function regularize(data) {
var N = data.length;
var min = data[0], max = data[0];
for (var i = 0; i < N; ++i) {
var val = data[i];
min = Math.min(min, val);
max = Math.max(max, val);
}
var scale = 255 / (max - min);
for (var i = 0; i < N; ++i) {
data[i] = Math.round((data[i] - min) * scale);
}
}
// 平均輝度の計算。
function calcAverage(data) {
var N = data.length;
var sum = 0;
for (var i = 0; i < N; ++i) {
sum += data[i];
}
return Math.round(sum / N);
}
// イベントの要素内座標を計算。
function localPos(ev) {
// window座標→要素内座標。
var cx = ev.clientX - ev.target.offsetLeft;
var cy = ev.clientY - ev.target.offsetTop;
// windowのスクロールを反映。
var body = document.body;
var docu = document.documentElement;
if (body.scrollTop > docu.scrollTop) { // chrome, iphone, android
cx += body.scrollLeft;
cy += body.scrollTop;
} else { // ie, firefox
cx += docu.scrollLeft;
cy += docu.scrollTop;
}
return {x:cx, y:cy};
}
/**
* ボーダーマップ上の位置クラス。
*/
// コンストラクタ。
function Pos() {
if (arguments.length == 2) { // new Pos(x, y)
this.x = arguments[0];
this.y = arguments[1];
} else { // new Pos(pos)
this.x = arguments[0].x;
this.y = arguments[0].y;
}
}
// 左辺、下辺、右辺、上辺か?
Pos.prototype.isLeftEdge = function() {return this.x == 0;}
Pos.prototype.isBottomEdge = function() {return this.y == Z_SIZE;}
Pos.prototype.isRightEdge = function() {return this.x == Z_SIZE;}
Pos.prototype.isTopEdge = function() {return this.y == 0;}
// どれかの辺か?
Pos.prototype.isEdge = function() {
return this.isLeftEdge() || this.isBottomEdge() || this.isRightEdge() || this.isTopEdge();
}
// ラスタスキャン。(0,0)→(Z_SIZE,Z_SIZE)
// @return false=スキャン終了。
Pos.prototype.scanNext = function() {
++this.x;
if (Z_SIZE < this.x) {
this.x = 0;
++this.y;
if (Z_SIZE < this.y) {
this.x = this.y = 0;
return false;
}
}
return true;
}
// 隣の位置(複製)を得る。
Pos.prototype.neighbor = function(dir) {
var p = new Pos(this.x, this.y);
p.move(dir);
return p;
}
// 移動。
Pos.prototype.move = function(dir) {
if (dir == D_LEFT ) --this.x;
else if (dir == D_DOWN ) ++this.y;
else if (dir == D_RIGHT) ++this.x;
else if (dir == D_UP ) --this.y;
}
// ボーダーの有無。
Pos.prototype.hasBorder = function(bmap) {
var dirs = bmap[this.y][this.x];
return dirs[D_LEFT] || dirs[D_DOWN] || dirs[D_RIGHT] || dirs[D_UP];
}
/**
* Canvasの拡張。
*/
// Canvasの初期化。
CanvasRenderingContext2D.initById = function(id, scale) {
var canvas = document.getElementById(id);
canvas.width = canvas.offsetWidth * scale; // 論理サイズを設定。
canvas.height = canvas.offsetHeight * scale;
return canvas.getContext('2d');
}
// モノクロデータ(0~255)の取得。
CanvasRenderingContext2D.prototype.getMonoImage = function(x, y, w, h) {
var imagedata = this.getImageData(x, y, w, h);
var data = imagedata.data;
var mono = [];
for (var i = 0; i < data.length; i += 4) {
mono[i / 4] = Math.round(data[i + 0] * 0.299 +
data[i + 1] * 0.587 +
data[i + 2] * 0.114);
}
return {width:w, height:h, data:mono};
}
// モノクロデータの描画。
CanvasRenderingContext2D.prototype.putMonoImage = function(mono, x, y) {
var imagedata = this.createImageData(mono.width, mono.height);
var data = imagedata.data;
for (var i = 0; i < data.length; i += 4) {
data[i + 0] =
data[i + 1] =
data[i + 2] = mono.data[i / 4];
data[i + 3] = 255;
}
this.putImageData(imagedata, x, y);
}
// 矩形の描画。
CanvasRenderingContext2D.prototype.rect = function(x, y, w, h, strokeColor, fillColor) {
this.save();
if (strokeColor) {
this.strokeStyle = strokeColor;
this.lineWidth = 1;
this.strokeRect(x, y, w, h);
}
if (fillColor) {
this.fillStyle = fillColor;
this.fillRect(x, y, w, h);
}
this.restore();
}
// 直線の描画。
CanvasRenderingContext2D.prototype.line = function(sx, sy, ex, ey, color, width) {
this.save();
if (color) {
this.strokeStyle = color;
}
if (width) {
this.lineWidth = width;
}
this.beginPath();
this.moveTo(sx, sy);
this.lineTo(ex, ey);
this.stroke();
this.restore();
}
// カーブの描画。
CanvasRenderingContext2D.prototype.curve = function(sx, sy, cpx, cpy, ex, ey, color, width) {
this.save();
if (color) {
this.strokeStyle = color;
}
if (width) {
this.lineWidth = width;
}
this.beginPath();
this.moveTo(sx, sy);
this.quadraticCurveTo(cpx, cpy, ex, ey);
this.stroke();
this.restore();
}
// テキストの描画。
CanvasRenderingContext2D.prototype.text = function(x, y, text, size, color, bgColor) {
this.save();
this.font = "%spx 'sans-serif'".replace('%s', size);
this.textBaseline = 'top';
var metrics = this.measureText(text);
if (color) {
this.fillStyle = bgColor;
this.fillRect(x, y, metrics.width, size);
}
if (bgColor) {
this.fillStyle = color;
this.fillText(text, x, y);
}
this.restore();
}
}
実行結果。
0.元画像
2.拡大
画像URL