# Gondolaを使用した簡単なOCRアクション

オートメーションスクリプトを実装する時に、コントロールと直接相互作用できない場合があります。コントロールがキャンバスまたはOpenGlビューポートで画像としてレンダリングされ、テキストが含まれている場合、OCRアクションが役立つ場合があります。ただし、OCRは薬がありません。ほとんどのOCRアルゴリズムは、非常に明確なテキストセグメントがある場合に最適に機能します。これらのセグメントは可能な限り高解像度(DPI)である必要があり、入力画像の文字はセグメンテーション後に"pixelated"されて表示されません。つまり、フォント、言語、複雑な背景、トレーニングが不十分なデータでは、結果が悪くなる可能性があります。

このガイドでは、よく知られているオープンソースOCRフレームワークであるTesseractをGondolaプロジェクトに追加し、Car Rentalアプリで日本語のテキストをチェックするために使用します。

# 前提条件

# OCRプロジェクトの構築

# サンプルプロジェクトを作成して、必要な依存関係をインストールします

ABTプロジェクトの作成から始めます。プロジェクトが作成されたら、ターミナルで次のコマンドを実行します:

# 画像をキャプチャするときに、ユニークな名前を作成するためのライブラリ
npm i uuid @types/uuid

# キャプチャされた画像を前処理するためのライブラリ
npm i pngjs @types/pngjs

# TesseractのJS/TSバージョン
npm i tesseract.js @types/tesseract.js

# Tesseractワーカーの作成

Tesseractとインタラクティブのためのソースコードを保存するにはsrc/utilities/ocr.tsファイルを作成します。

import Tesseract = require("tesseract.js");
import pngjs = require("pngjs");
import fs = require("fs");
import path = require("path");

export interface TextCoordinate {
    x0: number,
    y0: number,
    x1: number,
    y1: number
}

export interface Color {
    red: number,
    green: number,
    blue: number
}

var worker: Tesseract.TesseractStatic;

/**
 * 実行中に、Tesseract.jsは、OCRタスクを処理するにはワーカープロセスを作成します。
 * 最適なパフォーマンスのために、再利用のためにワーカーを作成して実行し続けます。
 */
export async function StartOcrWorker() {
    worker = Tesseract.create({ } as any);
}

/**
 * 終了時にワーカーを終了します。
 */
export function StopOcrWorker() {
    (worker as any).terminate()
}

/**
 * GetTextCoordinatesは、主に4つのステップを実行します:
 *  1. OCRの成功率を高めるには、画像を前処理します
 *  2. OCRを実行するには、Tesseractを呼び出します
 *  3. 指定されたテキストのすべての出現を検索します
 *  4. テキストが見つかったすべての座標を戻します
 * @param imagePath
 * @param text
 * @param options
 */
export async function GetTextCoordinates(imagePath: string, text: string,
    options?: {
        lang?: string,
        dpi?: number,
        invert?: boolean,
        textColor?: Color
    }): Promise<TextCoordinate[]> {

    let result: TextCoordinate[] = [];
    process.execArgv.length = 0 //Workaround for debugging

    //Step 1: 画像を前処理します
    if (options && options.invert) {
        const invertedImage = path.join(path.dirname(imagePath),
            `inverted_${path.basename(imagePath)}`);
        await preProcessImage(imagePath, options, invertedImage);
        imagePath = invertedImage;
    }

    //Step 2: OCRを実行します
    const recognizedResult = await worker.recognize(imagePath, options);
    if (recognizedResult && recognizedResult.text.indexOf(text) >= 0) {
        //Step 3: 指定されたテキストを含む行を検索します
        recognizedResult.lines.forEach((line) => {
            const index = line.text.indexOf(text)
            if (index >= 0) {
                //Step 4: テキストの座標を作成するには、すべてのシンボルの座標を取得します
                let textPosition: TextCoordinate = {
                    x0: Number.MAX_VALUE,
                    y0: Number.MAX_VALUE,
                    x1: -1,
                    y1: -1
                }
                for (let i = index; i < index + text.length - 1; i++) {
                    textPosition = increaseBox(textPosition, line.symbols[i].bbox);
                }
                result.push(textPosition);
            }
        });
    }
    return result;
}

/**
 * preProcessImageは、最高のOCR結果を得るために画像品質を改善するのに役立ちます。
 * この関数では、2つのアルゴリズムを実装します:
 *      1. 簡単な色反転
 *      2. テキスト以外のすべての色を白に変更して、テキストの色を黒に変更します
 * @param imagePath
 * @param options
 * @param invertedImage
 */
async function preProcessImage(imagePath: string, options: { lang?: string | undefined; dpi?: number | undefined; invert?: boolean | undefined; textColor?: Color | undefined; }, invertedImage: string) {
    await new Promise((resolve, reject) => {
        fs.createReadStream(imagePath)
            .pipe(new pngjs.PNG({
                filterType: 4
            }))
            .on('parsed', function (this: pngjs.PNG) {
                for (var y = 0; y < this.height; y++) {
                    for (var x = 0; x < this.width; x++) {
                        var idx = (this.width * y + x) << 2;
                        if (options.textColor) {
                            //色と一致する場はい => 黒に変更します
                            if (this.data[idx] == options.textColor.red
                                && this.data[idx + 1] == options.textColor.green
                                && this.data[idx + 2] == options.textColor.blue) {
                                this.data[idx] = 0;
                                this.data[idx + 1] = 0;
                                this.data[idx + 2] = 0;
                            }
                            else { //色と一致しない場合 => 白に変更します
                                this.data[idx] = 255;
                                this.data[idx + 1] = 255;
                                this.data[idx + 2] = 255;
                            }
                        }
                        else {
                            // 色を反転します
                            this.data[idx] = 255 - this.data[idx];
                            this.data[idx + 1] = 255 - this.data[idx + 1];
                            this.data[idx + 2] = 255 - this.data[idx + 2];
                        }
                    }
                }
                this.pack().pipe(fs.createWriteStream(invertedImage))
                    .on("close", () => resolve());
            });
    });
}

/**
 * 指定されたバウンドボックスを組み合わせて、より大きなバウンドボックスを作成します
 * @param tobeIncrease
 * @param bbox
 */
function increaseBox(tobeIncrease: TextCoordinate, bbox: Tesseract.Bbox): TextCoordinate {
    return {
        x0: Math.min(tobeIncrease.x0, bbox.x0),
        y0: Math.min(tobeIncrease.y0, bbox.y0),
        x1: Math.max(tobeIncrease.x1, bbox.x1),
        y1: Math.max(tobeIncrease.y1, bbox.y1),
    }
}

# GondolaのサンプルOCRページ

OCRユーティリティを作成した後、Page Objectを作成します。src/pagesフォルダーを右クリックして、ocrPage.tsという名前のファイルを作成します。次のコードをコピーしてocrPage.tsに貼り付けて保存します。

import { TextCoordinate, GetTextCoordinates, StartOcrWorker, StopOcrWorker } from "../utilities/ocr";
import { action, gondola, page } from "@logigear/gondola";
import fs = require("fs");
import path = require("path");
import uuid = require("uuid");

@page
export class ocrPage {

    @action("tap ocr text", "Taps on given text")
    public async tapText(text: string, language = "eng", index = 1, touchDuration = 100, invert = true) {
        const coordinates = await this.findTheText(text, language, invert);
        if (!coordinates || coordinates.length === 0) {
            gondola.checkEqual("Not found", `'${text}' is found`,
                "OCR function cannot find the given text");
        }
        let toClick: TextCoordinate;
        if (coordinates.length >= index) {
            toClick = coordinates[index - 1];
        } else {
            gondola.checkEqual(`Not found ${index}`,
                `'${text}' appears at least ${index} time(s)`,
                "OCR function cannot find the given text");
            return;
        }
        await this.tapAtCoordinate(toClick, touchDuration);
    }

    @action("check ocr text", "Checks given text appears on screen")
    public async checkText(text: string, language = "eng", index = 1, invert = true) {
        const coordinates = await this.findTheText(text, language, invert);
        if (!coordinates || coordinates.length === 0) {
            gondola.checkEqual("Not found", `'${text}' is found`,
                "OCR function cannot find the given text");
        }
        if (index && index > 0) {
            if (coordinates.length < index) {
                gondola.checkEqual(`Not found ${index}`,
                    `'${text}' appears at least ${index} time(s)`,
                    "OCR function cannot find the given text");
            }
        }
    }

    @action("start ocr worker", "Start the OCR worker")
    public async startOcrWorker(){
        StartOcrWorker();
    }

    @action("stop ocr worker", "Stop the OCR worker")
    public async stopOcrWorker(){
        StopOcrWorker();
    }

    private async tapAtCoordinate(toClick: TextCoordinate, touchDuration: number) {
        await (await gondola.getCurrentBrowser()).touchPerform([
            {
                action: "press",
                options: {
                    x: (toClick.x0 + toClick.x1) / 2,
                    y: (toClick.y0 + toClick.y1) / 2,
                },
            }, {
                action: "wait",
                options: {
                    ms: touchDuration,
                },
            }, {
                action: "release",
                options: {},
            },
        ]);
    }

    private async findTheText(text: string, language: string, invert = false) {
        //キャプチャイメージを保存する一時フォルダー
        if (!fs.existsSync("./temp-images")){
            fs.mkdirSync("./temp-images");
        }
        let imagePath = path.join("./temp-images", `ocr_image_${uuid.v4()}.png`)
        await gondola.saveScreenshot(imagePath);
        const caps = await gondola.getCapabilities();
        return GetTextCoordinates(imagePath, text, {
            lang: language, 
            /**TODOはiOSデバイスのDPIを取得します*/
            dpi: (caps.platformName === "android" ? caps.deviceScreenDensity : 300),
            invert,
            textColor: {red: 255, green: 255, blue: 255}
        });
    }
}
export default new ocrPage();

# Gondola Test Designerを使用してOCRテストを作成します

Page Object ocrPageを作成したら、Gondola Test Designerでこれらの新しいアクションを使用できるようになります。

src/testsフォルダーを右クリックして、New Gondola testをクリックします。アクションセルでCTRL + spaceを押すと、ocrアクションが表示されるはずです。

TIP

OCRはいつも正しいとは限りません。この例では、文字ログインが含まれるログインボタンは口グインとして認識されます。サンプルコードを使用してテストスイートにする場合は機能も活動し続きます (コードコメントのTODOタグを参照してください)。

TIP

Tesseractは言語データをダウンロードする必要があるため、テストを初めて実行するときは時間がかかる場合があります。

コンパイルしてプロジェクトを実行し、結果を確認します。 エミュレーター/デバイスが実行/接続されていること、およびAppiumサーバーが実行されていることを確認してください。

# スクリプトをコンパイルします
npm run compile

# AndroidでOCRテストを実行します
npm run test:android -- --grep OCR

# 結果を見ますす
npm run show-report
最終更新: 2020/12/28 4:12:58