【C++】OpenCV×Tesseractで作るリアルタイム自動車ナンバープレート認識入門
C++とOpenCVを組み合わせれば、画像を前処理で強調し輪郭抽出でプレート領域を特定し、Tesseractなどに渡して文字を読み取りナンバーを取得できます。
リアルタイム処理でも高速で、カメラ監視や駐車場管理へすぐ応用可能です。
システム全体の処理フロー
自動車ナンバープレート認識システムは、複数の処理ステップを連携させて動作します。
ここでは、リアルタイムで映像からナンバープレートを検出し、文字認識まで行う一連の流れを解説します。
フレームキャプチャ
リアルタイム認識の第一歩は、カメラや動画ファイルから映像フレームを取得することです。
OpenCVのVideoCapture
クラスを使うと、簡単にカメラ映像や動画ファイルを読み込めます。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::VideoCapture cap(0); // カメラデバイス0を開く
if (!cap.isOpened()) {
std::cerr << "カメラを開けませんでした" << std::endl;
return -1;
}
cv::Mat frame;
while (true) {
cap >> frame; // フレームを取得
if (frame.empty()) break;
cv::imshow("Camera Frame", frame);
if (cv::waitKey(30) >= 0) break; // キー入力で終了
}
return 0;
}
このコードは、PCに接続されたカメラから映像を取得し、ウィンドウに表示します。
cap >> frame;
でフレームを1枚ずつ読み込み、imshow
で表示しています。
リアルタイム処理では、このフレームを次の処理に渡していきます。
画像前処理
取得したフレームはそのままだとノイズや照明の影響を受けやすいため、認識精度を上げるために前処理を行います。
主な処理は以下の通りです。
- グレースケール変換
カラー情報は文字認識には不要なので、cvtColor
でグレースケール画像に変換します。
- ノイズ除去
ガウシアンブラーやビラテラルフィルタでノイズを減らし、輪郭検出の精度を上げます。
- コントラスト強調
CLAHE(適応的ヒストグラム平坦化)を使うと、局所的にコントラストを強調でき、文字の輪郭がはっきりします。
#include <opencv2/opencv.hpp>
cv::Mat preprocessImage(const cv::Mat& src) {
cv::Mat gray, blurred, claheImg;
// グレースケール変換
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
// ノイズ除去(ガウシアンブラー)
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0);
// CLAHEでコントラスト強調
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();
clahe->setClipLimit(2.0);
clahe->apply(blurred, claheImg);
return claheImg;
}
この関数は入力画像を受け取り、前処理済みのグレースケール画像を返します。
ノイズが減り、文字の輪郭が強調されるため、後続の輪郭検出やOCRの精度が向上します。
ナンバープレート候補検出
前処理した画像からナンバープレートの候補領域を検出します。
日本のナンバープレートは長方形で、文字が密集している特徴があります。
主な手法は以下です。
- エッジ検出
Cannyエッジ検出で輪郭を抽出します。
- 輪郭検出
findContours
で画像中の輪郭を取得し、面積やアスペクト比でナンバープレートらしい矩形を絞り込みます。
- 形状判定
輪郭を多角形近似し、四角形かどうかを判定します。
#include <opencv2/opencv.hpp>
#include <vector>
std::vector<cv::Rect> detectPlateCandidates(const cv::Mat& preprocessed) {
std::vector<cv::Rect> candidates;
cv::Mat edges;
cv::Canny(preprocessed, edges, 100, 200);
std::vector<std::vector<cv::Point>> contours;
cv::findContours(edges, contours, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
for (const auto& contour : contours) {
double area = cv::contourArea(contour);
if (area < 1000) continue; // 小さい輪郭は除外
std::vector<cv::Point> approx;
cv::approxPolyDP(contour, approx, 0.02 * cv::arcLength(contour, true), true);
if (approx.size() == 4 && cv::isContourConvex(approx)) {
cv::Rect rect = cv::boundingRect(approx);
float aspectRatio = (float)rect.width / rect.height;
if (aspectRatio > 2 && aspectRatio < 5) { // ナンバープレートの比率に近い
candidates.push_back(rect);
}
}
}
return candidates;
}
この関数は前処理画像を受け取り、ナンバープレート候補の矩形領域を返します。
輪郭の面積や四角形判定、アスペクト比で絞り込むことで、誤検出を減らせます。
文字領域の抽出
検出したナンバープレート候補から文字領域を切り出します。
ナンバープレートは平面であるため、透視変換で歪みを補正し、文字が水平に並ぶように整えます。
#include <opencv2/opencv.hpp>
cv::Mat warpPlate(const cv::Mat& src, const cv::Rect& plateRect) {
cv::Mat plate = src(plateRect).clone();
// ここでは簡単に切り出しのみ。透視変換は輪郭の4点座標が必要
// 実際はapproxPolyDPで得た4点を使い、getPerspectiveTransformで変換行列を作成します
return plate;
}
透視変換を行う場合は、輪郭の4点座標を使ってgetPerspectiveTransform
とwarpPerspective
を利用します。
これにより、ナンバープレートの傾きや歪みを補正し、OCRの認識率を高めます。
文字領域の分割は、水平投影やconnectedComponentsを使って文字ごとに分割する方法もありますが、Tesseractは単一の画像から文字認識できるため、基本的には切り出したナンバープレート画像をそのままOCRに渡します。
OCRによる文字認識
切り出したナンバープレート画像に対して、Tesseract OCRを使って文字認識を行います。
Tesseractは日本語やひらがなを含む文字セットに対応した学習データを用意することで、日本のナンバープレートの文字も認識可能です。
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include <opencv2/opencv.hpp>
#include <iostream>
std::string recognizeText(const cv::Mat& plateImg) {
tesseract::TessBaseAPI tess;
if (tess.Init(NULL, "jpn", tesseract::OEM_LSTM_ONLY)) {
std::cerr << "Tesseractの初期化に失敗しました" << std::endl;
return "";
}
tess.SetPageSegMode(tesseract::PSM_SINGLE_BLOCK);
tess.SetImage(plateImg.data, plateImg.cols, plateImg.rows, plateImg.channels(), plateImg.step);
char* outText = tess.GetUTF8Text();
std::string result(outText);
delete[] outText;
return result;
}
int main() {
cv::Mat plateImg = cv::imread("plate_sample.jpg", cv::IMREAD_GRAYSCALE);
if (plateImg.empty()) {
std::cerr << "画像が読み込めません" << std::endl;
return -1;
}
std::string text = recognizeText(plateImg);
std::cout << "認識結果: " << text << std::endl;
return 0;
}
このコードは、Tesseractの日本語モデルを使って画像から文字列を抽出します。
PSM_SINGLE_BLOCK
は文字が一つのブロックにまとまっている場合に適したモードです。
認識結果はUTF-8文字列として取得できます。
結果の整形と出力
OCRの結果はノイズや誤認識が含まれることが多いため、正規表現や文字列操作で整形します。
日本のナンバープレートは特定のフォーマットがあるため、以下のような処理を行います。
- 全角・半角の統一
- ハイフンやスペースの補完
- 県名や分類番号の正規表現チェック
- 不正な文字の除去
#include <regex>
#include <iostream>
std::string normalizePlateText(const std::string& rawText) {
// 全角数字を半角に変換(簡易例)
std::string normalized;
for (char c : rawText) {
if (c >= '0' && c <= '9') {
normalized += c - '0' + '0';
} else {
normalized += c;
}
}
// 正規表現でナンバープレート形式にマッチする部分を抽出
std::regex platePattern(R"((\d{2,3})\s*([あ-ん])\s*(\d{4}))");
std::smatch match;
if (std::regex_search(normalized, match, platePattern)) {
return match.str(0);
}
return normalized;
}
int main() {
std::string ocrResult = "123 あ 4567";
std::string cleaned = normalizePlateText(ocrResult);
std::cout << "整形後の文字列: " << cleaned << std::endl;
return 0;
}
整形後の文字列: 123 あ 4567
このように、OCR結果を正規表現で検証し、ナンバープレートの形式に合致する文字列を抽出します。
これにより誤認識の影響を減らし、システムの信頼性を高められます。
以上が、C++とOpenCV、Tesseractを使ったリアルタイム自動車ナンバープレート認識システムの全体的な処理フローです。
各ステップを組み合わせて実装することで、実用的な認識システムを構築できます。
画像前処理
カラースペース変換
グレースケール化
カラー画像はRGBやBGRの3チャンネルで構成されていますが、ナンバープレート認識では色の情報よりも明暗の差が重要です。
そこで、まずはカラー画像をグレースケール画像に変換します。
OpenCVではcvtColor
関数を使い、COLOR_BGR2GRAY
を指定するだけで簡単に変換できます。
#include <opencv2/opencv.hpp>
cv::Mat convertToGray(const cv::Mat& src) {
cv::Mat gray;
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
return gray;
}
int main() {
cv::Mat img = cv::imread("car.jpg");
if (img.empty()) return -1;
cv::Mat grayImg = convertToGray(img);
cv::imshow("Gray Image", grayImg);
cv::waitKey(0);
return 0;
}
グレースケール化により、画像の輝度情報だけを扱うため処理が軽くなり、輪郭検出や二値化の精度が向上します。
HSV・YCrCbの活用
ナンバープレートの色特徴を活かす場合は、HSVやYCrCbといったカラースペースを利用します。
特に日本のナンバープレートは白地に黒文字や緑地に白文字など色のパターンが決まっているため、色空間を変換して特定の色成分を抽出すると検出精度が上がります。
HSVでは色相(Hue)、彩度(Saturation)、明度(Value)に分かれ、色相を使って特定の色範囲を抽出しやすいです。
YCrCbは輝度(Y)と色差成分(Cr, Cb)に分かれ、輝度と色差を分離できるため、照明変化に強い特徴抽出が可能です。
#include <opencv2/opencv.hpp>
cv::Mat extractColorMask(const cv::Mat& src) {
cv::Mat hsv, mask;
cv::cvtColor(src, hsv, cv::COLOR_BGR2HSV);
// 白色の範囲をHSVで指定(例)
cv::Scalar lowerWhite(0, 0, 200);
cv::Scalar upperWhite(180, 30, 255);
cv::inRange(hsv, lowerWhite, upperWhite, mask);
return mask;
}
int main() {
cv::Mat img = cv::imread("car.jpg");
if (img.empty()) return -1;
cv::Mat whiteMask = extractColorMask(img);
cv::imshow("White Mask", whiteMask);
cv::waitKey(0);
return 0;
}
このように色空間を変換して特定色のマスクを作ることで、ナンバープレートの候補領域を絞り込みやすくなります。
ノイズ除去
GaussianBlur
画像に含まれるノイズは輪郭検出や文字認識の妨げになるため、ぼかし処理でノイズを減らします。
GaussianBlur
はガウシアンカーネルを使った平滑化で、ノイズを抑えつつエッジを比較的保つ特徴があります。
#include <opencv2/opencv.hpp>
cv::Mat applyGaussianBlur(const cv::Mat& src) {
cv::Mat blurred;
cv::GaussianBlur(src, blurred, cv::Size(5, 5), 0);
return blurred;
}
int main() {
cv::Mat img = cv::imread("plate_gray.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) return -1;
cv::Mat blurredImg = applyGaussianBlur(img);
cv::imshow("Gaussian Blur", blurredImg);
cv::waitKey(0);
return 0;
}
カーネルサイズはSize(5,5)
がよく使われますが、画像の解像度やノイズ量に応じて調整してください。
BilateralFilter
BilateralFilter
はエッジを保持しながらノイズを除去できるフィルタです。
ガウシアンブラーよりもエッジのぼやけを抑えたい場合に有効です。
特に文字の輪郭を鮮明に保ちたいナンバープレート認識に適しています。
#include <opencv2/opencv.hpp>
cv::Mat applyBilateralFilter(const cv::Mat& src) {
cv::Mat filtered;
cv::bilateralFilter(src, filtered, 9, 75, 75);
return filtered;
}
int main() {
cv::Mat img = cv::imread("plate_gray.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) return -1;
cv::Mat filteredImg = applyBilateralFilter(img);
cv::imshow("Bilateral Filter", filteredImg);
cv::waitKey(0);
return 0;
}
パラメータは順に、フィルタの直径、色空間のシグマ、座標空間のシグマです。
これらを調整して最適なノイズ除去効果を得られます。
コントラスト強調
CLAHE
CLAHE(Contrast Limited Adaptive Histogram Equalization)は、画像を小さな領域に分割して局所的にヒストグラム平坦化を行い、コントラストを強調します。
これにより、照明ムラがある画像でも文字の輪郭がはっきりし、認識精度が向上します。
#include <opencv2/opencv.hpp>
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("plate_gray.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) return -1;
cv::Mat claheImg = applyCLAHE(img);
cv::imshow("CLAHE", claheImg);
cv::waitKey(0);
return 0;
}
setClipLimit
はコントラストの強調度合いを調整するパラメータで、値が大きいほど強調されます。
適切な値を選ぶことで、文字の視認性を高められます。
これらの画像前処理を組み合わせることで、ナンバープレート検出や文字認識の精度を大きく向上させられます。
特にグレースケール化とノイズ除去、CLAHEによるコントラスト強調は基本的かつ効果的な処理です。
ナンバープレート候補検出
エッジ検出
Sobelフィルタ
Sobelフィルタは画像の勾配を計算し、エッジを強調するための基本的な手法です。
ナンバープレートの文字や枠はエッジがはっきりしているため、Sobelフィルタで縦横のエッジを抽出しやすくなります。
#include <opencv2/opencv.hpp>
cv::Mat applySobel(const cv::Mat& src) {
cv::Mat gradX, gradY, absGradX, absGradY, grad;
// X方向の勾配
cv::Sobel(src, gradX, CV_16S, 1, 0, 3);
cv::convertScaleAbs(gradX, absGradX);
// Y方向の勾配
cv::Sobel(src, gradY, CV_16S, 0, 1, 3);
cv::convertScaleAbs(gradY, absGradY);
// X,Yの勾配を合成
cv::addWeighted(absGradX, 0.5, absGradY, 0.5, 0, grad);
return grad;
}
int main() {
cv::Mat img = cv::imread("plate_gray.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) return -1;
cv::Mat sobelImg = applySobel(img);
cv::imshow("Sobel Edge", sobelImg);
cv::waitKey(0);
return 0;
}
Sobelフィルタはエッジの方向を検出できるため、ナンバープレートの縦横の線を強調し、輪郭検出の前処理として有効です。
Canny閾値最適化
Cannyエッジ検出はノイズに強く、エッジの細かい部分まで検出できるため、ナンバープレート検出でよく使われます。
閾値の設定が重要で、低すぎるとノイズが多く、高すぎるとエッジが欠落します。
#include <opencv2/opencv.hpp>
cv::Mat applyCanny(const cv::Mat& src, double lowThresh, double highThresh) {
cv::Mat edges;
cv::Canny(src, edges, lowThresh, highThresh);
return edges;
}
int main() {
cv::Mat img = cv::imread("plate_gray.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) return -1;
double lowThreshold = 50;
double highThreshold = 150;
cv::Mat edges = applyCanny(img, lowThreshold, highThreshold);
cv::imshow("Canny Edges", edges);
cv::waitKey(0);
return 0;
}
閾値は画像の明暗やノイズに応じて調整が必要です。
自動調整する場合はヒストグラム解析やOtsuの二値化を組み合わせることもあります。
輪郭抽出
findContoursモード選択
OpenCVのfindContours
関数は輪郭を抽出しますが、モードによって検出される輪郭の種類が異なります。
ナンバープレート検出では、RETR_TREE
やRETR_EXTERNAL
がよく使われます。
RETR_EXTERNAL
:最外輪郭のみ抽出。背景のノイズを減らしたい場合に有効RETR_TREE
:輪郭の階層構造を取得。複雑な形状の解析に適しています
#include <opencv2/opencv.hpp>
#include <vector>
std::vector<std::vector<cv::Point>> extractContours(const cv::Mat& edgeImg) {
std::vector<std::vector<cv::Point>> contours;
cv::findContours(edgeImg, contours, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
return contours;
}
int main() {
cv::Mat img = cv::imread("plate_edges.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) return -1;
auto contours = extractContours(img);
cv::Mat output = cv::Mat::zeros(img.size(), CV_8UC3);
for (const auto& contour : contours) {
cv::drawContours(output, std::vector<std::vector<cv::Point>>{contour}, -1, cv::Scalar(0, 255, 0), 2);
}
cv::imshow("Contours", output);
cv::waitKey(0);
return 0;
}
輪郭の階層情報を活用すると、ナンバープレートの内側の文字輪郭と外枠を区別しやすくなります。
面積・アスペクト比フィルタ
検出した輪郭はすべてナンバープレートとは限らないため、面積やアスペクト比でフィルタリングします。
日本のナンバープレートは横長の長方形で、アスペクト比はおおよそ2〜5の範囲に収まります。
#include <opencv2/opencv.hpp>
#include <vector>
std::vector<cv::Rect> filterContours(const std::vector<std::vector<cv::Point>>& contours) {
std::vector<cv::Rect> candidates;
for (const auto& contour : contours) {
double area = cv::contourArea(contour);
if (area < 1000) continue; // 小さい輪郭は除外
cv::Rect rect = cv::boundingRect(contour);
float aspectRatio = static_cast<float>(rect.width) / rect.height;
if (aspectRatio > 2.0 && aspectRatio < 5.0) {
candidates.push_back(rect);
}
}
return candidates;
}
このフィルタリングで、ナンバープレートらしい矩形領域を絞り込みます。
形状判定
四角形近似と頂点検証
輪郭を多角形近似し、頂点数が4つの四角形かどうかを判定します。
ナンバープレートは四角形であるため、この判定で誤検出を減らせます。
#include <opencv2/opencv.hpp>
#include <vector>
bool isRectangle(const std::vector<cv::Point>& contour) {
std::vector<cv::Point> approx;
double epsilon = 0.02 * cv::arcLength(contour, true);
cv::approxPolyDP(contour, approx, epsilon, true);
return (approx.size() == 4 && cv::isContourConvex(approx));
}
四角形であっても、凸であることを確認し、形状の妥当性をチェックします。
外接矩形の台形補正
ナンバープレートはカメラの角度によって台形に歪むことがあります。
検出した四角形の4点を使い、透視変換で正しい長方形に補正します。
#include <opencv2/opencv.hpp>
#include <vector>
cv::Mat correctPerspective(const cv::Mat& src, const std::vector<cv::Point>& quad) {
// 4点を順序付け(左上、右上、右下、左下)
std::vector<cv::Point2f> srcPts(4);
for (int i = 0; i < 4; ++i) {
srcPts[i] = quad[i];
}
// 出力画像のサイズを計算
float widthA = cv::norm(srcPts[2] - srcPts[3]);
float widthB = cv::norm(srcPts[1] - srcPts[0]);
float maxWidth = std::max(widthA, widthB);
float heightA = cv::norm(srcPts[1] - srcPts[2]);
float heightB = cv::norm(srcPts[0] - srcPts[3]);
float maxHeight = std::max(heightA, heightB);
std::vector<cv::Point2f> dstPts = {
cv::Point2f(0, 0),
cv::Point2f(maxWidth - 1, 0),
cv::Point2f(maxWidth - 1, maxHeight - 1),
cv::Point2f(0, maxHeight - 1)
};
cv::Mat M = cv::getPerspectiveTransform(srcPts, dstPts);
cv::Mat warped;
cv::warpPerspective(src, warped, M, cv::Size(maxWidth, maxHeight));
return warped;
}
この補正により、文字認識の精度が向上します。
カラー特徴活用
白地緑字・緑地白字対応
日本のナンバープレートは白地に黒文字だけでなく、緑地に白文字のものもあります。
色の特徴を活かして検出精度を上げるため、HSVやYCrCb空間で色範囲を指定してマスクを作成します。
#include <opencv2/opencv.hpp>
cv::Mat createGreenPlateMask(const cv::Mat& src) {
cv::Mat hsv;
cv::cvtColor(src, hsv, cv::COLOR_BGR2HSV);
// 緑色の範囲(例)
cv::Scalar lowerGreen(40, 40, 40);
cv::Scalar upperGreen(80, 255, 255);
cv::Mat mask;
cv::inRange(hsv, lowerGreen, upperGreen, mask);
return mask;
}
このマスクを使い、緑地のナンバープレート候補を抽出できます。
adaptiveThreshold
照明条件が不均一な場合、固定閾値の二値化はうまくいきません。
adaptiveThreshold
を使うと、局所的に閾値を計算して二値化できるため、文字の輪郭がはっきりします。
#include <opencv2/opencv.hpp>
cv::Mat applyAdaptiveThreshold(const cv::Mat& src) {
cv::Mat binarized;
cv::adaptiveThreshold(src, binarized, 255,
cv::ADAPTIVE_THRESH_GAUSSIAN_C,
cv::THRESH_BINARY_INV, 11, 2);
return binarized;
}
この処理は文字領域の抽出や輪郭検出の前に行うと効果的です。
ディープラーニング補完
YOLOv5カスタム学習
従来の画像処理だけでなく、YOLOv5などの物体検出モデルを使うとナンバープレート検出の精度と速度が向上します。
カスタムデータセットで日本のナンバープレートを学習させることで、複雑な背景や角度変化にも強くなります。
学習済みモデルを使う場合は、OpenCVのdnn
モジュールやPyTorchのC++ APIで推論を行います。
// YOLOv5推論はコードが長いため割愛しますが、
// モデルの読み込み、前処理、推論、後処理の流れで実装します。
Haar Cascade比較
Haar Cascadeは古典的な物体検出手法で、軽量かつリアルタイム処理に向いています。
ナンバープレート用のカスケード分類器を用意すれば、簡単に候補領域を検出可能です。
ただし、精度はディープラーニングに劣る場合があります。
#include <opencv2/opencv.hpp>
int main() {
cv::CascadeClassifier plateCascade;
if (!plateCascade.load("haarcascade_russian_plate_number.xml")) {
std::cerr << "カスケードファイルの読み込みに失敗しました" << std::endl;
return -1;
}
cv::Mat img = cv::imread("car.jpg");
if (img.empty()) return -1;
std::vector<cv::Rect> plates;
plateCascade.detectMultiScale(img, plates, 1.1, 3);
for (const auto& rect : plates) {
cv::rectangle(img, rect, cv::Scalar(0, 255, 0), 2);
}
cv::imshow("Detected Plates", img);
cv::waitKey(0);
return 0;
}
Haar Cascadeは学習済みモデルが公開されているため、すぐに試せる利点があります。
これらの手法を組み合わせてナンバープレート候補を検出し、後続の文字認識処理へとつなげます。
画像処理による輪郭検出とディープラーニングによる物体検出を適宜使い分けることで、精度と速度のバランスを取れます。
透視変換と切り出し
ナンバープレート検出後、カメラの角度や位置によって歪んだナンバープレート領域を正しい長方形に補正するために透視変換を行います。
これにより、文字認識の精度が大幅に向上します。
getPerspectiveTransform
getPerspectiveTransform
は、入力画像の4点と出力画像の4点を対応付けて、透視変換行列を計算する関数です。
ナンバープレートの4つの頂点座標を正確に取得し、変換後の長方形の座標と対応させることで、歪みを補正します。
#include <opencv2/opencv.hpp>
#include <vector>
cv::Mat getTransformMatrix(const std::vector<cv::Point2f>& srcPoints, const std::vector<cv::Point2f>& dstPoints) {
return cv::getPerspectiveTransform(srcPoints, dstPoints);
}
int main() {
// 例:歪んだナンバープレートの4点(左上、右上、右下、左下)
std::vector<cv::Point2f> srcPoints = {
cv::Point2f(100, 200),
cv::Point2f(400, 180),
cv::Point2f(420, 300),
cv::Point2f(120, 320)
};
// 変換後の長方形の4点(幅400、高さ120)
std::vector<cv::Point2f> dstPoints = {
cv::Point2f(0, 0),
cv::Point2f(400, 0),
cv::Point2f(400, 120),
cv::Point2f(0, 120)
};
cv::Mat M = getTransformMatrix(srcPoints, dstPoints);
std::cout << "透視変換行列:\n" << M << std::endl;
return 0;
}
この行列M
を使って、warpPerspective
で画像を変換します。
4点の順序は左上、右上、右下、左下の順に揃えることが重要です。
warpPerspective解像度
warpPerspective
は透視変換行列を用いて画像を変換しますが、出力画像の解像度(サイズ)を適切に設定することが重要です。
解像度が低すぎると文字が潰れて認識しづらくなり、高すぎると処理負荷が増えます。
#include <opencv2/opencv.hpp>
cv::Mat warpPlateImage(const cv::Mat& src, const cv::Mat& M, int width, int height) {
cv::Mat warped;
cv::warpPerspective(src, warped, M, cv::Size(width, height));
return warped;
}
int main() {
cv::Mat src = cv::imread("car.jpg");
if (src.empty()) return -1;
std::vector<cv::Point2f> srcPoints = {
cv::Point2f(100, 200),
cv::Point2f(400, 180),
cv::Point2f(420, 300),
cv::Point2f(120, 320)
};
int width = 400;
int height = 120;
std::vector<cv::Point2f> dstPoints = {
cv::Point2f(0, 0),
cv::Point2f(width, 0),
cv::Point2f(width, height),
cv::Point2f(0, height)
};
cv::Mat M = cv::getPerspectiveTransform(srcPoints, dstPoints);
cv::Mat warped = warpPlateImage(src, M, width, height);
cv::imshow("Warped Plate", warped);
cv::waitKey(0);
return 0;
}
出力サイズはナンバープレートの実際の縦横比に合わせるのが望ましく、一般的に幅は高さの約3倍程度が多いです。
解像度を適切に設定することで、OCRの認識率が向上します。
歪み補正精度
透視変換の精度は、入力の4点座標の正確さに大きく依存します。
頂点の検出がずれると、変換後の画像が歪んだり文字が斜めになったりして認識精度が落ちます。
- 頂点検出の工夫
輪郭近似で得た4点をサブピクセル精度で補正したり、コーナー検出アルゴリズム(HarrisコーナーやShi-Tomasi)を併用して精度を上げます。
- 座標の順序統一
4点の順序が不正確だと変換結果が乱れるため、左上、右上、右下、左下の順に必ず並べ替えます。
座標の重心を基準に角度を計算してソートする方法が一般的です。
- 変換後の画像品質
warpPerspective
の補間方法INTER_LINEAR
やINTER_CUBIC
を適切に選ぶことで、文字の輪郭が滑らかになり認識しやすくなります。
cv::Mat warpPlateImageHighQuality(const cv::Mat& src, const cv::Mat& M, int width, int height) {
cv::Mat warped;
cv::warpPerspective(src, warped, M, cv::Size(width, height), cv::INTER_CUBIC);
return warped;
}
このように補間方法をINTER_CUBIC
にすると、特に拡大時に画像が滑らかになります。
透視変換と切り出しは、ナンバープレート認識の中でも非常に重要な処理です。
正確な頂点検出と適切な解像度設定、補間方法の選択で、OCRの認識精度を大きく改善できます。
文字領域の分割
ナンバープレートから文字認識を行う前に、文字ごとに領域を分割することが重要です。
ここでは、水平・垂直投影ヒストグラムやconnectedComponentsを用いた文字領域の抽出方法、さらに日本のナンバープレート特有の分類番号やひらがな、番号の位置推定について解説します。
水平垂直投影ヒストグラム
投影ヒストグラムは、画像の各行または各列の画素値の合計を計算し、文字の位置を推定する手法です。
二値化画像に対して用いることで、文字の存在する領域を明確に把握できます。
- 水平投影ヒストグラム
画像の各行の白(または黒)画素数をカウントし、文字の上下の境界を検出します。
ナンバープレートの文字は横一列に並ぶため、文字領域の上下を特定するのに有効です。
- 垂直投影ヒストグラム
画像の各列の画素数をカウントし、文字の左右の境界を検出します。
これにより、文字ごとの区切りを推定できます。
#include <opencv2/opencv.hpp>
#include <vector>
#include <iostream>
void showProjectionHistograms(const cv::Mat& binaryImg) {
int rows = binaryImg.rows;
int cols = binaryImg.cols;
// 水平投影ヒストグラム
std::vector<int> horizontalHist(rows, 0);
for (int y = 0; y < rows; ++y) {
int count = 0;
for (int x = 0; x < cols; ++x) {
if (binaryImg.at<uchar>(y, x) > 0) count++;
}
horizontalHist[y] = count;
}
// 垂直投影ヒストグラム
std::vector<int> verticalHist(cols, 0);
for (int x = 0; x < cols; ++x) {
int count = 0;
for (int y = 0; y < rows; ++y) {
if (binaryImg.at<uchar>(y, x) > 0) count++;
}
verticalHist[x] = count;
}
// ヒストグラムの簡易表示
std::cout << "水平投影ヒストグラム(上位10行):" << std::endl;
for (int i = 0; i < 10 && i < rows; ++i) {
std::cout << "Row " << i << ": " << horizontalHist[i] << std::endl;
}
std::cout << "垂直投影ヒストグラム(左端10列):" << std::endl;
for (int i = 0; i < 10 && i < cols; ++i) {
std::cout << "Col " << i << ": " << verticalHist[i] << std::endl;
}
}
int main() {
cv::Mat plateImg = cv::imread("plate_binary.jpg", cv::IMREAD_GRAYSCALE);
if (plateImg.empty()) {
std::cerr << "画像が読み込めません" << std::endl;
return -1;
}
showProjectionHistograms(plateImg);
return 0;
}
水平投影で文字の上下境界を特定し、垂直投影で文字の左右境界を検出することで、文字領域の矩形を推定できます。
ピークと谷の位置を解析して文字の分割点を決めるのが一般的です。
connectedComponents
connectedComponents
は、二値画像の連結領域をラベリングし、個々の文字領域を抽出する手法です。
投影ヒストグラムよりも直接的に文字の領域を得られるため、複雑な背景や文字の重なりが少ない場合に有効です。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat binaryImg = cv::imread("plate_binary.jpg", cv::IMREAD_GRAYSCALE);
if (binaryImg.empty()) {
std::cerr << "画像が読み込めません" << std::endl;
return -1;
}
cv::Mat labels, stats, centroids;
int nLabels = cv::connectedComponentsWithStats(binaryImg, labels, stats, centroids);
std::cout << "検出されたラベル数: " << nLabels - 1 << std::endl; // 背景を除く
cv::Mat output = cv::Mat::zeros(binaryImg.size(), CV_8UC3);
for (int i = 1; i < nLabels; ++i) { // 0は背景
int x = stats.at<int>(i, cv::CC_STAT_LEFT);
int y = stats.at<int>(i, cv::CC_STAT_TOP);
int w = stats.at<int>(i, cv::CC_STAT_WIDTH);
int h = stats.at<int>(i, cv::CC_STAT_HEIGHT);
cv::rectangle(output, cv::Rect(x, y, w, h), cv::Scalar(0, 255, 0), 2);
}
cv::imshow("Connected Components", output);
cv::waitKey(0);
return 0;
}
このコードは、文字ごとの領域を矩形で囲み表示します。
connectedComponentsWithStats
は各領域の位置やサイズも取得できるため、文字の切り出しに便利です。
日本の分類番号・仮名・番号位置推定
日本のナンバープレートは、分類番号(数字)、ひらがな、そして登録番号(数字)という独特の構成を持っています。
これらは位置的に決まったパターンで配置されているため、文字領域の分割後に位置情報を活用して分類できます。
- 分類番号
ナンバープレートの左上に位置し、2桁または3桁の数字で構成されます。
- ひらがな
分類番号の右隣に1文字だけ配置されます。
ひらがなはTesseractの日本語モデルで認識可能です。
- 登録番号
ナンバープレートの右側に4桁の数字が並びます。
これらの位置関係を利用して、文字領域を左右に分割し、さらに文字数や大きさで分類番号・ひらがな・登録番号を推定します。
#include <opencv2/opencv.hpp>
#include <vector>
#include <iostream>
struct CharacterRegion {
cv::Rect rect;
std::string type; // "classification", "kana", "number"
};
std::vector<CharacterRegion> estimateCharacterPositions(const std::vector<cv::Rect>& charRects, int plateWidth) {
std::vector<CharacterRegion> regions;
for (const auto& rect : charRects) {
float centerX = rect.x + rect.width / 2.0f;
if (centerX < plateWidth * 0.3) {
regions.push_back({rect, "classification"});
} else if (centerX < plateWidth * 0.5) {
regions.push_back({rect, "kana"});
} else {
regions.push_back({rect, "number"});
}
}
return regions;
}
int main() {
// 例としてconnectedComponentsで得た文字矩形を用意
std::vector<cv::Rect> charRects = {
cv::Rect(10, 20, 30, 50), // 分類番号
cv::Rect(50, 20, 25, 50), // ひらがな
cv::Rect(90, 20, 25, 50), // 登録番号1
cv::Rect(120, 20, 25, 50), // 登録番号2
cv::Rect(150, 20, 25, 50), // 登録番号3
cv::Rect(180, 20, 25, 50) // 登録番号4
};
int plateWidth = 220;
auto regions = estimateCharacterPositions(charRects, plateWidth);
for (const auto& region : regions) {
std::cout << "領域: " << region.rect << " 種類: " << region.type << std::endl;
}
return 0;
}
このように文字の位置を基準に分類することで、OCR結果の後処理で文字種別ごとに適切な処理を行いやすくなります。
例えば、ひらがなは日本語モデルで認識し、数字は数字専用の処理を行うなどの工夫が可能です。
文字領域の分割はナンバープレート認識の精度に直結する重要な工程です。
投影ヒストグラムやconnectedComponentsを組み合わせ、日本のナンバープレート特有の文字配置を考慮した位置推定を行うことで、より正確な文字認識が実現できます。
OCRによる文字認識
Tesseract設定
Page Segmentation Mode
Tesseract OCRでは、画像内の文字の配置や構造に応じて文字認識の挙動を制御するために、Page Segmentation Mode(PSM)を設定します。
PSMは文字のブロックや行、単語の検出方法を指定し、認識精度に大きく影響します。
主なPSMの例は以下の通りです。
PSM番号 | 説明 |
---|---|
3 | 単一の完全なページ(デフォルト) |
6 | 単一の均一なテキストブロック |
7 | 単一のテキスト行 |
8 | 単一の単語 |
10 | 単一の文字 |
ナンバープレート認識では、文字が横一列に並んでいるため、PSM_SINGLE_LINE
(6または7)がよく使われます。
これにより、Tesseractは画像全体を一つのテキスト行として処理し、文字の切れ目を適切に判断します。
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat plateImg = cv::imread("plate_warped.jpg", cv::IMREAD_GRAYSCALE);
if (plateImg.empty()) {
std::cerr << "画像が読み込めません" << std::endl;
return -1;
}
tesseract::TessBaseAPI tess;
if (tess.Init(NULL, "jpn", tesseract::OEM_LSTM_ONLY)) {
std::cerr << "Tesseractの初期化に失敗しました" << std::endl;
return -1;
}
tess.SetPageSegMode(tesseract::PSM_SINGLE_LINE); // 単一行モード
tess.SetImage(plateImg.data, plateImg.cols, plateImg.rows, plateImg.channels(), plateImg.step);
char* outText = tess.GetUTF8Text();
std::cout << "認識結果: " << outText << std::endl;
delete[] outText;
tess.End();
return 0;
}
OCRエンジンモード
Tesseractは複数のOCRエンジンモード(OEM)を提供しており、認識アルゴリズムの選択が可能です。
OEM番号 | 説明 |
---|---|
0 | 旧Tesseractエンジンのみ使用 |
1 | LSTMニューラルネットワークのみ使用 |
2 | 旧エンジンとLSTMの両方を使用 |
3 | デフォルト(通常はLSTM) |
日本語のような複雑な文字認識にはLSTMベースのニューラルネットワーク(OEM=1)が推奨されます。
これにより、ひらがなや漢字も高精度に認識できます。
tess.Init(NULL, "jpn", tesseract::OEM_LSTM_ONLY);
学習データカスタマイズ
日本語traineddata追加
Tesseractは言語ごとに学習済みデータ(traineddata)を使用します。
日本のナンバープレートに含まれるひらがなや漢字を正確に認識するためには、日本語のtraineddataをインストールし、OCRエンジンに指定する必要があります。
日本語traineddataは公式のTesseractリポジトリやhttps://github.com/tesseract-ocr/tessdataから入手可能です。
tessdata
フォルダに配置し、Init
関数の第2引数に"jpn"
を指定します。
# 例: tessdataフォルダにjpn.traineddataを配置
cp jpn.traineddata /usr/share/tesseract-ocr/4.00/tessdata/
ファインチューニング
標準のtraineddataだけでは、ナンバープレート特有の文字やフォントに対応しきれない場合があります。
ファインチューニング(追加学習)を行うことで、認識精度を向上させられます。
ファインチューニングは、ナンバープレートの文字画像と正解テキストを用意し、Tesseractのtraining
ツールでモデルを再学習します。
具体的には以下の手順です。
- ナンバープレート文字の画像とテキストファイルを作成
tesstrain
ツールで学習データを生成- 既存の日本語モデルに追加学習を実施
- 新しいtraineddataを生成し、OCRに適用
ファインチューニングにより、特定のフォントや文字配置に最適化されたモデルが作成でき、誤認識を減らせます。
不確定文字ハンドリング
信頼度スコア
Tesseractは認識した文字ごとに信頼度スコア(confidence)を返します。
信頼度が低い文字は誤認識の可能性が高いため、後処理で検出しやすくなります。
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat plateImg = cv::imread("plate_warped.jpg", cv::IMREAD_GRAYSCALE);
if (plateImg.empty()) {
std::cerr << "画像が読み込めません" << std::endl;
return -1;
}
tesseract::TessBaseAPI tess;
if (tess.Init(NULL, "jpn", tesseract::OEM_LSTM_ONLY)) {
std::cerr << "Tesseractの初期化に失敗しました" << std::endl;
return -1;
}
tess.SetPageSegMode(tesseract::PSM_SINGLE_LINE);
tess.SetImage(plateImg.data, plateImg.cols, plateImg.rows, plateImg.channels(), plateImg.step);
tess.Recognize(0);
tesseract::ResultIterator* ri = tess.GetIterator();
tesseract::PageIteratorLevel level = tesseract::RIL_SYMBOL;
if (ri != nullptr) {
do {
const char* symbol = ri->GetUTF8Text(level);
float conf = ri->Confidence(level);
if (symbol != nullptr) {
std::cout << "文字: " << symbol << " 信頼度: " << conf << std::endl;
delete[] symbol;
}
} while (ri->Next(level));
}
tess.End();
return 0;
}
信頼度が一定以下の文字は、再認識や手動補正の対象にできます。
再認識ロジック
信頼度の低い文字や不自然な文字列が検出された場合、再認識を試みるロジックを組み込むと精度が向上します。
具体的には以下の方法があります。
- 画像の再前処理
二値化やコントラスト調整を変えて再度OCRにかける。
- 文字領域の再分割
文字の切り出し位置を微調整して再認識。
- 複数OCRエンジンの併用
Tesseract以外のOCRエンジンと比較し、信頼度の高い結果を採用。
- 辞書や正規表現による補正
ナンバープレートの形式に合わない文字列を検出し、候補文字列を置換。
これらの再認識処理を組み合わせることで、誤認識を減らし、より正確な文字列を得られます。
Tesseractの設定や学習データのカスタマイズ、信頼度を活用した不確定文字のハンドリングを適切に行うことで、日本のナンバープレートに特化した高精度な文字認識が実現します。
結果の後処理
OCRで認識した文字列は、そのままでは誤認識や表記ゆれが含まれていることが多いため、ナンバープレートとして正確に扱うために後処理を行います。
ここでは文字列の正規化、ナンバープレート形式の検証、そして重複検出のフィルタリング方法について詳しく解説します。
文字列正規化
全角半角変換
日本語環境では全角文字と半角文字が混在しやすく、OCR結果にも全角数字や全角英字が含まれることがあります。
ナンバープレートの数字や英字は基本的に半角で統一するのが望ましいため、全角から半角への変換を行います。
#include <iostream>
#include <string>
// 全角数字・英字を半角に変換する簡易関数
std::string zenkakuToHankaku(const std::string& input) {
std::string output;
for (unsigned char c : input) {
// 全角数字(0xFF10〜0xFF19)を半角に変換
if (c >= 0xEF && c <= 0xEF) {
// UTF-8の全角数字は3バイトなのでここでは簡易対応
// 実際はUTF-8のマルチバイト処理が必要
output += c; // ここは実装例のため省略
} else if (c >= '0' && c <= '9') {
output += c - '0' + '0';
} else {
output += c;
}
}
return output;
}
int main() {
std::string rawText = "1234あ56789";
std::string normalized = zenkakuToHankaku(rawText);
std::cout << "変換前: " << rawText << std::endl;
std::cout << "変換後: " << normalized << std::endl;
return 0;
}
変換前: 1234あ56789
変換後: 1234あ56789
実際にはUTF-8のマルチバイト文字を正しく処理するライブラリ(例えばiconv
やBoost.Locale
)を使うことが推奨されます。
ハイフン補完
日本のナンバープレートには分類番号と登録番号の間にハイフン(-)が入ることが多いですが、OCRではハイフンが認識されなかったり、誤認識されることがあります。
後処理でハイフンを補完することで、ナンバープレートの形式を整えます。
#include <iostream>
#include <string>
#include <regex>
std::string insertHyphen(const std::string& input) {
// 例: 分類番号(2〜3桁) + ひらがな + 登録番号(4桁)
// ひらがなは1文字なので、その前後にハイフンを入れる例
std::regex pattern(R"((\d{2,3})([あ-ん])(\d{4}))");
std::string replaced = std::regex_replace(input, pattern, "$1-$2-$3");
return replaced;
}
int main() {
std::string rawText = "123あ4567";
std::string withHyphen = insertHyphen(rawText);
std::cout << "補完前: " << rawText << std::endl;
std::cout << "補完後: " << withHyphen << std::endl;
return 0;
}
補完前: 123あ4567
補完後: 123-あ-4567
このように正規表現を使って、ナンバープレートの典型的な構造に合わせてハイフンを補完します。
ナンバー形式検証
正規表現チェック
ナンバープレートの文字列が正しい形式かどうかを検証するために、正規表現を用います。
日本のナンバープレートは「分類番号(2〜3桁の数字)」「ひらがな1文字」「登録番号(4桁の数字)」というパターンが基本です。
#include <iostream>
#include <regex>
bool validatePlateFormat(const std::string& plate) {
std::regex pattern(R"(^\d{2,3}-[あ-ん]-\d{4}$)");
return std::regex_match(plate, pattern);
}
int main() {
std::string plate1 = "123-あ-4567";
std::string plate2 = "12-い-345";
std::cout << plate1 << " は " << (validatePlateFormat(plate1) ? "有効" : "無効") << "な形式です。" << std::endl;
std::cout << plate2 << " は " << (validatePlateFormat(plate2) ? "有効" : "無効") << "な形式です。" << std::endl;
return 0;
}
123-あ-4567 は 有効な形式です。
12-い-345 は 無効な形式です。
このチェックで形式に合わない文字列は誤認識の可能性が高いため、再認識や補正の対象にできます。
県名マッピング
ナンバープレートの上部には都道府県名が記載されていることが多く、OCRで認識した文字列と県名のマッピングを行うことで、認識結果の信頼性を高められます。
例えば、認識結果の文字列に近い県名を辞書から検索し、誤認識を補正します。
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
std::string correctPrefectureName(const std::string& recognized, const std::vector<std::string>& prefectures) {
// 簡易的に最も類似度の高い県名を返す(ここでは完全一致のみ)
for (const auto& pref : prefectures) {
if (recognized.find(pref) != std::string::npos) {
return pref;
}
}
return "不明";
}
int main() {
std::vector<std::string> prefectures = {"東京", "大阪", "北海道", "福岡", "愛知"};
std::string ocrResult = "東k京";
std::string corrected = correctPrefectureName(ocrResult, prefectures);
std::cout << "認識結果: " << ocrResult << " 補正後: " << corrected << std::endl;
return 0;
}
認識結果: 東k京 補正後: 東京
実際には文字列の類似度計算(レーベンシュタイン距離など)を用いて誤認識を補正する方法が効果的です。
重複プレートフィルタリング
Temporal Window
リアルタイム映像から連続して同じナンバープレートが認識される場合、同一車両の重複検出を防ぐために時間的なウィンドウを設けてフィルタリングします。
例えば、同じナンバープレートが数秒以内に複数回認識された場合は1回だけ記録します。
#include <iostream>
#include <unordered_map>
#include <chrono>
#include <string>
class PlateFilter {
std::unordered_map<std::string, std::chrono::steady_clock::time_point> lastSeen;
std::chrono::seconds window;
public:
PlateFilter(int seconds) : window(seconds) {}
bool isNewPlate(const std::string& plate) {
auto now = std::chrono::steady_clock::now();
if (lastSeen.find(plate) == lastSeen.end() || now - lastSeen[plate] > window) {
lastSeen[plate] = now;
return true;
}
return false;
}
};
int main() {
PlateFilter filter(5); // 5秒間隔
std::string plate = "123-あ-4567";
if (filter.isNewPlate(plate)) {
std::cout << "新しいナンバープレート: " << plate << std::endl;
} else {
std::cout << "重複ナンバープレート検出: " << plate << std::endl;
}
return 0;
}
移動体追跡併用
カメラ映像で複数の車両が連続して映る場合、ナンバープレートの重複検出を防ぐために物体追跡(トラッキング)と組み合わせる方法があります。
車両の動きを追跡し、同一車両のナンバープレート認識結果を紐付けることで、誤検出や重複登録を減らせます。
OpenCVのMultiTracker
やSORT
、Deep SORT
などのトラッキングアルゴリズムを利用し、ナンバープレート認識結果と追跡IDを関連付ける実装が一般的です。
これらの後処理を適切に組み合わせることで、OCRの誤認識を減らし、正確で信頼性の高いナンバープレート情報を得られます。
特に日本のナンバープレート特有の形式を考慮した正規化と検証は、実用的なシステム構築に欠かせません。
パフォーマンスチューニング
リアルタイムで自動車ナンバープレート認識を行うには、処理速度の最適化が不可欠です。
ここでは、マルチスレッド化やGPUアクセラレーション、メモリ管理の工夫によるパフォーマンス向上手法を詳しく解説します。
マルチスレッド対応
OpenMP
OpenMPはC++で簡単に並列処理を実装できるAPIで、ループ処理の並列化に適しています。
画像処理の中で複数フレームの前処理や輪郭検出など、独立した処理を並列化することで高速化が可能です。
#include <opencv2/opencv.hpp>
#include <omp.h>
#include <vector>
#include <iostream>
int main() {
std::vector<cv::Mat> frames(10);
// 例として同じ画像を複数用意
cv::Mat img = cv::imread("plate.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) return -1;
for (auto& f : frames) f = img.clone();
std::vector<cv::Mat> results(frames.size());
#pragma omp parallel for
for (int i = 0; i < (int)frames.size(); ++i) {
// 例えばガウシアンブラーを並列処理
cv::GaussianBlur(frames[i], results[i], cv::Size(5,5), 0);
std::cout << "Thread " << omp_get_thread_num() << " processed frame " << i << std::endl;
}
cv::imshow("Processed", results[0]);
cv::waitKey(0);
return 0;
}
OpenMPはコンパイラの対応が必要ですが、コードの修正が少なく済み、ループ単位での高速化に効果的です。
std::threadキュー
C++標準のstd::thread
を使い、処理タスクをキューに入れてワーカースレッドで並列処理する方法もあります。
フレームの取得、前処理、認識などのパイプライン処理を分割し、スレッド間でキューを介してデータを受け渡すことで効率的に処理できます。
#include <opencv2/opencv.hpp>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <iostream>
std::queue<cv::Mat> frameQueue;
std::mutex mtx;
std::condition_variable cv;
bool finished = false;
void worker() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !frameQueue.empty() || finished; });
if (finished && frameQueue.empty()) break;
cv::Mat frame = frameQueue.front();
frameQueue.pop();
lock.unlock();
// ここで処理(例:前処理)
cv::Mat processed;
cv::GaussianBlur(frame, processed, cv::Size(5,5), 0);
std::cout << "Processed a frame in thread " << std::this_thread::get_id() << std::endl;
}
}
int main() {
std::thread workerThread(worker);
cv::Mat img = cv::imread("plate.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) return -1;
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
frameQueue.push(img.clone());
lock.unlock();
cv.notify_one();
}
{
std::unique_lock<std::mutex> lock(mtx);
finished = true;
}
cv.notify_one();
workerThread.join();
return 0;
}
この方法は複雑な処理の分割や複数段階のパイプライン処理に向いています。
GPUアクセラレーション
CUDA版OpenCV
CUDA対応のOpenCVを使うと、GPUの並列計算能力を活かして画像処理を高速化できます。
特にフィルタリングやエッジ検出、透視変換などの重い処理で効果が大きいです。
#include <opencv2/opencv.hpp>
#include <opencv2/cudafilters.hpp>
#include <iostream>
int main() {
cv::Mat img = cv::imread("plate.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) return -1;
cv::cuda::GpuMat d_img, d_blurred;
d_img.upload(img);
auto gaussianFilter = cv::cuda::createGaussianFilter(d_img.type(), d_blurred.type(), cv::Size(5,5), 0);
gaussianFilter->apply(d_img, d_blurred);
cv::Mat result;
d_blurred.download(result);
cv::imshow("CUDA Gaussian Blur", result);
cv::waitKey(0);
return 0;
}
CUDA版OpenCVはNVIDIA GPUが必要で、環境構築がやや複雑ですが、リアルタイム処理に大きな効果があります。
OpenCL
OpenCLはGPUだけでなくCPUやFPGAなど多様なデバイスで動作する並列計算フレームワークです。
OpenCVはOpenCLをバックエンドに持ち、UMat
を使うことで自動的にOpenCLアクセラレーションを利用できます。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat img = cv::imread("plate.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) return -1;
cv::UMat u_img, u_blurred;
img.copyTo(u_img);
cv::GaussianBlur(u_img, u_blurred, cv::Size(5,5), 0);
cv::Mat result = u_blurred.getMat(cv::ACCESS_READ);
cv::imshow("OpenCL Gaussian Blur", result);
cv::waitKey(0);
return 0;
}
OpenCLは環境依存が少なく、幅広いハードウェアで利用可能です。
メモリ管理最適化
Mat再利用
OpenCVのcv::Mat
は参照カウント方式でメモリ管理されていますが、毎フレーム新規にMat
を生成するとメモリ割り当て・解放のオーバーヘッドが発生します。
可能な限りMat
オブジェクトを再利用し、メモリの再割り当てを減らすことでパフォーマンスが向上します。
#include <opencv2/opencv.hpp>
int main() {
cv::Mat frame = cv::imread("plate.jpg", cv::IMREAD_GRAYSCALE);
if (frame.empty()) return -1;
cv::Mat processed(frame.size(), frame.type());
for (int i = 0; i < 100; ++i) {
// processedを再利用して処理
cv::GaussianBlur(frame, processed, cv::Size(5,5), 0);
// 他の処理も同様にprocessedを使い回す
}
return 0;
}
キャッシュライン意識
CPUのキャッシュ効率を高めるために、画像データのアクセスパターンを工夫します。
例えば、画像の行単位で連続したメモリにアクセスすることや、ループの順序を最適化することが重要です。
OpenCVのMat
は行ごとに連続したメモリを持つため、行方向のループを外側に、列方向のループを内側にするのが基本です。
#include <opencv2/opencv.hpp>
void processImage(cv::Mat& img) {
for (int y = 0; y < img.rows; ++y) {
uchar* rowPtr = img.ptr<uchar>(y);
for (int x = 0; x < img.cols; ++x) {
// 連続メモリアクセス
rowPtr[x] = 255 - rowPtr[x]; // ネガポジ反転例
}
}
}
このようにキャッシュラインを意識したアクセスは、特に大きな画像処理で効果を発揮します。
これらのパフォーマンスチューニング技術を適切に組み合わせることで、リアルタイムのナンバープレート認識システムの処理速度を大幅に向上させられます。
マルチスレッド化やGPU活用、メモリ管理の最適化は、実装環境やハードウェアに応じて柔軟に選択してください。
テストと評価
ナンバープレート認識システムの品質を確保するためには、適切な評価指標を用いて精度を測定し、様々な環境下での動作を検証することが重要です。
ここでは、代表的な精度指標や評価シナリオ、誤検出の傾向分析について詳しく解説します。
精度指標
Precision / Recall
Precision(適合率)は、認識結果のうち正しく認識されたものの割合を示します。
誤認識(偽陽性)が少ないほど高くなります。
:正しく認識されたナンバープレート数(True Positive) :誤って認識されたナンバープレート数(False Positive)
Recall(再現率)は、実際に存在するナンバープレートのうち、正しく認識された割合を示します。
見逃し(偽陰性)が少ないほど高くなります。
:認識できなかったナンバープレート数(False Negative)
これらの指標はトレードオフの関係にあり、システムの閾値設定や検出条件によって変動します。
F1-score
F1-scoreはPrecisionとRecallの調和平均で、両者のバランスを評価します。
値は0から1の範囲で、1に近いほど優れた性能を示します。
F1-scoreは単一の指標で全体の認識性能を把握したい場合に有効です。
ベンチマークシナリオ
静止画像セット
静止画像セットは、様々な条件下で撮影されたナンバープレート画像を集めたデータセットです。
異なる角度、距離、照明条件の画像を用いて認識精度を評価します。
- 利点
画像ごとに正解ラベルが明確で、詳細な誤認識分析が可能です。
- 評価例
画像ごとに認識結果と正解を比較し、Precision、Recall、F1-scoreを算出。
画像1: 正解「123-あ-4567」 認識「123-あ-4567」 → TP
画像2: 正解「12-い-3456」 認識「12-い-345」 → FN(誤認識)
画像3: 正解なし 認識「999-え-8888」 → FP(誤検出)
動画ストリーム
動画ストリーム評価は、実際のカメラ映像を用いてリアルタイム認識の性能を測定します。
連続フレームでの認識結果の安定性や処理速度も重要な評価ポイントです。
- 利点
実運用に近い環境での評価が可能です。
- 評価方法
フレームごとに認識結果を取得し、時間的に同一車両の認識をまとめて評価。
重複検出の抑制や追跡精度も考慮。
- 課題
正解ラベル付けが難しく、手動アノテーションや半自動ツールが必要でしょう。
誤検出パターン分析
夜間撮影
夜間は照明条件が悪く、ナンバープレートの反射や光源の影響で認識精度が低下しやすいです。
特にヘッドライトの光や街灯の反射がノイズとなり、誤検出や文字欠損が発生します。
- 対策例
赤外線カメラの利用や、画像前処理でのノイズ除去強化、露出補正。
- 評価ポイント
夜間専用データセットでのPrecision/Recall比較。
雨天・逆光
雨天では水滴や曇りによる画像のぼやけ、逆光ではナンバープレートが暗く写ることが多く、文字の輪郭が不明瞭になります。
これにより輪郭検出やOCRの誤認識が増加します。
- 対策例
コントラスト強調(CLAHE)、透視変換の精度向上、複数フレームの情報統合。
- 評価ポイント
雨天・逆光条件下での誤認識率や検出漏れ率の分析。
これらのテストと評価を通じて、ナンバープレート認識システムの強みと弱点を把握し、改善点を明確にできます。
多様な環境条件を想定した評価は、実用的で信頼性の高いシステム構築に欠かせません。
エラーハンドリング
ナンバープレート認識システムでは、様々なエラーや例外が発生する可能性があります。
これらを適切に処理し、システムの安定性と信頼性を確保することが重要です。
ここではOCRの例外処理、画像読み込み失敗時の対応、そしてナンバープレート未検出時のリカバリ方法について詳しく解説します。
OCR例外処理
Tesseract OCRを利用する際には、認識処理中にメモリ不足や不正な画像フォーマット、内部エラーなどが発生することがあります。
これらの例外をキャッチし、適切に処理することでシステムのクラッシュを防ぎます。
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <exception>
std::string performOCR(tesseract::TessBaseAPI& tess, const cv::Mat& img) {
try {
tess.SetImage(img.data, img.cols, img.rows, img.channels(), img.step);
char* outText = tess.GetUTF8Text();
if (!outText) {
throw std::runtime_error("OCR結果が取得できませんでした");
}
std::string result(outText);
delete[] outText;
return result;
} catch (const std::exception& e) {
std::cerr << "OCR処理中に例外が発生しました: " << e.what() << std::endl;
return "";
}
}
int main() {
tesseract::TessBaseAPI tess;
if (tess.Init(NULL, "jpn", tesseract::OEM_LSTM_ONLY)) {
std::cerr << "Tesseractの初期化に失敗しました" << std::endl;
return -1;
}
cv::Mat img = cv::imread("plate_warped.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) {
std::cerr << "画像が読み込めません" << std::endl;
return -1;
}
std::string text = performOCR(tess, img);
if (text.empty()) {
std::cerr << "OCR結果が空です" << std::endl;
} else {
std::cout << "認識結果: " << text << std::endl;
}
tess.End();
return 0;
}
この例では、OCR処理中に例外が発生した場合にキャッチしてエラーメッセージを表示し、空文字列を返すことで後続処理への影響を抑えています。
画像読み込み失敗対応
画像ファイルのパスが間違っている、ファイルが破損している、カメラが接続されていないなどの理由で画像の読み込みに失敗することがあります。
これを検出し、適切に処理しなければシステムが停止する恐れがあります。
#include <opencv2/opencv.hpp>
#include <iostream>
cv::Mat loadImageSafe(const std::string& path) {
cv::Mat img = cv::imread(path);
if (img.empty()) {
std::cerr << "画像の読み込みに失敗しました: " << path << std::endl;
// 代替処理として空のMatを返すか、デフォルト画像を返すことも可能
}
return img;
}
int main() {
std::string imagePath = "nonexistent.jpg";
cv::Mat img = loadImageSafe(imagePath);
if (img.empty()) {
std::cerr << "処理を中断します" << std::endl;
return -1;
}
// 画像処理続行
cv::imshow("Image", img);
cv::waitKey(0);
return 0;
}
読み込み失敗時はログを残し、ユーザーに通知するか、リトライや代替画像の利用などの対策を検討します。
プレート未検出リカバリ
ナンバープレートが検出できない場合も頻繁に発生します。
例えば、画像の解像度不足、角度や照明条件の悪さ、遮蔽物などが原因です。
未検出時に適切なリカバリ処理を行うことで、システムの信頼性を向上させます。
- 再試行
画像の前処理パラメータを変えて再度検出を試みる。
例えば、閾値の調整やフィルタの強度変更。
- 複数フレーム統合
動画ストリームの場合、前後のフレームの検出結果を統合し、未検出フレームを補完。
- ユーザー通知
検出失敗をログに記録し、必要に応じてユーザーに警告を表示。
#include <iostream>
#include <vector>
bool detectPlate(const cv::Mat& img, cv::Rect& plateRect) {
// 仮の検出処理。実際は輪郭検出などを行う
// ここでは失敗をシミュレート
return false;
}
int main() {
cv::Mat img = cv::imread("car.jpg");
if (img.empty()) {
std::cerr << "画像が読み込めません" << std::endl;
return -1;
}
cv::Rect plateRect;
bool detected = detectPlate(img, plateRect);
if (!detected) {
std::cerr << "ナンバープレートが検出できませんでした。再試行します。" << std::endl;
// 例: 前処理パラメータを変えて再試行
// ここでは再試行処理を省略
} else {
std::cout << "ナンバープレート検出成功: " << plateRect << std::endl;
}
return 0;
}
未検出時のリカバリはシステムの要件に応じて柔軟に設計し、処理の継続性を確保することが重要です。
これらのエラーハンドリングを適切に実装することで、ナンバープレート認識システムの堅牢性が向上し、実運用でのトラブルを減らせます。
拡張と応用
自動車ナンバープレート認識システムは、基本的な認識機能を超えて様々な分野で応用可能です。
ここでは、駐車場管理システムへの組込み、高速道路ETCとの連携、クラウド連携によるAPI提供、そして複数カメラを用いたマルチカメラ統合について詳しく解説します。
駐車場管理システム組込み
駐車場管理システムにナンバープレート認識を組み込むことで、入退場の自動化や料金計算の効率化が実現します。
カメラで車両のナンバープレートをリアルタイムに読み取り、データベースと照合して許可車両の判定や滞在時間の計測を行います。
- 入退場管理
車両がゲートに近づくとカメラが撮影し、ナンバープレートを認識。
許可車両ならゲートを自動開放し、入場時刻を記録します。
- 料金計算
出場時に再度認識し、入場時刻と比較して滞在時間を算出。
料金計算システムと連携して自動精算を行います。
- セキュリティ強化
不正侵入車両の検知やブラックリスト車両の警告表示も可能です。
// 駐車場管理システムの簡易例(擬似コード)
std::string plateNumber = recognizePlate(frame);
if (isAuthorizedVehicle(plateNumber)) {
openGate();
logEntry(plateNumber, currentTime());
} else {
alertSecurity(plateNumber);
}
このようにナンバープレート認識を組み込むことで、無人化や運用コスト削減が期待できます。
高速道路ETC連携
高速道路のETC(Electronic Toll Collection)システムと連携することで、ナンバープレート認識は料金所の補助的な役割を果たします。
ETCカードの読み取りに加え、ナンバープレート情報を取得して車両の特定精度を高めます。
- 二重認証
ETCカード情報とナンバープレートを照合し、不正利用や誤認識を防止。
- 料金所の混雑緩和
ナンバープレート認識で車両情報を事前に把握し、スムーズな通過を支援。
- 違反車両検知
未払い車両や不正通行車両の特定に活用。
ETCシステムとの連携は、リアルタイム性と高い認識精度が求められるため、システム全体の最適化が重要です。
クラウド連携とAPI提供
ナンバープレート認識結果をクラウドに送信し、APIとして提供することで、複数拠点や外部システムとの連携が容易になります。
- クラウドサーバーへの送信
認識したナンバープレート情報や画像をクラウドにアップロードし、集中管理。
- RESTful APIの提供
他システムからHTTPリクエストで認識結果を取得可能にし、柔軟な連携を実現。
- データ分析・活用
クラウド上で通行履歴の分析や統計処理を行い、交通管理やマーケティングに活用。
POST /api/plate-recognition
Content-Type: application/json
{
"plate_number": "123-あ-4567",
"timestamp": "2024-06-01T12:34:56Z",
"image_url": "https://example.com/images/plate123.jpg"
}
クラウド連携により、スケーラブルで拡張性の高いシステム構築が可能です。
マルチカメラ統合
複数のカメラを設置し、異なる角度や位置から同一車両のナンバープレートを認識することで、認識精度と信頼性を向上させられます。
- 視点補完
片方のカメラで認識できなかった場合でも、別のカメラで補完可能です。
- 追跡と識別
複数カメラの映像を連携し、車両の移動経路を追跡。
重複認識の排除や誤認識の低減に役立ちます。
- 負荷分散
処理を複数カメラ間で分散し、リアルタイム処理の負荷を軽減。
// マルチカメラの認識結果統合例(擬似コード)
std::vector<std::string> recognizedPlates;
for (auto& camera : cameras) {
std::string plate = camera.recognizePlate();
if (!plate.empty()) {
recognizedPlates.push_back(plate);
}
}
// 重複排除や信頼度評価を行う
std::string finalPlate = aggregateResults(recognizedPlates);
マルチカメラ統合は大規模施設や交通監視システムで特に有効で、認識の冗長性と精度向上に貢献します。
これらの拡張と応用により、ナンバープレート認識システムは単なる文字認識を超え、実社会の多様なニーズに応える高度なソリューションへと発展します。
用途に応じて適切な技術を組み合わせ、効率的かつ信頼性の高いシステム構築を目指してください。
保守とアップデート
ナンバープレート認識システムを長期的に安定稼働させるためには、ライブラリのバージョン管理やモデルの再学習、そしてログ収集とモニタリング体制の整備が欠かせません。
ここではそれぞれのポイントについて詳しく解説します。
ライブラリバージョン追従
OpenCVやTesseractなどの主要ライブラリは頻繁にアップデートされ、新機能の追加やバグ修正、パフォーマンス改善が行われています。
これらの恩恵を受けるために、定期的にバージョンを確認し、必要に応じてシステムを更新することが重要です。
- 互換性の確認
新しいバージョンにアップデートする際は、APIの変更や非推奨機能の有無を確認し、既存コードの動作に影響がないかテストを行います。
- 依存関係管理
CMakeやConanなどのパッケージマネージャーを活用し、ライブラリのバージョンを明示的に管理することで、環境差異によるトラブルを防止します。
- セキュリティパッチ適用
セキュリティ上の脆弱性が報告された場合は速やかに対応し、システムの安全性を確保します。
# 例: OpenCVのバージョン確認
pkg-config --modversion opencv4
モデル再学習フロー
ナンバープレート認識の精度を維持・向上させるためには、環境変化や新しい文字パターンに対応したモデルの再学習が必要です。
再学習フローを整備し、定期的にモデルを更新します。
- データ収集
実運用環境から誤認識例や新しい文字画像を収集し、学習データセットを拡充します。
- データ前処理・ラベリング
収集した画像に正確なラベルを付与し、学習に適した形式に整えます。
- 学習・検証
既存モデルに追加学習(ファインチューニング)を行い、検証データで性能を評価します。
- モデルデプロイ
新モデルを本番環境に展開し、旧モデルとの比較テストを経て切り替えます。
再学習フロー例:
1. 誤認識データ収集 → 2. ラベリング → 3. ファインチューニング → 4. 検証 → 5. デプロイ
自動化ツールやCI/CDパイプラインを導入すると効率的です。
ログとモニタリング
システムの稼働状況や認識結果を継続的に監視し、問題発生時に迅速に対応できる体制を構築します。
- ログ収集
認識結果、処理時間、エラー情報などを詳細にログに記録。
ログはファイルやデータベース、クラウドサービスに保存します。
- モニタリングツール
PrometheusやGrafanaなどを用いて、CPU/GPU使用率、メモリ消費、認識成功率などの指標をリアルタイムで可視化します。
- アラート設定
異常検知時にメールやチャットツールへ通知し、早期対応を促します。
// ログ出力例(簡易)
#include <iostream>
#include <chrono>
#include <ctime>
void logRecognitionResult(const std::string& plate, bool success) {
auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::cout << std::ctime(&now) << " 認識結果: " << plate << " 成功: " << (success ? "Yes" : "No") << std::endl;
}
ログとモニタリングはシステムの健全性を保ち、継続的な品質改善に役立ちます。
これらの保守・アップデート体制を整えることで、ナンバープレート認識システムの信頼性と性能を長期間にわたり維持し、変化する環境や要件に柔軟に対応できます。
セキュリティとプライバシー
自動車ナンバープレート認識システムは個人情報を扱うため、適切なセキュリティ対策とプライバシー保護が不可欠です。
ここでは、個人情報のマスキングと通信の暗号化について詳しく解説します。
個人情報マスキング
ナンバープレートは個人を特定できる情報であるため、システム内での取り扱いには細心の注意が必要です。
特に映像や画像を外部に送信・保存する際は、ナンバープレート部分をマスキング(ぼかしや黒塗り)してプライバシーを保護します。
- マスキング方法
ナンバープレートの検出領域を矩形で囲み、その部分にモザイク処理やぼかしをかけます。
OpenCVではGaussianBlur
やresize
を使ったモザイクが一般的です。
#include <opencv2/opencv.hpp>
void maskPlate(cv::Mat& img, const cv::Rect& plateRect) {
cv::Mat roi = img(plateRect);
cv::Mat blurred;
// モザイク処理:縮小して拡大
cv::resize(roi, blurred, cv::Size(roi.cols / 10, roi.rows / 10), 0, 0, cv::INTER_LINEAR);
cv::resize(blurred, blurred, roi.size(), 0, 0, cv::INTER_NEAREST);
blurred.copyTo(roi);
}
int main() {
cv::Mat img = cv::imread("car.jpg");
if (img.empty()) return -1;
cv::Rect plateRect(100, 200, 200, 50); // 例としてナンバープレート領域
maskPlate(img, plateRect);
cv::imshow("Masked Image", img);
cv::waitKey(0);
return 0;
}
- 適用タイミング
画像保存前や外部送信前にマスキングを行い、不要な個人情報の漏洩を防ぎます。
- 法令遵守
個人情報保護法や地域のプライバシー規制に準拠し、必要に応じてマスキング範囲や保存期間を設定します。
暗号化通信
ナンバープレート認識システムがクラウドや外部サーバーと通信する場合、通信経路の暗号化は必須です。
これにより、通信途中での情報漏洩や改ざんを防止します。
- TLS/SSLの利用
HTTPSやTLSを用いて通信を暗号化します。
API通信や画像アップロード時に安全なチャネルを確保します。
- 証明書管理
正規の証明書を使用し、中間者攻撃(MITM)を防ぎます。
自己署名証明書の場合はクライアント側で信頼設定が必要です。
- 認証と認可
APIキーやOAuthなどの認証機構を導入し、アクセス制御を強化します。
// C++でのHTTPS通信はlibcurlなどを利用
#include <curl/curl.h>
#include <iostream>
int main() {
CURL* curl = curl_easy_init();
if (curl) {
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/api/plate");
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); // 証明書検証有効
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
std::cerr << "通信エラー: " << curl_easy_strerror(res) << std::endl;
}
curl_easy_cleanup(curl);
}
return 0;
}
- データの暗号化
通信だけでなく、保存データの暗号化も検討し、情報漏洩リスクを低減します。
個人情報マスキングと暗号化通信は、ナンバープレート認識システムの信頼性を高めるための基本的かつ重要な対策です。
これらを適切に実装し、プライバシー保護とセキュリティ強化を両立させましょう。
まとめ
本記事では、C++とOpenCV、Tesseractを活用したリアルタイム自動車ナンバープレート認識の基本から応用までを解説しました。
画像前処理やナンバープレート検出、文字認識の設定、結果の後処理、パフォーマンスチューニング、テスト評価、エラーハンドリング、さらにはセキュリティ対策やシステム拡張まで幅広くカバーしています。
これにより、実用的で高精度なナンバープレート認識システムの構築に必要な知識と技術が身につきます。