Take’s diary

Macとマイコンに関すること--ワクワクの製作日記

Intel Edisonを遠隔操作 「Edisonがボール認識!!」 (Rapiro用Edisonキャリアボード+SimpleCV+flask)

 ESP-WROOM-02の記事が続いたので、今日はまたEdisonに戻って、ESPには到底できないようなことを実行してみました。今回はWebでEdisonを操作したり、ボール認識させた動画を配信させたりしてみます。

 f:id:TAKEsan:20160309194421j:plain:w280 f:id:TAKEsan:20160309194420j:plain:w280
左は白い円を認識している。右はOMURON非接触赤外線センサーで16か所の温度を測っている。切り替えはボタン操作)
          
      実際に動かしている動画です。画像にモアレができてますけど、動作は結構早い

 Edisonからの動画配信は、過去の記事により、SimpleCVで実現できています。
takesan.hatenablog.com
OpenCVが簡単に実行できたり、図形を合成してMjpeg配信できたりしてかなり面白いのですが、これだけではEdisonからの一方通行に過ぎません。今回は、クライアント側で動画を表示させながらEdisonを操作することに挑戦してみます。

まず材料

 Edison,USB外部機器が使えるEaglet、i2cセンサー(今回は制御が簡単なOMURON非接触赤外線センサー。電源を5Vから取れば一応動きます)、安いWebCamera、LED(フローラ用のだと抵抗付きなので実験用に最適)今回の材料はカメラ以外全部スイッチサイエンスで手に入れてます。接続は赤外線センサーが4本(SDA,SCL,GND,5V)。LEDは当然2本(+側はラピロボードのIO12(mraaでは20番に指定),GND)。カメラはUSBに差し込むだけです。
f:id:TAKEsan:20160307213444j:plain:w300f:id:TAKEsan:20150809111402j:plain:w200
      左:Rapiro用Edisonキャリアボード 右:OMURON非接触赤外線センサー

f:id:TAKEsan:20160307213445j:plain:w200f:id:TAKEsan:20160307213446j:plain:w300
            floral用抵抗付きLED
 今回はPython主体なので実験用に最適な、Rapiro用Edisonキャリアボードを使いました。理由は、「適度な大きさ、レベルシフターが必要無い(信号は全て3.3V出力)、電源出力が3,3Vと5V、カメラが使える、デジタル・アナログピンが使える。」です。そしてSDカードも外部記憶ディスクとして使えるのもGood!!。とりあえずピンヘッダを半田付けしてから。  
          f:id:TAKEsan:20160307213447j:plain:w300
 Edisonは他のEagletボードに載せ替えOKなので、小さくまとめたいのならカメラが使える、Intel Edison Block - Base+αとかHenryとかが最適です。Edison本体の差し替えは、コネクタの保証範囲である30回以下になっていますが、慎重に抜き差しすれば(私のは30回の保証範囲を軽く超えている)なんとか持つようです。ただし、くれぐれもroot以下のバックアップを怠りなく。そんな場合でもこのボードは、面倒な設定が不要で、/media/sdcardにコピーするだけ。ただし、i2c6が使えないのでArduinoIDEからだとi2cが操作できませんが、デジタルが6本(内PMW2本)****I2S2_FS-->10、I2S2_CLK-->13、I2S2_TXD-->11、I2S2_RXD-->12、IO12-->3 PWM、IO13-->5PWM****使用可能、さらにシリアルが使えます。Arduinoに慣れている方は、私の1月8日の記事とは違う発想で、プロセス間通信を使って時間が掛かる処理をArduino側で実行させるといった様な面白そうなことができそうです。このボードはEDISON FOR PYTHON ボードといった方が良いかも。Pythonからはmraaで全ての端子が使えますが、ピン番号は回路図を見れば分かります。ちょと分かりにくいのですが、ラピロボードの回路図からEdisonのGPIOピン名を調べて、Intel(R) breakout boardの対応表からmraa番号を探します。(例:IO12->GP12->20。mraaの対応表中Pinmode1によりmraa20番はPWMであることが分かります)
https://s3-ap-northeast-1.amazonaws.com/switch-science.public/schematic/Edison_Carrier_Board_for_Rapiro/Edison_Carrier_Board_for_Rapiro%282%29.pdf
mraa: Intel Edison

まずしなければいけないこと

 目標は、ボタンとスライダ操作それに動画の表示Python上で実現するために、いろいろ調べて、簡単そうな「flask 」を使うことにしました。とにかくflaskを使ってSimpleCVのMjpeg動画を送れれば完成したようなもんです。
 じゃーHtmlの知識もないのにどうしたらいいの? 例によってサンプルを徹底的に探しました。その中でも一番簡単に実現できそうなそうなサンプルがこれ。
python - How to send a pygame image over websockets? - Stack Overflow
 この場合はpythonOpencv=cv2を使って、SimpleCVの画像を送ってるようなのですけど、cv2(PythonOpencv)独自のCamera関数は使えないみたい。SimpleCVのCamera関数は簡単に認識するのに不思議。どうやらSimpleCVのCamera関数は、Pygameを使ってる様なので、原因を探るのはまた今度にします(多分Pygameインストールの時のvideo.hあたり)。
 まーそんなわけで、2日かかってSimpleCVのMjpeg動画がflaskで表示できるようになりました。カメラ表示部分の変換ソースは、bottol他のライブラリでも使えそうです。SimpleCV標準コマンドで画像配信するより、スピードが多少早いような気がします。変換部分が出来上がってみるとまー簡単だこと!!。Html部分を省いて、たった5行で出来ちゃいました。とても2日掛かりの労作とは思えませんよね。Html部分も私の能力に合わせて最小限にしました(詳しい方は笑ってください)。

あっそうだ。この「Eaglet」ラピロの頭だったんだ。

 このボードをラピロの頭に入れると、画像の分析、顔認識、さらには画像の加工までできて動画配信可能。おまけに配信先からはラピロを自由に動せる。アイディアがどんどん膨らみます。pi3が発売されたそうですがpi3にすればもっと早くなる。でも電源が!!てな時、Edisonは未だに強〜い味方です。
          f:id:TAKEsan:20160307213443j:plain:w300
 ただ、今回のスライダー操作ではにタイムラグがあるので、さらなる改良が必要。

SimpleCvについて

 この頃Edison向きにそこそこのスピードで画像処理するコツがつかめてきました。画像を分析する(例えば赤や青や白のボールを判断、顔認識するなど)には、よほど細かいものでない限り180X120ドット程度で十分なんです。でもそのままではWeb画面で表示すると画像が甘いので、実用性とEdisonの処理能力を考慮した限界である倍の解像度、すなわち320X240が許容範囲。これを内部的に半分にして処理すると、ロボットなどに使える実用スピードになります。最初のサンプル動画を見てください。これPythonインタプリタですよ。今回この部分もソースに書きましたので是非参考にしてみてください。

ソースの実行について

 まずflaskのインストール。その前にyoctoをクリーンインストールしてSimpleCVをインストールしておくこと。それから
pip install flask
今回追加するライブラリのインストールは、これだけです。

 そしてflaskの要求する実行用のディレクトリを作ります
◯◯◯◯◯/test.py
◯◯◯◯◯/templates/index.html
◯◯◯◯◯は好みのディレクトリとする。

 実行方法は、python main.py >/dev/null 2>&1 でプログラム起動。Mac又はiPhoneのサファリから ◯◯◯◯◯.local:5000 で画面が表示されます。
(>/dev/null 2>&1 の分は、普通にPythonを実行させると、main.pyの実効後SimpleCVからのlogが際限なく画面に表示されるので、余計な表示をストップさせるため)

test.py index.htmlは、以下のソース。
知識のある方はjavascliptを使うなりどうぞご自由に。

以下test.py

#!/usr/bin/env python
import gc
from flask import Flask, render_template, Response,request, redirect
from SimpleCV import *
from cStringIO import StringIO
import mraa
import time
gc.disable()
#************  UI   **************
atai="0.5"
show="Show"
on="ON"
#************PWM-LED**************
x = mraa.Pwm(20)
x.period_us(700)
x.enable(True)
#************  i2c  **************
D6T_addr = 0x0A
D6T_cmd =  0x4C
y = mraa.I2c( 1 )
#***********Main******************

cam=Camera(0,{ "width":320, "height":240 }) 
app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html',pwm="value="+atai,show=show,on=on)
    print atai
def gen(): 
    global show,on
    tdata=[]
    while True:
        if show == "Hide":        #*******Ball find********
            image=cam.getImage() 
            image.drawText("White Ball", 5, 5,(255,255,255),fontsize=30)
            img1 = image.scale(0.5)
            dist=img1.colorDistance((0,0,0))
            segmented=dist.stretch(225,225)
            blobs=segmented.findBlobs()
            if blobs:
                 circles=blobs.filter([b.isCircle(0.3) for b in blobs])
                 if circles:
                      image.drawCircle((circles[-1].x*2,circles[-1].y*2),circles[-1].radius()*2,(255,0,0),2)
        elif on == "OFF":         #********OMURON**********
            image=cam.getImage()
            y.address(D6T_addr)
            y.writeByte(0x4C)
	    z=y.read(35)
            for var in xrange(0, 16):
                   tdata.append((z[(var*2+2)]+(z[(var*2+3)]<<8))/float(10))
            for j in xrange(0,4):
               for i in range(0,4):
                   image.drawText(str(tdata[i+j*4]),20+i*80,15+j*60,(255,94,25),fontsize=30)
            tdata=[]
        else:
            image = cam.getImage()
        fp = StringIO()           #********Mjpeg**********
        image.save(fp, 'JPEG')
        frame=fp.getvalue()
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n\r\n')

@app.route('/vid')
def video_feed():
    return Response(gen(),mimetype='multipart/x-mixed-replace; boundary=frame')

@app.route('/contact', methods=['POST'])
def contact():
    global atai,on,show,x
    if request.method == 'POST':
        if request.form['submit'] == 'Show':
            show="Hide"
            on="ON"
        elif request.form['submit'] == 'Hide':
            show="Show"
       	elif request.form['submit'] == 'OFF':
       	    on="ON"
        elif request.form['submit'] == 'ON':
            on="OFF"
            show="Show"
        elif  request.form['range'] != '300':
            atai=request.form['range']
            xx=float(atai) 
            x.write(xx)
    return redirect('/')

if __name__ == '__main__':
    app.run(host='0.0.0.0')

以下 index.html

<html>
<font face="Chalkduster">
  <head>
    <title>Edison Test!!</title>
  </head>
  <body  onLoad="test()" bgcolor="#E7E8E2">
    <h1>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Edison Test!!</h1>
    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img id="bg" src="{{ url_for('video_feed') }}" width="320" height="240" />
    <form action="/contact" method="post">
      <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;BALL-->&nbsp;<input style="color:#000000;background-color:#CCEEFF 
;font-size:15;width:140px;height:30px" type="submit" name="submit" value={{show}}></p>
      <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TEMP  -->&nbsp;&nbsp;<input 
style="color:#000000;background-color:#00ccff;font-size:15;width:140px;height:30px" type="submit" name="submit" 
value={{on}}></p>
      <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PWM--->&nbsp;&nbsp;<input type="range" name="range"  min="0" max="1" step="0.01" 
{{pwm}}>
         <input style="color:#C000000 ;background-color:#FFEEFF ;font-size:15;width:70px;height:30px" 
type="submit" name="submit" value="Send"></p>
    </form>
  </body>
</font>
</html>

では、また。