M5StackとPythonで受入テスト自動化の要素技術を試す

##1. はじめに
[ArduinoとオシロスコープをPythonで制御して測定する](https://qiita.com/pbjpkas/items/bd483d4773468e26414c)の続編です。前回は性能テスト自動化の基礎となるオシロスコープの制御をPyVISAで行いましたが、今回は受入テスト自動化を想定しPySerialでIoTデバイスを制御するとともにデバイスの画面をWebcamで撮影してOpenCVとPyOCRで画像から文字列を抽出します。
![system.png](https://qiita-image-store.s3.amazonaws.com/0/221758/47a4461e-549b-9eff-27c5-0824652c4612.png)

OpenCVとPythonによる受入テスト自動化はJaSST'17 Tokyoの[「受け入れテストの自動化 ～ OpenCVの「眼」で捉え、Pythonの「脳」が思考し、Appiumの「指」で動かす」](http://www.jasst.jp/symposium/jasst17tokyo/details.html#C3)(講演資料：[受け入れテストの自動化](https://speakerdeck.com/cygames/shou-keru-retesutofalsezi-dong-hua-opencvfalse-yan-dezhuo-e-pythonfalse-noy-gasi-kao-si-appiumfalse-zhi-dedong-kasu))からヒントを得ています。なお、この講演資料は自動化だけでなく期待結果のデータの作り方やデータの構造などさまざまな工夫が紹介されていて参考になります。

##2. 構成要素
###2.1 ハードウェア
- M5Stack
  - [M5Stack](http://www.m5stack.com/)は5センチ角のスタッカブル(積み重ね可能)なIoTデバイスのプロトタイプモジュールです。
  - CPUに[ESP32](https://www.google.com/search?q=ESP32)を、ペリフェラルとしてWiFi&Bluetooth(技適取得済み)、2インチ(320x240)のTFT液晶、3個のボタン、スピーカー、[GROVE端子](https://www.google.com/search?q=GROVE端子)(I2C)、UARTやバッテリーなどを搭載し、C/C++やMicroPython、Scratchでプログラムを開発できます。
  - 9軸IMU(3軸加速度、3軸ジャイロ、3軸コンパスセンサ)や[NeoPixel](https://www.google.com/search?q=NeoPixel)(RGB LED)、マイクを備える上位機種やプロトタイピングを加速するさまざまなオプションが用意されています([AliExpress](https://ja.aliexpress.com/store/3226069)、[スイッチサイエンス(国内代理店)](https://www.switch-science.com/catalog/list/770/))。
  - M5StackはIoTデバイスのプロトタイプモジュールですが今回はこれをソフトウェアテストのツール開発のプロトタイプとして使用し、UARTからコマンドの引数で受け取った文字列を液晶画面に表示する機能をM5Stackに実装しました。詳細は[M5Stackでスマートウォッチ](https://qiita.com/pbjpkas/items/7098cab01dcdc03cc641)をご参照ください。
- [Logicool HD Webcam C270](https://www.logicool.co.jp/ja-jp/product/hd-webcam-c270)
  - 接写できるように[ピント調整](https://www.google.co.jp/search?hl=ja&as_q=logicool+c270+ピント調整)しています。改造すると保証が効かなくなりますので試す際は自己責任でお願いします。

###2.2 ソフトウェア
筆者はMinicondaでPythonの実行環境を用意しています。condaとpipは混ぜるな危険[^1]という話がありますのでcondaコマンドでインストールする方法を以下に示します。

- OpenCV
- Tesseract-OCR
- PyOCR
- PIL

####2.2.1 OpenCVのインストール
conda install -c conda-forge opencv
<details><summary>コマンド実行ログ</summary><div>

```
>conda install -c conda-forge opencv
Solving environment: done

## Package Plan ##

  environment location: C:\Users\hoge\Miniconda3

  added / updated specs:
    - opencv


The following packages will be downloaded:

    package                    |            build
    ---------------------------|-----------------
    win_inet_pton-1.0.1        |        py36_1002           5 KB  conda-forge
    urllib3-1.23               |        py36_1001         152 KB  conda-forge
    certifi-2018.10.15         |        py36_1000         138 KB  conda-forge
    icu-58.2                   |           vc14_0        21.8 MB  conda-forge
    qt-5.6.2                   |           vc14_1        55.6 MB  conda-forge
    pycosat-0.6.3              |py36hfa6e2cd_1001          97 KB  conda-forge
    requests-2.20.1            |        py36_1000          84 KB  conda-forge
    ruamel_yaml-0.15.71        |py36hfa6e2cd_1000         271 KB  conda-forge
    wincertstore-0.2           |        py36_1002          13 KB  conda-forge
    conda-4.5.11               |        py36_1000         655 KB  conda-forge
    blas-1.0                   |              mkl           6 KB
    mkl-2019.0                 |              118       178.1 MB
    wheel-0.32.2               |           py36_0          51 KB  conda-forge
    mkl_random-1.0.2           |           py36_0         267 KB  conda-forge
    chardet-3.0.4              |        py36_1003         209 KB  conda-forge
    intel-openmp-2019.0        |              118         1.7 MB
    python-3.6.6               |       he025d50_0        21.4 MB  conda-forge
    jpeg-9c                    |    hfa6e2cd_1001         314 KB  conda-forge
    libpng-1.6.34              |           vc14_0         547 KB  conda-forge
    numpy-base-1.15.4          |   py36h8128ebf_0         3.9 MB
    libwebp-0.5.2              |           vc14_7         1.1 MB  conda-forge
    zlib-1.2.11                |           vc14_0         119 KB  conda-forge
    pysocks-1.6.8              |        py36_1002          22 KB  conda-forge
    six-1.11.0                 |        py36_1001          20 KB  conda-forge
    cryptography-2.3.1         |py36h74b6da3_1000         506 KB  conda-forge
    cffi-1.11.5                |py36hfa6e2cd_1001         211 KB  conda-forge
    pyopenssl-18.0.0           |        py36_1000          80 KB  conda-forge
    icc_rt-2017.0.4            |       h97af966_0         8.0 MB
    setuptools-40.6.2          |           py36_0         585 KB  conda-forge
    pywin32-224                |py36hfa6e2cd_1000         6.8 MB  conda-forge
    pyvisa-1.9.1               |        py36_1000         232 KB  conda-forge
    mkl_fft-1.0.6              |           py36_0         121 KB  conda-forge
    pycparser-2.19             |             py_0          87 KB  conda-forge
    libtiff-4.0.9              |           vc14_0         616 KB  conda-forge
    asn1crypto-0.24.0          |        py36_1003         154 KB  conda-forge
    numpy-1.15.4               |   py36ha559c80_0          36 KB
    idna-2.7                   |        py36_1002         131 KB  conda-forge
    menuinst-1.4.14            |        py36_1000          93 KB  conda-forge
    pip-18.1                   |        py36_1000         1.8 MB  conda-forge
    opencv-3.4.3               | py36h597e314_201        54.0 MB  conda-forge
    ------------------------------------------------------------
                                           Total:       359.8 MB

The following NEW packages will be INSTALLED:

    blas:          1.0-mkl
    icc_rt:        2017.0.4-h97af966_0
    icu:           58.2-vc14_0            conda-forge [vc14]
    intel-openmp:  2019.0-118
    jpeg:          9c-hfa6e2cd_1001       conda-forge
    libpng:        1.6.34-vc14_0          conda-forge [vc14]
    libtiff:       4.0.9-vc14_0           conda-forge [vc14]
    libwebp:       0.5.2-vc14_7           conda-forge [vc14]
    mkl:           2019.0-118
    mkl_fft:       1.0.6-py36_0           conda-forge
    mkl_random:    1.0.2-py36_0           conda-forge
    numpy:         1.15.4-py36ha559c80_0
    numpy-base:    1.15.4-py36h8128ebf_0
    opencv:        3.4.3-py36h597e314_201 conda-forge
    qt:            5.6.2-vc14_1           conda-forge [vc14]
    zlib:          1.2.11-vc14_0          conda-forge [vc14]

The following packages will be UPDATED:

    asn1crypto:    0.24.0-py37_0                      --> 0.24.0-py36_1003          conda-forge
    certifi:       2018.10.15-py37_1000   conda-forge --> 2018.10.15-py36_1000      conda-forge
    cffi:          1.11.5-py37h74b6da3_1              --> 1.11.5-py36hfa6e2cd_1001  conda-forge
    chardet:       3.0.4-py37_1                       --> 3.0.4-py36_1003           conda-forge
    conda:         4.5.11-py37_1000       conda-forge --> 4.5.11-py36_1000          conda-forge
    cryptography:  2.3.1-py37h74b6da3_0               --> 2.3.1-py36h74b6da3_1000   conda-forge
    idna:          2.7-py37_0                         --> 2.7-py36_1002             conda-forge
    menuinst:      1.4.14-py37hfa6e2cd_0              --> 1.4.14-py36_1000          conda-forge
    pip:           10.0.1-py37_0                      --> 18.1-py36_1000            conda-forge
    pycosat:       0.6.3-py37hfa6e2cd_0               --> 0.6.3-py36hfa6e2cd_1001   conda-forge
    pycparser:     2.18-py37_1                        --> 2.19-py_0                 conda-forge
    pyopenssl:     18.0.0-py37_0                      --> 18.0.0-py36_1000          conda-forge
    pyserial:      3.4-py_2               conda-forge --> 3.4-py_2                  conda-forge
    pysocks:       1.6.8-py37_0                       --> 1.6.8-py36_1002           conda-forge
    pyvisa:        1.9.1-py37_1000        conda-forge --> 1.9.1-py36_1000           conda-forge
    pywin32:       223-py37hfa6e2cd_1                 --> 224-py36hfa6e2cd_1000     conda-forge
    requests:      2.19.1-py37_0                      --> 2.20.1-py36_1000          conda-forge
    ruamel_yaml:   0.15.46-py37hfa6e2cd_0             --> 0.15.71-py36hfa6e2cd_1000 conda-forge
    setuptools:    40.2.0-py37_0                      --> 40.6.2-py36_0             conda-forge
    six:           1.11.0-py37_1                      --> 1.11.0-py36_1001          conda-forge
    urllib3:       1.23-py37_0                        --> 1.23-py36_1001            conda-forge
    wheel:         0.31.1-py37_0                      --> 0.32.2-py36_0             conda-forge
    win_inet_pton: 1.0.1-py37_1                       --> 1.0.1-py36_1002           conda-forge
    wincertstore:  0.2-py37_0                         --> 0.2-py36_1002             conda-forge

The following packages will be DOWNGRADED:

    python:        3.7.0-hea74fb7_0                   --> 3.6.6-he025d50_0          conda-forge

Proceed ([y]/n)? y


Downloading and Extracting Packages
win_inet_pton-1.0.1  | 5 KB      | ###################################################################################### | 100%
urllib3-1.23         | 152 KB    | ###################################################################################### | 100%
certifi-2018.10.15   | 138 KB    | ###################################################################################### | 100%
icu-58.2             | 21.8 MB   | ###################################################################################### | 100%
qt-5.6.2             | 55.6 MB   | ###################################################################################### | 100%
pycosat-0.6.3        | 97 KB     | ###################################################################################### | 100%
requests-2.20.1      | 84 KB     | ###################################################################################### | 100%
ruamel_yaml-0.15.71  | 271 KB    | ###################################################################################### | 100%
wincertstore-0.2     | 13 KB     | ###################################################################################### | 100%
conda-4.5.11         | 655 KB    | ###################################################################################### | 100%
blas-1.0             | 6 KB      | ###################################################################################### | 100%
mkl-2019.0           | 178.1 MB  | ###################################################################################### | 100%
wheel-0.32.2         | 51 KB     | ###################################################################################### | 100%
mkl_random-1.0.2     | 267 KB    | ###################################################################################### | 100%
chardet-3.0.4        | 209 KB    | ###################################################################################### | 100%
intel-openmp-2019.0  | 1.7 MB    | ###################################################################################### | 100%
python-3.6.6         | 21.4 MB   | ###################################################################################### | 100%
jpeg-9c              | 314 KB    | ###################################################################################### | 100%
libpng-1.6.34        | 547 KB    | ###################################################################################### | 100%
numpy-base-1.15.4    | 3.9 MB    | ###################################################################################### | 100%
libwebp-0.5.2        | 1.1 MB    | ###################################################################################### | 100%
zlib-1.2.11          | 119 KB    | ###################################################################################### | 100%
pysocks-1.6.8        | 22 KB     | ###################################################################################### | 100%
six-1.11.0           | 20 KB     | ###################################################################################### | 100%
cryptography-2.3.1   | 506 KB    | ###################################################################################### | 100%
cffi-1.11.5          | 211 KB    | ###################################################################################### | 100%
pyopenssl-18.0.0     | 80 KB     | ###################################################################################### | 100%
icc_rt-2017.0.4      | 8.0 MB    | ###################################################################################### | 100%
setuptools-40.6.2    | 585 KB    | ###################################################################################### | 100%
pywin32-224          | 6.8 MB    | ###################################################################################### | 100%
pyvisa-1.9.1         | 232 KB    | ###################################################################################### | 100%
mkl_fft-1.0.6        | 121 KB    | ###################################################################################### | 100%
pycparser-2.19       | 87 KB     | ###################################################################################### | 100%
libtiff-4.0.9        | 616 KB    | ###################################################################################### | 100%
asn1crypto-0.24.0    | 154 KB    | ###################################################################################### | 100%
numpy-1.15.4         | 36 KB     | ###################################################################################### | 100%
idna-2.7             | 131 KB    | ###################################################################################### | 100%
menuinst-1.4.14      | 93 KB     | ###################################################################################### | 100%
pip-18.1             | 1.8 MB    | ###################################################################################### | 100%
opencv-3.4.3         | 54.0 MB   | ###################################################################################### | 100%
Preparing transaction: done
Verifying transaction: done
Executing transaction: done

```

</div></detail>

####2.2.2 Tesseract-OCRのインストール
#####2.2.2.1 Tesseract-OCRのダウンロードとインストール
[https://github.com/UB-Mannheim/tesseract/wiki](https://github.com/UB-Mannheim/tesseract/wiki)からtesseract-ocr-setup-3.05.02-20180621.exeをダウンロードし自分が使いそうな言語にチェックを入れてインストールします。

参考：[(2017年12月) PythonとOpenCVをこれからやってみる - 3 - 文字認識(1)](https://qiita.com/rrryutaro/items/0c0a7382560e1f67123b)

補足：
最新版のtesseract-ocr-w64-setup-<font color="red">v4.0.0.20181030</font>.exeはpythonから呼び出した際に以下のようなエラーダイアログが表示されたためtesseract-ocr-setup-<font color="red">3.05.02-20180621</font>.exeを使用しています。
![NotFoundEntryPoint.png](https://qiita-image-store.s3.amazonaws.com/0/221758/7d0d3834-2c47-676b-694e-5cb7d3937582.png)

#####2.2.2.2 Tesseract-OCRの環境変数の設定
PATHとTESSDATA_PREFIXを設定します。

```
>set path=%PATH%;C:\Program Files (x86)\Tesseract-OCR
>set TESSDATA_PREFIX=C:\Program Files (x86)\Tesseract-OCR
```

バージョンとインストールした言語を確認します。

```
>tesseract -v
tesseract 3.05.02
 leptonica-1.75.3
  libgif 5.1.4 : libjpeg 8d (libjpeg-turbo 1.5.3) : libpng 1.6.34 : libtiff 4.0.9 : zlib 1.2.11 : libwebp 0.6.1 : libopenjp2 2.2.0

>tesseract --list-langs
List of available languages (5):
chi_sim
chi_tra
eng
jpn
osd
```

参考：[pipを使わずwindows10にtesseractとPyOCRをインストールする方法](https://haitenaipants.hatenablog.com/entry/2018/06/02/193554)

####2.2.3 PyOCRのインストール
[https://anaconda.org/brianjmcguirk/pyocr/files](https://anaconda.org/brianjmcguirk/pyocr/files)からpyocr-0.5-py36h6a457c8_0.tar.bz2をダウンロードしcondaコマンドでインストールします。

conda install c:\Users\hoge\Downloads\pyocr-0.5-py36h6a457c8_0.tar.bz2
<details><summary>コマンド実行ログ</summary><div>

```
>conda install c:\Users\hoge\Downloads\pyocr-0.5-py36h6a457c8_0.tar.bz2

Downloading and Extracting Packages
######################################################################################################################### | 100%
Preparing transaction: done
Verifying transaction: done
Executing transaction: done

```

</div></detail>

続いて、tesseract.pyとbuilders.pyの"-psm"を"--psm"に修正します。

参考：[pipを使わずwindows10にtesseractとPyOCRをインストールする方法](https://haitenaipants.hatenablog.com/entry/2018/06/02/193554)
参考：[pyocrでtesseractを動かせなかった問題](https://madomadox.hatenablog.com/entry/2018/06/24/165317)

####2.2.4 PILのインストール
画像の読み込みに使用するPILライブラリをインストールします。

conda install -c conda-forge pillow
<details><summary>コマンド実行ログ</summary><div>

```
>conda install -c conda-forge pillow
Solving environment: done

## Package Plan ##

  environment location: C:\Users\hoge\Miniconda3

  added / updated specs:
    - pillow


The following packages will be downloaded:

    package                    |            build
    ---------------------------|-----------------
    tk-8.6.8                   |           vc14_0         3.8 MB  conda-forge
    olefile-0.46               |             py_0          31 KB  conda-forge
    freetype-2.9.1             |    he8b6a0d_1004         470 KB  conda-forge
    pillow-5.3.0               |py36h9a613e6_1000         778 KB  conda-forge
    ------------------------------------------------------------
                                           Total:         5.0 MB

The following NEW packages will be INSTALLED:

    freetype: 2.9.1-he8b6a0d_1004     conda-forge
    olefile:  0.46-py_0               conda-forge
    pillow:   5.3.0-py36h9a613e6_1000 conda-forge
    tk:       8.6.8-vc14_0            conda-forge [vc14]

Proceed ([y]/n)? y


Downloading and Extracting Packages
tk-8.6.8             | 3.8 MB    | ###################################################################################### | 100%
olefile-0.46         | 31 KB     | ###################################################################################### | 100%
freetype-2.9.1       | 470 KB    | ###################################################################################### | 100%
pillow-5.3.0         | 778 KB    | ###################################################################################### | 100%
Preparing transaction: done
Verifying transaction: done
Executing transaction: done

```

</div></detail>

##3. 自動化の要素技術を試す
###3.1 M5Stackの画面への文字列描画
M5Stackへ自作の以下のコマンドをPySerialで送信します。

1. serialをimportし通信設定を行う
- "x" を送信しコマンドモードへ遷移する(xは任意の文字)
- m5lcd clear (LCDを全面黒で描画する)
- m5lcd str 123-abc (123-abcという文字列を画面の中心に描画する)

```python
>python
Python 3.6.6 | packaged by conda-forge | (default, Jul 26 2018, 11:48:23) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>>
>>> import serial
>>> ser = serial.Serial("COM7", 115200)
>>>
>>>
>>> data = "x"
>>> data = data + '\r\n'
>>> ser.write(bytes(data, 'UTF-8'))
3
>>> data = "m5lcd clear"
>>> data = data + '\r\n'
>>> ser.write(bytes(data, 'UTF-8'))
13
>>> data = "m5lcd str 123-abc"
>>> data = data + '\r\n'
>>> ser.write(bytes(data, 'UTF-8'))
19
>>>
```

###3.2 Webcam画像のキャプチャ
Webcamの画像をOpenCVを使用して表示するとともに任意のファイル名(例：123abc.bmp)で画像を保存します。

キャプチャした画像を以下に示します。カメラがアクティブになると点灯する緑のLEDは写り込まないようテープで目隠しすると良いと思いました。
![123abc.png](https://qiita-image-store.s3.amazonaws.com/0/221758/bc8d10f7-6775-6d71-b343-444b0616f23b.png)

参考：[ゼロからはじめるPython(35) OpenCVで監視カメラを自作してみよう | マイナビニュース](https://news.mynavi.jp/article/zeropython-35/) camera.py

###3.3 文字列エリアの切り出し

```python
import cv2

filename = "123abc.bmp"

img = cv2.imread(filename, cv2.IMREAD_COLOR)
img2 = img[236:274, 240:400]

cv2.imshow('window title', img2)
cv2.waitKey(0)
```

任意のファイル名(例：123abc_core.png)で画像を保存します。
![123abc_core.png](https://qiita-image-store.s3.amazonaws.com/0/221758/c93081f3-d4ef-125f-ebbb-9fd037719ef5.png)

参考：[(2017年12月) PythonとOpenCVをこれからやってみる - 1 - はじめの一歩](https://qiita.com/rrryutaro/items/4c40873d6b06096cfe93)

###3.4 OCR

```python
from PIL import Image
import sys

import pyocr
import pyocr.builders

tools = pyocr.get_available_tools()
if len(tools) == 0:
    print("No OCR tool found")
    sys.exit(1)

tool = tools[0]

txt = tool.image_to_string(
    Image.open('123abc_core.png'),
    lang='eng',
    builder=pyocr.builders.TextBuilder()
)
print(txt)
```

スクリプトの実行結果を以下に示します。画面に表示した文字列を抽出できました。

```
>python ocr.py
123-abc
```

なお、「2.2.2.2 Tesseract-OCRの環境変数の設定」がされていないと

```
No OCR tool found
```

というメッセージが返ってきます。

参考：[pyocr README.markdown](https://gitlab.gnome.org/World/OpenPaperwork/pyocr/blob/master/README.markdown)
参考：[Pythonで画像からテキストを抽出する](https://qiita.com/m-sakano/items/85f7357f611d42311da4)

##4. おわりに
- Pythonを自動化システムのハブとすることでテスト対象デバイスの制御、測定器の制御、カメラ画像の取得や文字列の抽出を統合的に扱うことができました。WebやスマホアプリであればSeleniumやAppiumといった自動化のインフラが整っていますが組込みやIoTデバイスも自動化の第一歩の敷居は思ったほど高くないかもと思いました。
- IoTデバイスのプログラムをMicroPythonで作れたりテストベンチをPythonで書けるのはライブラリが充実していることもあってすごいなあ便利だなあと思いました。また、アセンブラやCを知らなくてもPythonを足掛かりにして組込みに参入できる(かもしれない)と考えると、Pythonでプログラムを書けるとできることが広がると思いました。
- 自動化プラットホームとしてExcel[^2][^3]とPythonを触ってみて適材適所と思いました。測定器でN回測定して最大値や最小値、平均値、中央値などを計算してグラフにしたりGo/No-GO判定するならExcelが便利だし受入テストであればPythonが便利と思いました。
- 最後に、筆者はPythonのプログラムはほとんど書いておらず参考にさせていただいた記事のプログラムをコピペしてちょっと直した程度です。記事を公開されている皆様に感謝いたします。

[^1]: [condaとpip：混ぜるな危険](http://onoz000.hatenablog.com/entry/2018/02/11/142347)
[^2]: [ArduinoとオシロスコープをExcelで制御して測定する](https://qiita.com/pbjpkas/items/7da7ff99e7226fa84bcb)
[^3]: [ArduinoとオシロスコープをExcelで制御して測定する(2) シャッタースピードの自動測定](https://qiita.com/pbjpkas/items/615a6eca0894c784e87a)