====== インタラクションデザイン演習実習A ======
このページは首都大学東京にて開講している、インタラクションデザイン演習実習Aにおける授業補助教材です。
ユーザとモノをつなぐ対話設計であるインタラクションデザインに関する体系的知識及び実践的基礎技術を身に付けることを目標とする.近年の論文からいくつかのトピックを議論し,最先端の研究分野をフォローできるための知識を獲得し,それらを実際に再現するために必要な技術要素を同時に身に付けることを目標とする.
====== 論文読解1:The smart floor: a mechanism for natural user identification and tracking ======
[[http://www.cc.gatech.edu/fce/pubs/floor-short.pdf|Short Paper]]
>Robert J. Orr and Gregory D. Abowd. 2000. The smart floor a mechanism for natural user identification and tracking. In CHI '00 Extended Abstracts on Human Factors in Computing Systems (CHI EA '00). ACM, New York, NY, USA, 275-276. DOI=10.1145/633292.633453 http://doi.acm.org/10.1145/633292.633453
2ページの英文ショートペーパーです.ロードセルを利用したシンプルなユーザ識別装置に関する報告です.識別にはユークリッド距離を 利用しています.なお,ウェブで検索をするとフルペーパーも見つかるので,詳細はそっちで確認することをおすすめします.ユーザが歩いた際における圧力や時間,力の分散などを特徴量として学習済みの特徴量をf,入力データをf'(ダッシュ)とした場合,次のような計算で距離を求めると,様々な学習済みデータとどの程度入力データがことなるのかを計算できるようになります. 本講義でのもっとも基本的で重要な考え方なので,しっかり理解しましょう.
* Nearest Neighbor参考ページ:[[http://tetsuakibaba.jp/index.php?page=workshop/PatternRecognition4InteractionDesign/main.html|インタラクションデザインの為のパターン認識入門]]
==== 2つの値(身長体重)と入力データ(マウスポインタ座標)の距離を計算してラベルづけを行うサンプル ====
Processing
// (x1, y1) -> male
int x1 = 165;
int y1 = 68;
// (x2, y2) -> female
int x2 = 160;
int y2 = 47;
void setup()
{
size(200,200);
}
void draw()
{
background(255);
fill(0,0,255);
ellipse(x1,y1, 10,10);
fill(255,0,0);
ellipse(x2,y2, 10,10);
fill(0,255,0);
ellipse(mouseX,mouseY, 10,10);
text("("+str(mouseX)+","+str(mouseY)+")", mouseX, mouseY);
float l1 = sqrt( (x1-mouseX)*(x1-mouseX)+(y1-mouseY)*(y1-mouseY) );
float l2 = sqrt( (x2-mouseX)*(x2-mouseX)+(y2-mouseY)*(y2-mouseY) );
if( l1 < l2 ){
line(mouseX,mouseY, x1,y1);
}
if( l2 < l1 ){
line(mouseX,mouseY, x2,y2);
}
}
python
x1 = 165
y1 = 68
x2 = 160
y2 = 47
def setup():
size(200,200)
def draw():
background(255)
fill(0,0,255)
ellipse(x1,y1,10,10)
fill(255,0,0)
ellipse(x2,y2,10,10)
fill(0,255,0)
ellipse(mouseX,mouseY,10,10)
text("("+str(mouseX)+","+str(mouseY)+")", mouseX, mouseY)
l1 = sqrt( (x1-mouseX)*(x1-mouseX)+(y1-mouseY)*(y1-mouseY) )
l2 = sqrt( (x2-mouseX)*(x2-mouseX)+(y2-mouseY)*(y2-mouseY) )
if l1 < l2:
line(mouseX,mouseY,x1,y1)
if l2 < l1:
line(mouseX,mouseY,x2,y2)
==== データ数を増やしてみる ====
Processing
int data[][] = {
{'M', 164, 60},
{'M', 178, 80},
{'M', 168, 69},
{'M', 170, 58},
{'M', 165, 68},
{'F', 160, 47},
{'F', 155, 45},
{'F', 164, 60},
{'F', 170, 62},
{'F', 148, 40}
};
void setup() {
size(200, 200);
}
void draw()
{
background(255);
for ( int i = 0; i < 10; i++ ) {
if ( data[i][0] == 'M' ) {
fill(0, 0, 255);
} else if ( data[i][0] == 'F' ) {
fill(255, 0, 0);
}
ellipse(data[i][1], data[i][2], 10, 10);
}
float distance[] = new float[10];
for( int i = 0; i < 10; i++ ){
distance[i] = sqrt( (mouseX-data[i][1])*(mouseX-data[i][1])+(mouseY-data[i][2])*(mouseY-data[i][2]));
}
float min = 1000.0;
int id_min = 0;
for( int i = 0; i < 10; i++ ){
if( min > distance[i] ){
min = distance[i];
id_min = i;
}
}
line(mouseX, mouseY, data[id_min][1], data[id_min][2]);
fill(0, 255, 0);
ellipse(mouseX, mouseY, 10, 10);
text("("+str(mouseX)+","+str(mouseY)+")", mouseX, mouseY);
}
Processing Python
data = [
['M', 164, 60, 0],
['M', 178, 80, 0],
['M', 168, 69, 0],
['M', 170, 58, 0],
['M', 165, 68, 0],
['F', 160, 47, 0],
['F', 155, 45, 0],
['F', 164, 60, 0],
['F', 170, 62, 0],
['F', 148, 40, 0]
]
def setup():
size(200,200)
def draw():
background(255)
for each_data in data:
if each_data[0] == 'M':
fill(0,0,255)
elif each_data[0] == 'F':
fill(255,0,0)
ellipse(each_data[1], each_data[2], 10, 10)
fill(0,255,0)
ellipse(mouseX, mouseY, 10,10)
text("("+str(mouseX)+","+str(mouseY)+")", mouseX, mouseY)
for each_data in data:
each_data[3] = dist(mouseX, mouseY, each_data[1], each_data[2]) #sqrt( (mouseX-each_data[1])*(mouseX-each_data[1])+(mouseY-each_data[2])*(mouseY-each_data[2]))
sorted_data = sorted(data, key=lambda x : x[3])
line(mouseX, mouseY, sorted_data[0][1], sorted_data[0][2])
----
====== 論文読解2:SmartVoice: 言語の壁を超えたプレゼンテーションサポーティングシステム ======
[[https://www.google.co.jp/url? sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=0CB4QFjAA&url=http%3A%2F%2Fwww.wiss.org%2FWISS2013Proceedings%2Foral%2FS1_002.pdf&ei=DMkxVeWUN6HWmgXQzIDQDg&usg=AFQjCNH3Hbx5TgPAt7zMg8ROJV1ludOAFw&sig2=MCE-38B_ujHpGaYSs00dPA&bvm=bv.91071109,d.dGY|Paper]]
> 李 翔, 暦本 純一, SmartVoice:言語の壁を越えたプレゼンテーションサポーティングシステム, 日本ソフトウェア科学会インタラクティブシステムとソフトウェアに関する研究会(WISS2013), 2013.
WISS2013での和文論文です.OF及びofxFaceTrackerを利用しています.読み上げテキストと音声フレーズとのマッチングにDPアルゴリズムを用いています. DPはDynamic Programingのことで,動的なプログラム(編集)を指します.一般的に原稿にそって人間が読み上げを行い,その結果,音声データの位置と 原稿データの位置のマッチングをとるには音声信号処理を行うことが多いです.一方でこの論文ではそのような真っ向勝負はせず, 原稿データを一度ボイスシンセサイザーから再生し,音声データ2を作成します.この2つのデータはフレーズ区切り情報が含まれていますが, その順番が一致していない可能性が大いにあります.そこで音声再生時間をマッチングの為のコスト距離として動的に順番を合わせること を行っています.また読み上げの際は,ただ単純に読むだけでなく発表者の表情をインタフェースに声量や速度を調整できるようにしています.
ここではDPアルゴリズムを理解することと,フェイストラッカーを利用して簡単な表情認識を行うことをします. 表情認識には,smart floorで学習した多次元特徴量の距離計算をベースに実装してみます.
===== ofxFaceTrackerの導入方法 =====
こちらに動画リンクを貼っておきますので、参照してください。
* https://youtu.be/gFEBUh4uAfc
===== 練習 =====
練習問題
FaceTrackerのgetGesture関数より口の開閉等の各データを取得し,それらリアルタイムデータと記録したデータ間においてN次のユークリッド距離を用いることで,任意表情の区別をせよ. ただし利用するGestureはLEFT_EYEBROW_HEIGHT, RIGHT_EYEBROW_HEIGHT, LEFT_EYE_OPENNESS, RIGHT_EYE_OPENNESS, MOUTH_HEIGHT, MOUTH_WIDTH, JAW_OPENNESSの7次元とする.
練習問題
上記練習問題の条件より,3種類以上の表情を識別するプログラムを記述せよ
課題
表情認識を利用して,緑色のプレイヤーを赤色のゴールに向かわせるシンプルなゲームを楽しみ,より早くクリアできるようにインタフェースを最適化して下さい.キーボードを利用してはいけません.あくまで顔の表情認識及び特徴データを利用してプレイヤーを操作して下さい.
* 日程:
* 場所:2号館501室
* 課題準備の解説動画:https://youtu.be/hKCIC5lxaC8
* 優勝者には景品をプレゼント
----
====== 論文読解3:Scratch Input: Creating Large, Inexpensive, Unpowered and Mobile finger Input Surfaces ======
[[http://www.chrisharrison.net/index.php/Research/ScratchInput|著者の研究紹介ページ(PDFあり)]]
> Harrison, Chris and Hudson, Scott E. Scratch Input: Creating Large, Inexpensive, Unpowered and Mobile finger Input Surfaces. In Proceedings of the 21st Annual ACM Symposium on User interface Software and Technology. UIST '08. ACM, New York, NY. 205-208.
タイトルにあるようにスクラッチ(ひっかき)音を入力とした操作インタフェースの提案を行っています.4ページの限られた文章の中で十分に技術再現が記述されていない部分が見受けられます. 音響信号の特徴量について,Early prototypeの際にはどのような特徴量を利用したかが明確に記述されておらず,動的伸縮法(Dynamic Time Warping)とナイーブベイズ分類器によって認識処理を行っています. おそらく入力信号から直接学習済み信号をDTWを利用して単純ベイズによるマッチング計算を行ったのだと思います. 最終的にはピークカウント及び振幅変動をベースとした決定木(Decision Tree)によるシンプルなアルゴリズムに落ち着いています.ただしこの部分も十分な記載が無いため,読解に少し努力が要ります.
スクラッチ音は3KHz以上の周波数成分を強く持つため,日常生活における様々な騒音環境下でも有効に機能すると述べています.実際に様々な素材で実験を行いある程度の認識精度があることを実験から示しています.
ここでは,信号処理をインタフェースとして利用するための基礎的な技術・知識を身に付けることが目標です. ここでは決定木(if文)ベースで実装を行いますが,興味のある人はDTWでナイーブベイズ識別行うプログラムも挑戦 してみるとよいでしょう.ただし著者らは精度が良くなかったと述べています.ナイーブベイズ識別に関しては 本サイトのインタラクションデザインの為のパターン認識入門を参照してください. この他ウェブサイトや参考書でもナイーブベイズに関する解説は多く入手できると思います.
====== 基礎練習:音の入力を取得する ======
Openframeworksを利用して音の入力を取得するプログラムをまずは記述してみましょう.最小構成となるコードを下記に記載し,そこから始めます.
#include "ofApp.h"
void ofApp::setup(){
ofSetFrameRate(60);
ofSoundStreamSettings settings;
sound_stream.printDeviceList();
settings.setInListener(this);
settings.sampleRate = 44100;
settings.numOutputChannels=0;
settings.numInputChannels=1;
settings.bufferSize = 1024;
sound_stream.setup(settings);
}
void ofApp::update(){
}
void ofApp::draw(){
ofBackground(50);
ofNoFill();
ofBeginShape();
for( int i = 0; i < 1024; i++ ){
ofVertex(i, 200*buffer_input[i]+ofGetHeight()/2);
}
ofEndShape();
}
void ofApp::audioIn(ofSoundBuffer &buffer)
{
for( int i = 0; i < 1024; i++ ){
buffer_input[i] = buffer[i];
}
}
#pragma once
#include "ofMain.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
void audioIn(ofSoundBuffer &buffer);
ofSoundStream sound_stream;
float buffer_input[1024];
};
===== 基礎知識:Easy DFT, DFT, FFT =====
入力された信号がどのような周波数成分を持つかを解析する一般的な手法です.すべての信号はsinとcosの足し合わせで表現できる.というフランスの数学者フーリエの考えに基づいた理論です. ここではその考えを直感的に表現してみたEasy DFTを実際に利用し,その後DFTを理解します.さらに高速化手法であるFFTを用いて周波数解析する方法に慣れてみましょう. ちなみにEasy DFTというのは存在しません.馬場が勝手に授業用に用意した簡易なDFTとなります.
* [[https://www.dropbox.com/sh/2vykoizyxor2mb8/AADp8DT1sWpPx4lAfkMVUAoWa?dl=0|Easy DFT]]
* [[https://www.dropbox.com/sh/m3u6g8fkja8vzie/AADId9YTGlrwAAquqOD71ADsa?dl=0|DFT]]
* [[https://www.dropbox.com/sh/1et3demv81qwtix/AAB86bxBQ-VtT4YyoY1_IjqHa?dl=0|FFT]]
===== Addonを利用する =====
この授業用にaddonを作成しておきました.
* https://github.com/TetsuakiBaba/ofxbSoundUtils.git
==== addonの追加方法 ====
{{ :lecture:ofのaddon追加方法とexampleの実行.mp4 |}}
===== 基礎練習=====
プログラムを実装していくにあたり,幾つかの基礎練習をします.
==== ofxGui ====
様々なパラメータを利用して,ユーザの入力値を調整していく際,GUI(Graphical User Interface)を利用してその値を手軽に変更できると,作業が捗ります. そこで,OpenFrameworksに標準(v.0.8以降)のofxGuiを利用して,プログラム内の変数を動的に変更する方法を練習します. このあたりは /examples/gui/guiExample の例を実行して内容を確認してみます.
==== C++における vector型 ====
様々な処理に配列は多く利用されます.信号処理や画像処理にかぎらず,一般的な計算には配列は多用されます. この配列を何度も利用しているうちに,いろいろと不便なことに気づきます.例えば最初は配列サイズを100確保 したのに途中からそれ以上必要になってしまった時,配列の中にあるデータを削除し,削除した分を詰めて配列に 再度格納したいとき等です.vector型に関する詳しい説明は多くの参考書やウェブサイトに譲るとして,ここでは 基本的な使い方を抑えたいと思います.emptyExampleをベースに実習してみましょう.
vectora; // intの配列をvector型で宣言
a.push_back(1); // a[0]に1が格納される
a.push_back(2); // a[1]に2が格納される
a.push_back(3); // a[2]に3が格納される
ofDrawBitmapString(ofToString(a.size()), 10,10); // (10,10)座標に3が表示される
a.pop_back(); // a[2]が削除され,a[0]=1, a[1]=2が残る
a.push_back(4); // a[2]に4が格納される
a.erase(a.begin()); // a[0]が削除され,後ろのデータが全て前に移動する→a[0]=2, a[1]=4
for( int i = 0; i < a.size(); i++ ){
ofDrawBitmapString("a["+ofToString(i)+"]="+ofToString(a[i]), 10, 20+i*10);
}
これまでの基礎知識を踏まえていよいよScratchInputの技術再現を行います.ただし著者らは認識のための特徴量に 関して詳細な情報を論文に載せていないので,ここではすこしアレンジを加えて実装を行ってみます.そこで,これから このプログラムのことをScratchInputZと呼ぶことにします.
==== 正規化 ====
論文読解2の最後にユーザの表情を認識するプログラムを実装しましたが,ここで,厳密にその評価を行うと 一つ問題があることがわかります.眉毛の高さや口の開閉等の中にはその変化の度合いが全て均等とは言い切れません. 例えばx,yにおいて(5,5)と(10,6)はxの値が大きく異るように見えますが,実際の計測データ上ではx:5-10,y:5-6 の範囲で取得されているとします.この場合,そのままユークリッド距離等を計算してしまうと,xの変化によって 識別結果が大きく左右されてしまいます.そこで,一般的には正規化処理を行うことでこの問題に対応します.
先ほどの例では(10,5), (5,6)でしたが,x:5-10, y:5-6であることから,それぞれを正規化すると (1,0), (0,1)となります.これでそれぞれの次元において,値は異なるものの,変化の度合いを揃えることができます.
===== ScratchInput =====
それでは、実際にScratchInputを実装していきます。と入っても論文映像で紹介されている様々なインタラクションとアプリケーションを実装はしません。論文中の実験で示されている種々のジェスチャー(Single/Double Tap, Single/Double/Triple/Quad Swipe)を認識するところまでを実装してみます。論文中ではピークカウントと決定木にて実装していると記述されています。
まずはプログラムの準備として、FFTの結果から
* 100Hz - 1000Hz
* 3000Hz - 8000Hz
のそれぞれの周波数帯パワーの合計値と平均値を計算し、同時にグラフにて表示します。そこからスタートします。以下プログラムをコピペしていきます。ただしaddonに ofxGui, ofxbSoundUtilsを利用するので、ProjectGeneratorで選択するのを忘れてないように注意してください。
#include "ofApp.h"
//--------------------------------------------------------------
void ofApp::setup(){
sound_utils.setup(512);
}
//--------------------------------------------------------------
void ofApp::update(){
sound_utils.update();
volume.push_back(sound_utils.fft.avg_power);
while( volume.size() > ofGetWidth()){
volume.erase(volume.begin());
}
float sum_100_1000 = 0.0;
float sum_3000_8000 = 0.0;
int count_100_1000 = 0;
int count_3000_8000 = 0;
for( int i = 0; i < sound_utils.fft.spectrum.size(); i++){
if(sound_utils.fft.spectrum[i].Hz >= 100 &&
sound_utils.fft.spectrum[i].Hz <= 1000 ){
sum_100_1000 = sum_100_1000 + sound_utils.fft.spectrum[i].power;
count_100_1000++;
}
else if(sound_utils.fft.spectrum[i].Hz >= 3000 &&
sound_utils.fft.spectrum[i].Hz <= 8000 ){
sum_3000_8000 = sum_3000_8000 + sound_utils.fft.spectrum[i].power;
count_3000_8000++;
}
}
float ave_100_1000 = sum_100_1000/(float)count_100_1000;
float ave_3000_8000 = sum_3000_8000/(float)count_3000_8000;
volume_100_1000.push_back(ave_100_1000);
volume_3000_8000.push_back(ave_3000_8000);
while( volume_100_1000.size() > ofGetWidth() ){
volume_100_1000.erase(volume_100_1000.begin());
}
while( volume_3000_8000.size() > ofGetWidth() ){
volume_3000_8000.erase(volume_3000_8000.begin());
}
}
//--------------------------------------------------------------
void ofApp::draw(){
ofNoFill();
ofBeginShape();
for( int i = 0; i < volume.size(); i++){
ofVertex(volume.size()-i, -1*volume[i]+ofGetHeight()*(1/6.0));
}
ofEndShape();
// 100 -1000 Hz
ofBeginShape();
for( int i = 0; i < volume_100_1000.size(); i++){
ofVertex(volume_100_1000.size()-i, -1*volume_100_1000[i]+ofGetHeight()*(3/6.0));
}
ofEndShape();
// 3000 -8000 Hz
ofBeginShape();
for( int i = 0; i < volume_3000_8000.size(); i++){
ofVertex(volume_3000_8000.size()-i, -1*volume_3000_8000[i]+ofGetHeight()*(5/6.0));
}
ofEndShape();
if( volume.size() > 0 ){
if( volume[volume.size()-1] > 20 ){
ofSetColor(255,0,0);
ofDrawBitmapString("Speaking", 10,10);
}
else{
ofSetColor(255,255,255);
}
}
ofDrawBitmapString(sound_utils.string_device_info, 20, 20);
}
#pragma once
#include "ofMain.h"
#include "ofxbSoundUtils.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
ofxbSoundUtils sound_utils;
vectorvolume;
vectorvolume_3000_8000;
vectorvolume_100_1000;
};
==== ofxGraphでグラフを置き換える ====
さて、vectorを利用するとグラフが描画できることがわかりましたが、このままでは少し使い勝手が悪いので、更にこのプロジェクトファイルにaddonを追加します。ofxGraphという馬場が作成したもので、手軽にグラフが作れるほか、サイズを変えたり、csvファイルにしたり、値を直接観察することができます。projectGeneratorを利用してofxGraphをまずは追加してください。なお、ofxGraphでは ofxXmlSettings のaddonも必要になるので、そちらも追加してください。ofxXmlSettings(デフォルトで入っています)も忘れずに追加してください。
* ofxGraph on Github: https://github.com/TetsuakiBaba/ofxGraph
* ofxGraph/Example/bin/ofxGraph フォルダをプロジェクトファイルの bin/ 以下にコピーしてください。
それでは実際に、fft_avg_powerを時系列のグラフにして表示してみます。これまでdraw()内に記述していたグラフ描画に関してはコメントアウトします。実行してみてマイク部を指先で叩いたときの様子をグラフに表示したものが下記の画像です。マウスポインタを近づけると該当箇所の値がわかるので、これを参考にしていきます。
{{ :lecture:スクリーンショット_2019-05-29_0.02.14.png?600 |}}
==== ある程度の音量区間を検出 ====
fftの平均パワーグラフを画面上に折れ線グラフで表示していますが、この値が一定値を超える、つまりしきい値を超えた場合にのみ、認識処理をしてみようと思います。そこで、まずはグラフを観察し、適切なしきい値を設定するために、ofxGuiを利用して動的に設定可能なしきい値パラメータを導入します。その値を超えているときだけ、 OFXGRAPH_POINT_LABEL_MARKERを利用して赤丸をプロットしてみます。下記に実際にしきい値を超えたときだけその点を赤丸で示した図を表示します。
{{ :lecture:スクリーンショット_2019-05-29_0.21.58.png?600 |}}
==== 赤丸が連続した区間だけ、認識用にデータを保持する ====
これでしきい値を超えた瞬間を捉えることができたので、その区間を認識処理すべき区間として、100-1000[Hz], 3000-8000[Hz]の値を別配列に保存しておきます。下記のようなグラフにしてみました。
{{ :lecture:スクリーンショット_2019-05-29_0.33.19.png?600 |}}
==== タップとスワイプの判定 ====
実際に値を観察してみるとわかると思いますが、タップの場合は中低域のパワーがつよく、スワイプの場合は小音域のパワーが強くでています。このことから、入力した音がタップorスワイプを判別するプログラムを記述してみましょう。
==== 課題 ====
タップ、スワイプ以外の、2種類の音を識別するプログラムを記述してください。
* 雛形:{{ :lecture:myscratchinput2019.zip |}}
* 提出先:kibaco
* 締切:6月12日(水)17:00(日本時間)迄
* 提出方法:認識している様子を動画で保存して提出してくだい。
* QuickTimeの新規画面収録を利用するとデスクトップ動画を保存することができます。
* スマートフォン等で様子を撮影してもらっても構いません。
* 動画サイズですが、100MB程度を目安にしてください。
====== 論文読解4:You Only Look Once: Unified, Real-Time Object Detection ======
[[https://arxiv.org/pdf/1506.02640.pdf]]
>Joseph Redmon∗, Santosh Divvala∗†, Ross Girshick¶, Ali Farhadi∗†
===== Abstract =====
We present YOLO, a new approach to object detection. Prior work on object detection repurposes classifiers to per- form detection. Instead, we frame object detection as a re- gression problem to spatially separated bounding boxes and associated class probabilities. A single neural network pre- dicts bounding boxes and class probabilities directly from full images in one evaluation. Since the whole detection pipeline is a single network, it can be optimized end-to-end directly on detection performance.
Our unified architecture is extremely fast. Our base YOLO model processes images in real-time at 45 frames per second. A smaller version of the network, Fast YOLO, processes an astounding 155 frames per second while still achieving double the mAP of other real-time detec- tors. Compared to state-of-the-art detection systems, YOLO makes more localization errors but is less likely to predict false positives on background. Finally, YOLO learns very general representations of objects. It outperforms other de- tection methods, including DPM and R-CNN, when gener- alizing from natural images to other domains like artwork.
{{youtube>VOC3huqHrss?medium}}
ここでは,リアルタイム物体検出で有名なYOLOの論文を読み,実際にYOLOを利用してみます.YOLOそのものを実装しませんが,深層学習におけるネットワークのデザインや意味に関して概略的に学び,それを利用したアプリケーションを作成するところまでを行ってみます.
===== YOLO をOpenframeworksで動かす =====
馬場がgithub上で公開している ofxOpenCvDnnObjectDetection というOF用のアドオンを利用します.
* https://github.com/TetsuakiBaba/ofxOpenCvDnnObjectDetection
Installの手順に従って準備して、動作を確認してみましょう。
===== カメラ画像をリアルタイムでObject Detectionする =====
インストールが終わったら、実際に動作させてみましょう。すでにカメラ画像をリアルタイム認識するサンプルは用意されていますが、
せっかくなのでここでは位置からプロジェクトを作成するところから始めてみます。手順としては
- projectGeneratorで ofxOpenCvDnnObjectDetection をaddonから選択すし、myApp/testObjectDetection をプロジェクトとして作成する
- examples/single_image/bin/data にある以下のファイルを testObjectDetection/bin/data にコピー
- yolov2-tiny.weights
- yolov2-tiny.cfg
- coco.txt
- コードをかいて、実行する。
===== 映像ファイルをリアルタイムにObject Detectionする =====
上記の手順を利用して、動画ファイルから実際に物体検出させてみましょう。これは今回の授業の課題です。
* 課題
* 締切:2019年6月19日(水) 〜17:00
* 提出先:kibaco
* 提出方法:認識しているところを動画に保存し、その動画を提出してくさい。
* 注意事項
* ofSaveScreenではalpha値を含むフォーマットになっているため、jpg形式で保存するとカラーがおかしくなります。(https://forum.openframeworks.cc/t/ofsavescreen-outputs-file-with-wrong-color/32615 )保存するファイルはpngにしてください。
* 認識対象となる動画はみなさんの好きな動画にしてください。
* 提出する動画ファイルサイズは 100MB程度になるよう(少しくらいなら超えても構いません)にしてください。解像度、動画の長さなどは特に指定しません。
* 作成する動画は授業でならったofSaveScreenを利用して作成してください。
* 作成した連番画像はffmpegを利用すると簡単に一つの動画にできます。ffmpegのインストールから使い方は [[ffmpeg:インストール|ffmpegをインストール]] を参照してください。
===== Dataset作成用アプリケーションを作る =====
一般にAnnotationツールと読んでいますが,そもそも作らなくても結構いろいろございます.詳しくは下記のリンクを参照してください.
手動でアノテーション(ラベル付)をするためのツールが紹介されています.
* https://en.wikipedia.org/wiki/List_of_manual_image_annotation_tools
この授業では既存アプリケーションではなく、ofxOpenCvDnnObjectDetectionに含まれているannotation Exampleを利用してアノテーションをすすめていきます。Example/annotationをビルドしてもよいですし、下記からアプリケーションをダウンロードしても構いません。
* {{ :lecture:インタラクションデザイン:annotation.zip |}}
==== 設計 ====
- イメージが保存されたディレクトリを読み込んで,アノテーションを行う
- 動画ファイルを読み込んで,任意時間ごとのフレームに対してアノテーションを行う
- カメラ画像をアノテーションする
==== アノテーションしてみよう ====
==== ofxOpenCvDnnObjectDetectionのExample/annotaion を利用してデータセットを作成する ====
先ずはannotationをXcode上で実行してアプリケーションを立ち上げます.annotation/bin/data/test_images
にサンプル学習セットが含まれているので,立ち上げた画面に test_images フォルダをドラッグアンドドロップして
ください.すると犬の画像と読み込まれたBounding Boxが同時に表示されます.矢印の右・左キーで対象となる画像を進めたり
戻したりできます.画像データは下記形式を守ってください.
* jpg形式のみ(拡張子名も.jpgのみで,JPGやjpegなどは不可)
* jpgおよび対応するtxtファイルのフォルダ名はallとしてください.
右のクラス一覧から登録したいクラス名をクリックして,ドラッグアンドドロップでバウンディングボックスを引くとそれが同名の
txtファイルにてリアルタイムで保存されます.実際に登録したものがtxtファイルに保存されるかを自分で確かめて見ましょう.
この他,controlキーを押すと,yolov2-tiny.weightsファイルからの自動認識結果も合わせて表示してくれます.
実際にannotation作業を行う際,同じようなバウンディングボックス作成が連続して苦痛なので,それを和らげるために,
自動で正しく認識している箇所はワンクリックで登録できる粋なはからいになっています.このような機能は先に紹介した
microsoftのannotationツールでも実装されています.ただし全く新しいデータ・セットを作成する際はcocoの80クラスでは
支援作業に意味をなさない場合がほとんどなので,一番最初はちょっとだけデータセット作成を完全に手動で行い.少しデータが
溜まったところ(500枚程度)で,一度学習を行い,次からはその学習済みファイルを利用することが有効だと思います.
=== png形式画像の一括jpg変換 ===
収集した画像がjpg形式ではなかった場合,それらを手作業で一つひとつjpg形式に変換するのはかなり骨がおれます.
例えば1,000枚の画像を集めてきたところ,その中にはpngファイル等のjpgではないものがいくつか含まれている場合を考えてみます.
このような場合でもコマンドを利用することで簡単に一括で,例えばpngファイルをjpgに変換することができます.
画像変換コマンドにはImageMagickというパッケージをインストールしておくと,mogrifyという画像変換コマンドがインストール
されるようになるので,まずはImageMagickをインストールします.
$ brew install imagemagick
その後,例えば hoge というフォルダ内にtest1.png, test2.png というpngファイルがあるとします.この場合,これらを
一括でjpg形式に変更するには hoge フォルダ内において,下記コマンドを実行すればよいです.
$ mogrify -quality 100 -format jpg *.png
これで png形式のファイルを一括で最高品質のjpgに変更してくれます.
=== 動画ファイルから一定の時間感覚でjpgファイルを切り出す ===
これはffmpegなるコマンドを使えば可能です.具体的には
$ ffmpeg -i input.mp4 -r 1 -q:v 1 %06d.jpg
ただし,input.mp4 は切り出し対象となる動画ファイルに置き換えてください.-r:一秒間に何枚切り出すか。-q:jpg画質で、1が最高
==== 作成したデータセットを学習させ,データセットに不備がないかを確認する ====
データセットを学習されるためには次に示す手順が必要です.
- 学習対象となる画像への絶対パスをまとめた train.txt (名前は任意)というテキストファイル
- クラス一覧を記した names.txt (名前は任意)というテキストファイル
- 上記 train.txt, names.txt や学習済みデータを保存する先のディレクトリ名 を記した meta.txt (名前は任意)
の合計3つのファイルを準備する必要があります.
=== train.txtファイルの生成 ===
このファイルには学習対象となる画像ファイルへのパスを一行に付き一枚ずつ入力しておけばよいです.これも手作業では
気の遠くなる作業ですが,コマンドを利用することで簡単にファイルを作成できます.annotation作業した画像が all という
フォルダ内に全て含=== 見出し ===
まれている場合,下記コマンドを実行することで,画像ファイルパス一覧をtrain.txtに保存することができます.
$ find `pwd`/all -name "*.jpg" > train.txt
findは検索コマンドです.pwdは今いるディレクトリまでのパスを教えてくれるコマンドです.上記を実行することで,allフォルダ以下にある,拡張子がjpgのファイルを見つけてきて,一行ずつ表示してくれ,その表示をtrain.txtファイルにそのまま出力(リダイレクト)しています.
=== names.txtファイルの作成 ===
このファイルがクラス名が記述されたファイルになります.これはすでにannotation作業を行った際,coco.namesというファイルを編集することで予めクラスファイルを作成できていますので,ここで利用したcoco.namesを names.txt として保存しておけば問題ありません.
=== meta.txtファイルの作成 ===
こちらは上記で作成したファイルの場所等を記しておくファイルです.具体的には下記のようにしておきます.
classes= 1 # ここはクラス数を記入する
train = /train.txt # にはそれぞれの環境に見合ったパスを入力する
names = /names.txt
backup = backup # ./darknetを実行する位置になるので,darknet/backup というフォルダに途中経過や最終結果を保存する
=== yolov2-tiny.cfg の修正 ===
今回はyolov2-tinyのネットワークを利用するので,こちらの雛形を流用します.
yolov2-tiny.cfgは darknet/cfg 以下に最初から含まれています.このファイルをまずは darknet/data/mydataset 以下に保存しましょう.
そしてyolov2-tiny.cfgを修正します.具体的には
* filter=425 を適切な数値に変更
* class=80 を適切な数値に変更
を行います.classは学習させるクラス数をそのまま記述すればよいです.filter=425と書かれているものは,(class数+5)*5で計算をします.例えば
クラス数が1であれば (1+5)*6 = 30 となります.また,80クラスの場合は (80+5)*5 = 425になっているわけです.
=== 学習 ===
では準備が整ったところで,実際に学習を行います.例えば darknet/data/mydataset 以下に先程のmeta.txt, train.txt, names.txt, yolov2-tiny.cfg, all/(アノテーションデータが入ったフォルダ)が置かれている場合,下記コマンドで学習を開始できます.
$ ./darknet detector train data/mydataset/meta.txt data/mydataset/yolov2-tiny.cfg
エラーなどがでず,そのまま学習が始まればデータ・セットはOKです.
Learning Rate: 0.001, Momentum: 0.9, Decay: 0.0005
Resizing
480
Loaded: 0.523287 seconds
Region Avg IOU: 0.092090, Class: 0.142081, Obj: 0.548218, No Obj: 0.456659, Avg Recall: 0.012723, count: 393
Region Avg IOU: 0.100210, Class: 0.157903, Obj: 0.522707, No Obj: 0.459687, Avg Recall: 0.016667, count: 420
1: 822.926941, 822.926941 avg, 0.001000 rate, 195.341817 seconds, 64 images
.
.
.
実際に学習をスタートされると気づくかと思いますが,とてつもなく時間がかかります.一回の学習(画像入力から重み調整まで)につき
上記の出力では195秒かかっています.それだけ学習されるかはデータ・セットの量やクラス数,実際の学習結果に応じて変化しますが,
cocoデータ・セットで学習されたyolov2-tiny.weightsファイルは約40万回(イテレーション)程度学習しています.
なので,単純に上記のような普通のPCでは902日(2.5年)かかる計算になります.あくまで目安ですのでご注意ください.
そこで作成したデータ・セットが問題なく学習できることを確認したら,そのデータ・セット一式を下記のアップロードリンクを利用して
馬場に送ってください.こちらの深層学習用PCで学習することで,1日程度で学習が終了します.
==== データのアップロード先 ====
作成したデータ・セットは下記リンクへzipファイルでまとめてアップロードしてください.
* https://www.dropbox.com/request/GwmzkBKnczulDbGMpcUt