サリーの顔を識別させる

ロボット用のcomputerであるraspberrypi3にインストールしたopencvには、人の顔や瞳を自動で認識する識別器がすでに付属している。それは大変便利で、raspberrypiに接続したカメラで、すぐに識別が可能になる。

もし、サリーの顔が識別できれば、ロボットはサリーに向けて自動で近づいたり離れたりできるようになる。これはとても便利だ。しかし、opencv付属の人の顔の識別器では、サリーの顔を識別できないことがわかった。だから、サリーの顔を識別する識別器を自作した。

参考にさせていただいたサイトは、こちらである。

今後、サリー以外の物体認識にも使いたいのでメモしておく。

(1)positive画像とnegative画像:検索画像のダウンロード
フリーのツールはたくさんあるので、なんでもいいと思うが、私は、こちらのを使わせていただいた。どのサイトか、使えないサイトもあったが、特に大きな問題はなかった。このダウンロードソフトを使って、ワードをrobot naoで検索して得られた画像、positive画像約650枚ほどを使った。ただし、gifは使えない、pngは使えるのかもしれないが、削除したので100枚ほど少なくなった。jpgは、jpegとしてもJPGと拡張子が違っていてもOKのようだ。
naoの顔が入っていないnegative画像も、このソフトで、確か「人形 部屋」とかいう検索をかけてダウンロードして、500ほどで打ち切った。

(2)顔画像の位置の指定
positive画像のそれぞれについて、naoの顔がある位置を指定するのがとても面倒だった。600枚ほどの位置指定に3,4時間かかったような気がする。TrainingAssistantというフリーのソフトを使わせていただいた。超便利だった。指示通りにアプリを組み込む。そして、先ほどダウンロードした画像を、上記サイトに指示されているフォルダーに放り込む。
% python views.py
で、自前のウェッブサーバーを立ち上げて、ブラウザで
http://localhost:5000
を表示させると、画像が順番に出てくるので、naoの顔の部分の座標をマウスをスライドさせて指定する。それらは、info.datファイルに次のような感じで自動的に保存される。
static/img/Nao_robot.jpg 1 434 2 316 271
static/img/29437446525_059b230e13_z.jpg 2 324 93 86 67 219 148 38 30
・・・・・・
最初が画像のパスで、次がその画像の中の顔の数、そして座標とサイズである。

(3)positive.datとnegative.datの作成
画像リストデータファイルを作成する。positive.datは、新しい場所に置き換えた場合も元々の場合も、そのフォルダーの絶対パスを冒頭のパスに全て書き換える。私の場合は、次のステップのvectorファイルの作成が、macではうまくいかず、raspberrypi に全部移したので、そちらのパスに書き換えた。negative.datは、単にその画像のある絶対パスのリストで良い。

(4)positiveベクトルの作成
Macのopencvでは、opencv_createsamplesがうまく機能しなかった。原因は不明。で、raspberrypiのopencvで作成することにした。linuxmintでもよかったかと思う。どのosのopencvでもいいのだ。
Raspberrypiのopencvは、cameraを使用する関係で、少し手の込んだインストールが必要で、。次のサイトに基づいている。
https://qiita.com/NaotakaSaito/items/f1f1548c8b760629cd26
あとは、positive.datが置いてあるフォルダでopencv_createsampleコマンドを実行すればいい。私の場合は次のようにした。
opencv_createsamples -info positive.dat -vec positive.vec -num 574 -w 40 -h 40
-numは、positive画像数である。-w 40 -h 40は、ベクトルサイズらしく、よくわからない。ただ、認識対象の映像の中で最小認識可能な画像サイズと考えればいいようだ。私の場合、画像サイズを、3320X240にしているので、その中で、顔が最小限どのくらいのサイズで映った時に認識して欲しいかという基準でやったが、もっと大きくてもいいかもしれない。(その後、60X60でやってみた。この60X60の方がnaoの顔を安定して識別しつずける可能性があるように思えた)

(5)学習器の作成
私の場合のコマンドは次のようになった。
opencv_traincascade -data /home/pi/Project/ObjectRecognition/data/model -vec /home/pi/Project/ObjectRecognition/src/positive.vec -bg /home/pi/Project/ObjectRecognition/src/negative.dat -numPos 450 -numNeg 400 -featureType HOG -maxFalseAlarmRate 0.1 -w 40 -h 40 -minHitRate 0.97 -numStages 17
絶対パスで指定しなくても良いかと思う。
途中までやったのは生かされる。

最終の出力は以下のようである。
---------------------------------------------
Training parameters are pre-loaded from the parameter file in data folder!
Please empty this folder if you want to use a NEW set of training parameters.
--------------------------------------------
PARAMETERS:
cascadeDirName: /home/pi/Project/ObjectRecognition/data/model
vecFileName: /home/pi/Project/ObjectRecognition/src/positive.vec
bgFileName: /home/pi/Project/ObjectRecognition/src/negative.dat
numPos: 450
numNeg: 400
numStages: 17
precalcValBufSize[Mb] : 1024
precalcIdxBufSize[Mb] : 1024
acceptanceRatioBreakValue : -1
stageType: BOOST
featureType: HOG
sampleWidth: 40
sampleHeight: 40
boostType: GAB
minHitRate: 0.97
maxFalseAlarmRate: 0.1
weightTrimRate: 0.95
maxDepth: 1
maxWeakCount: 100
Number of unique features given windowSize [40,40] : 100

Stages 0-4 are loaded

===== TRAINING 5-stage =====

Training until now has taken 0 days 0 hours 7 minutes 23 seconds.

===== TRAINING 6-stage =====
<BEGIN
POS count : consumed 450 : 522

衝突を回避しながらランダムウォーク

これはあまり舞台上で必要な機能ではないのですが、Lidar Lite V3を使って、衝突を回避しながら、壁沿いをランダムに歩くシミュレーションです。

残されたログはこれである。測定した距離に障害物があったら、それをMap情報にしたほうがいいよね。今は、全然地図を使っていないので、無駄な動きがある。

超音波距離センサーの問題

Lidar Lite V3の赤外線距離センサーを前方につけて、自己位置を同定したり、それをもとに、位置調整をしたりはできるようになった。

そこで、残りの三方についている超音波距離センサーでも、できるようにプログラムはくんだ。ただ、センサーがすぐに働かない。C++で書いたセンサーのプログラムを、JAVAのコアシステムから呼び出すようにしてあるのだが、少し、C++の方のプログラムを動かしてからじゃないと、距離が取れない。以下の写真の、右上のように直接何度か動かすと、左下のようにコアシステムから距離が取れて、どこまで正しいか検証はしていないが、自己位置を推定している。

Lidar Lite V3の時のような、電源問題ではないかと思った。ので、Lidar Lite V3と同じように、GPIOから電源をとってテストしようかと思ったが、そもそも、超音波センサーをあまり信頼していないのに、いじっても仕方がないなと思い。これはここで凍結しておこうと思う。

絶対使わないならば、センサーそのものを外しても良いのだが、まあ、その選択肢も睨みながらの保留だ。

自己位置認識とターゲットへの移動

Lidar Lite v3で測定された距離センサーデータ、方位センサーで認識した絶対ロボット角度、さらに舞台角度情報をもとにロボットが自己位置を認識し、目標指定位置へ移動するテスト。

まずその位置を舞台上の座標で、推定する。その後、指定位置方向へ向きを変えて(動画の場合、ほとんど変わっていない)移動するテスト。うまく動いているかどうかはこれからチェック。

地図と自己位置認識

ロボットの自己位置認識は、地図を作成しながらその地図の中の自己位置を捉えて行く方法がメジャーのような気がする。しかし、舞台上でロボットの自己位置認識をさせる場合は、舞台のマップは大概の場合すでに与えられている。そこで、舞台地図が与えられた場合、距離センサーの測定値から自己位置を推定する手続きを考える。地図を作成しながらより、もっと単純になる。(以下の図は、GrabitDesignerといアプリで描いた、今までは、illustratorを使っていたが、長期的には、無料ソフトがいいと思って鞍替えしている、よく似ているので使いやすい)

舞台上の左下を原点として、上側にy軸、横方向にx軸をとる。舞台奥行きはDepth、横幅はWidthで表されているとしよう。位置の点は黒丸で表される。また、ロボットの向き角度は、下手(左)から上手(右)に向かう方向をゼロとして、反時計回りの角度で表されるとしよう。

今、ロボットがθ方向に、距離dをセンサーによって取得したとしよう。ただし、その自己位置(x, y)はわかっていず、距離の対象になった点(x0, y0)もわかっていないとする。

ただし、重要なのは、この時点で、舞台が限られていることから、(x, y)が舞台の中にあるとしたらその場所は、自由ではありえない。対象となる点は、舞台の客側を除いた、(0, 0)→(o, Depth)→(Width, Depth)→(0, Width)をつなぐ線上のいずれかの点である。この舞台枠のラインをSとしよう。

今、測定誤差を前後左右の辺の長さが2εの正方形で表されるとしよう。(x0, y0)をS上に与えた時にd,θから規定される(x, y)が舞台の中に収まっている可能性があるかどうかを計算することができる。

<br />
x_{0}-d\cos\theta-\epsilon \le x \le x_{0}-d\cos\theta+\epsilon \\<br />
y_{0}-d\sin\theta-\epsilon \le y \le y_{0}-d\sin\theta+\epsilon \\<br />

上記不等式は、(x0, y0)から、ロボットの位置(x, y)の可能性の範囲を示している。この時、(x0, y0)を舞台枠Sに沿って動かせば、d, θを測定した場合のロボット位置の範囲全体が示せる。この領域をEとする。

今、ロボットが角度を変えたり、位置を変えたりして(あるいは、違う方向を向いたセンサー情報でも良い)、情報が追加されれば、その都度、この領域が与えられる。第i回目の測定で計算された領域をE_{i}, ????i=1,2,3,\cdots,nとするとその共通(積)集合、\bigcapE_{i}の中に、ロボットがいる可能性が高いということになる。

測定は、角度だけ変えるのが基本である。位置まで変えると、ややこしくなってしまうし、また、舞台上に机などがあると、このように単純にはいかないが、その場合も測定できるように拡張することは、それほど難しくないと考えている。

Lidar Lite v3の電源問題

予備のraspberryPI3で、Lidar lite v3が軽く動いたので、ロボット本体でも動くだろうと組み込んだら、I2Cのデバイスにすら認識しなくなった。

ほぼ、一日、すったもんだしていた。I2C上のコンパスセンサーなどは正しく認識する。同じバス上に置いた、しかも同じ電源を利用しているのに、i2cdetectでアドレスが引っかかってこない。どこに問題があるか、色々試してみたが、結局、電源の問題だった。デバイス電源は、raspberrypiに供給している同じ電源からとっていたのだが、こうする限り、デバイスを認識しない。コンパスセンサーはこれで良かったのだ。

GPIOピンの電源から5Vを引いてきて、デバイスに与えたら認識した。なんで、と思うが、微妙な問題があるのだろう。raspberrypiから、ロボットのヘッドへつなげるコードがまた二本増えてしまった(泣)

レーザー距離センサーLidar Lite v3を動かすjavaプログラム

RobotShopで16,000円以上もする距離センサーLidar Lite v3を購入した。

I2Sで動かそうとしたのだが、ネット上のプログラムでは動かなかった。理由はわからない。そこで、pi4jライブラリを使ってjavaで動かすプログラムを作成したら、すんなり動いてくれた。

ここに公開しておく。(こちらのpythonプログラムを参考にさせていただいた)

import com.pi4j.io.i2c.I2CBus;
import com.pi4j.io.i2c.I2CDevice;
import com.pi4j.io.i2c.I2CFactory;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
 
/**
 *
 * @author washida
 */
public class LidarLiteV3 {
    public static final int LIDAR_LITE_ADDR = 0x62; // address pin not connected (FLOATING)
    public static final byte ACQ_COMMAND = (byte) 0x00;
    public static final byte STATUS = (byte) 0x01;
    public static final byte FULL_DELAY_HIGH = (byte) 0x0f;
    public static final byte FULL_DELAY_LOW = (byte) 0x10;
 
    /**
     * @param args
     * @throws com.pi4j.io.i2c.I2CFactory.UnsupportedBusNumberException
     * @throws java.io.IOException
     */
    public static void main(String[] args) throws I2CFactory.UnsupportedBusNumberException, IOException  {
        I2CBus i2c = I2CFactory.getInstance(I2CBus.BUS_1);
        I2CDevice device = i2c.getDevice(LIDAR_LITE_ADDR);
        while (true) {
            int response = device.read(0x04);
            device.write(ACQ_COMMAND, (byte) response);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ex) {
                Logger.getLogger(LidarLiteV3.class.getName()).log(Level.SEVERE, null, ex);
            }
            int value = device.read(STATUS);
            while ((value &amp; 0x01) == 1) {
                value = device.read(STATUS);
            }
            int high = device.read(FULL_DELAY_HIGH);
            int low = device.read(FULL_DELAY_LOW);
            int val = (high &lt;&lt; 8) + low;
            int dist = val;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                Logger.getLogger(LidarLiteV3.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}