Tesseract OCRで文言描画の多言語対応テストを自動化する

Calendar for ソフトウェアテスト | Advent Calendar 2021 - Qiita」の記事です。

多言語をサポートするプロダクトの開発では、各言語ごとに表示文言が正しく描画されているかテストしたい場合があります。
今回は、その文言描画の多言語対応テストを、Tesseract OCRで自動化するアプローチについて解説します。

環境

テスト対象

今回はTwitterのUIのうち、英語、日本語、ドイツ語を対象にします。
次のような画像をキャプチャして、2列目の領域にそれぞれの言語の文言が適切に描画されているか、ビジュアルテストのアプローチで確認します。

eng.png
f:id:goyoki:20211215030022p:plain

jpn.png
f:id:goyoki:20211215030032p:plain

deu.png
f:id:goyoki:20211215030044p:plain

テスト

文言の内容の正確性(例えば翻訳の正確性)のテストは、実装中の文言データをチェックする方法が妥当な場合が多いです。
今回はそれ以外の、文言の描画の正確性(文字切れがないか、変な改行などないか、そもそも描画されているか)を自動でテストします。

1. 対象画面をキャプチャする

プロダクトの種類に応じた自動操作アプローチで、対象の画面を片っ端から静止画保存します。今回のように、WebサービスのUIを対象にして、pythonベースでテストを組む場合は、seleniumなどが妥当な実現手段になると思います。
今回は、その手段で、前述した英語・日本語・ドイツ語の設定画面キャプチャ画像(eng.png、jpn.png、deu.png)を、テスト対象画面として静止画保存します。

2. キャプチャ上の文言をOCRで抽出し、テキストベースで文言が正しいか確認する

次に、キャプチャ画像から文言を抽出して、テキストベースで文言の正確性を確認します。

テストオラクルとしては、コードが参照する文言データを利用できる場合が多いと思います。
サンプルレベルのかなり簡略化した例ですが、今回は、次ようなCSV形式で各国文言を列挙した文言ファイルがあるとします。テストではこれを読み込んで期待値に展開します。

stringdata.csv

"Your account","アカウント","Dein Account"
"Security and account access","セキュリティとアカウントアクセス","Sicherheit und Account-Zugriff"
"Privacy and safety","プライバシーと安全","Datenschutz und Sicherheit"
"Notifications","通知","Mitteilungen"
"Accessibility, display, and languages","アクセシビリティ、表示、言語","Barrierefreiheit, Anzeige und Sprachen"
"Additional resources","その他のリソース","Zusätzliche Ressourcen"

次にテストコードですが、一例として次のように実装できます。

test_display_all_words.py

import re
import csv
from PIL import Image
import pyocr
import pyocr.builders

def check_all_words(wordsfile, language, csv_column):
    '''指定された言語の表示文言が表示されているかチェック
    '''
    tool = pyocr.get_available_tools()[0]

    txt = tool.image_to_string(
        Image.open(language+'.png').crop((560, 100, 1200, 700)),#対象領域トリミング
        lang=language,
        builder=pyocr.builders.TextBuilder(tesseract_layout=6)
    )
    #言語特有の文字列処理
    if language == 'jpn':
        result = re.sub(r'([あ-んア-ン一-龥ー、])\s+((?=[あ-んア-ン一-龥ー、]))',r'\1\2', txt)
    else:
        result = txt

    with open(wordsfile) as f:
        reader = csv.reader(f)
        string_list_set = [row for row in reader]

    for string_list in string_list_set:
        if not string_list[csv_column] in result:
            print(f'failed: [{string_list[csv_column]}] is not found') #テスト失敗時の失敗箇所の提示のため-sオプション推奨
            return False
    return True

def test_all_words():
    '''各言語ごとの表示文言のチェック
    '''
    lang_list = [['eng', 0], ['jpn', 1], ['deu', 2]] #[lang_name, column_index of csv]

    for lang in lang_list:
        assert check_all_words('stringdata.csv', lang[0], lang[1]), lang[0]

テストコードの内容としては、対象領域に描画されている文言をPyOCRで読み取って、前述の文言ファイルの文言がその中で描画されているかチェックしています。

なおTwitter UIは文言描画がシンプルで明瞭なため、文言中の不正なスペース混入を除去する処理が必要な以外は、Tesseract OCRが提供するtessdata_bestの学習データをそのまま利用して十分な精度を確保できました。

テスト実行結果

テストを正常なキャプチャ画像で実行すると、テスト成功の結果が得られます。
次にキャプチャ画像を加工して、ドイツ語文言「Barrierefreiheit, Anzeige und Sprachen」の右端に文字切れを発生させてテストを実行すると、次のようにテスト失敗報告と、失敗原因となった文言を表示します。

pytest test_display_all_words.py -s
===================================================== test session starts =====================================================
(中略)                                                              

test_display_all_words.py failed: [Barrierefreiheit, Anzeige und Sprachen] is not found
F

========================================================== FAILURES ===========================================================
_______________________________________________________ test_all_words ________________________________________________________

    def test_all_words():
        '''各言語ごとの表示文言のチェック
        '''
        lang_list = [['eng', 0], ['jpn', 1], ['deu', 2]] #[lang_name, column_index of csv]
    
        for lang in lang_list:
>           assert check_all_words('stringdata.csv', lang[0], lang[1]), lang[0]
E           AssertionError: deu
E           assert False
E            +  where False = check_all_words('stringdata.csv', 'deu', 2)

test_display_all_words.py:39: AssertionError
=================================================== short test summary info ===================================================
FAILED test_display_all_words.py::test_all_words - AssertionError: deu
====================================================== 1 failed in 3.98s ======================================================

このアプローチの課題と対応方法

OCRを使った文言描画のテストの自動化には、利点・欠点それぞれあります。
結論を先に言うと、利点を活かし、欠点を補完するテストアプローチの工夫が求められます。

テストの偽陽性偽陰性の課題

まず欠点の1つ目ですが、テスト結果判定に機械学習を使用することから、テストの偽陽性偽陰性の問題に遭遇します。

このうち偽陽性については、テストが失敗したら本当に失敗なのか追加確認する運用で、安全方面に倒せます。

しかし偽陰性の方は問題です。若干の描画欠けが発生しても、機械学習エンジンが頭良く補完してテストを成功させてしまう場合があります。そのため、次のようなテストのアプローチの工夫が求められます。

  • 全文言の包括的なテストを、今回のOCRベースのアプローチで実装する
  • 継続的テストとしてのリグレッションテストにも、今回のOCRベースのアプローチで実装する
  • 一方、リスクレベルの高い文言描画については、適時のタイミングで、人による目視確認や、あるいは画像マッチングベースのアプローチでテストを実現する

描画座標依存によるFragile Testの課題

2つめの欠点ですが、次の要因から、今回のテストアプローチは対象画面の構成(レイアウトや描画位置座標)に依存します。

  • キャプチャ画像をそのままTesseract OCRに丸投げしても、複雑なレイアウト構成や、アイコン・境界などの意匠、透明化・影付きなどの効果といった、様々な要因で上手く描画文言を抽出できません。これに対策するためには、今回提示したテストコードでも実行していますが、対象領域をトリミングして、それぞれの領域に適した前処理や解析結果補正処理を実行する必要があります。すなわち、テストがレイアウトや描画位置座標に依存せざるを得ません。
  • キャプチャ画像の取得のため画面遷移の自動操作が必要です。全てを全自動にする場合、画面遷移操作をテストコード上に実装する必要があります。

上記の理由から、今回のテストアプローチでは、画面仕様や画面遷移仕様が変更される度にテストが壊れる場合があるという、Fragile Test問題に直面しがちです。

この対策として、例えば次のような工夫が求められます。

  • ソースコードから画面仕様や画面遷移仕様を解析してテスト操作を生成する処理を自動化する
  • テスト対象の限定。仕様が安定している画面の特定領域に限定してこのテストを適用する