【C++】OpenCV×HOG×SVMで実装する手書き文字認識入門
C++とOpenCVを使えば、画像をグレースケール化して二値化し、輪郭抽出後にリサイズするだけで学習用のデータが整い、HOG特徴量とSVM分類器を組み合わせれば高精度な手書き文字認識が可能です。
リアルタイム処理でもCPU負荷が小さく、組込み機器へも移植しやすいです。
データセットの準備
公開データセットの選定ポイント
手書き文字認識の学習には、質の高いデータセットが不可欠です。
公開されているデータセットを選ぶ際は、以下のポイントを重視してください。
- データの多様性
文字の書き手やスタイルが多様であることが重要です。
多様な手書き文字が含まれているほど、モデルの汎化性能が向上します。
- ラベルの正確さ
正確にラベル付けされていることが前提です。
誤ったラベルが多いと学習が妨げられます。
- データ量
十分なサンプル数があることが望ましいです。
特に深層学習を用いる場合は大量のデータが必要ですが、HOG+SVMでもある程度の量は必要です。
- 文字種の対応範囲
数字のみか、アルファベットや記号も含むか、対象とする文字種に合ったデータセットを選びます。
- フォーマットの扱いやすさ
画像ファイルや配列形式など、扱いやすいフォーマットで提供されているかも考慮します。
MNISTとEMNISTの比較
項目 | MNIST | EMNIST |
---|---|---|
文字種 | 0~9の数字 | 数字+大文字・小文字アルファベット |
サンプル数 | 70,000(学習60,000、テスト10,000) | 約814,255(複数のサブセットあり) |
画像サイズ | 28×28ピクセル | 28×28ピクセル |
ラベルの多様性 | 10クラス | 最大62クラス |
利用用途 | 数字認識に特化 | 数字と英字の認識に対応 |
MNISTは手書き数字認識の定番データセットで、シンプルかつ広く使われています。
EMNISTはMNISTを拡張し、英字も含むため、より多様な文字認識に適しています。
文字種が多い場合はEMNISTを選ぶと良いでしょう。
独自データ収集時の留意点
独自に手書き文字データを収集する場合は、以下の点に注意してください。
- 多様な書き手の確保
年齢や性別、筆圧や筆記具の違いなど、多様な条件の書き手からデータを集めることで、モデルの汎用性が高まります。
- 撮影・スキャン環境の統一
画像の解像度や照明条件をできるだけ一定に保つことで、ノイズや影響を減らせます。
- ラベル付けの正確性
手動でラベル付けする場合は、複数人でクロスチェックを行い、誤ラベルを減らす工夫が必要です。
- プライバシーと著作権の配慮
個人情報が含まれないようにし、収集時には同意を得ることが重要です。
- データフォーマットの統一
画像サイズやファイル形式を統一し、後の処理をスムーズにします。
データアノテーションの流れ
手書き文字認識では、画像に正確なラベルを付けることが学習の基盤となります。
アノテーションは以下の流れで進めます。
- 画像の収集
手書き文字を含む画像を用意します。
スキャンや撮影、既存データの利用など方法は様々です。
- 文字領域の切り出し
画像内の文字部分を切り出し、1文字ずつの画像に分割します。
輪郭抽出や領域分割を活用します。
- ラベル付け
各文字画像に対応する正しい文字ラベルを付与します。
- 検証と修正
ラベルの誤りや不適切な切り出しがないか確認し、必要に応じて修正します。
ラベリングツールの選択肢
ラベル付けを効率化するために、以下のようなツールを利用できます。
- LabelImg
画像の矩形領域にラベルを付けるGUIツール。
主に物体検出用ですが、文字領域の切り出しに使えます。
- VGG Image Annotator (VIA)
Webベースのアノテーションツールで、矩形や多角形の領域指定が可能です。
- 自作ツール
特定の用途に合わせて簡単なラベル付けツールをC++やPythonで作成することもあります。
- ExcelやCSVで管理
画像ファイル名とラベルを対応付けるだけなら、表計算ソフトで管理する方法もあります。
アノテーション精度向上のコツ
- 複数人でのラベル付けと照合
複数の担当者が同じデータをラベル付けし、結果を比較して誤りを減らします。
- ラベル付けルールの明確化
曖昧な文字や判別が難しいケースの対応方法を事前に決めておくと、一貫性が保てます。
- 定期的なレビュー
一定数のラベル付けごとにサンプルを抽出し、品質をチェックします。
- 自動化支援の活用
既存の認識モデルを使って予測ラベルを付け、修正だけを人が行う方法で効率化できます。
データ分割戦略
学習データを適切に分割することは、モデルの性能評価や過学習防止に欠かせません。
基本的には学習用、検証用、テスト用の3つに分けます。
学習用・検証用・テスト用のバランス
データセット | 役割 | 割合の目安 |
---|---|---|
学習用 | モデルのパラメータを学習 | 60~80% |
検証用 | ハイパーパラメータ調整やモデル選択 | 10~20% |
テスト用 | 最終的な性能評価 | 10~20% |
- 学習用データはモデルの重みや境界を決定するために使います。十分な量が必要です
- 検証用データは学習中にモデルの性能を評価し、過学習を防ぐために利用します。ハイパーパラメータの調整にも使います
- テスト用データは学習や調整に一切使わず、最終的なモデルの汎化性能を測定します
分割比率はデータ量や用途によって調整しますが、極端に少ない検証・テストデータは避けるべきです。
クロスバリデーション利用時の注意
クロスバリデーションは、データを複数の分割に分けて学習と評価を繰り返す手法です。
以下の点に注意してください。
- データのシャッフル
分割前にデータをランダムにシャッフルし、偏りを減らします。
- クラスバランスの維持
各分割において、クラスの割合が均等になるように分割するストラティファイドクロスバリデーションが望ましいです。
- 計算コストの増加
分割数が多いほど計算時間が増えるため、リソースと相談して適切な分割数を選びます。
- 過学習のリスク軽減
クロスバリデーションは過学習の検出に有効ですが、モデル選択時に検証データを使いすぎると過学習の原因になるため注意が必要です。
これらを踏まえ、データセットの準備段階で適切な分割を行い、モデルの信頼性を高めてください。
前処理パイプライン
手書き文字認識の精度を高めるためには、入力画像に対して適切な前処理を施すことが重要です。
ここでは、グレースケール化からノイズ除去、輪郭抽出、リサイズまでの一連の処理を詳しく解説します。
グレースケール化と輝度正規化
カラー画像を扱う場合、まずはグレースケール化を行い、輝度情報のみに変換します。
これにより、色情報の影響を排除し、文字の形状に集中した処理が可能になります。
OpenCVではcv::cvtColor
関数を使って簡単にグレースケール化できます。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat colorImage = cv::imread("handwritten.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;
}
色補正の必要性
撮影環境やスキャン条件によっては、画像の明るさやコントラストにムラが生じることがあります。
これを補正しないと、後続の二値化や特徴抽出で誤認識が増える可能性があります。
輝度正規化としては、以下の方法が有効です。
- ヒストグラム均一化(Histogram Equalization)
画像の輝度分布を均一化し、コントラストを強調します。
OpenCVのcv::equalizeHist
で実装可能です。
- CLAHE(Contrast Limited Adaptive Histogram Equalization)
局所的にヒストグラム均一化を行い、過剰なコントラスト強調を抑制します。
cv::createCLAHE
で利用できます。
cv::Mat equalizedImage;
cv::equalizeHist(grayImage, equalizedImage);
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();
clahe->setClipLimit(2.0);
cv::Mat claheImage;
clahe->apply(grayImage, claheImage);
これらの補正を行うことで、文字の輪郭がより鮮明になり、認識精度が向上します。
二値化手法の比較
二値化は、グレースケール画像を黒と白の2値画像に変換し、文字と背景を明確に分離する処理です。
代表的な手法を2つ紹介します。
Otsu法
Otsu法は画像のヒストグラムを解析し、自動的に最適なしきい値を決定する手法です。
単純かつ効果的で、多くのケースで利用されています。
cv::Mat binaryImage;
cv::threshold(grayImage, binaryImage, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
このコードは、grayImage
に対してOtsu法で二値化を行い、binaryImage
に結果を格納します。
自適応しきい値
画像の照明ムラが大きい場合は、画像全体で一つのしきい値を使うOtsu法よりも、自適応しきい値(Adaptive Threshold)が有効です。
局所的にしきい値を計算し、変化に対応します。
cv::Mat adaptiveBinary;
cv::adaptiveThreshold(grayImage, adaptiveBinary, 255,
cv::ADAPTIVE_THRESH_GAUSSIAN_C,
cv::THRESH_BINARY, 11, 2);
パラメータの11
はブロックサイズ、2
は定数で、これらを調整することで結果が変わります。
ノイズ除去
二値化後の画像には、細かいノイズや点状のゴミが残ることがあります。
これらを除去し、文字の輪郭を滑らかにするためにノイズ除去を行います。
メディアンフィルタ
メディアンフィルタは、各画素を周囲の画素の中央値に置き換えることで、塩胡椒ノイズを効果的に除去します。
エッジを保持しやすい特徴があります。
cv::Mat denoisedImage;
cv::medianBlur(binaryImage, denoisedImage, 3); // カーネルサイズは奇数で指定
モルフォロジー演算
モルフォロジー演算は、画像の形状を操作する処理で、ノイズ除去や穴埋めに使います。
代表的な演算は以下の通りです。
- 膨張(Dilation)
白い領域を拡大し、細かい穴を埋める。
- 収縮(Erosion)
白い領域を縮小し、小さなノイズを除去します。
- オープニング(Opening)
収縮の後に膨張を行い、ノイズを除去しつつ文字の形状を保ちます。
- クロージング(Closing)
膨張の後に収縮を行い、穴埋めに効果的。
cv::Mat morphImage;
cv::Mat element = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::morphologyEx(denoisedImage, morphImage, cv::MORPH_OPEN, element);
輪郭抽出とROI生成
文字の領域を正確に切り出すために、輪郭抽出を行います。
OpenCVのcv::findContours
関数を使うことで、画像中の連結した白い領域の輪郭を検出できます。
cv::findContoursの使い方
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(morphImage, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
RETR_EXTERNAL
は外側の輪郭のみを抽出しますCHAIN_APPROX_SIMPLE
は輪郭の点を圧縮して格納します
輪郭が検出できたら、各輪郭のバウンディングボックスを取得し、文字領域を切り出します。
for (size_t i = 0; i < contours.size(); i++) {
cv::Rect boundingBox = cv::boundingRect(contours[i]);
cv::Mat roi = morphImage(boundingBox);
// roiを使って特徴量抽出などを行う
}
バウンディングボックス調整
輪郭のバウンディングボックスは文字を囲みますが、余白が少なすぎたり大きすぎたりする場合があります。
以下の調整を行うことが多いです。
- 余白の追加
バウンディングボックスの周囲に数ピクセルの余白を加え、文字の切れを防ぐ。
- 画像サイズの制限
画像の端に近い場合は、余白追加時に画像外に出ないように調整。
int padding = 5;
int x = std::max(boundingBox.x - padding, 0);
int y = std::max(boundingBox.y - padding, 0);
int width = std::min(boundingBox.width + 2 * padding, morphImage.cols - x);
int height = std::min(boundingBox.height + 2 * padding, morphImage.rows - y);
cv::Rect adjustedBox(x, y, width, height);
cv::Mat adjustedROI = morphImage(adjustedBox);
画像リサイズとパディング
特徴量抽出や分類器の入力には、一定サイズの画像が必要です。
切り出した文字画像をリサイズし、必要に応じてパディングを加えます。
アスペクト比を保つ方法
文字の形状を歪めないために、リサイズ時はアスペクト比を維持することが望ましいです。
以下の手順で行います。
- 文字画像の縦横比を計算。
- 目標サイズのうち、縦または横のどちらかに合わせてリサイズ。
- 残りの方向にパディング(余白)を追加し、正方形や指定サイズに調整。
int targetSize = 64;
int width = adjustedROI.cols;
int height = adjustedROI.rows;
float scale = static_cast<float>(targetSize) / std::max(width, height);
cv::Mat resizedImage;
cv::resize(adjustedROI, resizedImage, cv::Size(), scale, scale);
int top = (targetSize - resizedImage.rows) / 2;
int bottom = targetSize - resizedImage.rows - top;
int left = (targetSize - resizedImage.cols) / 2;
int right = targetSize - resizedImage.cols - left;
cv::Mat paddedImage;
cv::copyMakeBorder(resizedImage, paddedImage, top, bottom, left, right, cv::BORDER_CONSTANT, cv::Scalar(0));
境界値の扱い
パディング時の境界値は通常、背景色に合わせて黒(0)や白(255)を指定します。
手書き文字認識では背景が黒、文字が白の二値画像が多いため、cv::Scalar(0)
で黒を指定することが一般的です。
このように前処理パイプラインを整えることで、文字の形状を損なわずに認識モデルに適した入力画像を準備できます。
データ拡張
手書き文字認識のモデルをより頑健にするためには、学習データの多様性を増やすことが重要です。
データ拡張は、既存の画像に対して様々な変換やノイズ付加を行い、学習データを人工的に増やす手法です。
ここでは形状変換、ノイズ付加、コントラスト変化の代表的な方法を解説します。
形状変換
形状変換は、文字画像の位置や大きさ、形状を変えることで、異なる書き方や視点の変化に対応できるようにします。
回転
文字画像を回転させることで、傾きや斜めの文字に対する認識性能を向上させます。
OpenCVのcv::warpAffine
を使い、回転行列を生成して適用します。
#include <opencv2/opencv.hpp>
#include <iostream>
cv::Mat rotateImage(const cv::Mat& src, double angle) {
cv::Point2f center(src.cols / 2.0F, src.rows / 2.0F);
cv::Mat rotMat = cv::getRotationMatrix2D(center, angle, 1.0);
cv::Mat dst;
cv::warpAffine(src, dst, rotMat, src.size(), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(0));
return dst;
}
int main() {
cv::Mat img = cv::imread("character.png", cv::IMREAD_GRAYSCALE);
if (img.empty()) {
std::cerr << "画像が読み込めませんでした。" << std::endl;
return -1;
}
cv::Mat rotated = rotateImage(img, 15); // 15度回転
cv::imshow("Original", img);
cv::imshow("Rotated", rotated);
cv::waitKey(0);
return 0;
}
// ウィンドウに元画像と15度回転した画像が表示される
回転角度は±10度〜±15度程度の範囲でランダムに変化させることが多いです。
大きすぎる回転は文字の意味を変える可能性があるため注意してください。
スケーリング
文字の大きさを変えることで、異なる筆圧や書き手のサイズ差に対応します。
リサイズ関数cv::resize
を使い、拡大・縮小を行います。
cv::Mat scaleImage(const cv::Mat& src, double scaleFactor) {
cv::Mat dst;
cv::resize(src, dst, cv::Size(), scaleFactor, scaleFactor, cv::INTER_LINEAR);
return dst;
}
int main() {
cv::Mat img = cv::imread("character.png", cv::IMREAD_GRAYSCALE);
if (img.empty()) {
std::cerr << "画像が読み込めませんでした。" << std::endl;
return -1;
}
cv::Mat scaledUp = scaleImage(img, 1.2); // 20%拡大
cv::Mat scaledDown = scaleImage(img, 0.8); // 20%縮小
cv::imshow("Original", img);
cv::imshow("Scaled Up", scaledUp);
cv::imshow("Scaled Down", scaledDown);
cv::waitKey(0);
return 0;
}
// 元画像、20%拡大画像、20%縮小画像が表示される
スケーリング後は、必要に応じてパディングやリサイズで入力サイズに合わせることが重要です。
アフィン変換
アフィン変換は回転・平行移動・拡大縮小・せん断(斜め変形)を組み合わせた変換で、より多様な形状変化を表現できます。
cv::getAffineTransform
やcv::warpAffine
を使います。
cv::Mat affineTransform(const cv::Mat& src) {
cv::Point2f srcTri[3];
cv::Point2f dstTri[3];
srcTri[0] = cv::Point2f(0, 0);
srcTri[1] = cv::Point2f(src.cols - 1, 0);
srcTri[2] = cv::Point2f(0, src.rows - 1);
dstTri[0] = cv::Point2f(src.cols*0.0f, src.rows*0.33f);
dstTri[1] = cv::Point2f(src.cols*0.85f, src.rows*0.25f);
dstTri[2] = cv::Point2f(src.cols*0.15f, src.rows*0.7f);
cv::Mat warpMat = cv::getAffineTransform(srcTri, dstTri);
cv::Mat dst;
cv::warpAffine(src, dst, warpMat, src.size(), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(0));
return dst;
}
int main() {
cv::Mat img = cv::imread("character.png", cv::IMREAD_GRAYSCALE);
if (img.empty()) {
std::cerr << "画像が読み込めませんでした。" << std::endl;
return -1;
}
cv::Mat affineImg = affineTransform(img);
cv::imshow("Original", img);
cv::imshow("Affine Transformed", affineImg);
cv::waitKey(0);
return 0;
}
// 元画像とアフィン変換後の画像が表示される
アフィン変換は文字の形状を自然に変化させるため、学習データの多様性を高めるのに効果的です。
ノイズ付加
ノイズを加えることで、実際の手書き文字画像に存在する様々なノイズに対してモデルの耐性を強化します。
Gaussianノイズ
ガウシアンノイズは平均0の正規分布に従うノイズで、画像全体にランダムな明るさの変動を加えます。
cv::Mat addGaussianNoise(const cv::Mat& src, double mean = 0.0, double stddev = 10.0) {
cv::Mat noise = cv::Mat(src.size(), CV_16SC1);
cv::randn(noise, mean, stddev);
cv::Mat dst;
src.convertTo(dst, CV_16SC1);
cv::addWeighted(dst, 1.0, noise, 1.0, 0.0, dst);
dst.convertTo(dst, src.type());
return dst;
}
int main() {
cv::Mat img = cv::imread("character.png", cv::IMREAD_GRAYSCALE);
if (img.empty()) {
std::cerr << "画像が読み込めませんでした。" << std::endl;
return -1;
}
cv::Mat noisyImg = addGaussianNoise(img);
cv::imshow("Original", img);
cv::imshow("Gaussian Noise Added", noisyImg);
cv::waitKey(0);
return 0;
}
// 元画像とガウシアンノイズを加えた画像が表示される
ノイズの標準偏差stddev
を調整して、ノイズの強さをコントロールします。
スペックルノイズ
スペックルノイズは乗算型のノイズで、画像の明るさに比例してノイズが乗るため、よりリアルなノイズ表現が可能です。
cv::Mat addSpeckleNoise(const cv::Mat& src) {
cv::Mat noise(src.size(), CV_32FC1);
cv::randn(noise, 0, 0.1);
cv::Mat srcFloat;
src.convertTo(srcFloat, CV_32FC1, 1.0 / 255.0);
cv::Mat noisy = srcFloat + srcFloat.mul(noise);
cv::Mat dst;
noisy.convertTo(dst, CV_8UC1, 255.0);
return dst;
}
int main() {
cv::Mat img = cv::imread("character.png", cv::IMREAD_GRAYSCALE);
if (img.empty()) {
std::cerr << "画像が読み込めませんでした。" << std::endl;
return -1;
}
cv::Mat noisyImg = addSpeckleNoise(img);
cv::imshow("Original", img);
cv::imshow("Speckle Noise Added", noisyImg);
cv::waitKey(0);
return 0;
}
// 元画像とスペックルノイズを加えた画像が表示される
スペックルノイズは特にスキャン画像やカメラ撮影画像のノイズに近い特性を持ちます。
コントラスト変化
文字の見え方を変えることで、異なる照明条件や筆圧の違いに対応できるようにします。
ヒストグラム平坦化
ヒストグラム平坦化は画像の輝度分布を均一化し、コントラストを強調します。
OpenCVのcv::equalizeHist
で実装します。
cv::Mat equalizeContrast(const cv::Mat& src) {
cv::Mat dst;
cv::equalizeHist(src, dst);
return dst;
}
int main() {
cv::Mat img = cv::imread("character.png", cv::IMREAD_GRAYSCALE);
if (img.empty()) {
std::cerr << "画像が読み込めませんでした。" << std::endl;
return -1;
}
cv::Mat equalizedImg = equalizeContrast(img);
cv::imshow("Original", img);
cv::imshow("Histogram Equalized", equalizedImg);
cv::waitKey(0);
return 0;
}
// 元画像とヒストグラム平坦化後の画像が表示される
文字の輪郭がよりはっきりし、認識しやすくなります。
CLAHE
CLAHE(Contrast Limited Adaptive Histogram Equalization)は、画像を小さな領域に分割して局所的にヒストグラム平坦化を行い、過剰なコントラスト強調を防ぎます。
cv::Mat applyCLAHE(const cv::Mat& src) {
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();
clahe->setClipLimit(2.0);
cv::Mat dst;
clahe->apply(src, dst);
return dst;
}
int main() {
cv::Mat img = cv::imread("character.png", cv::IMREAD_GRAYSCALE);
if (img.empty()) {
std::cerr << "画像が読み込めませんでした。" << std::endl;
return -1;
}
cv::Mat claheImg = applyCLAHE(img);
cv::imshow("Original", img);
cv::imshow("CLAHE Applied", claheImg);
cv::waitKey(0);
return 0;
}
// 元画像とCLAHE適用後の画像が表示される
CLAHEは特に照明ムラがある画像で効果的にコントラストを改善します。
これらのデータ拡張手法を組み合わせることで、学習データの多様性が増し、手書き文字認識モデルの汎化性能が向上します。
特徴量抽出:Histogram of Oriented Gradients
手書き文字認識において、画像から効果的な特徴量を抽出することは認識精度を左右します。
Histogram of Oriented Gradients(HOG)は、画像の局所的な勾配方向の分布を捉え、文字の形状を表現する代表的な特徴量です。
ここではHOGの理論的背景から、OpenCVのHOGDescriptor
の主要な設定項目、リサイズとの関係、そして特徴量ベクトルの正規化について詳しく解説します。
HOGの理論的背景
HOGは画像のエッジや輪郭の方向性を捉える特徴量で、以下の手順で計算されます。
- 勾配計算
画像の各画素に対して、x方向とy方向の勾配を計算します。
勾配はエッジの強さと方向を示し、文字の輪郭や線の形状を反映します。
- セル単位の勾配方向ヒストグラム作成
画像を小さなセル(例:8×8ピクセル)に分割し、各セル内の勾配方向を一定のビン数(例:9ビン)に分けてヒストグラムを作成します。
各ビンは特定の角度範囲を表し、勾配の強さに応じて重み付けされます。
- ブロック単位での正規化
複数のセルをまとめたブロック(例:2×2セル)単位でヒストグラムを正規化します。
これにより、照明やコントラストの変化に対して頑健な特徴量が得られます。
- 特徴ベクトルの連結
画像全体のブロックごとの正規化ヒストグラムを連結し、高次元の特徴ベクトルを生成します。
この特徴量は、文字の形状を詳細に表現しつつ、照明変化やノイズに強い性質を持つため、手書き文字認識に適しています。
OpenCVのHOGDescriptor設定項目
OpenCVのHOGDescriptor
クラスはHOG特徴量の計算を簡単に行えます。
主な設定項目は以下の通りです。
セルサイズ
セルサイズは勾配方向ヒストグラムを計算する単位のピクセル数です。
一般的には8×8ピクセルがよく使われます。
- 小さいセルサイズは細かい特徴を捉えやすいですが、特徴ベクトルが大きくなり計算コストが増加します
- 大きいセルサイズは計算が軽くなりますが、細かい形状の違いを捉えにくくなります
cv::HOGDescriptor hog(
cv::Size(64, 64), // ウィンドウサイズ
cv::Size(16, 16), // ブロックサイズ
cv::Size(8, 8), // ブロックストライド
cv::Size(8, 8), // セルサイズ
9 // オリエンテーション数
);
上記の例ではセルサイズが8×8ピクセルに設定されています。
ブロックサイズ
ブロックサイズは正規化を行うセルのまとまりのサイズです。
通常はセルサイズの整数倍で設定します(例:16×16ピクセルは2×2セル分)。
- 大きなブロックサイズはより広範囲の情報を正規化し、照明変化に強くなります
- 小さなブロックサイズは局所的な特徴を強調しますが、ノイズに敏感になることがあります
オリエンテーション数
オリエンテーション数は勾配方向ヒストグラムのビン数で、通常は9ビン(0°〜180°を20°刻み)を使います。
- ビン数が多いほど細かい方向の違いを捉えられますが、特徴ベクトルが大きくなります
- 少ないビン数は計算が軽くなりますが、方向の情報が粗くなります
リサイズとHOGウィンドウサイズの関係
HOG特徴量は固定サイズのウィンドウ内で計算されるため、入力画像のサイズをウィンドウサイズに合わせる必要があります。
例えば、HOGDescriptor
のウィンドウサイズが64×64ピクセルの場合、入力画像も64×64にリサイズします。
リサイズ時はアスペクト比を保つことが望ましく、必要に応じてパディングでサイズ調整を行います。
アスペクト比を崩すと文字の形状が歪み、特徴量の意味が変わってしまうため注意が必要です。
特徴量ベクトルの正規化
HOGの特徴量はブロック単位で正規化されますが、最終的な特徴ベクトル全体に対しても追加の正規化を行うことがあります。
これにより、特徴量のスケールを一定に保ち、分類器の学習を安定させます。
一般的な正規化手法には以下があります。
- L2ノルム正規化
特徴ベクトルの各要素の二乗和の平方根で割る方法。
特徴量の大きさを1に揃えます。
- L1ノルム正規化
各要素の絶対値の和で割る方法。
スパース性を保ちやすいです。
OpenCVのHOGDescriptor
は内部でブロック単位の正規化を自動的に行いますが、必要に応じて外部で追加の正規化を実装することも可能です。
これらの設定を適切に調整し、入力画像のサイズと整合させることで、HOG特徴量は手書き文字の形状を効果的に表現し、高精度な認識を支えます。
分類器:Support Vector Machine
手書き文字認識において、抽出した特徴量をもとに文字クラスを判別するための分類器として、Support Vector Machine(SVM)が広く使われています。
SVMは高次元空間での最適な境界線を見つけることで、分類精度を高める強力な手法です。
ここではSVMの基本原理からカーネル選択、ハイパーパラメータの調整、学習データのスケーリング、そしてOpenCVでの実装方法まで詳しく解説します。
SVMの基本原理
SVMは、2クラス分類問題において、データ点を分ける最適な境界線(ハイパープレーン)を見つけるアルゴリズムです。
最適な境界線とは、クラス間のマージン(境界線から最も近いデータ点までの距離)を最大化するものを指します。
具体的には、以下のような特徴があります。
- マージン最大化
境界線と最も近いサンプル(サポートベクター)との距離を最大化し、汎化性能を高めます。
- サポートベクターの利用
境界線の決定に寄与するデータ点のみを使い、効率的に学習します。
- 線形分離と非線形分離の対応
線形分離可能な場合は単純な直線や平面で分類し、非線形の場合はカーネル関数を用いて高次元空間に写像し、線形分離可能にします。
SVMは多クラス分類には直接対応していませんが、One-vs-AllやOne-vs-Oneの手法で拡張可能です。
カーネルの選択
カーネル関数は、入力特徴量を高次元空間に写像し、非線形なデータを線形に分離可能にする役割を持ちます。
代表的なカーネルには以下があります。
線形カーネル
線形カーネルは特徴量空間を変換せず、そのまま線形分離を行います。
計算コストが低く、特徴量が十分に分離可能な場合に有効です。
数式で表すと、2つの特徴ベクトル
となります。
線形カーネルはHOG特徴量のように高次元かつ線形分離可能な特徴に適しています。
RBFカーネル
RBF(Radial Basis Function)カーネルは非線形分離に強く、局所的な影響範囲を持つガウス関数を用います。
複雑なデータ分布に対応可能ですが、計算コストは高くなります。
数式は以下の通りです。
ここで、
RBFカーネルは特徴量の分布が複雑な場合に有効ですが、ハイパーパラメータの調整が重要です。
ハイパーパラメータ
SVMの性能はハイパーパラメータの設定に大きく依存します。
主に調整すべきパラメータは以下です。
C値とガンマの調整
- C値(ペナルティパラメータ)
誤分類をどれだけ許容するかを制御します。
大きい値は誤分類を厳しく罰し、学習データにフィットしやすくなりますが過学習のリスクも高まります。
小さい値は誤分類を許容し、汎化性能を高めます。
- ガンマ(RBFカーネルの場合)
ガンマはRBFカーネルの影響範囲を決めます。
大きい値は近傍のデータに強く影響し、複雑な境界を作ります。
小さい値は広範囲に影響し、滑らかな境界になります。
これらのパラメータはグリッドサーチやクロスバリデーションで最適値を探索します。
マージンと誤差許容のトレードオフ
SVMはマージン最大化を目指しますが、完全な線形分離が不可能な場合は誤差を許容します。
C値はこの誤差許容度を調整し、マージンの広さと誤分類のバランスを取ります。
- 大きなC値は誤分類を減らすためマージンが狭くなり、過学習しやすい
- 小さなC値はマージンを広く取り、多少の誤分類を許容し汎化性能を高める
学習データのスケーリング
SVMは特徴量のスケールに敏感なため、学習前に特徴量をスケーリングすることが推奨されます。
一般的な方法は以下です。
- 標準化(Zスコア正規化)
各特徴量を平均0、標準偏差1に変換します。
- 最小最大スケーリング
各特徴量を0〜1の範囲に収めます。
OpenCVでは自動スケーリング機能はありませんので、cv::normalize
や自作関数で前処理を行います。
cv::Mat scaledFeatures;
cv::normalize(features, scaledFeatures, 0, 1, cv::NORM_MINMAX);
スケーリングを行うことで、SVMの学習が安定し、収束速度も向上します。
実装時のcv::ml::SVM設定
OpenCVの機械学習モジュールcv::ml::SVM
を使ったSVMの実装例を示します。
#include <opencv2/opencv.hpp>
#include <opencv2/ml.hpp>
#include <iostream>
int main() {
// 特徴量とラベルの用意(例としてランダムデータ)
cv::Mat trainingData(100, 64, CV_32F);
cv::randu(trainingData, 0, 1);
cv::Mat labels(100, 1, CV_32S);
for (int i = 0; i < 100; i++) {
labels.at<int>(i, 0) = (i < 50) ? 1 : 2; // 2クラスラベル
}
// SVMの生成とパラメータ設定
cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
svm->setType(cv::ml::SVM::C_SVC);
svm->setKernel(cv::ml::SVM::RBF);
svm->setC(2.5);
svm->setGamma(0.05);
svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER, 1000, 1e-6));
// 学習
svm->train(trainingData, cv::ml::ROW_SAMPLE, labels);
// モデル保存
svm->save("svm_model.yml");
// 推論例
cv::Mat sample = trainingData.row(0);
float response = svm->predict(sample);
std::cout << "予測クラス: " << response << std::endl;
return 0;
}
予測クラス: 1
setType
でSVMの種類を指定(多クラス分類はC_SVC
)setKernel
でカーネル関数を選択(RBF
やLINEAR
など)setC
とsetGamma
でハイパーパラメータを設定train
で学習を実行し、predict
で推論を行います
このようにOpenCVのcv::ml::SVM
はシンプルなAPIでSVMを扱え、手書き文字認識の分類器として活用しやすいです。
学習プロセス
手書き文字認識モデルの学習は、特徴量の準備から分類器への入力、学習方法の選択、そして学習済みモデルの保存と再利用まで一連の流れで行います。
ここでは、OpenCVのMat
形式から特徴量マトリクスへの変換方法、バッチ学習とオンライン学習の違い、さらにモデルの保存と再利用について詳しく解説します。
データローダの作成
学習に用いる特徴量とラベルは、OpenCVのcv::Mat
形式で管理されることが多いです。
特徴量は各サンプルごとに1行のベクトルとして格納し、ラベルは対応するクラス番号を1列のベクトルとして用意します。
これらをSVMなどの機械学習モデルに渡すために、適切な形状のマトリクスを作成する必要があります。
OpenCV Matから特徴量マトリクスへ変換
例えば、手書き文字画像からHOG特徴量を抽出し、それを学習用の特徴量マトリクスにまとめる場合の流れを示します。
#include <opencv2/opencv.hpp>
#include <opencv2/ml.hpp>
#include <iostream>
#include <vector>
int main() {
// 例として複数の画像ファイルパスを用意
std::vector<std::string> imagePaths = {"char1.png", "char2.png", "char3.png"};
std::vector<int> labels = {0, 1, 2}; // 各画像のラベル
cv::HOGDescriptor hog(
cv::Size(64, 64), // ウィンドウサイズ
cv::Size(16, 16), // ブロックサイズ
cv::Size(8, 8), // ブロックストライド
cv::Size(8, 8), // セルサイズ
9 // オリエンテーション数
);
cv::Mat featureMat; // 特徴量行列(サンプル数×特徴量次元)
cv::Mat labelMat(labels.size(), 1, CV_32S);
for (size_t i = 0; i < imagePaths.size(); ++i) {
cv::Mat img = cv::imread(imagePaths[i], cv::IMREAD_GRAYSCALE);
if (img.empty()) {
std::cerr << "画像が読み込めません: " << imagePaths[i] << std::endl;
continue;
}
cv::resize(img, img, cv::Size(64, 64)); // HOGウィンドウサイズにリサイズ
std::vector<float> descriptors;
hog.compute(img, descriptors);
// descriptorsをMatに変換
cv::Mat descriptorMat(descriptors);
descriptorMat = descriptorMat.t(); // 行ベクトルに変換
featureMat.push_back(descriptorMat);
labelMat.at<int>(static_cast<int>(i), 0) = labels[i];
}
std::cout << "特徴量行列サイズ: " << featureMat.size() << std::endl;
std::cout << "ラベル行列サイズ: " << labelMat.size() << std::endl;
return 0;
}
このコードでは、複数の画像からHOG特徴量を抽出し、featureMat
に1サンプルずつ行ベクトルとして追加しています。
labelMat
には対応するラベルを格納します。
こうして作成した特徴量マトリクスとラベルマトリクスをSVMのtrain
関数に渡して学習を行います。
バッチ学習とオンライン学習の選択
学習方法には大きく分けてバッチ学習とオンライン学習があります。
- バッチ学習
全ての学習データを一括で用いてモデルを学習します。
SVMのような従来の機械学習アルゴリズムは基本的にバッチ学習です。
大量のデータを一度に処理するため、計算資源が必要ですが、モデルの収束が安定しやすい特徴があります。
- オンライン学習
データを逐次的に取り込みながらモデルを更新します。
データが大量で一度に処理できない場合や、リアルタイムでモデルを更新したい場合に有効です。
OpenCVのSVMはオンライン学習に対応していませんが、他のアルゴリズムやライブラリで利用可能です。
手書き文字認識の多くのケースでは、データセットが事前に用意されているためバッチ学習が主流です。
オンライン学習は、継続的に新しい手書き文字データを収集しモデルを更新するシナリオで検討されます。
モデル保存と再利用
学習済みモデルはファイルに保存し、後で再利用することが一般的です。
OpenCVのcv::ml::StatModel
クラスにはsave
とload
メソッドが用意されており、簡単にモデルの永続化が可能です。
cv::ml::StatModel::saveの活用
以下はSVMモデルの保存と読み込みの例です。
#include <opencv2/opencv.hpp>
#include <opencv2/ml.hpp>
#include <iostream>
int main() {
// 省略: 学習済みのSVMモデルを用意
cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
// ... svm->train(...) で学習済みと仮定
// モデルの保存
std::string modelPath = "svm_handwriting.yml";
svm->save(modelPath);
std::cout << "モデルを保存しました: " << modelPath << std::endl;
// モデルの読み込み
cv::Ptr<cv::ml::SVM> loadedSvm = cv::ml::SVM::load(modelPath);
if (loadedSvm.empty()) {
std::cerr << "モデルの読み込みに失敗しました。" << std::endl;
return -1;
}
std::cout << "モデルを読み込みました。" << std::endl;
// 推論例
cv::Mat sample; // 特徴量ベクトルを用意
// ... sampleの準備
float response = loadedSvm->predict(sample);
std::cout << "予測結果: " << response << std::endl;
return 0;
}
save
メソッドはXMLまたはYAML形式でモデルを保存し、load
で復元します。
これにより、学習済みモデルを何度も再学習せずに利用でき、推論処理の効率化が図れます。
これらの手順を踏むことで、効率的かつ再現性の高い手書き文字認識モデルの学習と運用が可能になります。
評価と検証
手書き文字認識モデルの性能を正確に把握し、改善点を見つけるためには、適切な評価指標の選定と検証手法が欠かせません。
ここでは代表的な評価指標の解説から、K-foldクロスバリデーションの活用、さらに誤分類のパターン解析方法について詳しく説明します。
指標選定
モデルの性能を評価する際には、単に正解率だけでなく、より詳細な指標を用いることで、誤分類の傾向やクラスごとの性能を把握できます。
正解率
正解率(Accuracy)は、全予測のうち正しく分類された割合を示します。
計算式は以下の通りです。
単純でわかりやすい指標ですが、クラスの不均衡がある場合は過大評価されることがあります。
例えば、あるクラスが極端に多い場合、そのクラスを常に予測するだけで高い正解率を得られることがあります。
精度・再現率・F1
より詳細な性能評価には、以下の指標が用いられます。
これらは特に二値分類や多クラス分類の各クラスごとに計算されます。
- 精度(Precision)
予測した正例のうち、実際に正例であった割合。
- 再現率(Recall)
実際の正例のうち、正しく予測された割合。
- F1スコア
精度と再現率の調和平均で、バランスの良い指標。
ここで、TPはTrue Positive(真陽性)、FPはFalse Positive(偽陽性)、FNはFalse Negative(偽陰性)を指します。
これらの指標は、特に誤分類の影響が大きいクラスに注目したい場合に有効です。
混同行列
混同行列(Confusion Matrix)は、実際のクラスと予測クラスのクロス集計表で、多クラス分類の性能を視覚的に把握できます。
例えば、3クラス分類の場合は以下のような行列になります。
実際\予測 | クラス1 | クラス2 | クラス3 |
---|---|---|---|
クラス1 | TP1 | FP12 | FP13 |
クラス2 | FP21 | TP2 | FP23 |
クラス3 | FP31 | FP32 | TP3 |
- 対角成分が正しく分類された数(True Positive)
- 非対角成分が誤分類の数を示します
混同行列を分析することで、どのクラス間で誤認識が多いかを特定し、モデル改善のヒントを得られます。
K-foldクロスバリデーション
K-foldクロスバリデーションは、データセットをK個の等しいサイズのサブセットに分割し、K回の学習と評価を繰り返す手法です。
各回で1つのサブセットを検証用に使い、残りを学習用に使います。
この方法の利点は以下の通りです。
- 過学習の検出
複数の分割で評価するため、特定のデータに依存した過学習を検出しやすい。
- 汎化性能の安定評価
全データを学習と検証に使うため、評価結果のばらつきが減少。
- データ不足時の有効活用
少ないデータでも効率的に学習と評価が可能です。
OpenCVでは直接的なクロスバリデーション関数はありませんが、自作でデータ分割と学習・評価を繰り返すことで実装できます。
エラーパターン解析
誤分類の傾向を把握することで、モデルの弱点を明確にし、改善策を検討できます。
以下の方法が有効です。
誤分類例の可視化
誤分類したサンプルを画像として表示し、どのような文字や特徴で誤認識が起きているかを目視で確認します。
例えば、似た形状の文字間で誤分類が多い場合、特徴量抽出や分類器の調整が必要と判断できます。
for (size_t i = 0; i < testSamples.size(); ++i) {
int predicted = svm->predict(testFeatures.row(i));
int actual = testLabels.at<int>(i, 0);
if (predicted != actual) {
cv::imshow("Misclassified Sample", testImages[i]);
std::cout << "実際のクラス: " << actual << ", 予測クラス: " << predicted << std::endl;
cv::waitKey(0);
}
}
クラスタリングで傾向把握
誤分類サンプルの特徴量をクラスタリング(例:k-means)し、誤認識のパターンをグループ化します。
これにより、どの特徴が混同されやすいか、どのような文字形状が問題かを定量的に分析できます。
cv::Mat misclassifiedFeatures; // 誤分類サンプルの特徴量を格納
int clusterCount = 3;
cv::Mat labels, centers;
cv::kmeans(misclassifiedFeatures, clusterCount, labels,
cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 10, 1.0),
3, cv::KMEANS_PP_CENTERS, centers);
クラスタごとに代表的な誤分類例を抽出し、特徴の共通点を探ることで、特徴量設計や前処理の改善に役立てられます。
これらの評価と検証手法を組み合わせることで、手書き文字認識モデルの性能を多角的に分析し、効果的な改善を進められます。
性能最適化
手書き文字認識システムの実用化にあたっては、推論速度やメモリ消費の最適化が重要です。
特にリアルタイム処理や大量データの処理では、計算効率を高める工夫が求められます。
ここでは、HOG特徴量計算やSVM推論の高速化、マルチスレッド化、メモリ削減、GPUアクセラレーションの活用方法について詳しく解説します。
推論速度のボトルネック
推論処理の中で特に時間がかかる部分を特定し、重点的に最適化を行うことが効果的です。
手書き文字認識では主にHOG特徴量の計算とSVMによる分類がボトルネックとなります。
HOG計算の高速化
HOG特徴量は画像の勾配計算やヒストグラム作成を多数のセル単位で行うため、計算負荷が高いです。
高速化のポイントは以下の通りです。
- 画像サイズの最適化
入力画像を必要最小限のサイズにリサイズし、計算量を削減します。
ただし、あまり小さくすると特徴の損失につながるためバランスが重要です。
- 計算の再利用
連続フレームや類似画像間で共通部分の勾配計算をキャッシュし、再計算を避ける工夫が有効です。
- SIMD命令の活用
OpenCVは内部でSSEやAVXなどのSIMD命令を利用していますが、コンパイル時に最適化オプションを有効にすることでさらに高速化できます。
- 並列処理
複数のセルやブロックの計算を並列化し、CPUコアを有効活用します。
SVM予測の高速化
SVMの推論は、サポートベクター数やカーネルの種類によって計算量が変わります。
高速化のポイントは以下です。
- 線形SVMの利用
線形カーネルは計算が単純で高速です。
特徴量が線形分離可能なら線形SVMを選択すると良いでしょう。
- サポートベクターの削減
学習時にサポートベクター数を減らす工夫(例:パラメータ調整やプルーニング)で推論速度を向上できます。
- 近似手法の活用
ランダムフォレストや軽量なニューラルネットワークなど、SVM以外の高速推論モデルを検討することもあります。
マルチスレッド化
CPUの複数コアを活用して処理を並列化することで、推論速度を大幅に改善できます。
OpenMPの導入
OpenMPはC++で簡単に並列処理を実装できるAPIです。
HOG計算や複数画像の推論を並列化する例を示します。
#include <omp.h>
#include <vector>
#include <opencv2/opencv.hpp>
void computeHOGBatch(const std::vector<cv::Mat>& images, std::vector<std::vector<float>>& features) {
#pragma omp parallel for
for (int i = 0; i < static_cast<int>(images.size()); ++i) {
cv::HOGDescriptor hog(cv::Size(64, 64), cv::Size(16,16), cv::Size(8,8), cv::Size(8,8), 9);
std::vector<float> descriptor;
hog.compute(images[i], descriptor);
features[i] = descriptor;
}
}
OpenMPの#pragma omp parallel for
を使うだけで、ループの各イテレーションが複数スレッドで並列実行されます。
コンパイル時に-fopenmp
オプションを付ける必要があります。
TBBの利用
Intel TBB(Threading Building Blocks)はより高度な並列処理ライブラリで、タスクベースの並列化が可能です。
OpenCVもTBBを内部で利用している場合があります。
#include <tbb/parallel_for.h>
#include <tbb/blocked_range.h>
#include <vector>
#include <opencv2/opencv.hpp>
void computeHOGBatchTBB(const std::vector<cv::Mat>& images, std::vector<std::vector<float>>& features) {
tbb::parallel_for(tbb::blocked_range<size_t>(0, images.size()),
[&](const tbb::blocked_range<size_t>& r) {
cv::HOGDescriptor hog(cv::Size(64, 64), cv::Size(16,16), cv::Size(8,8), cv::Size(8,8), 9);
for (size_t i = r.begin(); i != r.end(); ++i) {
std::vector<float> descriptor;
hog.compute(images[i], descriptor);
features[i] = descriptor;
}
}
);
}
TBBはスケジューリングが柔軟で、負荷分散が優れているため大規模処理に適しています。
メモリ消費の削減
大量の画像や特徴量を扱う場合、メモリ使用量を抑える工夫が必要です。
特徴量圧縮
HOG特徴量は高次元でメモリを多く消費します。
圧縮手法としては以下があります。
- 主成分分析(PCA)
特徴量の次元を削減し、情報を保持しつつメモリ使用量を減らします。
- 量子化
浮動小数点の特徴量を整数やビット列に変換し、メモリ効率を高めます。
cv::PCA pca(featureMat, cv::Mat(), cv::PCA::DATA_AS_ROW, 50); // 50次元に削減
cv::Mat reducedFeatures;
pca.project(featureMat, reducedFeatures);
画像キャッシュ戦略
同じ画像や類似画像に対して何度も特徴量を計算しないよう、計算済み特徴量をキャッシュする方法があります。
メモリと速度のトレードオフを考慮し、適切なキャッシュサイズを設定します。
GPUアクセラレーション
GPUを活用することで、HOG計算やSVM推論の大幅な高速化が期待できます。
CUDA版HOGの利用可能性
NVIDIAのCUDAを使ったHOG計算は、OpenCVのcuda::HOG
クラスで利用可能です。
GPUの並列処理能力を活かし、CPUより高速に特徴量を抽出できます。
#include <opencv2/opencv.hpp>
#include <opencv2/cudaobjdetect.hpp>
int main() {
cv::cuda::GpuMat d_img;
cv::Mat img = cv::imread("character.png", cv::IMREAD_GRAYSCALE);
d_img.upload(img);
cv::cuda::HOG hog(cv::Size(64, 64), cv::Size(16,16), cv::Size(8,8), cv::Size(8,8), 9);
cv::cuda::GpuMat d_descriptors;
hog.compute(d_img, d_descriptors);
// d_descriptorsをCPUにダウンロードして利用可能
cv::Mat descriptors;
d_descriptors.download(descriptors);
return 0;
}
CUDA対応GPUが必要で、環境構築に注意が必要ですが、リアルタイム処理に適しています。
OpenCLバックエンド
OpenCVはOpenCLを利用したGPUアクセラレーションもサポートしています。
UMat
を使うことで、対応するハードウェアで自動的にGPU処理が行われます。
cv::UMat u_img;
cv::Mat img = cv::imread("character.png", cv::IMREAD_GRAYSCALE);
img.copyTo(u_img);
cv::HOGDescriptor hog(cv::Size(64, 64), cv::Size(16,16), cv::Size(8,8), cv::Size(8,8), 9);
std::vector<float> descriptors;
hog.compute(u_img, descriptors);
OpenCLはCUDAに比べて対応デバイスが多いですが、性能は環境に依存します。
これらの性能最適化手法を組み合わせることで、手書き文字認識システムの推論速度を向上させ、メモリ使用量を抑えつつ、リアルタイム処理や大規模データ処理に対応できます。
システム統合
手書き文字認識の技術を実際のC++アプリケーションに組み込み、カメラ入力からリアルタイムに認識結果を得るためには、システム全体の設計とUI連携が重要です。
ここでは、C++アプリケーションへの組み込み方法、カメラ入力のストリーム処理時の注意点、そしてUI連携の具体例を解説します。
C++アプリケーションへの組み込み
手書き文字認識の処理をC++アプリケーションに組み込む際は、機能ごとにクラスを分割し、保守性と拡張性を確保する設計が望ましいです。
クラス設計例
以下は、認識処理をカプセル化したシンプルなクラス設計例です。
#include <opencv2/opencv.hpp>
#include <opencv2/ml.hpp>
class HandwritingRecognizer {
private:
cv::Ptr<cv::ml::SVM> svm;
cv::HOGDescriptor hog;
public:
HandwritingRecognizer()
: hog(cv::Size(64, 64), cv::Size(16,16), cv::Size(8,8), cv::Size(8,8), 9) {
svm = cv::ml::SVM::create();
}
bool loadModel(const std::string& modelPath) {
svm = cv::ml::SVM::load(modelPath);
return !svm.empty();
}
int predict(const cv::Mat& img) {
cv::Mat resized;
cv::resize(img, resized, cv::Size(64, 64));
std::vector<float> descriptors;
hog.compute(resized, descriptors);
cv::Mat descriptorMat(descriptors);
descriptorMat = descriptorMat.t();
return static_cast<int>(svm->predict(descriptorMat));
}
};
このクラスは、HOG特徴量の計算とSVMによる推論をまとめています。
loadModel
で学習済みモデルを読み込み、predict
で画像から文字クラスを予測します。
アプリケーション側はこのクラスをインスタンス化し、画像を渡すだけで認識結果を得られます。
カメラ入力のストリーム処理
リアルタイム認識では、カメラからの映像を連続的に取得し、フレームごとに認識処理を行います。
OpenCVのcv::VideoCapture
クラスを使うのが一般的です。
cv::VideoCapture利用時の注意
- カメラの初期化確認
VideoCapture
オブジェクトの生成後、isOpened()
でカメラが正常に開けているか必ず確認します。
cv::VideoCapture cap(0);
if (!cap.isOpened()) {
std::cerr << "カメラが開けませんでした。" << std::endl;
return -1;
}
- フレーム取得の安定性
cap.read(frame)
でフレームを取得しますが、失敗する場合があるため戻り値をチェックし、ループを継続するか判断します。
- フレームサイズの設定
認識処理の負荷を考慮し、必要に応じてcap.set(cv::CAP_PROP_FRAME_WIDTH, width)
やcv::CAP_PROP_FRAME_HEIGHT, height
で解像度を調整します。
- 遅延とフレームレート
処理が重いとフレーム遅延が発生するため、認識処理の高速化やスレッド分割を検討します。
- カメラの解放
処理終了時はcap.release()
でカメラリソースを解放します。
UIとの連携
認識結果をユーザーにわかりやすく提示するため、UIとの連携が重要です。
ここではQtウィジェットでの表示例と、コンソール出力との比較を示します。
Qtウィジェットでの表示
QtはC++でGUIアプリケーションを作成するためのフレームワークで、OpenCVの画像を簡単に表示できます。
以下は、認識結果を画像上に描画して表示する例です。
#include <QApplication>
#include <QLabel>
#include <QImage>
#include <QPixmap>
#include <opencv2/opencv.hpp>
QImage cvMatToQImage(const cv::Mat& mat) {
if (mat.type() == CV_8UC1) {
QVector<QRgb> colorTable;
for (int i = 0; i < 256; i++)
colorTable.push_back(qRgb(i, i, i));
QImage img(mat.data, mat.cols, mat.rows, static_cast<int>(mat.step), QImage::Format_Indexed8);
img.setColorTable(colorTable);
return img.copy();
} else if (mat.type() == CV_8UC3) {
QImage img(mat.data, mat.cols, mat.rows, static_cast<int>(mat.step), QImage::Format_RGB888);
return img.rgbSwapped();
}
return QImage();
}
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
cv::Mat frame = cv::imread("handwritten_sample.png", cv::IMREAD_COLOR);
if (frame.empty()) return -1;
// 文字認識結果を仮に描画
cv::putText(frame, "Predicted: 5", cv::Point(10, 30),
cv::FONT_HERSHEY_SIMPLEX, 1.0, cv::Scalar(0, 255, 0), 2);
QImage qimg = cvMatToQImage(frame);
QLabel label;
label.setPixmap(QPixmap::fromImage(qimg));
label.show();
return app.exec();
}
この例では、OpenCVのcv::Mat
をQtのQImage
に変換し、QLabel
で表示しています。
認識結果の文字列を画像に描画して視覚的にフィードバックできます。
コンソール出力との比較
コンソール出力は実装が簡単でデバッグに便利ですが、以下の制約があります。
- 視覚的情報が乏しい
画像の状態や認識結果の位置関係がわかりにくい。
- ユーザー体験が限定的
一般ユーザー向けのアプリケーションには不向き。
一方、QtなどのGUIを使うと、認識結果を画像上に重ねて表示したり、インタラクティブな操作が可能になるため、実用的なシステム構築に適しています。
これらの設計と実装を踏まえ、手書き文字認識を組み込んだC++アプリケーションを効率的に開発し、リアルタイム処理やユーザーインターフェースとの連携を実現できます。
テストとデバッグ
手書き文字認識システムの品質を保ち、安定した動作を実現するためには、テストとデバッグが欠かせません。
ここでは、ユニットテストの対象となる関数群、ログ出力の整備方法、そしてパフォーマンス計測の具体的な手法について詳しく解説します。
ユニットテスト対象
ユニットテストは、個々の関数やモジュールが期待通りに動作するかを検証するテストです。
手書き文字認識では、特に前処理や特徴量抽出の関数が正確に動作しているかを重点的にテストします。
前処理関数
前処理関数は画像のグレースケール化、二値化、ノイズ除去、輪郭抽出などを担います。
これらの関数は画像の品質に直結するため、以下の点をテストします。
- 入力画像に対して期待される出力が得られるか
例えば、グレースケール化後の画像が正しいチャンネル数か、二値化後に適切なしきい値で白黒が分かれているか。
- ノイズ除去の効果
ノイズを含む画像を入力し、ノイズが除去されているかを確認。
- 輪郭抽出の正確性
既知の形状を持つ画像で輪郭数や位置が正しいかを検証。
テストコード例(Google Testを想定):
TEST(PreprocessingTest, GrayscaleConversion) {
cv::Mat colorImg = cv::imread("color_sample.jpg");
cv::Mat grayImg;
cv::cvtColor(colorImg, grayImg, cv::COLOR_BGR2GRAY);
EXPECT_EQ(grayImg.channels(), 1);
EXPECT_EQ(grayImg.size(), colorImg.size());
}
特徴量抽出関数
特徴量抽出関数はHOGなどの計算を行い、分類器の入力となるベクトルを生成します。
テストでは以下を確認します。
- 特徴量の次元数が期待通りか
HOGのパラメータに基づく特徴量ベクトルの長さを検証。
- 同一画像に対して一貫した特徴量が得られるか
複数回計算して結果が一致するか。
- 異なる画像で特徴量が異なること
異なる文字画像で特徴量が区別可能な値になっているか。
テストコード例:
TEST(FeatureExtractionTest, HOGDescriptorSize) {
cv::Mat img = cv::imread("char_sample.png", cv::IMREAD_GRAYSCALE);
cv::resize(img, img, cv::Size(64, 64));
cv::HOGDescriptor hog(cv::Size(64,64), cv::Size(16,16), cv::Size(8,8), cv::Size(8,8), 9);
std::vector<float> descriptors;
hog.compute(img, descriptors);
EXPECT_EQ(descriptors.size(), hog.getDescriptorSize());
}
ログ出力の整備
デバッグや運用時の問題解析を容易にするため、適切なログ出力を整備します。
OpenCVのデバッグトレース
OpenCVは内部で詳細なデバッグ情報を出力する機能を持っています。
環境変数やビルド設定で有効化可能です。
- 環境変数設定
OPENCV_LOG_LEVEL
をDEBUG
やINFO
に設定すると、詳細なログが標準出力に表示されます。
- ログコールバックの設定
OpenCVのcv::utils::logging::setLogCallback
を使い、ログメッセージを独自の関数で受け取ることができます。
#include <opencv2/core/utils/logger.hpp>
void customLogCallback(cv::utils::logging::LogLevel level, const char* msg) {
std::cout << "[OpenCV Log] " << msg << std::endl;
}
int main() {
cv::utils::logging::setLogCallback(customLogCallback);
cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_DEBUG);
// OpenCV処理
}
これにより、ログをファイルに保存したり、GUIに表示したりすることが可能です。
パフォーマンス計測
処理速度のボトルネックを特定し、最適化の効果を検証するためにパフォーマンス計測を行います。
cv::TickMeter活用
OpenCVのcv::TickMeter
は簡単に処理時間を計測できるクラスです。
使い方は以下の通りです。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::TickMeter tm;
cv::Mat img = cv::imread("char_sample.png", cv::IMREAD_GRAYSCALE);
cv::resize(img, img, cv::Size(64, 64));
cv::HOGDescriptor hog(cv::Size(64,64), cv::Size(16,16), cv::Size(8,8), cv::Size(8,8), 9);
tm.start();
std::vector<float> descriptors;
hog.compute(img, descriptors);
tm.stop();
std::cout << "HOG計算時間: " << tm.getTimeMilli() << " ms" << std::endl;
return 0;
}
start()
で計測開始、stop()
で終了getTimeMilli()
でミリ秒単位の経過時間を取得
複数回の計測や平均化も簡単に行え、処理の高速化効果を定量的に評価できます。
これらのテストとデバッグ手法を体系的に導入することで、手書き文字認識システムの信頼性と品質を高め、開発効率を向上させられます。
よくあるトラブルと対処
手書き文字認識システムの開発や運用中に遭遇しやすいトラブルと、その対処法を解説します。
問題の原因を正しく把握し、適切な対応を行うことで、システムの安定性と精度を向上させられます。
学習が収束しない
学習が収束しないとは、SVMなどの分類器の学習過程で目的関数の最適化が進まず、モデルが安定しない状態を指します。
主な原因と対処法は以下の通りです。
- 原因1:ハイパーパラメータの不適切な設定
C値やガンマ値が極端に大きい・小さい場合、学習が発散したり収束しにくくなります。
対処法:グリッドサーチやクロスバリデーションで適切なパラメータを探索し、安定した値を選択します。
- 原因2:特徴量のスケーリング不足
特徴量のスケールが大きく異なると、最適化が困難になります。
対処法:学習前に特徴量を標準化(平均0、分散1)や最小最大スケーリングで正規化します。
- 原因3:データのノイズや異常値
学習データに極端な外れ値や誤ラベルが多いと収束が妨げられます。
対処法:データクリーニングを行い、異常値を除去または修正します。
- 原因4:学習データの不均衡
クラス間のサンプル数が大きく偏っていると、学習が偏り収束しにくくなります。
対処法:データのリサンプリングや重み付けを検討します。
過学習の兆候
過学習は、モデルが学習データに過度に適合し、未知のデータに対して性能が低下する現象です。
兆候と対策は以下です。
- 兆候1:学習データでの高精度に対し、検証データでの精度が低い
学習曲線を確認し、学習と検証の誤差差が大きい場合は過学習の可能性が高いです。
- 兆候2:モデルが複雑すぎる
サポートベクターが多すぎたり、カーネルパラメータが過剰に複雑な場合。
- 対処法
- ハイパーパラメータの調整(C値を小さくする、ガンマを適切に設定)
- 特徴量の次元削減(PCAなど)
- データ拡張で学習データを増やす
- 早期停止や正則化の導入(SVMではC値調整が正則化に相当)
推論結果が空になる
推論結果が空、つまり分類器が何も返さない、または無効な値を返す場合の原因と対処法です。
- 原因1:入力特徴量の形式不一致
推論時の特徴量ベクトルのサイズや型が学習時と異なります。
対処法:特徴量抽出後のベクトルサイズを確認し、学習時と同じ形式に揃えます。
- 原因2:モデルの読み込み失敗
学習済みモデルファイルが破損している、またはパスが間違っています。
対処法:モデルのロード処理の戻り値をチェックし、正しく読み込めているか確認します。
- 原因3:前処理の不備
入力画像の前処理が不適切で、特徴量が正しく抽出できていない。
対処法:前処理パイプラインを見直し、二値化やリサイズが正しく行われているか検証します。
- 原因4:推論関数の誤用
predict
関数の引数や戻り値の扱いが間違っています。
対処法:OpenCVのAPI仕様を再確認し、正しい使い方を実装します。
グレースケール化でコントラスト不足
グレースケール化後の画像で文字と背景のコントラストが低く、二値化や特徴量抽出がうまくいかない場合の問題と対策です。
- 原因1:撮影環境の影響
照明不足や影の影響で画像全体が暗くなっています。
対処法:撮影環境を改善し、均一な照明を確保します。
- 原因2:グレースケール変換の誤り
カラー画像のチャンネル順や変換コードが間違っています。
対処法:cv::cvtColor
の引数を正しく設定し、BGR→GRAY変換を行います。
- 原因3:輝度正規化不足
画像の輝度分布が偏っているため、文字と背景の差が小さい。
対処法:ヒストグラム均一化やCLAHE(適応的ヒストグラム均一化)を適用し、コントラストを強調します。
cv::Mat gray, claheImg;
cv::cvtColor(colorImg, gray, cv::COLOR_BGR2GRAY);
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();
clahe->setClipLimit(2.0);
clahe->apply(gray, claheImg);
- 原因4:二値化パラメータの不適合
固定しきい値やOtsu法が適切に機能していない。
対処法:自適応しきい値(二値化)を試し、局所的なしきい値で文字を抽出します。
これらのトラブルは開発段階でよく遭遇しますが、原因を正確に特定し適切に対処することで、手書き文字認識システムの信頼性と精度を大きく向上させられます。
拡張アイデア
手書き文字認識システムの性能向上や実用性拡大を目指すために、従来のHOG+SVM手法に加え、さまざまな拡張技術を取り入れることが効果的です。
ここでは、ディープラーニングとのハイブリッド、マルチクラス分類の拡張、そしてモデル蒸留による軽量化について解説します。
ディープラーニングとのハイブリッド
従来の特徴量抽出+SVM分類器に、ディープラーニングの強力な特徴抽出能力を組み合わせることで、認識精度の向上やロバスト性の強化が期待できます。
CNN特徴量の併用
畳み込みニューラルネットワーク(CNN)は画像の階層的な特徴を自動で学習し、高い識別性能を持ちます。
HOG特徴量とCNN特徴量を組み合わせる方法は以下のように進めます。
- CNNによる特徴抽出
事前学習済みのCNNモデル(例:ResNet、VGGなど)を用い、手書き文字画像の中間層の出力を特徴ベクトルとして抽出します。
これにより、文字の形状やパターンを深く捉えられます。
- HOG特徴量との結合
CNN特徴量とHOG特徴量を連結し、より豊富な情報を含む複合特徴ベクトルを作成します。
- SVMなどの分類器で学習
複合特徴量を入力としてSVMを学習させることで、従来のHOG単独よりも高精度な分類が可能になります。
このハイブリッド手法は、CNNの強力な表現力とHOGの局所的な勾配情報の両方を活かせるため、特にノイズや変形に強い認識が実現できます。
マルチクラス拡張
手書き文字認識では、多数の文字クラスを扱う必要があり、SVMのような二値分類器を多クラス対応させる工夫が必要です。
One-vs-All構成
One-vs-All(OvA)は、多クラス分類を複数の二値分類問題に分割する代表的な手法です。
- 仕組み
各クラスに対して、そのクラスを正例、他のすべてのクラスを負例とするSVMを学習します。
例えば、10クラスなら10個のSVMモデルを作成します。
- 推論時の判定
入力特徴量を全てのSVMに入力し、各モデルの出力スコアを比較して最も高いスコアを出したクラスを予測結果とします。
- 利点
実装が比較的簡単で、各クラスの識別性能を個別に最適化可能です。
- 注意点
クラス間の不均衡やスコアのスケール差に注意し、適切なスコア正規化や閾値設定が必要です。
OpenCVのcv::ml::SVM
は単一モデルで多クラス対応しますが、OvA構成を明示的に実装することで柔軟な制御が可能です。
モデル蒸留で軽量化
モデル蒸留(Model Distillation)は、大規模で高性能なモデル(教師モデル)から、小型で高速なモデル(生徒モデル)へ知識を転移させる技術です。
手書き文字認識においても、軽量化と高速推論を両立するために有効です。
- 手順
- 高精度な大規模モデル(例:深層CNN)を教師モデルとして学習。
- 教師モデルの出力(ソフトラベルや中間特徴)を用いて、小型モデルを学習。
- 小型モデルは教師モデルの知識を模倣しつつ、パラメータ数や計算量を削減。
- 効果
- 推論速度の向上
- メモリ使用量の削減
- 実機や組み込み環境での運用が容易に
- 適用例
HOG+SVMの代わりに、小型CNNを生徒モデルとして用い、教師モデルの出力を参考に学習させることで、軽量かつ高精度な認識器を構築できます。
モデル蒸留は、特にリソース制約のある環境での手書き文字認識システムにおいて、実用的なアプローチです。
これらの拡張アイデアを取り入れることで、従来のHOG+SVM手法の限界を超え、より高精度で効率的な手書き文字認識システムの構築が可能になります。
まとめ
この記事では、C++とOpenCVを用いた手書き文字認識の基本から応用までを解説しました。
前処理で画像を整え、HOG特徴量を抽出し、SVMで分類する流れを詳述。
データ準備や評価指標、性能最適化、システム統合のポイントも紹介しています。
さらに、ディープラーニングとのハイブリッドやモデル蒸留などの拡張手法も触れ、実用的かつ高精度な認識システム構築のための知識が得られます。