ビットコイン対円のティッカーを可視化

前回、pybitflyerを利用して bitFlyerからビットコイン対円のティッカーを2秒ごとに10分間分取得した。

testpy.hatenablog.com

ティッカーを取得、とかさらっと言っているが、実はFX初めて。 ただ株は少しやったことがあって、そうゆう人間からすると、 ティッカーを取得したら売買結果だった、というのはちょっと違和感があった。 と言うのもティッカーって、AppleならAAPLとか、Yahoo!Japanなら4689とか、企業を指すものだと思ってたから。 なので、FXで言うところのティッカーって何なのかを知るために、用語を調べたり、matplotlibで可視化してみた。

用語

まずは用語を少し調べてみたけど、こんな感じだろうか。。 間違ってたら教えて下さい。

  • best_ask: 最高買い価格
  • best_bid: 最低売り価格
  • best_ask_size: 最高買い価格の数
  • best_bid_size: 最低売り価格の数
  • ltp: 最終取引価格
  • total_ask_depth: 買い注文総数
  • total_bid_depth: 売り注文総数
  • volume_by_product: 価格ごとの出来高

best_ask, best_bid, ltpの単位は円でOKだと思うけど、 best_ask_size, best_bid_size, total_ask_depth, total_bid_depth, volume_by_productは単位はBTCかな? CSVのファイルの数値は少数第8位まであって、0.00000001BTC=1SatoshiなのでOKとは思うが。

なお、total_ask_depth, total_bid_depthのイメージは以下が分かりやすいと思う。 下のグラフの両端の数値がそれらに該当すると思われる。

f:id:Shoto:20171116004239j:plain https://en.wikipedia.org/wiki/Market_depth

コード

前回read_csv()で取得できる dfを次のplot_ticker()の引数として渡すと可視化できる。

def plot_ticker(self, df):
    fig = plt.figure(figsize=(16, 9), dpi=100)
    fig.patch.set_facecolor('white')

    ax = fig.add_subplot(3, 1, 1)
    plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
    ax.plot(df.index, df['best_ask'], ls='-', color='red', label='best_ask')
    ax.plot(df.index, df['best_bid'], ls='-', color='blue', label='best_bid')
    ax.plot(df.index, df['ltp'], ls='-', color='gray', label='ltp')
    plt.legend(loc='best')
    ax.grid()

    ax = fig.add_subplot(3, 1, 2)
    plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
    ax.plot(df.index, df['total_ask_depth'], ls='-', color='red', label='total_ask_depth')
    ax.plot(df.index, df['total_bid_depth'], ls='-', color='blue', label='total_bid_depth')
    plt.legend(loc='best')
    ax.grid()

    ax = fig.add_subplot(3, 1, 3)
    plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
    # ax.plot(df.index, df['volume'], ls='-', color='green', label='volume')
    ax.plot(df.index, df['volume_by_product'], ls='-', color='black', label='volume_by_product')
    plt.legend(loc='best')
    ax.grid()

    plt.tight_layout()
    plt.show()

結果

f:id:Shoto:20171116004242p:plain

  • 上グラフ
    best_askが上、best_bidが下のラインを支えていて, ltpがその中間にあることが確認できる。
  • 中グラフ
    観測した中ではtotal_ask_depthtotal_bid_depthをずっと下回っている。 あとtotal_ask_depthが一定なのに足して、total_bid_depthがたまに増える。
  • 下グラフ
    volume_by_productが急激に減ったと思ったら、BTCの価格が上がっている。

参考文献

bitFlyerからビットコイン対円のティッカーを取得

かなり前からビットコインが熱かったけど放置してたら完全に乗り遅れた。 今更ながらビットコインの波に乗ろうと思う。 まずは対円の情報をbitFlyerから取得しようと思う。 簡単のため、pybitflyerというPythonライブラリーを使う。

コード

api = pybitflyer.API()APIを取得して、 ticker = api.ticker(product_code='BTC_JPY')でティッカーが取得できる。 keyもsecretもいらず非常にシンプルだが、 サーバーへ2秒置きにアクセスして10分間分のティッカーを取得し、 CSVファイルに保存して、読み込みデータを表示するまでのコードを書いた。

# coding: utf-8
import pybitflyer
import pandas as pd
from time import sleep
from progressbar import ProgressBar

SLEEP_SECOND = 2  # データ取得間隔(秒)
N_MINUTE = 10  # データ取得時間(分)
DIR_DATA = '../data/'  # データ格納フォルダー
PRODUCT_CODE = 'BTC_JPY'  # 取得するデータ
FILE_BTC_JPY = 'BTC_JPY_TICKER.csv'  # TICKERファイル名


class Agent:
    def __init__(self):
        pass


    def main(self):
        self.get_ticker()
        df = self.read_ticker()


    """
  TICKER
  """
    def get_ticker(self):
        api = pybitflyer.API()
        tickers = []
        count = 60 // SLEEP_SECOND * N_MINUTE  # サーバーへのアクセス回数
        pb = ProgressBar(max_value=count)
        for i in range(count):
            ticker = api.ticker(product_code=PRODUCT_CODE)
            tickers.append(ticker)
            sleep(SLEEP_SECOND)
            pb.update(i)  # Update progressbar

        df = pd.DataFrame(tickers)
        df.to_csv(DIR_DATA+FILE_BTC_JPY, index=False)


    def read_ticker(self):
        df = pd.read_csv(DIR_DATA+FILE_BTC_JPY)
        df['timestamp'] = pd.to_datetime(df['timestamp'])  # stringからdatetimeへ

        keys = ['timestamp', 'product_code', 'tick_id', \
                'best_ask', 'best_ask_size', 'best_bid', 'best_bid_size', \
                'total_ask_depth', 'total_bid_depth', 'ltp', \
                'volume', 'volume_by_product']
        df = df[keys]  # カラムを表示したい順に並べる
        df = df.set_index('timestamp')  # timestampをindexに

        # debug
        print(df.head().to_string())
        print('')
        print(df.tail().to_string())
        print('')

        return df


if __name__ == "__main__":
    Agent().main()

結果

上記のコードをagent.pyに保存して、一つ上にdataフォルダーを作って置けば、 実行すればdataフォルダー内にCSVファイルが出力されているはず。 それを読み込んだ結果が以下の通り。 なかなか2秒ごとにデータ取得できないもんですな。

> python .\agent.py
                        product_code   tick_id  best_ask  best_ask_size  best_bid  best_bid_size  total_ask_depth  total_bid_depth       ltp         volume  volume_by_product
timestamp
2017-11-15 13:57:22.963      BTC_JPY  16622557  811140.0       1.200000  810785.0       1.822747      1781.033755      4285.326827  810785.0  207454.303454       19592.381994
2017-11-15 13:57:25.343      BTC_JPY  16622606  811139.0       0.682363  810490.0       3.907947      1782.946859      4276.717027  810490.0  207466.718872       19603.633094
2017-11-15 13:57:27.250      BTC_JPY  16622669  810678.0       0.028423  810490.0       3.907947      1784.647044      4277.266086  810679.0  207473.906758       19604.627594
2017-11-15 13:57:28.953      BTC_JPY  16622708  811068.0       0.949000  810491.0       0.005685      1785.397923      4273.360839  810490.0  207478.447690       19605.862525
2017-11-15 13:57:31.313      BTC_JPY  16622740  811066.0       0.307494  810491.0       0.012506      1784.854531      4273.544345  811067.0  207487.503203       19607.357628

                        product_code   tick_id  best_ask  best_ask_size  best_bid  best_bid_size  total_ask_depth  total_bid_depth       ltp         volume  volume_by_product
timestamp
2017-11-15 14:08:08.573      BTC_JPY  16637242  814999.0       0.800000  814900.0       0.167781      1714.455080      4223.328344  814900.0  206588.403105       19472.318694
2017-11-15 14:08:11.090      BTC_JPY  16637282  815000.0       5.638385  814697.0       0.320000      1714.380850      4225.357163  815000.0  206584.681053       19473.110742
2017-11-15 14:08:12.957      BTC_JPY  16637289  815000.0       4.738385  814698.0       1.120080      1713.758990      4225.517242  815000.0  206591.556565       19474.010742
2017-11-15 14:08:15.550      BTC_JPY  16637359  815000.0       1.123285  814800.0       0.160200      1710.768781      4219.641562  815000.0  206576.876997       19470.398019
2017-11-15 14:08:17.817      BTC_JPY  16637439  815000.0       0.784285  814800.0       0.160200      1700.492640      4216.927261  815000.0  206562.011497       19469.980019

参考文献

Twitter APIを使った検索方法

Twitter分析をすることになったため、APIを使った検索について調査検証を行った。 結論から言うと、公式のAPIは、パラメーターが少なくロクな検索ができないのだが、 クエリにパラメーターを含めることで様々な検索が可能になることが分かった。 以下にその方法を示す。

1. APIとの接続

APIへ接続する前に、CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRETを取得する。 Twitter REST APIの使い方とか読み進めていけばできると思う。 僕の場合は、昔に設定してたのが残っていたので、それを使用することにした。

取得できたら、設定ファイル(今回はconfig.py)を作成して、以下のように各コードを書き留めておく('xxx'の部分を変更)。 こうすることで、検索ファイルにべた書きするより多少リスクが軽減される。

CONSUMER_KEY        = 'xxx'
CONSUMER_SECRET     = 'xxx'
ACCESS_TOKEN        = 'xxx'
ACCESS_TOKEN_SECRET = 'xxx'

APIには、OAuthを簡単にしてくれるライブラリー requests_oauthlibOAuth1Sessionを使って接続する。 検索ファイル(今回はagent.py)を作成して、以下のように設定ファイル(config.py)から 各コードを取得し、OAuth1Sessionに渡すとAPIに接続できる。

from requests_oauthlib import OAuth1Session
import config

class Agent:

    ...

    def connect_api(self):
        api = OAuth1Session(config.CONSUMER_KEY,
                            config.CONSUMER_SECRET,
                            config.ACCESS_TOKEN,
                            config.ACCESS_TOKEN_SECRET)

        return api

2. 検索条件の設定

公式ドキュメント を見ると分かるが、ほとんど条件が指定できない。 しかし、queryに以下のように記述することで、様々な条件で検索することが可能になる。

    def make_params(self):
        query = '猫 filter:images min_replies:10 min_retweets:500 min_faves:500 exclude:retweets'
        params = {
            'q': query,
            'count': 20
        }

        return params

上記の条件は以下の通り。 他の条件については、Twitterの検索APIについて が詳しい。

key value exmaple discription
filter images 画像があるツイート
min_replies 10 リプライ数が指定値以上のツイート
min_retweets 500 リツイート数が指定値以上のツイート
min_faves 500 ライク数が指定値以上のツイート
exclude retweets リツイートでないツイート?

3. tweetの検索

上記で取得したapiparamsを引数とする検索用メソッドを以下のように作る。 ツイートはstatusesに入っているのでresultとして返す。

    def search_tweet(self, api, params):
        url = 'https://api.twitter.com/1.1/search/tweets.json'
        req = api.get(url, params=params)

        result = []
        if req.status_code == 200:
            tweets = json.loads(req.text)
            result = tweets['statuses']        
        else:
            print("ERROR!: %d" % req.status_code)
            result = None

        assert(len(result) > 0)

        return result

statusに入っているツイートは各々 Tweet data dictionariesTweet Data Dictionaryのkeyを持っている。 とりあえず簡単な分析に必要なkeyを標準出力させるメソッドは次の通り。

    def output_tweets(self, result):
        for r in result:
            for k,v in r.items():
                if k in ['text', 'retweet_count', 'favorite_count', 'id', 'created_at']:
                    print(k+':')
                    print(v)
                    print('    ')
            print('-----------------------------------------------------------------')

以下が出力結果。条件通りの検索結果が返ってきていることが分かる。

id:
925369434549624832

text:
我が家のネコさま。←むかし いま→ https://t.co/qg3TXEZpUv

favorite_count:
63259

created_at:
Tue Oct 31 14:30:40 +0000 2017

retweet_count:
30089

-----------------------------------------------------------------
id:
924974322115940357

text:
クロネコ https://t.co/JNZLtFHn2z

favorite_count:
19209

created_at:
Mon Oct 30 12:20:38 +0000 2017

retweet_count:
5106

-----------------------------------------------------------------
id:
924485965237731328

text:
【猫から学ぶ女子力】真枝アキ『彼氏のネコがかわいくない!』 https://t.co/y8lmBlbHkC #ツイ4 https://t.co/3J3AxKVnvV

favorite_count:
3255

created_at:
Sun Oct 29 04:00:04 +0000 2017

retweet_count:
847
-----------------------------------------------------------------

4. 文字化け対策

なおWindows環境だと、コマンドプロンプト上で、 上記のように日本語等を出力しようとすると、文字化けが起こる可能性が高い。 理由はPythonutf-8で扱っているのに、コマンドプロンプトはcp932を扱っているから。 もし文字化けが起こった場合は、以下を実行してみるとよい。

これでFuck コマンドプロンプトと思うことが、ちょっとだけ少なくなる。

参考文献

Playerと複数生成されるSpawnerの位置を取得

UnityでFlappy Birdを作ってみたが、すぐにPlayer(Flappy Bird)を自動制御したくなったので、まずはPlayerとSpawner(土管)の位置を取得してみた。


Flappy Bird Made with Unity

Spawnerの設定

Spawnerは土管で、1秒ごとにY軸をランダムにずらして生成し、Playerに向かわせている。 Spawnerが生成された際にタグを付けることで、Player側でSpawnerの位置を取得することができる。

 public GameObject wallPrefab;  // Spawnerとして土管のprefabを設定する
    public float interval;  // 1秒ごとに生成する
    private GameObject spawner;  // 生成されたSpawnerを格納する
    public string tag;  // Spawnerにタグを付ける

    // Use this for initialization
    IEnumerator Start () {
        while (true) {
            // 画面外の座標(12, 2)をベースにY軸を0.0~4.0だけランダムにずらしてSpawnerを生成するよう設定
            transform.position = new Vector2(transform.position.x, Random.Range(0.0f, 4.0f));
            // Spawnerを生成
            spawner = Instantiate(wallPrefab, transform.position, transform.rotation);
            // 生成したSpawnerにタグ付け!
            spawner.tag = "Wall";

            yield return new WaitForSeconds(interval);
        }
    }

なお、Spawnerに指定した土管prefabには1秒ごとに5ずつX軸マイナス方法へ向かうように設定してある。

Playerの設定

Playerはフレームごとに状態を観測することができる。 このスクリプトがPlayerのインスタンスになるので、Playerの位置は簡単に取得できるのだが、 Spawnerの位置は、上記のようにタグ付けをすることで、Playerからも取得することができるようになる。

    public float jumpPower;  // Playerは強さ5でジャンプ
    private int num_wall = 2;  // SpawnerはPlayerに最も近い2つだけ位置を取得

    // Update is called once per frame
    void Update () {
        // Initialize state
        // 観測する位置は以下の5つ
        // [player_pos_y, wall1_pos_x, wall1_pos_y, wall2_pos_x, wall2_pos_y]
        List<float> state = new List<float>();  // 位置を格納するリスト

        // Set player position x
        var pos_player = this.transform.position;  // Playerの位置
        state.Add(pos_player.y);  // リストにPlayerのY座標を追加

        // Set wall position x and y
        GameObject[] walls = GameObject.FindGameObjectsWithTag("Wall");  // タグから全Spawnerを取得
        foreach (var wall in walls) {  // 全Spawnerについて生成された順に
            var pos_wall = wall.gameObject.transform.position;  // Spawnerの位置を取得
            // SpawnerがPlayerより前にいて、かつSpawnerの数がリストに2つ未満の場合
            // If a wall is behind the player & the number of wall in state is not max
            if ((pos_player.x <= pos_wall.x + 1.0f) && (state.Count < 1+2*num_wall)) {  
                state.Add(pos_wall.x);  // SpanerのX座標を格納
                state.Add(pos_wall.y);  // SpanerのY座標を格納
            }
        }

        // debug
        // 取得したPlayerとSpanerの位置を出力
        string text = "";
        foreach (var s in state) {
            text = text + s.ToString() + " ";
        }
        print(text);

    }

位置の取得結果

Playerのスクリプトで設定したコンソールへの出力は以下のようになる。 数値の内容は[player_pos_y, wall1_pos_x, wall1_pos_y, wall2_pos_x, wall2_pos_y]。 SpawnerがX座標マイナス方向にずれていくのが観測できる。 PlayerのX座標は変わらないので、Y座標だけ取得している。

f:id:Shoto:20171103161032p:plain

ソースコード

SpawnerとPlayerの全ソースコードを以下に示しておく。

  • Spawner.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Spawner : MonoBehaviour {

    public GameObject wallPrefab;
    public float interval;
    private GameObject spawner;
    public string tag;

    // Use this for initialization
    IEnumerator Start () {
        while (true) {
            transform.position = new Vector2(transform.position.x, Random.Range(0.0f, 4.0f));
            spawner = Instantiate(wallPrefab, transform.position, transform.rotation);
            spawner.tag = "Wall";

            yield return new WaitForSeconds(interval);
        }
    }

    // Update is called once per frame
    void Update () {

    }
}
  • Player.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour {

    public float jumpPower;

    private int num_wall = 2;

    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () {
        // Initialize state
        // [player_pos_y, wall1_pos_x, wall1_pos_y, wall2_pos_x, wall2_pos_y]
        List<float> state = new List<float>();

        // Set player position x
        var pos_player = this.transform.position;
        state.Add(pos_player.y);

        // Set wall position x and y
        GameObject[] walls = GameObject.FindGameObjectsWithTag("Wall");
        foreach (var wall in walls) {
            var pos_wall = wall.gameObject.transform.position;
            // If a wall is behind the player & the number of wall in state is not max
            if ((pos_player.x <= pos_wall.x + 1.0f) && (state.Count < 1+2*num_wall)) {  
                state.Add(pos_wall.x);
                state.Add(pos_wall.y);
            }
        }

        // debug
        string text = "";
        foreach (var s in state) {
            text = text + s.ToString() + " ";
        }
        print(text);

        // Manual operation
        if (Input.GetButtonDown("Jump")) {
            GetComponent<Rigidbody2D>().velocity = new Vector2(0, jumpPower);
        }
    }

    /*
   void OnCollisionEnter2D (Collision2D other) {
       // Application.LoadLevel(Application.loadedLevel);  //old function
        UnityEngine.SceneManagement.SceneManager.LoadScene(UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex);    
   }
   */

    void OnCollisionEnter2D (Collision2D other) {
        Invoke("Restart", 1.0f);     
    }

    void Restart () {
        // Application.LoadLevel(Application.loadedLevel);  //old function
        UnityEngine.SceneManagement.SceneManager.LoadScene(UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex);   
    }

}

参考文献

GitHub Desktop for Windowsの"failed to sync this branch"の対処

GitHub Desktopを立ち上げた状態で別作業をしていたら、GitHub Desktop関連のエラーを示すポップアップが4つ連続で出てきた。 ポップアップを消した後も何度か出てきて、実際、GitHub Desktopで同期するとfailed to sync this branchと表示され、 GitHubと同期できなくなってしまった。

Shell操作で対処できている記事が幾つか見られたが、僕の環境ではまったく無意味だったため、 コンパネからGitHub Desktopの「アンインストールと変更」を行おうとしたところ、以下のようなウィンドウが出た。

f:id:Shoto:20170923222824p:plain

開いたときは、アプリケーションを以前の状態に復元します。が選択可能になっていたので、 選択して、OKボタンを押して、GitHub Desktopを再起動したら、うまく同期できるようになった。 復元後にGitHub Desktopの「アンインストールと変更」ボタンをクリックしたら、 アプリケーションを以前の状態に復元します。が選択できなくなっていた。 問題ある時のみ選択できるっぽい。

Windows10でMongoDBの設定

Windows10でMongoDBを使おうとしたら、少しつまづいたのでメモとして残しておく。

1. ダウンロードとインストール

MongoDB Download Centerから Windows Server 2008 R2 64-bit and later, with SSL support x64をダウンロードしてインストール。

2. パスを通す

デフォルトのパスC:\Program Files\MongoDB\Server\3.4\binをシステム環境のPathに登録。

3. ログとデータの保存場所を確保

僕の環境だとMongoDBをインストールしたCドライブが0.5TBしかないのに対して、Dドライブが1.8TBあるので、 ビッグデータに備えて、以下のようにDドライブにログとデータの保存場所を作成しておく。ここは各自の好みでOK。

D:\db\mongodb\log\
D:\db\mongodb\data\

4. MongoDBの設定ファイルの作成

MongoDBを起動するときは、毎回上記のパスを教える必要がある。 面倒なので、以下の内容の設定ファイルmongod.cfgを作成して、C:\Program Files\MongoDB\Server\3.4\bin\に置いておく。 拡張子は、cfgでもconfでもconfigでも何でもいいっぽい。なお、設定ファイルの置き場所も各自の好みでOK。

systemLog:
    destination: file
    path: D:\db\mongodb\log\mongod.log
storage:
    dbPath: D:\db\mongodb\data

5. 設定ファイルのWindowsサービス登録

上記で作成した設定ファイルをWindowsサービスに登録することで、Windowsと同時にMongoDBを起動できる。 コマンドプロンプトを管理者権限で立ち上げて、mongod --config "C:\Program Files\MongoDB\Server\3.4\bin\mongod.cfg" --installと入力して実行する。 しかし、以下のようなエラーが出る可能性がある。

PS C:\WINDOWS\system32> mongod --config "C:\Program Files\MongoDB\Server\3.4\bin\mongod.cfg" --install
Error parsing YAML config file: yaml-cpp: error at line 5, column 8: illegal map value
try 'C:\Program Files\MongoDB\Server\3.4\bin\mongod.exe --help' for more information

設定ファイルについて、以下の4つが守られていないことが原因と考えられる。

  • utf-8で保存
  • タブを使わない
  • systemLogとstorageの間を開けない
  • パスをダブルクォーテーションで囲まない

修正したら、以下の通り上手くいくはず。

PS C:\WINDOWS\system32> mongod --config "C:\Program Files\MongoDB\Server\3.4\bin\mongod.cfg" --install
PS C:\WINDOWS\system32>

6. Pythonからの操作

PythonからMongoDBを操作する場合は、pymongoを使えるようになるとよい。 以下の記事でCRUD操作の基礎を学べる。

testpy.hatenablog.com

参考文献

Swiftで写真を撮影して保存するiPhoneアプリを作成

iPhoneのカメラ機能を利用する入門的なアプリをサクッと作る。 フレームワークの各関数の使い方をよく忘れるのでメモ代わりに記録として残しておく。

画面

画面は1つのみで、プレビュー用のView(cameraView)とシャッター用のButton(take)の2つの部品だけを配置。 下図ではViewを正方形に配置しているが、実際に保存されるアスペクト比に近い形の方が良いとは思う。

f:id:Shoto:20170920011910p:plain

コード

以下のコードを実装して、iPhone繋いでBuild&Runして、takeボタン押せば、 プレビュー画面が写真としてカメラロールに保存される。 コードの解説については、コメントを参照。

import UIKit
import AVFoundation

class FirstViewController: UIViewController, AVCapturePhotoCaptureDelegate {
    // カメラ等のキャプチャに関連する入出力を管理するクラス
    var session: AVCaptureSession!
    // 写真データを取得するクラス
    var outout: AVCapturePhotoOutput?
    // カメラでキャプチャした映像をプレビューするクラス
    var previewLayer: AVCaptureVideoPreviewLayer?

    // カメラでキャプチャした映像をプレビューするエリア
    @IBOutlet weak var cameraView: UIView!

    // ロードされた直後に呼び出される(初回に一度のみ)
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    // 画面が表示される直前に呼び出される
    override func viewWillAppear(_ animated: Bool) {
        // キャプチャー入出力と写真データ取得のクラスの初期化
        session = AVCaptureSession()
        outout = AVCapturePhotoOutput()

        // 解像度の設定
        //session.sessionPreset = AVCaptureSessionPreset1920x1080
        session.sessionPreset = AVCaptureSessionPreset3840x2160

        // カメラの選択(背面・前面など)
        let camera = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)

        do {
            // 指定したデバイスをセッションに入力
            let input = try AVCaptureDeviceInput(device: camera)

            // 入力
            if (session.canAddInput(input)) {
                session.addInput(input)

                // 出力
                if (session.canAddOutput(outout)) {
                    session.addOutput(outout)
                    session.startRunning() // カメラ起動

                    // プレビューレイヤーを生成
                    previewLayer = AVCaptureVideoPreviewLayer(session: session)
                    // cameraViewの境界をプレビューレイヤーのフレームに設定
                    previewLayer?.frame = cameraView.bounds
                    // アスペクト比変更とレイヤー収納の有無
                    previewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill  // アスペクト比を変ない。レイヤーからはみ出した部分は隠す。
                    //previewLayer?.videoGravity = AVLayerVideoGravityResizeAspect  // アスペクト比を変えない。はみ出さないようにレイヤー内に収める。

                    // cameraViewのサブレイヤーにプレビューレイヤーを追加
                    cameraView.layer.addSublayer(previewLayer!)
                }
            }
        } catch {
            print(error)
        }
    }

    // ボタンをタップした時に呼ばれる
    @IBAction func takePhoto(_ sender: Any) {
        // 撮影設定
        let settingsForMonitoring = AVCapturePhotoSettings()
        settingsForMonitoring.flashMode = .auto  // フラッシュのモード
        settingsForMonitoring.isAutoStillImageStabilizationEnabled = true  // 手振れ補正
        settingsForMonitoring.isHighResolutionPhotoEnabled = false  // 最高解像度で撮影するか否か

        // シャッターを切る
        outout?.capturePhoto(with: settingsForMonitoring, delegate: self)
    }

    // AVCapturePhotoCaptureDelegateのデリゲート
    // カメラで撮影が完了した後呼ばれる。撮影データを加工したり、アルバムに保存したりする。
    func capture(_ captureOutput: AVCapturePhotoOutput,
                 didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?,
                 previewPhotoSampleBuffer: CMSampleBuffer?,
                 resolvedSettings: AVCaptureResolvedPhotoSettings,
                 bracketSettings: AVCaptureBracketedStillImageSettings?,
                 error: Error?) {

        if let photoSampleBuffer = photoSampleBuffer {
            // JPEG形式で画像データを取得
            let photoData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: photoSampleBuffer, previewPhotoSampleBuffer: previewPhotoSampleBuffer)
            // UIImage型に変換
            let image = UIImage(data: photoData!)

            // フォトライブラリに保存
            UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
        }
    }

    // 警告を受け取ったときに呼ばれる
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

トピックス

解像度の設定

// 解像度の設定
//session.sessionPreset = AVCaptureSessionPreset1920x1080
session.sessionPreset = AVCaptureSessionPreset3840x2160

もう少し写真の解像度を上げたいなどの場合は、ここで設定を行う。 指定できる解像度の種類は以下の通り。

Symbol Description
AVCaptureSessionPresetPhoto 高解像度の写真品質出力
AVCaptureSessionPresetHigh 高品質のビデオおよびオーディオ出力
AVCaptureSessionPresetMedium WiFi経由での共有出力ビデオおよびオーディオビットレート
AVCaptureSessionPresetLow 3G経由での共有出力ビデオおよびオーディオビットレート
AVCaptureSessionPreset320x240 320x240ピクセルビデオ出力
AVCaptureSessionPreset352x288 CIF画質(352x288ピクセル)ビデオ出力
AVCaptureSessionPreset640x480 VGA画質(640x480ピクセル)ビデオ出力
AVCaptureSessionPreset960x540 クオータHD品質(960x540ピクセル)ビデオ出力
AVCaptureSessionPreset1280x720 720p画質(1280x720ピクセル)のビデオ出力
AVCaptureSessionPreset1920x1080 1080p品質(1920x1080ピクセル)ビデオ出力
AVCaptureSessionPreset3840x2160 2160p(UHDまたは4Kとも呼ばれる)画質(3840x2160ピクセル)ビデオ出力
AVCaptureSessionPresetiFrame960x540 AACオーディオで960x540の高品質iFrame H.264ビデオを約30Mbits/secで実現する設定
AVCaptureSessionPresetiFrame1280x720 1280x720の高品質iFrame H.264ビデオをAACオーディオで約40 Mbits/secで実現する設定
AVCaptureSessionPresetInputPriority キャプチャセッションがオーディオおよびビデオ出力設定を制御しないことを指定

[iOS] AVFundationを使用して、「ビデオ録画」や「連写カメラ」や「QRコードリーダー」や「バーコードリーダー」を作ってみた - Developers.IO より

アスペクト比変更とレイヤー収納の有無

// アスペクト比変更とレイヤー収納の有無
previewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill  // アスペクト比を変ない。レイヤーからはみ出した部分は隠す。
//previewLayer?.videoGravity = AVLayerVideoGravityResizeAspect  // アスペクト比を変えない。はみ出さないようにレイヤー内に収める。

プレビューの変更を行う。上記の設定ではcameraViewに実際に撮影される写真の一部しかプレビューで表示されないが、 設定によっては全部表示させることも可能。以下の3種類がある。

Symbol Description
AVLayerVideoGravityResize アスペクト比を変えてレイヤーに収める。
AVLayerVideoGravityResizeAspect アスペクト比を変えない。はみ出さないようにレイヤー内に収める。
AVLayerVideoGravityResizeAspectFill アスペクト比を変ない。レイヤーからはみ出した部分は隠す。

AVCaptureVideoPreviewLayer の Video Gravity3種 - 木木木 より

参考文献