今回は3軸加速度センサとジャイロセンサを使って刀の振り方(ジェスチャ)のパターン認識をします。ジェスチャからパターンを識別するには、まず侍の各スキルのジェスチャを把握することが必要です。
公式チャンネルのジョブ紹介動画から刀の振り方を見ていきましょう。
なお、映像からの判別なので見間違いなど正確でない可能性があります。
侍のウェポンスキル(WS)は全部で11種類。アビリティについては今回はノータッチとします。
9:50あたりから侍の紹介になります。
1番目のスキルは刃風。
縦→左から右へ水平払い
2番目のスキルは満月。
ジャンプ→水平時計回り切り
3番目は士風。
ジャンプ切り上げ→切り下げ
4番目は花車。
左から右へ払う(下から上向き)→後ろまで払いつつしゃがみ、刀を正面に構えながらジャンプ→正面から左向きに切り下げ
5番目は刃風。
6番目は鳳蝶(アビリティ)。
7番目は陣風。
左上から右下払い、体の後ろまで振り切る→右突き
8番目は月光。
真上から左回りに刃先を1周→左上から右下払い→左方向に体を一回転→回転の勢いで右から左へ(水平)
9番目は居合術。
抜刀。
10番目は雪風。
ジャンプ→右手で右上から左下へ切り下げ→体を左回転→後ろ向きで右脇から後ろ突き
11番目は月光。
12番目は花車。
13番目は居合術。
紹介されてないWSには燕飛(遠隔)、風雅(扇状範囲)、桜花(範囲)があります。
これらは詳細が分からなかったので、今回は単体近接攻撃に絞ってジェスチャのパターン認識をします。
加速度や角度を使ったジェスチャ認識に関する予備知識が無かったので、単純に刀の振り方の向き(右、左、中央 / 上向き、下向き、水平)からパターンを決定します。
単体近接WSの刃風、陣風、士風、月光、居合術、花車、雪風の7種類(その他は燕飛、風雅、満月、桜花)を刀の振り方で分別すると次のようになります。
今回は7種類だけなので、ほぼ刀の振り方だけでパターンを区別することができそうです。
陣風と月光には別途何らかの判断が必要となりそうです。
ここで、今回使用するセンサについて説明します。
ジェスチャ認識として今回は角度と加速度を使用したいので、手元にあったジャイロセンサ(ENC-03R)と3軸加速度センサ(KXR94-2050)を使用しました。
ジャイロのドリフト補正についてはこちらのサイト(秋月 小型圧電振動ジャイロモジュール AE-GYRO-SMD/ENC-03R の使い方 – isis re-direct 2 hard)を参考にしてカルマンフィルタを導入しました。都合よくENC-03RとKXR94-2050向けにチューンされていたので、スケッチをそのまま組み込むことができました。ArduinoのADCの分解能が10bitであるのに対して、今回使用する無線デバイスであるESP32のADCは12bitなので、その点だけ気を付ける必要があります。
刃先の方向と加速度センサのいずれかの軸を一致させることで、刀を振った瞬間の急激な加速度を取得することができます。
またジャイロによる角度は刀の刃先が地面に対してどのように傾いているかを知ることで、刀の姿勢がある程度識別できます。
そのため、刀の向きに対して加速度センサとジャイロセンサは次のように固定します。
加速度センサは刀を正面に構えたときに手前側(自分側)にX軸を、頭上方向にY軸を、体の左方向にZ軸を取りました。
ジャイロは刀の刃の振る方向と、刀を軸にして回転したときの角度を出します。
続いてソフトウェアを組んでいきます。
ジェスチャをできるだけ正確に認識するために、ジェスチャを行なっている間にプッシュスイッチを押しっぱなしにすることで、ジェスチャの開始と終了を判別します。
ボタン押下中に刃先方向の加速度が大きく下がったときに、刀が振られたと判断します。
今回使用した加速度センサの出力電圧値が、1G(正確には9.8G)時に5V、0Gで3.5V、-1Gで0Vとなっています。
そこで平常時の3.5Vよりも低い2Vを下回ったときに、刀の振りを判定します。下の画像で1ヶ所だけ0Vに達している箇所がありますが、ここが刀を振ったときの加速度です。
7種類のWSを判別するために、縦方向と横方向の刀の振りについてプログラム上で認識する必要があります。
まずは縦方向について、刀の振り始めと振り終わりの地面に対する角度を比較することで、下向きの振りか、上向きか、水平かを判断できます。
今回のジャイロセンサの配置では、刀を振り上げた姿勢では角度が小さくなり、刀を振り下げた姿勢で角度が大きくなります。
そこで振り始めと振り終わりの角度の差をとることで、どの方向に振ったかがわかります。
続いて横方向ですが、角度だけでは判断が難しいので加速度を併用してジェスチャを判別します。
まず、角度を使って先ほどの縦方向の振り方の向きを決定します。これは刀の振り終わりの姿勢が振り下ろしと振り上げで変わってくるからです。振り下ろしの場合、判別したいWSは陣風、月光、雪風、刃風です。陣風と月光は別途判定するとして、左、右、垂直の3種類の方向の識別が必要です。左向きに振り下ろした場合、刀は刃の左側面が上を向くため、Y軸方向の加速度が重力によってマイナス方向(-1G)に振れます。逆に右向きの場合は刀は刃の右側面が上を向くため、Y軸方向の加速度が重力によって正の方向に増加します(+1G)。左右に傾かず、真下に振り下ろせばY軸方向の加速度は重力に影響されないので0Gとなります。ただ、刀を振れば重力以外の加速度もメチャクチャに掛かるので、振り下ろされた瞬間の前後数msで加速度の平均を取って重力の方向を判別します。
振り上げの場合も同様ですが、今回判別したい振り上げのWSは花風(右振り)と士風(真上)の2種類です。右向きに振り上げた場合、刀は刃の右側が上を向くため、Y軸方向にかかる重力加速度は+1Gとなります。真上に振り上げる場合の重力加速度は0Gなので、これで全パターンの振り方を判別できました。
ここで陣風と月光の判別は、1太刀目の判定後に数msだけ待機し、2太刀目の判定を連続して行なうことで判別します。
最後に、居合術は水平に左から右に向かって切ります。
判定したジェスチャからWSに番号を振って前回のプログラムでラズパイに送信します。
以上のジェスチャ判別を送信プログラムに組み込んだものがこちらになります。
ピン配置は次の通りです。
ここでincludeしているKalman.hは以下のライブラリを使用しています。
https://github.com/TKJElectronics/KalmanFilter
#include <Kalman.h> // Source: https://github.com/TKJElectronics/KalmanFilter
#include "WiFi.h"
#include <HTTPClient.h>
// WiFi credentials.
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASSWORD";
// Internet domain to request from:
const char * hostDomain = "192.168.***.***";
const char *sendServer = "192.168.***.***:8080";
const int hostPort = 8080;
int flg = 0;
HTTPClient http;
#define HORIZONTAL 30 //Pitch角 水平判定値
#define PIN_SW 19
#define COUNT_TIME 128
static const int pin_gyrox = 32; // X AXIS ROTATION V of ENC-03R
static const int pin_gyroy = 33; // Y AXIS ROTATION V of ENC-03R
static const int pin_accx = 2; // accelaration Y of KXR94-2050 (rotated left 90 degree on Breadboard)
static const int pin_accy = 0; // accelaration X of KXR94-2050 (rotated left 90 degree on Breadboard)
static const int pin_accz = 4; // accelaration Z of KXR94-2050
Kalman kalmanX; // Create the Kalman instances
Kalman kalmanY;
// IMU Data
int accX, accY, accZ;
double gyroX, gyroY;
double gyroX_mean, gyroY_mean;
double accXangle, accYangle; // Angle calculate using the accelerometer
double gyroXangle, gyroYangle; // Angle calculate using the gyro
double compAngleX, compAngleY; // Calculate the angle using a complementary filter
double kalAngleX, kalAngleY; // Calculate the angle using a Kalman filter
uint32_t timer;
//------------------------------
double angle_buf[COUNT_TIME];
double angle_buf_r[COUNT_TIME];
double avr_accy = 0.0;
int count = 0;
int sw_flg = 0;
int action_flg = 0;
int tachi = 0;
void setup() {
Serial.begin(9600);
pinMode(pin_gyrox, INPUT);
digitalWrite(pin_gyrox, HIGH);
pinMode(pin_gyroy, INPUT);
digitalWrite(pin_gyroy, HIGH);
pinMode(pin_accx, INPUT);
digitalWrite(pin_accx, HIGH);
pinMode(pin_accy, INPUT);
digitalWrite(pin_accy, HIGH);
pinMode(pin_accz, INPUT);
digitalWrite(pin_accz, HIGH);
pinMode(PIN_SW, INPUT);
// Giving it a little time because the serial monitor doesn't
// immediately attach. Want the firmware that's running to
// appear on each upload.
delay(2000);
Serial.println();
Serial.println("Running Firmware.");
// Connect to the WiFi network (see function below loop)
connectToWiFi(WIFI_SSID, WIFI_PASS);
Serial.println(hostDomain);
http.begin("http://192.168.###.###:8080/"); //Specify destination for HTTP request
http.addHeader("Content-Type", "text/plain"); //Specify content-type header
gyroX_mean = 0;
gyroY_mean = 0;
for (int i = 0; i < 1024; ++i) {
gyroX_mean += analogRead(pin_gyrox)/4;
gyroY_mean += analogRead(pin_gyroy)/4;
}
gyroX_mean /= 1024.;
gyroY_mean /= 1024.;
// Set kalman and gyro starting angle
accX = analogRead(pin_accx)/4 - 750;
accY = analogRead(pin_accy)/4 - 750;
accZ = analogRead(pin_accz)/4 - 750;
// atan2 outputs the value of -π to π (radians) - see http://en.wikipedia.org/wiki/Atan2
// We then convert it to 0 to 2π and then from radians to degrees
accYangle = (atan2(accX,accZ)+PI)*RAD_TO_DEG;
accXangle = (atan2(accY,accZ)+PI)*RAD_TO_DEG;
kalmanX.setAngle(accXangle); // Set starting angle
kalmanY.setAngle(accYangle);
gyroXangle = accXangle;
gyroYangle = accYangle;
compAngleX = accXangle;
compAngleY = accYangle;
timer = micros();
for(int i=0;i<COUNT_TIME;i++) {
angle_buf[i] = 0.0;
angle_buf_r[i] = 0.0;
}
}
void loop() {
uint32_t now = micros();
double dif = 0.0;
kalman_cal(now);
timer = now;
delay(1);
if(action_flg != 0) {
if(count > 100) {
dif = kalAngleY - angle_buf[0];
avr_accy /= count;
Serial.print("start:");
Serial.print(angle_buf[0]);
Serial.print("\t");
Serial.print("end:");
Serial.print(kalAngleY);
Serial.print("\t");
Serial.print("dif:");
Serial.print(dif);
Serial.print("\t");
Serial.print("angle X:");
Serial.print(kalAngleX);
Serial.print("\t");
Serial.print("angle Y:");
Serial.print(kalAngleY);
Serial.print("\n");
if(action_flg == 1) {
if(abs(dif) > HORIZONTAL) { //水平ではない
if(dif > 0) {
if(avr_accy < 2.4) { //下向き
//左
Serial.print("down-left\n");
//Yukikaze
Serial.print("Yukikaze\n");
tachi = 7;
}else if(avr_accy > 4.2) {
//右
Serial.print("down-right\n");
//Jinpu or Gekko
Serial.print("[phase 1]Gekko or Jinpu\n");
action_flg = 2;
}else {
//真下
Serial.print("down\n");
//Hakaze
Serial.print("Hakaze\n");
tachi = 1;
}
}else { //上向き(右のみ)
if(avr_accy < 4.2) {
Serial.print("up-right\n");
//Kasya
Serial.print("Kasya\n");
tachi = 6;
}else {
Serial.print("up\n");
//Shifu
Serial.print("Shifu\n");
tachi = 3;
}
}
}else {
//水平
if(avr_accy > 4.2) {
Serial.print("horizon-right\n");
Serial.print("Iai-jutu\n");
//Iai-jutu
tachi = 5;
}
}
Serial.print("avr accY");
Serial.print(avr_accy);
Serial.print("\n");
}else if(action_flg == 2) {
if(abs(dif) > HORIZONTAL) { //水平ではない
if(dif > 0) {
if(avr_accy < 2.4) { //下向き
//Gekko
Serial.print("Gekko\n");
tachi = 4;
}
}
}
//Jinpu
Serial.print("Jinpu\n");
if(tachi == 0) tachi = 2;
action_flg =0;
}
if(action_flg != 2) {
post_h();
action_flg = 0;
tachi = 0;
}
count = 0;
avr_accy = 0.0;
//TODO
delay(500);
}else { //if(count > 100)
avr_accy += analogRead(pin_accy)*5/4096;
count++;
}
}else if(digitalRead(PIN_SW) == 1) {
//SW chattering
if(sw_flg == 0) {
delay(10);
sw_flg = 1;
Serial.print("SW on\n");
}
//save angle log
if(count == COUNT_TIME-1) {
for(int i=0;i<COUNT_TIME-1;i++) {
angle_buf[i] = angle_buf[i+1];
angle_buf_r[i] = angle_buf_r[i+1];
}
angle_buf[COUNT_TIME-1] = kalAngleX;
angle_buf_r[COUNT_TIME-1] = kalAngleY;
}else {
angle_buf[count] = kalAngleX;
angle_buf_r[count] = kalAngleY;
count++;
}
//Y:1~2V motion detected
if(analogRead(pin_accz)*5/4096 < 1.5) {
action_flg = 1;
count = 0;
avr_accy = 0.0;
delay(100);
}
}else {
sw_flg = 0;
count = 0;
avr_accy = 0.0;
Serial.print(kalAngleX);
Serial.print("\t");
Serial.print("\t");
Serial.print(kalAngleY);
Serial.print("\t");
Serial.print("\r\n");
}
}
void post_h() {
if(WiFi.status() == WL_CONNECTED){ //Check WiFi connection status
int httpResponseCode = http.POST(String(tachi)); //Send the actual POST request
Serial.print("POST: ");
Serial.print(tachi);
Serial.print("\n");
}else{
Serial.println("Error in WiFi connection");
}
}
void connectToWiFi(const char * ssid, const char * pwd)
{
// Connect to Wifi.
Serial.println();
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
// Set WiFi to station mode and disconnect from an AP if it was previously connected
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
WiFi.begin(ssid, pwd);
Serial.println("Connecting...");
while (WiFi.status() != WL_CONNECTED) {
// Check to see if connecting failed.
// This is due to incorrect credentials
if (WiFi.status() == WL_CONNECT_FAILED) {
Serial.println("Failed to connect to WIFI. Please verify credentials: ");
Serial.println();
Serial.print("SSID: ");
Serial.println(ssid);
Serial.print("Password: ");
Serial.println(pwd);
Serial.println();
}
Serial.println("...");
delay(3000);
}
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
Serial.println("connected");
}