Jetson TX1 と SONY Camera Remote API で Deep Learning なデジカメを作る

# はじめに
NVIDIA Jetson TX1 にWebカメラを接続すれば，カメラに映ったものをリアルタイムに物体認識するようなプログラムを作ることができます．JetsonはGPUを積んでいるので Deep Learning の推論もスムーズに動きます．この記事では，JetsonにSONYのデジタルミラーレス一眼を接続するための方法を説明します．Python/OpenCV/Caffeで実装します．

# 準備
ハードウェア，ライブラリ等，色々準備していきます．

## SONY Camera Remote API 
- [SONY Camera Remote API](https://developer.sony.com/ja/develop/cameras/)

SONY製のデジタルカメラは外部からカメラを制御できるAPIを解放しています．上記WebサイトでAndroid版のSDKが配布されています．ただし，**この記事ではLinuxからPythonでカメラと接続するので上記SDKは不要です**．最近のSONY製デジタルカメラはだいたい対応しているようですが，本記事のコードはSONY ILCE-QX1（以下，QX1）で動作確認しています[^1]．

[^1]: その他のデジカメの対応状況 https://developer.sony.com/ja/develop/cameras/api-information/supported-devices

##  ハードウェア
- [NVIDIA Jetson TX1](https://www.nvidia.com/ja-jp/autonomous-machines/embedded-systems-dev-kits-modules/)
- [SONY ILCE-QX1](https://www.sony.jp/ichigan/products/ILCE-QX1/)

## ソフトウェア
Jetson TX1（以下，TX1）にJetPackでインストールしておきます．
- OpenCV4Tegra
- Caffe
- Python 2.7


インストール作業はこちらを参考にしました．
- [JETSON TX1が来てやったこと（１日目）](https://qiita.com/threecups/items/d0be58e133ca44f230c4)
- [Jetson TX1の覚え書き（JetPackを用いたセットアップ）](http://hirotaka-hachiya.hatenablog.com/entry/2017/05/02/111010)


## Pythonライブラリ
TX1 の Python 2.7 環境にインストールしておきます．
- [SonyPy](https://github.com/storborg/sonypy) [^2] 
- skimage
- numpy

[^2]: SonyPyのオリジナルコードは，MITライセンスで配布されており，Cart Logic, Inc. が著作権を保持しています．Copyright 2013 Cart Logic, Inc. Author: Scott Torborg, http://github.com/storborg/sonypy

# SonyPy修正
SonyPyはQX1には非対応のため下記の通り修正が必要です．

## sonypy/discovery.py
```Python
dd_regex = ('<av:X_ScalarWebAPI_Service>'
            '\s*'
            '<av:X_ScalarWebAPI_ServiceType>'
            '([^<]+)'
            '</av:X_ScalarWebAPI_ServiceType>'
            '<av:X_ScalarWebAPI_ActionList_URL>'
            '([^<]+)'
            '</av:X_ScalarWebAPI_ActionList_URL>'
            '\s*'
            '<av:X_ScalarWebAPI_AccessType/>'
            '</av:X_ScalarWebAPI_Service>')
```
```Python
def _parse_ssdp_response(self, data):
    lines = data.split('\n')
    if 'HTTP/1.1 200 OK' in lines[0]:
		print 'OK'
	else:
		print 'NG'

    headers = {}
    for line in lines[1:]:
        kv = line.split(': ', 1)
        key = kv[0]
        if len(kv) >= 2:
            val = kv[1]
        else:
            val = ''
    
        headers[key.lower()] = val
    return headers
```
```Python
def _parse_device_definition(self, doc):
    """
    Parse the XML device definition file.
    """
    services = {}
    for m in re.findall(dd_regex, doc):
        service_name = m[0]
        endpoint = m[1]
        services[service_name] = endpoint
    return services
```
```Python
def _read_device_definition(self, url):
    """
    Fetch and parse the device definition, and extract the URL endpoint for
    the camera API service.
    """
    r = requests.get(url)
    rt = r.text.strip().replace(" ", "").replace("\t", "").replace("\r", "").replace("\n", "")
    services = self._parse_device_definition(rt)
    return services['camera']
```

## sonypy/camera.py
```Python
def _do_request(self, method, service, *args):
    body = dict(method=method,
                params=args,
                id=1,
                version=self.version)
    data = json.dumps(body)
    r = requests.post(self.endpoint + '/' + service, data=data)
    resp = json.loads(r.text)
    return resp['result']
```
```Python
def _decode_common_header(self, buf):
    start, ptype, seq, timestamp = struct.unpack('>BBHI', buf)
    return start, ptype, seq, timestamp
```
```Python
def _decode_payload_header(self, ptype, buf):
    format = '>IBBBBIB'
    buf = buf[:struct.calcsize(format)]
    jpeg_size = struct.unpack('I', bytearray([buf[6], buf[5], buf[4], 0x00]))[0]
    padding_size = struct.unpack('I', bytearray([buf[7], 0x00, 0x00, 0x00]))[0]
    return jpeg_size, padding_size
```
```Python
def stream_liveview(self, url):
    """
    Connect to a liveview-format URL and yield a series of JPEG frames.
    """
    r = requests.get(url, stream=True)
    while True:
        # Read common header, 8 bytes.
        start, ptype, seq, timestamp = self._decode_common_header(r.raw.read(8))
        if start != 255:
            continue
        if ptype == 18:
            # skip
            r.raw.read(4+3+1+2+118+4+4+24)
        elif ptype == 1 or ptype == 17:
            # Read payload header, 128 bytes.
            jpeg_size, padding_size = \
                self._decode_payload_header(ptype, r.raw.read(128))
            # Read JPEG frame.
            jpeg_frame = r.raw.read(jpeg_size)
            # Throw away the padding.
            r.raw.read(padding_size)
            yield [jpeg_frame, seq, timestamp]
```
_do_request関数の修正にともない，呼び出し箇所も適宜修正する必要があります．呼び出し箇所は camera.py ファイル内部に複数存在するので，それぞれ引数に'camera'を追加します．例えば，下記の通り．

```Python
    result = self._do_request('actTakePicture', 'camera')
```





# 画像の取得から処理まで
QX1から画像を取得して，OpenCVでの画像処理やCaffeによる推論を実行するPythonプログラムを説明します．

## 準備
- QX1のWi-Fiアクセスポイントを立ち上げる
- TX1から上記のアクセスポイントにWi-Fi接続する

## ライブラリ読み込み
```Python
from sonypy import Discoverer, Camera
import cv2
import numpy
import skimage
import caffe
```
## QX1との接続確立
修正したSonyPyを使って，PythonからQX1との接続を確立します．

```Python
d = Discoverer()
c = d.discover()
cam = c[0]
versions = cam.get_versions()
stream_url = cam.start_liveview()
stream = cam.stream_liveview(stream_url) #BGR順
```
## QX1 -> OpenCV
QX1で取得できるライブビューストリームからnextで次々に画像(frame)を取得できます．frameは1枚のJPEG圧縮ファイルのバイナリデータです．OpenCVのimdecode関数でJPEGバイナリを復号して，numpy型に変換します．nextはfor/while文の中で使います．

```Python
cv2.namedWindow('img')
while True:
    # QX1 -> JPEG
    [frame, seq, ts] = stream.next()
    nparr = np.fromstring(frame, np.uint8)
    # JPEG -> OpenCV
    cv_img = cv2.imdecode(nparr, cv2.CV_LOAD_IMAGE_COLOR) #BGR順

    # (ここでリサイズ等のOpenCVによる色々な画像処理を実行)

    # 表示
    cv2.imshow('img', cv_img)
```
## OpenCV -> Caffe
```Python
    # OpenCV -> Caffe image
    # (w,h)はCaffeのNWに入力可能なサイズ
    cv_img = cv2.resize(cv_img, (w, h))
    caffe_img = skimage.img_as_float(cv_img).astype(np.float32)

    # (おまけ)
    # Caffeによる推論を実行．ただしCaffeのモデルファイル読み込みはwhileの前にやっておく
    net.blobs['data'].data[0] = transformer.preprocess('data', caffe_img)
    result = net.forward()
# end while

cv2.destroyAllWindows()
```

## その他
Caffeの推論処理とOpenCVの表示処理はシングルスレッドで回すと処理が重くなってカクカクすることがあります．Pythonのmultiprocessライブラリ等を使って，推論処理と表示処理を並列処理させると動作がスムーズです．

# おわりに
本記事ではLinuxをSONYのデジカメと接続する方法，OpenCV/Caffeでライブビューを取り扱う方法を説明しました．実はSONY以外のカメラメーカーもSDKを提供しています．RICOHは中判645Zにも対応！アツい！
- [RICOH Camera SDK](https://api.ricoh/)
- [OLYMPUS AIR01](https://olympus-imaging.jp/product/opc/a01/)

写真の美しさを評価するCaffeモデルを作成してJetsonに載せてデジカメを作り，CDS研究会で報告しました．
- 井上 義隆, 松村 択磨, 深澤 佑介, 山田 和宏, "深層学習による美的評価エンジンの開発と構図推薦カメラへの実装", 研究報告コンシューマ・デバイス＆システム（CDS）, 2018.