読者です 読者をやめる 読者になる 読者になる

俺の備忘録

個人的な備忘録です。

Raspberry Piで多脚ロボットを試作してみた

やったこと

Raspberry Piサーボモータを12個使って多脚ロボット(6脚)を試作してみた。

なぜ試作かというと、、、、
- 本気で作る場合、CADで製図して金属パーツ発注が必要そう(お金かかるし、CADなんて10年近く触ってない。)
- サーボモータは高いので、まずは最安レベルのサーボで感触掴んで見たほうが良さそう。
ということで、手持ちのパーツ(タミヤのユニバーサルプレートやサーボドライバ)の活用と、やっすいサーボモータの買い増しだけで、試作してみることにした。

ハード構成

こんな感じでタミヤのユニバーサルプレートLをベースに脚を左右それぞれ3本ずつ設置。
f:id:magayengineer:20160814183148j:plain

脚1本あたり、サーボモータNO4 Turnigy TG9z 9g / 1.7kg / 0.12sec マイクロサーボ)を2個使用。1つは脚の前後への移動、もう一つは脚の上下動をコントロールサーボモータのマウンターと脚本体は、100均で売っている2mmのプラ板を加工して作成。

f:id:magayengineer:20160814183221j:plain

サーボモータの制御には定番の PCA9685を使用。1つのPCA9685でサーボモータが16個まで制御可能。

f:id:magayengineer:20160814183254j:plain

Raspberry Pi + Pythonで使用する場合は、githubにあるドライバを導入すれば、簡単に使用することができる。導入もpipを使えばコマンド一発。

pip install adafruit-pca9685

電源は5V 2.1Aのモバイルバッテリー(ラブパイ用)と、エネループ4本(サーボ用)。 本当は、モバイルバッテリーにもう一つ付いている5V 1Aからとった電源(画面中央)を使いたかったが どうもモバイルバッテリーに保護回路が付いているようで、サーボを複数台同時に動かして1Aを超える電流が流れそうになると、2.1Aの口を使っているラズパイ含めて落っこちるので、サーボには別途専用の電源を用意した。少くとも今回の範囲だとエネループ4本でなんとかなった。 ちなみにテスターも持っていないし、使用したサーボモータは怪しい中国製でデータシートが存在していないので、正確にどの程度の電流が必要となったかは分からない。

f:id:magayengineer:20160814183322j:plain

脚の制御

画像のように2つのグループに分けて3本ずつ制御する。 つねにAかBのいずれかのグループが接地するように動かせば転倒しない。 で、具体的には以下のようにで脚を動かせばよい。

  1. Aの脚を振り上げて地面から離す
  2. Aの脚を前に進める
  3. Aの脚を下ろして接地させる
  4. Bの脚を振り上げて地面から離す
  5. Bの脚を前進める
  6. Aの脚で地面を蹴ってボディを前に進める
  7. Bの脚を下ろして接地させる
  8. Aの脚を振り上げて地面から離す
  9. Aの脚を前に進める
  10. Bの脚で地面を蹴ってボディを前に進める
  11. Aの脚を下ろして接地する
  12. 1に戻って繰り返し

f:id:magayengineer:20160814183402j:plain

動かして見た結果は?

一応、前に進みはしたものの、途中で潰れてしまった。(まぁ予想どおりではある)

理由としては、
- ボディが重すぎ。特にモバイルバッテリーが300g近い。
- サーボモータが力不足。
- 足回りのパーツの剛性が低く、重みや負荷で弾性変形するため、バランスを崩す。

妻には「生まれたばかりの子鹿みたい」と言われてしまった。

なお、モバイルバッテリーをボディから取り外して、手で持ってやると、やはりサーボの力不足や剛性不足を感じましたが、潰れずに進むことができた。

ただし、適当に書いた制御プログラムでも、前に進むことができ、動作もそれっぽくなるのが分かったのは収穫。

 今回の試作で分かった課題と対策のまとめ

サーボモータのパワー不足対策

今回使用したサーボは、1つ440円で格安だがトルクが1.5kg/cmでかなり貧弱。 感覚値ではあるが、5.0kg/cm以上あればなんとかなりそうな感じ。 5.0kg/cm程度のものは1000円前後で入手可能(12個だと12000円か...)

ボディの剛性対策

普通に考えれば1だが、2とか3で頑張るしかないかなぁ...
1. CADで製図して、金属パーツ発注。
2. プラ板を数枚貼り重ねてパーツを作る。
3. 硬い木材を加工してパーツを作る。

車体の軽量化

ラズパイ3を使用する時点で、重いモバイルバッテリーを使う必要がある。 そのため、ラズパイでの制御を諦め、
3.3V Arduino + 単三電池2本 + 昇圧コンバータ
とかにすればかなり軽量化できそう。

まぁ本製作するにしても、冬のボーナスが入ってからですかね...まだ8月だよ...

Raspberry PiでWifiラジコンを作ってPS3のコントローラで操作してみた(タンク編)

やったこと

やったことは前回(Raspberry PiでWifiラジコンを作ってPS3のコントローラで操作してみた - 俺の備忘録) とほとんど同様だが、今回は車体をタンクにしてみた。
キャタピラは男のロマン

f:id:magayengineer:20160728233823j:plain

システム構成

システム構成も前回(Raspberry PiでWifiラジコンを作ってPS3のコントローラで操作してみた - 俺の備忘録)とほぼ同一。 - ステアリング操作用に使っていたサーボは今回カメラ台制御に流用。 - 左右キャタピラはで別のモータで制御する。

f:id:magayengineer:20160728233859j:plain

用意したもの

これも大部分は前回(Raspberry PiでWifiラジコンを作ってPS3のコントローラで操作してみた - 俺の備忘録)と同様。

Raspberry Pi 3本体 Raspberry Pi3 Model B ボード&ケースセット (Element14版, Clear)-Physical Computing Lab

②モバイルバッテリー Raspberry Piとモータへの電源供給用として購入。

5V 2.1AでRapberry Piに電源供給し、 5V 1 AでDCモータ・サーボモータに電源供給する。
出力が2口あるため、これだけで電源がまかなえる。

(パワーアド)Poweradd Pilot 2GS 10000mAhモバイルバッテリー 急速充電 2USBポート iPhone6 / iPhone6s / iPhone5 / iPad / Xperia / Nexus等対応(シルバー)

③ブレッドボード
サンハヤト SAD-101 ニューブレッドボード サンハヤト SAD-101 ニューブレッドボード

④USBカメラ(マイク付き)
ELECOM WEBカメラ 200万画素 1/5インチCMOSセンサ マイク内蔵 コンパクトタイプ ブラック UCAM-C0220FBBK ELECOM WEBカメラ 200万画素 1/5インチCMOSセンサ マイク内蔵 コンパクトタイプ ブラック UCAM-C0220FBBK

サーボモータ・モータドライバ
アマゾンで売っているキットについてたものを利用。
アマゾンのレビューはイマイチだが、パーツを個々に注文しなくて良いので、入門用にある程度部品をまとめて揃えたい人には便利。

Raspberry Piで学ぶ電子工作 専用 実験キット 基本部品セット スターターパック (電子部品関連) Raspberry Piで学ぶ電子工作 専用 実験キット 基本部品セット スターターパック (電子部品関連)

⑦ユニバーサルプレートL
車体として使用。
タミヤ 楽しい工作シリーズ No.172 ユニバーサルプレートL 210×160mm (70172) タミヤ 楽しい工作シリーズ No.172 ユニバーサルプレートL 210×160mm (70172)

⑧シンプルギアボックス × 2
左右のキャタピラでひとつずつ。
楽しい工作シリーズ No.167 シングルギヤボックス 4速タイプ (70167) 楽しい工作シリーズ No.167 シングルギヤボックス 4速タイプ (70167)

⑨キャタピラ × 2
楽しい工作シリーズ No.100 トラック&ホイールセット (70100) 楽しい工作シリーズ No.100 トラック&ホイールセット (70100)

⑩ユニバーサルアーム
キャタピラのシャフトの固定に使用。

楽しい工作シリーズ No.143 ユニバーサルアームセット (70143) 楽しい工作シリーズ No.143 ユニバーサルアームセット (70143)

ホムセンで買ったステーっぽい何か × 4個
キャタピラシャフトの固定に利用。

f:id:magayengineer:20160716220451j:plain

⑫その他(家にあったもの) - USBケーブル
- SDカード
- はんだごて
- 太さ3mmのネジとボルト
- ピンドリル(3mm)
- 両面テープ
- ジャンパーピン(オス・オス、オス・メス)

車体組み立て

ポイントだけ説明。

ユニバーサルプレートにギアボックス×2とステーを取り付け、 キャタピラのシャフトを固定する。

f:id:magayengineer:20160728233940j:plain

横から見るとこんな感じ。足回りパーツは全てユニバーサルプレートの片面に寄せることで、もう片面にブレッドボードやラズパイ本体が余裕を持って搭載できる。 f:id:magayengineer:20160728234012j:plain

 回路

モータドライバは以下のように接続。

1つめのモータドライバは以下のように接続。

TA7291Pの1ピン:GND
TA7291Pの2ピン:DCモータと接続
TA7291Pの3ピン:未使用
TA7291Pの4ピン:10KΩの抵抗をつけてTA7291Pの7ピンと接続
TA7291Pの5ピン:GPIO25
TA7291Pの6ピン:GPIO24
TA7291Pの7ピン:10KΩの抵抗をつけてTA7291Pの4ピンと接続
TA7291Pの8ピン:未使用
TA7291Pの9ピン:電源(モバイルバッテリー)の+と接続 TA7291Pの10ピン:DCモータと接続

2つめのモータドライバは1つめのモータドライバと5ピンと6ピンだけ異なる。

TA7291Pの5ピン:GPIO20
TA7291Pの6ピン:GPIO21

サーボモータは以下のように接続。

コネクタ:GND
コネクタ:電源(モバイルバッテリー)の+と接続
コネクタ:GPIO18

タンクの操作方法の検討

キー操作は以下のようにした。 左旋回と右旋回をLボタンとRボタンにマッピングし、十字キーはカメラ制御専用にしたかったが、メインユーザである幼稚園児には難しそうなため、以下のようになった。

上キー:カメラを上に向ける
下キー:カメラを下に向ける
Xボタン:前進
Xボタン + 左キー:左旋回
☓ボタン + 右キー:右旋回

○ボタン:後退
○ボタン + 左キー:右旋回
○ボタン + 右キー:左旋回

クライアント側プログラム

前回(Raspberry PiでWifiラジコンを作ってPS3のコントローラで操作してみた - 俺の備忘録)とほとんど一緒。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import pygame
from pygame.locals import *
import time
from contextlib import closing
import socket


# PS3のコントローラのイベント処理クラス
class Ps3Controller:
    # 初期化
    def __init__(self, function):
        pygame.joystick.init()
        joys = pygame.joystick.Joystick(0)
        joys.init()
        self._function = function

    # 入力受付開始
    def start(self):
        pygame.init()

        while True:
           # イベントをハンドル
           self._function(pygame.event.get())
           time.sleep(0.1)

    # TODO
    def terminate(self):
        pass

# Raspberry PiのIPアドレスまたはホスト名
RASPBERRY_HOST = "192.168.0.30"

# Raspberry側の待ち受けポート
RASPBERRY_PORT = 9999

# pygameの定数とraspberry pi側にTCP/IPで指令を送る際に使用するコマンドのマッピング
TYPE_MAP = {JOYBUTTONDOWN: "DOWN",
            JOYBUTTONUP: "UP"}

BUTTON_MAP = {0: "SELECT",
              1: "ANALOG_LEFT",
              2: "ANALOG_RIGHT",
              3: "START",
              4: "UP",
              5: "RIGHT",
              6: "DOWN",
              7: "LEFT",
              8: "L2",
              9: "R2",
              10: "L1",
              11: "R1",
              12: "TRIANGLE",
              13: "CIRCLE",
              14: "CROSS",
              15: "SQUARE"}

# 扱うイベントのフィルタ
EVENT_FILTER= [(JOYBUTTONUP, 12),
                (JOYBUTTONDOWN, 12),
                (JOYBUTTONUP, 15),
                (JOYBUTTONDOWN, 15),
                (JOYBUTTONUP, 13),
                (JOYBUTTONDOWN, 13),
                (JOYBUTTONUP, 14),
                (JOYBUTTONDOWN, 14),
                (JOYBUTTONUP, 10),
                (JOYBUTTONDOWN, 10),
                (JOYBUTTONUP, 8),
                (JOYBUTTONDOWN, 8),
                (JOYBUTTONUP, 11),
                (JOYBUTTONDOWN, 11),
                (JOYBUTTONUP, 9),
                (JOYBUTTONDOWN, 9),
                (JOYBUTTONUP, 0),
                (JOYBUTTONDOWN, 0),
                (JOYBUTTONUP, 3),
                (JOYBUTTONDOWN, 3),
                (JOYBUTTONUP, 1),
                (JOYBUTTONDOWN, 1),
                (JOYBUTTONUP, 2),
                (JOYBUTTONDOWN, 2),
                (JOYBUTTONUP, 4),
                (JOYBUTTONDOWN, 4),
                (JOYBUTTONUP, 5),
                (JOYBUTTONDOWN, 5),
                (JOYBUTTONUP, 6),
                (JOYBUTTONDOWN, 6),
                (JOYBUTTONUP, 7),
                (JOYBUTTONDOWN, 7)]

# TCP/IPでRaspberry PIにコマンドを送る
def send_command(all_events):
    # 扱いたいイベントだけにフィルタする
    # 今回はボタン関連のイベントのみを扱う(コントローラの傾き等は扱わない)
    target_events = filter(lambda e:e.dict.has_key("button") and (e.type, e.dict["button"]) in EVENT_FILTER, all_events)

    command = ""
    for e in target_events:
        command += "(" + BUTTON_MAP.get(e.dict["button"]) + "," + TYPE_MAP.get(e.type) + ")&"

    # (ボタン名,イベント種別)&(ボタン名,イベント種別)&...の形式でRaspberry Piに送るコマンドを生成
    try:
        if len(command) > 0:
            print command

            bufsize = 4096

            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

            with closing(sock):
                sock.connect((RASPBERRY_HOST, RASPBERRY_PORT))
                sock.send(command)
    except Exception, e:
        print "networkerror"

if __name__ == '__main__':
    try:
        # PS3コントローラ初期化
        c = Ps3Controller(send_command)
        c.start()
    except KeyboardInterrupt:
        print "terminated"

Raspberry側プログラム

前回(Raspberry PiでWifiラジコンを作ってPS3のコントローラで操作してみた - 俺の備忘録)とほとんど一緒。 ただし、ボタンの組み合わせ操作に対応など一部改良。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import RPi.GPIO as GPIO
import wiringpi2 as wiringpi
import threading
import socket
from contextlib import closing

# Wifiラジコンカー
class Tank():
    # 最高スピード(Duty70%)
    SPEED_MAX = 70

    # 最低スピード(Duty40%)
    SPEED_MIN = 40

    # サーボのアングル最小
    ANGLE_MIN = 3.5

    # サーボのアングル最大
    ANGLE_MAX = 10

    def __init__(self):
        GPIO.setmode(GPIO.BCM)

        # 左輪
        GPIO.setup(24, GPIO.OUT)
        GPIO.setup(25, GPIO.OUT)
        # 右輪
        GPIO.setup(20, GPIO.OUT)
        GPIO.setup(21, GPIO.OUT)

        self._p0 = GPIO.PWM(25, 50)
        self._p1 = GPIO.PWM(24, 50)
        self._p2 = GPIO.PWM(20, 50)
        self._p3 = GPIO.PWM(21, 50)
        self._p0.start(0)
        self._p1.start(0)
        self._p2.start(0)
        self._p3.start(0)

        # サーボ
        wiringpi.wiringPiSetupGpio()

        # PWM出力ピンを指定
        wiringpi.pinMode(18, wiringpi.GPIO.PWM_OUTPUT)
        # 周波数を固定するための設定
        wiringpi.pwmSetMode(wiringpi.GPIO.PWM_MODE_MS)
         # 50 Hz。 <- 18750/(周波数)
        wiringpi.pwmSetClock(375)

        self._speed = 60
        self._angle = (Tank.ANGLE_MAX + Tank.ANGLE_MIN) / 2

    # 前進
    def go_ahead(self):
        # 左輪
        self._p0.ChangeDutyCycle(0)
        self._p1.ChangeDutyCycle(self._speed)

        # 右輪
        self._p2.ChangeDutyCycle(0)
        self._p3.ChangeDutyCycle(self._speed)

    # 後退
    def go_back(self):
        # 左輪
        self._p0.ChangeDutyCycle(self._speed)
        self._p1.ChangeDutyCycle(0)

        # 右輪
        self._p2.ChangeDutyCycle(self._speed)
        self._p3.ChangeDutyCycle(0)

    # 左旋回
    def turn_left(self):
        # 左輪
        self._p0.ChangeDutyCycle(self._speed)
        self._p1.ChangeDutyCycle(0)

        # 右輪
        self._p2.ChangeDutyCycle(0)
        self._p3.ChangeDutyCycle(self._speed)

    # 右旋回
    def turn_right(self):
        # 左輪
        self._p0.ChangeDutyCycle(0)
        self._p1.ChangeDutyCycle(self._speed)

        # 右輪
        self._p2.ChangeDutyCycle(self._speed)
        self._p3.ChangeDutyCycle(0)


    # カメラを少し上に向ける
    def up_camera(self):
        if self._angle < Tank.ANGLE_MAX:
            self._angle += 0.5
        wiringpi.pwmWrite(18, int(self._angle * 1024 / 100))

    # カメラを少し下に向ける
    def down_camera(self):
        if self._angle > Tank.ANGLE_MIN:
            self._angle -= 0.5
        wiringpi.pwmWrite(18, int(self._angle * 1024 / 100))

    # スピードアップ
    def speed_up(self):
        if self._speed < Tank.SPEED_MAX:
            self._speed += 10

    # スピードダウン
    def speed_down(self):
        if self._speed > Tank.SPEED_MIN:
            self._speed -= 10

    # 停止
    def stop(self):
        self._p0.ChangeDutyCycle(0)
        self._p1.ChangeDutyCycle(0)
        self._p2.ChangeDutyCycle(0)
        self._p3.ChangeDutyCycle(0)

    # 終了処理
    def terminate(self):
        self._p0.stop()
        self._p1.stop()
        self._p2.stop()
        self._p3.stop()
        GPIO.cleanup()

class CameraAngleControl(threading.Thread):
    def __init__(self):
        super(CameraAngleControl, self).__init__()

    def run(self):
        pass

# クライアントからTCP/IPで送られていたコマンドをハンドルして、
# Carクラスをコントロールするクラス
class TankController:
    def __init__(self, tank):
        self._tank = tank

        self._left = False
        self._right = False
        self._cross = False
        self._circle = False

        # コマンドとTANKの制御メソッドの対応づけ
        self._command_map = {
            "(UP,DOWN)": tank.up_camera,
            "(DOWN,DOWN)": tank.down_camera,
            "(L1,DOWN)": tank.speed_up,
            "(L2,DOWN)": tank.speed_down
        }


        # 状態とTANKの制御メソッドの対応づけ
        self._state_map = {
            0: tank.stop,
            1: tank.go_back,
            2: tank.go_ahead,
            3: tank.stop,
            4: tank.stop,
            5: tank.turn_left,
            6: tank.turn_right,
            7: tank.stop,
            8: tank.stop,
            9: tank.turn_right,
            10: tank.turn_left,
            11: tank.stop,
            12: tank.stop,
            13: tank.stop,
            14: tank.stop,
            15: tank.stop
        }

    def get_state(self):
        state = 0

        if self._left:
            state |= 8

        if self._right:
            state |= 4

        if self._cross:
            state |= 2

        if self._circle:
            state |= 1
        print state
        return state

    def change_flags(self, button, type):
        if button == "LEFT" and type == "UP":
            self._left = False
        elif button == "LEFT" and type == "DOWN":
            self._left = True
        elif button == "RIGHT" and type == "UP":
            self._right = False
        elif button == "RIGHT" and type == "DOWN":
            self._right = True
        elif button == "CROSS" and type == "UP":
            self._cross = False
        elif button == "CROSS" and type == "DOWN":
            self._cross = True
        elif button == "CIRCLE" and type == "UP":
            self._circle = False
        elif button == "CIRCLE" and type == "DOWN":
            self._circle = True

        print self._left, self._right, self._cross, self._circle


    # 送られてきたコマンドに対応するTankクラスの関数を実行する
    def control(self, commands):
        commands = commands.split("&")
        for command in commands:
            if len(command) > 0:
                if self._command_map.has_key(command):
                    # コマンドに対応する制御メソッドをそのまま実行
                    func = self._command_map.get(command)
                    if func != None:
                        func()
                else:
                    # 複数のボタンの押下状態を見て挙動を判定するもの
                    command = command.replace('(', '')
                    command = command.replace(')', '')
                    values = command.split(",")
                    self.change_flags(values[0], values[1])
                    func = self._state_map.get(self.get_state())
                    print func

                    if func != None:
                        func()

    # 終了処理
    def terminate(self):
        self._tank.terminate()

BUFF_SIZE = 4096
QUQUE_SIZE = 50

class CommandReciever():
    def __init__(self, host_name, port, function):
        self._host_name = host_name
        self._port = port
        self._function = function
        self._sock = None

    def start(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._sock = sock
        with closing(sock):
            sock.bind((self._host_name, self._port))
            sock.listen(QUQUE_SIZE)

            while True:
                conn, address = sock.accept()
                with closing(conn):
                    # コマンド受付
                    msg = conn.recv(BUFF_SIZE)
                    self._function(msg)

    def terminate(self):
        self._sock.close()


HOST_NAME = '192.168.0.30'
PORT = 9999
# メイン処理
if __name__ == '__main__':
    try:
        # Carクラスとコントローラクラスを初期化
        ctrl = TankController(Tank())

        reciever = CommandReciever(HOST_NAME, PORT, ctrl.control)
        reciever.start()
    except KeyboardInterrupt:
        ctrl.terminate()
        reciever.terminate()

今後について

練習や最低限の勉強(リハビリ?)じゃ出来たのでサーボモータを複数使用したロボット制作等をしていきたい! ただし、お小遣いが枯渇してしまったため、しばらくお休み!

Raspberry PiでWifiラジコンを作ってPS3のコントローラで操作してみた

やったこと

  • Raspbery Piを利用してラジコンを作った。
  • クライアントPC(ノートPC)にPS3のコントローラをつないで操作すると、Wifi経由でラジコンを制御できるようにした。
  • ラジコンにカメラを搭載し、クライアントPCからリアルタイムに映像を見れるようにした。

車体はこんな感じで、割と無理やり感満載。 2階建てにして、2階部分にモバイルバッテリーやカメラ、ラズパイを載せている。 f:id:magayengineer:20160716220217j:plain

ノートPCにPS3のコントローラをつないで、 Raspberry Piから送られてくるライブ映像を見ながら操作できる。
ちなみに、となりに置いてあるキュウリは本日収穫したもの。 f:id:magayengineer:20160716220311j:plain

システム構成

  • ライブ映像は、motionを利用。詳細は後述するが、インストールと簡単な設定だけでUSBカメラからライブ画像を取得できるようになる。

  • ラジコンの操作は、PS3のコントローラを利用。 ノートPC上で動くPythonプログラムでPS3コントローラの入力を受け取って、TCP/IPRaspberry Piに送信し、ラジコンを制御。

  • PS3のコントローラの入力をTCP/IPで受け取ったRaspberry Piは、モータドライバでモータを制御し、車体を進めたり、サーボを制御して、ステアを切ったりして動く。

  • ノートPCのOSは、Linux Mint 17.3 Mint

f:id:magayengineer:20160716220348j:plain

用意したもの

Raspberry Pi 3本体 Raspberry Pi3 Model B ボード&ケースセット (Element14版, Clear)-Physical Computing Lab

②モバイルバッテリー Raspberry Piとモータへの電源供給用として購入。

5V 2.1AでRapberry Piに電源供給し、 5V 1 AでDCモータ・サーボモータに電源供給する。
出力が2口あるため、これだけで電源がまかなえる。

(パワーアド)Poweradd Pilot 2GS 10000mAhモバイルバッテリー 急速充電 2USBポート iPhone6 / iPhone6s / iPhone5 / iPad / Xperia / Nexus等対応(シルバー)

③ブレッドボード
サンハヤト SAD-101 ニューブレッドボード サンハヤト SAD-101 ニューブレッドボード

④USBカメラ(マイク付き)
ELECOM WEBカメラ 200万画素 1/5インチCMOSセンサ マイク内蔵 コンパクトタイプ ブラック UCAM-C0220FBBK ELECOM WEBカメラ 200万画素 1/5インチCMOSセンサ マイク内蔵 コンパクトタイプ ブラック UCAM-C0220FBBK

⑤車体(モータ・ギア・タイヤ付き・ステアリング機構付き)
車体のベースとして使用する。
これを買った理由は、「ステアリング機構」が付いているため、サーボモータと、上手い感じに繋げられれば、ステアを切れると踏んだため。

タミヤ 楽しい工作シリーズ No.112 バギー工作基本セット (70112) タミヤ 楽しい工作シリーズ No.112 バギー工作基本セット (70112)

サーボモータ・モータドライバ
アマゾンで売っているキットについてたものを利用。
サーボモータ => SG-90
モータドライバ => TA7291P 

アマゾンのレビューはイマイチだが、パーツを個々に注文しなくて良いので、入門用にある程度部品をまとめて揃えたい人には便利。

Raspberry Piで学ぶ電子工作 専用 実験キット 基本部品セット スターターパック (電子部品関連) Raspberry Piで学ぶ電子工作 専用 実験キット 基本部品セット スターターパック (電子部品関連)

⑦ユニバーサルプレート
バイルバッテリーやブレッドボード、Raspberry Pi本体の固定用
楽しい工作シリーズ No.157 ユニバーサルプレート 2枚セット (70157) 楽しい工作シリーズ No.157 ユニバーサルプレート 2枚セット (70157)

ホムセンで買ったステーっぽい何か
車体を2階建てにするために使用。 2階部分にRaspberry Piやブレッドボード等を載せるために何でもいいので、それなりに硬くて高さがうまく稼げるものなら代用可能。 f:id:magayengineer:20160716220451j:plain

⑩ステンレスの針金(1.2ミリ)
家にあったものを利用。 ステアリングとサーボモータを連結するために使用。 f:id:magayengineer:20160716220458j:plain

⑪USBケーブル
バイルバッテリーから、モータ等への電源供給に使用する。
USBのケーブルを切断して、赤と黒の線だけ引っ張りだして5Vの電源として使う。 写真は赤と黒の線を引っ張りだしたあとに、ジャンパ線(オスメス)に合体したもの。

f:id:magayengineer:20160716220537j:plain

⑫その他(家にあったもの)
- SDカード
- はんだごて
- 太さ3mmのネジとボルト
- ピンドリル(3mm)
- 両面テープ
- ・ジャンパーピン(オス・オス、オス・メス)

車体組み立て

ポイントだけ説明。

次の写真のようにサーボとステアリング機構をつなげる。
- サーボは両面テーブで固定する。
- シャフトに針金を折り曲げたものを半田で固定する。
- サーボのアームに針金を無理やりとおし、シャフトに取り付けた針金の間に通す。

これでサーボのアームを動かすと、ステアが切れるようになる。 めっちゃ苦労した。

f:id:magayengineer:20160716220543j:plain

ホームセンターで買ったステーっぽいものをニコイチにして、 車体(木の部分)に固定し、2階建てにする。

2階部分はユニバーサルプレートを適当に固定する。

f:id:magayengineer:20160716220549j:plain

 回路

モータドライバは以下のように接続。

TA7291Pの1ピン:GND
TA7291Pの2ピン:DCモータと接続
TA7291Pの3ピン:未使用
TA7291Pの4ピン:10KΩの抵抗をつけてTA7291Pの7ピンと接続
TA7291Pの5ピン:GPIO25
TA7291Pの6ピン:GPIO24
TA7291Pの7ピン:10KΩの抵抗をつけてTA7291Pの4ピンと接続
TA7291Pの8ピン:未使用
TA7291Pの9ピン:電源(モバイルバッテリー)の+と接続 TA7291Pの10ピン:DCモータと接続

サーボモータは以下のように接続。

コネクタ:GND
コネクタ:電源(モバイルバッテリー)の+と接続
コネクタ:GPIO18

motionの設定

ライブ映像をストリーミングするためにRapberry PiにMotionを導入する。

インストール

sudo apt-get install motion

インストール完了後、設定ファイル(/etc/motion/motion.conf)を以下のように修正する。

stream_maxrate 30 # フレームレート
stream_localhost off # ライブ映像を他ホストに公開するか
webcontrol_localhost off 
stream_auth_method 1  # Basic認証
stream_authentication hoge:hoge # ブラウザアクセス時に使用するIDとパスワード
width 640
height 480
output_pictures off # 動体検知したときに写真を撮って自動保存するか
ffmpeg_output_movies off

設定完了後、motionを起動して、http://<ラズパイのIP>:8081/にブラウザでアクセスし、ライブ映像が見れればOK。

motionの起動方法

sudo motion

クライアント側プログラム

PS3のコントローラを使うために、ライブラリ(pygame)を導入。 PS3のコントローラで発生したイベントを受け取って、TCP/IPで送信する。 以下のフォーマットでイベントをRaspberry Pi側に送る仕様にした。  

(ボタン種別,イベント種別)&(ボタン種別,イベント種別)&

例:
(RIGHT,UP)&(CROSS,DOWN)&

#!/usr/bin/python
# -*- coding: utf-8 -*-

import pygame
from pygame.locals import *
import time
from contextlib import closing
import socket


# PS3のコントローラのイベント処理クラス
class Ps3Controller:
    # 初期化
    def __init__(self, function):
        pygame.joystick.init()
        joys = pygame.joystick.Joystick(0)
        joys.init()
        self._function = function

    # 入力受付開始
    def start(self):
        pygame.init()

        while True:
           # イベントをハンドル
           self._function(pygame.event.get())
           time.sleep(0.1)

    # TODO
    def terminate(self):
        pass

# Raspberry PiのIPアドレスまたはホスト名
RASPBERRY_HOST = "192.168.0.30"

# Raspberry側の待ち受けポート
RASPBERRY_PORT = 9999

# pygameの定数とraspberry pi側にTCP/IPで指令を送る際に使用するコマンドのマッピング
TYPE_MAP = {JOYBUTTONDOWN: "DOWN",
            JOYBUTTONUP: "UP"}

BUTTON_MAP = {0: "SELECT",
              1: "ANALOG_LEFT",
              2: "ANALOG_RIGHT",
              3: "START",
              4: "UP",
              5: "RIGHT",
              6: "DOWN",
              7: "LEFT",
              8: "L2",
              9: "R2",
              10: "L1",
              11: "R1",
              12: "TRIANGLE",
              13: "CIRCLE",
              14: "CROSS",
              15: "SQUARE"}

# 扱うイベントのフィルタ
EVENT_FILTER= [(JOYBUTTONUP, 12),
                (JOYBUTTONDOWN, 12),
                (JOYBUTTONUP, 15),
                (JOYBUTTONDOWN, 15),
                (JOYBUTTONUP, 13),
                (JOYBUTTONDOWN, 13),
                (JOYBUTTONUP, 14),
                (JOYBUTTONDOWN, 14),
                (JOYBUTTONUP, 10),
                (JOYBUTTONDOWN, 10),
                (JOYBUTTONUP, 8),
                (JOYBUTTONDOWN, 8),
                (JOYBUTTONUP, 11),
                (JOYBUTTONDOWN, 11),
                (JOYBUTTONUP, 9),
                (JOYBUTTONDOWN, 9),
                (JOYBUTTONUP, 0),
                (JOYBUTTONDOWN, 0),
                (JOYBUTTONUP, 3),
                (JOYBUTTONDOWN, 3),
                (JOYBUTTONUP, 1),
                (JOYBUTTONDOWN, 1),
                (JOYBUTTONUP, 2),
                (JOYBUTTONDOWN, 2),
                (JOYBUTTONUP, 4),
                (JOYBUTTONDOWN, 4),
                (JOYBUTTONUP, 5),
                (JOYBUTTONDOWN, 5),
                (JOYBUTTONUP, 6),
                (JOYBUTTONDOWN, 6),
                (JOYBUTTONUP, 7),
                (JOYBUTTONDOWN, 7)]

# TCP/IPでRaspberry PIにコマンドを送る
def send_command(all_events):
    # 扱いたいイベントだけにフィルタする
    # 今回はボタン関連のイベントのみを扱う(コントローラの傾き等は扱わない)
    target_events = filter(lambda e:e.dict.has_key("button") and (e.type, e.dict["button"]) in EVENT_FILTER, all_events)

    command = ""
    for e in target_events:
        command += "(" + BUTTON_MAP.get(e.dict["button"]) + "," + TYPE_MAP.get(e.type) + ")&"

    # (ボタン名,イベント種別)&(ボタン名,イベント種別)&...の形式でRaspberry Piに送るコマンドを生成
    if len(command) > 0:
        print command

        bufsize = 4096

        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        with closing(sock):
            sock.connect((RASPBERRY_HOST, RASPBERRY_PORT))
            sock.send(command)

if __name__ == '__main__':
    try:
        # PS3コントローラ初期化
        c = Ps3Controller(send_command)
        c.start()
    except KeyboardInterrupt:
        print "terminated"

リファクタしてないから汚いコードだ...

Raspberry側プログラム

Raspberry側のプログラム。 サーボの制御のために、wiringpiを利用。 クライアントからTCP/IPで送られてきたコマンドを解釈し、ラジコンを操作する。 Duty比を変化させることで、DCモータの正転・反転・速度と、 サーボモータの角度を制御している。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import RPi.GPIO as GPIO
import wiringpi2 as wiringpi
import socket
from contextlib import closing

# Wifiラジコンカー
class Car():
    # 最高スピード(Duty100%。 100%はモータに負荷がかかるので注意。)
    SPEED_MAX = 100

    # 最低スピード(Duty40%)
    SPEED_MIN = 40

    def __init__(self):
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(24, GPIO.OUT)
        GPIO.setup(25, GPIO.OUT)

        wiringpi.wiringPiSetupGpio()

        # PWM出力ピンを指定
        wiringpi.pinMode(18, wiringpi.GPIO.PWM_OUTPUT)
        # 周波数を固定するための設定
        wiringpi.pwmSetMode(wiringpi.GPIO.PWM_MODE_MS)

         # 50 Hz。 <- 18750/(周波数)
        wiringpi.pwmSetClock(375)

        self._p0 = GPIO.PWM(25, 50)
        self._p1 = GPIO.PWM(24, 50)
        self._p0.start(0)
        self._p1.start(0)

        self._speed = 60

    # 前進
    def go_ahead(self):
        self._p0.ChangeDutyCycle(0)
        self._p1.ChangeDutyCycle(self._speed)

    # 後退
    def go_back(self):
        self._p0.ChangeDutyCycle(self._speed)
        self._p1.ChangeDutyCycle(0)

    # ステアリングを左に切る
    def turn_left(self):
        wiringpi.pwmWrite(18, int(8.35 * 1024 / 100))

    # ステアリングを右に切る
    def turn_right(self):
        wiringpi.pwmWrite(18, int(5.125 * 1024 / 100))

    # ステアリングをまっすぐにする
    def turn_strait(self):
        wiringpi.pwmWrite(18, int(6.75 * 1024 / 100))

    # スピードアップ
    def speed_up(self):
        if self._speed < Car.SPEED_MAX:
            self._speed += 10

    # スピードダウン
    def speed_down(self):
        if self._speed > Car.SPEED_MIN:
            self._speed -= 10

    # 停止
    def stop(self):
        self._p0.ChangeDutyCycle(0)
        self._p1.ChangeDutyCycle(0)

    # 終了処理
    def terminate(self):
        self._p0.stop()
        self._p1.stop()
        GPIO.cleanup()

# クライアントからTCP/IPで送られていたコマンドをハンドルして、
# Carクラスをコントロールするクラス
class CarController:
    def __init__(self, car):
        self._car = car

        # コマンドとCarクラスのfunctionの対応関係マップ
        self._function_map = {
            "(CROSS,UP)":car.stop,
            "(CROSS,DOWN)":car.go_ahead,
            "(CIRCLE,UP)":car.stop,
            "(CIRCLE,DOWN)":car.go_back,
            "(RIGHT,UP)":car.turn_strait,
            "(RIGHT,DOWN)":car.turn_right,
            "(LEFT,UP)":car.turn_strait,
            "(LEFT,DOWN)":car.turn_left,
            "(UP,DOWN)":car.speed_up,
            "(DOWN,DOWN)":car.speed_down,
        }

    # 送られてきたコマンドに対応するcarクラスの関数を実行する
    def control(self, command):
        if self._function_map.has_key(command):
            f = self._function_map.get(command)
            f()

    # 終了処理
    def terminate(self):
        self._car.terminate()

# メイン処理
def main():
    # Carクラスとコントローラクラスを初期化
    car = Car()
    ctrl = CarController(car)

    # TCP/IPでコマンドを受け付ける
    host = '192.168.0.30'
    port = 9999
    queue = 10
    bufsize = 4096

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    with closing(sock):
        sock.bind((host, port))
        sock.listen(queue)
        while True:
            conn, address = sock.accept()
            with closing(conn):
                # コマンド受付
                msg = conn.recv(bufsize)
                # (ボタン名,イベント種別)&(ボタン名,イベント種別)&...の形式で送られてくるので&でスプリット
                commands = msg.split("&")
                for command in commands:
                    if len(command) > 0:
                        print command
                        # コントローラクラスにコマンドを渡す
                        ctrl.control(command)

if __name__ == '__main__':
    main()

リファクタしてないから汚いコードだ...

今後の課題

小さい車体に無理やり詰め込んだため、これ以上拡張が厳しい。 ベース車体を大きなものに変更し、いろいろつめ込んでいきたい。

Raspberry Piで音声認識してカメラで写真を撮ってメールで送ってみた

やったこと

Raspberry Piにマイク付きUSBカメラを接続し、特定の音声(キーワード)に反応し、カメラで写真を撮ってメール送信するシステムを作ってみた。

家で待ってる子供がカメラに向かって「パパ!」って叫んだら、子供の写真を撮って 仕事中のパパに届いたら楽しいかなと思って作った。

用意したもの

全てAmazonで購入。

Raspberry Pi 3本体
https://www.amazon.co.jp/gp/product/B01CSFZ4JG/ref=oh_aui_detailpage_o01_s01?ie=UTF8&psc=1

②USBカメラ(マイク付き)
ElecomのUCAM-C0220FBNBK
https://www.amazon.co.jp/gp/product/B00UZNLIBW/ref=oh_aui_detailpage_o01_s01?ie=UTF8&psc=1

③その他 SDカードや電源等も合わせて購入したけど、割愛。

システム構成

おおまかな流れは以下の通り。
- USBマイクから音声を入力し、音声認識ソフト(Julius)で解析
- Pythonプログラムで音声認識結果を取得し、特定のキーワードが含まれているか判定。
含まれているならUSBカメラで写真を撮る。
- 写真を撮ったらgmailで送る

f:id:magayengineer:20160706231649j:plain

写真撮影ソフトの準備

写真撮影にはfswebcamを利用。
ここを参考にした => http://seesaawiki.jp/w/renkin3q/d/Linux/fswebcam

①インストール

apt-get install fswebcam

②撮ってみる

fswebcam -r 640x480 <ファイル名> -F 10

-Fオプションでや-Sオプションをいじると、写真の明るさを調整できる。

マイクの準備

音声認識ソフトを使う際に、マイクはUSBマイクの優先度を上げておく必要があるので設定する。

①今の優先度の確認

sudo cat /proc/asound/modules

USBマイクの優先度が2番目ということがわかる

0 snd_bcm2835
1 snd_usb_audio

②優先度の変更 /etc/modprobe.d/alsa-base.confを以下のようにいじる。   なお、ファイルが存在しない場合は自分で作成する。

options snd_usb_audio index=0
options snd_bcm2835 index=1

終わったらリブートする

③USBマイクのカード番号とデバイス番号を調べる

arecord -l

カード 1: UCAMC0220F [UCAM-C0220F], デバイス 0: USB Audio [USB Audio]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0

カード1番の、デバイス0がUSBマイクということがわかる なお、認識されないことがよくある。その場合は、リブートを何回かすれば認識される。

④マイクの感度を設定

amixer sset Mic 4096 -c 0

4096は感度。最大値は使っているマイクで違う模様。 0はarecord -lで調べたデバイス番号を指定

⑤録音テスト

arecord -D plughw:1,0 -f cd test.wav

1,0はarecordで調べたカード番号とデバイス番号

ちょっとよくわかってないけど必要なこと

このモジュールを読まないと、音声認識ソフトがうまくうごかないっぽい。

/etc/modulesに以下を追記してリブート

snd-pcm-oss

音声認識ソフトの準備

julius(http://julius.osdn.jp/)を利用。 ここを見ながら設定した => http://feijoa.jp/laboratory/raspberrypi/julius/

①juliusをダウンロード&解凍&make&install

wget -O julius.tar.gz "https://osdn.jp/frs/redir.php?m=jaist&f=%2Fjulius%2F60273%2Fjulius-4.3.1.tar.gz"
tar -zxvf julius.tar.gz
cd julius
./configure
sudo make install

②ディクテーションキットと文法認識キットを取得

wget -O dictation-kit-v4.3.1-linux.tgz 'http://sourceforge.jp/frs/redir.php?m=jaist&f=%2Fjulius%2F60416%2Fdictation-kit-v4.3.1-linux.tgz'
wget -O grammar-kit-v4.1.tar.gz 'http://sourceforge.jp/frs/redir.php?m=osdn&f=%2Fjulius%2F51159%2Fgrammar-kit-v4.1.tar.gz'

解凍後、適当なフォルダに放り込んでおく

③単語帳作成 juliusに認識させたい単語を書いたファイルを作成する。 ここに書いた単語に、喋った内容が強制マッピングされる。 <= これはjuliusの動作設定次第。

dictionary.yomi

パパ   ぱぱ
大好き   だいすき
早く  はやく
ママ  まま
お土産   おみやげ
買ってきて かってきて
好き  すき
嫌い  きらい
ゴミ1 あ
ゴミ2 い

ポイントは、本当に認識させたい言葉以外の言葉をある程度登録しておくこと。 少ないと、あ => パパ, うんこ => パパみたいにマッピングされやすくなる。

④単語帳変換 juliusが認識できる形式に変換する。

iconv -f utf8 -t eucjp ~/modules/julius-kits/dictionary.yomi  | ./yomi2voca.pl > ~/modules/julius-kits/dictation-kit-v4.3.1-linux/base_dict.dic

⑤julius設定ファイル作成

custom.jconf

# 辞書ファイル指定
-w base_dict.dic
# 入力はマイクを使用
-input mic
# 解析結果をUTF8で取得
-charconv euc-jp utf8
# 以降はよくわからない!
-h model/phone_m/jnas-tri-3k16-gid.binhmm
-hlist model/phone_m/logicalTri
-output 1

⑥juliusを起動してみてテストする

custom.jconfをdictation-kit-v4.3.1-linuxディレクトリ直下に配置。 そして、カレントディレクトリをdictation-kit-v4.3.1-linuxに移動して以下のコマンドを実行

julius -C custom.jconf

試しに「パパ」って言ってみると,,,

pass1_best: パパ    
pass1_best_wordseq: パパ
pass1_best_phonemeseq: silB p a p a silE
pass1_best_score: -1628.528442
sentence1: パパ
wseq1: パパ
phseq1: silB p a p a silE
cmscore1: 0.238
score1: -1628.528442

<<< please speak >>>

たしかにパパって認識されている模様。

⑦モジュールモードでjuliusを起動する
モジュールモードで起動すると、juliusが10500ポートでTCP/IP接続を受け付けるようになる。 TCP/IPで接続をするとjuliusが音声認識を開始し、結果をTCP/IPで返してくれる。

モジュールモードで起動

julius -C custom.jconf -module

接続して結果を受け取るテストコード

#!/usr/bin/python
# -*- coding: utf-8 -*-
import socket
import cStringIO

# Raspberry PiのIPアドレス
host = '192.168.0.30'
# juliusの待ち受けポート
port = 10500

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))

xml_buff = ""
in_recoguout = False

while True:
    data = cStringIO.StringIO(sock.recv(4096))
    line = data.readline()
    # 認識結果はRECOGOUTタグで返ってくるのでそこだけ抽出
    while line:
        if line.startswith("<RECOGOUT>"):
            in_recoguout = True
            xml_buff += line
        elif line.startswith("</RECOGOUT>"):
            xml_buff += line
            print xml_buff
            in_recoguout = False
            xml_buff = ""
        else:
            if in_recoguout:
                xml_buff += line
        line = data.readline()
sock.close()

「パパ」って言うと、以下の結果が得られる。

<RECOGOUT>
  <SHYPO RANK="1" SCORE="-1970.796997" GRAM="0">
    <WHYPO WORD="パパ" CLASSID="パパ" PHONE="silB p a p a silE" CM="0.581"/>
  </SHYPO>
</RECOGOUT>

WHYPOタグのWORD属性を見れば、パパと呼ばれたか判断できる。

パパと呼ばれたら、写真を撮ってメールを送るpythonプログラム

juliusをモジュールモードで起動し、以下のプログラムを実行すると、 ”パパ”と呼ばれるたびに、写真を撮ってメールで送ることができる。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import subprocess
import sys
import smtplib
import email
import traceback
import socket
import cStringIO

from xml.etree.ElementTree import fromstring
from datetime import datetime
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText

# メールアカウント
MAIL_ADDRESS = "メールアドレス"
MAIL_PASSWARD = "パスワード"
TO_MAIL_ADDRESS = "送り先メールアドレス"

# SMTPサーバ設定
SMTP_SERVER = "smtp.gmail.com"
SMTP_PORT = 587

# Juliusサーバ
JULIUS_HOST = "localhost"
JULIUS_PORT = 10500


# メールメッセージを作成
def create_message(from_addr, to_addr, subject, body, attach_file=None):
    msg = MIMEMultipart()
    msg["From"] = from_addr
    msg["To"] = to_addr
    msg["Date"] = email.utils.formatdate()
    msg["Subject"] = Header(subject)
    body = MIMEText(body)
    msg.attach(body)

    # 添付ファイル
    if attach_file is not None:
        attachment = MIMEBase(attach_file['type'], attach_file['subtype'])
        file = open(attach_file['path'])
        attachment.set_payload(file.read())
        file.close()
        email.encoders.encode_base64(attachment)
        msg.attach(attachment)
        attachment.add_header("Content-Disposition","attachment", filename=attach_file['name'])
    return msg

# メールを送信する
def send_email(smtp_server, smtp_port, from_mail_address, from_mail_passward, to_mail_address, subject, body, attach_file=None):
    # メッセージの作成
    msg = create_message(from_mail_address, to_mail_address, subject, body, attach_file)
    # 送信
    smtpobj = smtplib.SMTP(smtp_server, smtp_port)
    smtpobj.ehlo()
    # gmailを使うのでTLSを用いる
    smtpobj.starttls()
    smtpobj.ehlo()
    smtpobj.login(from_mail_address, from_mail_passward)
    smtpobj.sendmail(from_mail_address, [to_mail_address], msg.as_string())
    smtpobj.close()


# juliusの解析結果のXMLをパース
def parse_recogout(xml_data):
    # scoreを取得(どれだけ入力音声が、認識結果と合致しているか)
    shypo = xml_data.find(".//SHYPO")
    if shypo is not None:
        score = shypo.get("SCORE")

    # 認識結果の単語を取得
    whypo = xml_data.find(".//WHYPO")
    if whypo is not None:
        word = whypo.get("WORD")
    return score, word

# fswebcamで写真を撮る
def take_picuture(picutre_dir):
    file_name = datetime.now().strftime('%Y%m%d_%H%M%S') + ".jpg"
    file_path = picture_dir + file_name
    cam_command = "fswebcam -r 640x480 {0} -F 10".format(file_path)
    p_camera = subprocess.Popen(cam_command,  shell=True)

    # 写真撮影完了までwaitする
    p_camera.wait()

    return file_name, file_path

if __name__ == "__main__":
    # 写真の保存先ディレクトリ
    picture_dir = sys.argv[1]
    if not picture_dir.endswith("/"):
        picture_dir += "/"

    try:
        # TCP/IPでjuliusに接続
        bufsize = 4096
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((JULIUS_HOST, JULIUS_PORT))
        sock_file = sock.makefile()

        xml_buff = ""
        in_recogout = False

        while True:
            # juliusから解析結果を取得
            data = cStringIO.StringIO(sock.recv(bufsize))

            # 解析結果から一行取得
            line = data.readline()

            while line:
                # 音声の解析結果を示す行だけ取り出して処理する。
                # RECOGOUTタグのみを抽出して処理する。
                if line.startswith("<RECOGOUT>"):
                    in_recogout = True
                    xml_buff += line
                elif line.startswith("</RECOGOUT>"):
                    xml_buff += line
                    xml_data = fromstring(xml_buff)
                    # XMLをパースして、解析結果を取得する
                    score, word = parse_recogout(xml_data)

                    if u'パパ' in word:
                        # パパと呼ばれたら写真を撮る
                        file_name, file_path = take_picuture(picture_dir)

                        # 添付ファイルの情報を作成
                      
                        # メール送信
                        send_email(SMTP_SERVER, SMTP_PORT, MAIL_ADDRESS, MAIL_PASSWARD, TO_MAIL_ADDRESS, "papa called", "papa called",  attach_file)

                    in_recogout = False
                    xml_buff = ""
                else:
                    if in_recogout:
                        xml_buff += line
                # 解析結果から一行取得
                line = data.readline()
    except Exception as e:
        print "error occurred", e, traceback.format_exc()
    finally:
        pass

今後の拡張について

  • juliusの設定をどうにかして文章をうまく認識できるようにしたい。今回は、単語の認識のみ扱った。文章を認識にも対応して、メール本文に音声認識結果を埋め込むとかしたい。
  • メールに返信したらRaspberry Piがメール本文に書かれたテキストを喋るようにしたい。

その他参考にしたサイト

VB.NETでノート内臓カメラをキャプチャ

作ったもの

  • ノートPCの内臓カメラをキャプチャ(プレビュー)
  • Enterキーを押すとキャプチャ画像を保存
  • VB.NETで作成

f:id:magayengineer:20160614225940p:plain

コード

Public Class Form1

    'Windows API
    Private Const WM_USER As Long = &H400
    Private Const WM_CAP_START As Long = WM_USER
    Private Const WM_CAP_DRIVER_CONNECT As Long = WM_CAP_START + 10
    Private Const WM_CAP_DRIVER_DISCONNECT As Long = WM_CAP_START + 11
    Private Const WM_CAP_SET_PREVIEW As Long = WM_CAP_START + 50
    Private Const WM_CAP_SET_PREVIEWRATE As Long = WM_CAP_START + 52
    Private Const WM_CAP_FILE_SAVEDIBA = WM_CAP_START + 25
    Private Const WS_CHILD = &H40000000
    Private Const WS_VISIBLE = &H10000000

    Private Declare Function capCreateCaptureWindow Lib "avicap32.dll" Alias "capCreateCaptureWindowA" (ByVal lpszWindowName As String, ByVal dwStyle As Integer, ByVal x As Integer, ByVal y As Integer, ByVal nWidth As Integer, ByVal nHeight As Short, ByVal hWndParent As Integer, ByVal nID As Integer) As Integer
    Private Declare Function sendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Short, ByVal lParam As String) As Integer
    Private Declare Function destroyWindow Lib "user32" Alias "DestroyWindow" (ByVal hndw As Integer) As Boolean
    Private Declare Function getTickCount Lib "kernel32" Alias "GetTickCount" () As Long


    'キャプチャ画面のハンドル
    Private cHandle As Long = -1

    'Formのロードイベント
    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'サイズ固定化
        Me.FormBorderStyle = FormBorderStyle.FixedSingle
        Me.MaximumSize = Me.Size
        Me.MinimumSize = Me.Size

        'キャプチャウィンドウ作成
        cHandle = capCreateCaptureWindow("", WS_VISIBLE Or WS_CHILD, 0, 0, Me.Width, Me.Height, Me.Handle, 0)
        If cHandle = -1 Then
            MessageBox.Show("capCreateCaptureWindow failed.")
            Exit Sub
        End If

        'カメラが準備できるまで少し待つ(必要があるらしい)
        Dim IniTime = getTickCount()
        While getTickCount() < (IniTime + 1000)
            Application.DoEvents()
        End While

        'リトライ回数
        Dim retryCount As Integer = 5
        Dim tryCount As Integer = 0
        Dim connectResult As Integer = 0

        '成功するまでカメラ接続を試行する(タイミングにより失敗することが多々ある.特にWin8.1)
        While tryCount < retryCount
            '注意:カメラが複数ある場合は第3引数を変える
            connectResult = sendMessage(cHandle, WM_CAP_DRIVER_CONNECT, 0, 0)
            If connectResult <> 0 Then
                Exit While
            End If
            tryCount += 1
        End While

        'カメラに接続失敗
        If connectResult = 0 Then
            Call destroyWindow(cHandle)
            cHandle = -1
            MessageBox.Show("WM_CAP_DRIVER_CONNECT failed.")
            Exit Sub
        End If

        Call sendMessage(cHandle, WM_CAP_SET_PREVIEWRATE, 60, 0) ' frame rateっぽい
        Call sendMessage(cHandle, WM_CAP_SET_PREVIEW, 1, 0)
    End Sub

    'Formのクローズイベント
    Private Sub Form1_FormClosed(sender As Object, e As FormClosedEventArgs) Handles MyBase.FormClosed
        If cHandle > -1 Then
            Call sendMessage(cHandle, WM_CAP_DRIVER_DISCONNECT, 0, 0)
            Call destroyWindow(cHandle)
            cHandle = -1
        End If
    End Sub

    '画面キャプチャ保存
    Private Sub From1_KeyDown(ByVal sender As Object, ByVal e As KeyEventArgs) Handles Me.KeyDown
        If e.KeyData = Keys.Enter Then
            If cHandle > -1 Then
                'Bitmapで一度保存
                Dim dateStr = DateTime.Now.ToString("yyyyMMdd_HHmmss")
                Dim bmpFile = dateStr + ".bmp"
                Call sendMessage(cHandle, WM_CAP_FILE_SAVEDIBA, 0, bmpFile)

                'PNGn変換
                Dim bmpData As New System.Drawing.Bitmap(bmpFile)
                bmpData.Save(dateStr + ".png", System.Drawing.Imaging.ImageFormat.Png)
                bmpData.Dispose()

                'Bitmap削除
                System.IO.File.Delete(bmpFile)
            End If
        End If
    End Sub
End Class

参考サイト

http://i-break.net/article/69427813.html https://msdn.microsoft.com/en-us/library/windows/desktop/dd757695(v=vs.85).aspx http://www.ecoop.net/coop/api/winmes.html https://social.msdn.microsoft.com/Forums/vstudio/en-US/6d2d2e0f-cab7-4299-a778-cb8d9bab4b0d/avicap32dll-wmcapdriverconnect-problem?forum=csharpgeneral

Pythonとmatplotlibで粒子群最適化アルゴリズムをアニメーション表示してみた

はじめに

オセロのAI作成は一回お休み。前回 => (Pythonの勉強がてらオセロAIを作ってみる③ - 俺の備忘録)
今回は、PythonPythonのグラフ描画ライブラリmatplotlibで、粒子群最適化アルゴリズムの最適化過程をアニメーション表示してみた。

粒子群最適化アルゴリズムとは

粒子群最適化アルゴリズムとは(Particle Swarm Optimization、以下PSO)とは、遺伝的アルゴリズムと同様のメタヒューリティクスな最適化アルゴリズムの一つで、ナップザック問題や巡回セールスマン問題などの最適化問題に用いられる。

このアルゴリズムは、虫や鳥の行動を模して考案されたもので、 ざっくり言うと、誰か1匹が良い解を見つけると、その近辺にいい解があるだろうと思って、他の虫や鳥も集まってくるという虫/鳥の知恵?修正?を真似したものになっている。 例えば、海で普段カモメは気ままにそれぞれ餌を探しているんだけど、 とある一匹がイワシの群れを見つけたら、他のカモメもその近辺に群がってくるって感かな。

図に表すと以下みたいな感じ。 f:id:magayengineer:20160407233227p:plain

f:id:magayengineer:20160407233235p:plain

f:id:magayengineer:20160407233238p:plain

f:id:magayengineer:20160407233241p:plain

f:id:magayengineer:20160407233244p:plain

このアルゴリズムのいいところは、なんといっても”簡単”であること。 遺伝的アルゴリズム等の知識がなくても、2〜3分あれば、きっとほとんどの人が理解できると思う。8年ぶりぐらいに触ってみたけど、すんなり再理解できた。

PSOアルゴリズム

■数式
PSOの数式は以下で表される。 要は、粒子は移動するんだけど、その時に現在の速度による影響と、その粒子の今まで見つけたベストの場所と、全体で今まで見つけたベストの場所を勘案しながら探索する。 こうすることで今まで見つけたベストの場所に全ての粒子が一直線で進まなくなり、 今まで見つけたベストの場所の周辺等をいい感じに探索することができる。


x = x + v


v = wv + c_1 r_1 (pbx-x) + c_2 r_2 (gbx-x)


x:粒子の現在値


v:現在の速度


w:慣性定数。次の速度を計算する際に現在の速度をどれだけ重視するか


r_1:0から1の乱数


r_2:0から1の乱数


c1:定数。パーソナルベストをどれだけ重視するか


c2:定数。グローバルベストをどれだけ重視するか


pbx:パーソナルベストの位置。つまり、その粒子が今までに見つけた最も良い位置。


gbx:グローバルベストの位置。つまり、全体で今までに見つけた最も良い位置。

■計算手順
具体的な計算手順は以下の通り。

  1. 全ての粒子の現在位置xとvをランダム値で初期化する。
  2. pbxは、各粒子の現在位置xで初期化
  3. gbxは、現在位置xの中で最も良い位置で初期化(最も良い位置は評価関数次第)
  4. 以下ループ。適当な回数で打ち切る。
    1. 前述の式で全粒子の現在位置更新。
    2. 評価関数で全粒子の現在位置を評価。
    3. パーソナルベストより評価が良くなった粒子はパーソナルベストを更新。
    4. グローバルベストより評価が良くなった粒子がいたらグローバルベストを更新。
    5. 各粒子の速度更新。

実装

PSOの粒子を表すクラス。 共通ロジック(座標と速度の更新)の実装と、I/Fの定義。 評価関数ごとに、このクラスのサブクラスを作成し、 評価関数ごとの都合によって変わる部分を実装する。

class BaseParticle:
    """
    PSOの基礎クラス
    """

    def __init__(self, w, x, v, c1, c2):
        """
        コンストラクタ
        :param w: 慣性定数
        :param x: 現在地(numpy.ndarray)
        :param v: 速度  (numpy.ndarray)
        :param c1:群のうちで良い位置に向かう粒子の割合
        :param c2:群のうちで良い位置に向かう粒子の割合
        :return 粒子のインスタンス
        """
        self._x = x
        self._v = v
        self._w = w
        self._c1 = c1
        self._c2 = c2

        # パーソナルベストを初期化
        self._p_best_x = x.copy()
        self._p_best_score = self.eval()

    def update(self, g_best_x):
        """
        粒子の更新を行う
        :param g_best_x:グローバルベスト
        :return:void
        """
        # 粒子の位置を更新
        self._x = self._x + self._v

        # 粒子の位置を補正
        self.clip_x()

        # 粒子の速度を更新
        r1 = np.random.rand()
        r2 = np.random.rand()
        # v = wv + c1r1(px - x) * c2r2 (gp - x)
        self._v = self._w * self._v + self._c1 * r1 * (self._p_best_x - self._x) + self._c2 * r2 * (g_best_x - self._x)

        # 速度を補正
        self.clip_v()

        # 評価
        score = self.eval()

        # パーソナルベスト更新
        if not self.is_better_than(score):
            self._p_best_score = score
            self._p_best_x = self._x.copy()

    @property
    def x(self):
        """
        :return: 現在の位置
        """
        return self._x

    @property
    def p_best_x(self):
        """
        :return: パーソナルベストの位置
        """
        return self._p_best_x

    @property
    def p_best_score(self):
        """
        :return:パーソナルベストの値
        """
        return self._p_best_score

    def eval(self):
        """
        評価。ここに評価関数を記述する。
        :return:評価値
        """
        pass

    @classmethod
    def create_particle(cls, w, x, v, c1, c2):
        """
        粒子作成用のファクトリメソッド
        :param w: 慣性定数
        :param x: 現在地(numpy.ndarray)
        :param v: 速度  (numpy.ndarray)
        :param c1:群のうちで良い位置に向かう粒子の割合
        :param c2:群のうちで良い位置に向かう粒子の割合
        :return 粒子のインスタンス
        """
        pass

    def is_better_than(self, score):
        """
        パーソナルベストが、引数scoreより良いか判定
        :param score:比較対象の値
        :return:パーソナルベストのほうが良ければtrue
        """
        pass

    def clip_x(self):
        """
        位置を補正する
        :return: void
        """
        pass

    def clip_v(self):
        """
        速度を補正する
        :return: void
        """
        pass

    @staticmethod
    def show_animation(animation_data):
        """
        最適化の過程をアニメーション表示する
        :param animation_data: アニメーション用データ
        :return: void
        """
        pass

各粒子の初期化、位置・速度・グローバルベストの更新制御処理

def calc(particle_class, times, num_of_particle, show_animation=False):
    """
    計算実行
    :param particle_class: 粒子のクラス
    :param times: 何回試行するかするか
    :param num_of_particle: 粒子数
    :param show_animation:グラフ表示有無
    :return: void
    """

    # グローバルベスト
    global_best_x = None
    global_best_score = None

    # 粒子郡
    particles = []

    if show_animation:
        animation_data = AnimationData()

    # 粒子初期化
    for n in range(num_of_particle):

        particle = particle_class.create_particle()
        particles.append(particle)

        if n == 0:
            global_best_x = particle.p_best_x
            global_best_score = particle.p_best_score
        else:
            if particle.is_better_than(global_best_score):
                global_best_score = particle.p_best_score
                global_best_x = particle.p_best_x

    # アニメーション表示用データ準備
    if show_animation:
        animation_data.add_best_particle_position_data(global_best_x, global_best_score)
        animation_data.add_particle_position_data(particles)

    for t in range(times):
        # 粒子更新
        for n, particle in enumerate(particles):
            particle.update(global_best_x)
            if particle.is_better_than(global_best_score):
                global_best_score = particle.p_best_score
                global_best_x = particle.p_best_x

        # アニメーション表示用データ準備
        if show_animation:
            animation_data.add_best_particle_position_data(global_best_x, global_best_score)
            animation_data.add_particle_position_data(particles)

        print "t =", t, "x =", global_best_x, "score =", global_best_score
    print "t =", t, "x =", global_best_x, "score =", global_best_score

    # graph表示
    if show_animation:
        particle_class.show_animation(animation_data)

最適解を求める対象の関数

Rastrigin Function


Z = 10n + \sum_i^n(x_i^2 - 10cos(2\pi x_i))


探索範囲:−5.12≦x_i≦5.12


最適解:f(0, 0, 0...)=0

n=2のときの外観は以下。 f:id:magayengineer:20160407233249p:plain

Rastrigin関数の最適解を求める粒子クラス

class RastriginParticle(BaseParticle):
    """
    Rastrigin関数
    """

    w = 0.9
    c1 = 0.9
    c2 = 0.9

    def eval(self):
        x = self._x
        x1 = x[0]
        x2 = x[1]
        return 10 * 2 + (x1 * x1 - 10 * np.cos(2 * np.pi * x1)) + (x2 * x2 - 10 * np.cos(2 * np.pi * x2))

    @classmethod
    def create_particle(cls):
        x = np.array([5.12 * 2 * np.random.rand() - 5.12, 5.12 * 2 * np.random.rand() - 5.12])
        v = np.array([5.12 * 2 * np.random.rand() - 5.12, 5.12 * 2 * np.random.rand() - 5.12])
        return RastriginParticle(cls.w, x, v, cls.c1, cls.c2)

    def is_better_than(self, score):
        if self._p_best_score < score:
            return True
        else:
            return False

    def clip_x(self):
        self._x = np.clip(self._x, -5.12, 5.12)

    def clip_v(self):
        self._v = np.clip(self._v, -5.12, 5.12)

計算結果

メイン関数。 30粒子で30ループさせてみる。

if __name__ == "__main__":
    calc(RastriginParticle, 100, 50, True)

無事に17回目のループで最適解を見つけた模様。

試行回数,   粒子の位置,                粒子の評価値
t = 0 x = [-1.05704654  0.79439026] score = 9.63082872935
t = 1 x = [ 0.86879518  0.85701424] score = 8.47060907598
t = 2 x = [-0.07911788  0.06427271] score = 2.02515850631
t = 3 x = [-0.07911788  0.06427271] score = 2.02515850631
t = 4 x = [-0.07911788  0.06427271] score = 2.02515850631
t = 5 x = [-0.07911788  0.06427271] score = 2.02515850631
t = 6 x = [-0.07911788  0.06427271] score = 2.02515850631
t = 7 x = [-0.07472074 -0.03142844] score = 1.28289421909
t = 8 x = [-0.07472074 -0.03142844] score = 1.28289421909
t = 9 x = [-0.07472074 -0.03142844] score = 1.28289421909
t = 10 x = [-0.03988766 -0.01807089] score = 0.378723517767
t = 11 x = [-0.03988766 -0.01807089] score = 0.378723517767
t = 12 x = [-0.03212093  0.01240108] score = 0.234496246395
t = 13 x = [-0.03212093  0.01240108] score = 0.234496246395
t = 14 x = [-0.03212093  0.01240108] score = 0.234496246395
t = 15 x = [-0.03212093  0.01240108] score = 0.234496246395
t = 16 x = [-0.02446902 -0.01417394] score = 0.158382181234
t = 17 x = [ 0.  0.] score = 0.0
t = 18 x = [ 0.  0.] score = 0.0
t = 19 x = [ 0.  0.] score = 0.0
t = 20 x = [ 0.  0.] score = 0.0
t = 21 x = [ 0.  0.] score = 0.0
t = 22 x = [ 0.  0.] score = 0.0
t = 23 x = [ 0.  0.] score = 0.0
t = 24 x = [ 0.  0.] score = 0.0
t = 25 x = [ 0.  0.] score = 0.0
t = 26 x = [ 0.  0.] score = 0.0
t = 27 x = [ 0.  0.] score = 0.0
t = 28 x = [ 0.  0.] score = 0.0
t = 29 x = [ 0.  0.] score = 0.0
t = 29 x = [ 0.  0.] score = 0.0

アニメーション表示すると

こんな感じ。赤い点がその時点でのグローバルベスト。 各粒子がグローバルベストを中心に探索しているのが見て取れる。

f:id:magayengineer:20160407233313g:plain

ソースコード全文

#!/usr/bin/python
# -*- coding: utf-8 -*-
import numpy as np
import math
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import matplotlib.animation as animation


class BaseParticle:
    """
    PSOの基礎クラス
    """

    def __init__(self, w, x, v, c1, c2):
        """
        コンストラクタ
        :param w: 慣性定数
        :param x: 現在地(numpy.ndarray)
        :param v: 速度  (numpy.ndarray)
        :param c1:群のうちで良い位置に向かう粒子の割合
        :param c2:群のうちで良い位置に向かう粒子の割合
        :return 粒子のインスタンス
        """
        self._x = x
        self._v = v
        self._w = w
        self._c1 = c1
        self._c2 = c2

        # パーソナルベストを初期化
        self._p_best_x = x.copy()
        self._p_best_score = self.eval()

    def update(self, g_best_x):
        """
        粒子の更新を行う
        :param g_best_x:グローバルベスト
        :return:void
        """
        # 粒子の位置を更新
        self._x = self._x + self._v

        # 粒子の位置を補正
        self.clip_x()

        # 粒子の速度を更新
        r1 = np.random.rand()
        r2 = np.random.rand()
        # v = wv + c1r1(px - x) * c2r2 (gp - x)
        self._v = self._w * self._v + self._c1 * r1 * (self._p_best_x - self._x) + self._c2 * r2 * (g_best_x - self._x)

        # 速度を補正
        self.clip_v()

        # 評価
        score = self.eval()

        # パーソナルベスト更新
        if not self.is_better_than(score):
            self._p_best_score = score
            self._p_best_x = self._x.copy()

    @property
    def x(self):
        """
        :return: 現在の位置
        """
        return self._x

    @property
    def p_best_x(self):
        """
        :return: パーソナルベストの位置
        """
        return self._p_best_x

    @property
    def p_best_score(self):
        """
        :return:パーソナルベストの値
        """
        return self._p_best_score

    def eval(self):
        """
        評価。ここに評価関数を記述する。
        :return:評価値
        """
        pass

    @classmethod
    def create_particle(cls, w, x, v, c1, c2):
        """
        粒子作成用のファクトリメソッド
        :param w: 慣性定数
        :param x: 現在地(numpy.ndarray)
        :param v: 速度  (numpy.ndarray)
        :param c1:群のうちで良い位置に向かう粒子の割合
        :param c2:群のうちで良い位置に向かう粒子の割合
        :return 粒子のインスタンス
        """
        pass

    def is_better_than(self, score):
        """
        パーソナルベストが、引数scoreより良いか判定
        :param score:比較対象の値
        :return:パーソナルベストのほうが良ければtrue
        """
        pass

    def clip_x(self):
        """
        位置を補正する
        :return: void
        """
        pass

    def clip_v(self):
        """
        速度を補正する
        :return: void
        """
        pass

    @staticmethod
    def show_animation(animation_data):
        """
        最適化の過程をアニメーション表示する
        :param animation_data: アニメーション用データ
        :return: void
        """
        pass


class XxYyParticle(BaseParticle):
    """
    Z = X * X + Y * Yの最適化を行うための粒子クラス
    """
    w = 0.9
    c1 = 0.9
    c2 = 0.9

    def eval(self):
        x = self._x
        return x[0] * x[0] + x[1] * x[1]

    @classmethod
    def create_particle(cls):

        x = np.array([200 * np.random.rand() - 100, 200 * np.random.rand() - 100])
        v = np.array([20 * np.random.rand() - 10, 20 * np.random.rand() - 10])
        return XxYyParticle(cls.w, x, v, cls.c1, cls.c2)

    def is_better_than(self, score):
        if self._p_best_score < score:
            return True
        else:
            return False

    def clip_x(self):
        self._x = np.clip(self._x, -100, 100)

    def clip_v(self):
        self._v = np.clip(self._v, -10, 10)

    @staticmethod
    def show_animation(graph_data):
        # 関数の外観を書く ######################################
        x = np.arange(-100, 100, 10)
        y = np.arange(-100, 100, 10)

        mesh_x, mesh_y = np.meshgrid(x, y)
        mesh_z = mesh_x * mesh_x + mesh_y * mesh_y

        show_3d_animation(mesh_x, mesh_y, mesh_z, graph_data)


class MatyasParticle(BaseParticle):
    """
    Matyasの最適化を行うための粒子クラス
    """
    w = 0.9
    c1 = 0.9
    c2 = 0.9

    def eval(self):
        x = self._x
        return 0.26*(x[0] * x[0] + x[1] * x[1])-0.48*x[0]*x[1]

    @classmethod
    def create_particle(cls):

        x = np.array([20 * np.random.rand() - 10, 20 * np.random.rand() - 10])
        v = np.array([2 * np.random.rand() - 1, 2 * np.random.rand() - 1])
        return MatyasParticle(cls.w, x, v, cls.c1, cls.c2)

    def is_better_than(self, score):
        if self._p_best_score < score:
            return True
        else:
            return False

    def clip_x(self):
        self._x = np.clip(self._x, -10, 10)

    def clip_v(self):
        self._v = np.clip(self._v, -10, 10)

    @staticmethod
    def show_animation(graph_data):
        # 関数の外観を書く ######################################
        x = np.arange(-15, 15, 0.5)
        y = np.arange(-15, 15, 0.5)

        mesh_x, mesh_y = np.meshgrid(x, y)
        mesh_z = 0.26 * (mesh_x * mesh_x + mesh_y * mesh_y) - 0.48 * mesh_x * mesh_y
        show_3d_animation(mesh_x, mesh_y, mesh_z, graph_data)


class LeviFunctionParticle(BaseParticle):
    """
    LeviFunction関数
    """
    w = 0.9
    c1 = 0.9
    c2 = 0.9

    def eval(self):
        x = self._x
        x1 = x[0]
        x2 = x[1]
        a = math.pow(np.sin(3 * np.pi * x1), 2)
        b = math.pow(x1 - 1, 2) * (1 + math.pow(np.sin(3 * np.pi * x2), 2))
        c = math.pow(x2 - 1, 2) * (1 + math.pow(np.sin(2 * np.pi * x2), 2))
        return a + b + c

    @classmethod
    def create_particle(cls):
        x = np.array([20 * np.random.rand() - 10, 20 * np.random.rand() - 10])
        v = np.array([20 * np.random.rand() - 10, 20 * np.random.rand() - 10])
        return LeviFunctionParticle(cls.w, x, v, cls.c1, cls.c2)

    def is_better_than(self, score):
        if self._p_best_score < score:
            return True
        else:
            return False

    def clip_x(self):
        self._x = np.clip(self._x, -10, 10)

    def clip_v(self):
        self._v = np.clip(self._v, -10, 10)

    @staticmethod
    def show_animation(graph_data):
        # 関数の外観を書く ######################################
        x = np.arange(-10, 10, 0.25)
        y = np.arange(-10, 10, 0.25)

        mesh_x, mesh_y = np.meshgrid(x, y)
        a = np.power(np.sin(3 * np.pi * mesh_x), 2)
        b = np.power(mesh_x - 1, 2) * (1 + np.power(np.sin(3*np.pi*mesh_y), 2))
        c = np.power(mesh_y - 1, 2) * (1 + np.power(np.sin(2*np.pi*mesh_x), 2))
        mesh_z = a + b + c

        show_3d_animation(mesh_x, mesh_y, mesh_z, graph_data)


class RastriginParticle(BaseParticle):
    """
    Rastrigin関数
    """

    w = 0.9
    c1 = 0.9
    c2 = 0.9

    def eval(self):
        x = self._x
        x1 = x[0]
        x2 = x[1]
        return 10 * 2 + (x1 * x1 - 10 * np.cos(2 * np.pi * x1)) + (x2 * x2 - 10 * np.cos(2 * np.pi * x2))

    @classmethod
    def create_particle(cls):
        x = np.array([5.12 * 2 * np.random.rand() - 5.12, 5.12 * 2 * np.random.rand() - 5.12])
        v = np.array([5.12 * 2 * np.random.rand() - 5.12, 5.12 * 2 * np.random.rand() - 5.12])
        return RastriginParticle(cls.w, x, v, cls.c1, cls.c2)

    def is_better_than(self, score):
        if self._p_best_score < score:
            return True
        else:
            return False

    def clip_x(self):
        self._x = np.clip(self._x, -5.12, 5.12)

    def clip_v(self):
        self._v = np.clip(self._v, -5.12, 5.12)

    @staticmethod
    def show_animation(graph_data):
        # 関数の外観を書く ######################################
        x = np.arange(-5.12, 5.12, 0.1)
        y = np.arange(-5.12, 5.12, 0.1)

        mesh_x, mesh_y = np.meshgrid(x, y)
        a = mesh_x * mesh_x - 10 * np.cos(2 * np.pi * mesh_x)
        b = mesh_y * mesh_y - 10 * np.cos(2 * np.pi * mesh_y)
        mesh_z = 10 * 2 + a + b

        show_3d_animation(mesh_x, mesh_y, mesh_z, graph_data)


class AnimationData:
    """
    Animationデータの保持クラス
    """
    def __init__(self):
        """
        コンストラクタ
        :return:
        """
        # 粒子の時系列ごとの座標データ
        self._particles_position_t = []
        # グローバルベストの時系列座標データ
        self._best_particle_position_t = None

    def add_particle_position_data(self, particles):
        """
        時刻tの粒子群の座標をデータに追加する
        :param particles: 時刻tの粒子データ
        :return: void
        """
        ptx = None
        for n, particle in enumerate(particles):
            if n == 0:
                ptx = np.array([particle.x.copy()])
                ptx = np.append(ptx, particle.eval())
            else:
                tmp = np.array([particle.x.copy()])
                tmp = np.append(tmp, particle.eval())
                ptx = np.vstack((ptx, tmp))
        self._particles_position_t.append(ptx)

    def add_best_particle_position_data(self, global_best_position, global_best_score):
        """
        時刻tの粒子群の座標をデータに追加する
        :param global_best_position:時刻tのグローバルベストの座標
        :param global_best_score:時刻tのグローバルベストの値
        :return:void
        """
        if self._best_particle_position_t is None:
            self._best_particle_position_t = np.array([global_best_position.copy()])
            self._best_particle_position_t = np.append(self._best_particle_position_t, global_best_score)
        else:
            tmp = np.array([global_best_position.copy()])
            tmp = np.append(tmp, global_best_score)
            self._best_particle_position_t = np.vstack((self._best_particle_position_t, tmp))

    @property
    def global_best_position(self):
        return self._best_particle_position_t

    @property
    def particle_position_t(self):
        return self._particles_position_t


def show_3d_animation(mesh_x, mesh_y, mesh_z, animation_data):
    """
    最適化の過程をアニメ表示
    :param mesh_x: グラフ描画用のメッシュデータ
    :param mesh_y: グラフ描画用のメッシュデータ
    :param mesh_z: グラフ描画用のメッシュデータ
    :param animation_data:粒子の座標データ(アニメーション用)
    :return:void
    """
    fig = plt.figure()
    ax = Axes3D(fig)
    ax.plot_wireframe(mesh_x, mesh_y, mesh_z, color='#B8F28C')
    ax.view_init(elev=75, azim=-50) #カメラ位置調整

    # アニメーションデータ作成

    ims = []
    gbp = animation_data.global_best_position
    for i in range(len(animation_data.particle_position_t)):
        pts = animation_data.particle_position_t[i]
        tim = ax.scatter3D(pts[:, 0], pts[:, 1], pts[:, 2],  c='black')
        gim = ax.scatter3D(gbp[i, 0], gbp[i, 1], gbp[i, 2], s=100, c='red')
        ims.append([tim, gim])

    # アニメーション作成 ※なぜか左辺を作らなきゃ動かないので注意
    ani = animation.ArtistAnimation(fig, ims, interval=1000, repeat_delay=10000)

    # グラフ表示
    plt.show()

    # アニメーション保存
    #ani.save("test.mp4")


def calc(particle_class, times, num_of_particle, show_animation=False):
    """
    計算実行
    :param particle_class: 粒子のクラス
    :param times: 何回試行するかするか
    :param num_of_particle: 粒子数
    :param show_animation:グラフ表示有無
    :return: void
    """

    # グローバルベスト
    global_best_x = None
    global_best_score = None

    # 粒子郡
    particles = []

    if show_animation:
        animation_data = AnimationData()

    # 粒子初期化
    for n in range(num_of_particle):

        particle = particle_class.create_particle()
        particles.append(particle)

        if n == 0:
            global_best_x = particle.p_best_x
            global_best_score = particle.p_best_score
        else:
            if particle.is_better_than(global_best_score):
                global_best_score = particle.p_best_score
                global_best_x = particle.p_best_x

    # アニメーション表示用データ準備
    if show_animation:
        animation_data.add_best_particle_position_data(global_best_x, global_best_score)
        animation_data.add_particle_position_data(particles)

    for t in range(times):
        # 粒子更新
        for n, particle in enumerate(particles):
            particle.update(global_best_x)
            if particle.is_better_than(global_best_score):
                global_best_score = particle.p_best_score
                global_best_x = particle.p_best_x

        # アニメーション表示用データ準備
        if show_animation:
            animation_data.add_best_particle_position_data(global_best_x, global_best_score)
            animation_data.add_particle_position_data(particles)

        print "t =", t, "x =", global_best_x, "score =", global_best_score
    print "t =", t, "x =", global_best_x, "score =", global_best_score

    # graph表示
    if show_animation:
        particle_class.show_animation(animation_data)

if __name__ == "__main__":
    #calc(XxYyParticle, 100, 30, True)
    #calc(MatyasParticle, 10, 30, True)
    #calc(LeviFunctionParticle, 30, 10, True)
    calc(RastriginParticle, 30, 30, True)

参考サイト

https://ja.wikipedia.org/wiki/%E7%B2%92%E5%AD%90%E7%BE%A4%E6%9C%80%E9%81%A9%E5%8C%96 http://qiita.com/tomitomi3/items/d4318bf7afbc1c835dda

Pythonの勉強がてらオセロAIを作ってみる③

はじめに

前回(Pythonの勉強がてらオセロAIを作ってみる① - 俺の備忘録)からの続き

  1. オセロの基本ロジックの実装
  2. 完全ランダムな手で打つAIの実装
  3. 今現在、最もたくさん石が取れる手を選ぶAIの実装 
  4. ちょっとだけオセロの定石を知ってるAIの実装
  5. もうちょっとオセロの定石を知っているAIの実装  <= 本稿はココ
  6. MinMax法で打つAIの実装
  7. AlphaBeta法で打つAIの実装
  8. モンテカルロ法で打つAIの実装
  9. モンテカルロ木探索?で打つAIの実装
  10. (このあたりで機械学習を取り入れたい?)

ちょっとだけオセロの定石を知っているAIの実装

以下の最低限の定石を加味した手を選ぶAIを作ってみた。

  • 4隅を優先して取る
  • 4隅に隣接する場所は避ける

初回(Pythonの勉強がてらオセロAIを作ってみる① - 俺の備忘録)のPlayer.pyを拡張した。

class RandomAiKnowGoodMove(Player):
    """ 最低限の手の良し悪しを知っているAI """

    def next_move(self, board):
        known_good_moves = [[0, 0], [0, 7], [7, 0], [7, 7]]
        known_bad_moves = [[0, 1], [1, 0], [1, 1], [0, 6], [1, 6], [1, 7], [6, 0], [6, 1], [7, 1], [7, 6], [6, 7], [6, 6]]

        all_candidates = ReverseCommon.get_puttable_points(board, self._color)

        # 4隅が取れるなら取る
        good_moves = filter(lambda good_move: good_move in known_good_moves, all_candidates)
        if len(good_moves) > 0:
            return good_moves[random.randint(0, len(good_moves) - 1)]

        # 4隅に隣接する場所は避ける
        not_bad_moves = filter(lambda  not_bad_move: not_bad_move not in (known_good_moves + known_bad_moves), all_candidates)
        if len(not_bad_moves) > 0:
            return not_bad_moves[random.randint(0, len(not_bad_moves) - 1)]

        return all_candidates[random.randint(0, len(all_candidates) - 1)]

戦わせてみる

それぞれのパターンで3000回ずつ戦わせてみた。 ※初回 (Pythonの勉強がてらオセロAIを作ってみる① - 俺の備忘録)のMain.pyを拡張し、30行目から34行目でAIを切り替えて戦わせる

パターン①:
黒(完全ランダムAI):72勝
白(定石知ってるAI):928勝

パターン②:
黒(定石を知っているAI):849勝
白(完全ランダムAI):151勝

パターン③:
黒(最も石を取る手を選ぶAI):89勝
白(定石知ってるAI):911

パターン④:
黒(定石を知っているAI):872勝
白(最も石を取る手を選ぶAI):128勝

やっぱり4隅の影響力は大きいなぁ。。。