ダンボーハック~よつばと!リボルテックダンボーのロボット化計画―第4章― 回路とプログラム ラズベリーパイ研究室

あずまきよひこさんの人気漫画「よつばと!」に登場する、超人気キャラクター「ダンボー」フィギュア「リボルテックダンボー」を改造して本当にロボットにしてみました。(ダンボー自体がロボットという設定ですが…)

フィギュアの大きさの都合上、手は動きませんし、勿論二足歩行もできませんが、その他については出来るだけオリジナルの設定を活かしつつ、機能を追加しました。

この第4章では、これまで作ってきた頭部と胴体を繋ぎ、そこから伸びているジャンパワイヤを Raspberry Pi に繋いでプログラム制御します。

気が向いたらあなたも、レッツダンボーハック!



おススメ!記事
Raspberry Pi 用「HAL」で、
カップラーメン・タイマーを作ってみよう!
ラズパイDIYの決定版! ソケットサーバー「HAL」をご紹介します。

※誠に申し訳ありませんがこちらの電子工作のコンテンツは弊社の実験制作例となっております。十分な安全が保障されているわけではないため、参照や実施は自己責任となってしまいますのでご注意ください。


よつばと!リボルテックダンボーを改造してみました

ダンボーを Raspberry Pi に接続

さて、これでダンボーのパーツが完成したので、Raspberry Pi に接続します。

まず、ダンボーの組み立て。

ダンボー組み立て図

これで、各電子部品から伸びているジャンパワイヤ―をブレッドボードに接続し、その先を Raspberry Pi に接続していきます。ブレッドボードは余裕を持って2つ使いました。

配線図は以下のとおりです。基本は当サイトの「HAL」で利用している当サイト推奨回路です。

ダンボー回路図

推奨回路との相違点は、赤外線受光モジュールが変わったのでコンデンサの容量を4.7μFに、その前の抵抗値130Ωに替えたこと、ステータス用の LED が単色 LED から RGB LED に変わったので抵抗を470Ω 3本に替えたことです。

注意点として、サーボモーターにつながる電池ボックスのマイナスにつながる線は、必ずGNDにも繋いで下さい。これを忘れると、サーボモーターが発熱して故障し、火事の原因にもなりかねません。

サーボモーターを使う上での注意点

Raspberry Pi では、B+ 以降のモデル(B+、2、3)では、PWM 信号という、デジタルで擬似的にアナログを表現する信号が 2 本まで使えます。

これは GPIO 18番と 19番なのですが、GPIO 制御ライブラリである WiringPi では、GPIO の制御に BCM2835 用のライブラリを利用しているそうで(Wiring Pi 参照)、Raspberry Pi の標準音声出力に利用している BCM2835 と共通であるため、サーボモーターの制御用に PWM 信号を利用すると Pi のアナログ端子や HDMI 端子から音声が出力できなくなります。また逆に、サーボモーターを利用できるように GPIO を設定しておくと、音楽を再生すると同時にサーボモーターが不規則に回転してしまいます。

このため、Raspberry Pi にサーボモーターを接続して制御させる場合は、音声出力を USB スピーカーにするか、USB 変換アダプターを利用する、または Bluetooth スピーカーを利用するなどする必要があります。

USB 変換アダプターとしては、次の製品がノイズがほとんど無くおすすめです。

半固定抵抗を使って顔の回転の制御

さて、では顔の制御を行っていきます。

前述のダンボー用回路で、右側に半固定抵抗が2つ設置してあります。これが、ダンボーの顔の水平方向、及び垂直方向の回転をテスト制御するためのマニピュレーターになります。

半固定抵抗はこんなものです。

半固定抵抗|秋月電子通商

抵抗値は10KΩ~50KΩくらいのもので、どれでも構いません。

半固定抵抗は、つまみを回転させることで分圧を行い、出力の電圧を変化させる事ができます。

分圧とは

この半固定抵抗で分圧した電圧値を アナログ/デジタル・コンバーターの MCP3208 で読み取って、その値をサーボモーターの Duty サイクルに変換することで、サーボモーターの回転を制御させます。

注意:サーボモーターを電池ボックスに接続した時に、サーボモーターが常に「ジッジッ…」と音を立てていたり、あるいは変な臭いがするなど、異常がある場合にはすぐに電池ボックスのスイッチを切るか、コードを抜いて下さい。回路が間違っている場合に、このような現象が起き、放置するとサーボモーターが発熱して故障してしまいます。

テストプログラムは以下のとおりです。(ソケットサーバー『HAL』のライブラリを利用しています。このテストプログラムは HAL 内の /apps ディレクトリ内に置くことを想定しています)

サーボのDuty サイクル算出プログラム(HALの組み込みライブラリ)

Servo.php
<?php
/* *
*
* socket server HAL v1.5
*
* (c) 2016 Katsuhiko Miki
*
* */
namespace Feijoa\HAL;

class Servo
{

public static function getDuty($ratio, $min_msec = 0.5, $max_msec = 2.4, $unit_msec = 20)
{
return (int)(1024 * (($min_msec + (($max_msec - $min_msec) * ($ratio / 100))) / $unit_msec));
}

}

サーボモーターリセットプログラム

サーボモーターをデフォルトの状態にするプログラムです。

※HAL::SPICLK, HAL::OUTPUT…といった定数は、HAL の /HAL/setting/HALSetting.php で定義されています。ReadADCクラスも、HAL 組み込みライブラリです。

servo_rest.php
<?php
namespace Feijoa\HAL;

// 必要なライブラリの読み込み
define("__HALROOT", "/var/www/HAL/"); // HAL までのパスを __HALROOT として定義
define("__PROG", "SERVO"); // データベース記録用のメタ情報
require(__HALROOT . "setting/requires.php"); // 必要なライブラリの読み込み

$PWM_VERTICAL = 18;
$PWM_HORIZONTAL = 19;

// GPIO セットアップ
wiringPiSetupGpio();

pinMode($PWM_VERTICAL, HAL::PWM);
pinMode($PWM_HORIZONTAL, HAL::PWM);

pwmSetMode(HAL::PWM_MODE_MS);

pwmSetClock(375); // 50 Hz。ここには 18750/(周波数) の計算値に近い整数を入れる

$duty_horizontal = Servo::getDuty(50, 0.5, 2.4, 20);
pwmWrite($PWM_HORIZONTAL, $duty_horizontal);

$duty_vertical = Servo::getDuty(50, 0.5, 2.4, 20);
pwmWrite($PWM_VERTICAL, $duty_vertical);

usleep(10000);

exit;

上記の Servo クラスは、第1引数で与えた割合を、サーボモーターを制御するための値に変換するためのクラスです。

第1引数にサーボを回転させたい比率、第2引数に比率 0 の時のそのサーボモーターでの最低 HIGH 時間、第3引数に比率 100 の時のそのサーボモーターでの最高 HIGH 時間、第4引数にユニット長を指定します。

上記サンプルでは 0.5ms、2.4ms、20ms を指定していますが、これは SG90 の仕様書 を参考にしています。

今回のダンボーハックで使っているサーボモータ―は別のものですが、仕様書が見当たらなかったため、試しに SG90 の使用で動かしてみた所、特に問題が見受けられませんでしたのでそのまま利用しました。

このリセットプログラムを下記のように実行し、

sudo php -q /var/www/HAL/apps/servo_reset.php

サーボモータの初期値の時に顔が正面を向くよう、サーボモーターの軸とサーボホーンの位置を合わせてください。

サーボモーターの回転テスト

サーボモーターの初期化の方法が分かったら、次はサーボモーターの回転可能値の取得です。

テストプログラムは以下です。

servo.php
<?php
namespace Feijoa\HAL;

// 必要なライブラリの読み込み
define("__HALROOT", "/var/www/HAL/"); // HAL までのパスを __HALROOT として定義
define("__PROG", "SERVO"); // データベース記録用のメタ情報
require(__HALROOT . "setting/requires.php"); // 必要なライブラリの読み込み

$PWM_H = 18;
$PWM_V = 19;

// GPIO セットアップ
wiringPiSetupGpio();

// SPI通信用の入出力を定義
pinMode(HAL::SPICLK, HAL::OUTPUT);
pinMode(HAL::SPIMOSI, HAL::OUTPUT);
pinMode(HAL::SPIMISO, HAL::INPUT);
pinMode(HAL::SPICS, HAL::OUTPUT);

pinMode($PWM_H, HAL::PWM);
pinMode($PWM_V, HAL::PWM);

pwmSetMode(HAL::PWM_MODE_MS);

pwmSetClock(375); // 50 Hz。ここには 18750/(周波数) の計算値に近い整数を入れる

$dutyH = Servo::getDuty(50, 0.5, 2.4, 20);
pwmWrite($PWM_H, $dutyH);

$duty_V = Servo::getDuty(50, 0.5, 2.4, 20);
pwmWrite($PWM_V, $duty_V);

while(true)
{
$val0 = ReadADC::exec(4);
$dutyH = Servo::getDuty($val0, 0.5, 2.4, 20);
pwmWrite($PWM_H, $dutyH);

$val1 = ReadADC::exec(5);
$duty_V = Servo::getDuty($val1, 0.5, 2.4, 20);
pwmWrite($PWM_V, $duty_V);

usleep(200000);

echo "{$dutyH} : {$val0} % / {$duty_V} : {$val1} %\n";
}

これは、半固定抵抗の値を MCP3208 で読み取って、その値をそれぞれ水平、垂直方向の回転用のサーボモーターの回転量に変換するプログラムです。

実際のテスト状況はこちらです。

このようにして、サーボモーターに無理のない可動範囲(%)を調べて下さい。「無理な状態」とは、ボディーやケーブルと干渉して回転できず、サーボモーターが「ジッジッ…」と音を立てたままの状態になることです。この場合、サーボモーターに負荷がかかり続けているので、音がしなくなるまでを可動範囲としてください。

顔認識

サーボモータの可動範囲が分かったら、次は顔認識です。せっかくダンボーの右目にカメラを埋め込んでいるので、人間の顔を認識したらそちらを向く処理を追加しましょう。

人間の顔認識には、OpenCV というオープンソースライブラリを使うのがお手軽で便利です。

まず、OpenCV をインストールできるように、Raspbian の パッケージリストを update します。

sudo apt-get update

update は、OS のパッケージリストの更新です。これを行うことで、最新で最適なパッケージの参照先が更新されます。

この状態で

sudo apt-get upgrade

を行うと、お使いの OS に既にインストールされているプログラムについても全て最新で最適な物に更新されます。

update を行ってから

sudo apt-get install libopencv-dev

を行うと、OpenCV での開発に必要なプログラムがインストールできます。この後で、python用の opencv ライブラリをインストールします。

sudo apt-get install python-opencv

3、4 年前には、PHP でも簡単に OpenCV を利用するためのライブラリが提供されていたのですが、時代の移り変わりは早いもので、今日現在(2016/10/27)簡単に OpenCV を利用するための PHP 用のライブラリがみつかりませんでした。

本サイトの Raspberry Pi の記事は「お手軽」を趣旨としてるので、ここは Python のライブラリを利用することにします。

さて、Python での OpenCV の利用準備が整ったら、後はプログラムです。

まず、Python での顔認識プログラムですが、これは、画像ファイルから顔の位置と大きさを認識するプログラムです。

設置場所は、やはり、/var/www/HAL/apps を想定しています。

hal_eye.py
# -*- coding: utf-8 -*-
import cv2
import sys
# import os
cascade_path = "/var/www/HAL/apps/haarcascade_frontalface_alt2.xml"

devide = 1

color = (255, 255, 255) # color of rectangle for face detection

def face_detect(file):
image = cv2.imread(file)
image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
image_gray = cv2.resize(image_gray, (image.shape[1]/devide, image.shape[0]/devide))

facerect = cascade.detectMultiScale(image_gray, scaleFactor=1.1, minNeighbors=1, minSize=(1, 1))

if len(facerect) > 0:
rect = facerect[0]
cv2.rectangle(image_gray, tuple(rect[0:2]),tuple(rect[0:2]+rect[2:4]), color, thickness=2)

rect *= devide
height, width, channels = image.shape[:3];
print str(rect[0]) + "," + str(rect[1]) + "," + str(rect[2]) + "," + str(rect[3]) + "," + str(width) + "," + str(height)

else:
print "";

return image_gray

if __name__ == '__main__':
param = sys.argv
if (len(param) != 2):
quit()

cascade = cv2.CascadeClassifier(cascade_path)

output_img = face_detect(param[1])

cv2.imwrite('/mnt/ramdisk_hal/hal_detect.jpg', output_img)

上記の

cascade_path = "/var/www/HAL/apps/haarcascade_frontalface_alt2.xml"

ですが、haarcascade_frontalface_alt2.xml は、OpenCV 用の汎用顔認識定義ファイルです。ソースコードをダウンロードすると中に同梱されているようですが、Source Forgeから以下でデウンロード出来るようです。(ただのXMLファイルです)

wget wget -O /var/www/HAL/apps/haarcascade_frontalface_alt2.xml http://opencvlibrary.svn.sourceforge.net/viewvc/opencvlibrary/trunk/opencv/data/haarcascades/haarcascade_frontalface_alt2.xml?revision=128

これで、人間の顔が写っている写真のパスをこの Python プログラムに第一引数として渡せば、顔の部分を認識して白枠で囲い、左上の座標、矩形の大きさ、画像自体の縦横のサイズを標準出力に出力した後、上記プログラムの最後の行にある /mnt/ramdisk_hal/hal_detect.jpg として出力してくれます。

/mnt/ramdisk_hal は、ソケットサーバー『HAL』で利用する汎用 RAM ディスクですので、自由にディレクトリを変えてもらってもかまいません。

なお、この Python用の OpenCV ライブラリでの顔認識は、かなり遅いです。100×100pixcels の画像で 0.5 秒程です。この後、PHP から raspistill コマンドを呼び出してカメラモジュールでの撮影からファイル書き出しまでを行いますが、こちらも 0.5 秒ほどかかるので、トータルで秒間 1 コマ程度のパフォーマンスしか出ないことは予めご了承ください。

Python の顔認識プログラムを PHP から呼び出す

さて、顔認識プログラムの準備が出来たら、これを PHP から呼びます。

Raspberry Pi のカメラモジュールは raspistill コマンドで写真撮影がで出来、-o オプションと -e オプション等で、出力ファイルの設定値を指定できます。

このように写真撮影してファイルを出力し、PHP から 出力ファイル情報を Python に渡して実行します。

hal_eye.php
<?php
namespace Feijoa\HAL;

// 必要なライブラリの読み込み
define("__HALROOT", "/var/www/HAL/"); // HAL までのパスを __HALROOT として定義
define("__PROG", "HAL_EYE"); // データベース記録用のメタ情報
require(__HALROOT . "setting/requires.php"); // 必要なライブラリの読み込み

while(true)
{
exec("sudo raspistill -o /mnt/ramdisk_hal/hal_eye.jpg -t 1 -w 100 -h 100 -e jpg -n");

$value = shell_exec("sudo python /var/www/HAL/apps/hal_eye.py /mnt/ramdisk_hal/hal_eye.jpg");

file_put_contents("/mnt/ramdisk_hal/hal_eye.data", $value);
}

shell_exec() は、Python からの標準出力値を PHP の変数に格納します。その後の file_put_contents() で、変数に格納された情報をそのままファイルとして書き出しています。

この書き出された値を、実際のサーボコントロール用のプログラムで読み込み、サーボモーターを回転させます。

顔認識状況の確認

ところで、これまでのプログラムで本当に顔が認識できているのか、実際に書き出された画像で確認してみたくなります。

/mnt/ramdisk_hal/hal_eye.jpg

に、認識画像が jpeg として書き出されていますので、これを WEB ブラウザでモニタリングしてみましょう。Apache などの WEB サーバーが起動していれば、ブラウザから簡単にこの画像をリアルタイム(といっても、秒間 1 コマ程度)できます。

まず、Apache 等の WEB サーバーのドキュメントルート下に、以下の HTML ファイルを作成します。

eye.html
<html>
<head>
<title>MyCamera1</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
</script>
<script>
var temp = $("<img/>");

setInterval(function(){
$.ajax({
type: "get",
url: "eye.php"
})
.done(function(result){
temp.on("load", function(){
$("#my-camera").attr("src", temp.attr("src"));
});
temp.attr("src", result);
})
}, 500);

</script>
</head>
<body>
<img id="my-camera" src="">
</body>
</html>

次に、上記 html の JavaScript から呼び出される PHP プログラム(画像ファイルを出力する)を作成します。

eye.php
<?php
$img = base64_encode(file_get_contents('/mnt/ramdisk_hal/hal_detect.jpg'));
echo "data:image/jpeg;base64,{$img}";

jQuery の Ajax でも簡単に通信できるよう、画像ファイルを Base64 エンコードして出力しています。

これで、WEB ブラウザから eye.html にアクセスすると、顔認識状況を画像で確認できます。例えば、http://192.168.1.**/eye.html などですね。

※Raspberry Pi をインターネットに公開している場合は、お部屋の中が筒抜けになってしまうので十分注意してください。

顔の回転制御プログラム

顔の回転を制御するプログラムは以下です。

場所はやはり、/var/www/HAL/apps 内を想定しています。かなり長いフラットなプログラムです。

global 宣言を多用していて無駄が多いのがおわかりいただけるでしょう。本来はクラスを作りたいところですが、何をしているのか把握しやすいように、敢えてフラットな設計にしてみました。あくまでもサンプルプログラムという位置づけです。

head_control.php
<?php
namespace Feijoa\HAL;

// 必要なライブラリの読み込み
define("__HALROOT", "/var/www/HAL/"); // HAL までのパスを __HALROOT として定義
define("__PROG", "HEAD_CONTROL"); // データベース記録用のメタ情報
require(__HALROOT . "setting/requires.php"); // 必要なライブラリの読み込み

/**
* 変数定義
*/

// PWM用GPIOピン番号
$PWM_H = HAL::PWM_H; //18;
$PWM_V = HAL::PWM_V; //19;

// サーボモーターの最小・最大duty(ms)
$servo_min = HAL::SERVO_MIN; //0.5;
$servo_max = HAL::SERVO_MAX; //2.4;

// サーボモーターのPWM Period(ms)
$servo_period = HAL::SERVO_PERIOD; //20;


// デフォルトダンボー顔位置率(%)
$now_H = 50;
$now_V = 55;

// 顔認識→顔回転率変換用除算数
$divide_h = 10;
$divide_v = 10;

// 顔回転ステップ数
$step = 1;

// ループ内での顔追跡フラグ
$find_flag = false;

// 顔向け中フラグ
$status_find = false;

// ダンボー顔左右最大回転率
$h_range = 19;

// ダンボ―顔上方向最大回転率
$v_range1 = 6.5;

// ダンボ―顔下方向最大回転率
$v_range2 = 10;

// 顔探索方向
$h_vector = -1;

// 顔探索フラグ
$seek_flag = false;

// LED発光済みフラグ
$led_flag = false;

// 顔探索カウンター(左右1回振りワンセット)
$seek_count = 0;

// 顔探索サーボ回転インターバル(マイクロ秒)
$seek_interval = 50000;

// 顔探索間隔(秒)
$seek_time_distance = 60;

// 次回顔探索開始時間(unixタイムスタンプ)
$seek_weit = time();

// PWM信号実行最低インターバル(マイクロ秒)
$pwm_interval = 20000;

// 顔をデフォルトに戻す場合の顔非検出時間(秒)
$face_reset_interval = 10;

// 顔をデフォルト位置に戻すタイマー
$face_reset_timer = time() + $face_reset_interval;

/*
* プログラム
*/

// GPIO セットアップ
wiringPiSetupGpio();

pinMode($PWM_V, HAL::PWM);
pinMode($PWM_H, HAL::PWM);

pwmSetMode(HAL::PWM_MODE_MS);

pwmSetClock(375); // 50 Hz。ここには 18750/(周波数) の計算値に近い整数を入れる

$duty_horizontal = Servo::getDuty($now_H, $servo_min, $servo_max, $servo_period);
pwmWrite($PWM_H, $duty_horizontal);
$before_duty_h = $duty_horizontal;

$duty_vertical = Servo::getDuty($now_V, $servo_min, $servo_max, $servo_period);
pwmWrite($PWM_V, $duty_vertical);
$before_duty_v = $duty_vertical;

$before_x = (int)(HAL::DANBOARD_VISION_SIZE / 2);
$before_y = (int)(HAL::DANBOARD_VISION_SIZE / 2);
$before_w = 0;
$before_h = 0;

while(true)
{
if(!file_exists(HAL::RAMDISK . "hal_eye.data"))
{
usleep($pwm_interval);
continue;
}

$csv = trim(file_get_contents("/mnt/ramdisk_hal/hal_eye.data"));
if(empty($csv))
{
usleep($pwm_interval);

if($status_find === true && $face_reset_timer < time())
{
reset_face_position();
$status_find = false;
continue;
}

if($seek_weit > time()){ continue; }

//seek();
//usleep($seek_interval * 10);

continue;
}

// 顔認識
$status_find = true;

// 顔をデフォルト位置に戻すタイマー設定
$face_reset_timer = time() + $face_reset_interval;

// 顔認識時LEDをイエローで点灯
if($seek_flag === true && $led_flag === false)
{
$led_flag = true;
flash_led();
}

// シーク開始タイムを延長
$seek_weit = time() + $seek_time_distance;

$data = explode(",", $csv);
$x = (int)$data[0];
$y = (int)$data[1];
$w = (int)$data[2];
$h = (int)$data[3];
$img_w = (int)$data[4]; // 画角横
$img_h = (int)$data[5]; // 画角縦

// 顔向け済みチェック
if($before_x === $x && $before_y === $y && $before_w === $w && $before_h === $h){ continue; }

// 画像上の顔認識中心座標
$x_center = $x + ($w / 2);
$y_center = $y + ($h / 2);

// 画像中心からのオフセットピクセル
$pix_H = ($img_w / 2) - $x_center;
$pix_V = ($img_h / 2) - $y_center + 10;

// 首振り幅を指数関数で求める
$H_range = (int)round(pow(abs($pix_H), 1.65) / 125);

// ダンボー顔回転率(%)
$x_adjust = ($pix_H / $divide_h) * $H_range;
$y_adjust = ($pix_V / $divide_v) * 3;

//echo "\n";

$find_flag = false;

// 水平方向回転
control_H($x_adjust);

// 垂直方向回転
control_V($y_adjust);

// LED発光
if($find_flag === true){ flash_led(); }

// 認識情報保持
$before_x = $x;
$before_y = $y;
$before_w = $w;
$before_h = $h;

usleep($pwm_interval);
}

/**
* 自動で顔を左右に振り、人間の顔を探す
*/
function seek()
{
global $seek_flag;
global $led_flag;
global $h_range;
global $v_range1;
global $v_range2;
global $h_vector;
global $PWM_H;
global $PWM_V;
global $now_H;
global $seek_count;
global $seek_weit;
global $seek_time_distance;
global $servo_min;
global $servo_max;
global $servo_period;
global $seek_interval;

$seek_flag = true;
$led_flag = false;

for($i=0; $i<5; $i++)
{
// 垂直方向
$now_V = 50;
$val_v = max(50 - $v_range2, min(50 + $v_range1, ($now_V + ($v_range1 * 0.75))));
$duty_v = Servo::getDuty($val_v, $servo_min, $servo_max, $servo_period);
pwmWrite($PWM_V, $duty_v);

// 水平方向
$now_H += $h_vector;
$seek_h = max(50 - $h_range, min(50 + $h_range, $now_H));
$duty_h = Servo::getDuty($seek_h, $servo_min, $servo_max, $servo_period);
pwmWrite($PWM_H, $duty_h);
//echo "seek: {$seek_h}% / duty: {$duty_h} \n";

if(abs(50 - $now_H) > $h_range)
{
$h_vector *= -1;
$seek_count++;
if($seek_count > 1)
{
// 顔を正面に戻す
reset_face_position();

$seek_count = 0;
$seek_weit = time() + $seek_time_distance;
break;
}
}
usleep($seek_interval);
}
}

/**
* 自動で顔を中央に戻す
*/
function reset_face_position()
{
global $seek_flag;
global $h_range;
global $h_vector;
global $PWM_H;
global $PWM_V;
global $now_H;
global $servo_min;
global $servo_max;
global $servo_period;
global $seek_interval;

$duty_v = Servo::getDuty(55, $servo_min, $servo_max, $servo_period);
pwmWrite($PWM_V, $duty_v);

$h_vector = -1;
if($now_H <= 50)
{
$h_vector = 1;
}

while(true)
{
if((int)round($now_H) === 50){ $seek_flag = false; break; }

$now_H += $h_vector;
$seek_h = max(50 - $h_range, min(50 + $h_range, $now_H));
$duty_h = Servo::getDuty($seek_h, $servo_min, $servo_max, $servo_period);
pwmWrite($PWM_H, $duty_h);

usleep($seek_interval);
}
return;
}

/**
* 水平方向の顔追従制御
*/
function control_H($x_adjust)
{
global $h_range;
global $h_vector;
global $PWM_H;
global $now_H;
global $step;
global $servo_min;
global $servo_max;
global $servo_period;
global $find_flag;
global $before_duty_h;

// 水平方向
$val_h = max(50 - $h_range, min(50 + $h_range, ($now_H - ($x_adjust / $step))));
$duty_h = Servo::getDuty($val_h, $servo_min, $servo_max, $servo_period);
//echo "val_H: {$val_h} / duty_H: {$duty_h}\n";
if($before_duty_h !== $duty_h){
pwmWrite($PWM_H, $duty_h);
$now_H = $val_h;
$before_duty_h = $duty_h;
$find_flag = true;
}
//echo "h= adjust: {$x_adjust} now_H: {$now_H} duty_H: {$duty_h}\n";
}

/**
* 垂直方向の顔追従制御
*/
function control_V($y_adjust)
{
global $v_range1;
global $v_range2;
global $PWM_V;
global $now_V;
global $step;
global $servo_min;
global $servo_max;
global $servo_period;
global $find_flag;
global $before_duty_v;

// 垂直方向
$val_v = max(50 - $v_range2, min(50 + $v_range1, ($now_V + ($y_adjust / $step))));
$duty_v = Servo::getDuty($val_v, $servo_min, $servo_max, $servo_period);
//echo "val_V: {$val_v} / duty_V: {$duty_v}\n";
if($before_duty_v !== $duty_v){
pwmWrite($PWM_V, $duty_v);
$now_V = $val_v;
$before_duty_v = $duty_v;
$find_flag = true;
}
//echo "v= adjust: {$y_adjust} now_V: {$now_V} duty_V: {$duty_v}\n";
}

/**
* LED発光
*/
function flash_led()
{
exec("sudo php -q ". dirname(__FILE__) ."/../system/executable/status_yellow.php");
}

以下 3 項目が、サーボモーターの回転制御値(%)です。サーボモーターに無理がかかって「ジッジッ…」と音がなり続けてしまう場合は、下記の数値を小さくして下さい。

// ダンボー顔左右最大回転率
$h_range = 19;

// ダンボ―顔上方向最大回転率
$v_range1 = 6.5;

// ダンボ―顔下方向最大回転率
$v_range2 = 10;

このプログラムはすこし複雑(難しいではなく、単に複雑である)事をしていますが、やっている事自体はとても簡単です。

要所は下の 3 行です。

$seek_h = max(50 - $h_range, min(50 + $h_range, $now_H));
$duty_h = Servo::getDuty($seek_h, $servo_min, $servo_max, $servo_period);
pwmWrite($PWM_H, $duty_h);

1 行目で、最大値・最小値以内の回転率を求め、2 行目でその値をサーボモーターで有効な値のデジタル値に変換し、3 行目で指定の GPIO ピンから出力する PWM 信号の値を決めます。

プログラムの前半部分では file_get_contents("/mnt/ramdisk_hal/hal_eye.data") で、Python で認識した顔情報(座標、矩形の大きさ、画像サイズ)を取得し、その値に合わせて顔の回転を行う命令を後半の各関数に指示する、という流れです。

まとめ

以上のような感じで、一通りの動作確認が行えるはずです。

あとは、実際にダンボーにどのような動作や認識をさせるか、という段階になってきますので、ソケットサーバー『HAL』を使って、自由な機能追加を行ってみて下さい。

今後公開されるソケットサーバー『HAL』には、ダンボー用のプログラムも同梱される予定です。

なお、今回の顔認識で OpenCV が導入できました。OpenCV を使うと、raspistill コマンド無しで、カメラモジュールから直接画像の取得ができそうです。

ですから、顔認識速度についてはまだまだ改良の余地が十分あることを最後に申し上げておきます。

ちなみに、今回のダンボーハックの舞台裏はこんな感じです。病院の集中治療室に入院した患者さんみたいになってますね…

ダンボーの裏側



ご購入

もう少々お待ち下さい。

体験版

version 1.5 から、IPアドレス制限が無くなり、ライセンス制になりました。ライセンスされていない場合、起動後30時間後に自動的に HAL を終了します。

体験版のインストーラー・スクリプトをダウンロード インストーラー・スクリプト
SHA-1: dd2a390b4f0f8c15301eaee23cd92bd5e831da91
※インストール方法については ソケットサーバー「HAL」の概要 (version 2.0対応版)~導入方法 を御覧ください。

体験版パッケージ
HAL_trial_2_0.tar.gz | version 2.0 | SHA-1: ce31802a5b423a9b0d73721a3d43e0585b009a6f
試用期限 2017/1/31 まで

この記事へのコメント

※現在コメントはMarkdown記法が強制です。>>Markdown の書き方


HAL(体験版)について

お名前:大林文明

2017-08-22 15:35:52

ラズベリーパイのIPアドレスが133.89.128.84なので今リリースのHALを使用したいなのでどうしたらいいです?

この記事に返信

このコメントに返信