🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

Azure Cognitive Services + WPFで画面キャプチャしてOCRと翻訳するツールを作った話

画像中の英語の長文を読むのがチョットつらい!英語ならまだしも中国語とか無理!

そんな思いから、画面中の任意の場所をキャプチャして、キャプチャ範囲内の文字列をOCRして、OCR結果を機械翻訳するツールを作りました。

OCRAzure Cognitive ServicesComputer VisionOCR API
翻訳はAzure Cognitive ServicesTranslator Text APIを使用します。

ガワはテキトーでいいので、.NET CoreのWPFで作りました。

こんな感じに動きます。

f:id:piyo_esq:20200125171902g:plain
ツールのデモ

成果物

GitHubに上げてます。

github.com

動かす際はImageOCR.App\app.configに下記4つの値を設定する必要があります。

なお、OCREndPointには?language=unk&amp;detectOrientation=trueあたりのQueryを。 TranslationのEndPointには?api-version=3.0&amp;to=jaというQueryを付与したほうがよいかも。

準備

OCRとTranslator TextのAPIが使用できるようCognitive Serviceをプロビジョニングします。

Azure CLIでやる方法

TextTranslationはLocationをglobalにしている点とSKU F0がアカウント毎に1個しか作れない点に注意です。
CognitiveServiceのSKUがS0なのでお金がかかる点に注意です。

$ResourceGroup="<任意のリソース名>"
$Location="<任意のロケーション名(例: japaneast)>"
az group create -n $ResourceGroup-l $Location
az cognitiveservices account create -g $ResourceGroup -l $Location -n OCR --sku S0 --kind CognitiveServices
az cognitiveservices account create -g $ResourceGroup -l global -n Translator --sku F0 --kind TextTranslation

下記コマンドでそれぞれのEndPointとAPI Keyを取得します。
WPFの設定で使用するのでメモっておきましょう。

echo "OCR API Key : $(az cognitiveservices account keys list -g $ResourceGroup -n OCR --query key1)"
echo "Translator API Key : $(az cognitiveservices account keys list -g $ResourceGroup -n Translator --query key1)"
echo (az cognitiveservices account list -g $ResourceGroup --query  '[].{Endpoint:endpoint, Name:name}')

--queryに関する内容はこちらのMicrosoft Docsを参考にしました。

Azure Portalでやる方法

Microsoft Docs的にはこちら?

docs.microsoft.com

以上で準備完了です。

OCRを試してみる

下記Microsoft Docsを参考にOCRを試してみましょう。

参考:クイックスタート
参考:サンプル

今回はMicrosoft.Azure.CognitiveServices.Vision.ComputerVisionを使用します。

OCRはこんな感じに使います。
C# 8.0を想定

var Credentials = new ApiKeyServiceClientCredentials("<API Key>");
var Endpoint = "<EndPoint>";
var imageFilePath = @"<Image FilePath>";
using var client = new ComputerVisionClient(Credentials) { Endpoint = Endpoint };
using var imageFileStream = File.OpenRead(imageFilePath);
OcrResult ocrResult = await client.RecognizePrintedTextInStreamAsync(true, imageFileStream);

下記画像をOCRすると…

f:id:piyo_esq:20200125204107p:plain
OCR対象画像

こんな感じのOcrResultが返ってきます。
※LINQPadでDumpしています

f:id:piyo_esq:20200125204124p:plain
OCR結果のDump

あとはlines毎にtextを結合していけば、元の文書になりますね!

Translator Text

続いて以下のMicrosoft Docsを参考に機械翻訳を試してみます。

参考:クイックスタート

Translator TextはLibraryが用意されていないっぽいので、普通にRequestを投げて翻訳してもらいましょう。

async Task Main()
{
    var endpoint = "<EndPoint>";
    var route = "<Route>"; // 例:translate?api-version=3.0&to=de&to=it&to=ja&to=th
    var subscriptionKey = "<API Key>";

    object[] body = new object[] { new { Text = "Hello World." } };
    var requestBody = JsonConvert.SerializeObject(body);

    using var client = new HttpClient();
    using var request = new HttpRequestMessage();
    request.Method = HttpMethod.Post;
    request.RequestUri = new Uri(endpoint + route);
    request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
    request.Headers.Add("Ocp-Apim-Subscription-Key", subscriptionKey);

    HttpResponseMessage response = await client.SendAsync(request);
    string result = await response.Content.ReadAsStringAsync();
    TranslationResult[] deserializedOutput = JsonConvert.DeserializeObject<TranslationResult[]>(result);
    foreach (TranslationResult o in deserializedOutput)
    {
        foreach (Translation t in o.Translations)
        {
            Console.WriteLine("Translated to {0}: {1}", t.To, t.Text);
        }
    }
}

TranslationResultの定義は以下の通り

public class TranslationResult
{
    public DetectedLanguage DetectedLanguage { get; set; }
    public TextResult SourceText { get; set; }
    public Translation[] Translations { get; set; }
}

public class DetectedLanguage
{
    public string Language { get; set; }
    public float Score { get; set; }
}

public class TextResult
{
    public string Text { get; set; }
    public string Script { get; set; }
}

public class Translation
{
    public string Text { get; set; }
    public TextResult Transliteration { get; set; }
    public string To { get; set; }
    public Alignment Alignment { get; set; }
    public SentenceLength SentLen { get; set; }
}

public class Alignment
{
    public string Proj { get; set; }
}

public class SentenceLength
{
    public int[] SrcSentLen { get; set; }
    public int[] TransSentLen { get; set; }
}

実行結果

f:id:piyo_esq:20200125204201p:plain
Hello World翻訳結果

LINQPadでDumpするとこんな感じです。
※EndPointのQueryミスって日本語が2回表示されています

f:id:piyo_esq:20200125210844p:plain
Translation結果Dump

WPF

OCRと翻訳部分が動いたので、あとはWPFでテキトーにガワを作ります。

先日作ったProject Templateを使用してプロジェクトを作成します。
この程度のアプリなのでCodeBehindにベタ書きでも良いとは思いますが、人はトンカチを持つと全てが釘に見えてしまうものです。

注意点というか、ポイントをちょこっと書いておきます。

指定範囲のみのキャプチャ

指定した範囲のみをキャプチャしたいので、下記Blogのコードを使用させて頂きました。

kaorun.hatenablog.com

上記コードの作者さんの言う通り、デュアルディスプレイにある領域はキャプチャできなかったり色々と制限はありますが、今回の用途には十分すぎるので、ほんのチョッピリ手を加えて使用します。

API Key, EndPointの設定

API KeyEndPointapp.configに記載しています。
ConfigurationManagerは既定ではExe名と同名のConfigファイルを読み込みます。
今回はapp.configという名前でConfigファイルを作ったので、OpenMappedExeConfigurationapp.configを指定しています。

devlights.hatenablog.com

なおAPI Keyなどのセンシティブな情報をConfigに記載するのはKey Vault警察逮捕案件なので…
ちゃんとする場合はKey Vaultで管理しましょう。

あと、XMLファイル内の&などの文字列はエスケープして記述する必要があるので注意。

stackoverflow.com

HttpClient Dispose警察案件

HttpClientを使いまわさずにDisposeしちゃってます。Dispose警察案件ですね。
※まぁそんな何回もキャプチャしないだろう、ということで妥協

詳しくはAzureの中の人である帝国兵さんのQiitaの記事をご参照ください

f:id:piyo_esq:20200125204518g:plain
🤖< rgr! rgr!

もしくは下記Microsoft Docs

docs.microsoft.com

System.Drawing

Bitmapクラスを使用するためにMicrosoft.Windows.Compatibilityを使用しています。

www.hanselman.com

Imageに表示中の画像ファイルの更新

Imageに表示中の画像はLockされている。

ので、Imageに表示するImageSourceはメモリにキャッシュした画像を表示する。

nineworks2.blog.fc2.com

高DPI対応

高DPI対応のためにapp.manifestを追加しdpiAwarenessdpiAwareを追加しました。

github.com

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
    </windowsSettings>
  </application>
</assembly>

おわり

OCRについて

OCR APIは対象言語が英語かつ画像の背景色が単色なら精度が高いですね。
背景がカラフルだと誤検出してしまうので、精度を上げるには画像をbinarizeしたり適宜フィルタリングした方がよいかも。

あと、他言語が混ざるとつらい感じになってしまいます。
例として中国のbili bili動画が作ったGolangのFrameworkをOCRすると...

f:id:piyo_esq:20200125203128p:plain
中国のbilibili動画が作ったGolang製Framework Kratos

LINQPadのDump結果

f:id:piyo_esq:20200125203253p:plain
OCRの結果をLINQPadでDump

aが化, oが 。 になっていますね。
中国語と英語が混ざった文章なので、難しいんでしょうね。

Translation Textについて

Translation Text APIの日本語翻訳は結構ひどいです。
Microsoft Docsの機械翻訳と同じエンジンなんでしょうか。

Google翻訳とかみらい翻訳並みになってくれ~

以上です。