20190512のC#に関する記事は7件です。

httpによるev3devモータ駆動サーバ

0.何のメモか

モータ駆動実験の目標:
先週までの実験で、加速度センサが置かれた状態、あるいは加速度センサにより計測できる「動き」を検出し、出力につなげる基本的な仕組みを理解した。
今週は、より高度な状態や「動き」を検出し、ユニークな出力に繋げるIoTシステムの設計・実装を行う。
実験者の準備としては、ev3devのイメージをダウンロードしてSDカードに焼いたものを持参することを仮定する。

1. ev3dev-lang-pythonの動作確認

1.1 インストール済のパッケージの確認

宿題にしてあったソフトのインストールと設定を前提に作業を行います。
https://qiita.com/takelab/items/ebde398893d05ca7a56a
OpenDHCPサーバでクロスケーブルにより有線接続されたev3がIPを自動取得。
その後、TeraTermでSSHアクセスを行う(ユーザ名:robot パスワード:maker)

リスト表示dpkg -l |grep ev3dev

ii  python3-ev3dev                             1.2.0                             all          Python language bindings for ev3dev
ii  python3-ev3dev2                            2.0.0~beta3                       all          Python language bindings for ev3dev

こんな結果が出るはず。
基本的に、最初にインストールされているpython3-ev3dev2の2.0.0~beta3を基準に実験を進める。つまり、'apt-get update'等はしない。

一応、何をセレクトしているか表示dpkg --get-selections

robot@ev3dev:~$ dpkg --get-selections |grep ev3
ev3-config                                      install
ev3-systemd                                     install
ev3dev-adduser-config                           install
ev3dev-base-files                               install
ev3dev-bluez-config                             install
ev3dev-connman-config                           install
ev3dev-media                                    install
ev3dev-rules                                    install
ev3dev-tools                                    install
ev3devkit-data                                  install
firmware-ev3                                    install
gir1.2-ev3devkit-0.5                            install
jri-11-ev3:armel                                install
libev3devkit-0.5-0                              install
linux-image-4.14.96-ev3dev-2.3.2-ev3            install
linux-image-ev3dev-ev3                          install
micropython-ev3dev2                             install
python3-ev3dev                                  install
python3-ev3dev2                                 install
rtl8188eu-modules-4.14.96-ev3dev-2.3.2-ev3      install
rtl8812au-modules-4.14.96-ev3dev-2.3.2-ev3      install

1.2 ev3devでのpython動作確認

公式資料の内容で確認する。

https://github.com/ev3dev/ev3dev-lang-python/blob/ev3dev-stretch/README.rst

コマンドラインからpython3で起動。
先頭の「>>>」は対話入力をしている印。
その後ろに以下の各行を一行づつ入力し、Enterキーを押す。
pythonの対話モード終了の際は「>>>」の後にexit()でEnterキーを押す。

>>> from ev3dev2.sound import Sound
>>> sound = Sound()
>>> sound.speak('Hello World')

1.3 ev3devでのモータ動作確認

同様にモータの動作も確認する。AポートとBポートにモータを接続する。
ここで、レポートのために、モータの接続図を作成するためのメモを取っておくこと。

>>> from ev3dev2.motor import LargeMotor, OUTPUT_A, OUTPUT_B, SpeedPercent
>>> m1 = LargeMotor(OUTPUT_A)
>>> m2 = LargeMotor(OUTPUT_B)
>>> m1.on(SpeedPercent(-10),brake=False)
>>> m1.on(SpeedPercent(0),brake=False)
>>> m2.on(SpeedPercent(10),brake=False)
>>> m1.on(SpeedPercent(0),brake=False)
>>> m1.on(SpeedPercent(-10),brake=False)
>>> m2.on(SpeedPercent(0),brake=False)
>>> m1.on(SpeedPercent(0),brake=False)

ちなみに、各要素は次のような結果を返す。

>>> print(LargeMotor)
<class 'ev3dev2.motor.LargeMotor'>
>>> print(OUTPUT_A)
ev3-ports:outA

モータ動作の仕様は、以下の通り(今回はサーバ経由で制御するので直接制御は行わない)
https://python-ev3dev.readthedocs.io/en/ev3dev-stretch/motors.html#ev3dev.motor.Motor.on_for_degrees
https://sites.google.com/site/ev3devpython/learn_ev3_python/using-motors

1.4 ev3devでhttpサーバ

python3 -m http.server 8000
が使える。

robot@ev3dev:~/dist-pack-ev3dev$ cd
robot@ev3dev:~$ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 ...
192.168.137.5 - - [04/Mar/2019 09:11:57] "GET / HTTP/1.1" 200 -
192.168.137.5 - - [04/Mar/2019 09:11:57] code 404, message File not found
192.168.137.5 - - [04/Mar/2019 09:11:57] "GET /favicon.ico HTTP/1.1" 404 -
^C
Keyboard interrupt received, exiting.

2. HTTPサーバの部分

1.4のようにpythonの標準ライブラリである、http.serverを使うことができることまで確認した。

2.1 デモにあるEV3D4のwebコントロール

デモ中のEV3D4WebControl.pyの先頭部分
WebControlledTankをimportして、それをEV3D4WebControlledに継承させて使っている。

EV3D4WebControl.py
#!/usr/bin/env python3

import logging
import sys
from ev3dev2.motor import OUTPUT_A, OUTPUT_B, OUTPUT_C, MediumMotor
from ev3dev2.control.webserver import WebControlledTank

class EV3D4WebControlled(WebControlledTank):
    def __init__(self, medium_motor=OUTPUT_A, left_motor=OUTPUT_C, right_motor=OUTPUT_B):
        WebControlledTank.__init__(self, left_motor, right_motor)
        self.medium_motor = MediumMotor(medium_motor)
        self.medium_motor.reset()
    #この後まだ数行続く

importされているWebControlledTankがある。ev3dev2.control.webserver.pyの先頭部分を見る。
from http.server import BaseHTTPRequestHandler, HTTPServerの行を見ると分かるように、やはりhttp.serverからimportをしているので、python3の標準的な実装を行っている様子。具体的には、/usr/lib/python3.5/の下のhttp/server.pyを見てやると具体的なことは分かる。
Webを検索すると、from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServerとやっている例があるが、python2とpython3の違いか?

webserver.py
#!/usr/bin/env python3

import logging
import os
import re
from ev3dev2.motor import MoveJoystick, list_motors, LargeMotor
from http.server import BaseHTTPRequestHandler, HTTPServer

継承しているWebControlledTankの確認

具体的には、MoveJoyStickの継承をして、TankWebHandlerをハンドラにして、RobotWebServerを使ったサーバ起動を行う。具体的なソース。

WebControlledTank.py
class WebControlledTank(MoveJoystick):
    """
    A tank that is controlled via a web browser
    """

    def __init__(self, left_motor, right_motor, port_number=8000, desc=None, motor_class=LargeMotor):
        MoveJoystick.__init__(self, left_motor, right_motor, desc, motor_class)
        self.www = RobotWebServer(self, TankWebHandler, port_number)

    def main(self):
        # start the web server
        self.www.run()

サーバ部分のRobotWebServer

サーバはself.content_server = HTTPServer(('', self.port_number), self.handler_class)でハンドラを分岐する。
具体的には、’’は自分のIPアドレス、ポート番号は8000, self.handler_classはTankWebHandlerになる。

RobotWebServer.py
class RobotWebServer(object):
    """
    A Web server so that 'robot' can be controlled via 'handler_class'
    """

    def __init__(self, robot, handler_class, port_number=8000):
        self.content_server = None
        self.handler_class = handler_class
        self.handler_class.robot = robot
        self.port_number = port_number

    def run(self):

        try:
            log.info("Started HTTP server (content) on port %d" % self.port_number)
            self.content_server = HTTPServer(('', self.port_number), self.handler_class)
            self.content_server.serve_forever()

        # Exit cleanly, stop both web servers and all motors
        except (KeyboardInterrupt, Exception) as e:
            log.exception(e)

            if self.content_server:
                self.content_server.socket.close()
                self.content_server = None

            for motor in list_motors():
                motor.stop()

2.2 HTTPメソッドハンドラ

2.1で説明したようにWebサーバのハンドラ制御部分が中心的になる。

TankWebHandlerはRobotWebHandlerを継承している。

RobotWebHandlerは後述のソースように標準的なBaseHTTPRequestHandlerを継承している。

TankWebHandler.py
class TankWebHandler(RobotWebHandler):
    def __str__(self):
        return "%s-TankWebHandler" % self.robot
    def do_GET(self):
        """
        Returns True if the requested URL is supported
        """
        if RobotWebHandler.do_GET(self):
            return True

実際のRobotWebHandlerは以下の通り。

RobotWebHandler.py
log = logging.getLogger(__name__)

# ==================
# Web Server classes
# ==================
class RobotWebHandler(BaseHTTPRequestHandler):
    """
    Base WebHandler class for various types of robots.
    RobotWebHandler's do_GET() will serve files, it is up to the child class to handle REST APIish GETs via their do_GET()
    self.robot is populated in RobotWebServer.__init__()
    """

    # File extension to mimetype
    mimetype = {
        'css'  : 'text/css',
        'gif'  : 'image/gif',
        'html' : 'text/html',
        'ico'  : 'image/x-icon',
        'jpg'  : 'image/jpg',
        'js'   : 'application/javascript',
        'png'  : 'image/png'
    }

    def do_GET(self):
        """
        If the request is for a known file type serve the file (or send a 404) and return True
        """

        if self.path == "/":
            self.path = "/index.html"

        # Serve a file (image, css, html, etc)
        if '.' in self.path:
            extension = self.path.split('.')[-1]
            mt = self.mimetype.get(extension)

            if mt:
                filename = os.curdir + os.sep + self.path

                # Open the static file requested and send it
                if os.path.exists(filename):
                    self.send_response(200)
                    self.send_header('Content-type', mt)
                    self.end_headers()

                    if extension in ('gif', 'ico', 'jpg', 'png'):
                        # Open in binary mode, do not encode
                        with open(filename, mode='rb') as fh:
                            self.wfile.write(fh.read())
                    else:
                        # Open as plain text and encode
                        with open(filename, mode='r') as fh:
                            self.wfile.write(fh.read().encode())
                else:
                    log.error("404: %s not found" % self.path)
                    self.send_error(404, 'File Not Found: %s' % self.path)
                return True

        return False

    def log_message(self, format, *args):
        """
        log using our own handler instead of BaseHTTPServer's
        """
        # log.debug(format % args)
        pass

2.3 簡易do_GET実装

以上までのソースを参考に、プログラム的には褒められないが、馬鹿正直なハンドラ部分を作ると次のようになる。
もちろんテストはブラウザからテストできる。

test.py
from http.server import HTTPServer, BaseHTTPRequestHandler

class MyHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        print(self.path)
        if self.path == "/":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("OK".encode('utf-8'))
            return

        elif self.path == "/command":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("OK".encode('utf-8'))
            return

        else:
            self.send_response(200)
            self.end_headers()
            self.wfile.write("NG".encode('utf-8'))
            return


if __name__ == "__main__":
    host = ''
    port = 8181

    server = HTTPServer((host, port), MyHandler)
    server.serve_forever()

3. モータ制御との連携

3.1 Python(ev3dev側)のサーバプログラム

2.3のプログラムに単にモータ管理用オブジェクトをグローバル変数化してif-elifで制御する単純プログラム。
まずはここから。これから良いプログラムにしてください。

実行は、python3 motorHttp.pyでOK。
終了はCtrl+C。

各if文の分岐先にある
m2.on(SpeedPercent(10),brake=False)がev3devに対する制御部分。

motorHttp.py
#!/usr/bin/env python3
#
#

import logging
import sys

from ev3dev2.motor import LargeMotor, OUTPUT_A, OUTPUT_B, SpeedPercent
from http.server import HTTPServer, BaseHTTPRequestHandler


class MyHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        global m1
        global m2
        print(self.path)
        if self.path == "/":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA: OFF B: OFF".encode('utf-8'))
            m1.on(SpeedPercent(0),brake=False)
            m2.on(SpeedPercent(0),brake=False)
            return

        elif self.path == "/a0":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA: OFF B:x".encode('utf-8'))
            m1.on(SpeedPercent(0),brake=False)
            return

        elif self.path == "/a1":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA: ON B:x".encode('utf-8'))
            m1.on(SpeedPercent(10),brake=False)
            return

        elif self.path == "/a-1":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA: RV B:x".encode('utf-8'))
            m1.on(SpeedPercent(-10),brake=False)
            return

        elif self.path == "/b0":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA:x B:OFF".encode('utf-8'))
            m2.on(SpeedPercent(0),brake=False)
            return

        elif self.path == "/b1":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA:x B:ON".encode('utf-8'))
            m2.on(SpeedPercent(10),brake=False)
            return

        elif self.path == "/b-1":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA:x B:RV".encode('utf-8'))
            m2.on(SpeedPercent(-10),brake=False)
            return

        else:
            self.send_response(200)
            self.end_headers()
            self.wfile.write("OK".encode('utf-8'))
            return

if __name__ == "__main__":
    host = ''
    port = 8181
    m1 = LargeMotor(OUTPUT_A)
    m2 = LargeMotor(OUTPUT_B)
    m1.on(SpeedPercent(0),brake=False)
    m2.on(SpeedPercent(0),brake=False)

    server = HTTPServer((host, port), MyHandler)
    server.serve_forever()

3.2 Windows側のクライアントプログラム

キーボード入力の先頭文字列を使い、3.1のサーバプログラムと連携する。
IPアドレスを固定で書いてあるので、IPアドレスで置換をかけて対処する。

key2HTTP.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.IO.Ports;
using System.IO;

namespace key2HTTP
{
    class Program
    {
        static void Main(string[] args)
        {
            ConsoleKeyInfo k;

            for (int i = 0; i < 100; i++)
            {
                //Console.WriteLine("InputInt Number");
                k = Console.ReadKey(true);
                string s = k.KeyChar.ToString();
                //この段階でiは文字列sが数字なら数字に、0とそれ以外は0になっている。
                Console.WriteLine("KEYBOARD INPUT: {0}", s);
                if (!string.IsNullOrEmpty(s))
                {
                    WebClient wc = new WebClient();
                    Stream st;
                    StreamReader sr;
                    //受信データに関する処理
                    if (s.StartsWith("0"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else if (s.StartsWith("a"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/a1");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else if (s.StartsWith("b"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/a-1");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else if (s.StartsWith("c"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/a0");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else if (s.StartsWith("e"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/b1");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else if (s.StartsWith("f"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/b-1");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else if (s.StartsWith("g"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/b0");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else
                    {
                        Console.WriteLine("We don't define the command");
                    }
                    wc.Dispose();
                }
            }
            Console.WriteLine("Press Enter Key");
            Console.ReadLine();
        }
    }
}

3.3 シリアル通信との連携

3.2節のプログラムを改造し、シリアル通信の基礎プログラムである以下のプログラムを作成せよ。

ComReceive.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.IO.Ports;
using System.IO;
using System.Threading;

namespace ComReceive
{
    class Program
    {
        static void Main(string[] args)
        {
            SerialPort sp = new SerialPort();
            sp.BaudRate = 9600;
            //sp.BaudRate = 115200;
            sp.PortName = "COM4"; //自分たちのCOMポート番号にすること
            // extra
            sp.Parity = Parity.None;
            sp.StopBits = StopBits.One;
            sp.DataBits = 8;
            //sp.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);
            // フロー制御はしません。
            // port.DtrEnable = false;
            //port.RtsEnable = false;

            //Start Send Charctor
            Console.Write("Press EnterKey to start!");
            Console.ReadLine();
            string req = "s";

            try
            {
                sp.Open();//シリアルポートのオープン
                sp.WriteLine(req);
                Console.WriteLine("send   :" + req);
            }
            catch (Exception e)
            {
                Console.WriteLine("Unexpected exception: ", e.ToString());
            }


            //while(true)
            for (int i = 0; i < 50; i++)
            {
                //シリアルポートからの受信
                string s = sp.ReadLine();
                if (!string.IsNullOrEmpty(s))
                {
                    WebClient wc = new WebClient();
                    Stream st;
                    StreamReader sr;
                    //受信データに関する処理
                    if (s.StartsWith("a"))
                    {
                        Console.WriteLine("Receive command: a");
                        st = wc.OpenRead("http://192.168.137.8:8181/a1");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                        //引数の値をミリ秒単位でみて時間待ちをする。
                        Thread.Sleep(1000);
                        st = wc.OpenRead("http://192.168.137.8:8181/a0");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else
                    {
                        Console.WriteLine("Receive undefined command !");
                        st = wc.OpenRead("http://192.168.137.8:8181/");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    wc.Dispose();
                }
            }
            Console.WriteLine("loop was over");
            sp.Close();//シリアルポートのクローズ
        }
    }
}

Arduinoの加速度センサのx軸の計測値変化だけを見て、シリアルポートに文字出力を行うプログラム例

sendCommand.c
void setup() {
  pinMode(8,OUTPUT);
  pinMode(12,OUTPUT);
  Serial.begin(9600);
}

void loop() {
  int i,f;
  int x, y, z;
  while(Serial.available() == 0);
  char c = Serial.read(); // 一文字分データを取り出す。

  f=0;
  while(1){
    x = analogRead(A0);
    y = analogRead(A1);
    z = analogRead(A2);
    if(f != 1 && x >600){
      Serial.println("a");
      f=1;
    }
    else if(f ==1 && x <550){
      f =0;
    }
    delay(100);
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityちゃんの頂点データを取得するだけ

最初に

点群データを扱う研究をしてたので,3Dキャラクターの点群データもほしいと思ってやりました.
Unityも久しぶりに使うのであまり詳しくないです.
最終的には頂点数*次元数の行列を得ます.
参考にした記事 : http://backbone-studio.com/brog-unity01/

Unityちゃん

Asset StoreからUnityちゃんをダウンロードし,unitychan.prefabをHierarchyビューにドラッグ&ドロップします.
unitychan.prefabの構造を眺めてみると以下のようになっています.
Hierarchy2.jpg
mesh_rootという空オブジェクトの子オブジェクトにそれぞれのパーツが入っており,それぞれにSkinned Mesh Rendererという頂点情報が乗ったメッシュがアタッチされています.
ここから取得するとよさそうです.

コード

以下のコードを書きます.

PointCloud.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ChildAccess : MonoBehaviour {

    public string filename = "test";
    void Start () {
        // 頂点数を数える
        int vtx_num = 0;
        foreach(Transform child in gameObject.transform){
            SkinnedMeshRenderer skin = child.GetComponent<SkinnedMeshRenderer>();
            vtx_num += skin.sharedMesh.vertices.Length;
        }
        Vector3[] vtx_posi_array = new Vector3[vtx_num];

        // 頂点座標を取得
        int count = 0;
        foreach(Transform child in gameObject.transform){
            SkinnedMeshRenderer skin = child.GetComponent<SkinnedMeshRenderer>();
            Mesh child_mesh = skin.sharedMesh;
            for(int i = 0; i < child_mesh.vertices.Length; i++){
                float x = child_mesh.vertices[i].x;
                float y = child_mesh.vertices[i].y;
                float z = child_mesh.vertices[i].z;
                vtx_posi_array[count] = new Vector3(x, y, z);
                count++;
            }
        }

        // csvファイルに書き込む
        try{
            filename = filename + ".csv";
            bool append = false;
            using(var sw = new System.IO.StreamWriter(@filename, append))
            {
                for(int i = 0; i < vtx_posi_array.Length; ++i){
                    sw.WriteLine("{0},{1},{2}", vtx_posi_array[i].x, vtx_posi_array[i].y, vtx_posi_array[i].z);
                }
            }
        }
        catch(System.Exception e)
        {
            Debug.Log(e.Message);
        }
    }
    void Update () {

    }
}

簡単に説明すると,mesh_rootの子オブジェクトを順番に走査しつつ頂点座標を取得.座標の配列に格納して,最後csvファイルで出力します.

このファイルをmesh_rootにアタッチして実行するだけです.最終的には11407*3の行列が取得できます.

おわりに

本当は頂点の色情報も加えて頂点数*(次元数+3色)の行列を得たかったんですが,上手くできませんでした.
なにか別の方法を試してもよさそうです.
あと他の3Dモデルもこのような構造だとこのファイル使い回せるんですがどうなんでしょう.

© UTJ/UCL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】サーバーなどを用意せずにオフライン報酬を実装する方法

はじめに

はじめまして,Nわかです。
初めての投稿でもあるので、軽く自己紹介。
現在起業した友人のもとでUnityエンジニアとして,大学に通いつつ働かせてもらっています。
現在1つのスマホ向けカジュアルゲーム制作を任されています。リモートでOKとのことで,ほぼ家や授業の合間などに作業させてもらっています。ホンマありがたいです。

その担当したゲームの機能の一つに「オフライン報酬」というものがあります。これはゲームをプレイしていなくても,ゲーム内コインがゲームをやっていなかった時間分貰えるもらえるというものです。
ソシャゲの時間経過で回復する体力(ゲームプレイ時に消費)などが近しい機能でしょう。

この機能を実装するにあたって,結構ググりましたが,直接的な記事は見当たりませんでした。
自分用の備忘録として,そして似たような機能を実装する方の助けに少しでもなれればと思います。

仕様

今回実装するにあたり,決められていた仕様をざっくりと。

  1. サーバーとのやり取りなどはしない
  2. 1分当たりに獲得できる報酬の量は決まっており,その量は成長させられる
  3. アプリをキルして再開もしくはキルせず再開(いったん別アプリをアクティブにしていた)のどちらの場合においても,報酬が発生していればポップアップ表示を行い報酬が獲得できる

本当にざっくりと、ですが。

実装

それでは実際に実装していくわけですが,やることとして大まかに2つ項目があります。

報酬の計算

サーバーとのやり取りをしないとのことなので,ローカルで時間を取得して前回の時間との差分を出して報酬を計算してあげる必要があります。

    private void SetOfflineEarning()
    {
        double minutes = CalcSecond(DateTime.UtcNow, _endTime) / 60;
        _endTime = DateTime.UtcNow;
        _earnOffline = (int) minutes * _offlineEarning;
        Save();
    }

DateTime.UtcNowで現在のUTC時刻を取得可能。Save();では,今回の機能に関係あるものでは_endTimeのみ保存しています。また,記載していませんが,ゲーム開始時に_endTimeを読み込んでいます。
1分あたりの報酬なので,今回は60で割っています。

ちなみにCalcSecond()は以下の通り。DateTimeでのUTC時刻の差分を出し,トータル秒数を返却するメソッドです。

    private double CalcSecond(DateTime now, DateTime prevTime)
    {
        return (now - prevTime).TotalSeconds;
    }

これで_earnOfflineにオフライン報酬が代入されている状態になったので,これを全体の稼ぎを示すメンバにでも加算してあげればよいでしょう。

アプリから離れたことの検知

アプリ起動時だけでなく,他のアプリから戻ってきた際にもポップアップ表示をしてあげないといけないので,アプリから離れたことを検知してあげる必要があります。

Unityには以下のようなprivateメソッドが用意されています。
OnApplicationPause(bool pauseStatus)
これはアプリが他のアプリに移ったりホーム画面に戻ったりすることによって一時中断されたときや,逆にアプリケーションに戻ってきた際に呼ばれるものです。
つまり,pauseStatusfalseの際は戻ってきたということになるので,ここにオフライン報酬の処理を記述してあげれば良さそうです。
ここで1つ注意すべき点としては,GoogleのAdMobを導入している際です。リワード広告を表示するとこれもアプリケーションを離れた判定を受けます。つまりリワード広告を閉じて戻ってきた際にも上記メソッドが呼ばれてしまうのです。
よって,もしリワード広告を導入しているのであれば,リワード広告を表示していた場合は無視するようにするべきでしょう。

    private void OnApplicationPause(bool pauseStatus)
    {
        //ゲーム中は無視
        if (GameSceneManager.IsGameStart) return;
        //リワード広告後は無視
        if (_isRewarding) return;
        if (!pauseStatus) _isOpenOfflineEarning = true;
        OpenOfflineEarning();
    }

OpenOfflineEarning()は以下のようになっています。

    public void OpenOfflineEarning()
    {
        //起動時にはオフライン報酬を表示
        if (_isOpenOfflineEarning)
        {
            _isOpenOfflineEarning = false;
            //もし稼ぎが0ならなし
            if (CalcOfflineEarning() <= 0) return;
            SceneManager.LoadSceneAsync(SceneName.OfflineEarningScene, LoadSceneMode.Additive);
        }
    }

わざわざメソッドにする意味あったか?とか今見返すと色々思うところがありますが,置いておきます笑
先ほどのSetOfflineEarningは読み込んだ先のシーン内にあるシーン管理スクリプトのStart()内で実行しています。
CalcOfflineEarning()SetOfflineEarningで行っている計算の部分のみを行い値だけ返却するメソッドです。報酬が0の場合はポップアップ表示をする必要がなく計算結果を代入されては困るので,if文で判定を行っています。
一応コードも載せておきます。(同じようなことを記述していてばかばかしいですね,もっと簡潔に書けそうではあります。)

    public int CalcOfflineEarning()
    {
        double minutes = _timer.CalcSecond(DateTime.UtcNow, _endTime) / 60;
        return (int) minutes * _offlineEarning;
    }

追加ロードするシーンは以下のようにポップアップ表示用UIを用意しておき,稼げる金額等をスクリプト側で書き換えています。
image.png

まとめ

初投稿で文章が読みづらいところもあるかとは思いますが,ご容赦ください。
今回基本的には時間差分で報酬を計算,アプリケーションに戻ってきたことを判定し,その際にポップアップ表示用のシーンを追加ロードという形で実装しました。
もっと良い方法もあるかとは思いますが,少しでも参考になれば幸いです。

追記(2019-05-13)

今回の実装では特段チート対策(端末の時刻をいじるなどに対する対策)はしていません。
企画の方に確認したところ,対策しなくてもいいとのことだったので,とりあえず現状は放置です。そのうち対策したものも載せるかもしれません。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ObjectPoolから出し入れするAnimatorの再生状態をシンクロさせてオークをたくさんダンスさせる

ezgif-1-abc5260e8697.gif

わかる
コードをよむ
わからない
つぎにすすむ

なにを言っているのか

ObjectPoolを用いて頻繁にHumanoidAnimatorで動くキャラの出現/消失を行います。
そのとき、新しく出したキャラも既に出ているキャラの動きに合わせたいです。

なにも考えずキャラを出し入れするとこんな感じになります。

fail.gif

統率の取れていないオークの群れなどお話になりませんね。
Animatordisableされると再生位置がリセットされます。そのため、ObjectPoolに回収されるとアニメーションが最初からになってしまいます。ObjectPoolの要素が足りなくなって新規作成されたキャラも同様です。

Animatorの再生位置をシンクロさせる

解説

動きを揃えるからにはまず基準を定めなければなりません。というわけではぴねこちゃんをオークリーダーに任命します。はぴねこちゃんにもオークにも同じAnimatorをセットします。
アニメーションはある程度長くて動きが大きいものならズレがわかりやすいので、ユニティちゃん Candy Rock Star ライブステージ!のダンスを使います。

Animatorのアニメーションを途中から再生するには以下のメソッドを使います。

Animator.Play
public void Play (int stateNameHash, int layer= -1, float normalizedTime= float.NegativeInfinity);

stateNameHashがアニメーションの種類、normalizedTimeはアニメーションの再生位置です。layerは特に変更しないので初期値の-1を入れます。
このふたつのパラメータをオークリーダーのAnimatorから取得できればよいわけです。取得の仕方は以下の通り。

var currentState = _masterAnimator.GetCurrentAnimatorStateInfo(0);
var stateHash = currentState.fullPathHash;
var normalizedYime = currentState.normalizedTime;

問題はこの値を取得するタイミングです。Animatorのアニメーションはいつ更新されているのか。イベント実行順を見てみます。

Unity公式 : イベント関数の実行順

monobehaviour_flowchart.png

PhysicsブロックとGame LogicブロックそれぞれにInternal Animation Updateの記載があります。この2つが実行される前にパラメータをセットしておけば大丈夫そうです。というわけでFixedUpdate
ただし、同じFixedUpdateのタイミングで「オークリーダーからstateを取得」→「各オークに通知」する必要があります。異なるComponent間の実行順は保証されていないので一工夫……しないでUniRxSubjectを使いましょう。

OrcSecurer
// オークリーダーのAnimator
[SerializeField] private Animator _masterAnimator;
// オークリーダーの初期位置
private Vector3 _masterStartPosition;

// 初期位置からの差分, stateNameHash, normalizedTime
public readonly Subject<(Vector3, int, float)> MasterInfo = new Subject<(Vector3, int, float)>();

void Start()
{
    _masterStartPosition = _masterAnimator.rootPosition;

    Observable.EveryFixedUpdate().TakeUntilDestroy(_masterAnimator.gameObject).Subscribe(l =>
    {
        var currentDiff = _masterAnimator.rootPosition - _masterStartPosition;
        var currentState = _masterAnimator.GetCurrentAnimatorStateInfo(0);

        MasterInfo.OnNext((currentDiff, currentState.fullPathHash, currentState.normalizedTime));
    });
}

後はこのクラスをSingletonにしておきます。そしてObjectPoolから取り出されたときに各オークはこのSubjectを購読すればいいわけです。

各オークの初期化時
OrcSecurer.Instance.MasterInfo.TakeUntilDisable(this).Subscribe(tuple =>
{
    _animator.gameObject.transform.position = _startPosition + tuple.Item1;
    _animator.Play(tuple.Item2, -1, tuple.Item3);
});

これで無事同じFixedUpdateのタイミングで「オークリーダーからstateを取得」→「各オークに通知」をすることができました。

説明するタイミングを逃しましたが、同時に流しているVector3は初期位置からApply Root Motionによってどれだけ移動したか……の差分です。アニメーションの再生位置をすっとばすことで、それまでアニメーションによって移動したはずの座標位置もすっとばされています。なので合わせて補正してあげないといけません。

また、もしUniRxが使えない場合は[DefaultExecutionOrder()]属性でオークリーダーからstateを取得するクラスの実行優先度を高めて先にstateを取得します。Singletonにしておくことで自由にアクセスできるようになるので、各オークにアタッチされたComponentから取得しましょう。1

主題はこれで終わりです。改めて最初のgifを見てください。ちゃんとシンクロしているのがわかる2と思います。

コード

OrcSecurer

オークリーダーはぴねこちゃんのstateを取得するクラス。同時にオークをObjectPoolから出し入れもする。デブクラス。
めんどくせえからやってないけどちゃんと別クラスに分割しましょう。

OrcSecurer
using System;
using System.Collections.Generic;
using UniRx;
using UniRx.Toolkit;
using UnityEngine;

public class OrcSecurer : SingletonMonoBehaviour<OrcSecurer>
{
    [SerializeField] private Animator _masterAnimator;
    [SerializeField] private AnimatorSynchronizer _orcPrefab;

    private const int AppearCount = 20;
    public readonly Subject<(Vector3, int, float)> MasterInfo = new Subject<(Vector3, int, float)>();

    private Vector3 _masterStartPosition;

    void Start()
    {
        _masterStartPosition = _masterAnimator.rootPosition;

        Observable.EveryFixedUpdate().TakeUntilDestroy(_masterAnimator.gameObject).Subscribe(l =>
        {
            var currentDiff = _masterAnimator.rootPosition - _masterStartPosition;
            var currentState = _masterAnimator.GetCurrentAnimatorStateInfo(0);

            MasterInfo.OnNext((currentDiff, currentState.fullPathHash, currentState.normalizedTime));
        });

        var currentAppearList = new List<AnimatorSynchronizer>(AppearCount);
        var orcPool = new OrcPool(_orcPrefab, this.transform);

        for (var count = 0; count < AppearCount; count++)
        {
            var orc = orcPool.Rent();
            orc.Ready();
            currentAppearList.Add(orc);
        }

        Observable.Interval(TimeSpan.FromMilliseconds(400)).TakeUntilDestroy(this).Subscribe(
            l =>
            {
                if (UnityEngine.Random.Range(0, 2) == 0 && currentAppearList.Count < AppearCount * 2)
                {
                    var orc = orcPool.Rent();
                    orc.Ready();
                    currentAppearList.Add(orc);
                    return;
                }

                var index = UnityEngine.Random.Range(0, currentAppearList.Count);
                var removeOrc = currentAppearList[index];
                currentAppearList.RemoveAt(index);
                orcPool.Return(removeOrc);
            },
            () => { orcPool.Dispose(); });
    }

    private class OrcPool : ObjectPool<AnimatorSynchronizer>
    {
        private readonly AnimatorSynchronizer _prefab;
        private readonly Transform _parent;

        public OrcPool(AnimatorSynchronizer prefab, Transform parent)
        {
            _prefab = prefab;
            _parent = parent;
        }

        protected override AnimatorSynchronizer CreateInstance()
        {
            return GameObject.Instantiate(_prefab, _parent);
        }

        protected override void OnBeforeRent(AnimatorSynchronizer instance)
        {
            instance.gameObject.transform.position = new Vector3(0, -10, 0);
            base.OnBeforeRent(instance);
        }

        protected override void OnBeforeReturn(AnimatorSynchronizer instance)
        {
            base.OnBeforeReturn(instance);
            instance.gameObject.transform.position = new Vector3(0, -10, 0);
        }
    }
}

AnimatorSynchronizer

各オークにアタッチされています。

AnimatorSynchronizer
using UniRx;
using UnityEngine;

public class AnimatorSynchronizer : MonoBehaviour
{
    [SerializeField] private Animator _animator;

    private Vector3 _startPosition;
    private readonly float[] SquarePoints = {-3f, -1.5f, 0, 1.5f, 3f};

    public void Ready()
    {
        SetupStartPosition(new Vector3(SquarePoints[Random.Range(0, SquarePoints.Length)], 0f, SquarePoints[Random.Range(0, SquarePoints.Length)]));
    }

    public void SetupStartPosition(Vector3 startPosition)
    {
        _startPosition = startPosition;

        OrcSecurer.Instance.MasterInfo.TakeUntilDisable(this).Subscribe(tuple =>
        {
            _animator.gameObject.transform.position = _startPosition + tuple.Item1;
            _animator.Play(tuple.Item2, -1, tuple.Item3);
        });
    }
}

余録

ObjectPool

protected override void OnBeforeRent(AnimatorSynchronizer instance)
{
    instance.gameObject.transform.position = new Vector3(0, -10, 0);
    base.OnBeforeRent(instance);
}

protected override void OnBeforeReturn(AnimatorSynchronizer instance)
{
    base.OnBeforeReturn(instance);
    instance.gameObject.transform.position = new Vector3(0, -10, 0);
}

なぜインスタンスに対してObjectPool出入りのときに変な座標をセットしているかというと、ObjectPoolから出てenableになったキャラが指定された座標に移動する前に一瞬だけ見えてしまうからです。かなりちらついて気になります。新しく生成されたキャラも座標は(0, 0, 0)なのでこちらも同様です。
出現の瞬間にプレイヤーから見えなければいいわけなので、ObjectPoolの中にいる間は地の底に沈めることで誤魔化しています。どうせdisableなのでどこにいてもいいでしょう。たぶん。

あと、書いていて思ったのですが、ObjectPoolは別クラスにするよりプーリングするクラスの中にサブクラスとして書いちゃったほうがわかりやすくていいかもしれません。行数が増えすぎるかもですが。

Animator

Unity歴1年ちょっと、ありものを使うばかりでAnimation関係を触ったことなかったんですが、なんか、これ、闇、深い……?
いろいろ便利にラッピングされているっぽくて、奥底に何が潜んでいるかわからない恐怖を感じる。

毎フレームパラメータをセットしているのが気持ち悪くて、というかそもそも最初のアニメーション開始位置さえ合わせちゃえば後はみんな同じなのでは? と思って最初の10フレームだけ補正をかけるなどしたのですが、じわじわずれてきます。
Updateにタイミングを変更してもだめ。UniRxではなくMonoBehaviourのコールバックを使ってもだめ。
ということは肉眼で検知できないレベルでオークリーダーとオークのダンスがずれている?3 謎。
あと、ステートマシーンによるアニメーションの遷移ですが、これみんなよく管理できてますね……つらい……なんだこれは……。

まとめ

自分で作る前にUnityでダンス動画とか作ってる人のブログ漁ったらよかったかも。同じことを既に実行済みの人がいるはず。
あと、MMDの動画を参考資料としていろいろ見たのですが、みんなとんでもねえ性癖でとんでもねえなって思いました。

おしまい。

素材

はぴねこちゃん4
HarpyCat

オーク5
POLYGON - Fantasy Rivals

ダンス
ユニティちゃんライブステージ! -Candy Rock Star-
© Unity Technologies Japan/UCL

背景
Farland Skies - Cloudy Crown

参考

Animatorでアニメーションを途中から再生する
これが知りたかった。

UniRxのObjectPoolを利用する
くわしい。

【Unity】Animatorの更新タイミングを変更する
これをなんかうまいことすればずれる問題も解決するような気がしないでもないが、毎フレームセットしたところでパフォーマンスへの負荷も大きくないので調べる気がない。


  1. たぶんうまくいくと思うけどあんまり自信ない 

  2. あなたも「わかる」側になりました 

  3. こんなザマじゃ女騎士サマに勝てねえよなぁ? 

  4. 「harpy」ってそらで書けたのですが、いったいどこで綴り覚えたんだっけ…… 

  5. セールで買ったけど使ってなかったからつい 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPでC#のLinqライクなクラスの実装

はじめに

PHPで関数型プログラミングの側面について勉強を行いました。

PHPはマルチパラダイムとまでは言いませんが、オブジェクト指向プログラミング関数型プログラミング が扱えます。メソッドチェーンについて少し触れたことがあったので、勉強がてら作成してみました。
今回作成したLcq(List control query)というクラスはC#で実装されているLinqという機能を模して作っています。Linqは非常に便利なので、C#erは覚えたほうがいいです。
PHPでもLinqを模したライブラリとしてGinqというのがあります。今回は勉強のために車輪の再発明を行いました。

関数型プログラミングについて

これに関しては長くなるのでここでは深く触れません。
関数型プログラミングとは、「データに何らかの処理を加えていく」の連続で組み立てていくものです。プログラムの関数と言うより数学の関数をイメージするといいかもしれません。
データ ⇒ f(データ) ⇒ データ´ ⇒ g(データ´) ⇒ データ´´

身近な関数型プログラミング言語としてSQLがあります。
以下の画像が関数型プログラミングの概要図になります。
image.png (389.7 kB)
(この画像は俺が後輩に対してUniRxを教える際に使った資料の一部を切り取った物です。SlideShareに載せてるので、興味があればご一読くださいな。https://www.slideshare.net/YuujirouItou/rx-class)

関数型プログラミングについて深く知りたい方は以下をご覧ください。
https://qiita.com/stkdev/items/5c021d4e5d54d56b927c

PHPの関数型プログラミングの仕様

PHPの関数型の特徴として、クロージャが使える事があげられます。
以下を参考にしてください。
https://www.sejuku.net/blog/80737

ただC#のようにクロージャに対して引数、返り値共に指定できないので、少し不便に感じました。

Lcqの実装

お待たせしました。本編でございます。
今回実装するLinqライクなクラスにはSelect操作とWhere操作しか実装していません。
PHPDocを使ってふんだんにコメントを書いているので、ここでの説明はある程度省略します。

<?php

/**
* PHP上でC#のLinqを行うための模倣クラス
* 現在はSelectとWhereのみ実装済
* (List control queryの略)
*/
class Lcq{
   //----Fields----
   private $arr;


   //----Methods----
   /**
    * コンストラクタ
    *
    * @param Array $arr
    */
   public function __construct($arr){
       //値の検証
       if(!is_array($arr)){
           throw new Exception('配列ではない引数の代入');
       }

       $this->arr = $arr;
   }

   //--Controllers--
   /**
    * 射影・選択を行うためのメソッド
    * 引数には射影用の関数を代入する事。
    * 関数の動作が不可能な場合は例外を出力。
    *
    * @param function $func
    * @return Lcq
    */
   public function select($func){
       //--Validate--
       if(!is_callable($func)){
           throw new Exception("Selectの引数が関数ではありません");
       }

       $result = [];
       //全要素に対して射影を行うループ処理
       for($i = 0; $i < count($this->arr); $i++){
           try{
               //引数に渡された関数にて選択・射影処理を行い、結果を格納。
               $tmp = $func($this->arr[$i]);
               array_push($result, $tmp);
           } catch (Exception $e){
               throw new Exception($e);
           }
       }

       //保持変数を入れ替え
       $this->arr = $result;
       return $this;
   }

   /**
    * SQLのWhereを行うためのメソッド
    * 引数には条件指定用の関数を代入する事。
    * 関数の動作が不可能な場合は例外を出力。
    *
    * @param function $func
    * @return Lcq
    */
   public function where($func){
       //--Validate--
       if(!is_callable($func)){
           throw new Exception("Selectの引数が関数ではありません");
       }

       $result = [];
       //全要素に対して条件判定処理を行うループ処理
       for($i = 0; $i < count($this->arr); $i++){
           try{
               //渡された関数で条件判定
               $compareResult = $func($this->arr[$i]);
               //bool値がtrueな場合、配列に追加する。bool値でなければ例外を返す
               if(!is_bool($compareResult)) throw new Exception("Whereに渡された関数の戻り値がbool値ではありません");
               if($compareResult) array_push($result, $this->arr[$i]);
           } catch (Exception $e){
               //例外が出た場合は何もせずに自身を返却
               print "Whereが成功しませんでした";
               return $this;
           }
       }

       //保持変数を入れ替え
       $this->arr = $result;
       return $this;
   }

   //--validate--
   /**
    * 配列の検証を行う関数
    *
    * @return Exception|void
    */
   private function arrayValidate(){
       if(!is_array($this->arr) || count($this->arr) <= 0){
           throw new Exception("配列の長さが0又は配列ではない値の保持");
       }
   }

   //--Getter--
   /**
    * 現在操作中の配列を返却する関数
    *
    * @return Array
    */
   public function toArray(){
       $this->arrayValidate();

       return $this->arr;
   }

   //--Creater--
   /**
    * 配列をLcqオブジェクトに変換する静的関数。
    * 配列を渡さない場合、例外を出力する
    *
    * @param Array $arr
    * @return Lcq
    */
   public static function From($arr){
       if(!is_array($arr) || count($arr) <= 0){
           throw new Exception("配列の長さが0又は配列ではない値の保持");
       }

       return new Lcq($arr);
   }
}

//配列からLCQオブジェクトを作成する
$arr = [2, 3, 5, 6, 3, 2];
$lcq = Lcq::From($arr);

/*
*メソッドチェーンにてコレクションを操作する
*即時関数にて比較用関数を渡している
*/
$result = $lcq
       ->select(function($x){return $x + 2;})  //x + 2で射影
       ->where(function($x){return $x >= 5;})  //x が 5以上の場合条件クリア
       ->toArray();

//操作した配列の内容を出力する
print "-------------------before-------------------" . PHP_EOL;
print_r($arr);
print "-------------------after--------------------" . PHP_EOL;
print_r($result);

以下が出力内容です。

-------------------before-------------------
Array
(
    [0] => 2
    [1] => 3
    [2] => 5
    [3] => 6
    [4] => 3
    [5] => 2
)
-------------------after--------------------
Array
(
    [0] => 5
    [1] => 7
    [2] => 8
    [3] => 5
)

おわりに

PHPは動的型付けなので、型宣言が無いですよね。
今回のLcqの開発で分かった型宣言が無いメリットデメリットがいくつかありました。
メリット
 ・勝手にGenericsとして実装されるため、ユーザが型を指定しなくてよい
 ・配列がほぼ可変長なので、新しく作る必要がほぼない
デメリット
 ・値が入っているかや、関数であるか証明するという検証が大変
 ・クロージャに対して返り値の型や引数の数等指定できないので、プログラムの動作に保証がほとんど無い

短い時間のみでの作成のため、設計もガバガバで動作も確証できません・・・。
配列の置き換え部分などが雑なので、メモリ使用量も多いと思います。
いい改善案などあったら仰っていただけますと幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【10分でできる】AzureのFace APIを使って画像から顔を検出する

AzureのAPIを使用して、ささっと画像ファイルから顔情報を取得してみます。意外とハードルが低いので、これをベースに他のAPIやアイデアと紐づけると面白いものが作れそうですよね。

利用するサービス

Microsoft Azure Face API(のDetect機能)

Azureの中でもCognitive Servicesと呼ばれる、AIや機械学習を用いたサービスの内の一つです。

何ができるのか

画像ファイル内に含まれる人物の顔を検出したり、比較対象の画像に写っている人が同一人物かを検証できたりします。
Face API紹介ページにデモがあるので、実際にどういうことができるのか試してみると面白いです。
今回使用するのはDetect(顔検出)という機能ですが、Face APIではこの他に以下の機能が提供されています。

①Find Similar
②Group
③Identify
④Verify
※①③④を利用するためにはDetect実行時に発行されるFace IDが必要となります。

やり方

APIキーの取得

こちらのページから、「Face」という項目の横にある「APIキーの取得」というボタンを選択してください。
キャプチャ.PNG

まだAzureのアカウントを持っていない方は以下のような画面が出ると思います。
Azureのサービスを利用するために、まずはアカウントを登録する必要があります。
キャプチャ2.PNG
こちらにも書いてありますが、Face APIは「20件のトランザクション/分」であれば毎月30,000トランザクションまでは無料で使用できるようです。お試しで利用する分にはとりあえず問題ないかと思います。
とりあえずやってみたいということであれば、7日間だけ利用できるゲストユーザーでもいいでしょう。

登録できたら、以下のような画面が表示されると思います。
キャプチャ3.PNG
この内、「キー1」または「キー2」がAPIキーです。この後使うので、メモしておいてください。
ちなみになぜAPIキーが2つあるのかは不明です…。
とりあえずどちらを使っても問題なくデータが取得できました。

コードをコピペする

ソースコードは公式のFace APIクイックスタートに記載のサンプルを持ってきて、一部コメントを日本語にしただけです。こう書けば使えるよ!という情報は公式ページ見れば全部載っています。
(この記事の意味…?)

今回はC#で書いてますが、Face APIクイックスタートの遷移先のサイドバーから以下のサンプルも表示できます。
(GO, Java, JavaScript, Node.js, PHP, Python, Ruby, cURL)

以下はC#のコンソールアプリ(.NET Framework)のソースです。
新規プロジェクトを作成して、using含めコピペしてください。

Program.cs
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;

namespace FaceDetection
{
    class Program
    {
        const string subscriptionKey = "あなたのAPIキー";
        const string uriBase = "https://westcentralus.api.cognitive.microsoft.com/face/v1.0/detect";

        static void Main(string[] args)
        {
            Console.WriteLine("Detect faces:");
            Console.Write("Enter the path to an image with faces that you wish to analyze: ");

            // 画像ファイルパスの入力
            string imageFilePath = Console.ReadLine();

            if (File.Exists(imageFilePath))
            {
                try
                {
                    MakeAnalysisRequest(imageFilePath);
                    Console.WriteLine("\nWait a moment for the results to appear.\n");
                }
                catch (Exception e)
                {
                    Console.WriteLine("\n" + e.Message + "\nPress Enter to exit...\n");
                }
            }
            else
            {
                Console.WriteLine("\nInvalid file path.\nPress Enter to exit...\n");
            }
            Console.ReadLine();
        }

        // Face APIを使用して画像分析を行う
        static async void MakeAnalysisRequest(string imageFilePath)
        {
            HttpClient client = new HttpClient();

            // リクエストヘッダー
            client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", subscriptionKey);

            // リクエストパラメータ
            string requestParameters = "returnFaceId=true&returnFaceLandmarks=false" +
                "&returnFaceAttributes=age,gender,headPose,smile,facialHair,glasses," +
                "emotion,hair,makeup,occlusion,accessories,blur,exposure,noise";

            // Face API呼び出し時のURLを作成
            string uri = uriBase + "?" + requestParameters;

            HttpResponseMessage response;

            // ローカルの画像ファイルパスを文字列からバイト型配列へ変換
            byte[] byteData = GetImageAsByteArray(imageFilePath);

            using (ByteArrayContent content = new ByteArrayContent(byteData))
            {
                // リクエストヘッダーの作成
                content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

                // Face APIの呼び出し
                response = await client.PostAsync(uri, content);

                // 実行結果からJSONの取得
                string contentString = await response.Content.ReadAsStringAsync();

                // JSONの出力
                Console.WriteLine("\nResponse:\n");
                Console.WriteLine(JsonPrettyPrint(contentString));
                Console.WriteLine("\nPress Enter to exit...");
            }
        }

        // ファイルパス上にある画像をバイト配列に変換
        static byte[] GetImageAsByteArray(string imageFilePath)
        {
            using (FileStream fileStream =
                new FileStream(imageFilePath, FileMode.Open, FileAccess.Read))
            {
                BinaryReader binaryReader = new BinaryReader(fileStream);
                return binaryReader.ReadBytes((int)fileStream.Length);
            }
        }

        // JSONの整形を行う
        static string JsonPrettyPrint(string json)
        {
            if (string.IsNullOrEmpty(json))
                return string.Empty;

            json = json.Replace(Environment.NewLine, "").Replace("\t", "");

            StringBuilder sb = new StringBuilder();
            bool quote = false;
            bool ignore = false;
            int offset = 0;
            int indentLength = 3;

            foreach (char ch in json)
            {
                switch (ch)
                {
                    case '"':
                        if (!ignore) quote = !quote;
                        break;
                    case '\'':
                        if (quote) ignore = !ignore;
                        break;
                }

                if (quote)
                    sb.Append(ch);
                else
                {
                    switch (ch)
                    {
                        case '{':
                        case '[':
                            sb.Append(ch);
                            sb.Append(Environment.NewLine);
                            sb.Append(new string(' ', ++offset * indentLength));
                            break;
                        case '}':
                        case ']':
                            sb.Append(Environment.NewLine);
                            sb.Append(new string(' ', --offset * indentLength));
                            sb.Append(ch);
                            break;
                        case ',':
                            sb.Append(ch);
                            sb.Append(Environment.NewLine);
                            sb.Append(new string(' ', offset * indentLength));
                            break;
                        case ':':
                            sb.Append(ch);
                            sb.Append(' ');
                            break;
                        default:
                            if (ch != ' ') sb.Append(ch);
                            break;
                    }
                }
            }

            return sb.ToString().Trim();
        }
    }
}

APIキーを書き換える

const string subscriptionKey = "あなたのAPIキー";を、先ほどメモしたAPIキーに書き換えてください。

完成!

実行してコンソール上で画像ファイルのパスを指定してあげれば、解析結果が返ってきます。簡単ですね。

uriBaseにはFace API取得時に表示された「エンドポイント+/detect」で、接続先のURLを設定しています。なのでもしFace APIのFind Similarを使う場合には「エンドポイント+/findsimilars」のように指定することになります。
もっと具体的なURLの書き方についてはFace API公式リファレンスの「Request URL」の項目を参考にしてみてください。

また、requestParametersreturnFaceAttributes=の後に指定している項目が、顔の属性としてJSON形式で返ってきます。例えば年齢と性別の情報だけが欲しければ、
"&returnFaceAttributes=age,gender"
としてあげればOKです。

どのような情報が取得できるのか?

上記コードを実行してリクエストが成功すれば、画像の解析結果がJSONで返ってきます。

今回はぱくたそよりダウンロードした以下の画像を使用してみます。
images.jpg
ちなみにJPEG、PNG、GIF(検出対象は最初のフレームのみ)、BMPの画像ファイルがサポートされており、画像サイズは1KB以上、6MB以下でなければなりません。

Face APIに画像を投げると、以下のJSONがかえってきました。
右側の「◆」「・」に各項目の説明を記載しています。

[
   {
      "faceId": "674125e9-c135-4755-b845-a4a99166c093",
      "faceRectangle": {        ◆顔の座標
         "top": 58,
         "left": 72,
         "width": 92,
         "height": 92
      },
      "faceAttributes": {
         "smile": 0.0,          ◆笑顔
         "headPose": {          ◆顔の傾き
            "pitch": -15.2,     ・上を向いているか、下を向いているか
            "roll": 3.8,        ・顔の向きはそのままに、時計回りにどのくらい傾きがあるか
            "yaw": 0.8          ・右を向いているか、左を向いているか
         },
         "gender": "male",      ◆性別
         "age": 25.0,           ◆年齢
         "facialHair": {        ◆髭
            "moustache": 0.1,   ・くち髭
            "beard": 0.1,       ・あご髭
            "sideburns": 0.1    ・ほお髯
         },
         "glasses": "NoGlasses",◆眼鏡の有無
         "emotion": {           ◆感情
            "anger": 0.001,     ・怒り
            "contempt": 0.0,    ・軽蔑
            "disgust": 0.0,     ・嫌悪
            "fear": 0.0,        ・恐れ
            "happiness": 0.0,   ・幸せ
            "neutral": 0.999,   ・通常
            "sadness": 0.0,     ・悲しみ
            "surprise": 0.0     ・驚き
         },
         "blur": {              ◆対象がぼやけて写っているか
            "blurLevel": "low",
            "value": 0.04
         },
         "exposure": {          ◆露出度
            "exposureLevel": "goodExposure",
            "value": 0.69
         },
         "noise": {             ◆ノイズ
            "noiseLevel": "low",
            "value": 0.0
         },
         "makeup": {            ◆化粧
            "eyeMakeup": false, ・アイメイク
            "lipMakeup": false  ・口紅、リップ
         },
         "accessories": [       ◆アクセサリーの有無(帽子含む)

         ],
         "occlusion": {         ◆顔を塞いでいるか
            "foreheadOccluded": false,  ・額
            "eyeOccluded": false,       ・目
            "mouthOccluded": false      ・口
         },
         "hair": {              ◆髪の毛
            "bald": 0.04,       ・薄毛の度合い
            "invisible": false, ・帽子等で髪の毛が見えない場合にtrue
            "hairColor": [      ・髪色(数値でソートされて表示。"invisible": trueだと表示されない)
               {
                  "color": "black",
                  "confidence": 0.98
               },
               {
                  "color": "brown",
                  "confidence": 0.92
               },
               {
                  "color": "other",
                  "confidence": 0.31
               },
               {
                  "color": "gray",
                  "confidence": 0.26
               },
               {
                  "color": "red",
                  "confidence": 0.1
               },
               {
                  "color": "blond",
                  "confidence": 0.09
               }
            ]
         }
      }
   }
]

結構詳細にわかりますね。
今回は1人分だけですが、同じ画像に2人以上写っていた場合、検出できた人数分の配列が返ってきます。
Detectだけでも、趣味レベルでの単純な画像ファイルの振り分けなどいろいろできそうです。

ちなみに以下のように人の顔が写っていない(と認識された)場合、
eltuneko9V9A9721_TP_V.jpg
特にエラーとはならずに空っぽの[]が返ってきます。

今回はFace APIを使用して画像から顔の属性を検出してみました。
次回はDetectで取得したFace IDを使って、2枚の画像の中に同じ人物がいるかを検出してみたいと思います。

参考ページ

Face API紹介ページ
Face APIクイックスタート(サンプルコード)
Face API公式リファレンス

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#コードのPlantUmlのクラス関係図をpythonで自動作成してみた

C#コードのPlantUmlのクラス関係図をpythonで自動作成してみた

Qiitaは初めての投稿で色々とまだまだですが、投稿することに意味がある気がしてきたので始めてみる。
(恥ずかしながら、Qiitaをキータと読むことも、1ヶ月前に知りました笑)

※コードをたまに記載していますが、まだまだ修行中なので何かあればアドバイスお願いします。

きっかけ

アルバイト先でコードの整理を頼まれました。
なんやかんやあってコードの保守が難しくなった模様。
そのためのステップとして、まずはPlantUmlを使ってコードの流れを掴みたい!
とのこと

しかしコードの量が多すぎてまだまだ未熟な私にはとても手に負えない分量。

とあらば機械に任せてしまうのが得策では?と考えて自動化スクリプト?を作ってみました。

コードの仕組み

1.ファイルを走査する

for curDir, dirs, files in os.walk(main_folder_path):
    for file_ in files:
        if file_.endswith('cs'):
            file_path = curDir + "/" + file_

2.宣言されたクラス名とnewされたクラス名をそれぞれ取得する

ー>汚すぎるのでまだ見せれません笑
綺麗に書けたらまた投稿します

3.宣言されたクラス名とnewされたクラス名をPlantUmlにまとめる

class PlantUml_Factory():
    def __init__(self):
        self.PlantUml_code = ""
        self.PlantUml_code += "@startuml salt\n"

    def register_file__class(self,classname_lst,new_class_lst,file_):
        file_ = file_.replace(".","")
        file_ = file_.replace(" ","")

        add_code = ""
        add_code += "\n"
        add_code += "file {}".format(file_) + "{\n"
        for class_inherit in classname_lst:
            for class_ in class_inherit:
                add_code += "   class {} ".format(class_) + "" + "\n"

            if len(class_inherit) != 1:
                add_code += "\n"
                add_code += "   "
                add_code += "<-".join(class_inherit)
                add_code += "\n"

        add_code += "}\n"
        self.PlantUml_code += add_code

    def Construct(self):
        self.PlantUml_code += "@enduml"
        return self.PlantUml_code

ここの部分はまだまだ綺麗になりそうですねー...
アドバイスあればお願いします!

4.あとは出力のみ
あとは各々の方法でPlantUmlのコードを作成するのみ!

これからの野望

1.newされたクラスも関係図に組み込みたい!
2.関数とか変数とかもどうにか表示してみたい!
3.全体図だけでなく、1部分だけの図も作りたい!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む