OpenCV

【C++】OpenCVとTesseractで作る実用的な光学文字認識システム:画像前処理からテキスト抽出まで

C++とOpenCVを組み合わせることで、画像内の文字認識が可能な仕組みです。

画像をグレースケール変換、ノイズ除去、二値化などで前処理し、OCRエンジン(例:Tesseract)へ入力する流れとなります。

得られた文字情報はシステムの自動化やデータ管理などに活かされ、効率的な運用が期待できる手法です。

画像前処理アプローチ

画像の読み込みとフォーマット変換

カラー画像からグレースケールへの変換

まずは画像をcv::imreadで読み込み、カラー画像からグレースケールに変換します。

カラー画像は情報量が多くなるため、処理速度の向上やメモリ使用量の削減に向けてグレースケール変換を実施する方法があります。

以下のサンプルコードでは、cv::cvtColorを使ってカラー画像からグレースケール画像に変換する手順を示しています。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // 入力画像をカラーで読み込み
    cv::Mat colorImage = cv::imread("input.jpg");
    if (colorImage.empty()) {
        std::cerr << "画像が読み込めませんでした" << std::endl;
        return -1;
    }
    // グレースケール画像に変換
    cv::Mat grayImage;
    cv::cvtColor(colorImage, grayImage, cv::COLOR_BGR2GRAY);
    // 変換結果を表示
    cv::imshow("Gray Image", grayImage);
    cv::waitKey(0);
    return 0;
}

サイズ・解像度の最適化

画像サイズが大きすぎると処理に時間がかかるため、適切なサイズにリサイズする工夫が必要です。

リサイズによってOCRエンジンに与える負荷が軽減されるほか、解像度によるノイズの影響も和らげることができます。

以下のサンプルコードは、画像サイズを半分に縮小する例を示しています。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat image = cv::imread("input.jpg");
    if (image.empty()) {
        std::cerr << "画像が読み込めませんでした" << std::endl;
        return -1;
    }
    cv::Mat resizedImage;
    // 画像サイズを半分にリサイズ
    cv::resize(image, resizedImage, cv::Size(image.cols / 2, image.rows / 2));
    cv::imshow("Resized Image", resizedImage);
    cv::waitKey(0);
    return 0;
}
(リサイズされた画像が表示されます)

ノイズ除去と平滑化処理

Gaussian Blurの適用

画像のノイズがOCRの精度に悪影響を及ぼすため、GaussianBlurを利用して平滑化を図る方法を紹介します。

ぼかし処理により、不要なノイズが除去されるとともに、文字部分の輪郭が強調される場合があります。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat grayImage = cv::imread("input_gray.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) {
        std::cerr << "グレースケール画像が読み込めませんでした" << std::endl;
        return -1;
    }
    cv::Mat blurredImage;
    // ガウシアンブラーでノイズを除去(カーネルサイズ5x5)
    cv::GaussianBlur(grayImage, blurredImage, cv::Size(5, 5), 0);
    cv::imshow("Blurred Image", blurredImage);
    cv::waitKey(0);
    return 0;
}
(平滑化された画像が表示されます)
ややぼかしが入る

中央値フィルタなどの利用検討

状況によっては、中央値フィルタcv::medianBlurがGaussian Blurよりも適している場合もあります。

特に、塩胡椒ノイズ(白黒のランダムなノイズ)には効果的です。

以下のサンプルコードは、中央値フィルタで画像を平滑化する手順を示しています。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat grayImage = cv::imread("input_gray.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) {
        std::cerr << "グレースケール画像が読み込めませんでした" << std::endl;
        return -1;
    }
    cv::Mat medianBlurImage;
    // 中央値フィルタを適用(カーネルサイズ3)
    cv::medianBlur(grayImage, medianBlurImage, 3);
    cv::imshow("Median Blurred Image", medianBlurImage);
    cv::waitKey(0);
    return 0;
}
(中央値フィルタが適用された画像が表示されます)

画像の二値化手法

閾値処理の選定

画像の二値化は、文字と背景の区別を明確にするために重要な処理です。

固定の閾値を使う方法では、明るさが一定の画像に対して有効です。

cv::thresholdを利用して、画像を二値化するサンプルコードは以下の通りです。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat grayImage = cv::imread("input_gray.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) {
        std::cerr << "グレースケール画像が読み込めませんでした" << std::endl;
        return -1;
    }
    cv::Mat binaryImage;
    // 閾値処理による二値化、閾値128, 最大値255
    cv::threshold(grayImage, binaryImage, 128, 255, cv::THRESH_BINARY);
    cv::imshow("Binary Image", binaryImage);
    cv::waitKey(0);
    return 0;
}

Adaptive Thresholdingの活用

照明条件が均一でない画像の場合、Adaptive Thresholdingが効果を発揮します。

局所的な輝度を元に閾値を計算することで、文字部分と背景をより正確に分離できます。

以下はcv::adaptiveThresholdを利用した例です。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::Mat grayImage = cv::imread("input_gray.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) {
        std::cerr << "グレースケール画像が読み込めませんでした" << std::endl;
        return -1;
    }
    cv::Mat adaptiveBinaryImage;
    // Adaptive Thresholdingの適用
    cv::adaptiveThreshold(grayImage, adaptiveBinaryImage, 255,
                          cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, 11, 2);
    cv::imshow("Adaptive Binary Image", adaptiveBinaryImage);
    cv::waitKey(0);
    return 0;
}

OCRエンジンとの連携

Tesseractの基本設定

言語パックと初期化パラメータ

TesseractはOCRエンジンのひとつで、事前に用意された言語パックを利用する仕組みです。

初期化時に使用する言語やその他のパラメータは、プロジェクトのニーズに合わせて選ぶことが大切です。

以下のコード例では、英語の言語パックを指定して初期化する方法を示しています。

#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // Tesseractの初期化のためのインスタンス生成
    tesseract::TessBaseAPI *ocr = new tesseract::TessBaseAPI();
    // 英語の言語パックで初期化
    if (ocr->Init(nullptr, "eng")) {
        std::cerr << "Tesseractの初期化に失敗しました" << std::endl;
        return -1;
    }
    // ここに画像の前処理結果が入る想定
    cv::Mat binaryImage = cv::imread("input_binary.jpg", cv::IMREAD_GRAYSCALE);
    if (binaryImage.empty()) {
        std::cerr << "二値化画像が読み込めませんでした" << std::endl;
        ocr->End();
        return -1;
    }
    // OCRの実行用の画像データをセット
    ocr->SetImage(binaryImage.data, binaryImage.cols, binaryImage.rows, 1,
                  binaryImage.step);
    char* outText = ocr->GetUTF8Text();
    std::cout << "抽出結果: " << std::endl << outText << std::endl;
    delete [] outText;
    ocr->End();
    return 0;
}
抽出結果:
(画像から抽出されたテキストが表示されます)

設定パラメータの最適化

Tesseractのパラメータを適切に設定することで、認識精度が向上する場合があります。

たとえば、ページ分割モード(PSM)やOCRエンジンモード(OEM)の値をプロジェクトに応じて調整する方法があります。

設定は、SetVariableメソッドなどを使って行うことができるため、実際の文字認識の精度を見ながらパラメータを試行錯誤していくのがおすすめです。

画像データの供給方法

OpenCVとのデータ連携

OpenCVで前処理した画像のデータをTesseractへ渡す際は、画像のバイナリデータと各パラメータ(画像の幅、高さ、チャンネル数、画像の幅に応じたバイト数)を適切に設定する必要があります。

これによって、Tesseractが画像情報を正しく解析し、テキストの抽出がスムーズに行えるようになります。

前述のサンプルコード内でocr->SetImageがこの役割を担っています。

メモリ管理の工夫

TesseractのOCR実行後は、抽出したテキストがヒープ上に確保されることが多いため、利用後に必ずメモリ解放する工夫が必要です。

delete[]を利用して抽出テキストのメモリを解放する処理を忘れずに入れることで、メモリリークを防ぐことができます。

また、OCRエンジンの終了処理としてocr->End()を呼び出す点にも注意が必要です。

OCR結果の取得とテキスト処理

テキスト抽出後の文字列処理

OCRによって抽出されたテキストは、そのままでは不要な空白や改行が混在していることがあります。

標準ライブラリの文字列操作機能を利用して、正規表現や文字列フィルタリングを行うと、抽出結果がより読みやすくなります。

例えば、空白の連続を一つのスペースにまとめたり、改行を削除したりする方法があります。

不要部分のフィルタリング

抽出テキストの中には、背景ノイズや前処理の影響によって誤認識が混じってしまう場合があります。

こうした不要部分を取り除くために、特定のパターンや特殊文字をリストアップし、フィルタリング処理を実装すると、実用性が向上します。

シンプルな方法として、std::stringfinderaseを駆使して、不要な文字列を削除する処理が考えられます。

認識精度向上の工夫

前処理による精度改善

画像補正手法の検討

画像の明るさやコントラストがOCRの認識精度に影響する場合があるので、cv::equalizeHistやコントラスト調整を試すのもひとつの方法です。

こうした画像補正手法を適用すると、文字と背景の差異がより顕著になり、OCRの結果が改善する可能性が高まります。

興味があれば、ヒストグラム平坦化も試してみるとよいでしょう。

輪郭抽出と領域分割の活用

cv::findContoursを用いることで、画像内の文字領域を個別に抽出する手法があります。

抽出された輪郭情報から矩形領域を計算し、その領域ごとにOCRを実施することで、混在した文字の分離がわかりやすくなります。

局所的に認識精度を高めるためのアプローチとして検討する価値がある方法です。

パラメータ調整アプローチ

Tesseractパラメータの検証

Tesseractにはさまざまなパラメータが存在し、これらの値を調整することで認識精度が大きく向上することがあります。

PSMモードやOEMモードの設定変更など、文字認識結果を注意深く観察しながらパラメータを変更することが推奨されます。

実際にテストケースを複数用意し、最適な組み合わせを見つけるとよいでしょう。

画像フィルタ設定の最適化

画像の前処理段階で利用しているフィルタのパラメータも、条件に応じて微調整する必要があります。

Gaussian BlurのカーネルサイズやAdaptive Thresholdingのブロックサイズ、定数項などが、最終的なOCR結果に影響を及ぼすため、試行錯誤しながら最適な値を見つけると効果的です。

エラー検出と修正処理

認識エラーのパターン分析

OCRの結果から誤認識のパターンを抽出しておくと、どの種類のエラーが頻発しているか把握しやすくなります。

たとえば、似た文字の誤認識や特定の記号が間違って認識される場合には、後処理で正規表現などを使って補正する方法を検討するとよいでしょう。

結果検証と再処理の仕組み

認識結果に対して一定の閾値やルールを設け、問題がある場合には再度前処理やパラメータの組み合わせを変えて再処理する仕組みを取り入れると、システム全体の信頼性が向上します。

場合によっては、ユーザーに結果確認を促すインターフェースも有用です。

デバッグおよびパフォーマンス最適化

ログ出力と検証ツールの利用

画像処理結果の可視化

処理の各段階で画像を保存するか、ウィンドウに表示することで、前処理の動作確認がしやすくなります。

各フィルタを適用した後の画像を比較することで、最適なパラメータを見つける手助けになります。

また、画像の変化をグラフやヒストグラムで表示するツールを併用するのもおすすめです。

認識結果の評価手法

OCR結果を評価するためのチェックリストを作成したり、一定基準に沿ったスコアを算出する仕組みを導入することで、システム全体の品質管理に役立ちます。

定量的な評価ができれば、デバッグの効率も向上します。

処理速度とメモリ使用の最適化

並列処理の導入検討

複数画像を同時に処理する場合や、リアルタイム性を求める場合には、マルチスレッドを利用して並列処理を実現すると効果的です。

C++標準ライブラリのstd::threadやOpenCVの並列処理機能を活用することで、処理速度を向上させる工夫が可能です。

#include <opencv2/opencv.hpp>
#include <thread>
#include <vector>
#include <iostream>
void processImage(const std::string& filename) {
    cv::Mat image = cv::imread(filename, cv::IMREAD_GRAYSCALE);
    if (image.empty()) {
        std::cerr << filename << "の読み込みに失敗しました" << std::endl;
        return;
    }
    cv::Mat result;
    cv::GaussianBlur(image, result, cv::Size(5, 5), 0);
    std::cout << filename << "の処理が完了しました" << std::endl;
}
int main() {
    std::vector<std::string> files = {"image1.jpg", "image2.jpg", "image3.jpg"};
    std::vector<std::thread> threads;
    for (const auto& file : files) {
        threads.emplace_back(processImage, file);
    }
    for (auto& th : threads) {
        th.join();
    }
    return 0;
}
image1.jpgの処理が完了しました
image2.jpgの処理が完了しました
image3.jpgの処理が完了しました

軽量化施策の実施方法

画像のリサイズ、不要なデータの削除、メモリ管理の徹底など、各フェーズで軽量化施策を講じると、全体のパフォーマンス改善につながります。

システム要求に合わせて処理の優先順位を調整し、必要な部分のみ高精度な処理を行うなど、柔軟な実装を心がけるとよいでしょう。

実装上の留意点と注意事項

ライブラリ間の互換性確認

OpenCVとTesseractのバージョン管理

OpenCVやTesseractはバージョンごとにAPIの仕様変更が発生することがあるため、利用するライブラリ同士の互換性を確認することが大切です。

開発環境を整える際は、各ライブラリのドキュメントをチェックし、互換性が保たれているバージョンを選ぶと安心です。

データ連携の注意点

画像データのフォーマットやメモリレイアウトなど、OpenCVとTesseract間でのデータ連携には細かい注意が必要です。

画像のチャネル数やバイト幅などが正しく一致していることを確認しながら実装することで、予期せぬエラーを回避できます。

エラー発生時の対応策

例外処理とリカバリ手順

実装中に予期しない状況が発生した際には、例外処理を適切に用意することが重要です。

try-catchブロックでエラー発生箇所を特定し、必要に応じてエラーメッセージのログ出力やリカバリ手順を実装すると、システムの安定稼働が期待できます。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    try {
        cv::Mat image = cv::imread("nonexistent.jpg");
        if (image.empty()) {
            throw std::runtime_error("画像の読み込みに失敗しました");
        }
        cv::imshow("Image", image);
        cv::waitKey(0);
    } catch (const std::exception& e) {
        std::cerr << "エラー発生: " << e.what() << std::endl;
    }
    return 0;
}
エラー発生: 画像の読み込みに失敗しました

デバッグ時のチェックポイント

各処理フェーズ(画像読み込み、前処理、OCR実行など)で途中経過をログ出力したり、画像表示機能を利用することで、どの段階で問題が発生しているかを特定しやすくなります。

小さなテストケースごとに確認を行うと、後続の処理にスムーズに組み込むことができるでしょう。

応用事例と発展利用

一括処理システムへの組込み事例

定型文書の自動読み取り

定型フォーマットの文書に対しては、OCRシステムを自動読み取りの一部に組み込むと、手作業のデータ入力作業を大幅に削減できる可能性があります。

テンプレートに合わせた前処理や領域抽出を行い、正確な文字認識結果を得る工夫が効果的です。

バッチ処理での活用方法

大量の画像ファイルや文書を一括処理する場合、前述の並列処理技術と組み合わせてバッチ処理システムに組み込むと、処理時間が短縮されることが期待できます。

ユーザーが手動で個々の画像を処理する手間が省け、業務効率が向上します。

リアルタイム文字認識の試み

カメラ画像でのOCR実装検討

セキュリティカメラやスマートデバイスなどからのリアルタイム映像を用いて、即座にOCR処理を行うケースもあります。

カメラからキャプチャした各フレームに対して前処理とOCRを実施することで、迅速な文字認識が可能となります。

端末の性能に応じた並列処理や軽量化策を組み合わせるとよいでしょう。

動画データからのテキスト抽出方法

動画データの場合、各フレームを個別に処理し、時系列で文字認識結果をまとめる手法があります。

特殊なシーンや動きのある状況でも、一定のフレームごとにOCRを実行することで、リアルタイムに近いテキスト抽出が実現できます。

動画のフレームレートやシーン切り替えに配慮した設計が求められます。

まとめ

今回の内容では、画像前処理からOCRエンジンとの連携、そして認識精度向上やパフォーマンスの最適化に至るまでの各工程について詳しく紹介しました。

実際のプロジェクトに組み込む際は、環境に合わせた調整が求められるため、柔軟に対応できる設計を意識することが大切です。

これから実装を進める際に、参考にしていただけると嬉しいです。

関連記事

Back to top button
目次へ