mirror of
https://github.com/kerberos-io/openalpr-base.git
synced 2025-10-08 15:00:07 +08:00
Merge branch 'master' into wts
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,11 @@ TARGET_LINK_LIBRARIES(classifychars
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
ADD_EXECUTABLE( benchmark benchmark.cpp )
|
ADD_EXECUTABLE(benchmark
|
||||||
|
benchmark/benchmark.cpp
|
||||||
|
benchmark/benchmark_utils.cpp
|
||||||
|
benchmark/endtoendtest.cpp
|
||||||
|
)
|
||||||
TARGET_LINK_LIBRARIES(benchmark
|
TARGET_LINK_LIBRARIES(benchmark
|
||||||
openalpr-static
|
openalpr-static
|
||||||
support
|
support
|
||||||
@@ -37,3 +41,9 @@ TARGET_LINK_LIBRARIES(prepcharsfortraining
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ADD_EXECUTABLE( tagplates tagplates.cpp )
|
||||||
|
TARGET_LINK_LIBRARIES(tagplates
|
||||||
|
support
|
||||||
|
${OpenCV_LIBS}
|
||||||
|
)
|
||||||
|
|
||||||
|
@@ -28,10 +28,8 @@
|
|||||||
|
|
||||||
#include "alpr_impl.h"
|
#include "alpr_impl.h"
|
||||||
|
|
||||||
//#include "stage1.h"
|
#include "endtoendtest.h"
|
||||||
//#include "stage2.h"
|
|
||||||
//#include "stateidentifier.h"
|
|
||||||
//#include "utility.h"
|
|
||||||
#include "support/filesystem.h"
|
#include "support/filesystem.h"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
@@ -42,6 +40,10 @@ using namespace cv;
|
|||||||
|
|
||||||
void outputStats(vector<double> datapoints);
|
void outputStats(vector<double> datapoints);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int main( int argc, const char** argv )
|
int main( int argc, const char** argv )
|
||||||
{
|
{
|
||||||
string country;
|
string country;
|
||||||
@@ -284,38 +286,9 @@ int main( int argc, const char** argv )
|
|||||||
}
|
}
|
||||||
else if (benchmarkName.compare("endtoend") == 0)
|
else if (benchmarkName.compare("endtoend") == 0)
|
||||||
{
|
{
|
||||||
Alpr alpr(country);
|
EndToEndTest e2eTest(inDir, outDir);
|
||||||
alpr.setDetectRegion(true);
|
e2eTest.runTest(country, files);
|
||||||
|
|
||||||
ofstream outputdatafile;
|
|
||||||
|
|
||||||
outputdatafile.open("results.txt");
|
|
||||||
|
|
||||||
for (int i = 0; i< files.size(); i++)
|
|
||||||
{
|
|
||||||
if (hasEnding(files[i], ".png"))
|
|
||||||
{
|
|
||||||
string fullpath = inDir + "/" + files[i];
|
|
||||||
frame = imread( fullpath.c_str() );
|
|
||||||
|
|
||||||
vector<uchar> buffer;
|
|
||||||
imencode(".bmp", frame, buffer );
|
|
||||||
|
|
||||||
vector<AlprResult> results = alpr.recognize(buffer);
|
|
||||||
|
|
||||||
outputdatafile << files[i] << ": ";
|
|
||||||
for (int z = 0; z < results.size(); z++)
|
|
||||||
{
|
|
||||||
outputdatafile << results[z].bestPlate.characters << ", ";
|
|
||||||
}
|
|
||||||
outputdatafile << endl;
|
|
||||||
|
|
||||||
imshow("Current LP", frame);
|
|
||||||
waitKey(5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outputdatafile.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
src/misc_utilities/benchmark/benchmark_utils.cpp
Normal file
24
src/misc_utilities/benchmark/benchmark_utils.cpp
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
#include "support/filesystem.h"
|
||||||
|
#include "benchmark_utils.h"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
vector<string> filterByExtension(vector<string> fileList, string extension)
|
||||||
|
{
|
||||||
|
vector<string> filteredList;
|
||||||
|
|
||||||
|
if (extension.size() == 0)
|
||||||
|
return filteredList;
|
||||||
|
|
||||||
|
if (extension[0] != '.')
|
||||||
|
extension = "." + extension;
|
||||||
|
|
||||||
|
for (int i = 0; i < fileList.size(); i++)
|
||||||
|
{
|
||||||
|
if (hasEnding(fileList[i], extension))
|
||||||
|
filteredList.push_back(fileList[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredList;
|
||||||
|
}
|
9
src/misc_utilities/benchmark/benchmark_utils.h
Normal file
9
src/misc_utilities/benchmark/benchmark_utils.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#ifndef OPENALPR_BENCHMARKUTILS_H
|
||||||
|
#define OPENALPR_BENCHMARKUTILS_H
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
std::vector<std::string> filterByExtension(std::vector<std::string> fileList, std::string extension);
|
||||||
|
|
||||||
|
|
||||||
|
#endif // OPENALPR_BENCHMARKUTILS_H
|
257
src/misc_utilities/benchmark/endtoendtest.cpp
Normal file
257
src/misc_utilities/benchmark/endtoendtest.cpp
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
#include "endtoendtest.h"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
using namespace cv;
|
||||||
|
|
||||||
|
|
||||||
|
EndToEndTest::EndToEndTest(string inputDir, string outputDir)
|
||||||
|
{
|
||||||
|
this->inputDir = inputDir;
|
||||||
|
this->outputDir = outputDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void EndToEndTest::runTest(string country, vector<std::string> files)
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
AlprImpl alpr(country);
|
||||||
|
alpr.config->debugOff();
|
||||||
|
alpr.setDetectRegion(false);
|
||||||
|
|
||||||
|
vector<EndToEndBenchmarkResult> benchmarkResults;
|
||||||
|
|
||||||
|
vector<string> textFiles = filterByExtension(files, ".txt");
|
||||||
|
|
||||||
|
for (int i = 0; i< textFiles.size(); i++)
|
||||||
|
{
|
||||||
|
cout << "Benchmarking file " << (i + 1) << " / " << textFiles.size() << " -- " << textFiles[i] << endl;
|
||||||
|
EndToEndBenchmarkResult benchmarkResult;
|
||||||
|
|
||||||
|
string fulltextpath = inputDir + "/" + textFiles[i];
|
||||||
|
|
||||||
|
ifstream inputFile(fulltextpath.c_str());
|
||||||
|
string line;
|
||||||
|
|
||||||
|
getline(inputFile, line);
|
||||||
|
|
||||||
|
istringstream ss(line);
|
||||||
|
|
||||||
|
string imgfile, plate_number;
|
||||||
|
int x, y, w, h;
|
||||||
|
|
||||||
|
ss >> imgfile >> x >> y >> w >> h >> plate_number;
|
||||||
|
|
||||||
|
string fullimgpath = inputDir + "/" + imgfile;
|
||||||
|
|
||||||
|
benchmarkResult.imageName = imgfile;
|
||||||
|
|
||||||
|
Mat frame = imread( fullimgpath.c_str() );
|
||||||
|
|
||||||
|
Rect actualPlateRect(x, y, w, h);
|
||||||
|
|
||||||
|
AlprFullDetails recognitionDetails = alpr.recognizeFullDetails(frame);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//cv::circle(frame, centerPoint, 2, Scalar(0, 0, 255), 5);
|
||||||
|
//drawAndWait(&frame);
|
||||||
|
|
||||||
|
benchmarkResult.detectionFalsePositives = 0;
|
||||||
|
|
||||||
|
for (int z = 0; z < recognitionDetails.plateRegions.size(); z++)
|
||||||
|
{
|
||||||
|
benchmarkResult.detectionFalsePositives += totalRectCount(recognitionDetails.plateRegions[z]);
|
||||||
|
|
||||||
|
bool rectmatches = rectMatches(actualPlateRect, recognitionDetails.plateRegions[z]);
|
||||||
|
|
||||||
|
|
||||||
|
if (rectmatches)
|
||||||
|
{
|
||||||
|
// This region matches our plate_number
|
||||||
|
benchmarkResult.detectedPlate = true;
|
||||||
|
benchmarkResult.detectionFalsePositives--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
benchmarkResult.resultsFalsePositives = recognitionDetails.results.size();
|
||||||
|
|
||||||
|
// Determine if the top result and the top N results match the correct value
|
||||||
|
for (int z = 0; z < recognitionDetails.results.size(); z++)
|
||||||
|
{
|
||||||
|
//cout << "Actual: " << plate_number << endl;
|
||||||
|
//cout << "Candidate: " << recognitionDetails.results[z].bestPlate.characters << endl;
|
||||||
|
if (recognitionDetails.results[z].bestPlate.characters == plate_number)
|
||||||
|
{
|
||||||
|
benchmarkResult.topResultCorrect = true;
|
||||||
|
benchmarkResult.top10ResultCorrect = true;
|
||||||
|
benchmarkResult.resultsFalsePositives--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int idx = 0; idx < recognitionDetails.results[z].topNPlates.size(); idx++)
|
||||||
|
{
|
||||||
|
if (recognitionDetails.results[z].topNPlates[idx].characters == plate_number)
|
||||||
|
{
|
||||||
|
benchmarkResult.top10ResultCorrect = true;
|
||||||
|
benchmarkResult.resultsFalsePositives--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (benchmarkResult.top10ResultCorrect)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
benchmarkResults.push_back(benchmarkResult);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print results data
|
||||||
|
|
||||||
|
ofstream data;
|
||||||
|
string outputResultsFile = outputDir + "/results.txt";
|
||||||
|
data.open(outputResultsFile.c_str());
|
||||||
|
|
||||||
|
data << "Image name Detected Plate # False Detections Top Result Correct Top 10 Correct # False Results" << endl;
|
||||||
|
for (int i = 0; i < benchmarkResults.size(); i++)
|
||||||
|
{
|
||||||
|
EndToEndBenchmarkResult br = benchmarkResults[i];
|
||||||
|
data << br.imageName << "\t" << br.detectedPlate << "\t" << br.detectionFalsePositives << "\t" << br.topResultCorrect << "\t" << br.top10ResultCorrect << "\t" << br.resultsFalsePositives << endl;
|
||||||
|
}
|
||||||
|
data.close();
|
||||||
|
|
||||||
|
// Print summary data
|
||||||
|
|
||||||
|
|
||||||
|
int totalDetections = 0;
|
||||||
|
int totalTopResultCorrect = 0;
|
||||||
|
int totalTop10Correct = 0;
|
||||||
|
int falseDetectionPositives = 0;
|
||||||
|
int falseResults = 0;
|
||||||
|
for (int i = 0; i < benchmarkResults.size(); i++)
|
||||||
|
{
|
||||||
|
if (benchmarkResults[i].detectedPlate) totalDetections++;
|
||||||
|
if (benchmarkResults[i].topResultCorrect) totalTopResultCorrect++;
|
||||||
|
if (benchmarkResults[i].top10ResultCorrect) totalTop10Correct++;
|
||||||
|
|
||||||
|
falseDetectionPositives += benchmarkResults[i].detectionFalsePositives;
|
||||||
|
falseResults += benchmarkResults[i].resultsFalsePositives;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Percentage of how many are correct (higher is better)
|
||||||
|
float detectionScore = 100.0 * ((float) totalDetections) / ((float) benchmarkResults.size());
|
||||||
|
float topResultScore = 100.0 * ((float) totalTopResultCorrect) / ((float) benchmarkResults.size());
|
||||||
|
float top10ResultScore = 100.0 * ((float) totalTop10Correct) / ((float) benchmarkResults.size());
|
||||||
|
|
||||||
|
// How many false positives per image (higher is worse)
|
||||||
|
float falseDetectionPositivesScore = ((float) falseDetectionPositives) / ((float) benchmarkResults.size());
|
||||||
|
float falseResultsScore = ((float) falseResults) / ((float) benchmarkResults.size());
|
||||||
|
|
||||||
|
string outputSummaryFile = outputDir + "/summary.txt";
|
||||||
|
data.open(outputSummaryFile.c_str());
|
||||||
|
|
||||||
|
data << "-------------------" << endl;
|
||||||
|
data << "| SUMMARY |" << endl;
|
||||||
|
data << "-------------------" << endl;
|
||||||
|
data << endl;
|
||||||
|
data << "Accuracy scores (higher is better)" << endl;
|
||||||
|
data << "Percent of plates DETECTED: " << detectionScore << endl;
|
||||||
|
data << "Percent of correct TOP10: " << top10ResultScore << endl;
|
||||||
|
data << "Percent of correct MATCHES: " << topResultScore << endl;
|
||||||
|
data << endl;
|
||||||
|
data << "False Positives Score (lower is better)" << endl;
|
||||||
|
data << "False DETECTIONS per image: " << falseDetectionPositivesScore << endl;
|
||||||
|
data << "False RESULTS per image: " << falseResultsScore << endl;
|
||||||
|
|
||||||
|
data.close();
|
||||||
|
|
||||||
|
|
||||||
|
// Print the contents of these files now:
|
||||||
|
string line;
|
||||||
|
ifstream resultsFileIn(outputResultsFile.c_str());
|
||||||
|
while(getline(resultsFileIn, line))
|
||||||
|
{
|
||||||
|
cout << line << endl;
|
||||||
|
}
|
||||||
|
ifstream summaryFileIn(outputSummaryFile.c_str());
|
||||||
|
while(getline(summaryFileIn, line))
|
||||||
|
{
|
||||||
|
cout << line << endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool EndToEndTest::rectMatches(cv::Rect actualPlate, PlateRegion candidate)
|
||||||
|
{
|
||||||
|
// Determine if this region matches our plate in the image
|
||||||
|
// Do this simply by verifying that the center point of the plate is within the region
|
||||||
|
// And that the plate region is not x% larger or smaller
|
||||||
|
|
||||||
|
const float MAX_SIZE_PERCENT_LARGER = 0.65;
|
||||||
|
|
||||||
|
//int plateCenterX = actualPlate.x + (int) (((float) actualPlate.width) / 2.0);
|
||||||
|
//int plateCenterY = actualPlate.y + (int) (((float) actualPlate.height) / 2.0);
|
||||||
|
//Point centerPoint(plateCenterX, plateCenterY);
|
||||||
|
|
||||||
|
vector<Point> requiredPoints;
|
||||||
|
requiredPoints.push_back(Point( actualPlate.x + (int) (((float) actualPlate.width) * 0.2),
|
||||||
|
actualPlate.y + (int) (((float) actualPlate.height) * 0.15)
|
||||||
|
));
|
||||||
|
requiredPoints.push_back(Point( actualPlate.x + (int) (((float) actualPlate.width) * 0.8),
|
||||||
|
actualPlate.y + (int) (((float) actualPlate.height) * 0.15)
|
||||||
|
));
|
||||||
|
requiredPoints.push_back(Point( actualPlate.x + (int) (((float) actualPlate.width) * 0.2),
|
||||||
|
actualPlate.y + (int) (((float) actualPlate.height) * 0.85)
|
||||||
|
));
|
||||||
|
requiredPoints.push_back(Point( actualPlate.x + (int) (((float) actualPlate.width) * 0.8),
|
||||||
|
actualPlate.y + (int) (((float) actualPlate.height) * 0.85)
|
||||||
|
));
|
||||||
|
|
||||||
|
|
||||||
|
float sizeDiff = 1.0 - ((float) actualPlate.area()) / ((float) candidate.rect.area());
|
||||||
|
|
||||||
|
//cout << "Candidate: " << candidate.rect.x << "," << candidate.rect.y << " " << candidate.rect.width << "-" << candidate.rect.height << endl;
|
||||||
|
//cout << "Actual: " << actualPlate.x << "," << actualPlate.y << " " << actualPlate.width << "-" << actualPlate.height << endl;
|
||||||
|
|
||||||
|
//cout << "size diff: " << sizeDiff << endl;
|
||||||
|
|
||||||
|
bool hasAllPoints = true;
|
||||||
|
for (int z = 0; z < requiredPoints.size(); z++)
|
||||||
|
{
|
||||||
|
if (candidate.rect.contains(requiredPoints[z]) == false)
|
||||||
|
hasAllPoints = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ( hasAllPoints &&
|
||||||
|
(sizeDiff < MAX_SIZE_PERCENT_LARGER) )
|
||||||
|
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int i = 0; i < candidate.children.size(); i++)
|
||||||
|
{
|
||||||
|
if (rectMatches(actualPlate, candidate.children[i]))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EndToEndTest::totalRectCount(PlateRegion rootCandidate)
|
||||||
|
{
|
||||||
|
int childCount = 0;
|
||||||
|
for (int i = 0; i < rootCandidate.children.size(); i++)
|
||||||
|
{
|
||||||
|
childCount += totalRectCount(rootCandidate.children[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return childCount + 1;
|
||||||
|
}
|
47
src/misc_utilities/benchmark/endtoendtest.h
Normal file
47
src/misc_utilities/benchmark/endtoendtest.h
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#ifndef OPENALPR_ENDTOENDTEST_H
|
||||||
|
#define OPENALPR_ENDTOENDTEST_H
|
||||||
|
|
||||||
|
|
||||||
|
#include "opencv2/highgui/highgui.hpp"
|
||||||
|
#include "opencv2/imgproc/imgproc.hpp"
|
||||||
|
#include "alpr_impl.h"
|
||||||
|
#include "benchmark_utils.h"
|
||||||
|
|
||||||
|
class EndToEndTest
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
EndToEndTest(std::string inputDir, std::string outputDir);
|
||||||
|
void runTest(std::string country, std::vector<std::string> files);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
bool rectMatches(cv::Rect actualPlate, PlateRegion candidate);
|
||||||
|
int totalRectCount(PlateRegion rootCandidate);
|
||||||
|
|
||||||
|
std::string inputDir;
|
||||||
|
std::string outputDir;
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class EndToEndBenchmarkResult {
|
||||||
|
public:
|
||||||
|
EndToEndBenchmarkResult()
|
||||||
|
{
|
||||||
|
this->imageName = "";
|
||||||
|
this->detectedPlate = false;
|
||||||
|
this->topResultCorrect = false;
|
||||||
|
this->top10ResultCorrect = false;
|
||||||
|
this->detectionFalsePositives = 0;
|
||||||
|
this->resultsFalsePositives = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string imageName;
|
||||||
|
bool detectedPlate;
|
||||||
|
bool topResultCorrect;
|
||||||
|
bool top10ResultCorrect;
|
||||||
|
int detectionFalsePositives;
|
||||||
|
int resultsFalsePositives;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //OPENALPR_ENDTOENDTEST_H
|
233
src/misc_utilities/tagplates.cpp
Normal file
233
src/misc_utilities/tagplates.cpp
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2013 New Designs Unlimited, LLC
|
||||||
|
* Opensource Automated License Plate Recognition [http://www.openalpr.com]
|
||||||
|
*
|
||||||
|
* This file is part of OpenAlpr.
|
||||||
|
*
|
||||||
|
* OpenAlpr is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License
|
||||||
|
* version 3 as published by the Free Software Foundation
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#include "opencv2/highgui/highgui.hpp"
|
||||||
|
#include "opencv2/imgproc/imgproc.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
#include "support/filesystem.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
const int LEFT_ARROW_KEY = 2;
|
||||||
|
const int RIGHT_ARROW_KEY = 3;
|
||||||
|
const int SPACE_KEY = 32;
|
||||||
|
const int ENTER_KEY = 13;
|
||||||
|
const int ESCAPE_KEY = 27;
|
||||||
|
const int BACKSPACE_KEY = 8;
|
||||||
|
|
||||||
|
const int DOWN_ARROW_KEY = 1;
|
||||||
|
const int UP_ARROW_KEY= 0;
|
||||||
|
|
||||||
|
#else
|
||||||
|
const int LEFT_ARROW_KEY = 81;
|
||||||
|
const int RIGHT_ARROW_KEY = 83;
|
||||||
|
const int SPACE_KEY = 32;
|
||||||
|
const int ENTER_KEY = 10;
|
||||||
|
const int ESCAPE_KEY = 27;
|
||||||
|
const int BACKSPACE_KEY = 8;
|
||||||
|
|
||||||
|
const int DOWN_ARROW_KEY = 84;
|
||||||
|
const int UP_ARROW_KEY= 82;
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
using namespace cv;
|
||||||
|
|
||||||
|
static bool ldragging = false;
|
||||||
|
static int xPos1 = 0;
|
||||||
|
static int yPos1 = 0;
|
||||||
|
static int xPos2 = 0;
|
||||||
|
static int yPos2 = 0;
|
||||||
|
const float ASPECT_RATIO = 2.0;
|
||||||
|
|
||||||
|
static bool rdragging = false;
|
||||||
|
static int rDragStartX = 0;
|
||||||
|
static int rDragStartY = 0;
|
||||||
|
|
||||||
|
void mouseCallback(int event, int x, int y, int flags, void* userdata)
|
||||||
|
{
|
||||||
|
if ( event == EVENT_LBUTTONDOWN )
|
||||||
|
{
|
||||||
|
ldragging = true;
|
||||||
|
xPos1 = x;
|
||||||
|
yPos1 = y;
|
||||||
|
xPos2 = x;
|
||||||
|
yPos2 = y;
|
||||||
|
}
|
||||||
|
else if ( event == EVENT_LBUTTONUP )
|
||||||
|
{
|
||||||
|
ldragging = false;
|
||||||
|
}
|
||||||
|
else if ( event == EVENT_RBUTTONDOWN )
|
||||||
|
{
|
||||||
|
rdragging = true;
|
||||||
|
rDragStartX = x;
|
||||||
|
rDragStartY = y;
|
||||||
|
}
|
||||||
|
else if ( event == EVENT_RBUTTONUP )
|
||||||
|
{
|
||||||
|
rdragging = false;
|
||||||
|
}
|
||||||
|
else if ( event == EVENT_MOUSEMOVE )
|
||||||
|
{
|
||||||
|
if (ldragging && x > xPos1 && y > yPos1)
|
||||||
|
{
|
||||||
|
int w = x - xPos1;
|
||||||
|
int h = (int) (((float) w) / ASPECT_RATIO);
|
||||||
|
|
||||||
|
xPos2 = x;
|
||||||
|
yPos2 = yPos1 + h;
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (rdragging)
|
||||||
|
{
|
||||||
|
int xDiff = x - rDragStartX;
|
||||||
|
int yDiff = y - rDragStartY;
|
||||||
|
|
||||||
|
xPos1 += xDiff;
|
||||||
|
yPos1 += yDiff;
|
||||||
|
xPos2 += xDiff;
|
||||||
|
yPos2 += yDiff;
|
||||||
|
|
||||||
|
rDragStartX = x;
|
||||||
|
rDragStartY = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
int main( int argc, const char** argv )
|
||||||
|
{
|
||||||
|
string country;
|
||||||
|
string inDir;
|
||||||
|
string outDir;
|
||||||
|
|
||||||
|
//Check if user specify image to process
|
||||||
|
if(argc == 4)
|
||||||
|
{
|
||||||
|
country = argv[1];
|
||||||
|
inDir = argv[2];
|
||||||
|
outDir = argv[3];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf("Use:\n\t%s [country] [img input dir] [data output dir]\n",argv[0]);
|
||||||
|
printf("\tex: %s us ./usimages ./usdata\n",argv[0]);
|
||||||
|
printf("\n\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DirectoryExists(inDir.c_str()) == false)
|
||||||
|
{
|
||||||
|
printf("Input dir does not exist\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (DirectoryExists(outDir.c_str()) == false)
|
||||||
|
{
|
||||||
|
printf("Output dir does not exist\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
vector<string> files = getFilesInDir(inDir.c_str());
|
||||||
|
|
||||||
|
vector<string> imgFiles;
|
||||||
|
sort( files.begin(), files.end(), stringCompare );
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < files.size(); i++)
|
||||||
|
{
|
||||||
|
if (hasEnding(files[i], ".png") || hasEnding(files[i], ".jpg"))
|
||||||
|
{
|
||||||
|
imgFiles.push_back(files[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i< imgFiles.size(); i++)
|
||||||
|
{
|
||||||
|
|
||||||
|
cout << "Loading: " << imgFiles[i] << " (" << (i+1) << "/" << imgFiles.size() << ")" << endl;
|
||||||
|
|
||||||
|
string fullimgpath = inDir + "/" + imgFiles[i];
|
||||||
|
|
||||||
|
Mat frame = imread( fullimgpath.c_str() );
|
||||||
|
|
||||||
|
if (frame.cols == 0 || frame.rows == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
|
||||||
|
string curplatetag = "";
|
||||||
|
//Create a window
|
||||||
|
namedWindow("Input image", 1);
|
||||||
|
|
||||||
|
//set the callback function for any mouse event
|
||||||
|
setMouseCallback("Input image", mouseCallback, NULL);
|
||||||
|
|
||||||
|
char key = (char) cv::waitKey(50);
|
||||||
|
while (key != ENTER_KEY)
|
||||||
|
{
|
||||||
|
|
||||||
|
if ((key >= '0' && key <= '9') || (key >= 'a' && key <= 'z'))
|
||||||
|
{
|
||||||
|
curplatetag = curplatetag + (char) toupper( key );
|
||||||
|
}
|
||||||
|
else if (key == BACKSPACE_KEY)
|
||||||
|
{
|
||||||
|
curplatetag = curplatetag.substr(0, curplatetag.size() - 1);
|
||||||
|
}
|
||||||
|
Mat tmpFrame(frame.size(), frame.type());
|
||||||
|
frame.copyTo(tmpFrame);
|
||||||
|
rectangle(tmpFrame, Point(xPos1, yPos1), Point(xPos2, yPos2), Scalar(0, 0, 255), 2);
|
||||||
|
|
||||||
|
rectangle(tmpFrame, Point(xPos1, yPos1 - 35), Point(xPos1 + 175, yPos1 - 5), Scalar(255, 255, 255), CV_FILLED);
|
||||||
|
putText(tmpFrame, curplatetag, Point(xPos1 + 2, yPos1 - 10), FONT_HERSHEY_PLAIN, 1.5, Scalar(100,50,0), 2);
|
||||||
|
|
||||||
|
imshow("Input image", tmpFrame);
|
||||||
|
|
||||||
|
key = cv::waitKey(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curplatetag != "")
|
||||||
|
{
|
||||||
|
// Write the data to disk
|
||||||
|
ofstream outputdatafile;
|
||||||
|
|
||||||
|
std::string outputTextFile = outDir + "/" + filenameWithoutExtension(files[i]) + ".txt";
|
||||||
|
outputdatafile.open(outputTextFile.c_str());
|
||||||
|
|
||||||
|
outputdatafile << files[i] << "\t" << xPos1 << "\t" << yPos1 << "\t" << (xPos2 - xPos1) << "\t" << (yPos2 - yPos1) << "\t" << curplatetag << endl;
|
||||||
|
outputdatafile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@@ -41,12 +41,12 @@ std::vector<AlprResult> Alpr::recognize(std::string filepath)
|
|||||||
std::vector<AlprResult> Alpr::recognize(std::vector<unsigned char> imageBuffer)
|
std::vector<AlprResult> Alpr::recognize(std::vector<unsigned char> imageBuffer)
|
||||||
{
|
{
|
||||||
// Not sure if this actually works
|
// Not sure if this actually works
|
||||||
cv::Mat img = cv::imdecode(Mat(imageBuffer), 1);
|
cv::Mat img = cv::imdecode(cv::Mat(imageBuffer), 1);
|
||||||
|
|
||||||
return impl->recognize(img);
|
return impl->recognize(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
string Alpr::toJson(const vector< AlprResult > results, double processing_time_ms)
|
std::string Alpr::toJson(const std::vector< AlprResult > results, double processing_time_ms)
|
||||||
{
|
{
|
||||||
return impl->toJson(results, processing_time_ms);
|
return impl->toJson(results, processing_time_ms);
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,9 @@
|
|||||||
|
|
||||||
void plateAnalysisThread(void* arg);
|
void plateAnalysisThread(void* arg);
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
using namespace cv;
|
||||||
|
|
||||||
AlprImpl::AlprImpl(const std::string country, const std::string configFile, const std::string runtimeDir)
|
AlprImpl::AlprImpl(const std::string country, const std::string configFile, const std::string runtimeDir)
|
||||||
{
|
{
|
||||||
config = new Config(country, configFile, runtimeDir);
|
config = new Config(country, configFile, runtimeDir);
|
||||||
@@ -63,12 +66,12 @@ bool AlprImpl::isLoaded()
|
|||||||
return config->loaded;
|
return config->loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AlprFullDetails AlprImpl::recognizeFullDetails(cv::Mat img)
|
||||||
std::vector<AlprResult> AlprImpl::recognize(cv::Mat img)
|
|
||||||
{
|
{
|
||||||
timespec startTime;
|
timespec startTime;
|
||||||
getTime(&startTime);
|
getTime(&startTime);
|
||||||
|
|
||||||
|
AlprFullDetails response;
|
||||||
|
|
||||||
if (!img.data)
|
if (!img.data)
|
||||||
{
|
{
|
||||||
@@ -77,11 +80,16 @@ std::vector<AlprResult> AlprImpl::recognize(cv::Mat img)
|
|||||||
std::cerr << "Invalid image" << std::endl;
|
std::cerr << "Invalid image" << std::endl;
|
||||||
|
|
||||||
vector<AlprResult> emptyVector;
|
vector<AlprResult> emptyVector;
|
||||||
return emptyVector;
|
response.results = emptyVector;
|
||||||
|
|
||||||
|
vector<PlateRegion> emptyVector2;
|
||||||
|
response.plateRegions = emptyVector2;
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all the candidate regions
|
// Find all the candidate regions
|
||||||
vector<PlateRegion> plateRegions = plateDetector->detect(img);
|
response.plateRegions = plateDetector->detect(img);
|
||||||
|
|
||||||
// Get the number of threads specified and make sure the value is sane (cannot be greater than CPU cores or less than 1)
|
// Get the number of threads specified and make sure the value is sane (cannot be greater than CPU cores or less than 1)
|
||||||
int numThreads = config->multithreading_cores;
|
int numThreads = config->multithreading_cores;
|
||||||
@@ -91,7 +99,7 @@ std::vector<AlprResult> AlprImpl::recognize(cv::Mat img)
|
|||||||
numThreads = 1;
|
numThreads = 1;
|
||||||
|
|
||||||
|
|
||||||
PlateDispatcher dispatcher(plateRegions, &img,
|
PlateDispatcher dispatcher(response.plateRegions, &img,
|
||||||
config, stateIdentifier, ocr,
|
config, stateIdentifier, ocr,
|
||||||
topN, detectRegion, defaultRegion);
|
topN, detectRegion, defaultRegion);
|
||||||
|
|
||||||
@@ -120,9 +128,9 @@ std::vector<AlprResult> AlprImpl::recognize(cv::Mat img)
|
|||||||
|
|
||||||
if (config->debugGeneral && config->debugShowImages)
|
if (config->debugGeneral && config->debugShowImages)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < plateRegions.size(); i++)
|
for (int i = 0; i < response.plateRegions.size(); i++)
|
||||||
{
|
{
|
||||||
rectangle(img, plateRegions[i].rect, Scalar(0, 0, 255), 2);
|
rectangle(img, response.plateRegions[i].rect, Scalar(0, 0, 255), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < dispatcher.getRecognitionResults().size(); i++)
|
for (int i = 0; i < dispatcher.getRecognitionResults().size(); i++)
|
||||||
@@ -143,7 +151,7 @@ std::vector<AlprResult> AlprImpl::recognize(cv::Mat img)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
vector<AlprResult> results = dispatcher.getRecognitionResults();
|
response.results = dispatcher.getRecognitionResults();
|
||||||
|
|
||||||
if (config->debugPauseOnFrame)
|
if (config->debugPauseOnFrame)
|
||||||
{
|
{
|
||||||
@@ -152,7 +160,13 @@ std::vector<AlprResult> AlprImpl::recognize(cv::Mat img)
|
|||||||
{}
|
{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<AlprResult> AlprImpl::recognize(cv::Mat img)
|
||||||
|
{
|
||||||
|
AlprFullDetails fullDetails = recognizeFullDetails(img);
|
||||||
|
return fullDetails.results;
|
||||||
}
|
}
|
||||||
|
|
||||||
void plateAnalysisThread(void* arg)
|
void plateAnalysisThread(void* arg)
|
||||||
|
@@ -47,6 +47,13 @@
|
|||||||
|
|
||||||
#define ALPR_NULL_PTR 0
|
#define ALPR_NULL_PTR 0
|
||||||
|
|
||||||
|
|
||||||
|
struct AlprFullDetails
|
||||||
|
{
|
||||||
|
std::vector<PlateRegion> plateRegions;
|
||||||
|
std::vector<AlprResult> results;
|
||||||
|
};
|
||||||
|
|
||||||
class AlprImpl
|
class AlprImpl
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -54,15 +61,16 @@ class AlprImpl
|
|||||||
AlprImpl(const std::string country, const std::string configFile = "", const std::string runtimeDir = "");
|
AlprImpl(const std::string country, const std::string configFile = "", const std::string runtimeDir = "");
|
||||||
virtual ~AlprImpl();
|
virtual ~AlprImpl();
|
||||||
|
|
||||||
|
AlprFullDetails recognizeFullDetails(cv::Mat img);
|
||||||
std::vector<AlprResult> recognize(cv::Mat img);
|
std::vector<AlprResult> recognize(cv::Mat img);
|
||||||
|
|
||||||
void applyRegionTemplate(AlprResult* result, std::string region);
|
void applyRegionTemplate(AlprResult* result, std::string region);
|
||||||
|
|
||||||
void setDetectRegion(bool detectRegion);
|
void setDetectRegion(bool detectRegion);
|
||||||
void setTopN(int topn);
|
void setTopN(int topn);
|
||||||
void setDefaultRegion(string region);
|
void setDefaultRegion(std::string region);
|
||||||
|
|
||||||
std::string toJson(const vector<AlprResult> results, double processing_time_ms = -1);
|
std::string toJson(const std::vector<AlprResult> results, double processing_time_ms = -1);
|
||||||
static std::string getVersion();
|
static std::string getVersion();
|
||||||
|
|
||||||
Config* config;
|
Config* config;
|
||||||
@@ -85,7 +93,7 @@ class AlprImpl
|
|||||||
class PlateDispatcher
|
class PlateDispatcher
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PlateDispatcher(vector<PlateRegion> plateRegions, cv::Mat* image,
|
PlateDispatcher(std::vector<PlateRegion> plateRegions, cv::Mat* image,
|
||||||
Config* config,
|
Config* config,
|
||||||
StateIdentifier* stateIdentifier,
|
StateIdentifier* stateIdentifier,
|
||||||
OCR* ocr,
|
OCR* ocr,
|
||||||
@@ -106,7 +114,7 @@ class PlateDispatcher
|
|||||||
{
|
{
|
||||||
tthread::lock_guard<tthread::mutex> guard(mMutex);
|
tthread::lock_guard<tthread::mutex> guard(mMutex);
|
||||||
|
|
||||||
Mat img(this->frame->size(), this->frame->type());
|
cv::Mat img(this->frame->size(), this->frame->type());
|
||||||
this->frame->copyTo(img);
|
this->frame->copyTo(img);
|
||||||
|
|
||||||
return img;
|
return img;
|
||||||
@@ -139,7 +147,7 @@ class PlateDispatcher
|
|||||||
recognitionResults.push_back(recognitionResult);
|
recognitionResults.push_back(recognitionResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
vector<AlprResult> getRecognitionResults()
|
std::vector<AlprResult> getRecognitionResults()
|
||||||
{
|
{
|
||||||
return recognitionResults;
|
return recognitionResults;
|
||||||
}
|
}
|
||||||
@@ -159,8 +167,8 @@ class PlateDispatcher
|
|||||||
tthread::mutex mMutex;
|
tthread::mutex mMutex;
|
||||||
|
|
||||||
cv::Mat* frame;
|
cv::Mat* frame;
|
||||||
vector<PlateRegion> plateRegions;
|
std::vector<PlateRegion> plateRegions;
|
||||||
vector<AlprResult> recognitionResults;
|
std::vector<AlprResult> recognitionResults;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -130,7 +130,9 @@ CharacterSegmenter::CharacterSegmenter(Mat img, bool invertedColors, Config* con
|
|||||||
Mat histoCopy(vertHistogram.histoImg.size(), vertHistogram.histoImg.type());
|
Mat histoCopy(vertHistogram.histoImg.size(), vertHistogram.histoImg.type());
|
||||||
//vertHistogram.copyTo(histoCopy);
|
//vertHistogram.copyTo(histoCopy);
|
||||||
cvtColor(vertHistogram.histoImg, histoCopy, CV_GRAY2RGB);
|
cvtColor(vertHistogram.histoImg, histoCopy, CV_GRAY2RGB);
|
||||||
allHistograms.push_back(histoCopy);
|
|
||||||
|
string label = "threshold: " + toString(i);
|
||||||
|
allHistograms.push_back(addLabel(histoCopy, label));
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -144,7 +146,7 @@ CharacterSegmenter::CharacterSegmenter(Mat img, bool invertedColors, Config* con
|
|||||||
rectangle(allHistograms[i], charBoxes[cboxIdx], Scalar(0, 255, 0));
|
rectangle(allHistograms[i], charBoxes[cboxIdx], Scalar(0, 255, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
Mat histDashboard = drawImageDashboard(allHistograms, allHistograms[0].type(), 3);
|
Mat histDashboard = drawImageDashboard(allHistograms, allHistograms[0].type(), 1);
|
||||||
displayImage(config, "Char seg histograms", histDashboard);
|
displayImage(config, "Char seg histograms", histDashboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -200,6 +200,7 @@ void Config::debugOff()
|
|||||||
debugColorFiler = false;
|
debugColorFiler = false;
|
||||||
debugOcr = false;
|
debugOcr = false;
|
||||||
debugPostProcess = false;
|
debugPostProcess = false;
|
||||||
|
debugPauseOnFrame = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -19,6 +19,10 @@
|
|||||||
|
|
||||||
#include "ocr.h"
|
#include "ocr.h"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
using namespace cv;
|
||||||
|
using namespace tesseract;
|
||||||
|
|
||||||
OCR::OCR(Config* config)
|
OCR::OCR(Config* config)
|
||||||
{
|
{
|
||||||
const string EXPECTED_TESSERACT_VERSION = "3.03";
|
const string EXPECTED_TESSERACT_VERSION = "3.03";
|
||||||
|
@@ -31,9 +31,6 @@
|
|||||||
#include "opencv2/imgproc/imgproc.hpp"
|
#include "opencv2/imgproc/imgproc.hpp"
|
||||||
|
|
||||||
#include "tesseract/baseapi.h"
|
#include "tesseract/baseapi.h"
|
||||||
using namespace tesseract;
|
|
||||||
using namespace std;
|
|
||||||
using namespace cv;
|
|
||||||
|
|
||||||
class OCR
|
class OCR
|
||||||
{
|
{
|
||||||
@@ -42,7 +39,7 @@ class OCR
|
|||||||
OCR(Config* config);
|
OCR(Config* config);
|
||||||
virtual ~OCR();
|
virtual ~OCR();
|
||||||
|
|
||||||
void performOCR(vector<Mat> thresholds, vector<Rect> charRegions);
|
void performOCR(std::vector<cv::Mat> thresholds, std::vector<cv::Rect> charRegions);
|
||||||
|
|
||||||
PostProcess* postProcessor;
|
PostProcess* postProcessor;
|
||||||
//string recognizedText;
|
//string recognizedText;
|
||||||
@@ -52,7 +49,7 @@ class OCR
|
|||||||
private:
|
private:
|
||||||
Config* config;
|
Config* config;
|
||||||
|
|
||||||
TessBaseAPI *tesseract;
|
tesseract::TessBaseAPI *tesseract;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -101,3 +101,9 @@ bool stringCompare( const std::string &left, const std::string &right )
|
|||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string filenameWithoutExtension(std::string filename)
|
||||||
|
{
|
||||||
|
int lastindex = filename.find_last_of(".");
|
||||||
|
return filename.substr(0, lastindex);
|
||||||
|
}
|
@@ -21,6 +21,8 @@ bool startsWith(std::string const &fullString, std::string const &prefix);
|
|||||||
bool hasEnding (std::string const &fullString, std::string const &ending);
|
bool hasEnding (std::string const &fullString, std::string const &ending);
|
||||||
bool hasEndingInsensitive(const std::string& fullString, const std::string& ending);
|
bool hasEndingInsensitive(const std::string& fullString, const std::string& ending);
|
||||||
|
|
||||||
|
std::string filenameWithoutExtension(std::string filename);
|
||||||
|
|
||||||
bool DirectoryExists( const char* pzPath );
|
bool DirectoryExists( const char* pzPath );
|
||||||
bool fileExists( const char* pzPath );
|
bool fileExists( const char* pzPath );
|
||||||
std::vector<std::string> getFilesInDir(const char* dirPath);
|
std::vector<std::string> getFilesInDir(const char* dirPath);
|
||||||
|
@@ -375,3 +375,23 @@ LineSegment LineSegment::getParallelLine(float distance)
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string toString(int value)
|
||||||
|
{
|
||||||
|
stringstream ss;
|
||||||
|
ss << value;
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
std::string toString(float value)
|
||||||
|
{
|
||||||
|
stringstream ss;
|
||||||
|
ss << value;
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
std::string toString(double value)
|
||||||
|
{
|
||||||
|
stringstream ss;
|
||||||
|
ss << value;
|
||||||
|
return ss.str();
|
||||||
|
}
|
@@ -106,4 +106,9 @@ cv::Rect expandRect(cv::Rect original, int expandXPixels, int expandYPixels, int
|
|||||||
|
|
||||||
cv::Mat addLabel(cv::Mat input, std::string label);
|
cv::Mat addLabel(cv::Mat input, std::string label);
|
||||||
|
|
||||||
|
|
||||||
|
std::string toString(int value);
|
||||||
|
std::string toString(float value);
|
||||||
|
std::string toString(double value);
|
||||||
|
|
||||||
#endif // OPENALPR_UTILITY_H
|
#endif // OPENALPR_UTILITY_H
|
||||||
|
Reference in New Issue
Block a user