Playerと複数生成されるSpawnerの位置を取得

UnityでFlappy Birdを作ってみたが、すぐにPlayer(Flappy Bird)を自動制御したくなったので、まずはPlayerとSpawner(土管)の位置を取得してみた。


Flappy Bird Made with Unity

Spawnerの設定

Spawnerは土管で、1秒ごとにY軸をランダムにずらして生成し、Playerに向かわせている。 Spawnerが生成された際にタグを付けることで、Player側でSpawnerの位置を取得することができる。

 public GameObject wallPrefab;  // Spawnerとして土管のprefabを設定する
    public float interval;  // 1秒ごとに生成する
    private GameObject spawner;  // 生成されたSpawnerを格納する
    public string tag;  // Spawnerにタグを付ける

    // Use this for initialization
    IEnumerator Start () {
        while (true) {
            // 画面外の座標(12, 2)をベースにY軸を0.0~4.0だけランダムにずらしてSpawnerを生成するよう設定
            transform.position = new Vector2(transform.position.x, Random.Range(0.0f, 4.0f));
            // Spawnerを生成
            spawner = Instantiate(wallPrefab, transform.position, transform.rotation);
            // 生成したSpawnerにタグ付け!
            spawner.tag = "Wall";

            yield return new WaitForSeconds(interval);
        }
    }

なお、Spawnerに指定した土管prefabには1秒ごとに5ずつX軸マイナス方法へ向かうように設定してある。

Playerの設定

Playerはフレームごとに状態を観測することができる。 このスクリプトがPlayerのインスタンスになるので、Playerの位置は簡単に取得できるのだが、 Spawnerの位置は、上記のようにタグ付けをすることで、Playerからも取得することができるようになる。

    public float jumpPower;  // Playerは強さ5でジャンプ
    private int num_wall = 2;  // SpawnerはPlayerに最も近い2つだけ位置を取得

    // Update is called once per frame
    void Update () {
        // Initialize state
        // 観測する位置は以下の5つ
        // [player_pos_y, wall1_pos_x, wall1_pos_y, wall2_pos_x, wall2_pos_y]
        List<float> state = new List<float>();  // 位置を格納するリスト

        // Set player position x
        var pos_player = this.transform.position;  // Playerの位置
        state.Add(pos_player.y);  // リストにPlayerのY座標を追加

        // Set wall position x and y
        GameObject[] walls = GameObject.FindGameObjectsWithTag("Wall");  // タグから全Spawnerを取得
        foreach (var wall in walls) {  // 全Spawnerについて生成された順に
            var pos_wall = wall.gameObject.transform.position;  // Spawnerの位置を取得
            // SpawnerがPlayerより前にいて、かつSpawnerの数がリストに2つ未満の場合
            // If a wall is behind the player & the number of wall in state is not max
            if ((pos_player.x <= pos_wall.x + 1.0f) && (state.Count < 1+2*num_wall)) {  
                state.Add(pos_wall.x);  // SpanerのX座標を格納
                state.Add(pos_wall.y);  // SpanerのY座標を格納
            }
        }

        // debug
        // 取得したPlayerとSpanerの位置を出力
        string text = "";
        foreach (var s in state) {
            text = text + s.ToString() + " ";
        }
        print(text);

    }

位置の取得結果

Playerのスクリプトで設定したコンソールへの出力は以下のようになる。 数値の内容は[player_pos_y, wall1_pos_x, wall1_pos_y, wall2_pos_x, wall2_pos_y]。 SpawnerがX座標マイナス方向にずれていくのが観測できる。 PlayerのX座標は変わらないので、Y座標だけ取得している。

f:id:Shoto:20171103161032p:plain

ソースコード

SpawnerとPlayerの全ソースコードを以下に示しておく。

  • Spawner.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Spawner : MonoBehaviour {

    public GameObject wallPrefab;
    public float interval;
    private GameObject spawner;
    public string tag;

    // Use this for initialization
    IEnumerator Start () {
        while (true) {
            transform.position = new Vector2(transform.position.x, Random.Range(0.0f, 4.0f));
            spawner = Instantiate(wallPrefab, transform.position, transform.rotation);
            spawner.tag = "Wall";

            yield return new WaitForSeconds(interval);
        }
    }

    // Update is called once per frame
    void Update () {

    }
}
  • Player.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour {

    public float jumpPower;

    private int num_wall = 2;

    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () {
        // Initialize state
        // [player_pos_y, wall1_pos_x, wall1_pos_y, wall2_pos_x, wall2_pos_y]
        List<float> state = new List<float>();

        // Set player position x
        var pos_player = this.transform.position;
        state.Add(pos_player.y);

        // Set wall position x and y
        GameObject[] walls = GameObject.FindGameObjectsWithTag("Wall");
        foreach (var wall in walls) {
            var pos_wall = wall.gameObject.transform.position;
            // If a wall is behind the player & the number of wall in state is not max
            if ((pos_player.x <= pos_wall.x + 1.0f) && (state.Count < 1+2*num_wall)) {  
                state.Add(pos_wall.x);
                state.Add(pos_wall.y);
            }
        }

        // debug
        string text = "";
        foreach (var s in state) {
            text = text + s.ToString() + " ";
        }
        print(text);

        // Manual operation
        if (Input.GetButtonDown("Jump")) {
            GetComponent<Rigidbody2D>().velocity = new Vector2(0, jumpPower);
        }
    }

    /*
   void OnCollisionEnter2D (Collision2D other) {
       // Application.LoadLevel(Application.loadedLevel);  //old function
        UnityEngine.SceneManagement.SceneManager.LoadScene(UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex);    
   }
   */

    void OnCollisionEnter2D (Collision2D other) {
        Invoke("Restart", 1.0f);     
    }

    void Restart () {
        // Application.LoadLevel(Application.loadedLevel);  //old function
        UnityEngine.SceneManagement.SceneManager.LoadScene(UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex);   
    }

}

参考文献

GitHub Desktop for Windowsの"failed to sync this branch"の対処

GitHub Desktopを立ち上げた状態で別作業をしていたら、GitHub Desktop関連のエラーを示すポップアップが4つ連続で出てきた。 ポップアップを消した後も何度か出てきて、実際、GitHub Desktopで同期するとfailed to sync this branchと表示され、 GitHubと同期できなくなってしまった。

Shell操作で対処できている記事が幾つか見られたが、僕の環境ではまったく無意味だったため、 コンパネからGitHub Desktopの「アンインストールと変更」を行おうとしたところ、以下のようなウィンドウが出た。

f:id:Shoto:20170923222824p:plain

開いたときは、アプリケーションを以前の状態に復元します。が選択可能になっていたので、 選択して、OKボタンを押して、GitHub Desktopを再起動したら、うまく同期できるようになった。 復元後にGitHub Desktopの「アンインストールと変更」ボタンをクリックしたら、 アプリケーションを以前の状態に復元します。が選択できなくなっていた。 問題ある時のみ選択できるっぽい。

Windows10でMongoDBの設定

Windows10でMongoDBを使おうとしたら、少しつまづいたのでメモとして残しておく。

1. ダウンロードとインストール

MongoDB Download Centerから Windows Server 2008 R2 64-bit and later, with SSL support x64をダウンロードしてインストール。

2. パスを通す

デフォルトのパスC:\Program Files\MongoDB\Server\3.4\binをシステム環境のPathに登録。

3. ログとデータの保存場所を確保

僕の環境だとMongoDBをインストールしたCドライブが0.5TBしかないのに対して、Dドライブが1.8TBあるので、 ビッグデータに備えて、以下のようにDドライブにログとデータの保存場所を作成しておく。ここは各自の好みでOK。

D:\db\mongodb\log\
D:\db\mongodb\data\

4. MongoDBの設定ファイルの作成

MongoDBを起動するときは、毎回上記のパスを教える必要がある。 面倒なので、以下の内容の設定ファイルmongod.cfgを作成して、C:\Program Files\MongoDB\Server\3.4\bin\に置いておく。 拡張子は、cfgでもconfでもconfigでも何でもいいっぽい。なお、設定ファイルの置き場所も各自の好みでOK。

systemLog:
    destination: file
    path: D:\db\mongodb\log\mongod.log
storage:
    dbPath: D:\db\mongodb\data

5. 設定ファイルのWindowsサービス登録

上記で作成した設定ファイルをWindowsサービスに登録することで、Windowsと同時にMongoDBを起動できる。 コマンドプロンプトを管理者権限で立ち上げて、mongod --config "C:\Program Files\MongoDB\Server\3.4\bin\mongod.cfg" --installと入力して実行する。 しかし、以下のようなエラーが出る可能性がある。

PS C:\WINDOWS\system32> mongod --config "C:\Program Files\MongoDB\Server\3.4\bin\mongod.cfg" --install
Error parsing YAML config file: yaml-cpp: error at line 5, column 8: illegal map value
try 'C:\Program Files\MongoDB\Server\3.4\bin\mongod.exe --help' for more information

設定ファイルについて、以下の4つが守られていないことが原因と考えられる。

  • utf-8で保存
  • タブを使わない
  • systemLogとstorageの間を開けない
  • パスをダブルクォーテーションで囲まない

修正したら、以下の通り上手くいくはず。

PS C:\WINDOWS\system32> mongod --config "C:\Program Files\MongoDB\Server\3.4\bin\mongod.cfg" --install
PS C:\WINDOWS\system32>

6. Pythonからの操作

PythonからMongoDBを操作する場合は、pymongoを使えるようになるとよい。 以下の記事でCRUD操作の基礎を学べる。

testpy.hatenablog.com

参考文献

Swiftで写真を撮影して保存するiPhoneアプリを作成

iPhoneのカメラ機能を利用する入門的なアプリをサクッと作る。 フレームワークの各関数の使い方をよく忘れるのでメモ代わりに記録として残しておく。

画面

画面は1つのみで、プレビュー用のView(cameraView)とシャッター用のButton(take)の2つの部品だけを配置。 下図ではViewを正方形に配置しているが、実際に保存されるアスペクト比に近い形の方が良いとは思う。

f:id:Shoto:20170920011910p:plain

コード

以下のコードを実装して、iPhone繋いでBuild&Runして、takeボタン押せば、 プレビュー画面が写真としてカメラロールに保存される。 コードの解説については、コメントを参照。

import UIKit
import AVFoundation

class FirstViewController: UIViewController, AVCapturePhotoCaptureDelegate {
    // カメラ等のキャプチャに関連する入出力を管理するクラス
    var session: AVCaptureSession!
    // 写真データを取得するクラス
    var outout: AVCapturePhotoOutput?
    // カメラでキャプチャした映像をプレビューするクラス
    var previewLayer: AVCaptureVideoPreviewLayer?

    // カメラでキャプチャした映像をプレビューするエリア
    @IBOutlet weak var cameraView: UIView!

    // ロードされた直後に呼び出される(初回に一度のみ)
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    // 画面が表示される直前に呼び出される
    override func viewWillAppear(_ animated: Bool) {
        // キャプチャー入出力と写真データ取得のクラスの初期化
        session = AVCaptureSession()
        outout = AVCapturePhotoOutput()

        // 解像度の設定
        //session.sessionPreset = AVCaptureSessionPreset1920x1080
        session.sessionPreset = AVCaptureSessionPreset3840x2160

        // カメラの選択(背面・前面など)
        let camera = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)

        do {
            // 指定したデバイスをセッションに入力
            let input = try AVCaptureDeviceInput(device: camera)

            // 入力
            if (session.canAddInput(input)) {
                session.addInput(input)

                // 出力
                if (session.canAddOutput(outout)) {
                    session.addOutput(outout)
                    session.startRunning() // カメラ起動

                    // プレビューレイヤーを生成
                    previewLayer = AVCaptureVideoPreviewLayer(session: session)
                    // cameraViewの境界をプレビューレイヤーのフレームに設定
                    previewLayer?.frame = cameraView.bounds
                    // アスペクト比変更とレイヤー収納の有無
                    previewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill  // アスペクト比を変ない。レイヤーからはみ出した部分は隠す。
                    //previewLayer?.videoGravity = AVLayerVideoGravityResizeAspect  // アスペクト比を変えない。はみ出さないようにレイヤー内に収める。

                    // cameraViewのサブレイヤーにプレビューレイヤーを追加
                    cameraView.layer.addSublayer(previewLayer!)
                }
            }
        } catch {
            print(error)
        }
    }

    // ボタンをタップした時に呼ばれる
    @IBAction func takePhoto(_ sender: Any) {
        // 撮影設定
        let settingsForMonitoring = AVCapturePhotoSettings()
        settingsForMonitoring.flashMode = .auto  // フラッシュのモード
        settingsForMonitoring.isAutoStillImageStabilizationEnabled = true  // 手振れ補正
        settingsForMonitoring.isHighResolutionPhotoEnabled = false  // 最高解像度で撮影するか否か

        // シャッターを切る
        outout?.capturePhoto(with: settingsForMonitoring, delegate: self)
    }

    // AVCapturePhotoCaptureDelegateのデリゲート
    // カメラで撮影が完了した後呼ばれる。撮影データを加工したり、アルバムに保存したりする。
    func capture(_ captureOutput: AVCapturePhotoOutput,
                 didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?,
                 previewPhotoSampleBuffer: CMSampleBuffer?,
                 resolvedSettings: AVCaptureResolvedPhotoSettings,
                 bracketSettings: AVCaptureBracketedStillImageSettings?,
                 error: Error?) {

        if let photoSampleBuffer = photoSampleBuffer {
            // JPEG形式で画像データを取得
            let photoData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: photoSampleBuffer, previewPhotoSampleBuffer: previewPhotoSampleBuffer)
            // UIImage型に変換
            let image = UIImage(data: photoData!)

            // フォトライブラリに保存
            UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
        }
    }

    // 警告を受け取ったときに呼ばれる
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

トピックス

解像度の設定

// 解像度の設定
//session.sessionPreset = AVCaptureSessionPreset1920x1080
session.sessionPreset = AVCaptureSessionPreset3840x2160

もう少し写真の解像度を上げたいなどの場合は、ここで設定を行う。 指定できる解像度の種類は以下の通り。

Symbol Description
AVCaptureSessionPresetPhoto 高解像度の写真品質出力
AVCaptureSessionPresetHigh 高品質のビデオおよびオーディオ出力
AVCaptureSessionPresetMedium WiFi経由での共有出力ビデオおよびオーディオビットレート
AVCaptureSessionPresetLow 3G経由での共有出力ビデオおよびオーディオビットレート
AVCaptureSessionPreset320x240 320x240ピクセルビデオ出力
AVCaptureSessionPreset352x288 CIF画質(352x288ピクセル)ビデオ出力
AVCaptureSessionPreset640x480 VGA画質(640x480ピクセル)ビデオ出力
AVCaptureSessionPreset960x540 クオータHD品質(960x540ピクセル)ビデオ出力
AVCaptureSessionPreset1280x720 720p画質(1280x720ピクセル)のビデオ出力
AVCaptureSessionPreset1920x1080 1080p品質(1920x1080ピクセル)ビデオ出力
AVCaptureSessionPreset3840x2160 2160p(UHDまたは4Kとも呼ばれる)画質(3840x2160ピクセル)ビデオ出力
AVCaptureSessionPresetiFrame960x540 AACオーディオで960x540の高品質iFrame H.264ビデオを約30Mbits/secで実現する設定
AVCaptureSessionPresetiFrame1280x720 1280x720の高品質iFrame H.264ビデオをAACオーディオで約40 Mbits/secで実現する設定
AVCaptureSessionPresetInputPriority キャプチャセッションがオーディオおよびビデオ出力設定を制御しないことを指定

[iOS] AVFundationを使用して、「ビデオ録画」や「連写カメラ」や「QRコードリーダー」や「バーコードリーダー」を作ってみた - Developers.IO より

アスペクト比変更とレイヤー収納の有無

// アスペクト比変更とレイヤー収納の有無
previewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill  // アスペクト比を変ない。レイヤーからはみ出した部分は隠す。
//previewLayer?.videoGravity = AVLayerVideoGravityResizeAspect  // アスペクト比を変えない。はみ出さないようにレイヤー内に収める。

プレビューの変更を行う。上記の設定ではcameraViewに実際に撮影される写真の一部しかプレビューで表示されないが、 設定によっては全部表示させることも可能。以下の3種類がある。

Symbol Description
AVLayerVideoGravityResize アスペクト比を変えてレイヤーに収める。
AVLayerVideoGravityResizeAspect アスペクト比を変えない。はみ出さないようにレイヤー内に収める。
AVLayerVideoGravityResizeAspectFill アスペクト比を変ない。レイヤーからはみ出した部分は隠す。

AVCaptureVideoPreviewLayer の Video Gravity3種 - 木木木 より

参考文献

V-REPのLine TracerをPythonで制御する

急に3Dシミュレーションやりたい欲が出てきたのでV-REPというのを使ってみた。 V-REPを選択した理由は次の通り。

  • Pythonで制御できる
  • いろんなリアルロボットをシミュレートできる
  • 以下の記事があったのでハードルが低かった

chachay.hatenablog.com

この記事に沿って進めていくだけで、以下の動画のようにPythonでLine Tracerを制御できるようになる。 しかし、途中からLine Trace機能が効かなくなって、Line Tracerが落下するという結末が待っていた。

www.youtube.com

そこで、その原因を究明するために、ソースコードを少しだけちゃんと読んでみた。

ソースコード解析

前半はV-REPをPythonから制御するための前処理的なことなので飛ばして、まずは扱うUIとObjectの取得から。 次の処理では、メソッドの第2引数から分かるように、順にセンサーディスプレイ、左・中央・右センサー、左・右車輪と、1つのUIと5つのObjectを取得している。

# Get obejects
res, display           = vrep.simxGetUIHandle(clientID, "sensorDisplay", vrep.simx_opmode_blocking)
res, leftSensor        = vrep.simxGetObjectHandle(clientID, "LeftSensor", vrep.simx_opmode_blocking)
res, middleSensor      = vrep.simxGetObjectHandle(clientID, "MiddleSensor", vrep.simx_opmode_blocking)
res, rightSensor       = vrep.simxGetObjectHandle(clientID, "RightSensor", vrep.simx_opmode_blocking)
res, leftJointDynamic  = vrep.simxGetObjectHandle(clientID, "DynamicLeftJoint" , vrep.simx_opmode_blocking)  # Left wheel
res, rightJointDynamic = vrep.simxGetObjectHandle(clientID, "DynamicRightJoint", vrep.simx_opmode_blocking)  # Right wheel

最初の"sensorDisplay"とは、下図の左上にあるUI。 Left sensorが黒く、それ以外が黄色になっている。 これはLine Tracerの前方に付いている3つのセンサーを見ると分かる。 左センサーが黒いラインを捉えているため、UIにそれが反映されている。

f:id:Shoto:20170917235800p:plain

以下がその設定メソッドsetLeds()elHandleに上記で取得した"sensorDisplay"のObject displayを第1引数として渡す。 第2~4引数のleft, middle, rightは、TrueかFalseで、Falseがラインを捉えていることを示す。 最初にFalseで初期化して、Trueなら設定し直すという感じかな、たぶん。 setLeds()という名前にしているのは、sensorをLEDとしてUIで表現しているからだと思う。

# Update the sensor display.
def setLeds(elHandle, left, middle, right):
    # Initialize display at first.
    vrep.simxSetUIButtonProperty(clientID, elHandle, 8, vrep.sim_buttonproperty_staydown, vrep.simx_opmode_oneshot)
    vrep.simxSetUIButtonProperty(clientID, elHandle, 16, vrep.sim_buttonproperty_staydown, vrep.simx_opmode_oneshot)
    vrep.simxSetUIButtonProperty(clientID, elHandle, 24, vrep.sim_buttonproperty_staydown, vrep.simx_opmode_oneshot)

    # Set LEDs of the display if sensors don't catch the line.
    if left:
        vrep.simxSetUIButtonProperty(clientID, elHandle, 8, vrep.sim_buttonproperty_staydown+vrep.sim_buttonproperty_isdown, vrep.simx_opmode_oneshot)
    if middle:
        vrep.simxSetUIButtonProperty(clientID, elHandle, 16, vrep.sim_buttonproperty_staydown+vrep.sim_buttonproperty_isdown, vrep.simx_opmode_oneshot)
    if right:
        vrep.simxSetUIButtonProperty(clientID, elHandle, 24, vrep.sim_buttonproperty_staydown+vrep.sim_buttonproperty_isdown, vrep.simx_opmode_oneshot)

続いて、本処理に入る前にセンサー情報を格納するlistを初期化。

# Initialize sensors
sensorReading = [False, False, False]  # Left, Middle, Right
sensorReading[0] = (vrep.simxReadVisionSensor(clientID, leftSensor, vrep.simx_opmode_streaming) == 1)
sensorReading[1] = (vrep.simxReadVisionSensor(clientID, middleSensor, vrep.simx_opmode_streaming) == 1)
sensorReading[2] = (vrep.simxReadVisionSensor(clientID, rightSensor, vrep.simx_opmode_streaming) == 1)

そして本処理だが、ここがLine Tracerが落下した原因の一つ。 whileの行に50秒以内であれば制御する書いてある。 確かに観測してるとゲートを通る直前ぐらいで50秒が過ぎている。 このときにPythonの処理が終わって、その時の左右の車輪の速度が同じでかつ前進しているから、そのままLine Tracerが落下すると思われる。 なので、とりあえずは50を∞に近い数字すれば半永久的にLine Traceしてくれるはず。
なお後に続くのは、センサー情報を取得して上記のsetLeds()でディスプレイにしている処理。

while time.time() - startTime < 50:  # Set near ∞ if you want!!!!!!!!!!
    # Try to retrieve the streamed data.
    returnCode, data = vrep.simxGetIntegerParameter(clientID,vrep.sim_intparam_mouse_x,vrep.simx_opmode_buffer)

    # Read the sensors
    sensorReading[0] = (vrep.simxReadVisionSensor(clientID, leftSensor, vrep.simx_opmode_buffer)[1])
    sensorReading[1] = (vrep.simxReadVisionSensor(clientID, middleSensor, vrep.simx_opmode_buffer)[1])
    sensorReading[2] = (vrep.simxReadVisionSensor(clientID, rightSensor, vrep.simx_opmode_buffer)[1])

    # Update the sensor display
    setLeds(display, sensorReading[0], sensorReading[1], sensorReading[2])

最後はLine Tracerの制御。 まずはまっすぐ進むように左右の車輪を同じ速度に設定。 その後、左センサーがラインを捉えていたら左に少し曲がるように設定。そのまた逆も然り。 そして、その設定コマンドをLine Tracerに送る。

   # Decide about both left and right velocities.
    s = 1.0
    linearVelocityLeft  = nominalLinearVelocity*s
    linearVelocityRight = nominalLinearVelocity*s

    # Turn left a little if the left sensor catches line(False), and vice versa.
    if not sensorReading[0]:
        linearVelocityLeft  = linearVelocityLeft*0.3
    if not sensorReading[2]:
        linearVelocityRight = linearVelocityRight*0.3

    # Update both left and right velocities.
    vrep.simxSetJointTargetVelocity(clientID, leftJointDynamic, linearVelocityLeft/(s*wheelRadius), vrep.simx_opmode_oneshot)
    vrep.simxSetJointTargetVelocity(clientID, rightJointDynamic, linearVelocityRight/(s*wheelRadius), vrep.simx_opmode_oneshot)

    time.sleep(0.005)

上でwhileの処理時間を∞に近い数字すればいいと言ったが、そんなことしなくてもシンプルに処理が終わったらLine Tracerを止めればいい、という考えもある。そのためには、与える速度を左右の車輪共に0.0にして、かつ第4引数をvrep.simx_opmode_blockingにすればOK。ちなみに、vrep.simx_opmode_oneshotは前のコマンドと同じものを返すみたい?なので、速度を0.0にしても、そのまま落下する結末を免れることができなかった。詳しくは参考文献を見て下さい。

# Stop the line tracer.
vrep.simxSetJointTargetVelocity(clientID, leftJointDynamic, 0.0, vrep.simx_opmode_blocking)
vrep.simxSetJointTargetVelocity(clientID, rightJointDynamic, 0.0, vrep.simx_opmode_blocking)

以上。

ソースコード

# -*- coding: utf-8 -*-
try:
    import vrep
except:
    print ('--------------------------------------------------------------')
    print ('"vrep.py" could not be imported. This means very probably that')
    print ('either "vrep.py" or the remoteApi library could not be found.')
    print ('Make sure both are in the same folder as this file,')
    print ('or appropriately adjust the file "vrep.py"')
    print ('--------------------------------------------------------------')
    print ('')

import time
import sys
import ctypes

print ('Program started')
vrep.simxFinish(-1)
clientID = vrep.simxStart('127.0.0.1', 19999, True, True, 5000, 5)

if clientID != -1:
    print ('Connected to remote API server')
else:
    print ('Failed connecting to remote API server')
    sys.exit('Program Ended')
    
nominalLinearVelocity = 0.3
wheelRadius           = 0.027
interWheelDistance    = 0.119
    
res, objs = vrep.simxGetObjects(clientID, vrep.sim_handle_all, vrep.simx_opmode_blocking)
if res == vrep.simx_return_ok:
    print('Number of objects in the scene: ', len(objs))
else:
    print('Remote API function call returned with error code: ',res)

time.sleep(2)

startTime = time.time()
vrep.simxGetIntegerParameter(clientID, vrep.sim_intparam_mouse_x, vrep.simx_opmode_streaming)

# Get obejects.
res, display           = vrep.simxGetUIHandle(clientID, "sensorDisplay", vrep.simx_opmode_blocking)
res, leftSensor        = vrep.simxGetObjectHandle(clientID, "LeftSensor", vrep.simx_opmode_blocking)
res, middleSensor      = vrep.simxGetObjectHandle(clientID, "MiddleSensor", vrep.simx_opmode_blocking)
res, rightSensor       = vrep.simxGetObjectHandle(clientID, "RightSensor", vrep.simx_opmode_blocking)
res, leftJointDynamic  = vrep.simxGetObjectHandle(clientID, "DynamicLeftJoint" , vrep.simx_opmode_blocking)  # Left wheel
res, rightJointDynamic = vrep.simxGetObjectHandle(clientID, "DynamicRightJoint", vrep.simx_opmode_blocking)  # Right wheel

if res != vrep.simx_return_ok:
    print('Failed to get sensor Handler')
    vrep.simxFinish(clientID)
    sys.exit('Program ended')

# Update the sensor display.
def setLeds(elHandle, left, middle, right):
    # Initialize display at first.
    vrep.simxSetUIButtonProperty(clientID, elHandle, 8, vrep.sim_buttonproperty_staydown, vrep.simx_opmode_oneshot)
    vrep.simxSetUIButtonProperty(clientID, elHandle, 16, vrep.sim_buttonproperty_staydown, vrep.simx_opmode_oneshot)
    vrep.simxSetUIButtonProperty(clientID, elHandle, 24, vrep.sim_buttonproperty_staydown, vrep.simx_opmode_oneshot)
    
    # Set LEDs of the display if sensors don't catch the line.
    if left:
        vrep.simxSetUIButtonProperty(clientID, elHandle, 8, vrep.sim_buttonproperty_staydown+vrep.sim_buttonproperty_isdown, vrep.simx_opmode_oneshot)
    if middle:
        vrep.simxSetUIButtonProperty(clientID, elHandle, 16, vrep.sim_buttonproperty_staydown+vrep.sim_buttonproperty_isdown, vrep.simx_opmode_oneshot)
    if right:
        vrep.simxSetUIButtonProperty(clientID, elHandle, 24, vrep.sim_buttonproperty_staydown+vrep.sim_buttonproperty_isdown, vrep.simx_opmode_oneshot)

# Initialize sensors
sensorReading = [False, False, False]  # Left, Middle, Right
sensorReading[0] = (vrep.simxReadVisionSensor(clientID, leftSensor, vrep.simx_opmode_streaming) == 1)
sensorReading[1] = (vrep.simxReadVisionSensor(clientID, middleSensor, vrep.simx_opmode_streaming) == 1)
sensorReading[2] = (vrep.simxReadVisionSensor(clientID, rightSensor, vrep.simx_opmode_streaming) == 1)

while time.time() - startTime < 50:
    # Try to retrieve the streamed data.
    returnCode, data = vrep.simxGetIntegerParameter(clientID,vrep.sim_intparam_mouse_x,vrep.simx_opmode_buffer)
    
    # Read the sensors
    sensorReading[0] = (vrep.simxReadVisionSensor(clientID, leftSensor, vrep.simx_opmode_buffer)[1])
    sensorReading[1] = (vrep.simxReadVisionSensor(clientID, middleSensor, vrep.simx_opmode_buffer)[1])
    sensorReading[2] = (vrep.simxReadVisionSensor(clientID, rightSensor, vrep.simx_opmode_buffer)[1])
    
    # Update the sensor display
    setLeds(display, sensorReading[0], sensorReading[1], sensorReading[2])

    # Decide about both left and right velocities.
    s = 1.0
    linearVelocityLeft  = nominalLinearVelocity*s
    linearVelocityRight = nominalLinearVelocity*s

    # Trun left a little if the left sensor catches line(False), and vice versa. 
    if not sensorReading[0]:
        linearVelocityLeft  = linearVelocityLeft*0.3
    if not sensorReading[2]:
        linearVelocityRight = linearVelocityRight*0.3

    # Update both left and right velocities.
    vrep.simxSetJointTargetVelocity(clientID, leftJointDynamic, linearVelocityLeft/(s*wheelRadius), vrep.simx_opmode_oneshot)
    vrep.simxSetJointTargetVelocity(clientID, rightJointDynamic, linearVelocityRight/(s*wheelRadius), vrep.simx_opmode_oneshot)
    
    time.sleep(0.005)

# Stop the line tracer.
vrep.simxSetJointTargetVelocity(clientID, leftJointDynamic, 0.0, vrep.simx_opmode_blocking)
vrep.simxSetJointTargetVelocity(clientID, rightJointDynamic, 0.0, vrep.simx_opmode_blocking)

参考文献

複数画像をKerasのVGG16で特徴抽出してk-means++でクラスタリング

VGG16, VGG19, ResNet50, InceptionV3など、 ImageNetで学習済みのモデルがKerasで使える。 物体認識だけでなく特徴抽出にも使えるので、 複数画像をVGG16で特徴抽出して、これをk-means++でクラスタリングしてみた。 なお複数画像は、ハワイで撮影したフラダンスの動画をフレーム分割して用意した。 以下に説明するコードは、ここ に置いておく。

ディレクトリー構成

ディレクトリー構成は以下の通り。 src/image_clustering.py が実行ファイルとなる。 data/video/クラスタリングしたい動画を置く。 data/images/targrt/ に動画をフレーム分割した画像が保存される。 data/images/clustered/ に分類済みの画像が保存される。 なお、動画でなくても、クラスタリングしたい画像を data/images/targrt/ に置くこともできる。 なお、ここ には src/image_clustering.py しかないので、 data/video/data/images/ フォルダーを追加して使ってください。

image_clustering
├─data
│  ├─images
│  │  ├─clustered
│  │  └─target
│  └─video
└─src
    └─image_clustering.py

初期設定

グローバル変数は固定。 __init__()の引数に、動画の場合は video_file にファイル名を指定する。 画像の場合は、input_video をFalseに変更する。

DATA_DIR = '../data/'
VIDEOS_DIR = '../data/video/'                        # The place to put the video
TARGET_IMAGES_DIR = '../data/images/target/'         # The place to put the images which you want to execute clustering
CLUSTERED_IMAGES_DIR = '../data/images/clustered/'   # The place to put the images which are clustered
IMAGE_LABEL_FILE ='image_label.csv'                  # Image name and its label

class Image_Clustering:
    def __init__(self, n_clusters=50, video_file='IMG_2140.MOV', image_file_temp='img_%s.png', input_video=True):
        self.n_clusters = n_clusters            # The number of cluster
        self.video_file = video_file            # Input video file name
        self.image_file_temp = image_file_temp  # Image file name template
        self.input_video = input_video          # If input data is a video

メイン関数

以下の順で処理を行う。1.は動画の場合のみ。

  1. 動画のフレーム分割
  2. 画像のラベリング(特徴抽出とクラスタリング
  3. 画像の分類(ラベルごとにディレクトリーにまとめる)
def main(self):
    if self.input_video == True:
        self.video_2_frames()
    self.label_images()
    self.classify_images()

動画のフレーム分割

OpenCVでフレーム分割して、data/images/targrt/ に保存する。

def video_2_frames(self):
    print('Video to frames...')
    cap = cv2.VideoCapture(VIDEOS_DIR+self.video_file)

    # Remove and make a directory.
    if os.path.exists(TARGET_IMAGES_DIR):
        shutil.rmtree(TARGET_IMAGES_DIR)  # Delete an entire directory tree
    if not os.path.exists(TARGET_IMAGES_DIR):
        os.makedirs(TARGET_IMAGES_DIR)  # Make a directory

    i = 0
    while(cap.isOpened()):
        flag, frame = cap.read()  # Capture frame-by-frame
        if flag == False:
            break  # A frame is not left
        cv2.imwrite(TARGET_IMAGES_DIR+self.image_file_temp % str(i).zfill(6), frame)  # Save a frame
        i += 1
        print('Save', TARGET_IMAGES_DIR+self.image_file_temp % str(i).zfill(6))

    cap.release()  # When everything done, release the capture
    print('')

実行すると、次々と画像が保存されていく。 フラダンスの動画は17秒だが、フレーム数は530枚となっている。

> python .\image_clustering.py
Video to frames...
Save ../data/images/target/img_000001.png
Save ../data/images/target/img_000002.png
Save ../data/images/target/img_000003.png
...
Save ../data/images/target/img_000530.png

画像のラベリング(特徴抽出とクラスタリング

モデルにはVGG16を使用。 include_top=Falseとすることで、特徴抽出が可能になる。 動画を分割したフレームはpngで保存しているが、画像はjpgも対象。 jpegとかも対象にしたい場合は、この辺を書き換えてください。 後述する__feature_extraction()にVGG16のモデルと画像を渡して特徴を取得する。 全画像の特徴をk-means++でクラスタリングを行い、ラベルを取得して画像とセットにして、pandasでCSVに保存する。

def label_images(self):
    print('Label images...')

    # Load a model
    model = VGG16(weights='imagenet', include_top=False)

    # Get images
    images = [f for f in os.listdir(TARGET_IMAGES_DIR) if f[-4:] in ['.png', '.jpg']]
    assert(len(images)>0)

    X = []
    pb = ProgressBar(max_value=len(images))
    for i in range(len(images)):
        # Extract image features
        feat = self.__feature_extraction(model, TARGET_IMAGES_DIR+images[i])
        X.append(feat)
        pb.update(i)  # Update progressbar

    # Clutering images by k-means++
    X = np.array(X)
    kmeans = KMeans(n_clusters=self.n_clusters, random_state=0).fit(X)
    print('')
    print('labels:')
    print(kmeans.labels_)
    print('')

    # Merge images and labels
    df = pd.DataFrame({'image': images, 'label': kmeans.labels_})
    df.to_csv(DATA_DIR+IMAGE_LABEL_FILE, index=False)

__feature_extraction() の中身は、 Applications - Keras Documentation に書かれてある通り。 リサイズして、4次元データにして、zero-centeringしてから特徴を取得。 ただし、特徴は (1, 7, 7, 512) の4次元で返ってくるので、 flatten()(25088,) の1次元に変換する。

def __feature_extraction(self, model, img_path):
    img = image.load_img(img_path, target_size=(224, 224))  # resize
    x = image.img_to_array(img)
    x = np.expand_dims(x, axis=0)  # add a dimention of samples
    x = preprocess_input(x)  # RGB 2 BGR and zero-centering by mean pixel based on the position of channels

    feat = model.predict(x)  # Get image features
    feat = feat.flatten()  # Convert 3-dimentional matrix to (1, n) array

    return feat

実行した結果、画像530枚でも、VGG16による特徴抽出は24秒で終わった。なお、GPUGeForce 1080 x1。 k-means++で50クラスタに分類しているが、だいたい連続するフレームが同じラベルになっていることが分かる。 しかし、ラベル7などフレームが飛んでいても同じラベルになっているものもある。

Label images...
 99% (527 of 530) |################################## | Elapsed Time: 0:00:24 ETA: 0:00:00
labels:
[28 28 28 28 28 28 28 28 28  3  3  3  3  3  3  3  3  3  3  3  3  3  3  3 16
 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 26 26 26 26 26 26 26 26 26
 26 26 26 26 26 26 26 26 26 11 11 11 11 11 11 11 11 11 32 32 32 32 32 32 32
 32 32 32 32 27 27 27 27  9  9  9  9  9  9  9  9  9  9  9  9  9  9  9  9  9
  9  9  9  9  9 37 37 37 37 37 37 37 37 37 37 37 37 37 37 21 21 21 21 21 21
 21 21 21 21 21 21 21 21 21 21 21 29 29 29 29 29 29 29 29 29 29 29 29 29  1
  1  1  1  1 42 42 42 42 42 42 45 45 45 45 45 45 45 45 45 45 45  4  4  4  4
  4  4  4  4  4  4  4  4  4  4  4  4  4 33 33 33 33 33 33 33 33 33 33 33 33
 33 33 33 35 35 35 35 35 35 35 35 35 35 35 35 19 19 19 19 19 19 41 41 41 41
 41 41 41 49 49 49 49 49 49 49 13 13 13 13 13 13 44 44 44 44 44 44 44 44 44
 44 44  5  5 17 17 17 17 17 17 17 17 17 17 13 13  5  5 34 34 34 34 34 34 34
 34 34 34  5  5  5  5  5  5 25 25 25 25 25 25 25 25 25 15 15 15 15 15 15 31
 31 31 31 31 48 48 48 48 48 48 40 40 40 40 40 40 40 20 20 20 20 20 20 20 39
 39 39 39 39 39 39 39  2  2  2  2  2  2  2  2  2  2  2  2  2  2 18 18 18 18
 18 18 18 18 18 18 18 18 18 46 46 46 46 46 46 46 46 46 47 47 47 47 47 47 47
 47 47 22 22 22 22 22 22  6  6  6  6  6  6  6  6  6  6 43 43 43 43 43 43 43
 43 43 43 30 30 30 30 30 30 30 30 30 30  7  7  7  7  7  7  7  7  7  7  7  7
  7  7  7 38 38 38 38 38 38 38 38 38 38 38 38 38 38 38  7  7  7 10 10 10 10
 10 10 10 10 10 24 24 24 24 24 24 24 24 24 14 14  0  0  0  0  0  0  0  0 14
 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 36 36 36 36 36 36 36
 36 36 36 36 36 12 12 12 12 12 12 12 12  8  8  8  8  8  8  8  8 23 23 23 23
 23 23 23 23 23]

CSVに保存したデータは以下の通り。

image,label
img_000000.png,28
img_000001.png,28
img_000002.png,28
...
img_000009.png,3
img_000010.png,3
img_000011.png,3
...
img_000024.png,16
img_000025.png,16
img_000026.png,16
...

画像の分類(ラベルごとにディレクトリーにまとめる)

CSVファイルを読み込んで、ラベルの数だけ data/images/clustered/ にラベル名のディレクトリーを作成し、 data/images/target/ から画像をラベルごとにコピペする。

def classify_images(self):
    print('Classify images...')

    # Get labels and images
    df = pd.read_csv(DATA_DIR+IMAGE_LABEL_FILE)
    labels = list(set(df['label'].values))

    # Delete images which were clustered before
    if os.path.exists(CLUSTERED_IMAGES_DIR):
        shutil.rmtree(CLUSTERED_IMAGES_DIR)

    for label in labels:
        print('Copy and paste label %s images.' % label)

        # Make directories named each label
        new_dir = CLUSTERED_IMAGES_DIR + str(label) + '/'
        if not os.path.exists(new_dir):
            os.makedirs(new_dir)

        # Copy images to the directories
        clustered_images = df[df['label']==label]['image'].values
        for ci in clustered_images:
            src = TARGET_IMAGES_DIR + ci
            dst = CLUSTERED_IMAGES_DIR + str(label) + '/' + ci
            shutil.copyfile(src, dst)

以下が実行ログ。

Classify images...
Copy and paste label 0 images.
Copy and paste label 1 images.
Copy and paste label 2 images.
...
Copy and paste label 49 images.

分類結果

先ほどのラベル7の画像を見てみると、左手を頭より上に、右手を胸の高さに上げている画像がまとめられている。 画像のindexは427から443に富んでいるが、どちらも同じポーズをしている。

f:id:Shoto:20170722193736p:plain

またラベル45の画像を見てみると、両手を胸の高さに上げて、海の方を向いている画像がまとめられている。

f:id:Shoto:20170722193749p:plain

感想

今回、動画からフレーム分割した画像ということもあって、特徴がほとんど変わらない画像が対象となってはいるが、 上手くクラスタリングできている。今後はなるべく似ていない画像で試してみようと思う。 あとできれば、k-means++を使わずにCNNに閉じた方法があれば試してみたい。

参考文献

listやarrayからPandas DataFrameを作成

カラム名をkey、listやarrayをvalueにして辞書を作成して、DataFrameに突っ込むとできる。

import numpy as np
import pandas as pd

def make_df_from_list_and_array():
    col1 = [0,1,2,3,4]
    col2 = np.array([5,6,7,8,9])

    df = pd.DataFrame({'col_list':col1, 'col_array':col2})
    print(df)

コードはここ

>>> import test
>>> test.make_df_from_list_and_array()
   col_array  col_list
0          5         0
1          6         1
2          7         2
3          8         3
4          9         4