Merge branch 'master' into v2.0

Conflicts:
	src/main.cpp
This commit is contained in:
Matt Hill
2014-10-23 17:26:52 -04:00
39 changed files with 2208 additions and 1435 deletions

16
.travis.yml Normal file
View File

@@ -0,0 +1,16 @@
before_install:
- sudo add-apt-repository ppa:yjwong/opencv2 -y
- sudo add-apt-repository ppa:lyrasis/precise-backports -y
- sudo apt-get update -q
install: sudo apt-get -y install libopencv-dev libtesseract-dev git cmake build-essential libleptonica-dev liblog4cplus-dev libcurl3-dev beanstalkd
before_script:
- mkdir -p ./src/build/
- cd ./src/build/
- cmake ..
script:
- make
- make classifychars tagplates sortstate benchmark prepcharsfortraining

View File

@@ -15,22 +15,21 @@ For example, the following output is created by analyzing this image:
![Plate Image](http://www.openalpr.com/images/demoscreenshots/plate3.png "Input image")
The library is told that this is a Missouri (MO) license plate which validates the plate letters against a regional template.
```
user@linux:~/openalpr$ alpr ./samplecar.png -t mo -r ~/openalpr/runtime_dir/
user@linux:~/openalpr$ alpr ./samplecar.png
plate0: top 10 results -- Processing Time = 58.1879ms.
- PE3R2X confidence: 88.9371 template_match: 1
- PE32X confidence: 78.1385 template_match: 0
- PE3R2 confidence: 77.5444 template_match: 0
- PE3R2Y confidence: 76.1448 template_match: 1
- P63R2X confidence: 72.9016 template_match: 0
- FE3R2X confidence: 72.1147 template_match: 1
- PE32 confidence: 66.7458 template_match: 0
- PE32Y confidence: 65.3462 template_match: 0
- P632X confidence: 62.1031 template_match: 0
- P63R2 confidence: 61.5089 template_match: 0
- PE3R2X confidence: 88.9371
- PE32X confidence: 78.1385
- PE3R2 confidence: 77.5444
- PE3R2Y confidence: 76.1448
- P63R2X confidence: 72.9016
- FE3R2X confidence: 72.1147
- PE32 confidence: 66.7458
- PE32Y confidence: 65.3462
- P632X confidence: 62.1031
- P63R2 confidence: 61.5089
```
@@ -41,20 +40,19 @@ user@linux:~/openalpr$ alpr --help
USAGE:
alpr [-t <region code>] [-r <runtime_dir>] [-n <topN>]
[--seek <integer_ms>] [-c <country_code>]
[--clock] [-d] [-j] [--] [--version] [-h]
<image_file_path>
alpr [-c <country_code>] [--config <config_file>] [-n <topN>] [--seek
<integer_ms>] [-t <region code>] [--clock] [-d] [-j] [--]
[--version] [-h] <image_file_path>
Where:
-t <region code>, --template_region <region code>
Attempt to match the plate number against a region template (e.g., md
for Maryland, ca for California)
-c <country_code>, --country <country_code>
Country code to identify (either us for USA or eu for Europe).
Default=us
-r <runtime_dir>, --runtime_dir <runtime_dir>
Path to the OpenAlpr runtime data directory
--config <config_file>
Path to the openalpr.conf file
-n <topN>, --topn <topN>
Max number of possible plate numbers to return. Default=10
@@ -62,9 +60,9 @@ Where:
--seek <integer_ms>
Seek to the specied millisecond in a video file. Default=0
-c <country_code>, --country <country_code>
Country code to identify (either us for USA or eu for Europe).
Default=us
-t <region code>, --template_region <region code>
Attempt to match the plate number against a region template (e.g., md
for Maryland, ca for California)
--clock
Measure/print the total time to process image and all plates.
@@ -86,10 +84,11 @@ Where:
Displays usage information and exits.
<image_file_path>
(required) Image containing license plates
Image containing license plates
OpenAlpr Command Line Utility
```
@@ -109,6 +108,8 @@ Install OpenALPR on Ubuntu 14.04 x64 with the following commands:
Compiling
-----------
[![Build Status](https://travis-ci.org/openalpr/openalpr.svg?branch=master)](https://travis-ci.org/openalpr/openalpr)
OpenALPR compiles and runs on Linux, Mac OSX and Windows.
OpenALPR requires the following additional libraries:

View File

@@ -1,7 +1,7 @@
[common]
; Specify the path to the runtime data directory
runtime_dir = /usr/share/openalpr/runtime_data
runtime_dir = ${CMAKE_INSTALL_PREFIX}/share/openalpr/runtime_data
ocr_img_size_percent = 1.33333333
@@ -83,6 +83,8 @@ segmentation_max_segment_width_percent_vs_average = 1.35;
plate_width_mm = 304.8
plate_height_mm = 152.4
multiline = 0
char_height_mm = 70
char_width_mm = 35
char_whitespace_top_mm = 38
@@ -102,6 +104,7 @@ min_plate_size_height_px = 35
ocr_language = lus
[eu]
; One-line European style plates
; 35-50; 45-60, 55-70, 65-80, 75-90
char_analysis_min_pct = 0.35
@@ -116,6 +119,8 @@ segmentation_max_segment_width_percent_vs_average = 2.0;
plate_width_mm = 520
plate_height_mm = 110
multiline = 0
char_height_mm = 80
char_width_mm = 53
char_whitespace_top_mm = 10

View File

@@ -1,6 +1,6 @@
project(src)
#set(CMAKE_BUILD_TYPE Debug)
cmake_minimum_required (VERSION 2.6)
# Set the OpenALPR version in cmake, and also add it as a DEFINE for the code to access
@@ -15,6 +15,19 @@ add_definitions( -DOPENALPR_PATCH_VERSION=${OPENALPR_PATCH_VERSION})
SET(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake_modules/")
# TODO: switch to http://www.cmake.org/cmake/help/v2.8.5/cmake.html#module:GNUInstallDirs ?
IF (NOT CMAKE_INSTALL_SYSCONFDIR)
SET(CMAKE_INSTALL_SYSCONFDIR "${CMAKE_INSTALL_PREFIX}/etc")
ENDIF()
IF ( NOT DEFINED WITH_DAEMON )
SET(WITH_DAEMON ON)
ENDIF()
IF (WIN32 AND WITH_DAEMON)
MESSAGE(WARNING "Skipping alprd daemon installation, as it is not supported in Windows.")
SET(WITH_DAEMON OFF)
ENDIF()
FIND_PACKAGE( Tesseract REQUIRED )
@@ -64,7 +77,7 @@ TARGET_LINK_LIBRARIES(alpr
)
# Compile the alprd library on Unix-based OS
IF (NOT WIN32)
IF (WITH_DAEMON)
ADD_EXECUTABLE( alprd daemon.cpp daemon/beanstalk.c daemon/beanstalk.cc )
TARGET_LINK_LIBRARIES(alprd
@@ -88,12 +101,18 @@ add_subdirectory(openalpr)
add_subdirectory(video)
install (TARGETS alpr DESTINATION /usr/bin)
install (TARGETS alprd DESTINATION /usr/bin)
install (FILES ${CMAKE_SOURCE_DIR}/../doc/man/alpr.1 DESTINATION /usr/share/man/man1 COMPONENT doc)
install (DIRECTORY ${CMAKE_SOURCE_DIR}/../runtime_data DESTINATION /usr/share/openalpr/)
install (FILES ${CMAKE_SOURCE_DIR}/../config/openalpr.conf DESTINATION /etc/openalpr/ COMPONENT config)
install (FILES ${CMAKE_SOURCE_DIR}/../config/alprd.conf DESTINATION /etc/openalpr/ COMPONENT config)
install (TARGETS alpr DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
install (FILES ${CMAKE_SOURCE_DIR}/../doc/man/alpr.1 DESTINATION ${CMAKE_INSTALL_PREFIX}/share/man/man1 COMPONENT doc)
install (DIRECTORY ${CMAKE_SOURCE_DIR}/../runtime_data DESTINATION ${CMAKE_INSTALL_PREFIX}/share/openalpr)
# set runtime_data to reflect the current CMAKE_INSTALL_PREFIX
CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/../config/openalpr.conf.in ${CMAKE_CURRENT_BINARY_DIR}/config/openalpr.conf)
install (FILES ${CMAKE_CURRENT_BINARY_DIR}/config/openalpr.conf DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/openalpr/ COMPONENT config)
IF (WITH_DAEMON)
install (TARGETS alprd DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
install (FILES ${CMAKE_SOURCE_DIR}/../config/alprd.conf DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/openalpr COMPONENT config)
ENDIF()
SET(CPACK_PACKAGE_VERSION ${OPENALPR_VERSION})
@@ -107,7 +126,7 @@ SET(CPACK_STRIP_FILES "1")
SET (CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
SET (CPACK_DEBIAN_PACKAGE_SECTION "video")
SET (CPACK_DEBIAN_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR})
SET (CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.1.3), libgcc1 (>= 4.1.1), libtesseract3 (>= 3.0.3), libopencv-core2.4 (>= 2.4.8), libopencv-objdetect2.4 (>= 2.4.8), libopencv-highgui2.4 (>= 2.4.8), libopencv-imgproc2.4 (>= 2.4.8), libopencv-flann2.4 (>= 2.4.8), libopencv-features2d2.4 (>= 2.4.8), libzmq1, liblog4cplus-1.0-4, libcurl3, beanstalkd")
SET (CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.1.3), libgcc1 (>= 4.1.1), libtesseract3 (>= 3.0.3), libopencv-core2.4 (>= 2.4.8), libopencv-objdetect2.4 (>= 2.4.8), libopencv-highgui2.4 (>= 2.4.8), libopencv-imgproc2.4 (>= 2.4.8), libopencv-flann2.4 (>= 2.4.8), libopencv-features2d2.4 (>= 2.4.8), libopencv-video2.4 (>= 2.4.8), libopencv-gpu2.4 (>=2.4.8), liblog4cplus-1.0-4, libcurl3, beanstalkd")
SET (CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/../LICENSE")
SET (CPACK_PACKAGE_DESCRIPTION "OpenALPR - Open Source Automatic License Plate Recognition")
@@ -118,3 +137,14 @@ SET (CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}_${CPACK_PACKAGE_VERSION}_${C
SET (CPACK_COMPONENTS_ALL Libraries ApplicationData)
INCLUDE(CPack)
# ----------------------------------------------------------------------------
# Uninstall target, for "make uninstall"
# http://www.cmake.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F
# ----------------------------------------------------------------------------
CONFIGURE_FILE(
"${CMAKE_MODULE_PATH}/templates/cmake_uninstall.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake"
@ONLY)
ADD_CUSTOM_TARGET(uninstall COMMAND ${CMAKE_COMMAND} -P "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake")

View File

@@ -0,0 +1,21 @@
if(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt")
message(FATAL_ERROR "Cannot find install manifest: @CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt")
endif(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt")
file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files)
string(REGEX REPLACE "\n" ";" files "${files}")
foreach(file ${files})
message(STATUS "Uninstalling $ENV{DESTDIR}${file}")
if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}")
exec_program(
"@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\""
OUTPUT_VARIABLE rm_out
RETURN_VALUE rm_retval
)
if(NOT "${rm_retval}" STREQUAL 0)
message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}")
endif(NOT "${rm_retval}" STREQUAL 0)
else(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}")
message(STATUS "File $ENV{DESTDIR}${file} does not exist.")
endif(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}")
endforeach(file)

View File

@@ -2,6 +2,7 @@
#include <unistd.h>
#include <sstream>
#include <execinfo.h>
#include "daemon/beanstalk.hpp"
#include "video/logging_videobuffer.h"
@@ -34,6 +35,7 @@ const std::string BEANSTALK_QUEUE_HOST="127.0.0.1";
const int BEANSTALK_PORT=11300;
const std::string BEANSTALK_TUBE_NAME="alprd";
struct CaptureThreadData
{
std::string stream_url;
@@ -54,12 +56,26 @@ struct UploadThreadData
std::string upload_url;
};
void segfault_handler(int sig) {
void *array[10];
size_t size;
// get void*'s for all entries on the stack
size = backtrace(array, 10);
// print out all the frames to stderr
fprintf(stderr, "Error: signal %d:\n", sig);
backtrace_symbols_fd(array, size, STDERR_FILENO);
exit(1);
}
bool daemon_active;
static log4cplus::Logger logger;
int main( int argc, const char** argv )
{
signal(SIGSEGV, segfault_handler); // install our segfault handler
daemon_active = true;
bool noDaemon = false;
@@ -269,14 +285,15 @@ void streamRecognitionThread(void* arg)
{
long epoch_time = getEpochTime();
std::stringstream uuid;
uuid << tdata->site_id << "-cam" << tdata->camera_id << "-" << epoch_time;
std::stringstream uuid_ss;
uuid_ss << tdata->site_id << "-cam" << tdata->camera_id << "-" << epoch_time;
std::string uuid = uuid_ss.str();
// Save the image to disk (using the UUID)
if (tdata->output_images)
{
std::stringstream ss;
ss << tdata->output_image_folder << "/" << uuid.str() << ".jpg";
ss << tdata->output_image_folder << "/" << uuid << ".jpg";
cv::imwrite(ss.str(), latestFrame);
}
@@ -286,7 +303,7 @@ void streamRecognitionThread(void* arg)
std::string json = alpr.toJson(results);
cJSON *root = cJSON_Parse(json.c_str());
cJSON_AddStringToObject(root, "uuid", uuid.str().c_str());
cJSON_AddStringToObject(root, "uuid", uuid.c_str());
cJSON_AddNumberToObject(root, "camera_id", tdata->camera_id);
cJSON_AddStringToObject(root, "site_id", tdata->site_id.c_str());
cJSON_AddNumberToObject(root, "img_width", latestFrame.cols);

View File

@@ -38,11 +38,11 @@ const std::string MAIN_WINDOW_NAME = "ALPR main window";
const bool SAVE_LAST_VIDEO_STILL = false;
const std::string LAST_VIDEO_STILL_LOCATION = "/tmp/laststill.jpg";
/** Function Headers */
bool detectandshow(Alpr* alpr, cv::Mat frame, std::string region, bool writeJson);
bool measureProcessingTime = false;
std::string templateRegion;
// This boolean is set to false when the user hits terminates (e.g., CTRL+C )
// so we can end infinite loops for things like video processing.
@@ -55,7 +55,6 @@ int main( int argc, const char** argv )
bool outputJson = false;
int seektoms = 0;
bool detectRegion = false;
std::string templateRegion;
std::string country;
int topn;
@@ -296,7 +295,7 @@ bool detectandshow( Alpr* alpr, cv::Mat frame, std::string region, bool writeJso
{
for (int i = 0; i < results.plates.size(); i++)
{
std::cout << "plate" << i << ": " << results.plates[i].result_count << " results";
std::cout << "plate" << i << ": " << results.plates[i].topNPlates.size() << " results";
if (measureProcessingTime)
std::cout << " -- Processing Time = " << results.plates[i].processing_time_ms << "ms.";
std::cout << std::endl;
@@ -304,7 +303,11 @@ bool detectandshow( Alpr* alpr, cv::Mat frame, std::string region, bool writeJso
for (int k = 0; k < results.plates[i].topNPlates.size(); k++)
{
std::cout << " - " << results.plates[i].topNPlates[k].characters << "\t confidence: " << results.plates[i].topNPlates[k].overall_confidence << "\t template_match: " << results.plates[i].topNPlates[k].matches_template << std::endl;
std::cout << " - " << results.plates[i].topNPlates[k].characters << "\t confidence: " << results.plates[i].topNPlates[k].overall_confidence;
if (templateRegion.size() > 0)
std::cout << "\t template_match: " << results.plates[i].topNPlates[k].matches_template;
std::cout << std::endl;
}
}
}

View File

@@ -116,14 +116,15 @@ int main( int argc, const char** argv )
CharacterRegion charRegion(&pipeline_data);
if (abs(charRegion.getTopLine().angle) > 4)
if (pipeline_data.textLines.size() > 0 &&
abs(pipeline_data.textLines[0].angle) > 4)
{
// Rotate image:
Mat rotated(frame.size(), frame.type());
Mat rot_mat( 2, 3, CV_32FC1 );
Point center = Point( frame.cols/2, frame.rows/2 );
rot_mat = getRotationMatrix2D( center, charRegion.getTopLine().angle, 1.0 );
rot_mat = getRotationMatrix2D( center, pipeline_data.textLines[0].angle, 1.0 );
warpAffine( frame, rotated, rot_mat, frame.size() );
rotated.copyTo(frame);

View File

@@ -134,14 +134,14 @@ int main( int argc, const char** argv )
CharacterRegion regionizer(&pipeline_data);
if (abs(regionizer.getTopLine().angle) > 4)
if (abs(pipeline_data.textLines[0].angle) > 4)
{
// Rotate image:
Mat rotated(frame.size(), frame.type());
Mat rot_mat( 2, 3, CV_32FC1 );
Point center = Point( frame.cols/2, frame.rows/2 );
rot_mat = getRotationMatrix2D( center, regionizer.getTopLine().angle, 1.0 );
rot_mat = getRotationMatrix2D( center, pipeline_data.textLines[0].angle, 1.0 );
warpAffine( frame, rotated, rot_mat, frame.size() );
rotated.copyTo(frame);

View File

@@ -64,7 +64,7 @@ static int xPos1 = 0;
static int yPos1 = 0;
static int xPos2 = 0;
static int yPos2 = 0;
const float ASPECT_RATIO = 4.33333;
const float ASPECT_RATIO = 1.404;
static bool rdragging = false;
static int rDragStartX = 0;

View File

@@ -1,7 +1,6 @@
set(lpr_source_files
alpr.cpp
alpr_impl.cpp
@@ -22,7 +21,11 @@ set(lpr_source_files
segmentation/verticalhistogram.cpp
platecorners.cpp
colorfilter.cpp
characteranalysis.cpp
textdetection/characteranalysis.cpp
textdetection/platemask.cpp
textdetection/textcontours.cpp
textdetection/textline.cpp
textdetection/linefinder.cpp
pipeline_data.cpp
trex.c
cjson.c
@@ -39,15 +42,15 @@ add_library(openalpr SHARED ${lpr_source_files} )
set_target_properties(openalpr PROPERTIES SOVERSION ${OPENALPR_MAJOR_VERSION})
TARGET_LINK_LIBRARIES(openalpr
TARGET_LINK_LIBRARIES(openalpr
support
${OpenCV_LIBS}
${Tesseract_LIBRARIES}
)
)
install (FILES alpr.h DESTINATION /usr/include)
install (TARGETS openalpr DESTINATION /usr/lib)
install (FILES alpr.h DESTINATION ${CMAKE_INSTALL_PREFIX}/include)
install (TARGETS openalpr DESTINATION ${CMAKE_INSTALL_PREFIX}/lib)
# Add definition for default config file
add_definitions(-DDEFAULT_CONFIG_FILE="/etc/openalpr/openalpr.conf")
add_definitions(-DDEFAULT_CONFIG_FILE="${CMAKE_INSTALL_SYSCONFDIR}/openalpr/openalpr.conf")

View File

@@ -1,952 +0,0 @@
/*
* Copyright (c) 2014 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 "characteranalysis.h"
using namespace cv;
using namespace std;
CharacterAnalysis::CharacterAnalysis(PipelineData* pipeline_data)
{
this->pipeline_data = pipeline_data;
this->config = pipeline_data->config;
this->hasPlateMask = false;
if (this->config->debugCharAnalysis)
cout << "Starting CharacterAnalysis identification" << endl;
}
CharacterAnalysis::~CharacterAnalysis()
{
}
void CharacterAnalysis::analyze()
{
pipeline_data->clearThresholds();
pipeline_data->thresholds = produceThresholds(pipeline_data->crop_gray, config);
timespec startTime;
getTime(&startTime);
for (uint i = 0; i < pipeline_data->thresholds.size(); i++)
{
vector<vector<Point> > contours;
vector<Vec4i> hierarchy;
Mat tempThreshold(pipeline_data->thresholds[i].size(), CV_8U);
pipeline_data->thresholds[i].copyTo(tempThreshold);
findContours(tempThreshold,
contours, // a vector of contours
hierarchy,
CV_RETR_TREE, // retrieve all contours
CV_CHAIN_APPROX_SIMPLE ); // all pixels of each contours
allContours.push_back(contours);
allHierarchy.push_back(hierarchy);
}
if (config->debugTiming)
{
timespec endTime;
getTime(&endTime);
cout << " -- Character Analysis Find Contours Time: " << diffclock(startTime, endTime) << "ms." << endl;
}
//Mat img_equalized = equalizeBrightness(img_gray);
getTime(&startTime);
for (uint i = 0; i < pipeline_data->thresholds.size(); i++)
{
vector<bool> goodIndices = this->filter(pipeline_data->thresholds[i], allContours[i], allHierarchy[i]);
charSegments.push_back(goodIndices);
if (config->debugCharAnalysis)
cout << "Threshold " << i << " had " << getGoodIndicesCount(goodIndices) << " good indices." << endl;
}
if (config->debugTiming)
{
timespec endTime;
getTime(&endTime);
cout << " -- Character Analysis Filter Time: " << diffclock(startTime, endTime) << "ms." << endl;
}
this->plateMask = findOuterBoxMask();
if (hasPlateMask)
{
// Filter out bad contours now that we have an outer box mask...
for (uint i = 0; i < pipeline_data->thresholds.size(); i++)
{
charSegments[i] = filterByOuterMask(allContours[i], allHierarchy[i], charSegments[i]);
}
}
int bestFitScore = -1;
int bestFitIndex = -1;
for (uint i = 0; i < pipeline_data->thresholds.size(); i++)
{
//vector<bool> goodIndices = this->filter(thresholds[i], allContours[i], allHierarchy[i]);
//charSegments.push_back(goodIndices);
int segmentCount = getGoodIndicesCount(charSegments[i]);
if (segmentCount > bestFitScore)
{
bestFitScore = segmentCount;
bestFitIndex = i;
bestCharSegments = charSegments[i];
bestThreshold = pipeline_data->thresholds[i];
bestContours = allContours[i];
bestHierarchy = allHierarchy[i];
bestCharSegmentsCount = segmentCount;
}
}
if (this->config->debugCharAnalysis)
cout << "Best fit score: " << bestFitScore << " Index: " << bestFitIndex << endl;
if (bestFitScore <= 1)
return;
//getColorMask(img, allContours, allHierarchy, charSegments);
if (this->config->debugCharAnalysis)
{
Mat img_contours(bestThreshold.size(), CV_8U);
bestThreshold.copyTo(img_contours);
cvtColor(img_contours, img_contours, CV_GRAY2RGB);
vector<vector<Point> > allowedContours;
for (uint i = 0; i < bestContours.size(); i++)
{
if (bestCharSegments[i])
allowedContours.push_back(bestContours[i]);
}
drawContours(img_contours, bestContours,
-1, // draw all contours
cv::Scalar(255,0,0), // in blue
1); // with a thickness of 1
drawContours(img_contours, allowedContours,
-1, // draw all contours
cv::Scalar(0,255,0), // in green
1); // with a thickness of 1
displayImage(config, "Matching Contours", img_contours);
}
//charsegments = this->getPossibleCharRegions(img_threshold, allContours, allHierarchy, STARTING_MIN_HEIGHT + (bestFitIndex * HEIGHT_STEP), STARTING_MAX_HEIGHT + (bestFitIndex * HEIGHT_STEP));
this->linePolygon = getBestVotedLines(pipeline_data->crop_gray, bestContours, bestCharSegments);
if (this->linePolygon.size() > 0)
{
this->topLine = LineSegment(this->linePolygon[0].x, this->linePolygon[0].y, this->linePolygon[1].x, this->linePolygon[1].y);
this->bottomLine = LineSegment(this->linePolygon[3].x, this->linePolygon[3].y, this->linePolygon[2].x, this->linePolygon[2].y);
//this->charArea = getCharSegmentsBetweenLines(bestThreshold, bestContours, this->linePolygon);
filterBetweenLines(bestThreshold, bestContours, bestHierarchy, linePolygon, bestCharSegments);
this->charArea = getCharArea();
if (this->charArea.size() > 0)
{
this->charBoxTop = LineSegment(this->charArea[0].x, this->charArea[0].y, this->charArea[1].x, this->charArea[1].y);
this->charBoxBottom = LineSegment(this->charArea[3].x, this->charArea[3].y, this->charArea[2].x, this->charArea[2].y);
this->charBoxLeft = LineSegment(this->charArea[3].x, this->charArea[3].y, this->charArea[0].x, this->charArea[0].y);
this->charBoxRight = LineSegment(this->charArea[2].x, this->charArea[2].y, this->charArea[1].x, this->charArea[1].y);
}
}
this->thresholdsInverted = isPlateInverted();
}
int CharacterAnalysis::getGoodIndicesCount(vector<bool> goodIndices)
{
int count = 0;
for (uint i = 0; i < goodIndices.size(); i++)
{
if (goodIndices[i])
count++;
}
return count;
}
Mat CharacterAnalysis::findOuterBoxMask()
{
double min_parent_area = config->templateHeightPx * config->templateWidthPx * 0.10; // Needs to be at least 10% of the plate area to be considered.
int winningIndex = -1;
int winningParentId = -1;
int bestCharCount = 0;
double lowestArea = 99999999999999;
if (this->config->debugCharAnalysis)
cout << "CharacterAnalysis::findOuterBoxMask" << endl;
for (uint imgIndex = 0; imgIndex < allContours.size(); imgIndex++)
{
//vector<bool> charContours = filter(thresholds[imgIndex], allContours[imgIndex], allHierarchy[imgIndex]);
int charsRecognized = 0;
int parentId = -1;
bool hasParent = false;
for (uint i = 0; i < charSegments[imgIndex].size(); i++)
{
if (charSegments[imgIndex][i]) charsRecognized++;
if (charSegments[imgIndex][i] && allHierarchy[imgIndex][i][3] != -1)
{
parentId = allHierarchy[imgIndex][i][3];
hasParent = true;
}
}
if (charsRecognized == 0)
continue;
if (hasParent)
{
double boxArea = contourArea(allContours[imgIndex][parentId]);
if (boxArea < min_parent_area)
continue;
if ((charsRecognized > bestCharCount) ||
(charsRecognized == bestCharCount && boxArea < lowestArea))
//(boxArea < lowestArea)
{
bestCharCount = charsRecognized;
winningIndex = imgIndex;
winningParentId = parentId;
lowestArea = boxArea;
}
}
}
if (this->config->debugCharAnalysis)
cout << "Winning image index (findOuterBoxMask) is: " << winningIndex << endl;
if (winningIndex != -1 && bestCharCount >= 3)
{
int longestChildIndex = -1;
double longestChildLength = 0;
// Find the child with the longest permiter/arc length ( just for kicks)
for (uint i = 0; i < allContours[winningIndex].size(); i++)
{
for (uint j = 0; j < allContours[winningIndex].size(); j++)
{
if (allHierarchy[winningIndex][j][3] == winningParentId)
{
double arclength = arcLength(allContours[winningIndex][j], false);
if (arclength > longestChildLength)
{
longestChildIndex = j;
longestChildLength = arclength;
}
}
}
}
Mat mask = Mat::zeros(pipeline_data->thresholds[winningIndex].size(), CV_8U);
// get rid of the outline by drawing a 1 pixel width black line
drawContours(mask, allContours[winningIndex],
winningParentId, // draw this contour
cv::Scalar(255,255,255), // in
CV_FILLED,
8,
allHierarchy[winningIndex],
0
);
// Morph Open the mask to get rid of any little connectors to non-plate portions
int morph_elem = 2;
int morph_size = 3;
Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) );
//morphologyEx( mask, mask, MORPH_CLOSE, element );
morphologyEx( mask, mask, MORPH_OPEN, element );
//morph_size = 1;
//element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) );
//dilate(mask, mask, element);
// Drawing the edge black effectively erodes the image. This may clip off some extra junk from the edges.
// We'll want to do the contour again and find the larges one so that we remove the clipped portion.
vector<vector<Point> > contoursSecondRound;
findContours(mask, contoursSecondRound, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
int biggestContourIndex = -1;
double largestArea = 0;
for (uint c = 0; c < contoursSecondRound.size(); c++)
{
double area = contourArea(contoursSecondRound[c]);
if (area > largestArea)
{
biggestContourIndex = c;
largestArea = area;
}
}
if (biggestContourIndex != -1)
{
mask = Mat::zeros(pipeline_data->thresholds[winningIndex].size(), CV_8U);
vector<Point> smoothedMaskPoints;
approxPolyDP(contoursSecondRound[biggestContourIndex], smoothedMaskPoints, 2, true);
vector<vector<Point> > tempvec;
tempvec.push_back(smoothedMaskPoints);
//fillPoly(mask, smoothedMaskPoints.data(), smoothedMaskPoints, Scalar(255,255,255));
drawContours(mask, tempvec,
0, // draw this contour
cv::Scalar(255,255,255), // in
CV_FILLED,
8,
allHierarchy[winningIndex],
0
);
}
if (this->config->debugCharAnalysis)
{
vector<Mat> debugImgs;
Mat debugImgMasked = Mat::zeros(pipeline_data->thresholds[winningIndex].size(), CV_8U);
pipeline_data->thresholds[winningIndex].copyTo(debugImgMasked, mask);
debugImgs.push_back(mask);
debugImgs.push_back(pipeline_data->thresholds[winningIndex]);
debugImgs.push_back(debugImgMasked);
Mat dashboard = drawImageDashboard(debugImgs, CV_8U, 1);
displayImage(config, "Winning outer box", dashboard);
}
hasPlateMask = true;
return mask;
}
hasPlateMask = false;
Mat fullMask = Mat::zeros(pipeline_data->thresholds[0].size(), CV_8U);
bitwise_not(fullMask, fullMask);
return fullMask;
}
Mat CharacterAnalysis::getCharacterMask()
{
Mat charMask = Mat::zeros(bestThreshold.size(), CV_8U);
for (uint i = 0; i < bestContours.size(); i++)
{
if (bestCharSegments[i] == false)
continue;
drawContours(charMask, bestContours,
i, // draw this contour
cv::Scalar(255,255,255), // in
CV_FILLED,
8,
bestHierarchy,
1
);
}
return charMask;
}
// Returns a polygon "stripe" across the width of the character region. The lines are voted and the polygon starts at 0 and extends to image width
vector<Point> CharacterAnalysis::getBestVotedLines(Mat img, vector<vector<Point> > contours, vector<bool> goodIndices)
{
//if (this->debug)
// cout << "CharacterAnalysis::getBestVotedLines" << endl;
vector<Point> bestStripe;
vector<Rect> charRegions;
for (uint i = 0; i < contours.size(); i++)
{
if (goodIndices[i])
charRegions.push_back(boundingRect(contours[i]));
}
// Find the best fit line segment that is parallel with the most char segments
if (charRegions.size() <= 1)
{
// Maybe do something about this later, for now let's just ignore
}
else
{
vector<LineSegment> topLines;
vector<LineSegment> bottomLines;
// Iterate through each possible char and find all possible lines for the top and bottom of each char segment
for (uint i = 0; i < charRegions.size() - 1; i++)
{
for (uint k = i+1; k < charRegions.size(); k++)
{
//Mat tempImg;
//result.copyTo(tempImg);
Rect* leftRect;
Rect* rightRect;
if (charRegions[i].x < charRegions[k].x)
{
leftRect = &charRegions[i];
rightRect = &charRegions[k];
}
else
{
leftRect = &charRegions[k];
rightRect = &charRegions[i];
}
//rectangle(tempImg, *leftRect, Scalar(0, 255, 0), 2);
//rectangle(tempImg, *rightRect, Scalar(255, 255, 255), 2);
int x1, y1, x2, y2;
if (leftRect->y > rightRect->y) // Rising line, use the top left corner of the rect
{
x1 = leftRect->x;
x2 = rightRect->x;
}
else // falling line, use the top right corner of the rect
{
x1 = leftRect->x + leftRect->width;
x2 = rightRect->x + rightRect->width;
}
y1 = leftRect->y;
y2 = rightRect->y;
//cv::line(tempImg, Point(x1, y1), Point(x2, y2), Scalar(0, 0, 255));
topLines.push_back(LineSegment(x1, y1, x2, y2));
if (leftRect->y > rightRect->y) // Rising line, use the bottom right corner of the rect
{
x1 = leftRect->x + leftRect->width;
x2 = rightRect->x + rightRect->width;
}
else // falling line, use the bottom left corner of the rect
{
x1 = leftRect->x;
x2 = rightRect->x;
}
y1 = leftRect->y + leftRect->height;
y2 = rightRect->y + leftRect->height;
//cv::line(tempImg, Point(x1, y1), Point(x2, y2), Scalar(0, 0, 255));
bottomLines.push_back(LineSegment(x1, y1, x2, y2));
//drawAndWait(&tempImg);
}
}
int bestScoreIndex = 0;
int bestScore = -1;
int bestScoreDistance = -1; // Line segment distance is used as a tie breaker
// Now, among all possible lines, find the one that is the best fit
for (uint i = 0; i < topLines.size(); i++)
{
float SCORING_MIN_THRESHOLD = 0.97;
float SCORING_MAX_THRESHOLD = 1.03;
int curScore = 0;
for (uint charidx = 0; charidx < charRegions.size(); charidx++)
{
float topYPos = topLines[i].getPointAt(charRegions[charidx].x);
float botYPos = bottomLines[i].getPointAt(charRegions[charidx].x);
float minTop = charRegions[charidx].y * SCORING_MIN_THRESHOLD;
float maxTop = charRegions[charidx].y * SCORING_MAX_THRESHOLD;
float minBot = (charRegions[charidx].y + charRegions[charidx].height) * SCORING_MIN_THRESHOLD;
float maxBot = (charRegions[charidx].y + charRegions[charidx].height) * SCORING_MAX_THRESHOLD;
if ( (topYPos >= minTop && topYPos <= maxTop) &&
(botYPos >= minBot && botYPos <= maxBot))
{
curScore++;
}
//cout << "Slope: " << topslope << " yPos: " << topYPos << endl;
//drawAndWait(&tempImg);
}
// Tie goes to the one with longer line segments
if ((curScore > bestScore) ||
(curScore == bestScore && topLines[i].length > bestScoreDistance))
{
bestScore = curScore;
bestScoreIndex = i;
// Just use x distance for now
bestScoreDistance = topLines[i].length;
}
}
if (this->config->debugCharAnalysis)
{
cout << "The winning score is: " << bestScore << endl;
// Draw the winning line segment
//Mat tempImg;
//result.copyTo(tempImg);
//cv::line(tempImg, topLines[bestScoreIndex].p1, topLines[bestScoreIndex].p2, Scalar(0, 0, 255), 2);
//cv::line(tempImg, bottomLines[bestScoreIndex].p1, bottomLines[bestScoreIndex].p2, Scalar(0, 0, 255), 2);
//displayImage(config, "Lines", tempImg);
}
//winningLines.push_back(topLines[bestScoreIndex]);
//winningLines.push_back(bottomLines[bestScoreIndex]);
Point topLeft = Point(0, topLines[bestScoreIndex].getPointAt(0) );
Point topRight = Point(img.cols, topLines[bestScoreIndex].getPointAt(img.cols));
Point bottomRight = Point(img.cols, bottomLines[bestScoreIndex].getPointAt(img.cols));
Point bottomLeft = Point(0, bottomLines[bestScoreIndex].getPointAt(0));
bestStripe.push_back(topLeft);
bestStripe.push_back(topRight);
bestStripe.push_back(bottomRight);
bestStripe.push_back(bottomLeft);
}
return bestStripe;
}
vector<bool> CharacterAnalysis::filter(Mat img, vector<vector<Point> > contours, vector<Vec4i> hierarchy)
{
static int STARTING_MIN_HEIGHT = round (((float) img.rows) * config->charAnalysisMinPercent);
static int STARTING_MAX_HEIGHT = round (((float) img.rows) * (config->charAnalysisMinPercent + config->charAnalysisHeightRange));
static int HEIGHT_STEP = round (((float) img.rows) * config->charAnalysisHeightStepSize);
static int NUM_STEPS = config->charAnalysisNumSteps;
vector<bool> charSegments;
int bestFitScore = -1;
for (int i = 0; i < NUM_STEPS; i++)
{
int goodIndicesCount;
vector<bool> goodIndices(contours.size());
for (uint z = 0; z < goodIndices.size(); z++) goodIndices[z] = true;
goodIndices = this->filterByBoxSize(contours, goodIndices, STARTING_MIN_HEIGHT + (i * HEIGHT_STEP), STARTING_MAX_HEIGHT + (i * HEIGHT_STEP));
goodIndicesCount = getGoodIndicesCount(goodIndices);
if ( goodIndicesCount == 0 || goodIndicesCount <= bestFitScore) // Don't bother doing more filtering if we already lost...
continue;
goodIndices = this->filterContourHoles(contours, hierarchy, goodIndices);
goodIndicesCount = getGoodIndicesCount(goodIndices);
if ( goodIndicesCount == 0 || goodIndicesCount <= bestFitScore) // Don't bother doing more filtering if we already lost...
continue;
//goodIndices = this->filterByParentContour( contours, hierarchy, goodIndices);
vector<Point> lines = getBestVotedLines(img, contours, goodIndices);
goodIndices = this->filterBetweenLines(img, contours, hierarchy, lines, goodIndices);
int segmentCount = getGoodIndicesCount(goodIndices);
if (segmentCount > bestFitScore)
{
bestFitScore = segmentCount;
charSegments = goodIndices;
}
}
return charSegments;
}
// Goes through the contours for the plate and picks out possible char segments based on min/max height
vector<bool> CharacterAnalysis::filterByBoxSize(vector< vector< Point> > contours, vector<bool> goodIndices, int minHeightPx, int maxHeightPx)
{
float idealAspect=config->charWidthMM / config->charHeightMM;
float aspecttolerance=0.25;
vector<bool> includedIndices(contours.size());
for (uint j = 0; j < contours.size(); j++)
includedIndices.push_back(false);
for (uint i = 0; i < contours.size(); i++)
{
if (goodIndices[i] == false)
continue;
//Create bounding rect of object
Rect mr= boundingRect(contours[i]);
float minWidth = mr.height * 0.2;
//Crop image
//Mat auxRoi(img, mr);
if(mr.height >= minHeightPx && mr.height <= maxHeightPx && mr.width > minWidth)
{
float charAspect= (float)mr.width/(float)mr.height;
if (abs(charAspect - idealAspect) < aspecttolerance)
includedIndices[i] = true;
}
}
return includedIndices;
}
vector< bool > CharacterAnalysis::filterContourHoles(vector< vector< Point > > contours, vector< Vec4i > hierarchy, vector< bool > goodIndices)
{
vector<bool> includedIndices(contours.size());
for (uint j = 0; j < contours.size(); j++)
includedIndices.push_back(false);
for (uint i = 0; i < contours.size(); i++)
{
if (goodIndices[i] == false)
continue;
int parentIndex = hierarchy[i][3];
if (parentIndex >= 0 && goodIndices[parentIndex])
{
// this contour is a child of an already identified contour. REMOVE it
if (this->config->debugCharAnalysis)
{
cout << "filterContourHoles: contour index: " << i << endl;
}
}
else
{
includedIndices[i] = true;
}
}
return includedIndices;
}
// Goes through the contours for the plate and picks out possible char segments based on min/max height
// returns a vector of indices corresponding to valid contours
vector<bool> CharacterAnalysis::filterByParentContour( vector< vector< Point> > contours, vector<Vec4i> hierarchy, vector<bool> goodIndices)
{
vector<bool> includedIndices(contours.size());
for (uint j = 0; j < contours.size(); j++)
includedIndices[j] = false;
vector<int> parentIDs;
vector<int> votes;
for (uint i = 0; i < contours.size(); i++)
{
if (goodIndices[i] == false)
continue;
int voteIndex = -1;
int parentID = hierarchy[i][3];
// check if parentID is already in the lsit
for (uint j = 0; j < parentIDs.size(); j++)
{
if (parentIDs[j] == parentID)
{
voteIndex = j;
break;
}
}
if (voteIndex == -1)
{
parentIDs.push_back(parentID);
votes.push_back(1);
}
else
{
votes[voteIndex] = votes[voteIndex] + 1;
}
}
// Tally up the votes, pick the winner
int totalVotes = 0;
int winningParentId = 0;
int highestVotes = 0;
for (uint i = 0; i < parentIDs.size(); i++)
{
if (votes[i] > highestVotes)
{
winningParentId = parentIDs[i];
highestVotes = votes[i];
}
totalVotes += votes[i];
}
// Now filter out all the contours with a different parent ID (assuming the totalVotes > 2)
for (uint i = 0; i < contours.size(); i++)
{
if (goodIndices[i] == false)
continue;
if (totalVotes <= 2)
{
includedIndices[i] = true;
}
else if (hierarchy[i][3] == winningParentId)
{
includedIndices[i] = true;
}
}
return includedIndices;
}
vector<bool> CharacterAnalysis::filterBetweenLines(Mat img, vector<vector<Point> > contours, vector<Vec4i> hierarchy, vector<Point> outerPolygon, vector<bool> goodIndices)
{
static float MIN_AREA_PERCENT_WITHIN_LINES = 0.88;
static float MAX_DISTANCE_PERCENT_FROM_LINES = 0.15;
vector<bool> includedIndices(contours.size());
for (uint j = 0; j < contours.size(); j++)
includedIndices[j] = false;
if (outerPolygon.size() == 0)
return includedIndices;
vector<Point> validPoints;
// Figure out the line height
LineSegment topLine(outerPolygon[0].x, outerPolygon[0].y, outerPolygon[1].x, outerPolygon[1].y);
LineSegment bottomLine(outerPolygon[3].x, outerPolygon[3].y, outerPolygon[2].x, outerPolygon[2].y);
float x = ((float) img.cols) / 2;
Point midpoint = Point(x, bottomLine.getPointAt(x));
Point acrossFromMidpoint = topLine.closestPointOnSegmentTo(midpoint);
float lineHeight = distanceBetweenPoints(midpoint, acrossFromMidpoint);
// Create a white mask for the area inside the polygon
Mat outerMask = Mat::zeros(img.size(), CV_8U);
Mat innerArea(img.size(), CV_8U);
fillConvexPoly(outerMask, outerPolygon.data(), outerPolygon.size(), Scalar(255,255,255));
// For each contour, determine if enough of it is between the lines to qualify
for (uint i = 0; i < contours.size(); i++)
{
if (goodIndices[i] == false)
continue;
innerArea.setTo(Scalar(0,0,0));
drawContours(innerArea, contours,
i, // draw this contour
cv::Scalar(255,255,255), // in
CV_FILLED,
8,
hierarchy,
0
);
bitwise_and(innerArea, outerMask, innerArea);
vector<vector<Point> > tempContours;
findContours(innerArea, tempContours,
CV_RETR_EXTERNAL, // retrieve the external contours
CV_CHAIN_APPROX_SIMPLE ); // all pixels of each contours );
double totalArea = contourArea(contours[i]);
double areaBetweenLines = 0;
for (uint tempContourIdx = 0; tempContourIdx < tempContours.size(); tempContourIdx++)
{
areaBetweenLines += contourArea(tempContours[tempContourIdx]);
}
if (areaBetweenLines / totalArea < MIN_AREA_PERCENT_WITHIN_LINES)
{
// Not enough area is inside the lines.
continue;
}
// now check to make sure that the top and bottom of the contour are near enough to the lines
// First get the high and low point for the contour
// Remember that origin is top-left, so the top Y values are actually closer to 0.
int highPointIndex = 0;
int highPointValue = 999999999;
int lowPointIndex = 0;
int lowPointValue = 0;
for (uint cidx = 0; cidx < contours[i].size(); cidx++)
{
if (contours[i][cidx].y < highPointValue)
{
highPointIndex = cidx;
highPointValue = contours[i][cidx].y;
}
if (contours[i][cidx].y > lowPointValue)
{
lowPointIndex = cidx;
lowPointValue = contours[i][cidx].y;
}
}
// Get the absolute distance from the top and bottom lines
Point closestTopPoint = topLine.closestPointOnSegmentTo(contours[i][highPointIndex]);
Point closestBottomPoint = bottomLine.closestPointOnSegmentTo(contours[i][lowPointIndex]);
float absTopDistance = distanceBetweenPoints(closestTopPoint, contours[i][highPointIndex]);
float absBottomDistance = distanceBetweenPoints(closestBottomPoint, contours[i][lowPointIndex]);
float maxDistance = lineHeight * MAX_DISTANCE_PERCENT_FROM_LINES;
if (absTopDistance < maxDistance && absBottomDistance < maxDistance)
{
includedIndices[i] = true;
}
}
return includedIndices;
}
std::vector< bool > CharacterAnalysis::filterByOuterMask(vector< vector< Point > > contours, vector< Vec4i > hierarchy, std::vector< bool > goodIndices)
{
float MINIMUM_PERCENT_LEFT_AFTER_MASK = 0.1;
float MINIMUM_PERCENT_OF_CHARS_INSIDE_PLATE_MASK = 0.6;
if (hasPlateMask == false)
return goodIndices;
vector<bool> passingIndices;
for (uint i = 0; i < goodIndices.size(); i++)
passingIndices.push_back(false);
Mat tempMaskedContour = Mat::zeros(plateMask.size(), CV_8U);
Mat tempFullContour = Mat::zeros(plateMask.size(), CV_8U);
int charsInsideMask = 0;
int totalChars = 0;
for (uint i=0; i < goodIndices.size(); i++)
{
if (goodIndices[i] == false)
continue;
totalChars++;
drawContours(tempFullContour, contours, i, Scalar(255,255,255), CV_FILLED, 8, hierarchy);
bitwise_and(tempFullContour, plateMask, tempMaskedContour);
float beforeMaskWhiteness = mean(tempFullContour)[0];
float afterMaskWhiteness = mean(tempMaskedContour)[0];
if (afterMaskWhiteness / beforeMaskWhiteness > MINIMUM_PERCENT_LEFT_AFTER_MASK)
{
charsInsideMask++;
passingIndices[i] = true;
}
}
if (totalChars == 0)
return goodIndices;
// Check to make sure that this is a valid box. If the box is too small (e.g., 1 char is inside, and 3 are outside)
// then don't use this to filter.
float percentCharsInsideMask = ((float) charsInsideMask) / ((float) totalChars);
if (percentCharsInsideMask < MINIMUM_PERCENT_OF_CHARS_INSIDE_PLATE_MASK)
return goodIndices;
return passingIndices;
}
bool CharacterAnalysis::isPlateInverted()
{
Mat charMask = getCharacterMask();
Scalar meanVal = mean(bestThreshold, charMask)[0];
if (this->config->debugCharAnalysis)
cout << "CharacterAnalysis, plate inverted: MEAN: " << meanVal << " : " << bestThreshold.type() << endl;
if (meanVal[0] < 100) // Half would be 122.5. Give it a little extra oomf before saying it needs inversion. Most states aren't inverted.
return true;
return false;
}
bool CharacterAnalysis::verifySize(Mat r, float minHeightPx, float maxHeightPx)
{
//Char sizes 45x90
float aspect=config->charWidthMM / config->charHeightMM;
float charAspect= (float)r.cols/(float)r.rows;
float error=0.35;
//float minHeight=TEMPLATE_PLATE_HEIGHT * .35;
//float maxHeight=TEMPLATE_PLATE_HEIGHT * .65;
//We have a different aspect ratio for number 1, and it can be ~0.2
float minAspect=0.2;
float maxAspect=aspect+aspect*error;
//area of pixels
float area=countNonZero(r);
//bb area
float bbArea=r.cols*r.rows;
//% of pixel in area
float percPixels=area/bbArea;
//if(DEBUG)
//cout << "Aspect: "<< aspect << " ["<< minAspect << "," << maxAspect << "] " << "Area "<< percPixels <<" Char aspect " << charAspect << " Height char "<< r.rows << "\n";
if(percPixels < 0.8 && charAspect > minAspect && charAspect < maxAspect && r.rows >= minHeightPx && r.rows < maxHeightPx)
return true;
else
return false;
}
vector<Point> CharacterAnalysis::getCharArea()
{
const int MAX = 100000;
const int MIN= -1;
int leftX = MAX;
int rightX = MIN;
for (uint i = 0; i < bestContours.size(); i++)
{
if (bestCharSegments[i] == false)
continue;
for (uint z = 0; z < bestContours[i].size(); z++)
{
if (bestContours[i][z].x < leftX)
leftX = bestContours[i][z].x;
if (bestContours[i][z].x > rightX)
rightX = bestContours[i][z].x;
}
}
vector<Point> charArea;
if (leftX != MAX && rightX != MIN)
{
Point tl(leftX, topLine.getPointAt(leftX));
Point tr(rightX, topLine.getPointAt(rightX));
Point br(rightX, bottomLine.getPointAt(rightX));
Point bl(leftX, bottomLine.getPointAt(leftX));
charArea.push_back(tl);
charArea.push_back(tr);
charArea.push_back(br);
charArea.push_back(bl);
}
return charArea;
}

View File

@@ -1,90 +0,0 @@
/*
* Copyright (c) 2014 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/>.
*/
#ifndef OPENALPR_CHARACTERANALYSIS_H
#define OPENALPR_CHARACTERANALYSIS_H
#include "opencv2/imgproc/imgproc.hpp"
#include "constants.h"
#include "utility.h"
#include "config.h"
#include "pipeline_data.h"
class CharacterAnalysis
{
public:
CharacterAnalysis(PipelineData* pipeline_data);
virtual ~CharacterAnalysis();
bool hasPlateMask;
cv::Mat plateMask;
cv::Mat bestThreshold;
std::vector<std::vector<cv::Point> > bestContours;
std::vector<cv::Vec4i> bestHierarchy;
std::vector<bool> bestCharSegments;
int bestCharSegmentsCount;
LineSegment topLine;
LineSegment bottomLine;
std::vector<cv::Point> linePolygon;
std::vector<cv::Point> charArea;
LineSegment charBoxTop;
LineSegment charBoxBottom;
LineSegment charBoxLeft;
LineSegment charBoxRight;
bool thresholdsInverted;
std::vector<std::vector<std::vector<cv::Point> > > allContours;
std::vector<std::vector<cv::Vec4i> > allHierarchy;
std::vector<std::vector<bool> > charSegments;
void analyze();
cv::Mat getCharacterMask();
private:
PipelineData* pipeline_data;
Config* config;
cv::Mat findOuterBoxMask( );
bool isPlateInverted();
std::vector<bool> filter(cv::Mat img, std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy);
std::vector<bool> filterByBoxSize(std::vector<std::vector<cv::Point> > contours, std::vector<bool> goodIndices, int minHeightPx, int maxHeightPx);
std::vector<bool> filterByParentContour( std::vector< std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, std::vector<bool> goodIndices);
std::vector<bool> filterContourHoles(std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, std::vector<bool> goodIndices);
std::vector<bool> filterByOuterMask(std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, std::vector<bool> goodIndices);
std::vector<cv::Point> getCharArea();
std::vector<cv::Point> getBestVotedLines(cv::Mat img, std::vector<std::vector<cv::Point> > contours, std::vector<bool> goodIndices);
//vector<Point> getCharSegmentsBetweenLines(Mat img, vector<vector<Point> > contours, vector<Point> outerPolygon);
std::vector<bool> filterBetweenLines(cv::Mat img, std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, std::vector<cv::Point> outerPolygon, std::vector<bool> goodIndices);
bool verifySize(cv::Mat r, float minHeightPx, float maxHeightPx);
int getGoodIndicesCount(std::vector<bool> goodIndices);
};
#endif // OPENALPR_CHARACTERANALYSIS_H

View File

@@ -38,9 +38,8 @@ CharacterRegion::CharacterRegion(PipelineData* pipeline_data)
charAnalysis = new CharacterAnalysis(pipeline_data);
charAnalysis->analyze();
pipeline_data->plate_inverted = charAnalysis->thresholdsInverted;
pipeline_data->plate_mask = charAnalysis->plateMask;
if (this->debug && charAnalysis->linePolygon.size() > 0)
if (this->debug && pipeline_data->textLines.size() > 0)
{
vector<Mat> tempDash;
for (uint z = 0; z < pipeline_data->thresholds.size(); z++)
@@ -59,30 +58,35 @@ CharacterRegion::CharacterRegion(PipelineData* pipeline_data)
for (uint z = 0; z < charAnalysis->bestContours.size(); z++)
{
Scalar dcolor(255,0,0);
if (charAnalysis->bestCharSegments[z])
if (charAnalysis->bestContours.goodIndices[z])
dcolor = Scalar(0,255,0);
drawContours(bestVal, charAnalysis->bestContours, z, dcolor, 1);
drawContours(bestVal, charAnalysis->bestContours.contours, z, dcolor, 1);
}
tempDash.push_back(bestVal);
displayImage(config, "Character Region Step 1 Thresholds", drawImageDashboard(tempDash, bestVal.type(), 3));
}
if (charAnalysis->linePolygon.size() > 0)
if (pipeline_data->textLines.size() > 0)
{
int confidenceDrainers = 0;
int charSegmentCount = charAnalysis->bestCharSegmentsCount;
int charSegmentCount = charAnalysis->bestContours.getGoodIndicesCount();
if (charSegmentCount == 1)
confidenceDrainers += 91;
else if (charSegmentCount < 5)
confidenceDrainers += (5 - charSegmentCount) * 10;
int absangle = abs(charAnalysis->topLine.angle);
// Use the angle for the first line -- assume they'll always be parallel for multi-line plates
int absangle = abs(pipeline_data->textLines[0].topLine.angle);
if (absangle > config->maxPlateAngleDegrees)
confidenceDrainers += 91;
else if (absangle > 1)
confidenceDrainers += (config->maxPlateAngleDegrees - absangle) ;
// If a multiline plate has only one line, disqualify
if (pipeline_data->isMultiline && pipeline_data->textLines.size() < 2)
confidenceDrainers += 95;
if (confidenceDrainers >= 100)
this->confidence=1;
else
@@ -103,38 +107,3 @@ CharacterRegion::~CharacterRegion()
}
LineSegment CharacterRegion::getTopLine()
{
return charAnalysis->topLine;
}
LineSegment CharacterRegion::getBottomLine()
{
return charAnalysis->bottomLine;
}
vector<Point> CharacterRegion::getCharArea()
{
return charAnalysis->charArea;
}
LineSegment CharacterRegion::getCharBoxTop()
{
return charAnalysis->charBoxTop;
}
LineSegment CharacterRegion::getCharBoxBottom()
{
return charAnalysis->charBoxBottom;
}
LineSegment CharacterRegion::getCharBoxLeft()
{
return charAnalysis->charBoxLeft;
}
LineSegment CharacterRegion::getCharBoxRight()
{
return charAnalysis->charBoxRight;
}

View File

@@ -23,7 +23,7 @@
#include "opencv2/imgproc/imgproc.hpp"
#include "constants.h"
#include "utility.h"
#include "characteranalysis.h"
#include "textdetection/characteranalysis.h"
#include "config.h"
#include "pipeline_data.h"
@@ -37,14 +37,6 @@ class CharacterRegion
int confidence;
LineSegment getTopLine();
LineSegment getBottomLine();
std::vector<cv::Point> getCharArea();
LineSegment getCharBoxTop();
LineSegment getCharBoxBottom();
LineSegment getCharBoxLeft();
LineSegment getCharBoxRight();
protected:
@@ -54,20 +46,6 @@ class CharacterRegion
CharacterAnalysis *charAnalysis;
cv::Mat findOuterBoxMask(std::vector<cv::Mat> thresholds, std::vector<std::vector<std::vector<cv::Point> > > allContours, std::vector<std::vector<cv::Vec4i> > allHierarchy);
std::vector<bool> filter(cv::Mat img, std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy);
std::vector<bool> filterByBoxSize(cv::Mat img, std::vector<std::vector<cv::Point> > contours, std::vector<bool> goodIndices, float minHeightPx, float maxHeightPx);
std::vector<bool> filterByParentContour( std::vector< std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, std::vector<bool> goodIndices);
std::vector<bool> filterContourHoles(std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, std::vector<bool> goodIndices);
std::vector<cv::Point> getBestVotedLines(cv::Mat img, std::vector<std::vector<cv::Point> > contours, std::vector<bool> goodIndices);
//vector<Point> getCharSegmentsBetweenLines(Mat img, vector<vector<Point> > contours, vector<Point> outerPolygon);
std::vector<bool> filterBetweenLines(cv::Mat img, std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, std::vector<cv::Point> outerPolygon, std::vector<bool> goodIndices);
cv::Mat getCharacterMask(cv::Mat img, std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, std::vector<bool> goodIndices);
std::vector<cv::Rect> wrapContours(std::vector<std::vector<cv::Point> > contours);
bool verifySize(cv::Mat r, float minHeightPx, float maxHeightPx);
int getGoodIndicesCount(std::vector<bool> goodIndices);
bool isPlateInverted(cv::Mat threshold, std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, std::vector<bool> goodIndices);

View File

@@ -139,6 +139,8 @@ void Config::loadValues(string country)
minPlateSizeWidthPx = getInt(country, "min_plate_size_width_px", 100);
minPlateSizeHeightPx = getInt(country, "min_plate_size_height_px", 100);
multiline = getBoolean(country, "multiline", false);
plateWidthMM = getFloat(country, "plate_width_mm", 100);
plateHeightMM = getFloat(country, "plate_height_mm", 100);

View File

@@ -57,6 +57,8 @@ class Config
float minPlateSizeWidthPx;
float minPlateSizeHeightPx;
bool multiline;
float plateWidthMM;
float plateHeightMM;

View File

@@ -17,6 +17,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <opencv2/core/core.hpp>
#include "licenseplatecandidate.h"
using namespace std;
@@ -40,11 +42,10 @@ void LicensePlateCandidate::recognize()
charSegmenter = NULL;
pipeline_data->plate_area_confidence = 0;
pipeline_data->isMultiline = config->multiline;
int expandX = round(this->pipeline_data->regionOfInterest.width * 0.20);
int expandY = round(this->pipeline_data->regionOfInterest.height * 0.15);
// expand box by 15% in all directions
Rect expandedRegion = expandRect( this->pipeline_data->regionOfInterest, expandX, expandY, this->pipeline_data->grayImg.cols, this->pipeline_data->grayImg.rows) ;
Rect expandedRegion = this->pipeline_data->regionOfInterest;
pipeline_data->crop_gray = Mat(this->pipeline_data->grayImg, expandedRegion);
resize(pipeline_data->crop_gray, pipeline_data->crop_gray, Size(config->templateWidthPx, config->templateHeightPx));
@@ -54,24 +55,67 @@ void LicensePlateCandidate::recognize()
if (charRegion.confidence > 10)
{
PlateLines plateLines(config);
PlateLines plateLines(pipeline_data);
plateLines.processImage(pipeline_data->plate_mask, &charRegion, 1.10);
plateLines.processImage(pipeline_data->crop_gray, &charRegion, 0.9);
if (pipeline_data->hasPlateBorder)
plateLines.processImage(pipeline_data->plateBorderMask, 1.10);
PlateCorners cornerFinder(pipeline_data->crop_gray, &plateLines, &charRegion, config);
plateLines.processImage(pipeline_data->crop_gray, 0.9);
PlateCorners cornerFinder(pipeline_data->crop_gray, &plateLines, pipeline_data);
vector<Point> smallPlateCorners = cornerFinder.findPlateCorners();
if (cornerFinder.confidence > 0)
{
timespec startTime;
getTime(&startTime);
Mat originalCrop = pipeline_data->crop_gray;
pipeline_data->plate_corners = transformPointsToOriginalImage(this->pipeline_data->grayImg, pipeline_data->crop_gray, expandedRegion, smallPlateCorners);
pipeline_data->crop_gray = deSkewPlate(this->pipeline_data->grayImg, pipeline_data->plate_corners);
Size outputImageSize = getOutputImageSize(pipeline_data->plate_corners);
Mat transmtx = getTransformationMatrix(pipeline_data->plate_corners, outputImageSize);
pipeline_data->crop_gray = deSkewPlate(this->pipeline_data->grayImg, outputImageSize, transmtx);
// Apply a perspective transformation to the TextLine objects
// to match the newly deskewed license plate crop
vector<TextLine> newLines;
for (uint i = 0; i < pipeline_data->textLines.size(); i++)
{
vector<Point2f> textArea = transformPointsToOriginalImage(this->pipeline_data->grayImg, originalCrop, expandedRegion,
pipeline_data->textLines[i].textArea);
vector<Point2f> linePolygon = transformPointsToOriginalImage(this->pipeline_data->grayImg, originalCrop, expandedRegion,
pipeline_data->textLines[i].linePolygon);
vector<Point2f> textAreaRemapped;
vector<Point2f> linePolygonRemapped;
perspectiveTransform(textArea, textAreaRemapped, transmtx);
perspectiveTransform(linePolygon, linePolygonRemapped, transmtx);
newLines.push_back(TextLine(textAreaRemapped, linePolygonRemapped));
}
pipeline_data->textLines.clear();
for (uint i = 0; i < newLines.size(); i++)
pipeline_data->textLines.push_back(newLines[i]);
if (config->debugTiming)
{
timespec endTime;
getTime(&endTime);
cout << "deskew Time: " << diffclock(startTime, endTime) << "ms." << endl;
}
charSegmenter = new CharacterSegmenter(pipeline_data);
//this->recognizedText = ocr->recognizedText;
//strcpy(this->recognizedText, ocr.recognizedText);
pipeline_data->plate_area_confidence = 100;
}
@@ -97,13 +141,9 @@ vector<Point2f> LicensePlateCandidate::transformPointsToOriginalImage(Mat bigIma
return cornerPoints;
}
Mat LicensePlateCandidate::deSkewPlate(Mat inputImage, vector<Point2f> corners)
Size LicensePlateCandidate::getOutputImageSize(vector<Point2f> corners)
{
timespec startTime;
getTime(&startTime);
// Figure out the appoximate width/height of the license plate region, so we can maintain the aspect ratio.
// Figure out the approximate width/height of the license plate region, so we can maintain the aspect ratio.
LineSegment leftEdge(round(corners[3].x), round(corners[3].y), round(corners[0].x), round(corners[0].y));
LineSegment rightEdge(round(corners[2].x), round(corners[2].y), round(corners[1].x), round(corners[1].y));
LineSegment topEdge(round(corners[0].x), round(corners[0].y), round(corners[1].x), round(corners[1].y));
@@ -112,7 +152,6 @@ Mat LicensePlateCandidate::deSkewPlate(Mat inputImage, vector<Point2f> corners)
float w = distanceBetweenPoints(leftEdge.midpoint(), rightEdge.midpoint());
float h = distanceBetweenPoints(bottomEdge.midpoint(), topEdge.midpoint());
float aspect = w/h;
int width = config->ocrImageWidthPx;
int height = round(((float) width) / aspect);
if (height > config->ocrImageHeightPx)
@@ -121,27 +160,34 @@ Mat LicensePlateCandidate::deSkewPlate(Mat inputImage, vector<Point2f> corners)
width = round(((float) height) * aspect);
}
Mat deskewed(height, width, this->pipeline_data->grayImg.type());
return Size(width, height);
}
Mat LicensePlateCandidate::getTransformationMatrix(vector<Point2f> corners, Size outputImageSize)
{
// Corners of the destination image
vector<Point2f> quad_pts;
quad_pts.push_back(Point2f(0, 0));
quad_pts.push_back(Point2f(deskewed.cols, 0));
quad_pts.push_back(Point2f(deskewed.cols, deskewed.rows));
quad_pts.push_back(Point2f(0, deskewed.rows));
quad_pts.push_back(Point2f(outputImageSize.width, 0));
quad_pts.push_back(Point2f(outputImageSize.width, outputImageSize.height));
quad_pts.push_back(Point2f(0, outputImageSize.height));
// Get transformation matrix
Mat transmtx = getPerspectiveTransform(corners, quad_pts);
// Apply perspective transformation
warpPerspective(inputImage, deskewed, transmtx, deskewed.size(), INTER_CUBIC);
return transmtx;
}
Mat LicensePlateCandidate::deSkewPlate(Mat inputImage, Size outputImageSize, Mat transformationMatrix)
{
Mat deskewed(outputImageSize, this->pipeline_data->grayImg.type());
// Apply perspective transformation to the image
warpPerspective(inputImage, deskewed, transformationMatrix, deskewed.size(), INTER_CUBIC);
if (config->debugTiming)
{
timespec endTime;
getTime(&endTime);
cout << "deskew Time: " << diffclock(startTime, endTime) << "ms." << endl;
}
if (this->config->debugGeneral)
displayImage(config, "quadrilateral", deskewed);
@@ -149,3 +195,5 @@ Mat LicensePlateCandidate::deSkewPlate(Mat inputImage, vector<Point2f> corners)
return deskewed;
}

View File

@@ -59,8 +59,10 @@ class LicensePlateCandidate
cv::Mat filterByCharacterHue(std::vector<std::vector<cv::Point> > charRegionContours);
std::vector<cv::Point> findPlateCorners(cv::Mat inputImage, PlateLines plateLines, CharacterRegion charRegion); // top-left, top-right, bottom-right, bottom-left
cv::Size getOutputImageSize(std::vector<cv::Point2f> corners);
std::vector<cv::Point2f> transformPointsToOriginalImage(cv::Mat bigImage, cv::Mat smallImage, cv::Rect region, std::vector<cv::Point> corners);
cv::Mat deSkewPlate(cv::Mat inputImage, std::vector<cv::Point2f> corners);
cv::Mat getTransformationMatrix(std::vector<cv::Point2f> corners, cv::Size outputImageSize);
cv::Mat deSkewPlate(cv::Mat inputImage, cv::Size outputImageSize, cv::Mat transformationMatrix);
};

View File

@@ -5,6 +5,7 @@
#include "opencv2/imgproc/imgproc.hpp"
#include "utility.h"
#include "config.h"
#include "textdetection/textline.h"
class PipelineData
{
@@ -22,8 +23,14 @@ class PipelineData
cv::Mat grayImg;
cv::Rect regionOfInterest;
bool isMultiline;
cv::Mat crop_gray;
cv::Mat plate_mask;
bool hasPlateBorder;
cv::Mat plateBorderMask;
std::vector<TextLine> textLines;
std::vector<cv::Mat> thresholds;
std::vector<cv::Point2f> plate_corners;

View File

@@ -22,26 +22,21 @@
using namespace cv;
using namespace std;
PlateCorners::PlateCorners(Mat inputImage, PlateLines* plateLines, CharacterRegion* charRegion, Config* config)
PlateCorners::PlateCorners(Mat inputImage, PlateLines* plateLines, PipelineData* pipelineData) :
tlc(pipelineData)
{
this->config = config;
this->pipelineData = pipelineData;
if (this->config->debugPlateCorners)
if (pipelineData->config->debugPlateCorners)
cout << "PlateCorners constructor" << endl;
this->inputImage = inputImage;
this->plateLines = plateLines;
this->charRegion = charRegion;
this->bestHorizontalScore = 9999999999999;
this->bestVerticalScore = 9999999999999;
Point topPoint = charRegion->getTopLine().midpoint();
Point bottomPoint = charRegion->getBottomLine().closestPointOnSegmentTo(topPoint);
this->charHeight = distanceBetweenPoints(topPoint, bottomPoint);
this->charAngle = angleBetweenPoints(charRegion->getCharArea()[0], charRegion->getCharArea()[1]);
}
PlateCorners::~PlateCorners()
@@ -50,7 +45,7 @@ PlateCorners::~PlateCorners()
vector<Point> PlateCorners::findPlateCorners()
{
if (this->config->debugPlateCorners)
if (pipelineData->config->debugPlateCorners)
cout << "PlateCorners::findPlateCorners" << endl;
timespec startTime;
@@ -81,21 +76,25 @@ vector<Point> PlateCorners::findPlateCorners()
}
}
if (this->config->debugPlateCorners)
if (pipelineData->config->debugPlateCorners)
{
cout << "Drawing debug stuff..." << endl;
Mat imgCorners = Mat(inputImage.size(), inputImage.type());
inputImage.copyTo(imgCorners);
for (uint linenum = 0; linenum < pipelineData->textLines.size(); linenum++)
{
for (int i = 0; i < 4; i++)
circle(imgCorners, charRegion->getCharArea()[i], 2, Scalar(0, 0, 0));
circle(imgCorners, pipelineData->textLines[linenum].textArea[i], 2, Scalar(0, 0, 0));
}
line(imgCorners, this->bestTop.p1, this->bestTop.p2, Scalar(255, 0, 0), 1, CV_AA);
line(imgCorners, this->bestRight.p1, this->bestRight.p2, Scalar(0, 0, 255), 1, CV_AA);
line(imgCorners, this->bestBottom.p1, this->bestBottom.p2, Scalar(0, 0, 255), 1, CV_AA);
line(imgCorners, this->bestLeft.p1, this->bestLeft.p2, Scalar(255, 0, 0), 1, CV_AA);
displayImage(config, "Winning top/bottom Boundaries", imgCorners);
displayImage(pipelineData->config, "Winning top/bottom Boundaries", imgCorners);
}
// Check if a left/right edge has been established.
@@ -112,7 +111,7 @@ vector<Point> PlateCorners::findPlateCorners()
corners.push_back(bestBottom.intersection(bestRight));
corners.push_back(bestBottom.intersection(bestLeft));
if (config->debugTiming)
if (pipelineData->config->debugTiming)
{
timespec endTime;
getTime(&endTime);
@@ -129,8 +128,9 @@ void PlateCorners::scoreVerticals(int v1, int v2)
LineSegment left;
LineSegment right;
float charHeightToPlateWidthRatio = config->plateWidthMM / config->charHeightMM;
float idealPixelWidth = this->charHeight * (charHeightToPlateWidthRatio * 1.03); // Add 3% so we don't clip any characters
float charHeightToPlateWidthRatio = pipelineData->config->plateWidthMM / pipelineData->config->charHeightMM;
float idealPixelWidth = tlc.charHeight * (charHeightToPlateWidthRatio * 1.03); // Add 3% so we don't clip any characters
float confidenceDiff = 0;
float missingSegmentPenalty = 0;
@@ -138,12 +138,9 @@ void PlateCorners::scoreVerticals(int v1, int v2)
if (v1 == NO_LINE && v2 == NO_LINE)
{
//return;
Point centerTop = charRegion->getCharBoxTop().midpoint();
Point centerBottom = charRegion->getCharBoxBottom().midpoint();
LineSegment centerLine = LineSegment(centerBottom.x, centerBottom.y, centerTop.x, centerTop.y);
left = centerLine.getParallelLine(idealPixelWidth / 2);
right = centerLine.getParallelLine(-1 * idealPixelWidth / 2 );
left = tlc.centerVerticalLine.getParallelLine(-1 * idealPixelWidth / 2);
right = tlc.centerVerticalLine.getParallelLine(idealPixelWidth / 2 );
missingSegmentPenalty += SCORING_MISSING_SEGMENT_PENALTY_VERTICAL * 2;
confidenceDiff += 2;
@@ -173,12 +170,9 @@ void PlateCorners::scoreVerticals(int v1, int v2)
score += confidenceDiff * SCORING_LINE_CONFIDENCE_WEIGHT;
score += missingSegmentPenalty;
// Make sure this line is to the left of our license plate letters
if (left.isPointBelowLine(charRegion->getCharBoxLeft().midpoint()) == false)
return;
// Make sure this line is to the right of our license plate letters
if (right.isPointBelowLine(charRegion->getCharBoxRight().midpoint()))
// Make sure that the left and right lines are to the left and right of our text
// area
if (tlc.isLeftOfText(left) < 1 || tlc.isLeftOfText(right) > -1)
return;
/////////////////////////////////////////////////////////////////////////
@@ -203,7 +197,7 @@ void PlateCorners::scoreVerticals(int v1, int v2)
// Score angle difference from detected character box
/////////////////////////////////////////////////////////////////////////
float perpendicularCharAngle = charAngle - 90;
float perpendicularCharAngle = tlc.charAngle - 90;
float charanglediff = abs(perpendicularCharAngle - left.angle) + abs(perpendicularCharAngle - right.angle);
score += charanglediff * SCORING_ANGLE_MATCHES_LPCHARS_WEIGHT;
@@ -212,8 +206,8 @@ void PlateCorners::scoreVerticals(int v1, int v2)
// SCORE the shape wrt character position and height relative to position
//////////////////////////////////////////////////////////////////////////
Point leftMidLinePoint = left.closestPointOnSegmentTo(charRegion->getCharBoxLeft().midpoint());
Point rightMidLinePoint = right.closestPointOnSegmentTo(charRegion->getCharBoxRight().midpoint());
Point leftMidLinePoint = left.closestPointOnSegmentTo(tlc.centerVerticalLine.midpoint());
Point rightMidLinePoint = right.closestPointOnSegmentTo(tlc.centerVerticalLine.midpoint());
float plateDistance = abs(idealPixelWidth - distanceBetweenPoints(leftMidLinePoint, rightMidLinePoint));
@@ -223,9 +217,9 @@ void PlateCorners::scoreVerticals(int v1, int v2)
{
float scorecomponent;
if (this->config->debugPlateCorners)
if (pipelineData->config->debugPlateCorners)
{
cout << "xx xx Score: charHeight " << this->charHeight << endl;
cout << "xx xx Score: charHeight " << tlc.charHeight << endl;
cout << "xx xx Score: idealwidth " << idealPixelWidth << endl;
cout << "xx xx Score: v1,v2= " << v1 << "," << v2 << endl;
cout << "xx xx Score: Left= " << left.str() << endl;
@@ -278,8 +272,8 @@ void PlateCorners::scoreHorizontals(int h1, int h2)
LineSegment top;
LineSegment bottom;
float charHeightToPlateHeightRatio = config->plateHeightMM / config->charHeightMM;
float idealPixelHeight = this->charHeight * charHeightToPlateHeightRatio;
float charHeightToPlateHeightRatio = pipelineData->config->plateHeightMM / pipelineData->config->charHeightMM;
float idealPixelHeight = tlc.charHeight * charHeightToPlateHeightRatio;
float confidenceDiff = 0;
float missingSegmentPenalty = 0;
@@ -287,12 +281,10 @@ void PlateCorners::scoreHorizontals(int h1, int h2)
if (h1 == NO_LINE && h2 == NO_LINE)
{
// return;
Point centerLeft = charRegion->getCharBoxLeft().midpoint();
Point centerRight = charRegion->getCharBoxRight().midpoint();
LineSegment centerLine = LineSegment(centerLeft.x, centerLeft.y, centerRight.x, centerRight.y);
top = centerLine.getParallelLine(idealPixelHeight / 2);
bottom = centerLine.getParallelLine(-1 * idealPixelHeight / 2 );
top = tlc.centerHorizontalLine.getParallelLine(idealPixelHeight / 2);
bottom = tlc.centerHorizontalLine.getParallelLine(-1 * idealPixelHeight / 2 );
missingSegmentPenalty += SCORING_MISSING_SEGMENT_PENALTY_HORIZONTAL * 2;
confidenceDiff += 2;
@@ -322,12 +314,9 @@ void PlateCorners::scoreHorizontals(int h1, int h2)
score += confidenceDiff * SCORING_LINE_CONFIDENCE_WEIGHT;
score += missingSegmentPenalty;
// Make sure this line is above our license plate letters
if (top.isPointBelowLine(charRegion->getCharBoxTop().midpoint()) == false)
return;
// Make sure this line is below our license plate letters
if (bottom.isPointBelowLine(charRegion->getCharBoxBottom().midpoint()))
// Make sure that the top and bottom lines are above and below
// the text area
if (tlc.isAboveText(top) < 1 || tlc.isAboveText(bottom) > -1)
return;
// We now have 4 possible lines. Let's put them to the test and score them...
@@ -352,8 +341,8 @@ void PlateCorners::scoreHorizontals(int h1, int h2)
// Get the height difference
float heightRatio = charHeight / plateHeightPx;
float idealHeightRatio = (config->charHeightMM / config->plateHeightMM);
float heightRatio = tlc.charHeight / plateHeightPx;
float idealHeightRatio = (pipelineData->config->charHeightMM / pipelineData->config->plateHeightMM);
//if (leftRatio < MIN_CHAR_HEIGHT_RATIO || leftRatio > MAX_CHAR_HEIGHT_RATIO || rightRatio < MIN_CHAR_HEIGHT_RATIO || rightRatio > MAX_CHAR_HEIGHT_RATIO)
float heightRatioDiff = abs(heightRatio - idealHeightRatio);
// Ideal ratio == ~.45
@@ -373,7 +362,7 @@ void PlateCorners::scoreHorizontals(int h1, int h2)
// SCORE the middliness of the stuff. We want our top and bottom line to have the characters right towards the middle
//////////////////////////////////////////////////////////////////////////
Point charAreaMidPoint = charRegion->getCharBoxLeft().midpoint();
Point charAreaMidPoint = tlc.centerVerticalLine.midpoint();
Point topLineSpot = top.closestPointOnSegmentTo(charAreaMidPoint);
Point botLineSpot = bottom.closestPointOnSegmentTo(charAreaMidPoint);
@@ -395,7 +384,7 @@ void PlateCorners::scoreHorizontals(int h1, int h2)
// SCORE: the shape for angles matching the character region
//////////////////////////////////////////////////////////////
float charanglediff = abs(charAngle - top.angle) + abs(charAngle - bottom.angle);
float charanglediff = abs(tlc.charAngle - top.angle) + abs(tlc.charAngle - bottom.angle);
score += charanglediff * SCORING_ANGLE_MATCHES_LPCHARS_WEIGHT;
@@ -406,9 +395,9 @@ void PlateCorners::scoreHorizontals(int h1, int h2)
{
float scorecomponent;
if (this->config->debugPlateCorners)
if (pipelineData->config->debugPlateCorners)
{
cout << "xx xx Score: charHeight " << this->charHeight << endl;
cout << "xx xx Score: charHeight " << tlc.charHeight << endl;
cout << "xx xx Score: idealHeight " << idealPixelHeight << endl;
cout << "xx xx Score: h1,h2= " << h1 << "," << h2 << endl;
cout << "xx xx Score: Top= " << top.str() << endl;
@@ -448,3 +437,152 @@ void PlateCorners::scoreHorizontals(int h1, int h2)
bestBottom = LineSegment(bottom.p1.x, bottom.p1.y, bottom.p2.x, bottom.p2.y);
}
}
TextLineCollection::TextLineCollection(PipelineData* pipelineData) {
this->pipelineData = pipelineData;
charHeight = 0;
charAngle = 0;
for (uint i = 0; i < pipelineData->textLines.size(); i++)
{
charHeight += pipelineData->textLines[i].lineHeight;
charAngle += pipelineData->textLines[i].angle;
}
charHeight = charHeight / pipelineData->textLines.size();
charAngle = charAngle / pipelineData->textLines.size();
this->topCharArea = pipelineData->textLines[0].charBoxTop;
this->bottomCharArea = pipelineData->textLines[0].charBoxBottom;
for (uint i = 1; i < pipelineData->textLines.size(); i++)
{
if (this->topCharArea.isPointBelowLine(pipelineData->textLines[i].charBoxTop.midpoint()) == false)
this->topCharArea = pipelineData->textLines[i].charBoxTop;
if (this->bottomCharArea.isPointBelowLine(pipelineData->textLines[i].charBoxBottom.midpoint()))
this->bottomCharArea = pipelineData->textLines[i].charBoxBottom;
}
longerSegment = this->bottomCharArea;
shorterSegment = this->topCharArea;
if (this->topCharArea.length > this->bottomCharArea.length)
{
longerSegment = this->topCharArea;
shorterSegment = this->bottomCharArea;
}
findCenterHorizontal();
findCenterVertical();
// Center Vertical Line
if (pipelineData->config->debugPlateCorners)
{
Mat debugImage = Mat::zeros(pipelineData->crop_gray.size(), CV_8U);
line(debugImage, this->centerHorizontalLine.p1, this->centerHorizontalLine.p2, Scalar(255,255,255), 2);
line(debugImage, this->centerVerticalLine.p1, this->centerVerticalLine.p2, Scalar(255,255,255), 2);
displayImage(pipelineData->config, "Plate Corner Center lines", debugImage);
}
}
// Returns 1 for above, 0 for within, and -1 for below
int TextLineCollection::isAboveText(LineSegment line) {
// Test four points (left and right corner of top and bottom line)
Point topLeft = line.closestPointOnSegmentTo(topCharArea.p1);
Point topRight = line.closestPointOnSegmentTo(topCharArea.p2);
bool lineIsBelowTop = topCharArea.isPointBelowLine(topLeft) || topCharArea.isPointBelowLine(topRight);
if (!lineIsBelowTop)
return 1;
Point bottomLeft = line.closestPointOnSegmentTo(bottomCharArea.p1);
Point bottomRight = line.closestPointOnSegmentTo(bottomCharArea.p2);
bool lineIsBelowBottom = bottomCharArea.isPointBelowLine(bottomLeft) &&
bottomCharArea.isPointBelowLine(bottomRight);
if (lineIsBelowBottom)
return -1;
return 0;
}
// Returns 1 for left, 0 for within, and -1 for to the right
int TextLineCollection::isLeftOfText(LineSegment line) {
LineSegment leftSide = LineSegment(bottomCharArea.p1, topCharArea.p1);
Point topLeft = line.closestPointOnSegmentTo(leftSide.p2);
Point bottomLeft = line.closestPointOnSegmentTo(leftSide.p1);
bool lineIsAboveLeft = (!leftSide.isPointBelowLine(topLeft)) && (!leftSide.isPointBelowLine(bottomLeft));
if (lineIsAboveLeft)
return 1;
LineSegment rightSide = LineSegment(bottomCharArea.p2, topCharArea.p2);
Point topRight = line.closestPointOnSegmentTo(rightSide.p2);
Point bottomRight = line.closestPointOnSegmentTo(rightSide.p1);
bool lineIsBelowRight = rightSide.isPointBelowLine(topRight) && rightSide.isPointBelowLine(bottomRight);
if (lineIsBelowRight)
return -1;
return 0;
}
void TextLineCollection::findCenterHorizontal() {
// To find the center horizontal line:
// Find the longer of the lines (if multiline)
// Get the nearest point on the bottom-most line for the
// left and right
Point leftP1 = shorterSegment.closestPointOnSegmentTo(longerSegment.p1);
Point leftP2 = longerSegment.p1;
LineSegment left = LineSegment(leftP1, leftP2);
Point leftMidpoint = left.midpoint();
Point rightP1 = shorterSegment.closestPointOnSegmentTo(longerSegment.p2);
Point rightP2 = longerSegment.p2;
LineSegment right = LineSegment(rightP1, rightP2);
Point rightMidpoint = right.midpoint();
this->centerHorizontalLine = LineSegment(leftMidpoint, rightMidpoint);
}
void TextLineCollection::findCenterVertical() {
// To find the center vertical line:
// Choose the longest line (if multiline)
// Get the midpoint
// Draw a line up/down using the closest point on the bottom line
Point p1 = longerSegment.midpoint();
Point p2 = shorterSegment.closestPointOnSegmentTo(p1);
// Draw bottom to top
if (p1.y < p2.y)
this->centerVerticalLine = LineSegment(p1, p2);
else
this->centerVerticalLine = LineSegment(p2, p1);
}

View File

@@ -43,11 +43,43 @@
#define SCORING_VERTICALDISTANCE_FROMEDGE_WEIGHT 0.05
class TextLineCollection
{
public:
TextLineCollection(PipelineData* pipelineData);
int isLeftOfText(LineSegment line);
int isAboveText(LineSegment line);
LineSegment centerHorizontalLine;
LineSegment centerVerticalLine;
float charHeight;
float charAngle;
private:
PipelineData* pipelineData;
LineSegment topCharArea;
LineSegment bottomCharArea;
LineSegment longerSegment;
LineSegment shorterSegment;
cv::Mat textMask;
void findCenterHorizontal();
void findCenterVertical();
};
class PlateCorners
{
public:
PlateCorners(cv::Mat inputImage, PlateLines* plateLines, CharacterRegion* charRegion, Config* config);
PlateCorners(cv::Mat inputImage, PlateLines* plateLines, PipelineData* pipelineData) ;
virtual ~PlateCorners();
std::vector<cv::Point> findPlateCorners();
@@ -56,10 +88,10 @@ class PlateCorners
private:
Config* config;
PipelineData* pipelineData;
cv::Mat inputImage;
float charHeight;
float charAngle;
TextLineCollection tlc;
float bestHorizontalScore;
float bestVerticalScore;
@@ -69,7 +101,6 @@ class PlateCorners
LineSegment bestRight;
PlateLines* plateLines;
CharacterRegion* charRegion;
void scoreHorizontals( int h1, int h2 );
void scoreVerticals( int v1, int v2 );

View File

@@ -25,10 +25,11 @@ using namespace std;
const float MIN_CONFIDENCE = 0.3;
PlateLines::PlateLines(Config* config)
PlateLines::PlateLines(PipelineData* pipelineData)
{
this->config = config;
this->debug = config->debugPlateLines;
this->pipelineData = pipelineData;
this->debug = pipelineData->config->debugPlateLines;
if (debug)
cout << "PlateLines constructor" << endl;
@@ -38,7 +39,7 @@ PlateLines::~PlateLines()
{
}
void PlateLines::processImage(Mat inputImage, CharacterRegion* charRegion, float sensitivity)
void PlateLines::processImage(Mat inputImage, float sensitivity)
{
if (this->debug)
cout << "PlateLines findLines" << endl;
@@ -59,7 +60,6 @@ void PlateLines::processImage(Mat inputImage, CharacterRegion* charRegion, float
adaptiveBilateralFilter(inputImage, smoothed, Size(3,3), 45, 45);
int morph_elem = 2;
int morph_size = 2;
Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) );
@@ -69,11 +69,18 @@ void PlateLines::processImage(Mat inputImage, CharacterRegion* charRegion, float
Canny(smoothed, edges, 66, 133);
// Create a mask that is dilated based on the detected characters
vector<vector<Point> > polygons;
polygons.push_back(charRegion->getCharArea());
Mat mask = Mat::zeros(inputImage.size(), CV_8U);
for (uint i = 0; i < pipelineData->textLines.size(); i++)
{
vector<vector<Point> > polygons;
polygons.push_back(pipelineData->textLines[i].textArea);
fillPoly(mask, polygons, Scalar(255,255,255));
}
dilate(mask, mask, getStructuringElement( 1, Size( 1 + 1, 2*1+1 ), Point( 1, 1 ) ));
bitwise_not(mask, mask);
@@ -114,10 +121,10 @@ void PlateLines::processImage(Mat inputImage, CharacterRegion* charRegion, float
images.push_back(debugImgVert);
Mat dashboard = drawImageDashboard(images, debugImgVert.type(), 1);
displayImage(config, "Hough Lines", dashboard);
displayImage(pipelineData->config, "Hough Lines", dashboard);
}
if (config->debugTiming)
if (pipelineData->config->debugTiming)
{
timespec endTime;
getTime(&endTime);
@@ -134,8 +141,8 @@ vector<PlateLine> PlateLines::getLines(Mat edges, float sensitivityMultiplier, b
if (this->debug)
cout << "PlateLines::getLines" << endl;
static int HORIZONTAL_SENSITIVITY = config->plateLinesSensitivityHorizontal;
static int VERTICAL_SENSITIVITY = config->plateLinesSensitivityVertical;
static int HORIZONTAL_SENSITIVITY = pipelineData->config->plateLinesSensitivityHorizontal;
static int VERTICAL_SENSITIVITY = pipelineData->config->plateLinesSensitivityVertical;
vector<Vec2f> allLines;
vector<PlateLine> filteredLines;

View File

@@ -37,10 +37,10 @@ class PlateLines
{
public:
PlateLines(Config* config);
PlateLines(PipelineData* pipelineData);
virtual ~PlateLines();
void processImage(cv::Mat img, CharacterRegion* charRegion, float sensitivity=1.0);
void processImage(cv::Mat img, float sensitivity=1.0);
std::vector<PlateLine> horizontalLines;
std::vector<PlateLine> verticalLines;
@@ -49,7 +49,7 @@ class PlateLines
private:
Config* config;
PipelineData* pipelineData;
bool debug;
cv::Mat customGrayscaleConversion(cv::Mat src);

View File

@@ -17,6 +17,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <opencv2/core/core.hpp>
#include "charactersegmenter.h"
using namespace cv;
@@ -37,76 +39,70 @@ CharacterSegmenter::CharacterSegmenter(PipelineData* pipeline_data)
timespec startTime;
getTime(&startTime);
if (pipeline_data->plate_inverted)
bitwise_not(pipeline_data->crop_gray, pipeline_data->crop_gray);
pipeline_data->clearThresholds();
pipeline_data->thresholds = produceThresholds(pipeline_data->crop_gray, config);
// TODO: Perhaps a bilateral filter would be better here.
medianBlur(pipeline_data->crop_gray, pipeline_data->crop_gray, 3);
if (this->config->debugCharSegmenter)
cout << "Segmenter: inverted: " << pipeline_data->plate_inverted << endl;
if (pipeline_data->plate_inverted)
bitwise_not(pipeline_data->crop_gray, pipeline_data->crop_gray);
charAnalysis = new CharacterAnalysis(pipeline_data);
charAnalysis->analyze();
if (this->config->debugCharSegmenter)
{
displayImage(config, "CharacterSegmenter Thresholds", drawImageDashboard(pipeline_data->thresholds, CV_8U, 3));
}
if (this->config->debugCharSegmenter && charAnalysis->linePolygon.size() > 0)
// if (this->config->debugCharSegmenter && pipeline_data->textLines.size() > 0)
// {
// Mat img_contours(charAnalysis->bestThreshold.size(), CV_8U);
// charAnalysis->bestThreshold.copyTo(img_contours);
// cvtColor(img_contours, img_contours, CV_GRAY2RGB);
//
// vector<vector<Point> > allowedContours;
// for (uint i = 0; i < charAnalysis->bestContours.size(); i++)
// {
// if (charAnalysis->bestContours.goodIndices[i])
// allowedContours.push_back(charAnalysis->bestContours.contours[i]);
// }
//
// drawContours(img_contours, charAnalysis->bestContours.contours,
// -1, // draw all contours
// cv::Scalar(255,0,0), // in blue
// 1); // with a thickness of 1
//
// drawContours(img_contours, allowedContours,
// -1, // draw all contours
// cv::Scalar(0,255,0), // in green
// 1); // with a thickness of 1
//
//
// line(img_contours, pipeline_data->textLines[0].linePolygon[0], pipeline_data->textLines[0].linePolygon[1], Scalar(255, 0, 255), 1);
// line(img_contours, pipeline_data->textLines[0].linePolygon[3], pipeline_data->textLines[0].linePolygon[2], Scalar(255, 0, 255), 1);
//
//
// Mat bordered = addLabel(img_contours, "Best Contours");
// imgDbgGeneral.push_back(bordered);
// }
for (uint lineidx = 0; lineidx < pipeline_data->textLines.size(); lineidx++)
{
Mat img_contours(charAnalysis->bestThreshold.size(), CV_8U);
charAnalysis->bestThreshold.copyTo(img_contours);
cvtColor(img_contours, img_contours, CV_GRAY2RGB);
this->top = pipeline_data->textLines[lineidx].topLine;
this->bottom = pipeline_data->textLines[lineidx].bottomLine;
vector<vector<Point> > allowedContours;
for (uint i = 0; i < charAnalysis->bestContours.size(); i++)
{
if (charAnalysis->bestCharSegments[i])
allowedContours.push_back(charAnalysis->bestContours[i]);
}
float avgCharHeight = pipeline_data->textLines[lineidx].lineHeight;
float height_to_width_ratio = pipeline_data->config->charHeightMM / pipeline_data->config->charWidthMM;
float avgCharWidth = avgCharHeight / height_to_width_ratio;
drawContours(img_contours, charAnalysis->bestContours,
-1, // draw all contours
cv::Scalar(255,0,0), // in blue
1); // with a thickness of 1
drawContours(img_contours, allowedContours,
-1, // draw all contours
cv::Scalar(0,255,0), // in green
1); // with a thickness of 1
if (charAnalysis->linePolygon.size() > 0)
{
line(img_contours, charAnalysis->linePolygon[0], charAnalysis->linePolygon[1], Scalar(255, 0, 255), 1);
line(img_contours, charAnalysis->linePolygon[3], charAnalysis->linePolygon[2], Scalar(255, 0, 255), 1);
}
Mat bordered = addLabel(img_contours, "Best Contours");
imgDbgGeneral.push_back(bordered);
}
if (charAnalysis->linePolygon.size() > 0)
{
this->top = LineSegment(charAnalysis->linePolygon[0].x, charAnalysis->linePolygon[0].y, charAnalysis->linePolygon[1].x, charAnalysis->linePolygon[1].y);
this->bottom = LineSegment(charAnalysis->linePolygon[3].x, charAnalysis->linePolygon[3].y, charAnalysis->linePolygon[2].x, charAnalysis->linePolygon[2].y);
vector<int> charWidths;
vector<int> charHeights;
for (uint i = 0; i < charAnalysis->bestContours.size(); i++)
{
if (charAnalysis->bestCharSegments[i] == false)
continue;
Rect mr = boundingRect(charAnalysis->bestContours[i]);
charWidths.push_back(mr.width);
charHeights.push_back(mr.height);
}
float avgCharWidth = median(charWidths.data(), charWidths.size());
float avgCharHeight = median(charHeights.data(), charHeights.size());
removeSmallContours(pipeline_data->thresholds, charAnalysis->allContours, avgCharWidth, avgCharHeight);
removeSmallContours(pipeline_data->thresholds, avgCharHeight, pipeline_data->textLines[lineidx]);
// Do the histogram analysis to figure out char regions
@@ -115,12 +111,12 @@ CharacterSegmenter::CharacterSegmenter(PipelineData* pipeline_data)
vector<Mat> allHistograms;
vector<Rect> allBoxes;
for (uint i = 0; i < charAnalysis->allContours.size(); i++)
vector<Rect> lineBoxes;
for (uint i = 0; i < pipeline_data->thresholds.size(); i++)
{
Mat histogramMask = Mat::zeros(pipeline_data->thresholds[i].size(), CV_8U);
fillConvexPoly(histogramMask, charAnalysis->linePolygon.data(), charAnalysis->linePolygon.size(), Scalar(255,255,255));
fillConvexPoly(histogramMask, pipeline_data->textLines[lineidx].linePolygon.data(), pipeline_data->textLines[lineidx].linePolygon.size(), Scalar(255,255,255));
VerticalHistogram vertHistogram(pipeline_data->thresholds[i], histogramMask);
@@ -150,16 +146,16 @@ CharacterSegmenter::CharacterSegmenter(PipelineData* pipeline_data)
}
for (uint z = 0; z < charBoxes.size(); z++)
allBoxes.push_back(charBoxes[z]);
lineBoxes.push_back(charBoxes[z]);
//drawAndWait(&histogramMask);
}
float medianCharWidth = avgCharWidth;
vector<int> widthValues;
// Compute largest char width
for (uint i = 0; i < allBoxes.size(); i++)
for (uint i = 0; i < lineBoxes.size(); i++)
{
widthValues.push_back(allBoxes[i].width);
widthValues.push_back(lineBoxes[i].width);
}
medianCharWidth = median(widthValues.data(), widthValues.size());
@@ -171,8 +167,7 @@ CharacterSegmenter::CharacterSegmenter(PipelineData* pipeline_data)
cout << " -- Character Segmentation Create and Score Histograms Time: " << diffclock(startTime, endTime) << "ms." << endl;
}
//ColorFilter colorFilter(img, charAnalysis->getCharacterMask());
vector<Rect> candidateBoxes = getBestCharBoxes(pipeline_data->thresholds[0], allBoxes, medianCharWidth);
vector<Rect> candidateBoxes = getBestCharBoxes(pipeline_data->thresholds[0], lineBoxes, medianCharWidth);
if (this->config->debugCharSegmenter)
{
@@ -194,18 +189,14 @@ CharacterSegmenter::CharacterSegmenter(PipelineData* pipeline_data)
getTime(&startTime);
filterEdgeBoxes(pipeline_data->thresholds, candidateBoxes, medianCharWidth, avgCharHeight);
candidateBoxes = filterMostlyEmptyBoxes(pipeline_data->thresholds, candidateBoxes);
candidateBoxes = combineCloseBoxes(candidateBoxes, medianCharWidth);
cleanCharRegions(pipeline_data->thresholds, candidateBoxes);
cleanMostlyFullBoxes(pipeline_data->thresholds, candidateBoxes);
//cleanBasedOnColor(thresholds, colorFilter.colorMask, candidateBoxes);
candidateBoxes = filterMostlyEmptyBoxes(pipeline_data->thresholds, candidateBoxes);
pipeline_data->charRegions = candidateBoxes;
for (uint cbox = 0; cbox < candidateBoxes.size(); cbox++)
pipeline_data->charRegions.push_back(candidateBoxes[cbox]);
if (config->debugTiming)
{
@@ -227,6 +218,8 @@ CharacterSegmenter::CharacterSegmenter(PipelineData* pipeline_data)
}
}
cleanCharRegions(pipeline_data->thresholds, pipeline_data->charRegions);
if (config->debugTiming)
{
timespec endTime;
@@ -237,7 +230,7 @@ CharacterSegmenter::CharacterSegmenter(PipelineData* pipeline_data)
CharacterSegmenter::~CharacterSegmenter()
{
delete charAnalysis;
}
// Given a histogram and the horizontal line boundaries, respond with an array of boxes where the characters are
@@ -298,7 +291,7 @@ vector<Rect> CharacterSegmenter::getHistogramBoxes(VerticalHistogram histogram,
vector<Rect> CharacterSegmenter::getBestCharBoxes(Mat img, vector<Rect> charBoxes, float avgCharWidth)
{
float MAX_SEGMENT_WIDTH = avgCharWidth * 1.55;
float MAX_SEGMENT_WIDTH = avgCharWidth * 1.65;
// This histogram is based on how many char boxes (from ALL of the many thresholded images) are covering each column
// Makes a sort of histogram from all the previous char boxes. Figures out the best fit from that.
@@ -443,23 +436,33 @@ vector<Rect> CharacterSegmenter::get1DHits(Mat img, int yOffset)
return hits;
}
void CharacterSegmenter::removeSmallContours(vector<Mat> thresholds, vector<vector<vector<Point > > > allContours, float avgCharWidth, float avgCharHeight)
void CharacterSegmenter::removeSmallContours(vector<Mat> thresholds, float avgCharHeight, TextLine textLine)
{
//const float MIN_CHAR_AREA = 0.02 * avgCharWidth * avgCharHeight; // To clear out the tiny specks
const float MIN_CONTOUR_HEIGHT = 0.3 * avgCharHeight;
Mat textLineMask = Mat::zeros(thresholds[0].size(), CV_8U);
fillConvexPoly(textLineMask, textLine.linePolygon.data(), textLine.linePolygon.size(), Scalar(255,255,255));
for (uint i = 0; i < thresholds.size(); i++)
{
for (uint c = 0; c < allContours[i].size(); c++)
vector<vector<Point> > contours;
vector<Vec4i> hierarchy;
Mat thresholdsCopy = Mat::zeros(thresholds[i].size(), thresholds[i].type());
thresholds[i].copyTo(thresholdsCopy, textLineMask);
findContours(thresholdsCopy, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE);
for (uint c = 0; c < contours.size(); c++)
{
if (allContours[i][c].size() == 0)
if (contours[c].size() == 0)
continue;
Rect mr = boundingRect(allContours[i][c]);
Rect mr = boundingRect(contours[c]);
if (mr.height < MIN_CONTOUR_HEIGHT)
{
// Erase it
drawContours(thresholds[i], allContours[i], c, Scalar(0, 0, 0), -1);
drawContours(thresholds[i], contours, c, Scalar(0, 0, 0), -1);
continue;
}
}
@@ -956,69 +959,6 @@ void CharacterSegmenter::filterEdgeBoxes(vector<Mat> thresholds, const vector<Re
}
}
// TECHNIQUE #2
// Check for tall skinny blobs on the edge boxes. If they're too long and skinny, maks the whole char region
/*
*
float MIN_EDGE_CONTOUR_HEIGHT = avgCharHeight * 0.7;
float MIN_EDGE_CONTOUR_AREA_PCT = avgCharHeight * 0.1;
for (int i = 0; i < thresholds.size(); i++)
{
// Just check the first and last char box. If the contour extends too far above/below the line. Drop it.
for (int boxidx = 0; boxidx < charRegions.size(); boxidx++)
{
if (boxidx != 0 || boxidx != charRegions.size() -1)
{
// This is a middle box. we never want to filter these here.
continue;
}
vector<vector<Point> > contours;
Mat mask = Mat::zeros(thresholds[i].size(),CV_8U);
rectangle(mask, charRegions[boxidx], Scalar(255,255,255), CV_FILLED);
bitwise_and(thresholds[i], mask, mask);
findContours(mask, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
//int tallContourIndex = isSkinnyLineInsideBox(thresholds[i], charRegions[boxidx], allContours[i], hierarchy[i], avgCharWidth, avgCharHeight);
float tallestContourHeight = 0;
float fattestContourWidth = 0;
float biggestContourArea = 0;
for (int c = 0; c < contours.size(); c++)
{
Rect r = boundingRect(contours[c]);
if (r.height > tallestContourHeight)
tallestContourHeight = r.height;
if (r.width > fattestContourWidth)
fattestContourWidth = r.width;
float a = r.area();
if (a > biggestContourArea)
biggestContourArea = a;
}
float minArea = charRegions[boxidx].area() * MIN_EDGE_CONTOUR_AREA_PCT;
if ((fattestContourWidth < MIN_BOX_WIDTH_PX) ||
(tallestContourHeight < MIN_EDGE_CONTOUR_HEIGHT) ||
(biggestContourArea < minArea)
)
{
// Find a good place to MASK this contour.
// for now, just mask the whole thing
if (this->debug)
{
rectangle(imgDbgCleanStages[i], charRegions[boxidx], COLOR_DEBUG_EDGE, 2);
cout << "Edge Filter: threshold " << i << " box " << boxidx << endl;
}
rectangle(thresholds[i], charRegions[boxidx], Scalar(0,0,0), -1);
}
else
{
filteredCharRegions.push_back(charRegions[boxidx]);
}
}
}
*/
}
int CharacterSegmenter::getLongestBlobLengthBetweenLines(Mat img, int col)

View File

@@ -28,6 +28,7 @@
#include "colorfilter.h"
#include "verticalhistogram.h"
#include "config.h"
#include "textdetection/textcontours.h"
//const float MIN_BOX_WIDTH_PX = 4; // 4 pixels
@@ -54,7 +55,6 @@ class CharacterSegmenter
Config* config;
PipelineData* pipeline_data;
CharacterAnalysis* charAnalysis;
LineSegment top;
LineSegment bottom;
@@ -62,20 +62,10 @@ class CharacterSegmenter
std::vector<cv::Mat> imgDbgGeneral;
std::vector<cv::Mat> imgDbgCleanStages;
std::vector<bool> filter(cv::Mat img, std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy);
std::vector<bool> filterByBoxSize(std::vector< std::vector<cv::Point> > contours, std::vector<bool> goodIndices, float minHeightPx, float maxHeightPx);
std::vector<bool> filterBetweenLines(cv::Mat img, std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, std::vector<cv::Point> outerPolygon, std::vector<bool> goodIndices);
std::vector<bool> filterContourHoles(std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, std::vector<bool> goodIndices);
std::vector<cv::Point> getBestVotedLines(cv::Mat img, std::vector<std::vector<cv::Point> > contours, std::vector<bool> goodIndices);
int getGoodIndicesCount(std::vector<bool> goodIndices);
cv::Mat getCharacterMask(cv::Mat img_threshold, std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, std::vector<bool> goodIndices);
cv::Mat getCharBoxMask(cv::Mat img_threshold, std::vector<cv::Rect> charBoxes);
void removeSmallContours(std::vector<cv::Mat> thresholds, std::vector<std::vector<std::vector<cv::Point > > > allContours, float avgCharWidth, float avgCharHeight);
void removeSmallContours(std::vector<cv::Mat> thresholds, float avgCharHeight, TextLine textLine);
cv::Mat getVerticalHistogram(cv::Mat img, cv::Mat mask);
std::vector<cv::Rect> getHistogramBoxes(VerticalHistogram histogram, float avgCharWidth, float avgCharHeight, float* score);
std::vector<cv::Rect> getBestCharBoxes(cv::Mat img, std::vector<cv::Rect> charBoxes, float avgCharWidth);
std::vector<cv::Rect> combineCloseBoxes( std::vector<cv::Rect> charBoxes, float avgCharWidth);
@@ -92,7 +82,6 @@ class CharacterSegmenter
int isSkinnyLineInsideBox(cv::Mat threshold, cv::Rect box, std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, float avgCharWidth, float avgCharHeight);
std::vector<cv::Point> getEncapsulatingLines(cv::Mat img, std::vector<std::vector<cv::Point> > contours, std::vector<bool> goodIndices);
};
#endif // OPENALPR_CHARACTERSEGMENTER_H

View File

@@ -149,7 +149,9 @@ long getEpochTime()
{
struct timeval tp;
gettimeofday(&tp, NULL);
long int ms = tp.tv_sec * 1000 + tp.tv_usec / 1000;
long ms = tp.tv_sec * 1000 + tp.tv_usec / 1000;
return ms;
}
#endif

View File

@@ -0,0 +1,589 @@
/*
* Copyright (c) 2014 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/imgproc/imgproc.hpp>
#include "characteranalysis.h"
#include "linefinder.h"
using namespace cv;
using namespace std;
bool sort_text_line(TextLine i, TextLine j) { return (i.topLine.p1.y < j.topLine.p1.y); }
CharacterAnalysis::CharacterAnalysis(PipelineData* pipeline_data)
{
this->pipeline_data = pipeline_data;
this->config = pipeline_data->config;
if (this->config->debugCharAnalysis)
cout << "Starting CharacterAnalysis identification" << endl;
}
CharacterAnalysis::~CharacterAnalysis()
{
}
void CharacterAnalysis::analyze()
{
pipeline_data->clearThresholds();
pipeline_data->thresholds = produceThresholds(pipeline_data->crop_gray, config);
timespec startTime;
getTime(&startTime);
pipeline_data->textLines.clear();
for (uint i = 0; i < pipeline_data->thresholds.size(); i++)
{
TextContours tc(pipeline_data->thresholds[i]);
allTextContours.push_back(tc);
}
if (config->debugTiming)
{
timespec endTime;
getTime(&endTime);
cout << " -- Character Analysis Find Contours Time: " << diffclock(startTime, endTime) << "ms." << endl;
}
//Mat img_equalized = equalizeBrightness(img_gray);
getTime(&startTime);
for (uint i = 0; i < pipeline_data->thresholds.size(); i++)
{
this->filter(pipeline_data->thresholds[i], allTextContours[i]);
if (config->debugCharAnalysis)
cout << "Threshold " << i << " had " << allTextContours[i].getGoodIndicesCount() << " good indices." << endl;
}
if (config->debugTiming)
{
timespec endTime;
getTime(&endTime);
cout << " -- Character Analysis Filter Time: " << diffclock(startTime, endTime) << "ms." << endl;
}
PlateMask plateMask(pipeline_data);
plateMask.findOuterBoxMask(allTextContours);
pipeline_data->hasPlateBorder = plateMask.hasPlateMask;
pipeline_data->plateBorderMask = plateMask.getMask();
if (plateMask.hasPlateMask)
{
// Filter out bad contours now that we have an outer box mask...
for (uint i = 0; i < pipeline_data->thresholds.size(); i++)
{
filterByOuterMask(allTextContours[i]);
}
}
int bestFitScore = -1;
int bestFitIndex = -1;
for (uint i = 0; i < pipeline_data->thresholds.size(); i++)
{
int segmentCount = allTextContours[i].getGoodIndicesCount();
if (segmentCount > bestFitScore)
{
bestFitScore = segmentCount;
bestFitIndex = i;
bestThreshold = pipeline_data->thresholds[i];
bestContours = allTextContours[i];
}
}
if (this->config->debugCharAnalysis)
cout << "Best fit score: " << bestFitScore << " Index: " << bestFitIndex << endl;
if (bestFitScore <= 1)
return;
//getColorMask(img, allContours, allHierarchy, charSegments);
if (this->config->debugCharAnalysis)
{
Mat img_contours = bestContours.drawDebugImage(bestThreshold);
displayImage(config, "Matching Contours", img_contours);
}
LineFinder lf(pipeline_data);
vector<vector<Point> > linePolygons = lf.findLines(pipeline_data->crop_gray, bestContours);
vector<TextLine> tempTextLines;
for (uint i = 0; i < linePolygons.size(); i++)
{
vector<Point> linePolygon = linePolygons[i];
LineSegment topLine = LineSegment(linePolygon[0].x, linePolygon[0].y, linePolygon[1].x, linePolygon[1].y);
LineSegment bottomLine = LineSegment(linePolygon[3].x, linePolygon[3].y, linePolygon[2].x, linePolygon[2].y);
vector<Point> textArea = getCharArea(topLine, bottomLine);
TextLine textLine(textArea, linePolygon);
tempTextLines.push_back(textLine);
}
filterBetweenLines(bestThreshold, bestContours, tempTextLines);
// Sort the lines from top to bottom.
std::sort(tempTextLines.begin(), tempTextLines.end(), sort_text_line);
// Now that we've filtered a few more contours, re-do the text area.
for (uint i = 0; i < tempTextLines.size(); i++)
{
vector<Point> updatedTextArea = getCharArea(tempTextLines[i].topLine, tempTextLines[i].bottomLine);
vector<Point> linePolygon = tempTextLines[i].linePolygon;
if (updatedTextArea.size() > 0 && linePolygon.size() > 0)
{
pipeline_data->textLines.push_back(TextLine(updatedTextArea, linePolygon));
}
}
this->thresholdsInverted = isPlateInverted();
}
Mat CharacterAnalysis::getCharacterMask()
{
Mat charMask = Mat::zeros(bestThreshold.size(), CV_8U);
for (uint i = 0; i < bestContours.size(); i++)
{
if (bestContours.goodIndices[i] == false)
continue;
drawContours(charMask, bestContours.contours,
i, // draw this contour
cv::Scalar(255,255,255), // in
CV_FILLED,
8,
bestContours.hierarchy,
1
);
}
return charMask;
}
void CharacterAnalysis::filter(Mat img, TextContours& textContours)
{
static int STARTING_MIN_HEIGHT = round (((float) img.rows) * config->charAnalysisMinPercent);
static int STARTING_MAX_HEIGHT = round (((float) img.rows) * (config->charAnalysisMinPercent + config->charAnalysisHeightRange));
static int HEIGHT_STEP = round (((float) img.rows) * config->charAnalysisHeightStepSize);
static int NUM_STEPS = config->charAnalysisNumSteps;
int bestFitScore = -1;
vector<bool> bestIndices;
for (int i = 0; i < NUM_STEPS; i++)
{
//vector<bool> goodIndices(contours.size());
for (uint z = 0; z < textContours.size(); z++) textContours.goodIndices[z] = true;
this->filterByBoxSize(textContours, STARTING_MIN_HEIGHT + (i * HEIGHT_STEP), STARTING_MAX_HEIGHT + (i * HEIGHT_STEP));
int goodIndices = textContours.getGoodIndicesCount();
if ( goodIndices == 0 || goodIndices <= bestFitScore) // Don't bother doing more filtering if we already lost...
continue;
this->filterContourHoles(textContours);
goodIndices = textContours.getGoodIndicesCount();
if ( goodIndices == 0 || goodIndices <= bestFitScore) // Don't bother doing more filtering if we already lost...
continue;
int segmentCount = textContours.getGoodIndicesCount();
if (segmentCount > bestFitScore)
{
bestFitScore = segmentCount;
bestIndices = textContours.getIndicesCopy();
}
}
textContours.setIndices(bestIndices);
}
// Goes through the contours for the plate and picks out possible char segments based on min/max height
void CharacterAnalysis::filterByBoxSize(TextContours& textContours, int minHeightPx, int maxHeightPx)
{
float idealAspect=config->charWidthMM / config->charHeightMM;
float aspecttolerance=0.25;
for (uint i = 0; i < textContours.size(); i++)
{
if (textContours.goodIndices[i] == false)
continue;
textContours.goodIndices[i] = false; // Set it to not included unless it proves valid
//Create bounding rect of object
Rect mr= boundingRect(textContours.contours[i]);
float minWidth = mr.height * 0.2;
//Crop image
//cout << "Height: " << minHeightPx << " - " << mr.height << " - " << maxHeightPx << " ////// Width: " << mr.width << " - " << minWidth << endl;
if(mr.height >= minHeightPx && mr.height <= maxHeightPx && mr.width > minWidth)
{
float charAspect= (float)mr.width/(float)mr.height;
//cout << " -- stage 2 aspect: " << abs(charAspect) << " - " << aspecttolerance << endl;
if (abs(charAspect - idealAspect) < aspecttolerance)
textContours.goodIndices[i] = true;
}
}
}
void CharacterAnalysis::filterContourHoles(TextContours& textContours)
{
for (uint i = 0; i < textContours.size(); i++)
{
if (textContours.goodIndices[i] == false)
continue;
textContours.goodIndices[i] = false; // Set it to not included unless it proves valid
int parentIndex = textContours.hierarchy[i][3];
if (parentIndex >= 0 && textContours.goodIndices[parentIndex])
{
// this contour is a child of an already identified contour. REMOVE it
if (this->config->debugCharAnalysis)
{
cout << "filterContourHoles: contour index: " << i << endl;
}
}
else
{
textContours.goodIndices[i] = true;
}
}
}
// Goes through the contours for the plate and picks out possible char segments based on min/max height
// returns a vector of indices corresponding to valid contours
void CharacterAnalysis::filterByParentContour( TextContours& textContours)
{
vector<int> parentIDs;
vector<int> votes;
for (uint i = 0; i < textContours.size(); i++)
{
if (textContours.goodIndices[i] == false)
continue;
textContours.goodIndices[i] = false; // Set it to not included unless it proves
int voteIndex = -1;
int parentID = textContours.hierarchy[i][3];
// check if parentID is already in the lsit
for (uint j = 0; j < parentIDs.size(); j++)
{
if (parentIDs[j] == parentID)
{
voteIndex = j;
break;
}
}
if (voteIndex == -1)
{
parentIDs.push_back(parentID);
votes.push_back(1);
}
else
{
votes[voteIndex] = votes[voteIndex] + 1;
}
}
// Tally up the votes, pick the winner
int totalVotes = 0;
int winningParentId = 0;
int highestVotes = 0;
for (uint i = 0; i < parentIDs.size(); i++)
{
if (votes[i] > highestVotes)
{
winningParentId = parentIDs[i];
highestVotes = votes[i];
}
totalVotes += votes[i];
}
// Now filter out all the contours with a different parent ID (assuming the totalVotes > 2)
for (uint i = 0; i < textContours.size(); i++)
{
if (textContours.goodIndices[i] == false)
continue;
if (totalVotes <= 2)
{
textContours.goodIndices[i] = true;
}
else if (textContours.hierarchy[i][3] == winningParentId)
{
textContours.goodIndices[i] = true;
}
}
}
void CharacterAnalysis::filterBetweenLines(Mat img, TextContours& textContours, vector<TextLine> textLines )
{
static float MIN_AREA_PERCENT_WITHIN_LINES = 0.88;
static float MAX_DISTANCE_PERCENT_FROM_LINES = 0.15;
if (textLines.size() == 0)
return;
vector<Point> validPoints;
// Create a white mask for the area inside the polygon
Mat outerMask = Mat::zeros(img.size(), CV_8U);
for (uint i = 0; i < textLines.size(); i++)
fillConvexPoly(outerMask, textLines[i].linePolygon.data(), textLines[i].linePolygon.size(), Scalar(255,255,255));
// For each contour, determine if enough of it is between the lines to qualify
for (uint i = 0; i < textContours.size(); i++)
{
if (textContours.goodIndices[i] == false)
continue;
float percentInsideMask = getContourAreaPercentInsideMask(outerMask,
textContours.contours,
textContours.hierarchy,
(int) i);
if (percentInsideMask < MIN_AREA_PERCENT_WITHIN_LINES)
{
// Not enough area is inside the lines.
if (config->debugCharAnalysis)
cout << "Rejecting due to insufficient area" << endl;
textContours.goodIndices[i] = false;
continue;
}
// now check to make sure that the top and bottom of the contour are near enough to the lines
// First get the high and low point for the contour
// Remember that origin is top-left, so the top Y values are actually closer to 0.
Rect brect = boundingRect(textContours.contours[i]);
int xmiddle = brect.x + (brect.width / 2);
Point topMiddle = Point(xmiddle, brect.y);
Point botMiddle = Point(xmiddle, brect.y+brect.height);
// Get the absolute distance from the top and bottom lines
for (uint i = 0; i < textLines.size(); i++)
{
Point closestTopPoint = textLines[i].topLine.closestPointOnSegmentTo(topMiddle);
Point closestBottomPoint = textLines[i].bottomLine.closestPointOnSegmentTo(botMiddle);
float absTopDistance = distanceBetweenPoints(closestTopPoint, topMiddle);
float absBottomDistance = distanceBetweenPoints(closestBottomPoint, botMiddle);
float maxDistance = textLines[i].lineHeight * MAX_DISTANCE_PERCENT_FROM_LINES;
if (absTopDistance < maxDistance && absBottomDistance < maxDistance)
{
// It's ok, leave it as-is.
}
else
{
textContours.goodIndices[i] = false;
if (config->debugCharAnalysis)
cout << "Rejecting due to top/bottom points that are out of range" << endl;
}
}
}
}
void CharacterAnalysis::filterByOuterMask(TextContours& textContours)
{
float MINIMUM_PERCENT_LEFT_AFTER_MASK = 0.1;
float MINIMUM_PERCENT_OF_CHARS_INSIDE_PLATE_MASK = 0.6;
if (this->pipeline_data->hasPlateBorder == false)
return;
cv::Mat plateMask = pipeline_data->plateBorderMask;
Mat tempMaskedContour = Mat::zeros(plateMask.size(), CV_8U);
Mat tempFullContour = Mat::zeros(plateMask.size(), CV_8U);
int charsInsideMask = 0;
int totalChars = 0;
vector<bool> originalindices;
for (uint i = 0; i < textContours.size(); i++)
originalindices.push_back(textContours.goodIndices[i]);
for (uint i=0; i < textContours.size(); i++)
{
if (textContours.goodIndices[i] == false)
continue;
totalChars++;
drawContours(tempFullContour, textContours.contours, i, Scalar(255,255,255), CV_FILLED, 8, textContours.hierarchy);
bitwise_and(tempFullContour, plateMask, tempMaskedContour);
float beforeMaskWhiteness = mean(tempFullContour)[0];
float afterMaskWhiteness = mean(tempMaskedContour)[0];
if (afterMaskWhiteness / beforeMaskWhiteness > MINIMUM_PERCENT_LEFT_AFTER_MASK)
{
charsInsideMask++;
textContours.goodIndices[i] = true;
}
}
if (totalChars == 0)
{
textContours.goodIndices = originalindices;
return;
}
// Check to make sure that this is a valid box. If the box is too small (e.g., 1 char is inside, and 3 are outside)
// then don't use this to filter.
float percentCharsInsideMask = ((float) charsInsideMask) / ((float) totalChars);
if (percentCharsInsideMask < MINIMUM_PERCENT_OF_CHARS_INSIDE_PLATE_MASK)
{
textContours.goodIndices = originalindices;
return;
}
}
bool CharacterAnalysis::isPlateInverted()
{
Mat charMask = getCharacterMask();
Scalar meanVal = mean(bestThreshold, charMask)[0];
if (this->config->debugCharAnalysis)
cout << "CharacterAnalysis, plate inverted: MEAN: " << meanVal << " : " << bestThreshold.type() << endl;
if (meanVal[0] < 100) // Half would be 122.5. Give it a little extra oomf before saying it needs inversion. Most states aren't inverted.
return true;
return false;
}
bool CharacterAnalysis::verifySize(Mat r, float minHeightPx, float maxHeightPx)
{
//Char sizes 45x90
float aspect=config->charWidthMM / config->charHeightMM;
float charAspect= (float)r.cols/(float)r.rows;
float error=0.35;
//float minHeight=TEMPLATE_PLATE_HEIGHT * .35;
//float maxHeight=TEMPLATE_PLATE_HEIGHT * .65;
//We have a different aspect ratio for number 1, and it can be ~0.2
float minAspect=0.2;
float maxAspect=aspect+aspect*error;
//area of pixels
float area=countNonZero(r);
//bb area
float bbArea=r.cols*r.rows;
//% of pixel in area
float percPixels=area/bbArea;
//if(DEBUG)
//cout << "Aspect: "<< aspect << " ["<< minAspect << "," << maxAspect << "] " << "Area "<< percPixels <<" Char aspect " << charAspect << " Height char "<< r.rows << "\n";
if(percPixels < 0.8 && charAspect > minAspect && charAspect < maxAspect && r.rows >= minHeightPx && r.rows < maxHeightPx)
return true;
else
return false;
}
vector<Point> CharacterAnalysis::getCharArea(LineSegment topLine, LineSegment bottomLine)
{
const int MAX = 100000;
const int MIN= -1;
int leftX = MAX;
int rightX = MIN;
for (uint i = 0; i < bestContours.size(); i++)
{
if (bestContours.goodIndices[i] == false)
continue;
for (uint z = 0; z < bestContours.contours[i].size(); z++)
{
if (bestContours.contours[i][z].x < leftX)
leftX = bestContours.contours[i][z].x;
if (bestContours.contours[i][z].x > rightX)
rightX = bestContours.contours[i][z].x;
}
}
vector<Point> charArea;
if (leftX != MAX && rightX != MIN)
{
Point tl(leftX, topLine.getPointAt(leftX));
Point tr(rightX, topLine.getPointAt(rightX));
Point br(rightX, bottomLine.getPointAt(rightX));
Point bl(leftX, bottomLine.getPointAt(leftX));
charArea.push_back(tl);
charArea.push_back(tr);
charArea.push_back(br);
charArea.push_back(bl);
}
return charArea;
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2014 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/>.
*/
#ifndef OPENALPR_CHARACTERANALYSIS_H
#define OPENALPR_CHARACTERANALYSIS_H
#include <algorithm>
#include "opencv2/imgproc/imgproc.hpp"
#include "utility.h"
#include "config.h"
#include "pipeline_data.h"
#include "textcontours.h"
#include "platemask.h"
#include "linefinder.h"
class CharacterAnalysis
{
public:
CharacterAnalysis(PipelineData* pipeline_data);
virtual ~CharacterAnalysis();
cv::Mat bestThreshold;
TextContours bestContours;
bool thresholdsInverted;
std::vector<TextContours> allTextContours;
void analyze();
cv::Mat getCharacterMask();
private:
PipelineData* pipeline_data;
Config* config;
cv::Mat findOuterBoxMask( );
bool isPlateInverted();
void filter(cv::Mat img, TextContours& textContours);
void filterByBoxSize(TextContours& textContours, int minHeightPx, int maxHeightPx);
void filterByParentContour( TextContours& textContours );
void filterContourHoles(TextContours& textContours);
void filterByOuterMask(TextContours& textContours);
std::vector<cv::Point> getCharArea(LineSegment topLine, LineSegment bottomLine);
void filterBetweenLines(cv::Mat img, TextContours& textContours, std::vector<TextLine> textLines );
bool verifySize(cv::Mat r, float minHeightPx, float maxHeightPx);
};
#endif // OPENALPR_CHARACTERANALYSIS_H

View File

@@ -0,0 +1,253 @@
/*
* Copyright (c) 2014 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/core/core.hpp>
#include "linefinder.h"
#include "utility.h"
#include "pipeline_data.h"
using namespace std;
using namespace cv;
LineFinder::LineFinder(PipelineData* pipeline_data) {
this->pipeline_data = pipeline_data;
}
LineFinder::~LineFinder() {
}
vector<vector<Point> > LineFinder::findLines(Mat image, const TextContours contours)
{
const float MIN_AREA_TO_IGNORE = 0.65;
vector<vector<Point> > linesFound;
cvtColor(image, image, CV_GRAY2BGR);
vector<CharPointInfo> charPoints;
for (uint i = 0; i < contours.contours.size(); i++)
{
if (contours.goodIndices[i] == false)
continue;
charPoints.push_back( CharPointInfo(contours.contours[i], i) );
}
vector<Point> bestLine = getBestLine(contours, charPoints);
if (bestLine.size() > 0)
linesFound.push_back(bestLine);
if (pipeline_data->isMultiline)
{
// we have a two-line plate. Find the next best line, removing the tops/bottoms from before.
// Create a mask from the bestLine area, and remove all contours with tops that fall inside of it.
vector<CharPointInfo> remainingPoints;
for (uint i = 0; i < charPoints.size(); i++)
{
Mat mask = Mat::zeros(Size(contours.width, contours.height), CV_8U);
fillConvexPoly(mask, bestLine.data(), bestLine.size(), Scalar(255,255,255));
float percentInside = getContourAreaPercentInsideMask(mask, contours.contours, contours.hierarchy, charPoints[i].contourIndex);
if (percentInside < MIN_AREA_TO_IGNORE)
{
remainingPoints.push_back(charPoints[i]);
}
}
vector<Point> nextBestLine = getBestLine(contours, remainingPoints);
if (nextBestLine.size() > 0)
linesFound.push_back(nextBestLine);
}
return linesFound;
}
// Returns a polygon "stripe" across the width of the character region. The lines are voted and the polygon starts at 0 and extends to image width
vector<Point> LineFinder::getBestLine(const TextContours contours, vector<CharPointInfo> charPoints)
{
vector<Point> bestStripe;
// Find the best fit line segment that is parallel with the most char segments
if (charPoints.size() <= 1)
{
// Maybe do something about this later, for now let's just ignore
return bestStripe;
}
vector<int> charheights;
for (uint i = 0; i < charPoints.size(); i++)
charheights.push_back(charPoints[i].boundingBox.height);
float medianCharHeight = median(charheights.data(), charheights.size());
vector<LineSegment> topLines;
vector<LineSegment> bottomLines;
// Iterate through each possible char and find all possible lines for the top and bottom of each char segment
for (uint i = 0; i < charPoints.size() - 1; i++)
{
for (uint k = i+1; k < charPoints.size(); k++)
{
int leftCPIndex, rightCPIndex;
if (charPoints[i].top.x < charPoints[k].top.x)
{
leftCPIndex = i;
rightCPIndex = k;
}
else
{
leftCPIndex = k;
rightCPIndex = i;
}
LineSegment top(charPoints[leftCPIndex].top, charPoints[rightCPIndex].top);
LineSegment bottom(charPoints[leftCPIndex].bottom, charPoints[rightCPIndex].bottom);
// Only allow lines that have a sane angle
// if (abs(top.angle) <= pipeline_data->config->maxPlateAngleDegrees &&
// abs(bottom.angle) <= pipeline_data->config->maxPlateAngleDegrees)
// {
// topLines.push_back(top);
// bottomLines.push_back(bottom);
// }
LineSegment parallelBot = top.getParallelLine(medianCharHeight * -1);
LineSegment parallelTop = bottom.getParallelLine(medianCharHeight);
// Only allow lines that have a sane angle
if (abs(top.angle) <= pipeline_data->config->maxPlateAngleDegrees &&
abs(parallelBot.angle) <= pipeline_data->config->maxPlateAngleDegrees)
{
topLines.push_back(top);
bottomLines.push_back(parallelBot);
}
// Only allow lines that have a sane angle
if (abs(parallelTop.angle) <= pipeline_data->config->maxPlateAngleDegrees &&
abs(bottom.angle) <= pipeline_data->config->maxPlateAngleDegrees)
{
topLines.push_back(parallelTop);
bottomLines.push_back(bottom);
}
}
}
int bestScoreIndex = 0;
int bestScore = -1;
int bestScoreDistance = -1; // Line segment distance is used as a tie breaker
// Now, among all possible lines, find the one that is the best fit
for (uint i = 0; i < topLines.size(); i++)
{
float SCORING_MIN_THRESHOLD = 0.97;
float SCORING_MAX_THRESHOLD = 1.03;
int curScore = 0;
for (uint charidx = 0; charidx < charPoints.size(); charidx++)
{
float topYPos = topLines[i].getPointAt(charPoints[charidx].top.x);
float botYPos = bottomLines[i].getPointAt(charPoints[charidx].bottom.x);
float minTop = charPoints[charidx].top.y * SCORING_MIN_THRESHOLD;
float maxTop = charPoints[charidx].top.y * SCORING_MAX_THRESHOLD;
float minBot = (charPoints[charidx].bottom.y) * SCORING_MIN_THRESHOLD;
float maxBot = (charPoints[charidx].bottom.y) * SCORING_MAX_THRESHOLD;
if ( (topYPos >= minTop && topYPos <= maxTop) &&
(botYPos >= minBot && botYPos <= maxBot))
{
curScore++;
}
//cout << "Slope: " << topslope << " yPos: " << topYPos << endl;
//drawAndWait(&tempImg);
}
// Tie goes to the one with longer line segments
if ((curScore > bestScore) ||
(curScore == bestScore && topLines[i].length > bestScoreDistance))
{
bestScore = curScore;
bestScoreIndex = i;
// Just use x distance for now
bestScoreDistance = topLines[i].length;
}
}
if (bestScore < 0)
return bestStripe;
if (pipeline_data->config->debugCharAnalysis)
{
cout << "The winning score is: " << bestScore << endl;
// Draw the winning line segment
Mat tempImg = Mat::zeros(Size(contours.width, contours.height), CV_8U);
cvtColor(tempImg, tempImg, CV_GRAY2BGR);
cv::line(tempImg, topLines[bestScoreIndex].p1, topLines[bestScoreIndex].p2, Scalar(0, 0, 255), 2);
cv::line(tempImg, bottomLines[bestScoreIndex].p1, bottomLines[bestScoreIndex].p2, Scalar(0, 0, 255), 2);
displayImage(pipeline_data->config, "Winning lines", tempImg);
}
Point topLeft = Point(0, topLines[bestScoreIndex].getPointAt(0) );
Point topRight = Point(contours.width, topLines[bestScoreIndex].getPointAt(contours.width));
Point bottomRight = Point(contours.width, bottomLines[bestScoreIndex].getPointAt(contours.width));
Point bottomLeft = Point(0, bottomLines[bestScoreIndex].getPointAt(0));
bestStripe.push_back(topLeft);
bestStripe.push_back(topRight);
bestStripe.push_back(bottomRight);
bestStripe.push_back(bottomLeft);
return bestStripe;
}
CharPointInfo::CharPointInfo(vector<Point> contour, int index) {
this->contourIndex = index;
this->boundingBox = cv::boundingRect( Mat(contour) );
int x = boundingBox.x + (boundingBox.width / 2);
int y = boundingBox.y;
this->top = Point(x, y);
x = boundingBox.x + (boundingBox.width / 2);
y = boundingBox.y + boundingBox.height;
this->bottom = Point(x,y);
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2014 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/>.
*/
// This class finds lines of text given an array of contours
#ifndef OPENALPR_LINEFINDER_H
#define OPENALPR_LINEFINDER_H
#include "opencv2/imgproc/imgproc.hpp"
#include "textcontours.h"
#include "textline.h"
#include "pipeline_data.h"
class CharPointInfo
{
public:
CharPointInfo(std::vector<cv::Point> contour, int index);
cv::Rect boundingBox;
cv::Point top;
cv::Point bottom;
int contourIndex;
};
class LineFinder {
public:
LineFinder(PipelineData* pipeline_data);
virtual ~LineFinder();
std::vector<std::vector<cv::Point> > findLines(cv::Mat image, const TextContours contours);
private:
PipelineData* pipeline_data;
std::vector<cv::Point> getBestLine(const TextContours contours, std::vector<CharPointInfo> charPoints);
};
#endif /* OPENALPR_LINEFINDER_H */

View File

@@ -0,0 +1,199 @@
/*
* Copyright (c) 2014 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 "platemask.h"
using namespace std;
using namespace cv;
PlateMask::PlateMask(PipelineData* pipeline_data) {
this->pipeline_data = pipeline_data;
this->hasPlateMask = false;
}
PlateMask::~PlateMask() {
}
cv::Mat PlateMask::getMask() {
return this->plateMask;
}
void PlateMask::findOuterBoxMask( vector<TextContours > contours )
{
double min_parent_area = pipeline_data->config->templateHeightPx * pipeline_data->config->templateWidthPx * 0.10; // Needs to be at least 10% of the plate area to be considered.
int winningIndex = -1;
int winningParentId = -1;
int bestCharCount = 0;
double lowestArea = 99999999999999;
if (pipeline_data->config->debugCharAnalysis)
cout << "CharacterAnalysis::findOuterBoxMask" << endl;
for (uint imgIndex = 0; imgIndex < contours.size(); imgIndex++)
{
//vector<bool> charContours = filter(thresholds[imgIndex], allContours[imgIndex], allHierarchy[imgIndex]);
int charsRecognized = 0;
int parentId = -1;
bool hasParent = false;
for (uint i = 0; i < contours[imgIndex].goodIndices.size(); i++)
{
if (contours[imgIndex].goodIndices[i]) charsRecognized++;
if (contours[imgIndex].goodIndices[i] && contours[imgIndex].hierarchy[i][3] != -1)
{
parentId = contours[imgIndex].hierarchy[i][3];
hasParent = true;
}
}
if (charsRecognized == 0)
continue;
if (hasParent)
{
double boxArea = contourArea(contours[imgIndex].contours[parentId]);
if (boxArea < min_parent_area)
continue;
if ((charsRecognized > bestCharCount) ||
(charsRecognized == bestCharCount && boxArea < lowestArea))
//(boxArea < lowestArea)
{
bestCharCount = charsRecognized;
winningIndex = imgIndex;
winningParentId = parentId;
lowestArea = boxArea;
}
}
}
if (pipeline_data->config->debugCharAnalysis)
cout << "Winning image index (findOuterBoxMask) is: " << winningIndex << endl;
if (winningIndex != -1 && bestCharCount >= 3)
{
int longestChildIndex = -1;
double longestChildLength = 0;
// Find the child with the longest permiter/arc length ( just for kicks)
for (uint i = 0; i < contours[winningIndex].size(); i++)
{
for (uint j = 0; j < contours[winningIndex].size(); j++)
{
if (contours[winningIndex].hierarchy[j][3] == winningParentId)
{
double arclength = arcLength(contours[winningIndex].contours[j], false);
if (arclength > longestChildLength)
{
longestChildIndex = j;
longestChildLength = arclength;
}
}
}
}
Mat mask = Mat::zeros(pipeline_data->thresholds[winningIndex].size(), CV_8U);
// get rid of the outline by drawing a 1 pixel width black line
drawContours(mask, contours[winningIndex].contours,
winningParentId, // draw this contour
cv::Scalar(255,255,255), // in
CV_FILLED,
8,
contours[winningIndex].hierarchy,
0
);
// Morph Open the mask to get rid of any little connectors to non-plate portions
int morph_elem = 2;
int morph_size = 3;
Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) );
//morphologyEx( mask, mask, MORPH_CLOSE, element );
morphologyEx( mask, mask, MORPH_OPEN, element );
//morph_size = 1;
//element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) );
//dilate(mask, mask, element);
// Drawing the edge black effectively erodes the image. This may clip off some extra junk from the edges.
// We'll want to do the contour again and find the larges one so that we remove the clipped portion.
vector<vector<Point> > contoursSecondRound;
findContours(mask, contoursSecondRound, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
int biggestContourIndex = -1;
double largestArea = 0;
for (uint c = 0; c < contoursSecondRound.size(); c++)
{
double area = contourArea(contoursSecondRound[c]);
if (area > largestArea)
{
biggestContourIndex = c;
largestArea = area;
}
}
if (biggestContourIndex != -1)
{
mask = Mat::zeros(pipeline_data->thresholds[winningIndex].size(), CV_8U);
vector<Point> smoothedMaskPoints;
approxPolyDP(contoursSecondRound[biggestContourIndex], smoothedMaskPoints, 2, true);
vector<vector<Point> > tempvec;
tempvec.push_back(smoothedMaskPoints);
//fillPoly(mask, smoothedMaskPoints.data(), smoothedMaskPoints, Scalar(255,255,255));
drawContours(mask, tempvec,
0, // draw this contour
cv::Scalar(255,255,255), // in
CV_FILLED,
8,
contours[winningIndex].hierarchy,
0
);
}
if (pipeline_data->config->debugCharAnalysis)
{
vector<Mat> debugImgs;
Mat debugImgMasked = Mat::zeros(pipeline_data->thresholds[winningIndex].size(), CV_8U);
pipeline_data->thresholds[winningIndex].copyTo(debugImgMasked, mask);
debugImgs.push_back(mask);
debugImgs.push_back(pipeline_data->thresholds[winningIndex]);
debugImgs.push_back(debugImgMasked);
Mat dashboard = drawImageDashboard(debugImgs, CV_8U, 1);
displayImage(pipeline_data->config, "Winning outer box", dashboard);
}
hasPlateMask = true;
this->plateMask = mask;
}
hasPlateMask = false;
Mat fullMask = Mat::zeros(pipeline_data->thresholds[0].size(), CV_8U);
bitwise_not(fullMask, fullMask);
this->plateMask = fullMask;
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2014 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/>.
*/
#ifndef OPENALPR_PLATEMASK_H
#define OPENALPR_PLATEMASK_H
#include "opencv2/imgproc/imgproc.hpp"
#include "pipeline_data.h"
#include "textcontours.h"
class PlateMask {
public:
PlateMask(PipelineData* pipeline_data);
virtual ~PlateMask();
bool hasPlateMask;
cv::Mat getMask();
void findOuterBoxMask(std::vector<TextContours > contours);
private:
PipelineData* pipeline_data;
cv::Mat plateMask;
};
#endif /* OPENALPR_PLATEMASK_H */

View File

@@ -0,0 +1,138 @@
/*
* Copyright (c) 2014 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 "textcontours.h"
using namespace std;
using namespace cv;
TextContours::TextContours() {
}
TextContours::TextContours(cv::Mat threshold) {
load(threshold);
}
TextContours::~TextContours() {
}
void TextContours::load(cv::Mat threshold) {
Mat tempThreshold(threshold.size(), CV_8U);
threshold.copyTo(tempThreshold);
findContours(tempThreshold,
contours, // a vector of contours
hierarchy,
CV_RETR_TREE, // retrieve all contours
CV_CHAIN_APPROX_SIMPLE ); // all pixels of each contours
for (uint i = 0; i < contours.size(); i++)
goodIndices.push_back(true);
this->width = threshold.cols;
this->height = threshold.rows;
}
uint TextContours::size() {
return contours.size();
}
int TextContours::getGoodIndicesCount()
{
int count = 0;
for (uint i = 0; i < goodIndices.size(); i++)
{
if (goodIndices[i])
count++;
}
return count;
}
std::vector<bool> TextContours::getIndicesCopy()
{
vector<bool> copyArray;
for (uint i = 0; i < goodIndices.size(); i++)
{
bool val = goodIndices[i];
copyArray.push_back(goodIndices[i]);
}
return copyArray;
}
void TextContours::setIndices(std::vector<bool> newIndices)
{
if (newIndices.size() == goodIndices.size())
{
for (uint i = 0; i < newIndices.size(); i++)
goodIndices[i] = newIndices[i];
}
else
{
assert("Invalid set operation on indices");
}
}
Mat TextContours::drawDebugImage() {
Mat img_contours = Mat::zeros(Size(width, height), CV_8U);
return drawDebugImage(img_contours);
}
Mat TextContours::drawDebugImage(Mat baseImage) {
Mat img_contours(baseImage.size(), CV_8U);
baseImage.copyTo(img_contours);
cvtColor(img_contours, img_contours, CV_GRAY2RGB);
vector<vector<Point> > allowedContours;
for (uint i = 0; i < this->contours.size(); i++)
{
if (this->goodIndices[i])
allowedContours.push_back(this->contours[i]);
}
drawContours(img_contours, this->contours,
-1, // draw all contours
cv::Scalar(255,0,0), // in blue
1); // with a thickness of 1
drawContours(img_contours, allowedContours,
-1, // draw all contours
cv::Scalar(0,255,0), // in green
1); // with a thickness of 1
return img_contours;
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2014 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/>.
*/
#ifndef TEXTCONTOURS_H
#define TEXTCONTOURS_H
#include <vector>
#include "opencv2/imgproc/imgproc.hpp"
class TextContours {
public:
TextContours();
TextContours(cv::Mat threshold);
virtual ~TextContours();
void load(cv::Mat threshold);
int width;
int height;
std::vector<bool> goodIndices;
std::vector<std::vector<cv::Point> > contours;
std::vector<cv::Vec4i> hierarchy;
uint size();
int getGoodIndicesCount();
std::vector<bool> getIndicesCopy();
void setIndices(std::vector<bool> newIndices);
cv::Mat drawDebugImage();
cv::Mat drawDebugImage(cv::Mat baseImage);
private:
};
#endif /* TEXTCONTOURS_H */

View File

@@ -0,0 +1,104 @@
/*
* Copyright (c) 2014 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/imgproc/imgproc.hpp>
#include "textline.h"
using namespace cv;
TextLine::TextLine(std::vector<cv::Point2f> textArea, std::vector<cv::Point2f> linePolygon) {
std::vector<Point> textAreaInts, linePolygonInts;
for (uint i = 0; i < textArea.size(); i++)
textAreaInts.push_back(Point(round(textArea[i].x), round(textArea[i].y)));
for (uint i = 0; i < linePolygon.size(); i++)
linePolygonInts.push_back(Point(round(linePolygon[i].x), round(linePolygon[i].y)));
initialize(textAreaInts, linePolygonInts);
}
TextLine::TextLine(std::vector<cv::Point> textArea, std::vector<cv::Point> linePolygon) {
initialize(textArea, linePolygon);
}
TextLine::~TextLine() {
}
void TextLine::initialize(std::vector<cv::Point> textArea, std::vector<cv::Point> linePolygon) {
if (textArea.size() > 0)
{
if (this->textArea.size() > 0)
this->textArea.clear();
if (this->linePolygon.size() > 0)
this->linePolygon.clear();
for (uint i = 0; i < textArea.size(); i++)
this->textArea.push_back(textArea[i]);
for (uint i = 0; i < linePolygon.size(); i++)
this->linePolygon.push_back(linePolygon[i]);
this->topLine = LineSegment(linePolygon[0].x, linePolygon[0].y, linePolygon[1].x, linePolygon[1].y);
this->bottomLine = LineSegment(linePolygon[3].x, linePolygon[3].y, linePolygon[2].x, linePolygon[2].y);
this->charBoxTop = LineSegment(textArea[0].x, textArea[0].y, textArea[1].x, textArea[1].y);
this->charBoxBottom = LineSegment(textArea[3].x, textArea[3].y, textArea[2].x, textArea[2].y);
this->charBoxLeft = LineSegment(textArea[3].x, textArea[3].y, textArea[0].x, textArea[0].y);
this->charBoxRight = LineSegment(textArea[2].x, textArea[2].y, textArea[1].x, textArea[1].y);
// Calculate line height
float x = ((float) linePolygon[1].x) / 2;
Point midpoint = Point(x, bottomLine.getPointAt(x));
Point acrossFromMidpoint = topLine.closestPointOnSegmentTo(midpoint);
this->lineHeight = distanceBetweenPoints(midpoint, acrossFromMidpoint);
this->angle = (topLine.angle + bottomLine.angle) / 2;
}
}
cv::Mat TextLine::drawDebugImage(cv::Mat baseImage) {
cv::Mat debugImage(baseImage.size(), baseImage.type());
baseImage.copyTo(debugImage);
cv::cvtColor(debugImage, debugImage, CV_GRAY2BGR);
fillConvexPoly(debugImage, linePolygon.data(), linePolygon.size(), Scalar(0,0,165));
fillConvexPoly(debugImage, textArea.data(), textArea.size(), Scalar(125,255,0));
line(debugImage, topLine.p1, topLine.p2, Scalar(255,0,0), 1);
line(debugImage, bottomLine.p1, bottomLine.p2, Scalar(255,0,0), 1);
line(debugImage, charBoxTop.p1, charBoxTop.p2, Scalar(0,125,125), 1);
line(debugImage, charBoxLeft.p1, charBoxLeft.p2, Scalar(0,125,125), 1);
line(debugImage, charBoxRight.p1, charBoxRight.p2, Scalar(0,125,125), 1);
line(debugImage, charBoxBottom.p1, charBoxBottom.p2, Scalar(0,125,125), 1);
return debugImage;
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (c) 2014 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/>.
*/
#ifndef OPENALPR_TEXTLINE_H
#define OPENALPR_TEXTLINE_H
#include "utility.h"
#include "opencv2/imgproc/imgproc.hpp"
class TextLine {
public:
TextLine(std::vector<cv::Point> textArea, std::vector<cv::Point> linePolygon);
TextLine(std::vector<cv::Point2f> textArea, std::vector<cv::Point2f> linePolygon);
virtual ~TextLine();
std::vector<cv::Point> linePolygon;
std::vector<cv::Point> textArea;
LineSegment topLine;
LineSegment bottomLine;
LineSegment charBoxTop;
LineSegment charBoxBottom;
LineSegment charBoxLeft;
LineSegment charBoxRight;
float lineHeight;
float angle;
cv::Mat drawDebugImage(cv::Mat baseImage);
private:
void initialize(std::vector<cv::Point> textArea, std::vector<cv::Point> linePolygon);
};
#endif /* OPENALPR_TEXTLINE_H */

View File

@@ -17,6 +17,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <opencv2/core/core.hpp>
#include "utility.h"
using namespace cv;
@@ -379,6 +381,36 @@ LineSegment LineSegment::getParallelLine(float distance)
return result;
}
// Given a contour and a mask, this function determines what percentage of the contour (area)
// is inside the masked area.
float getContourAreaPercentInsideMask(cv::Mat mask, std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, int contourIndex)
{
Mat innerArea = Mat::zeros(mask.size(), CV_8U);
drawContours(innerArea, contours,
contourIndex, // draw this contour
cv::Scalar(255,255,255), // in
CV_FILLED,
8,
hierarchy,
2
);
int startingPixels = cv::countNonZero(innerArea);
//drawAndWait(&innerArea);
bitwise_and(innerArea, mask, innerArea);
int endingPixels = cv::countNonZero(innerArea);
//drawAndWait(&innerArea);
return ((float) endingPixels) / ((float) startingPixels);
}
std::string toString(int value)
{

View File

@@ -100,6 +100,8 @@ float angleBetweenPoints(cv::Point p1, cv::Point p2);
cv::Size getSizeMaintainingAspect(cv::Mat inputImg, int maxWidth, int maxHeight);
float getContourAreaPercentInsideMask(cv::Mat mask, std::vector<std::vector<cv::Point> > contours, std::vector<cv::Vec4i> hierarchy, int contourIndex);
cv::Mat equalizeBrightness(cv::Mat img);
cv::Rect expandRect(cv::Rect original, int expandXPixels, int expandYPixels, int maxX, int maxY);