2015年8月26日
node.jsによる細線化処理の比較
細線化は、画像処理で、二値化やフィルター処理に続いて一般的な処理で、線情報の特徴抽出するための前処理である。ここでは、3つのアルゴリズム(Zhang-Suen、NWG:Nagendraprasad-Wang-Gupta、田村)で細線化にどのような特徴(癖)を持っているか確認を行うこと、node.jsでそれぞれのパフォーマンスを確認することを目的とする。
検討のための元ネタは、次のサイトを利用した(アルゴリズムの解説はこちらのサイトへ)。(1)は、javaからnode.jsへの変換、(2)は、ほんの少々手を入れ、それそれnode.jsの関数とした。これらの関数にBMP画像の読み書き処理を加え、評価するプログラムとした。
(1) Zhang-Suen thinning algorithm
http://rosettacode.org/wiki/Zhang-Suen_thinning_algorithm
(2) Javascriptで細線化
http://www.hundredsoft.jp/win7blog/log/eid119.html
細線化結果
プログラムで細線化を行い、次のような画像を得た。
元画像(1bit) Zhang-Suen法
NWG法 田村法
各アルゴリズムを比較すると、NWG法が一番きれいな細線化であった。Zhang-Suen法は一部に細線化できるところ(図中の赤丸)が見られた。田村法は細線化度が高いが、ヒゲが多く見られた(細線化として正しいように思う)。
また、Zhang-Suen法で、上記(1)のロジック、上記(2)のロジックで、全く同じ画像が得られた(念のために画像ファイルのMD5値も一致した)。
処理スピード
元画像(640×480)に対して、i5-3337U CPU, 1.8GHzのPC環境で、Zhang-Suen法ロジック(1)は116.7msec、Zhang-Suen法ロジック(2)は51.9msec、NWG法は51..2msec、田村法は41.6msecであった。
結果
細線化はアルゴリズムにより、処理に結果が異なることが分かった。対象画像からどのようなものを抽出するかによって、アルゴリズムを選択する必要がある。
node.jsで画像処理なんかできるの?、遅いのでは?、このように思われる人が多いと思う。実際動かしてみてnode.jsでも十分な処理スピードであった。
細線化関数
/* Zhang-Suen thinning algorithm http://rosettacode.org/wiki/Zhang-Suen_thinning_algorithm Javascriptで細線化 http://www.hundredsoft.jp/win7blog/log/eid119.html Arranged by T. Osaka(2015/8/26) */ var thin=function() { var grid; var width; var height; /* Zhang-Suen thinning algorithm http://rosettacode.org/wiki/Zhang-Suen_thinning_algorithm */ this.ZhangSuen = function(g, w, h, x1, y1, x2, y2) { var nbrs = [[0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1]]; var nbrGroups = [[[0, 2, 4], [2, 4, 6]], [[0, 2, 6], [0, 4, 6]]]; width = w; height = h; grid = new Buffer(width * height); for (var i = 0; i < width * height; i++) grid[i] = g[i]; var firstStep = false; var hasChanged; do { hasChanged = false; firstStep = !firstStep; var toWhite = []; for (var r = y1; r < y2; r++) { for (var c = x1; c < x2; c++) { if (grid[r * width + c] != 0x00) continue; var nn = numNeighbors(r, c); if (nn < 2 || nn > 6) continue; if (numTransitions(r, c) != 1) continue; if (!atLeastOneIsWhite(r, c, firstStep ? 0 : 1)) continue; toWhite.push({x:c, y:r}); hasChanged = true; } } for (var p in toWhite) grid[toWhite[p].y * width + toWhite[p].x] = 0xff; } while (hasChanged || firstStep); function numNeighbors(r, c) { var count = 0; for (var i = 0; i < nbrs.length - 1; i++) { if (grid[(r + nbrs[i][1]) * width + c + nbrs[i][0]] == 0x00) count++; } return count; } function numTransitions(r, c) { var count = 0; for (var i = 0; i < nbrs.length - 1; i++) { if (grid[(r + nbrs[i][1]) * width + c + nbrs[i][0]] == 0xff) { if (grid[(r + nbrs[i + 1][1]) * width + c + nbrs[i + 1][0]] == 0x00) count++; } } return count; } function atLeastOneIsWhite(r, c, step) { var count = 0; var group = nbrGroups[step]; for (var i = 0; i < 2; i++) { for (var j = 0; j < group[i].length; j++) { var nbr = nbrs[group[i][j]]; if (grid[(r + nbr[1]) * width + c + nbr[0]] == 0xff) { count++; break; } } } return (count > 1); } }; // // Javascriptで細線化 Zhang-Suen Algorithm // http://www.hundredsoft.jp/win7blog/log/eid119.html // this.zhang_suen = function(g, w, h, x1,y1, x2, y2) { width = w; height = h; grid = new Buffer(width * height); for (var i = 0; i < g.length; i++) { grid[i] = ~g[i]; g[i] = ~g[i]; } var bFlag = true; for (var k = 0; k < 100 && bFlag; k++) { if (!(k & 1)) { bFlag = false; } for (var i = 0; i < g.length; i++) { g[i] = grid[i]; } for (var y = y1; y < y2; y++) { for (var x = x1; x < x2; x++) { var i = y * width + x; if (g[i]) { // [p9 p2 p3] // [p8 p1 p4] // [p7 p6 p5] var p1 = 1; var p2 = g[i - width ] ? 1 : 0; var p3 = g[i - width + 1] ? 1 : 0; var p4 = g[i + 1] ? 1 : 0; var p5 = g[i + width + 1] ? 1 : 0; var p6 = g[i + width ] ? 1 : 0; var p7 = g[i + width - 1] ? 1 : 0; var p8 = g[i - 1] ? 1 : 0; var p9 = g[i - width - 1] ? 1 : 0; var a = 0; if (!p2 && p3) {a++;} if (!p3 && p4) {a++;} if (!p4 && p5) {a++;} if (!p5 && p6) {a++;} if (!p6 && p7) {a++;} if (!p7 && p8) {a++;} if (!p8 && p9) {a++;} if (!p9 && p2) {a++;} var b = p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9; if (a == 1 && 2 <= b && b <= 6) { if ((!(k & 1) && p2*p4*p6 == 0 && p4*p6*p8 == 0) || ( (k & 1) && p2*p4*p8 == 0 && p2*p6*p8 == 0)) { grid[i] = 0; bFlag = true; } } } } } } for (var i = 0; i < grid.length; i++) { g[i] = ~g[i]; grid[i] = ~grid[i]; } }; // // Javascriptで細線化 NWG Algorithm // http://www.hundredsoft.jp/win7blog/log/eid119.html // this.nwg_method = function(g, w, h, x1,y1, x2, y2) { width = w; height = h; grid = new Buffer(width * height); for (var i = 0; i < g.length; i++) { grid[i] = ~g[i]; g[i] = ~g[i]; } var bFlag = true; for (var k = 0; k < 100 && bFlag; k++) { bFlag = false; for (var i = 0; i < g.length; i++) { g[i] = grid[i]; } for (var y = y1; y < y2; y++) { for (var x = x1; x < x2; x++) { var i = y * w + x; if (g[i]) { // [p7 p0 p1] // [p6 p2] // [p5 p4 p3] var p0 = (g[i - w ]) ? 1 : 0; var p1 = (g[i - w + 1]) ? 1 : 0; var p2 = (g[i +1]) ? 1 : 0; var p3 = (g[i + w + 1]) ? 1 : 0; var p4 = (g[i + w ]) ? 1 : 0; var p5 = (g[i + w - 1]) ? 1 : 0; var p6 = (g[i -1]) ? 1 : 0; var p7 = (g[i - w - 1]) ? 1 : 0; var a = 0; if (!p0 && p1) {a++;} if (!p1 && p2) {a++;} if (!p2 && p3) {a++;} if (!p3 && p4) {a++;} if (!p4 && p5) {a++;} if (!p5 && p6) {a++;} if (!p6 && p7) {a++;} if (!p7 && p0) {a++;} var b = p0 + p1 + p2 + p3 + p4 + p5 + p6 + p7; if (2 <= b && b <= 6) { var c = 0; if ((p0 + p1 + p2 + p5 == 0 && p4 + p6 == 2) || (p2 + p3 + p4 + p7 == 0 && p0 + p6 == 2)) { c = 1; } if (a == 1 || c == 1) { var e = (p2+p4) * p0 * p6; var f = (p0+p6) * p2 * p4; if ((!(k & 1) && e == 0) || ( (k & 1) && f == 0)) { grid[i] = 0; bFlag = true; } } } } } } } for (var i = 0; i < grid.length; i++) { g[i] = ~g[i]; grid[i] = ~grid[i]; } }; // // Javascriptで細線化 田村 Algorithm // http://www.hundredsoft.jp/win7blog/log/eid119.html // this.tamura = function(g, w, h, x1,y1, x2, y2) { width = w; height = h; grid = new Buffer(width * height); for (var i = 0; i < g.length; i++) { grid[i] = ~g[i]; g[i] = ~g[i]; } // [p0 p1 p2] // [p3 p4 p5] // [p6 p7 p8] // // P[0-8]を2bitで表す。白bit:10, 黒bit:01, その他bit:00 // 先頭にbit00を付加し、1バイト/列で1つのパターンを3バイトで表現。 // Bit並びは、00[p0][p1][p2]00[p3][p4][p5]00[p6][p7][p8]とする。 // 除去するパターン(1) var pat1 = new Array(0x040800, 0x000900); // 除去しないパターン(1) var pat1n = new Array(0x040a09, 0x182900, 0x001908, 0x042804, 0x081900, 0x040a04, 0x001824, 0x241800, 0x060900, 0x000906, 0x192a11, 0x190a19, 0x112a19, 0x192819); // 除去するパターン(2) var pat2 = new Array(0x000804, 0x001800); // 除去しないパターン(2) var pat2n = new Array(0x182804, 0x001a09, 0x001908, 0x042804, 0x081900, 0x040a04, 0x001824, 0x241800, 0x060900, 0x000906, 0x192a11, 0x190a19, 0x112a19, 0x192819); var bFlag = true; for (var k = 0; k < 100 && bFlag; k++) { bFlag = false; for (var i = 0; i < g.length; i++) { g[i] = grid[i]; } var pat, patn; if (k & 1) { pat = pat2; patn = pat2n; } else { pat = pat1; patn = pat1n; } for (var y = y1; y < y2; y++) { for (var x = x1; x < x2; x++) { var i = y * w + x; if (g[i]) { var f = 0x000800; if (g[i - w - 1]) {f |= 0x200000;} else {f |= 0x100000;} if (g[i - w ]) {f |= 0x080000;} else {f |= 0x040000;} if (g[i - w + 1]) {f |= 0x020000;} else {f |= 0x010000;} if (g[i -1]) {f |= 0x002000;} else {f |= 0x001000;} // if (g[i ]) {f |= 0x000800;} // else {f |= 0x000400;} if (g[i +1]) {f |= 0x000200;} else {f |= 0x000100;} if (g[i + w - 1]) {f |= 0x000020;} else {f |= 0x000010;} if (g[i + w ]) {f |= 0x000008;} else {f |= 0x000004;} if (g[i + w + 1]) {f |= 0x000002;} else {f |= 0x000001;} // 除去するパターンに一致 if ((f & pat[0]) == pat[0] || (f & pat[1]) == pat[1]) { // 除去しないパターンに一致 if ((f & patn[ 0]) == patn[ 0] || (f & patn[ 1]) == patn[ 1] || (f & patn[ 2]) == patn[ 2] || (f & patn[ 3]) == patn[ 3] || (f & patn[ 4]) == patn[ 4] || (f & patn[ 5]) == patn[ 5] || (f & patn[ 6]) == patn[ 6] || (f & patn[ 7]) == patn[ 7] || (f & patn[ 8]) == patn[ 8] || (f & patn[ 9]) == patn[ 9] || (f & patn[10]) == patn[10] || (f & patn[11]) == patn[11] || (f & patn[12]) == patn[12] || (f & patn[13]) == patn[13]) { ; } else { grid[i] = 0; bFlag = true; } } } } } } for (var i = 0; i < grid.length; i++) { g[i] = ~g[i]; grid[i] = ~grid[i]; } }; this.getData = function() { return grid; } } module.exports = thin;
(記 大坂 哲司)