Lチカで始めるテスト自動化(5)WebカメラおよびOCRの組込み

##1. はじめに
Lチカで始めるテスト自動化シリーズ第五弾です。

[M5StackとPythonで受入テスト自動化の要素技術を試す](https://qiita.com/pbjpkas/items/97b6e596a4009a71f1ab)でインタラクティブシェルで行っていたWebカメラの画像キャプチャや画像の切り出し、OCRをテストランナーに組み込んで自動化します。

これまでの記事はこちらをご覧ください。

1. [Lチカで始めるテスト自動化](https://qiita.com/pbjpkas/items/fd4bd2689deda2d793aa)
2. [Lチカで始めるテスト自動化(2)テストスクリプトの保守性向上](https://qiita.com/pbjpkas/items/337bce392281ee5f0526)
3. [Lチカで始めるテスト自動化(3)オシロスコープの組込み](https://qiita.com/pbjpkas/items/b80b7e199a5aed4688a4)
4. [Lチカで始めるテスト自動化(4)テストスクリプトの保守性向上(2)](https://qiita.com/pbjpkas/items/a02c4128baf5a3f2a46c)

##2. テストランナーの改修
- 以下のコマンドおよび関数を作成します。

|コマンド|引数|機能
|--------+--------+--------
|open_cam|カメラデバイスの番号|OpenCVのVideoCaptureを実行する
|close_cam|なし|OpenCVのVideoCaptureをreleaseする
|capture_cam|ファイル名|カメラ画像をキャプチャしファイルに保存する
|crop_img|入力ファイル名<br>切り出し位置（縦）<br>切り出し位置（横）<br>出力ファイル名|指定されたエリアを画像ファイルから切り出してファイルに保存する
|open_ocr|なし|PyOCRを利用可能にする
|exec_ocr|OCRにかける画像ファイル名|画像ファイルからUS ASCIIの文字列をOCRする

- 冗長なコードを改修に併せて削除しました。

```python:test-runner.py
#!/usr/bin/python3

#
# This software includes the work that is distributed in the Apache License 2.0
#

from time import sleep
import serial
import codecs
import csv
import sys
import visa
import cv2
from PIL import Image
import pyocr
import pyocr.builders

UNINITIALIZED = 0xdeadbeef

def open_cam(camera_number):
    return cv2.VideoCapture(camera_number)

def close_cam(cam):
    if cam != UNINITIALIZED:
        cam.release()

def capture_cam(cam, filename):
    if cam != UNINITIALIZED:
        _, img = cam.read()
        cv2.imwrite(filename, img)
        return True
    else:
        print("CAM Not Ready.")
        return False

def crop_img(filename_in, v, h, filename_out):
    img = cv2.imread(filename_in, cv2.IMREAD_COLOR)
    v0 = int(v.split(':')[0])
    v1 = int(v.split(':')[1])
    h0 = int(h.split(':')[0])
    h1 = int(h.split(':')[1])
    img2 = img[v0:v1, h0:h1]
    cv2.imwrite(filename_out, img2)
    return True

def open_ocr():
    ocr = pyocr.get_available_tools()
    if len(ocr) != 0:
        ocr = ocr[0]
    else:
        ocr = UNINITIALIZED
        print("OCR Not Ready.")
    return ocr

def exec_ocr(ocr, filename):
    try:
        txt = ocr.image_to_string(
            Image.open(filename),
            lang = "eng",
            builder = pyocr.builders.TextBuilder()
        )
    except:
        print("OCR Not Ready.")
    else:
        return txt

def serial_write(h, string):
    if h != UNINITIALIZED:
        string = string + '\n'
        string = str.encode(string)
        h.write(string)
        return True
    else:
        print("UART Not Initialized.")
        return False

def open_dso():
    rm = visa.ResourceManager()
    resources = rm.list_resources()
    #print(resources)
    for resource in resources:
        #print(resource)
        try:
            dso = rm.open_resource(resource)
        except:
            print(resource, "Not Found.")
        else:
            print(resource, "Detected.")
            return dso

    #Throw an error to caller if none succeed.
    return dso

def main():
    is_passed = True
    val = str(UNINITIALIZED)
    cam = UNINITIALIZED
    ocr = UNINITIALIZED
    dso = UNINITIALIZED
    uart = UNINITIALIZED

    with codecs.open('script.csv', 'r', 'utf-8') as file:
        script = csv.reader(file, delimiter=',', lineterminator='\r\n', quotechar='"')

        with codecs.open('result.csv', 'w', 'utf-8') as file:
            result = csv.writer(file, delimiter=',', lineterminator='\r\n', quotechar='"')

            for cmd in script:
                print(cmd)

                if "#" in cmd[0]:
                    pass

                elif cmd[0] == "sleep":
                    sleep(float(cmd[1]))

                elif cmd[0] == "open_cam":
                    cam = open_cam(int(cmd[1]))

                elif cmd[0] == "close_cam":
                    close_cam(cam)
                    cam = UNINITIALIZED

                elif cmd[0] == "capture_cam":
                    ret = capture_cam(cam, cmd[1])
                    if ret == False:
                        is_passed = False

                elif cmd[0] == "crop_img":
                    crop_img(cmd[1], cmd[2], cmd[3], cmd[4])

                elif cmd[0] == "open_ocr":
                    ocr = open_ocr()
                    if ocr == UNINITIALIZED:
                        is_passed = False

                elif cmd[0] == "exec_ocr":
                    try:
                        val = exec_ocr(ocr, cmd[1])
                    except:
                        is_passed = False
                    else:
                        cmd.append(str(val))

                elif cmd[0] == "open_dso":
                    try:
                        dso = open_dso()
                    except:
                        is_passed = False

                elif cmd[0] == "dso":
                    try:
                        if "?" in cmd[1]:
                            val = dso.query(cmd[1]).rstrip().replace(",", "-")
                            cmd.append(val)
                        else:
                            dso.write(cmd[1])
                    except:
                        is_passed = False

                elif cmd[0] == "open_uart":
                    try:
                        uart = serial.Serial(cmd[1], 115200, timeout=1.0, dsrdtr=1)
                    except:
                        is_passed = False

                elif cmd[0] == "send":
                    ret = serial_write(uart, cmd[1])
                    if ret == False:
                        is_passed = False

                elif cmd[0] == "rcvd":
                    try:
                        val = uart.readline().strip().decode('utf-8')
                        cmd.append(val)
                    except:
                        is_passed = False

                elif cmd[0] == "eval_str_eq":
                    if str(val) != str(cmd[1]):
                        is_passed = False

                elif cmd[0] == "eval_int_eq":
                    if int(val) != int(cmd[1]):
                        is_passed = False

                elif cmd[0] == "eval_int_gt":
                    if int(val) < int(cmd[1]):
                        is_passed = False

                elif cmd[0] == "eval_int_lt":
                    if int(val) > int(cmd[1]):
                        is_passed = False

                elif cmd[0] == "eval_dbl_eq":
                    if float(val) != float(cmd[1]):
                        is_passed = False

                elif cmd[0] == "eval_dbl_gt":
                    if float(val) < float(cmd[1]):
                        is_passed = False

                elif cmd[0] == "eval_dbl_lt":
                    if float(val) > float(cmd[1]):
                        is_passed = False

                else:
                    cmd.append("#")

                if is_passed == True:
                    cmd.append("OK")
                    print(cmd)
                    result.writerow(cmd)
                else:
                    cmd.append("NG")
                    print(cmd)
                    result.writerow(cmd)
                    close_cam(cam)
                    print("FAIL")
                    sys.exit(1)

    if is_passed == True:
        close_cam(cam)
        print("PASS")
        sys.exit(0)

main()
```

##3. テストスクリプト
crop_imgコマンドの確認は[M5StackとPythonで受入テスト自動化の要素技術を試す](https://qiita.com/pbjpkas/items/97b6e596a4009a71f1ab)の[3.2 Webcam画像のキャプチャ](https://qiita.com/pbjpkas/items/97b6e596a4009a71f1ab#32-webcam画像のキャプチャ)で生成された123abc.bmpファイルを使用しています。

```sh:script.csv
open_cam,1
capture_cam,abc1234.jpg
close_cam
crop_img,123abc.bmp,234:274,240:400,123abc_crop.bmp
open_ocr
exec_ocr,123abc_crop.bmp
eval_str_eq,123-abc
```

##4. テスト実行結果

- 実行環境はWindows10(1909)+Anaconda3 2019.07 (Python 3.6.9 64-bit)です[^1]。
- Webカメラの画像を保存できました。
- crop_imgコマンドで切り出した画像をOCRにかけて、期待値の文字列と一致することを自動で確認できました。

```sh:result.csv
open_cam,1,OK
capture_cam,abc1234.jpg,OK
close_cam,OK
crop_img,123abc.bmp,234:274,240:400,123abc_crop.bmp,OK
open_ocr,OK
exec_ocr,123abc_crop.bmp,123-abc,OK
eval_str_eq,123-abc,OK
```

##5. おわりに
UARTのコマンド送受信、組込み機器のスイッチのON/OFF、ArduinoのA/Dコンバータを使った電圧測定、オシロスコープの組込み、Webカメラの組込み、画像の切り出し、OCRを使った文字列の抽出と比較をできるようになりました。組込み機器やIoTデバイスのシステムレベルのCI/CDの可能性がだいぶ上がったように思います。

[^1]: OpenCV、Tesseract-OCR、PyOCR、PILのインストールやTesseract-OCRの環境変数の設定は[M5StackとPythonで受入テスト自動化の要素技術を試す](https://qiita.com/pbjpkas/items/97b6e596a4009a71f1ab)をご覧ください。