workshop/InteractionDesignA/main

インタラクションデザイン演習・実習

ここまでの経過
ofApp.cpp
#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
  ofSetFrameRate(60);
  ofSetVerticalSync(true);
  
  cam.initGrabber(640, 480);
  tracker.setup();
  
}

//--------------------------------------------------------------
void ofApp::update(){
  cam.update();
  if(cam.isFrameNew()) {
    tracker.update(toCv(cam));
    
    if( tracker.getFound() == true) {
      face_outline = tracker.getImageFeature(ofxFaceTracker::FACE_OUTLINE);
      face_lefteye = tracker.getImageFeature(ofxFaceTracker::LEFT_EYE);
      face_righteye = tracker.getImageFeature(ofxFaceTracker::RIGHT_EYE);
      face_righteyetop = tracker.getImageFeature(ofxFaceTracker::RIGHT_EYE_TOP);
      face_lefteyetop = tracker.getImageFeature(ofxFaceTracker::LEFT_EYE_TOP);
      face_lefteyebrow = tracker.getImageFeature(ofxFaceTracker::LEFT_EYEBROW);
      face_righteyebrow = tracker.getImageFeature(ofxFaceTracker::RIGHT_EYEBROW);
      face_nosebase = tracker.getImageFeature(ofxFaceTracker::NOSE_BASE);
      face_nosebridge = tracker.getImageFeature(ofxFaceTracker::NOSE_BRIDGE);
      face_innermouth = tracker.getImageFeature(ofxFaceTracker::INNER_MOUTH);
      face_outermouth = tracker.getImageFeature(ofxFaceTracker::OUTER_MOUTH);

      float x1,y1, x2,y2;
      x1 = face_lefteye.getBoundingBox().getCenter().x;
      y1 = face_lefteye.getBoundingBox().getCenter().y;
      x2 = face_righteye.getBoundingBox().getCenter().x;
      y2 = face_righteye.getBoundingBox().getCenter().y;
      d_eyes =  sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
      d_eyes = d_eyes/face_outline.getBoundingBox().getWidth();

      // 記述
      ofVec2f p1(face_nosebase.getBoundingBox().getCenter().x,
                 face_nosebase.getBoundingBox().getCenter().y);
      ofVec2f p2(face_lefteye.getBoundingBox().getCenter().x,
                 face_lefteye.getBoundingBox().getCenter().y);
      float Wmax = face_outline.getBoundingBox().getWidth();
      f[0] = p1.distance(p2)/Wmax;
      
      p2.set(face_righteye.getBoundingBox().getCenter().x,
             face_righteye.getBoundingBox().getCenter().y);
      f[1] = p1.distance(p2)/Wmax;
      
      p1.set(face_lefteye.getBoundingBox().getCenter().x,
             face_lefteye.getBoundingBox().getCenter().y);
      p2.set(face_righteye.getBoundingBox().getCenter().x,
             face_righteye.getBoundingBox().getCenter().y);
      f[2] = p1.distance(p2)/Wmax;
      
      p1.set(face_innermouth.getBoundingBox().getCenter().x,
             face_innermouth.getBoundingBox().getCenter().y);
      p2.set(face_lefteye.getBoundingBox().getCenter().x,
             face_lefteye.getBoundingBox().getCenter().y);
      f[3] = p1.distance(p2)/Wmax;
      
      p2.set(face_righteye.getBoundingBox().getCenter().x,
             face_righteye.getBoundingBox().getCenter().y);
      f[4] = p1.distance(p2)/Wmax;

      p2.set(face_nosebase.getBoundingBox().getCenter().x,
             face_nosebase.getBoundingBox().getCenter().y);
      f[5] = p1.distance(p2)/Wmax;
      // ここまでが特徴量計算
      
      // 学習データとリアルタイム入力データを比較する
      float d;
      d_min = 100000.0;
      for( int i = 0; i < 24; i++ ){
        
        d = 0;
        for( int j = 0; j < 6; j++ ){
          d = d + sqrt( pow(f[j]-f_data[i][j], 2) );
        }
        if( d < d_min ){
          d_min = d;
          id = i;
        }
        
      }
      
    }
  }
}

//--------------------------------------------------------------
void ofApp::draw(){
  ofSetColor(255,255,255);
//  cam.draw(0,0);
  ofNoFill();
  
  if( tracker.getFound()){
    ofSetColor(255,255,255);
   // ofRect(face_outline.getBoundingBox());
    face_outline.draw();
    face_lefteye.draw();
    face_righteye.draw();
    face_lefteyetop.draw();
    face_righteyetop.draw();
    face_righteyebrow.draw();
    face_lefteyebrow.draw();
    face_nosebase.draw();
    face_nosebridge.draw();
    face_innermouth.draw();
    face_outermouth.draw();
    
    for( int i = 0; i < 6; i++ ){
      ofDrawBitmapString(ofToString(f[i]), 20, 20+20*i);
    }
    ofDrawBitmapString("1st Label: "+ f_label[id], 20, 200);
    ofDrawBitmapString("2nd Label: "+ f_label[id], 20, 220);
    ofDrawBitmapString("3rd Label: "+ f_label[id], 20, 240);
    ofDrawBitmapString("4th Label: "+ f_label[id], 20, 260);
    ofDrawBitmapString("5th Label: "+ f_label[id], 20, 280);
  }
}

void ofApp::keyPressed(int key)
{
  if( key == ' ' ){
    for( int i = 0; i < 6; i++ ){
      cout << f[i] << ",";
    }
    cout << endl;
  }
  
}


ofApp.h
#pragma once

#include "ofMain.h"
#include "ofxOpenCv.h"
#include "ofxFaceTracker.h"
#include "ofxCv.h"
using namespace ofxCv;

class ofApp : public ofBaseApp{
  
public:
  void setup();
  void update();
  void draw();
  void keyPressed(int key);
  
  ofVideoGrabber cam;
  ofxFaceTracker tracker;
  
  ofPolyline face_outline;
  ofPolyline face_lefteye;
  ofPolyline face_righteye;
  ofPolyline face_lefteyetop;
  ofPolyline face_righteyetop;
  ofPolyline face_righteyebrow;
  ofPolyline face_lefteyebrow;
  ofPolyline face_nosebase;
  ofPolyline face_nosebridge;
  ofPolyline face_innermouth;
  ofPolyline face_outermouth;
  
  float f[6]; // feature vector
  float d_eyes;
  string f_label[24] = {
    "baba",
    "oshimi",
    "shimizu",
    "ishisone",
    "yamamoto",
    "yuka iwakiri",
    "Mizuochi",
    "watanabe",
    "akiyama",
    "yuki nishimura",
    "MatsuhisaArata",
    "komuro",
    "kawamoto",
    "MoriyasuYuki",
    "tomigahara",
    "nakabayashi",
    "Washida",
    "adachi",
    "HyakudaMiyu",
    "miyazaki",
    "ishida",
    "kadokawa",
    "Ito",
    "ota"
  };
  float f_data[24][6] = {
    0.370257,0.371163,0.43258,0.535523,0.532393,0.187163, // baba
    0.459561,0.460181,0.45292,0.59919,0.586181,0.148298,  // oshimi
    0.430433,0.407373,0.439436,0.542988,0.534246,0.13597, // shimizu
    0.422714,0.415589,0.44051,0.549645,0.554631,0.150267, // ishisone
    0.382835,0.384571,0.440313,0.552813,0.550153,0.191428, // yamamoto
    0.404457,0.404457,0.464072,0.584925,0.603713,0.200586, // iwakiri
    0.452208,0.448805,0.47714,0.58728,0.597626,0.160829, // mizouchi
    0.448265,0.447662,0.556803,0.578956,0.575832,0.141881, // watanabe
    0.39728,0.413873,0.451812,0.535899,0.544494,0.154063, // akiyama
    0.452399,0.456414,0.440701,0.605559,0.6097,0.168862, // nishimura
    0.419564,0.399718,0.429543,0.556803,0.552554,0.163486, //matsuhisa
    0.41601,0.438202,0.438925,0.566923,0.57166,0.160066, // komuro
    0.426778,0.426778,0.431026,0.590969,0.589172,0.18024, // kawamoto
    0.375357,0.356279,0.424179,0.523429,0.516958,0.177383, // moriyasu
    0.401452,0.425011,0.413024,0.574572,0.598956,0.191239, // tomigahara
    0.347162,0.362726,0.362726,0.482143,0.491436,0.16129,  // nakabayashi,
    0.395024,0.391017,0.422592,0.506611,0.518161,0.136399, // watshida
    0.432933,0.421668,0.431693,0.587692,0.587169,0.177993, // adachi
    0.417576,0.42546,0.452324,0.568627,0.575322,0.169654, // hyakuda
    0.461068,0.486499,0.441604,0.631732,0.655549,0.185361, //miyazaki
    0.376225,0.382531,0.43423,0.510884,0.520496,0.156678, // ishida
    0.410518,0.424988,0.454646,0.58102,0.591329,0.189852, // kadokawa,
    0.475406,0.454339,0.507206,0.607821,0.593703,0.155152, // ito
    0.41279,0.410999,0.436605,0.529621,0.538464,0.138568 // ota
  };
  float d_min;
  int id;
};

ユーザとモノをつなぐ対話設計であるインタラクションデザインに関する体系的知識及び実践的基礎技術を身に付けることを目標とする.近年の論文からいくつかのトピックを議論し,最先端の研究分野をフォローできるための知識を獲得し,それらを実際に再現するために必要な技術要素を同時に身に付けることを目標とする.

本授業は首都大学東京インダストリアルアートコースに向けて開講している授業となります.

論文読解1:The smart floor: a mechanism for natural user identification and tracking

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'(ダッシュ)とした場合,次のような計算で距離を求めると,様々な学習済みデータとどの程度入力データがことなるのかを計算できるようになります. 本講義でのもっとも基本的で重要な考え方なので,しっかり理解しましょう. d = k = 1 n f k - f k ' 2


論文読解2:SmartVoice: 言語の壁を超えたプレゼンテーションサポーティングシステム

SmartVoice from WISS2013

WISS2013での発表映像(28分位から)

李 翔, 暦本 純一, SmartVoice:言語の壁を越えたプレゼンテーションサポーティングシステム, 日本ソフトウェア科学会インタラクティブシステムとソフトウェアに関する研究会(WISS2013), 2013.

WISS2013での和文論文です.OF及びofxFaceTrackerを利用しています.読み上げテキストと音声フレーズとのマッチングにDPアルゴリズムを用いています. DPはDynamic Programingのことで,動的なプログラム(編集)を指します.一般的に原稿にそって人間が読み上げを行い,その結果,音声データの位置と 原稿データの位置のマッチングをとるには音声信号処理を行うことが多いです.一方でこの論文ではそのような真っ向勝負はせず, 原稿データを一度ボイスシンセサイザーから再生し,音声データ2を作成します.この2つのデータはフレーズ区切り情報が含まれていますが, その順番が一致していない可能性が大いにあります.そこで音声再生時間をマッチングの為のコスト距離として動的に順番を合わせること を行っています.また読み上げの際は,ただ単純に読むだけでなく発表者の表情をインタフェースに声量や速度を調整できるようにしています.

ここではDPアルゴリズムを理解することと,フェイストラッカーを利用して簡単な表情認識を行うことをします. 表情認識には,smart floorで学習した多次元特徴量の距離計算をベースに実装してみます.

形状取得の雛形

ofApp.h
#pragma once

#include "ofMain.h"
#include "ofxOpenCv.h"
#include "ofxFaceTracker.h"
#include "ofxCv.h"
using namespace ofxCv;

class ofApp : public ofBaseApp{
  
public:
  void setup();
  void update();
  void draw();
  
  ofVideoGrabber cam;
  ofxFaceTracker tracker;
  
  ofPolyline face_outline;
  ofPolyline face_lefteye;
  ofPolyline face_righteye;
  ofPolyline face_lefteyetop;
  ofPolyline face_righteyetop;
  ofPolyline face_righteyebrow;
  ofPolyline face_lefteyebrow;
  ofPolyline face_nosebase;
  ofPolyline face_nosebridge;
  ofPolyline face_innermouth;
  ofPolyline face_outermouth;
  
  float d_eyes;
};
ofApp.cpp
#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
  ofSetFrameRate(60);
  ofSetVerticalSync(true);
  
  cam.initGrabber(640, 480);
  tracker.setup();
}

//--------------------------------------------------------------
void ofApp::update(){
  cam.update();
  if(cam.isFrameNew()) {
    tracker.update(toCv(cam));
    
    if( tracker.getFound() == true) {
      face_outline = tracker.getImageFeature(ofxFaceTracker::FACE_OUTLINE);
      face_lefteye = tracker.getImageFeature(ofxFaceTracker::LEFT_EYE);
      face_righteye = tracker.getImageFeature(ofxFaceTracker::RIGHT_EYE);
      face_righteyetop = tracker.getImageFeature(ofxFaceTracker::RIGHT_EYE_TOP);
      face_lefteyetop = tracker.getImageFeature(ofxFaceTracker::LEFT_EYE_TOP);
      face_lefteyebrow = tracker.getImageFeature(ofxFaceTracker::LEFT_EYEBROW);
      face_righteyebrow = tracker.getImageFeature(ofxFaceTracker::RIGHT_EYEBROW);
      face_nosebase = tracker.getImageFeature(ofxFaceTracker::NOSE_BASE);
      face_nosebridge = tracker.getImageFeature(ofxFaceTracker::NOSE_BRIDGE);
      face_innermouth = tracker.getImageFeature(ofxFaceTracker::INNER_MOUTH);
      face_outermouth = tracker.getImageFeature(ofxFaceTracker::OUTER_MOUTH);

      float x1,y1, x2,y2;
      x1 = face_lefteye.getBoundingBox().getCenter().x;
      y1 = face_lefteye.getBoundingBox().getCenter().y;
      x2 = face_righteye.getBoundingBox().getCenter().x;
      y2 = face_righteye.getBoundingBox().getCenter().y;
      d_eyes =  sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
      d_eyes = d_eyes/face_outline.getBoundingBox().getWidth();

    }
  }
  
}

//--------------------------------------------------------------
void ofApp::draw(){
  ofSetColor(255,255,255);
//  cam.draw(0,0);
  ofNoFill();
  
  if( tracker.getFound()){
    ofSetColor(255,255,255);
   // ofRect(face_outline.getBoundingBox());
    face_outline.draw();
    face_lefteye.draw();
    face_righteye.draw();
    face_lefteyetop.draw();
    face_righteyetop.draw();
    face_righteyebrow.draw();
    face_lefteyebrow.draw();
    face_nosebase.draw();
    face_nosebridge.draw();
    face_innermouth.draw();
    face_outermouth.draw();

    ofDrawBitmapString(ofToString(d_eyes), 20, 20);
    
    ofDrawBitmapString("("+ofToString((int)face_outline.getBoundingBox().x)+
                       ","+ofToString((int)face_outline.getBoundingBox().y)+
                       ")",
                       face_outline.getBoundingBox().x,
                       face_outline.getBoundingBox().y);
  }
  
  
}

練習問題

FaceTrackerのgetGesture関数より口の開閉等の各データを取得し,それらリアルタイムデータと記録したデータ間においてN次のユークリッド距離を用いることで,任意表情の区別をせよ. ただし利用するGestureはLEFT_EYEBROW_HEIGHT, RIGHT_EYEBROW_HEIGHT, LEFT_EYE_OPENNESS, RIGHT_EYE_OPENNESS, MOUTH_HEIGHT, MOUTH_WIDTH, JAW_OPENNESSの7次元とする.


サンプル1:FaceTracker雛形
サンプル2:FaceTrackerによるパラメータ表示やユークリッド距離を示したサンプル

課題

上記練習問題の条件より,3種類以上の表情を識別するプログラムを記述せよ
サンプル3:FaceTrackerとユークリッド距離の類似度による表情識別


論文読解3:Scratch Input: Creating Large, Inexpensive, Unpowered and Mobile finger Input Surfaces

著者の研究紹介ページ(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でナイーブベイズ識別行うプログラムも挑戦 してみるとよいでしょう.ただし著者らは精度が良くなかったと述べています.ナイーブベイズ識別に関しては 本サイトのインタラクションデザインの為のパターン認識入門を参照してください. この他ウェブサイトや参考書でもナイーブベイズに関する解説は多く入手できると思います.

基礎知識:Easy DFT, DFT, FFT

入力された信号がどのような周波数成分を持つかを解析する一般的な手法です.すべての信号はsinとcosの足し合わせで表現できる.というフランスの数学者フーリエの考えに基づいた理論です. ここではその考えを直感的に表現してみたEasy DFTを実際に利用し,その後DFTを理解します.さらに高速化手法であるFFTを用いて周波数解析する方法に慣れてみましょう. ちなみにEasy DFTというのは存在しません.馬場が勝手に授業用に用意した簡易なDFTとなります.

基礎練習:GUI操作

様々なパラメータを利用して,ユーザの入力値を調整していく際,GUI(Graphical User Interface)を利用してその値を手軽に変更できると,作業が捗ります. そこで,OpenFrameworksに標準(v.0.8以降)のofxGuiを利用して,プログラム内の変数を動的に変更する方法を練習します. このあたりは /examples/gui/guiExample の例を実行して内容を確認してみます.

基礎練習:C++における vector型

様々な処理に配列は多く利用されます.信号処理や画像処理にかぎらず,一般的な計算には配列は多用されます. この配列を何度も利用しているうちに,いろいろと不便なことに気づきます.例えば最初は配列サイズを100確保 したのに途中からそれ以上必要になってしまった時,配列の中にあるデータを削除し,削除した分を詰めて配列に 再度格納したいとき等です.vector型に関する詳しい説明は多くの参考書やウェブサイトに譲るとして,ここでは 基本的な使い方を抑えたいと思います.emptyExampleをベースに実習してみましょう.

ofApp::draw()
  vector<int>a;  // 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の変化によって 識別結果が大きく左右されてしまいます.そこで,一般的には正規化処理を行うことでこの問題に対応します. 具体的には次のような式を用います x = x - x min x max - x min

すると,先ほどの例では(10,5), (5,6)でしたが,x:5-10, y:5-6であることから,それぞれを正規化すると (1,0), (0,1)となります.これでそれぞれの次元において,値は異なるものの,変化の度合いを揃えることができます.

ScratchInputZ

まずは上記のテンプレートをダウンロードして実行して下さい.ofxGuiとvector型を用いて記述されています. 一緒に読んでプログラムになにが記述されているかを把握します.

課題
  • テンプレートを利用して,数種類のスクラッチインプットを識別するプログラムを記述してみましょう. ただし,特徴量として,次を利用してみます.ピークカウント,正規化周波数積分(0-3000),(3000-8000),(10000-20000). また,認識には論文と同様決定木にて実装してください.
  • 答え:ScratchInputZ

Wekaを使う

Weka Data Mining Software in Java
Weka is a collection of machine learning algorithms for data mining tasks. The algorithms can either be applied directly to a dataset or called from your own Java code. Weka contains tools for data pre-processing, classification, regression, clustering, association rules, and visualization. It is also well-suited for developing new machine learning schemes.

Wekaとは手軽に様々な機械学習アルゴリズムを実践できるソフトウェアになります.これまでの論文読解では,一つのラベルにつき, 一つの学習データと比べるだけのものでした.しかし,各ラベルに対して適合するデータがひとつとは限りません.このような場合 一般的に機械学習を用いるのが一般的です.決定木やkNNやナイーブベイズ,ニューラルネットやSVM等もこのソフトウェアを利用して学習・評価を 行うことが出来ます.この演習実習ではほとんどが教師あり学習なので,クラスタリング等にはあまり触れていませんが,それらも Weka上で実行することが出来ます.簡単な例題を通じてWekaとarffファイルの使い方を学習します.

練習問題
  • 実際にScratchInputZの入力でデータ識別をWekaの決定木にて実行してみましょう.

論文読解4:SideSight: multi-"touch" interaction around small devices

Alex Butler, Shahram Izadi, and Steve Hodges October 2008 Author's Page

Butlerらによる,フォトリフレクタを用いた線形多点配置によるマルチタッチ操作に関する報告になります.上記Author's Pageの リンクからPDFがダウンロードできいます.HTCのスマートフォンに小型のフォトリフレクタを直線上に十個配し,各反射光データから ユーザの指先位置を特定し,いくつかのジェスチャ操作を可能にしています.これまでPCに予め備えられたカメラやマイクを利用して データをセンシングしていましたが,今回はArduinoとフォトリフレクタを利用して,データを取得・観察してみます.

fritzing:回路図作成ソフトウェアを使う

Fritzing is an open-source hardware initiative that makes electronics accessible as a creative material for anyone. We offer a software tool, a community website and services in the spirit of Processing and Arduino, fostering a creative ecosystem that allows users to document their prototypes, share them with others, teach electronics in a classroom, and layout and manufacture professional pcbs. fritzingのページ

フォトリフレクタは赤外線発光素子と赤外線受光素子からなるモジュールで,その反射光強度を測定することでモジュールとオブジェクトの 距離を電位差で取得することができます.単純に距離センサだけでなく,周辺環境の状態を測定するために便利なモジュールとして,古くからさまざま な研究で利用されています.LEDへの供給電流と,受光トランジスタ部のセンシング強度を調整することで,適切な検知範囲を調整できます. まずは下記のような回路をブレッドボード上で作成してみましょう.

図:フォトリフレクタとArduinoの配線回路図

重要:このような回路図は必ず制作の過程で残して下さい.手書きでも構いません

練習
  • 回路図作成ソフトウェア「fritzing」ダウンロードして,起動してみましょう
  • 上記回路図をfritzingを用いて自分でも作成してみましょう.
  • 上記回路図を基に,センサモジュールを追加し,3つのセンサ値をそれぞれ,0,1,2チャンネルで読み取れるようにしましょう

ArduinoとOpenFrameworksでセンサ値を表示する

センサからデータを取得するには,回路図の設計とその値をArduinoのanalogRead()関数を用いれば良いです. まず,得られたデータの特徴づけには,実際に得られるデータ(生データ)を観察することが最も基本的且つ重要と なります.まずは得られるデータをOpenFrameworks上でグラフとして観察可能なプログラムを準備します. 実はこのようなグラフ表示はすでに学習したScratchInputのプログラム内にて,すでに利用しています. 具体的にはコンテナ(vector)を利用して,データを逐次更新・表示するものです.

ではまず,センサ1チャンネル分を表示してみましょう.

Arduino側プログラム
void setup()
{
  Serial.begin(9600);
}
void loop()
{
  int a = analogRead(0);
  a = map(a, 0,1023, 0,99);
  Serial.write(a);
  delay(30);
}

ofApp.cpp
#include "ofApp.h"

void ofApp::setup(){
  serial.setup(0, 9600); // 一番最後に接続したUSB
}

void ofApp::update(){
  if( serial.available() > 0 ){
    data.push_back(serial.readByte());
    if( data.size() > ofGetWidth() ){
      data.erase(data.begin());
    }
  }
}

void ofApp::draw(){
  ofBackground(100);

  ofNoFill();
  ofBeginShape();
  for(int i = 0; i < data.size(); i++ ){
    ofVertex((data.size()-i), data[i]+ofGetHeight()/2);
  }
  ofEndShape();
}

void ofApp::keyPressed(int key){
}
ofApp.h
#pragma once

#include "ofMain.h"

class ofApp : public ofBaseApp{

public:
  void setup();
  void update();
  void draw();
  
  void keyPressed(int key);
  ofSerial serial;
  vector<int> data;
};

次にセンサのチャンネル数を増やしてみます.ここで,Arduino側からの送信値をA0,A1,A2...の順番に送った場合, openFrameworks側で,得られたデータがアナログチャンネルのどれに当たるかを知る必要があります. 例えばopenFrameworks側でA0と思って受け取ったデータが実際にはA1のデータであっては困ります.そこで, Arduino側のデータ送信にフォーマットを決めます.つまり,次のようにしてみましょう.

  • 1バイト目:チャンネル番号+100, 2バイト目:値(0-99)

このようにすることで,1バイト目と2バイト目は値の範囲がかぶることが無いので,必ずどちらのデータが 来たのか,プログラム上で判別することが出来ます.プログラム内では最初の1バイト目だけをまず監視し, 100以上の値が来た場合,次のバイトを該当するチャンネルのデータとして読み込みます.

QTR-8Aを使う

RPR-220を利用してフォトリフレクタの値を複数読みだしてみました.SideSightでは片側に10個のフォトリフレクタを 配置しています.ここで,Arduino Unoではアナログ入力のチャンネル数が6までしかないため,今回は6チャンネルの入力 で実践してみます.RPR0-220を6つ並べても良いのですが,すでにPololu社から8チャンネルの線形配置フォトリフレクタから 販売されています.そこでこちらを使って実際の値を観察することから始めます.ちなみにGitHubを通してQTR-8シリーズの 値をArduino上でいろいろ検証できるライブラリが提供されています.興味のある人は試してください.この演習実習では 実際のデータ処理はすべてコンピュータ側(OpenFrameworks)にて行うので,特になんの前処理も行わず,Arudinoの アナログチャンネルデータをすべてPCに送信するだけのシンプルなものにとどめます.

ofApp.cpp
#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
  serial.setup(0,9600);
}

//--------------------------------------------------------------
void ofApp::update(){
  unsigned char ch;
  while( serial.available() >= 2 ){
    ch = serial.readByte();
    if(  ch >= 100 ){
      unsigned char val = serial.readByte();
      a[ ( ch-100) ].push_back(val);
      while( a[(ch-100)].size() > ofGetWidth()){
        a[(ch-100)].erase(a[(ch-100)].begin());
      }
    }
  }
}

//--------------------------------------------------------------
void ofApp::draw(){
  ofSetColor(0,0,0);
  ofNoFill();

  for( int j = 0; j < 6; j++ ){
    ofDrawBitmapString(ofToString(a[j].size()),
                       10, 100+100*j);
    ofBeginShape();
    for( int i = 0; i< a[j].size(); i++ ){
      ofVertex(i, a[j][(a[j].size()-1-i)]+100*j);
    }
    ofEndShape();
  }
  
}

ofApp.h
#pragma once

#include "ofMain.h"

class ofApp : public ofBaseApp{
  
public:
		void setup();
		void update();
		void draw();
  
  ofSerial serial;
  vector <int> a[6];
  
};
arduino用プログラム
void setup()
{
  Serial.begin(9600);
}

void loop()
{
  int a[6];
  for( int i = 0; i < 6; i++ ){
    a[i] = analogRead(i);
    a[i] = map(a[i], 0, 1023, 0, 99);
    Serial.write(i+100);
    Serial.write(a[i]);
  }

  delay(30);
}

OFを実行した際の画面,上から順に0-5チャンネルのアナログ値をグラフ表示しています.

まずは,上記サンプルを動作させてください.データを格納するコンテナ(vector)は画面横サイズをMAXとしています.上から順に 各チャンネルデータを a[i] に保存し,そのデータをグラフとして表示しています.また,各チャンネルの最新値を左上に数値で 表示すると同時に折れ線グラフとしても表示しています.

では次に,指先位置を検知するにはどのようにすればよいか考えてみます.SideSightではセンサデータからDepthマップを 作成し,その領域を検出後,中間位置を指先の位置としています.すでに基となるデータは取得しています.6chのセンサ値を 取得していますが,実際にこれだけでは,解像度の低い位置(6段階)しか検知できません.そこで,Depthマップ同様に, 各センサ間の値を補間することで,各センサ間のデータを作成してみます.このような手法は一般に補間(Interpolation)や 内挿と呼ばれます.詳しくはWiki等で調べてみてください.ここでは各頂点を直線で結ぶ線形補間を利用します.

線形補間

openFrameworksでは簡単な線形補間が実装されています.そこでこれを利用して線形補間を行ってみましょう. OpenframeworksではofPolylineにそのメソッドが実装されています.最初に3点の頂点座標を用意し,それを 元に3点しかなかった頂点座標を線形補間して10点にしてみます.

ofApp.cpp
#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
  p.addVertex(20,200);
  p.addVertex(120,100);
  p.addVertex(220,200);
}

//--------------------------------------------------------------
void ofApp::update(){
  
}
//--------------------------------------------------------------
void ofApp::draw(){
  ofBackground(100);
  ofNoFill();
  
  ofSetColor(255);
  ofBeginShape();
  ofVertex(p.getVertices()[0]);
  ofVertex(p.getVertices()[1]);
  ofVertex(p.getVertices()[2]);
  ofEndShape();
  ofDrawBitmapString("Original Data Size:"+ofToString(p.size()),
                     20, ofGetHeight()/2-100);
  
  
  
  ofSetColor(0,255,0);
  ofPolyline p_resample;
  p_resample = p.getResampledByCount(10);

  ofBeginShape();
  for( int i = 0; i < 9; i++ ){
    ofVertex(p_resample.getVertices()[i].x+200,
             p_resample.getVertices()[i].y);
  }
  ofEndShape();
  ofDrawBitmapString("Original Data Size:"+ofToString(p_resample.size()),
                     220, ofGetHeight()/2-100);

}
ofApp.h
#pragma once

#include "ofMain.h"

class ofApp : public ofBaseApp{
public:
  void setup();
  void update();
  void draw();
  
  ofPolyline p;
};

実行すると上記のような画面となります.左側は3点の頂点を直線で表記したもの.右はそれを10点でリサンプル したデータの各点を半径2の円で描いたものです.データサイズが3から10に増えているのがわかります. 現行(v0.8.4以前)のOpenframeworksではこのgetResampleByCount関数に不備があり,指定した数で リサンプル出来なかったり,N-1の点の位置がずれて計算されています.すでにフォーラムに書き込みがあり, 次のバージョンからは修正される予定ですが,現状では問題を抱えています

指位置の検出

ここまでの準備を基に,6頂点データを一旦リサンプルし,それぞれのyデータをdepthデータとします. Depthデータを基にオブジェクト検出の閾値を設定し,検出範囲を判定し,その中間点を指位置とするプログラムに 修正してみましょう.下記は動作している際の映像になります.ユーザの指位置を6chのアナログデータですがかなり精確に 捉えているのがわかります.

簡単なジェスチャ操作の認識

上記により,ポインティングが可能になりました.ここでは更にタップ,スワイプといったジェスチャを 認識するプログラムを記述してみます.下記に雛形となるプログラムを置きます.前回記述したプログラムは ofxQTR8AというOpenFrameworksのaddonとして整理しておきました.一緒に確認してみましょう.

今回からQTR8AをArduinoのソケットに直結出来るようにしておきました.MacBookAir の横におくと,いい塩梅でアームレスト部分を拡張入力デバイスに利用できます.


論文読解5:Gestures without libraries, toolkits or training: A $1 recognizer for user interface prototypes.

これまで学習してきた内容を元に数字のオンラインストローク認識を行うアプリケーションを開発してみます. 日本語のすべての文字に対応するには様々な工夫が必要となります.例えばZinniaではSVMによる手書き文字認識インタフェース を提供しています.アルゴリズムを作るだけでなく,コーディングや学習データ登録を考えると大きなコストがかかるのが容易に 想像できるかと思います.本演習/実習の最後では,数字を認識対象にしたオンラインストローク認識アプリケーションを開発します.

ここで紹介する認識手順と類似する手法は2007年のACM UISTにて発表された内容をベースにしています. ジェスチャー認識には様々なアルゴリズムが利用されていますが,高速で認識精度がより本手法を学習します.元々このやり方は 馬場が個人的にこの方法がいいなと使っていたのですが,似たようなアルゴリズムでやっている先行研究がこちらになります.

Wobbrock, J.O., Wilson, A.D. and Li, Y. (2007). Gestures without libraries, toolkits or training: A $1 recognizer for user interface prototypes. Proceedings of the ACM Symposium on User Interface Software and Technology (UIST '07). Newport, Rhode Island (October 7-10, 2007). New York: ACM Press, pp. 159-168.

一般的に文字の認識にはオンラインとオフラインがあり,オンラインは筆跡データ(座標や画数)を保持したもの, オフラインは記述された後の画像データと考えてください.筆記のプロセスのある/なしになります.今回はオンラインを対象とします.

ストロークを取得するプログラム

すでにオンラインストローク認識によるアプリケーションは数多く報告されており,iOSやAndroidアプリでもすぐに体験できます. 例えば MySctipt.com では,それらアプリケーションやSDKを配布しています. これらアプリケーションを本腰いれて作ることは,研究の観点からは面白くありませんが,演習実習としては意義のある学習になります. では,まず,実際にOpenframeworksを使い,マウスで画面上に線を各プログラムを記述することから始めます.それらのデータから 特徴量を決定し,実際に高精度の数字認識を行うプログラムを記述してみましょう.

マウスの軌跡を描画するプログラム
ofApp.cpp
#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){}

//--------------------------------------------------------------
void ofApp::update(){}

//--------------------------------------------------------------
void ofApp::draw(){
	stroke.draw();
}

//--------------------------------------------------------------
void ofApp::keyPressed(int key){}

//--------------------------------------------------------------
void ofApp::mouseDragged(int x, int y, int button ){
	if( ofGetMousePressed() == true ){
		stroke.addVertex(x, y);
	}
}

//--------------------------------------------------------------
void ofApp::mousePressed(int x, int y, int button){
	stroke.addVertex(x, y);
}

//--------------------------------------------------------------
void ofApp::mouseReleased(int x, int y, int button){
	stroke.addVertex(x, y);
}


ofApp.h
#pragma once

#include "ofMain.h"

class ofApp : public ofBaseApp{

public:
	void setup();
	void update();
	void draw();
	
	void keyPressed(int key);
	void mouseDragged(int x, int y, int button);
	void mousePressed(int x, int y, int button);
	void mouseReleased(int x, int y, int button);
	
	ofPolyline stroke;

};

このままでは画数を識別せず,すべて一筆書きとしてデータが扱われてしまいます. そこで,画数も保存するプログラムに変更します.これにはofPolylineをvector形式で 作成し直します.これで,ユーザのストロークデータを保持できるようになりました.なお,ofPolylineとは 頂点を複数個持つ線分を保持するために用意されたクラスになります.頂点の追加にはaddVertex関数を利用します.

マウスの軌跡を画数ごとに表示する
ofApp.cpp
#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){}
//--------------------------------------------------------------
void ofApp::update(){}
//--------------------------------------------------------------
void ofApp::draw(){
	if( stroke.size() > 0 ){
		for( int i = 0; i < stroke.size(); i++ ){
			stroke[i].draw();
		}
	}
}

//--------------------------------------------------------------
void ofApp::keyPressed(int key){}

//--------------------------------------------------------------
void ofApp::mouseDragged(int x, int y, int button ){
	if( ofGetMousePressed() == true ){
		stroke[stroke.size()-1].addVertex(x,y);
	}
}

//--------------------------------------------------------------
void ofApp::mousePressed(int x, int y, int button){
	ofPolyline p;
	p.addVertex(x,y);
	stroke.push_back(p);
}

//--------------------------------------------------------------
void ofApp::mouseReleased(int x, int y, int button){
	stroke[stroke.size()-1].addVertex(x,y);
}


ofApp.h
#pragma once

#include "ofMain.h"

class ofApp : public ofBaseApp{

public:
	void setup();
	void update();
	void draw();
	
	void keyPressed(int key);
	void mouseDragged(int x, int y, int button);
	void mousePressed(int x, int y, int button);
	void mouseReleased(int x, int y, int button);
	
	vector < ofPolyline > stroke;
};

特徴量の抽出

では次に,実際のサンプリングデータを観察し,どのような特徴量があればよいかを検討します. 現状のプログラムでは,データを線分で結んでいるため,より詳細に観察するには,データの点も同時に 描画してみましょう.

ofApp::draw()を書き換え

void ofApp::draw(){
	if( stroke.size() > 0 ){
		for( int i = 0; i < stroke.size(); i++ ){
			stroke[i].draw();
			for( int j = 0; j < stroke[i].size(); j++ ){
				ofCircle(stroke[i].getVertices()[j], 3);
			}
		}
	}
}

下の映像をみて分かる通り,実際にサンプリングされているデータは,ストロークの移動速度により,値が重複したり, 座標間に空白の多く含まれる場合が多々あります.特徴量はこれらのストローク移動速度に影響を受けないものにする必要が あります.実際どのようにすればよいか,まずは自分で考えてみてください.このストロークデータをそのまま利用せず,なにかしらの 前処理が必要であることが考えられるかと思います.

論文読解5(その2):ストロークデータの単純化(Simplification)

ALGORITHMS FOR THE REDUCTION OF THE NUMBER OF POINTS REQUIRED TO REPRESENT A DIGITIZED LINE OR ITS CARICATURE

DAVID H DOUGLAS AND THOMAS K PEUCKER, University of Ottawa/Simon Fraser University,British Columbia

まずは,このすんごい古い論文を読んでみましょう.近年のコンピュータ・グラフィックにおいて,基礎的なline reductionに関する論文です. この手法はOpenframeworks上においても,simplify()関数によって実装・利用されています.下のコードからmouseRelease()の部分を 書き換えてみましょう.この手法をもちいることで,先のようなサンプリング座標は特徴を持つ箇所のみが残され,その他の意味のない座標が覗かれているのがわかるかと思います. たった一行の追加ですが,中身ではDouglass and Puckerのアルゴリズムが動いています.プログラム自体は再帰呼び出しを行うもので, 一度よく読み砕いてみても良いかと思います.今回の認識アルゴリズムではあまり意味のない部分ですが,例えばDTWにてマッチング処理を 行う場合,効率よく計算コストを下げることができるようになります.

ofApp:mouseReleased()を書き換え
void ofApp::mouseReleased(int x, int y, int button){
	stroke[stroke.size()-1].addVertex(x,y);
	stroke[stroke.size()-1].simplify(10);
}

座標データの正規化

ここまででストロークデータの前処理後,ある程度スッキリとした座標を取得できるようになりました.では次に記述された数字を認識するためには, どのような前処理を加える必要があるでしょうか?現状のデータでは,x,yの座標となっていますが,実際には書き出す位置や記述する文字の大きさに 対して影響を受けない特徴量とする必要があります.そうでなければ,座標が同じで大きさも同じサイズでないとうまく認識できなくなってしまいます.

座標移動:x,y値の最小値を座標基準(0,0)に変更

まずは,各x, y座標の最小値を基準に計算し直します.具体的には入力データにおけるx,y座標のそれぞれの最小値を求めます. もとまった最小値 Xmin, Yminを利用して,各座標をX-Xmin, Y-Yminにて計算し直します.

拡大縮小:入力された座標郡を横幅を基準に拡大/縮小し,座標を保存し直す

入力されたストロークデータは全て同じサイズで入力されるとは限りません.そこで,どのようなサイズのストロークが入力された場合でも, そのサイズの大小を無視できるように,ある一定の基準を基に,拡大縮小を行います.今回は横幅を100pxとなるよう,座標データの拡大縮小を計算し直します. 例えば実際に入力されたストロークデータの横幅(Xwidth)が250pxだった場合,拡大率 Mは,M = 100/Xwidth にて計算できます.このMを各座標に 対して乗算すればよいです.ただしこれだけでは,縦長の線を引いた場合,Xを基準にしては非常に大きな線分になってしまうため,縦横のどちらを基準にするかは大小関係を見た上で実行します.

特徴量の決定

以上の工程から抽出できた座標を特徴データとします.ただしこのままでは比較する特徴ベクトルの次元数が異なることが考えられるので, リサンプルによって次元を予め合わせておきます.今回はリサンプル数を10とします.なお,ofPolylineにすでにResampleが実装されているので その関数を利用します.これは線分の長さを考慮し,それを距離に応じてリサンプルし直すものになっています.今回は数字のみを対象としているため, その認識に適した処理が望まれます.ストロークデータは座標だけでなく,画数も利用しなければなりませんが.今回は10種類の数字を扱うだけのため, 複数の画数で入力された場合もそれを一筆書きのストロークデータとして,登録してしまいます.

識別器の決定

今回は実装の容易さからkNNアルゴリズムにてプログラムを実現します.k=1の最近傍探索とし,距離計算にはユークリッド距離を用います.もちろん kNNといっても計算方法には様々な種類があり,距離計算にマハラノビス距離を用いるものはよく知られた事例でもあります.余裕のある人はk=3等の他の パラメータを利用して識別結果を確認してみましょう.

学習データを登録する

下記に示すような形式にて,登録したストロークデータを保存することにします.最初にストローク名のあと,計10点の頂点を(x,y)形式にて保存します.

strokes.txt
0 (29.9903,0) (12.4813,13.5148) (3.37186,35.5292) (1.08183,59.0605) (0,82.8606) (12.1319,100) (33.0249,90.5739) (48.7737,72.6968) (44.8305,49.5768) (22.5645,11.9955) 
1 (0,0) (0.282469,10) (0.564972,20) (0.847458,30) (1.12994,40) (1.41243,50) (1.69492,60) (1.9774,70) (2.25989,80) (2.82486,100) 
2 (0,0) (18.5955,8.06976) (34.1103,19.9747) (45.9995,36.3931) (49.4915,54.7736) (46.1091,74.7605) (31.278,86.5432) (13.439,96.1706) (29.231,98.125) (49.4803,99.0625) (69.7297,100) 
3 (2.39521,0) (26.3755,0.978783) (42.5846,14.0671) (29.9077,33.0757) (8.62164,44.1623) (29.426,47.4161) (51.735,52.6674) (62.5075,72.1848) (45.3286,88.9448) (23.7802,96.7572) (0,100) 
4 (32.4416,0) (16.2208,25.7471) (0,51.4941) (18.7889,59.4572) (49.148,61.5429) (64.873,54.0422) (50.8337,27.0436) (38.9122,8.70801) (38.9122,39.1387) (38.9122,100) 
5 (9.19822,0) (9.19822,30.1312) (12.4968,59.4875) (38.4347,71.3641) (38.9719,91.3492) (11.0046,100) (0,85.0229) (3.28501,55.0713) (6.57003,25.1197) (50.3768,6.76067) 
6 (29.7727,0) (22.7729,17.7687) (15.7731,35.5374) (8.77329,53.3062) (1.77348,71.0749) (0,89.8359) (10.3436,100) (25.9275,93.8715) (37.296,79.3262) (26.534,64.6418) (7.44317,64.1258) 
7 (1.57891,0) (0.789448,21.0523) (0,42.1046) (3.01294,23.5513) (14.0903,11.8919) (35.1574,11.8919) (51.3689,16.2408) (49.0566,37.1806) (46.7442,58.1204) (42.1195,100) 
8 (33.044,12.0913) (11.5722,0) (4.52375,23.4723) (21.9496,44.6119) (39.3755,65.7515) (39.5806,91.3887) (16.8836,100) (0,85.4408) (7.75771,60.711) (25.2963,39.6647) (42.8348,18.6185) 
9 (35.0237,28.3361) (28.6908,9.33749) (17.2621,0) (0.669372,7.82418) (0,27.0194) (17.8331,32.6159) (35.2942,23.2151) (33.1488,40.8842) (29.5772,60.5895) (22.4341,100) 

学習データの読み込み

では次に,上記で作成された辞書を読み込むプログラムを追記します.今回の書式では一行がひとつの入力データになるため,ファイル編集を手動でおこなうことも 可能です.この機能を実装することで,ひと通りの機能を実現できます.上記で示したstrokes.txtを読み込み,1つの学習データで認識精度がどの程度のもの なのか,各自で確認してみます.なお,このデータは馬場が書いた字になっているので,それぞれの書き癖や違いによって,ご認識する可能性が高いと考えられます.

完成品

ScriptNumberRecognizerのプログラム

認識精度を高める

学習データを各自一つの文字に対して10程度入力し,自分の字については高精度の結果を返す識別器をまずは作成してみましょう.なお,正解率を算出する ための試行回数は10回とします.

  1. 0-9までの数字を1文字分辞書登録し,その精度を正解率にて確認
  2. 0-9までの数字を各文字について10文字分辞書登録し,その精度を正解率にて確認
  3. 0-9までの数字を皆のデータをまとめて登録し,その精度を正解率にて確認
700ストロークのデータ

この回では論文読解と合わせて実装を行うため,わざわざストロークの単純化を行いましたが,結局は最後にリサンプルしているので, 単純化せずにリサンプル処理に直接まわしても,さほど認識精度に影響を与えないかもしれません.是非試して確認してみてください. ここでのアルゴリズムを利用して,論文読解4で作ったセンシングシステムでストローク認識を行ってみるのも 面白いかもしれませんね.

手書きのタイマーをつくる

雛形

課題にあたり,下記仕様を満たすこと

  1. 分:0-99, 秒:0-59を手書きで設定できる
  2. 手書き入力認識時に任意の音を鳴らすこと
  3. スペースキーでカウントダウンのスタート,ポーズを切り替えられる
  4. アラームはスペースキーを押すと止まる(スペースキーを押さなければ鳴り続ける)
  5. 「☓」(ばつ)を入力すると,00:00になる
  6. Macのバンドルアプリケーションとして仕上げる.http://tetsuakibaba.jp/index.php?page=workshop/of/of_application_export を参照してください.


おわりに

本演習実習では,学部3年生のアートコース学生向けの授業となっています.数学的にそれ程複雑なことは行ってはいませんが,インタフェース・インタラクション デザインにおける重要な基礎技術をみにつける事を目標にしています.また,論文といった具体的な事例を通じ,研究者たちがどのように考え,どのように作り, どのように論文を書いているかを体現しました.プログラムに慣れていない学生にとっては大変な部分も多かったと思いますが,その分みのりのある内容に なったのでは無いかと思っています.

後期のインタラクションデザイン演習・実習では,ここまでの知識を利用して自分たちで考え・実装を行うことをします.ここまで一緒についてきてくれた 皆さんと一緒にまた,後期にお会いできるのを切望しています.是非また後期で!!

クラスの練習

ofApp.cpp
#include "ofApp.h"

shakingText::shakingText()
{
  x = y = 100;
  r = 255;
  g = 0;
  b = 0;
  shake = 5.0;
  str = 'a';
}
shakingText::~shakingText()
{
}

void shakingText::draw()
{
  ofSetColor(r,g,b);
  ofDrawBitmapString(str, x+ofRandom(shake), y+ofRandom(shake));
}
void shakingText::update()
{
  y = y + 1;
  if( y > ofGetHeight() ){
    y = 0;
  }
}

void shakingText::setup(string s,
                        int _x, int _y,
                        int _r, int _g, int _b,
                        float _shake)
{
  x = _x;
  y = _y;
  r = _r;
  g = _g;
  b = _b;
  shake = _shake;
  str = s;
}

//--------------------------------------------------------------
void ofApp::setup(){
  for( int i = 0; i < 100; i++ ){
    st[i].setup(ofToString((int)ofRandom(10)),
             ofRandom(ofGetWidth()), ofRandom(ofGetHeight()),
             255, 0, 0,
             ofRandom(5.0));
  }
}

//--------------------------------------------------------------
void ofApp::update(){
  for( int i = 0; i < 100; i++ ){
    st[i].update();
  }
}

//--------------------------------------------------------------
void ofApp::draw(){
  ofBackground(0);
  for( int i = 0; i < 100; i++ ){
    st[i].draw();
  }
}

//--------------------------------------------------------------
void ofApp::keyPressed(int key){

}

//--------------------------------------------------------------
void ofApp::keyReleased(int key){

}

//--------------------------------------------------------------
void ofApp::mouseMoved(int x, int y ){

}

//--------------------------------------------------------------
void ofApp::mouseDragged(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::mousePressed(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::mouseReleased(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::windowResized(int w, int h){

}

//--------------------------------------------------------------
void ofApp::gotMessage(ofMessage msg){

}

//--------------------------------------------------------------
void ofApp::dragEvent(ofDragInfo dragInfo){ 

}

ofApp.h
#pragma once

#include "ofMain.h"



class shakingText{
public:
  shakingText();  // Initialize
  ~shakingText(); // finalize
  int x;
  int y;
  int r,g,b;
  float shake;
  string str;
  void draw();
  void update();
  void setup(string s,
             int _x, int _y,
             int _r, int _g, int _b,
             float _shake);
private:
};

class ofApp : public ofBaseApp{
public:
		void setup();
		void update();
		void draw();
  
		void keyPressed(int key);
		void keyReleased(int key);
		void mouseMoved(int x, int y );
		void mouseDragged(int x, int y, int button);
		void mousePressed(int x, int y, int button);
		void mouseReleased(int x, int y, int button);
		void windowResized(int w, int h);
		void dragEvent(ofDragInfo dragInfo);
		void gotMessage(ofMessage msg);
  
  shakingText st[100];
};

int data[(16*2+1)*10] = {
  '0', 47,0,36,-11,22,-17,6,-21,-8,-24,-24,-23,-39,-17,-48,-4,-50,10,-37,18,-21,22,-5,22,10,21,25,16,39,9,49,-2,
  '1', 50,0,43,0,36,0,30,0,23,0,16,0,10,0,3,0,-3,0,-10,0,-16,0,-23,0,-30,0,-36,-1,-43,-1,-49,0,// 30
  '2', 54,0,48,11,41,22,30,29,18,26,6,21,-1,11,-6,-1,-9,-13,-12,-26,-14,-39,-18,-30,-24,-19,-30,-7,-37,3,-45,12, // 30
  '3', 58,0,50,10,39,18,25,17,17,6,14,-6,12,-10,4,0,-3,11,-14,20,-27,18,-36,8,-41,-4,-39,-18,-34,-31,-25,-41, // 30
  '4', 47,0,33,-10,18,-20,3,-30,-9,-36,-11,-18,-9,0,-6,16,-2,34,8,33,17,18,17,2,0,0,-17,1,-35,3,-52,7, //
  '5', 48,0,28,-7,7,-16,-10,-15,-17,5,-27,23,-45,11,-51,-8,-42,-27,-20,-28,0,-22,21,-14,37,-5,30,15,23,35,15,54,
  '6', 61,0,47,-4,33,-8,19,-12,6,-16,-8,-18,-22,-19,-35,-14,-38,0,-34,12,-24,23,-13,31,-1,26,3,13,4,0,1,-14,
  '7', 50,0,42,8,34,16,25,24,17,32,9,41,2,36,-1,25,-6,14,-10,3,-14,-7,-18,-18,-23,-28,-29,-39,-35,-49,-42,-58,
  '8', 43,0,49,-17,36,-29,17,-31,2,-22,1,-3,-1,15,-11,31,-29,34,-45,25,-50,7,-39,-7,-20,-10,-1,-4,15,3,33,10,
  '9', 30,0,35,-15,35,-32,25,-45,9,-44,-5,-36,-6,-23,4,-11,17,0,23,8,7,13,-6,20,-21,28,-36,37,-50,45,-63,54,
};
ofApp.cpp
#include "ofApp.h"

int data[(16*2+1)*10] = {
  '0', 47,0,36,-11,22,-17,6,-21,-8,-24,-24,-23,-39,-17,-48,-4,-50,10,-37,18,-21,22,-5,22,10,21,25,16,39,9,49,-2,
  '1', 50,0,43,0,36,0,30,0,23,0,16,0,10,0,3,0,-3,0,-10,0,-16,0,-23,0,-30,0,-36,-1,-43,-1,-49,0,// 30
  '2', 54,0,48,11,41,22,30,29,18,26,6,21,-1,11,-6,-1,-9,-13,-12,-26,-14,-39,-18,-30,-24,-19,-30,-7,-37,3,-45,12, // 30
  '3', 58,0,50,10,39,18,25,17,17,6,14,-6,12,-10,4,0,-3,11,-14,20,-27,18,-36,8,-41,-4,-39,-18,-34,-31,-25,-41, // 30
  '4', 47,0,33,-10,18,-20,3,-30,-9,-36,-11,-18,-9,0,-6,16,-2,34,8,33,17,18,17,2,0,0,-17,1,-35,3,-52,7, //
  '5', 48,0,28,-7,7,-16,-10,-15,-17,5,-27,23,-45,11,-51,-8,-42,-27,-20,-28,0,-22,21,-14,37,-5,30,15,23,35,15,54,
  '6', 61,0,47,-4,33,-8,19,-12,6,-16,-8,-18,-22,-19,-35,-14,-38,0,-34,12,-24,23,-13,31,-1,26,3,13,4,0,1,-14,
  '7', 50,0,42,8,34,16,25,24,17,32,9,41,2,36,-1,25,-6,14,-10,3,-14,-7,-18,-18,-23,-28,-29,-39,-35,-49,-42,-58,
  '8', 43,0,49,-17,36,-29,17,-31,2,-22,1,-3,-1,15,-11,31,-29,34,-45,25,-50,7,-39,-7,-20,-10,-1,-4,15,3,33,10,
  '9', 30,0,35,-15,35,-32,25,-45,9,-44,-5,-36,-6,-23,4,-11,17,0,23,8,7,13,-6,20,-21,28,-36,37,-50,45,-63,54,
};

//--------------------------------------------------------------
void ofApp::setup(){

}

//--------------------------------------------------------------
void ofApp::update(){

}

//--------------------------------------------------------------
void ofApp::draw(){
  ofSetColor(0,0,0);
  ofNoFill();
  stroke.draw();
  for( int i = 0; i < stroke.size(); i++ ){
    ofCircle(stroke.getVertices()[i], 2);
  }
  
  ofSetColor(255,0,0);
  if( stroke.size() > 0 ){
    ofCircle(stroke.getVertices()[0],2);
    ofCircle(average, 2);
    ofDrawBitmapString(ofToString(ofRadToDeg(theta)), average);
  }
  
  ofSetColor(0,255,0);
  stroke_theta.draw();
  
  ofSetColor(0,0,255);
  stroke_theta_mag.draw();
}

//--------------------------------------------------------------
void ofApp::keyPressed(int key){
  if( key == ' ' ){
    stroke.clear();
    stroke_theta.clear();
    stroke_theta_mag.clear();
  }
}

//--------------------------------------------------------------
void ofApp::keyReleased(int key){

}

//--------------------------------------------------------------
void ofApp::mouseMoved(int x, int y ){

}

//--------------------------------------------------------------
void ofApp::mouseDragged(int x, int y, int button){
  stroke.addVertex(x,y);
}

//--------------------------------------------------------------
void ofApp::mousePressed(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::mouseReleased(int x, int y, int button){
  
  // step 1:リサンプリング
  stroke = stroke.getResampledByCount(16);
  
  // step 2: 回転角度を計算
  average.set(0,0);
  for( int i = 0;i < stroke.size(); i++ ){
    average.x = average.x + stroke.getVertices()[i].x;
    average.y = average.y + stroke.getVertices()[i].y;
  }
  average.x = average.x/(float)stroke.size();
  average.y = average.y/(float)stroke.size();
  
  theta = atan2(average.x-stroke.getVertices()[0].x,
                average.y-stroke.getVertices()[0].y);
  
  theta = theta + 3.14/2;

  // 原点移動して,全頂点を回転
  for( int i = 0; i < stroke.size(); i++ ){
    float x,y;
    x = stroke.getVertices()[i].x - average.x;
    y = stroke.getVertices()[i].y - average.y;
    stroke_theta.addVertex(x*cos(theta)-y*sin(theta),
                           x*sin(theta)+y*cos(theta));
  }
  
  // 元の位置に戻す
  for( int i = 0; i < stroke.size(); i++ ){
    stroke_theta.getVertices()[i].x = stroke_theta.getVertices()[i].x + average.x;
    stroke_theta.getVertices()[i].y = stroke_theta.getVertices()[i].y + average.y;
  }

  // step 3:拡大縮小(Scaling)
  float mag;
  if( stroke_theta.getBoundingBox().getWidth() >= stroke_theta.getBoundingBox().getHeight() ){
    mag = 100/stroke_theta.getBoundingBox().getWidth();
  }
  else{
    mag = 100/stroke_theta.getBoundingBox().getHeight();
  }
  
  for( int i = 0; i < stroke.size(); i++ ){
    stroke_theta_mag.addVertex((stroke_theta.getVertices()[i].x-average.x)*mag,
                               (stroke_theta.getVertices()[i].y-average.y)*mag);
    stroke_theta_mag.getVertices()[i].x = stroke_theta_mag.getVertices()[i].x + average.x;
    stroke_theta_mag.getVertices()[i].y = stroke_theta_mag.getVertices()[i].y + average.y;
  }
  
  cout << stroke.size() << ": ";
  for( int i = 0; i < stroke.size(); i++ ){
    cout << (int)(stroke_theta_mag.getVertices()[i].x - average.x) << ",";
    cout << (int)(stroke_theta_mag.getVertices()[i].y - average.y) << ",";
  }
  cout << endl;
  
  ofPolyline stroke_input;
  for( int i = 0; i < stroke.size(); i++ ){
    stroke_input.addVertex(stroke_theta_mag.getVertices()[i].x - average.x,
                           stroke_theta_mag.getVertices()[i].y - average.y);
  }
  
  int data_input[32];
  int j = 0;
  for( int i = 0; i < 32; i=i+2){
    data_input[i] = stroke_input.getVertices()[j].x;
    data_input[i+1] = stroke_input.getVertices()[j].y;
    j++;
  }
  
  float sum[10];
  for( j = 0; j < 10; j++ ){
    sum[j] = 0;
    for( int i = 0; i < 32; i++ ){
      sum[j] = sum[j] + (data[i+1+j*33]-data_input[i])*(data[i+1+j*33]-data_input[i]);
    }
    sum[j] = sqrt(sum[j]);
  }
  
  for( int i = 0; i < 10; i++ ){
    cout << i << ": " << sum[i] << endl;
  }
  
}

//--------------------------------------------------------------
void ofApp::windowResized(int w, int h){
  
}

//--------------------------------------------------------------
void ofApp::gotMessage(ofMessage msg){
  
}

//--------------------------------------------------------------
void ofApp::dragEvent(ofDragInfo dragInfo){
  
}

ofApp.h
#pragma once

#include "ofMain.h"

class ofApp : public ofBaseApp{

	public:
		void setup();
		void update();
		void draw();

		void keyPressed(int key);
		void keyReleased(int key);
		void mouseMoved(int x, int y );
		void mouseDragged(int x, int y, int button);
		void mousePressed(int x, int y, int button);
		void mouseReleased(int x, int y, int button);
		void windowResized(int w, int h);
		void dragEvent(ofDragInfo dragInfo);
		void gotMessage(ofMessage msg);
  
  ofPolyline stroke;
  ofPolyline stroke_theta;
  ofPolyline stroke_theta_mag;
		ofPoint average;
  float theta;
};


Copyright (c) 2015 Tetsuaki BABA all rights reserved.