adbコマンドを使用してスマホで撮影した画像をローカルPCにコピーしてみた

## やること
最近は画像系の仕事をする機会が多く、今回はスマホで撮影した画像をPC側に(自動で)送る方法を検証したので、その結果を記事にします。
android開発のお作法にのっとっていない可能性はありますが、とりあえずこの方法でできたという方法を記載します。

前提としてPCとスマホはケーブルでつながっていて、スマホで撮影した画像をPC側にコピーするところまでとします。
(本来はその後、PC側で撮影した画像を加工したり、機械学習で使用したりとするが、まずは撮影して画像を持ってくるところまで)

ちなみに、当職はandroidアプリの開発経験はなく、今回たまたまandroidのスマホで画像を取らなければならなくなった為にコマンドを調べていたという経緯があります。

## 環境
PC：windows10
Python 3.7.3
スマホ：Galaxy S8
Android：バージョン 9

コマンドの実行に関しては、基本的にPythonのNotebookで確認してます。（たまにコマンドラインで実行）
もし、うまく実行できないなどあれば教えてくださると助かります。修正します。

adbコマンドという外部からAndroid 端末と通信できるコマンドライン・ツールを用いてスマホ(Galaxy S8)を操作しました。

adbコマンドリファレンス
https://developer.android.com/studio/command-line/adb.html?hl=ja

## 準備

■モジュール

準備として使用しているPCでadbコマンドが打てるようになる必要があります
下記からモジュールをダウンロードしました

https://developer.android.com/studio/releases/platform-tools.html

上記モジュールはzipファイルなので解凍してパスを通してください
最終的にコマンドラインでadbコマンドが打てるようになっていればOKです

```python:adbコマンド(コマンドラインから実行)
> adb
Android Debug Bridge version 1.0.41
Version 29.0.1-5644136
Installed as C:\work\util\adb-tool\platform-tools\adb.exe

global options:
 -a         listen on all network interfaces, not just localhost
 -d         use USB device (error if multiple devices connected)
 -e         use TCP/IP device (error if multiple TCP/IP devices available)
 -s SERIAL  use device with given serial (overrides $ANDROID_SERIAL)
 -t ID      use device with given transport id
 -H         name of adb server host [default=localhost]
 -P         port of adb server [default=5037]
 -L SOCKET  listen on given socket for adb server [default=tcp:localhost:5037]
・
・
<省略>
```


■設定
スマホ側の「開発者向けオプション」で下記設定をONにしました。
　・USBデバッグ

■接続時
PCにスマホを接続すると下記の警告が出ますが、「許可」してください。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225931/5a6f71a0-bc73-8a06-ee12-0ad2c90463d4.jpeg" width=30%>


## 手順

下記の順番で検証していきます。

①カメラアプリを起動・停止
②撮影
③撮影した画像をPCにコピー
④スマホ内の画像を削除
⑤最後にコードまとめ

# ①カメラアプリを起動・停止

まずは、カメラアプリを起動します。
Pythonから外部プロセスを呼び出すにはsubprocessモジュールを使用するのが推奨されているらしく、その方法で実行しました。コマンドは引数を含めすべてをcmdの変数にいれて、最終的にsubprocess.callで実行しています。

他にも方法はたくさんあると思いますが、色々試した結果今回はcmdにコマンドを入れてから実行する方法で統一しました

```python:カメラ起動
import subprocess

cmd = ('adb', 'shell', 'am', 'start', '-a', 'android.media.action.STILL_IMAGE_CAMERA')
subprocess.call(cmd)
```

うまくいけば、カメラアプリが起動しているはずです。

カメラの停止は下記で停止しました。
何故か"am" "stop"というコマンドがなく?カメラアプリのパッケージ名を調べて、それを停止しました。

カメラアプリのパッケージは下記のコマンドで確認しました。
cameraっぽい名前のアプリ(package:com.sec.android.app.camera)を指定して、"force-stop"したらうまく停止してくれました。

```python:カメラアプリのパッケージ名確認(コマンドラインから実行)
> adb shell pm list packages | findstr camera
```
```python:結果
package:com.sec.factory.camera
package:com.samsung.android.app.camera.sticker.facear.preload
package:com.sec.factory.iris.usercamera
package:com.samsung.android.app.camera.sticker.stamp.preload
package:com.sec.android.app.camera
```


```python:カメラ停止
import subprocess

cmd = ('adb', 'shell', 'am', 'force-stop', 'com.sec.android.app.camera')
subprocess.call(cmd)
```

うまくいっていれば、起動したカメラアプリが停止しているはずです。

# ②撮影

次に撮影を行います。
撮影に関しても、cmdの変数にすべて入れてから実行します


```python:カメラ撮影
import subprocess

cmd = ('adb', 'shell', 'input keyevent KEYCODE_CAMERA')
subprocess.call(cmd)
```

成功すると画像が撮影されているはずです。
ちなみに、カメラを起動していない状態でこのコマンドを実行すると、カメラが起動します。
起動コマンドと撮影コマンドが同じというのも気持ち悪いと思ったので、今回は起動と撮影のコマンドは一応分けています。

# ③撮影した画像をPCにコピー

次に撮影した画像をPC側にコピーします。

ちなみに、私のスマホでは下記の「/storage/self/primary/DCIM/Camera/」ディレクトリに画像が保存されていました。
（androidはGalaxy S8しか触ったことないので、他のAndroidスマホで同じ構成かどうかは不明。）

```python:撮影された画像確認（コマンドラインで確認）
> adb shell ls -l /storage/self/primary/DCIM/Camera/
```
```python:結果
total 16984
-rw-rw---- 1 root sdcard_rw 3595120 2019-08-16 18:18 20190816_181802.jpg
-rw-rw---- 1 root sdcard_rw 4586823 2019-08-16 18:36 20190816_183614.jpg
-rw-rw---- 1 root sdcard_rw 4590726 2019-08-16 18:36 20190816_183618.jpg
-rw-rw---- 1 root sdcard_rw 4613650 2019-08-16 18:36 20190816_183652.jpg
```

撮影された画像をPCにコピーします
lsで取得したファイル名がバイナリ？だったので、decodeなど色々工夫して1ファイルづつに分割しました
(もしかしたらうまくファイル名を取得できる方法があるかもしれませんが、今回はこの方法しか思いつかなかった・・・)

Camera配下を「*」にしているのはすべての画像を一度に持ってきたかったからです。
（特定の画像だけとか、lsのオプションによってうまくできると思います）

```python:画像のファイル名取得
cmd=('adb', 'shell', 'ls', '/storage/self/primary/DCIM/Camera/*')
fnames=subprocess.check_output(cmd).decode('utf-8').rstrip().split('\r\n')
fnames
```
```python:結果
['/storage/self/primary/DCIM/Camera/20190816_181802.jpg',
 '/storage/self/primary/DCIM/Camera/20190816_183614.jpg',
 '/storage/self/primary/DCIM/Camera/20190816_183618.jpg',
 '/storage/self/primary/DCIM/Camera/20190816_183652.jpg']
```

画像をローカルPCの「C:/work/pic」にコピーします

```python:画像をPCにコピー
for fname in fnames:
    cmd = ('adb', 'pull', fname, 'C:/work/pic')
    subprocess.call(cmd)
```

PC側にコピーされたことを確認します

![screenshot.386.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225931/91d13507-1fbb-3798-6936-ac2d1895a0f4.jpeg)


# ④スマホ内の画像を削除

今回は撮影した画像をすべてPC側に持ってくるという仕様にしているので、撮影後はスマホ側の画像はすべて削除することにしました。
下記でスマホ側の画像を削除することが可能です

```python:スマホ側の画像を削除
for fname in fnames:
    cmd = ('adb', 'shell','rm', fname)
    subprocess.check_output(cmd)
```


コマンド自体はうまくいくのだが、たまにPCからCamera配下のスマホ画像を確認しようとすると反映が遅かったり、遅延しているように見えるなど調子が悪いときがあった。
（調子が良いときは即時反映されて見えるのでスマホ側の問題かもしれません）


# ⑤最後にコードまとめ

今までの①～④のコードを関数化してまとめてみました。
少し改良し渡した引数(num)分の撮影を2秒おきに行い、PC側にコピーするというものです。
（元々撮影対象をターンテーブルに置いて、ターンテーブルを回しながら一定の間隔で360°画像を取りたかったので、今回のようなコードになりました）

簡易的に作成しましたので、参考にする場合は用途に合わせて修正してご利用ください。
今回のコードはスマホ側の画像が0枚の状態からじゃないとうまく動かないようになっています。

```python:関数定義
import subprocess
import time
import os

PICDIR='C:/work/pic'

# 関数定義
def pic_shot(num):
    """
    渡された引数の回数分をandroid端末でカメラ撮影を実施
    カメラ画像をPC側へコピー
    android端末の画像を削除(0枚の状態がデフォルト)
    最後に撮影した画像ファイルの名前をリターンする
    """
    
    # カメラアプリ起動用のコマンド
    print("カメラを起動します")
    cmd = ('adb', 'shell', 'am', 'start', '-a', 'android.media.action.STILL_IMAGE_CAMERA')
    subprocess.call(cmd)
    time.sleep(1)
    
    # num回撮影
    print('撮影を' + str(num) + '回実行します')
    for i in range(1,num+1):
        cmd = ('adb', 'shell', 'input keyevent KEYCODE_CAMERA')
        subprocess.call(cmd)
        time.sleep(2)
    
    # アンドロイド側の画像名を取得
    print("画像のコピー処理を行います")
    cmd = ('adb', 'shell', 'ls', '/storage/self/primary/DCIM/Camera/*')
    # バイナリの為decodeして１つ１つのファイル名に分割しファイルがnumファイルあればOK
    fnames=subprocess.check_output(cmd).decode('utf-8').rstrip().split('\r\n')

    if len(fnames) != num:
        print('画像が' + str(num) + '枚ではありませんスマホのフォルダを確認してください')
        return 1
    else:
        # 撮影したnum枚の画像をPCへコピーする
        print('スマホの画像が' + str(num) + '枚です。PCにコピー処理を行います')
        for fname in fnames:
            cmd = ('adb', 'pull', fname, PICDIR)
            subprocess.check_output(cmd)
        
        # android内の撮影したnum枚の画像を削除する
        for fname in fnames:
            cmd = ('adb', 'shell','rm', fname)
            subprocess.check_output(cmd)
            time.sleep(1)
        
        # カメラアプリ停止用のコマンド
        print('カメラを停止します')
        cmd = ('adb', 'shell', 'am', 'force-stop', 'com.sec.android.app.camera')
        subprocess.call(cmd)
        
        # 画像名を取得しリターンする
        print('画像のファイル名出力します')
        files=os.listdir(PICDIR)

        return files

```

実際に関数を実行して画像を撮影

```python:実行
pic_shot(5)
```

```python:実行結果
カメラを起動します
撮影を5回実行します
画像のコピー処理を行います
スマホの画像が5枚です。PCにコピー処理を行います
カメラを停止します
画像のファイル名出力します

['20190816_234209.jpg',
 '20190816_234212.jpg',
 '20190816_234215.jpg',
 '20190816_234218.jpg',
 '20190816_234220.jpg']

```

指定した枚数分撮影され、PC側に画像がコピーされています


![screenshot.389.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225931/a5d97fb0-8be1-71ea-c670-0e4f8b825ec5.jpeg)


一旦今回はここまでで終わりとします。
初めてadbコマンドというものを触ったが結構癖のあるコマンドだった。。。

また何かの機会に触ることがあれば第2弾として記事にしたいと思います。