
【PowerShell】Windows標準機能のみを使ってOCR実行
Windows10以降のPCには、デフォルトでOCR機能(画像を読み取ってテキスト抽出する機能)が備わっています。そこで、外部ソフトを一切介さずに、Windows標準機能だけを使ってOCR実行するスクリプトを紹介します。
①組織ポリシーの制約が多い環境下でも使いたい。
②外部言語からサクッとOCR機能を呼び出したい。
という需要は割と多いはず…。
具体的には、Windows7以降で標準搭載されているPowerShell(Windows環境内で動作する多機能スクリプト言語)を使って、Windows10以降で標準搭載されているWinRT APIへアクセスし、OCR機能を呼び出します。
PowerShellスクリプトはこちら。
# WinRT APIを呼出して、画像ファイルからテキストを抽出
# WinRTの使用準備
try {
# WinRTアセンブリのロード
Add-Type -AssemblyName System.Runtime.WindowsRuntime
# 必要なクラスのロード
$null = [Windows.Storage.StorageFile, Windows.Storage, ContentType = WindowsRuntime]
$null = [Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]
$null = [Windows.Graphics.Imaging.SoftwareBitmap, Windows.Foundation, ContentType = WindowsRuntime]
$null = [Windows.Media.Ocr.OcrEngine]::AvailableRecognizerLanguages
# GetAwaiterメソッドを取得
$awaiterMethod = [WindowsRuntimeSystemExtensions].GetMember('GetAwaiter', 'Method', 'Public,Static') |
Where-Object { $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' } |
Select-Object -First 1
Write-Output "WinRTの初期化が正常に行われました。"
}
catch {
Write-Error "WinRT初期化中にエラーが発生しました: $($_.Exception.Message)"
}
# 非同期操作を同期的に実行するヘルパー関数
function Invoke-Async {
param (
[object]$AsyncTask, #非同期操作(IAsyncOperation)
[Type]$ReturnType #戻り値の型
)
try {
$genericMethod = $awaiterMethod.MakeGenericMethod($ReturnType)
$awaiter = $genericMethod.Invoke($null, @($AsyncTask))
return $awaiter.GetResult()
}
catch {
Write-Error "非同期操作の実行中にエラーが発生しました: $($_.Exception.Message)"
}
}
# OCR処理関数
function ExecOCR_byWinRT {
begin {
# OCRエンジンの初期化
if ($null -ne $Language) {
$ocrEngine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage($Language)
} else {
$ocrEngine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
}
if ($ocrEngine -eq $null) {
Write-Error "OCRエンジンの初期化中にエラーが発生しました: $($_.Exception.Message)"
return
}
}
process {
# OCRを同期的に実行
try {
# 画像ファイルの取得
$fileTask = [Windows.Storage.StorageFile]::GetFileFromPathAsync($Path)
$storageFile = Invoke-Async $fileTask -ReturnType ([Windows.Storage.StorageFile])
# ファイルストリーム作成
$contentTask = $storageFile.OpenAsync([Windows.Storage.FileAccessMode]::Read)
$fileStream = Invoke-Async $contentTask -ReturnType ([Windows.Storage.Streams.IRandomAccessStream])
# ビットマップデコーダー作成
$decoderTask = [Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($fileStream)
$bitmapDecoder = Invoke-Async $decoderTask -ReturnType ([Windows.Graphics.Imaging.BitmapDecoder])
# 画像をビットマップ形式へ変換
$bitmapTask = $bitmapDecoder.GetSoftwareBitmapAsync()
$softwareBitmap = Invoke-Async $bitmapTask -ReturnType ([Windows.Graphics.Imaging.SoftwareBitmap])
# ビットマップデータをOCRエンジンに渡して文字認識を実行
$ocrTask = $ocrEngine.RecognizeAsync($softwareBitmap)
$ocrResult = Invoke-Async $ocrTask -ReturnType ([Windows.Media.Ocr.OcrResult])
# 結果の返却
return $ocrResult.Lines | Select-Object -Property Text
} catch {
Write-Error "エラー発生箇所: OCR処理中. 詳細: $_.Exception.Message"
return
}
}
}
# サンプル画像パスと言語の指定
$path = "●●●●●"
$language = New-Object Windows.Globalization.Language "ja"
# OCR 実行
$result = ExecOCR_byWinRT -Path $path -Language $language
if ($result) {
Write-Output "OCR 結果:"
$result | ForEach-Object { Write-Output $_.Text }
} else {
Write-Error "OCR 処理に失敗しました。"
}
コード内の●●●●●を、画像データのファイルパスに置き換えて下さい。Windows10以降の環境であれば、これでテキスト抽出できます。
更に、このPowerShellコードを外部言語(VBAなど)から実行してやることで、「外部言語からOCR実行」も可能になりますね。
つまり、PowerShellを経由することで、VBA等から気軽にOCR機能を呼出せるようになります。そう考えると出番も多いのでは。
VBAコードはこちら
Private Sub ExecOCR()
'PowerShellスクリプトを文字列として定義
Dim ImagePath As String
Dim LanguageCode As String
Dim Script1 As String
Dim Script2 As String
Dim Script3 As String
Dim Script4 As String
Dim Script5 As String
Dim PowerShellScript As String
'OCR対象の画像パスと言語コード
ImagePath = "●●●●●"
LanguageCode = "ja"
Script1 = _
"try {" & vbCrLf & _
" Add-Type -AssemblyName System.Runtime.WindowsRuntime" & vbCrLf & _
" $null = [Windows.Storage.StorageFile, Windows.Storage, ContentType = WindowsRuntime]" & vbCrLf & _
" $null = [Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]" & vbCrLf & _
" $null = [Windows.Graphics.Imaging.SoftwareBitmap, Windows.Foundation, ContentType = WindowsRuntime]" & vbCrLf & _
" $null = [Windows.Media.Ocr.OcrEngine]::AvailableRecognizerLanguages" & vbCrLf & _
" $awaiterMethod = [WindowsRuntimeSystemExtensions].GetMember('GetAwaiter', 'Method', 'Public,Static') |" & vbCrLf & _
" Where-Object { $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' } |" & vbCrLf & _
" Select-Object -First 1" & vbCrLf & _
" } catch {" & vbCrLf & _
" Write-Error 'WinRT初期化中にエラーが発生しました: $($_.Exception.Message)'" & vbCrLf & _
" }" & vbCrLf
Script2 = _
" function Invoke-Async {" & vbCrLf & _
" param (" & vbCrLf & _
" [object]$AsyncTask," & vbCrLf & _
" [Type]$ReturnType" & vbCrLf & _
" )" & vbCrLf & _
" try {" & vbCrLf & _
" $genericMethod = $awaiterMethod.MakeGenericMethod($ReturnType)" & vbCrLf & _
" $awaiter = $genericMethod.Invoke($null, @($AsyncTask))" & vbCrLf & _
" return $awaiter.GetResult()" & vbCrLf & _
" } catch {" & vbCrLf & _
" Write-Error '非同期操作の実行中にエラーが発生しました: $($_.Exception.Message)'" & vbCrLf & _
" }" & vbCrLf & " }" & vbCrLf
Script3 = _
" function ExecOCR_byWinRT {" & vbCrLf & _
" begin {" & vbCrLf & _
" if ($null -ne $Language) {" & vbCrLf & _
" $ocrEngine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage($Language)" & vbCrLf & _
" } else {" & vbCrLf & _
" $ocrEngine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()" & vbCrLf & _
" }" & vbCrLf & _
" if ($ocrEngine -eq $null) {" & vbCrLf & _
" Write-Error 'OCRエンジンの初期化中にエラーが発生しました: $($_.Exception.Message)'" & vbCrLf & _
" return" & vbCrLf & _
" }" & vbCrLf & " }" & vbCrLf
Script4 = _
" process {" & vbCrLf & _
" try {" & vbCrLf & _
" $fileTask = [Windows.Storage.StorageFile]::GetFileFromPathAsync($Path)" & vbCrLf & _
" $storageFile = Invoke-Async $fileTask -ReturnType ([Windows.Storage.StorageFile])" & vbCrLf & _
" $contentTask = $storageFile.OpenAsync([Windows.Storage.FileAccessMode]::Read)" & vbCrLf & _
" $fileStream = Invoke-Async $contentTask -ReturnType ([Windows.Storage.Streams.IRandomAccessStream])" & vbCrLf & _
" $decoderTask = [Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($fileStream)" & vbCrLf & _
" $bitmapDecoder = Invoke-Async $decoderTask -ReturnType ([Windows.Graphics.Imaging.BitmapDecoder])" & vbCrLf & _
" $bitmapTask = $bitmapDecoder.GetSoftwareBitmapAsync()" & vbCrLf & _
" $softwareBitmap = Invoke-Async $bitmapTask -ReturnType ([Windows.Graphics.Imaging.SoftwareBitmap])" & vbCrLf & _
" $ocrTask = $ocrEngine.RecognizeAsync($softwareBitmap)" & vbCrLf & _
" $ocrResult = Invoke-Async $ocrTask -ReturnType ([Windows.Media.Ocr.OcrResult])" & vbCrLf & _
" return $ocrResult.Lines | Select-Object -Property Text" & vbCrLf & _
" } catch {" & vbCrLf & _
" Write-Error 'エラー発生箇所: OCR処理中. 詳細: $_.Exception.Message'" & vbCrLf & _
" return" & vbCrLf & _
" }" & vbCrLf & " }" & vbCrLf & "}" & vbCrLf
Script5 = _
"$path = '" & ImagePath & "'" & vbCrLf & _
"$language = New-Object Windows.Globalization.Language '" & LanguageCode & "'" & vbCrLf & _
"$result = ExecOCR_byWinRT -Path $path -Language $language" & vbCrLf & _
"if ($result) {" & vbCrLf & _
" Write-Output $result | ForEach-Object { Write-Output $_.Text }" & vbCrLf & _
"} else {" & vbCrLf & _
" Write-Error 'OCR 処理に失敗しました。'" & vbCrLf & _
"}"
PowerShellScript = Script1 & Script2 & Script3 & Script4 & Script5
'一時ファイルにスクリプトを保存
Dim fileNum As Integer
Dim tempScriptPath As String
tempScriptPath = Environ("TEMP") & "\OCR_Script.txt"
fileNum = FreeFile
Open tempScriptPath For Output As #fileNum
Print #fileNum, PowerShellScript
Close #fileNum
'PowerShellコマンドの構築&実行
Dim Command As String
Dim Shell As Object
Dim ExecObj As Object
Command = "powershell -NoProfile -ExecutionPolicy Bypass -Command ""& { Get-Content '" & tempScriptPath & "' | Out-String | Invoke-Expression }"""
Set Shell = CreateObject("WScript.Shell")
On Error Resume Next
Set ExecObj = Shell.Exec(Command)
On Error GoTo 0
'標準出力を読み取る
Dim ErrorOutput As String
Dim Output As String
If Not ExecObj Is Nothing Then
Do While ExecObj.Status = 0
DoEvents
Loop
Output = ExecObj.StdOut.ReadAll
ErrorOutput = ExecObj.StdErr.ReadAll
If Len(ErrorOutput) > 0 Then
Debug.Print "エラー: " & vbCrLf & ErrorOutput
Else
Output = Replace(Output, " ", "")
Debug.Print "OCR結果: " & vbCrLf & Output
End If
Else
Debug.Print "PowerShellスクリプトの実行に失敗しました。"
End If
'一時ファイルを削除
On Error Resume Next
Kill tempScriptPath
On Error GoTo 0
End Sub
VBAからPowerShellを遠隔起動して、結果をVBA側で受け取っています。
WinRT APIは従来のCOM形式(.tlbや.dll)ではなく専用形式(.winmd)で提供されているため、互換性がないVBAでは直接扱えません。※VBAの参照設定やDeclareステートメントによる宣言ではそもそも無理。
そこで、.winmd形式をサポートしているPowerShell(つまり.NET環境)を経由することでサクッとWinRT APIを呼出し、処理結果だけをVBA側に戻しています。
遠隔起動するスクリプト自体も、文字列としてVBAに埋込んであるのでコード管理が楽です。
ここからは備忘録です。
Windowsの主要な2つのAPI。
【Win32 API】
・Windows95の時代から存在する従来のAPI。
・Windowsの基本的な機能を提供する低レベルAPI群。
・デスクトップアプリケーションの基盤。
・ファイル操作、ウィンドウ操作、メモリ管理、ネットワーク…等、OSコアに直接アクセス可能。
・低レベルであるため、多くの処理を手動で行う必要がある。
・デバイス間の互換性やモダンなUI開発には向いていない。
【WinRT(Runtime) API】
・Windows8以降に導入されたモダンなAPI。
・Win32 APIの上位レイヤーに構築された、より高レベルのAPI群。
・クロスプラットフォーム(PC、タブレット、スマホ等)で動作するUWPアプリケーションの基盤。
・非同期操作を標準化しており、UI応答性を重視。
・.NET、C++、JavaScript等の主要言語で統一的に利用可能。
WinRTが登場した背景
①異なるデバイスで共通のAPIが要求されるようになった。
(従来のWin32 APIではデバイス間の統一性を確保しづらかった)
②UIがフリーズしないように、非同期処理が重要になった。
(非同期操作をAPIレベルで標準化したかった)
低レベル制御を目的としたWin32と、高レベル処理を簡単に利用する目的のWinRTという設計思想の違いから、Windows8以降の環境では2つのAPI群が存在。OCRのような高レベル処理を実装するならまずWInRTの方を検討。
PowerShellコードの補足
以下、今回のPowerShellスクリプトの補足…というか備忘録。
Add-Type -AssemblyName System.Runtime.WindowsRuntime
PowerShell(つまり.NET環境)からWinRT APIを呼出す場合、そのままではWinRT型を直接利用できないため、WinRT型アセンブリをロードする必要あり。「System.Runtime.WindowsRuntime」という.NET型とWinRT型の橋渡しをするアセンブリ(=.NETランタイムアセンブリ)をPowerShellセッションへロードしている。
$null = [Windows.Storage.StorageFile, Windows.Storage, ContentType = WindowsRuntime] #画像ファイルの操作
$null = [Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime] #OCR処理のコア
$null = [Windows.Graphics.Imaging.SoftwareBitmap, Windows.Foundation, ContentType = WindowsRuntime] #画像データをビットマップ形式で操作
ここではWinRT型の各クラス宣言が目的なので、左辺を$nullとして結果を破棄。PowerShellセッション上で認識できるようにする「型の宣言」のイメージ。ただし、この時点ではクラスのロード(=メモリに配置して使用可能な状態にする)は行われず、実際に静的プロパティ・メソッドが参照されたりインスタンス生成されたタイミングで初めてロードされる。
なお右辺の内訳は次の通り。
1項:操作する具体的なクラス名を指定。
2項:名前空間を指定。
3項:この型がWinRT型であることを指定。
$null = [Windows.Media.Ocr.OcrEngine]::AvailableRecognizerLanguages
Windows.Media.Ocr.OcrEngineクラスのAvailableRecognizerLanguagesプロパティを参照している。これは静的プロパティのため、参照された時点でそのクラス全体(ここではOcrEngineクラス)がロードされる。クラスのロードが目的なので、左辺を$nullとして結果を破棄。
$awaiterMethod = [WindowsRuntimeSystemExtensions].GetMember('GetAwaiter', 'Method', 'Public,Static') |
Where-Object { $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' } |
Select-Object -First 1
後続のヘルパー関数内で使用するためのGetAwaiterメソッド(WinRT API)を定義する。
WinRTのメソッドには、同じ名前で複数のオーバーロード(=引数や戻り値の型・数が異なるバージョン)が存在する可能性がある。単純にGetAwaiterメソッドを呼出そうとしてもどのオーバーロードを指すのか分からず実行できない。ここでは「何らかのデータ型の引数を1つとる様なGetAwaiterメソッド」を特定し、左辺$awaiterMethod へ代入している。
【1行目】
WindowsRuntimeSystemExtensionsクラスのGetMemberメソッドを使い、名前がGetAwaiterでStatic(静的)かつPublic(公開)のメソッドを全て探している。GetMemberメソッドの戻り値はMemberInfoオブジェクトの配列。
【2行目】
Where-Objectフィルタを使い、取得した配列の中から「何らかの引数を1つとるような、ジェネリック型で定義されたIAsyncOperation<T>」に当てはまるオブジェクトを選択。
※「IAsyncOperation`1」のバッククォート内の数字は引数の数を示す。
引数<T>の型は、ジェネリック型として定義されているためここでは<T>だが、実際には<string>や<int>などの具体的な型が動的に代入される。
ジェネリック型とは、データ型そのものをパラメータとして扱える仕組みのこと。型を変数のように扱い、具体的な型を動的に指定できるためコードの再利用性が向上。ジェネリック型は型パラメータ(慣例的にT)を使用して定義される。Tの他にも、TResultやTValueやTKeyなど、慣例としてTの後に用途を示す単語が加えられることも。
IAsyncOperation<T>とは、WinRTの非同期操作を表現するインターフェースのこと。非同期操作の進行状況や結果を管理し、完了したときに戻り値を返す。.NET における Task<T> に似た概念。
文脈によっては「インターフェース自体を型として扱う」ことがあるため、IAsyncOperation<T>は非同期操作を表現するジェネリックインターフェースであり、かつ、戻り値や引数として使われるデータ型を意味する。
なお戻り値が無い場合は「IAsyncAction」を使用する。
特徴
・WinRTに特化(WinRT APIにおける非同期操作の標準的な方法)
・非同期処理を行うメソッドの戻り値として使用される。
(例えばIAsyncOperation<string> は文字列を返す非同期操作を表す)
・Completedプロパティで、非同期操作が完了したときの処理を定義可。
・ジェネリック型であり、任意の型を非同期操作の戻り値として扱う。
【3行目】
フィルタリング後の1つ目のオブジェクトを選択し、左辺$awaiterMethodへ代入。左辺$awaiterMethodには、GetAwaiter メソッドの情報(MethodInfo オブジェクト)が格納される。
GetAwaiterを使う事で、WinRT非同期操作(IAsyncOperation<T>)を、.NET標準のTaskベースの非同期プログラミングと統一的に扱えるようになる。
# 非同期操作を同期的に実行するヘルパー関数
function Invoke-Async {
param (
[object]$AsyncTask, #非同期操作(IAsyncOperation)
[Type]$ReturnType #戻り値の型
)
try {
$genericMethod = $awaiterMethod.MakeGenericMethod($ReturnType)
$awaiter = $genericMethod.Invoke($null, @($AsyncTask))
return $awaiter.GetResult()
}
catch {
Write-Error "非同期操作の実行中にエラーが発生しました: $($_.Exception.Message)"
}
}
PowerShellはシングルスレッドであり同期的なため、後続の非同期操作(WinRT API)を同期的に処理するためのヘルパー関数を作成する。
# OCR処理関数
function ExecOCR_byWinRT {
begin {
# OCRエンジンの初期化
if ($null -ne $Language) {
$ocrEngine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage($Language)
} else {
$ocrEngine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
}
if ($ocrEngine -eq $null) {
Write-Error "OCRエンジンの初期化中にエラーが発生しました: $($_.Exception.Message)"
return
}
}
process {
# OCRを同期的に実行
try {
# 画像ファイルの取得
$fileTask = [Windows.Storage.StorageFile]::GetFileFromPathAsync($Path)
$storageFile = Invoke-Async $fileTask -ReturnType ([Windows.Storage.StorageFile])
# ファイルストリーム作成
$contentTask = $storageFile.OpenAsync([Windows.Storage.FileAccessMode]::Read)
$fileStream = Invoke-Async $contentTask -ReturnType ([Windows.Storage.Streams.IRandomAccessStream])
# ビットマップデコーダー作成
$decoderTask = [Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($fileStream)
$bitmapDecoder = Invoke-Async $decoderTask -ReturnType ([Windows.Graphics.Imaging.BitmapDecoder])
# 画像をビットマップ形式へ変換
$bitmapTask = $bitmapDecoder.GetSoftwareBitmapAsync()
$softwareBitmap = Invoke-Async $bitmapTask -ReturnType ([Windows.Graphics.Imaging.SoftwareBitmap])
# ビットマップデータをOCRエンジンに渡して文字認識を実行
$ocrTask = $ocrEngine.RecognizeAsync($softwareBitmap)
$ocrResult = Invoke-Async $ocrTask -ReturnType ([Windows.Media.Ocr.OcrResult])
# 結果の返却
return $ocrResult.Lines | Select-Object -Property Text
} catch {
Write-Error "エラー発生箇所: OCR処理中. 詳細: $_.Exception.Message"
return
}
}
}
【Beginブロック】
WinRTのバックエンドで動作する実際のOCRエンジンをここで初期化する。
TryCreateFromLanguageメソッドを使って、指定された言語でOCRエンジンを作成。
【Processブロック】
先ほど作成したヘルパー関数Invoke-Asyncを使いながら、次の処理を実行。
1.画像ファイルの取得
ReturnTypeで、戻り値の型Windows.Storage.StorageFileを指定。
2.ファイルストリームの作成
読取り専用でファイルを開き、ストリームを作成。
3.画像のデコード
画像データを操作するためのビットマップデコーダーを作成。
画像をビットマップ形式へ変換。
4.OCRの実行
RecognizeAsyncメソッドで、ビットマップデータを解析。
OcrResultオブジェクトとして返される。
5.結果の返却
OcrResult.Linesは、各行のテキスト情報を保持するコレクション。
Select-Objectを使い、各行のTextプロパティのみ抽出して結果を返す。