こんにちは!
デザイナーの伊東(@ALAKIWebVRAR1)です。
※TwitterでもWebARの事を中心に情報発信中!
よかったらフォローお願いしますm(_ _)m
Follow @ALAKIWebVRAR1
アプリをインストールする必要がなく、ブラウザで簡単に体験していただけるWebAR。
これまでARkit2や8th Wall WebによるマーカーレスWebAR、AR.jsを利用したマーカー型WebARをご紹介してきました。
今、話題の「5G」により、世間でも話題の注目の技術になってきた実感があります。
今回チャレンジしたいのは、通常の四角い黒の線に囲まれたマーカーではなく、自由に指定した特定の画像に対して3Dオブジェクトを表示させる、というものです。
ありもののプラットフォームを利用して簡単に開発するというよりは、ARの「認識」と「出力」という仕組みを分離して実現するため、中級〜上級者向けの内容になっているかと思います。
この記事では、
- 任意の画像の特徴点と、Webカメラに映った映像の特徴点の抽出
- 抽出した類似点同士を比較して、類似点を検出する
という、ARの「認識」の部分の仕組みを構築していきたいと思います。
そこで今回使用するのが、tracking.jsという画像認識に特化したプラグインです。
「認識」にtracking.jsを利用し、最終的には「出力」部分にAR.jsを利用して実現するというところを目標にやっていきたいと思います。
最後までよろしくお願いします!
下準備
まずはtracking.jsをダウンロードしましょう。
下記サイトにアクセスして、「Download tracking.js」をクリックしてください。
https://trackingjs.com/
ダウンロードしたzipファイルを展開すると、中に「examples」というフォルダがあるので開いてください。
これらのサンプルをベースにして、開発を進めていきたいと思います。
画像の特徴点の抽出
まずは任意の静止画の特徴点を抽出したいと思います。
「examples」フォルダの中の、「fast.html」というベースにカスタマイズしていきたいと思います。
以下が余分なスタイルなどを排除したソースコードになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
<!doctype html> <html> <head> <meta charset="utf-8"> <title>tracking.js - feature detection</title> <link rel="stylesheet" href="assets/demo.css"> <script src="../build/tracking-min.js"></script> <script src="../node_modules/dat.gui/build/dat.gui.min.js"></script> <style> .demo-container { background: #131112; } #image { position: absolute; left: -1000px; top: -1000px; } #canvas { position: absolute; left: 50%; top: 50%; margin: -200px 0 0 -200px; } </style> </head> <body> <div class="demo-title"> <p><a href="http://trackingjs.com" target="_parent">tracking.js</a> - detect feature points on a image</p> </div> <div class="demo-frame"> <div class="demo-container"> <img id="image" src="assets/fast.png" /> <canvas id="canvas" width="400" height="400"></canvas> </div> </div> <script> window.onload = function() { var width = 400; var height = 400; var canvas = document.getElementById('canvas'); var context = canvas.getContext('2d'); var image = document.getElementById('image'); window.fastThreshold = 10; var doFindFeatures = function() { tracking.Fast.THRESHOLD = window.fastThreshold; context.drawImage(image, 0, 0, width, height); var imageData = context.getImageData(0, 0, width, height); var gray = tracking.Image.grayscale(imageData.data, width, height); var corners = tracking.Fast.findCorners(gray, width, height); for (var i = 0; i < corners.length; i += 2) { context.fillStyle = '#f00'; context.fillRect(corners[i], corners[i + 1], 3, 3); } }; doFindFeatures(); var gui = new dat.GUI(); gui.add(window, 'fastThreshold', 0, 100).onChange(doFindFeatures); } </script> </body> </html> |
ポイントとなる点を解説していきます。
.demo-frame内に、使用する画像とその画像と同じ大きさのcanvasを用意します。
1 2 3 4 5 6 |
<div class="demo-frame"> <div class="demo-container"> <img id="image" src="assets/fast.png" /> <canvas id="canvas" width="400" height="400"></canvas> </div> </div> |
jsの記述を見てみると、画像をcanvasに描き込んでいるのがわかります。
1 2 3 4 5 6 7 8 |
var canvas = document.getElementById('canvas'); var context = canvas.getContext('2d'); var image = document.getElementById('image'); 〜 context.drawImage(image, 0, 0, width, height); /* キャンバスに画像を描き込む */ |
tracking.jsで画像や動画を扱う際は、必ずcanvasにデータを描き込んでおく必要があります。
canvasにすると、特徴点を抽出したり、比較しやすいデータの形になるようです。
下のコードでは、特徴点を抽出する際の「しきい値」のデフォルト値を設定しています。
1 |
window.fastThreshold = 10; |
しきい値を少なくすれば抽出される点は増え、しきい値を大きくすれば抽出される点は減ります。
画像によってこの値を調節して、適切に抽出されるようにチューニングしましょう。
肝心の特徴点を抽出する部分のコードを見てみましょう。
1 2 3 4 5 6 7 8 |
var imageData = context.getImageData(0, 0, width, height); var gray = tracking.Image.grayscale(imageData.data, width, height); var corners = tracking.Fast.findCorners(gray, width, height); for (var i = 0; i < corners.length; i += 2) { context.fillStyle = '#f00'; context.fillRect(corners[i], corners[i + 1], 3, 3); } |
変数imageDataにはcanvasに描き込んだ画像をデータとして代入しています。
次に、変数imageDataを一度白黒画像に変換して、変数grayに代入しています。
tracking.jsでは、特徴点を抽出する前に、より抽出の精度を高めるために最適な形に画像処理を行います。
特徴点の抽出には色情報は不要なので、白黒加工を施しているようです。
画像処理の次に特徴点を抽出して、変数cornersに代入します。
tracking.Fast.findCornersが抽出する関数です。
引数には画像データとその幅と高さを渡してあげます。
最後に、先ほど抽出した特徴点を、for関数で一つずつ描画してあげます。
以上が基本的な静止画像の特徴点の抽出の仕組みです。
この仕組みはWebカメラの映像の特徴点の抽出にも応用が効きます。
画像とWebカメラの映像の類似点の検出
画像と映像の特徴点が抽出できれば、その二つを照らし合わせて類似点を検出しましょう。
ソースの全体像は割愛して、htmlとjs部分を分けてみていきます。
1 2 3 4 5 6 7 |
<div class="demo-frame"> <div class="demo-container"> <img id="image1" src="assets/logo1.png" /> <video id="video" width="800" height="600" preload autoplay loop muted controls></video> <canvas id="canvas" width="1000" height="600"></canvas> </div> </div> |
先ほどの静止画のパターンと同様、.demo-frameに素材を入れておきます。
今回は画像と、Webカメラの映像を出力するためのvideoタグを設置します。
そして画像とvideoタグの映像は、両方ともcanvasに描き込みます。
そのため、canvasの幅はvideoタグとimgタグの幅の合計値にしておきましょう。
次にjs部分をみていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
(function() { // logoTracker ====================================================== var logoTracker = function() { logoTracker.base(this, 'constructor'); }; tracking.inherits(logoTracker, tracking.Tracker); logoTracker.prototype.templateDescriptors_ = null; logoTracker.prototype.templateKeypoints_ = null; logoTracker.prototype.fastThreshold = 60; logoTracker.prototype.blur = 3; logoTracker.prototype.setTemplate = function(pixels, width, height) { var blur = tracking.Image.blur(pixels, width, height, 3); var grayscale = tracking.Image.grayscale(pixels, width, height); this.templateKeypoints_ = tracking.Fast.findCorners(grayscale, width, height); this.templateDescriptors_ = tracking.Brief.getDescriptors(grayscale, width, this.templateKeypoints_); }; logoTracker.prototype.track = function(pixels, width, height) { var blur = tracking.Image.blur(pixels, width, height, this.blur); var grayscale = tracking.Image.grayscale(pixels, width, height); var keypoints = tracking.Fast.findCorners(grayscale, width, height, this.fastThreshold); var descriptors = tracking.Brief.getDescriptors(grayscale, width, keypoints); this.emit('track', { data: tracking.Brief.reciprocalMatch(this.templateKeypoints_, this.templateDescriptors_, keypoints, descriptors) }); }; // Track =================================================================== var boxLeft = 800; var video = document.getElementById('video'); var canvas = document.getElementById('canvas'); var image1 = document.getElementById('image1'); var canvasRect = canvas.getBoundingClientRect(); var context = canvas.getContext('2d'); var templateImageData; var capturing = false; var videoHeight = 600; var videoWidth = 800; var tracker = new logoTracker(); tracker.on('track', function(event) { stats.end(); if (capturing) { return; } // Sorts best matches by confidence. event.data.sort(function(a, b) { return b.confidence - a.confidence; }); // Re-draws template on canvas. context.putImageData(templateImageData, boxLeft, 0); // Plots lines connecting matches. for (var i = 0; i < Math.min(10, event.data.length); i++) { var template = event.data[i].keypoint1; var frame = event.data[i].keypoint2; if(event.data[i].confidence > 0.9) { context.beginPath(); context.strokeStyle = 'magenta'; context.moveTo(frame[0], frame[1]); context.lineTo(boxLeft + template[0], template[1]); context.stroke(); } } }); var trackerTask = tracking.track(video, tracker, { camera: true }); // Waits for the user to accept the camera. trackerTask.stop(); // Sync video ============================================================ function requestFrame() { window.requestAnimationFrame(function() { context.clearRect(0, 0, canvas.width, canvas.height); if (video.readyState === video.HAVE_ENOUGH_DATA) { try { context.drawImage(video, 0, 0, videoWidth, videoHeight); } catch (err) {} } requestFrame(); }); } requestFrame(); context.drawImage(image1, 800, 0, 200, 200); templateImageData = context.getImageData(800, 0, 200, 200); tracker.setTemplate(templateImageData.data, 200, 200); trackerTask.run(); // GUI Controllers var gui = new dat.GUI(); gui.add(tracker, 'fastThreshold', 20, 100).step(5); gui.add(tracker, 'blur', 1.1, 5.0).step(0.1); }()); |
まずは静止画像と映像の特徴点の抽出のための関数を作っておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
logoTracker.prototype.setTemplate = function(pixels, width, height) { var blur = tracking.Image.blur(pixels, width, height, 3); var grayscale = tracking.Image.grayscale(pixels, width, height); this.templateKeypoints_ = tracking.Fast.findCorners(grayscale, width, height); this.templateDescriptors_ = tracking.Brief.getDescriptors(grayscale, width, this.templateKeypoints_); }; logoTracker.prototype.track = function(pixels, width, height) { var blur = tracking.Image.blur(pixels, width, height, this.blur); var grayscale = tracking.Image.grayscale(pixels, width, height); var keypoints = tracking.Fast.findCorners(grayscale, width, height, this.fastThreshold); var descriptors = tracking.Brief.getDescriptors(grayscale, width, keypoints); this.emit('track', { data: tracking.Brief.reciprocalMatch(this.templateKeypoints_, this.templateDescriptors_, keypoints, descriptors) }); }; |
logoTracker.prototype.setTemplateに静止画像の特徴点を、logoTracker.prototype.trackに映像の特徴点を戻り値として代入されるようにしておきました。
抽出前の画像処理として、白黒加工とぼかし処理をしてあります。
ちなみになぜぼかし処理をかけているかというと、Webカメラの映像にはノイズが多いため、そのノイズが特徴点として検索されないようぼかしているようです。
また、logoTracker.prototype.trackには、”track”という類似点が検出された際のイベントを受け取れるようにしておきます。
trackイベントを受け取った際の処理はこちらです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
tracker.on('track', function(event) { stats.end(); if (capturing) { return; } // Sorts best matches by confidence. event.data.sort(function(a, b) { return b.confidence - a.confidence; }); // Re-draws template on canvas. context.putImageData(templateImageData, boxLeft, 0); // Plots lines connecting matches. for (var i = 0; i < Math.min(10, event.data.length); i++) { var template = event.data[i].keypoint1; var frame = event.data[i].keypoint2; if(event.data[i].confidence > 0.9) { context.beginPath(); context.strokeStyle = 'magenta'; context.moveTo(frame[0], frame[1]); context.lineTo(boxLeft + template[0], template[1]); context.stroke(); } } }); |
// Plots lines connecting matches.というコメント以下が、検出された類似点を元に、特徴点と特徴点の間に線を引くという処理をしています。
ここでポイントなのが、不要な類似点をいかに消すかというところです。
普通に類似点を検出すると、静止画像と全く違う形のものがWebカメラに映っても類似点として検出されることがあります。
不要な類似点を消すためには、受け取ったtrackイベントが持っている”confidence”という値を利用します。このconfidenceは、マッチした部分の信頼度を数値(0.5〜1)として持っています。
信頼度が0.9以上の場合のみ線を描画するという条件分岐を記述してあげれば、信頼度の低いマッチに関してはシャットアウトできるというわけです。
注意が必要なのは、Webカメラを起動している環境によって、同じ物どうしを照らし合わせても信頼度が変わってくるという点です。
明るい部屋では検出された類似点が、暗い部屋では検出されない、といったことがおきます。
明るさに応じてconfidenceの許容値を操作できるような配慮をしてあげると良いでしょう。
まとめ
いかがだったでしょうか。
今回は任意の画像を認識させて3Dオブジェクトを表示させるための、「認識」の部分にフォーカスを当てて解説しました。画像認識の際に必要な難しい数学の知識などを、tracking.jsは関数として簡単に利用できるようにしてくれています。
類似点が検出できれば、特異点の検出にも応用が効き、WebAR以外にも様々な用途が考えられます。
tracking.jsには他にも複数のサンプルが用意されているので、興味がある方はぜひ遊んでみてください。
「5G」が話題になっていることや、「大阪万博」も近くにつれて、さらにWebARは盛り上がりを見せてくるかと思います。
弊社でも実際にお問い合わせをいただく件数が増えてきたのを実感しています。
WebARのご相談はいつでもお待ちしておりますので、下記URLからお問い合わせください!
https://alaki.co.jp/contact/