ScratchとPythonをつなげて顔認識やメール・Slackを送ったりする話

子どもでも使えるプログラミング言語ScratchとPythonをつなげてScratchの値を送受信。ScratchではできないメールやSlackの送信、画像認識やWeb連携、データをテキスト出力、OSコマンドの実行などをする話。

何ができるの?

  • Pythonで動く何か(結果)をScratchで使いたい
  • Scratchから任意のOSコマンドを実行したい(メールやSlackへ送信、Webからファイルを取得、テキストで出力など)
  • USBカメラによる顔認識や色認識結果をScratchで使いたい
  • 遠隔/ローカルのScratchへPythonの値を送りたい
  • 遠隔/ローカルのScratchからPythonで値を受け取りたい
  • 遠隔/ローカルのPython/ScratchへPython/Scratch/Pyonkeeの値を中継したい
    などなど

工夫次第でいろんな事ができるようになるのでは無いかと思います。

対象の方

  • 親子でプログラミングを楽しみたい(お父ちゃんが凄いのをPythonで作って、Scratchでつながせ凄さを見せつける)
  • Scratchはだいたい分かる。Pythonに挑戦しようと思うが、作りたいものが思いつかない
  • 黒い画面はさほど怖くない
  • google先生に聞いて何となく進められる

対象じゃない方

  • Pythonで何でもできる

環境の準備

Pythonが動く環境が必要です(Scratchと同一PCで無くても良いです)。

Pythonのインストール

今回はRaspberryPiのPython2.7を使いました。

RaspberryPiやMacの場合はPythonが初めから使えるのでそのままで。Windows10ならBash on windowsを有効にしてみると良いのではないかと思います。それ以外の方は… google先生に聞きながらPythonのインストールを頑張ってみて下さい。

python --version
Python 2.7.3

pipのインストール

Pythonのパッケージを良い感じに管理(インストール)してくれるpipを入れます。

raspberry Piやbash on windowsなら、

sudo apt-update
sudo apt-get install python-pip

Macなら、

easy_install pip

pip –versionと打ってエラーが出なければOKです。

$ pip --version
pip 1.1 from /usr/lib/python2.7/dist-packages (python 2.7)

scratchpyのインストール

PythonでScratchの遠隔センサープロトコルを簡単に使うためのライブラリscratchpyをインストールします。

sudo pip install scratchpy

動作テスト

pythonを実行して、import scratch と打ってエラーが出なければOK(Ctrl+Dで終了)。

$ python
Python 2.7.6 (default, Oct 26 2016, 20:30:19)
[GCC 4.8.4] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import scratch
>>> Ctrl+D

 

import scratchでエラーが出なければOK

エラーが出るようであれば、
https://raw.githubusercontent.com/pilliq/scratchpy/master/scratch/scratch.py
をダウンロードして、実行したい場所/ファイルと同じディレクトリに置いて試してみて下さい。

Scratchの遠隔センサーとファイアーウォールの設定

Scratch1.4の遠隔センサー接続を有効にします(2.0では使えません)。「調べる→スライダーセンサーの値→右クリック→遠隔センサー接続を有効にする」を押します。

調べる→スライダーセンサーの値→右クリック→遠隔センサー接続を有効にする

他のPCからScratchが実行されているPCへ値を送りたい場合は、Scratchが入っているPCのファイアーウォールの設定を変更する必要があります。TCPポート42001のみを開ける、Scratchの通信は全て許可すれば良いのですが、一時的に体験/テストするだけであればファイアーウォールを一時的に無効にしても良いかと思います。

ESETの場合。Scratchが入っている側のPCのファイアーウォールを一時的に無効にする。

scratchpyの使い方(基本)

詳しくは上記の公式ページに書いてありますが、これを使うとPythonからScratchへ「センサーの値」と「ブロードキャスト(○○を送る)」を送受信できます。

Python→Scratchへ値を送る

同一若しくは別PCのScratchへPythonの値を送ることができます。下記にセンサーと書いてありますが単に値を送ると思って下さい。数字だけでなく、文字列を送ることも可能です。

下記のコードを編集するには RaspberryPiとMacであればGUIエディタでも大丈夫ですが、Bash on windowsの場合はnanoとかを使ってみて下さい(2017/4現在Bash on Windowsでは日本語は怪しいので打たない感じで)。

# -*- coding: utf-8 -*-

import scratch, time

#192.168.11.20のScratchへ送る場合
s = scratch.Scratch(host='192.168.11.20')
#同一PCのScratchへ送る場合
#s = scratch.Scratch(host='localhost')

s.connect()

a = 0

while True:
    try:
        a = a + 1
        # センサーの値を送る
        s.sensorupdate({'a' : a})
        time.sleep(0.5)
    except KeyboardInterrupt:
        print "Scratch: Disconnected from Scratch"
        break

s.disconnect()

上記を下記のように実行します(終了はCtrl+C)。

python send_sensor.py

これでScrachのaセンサーの値が1づつ増えていきます。

上記を実行するとこんな感じで増えていく

Python→Scratchへブロードキャストを送る

※2017/4月現在、bash on windowsでは何故か動きませんでした。

# -*- coding: utf-8 -*-

import scratch

#192.168.11.20のScratchへ送る場合
s = scratch.Scratch(host='192.168.11.20')
s.connect()

# Hello, Scratch!を送る
s.broadcast('Hello, Scratch!')

# 切断
s.disconnect()

下記で実行。

python send_broadcast.py

このブロック下が動かせる

Scratch→Pythonへ値を受け取る

Scratchのグローバル変数の値が変化したらPythonで値を受け取ることができます。
Scratch側は例えばこんなプログラム。

グローバル変数の値が変わったらPythonに送られる

# -*- coding: utf-8 -*-

import scratch

#192.168.11.20のScratchから受け取る場合
s = scratch.Scratch(host='192.168.11.20')
s.connect()

while True:
    try:
        msg = s.receive()
        print "Scratch: receive:",msg
        # Scratchでの変数名send_msgの値があれば表示
        if 'send_msg' in msg[1]:
            val = msg[1]['send_msg']
            print val
    except KeyboardInterrupt:
        print "Scratch: Disconnected from Scratch"
        break

s.disconnect()

Scratch→Pythonへブロードキャストを受け取る

# -*- coding: utf-8 -*-

import scratch

#192.168.11.20のScratchから受け取る場合
s = scratch.Scratch(host='192.168.11.20')

s.connect()

while True:
    try:
        msg = s.receive()
        print "Scratch: receive:",msg
        if msg[0] == 'broadcast':
            print msg[1]
    except KeyboardInterrupt:
        print "Scratch: Disconnected from Scratch"
        break

s.disconnect()

○○を送るをPythonで受け取れます

Scratch→Pythonへ値かブロードキャストを受け取る

上記のサンプルは一度に複数の値が変化した場合1つしか受け取れませんが、下記なら複数受け取れます。

# -*- coding: utf-8 -*-

import scratch

s = scratch.Scratch(host='192.168.11.20')
s.connect()

def listen():
    while True:
        try:
            yield s.receive()
        except scratch.ScratchError:
            s.disconnect()
            raise StopIteration
        except KeyboardInterrupt:
            print "Scratch: Disconnected from Scratch"
            s.disconnect()
            break

for msg in listen():
    if msg[0] == 'broadcast':
        # ブロードキャストを受け取った時の処理をこの下に書く
        print msg
    elif msg[0] == 'sensor-update':
        # センサーを受け取った時の処理をこの下に書く
        if 'send_msg' in msg[1]:
            val = msg[1]['send_msg']
            print 'send_msg:' + str(val)

値を中継する

Scratch1 – PC(Python) – Scratch2など、Scratchから他のScratchへ値を中継します。上記の例をひっつけただけです。ただ、これはScratch 1.4であれば標準でもMESHの仕組みを使えば可能で、MESHのほうが簡単です。

特定のセンサーやブロードキャストだけを転送したい。受け取った値をPythonで加工してから転送したい時などに使えるのでは無いかと思います。

Meshが有効なScratchの場合 Shift+共有でこの画面が出る。ネットワークに繋がった複数のScratch同士で値の共有が可能。

下記は、scratch1のsend_msgをscratch2のfrom_srcatch1へ転送するサンプル。

# -*- coding: utf-8 -*-

import scratch

# scratch1のIPアドレス
s = scratch.Scratch(host='192.168.11.20')
s.connect()

# scratch2のIPアドレス
s2 = scratch.Scratch(host='localhost')
s2.connect()

def listen():
    while True:
        try:
            yield s.receive()
        except scratch.ScratchError:
            s.disconnect()
            s2.disconnect()
            raise StopIteration
        except KeyboardInterrupt:
            print "Scratch: Disconnected from Scratch"
            s.disconnect()
            s2.disconnect()
            break

for msg in listen():
    if msg[0] == 'broadcast':
        # デバッグ用
        print msg
    elif msg[0] == 'sensor-update':
        if 'send_msg' in msg[1]:
            val = msg[1]['send_msg']
            # デバッグ用
            print 'send_msg:' + str(val)
            # Scratch1から受け取ったsend_msg値を、Scratch2へ送る
            s2.sensorupdate({'from_scratch1' : val})

単なる中継であれば標準機能のMESHを使ったほうが楽ですが…

scratchpyの使い方(応用)

楽しいのはココからです。上記の基本を踏まえ、色々作っていきます。

任意のOSコマンドを実行する

既存のプログラム(バイナリ、Python、他の言語)を使って色々遊びたい場合、OSコマンドを実行できると便利ですよね。実行したいコマンドを受け取って、Pythonのsubprocessで、shell実行をすればOSコマンドを実行できます。

※コマンド次第ではファイルの書き換えや削除など、何でもできてしまうので色々気をつけて下さい。

# -*- coding: utf-8 -*-

import scratch
import subprocess

# ScratchのIPアドレス
s = scratch.Scratch(host='192.168.11.20')
s.connect()

def listen():
    while True:
        try:
            yield s.receive()
        except scratch.ScratchError:
            s.disconnect()
            raise StopIteration
        except KeyboardInterrupt:
            print "Scratch: Disconnected from Scratch"
            s.disconnect()
            break

for msg in listen():
    if msg[0] == 'broadcast':
        # デバッグ用
        print msg
    elif msg[0] == 'sensor-update':
        if 'shell' in msg[1]:
            val = msg[1]['shell']
            cmd = str(val)

            # デバッグ用
            print 'shell:' + str(val)

            # shellコマンドの実行
            p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            stdout_data, stderr_data = p.communicate()
            # 標準、エラー出力をScratchに送り返す
            s.sensorupdate({'shell_stdout' : stdout_data.replace('\r\n',' ')})
            s.sensorupdate({'shell_stderr' : stderr_data.replace('\r\n',' ')})

            # デバッグ用
            print 'stdout:' + str(stdout_data) + 'stderr:' + str(stderr_data)

上記を実行し、グローバル変数shellを変化させるとOSコマンドが実行されます。

pwdコマンド(現在のディレクトリ)を送って、返り値がstdoutに入ったの図

現時刻を取得

気をつけなければならないのは、先にも書きましたが変数に変化が無いとsensor-updateを送らないこと。2回目を送る場合は少し何かを変えるなど工夫が必要です。

連続して同じ値は送れない(値に変更が合った場合のみ送信される仕組み)

export LANG=C;time=`date +'%Y-%m-%d %H:%M:%S'`;echo $time

Scratch 1.4では取得できない現在時刻の例

テキストログを保存

上を少しだけ応用して、現時刻の入ったログを保存。Pi_scratch等を使用してIoT的な何かを作ってログやCSVを残したいという時に使えるのではないかと。

export LANG=C;time=`date +'%Y-%m-%d %H:%M:%S'`;echo $time 1 >> test.log
root@raspberrypi:~/Python# cat test.log 
2017-04-18 14:14:23 1
2017-04-18 14:14:23 2
2017-04-18 14:14:23 3
2017-04-18 14:14:23 4
2017-04-18 14:14:23 5
2017-04-18 14:14:23 6
2017-04-18 14:14:23 7
2017-04-18 14:14:23 8
2017-04-18 14:14:23 9
2017-04-18 14:14:23 10

こんな感じにすると時刻付きのログテキストが残せる

Scratchからメールを送信

ワンライナーで書ければコマンドで動く物はだいたい動きます。改行したい場合はPythonスクリプトで\rとかを改行に置換すると良さそうですがやってません。Scratch→Pythonは日本語も大丈夫でした。

echo 'Scratchからのメール送信テストです。' | mail -s 'test' root

Scratchからメールを送るの図。例は固定ですが、本来は先の○と○をつなげるで良い感じに。

RaspberryPiで予めメールサーバ(postfix)の設定がしてあります

ScratchからSlackへメッセージを送信

コマンドが打てると言うことは、アイデア次第でかなり色々な事ができます。curlの戻り値はstdoutに入るので、他のWebAPI(天気や本など)にもアクセス可能です。

curl -XPOST -d "token=トークンをココへ" -d "channel=#general" -d "text=test from scratch" -d "username=Scratch" "https://slack.com/api/chat.postMessage"

ある程度、長いのでも大丈夫っぽい

Slackにもメッセージが送れる

他にも下記のように、OpenJTalkで喋らせたり、一眼レフカメラに写真を取らせたり、音声認識の結果を知らせたりもできるのでは無いかと思います(宣伝)。

USBカメラで顔認識したらScratchに知らせる

Pythonで書かれたサンプルコードに先程のsensorupdateかbroadcastをはさんであげます。

今回の例だけでなく、Pythonで書かれた何かに下記の数行を入れてあげるだけでScratchから使えるかもしれません。Pythonだけで作られた何かの場合はコードが複雑で、挟む個所も分かりにくいですが、今回のようにメイン(OpenCV)があり、それを利用するようなプログラムの場合、コードも短く、見やすい場合が多い気がします。

顔認識にはopen cvを使います。RaspberryPi以外でのOpenCVの入れ方は… 調べてみて下さい。PythonからOpenCVを使えるようにセットアップする必要があります。

RaspberryPi(debian系)なら↓だけ。あら簡単。

apt-get update
apt-get install libopencv-dev
apt-get install python-opencv

このあたりを見てテストして、顔認識ができるところまでやります。

あとは、顔が見つかった部分で先程のsensorupdateを挟めば良いので↓の感じで。特定色の色認識などもOpenCVを使って同様にできると思います。

#!/usr/bin/python
"""
This program is demonstration for face and object detection using haar-like features.
The program finds faces in a camera image or video stream and displays a red box around them.

Original C implementation by: ?
Python implementation by: Roman Stanchak, James Bowman
"""
import sys
import cv2.cv as cv
from optparse import OptionParser
import scratch
s = scratch.Scratch(host='localhost')
s.connect()

# Parameters for haar detection
# From the API:
        if faces:
            for ((x, y, w, h), n) in faces:
                # the input to cv.HaarDetectObjects was resized, so scale the 
                # bounding box of each face and convert it to two CvPoints
                pt1 = (int(x * image_scale), int(y * image_scale))
                pt2 = (int((x + w) * image_scale), int((y + h) * image_scale))
                cv.Rectangle(img, pt1, pt2, cv.RGB(255, 0, 0), 3, 8, 0)
                print "face!!"
                s.sensorupdate({'face' : 1})
        else:
                s.sensorupdate({'face' : 0})

    cv.ShowImage("result", img)

顔認識されるとScratchへ情報を送る(赤枠はサンプルコード)。pythonで書かれている場合はサンプルコードとかで良い感じにできる場合も多いかと。

ソースコード

ここに上げました。Python慣れていないので、この書き方はイマイチでは?などありましたら、pull requestいただけると幸いです。

git clone https://github.com/koike-moyashi/scratch_python_sample.git

とかすると良いのではなかろうかと。

最後に

これならPythonで全部やっちゃえば… と言う気持ちは分からないでも無いのですが、画面描画するゲームやインタラクティブ作品と組み合わせたり、Pythonやってみたけどhello world以上何を作れば良いのかなぁと思ってる方なんかは徐々にPython触れられる気がしました。

また、ScratchをGUIフロントとして使い、タッチパネルでスイッチ的な何かに使い、裏側ではPythonで動くみたいなのも作れそう。

既存のバイナリプログラムやpythonプログラム、RaspberryPi+Scratchで各種センサーやモータがあれこれできるpi_scratchなどと組み合わせると、楽しい作品を作ることができるのではないかなぁと思いました。

こんなの作ったよとかありましたら、是非コメント欄で教えてください。

このエントリーをはてなブックマークに追加

同一タグの最新記事:

同一カテゴリ(IT)の最新記事:

最近投稿された記事:

他のカテゴリの記事も読んでみる: