From 405fcdb3a2638d346f5bc33ec234046934b89abe Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 2 Jan 2014 16:00:26 -0600 Subject: [PATCH 01/14] Added TClap command line parsing library --- src/tclap/Arg.h | 692 +++++++++++++++++++++++++++ src/tclap/ArgException.h | 200 ++++++++ src/tclap/ArgTraits.h | 87 ++++ src/tclap/CmdLine.h | 633 ++++++++++++++++++++++++ src/tclap/CmdLineInterface.h | 150 ++++++ src/tclap/CmdLineOutput.h | 74 +++ src/tclap/Constraint.h | 68 +++ src/tclap/DocBookOutput.h | 299 ++++++++++++ src/tclap/HelpVisitor.h | 76 +++ src/tclap/IgnoreRestVisitor.h | 52 ++ src/tclap/MultiArg.h | 433 +++++++++++++++++ src/tclap/MultiSwitchArg.h | 216 +++++++++ src/tclap/OptionalUnlabeledTracker.h | 62 +++ src/tclap/StandardTraits.h | 208 ++++++++ src/tclap/StdOutput.h | 298 ++++++++++++ src/tclap/SwitchArg.h | 266 ++++++++++ src/tclap/UnlabeledMultiArg.h | 301 ++++++++++++ src/tclap/UnlabeledValueArg.h | 340 +++++++++++++ src/tclap/ValueArg.h | 425 ++++++++++++++++ src/tclap/ValuesConstraint.h | 148 ++++++ src/tclap/VersionVisitor.h | 81 ++++ src/tclap/Visitor.h | 53 ++ src/tclap/XorHandler.h | 166 +++++++ src/tclap/ZshCompletionOutput.h | 323 +++++++++++++ 24 files changed, 5651 insertions(+) create mode 100644 src/tclap/Arg.h create mode 100644 src/tclap/ArgException.h create mode 100644 src/tclap/ArgTraits.h create mode 100644 src/tclap/CmdLine.h create mode 100644 src/tclap/CmdLineInterface.h create mode 100644 src/tclap/CmdLineOutput.h create mode 100644 src/tclap/Constraint.h create mode 100644 src/tclap/DocBookOutput.h create mode 100644 src/tclap/HelpVisitor.h create mode 100644 src/tclap/IgnoreRestVisitor.h create mode 100644 src/tclap/MultiArg.h create mode 100644 src/tclap/MultiSwitchArg.h create mode 100644 src/tclap/OptionalUnlabeledTracker.h create mode 100644 src/tclap/StandardTraits.h create mode 100644 src/tclap/StdOutput.h create mode 100644 src/tclap/SwitchArg.h create mode 100644 src/tclap/UnlabeledMultiArg.h create mode 100644 src/tclap/UnlabeledValueArg.h create mode 100644 src/tclap/ValueArg.h create mode 100644 src/tclap/ValuesConstraint.h create mode 100644 src/tclap/VersionVisitor.h create mode 100644 src/tclap/Visitor.h create mode 100644 src/tclap/XorHandler.h create mode 100644 src/tclap/ZshCompletionOutput.h diff --git a/src/tclap/Arg.h b/src/tclap/Arg.h new file mode 100644 index 0000000..d653164 --- /dev/null +++ b/src/tclap/Arg.h @@ -0,0 +1,692 @@ +// -*- Mode: c++; c-basic-offset: 4; tab-width: 4; -*- + +/****************************************************************************** + * + * file: Arg.h + * + * Copyright (c) 2003, Michael E. Smoot . + * Copyright (c) 2004, Michael E. Smoot, Daniel Aarno . + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + + +#ifndef TCLAP_ARGUMENT_H +#define TCLAP_ARGUMENT_H + +#ifdef HAVE_CONFIG_H +#include +#else +#define HAVE_SSTREAM +#endif + +#include +#include +#include +#include +#include +#include + +#if defined(HAVE_SSTREAM) +#include +typedef std::istringstream istringstream; +#elif defined(HAVE_STRSTREAM) +#include +typedef std::istrstream istringstream; +#else +#error "Need a stringstream (sstream or strstream) to compile!" +#endif + +#include "ArgException.h" +#include "Visitor.h" +#include "CmdLineInterface.h" +#include "ArgTraits.h" +#include "StandardTraits.h" + +namespace TCLAP { + +/** + * A virtual base class that defines the essential data for all arguments. + * This class, or one of its existing children, must be subclassed to do + * anything. + */ +class Arg +{ + private: + /** + * Prevent accidental copying. + */ + Arg(const Arg& rhs); + + /** + * Prevent accidental copying. + */ + Arg& operator=(const Arg& rhs); + + /** + * Indicates whether the rest of the arguments should be ignored. + */ + static bool& ignoreRestRef() { static bool ign = false; return ign; } + + /** + * The delimiter that separates an argument flag/name from the + * value. + */ + static char& delimiterRef() { static char delim = ' '; return delim; } + + protected: + + /** + * The single char flag used to identify the argument. + * This value (preceded by a dash {-}), can be used to identify + * an argument on the command line. The _flag can be blank, + * in fact this is how unlabeled args work. Unlabeled args must + * override appropriate functions to get correct handling. Note + * that the _flag does NOT include the dash as part of the flag. + */ + std::string _flag; + + /** + * A single work namd indentifying the argument. + * This value (preceded by two dashed {--}) can also be used + * to identify an argument on the command line. Note that the + * _name does NOT include the two dashes as part of the _name. The + * _name cannot be blank. + */ + std::string _name; + + /** + * Description of the argument. + */ + std::string _description; + + /** + * Indicating whether the argument is required. + */ + bool _required; + + /** + * Label to be used in usage description. Normally set to + * "required", but can be changed when necessary. + */ + std::string _requireLabel; + + /** + * Indicates whether a value is required for the argument. + * Note that the value may be required but the argument/value + * combination may not be, as specified by _required. + */ + bool _valueRequired; + + /** + * Indicates whether the argument has been set. + * Indicates that a value on the command line has matched the + * name/flag of this argument and the values have been set accordingly. + */ + bool _alreadySet; + + /** + * A pointer to a vistitor object. + * The visitor allows special handling to occur as soon as the + * argument is matched. This defaults to NULL and should not + * be used unless absolutely necessary. + */ + Visitor* _visitor; + + /** + * Whether this argument can be ignored, if desired. + */ + bool _ignoreable; + + /** + * Indicates that the arg was set as part of an XOR and not on the + * command line. + */ + bool _xorSet; + + bool _acceptsMultipleValues; + + /** + * Performs the special handling described by the Vistitor. + */ + void _checkWithVisitor() const; + + /** + * Primary constructor. YOU (yes you) should NEVER construct an Arg + * directly, this is a base class that is extended by various children + * that are meant to be used. Use SwitchArg, ValueArg, MultiArg, + * UnlabeledValueArg, or UnlabeledMultiArg instead. + * + * \param flag - The flag identifying the argument. + * \param name - The name identifying the argument. + * \param desc - The description of the argument, used in the usage. + * \param req - Whether the argument is required. + * \param valreq - Whether the a value is required for the argument. + * \param v - The visitor checked by the argument. Defaults to NULL. + */ + Arg( const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + bool valreq, + Visitor* v = NULL ); + + public: + /** + * Destructor. + */ + virtual ~Arg(); + + /** + * Adds this to the specified list of Args. + * \param argList - The list to add this to. + */ + virtual void addToList( std::list& argList ) const; + + /** + * Begin ignoring arguments since the "--" argument was specified. + */ + static void beginIgnoring() { ignoreRestRef() = true; } + + /** + * Whether to ignore the rest. + */ + static bool ignoreRest() { return ignoreRestRef(); } + + /** + * The delimiter that separates an argument flag/name from the + * value. + */ + static char delimiter() { return delimiterRef(); } + + /** + * The char used as a place holder when SwitchArgs are combined. + * Currently set to the bell char (ASCII 7). + */ + static char blankChar() { return (char)7; } + + /** + * The char that indicates the beginning of a flag. Defaults to '-', but + * clients can define TCLAP_FLAGSTARTCHAR to override. + */ +#ifndef TCLAP_FLAGSTARTCHAR +#define TCLAP_FLAGSTARTCHAR '-' +#endif + static char flagStartChar() { return TCLAP_FLAGSTARTCHAR; } + + /** + * The sting that indicates the beginning of a flag. Defaults to "-", but + * clients can define TCLAP_FLAGSTARTSTRING to override. Should be the same + * as TCLAP_FLAGSTARTCHAR. + */ +#ifndef TCLAP_FLAGSTARTSTRING +#define TCLAP_FLAGSTARTSTRING "-" +#endif + static const std::string flagStartString() { return TCLAP_FLAGSTARTSTRING; } + + /** + * The sting that indicates the beginning of a name. Defaults to "--", but + * clients can define TCLAP_NAMESTARTSTRING to override. + */ +#ifndef TCLAP_NAMESTARTSTRING +#define TCLAP_NAMESTARTSTRING "--" +#endif + static const std::string nameStartString() { return TCLAP_NAMESTARTSTRING; } + + /** + * The name used to identify the ignore rest argument. + */ + static const std::string ignoreNameString() { return "ignore_rest"; } + + /** + * Sets the delimiter for all arguments. + * \param c - The character that delimits flags/names from values. + */ + static void setDelimiter( char c ) { delimiterRef() = c; } + + /** + * Pure virtual method meant to handle the parsing and value assignment + * of the string on the command line. + * \param i - Pointer the the current argument in the list. + * \param args - Mutable list of strings. What is + * passed in from main. + */ + virtual bool processArg(int *i, std::vector& args) = 0; + + /** + * Operator ==. + * Equality operator. Must be virtual to handle unlabeled args. + * \param a - The Arg to be compared to this. + */ + virtual bool operator==(const Arg& a) const; + + /** + * Returns the argument flag. + */ + const std::string& getFlag() const; + + /** + * Returns the argument name. + */ + const std::string& getName() const; + + /** + * Returns the argument description. + */ + std::string getDescription() const; + + /** + * Indicates whether the argument is required. + */ + virtual bool isRequired() const; + + /** + * Sets _required to true. This is used by the XorHandler. + * You really have no reason to ever use it. + */ + void forceRequired(); + + /** + * Sets the _alreadySet value to true. This is used by the XorHandler. + * You really have no reason to ever use it. + */ + void xorSet(); + + /** + * Indicates whether a value must be specified for argument. + */ + bool isValueRequired() const; + + /** + * Indicates whether the argument has already been set. Only true + * if the arg has been matched on the command line. + */ + bool isSet() const; + + /** + * Indicates whether the argument can be ignored, if desired. + */ + bool isIgnoreable() const; + + /** + * A method that tests whether a string matches this argument. + * This is generally called by the processArg() method. This + * method could be re-implemented by a child to change how + * arguments are specified on the command line. + * \param s - The string to be compared to the flag/name to determine + * whether the arg matches. + */ + virtual bool argMatches( const std::string& s ) const; + + /** + * Returns a simple string representation of the argument. + * Primarily for debugging. + */ + virtual std::string toString() const; + + /** + * Returns a short ID for the usage. + * \param valueId - The value used in the id. + */ + virtual std::string shortID( const std::string& valueId = "val" ) const; + + /** + * Returns a long ID for the usage. + * \param valueId - The value used in the id. + */ + virtual std::string longID( const std::string& valueId = "val" ) const; + + /** + * Trims a value off of the flag. + * \param flag - The string from which the flag and value will be + * trimmed. Contains the flag once the value has been trimmed. + * \param value - Where the value trimmed from the string will + * be stored. + */ + virtual void trimFlag( std::string& flag, std::string& value ) const; + + /** + * Checks whether a given string has blank chars, indicating that + * it is a combined SwitchArg. If so, return true, otherwise return + * false. + * \param s - string to be checked. + */ + bool _hasBlanks( const std::string& s ) const; + + /** + * Sets the requireLabel. Used by XorHandler. You shouldn't ever + * use this. + * \param s - Set the requireLabel to this value. + */ + void setRequireLabel( const std::string& s ); + + /** + * Used for MultiArgs and XorHandler to determine whether args + * can still be set. + */ + virtual bool allowMore(); + + /** + * Use by output classes to determine whether an Arg accepts + * multiple values. + */ + virtual bool acceptsMultipleValues(); + + /** + * Clears the Arg object and allows it to be reused by new + * command lines. + */ + virtual void reset(); +}; + +/** + * Typedef of an Arg list iterator. + */ +typedef std::list::iterator ArgListIterator; + +/** + * Typedef of an Arg vector iterator. + */ +typedef std::vector::iterator ArgVectorIterator; + +/** + * Typedef of a Visitor list iterator. + */ +typedef std::list::iterator VisitorListIterator; + +/* + * Extract a value of type T from it's string representation contained + * in strVal. The ValueLike parameter used to select the correct + * specialization of ExtractValue depending on the value traits of T. + * ValueLike traits use operator>> to assign the value from strVal. + */ +template void +ExtractValue(T &destVal, const std::string& strVal, ValueLike vl) +{ + static_cast(vl); // Avoid warning about unused vl + std::istringstream is(strVal); + + int valuesRead = 0; + while ( is.good() ) { + if ( is.peek() != EOF ) +#ifdef TCLAP_SETBASE_ZERO + is >> std::setbase(0) >> destVal; +#else + is >> destVal; +#endif + else + break; + + valuesRead++; + } + + if ( is.fail() ) + throw( ArgParseException("Couldn't read argument value " + "from string '" + strVal + "'")); + + + if ( valuesRead > 1 ) + throw( ArgParseException("More than one valid value parsed from " + "string '" + strVal + "'")); + +} + +/* + * Extract a value of type T from it's string representation contained + * in strVal. The ValueLike parameter used to select the correct + * specialization of ExtractValue depending on the value traits of T. + * StringLike uses assignment (operator=) to assign from strVal. + */ +template void +ExtractValue(T &destVal, const std::string& strVal, StringLike sl) +{ + static_cast(sl); // Avoid warning about unused sl + SetString(destVal, strVal); +} + +////////////////////////////////////////////////////////////////////// +//BEGIN Arg.cpp +////////////////////////////////////////////////////////////////////// + +inline Arg::Arg(const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + bool valreq, + Visitor* v) : + _flag(flag), + _name(name), + _description(desc), + _required(req), + _requireLabel("required"), + _valueRequired(valreq), + _alreadySet(false), + _visitor( v ), + _ignoreable(true), + _xorSet(false), + _acceptsMultipleValues(false) +{ + if ( _flag.length() > 1 ) + throw(SpecificationException( + "Argument flag can only be one character long", toString() ) ); + + if ( _name != ignoreNameString() && + ( _flag == Arg::flagStartString() || + _flag == Arg::nameStartString() || + _flag == " " ) ) + throw(SpecificationException("Argument flag cannot be either '" + + Arg::flagStartString() + "' or '" + + Arg::nameStartString() + "' or a space.", + toString() ) ); + + if ( ( _name.substr( 0, Arg::flagStartString().length() ) == Arg::flagStartString() ) || + ( _name.substr( 0, Arg::nameStartString().length() ) == Arg::nameStartString() ) || + ( _name.find( " ", 0 ) != std::string::npos ) ) + throw(SpecificationException("Argument name begin with either '" + + Arg::flagStartString() + "' or '" + + Arg::nameStartString() + "' or space.", + toString() ) ); + +} + +inline Arg::~Arg() { } + +inline std::string Arg::shortID( const std::string& valueId ) const +{ + std::string id = ""; + + if ( _flag != "" ) + id = Arg::flagStartString() + _flag; + else + id = Arg::nameStartString() + _name; + + if ( _valueRequired ) + id += std::string( 1, Arg::delimiter() ) + "<" + valueId + ">"; + + if ( !_required ) + id = "[" + id + "]"; + + return id; +} + +inline std::string Arg::longID( const std::string& valueId ) const +{ + std::string id = ""; + + if ( _flag != "" ) + { + id += Arg::flagStartString() + _flag; + + if ( _valueRequired ) + id += std::string( 1, Arg::delimiter() ) + "<" + valueId + ">"; + + id += ", "; + } + + id += Arg::nameStartString() + _name; + + if ( _valueRequired ) + id += std::string( 1, Arg::delimiter() ) + "<" + valueId + ">"; + + return id; + +} + +inline bool Arg::operator==(const Arg& a) const +{ + if ( ( _flag != "" && _flag == a._flag ) || _name == a._name) + return true; + else + return false; +} + +inline std::string Arg::getDescription() const +{ + std::string desc = ""; + if ( _required ) + desc = "(" + _requireLabel + ") "; + +// if ( _valueRequired ) +// desc += "(value required) "; + + desc += _description; + return desc; +} + +inline const std::string& Arg::getFlag() const { return _flag; } + +inline const std::string& Arg::getName() const { return _name; } + +inline bool Arg::isRequired() const { return _required; } + +inline bool Arg::isValueRequired() const { return _valueRequired; } + +inline bool Arg::isSet() const +{ + if ( _alreadySet && !_xorSet ) + return true; + else + return false; +} + +inline bool Arg::isIgnoreable() const { return _ignoreable; } + +inline void Arg::setRequireLabel( const std::string& s) +{ + _requireLabel = s; +} + +inline bool Arg::argMatches( const std::string& argFlag ) const +{ + if ( ( argFlag == Arg::flagStartString() + _flag && _flag != "" ) || + argFlag == Arg::nameStartString() + _name ) + return true; + else + return false; +} + +inline std::string Arg::toString() const +{ + std::string s = ""; + + if ( _flag != "" ) + s += Arg::flagStartString() + _flag + " "; + + s += "(" + Arg::nameStartString() + _name + ")"; + + return s; +} + +inline void Arg::_checkWithVisitor() const +{ + if ( _visitor != NULL ) + _visitor->visit(); +} + +/** + * Implementation of trimFlag. + */ +inline void Arg::trimFlag(std::string& flag, std::string& value) const +{ + int stop = 0; + for ( int i = 0; static_cast(i) < flag.length(); i++ ) + if ( flag[i] == Arg::delimiter() ) + { + stop = i; + break; + } + + if ( stop > 1 ) + { + value = flag.substr(stop+1); + flag = flag.substr(0,stop); + } + +} + +/** + * Implementation of _hasBlanks. + */ +inline bool Arg::_hasBlanks( const std::string& s ) const +{ + for ( int i = 1; static_cast(i) < s.length(); i++ ) + if ( s[i] == Arg::blankChar() ) + return true; + + return false; +} + +inline void Arg::forceRequired() +{ + _required = true; +} + +inline void Arg::xorSet() +{ + _alreadySet = true; + _xorSet = true; +} + +/** + * Overridden by Args that need to added to the end of the list. + */ +inline void Arg::addToList( std::list& argList ) const +{ + argList.push_front( const_cast(this) ); +} + +inline bool Arg::allowMore() +{ + return false; +} + +inline bool Arg::acceptsMultipleValues() +{ + return _acceptsMultipleValues; +} + +inline void Arg::reset() +{ + _xorSet = false; + _alreadySet = false; +} + +////////////////////////////////////////////////////////////////////// +//END Arg.cpp +////////////////////////////////////////////////////////////////////// + +} //namespace TCLAP + +#endif + diff --git a/src/tclap/ArgException.h b/src/tclap/ArgException.h new file mode 100644 index 0000000..3411aa9 --- /dev/null +++ b/src/tclap/ArgException.h @@ -0,0 +1,200 @@ +// -*- Mode: c++; c-basic-offset: 4; tab-width: 4; -*- + +/****************************************************************************** + * + * file: ArgException.h + * + * Copyright (c) 2003, Michael E. Smoot . + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + + +#ifndef TCLAP_ARG_EXCEPTION_H +#define TCLAP_ARG_EXCEPTION_H + +#include +#include + +namespace TCLAP { + +/** + * A simple class that defines and argument exception. Should be caught + * whenever a CmdLine is created and parsed. + */ +class ArgException : public std::exception +{ + public: + + /** + * Constructor. + * \param text - The text of the exception. + * \param id - The text identifying the argument source. + * \param td - Text describing the type of ArgException it is. + * of the exception. + */ + ArgException( const std::string& text = "undefined exception", + const std::string& id = "undefined", + const std::string& td = "Generic ArgException") + : std::exception(), + _errorText(text), + _argId( id ), + _typeDescription(td) + { } + + /** + * Destructor. + */ + virtual ~ArgException() throw() { } + + /** + * Returns the error text. + */ + std::string error() const { return ( _errorText ); } + + /** + * Returns the argument id. + */ + std::string argId() const + { + if ( _argId == "undefined" ) + return " "; + else + return ( "Argument: " + _argId ); + } + + /** + * Returns the arg id and error text. + */ + const char* what() const throw() + { + static std::string ex; + ex = _argId + " -- " + _errorText; + return ex.c_str(); + } + + /** + * Returns the type of the exception. Used to explain and distinguish + * between different child exceptions. + */ + std::string typeDescription() const + { + return _typeDescription; + } + + + private: + + /** + * The text of the exception message. + */ + std::string _errorText; + + /** + * The argument related to this exception. + */ + std::string _argId; + + /** + * Describes the type of the exception. Used to distinguish + * between different child exceptions. + */ + std::string _typeDescription; + +}; + +/** + * Thrown from within the child Arg classes when it fails to properly + * parse the argument it has been passed. + */ +class ArgParseException : public ArgException +{ + public: + /** + * Constructor. + * \param text - The text of the exception. + * \param id - The text identifying the argument source + * of the exception. + */ + ArgParseException( const std::string& text = "undefined exception", + const std::string& id = "undefined" ) + : ArgException( text, + id, + std::string( "Exception found while parsing " ) + + std::string( "the value the Arg has been passed." )) + { } +}; + +/** + * Thrown from CmdLine when the arguments on the command line are not + * properly specified, e.g. too many arguments, required argument missing, etc. + */ +class CmdLineParseException : public ArgException +{ + public: + /** + * Constructor. + * \param text - The text of the exception. + * \param id - The text identifying the argument source + * of the exception. + */ + CmdLineParseException( const std::string& text = "undefined exception", + const std::string& id = "undefined" ) + : ArgException( text, + id, + std::string( "Exception found when the values ") + + std::string( "on the command line do not meet ") + + std::string( "the requirements of the defined ") + + std::string( "Args." )) + { } +}; + +/** + * Thrown from Arg and CmdLine when an Arg is improperly specified, e.g. + * same flag as another Arg, same name, etc. + */ +class SpecificationException : public ArgException +{ + public: + /** + * Constructor. + * \param text - The text of the exception. + * \param id - The text identifying the argument source + * of the exception. + */ + SpecificationException( const std::string& text = "undefined exception", + const std::string& id = "undefined" ) + : ArgException( text, + id, + std::string("Exception found when an Arg object ")+ + std::string("is improperly defined by the ") + + std::string("developer." )) + { } + +}; + +class ExitException { +public: + ExitException(int estat) : _estat(estat) {} + + int getExitStatus() const { return _estat; } + +private: + int _estat; +}; + +} // namespace TCLAP + +#endif + diff --git a/src/tclap/ArgTraits.h b/src/tclap/ArgTraits.h new file mode 100644 index 0000000..0b2c18f --- /dev/null +++ b/src/tclap/ArgTraits.h @@ -0,0 +1,87 @@ +// -*- Mode: c++; c-basic-offset: 4; tab-width: 4; -*- + +/****************************************************************************** + * + * file: ArgTraits.h + * + * Copyright (c) 2007, Daniel Aarno, Michael E. Smoot . + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +// This is an internal tclap file, you should probably not have to +// include this directly + +#ifndef TCLAP_ARGTRAITS_H +#define TCLAP_ARGTRAITS_H + +namespace TCLAP { + +// We use two empty structs to get compile type specialization +// function to work + +/** + * A value like argument value type is a value that can be set using + * operator>>. This is the default value type. + */ +struct ValueLike { + typedef ValueLike ValueCategory; + virtual ~ValueLike() {} +}; + +/** + * A string like argument value type is a value that can be set using + * operator=(string). Usefull if the value type contains spaces which + * will be broken up into individual tokens by operator>>. + */ +struct StringLike { + virtual ~StringLike() {} +}; + +/** + * A class can inherit from this object to make it have string like + * traits. This is a compile time thing and does not add any overhead + * to the inherenting class. + */ +struct StringLikeTrait { + typedef StringLike ValueCategory; + virtual ~StringLikeTrait() {} +}; + +/** + * A class can inherit from this object to make it have value like + * traits. This is a compile time thing and does not add any overhead + * to the inherenting class. + */ +struct ValueLikeTrait { + typedef ValueLike ValueCategory; + virtual ~ValueLikeTrait() {} +}; + +/** + * Arg traits are used to get compile type specialization when parsing + * argument values. Using an ArgTraits you can specify the way that + * values gets assigned to any particular type during parsing. The two + * supported types are StringLike and ValueLike. + */ +template +struct ArgTraits { + typedef typename T::ValueCategory ValueCategory; + virtual ~ArgTraits() {} + //typedef ValueLike ValueCategory; +}; + +#endif + +} // namespace diff --git a/src/tclap/CmdLine.h b/src/tclap/CmdLine.h new file mode 100644 index 0000000..0e7dda3 --- /dev/null +++ b/src/tclap/CmdLine.h @@ -0,0 +1,633 @@ +// -*- Mode: c++; c-basic-offset: 4; tab-width: 4; -*- + +/****************************************************************************** + * + * file: CmdLine.h + * + * Copyright (c) 2003, Michael E. Smoot . + * Copyright (c) 2004, Michael E. Smoot, Daniel Aarno. + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +#ifndef TCLAP_CMDLINE_H +#define TCLAP_CMDLINE_H + +#include "SwitchArg.h" +#include "MultiSwitchArg.h" +#include "UnlabeledValueArg.h" +#include "UnlabeledMultiArg.h" + +#include "XorHandler.h" +#include "HelpVisitor.h" +#include "VersionVisitor.h" +#include "IgnoreRestVisitor.h" + +#include "CmdLineOutput.h" +#include "StdOutput.h" + +#include "Constraint.h" +#include "ValuesConstraint.h" + +#include +#include +#include +#include +#include +#include +#include // Needed for exit(), which isn't defined in some envs. + +namespace TCLAP { + +template void DelPtr(T ptr) +{ + delete ptr; +} + +template void ClearContainer(C &c) +{ + typedef typename C::value_type value_type; + std::for_each(c.begin(), c.end(), DelPtr); + c.clear(); +} + + +/** + * The base class that manages the command line definition and passes + * along the parsing to the appropriate Arg classes. + */ +class CmdLine : public CmdLineInterface +{ + protected: + + /** + * The list of arguments that will be tested against the + * command line. + */ + std::list _argList; + + /** + * The name of the program. Set to argv[0]. + */ + std::string _progName; + + /** + * A message used to describe the program. Used in the usage output. + */ + std::string _message; + + /** + * The version to be displayed with the --version switch. + */ + std::string _version; + + /** + * The number of arguments that are required to be present on + * the command line. This is set dynamically, based on the + * Args added to the CmdLine object. + */ + int _numRequired; + + /** + * The character that is used to separate the argument flag/name + * from the value. Defaults to ' ' (space). + */ + char _delimiter; + + /** + * The handler that manages xoring lists of args. + */ + XorHandler _xorHandler; + + /** + * A list of Args to be explicitly deleted when the destructor + * is called. At the moment, this only includes the three default + * Args. + */ + std::list _argDeleteOnExitList; + + /** + * A list of Visitors to be explicitly deleted when the destructor + * is called. At the moment, these are the Vistors created for the + * default Args. + */ + std::list _visitorDeleteOnExitList; + + /** + * Object that handles all output for the CmdLine. + */ + CmdLineOutput* _output; + + /** + * Should CmdLine handle parsing exceptions internally? + */ + bool _handleExceptions; + + /** + * Throws an exception listing the missing args. + */ + void missingArgsException(); + + /** + * Checks whether a name/flag string matches entirely matches + * the Arg::blankChar. Used when multiple switches are combined + * into a single argument. + * \param s - The message to be used in the usage. + */ + bool _emptyCombined(const std::string& s); + + /** + * Perform a delete ptr; operation on ptr when this object is deleted. + */ + void deleteOnExit(Arg* ptr); + + /** + * Perform a delete ptr; operation on ptr when this object is deleted. + */ + void deleteOnExit(Visitor* ptr); + +private: + + /** + * Prevent accidental copying. + */ + CmdLine(const CmdLine& rhs); + CmdLine& operator=(const CmdLine& rhs); + + /** + * Encapsulates the code common to the constructors + * (which is all of it). + */ + void _constructor(); + + + /** + * Is set to true when a user sets the output object. We use this so + * that we don't delete objects that are created outside of this lib. + */ + bool _userSetOutput; + + /** + * Whether or not to automatically create help and version switches. + */ + bool _helpAndVersion; + + public: + + /** + * Command line constructor. Defines how the arguments will be + * parsed. + * \param message - The message to be used in the usage + * output. + * \param delimiter - The character that is used to separate + * the argument flag/name from the value. Defaults to ' ' (space). + * \param version - The version number to be used in the + * --version switch. + * \param helpAndVersion - Whether or not to create the Help and + * Version switches. Defaults to true. + */ + CmdLine(const std::string& message, + const char delimiter = ' ', + const std::string& version = "none", + bool helpAndVersion = true); + + /** + * Deletes any resources allocated by a CmdLine object. + */ + virtual ~CmdLine(); + + /** + * Adds an argument to the list of arguments to be parsed. + * \param a - Argument to be added. + */ + void add( Arg& a ); + + /** + * An alternative add. Functionally identical. + * \param a - Argument to be added. + */ + void add( Arg* a ); + + /** + * Add two Args that will be xor'd. If this method is used, add does + * not need to be called. + * \param a - Argument to be added and xor'd. + * \param b - Argument to be added and xor'd. + */ + void xorAdd( Arg& a, Arg& b ); + + /** + * Add a list of Args that will be xor'd. If this method is used, + * add does not need to be called. + * \param xors - List of Args to be added and xor'd. + */ + void xorAdd( std::vector& xors ); + + /** + * Parses the command line. + * \param argc - Number of arguments. + * \param argv - Array of arguments. + */ + void parse(int argc, const char * const * argv); + + /** + * Parses the command line. + * \param args - A vector of strings representing the args. + * args[0] is still the program name. + */ + void parse(std::vector& args); + + /** + * + */ + CmdLineOutput* getOutput(); + + /** + * + */ + void setOutput(CmdLineOutput* co); + + /** + * + */ + std::string& getVersion(); + + /** + * + */ + std::string& getProgramName(); + + /** + * + */ + std::list& getArgList(); + + /** + * + */ + XorHandler& getXorHandler(); + + /** + * + */ + char getDelimiter(); + + /** + * + */ + std::string& getMessage(); + + /** + * + */ + bool hasHelpAndVersion(); + + /** + * Disables or enables CmdLine's internal parsing exception handling. + * + * @param state Should CmdLine handle parsing exceptions internally? + */ + void setExceptionHandling(const bool state); + + /** + * Returns the current state of the internal exception handling. + * + * @retval true Parsing exceptions are handled internally. + * @retval false Parsing exceptions are propagated to the caller. + */ + bool getExceptionHandling() const; + + /** + * Allows the CmdLine object to be reused. + */ + void reset(); + +}; + + +/////////////////////////////////////////////////////////////////////////////// +//Begin CmdLine.cpp +/////////////////////////////////////////////////////////////////////////////// + +inline CmdLine::CmdLine(const std::string& m, + char delim, + const std::string& v, + bool help ) + : + _argList(std::list()), + _progName("not_set_yet"), + _message(m), + _version(v), + _numRequired(0), + _delimiter(delim), + _xorHandler(XorHandler()), + _argDeleteOnExitList(std::list()), + _visitorDeleteOnExitList(std::list()), + _output(0), + _handleExceptions(true), + _userSetOutput(false), + _helpAndVersion(help) +{ + _constructor(); +} + +inline CmdLine::~CmdLine() +{ + ClearContainer(_argDeleteOnExitList); + ClearContainer(_visitorDeleteOnExitList); + + if ( !_userSetOutput ) { + delete _output; + _output = 0; + } +} + +inline void CmdLine::_constructor() +{ + _output = new StdOutput; + + Arg::setDelimiter( _delimiter ); + + Visitor* v; + + if ( _helpAndVersion ) + { + v = new HelpVisitor( this, &_output ); + SwitchArg* help = new SwitchArg("h","help", + "Displays usage information and exits.", + false, v); + add( help ); + deleteOnExit(help); + deleteOnExit(v); + + v = new VersionVisitor( this, &_output ); + SwitchArg* vers = new SwitchArg("","version", + "Displays version information and exits.", + false, v); + add( vers ); + deleteOnExit(vers); + deleteOnExit(v); + } + + v = new IgnoreRestVisitor(); + SwitchArg* ignore = new SwitchArg(Arg::flagStartString(), + Arg::ignoreNameString(), + "Ignores the rest of the labeled arguments following this flag.", + false, v); + add( ignore ); + deleteOnExit(ignore); + deleteOnExit(v); +} + +inline void CmdLine::xorAdd( std::vector& ors ) +{ + _xorHandler.add( ors ); + + for (ArgVectorIterator it = ors.begin(); it != ors.end(); it++) + { + (*it)->forceRequired(); + (*it)->setRequireLabel( "OR required" ); + add( *it ); + } +} + +inline void CmdLine::xorAdd( Arg& a, Arg& b ) +{ + std::vector ors; + ors.push_back( &a ); + ors.push_back( &b ); + xorAdd( ors ); +} + +inline void CmdLine::add( Arg& a ) +{ + add( &a ); +} + +inline void CmdLine::add( Arg* a ) +{ + for( ArgListIterator it = _argList.begin(); it != _argList.end(); it++ ) + if ( *a == *(*it) ) + throw( SpecificationException( + "Argument with same flag/name already exists!", + a->longID() ) ); + + a->addToList( _argList ); + + if ( a->isRequired() ) + _numRequired++; +} + + +inline void CmdLine::parse(int argc, const char * const * argv) +{ + // this step is necessary so that we have easy access to + // mutable strings. + std::vector args; + for (int i = 0; i < argc; i++) + args.push_back(argv[i]); + + parse(args); +} + +inline void CmdLine::parse(std::vector& args) +{ + bool shouldExit = false; + int estat = 0; + + try { + _progName = args.front(); + args.erase(args.begin()); + + int requiredCount = 0; + + for (int i = 0; static_cast(i) < args.size(); i++) + { + bool matched = false; + for (ArgListIterator it = _argList.begin(); + it != _argList.end(); it++) { + if ( (*it)->processArg( &i, args ) ) + { + requiredCount += _xorHandler.check( *it ); + matched = true; + break; + } + } + + // checks to see if the argument is an empty combined + // switch and if so, then we've actually matched it + if ( !matched && _emptyCombined( args[i] ) ) + matched = true; + + if ( !matched && !Arg::ignoreRest() ) + throw(CmdLineParseException("Couldn't find match " + "for argument", + args[i])); + } + + if ( requiredCount < _numRequired ) + missingArgsException(); + + if ( requiredCount > _numRequired ) + throw(CmdLineParseException("Too many arguments!")); + + } catch ( ArgException& e ) { + // If we're not handling the exceptions, rethrow. + if ( !_handleExceptions) { + throw; + } + + try { + _output->failure(*this,e); + } catch ( ExitException &ee ) { + estat = ee.getExitStatus(); + shouldExit = true; + } + } catch (ExitException &ee) { + // If we're not handling the exceptions, rethrow. + if ( !_handleExceptions) { + throw; + } + + estat = ee.getExitStatus(); + shouldExit = true; + } + + if (shouldExit) + exit(estat); +} + +inline bool CmdLine::_emptyCombined(const std::string& s) +{ + if ( s.length() > 0 && s[0] != Arg::flagStartChar() ) + return false; + + for ( int i = 1; static_cast(i) < s.length(); i++ ) + if ( s[i] != Arg::blankChar() ) + return false; + + return true; +} + +inline void CmdLine::missingArgsException() +{ + int count = 0; + + std::string missingArgList; + for (ArgListIterator it = _argList.begin(); it != _argList.end(); it++) + { + if ( (*it)->isRequired() && !(*it)->isSet() ) + { + missingArgList += (*it)->getName(); + missingArgList += ", "; + count++; + } + } + missingArgList = missingArgList.substr(0,missingArgList.length()-2); + + std::string msg; + if ( count > 1 ) + msg = "Required arguments missing: "; + else + msg = "Required argument missing: "; + + msg += missingArgList; + + throw(CmdLineParseException(msg)); +} + +inline void CmdLine::deleteOnExit(Arg* ptr) +{ + _argDeleteOnExitList.push_back(ptr); +} + +inline void CmdLine::deleteOnExit(Visitor* ptr) +{ + _visitorDeleteOnExitList.push_back(ptr); +} + +inline CmdLineOutput* CmdLine::getOutput() +{ + return _output; +} + +inline void CmdLine::setOutput(CmdLineOutput* co) +{ + if ( !_userSetOutput ) + delete _output; + _userSetOutput = true; + _output = co; +} + +inline std::string& CmdLine::getVersion() +{ + return _version; +} + +inline std::string& CmdLine::getProgramName() +{ + return _progName; +} + +inline std::list& CmdLine::getArgList() +{ + return _argList; +} + +inline XorHandler& CmdLine::getXorHandler() +{ + return _xorHandler; +} + +inline char CmdLine::getDelimiter() +{ + return _delimiter; +} + +inline std::string& CmdLine::getMessage() +{ + return _message; +} + +inline bool CmdLine::hasHelpAndVersion() +{ + return _helpAndVersion; +} + +inline void CmdLine::setExceptionHandling(const bool state) +{ + _handleExceptions = state; +} + +inline bool CmdLine::getExceptionHandling() const +{ + return _handleExceptions; +} + +inline void CmdLine::reset() +{ + for( ArgListIterator it = _argList.begin(); it != _argList.end(); it++ ) + (*it)->reset(); + + _progName.clear(); +} + +/////////////////////////////////////////////////////////////////////////////// +//End CmdLine.cpp +/////////////////////////////////////////////////////////////////////////////// + + + +} //namespace TCLAP +#endif diff --git a/src/tclap/CmdLineInterface.h b/src/tclap/CmdLineInterface.h new file mode 100644 index 0000000..1b25e9b --- /dev/null +++ b/src/tclap/CmdLineInterface.h @@ -0,0 +1,150 @@ + +/****************************************************************************** + * + * file: CmdLineInterface.h + * + * Copyright (c) 2003, Michael E. Smoot . + * Copyright (c) 2004, Michael E. Smoot, Daniel Aarno. + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +#ifndef TCLAP_COMMANDLINE_INTERFACE_H +#define TCLAP_COMMANDLINE_INTERFACE_H + +#include +#include +#include +#include +#include + + +namespace TCLAP { + +class Arg; +class CmdLineOutput; +class XorHandler; + +/** + * The base class that manages the command line definition and passes + * along the parsing to the appropriate Arg classes. + */ +class CmdLineInterface +{ + public: + + /** + * Destructor + */ + virtual ~CmdLineInterface() {} + + /** + * Adds an argument to the list of arguments to be parsed. + * \param a - Argument to be added. + */ + virtual void add( Arg& a )=0; + + /** + * An alternative add. Functionally identical. + * \param a - Argument to be added. + */ + virtual void add( Arg* a )=0; + + /** + * Add two Args that will be xor'd. + * If this method is used, add does + * not need to be called. + * \param a - Argument to be added and xor'd. + * \param b - Argument to be added and xor'd. + */ + virtual void xorAdd( Arg& a, Arg& b )=0; + + /** + * Add a list of Args that will be xor'd. If this method is used, + * add does not need to be called. + * \param xors - List of Args to be added and xor'd. + */ + virtual void xorAdd( std::vector& xors )=0; + + /** + * Parses the command line. + * \param argc - Number of arguments. + * \param argv - Array of arguments. + */ + virtual void parse(int argc, const char * const * argv)=0; + + /** + * Parses the command line. + * \param args - A vector of strings representing the args. + * args[0] is still the program name. + */ + void parse(std::vector& args); + + /** + * Returns the CmdLineOutput object. + */ + virtual CmdLineOutput* getOutput()=0; + + /** + * \param co - CmdLineOutput object that we want to use instead. + */ + virtual void setOutput(CmdLineOutput* co)=0; + + /** + * Returns the version string. + */ + virtual std::string& getVersion()=0; + + /** + * Returns the program name string. + */ + virtual std::string& getProgramName()=0; + + /** + * Returns the argList. + */ + virtual std::list& getArgList()=0; + + /** + * Returns the XorHandler. + */ + virtual XorHandler& getXorHandler()=0; + + /** + * Returns the delimiter string. + */ + virtual char getDelimiter()=0; + + /** + * Returns the message string. + */ + virtual std::string& getMessage()=0; + + /** + * Indicates whether or not the help and version switches were created + * automatically. + */ + virtual bool hasHelpAndVersion()=0; + + /** + * Resets the instance as if it had just been constructed so that the + * instance can be reused. + */ + virtual void reset()=0; +}; + +} //namespace + + +#endif diff --git a/src/tclap/CmdLineOutput.h b/src/tclap/CmdLineOutput.h new file mode 100644 index 0000000..71ee5a3 --- /dev/null +++ b/src/tclap/CmdLineOutput.h @@ -0,0 +1,74 @@ + + +/****************************************************************************** + * + * file: CmdLineOutput.h + * + * Copyright (c) 2004, Michael E. Smoot + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +#ifndef TCLAP_CMDLINEOUTPUT_H +#define TCLAP_CMDLINEOUTPUT_H + +#include +#include +#include +#include +#include +#include + +namespace TCLAP { + +class CmdLineInterface; +class ArgException; + +/** + * The interface that any output object must implement. + */ +class CmdLineOutput +{ + + public: + + /** + * Virtual destructor. + */ + virtual ~CmdLineOutput() {} + + /** + * Generates some sort of output for the USAGE. + * \param c - The CmdLine object the output is generated for. + */ + virtual void usage(CmdLineInterface& c)=0; + + /** + * Generates some sort of output for the version. + * \param c - The CmdLine object the output is generated for. + */ + virtual void version(CmdLineInterface& c)=0; + + /** + * Generates some sort of output for a failure. + * \param c - The CmdLine object the output is generated for. + * \param e - The ArgException that caused the failure. + */ + virtual void failure( CmdLineInterface& c, + ArgException& e )=0; + +}; + +} //namespace TCLAP +#endif diff --git a/src/tclap/Constraint.h b/src/tclap/Constraint.h new file mode 100644 index 0000000..a92acf9 --- /dev/null +++ b/src/tclap/Constraint.h @@ -0,0 +1,68 @@ + +/****************************************************************************** + * + * file: Constraint.h + * + * Copyright (c) 2005, Michael E. Smoot + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +#ifndef TCLAP_CONSTRAINT_H +#define TCLAP_CONSTRAINT_H + +#include +#include +#include +#include +#include +#include + +namespace TCLAP { + +/** + * The interface that defines the interaction between the Arg and Constraint. + */ +template +class Constraint +{ + + public: + /** + * Returns a description of the Constraint. + */ + virtual std::string description() const =0; + + /** + * Returns the short ID for the Constraint. + */ + virtual std::string shortID() const =0; + + /** + * The method used to verify that the value parsed from the command + * line meets the constraint. + * \param value - The value that will be checked. + */ + virtual bool check(const T& value) const =0; + + /** + * Destructor. + * Silences warnings about Constraint being a base class with virtual + * functions but without a virtual destructor. + */ + virtual ~Constraint() { ; } +}; + +} //namespace TCLAP +#endif diff --git a/src/tclap/DocBookOutput.h b/src/tclap/DocBookOutput.h new file mode 100644 index 0000000..a42ca27 --- /dev/null +++ b/src/tclap/DocBookOutput.h @@ -0,0 +1,299 @@ +// -*- Mode: c++; c-basic-offset: 4; tab-width: 4; -*- + +/****************************************************************************** + * + * file: DocBookOutput.h + * + * Copyright (c) 2004, Michael E. Smoot + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +#ifndef TCLAP_DOCBOOKOUTPUT_H +#define TCLAP_DOCBOOKOUTPUT_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace TCLAP { + +/** + * A class that generates DocBook output for usage() method for the + * given CmdLine and its Args. + */ +class DocBookOutput : public CmdLineOutput +{ + + public: + + /** + * Prints the usage to stdout. Can be overridden to + * produce alternative behavior. + * \param c - The CmdLine object the output is generated for. + */ + virtual void usage(CmdLineInterface& c); + + /** + * Prints the version to stdout. Can be overridden + * to produce alternative behavior. + * \param c - The CmdLine object the output is generated for. + */ + virtual void version(CmdLineInterface& c); + + /** + * Prints (to stderr) an error message, short usage + * Can be overridden to produce alternative behavior. + * \param c - The CmdLine object the output is generated for. + * \param e - The ArgException that caused the failure. + */ + virtual void failure(CmdLineInterface& c, + ArgException& e ); + + protected: + + /** + * Substitutes the char r for string x in string s. + * \param s - The string to operate on. + * \param r - The char to replace. + * \param x - What to replace r with. + */ + void substituteSpecialChars( std::string& s, char r, std::string& x ); + void removeChar( std::string& s, char r); + void basename( std::string& s ); + + void printShortArg(Arg* it); + void printLongArg(Arg* it); + + char theDelimiter; +}; + + +inline void DocBookOutput::version(CmdLineInterface& _cmd) +{ + std::cout << _cmd.getVersion() << std::endl; +} + +inline void DocBookOutput::usage(CmdLineInterface& _cmd ) +{ + std::list argList = _cmd.getArgList(); + std::string progName = _cmd.getProgramName(); + std::string xversion = _cmd.getVersion(); + theDelimiter = _cmd.getDelimiter(); + XorHandler xorHandler = _cmd.getXorHandler(); + std::vector< std::vector > xorList = xorHandler.getXorList(); + basename(progName); + + std::cout << "" << std::endl; + std::cout << "" << std::endl << std::endl; + + std::cout << "" << std::endl; + + std::cout << "" << std::endl; + std::cout << "" << progName << "" << std::endl; + std::cout << "1" << std::endl; + std::cout << "" << std::endl; + + std::cout << "" << std::endl; + std::cout << "" << progName << "" << std::endl; + std::cout << "" << _cmd.getMessage() << "" << std::endl; + std::cout << "" << std::endl; + + std::cout << "" << std::endl; + std::cout << "" << std::endl; + + std::cout << "" << progName << "" << std::endl; + + // xor + for ( int i = 0; (unsigned int)i < xorList.size(); i++ ) + { + std::cout << "" << std::endl; + for ( ArgVectorIterator it = xorList[i].begin(); + it != xorList[i].end(); it++ ) + printShortArg((*it)); + + std::cout << "" << std::endl; + } + + // rest of args + for (ArgListIterator it = argList.begin(); it != argList.end(); it++) + if ( !xorHandler.contains( (*it) ) ) + printShortArg((*it)); + + std::cout << "" << std::endl; + std::cout << "" << std::endl; + + std::cout << "" << std::endl; + std::cout << "Description" << std::endl; + std::cout << "" << std::endl; + std::cout << _cmd.getMessage() << std::endl; + std::cout << "" << std::endl; + std::cout << "" << std::endl; + + std::cout << "" << std::endl; + std::cout << "Options" << std::endl; + + std::cout << "" << std::endl; + + for (ArgListIterator it = argList.begin(); it != argList.end(); it++) + printLongArg((*it)); + + std::cout << "" << std::endl; + std::cout << "" << std::endl; + + std::cout << "" << std::endl; + std::cout << "Version" << std::endl; + std::cout << "" << std::endl; + std::cout << xversion << std::endl; + std::cout << "" << std::endl; + std::cout << "" << std::endl; + + std::cout << "" << std::endl; + +} + +inline void DocBookOutput::failure( CmdLineInterface& _cmd, + ArgException& e ) +{ + static_cast(_cmd); // unused + std::cout << e.what() << std::endl; + throw ExitException(1); +} + +inline void DocBookOutput::substituteSpecialChars( std::string& s, + char r, + std::string& x ) +{ + size_t p; + while ( (p = s.find_first_of(r)) != std::string::npos ) + { + s.erase(p,1); + s.insert(p,x); + } +} + +inline void DocBookOutput::removeChar( std::string& s, char r) +{ + size_t p; + while ( (p = s.find_first_of(r)) != std::string::npos ) + { + s.erase(p,1); + } +} + +inline void DocBookOutput::basename( std::string& s ) +{ + size_t p = s.find_last_of('/'); + if ( p != std::string::npos ) + { + s.erase(0, p + 1); + } +} + +inline void DocBookOutput::printShortArg(Arg* a) +{ + std::string lt = "<"; + std::string gt = ">"; + + std::string id = a->shortID(); + substituteSpecialChars(id,'<',lt); + substituteSpecialChars(id,'>',gt); + removeChar(id,'['); + removeChar(id,']'); + + std::string choice = "opt"; + if ( a->isRequired() ) + choice = "plain"; + + std::cout << "acceptsMultipleValues() ) + std::cout << " rep='repeat'"; + + + std::cout << '>'; + if ( !a->getFlag().empty() ) + std::cout << a->flagStartChar() << a->getFlag(); + else + std::cout << a->nameStartString() << a->getName(); + if ( a->isValueRequired() ) + { + std::string arg = a->shortID(); + removeChar(arg,'['); + removeChar(arg,']'); + removeChar(arg,'<'); + removeChar(arg,'>'); + arg.erase(0, arg.find_last_of(theDelimiter) + 1); + std::cout << theDelimiter; + std::cout << "" << arg << ""; + } + std::cout << "" << std::endl; + +} + +inline void DocBookOutput::printLongArg(Arg* a) +{ + std::string lt = "<"; + std::string gt = ">"; + + std::string desc = a->getDescription(); + substituteSpecialChars(desc,'<',lt); + substituteSpecialChars(desc,'>',gt); + + std::cout << "" << std::endl; + + if ( !a->getFlag().empty() ) + { + std::cout << "" << std::endl; + std::cout << "" << std::endl; + std::cout << "" << std::endl; + } + + std::cout << "" << std::endl; + std::cout << "" << std::endl; + std::cout << "" << std::endl; + + std::cout << "" << std::endl; + std::cout << "" << std::endl; + std::cout << desc << std::endl; + std::cout << "" << std::endl; + std::cout << "" << std::endl; + + std::cout << "" << std::endl; +} + +} //namespace TCLAP +#endif diff --git a/src/tclap/HelpVisitor.h b/src/tclap/HelpVisitor.h new file mode 100644 index 0000000..076c640 --- /dev/null +++ b/src/tclap/HelpVisitor.h @@ -0,0 +1,76 @@ + +/****************************************************************************** + * + * file: HelpVisitor.h + * + * Copyright (c) 2003, Michael E. Smoot . + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +#ifndef TCLAP_HELP_VISITOR_H +#define TCLAP_HELP_VISITOR_H + +#include "CmdLineInterface.h" +#include "CmdLineOutput.h" +#include "Visitor.h" + +namespace TCLAP { + +/** + * A Visitor object that calls the usage method of the given CmdLineOutput + * object for the specified CmdLine object. + */ +class HelpVisitor: public Visitor +{ + private: + /** + * Prevent accidental copying. + */ + HelpVisitor(const HelpVisitor& rhs); + HelpVisitor& operator=(const HelpVisitor& rhs); + + protected: + + /** + * The CmdLine the output will be generated for. + */ + CmdLineInterface* _cmd; + + /** + * The output object. + */ + CmdLineOutput** _out; + + public: + + /** + * Constructor. + * \param cmd - The CmdLine the output will be generated for. + * \param out - The type of output. + */ + HelpVisitor(CmdLineInterface* cmd, CmdLineOutput** out) + : Visitor(), _cmd( cmd ), _out( out ) { } + + /** + * Calls the usage method of the CmdLineOutput for the + * specified CmdLine. + */ + void visit() { (*_out)->usage(*_cmd); throw ExitException(0); } + +}; + +} + +#endif diff --git a/src/tclap/IgnoreRestVisitor.h b/src/tclap/IgnoreRestVisitor.h new file mode 100644 index 0000000..09bdba7 --- /dev/null +++ b/src/tclap/IgnoreRestVisitor.h @@ -0,0 +1,52 @@ + +/****************************************************************************** + * + * file: IgnoreRestVisitor.h + * + * Copyright (c) 2003, Michael E. Smoot . + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + + +#ifndef TCLAP_IGNORE_REST_VISITOR_H +#define TCLAP_IGNORE_REST_VISITOR_H + +#include "Visitor.h" +#include "Arg.h" + +namespace TCLAP { + +/** + * A Vistor that tells the CmdLine to begin ignoring arguments after + * this one is parsed. + */ +class IgnoreRestVisitor: public Visitor +{ + public: + + /** + * Constructor. + */ + IgnoreRestVisitor() : Visitor() {} + + /** + * Sets Arg::_ignoreRest. + */ + void visit() { Arg::beginIgnoring(); } +}; + +} + +#endif diff --git a/src/tclap/MultiArg.h b/src/tclap/MultiArg.h new file mode 100644 index 0000000..63c88c7 --- /dev/null +++ b/src/tclap/MultiArg.h @@ -0,0 +1,433 @@ +/****************************************************************************** + * + * file: MultiArg.h + * + * Copyright (c) 2003, Michael E. Smoot . + * Copyright (c) 2004, Michael E. Smoot, Daniel Aarno. + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + + +#ifndef TCLAP_MULTIPLE_ARGUMENT_H +#define TCLAP_MULTIPLE_ARGUMENT_H + +#include +#include + +#include "Arg.h" +#include "Constraint.h" + +namespace TCLAP { +/** + * An argument that allows multiple values of type T to be specified. Very + * similar to a ValueArg, except a vector of values will be returned + * instead of just one. + */ +template +class MultiArg : public Arg +{ +public: + typedef std::vector container_type; + typedef typename container_type::iterator iterator; + typedef typename container_type::const_iterator const_iterator; + +protected: + + /** + * The list of values parsed from the CmdLine. + */ + std::vector _values; + + /** + * The description of type T to be used in the usage. + */ + std::string _typeDesc; + + /** + * A list of constraint on this Arg. + */ + Constraint* _constraint; + + /** + * Extracts the value from the string. + * Attempts to parse string as type T, if this fails an exception + * is thrown. + * \param val - The string to be read. + */ + void _extractValue( const std::string& val ); + + /** + * Used by XorHandler to decide whether to keep parsing for this arg. + */ + bool _allowMore; + +public: + + /** + * Constructor. + * \param flag - The one character flag that identifies this + * argument on the command line. + * \param name - A one word name for the argument. Can be + * used as a long flag on the command line. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param typeDesc - A short, human readable description of the + * type that this object expects. This is used in the generation + * of the USAGE statement. The goal is to be helpful to the end user + * of the program. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + MultiArg( const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + const std::string& typeDesc, + Visitor* v = NULL); + + /** + * Constructor. + * \param flag - The one character flag that identifies this + * argument on the command line. + * \param name - A one word name for the argument. Can be + * used as a long flag on the command line. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param typeDesc - A short, human readable description of the + * type that this object expects. This is used in the generation + * of the USAGE statement. The goal is to be helpful to the end user + * of the program. + * \param parser - A CmdLine parser object to add this Arg to + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + MultiArg( const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + const std::string& typeDesc, + CmdLineInterface& parser, + Visitor* v = NULL ); + + /** + * Constructor. + * \param flag - The one character flag that identifies this + * argument on the command line. + * \param name - A one word name for the argument. Can be + * used as a long flag on the command line. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param constraint - A pointer to a Constraint object used + * to constrain this Arg. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + MultiArg( const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + Constraint* constraint, + Visitor* v = NULL ); + + /** + * Constructor. + * \param flag - The one character flag that identifies this + * argument on the command line. + * \param name - A one word name for the argument. Can be + * used as a long flag on the command line. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param constraint - A pointer to a Constraint object used + * to constrain this Arg. + * \param parser - A CmdLine parser object to add this Arg to + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + MultiArg( const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + Constraint* constraint, + CmdLineInterface& parser, + Visitor* v = NULL ); + + /** + * Handles the processing of the argument. + * This re-implements the Arg version of this method to set the + * _value of the argument appropriately. It knows the difference + * between labeled and unlabeled. + * \param i - Pointer the the current argument in the list. + * \param args - Mutable list of strings. Passed from main(). + */ + virtual bool processArg(int* i, std::vector& args); + + /** + * Returns a vector of type T containing the values parsed from + * the command line. + */ + const std::vector& getValue(); + + /** + * Returns an iterator over the values parsed from the command + * line. + */ + const_iterator begin() const { return _values.begin(); } + + /** + * Returns the end of the values parsed from the command + * line. + */ + const_iterator end() const { return _values.end(); } + + /** + * Returns the a short id string. Used in the usage. + * \param val - value to be used. + */ + virtual std::string shortID(const std::string& val="val") const; + + /** + * Returns the a long id string. Used in the usage. + * \param val - value to be used. + */ + virtual std::string longID(const std::string& val="val") const; + + /** + * Once we've matched the first value, then the arg is no longer + * required. + */ + virtual bool isRequired() const; + + virtual bool allowMore(); + + virtual void reset(); + +private: + /** + * Prevent accidental copying + */ + MultiArg(const MultiArg& rhs); + MultiArg& operator=(const MultiArg& rhs); + +}; + +template +MultiArg::MultiArg(const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + const std::string& typeDesc, + Visitor* v) : + Arg( flag, name, desc, req, true, v ), + _values(std::vector()), + _typeDesc( typeDesc ), + _constraint( NULL ), + _allowMore(false) +{ + _acceptsMultipleValues = true; +} + +template +MultiArg::MultiArg(const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + const std::string& typeDesc, + CmdLineInterface& parser, + Visitor* v) +: Arg( flag, name, desc, req, true, v ), + _values(std::vector()), + _typeDesc( typeDesc ), + _constraint( NULL ), + _allowMore(false) +{ + parser.add( this ); + _acceptsMultipleValues = true; +} + +/** + * + */ +template +MultiArg::MultiArg(const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + Constraint* constraint, + Visitor* v) +: Arg( flag, name, desc, req, true, v ), + _values(std::vector()), + _typeDesc( constraint->shortID() ), + _constraint( constraint ), + _allowMore(false) +{ + _acceptsMultipleValues = true; +} + +template +MultiArg::MultiArg(const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + Constraint* constraint, + CmdLineInterface& parser, + Visitor* v) +: Arg( flag, name, desc, req, true, v ), + _values(std::vector()), + _typeDesc( constraint->shortID() ), + _constraint( constraint ), + _allowMore(false) +{ + parser.add( this ); + _acceptsMultipleValues = true; +} + +template +const std::vector& MultiArg::getValue() { return _values; } + +template +bool MultiArg::processArg(int *i, std::vector& args) +{ + if ( _ignoreable && Arg::ignoreRest() ) + return false; + + if ( _hasBlanks( args[*i] ) ) + return false; + + std::string flag = args[*i]; + std::string value = ""; + + trimFlag( flag, value ); + + if ( argMatches( flag ) ) + { + if ( Arg::delimiter() != ' ' && value == "" ) + throw( ArgParseException( + "Couldn't find delimiter for this argument!", + toString() ) ); + + // always take the first one, regardless of start string + if ( value == "" ) + { + (*i)++; + if ( static_cast(*i) < args.size() ) + _extractValue( args[*i] ); + else + throw( ArgParseException("Missing a value for this argument!", + toString() ) ); + } + else + _extractValue( value ); + + /* + // continuing taking the args until we hit one with a start string + while ( (unsigned int)(*i)+1 < args.size() && + args[(*i)+1].find_first_of( Arg::flagStartString() ) != 0 && + args[(*i)+1].find_first_of( Arg::nameStartString() ) != 0 ) + _extractValue( args[++(*i)] ); + */ + + _alreadySet = true; + _checkWithVisitor(); + + return true; + } + else + return false; +} + +/** + * + */ +template +std::string MultiArg::shortID(const std::string& val) const +{ + static_cast(val); // Ignore input, don't warn + return Arg::shortID(_typeDesc) + " ... "; +} + +/** + * + */ +template +std::string MultiArg::longID(const std::string& val) const +{ + static_cast(val); // Ignore input, don't warn + return Arg::longID(_typeDesc) + " (accepted multiple times)"; +} + +/** + * Once we've matched the first value, then the arg is no longer + * required. + */ +template +bool MultiArg::isRequired() const +{ + if ( _required ) + { + if ( _values.size() > 1 ) + return false; + else + return true; + } + else + return false; + +} + +template +void MultiArg::_extractValue( const std::string& val ) +{ + try { + T tmp; + ExtractValue(tmp, val, typename ArgTraits::ValueCategory()); + _values.push_back(tmp); + } catch( ArgParseException &e) { + throw ArgParseException(e.error(), toString()); + } + + if ( _constraint != NULL ) + if ( ! _constraint->check( _values.back() ) ) + throw( CmdLineParseException( "Value '" + val + + "' does not meet constraint: " + + _constraint->description(), + toString() ) ); +} + +template +bool MultiArg::allowMore() +{ + bool am = _allowMore; + _allowMore = true; + return am; +} + +template +void MultiArg::reset() +{ + Arg::reset(); + _values.clear(); +} + +} // namespace TCLAP + +#endif diff --git a/src/tclap/MultiSwitchArg.h b/src/tclap/MultiSwitchArg.h new file mode 100644 index 0000000..7661d85 --- /dev/null +++ b/src/tclap/MultiSwitchArg.h @@ -0,0 +1,216 @@ + +/****************************************************************************** +* +* file: MultiSwitchArg.h +* +* Copyright (c) 2003, Michael E. Smoot . +* Copyright (c) 2004, Michael E. Smoot, Daniel Aarno. +* Copyright (c) 2005, Michael E. Smoot, Daniel Aarno, Erik Zeek. +* All rights reverved. +* +* See the file COPYING in the top directory of this distribution for +* more information. +* +* THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS +* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +* +*****************************************************************************/ + + +#ifndef TCLAP_MULTI_SWITCH_ARG_H +#define TCLAP_MULTI_SWITCH_ARG_H + +#include +#include + +#include "SwitchArg.h" + +namespace TCLAP { + +/** +* A multiple switch argument. If the switch is set on the command line, then +* the getValue method will return the number of times the switch appears. +*/ +class MultiSwitchArg : public SwitchArg +{ + protected: + + /** + * The value of the switch. + */ + int _value; + + /** + * Used to support the reset() method so that ValueArg can be + * reset to their constructed value. + */ + int _default; + + public: + + /** + * MultiSwitchArg constructor. + * \param flag - The one character flag that identifies this + * argument on the command line. + * \param name - A one word name for the argument. Can be + * used as a long flag on the command line. + * \param desc - A description of what the argument is for or + * does. + * \param init - Optional. The initial/default value of this Arg. + * Defaults to 0. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + MultiSwitchArg(const std::string& flag, + const std::string& name, + const std::string& desc, + int init = 0, + Visitor* v = NULL); + + + /** + * MultiSwitchArg constructor. + * \param flag - The one character flag that identifies this + * argument on the command line. + * \param name - A one word name for the argument. Can be + * used as a long flag on the command line. + * \param desc - A description of what the argument is for or + * does. + * \param parser - A CmdLine parser object to add this Arg to + * \param init - Optional. The initial/default value of this Arg. + * Defaults to 0. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + MultiSwitchArg(const std::string& flag, + const std::string& name, + const std::string& desc, + CmdLineInterface& parser, + int init = 0, + Visitor* v = NULL); + + + /** + * Handles the processing of the argument. + * This re-implements the SwitchArg version of this method to set the + * _value of the argument appropriately. + * \param i - Pointer the the current argument in the list. + * \param args - Mutable list of strings. Passed + * in from main(). + */ + virtual bool processArg(int* i, std::vector& args); + + /** + * Returns int, the number of times the switch has been set. + */ + int getValue(); + + /** + * Returns the shortID for this Arg. + */ + std::string shortID(const std::string& val) const; + + /** + * Returns the longID for this Arg. + */ + std::string longID(const std::string& val) const; + + void reset(); + +}; + +////////////////////////////////////////////////////////////////////// +//BEGIN MultiSwitchArg.cpp +////////////////////////////////////////////////////////////////////// +inline MultiSwitchArg::MultiSwitchArg(const std::string& flag, + const std::string& name, + const std::string& desc, + int init, + Visitor* v ) +: SwitchArg(flag, name, desc, false, v), +_value( init ), +_default( init ) +{ } + +inline MultiSwitchArg::MultiSwitchArg(const std::string& flag, + const std::string& name, + const std::string& desc, + CmdLineInterface& parser, + int init, + Visitor* v ) +: SwitchArg(flag, name, desc, false, v), +_value( init ), +_default( init ) +{ + parser.add( this ); +} + +inline int MultiSwitchArg::getValue() { return _value; } + +inline bool MultiSwitchArg::processArg(int *i, std::vector& args) +{ + if ( _ignoreable && Arg::ignoreRest() ) + return false; + + if ( argMatches( args[*i] )) + { + // so the isSet() method will work + _alreadySet = true; + + // Matched argument: increment value. + ++_value; + + _checkWithVisitor(); + + return true; + } + else if ( combinedSwitchesMatch( args[*i] ) ) + { + // so the isSet() method will work + _alreadySet = true; + + // Matched argument: increment value. + ++_value; + + // Check for more in argument and increment value. + while ( combinedSwitchesMatch( args[*i] ) ) + ++_value; + + _checkWithVisitor(); + + return false; + } + else + return false; +} + +inline std::string +MultiSwitchArg::shortID(const std::string& val) const +{ + return Arg::shortID(val) + " ... "; +} + +inline std::string +MultiSwitchArg::longID(const std::string& val) const +{ + return Arg::longID(val) + " (accepted multiple times)"; +} + +inline void +MultiSwitchArg::reset() +{ + MultiSwitchArg::_value = MultiSwitchArg::_default; +} + +////////////////////////////////////////////////////////////////////// +//END MultiSwitchArg.cpp +////////////////////////////////////////////////////////////////////// + +} //namespace TCLAP + +#endif diff --git a/src/tclap/OptionalUnlabeledTracker.h b/src/tclap/OptionalUnlabeledTracker.h new file mode 100644 index 0000000..8174c5f --- /dev/null +++ b/src/tclap/OptionalUnlabeledTracker.h @@ -0,0 +1,62 @@ + + +/****************************************************************************** + * + * file: OptionalUnlabeledTracker.h + * + * Copyright (c) 2005, Michael E. Smoot . + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + + +#ifndef TCLAP_OPTIONAL_UNLABELED_TRACKER_H +#define TCLAP_OPTIONAL_UNLABELED_TRACKER_H + +#include + +namespace TCLAP { + +class OptionalUnlabeledTracker +{ + + public: + + static void check( bool req, const std::string& argName ); + + static void gotOptional() { alreadyOptionalRef() = true; } + + static bool& alreadyOptional() { return alreadyOptionalRef(); } + + private: + + static bool& alreadyOptionalRef() { static bool ct = false; return ct; } +}; + + +inline void OptionalUnlabeledTracker::check( bool req, const std::string& argName ) +{ + if ( OptionalUnlabeledTracker::alreadyOptional() ) + throw( SpecificationException( + "You can't specify ANY Unlabeled Arg following an optional Unlabeled Arg", + argName ) ); + + if ( !req ) + OptionalUnlabeledTracker::gotOptional(); +} + + +} // namespace TCLAP + +#endif diff --git a/src/tclap/StandardTraits.h b/src/tclap/StandardTraits.h new file mode 100644 index 0000000..46d7f6f --- /dev/null +++ b/src/tclap/StandardTraits.h @@ -0,0 +1,208 @@ +// -*- Mode: c++; c-basic-offset: 4; tab-width: 4; -*- + +/****************************************************************************** + * + * file: StandardTraits.h + * + * Copyright (c) 2007, Daniel Aarno, Michael E. Smoot . + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +// This is an internal tclap file, you should probably not have to +// include this directly + +#ifndef TCLAP_STANDARD_TRAITS_H +#define TCLAP_STANDARD_TRAITS_H + +#ifdef HAVE_CONFIG_H +#include // To check for long long +#endif + +// If Microsoft has already typedef'd wchar_t as an unsigned +// short, then compiles will break because it's as if we're +// creating ArgTraits twice for unsigned short. Thus... +#ifdef _MSC_VER +#ifndef _NATIVE_WCHAR_T_DEFINED +#define TCLAP_DONT_DECLARE_WCHAR_T_ARGTRAITS +#endif +#endif + +namespace TCLAP { + +// ====================================================================== +// Integer types +// ====================================================================== + +/** + * longs have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; + +/** + * ints have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; + +/** + * shorts have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; + +/** + * chars have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; + +#ifdef HAVE_LONG_LONG +/** + * long longs have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; +#endif + +// ====================================================================== +// Unsigned integer types +// ====================================================================== + +/** + * unsigned longs have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; + +/** + * unsigned ints have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; + +/** + * unsigned shorts have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; + +/** + * unsigned chars have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; + +// Microsoft implements size_t awkwardly. +#if defined(_MSC_VER) && defined(_M_X64) +/** + * size_ts have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; +#endif + + +#ifdef HAVE_LONG_LONG +/** + * unsigned long longs have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; +#endif + +// ====================================================================== +// Float types +// ====================================================================== + +/** + * floats have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; + +/** + * doubles have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; + +// ====================================================================== +// Other types +// ====================================================================== + +/** + * bools have value-like semantics. + */ +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; + + +/** + * wchar_ts have value-like semantics. + */ +#ifndef TCLAP_DONT_DECLARE_WCHAR_T_ARGTRAITS +template<> +struct ArgTraits { + typedef ValueLike ValueCategory; +}; +#endif + +/** + * Strings have string like argument traits. + */ +template<> +struct ArgTraits { + typedef StringLike ValueCategory; +}; + +template +void SetString(T &dst, const std::string &src) +{ + dst = src; +} + +} // namespace + +#endif + diff --git a/src/tclap/StdOutput.h b/src/tclap/StdOutput.h new file mode 100644 index 0000000..ec1cb93 --- /dev/null +++ b/src/tclap/StdOutput.h @@ -0,0 +1,298 @@ +// -*- Mode: c++; c-basic-offset: 4; tab-width: 4; -*- + +/****************************************************************************** + * + * file: StdOutput.h + * + * Copyright (c) 2004, Michael E. Smoot + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +#ifndef TCLAP_STDCMDLINEOUTPUT_H +#define TCLAP_STDCMDLINEOUTPUT_H + +#include +#include +#include +#include +#include + +#include "CmdLineInterface.h" +#include "CmdLineOutput.h" +#include "XorHandler.h" +#include "Arg.h" + +namespace TCLAP { + +/** + * A class that isolates any output from the CmdLine object so that it + * may be easily modified. + */ +class StdOutput : public CmdLineOutput +{ + + public: + + /** + * Prints the usage to stdout. Can be overridden to + * produce alternative behavior. + * \param c - The CmdLine object the output is generated for. + */ + virtual void usage(CmdLineInterface& c); + + /** + * Prints the version to stdout. Can be overridden + * to produce alternative behavior. + * \param c - The CmdLine object the output is generated for. + */ + virtual void version(CmdLineInterface& c); + + /** + * Prints (to stderr) an error message, short usage + * Can be overridden to produce alternative behavior. + * \param c - The CmdLine object the output is generated for. + * \param e - The ArgException that caused the failure. + */ + virtual void failure(CmdLineInterface& c, + ArgException& e ); + + protected: + + /** + * Writes a brief usage message with short args. + * \param c - The CmdLine object the output is generated for. + * \param os - The stream to write the message to. + */ + void _shortUsage( CmdLineInterface& c, std::ostream& os ) const; + + /** + * Writes a longer usage message with long and short args, + * provides descriptions and prints message. + * \param c - The CmdLine object the output is generated for. + * \param os - The stream to write the message to. + */ + void _longUsage( CmdLineInterface& c, std::ostream& os ) const; + + /** + * This function inserts line breaks and indents long strings + * according the params input. It will only break lines at spaces, + * commas and pipes. + * \param os - The stream to be printed to. + * \param s - The string to be printed. + * \param maxWidth - The maxWidth allowed for the output line. + * \param indentSpaces - The number of spaces to indent the first line. + * \param secondLineOffset - The number of spaces to indent the second + * and all subsequent lines in addition to indentSpaces. + */ + void spacePrint( std::ostream& os, + const std::string& s, + int maxWidth, + int indentSpaces, + int secondLineOffset ) const; + +}; + + +inline void StdOutput::version(CmdLineInterface& _cmd) +{ + std::string progName = _cmd.getProgramName(); + std::string xversion = _cmd.getVersion(); + + std::cout << std::endl << progName << " version: " + << xversion << std::endl << std::endl; +} + +inline void StdOutput::usage(CmdLineInterface& _cmd ) +{ + std::cout << std::endl << "USAGE: " << std::endl << std::endl; + + _shortUsage( _cmd, std::cout ); + + std::cout << std::endl << std::endl << "Where: " << std::endl << std::endl; + + _longUsage( _cmd, std::cout ); + + std::cout << std::endl; + +} + +inline void StdOutput::failure( CmdLineInterface& _cmd, + ArgException& e ) +{ + std::string progName = _cmd.getProgramName(); + + std::cerr << "PARSE ERROR: " << e.argId() << std::endl + << " " << e.error() << std::endl << std::endl; + + if ( _cmd.hasHelpAndVersion() ) + { + std::cerr << "Brief USAGE: " << std::endl; + + _shortUsage( _cmd, std::cerr ); + + std::cerr << std::endl << "For complete USAGE and HELP type: " + << std::endl << " " << progName << " --help" + << std::endl << std::endl; + } + else + usage(_cmd); + + throw ExitException(1); +} + +inline void +StdOutput::_shortUsage( CmdLineInterface& _cmd, + std::ostream& os ) const +{ + std::list argList = _cmd.getArgList(); + std::string progName = _cmd.getProgramName(); + XorHandler xorHandler = _cmd.getXorHandler(); + std::vector< std::vector > xorList = xorHandler.getXorList(); + + std::string s = progName + " "; + + // first the xor + for ( int i = 0; static_cast(i) < xorList.size(); i++ ) + { + s += " {"; + for ( ArgVectorIterator it = xorList[i].begin(); + it != xorList[i].end(); it++ ) + s += (*it)->shortID() + "|"; + + s[s.length()-1] = '}'; + } + + // then the rest + for (ArgListIterator it = argList.begin(); it != argList.end(); it++) + if ( !xorHandler.contains( (*it) ) ) + s += " " + (*it)->shortID(); + + // if the program name is too long, then adjust the second line offset + int secondLineOffset = static_cast(progName.length()) + 2; + if ( secondLineOffset > 75/2 ) + secondLineOffset = static_cast(75/2); + + spacePrint( os, s, 75, 3, secondLineOffset ); +} + +inline void +StdOutput::_longUsage( CmdLineInterface& _cmd, + std::ostream& os ) const +{ + std::list argList = _cmd.getArgList(); + std::string message = _cmd.getMessage(); + XorHandler xorHandler = _cmd.getXorHandler(); + std::vector< std::vector > xorList = xorHandler.getXorList(); + + // first the xor + for ( int i = 0; static_cast(i) < xorList.size(); i++ ) + { + for ( ArgVectorIterator it = xorList[i].begin(); + it != xorList[i].end(); + it++ ) + { + spacePrint( os, (*it)->longID(), 75, 3, 3 ); + spacePrint( os, (*it)->getDescription(), 75, 5, 0 ); + + if ( it+1 != xorList[i].end() ) + spacePrint(os, "-- OR --", 75, 9, 0); + } + os << std::endl << std::endl; + } + + // then the rest + for (ArgListIterator it = argList.begin(); it != argList.end(); it++) + if ( !xorHandler.contains( (*it) ) ) + { + spacePrint( os, (*it)->longID(), 75, 3, 3 ); + spacePrint( os, (*it)->getDescription(), 75, 5, 0 ); + os << std::endl; + } + + os << std::endl; + + spacePrint( os, message, 75, 3, 0 ); +} + +inline void StdOutput::spacePrint( std::ostream& os, + const std::string& s, + int maxWidth, + int indentSpaces, + int secondLineOffset ) const +{ + int len = static_cast(s.length()); + + if ( (len + indentSpaces > maxWidth) && maxWidth > 0 ) + { + int allowedLen = maxWidth - indentSpaces; + int start = 0; + while ( start < len ) + { + // find the substring length + // int stringLen = std::min( len - start, allowedLen ); + // doing it this way to support a VisualC++ 2005 bug + using namespace std; + int stringLen = min( len - start, allowedLen ); + + // trim the length so it doesn't end in middle of a word + if ( stringLen == allowedLen ) + while ( stringLen >= 0 && + s[stringLen+start] != ' ' && + s[stringLen+start] != ',' && + s[stringLen+start] != '|' ) + stringLen--; + + // ok, the word is longer than the line, so just split + // wherever the line ends + if ( stringLen <= 0 ) + stringLen = allowedLen; + + // check for newlines + for ( int i = 0; i < stringLen; i++ ) + if ( s[start+i] == '\n' ) + stringLen = i+1; + + // print the indent + for ( int i = 0; i < indentSpaces; i++ ) + os << " "; + + if ( start == 0 ) + { + // handle second line offsets + indentSpaces += secondLineOffset; + + // adjust allowed len + allowedLen -= secondLineOffset; + } + + os << s.substr(start,stringLen) << std::endl; + + // so we don't start a line with a space + while ( s[stringLen+start] == ' ' && start < len ) + start++; + + start += stringLen; + } + } + else + { + for ( int i = 0; i < indentSpaces; i++ ) + os << " "; + os << s << std::endl; + } +} + +} //namespace TCLAP +#endif diff --git a/src/tclap/SwitchArg.h b/src/tclap/SwitchArg.h new file mode 100644 index 0000000..3a93a93 --- /dev/null +++ b/src/tclap/SwitchArg.h @@ -0,0 +1,266 @@ + +/****************************************************************************** + * + * file: SwitchArg.h + * + * Copyright (c) 2003, Michael E. Smoot . + * Copyright (c) 2004, Michael E. Smoot, Daniel Aarno. + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + + +#ifndef TCLAP_SWITCH_ARG_H +#define TCLAP_SWITCH_ARG_H + +#include +#include + +#include "Arg.h" + +namespace TCLAP { + +/** + * A simple switch argument. If the switch is set on the command line, then + * the getValue method will return the opposite of the default value for the + * switch. + */ +class SwitchArg : public Arg +{ + protected: + + /** + * The value of the switch. + */ + bool _value; + + /** + * Used to support the reset() method so that ValueArg can be + * reset to their constructed value. + */ + bool _default; + + public: + + /** + * SwitchArg constructor. + * \param flag - The one character flag that identifies this + * argument on the command line. + * \param name - A one word name for the argument. Can be + * used as a long flag on the command line. + * \param desc - A description of what the argument is for or + * does. + * \param def - The default value for this Switch. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + SwitchArg(const std::string& flag, + const std::string& name, + const std::string& desc, + bool def = false, + Visitor* v = NULL); + + + /** + * SwitchArg constructor. + * \param flag - The one character flag that identifies this + * argument on the command line. + * \param name - A one word name for the argument. Can be + * used as a long flag on the command line. + * \param desc - A description of what the argument is for or + * does. + * \param parser - A CmdLine parser object to add this Arg to + * \param def - The default value for this Switch. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + SwitchArg(const std::string& flag, + const std::string& name, + const std::string& desc, + CmdLineInterface& parser, + bool def = false, + Visitor* v = NULL); + + + /** + * Handles the processing of the argument. + * This re-implements the Arg version of this method to set the + * _value of the argument appropriately. + * \param i - Pointer the the current argument in the list. + * \param args - Mutable list of strings. Passed + * in from main(). + */ + virtual bool processArg(int* i, std::vector& args); + + /** + * Checks a string to see if any of the chars in the string + * match the flag for this Switch. + */ + bool combinedSwitchesMatch(std::string& combined); + + /** + * Returns bool, whether or not the switch has been set. + */ + bool getValue(); + + virtual void reset(); + + private: + /** + * Checks to see if we've found the last match in + * a combined string. + */ + bool lastCombined(std::string& combined); + + /** + * Does the common processing of processArg. + */ + void commonProcessing(); +}; + +////////////////////////////////////////////////////////////////////// +//BEGIN SwitchArg.cpp +////////////////////////////////////////////////////////////////////// +inline SwitchArg::SwitchArg(const std::string& flag, + const std::string& name, + const std::string& desc, + bool default_val, + Visitor* v ) +: Arg(flag, name, desc, false, false, v), + _value( default_val ), + _default( default_val ) +{ } + +inline SwitchArg::SwitchArg(const std::string& flag, + const std::string& name, + const std::string& desc, + CmdLineInterface& parser, + bool default_val, + Visitor* v ) +: Arg(flag, name, desc, false, false, v), + _value( default_val ), + _default(default_val) +{ + parser.add( this ); +} + +inline bool SwitchArg::getValue() { return _value; } + +inline bool SwitchArg::lastCombined(std::string& combinedSwitches ) +{ + for ( unsigned int i = 1; i < combinedSwitches.length(); i++ ) + if ( combinedSwitches[i] != Arg::blankChar() ) + return false; + + return true; +} + +inline bool SwitchArg::combinedSwitchesMatch(std::string& combinedSwitches ) +{ + // make sure this is actually a combined switch + if ( combinedSwitches.length() > 0 && + combinedSwitches[0] != Arg::flagStartString()[0] ) + return false; + + // make sure it isn't a long name + if ( combinedSwitches.substr( 0, Arg::nameStartString().length() ) == + Arg::nameStartString() ) + return false; + + // make sure the delimiter isn't in the string + if ( combinedSwitches.find_first_of( Arg::delimiter() ) != std::string::npos ) + return false; + + // ok, we're not specifying a ValueArg, so we know that we have + // a combined switch list. + for ( unsigned int i = 1; i < combinedSwitches.length(); i++ ) + if ( _flag.length() > 0 && + combinedSwitches[i] == _flag[0] && + _flag[0] != Arg::flagStartString()[0] ) + { + // update the combined switches so this one is no longer present + // this is necessary so that no unlabeled args are matched + // later in the processing. + //combinedSwitches.erase(i,1); + combinedSwitches[i] = Arg::blankChar(); + return true; + } + + // none of the switches passed in the list match. + return false; +} + +inline void SwitchArg::commonProcessing() +{ + if ( _xorSet ) + throw(CmdLineParseException( + "Mutually exclusive argument already set!", toString())); + + if ( _alreadySet ) + throw(CmdLineParseException("Argument already set!", toString())); + + _alreadySet = true; + + if ( _value == true ) + _value = false; + else + _value = true; + + _checkWithVisitor(); +} + +inline bool SwitchArg::processArg(int *i, std::vector& args) +{ + if ( _ignoreable && Arg::ignoreRest() ) + return false; + + // if the whole string matches the flag or name string + if ( argMatches( args[*i] ) ) + { + commonProcessing(); + + return true; + } + // if a substring matches the flag as part of a combination + else if ( combinedSwitchesMatch( args[*i] ) ) + { + // check again to ensure we don't misinterpret + // this as a MultiSwitchArg + if ( combinedSwitchesMatch( args[*i] ) ) + throw(CmdLineParseException("Argument already set!", + toString())); + + commonProcessing(); + + // We only want to return true if we've found the last combined + // match in the string, otherwise we return true so that other + // switches in the combination will have a chance to match. + return lastCombined( args[*i] ); + } + else + return false; +} + +inline void SwitchArg::reset() +{ + Arg::reset(); + _value = _default; +} +////////////////////////////////////////////////////////////////////// +//End SwitchArg.cpp +////////////////////////////////////////////////////////////////////// + +} //namespace TCLAP + +#endif diff --git a/src/tclap/UnlabeledMultiArg.h b/src/tclap/UnlabeledMultiArg.h new file mode 100644 index 0000000..d38ce91 --- /dev/null +++ b/src/tclap/UnlabeledMultiArg.h @@ -0,0 +1,301 @@ + +/****************************************************************************** + * + * file: UnlabeledMultiArg.h + * + * Copyright (c) 2003, Michael E. Smoot. + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + + +#ifndef TCLAP_MULTIPLE_UNLABELED_ARGUMENT_H +#define TCLAP_MULTIPLE_UNLABELED_ARGUMENT_H + +#include +#include + +#include "MultiArg.h" +#include "OptionalUnlabeledTracker.h" + +namespace TCLAP { + +/** + * Just like a MultiArg, except that the arguments are unlabeled. Basically, + * this Arg will slurp up everything that hasn't been matched to another + * Arg. + */ +template +class UnlabeledMultiArg : public MultiArg +{ + + // If compiler has two stage name lookup (as gcc >= 3.4 does) + // this is requried to prevent undef. symbols + using MultiArg::_ignoreable; + using MultiArg::_hasBlanks; + using MultiArg::_extractValue; + using MultiArg::_typeDesc; + using MultiArg::_name; + using MultiArg::_description; + using MultiArg::_alreadySet; + using MultiArg::toString; + + public: + + /** + * Constructor. + * \param name - The name of the Arg. Note that this is used for + * identification, not as a long flag. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param typeDesc - A short, human readable description of the + * type that this object expects. This is used in the generation + * of the USAGE statement. The goal is to be helpful to the end user + * of the program. + * \param ignoreable - Whether or not this argument can be ignored + * using the "--" flag. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + UnlabeledMultiArg( const std::string& name, + const std::string& desc, + bool req, + const std::string& typeDesc, + bool ignoreable = false, + Visitor* v = NULL ); + /** + * Constructor. + * \param name - The name of the Arg. Note that this is used for + * identification, not as a long flag. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param typeDesc - A short, human readable description of the + * type that this object expects. This is used in the generation + * of the USAGE statement. The goal is to be helpful to the end user + * of the program. + * \param parser - A CmdLine parser object to add this Arg to + * \param ignoreable - Whether or not this argument can be ignored + * using the "--" flag. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + UnlabeledMultiArg( const std::string& name, + const std::string& desc, + bool req, + const std::string& typeDesc, + CmdLineInterface& parser, + bool ignoreable = false, + Visitor* v = NULL ); + + /** + * Constructor. + * \param name - The name of the Arg. Note that this is used for + * identification, not as a long flag. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param constraint - A pointer to a Constraint object used + * to constrain this Arg. + * \param ignoreable - Whether or not this argument can be ignored + * using the "--" flag. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + UnlabeledMultiArg( const std::string& name, + const std::string& desc, + bool req, + Constraint* constraint, + bool ignoreable = false, + Visitor* v = NULL ); + + /** + * Constructor. + * \param name - The name of the Arg. Note that this is used for + * identification, not as a long flag. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param constraint - A pointer to a Constraint object used + * to constrain this Arg. + * \param parser - A CmdLine parser object to add this Arg to + * \param ignoreable - Whether or not this argument can be ignored + * using the "--" flag. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + UnlabeledMultiArg( const std::string& name, + const std::string& desc, + bool req, + Constraint* constraint, + CmdLineInterface& parser, + bool ignoreable = false, + Visitor* v = NULL ); + + /** + * Handles the processing of the argument. + * This re-implements the Arg version of this method to set the + * _value of the argument appropriately. It knows the difference + * between labeled and unlabeled. + * \param i - Pointer the the current argument in the list. + * \param args - Mutable list of strings. Passed from main(). + */ + virtual bool processArg(int* i, std::vector& args); + + /** + * Returns the a short id string. Used in the usage. + * \param val - value to be used. + */ + virtual std::string shortID(const std::string& val="val") const; + + /** + * Returns the a long id string. Used in the usage. + * \param val - value to be used. + */ + virtual std::string longID(const std::string& val="val") const; + + /** + * Opertor ==. + * \param a - The Arg to be compared to this. + */ + virtual bool operator==(const Arg& a) const; + + /** + * Pushes this to back of list rather than front. + * \param argList - The list this should be added to. + */ + virtual void addToList( std::list& argList ) const; +}; + +template +UnlabeledMultiArg::UnlabeledMultiArg(const std::string& name, + const std::string& desc, + bool req, + const std::string& typeDesc, + bool ignoreable, + Visitor* v) +: MultiArg("", name, desc, req, typeDesc, v) +{ + _ignoreable = ignoreable; + OptionalUnlabeledTracker::check(true, toString()); +} + +template +UnlabeledMultiArg::UnlabeledMultiArg(const std::string& name, + const std::string& desc, + bool req, + const std::string& typeDesc, + CmdLineInterface& parser, + bool ignoreable, + Visitor* v) +: MultiArg("", name, desc, req, typeDesc, v) +{ + _ignoreable = ignoreable; + OptionalUnlabeledTracker::check(true, toString()); + parser.add( this ); +} + + +template +UnlabeledMultiArg::UnlabeledMultiArg(const std::string& name, + const std::string& desc, + bool req, + Constraint* constraint, + bool ignoreable, + Visitor* v) +: MultiArg("", name, desc, req, constraint, v) +{ + _ignoreable = ignoreable; + OptionalUnlabeledTracker::check(true, toString()); +} + +template +UnlabeledMultiArg::UnlabeledMultiArg(const std::string& name, + const std::string& desc, + bool req, + Constraint* constraint, + CmdLineInterface& parser, + bool ignoreable, + Visitor* v) +: MultiArg("", name, desc, req, constraint, v) +{ + _ignoreable = ignoreable; + OptionalUnlabeledTracker::check(true, toString()); + parser.add( this ); +} + + +template +bool UnlabeledMultiArg::processArg(int *i, std::vector& args) +{ + + if ( _hasBlanks( args[*i] ) ) + return false; + + // never ignore an unlabeled multi arg + + + // always take the first value, regardless of the start string + _extractValue( args[(*i)] ); + + /* + // continue taking args until we hit the end or a start string + while ( (unsigned int)(*i)+1 < args.size() && + args[(*i)+1].find_first_of( Arg::flagStartString() ) != 0 && + args[(*i)+1].find_first_of( Arg::nameStartString() ) != 0 ) + _extractValue( args[++(*i)] ); + */ + + _alreadySet = true; + + return true; +} + +template +std::string UnlabeledMultiArg::shortID(const std::string& val) const +{ + static_cast(val); // Ignore input, don't warn + return std::string("<") + _typeDesc + "> ..."; +} + +template +std::string UnlabeledMultiArg::longID(const std::string& val) const +{ + static_cast(val); // Ignore input, don't warn + return std::string("<") + _typeDesc + "> (accepted multiple times)"; +} + +template +bool UnlabeledMultiArg::operator==(const Arg& a) const +{ + if ( _name == a.getName() || _description == a.getDescription() ) + return true; + else + return false; +} + +template +void UnlabeledMultiArg::addToList( std::list& argList ) const +{ + argList.push_back( const_cast(static_cast(this)) ); +} + +} + +#endif diff --git a/src/tclap/UnlabeledValueArg.h b/src/tclap/UnlabeledValueArg.h new file mode 100644 index 0000000..8debba7 --- /dev/null +++ b/src/tclap/UnlabeledValueArg.h @@ -0,0 +1,340 @@ + +/****************************************************************************** + * + * file: UnlabeledValueArg.h + * + * Copyright (c) 2003, Michael E. Smoot . + * Copyright (c) 2004, Michael E. Smoot, Daniel Aarno. + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + + +#ifndef TCLAP_UNLABELED_VALUE_ARGUMENT_H +#define TCLAP_UNLABELED_VALUE_ARGUMENT_H + +#include +#include + +#include "ValueArg.h" +#include "OptionalUnlabeledTracker.h" + + +namespace TCLAP { + +/** + * The basic unlabeled argument that parses a value. + * This is a template class, which means the type T defines the type + * that a given object will attempt to parse when an UnlabeledValueArg + * is reached in the list of args that the CmdLine iterates over. + */ +template +class UnlabeledValueArg : public ValueArg +{ + + // If compiler has two stage name lookup (as gcc >= 3.4 does) + // this is requried to prevent undef. symbols + using ValueArg::_ignoreable; + using ValueArg::_hasBlanks; + using ValueArg::_extractValue; + using ValueArg::_typeDesc; + using ValueArg::_name; + using ValueArg::_description; + using ValueArg::_alreadySet; + using ValueArg::toString; + + public: + + /** + * UnlabeledValueArg constructor. + * \param name - A one word name for the argument. Note that this is used for + * identification, not as a long flag. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param value - The default value assigned to this argument if it + * is not present on the command line. + * \param typeDesc - A short, human readable description of the + * type that this object expects. This is used in the generation + * of the USAGE statement. The goal is to be helpful to the end user + * of the program. + * \param ignoreable - Allows you to specify that this argument can be + * ignored if the '--' flag is set. This defaults to false (cannot + * be ignored) and should generally stay that way unless you have + * some special need for certain arguments to be ignored. + * \param v - Optional Vistor. You should leave this blank unless + * you have a very good reason. + */ + UnlabeledValueArg( const std::string& name, + const std::string& desc, + bool req, + T value, + const std::string& typeDesc, + bool ignoreable = false, + Visitor* v = NULL); + + /** + * UnlabeledValueArg constructor. + * \param name - A one word name for the argument. Note that this is used for + * identification, not as a long flag. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param value - The default value assigned to this argument if it + * is not present on the command line. + * \param typeDesc - A short, human readable description of the + * type that this object expects. This is used in the generation + * of the USAGE statement. The goal is to be helpful to the end user + * of the program. + * \param parser - A CmdLine parser object to add this Arg to + * \param ignoreable - Allows you to specify that this argument can be + * ignored if the '--' flag is set. This defaults to false (cannot + * be ignored) and should generally stay that way unless you have + * some special need for certain arguments to be ignored. + * \param v - Optional Vistor. You should leave this blank unless + * you have a very good reason. + */ + UnlabeledValueArg( const std::string& name, + const std::string& desc, + bool req, + T value, + const std::string& typeDesc, + CmdLineInterface& parser, + bool ignoreable = false, + Visitor* v = NULL ); + + /** + * UnlabeledValueArg constructor. + * \param name - A one word name for the argument. Note that this is used for + * identification, not as a long flag. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param value - The default value assigned to this argument if it + * is not present on the command line. + * \param constraint - A pointer to a Constraint object used + * to constrain this Arg. + * \param ignoreable - Allows you to specify that this argument can be + * ignored if the '--' flag is set. This defaults to false (cannot + * be ignored) and should generally stay that way unless you have + * some special need for certain arguments to be ignored. + * \param v - Optional Vistor. You should leave this blank unless + * you have a very good reason. + */ + UnlabeledValueArg( const std::string& name, + const std::string& desc, + bool req, + T value, + Constraint* constraint, + bool ignoreable = false, + Visitor* v = NULL ); + + + /** + * UnlabeledValueArg constructor. + * \param name - A one word name for the argument. Note that this is used for + * identification, not as a long flag. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param value - The default value assigned to this argument if it + * is not present on the command line. + * \param constraint - A pointer to a Constraint object used + * to constrain this Arg. + * \param parser - A CmdLine parser object to add this Arg to + * \param ignoreable - Allows you to specify that this argument can be + * ignored if the '--' flag is set. This defaults to false (cannot + * be ignored) and should generally stay that way unless you have + * some special need for certain arguments to be ignored. + * \param v - Optional Vistor. You should leave this blank unless + * you have a very good reason. + */ + UnlabeledValueArg( const std::string& name, + const std::string& desc, + bool req, + T value, + Constraint* constraint, + CmdLineInterface& parser, + bool ignoreable = false, + Visitor* v = NULL); + + /** + * Handles the processing of the argument. + * This re-implements the Arg version of this method to set the + * _value of the argument appropriately. Handling specific to + * unlabled arguments. + * \param i - Pointer the the current argument in the list. + * \param args - Mutable list of strings. + */ + virtual bool processArg(int* i, std::vector& args); + + /** + * Overrides shortID for specific behavior. + */ + virtual std::string shortID(const std::string& val="val") const; + + /** + * Overrides longID for specific behavior. + */ + virtual std::string longID(const std::string& val="val") const; + + /** + * Overrides operator== for specific behavior. + */ + virtual bool operator==(const Arg& a ) const; + + /** + * Instead of pushing to the front of list, push to the back. + * \param argList - The list to add this to. + */ + virtual void addToList( std::list& argList ) const; + +}; + +/** + * Constructor implemenation. + */ +template +UnlabeledValueArg::UnlabeledValueArg(const std::string& name, + const std::string& desc, + bool req, + T val, + const std::string& typeDesc, + bool ignoreable, + Visitor* v) +: ValueArg("", name, desc, req, val, typeDesc, v) +{ + _ignoreable = ignoreable; + + OptionalUnlabeledTracker::check(req, toString()); + +} + +template +UnlabeledValueArg::UnlabeledValueArg(const std::string& name, + const std::string& desc, + bool req, + T val, + const std::string& typeDesc, + CmdLineInterface& parser, + bool ignoreable, + Visitor* v) +: ValueArg("", name, desc, req, val, typeDesc, v) +{ + _ignoreable = ignoreable; + OptionalUnlabeledTracker::check(req, toString()); + parser.add( this ); +} + +/** + * Constructor implemenation. + */ +template +UnlabeledValueArg::UnlabeledValueArg(const std::string& name, + const std::string& desc, + bool req, + T val, + Constraint* constraint, + bool ignoreable, + Visitor* v) +: ValueArg("", name, desc, req, val, constraint, v) +{ + _ignoreable = ignoreable; + OptionalUnlabeledTracker::check(req, toString()); +} + +template +UnlabeledValueArg::UnlabeledValueArg(const std::string& name, + const std::string& desc, + bool req, + T val, + Constraint* constraint, + CmdLineInterface& parser, + bool ignoreable, + Visitor* v) +: ValueArg("", name, desc, req, val, constraint, v) +{ + _ignoreable = ignoreable; + OptionalUnlabeledTracker::check(req, toString()); + parser.add( this ); +} + +/** + * Implementation of processArg(). + */ +template +bool UnlabeledValueArg::processArg(int *i, std::vector& args) +{ + + if ( _alreadySet ) + return false; + + if ( _hasBlanks( args[*i] ) ) + return false; + + // never ignore an unlabeled arg + + _extractValue( args[*i] ); + _alreadySet = true; + return true; +} + +/** + * Overriding shortID for specific output. + */ +template +std::string UnlabeledValueArg::shortID(const std::string& val) const +{ + static_cast(val); // Ignore input, don't warn + return std::string("<") + _typeDesc + ">"; +} + +/** + * Overriding longID for specific output. + */ +template +std::string UnlabeledValueArg::longID(const std::string& val) const +{ + static_cast(val); // Ignore input, don't warn + + // Ideally we would like to be able to use RTTI to return the name + // of the type required for this argument. However, g++ at least, + // doesn't appear to return terribly useful "names" of the types. + return std::string("<") + _typeDesc + ">"; +} + +/** + * Overriding operator== for specific behavior. + */ +template +bool UnlabeledValueArg::operator==(const Arg& a ) const +{ + if ( _name == a.getName() || _description == a.getDescription() ) + return true; + else + return false; +} + +template +void UnlabeledValueArg::addToList( std::list& argList ) const +{ + argList.push_back( const_cast(static_cast(this)) ); +} + +} +#endif diff --git a/src/tclap/ValueArg.h b/src/tclap/ValueArg.h new file mode 100644 index 0000000..95f9bb7 --- /dev/null +++ b/src/tclap/ValueArg.h @@ -0,0 +1,425 @@ +/****************************************************************************** + * + * file: ValueArg.h + * + * Copyright (c) 2003, Michael E. Smoot . + * Copyright (c) 2004, Michael E. Smoot, Daniel Aarno. + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + + +#ifndef TCLAP_VALUE_ARGUMENT_H +#define TCLAP_VALUE_ARGUMENT_H + +#include +#include + +#include "Arg.h" +#include "Constraint.h" + +namespace TCLAP { + +/** + * The basic labeled argument that parses a value. + * This is a template class, which means the type T defines the type + * that a given object will attempt to parse when the flag/name is matched + * on the command line. While there is nothing stopping you from creating + * an unflagged ValueArg, it is unwise and would cause significant problems. + * Instead use an UnlabeledValueArg. + */ +template +class ValueArg : public Arg +{ + protected: + + /** + * The value parsed from the command line. + * Can be of any type, as long as the >> operator for the type + * is defined. + */ + T _value; + + /** + * Used to support the reset() method so that ValueArg can be + * reset to their constructed value. + */ + T _default; + + /** + * A human readable description of the type to be parsed. + * This is a hack, plain and simple. Ideally we would use RTTI to + * return the name of type T, but until there is some sort of + * consistent support for human readable names, we are left to our + * own devices. + */ + std::string _typeDesc; + + /** + * A Constraint this Arg must conform to. + */ + Constraint* _constraint; + + /** + * Extracts the value from the string. + * Attempts to parse string as type T, if this fails an exception + * is thrown. + * \param val - value to be parsed. + */ + void _extractValue( const std::string& val ); + + public: + + /** + * Labeled ValueArg constructor. + * You could conceivably call this constructor with a blank flag, + * but that would make you a bad person. It would also cause + * an exception to be thrown. If you want an unlabeled argument, + * use the other constructor. + * \param flag - The one character flag that identifies this + * argument on the command line. + * \param name - A one word name for the argument. Can be + * used as a long flag on the command line. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param value - The default value assigned to this argument if it + * is not present on the command line. + * \param typeDesc - A short, human readable description of the + * type that this object expects. This is used in the generation + * of the USAGE statement. The goal is to be helpful to the end user + * of the program. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + ValueArg( const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + T value, + const std::string& typeDesc, + Visitor* v = NULL); + + + /** + * Labeled ValueArg constructor. + * You could conceivably call this constructor with a blank flag, + * but that would make you a bad person. It would also cause + * an exception to be thrown. If you want an unlabeled argument, + * use the other constructor. + * \param flag - The one character flag that identifies this + * argument on the command line. + * \param name - A one word name for the argument. Can be + * used as a long flag on the command line. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param value - The default value assigned to this argument if it + * is not present on the command line. + * \param typeDesc - A short, human readable description of the + * type that this object expects. This is used in the generation + * of the USAGE statement. The goal is to be helpful to the end user + * of the program. + * \param parser - A CmdLine parser object to add this Arg to + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + ValueArg( const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + T value, + const std::string& typeDesc, + CmdLineInterface& parser, + Visitor* v = NULL ); + + /** + * Labeled ValueArg constructor. + * You could conceivably call this constructor with a blank flag, + * but that would make you a bad person. It would also cause + * an exception to be thrown. If you want an unlabeled argument, + * use the other constructor. + * \param flag - The one character flag that identifies this + * argument on the command line. + * \param name - A one word name for the argument. Can be + * used as a long flag on the command line. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param value - The default value assigned to this argument if it + * is not present on the command line. + * \param constraint - A pointer to a Constraint object used + * to constrain this Arg. + * \param parser - A CmdLine parser object to add this Arg to. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + ValueArg( const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + T value, + Constraint* constraint, + CmdLineInterface& parser, + Visitor* v = NULL ); + + /** + * Labeled ValueArg constructor. + * You could conceivably call this constructor with a blank flag, + * but that would make you a bad person. It would also cause + * an exception to be thrown. If you want an unlabeled argument, + * use the other constructor. + * \param flag - The one character flag that identifies this + * argument on the command line. + * \param name - A one word name for the argument. Can be + * used as a long flag on the command line. + * \param desc - A description of what the argument is for or + * does. + * \param req - Whether the argument is required on the command + * line. + * \param value - The default value assigned to this argument if it + * is not present on the command line. + * \param constraint - A pointer to a Constraint object used + * to constrain this Arg. + * \param v - An optional visitor. You probably should not + * use this unless you have a very good reason. + */ + ValueArg( const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + T value, + Constraint* constraint, + Visitor* v = NULL ); + + /** + * Handles the processing of the argument. + * This re-implements the Arg version of this method to set the + * _value of the argument appropriately. It knows the difference + * between labeled and unlabeled. + * \param i - Pointer the the current argument in the list. + * \param args - Mutable list of strings. Passed + * in from main(). + */ + virtual bool processArg(int* i, std::vector& args); + + /** + * Returns the value of the argument. + */ + T& getValue() ; + + /** + * Specialization of shortID. + * \param val - value to be used. + */ + virtual std::string shortID(const std::string& val = "val") const; + + /** + * Specialization of longID. + * \param val - value to be used. + */ + virtual std::string longID(const std::string& val = "val") const; + + virtual void reset() ; + +private: + /** + * Prevent accidental copying + */ + ValueArg(const ValueArg& rhs); + ValueArg& operator=(const ValueArg& rhs); +}; + + +/** + * Constructor implementation. + */ +template +ValueArg::ValueArg(const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + T val, + const std::string& typeDesc, + Visitor* v) +: Arg(flag, name, desc, req, true, v), + _value( val ), + _default( val ), + _typeDesc( typeDesc ), + _constraint( NULL ) +{ } + +template +ValueArg::ValueArg(const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + T val, + const std::string& typeDesc, + CmdLineInterface& parser, + Visitor* v) +: Arg(flag, name, desc, req, true, v), + _value( val ), + _default( val ), + _typeDesc( typeDesc ), + _constraint( NULL ) +{ + parser.add( this ); +} + +template +ValueArg::ValueArg(const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + T val, + Constraint* constraint, + Visitor* v) +: Arg(flag, name, desc, req, true, v), + _value( val ), + _default( val ), + _typeDesc( constraint->shortID() ), + _constraint( constraint ) +{ } + +template +ValueArg::ValueArg(const std::string& flag, + const std::string& name, + const std::string& desc, + bool req, + T val, + Constraint* constraint, + CmdLineInterface& parser, + Visitor* v) +: Arg(flag, name, desc, req, true, v), + _value( val ), + _default( val ), + _typeDesc( constraint->shortID() ), + _constraint( constraint ) +{ + parser.add( this ); +} + + +/** + * Implementation of getValue(). + */ +template +T& ValueArg::getValue() { return _value; } + +/** + * Implementation of processArg(). + */ +template +bool ValueArg::processArg(int *i, std::vector& args) +{ + if ( _ignoreable && Arg::ignoreRest() ) + return false; + + if ( _hasBlanks( args[*i] ) ) + return false; + + std::string flag = args[*i]; + + std::string value = ""; + trimFlag( flag, value ); + + if ( argMatches( flag ) ) + { + if ( _alreadySet ) + { + if ( _xorSet ) + throw( CmdLineParseException( + "Mutually exclusive argument already set!", + toString()) ); + else + throw( CmdLineParseException("Argument already set!", + toString()) ); + } + + if ( Arg::delimiter() != ' ' && value == "" ) + throw( ArgParseException( + "Couldn't find delimiter for this argument!", + toString() ) ); + + if ( value == "" ) + { + (*i)++; + if ( static_cast(*i) < args.size() ) + _extractValue( args[*i] ); + else + throw( ArgParseException("Missing a value for this argument!", + toString() ) ); + } + else + _extractValue( value ); + + _alreadySet = true; + _checkWithVisitor(); + return true; + } + else + return false; +} + +/** + * Implementation of shortID. + */ +template +std::string ValueArg::shortID(const std::string& val) const +{ + static_cast(val); // Ignore input, don't warn + return Arg::shortID( _typeDesc ); +} + +/** + * Implementation of longID. + */ +template +std::string ValueArg::longID(const std::string& val) const +{ + static_cast(val); // Ignore input, don't warn + return Arg::longID( _typeDesc ); +} + +template +void ValueArg::_extractValue( const std::string& val ) +{ + try { + ExtractValue(_value, val, typename ArgTraits::ValueCategory()); + } catch( ArgParseException &e) { + throw ArgParseException(e.error(), toString()); + } + + if ( _constraint != NULL ) + if ( ! _constraint->check( _value ) ) + throw( CmdLineParseException( "Value '" + val + + + "' does not meet constraint: " + + _constraint->description(), + toString() ) ); +} + +template +void ValueArg::reset() +{ + Arg::reset(); + _value = _default; +} + +} // namespace TCLAP + +#endif diff --git a/src/tclap/ValuesConstraint.h b/src/tclap/ValuesConstraint.h new file mode 100644 index 0000000..27d3a6d --- /dev/null +++ b/src/tclap/ValuesConstraint.h @@ -0,0 +1,148 @@ + + +/****************************************************************************** + * + * file: ValuesConstraint.h + * + * Copyright (c) 2005, Michael E. Smoot + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +#ifndef TCLAP_VALUESCONSTRAINT_H +#define TCLAP_VALUESCONSTRAINT_H + +#include +#include +#include "Constraint.h" + +#ifdef HAVE_CONFIG_H +#include +#else +#define HAVE_SSTREAM +#endif + +#if defined(HAVE_SSTREAM) +#include +#elif defined(HAVE_STRSTREAM) +#include +#else +#error "Need a stringstream (sstream or strstream) to compile!" +#endif + +namespace TCLAP { + +/** + * A Constraint that constrains the Arg to only those values specified + * in the constraint. + */ +template +class ValuesConstraint : public Constraint +{ + + public: + + /** + * Constructor. + * \param allowed - vector of allowed values. + */ + ValuesConstraint(std::vector& allowed); + + /** + * Virtual destructor. + */ + virtual ~ValuesConstraint() {} + + /** + * Returns a description of the Constraint. + */ + virtual std::string description() const; + + /** + * Returns the short ID for the Constraint. + */ + virtual std::string shortID() const; + + /** + * The method used to verify that the value parsed from the command + * line meets the constraint. + * \param value - The value that will be checked. + */ + virtual bool check(const T& value) const; + + protected: + + /** + * The list of valid values. + */ + std::vector _allowed; + + /** + * The string used to describe the allowed values of this constraint. + */ + std::string _typeDesc; + +}; + +template +ValuesConstraint::ValuesConstraint(std::vector& allowed) +: _allowed(allowed), + _typeDesc("") +{ + for ( unsigned int i = 0; i < _allowed.size(); i++ ) + { + +#if defined(HAVE_SSTREAM) + std::ostringstream os; +#elif defined(HAVE_STRSTREAM) + std::ostrstream os; +#else +#error "Need a stringstream (sstream or strstream) to compile!" +#endif + + os << _allowed[i]; + + std::string temp( os.str() ); + + if ( i > 0 ) + _typeDesc += "|"; + _typeDesc += temp; + } +} + +template +bool ValuesConstraint::check( const T& val ) const +{ + if ( std::find(_allowed.begin(),_allowed.end(),val) == _allowed.end() ) + return false; + else + return true; +} + +template +std::string ValuesConstraint::shortID() const +{ + return _typeDesc; +} + +template +std::string ValuesConstraint::description() const +{ + return _typeDesc; +} + + +} //namespace TCLAP +#endif + diff --git a/src/tclap/VersionVisitor.h b/src/tclap/VersionVisitor.h new file mode 100644 index 0000000..d7e67d8 --- /dev/null +++ b/src/tclap/VersionVisitor.h @@ -0,0 +1,81 @@ +// -*- Mode: c++; c-basic-offset: 4; tab-width: 4; -*- + +/****************************************************************************** + * + * file: VersionVisitor.h + * + * Copyright (c) 2003, Michael E. Smoot . + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + + +#ifndef TCLAP_VERSION_VISITOR_H +#define TCLAP_VERSION_VISITOR_H + +#include "CmdLineInterface.h" +#include "CmdLineOutput.h" +#include "Visitor.h" + +namespace TCLAP { + +/** + * A Vistor that will call the version method of the given CmdLineOutput + * for the specified CmdLine object and then exit. + */ +class VersionVisitor: public Visitor +{ + private: + /** + * Prevent accidental copying + */ + VersionVisitor(const VersionVisitor& rhs); + VersionVisitor& operator=(const VersionVisitor& rhs); + + protected: + + /** + * The CmdLine of interest. + */ + CmdLineInterface* _cmd; + + /** + * The output object. + */ + CmdLineOutput** _out; + + public: + + /** + * Constructor. + * \param cmd - The CmdLine the output is generated for. + * \param out - The type of output. + */ + VersionVisitor( CmdLineInterface* cmd, CmdLineOutput** out ) + : Visitor(), _cmd( cmd ), _out( out ) { } + + /** + * Calls the version method of the output object using the + * specified CmdLine. + */ + void visit() { + (*_out)->version(*_cmd); + throw ExitException(0); + } + +}; + +} + +#endif diff --git a/src/tclap/Visitor.h b/src/tclap/Visitor.h new file mode 100644 index 0000000..38ddcbd --- /dev/null +++ b/src/tclap/Visitor.h @@ -0,0 +1,53 @@ + +/****************************************************************************** + * + * file: Visitor.h + * + * Copyright (c) 2003, Michael E. Smoot . + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + + +#ifndef TCLAP_VISITOR_H +#define TCLAP_VISITOR_H + +namespace TCLAP { + +/** + * A base class that defines the interface for visitors. + */ +class Visitor +{ + public: + + /** + * Constructor. Does nothing. + */ + Visitor() { } + + /** + * Destructor. Does nothing. + */ + virtual ~Visitor() { } + + /** + * Does nothing. Should be overridden by child. + */ + virtual void visit() { } +}; + +} + +#endif diff --git a/src/tclap/XorHandler.h b/src/tclap/XorHandler.h new file mode 100644 index 0000000..13551fb --- /dev/null +++ b/src/tclap/XorHandler.h @@ -0,0 +1,166 @@ + +/****************************************************************************** + * + * file: XorHandler.h + * + * Copyright (c) 2003, Michael E. Smoot . + * Copyright (c) 2004, Michael E. Smoot, Daniel Aarno. + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +#ifndef TCLAP_XORHANDLER_H +#define TCLAP_XORHANDLER_H + +#include "Arg.h" +#include +#include +#include +#include + +namespace TCLAP { + +/** + * This class handles lists of Arg's that are to be XOR'd on the command + * line. This is used by CmdLine and you shouldn't ever use it. + */ +class XorHandler +{ + protected: + + /** + * The list of of lists of Arg's to be or'd together. + */ + std::vector< std::vector > _orList; + + public: + + /** + * Constructor. Does nothing. + */ + XorHandler( ) : _orList(std::vector< std::vector >()) {} + + /** + * Add a list of Arg*'s that will be orred together. + * \param ors - list of Arg* that will be xor'd. + */ + void add( std::vector& ors ); + + /** + * Checks whether the specified Arg is in one of the xor lists and + * if it does match one, returns the size of the xor list that the + * Arg matched. If the Arg matches, then it also sets the rest of + * the Arg's in the list. You shouldn't use this. + * \param a - The Arg to be checked. + */ + int check( const Arg* a ); + + /** + * Returns the XOR specific short usage. + */ + std::string shortUsage(); + + /** + * Prints the XOR specific long usage. + * \param os - Stream to print to. + */ + void printLongUsage(std::ostream& os); + + /** + * Simply checks whether the Arg is contained in one of the arg + * lists. + * \param a - The Arg to be checked. + */ + bool contains( const Arg* a ); + + std::vector< std::vector >& getXorList(); + +}; + + +////////////////////////////////////////////////////////////////////// +//BEGIN XOR.cpp +////////////////////////////////////////////////////////////////////// +inline void XorHandler::add( std::vector& ors ) +{ + _orList.push_back( ors ); +} + +inline int XorHandler::check( const Arg* a ) +{ + // iterate over each XOR list + for ( int i = 0; static_cast(i) < _orList.size(); i++ ) + { + // if the XOR list contains the arg.. + ArgVectorIterator ait = std::find( _orList[i].begin(), + _orList[i].end(), a ); + if ( ait != _orList[i].end() ) + { + // first check to see if a mutually exclusive switch + // has not already been set + for ( ArgVectorIterator it = _orList[i].begin(); + it != _orList[i].end(); + it++ ) + if ( a != (*it) && (*it)->isSet() ) + throw(CmdLineParseException( + "Mutually exclusive argument already set!", + (*it)->toString())); + + // go through and set each arg that is not a + for ( ArgVectorIterator it = _orList[i].begin(); + it != _orList[i].end(); + it++ ) + if ( a != (*it) ) + (*it)->xorSet(); + + // return the number of required args that have now been set + if ( (*ait)->allowMore() ) + return 0; + else + return static_cast(_orList[i].size()); + } + } + + if ( a->isRequired() ) + return 1; + else + return 0; +} + +inline bool XorHandler::contains( const Arg* a ) +{ + for ( int i = 0; static_cast(i) < _orList.size(); i++ ) + for ( ArgVectorIterator it = _orList[i].begin(); + it != _orList[i].end(); + it++ ) + if ( a == (*it) ) + return true; + + return false; +} + +inline std::vector< std::vector >& XorHandler::getXorList() +{ + return _orList; +} + + + +////////////////////////////////////////////////////////////////////// +//END XOR.cpp +////////////////////////////////////////////////////////////////////// + +} //namespace TCLAP + +#endif diff --git a/src/tclap/ZshCompletionOutput.h b/src/tclap/ZshCompletionOutput.h new file mode 100644 index 0000000..0b37fc7 --- /dev/null +++ b/src/tclap/ZshCompletionOutput.h @@ -0,0 +1,323 @@ +// -*- Mode: c++; c-basic-offset: 4; tab-width: 4; -*- + +/****************************************************************************** + * + * file: ZshCompletionOutput.h + * + * Copyright (c) 2006, Oliver Kiddle + * All rights reverved. + * + * See the file COPYING in the top directory of this distribution for + * more information. + * + * THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + *****************************************************************************/ + +#ifndef TCLAP_ZSHCOMPLETIONOUTPUT_H +#define TCLAP_ZSHCOMPLETIONOUTPUT_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace TCLAP { + +/** + * A class that generates a Zsh completion function as output from the usage() + * method for the given CmdLine and its Args. + */ +class ZshCompletionOutput : public CmdLineOutput +{ + + public: + + ZshCompletionOutput(); + + /** + * Prints the usage to stdout. Can be overridden to + * produce alternative behavior. + * \param c - The CmdLine object the output is generated for. + */ + virtual void usage(CmdLineInterface& c); + + /** + * Prints the version to stdout. Can be overridden + * to produce alternative behavior. + * \param c - The CmdLine object the output is generated for. + */ + virtual void version(CmdLineInterface& c); + + /** + * Prints (to stderr) an error message, short usage + * Can be overridden to produce alternative behavior. + * \param c - The CmdLine object the output is generated for. + * \param e - The ArgException that caused the failure. + */ + virtual void failure(CmdLineInterface& c, + ArgException& e ); + + protected: + + void basename( std::string& s ); + void quoteSpecialChars( std::string& s ); + + std::string getMutexList( CmdLineInterface& _cmd, Arg* a ); + void printOption( Arg* it, std::string mutex ); + void printArg( Arg* it ); + + std::map common; + char theDelimiter; +}; + +ZshCompletionOutput::ZshCompletionOutput() +: common(std::map()), + theDelimiter('=') +{ + common["host"] = "_hosts"; + common["hostname"] = "_hosts"; + common["file"] = "_files"; + common["filename"] = "_files"; + common["user"] = "_users"; + common["username"] = "_users"; + common["directory"] = "_directories"; + common["path"] = "_directories"; + common["url"] = "_urls"; +} + +inline void ZshCompletionOutput::version(CmdLineInterface& _cmd) +{ + std::cout << _cmd.getVersion() << std::endl; +} + +inline void ZshCompletionOutput::usage(CmdLineInterface& _cmd ) +{ + std::list argList = _cmd.getArgList(); + std::string progName = _cmd.getProgramName(); + std::string xversion = _cmd.getVersion(); + theDelimiter = _cmd.getDelimiter(); + basename(progName); + + std::cout << "#compdef " << progName << std::endl << std::endl << + "# " << progName << " version " << _cmd.getVersion() << std::endl << std::endl << + "_arguments -s -S"; + + for (ArgListIterator it = argList.begin(); it != argList.end(); it++) + { + if ( (*it)->shortID().at(0) == '<' ) + printArg((*it)); + else if ( (*it)->getFlag() != "-" ) + printOption((*it), getMutexList(_cmd, *it)); + } + + std::cout << std::endl; +} + +inline void ZshCompletionOutput::failure( CmdLineInterface& _cmd, + ArgException& e ) +{ + static_cast(_cmd); // unused + std::cout << e.what() << std::endl; +} + +inline void ZshCompletionOutput::quoteSpecialChars( std::string& s ) +{ + size_t idx = s.find_last_of(':'); + while ( idx != std::string::npos ) + { + s.insert(idx, 1, '\\'); + idx = s.find_last_of(':', idx); + } + idx = s.find_last_of('\''); + while ( idx != std::string::npos ) + { + s.insert(idx, "'\\'"); + if (idx == 0) + idx = std::string::npos; + else + idx = s.find_last_of('\'', --idx); + } +} + +inline void ZshCompletionOutput::basename( std::string& s ) +{ + size_t p = s.find_last_of('/'); + if ( p != std::string::npos ) + { + s.erase(0, p + 1); + } +} + +inline void ZshCompletionOutput::printArg(Arg* a) +{ + static int count = 1; + + std::cout << " \\" << std::endl << " '"; + if ( a->acceptsMultipleValues() ) + std::cout << '*'; + else + std::cout << count++; + std::cout << ':'; + if ( !a->isRequired() ) + std::cout << ':'; + + std::cout << a->getName() << ':'; + std::map::iterator compArg = common.find(a->getName()); + if ( compArg != common.end() ) + { + std::cout << compArg->second; + } + else + { + std::cout << "_guard \"^-*\" " << a->getName(); + } + std::cout << '\''; +} + +inline void ZshCompletionOutput::printOption(Arg* a, std::string mutex) +{ + std::string flag = a->flagStartChar() + a->getFlag(); + std::string name = a->nameStartString() + a->getName(); + std::string desc = a->getDescription(); + + // remove full stop and capitalisation from description as + // this is the convention for zsh function + if (!desc.compare(0, 12, "(required) ")) + { + desc.erase(0, 12); + } + if (!desc.compare(0, 15, "(OR required) ")) + { + desc.erase(0, 15); + } + size_t len = desc.length(); + if (len && desc.at(--len) == '.') + { + desc.erase(len); + } + if (len) + { + desc.replace(0, 1, 1, tolower(desc.at(0))); + } + + std::cout << " \\" << std::endl << " '" << mutex; + + if ( a->getFlag().empty() ) + { + std::cout << name; + } + else + { + std::cout << "'{" << flag << ',' << name << "}'"; + } + if ( theDelimiter == '=' && a->isValueRequired() ) + std::cout << "=-"; + quoteSpecialChars(desc); + std::cout << '[' << desc << ']'; + + if ( a->isValueRequired() ) + { + std::string arg = a->shortID(); + arg.erase(0, arg.find_last_of(theDelimiter) + 1); + if ( arg.at(arg.length()-1) == ']' ) + arg.erase(arg.length()-1); + if ( arg.at(arg.length()-1) == ']' ) + { + arg.erase(arg.length()-1); + } + if ( arg.at(0) == '<' ) + { + arg.erase(arg.length()-1); + arg.erase(0, 1); + } + size_t p = arg.find('|'); + if ( p != std::string::npos ) + { + do + { + arg.replace(p, 1, 1, ' '); + } + while ( (p = arg.find_first_of('|', p)) != std::string::npos ); + quoteSpecialChars(arg); + std::cout << ": :(" << arg << ')'; + } + else + { + std::cout << ':' << arg; + std::map::iterator compArg = common.find(arg); + if ( compArg != common.end() ) + { + std::cout << ':' << compArg->second; + } + } + } + + std::cout << '\''; +} + +inline std::string ZshCompletionOutput::getMutexList( CmdLineInterface& _cmd, Arg* a) +{ + XorHandler xorHandler = _cmd.getXorHandler(); + std::vector< std::vector > xorList = xorHandler.getXorList(); + + if (a->getName() == "help" || a->getName() == "version") + { + return "(-)"; + } + + std::ostringstream list; + if ( a->acceptsMultipleValues() ) + { + list << '*'; + } + + for ( int i = 0; static_cast(i) < xorList.size(); i++ ) + { + for ( ArgVectorIterator it = xorList[i].begin(); + it != xorList[i].end(); + it++) + if ( a == (*it) ) + { + list << '('; + for ( ArgVectorIterator iu = xorList[i].begin(); + iu != xorList[i].end(); + iu++ ) + { + bool notCur = (*iu) != a; + bool hasFlag = !(*iu)->getFlag().empty(); + if ( iu != xorList[i].begin() && (notCur || hasFlag) ) + list << ' '; + if (hasFlag) + list << (*iu)->flagStartChar() << (*iu)->getFlag() << ' '; + if ( notCur || hasFlag ) + list << (*iu)->nameStartString() << (*iu)->getName(); + } + list << ')'; + return list.str(); + } + } + + // wasn't found in xor list + if (!a->getFlag().empty()) { + list << "(" << a->flagStartChar() << a->getFlag() << ' ' << + a->nameStartString() << a->getName() << ')'; + } + + return list.str(); +} + +} //namespace TCLAP +#endif From 9aea5c3f0025c60a1d7ab484672247831915480d Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 2 Jan 2014 16:01:35 -0600 Subject: [PATCH 02/14] Added openAlpr support library --- src/openalpr/support/CMakeLists.txt | 9 + src/openalpr/support/filesystem.cpp | 76 ++ src/openalpr/support/filesystem.h | 30 + src/openalpr/support/timing.cpp | 59 ++ src/openalpr/support/timing.h | 16 + src/openalpr/support/tinydir.h | 423 +++++++++ src/openalpr/support/windows/dirent.h | 889 ++++++++++++++++++ src/openalpr/support/windows/unistd_partial.h | 42 + src/openalpr/support/windows/utils.h | 7 + 9 files changed, 1551 insertions(+) create mode 100644 src/openalpr/support/CMakeLists.txt create mode 100644 src/openalpr/support/filesystem.cpp create mode 100644 src/openalpr/support/filesystem.h create mode 100644 src/openalpr/support/timing.cpp create mode 100644 src/openalpr/support/timing.h create mode 100644 src/openalpr/support/tinydir.h create mode 100644 src/openalpr/support/windows/dirent.h create mode 100644 src/openalpr/support/windows/unistd_partial.h create mode 100644 src/openalpr/support/windows/utils.h diff --git a/src/openalpr/support/CMakeLists.txt b/src/openalpr/support/CMakeLists.txt new file mode 100644 index 0000000..10898a3 --- /dev/null +++ b/src/openalpr/support/CMakeLists.txt @@ -0,0 +1,9 @@ + + +set(support_source_files + filesystem.cpp + timing.cpp +) + + +add_library(support ${support_source_files}) \ No newline at end of file diff --git a/src/openalpr/support/filesystem.cpp b/src/openalpr/support/filesystem.cpp new file mode 100644 index 0000000..32fe1cd --- /dev/null +++ b/src/openalpr/support/filesystem.cpp @@ -0,0 +1,76 @@ +#include "filesystem.h" + + + +bool hasEnding (std::string const &fullString, std::string const &ending) +{ + if (fullString.length() >= ending.length()) { + return (0 == fullString.compare (fullString.length() - ending.length(), ending.length(), ending)); + } else { + return false; + } +} + +bool DirectoryExists( const char* pzPath ) +{ + if ( pzPath == NULL) return false; + + DIR *pDir; + bool bExists = false; + + pDir = opendir (pzPath); + + if (pDir != NULL) + { + bExists = true; + (void) closedir (pDir); + } + + return bExists; +} + +bool fileExists( const char* pzPath ) +{ + if (pzPath == NULL) return false; + + bool fExists = false; + std::ifstream f(pzPath); + fExists = f.is_open(); + f.close(); + return fExists; +} + +std::vector getFilesInDir(const char* dirPath) +{ + DIR *dir; + + std::vector files; + + struct dirent *ent; + if ((dir = opendir (dirPath)) != NULL) { + /* print all the files and directories within directory */ + while ((ent = readdir (dir)) != NULL) { + if (strcmp(ent->d_name, ".") != 0 && strcmp(ent->d_name, "..") != 0) + files.push_back(ent->d_name); + } + closedir (dir); + } else { + /* could not open directory */ + perror (""); + return files; + } + + return files; +} + + +bool stringCompare( const std::string &left, const std::string &right ){ + for( std::string::const_iterator lit = left.begin(), rit = right.begin(); lit != left.end() && rit != right.end(); ++lit, ++rit ) + if( tolower( *lit ) < tolower( *rit ) ) + return true; + else if( tolower( *lit ) > tolower( *rit ) ) + return false; + if( left.size() < right.size() ) + return true; + return false; +} \ No newline at end of file diff --git a/src/openalpr/support/filesystem.h b/src/openalpr/support/filesystem.h new file mode 100644 index 0000000..cb83b30 --- /dev/null +++ b/src/openalpr/support/filesystem.h @@ -0,0 +1,30 @@ + +#ifndef FILESYSTEM_H +#define FILESYSTEM_H + + +#ifdef WINDOWS + #include "windows/dirent.h" + #include "windows/utils.h" + #include "windows/unistd_partial.h" +#else + #include + #include +#endif + +#include +#include +#include +#include +#include + + + bool hasEnding (std::string const &fullString, std::string const &ending); + bool DirectoryExists( const char* pzPath ); + bool fileExists( const char* pzPath ); + std::vector getFilesInDir(const char* dirPath); + + bool stringCompare( const std::string &left, const std::string &right ); + + +#endif // FILESYSTEM_H \ No newline at end of file diff --git a/src/openalpr/support/timing.cpp b/src/openalpr/support/timing.cpp new file mode 100644 index 0000000..438660c --- /dev/null +++ b/src/openalpr/support/timing.cpp @@ -0,0 +1,59 @@ +#include "timing.h" + +#ifdef WINDOWS + + +timespec diff(timespec start, timespec end); + +void getTime(timespec* time) +{ + // Do nothing on Windows +} +double diffclock(timespec time1,timespec time2) +{ + // Mock this out for Windows + return 0; +} + +timespec diff(timespec start, timespec end) +{ + // Mock this out for Windows + return 0; +} + +#else + + +timespec diff(timespec start, timespec end); + +void getTime(timespec* time) +{ + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, time); +} +double diffclock(timespec time1,timespec time2) +{ + + timespec delta = diff(time1,time2); + double milliseconds = (delta.tv_sec * 1000) + (((double) delta.tv_nsec) / 1000000.0); + + + return milliseconds; + + +} + +timespec diff(timespec start, timespec end) +{ + timespec temp; + if ((end.tv_nsec-start.tv_nsec)<0) { + temp.tv_sec = end.tv_sec-start.tv_sec-1; + temp.tv_nsec = 1000000000+end.tv_nsec-start.tv_nsec; + } else { + temp.tv_sec = end.tv_sec-start.tv_sec; + temp.tv_nsec = end.tv_nsec-start.tv_nsec; + } + return temp; +} + + +#endif diff --git a/src/openalpr/support/timing.h b/src/openalpr/support/timing.h new file mode 100644 index 0000000..e7cabb3 --- /dev/null +++ b/src/openalpr/support/timing.h @@ -0,0 +1,16 @@ +#ifndef TIMING_H +#define TIMING_H + +#include + + +#ifdef WINDOWS + // Mock this out for Windows + #define timespec int +#endif + + void getTime(timespec* time); + double diffclock(timespec time1,timespec time2); + + +#endif \ No newline at end of file diff --git a/src/openalpr/support/tinydir.h b/src/openalpr/support/tinydir.h new file mode 100644 index 0000000..694f5cd --- /dev/null +++ b/src/openalpr/support/tinydir.h @@ -0,0 +1,423 @@ +/* +Copyright (c) 2013, Cong Xu +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#ifndef TINYDIR_H +#define TINYDIR_H + +#include +#include +#include +#ifdef _MSC_VER +#define WIN32_LEAN_AND_MEAN +#include +#pragma warning (disable : 4996) +#else +#include +#include +#endif + + +/* types */ + +#define _TINYDIR_PATH_MAX 4096 +#ifdef _MSC_VER +/* extra chars for the "\\*" mask */ +#define _TINYDIR_PATH_EXTRA 2 +#else +#define _TINYDIR_PATH_EXTRA 0 +#endif +#define _TINYDIR_FILENAME_MAX 256 + +#ifdef _MSC_VER +#define strncasecmp _strnicmp +#endif + +#ifdef _MSC_VER +#define _TINYDIR_FUNC static __inline +#else +#define _TINYDIR_FUNC static __inline__ +#endif + +typedef struct +{ + char path[_TINYDIR_PATH_MAX]; + char name[_TINYDIR_FILENAME_MAX]; + int is_dir; + int is_reg; + +#ifdef _MSC_VER +#else + struct stat _s; +#endif +} tinydir_file; + +typedef struct +{ + char path[_TINYDIR_PATH_MAX]; + int has_next; + int n_files; + + tinydir_file *_files; +#ifdef _MSC_VER + HANDLE _h; + WIN32_FIND_DATA _f; +#else + DIR *_d; + struct dirent *_e; +#endif +} tinydir_dir; + + +/* declarations */ + +_TINYDIR_FUNC +int tinydir_open(tinydir_dir *dir, const char *path); +_TINYDIR_FUNC +int tinydir_open_sorted(tinydir_dir *dir, const char *path); +_TINYDIR_FUNC +void tinydir_close(tinydir_dir *dir); + +_TINYDIR_FUNC +int tinydir_next(tinydir_dir *dir); +_TINYDIR_FUNC +int tinydir_readfile(const tinydir_dir *dir, tinydir_file *file); +_TINYDIR_FUNC +int tinydir_readfile_n(const tinydir_dir *dir, tinydir_file *file, int i); +_TINYDIR_FUNC +int tinydir_open_subdir_n(tinydir_dir *dir, int i); + +_TINYDIR_FUNC +int _tinydir_file_cmp(const void *a, const void *b); + + +/* definitions*/ + +_TINYDIR_FUNC +int tinydir_open(tinydir_dir *dir, const char *path) +{ + if (dir == NULL || path == NULL || strlen(path) == 0) + { + errno = EINVAL; + return -1; + } + if (strlen(path) + _TINYDIR_PATH_EXTRA >= _TINYDIR_PATH_MAX) + { + errno = ENAMETOOLONG; + return -1; + } + + /* initialise dir */ + dir->_files = NULL; +#ifdef _MSC_VER + dir->_h = INVALID_HANDLE_VALUE; +#else + dir->_d = NULL; +#endif + tinydir_close(dir); + + strcpy(dir->path, path); +#ifdef _MSC_VER + strcat(dir->path, "\\*"); + dir->_h = FindFirstFile(dir->path, &dir->_f); + dir->path[strlen(dir->path) - 2] = '\0'; + if (dir->_h == INVALID_HANDLE_VALUE) +#else + dir->_d = opendir(path); + if (dir->_d == NULL) +#endif + { + errno = ENOENT; + goto bail; + } + + /* read first file */ + dir->has_next = 1; +#ifndef _MSC_VER + dir->_e = readdir(dir->_d); + if (dir->_e == NULL) + { + dir->has_next = 0; + } +#endif + + return 0; + +bail: + tinydir_close(dir); + return -1; +} + +_TINYDIR_FUNC +int tinydir_open_sorted(tinydir_dir *dir, const char *path) +{ + if (tinydir_open(dir, path) == -1) + { + return -1; + } + + dir->n_files = 0; + while (dir->has_next) + { + tinydir_file *p_file; + dir->n_files++; + dir->_files = (tinydir_file *)realloc(dir->_files, sizeof(tinydir_file)*dir->n_files); + if (dir->_files == NULL) + { + errno = ENOMEM; + goto bail; + } + + p_file = &dir->_files[dir->n_files - 1]; + if (tinydir_readfile(dir, p_file) == -1) + { + goto bail; + } + + if (tinydir_next(dir) == -1) + { + goto bail; + } + } + + qsort(dir->_files, dir->n_files, sizeof(tinydir_file), _tinydir_file_cmp); + + return 0; + +bail: + tinydir_close(dir); + return -1; +} + +_TINYDIR_FUNC +void tinydir_close(tinydir_dir *dir) +{ + if (dir == NULL) + { + return; + } + + memset(dir->path, 0, sizeof(dir->path)); + dir->has_next = 0; + dir->n_files = -1; + if (dir->_files != NULL) + { + free(dir->_files); + } + dir->_files = NULL; +#ifdef _MSC_VER + if (dir->_h != INVALID_HANDLE_VALUE) + { + FindClose(dir->_h); + } + dir->_h = INVALID_HANDLE_VALUE; +#else + if (dir->_d) + { + closedir(dir->_d); + } + dir->_d = NULL; + dir->_e = NULL; +#endif +} + +_TINYDIR_FUNC +int tinydir_next(tinydir_dir *dir) +{ + if (dir == NULL) + { + errno = EINVAL; + return -1; + } + if (!dir->has_next) + { + errno = ENOENT; + return -1; + } + +#ifdef _MSC_VER + if (FindNextFile(dir->_h, &dir->_f) == 0) +#else + dir->_e = readdir(dir->_d); + if (dir->_e == NULL) +#endif + { + dir->has_next = 0; +#ifdef _MSC_VER + if (GetLastError() != ERROR_SUCCESS && + GetLastError() != ERROR_NO_MORE_FILES) + { + tinydir_close(dir); + errno = EIO; + return -1; + } +#endif + } + + return 0; +} + +_TINYDIR_FUNC +int tinydir_readfile(const tinydir_dir *dir, tinydir_file *file) +{ + if (dir == NULL || file == NULL) + { + errno = EINVAL; + return -1; + } +#ifdef _MSC_VER + if (dir->_h == INVALID_HANDLE_VALUE) +#else + if (dir->_e == NULL) +#endif + { + errno = ENOENT; + return -1; + } + if (strlen(dir->path) + + strlen( +#ifdef _MSC_VER + dir->_f.cFileName +#else + dir->_e->d_name +#endif + ) + 1 + _TINYDIR_PATH_EXTRA >= + _TINYDIR_PATH_MAX) + { + /* the path for the file will be too long */ + errno = ENAMETOOLONG; + return -1; + } + if (strlen( +#ifdef _MSC_VER + dir->_f.cFileName +#else + dir->_e->d_name +#endif + ) >= _TINYDIR_FILENAME_MAX) + { + errno = ENAMETOOLONG; + return -1; + } + + strcpy(file->path, dir->path); + strcat(file->path, "/"); + strcpy(file->name, +#ifdef _MSC_VER + dir->_f.cFileName +#else + dir->_e->d_name +#endif + ); + strcat(file->path, file->name); +#ifndef _MSC_VER + if (stat(file->path, &file->_s) == -1) + { + return -1; + } +#endif + file->is_dir = +#ifdef _MSC_VER + !!(dir->_f.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY); +#else + S_ISDIR(file->_s.st_mode); +#endif + file->is_reg = +#ifdef _MSC_VER + !!(dir->_f.dwFileAttributes & FILE_ATTRIBUTE_NORMAL) || + ( + !(dir->_f.dwFileAttributes & FILE_ATTRIBUTE_DEVICE) && + !(dir->_f.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && + !(dir->_f.dwFileAttributes & FILE_ATTRIBUTE_ENCRYPTED) && +#ifdef FILE_ATTRIBUTE_INTEGRITY_STREAM + !(dir->_f.dwFileAttributes & FILE_ATTRIBUTE_INTEGRITY_STREAM) && +#endif +#ifdef FILE_ATTRIBUTE_NO_SCRUB_DATA + !(dir->_f.dwFileAttributes & FILE_ATTRIBUTE_NO_SCRUB_DATA) && +#endif + !(dir->_f.dwFileAttributes & FILE_ATTRIBUTE_OFFLINE) && + !(dir->_f.dwFileAttributes & FILE_ATTRIBUTE_TEMPORARY)); +#else + S_ISREG(file->_s.st_mode); +#endif + + return 0; +} + +_TINYDIR_FUNC +int tinydir_readfile_n(const tinydir_dir *dir, tinydir_file *file, int i) +{ + if (dir == NULL || file == NULL || i < 0) + { + errno = EINVAL; + return -1; + } + if (i >= dir->n_files) + { + errno = ENOENT; + return -1; + } + + memcpy(file, &dir->_files[i], sizeof(tinydir_file)); + + return 0; +} + +_TINYDIR_FUNC +int tinydir_open_subdir_n(tinydir_dir *dir, int i) +{ + char path[_TINYDIR_PATH_MAX]; + if (dir == NULL || i < 0) + { + errno = EINVAL; + return -1; + } + if (i >= dir->n_files || !dir->_files[i].is_dir) + { + errno = ENOENT; + return -1; + } + + strcpy(path, dir->_files[i].path); + tinydir_close(dir); + if (tinydir_open_sorted(dir, path) == -1) + { + return -1; + } + + return 0; +} + +_TINYDIR_FUNC +int _tinydir_file_cmp(const void *a, const void *b) +{ + const tinydir_file *fa = (const tinydir_file *)a; + const tinydir_file *fb = (const tinydir_file *)b; + if (fa->is_dir != fb->is_dir) + { + return -(fa->is_dir - fb->is_dir); + } + return strncasecmp(fa->name, fb->name, _TINYDIR_FILENAME_MAX); +} + +#endif \ No newline at end of file diff --git a/src/openalpr/support/windows/dirent.h b/src/openalpr/support/windows/dirent.h new file mode 100644 index 0000000..cf3fe56 --- /dev/null +++ b/src/openalpr/support/windows/dirent.h @@ -0,0 +1,889 @@ +/* + * dirent.h - dirent API for Microsoft Visual Studio + * + * Copyright (C) 2006-2012 Toni Ronkko + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * ``Software''), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL TONI RONKKO BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * + * Version 1.13, Dec 12 2012, Toni Ronkko + * Use traditional 8+3 file name if the name cannot be represented in the + * default ANSI code page. Now compiles again with MSVC 6.0. Thanks to + * Konstantin Khomoutov for testing. + * + * Version 1.12.1, Oct 1 2012, Toni Ronkko + * Bug fix: renamed wide-character DIR structure _wDIR to _WDIR (with + * capital W) in order to maintain compatibility with MingW. + * + * Version 1.12, Sep 30 2012, Toni Ronkko + * Define PATH_MAX and NAME_MAX. Added wide-character variants _wDIR, + * _wdirent, _wopendir(), _wreaddir(), _wclosedir() and _wrewinddir(). + * Thanks to Edgar Buerkle and Jan Nijtmans for ideas and code. + * + * Do not include windows.h. This allows dirent.h to be integrated more + * easily into programs using winsock. Thanks to Fernando Azaldegui. + * + * Version 1.11, Mar 15, 2011, Toni Ronkko + * Defined FILE_ATTRIBUTE_DEVICE for MSVC 6.0. + * + * Version 1.10, Aug 11, 2010, Toni Ronkko + * Added d_type and d_namlen fields to dirent structure. The former is + * especially useful for determining whether directory entry represents a + * file or a directory. For more information, see + * http://www.delorie.com/gnu/docs/glibc/libc_270.html + * + * Improved conformance to the standards. For example, errno is now set + * properly on failure and assert() is never used. Thanks to Peter Brockam + * for suggestions. + * + * Fixed a bug in rewinddir(): when using relative directory names, change + * of working directory no longer causes rewinddir() to fail. + * + * Version 1.9, Dec 15, 2009, John Cunningham + * Added rewinddir member function + * + * Version 1.8, Jan 18, 2008, Toni Ronkko + * Using FindFirstFileA and WIN32_FIND_DATAA to avoid converting string + * between multi-byte and unicode representations. This makes the + * code simpler and also allows the code to be compiled under MingW. Thanks + * to Azriel Fasten for the suggestion. + * + * Mar 4, 2007, Toni Ronkko + * Bug fix: due to the strncpy_s() function this file only compiled in + * Visual Studio 2005. Using the new string functions only when the + * compiler version allows. + * + * Nov 2, 2006, Toni Ronkko + * Major update: removed support for Watcom C, MS-DOS and Turbo C to + * simplify the file, updated the code to compile cleanly on Visual + * Studio 2005 with both unicode and multi-byte character strings, + * removed rewinddir() as it had a bug. + * + * Aug 20, 2006, Toni Ronkko + * Removed all remarks about MSVC 1.0, which is antiqued now. Simplified + * comments by removing SGML tags. + * + * May 14 2002, Toni Ronkko + * Embedded the function definitions directly to the header so that no + * source modules need to be included in the Visual Studio project. Removed + * all the dependencies to other projects so that this header file can be + * used independently. + * + * May 28 1998, Toni Ronkko + * First version. + *****************************************************************************/ +#ifndef DIRENT_H +#define DIRENT_H + +#if !defined(_68K_) && !defined(_MPPC_) && !defined(_X86_) && !defined(_IA64_) && !defined(_AMD64_) && defined(_M_IX86) +# define _X86_ +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Indicates that d_type field is available in dirent structure */ +#define _DIRENT_HAVE_D_TYPE + +/* Indicates that d_namlen field is available in dirent structure */ +#define _DIRENT_HAVE_D_NAMLEN + +/* Entries missing from MSVC 6.0 */ +#if !defined(FILE_ATTRIBUTE_DEVICE) +# define FILE_ATTRIBUTE_DEVICE 0x40 +#endif + +/* File type and permission flags for stat() */ +#if !defined(S_IFMT) +# define S_IFMT _S_IFMT /* File type mask */ +#endif +#if !defined(S_IFDIR) +# define S_IFDIR _S_IFDIR /* Directory */ +#endif +#if !defined(S_IFCHR) +# define S_IFCHR _S_IFCHR /* Character device */ +#endif +#if !defined(S_IFFIFO) +# define S_IFFIFO _S_IFFIFO /* Pipe */ +#endif +#if !defined(S_IFREG) +# define S_IFREG _S_IFREG /* Regular file */ +#endif +#if !defined(S_IREAD) +# define S_IREAD _S_IREAD /* Read permission */ +#endif +#if !defined(S_IWRITE) +# define S_IWRITE _S_IWRITE /* Write permission */ +#endif +#if !defined(S_IEXEC) +# define S_IEXEC _S_IEXEC /* Execute permission */ +#endif +#if !defined(S_IFIFO) +# define S_IFIFO _S_IFIFO /* Pipe */ +#endif +#if !defined(S_IFBLK) +# define S_IFBLK 0 /* Block device */ +#endif +#if !defined(S_IFLNK) +# define S_IFLNK 0 /* Link */ +#endif +#if !defined(S_IFSOCK) +# define S_IFSOCK 0 /* Socket */ +#endif + +#if defined(_MSC_VER) +# define S_IRUSR S_IREAD /* Read user */ +# define S_IWUSR S_IWRITE /* Write user */ +# define S_IXUSR 0 /* Execute user */ +# define S_IRGRP 0 /* Read group */ +# define S_IWGRP 0 /* Write group */ +# define S_IXGRP 0 /* Execute group */ +# define S_IROTH 0 /* Read others */ +# define S_IWOTH 0 /* Write others */ +# define S_IXOTH 0 /* Execute others */ +#endif + +/* Maximum length of file name */ +#if !defined(PATH_MAX) +# define PATH_MAX MAX_PATH +#endif +#if !defined(FILENAME_MAX) +# define FILENAME_MAX MAX_PATH +#endif +#if !defined(NAME_MAX) +# define NAME_MAX FILENAME_MAX +#endif + +/* File type flags for d_type */ +#define DT_UNKNOWN 0 +#define DT_REG S_IFREG +#define DT_DIR S_IFDIR +#define DT_FIFO S_IFIFO +#define DT_SOCK S_IFSOCK +#define DT_CHR S_IFCHR +#define DT_BLK S_IFBLK + +/* Macros for converting between st_mode and d_type */ +#define IFTODT(mode) ((mode) & S_IFMT) +#define DTTOIF(type) (type) + +/* + * File type macros. Note that block devices, sockets and links cannot be + * distinguished on Windows and the macros S_ISBLK, S_ISSOCK and S_ISLNK are + * only defined for compatibility. These macros should always return false + * on Windows. + */ +#define S_ISFIFO(mode) (((mode) & S_IFMT) == S_IFIFO) +#define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) +#define S_ISREG(mode) (((mode) & S_IFMT) == S_IFREG) +#define S_ISLNK(mode) (((mode) & S_IFMT) == S_IFLNK) +#define S_ISSOCK(mode) (((mode) & S_IFMT) == S_IFSOCK) +#define S_ISCHR(mode) (((mode) & S_IFMT) == S_IFCHR) +#define S_ISBLK(mode) (((mode) & S_IFMT) == S_IFBLK) + +/* Return the exact length of d_namlen without zero terminator */ +#define _D_EXACT_NAMLEN(p) ((p)->d_namlen) + +/* Return number of bytes needed to store d_namlen */ +#define _D_ALLOC_NAMLEN(p) (PATH_MAX + 1) + + +#ifdef __cplusplus +extern "C" { +#endif + + +/* Wide-character version */ +struct _wdirent { + long d_ino; /* Always zero */ + unsigned short d_reclen; /* Structure size */ + size_t d_namlen; /* Length of name without \0 */ + int d_type; /* File type */ + wchar_t d_name[PATH_MAX + 1]; /* File name */ +}; +typedef struct _wdirent _wdirent; + +struct _WDIR { + struct _wdirent ent; /* Current directory entry */ + WIN32_FIND_DATAW data; /* Private file data */ + int cached; /* True if data is valid */ + HANDLE handle; /* Win32 search handle */ + wchar_t *patt; /* Initial directory name */ +}; +typedef struct _WDIR _WDIR; + +static _WDIR *_wopendir (const wchar_t *dirname); +static struct _wdirent *_wreaddir (_WDIR *dirp); +static int _wclosedir (_WDIR *dirp); +static void _wrewinddir (_WDIR* dirp); + + +/* For compatibility with Symbian */ +#define wdirent _wdirent +#define WDIR _WDIR +#define wopendir _wopendir +#define wreaddir _wreaddir +#define wclosedir _wclosedir +#define wrewinddir _wrewinddir + + +/* Multi-byte character versions */ +struct dirent { + long d_ino; /* Always zero */ + unsigned short d_reclen; /* Structure size */ + size_t d_namlen; /* Length of name without \0 */ + int d_type; /* File type */ + char d_name[PATH_MAX + 1]; /* File name */ +}; +typedef struct dirent dirent; + +struct DIR { + struct dirent ent; + struct _WDIR *wdirp; +}; +typedef struct DIR DIR; + +static DIR *opendir (const char *dirname); +static struct dirent *readdir (DIR *dirp); +static int closedir (DIR *dirp); +static void rewinddir (DIR* dirp); + + +/* Internal utility functions */ +static WIN32_FIND_DATAW *dirent_first (_WDIR *dirp); +static WIN32_FIND_DATAW *dirent_next (_WDIR *dirp); + +static int dirent_mbstowcs_s( + size_t *pReturnValue, + wchar_t *wcstr, + size_t sizeInWords, + const char *mbstr, + size_t count); + +static int dirent_wcstombs_s( + size_t *pReturnValue, + char *mbstr, + size_t sizeInBytes, + const wchar_t *wcstr, + size_t count); + +static void dirent_set_errno (int error); + +/* + * Open directory stream DIRNAME for read and return a pointer to the + * internal working area that is used to retrieve individual directory + * entries. + */ +static _WDIR* +_wopendir( + const wchar_t *dirname) +{ + _WDIR *dirp = NULL; + int error; + + /* Must have directory name */ + if (dirname == NULL || dirname[0] == '\0') { + dirent_set_errno (ENOENT); + return NULL; + } + + /* Allocate new _WDIR structure */ + dirp = (_WDIR*) malloc (sizeof (struct _WDIR)); + if (dirp != NULL) { + DWORD n; + + /* Reset _WDIR structure */ + dirp->handle = INVALID_HANDLE_VALUE; + dirp->patt = NULL; + dirp->cached = 0; + + /* Compute the length of full path plus zero terminator */ + n = GetFullPathNameW (dirname, 0, NULL, NULL); + + /* Allocate room for absolute directory name and search pattern */ + dirp->patt = (wchar_t*) malloc (sizeof (wchar_t) * n + 16); + if (dirp->patt) { + + /* + * Convert relative directory name to an absolute one. This + * allows rewinddir() to function correctly even when current + * working directory is changed between opendir() and rewinddir(). + */ + n = GetFullPathNameW (dirname, n, dirp->patt, NULL); + if (n > 0) { + wchar_t *p; + + /* Append search pattern \* to the directory name */ + p = dirp->patt + n; + if (dirp->patt < p) { + switch (p[-1]) { + case '\\': + case '/': + case ':': + /* Directory ends in path separator, e.g. c:\temp\ */ + /*NOP*/; + break; + + default: + /* Directory name doesn't end in path separator */ + *p++ = '\\'; + } + } + *p++ = '*'; + *p = '\0'; + + /* Open directory stream and retrieve the first entry */ + if (dirent_first (dirp)) { + /* Directory stream opened successfully */ + error = 0; + } else { + /* Cannot retrieve first entry */ + error = 1; + dirent_set_errno (ENOENT); + } + + } else { + /* Cannot retrieve full path name */ + dirent_set_errno (ENOENT); + error = 1; + } + + } else { + /* Cannot allocate memory for search pattern */ + error = 1; + } + + } else { + /* Cannot allocate _WDIR structure */ + error = 1; + } + + /* Clean up in case of error */ + if (error && dirp) { + _wclosedir (dirp); + dirp = NULL; + } + + return dirp; +} + +/* + * Read next directory entry. The directory entry is returned in dirent + * structure in the d_name field. Individual directory entries returned by + * this function include regular files, sub-directories, pseudo-directories + * "." and ".." as well as volume labels, hidden files and system files. + */ +static struct _wdirent* +_wreaddir( + _WDIR *dirp) +{ + WIN32_FIND_DATAW *datap; + struct _wdirent *entp; + + /* Read next directory entry */ + datap = dirent_next (dirp); + if (datap) { + size_t n; + DWORD attr; + + /* Pointer to directory entry to return */ + entp = &dirp->ent; + + /* + * Copy file name as wide-character string. If the file name is too + * long to fit in to the destination buffer, then truncate file name + * to PATH_MAX characters and zero-terminate the buffer. + */ + n = 0; + while (n < PATH_MAX && datap->cFileName[n] != 0) { + entp->d_name[n] = datap->cFileName[n]; + n++; + } + dirp->ent.d_name[n] = 0; + + /* Length of file name excluding zero terminator */ + entp->d_namlen = n; + + /* File type */ + attr = datap->dwFileAttributes; + if ((attr & FILE_ATTRIBUTE_DEVICE) != 0) { + entp->d_type = DT_CHR; + } else if ((attr & FILE_ATTRIBUTE_DIRECTORY) != 0) { + entp->d_type = DT_DIR; + } else { + entp->d_type = DT_REG; + } + + /* Reset dummy fields */ + entp->d_ino = 0; + entp->d_reclen = sizeof (struct _wdirent); + + } else { + + /* Last directory entry read */ + entp = NULL; + + } + + return entp; +} + +/* + * Close directory stream opened by opendir() function. This invalidates the + * DIR structure as well as any directory entry read previously by + * _wreaddir(). + */ +static int +_wclosedir( + _WDIR *dirp) +{ + int ok; + if (dirp) { + + /* Release search handle */ + if (dirp->handle != INVALID_HANDLE_VALUE) { + FindClose (dirp->handle); + dirp->handle = INVALID_HANDLE_VALUE; + } + + /* Release search pattern */ + if (dirp->patt) { + free (dirp->patt); + dirp->patt = NULL; + } + + /* Release directory structure */ + free (dirp); + ok = /*success*/0; + + } else { + /* Invalid directory stream */ + dirent_set_errno (EBADF); + ok = /*failure*/-1; + } + return ok; +} + +/* + * Rewind directory stream such that _wreaddir() returns the very first + * file name again. + */ +static void +_wrewinddir( + _WDIR* dirp) +{ + if (dirp) { + /* Release existing search handle */ + if (dirp->handle != INVALID_HANDLE_VALUE) { + FindClose (dirp->handle); + } + + /* Open new search handle */ + dirent_first (dirp); + } +} + +/* Get first directory entry (internal) */ +static WIN32_FIND_DATAW* +dirent_first( + _WDIR *dirp) +{ + WIN32_FIND_DATAW *datap; + + /* Open directory and retrieve the first entry */ + dirp->handle = FindFirstFileW (dirp->patt, &dirp->data); + if (dirp->handle != INVALID_HANDLE_VALUE) { + + /* a directory entry is now waiting in memory */ + datap = &dirp->data; + dirp->cached = 1; + + } else { + + /* Failed to re-open directory: no directory entry in memory */ + dirp->cached = 0; + datap = NULL; + + } + return datap; +} + +/* Get next directory entry (internal) */ +static WIN32_FIND_DATAW* +dirent_next( + _WDIR *dirp) +{ + WIN32_FIND_DATAW *p; + + /* Get next directory entry */ + if (dirp->cached != 0) { + + /* A valid directory entry already in memory */ + p = &dirp->data; + dirp->cached = 0; + + } else if (dirp->handle != INVALID_HANDLE_VALUE) { + + /* Get the next directory entry from stream */ + if (FindNextFileW (dirp->handle, &dirp->data) != FALSE) { + /* Got a file */ + p = &dirp->data; + } else { + /* The very last entry has been processed or an error occured */ + FindClose (dirp->handle); + dirp->handle = INVALID_HANDLE_VALUE; + p = NULL; + } + + } else { + + /* End of directory stream reached */ + p = NULL; + + } + + return p; +} + +/* + * Open directory stream using plain old C-string. + */ +static DIR* +opendir( + const char *dirname) +{ + struct DIR *dirp; + int error; + + /* Must have directory name */ + if (dirname == NULL || dirname[0] == '\0') { + dirent_set_errno (ENOENT); + return NULL; + } + + /* Allocate memory for DIR structure */ + dirp = (DIR*) malloc (sizeof (struct DIR)); + if (dirp) { + wchar_t wname[PATH_MAX + 1]; + size_t n; + + /* Convert directory name to wide-character string */ + error = dirent_mbstowcs_s( + &n, wname, PATH_MAX + 1, dirname, PATH_MAX); + if (!error) { + + /* Open directory stream using wide-character name */ + dirp->wdirp = _wopendir (wname); + if (dirp->wdirp) { + /* Directory stream opened */ + error = 0; + } else { + /* Failed to open directory stream */ + error = 1; + } + + } else { + /* + * Cannot convert file name to wide-character string. This + * occurs if the string contains invalid multi-byte sequences or + * the output buffer is too small to contain the resulting + * string. + */ + error = 1; + } + + } else { + /* Cannot allocate DIR structure */ + error = 1; + } + + /* Clean up in case of error */ + if (error && dirp) { + free (dirp); + dirp = NULL; + } + + return dirp; +} + +/* + * Read next directory entry. + * + * When working with text consoles, please note that file names returned by + * readdir() are represented in the default ANSI code page while any output to + * console is typically formatted on another code page. Thus, non-ASCII + * characters in file names will not usually display correctly on console. The + * problem can be fixed in two ways: (1) change the character set of console + * to 1252 using chcp utility and use Lucida Console font, or (2) use + * _cprintf function when writing to console. The _cprinf() will re-encode + * ANSI strings to the console code page so many non-ASCII characters will + * display correcly. + */ +static struct dirent* +readdir( + DIR *dirp) +{ + WIN32_FIND_DATAW *datap; + struct dirent *entp; + + /* Read next directory entry */ + datap = dirent_next (dirp->wdirp); + if (datap) { + size_t n; + int error; + + /* Attempt to convert file name to multi-byte string */ + error = dirent_wcstombs_s( + &n, dirp->ent.d_name, MAX_PATH + 1, datap->cFileName, MAX_PATH); + + /* + * If the file name cannot be represented by a multi-byte string, + * then attempt to use old 8+3 file name. This allows traditional + * Unix-code to access some file names despite of unicode + * characters, although file names may seem unfamiliar to the user. + * + * Be ware that the code below cannot come up with a short file + * name unless the file system provides one. At least + * VirtualBox shared folders fail to do this. + */ + if (error && datap->cAlternateFileName[0] != '\0') { + error = dirent_wcstombs_s( + &n, dirp->ent.d_name, MAX_PATH + 1, datap->cAlternateFileName, + sizeof (datap->cAlternateFileName) / + sizeof (datap->cAlternateFileName[0])); + } + + if (!error) { + DWORD attr; + + /* Initialize directory entry for return */ + entp = &dirp->ent; + + /* Length of file name excluding zero terminator */ + entp->d_namlen = n - 1; + + /* File attributes */ + attr = datap->dwFileAttributes; + if ((attr & FILE_ATTRIBUTE_DEVICE) != 0) { + entp->d_type = DT_CHR; + } else if ((attr & FILE_ATTRIBUTE_DIRECTORY) != 0) { + entp->d_type = DT_DIR; + } else { + entp->d_type = DT_REG; + } + + /* Reset dummy fields */ + entp->d_ino = 0; + entp->d_reclen = sizeof (struct dirent); + + } else { + /* + * Cannot convert file name to multi-byte string so construct + * an errornous directory entry and return that. Note that + * we cannot return NULL as that would stop the processing + * of directory entries completely. + */ + entp = &dirp->ent; + entp->d_name[0] = '?'; + entp->d_name[1] = '\0'; + entp->d_namlen = 1; + entp->d_type = DT_UNKNOWN; + entp->d_ino = 0; + entp->d_reclen = 0; + } + + } else { + /* No more directory entries */ + entp = NULL; + } + + return entp; +} + +/* + * Close directory stream. + */ +static int +closedir( + DIR *dirp) +{ + int ok; + if (dirp) { + + /* Close wide-character directory stream */ + ok = _wclosedir (dirp->wdirp); + dirp->wdirp = NULL; + + /* Release multi-byte character version */ + free (dirp); + + } else { + + /* Invalid directory stream */ + dirent_set_errno (EBADF); + ok = /*failure*/-1; + + } + return ok; +} + +/* + * Rewind directory stream to beginning. + */ +static void +rewinddir( + DIR* dirp) +{ + /* Rewind wide-character string directory stream */ + _wrewinddir (dirp->wdirp); +} + +/* Convert multi-byte string to wide character string */ +static int +dirent_mbstowcs_s( + size_t *pReturnValue, + wchar_t *wcstr, + size_t sizeInWords, + const char *mbstr, + size_t count) +{ + int error; + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + + /* Microsoft Visual Studio 2005 or later */ + error = mbstowcs_s (pReturnValue, wcstr, sizeInWords, mbstr, count); + +#else + + /* Older Visual Studio or non-Microsoft compiler */ + size_t n; + + /* Convert to wide-character string */ + n = mbstowcs (wcstr, mbstr, count); + if (n < sizeInWords) { + + /* Zero-terminate output buffer */ + if (wcstr) { + wcstr[n] = 0; + } + + /* Length of resuting multi-byte string WITH zero terminator */ + if (pReturnValue) { + *pReturnValue = n + 1; + } + + /* Success */ + error = 0; + + } else { + + /* Could not convert string */ + error = 1; + + } + +#endif + + return error; +} + +/* Convert wide-character string to multi-byte string */ +static int +dirent_wcstombs_s( + size_t *pReturnValue, + char *mbstr, + size_t sizeInBytes, + const wchar_t *wcstr, + size_t count) +{ + int error; + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + + /* Microsoft Visual Studio 2005 or later */ + error = wcstombs_s (pReturnValue, mbstr, sizeInBytes, wcstr, count); + +#else + + /* Older Visual Studio or non-Microsoft compiler */ + size_t n; + + /* Convert to multi-byte string */ + n = wcstombs (mbstr, wcstr, count); + if (n < sizeInBytes) { + + /* Zero-terminate output buffer */ + if (mbstr) { + mbstr[n] = '\0'; + } + + /* Lenght of resulting multi-bytes string WITH zero-terminator */ + if (pReturnValue) { + *pReturnValue = n + 1; + } + + /* Success */ + error = 0; + + } else { + + /* Cannot convert string */ + error = 1; + + } + +#endif + + return error; +} + +/* Set errno variable */ +static void +dirent_set_errno( + int error) +{ +#if defined(_MSC_VER) + + /* Microsoft Visual Studio */ + _set_errno (error); + +#else + + /* Non-Microsoft compiler */ + errno = error; + +#endif +} + + +#ifdef __cplusplus +} +#endif +#endif /*DIRENT_H*/ + diff --git a/src/openalpr/support/windows/unistd_partial.h b/src/openalpr/support/windows/unistd_partial.h new file mode 100644 index 0000000..3c2ef38 --- /dev/null +++ b/src/openalpr/support/windows/unistd_partial.h @@ -0,0 +1,42 @@ +#ifndef _UNISTD_H +#define _UNISTD_H 1 + +/* This file intended to serve as a drop-in replacement for + * unistd.h on Windows + * Please add functionality as neeeded + */ + +#include +#include +//#include /* getopt from: http://www.pwilson.net/sample.html. */ +#include /* for getpid() and the exec..() family */ + +#define srandom srand +#define random rand + +/* Values for the second argument to access. + These may be OR'd together. */ +#define R_OK 4 /* Test for read permission. */ +#define W_OK 2 /* Test for write permission. */ +//#define X_OK 1 /* execute permission - unsupported in windows*/ +#define F_OK 0 /* Test for existence. */ + +#define access _access +#define ftruncate _chsize + +#define ssize_t int + +#define STDIN_FILENO 0 +#define STDOUT_FILENO 1 +#define STDERR_FILENO 2 +/* should be in some equivalent to */ +typedef __int8 int8_t; +typedef __int16 int16_t; +typedef __int32 int32_t; +typedef __int64 int64_t; +typedef unsigned __int8 uint8_t; +typedef unsigned __int16 uint16_t; +typedef unsigned __int32 uint32_t; +typedef unsigned __int64 uint64_t; + +#endif /* unistd.h */ \ No newline at end of file diff --git a/src/openalpr/support/windows/utils.h b/src/openalpr/support/windows/utils.h new file mode 100644 index 0000000..7519f41 --- /dev/null +++ b/src/openalpr/support/windows/utils.h @@ -0,0 +1,7 @@ + +#include + +static inline double round(double val) +{ + return floor(val + 0.5); +} \ No newline at end of file From df18fc0404a50885b5a61fe19cc2fc5765195ec6 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 2 Jan 2014 16:02:01 -0600 Subject: [PATCH 03/14] Added simpleini library --- src/openalpr/simpleini/CMakeLists.txt | 8 + src/openalpr/simpleini/ConvertUTF.c | 539 ++++ src/openalpr/simpleini/ConvertUTF.h | 149 ++ src/openalpr/simpleini/LICENCE.txt | 22 + src/openalpr/simpleini/README.md | 150 ++ src/openalpr/simpleini/ini.syn | 36 + src/openalpr/simpleini/simpleini.doxy | 1321 ++++++++++ src/openalpr/simpleini/simpleini.h | 3385 +++++++++++++++++++++++++ src/openalpr/simpleini/snippets.cpp | 123 + 9 files changed, 5733 insertions(+) create mode 100644 src/openalpr/simpleini/CMakeLists.txt create mode 100644 src/openalpr/simpleini/ConvertUTF.c create mode 100644 src/openalpr/simpleini/ConvertUTF.h create mode 100644 src/openalpr/simpleini/LICENCE.txt create mode 100644 src/openalpr/simpleini/README.md create mode 100644 src/openalpr/simpleini/ini.syn create mode 100644 src/openalpr/simpleini/simpleini.doxy create mode 100644 src/openalpr/simpleini/simpleini.h create mode 100644 src/openalpr/simpleini/snippets.cpp diff --git a/src/openalpr/simpleini/CMakeLists.txt b/src/openalpr/simpleini/CMakeLists.txt new file mode 100644 index 0000000..36baec2 --- /dev/null +++ b/src/openalpr/simpleini/CMakeLists.txt @@ -0,0 +1,8 @@ + + +set(simpleini_source_files + ConvertUTF.c +) + + +add_library(simpleini ${simpleini_source_files}) \ No newline at end of file diff --git a/src/openalpr/simpleini/ConvertUTF.c b/src/openalpr/simpleini/ConvertUTF.c new file mode 100644 index 0000000..9b3deeb --- /dev/null +++ b/src/openalpr/simpleini/ConvertUTF.c @@ -0,0 +1,539 @@ +/* + * Copyright 2001-2004 Unicode, Inc. + * + * Disclaimer + * + * This source code is provided as is by Unicode, Inc. No claims are + * made as to fitness for any particular purpose. No warranties of any + * kind are expressed or implied. The recipient agrees to determine + * applicability of information provided. If this file has been + * purchased on magnetic or optical media from Unicode, Inc., the + * sole remedy for any claim will be exchange of defective media + * within 90 days of receipt. + * + * Limitations on Rights to Redistribute This Code + * + * Unicode, Inc. hereby grants the right to freely use the information + * supplied in this file in the creation of products supporting the + * Unicode Standard, and to make copies of this file in any form + * for internal or external distribution as long as this notice + * remains attached. + */ + +/* --------------------------------------------------------------------- + + Conversions between UTF32, UTF-16, and UTF-8. Source code file. + Author: Mark E. Davis, 1994. + Rev History: Rick McGowan, fixes & updates May 2001. + Sept 2001: fixed const & error conditions per + mods suggested by S. Parent & A. Lillich. + June 2002: Tim Dodd added detection and handling of incomplete + source sequences, enhanced error detection, added casts + to eliminate compiler warnings. + July 2003: slight mods to back out aggressive FFFE detection. + Jan 2004: updated switches in from-UTF8 conversions. + Oct 2004: updated to use UNI_MAX_LEGAL_UTF32 in UTF-32 conversions. + + See the header file "ConvertUTF.h" for complete documentation. + +------------------------------------------------------------------------ */ + + +#include "ConvertUTF.h" +#ifdef CVTUTF_DEBUG +#include +#endif + +static const int halfShift = 10; /* used for shifting by 10 bits */ + +static const UTF32 halfBase = 0x0010000UL; +static const UTF32 halfMask = 0x3FFUL; + +#define UNI_SUR_HIGH_START (UTF32)0xD800 +#define UNI_SUR_HIGH_END (UTF32)0xDBFF +#define UNI_SUR_LOW_START (UTF32)0xDC00 +#define UNI_SUR_LOW_END (UTF32)0xDFFF +#define false 0 +#define true 1 + +/* --------------------------------------------------------------------- */ + +ConversionResult ConvertUTF32toUTF16 ( + const UTF32** sourceStart, const UTF32* sourceEnd, + UTF16** targetStart, UTF16* targetEnd, ConversionFlags flags) { + ConversionResult result = conversionOK; + const UTF32* source = *sourceStart; + UTF16* target = *targetStart; + while (source < sourceEnd) { + UTF32 ch; + if (target >= targetEnd) { + result = targetExhausted; break; + } + ch = *source++; + if (ch <= UNI_MAX_BMP) { /* Target is a character <= 0xFFFF */ + /* UTF-16 surrogate values are illegal in UTF-32; 0xffff or 0xfffe are both reserved values */ + if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_LOW_END) { + if (flags == strictConversion) { + --source; /* return to the illegal value itself */ + result = sourceIllegal; + break; + } else { + *target++ = UNI_REPLACEMENT_CHAR; + } + } else { + *target++ = (UTF16)ch; /* normal case */ + } + } else if (ch > UNI_MAX_LEGAL_UTF32) { + if (flags == strictConversion) { + result = sourceIllegal; + } else { + *target++ = UNI_REPLACEMENT_CHAR; + } + } else { + /* target is a character in range 0xFFFF - 0x10FFFF. */ + if (target + 1 >= targetEnd) { + --source; /* Back up source pointer! */ + result = targetExhausted; break; + } + ch -= halfBase; + *target++ = (UTF16)((ch >> halfShift) + UNI_SUR_HIGH_START); + *target++ = (UTF16)((ch & halfMask) + UNI_SUR_LOW_START); + } + } + *sourceStart = source; + *targetStart = target; + return result; +} + +/* --------------------------------------------------------------------- */ + +ConversionResult ConvertUTF16toUTF32 ( + const UTF16** sourceStart, const UTF16* sourceEnd, + UTF32** targetStart, UTF32* targetEnd, ConversionFlags flags) { + ConversionResult result = conversionOK; + const UTF16* source = *sourceStart; + UTF32* target = *targetStart; + UTF32 ch, ch2; + while (source < sourceEnd) { + const UTF16* oldSource = source; /* In case we have to back up because of target overflow. */ + ch = *source++; + /* If we have a surrogate pair, convert to UTF32 first. */ + if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_HIGH_END) { + /* If the 16 bits following the high surrogate are in the source buffer... */ + if (source < sourceEnd) { + ch2 = *source; + /* If it's a low surrogate, convert to UTF32. */ + if (ch2 >= UNI_SUR_LOW_START && ch2 <= UNI_SUR_LOW_END) { + ch = ((ch - UNI_SUR_HIGH_START) << halfShift) + + (ch2 - UNI_SUR_LOW_START) + halfBase; + ++source; + } else if (flags == strictConversion) { /* it's an unpaired high surrogate */ + --source; /* return to the illegal value itself */ + result = sourceIllegal; + break; + } + } else { /* We don't have the 16 bits following the high surrogate. */ + --source; /* return to the high surrogate */ + result = sourceExhausted; + break; + } + } else if (flags == strictConversion) { + /* UTF-16 surrogate values are illegal in UTF-32 */ + if (ch >= UNI_SUR_LOW_START && ch <= UNI_SUR_LOW_END) { + --source; /* return to the illegal value itself */ + result = sourceIllegal; + break; + } + } + if (target >= targetEnd) { + source = oldSource; /* Back up source pointer! */ + result = targetExhausted; break; + } + *target++ = ch; + } + *sourceStart = source; + *targetStart = target; +#ifdef CVTUTF_DEBUG +if (result == sourceIllegal) { + fprintf(stderr, "ConvertUTF16toUTF32 illegal seq 0x%04x,%04x\n", ch, ch2); + fflush(stderr); +} +#endif + return result; +} + +/* --------------------------------------------------------------------- */ + +/* + * Index into the table below with the first byte of a UTF-8 sequence to + * get the number of trailing bytes that are supposed to follow it. + * Note that *legal* UTF-8 values can't have 4 or 5-bytes. The table is + * left as-is for anyone who may want to do such conversion, which was + * allowed in earlier algorithms. + */ +static const char trailingBytesForUTF8[256] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3,4,4,4,4,5,5,5,5 +}; + +/* + * Magic values subtracted from a buffer value during UTF8 conversion. + * This table contains as many values as there might be trailing bytes + * in a UTF-8 sequence. + */ +static const UTF32 offsetsFromUTF8[6] = { 0x00000000UL, 0x00003080UL, 0x000E2080UL, + 0x03C82080UL, 0xFA082080UL, 0x82082080UL }; + +/* + * Once the bits are split out into bytes of UTF-8, this is a mask OR-ed + * into the first byte, depending on how many bytes follow. There are + * as many entries in this table as there are UTF-8 sequence types. + * (I.e., one byte sequence, two byte... etc.). Remember that sequencs + * for *legal* UTF-8 will be 4 or fewer bytes total. + */ +static const UTF8 firstByteMark[7] = { 0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }; + +/* --------------------------------------------------------------------- */ + +/* The interface converts a whole buffer to avoid function-call overhead. + * Constants have been gathered. Loops & conditionals have been removed as + * much as possible for efficiency, in favor of drop-through switches. + * (See "Note A" at the bottom of the file for equivalent code.) + * If your compiler supports it, the "isLegalUTF8" call can be turned + * into an inline function. + */ + +/* --------------------------------------------------------------------- */ + +ConversionResult ConvertUTF16toUTF8 ( + const UTF16** sourceStart, const UTF16* sourceEnd, + UTF8** targetStart, UTF8* targetEnd, ConversionFlags flags) { + ConversionResult result = conversionOK; + const UTF16* source = *sourceStart; + UTF8* target = *targetStart; + while (source < sourceEnd) { + UTF32 ch; + unsigned short bytesToWrite = 0; + const UTF32 byteMask = 0xBF; + const UTF32 byteMark = 0x80; + const UTF16* oldSource = source; /* In case we have to back up because of target overflow. */ + ch = *source++; + /* If we have a surrogate pair, convert to UTF32 first. */ + if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_HIGH_END) { + /* If the 16 bits following the high surrogate are in the source buffer... */ + if (source < sourceEnd) { + UTF32 ch2 = *source; + /* If it's a low surrogate, convert to UTF32. */ + if (ch2 >= UNI_SUR_LOW_START && ch2 <= UNI_SUR_LOW_END) { + ch = ((ch - UNI_SUR_HIGH_START) << halfShift) + + (ch2 - UNI_SUR_LOW_START) + halfBase; + ++source; + } else if (flags == strictConversion) { /* it's an unpaired high surrogate */ + --source; /* return to the illegal value itself */ + result = sourceIllegal; + break; + } + } else { /* We don't have the 16 bits following the high surrogate. */ + --source; /* return to the high surrogate */ + result = sourceExhausted; + break; + } + } else if (flags == strictConversion) { + /* UTF-16 surrogate values are illegal in UTF-32 */ + if (ch >= UNI_SUR_LOW_START && ch <= UNI_SUR_LOW_END) { + --source; /* return to the illegal value itself */ + result = sourceIllegal; + break; + } + } + /* Figure out how many bytes the result will require */ + if (ch < (UTF32)0x80) { bytesToWrite = 1; + } else if (ch < (UTF32)0x800) { bytesToWrite = 2; + } else if (ch < (UTF32)0x10000) { bytesToWrite = 3; + } else if (ch < (UTF32)0x110000) { bytesToWrite = 4; + } else { bytesToWrite = 3; + ch = UNI_REPLACEMENT_CHAR; + } + + target += bytesToWrite; + if (target > targetEnd) { + source = oldSource; /* Back up source pointer! */ + target -= bytesToWrite; result = targetExhausted; break; + } + switch (bytesToWrite) { /* note: everything falls through. */ + case 4: *--target = (UTF8)((ch | byteMark) & byteMask); ch >>= 6; + case 3: *--target = (UTF8)((ch | byteMark) & byteMask); ch >>= 6; + case 2: *--target = (UTF8)((ch | byteMark) & byteMask); ch >>= 6; + case 1: *--target = (UTF8)(ch | firstByteMark[bytesToWrite]); + } + target += bytesToWrite; + } + *sourceStart = source; + *targetStart = target; + return result; +} + +/* --------------------------------------------------------------------- */ + +/* + * Utility routine to tell whether a sequence of bytes is legal UTF-8. + * This must be called with the length pre-determined by the first byte. + * If not calling this from ConvertUTF8to*, then the length can be set by: + * length = trailingBytesForUTF8[*source]+1; + * and the sequence is illegal right away if there aren't that many bytes + * available. + * If presented with a length > 4, this returns false. The Unicode + * definition of UTF-8 goes up to 4-byte sequences. + */ + +static Boolean isLegalUTF8(const UTF8 *source, int length) { + UTF8 a; + const UTF8 *srcptr = source+length; + switch (length) { + default: return false; + /* Everything else falls through when "true"... */ + case 4: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return false; + case 3: if ((a = (*--srcptr)) < 0x80 || a > 0xBF) return false; + case 2: if ((a = (*--srcptr)) > 0xBF) return false; + + switch (*source) { + /* no fall-through in this inner switch */ + case 0xE0: if (a < 0xA0) return false; break; + case 0xED: if (a > 0x9F) return false; break; + case 0xF0: if (a < 0x90) return false; break; + case 0xF4: if (a > 0x8F) return false; break; + default: if (a < 0x80) return false; + } + + case 1: if (*source >= 0x80 && *source < 0xC2) return false; + } + if (*source > 0xF4) return false; + return true; +} + +/* --------------------------------------------------------------------- */ + +/* + * Exported function to return whether a UTF-8 sequence is legal or not. + * This is not used here; it's just exported. + */ +Boolean isLegalUTF8Sequence(const UTF8 *source, const UTF8 *sourceEnd) { + int length = trailingBytesForUTF8[*source]+1; + if (source+length > sourceEnd) { + return false; + } + return isLegalUTF8(source, length); +} + +/* --------------------------------------------------------------------- */ + +ConversionResult ConvertUTF8toUTF16 ( + const UTF8** sourceStart, const UTF8* sourceEnd, + UTF16** targetStart, UTF16* targetEnd, ConversionFlags flags) { + ConversionResult result = conversionOK; + const UTF8* source = *sourceStart; + UTF16* target = *targetStart; + while (source < sourceEnd) { + UTF32 ch = 0; + unsigned short extraBytesToRead = trailingBytesForUTF8[*source]; + if (source + extraBytesToRead >= sourceEnd) { + result = sourceExhausted; break; + } + /* Do this check whether lenient or strict */ + if (! isLegalUTF8(source, extraBytesToRead+1)) { + result = sourceIllegal; + break; + } + /* + * The cases all fall through. See "Note A" below. + */ + switch (extraBytesToRead) { + case 5: ch += *source++; ch <<= 6; /* remember, illegal UTF-8 */ + case 4: ch += *source++; ch <<= 6; /* remember, illegal UTF-8 */ + case 3: ch += *source++; ch <<= 6; + case 2: ch += *source++; ch <<= 6; + case 1: ch += *source++; ch <<= 6; + case 0: ch += *source++; + } + ch -= offsetsFromUTF8[extraBytesToRead]; + + if (target >= targetEnd) { + source -= (extraBytesToRead+1); /* Back up source pointer! */ + result = targetExhausted; break; + } + if (ch <= UNI_MAX_BMP) { /* Target is a character <= 0xFFFF */ + /* UTF-16 surrogate values are illegal in UTF-32 */ + if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_LOW_END) { + if (flags == strictConversion) { + source -= (extraBytesToRead+1); /* return to the illegal value itself */ + result = sourceIllegal; + break; + } else { + *target++ = UNI_REPLACEMENT_CHAR; + } + } else { + *target++ = (UTF16)ch; /* normal case */ + } + } else if (ch > UNI_MAX_UTF16) { + if (flags == strictConversion) { + result = sourceIllegal; + source -= (extraBytesToRead+1); /* return to the start */ + break; /* Bail out; shouldn't continue */ + } else { + *target++ = UNI_REPLACEMENT_CHAR; + } + } else { + /* target is a character in range 0xFFFF - 0x10FFFF. */ + if (target + 1 >= targetEnd) { + source -= (extraBytesToRead+1); /* Back up source pointer! */ + result = targetExhausted; break; + } + ch -= halfBase; + *target++ = (UTF16)((ch >> halfShift) + UNI_SUR_HIGH_START); + *target++ = (UTF16)((ch & halfMask) + UNI_SUR_LOW_START); + } + } + *sourceStart = source; + *targetStart = target; + return result; +} + +/* --------------------------------------------------------------------- */ + +ConversionResult ConvertUTF32toUTF8 ( + const UTF32** sourceStart, const UTF32* sourceEnd, + UTF8** targetStart, UTF8* targetEnd, ConversionFlags flags) { + ConversionResult result = conversionOK; + const UTF32* source = *sourceStart; + UTF8* target = *targetStart; + while (source < sourceEnd) { + UTF32 ch; + unsigned short bytesToWrite = 0; + const UTF32 byteMask = 0xBF; + const UTF32 byteMark = 0x80; + ch = *source++; + if (flags == strictConversion ) { + /* UTF-16 surrogate values are illegal in UTF-32 */ + if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_LOW_END) { + --source; /* return to the illegal value itself */ + result = sourceIllegal; + break; + } + } + /* + * Figure out how many bytes the result will require. Turn any + * illegally large UTF32 things (> Plane 17) into replacement chars. + */ + if (ch < (UTF32)0x80) { bytesToWrite = 1; + } else if (ch < (UTF32)0x800) { bytesToWrite = 2; + } else if (ch < (UTF32)0x10000) { bytesToWrite = 3; + } else if (ch <= UNI_MAX_LEGAL_UTF32) { bytesToWrite = 4; + } else { bytesToWrite = 3; + ch = UNI_REPLACEMENT_CHAR; + result = sourceIllegal; + } + + target += bytesToWrite; + if (target > targetEnd) { + --source; /* Back up source pointer! */ + target -= bytesToWrite; result = targetExhausted; break; + } + switch (bytesToWrite) { /* note: everything falls through. */ + case 4: *--target = (UTF8)((ch | byteMark) & byteMask); ch >>= 6; + case 3: *--target = (UTF8)((ch | byteMark) & byteMask); ch >>= 6; + case 2: *--target = (UTF8)((ch | byteMark) & byteMask); ch >>= 6; + case 1: *--target = (UTF8) (ch | firstByteMark[bytesToWrite]); + } + target += bytesToWrite; + } + *sourceStart = source; + *targetStart = target; + return result; +} + +/* --------------------------------------------------------------------- */ + +ConversionResult ConvertUTF8toUTF32 ( + const UTF8** sourceStart, const UTF8* sourceEnd, + UTF32** targetStart, UTF32* targetEnd, ConversionFlags flags) { + ConversionResult result = conversionOK; + const UTF8* source = *sourceStart; + UTF32* target = *targetStart; + while (source < sourceEnd) { + UTF32 ch = 0; + unsigned short extraBytesToRead = trailingBytesForUTF8[*source]; + if (source + extraBytesToRead >= sourceEnd) { + result = sourceExhausted; break; + } + /* Do this check whether lenient or strict */ + if (! isLegalUTF8(source, extraBytesToRead+1)) { + result = sourceIllegal; + break; + } + /* + * The cases all fall through. See "Note A" below. + */ + switch (extraBytesToRead) { + case 5: ch += *source++; ch <<= 6; + case 4: ch += *source++; ch <<= 6; + case 3: ch += *source++; ch <<= 6; + case 2: ch += *source++; ch <<= 6; + case 1: ch += *source++; ch <<= 6; + case 0: ch += *source++; + } + ch -= offsetsFromUTF8[extraBytesToRead]; + + if (target >= targetEnd) { + source -= (extraBytesToRead+1); /* Back up the source pointer! */ + result = targetExhausted; break; + } + if (ch <= UNI_MAX_LEGAL_UTF32) { + /* + * UTF-16 surrogate values are illegal in UTF-32, and anything + * over Plane 17 (> 0x10FFFF) is illegal. + */ + if (ch >= UNI_SUR_HIGH_START && ch <= UNI_SUR_LOW_END) { + if (flags == strictConversion) { + source -= (extraBytesToRead+1); /* return to the illegal value itself */ + result = sourceIllegal; + break; + } else { + *target++ = UNI_REPLACEMENT_CHAR; + } + } else { + *target++ = ch; + } + } else { /* i.e., ch > UNI_MAX_LEGAL_UTF32 */ + result = sourceIllegal; + *target++ = UNI_REPLACEMENT_CHAR; + } + } + *sourceStart = source; + *targetStart = target; + return result; +} + +/* --------------------------------------------------------------------- + + Note A. + The fall-through switches in UTF-8 reading code save a + temp variable, some decrements & conditionals. The switches + are equivalent to the following loop: + { + int tmpBytesToRead = extraBytesToRead+1; + do { + ch += *source++; + --tmpBytesToRead; + if (tmpBytesToRead) ch <<= 6; + } while (tmpBytesToRead > 0); + } + In UTF-8 writing code, the switches on "bytesToWrite" are + similarly unrolled loops. + + --------------------------------------------------------------------- */ diff --git a/src/openalpr/simpleini/ConvertUTF.h b/src/openalpr/simpleini/ConvertUTF.h new file mode 100644 index 0000000..14d7b70 --- /dev/null +++ b/src/openalpr/simpleini/ConvertUTF.h @@ -0,0 +1,149 @@ +/* + * Copyright 2001-2004 Unicode, Inc. + * + * Disclaimer + * + * This source code is provided as is by Unicode, Inc. No claims are + * made as to fitness for any particular purpose. No warranties of any + * kind are expressed or implied. The recipient agrees to determine + * applicability of information provided. If this file has been + * purchased on magnetic or optical media from Unicode, Inc., the + * sole remedy for any claim will be exchange of defective media + * within 90 days of receipt. + * + * Limitations on Rights to Redistribute This Code + * + * Unicode, Inc. hereby grants the right to freely use the information + * supplied in this file in the creation of products supporting the + * Unicode Standard, and to make copies of this file in any form + * for internal or external distribution as long as this notice + * remains attached. + */ + +/* --------------------------------------------------------------------- + + Conversions between UTF32, UTF-16, and UTF-8. Header file. + + Several funtions are included here, forming a complete set of + conversions between the three formats. UTF-7 is not included + here, but is handled in a separate source file. + + Each of these routines takes pointers to input buffers and output + buffers. The input buffers are const. + + Each routine converts the text between *sourceStart and sourceEnd, + putting the result into the buffer between *targetStart and + targetEnd. Note: the end pointers are *after* the last item: e.g. + *(sourceEnd - 1) is the last item. + + The return result indicates whether the conversion was successful, + and if not, whether the problem was in the source or target buffers. + (Only the first encountered problem is indicated.) + + After the conversion, *sourceStart and *targetStart are both + updated to point to the end of last text successfully converted in + the respective buffers. + + Input parameters: + sourceStart - pointer to a pointer to the source buffer. + The contents of this are modified on return so that + it points at the next thing to be converted. + targetStart - similarly, pointer to pointer to the target buffer. + sourceEnd, targetEnd - respectively pointers to the ends of the + two buffers, for overflow checking only. + + These conversion functions take a ConversionFlags argument. When this + flag is set to strict, both irregular sequences and isolated surrogates + will cause an error. When the flag is set to lenient, both irregular + sequences and isolated surrogates are converted. + + Whether the flag is strict or lenient, all illegal sequences will cause + an error return. This includes sequences such as: , , + or in UTF-8, and values above 0x10FFFF in UTF-32. Conformant code + must check for illegal sequences. + + When the flag is set to lenient, characters over 0x10FFFF are converted + to the replacement character; otherwise (when the flag is set to strict) + they constitute an error. + + Output parameters: + The value "sourceIllegal" is returned from some routines if the input + sequence is malformed. When "sourceIllegal" is returned, the source + value will point to the illegal value that caused the problem. E.g., + in UTF-8 when a sequence is malformed, it points to the start of the + malformed sequence. + + Author: Mark E. Davis, 1994. + Rev History: Rick McGowan, fixes & updates May 2001. + Fixes & updates, Sept 2001. + +------------------------------------------------------------------------ */ + +/* --------------------------------------------------------------------- + The following 4 definitions are compiler-specific. + The C standard does not guarantee that wchar_t has at least + 16 bits, so wchar_t is no less portable than unsigned short! + All should be unsigned values to avoid sign extension during + bit mask & shift operations. +------------------------------------------------------------------------ */ + +typedef unsigned int UTF32; /* at least 32 bits */ +typedef unsigned short UTF16; /* at least 16 bits */ +typedef unsigned char UTF8; /* typically 8 bits */ +typedef unsigned char Boolean; /* 0 or 1 */ + +/* Some fundamental constants */ +#define UNI_REPLACEMENT_CHAR (UTF32)0x0000FFFD +#define UNI_MAX_BMP (UTF32)0x0000FFFF +#define UNI_MAX_UTF16 (UTF32)0x0010FFFF +#define UNI_MAX_UTF32 (UTF32)0x7FFFFFFF +#define UNI_MAX_LEGAL_UTF32 (UTF32)0x0010FFFF + +typedef enum { + conversionOK, /* conversion successful */ + sourceExhausted, /* partial character in source, but hit end */ + targetExhausted, /* insuff. room in target for conversion */ + sourceIllegal /* source sequence is illegal/malformed */ +} ConversionResult; + +typedef enum { + strictConversion = 0, + lenientConversion +} ConversionFlags; + +/* This is for C++ and does no harm in C */ +#ifdef __cplusplus +extern "C" { +#endif + +ConversionResult ConvertUTF8toUTF16 ( + const UTF8** sourceStart, const UTF8* sourceEnd, + UTF16** targetStart, UTF16* targetEnd, ConversionFlags flags); + +ConversionResult ConvertUTF16toUTF8 ( + const UTF16** sourceStart, const UTF16* sourceEnd, + UTF8** targetStart, UTF8* targetEnd, ConversionFlags flags); + +ConversionResult ConvertUTF8toUTF32 ( + const UTF8** sourceStart, const UTF8* sourceEnd, + UTF32** targetStart, UTF32* targetEnd, ConversionFlags flags); + +ConversionResult ConvertUTF32toUTF8 ( + const UTF32** sourceStart, const UTF32* sourceEnd, + UTF8** targetStart, UTF8* targetEnd, ConversionFlags flags); + +ConversionResult ConvertUTF16toUTF32 ( + const UTF16** sourceStart, const UTF16* sourceEnd, + UTF32** targetStart, UTF32* targetEnd, ConversionFlags flags); + +ConversionResult ConvertUTF32toUTF16 ( + const UTF32** sourceStart, const UTF32* sourceEnd, + UTF16** targetStart, UTF16* targetEnd, ConversionFlags flags); + +Boolean isLegalUTF8Sequence(const UTF8 *source, const UTF8 *sourceEnd); + +#ifdef __cplusplus +} +#endif + +/* --------------------------------------------------------------------- */ diff --git a/src/openalpr/simpleini/LICENCE.txt b/src/openalpr/simpleini/LICENCE.txt new file mode 100644 index 0000000..319e110 --- /dev/null +++ b/src/openalpr/simpleini/LICENCE.txt @@ -0,0 +1,22 @@ +SimpleIni library license: + +The MIT License (MIT) + +Copyright (c) 2006-2013 Brodie Thiesfield + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/openalpr/simpleini/README.md b/src/openalpr/simpleini/README.md new file mode 100644 index 0000000..2e0d25e --- /dev/null +++ b/src/openalpr/simpleini/README.md @@ -0,0 +1,150 @@ +simpleini +========= + +A cross-platform library that provides a simple API to read and write INI-style configuration files. It supports data files in ASCII, MBCS and Unicode. It is designed explicitly to be portable to any platform and has been tested on Windows, WinCE and Linux. Released as open-source and free using the MIT licence. + +# Feature Summary + +- MIT Licence allows free use in all software (including GPL and commercial) +- multi-platform (Windows 95/98/ME/NT/2K/XP/2003, Windows CE, Linux, Unix) +- loading and saving of INI-style configuration files +- configuration files can have any newline format on all platforms +- liberal acceptance of file format + * key/values with no section + * removal of whitespace around sections, keys and values +- support for multi-line values (values with embedded newline characters) +- optional support for multiple keys with the same name +- optional case-insensitive sections and keys (for ASCII characters only) +- saves files with sections and keys in the same order as they were loaded +- preserves comments on the file, section and keys where possible. +- supports both char or wchar_t programming interfaces +- supports both MBCS (system locale) and UTF-8 file encodings +- system locale does not need to be UTF-8 on Linux/Unix to load UTF-8 file +- support for non-ASCII characters in section, keys, values and comments +- support for non-standard character types or file encodings via user-written converter classes +- support for adding/modifying values programmatically +- compiles cleanly in the following compilers: + * Windows/VC6 (warning level 3) + * Windows/VC.NET 2003 (warning level 4) + * Windows/VC 2005 (warning level 4) + * Linux/gcc (-Wall) + * Windows/MinGW GCC + +# Documentation + +Full documentation of the interface is available in doxygen format. + +# Examples + +These snippets are included with the distribution in the file snippets.cpp. + +### SIMPLE USAGE + +```c++ +CSimpleIniA ini; +ini.SetUnicode(); +ini.LoadFile("myfile.ini"); +const char * pVal = ini.GetValue("section", "key", "default"); +ini.SetValue("section", "key", "newvalue"); +``` + +### LOADING DATA + +```c++ +// load from a data file +CSimpleIniA ini(a_bIsUtf8, a_bUseMultiKey, a_bUseMultiLine); +SI_Error rc = ini.LoadFile(a_pszFile); +if (rc < 0) return false; + +// load from a string +std::string strData; +rc = ini.LoadData(strData.c_str(), strData.size()); +if (rc < 0) return false; +``` + +### GETTING SECTIONS AND KEYS + +```c++ +// get all sections +CSimpleIniA::TNamesDepend sections; +ini.GetAllSections(sections); + +// get all keys in a section +CSimpleIniA::TNamesDepend keys; +ini.GetAllKeys("section-name", keys); +``` + +### GETTING VALUES + +```c++ +// get the value of a key +const char * pszValue = ini.GetValue("section-name", + "key-name", NULL /*default*/); + +// get the value of a key which may have multiple +// values. If bHasMultipleValues is true, then just +// one value has been returned +bool bHasMultipleValues; +pszValue = ini.GetValue("section-name", "key-name", + NULL /*default*/, &bHasMultipleValues); + +// get all values of a key with multiple values +CSimpleIniA::TNamesDepend values; +ini.GetAllValues("section-name", "key-name", values); + +// sort the values into the original load order +values.sort(CSimpleIniA::Entry::LoadOrder()); + +// output all of the items +CSimpleIniA::TNamesDepend::const_iterator i; +for (i = values.begin(); i != values.end(); ++i) { + printf("key-name = '%s'\n", i->pItem); +} +``` + +### MODIFYING DATA + +```c++ +// adding a new section +rc = ini.SetValue("new-section", NULL, NULL); +if (rc < 0) return false; +printf("section: %s\n", rc == SI_INSERTED ? + "inserted" : "updated"); + +// adding a new key ("new-section" will be added +// automatically if it doesn't already exist) +rc = ini.SetValue("new-section", "new-key", "value"); +if (rc < 0) return false; +printf("key: %s\n", rc == SI_INSERTED ? + "inserted" : "updated"); + +// changing the value of a key +rc = ini.SetValue("section", "key", "updated-value"); +if (rc < 0) return false; +printf("key: %s\n", rc == SI_INSERTED ? + "inserted" : "updated"); +``` + +### DELETING DATA + +```c++ +// deleting a key from a section. Optionally the entire +// section may be deleted if it is now empty. +ini.Delete("section-name", "key-name", + true /*delete the section if empty*/); + +// deleting an entire section and all keys in it +ini.Delete("section-name", NULL); +``` + +### SAVING DATA + +```c++ +// save the data to a string +rc = ini.Save(strData); +if (rc < 0) return false; + +// save the data back to the file +rc = ini.SaveFile(a_pszFile); +if (rc < 0) return false; +``` diff --git a/src/openalpr/simpleini/ini.syn b/src/openalpr/simpleini/ini.syn new file mode 100644 index 0000000..b9c9d01 --- /dev/null +++ b/src/openalpr/simpleini/ini.syn @@ -0,0 +1,36 @@ +; Syntax file for ini files - contributed by Brodie Thiesfield +; +; Suggested Colors: +; Comments (;#) Comments, Comments 2 Green +; Sections Characters Red +; Values Strings Blue + +C=1 + +[Syntax] +Namespace1 = 6 +IgnoreCase = Yes +KeyWordLength = 1 +BracketChars = +OperatorChars = +PreprocStart = +SyntaxStart = +SyntaxEnd = +HexPrefix = +CommentStart = +CommentEnd = +CommentStartAlt = +CommentEndAlt = +SingleComment = # +SingleCommentCol = +SingleCommentAlt = ; +SingleCommentColAlt = +SingleCommentEsc = +StringsSpanLines = No +StringStart = +StringEnd = +StringAlt = = +StringEsc = +CharStart = [ +CharEnd = ] +CharEsc = diff --git a/src/openalpr/simpleini/simpleini.doxy b/src/openalpr/simpleini/simpleini.doxy new file mode 100644 index 0000000..b9291e7 --- /dev/null +++ b/src/openalpr/simpleini/simpleini.doxy @@ -0,0 +1,1321 @@ +# Doxyfile 1.5.4 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project +# +# All text after a hash (#) is considered a comment and will be ignored +# The format is: +# TAG = value [value, ...] +# For lists items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (" ") + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file that +# follow. The default is UTF-8 which is also the encoding used for all text before +# the first occurrence of this tag. Doxygen uses libiconv (or the iconv built into +# libc) for the transcoding. See http://www.gnu.org/software/libiconv for the list of +# possible encodings. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded +# by quotes) that should identify the project. + +PROJECT_NAME = SimpleIni + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. +# This could be handy for archiving the generated documentation or +# if some version control system is used. + +PROJECT_NUMBER = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) +# base path where the generated documentation will be put. +# If a relative path is entered, it will be relative to the location +# where doxygen was started. If left blank the current directory will be used. + +OUTPUT_DIRECTORY = D:/src/simpleini-doc + +# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create +# 4096 sub-directories (in 2 levels) under the output directory of each output +# format and will distribute the generated files over these directories. +# Enabling this option can be useful when feeding doxygen a huge amount of +# source files, where putting all generated files in the same directory would +# otherwise cause performance problems for the file system. + +CREATE_SUBDIRS = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# The default language is English, other supported languages are: +# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, +# Croatian, Czech, Danish, Dutch, Finnish, French, German, Greek, Hungarian, +# Italian, Japanese, Japanese-en (Japanese with English messages), Korean, +# Korean-en, Lithuanian, Norwegian, Polish, Portuguese, Romanian, Russian, +# Serbian, Slovak, Slovene, Spanish, Swedish, and Ukrainian. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will +# include brief member descriptions after the members that are listed in +# the file and class documentation (similar to JavaDoc). +# Set to NO to disable this. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend +# the brief description of a member or function before the detailed description. +# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator +# that is used to form the text in various listings. Each string +# in this list, if found as the leading text of the brief description, will be +# stripped from the text and the result after processing the whole list, is +# used as the annotated text. Otherwise, the brief description is used as-is. +# If left blank, the following values are used ("$name" is automatically +# replaced with the name of the entity): "The $name class" "The $name widget" +# "The $name file" "is" "provides" "specifies" "contains" +# "represents" "a" "an" "the" + +ABBREVIATE_BRIEF = "The $name class " \ + "The $name widget " \ + "The $name file " \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full +# path before files name in the file list and in the header files. If set +# to NO the shortest path that makes the file name unique will be used. + +FULL_PATH_NAMES = YES + +# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag +# can be used to strip a user-defined part of the path. Stripping is +# only done if one of the specified strings matches the left-hand part of +# the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the +# path to strip. + +STRIP_FROM_PATH = "D:/src/simpleini/ " + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of +# the path mentioned in the documentation of a class, which tells +# the reader which header file to include in order to use a class. +# If left blank only the name of the header file containing the class +# definition is used. Otherwise one should specify the include paths that +# are normally passed to the compiler using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter +# (but less readable) file names. This can be useful is your file systems +# doesn't support long names like on DOS, Mac, or CD-ROM. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen +# will interpret the first line (until the first dot) of a JavaDoc-style +# comment as the brief description. If set to NO, the JavaDoc +# comments will behave just like regular Qt-style comments +# (thus requiring an explicit @brief command for a brief description.) + +JAVADOC_AUTOBRIEF = YES + +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will +# interpret the first line (until the first dot) of a Qt-style +# comment as the brief description. If set to NO, the comments +# will behave just like regular Qt-style comments (thus requiring +# an explicit \brief command for a brief description.) + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen +# treat a multi-line C++ special comment block (i.e. a block of //! or /// +# comments) as a brief description. This used to be the default behaviour. +# The new default is to treat a multi-line C++ comment block as a detailed +# description. Set this tag to YES if you prefer the old behaviour instead. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the DETAILS_AT_TOP tag is set to YES then Doxygen +# will output the detailed description near the top, like JavaDoc. +# If set to NO, the detailed description appears after the member +# documentation. + +DETAILS_AT_TOP = NO + +# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented +# member inherits the documentation from any documented member that it +# re-implements. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce +# a new page for each member. If set to NO, the documentation of a member will +# be part of the file/class/namespace that contains it. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. +# Doxygen uses this value to replace tabs by spaces in code fragments. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that acts +# as commands in the documentation. An alias has the form "name=value". +# For example adding "sideeffect=\par Side Effects:\n" will allow you to +# put the command \sideeffect (or @sideeffect) in the documentation, which +# will result in a user-defined paragraph with heading "Side Effects:". +# You can put \n's in the value part of an alias to insert newlines. + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C +# sources only. Doxygen will then generate output that is more tailored for C. +# For instance, some of the names that are used will be different. The list +# of all members will be omitted, etc. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java +# sources only. Doxygen will then generate output that is more tailored for Java. +# For instance, namespaces will be presented as packages, qualified scopes +# will look different, etc. + +OPTIMIZE_OUTPUT_JAVA = NO + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want to +# include (a tag file for) the STL sources as input, then you should +# set this tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. +# func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. +# Doxygen will parse them like normal C++ but will assume all classes use public +# instead of private inheritance when no explicit protection keyword is present. + +SIP_SUPPORT = NO + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES (the default) to allow class member groups of +# the same type (for instance a group of public functions) to be put as a +# subgroup of that type (e.g. under the Public Functions section). Set it to +# NO to prevent subgrouping. Alternatively, this can be done per class using +# the \nosubgrouping command. + +SUBGROUPING = YES + +# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct (or union) is +# documented as struct with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically +# be useful for C code where the coding convention is that all structs are +# typedef'ed and only the typedef is referenced never the struct's name. + +TYPEDEF_HIDES_STRUCT = NO + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. +# Private class members and static file members will be hidden unless +# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class +# will be included in the documentation. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_STATIC tag is set to YES all static members of a file +# will be included in the documentation. + +EXTRACT_STATIC = YES + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) +# defined locally in source files will be included in the documentation. +# If set to NO only classes defined in header files are included. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. When set to YES local +# methods, which are defined in the implementation section but not in +# the interface are included in the documentation. +# If set to NO (the default) only methods in the interface are included. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be extracted +# and appear in the documentation as a namespace called 'anonymous_namespace{file}', +# where file will be replaced with the base name of the file that contains the anonymous +# namespace. By default anonymous namespace are hidden. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members of documented classes, files or namespaces. +# If set to NO (the default) these members will be included in the +# various overviews, but no documentation section is generated. +# This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. +# If set to NO (the default) these classes will be included in the various +# overviews. This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all +# friend (class|struct|union) declarations. +# If set to NO (the default) these declarations will be included in the +# documentation. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. +# If set to NO (the default) these blocks will be appended to the +# function's detailed documentation block. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation +# that is typed after a \internal command is included. If the tag is set +# to NO (the default) then the documentation will be excluded. +# Set it to YES to include the internal documentation. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate +# file names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen +# will show members with their full class and namespace scopes in the +# documentation. If set to YES the scope will be hidden. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen +# will put a list of the files that are included by a file in the documentation +# of that file. + +SHOW_INCLUDE_FILES = YES + +# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] +# is inserted in the documentation for inline members. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen +# will sort the (detailed) documentation of file and class members +# alphabetically by member name. If set to NO the members will appear in +# declaration order. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the +# brief documentation of file, namespace and class members alphabetically +# by member name. If set to NO (the default) the members will appear in +# declaration order. + +SORT_BRIEF_DOCS = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be +# sorted by fully-qualified names, including namespaces. If set to +# NO (the default), the class list will be sorted only by class name, +# not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the +# alphabetical list. + +SORT_BY_SCOPE_NAME = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or +# disable (NO) the todo list. This list is created by putting \todo +# commands in the documentation. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or +# disable (NO) the test list. This list is created by putting \test +# commands in the documentation. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or +# disable (NO) the bug list. This list is created by putting \bug +# commands in the documentation. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or +# disable (NO) the deprecated list. This list is created by putting +# \deprecated commands in the documentation. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional +# documentation sections, marked by \if sectionname ... \endif. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines +# the initial value of a variable or define consists of for it to appear in +# the documentation. If the initializer consists of more lines than specified +# here it will be hidden. Use a value of 0 to hide initializers completely. +# The appearance of the initializer of individual variables and defines in the +# documentation can be controlled using \showinitializer or \hideinitializer +# command in the documentation regardless of this setting. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated +# at the bottom of the documentation of classes and structs. If set to YES the +# list will mention the files that were used to generate the documentation. + +SHOW_USED_FILES = YES + +# If the sources in your project are distributed over multiple directories +# then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy +# in the documentation. The default is NO. + +SHOW_DIRECTORIES = NO + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from the +# version control system). Doxygen will invoke the program by executing (via +# popen()) the command , where is the value of +# the FILE_VERSION_FILTER tag, and is the name of an input file +# provided by doxygen. Whatever the program writes to standard output +# is used as the file version. See the manual for examples. + +FILE_VERSION_FILTER = + +#--------------------------------------------------------------------------- +# configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated +# by doxygen. Possible values are YES and NO. If left blank NO is used. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated by doxygen. Possible values are YES and NO. If left blank +# NO is used. + +WARNINGS = YES + +# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings +# for undocumented members. If EXTRACT_ALL is set to YES then this flag will +# automatically be disabled. + +WARN_IF_UNDOCUMENTED = YES + +# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some +# parameters in a documented function, or documenting parameters that +# don't exist or using markup commands wrongly. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be abled to get warnings for +# functions that are documented, but have no documentation for their parameters +# or return value. If set to NO (the default) doxygen will only warn about +# wrong or incomplete parameter documentation, but not about the absence of +# documentation. + +WARN_NO_PARAMDOC = YES + +# The WARN_FORMAT tag determines the format of the warning messages that +# doxygen can produce. The string should contain the $file, $line, and $text +# tags, which will be replaced by the file and line number from which the +# warning originated and the warning text. Optionally the format may contain +# $version, which will be replaced by the version of the file (if it could +# be obtained via FILE_VERSION_FILTER) + +WARN_FORMAT = "$file($line) : $text " + +# The WARN_LOGFILE tag can be used to specify a file to which warning +# and error messages should be written. If left blank the output is written +# to stderr. + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag can be used to specify the files and/or directories that contain +# documented source files. You may enter file names like "myfile.cpp" or +# directories like "/usr/src/myproject". Separate the files or directories +# with spaces. + +INPUT = D:/src/simpleini/SimpleIni.h + +# This tag can be used to specify the character encoding of the source files that +# doxygen parses. Internally doxygen uses the UTF-8 encoding, which is also the default +# input encoding. Doxygen uses libiconv (or the iconv built into libc) for the transcoding. +# See http://www.gnu.org/software/libiconv for the list of possible encodings. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank the following patterns are tested: +# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx +# *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.py *.f90 + +FILE_PATTERNS = *.h + +# The RECURSIVE tag can be used to turn specify whether or not subdirectories +# should be searched for input files as well. Possible values are YES and NO. +# If left blank NO is used. + +RECURSIVE = NO + +# The EXCLUDE tag can be used to specify files and/or directories that should +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used select whether or not files or +# directories that are symbolic links (a Unix filesystem feature) are excluded +# from the input. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. Note that the wildcards are matched +# against the file with absolute path, so to exclude all test directories +# for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the output. +# The symbol name can be a fully qualified name, a word, or if the wildcard * is used, +# a substring. Examples: ANamespace, AClass, AClass::ANamespace, ANamespace::*Test + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or +# directories that contain example code fragments that are included (see +# the \include command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank all files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude +# commands irrespective of the value of the RECURSIVE tag. +# Possible values are YES and NO. If left blank NO is used. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or +# directories that contain image that are included in the documentation (see +# the \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command , where +# is the value of the INPUT_FILTER tag, and is the name of an +# input file. Doxygen will then use the output that the filter program writes +# to standard output. If FILTER_PATTERNS is specified, this tag will be +# ignored. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: +# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further +# info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER +# is applied to all files. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will be used to filter the input files when producing source +# files to browse (i.e. when SOURCE_BROWSER is set to YES). + +FILTER_SOURCE_FILES = NO + +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will +# be generated. Documented entities will be cross-referenced with these sources. +# Note: To get rid of all source code in the generated output, make sure also +# VERBATIM_HEADERS is set to NO. If you have enabled CALL_GRAPH or CALLER_GRAPH +# then you must also enable this option. If you don't then doxygen will produce +# a warning and turn it on anyway + +SOURCE_BROWSER = YES + +# Setting the INLINE_SOURCES tag to YES will include the body +# of functions and classes directly in the documentation. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct +# doxygen to hide any special comment blocks from generated source code +# fragments. Normal C and C++ comments will always remain visible. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES (the default) +# then for each documented function all documented +# functions referencing it will be listed. + +REFERENCED_BY_RELATION = YES + +# If the REFERENCES_RELATION tag is set to YES (the default) +# then for each documented function all documented entities +# called/used by that function will be listed. + +REFERENCES_RELATION = YES + +# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) +# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from +# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will +# link to the source code. Otherwise they will link to the documentstion. + +REFERENCES_LINK_SOURCE = YES + +# If the USE_HTAGS tag is set to YES then the references to source code +# will point to the HTML generated by the htags(1) tool instead of doxygen +# built-in source browser. The htags tool is part of GNU's global source +# tagging system (see http://www.gnu.org/software/global/global.html). You +# will need version 4.8.6 or higher. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen +# will generate a verbatim copy of the header file for each class for +# which an include is specified. Set to NO to disable this. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index +# of all compounds will be generated. Enable this if the project +# contains a lot of classes, structs, unions or interfaces. + +ALPHABETICAL_INDEX = NO + +# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then +# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns +# in which this list will be split (can be a number in the range [1..20]) + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all +# classes will be put under the same header in the alphabetical index. +# The IGNORE_PREFIX tag can be used to specify one or more prefixes that +# should be ignored while generating the index headers. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES (the default) Doxygen will +# generate HTML output. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `html' will be used as the default path. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for +# each generated HTML page (for example: .htm,.php,.asp). If it is left blank +# doxygen will generate files with .html extension. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a personal HTML header for +# each generated HTML page. If it is left blank doxygen will generate a +# standard header. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a personal HTML footer for +# each generated HTML page. If it is left blank doxygen will generate a +# standard footer. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading +# style sheet that is used by each HTML page. It can be used to +# fine-tune the look of the HTML output. If the tag is left blank doxygen +# will generate a default style sheet. Note that doxygen will try to copy +# the style sheet file to the HTML output directory, so don't put your own +# stylesheet in the HTML output directory as well, or it will be erased! + +HTML_STYLESHEET = + +# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, +# files or namespaces will be aligned in HTML using tables. If set to +# NO a bullet list will be used. + +HTML_ALIGN_MEMBERS = YES + +# If the GENERATE_HTMLHELP tag is set to YES, additional index files +# will be generated that can be used as input for tools like the +# Microsoft HTML help workshop to generate a compressed HTML help file (.chm) +# of the generated HTML documentation. + +GENERATE_HTMLHELP = NO + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. For this to work a browser that supports +# JavaScript and DHTML is required (for instance Mozilla 1.0+, Firefox +# Netscape 6.0+, Internet explorer 5.0+, Konqueror, or Safari). + +HTML_DYNAMIC_SECTIONS = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can +# be used to specify the file name of the resulting .chm file. You +# can add a path in front of the file if the result should not be +# written to the html output directory. + +CHM_FILE = + +# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can +# be used to specify the location (absolute path including file name) of +# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run +# the HTML help compiler on the generated index.hhp. + +HHC_LOCATION = + +# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag +# controls if a separate .chi index file is generated (YES) or that +# it should be included in the master .chm file (NO). + +GENERATE_CHI = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag +# controls whether a binary table of contents is generated (YES) or a +# normal table of contents (NO) in the .chm file. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members +# to the contents of the HTML help documentation and to the tree view. + +TOC_EXPAND = NO + +# The DISABLE_INDEX tag can be used to turn on/off the condensed index at +# top of each HTML page. The value NO (the default) enables the index and +# the value YES disables it. + +DISABLE_INDEX = NO + +# This tag can be used to set the number of enum values (range [1..20]) +# that doxygen will group on one line in the generated HTML documentation. + +ENUM_VALUES_PER_LINE = 4 + +# If the GENERATE_TREEVIEW tag is set to YES, a side panel will be +# generated containing a tree-like index structure (just like the one that +# is generated for HTML Help). For this to work a browser that supports +# JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+, +# Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are +# probably better off using the HTML help feature. + +GENERATE_TREEVIEW = NO + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be +# used to set the initial width (in pixels) of the frame in which the tree +# is shown. + +TREEVIEW_WIDTH = 250 + +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will +# generate Latex output. + +GENERATE_LATEX = NO + +# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `latex' will be used as the default path. + +LATEX_OUTPUT = latex + +# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be +# invoked. If left blank `latex' will be used as the default command name. + +LATEX_CMD_NAME = latex + +# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to +# generate index for LaTeX. If left blank `makeindex' will be used as the +# default command name. + +MAKEINDEX_CMD_NAME = makeindex + +# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact +# LaTeX documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_LATEX = NO + +# The PAPER_TYPE tag can be used to set the paper type that is used +# by the printer. Possible values are: a4, a4wide, letter, legal and +# executive. If left blank a4wide will be used. + +PAPER_TYPE = a4wide + +# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX +# packages that should be included in the LaTeX output. + +EXTRA_PACKAGES = + +# The LATEX_HEADER tag can be used to specify a personal LaTeX header for +# the generated latex document. The header should contain everything until +# the first chapter. If it is left blank doxygen will generate a +# standard header. Notice: only use this tag if you know what you are doing! + +LATEX_HEADER = + +# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated +# is prepared for conversion to pdf (using ps2pdf). The pdf file will +# contain links (just like the HTML output) instead of page references +# This makes the output suitable for online browsing using a pdf viewer. + +PDF_HYPERLINKS = NO + +# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of +# plain latex in the generated Makefile. Set this option to YES to get a +# higher quality PDF documentation. + +USE_PDFLATEX = NO + +# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. +# command to the generated LaTeX files. This will instruct LaTeX to keep +# running if errors occur, instead of asking the user for help. +# This option is also used when generating formulas in HTML. + +LATEX_BATCHMODE = NO + +# If LATEX_HIDE_INDICES is set to YES then doxygen will not +# include the index chapters (such as File Index, Compound Index, etc.) +# in the output. + +LATEX_HIDE_INDICES = NO + +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- + +# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output +# The RTF output is optimized for Word 97 and may not look very pretty with +# other RTF readers or editors. + +GENERATE_RTF = NO + +# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `rtf' will be used as the default path. + +RTF_OUTPUT = rtf + +# If the COMPACT_RTF tag is set to YES Doxygen generates more compact +# RTF documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_RTF = NO + +# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated +# will contain hyperlink fields. The RTF file will +# contain links (just like the HTML output) instead of page references. +# This makes the output suitable for online browsing using WORD or other +# programs which support those fields. +# Note: wordpad (write) and others do not support links. + +RTF_HYPERLINKS = NO + +# Load stylesheet definitions from file. Syntax is similar to doxygen's +# config file, i.e. a series of assignments. You only have to provide +# replacements, missing definitions are set to their default value. + +RTF_STYLESHEET_FILE = + +# Set optional variables used in the generation of an rtf document. +# Syntax is similar to doxygen's config file. + +RTF_EXTENSIONS_FILE = + +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- + +# If the GENERATE_MAN tag is set to YES (the default) Doxygen will +# generate man pages + +GENERATE_MAN = NO + +# The MAN_OUTPUT tag is used to specify where the man pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `man' will be used as the default path. + +MAN_OUTPUT = man + +# The MAN_EXTENSION tag determines the extension that is added to +# the generated man pages (default is the subroutine's section .3) + +MAN_EXTENSION = .3 + +# If the MAN_LINKS tag is set to YES and Doxygen generates man output, +# then it will generate one additional man file for each entity +# documented in the real man page(s). These additional files +# only source the real man page, but without them the man command +# would be unable to find the correct page. The default is NO. + +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- + +# If the GENERATE_XML tag is set to YES Doxygen will +# generate an XML file that captures the structure of +# the code including all documentation. + +GENERATE_XML = NO + +# The XML_OUTPUT tag is used to specify where the XML pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `xml' will be used as the default path. + +XML_OUTPUT = xml + +# The XML_SCHEMA tag can be used to specify an XML schema, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_SCHEMA = + +# The XML_DTD tag can be used to specify an XML DTD, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_DTD = + +# If the XML_PROGRAMLISTING tag is set to YES Doxygen will +# dump the program listings (including syntax highlighting +# and cross-referencing information) to the XML output. Note that +# enabling this will significantly increase the size of the XML output. + +XML_PROGRAMLISTING = YES + +#--------------------------------------------------------------------------- +# configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will +# generate an AutoGen Definitions (see autogen.sf.net) file +# that captures the structure of the code including all +# documentation. Note that this feature is still experimental +# and incomplete at the moment. + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +# If the GENERATE_PERLMOD tag is set to YES Doxygen will +# generate a Perl module file that captures the structure of +# the code including all documentation. Note that this +# feature is still experimental and incomplete at the +# moment. + +GENERATE_PERLMOD = NO + +# If the PERLMOD_LATEX tag is set to YES Doxygen will generate +# the necessary Makefile rules, Perl scripts and LaTeX code to be able +# to generate PDF and DVI output from the Perl module output. + +PERLMOD_LATEX = NO + +# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be +# nicely formatted so it can be parsed by a human reader. This is useful +# if you want to understand what is going on. On the other hand, if this +# tag is set to NO the size of the Perl module output will be much smaller +# and Perl will parse it just the same. + +PERLMOD_PRETTY = YES + +# The names of the make variables in the generated doxyrules.make file +# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. +# This is useful so different doxyrules.make files included by the same +# Makefile don't overwrite each other's variables. + +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will +# evaluate all C-preprocessor directives found in the sources and include +# files. + +ENABLE_PREPROCESSING = YES + +# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro +# names in the source code. If set to NO (the default) only conditional +# compilation will be performed. Macro expansion can be done in a controlled +# way by setting EXPAND_ONLY_PREDEF to YES. + +MACRO_EXPANSION = NO + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES +# then the macro expansion is limited to the macros specified with the +# PREDEFINED and EXPAND_AS_DEFINED tags. + +EXPAND_ONLY_PREDEF = NO + +# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files +# in the INCLUDE_PATH (see below) will be search if a #include is found. + +SEARCH_INCLUDES = YES + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by +# the preprocessor. + +INCLUDE_PATH = + +# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard +# patterns (like *.h and *.hpp) to filter out the header-files in the +# directories. If left blank, the patterns specified with FILE_PATTERNS will +# be used. + +INCLUDE_FILE_PATTERNS = + +# The PREDEFINED tag can be used to specify one or more macro names that +# are defined before the preprocessor is started (similar to the -D option of +# gcc). The argument of the tag is a list of macros of the form: name +# or name=definition (no spaces). If the definition and the = are +# omitted =1 is assumed. To prevent a macro definition from being +# undefined via #undef or recursively expanded use the := operator +# instead of the = operator. + +PREDEFINED = SI_HAS_WIDE_FILE \ + SI_SUPPORT_IOSTREAMS + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then +# this tag can be used to specify a list of macro names that should be expanded. +# The macro definition that is found in the sources will be used. +# Use the PREDEFINED tag if you want to use a different macro definition. + +EXPAND_AS_DEFINED = + +# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then +# doxygen's preprocessor will remove all function-like macros that are alone +# on a line, have an all uppercase name, and do not end with a semicolon. Such +# function macros are typically used for boiler-plate code, and will confuse +# the parser if not removed. + +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration::additions related to external references +#--------------------------------------------------------------------------- + +# The TAGFILES option can be used to specify one or more tagfiles. +# Optionally an initial location of the external documentation +# can be added for each tagfile. The format of a tag file without +# this location is as follows: +# TAGFILES = file1 file2 ... +# Adding location for the tag files is done as follows: +# TAGFILES = file1=loc1 "file2 = loc2" ... +# where "loc1" and "loc2" can be relative or absolute paths or +# URLs. If a location is present for each tag, the installdox tool +# does not have to be run to correct the links. +# Note that each tag file must have a unique name +# (where the name does NOT include the path) +# If a tag file is not located in the directory in which doxygen +# is run, you must also specify the path to the tagfile here. + +TAGFILES = + +# When a file name is specified after GENERATE_TAGFILE, doxygen will create +# a tag file that is based on the input files it reads. + +GENERATE_TAGFILE = + +# If the ALLEXTERNALS tag is set to YES all external classes will be listed +# in the class index. If set to NO only the inherited external classes +# will be listed. + +ALLEXTERNALS = NO + +# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will +# be listed. + +EXTERNAL_GROUPS = YES + +# The PERL_PATH should be the absolute path and name of the perl script +# interpreter (i.e. the result of `which perl'). + +PERL_PATH = /usr/bin/perl + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will +# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base +# or super classes. Setting the tag to NO turns the diagrams off. Note that +# this option is superseded by the HAVE_DOT option below. This is only a +# fallback. It is recommended to install and use dot, since it yields more +# powerful graphs. + +CLASS_DIAGRAMS = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. Doxygen will then run the mscgen tool (see http://www.mcternan.me.uk/mscgen/) to +# produce the chart and insert it in the documentation. The MSCGEN_PATH tag allows you to +# specify the directory where the mscgen tool resides. If left empty the tool is assumed to +# be found in the default search path. + +MSCGEN_PATH = + +# If set to YES, the inheritance and collaboration graphs will hide +# inheritance and usage relations if the target is undocumented +# or is not a class. + +HIDE_UNDOC_RELATIONS = YES + +# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# available from the path. This tool is part of Graphviz, a graph visualization +# toolkit from AT&T and Lucent Bell Labs. The other options in this section +# have no effect if this option is set to NO (the default) + +HAVE_DOT = NO + +# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect inheritance relations. Setting this tag to YES will force the +# the CLASS_DIAGRAMS tag to NO. + +CLASS_GRAPH = YES + +# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect implementation dependencies (inheritance, containment, and +# class references variables) of the class with other documented classes. + +COLLABORATION_GRAPH = YES + +# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for groups, showing the direct groups dependencies + +GROUP_GRAPHS = YES + +# If the UML_LOOK tag is set to YES doxygen will generate inheritance and +# collaboration diagrams in a style similar to the OMG's Unified Modeling +# Language. + +UML_LOOK = NO + +# If set to YES, the inheritance and collaboration graphs will show the +# relations between templates and their instances. + +TEMPLATE_RELATIONS = NO + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT +# tags are set to YES then doxygen will generate a graph for each documented +# file showing the direct and indirect include dependencies of the file with +# other documented files. + +INCLUDE_GRAPH = YES + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and +# HAVE_DOT tags are set to YES then doxygen will generate a graph for each +# documented header file showing the documented files that directly or +# indirectly include this file. + +INCLUDED_BY_GRAPH = YES + +# If the CALL_GRAPH, SOURCE_BROWSER and HAVE_DOT tags are set to YES then doxygen will +# generate a call dependency graph for every global function or class method. +# Note that enabling this option will significantly increase the time of a run. +# So in most cases it will be better to enable call graphs for selected +# functions only using the \callgraph command. + +CALL_GRAPH = NO + +# If the CALLER_GRAPH, SOURCE_BROWSER and HAVE_DOT tags are set to YES then doxygen will +# generate a caller dependency graph for every global function or class method. +# Note that enabling this option will significantly increase the time of a run. +# So in most cases it will be better to enable caller graphs for selected +# functions only using the \callergraph command. + +CALLER_GRAPH = NO + +# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen +# will graphical hierarchy of all classes instead of a textual one. + +GRAPHICAL_HIERARCHY = YES + +# If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES +# then doxygen will show the dependencies a directory has on other directories +# in a graphical way. The dependency relations are determined by the #include +# relations between the files in the directories. + +DIRECTORY_GRAPH = YES + +# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images +# generated by dot. Possible values are png, jpg, or gif +# If left blank png will be used. + +DOT_IMAGE_FORMAT = png + +# The tag DOT_PATH can be used to specify the path where the dot tool can be +# found. If left blank, it is assumed the dot tool can be found in the path. + +DOT_PATH = + +# The DOTFILE_DIRS tag can be used to specify one or more directories that +# contain dot files that are included in the documentation (see the +# \dotfile command). + +DOTFILE_DIRS = + +# The MAX_DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of +# nodes that will be shown in the graph. If the number of nodes in a graph +# becomes larger than this value, doxygen will truncate the graph, which is +# visualized by representing a node as a red box. Note that doxygen if the number +# of direct children of the root node in a graph is already larger than +# MAX_DOT_GRAPH_NOTES then the graph will not be shown at all. Also note +# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. + +DOT_GRAPH_MAX_NODES = 50 + +# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the +# graphs generated by dot. A depth value of 3 means that only nodes reachable +# from the root by following a path via at most 3 edges will be shown. Nodes +# that lay further from the root node will be omitted. Note that setting this +# option to 1 or 2 may greatly reduce the computation time needed for large +# code bases. Also note that the size of a graph can be further restricted by +# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. + +MAX_DOT_GRAPH_DEPTH = 1000 + +# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent +# background. This is disabled by default, which results in a white background. +# Warning: Depending on the platform used, enabling this option may lead to +# badly anti-aliased labels on the edges of a graph (i.e. they become hard to +# read). + +DOT_TRANSPARENT = NO + +# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output +# files in one run (i.e. multiple -o and -T options on the command line). This +# makes dot run faster, but since only newer versions of dot (>1.8.10) +# support this, this feature is disabled by default. + +DOT_MULTI_TARGETS = NO + +# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will +# generate a legend page explaining the meaning of the various boxes and +# arrows in the dot generated graphs. + +GENERATE_LEGEND = YES + +# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will +# remove the intermediate dot files that are used to generate +# the various graphs. + +DOT_CLEANUP = YES + +#--------------------------------------------------------------------------- +# Configuration::additions related to the search engine +#--------------------------------------------------------------------------- + +# The SEARCHENGINE tag specifies whether or not a search engine should be +# used. If set to NO the values of all tags below this one will be ignored. + +SEARCHENGINE = NO diff --git a/src/openalpr/simpleini/simpleini.h b/src/openalpr/simpleini/simpleini.h new file mode 100644 index 0000000..5db9a76 --- /dev/null +++ b/src/openalpr/simpleini/simpleini.h @@ -0,0 +1,3385 @@ +/** @mainpage + + +
Library SimpleIni +
File SimpleIni.h +
Author Brodie Thiesfield [code at jellycan dot com] +
Source https://github.com/brofield/simpleini +
Version 4.17 +
+ + Jump to the @link CSimpleIniTempl CSimpleIni @endlink interface documentation. + + @section intro INTRODUCTION + + This component allows an INI-style configuration file to be used on both + Windows and Linux/Unix. It is fast, simple and source code using this + component will compile unchanged on either OS. + + + @section features FEATURES + + - MIT Licence allows free use in all software (including GPL and commercial) + - multi-platform (Windows 95/98/ME/NT/2K/XP/2003, Windows CE, Linux, Unix) + - loading and saving of INI-style configuration files + - configuration files can have any newline format on all platforms + - liberal acceptance of file format + - key/values with no section + - removal of whitespace around sections, keys and values + - support for multi-line values (values with embedded newline characters) + - optional support for multiple keys with the same name + - optional case-insensitive sections and keys (for ASCII characters only) + - saves files with sections and keys in the same order as they were loaded + - preserves comments on the file, section and keys where possible. + - supports both char or wchar_t programming interfaces + - supports both MBCS (system locale) and UTF-8 file encodings + - system locale does not need to be UTF-8 on Linux/Unix to load UTF-8 file + - support for non-ASCII characters in section, keys, values and comments + - support for non-standard character types or file encodings + via user-written converter classes + - support for adding/modifying values programmatically + - compiles cleanly in the following compilers: + - Windows/VC6 (warning level 3) + - Windows/VC.NET 2003 (warning level 4) + - Windows/VC 2005 (warning level 4) + - Linux/gcc (-Wall) + + + @section usage USAGE SUMMARY + + -# Define the appropriate symbol for the converter you wish to use and + include the SimpleIni.h header file. If no specific converter is defined + then the default converter is used. The default conversion mode uses + SI_CONVERT_WIN32 on Windows and SI_CONVERT_GENERIC on all other + platforms. If you are using ICU then SI_CONVERT_ICU is supported on all + platforms. + -# Declare an instance the appropriate class. Note that the following + definitions are just shortcuts for commonly used types. Other types + (PRUnichar, unsigned short, unsigned char) are also possible. + +
Interface Case-sensitive Load UTF-8 Load MBCS Typedef +
SI_CONVERT_GENERIC +
char No Yes Yes #1 CSimpleIniA +
char Yes Yes Yes CSimpleIniCaseA +
wchar_t No Yes Yes CSimpleIniW +
wchar_t Yes Yes Yes CSimpleIniCaseW +
SI_CONVERT_WIN32 +
char No No #2 Yes CSimpleIniA +
char Yes Yes Yes CSimpleIniCaseA +
wchar_t No Yes Yes CSimpleIniW +
wchar_t Yes Yes Yes CSimpleIniCaseW +
SI_CONVERT_ICU +
char No Yes Yes CSimpleIniA +
char Yes Yes Yes CSimpleIniCaseA +
UChar No Yes Yes CSimpleIniW +
UChar Yes Yes Yes CSimpleIniCaseW +
+ #1 On Windows you are better to use CSimpleIniA with SI_CONVERT_WIN32.
+ #2 Only affects Windows. On Windows this uses MBCS functions and + so may fold case incorrectly leading to uncertain results. + -# Call LoadData() or LoadFile() to load and parse the INI configuration file + -# Access and modify the data of the file using the following functions + +
GetAllSections Return all section names +
GetAllKeys Return all key names within a section +
GetAllValues Return all values within a section & key +
GetSection Return all key names and values in a section +
GetSectionSize Return the number of keys in a section +
GetValue Return a value for a section & key +
SetValue Add or update a value for a section & key +
Delete Remove a section, or a key from a section +
+ -# Call Save() or SaveFile() to save the INI configuration data + + @section iostreams IO STREAMS + + SimpleIni supports reading from and writing to STL IO streams. Enable this + by defining SI_SUPPORT_IOSTREAMS before including the SimpleIni.h header + file. Ensure that if the streams are backed by a file (e.g. ifstream or + ofstream) then the flag ios_base::binary has been used when the file was + opened. + + @section multiline MULTI-LINE VALUES + + Values that span multiple lines are created using the following format. + +
+        key = <<
+
+    Note the following:
+    - The text used for ENDTAG can be anything and is used to find
+      where the multi-line text ends.
+    - The newline after ENDTAG in the start tag, and the newline
+      before ENDTAG in the end tag is not included in the data value.
+    - The ending tag must be on it's own line with no whitespace before
+      or after it.
+    - The multi-line value is modified at load so that each line in the value
+      is delimited by a single '\\n' character on all platforms. At save time
+      it will be converted into the newline format used by the current
+      platform.
+
+    @section comments COMMENTS
+
+    Comments are preserved in the file within the following restrictions:
+    - Every file may have a single "file comment". It must start with the
+      first character in the file, and will end with the first non-comment
+      line in the file.
+    - Every section may have a single "section comment". It will start
+      with the first comment line following the file comment, or the last
+      data entry. It ends at the beginning of the section.
+    - Every key may have a single "key comment". This comment will start
+      with the first comment line following the section start, or the file
+      comment if there is no section name.
+    - Comments are set at the time that the file, section or key is first
+      created. The only way to modify a comment on a section or a key is to
+      delete that entry and recreate it with the new comment. There is no
+      way to change the file comment.
+
+    @section save SAVE ORDER
+
+    The sections and keys are written out in the same order as they were
+    read in from the file. Sections and keys added to the data after the
+    file has been loaded will be added to the end of the file when it is
+    written. There is no way to specify the location of a section or key
+    other than in first-created, first-saved order.
+
+    @section notes NOTES
+
+    - To load UTF-8 data on Windows 95, you need to use Microsoft Layer for
+      Unicode, or SI_CONVERT_GENERIC, or SI_CONVERT_ICU.
+    - When using SI_CONVERT_GENERIC, ConvertUTF.c must be compiled and linked.
+    - When using SI_CONVERT_ICU, ICU header files must be on the include
+      path and icuuc.lib must be linked in.
+    - To load a UTF-8 file on Windows AND expose it with SI_CHAR == char,
+      you should use SI_CONVERT_GENERIC.
+    - The collation (sorting) order used for sections and keys returned from
+      iterators is NOT DEFINED. If collation order of the text is important
+      then it should be done yourself by either supplying a replacement
+      SI_STRLESS class, or by sorting the strings external to this library.
+    - Usage of the  header on Windows can be disabled by defining
+      SI_NO_MBCS. This is defined automatically on Windows CE platforms.
+
+    @section contrib CONTRIBUTIONS
+    
+    - 2010/05/03: Tobias Gehrig: added GetDoubleValue()
+
+    @section licence MIT LICENCE
+
+    The licence text below is the boilerplate "MIT Licence" used from:
+    http://www.opensource.org/licenses/mit-license.php
+
+    Copyright (c) 2006-2012, Brodie Thiesfield
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is furnished
+    to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+    FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+    COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+    IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+    CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+#ifndef INCLUDED_SimpleIni_h
+#define INCLUDED_SimpleIni_h
+
+#if defined(_MSC_VER) && (_MSC_VER >= 1020)
+# pragma once
+#endif
+
+// Disable these warnings in MSVC:
+//  4127 "conditional expression is constant" as the conversion classes trigger
+//  it with the statement if (sizeof(SI_CHAR) == sizeof(char)). This test will
+//  be optimized away in a release build.
+//  4503 'insert' : decorated name length exceeded, name was truncated
+//  4702 "unreachable code" as the MS STL header causes it in release mode.
+//  Again, the code causing the warning will be cleaned up by the compiler.
+//  4786 "identifier truncated to 256 characters" as this is thrown hundreds
+//  of times VC6 as soon as STL is used.
+#ifdef _MSC_VER
+# pragma warning (push)
+# pragma warning (disable: 4127 4503 4702 4786)
+#endif
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#ifdef SI_SUPPORT_IOSTREAMS
+# include 
+#endif // SI_SUPPORT_IOSTREAMS
+
+#ifdef _DEBUG
+# ifndef assert
+#  include 
+# endif
+# define SI_ASSERT(x)   assert(x)
+#else
+# define SI_ASSERT(x)
+#endif
+
+enum SI_Error {
+    SI_OK       =  0,   //!< No error
+    SI_UPDATED  =  1,   //!< An existing value was updated
+    SI_INSERTED =  2,   //!< A new value was inserted
+
+    // note: test for any error with (retval < 0)
+    SI_FAIL     = -1,   //!< Generic failure
+    SI_NOMEM    = -2,   //!< Out of memory error
+    SI_FILE     = -3    //!< File error (see errno for detail error)
+};
+
+#define SI_UTF8_SIGNATURE     "\xEF\xBB\xBF"
+
+#ifdef _WIN32
+# define SI_NEWLINE_A   "\r\n"
+# define SI_NEWLINE_W   L"\r\n"
+#else // !_WIN32
+# define SI_NEWLINE_A   "\n"
+# define SI_NEWLINE_W   L"\n"
+#endif // _WIN32
+
+#if defined(SI_CONVERT_ICU)
+# include 
+#endif
+
+#if defined(_WIN32)
+# define SI_HAS_WIDE_FILE
+# define SI_WCHAR_T     wchar_t
+#elif defined(SI_CONVERT_ICU)
+# define SI_HAS_WIDE_FILE
+# define SI_WCHAR_T     UChar
+#endif
+
+
+// ---------------------------------------------------------------------------
+//                              MAIN TEMPLATE CLASS
+// ---------------------------------------------------------------------------
+
+/** Simple INI file reader.
+
+    This can be instantiated with the choice of unicode or native characterset,
+    and case sensitive or insensitive comparisons of section and key names.
+    The supported combinations are pre-defined with the following typedefs:
+
+    
+        
Interface Case-sensitive Typedef +
char No CSimpleIniA +
char Yes CSimpleIniCaseA +
wchar_t No CSimpleIniW +
wchar_t Yes CSimpleIniCaseW +
+ + Note that using other types for the SI_CHAR is supported. For instance, + unsigned char, unsigned short, etc. Note that where the alternative type + is a different size to char/wchar_t you may need to supply new helper + classes for SI_STRLESS and SI_CONVERTER. + */ +template +class CSimpleIniTempl +{ +public: + typedef SI_CHAR SI_CHAR_T; + + /** key entry */ + struct Entry { + const SI_CHAR * pItem; + const SI_CHAR * pComment; + int nOrder; + + Entry(const SI_CHAR * a_pszItem = NULL, int a_nOrder = 0) + : pItem(a_pszItem) + , pComment(NULL) + , nOrder(a_nOrder) + { } + Entry(const SI_CHAR * a_pszItem, const SI_CHAR * a_pszComment, int a_nOrder) + : pItem(a_pszItem) + , pComment(a_pszComment) + , nOrder(a_nOrder) + { } + Entry(const Entry & rhs) { operator=(rhs); } + Entry & operator=(const Entry & rhs) { + pItem = rhs.pItem; + pComment = rhs.pComment; + nOrder = rhs.nOrder; + return *this; + } + +#if defined(_MSC_VER) && _MSC_VER <= 1200 + /** STL of VC6 doesn't allow me to specify my own comparator for list::sort() */ + bool operator<(const Entry & rhs) const { return LoadOrder()(*this, rhs); } + bool operator>(const Entry & rhs) const { return LoadOrder()(rhs, *this); } +#endif + + /** Strict less ordering by name of key only */ + struct KeyOrder : std::binary_function { + bool operator()(const Entry & lhs, const Entry & rhs) const { + const static SI_STRLESS isLess = SI_STRLESS(); + return isLess(lhs.pItem, rhs.pItem); + } + }; + + /** Strict less ordering by order, and then name of key */ + struct LoadOrder : std::binary_function { + bool operator()(const Entry & lhs, const Entry & rhs) const { + if (lhs.nOrder != rhs.nOrder) { + return lhs.nOrder < rhs.nOrder; + } + return KeyOrder()(lhs.pItem, rhs.pItem); + } + }; + }; + + /** map keys to values */ + typedef std::multimap TKeyVal; + + /** map sections to key/value map */ + typedef std::map TSection; + + /** set of dependent string pointers. Note that these pointers are + dependent on memory owned by CSimpleIni. + */ + typedef std::list TNamesDepend; + + /** interface definition for the OutputWriter object to pass to Save() + in order to output the INI file data. + */ + class OutputWriter { + public: + OutputWriter() { } + virtual ~OutputWriter() { } + virtual void Write(const char * a_pBuf) = 0; + private: + OutputWriter(const OutputWriter &); // disable + OutputWriter & operator=(const OutputWriter &); // disable + }; + + /** OutputWriter class to write the INI data to a file */ + class FileWriter : public OutputWriter { + FILE * m_file; + public: + FileWriter(FILE * a_file) : m_file(a_file) { } + void Write(const char * a_pBuf) { + fputs(a_pBuf, m_file); + } + private: + FileWriter(const FileWriter &); // disable + FileWriter & operator=(const FileWriter &); // disable + }; + + /** OutputWriter class to write the INI data to a string */ + class StringWriter : public OutputWriter { + std::string & m_string; + public: + StringWriter(std::string & a_string) : m_string(a_string) { } + void Write(const char * a_pBuf) { + m_string.append(a_pBuf); + } + private: + StringWriter(const StringWriter &); // disable + StringWriter & operator=(const StringWriter &); // disable + }; + +#ifdef SI_SUPPORT_IOSTREAMS + /** OutputWriter class to write the INI data to an ostream */ + class StreamWriter : public OutputWriter { + std::ostream & m_ostream; + public: + StreamWriter(std::ostream & a_ostream) : m_ostream(a_ostream) { } + void Write(const char * a_pBuf) { + m_ostream << a_pBuf; + } + private: + StreamWriter(const StreamWriter &); // disable + StreamWriter & operator=(const StreamWriter &); // disable + }; +#endif // SI_SUPPORT_IOSTREAMS + + /** Characterset conversion utility class to convert strings to the + same format as is used for the storage. + */ + class Converter : private SI_CONVERTER { + public: + using SI_CONVERTER::SizeToStore; + Converter(bool a_bStoreIsUtf8) : SI_CONVERTER(a_bStoreIsUtf8) { + m_scratch.resize(1024); + } + Converter(const Converter & rhs) { operator=(rhs); } + Converter & operator=(const Converter & rhs) { + m_scratch = rhs.m_scratch; + return *this; + } + bool ConvertToStore(const SI_CHAR * a_pszString) { + size_t uLen = SizeToStore(a_pszString); + if (uLen == (size_t)(-1)) { + return false; + } + while (uLen > m_scratch.size()) { + m_scratch.resize(m_scratch.size() * 2); + } + return SI_CONVERTER::ConvertToStore( + a_pszString, + const_cast(m_scratch.data()), + m_scratch.size()); + } + const char * Data() { return m_scratch.data(); } + private: + std::string m_scratch; + }; + +public: + /*-----------------------------------------------------------------------*/ + + /** Default constructor. + + @param a_bIsUtf8 See the method SetUnicode() for details. + @param a_bMultiKey See the method SetMultiKey() for details. + @param a_bMultiLine See the method SetMultiLine() for details. + */ + CSimpleIniTempl( + bool a_bIsUtf8 = false, + bool a_bMultiKey = false, + bool a_bMultiLine = false + ); + + /** Destructor */ + ~CSimpleIniTempl(); + + /** Deallocate all memory stored by this object */ + void Reset(); + + /** Has any data been loaded */ + bool IsEmpty() const { return m_data.empty(); } + + /*-----------------------------------------------------------------------*/ + /** @{ @name Settings */ + + /** Set the storage format of the INI data. This affects both the loading + and saving of the INI data using all of the Load/Save API functions. + This value cannot be changed after any INI data has been loaded. + + If the file is not set to Unicode (UTF-8), then the data encoding is + assumed to be the OS native encoding. This encoding is the system + locale on Linux/Unix and the legacy MBCS encoding on Windows NT/2K/XP. + If the storage format is set to Unicode then the file will be loaded + as UTF-8 encoded data regardless of the native file encoding. If + SI_CHAR == char then all of the char* parameters take and return UTF-8 + encoded data regardless of the system locale. + + \param a_bIsUtf8 Assume UTF-8 encoding for the source? + */ + void SetUnicode(bool a_bIsUtf8 = true) { + if (!m_pData) m_bStoreIsUtf8 = a_bIsUtf8; + } + + /** Get the storage format of the INI data. */ + bool IsUnicode() const { return m_bStoreIsUtf8; } + + /** Should multiple identical keys be permitted in the file. If set to false + then the last value encountered will be used as the value of the key. + If set to true, then all values will be available to be queried. For + example, with the following input: + +
+        [section]
+        test=value1
+        test=value2
+        
+ + Then with SetMultiKey(true), both of the values "value1" and "value2" + will be returned for the key test. If SetMultiKey(false) is used, then + the value for "test" will only be "value2". This value may be changed + at any time. + + \param a_bAllowMultiKey Allow multi-keys in the source? + */ + void SetMultiKey(bool a_bAllowMultiKey = true) { + m_bAllowMultiKey = a_bAllowMultiKey; + } + + /** Get the storage format of the INI data. */ + bool IsMultiKey() const { return m_bAllowMultiKey; } + + /** Should data values be permitted to span multiple lines in the file. If + set to false then the multi-line construct << + SI_CHAR FORMAT + char same format as when loaded (MBCS or UTF-8) + wchar_t UTF-8 + other UTF-8 + + + Note that comments from the original data is preserved as per the + documentation on comments. The order of the sections and values + from the original file will be preserved. + + Any data prepended or appended to the output device must use the the + same format (MBCS or UTF-8). You may use the GetConverter() method to + convert text to the correct format regardless of the output format + being used by SimpleIni. + + To add a BOM to UTF-8 data, write it out manually at the very beginning + like is done in SaveFile when a_bUseBOM is true. + + @param a_oOutput Output writer to write the data to. + + @param a_bAddSignature Prepend the UTF-8 BOM if the output data is in + UTF-8 format. If it is not UTF-8 then this value is + ignored. Do not set this to true if anything has + already been written to the OutputWriter. + + @return SI_Error See error definitions + */ + SI_Error Save( + OutputWriter & a_oOutput, + bool a_bAddSignature = false + ) const; + +#ifdef SI_SUPPORT_IOSTREAMS + /** Save the INI data to an ostream. See Save() for details. + + @param a_ostream String to have the INI data appended to. + + @param a_bAddSignature Prepend the UTF-8 BOM if the output data is in + UTF-8 format. If it is not UTF-8 then this value is + ignored. Do not set this to true if anything has + already been written to the stream. + + @return SI_Error See error definitions + */ + SI_Error Save( + std::ostream & a_ostream, + bool a_bAddSignature = false + ) const + { + StreamWriter writer(a_ostream); + return Save(writer, a_bAddSignature); + } +#endif // SI_SUPPORT_IOSTREAMS + + /** Append the INI data to a string. See Save() for details. + + @param a_sBuffer String to have the INI data appended to. + + @param a_bAddSignature Prepend the UTF-8 BOM if the output data is in + UTF-8 format. If it is not UTF-8 then this value is + ignored. Do not set this to true if anything has + already been written to the string. + + @return SI_Error See error definitions + */ + SI_Error Save( + std::string & a_sBuffer, + bool a_bAddSignature = false + ) const + { + StringWriter writer(a_sBuffer); + return Save(writer, a_bAddSignature); + } + + /*-----------------------------------------------------------------------*/ + /** @} + @{ @name Accessing INI Data */ + + /** Retrieve all section names. The list is returned as an STL vector of + names and can be iterated or searched as necessary. Note that the + sort order of the returned strings is NOT DEFINED. You can sort + the names into the load order if desired. Search this file for ".sort" + for an example. + + NOTE! This structure contains only pointers to strings. The actual + string data is stored in memory owned by CSimpleIni. Ensure that the + CSimpleIni object is not destroyed or Reset() while these pointers + are in use! + + @param a_names Vector that will receive all of the section + names. See note above! + */ + void GetAllSections( + TNamesDepend & a_names + ) const; + + /** Retrieve all unique key names in a section. The sort order of the + returned strings is NOT DEFINED. You can sort the names into the load + order if desired. Search this file for ".sort" for an example. Only + unique key names are returned. + + NOTE! This structure contains only pointers to strings. The actual + string data is stored in memory owned by CSimpleIni. Ensure that the + CSimpleIni object is not destroyed or Reset() while these strings + are in use! + + @param a_pSection Section to request data for + @param a_names List that will receive all of the key + names. See note above! + + @return true Section was found. + @return false Matching section was not found. + */ + bool GetAllKeys( + const SI_CHAR * a_pSection, + TNamesDepend & a_names + ) const; + + /** Retrieve all values for a specific key. This method can be used when + multiple keys are both enabled and disabled. Note that the sort order + of the returned strings is NOT DEFINED. You can sort the names into + the load order if desired. Search this file for ".sort" for an example. + + NOTE! The returned values are pointers to string data stored in memory + owned by CSimpleIni. Ensure that the CSimpleIni object is not destroyed + or Reset while you are using this pointer! + + @param a_pSection Section to search + @param a_pKey Key to search for + @param a_values List to return if the key is not found + + @return true Key was found. + @return false Matching section/key was not found. + */ + bool GetAllValues( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + TNamesDepend & a_values + ) const; + + /** Query the number of keys in a specific section. Note that if multiple + keys are enabled, then this value may be different to the number of + keys returned by GetAllKeys. + + @param a_pSection Section to request data for + + @return -1 Section does not exist in the file + @return >=0 Number of keys in the section + */ + int GetSectionSize( + const SI_CHAR * a_pSection + ) const; + + /** Retrieve all key and value pairs for a section. The data is returned + as a pointer to an STL map and can be iterated or searched as + desired. Note that multiple entries for the same key may exist when + multiple keys have been enabled. + + NOTE! This structure contains only pointers to strings. The actual + string data is stored in memory owned by CSimpleIni. Ensure that the + CSimpleIni object is not destroyed or Reset() while these strings + are in use! + + @param a_pSection Name of the section to return + @return boolean Was a section matching the supplied + name found. + */ + const TKeyVal * GetSection( + const SI_CHAR * a_pSection + ) const; + + /** Retrieve the value for a specific key. If multiple keys are enabled + (see SetMultiKey) then only the first value associated with that key + will be returned, see GetAllValues for getting all values with multikey. + + NOTE! The returned value is a pointer to string data stored in memory + owned by CSimpleIni. Ensure that the CSimpleIni object is not destroyed + or Reset while you are using this pointer! + + @param a_pSection Section to search + @param a_pKey Key to search for + @param a_pDefault Value to return if the key is not found + @param a_pHasMultiple Optionally receive notification of if there are + multiple entries for this key. + + @return a_pDefault Key was not found in the section + @return other Value of the key + */ + const SI_CHAR * GetValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + const SI_CHAR * a_pDefault = NULL, + bool * a_pHasMultiple = NULL + ) const; + + /** Retrieve a numeric value for a specific key. If multiple keys are enabled + (see SetMultiKey) then only the first value associated with that key + will be returned, see GetAllValues for getting all values with multikey. + + @param a_pSection Section to search + @param a_pKey Key to search for + @param a_nDefault Value to return if the key is not found + @param a_pHasMultiple Optionally receive notification of if there are + multiple entries for this key. + + @return a_nDefault Key was not found in the section + @return other Value of the key + */ + long GetLongValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + long a_nDefault = 0, + bool * a_pHasMultiple = NULL + ) const; + + /** Retrieve a numeric value for a specific key. If multiple keys are enabled + (see SetMultiKey) then only the first value associated with that key + will be returned, see GetAllValues for getting all values with multikey. + + @param a_pSection Section to search + @param a_pKey Key to search for + @param a_nDefault Value to return if the key is not found + @param a_pHasMultiple Optionally receive notification of if there are + multiple entries for this key. + + @return a_nDefault Key was not found in the section + @return other Value of the key + */ + double GetDoubleValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + double a_nDefault = 0, + bool * a_pHasMultiple = NULL + ) const; + + /** Retrieve a boolean value for a specific key. If multiple keys are enabled + (see SetMultiKey) then only the first value associated with that key + will be returned, see GetAllValues for getting all values with multikey. + + Strings starting with "t", "y", "on" or "1" are returned as logically true. + Strings starting with "f", "n", "of" or "0" are returned as logically false. + For all other values the default is returned. Character comparisons are + case-insensitive. + + @param a_pSection Section to search + @param a_pKey Key to search for + @param a_bDefault Value to return if the key is not found + @param a_pHasMultiple Optionally receive notification of if there are + multiple entries for this key. + + @return a_nDefault Key was not found in the section + @return other Value of the key + */ + bool GetBoolValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + bool a_bDefault = false, + bool * a_pHasMultiple = NULL + ) const; + + /** Add or update a section or value. This will always insert + when multiple keys are enabled. + + @param a_pSection Section to add or update + @param a_pKey Key to add or update. Set to NULL to + create an empty section. + @param a_pValue Value to set. Set to NULL to create an + empty section. + @param a_pComment Comment to be associated with the section or the + key. If a_pKey is NULL then it will be associated + with the section, otherwise the key. Note that a + comment may be set ONLY when the section or key is + first created (i.e. when this function returns the + value SI_INSERTED). If you wish to create a section + with a comment then you need to create the section + separately to the key. The comment string must be + in full comment form already (have a comment + character starting every line). + @param a_bForceReplace Should all existing values in a multi-key INI + file be replaced with this entry. This option has + no effect if not using multi-key files. The + difference between Delete/SetValue and SetValue + with a_bForceReplace = true, is that the load + order and comment will be preserved this way. + + @return SI_Error See error definitions + @return SI_UPDATED Value was updated + @return SI_INSERTED Value was inserted + */ + SI_Error SetValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + const SI_CHAR * a_pValue, + const SI_CHAR * a_pComment = NULL, + bool a_bForceReplace = false + ) + { + return AddEntry(a_pSection, a_pKey, a_pValue, a_pComment, a_bForceReplace, true); + } + + /** Add or update a numeric value. This will always insert + when multiple keys are enabled. + + @param a_pSection Section to add or update + @param a_pKey Key to add or update. + @param a_nValue Value to set. + @param a_pComment Comment to be associated with the key. See the + notes on SetValue() for comments. + @param a_bUseHex By default the value will be written to the file + in decimal format. Set this to true to write it + as hexadecimal. + @param a_bForceReplace Should all existing values in a multi-key INI + file be replaced with this entry. This option has + no effect if not using multi-key files. The + difference between Delete/SetLongValue and + SetLongValue with a_bForceReplace = true, is that + the load order and comment will be preserved this + way. + + @return SI_Error See error definitions + @return SI_UPDATED Value was updated + @return SI_INSERTED Value was inserted + */ + SI_Error SetLongValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + long a_nValue, + const SI_CHAR * a_pComment = NULL, + bool a_bUseHex = false, + bool a_bForceReplace = false + ); + + /** Add or update a double value. This will always insert + when multiple keys are enabled. + + @param a_pSection Section to add or update + @param a_pKey Key to add or update. + @param a_nValue Value to set. + @param a_pComment Comment to be associated with the key. See the + notes on SetValue() for comments. + @param a_bForceReplace Should all existing values in a multi-key INI + file be replaced with this entry. This option has + no effect if not using multi-key files. The + difference between Delete/SetDoubleValue and + SetDoubleValue with a_bForceReplace = true, is that + the load order and comment will be preserved this + way. + + @return SI_Error See error definitions + @return SI_UPDATED Value was updated + @return SI_INSERTED Value was inserted + */ + SI_Error SetDoubleValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + double a_nValue, + const SI_CHAR * a_pComment = NULL, + bool a_bForceReplace = false + ); + + /** Add or update a boolean value. This will always insert + when multiple keys are enabled. + + @param a_pSection Section to add or update + @param a_pKey Key to add or update. + @param a_bValue Value to set. + @param a_pComment Comment to be associated with the key. See the + notes on SetValue() for comments. + @param a_bForceReplace Should all existing values in a multi-key INI + file be replaced with this entry. This option has + no effect if not using multi-key files. The + difference between Delete/SetBoolValue and + SetBoolValue with a_bForceReplace = true, is that + the load order and comment will be preserved this + way. + + @return SI_Error See error definitions + @return SI_UPDATED Value was updated + @return SI_INSERTED Value was inserted + */ + SI_Error SetBoolValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + bool a_bValue, + const SI_CHAR * a_pComment = NULL, + bool a_bForceReplace = false + ); + + /** Delete an entire section, or a key from a section. Note that the + data returned by GetSection is invalid and must not be used after + anything has been deleted from that section using this method. + Note when multiple keys is enabled, this will delete all keys with + that name; there is no way to selectively delete individual key/values + in this situation. + + @param a_pSection Section to delete key from, or if + a_pKey is NULL, the section to remove. + @param a_pKey Key to remove from the section. Set to + NULL to remove the entire section. + @param a_bRemoveEmpty If the section is empty after this key has + been deleted, should the empty section be + removed? + + @return true Key or section was deleted. + @return false Key or section was not found. + */ + bool Delete( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + bool a_bRemoveEmpty = false + ); + + /*-----------------------------------------------------------------------*/ + /** @} + @{ @name Converter */ + + /** Return a conversion object to convert text to the same encoding + as is used by the Save(), SaveFile() and SaveString() functions. + Use this to prepare the strings that you wish to append or prepend + to the output INI data. + */ + Converter GetConverter() const { + return Converter(m_bStoreIsUtf8); + } + + /*-----------------------------------------------------------------------*/ + /** @} */ + +private: + // copying is not permitted + CSimpleIniTempl(const CSimpleIniTempl &); // disabled + CSimpleIniTempl & operator=(const CSimpleIniTempl &); // disabled + + /** Parse the data looking for a file comment and store it if found. + */ + SI_Error FindFileComment( + SI_CHAR *& a_pData, + bool a_bCopyStrings + ); + + /** Parse the data looking for the next valid entry. The memory pointed to + by a_pData is modified by inserting NULL characters. The pointer is + updated to the current location in the block of text. + */ + bool FindEntry( + SI_CHAR *& a_pData, + const SI_CHAR *& a_pSection, + const SI_CHAR *& a_pKey, + const SI_CHAR *& a_pVal, + const SI_CHAR *& a_pComment + ) const; + + /** Add the section/key/value to our data. + + @param a_pSection Section name. Sections will be created if they + don't already exist. + @param a_pKey Key name. May be NULL to create an empty section. + Existing entries will be updated. New entries will + be created. + @param a_pValue Value for the key. + @param a_pComment Comment to be associated with the section or the + key. If a_pKey is NULL then it will be associated + with the section, otherwise the key. This must be + a string in full comment form already (have a + comment character starting every line). + @param a_bForceReplace Should all existing values in a multi-key INI + file be replaced with this entry. This option has + no effect if not using multi-key files. The + difference between Delete/AddEntry and AddEntry + with a_bForceReplace = true, is that the load + order and comment will be preserved this way. + @param a_bCopyStrings Should copies of the strings be made or not. + If false then the pointers will be used as is. + */ + SI_Error AddEntry( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + const SI_CHAR * a_pValue, + const SI_CHAR * a_pComment, + bool a_bForceReplace, + bool a_bCopyStrings + ); + + /** Is the supplied character a whitespace character? */ + inline bool IsSpace(SI_CHAR ch) const { + return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'); + } + + /** Does the supplied character start a comment line? */ + inline bool IsComment(SI_CHAR ch) const { + return (ch == ';' || ch == '#'); + } + + + /** Skip over a newline character (or characters) for either DOS or UNIX */ + inline void SkipNewLine(SI_CHAR *& a_pData) const { + a_pData += (*a_pData == '\r' && *(a_pData+1) == '\n') ? 2 : 1; + } + + /** Make a copy of the supplied string, replacing the original pointer */ + SI_Error CopyString(const SI_CHAR *& a_pString); + + /** Delete a string from the copied strings buffer if necessary */ + void DeleteString(const SI_CHAR * a_pString); + + /** Internal use of our string comparison function */ + bool IsLess(const SI_CHAR * a_pLeft, const SI_CHAR * a_pRight) const { + const static SI_STRLESS isLess = SI_STRLESS(); + return isLess(a_pLeft, a_pRight); + } + + bool IsMultiLineTag(const SI_CHAR * a_pData) const; + bool IsMultiLineData(const SI_CHAR * a_pData) const; + bool LoadMultiLineText( + SI_CHAR *& a_pData, + const SI_CHAR *& a_pVal, + const SI_CHAR * a_pTagName, + bool a_bAllowBlankLinesInComment = false + ) const; + bool IsNewLineChar(SI_CHAR a_c) const; + + bool OutputMultiLineText( + OutputWriter & a_oOutput, + Converter & a_oConverter, + const SI_CHAR * a_pText + ) const; + +private: + /** Copy of the INI file data in our character format. This will be + modified when parsed to have NULL characters added after all + interesting string entries. All of the string pointers to sections, + keys and values point into this block of memory. + */ + SI_CHAR * m_pData; + + /** Length of the data that we have stored. Used when deleting strings + to determine if the string is stored here or in the allocated string + buffer. + */ + size_t m_uDataLen; + + /** File comment for this data, if one exists. */ + const SI_CHAR * m_pFileComment; + + /** Parsed INI data. Section -> (Key -> Value). */ + TSection m_data; + + /** This vector stores allocated memory for copies of strings that have + been supplied after the file load. It will be empty unless SetValue() + has been called. + */ + TNamesDepend m_strings; + + /** Is the format of our datafile UTF-8 or MBCS? */ + bool m_bStoreIsUtf8; + + /** Are multiple values permitted for the same key? */ + bool m_bAllowMultiKey; + + /** Are data values permitted to span multiple lines? */ + bool m_bAllowMultiLine; + + /** Should spaces be written out surrounding the equals sign? */ + bool m_bSpaces; + + /** Next order value, used to ensure sections and keys are output in the + same order that they are loaded/added. + */ + int m_nOrder; +}; + +// --------------------------------------------------------------------------- +// IMPLEMENTATION +// --------------------------------------------------------------------------- + +template +CSimpleIniTempl::CSimpleIniTempl( + bool a_bIsUtf8, + bool a_bAllowMultiKey, + bool a_bAllowMultiLine + ) + : m_pData(0) + , m_uDataLen(0) + , m_pFileComment(NULL) + , m_bStoreIsUtf8(a_bIsUtf8) + , m_bAllowMultiKey(a_bAllowMultiKey) + , m_bAllowMultiLine(a_bAllowMultiLine) + , m_bSpaces(true) + , m_nOrder(0) +{ } + +template +CSimpleIniTempl::~CSimpleIniTempl() +{ + Reset(); +} + +template +void +CSimpleIniTempl::Reset() +{ + // remove all data + delete[] m_pData; + m_pData = NULL; + m_uDataLen = 0; + m_pFileComment = NULL; + if (!m_data.empty()) { + m_data.erase(m_data.begin(), m_data.end()); + } + + // remove all strings + if (!m_strings.empty()) { + typename TNamesDepend::iterator i = m_strings.begin(); + for (; i != m_strings.end(); ++i) { + delete[] const_cast(i->pItem); + } + m_strings.erase(m_strings.begin(), m_strings.end()); + } +} + +template +SI_Error +CSimpleIniTempl::LoadFile( + const char * a_pszFile + ) +{ + FILE * fp = NULL; +#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE + fopen_s(&fp, a_pszFile, "rb"); +#else // !__STDC_WANT_SECURE_LIB__ + fp = fopen(a_pszFile, "rb"); +#endif // __STDC_WANT_SECURE_LIB__ + if (!fp) { + return SI_FILE; + } + SI_Error rc = LoadFile(fp); + fclose(fp); + return rc; +} + +#ifdef SI_HAS_WIDE_FILE +template +SI_Error +CSimpleIniTempl::LoadFile( + const SI_WCHAR_T * a_pwszFile + ) +{ +#ifdef _WIN32 + FILE * fp = NULL; +#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE + _wfopen_s(&fp, a_pwszFile, L"rb"); +#else // !__STDC_WANT_SECURE_LIB__ + fp = _wfopen(a_pwszFile, L"rb"); +#endif // __STDC_WANT_SECURE_LIB__ + if (!fp) return SI_FILE; + SI_Error rc = LoadFile(fp); + fclose(fp); + return rc; +#else // !_WIN32 (therefore SI_CONVERT_ICU) + char szFile[256]; + u_austrncpy(szFile, a_pwszFile, sizeof(szFile)); + return LoadFile(szFile); +#endif // _WIN32 +} +#endif // SI_HAS_WIDE_FILE + +template +SI_Error +CSimpleIniTempl::LoadFile( + FILE * a_fpFile + ) +{ + // load the raw file data + int retval = fseek(a_fpFile, 0, SEEK_END); + if (retval != 0) { + return SI_FILE; + } + long lSize = ftell(a_fpFile); + if (lSize < 0) { + return SI_FILE; + } + if (lSize == 0) { + return SI_OK; + } + + // allocate and ensure NULL terminated + char * pData = new char[lSize+1]; + if (!pData) { + return SI_NOMEM; + } + pData[lSize] = 0; + + // load data into buffer + fseek(a_fpFile, 0, SEEK_SET); + size_t uRead = fread(pData, sizeof(char), lSize, a_fpFile); + if (uRead != (size_t) lSize) { + delete[] pData; + return SI_FILE; + } + + // convert the raw data to unicode + SI_Error rc = LoadData(pData, uRead); + delete[] pData; + return rc; +} + +template +SI_Error +CSimpleIniTempl::LoadData( + const char * a_pData, + size_t a_uDataLen + ) +{ + SI_CONVERTER converter(m_bStoreIsUtf8); + + if (a_uDataLen == 0) { + return SI_OK; + } + + // consume the UTF-8 BOM if it exists + if (m_bStoreIsUtf8 && a_uDataLen >= 3) { + if (memcmp(a_pData, SI_UTF8_SIGNATURE, 3) == 0) { + a_pData += 3; + a_uDataLen -= 3; + } + } + + // determine the length of the converted data + size_t uLen = converter.SizeFromStore(a_pData, a_uDataLen); + if (uLen == (size_t)(-1)) { + return SI_FAIL; + } + + // allocate memory for the data, ensure that there is a NULL + // terminator wherever the converted data ends + SI_CHAR * pData = new SI_CHAR[uLen+1]; + if (!pData) { + return SI_NOMEM; + } + memset(pData, 0, sizeof(SI_CHAR)*(uLen+1)); + + // convert the data + if (!converter.ConvertFromStore(a_pData, a_uDataLen, pData, uLen)) { + delete[] pData; + return SI_FAIL; + } + + // parse it + const static SI_CHAR empty = 0; + SI_CHAR * pWork = pData; + const SI_CHAR * pSection = ∅ + const SI_CHAR * pItem = NULL; + const SI_CHAR * pVal = NULL; + const SI_CHAR * pComment = NULL; + + // We copy the strings if we are loading data into this class when we + // already have stored some. + bool bCopyStrings = (m_pData != NULL); + + // find a file comment if it exists, this is a comment that starts at the + // beginning of the file and continues until the first blank line. + SI_Error rc = FindFileComment(pWork, bCopyStrings); + if (rc < 0) return rc; + + // add every entry in the file to the data table + while (FindEntry(pWork, pSection, pItem, pVal, pComment)) { + rc = AddEntry(pSection, pItem, pVal, pComment, false, bCopyStrings); + if (rc < 0) return rc; + } + + // store these strings if we didn't copy them + if (bCopyStrings) { + delete[] pData; + } + else { + m_pData = pData; + m_uDataLen = uLen+1; + } + + return SI_OK; +} + +#ifdef SI_SUPPORT_IOSTREAMS +template +SI_Error +CSimpleIniTempl::LoadData( + std::istream & a_istream + ) +{ + std::string strData; + char szBuf[512]; + do { + a_istream.get(szBuf, sizeof(szBuf), '\0'); + strData.append(szBuf); + } + while (a_istream.good()); + return LoadData(strData); +} +#endif // SI_SUPPORT_IOSTREAMS + +template +SI_Error +CSimpleIniTempl::FindFileComment( + SI_CHAR *& a_pData, + bool a_bCopyStrings + ) +{ + // there can only be a single file comment + if (m_pFileComment) { + return SI_OK; + } + + // Load the file comment as multi-line text, this will modify all of + // the newline characters to be single \n chars + if (!LoadMultiLineText(a_pData, m_pFileComment, NULL, false)) { + return SI_OK; + } + + // copy the string if necessary + if (a_bCopyStrings) { + SI_Error rc = CopyString(m_pFileComment); + if (rc < 0) return rc; + } + + return SI_OK; +} + +template +bool +CSimpleIniTempl::FindEntry( + SI_CHAR *& a_pData, + const SI_CHAR *& a_pSection, + const SI_CHAR *& a_pKey, + const SI_CHAR *& a_pVal, + const SI_CHAR *& a_pComment + ) const +{ + a_pComment = NULL; + + SI_CHAR * pTrail = NULL; + while (*a_pData) { + // skip spaces and empty lines + while (*a_pData && IsSpace(*a_pData)) { + ++a_pData; + } + if (!*a_pData) { + break; + } + + // skip processing of comment lines but keep a pointer to + // the start of the comment. + if (IsComment(*a_pData)) { + LoadMultiLineText(a_pData, a_pComment, NULL, true); + continue; + } + + // process section names + if (*a_pData == '[') { + // skip leading spaces + ++a_pData; + while (*a_pData && IsSpace(*a_pData)) { + ++a_pData; + } + + // find the end of the section name (it may contain spaces) + // and convert it to lowercase as necessary + a_pSection = a_pData; + while (*a_pData && *a_pData != ']' && !IsNewLineChar(*a_pData)) { + ++a_pData; + } + + // if it's an invalid line, just skip it + if (*a_pData != ']') { + continue; + } + + // remove trailing spaces from the section + pTrail = a_pData - 1; + while (pTrail >= a_pSection && IsSpace(*pTrail)) { + --pTrail; + } + ++pTrail; + *pTrail = 0; + + // skip to the end of the line + ++a_pData; // safe as checked that it == ']' above + while (*a_pData && !IsNewLineChar(*a_pData)) { + ++a_pData; + } + + a_pKey = NULL; + a_pVal = NULL; + return true; + } + + // find the end of the key name (it may contain spaces) + // and convert it to lowercase as necessary + a_pKey = a_pData; + while (*a_pData && *a_pData != '=' && !IsNewLineChar(*a_pData)) { + ++a_pData; + } + + // if it's an invalid line, just skip it + if (*a_pData != '=') { + continue; + } + + // empty keys are invalid + if (a_pKey == a_pData) { + while (*a_pData && !IsNewLineChar(*a_pData)) { + ++a_pData; + } + continue; + } + + // remove trailing spaces from the key + pTrail = a_pData - 1; + while (pTrail >= a_pKey && IsSpace(*pTrail)) { + --pTrail; + } + ++pTrail; + *pTrail = 0; + + // skip leading whitespace on the value + ++a_pData; // safe as checked that it == '=' above + while (*a_pData && !IsNewLineChar(*a_pData) && IsSpace(*a_pData)) { + ++a_pData; + } + + // find the end of the value which is the end of this line + a_pVal = a_pData; + while (*a_pData && !IsNewLineChar(*a_pData)) { + ++a_pData; + } + + // remove trailing spaces from the value + pTrail = a_pData - 1; + if (*a_pData) { // prepare for the next round + SkipNewLine(a_pData); + } + while (pTrail >= a_pVal && IsSpace(*pTrail)) { + --pTrail; + } + ++pTrail; + *pTrail = 0; + + // check for multi-line entries + if (m_bAllowMultiLine && IsMultiLineTag(a_pVal)) { + // skip the "<<<" to get the tag that will end the multiline + const SI_CHAR * pTagName = a_pVal + 3; + return LoadMultiLineText(a_pData, a_pVal, pTagName); + } + + // return the standard entry + return true; + } + + return false; +} + +template +bool +CSimpleIniTempl::IsMultiLineTag( + const SI_CHAR * a_pVal + ) const +{ + // check for the "<<<" prefix for a multi-line entry + if (*a_pVal++ != '<') return false; + if (*a_pVal++ != '<') return false; + if (*a_pVal++ != '<') return false; + return true; +} + +template +bool +CSimpleIniTempl::IsMultiLineData( + const SI_CHAR * a_pData + ) const +{ + // data is multi-line if it has any of the following features: + // * whitespace prefix + // * embedded newlines + // * whitespace suffix + + // empty string + if (!*a_pData) { + return false; + } + + // check for prefix + if (IsSpace(*a_pData)) { + return true; + } + + // embedded newlines + while (*a_pData) { + if (IsNewLineChar(*a_pData)) { + return true; + } + ++a_pData; + } + + // check for suffix + if (IsSpace(*--a_pData)) { + return true; + } + + return false; +} + +template +bool +CSimpleIniTempl::IsNewLineChar( + SI_CHAR a_c + ) const +{ + return (a_c == '\n' || a_c == '\r'); +} + +template +bool +CSimpleIniTempl::LoadMultiLineText( + SI_CHAR *& a_pData, + const SI_CHAR *& a_pVal, + const SI_CHAR * a_pTagName, + bool a_bAllowBlankLinesInComment + ) const +{ + // we modify this data to strip all newlines down to a single '\n' + // character. This means that on Windows we need to strip out some + // characters which will make the data shorter. + // i.e. LINE1-LINE1\r\nLINE2-LINE2\0 will become + // LINE1-LINE1\nLINE2-LINE2\0 + // The pDataLine entry is the pointer to the location in memory that + // the current line needs to start to run following the existing one. + // This may be the same as pCurrLine in which case no move is needed. + SI_CHAR * pDataLine = a_pData; + SI_CHAR * pCurrLine; + + // value starts at the current line + a_pVal = a_pData; + + // find the end tag. This tag must start in column 1 and be + // followed by a newline. No whitespace removal is done while + // searching for this tag. + SI_CHAR cEndOfLineChar = *a_pData; + for(;;) { + // if we are loading comments then we need a comment character as + // the first character on every line + if (!a_pTagName && !IsComment(*a_pData)) { + // if we aren't allowing blank lines then we're done + if (!a_bAllowBlankLinesInComment) { + break; + } + + // if we are allowing blank lines then we only include them + // in this comment if another comment follows, so read ahead + // to find out. + SI_CHAR * pCurr = a_pData; + int nNewLines = 0; + while (IsSpace(*pCurr)) { + if (IsNewLineChar(*pCurr)) { + ++nNewLines; + SkipNewLine(pCurr); + } + else { + ++pCurr; + } + } + + // we have a comment, add the blank lines to the output + // and continue processing from here + if (IsComment(*pCurr)) { + for (; nNewLines > 0; --nNewLines) *pDataLine++ = '\n'; + a_pData = pCurr; + continue; + } + + // the comment ends here + break; + } + + // find the end of this line + pCurrLine = a_pData; + while (*a_pData && !IsNewLineChar(*a_pData)) ++a_pData; + + // move this line down to the location that it should be if necessary + if (pDataLine < pCurrLine) { + size_t nLen = (size_t) (a_pData - pCurrLine); + memmove(pDataLine, pCurrLine, nLen * sizeof(SI_CHAR)); + pDataLine[nLen] = '\0'; + } + + // end the line with a NULL + cEndOfLineChar = *a_pData; + *a_pData = 0; + + // if are looking for a tag then do the check now. This is done before + // checking for end of the data, so that if we have the tag at the end + // of the data then the tag is removed correctly. + if (a_pTagName && + (!IsLess(pDataLine, a_pTagName) && !IsLess(a_pTagName, pDataLine))) + { + break; + } + + // if we are at the end of the data then we just automatically end + // this entry and return the current data. + if (!cEndOfLineChar) { + return true; + } + + // otherwise we need to process this newline to ensure that it consists + // of just a single \n character. + pDataLine += (a_pData - pCurrLine); + *a_pData = cEndOfLineChar; + SkipNewLine(a_pData); + *pDataLine++ = '\n'; + } + + // if we didn't find a comment at all then return false + if (a_pVal == a_pData) { + a_pVal = NULL; + return false; + } + + // the data (which ends at the end of the last line) needs to be + // null-terminated BEFORE before the newline character(s). If the + // user wants a new line in the multi-line data then they need to + // add an empty line before the tag. + *--pDataLine = '\0'; + + // if looking for a tag and if we aren't at the end of the data, + // then move a_pData to the start of the next line. + if (a_pTagName && cEndOfLineChar) { + SI_ASSERT(IsNewLineChar(cEndOfLineChar)); + *a_pData = cEndOfLineChar; + SkipNewLine(a_pData); + } + + return true; +} + +template +SI_Error +CSimpleIniTempl::CopyString( + const SI_CHAR *& a_pString + ) +{ + size_t uLen = 0; + if (sizeof(SI_CHAR) == sizeof(char)) { + uLen = strlen((const char *)a_pString); + } + else if (sizeof(SI_CHAR) == sizeof(wchar_t)) { + uLen = wcslen((const wchar_t *)a_pString); + } + else { + for ( ; a_pString[uLen]; ++uLen) /*loop*/ ; + } + ++uLen; // NULL character + SI_CHAR * pCopy = new SI_CHAR[uLen]; + if (!pCopy) { + return SI_NOMEM; + } + memcpy(pCopy, a_pString, sizeof(SI_CHAR)*uLen); + m_strings.push_back(pCopy); + a_pString = pCopy; + return SI_OK; +} + +template +SI_Error +CSimpleIniTempl::AddEntry( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + const SI_CHAR * a_pValue, + const SI_CHAR * a_pComment, + bool a_bForceReplace, + bool a_bCopyStrings + ) +{ + SI_Error rc; + bool bInserted = false; + + SI_ASSERT(!a_pComment || IsComment(*a_pComment)); + + // if we are copying strings then make a copy of the comment now + // because we will need it when we add the entry. + if (a_bCopyStrings && a_pComment) { + rc = CopyString(a_pComment); + if (rc < 0) return rc; + } + + // create the section entry if necessary + typename TSection::iterator iSection = m_data.find(a_pSection); + if (iSection == m_data.end()) { + // if the section doesn't exist then we need a copy as the + // string needs to last beyond the end of this function + if (a_bCopyStrings) { + rc = CopyString(a_pSection); + if (rc < 0) return rc; + } + + // only set the comment if this is a section only entry + Entry oSection(a_pSection, ++m_nOrder); + if (a_pComment && (!a_pKey || !a_pValue)) { + oSection.pComment = a_pComment; + } + + typename TSection::value_type oEntry(oSection, TKeyVal()); + typedef typename TSection::iterator SectionIterator; + std::pair i = m_data.insert(oEntry); + iSection = i.first; + bInserted = true; + } + if (!a_pKey || !a_pValue) { + // section only entries are specified with pItem and pVal as NULL + return bInserted ? SI_INSERTED : SI_UPDATED; + } + + // check for existence of the key + TKeyVal & keyval = iSection->second; + typename TKeyVal::iterator iKey = keyval.find(a_pKey); + + // remove all existing entries but save the load order and + // comment of the first entry + int nLoadOrder = ++m_nOrder; + if (iKey != keyval.end() && m_bAllowMultiKey && a_bForceReplace) { + const SI_CHAR * pComment = NULL; + while (iKey != keyval.end() && !IsLess(a_pKey, iKey->first.pItem)) { + if (iKey->first.nOrder < nLoadOrder) { + nLoadOrder = iKey->first.nOrder; + pComment = iKey->first.pComment; + } + ++iKey; + } + if (pComment) { + DeleteString(a_pComment); + a_pComment = pComment; + CopyString(a_pComment); + } + Delete(a_pSection, a_pKey); + iKey = keyval.end(); + } + + // make string copies if necessary + bool bForceCreateNewKey = m_bAllowMultiKey && !a_bForceReplace; + if (a_bCopyStrings) { + if (bForceCreateNewKey || iKey == keyval.end()) { + // if the key doesn't exist then we need a copy as the + // string needs to last beyond the end of this function + // because we will be inserting the key next + rc = CopyString(a_pKey); + if (rc < 0) return rc; + } + + // we always need a copy of the value + rc = CopyString(a_pValue); + if (rc < 0) return rc; + } + + // create the key entry + if (iKey == keyval.end() || bForceCreateNewKey) { + Entry oKey(a_pKey, nLoadOrder); + if (a_pComment) { + oKey.pComment = a_pComment; + } + typename TKeyVal::value_type oEntry(oKey, static_cast(NULL)); + iKey = keyval.insert(oEntry); + bInserted = true; + } + iKey->second = a_pValue; + return bInserted ? SI_INSERTED : SI_UPDATED; +} + +template +const SI_CHAR * +CSimpleIniTempl::GetValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + const SI_CHAR * a_pDefault, + bool * a_pHasMultiple + ) const +{ + if (a_pHasMultiple) { + *a_pHasMultiple = false; + } + if (!a_pSection || !a_pKey) { + return a_pDefault; + } + typename TSection::const_iterator iSection = m_data.find(a_pSection); + if (iSection == m_data.end()) { + return a_pDefault; + } + typename TKeyVal::const_iterator iKeyVal = iSection->second.find(a_pKey); + if (iKeyVal == iSection->second.end()) { + return a_pDefault; + } + + // check for multiple entries with the same key + if (m_bAllowMultiKey && a_pHasMultiple) { + typename TKeyVal::const_iterator iTemp = iKeyVal; + if (++iTemp != iSection->second.end()) { + if (!IsLess(a_pKey, iTemp->first.pItem)) { + *a_pHasMultiple = true; + } + } + } + + return iKeyVal->second; +} + +template +long +CSimpleIniTempl::GetLongValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + long a_nDefault, + bool * a_pHasMultiple + ) const +{ + // return the default if we don't have a value + const SI_CHAR * pszValue = GetValue(a_pSection, a_pKey, NULL, a_pHasMultiple); + if (!pszValue || !*pszValue) return a_nDefault; + + // convert to UTF-8/MBCS which for a numeric value will be the same as ASCII + char szValue[64] = { 0 }; + SI_CONVERTER c(m_bStoreIsUtf8); + if (!c.ConvertToStore(pszValue, szValue, sizeof(szValue))) { + return a_nDefault; + } + + // handle the value as hex if prefaced with "0x" + long nValue = a_nDefault; + char * pszSuffix = szValue; + if (szValue[0] == '0' && (szValue[1] == 'x' || szValue[1] == 'X')) { + if (!szValue[2]) return a_nDefault; + nValue = strtol(&szValue[2], &pszSuffix, 16); + } + else { + nValue = strtol(szValue, &pszSuffix, 10); + } + + // any invalid strings will return the default value + if (*pszSuffix) { + return a_nDefault; + } + + return nValue; +} + +template +SI_Error +CSimpleIniTempl::SetLongValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + long a_nValue, + const SI_CHAR * a_pComment, + bool a_bUseHex, + bool a_bForceReplace + ) +{ + // use SetValue to create sections + if (!a_pSection || !a_pKey) return SI_FAIL; + + // convert to an ASCII string + char szInput[64]; +#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE + sprintf_s(szInput, a_bUseHex ? "0x%lx" : "%ld", a_nValue); +#else // !__STDC_WANT_SECURE_LIB__ + sprintf(szInput, a_bUseHex ? "0x%lx" : "%ld", a_nValue); +#endif // __STDC_WANT_SECURE_LIB__ + + // convert to output text + SI_CHAR szOutput[64]; + SI_CONVERTER c(m_bStoreIsUtf8); + c.ConvertFromStore(szInput, strlen(szInput) + 1, + szOutput, sizeof(szOutput) / sizeof(SI_CHAR)); + + // actually add it + return AddEntry(a_pSection, a_pKey, szOutput, a_pComment, a_bForceReplace, true); +} + +template +double +CSimpleIniTempl::GetDoubleValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + double a_nDefault, + bool * a_pHasMultiple + ) const +{ + // return the default if we don't have a value + const SI_CHAR * pszValue = GetValue(a_pSection, a_pKey, NULL, a_pHasMultiple); + if (!pszValue || !*pszValue) return a_nDefault; + + // convert to UTF-8/MBCS which for a numeric value will be the same as ASCII + char szValue[64] = { 0 }; + SI_CONVERTER c(m_bStoreIsUtf8); + if (!c.ConvertToStore(pszValue, szValue, sizeof(szValue))) { + return a_nDefault; + } + + char * pszSuffix = NULL; + double nValue = strtod(szValue, &pszSuffix); + + // any invalid strings will return the default value + if (!pszSuffix || *pszSuffix) { + return a_nDefault; + } + + return nValue; +} + +template +SI_Error +CSimpleIniTempl::SetDoubleValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + double a_nValue, + const SI_CHAR * a_pComment, + bool a_bForceReplace + ) +{ + // use SetValue to create sections + if (!a_pSection || !a_pKey) return SI_FAIL; + + // convert to an ASCII string + char szInput[64]; +#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE + sprintf_s(szInput, "%f", a_nValue); +#else // !__STDC_WANT_SECURE_LIB__ + sprintf(szInput, "%f", a_nValue); +#endif // __STDC_WANT_SECURE_LIB__ + + // convert to output text + SI_CHAR szOutput[64]; + SI_CONVERTER c(m_bStoreIsUtf8); + c.ConvertFromStore(szInput, strlen(szInput) + 1, + szOutput, sizeof(szOutput) / sizeof(SI_CHAR)); + + // actually add it + return AddEntry(a_pSection, a_pKey, szOutput, a_pComment, a_bForceReplace, true); +} + +template +bool +CSimpleIniTempl::GetBoolValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + bool a_bDefault, + bool * a_pHasMultiple + ) const +{ + // return the default if we don't have a value + const SI_CHAR * pszValue = GetValue(a_pSection, a_pKey, NULL, a_pHasMultiple); + if (!pszValue || !*pszValue) return a_bDefault; + + // we only look at the minimum number of characters + switch (pszValue[0]) { + case 't': case 'T': // true + case 'y': case 'Y': // yes + case '1': // 1 (one) + return true; + + case 'f': case 'F': // false + case 'n': case 'N': // no + case '0': // 0 (zero) + return false; + + case 'o': case 'O': + if (pszValue[1] == 'n' || pszValue[1] == 'N') return true; // on + if (pszValue[1] == 'f' || pszValue[1] == 'F') return false; // off + break; + } + + // no recognized value, return the default + return a_bDefault; +} + +template +SI_Error +CSimpleIniTempl::SetBoolValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + bool a_bValue, + const SI_CHAR * a_pComment, + bool a_bForceReplace + ) +{ + // use SetValue to create sections + if (!a_pSection || !a_pKey) return SI_FAIL; + + // convert to an ASCII string + const char * pszInput = a_bValue ? "true" : "false"; + + // convert to output text + SI_CHAR szOutput[64]; + SI_CONVERTER c(m_bStoreIsUtf8); + c.ConvertFromStore(pszInput, strlen(pszInput) + 1, + szOutput, sizeof(szOutput) / sizeof(SI_CHAR)); + + // actually add it + return AddEntry(a_pSection, a_pKey, szOutput, a_pComment, a_bForceReplace, true); +} + +template +bool +CSimpleIniTempl::GetAllValues( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + TNamesDepend & a_values + ) const +{ + a_values.clear(); + + if (!a_pSection || !a_pKey) { + return false; + } + typename TSection::const_iterator iSection = m_data.find(a_pSection); + if (iSection == m_data.end()) { + return false; + } + typename TKeyVal::const_iterator iKeyVal = iSection->second.find(a_pKey); + if (iKeyVal == iSection->second.end()) { + return false; + } + + // insert all values for this key + a_values.push_back(Entry(iKeyVal->second, iKeyVal->first.pComment, iKeyVal->first.nOrder)); + if (m_bAllowMultiKey) { + ++iKeyVal; + while (iKeyVal != iSection->second.end() && !IsLess(a_pKey, iKeyVal->first.pItem)) { + a_values.push_back(Entry(iKeyVal->second, iKeyVal->first.pComment, iKeyVal->first.nOrder)); + ++iKeyVal; + } + } + + return true; +} + +template +int +CSimpleIniTempl::GetSectionSize( + const SI_CHAR * a_pSection + ) const +{ + if (!a_pSection) { + return -1; + } + + typename TSection::const_iterator iSection = m_data.find(a_pSection); + if (iSection == m_data.end()) { + return -1; + } + const TKeyVal & section = iSection->second; + + // if multi-key isn't permitted then the section size is + // the number of keys that we have. + if (!m_bAllowMultiKey || section.empty()) { + return (int) section.size(); + } + + // otherwise we need to count them + int nCount = 0; + const SI_CHAR * pLastKey = NULL; + typename TKeyVal::const_iterator iKeyVal = section.begin(); + for (int n = 0; iKeyVal != section.end(); ++iKeyVal, ++n) { + if (!pLastKey || IsLess(pLastKey, iKeyVal->first.pItem)) { + ++nCount; + pLastKey = iKeyVal->first.pItem; + } + } + return nCount; +} + +template +const typename CSimpleIniTempl::TKeyVal * +CSimpleIniTempl::GetSection( + const SI_CHAR * a_pSection + ) const +{ + if (a_pSection) { + typename TSection::const_iterator i = m_data.find(a_pSection); + if (i != m_data.end()) { + return &(i->second); + } + } + return 0; +} + +template +void +CSimpleIniTempl::GetAllSections( + TNamesDepend & a_names + ) const +{ + a_names.clear(); + typename TSection::const_iterator i = m_data.begin(); + for (int n = 0; i != m_data.end(); ++i, ++n ) { + a_names.push_back(i->first); + } +} + +template +bool +CSimpleIniTempl::GetAllKeys( + const SI_CHAR * a_pSection, + TNamesDepend & a_names + ) const +{ + a_names.clear(); + + if (!a_pSection) { + return false; + } + + typename TSection::const_iterator iSection = m_data.find(a_pSection); + if (iSection == m_data.end()) { + return false; + } + + const TKeyVal & section = iSection->second; + const SI_CHAR * pLastKey = NULL; + typename TKeyVal::const_iterator iKeyVal = section.begin(); + for (int n = 0; iKeyVal != section.end(); ++iKeyVal, ++n ) { + if (!pLastKey || IsLess(pLastKey, iKeyVal->first.pItem)) { + a_names.push_back(iKeyVal->first); + pLastKey = iKeyVal->first.pItem; + } + } + + return true; +} + +template +SI_Error +CSimpleIniTempl::SaveFile( + const char * a_pszFile, + bool a_bAddSignature + ) const +{ + FILE * fp = NULL; +#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE + fopen_s(&fp, a_pszFile, "wb"); +#else // !__STDC_WANT_SECURE_LIB__ + fp = fopen(a_pszFile, "wb"); +#endif // __STDC_WANT_SECURE_LIB__ + if (!fp) return SI_FILE; + SI_Error rc = SaveFile(fp, a_bAddSignature); + fclose(fp); + return rc; +} + +#ifdef SI_HAS_WIDE_FILE +template +SI_Error +CSimpleIniTempl::SaveFile( + const SI_WCHAR_T * a_pwszFile, + bool a_bAddSignature + ) const +{ +#ifdef _WIN32 + FILE * fp = NULL; +#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE + _wfopen_s(&fp, a_pwszFile, L"wb"); +#else // !__STDC_WANT_SECURE_LIB__ + fp = _wfopen(a_pwszFile, L"wb"); +#endif // __STDC_WANT_SECURE_LIB__ + if (!fp) return SI_FILE; + SI_Error rc = SaveFile(fp, a_bAddSignature); + fclose(fp); + return rc; +#else // !_WIN32 (therefore SI_CONVERT_ICU) + char szFile[256]; + u_austrncpy(szFile, a_pwszFile, sizeof(szFile)); + return SaveFile(szFile, a_bAddSignature); +#endif // _WIN32 +} +#endif // SI_HAS_WIDE_FILE + +template +SI_Error +CSimpleIniTempl::SaveFile( + FILE * a_pFile, + bool a_bAddSignature + ) const +{ + FileWriter writer(a_pFile); + return Save(writer, a_bAddSignature); +} + +template +SI_Error +CSimpleIniTempl::Save( + OutputWriter & a_oOutput, + bool a_bAddSignature + ) const +{ + Converter convert(m_bStoreIsUtf8); + + // add the UTF-8 signature if it is desired + if (m_bStoreIsUtf8 && a_bAddSignature) { + a_oOutput.Write(SI_UTF8_SIGNATURE); + } + + // get all of the sections sorted in load order + TNamesDepend oSections; + GetAllSections(oSections); +#if defined(_MSC_VER) && _MSC_VER <= 1200 + oSections.sort(); +#elif defined(__BORLANDC__) + oSections.sort(Entry::LoadOrder()); +#else + oSections.sort(typename Entry::LoadOrder()); +#endif + + // write the file comment if we have one + bool bNeedNewLine = false; + if (m_pFileComment) { + if (!OutputMultiLineText(a_oOutput, convert, m_pFileComment)) { + return SI_FAIL; + } + bNeedNewLine = true; + } + + // iterate through our sections and output the data + typename TNamesDepend::const_iterator iSection = oSections.begin(); + for ( ; iSection != oSections.end(); ++iSection ) { + // write out the comment if there is one + if (iSection->pComment) { + if (bNeedNewLine) { + a_oOutput.Write(SI_NEWLINE_A); + a_oOutput.Write(SI_NEWLINE_A); + } + if (!OutputMultiLineText(a_oOutput, convert, iSection->pComment)) { + return SI_FAIL; + } + bNeedNewLine = false; + } + + if (bNeedNewLine) { + a_oOutput.Write(SI_NEWLINE_A); + a_oOutput.Write(SI_NEWLINE_A); + bNeedNewLine = false; + } + + // write the section (unless there is no section name) + if (*iSection->pItem) { + if (!convert.ConvertToStore(iSection->pItem)) { + return SI_FAIL; + } + a_oOutput.Write("["); + a_oOutput.Write(convert.Data()); + a_oOutput.Write("]"); + a_oOutput.Write(SI_NEWLINE_A); + } + + // get all of the keys sorted in load order + TNamesDepend oKeys; + GetAllKeys(iSection->pItem, oKeys); +#if defined(_MSC_VER) && _MSC_VER <= 1200 + oKeys.sort(); +#elif defined(__BORLANDC__) + oKeys.sort(Entry::LoadOrder()); +#else + oKeys.sort(typename Entry::LoadOrder()); +#endif + + // write all keys and values + typename TNamesDepend::const_iterator iKey = oKeys.begin(); + for ( ; iKey != oKeys.end(); ++iKey) { + // get all values for this key + TNamesDepend oValues; + GetAllValues(iSection->pItem, iKey->pItem, oValues); + + typename TNamesDepend::const_iterator iValue = oValues.begin(); + for ( ; iValue != oValues.end(); ++iValue) { + // write out the comment if there is one + if (iValue->pComment) { + a_oOutput.Write(SI_NEWLINE_A); + if (!OutputMultiLineText(a_oOutput, convert, iValue->pComment)) { + return SI_FAIL; + } + } + + // write the key + if (!convert.ConvertToStore(iKey->pItem)) { + return SI_FAIL; + } + a_oOutput.Write(convert.Data()); + + // write the value + if (!convert.ConvertToStore(iValue->pItem)) { + return SI_FAIL; + } + a_oOutput.Write(m_bSpaces ? " = " : "="); + if (m_bAllowMultiLine && IsMultiLineData(iValue->pItem)) { + // multi-line data needs to be processed specially to ensure + // that we use the correct newline format for the current system + a_oOutput.Write("<<pItem)) { + return SI_FAIL; + } + a_oOutput.Write("END_OF_TEXT"); + } + else { + a_oOutput.Write(convert.Data()); + } + a_oOutput.Write(SI_NEWLINE_A); + } + } + + bNeedNewLine = true; + } + + return SI_OK; +} + +template +bool +CSimpleIniTempl::OutputMultiLineText( + OutputWriter & a_oOutput, + Converter & a_oConverter, + const SI_CHAR * a_pText + ) const +{ + const SI_CHAR * pEndOfLine; + SI_CHAR cEndOfLineChar = *a_pText; + while (cEndOfLineChar) { + // find the end of this line + pEndOfLine = a_pText; + for (; *pEndOfLine && *pEndOfLine != '\n'; ++pEndOfLine) /*loop*/ ; + cEndOfLineChar = *pEndOfLine; + + // temporarily null terminate, convert and output the line + *const_cast(pEndOfLine) = 0; + if (!a_oConverter.ConvertToStore(a_pText)) { + return false; + } + *const_cast(pEndOfLine) = cEndOfLineChar; + a_pText += (pEndOfLine - a_pText) + 1; + a_oOutput.Write(a_oConverter.Data()); + a_oOutput.Write(SI_NEWLINE_A); + } + return true; +} + +template +bool +CSimpleIniTempl::Delete( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + bool a_bRemoveEmpty + ) +{ + if (!a_pSection) { + return false; + } + + typename TSection::iterator iSection = m_data.find(a_pSection); + if (iSection == m_data.end()) { + return false; + } + + // remove a single key if we have a keyname + if (a_pKey) { + typename TKeyVal::iterator iKeyVal = iSection->second.find(a_pKey); + if (iKeyVal == iSection->second.end()) { + return false; + } + + // remove any copied strings and then the key + typename TKeyVal::iterator iDelete; + do { + iDelete = iKeyVal++; + + DeleteString(iDelete->first.pItem); + DeleteString(iDelete->second); + iSection->second.erase(iDelete); + } + while (iKeyVal != iSection->second.end() + && !IsLess(a_pKey, iKeyVal->first.pItem)); + + // done now if the section is not empty or we are not pruning away + // the empty sections. Otherwise let it fall through into the section + // deletion code + if (!a_bRemoveEmpty || !iSection->second.empty()) { + return true; + } + } + else { + // delete all copied strings from this section. The actual + // entries will be removed when the section is removed. + typename TKeyVal::iterator iKeyVal = iSection->second.begin(); + for ( ; iKeyVal != iSection->second.end(); ++iKeyVal) { + DeleteString(iKeyVal->first.pItem); + DeleteString(iKeyVal->second); + } + } + + // delete the section itself + DeleteString(iSection->first.pItem); + m_data.erase(iSection); + + return true; +} + +template +void +CSimpleIniTempl::DeleteString( + const SI_CHAR * a_pString + ) +{ + // strings may exist either inside the data block, or they will be + // individually allocated and stored in m_strings. We only physically + // delete those stored in m_strings. + if (a_pString < m_pData || a_pString >= m_pData + m_uDataLen) { + typename TNamesDepend::iterator i = m_strings.begin(); + for (;i != m_strings.end(); ++i) { + if (a_pString == i->pItem) { + delete[] const_cast(i->pItem); + m_strings.erase(i); + break; + } + } + } +} + +// --------------------------------------------------------------------------- +// CONVERSION FUNCTIONS +// --------------------------------------------------------------------------- + +// Defines the conversion classes for different libraries. Before including +// SimpleIni.h, set the converter that you wish you use by defining one of the +// following symbols. +// +// SI_CONVERT_GENERIC Use the Unicode reference conversion library in +// the accompanying files ConvertUTF.h/c +// SI_CONVERT_ICU Use the IBM ICU conversion library. Requires +// ICU headers on include path and icuuc.lib +// SI_CONVERT_WIN32 Use the Win32 API functions for conversion. + +#if !defined(SI_CONVERT_GENERIC) && !defined(SI_CONVERT_WIN32) && !defined(SI_CONVERT_ICU) +# ifdef _WIN32 +# define SI_CONVERT_WIN32 +# else +# define SI_CONVERT_GENERIC +# endif +#endif + +/** + * Generic case-sensitive less than comparison. This class returns numerically + * ordered ASCII case-sensitive text for all possible sizes and types of + * SI_CHAR. + */ +template +struct SI_GenericCase { + bool operator()(const SI_CHAR * pLeft, const SI_CHAR * pRight) const { + long cmp; + for ( ;*pLeft && *pRight; ++pLeft, ++pRight) { + cmp = (long) *pLeft - (long) *pRight; + if (cmp != 0) { + return cmp < 0; + } + } + return *pRight != 0; + } +}; + +/** + * Generic ASCII case-insensitive less than comparison. This class returns + * numerically ordered ASCII case-insensitive text for all possible sizes + * and types of SI_CHAR. It is not safe for MBCS text comparison where + * ASCII A-Z characters are used in the encoding of multi-byte characters. + */ +template +struct SI_GenericNoCase { + inline SI_CHAR locase(SI_CHAR ch) const { + return (ch < 'A' || ch > 'Z') ? ch : (ch - 'A' + 'a'); + } + bool operator()(const SI_CHAR * pLeft, const SI_CHAR * pRight) const { + long cmp; + for ( ;*pLeft && *pRight; ++pLeft, ++pRight) { + cmp = (long) locase(*pLeft) - (long) locase(*pRight); + if (cmp != 0) { + return cmp < 0; + } + } + return *pRight != 0; + } +}; + +/** + * Null conversion class for MBCS/UTF-8 to char (or equivalent). + */ +template +class SI_ConvertA { + bool m_bStoreIsUtf8; +protected: + SI_ConvertA() { } +public: + SI_ConvertA(bool a_bStoreIsUtf8) : m_bStoreIsUtf8(a_bStoreIsUtf8) { } + + /* copy and assignment */ + SI_ConvertA(const SI_ConvertA & rhs) { operator=(rhs); } + SI_ConvertA & operator=(const SI_ConvertA & rhs) { + m_bStoreIsUtf8 = rhs.m_bStoreIsUtf8; + return *this; + } + + /** Calculate the number of SI_CHAR required for converting the input + * from the storage format. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to SI_CHAR. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @return Number of SI_CHAR required by the string when + * converted. If there are embedded NULL bytes in the + * input data, only the string up and not including + * the NULL byte will be converted. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeFromStore( + const char * a_pInputData, + size_t a_uInputDataLen) + { + (void)a_pInputData; + SI_ASSERT(a_uInputDataLen != (size_t) -1); + + // ASCII/MBCS/UTF-8 needs no conversion + return a_uInputDataLen; + } + + /** Convert the input string from the storage format to SI_CHAR. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to SI_CHAR. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @param a_pOutputData Pointer to the output buffer to received the + * converted data. + * @param a_uOutputDataSize Size of the output buffer in SI_CHAR. + * @return true if all of the input data was successfully + * converted. + */ + bool ConvertFromStore( + const char * a_pInputData, + size_t a_uInputDataLen, + SI_CHAR * a_pOutputData, + size_t a_uOutputDataSize) + { + // ASCII/MBCS/UTF-8 needs no conversion + if (a_uInputDataLen > a_uOutputDataSize) { + return false; + } + memcpy(a_pOutputData, a_pInputData, a_uInputDataLen); + return true; + } + + /** Calculate the number of char required by the storage format of this + * data. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated string to calculate the number of + * bytes required to be converted to storage format. + * @return Number of bytes required by the string when + * converted to storage format. This size always + * includes space for the terminating NULL character. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeToStore( + const SI_CHAR * a_pInputData) + { + // ASCII/MBCS/UTF-8 needs no conversion + return strlen((const char *)a_pInputData) + 1; + } + + /** Convert the input string to the storage format of this data. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated source string to convert. All of + * the data will be converted including the + * terminating NULL character. + * @param a_pOutputData Pointer to the buffer to receive the converted + * string. + * @param a_uOutputDataSize Size of the output buffer in char. + * @return true if all of the input data, including the + * terminating NULL character was successfully + * converted. + */ + bool ConvertToStore( + const SI_CHAR * a_pInputData, + char * a_pOutputData, + size_t a_uOutputDataSize) + { + // calc input string length (SI_CHAR type and size independent) + size_t uInputLen = strlen((const char *)a_pInputData) + 1; + if (uInputLen > a_uOutputDataSize) { + return false; + } + + // ascii/UTF-8 needs no conversion + memcpy(a_pOutputData, a_pInputData, uInputLen); + return true; + } +}; + + +// --------------------------------------------------------------------------- +// SI_CONVERT_GENERIC +// --------------------------------------------------------------------------- +#ifdef SI_CONVERT_GENERIC + +#define SI_Case SI_GenericCase +#define SI_NoCase SI_GenericNoCase + +#include +#include "ConvertUTF.h" + +/** + * Converts UTF-8 to a wchar_t (or equivalent) using the Unicode reference + * library functions. This can be used on all platforms. + */ +template +class SI_ConvertW { + bool m_bStoreIsUtf8; +protected: + SI_ConvertW() { } +public: + SI_ConvertW(bool a_bStoreIsUtf8) : m_bStoreIsUtf8(a_bStoreIsUtf8) { } + + /* copy and assignment */ + SI_ConvertW(const SI_ConvertW & rhs) { operator=(rhs); } + SI_ConvertW & operator=(const SI_ConvertW & rhs) { + m_bStoreIsUtf8 = rhs.m_bStoreIsUtf8; + return *this; + } + + /** Calculate the number of SI_CHAR required for converting the input + * from the storage format. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to SI_CHAR. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @return Number of SI_CHAR required by the string when + * converted. If there are embedded NULL bytes in the + * input data, only the string up and not including + * the NULL byte will be converted. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeFromStore( + const char * a_pInputData, + size_t a_uInputDataLen) + { + SI_ASSERT(a_uInputDataLen != (size_t) -1); + + if (m_bStoreIsUtf8) { + // worst case scenario for UTF-8 to wchar_t is 1 char -> 1 wchar_t + // so we just return the same number of characters required as for + // the source text. + return a_uInputDataLen; + } + +#if defined(SI_NO_MBSTOWCS_NULL) || (!defined(_MSC_VER) && !defined(_linux)) + // fall back processing for platforms that don't support a NULL dest to mbstowcs + // worst case scenario is 1:1, this will be a sufficient buffer size + (void)a_pInputData; + return a_uInputDataLen; +#else + // get the actual required buffer size + return mbstowcs(NULL, a_pInputData, a_uInputDataLen); +#endif + } + + /** Convert the input string from the storage format to SI_CHAR. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to SI_CHAR. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @param a_pOutputData Pointer to the output buffer to received the + * converted data. + * @param a_uOutputDataSize Size of the output buffer in SI_CHAR. + * @return true if all of the input data was successfully + * converted. + */ + bool ConvertFromStore( + const char * a_pInputData, + size_t a_uInputDataLen, + SI_CHAR * a_pOutputData, + size_t a_uOutputDataSize) + { + if (m_bStoreIsUtf8) { + // This uses the Unicode reference implementation to do the + // conversion from UTF-8 to wchar_t. The required files are + // ConvertUTF.h and ConvertUTF.c which should be included in + // the distribution but are publically available from unicode.org + // at http://www.unicode.org/Public/PROGRAMS/CVTUTF/ + ConversionResult retval; + const UTF8 * pUtf8 = (const UTF8 *) a_pInputData; + if (sizeof(wchar_t) == sizeof(UTF32)) { + UTF32 * pUtf32 = (UTF32 *) a_pOutputData; + retval = ConvertUTF8toUTF32( + &pUtf8, pUtf8 + a_uInputDataLen, + &pUtf32, pUtf32 + a_uOutputDataSize, + lenientConversion); + } + else if (sizeof(wchar_t) == sizeof(UTF16)) { + UTF16 * pUtf16 = (UTF16 *) a_pOutputData; + retval = ConvertUTF8toUTF16( + &pUtf8, pUtf8 + a_uInputDataLen, + &pUtf16, pUtf16 + a_uOutputDataSize, + lenientConversion); + } + return retval == conversionOK; + } + + // convert to wchar_t + size_t retval = mbstowcs(a_pOutputData, + a_pInputData, a_uOutputDataSize); + return retval != (size_t)(-1); + } + + /** Calculate the number of char required by the storage format of this + * data. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated string to calculate the number of + * bytes required to be converted to storage format. + * @return Number of bytes required by the string when + * converted to storage format. This size always + * includes space for the terminating NULL character. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeToStore( + const SI_CHAR * a_pInputData) + { + if (m_bStoreIsUtf8) { + // worst case scenario for wchar_t to UTF-8 is 1 wchar_t -> 6 char + size_t uLen = 0; + while (a_pInputData[uLen]) { + ++uLen; + } + return (6 * uLen) + 1; + } + else { + size_t uLen = wcstombs(NULL, a_pInputData, 0); + if (uLen == (size_t)(-1)) { + return uLen; + } + return uLen + 1; // include NULL terminator + } + } + + /** Convert the input string to the storage format of this data. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated source string to convert. All of + * the data will be converted including the + * terminating NULL character. + * @param a_pOutputData Pointer to the buffer to receive the converted + * string. + * @param a_uOutputDataSize Size of the output buffer in char. + * @return true if all of the input data, including the + * terminating NULL character was successfully + * converted. + */ + bool ConvertToStore( + const SI_CHAR * a_pInputData, + char * a_pOutputData, + size_t a_uOutputDataSize + ) + { + if (m_bStoreIsUtf8) { + // calc input string length (SI_CHAR type and size independent) + size_t uInputLen = 0; + while (a_pInputData[uInputLen]) { + ++uInputLen; + } + ++uInputLen; // include the NULL char + + // This uses the Unicode reference implementation to do the + // conversion from wchar_t to UTF-8. The required files are + // ConvertUTF.h and ConvertUTF.c which should be included in + // the distribution but are publically available from unicode.org + // at http://www.unicode.org/Public/PROGRAMS/CVTUTF/ + ConversionResult retval; + UTF8 * pUtf8 = (UTF8 *) a_pOutputData; + if (sizeof(wchar_t) == sizeof(UTF32)) { + const UTF32 * pUtf32 = (const UTF32 *) a_pInputData; + retval = ConvertUTF32toUTF8( + &pUtf32, pUtf32 + uInputLen, + &pUtf8, pUtf8 + a_uOutputDataSize, + lenientConversion); + } + else if (sizeof(wchar_t) == sizeof(UTF16)) { + const UTF16 * pUtf16 = (const UTF16 *) a_pInputData; + retval = ConvertUTF16toUTF8( + &pUtf16, pUtf16 + uInputLen, + &pUtf8, pUtf8 + a_uOutputDataSize, + lenientConversion); + } + return retval == conversionOK; + } + else { + size_t retval = wcstombs(a_pOutputData, + a_pInputData, a_uOutputDataSize); + return retval != (size_t) -1; + } + } +}; + +#endif // SI_CONVERT_GENERIC + + +// --------------------------------------------------------------------------- +// SI_CONVERT_ICU +// --------------------------------------------------------------------------- +#ifdef SI_CONVERT_ICU + +#define SI_Case SI_GenericCase +#define SI_NoCase SI_GenericNoCase + +#include + +/** + * Converts MBCS/UTF-8 to UChar using ICU. This can be used on all platforms. + */ +template +class SI_ConvertW { + const char * m_pEncoding; + UConverter * m_pConverter; +protected: + SI_ConvertW() : m_pEncoding(NULL), m_pConverter(NULL) { } +public: + SI_ConvertW(bool a_bStoreIsUtf8) : m_pConverter(NULL) { + m_pEncoding = a_bStoreIsUtf8 ? "UTF-8" : NULL; + } + + /* copy and assignment */ + SI_ConvertW(const SI_ConvertW & rhs) { operator=(rhs); } + SI_ConvertW & operator=(const SI_ConvertW & rhs) { + m_pEncoding = rhs.m_pEncoding; + m_pConverter = NULL; + return *this; + } + ~SI_ConvertW() { if (m_pConverter) ucnv_close(m_pConverter); } + + /** Calculate the number of UChar required for converting the input + * from the storage format. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to UChar. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @return Number of UChar required by the string when + * converted. If there are embedded NULL bytes in the + * input data, only the string up and not including + * the NULL byte will be converted. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeFromStore( + const char * a_pInputData, + size_t a_uInputDataLen) + { + SI_ASSERT(a_uInputDataLen != (size_t) -1); + + UErrorCode nError; + + if (!m_pConverter) { + nError = U_ZERO_ERROR; + m_pConverter = ucnv_open(m_pEncoding, &nError); + if (U_FAILURE(nError)) { + return (size_t) -1; + } + } + + nError = U_ZERO_ERROR; + int32_t nLen = ucnv_toUChars(m_pConverter, NULL, 0, + a_pInputData, (int32_t) a_uInputDataLen, &nError); + if (U_FAILURE(nError) && nError != U_BUFFER_OVERFLOW_ERROR) { + return (size_t) -1; + } + + return (size_t) nLen; + } + + /** Convert the input string from the storage format to UChar. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to UChar. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @param a_pOutputData Pointer to the output buffer to received the + * converted data. + * @param a_uOutputDataSize Size of the output buffer in UChar. + * @return true if all of the input data was successfully + * converted. + */ + bool ConvertFromStore( + const char * a_pInputData, + size_t a_uInputDataLen, + UChar * a_pOutputData, + size_t a_uOutputDataSize) + { + UErrorCode nError; + + if (!m_pConverter) { + nError = U_ZERO_ERROR; + m_pConverter = ucnv_open(m_pEncoding, &nError); + if (U_FAILURE(nError)) { + return false; + } + } + + nError = U_ZERO_ERROR; + ucnv_toUChars(m_pConverter, + a_pOutputData, (int32_t) a_uOutputDataSize, + a_pInputData, (int32_t) a_uInputDataLen, &nError); + if (U_FAILURE(nError)) { + return false; + } + + return true; + } + + /** Calculate the number of char required by the storage format of this + * data. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated string to calculate the number of + * bytes required to be converted to storage format. + * @return Number of bytes required by the string when + * converted to storage format. This size always + * includes space for the terminating NULL character. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeToStore( + const UChar * a_pInputData) + { + UErrorCode nError; + + if (!m_pConverter) { + nError = U_ZERO_ERROR; + m_pConverter = ucnv_open(m_pEncoding, &nError); + if (U_FAILURE(nError)) { + return (size_t) -1; + } + } + + nError = U_ZERO_ERROR; + int32_t nLen = ucnv_fromUChars(m_pConverter, NULL, 0, + a_pInputData, -1, &nError); + if (U_FAILURE(nError) && nError != U_BUFFER_OVERFLOW_ERROR) { + return (size_t) -1; + } + + return (size_t) nLen + 1; + } + + /** Convert the input string to the storage format of this data. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated source string to convert. All of + * the data will be converted including the + * terminating NULL character. + * @param a_pOutputData Pointer to the buffer to receive the converted + * string. + * @param a_pOutputDataSize Size of the output buffer in char. + * @return true if all of the input data, including the + * terminating NULL character was successfully + * converted. + */ + bool ConvertToStore( + const UChar * a_pInputData, + char * a_pOutputData, + size_t a_uOutputDataSize) + { + UErrorCode nError; + + if (!m_pConverter) { + nError = U_ZERO_ERROR; + m_pConverter = ucnv_open(m_pEncoding, &nError); + if (U_FAILURE(nError)) { + return false; + } + } + + nError = U_ZERO_ERROR; + ucnv_fromUChars(m_pConverter, + a_pOutputData, (int32_t) a_uOutputDataSize, + a_pInputData, -1, &nError); + if (U_FAILURE(nError)) { + return false; + } + + return true; + } +}; + +#endif // SI_CONVERT_ICU + + +// --------------------------------------------------------------------------- +// SI_CONVERT_WIN32 +// --------------------------------------------------------------------------- +#ifdef SI_CONVERT_WIN32 + +#define SI_Case SI_GenericCase + +// Windows CE doesn't have errno or MBCS libraries +#ifdef _WIN32_WCE +# ifndef SI_NO_MBCS +# define SI_NO_MBCS +# endif +#endif + +#include +#ifdef SI_NO_MBCS +# define SI_NoCase SI_GenericNoCase +#else // !SI_NO_MBCS +/** + * Case-insensitive comparison class using Win32 MBCS functions. This class + * returns a case-insensitive semi-collation order for MBCS text. It may not + * be safe for UTF-8 text returned in char format as we don't know what + * characters will be folded by the function! Therefore, if you are using + * SI_CHAR == char and SetUnicode(true), then you need to use the generic + * SI_NoCase class instead. + */ +#include +template +struct SI_NoCase { + bool operator()(const SI_CHAR * pLeft, const SI_CHAR * pRight) const { + if (sizeof(SI_CHAR) == sizeof(char)) { + return _mbsicmp((const unsigned char *)pLeft, + (const unsigned char *)pRight) < 0; + } + if (sizeof(SI_CHAR) == sizeof(wchar_t)) { + return _wcsicmp((const wchar_t *)pLeft, + (const wchar_t *)pRight) < 0; + } + return SI_GenericNoCase()(pLeft, pRight); + } +}; +#endif // SI_NO_MBCS + +/** + * Converts MBCS and UTF-8 to a wchar_t (or equivalent) on Windows. This uses + * only the Win32 functions and doesn't require the external Unicode UTF-8 + * conversion library. It will not work on Windows 95 without using Microsoft + * Layer for Unicode in your application. + */ +template +class SI_ConvertW { + UINT m_uCodePage; +protected: + SI_ConvertW() { } +public: + SI_ConvertW(bool a_bStoreIsUtf8) { + m_uCodePage = a_bStoreIsUtf8 ? CP_UTF8 : CP_ACP; + } + + /* copy and assignment */ + SI_ConvertW(const SI_ConvertW & rhs) { operator=(rhs); } + SI_ConvertW & operator=(const SI_ConvertW & rhs) { + m_uCodePage = rhs.m_uCodePage; + return *this; + } + + /** Calculate the number of SI_CHAR required for converting the input + * from the storage format. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to SI_CHAR. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @return Number of SI_CHAR required by the string when + * converted. If there are embedded NULL bytes in the + * input data, only the string up and not including + * the NULL byte will be converted. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeFromStore( + const char * a_pInputData, + size_t a_uInputDataLen) + { + SI_ASSERT(a_uInputDataLen != (size_t) -1); + + int retval = MultiByteToWideChar( + m_uCodePage, 0, + a_pInputData, (int) a_uInputDataLen, + 0, 0); + return (size_t)(retval > 0 ? retval : -1); + } + + /** Convert the input string from the storage format to SI_CHAR. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to SI_CHAR. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @param a_pOutputData Pointer to the output buffer to received the + * converted data. + * @param a_uOutputDataSize Size of the output buffer in SI_CHAR. + * @return true if all of the input data was successfully + * converted. + */ + bool ConvertFromStore( + const char * a_pInputData, + size_t a_uInputDataLen, + SI_CHAR * a_pOutputData, + size_t a_uOutputDataSize) + { + int nSize = MultiByteToWideChar( + m_uCodePage, 0, + a_pInputData, (int) a_uInputDataLen, + (wchar_t *) a_pOutputData, (int) a_uOutputDataSize); + return (nSize > 0); + } + + /** Calculate the number of char required by the storage format of this + * data. The storage format is always UTF-8. + * + * @param a_pInputData NULL terminated string to calculate the number of + * bytes required to be converted to storage format. + * @return Number of bytes required by the string when + * converted to storage format. This size always + * includes space for the terminating NULL character. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeToStore( + const SI_CHAR * a_pInputData) + { + int retval = WideCharToMultiByte( + m_uCodePage, 0, + (const wchar_t *) a_pInputData, -1, + 0, 0, 0, 0); + return (size_t) (retval > 0 ? retval : -1); + } + + /** Convert the input string to the storage format of this data. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated source string to convert. All of + * the data will be converted including the + * terminating NULL character. + * @param a_pOutputData Pointer to the buffer to receive the converted + * string. + * @param a_pOutputDataSize Size of the output buffer in char. + * @return true if all of the input data, including the + * terminating NULL character was successfully + * converted. + */ + bool ConvertToStore( + const SI_CHAR * a_pInputData, + char * a_pOutputData, + size_t a_uOutputDataSize) + { + int retval = WideCharToMultiByte( + m_uCodePage, 0, + (const wchar_t *) a_pInputData, -1, + a_pOutputData, (int) a_uOutputDataSize, 0, 0); + return retval > 0; + } +}; + +#endif // SI_CONVERT_WIN32 + + +// --------------------------------------------------------------------------- +// TYPE DEFINITIONS +// --------------------------------------------------------------------------- + +typedef CSimpleIniTempl,SI_ConvertA > CSimpleIniA; +typedef CSimpleIniTempl,SI_ConvertA > CSimpleIniCaseA; + +#if defined(SI_CONVERT_ICU) +typedef CSimpleIniTempl,SI_ConvertW > CSimpleIniW; +typedef CSimpleIniTempl,SI_ConvertW > CSimpleIniCaseW; +#else +typedef CSimpleIniTempl,SI_ConvertW > CSimpleIniW; +typedef CSimpleIniTempl,SI_ConvertW > CSimpleIniCaseW; +#endif + +#ifdef _UNICODE +# define CSimpleIni CSimpleIniW +# define CSimpleIniCase CSimpleIniCaseW +# define SI_NEWLINE SI_NEWLINE_W +#else // !_UNICODE +# define CSimpleIni CSimpleIniA +# define CSimpleIniCase CSimpleIniCaseA +# define SI_NEWLINE SI_NEWLINE_A +#endif // _UNICODE + +#ifdef _MSC_VER +# pragma warning (pop) +#endif + +#endif // INCLUDED_SimpleIni_h + diff --git a/src/openalpr/simpleini/snippets.cpp b/src/openalpr/simpleini/snippets.cpp new file mode 100644 index 0000000..e2295fe --- /dev/null +++ b/src/openalpr/simpleini/snippets.cpp @@ -0,0 +1,123 @@ +// File: snippets.cpp +// Library: SimpleIni +// Author: Brodie Thiesfield +// Source: http://code.jellycan.com/simpleini/ +// +// Snippets that are used on the website + +#ifdef _WIN32 +# pragma warning(disable: 4786) +#endif + +#ifndef _WIN32 +# include +#endif +#include + +#define SI_SUPPORT_IOSTREAMS +#include "SimpleIni.h" + +bool +snippets( + const char * a_pszFile, + bool a_bIsUtf8, + bool a_bUseMultiKey, + bool a_bUseMultiLine + ) +{ + // LOADING DATA + + // load from a data file + CSimpleIniA ini(a_bIsUtf8, a_bUseMultiKey, a_bUseMultiLine); + SI_Error rc = ini.LoadFile(a_pszFile); + if (rc < 0) return false; + + // load from a string + std::string strData; + rc = ini.LoadData(strData.c_str(), strData.size()); + if (rc < 0) return false; + + // GETTING SECTIONS AND KEYS + + // get all sections + CSimpleIniA::TNamesDepend sections; + ini.GetAllSections(sections); + + // get all keys in a section + CSimpleIniA::TNamesDepend keys; + ini.GetAllKeys("section-name", keys); + + // GETTING VALUES + + // get the value of a key + const char * pszValue = ini.GetValue("section-name", + "key-name", NULL /*default*/); + + // get the value of a key which may have multiple + // values. If bHasMultipleValues is true, then just + // one value has been returned + bool bHasMultipleValues; + pszValue = ini.GetValue("section-name", "key-name", + NULL /*default*/, &bHasMultipleValues); + + // get all values of a key with multiple values + CSimpleIniA::TNamesDepend values; + ini.GetAllValues("section-name", "key-name", values); + + // sort the values into the original load order +#if defined(_MSC_VER) && _MSC_VER <= 1200 + /** STL of VC6 doesn't allow me to specify my own comparator for list::sort() */ + values.sort(); +#else + values.sort(CSimpleIniA::Entry::LoadOrder()); +#endif + + // output all of the items + CSimpleIniA::TNamesDepend::const_iterator i; + for (i = values.begin(); i != values.end(); ++i) { + printf("key-name = '%s'\n", i->pItem); + } + + // MODIFYING DATA + + // adding a new section + rc = ini.SetValue("new-section", NULL, NULL); + if (rc < 0) return false; + printf("section: %s\n", rc == SI_INSERTED ? + "inserted" : "updated"); + + // adding a new key ("new-section" will be added + // automatically if it doesn't already exist. + rc = ini.SetValue("new-section", "new-key", "value"); + if (rc < 0) return false; + printf("key: %s\n", rc == SI_INSERTED ? + "inserted" : "updated"); + + // changing the value of a key + rc = ini.SetValue("section", "key", "updated-value"); + if (rc < 0) return false; + printf("key: %s\n", rc == SI_INSERTED ? + "inserted" : "updated"); + + // DELETING DATA + + // deleting a key from a section. Optionally the entire + // section may be deleted if it is now empty. + ini.Delete("section-name", "key-name", + true /*delete the section if empty*/); + + // deleting an entire section and all keys in it + ini.Delete("section-name", NULL); + + // SAVING DATA + + // save the data to a string + rc = ini.Save(strData); + if (rc < 0) return false; + + // save the data back to the file + rc = ini.SaveFile(a_pszFile); + if (rc < 0) return false; + + return true; +} From aece11e1a2e49b88c99b1155af5eb6dc7c2188e2 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 2 Jan 2014 16:02:21 -0600 Subject: [PATCH 04/14] Added OpenALPR library --- src/openalpr/TRexpp.h | 75 ++ src/openalpr/alpr.cpp | 89 ++ src/openalpr/alpr.h | 88 ++ src/openalpr/alpr_impl.cpp | 229 +++++ src/openalpr/alpr_impl.h | 73 ++ src/openalpr/binarize_wolf.cpp | 372 ++++++++ src/openalpr/binarize_wolf.h | 57 ++ src/openalpr/characteranalysis.cpp | 1040 +++++++++++++++++++++ src/openalpr/characteranalysis.h | 104 +++ src/openalpr/characterregion.cpp | 194 ++++ src/openalpr/characterregion.h | 87 ++ src/openalpr/charactersegmenter.cpp | 1188 ++++++++++++++++++++++++ src/openalpr/charactersegmenter.h | 106 +++ src/openalpr/cjson.c | 596 ++++++++++++ src/openalpr/cjson.h | 143 +++ src/openalpr/colorfilter.cpp | 421 +++++++++ src/openalpr/colorfilter.h | 65 ++ src/openalpr/config.cpp | 219 +++++ src/openalpr/config.h | 125 +++ src/openalpr/constants.h | 21 + src/openalpr/featurematcher.cpp | 432 +++++++++ src/openalpr/featurematcher.h | 93 ++ src/openalpr/licenseplatecandidate.cpp | 202 ++++ src/openalpr/licenseplatecandidate.h | 83 ++ src/openalpr/linux_dev.h | 28 + src/openalpr/ocr.cpp | 145 +++ src/openalpr/ocr.h | 69 ++ src/openalpr/platecorners.cpp | 447 +++++++++ src/openalpr/platecorners.h | 83 ++ src/openalpr/platelines.cpp | 366 ++++++++ src/openalpr/platelines.h | 62 ++ src/openalpr/postprocess.cpp | 478 ++++++++++ src/openalpr/postprocess.h | 130 +++ src/openalpr/regiondetector.cpp | 115 +++ src/openalpr/regiondetector.h | 68 ++ src/openalpr/stateidentifier.cpp | 98 ++ src/openalpr/stateidentifier.h | 59 ++ src/openalpr/trex.c | 643 +++++++++++++ src/openalpr/trex.h | 67 ++ src/openalpr/utility.cpp | 400 ++++++++ src/openalpr/utility.h | 119 +++ src/openalpr/verticalhistogram.cpp | 155 ++++ src/openalpr/verticalhistogram.h | 62 ++ 43 files changed, 9696 insertions(+) create mode 100644 src/openalpr/TRexpp.h create mode 100644 src/openalpr/alpr.cpp create mode 100644 src/openalpr/alpr.h create mode 100644 src/openalpr/alpr_impl.cpp create mode 100644 src/openalpr/alpr_impl.h create mode 100644 src/openalpr/binarize_wolf.cpp create mode 100644 src/openalpr/binarize_wolf.h create mode 100644 src/openalpr/characteranalysis.cpp create mode 100644 src/openalpr/characteranalysis.h create mode 100644 src/openalpr/characterregion.cpp create mode 100644 src/openalpr/characterregion.h create mode 100644 src/openalpr/charactersegmenter.cpp create mode 100644 src/openalpr/charactersegmenter.h create mode 100644 src/openalpr/cjson.c create mode 100644 src/openalpr/cjson.h create mode 100644 src/openalpr/colorfilter.cpp create mode 100644 src/openalpr/colorfilter.h create mode 100644 src/openalpr/config.cpp create mode 100644 src/openalpr/config.h create mode 100644 src/openalpr/constants.h create mode 100644 src/openalpr/featurematcher.cpp create mode 100644 src/openalpr/featurematcher.h create mode 100644 src/openalpr/licenseplatecandidate.cpp create mode 100644 src/openalpr/licenseplatecandidate.h create mode 100644 src/openalpr/linux_dev.h create mode 100644 src/openalpr/ocr.cpp create mode 100644 src/openalpr/ocr.h create mode 100644 src/openalpr/platecorners.cpp create mode 100644 src/openalpr/platecorners.h create mode 100644 src/openalpr/platelines.cpp create mode 100644 src/openalpr/platelines.h create mode 100644 src/openalpr/postprocess.cpp create mode 100644 src/openalpr/postprocess.h create mode 100644 src/openalpr/regiondetector.cpp create mode 100644 src/openalpr/regiondetector.h create mode 100644 src/openalpr/stateidentifier.cpp create mode 100644 src/openalpr/stateidentifier.h create mode 100644 src/openalpr/trex.c create mode 100644 src/openalpr/trex.h create mode 100644 src/openalpr/utility.cpp create mode 100644 src/openalpr/utility.h create mode 100644 src/openalpr/verticalhistogram.cpp create mode 100644 src/openalpr/verticalhistogram.h diff --git a/src/openalpr/TRexpp.h b/src/openalpr/TRexpp.h new file mode 100644 index 0000000..8391e47 --- /dev/null +++ b/src/openalpr/TRexpp.h @@ -0,0 +1,75 @@ +#ifndef _TREXPP_H_ +#define _TREXPP_H_ +/*************************************************************** + T-Rex a tiny regular expression library + + Copyright (C) 2003-2004 Alberto Demichelis + + This software is provided 'as-is', without any express + or implied warranty. In no event will the authors be held + liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for + any purpose, including commercial applications, and to alter + it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; + you must not claim that you wrote the original software. + If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but + is not required. + + 2. Altered source versions must be plainly marked as such, + and must not be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any + source distribution. + +****************************************************************/ + +extern "C" { +#include "trex.h" +} + +struct TRexParseException{TRexParseException(const TRexChar *c):desc(c){}const TRexChar *desc;}; + +class TRexpp { +public: + TRexpp() { _exp = (TRex *)0; } + ~TRexpp() { CleanUp(); } + // compiles a regular expression + void Compile(const TRexChar *pattern) { + const TRexChar *error; + CleanUp(); + if(!(_exp = trex_compile(pattern,&error))) + throw TRexParseException(error); + } + // return true if the given text match the expression + bool Match(const TRexChar* text) { + return _exp?(trex_match(_exp,text) != 0):false; + } + // Searches for the first match of the expression in a zero terminated string + bool Search(const TRexChar* text, const TRexChar** out_begin, const TRexChar** out_end) { + return _exp?(trex_search(_exp,text,out_begin,out_end) != 0):false; + } + // Searches for the first match of the expression in a string sarting at text_begin and ending at text_end + bool SearchRange(const TRexChar* text_begin,const TRexChar* text_end,const TRexChar** out_begin, const TRexChar** out_end) { + return _exp?(trex_searchrange(_exp,text_begin,text_end,out_begin,out_end) != 0):false; + } + bool GetSubExp(int n, const TRexChar** out_begin, int *out_len) + { + TRexMatch match; + TRexBool res = _exp?(trex_getsubexp(_exp,n,&match)):TRex_False; + if(res) { + *out_begin = match.begin; + *out_len = match.len; + return true; + } + return false; + } + int GetSubExpCount() { return _exp?trex_getsubexpcount(_exp):0; } +private: + void CleanUp() { if(_exp) trex_free(_exp); _exp = (TRex *)0; } + TRex *_exp; +}; +#endif //_TREXPP_H_ \ No newline at end of file diff --git a/src/openalpr/alpr.cpp b/src/openalpr/alpr.cpp new file mode 100644 index 0000000..074d992 --- /dev/null +++ b/src/openalpr/alpr.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#include "alpr.h" +#include "alpr_impl.h" + + + + +// ALPR code + +Alpr::Alpr(const std::string country, const std::string runtimeDir) +{ + impl = new AlprImpl(country, runtimeDir); +} +Alpr::~Alpr() +{ + delete impl; +} +std::vector Alpr::recognize(std::string filepath) +{ + cv::Mat img = cv::imread(filepath, CV_LOAD_IMAGE_COLOR); + return impl->recognize(img); + +} + + +std::vector Alpr::recognize(std::vector imageBuffer) +{ + // Not sure if this actually works + cv::Mat img = cv::imdecode(Mat(imageBuffer), 1); + + return impl->recognize(img); +} + + +string Alpr::toJson(const vector< AlprResult > results) +{ + return impl->toJson(results); +} + + +void Alpr::setDetectRegion(bool detectRegion) +{ + impl->setDetectRegion(detectRegion); +} +void Alpr::setTopN(int topN) +{ + impl->setTopN(topN); +} +void Alpr::setDefaultRegion(std::string region) +{ + impl->setDefaultRegion(region); +} + +bool Alpr::isLoaded() +{ + return true; +} + + + +// Results code + + +AlprResult::AlprResult() +{ + +} +AlprResult::~AlprResult() +{ + +} diff --git a/src/openalpr/alpr.h b/src/openalpr/alpr.h new file mode 100644 index 0000000..7aeb890 --- /dev/null +++ b/src/openalpr/alpr.h @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + +#ifndef ALPR_H +#define ALPR_H + +#include +#include + +#define OPENALPR_VERSION "0.3" + +struct AlprPlate +{ + std::string characters; + float overall_confidence; + + bool matches_template; + //int char_confidence[]; +}; + +struct AlprCoordinate +{ + int x; + int y; +}; + +class AlprResult +{ + public: + AlprResult(); + virtual ~AlprResult(); + + int requested_topn; + int result_count; + + AlprPlate bestPlate; + std::vector topNPlates; + + float processing_time_ms; + AlprCoordinate plate_points[4]; + + int regionConfidence; + std::string region; +}; + + + +class AlprImpl; +class Alpr +{ + + public: + Alpr(const std::string country, const std::string runtimeDir = ""); + virtual ~Alpr(); + + void setDetectRegion(bool detectRegion); + void setTopN(int topN); + void setDefaultRegion(std::string region); + + std::vector recognize(std::string filepath); + std::vector recognize(std::vector imageBuffer); + + std::string toJson(const std::vector results); + + bool isLoaded(); + + private: + AlprImpl* impl; +}; + +#endif // APLR_H \ No newline at end of file diff --git a/src/openalpr/alpr_impl.cpp b/src/openalpr/alpr_impl.cpp new file mode 100644 index 0000000..621a1ae --- /dev/null +++ b/src/openalpr/alpr_impl.cpp @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#include "alpr_impl.h" + + +AlprImpl::AlprImpl(const std::string country, const std::string runtimeDir) +{ + config = new Config(country, runtimeDir); + plateDetector = new RegionDetector(config); + stateIdentifier = new StateIdentifier(config); + ocr = new OCR(config); + + this->detectRegion = DEFAULT_DETECT_REGION; + this->topN = DEFAULT_TOPN; + this->defaultRegion = ""; + +} +AlprImpl::~AlprImpl() +{ + delete config; + delete plateDetector; + delete stateIdentifier; + delete ocr; +} + + +std::vector AlprImpl::recognize(cv::Mat img) +{ + timespec startTime; + getTime(&startTime); + + vector response; + + + vector plateRegions = plateDetector->detect(img); + + + // Recognize. + + for (int i = 0; i < plateRegions.size(); i++) + { + timespec platestarttime; + getTime(&platestarttime); + + LicensePlateCandidate lp(img, plateRegions[i], config); + + lp.recognize(); + + + if (lp.confidence > 10) + { + AlprResult plateResult; + plateResult.region = defaultRegion; + plateResult.regionConfidence = 0; + + for (int pointidx = 0; pointidx < 4; pointidx++) + { + plateResult.plate_points[pointidx].x = (int) lp.plateCorners[pointidx].x; + plateResult.plate_points[pointidx].y = (int) lp.plateCorners[pointidx].y; + } + + if (detectRegion) + { + char statecode[4]; + plateResult.regionConfidence = stateIdentifier->recognize(img, plateRegions[i], statecode); + if (plateResult.regionConfidence > 0) + { + plateResult.region = statecode; + } + } + + + ocr->performOCR(lp.charSegmenter->getThresholds(), lp.charSegmenter->characters); + + ocr->postProcessor->analyze(plateResult.region, topN); + + //plateResult.characters = ocr->postProcessor->bestChars; + const vector ppResults = ocr->postProcessor->getResults(); + + int bestPlateIndex = 0; + + for (int pp = 0; pp < ppResults.size(); pp++) + { + if (pp >= topN) + break; + + // Set our "best plate" match to either the first entry, or the first entry with a postprocessor template match + if (bestPlateIndex == 0 && ppResults[pp].matchesTemplate) + bestPlateIndex = pp; + + if (ppResults[pp].letters.size() >= config->postProcessMinCharacters && + ppResults[pp].letters.size() <= config->postProcessMaxCharacters) + { + AlprPlate aplate; + aplate.characters = ppResults[pp].letters; + aplate.overall_confidence = ppResults[pp].totalscore; + aplate.matches_template = ppResults[pp].matchesTemplate; + plateResult.topNPlates.push_back(aplate); + } + } + plateResult.result_count = plateResult.topNPlates.size(); + + if (plateResult.topNPlates.size() > 0) + plateResult.bestPlate = plateResult.topNPlates[bestPlateIndex]; + + timespec plateEndTime; + getTime(&plateEndTime); + plateResult.processing_time_ms = diffclock(platestarttime, plateEndTime); + + if (plateResult.result_count > 0) + response.push_back(plateResult); + + if (config->debugGeneral) + { + rectangle(img, plateRegions[i], Scalar(0, 255, 0), 2); + for (int z = 0; z < 4; z++) + line(img, lp.plateCorners[z], lp.plateCorners[(z + 1) % 4], Scalar(255,0,255), 2); + } + + + } + else + { + if (config->debugGeneral) + rectangle(img, plateRegions[i], Scalar(0, 0, 255), 2); + } + + } + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "Total Time to process image: " << diffclock(startTime, endTime) << "ms." << endl; + } + + if (config->debugGeneral && config->debugShowImages) + { + displayImage(config, "Main Image", img); + // Pause indefinitely until they press a key + while ((char) cv::waitKey(50) == -1) + {} + } + return response; +} + +string AlprImpl::toJson(const vector< AlprResult > results) +{ + cJSON *root = cJSON_CreateArray(); + + for (int i = 0; i < results.size(); i++) + { + cJSON *resultObj = createJsonObj( &results[i] ); + cJSON_AddItemToArray(root, resultObj); + } + + // Print the JSON object to a string and return + char *out; + out=cJSON_PrintUnformatted(root); + cJSON_Delete(root); + + string response(out); + + free(out); + return response; +} + + + +cJSON* AlprImpl::createJsonObj(const AlprResult* result) +{ + cJSON *root, *coords; + + root=cJSON_CreateObject(); + + cJSON_AddStringToObject(root,"plate", result->bestPlate.characters.c_str()); + cJSON_AddNumberToObject(root,"confidence", result->bestPlate.overall_confidence); + cJSON_AddNumberToObject(root,"matches_template", result->bestPlate.matches_template); + + cJSON_AddStringToObject(root,"region", result->region.c_str()); + cJSON_AddNumberToObject(root,"region_confidence", result->regionConfidence); + + cJSON_AddNumberToObject(root,"processing_time_ms", result->processing_time_ms); + + cJSON_AddItemToObject(root, "coordinates", coords=cJSON_CreateArray()); + for (int i=0;i<4;i++) + { + cJSON *coords_object; + coords_object = cJSON_CreateObject(); + cJSON_AddNumberToObject(coords_object, "x", result->plate_points[i].x); + cJSON_AddNumberToObject(coords_object, "y", result->plate_points[i].y); + + cJSON_AddItemToArray(coords, coords_object); + } + + return root; +} + + +void AlprImpl::setDetectRegion(bool detectRegion) +{ + this->detectRegion = detectRegion; +} +void AlprImpl::setTopN(int topn) +{ + this->topN = topn; +} +void AlprImpl::setDefaultRegion(string region) +{ + this->defaultRegion = region; +} + diff --git a/src/openalpr/alpr_impl.h b/src/openalpr/alpr_impl.h new file mode 100644 index 0000000..31f1e4f --- /dev/null +++ b/src/openalpr/alpr_impl.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + +#ifndef ALPRIMPL_H +#define ALPRIMPL_H + +#include "alpr.h" +#include "config.h" + +#include "regiondetector.h" +#include "licenseplatecandidate.h" +#include "stateidentifier.h" +#include "charactersegmenter.h" +#include "ocr.h" + +#include "cjson.h" + +#include + + +#define DEFAULT_TOPN 25 +#define DEFAULT_DETECT_REGION false + +class AlprImpl +{ + + public: + AlprImpl(const std::string country, const std::string runtimeDir = ""); + virtual ~AlprImpl(); + + std::vector recognize(cv::Mat img); + + void applyRegionTemplate(AlprResult* result, std::string region); + + void setDetectRegion(bool detectRegion); + void setTopN(int topn); + void setDefaultRegion(string region); + + std::string toJson(const vector results); + + Config* config; + + private: + + RegionDetector* plateDetector; + StateIdentifier* stateIdentifier; + OCR* ocr; + + int topN; + bool detectRegion; + std::string defaultRegion; + + cJSON* createJsonObj(const AlprResult* result); +}; + +#endif // ALPRIMPL_H \ No newline at end of file diff --git a/src/openalpr/binarize_wolf.cpp b/src/openalpr/binarize_wolf.cpp new file mode 100644 index 0000000..198aad1 --- /dev/null +++ b/src/openalpr/binarize_wolf.cpp @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +/************************************************************** + * Binarization with several methods + * (0) Niblacks method + * (1) Sauvola & Co. + * ICDAR 1997, pp 147-152 + * (2) by myself - Christian Wolf + * Research notebook 19.4.2001, page 129 + * (3) by myself - Christian Wolf + * 20.4.2007 + * + * See also: + * Research notebook 24.4.2001, page 132 (Calculation of s) + **************************************************************/ + +#include "binarize_wolf.h" + + +// ************************************************************* +// glide a window across the image and +// create two maps: mean and standard deviation. +// ************************************************************* + + +float calcLocalStats (Mat &im, Mat &map_m, Mat &map_s, int winx, int winy) { + + float m,s,max_s, sum, sum_sq, foo; + int wxh = winx/2; + int wyh = winy/2; + int x_firstth= wxh; + int y_lastth = im.rows-wyh-1; + int y_firstth= wyh; + float winarea = winx*winy; + + max_s = 0; + for (int j = y_firstth ; j<=y_lastth; j++) + { + // Calculate the initial window at the beginning of the line + sum = sum_sq = 0; + for (int wy=0 ; wy max_s) + max_s = s; + map_m.fset(x_firstth, j, m); + map_s.fset(x_firstth, j, s); + + // Shift the window, add and remove new/old values to the histogram + for (int i=1 ; i <= im.cols-winx; i++) { + + // Remove the left old column and add the right new column + for (int wy=0; wy max_s) + max_s = s; + map_m.fset(i+wxh, j, m); + map_s.fset(i+wxh, j, s); + } + } + + return max_s; +} + + +/********************************************************** + * The binarization routine + **********************************************************/ + + +void NiblackSauvolaWolfJolion (Mat im, Mat output, NiblackVersion version, + int winx, int winy, float k) { + + float dR = BINARIZEWOLF_DEFAULTDR; + + float m, s, max_s; + float th=0; + double min_I, max_I; + int wxh = winx/2; + int wyh = winy/2; + int x_firstth= wxh; + int x_lastth = im.cols-wxh-1; + int y_lastth = im.rows-wyh-1; + int y_firstth= wyh; + int mx, my; + + // Create local statistics and store them in a float matrices + Mat map_m = Mat::zeros (im.rows, im.cols, CV_32F); + Mat map_s = Mat::zeros (im.rows, im.cols, CV_32F); + max_s = calcLocalStats (im, map_m, map_s, winx, winy); + + minMaxLoc(im, &min_I, &max_I); + + Mat thsurf (im.rows, im.cols, CV_32F); + + // Create the threshold surface, including border processing + // ---------------------------------------------------- + + for (int j = y_firstth ; j<=y_lastth; j++) { + + // NORMAL, NON-BORDER AREA IN THE MIDDLE OF THE WINDOW: + for (int i=0 ; i <= im.cols-winx; i++) { + + m = map_m.fget(i+wxh, j); + s = map_s.fget(i+wxh, j); + + // Calculate the threshold + switch (version) { + + case NIBLACK: + th = m + k*s; + break; + + case SAUVOLA: + th = m * (1 + k*(s/dR-1)); + break; + + case WOLFJOLION: + th = m + k * (s/max_s-1) * (m-min_I); + break; + + default: + cerr << "Unknown threshold type in ImageThresholder::surfaceNiblackImproved()\n"; + exit (1); + } + + thsurf.fset(i+wxh,j,th); + + if (i==0) { + // LEFT BORDER + for (int i=0; i<=x_firstth; ++i) + thsurf.fset(i,j,th); + + // LEFT-UPPER CORNER + if (j==y_firstth) + for (int u=0; u= thsurf.fget(x,y)) + { + output.uset(x,y,255); + } + else + { + output.uset(x,y,0); + } + } +} + +/********************************************************** + * The main function + **********************************************************/ + + +/********************************************************** + * Usage + **********************************************************/ +/* +static void usage (char *com) { + cerr << "usage: " << com << " [ -x -y -k ] [ version ] \n\n" + << "version: n Niblack (1986) needs white text on black background\n" + << " s Sauvola et al. (1997) needs black text on white background\n" + << " w Wolf et al. (2001) needs black text on white background\n" + << "\n" + << "Default version: w (Wolf et al. 2001)\n" + << "\n" + << "example:\n" + << " " << com << " w in.pgm out.pgm\n" + << " " << com << " in.pgm out.pgm\n" + << " " << com << " s -x 50 -y 50 -k 0.6 in.pgm out.pgm\n"; +} + +int main (int argc, char **argv) +{ + char version; + int c; + int winx=0, winy=0; + float optK=0.5; + bool didSpecifyK=false; + NiblackVersion versionCode; + char *inputname, *outputname, *versionstring; + + cerr << "===========================================================\n" + << "Christian Wolf, LIRIS Laboratory, Lyon, France.\n" + << "christian.wolf@liris.cnrs.fr\n" + << "Version " << BINARIZEWOLF_VERSION << endl + << "===========================================================\n"; + + // Argument processing + while ((c = getopt (argc, argv, "x:y:k:")) != EOF) { + + switch (c) { + + case 'x': + winx = atof(optarg); + break; + + case 'y': + winy = atof(optarg); + break; + + case 'k': + optK = atof(optarg); + didSpecifyK = true; + break; + + case '?': + usage (*argv); + cerr << "\nProblem parsing the options!\n\n"; + exit (1); + } + } + + switch(argc-optind) + { + case 3: + versionstring=argv[optind]; + inputname=argv[optind+1]; + outputname=argv[optind+2]; + break; + + case 2: + versionstring=(char *) "w"; + inputname=argv[optind]; + outputname=argv[optind+1]; + break; + + default: + usage (*argv); + exit (1); + } + + cerr << "Adaptive binarization\n" + << "Threshold calculation: "; + + // Determine the method + version = versionstring[0]; + switch (version) + { + case 'n': + versionCode = NIBLACK; + cerr << "Niblack (1986)\n"; + break; + + case 's': + versionCode = SAUVOLA; + cerr << "Sauvola et al. (1997)\n"; + break; + + case 'w': + versionCode = WOLFJOLION; + cerr << "Wolf and Jolion (2001)\n"; + break; + + default: + usage (*argv); + cerr << "\nInvalid version: '" << version << "'!"; + } + + + cerr << "parameter k=" << optK << endl; + + if (!didSpecifyK) + cerr << "Setting k to default value " << optK << endl; + + + // Load the image in grayscale mode + Mat input = imread(inputname,CV_LOAD_IMAGE_GRAYSCALE); + + + if ((input.rows<=0) || (input.cols<=0)) { + cerr << "*** ERROR: Couldn't read input image " << inputname << endl; + exit(1); + } + + + // Treat the window size + if (winx==0||winy==0) { + cerr << "Input size: " << input.cols << "x" << input.rows << endl; + winy = (int) (2.0 * input.rows-1)/3; + winx = (int) input.cols-1 < winy ? input.cols-1 : winy; + // if the window is too big, than we asume that the image + // is not a single text box, but a document page: set + // the window size to a fixed constant. + if (winx > 100) + winx = winy = 40; + cerr << "Setting window size to [" << winx + << "," << winy << "].\n"; + } + + // Threshold + Mat output (input.rows, input.cols, CV_8U); + NiblackSauvolaWolfJolion (input, output, versionCode, winx, winy, optK); + + // Write the tresholded file + cerr << "Writing binarized image to file '" << outputname << "'.\n"; + imwrite (outputname, output); + + return 0; +} +*/ \ No newline at end of file diff --git a/src/openalpr/binarize_wolf.h b/src/openalpr/binarize_wolf.h new file mode 100644 index 0000000..93604f4 --- /dev/null +++ b/src/openalpr/binarize_wolf.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + +#ifndef BINARIZEWOLF_H +#define BINARIZEWOLF_H + +#include "support/filesystem.h" + +#include +#include +#include +#include + +using namespace std; +using namespace cv; + +enum NiblackVersion +{ + NIBLACK=0, + SAUVOLA, + WOLFJOLION, +}; + +#define BINARIZEWOLF_VERSION "2.3 (February 26th, 2013)" +#define BINARIZEWOLF_DEFAULTDR 128 + + +#define uget(x,y) at(y,x) +#define uset(x,y,v) at(y,x)=v; +#define fget(x,y) at(y,x) +#define fset(x,y,v) at(y,x)=v; + + + +void NiblackSauvolaWolfJolion (Mat im, Mat output, NiblackVersion version, + int winx, int winy, float k); + + + +#endif // BINARIZEWOLF_H \ No newline at end of file diff --git a/src/openalpr/characteranalysis.cpp b/src/openalpr/characteranalysis.cpp new file mode 100644 index 0000000..60106ab --- /dev/null +++ b/src/openalpr/characteranalysis.cpp @@ -0,0 +1,1040 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#include "characteranalysis.h" +#include + +CharacterAnalysis::CharacterAnalysis(Mat img, Config* config) +{ + + this->config = config; + + this->hasPlateMask = false; + + if (this->config->debugCharAnalysis) + cout << "Starting CharacterAnalysis identification" << endl; + + + if (img.type() != CV_8U) + cvtColor( img, this->img_gray, CV_BGR2GRAY ); + else + { + img_gray = Mat(img.size(), img.type()); + img.copyTo(img_gray); + } + +} + +CharacterAnalysis::~CharacterAnalysis() +{ + for (int i = 0; i < thresholds.size(); i++) + { + thresholds[i].release(); + } + thresholds.clear(); +} + + + +void CharacterAnalysis::analyze() +{ + + + thresholds = produceThresholds(img_gray, config); + + + /* + // Morph Close the gray image to make it easier to detect blobs + int morph_elem = 1; + int morph_size = 1; + Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) ); + + for (int i = 0; i < thresholds.size(); i++) + { + //morphologyEx( mask, mask, MORPH_CLOSE, element ); + morphologyEx( thresholds[i], thresholds[i], MORPH_OPEN, element ); + //dilate( thresholds[i], thresholds[i], element ); + + } + */ + + + timespec startTime; + getTime(&startTime); + + + for (int i = 0; i < thresholds.size(); i++) + { + vector > contours; + vector hierarchy; + + Mat tempThreshold(thresholds[i].size(), CV_8U); + 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 (int i = 0; i < thresholds.size(); i++) + { + vector goodIndices = this->filter(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 (int i = 0; i < thresholds.size(); i++) + { + charSegments[i] = filterByOuterMask(allContours[i], allHierarchy[i], charSegments[i]); + } + } + + + int bestFitScore = -1; + int bestFitIndex = -1; + for (int i = 0; i < thresholds.size(); i++) + { + + //vector 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 = 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 > allowedContours; + for (int 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(img_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 goodIndices) +{ + int count = 0; + for (int 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 (int imgIndex = 0; imgIndex < allContours.size(); imgIndex++) + { + //vector charContours = filter(thresholds[imgIndex], allContours[imgIndex], allHierarchy[imgIndex]); + + int charsRecognized = 0; + int parentId = -1; + bool hasParent = false; + for (int 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 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 (int i = 0; i < allContours[winningIndex].size(); i++) + { + for (int 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(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 > contoursSecondRound; + + findContours(mask, contoursSecondRound, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE); + int biggestContourIndex = -1; + double largestArea = 0; + for (int c = 0; c < contoursSecondRound.size(); c++) + { + double area = contourArea(contoursSecondRound[c]); + if (area > largestArea) + { + biggestContourIndex = c; + largestArea = area; + } + } + + if (biggestContourIndex != -1) + { + mask = Mat::zeros(thresholds[winningIndex].size(), CV_8U); + + vector smoothedMaskPoints; + approxPolyDP(contoursSecondRound[biggestContourIndex], smoothedMaskPoints, 2, true); + + vector > 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 debugImgs; + Mat debugImgMasked = Mat::zeros(thresholds[winningIndex].size(), CV_8U); + + thresholds[winningIndex].copyTo(debugImgMasked, mask); + + debugImgs.push_back(mask); + debugImgs.push_back(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(thresholds[0].size(), CV_8U); + bitwise_not(fullMask, fullMask); + return fullMask; + + + +} + + +Mat CharacterAnalysis::getCharacterMask() +{ + + Mat charMask = Mat::zeros(bestThreshold.size(), CV_8U); + + for (int 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 + ); + + // get rid of the outline by drawing a 1 pixel width black line + drawContours(charMask, bestContours, + i, // draw this contour + cv::Scalar(0,0,0), // in + 1, + 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 CharacterAnalysis::getBestVotedLines(Mat img, vector > contours, vector goodIndices) +{ + + //if (this->debug) + // cout << "CharacterAnalysis::getBestVotedLines" << endl; + + vector bestStripe; + + vector charRegions; + + for (int 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 topLines; + vector bottomLines; + // Iterate through each possible char and find all possible lines for the top and bottom of each char segment + for (int i = 0; i < charRegions.size() - 1; i++) + { + for (int 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 (int i = 0; i < topLines.size(); i++) + { + float SCORING_MIN_THRESHOLD = 0.97; + float SCORING_MAX_THRESHOLD = 1.03; + + + int curScore = 0; + for (int 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 CharacterAnalysis::filter(Mat img, vector > contours, vector 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 charSegments; + int bestFitScore = -1; + for (int i = 0; i < NUM_STEPS; i++) + { + int goodIndicesCount; + + vector goodIndices(contours.size()); + for (int 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 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 CharacterAnalysis::filterByBoxSize(vector< vector< Point> > contours, vector goodIndices, int minHeightPx, int maxHeightPx) +{ + + float idealAspect=config->charWidthMM / config->charHeightMM; + float aspecttolerance=0.25; + + + vector includedIndices(contours.size()); + for (int j = 0; j < contours.size(); j++) + includedIndices.push_back(false); + + for (int 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 includedIndices(contours.size()); + for (int j = 0; j < contours.size(); j++) + includedIndices.push_back(false); + + for (int i = 0; i < contours.size(); i++) + { + if (goodIndices[i] == false) + continue; + + int parentIndex = hierarchy[i][3]; + + if (parentIndex >= 0 && goodIndices[parentIndex] == true) + { + // 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 CharacterAnalysis::filterByParentContour( vector< vector< Point> > contours, vector hierarchy, vector goodIndices) +{ + + vector includedIndices(contours.size()); + for (int j = 0; j < contours.size(); j++) + includedIndices[j] = false; + + vector parentIDs; + vector votes; + + for (int 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 (int 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 (int 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 (int 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 CharacterAnalysis::filterBetweenLines(Mat img, vector > contours, vector hierarchy, vector outerPolygon, vector goodIndices) +{ + static float MIN_AREA_PERCENT_WITHIN_LINES = 0.88; + + vector includedIndices(contours.size()); + for (int j = 0; j < contours.size(); j++) + includedIndices[j] = false; + + + if (outerPolygon.size() == 0) + return includedIndices; + + vector 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 = Mat::zeros(img.size(), CV_8U); + fillConvexPoly(outerMask, outerPolygon.data(), outerPolygon.size(), Scalar(255,255,255)); + + + for (int i = 0; i < contours.size(); i++) + { + if (goodIndices[i] == false) + continue; + + // get rid of the outline by drawing a 1 pixel width black line + drawContours(innerArea, contours, + i, // draw this contour + cv::Scalar(255,255,255), // in + CV_FILLED, + 8, + hierarchy, + 0 + ); + + + bitwise_and(innerArea, outerMask, innerArea); + + + vector > 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 (int tempContourIdx = 0; tempContourIdx < tempContours.size(); tempContourIdx++) + { + areaBetweenLines += contourArea(tempContours[tempContourIdx]); + + } + + + if (areaBetweenLines / totalArea >= MIN_AREA_PERCENT_WITHIN_LINES) + { + includedIndices[i] = true; + } + + innerArea.setTo(Scalar(0,0,0)); + } + + 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 passingIndices; + for (int 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 (int 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 CharacterAnalysis::getCharArea() +{ + const int MAX = 100000; + const int MIN= -1; + + int leftX = MAX; + int rightX = MIN; + + for (int i = 0; i < bestContours.size(); i++) + { + if (bestCharSegments[i] == false) + continue; + + for (int 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 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; +} diff --git a/src/openalpr/characteranalysis.h b/src/openalpr/characteranalysis.h new file mode 100644 index 0000000..5a0343d --- /dev/null +++ b/src/openalpr/characteranalysis.h @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + +#ifndef CHARACTERANALYSIS_H +#define CHARACTERANALYSIS_H + +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "constants.h" +#include "utility.h" +#include "config.h" + +using namespace cv; +using namespace std; + + +class CharacterAnalysis +{ + + public: + CharacterAnalysis(Mat img, Config* config); + virtual ~CharacterAnalysis(); + + bool hasPlateMask; + Mat plateMask; + + Mat bestThreshold; + vector > bestContours; + vector bestHierarchy; + vector bestCharSegments; + int bestCharSegmentsCount; + + LineSegment topLine; + LineSegment bottomLine; + vector linePolygon; + vector charArea; + + LineSegment charBoxTop; + LineSegment charBoxBottom; + LineSegment charBoxLeft; + LineSegment charBoxRight; + + bool thresholdsInverted; + + vector thresholds; + vector > > allContours; + vector > allHierarchy; + vector > charSegments; + + void analyze(); + + Mat getCharacterMask(); + + + + private: + Config* config; + + Mat img_gray; + + Mat findOuterBoxMask( ); + + + bool isPlateInverted(); + vector filter(Mat img, vector > contours, vector hierarchy); + + vector filterByBoxSize(vector > contours, vector goodIndices, int minHeightPx, int maxHeightPx); + vector filterByParentContour( vector< vector< Point> > contours, vector hierarchy, vector goodIndices); + vector filterContourHoles(vector > contours, vector hierarchy, vector goodIndices); + vector filterByOuterMask(vector > contours, vector hierarchy, vector goodIndices); + + vector getCharArea(); + vector getBestVotedLines(Mat img, vector > contours, vector goodIndices); + //vector getCharSegmentsBetweenLines(Mat img, vector > contours, vector outerPolygon); + vector filterBetweenLines(Mat img, vector > contours, vector hierarchy, vector outerPolygon, vector goodIndices); + + bool verifySize(Mat r, float minHeightPx, float maxHeightPx); + + int getGoodIndicesCount(vector goodIndices); + + +}; + +#endif // CHARACTERANALYSIS_H + + diff --git a/src/openalpr/characterregion.cpp b/src/openalpr/characterregion.cpp new file mode 100644 index 0000000..70515c4 --- /dev/null +++ b/src/openalpr/characterregion.cpp @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#include "characterregion.h" +//#include +#include + +CharacterRegion::CharacterRegion(Mat img, Config* config) +{ + this->config = config; + this->debug = config->debugCharRegions; + + this->confidence = 0; + + + + if (this->debug) + cout << "Starting CharacterRegion identification" << endl; + + timespec startTime; + getTime(&startTime); + + + charAnalysis = new CharacterAnalysis(img, config); + charAnalysis->analyze(); + + + if (this->debug) + { + vector tempDash; + for (int z = 0; z < charAnalysis->thresholds.size(); z++) + { + Mat tmp(charAnalysis->thresholds[z].size(), charAnalysis->thresholds[z].type()); + charAnalysis->thresholds[z].copyTo(tmp); + cvtColor(tmp, tmp, CV_GRAY2BGR); + + tempDash.push_back(tmp); + } + + + Mat bestVal(charAnalysis->bestThreshold.size(), charAnalysis->bestThreshold.type()); + charAnalysis->bestThreshold.copyTo(bestVal); + cvtColor(bestVal, bestVal, CV_GRAY2BGR); + + for (int z = 0; z < charAnalysis->bestContours.size(); z++) + { + Scalar dcolor(255,0,0); + if (charAnalysis->bestCharSegments[z]) + dcolor = Scalar(0,255,0); + drawContours(bestVal, charAnalysis->bestContours, z, dcolor, 1); + } + tempDash.push_back(bestVal); + displayImage(config, "Character Region Step 1 Thresholds", drawImageDashboard(tempDash, bestVal.type(), 3)); + } + + + + if (this->debug) + { + /* + Mat img_contours(img_threshold.size(), CV_8U); + img_threshold.copyTo(img_contours); + cvtColor(img_contours, img_contours, CV_GRAY2RGB); + + vector > allowedContours; + for (int i = 0; i < contours.size(); i++) + { + if (charSegments[i]) + allowedContours.push_back(contours[i]); + } + + drawContours(img_contours, 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 + + + 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)); + + + if (charAnalysis->linePolygon.size() > 0) + { + + int confidenceDrainers = 0; + int charSegmentCount = charAnalysis->bestCharSegmentsCount; + if (charSegmentCount == 1) + confidenceDrainers += 91; + else if (charSegmentCount < 5) + confidenceDrainers += (5 - charSegmentCount) * 10; + + int absangle = abs(charAnalysis->topLine.angle); + if (absangle > 10) + confidenceDrainers += 91; + else if (absangle > 1) + confidenceDrainers += (10 - absangle) * 5; + + + if (confidenceDrainers >= 100) + this->confidence=1; + else + this->confidence = 100 - confidenceDrainers; + + } + + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "Character Region Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + +} + +CharacterRegion::~CharacterRegion() +{ + delete(charAnalysis); +} + + + +Mat CharacterRegion::getPlateMask() +{ + return charAnalysis->plateMask; +} + +LineSegment CharacterRegion::getTopLine() +{ + return charAnalysis->topLine; +} + +LineSegment CharacterRegion::getBottomLine() +{ + return charAnalysis->bottomLine; +} + +vector 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; +} + +bool CharacterRegion::thresholdsInverted() +{ + return charAnalysis->thresholdsInverted; +} + + diff --git a/src/openalpr/characterregion.h b/src/openalpr/characterregion.h new file mode 100644 index 0000000..450108d --- /dev/null +++ b/src/openalpr/characterregion.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + +#ifndef CHARACTERREGION_H +#define CHARACTERREGION_H + +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "constants.h" +#include "utility.h" +#include "characteranalysis.h" +#include "config.h" + +using namespace cv; +using namespace std; + + +class CharacterRegion +{ + + public: + CharacterRegion(Mat img, Config* config); + virtual ~CharacterRegion(); + + CharacterAnalysis *charAnalysis; + + int confidence; + Mat getPlateMask(); + + LineSegment getTopLine(); + LineSegment getBottomLine(); + //vector getLinePolygon(); + vector getCharArea(); + + LineSegment getCharBoxTop(); + LineSegment getCharBoxBottom(); + LineSegment getCharBoxLeft(); + LineSegment getCharBoxRight(); + + bool thresholdsInverted(); + + protected: + Config* config; + bool debug; + + Mat findOuterBoxMask(vector thresholds, vector > > allContours, vector > allHierarchy); + + vector filter(Mat img, vector > contours, vector hierarchy); + vector filterByBoxSize(Mat img, vector > contours, vector goodIndices, float minHeightPx, float maxHeightPx); + vector filterByParentContour( vector< vector< Point> > contours, vector hierarchy, vector goodIndices); + vector filterContourHoles(vector > contours, vector hierarchy, vector goodIndices); + + vector getBestVotedLines(Mat img, vector > contours, vector goodIndices); + //vector getCharSegmentsBetweenLines(Mat img, vector > contours, vector outerPolygon); + vector filterBetweenLines(Mat img, vector > contours, vector hierarchy, vector outerPolygon, vector goodIndices); + Mat getCharacterMask(Mat img, vector > contours, vector hierarchy, vector goodIndices); + + vector wrapContours(vector > contours); + bool verifySize(Mat r, float minHeightPx, float maxHeightPx); + + int getGoodIndicesCount(vector goodIndices); + + bool isPlateInverted(Mat threshold, vector > contours, vector hierarchy, vector goodIndices); + +}; + +#endif // CHARACTERREGION_H + + diff --git a/src/openalpr/charactersegmenter.cpp b/src/openalpr/charactersegmenter.cpp new file mode 100644 index 0000000..30292ed --- /dev/null +++ b/src/openalpr/charactersegmenter.cpp @@ -0,0 +1,1188 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#include "charactersegmenter.h" + +CharacterSegmenter::CharacterSegmenter(Mat img, bool invertedColors, Config* config) +{ + + this->config = config; + + this->confidence = 0; + + if (this->config->debugCharSegmenter) + cout << "Starting CharacterSegmenter" << endl; + + //CharacterRegion charRegion(img, debug); + + timespec startTime; + getTime(&startTime); + + + Mat img_gray(img.size(), CV_8U); + cvtColor( img, img_gray, CV_BGR2GRAY ); + //normalize(img_gray, img_gray, 0, 255, CV_MINMAX ); + medianBlur(img_gray, img_gray, 3); + + if (invertedColors) + bitwise_not(img_gray, img_gray); + + + charAnalysis = new CharacterAnalysis(img_gray, config); + charAnalysis->analyze(); + + + + if (this->config->debugCharSegmenter) + { + displayImage(config, "CharacterSegmenter Thresholds", drawImageDashboard(charAnalysis->thresholds, CV_8U, 3)); + } + + + + + + + if (this->config->debugCharSegmenter) + { + + Mat img_contours(charAnalysis->bestThreshold.size(), CV_8U); + charAnalysis->bestThreshold.copyTo(img_contours); + cvtColor(img_contours, img_contours, CV_GRAY2RGB); + + vector > allowedContours; + for (int i = 0; i < charAnalysis->bestContours.size(); i++) + { + if (charAnalysis->bestCharSegments[i]) + allowedContours.push_back(charAnalysis->bestContours[i]); + } + + 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); + } + + // Figure out the average character width + float totalCharWidth = 0; + float totalCharHeight = 0; + + 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); + + for (int i = 0; i < charAnalysis->bestContours.size(); i++) + { + if (charAnalysis->bestCharSegments[i] == false) + continue; + + Rect mr = boundingRect(charAnalysis->bestContours[i]); + totalCharWidth += mr.width; + totalCharHeight += mr.height; + } + + int numSamples = charAnalysis->bestCharSegmentsCount; + float avgCharWidth = totalCharWidth / numSamples; + float avgCharHeight = totalCharHeight / numSamples; + + removeSmallContours(charAnalysis->thresholds, charAnalysis->allContours, avgCharWidth, avgCharHeight); + + // Do the histogram analysis to figure out char regions + + + timespec startTime; + getTime(&startTime); + + + vector allHistograms; + + vector allBoxes; + for (int i = 0; i < charAnalysis->allContours.size(); i++) + { + + + Mat histogramMask = Mat::zeros(charAnalysis->thresholds[i].size(), CV_8U); + + fillConvexPoly(histogramMask, charAnalysis->linePolygon.data(), charAnalysis->linePolygon.size(), Scalar(255,255,255)); + + + VerticalHistogram vertHistogram(charAnalysis->thresholds[i], histogramMask); + + + if (this->config->debugCharSegmenter) + { + Mat histoCopy(vertHistogram.debugImg.size(), vertHistogram.debugImg.type()); + //vertHistogram.copyTo(histoCopy); + cvtColor(vertHistogram.debugImg, histoCopy, CV_GRAY2RGB); + allHistograms.push_back(histoCopy); + } + +// + float score = 0; + vector charBoxes = getHistogramBoxes(vertHistogram.debugImg, avgCharWidth, avgCharHeight, &score); + + + if (this->config->debugCharSegmenter) + { + for (int cboxIdx = 0; cboxIdx < charBoxes.size(); cboxIdx++) + { + rectangle(allHistograms[i], charBoxes[cboxIdx], Scalar(0, 255, 0)); + } + + Mat histDashboard = drawImageDashboard(allHistograms, allHistograms[0].type(), 3); + displayImage(config, "Char seg histograms", histDashboard); + } + + for (int z = 0; z < charBoxes.size(); z++) + allBoxes.push_back(charBoxes[z]); + //drawAndWait(&histogramMask); + } + + + float biggestCharWidth = avgCharWidth; + // Compute largest char width + for (int i = 0; i < allBoxes.size(); i++) + { + if (allBoxes[i].width > biggestCharWidth) + biggestCharWidth = allBoxes[i].width; + } + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << " -- Character Segmentation Create and Score Histograms Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + //ColorFilter colorFilter(img, charAnalysis->getCharacterMask()); + vector candidateBoxes = getBestCharBoxes(charAnalysis->thresholds[0], allBoxes, biggestCharWidth); + + + if (this->config->debugCharSegmenter) + { + // Setup the dashboard images to show the cleaning filters + for (int i = 0; i < charAnalysis->thresholds.size(); i++) + { + Mat cleanImg = Mat::zeros(charAnalysis->thresholds[i].size(), charAnalysis->thresholds[i].type()); + Mat boxMask = getCharBoxMask(charAnalysis->thresholds[i], candidateBoxes); + charAnalysis->thresholds[i].copyTo(cleanImg); + bitwise_and(cleanImg, boxMask, cleanImg); + cvtColor(cleanImg, cleanImg, CV_GRAY2BGR); + + for (int c = 0; c < candidateBoxes.size(); c++) + rectangle(cleanImg, candidateBoxes[c], Scalar(0, 255, 0), 1); + imgDbgCleanStages.push_back(cleanImg); + } + } + + + getTime(&startTime); + + filterEdgeBoxes(charAnalysis->thresholds, candidateBoxes, biggestCharWidth, avgCharHeight); + + candidateBoxes = filterMostlyEmptyBoxes(charAnalysis->thresholds, candidateBoxes); + + candidateBoxes = combineCloseBoxes(candidateBoxes, biggestCharWidth); + + cleanCharRegions(charAnalysis->thresholds, candidateBoxes); + cleanMostlyFullBoxes(charAnalysis->thresholds, candidateBoxes); + + //cleanBasedOnColor(thresholds, colorFilter.colorMask, candidateBoxes); + + candidateBoxes = filterMostlyEmptyBoxes(charAnalysis->thresholds, candidateBoxes); + this->characters = candidateBoxes; + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << " -- Character Segmentation Box cleaning/filtering Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + if (this->config->debugCharSegmenter) + { + + + Mat imgDash = drawImageDashboard(charAnalysis->thresholds, CV_8U, 3); + displayImage(config, "Segmentation after cleaning", imgDash); + + Mat generalDash = drawImageDashboard(this->imgDbgGeneral, this->imgDbgGeneral[0].type(), 2); + displayImage(config, "Segmentation General", generalDash); + + Mat cleanImgDash = drawImageDashboard(this->imgDbgCleanStages, this->imgDbgCleanStages[0].type(), 3); + displayImage(config, "Segmentation Clean Filters", cleanImgDash); + } + } + + + + + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "Character Segmenter Time: " << diffclock(startTime, endTime) << "ms." << endl; + } +} + +CharacterSegmenter::~CharacterSegmenter() +{ + delete charAnalysis; +} + + + + + + +// Given a histogram and the horizontal line boundaries, respond with an array of boxes where the characters are +// Scores the histogram quality as well based on num chars, char volume, and even separation +vector CharacterSegmenter::getHistogramBoxes(Mat histogram, float avgCharWidth, float avgCharHeight, float* score) +{ + float MIN_HISTOGRAM_HEIGHT = avgCharHeight * config->segmentationMinCharHeightPercent; + + float MAX_SEGMENT_WIDTH = avgCharWidth * config->segmentationMaxCharWidthvsAverage; + + //float MIN_BOX_AREA = (avgCharWidth * avgCharHeight) * 0.25; + + int pxLeniency = 2; + + vector charBoxes; + vector allBoxes = get1DHits(histogram, pxLeniency); + + for (int i = 0; i < allBoxes.size(); i++) + { + + if (allBoxes[i].width >= config->segmentationMinBoxWidthPx && allBoxes[i].width <= MAX_SEGMENT_WIDTH && + allBoxes[i].area() > MIN_HISTOGRAM_HEIGHT ) + { + charBoxes.push_back(allBoxes[i]); + } + + } + + + + return charBoxes; +} + +vector CharacterSegmenter::getBestCharBoxes(Mat img, vector charBoxes, float avgCharWidth) +{ + + + float MAX_SEGMENT_WIDTH = avgCharWidth * 1.55; + + // 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. + + Mat histoImg = Mat::zeros(Size(img.cols, img.rows), CV_8U); + + + int columnCount; + + for (int col = 0; col < img.cols; col++) + { + columnCount = 0; + + for (int i = 0; i < charBoxes.size(); i++) + { + if (col >= charBoxes[i].x && col < (charBoxes[i].x + charBoxes[i].width)) + columnCount++; + } + + // Fill the line of the histogram + for (; columnCount > 0; columnCount--) + histoImg.at(histoImg.rows - columnCount, col) = 255; + } + + + // Go through each row in the histoImg and score it. Try to find the single line that gives me the most right-sized character regions (based on avgCharWidth) + + int bestRowIndex = 0; + float bestRowScore = 0; + vector bestBoxes; + + + for (int row = 0; row < histoImg.rows; row++) + { + vector validBoxes; + vector allBoxes = get1DHits(histoImg, row); + + if (this->config->debugCharSegmenter) + cout << "All Boxes size " << allBoxes.size() << endl; + + if (allBoxes.size() == 0) + break; + + float rowScore = 0; + + for (int boxidx = 0; boxidx < allBoxes.size(); boxidx++) + { + int w = allBoxes[boxidx].width; + if (w >= config->segmentationMinBoxWidthPx && w <= MAX_SEGMENT_WIDTH) + { + float widthDiffPixels = abs(w - avgCharWidth); + float widthDiffPercent = widthDiffPixels / avgCharWidth; + rowScore += 10 * (1 - widthDiffPercent); + + if (widthDiffPercent < 0.25) // Bonus points when it's close to the average character width + rowScore += 8; + + validBoxes.push_back(allBoxes[boxidx]); + } + } + + + if (rowScore > bestRowScore) + { + bestRowScore = rowScore; + bestRowIndex = row; + bestBoxes = validBoxes; + } + } + + + + if (this->config->debugCharSegmenter) + { + cvtColor(histoImg, histoImg, CV_GRAY2BGR); + line(histoImg, Point(0, histoImg.rows - 1 - bestRowIndex), Point(histoImg.cols, histoImg.rows - 1 - bestRowIndex), Scalar(0, 255, 0)); + + Mat imgBestBoxes(img.size(), img.type()); + img.copyTo(imgBestBoxes); + cvtColor(imgBestBoxes, imgBestBoxes, CV_GRAY2BGR); + for (int i = 0; i < bestBoxes.size(); i++) + rectangle(imgBestBoxes, bestBoxes[i], Scalar(0, 255, 0)); + + this->imgDbgGeneral.push_back(addLabel(histoImg, "All Histograms")); + this->imgDbgGeneral.push_back(addLabel(imgBestBoxes, "Best Boxes")); + + } + + + return bestBoxes; +} + +vector CharacterSegmenter::get1DHits(Mat img, int yOffset) +{ + vector hits; + + bool onSegment = false; + int curSegmentLength = 0; + for (int col = 0; col < img.cols; col++) + { + bool isOn = img.at(img.rows - 1 - yOffset, col); + if (isOn) + { + // We're on a segment. Increment the length + onSegment = true; + curSegmentLength++; + } + + if ((isOn == false && onSegment == true) || + (col == img.cols - 1 && onSegment == true)) + { + // A segment just ended or we're at the very end of the row and we're on a segment + Point topLeft = Point(col - curSegmentLength, top.getPointAt(col - curSegmentLength) - 1); + Point botRight = Point(col, bottom.getPointAt(col) + 1); + hits.push_back(Rect(topLeft, botRight)); + + onSegment = false; + curSegmentLength = 0; + } + + } + + + return hits; +} + + +void CharacterSegmenter::removeSmallContours(vector thresholds, vector > > allContours, float avgCharWidth, float avgCharHeight) +{ + //const float MIN_CHAR_AREA = 0.02 * avgCharWidth * avgCharHeight; // To clear out the tiny specks + const float MIN_CONTOUR_HEIGHT = 0.3 * avgCharHeight; + + for (int i = 0; i < thresholds.size(); i++) + { + + for (int c = 0; c < allContours[i].size(); c++) + { + if (allContours[i][c].size() == 0) + continue; + + Rect mr = boundingRect(allContours[i][c]); + if (mr.height < MIN_CONTOUR_HEIGHT) + { + // Erase it + drawContours(thresholds[i], allContours[i], c, Scalar(0, 0, 0), -1); + continue; + } + + } + } +} + +vector CharacterSegmenter::combineCloseBoxes( vector charBoxes, float biggestCharWidth) +{ + vector newCharBoxes; + + for (int i = 0; i < charBoxes.size(); i++) + { + if (i == charBoxes.size() - 1) + { + newCharBoxes.push_back(charBoxes[i]); + break; + } + float bigWidth = (charBoxes[i + 1].x + charBoxes[i + 1].width - charBoxes[i].x); + + float w1Diff = abs(charBoxes[i].width - biggestCharWidth); + float w2Diff = abs(charBoxes[i + 1].width - biggestCharWidth); + float bigDiff = abs(bigWidth - biggestCharWidth); + bigDiff *= 1.3; // Make it a little harder to merge boxes. + + if (bigDiff < w1Diff && bigDiff < w2Diff) + { + Rect bigRect(charBoxes[i].x, charBoxes[i].y, bigWidth, charBoxes[i].height); + newCharBoxes.push_back(bigRect); + if (this->config->debugCharSegmenter) + { + for (int z = 0; z < charAnalysis->thresholds.size(); z++) + { + Point center(bigRect.x + bigRect.width / 2, bigRect.y + bigRect.height / 2); + RotatedRect rrect(center, Size2f(bigRect.width, bigRect.height + (bigRect.height / 2)), 0); + ellipse(imgDbgCleanStages[z], rrect, Scalar(0,255,0), 1); + } + cout << "Merging 2 boxes -- " << i << " and " << i + 1 << endl; + } + + i++; + } + else + { + newCharBoxes.push_back(charBoxes[i]); + } + + + + } + + return newCharBoxes; +} + +void CharacterSegmenter::cleanCharRegions(vector thresholds, vector charRegions) +{ + const float MIN_SPECKLE_HEIGHT_PERCENT = 0.13; + const float MIN_SPECKLE_WIDTH_PX = 3; + const float MIN_CONTOUR_AREA_PERCENT = 0.1; + const float MIN_CONTOUR_HEIGHT_PERCENT = 0.60; + + Mat mask = getCharBoxMask(thresholds[0], charRegions); + + + for (int i = 0; i < thresholds.size(); i++) + { + bitwise_and(thresholds[i], mask, thresholds[i]); + vector > contours; + + Mat tempImg(thresholds[i].size(), thresholds[i].type()); + thresholds[i].copyTo(tempImg); + + //Mat element = getStructuringElement( 1, +// Size( 2 + 1, 2+1 ), +// Point( 1, 1 ) ); + //dilate(thresholds[i], tempImg, element); + //morphologyEx(thresholds[i], tempImg, MORPH_CLOSE, element); + //drawAndWait(&tempImg); + + findContours(tempImg, contours, RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE); + + for (int j = 0; j < charRegions.size(); j++) + { + const float MIN_SPECKLE_HEIGHT = ((float)charRegions[j].height) * MIN_SPECKLE_HEIGHT_PERCENT; + const float MIN_CONTOUR_AREA = ((float)charRegions[j].area()) * MIN_CONTOUR_AREA_PERCENT; + + + int tallestContourHeight = 0; + float totalArea = 0; + for (int c = 0; c < contours.size(); c++) + { + if (contours[c].size() == 0) + continue; + if (charRegions[j].contains(contours[c][0]) == false) + continue; + + + + Rect r = boundingRect(contours[c]); + + if (r.height <= MIN_SPECKLE_HEIGHT || r.width <= MIN_SPECKLE_WIDTH_PX) + { + // Erase this speckle + drawContours(thresholds[i], contours, c, Scalar(0,0,0), CV_FILLED); + + if (this->config->debugCharSegmenter) + { + drawContours(imgDbgCleanStages[i], contours, c, COLOR_DEBUG_SPECKLES, CV_FILLED); + } + } + else + { + if (r.height > tallestContourHeight) + tallestContourHeight = r.height; + + totalArea += contourArea(contours[c]); + + + } + //else if (r.height > tallestContourHeight) + //{ + // tallestContourIndex = c; + // tallestContourHeight = h; + //} + + } + + + + if (totalArea < MIN_CONTOUR_AREA) + { + // Character is not voluminous enough. Erase it. + if (this->config->debugCharSegmenter) + { + cout << "Character CLEAN: (area) removing box " << j << " in threshold " << i << " -- Area " << totalArea << " < " << MIN_CONTOUR_AREA << endl; + + Rect boxTop(charRegions[j].x, charRegions[j].y - 10, charRegions[j].width, 10); + rectangle(imgDbgCleanStages[i], boxTop, COLOR_DEBUG_MIN_AREA, -1); + } + + + rectangle(thresholds[i], charRegions[j], Scalar(0, 0, 0), -1); + } + else if (tallestContourHeight < ((float) charRegions[j].height * MIN_CONTOUR_HEIGHT_PERCENT)) + { + // This character is too short. Black the whole thing out + if (this->config->debugCharSegmenter) + { + cout << "Character CLEAN: (height) removing box " << j << " in threshold " << i << " -- Height " << tallestContourHeight << " < " << ((float) charRegions[j].height * MIN_CONTOUR_HEIGHT_PERCENT) << endl; + + Rect boxBottom(charRegions[j].x, charRegions[j].y + charRegions[j].height, charRegions[j].width, 10); + rectangle(imgDbgCleanStages[i], boxBottom, COLOR_DEBUG_MIN_HEIGHT, -1); + } + rectangle(thresholds[i], charRegions[j], Scalar(0, 0, 0), -1); + } + + } + + + Mat closureElement = getStructuringElement( 1, + Size( 2 + 1, 2+1 ), + Point( 1, 1 ) ); + + //morphologyEx(thresholds[i], thresholds[i], MORPH_OPEN, element); + + //dilate(thresholds[i], thresholds[i], element); + //erode(thresholds[i], thresholds[i], element); + + morphologyEx(thresholds[i], thresholds[i], MORPH_CLOSE, closureElement); + + // Lastly, draw a clipping line between each character boxes + for (int j = 0; j < charRegions.size(); j++) + { + line(thresholds[i], Point(charRegions[j].x - 1, charRegions[j].y), Point(charRegions[j].x - 1, charRegions[j].y + charRegions[j].height), Scalar(0, 0, 0)); + line(thresholds[i], Point(charRegions[j].x + charRegions[j].width + 1, charRegions[j].y), Point(charRegions[j].x + charRegions[j].width + 1, charRegions[j].y + charRegions[j].height), Scalar(0, 0, 0)); + } + } + +} + + +void CharacterSegmenter::cleanBasedOnColor(vector thresholds, Mat colorMask, vector charRegions) +{ + // If I knock out x% of the contour area from this thing (after applying the color filter) + // Consider it a bad news bear. REmove the whole area. + const float MIN_PERCENT_CHUNK_REMOVED = 0.6; + + for (int i = 0; i < thresholds.size(); i++) + { + + for (int j = 0; j < charRegions.size(); j++) + { + + Mat boxChar = Mat::zeros(thresholds[i].size(), CV_8U); + rectangle(boxChar, charRegions[j], Scalar(255,255,255), CV_FILLED); + + bitwise_and(thresholds[i], boxChar, boxChar); + + + + float meanBefore = mean(boxChar, boxChar)[0]; + + Mat thresholdCopy(thresholds[i].size(), CV_8U); + bitwise_and(colorMask, boxChar, thresholdCopy); + + float meanAfter = mean(thresholdCopy, boxChar)[0]; + + if (meanAfter < meanBefore * (1-MIN_PERCENT_CHUNK_REMOVED)) + { + rectangle(thresholds[i], charRegions[j], Scalar(0,0,0), CV_FILLED); + + if (this->config->debugCharSegmenter) + { + //vector tmpDebug; + //Mat thresholdCopy2 = Mat::zeros(thresholds[i].size(), CV_8U); + //rectangle(thresholdCopy2, charRegions[j], Scalar(255,255,255), CV_FILLED); + //tmpDebug.push_back(addLabel(thresholdCopy2, "box Mask")); + //bitwise_and(thresholds[i], thresholdCopy2, thresholdCopy2); + //tmpDebug.push_back(addLabel(thresholdCopy2, "box Mask + Thresh")); + //bitwise_and(colorMask, thresholdCopy2, thresholdCopy2); + //tmpDebug.push_back(addLabel(thresholdCopy2, "box Mask + Thresh + Color")); +// +// Mat tmpytmp = addLabel(thresholdCopy2, "box Mask + Thresh + Color"); +// Mat tmpx = drawImageDashboard(tmpDebug, tmpytmp.type(), 1); + //drawAndWait( &tmpx ); + + + cout << "Segmentation Filter Clean by Color: Removed Threshold " << i << " charregion " << j << endl; + cout << "Segmentation Filter Clean by Color: before=" << meanBefore << " after=" << meanAfter << endl; + + Point topLeft(charRegions[j].x, charRegions[j].y); + circle(imgDbgCleanStages[i], topLeft, 5, COLOR_DEBUG_COLORFILTER, CV_FILLED); + } + } + } + + } +} + + +void CharacterSegmenter::cleanMostlyFullBoxes(vector thresholds, const vector charRegions) +{ + float MAX_FILLED = 0.95 * 255; + + for (int i = 0; i < charRegions.size(); i++) + { + Mat mask = Mat::zeros(thresholds[0].size(), CV_8U); + rectangle(mask, charRegions[i], Scalar(255,255,255), -1); + + for (int j = 0; j < thresholds.size(); j++) + { + if (mean(thresholds[j], mask)[0] > MAX_FILLED) + { + // Empty the rect + rectangle(thresholds[j], charRegions[i], Scalar(0,0,0), -1); + + if (this->config->debugCharSegmenter) + { + Rect inset(charRegions[i].x + 2, charRegions[i].y - 2, charRegions[i].width - 4, charRegions[i].height - 4); + rectangle(imgDbgCleanStages[j], inset, COLOR_DEBUG_FULLBOX, 1); + } + } + } + } +} +vector CharacterSegmenter::filterMostlyEmptyBoxes(vector thresholds, const vector charRegions) +{ + // Of the n thresholded images, if box 3 (for example) is empty in half (for example) of the thresholded images, + // clear all data for every box #3. + + //const float MIN_AREA_PERCENT = 0.1; + const float MIN_CONTOUR_HEIGHT_PERCENT = 0.65; + + Mat mask = getCharBoxMask(thresholds[0], charRegions); + + vector boxScores(charRegions.size()); + + for (int i = 0; i < charRegions.size(); i++) + boxScores[i] = 0; + + for (int i = 0; i < thresholds.size(); i++) + { + + for (int j = 0; j < charRegions.size(); j++) + { + //float minArea = charRegions[j].area() * MIN_AREA_PERCENT; + + Mat tempImg = Mat::zeros(thresholds[i].size(), thresholds[i].type()); + rectangle(tempImg, charRegions[j], Scalar(255,255,255), CV_FILLED); + bitwise_and(thresholds[i], tempImg, tempImg); + + vector > contours; + findContours(tempImg, contours, RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE); + + float biggestContourHeight = 0; + + vector allPointsInBox; + for (int c = 0; c < contours.size(); c++) + { + if (contours[c].size() == 0) + continue; + + for (int z = 0; z < contours[c].size(); z++) + allPointsInBox.push_back(contours[c][z]); + + } + + float height = 0; + if (allPointsInBox.size() > 0) + { + height = boundingRect(allPointsInBox).height; + } + + + if (height >= ((float) charRegions[j].height * MIN_CONTOUR_HEIGHT_PERCENT)) + { + boxScores[j] = boxScores[j] + 1; + } + else if (this->config->debugCharSegmenter) + { + drawX(imgDbgCleanStages[i], charRegions[j], COLOR_DEBUG_EMPTYFILTER, 3); + } + + + } + + } + + vector newCharRegions; + + int maxBoxScore = 0; + for (int i = 0; i < charRegions.size(); i++) + { + if (boxScores[i] > maxBoxScore) + maxBoxScore = boxScores[i]; + } + + // Need a good char sample in at least 50% of the boxes for it to be valid. + int MIN_FULL_BOXES = maxBoxScore * 0.49; + + // Now check each score. If it's below the minimum, remove the charRegion + for (int i = 0; i < charRegions.size(); i++) + { + if (boxScores[i] > MIN_FULL_BOXES) + newCharRegions.push_back(charRegions[i]); + else + { + // Erase the box from the Mat... mainly for debug purposes + if (this->config->debugCharSegmenter) + { + cout << "Mostly Empty Filter: box index: " << i; + cout << " this box had a score of : " << boxScores[i];; + cout << " MIN_FULL_BOXES: " << MIN_FULL_BOXES << endl;; + + for (int z = 0; z < thresholds.size(); z++) + { + rectangle(thresholds[z], charRegions[i], Scalar(0,0,0), -1); + + drawX(imgDbgCleanStages[z], charRegions[i], COLOR_DEBUG_EMPTYFILTER, 1); + } + + } + } + + if (this->config->debugCharSegmenter) + cout << " Box Score: " << boxScores[i] << endl; + } + + return newCharRegions; +} + +void CharacterSegmenter::filterEdgeBoxes(vector thresholds, const vector charRegions, float avgCharWidth, float avgCharHeight) +{ + const float MIN_ANGLE_FOR_ROTATION = 0.4; + int MIN_CONNECTED_EDGE_PIXELS = (avgCharHeight * 1.5); + + // Sometimes the rectangle won't be very tall, making it impossible to detect an edge + // Adjust for this here. + int alternate = thresholds[0].rows * 0.92; + if (alternate < MIN_CONNECTED_EDGE_PIXELS && alternate > avgCharHeight) + MIN_CONNECTED_EDGE_PIXELS = alternate; + + // + // Pay special attention to the edge boxes. If it's a skinny box, and the vertical height extends above our bounds... remove it. + //while (charBoxes.size() > 0 && charBoxes[charBoxes.size() - 1].width < MIN_SEGMENT_WIDTH_EDGES) + // charBoxes.erase(charBoxes.begin() + charBoxes.size() - 1); + // Now filter the "edge" boxes. We don't want to include skinny boxes on the edges, since these could be plate boundaries + //while (charBoxes.size() > 0 && charBoxes[0].width < MIN_SEGMENT_WIDTH_EDGES) + // charBoxes.erase(charBoxes.begin() + 0); + + + // TECHNIQUE #1 + // Check for long vertical lines. Once the line is too long, mask the whole region + + if (charRegions.size() <= 1) + return; + + // Check both sides to see where the edges are + // The first starts at the right edge of the leftmost char region and works its way left + // The second starts at the left edge of the rightmost char region and works its way right. + // We start by rotating the threshold image to the correct angle + // then check each column 1 by 1. + + vector leftEdges; + vector rightEdges; + + for (int i = 0; i < thresholds.size(); i++) + { + Mat rotated; + + if (top.angle > MIN_ANGLE_FOR_ROTATION) + { + // Rotate image: + rotated = Mat(thresholds[i].size(), thresholds[i].type()); + Mat rot_mat( 2, 3, CV_32FC1 ); + Point center = Point( thresholds[i].cols/2, thresholds[i].rows/2 ); + + rot_mat = getRotationMatrix2D( center, top.angle, 1.0 ); + warpAffine( thresholds[i], rotated, rot_mat, thresholds[i].size() ); + } + else + { + rotated = thresholds[i]; + } + + int leftEdgeX = 0; + int rightEdgeX = rotated.cols; + // Do the left side + int col = charRegions[0].x + charRegions[0].width; + while (col >= 0) + { + + int rowLength = getLongestBlobLengthBetweenLines(rotated, col); + + if (rowLength > MIN_CONNECTED_EDGE_PIXELS) + { + leftEdgeX = col; + break; + } + + col--; + } + + col = charRegions[charRegions.size() - 1].x; + while (col < rotated.cols) + { + + int rowLength = getLongestBlobLengthBetweenLines(rotated, col); + + if (rowLength > MIN_CONNECTED_EDGE_PIXELS) + { + rightEdgeX = col; + break; + } + col++; + } + + + if (leftEdgeX != 0) + leftEdges.push_back(leftEdgeX); + if (rightEdgeX != thresholds[i].cols) + rightEdges.push_back(rightEdgeX); + + } + + int leftEdge = 0; + int rightEdge = thresholds[0].cols; + + // Assign the edge values to the SECOND closest value + if (leftEdges.size() > 1) + { + sort (leftEdges.begin(), leftEdges.begin()+leftEdges.size()); + leftEdge = leftEdges[leftEdges.size() - 2] + 1; + } + if (rightEdges.size() > 1) + { + sort (rightEdges.begin(), rightEdges.begin()+rightEdges.size()); + rightEdge = rightEdges[1] - 1; + } + + if (leftEdge != 0 || rightEdge != thresholds[0].cols) + { + Mat mask = Mat::zeros(thresholds[0].size(), CV_8U); + rectangle(mask, Point(leftEdge, 0), Point(rightEdge, thresholds[0].rows), Scalar(255,255,255), -1); + + if (top.angle > MIN_ANGLE_FOR_ROTATION) + { + // Rotate mask: + Mat rot_mat( 2, 3, CV_32FC1 ); + Point center = Point( mask.cols/2, mask.rows/2 ); + + rot_mat = getRotationMatrix2D( center, top.angle * -1, 1.0 ); + warpAffine( mask, mask, rot_mat, mask.size() ); + } + + + // If our edge mask covers more than x% of the char region, mask the whole thing... + const float MAX_COVERAGE_PERCENT = 0.6; + int leftCoveragePx = leftEdge - charRegions[0].x; + float leftCoveragePercent = ((float) leftCoveragePx) / ((float) charRegions[0].width); + float rightCoveragePx = (charRegions[charRegions.size() -1].x + charRegions[charRegions.size() -1].width) - rightEdge; + float rightCoveragePercent = ((float) rightCoveragePx) / ((float) charRegions[charRegions.size() -1].width); + if ((leftCoveragePercent > MAX_COVERAGE_PERCENT) || + (charRegions[0].width - leftCoveragePx < config->segmentationMinBoxWidthPx)) + { + rectangle(mask, charRegions[0], Scalar(0,0,0), -1); // Mask the whole region + if (this->config->debugCharSegmenter) + cout << "Edge Filter: Entire left region is erased" << endl; + } + if ((rightCoveragePercent > MAX_COVERAGE_PERCENT) || + (charRegions[charRegions.size() -1].width - rightCoveragePx < config->segmentationMinBoxWidthPx)) + { + rectangle(mask, charRegions[charRegions.size() -1], Scalar(0,0,0), -1); + if (this->config->debugCharSegmenter) + cout << "Edge Filter: Entire right region is erased" << endl; + } + + for (int i = 0; i < thresholds.size(); i++) + { + bitwise_and(thresholds[i], mask, thresholds[i]); + } + + + if (this->config->debugCharSegmenter) + { + cout << "Edge Filter: left=" << leftEdge << " right=" << rightEdge << endl; + Mat bordered = addLabel(mask, "Edge Filter #1"); + imgDbgGeneral.push_back(bordered); + + Mat invertedMask(mask.size(), mask.type()); + bitwise_not(mask, invertedMask); + for (int z = 0; z < imgDbgCleanStages.size(); z++) + fillMask(imgDbgCleanStages[z], invertedMask, Scalar(0,0,255)); + } + } + + // 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 > 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) +{ + + int longestBlobLength = 0; + + bool onSegment = false; + bool wasbetweenLines = false; + float curSegmentLength = 0; + for (int row = 0; row < img.rows; row++) + { + bool isbetweenLines = false; + + bool isOn = img.at(row, col); + // check two rows at a time. + if (!isOn && col < img.cols) + isOn = img.at(row, col); + + if (isOn) + { + // We're on a segment. Increment the length + isbetweenLines = top.isPointBelowLine(Point(col, row)) && !bottom.isPointBelowLine(Point(col, row)); + float incrementBy = 1; + + // Add a little extra to the score if this is outside of the lines + if (!isbetweenLines) + incrementBy = 1.1; + + onSegment = true; + curSegmentLength += incrementBy; + } + if (isOn && isbetweenLines) + { + wasbetweenLines = true; + } + + if ((isOn == false && onSegment == true) || + (row == img.rows - 1 && onSegment == true)) + { + if (wasbetweenLines && curSegmentLength > longestBlobLength) + longestBlobLength = curSegmentLength; + + onSegment = false; + isbetweenLines = false; + curSegmentLength = 0; + } + + } + + + return longestBlobLength; +} + +// Checks to see if a skinny, tall line (extending above or below the char Height) is inside the given box. +// Returns the contour index if true. -1 otherwise +int CharacterSegmenter::isSkinnyLineInsideBox(Mat threshold, Rect box, vector > contours, vector hierarchy, float avgCharWidth, float avgCharHeight) +{ + float MIN_EDGE_CONTOUR_HEIGHT = avgCharHeight * 1.25; + + // Sometimes the threshold is smaller than the MIN_EDGE_CONTOUR_HEIGHT. + // In that case, adjust to be smaller + int alternate = threshold.rows * 0.92; + if (alternate < MIN_EDGE_CONTOUR_HEIGHT && alternate > avgCharHeight) + MIN_EDGE_CONTOUR_HEIGHT = alternate; + + Rect slightlySmallerBox(box.x, box.y, box.width, box.height); + Mat boxMask = Mat::zeros(threshold.size(), CV_8U); + rectangle(boxMask, slightlySmallerBox, Scalar(255, 255, 255), -1); + + + for (int i = 0; i < contours.size(); i++) + { + // Only bother with the big boxes + if (boundingRect(contours[i]).height < MIN_EDGE_CONTOUR_HEIGHT) + continue; + + Mat tempImg = Mat::zeros(threshold.size(), CV_8U); + drawContours(tempImg, contours, i, Scalar(255,255,255), -1, 8, hierarchy, 1); + bitwise_and(tempImg, boxMask, tempImg); + + vector > subContours; + findContours(tempImg, subContours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE); + int tallestContourIdx = -1; + int tallestContourHeight = 0; + int tallestContourWidth = 0; + float tallestContourArea = 0; + for (int s = 0; s < subContours.size(); s++) + { + Rect r = boundingRect(subContours[s]); + if (r.height > tallestContourHeight) + { + tallestContourIdx = s; + tallestContourHeight = r.height; + tallestContourWidth = r.width; + tallestContourArea = contourArea(subContours[s]); + } + } + + if (tallestContourIdx != -1) + { + + + //cout << "Edge Filter: " << tallestContourHeight << " -- " << avgCharHeight << endl; + if (tallestContourHeight >= avgCharHeight * 0.9 && + ((tallestContourWidth < config->segmentationMinBoxWidthPx) || (tallestContourArea < avgCharWidth * avgCharHeight * 0.1))) + { + cout << "Edge Filter: Avg contour width: " << avgCharWidth << " This guy is: " << tallestContourWidth << endl; + cout << "Edge Filter: tallestContourArea: " << tallestContourArea << " Minimum: " << avgCharWidth * avgCharHeight * 0.1 << endl; + return i; + } + } + } + + return -1; +} + + + +Mat CharacterSegmenter::getCharBoxMask(Mat img_threshold, vector charBoxes) +{ + + Mat mask = Mat::zeros(img_threshold.size(), CV_8U); + for (int i = 0; i < charBoxes.size(); i++) + rectangle(mask, charBoxes[i], Scalar(255, 255, 255), -1); + + return mask; +} + +vector CharacterSegmenter::getThresholds() +{ + return charAnalysis->thresholds; +} + + diff --git a/src/openalpr/charactersegmenter.h b/src/openalpr/charactersegmenter.h new file mode 100644 index 0000000..d5074e6 --- /dev/null +++ b/src/openalpr/charactersegmenter.h @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + +#ifndef CHARACTERSEGMENTER_H +#define CHARACTERSEGMENTER_H + +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "constants.h" +#include "binarize_wolf.h" +#include "utility.h" +#include "characterregion.h" +#include "colorfilter.h" +#include "verticalhistogram.h" +#include "config.h" + +using namespace cv; +using namespace std; + +//const float MIN_BOX_WIDTH_PX = 4; // 4 pixels + +const Scalar COLOR_DEBUG_EDGE(0,0,255); // Red +const Scalar COLOR_DEBUG_SPECKLES(0,0,255); // Red +const Scalar COLOR_DEBUG_MIN_HEIGHT(255,0,0); // Blue +const Scalar COLOR_DEBUG_MIN_AREA(255,0,0); // Blue +const Scalar COLOR_DEBUG_FULLBOX(255,255,0); // Blue-green +const Scalar COLOR_DEBUG_COLORFILTER(255,0,255); // Magenta +const Scalar COLOR_DEBUG_EMPTYFILTER(0,255,255); // Yellow + +class CharacterSegmenter +{ + + public: + CharacterSegmenter(Mat img, bool invertedColors, Config* config); + virtual ~CharacterSegmenter(); + + vector characters; + int confidence; + + vector getThresholds(); + + private: + Config* config; + + CharacterAnalysis* charAnalysis; + + LineSegment top; + LineSegment bottom; + + vector imgDbgGeneral; + vector imgDbgCleanStages; + + vector filter(Mat img, vector > contours, vector hierarchy); + vector filterByBoxSize(vector< vector< Point> > contours, vector goodIndices, float minHeightPx, float maxHeightPx); + vector filterBetweenLines(Mat img, vector > contours, vector hierarchy, vector outerPolygon, vector goodIndices); + vector filterContourHoles(vector > contours, vector hierarchy, vector goodIndices); + + vector getBestVotedLines(Mat img, vector > contours, vector goodIndices); + int getGoodIndicesCount(vector goodIndices); + + Mat getCharacterMask(Mat img_threshold, vector > contours, vector hierarchy, vector goodIndices); + Mat getCharBoxMask(Mat img_threshold, vector charBoxes); + + void removeSmallContours(vector thresholds, vector > > allContours, float avgCharWidth, float avgCharHeight); + + Mat getVerticalHistogram(Mat img, Mat mask); + vector getHistogramBoxes(Mat histogram, float avgCharWidth, float avgCharHeight, float* score); + vector getBestCharBoxes(Mat img, vector charBoxes, float avgCharWidth); + vector combineCloseBoxes( vector charBoxes, float avgCharWidth); + + vector get1DHits(Mat img, int yOffset); + + void cleanCharRegions(vector thresholds, vector charRegions); + void cleanBasedOnColor(vector thresholds, Mat colorMask, vector charRegions); + void cleanMostlyFullBoxes(vector thresholds, const vector charRegions); + vector filterMostlyEmptyBoxes(vector thresholds, const vector charRegions); + void filterEdgeBoxes(vector thresholds, const vector charRegions, float avgCharWidth, float avgCharHeight); + + int getLongestBlobLengthBetweenLines(Mat img, int col); + + int isSkinnyLineInsideBox(Mat threshold, Rect box, vector > contours, vector hierarchy, float avgCharWidth, float avgCharHeight); + + vector getEncapsulatingLines(Mat img, vector > contours, vector goodIndices); +}; + +#endif // CHARACTERSEGMENTER_H + + diff --git a/src/openalpr/cjson.c b/src/openalpr/cjson.c new file mode 100644 index 0000000..7331258 --- /dev/null +++ b/src/openalpr/cjson.c @@ -0,0 +1,596 @@ +/* + Copyright (c) 2009 Dave Gamble + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* cJSON */ +/* JSON parser in C. */ + +#include +#include +#include +#include +#include +#include +#include +#include "cjson.h" + +static const char *ep; + +const char *cJSON_GetErrorPtr(void) {return ep;} + +static int cJSON_strcasecmp(const char *s1,const char *s2) +{ + if (!s1) return (s1==s2)?0:1;if (!s2) return 1; + for(; tolower(*s1) == tolower(*s2); ++s1, ++s2) if(*s1 == 0) return 0; + return tolower(*(const unsigned char *)s1) - tolower(*(const unsigned char *)s2); +} + +static void *(*cJSON_malloc)(size_t sz) = malloc; +static void (*cJSON_free)(void *ptr) = free; + +static char* cJSON_strdup(const char* str) +{ + size_t len; + char* copy; + + len = strlen(str) + 1; + if (!(copy = (char*)cJSON_malloc(len))) return 0; + memcpy(copy,str,len); + return copy; +} + +void cJSON_InitHooks(cJSON_Hooks* hooks) +{ + if (!hooks) { /* Reset hooks */ + cJSON_malloc = malloc; + cJSON_free = free; + return; + } + + cJSON_malloc = (hooks->malloc_fn)?hooks->malloc_fn:malloc; + cJSON_free = (hooks->free_fn)?hooks->free_fn:free; +} + +/* Internal constructor. */ +static cJSON *cJSON_New_Item(void) +{ + cJSON* node = (cJSON*)cJSON_malloc(sizeof(cJSON)); + if (node) memset(node,0,sizeof(cJSON)); + return node; +} + +/* Delete a cJSON structure. */ +void cJSON_Delete(cJSON *c) +{ + cJSON *next; + while (c) + { + next=c->next; + if (!(c->type&cJSON_IsReference) && c->child) cJSON_Delete(c->child); + if (!(c->type&cJSON_IsReference) && c->valuestring) cJSON_free(c->valuestring); + if (c->string) cJSON_free(c->string); + cJSON_free(c); + c=next; + } +} + +/* Parse the input text to generate a number, and populate the result into item. */ +static const char *parse_number(cJSON *item,const char *num) +{ + double n=0,sign=1,scale=0;int subscale=0,signsubscale=1; + + if (*num=='-') sign=-1,num++; /* Has sign? */ + if (*num=='0') num++; /* is zero */ + if (*num>='1' && *num<='9') do n=(n*10.0)+(*num++ -'0'); while (*num>='0' && *num<='9'); /* Number? */ + if (*num=='.' && num[1]>='0' && num[1]<='9') {num++; do n=(n*10.0)+(*num++ -'0'),scale--; while (*num>='0' && *num<='9');} /* Fractional part? */ + if (*num=='e' || *num=='E') /* Exponent? */ + { num++;if (*num=='+') num++; else if (*num=='-') signsubscale=-1,num++; /* With sign? */ + while (*num>='0' && *num<='9') subscale=(subscale*10)+(*num++ - '0'); /* Number? */ + } + + n=sign*n*pow(10.0,(scale+subscale*signsubscale)); /* number = +/- number.fraction * 10^+/- exponent */ + + item->valuedouble=n; + item->valueint=(int)n; + item->type=cJSON_Number; + return num; +} + +/* Render the number nicely from the given item into a string. */ +static char *print_number(cJSON *item) +{ + char *str; + double d=item->valuedouble; + if (fabs(((double)item->valueint)-d)<=DBL_EPSILON && d<=INT_MAX && d>=INT_MIN) + { + str=(char*)cJSON_malloc(21); /* 2^64+1 can be represented in 21 chars. */ + if (str) sprintf(str,"%d",item->valueint); + } + else + { + str=(char*)cJSON_malloc(64); /* This is a nice tradeoff. */ + if (str) + { + if (fabs(floor(d)-d)<=DBL_EPSILON && fabs(d)<1.0e60)sprintf(str,"%.0f",d); + else if (fabs(d)<1.0e-6 || fabs(d)>1.0e9) sprintf(str,"%e",d); + else sprintf(str,"%f",d); + } + } + return str; +} + +static unsigned parse_hex4(const char *str) +{ + unsigned h=0; + if (*str>='0' && *str<='9') h+=(*str)-'0'; else if (*str>='A' && *str<='F') h+=10+(*str)-'A'; else if (*str>='a' && *str<='f') h+=10+(*str)-'a'; else return 0; + h=h<<4;str++; + if (*str>='0' && *str<='9') h+=(*str)-'0'; else if (*str>='A' && *str<='F') h+=10+(*str)-'A'; else if (*str>='a' && *str<='f') h+=10+(*str)-'a'; else return 0; + h=h<<4;str++; + if (*str>='0' && *str<='9') h+=(*str)-'0'; else if (*str>='A' && *str<='F') h+=10+(*str)-'A'; else if (*str>='a' && *str<='f') h+=10+(*str)-'a'; else return 0; + h=h<<4;str++; + if (*str>='0' && *str<='9') h+=(*str)-'0'; else if (*str>='A' && *str<='F') h+=10+(*str)-'A'; else if (*str>='a' && *str<='f') h+=10+(*str)-'a'; else return 0; + return h; +} + +/* Parse the input text into an unescaped cstring, and populate item. */ +static const unsigned char firstByteMark[7] = { 0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }; +static const char *parse_string(cJSON *item,const char *str) +{ + const char *ptr=str+1;char *ptr2;char *out;int len=0;unsigned uc,uc2; + if (*str!='\"') {ep=str;return 0;} /* not a string! */ + + while (*ptr!='\"' && *ptr && ++len) if (*ptr++ == '\\') ptr++; /* Skip escaped quotes. */ + + out=(char*)cJSON_malloc(len+1); /* This is how long we need for the string, roughly. */ + if (!out) return 0; + + ptr=str+1;ptr2=out; + while (*ptr!='\"' && *ptr) + { + if (*ptr!='\\') *ptr2++=*ptr++; + else + { + ptr++; + switch (*ptr) + { + case 'b': *ptr2++='\b'; break; + case 'f': *ptr2++='\f'; break; + case 'n': *ptr2++='\n'; break; + case 'r': *ptr2++='\r'; break; + case 't': *ptr2++='\t'; break; + case 'u': /* transcode utf16 to utf8. */ + uc=parse_hex4(ptr+1);ptr+=4; /* get the unicode char. */ + + if ((uc>=0xDC00 && uc<=0xDFFF) || uc==0) break; /* check for invalid. */ + + if (uc>=0xD800 && uc<=0xDBFF) /* UTF16 surrogate pairs. */ + { + if (ptr[1]!='\\' || ptr[2]!='u') break; /* missing second-half of surrogate. */ + uc2=parse_hex4(ptr+3);ptr+=6; + if (uc2<0xDC00 || uc2>0xDFFF) break; /* invalid second-half of surrogate. */ + uc=0x10000 + (((uc&0x3FF)<<10) | (uc2&0x3FF)); + } + + len=4;if (uc<0x80) len=1;else if (uc<0x800) len=2;else if (uc<0x10000) len=3; ptr2+=len; + + switch (len) { + case 4: *--ptr2 =((uc | 0x80) & 0xBF); uc >>= 6; + case 3: *--ptr2 =((uc | 0x80) & 0xBF); uc >>= 6; + case 2: *--ptr2 =((uc | 0x80) & 0xBF); uc >>= 6; + case 1: *--ptr2 =(uc | firstByteMark[len]); + } + ptr2+=len; + break; + default: *ptr2++=*ptr; break; + } + ptr++; + } + } + *ptr2=0; + if (*ptr=='\"') ptr++; + item->valuestring=out; + item->type=cJSON_String; + return ptr; +} + +/* Render the cstring provided to an escaped version that can be printed. */ +static char *print_string_ptr(const char *str) +{ + const char *ptr;char *ptr2,*out;int len=0;unsigned char token; + + if (!str) return cJSON_strdup(""); + ptr=str;while ((token=*ptr) && ++len) {if (strchr("\"\\\b\f\n\r\t",token)) len++; else if (token<32) len+=5;ptr++;} + + out=(char*)cJSON_malloc(len+3); + if (!out) return 0; + + ptr2=out;ptr=str; + *ptr2++='\"'; + while (*ptr) + { + if ((unsigned char)*ptr>31 && *ptr!='\"' && *ptr!='\\') *ptr2++=*ptr++; + else + { + *ptr2++='\\'; + switch (token=*ptr++) + { + case '\\': *ptr2++='\\'; break; + case '\"': *ptr2++='\"'; break; + case '\b': *ptr2++='b'; break; + case '\f': *ptr2++='f'; break; + case '\n': *ptr2++='n'; break; + case '\r': *ptr2++='r'; break; + case '\t': *ptr2++='t'; break; + default: sprintf(ptr2,"u%04x",token);ptr2+=5; break; /* escape and print */ + } + } + } + *ptr2++='\"';*ptr2++=0; + return out; +} +/* Invote print_string_ptr (which is useful) on an item. */ +static char *print_string(cJSON *item) {return print_string_ptr(item->valuestring);} + +/* Predeclare these prototypes. */ +static const char *parse_value(cJSON *item,const char *value); +static char *print_value(cJSON *item,int depth,int fmt); +static const char *parse_array(cJSON *item,const char *value); +static char *print_array(cJSON *item,int depth,int fmt); +static const char *parse_object(cJSON *item,const char *value); +static char *print_object(cJSON *item,int depth,int fmt); + +/* Utility to jump whitespace and cr/lf */ +static const char *skip(const char *in) {while (in && *in && (unsigned char)*in<=32) in++; return in;} + +/* Parse an object - create a new root, and populate. */ +cJSON *cJSON_ParseWithOpts(const char *value,const char **return_parse_end,int require_null_terminated) +{ + const char *end=0; + cJSON *c=cJSON_New_Item(); + ep=0; + if (!c) return 0; /* memory fail */ + + end=parse_value(c,skip(value)); + if (!end) {cJSON_Delete(c);return 0;} /* parse failure. ep is set. */ + + /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */ + if (require_null_terminated) {end=skip(end);if (*end) {cJSON_Delete(c);ep=end;return 0;}} + if (return_parse_end) *return_parse_end=end; + return c; +} +/* Default options for cJSON_Parse */ +cJSON *cJSON_Parse(const char *value) {return cJSON_ParseWithOpts(value,0,0);} + +/* Render a cJSON item/entity/structure to text. */ +char *cJSON_Print(cJSON *item) {return print_value(item,0,1);} +char *cJSON_PrintUnformatted(cJSON *item) {return print_value(item,0,0);} + +/* Parser core - when encountering text, process appropriately. */ +static const char *parse_value(cJSON *item,const char *value) +{ + if (!value) return 0; /* Fail on null. */ + if (!strncmp(value,"null",4)) { item->type=cJSON_NULL; return value+4; } + if (!strncmp(value,"false",5)) { item->type=cJSON_False; return value+5; } + if (!strncmp(value,"true",4)) { item->type=cJSON_True; item->valueint=1; return value+4; } + if (*value=='\"') { return parse_string(item,value); } + if (*value=='-' || (*value>='0' && *value<='9')) { return parse_number(item,value); } + if (*value=='[') { return parse_array(item,value); } + if (*value=='{') { return parse_object(item,value); } + + ep=value;return 0; /* failure. */ +} + +/* Render a value to text. */ +static char *print_value(cJSON *item,int depth,int fmt) +{ + char *out=0; + if (!item) return 0; + switch ((item->type)&255) + { + case cJSON_NULL: out=cJSON_strdup("null"); break; + case cJSON_False: out=cJSON_strdup("false");break; + case cJSON_True: out=cJSON_strdup("true"); break; + case cJSON_Number: out=print_number(item);break; + case cJSON_String: out=print_string(item);break; + case cJSON_Array: out=print_array(item,depth,fmt);break; + case cJSON_Object: out=print_object(item,depth,fmt);break; + } + return out; +} + +/* Build an array from input text. */ +static const char *parse_array(cJSON *item,const char *value) +{ + cJSON *child; + if (*value!='[') {ep=value;return 0;} /* not an array! */ + + item->type=cJSON_Array; + value=skip(value+1); + if (*value==']') return value+1; /* empty array. */ + + item->child=child=cJSON_New_Item(); + if (!item->child) return 0; /* memory fail */ + value=skip(parse_value(child,skip(value))); /* skip any spacing, get the value. */ + if (!value) return 0; + + while (*value==',') + { + cJSON *new_item; + if (!(new_item=cJSON_New_Item())) return 0; /* memory fail */ + child->next=new_item;new_item->prev=child;child=new_item; + value=skip(parse_value(child,skip(value+1))); + if (!value) return 0; /* memory fail */ + } + + if (*value==']') return value+1; /* end of array */ + ep=value;return 0; /* malformed. */ +} + +/* Render an array to text */ +static char *print_array(cJSON *item,int depth,int fmt) +{ + char **entries; + char *out=0,*ptr,*ret;int len=5; + cJSON *child=item->child; + int numentries=0,i=0,fail=0; + + /* How many entries in the array? */ + while (child) numentries++,child=child->next; + /* Explicitly handle numentries==0 */ + if (!numentries) + { + out=(char*)cJSON_malloc(3); + if (out) strcpy(out,"[]"); + return out; + } + /* Allocate an array to hold the values for each */ + entries=(char**)cJSON_malloc(numentries*sizeof(char*)); + if (!entries) return 0; + memset(entries,0,numentries*sizeof(char*)); + /* Retrieve all the results: */ + child=item->child; + while (child && !fail) + { + ret=print_value(child,depth+1,fmt); + entries[i++]=ret; + if (ret) len+=strlen(ret)+2+(fmt?1:0); else fail=1; + child=child->next; + } + + /* If we didn't fail, try to malloc the output string */ + if (!fail) out=(char*)cJSON_malloc(len); + /* If that fails, we fail. */ + if (!out) fail=1; + + /* Handle failure. */ + if (fail) + { + for (i=0;itype=cJSON_Object; + value=skip(value+1); + if (*value=='}') return value+1; /* empty array. */ + + item->child=child=cJSON_New_Item(); + if (!item->child) return 0; + value=skip(parse_string(child,skip(value))); + if (!value) return 0; + child->string=child->valuestring;child->valuestring=0; + if (*value!=':') {ep=value;return 0;} /* fail! */ + value=skip(parse_value(child,skip(value+1))); /* skip any spacing, get the value. */ + if (!value) return 0; + + while (*value==',') + { + cJSON *new_item; + if (!(new_item=cJSON_New_Item())) return 0; /* memory fail */ + child->next=new_item;new_item->prev=child;child=new_item; + value=skip(parse_string(child,skip(value+1))); + if (!value) return 0; + child->string=child->valuestring;child->valuestring=0; + if (*value!=':') {ep=value;return 0;} /* fail! */ + value=skip(parse_value(child,skip(value+1))); /* skip any spacing, get the value. */ + if (!value) return 0; + } + + if (*value=='}') return value+1; /* end of array */ + ep=value;return 0; /* malformed. */ +} + +/* Render an object to text. */ +static char *print_object(cJSON *item,int depth,int fmt) +{ + char **entries=0,**names=0; + char *out=0,*ptr,*ret,*str;int len=7,i=0,j; + cJSON *child=item->child; + int numentries=0,fail=0; + /* Count the number of entries. */ + while (child) numentries++,child=child->next; + /* Explicitly handle empty object case */ + if (!numentries) + { + out=(char*)cJSON_malloc(fmt?depth+4:3); + if (!out) return 0; + ptr=out;*ptr++='{'; + if (fmt) {*ptr++='\n';for (i=0;ichild;depth++;if (fmt) len+=depth; + while (child) + { + names[i]=str=print_string_ptr(child->string); + entries[i++]=ret=print_value(child,depth,fmt); + if (str && ret) len+=strlen(ret)+strlen(str)+2+(fmt?2+depth:0); else fail=1; + child=child->next; + } + + /* Try to allocate the output string */ + if (!fail) out=(char*)cJSON_malloc(len); + if (!out) fail=1; + + /* Handle failure */ + if (fail) + { + for (i=0;ichild;int i=0;while(c)i++,c=c->next;return i;} +cJSON *cJSON_GetArrayItem(cJSON *array,int item) {cJSON *c=array->child; while (c && item>0) item--,c=c->next; return c;} +cJSON *cJSON_GetObjectItem(cJSON *object,const char *string) {cJSON *c=object->child; while (c && cJSON_strcasecmp(c->string,string)) c=c->next; return c;} + +/* Utility for array list handling. */ +static void suffix_object(cJSON *prev,cJSON *item) {prev->next=item;item->prev=prev;} +/* Utility for handling references. */ +static cJSON *create_reference(cJSON *item) {cJSON *ref=cJSON_New_Item();if (!ref) return 0;memcpy(ref,item,sizeof(cJSON));ref->string=0;ref->type|=cJSON_IsReference;ref->next=ref->prev=0;return ref;} + +/* Add item to array/object. */ +void cJSON_AddItemToArray(cJSON *array, cJSON *item) {cJSON *c=array->child;if (!item) return; if (!c) {array->child=item;} else {while (c && c->next) c=c->next; suffix_object(c,item);}} +void cJSON_AddItemToObject(cJSON *object,const char *string,cJSON *item) {if (!item) return; if (item->string) cJSON_free(item->string);item->string=cJSON_strdup(string);cJSON_AddItemToArray(object,item);} +void cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item) {cJSON_AddItemToArray(array,create_reference(item));} +void cJSON_AddItemReferenceToObject(cJSON *object,const char *string,cJSON *item) {cJSON_AddItemToObject(object,string,create_reference(item));} + +cJSON *cJSON_DetachItemFromArray(cJSON *array,int which) {cJSON *c=array->child;while (c && which>0) c=c->next,which--;if (!c) return 0; + if (c->prev) c->prev->next=c->next;if (c->next) c->next->prev=c->prev;if (c==array->child) array->child=c->next;c->prev=c->next=0;return c;} +void cJSON_DeleteItemFromArray(cJSON *array,int which) {cJSON_Delete(cJSON_DetachItemFromArray(array,which));} +cJSON *cJSON_DetachItemFromObject(cJSON *object,const char *string) {int i=0;cJSON *c=object->child;while (c && cJSON_strcasecmp(c->string,string)) i++,c=c->next;if (c) return cJSON_DetachItemFromArray(object,i);return 0;} +void cJSON_DeleteItemFromObject(cJSON *object,const char *string) {cJSON_Delete(cJSON_DetachItemFromObject(object,string));} + +/* Replace array/object items with new ones. */ +void cJSON_ReplaceItemInArray(cJSON *array,int which,cJSON *newitem) {cJSON *c=array->child;while (c && which>0) c=c->next,which--;if (!c) return; + newitem->next=c->next;newitem->prev=c->prev;if (newitem->next) newitem->next->prev=newitem; + if (c==array->child) array->child=newitem; else newitem->prev->next=newitem;c->next=c->prev=0;cJSON_Delete(c);} +void cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem){int i=0;cJSON *c=object->child;while(c && cJSON_strcasecmp(c->string,string))i++,c=c->next;if(c){newitem->string=cJSON_strdup(string);cJSON_ReplaceItemInArray(object,i,newitem);}} + +/* Create basic types: */ +cJSON *cJSON_CreateNull(void) {cJSON *item=cJSON_New_Item();if(item)item->type=cJSON_NULL;return item;} +cJSON *cJSON_CreateTrue(void) {cJSON *item=cJSON_New_Item();if(item)item->type=cJSON_True;return item;} +cJSON *cJSON_CreateFalse(void) {cJSON *item=cJSON_New_Item();if(item)item->type=cJSON_False;return item;} +cJSON *cJSON_CreateBool(int b) {cJSON *item=cJSON_New_Item();if(item)item->type=b?cJSON_True:cJSON_False;return item;} +cJSON *cJSON_CreateNumber(double num) {cJSON *item=cJSON_New_Item();if(item){item->type=cJSON_Number;item->valuedouble=num;item->valueint=(int)num;}return item;} +cJSON *cJSON_CreateString(const char *string) {cJSON *item=cJSON_New_Item();if(item){item->type=cJSON_String;item->valuestring=cJSON_strdup(string);}return item;} +cJSON *cJSON_CreateArray(void) {cJSON *item=cJSON_New_Item();if(item)item->type=cJSON_Array;return item;} +cJSON *cJSON_CreateObject(void) {cJSON *item=cJSON_New_Item();if(item)item->type=cJSON_Object;return item;} + +/* Create Arrays: */ +cJSON *cJSON_CreateIntArray(const int *numbers,int count) {int i;cJSON *n=0,*p=0,*a=cJSON_CreateArray();for(i=0;a && ichild=n;else suffix_object(p,n);p=n;}return a;} +cJSON *cJSON_CreateFloatArray(const float *numbers,int count) {int i;cJSON *n=0,*p=0,*a=cJSON_CreateArray();for(i=0;a && ichild=n;else suffix_object(p,n);p=n;}return a;} +cJSON *cJSON_CreateDoubleArray(const double *numbers,int count) {int i;cJSON *n=0,*p=0,*a=cJSON_CreateArray();for(i=0;a && ichild=n;else suffix_object(p,n);p=n;}return a;} +cJSON *cJSON_CreateStringArray(const char **strings,int count) {int i;cJSON *n=0,*p=0,*a=cJSON_CreateArray();for(i=0;a && ichild=n;else suffix_object(p,n);p=n;}return a;} + +/* Duplication */ +cJSON *cJSON_Duplicate(cJSON *item,int recurse) +{ + cJSON *newitem,*cptr,*nptr=0,*newchild; + /* Bail on bad ptr */ + if (!item) return 0; + /* Create new item */ + newitem=cJSON_New_Item(); + if (!newitem) return 0; + /* Copy over all vars */ + newitem->type=item->type&(~cJSON_IsReference),newitem->valueint=item->valueint,newitem->valuedouble=item->valuedouble; + if (item->valuestring) {newitem->valuestring=cJSON_strdup(item->valuestring); if (!newitem->valuestring) {cJSON_Delete(newitem);return 0;}} + if (item->string) {newitem->string=cJSON_strdup(item->string); if (!newitem->string) {cJSON_Delete(newitem);return 0;}} + /* If non-recursive, then we're done! */ + if (!recurse) return newitem; + /* Walk the ->next chain for the child. */ + cptr=item->child; + while (cptr) + { + newchild=cJSON_Duplicate(cptr,1); /* Duplicate (with recurse) each item in the ->next chain */ + if (!newchild) {cJSON_Delete(newitem);return 0;} + if (nptr) {nptr->next=newchild,newchild->prev=nptr;nptr=newchild;} /* If newitem->child already set, then crosswire ->prev and ->next and move on */ + else {newitem->child=newchild;nptr=newchild;} /* Set newitem->child and move to it */ + cptr=cptr->next; + } + return newitem; +} + +void cJSON_Minify(char *json) +{ + char *into=json; + while (*json) + { + if (*json==' ') json++; + else if (*json=='\t') json++; // Whitespace characters. + else if (*json=='\r') json++; + else if (*json=='\n') json++; + else if (*json=='/' && json[1]=='/') while (*json && *json!='\n') json++; // double-slash comments, to end of line. + else if (*json=='/' && json[1]=='*') {while (*json && !(*json=='*' && json[1]=='/')) json++;json+=2;} // multiline comments. + else if (*json=='\"'){*into++=*json++;while (*json && *json!='\"'){if (*json=='\\') *into++=*json++;*into++=*json++;}*into++=*json++;} // string literals, which are \" sensitive. + else *into++=*json++; // All other characters. + } + *into=0; // and null-terminate. +} \ No newline at end of file diff --git a/src/openalpr/cjson.h b/src/openalpr/cjson.h new file mode 100644 index 0000000..867b7c3 --- /dev/null +++ b/src/openalpr/cjson.h @@ -0,0 +1,143 @@ +/* + Copyright (c) 2009 Dave Gamble + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef cJSON__h +#define cJSON__h + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* cJSON Types: */ +#define cJSON_False 0 +#define cJSON_True 1 +#define cJSON_NULL 2 +#define cJSON_Number 3 +#define cJSON_String 4 +#define cJSON_Array 5 +#define cJSON_Object 6 + +#define cJSON_IsReference 256 + +/* The cJSON structure: */ +typedef struct cJSON { + struct cJSON *next,*prev; /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */ + struct cJSON *child; /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */ + + int type; /* The type of the item, as above. */ + + char *valuestring; /* The item's string, if type==cJSON_String */ + int valueint; /* The item's number, if type==cJSON_Number */ + double valuedouble; /* The item's number, if type==cJSON_Number */ + + char *string; /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ +} cJSON; + +typedef struct cJSON_Hooks { + void *(*malloc_fn)(size_t sz); + void (*free_fn)(void *ptr); +} cJSON_Hooks; + +/* Supply malloc, realloc and free functions to cJSON */ +extern void cJSON_InitHooks(cJSON_Hooks* hooks); + + +/* Supply a block of JSON, and this returns a cJSON object you can interrogate. Call cJSON_Delete when finished. */ +extern cJSON *cJSON_Parse(const char *value); +/* Render a cJSON entity to text for transfer/storage. Free the char* when finished. */ +extern char *cJSON_Print(cJSON *item); +/* Render a cJSON entity to text for transfer/storage without any formatting. Free the char* when finished. */ +extern char *cJSON_PrintUnformatted(cJSON *item); +/* Delete a cJSON entity and all subentities. */ +extern void cJSON_Delete(cJSON *c); + +/* Returns the number of items in an array (or object). */ +extern int cJSON_GetArraySize(cJSON *array); +/* Retrieve item number "item" from array "array". Returns NULL if unsuccessful. */ +extern cJSON *cJSON_GetArrayItem(cJSON *array,int item); +/* Get item "string" from object. Case insensitive. */ +extern cJSON *cJSON_GetObjectItem(cJSON *object,const char *string); + +/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */ +extern const char *cJSON_GetErrorPtr(void); + +/* These calls create a cJSON item of the appropriate type. */ +extern cJSON *cJSON_CreateNull(void); +extern cJSON *cJSON_CreateTrue(void); +extern cJSON *cJSON_CreateFalse(void); +extern cJSON *cJSON_CreateBool(int b); +extern cJSON *cJSON_CreateNumber(double num); +extern cJSON *cJSON_CreateString(const char *string); +extern cJSON *cJSON_CreateArray(void); +extern cJSON *cJSON_CreateObject(void); + +/* These utilities create an Array of count items. */ +extern cJSON *cJSON_CreateIntArray(const int *numbers,int count); +extern cJSON *cJSON_CreateFloatArray(const float *numbers,int count); +extern cJSON *cJSON_CreateDoubleArray(const double *numbers,int count); +extern cJSON *cJSON_CreateStringArray(const char **strings,int count); + +/* Append item to the specified array/object. */ +extern void cJSON_AddItemToArray(cJSON *array, cJSON *item); +extern void cJSON_AddItemToObject(cJSON *object,const char *string,cJSON *item); +/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */ +extern void cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item); +extern void cJSON_AddItemReferenceToObject(cJSON *object,const char *string,cJSON *item); + +/* Remove/Detatch items from Arrays/Objects. */ +extern cJSON *cJSON_DetachItemFromArray(cJSON *array,int which); +extern void cJSON_DeleteItemFromArray(cJSON *array,int which); +extern cJSON *cJSON_DetachItemFromObject(cJSON *object,const char *string); +extern void cJSON_DeleteItemFromObject(cJSON *object,const char *string); + +/* Update array items. */ +extern void cJSON_ReplaceItemInArray(cJSON *array,int which,cJSON *newitem); +extern void cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem); + +/* Duplicate a cJSON item */ +extern cJSON *cJSON_Duplicate(cJSON *item,int recurse); +/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will +need to be released. With recurse!=0, it will duplicate any children connected to the item. +The item->next and ->prev pointers are always zero on return from Duplicate. */ + +/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ +extern cJSON *cJSON_ParseWithOpts(const char *value,const char **return_parse_end,int require_null_terminated); + +extern void cJSON_Minify(char *json); + +/* Macros for creating things quickly. */ +#define cJSON_AddNullToObject(object,name) cJSON_AddItemToObject(object, name, cJSON_CreateNull()) +#define cJSON_AddTrueToObject(object,name) cJSON_AddItemToObject(object, name, cJSON_CreateTrue()) +#define cJSON_AddFalseToObject(object,name) cJSON_AddItemToObject(object, name, cJSON_CreateFalse()) +#define cJSON_AddBoolToObject(object,name,b) cJSON_AddItemToObject(object, name, cJSON_CreateBool(b)) +#define cJSON_AddNumberToObject(object,name,n) cJSON_AddItemToObject(object, name, cJSON_CreateNumber(n)) +#define cJSON_AddStringToObject(object,name,s) cJSON_AddItemToObject(object, name, cJSON_CreateString(s)) + +/* When assigning an integer value, it needs to be propagated to valuedouble too. */ +#define cJSON_SetIntValue(object,val) ((object)?(object)->valueint=(object)->valuedouble=(val):(val)) + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/openalpr/colorfilter.cpp b/src/openalpr/colorfilter.cpp new file mode 100644 index 0000000..71e938f --- /dev/null +++ b/src/openalpr/colorfilter.cpp @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + +#include "colorfilter.h" + + + +ColorFilter::ColorFilter(Mat image, Mat characterMask, Config* config) +{ + + timespec startTime; + getTime(&startTime); + + this->config = config; + + this->debug = config->debugColorFiler; + + + this->grayscale = imageIsGrayscale(image); + + if (this->debug) + cout << "ColorFilter: isGrayscale = " << grayscale << endl; + + this->hsv = Mat(image.size(), image.type()); + cvtColor( image, this->hsv, CV_BGR2HSV ); + preprocessImage(); + + this->charMask = characterMask; + + this->colorMask = Mat(image.size(), CV_8U); + + findCharColors(); + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << " -- ColorFilter Time: " << diffclock(startTime, endTime) << "ms." << endl; + } +} + +ColorFilter::~ColorFilter() +{ + +} + +bool ColorFilter::imageIsGrayscale(Mat image) +{ + // Check whether the original image is grayscale. If it is, we shouldn't attempt any color filter + for (int row = 0; row < image.rows; row++) + { + for (int col = 0; col < image.cols; col++) + { + int r = (int) image.at(row, col)[0]; + int g = (int) image.at(row, col)[1]; + int b = (int) image.at(row, col)[2]; + + if (r == g == b) + { + // So far so good + } + else + { + // Image is color. + return false; + } + } + } + + return true; +} + +void ColorFilter::preprocessImage() +{ + // Equalize the brightness on the HSV channel "V" + vector channels; + split(this->hsv,channels); + Mat img_equalized = equalizeBrightness(channels[2]); + merge(channels,this->hsv); +} + +// Gets the hue/sat/val for areas that we believe are license plate characters +// Then uses that to filter the whole image and provide a mask. +void ColorFilter::findCharColors() +{ + int MINIMUM_SATURATION = 45; + + if (this->debug) + cout << "ColorFilter::findCharColors" << endl; + + //charMask.copyTo(this->colorMask); + this->colorMask = Mat::zeros(charMask.size(), CV_8U); + bitwise_not(this->colorMask, this->colorMask); + + Mat erodedCharMask(charMask.size(), CV_8U); + Mat element = getStructuringElement( 1, + Size( 2 + 1, 2+1 ), + Point( 1, 1 ) ); + erode(charMask, erodedCharMask, element); + + vector > contours; + vector hierarchy; + findContours(erodedCharMask, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE); + + + + vector hMeans, sMeans, vMeans; + vector hStdDevs, sStdDevs, vStdDevs; + + for (int i = 0; i < contours.size(); i++) + { + if (hierarchy[i][3] != -1) + continue; + + Mat singleCharMask = Mat::zeros(hsv.size(), CV_8U); + + drawContours(singleCharMask, contours, + i, // draw this contour + cv::Scalar(255,255,255), // in + CV_FILLED, + 8, + hierarchy + ); + + // get rid of the outline by drawing a 1 pixel width black line + drawContours(singleCharMask, contours, + i, // draw this contour + cv::Scalar(0,0,0), // in + 1, + 8, + hierarchy + ); + + + + + //drawAndWait(&singleCharMask); + + Scalar mean; + Scalar stddev; + meanStdDev(hsv, mean, stddev, singleCharMask); + + if (this->debug) + { + cout << "ColorFilter " << setw(3) << i << ". Mean: h: " << setw(7) << mean[0] << " s: " << setw(7) <getMajorityOpinion(hMeans, .65, 30); + int bestSatIndex = this->getMajorityOpinion(sMeans, .65, 35); + int bestValIndex = this->getMajorityOpinion(vMeans, .65, 30); + + + if (sMeans[bestSatIndex] < MINIMUM_SATURATION) + return; + + + bool doHueFilter = false, doSatFilter = false, doValFilter = false; + float hueMin, hueMax; + float satMin, satMax; + float valMin, valMax; + + if (this->debug) + cout << "ColorFilter Winning indices:" << endl; + if (bestHueIndex != -1) + { + doHueFilter = true; + hueMin = hMeans[bestHueIndex] - (2 * hStdDevs[bestHueIndex]); + hueMax = hMeans[bestHueIndex] + (2 * hStdDevs[bestHueIndex]); + + if (abs(hueMin - hueMax) < 20) + { + hueMin = hMeans[bestHueIndex] - 20; + hueMax = hMeans[bestHueIndex] + 20; + } + + if (hueMin < 0) + hueMin = 0; + if (hueMax > 180) + hueMax = 180; + + if (this->debug) + cout << "ColorFilter Hue: " << bestHueIndex << " : " << setw(7) << hMeans[bestHueIndex] << " -- " << hueMin << "-" << hueMax << endl; + } + if (bestSatIndex != -1) + { + doSatFilter = true; + + satMin = sMeans[bestSatIndex] - (2 * sStdDevs[bestSatIndex]); + satMax = sMeans[bestSatIndex] + (2 * sStdDevs[bestSatIndex]); + + if (abs(satMin - satMax) < 20) + { + satMin = sMeans[bestSatIndex] - 20; + satMax = sMeans[bestSatIndex] + 20; + } + + if (satMin < 0) + satMin = 0; + if (satMax > 255) + satMax = 255; + + if (this->debug) + cout << "ColorFilter Sat: " << bestSatIndex << " : " << setw(7) << sMeans[bestSatIndex] << " -- " << satMin << "-" << satMax << endl; + } + if (bestValIndex != -1) + { + doValFilter = true; + + valMin = vMeans[bestValIndex] - (1.5 * vStdDevs[bestValIndex]); + valMax = vMeans[bestValIndex] + (1.5 * vStdDevs[bestValIndex]); + + if (abs(valMin - valMax) < 20) + { + valMin = vMeans[bestValIndex] - 20; + valMax = vMeans[bestValIndex] + 20; + } + + if (valMin < 0) + valMin = 0; + if (valMax > 255) + valMax = 255; + + if (this->debug) + cout << "ColorFilter Val: " << bestValIndex << " : " << setw(7) << vMeans[bestValIndex] << " -- " << valMin << "-" << valMax << endl; + } + + + + Mat imgDebugHueOnly = Mat::zeros(hsv.size(), hsv.type()); + Mat imgDebug = Mat::zeros(hsv.size(), hsv.type()); + Mat imgDistanceFromCenter = Mat::zeros(hsv.size(), CV_8U); + Mat debugMask = Mat::zeros(hsv.size(), CV_8U); + bitwise_not(debugMask, debugMask); + + for (int row = 0; row < charMask.rows; row++) + { + for (int col = 0; col < charMask.cols; col++) + { + int h = (int) hsv.at(row, col)[0]; + int s = (int) hsv.at(row, col)[1]; + int v = (int) hsv.at(row, col)[2]; + + bool hPasses = true; + bool sPasses = true; + bool vPasses = true; + + int vDistance = abs(v - vMeans[bestValIndex]); + + imgDebugHueOnly.at(row, col)[0] = h; + imgDebugHueOnly.at(row, col)[1] = 255; + imgDebugHueOnly.at(row, col)[2] = 255; + + imgDebug.at(row, col)[0] = 255; + imgDebug.at(row, col)[1] = 255; + imgDebug.at(row, col)[2] = 255; + + if (doHueFilter && (h < hueMin || h > hueMax)) + { + hPasses = false; + imgDebug.at(row, col)[0] = 0; + debugMask.at(row, col) = 0; + } + if (doSatFilter && (s < satMin || s > satMax)) + { + sPasses = false; + imgDebug.at(row, col)[1] = 0; + } + if (doValFilter && (v < valMin || v > valMax)) + { + vPasses = false; + imgDebug.at(row, col)[2] = 0; + } + + //if (pixelPasses) + // colorMask.at(row, col) = 255; + //else + //imgDebug.at(row, col)[0] = hPasses & 255; + //imgDebug.at(row, col)[1] = sPasses & 255; + //imgDebug.at(row, col)[2] = vPasses & 255; + + if ((hPasses) || (hPasses && sPasses))//(hPasses && vPasses) || (sPasses && vPasses) || + this->colorMask.at(row, col) = 255; + else + this->colorMask.at(row, col) = 0; + + + if ((hPasses && sPasses) || (hPasses && vPasses) || (sPasses && vPasses)) + { + vDistance = pow(vDistance, 0.9); + } + else + { + vDistance = pow(vDistance, 1.1); + } + if (vDistance > 255) + vDistance = 255; + imgDistanceFromCenter.at(row, col) = vDistance; + } + } + + + + vector debugImagesSet; + + if (this->debug) + { + debugImagesSet.push_back(addLabel(charMask, "Charecter mask")); + //debugImagesSet1.push_back(erodedCharMask); + Mat maskCopy(colorMask.size(), colorMask.type()); + colorMask.copyTo(maskCopy); + debugImagesSet.push_back(addLabel(maskCopy, "color Mask Before")); + } + + + Mat bigElement = getStructuringElement( 1, + Size( 3 + 1, 3+1 ), + Point( 1, 1 ) ); + + Mat smallElement = getStructuringElement( 1, + Size( 1 + 1, 1+1 ), + Point( 1, 1 ) ); + + morphologyEx(this->colorMask, this->colorMask, MORPH_CLOSE, bigElement); + //dilate(this->colorMask, this->colorMask, bigElement); + + Mat combined(charMask.size(), charMask.type()); + bitwise_and(charMask, colorMask, combined); + + if (this->debug) + { + debugImagesSet.push_back(addLabel(colorMask, "Color Mask After")); + + debugImagesSet.push_back(addLabel(combined, "Combined")); + + //displayImage(config, "COLOR filter Mask", colorMask); + debugImagesSet.push_back(addLabel(imgDebug, "Color filter Debug")); + + cvtColor(imgDebugHueOnly, imgDebugHueOnly, CV_HSV2BGR); + debugImagesSet.push_back(addLabel(imgDebugHueOnly, "Color Filter Hue")); + + equalizeHist(imgDistanceFromCenter, imgDistanceFromCenter); + debugImagesSet.push_back(addLabel(imgDistanceFromCenter, "COLOR filter Distance")); + + debugImagesSet.push_back(addLabel(debugMask, "COLOR Hues off")); + + + Mat dashboard = drawImageDashboard(debugImagesSet, imgDebugHueOnly.type(), 3); + displayImage(config, "Color Filter Images", dashboard); + } + +} + + + +// Goes through an array of values, picks the winner based on the highest percentage of other values that are within the maxValDifference +// Return -1 if it fails. +int ColorFilter::getMajorityOpinion(vector values, float minPercentAgreement, float maxValDifference) +{ + float bestPercentAgreement = 0; + float lowestOverallDiff = 1000000000; + int bestPercentAgreementIndex = -1; + + for (int i = 0; i < values.size(); i++) + { + int valuesInRange = 0; + float overallDiff = 0; + for (int j = 0; j < values.size(); j++) + { + float diff = abs(values[i] - values[j]); + if (diff < maxValDifference) + valuesInRange++; + + overallDiff += diff; + } + + float percentAgreement = ((float) valuesInRange) / ((float) values.size()); + if (overallDiff < lowestOverallDiff && percentAgreement >= bestPercentAgreement && percentAgreement >= minPercentAgreement) + { + bestPercentAgreement = percentAgreement; + bestPercentAgreementIndex = i; + lowestOverallDiff = overallDiff; + } + } + + return bestPercentAgreementIndex; +} diff --git a/src/openalpr/colorfilter.h b/src/openalpr/colorfilter.h new file mode 100644 index 0000000..d4075ac --- /dev/null +++ b/src/openalpr/colorfilter.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + +#ifndef COLORFILTER_H +#define COLORFILTER_H + +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/imgproc/imgproc.hpp" + +#include "constants.h" +#include "utility.h" +#include "config.h" + +using namespace cv; +using namespace std; + + + +class ColorFilter +{ + + public: + ColorFilter(Mat image, Mat characterMask, Config* config); + virtual ~ColorFilter(); + + Mat colorMask; + + + + private: + + Config* config; + bool debug; + + Mat hsv; + Mat charMask; + + + bool grayscale; + + void preprocessImage(); + void findCharColors(); + + bool imageIsGrayscale(Mat image); + int getMajorityOpinion(vector values, float minPercentAgreement, float maxValDifference); +}; + +#endif // COLORFILTER_H \ No newline at end of file diff --git a/src/openalpr/config.cpp b/src/openalpr/config.cpp new file mode 100644 index 0000000..50a1720 --- /dev/null +++ b/src/openalpr/config.cpp @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#include "config.h" + + +Config::Config(const std::string country, const std::string runtimeBaseDir) +{ + this->runtimeBaseDir = runtimeBaseDir; + + ini = new CSimpleIniA(); + + char* envRuntimeDir; + envRuntimeDir = getenv (ENV_VARIABLE_RUNTIME_DIR); + if (runtimeBaseDir.compare("") != 0) + { + // User has supplied a runtime directory. Use that. + + } + else if (envRuntimeDir!=NULL) + { + // Environment variable is non-empty. Use that. + this->runtimeBaseDir = envRuntimeDir; + } + else + { + // Use the default + this->runtimeBaseDir = DEFAULT_RUNTIME_DIR; + } + + string configFile = (this->runtimeBaseDir + CONFIG_FILE); + + if (DirectoryExists(this->runtimeBaseDir.c_str()) == false) + { + std::cerr << "--(!)Runtime directory '" << this->runtimeBaseDir << "' does not exist!" << endl; + return; + } + else if (fileExists(configFile.c_str()) == false) + { + std::cerr << "--(!)Runtime directory '" << this->runtimeBaseDir << "' does not contain a config file '" << CONFIG_FILE << "'!" << endl; + return; + } + + ini->LoadFile(configFile.c_str()); + + this->country = country; + + loadValues(country); +} +Config::~Config() +{ + delete ini; +} + +void Config::loadValues(string country) +{ + maxPlateWidthPercent = getFloat("common", "max_plate_width_percent", 100); + maxPlateHeightPercent = getFloat("common", "max_plate_height_percent", 100); + + minPlateSizeWidthPx = getInt(country, "min_plate_size_width_px", 100); + minPlateSizeHeightPx = getInt(country, "min_plate_size_height_px", 100); + + plateWidthMM = getFloat(country, "plate_width_mm", 100); + plateHeightMM = getFloat(country, "plate_height_mm", 100); + + charHeightMM = getFloat(country, "char_height_mm", 100); + charWidthMM = getFloat(country, "char_width_mm", 100); + charWhitespaceTopMM = getFloat(country, "char_whitespace_top_mm", 100); + charWhitespaceBotMM = getFloat(country, "char_whitespace_bot_mm", 100); + + templateWidthPx = getInt(country, "template_max_width_px", 100); + templateHeightPx = getInt(country, "template_max_height_px", 100); + + float ocrImagePercent = getFloat("common", "ocr_img_size_percent", 100); + ocrImageWidthPx = round(((float) templateWidthPx) * ocrImagePercent); + ocrImageHeightPx = round(((float)templateHeightPx) * ocrImagePercent); + + + float stateIdImagePercent = getFloat("common", "state_id_img_size_percent", 100); + stateIdImageWidthPx = round(((float)templateWidthPx) * ocrImagePercent); + stateIdimageHeightPx = round(((float)templateHeightPx) * ocrImagePercent); + + + charAnalysisMinPercent = getFloat(country, "char_analysis_min_pct", 0); + charAnalysisHeightRange = getFloat(country, "char_analysis_height_range", 0); + charAnalysisHeightStepSize = getFloat(country, "char_analysis_height_step_size", 0); + charAnalysisNumSteps = getInt(country, "char_analysis_height_num_steps", 0); + + segmentationMinBoxWidthPx = getInt(country, "segmentation_min_box_width_px", 0); + segmentationMinCharHeightPercent = getFloat(country, "segmentation_min_charheight_percent", 0); + segmentationMaxCharWidthvsAverage = getFloat(country, "segmentation_max_segment_width_percent_vs_average", 0); + + plateLinesSensitivityVertical = getFloat(country, "plateline_sensitivity_vertical", 0); + plateLinesSensitivityHorizontal = getFloat(country, "plateline_sensitivity_horizontal", 0); + + ocrLanguage = getString(country, "ocr_language", "none"); + ocrMinFontSize = getInt("common", "ocr_min_font_point", 100); + + postProcessMinConfidence = getFloat("common", "postprocess_min_confidence", 100); + postProcessConfidenceSkipLevel = getFloat("common", "postprocess_confidence_skip_level", 100); + postProcessMaxSubstitutions = getInt("common", "postprocess_max_substitutions", 100); + postProcessMinCharacters = getInt("common", "postprocess_min_characers", 100); + postProcessMaxCharacters = getInt("common", "postprocess_max_characers", 100); + + debugGeneral = getBoolean("debug", "general", false); + debugTiming = getBoolean("debug", "timing", false); + debugStateId = getBoolean("debug", "state_id", false); + debugPlateLines = getBoolean("debug", "plate_lines", false); + debugPlateCorners = getBoolean("debug", "plate_corners", false); + debugCharRegions = getBoolean("debug", "char_regions", false); + debugCharSegmenter = getBoolean("debug", "char_segment", false); + debugCharAnalysis = getBoolean("debug", "char_analysis", false); + debugColorFiler = getBoolean("debug", "color_filter", false); + debugOcr = getBoolean("debug", "ocr", false); + debugPostProcess = getBoolean("debug", "postprocess", false); + debugShowImages = getBoolean("debug", "show_images", false); + +} + +void Config::debugOff() +{ + debugGeneral = false; + debugTiming = false; + debugStateId = false; + debugPlateLines = false; + debugPlateCorners = false; + debugCharRegions = false; + debugCharSegmenter = false; + debugCharAnalysis = false; + debugColorFiler = false; + debugOcr = false; + debugPostProcess = false; +} + + +string Config::getCascadeRuntimeDir() +{ + return this->runtimeBaseDir + CASCADE_DIR; +} +string Config::getKeypointsRuntimeDir() +{ + return this->runtimeBaseDir + KEYPOINTS_DIR; +} +string Config::getPostProcessRuntimeDir() +{ + return this->runtimeBaseDir + POSTPROCESS_DIR; +} +string Config::getTessdataPrefix() +{ + return "TESSDATA_PREFIX=" + this->runtimeBaseDir + "/ocr/"; +} + + + + +float Config::getFloat(string section, string key, float defaultValue) +{ + const char * pszValue = ini->GetValue(section.c_str(), key.c_str(), NULL /*default*/); + if (pszValue == NULL) + { + std::cout << "Error: missing configuration entry for: " << section << "->" << key << endl; + return defaultValue; + } + + float val = atof(pszValue); + return val; +} +int Config::getInt(string section, string key, int defaultValue) +{ + const char * pszValue = ini->GetValue(section.c_str(), key.c_str(), NULL /*default*/); + if (pszValue == NULL) + { + std::cout << "Error: missing configuration entry for: " << section << "->" << key << endl; + return defaultValue; + } + + int val = atoi(pszValue); + return val; +} +bool Config::getBoolean(string section, string key, bool defaultValue) +{ + const char * pszValue = ini->GetValue(section.c_str(), key.c_str(), NULL /*default*/); + if (pszValue == NULL) + { + std::cout << "Error: missing configuration entry for: " << section << "->" << key << endl; + return defaultValue; + } + + int val = atoi(pszValue); + return val != 0; +} +string Config::getString(string section, string key, string defaultValue) +{ + const char * pszValue = ini->GetValue(section.c_str(), key.c_str(), NULL /*default*/); + if (pszValue == NULL) + { + std::cout << "Error: missing configuration entry for: " << section << "->" << key << endl; + return defaultValue; + } + + string val = string(pszValue); + return val; +} diff --git a/src/openalpr/config.h b/src/openalpr/config.h new file mode 100644 index 0000000..b18dc07 --- /dev/null +++ b/src/openalpr/config.h @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + +#ifndef CONFIG_H +#define CONFIG_H + + +#include "simpleini/simpleini.h" +#include "support/filesystem.h" + +#include "linux_dev.h" + +#include +#include +#include /* getenv */ +#include + +using namespace std; + +class Config +{ + + public: + Config(const std::string country, const std::string runtimeDir = ""); + virtual ~Config(); + + string country; + + float maxPlateWidthPercent; + float maxPlateHeightPercent; + + float minPlateSizeWidthPx; + float minPlateSizeHeightPx; + + float plateWidthMM; + float plateHeightMM; + + float charHeightMM; + float charWidthMM; + float charWhitespaceTopMM; + float charWhitespaceBotMM; + + int templateWidthPx; + int templateHeightPx; + + int ocrImageWidthPx; + int ocrImageHeightPx; + + int stateIdImageWidthPx; + int stateIdimageHeightPx; + + float charAnalysisMinPercent; + float charAnalysisHeightRange; + float charAnalysisHeightStepSize; + int charAnalysisNumSteps; + + float plateLinesSensitivityVertical; + float plateLinesSensitivityHorizontal; + + int segmentationMinBoxWidthPx; + float segmentationMinCharHeightPercent; + float segmentationMaxCharWidthvsAverage; + + string ocrLanguage; + int ocrMinFontSize; + + float postProcessMinConfidence; + float postProcessConfidenceSkipLevel; + int postProcessMaxSubstitutions; + int postProcessMinCharacters; + int postProcessMaxCharacters; + + + bool debugGeneral; + bool debugTiming; + bool debugStateId; + bool debugPlateLines; + bool debugPlateCorners; + bool debugCharRegions; + bool debugCharSegmenter; + bool debugCharAnalysis; + bool debugColorFiler; + bool debugOcr; + bool debugPostProcess; + bool debugShowImages; + + void debugOff(); + + string getKeypointsRuntimeDir(); + string getCascadeRuntimeDir(); + string getPostProcessRuntimeDir(); + string getTessdataPrefix(); + +private: + CSimpleIniA* ini; + + string runtimeBaseDir; + + void loadValues(string country); + + int getInt(string section, string key, int defaultValue); + float getFloat(string section, string key, float defaultValue); + string getString(string section, string key, string defaultValue); + bool getBoolean(string section, string key, bool defaultValue); +}; + + +#endif // CONFIG_H \ No newline at end of file diff --git a/src/openalpr/constants.h b/src/openalpr/constants.h new file mode 100644 index 0000000..b014f7b --- /dev/null +++ b/src/openalpr/constants.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#include "linux_dev.h" + diff --git a/src/openalpr/featurematcher.cpp b/src/openalpr/featurematcher.cpp new file mode 100644 index 0000000..b21a289 --- /dev/null +++ b/src/openalpr/featurematcher.cpp @@ -0,0 +1,432 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + +#include "featurematcher.h" + + +//const int DEFAULT_QUERY_FEATURES = 305; +//const int DEFAULT_TRAINING_FEATURES = 305; +const float MAX_DISTANCE_TO_MATCH = 100.0f; + + +FeatureMatcher::FeatureMatcher(Config* config) +{ + this->config = config; + + //this->descriptorMatcher = DescriptorMatcher::create( "BruteForce-HammingLUT" ); + this->descriptorMatcher = new BFMatcher(NORM_HAMMING, false); + + //this->descriptorMatcher = DescriptorMatcher::create( "FlannBased" ); + + + this->detector = new FastFeatureDetector(10, true); + this->extractor = new BRISK(10, 1, 0.9); +} + +FeatureMatcher::~FeatureMatcher() +{ + for (int i = 0; i < trainingImgKeypoints.size(); i++) + trainingImgKeypoints[i].clear(); + trainingImgKeypoints.clear(); + + descriptorMatcher.release(); + detector.release(); + extractor.release(); + +} + + +bool FeatureMatcher::isLoaded() +{ + if( detector.empty() || extractor.empty() || descriptorMatcher.empty() ) + { + return false; + } + + return true; +} + +int FeatureMatcher::numTrainingElements() +{ + return billMapping.size(); +} + + + + +void FeatureMatcher::surfStyleMatching( const Mat& queryDescriptors, vector queryKeypoints, + vector& matches12 ) +{ + vector > matchesKnn; + + this->descriptorMatcher->radiusMatch(queryDescriptors, matchesKnn, MAX_DISTANCE_TO_MATCH); + + + vector tempMatches; + _surfStyleMatching(queryDescriptors, matchesKnn, tempMatches); + + crisscrossFiltering(queryKeypoints, tempMatches, matches12); +} + + + +void FeatureMatcher::_surfStyleMatching(const Mat& queryDescriptors, vector > matchesKnn, vector& matches12) +{ + + //objectMatches.clear(); + //objectMatches.resize(objectIds.size()); + //cout << "starting matcher" << matchesKnn.size() << endl; + for (int descInd = 0; descInd < queryDescriptors.rows; descInd++) + { + const std::vector & matches = matchesKnn[descInd]; + //cout << "two: " << descInd << ":" << matches.size() << endl; + + // Check to make sure we have 2 matches. I think this is always the case, but it doesn't hurt to be sure + if (matchesKnn[descInd].size() > 1) + { + + // Next throw out matches with a crappy score + // Ignore... already handled by the radiusMatch + //if (matchesKnn[descInd][0].distance < MAX_DISTANCE_TO_MATCH) + //{ + float ratioThreshold = 0.75; + + // Check if both matches came from the same image. If they both came from the same image, score them slightly less harshly + if (matchesKnn[descInd][0].imgIdx == matchesKnn[descInd][1].imgIdx) + { + ratioThreshold = 0.85; + } + + if ((matchesKnn[descInd][0].distance / matchesKnn[descInd][1].distance) < ratioThreshold) + { + bool already_exists = false; + // Quickly run through the matches we've already added and make sure it's not a duplicate... + for (int q = 0; q < matches12.size(); q++) + { + if (matchesKnn[descInd][0].queryIdx == matches12[q].queryIdx) + { + already_exists = true; + break; + } + else if ((matchesKnn[descInd][0].trainIdx == matches12[q].trainIdx) && + (matchesKnn[descInd][0].imgIdx == matches12[q].imgIdx)) + { + already_exists = true; + break; + } + } + + // Good match. + if (already_exists == false) + matches12.push_back(matchesKnn[descInd][0]); + } + + + //} + } + else if (matchesKnn[descInd].size() == 1) + { + // Only match? Does this ever happen? + matches12.push_back(matchesKnn[descInd][0]); + } + // In the ratio test, we will compare the quality of a match with the next match that is not from the same object: + // we can accept several matches with similar scores as long as they are for the same object. Those should not be + // part of the model anyway as they are not discriminative enough + + //for (unsigned int first_index = 0; first_index < matches.size(); ++first_index) + //{ + + //matches12.push_back(match); + //} + + + } + + + +} + +// Compares the matches keypoints for parallel lines. Removes matches that are criss-crossing too much +// We assume that license plates won't be upside-down or backwards. So expect lines to be closely parallel +void FeatureMatcher::crisscrossFiltering(const vector queryKeypoints, const vector inputMatches, vector &outputMatches) +{ + + Rect crissCrossAreaVertical(0, 0, config->stateIdImageWidthPx, config->stateIdimageHeightPx * 2); + Rect crissCrossAreaHorizontal(0, 0, config->stateIdImageWidthPx * 2, config->stateIdimageHeightPx); + + for (int i = 0; i < billMapping.size(); i++) + { + vector matchesForOnePlate; + for (int j = 0; j < inputMatches.size(); j++) + { + if (inputMatches[j].imgIdx == i) + matchesForOnePlate.push_back(inputMatches[j]); + } + + // For each plate, compare the lines for the keypoints (training image and query image) + // go through each line between keypoints and filter out matches that are criss-crossing + vector vlines; + vector hlines; + vector matchIdx; + + for (int j = 0; j < matchesForOnePlate.size(); j++) + { + KeyPoint tkp = trainingImgKeypoints[i][matchesForOnePlate[j].trainIdx]; + KeyPoint qkp = queryKeypoints[matchesForOnePlate[j].queryIdx]; + + vlines.push_back(LineSegment(tkp.pt.x, tkp.pt.y + config->stateIdimageHeightPx, qkp.pt.x, qkp.pt.y)); + hlines.push_back(LineSegment(tkp.pt.x, tkp.pt.y, qkp.pt.x + config->stateIdImageWidthPx, qkp.pt.y)); + matchIdx.push_back(j); + } + + + + // Iterate through each line (n^2) removing the one with the most criss-crosses until there are none left. + int mostIntersections = 1; + while (mostIntersections > 0 && vlines.size() > 0) + { + int mostIntersectionsIndex = -1; + mostIntersections = 0; + + for (int j = 0; j < vlines.size(); j++) + { + int intrCount = 0; + for (int q = 0; q < vlines.size(); q++) + { + Point vintr = vlines[j].intersection(vlines[q]); + Point hintr = hlines[j].intersection(hlines[q]); + float vangleDiff = abs(vlines[j].angle - vlines[q].angle); + float hangleDiff = abs(hlines[j].angle - hlines[q].angle); + if (vintr.inside(crissCrossAreaVertical) && vangleDiff > 10) + { + intrCount++; + } + else if (hintr.inside(crissCrossAreaHorizontal) && hangleDiff > 10) + { + intrCount++; + } + } + + if (intrCount > mostIntersections) + { + mostIntersections = intrCount; + mostIntersectionsIndex = j; + } + } + + if (mostIntersectionsIndex >= 0) + { + if (this->config->debugStateId) + cout << "Filtered intersection! " << billMapping[i] << endl; + vlines.erase(vlines.begin() + mostIntersectionsIndex); + hlines.erase(hlines.begin() + mostIntersectionsIndex); + matchIdx.erase(matchIdx.begin() + mostIntersectionsIndex); + } + + } + + // Push the non-crisscrosses back on the list + for (int j = 0; j < matchIdx.size(); j++) + { + outputMatches.push_back(matchesForOnePlate[matchIdx[j]]); + } + } + +} + + +// Returns true if successful, false otherwise +bool FeatureMatcher::loadRecognitionSet(string country) +{ + std::ostringstream out; + out << config->getKeypointsRuntimeDir() << "/" << country << "/"; + string country_dir = out.str(); + + + if (DirectoryExists(country_dir.c_str())) + { + vector trainImages; + vector plateFiles = getFilesInDir(country_dir.c_str()); + + for (int i = 0; i < plateFiles.size(); i++) + { + if (hasEnding(plateFiles[i], ".jpg") == false) + continue; + + string fullpath = country_dir + plateFiles[i]; + Mat img = imread( fullpath ); + + // convert to gray and resize to the size of the templates + cvtColor(img, img, CV_BGR2GRAY); + resize(img, img, getSizeMaintainingAspect(img, config->stateIdImageWidthPx, config->stateIdimageHeightPx)); + + if( img.empty() ) + { + cout << "Can not read images" << endl; + return -1; + } + + + Mat descriptors; + + vector keypoints; + detector->detect( img, keypoints ); + extractor->compute(img, keypoints, descriptors); + + if (descriptors.cols > 0) + { + billMapping.push_back(plateFiles[i].substr(0, 2)); + trainImages.push_back(descriptors); + trainingImgKeypoints.push_back(keypoints); + } + + } + + + this->descriptorMatcher->add(trainImages); + this->descriptorMatcher->train(); + + return true; + } + + return false; + +} + + + +RecognitionResult FeatureMatcher::recognize( const Mat& queryImg, bool drawOnImage, Mat* outputImage, + bool debug_on, vector debug_matches_array + ) +{ + RecognitionResult result; + + result.haswinner = false; + + Mat queryDescriptors; + vector queryKeypoints; + + detector->detect( queryImg, queryKeypoints ); + extractor->compute(queryImg, queryKeypoints, queryDescriptors); + + + + if (queryKeypoints.size() <= 5) + { + // Cut it loose if there's less than 5 keypoints... nothing would ever match anyway and it could crash the matcher. + if (drawOnImage) + { + drawKeypoints( queryImg, queryKeypoints, *outputImage, CV_RGB(0, 255, 0), DrawMatchesFlags::DEFAULT ); + } + return result; + } + + + + vector filteredMatches; + + surfStyleMatching( queryDescriptors, queryKeypoints, filteredMatches ); + + + // Create and initialize the counts to 0 + std::vector bill_match_counts( billMapping.size() ); + + for (int i = 0; i < billMapping.size(); i++) { bill_match_counts[i] = 0; } + + for (int i = 0; i < filteredMatches.size(); i++) + { + bill_match_counts[filteredMatches[i].imgIdx]++; + //if (filteredMatches[i].imgIdx + } + + + + float max_count = 0; // represented as a percent (0 to 100) + int secondmost_count = 0; + int maxcount_index = -1; + for (int i = 0; i < billMapping.size(); i++) + { + if (bill_match_counts[i] > max_count && bill_match_counts[i] >= 4) + { + secondmost_count = max_count; + if (secondmost_count <= 2) // A value of 1 or 2 is effectively 0 + secondmost_count = 0; + + max_count = bill_match_counts[i]; + maxcount_index = i; + } + } + + float score = ((max_count - secondmost_count - 3) / 10) * 100; + if (score < 0) + score = 0; + else if (score > 100) + score = 100; + + + if (score > 0) + { + result.haswinner = true; + result.winner = billMapping[maxcount_index]; + result.confidence = score; + + if (drawOnImage) + { + vector positiveMatches; + for (int i = 0; i < filteredMatches.size(); i++) + { + if (filteredMatches[i].imgIdx == maxcount_index) + { + positiveMatches.push_back( queryKeypoints[filteredMatches[i].queryIdx] ); + } + } + + Mat tmpImg; + drawKeypoints( queryImg, queryKeypoints, tmpImg, CV_RGB(185, 0, 0), DrawMatchesFlags::DEFAULT ); + drawKeypoints( tmpImg, positiveMatches, *outputImage, CV_RGB(0, 255, 0), DrawMatchesFlags::DEFAULT ); + + if (result.haswinner == true) + { + + std::ostringstream out; + out << result.winner << " (" << result.confidence << "%)"; + + // we detected a bill, let the people know! + //putText(*outputImage, out.str(), Point(15, 27), FONT_HERSHEY_DUPLEX, 1.1, CV_RGB(0, 0, 0), 2); + } + } + + } + + if (this->config->debugStateId) + { + + for (int i = 0; i < billMapping.size(); i++) + { + cout << billMapping[i] << " : " << bill_match_counts[i] << endl; + } + + } + + return result; + + +} + diff --git a/src/openalpr/featurematcher.h b/src/openalpr/featurematcher.h new file mode 100644 index 0000000..733d75a --- /dev/null +++ b/src/openalpr/featurematcher.h @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + + +#ifndef FEATUREMATCHER_H +#define FEATUREMATCHER_H + +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/calib3d/calib3d.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "opencv2/features2d/features2d.hpp" +#include "opencv2/video/tracking.hpp" + +#include "support/filesystem.h" +#include "constants.h" +#include "utility.h" +#include "config.h" + +using namespace cv; +using namespace std; + + + +struct RecognitionResult { + bool haswinner; + string winner; + int confidence; +} ; + +class FeatureMatcher +{ + + public: + FeatureMatcher(Config* config); + virtual ~FeatureMatcher(); + + + + RecognitionResult recognize( const Mat& queryImg, bool drawOnImage, Mat* outputImage, + bool debug_on, vector debug_matches_array ); + + + bool loadRecognitionSet(string country); + + bool isLoaded(); + + int numTrainingElements(); + + private: + Config* config; + + Ptr descriptorMatcher; + Ptr detector; + Ptr extractor; + + + vector > trainingImgKeypoints; + + + void _surfStyleMatching(const Mat& queryDescriptors, vector > matchesKnn, vector& matches12); + + void crisscrossFiltering(const vector queryKeypoints, const vector inputMatches, vector &outputMatches); + + vector billMapping; + + + + void surfStyleMatching( const Mat& queryDescriptors, vector queryKeypoints, + vector& matches12 ); + +}; + +#endif // FEATUREMATCHER_H + + diff --git a/src/openalpr/licenseplatecandidate.cpp b/src/openalpr/licenseplatecandidate.cpp new file mode 100644 index 0000000..2de4c6d --- /dev/null +++ b/src/openalpr/licenseplatecandidate.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#include "licenseplatecandidate.h" + + +LicensePlateCandidate::LicensePlateCandidate(Mat frame, Rect regionOfInterest, Config* config) +{ + this->config = config; + + this->frame = frame; + this->plateRegion = regionOfInterest; +} + +LicensePlateCandidate::~LicensePlateCandidate() +{ + delete charSegmenter; +} + +// Must delete this pointer in parent class +void LicensePlateCandidate::recognize() +{ + charSegmenter = NULL; + + + this->confidence = 0; + + int expandX = round(this->plateRegion.width * 0.15); + int expandY = round(this->plateRegion.height * 0.10); + // expand box by 15% in all directions + Rect expandedRegion = expandRect( this->plateRegion, expandX, expandY, frame.cols, frame.rows) ; + + + + + Mat plate_bgr = Mat(frame, expandedRegion); + resize(plate_bgr, plate_bgr, Size(config->templateWidthPx, config->templateHeightPx)); + + Mat plate_bgr_cleaned = Mat(plate_bgr.size(), plate_bgr.type()); + this->cleanupColors(plate_bgr, plate_bgr_cleaned); + + + CharacterRegion charRegion(plate_bgr, config); + + + if (charRegion.confidence > 10) + { + + PlateLines plateLines(config); + //Mat boogedy = charRegion.getPlateMask(); + + plateLines.processImage(charRegion.getPlateMask(), 1.15); + plateLines.processImage(plate_bgr_cleaned, 0.9); + + PlateCorners cornerFinder(plate_bgr, &plateLines, &charRegion, config); + vector smallPlateCorners = cornerFinder.findPlateCorners(); + + if (cornerFinder.confidence > 0) + { + this->plateCorners = transformPointsToOriginalImage(frame, plate_bgr, expandedRegion, smallPlateCorners); + + + this->deskewed = deSkewPlate(frame, this->plateCorners); + + + charSegmenter = new CharacterSegmenter(deskewed, charRegion.thresholdsInverted(), config); + + + //this->recognizedText = ocr->recognizedText; + //strcpy(this->recognizedText, ocr.recognizedText); + + this->confidence = 100; + + } + charRegion.confidence = 0; + } + + +} + + + + + +// Re-maps the coordinates from the smallImage to the coordinate space of the bigImage. +vector LicensePlateCandidate::transformPointsToOriginalImage(Mat bigImage, Mat smallImage, Rect region, vector corners) +{ + vector cornerPoints; + for (int i = 0; i < corners.size(); i++) + { + float bigX = (corners[i].x * ((float) region.width / smallImage.cols)); + float bigY = (corners[i].y * ((float) region.height / smallImage.rows)); + + bigX = bigX + region.x; + bigY = bigY + region.y; + + cornerPoints.push_back(Point2f(bigX, bigY)); + } + + return cornerPoints; +} + + +Mat LicensePlateCandidate::deSkewPlate(Mat inputImage, vector corners) +{ + + // Figure out the appoximate 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)); + LineSegment bottomEdge(round(corners[3].x), round(corners[3].y), round(corners[2].x), round(corners[2].y)); + + 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) + { + height = config->ocrImageHeightPx; + width = round(((float) height) * aspect); + } + + Mat deskewed(height, width, frame.type()); + + // Corners of the destination image + vector 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)); + + // Get transformation matrix + Mat transmtx = getPerspectiveTransform(corners, quad_pts); + + // Apply perspective transformation + warpPerspective(inputImage, deskewed, transmtx, deskewed.size()); + + if (this->config->debugGeneral) + displayImage(config, "quadrilateral", deskewed); + + return deskewed; +} + + +void LicensePlateCandidate::cleanupColors(Mat inputImage, Mat outputImage) +{ + if (this->config->debugGeneral) + cout << "LicensePlate::cleanupColors" << endl; + + //Mat normalized(inputImage.size(), inputImage.type()); + + Mat intermediate(inputImage.size(), inputImage.type()); + + normalize(inputImage, intermediate, 0, 255, CV_MINMAX ); + + // Equalize intensity: + if(intermediate.channels() >= 3) + { + Mat ycrcb; + + cvtColor(intermediate,ycrcb,CV_BGR2YCrCb); + + vector channels; + split(ycrcb,channels); + + equalizeHist(channels[0], channels[0]); + + merge(channels,ycrcb); + + cvtColor(ycrcb,intermediate,CV_YCrCb2BGR); + + //ycrcb.release(); + } + + + bilateralFilter(intermediate, outputImage, 3, 25, 35); + + + if (this->config->debugGeneral) + { + displayImage(config, "After cleanup", outputImage); + } + +} diff --git a/src/openalpr/licenseplatecandidate.h b/src/openalpr/licenseplatecandidate.h new file mode 100644 index 0000000..a34a799 --- /dev/null +++ b/src/openalpr/licenseplatecandidate.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + +#ifndef STAGE2_H +#define STAGE2_H + +#include +#include +#include +//#include + +#include "opencv2/imgproc/imgproc.hpp" +#include "opencv2/core/core.hpp" +#include "opencv2/highgui/highgui.hpp" + +#include "utility.h" +#include "constants.h" +#include "platelines.h" +#include "characterregion.h" +#include "charactersegmenter.h" +#include "platecorners.h" +#include "config.h" + +using namespace std; +using namespace cv; + + + +//vector getCharacterRegions(Mat frame, vector regionsOfInterest); +//vector getCharSegmentsBetweenLines(Mat img, vector > contours, LineSegment top, LineSegment bottom); + +class LicensePlateCandidate +{ + + public: + LicensePlateCandidate(Mat frame, Rect regionOfInterest, Config* config); + virtual ~LicensePlateCandidate(); + + float confidence; // 0-100 + //vector points; // top-left, top-right, bottom-right, bottom-left + vector plateCorners; + + void recognize(); + + Mat deskewed; + CharacterSegmenter* charSegmenter; + + private: + + Config* config; + + + Mat frame; + Rect plateRegion; + + void cleanupColors(Mat inputImage, Mat outputImage); + Mat filterByCharacterHue(vector > charRegionContours); + vector findPlateCorners(Mat inputImage, PlateLines plateLines, CharacterRegion charRegion); // top-left, top-right, bottom-right, bottom-left + + vector transformPointsToOriginalImage(Mat bigImage, Mat smallImage, Rect region, vector corners); + Mat deSkewPlate(Mat inputImage, vector corners); + +}; + + +#endif // STAGE2_H \ No newline at end of file diff --git a/src/openalpr/linux_dev.h b/src/openalpr/linux_dev.h new file mode 100644 index 0000000..8468d96 --- /dev/null +++ b/src/openalpr/linux_dev.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#define CONFIG_FILE "/openalpr.conf" +#define KEYPOINTS_DIR "/keypoints" +#define CASCADE_DIR "/region/" +#define POSTPROCESS_DIR "/postprocess" + +#define DEFAULT_RUNTIME_DIR "/home/mhill/projects/alpr/runtime_data" +#define ENV_VARIABLE_RUNTIME_DIR "OPENALPR_RUNTIME_DIR" + + diff --git a/src/openalpr/ocr.cpp b/src/openalpr/ocr.cpp new file mode 100644 index 0000000..b7d5976 --- /dev/null +++ b/src/openalpr/ocr.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + + +#include "ocr.h" + +OCR::OCR(Config* config) +{ + this->config = config; + + this->postProcessor = new PostProcess(config); + + tesseract=new TessBaseAPI(); + + // Tesseract requires the prefix directory to be set as an env variable + vector tessdataPrefix(config->getTessdataPrefix().size()); + + strcpy(tessdataPrefix.data(), config->getTessdataPrefix().c_str()); + putenv(tessdataPrefix.data()); + + + tesseract->Init("", config->ocrLanguage.c_str() ); + tesseract->SetVariable("save_blob_choices", "T"); + //tesseract->SetVariable("tessedit_char_whitelist", "ABCDEFGHIJKLMNPQRSTUVWXYZ1234567890"); + tesseract->SetPageSegMode(PSM_SINGLE_CHAR); +} + +OCR::~OCR() +{ + tesseract->Clear(); + delete postProcessor; + delete tesseract; +} + + +void OCR::performOCR(vector thresholds, vector charRegions) +{ + + + timespec startTime; + getTime(&startTime); + + + postProcessor->clear(); + + + for (int i = 0; i < thresholds.size(); i++) + { + + // Make it black text on white background + bitwise_not(thresholds[i], thresholds[i]); + tesseract->SetImage((uchar*) thresholds[i].data, thresholds[i].size().width, thresholds[i].size().height, thresholds[i].channels(), thresholds[i].step1()); + + + for (int j = 0; j < charRegions.size(); j++) + { + Rect expandedRegion = expandRect( charRegions[j], 2, 2, thresholds[i].cols, thresholds[i].rows) ; + + tesseract->SetRectangle(expandedRegion.x, expandedRegion.y, expandedRegion.width, expandedRegion.height); + tesseract->Recognize(NULL); + + tesseract::ResultIterator* ri = tesseract->GetIterator(); + tesseract::PageIteratorLevel level = tesseract::RIL_SYMBOL; + do { + const char* symbol = ri->GetUTF8Text(level); + float conf = ri->Confidence(level); + + bool dontcare; + int fontindex = 0; + int pointsize = 0; + const char* fontName = ri->WordFontAttributes(&dontcare, &dontcare, &dontcare, &dontcare, &dontcare, &dontcare, &pointsize, &fontindex); + + if(symbol != 0 && pointsize >= config->ocrMinFontSize) { + postProcessor->addLetter(*symbol, j, conf); + + + if (this->config->debugOcr) + printf("charpos%d: threshold %d: symbol %s, conf: %f font: %s (index %d) size %dpx", j, i, symbol, conf, fontName, fontindex, pointsize); + + bool indent = false; + tesseract::ChoiceIterator ci(*ri); + do { + const char* choice = ci.GetUTF8Text(); + + postProcessor->addLetter(*choice, j, ci.Confidence()); + + + //letterScores.addScore(*choice, j, ci.Confidence() - MIN_CONFIDENCE); + if (this->config->debugOcr) + { + if (indent) printf("\t\t "); + printf("\t- "); + printf("%s conf: %f\n", choice, ci.Confidence()); + } + + indent = true; + } while(ci.Next()); + } + + if (this->config->debugOcr) + printf("---------------------------------------------\n"); + + delete[] symbol; + } while((ri->Next(level))); + + delete ri; + } + + + } + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "OCR Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + + + +} + + + + + diff --git a/src/openalpr/ocr.h b/src/openalpr/ocr.h new file mode 100644 index 0000000..7bf6d0f --- /dev/null +++ b/src/openalpr/ocr.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + + +#ifndef OCR_H +#define OCR_H + + #include + #include + +#include "utility.h" +#include "postprocess.h" +#include "config.h" + +#include "constants.h" +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/imgproc/imgproc.hpp" + +#include "baseapi.h" +using namespace tesseract; +using namespace std; +using namespace cv; + + +class OCR +{ + + public: + OCR(Config* config); + virtual ~OCR(); + + void performOCR(vector thresholds, vector charRegions); + + PostProcess* postProcessor; + //string recognizedText; + //float confidence; + //float overallConfidence; + + + private: + Config* config; + + TessBaseAPI *tesseract; + + + +}; + + + +#endif // OCR_H diff --git a/src/openalpr/platecorners.cpp b/src/openalpr/platecorners.cpp new file mode 100644 index 0000000..bd298b5 --- /dev/null +++ b/src/openalpr/platecorners.cpp @@ -0,0 +1,447 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + +#include "platecorners.h" + +PlateCorners::PlateCorners(Mat inputImage, PlateLines* plateLines, CharacterRegion* charRegion, Config* config) +{ + this->config = config; + + if (this->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->charHeight = distanceBetweenPoints(charRegion->getCharArea()[0], charRegion->getCharArea()[3]); + //this->charHeight = this->charHeight - 2; // Adjust since this height is a box around our char. + // Adjust the char height for the difference in size... + //this->charHeight = ((float) inputImage.size().height / (float) TEMPLATE_PLATE_HEIGHT) * this->charHeight; + + this->charAngle = angleBetweenPoints(charRegion->getCharArea()[0], charRegion->getCharArea()[1]); +} + +PlateCorners::~PlateCorners() +{ + +} + +vector PlateCorners::findPlateCorners() +{ + if (this->config->debugPlateCorners) + cout << "PlateCorners::findPlateCorners" << endl; + + timespec startTime; + getTime(&startTime); + + int horizontalLines = this->plateLines->horizontalLines.size(); + int verticalLines = this->plateLines->verticalLines.size(); + + + // layout horizontal lines + for (int h1 = NO_LINE; h1 < horizontalLines; h1++) + { + for (int h2 = NO_LINE; h2 < horizontalLines; h2++) + { + if (h1 == h2 && h1 != NO_LINE) continue; + + this->scoreHorizontals(h1, h2); + + } + } + + // layout vertical lines + for (int v1 = NO_LINE; v1 < verticalLines; v1++) + { + for (int v2 = NO_LINE; v2 < verticalLines; v2++) + { + if (v1 == v2 && v1 != NO_LINE) continue; + + this->scoreVerticals(v1, v2); + } + } + + + if (this->config->debugPlateCorners) + { + + cout << "Drawing debug stuff..." << endl; + + Mat imgCorners = Mat(inputImage.size(), inputImage.type()); + inputImage.copyTo(imgCorners); + for (int i = 0; i < 4; i++) + circle(imgCorners, charRegion->getCharArea()[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); + + } + + // Check if a left/right edge has been established. + if (bestLeft.p1.x == 0 && bestLeft.p1.y == 0 && bestLeft.p2.x == 0 && bestLeft.p2.y == 0) + confidence = 0; + else if (bestTop.p1.x == 0 && bestTop.p1.y == 0 && bestTop.p2.x == 0 && bestTop.p2.y == 0) + confidence = 0; + else + confidence = 100; + + vector corners; + corners.push_back(bestTop.intersection(bestLeft)); + corners.push_back(bestTop.intersection(bestRight)); + corners.push_back(bestBottom.intersection(bestRight)); + corners.push_back(bestBottom.intersection(bestLeft)); + + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "Plate Corners Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + return corners; +} + + +void PlateCorners::scoreVerticals(int v1, int v2) +{ + + float score = 0; // Lower is better + + LineSegment left; + LineSegment right; + + + + float charHeightToPlateWidthRatio = config->plateWidthMM / config->charHeightMM; + float idealPixelWidth = this->charHeight * (charHeightToPlateWidthRatio * 1.05); // Add 10% so we don't clip any characters + + 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 ); + + score += SCORING_MISSING_SEGMENT_PENALTY_VERTICAL * 2; + } + else if (v1 != NO_LINE && v2 != NO_LINE) + { + left = this->plateLines->verticalLines[v1]; + right = this->plateLines->verticalLines[v2]; + } + else if (v1 == NO_LINE && v2 != NO_LINE) + { + right = this->plateLines->verticalLines[v2]; + left = right.getParallelLine(idealPixelWidth); + score += SCORING_MISSING_SEGMENT_PENALTY_VERTICAL; + } + else if (v1 != NO_LINE && v2 == NO_LINE) + { + left = this->plateLines->verticalLines[v1]; + right = left.getParallelLine(-1 * idealPixelWidth); + score += SCORING_MISSING_SEGMENT_PENALTY_VERTICAL; + } + + + + // 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())) + return; + + + ///////////////////////////////////////////////////////////////////////// + // Score "Distance from the edge... + ///////////////////////////////////////////////////////////////////////// + + float leftDistanceFromEdge = abs((float) (left.p1.x + left.p2.x) / 2); + float rightDistanceFromEdge = abs(this->inputImage.cols - ((float) (right.p1.x + right.p2.x) / 2)); + + float distanceFromEdge = leftDistanceFromEdge + rightDistanceFromEdge; + score += distanceFromEdge * SCORING_VERTICALDISTANCE_FROMEDGE_WEIGHT; + + + ///////////////////////////////////////////////////////////////////////// + // Score "Boxiness" of the 4 lines. How close is it to a parallelogram? + ///////////////////////////////////////////////////////////////////////// + + float verticalAngleDiff = abs(left.angle - right.angle); + + score += (verticalAngleDiff) * SCORING_BOXINESS_WEIGHT; + + + ////////////////////////////////////////////////////////////////////////// + // 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()); + + float plateDistance = abs(idealPixelWidth - distanceBetweenPoints(leftMidLinePoint, rightMidLinePoint)); + + score += plateDistance * SCORING_VERTICALDISTANCE_WEIGHT; + + if (score < this->bestVerticalScore) + { + float scorecomponent; + + + if (this->config->debugPlateCorners) + { + cout << "xx xx Score: charHeight " << this->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; + cout << "xx xx Score: Right= " << right.str() << endl; + + cout << "Vertical breakdown Score:" << endl; + cout << " -- Boxiness Score: " << verticalAngleDiff << " -- Weight (" << SCORING_BOXINESS_WEIGHT << ")" << endl; + scorecomponent = verticalAngleDiff * SCORING_BOXINESS_WEIGHT; + cout << " -- -- Score: " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Distance From Edge Score: " << distanceFromEdge << " -- Weight (" << SCORING_VERTICALDISTANCE_FROMEDGE_WEIGHT << ")" << endl; + scorecomponent = distanceFromEdge * SCORING_VERTICALDISTANCE_FROMEDGE_WEIGHT; + cout << " -- -- Score: " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Distance Score: " << plateDistance << " -- Weight (" << SCORING_VERTICALDISTANCE_WEIGHT << ")" << endl; + scorecomponent = plateDistance * SCORING_VERTICALDISTANCE_WEIGHT; + cout << " -- -- Score: " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Score: " << score << endl; + } + + this->bestVerticalScore = score; + bestLeft = LineSegment(left.p1.x, left.p1.y, left.p2.x, left.p2.y); + bestRight = LineSegment(right.p1.x, right.p1.y, right.p2.x, right.p2.y); + } + +} +// Score a collection of lines as a possible license plate region. +// If any segments are missing, extrapolate the missing pieces +void PlateCorners::scoreHorizontals(int h1, int h2) +{ + + //if (this->debug) + // cout << "PlateCorners::scorePlate" << endl; + + float score = 0; // Lower is better + + LineSegment top; + LineSegment bottom; + + float charHeightToPlateHeightRatio = config->plateHeightMM / config->charHeightMM; + float idealPixelHeight = this->charHeight * charHeightToPlateHeightRatio; + + + 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 ); + + score += SCORING_MISSING_SEGMENT_PENALTY_HORIZONTAL * 2; + } + else if (h1 != NO_LINE && h2 != NO_LINE) + { + top = this->plateLines->horizontalLines[h1]; + bottom = this->plateLines->horizontalLines[h2]; + } + else if (h1 == NO_LINE && h2 != NO_LINE) + { + bottom = this->plateLines->horizontalLines[h2]; + top = bottom.getParallelLine(idealPixelHeight); + score += SCORING_MISSING_SEGMENT_PENALTY_HORIZONTAL; + } + else if (h1 != NO_LINE && h2 == NO_LINE) + { + top = this->plateLines->horizontalLines[h1]; + bottom = top.getParallelLine(-1 * idealPixelHeight); + score += SCORING_MISSING_SEGMENT_PENALTY_HORIZONTAL; + } + + + + // 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())) + return; + + + + + + + // We now have 4 possible lines. Let's put them to the test and score them... + + + + + ///////////////////////////////////////////////////////////////////////// + // Score "Boxiness" of the 4 lines. How close is it to a parallelogram? + ///////////////////////////////////////////////////////////////////////// + + float horizontalAngleDiff = abs(top.angle - bottom.angle); + + + score += (horizontalAngleDiff) * SCORING_BOXINESS_WEIGHT; +// if (this->debug) +// cout << "PlateCorners boxiness score: " << (horizontalAngleDiff + verticalAngleDiff) * SCORING_BOXINESS_WEIGHT << endl; + + + ////////////////////////////////////////////////////////////////////////// + // SCORE the shape wrt character position and height relative to position + ////////////////////////////////////////////////////////////////////////// + + Point topPoint = top.midpoint(); + Point botPoint = bottom.closestPointOnSegmentTo(topPoint); + float plateHeightPx = distanceBetweenPoints(topPoint, botPoint); + + // Get the height difference + + + float heightRatio = charHeight / plateHeightPx; + float idealHeightRatio = (config->charHeightMM / 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 + + // Get the distance from the top and the distance from the bottom + // Take the average distances from the corners of the character region to the top/bottom lines +// float topDistance = distanceBetweenPoints(topMidLinePoint, charRegion->getCharBoxTop().midpoint()); +// float bottomDistance = distanceBetweenPoints(bottomMidLinePoint, charRegion->getCharBoxBottom().midpoint()); + +// float idealTopDistance = charHeight * (TOP_WHITESPACE_HEIGHT_MM / CHARACTER_HEIGHT_MM); +// float idealBottomDistance = charHeight * (BOTTOM_WHITESPACE_HEIGHT_MM / CHARACTER_HEIGHT_MM); +// float distScore = abs(topDistance - idealTopDistance) + abs(bottomDistance - idealBottomDistance); + + + score += heightRatioDiff * SCORING_PLATEHEIGHT_WEIGHT; + + ////////////////////////////////////////////////////////////////////////// + // 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 topLineSpot = top.closestPointOnSegmentTo(charAreaMidPoint); + Point botLineSpot = bottom.closestPointOnSegmentTo(charAreaMidPoint); + + float topDistanceFromMiddle = distanceBetweenPoints(topLineSpot, charAreaMidPoint); + float bottomDistanceFromMiddle = distanceBetweenPoints(topLineSpot, charAreaMidPoint); + + float idealDistanceFromMiddle = idealPixelHeight / 2; + + float middleScore = abs(topDistanceFromMiddle - idealDistanceFromMiddle) + abs(bottomDistanceFromMiddle - idealDistanceFromMiddle); + + score += middleScore * SCORING_TOP_BOTTOM_SPACE_VS_CHARHEIGHT_WEIGHT; + +// if (this->debug) +// { +// cout << "PlateCorners boxiness score: " << avgRatio * SCORING_TOP_BOTTOM_SPACE_VS_CHARHEIGHT_WEIGHT << endl; +// cout << "PlateCorners boxiness score: " << distScore * SCORING_PLATEHEIGHT_WEIGHT << endl; +// } + ////////////////////////////////////////////////////////////// + // SCORE: the shape for angles matching the character region + ////////////////////////////////////////////////////////////// + + float charanglediff = abs(charAngle - top.angle) + abs(charAngle - bottom.angle); + + + score += charanglediff * SCORING_ANGLE_MATCHES_LPCHARS_WEIGHT; + +// if (this->debug) +// cout << "PlateCorners boxiness score: " << charanglediff * SCORING_ANGLE_MATCHES_LPCHARS_WEIGHT << endl; + + + + if (score < this->bestHorizontalScore) + { + float scorecomponent; + + if (this->config->debugPlateCorners) + { + cout << "xx xx Score: charHeight " << this->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; + cout << "xx xx Score: Bottom= " << bottom.str() << endl; + + cout << "Horizontal breakdown Score:" << endl; + cout << " -- Boxiness Score: " << horizontalAngleDiff << " -- Weight (" << SCORING_BOXINESS_WEIGHT << ")" << endl; + scorecomponent = horizontalAngleDiff * SCORING_BOXINESS_WEIGHT; + cout << " -- -- Score: " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Height Ratio Diff Score: " << heightRatioDiff << " -- Weight (" << SCORING_PLATEHEIGHT_WEIGHT << ")" << endl; + scorecomponent = heightRatioDiff * SCORING_PLATEHEIGHT_WEIGHT; + cout << " -- -- " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Distance Score: " << middleScore << " -- Weight (" << SCORING_TOP_BOTTOM_SPACE_VS_CHARHEIGHT_WEIGHT << ")" << endl; + scorecomponent = middleScore * SCORING_TOP_BOTTOM_SPACE_VS_CHARHEIGHT_WEIGHT; + cout << " -- -- Score: " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Char angle Score: " << charanglediff << " -- Weight (" << SCORING_ANGLE_MATCHES_LPCHARS_WEIGHT << ")" << endl; + scorecomponent = charanglediff * SCORING_ANGLE_MATCHES_LPCHARS_WEIGHT; + cout << " -- -- Score: " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Score: " << score << endl; + } + this->bestHorizontalScore = score; + bestTop = LineSegment(top.p1.x, top.p1.y, top.p2.x, top.p2.y); + bestBottom = LineSegment(bottom.p1.x, bottom.p1.y, bottom.p2.x, bottom.p2.y); + } + + + +} diff --git a/src/openalpr/platecorners.h b/src/openalpr/platecorners.h new file mode 100644 index 0000000..6c2aecc --- /dev/null +++ b/src/openalpr/platecorners.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + +#ifndef PLATECORNERS_H +#define PLATECORNERS_H + +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "characterregion.h" +#include "platelines.h" +#include "utility.h" +#include "config.h" + +using namespace cv; +using namespace std; + +#define NO_LINE -1 + + +#define SCORING_MISSING_SEGMENT_PENALTY_VERTICAL 10 +#define SCORING_MISSING_SEGMENT_PENALTY_HORIZONTAL 15 + +#define SCORING_BOXINESS_WEIGHT 0.8 +#define SCORING_PLATEHEIGHT_WEIGHT 2.2 +#define SCORING_TOP_BOTTOM_SPACE_VS_CHARHEIGHT_WEIGHT 0.05 +#define SCORING_ANGLE_MATCHES_LPCHARS_WEIGHT 1.1 +#define SCORING_VERTICALDISTANCE_WEIGHT 0.1 + +#define SCORING_VERTICALDISTANCE_FROMEDGE_WEIGHT 0.05 + +class PlateCorners +{ + + public: + PlateCorners(Mat inputImage, PlateLines* plateLines, CharacterRegion* charRegion, Config* config); + virtual ~PlateCorners(); + + vector findPlateCorners(); + + float confidence; + + private: + + Config* config; + Mat inputImage; + float charHeight; + float charAngle; + + float bestHorizontalScore; + float bestVerticalScore; + LineSegment bestTop; + LineSegment bestBottom; + LineSegment bestLeft; + LineSegment bestRight; + + PlateLines* plateLines; + CharacterRegion* charRegion; + + void scoreHorizontals( int h1, int h2 ); + void scoreVerticals( int v1, int v2 ); + +}; + +#endif // PLATELINES_H + + diff --git a/src/openalpr/platelines.cpp b/src/openalpr/platelines.cpp new file mode 100644 index 0000000..c07d297 --- /dev/null +++ b/src/openalpr/platelines.cpp @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#include "platelines.h" + + +PlateLines::PlateLines(Config* config) +{ + this->config = config; + this->debug = config->debugPlateLines; + + if (debug) + cout << "PlateLines constructor" << endl; + + +} + +PlateLines::~PlateLines() +{ + +} + + + +void PlateLines::processImage(Mat inputImage, float sensitivity) +{ + if (this->debug) + cout << "PlateLines findLines" << endl; + + + timespec startTime; + getTime(&startTime); + + + Mat smoothed(inputImage.size(), inputImage.type()); + inputImage.copyTo(smoothed); + 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 ) ); + + morphologyEx( smoothed, smoothed, MORPH_CLOSE, element ); + + morph_size = 1; + element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) ); + + //morphologyEx( thresholded, thresholded, MORPH_GRADIENT, element ); + + morph_size = 1; + element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) ); + morphologyEx( smoothed, smoothed, MORPH_OPEN, element ); + + + + Mat edges(inputImage.size(), inputImage.type()); + Canny(smoothed, edges, 66, 133); + + + vector hlines = this->getLines(edges, sensitivity, false); + vector vlines = this->getLines(edges, sensitivity, true); + for (int i = 0; i < hlines.size(); i++) + this->horizontalLines.push_back(hlines[i]); + for (int i = 0; i < vlines.size(); i++) + this->verticalLines.push_back(vlines[i]); + + + + + // if debug is enabled, draw the image + if (this->debug) + { + Mat debugImgHoriz(edges.size(), edges.type()); + Mat debugImgVert(edges.size(), edges.type()); + edges.copyTo(debugImgHoriz); + edges.copyTo(debugImgVert); + cvtColor(debugImgHoriz,debugImgHoriz,CV_GRAY2BGR); + cvtColor(debugImgVert,debugImgVert,CV_GRAY2BGR); + + for( size_t i = 0; i < this->horizontalLines.size(); i++ ) + { + line( debugImgHoriz, this->horizontalLines[i].p1, this->horizontalLines[i].p2, Scalar(0,0,255), 1, CV_AA); + } + + for( size_t i = 0; i < this->verticalLines.size(); i++ ) + { + line( debugImgVert, this->verticalLines[i].p1, this->verticalLines[i].p2, Scalar(0,0,255), 1, CV_AA); + } + + vector images; + images.push_back(debugImgHoriz); + images.push_back(debugImgVert); + + Mat dashboard = drawImageDashboard(images, debugImgVert.type(), 1); + displayImage(config, "Hough Lines", dashboard); + } + + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "Plate Lines Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + //smoothed.release(); + + + //////////////// METHOD2!!!!!!!//////////////////// + + /* + Mat imgBlur; + Mat imgCanny; + GaussianBlur(inputImage, imgBlur, Size(9, 9), 1, 1); + + + + Canny(imgBlur, imgCanny, 10, 30, 3); + + + + //int morph_elem = 2; + //int morph_size = 1; + //Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) ); + morphologyEx( imgCanny, imgCanny, MORPH_CLOSE, element ); + + + Mat imgShaped; + imgCanny.copyTo(imgShaped); + //Find contours of possibles characters + vector< vector< Point> > biggestShapes; + findContours(imgShaped, + biggestShapes, // a vector of contours + CV_RETR_EXTERNAL, // retrieve the external contours + CV_CHAIN_APPROX_SIMPLE ); // all pixels of each contours + + // Draw blue contours on a white image + //cvtColor(imgShaped, imgShaped, CV_GRAY2RGB); + cv::drawContours(imgShaped,biggestShapes, + -1, // draw all contours + cv::Scalar(255,255,255), // in blue + 1); // with a thickness of 1 + + displayImage(config, "Blurred", imgCanny); + displayImage(config, "Blurred Contours", imgShaped); + + vector shapeRects( biggestShapes.size() ); + + vector >hull( biggestShapes.size() ); + for( int i = 0; i < biggestShapes.size(); i++ ) + { + //approxPolyDP( Mat(biggestShapes[i]), shapeRects[i], 3, true ); + convexHull( biggestShapes[i], hull[i], false ); + //approxPolyDP( biggestShapes[i], hull[i], 10, true ); + + //minEnclosingCircle( (Mat)contours_poly[i], center[i], radius[i] ); + } + */ + +} + +/* +vector PlateLines::getLines(Mat edges, bool vertical) +{ + + vector filteredLines; + + int sensitivity; + + LSWMS lswms(Size(edges.cols, edges.rows), 3, 155, false); + + vector lsegs; + vector errors; + lswms.run(edges, lsegs, errors); + + + for( size_t i = 0; i < lsegs.size(); i++ ) + { + + if (vertical) + { + LineSegment candidate; + if (lsegs[i][0].y <= lsegs[i][1].y) + candidate = LineSegment(lsegs[i][0].x, lsegs[i][0].y, lsegs[i][1].x, lsegs[i][1].y); + else + candidate = LineSegment(lsegs[i][1].x, lsegs[i][1].y, lsegs[i][0].x, lsegs[i][0].y); + + cout << "VERT Angle: " << candidate.angle << endl; + //if ((candidate.angle > 70 && candidate.angle < 110) || (candidate.angle > 250 && candidate.angle < 290)) + //{ + // good vertical + filteredLines.push_back(candidate); + + //} + } + else + { + LineSegment candidate; + if (lsegs[i][0].x <= lsegs[i][1].x) + candidate = LineSegment(lsegs[i][0].x, lsegs[i][0].y, lsegs[i][1].x, lsegs[i][1].y); + else + candidate = LineSegment(lsegs[i][1].x, lsegs[i][1].y, lsegs[i][0].x, lsegs[i][0].y); + cout << "HORIZAngle: " << candidate.angle << endl; + + //if ( (candidate.angle > -20 && candidate.angle < 20) || (candidate.angle > 160 && candidate.angle < 200)) + //{ + // good horizontal + filteredLines.push_back(candidate); + + //} + } + } + + // if debug is enabled, draw the image + if (this->debug) + { + Mat debugImg(edges.size(), edges.type()); + edges.copyTo(debugImg); + cvtColor(debugImg,debugImg,CV_GRAY2BGR); + + for( size_t i = 0; i < filteredLines.size(); i++ ) + { + + line( debugImg, filteredLines[i].p1, filteredLines[i].p2, Scalar(0,0,255), 1, CV_AA); + } + if (vertical) + displayImage(config, "Lines Vertical", debugImg); + else + displayImage(config, "Lines Horizontal", debugImg); + } + + return filteredLines; +} +*/ + + +vector PlateLines::getLines(Mat edges, float sensitivityMultiplier, bool vertical) +{ + if (this->debug) + cout << "PlateLines::getLines" << endl; + + static int HORIZONTAL_SENSITIVITY = config->plateLinesSensitivityHorizontal; + static int VERTICAL_SENSITIVITY = config->plateLinesSensitivityVertical; + + vector allLines; + vector filteredLines; + + int sensitivity; + if (vertical) + sensitivity = VERTICAL_SENSITIVITY * (1.0 / sensitivityMultiplier); + else + sensitivity = HORIZONTAL_SENSITIVITY * (1.0 / sensitivityMultiplier); + + HoughLines( edges, allLines, 1, CV_PI/180, sensitivity, 0, 0 ); + + + for( size_t i = 0; i < allLines.size(); i++ ) + { + float rho = allLines[i][0], theta = allLines[i][1]; + Point pt1, pt2; + double a = cos(theta), b = sin(theta); + double x0 = a*rho, y0 = b*rho; + + double angle = theta * (180 / CV_PI); + pt1.x = cvRound(x0 + 1000*(-b)); + pt1.y = cvRound(y0 + 1000*(a)); + pt2.x = cvRound(x0 - 1000*(-b)); + pt2.y = cvRound(y0 - 1000*(a)); + + if (vertical) + { + if (angle < 20 || angle > 340 || (angle > 160 && angle < 210)) + { + // good vertical + + LineSegment line; + if (pt1.y <= pt2.y) + line = LineSegment(pt2.x, pt2.y, pt1.x, pt1.y); + else + line = LineSegment(pt1.x, pt1.y, pt2.x, pt2.y); + + // Get rid of the -1000, 1000 stuff. Terminate at the edges of the image + // Helps with debugging/rounding issues later + LineSegment top(0, 0, edges.cols, 0); + LineSegment bottom(0, edges.rows, edges.cols, edges.rows); + Point p1 = line.intersection(bottom); + Point p2 = line.intersection(top); + filteredLines.push_back(LineSegment(p1.x, p1.y, p2.x, p2.y)); + } + } + else + { + + if ( (angle > 70 && angle < 110) || (angle > 250 && angle < 290)) + { + // good horizontal + + LineSegment line; + if (pt1.x <= pt2.x) + line = LineSegment(pt1.x, pt1.y, pt2.x, pt2.y); + else + line =LineSegment(pt2.x, pt2.y, pt1.x, pt1.y); + + // Get rid of the -1000, 1000 stuff. Terminate at the edges of the image + // Helps with debugging/ rounding issues later + int newY1 = line.getPointAt(0); + int newY2 = line.getPointAt(edges.cols); + + filteredLines.push_back(LineSegment(0, newY1, edges.cols, newY2)); + } + } + } + + + return filteredLines; +} + + + + + +Mat PlateLines::customGrayscaleConversion(Mat src) +{ + Mat img_hsv; + cvtColor(src,img_hsv,CV_BGR2HSV); + + + Mat grayscale = Mat(img_hsv.size(), CV_8U ); + Mat hue(img_hsv.size(), CV_8U ); + + for (int row = 0; row < img_hsv.rows; row++) + { + for (int col = 0; col < img_hsv.cols; col++) + { + int h = (int) img_hsv.at(row, col)[0]; + int s = (int) img_hsv.at(row, col)[1]; + int v = (int) img_hsv.at(row, col)[2]; + + int pixval = pow(v, 1.05); + + + if (pixval > 255) + pixval = 255; + grayscale.at(row, col) = pixval; + + hue.at(row, col) = h * (255.0 / 180.0); + } + } + + //displayImage(config, "Hue", hue); + return grayscale; +} \ No newline at end of file diff --git a/src/openalpr/platelines.h b/src/openalpr/platelines.h new file mode 100644 index 0000000..59e05f7 --- /dev/null +++ b/src/openalpr/platelines.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + +#ifndef PLATELINES_H +#define PLATELINES_H + +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "utility.h" +#include "binarize_wolf.h" +//#include "lswms.h" +#include "config.h" + +using namespace cv; +using namespace std; + + +class PlateLines +{ + + public: + PlateLines(Config* config); + virtual ~PlateLines(); + + void processImage(Mat img, float sensitivity=1.0); + + vector horizontalLines; + vector verticalLines; + + vector winningCorners; + + private: + Config* config; + bool debug; + + + Mat customGrayscaleConversion(Mat src); + void findLines(Mat inputImage); + vector getLines(Mat edges, float sensitivityMultiplier, bool vertical); +}; + +#endif // PLATELINES_H + + diff --git a/src/openalpr/postprocess.cpp b/src/openalpr/postprocess.cpp new file mode 100644 index 0000000..7e28694 --- /dev/null +++ b/src/openalpr/postprocess.cpp @@ -0,0 +1,478 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#include "postprocess.h" + + +PostProcess::PostProcess(Config* config) +{ + this->config = config; + + stringstream filename; + filename << config->getPostProcessRuntimeDir() << "/" << config->country << ".patterns"; + + std::ifstream infile(filename.str().c_str()); + + + string region, pattern; + while (infile >> region >> pattern) + { + RegexRule* rule = new RegexRule(region, pattern); + //cout << "REGION: " << region << " PATTERN: " << pattern << endl; + + if (rules.find(region) == rules.end()) + { + vector newRule; + newRule.push_back(rule); + rules[region] = newRule; + } + else + { + vector oldRule = rules[region]; + oldRule.push_back(rule); + rules[region] = oldRule; + } + } + + //vector test = rules["base"]; + //for (int i = 0; i < test.size(); i++) + // cout << "Rule: " << test[i].regex << endl; + +} + +PostProcess::~PostProcess() +{ + // TODO: Delete all entries in rules vector + map >::iterator iter; + + for (iter = rules.begin(); iter != rules.end(); ++iter) { + for (int i = 0; i < iter->second.size(); i++) + { + delete iter->second[i]; + } + + } +} + + +void PostProcess::addLetter(char letter, int charposition, float score) +{ + if (score < config->postProcessMinConfidence) + return; + + insertLetter(letter, charposition, score); + + if (score < config->postProcessConfidenceSkipLevel) + { + float adjustedScore = abs(config->postProcessConfidenceSkipLevel - score) + config->postProcessMinConfidence; + insertLetter(SKIP_CHAR, charposition, adjustedScore ); + } + + //if (letter == '0') + //{ + // insertLetter('O', charposition, score - 0.5); + //} + +} + +void PostProcess::insertLetter(char letter, int charposition, float score) +{ + + score = score - config->postProcessMinConfidence; + + + int existingIndex = -1; + if (letters.size() < charposition + 1) + { + for (int i = letters.size(); i < charposition + 1; i++) + { + vector tmp; + letters.push_back(tmp); + } + } + + for (int i = 0; i < letters[charposition].size(); i++) + { + if (letters[charposition][i].letter == letter && + letters[charposition][i].charposition == charposition) + { + existingIndex = i; + break; + } + } + + if (existingIndex == -1) + { + Letter newLetter; + newLetter.charposition = charposition; + newLetter.letter = letter; + newLetter.occurences = 1; + newLetter.totalscore = score; + letters[charposition].push_back(newLetter); + } + else + { + letters[charposition][existingIndex].occurences = letters[charposition][existingIndex].occurences + 1; + letters[charposition][existingIndex].totalscore = letters[charposition][existingIndex].totalscore + score; + } + +} + + +void PostProcess::clear() +{ + for (int i = 0; i < letters.size(); i++) + { + letters[i].clear(); + } + letters.resize(0); + + unknownCharPositions.clear(); + unknownCharPositions.resize(0); + allPossibilities.clear(); + //allPossibilities.resize(0); + + bestChars = ""; + matchesTemplate = false; +} +void PostProcess::analyze(string templateregion, int topn) +{ + + timespec startTime; + getTime(&startTime); + + + + // Get a list of missing positions + for (int i = letters.size() -1; i >= 0; i--) + { + if (letters[i].size() == 0) + { + unknownCharPositions.push_back(i); + } + } + + + if (letters.size() == 0) + return; + + + // Sort the letters as they are + for (int i = 0; i < letters.size(); i++) + { + if (letters[i].size() > 0) + sort(letters[i].begin(), letters[i].end(), letterCompare); + } + + + + //getTopN(); + vector tmp; + findAllPermutations(tmp, 0, config->postProcessMaxSubstitutions); + + + timespec sortStartTime; + getTime(&sortStartTime); + + int numelements = topn; + if (allPossibilities.size() < topn) + numelements = allPossibilities.size() - 1; + + partial_sort( allPossibilities.begin(), allPossibilities.begin() + numelements, allPossibilities.end() - 1, wordCompare ); + + if (config->debugTiming) + { + timespec sortEndTime; + getTime(&sortEndTime); + cout << " -- PostProcess Sort Time: " << diffclock(sortStartTime, sortEndTime) << "ms." << endl; + } + + + + matchesTemplate = false; + + + if (templateregion != "") + { + vector regionRules = rules[templateregion]; + + for (int i = 0; i < allPossibilities.size(); i++) + { + for (int j = 0; j < regionRules.size(); j++) + { + allPossibilities[i].matchesTemplate = regionRules[j]->match(allPossibilities[i].letters); + if (allPossibilities[i].matchesTemplate) + { + allPossibilities[i].letters = regionRules[j]->filterSkips(allPossibilities[i].letters); + //bestChars = regionRules[j]->filterSkips(allPossibilities[i].letters); + matchesTemplate = true; + break; + } + } + + + + if (i >= topn - 1) + break; + //if (matchesTemplate || i >= TOP_N - 1) + //break; + } + } + + if (matchesTemplate) + { + for (int z = 0; z < allPossibilities.size(); z++) + { + if (allPossibilities[z].matchesTemplate) + { + bestChars = allPossibilities[z].letters; + break; + } + } + } + else + { + bestChars = allPossibilities[0].letters; + } + + // Now adjust the confidence scores to a percentage value + if (allPossibilities.size() > 0) + { + float maxPercentScore = calculateMaxConfidenceScore(); + float highestRelativeScore = (float) allPossibilities[0].totalscore; + + for (int i = 0; i < allPossibilities.size(); i++) + { + allPossibilities[i].totalscore = maxPercentScore * (allPossibilities[i].totalscore / highestRelativeScore); + } + + } + + + + if (this->config->debugPostProcess) + { + + // Print all letters + for (int i = 0; i < letters.size(); i++) + { + for (int j = 0; j < letters[i].size(); j++) + cout << "PostProcess Letter: " << letters[i][j].charposition << " " << letters[i][j].letter << " -- score: " << letters[i][j].totalscore << " -- occurences: " << letters[i][j].occurences << endl; + } + + // Print top words + for (int i = 0; i < allPossibilities.size(); i++) + { + cout << "Top " << topn << " Possibilities: " << allPossibilities[i].letters << " :\t" << allPossibilities[i].totalscore; + if (allPossibilities[i].letters == bestChars) + cout << " <--- "; + cout << endl; + + if (i >= topn - 1) + break; + } + cout << allPossibilities.size() << " total permutations" << endl; + } + + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "PostProcess Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + if (this->config->debugPostProcess) + cout << "PostProcess Analysis Complete: " << bestChars << " -- MATCH: " << matchesTemplate << endl; +} + +float PostProcess::calculateMaxConfidenceScore() +{ + // Take the best score for each char position and average it. + + float totalScore = 0; + int numScores = 0; + // Get a list of missing positions + for (int i = 0; i < letters.size(); i++) + { + if (letters[i].size() > 0) + { + totalScore += (letters[i][0].totalscore / letters[i][0].occurences) + config->postProcessMinConfidence; + numScores++; + } + } + + if (numScores == 0) + return 0; + + return totalScore / ((float) numScores); +} + + +const vector PostProcess::getResults() +{ + return this->allPossibilities; +} + +void PostProcess::findAllPermutations(vector prevletters, int charPos, int substitutionsLeft) +{ + + if (substitutionsLeft < 0) + return; + + // Add my letter to the chain and recurse + for (int i = 0; i < letters[charPos].size(); i++) + { + + if (charPos == letters.size() - 1) + { + // Last letter, add the word + PPResult possibility; + possibility.letters = ""; + possibility.totalscore = 0; + possibility.matchesTemplate = false; + for (int z = 0; z < prevletters.size(); z++) + { + if (prevletters[z].letter != SKIP_CHAR) + possibility.letters = possibility.letters + prevletters[z].letter; + possibility.totalscore = possibility.totalscore + prevletters[z].totalscore; + } + + if (letters[charPos][i].letter != SKIP_CHAR) + possibility.letters = possibility.letters + letters[charPos][i].letter; + possibility.totalscore = possibility.totalscore +letters[charPos][i].totalscore; + + allPossibilities.push_back(possibility); + } + else + { + prevletters.push_back(letters[charPos][i]); + + float scorePercentDiff = abs( letters[charPos][0].totalscore - letters[charPos][i].totalscore ) / letters[charPos][0].totalscore; + if (i != 0 && letters[charPos][i].letter != SKIP_CHAR && scorePercentDiff > 0.10f ) + findAllPermutations(prevletters, charPos + 1, substitutionsLeft - 1); + else + findAllPermutations(prevletters, charPos + 1, substitutionsLeft); + + prevletters.pop_back(); + } + } + + if (letters[charPos].size() == 0) + { + // No letters for this char position... + // Just pass it along + findAllPermutations(prevletters, charPos + 1, substitutionsLeft); + } + + + +} + + + + +bool wordCompare( const PPResult &left, const PPResult &right ){ + if (left.totalscore < right.totalscore) + return false; + return true; + +} + +bool letterCompare( const Letter &left, const Letter &right ) +{ + if (left.totalscore < right.totalscore) + return false; + return true; +} + + +RegexRule::RegexRule(string region, string pattern) +{ + this->original = pattern; + this->region = region; + + numchars = 0; + for (int i = 0; i < pattern.size(); i++) + { + if (pattern.at(i) == '[') + { + while (pattern.at(i) != ']' ) + { + this->regex = this->regex + pattern.at(i); + i++; + } + this->regex = this->regex + ']'; + + } + else if (pattern.at(i) == '?') + { + this->regex = this->regex + '.'; + this->skipPositions.push_back(numchars); + } + else if (pattern.at(i) == '@') + { + this->regex = this->regex + "\\a"; + } + else if (pattern.at(i) == '#') + { + this->regex = this->regex + "\\d"; + } + + numchars++; + } + + trexp.Compile(this->regex.c_str()); + + //cout << "AA " << this->region << ": " << original << " regex: " << regex << endl; + //for (int z = 0; z < this->skipPositions.size(); z++) + // cout << "AA Skip position: " << skipPositions[z] << endl; +} + + +bool RegexRule::match(string text) +{ + if (text.length() != numchars) + return false; + + return trexp.Match(text.c_str()); +} + +string RegexRule::filterSkips(string text) +{ + string response = ""; + for (int i = 0; i < text.size(); i++) + { + bool skip = false; + for (int j = 0; j < skipPositions.size(); j++) + { + if (skipPositions[j] == i) + { + skip = true; + break; + } + } + + if (skip == false) + response = response + text[i]; + } + + return response; +} diff --git a/src/openalpr/postprocess.h b/src/openalpr/postprocess.h new file mode 100644 index 0000000..f13d4e5 --- /dev/null +++ b/src/openalpr/postprocess.h @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + +#ifndef POSTPROCESS_H +#define POSTPROCESS_H + + #include "TRexpp.h" + #include "constants.h" + #include "utility.h" + #include + #include + #include + #include +#include "config.h" + +using namespace std; + + +#define SKIP_CHAR '~' + +struct Letter +{ + char letter; + int charposition; + float totalscore; + int occurences; +}; + +struct PPResult +{ + string letters; + float totalscore; + bool matchesTemplate; +}; + + +bool wordCompare( const PPResult &left, const PPResult &right ); +bool letterCompare( const Letter &left, const Letter &right ); + + +class RegexRule +{ + public: + RegexRule(string region, string pattern); + + + bool match(string text); + string filterSkips(string text); + + private: + int numchars; + TRexpp trexp; + string original; + string regex; + string region; + vector skipPositions; +}; + + +class PostProcess +{ + public: + PostProcess(Config* config); + ~PostProcess(); + + void addLetter(char letter, int charposition, float score); + + void clear(); + void analyze(string templateregion, int topn); + + string bestChars; + bool matchesTemplate; + + const vector getResults(); + + private: + Config* config; + //void getTopN(); + void findAllPermutations(vector prevletters, int charPos, int substitutionsLeft); + + void insertLetter(char letter, int charPosition, float score); + + map > rules; + + float calculateMaxConfidenceScore(); + + vector > letters; + vector unknownCharPositions; + + + vector allPossibilities; +}; + +/* +class LetterScores +{ + public: + LetterScores(int numCharPositions); + + void addScore(char letter, int charposition, float score); + + vector getBestScore(); + float getConfidence(); + + private: + int numCharPositions; + + vector letters; + vector charpositions; + vector scores; +}; +*/ +#endif // POSTPROCESS_H \ No newline at end of file diff --git a/src/openalpr/regiondetector.cpp b/src/openalpr/regiondetector.cpp new file mode 100644 index 0000000..cd64969 --- /dev/null +++ b/src/openalpr/regiondetector.cpp @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + +#include "regiondetector.h" + + +RegionDetector::RegionDetector(Config* config) +{ + this->config = config; + // Don't scale. Can change this in the future (i.e., maximum resolution preference, or some such). + this->scale_factor = 1.0f; + + + // Load the cascade classifier + if( this->plate_cascade.load( config->getCascadeRuntimeDir() + config->country + ".xml" ) ) + { + this->loaded = true; + } + else + { + printf("--(!)Error loading classifier\n"); + this->loaded = false; + } + +} + +RegionDetector::~RegionDetector() +{ + +} + + + +bool RegionDetector::isLoaded() +{ + return this->loaded; +} + + +vector RegionDetector::detect(Mat frame) +{ + + Mat frame_gray; + cvtColor( frame, frame_gray, CV_BGR2GRAY ); + + vector regionsOfInterest = doCascade(frame_gray); + + return regionsOfInterest; +} + + +/** @function detectAndDisplay */ +vector RegionDetector::doCascade(Mat frame) +{ + //float scale_factor = 1; + int w = frame.size().width; + int h = frame.size().height; + + vector plates; + + equalizeHist( frame, frame ); + resize(frame, frame, Size(w * this->scale_factor, h * this->scale_factor)); + + //-- Detect plates + timespec startTime; + getTime(&startTime); + + Size minSize(config->minPlateSizeWidthPx * this->scale_factor, config->minPlateSizeHeightPx * this->scale_factor); + Size maxSize(w * config->maxPlateWidthPercent * this->scale_factor, h * config->maxPlateHeightPercent * this->scale_factor); + + + plate_cascade.detectMultiScale( frame, plates, 1.1, 3, + 0, + //0|CV_HAAR_SCALE_IMAGE, + minSize, maxSize ); + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "LBP Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + + + for( int i = 0; i < plates.size(); i++ ) + { + plates[i].x = plates[i].x / scale_factor; + plates[i].y = plates[i].y / scale_factor; + plates[i].width = plates[i].width / scale_factor; + plates[i].height = plates[i].height / scale_factor; + } + + return plates; + +} diff --git a/src/openalpr/regiondetector.h b/src/openalpr/regiondetector.h new file mode 100644 index 0000000..0d53a0d --- /dev/null +++ b/src/openalpr/regiondetector.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + + +#ifndef REGIONDETECTOR_H +#define REGIONDETECTOR_H + +#include +#include + +#include "opencv2/objdetect/objdetect.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "opencv2/core/core.hpp" +#include "opencv2/ml/ml.hpp" + #include "opencv2/highgui/highgui.hpp" + +#include "utility.h" +#include "support/timing.h" +#include "constants.h" + + + + + +class RegionDetector +{ + + public: + RegionDetector(Config* config); + virtual ~RegionDetector(); + + bool isLoaded(); + vector detect(Mat frame); + + private: + Config* config; + + float scale_factor; + CascadeClassifier plate_cascade; + CvSVM* svmClassifier; + bool loaded; + + vector doCascade(Mat frame); + +}; + + + + +#endif // REGIONDETECTOR_H diff --git a/src/openalpr/stateidentifier.cpp b/src/openalpr/stateidentifier.cpp new file mode 100644 index 0000000..4c1afe0 --- /dev/null +++ b/src/openalpr/stateidentifier.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#include "stateidentifier.h" + + +StateIdentifier::StateIdentifier(Config* config) +{ + this->config = config; + + featureMatcher = new FeatureMatcher(config); + + if (featureMatcher->isLoaded() == false) + { + cout << "Can not create detector or descriptor extractor or descriptor matcher of given types" << endl; + return; + } + + featureMatcher->loadRecognitionSet(config->country); +} + +StateIdentifier::~StateIdentifier() +{ + delete featureMatcher; +} + +int StateIdentifier::recognize(Mat img, Rect frame, char* stateCode) +{ + Mat croppedImage = Mat(img, frame); + + return this->recognize(croppedImage, stateCode); +} +// Attempts to recognize the plate. Returns a confidence level. Updates teh "stateCode" variable +// with the value of the country/state +int StateIdentifier::recognize(Mat img, char* stateCode) +{ + + timespec startTime; + getTime(&startTime); + + cvtColor(img, img, CV_BGR2GRAY); + + resize(img, img, getSizeMaintainingAspect(img, config->stateIdImageWidthPx, config->stateIdimageHeightPx)); + + Mat plateImg(img.size(), img.type()); + //plateImg = equalizeBrightness(img); + img.copyTo(plateImg); + + Mat debugImg(plateImg.size(), plateImg.type()); + plateImg.copyTo(debugImg); + vector matchesArray(featureMatcher->numTrainingElements()); + + + RecognitionResult result = featureMatcher->recognize(plateImg, true, &debugImg, true, matchesArray ); + + if (this->config->debugStateId) + { + + + displayImage(config, "State Identifier1", plateImg); + displayImage(config, "State Identifier", debugImg); + cout << result.haswinner << " : " << result.confidence << " : " << result.winner << endl; + } + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "State Identification Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + + if (result.haswinner == false) + return 0; + + strcpy(stateCode, result.winner.c_str()); + + + return result.confidence; +} + diff --git a/src/openalpr/stateidentifier.h b/src/openalpr/stateidentifier.h new file mode 100644 index 0000000..3161541 --- /dev/null +++ b/src/openalpr/stateidentifier.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + +#ifndef STATEIDENTIFIER_H +#define STATEIDENTIFIER_H + +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "constants.h" +#include "featurematcher.h" +#include "utility.h" +#include "config.h" + + + +class StateIdentifier +{ + + public: + StateIdentifier(Config* config); + virtual ~StateIdentifier(); + + int recognize(Mat img, Rect frame, char* stateCode); + int recognize(Mat img, char* stateCode); + + //int confidence; + + protected: + Config* config; + + + private: + + + FeatureMatcher* featureMatcher; + +}; + +#endif // STATEIDENTIFIER_H + + diff --git a/src/openalpr/trex.c b/src/openalpr/trex.c new file mode 100644 index 0000000..b9e0690 --- /dev/null +++ b/src/openalpr/trex.c @@ -0,0 +1,643 @@ +/* see copyright notice in trex.h */ +#include +#include +#include +#include +#include "trex.h" + +#ifdef _UINCODE +#define scisprint iswprint +#define scstrlen wcslen +#define scprintf wprintf +#define _SC(x) L(x) +#else +#define scisprint isprint +#define scstrlen strlen +#define scprintf printf +#define _SC(x) (x) +#endif + +#ifdef _DEBUG +#include + +static const TRexChar *g_nnames[] = +{ + _SC("NONE"),_SC("OP_GREEDY"), _SC("OP_OR"), + _SC("OP_EXPR"),_SC("OP_NOCAPEXPR"),_SC("OP_DOT"), _SC("OP_CLASS"), + _SC("OP_CCLASS"),_SC("OP_NCLASS"),_SC("OP_RANGE"),_SC("OP_CHAR"), + _SC("OP_EOL"),_SC("OP_BOL"),_SC("OP_WB") +}; + +#endif +#define OP_GREEDY (MAX_CHAR+1) // * + ? {n} +#define OP_OR (MAX_CHAR+2) +#define OP_EXPR (MAX_CHAR+3) //parentesis () +#define OP_NOCAPEXPR (MAX_CHAR+4) //parentesis (?:) +#define OP_DOT (MAX_CHAR+5) +#define OP_CLASS (MAX_CHAR+6) +#define OP_CCLASS (MAX_CHAR+7) +#define OP_NCLASS (MAX_CHAR+8) //negates class the [^ +#define OP_RANGE (MAX_CHAR+9) +#define OP_CHAR (MAX_CHAR+10) +#define OP_EOL (MAX_CHAR+11) +#define OP_BOL (MAX_CHAR+12) +#define OP_WB (MAX_CHAR+13) + +#define TREX_SYMBOL_ANY_CHAR ('.') +#define TREX_SYMBOL_GREEDY_ONE_OR_MORE ('+') +#define TREX_SYMBOL_GREEDY_ZERO_OR_MORE ('*') +#define TREX_SYMBOL_GREEDY_ZERO_OR_ONE ('?') +#define TREX_SYMBOL_BRANCH ('|') +#define TREX_SYMBOL_END_OF_STRING ('$') +#define TREX_SYMBOL_BEGINNING_OF_STRING ('^') +#define TREX_SYMBOL_ESCAPE_CHAR ('\\') + + +typedef int TRexNodeType; + +typedef struct tagTRexNode{ + TRexNodeType type; + int left; + int right; + int next; +}TRexNode; + +struct TRex{ + const TRexChar *_eol; + const TRexChar *_bol; + const TRexChar *_p; + int _first; + int _op; + TRexNode *_nodes; + int _nallocated; + int _nsize; + int _nsubexpr; + TRexMatch *_matches; + int _currsubexp; + void *_jmpbuf; + const TRexChar **_error; +}; + +static int trex_list(TRex *exp); + +static int trex_newnode(TRex *exp, TRexNodeType type) +{ + TRexNode n; + int newid; + n.type = type; + n.next = n.right = n.left = -1; + if(type == OP_EXPR) + n.right = exp->_nsubexpr++; + if(exp->_nallocated < (exp->_nsize + 1)) { + int oldsize = exp->_nallocated; + exp->_nallocated *= 2; + exp->_nodes = (TRexNode *)realloc(exp->_nodes, exp->_nallocated * sizeof(TRexNode)); + } + exp->_nodes[exp->_nsize++] = n; + newid = exp->_nsize - 1; + return (int)newid; +} + +static void trex_error(TRex *exp,const TRexChar *error) +{ + if(exp->_error) *exp->_error = error; + longjmp(*((jmp_buf*)exp->_jmpbuf),-1); +} + +static void trex_expect(TRex *exp, int n){ + if((*exp->_p) != n) + trex_error(exp, _SC("expected paren")); + exp->_p++; +} + +static TRexChar trex_escapechar(TRex *exp) +{ + if(*exp->_p == TREX_SYMBOL_ESCAPE_CHAR){ + exp->_p++; + switch(*exp->_p) { + case 'v': exp->_p++; return '\v'; + case 'n': exp->_p++; return '\n'; + case 't': exp->_p++; return '\t'; + case 'r': exp->_p++; return '\r'; + case 'f': exp->_p++; return '\f'; + default: return (*exp->_p++); + } + } else if(!scisprint(*exp->_p)) trex_error(exp,_SC("letter expected")); + return (*exp->_p++); +} + +static int trex_charclass(TRex *exp,int classid) +{ + int n = trex_newnode(exp,OP_CCLASS); + exp->_nodes[n].left = classid; + return n; +} + +static int trex_charnode(TRex *exp,TRexBool isclass) +{ + TRexChar t; + if(*exp->_p == TREX_SYMBOL_ESCAPE_CHAR) { + exp->_p++; + switch(*exp->_p) { + case 'n': exp->_p++; return trex_newnode(exp,'\n'); + case 't': exp->_p++; return trex_newnode(exp,'\t'); + case 'r': exp->_p++; return trex_newnode(exp,'\r'); + case 'f': exp->_p++; return trex_newnode(exp,'\f'); + case 'v': exp->_p++; return trex_newnode(exp,'\v'); + case 'a': case 'A': case 'w': case 'W': case 's': case 'S': + case 'd': case 'D': case 'x': case 'X': case 'c': case 'C': + case 'p': case 'P': case 'l': case 'u': + { + t = *exp->_p; exp->_p++; + return trex_charclass(exp,t); + } + case 'b': + case 'B': + if(!isclass) { + int node = trex_newnode(exp,OP_WB); + exp->_nodes[node].left = *exp->_p; + exp->_p++; + return node; + } //else default + default: + t = *exp->_p; exp->_p++; + return trex_newnode(exp,t); + } + } + else if(!scisprint(*exp->_p)) { + + trex_error(exp,_SC("letter expected")); + } + t = *exp->_p; exp->_p++; + return trex_newnode(exp,t); +} +static int trex_class(TRex *exp) +{ + int ret = -1; + int first = -1,chain; + if(*exp->_p == TREX_SYMBOL_BEGINNING_OF_STRING){ + ret = trex_newnode(exp,OP_NCLASS); + exp->_p++; + }else ret = trex_newnode(exp,OP_CLASS); + + if(*exp->_p == ']') trex_error(exp,_SC("empty class")); + chain = ret; + while(*exp->_p != ']' && exp->_p != exp->_eol) { + if(*exp->_p == '-' && first != -1){ + int r,t; + if(*exp->_p++ == ']') trex_error(exp,_SC("unfinished range")); + r = trex_newnode(exp,OP_RANGE); + if(first>*exp->_p) trex_error(exp,_SC("invalid range")); + if(exp->_nodes[first].type == OP_CCLASS) trex_error(exp,_SC("cannot use character classes in ranges")); + exp->_nodes[r].left = exp->_nodes[first].type; + t = trex_escapechar(exp); + exp->_nodes[r].right = t; + exp->_nodes[chain].next = r; + chain = r; + first = -1; + } + else{ + if(first!=-1){ + int c = first; + exp->_nodes[chain].next = c; + chain = c; + first = trex_charnode(exp,TRex_True); + } + else{ + first = trex_charnode(exp,TRex_True); + } + } + } + if(first!=-1){ + int c = first; + exp->_nodes[chain].next = c; + chain = c; + first = -1; + } + /* hack? */ + exp->_nodes[ret].left = exp->_nodes[ret].next; + exp->_nodes[ret].next = -1; + return ret; +} + +static int trex_parsenumber(TRex *exp) +{ + int ret = *exp->_p-'0'; + int positions = 10; + exp->_p++; + while(isdigit(*exp->_p)) { + ret = ret*10+(*exp->_p++-'0'); + if(positions==1000000000) trex_error(exp,_SC("overflow in numeric constant")); + positions *= 10; + }; + return ret; +} + +static int trex_element(TRex *exp) +{ + int ret = -1; + switch(*exp->_p) + { + case '(': { + int expr,newn; + exp->_p++; + + + if(*exp->_p =='?') { + exp->_p++; + trex_expect(exp,':'); + expr = trex_newnode(exp,OP_NOCAPEXPR); + } + else + expr = trex_newnode(exp,OP_EXPR); + newn = trex_list(exp); + exp->_nodes[expr].left = newn; + ret = expr; + trex_expect(exp,')'); + } + break; + case '[': + exp->_p++; + ret = trex_class(exp); + trex_expect(exp,']'); + break; + case TREX_SYMBOL_END_OF_STRING: exp->_p++; ret = trex_newnode(exp,OP_EOL);break; + case TREX_SYMBOL_ANY_CHAR: exp->_p++; ret = trex_newnode(exp,OP_DOT);break; + default: + ret = trex_charnode(exp,TRex_False); + break; + } + + { + int op; + TRexBool isgreedy = TRex_False; + unsigned short p0 = 0, p1 = 0; + switch(*exp->_p){ + case TREX_SYMBOL_GREEDY_ZERO_OR_MORE: p0 = 0; p1 = 0xFFFF; exp->_p++; isgreedy = TRex_True; break; + case TREX_SYMBOL_GREEDY_ONE_OR_MORE: p0 = 1; p1 = 0xFFFF; exp->_p++; isgreedy = TRex_True; break; + case TREX_SYMBOL_GREEDY_ZERO_OR_ONE: p0 = 0; p1 = 1; exp->_p++; isgreedy = TRex_True; break; + case '{': + exp->_p++; + if(!isdigit(*exp->_p)) trex_error(exp,_SC("number expected")); + p0 = (unsigned short)trex_parsenumber(exp); + /*******************************/ + switch(*exp->_p) { + case '}': + p1 = p0; exp->_p++; + break; + case ',': + exp->_p++; + p1 = 0xFFFF; + if(isdigit(*exp->_p)){ + p1 = (unsigned short)trex_parsenumber(exp); + } + trex_expect(exp,'}'); + break; + default: + trex_error(exp,_SC(", or } expected")); + } + /*******************************/ + isgreedy = TRex_True; + break; + + } + if(isgreedy) { + int nnode = trex_newnode(exp,OP_GREEDY); + op = OP_GREEDY; + exp->_nodes[nnode].left = ret; + exp->_nodes[nnode].right = ((p0)<<16)|p1; + ret = nnode; + } + } + if((*exp->_p != TREX_SYMBOL_BRANCH) && (*exp->_p != ')') && (*exp->_p != TREX_SYMBOL_GREEDY_ZERO_OR_MORE) && (*exp->_p != TREX_SYMBOL_GREEDY_ONE_OR_MORE) && (*exp->_p != '\0')) { + int nnode = trex_element(exp); + exp->_nodes[ret].next = nnode; + } + + return ret; +} + +static int trex_list(TRex *exp) +{ + int ret=-1,e; + if(*exp->_p == TREX_SYMBOL_BEGINNING_OF_STRING) { + exp->_p++; + ret = trex_newnode(exp,OP_BOL); + } + e = trex_element(exp); + if(ret != -1) { + exp->_nodes[ret].next = e; + } + else ret = e; + + if(*exp->_p == TREX_SYMBOL_BRANCH) { + int temp,tright; + exp->_p++; + temp = trex_newnode(exp,OP_OR); + exp->_nodes[temp].left = ret; + tright = trex_list(exp); + exp->_nodes[temp].right = tright; + ret = temp; + } + return ret; +} + +static TRexBool trex_matchcclass(int cclass,TRexChar c) +{ + switch(cclass) { + case 'a': return isalpha(c)?TRex_True:TRex_False; + case 'A': return !isalpha(c)?TRex_True:TRex_False; + case 'w': return (isalnum(c) || c == '_')?TRex_True:TRex_False; + case 'W': return (!isalnum(c) && c != '_')?TRex_True:TRex_False; + case 's': return isspace(c)?TRex_True:TRex_False; + case 'S': return !isspace(c)?TRex_True:TRex_False; + case 'd': return isdigit(c)?TRex_True:TRex_False; + case 'D': return !isdigit(c)?TRex_True:TRex_False; + case 'x': return isxdigit(c)?TRex_True:TRex_False; + case 'X': return !isxdigit(c)?TRex_True:TRex_False; + case 'c': return iscntrl(c)?TRex_True:TRex_False; + case 'C': return !iscntrl(c)?TRex_True:TRex_False; + case 'p': return ispunct(c)?TRex_True:TRex_False; + case 'P': return !ispunct(c)?TRex_True:TRex_False; + case 'l': return islower(c)?TRex_True:TRex_False; + case 'u': return isupper(c)?TRex_True:TRex_False; + } + return TRex_False; /*cannot happen*/ +} + +static TRexBool trex_matchclass(TRex* exp,TRexNode *node,TRexChar c) +{ + do { + switch(node->type) { + case OP_RANGE: + if(c >= node->left && c <= node->right) return TRex_True; + break; + case OP_CCLASS: + if(trex_matchcclass(node->left,c)) return TRex_True; + break; + default: + if(c == node->type)return TRex_True; + } + } while((node->next != -1) && (node = &exp->_nodes[node->next])); + return TRex_False; +} + +static const TRexChar *trex_matchnode(TRex* exp,TRexNode *node,const TRexChar *str,TRexNode *next) +{ + + TRexNodeType type = node->type; + switch(type) { + case OP_GREEDY: { + //TRexNode *greedystop = (node->next != -1) ? &exp->_nodes[node->next] : NULL; + TRexNode *greedystop = NULL; + int p0 = (node->right >> 16)&0x0000FFFF, p1 = node->right&0x0000FFFF, nmaches = 0; + const TRexChar *s=str, *good = str; + + if(node->next != -1) { + greedystop = &exp->_nodes[node->next]; + } + else { + greedystop = next; + } + + while((nmaches == 0xFFFF || nmaches < p1)) { + + const TRexChar *stop; + if(!(s = trex_matchnode(exp,&exp->_nodes[node->left],s,greedystop))) + break; + nmaches++; + good=s; + if(greedystop) { + //checks that 0 matches satisfy the expression(if so skips) + //if not would always stop(for instance if is a '?') + if(greedystop->type != OP_GREEDY || + (greedystop->type == OP_GREEDY && ((greedystop->right >> 16)&0x0000FFFF) != 0)) + { + TRexNode *gnext = NULL; + if(greedystop->next != -1) { + gnext = &exp->_nodes[greedystop->next]; + }else if(next && next->next != -1){ + gnext = &exp->_nodes[next->next]; + } + stop = trex_matchnode(exp,greedystop,s,gnext); + if(stop) { + //if satisfied stop it + if(p0 == p1 && p0 == nmaches) break; + else if(nmaches >= p0 && p1 == 0xFFFF) break; + else if(nmaches >= p0 && nmaches <= p1) break; + } + } + } + + if(s >= exp->_eol) + break; + } + if(p0 == p1 && p0 == nmaches) return good; + else if(nmaches >= p0 && p1 == 0xFFFF) return good; + else if(nmaches >= p0 && nmaches <= p1) return good; + return NULL; + } + case OP_OR: { + const TRexChar *asd = str; + TRexNode *temp=&exp->_nodes[node->left]; + while( (asd = trex_matchnode(exp,temp,asd,NULL)) ) { + if(temp->next != -1) + temp = &exp->_nodes[temp->next]; + else + return asd; + } + asd = str; + temp = &exp->_nodes[node->right]; + while( (asd = trex_matchnode(exp,temp,asd,NULL)) ) { + if(temp->next != -1) + temp = &exp->_nodes[temp->next]; + else + return asd; + } + return NULL; + break; + } + case OP_EXPR: + case OP_NOCAPEXPR:{ + TRexNode *n = &exp->_nodes[node->left]; + const TRexChar *cur = str; + int capture = -1; + if(node->type != OP_NOCAPEXPR && node->right == exp->_currsubexp) { + capture = exp->_currsubexp; + exp->_matches[capture].begin = cur; + exp->_currsubexp++; + } + + do { + TRexNode *subnext = NULL; + if(n->next != -1) { + subnext = &exp->_nodes[n->next]; + }else { + subnext = next; + } + if(!(cur = trex_matchnode(exp,n,cur,subnext))) { + if(capture != -1){ + exp->_matches[capture].begin = 0; + exp->_matches[capture].len = 0; + } + return NULL; + } + } while((n->next != -1) && (n = &exp->_nodes[n->next])); + + if(capture != -1) + exp->_matches[capture].len = cur - exp->_matches[capture].begin; + return cur; + } + case OP_WB: + if(str == exp->_bol && !isspace(*str) + || (str == exp->_eol && !isspace(*(str-1))) + || (!isspace(*str) && isspace(*(str+1))) + || (isspace(*str) && !isspace(*(str+1))) ) { + return (node->left == 'b')?str:NULL; + } + return (node->left == 'b')?NULL:str; + case OP_BOL: + if(str == exp->_bol) return str; + return NULL; + case OP_EOL: + if(str == exp->_eol) return str; + return NULL; + case OP_DOT:{ + *str++; + } + return str; + case OP_NCLASS: + case OP_CLASS: + if(trex_matchclass(exp,&exp->_nodes[node->left],*str)?(type == OP_CLASS?TRex_True:TRex_False):(type == OP_NCLASS?TRex_True:TRex_False)) { + *str++; + return str; + } + return NULL; + case OP_CCLASS: + if(trex_matchcclass(node->left,*str)) { + *str++; + return str; + } + return NULL; + default: /* char */ + if(*str != node->type) return NULL; + *str++; + return str; + } + return NULL; +} + +/* public api */ +TRex *trex_compile(const TRexChar *pattern,const TRexChar **error) +{ + TRex *exp = (TRex *)malloc(sizeof(TRex)); + exp->_eol = exp->_bol = NULL; + exp->_p = pattern; + exp->_nallocated = (int)scstrlen(pattern) * sizeof(TRexChar); + exp->_nodes = (TRexNode *)malloc(exp->_nallocated * sizeof(TRexNode)); + exp->_nsize = 0; + exp->_matches = 0; + exp->_nsubexpr = 0; + exp->_first = trex_newnode(exp,OP_EXPR); + exp->_error = error; + exp->_jmpbuf = malloc(sizeof(jmp_buf)); + if(setjmp(*((jmp_buf*)exp->_jmpbuf)) == 0) { + int res = trex_list(exp); + exp->_nodes[exp->_first].left = res; + if(*exp->_p!='\0') + trex_error(exp,_SC("unexpected character")); +#ifdef _DEBUG + { + int nsize,i; + TRexNode *t; + nsize = exp->_nsize; + t = &exp->_nodes[0]; + scprintf(_SC("\n")); + for(i = 0;i < nsize; i++) { + if(exp->_nodes[i].type>MAX_CHAR) + scprintf(_SC("[%02d] %10s "),i,g_nnames[exp->_nodes[i].type-MAX_CHAR]); + else + scprintf(_SC("[%02d] %10c "),i,exp->_nodes[i].type); + scprintf(_SC("left %02d right %02d next %02d\n"),exp->_nodes[i].left,exp->_nodes[i].right,exp->_nodes[i].next); + } + scprintf(_SC("\n")); + } +#endif + exp->_matches = (TRexMatch *) malloc(exp->_nsubexpr * sizeof(TRexMatch)); + memset(exp->_matches,0,exp->_nsubexpr * sizeof(TRexMatch)); + } + else{ + trex_free(exp); + return NULL; + } + return exp; +} + +void trex_free(TRex *exp) +{ + if(exp) { + if(exp->_nodes) free(exp->_nodes); + if(exp->_jmpbuf) free(exp->_jmpbuf); + if(exp->_matches) free(exp->_matches); + free(exp); + } +} + +TRexBool trex_match(TRex* exp,const TRexChar* text) +{ + const TRexChar* res = NULL; + exp->_bol = text; + exp->_eol = text + scstrlen(text); + exp->_currsubexp = 0; + res = trex_matchnode(exp,exp->_nodes,text,NULL); + if(res == NULL || res != exp->_eol) + return TRex_False; + return TRex_True; +} + +TRexBool trex_searchrange(TRex* exp,const TRexChar* text_begin,const TRexChar* text_end,const TRexChar** out_begin, const TRexChar** out_end) +{ + const TRexChar *cur = NULL; + int node = exp->_first; + if(text_begin >= text_end) return TRex_False; + exp->_bol = text_begin; + exp->_eol = text_end; + do { + cur = text_begin; + while(node != -1) { + exp->_currsubexp = 0; + cur = trex_matchnode(exp,&exp->_nodes[node],cur,NULL); + if(!cur) + break; + node = exp->_nodes[node].next; + } + *text_begin++; + } while(cur == NULL && text_begin != text_end); + + if(cur == NULL) + return TRex_False; + + --text_begin; + + if(out_begin) *out_begin = text_begin; + if(out_end) *out_end = cur; + return TRex_True; +} + +TRexBool trex_search(TRex* exp,const TRexChar* text, const TRexChar** out_begin, const TRexChar** out_end) +{ + return trex_searchrange(exp,text,text + scstrlen(text),out_begin,out_end); +} + +int trex_getsubexpcount(TRex* exp) +{ + return exp->_nsubexpr; +} + +TRexBool trex_getsubexp(TRex* exp, int n, TRexMatch *subexp) +{ + if( n<0 || n >= exp->_nsubexpr) return TRex_False; + *subexp = exp->_matches[n]; + return TRex_True; +} + diff --git a/src/openalpr/trex.h b/src/openalpr/trex.h new file mode 100644 index 0000000..bf268b1 --- /dev/null +++ b/src/openalpr/trex.h @@ -0,0 +1,67 @@ +#ifndef _TREX_H_ +#define _TREX_H_ +/*************************************************************** + T-Rex a tiny regular expression library + + Copyright (C) 2003-2006 Alberto Demichelis + + This software is provided 'as-is', without any express + or implied warranty. In no event will the authors be held + liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for + any purpose, including commercial applications, and to alter + it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; + you must not claim that you wrote the original software. + If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but + is not required. + + 2. Altered source versions must be plainly marked as such, + and must not be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any + source distribution. + +****************************************************************/ + +#ifdef _UNICODE +#define TRexChar unsigned short +#define MAX_CHAR 0xFFFF +#define _TREXC(c) L##c +#define trex_strlen wcslen +#define trex_printf wprintf +#else +#define TRexChar char +#define MAX_CHAR 0xFF +#define _TREXC(c) (c) +#define trex_strlen strlen +#define trex_printf printf +#endif + +#ifndef TREX_API +#define TREX_API extern +#endif + +#define TRex_True 1 +#define TRex_False 0 + +typedef unsigned int TRexBool; +typedef struct TRex TRex; + +typedef struct { + const TRexChar *begin; + int len; +} TRexMatch; + +TREX_API TRex *trex_compile(const TRexChar *pattern,const TRexChar **error); +TREX_API void trex_free(TRex *exp); +TREX_API TRexBool trex_match(TRex* exp,const TRexChar* text); +TREX_API TRexBool trex_search(TRex* exp,const TRexChar* text, const TRexChar** out_begin, const TRexChar** out_end); +TREX_API TRexBool trex_searchrange(TRex* exp,const TRexChar* text_begin,const TRexChar* text_end,const TRexChar** out_begin, const TRexChar** out_end); +TREX_API int trex_getsubexpcount(TRex* exp); +TREX_API TRexBool trex_getsubexp(TRex* exp, int n, TRexMatch *subexp); + +#endif diff --git a/src/openalpr/utility.cpp b/src/openalpr/utility.cpp new file mode 100644 index 0000000..a605aa8 --- /dev/null +++ b/src/openalpr/utility.cpp @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + + +#include "utility.h" + + #include + +Rect expandRect(Rect original, int expandXPixels, int expandYPixels, int maxX, int maxY) +{ + Rect expandedRegion = Rect(original); + + float halfX = round((float) expandXPixels / 2.0); + float halfY = round((float) expandYPixels / 2.0); + expandedRegion.x = expandedRegion.x - halfX; + expandedRegion.width = expandedRegion.width + expandXPixels; + expandedRegion.y = expandedRegion.y - halfY; + expandedRegion.height = expandedRegion.height + expandYPixels; + + if (expandedRegion.x < 0) + expandedRegion.x = 0; + if (expandedRegion.y < 0) + expandedRegion.y = 0; + if (expandedRegion.x + expandedRegion.width > maxX) + expandedRegion.width = maxX - expandedRegion.x; + if (expandedRegion.y + expandedRegion.height > maxY) + expandedRegion.height = maxY - expandedRegion.y; + + return expandedRegion; +} + +Mat drawImageDashboard(vector images, int imageType, int numColumns) +{ + int numRows = ceil((float) images.size() / (float) numColumns); + + Mat dashboard(Size(images[0].cols * numColumns, images[0].rows * numRows), imageType); + + for (int i = 0; i < numColumns * numRows; i++) + { + if (i < images.size()) + images[i].copyTo(dashboard(Rect((i%numColumns) * images[i].cols, floor((float) i/numColumns) * images[i].rows, images[i].cols, images[i].rows))); + else + { + Mat black = Mat::zeros(images[0].size(), imageType); + black.copyTo(dashboard(Rect((i%numColumns) * images[0].cols, floor((float) i/numColumns) * images[0].rows, images[0].cols, images[0].rows))); + } + } + + return dashboard; +} + +Mat addLabel(Mat input, string label) +{ + const int border_size = 1; + const Scalar border_color(0,0,255); + const int extraHeight = 20; + const Scalar bg(222,222,222); + const Scalar fg(0,0,0); + + Rect destinationRect(border_size, extraHeight, input.cols, input.rows); + Mat newImage(Size(input.cols + (border_size), input.rows + extraHeight + (border_size )), input.type()); + input.copyTo(newImage(destinationRect)); + + cout << " Adding label " << label << endl; + if (input.type() == CV_8U) + cvtColor(newImage, newImage, CV_GRAY2BGR); + + rectangle(newImage, Point(0,0), Point(input.cols, extraHeight), bg, CV_FILLED); + putText(newImage, label, Point(5, extraHeight - 5), CV_FONT_HERSHEY_PLAIN , 0.7, fg); + + rectangle(newImage, Point(0,0), Point(newImage.cols - 1, newImage.rows -1), border_color, border_size); + + return newImage; +} + + + +void drawAndWait(cv::Mat* frame) +{ + cv::imshow("Temp Window", *frame); + + while (cv::waitKey(50) == -1) + { + // loop + } + + cv::destroyWindow("Temp Window"); +} + +void displayImage(Config* config, string windowName, cv::Mat frame) +{ + if (config->debugShowImages) + imshow(windowName, frame); +} + +vector produceThresholds(const Mat img_gray, Config* config) +{ + const int THRESHOLD_COUNT = 10; + //Mat img_equalized = equalizeBrightness(img_gray); + + timespec startTime; + getTime(&startTime); + + vector thresholds; + + //#pragma omp parallel for + for (int i = 0; i < THRESHOLD_COUNT; i++) + thresholds.push_back(Mat(img_gray.size(), CV_8U)); + + + for (int i = 0; i < THRESHOLD_COUNT; i++) + { + + + if (i <= 2) //0-2 + { + int k = ((i%3) * 5) + 7; // 7, 12, 17 + if (k==12) k = 13; // change 12 to 13 + //#pragma omp ordered + adaptiveThreshold(img_gray, thresholds[i], 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY_INV , k, 3); + } + else if (i <= 6) //3-6 + { + int k = i%2; // 0 or 1 + int win = 18 + (k * 4); // 18 or 22 + //#pragma omp ordered + NiblackSauvolaWolfJolion (img_gray, thresholds[i], WOLFJOLION, win, win, 0.05 + (k * 0.35)); + bitwise_not(thresholds[i], thresholds[i]); + + } + else if (i <= 9) //7-9 + { + int k = (i%3) + 1; // 1,2,3 + //#pragma omp ordered + NiblackSauvolaWolfJolion (img_gray, thresholds[i], SAUVOLA, 12, 12, 0.18 * k); + bitwise_not(thresholds[i], thresholds[i]); + + } + + + + + } + + + + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << " -- Produce Threshold Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + return thresholds; + //threshold(img_equalized, img_threshold, 100, 255, THRESH_BINARY); + +} + + + + +Mat equalizeBrightness(Mat img) +{ + + // Divide the image by its morphologically closed counterpart + Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(19,19)); + Mat closed; + morphologyEx(img, closed, MORPH_CLOSE, kernel); + + img.convertTo(img, CV_32FC1); // divide requires floating-point + divide(img, closed, img, 1, CV_32FC1); + normalize(img, img, 0, 255, NORM_MINMAX); + img.convertTo(img, CV_8U); // convert back to unsigned int + + + return img; +} + +void drawRotatedRect(Mat* img, RotatedRect rect, Scalar color, int thickness) +{ + + Point2f rect_points[4]; + rect.points( rect_points ); + for( int j = 0; j < 4; j++ ) + line( *img, rect_points[j], rect_points[(j+1)%4], color, thickness, 8 ); + +} + +void fillMask(Mat img, const Mat mask, Scalar color) +{ + for (int row = 0; row < img.rows; row++) + { + for (int col = 0; col < img.cols; col++) + { + int m = (int) mask.at(row, col); + + if (m) + { + for (int z = 0; z < 3; z++) + { + int prevVal = img.at(row, col)[z]; + img.at(row, col)[z] = ((int) color[z]) | prevVal; + } + } + + } + } +} + + +void drawX(Mat img, Rect rect, Scalar color, int thickness) +{ + Point tl(rect.x, rect.y); + Point tr(rect.x + rect.width, rect.y); + Point bl(rect.x, rect.y + rect.height); + Point br(rect.x + rect.width, rect.y + rect.height); + + line(img, tl, br, color, thickness); + line(img, bl, tr, color, thickness); +} + +double distanceBetweenPoints(Point p1, Point p2) +{ + float asquared = (p2.x - p1.x)*(p2.x - p1.x); + float bsquared = (p2.y - p1.y)*(p2.y - p1.y); + + return sqrt(asquared + bsquared); +} + +float angleBetweenPoints(Point p1, Point p2) +{ + int deltaY = p2.y - p1.y; + int deltaX = p2.x - p1.x; + + return atan2((float) deltaY, (float) deltaX) * (180 / CV_PI); +} + + +Size getSizeMaintainingAspect(Mat inputImg, int maxWidth, int maxHeight) +{ + float aspect = ((float) inputImg.cols) / ((float) inputImg.rows); + + if (maxWidth / aspect > maxHeight) + { + return Size(maxHeight * aspect, maxHeight); + } + else + { + return Size(maxWidth, maxWidth / aspect); + } +} + + +LineSegment::LineSegment() +{ + init(0, 0, 0, 0); +} + +LineSegment::LineSegment(Point p1, Point p2) +{ + init(p1.x, p1.y, p2.x, p2.y); +} +LineSegment::LineSegment(int x1, int y1, int x2, int y2) +{ + init(x1, y1, x2, y2); +} + +void LineSegment::init(int x1, int y1, int x2, int y2) +{ + this->p1 = Point(x1, y1); + this->p2 = Point(x2, y2); + + if (p2.x - p1.x == 0) + this->slope = 0.00000000001; + else + this->slope = (float) (p2.y - p1.y) / (float) (p2.x - p1.x); + + this->length = distanceBetweenPoints(p1, p2); + + this->angle = angleBetweenPoints(p1, p2); +} + +bool LineSegment::isPointBelowLine( Point tp ){ + return ((p2.x - p1.x)*(tp.y - p1.y) - (p2.y - p1.y)*(tp.x - p1.x)) > 0; +} + +float LineSegment::getPointAt(float x) +{ + return slope * (x - p2.x) + p2.y; +} + +Point LineSegment::closestPointOnSegmentTo(Point p) +{ + float top = (p.x - p1.x) * (p2.x - p1.x) + (p.y - p1.y)*(p2.y - p1.y); + + float bottom = distanceBetweenPoints(p2, p1); + bottom = bottom * bottom; + + float u = top / bottom; + + float x = p1.x + u * (p2.x - p1.x); + float y = p1.y + u * (p2.y - p1.y); + + return Point(x, y); +} + +Point LineSegment::intersection(LineSegment line) +{ + float c1, c2; + float intersection_X = -1, intersection_Y= -1; + + + c1 = p1.y - slope * p1.x; // which is same as y2 - slope * x2 + + c2 = line.p2.y - line.slope * line.p2.x; // which is same as y2 - slope * x2 + + if( (slope - line.slope) == 0) + { + //std::cout << "No Intersection between the lines" << endl; + } + else if (p1.x == p2.x) + { + // Line1 is vertical + return Point(p1.x, line.getPointAt(p1.x)); + } + else if (line.p1.x == line.p2.x) + { + // Line2 is vertical + return Point(line.p1.x, getPointAt(line.p1.x)); + } + else + { + intersection_X = (c2 - c1) / (slope - line.slope); + intersection_Y = slope * intersection_X + c1; + + } + + return Point(intersection_X, intersection_Y); + +} + + + +Point LineSegment::midpoint() +{ + // Handle the case where the line is vertical + if (p1.x == p2.x) + { + float ydiff = p2.y-p1.y; + float y = p1.y + (ydiff/2); + return Point(p1.x, y); + } + float diff = p2.x - p1.x; + float midX = ((float) p1.x) + (diff / 2); + int midY = getPointAt(midX); + + return Point(midX, midY); +} + +LineSegment LineSegment::getParallelLine(float distance) +{ + float diff_x = p2.x - p1.x; + float diff_y = p2.y - p1.y; + float angle = atan2( diff_x, diff_y); + float dist_x = distance * cos(angle); + float dist_y = -distance * sin(angle); + + int offsetX = (int)round(dist_x); + int offsetY = (int)round(dist_y); + + LineSegment result(p1.x + offsetX, p1.y + offsetY, + p2.x + offsetX, p2.y + offsetY); + + return result; +} + + + + + diff --git a/src/openalpr/utility.h b/src/openalpr/utility.h new file mode 100644 index 0000000..26f6add --- /dev/null +++ b/src/openalpr/utility.h @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + + +#ifndef UTILITY_H +#define UTILITY_H + + + + +#include +#include +#include + +#include "constants.h" +#include "support/timing.h" + #include "opencv2/highgui/highgui.hpp" + #include "opencv2/imgproc/imgproc.hpp" +#include "opencv2/core/core.hpp" +#include "binarize_wolf.h" +#include +#include "config.h" + + + + /* +struct LineSegment +{ + float x1; + float y1; + float x2; + float y2; +}; +*/ + + +class LineSegment +{ + + public: + Point p1, p2; + float slope; + float length; + float angle; + + // LineSegment(Point point1, Point point2); + LineSegment(); + LineSegment(int x1, int y1, int x2, int y2); + LineSegment(Point p1, Point p2); + + void init(int x1, int y1, int x2, int y2); + + bool isPointBelowLine(Point tp); + + float getPointAt(float x); + + Point closestPointOnSegmentTo(Point p); + + Point intersection(LineSegment line); + + LineSegment getParallelLine(float distance); + + Point midpoint(); + + inline std::string str() + { + std::stringstream ss; + ss << "(" << p1.x << ", " << p1.y << ") : (" << p2.x << ", " << p2.y << ")"; + return ss.str() ; + } + + +}; + + + + vector produceThresholds(const Mat img_gray, Config* config); + + Mat drawImageDashboard(vector images, int imageType, int numColumns); + + void displayImage(Config* config, string windowName, cv::Mat frame); + void drawAndWait(cv::Mat* frame); + + double distanceBetweenPoints(Point p1, Point p2); + + void drawRotatedRect(Mat* img, RotatedRect rect, Scalar color, int thickness); + + void drawX(Mat img, Rect rect, Scalar color, int thickness); + void fillMask(Mat img, const Mat mask, Scalar color); + + float angleBetweenPoints(Point p1, Point p2); + + Size getSizeMaintainingAspect(Mat inputImg, int maxWidth, int maxHeight); + + Mat equalizeBrightness(Mat img); + + Rect expandRect(Rect original, int expandXPixels, int expandYPixels, int maxX, int maxY); + + Mat addLabel(Mat input, string label); + +#endif // UTILITY_H diff --git a/src/openalpr/verticalhistogram.cpp b/src/openalpr/verticalhistogram.cpp new file mode 100644 index 0000000..65ae68e --- /dev/null +++ b/src/openalpr/verticalhistogram.cpp @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#include "verticalhistogram.h" + +VerticalHistogram::VerticalHistogram(Mat inputImage, Mat mask) +{ + analyzeImage(inputImage, mask); + + + +} + +VerticalHistogram::~VerticalHistogram() +{ + debugImg.release(); + colHeights.clear(); +} + + +void VerticalHistogram::analyzeImage(Mat inputImage, Mat mask) +{ + highestPeak = 0; + lowestValley = inputImage.rows; + + + debugImg = Mat::zeros(inputImage.size(), CV_8U); + + int columnCount; + + for (int col = 0; col < inputImage.cols; col++) + { + columnCount = 0; + + for (int row = 0; row < inputImage.rows; row++) + { + + if (inputImage.at(row, col) > 0 && mask.at(row, col) > 0) + columnCount++; + } + + + for (; columnCount > 0; columnCount--) + debugImg.at(inputImage.rows - columnCount, col) = 255; + + this->colHeights.push_back(columnCount); + + if (columnCount < lowestValley) + lowestValley = columnCount; + if (columnCount > highestPeak) + highestPeak = columnCount; + } + +} + +void VerticalHistogram::findValleys() +{ + int MINIMUM_PEAK_HEIGHT = (int) (((float) highestPeak) * 0.75); + + int totalWidth = colHeights.size(); + + int midpoint = ((highestPeak - lowestValley) / 2) + lowestValley; + + HistogramDirection prevDirection = FALLING; + + int relativePeakHeight = 0; + int valleyStart = 0; + + for (int i = 0; i < totalWidth; i++) + { + bool aboveMidpoint = (colHeights[i] >= midpoint); + + if (aboveMidpoint) + { + if (colHeights[i] > relativePeakHeight) + relativePeakHeight = colHeights[i]; + + + prevDirection = FLAT; + + } + else + { + relativePeakHeight = 0; + + HistogramDirection direction = getHistogramDirection(i); + + if ((prevDirection == FALLING || prevDirection == FLAT) && direction == RISING) + { + + } + else if ((prevDirection == FALLING || prevDirection == FLAT) && direction == RISING) + { + + } + } + + } +} + +HistogramDirection VerticalHistogram::getHistogramDirection(int index) +{ + int EXTRA_WIDTH_TO_AVERAGE = 2; + + float trailingAverage = 0; + float forwardAverage = 0; + + int trailStartIndex = index - EXTRA_WIDTH_TO_AVERAGE; + if (trailStartIndex < 0) + trailStartIndex = 0; + int forwardEndIndex = index + EXTRA_WIDTH_TO_AVERAGE; + if (forwardEndIndex >= colHeights.size()) + forwardEndIndex = colHeights.size() - 1; + + for (int i = index; i >= trailStartIndex; i--) + { + trailingAverage += colHeights[i]; + } + trailingAverage = trailingAverage / ((float) (1 + index - trailStartIndex)); + + for (int i = index; i <= forwardEndIndex; i++) + { + forwardAverage += colHeights[i]; + } + forwardAverage = forwardAverage / ((float) (1 + forwardEndIndex - index)); + + + float diff = forwardAverage - trailingAverage; + float minDiff = ((float) (highestPeak - lowestValley)) * 0.10; + + if (diff > minDiff) + return RISING; + else if (diff < minDiff) + return FALLING; + else + return FLAT; +} + + diff --git a/src/openalpr/verticalhistogram.h b/src/openalpr/verticalhistogram.h new file mode 100644 index 0000000..cae304d --- /dev/null +++ b/src/openalpr/verticalhistogram.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + +#ifndef VERTICALHISTOGRAM_H +#define VERTICALHISTOGRAM_H + +#include "opencv2/imgproc/imgproc.hpp" + +using namespace cv; +using namespace std; + + +struct Valley +{ + int startIndex; + int endIndex; + int width; + int pixelsWithin; +}; + +enum HistogramDirection { RISING, FALLING, FLAT }; + +class VerticalHistogram +{ + + public: + VerticalHistogram(Mat inputImage, Mat mask); + virtual ~VerticalHistogram(); + + Mat debugImg; + + + private: + vector colHeights; + int highestPeak; + int lowestValley; + vector valleys; + + void analyzeImage(Mat inputImage, Mat mask); + void findValleys(); + + HistogramDirection getHistogramDirection(int index); +}; + +#endif // VERTICALHISTOGRAM_H \ No newline at end of file From 19069aab92165ca312d36a84346cf0c811998538 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 2 Jan 2014 16:02:41 -0600 Subject: [PATCH 05/14] Added CMake config --- src/openalpr/CMakeLists.txt | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/openalpr/CMakeLists.txt diff --git a/src/openalpr/CMakeLists.txt b/src/openalpr/CMakeLists.txt new file mode 100644 index 0000000..79097cc --- /dev/null +++ b/src/openalpr/CMakeLists.txt @@ -0,0 +1,34 @@ + + + + +set(lpr_source_files + alpr.cpp + alpr_impl.cpp + config.cpp + regiondetector.cpp + licenseplatecandidate.cpp + utility.cpp + stateidentifier.cpp + featurematcher.cpp + ocr.cpp + postprocess.cpp + binarize_wolf.cpp + platelines.cpp + characterregion.cpp + charactersegmenter.cpp + platecorners.cpp + colorfilter.cpp + characteranalysis.cpp + verticalhistogram.cpp + trex.c + cjson.c +) + + +add_library(openalpr ${lpr_source_files}) + + + +add_subdirectory(simpleini) +add_subdirectory(support) \ No newline at end of file From 2b221e409e500203d798dc747853b76d504a1500 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 2 Jan 2014 16:03:00 -0600 Subject: [PATCH 06/14] Added Main CLI program and CMake script --- src/CMakeLists.txt | 56 ++++++++++ src/main.cpp | 255 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 src/CMakeLists.txt create mode 100644 src/main.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..06628d5 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,56 @@ +project(src) + + +cmake_minimum_required (VERSION 2.6) + +IF (WIN32) + add_definitions( -DWINDOWS) + add_definitions( -DNOMINMAX) + + SET(OpenCV_DIR "C:\\projects\\alpr\\libraries\\opencv") + SET(Tesseract_DIR "C:\\projects\\alpr\\libraries\\tesseract-3.02") + + include_directories( + ${Tesseract_DIR}/include/tesseract + + ) + link_directories( ${Tesseract_DIR}/lib/ ) +ELSE() + + SET(OpenCV_DIR "../libraries/opencv/") + SET(Tesseract_DIR "/home/mhill/projects/alpr/libraries/tesseract-ocr") + + include_directories( + ${Tesseract_DIR}/api + ${Tesseract_DIR}/ccutil/ + ${Tesseract_DIR}/ccstruct/ + ${Tesseract_DIR}/ccmain/ + + ) + link_directories( ${Tesseract_DIR}/api/.libs/ ) +ENDIF() + + +# Opencv Package +FIND_PACKAGE( OpenCV REQUIRED ) +IF (${OpenCV_VERSION} VERSION_LESS 2.3.0) + MESSAGE(FATAL_ERROR "OpenCV version is not compatible : ${OpenCV_VERSION}") +ENDIF() + + +include_directories(./openalpr ) + + +set(CMAKE_CSS_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Wall ") +ADD_EXECUTABLE( alpr main.cpp ) +TARGET_LINK_LIBRARIES(alpr + openalpr + support + ${OpenCV_LIBS} + tesseract + ) + + +add_subdirectory(openalpr) +add_subdirectory(misc_utilities) + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..0386964 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + + + #include + #include + #include + + #include "opencv2/highgui/highgui.hpp" + #include "opencv2/imgproc/imgproc.hpp" + + + #include "tclap/CmdLine.h" + #include "support/filesystem.h" + #include "support/timing.h" + #include "alpr.h" + + + + 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; + + int main( int argc, const char** argv ) + { + std::string filename; + std::string runtimePath = ""; + bool outputJson = false; + int seektoms = 0; + bool detectRegion = false; + std::string templateRegion; + std::string country; + int topn; + + try { + + TCLAP::CmdLine cmd("OpenAlpr Command Line Utility", ' ', OPENALPR_VERSION); + + TCLAP::UnlabeledValueArg fileArg( "image_file", "Image containing license plates", true, "", "image_file_path" ); + + TCLAP::ValueArg countryCodeArg("c","country","Country code to identify (either us for USA or eu for Europe). Default=us",false, "us" ,"country_code"); + TCLAP::ValueArg seekToMsArg("","seek","Seek to the specied millisecond in a video file. Default=0",false, 0 ,"integer_ms"); + TCLAP::ValueArg runtimeDirArg("r","runtime_dir","Path to the OpenAlpr runtime data directory",false, "" ,"runtime_dir"); + TCLAP::ValueArg templateRegionArg("t","template_region","Attempt to match the plate number against a region template (e.g., md for Maryland, ca for California)",false, "" ,"region code"); + TCLAP::ValueArg topNArg("n","topn","Max number of possible plate numbers to return. Default=10",false, 10 ,"topN"); + + TCLAP::SwitchArg jsonSwitch("j","json","Output recognition results in JSON format. Default=off", cmd, false); + TCLAP::SwitchArg detectRegionSwitch("d","detect_region","Attempt to detect the region of the plate image. Default=off", cmd, false); + TCLAP::SwitchArg clockSwitch("","clock","Measure/print the total time to process image and all plates. Default=off", cmd, false); + + cmd.add( fileArg ); + cmd.add( countryCodeArg ); + cmd.add( seekToMsArg ); + cmd.add( topNArg ); + cmd.add( runtimeDirArg ); + cmd.add( templateRegionArg ); + + cmd.parse( argc, argv ); + + filename = fileArg.getValue(); + + country = countryCodeArg.getValue(); + seektoms = seekToMsArg.getValue(); + outputJson = jsonSwitch.getValue(); + runtimePath = runtimeDirArg.getValue(); + detectRegion = detectRegionSwitch.getValue(); + templateRegion = templateRegionArg.getValue(); + topn = topNArg.getValue(); + measureProcessingTime = clockSwitch.getValue(); + + } catch (TCLAP::ArgException &e) // catch any exceptions + { + std::cerr << "error: " << e.error() << " for arg " << e.argId() << std::endl; + return 1; + } + + cv::Mat frame; + + + Alpr alpr(country, runtimePath); + alpr.setTopN(topn); + + if (detectRegion) + alpr.setDetectRegion(detectRegion); + + if (strcmp(templateRegion.c_str(), "") != 0) + { + alpr.setDefaultRegion(templateRegion); + } + + if (alpr.isLoaded() == false) + { + std::cerr << "Error loading OpenAlpr" << std::endl; + return 1; + } + + if (strcmp(filename.c_str(), "webcam") == 0) + { + int framenum = 0; + cv::VideoCapture cap(0); + if (!cap.isOpened()) + { + std::cout << "Error opening webcam" << std::endl; + return 1; + } + + while (cap.read(frame) == true) + { + detectandshow(&alpr, frame, "", outputJson); + cv::waitKey(1); + framenum++; + } + } + else if (hasEnding(filename, ".avi") || hasEnding(filename, ".mp4") || hasEnding(filename, ".webm") || hasEnding(filename, ".flv")) + { + int framenum = 0; + + cv::VideoCapture cap=cv::VideoCapture(); + cap.open(filename); + cap.set(CV_CAP_PROP_POS_MSEC, seektoms); + + while (cap.read(frame) == true) + { + if (SAVE_LAST_VIDEO_STILL == true) + { + cv::imwrite(LAST_VIDEO_STILL_LOCATION, frame); + } + std::cout << "Frame: " << framenum << std::endl; + + detectandshow( &alpr, frame, "", outputJson); + //create a 1ms delay + cv::waitKey(1); + framenum++; + } + + } + else if (hasEnding(filename, ".png") || hasEnding(filename, ".jpg") || hasEnding(filename, ".gif")) + { + frame = cv::imread( filename ); + + detectandshow( &alpr, frame, "", outputJson); + + + } + else if (DirectoryExists(filename.c_str())) + { + std::vector files = getFilesInDir(filename.c_str()); + + std::sort( files.begin(), files.end(), stringCompare ); + + for (int i = 0; i< files.size(); i++) + { + if (hasEnding(files[i], ".jpg") || hasEnding(files[i], ".png")) + { + std::string fullpath = filename + "/" + files[i]; + std::cout << fullpath << std::endl; + frame = cv::imread( fullpath.c_str() ); + if (detectandshow( &alpr, frame, "", outputJson)) + { + //while ((char) cv::waitKey(50) != 'c') { } + } + else + { + //cv::waitKey(50); + } + } + + } + } + else + { + std::cerr << "Unknown file type" << std::endl; + return 1; + } + + + + return 0; + } + + + bool detectandshow( Alpr* alpr, cv::Mat frame, std::string region, bool writeJson) + { + + + std::vector buffer; + cv::imencode(".bmp", frame, buffer ); + + + timespec startTime; + getTime(&startTime); + + + std::vector results = alpr->recognize(buffer); + + + if (writeJson) + { + std::cout << alpr->toJson(results) << std::endl; + } + else + { + for (int i = 0; i < results.size(); i++) + { + std::cout << "plate" << i << ": " << results[i].result_count << " results -- Processing Time = " << results[i].processing_time_ms << "ms." << std::endl; + + for (int k = 0; k < results[i].topNPlates.size(); k++) + { + std::cout << " - " << results[i].topNPlates[k].characters << "\t confidence: " << results[i].topNPlates[k].overall_confidence << "\t template_match: " << results[i].topNPlates[k].matches_template << std::endl; + } + + } + } + + timespec endTime; + getTime(&endTime); + if (measureProcessingTime) + std::cout << "Total Time to process image: " << diffclock(startTime, endTime) << "ms." << std::endl; + + + if (results.size() > 0) + return true; + return false; + + } + + + + From 00b32d821f714b61507ad5b705654bb0ec144f99 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 2 Jan 2014 16:07:07 -0600 Subject: [PATCH 07/14] Added runtime data --- runtime_data/keypoints/us/ak2008.jpg | Bin 0 -> 10678 bytes runtime_data/keypoints/us/al2002.jpg | Bin 0 -> 10494 bytes runtime_data/keypoints/us/ar2006.jpg | Bin 0 -> 8507 bytes runtime_data/keypoints/us/az1996.jpg | Bin 0 -> 12055 bytes runtime_data/keypoints/us/ca1993.jpg | Bin 0 -> 5364 bytes runtime_data/keypoints/us/co2000.jpg | Bin 0 -> 11371 bytes runtime_data/keypoints/us/ct2000.jpg | Bin 0 -> 10661 bytes runtime_data/keypoints/us/dc2003.jpg | Bin 0 -> 8788 bytes runtime_data/keypoints/us/de1970.jpg | Bin 0 -> 7032 bytes runtime_data/keypoints/us/fl2004.jpg | Bin 0 -> 8145 bytes runtime_data/keypoints/us/ga2007.jpg | Bin 0 -> 6506 bytes runtime_data/keypoints/us/hi1991.jpg | Bin 0 -> 7566 bytes runtime_data/keypoints/us/ia1997.jpg | Bin 0 -> 9371 bytes runtime_data/keypoints/us/id2006.jpg | Bin 0 -> 10690 bytes runtime_data/keypoints/us/il2002.jpg | Bin 0 -> 8414 bytes runtime_data/keypoints/us/in2009.jpg | Bin 0 -> 7637 bytes runtime_data/keypoints/us/ks2007b.jpg | Bin 0 -> 8207 bytes runtime_data/keypoints/us/ky2005.jpg | Bin 0 -> 8144 bytes runtime_data/keypoints/us/la2006.jpg | Bin 0 -> 8074 bytes runtime_data/keypoints/us/ma1987.jpg | Bin 0 -> 6628 bytes runtime_data/keypoints/us/md2006.jpg | Bin 0 -> 6297 bytes runtime_data/keypoints/us/md2006b.jpg | Bin 0 -> 9511 bytes runtime_data/keypoints/us/md2006c.jpg | Bin 0 -> 10889 bytes runtime_data/keypoints/us/me1999.jpg | Bin 0 -> 9018 bytes runtime_data/keypoints/us/me1999b.jpg | Bin 0 -> 12876 bytes runtime_data/keypoints/us/mi2007.jpg | Bin 0 -> 8548 bytes runtime_data/keypoints/us/mn2000.jpg | Bin 0 -> 7524 bytes runtime_data/keypoints/us/mo2006b.jpg | Bin 0 -> 10016 bytes runtime_data/keypoints/us/mo2009.jpg | Bin 0 -> 6598 bytes runtime_data/keypoints/us/ms2003.jpg | Bin 0 -> 10144 bytes runtime_data/keypoints/us/mt2010.jpg | Bin 0 -> 8884 bytes runtime_data/keypoints/us/nc1982.jpg | Bin 0 -> 10222 bytes runtime_data/keypoints/us/nd1993.jpg | Bin 0 -> 9919 bytes runtime_data/keypoints/us/ne2005.jpg | Bin 0 -> 11204 bytes runtime_data/keypoints/us/nh1999.jpg | Bin 0 -> 10588 bytes runtime_data/keypoints/us/nj1993.jpg | Bin 0 -> 8021 bytes runtime_data/keypoints/us/nm2010.jpg | Bin 0 -> 9954 bytes runtime_data/keypoints/us/nv2001.jpg | Bin 0 -> 8876 bytes runtime_data/keypoints/us/ny2010.jpg | Bin 0 -> 8844 bytes runtime_data/keypoints/us/oh2004.jpg | Bin 0 -> 9627 bytes runtime_data/keypoints/us/ok2009.jpg | Bin 0 -> 8673 bytes runtime_data/keypoints/us/or1990.jpg | Bin 0 -> 7824 bytes runtime_data/keypoints/us/pa2004.jpg | Bin 0 -> 8839 bytes runtime_data/keypoints/us/ri1996.jpg | Bin 0 -> 6534 bytes runtime_data/keypoints/us/sc2008.jpg | Bin 0 -> 10514 bytes runtime_data/keypoints/us/sd2007.jpg | Bin 0 -> 10588 bytes runtime_data/keypoints/us/tn2007.jpg | Bin 0 -> 7651 bytes runtime_data/keypoints/us/tx2009.jpg | Bin 0 -> 10330 bytes runtime_data/keypoints/us/ut2009.jpg | Bin 0 -> 9448 bytes runtime_data/keypoints/us/va2003.jpg | Bin 0 -> 8152 bytes runtime_data/keypoints/us/vt1985.jpg | Bin 0 -> 8085 bytes runtime_data/keypoints/us/wa1998.jpg | Bin 0 -> 9813 bytes runtime_data/keypoints/us/wi2007.jpg | Bin 0 -> 6823 bytes runtime_data/keypoints/us/wv1995.jpg | Bin 0 -> 7659 bytes runtime_data/keypoints/us/wy2000.jpg | Bin 0 -> 12087 bytes runtime_data/ocr/tessdata/leu.traineddata | Bin 0 -> 324589 bytes runtime_data/ocr/tessdata/lus.traineddata | Bin 0 -> 319970 bytes runtime_data/openalpr.conf | 115 ++ runtime_data/postprocess/eu.patterns | 0 runtime_data/postprocess/readme.txt | 7 + runtime_data/postprocess/us.patterns | 212 ++++ runtime_data/region/eu.xml | 788 +++++++++++++ runtime_data/region/us.xml | 1256 +++++++++++++++++++++ 63 files changed, 2378 insertions(+) create mode 100644 runtime_data/keypoints/us/ak2008.jpg create mode 100644 runtime_data/keypoints/us/al2002.jpg create mode 100644 runtime_data/keypoints/us/ar2006.jpg create mode 100644 runtime_data/keypoints/us/az1996.jpg create mode 100644 runtime_data/keypoints/us/ca1993.jpg create mode 100644 runtime_data/keypoints/us/co2000.jpg create mode 100644 runtime_data/keypoints/us/ct2000.jpg create mode 100644 runtime_data/keypoints/us/dc2003.jpg create mode 100644 runtime_data/keypoints/us/de1970.jpg create mode 100644 runtime_data/keypoints/us/fl2004.jpg create mode 100644 runtime_data/keypoints/us/ga2007.jpg create mode 100644 runtime_data/keypoints/us/hi1991.jpg create mode 100644 runtime_data/keypoints/us/ia1997.jpg create mode 100644 runtime_data/keypoints/us/id2006.jpg create mode 100644 runtime_data/keypoints/us/il2002.jpg create mode 100644 runtime_data/keypoints/us/in2009.jpg create mode 100644 runtime_data/keypoints/us/ks2007b.jpg create mode 100644 runtime_data/keypoints/us/ky2005.jpg create mode 100644 runtime_data/keypoints/us/la2006.jpg create mode 100644 runtime_data/keypoints/us/ma1987.jpg create mode 100644 runtime_data/keypoints/us/md2006.jpg create mode 100644 runtime_data/keypoints/us/md2006b.jpg create mode 100644 runtime_data/keypoints/us/md2006c.jpg create mode 100644 runtime_data/keypoints/us/me1999.jpg create mode 100644 runtime_data/keypoints/us/me1999b.jpg create mode 100644 runtime_data/keypoints/us/mi2007.jpg create mode 100644 runtime_data/keypoints/us/mn2000.jpg create mode 100644 runtime_data/keypoints/us/mo2006b.jpg create mode 100644 runtime_data/keypoints/us/mo2009.jpg create mode 100644 runtime_data/keypoints/us/ms2003.jpg create mode 100644 runtime_data/keypoints/us/mt2010.jpg create mode 100644 runtime_data/keypoints/us/nc1982.jpg create mode 100644 runtime_data/keypoints/us/nd1993.jpg create mode 100644 runtime_data/keypoints/us/ne2005.jpg create mode 100644 runtime_data/keypoints/us/nh1999.jpg create mode 100644 runtime_data/keypoints/us/nj1993.jpg create mode 100644 runtime_data/keypoints/us/nm2010.jpg create mode 100644 runtime_data/keypoints/us/nv2001.jpg create mode 100644 runtime_data/keypoints/us/ny2010.jpg create mode 100644 runtime_data/keypoints/us/oh2004.jpg create mode 100644 runtime_data/keypoints/us/ok2009.jpg create mode 100644 runtime_data/keypoints/us/or1990.jpg create mode 100644 runtime_data/keypoints/us/pa2004.jpg create mode 100644 runtime_data/keypoints/us/ri1996.jpg create mode 100644 runtime_data/keypoints/us/sc2008.jpg create mode 100644 runtime_data/keypoints/us/sd2007.jpg create mode 100644 runtime_data/keypoints/us/tn2007.jpg create mode 100644 runtime_data/keypoints/us/tx2009.jpg create mode 100644 runtime_data/keypoints/us/ut2009.jpg create mode 100644 runtime_data/keypoints/us/va2003.jpg create mode 100644 runtime_data/keypoints/us/vt1985.jpg create mode 100644 runtime_data/keypoints/us/wa1998.jpg create mode 100644 runtime_data/keypoints/us/wi2007.jpg create mode 100644 runtime_data/keypoints/us/wv1995.jpg create mode 100644 runtime_data/keypoints/us/wy2000.jpg create mode 100644 runtime_data/ocr/tessdata/leu.traineddata create mode 100644 runtime_data/ocr/tessdata/lus.traineddata create mode 100644 runtime_data/openalpr.conf create mode 100644 runtime_data/postprocess/eu.patterns create mode 100644 runtime_data/postprocess/readme.txt create mode 100644 runtime_data/postprocess/us.patterns create mode 100644 runtime_data/region/eu.xml create mode 100644 runtime_data/region/us.xml diff --git a/runtime_data/keypoints/us/ak2008.jpg b/runtime_data/keypoints/us/ak2008.jpg new file mode 100644 index 0000000000000000000000000000000000000000..166b85612411901ed3bcc5162ab8b5c730379220 GIT binary patch literal 10678 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/al2002.jpg b/runtime_data/keypoints/us/al2002.jpg new file mode 100644 index 0000000000000000000000000000000000000000..572db634a0087f13e345cdab3eb7aab6eb778f5f GIT binary patch literal 10494 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/az1996.jpg b/runtime_data/keypoints/us/az1996.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2ad91f621a7fa008f95ee3f83c20957c36a5e236 GIT binary patch literal 12055 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 Pn4nAloUr-W+4%nep${@Q literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/ct2000.jpg b/runtime_data/keypoints/us/ct2000.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6e46ce5993f0c8380937c0f5afffdfd8da4edc8f GIT binary patch literal 10661 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/ia1997.jpg b/runtime_data/keypoints/us/ia1997.jpg new file mode 100644 index 0000000000000000000000000000000000000000..950f7c7b26609c8399faea91f9c6e7c19711dc45 GIT binary patch literal 9371 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/id2006.jpg b/runtime_data/keypoints/us/id2006.jpg new file mode 100644 index 0000000000000000000000000000000000000000..10ccd1e0ccc1fa981d755f78fca8c1a1a490032b GIT binary patch literal 10690 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/il2002.jpg b/runtime_data/keypoints/us/il2002.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a84822f2461a508e78d018e8438a53ac8e1d4ffc GIT binary patch literal 8414 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/ks2007b.jpg b/runtime_data/keypoints/us/ks2007b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..208c923249dd90970bdbf99cef35c8a810396b57 GIT binary patch literal 8207 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/la2006.jpg b/runtime_data/keypoints/us/la2006.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d5aa446398d4e5fe5726aa0cc01758b0c58787ff GIT binary patch literal 8074 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 K=72kT_J05-Nu}Zd literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/md2006.jpg b/runtime_data/keypoints/us/md2006.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eefe67d65ed27f0c9b1e9e8c3160f3d3fa4d70c0 GIT binary patch literal 6297 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 WR`j&DuQK$IH<}c6omEcZr~eDAX!Lsk literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/md2006b.jpg b/runtime_data/keypoints/us/md2006b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..03da1290dfc72be9dfdb94de8aecba4343eaf8d1 GIT binary patch literal 9511 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 KG90I;h5rKO1%F5Y literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/md2006c.jpg b/runtime_data/keypoints/us/md2006c.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a23934099be34f63664c359362ea50d94242eedf GIT binary patch literal 10889 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 Smev~Sy(gl?%Dl1nbN>TiD_9Hw literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/mi2007.jpg b/runtime_data/keypoints/us/mi2007.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1fb9ad1e006267ad62af234feb394e5b427f4b39 GIT binary patch literal 8548 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/mo2009.jpg b/runtime_data/keypoints/us/mo2009.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5379d7f6e23dfed1e2bfe4567646cbebc12489b7 GIT binary patch literal 6598 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/ms2003.jpg b/runtime_data/keypoints/us/ms2003.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6bc1876b32244ae2692a2c93e92f9284ce259c33 GIT binary patch literal 10144 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

mZfLI zfr$EgDFkqSjrc8Zm1(j8|GfIHmoxA5jzvw$*DKfI*Vbq5QO`>Yrv}6rMlsf^Cz~)E zO9zqx+txJ|2jAsQud)c8%t;cv;#-}DkFkIG-w@B_y3PK$Ol}~np!xGJWJ`Nmk>8_( zwx-cfQm@?#jGu`>XlWjss8JhS$!dd{P}Eb+$7($>Ql1x^$nF!qtji4X3B`EuNB zz4PTN^?f@(j~6_mUskTF@5w!_hO^)+j}4{>hOXI1$B@ONW&OcScODYSUIzo|6cU4h?e#ovkB3@S>Z#At1BnN3@q4J;Aw4GGPhu6sF7dwLZionGW?DM{`x>a zvRkluM&^-6cw(u~SaIae#QbM>L6wYDOz|(LS4-*Rp^7a>6<*9AJy#8`)=O$iTs2lb zRN~@#T*jk}nR#P6COSCMCBtcx$!Vbv(3gc7CSoBT<_ytYPkdqvBaf9fhP$K(2jTP6 zedB2?6^~)Kv)qlr2x~GhbgJOsXfl0hXmzGop?Jj1w40$tL)qHhuop7^v(Yawih0!b$7CgImJmAj> ziF<-SG;d^n`_wmgZd-nUC$GXap^>@>?rg|AdK&7yB%^dL&;;f&HUpa$Ap^*|bXPK9 zO_}-NdZJ{1>4V1B?LfCRj6_oigsoHDw^*sRzUVVDihm~l%s8f=HET-Q4Zk}nPAh1g zHEM^zYbp5}t{U_pydYj3?e~l>H%I%!#5%L@U#w-^XE_%Xe_SWG#?Ej#HU@MjQhIj4 z%=8v}Xg~-V5J_&bY|6RwE?K&C@UIj9-;N7mc1aBs5z~`_@3t@^%Xa4svJCnfjQ4=h zXXL@WCkxMhZ+r}Za6xcUsSDXt2Skh6s4r431MTI!l;DBO1Q8X?_wmSfAtB(qx!^2n zrBUxSLB29wA>TM?_pGVzvDqDVBT)~Yy0@QU%MXabhV^lz^Jb2NZOS=6$iP?kVU35^ zeK7c*S@E-X-F>+084t?yXkKW$AlP#8r?Q8}q^&x0F}%l~YNG(!%k$E5O_4VbR@r3uaijlDZv)IAkEp0ZZ3ZE{B_HG@yMghM29hW<0pRfCY$+d(P!68gvh8zP<% z;H5OK^4%Zu&F|bEmr6QqT(E|bKp3@;)L!2G^$XvEXPh##2dxO|xK}khA;;}kTSjr6 z@oNoZ{_K_6SBzO3MyA2?0zO$*A9W9T>+Fc{G|dHztL(SbBl>o0iaE@1qqjzwMqXZ(U)Tn{QTT7bm2`bHF6=uKjjQvP2Q}e;ImH&!(rMx&Pxo09lGJ zgkUGSSwwh9Gi{T^;~)|j#g9b>6GC@>qS!${W)ZZo-dfo3$9nL;W;XY)nIWUT=)n4= zE3%&Dn}A(@7!HNUu(Ii@qB{+b9=YI4{TE5^206&UBiA|~dVt!@XCvrBfPz+@EY>xs zKxXpD@7+^M&K$htMtalAh1+r2Ms7*($nsREE7uy2U2^(9NeuMHA3+X%4!E}6d(M*# zx!pPqE%do|3j#cD&&VEh%yv!7`dzKHW)+LSQ4>28Fay>b3WFyRbSr(x0QI>tG`KNk zJl{sRF;kJjPXrFs^B%IN$!S~k)``^_@(tzleAXBf)sV}Pb)#F8K(QdSb zw%YI7aPlen%E)`9BYy)^1LF|Qh18&2iP{06DB9ox=N;+}g_g~SA-%kAS}&-dg z`1DG3Q@nV6(7@?ip3Mntbm3)*mwtx}VdA3c^pgvwwHM7AbGH^2yFQcdj;4w7J{14Z zdz#fdhOSIA@IiLU<7D-xI`zvuodv)8Hcg%%Q`dc}H0q+gDmTCJX}*`~uDobgbQp_t z96kp%hi!e~e^AV<@9j9*NYCHD_KJr7UH%vuphB2+mRV&6wB+2#S1kE*F=|sy_cFC{ zFQ;`V823l~EaAk?I0*rJ=hGhfp?yYC%l6Y!IxSM!a)?*FXz3HZqTah-SzaSn33{^+ zJ}^x9O?@(mliCdOXKtmT7uURxoQAMgk0b^&M(}js?VM4}>8STm6?_D`NP9%v9>rH# z(CPcAn-f|!a2h2jLu0H=H)8kF@tmv9QLy+@?w-h-TgKt-t@zNhiQ`z~uHaU51g6yU zboj%BV!p_o8D)O8mX@c~;2YNWqm;Nvh2?jhQ}~G5ds90YR&yFdp|RfnjI69_0iUHP zF{f?qi+E!c66@Zu@SQ7hJm67}ye<53prPqbpyLeO!G*8$6wwc1^ezxd@N4nkA(V)Q zi;9)@VOq9&UAv0{dWY*rj2CQeee14^#oyXKErW>+G@B||Pvt7j26qs(%4mnL?G~`F zx~VRo3&_X|9<2fuL7!cF*PJzQ+V6CmXNdXkEkT{OC7Y1-`oFg)9ZqG@Woa4c@i!3{ zLWLcD*c}A)Zskv&)4$4VxT+>FQ_uh25R4-OcV`Y{+3_&TZ+}e7#+p9bWY{=WHeNyP zQ$zS4zE9JB;&i{{;U6*pJdvx93G7ZDc}c19rO}h8T$*}hAm$^bX_Fj})J6VY%hfQY zx!V^0RI_4^kI`umQ~7~;ueH9fE&vBlj?n!FD{Fwe(2WpP&;R78cr3&r)5gBUwV?3q zt1TIbyFAya@XnKa7cm(gS?Qv*+@oIfS@wFi$D_wg^)H|#PVb!8)~RZ_j?F#3pYGoM zXs^;t+TSo?Hu-V`<$G|Z@pgWR`)*CKm(#N`A!~MT-S^ubR;2Mu4^Czj;PojU`}?h- zr=5OhJt}5-2X(I4NlJx=;v<%?H>64Q-Uu!!bHc}t?kC!Z+8{P-gUmt7{;A<6+8Vi;MP5G4>b5g#R}&!5b5gpL)b@yC~lGK zBle^(mw{658H6c5=&mkF4jkE=VDkyCTMxJ@gArdiIsvO&I7D$bOdZO&YTR?Pzb81B z@uoIcR!)-US!!Cd|8SUH!ol0OcMm}9wO#z>UlddP*BKHoQ_XB@l5Cc@#k9@33u{MY zrCa5b{LddNlyx<+Pbwxb@*CazehR25=CFaY#v6>`7%j$6QVi)K!8Eq-aP_E{Hp*WQe@=NtxfBO`x`r z05CJ>T(uY`y*CPE`mM|$Og$A%ox`=&EH#m0Yv$IpzMqBG4O;i@>M>7vh7 zBENQ1%{IHRZWpo!7jkthd1dc48AwjAJqwbMQYCFy9QA6QoGi27mN0iZKa2X+o@S9b z>xi@Ca=68)cub_vJt*XWjkV)7;3vsTu&q#=jS)&}os{-B@4KeH-AWI^Y82v1IB-^E z;Nn*QzuWIp)aO+Ytux1pLl*cL&2(_~#;7elOl>F9=L_WF6EHUB2S4cK{1}qd)_0G% zQ|dgSe2~$TA2FdpL^~tu<4K%1aro&VtPdP4dp<1x&8S+fzbW1q?W@TYf!h~Qg+SKoNp*xKCQ&>?-aXkP;E(P0?gc>sMlVljCcDe$mLdMfm#3qk#) z1nT_u_`sC^i-^PnsW7%!=Mwa;cz=`--X{nHVt_{ZUU8JsFKK_~B=^0~guNrthVNx` z8MrB9W9b3P`#GYKNUgQxLl!yLc3)jM;i-~P~66KPJ@Wo4_5Wl?_K zH%e2k+Ivtgg3C$DGM#NgRdV=Njf*0;zYgD?*yfrDS#t;ElNf z+m58ulKGw2%RYyCG2k10^>)PcR$j!&cZ;2hYbmEQPWzGe-=K~o%h#}9UN=lBC8T|2 zw`CNuXS*I4@|(6!G>`h6ssGlg`5-9i4NhpT8lnE+tI^QXdwKq7X+mPT9XNCelZTXj z2h|u-A9uczU+g@36p--7-CQEIfRlds?%rdyUhVnqPAy9MA0L8C4HP<``@KQ$IzdVe4_nQ?kfNExL_!@&ZSIK&RQfVxGYY2o27f?Tlc)5E;~fW0Ie18A}JAl+ZiT5FW}tC>YQ;e5(U1|~(x zK)1f-fVFK?b|IyNh!MAy4JPd`zQ-C4NxuM@eY=9}ymlH@fYa){a7{-ZoZk97>b^Q_ zAShxc?D$!?v`@I%U7FcN&j$v^^FIO7C@HMl|Go}4x0k~&N)F+&0rBPj z<8fT@(-#FvdF-^Eqf8RVF4J91 zt1tB-t1}CxFr7s?JaOUkC#VLI0rLhJuKgRnqIenau{!x&QQ(H7{k58A-UU5>pF z2{|KL>_Dr-9!MB`f&mlvBtIo0q>3Wx>^@XT>Q96kulVDIk1~wQM_>wDu=z4$>;)w8 z`8pZ+b_eNIPB}C2bc$Hno9#Hu#q>OU42rIW?8u9glH=REj3~X+;mvjMufo=kb#I5s zK&Sc4ksKvSNh+y12`fkm@CmNXaB=AB#Gj3I-I6;^EgZBPZ(XLt`M#RT9?@@W-!x~lf*)n9V`s5uSwZ+Zfw+$foM316V@+@ zE?DmC~gFO&w%>#A<3q(0HRYZ- z%9!+?zDJb)V!e9t!&4S`bLjnwku%?)iy6s_t+T#OrI}jHt0~fIKzC7t6F$bW?ff)b@he#(#xNQ z!QujT{9(jZpJ~3qY)LX8Cy}NL`DI8sBfKM?v||QA>(Xn&s=)+P+1e3Or1-D84M-Ja z)8pkmXd{MyW9iev=$li1cmHR*O4@=>whZF zDiw8CJn;*OKx@jQt)>SSD#$=91sf9t#e-9U;LN={DG9+y6(#&c%COWSKGL7DQ*~tR zQ^b`iV>wiyuYS`-a#UkDU<y7K=!yMZZKm+dlL(P=!Rr&u zhc(rz_9?0kgguW`xPrLE10Yg%7 zxmcuBfyF>aPP|WO4-O{F7y};cQ9P}IpB;J%Due7M_xyzd{tPvY*VMy729|l-ut%>- zynVdn-T1oSD46&;{(0*A)qQObB$|=DR)XzG3W@IKn*(Uz2a@#Q#JWmLA8s|$=U9=S zBraj46yw%2bympf%B{qWv?_pq^4a?KB@Ip0SD$sjrcN%`*1L=f?g!TdG{JMJ~uIjzb2R{9X*+aY$x*KpjZ} zZb6P5pjdbc9{_c+-;6eW865>3igaXlFj}We^@(TBpfJ71>QLU>3>WYueXk^a0+s7V zmb}0#6q}%9^&4Hre6&Vm kOwdnz;G({@lIS*=O&4J$bzdxTCJDrVJn=A_A!19DwT? zz%u{|keHYlNOE%_At50JQIUaegocumoQjr)j*gawmX@B0gPERzjggj?g@=WWlM@UE z(=*@YyUWGL!3E~}&r67Irh-U8)MRATTnw}fT>sB<-3p*31B3!DfJAozw`hrgv_#jP z05AYRL~^6;e-!>-LUapAe4`~9IR)j-fSNmiTSP$MtsDJs0B%MH-#iBp(~{8fh$@lN zzj_0@>%kxvj>;nAeOl4Rs5`vJC;rwmf}DbhnT3_@-hKWD0uLo5rJg{fWu85Mp{$~+ zrmm-NU}$7)Vrp$;YiI8ObM*4|@%8f$fJeT2ANAqmr|5*l&q>KCUsBV)XXoVRS^!AP5M#sh{Ca0#CmRDBS*6|ygTl)uxN5?0pXXh9H;UWS6 z|0mXell>oDv^QM0Zp=aq`VSY;Ex(%+NJ~t@BT7oA^a}LGgZ{2qI2prJR8~bBIj^|x z9^+fjVG1TbiKTn{|3UjNvj01OU6NwdT~%vLr%pDwkl@pGe|69>sysb;x?;}CZ|;iVc6f$snu3%46-NhvQ|5o5 zda&T3q>o!NNSM>Cp9JCnt=PcfJqtZ6iW!PWMEV=m&yzy;x4x5~f=vy;B0z8~+bQMu zmHxY{OwDGYwOGP?ov-ODrxV|_uWF!I21-BX)N+E6FTaK!QO0~MA>O=i!iS1bsTXap zN-~R3ofiZ2G6rccYXUpo0!tm0aXakqp8tqT$fS?WTww)&)A$bFQ->-dF6O$@bJ!g+ie!%6YlOW7%=IW|XEojtn zfkBmRTI-LvX^y2Mp7u~8P;HB0u&0i%)z_0m#$cA{+pimh*n4}HUw(%lgHtR`V|Q(Z z5+(5#*tl)ome14*Z_qQb`pr$16;;V67pqE66v@dd@$X)t*Gm@aqhnlqnRK_T#P}Mv z=d}sVf)=yRJ%Py=v;;}UZ-XUkIYzz*pIq<=HI9PS4|+Y^7qkYL?6(&=X>RGuTac@f zR9*p$_Llf@5yJLfHsRNS1#-~Qn7Il~b;^rwtoY~P)O+xB);xqSG}dbzExDIFo;;n- zHY%pbC%XG^DRqzf7OL);W4Z1ca3@F&9bkH!Vxk|kX0pznrwTFLD-4Xy#q$3v+9zH=h%m#Nzm`mYdT0mG=$v_%ck#AG<6e zjYss8?O+O?nRn`2S=>W43<)i;#lN>UhPR$LwENb_S~nz%Kgu0@_kkE#`%YeT)8JEw zZbyluIRPBp_2HY@^MrR#E(zP5BeT7~XXo$85YF(|fTy|4!ZNcK_HpOVp=V`>(W{R_ z1pLJ=U)ADve8{H*){MW;Id4&lw&Gc~9G~sebbvZ={$ZBdCuF5eQ?svsF7Xhruwb>E z`VHRBClG!ejwF*d`9-Q2S82@&3tt1sTP}qKG$sH+6HlzuLradX0pa?-MzrQrZGLWo z)q0o&W#2GWTVjtR=t$N+X$9;%j2T{Y*(X%;S^OMUckpyi_TYtHqUmg#u4l?a(`3Yf z4baOET}Mwtc$}-6z}R+a+r!zG0lAtl*hs6%tH_z&`gvcw$_#VwW95!BR{&?MVa+}l z>Phtdy(2#{PIa=N^S6^7Kc+4w`UjyRw zo+psDq45e|uK||L`jXq{-T_Bbr901z7v@y#D*1B1@uj?b^aV3A$x4hub(@*+Cr0su zA`NkYU(Blcd_h6A&`;NZ?GcC_Kb;JXdH0p$Pme?U#03eu*Tvo(_9enfvq?>@`Qizn zS52ty!KFTqHpQ$6-6`$Sll>R%8G#GtmB7F1-kvwFe zvl|$?81I#Fs$JI&8FDJ9P2!J`UK#3TkXciO@bX6X{1|*qH>D)Y1?LEcUM!S2nA~w* zl1s-lha@%MU!LB0CfA|6a19{Kh|=1bDanM$S5|Okoj^&Q!7qf)PeLCsdd&M#Kk!Oryxn&vaJS%EFTXKhBVFJ_3y2Doyu*U zrIaA&AZ>VG6UT}ToYnO%#QABVP(mmwa;ezdZW?FYFnYzBxgqw#>Kb5=9<&zXneHsM zmUGYleK|pX$OR1PdbGfG76(twHDh4vIFuNm>5z26-s z3s!HDE!So5O{godj#NLcUHBp~)8WUFasSjB&5leTr_YlAj(tg)s?(TcFed9(8NB=E zN^3O8XmNoI=JBg45Pq*AbjMtM{tgb4Q)m#Y1`wtU$L)qE4SF(%h>abW5pBcYZVd*N zH>k&YQ1_>D_YQc*HYyFZvxq_k7&oBoa!+6-XD-J~=s>4A$luyDrj7^pimeyOYXG4N zZ3rfrEQ?w;@e$JU9{WQwc2!tm8-IUfQ3^86ij+;K*vSt2JhqeFu+Xzi7|qp^ncmF* zZhm}q`!Vn=!(i_k5OFj*Cj2L#Zl?QQ4rBoJ^p!8>Dc9uIq}-TvkijzNYa{VZ50=t< zuc$XL%M9@{Xi_cd;|rVwm}JA$F4ET}#r&6`cQ3uU@TflDU1ZFVhzIU|&EH*@KIpeMyN&P;(pavhwXO>g|D@%8ubazONo_geqNDG{>n=qSfv+%(>WQd`? zw_9vKA*T@hSZ;z!7m)jz*ZED=a#}yMePl%qrKz|_3JoUW@OB?G~gOPNQeGrYO zrWC-(7x(RQ*iU-3RLjrh;vdg9mD<1`i$eF}jVn#MzE5w7Sqg>! zMDbky=yq7s@@xr&028QT=)1#mFIFU@v1Szq%hdw>NgTxtU!N|DvOHQzx0VmSZ72@d zdOFm9C@nS%@0f_y{PTKml^`>EeCCp0xfhjaV?Mn5W2dtU(=N2)8e*0nmx=YTFtT<2 zc@3a)ts#Hx%BH=u1pJ%+4)84A_h>77q9zc66LzjnNHc6CsZI`?4NzX_OEP|L{pQe< zh?89JcoP}Yv9u6jKb)Z%?$cOrS0Ckv%Axynp5n`C)Q=I<3fMGbbWe{ynoqm8I7Je_$}{luw2tSMf<9{-m7iQhMTteM z7DWH{Mf;X5eq%S>mt*_aXZ|9TR(XL6BN_Zbu#q#EreL`U;#+bcotv(gnEbOLVWzQR z%Nq+$<9Qj0=c6NkqrysCw7iN3HDlu-bOx4+};q3A62!aKC=Yo_~!Gs?5;>oq`P87H+OU;C|ft%a#pyX zux_hZsCMx{Sno%CphC+FFh^*4ZKjEMArQLsH6E=j>tW0ppZD8#)N}s5;Nh?Vz=$9$ zn~LwHyP@xitynmFyk}zw~ssNaGnhn@M`GA7JI*koMZ8$c(Gr{3EWQvT^_3T z@MhEE&W(J*j4XO5EH_w^4~_=DKq84C?66mD=^)r&zx>)BO>(~ww?w~*9&aSSio0}FtJnC$j<^SR? zt^r~0P4af5ZWVZ`X|4LA%~PW%n$)P=E-7D)K{km25k}tC1b<*{KD?RvgSYV#&UAkL z8erV`tqsi{XlV1%~o4PICuv<|m)x*AeZDIdQV!T0mGvp48thOqU6ReP2L9AAVq&)0J9 zBVP5DXN}FY&AWy7&lWr>4eo@;E3vXV-vm&2tuov*G>LLHW3Nmm@1dukX{d$0nM0SL zyhgt!Q1yMs*XL-r^KEIdmVZZ`C{vz0fUi#rHbk|C0%nvw1GwQ~g5+Adb7=*+Kwa-QoFW8ku<1nAm)awvvEQobzks20^g#+C57Un-A_7uC- z#r(T@CEii1a^On<9FbuLFCObNK=IK4D4ljQsPK<`%R?P%PipP)ugR`o zR>s4G=12)`KPl0!G!w<+gJE5rswVCJay?bCv==!1K1w(35A>#DduNUun8 zzk_gPALsBQtn9bv!yq?-=S;8BhKjnHf#(WTR>R5(a%)HShpPn9krDI(Tw2l>%aOf9 zp%2A-_!7}m+3^sJwkbf2Tsjjeiny=~C!A>4umV;Pp&5tgzX3`^VjfBWj#K%60-g|Q zhTl!r`6FQriIJ%D5u(UR>wJl3c24q4M!8jwZADTwUPRn0b-3TAS~|E)T`6|%UjdO8 zIfs>9WSFuN#(_5danYfQ&HE8;#QUtF!# zolw)wY+~icup!dWhwE1=hZ_Vgc27fZGd!U_Yi(7CmW&e_?HR+LGAA8o$uu)@#B zuiheG52JaNfy?DN=0DEmFUO9Dw{0J15_fAUYc~sQ4tXG}a}po~?-5X$d>24CSYBSC zWkube_`sw0KFZiyUusWo>gd4b9mhT8?E|HRW zDI${D07MabuK^UJxK<{nMWS%JlM}|2;xGv(t{igumP;Yiak(i)4+N$ugki^5ZJoe7 z*C!mPe`dgG^l4%&dF>ol(wL%_?bGt|33vY|*mu?HADxv1bqc6|-!=LYW+Q*9HpSOf^70IIh&5LiN zsrLIhyPCY(msN9gdRDT5^X1jSx|^lzRSiDzFX|TarjitOOD3oRkQM%{4aalzI4-yy zdLavZ6t4fRWVHMOsf`;i@-_|n_EcP3chs~0Br3|lEU`iARcxPESm-pT{53$m%xg`4 zx)aMM&cTvn*vb!zkl{*g%iC@nj9ylZ?szmc?dvi8VNNcB9K^CSUM0bvGWuwKe%Ck6 zfV^>_y_c1nvo#DT8~`eiKFKSb=UDcd_>2qebx9f=0aj2Zlyp2G@FbqcZTQ!EZ^ftB zSw#|~CO|FQCd|xkRFh2yR|V_99rkhnw-0|7cK+CKIWx{6dRo7d!oHfyTKG=lo!P8s zRQ6GPU}ry$GMl*{!hx=ie7G6Rcjd$H*5Ohj%5YL3A~4QEaoaF6m4B9*yA*g!#HiU` z6*DB%_sjNc`79B!a>-cZnl$kVkRR2h#Ml}EENEw4>EP?p9%xin4p=SYl~?60V@;yA z^*B6e6gR787I-8~XV8m${w7{odQiy;Z_?x9$`?>FyFP6sZF3FCbJn)ByQ0xZ8$TuY zWqwuu*0_TD-7zn#-iC@B;g{p=f!ui6qgw)R1rwgU3Fjok_Jw@l#5ky9lVW>KS5qpn z6y)DO4%{J}NM2qBCLQyZiivASIJmkbtj~?#n0dGkLTo6NQaq;?d@p?;(lCY#a}hKdqQstV7Ae(EoG*Sa}C!_z>ygtQ-ij0$rJB2JS74@CgxGOQs2}d zf~mkkTr#7|UjsMjzQvAnEXRZA?%vJs2Tk= z966G27`pRri0^hA0!gh>ARnsmrNG@GS1^I{GhLGypfX<~Xh%Bn4EEkR_Z2W>_Ub!_j_ zY`oA(UY=jA_B=n}?YxIcQu75Vvd(UP5N=f4-9&eO{#rS>p~m+> zmSjDJohg%-bZu3`^;lk zs*ZlO+f*1Pl$H>jvCdh?Pkf^AYoSDMbOKvNLmy>J^cxAM*?LGRg}{{CKG(aoCmay_ z;hswQ7L-;yNzz-erru@ueyuO8)nDfCtG-~B{JN_m{l@os#xX8-YbrXE_`^0v3)xR0 zog+Imd?qGz;-h6c3eAeAjNiOSet!QBv0J>e*N~899NT(J8h}*d6j9MA(T6Q~nw3@>$<;p5W3o;C2H0da4R|j}6`TI&VVQ`!XyN>wECCqKzm$!3(2xg;d!zkeq-{ zDKN)+s>VxsO|+aD2F0;$pFXC}zE>DKhO^1l!H4Woix`t~_+q73jk3dY2Xc@PkLfaI z{g|*P|L(t5i|^udE>cIU5Pjaq6PX(Xl8G@TXuVp4omb>zp^B105%>S z7g{x4QfK=Rr<{tZhJkeH2T~bo6-j5qYcs4TR|9-@<3JyONz87V7HiQtxnHihBhSKU zdwTdI0=5)4)9?cV`ne2CjSk0^p~VyF%|H7dTQ~04MtjHIHa6Xs60Mo5naz7C8)L#a zrpju|7q7^R@QMHwD@gB$q-1ZFFPO<~N)#oUDiN(45FR0xd^46We9Gweu^%1>Npd#y zdR9(p^Rn=PU^EdFmG~%(^hPi|{7E!E_R0gs*sJEeaw&#@_Vg!F{c24=ErtFt8s8c; z3C36bONlfq64$iVh}WxWASR1lol z9*e8nT)PRh01f*`vL=XZDB3_-^#h~q;1X!`Q1i(fU%&QH(&5N0QdasQF+;=B^d z+XANFm6(~a84cnIwzl;Y8*apVxtOMsB6n4yCnOw+-3ob)k|VEQ zoXHm(3mrFir{?g&Tu|B&K(XN9L!Yc#zuITpVX z-4`?)^>JX<+*_XQaNmC>i?Q+GS|>u@+Ick_KEYqcGQU@bjH+Fs#L^7rv@y8fl;Gf1 zOt3LlJ?jY31oHDZAB%v}XN&T)QaHz3utbM%cOD>iCY7jES$jC()WK1r5-wsmy|eda z#&Z^$BstdrN3lWj^Y4`Iv5n|r3>jCG7Xmt;3kv=C;X`|$&zpW*lAFYjeyj$}X0~a` z=yD~_l;L*o6E_f+8lz;sUh*=wj@MtiA0bJs;& z^!%}#MAS&~T!QiN6B-36z63GQKn}1QBriBrE!)VmpRJqn8U;!}{D<02q~Lq|>ywCG!ie)PHcoj_*)+Vm-GkA-xif{7}no- z%Ix*?S2Lo2;p4ti+3W^t#`GFsr{c?#eDWwKuIAGrz)YZx4=F<}OYtYjXjw3W=FH+y zw+1=CnQVg;=U$9xdz~lC$h*B`!ATEeLetgNG{Ka!y|;awp9c{0erdJdFdDdW58f^u zle8HNp`Q8R8}MUScmA!?8Za#NPl#?!?@s=`2lW}ly)?DvF?@as-`%mU*e4ambC~85!1WaGN3dCNBH@n2RFw z{;W;aajB0+&K0Szt+4ab>@%!GM2m)mPb+oDanzRPnhg^MBUTA(E=gZ7i@b~qxqd}a zpm-Lf=WQ=E$v$Zc_G*e2()67bY}l<(U)QuWzw^RfH~o(ImC+oQ*BALbv7%Du?# zE$HW;H0|5rIoQ$e+i+hLalUBSM=Zg9N$!tlFJb9mC%$_ob6tTjhB7|HYQl)j;+wiw zE_|YyTU-B}ymzeK$odvbPC(LtSWlJ>krMCiQQxa>c+c5akGwL(QCYbNdd?+F@s(&D zc{){i+e(Yxc#)$vRT?~XI9JI|7xBGb@@VqtF~il%=DmqHpWA$sDjxaS%K2g<@q`_< z)qOOacIF_Z3rg?2vx0jZB+uE5({^vyu69+sOIbyssDPhA%8Z%Ffs4$}YW4QJ2pMi3 zOTDu*^zz*UOA*|k1&IzoEPSZn@uGw7Xz1Rwznj^+*Xld@b7ABC!a<#FYb>W2m79HY z2H%PW<=5PyD|xwwrX*QY!e^5v1p~1f?-=%&hYc{`o+S%j&I`m^8bxn!;G> zUSrq7Ueac5gKb~8Tfub8S673ohWpaGpg8RgY{-4HpC$X0n!~!5XDO!2xjhgb z4mZ>pxc%Gywn>_AbG}KCk)}$=;TP}X!tqA|A;z?7VMfG3b#7m{Iw{8W5{kW#qze}X zUXt505AEWvP)Eu!K0Ih+`&jFAJiGwlN=$!twU9EB)#&fC+uzQIRMQ)%O#=0Po0^`V zDOVXqL=V-+o6d^Kp;GTBo{j|1D^6Pyr(3~28gn&p8foV3T2BT6b|0MR%t(h8Jb|w4lA&*f<(!7GB%kIiWyTxYU@_lbAvr~7PNV6tO2hD# zg|_ugRpbSsrrsvgM3HES*CLeSa4X1Ock+;OTfi_7a_c-czaCLavtS{PgK*mfa!e#> z7pRdo`W9`)p7gR{T50Q~|2S<9TB|TWxqC9&%{e4h=kD7F(pZUCDU-9Y&SDwaursvz zs`x(2Q`OkS_~JmR_n5Z^ekSMGE_*P$Sn@u}Xy176o4AFOK$RHz1P4{oj+=#i09LEn ziN-OGqvaFbCXd0ZwM_m2->F(wAQ0`f0n&OA48G}J?KD($M+qU7p`&frfG+*`4Ga_M zZD1d7!z00~w3@OnBR_E7^J%+=Z12sWVu(}2nfk-iPRf_p07jT^=oFjJmSjnXY<2SP zMxUS6u8vj!2NwuZbb){O_?VsLP2+DL%D@FFMz9{M1!b=&m!pJVwa?aTy@hVtfQwGxYI zsY_k~^7{uJFc9Q1>h~%Yhlk~}Zx21f{~`ZC7vgS6*Wcqx!@5r-vg^;YZa!LU`zi0+ z*u7xH>UN)8Wx;eaHeF77Nt@Q+L-@I=UD1NW++0nLU`JP=P^T*uL z?>I@lM{T`oLLc>-56C=M8nV6k=hG(QVp7Y-Xq+Cs@H6`ej&;)nL_B9|nuOj)C z#b98g>&sXR7}Vk4&J{L;|IB6i^_(5f1Qy%$bNwzV_47) z%F$Ghcd=6(R;%$8VQ}P^4NIOEk+qmVdLHo~kCbfjU{Qj5Fv!bNfVx~2g z62IU4FSG>IHK$AhzzR+w)QuDNm(VNTiwG=>4RWo;SCRM+arpyf8eV>WNSFQ0mEbPXVCXR1YTkKAOnKqU#7M?eimd+#`-X<)l>z^WOAr2%wU-k*n(;5~yU%FO z+Mlt7!kl}iv^oZysKtxW$RH)omJ`289Thi6m*G5zAsu{J>mAiuEp4q}$JDdHV&k(A zyeh4QxsvyOb%gBazJ|px&qxGPTfY9U< z+_QoSNy+^9#VuTmkVwSWl0Q98i1maCZeJEg0q2jKcml`W=^Nc9SItW@P=x{WD=HtT#9R#F?)F z%%(q_4F65zdWK#V3eIOw7;Od3LEB#sKK1gWPW5uA=Xvu)kd@m$T|t$zfsmB%vUk~; z782=`X9^=2)U-=@=6Gc{x&oMov^9J#knbj1N>im-WWm|@QRxpC?THO0HP%F+JzxT%r&HXKw>~RSOV?2Rm zOb-l7dRk3byGeqLC&sP;Q+~Wu5>fm((a?SUW87cS@LZWwsDv3o^M#y-b#_k6owVZn z8P)cYEeOe%88m83JI}FF@e>a)WJ`cQ)U4lU1Gw*~g3#_-B7(f8Bf0zHCNse)rZA!P zG^Izcm>$6bmy*c|TQErmO zOWz(|VE@d-^BF#8M8vyyy1T2LxAa^*yOjD9cTNI1nPVYnYOon}12*aQTkox`Tm!%b z7Xut-=8X1R4&QtvB@O1dq%PJ*rF_c@XUu0h_j?&Cu}&3yR163@5tcf>#g+V#ykLvv z6`jgj+;XxStJF)rTogrm_-TVjHM+xrtNw%(=h}-`PpZe`4t&1{O-Na{hsIh>-+9L&p!X0wfA}7v(8@YU3+6^uuA~N3uQHB00##LP`$eV>>Qv3 z;Dhk+@Id%?H++120x$_7_^y!?6BCh8l2cJpl2cMr-)DS4P4keJl9G;vGICn$A1Yj~kLNW*qB@N{Ncf)o7l!QPSa0S9)18^yEK$JMx9)KAD zaPaTY{s;JfHym6L-W^IpqI<-52K5vGE)EEUdx!syz@2sQ-E#ns5}%4yP=SD2+Y-zM zqY(;A$RlJ|tnQ@M89U$*w(km5_&;7axW0ECM2UybDo8-3pbfT!QL_n!5z;6on)XDCN1+nJLfws+>J?^ zX6W3wRhB@vzwjmf_39F>2Y_cs{|TmjU}JVzAbF>^sVqk_9bKkVk|Jpg`JD4W)Ol&< zhZ}0)#sd87q8V20pHEp2U6(BCle0h1H$pFV!7C@NA>EJ2e;y*o8{Nn*d|w~p`RZ&L zL@m93Y9k$finmL@-48zbuKHH(tFbj}O5d8SaL)0C*Z2Ii<+oh4KaXuIh5Gs)yEH@D zC*jMkzYK3>im-s+<0YNq-akZmGxGsxtIX*KOV@KLt-(SJ(t$H>;C;)am^lR*YSiJ4 z)VNc+ekK7@r^=yle6Qf%fQC)<21)itIJj|ts>Y93#BPxL%F(fwMEAYS+il-B2aV*1 z4kOUrSWhevHxBPwF^?&3lpEc^0(O-`EwPfK3<<^E5)JrSl>OWh&3dHxzYNASMhB{f z6Btrh1ex3eAoay(uFMT9b~ojp_Aej>a)NNwr`5A-C>C(e#aNjO+Ady@V}ZFR!>%rS z<@1HTE!8!-@nxlWJe&P9hri|28OGdGhuzqR4nOEq+?Q4zkf7y%+6{kMn7)Dq3IZH1 z23o%lo?roGEU;ULI!s#!jI+PoVTIrRG1poP5C4M`r5t#M1s=8*IZQuDP199YeC|e{ z@a4F!s(*;QLdZd#ue;Z*nP0nKv{(K4B_sh05PykNXMyi7Vu2qz%!rzZ2n!r0g&$Ci zN6WQA6tT+_6`kk?YQ^zp!PQch64^KtB(4&%Ms?~3LfuEGI%Y-KR0?kcx%QjWdtZv4 zl1*dCHJRz(_Pt7gs^4K!xESWrtnna+ZeoF(nwDc_R8(Q`THPl+O0En~L9}BWTvxsP z&8*o^g`!!1F`pryq{P=8LBC|LY);Wzx;HsWvw6MXu(aagUp*oa2JkI=h_HE;y~L{>qo_| zM#clki7Z?Re;RI{MM(*6SXsa}m3@B6$bG zqh445I#jwwLwGyWb1usl!uQ4^j_u(35@vfhHnKKARfj(e@_|gV*xKCv0({|rwjqLB z@glwe;_in-Imycl|kamt`P zM1W!i=PI#4*-@n(m(ROa&8!hbeAUtxiq<6T1^rgrWn z1Qrlfr7mHy$xMaDU1G{+H)H~7-vvAUBz) zjmR9IOdl#(!zVa-&2HUcf4KN}p_f68v$Y^fR6EfuiUuyVFv^#tA!YlXN3Akdr7c^) z*O^sg^6CWhp~3^H-&R+j_;*ObwMKwfA8_k9Wi}aM9d^^1%UI`Y+~dMHr58c;?H(;8 zpwy`!YO6hIzYv$Etsz~$v=rCsIHyjTD6l{G|AmbCxB(#S64HZGuM(#C#W~=cRRdRM zjc<9$XqBw1=+F|YIpr`oG;OV#Gx#`eDCZi@ojlB%#2nN#$ZTHEte4nYU^sYE zwED{&=32pa?V+@;{LP)gbL|VfH5;biBFps8{Qg8ym3vLg5Rq-ibK+x&-+r9Qu$PBC zS0tUe25V>TC(W_YSn)+j5!tP=Acg)QOR*K{n0U@*$okM(1d7izcTtx+c>0MNIEAdk@#d(CB zQ*0HjwX@5SP3Ts)PoJbe#i;F;FC{BDr_Sl&t7UzHos8gncBz-yb2dcjQfYJyHn7GL z2xcD0CI3jOZ%tYF+NY9DP1fF_Ujs!}@T>?Nm2yEfw<1ZNS<-@U0vH;!)L|}g5qGvl z(RTtb9uQakP`_1=8~?6mSFE<8?vS4--uz=N4~+M?Pb{W&#NG44r$W=Eo42#Vmd)qa zqWGdDQua5mnuy2c7*gn$vP>^LqX?STs@VC;g)Li%+%G+Sb{lqKJ4KX1xVP)29&s%4 zMV~Pfoz%~0DEIMKp2QC7Y7#{()F-sYKGIgLHR_db3p32A zOBr(=?s?dn6Xg~piQnT?Z2sO@t9S%NBB2ako0mG>M~TKBT6jzL7n`)Oainl@Nk|s8 zxHL8PL(#MK;RpuU>m*4HH6Ga?JkhaB8me14+YZC@9op0F=8p$ohT?_>!v>2#&v79j zpZC)ngr#X_WjjMYckHTF6@5q1=;2O42iL%tqjJQ+F_C^90JT$ucZ`{bv7g zTIMESk_iiV1l~Lu$2?Ly_>pL|n!)V>U&3ucFXJXG|F%iWS%kE;sa2{T)TA8rB^QK0 zPi9FN#Y7YarAjVR3-1cG3XGk|sPvT>cBRKIH~X{(bjL2vQXxXDsxgUr%*olAu`TE2 z`wzM07@BqD&Ki720_j}F3`2zV1;FH^Hyr49 zPx~)#9$)t&EIs6Jc*@AD2lt>Rr%XyitE?0ifAz>slpq>FC)4|K z#C7dcJKfcYeTsvYI9act8)nrBINMkJTH0YHadMiUDS6L@LW1b%{I}xR@<9L2RNX>= zRi57m>+>qj7mik|PdHfLb7XpWs;^=$ZtWhGzq1_ZIO)I8iS`WZlVDbwjkD*s)z_l~ z!g@vXoii<-O0HDmGid(qH`TQrr}!dv)(@RvXpWZ}c{WIY+iUA6AvpxvQX{O6k8I8iajD+c1uIV5~EHJ9*tX~jH*FP|S+`vTe%4@06 z1}QEWvu;P{p&8<#Fak%KDBNG;ozLD7{c7Dwhh%b>x`l6~+edTvq2gf;4VNp;!Fe0k zLTSl{N%6v0@JuZW`avC8iGJF^K^f3Q#4*`IX&*(a@N{vT3KlSrvxnN+UD|jpWCnO) zxQ{&hIc|i*41XylM1fDPbiT7oB0(lMVrR@morV4Twa;CuTW5V-c%M-C$`++69{=C;)4V4^wL4uI%}rKyq8>Z zwpRb4_h#Tn+1#59sW&)}79MDAaCvUXdajA@%pFwhcS~S_cWy8HHLH8EK<>Sjp!;GD zQVCpO&p<~1SGVwcdtLxRWO1kfew`mAhr9A&ZL6B_=E38Mgq1dKl3CNCTr4mUvKb6t zu@t}4b;g#D1c04Pi{t5f+nq}>-hzW5B_AAF@Kw;_jpOEhPD;{tF9^}@U^%yD3ghPz zPu2Xj`$0?9^qW#xAlso$TjoHboRY%q`wWX`4w88T;}At9Bt#CqD`+cU2P&ogp$W=Q zH1-F}LbD_~w~1s4z$QPnQW!MQnJ3$t%;o&4f}v7gEaWD#xsz0J$QMxeT*IZ5 z6^`!`PPQ~WXGifHq^H125|pUI-sT|lJ^M4L@Qq-&iuQ%>OO_$0%vJfFj<0knF z*OI{dI+T>*!aKi7>HAq)~P1j2{C`f6NXY39qV!GmlMhb}h3 zc!A)!dNaX>RA48&Evq!BrhJp!asmrv>YjZ)dk&%`jp*!?5c$e0&OQ$_Y-veU!;4oK z*gDA{Bu$7wtj58yfIh!HWFU{jrEb-_LZX*7js%c7k=*^*HWu#B5+4uqY-3;dvGNBO6M$^ zmMt$Lo0<#7NPeWbqwv9B#%2Fvaw#;vhjNacag>dop$ZH|#i1@Iu61q_3YVWBon)!G zlc|fN*BaJt;gCfYx^do4$=;tNSfJxMtc?-R!SG*6H}4<09QKem8-JUbsG&1ck~?R9 zSm1=x^wr;&r&ZrZRS(VVqiVgJ9Ggal6^-uf%3pAuK&(0e8eGAUt`55M;paFUJ$?BAa>I@fNr?N z0<#>xJGM9i&@(!@ z^hg%FCScQ`Mz1-u>HdT+sBMb9 z5>;su*5b6Byi<~$QM)P!zcV-g%`m5j`+!x&dS-ly$|qsqztGrg0q?R~?rNSo{Txw8 z&iV63wePXNjCLMh&AcqeQbSeCm+_V+Q2tDH((fP_QObq*=rd}_?&@?8*(ZfR(Z}jj zO}%hbmeXTRTYWaH_<$;Pq2qC67^H_pdl&SFP4{6MXYDkK#)T0S(LY}8iZRPInWy7u z;hb3h!7cz^jIHZGd>=IiVmMIP#-);%y$g#wKK})4XU=OQ`Psm*kd+lwq{o1?pD|QESVaJ`u>HK|f(6*nP}2zw#>Y#gT$Lfk zbF(>h|Q=nJ_79bUl6mG%haH~A;v zZ7tFoDOrraP-m0VpZ#U)HUMQAkB_UFM#p&8ub|;r@Di5 z`}y4aN*z32PUmG$fd$ZCoA@Xr?5{qs5IB4PBa_ECeH$H=;`<=`st$fdSkNG4U&h|q z;w$r`jj82lSah~*$Yp#iVeiS}vgz=}aF1XY&O}kih!Bn4VV`F<9NJ%c5#w6x7x3Ca z?_Zi(Gf6(XyEw|1ary~Y)JY;p#1p=x+P@BoO1{MXZ%Mn0 zGwB5UX~>A^;#km#(+XGazs5S;W7%qSY;1^BD|Y}=uw?M7jegofnBFiJoQR}3T8#p$Qy+1d&iuI>Y0!)5dbfLXrlvPpPv6GmCqDb+z-g3ciKOq8 zA(NyX^vf{FP#>!h6^(S|Qf$^6(Dk<4ds0!>m5ujP3ctQitwdwWXZS47tv*p0 z(ZK0B&4Gig_gtj$Di=d`1f)y^qK54mJYV+0HdF=&`<|pq_DYTjY6mpGDcZ=9p=n+J zg2X(V3Q}3>c6wA;yeWRD2II^Z%POf0r_aClnzDk7CHA}-kgQW==>>M)U=VlW|g6OP@Wcc@})(8c|mb~uGdblG3jM1 zb*Br6*o$OO9#|KX_=hj8b0{9VZ`cppoHhFqR)VziUfmXQ{O$EK{7ck@bxbJXcqL1P zyRgb*OY5p}w~AA8vKaU+6*F$9eK;j-eNi*Rm{Z4qN06EPsA~WGmFL6CxcUt8ds`1N zv$mgIPK{mJBA@qUTYbJ)<~q)VG)bYxzO5O%%_1Wu~ zfBB^#kJYn*(~2DRi}%b?x3m{-uf~@Srb+``D`)pe6~|o77u53*>>MVHfucG)Vw9kE#Vp$>K`p{K8+?-Y@hR+e36v(9V7j$Qh5p?K}U}Vpid2XHfSeSo--S zTYID{uj@Zovy~=gX$2j5$$0@4tH=AX7=`i+$GZa%J_aqLQzDV;%9`I|7ELj~npzu0br#u^km{f;D$Pua2RF#InkG+RyvJv8nu&eqUPDzuJpk8GI3) zDtk-%?`4A!UN{d$lLL`CE;nb~%X^xncPsEvuKb&VH6+r_y0FKtaiYSF)P7K&Gh$?E?&boFQz!f4RmPf7g!j+(008?WYY3o9JdjCQv0~r zcT3%NDEI!YSl(L_G2dg;l@Z&IYa9ddb~wqf#W{8wWBiubMe}JB(-mL;l#E>B3}guN_E<`Ole^%toe)1;v9QO0Sy2Qh0c)q|YrbK5l!nrG&Wo`0_D zfk;hf`Gqrab;F5^M%$Np4{mEq9i@I5+R8rON2;>M-Z!mX@=(V?7}$NRmz9FQhY;4i z*ZPsVJ}F9G*Ol}{MtaZp=_zA{3cIYsQSO?obaxy3lsA%8tkSK>p_;_(CUwf`62Et* zKKd1ZD&BKHFd`w5uzZfVWRGtt`>(>tAe{o?y8zK!mamP3QQvvRUo z9$R%{`=`9W(x|?0QK_$o%NY`0=!Ui;X{RwVs1v6cs`Vk_=>2>Y|34MC4Xv~5wgO#$Uirj9 zbYho_g2)caj@Tv9+xKYViaMuwyosOEF`XdEn=_nmZRD#s{tx3fIA>)~_dIC!u7?Qm z7>`Y9j^BzUMoes8B{%1nO~|qN~85c=n=*#UQg#Iw$4aOLZPO3x=!Xpnf# zAX(Ph2WzKXHip+e(~66J0t{{Md>=J3EU*z`N?V>aJot&|zaf6nAZHm|v4MADdHmGW zB9hs(LA#3jrmc_owNpEVI)j=wXJe08!ap0Mh;Rl@m^Cl^ z%5fn2Pw|11RVA!7xzFcjr7W8ufV_2t?734>Qk~dblZH{exu|m{P9`EyPv!_=yDWlI z=6_oHQ?|MDXG%r>a=F0@(v#QYU2r#@Ww^>VRBEp-lpx8L_7e`SMM8Lpwz=+sPnS9~ zXh&H+j5~ebomDJeAcAfXsnP!8G)7?KRXtbSeDM+Ozvb<3Rl2di`UKpAz8`9SIIEde z;jATqn14UIKcdX}`Ij^trEt=DHBI#0@v6M7(|UQU=&u^F%#3q5n3!rH|CV47Rarb{ zE^&}m{DDub5!d-~qEX3P(s&|91Y<&6U!%g(`%r(+*W0~^7>;&L1Jk&v7H1@1S@ic} zn)C((ETB|L8XZGeca6TWTt z#VK}#{2MIUlwb8w)(g8gX%Y3y_-v8}L$XOs5yp-=im}i$6ZZocX_#e6V!)57)xcE) zV$dgqKMlK^f%9SV+;&it!}9K%ZgJ!{lg)=wxtQp$fd=HRL?ikM1@v@tS${g^>KC@! zxTjsU7T{g&ZB|g9@#le*&Y-LU0fYU_hi63xe(cPDu0EX2Vu5893>%mQZubTfa@LPN zVcs|{ElXP|bCP&;W5nzH0N%KQ;Xl;W9ToXBhj;L!i3CM;SdIE?N$djh(T!RnPm5JC z*$`Q<9X08;Nf(dClvt}KBMXK+Xypgdm5|9DzGd3ZzAwT3F0rt=@wkg;ic6%APv5(x zYCyYcOH)?9>Bfkqq|WYM;K7oUGis*?j-&UjI1-%T_sIY;DiEq+*np+Ucp}>TAAX-w zCx^moNIF0g0;P% z78dv<<3#f?>&o;+dT{G}y#Kxnq3S@EwjyI;7W$s+plSYB@eI>GGj$PHiR7~(1H=_P?o8JLX_$#KW;_i-G z0pw2dHMH@v@|K#J5^k*m!vaJM3=IM|735Wfw*w}M-(|WA18_R=&MV@~roE-#klXtT zZXfkCp(#BBN@6wGZM74d3D)UG#>`iO*r1#AcN>C8Ngn7jIf(08=pbpDa)9(@&FpUR zP~9;W2qmU}2(hi7&4pS&lw>KoRQWPDb`hyYA5LV`OCM zglOQLNan0e?Vx>R0>`m<4ZhC3ptAEei$FsZxbvH;Ws!Nr^d2;%1*H-}Fop%FcJ|>5 z1LPr&Q@G!*1#jCF{RVvQ-llj_+q37|va8{jEzQpNGg^^8T7~ro-Jm3nhEOB2?a9{z zv4+aOI}c_YHW3 z0UrI~G7UcNLikQq>eB}^mtD-%7uhag-6k_f-=?}K`eUXVk+ zwAEs*6)I@I6Q7Bvv6=KD=S|lvosIVDMB>)2OKZx%mZn?8!&9QIi{F%aGqY#HBSri; zLMnBY?6Ww^;8W{R(REhr0Q_F{ll+F>N z(~a3yF_om^&l}kulbgg#El)!_OK|ott%MeO({HEA`fBKJRYdq_V-WNCx(uRB?VFHl?^Hk(FKEV2?MB88Ja<(=_>}quTho!#Z?EH_v1XhH~Kwk5?+p$ z;l5d8Ov;)8Aa`L62tEy>XdJAXc<=UeK~_pjhOB9QSV!U??Rz&JQoRGeKZOtI{x~$# z!1TWERhAxqbx6+`Ip?=rVJQ2sa#TS0@>$;CAI_dgBYyU^XLOxA2!pAl)X5h2rn8S^tb<0TPx*IFT5i$4%U{5qR|!@j z#zd=R65!LN5ZcXIlbWs6Z-*_etnvdF^TqZ{XvEDEQ0FYyjMv1E`?%v1O#f=cwjNnW vByCDCT0jKkd%bB-FCGfzD+;7;L4tBb=F_yketc4;@SH{q=l`;Uv48#t#IGs* literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/ne2005.jpg b/runtime_data/keypoints/us/ne2005.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08964bc98bb5bc96e62c5143e5bb6cf461dec0e1 GIT binary patch literal 11204 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/nh1999.jpg b/runtime_data/keypoints/us/nh1999.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e5beb58c3f6a1afe7fa9f77cb81ee9589673eff7 GIT binary patch literal 10588 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 T>X(`eFptSBd}S+gGWtINv0AFP literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/nj1993.jpg b/runtime_data/keypoints/us/nj1993.jpg new file mode 100644 index 0000000000000000000000000000000000000000..646b67780c18db52d14657261845a880a9b7e73f GIT binary patch literal 8021 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/nv2001.jpg b/runtime_data/keypoints/us/nv2001.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1fc5671360499c3d58c654d5580062420d757ead GIT binary patch literal 8876 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 ZVQm`;L{;(lvaGG0=T^vQ(P-Sv{x4v!qFMj| literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/ny2010.jpg b/runtime_data/keypoints/us/ny2010.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2e123d713172056de432e087d39322811d970db4 GIT binary patch literal 8844 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 If-%$o1M2~9)&Kwi literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/oh2004.jpg b/runtime_data/keypoints/us/oh2004.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e6f5ef0c9c7e7daed6eead281eb2a60a102ebd04 GIT binary patch literal 9627 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/ok2009.jpg b/runtime_data/keypoints/us/ok2009.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c8c49eb1fed15a731cedad44303e20d895fb70b6 GIT binary patch literal 8673 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

Licw~1?$zG%GTeF9=4vuT=6MFEJ0F zmbJMF+1f!Q!O&{OkLB54lu*!H58^YgoIE}|Ne9xtCb#U2wscqQLHJMwYobq{jqWX& z7xn(OXWbD|1=YqF9Yd9bV!FV5o)Vlyx%C*Y50!Xxlk{GB_^;on$k;$>CFfbq^?09L zx8jGQIUUea^(GtpI>qaypCpLCg1KGXz58 z0cN1(43qauuBUO3(8A=oj`o(H5WzvIC z*4JGl{s*s(*;y!S6K5WWuC|pjFFYyPd|gb@t{FV$squKtpZb1H<7T(zEpWD36B=TI zB9^4R3_Q->H77G>nnv1>Z|?tWJF(g@qi8tC`6u%u1UU}mrdfWymTv8&L)x#|KZrby z?+cpU{hse=k=Zor)|?H)jOVV+aS_MTaAK!6DUjlsnJ!*sA zWP8tVcSnrk)u|`$p!kevcUoEJ%D0EwK~;HO-hyF?Oi~K{#h}`IulzrtBhptQ+#=KW zlPSvc6Y2-}O8fQjFN5TA)v%`jO zzINX(lP(Jn($#dBa|e6Yq7ic%uB+H@`g+jMIGzWXj+nV`kSI+ zBN0)(uzfw^;sZC}Rcs~?JmBtVX<6Caq=)E5$Md5nMJOJZQ+e_QJJ8vqR*B^z{wkqo zNclG|lvv7HlnqgX_PVC*6T>Y1hZ(ZIx(3sNTPlGasyz6}jdxuI33uYwOa91)m(#-)QTuV^Rf+769z zizbiWB@QLx*0f=1d&P8MfJO=o*9=lPZFK8?EFwsMFRP_HPb2N=GCoa)HmyM(xhZq9 zVy44|@w}kkw>ITi(Chn7HfP{wY-U>BT1AxB+bZAPq++yKY$`&2UQ^9Tv7BsJU-`%S z{FKdL^?2Gn1nLiZe889Z*}m@w0=KzLqt%c{ANyaiT;Bc`Sz&*Gnd>7x-ngSt^Ii%c z4Bp>s=}yH5ha+%nILv+tP2EYYj+DO_k6~D;2My8&$He-+q*zN`@;E>8?ZbQO5*;u) z*0UAGK7SXStOE)T$2kNMeR8dHENb5{@m%!~ISd~UCs=Adf1z{Z%8vu{V!KbWYnP|L zXi@HrHP`tvdjt~xq#CZ>kN{=2=lXdEb9w(x$-o7%A!Lm z&;OGCDU|17g8k6F5!|(n-=-&TT9#R(Zy6CUwEiTq$ABbsiNe^en5u??5sLl%SC4$G z`Y!jao;}=Naw(EFSw(D)xbjIeSSzA49ax`^#xk0fQqe2MjnRlIn-CWWe_32y) zNrsDC9{gU6KFdcV6ew-%6T+-#CE^(O+On@mnqq}Im{F`?-f+LXZ|7y82%07(WFbvEW1oJp996_Xp%*VQsGrog0@64^2_z zA#gN;?KZz95rJRAQM^)Vk~5GM89E@%I5CqDHIUVRDNc?A9vwH){510DV*~Vo?Sk#? zOUA;R-}eO_Cng}F=CuB(luoGO{^zG44D=1e6cFxz0kz%xJ_y*$jXbVGY+r45hH$JOO{ak@TX3uHBp*6iakjJ3Ts^S-;J>Jy`w)zgTXCwV-dLLKevQ8aKI+@*S^D9GEo zWzrkLv{-V{3PIZHJ7|!$97vhU`5RShdcKJ6wBzVruQv=W$rEnwEPFcqH3bHqzh2X( z6ca3L?9II9tCmy+!v{0V3`^TM5qX8FER08CIn?iT;J>uH3uo@KtA3vEaOV!{q|CW7 zqnnjxS6Psb0QN9aXy8+*-$m%tvqjb-E39rglbCr+EH&r$@<)wrD>^Wr)hyI&)~8_c zSPSf%iX3Z@BUsz*=9&~J665+(y%=S%?#KPPoLYw2WpD&m)a|>Vczh5g8wyTx)!4uj ziU7Oe+l(-ac!MUpqo3+m_jnX7YoexIOWo#Y^=fk{sfXX9;N=jB?qO{KVdA z;SMXuN>`PhC&A6wWMyv;j)X{S6{;Y{#E^N(Ft+Tk@|$6e3u?9UUN+ zME&^d4n8}0QSYbm|HQ{92->%;*x!GJ)3nMYI!>RAE;ukICAemL=lyzHt2jG>8`&`> z(4Miqk1;4;>K68#cRAg@oE@IpLlR+jZYG^b(k!yFG3g0Y5XOf1r>6RcSI{T_7iFBz ACjbBd literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/ri1996.jpg b/runtime_data/keypoints/us/ri1996.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2bba7bd1e8b167e4b1f6beb2375333da693f0cb2 GIT binary patch literal 6534 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 I6Z7N00P(M^-2eap literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/sc2008.jpg b/runtime_data/keypoints/us/sc2008.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1857c2e221557bd17e6d1bf1be7e00b607e82be1 GIT binary patch literal 10514 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 gSN>LCSASHnG4xtDty+{vDDS~uDmEJ)i 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 Wmyc8LW#8LC(ETXz3$Z|vfBpw}h1&T5 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/tx2009.jpg b/runtime_data/keypoints/us/tx2009.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bfce33d22b7cd1d08770c7fb86a7044ecc30ddac GIT binary patch literal 10330 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/ut2009.jpg b/runtime_data/keypoints/us/ut2009.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6f70a9def4278fa166b1ae0145baae0d7df9044f GIT binary patch literal 9448 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 literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/vt1985.jpg b/runtime_data/keypoints/us/vt1985.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dfb053d4120efd0660a5bc166075320803b2f803 GIT binary patch literal 8085 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 NwC^v@w4iB|{|i-Q_Ba3l literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/wa1998.jpg b/runtime_data/keypoints/us/wa1998.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b2769e21acf5d49c3d29c64c3d171b7abafa8645 GIT binary patch literal 9813 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 e@dcI5MOKqx2+PehkqF#R82JsCa# literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/wv1995.jpg b/runtime_data/keypoints/us/wv1995.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f974ab30ce68f5b6f5ca8262a6ab5e6c42b6e465 GIT binary patch literal 7659 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 F{vWXCCe8o= literal 0 HcmV?d00001 diff --git a/runtime_data/keypoints/us/wy2000.jpg b/runtime_data/keypoints/us/wy2000.jpg new file mode 100644 index 0000000000000000000000000000000000000000..abaae0a2124b3ed06a208d8788a3ac287c79aefd GIT binary patch literal 12087 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 literal 0 HcmV?d00001 diff --git a/runtime_data/ocr/tessdata/lus.traineddata b/runtime_data/ocr/tessdata/lus.traineddata new file mode 100644 index 0000000000000000000000000000000000000000..2ab3d9094ea37819b1138f420d55bb60975d6d02 GIT binary patch literal 319970 zcmeFa4SW>Uz5hR(%tD~r&Lq1)ywYwUAW+*TJP6+UFbgxuM$yi|Op=AfhL{ZrN^e?| z016r~!4Sn8yt|kLV!1JdWOn~^iL|VKFa6Kxxo+|Kq(5jcZ0e5h}=~3e*8spnB#`!3T`Six*m zz>Wv{04vZ@0XrTT1FXP61?+f`I=~82Q2{$1qz$lwG*rNj2k8T>ARQI3`hr|McOXY53v3L2(sY=YlIpbhd)wV!4qp0F2D?5x882$a@;O=$$|(Hcb%8GbkseCo zr^M?Xi`UNc_%RvH#|AsC;uj9PK0Etz+x(RDdf&XzdiI^3>Y;(AqrtA1sv?n5jZnFoP!-yE|&ImbXxI`G5kNqZ#P_TTKt05{({y1{nL71{;#(`JDep#S;crIrV{xXYT& zA<*X-u5Ujh!OLFno3xP6TMwG%D;gxTUS2Y}d6m+^nHh2MWb1)bK6XBWWJCY7*UNhO zi-kjE?tt@}DVV8>WmM*~kScf!C?hEqmRX|ANhoKDyqOLprt^S}#xs5-W3$sr z^PQWTFo@o1$xIj0$vISaDWx+jHmIqCIA;OnU`k!9L}tb6TnX{JU1mC57C+u6TvuEC zX478m`ua)iY3a3pG`Ovz_u>Ks1^?e0eoM$R!aixT{5 zs>@8xMfLKW2y(lfFP1jX7vKji^EoQ3yQa^yj6__&JH* zNV~oJb6*nVXG~;s+ll4$Zl4xE2X40fD@=ZFrpeD){4BwLP7*KiQ+~z#Si;%&)ov%^ zA$}^8H?)%4olD1dZ6}JXe4*4a9gSD9{eM*hanN9=%}+@Sr}2G~aqEJ`kHpKD&d;W{ zDx?{|gu&*%P|B#E#`noyep)M950 zHVdZho03%~ZrUCCBzOdi+jW-TS`U|%f}JAG>f!{ElNfgdWjt++7f265U_9IXxXT~x z`OK3Ust1)}5G2WL7s-o*CY3djGG5Z6DJM%>jF}?Kcu8dAU7X0KWBjOxeFz{KKXS6U zQ}w22K<$5aeb47N-;)H%@+FkSubN!(s>wN7HM#Ay$GZfJU%>l;0sL(6Seei-?eR8; z_44P2J-wZFJqarKCA5=j`jYr8WC=L3z=e#@0zYzc7Wk17uH2K+1 zfN~s-#E@tPXRn9HD%%`Z@pHqT-cGxoKKyJw;%Plfz^{rd(asT!Ulk)oj0b!HXTc| zX1o4NP0V^pL;w0b)?79>d-o;c!3!5i)_@q_oA~F~IrQg0rz~0T?*)=EV*CZu8vS*| zIlNe4|MJ(t@XuxZ|FQ-8Z)%rX{zqvnlUJ~Ine$U+rvw6B5#iW-HYKZk;rz1J?(^t4 z=1)L^5FPGA(8*itFFJY&0>^c?eb;*M@ zEjb9>IM$4KJ+aY!TXoVgZ%T3>$N>G9)I z0-hpaCV2a#$GSdczTi^%X*%o2<`&Qf5`GGB?b}Q_oYUHpImh--PUj;wbA!{(y2mDpm zV2Py;ei{6_#h>#;so`S$NXBnw&N|smWj&3jR8)oMToJs}?l&Y5KiaN0?S4hH9bInw z({_h>sNL=M&jP!IpGtCWltU$!!P{gv@l&Gxz49aAOtLbWQYT)oT#lsivWul8yNKVx zMd3(6eCMUbeL4J?<-G03S`Qp67_Vyer&5PV%xCG~=BfRWt_$$<7C&$C^TdzoIO2DJ zpUf@s_f6s;ZW`<~OP~Ln>cS7Te{X_ar82z-bey1dDVJHwttP+Rh4v->DkRfPUDFQm9k4oBs+2=zraAOG}uYJ5~@U!KV##GAFuO=On&~5 z$!~FbEp9ve@VhL2v_Io#sQA@C8NtAQ+1!1T_^C|%G?bdj2YELyd%b+nRewR=uTW@?&Y?>ZOh+yfl`cv4a0}SD_e=#mhVsY9jS!Ih~vQjooe%WBW z0XdCjk(@T1cHRFUDE?;Qo^N>EOxunU4a0G}~@-ydv=%g&N}z6wV$- z{M7E?ryLAk1?@kG68uhd0PL9T(GuY+J&2uWbXrX`m+hul~2DiCCou7=; z_F2?`jN8tOF78Wz!REbKe(4MRXNCW4fj%ujTRaUHOB`z0&_CJsidQa{)@)x%^kt=g zU;IaPXh4MZK}tYX9kZ;Qev$OD@q{hWuP->z*A5c+1a6d-qd70xgUzk%^n3rhGon*uAV z@F(y*j!H+imd*0cx}TLqM^-xic>HJ0(Su9G|303JY!hFrGfKoE?Xv?$hh(JG&kndL z3)7HZLn$;2UU%qned=1iG(wgSCm-PV_sI=|z#jpA=VkI^ooQg#zkQPRM?yKv4gaHv z&h745+W(}8&J%%Ib-!30oqRa(`X7I>dRTH}!yA7LR9yAYkxiX-#>&{$M>bU(9ZCMx zjZgZGj%5Gp#g9`ODT_mXUXRdI8rRnFrIE7lMDnls{gJY4=VkL-+}7^HGk&@{#dvI| ze3t2d_=4f!PZEc09|nJQi#X(PzR@smwU%^vVMjw?wUN}gt+OFeQN8lWQ=JXQ%Ieie zo@z2Wld4xYZq7G4ldD%Res3W%blc*Ng@XE$FYPr(LlBgX&`V~^*?c6YQ`Q^1g z@t^(a<+l7oR6!*CRRkARFctnP&2i=SYv8ZuIEKJq8HQg|;IEK|0)D@LPIcLlkCCb1 z|G3HDiEP}w#82x{_+Am#Cv;muM}YBXy`gb(L`hWE?buEtB)&I?Qu#8sADbT@l6@aX zISTDh+>_w1BDpIzfWH#_?|>g^@*A2GB!0u>k4-iCk&K_Rar0t-EckQg7aHy)L&-97 z;16Bsm!+D=oh$n#{0824Z2xAWb=m$$dC{RiKYSkJkL04Vz+VFX%CQ`It08R@B$n-& zvY{x%zaR95Qog>0IG49{wOYBJWqElzOZXqdtr%YBe;h9OA9x2^Zg1js`Ab#>c zc3vL!`RhdWnCpW6oOqddy;uaL<$svJ!fSFDuHY5TD}Si7?ZUSOoQxmaJ2L7+mL|Vk zT_)?(47Y5gNa{2YyRemmA6t(^{AZJTr?J6Kt9WTa%rqb+J0)L|>@@22%xC2J#TI=`gF68}ia!JhkcwPgCA5mvgWUkC=l zPPS|sGthzfX|TKrSL&sbz_%;Jukr1v#AQU)-^Dni>hGgm9aVp?l?&N;9sCCPRpx(O zBP*Hz5w2}Bb6i_Q1hrXwrI{Y9p9ozpUE*L*Xs*B6=s1m#%(ALDsD42$!|@q;_;KvC z;6MC$DscslY-0RPjhh(%)+#I0H~z%$m#eF7{wy&T*YEC;8zKg2C7k%ZcOnN9x3%MiKWkHEAN{xWj2vj51lvJdrlm=#Y(tpKSWB zzf-Pm+O_G2{%W~;D&6RT84O!DCa^)O%?TOh_3~bbyCuWZ|I*I?u30RNn-MgOrp%Id+pGW2z za?>{OHy~?t&D{`^19QM%7v#o#yTIZvh+zEVvm%(kidXIJ=Zm$!%Y%AK(9Q`nViZNRDjP3Z?{D8E9xBsf zB;2>a7T24s^tJptm)~3u$z|u)g7D9Ue1$DQTRaT|O>^$A?)T?$nH@kmbi_4JZsI;C z?djk=tdpTH#!lXe7$^2gk5x`UQa@Enn$Ck0*(peu%(L@Mm#m9LA_s4xi-xEwPDK}U ztt>t#Rq&tZIr>qIe4`Q-*g!Y5*|pGb*)Hr!kbXP8Xc!j^;*NI`m&^3>b0Gd>dTUEwR3~` zU!yjZMGXBS{F1e%lq%Mr68(g=HL~c~F8)mWgAW;ZPu=lHMn`T)JbwJ?*3pA26XAba zMh|X_d+q6>($U{)zq4r3r=@8>s=u>n?E59Dw=bT*#NFxjuUc&6yVDi_s>b=@vGoqmvI8 z{r>oWy?RY@)w6FL&wcs34|SdFe!6JY4-R&ne17Mr6+b@M`1F#7_bX~1UAl7_^C?S1 z3*G5SwXbg3^u*Z4haY|A#G==yesYlbU!Qv7An`xek6*R6AEKZ^V!wOSfikh1KkyIi zl({#vP;SO`S-~}l+H0xsKV!4C|5+doIgy{*@Qbapk`FI={p2$rr6yNB_r|IG_iG;N zI@P^rS@qioyH0K0bEf9)gN=KZgg>eQ|H)Y%*H7mP%y7yW9`<};o5dYTdnn&+Gxy-0FI&QnWW1*N~bX zs-5Ya>@0~=ao?Nd#CeCEu@cHyzS~SX_|-e(*?P2D{-@3IKkdE#=Mdvhf24(R zX0*04?yFnYQyv&m&iGfl;7>3pT+U?TcXE>_6A?Zh2((YeQxO9j-6zd=fTlDDJE>&9 z2h{%9U%3nTpAyUelvw^JX!)PuM;8CyBjNn2niUzRPw!h+_4Z3wf3lbIz=*~?8V~+y zhM^lNT-G$)>f;*1B{2RdE`fnY^{&T2`Ds1C&(;Mg(K=D~`XBf!jFZZJEcGrQ^FR8B zRn)HD^u3KHzsY@;zy5B$81HR#}qp zMiDp81ZZIX3aOL7g8$J!3=)@O@mFM+{0FV}KRCkVuh6v{X*$0&??!6>ktK;YQgv6C zB)Ta_l*F-gmBcX!0bh~Oc=K)(`quyl#*cX=#rz&yTBT4;LhudK{{)Z0pXi2B^}=1$ z&NUgIK0w>e+ZlhkpXG@9g={?YC*W7bk&M4G`r9}X!v91Mqa0@@7vB~gK}SeWTeNvS zBGKs-28sPXZ|gT3mF*R5ea*&Zdk-t)W@D3ERpO|Q5m~F|u{``{fc#J9;SX|&Kl8|D zlmEk0Cja`ACV%?IbNRExk(w@$|M@0)pFaL)FjoS9(q{ReHp@Rq4CB)H!Dn4>^+`n} zUYDLULDVJ3lDk*lFJ?%Ng{L3K=Mnf%Z(3l~g8!3EcZHkK{-;kqux}Y%T+uPx<;2+?B22(PB)qDZ}a~b@iDme^XFPa$a6TiP(uVwu2oBVq${yod882_pKs^`|h|C|iz z#d8Zp_aER-zz=`L_*ssB2kj32`2PN_m*09HI6Jxi1LM|ge$Vs>cctWAFgba+J18ml zga3!8xnK8yf5H~-x7&L8AGi7EHkkZBe%j=}{b`dw<#>M89`HMlhxA2h1)>xD*9_Zk z`X9HQ8{h}P@2X^;;H>|#`TNtJUj{$=5;1WT%ZiT$NO@bvgXf1^I`8JNe_B>Bc=fxa8j> zv+KE}wfG(y9#et8(y>3X#D|{@|D%h%py2vR8ig-o2jK`fN%BNS!v9Pvq2#zx$Oxne zZhX7ho9q~a@;P~aJ6~*!1NZwmG#AXVFL_wsh) z;3o4ZV*#@H`DcCFOV%PUH?7$&G0x?8fnVpQhg=pnD#4irzSO^XUgC1TvdQR^;vbps z3$8eAnKWE#np2Ho6lwLZwXIBVAaX}@l#qu(OP|1f@0 zWG6RqVzd;8i6|5MC5;!Y>2L~UoecG>@DhtAKb=TvSiS)NGt0`^oQIWX!}|#EKeJfU zbO|_dox-`fLhQ(sv0vwqKhbz6^Cud8->qx8TCNg67HjjBqTYo6(Xy3!e(qEJ}_K{D#w+jtt-`raGm5RzpL>s_-rKvv%&>xoXd?X70RoL-c>u)vd}V|6}sw z2z$Bw#7l#OpML%)uE6v^aRuam%14jRO1S6v(=C&(sZ5v+|1)WLn`hS3#rr3Hr@f$f z(R2G3{iwd6c4hTE4ugYzY&dNurs;a6%`Z`ri1AzX2c{Ljf#zx{yx&&iX&-Sndk z*MPqQ{O$|!W3!^c{!pXD`mr)yccIMuNuOl?M{)>RW6=JF=-%$m*8Y$$x_1QbtULbv z=(PHxO~;=;Gc~R1*>%Soj{NZPuD?EU{MoLX-|jm3!pY~l9)SP3w_#g(nE8`hh4~YI zrb7PYiSX8k-+JZ5eXpPT^x&KSjy(C7gOC4c!*?I6`_GD5efU+Y{UxvxKhjIwR#I3=$# zO#jod(eyuS{*(D11KSV0r$Yz76WhNGX4(yqUZlfHQKvHak<*><|}i0xCS!2eMHpIrDK$B@H;)P`rCpKbc9Gik|HFT6qg4|VRYB`$%3H{!MX867efsZ(=+vD^?oF z2!$6?(e9CQlsXms@FoRQCtLn!3j7ZT{}a$Q+{KaqVg9OpNyobZ@>ef(9t}5!S9blq z+v4BKvg+9-to_rUSpxo!l=M*E%ygqYCA~U*rcWvL>6yCEr^qr=mXT_#DoG0YA03Iq zfGXqK4~K#N{8Wy>`Vc?mXYiLW|C29C zSp0qb5A!GI`=4>(paI>b7=Jo~@mnePOE1dAZzj*y!;6nieA*MQq+xw2W$>dpSpOxY zZu%>)PX0=V|9Sq1#edr3-)r&T&G>bV`5*X`8<>BP5^ul)xT~c^*8U@;DJ)&m7#fYQ zjNCY$Q@SXz53v4OX!QPx&sQ9a0BI*CM_EFE7yAufB5v|;7^GDxEcNab)Bk*g_9y>i zwtq#2+5QJJ%=SNMwLkpN_h>r5I`4a?KS}%^)pK=aqCq*LZ466S+Zbv{PH1z}20`ng zBEauriJL#|1TSqw?9PL}NwZn}*e)4A#;A(vm(-Wd<>=i8@f*R9))JR-==r16&J|r} z{!ZLAv&sMI zzP|hi)8S9TRnMXQiNDAX|0C%_{KyT_-Kz7dmmZo>oi2x#++BTtZH8RC@btQ6nVE8J=IPC0 zejIj8lEq-e~ z*mLjBZZ}cZ_7equ+#h84!LPB@B5=Q%7SN4hp>*7jcI3V9cCPpR_5LmI-QT_5cZ~c` zrtN=fiGSx^Y&-ezuMe>8X3JmiXKC`2KOz6K$nrneTxI#6@qdoIp#MQXAja?NZT}1N z_iq0dKb{i6_>KDPh)~d%z2M0F;T=`J;`>jZyzkWM){{M3-h1KbmhxlpKW8j|b=34% zJJ&TF#diMTUw?d@w)-uAy`5#=#+2jXM$7-?SpLV;$Nxl-KM~y<;0v++xGIqn(&|5k z{(Kw_rVo(tIYDKKMq!ZHZmnDm^A0pac1plap`tt1X=|xjde$u+A=k&>YPlEsB zE$?l67yRK5f4|kJV*V{`976k_SoApaKZlFpf4b5RY?uQ7(>3})R=oFk)~-pW|0$f5 zmPP(2Vf3&p%U@9v{}=T?7v;a_I^zGhu`Yk-u9i)2Ha6rpG?s^Jg6I#Qj{aA{P3eZN zBj@Wp{7;k~&^7Wu(ikZl$AUqEOL|&LVE*Un5=xG%Lz4d?Z$)SCcCzNAt(}_HPOF7K zH_qdq7f0=1)0jO5-1FbW_vYC-rKuqtD(%GmfOP1DzoPpa72H*d{R)XEead*en`yrB z90YhJ#`AeOAZ~lb`t$3W2+mm{?01ZpQ*ma2`@=7q1WDX>Qt|5@*nNsP9)S~{%1)d6 z>t*>0mPT774VRkc)MtNRuJ^I0|ML9L`QFI`JX+5A$#b~$<6Jm<;xP>`@sb%>Njs|5;?1{>MxeJ}8&`5B#=4{s;V*>rc>Ny@Tz4RDt=QI8~Ua zJtm8Zl>Z@*vT`EEM{Bd>iJ~gRXtU)+kyoc0!{s7_m#%~VsZUQ|vjlg{SFKN9laK2# z+*eW;ZU|S0E9&d^?c7tn=V3G9f9jsA3s=LR5WmrvU$U5x>_hG1DOs;mGXF#V1pSuR z816AyVzKszfO|?7{LdfjM_-raxu@>Q`bncy&+NL~`bp8qrx&$PijfQEFZyBoqBI%) z=ldlMb29$2%s zJdD51YX4>q#}$0~+9{c3oS)+32HP1+r+`%9BPb=LD&qRf>vU!162B2BNCkhsF{Us8 zf3ioQC<}&}2_ounXPj=n>X5L@!I(e$=fj9Gh!Te7R@xwn5zvWN1 zZQHsT{^!)OQ)hbMe@-1cd8P;cXYYVpA!B@!1ISVjkuE(I|YiE_x98gaXMV$R=6~j&+<9hETI24y@F#m7&wGDvPtWOnZ{~e^ zmzmGN|1^d#z+YRtb@QvQ?AX^)-L&uJN0FhXePy?A2#3}*h4EKFjS$-3>y*Wy2>%1i zP$~*uC#*@(Ob2o~${OV$e3gq_F(}2AyOBRx4vt7ExQG0YBfS9r2ls!CB>(fy zT@L22{_?_X=C7VPlLmg%U*)~Oj<|oH_vvOcE&lYskU#0HPVdNv|LI(n_l7?+BX`xR zoIBuua#PCc@4#nygQC1M2mYtQ>ukfuqlUcBQcV{1GDVR~WYOit^HoaF`@@U!@}THi zfecxh#xljVXuGICDBq?|1^*20O6GrNa93}iS|pVW=dJ+1RPtRe3jQjj+^-pjm#pnD zmKjFl?i%txYxCZ(F}dFlo4Ij8n8xcH1K93Eh91iCr5iy-ug>xLj8b?XN?8tO=E!nL zmMh8sge1Pwfd9E5zqKCAOGVdotWT(2bj`qeq_IqK-3R^|@=W!I;3t3e!$Q{o=bdZv z$(vlkaugTU?w?~Q0nKRlzk$61{Izc|&b;n7Ozuu=JwkVw{Kk!RvV{Mq@w%WZyn*AY zD#>!clwxy88uL;Ht`k*jjgP!SoR8(TzC!%J8`om;k1I3zT_(R@$c6t=^&wpG zWE=;jD5-yrkH>i8$0aOo>X9>elfN8E+fS*T;5~;wX!2)Piu!bTypffRQ_T!A?qfuBP=OPL{- zB}uXDlD_0&@Z%^P{5j*-g4>us(ehq^Kcx2md#Bm{N2}O&G;U*tx&2uFM?J^?IIfm{ zO1#J-mLsJ6dMdjle~_h|9>giQBFR28d0A&=Tx?kBI-3qf;-gS@$arU6j8jU1mjJJn z#O~MPrB}d@L4OPUxc^8$#`ulA=NW%Z-WkUKX5RbN#Q)~Fjqry!e)jgCnP>H%i5rP# zcU)aLk35NEL}kEEjmoY{!*JkxXxy5G?S#Yrshi2Ol<|BX7smHV)heqz)(7p+&b!e5 zX1^a^SKK&{K|hq24KurK|I@+t%SMa8(c*8k`13Zx-}drY#?}EQN3V(%sf$>*Z{J-hLf6DX^Zt^F0nf+(3`B$s|&-H(f9B%gi zfxl`7e`O52U+db+7?Zzr7Wjx?v$*G3nZ+`NH}f_b=i9iB1;5DdGt;$bBUKKN{{eRZ z{^zdlpQr!r{w*K8(EUU@{Lh#0*0}WqzE=&yQ21iJy;^iJ!Ol@eK!0{CJKT z zbryJR3Y6f-u8qWXIG%dXu5+!#BQQl3Hy5Ctus^zu`nn0=$qk=Ac{8{lfd9Gw)Zb6{ z!2i5(l<~t~neBgswg2J!f@9+wlPYqS==V%MopwMSFWr-Hdh`JyUaHI5HF^bZ?5e9Q z96cTWr>-&q-y56W$C>4D!k-vUeAE)R+&955OZaLL{4z%=_ojq>6J^X|B`&Eh!haLE zANY9JyUTVyfBLC68{cheXlmS6llS{3$i`ZK-sVP@@%IIJ-UENK1pX(%aB$<{e-d!L z=9229glGr!tC{dWtPJl1=g0hyl?;Zit{GIH#6LfGIG=w!ICE=u{ozJ%|J$2-$e$b7 z0sli;PidIvGa+_TW4L7aEAl_+DFA=vz>yc-4RD^0loIH?9lZUMUT5pUazNbniuLE$ z*TkBfl;C!upA`VV@2iA%<%>a_F^mPaL@SpCql6a|1`wIqob7{C( zny>!f`+YGkHvThTyueEJUk&DSP|4-ym*sz0Jy`ywC4G3C{)g5BU+2RA;65bPh0jU1 zWN3IkjW|pSSXtCBX}mcdeLwLf_#bLFdfu>y;}ZL1oK>cz*W;||5^)aVbUURfKQW}asl&&oZ7X^4}Ecu_jj5Q05A@DyL zYcg@WNp1I*I{1_7ZAbUk?c1}bdfzEK8|$9K_yu8IqXQiAS6co=Wh``Y#!192T|$yE z1i$1E@VUnPiK<OY2k*(`NSW7 zZ&xw#hd)5(1afiyPyFQFI;%~8)!Dh_DEXg?H+Jj`)i&*`c*4w;cN`AG2OUkC(@yQ* zp%|qao|IkYR$8PM%~N*O@)q?FWm$_>rFm9fRpv7eY4Hck(ncIumolPZua^GM z&r?S1T*~>9AH8ky&S_lB^i>%Ji>65Xhszn;=jTfMzblV85ttwP)xGd1Me{pCOAMpF zFzskVV{UF?j>WI1nf!Vx@#o_6+VUrQX1Wnla%zydWlGK+x<9>`o_~#Ra-LO`!*ha$ z{;s058^{ht0l%S_8A?k@ixFQ|v%ID45M^1*YgI=4%9=7?=!g-2pr%ZI{D?8)K=mru zN#gHXt)3+Q*3|{XKchTj`^`mMa|}GtHMv~#Xl}#_Bh65X!H+bGzz^PrCh#NQ+l}_O zGRH_|<8>o|0}q{Ypf8`HE4shXhbKtG|Cnj}pSIX^S?-Ydw#*D9{8i>7jKBPma>ifY zZe<5@WzBM52oe7x;eQ6h|J+wRedisj`kR=%8$_OC$uBOj_Lld-+g^Sxb}mD;)fCuI7c zIerHh!8x1Le#EkU?vJSdj}h$UUu*F{eqnyP{|NhlK>lZqoB1DHe_ScuQ%D~CN~w4; zj)xILq}`3kNT+oA5Kk$0wHWc>_pL8CT;TVCpI=_FJgOz#;8!4nX7YNy!A~dub6Df2 z<9cW0Va6$CZe&@K1bzqjAHJPs`?}jr{wLrM;aYQk-eL0pxWnSFw%R|#+R%)uJbW2;?oq`3#dAZ)r*T-Qx{We~oCZMyGPm-r$JH&~XG9I7R=tq|1 zWZp-8#Y-vZZ<+Uh>Bl;V%gEVk@h>#_3-d#){c{Y%;*X`tm_Gsk_cladMdu~gzB7xw zk7L|Bsb;pNk^gawYs29|<;2+1RNZj#MvBkK!*+z{BNc&Lu38mUXeapPs@1$2OV?1h z@#+w|+)T&wk$6<(Qk5Z$oBGjM{>M!CEAl`3M;*lVIsC=BbX^@X$p5(U4MhBn@DJpD zZuoO#8YTP>CHbG9VX@q`ZBvWDFN`cLE(E`x;wvJ4aQE_0v(_V?@uTA<`pc028L30o zrK{RVNH2ASZp0EVGS?5EvLvUoArn3&Kc{;m{Ey+!`RGshTtYt2+CQmq8EgL}tNl}R zbZYeVv*nCXA8ol-v_?c0|Bxr(@p@O&kBl;`f>5`GT;C;XGWZ+!~?Q=Pf@*!wuI zsrJ{u!1!+oTKqwC`$^Sx<7mU~`NaQjsAzsJ^FN8#T*vzVh~gB}{}}K;SHb_3=2gag zAN(y_Dr3IqRk=g8@~m;1i?3Sc)5dA6{eMac{sLqvWjrPL@m!=J`5!lZNW%Y&t-hfD z`M~r)doBO7_bKA9&8*+o(SYMW@z>$_pA^h5qXfTFXY%9uKa3yG|4F=N><;Qb1An!{ z^gmYY_d2xFX#AZOuUjh>4JX>oAwyZRgs-L4$=l-m*9CvYBjXvr z3+qAr9Ou~|fnCN-3Abyg?a`*c3a0kqzlm}0eRtWO=TCq9=9YJ#4{vQeRJ)-RXf4P%Ab*VX~2W+fAFMrlk?o{<*~W?r}b9Sn`;jJ(;EF%2DJ~5p9|#|Uhd_3 zAA5S+*L+a`yPiu;4!fYe!QhJ=#qvLf<$nyz|FDGrIVa(Ntm)u?aATwW?4W_H17pB1 z)*c>?izBWJq}^U-qN67MNQwae!(qP`q;#BPp#M)LCA<#K+7|>;$^RJl1=5ry&z#MB z*?65U>X`p2aG?JWzC=_3Pb`$5=Y8zg=^#e)K!f9X|6akJB{-%~76^Dg5i&u*pU(6O zqKEEZo6a(ZGJ)rY^Mf!Q&kckBab;x~nf@W}^h!#6-gRsVZ^L~h$BypZM*gSgl$rZZ zHHIz!V?0A^3jeU(;;*E30}v9jna8J_nbhNXn3s3BE|7z~`1F8RbE-Ihqv`^xF~z_W z2a42@Q?yiL%oO2j_$&k7>T1hhxnjs;CGz@kV;CBdmxsBBNum6Vj!g1D@DG{Pf2NcC z$+pe#KhQqR|FDGr*=+ltKhg3GN&FwiFLAGNaplnwlJFXr*b+0C$J3OM$-LI?9@CPV zBqQDX)9|?$bQ@)qI!-v5|8dLkKlQopK*&hR-;jD89^8Vz50N=hTWh#kK4!QBIT_K~ zTHQUbP><1G*WEvz4F1=_AFmHH*2*To=}*q`qyG>1E&s#veE);Li{^m;!P8tmo8LF< z&@lJ@b+@EvH;y&R)>Twx=Z%$>id9wFPyl~tBz?HCHpc)zxC;%FztG~Z4+Ze~=l6%IL+RE2%uHlsxcUkBpBi|dCoKPi zWd5h-tp7=}{g2U7awt1))m1B6+q&*aeEB;ET3=g#PvR@zIRJl>J?1xaR{9=4GAs7L zx{8dQd++o=^!9Jp?_4^`m;BpTkL;W_v1R(|t`qka&D$U2ga0Yk_K%j+cNmKd?;>~y zW2#{klYanz;9~p+zQ=_2?7*pEW{zL)FyMWV=H|u_r zacb|KnZ$o;>7<$9|76eNi7hk0|3DG9KZf}$j`+!&c;SDTKVkf7#@bxsPcvRGruJu9 z6py`SZ3E+nPhj3|1+P28UM~tjM(2)AI#|4t7Sga_1pD5OSzfwSD)-#LjG#~hJ^t8`<);7^VB zC4s*@JZ@FRiq_Ug_T2FD!Cw;p4X+&hCHzl#Tm|^a|KR!FuFQ7$A6-#Lwo9s4aH=Ci zjJG+}?-jt`oOUbA_Ox3Kk@4SZ@~0)4{Am_H{MPHmsn~wN|86SUAN@W%QvJf%JWhXY z82HJTh*wJB_fQgldGt_;_{(DkNyLwY{{cVz3GtVOSIi{-7eD^x6~zC`kAE3O{42sM zdF+X<>F_81!K%Iw6DjM@m1;@G}2%pKfNcKHWq9XLpd&Ej{0!6leOM)G_3L zYU}f3aCkKGu;_fqIr^T}iJl6;F4_+*7g;)Y6r zrWN2GT7tt3<6ozk{Oc5xzvfqrKWE*qO#ZpQGWq9(&hlgXhX9MHk{y(mEKUCXn!~PA8LR7EsMX_ z;`dwp>7^FGOzRau{|DmF&q9}A@ZY#$)*zf|J8n47|IDeQ@#EU=-Kc2ff9|a}+~j|j z^x^k)oBZY72N{2P^`m@czTqx4ZsXgQ#l*|XTfz z=>H=dxM_^;Uo(imyPddyeFRmBz0EWPhW;@^QZ`SZtvpZt$e zO8#M_9*~U;JSRyG$l}%He*zL^D)T6MC(8i%{XheM2lNl|cMzvinAt)6N}-?jgMg8P zhc&Lv4dAMc+CLB|KtBy#jGwY&dIY!MAxtLz7KeWQk>AhdS{_)j2P(EcZK^2fJ;AO6SK&-ktNVE$*JZZQ9|1pcR%{7>W2 zaBWR`ctKuwQzou|{dwK*oBqn;*DZdT@niiMKN9>QtN`)1i!S2V!9V51j9~%rk3aIe ztDbFj1b?#r_c!gF$#p%o{=FMFr4xVdv{d-3SA2nMbH`Ep|K*0C-9YVs0@?Bw_~YlH zuO{Qy52a?Z{VbT8S)V(hdTn~{Pmaw0F1XSE=hOpc{~yc$EbTV?FD?Cu`5!<0Cu@Iw zVTVTTFXxw062G>#7|*J~-~S2~8}q2YOmLz*uHNe~;7`WXPj?5(MPq!`f5hgNJA!kn z9v`3UeP5yU&#foj!5tszvkkvh)8IOK*ImDM6Q(3 zhe$sZgQFb=@+S=oO#j2U;eUEwF#XTRroUR+Y4LAm{>LA`1)e(>-yhuFuDOa0FE>uo zgkn*H|It)R_#br|46)R=6?K9Trc%Rl{h>W1pY^oMzJeNNgBDU!cp32()t3?FlDN@o8n+{}vwU?_=@@SCGAxEOzXU;ZZ3{~R^B^J@LM`S2)t zwWj|`ukK*pC*9x9lAqa(>qLn|n*IcTnSfoo*`9)yLzi+iTr4jxFC;eK#a}0X)fR6jNQ78_Uxa%ytH^~ZyM~Rb^4kHlgsApmtUCSTt8v?47>xMH{SL&Ulf47 z+4yy-dG7Kq-TzE6`vn z{~U!s!RI>l|FN=hGyD|>-SStG_zT7_acj6y<)b1bVGTF2brSrKMhX9;yT`WVCi!NK zFow0vO-i3N&PXjYGfBBcN!P|1iZ?|MHpJ%oqJ4RlYp39IQ#SddU6bLjByY43@3|sc z!k-lNc=#9zU$P{`%fn?wH45Y9R9Pu8a>$>gg#5u?e**rko8Jh3(sMuS|6`}w|EC80 ze`4(qe{~UlwjRm+yu-x*knhPOsj)5V9?p1j?|gsR&tF;pTW3Zc~>1e zd~*CPKfcaTV)V)C$cef)x-vd%WVEh`Ydl%57+id=Nr=~pKVsj-uY-<35aMqdv80zO86QC*WbSspC^N?ltNFAtK- zD(U(W+h0d2`5$l9SsFWOs7%ex&Jt4uvPI z3X}iox#`s&@;^N{{kG?T5B_Ar!SKVro#cON4#NL{|1I)AHxvISr=Ds9#jk269p zTRUqExMxui|I?~(J1qK=g5^0|H;U!cgTD4VLSFpcK&5R? z8l|)Csx$>XE?n?IvVyy2Hw;RX%W(H;0rFSW{)1*y`(NjA&XUVspFLJ^Yl_klbCr+x6O zkN!ppJspTO-I)I2}*qK6aUf56YtdVn9-@oh;KfAVybKUpFEgoHr`n;{8vkRs-}xF9_Dgt{ey9of6aIhe*Y?Ich!ox@|a=hzqB&0DrT76 zb%?dUZ~c+{tMu^lnKMt^@J+Rc{Lc;Ftn6X_XK1D6e<}l(|0%i^{^yqoQsOt@e}0+3 z#eD<*$Kp@2_@~Pv_$!lE$X;sy@WcP)BjdhRQVf2Hi!C94C2{V`bzWJN zz+bT(e+6C;vQ~kA7Wh+^gIh>Q;E7)Vzf9{DuVef1imvfFnH~7rN54KN^L00#-%1&a z9A9swQjTf&Hy~5Sv@iRU-w^!n(q)GYx67~imTj^4!JT38cUk;hP2_(NyH;8}Aj{IC>zeAp~sX5;c<-SPo;&8__J$ArZTC^Xu(ziC zZ@W_6uDun+KVEM{`|s_!(c-_c>+h`n$9IJv#f`Ukh!Xt6Ya`PFrKxwQ|2r~yo-BS_ zeI}Nr9)R~r2^2B^a~tlfB7b!o`~vwaKl4{|S*p$#`rC2b(Yb^XCDi^dk6WVlcj37( z)c#|3;Jzqo|4*_fr0#cA{p%<9+_#_wdF14Me{XeE-QV@z{XH|e)8v0X@C&EkE`M$@ z@xS6*a<|F<=~D22gzX2}b?mZFTMf@KZ-}Klt(6@Vjc;Wd-~$@GIcw{P^qYvjssgBz!&y=>HA=XzSRn|fLuUH{hg-c8{D5d1fOkS=`qcKN;VKdvp{ zzjo6&YX47fIDP};M>78S{~Xt~xq|gy>Pqu7{}ZzOPqpcPh@1JJW4Qn8gYYr*8xJ#o zWpuz_9ie`~{`j-}6624RB*yPTj>38fqSrgxGk zeW&V=H|}}F(KQ#|Y7hL?&ykzc15A= z!%1}e6#B?WF}TN!_+#+5F0|NGIeOJFeHK!z9+u9sYEF9U_*7ZIUu{6@Le(qGU%B9~ z@aY8pGLHODhGF`jF3bOP_47ZBpU%Q9{{!y5AD`Ltd=L8n9BT@14Iit~J3G+sIH9b? z_Je_Y1pb7ckOP0hE`P|M&}9%^$44XSS}cagr4htajO@U5g~~_EPNc?*R>J?_Itfpl zlgn5NeG)zTc@_PyocP+sEtpS%yb246EnW!&sC4&xXt!xB~D#)Sr~4j(3#G@P$a??&U|71NJAxL&N2! zL$&;m#YKLKakjJMWUMMaxX#ZKYdjLo!O0~Y8@_h`Lwr~=mdmm~uSxX@-W4^P6nMK!;xcgML76^q_Ys=>NmzSXtOF&G9rH=Z(kof_9esEWgcjDarGA zvO86R#5y2ZJ)e=ZrV{hK&mn>RRZ zGR~X?!{qm%E?RB^+dUjCiOYjBC2-D3Y)-5U4G#a~#C^q-IKJt8x*K=o;*SqUv4f;w zro(eKX*Inlo5}dyhSd)zWHfG`0MGM&c<;v#?Acs>`jnlf|7nDOD5fI`_!qeiLj?bw zoaujNagp3a_#cfDzQ+&;G5_O6n*Qf(n*PTL|Km%=-t6$@`BEq2dj?;ge8&_oj^g9E zD-&@W1s~5{iS=+e;u*h0>j7>%OB^1?A0mDXA>v~Xot&WcsO zdE;^w$EvFJo37uS5;61cs>i47lp}mg^&e09nH(Wc6Ru25lp^GK;YvK#%HbFzMB&fc zNsc&Il!KS0hVQm_t;kO@7$gGFD>g1X274!Eo1&<&#lUDUtPcRhUZee zs}Fs6a>)lR(T~h*+*I`5{+O1T>Hk(#C&%m`G2(kaohLvAbdHBk(`qk1dBkA^%ebe{yo^KJq6Yp1h$4{-^cy zsr(OGB3owe+Fbnl{>bu~`cuV^$&sb$`odxj{CZAis#qev7$^XLGx!tG?)&u>V?0AN zzBD*v3~e`ZM>6=~dzk-$?`go_!6WaJ8$kZa(6=pu|9Kt$XA%4lvU@)K&+Ejml$X{p z|5M&x-E;HmJr9+4_>`IBRjP zWf=TVN#=VBdbT^;uKb4(IuX<{9cnEhrr-lGp=F$-I4zy~f&Y=!!oBJOU z_=mL(B7R9U`8n`w-2T}Pw7(Y_f%f-urT-lXemt1uHSjOcy@A)jzbDV+KLzizAMbH8{--{?!q?i;^Meh09*kiNOpJr6~d!v7pb{~zi<)6V?KnlVaI zl4N|ce6;e@L6Y$)xPLmRdDAm5P2v;2(fs)Q8Ht={a9i4))A6}Z{0?0d#0|*OXz&jr zZigp+qh^C|PeBdKmtP`p*!5aPRNN$nYl{ z_r5eqhX2{P8{CQCXg;~{rHMY`|F;>5J^}o5r@PC+pOog%tHH0NI!f1qAML&WaqzSD zcbNQ-5&vrreNE&D^$vKSHQ?XYp|Ji}EYTnCS$~p89_EyGSDSgJGbv6V=9IhPubBTi zvUx&IQ-$x$k)E5gc0U~TNn_8=?;sDu|GZ=QpEmfPwxz!tQv&|~UHZ~Q@;`sce`(_X z$K1OBMp4~;|FdBh5(M0Zosfvs1QLo=E4HAm);bF_VHXpa4Ra>DAwg(&7l=2g38iDI#Df=Rgbc^*(ikN}CdhMP@VVo7cLf(44V7O!0L|NhQQ0I6-YectE! zk0xJcv)O#+?Ck8E&;HIC@y0779|iV|_oQ9b`b7Su@!8*RWAY~w|J@ejn%8^q=Ge`o*%f8> z=xSMje%;br%11R{JA)`)4$dI|Y93f8b9j$^ZP} zD%1ZQX9`lU78Uj^{~BIzsIL*ZuW-eE&h3{LhRfew^b``_p+M&N~7P;W2fq1L2mVbHC2H&p!X- z-d^s1ir#HO?K}A^-hc1m{y}oVKPU$L3Hreb+MoKF#P6W(;QZuIL@72yauffNA71-} z0{$aEyluV5cIq1Vo-^6bQ+M)AJN5We4;5wz zFzrbA2i?Z`?fOv8pTvF7(At5>u*Zp9&2y@M_4RW-jw6pz!e9N;laKl(@>gE?pCJ5? z7yjpNRw`gR{P7zSqUfLq;E&4EW&Ah`Z)rKn>wk#*g8zBX@;^P~PdGnqxdeWJ`ycWr z)TO!1|0p!}V3=rEFpD1i52jv&&w>Bp3IAiIs*wMI_p!2F35Zyi+Z7LTwp=D8!mrGB zyNO>|n;;XvuonIY3~KC>_TO{@=iw*o`?$IPS@b63{wMq<)9Xmvf`3itMSkip5Rwz9cz6PIBoymln<&t~Ya0 zJ#)cRxxUE%px)ResW$R%f8&4p_<8azz_)*r#6`4W zb3VxWlZyH0FZ_LL_5XR``M&6XWWZd;_iqFIJ-_qMZvX#@|52>*U#cX*>uB&n68w*q z#h0adJKYa;e7HYd^n&{uv|?XcFE8BkrFKuu=!^Slc#`PoLP@^{;qo_$o4JTV$kt9} z`5&8A|6}L=Cv4c8$p3`%?AyrySUHa0uCtpkU`%JC?`DW27yQF$JYE{UNCuC`7rpE$ zi7QQ}Ob}>?zBCEI8uqWT}Hxn;+@x4*bqI`^5GXFqFdxdpeAKLJ1P$N44lC!=w=N=M@l zh4-QPkrK>w4wZ1bhIzfE0VpdMVUq_4F2zidCvkh$eQ%xJR{m=Cy6-(FQnpnka* zu)+WQ{5$KCY!&5IA3uC1$ywEb{8)6p+L7J6^FLj_w!?|{Y&H1{;}(C7!+}qPg+Unp zs}MgjIvM=H?s~!pe}Zug;NR5ee;(>R_XznPGs#~yWA2DPe{~uEP>VlVbYeTQZ#2Y( z;LisC^WgW_zm@&*_wKre{ceBN$KQMLO16Jr75vX&--?cokC&VoJc{_2d^~8B$$xe3 ztDN68sazMq?{c>@VQ`JZrR;VJgMD}wmr6XNlO^JoxIc;0I1Kn7)cu(L=jrB0xc_;& zc~=+w&)XRP#o&Mbg!`eS+=s-kD6;YMq25rgq8Wc2<}EQ4A2J)caM8Nt>=5`L1b-d) z9|nI9_#fVNWq9Adj*lPi9sC3E!~Z1R|7unFpC5cX5$SpF!Jnthc(pd=o{~9wZn^F% zWThpy}%iWGHaX|iK*ji^T_C+mg8J;fmZ1OBWaybpu_p)@T2W3(*FBL8EY zT$I(xe#4CJu+fI({EE?n^Y>guH%{geKkk*6!}(i`oKU^zxd)9vpze2`j~=)S^E~Y@ zKXdo4!Nh;&?%u)o@9T(e?y66^|8REn=?~-f`(qwu)`vc6M$E4lmiYAC7}Zr_-rshn z*}*>r{OHH)Q^0SJ+w|8b57(r)%DN|y&{7+axkI$n)2b2o4XzHHR`WjRf8JK}s1Atz zn*7gj?yvAXC{ge?hjWz}xF1|(C~@Qx+ScSzO zxA?7WWG%-0Fs9VN^9x6*%*Xi{W8my$_AzlJaW{G+Iq)dS3)D!?N2)Y*fju(#W4Dy6 zN!9kbZhvmIvpOlyRxoL|L&JVwFlDzB`;XwK_6NV;CkojfPu5{JnL?Rjp$+sFC+ zlWd&dKg9-qH~1649|wQG;f}_*$$!$~KV$Kq*}3-?_^ZfA_w2oi>X)`6seb8`!dJe9 z_KzL`zaRXlA2WhspHkj_GH<~mT`Z5C%sY*Y>Mh36FeCrNIzz~9t3s?~3jEK~(QM%W z_#bdD9MJmgPRsv1viC-D{D9WHA5W4*C;Sii?M}|`>2LCTIKNNuOeX%Ape!Z+E*mRF z`=5WuJ*O1)OTF)q|Dh!R!}-&B{n8rLUs3(i8meCcf1SnO)y(-lUB{;^SmYBEW5=hQ zUc?}|o3oCCKQ=GU4#Pim?)X*6v*-@_82pb1{>1b@SCaqvw&{QNlK*MVyK7Qrxye88 zHIv`}GUh-XTx0Vuw{kMBtL&||>67|NS{(kz-7i(^>2mwr!;<88x>N{>i}@kTa(d8P zdm*0X%tr3)%5oMsX#OID`E{n;-HGLFC-aJNU56&#{!DDpn~mdh8P)%=BXi+T z?*3J%YSDE0LwHEyhd+V8x-#%-Q-WXj#+=%up_0xHNt!zu?*pjkbQ^Z^KgkNam?48DC+ezlY&AAASWLW$Lum3Uq zkHMlwA^9JOrjg`-kg2hC-2Y6B)f?o0GGq04nePJ+MQtkn^i1 zzpB=%jbrqF72et>KD~?etDV~M>}Q+%Cp1m#c>c`0SJ<~r>wy2c68Y+%ip~!ak>{^H zY5J?N5Odq6!avadlO=9+oy>cnBui{U?pTy1Hn^z$G5%dL{Yee740S+%0IwRlRzJq` zyX1eC2a0bY|FdjOa2)&zQ+v>^Nb*;51ruG%+%{Qdf@6ve^YSyBW3ruLB$3mVptADP z(3q5d6@?u?J@@00{SM#R@qf?n8j^7Q&W^vHe{Z1u?42F(KSLxW{MC>Q%U?~f{MCdV zmEd3J3I>;nJ6(Tt5&t&vk1ozHKI0;OVD$MDHIDO>aJX(+AT-Y8F1h9G^N;i%xcBVs z8~$9hsj{Vs^Ly*!;K%a|efqld?08+tb{Q`6CsnQl#{(Fj zRW2krwk0S+RIoXY_rn-&oNXUA{R8pS_^-;M=5Z$Xx!cMA9C+mH4XFRww6f@w$ ze*CUb6%kxn;P*s`pZ7n(FNpAOjs%;axHA$Q{p|MaRo6{&_S2KH-?+}OC)5r^hLdcy z1It&ZtV?p%POVyf^|MK7haaO%P3!z+c_7h{GOEE3qY~80glxs;c)$mLA`9S0Cl}e2 zAanj&)K}wa;QS|BT2>aF7#y6Z}>963hQ2aNonkzO-ST`bFe{C>@t%0DUD~c1Xz!47|yx5RzlraIA=^P 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 OF#Hb$IuD*ZA%6lFH?5ig literal 0 HcmV?d00001 diff --git a/runtime_data/openalpr.conf b/runtime_data/openalpr.conf new file mode 100644 index 0000000..094c907 --- /dev/null +++ b/runtime_data/openalpr.conf @@ -0,0 +1,115 @@ +[common] + + +ocr_img_size_percent = 1.33333333 +state_id_img_size_percent = 2.0 + +max_plate_width_percent = 100 +max_plate_height_percent = 100 + + +ocr_min_font_point = 6 + +; Minimum OCR confidence percent to consider. +postprocess_min_confidence = 60 + +; Any OCR character lower than this will also add an equally likely +; chance that the character is incorrect and will be skipped. Value is a confidence percent +postprocess_confidence_skip_level = 75 + +; Reduces the total permutations to consider for scoring. +postprocess_max_substitutions = 2 + +; Results with fewer characters will be discarded +postprocess_min_characers = 4 +postprocess_max_characers = 8 + +[debug] +general = 0 +timing = 0 +state_id = 0 +plate_lines = 0 +plate_corners = 0 +char_regions = 0 +char_segment = 0 +char_analysis = 0 +color_filter = 0 +ocr = 0 +postprocess = 0 +show_images = 0 + + + +;;; Country Specific variables ;;;; + +[us] + +; 30-50, 40-60, 50-70, 60-80, 70-90 +char_analysis_min_pct = 0.30 +char_analysis_height_range = 0.20 +char_analysis_height_step_size = 0.10 +char_analysis_height_num_steps = 5 + +segmentation_min_box_width_px = 4 +segmentation_min_charheight_percent = 0.3; +segmentation_max_segment_width_percent_vs_average = 1.35; + +plate_width_mm = 304.8 +plate_height_mm = 152.4 + +char_height_mm = 70 +char_width_mm = 35 +char_whitespace_top_mm = 38 +char_whitespace_bot_mm = 38 + +template_max_width_px = 120 +template_max_height_px = 60 + +; Higher sensitivity means less lines +plateline_sensitivity_vertical = 25 +plateline_sensitivity_horizontal = 45 + + +; Regions smaller than this will be disqualified +min_plate_size_width_px = 70 +min_plate_size_height_px = 35 + +ocr_language = lus + +[eu] + +; 55-70, 65-80, 75-90, 85-100 +char_analysis_min_pct = 0.45 +char_analysis_height_range = 0.15 +char_analysis_height_step_size = 0.10 +char_analysis_height_num_steps = 5 + + +segmentation_min_box_width_px = 8 +segmentation_min_charheight_percent = 0.5; +segmentation_max_segment_width_percent_vs_average = 2.0; + +plate_width_mm = 520 +plate_height_mm = 110 + +char_height_mm = 80 +char_width_mm = 53 +char_whitespace_top_mm = 10 +char_whitespace_bot_mm = 10 + +template_max_width_px = 184 +template_max_height_px = 46 + +; Higher sensitivity means less lines +plateline_sensitivity_vertical = 18 +plateline_sensitivity_horizontal = 55 + +; Regions smaller than this will be disqualified +min_plate_size_width_px = 100 +min_plate_size_height_px = 20 + +ocr_language = leu + + + + diff --git a/runtime_data/postprocess/eu.patterns b/runtime_data/postprocess/eu.patterns new file mode 100644 index 0000000..e69de29 diff --git a/runtime_data/postprocess/readme.txt b/runtime_data/postprocess/readme.txt new file mode 100644 index 0000000..92013a2 --- /dev/null +++ b/runtime_data/postprocess/readme.txt @@ -0,0 +1,7 @@ +Each line is a possible lp pattern organized by region/state and then likelihood. + +The parser goes through each line and tries to match +@ = any letter +# = any number +? = a skip position (can be anything, but remove it if encountered) +[A-FGZ] is just a single char position with specific letter requirements. In this example, the regex defines characters ABCDEFGZ diff --git a/runtime_data/postprocess/us.patterns b/runtime_data/postprocess/us.patterns new file mode 100644 index 0000000..be3ce6a --- /dev/null +++ b/runtime_data/postprocess/us.patterns @@ -0,0 +1,212 @@ +base @@@#### +base @@@### +base ###@@@ +al #@##@## +al ##@##@# +al @@##### +al #####@@ +al #@####@ +al ##@###@ +al ##@#@#@ +ak @@@### +as #### +az @@@#### +az ?@@@#### +az ###@@@ +ar ###@@@ +ar @@@### +ca #@@@### +ca #@@@### +ca #@##### +ca #####@# +ca ###@@@ +co ###@@@ +co @@#### +co @@@### +co @@@#### +ct #@@@@# +ct ###@@@ +ct ##### +ct ###### +ct @### +ct @@### +ct @@#### +de ###### +de ##### +de #### +de ### +dc @@#### +dc ###### +fl @@@@## +fl ####[GH]@ +fl ###[H-Y]@@ +fl @###@@ +fl @##@@@ +fl @###@@ +fl @@@?@## +fl ###?#[GH]@ +fl ###?[H-Y]@@ +fl @##?#@@ +fl @##?@@@ +fl @##?#@@ +ga @@@#### +ga ####@@@ +ga ####@@ +ga #####[Q]@ +ga ###@@@ +gu @@#### +gu @@@#### +gu @@@###@ +hi [A-HJKNMPR-Z]@@### +id @###### +id #@##### +id #@@#### +id #@@@### +id [A]#####[T] +id [A]@####[T] +id #[A]####[T] +id #[A]@###[T] +id [BU]#### +id ####[BEFGHIJKLMNPRSTUXYZ] +id ##[S][A][S] +id #@@#[S] +id [J]@### +id #####[BCS] +id ###@[E] +id ##@@[E] +id ##### +il @###### +il ##### +il ###### +il @@#### +il @@@### +il @##### +il ####### +il ####@ +il #####@ +in ###@ +in ###@@ +in ###@@@ +in #### +ia @@@### +ia ###@@@ +ia ####@@ +ks ###@@@ +ks @@@### +ky ###@@@ +la @@@### +me ####@@ +me ###@@ +me ##@@ +me ###@@@ +ms @@@@@# +ms @@@### +ms @@@?### +ms ##[W]## +md #@@@## +md #[AB]#### +md @@@### +md ###[AB][A-N][A-MY] +md [AB][A-E][A-Y]##@ +md #####[ABCY] +md @##### +ma ###@@# +ma #@@### +ma #@@@## +ma ###### +ma ###@@@ +ma ####@@ +ma ##@@## +mi @@@#### +mi #@@@## +mn ###@@@ +mn @@@### +mo @@#@#@ +mo ###@@@ +mo #@@##@ +mt ######[A] +mt #####@ +mt ####@ +ne #@#### +ne #@@### +ne ##@### +ne ##@@## +nv ###@@@ +nv @##### +nv @@#### +nv @@@### +nh ###### +nh ####### +nh @@@### +nj @##@@@ +nj @@@##@ +nj @@###@ +nj @@@#### +nm @@@### +nm ###@@@ +nm @@@### +nm @@### +nm @### +ny @@@#### +ny @@@### +ny #@@### +ny @#@### +ny @###@@ +ny @@###@ +nc @@@#### +nd @@@### +mp @@@### +oh @@@#### +oh @@##@@ +ok ###@@@ +or ###@@@ +or @@@### +or ###?@@@ +or @@@?### +pa @@@#### +pr @@@### +ri @@### +ri ###### +ri ##### +sc @@@### +sc ####@@ +sd #@@### +sd ##@### +sd ##@@## +sd ##@@@# +tn ###@@@ +tx @@@#### +tx @##@@@ +tx ###@@@ +tx @@@### +tx @@#@### +ut @###@@ +ut @###@@ +ut ###@# +ut ###@@@ +ut @@@### +ut @##?#@@ +ut @##?#@@ +ut ###?@# +ut ###?@@@ +ut @@@?### +vt @@@### +vt ###@### +vt ##[B]## +vi @@@### +va @@@#### +va [J-Z]@@#### +va @@@### +va @@#### +va ####@@ +va #####[JY] +wa ###@@@ +wa @@@#### +wv [1-9DON]@@### +wv [1-9DON]@#### +wy ###### +wy ####### +wy ##### +wy ####@ +wy ###@@ +wy ##@@@ diff --git a/runtime_data/region/eu.xml b/runtime_data/region/eu.xml new file mode 100644 index 0000000..b9463de --- /dev/null +++ b/runtime_data/region/eu.xml @@ -0,0 +1,788 @@ + + + + BOOST + LBP + 13 + 52 + + GAB + 9.9500000476837158e-01 + 5.0000000000000000e-01 + 9.4999999999999996e-01 + 1 + 100 + + 256 + 1 + 13 + + + <_> + 5 + -1.2342801094055176e+00 + + <_> + + 0 -1 30 352328156 -192015 -15754753 -173569 -1164334081 + -5526021 -1649426433 -1073862520 + + -8.3821654319763184e-01 4.5727136731147766e-01 + <_> + + 0 -1 4 -286263569 -2102402322 -534582034 -353374577 -2098177 + 2011675391 -1473 -150996993 + + -7.1907609701156616e-01 5.3620684146881104e-01 + <_> + + 0 -1 17 -1679491076 -36390424 -1764698371 1071385316 + -139804689 -4539205 -1078198529 -1882715412 + + -6.6871184110641479e-01 5.8701753616333008e-01 + <_> + + 0 -1 42 806360401 -8441479 -41 2145992703 -1077740829 + -69494529 -1382096961 1035868412 + + -7.2897046804428101e-01 4.8680499196052551e-01 + <_> + + 0 -1 58 -1050130 181039615 1040326952 61194898 1532960676 + 589570476 2004968781 2145386463 + + -7.2125834226608276e-01 5.0491940975189209e-01 + + <_> + 3 + -1.1206305027008057e+00 + + <_> + + 0 -1 75 -3325 -300158306 -1055923968 -1051789814 -15487472 + 26714273 -356277760 -134218769 + + -8.3067792654037476e-01 4.7307375073432922e-01 + <_> + + 0 -1 22 -2144729862 -1107754033 -2146876419 -7702187 + -1144850757 -878974541 -1555824641 -1414656342 + + -7.7843278646469116e-01 4.9317824840545654e-01 + <_> + + 0 -1 25 -403181842 653246531 -522927960 -1466965298 + -162827985 1387782497 -31897673 -201331721 + + -7.6949650049209595e-01 4.8848018050193787e-01 + + <_> + 5 + -1.1834602355957031e+00 + + <_> + + 0 -1 34 1969028604 -174603 -1760004131 -2419235 -660035078 + -345927 -1685546577 -1081209844 + + -8.2657516002655029e-01 4.6755406260490417e-01 + <_> + + 0 -1 13 -1815816452 -539327252 -972892705 -652156 + -1424303361 -36085509 -83694673 -1102173014 + + -7.0588159561157227e-01 5.1438063383102417e-01 + <_> + + 0 -1 57 67119184 -103071403 -576989217 1967067647 + -1147610369 -1141124982 -1184727105 -1681315656 + + -6.7077225446701050e-01 4.9133408069610596e-01 + <_> + + 0 -1 33 525342096 1604130064 1427685243 -6342179 -1713620348 + -277141107 -1895198281 -1952499204 + + -6.2518852949142456e-01 5.1328247785568237e-01 + <_> + + 0 -1 55 -421271550 -1071591260 -788168569 1657793455 + -497851773 671337482 -475700442 1904211543 + + -6.5562003850936890e-01 5.0863337516784668e-01 + + <_> + 5 + -1.4445747137069702e+00 + + <_> + + 0 -1 12 -1534405121 -7603749 1997535231 -545009153 + -640697349 -5256817 -272638977 -1427382849 + + -7.8040641546249390e-01 4.3654364347457886e-01 + <_> + + 0 -1 32 1573951736 -732840 482392825 -93088771 -1162303268 + -1074128191 -1618162693 1065880648 + + -8.0440253019332886e-01 3.4074461460113525e-01 + <_> + + 0 -1 50 -196868 -1081042020 -652693028 993800656 -1098090051 + 465070273 197966809 -1089 + + -6.0474485158920288e-01 4.6314033865928650e-01 + <_> + + 0 -1 35 806409680 -537310891 33015157 1507693981 -1832731928 + -14137270 -1129420100 409740064 + + -7.3604851961135864e-01 4.1595551371574402e-01 + <_> + + 0 -1 23 -957878746 -1999965426 -2045851578 -1773288254 + -92571682 1243334953 -1858941089 1204805279 + + -7.1337687969207764e-01 4.1314238309860229e-01 + + <_> + 6 + -1.7404717206954956e+00 + + <_> + + 0 -1 1 -290783233 -8662063 -583139329 -534081 -1446248449 + -6302979 -81007617 -286262273 + + -7.2589415311813354e-01 4.9178531765937805e-01 + <_> + + 0 -1 74 -245 -281018657 -145631488 -79243094 -67380702 + 1139491271 -254548224 -134742081 + + -5.6722396612167358e-01 5.1794242858886719e-01 + <_> + + 0 -1 16 -1410585858 -11625066 -1592084291 1071133834 + -75633666 -1156933509 -1650494993 -1965380226 + + -5.7949805259704590e-01 4.6442687511444092e-01 + <_> + + 0 -1 41 276829264 -6467111 -547684897 -538763841 -1700545041 + -69220102 -1380516933 -1122040423 + + -6.5663957595825195e-01 3.7706291675567627e-01 + <_> + + 0 -1 45 -822084950 549227283 -1023008128 263662540 907280548 + 357251361 794240313 1988100095 + + -6.9282585382461548e-01 4.0284207463264465e-01 + <_> + + 0 -1 63 -7608534 1707979879 1950662688 1930289631 927414213 + 1145794859 -206058507 -207093825 + + -6.0658127069473267e-01 3.8594201207160950e-01 + + <_> + 6 + -1.7376002073287964e+00 + + <_> + + 0 -1 15 -67113473 -50397705 -671624993 -33621601 -36703233 + -33825569 -536871937 -1 + + -6.9109666347503662e-01 6.3259667158126831e-01 + <_> + + 0 -1 73 -67375357 -438349313 -82707456 -495457425 -142611689 + 1652538223 -104080572 -138412593 + + -6.1666935682296753e-01 4.3335196375846863e-01 + <_> + + 0 -1 43 402657488 -44578 -648282633 -134785097 -1410444545 + -4211781 -1648690977 -1682372168 + + -6.8725508451461792e-01 4.1583669185638428e-01 + <_> + + 0 -1 28 -279164164 -1074989448 -1655775393 -1159327304 + 995687420 -1410851 2070855679 -1970787324 + + -7.3297709226608276e-01 3.7756869196891785e-01 + <_> + + 0 -1 5 -403189969 -1361908067 1116678830 -386513329 + 1742103151 1414519535 1946120914 -148111428 + + -5.0710958242416382e-01 4.9361771345138550e-01 + <_> + + 0 -1 47 489953432 1071731312 -1554270735 2134729717 + 1527790300 2078153336 -1157828613 -549552899 + + -6.5112805366516113e-01 3.9441567659378052e-01 + + <_> + 6 + -1.4720557928085327e+00 + + <_> + + 0 -1 71 -249 2073550167 -1086088193 -9729 -135266717 + -2387277 -89130546 -67109889 + + -7.1864211559295654e-01 4.3991416692733765e-01 + <_> + + 0 -1 21 -65537 -1073816065 788447231 -1078805068 -65537 -2 + -16844801 -1073807905 + + -4.9692794680595398e-01 5.7111859321594238e-01 + <_> + + 0 -1 6 -286265361 -522198801 -300226866 -824457913 + 1999336254 1165174031 -75536539 -134217733 + + -5.0260621309280396e-01 4.8911646008491516e-01 + <_> + + 0 -1 60 134224976 -42115 503045631 -104909380 -1423213585 + -1093682593 -1346775649 503843200 + + -8.1539380550384521e-01 2.9674032330513000e-01 + <_> + + 0 -1 27 -606607953 147439780 -123759474 148890575 + -1222134481 -265879523 -738331785 -100663425 + + -4.9730581045150757e-01 4.8398590087890625e-01 + <_> + + 0 -1 59 -939031293 -134221841 -5 -268436481 -67108929 -1 -1 + -1071651065 + + -4.1419434547424316e-01 5.8424508571624756e-01 + + <_> + 7 + -1.9503176212310791e+00 + + <_> + + 0 -1 77 -67108933 -541065249 -610542320 -1049601 -289407299 + -572527621 -75498256 -1 + + -6.8429845571517944e-01 4.5343136787414551e-01 + <_> + + 0 -1 18 -1431832585 -9502721 2147483647 -34603011 -840958017 + -268435457 -1094713345 -1397818113 + + -4.7899335622787476e-01 5.1089739799499512e-01 + <_> + + 0 -1 54 -1067203069 -536870985 -4097 -1 -268437505 -1 + -38019153 -1039416409 + + -3.8713064789772034e-01 5.8315634727478027e-01 + <_> + + 0 -1 44 1043073500 -1619165736 -1765999621 -11021831 + -1678714630 -229827 869072861 957881548 + + -7.2454583644866943e-01 3.4410527348518372e-01 + <_> + + 0 -1 62 -268507652 871379856 -1983639004 1038103444 + -1082884100 260856189 458107068 2146402047 + + -6.6823595762252808e-01 3.6522203683853149e-01 + <_> + + 0 -1 2 -285227361 -1565854002 -1366888194 -2033032641 + 1475044343 1702916555 -208944208 -143661409 + + -4.8592305183410645e-01 4.9365350604057312e-01 + <_> + + 0 -1 49 -3154 674558130 -909431384 1740634054 1667371440 + 873989589 1381178774 1610071935 + + -6.2329936027526855e-01 3.7792292237281799e-01 + + <_> + 7 + -1.3005143404006958e+00 + + <_> + + 0 -1 8 -150997249 -186651393 -51909121 -264193 -70542337 + -138413329 -3277825 -1310801 + + -6.7938089370727539e-01 4.6835443377494812e-01 + <_> + + 0 -1 29 -1595932528 -10275 -371240481 -2902153 -352329729 + -89138473 -1684616193 -1142150132 + + -6.8929862976074219e-01 2.8295361995697021e-01 + <_> + + 0 -1 76 -75501649 -1478560481 -78783200 -669057349 + -285343931 115340787 -136381440 -170655777 + + -5.4028475284576416e-01 4.0457248687744141e-01 + <_> + + 0 -1 66 -135604068 -1616889908 176728724 1050470380 + -410239956 1860975396 986186492 2146959103 + + -6.4993071556091309e-01 3.1024640798568726e-01 + <_> + + 0 -1 53 -37815298 -1756071972 932584732 502929916 + -1620288551 714890421 1005624314 -83203 + + -4.8649874329566956e-01 4.4701680541038513e-01 + <_> + + 0 -1 68 -1071175933 -1 -129 -4101 -390074625 -268468225 + -1025 -2111092861 + + -4.5232096314430237e-01 4.7892215847969055e-01 + <_> + + 0 -1 3 -1075665154 -12306554 151832703 -720964926 + -2003879185 -71727650 -742296849 715269738 + + -6.1241555213928223e-01 3.7678867578506470e-01 + + <_> + 7 + -1.3320474624633789e+00 + + <_> + + 0 -1 70 -2297 -2201 -606217217 -536871013 -605029633 + -605045341 -84149761 -1 + + -6.7704600095748901e-01 4.7368422150611877e-01 + <_> + + 0 -1 20 -268437585 715893674 -980686082 -1162892626 + 2147438079 -604766342 -2411555 -33554657 + + -5.0731199979782104e-01 4.4156819581985474e-01 + <_> + + 0 -1 10 185116406 -1615877553 -1345158081 -1115716262 + 2044668927 2141941247 -873464833 -415058 + + -5.0882929563522339e-01 3.9274227619171143e-01 + <_> + + 0 -1 40 -416551126 541063119 1720361486 1941921098 928217927 + 1077374554 -470371167 -169607201 + + -5.6418907642364502e-01 3.5969066619873047e-01 + <_> + + 0 -1 39 -2144965630 -1786853385 983290681 -193990065 + -1435593777 -874870781 -148652293 1076348426 + + -6.1384546756744385e-01 3.4418886899948120e-01 + <_> + + 0 -1 14 1324412412 -35863564 -535364629 -1764835842 + 2115366735 -1212035965 -8000562 -33887267 + + -5.4024517536163330e-01 3.6199000477790833e-01 + <_> + + 0 -1 37 -120393908 971551622 -1026612532 804784283 701349152 + 787773517 723802396 2147483639 + + -6.4826697111129761e-01 3.3292853832244873e-01 + + <_> + 7 + -1.6739253997802734e+00 + + <_> + + 0 -1 24 -33557249 83864537 -543172129 -536871533 -16793857 + -321942533 -18096129 -1061 + + -6.8337130546569824e-01 3.4821429848670959e-01 + <_> + + 0 -1 64 -8429772 -1080077475 -217843395 -1078101315 + -73680708 -6723397 -1661387332 -1648672976 + + -5.9955561161041260e-01 3.2578879594802856e-01 + <_> + + 0 -1 48 -127905580 -79939080 -1108012101 -537627339 + -1298089842 -76845170 -1912940051 85983264 + + -7.3458421230316162e-01 2.5306507945060730e-01 + <_> + + 0 -1 52 17 -2246729 -777270789 -539231201 -1081169169 + -76884579 -1748256325 683018597 + + -7.9745399951934814e-01 2.6082354784011841e-01 + <_> + + 0 -1 9 -890241298 -463670186 -795681442 -51970621 1909811571 + -114822853 -24184323 -50869249 + + -5.2764928340911865e-01 3.5589152574539185e-01 + <_> + + 0 -1 51 -433068542 -991182329 44441604 306607153 1449382759 + 125030915 1409898947 -671101193 + + -5.8401101827621460e-01 3.5362774133682251e-01 + <_> + + 0 -1 38 2111600636 1066940316 863786494 2140674937 978850808 + 455878812 1503801855 1002372991 + + -7.2070401906967163e-01 2.7193057537078857e-01 + + <_> + 7 + -1.4490258693695068e+00 + + <_> + + 0 -1 11 -68097 -1080405289 -1367935265 533863645 -721445889 + -2391185 -278987041 -1049089 + + -7.1471744775772095e-01 2.5204733014106750e-01 + <_> + + 0 -1 72 -6555889 1097593319 -17379062 -369165825 1604055815 + 1634328719 -274734270 -470810629 + + -5.6740534305572510e-01 3.2793164253234863e-01 + <_> + + 0 -1 31 -134320292 2029845341 938917884 855571322 1238654844 + 1507329893 2147228636 -2564101 + + -6.1741048097610474e-01 2.8779673576354980e-01 + <_> + + 0 -1 46 -1073495038 -545262305 -80218625 -538970417 + -604251650 -6629485 -83904001 -1072987226 + + -4.6788156032562256e-01 3.9040920138359070e-01 + <_> + + 0 -1 61 -2013248189 -740827685 -222339873 -1051777 + -337648385 -23390301 -274923557 -2102290957 + + -4.4455590844154358e-01 4.3781617283821106e-01 + <_> + + 0 -1 67 -425532380 -1948260198 -771398400 1078643656 + -610280144 1107493733 1455380011 1207686574 + + -6.3499057292938232e-01 3.1427818536758423e-01 + <_> + + 0 -1 0 -1465474633 -1346333225 -1384647689 -439632648 + 555739383 1639682123 -18940001 615339959 + + -6.4793455600738525e-01 2.6171669363975525e-01 + + <_> + 7 + -1.8950796127319336e+00 + + <_> + + 0 -1 69 -67117281 -2629289 -188747521 -2107429 -5248769 + -624954197 -606086145 -71565729 + + -6.4841037988662720e-01 5.0977444648742676e-01 + <_> + + 0 -1 26 -277092053 1219489222 -272144530 -419430680 + -1034030083 -1092103687 -7605451 -603981825 + + -5.3364491462707520e-01 3.6466205120086670e-01 + <_> + + 0 -1 65 465055548 1006376861 1063125433 -1078365731 + 2023522188 1553474557 1872486879 2079284637 + + -6.4313161373138428e-01 2.6651522517204285e-01 + <_> + + 0 -1 7 -54854582 1761013467 2121979482 -88981490 1394046423 + -147368984 -674043102 -42668425 + + -6.1165940761566162e-01 2.7585926651954651e-01 + <_> + + 0 -1 56 -71748 1037336260 -1983891980 -1078316142 1056466172 + 704383733 871119804 1073643373 + + -7.3266768455505371e-01 2.3998503386974335e-01 + <_> + + 0 -1 19 -1968540329 2012184063 -16777217 -134234115 + -1342177281 -8388737 -402661386 -840250051 + + -3.9631426334381104e-01 4.8900303244590759e-01 + <_> + + 0 -1 36 -102240505 -579600401 -402662694 -152829955 + -671612979 224878431 -1153443866 -487325705 + + -3.8574314117431641e-01 5.1558119058609009e-01 + + <_> + + 0 0 3 1 + <_> + + 0 0 4 1 + <_> + + 0 1 1 3 + <_> + + 0 1 12 1 + <_> + + 0 2 3 2 + <_> + + 0 3 1 2 + <_> + + 0 6 1 1 + <_> + + 0 6 2 2 + <_> + + 0 6 3 2 + <_> + + 0 7 2 1 + <_> + + 0 9 16 1 + <_> + + 0 10 4 1 + <_> + + 1 0 3 1 + <_> + + 1 1 14 1 + <_> + + 1 6 1 2 + <_> + + 1 6 2 2 + <_> + + 1 10 7 1 + <_> + + 1 10 13 1 + <_> + + 2 0 2 1 + <_> + + 2 0 3 1 + <_> + + 2 1 2 3 + <_> + + 2 10 3 1 + <_> + + 3 0 8 1 + <_> + + 3 1 2 3 + <_> + + 3 2 1 1 + <_> + + 3 5 2 1 + <_> + + 4 2 1 1 + <_> + + 4 4 1 1 + <_> + + 4 10 7 1 + <_> + + 5 0 4 1 + <_> + + 5 0 14 1 + <_> + + 5 7 1 2 + <_> + + 7 10 12 1 + <_> + + 8 9 14 1 + <_> + + 9 10 14 1 + <_> + + 10 1 13 1 + <_> + + 10 4 2 1 + <_> + + 12 3 1 3 + <_> + + 13 7 2 2 + <_> + + 14 0 1 3 + <_> + + 16 6 2 1 + <_> + + 17 0 4 1 + <_> + + 18 0 4 1 + <_> + + 18 0 5 1 + <_> + + 18 10 7 1 + <_> + + 19 3 2 3 + <_> + + 20 0 1 2 + <_> + + 20 9 6 1 + <_> + + 21 1 8 1 + <_> + + 21 3 2 3 + <_> + + 22 7 1 2 + <_> + + 24 3 2 1 + <_> + + 25 0 1 1 + <_> + + 25 7 1 2 + <_> + + 27 0 1 2 + <_> + + 27 1 1 3 + <_> + + 27 7 1 2 + <_> + + 28 0 7 1 + <_> + + 28 4 2 3 + <_> + + 31 0 1 2 + <_> + + 31 0 4 1 + <_> + + 31 1 1 1 + <_> + + 33 7 1 2 + <_> + + 34 6 2 1 + <_> + + 34 10 6 1 + <_> + + 36 9 3 1 + <_> + + 37 4 1 3 + <_> + + 39 2 1 3 + <_> + + 40 0 1 2 + <_> + + 49 0 1 1 + <_> + + 49 0 1 2 + <_> + + 49 1 1 1 + <_> + + 49 2 1 1 + <_> + + 49 3 1 1 + <_> + + 49 3 1 2 + <_> + + 49 4 1 2 + <_> + + 49 8 1 1 + <_> + + 49 9 1 1 + diff --git a/runtime_data/region/us.xml b/runtime_data/region/us.xml new file mode 100644 index 0000000..4fde906 --- /dev/null +++ b/runtime_data/region/us.xml @@ -0,0 +1,1256 @@ + + + + BOOST + LBP + 18 + 36 + + GAB + 9.9500000476837158e-01 + 5.0000000000000000e-01 + 9.4999999999999996e-01 + 1 + 100 + + 256 + 1 + 16 + + + <_> + 5 + -1.0164581537246704e+00 + + <_> + + 0 -1 121 -2101490 8645451 -283130880 -521148469 -204483069 + 12806163 -87825664 -134217729 + + -6.9118094444274902e-01 5.3935295343399048e-01 + <_> + + 0 -1 10 -279974098 -496242961 -1069413754 -393288982 + 1933003556 1107542979 1930908736 -137887881 + + -6.3112831115722656e-01 5.1992321014404297e-01 + <_> + + 0 -1 22 35655605 -827433 -5415457 -36391040 1874996219 + -34146 -275874833 706740768 + + -6.8107974529266357e-01 5.0729984045028687e-01 + <_> + + 0 -1 45 905461180 1071454716 -1190553121 -1107455523 + -3604483 969733017 -1229826 1001921916 + + -6.8964874744415283e-01 4.4974544644355774e-01 + <_> + + 0 -1 73 -83890686 -2137546038 16777224 -1060960242 952574500 + 46351119 -801677308 1400893103 + + -5.9438413381576538e-01 5.3718543052673340e-01 + + <_> + 3 + -9.6158474683761597e-01 + + <_> + + 0 -1 7 -342891990 -858787222 -993861598 -420558910 + 1895973654 1112604759 -1021099775 -143132809 + + -8.1113457679748535e-01 4.5976096391677856e-01 + <_> + + 0 -1 108 -335813114 -529607678 5522944 1084412484 1620529409 + 1076093571 -981147648 -201327637 + + -7.1228593587875366e-01 5.3981220722198486e-01 + <_> + + 0 -1 49 5136 269795672 -1861144515 -1076422671 -611597063 + -1149220182 -1585173830 -1141147489 + + -6.3254243135452271e-01 5.6183582544326782e-01 + + <_> + 5 + -1.0309549570083618e+00 + + <_> + + 0 -1 51 268437744 290500948 -1114719811 -6704805 -1618149379 + -1076078413 -357921859 -1078915093 + + -7.1921354532241821e-01 3.8260513544082642e-01 + <_> + + 0 -1 122 -17830386 13563031 -953434100 -2008231014 + -220995577 1076482227 -637821440 -789577749 + + -6.3529521226882935e-01 4.3188422918319702e-01 + <_> + + 0 -1 36 496508412 -1653307174 957356029 1071120604 314050812 + -72849222 724646911 458762496 + + -7.2846937179565430e-01 3.9699622988700867e-01 + <_> + + 0 -1 81 -2643238 931172412 4194440 990384812 172499336 + 574624745 964690924 -134220801 + + -4.9917802214622498e-01 5.5823230743408203e-01 + <_> + + 0 -1 60 819200221 -545819180 -786991361 -543199009 + -1165378309 -1145372421 -762590469 -1367855576 + + -5.7338857650756836e-01 5.2183729410171509e-01 + + <_> + 6 + -1.3750067949295044e+00 + + <_> + + 0 -1 86 -1073225214 -745282173 1611024547 -682630394 + -233847825 -1038097918 -455611717 -1060641149 + + -6.2025314569473267e-01 4.0825036168098450e-01 + <_> + + 0 -1 43 876613916 -40037089 -783166017 -8432889 -1171594753 + -4274039 -133657938 -1467482104 + + -5.8861476182937622e-01 4.7982457280158997e-01 + <_> + + 0 -1 6 -336597458 -490215229 -1058878458 -489764182 + 1668641655 1393971207 -217128362 -134745097 + + -5.6835436820983887e-01 4.8286029696464539e-01 + <_> + + 0 -1 20 79692020 534677841 424218623 1070072336 268704925 + -73329762 -1079369730 -1073909782 + + -6.0406535863876343e-01 4.6269953250885010e-01 + <_> + + 0 -1 97 -21330 421536498 -1342363608 -214028566 1836585004 + 286537041 1865875704 -1025 + + -4.6817129850387573e-01 5.8012592792510986e-01 + <_> + + 0 -1 63 909651700 -2612866 -1081069153 -1084489636 839925759 + -1079235939 998865919 -1111744804 + + -6.2081623077392578e-01 4.2852258682250977e-01 + + <_> + 7 + -1.3817892074584961e+00 + + <_> + + 0 -1 90 -1073233918 -7867830 1774438879 -174071546 + -487591697 -353447872 1630530447 -795351289 + + -6.0853856801986694e-01 3.0612245202064514e-01 + <_> + + 0 -1 27 -866087472 -47148812 1543519733 -538179376 178522620 + -1091040067 -1966043204 -1428943612 + + -5.7823383808135986e-01 3.8258698582649231e-01 + <_> + + 0 -1 37 289739038 2134880727 -1687055361 2138284927 + -1738673666 -8005187 1509787391 172952264 + + -6.0462307929992676e-01 3.4132298827171326e-01 + <_> + + 0 -1 112 -1063522814 1090515890 -987518497 -975187365 + -666902270 -2004358237 -456460858 14679803 + + -5.6259483098983765e-01 4.1315501928329468e-01 + <_> + + 0 -1 71 893136892 424984573 -641843717 -1078150900 + 2109476861 -1154906888 -1079493636 1056454904 + + -7.5118625164031982e-01 2.5987851619720459e-01 + <_> + + 0 -1 77 -1060641790 2002240806 -258542561 -578727421 + -292822385 -1878916986 -800094261 -1068244437 + + -4.5728075504302979e-01 4.6161931753158569e-01 + <_> + + 0 -1 28 12912 2002213853 1558175231 -1256749681 -1424237825 + -103100885 -410259713 4653568 + + -6.8373686075210571e-01 2.8601825237274170e-01 + + <_> + 7 + -1.0901353359222412e+00 + + <_> + + 0 -1 92 -3163938 396397186 -1122500432 1003139974 2041847944 + 271073553 332334300 2147483647 + + -7.0157480239868164e-01 2.2578348219394684e-01 + <_> + + 0 -1 120 -268440822 4184991 -674370816 1659876317 -370410686 + 2000418299 -84149504 -252706881 + + -6.0130935907363892e-01 3.3310183882713318e-01 + <_> + + 0 -1 42 -1039941118 -217616093 1615063327 -241712605 + -687289430 -1144354306 -334762162 -1066932381 + + -4.4966760277748108e-01 4.6698939800262451e-01 + <_> + + 0 -1 68 -2122284912 -1090455534 -1681680995 1070641104 + -20972611 -1078224767 -1410137411 -1343018573 + + -5.3551906347274780e-01 3.7870019674301147e-01 + <_> + + 0 -1 88 -1070450 870985048 42139824 996573700 -1106765700 + 201873562 1743410140 1540358111 + + -5.2978163957595825e-01 3.6144843697547913e-01 + <_> + + 0 -1 31 -1939853392 -577122062 1502616063 1599219130 + 227580924 -914321004 -370630673 214173504 + + -6.0552209615707397e-01 3.4924620389938354e-01 + <_> + + 0 -1 126 -541065224 422115384 -1057384 -1090060358 + -1673538307 -1438728201 2113920252 -15467077 + + -3.8117402791976929e-01 5.2778989076614380e-01 + + <_> + 8 + -1.3476891517639160e+00 + + <_> + + 0 -1 11 -286265649 -287390017 -827683226 -290536562 + 1395093845 1931956081 -285903036 -134744073 + + -6.2529915571212769e-01 2.8105655312538147e-01 + <_> + + 0 -1 56 -1071127037 -50863125 -289407402 -239343617 + -147853846 -1018253502 -1062801473 -800590013 + + -3.9758357405662537e-01 5.0723552703857422e-01 + <_> + + 0 -1 33 -2080373888 -546589771 -171995141 -2638918 + -342044931 -1062913 -1177818178 -1977612032 + + -5.8244431018829346e-01 3.3762589097023010e-01 + <_> + + 0 -1 75 -356520186 615433775 -307301214 -529603718 + 1675579941 1194556139 -172625312 -167774209 + + -5.1843309402465820e-01 3.6296874284744263e-01 + <_> + + 0 -1 23 2097328 -4262985 -1538521673 -1216114040 1662518268 + -18070610 -1920135429 -271626369 + + -4.2509132623672485e-01 4.5544615387916565e-01 + <_> + + 0 -1 5 -294688977 -472912437 1799466506 -353440566 655565423 + 850647851 1444407287 -164628619 + + -4.9901044368743896e-01 3.8141638040542603e-01 + <_> + + 0 -1 110 83345431 12774355 -251191905 -2231585 -525861961 + 1506648762 -1997276758 -2065699353 + + -4.2755702137947083e-01 4.3639278411865234e-01 + <_> + + 0 -1 48 1358977424 -1791707260 -81636387 -6754249 -106832451 + -111112061 -979761441 -1085463894 + + -5.1844465732574463e-01 3.5742419958114624e-01 + + <_> + 8 + -1.6724135875701904e+00 + + <_> + + 0 -1 123 -209 148633591 -25698048 -520093961 -1209 133169151 + -103812348 -721428481 + + -6.5783321857452393e-01 1.9410495460033417e-01 + <_> + + 0 -1 61 -717179948 -9374236 -581713921 -1076841616 + 1323720701 -1081549914 -272744449 722468864 + + -5.6369322538375854e-01 2.9236978292465210e-01 + <_> + + 0 -1 3 -500750814 -487627273 -2131735954 -87102706 + 1969713956 1618695899 1989624053 -135793865 + + -5.4797309637069702e-01 3.1121733784675598e-01 + <_> + + 0 -1 57 4194384 2113868221 -45051395 -2157229 -722703681 + -67236929 -1155745027 4204544 + + -7.6136893033981323e-01 2.4244518578052521e-01 + <_> + + 0 -1 102 -203491074 836792416 978849880 980465884 333488593 + 282654613 -1029668628 -335545601 + + -4.4871535897254944e-01 3.8452658057212830e-01 + <_> + + 0 -1 24 -1757501443 -1076483905 -1653865985 1067193105 + 704908280 -1076312132 -105239847 -1090905634 + + -4.0212839841842651e-01 4.5655438303947449e-01 + <_> + + 0 -1 93 -68224350 -498997469 -723851742 -1331496190 + 1637503309 1101152343 1908400422 -168296449 + + -5.0145322084426880e-01 3.3501255512237549e-01 + <_> + + 0 -1 15 -545370627 -78245 -444461090 -1096931590 199758333 + 1310203059 1331437533 1566725100 + + -5.0831568241119385e-01 3.5741838812828064e-01 + + <_> + 8 + -1.0506174564361572e+00 + + <_> + + 0 -1 124 -67109109 1113576183 -14160600 -924854433 + -341836020 382462907 -69206268 -184549381 + + -6.0169494152069092e-01 2.2098569571971893e-01 + <_> + + 0 -1 55 -1073512445 -788345853 74174129 -105666001 + -632894997 -1073630138 -1046500353 -1055932277 + + -4.1919940710067749e-01 4.3400409817695618e-01 + <_> + + 0 -1 99 -1066933725 -169871541 -2047083273 -176820373 + -1194670898 -2002588448 -532938753 -1071120757 + + -3.4907507896423340e-01 4.8660326004028320e-01 + <_> + + 0 -1 44 770194932 532454301 -155316322 536753624 532750332 + -1617090311 999832316 1072434316 + + -7.5946396589279175e-01 2.3567344248294830e-01 + <_> + + 0 -1 8 -858790034 1759166227 -390697462 -84152498 2003005808 + 1459833890 -244295820 1970765431 + + -7.4084985256195068e-01 2.1838405728340149e-01 + <_> + + 0 -1 30 268440317 2069442837 291833788 899196306 1688807419 + 440540331 1403083432 870126063 + + -5.8843880891799927e-01 2.9690697789192200e-01 + <_> + + 0 -1 118 264220423 137016315 -883978220 -1712087557 + -539766463 1084217535 -325453368 -457179137 + + -5.0881922245025635e-01 3.2800257205963135e-01 + <_> + + 0 -1 84 -492052222 -746653870 -992126635 -5251052 + -1417702965 -1599993849 -477111705 1074258246 + + -4.9887087941169739e-01 3.7321901321411133e-01 + + <_> + 9 + -1.0952166318893433e+00 + + <_> + + 0 -1 14 -67174401 -341121 -747439362 -2959238 1588467868 + 1540575161 -359706659 -537763845 + + -5.1352906227111816e-01 3.3938100934028625e-01 + <_> + + 0 -1 94 -2098946 1524162928 480250392 2084999728 1539835836 + 410534280 1382295996 -1041 + + -4.2444497346878052e-01 3.6781191825866699e-01 + <_> + + 0 -1 106 -2106421449 -32833 -4169 -1150550021 -1095325697 + -16777254 -69633 -1367203038 + + -3.5375756025314331e-01 4.4176253676414490e-01 + <_> + + 0 -1 74 -211830852 977036212 -1324474212 466099646 + 1531197468 693137649 1966932760 2147483611 + + -5.3484636545181274e-01 2.9696145653724670e-01 + <_> + + 0 -1 53 -395318034 -466107909 -804811776 -218127710 + 1433621701 1155756407 -1272463945 -201328641 + + -4.2170733213424683e-01 3.7720391154289246e-01 + <_> + + 0 -1 19 -397670742 -1612936999 -1076229793 1071130368 + 689441036 -347459953 1504251647 -69385219 + + -5.1462930440902710e-01 2.9735124111175537e-01 + <_> + + 0 -1 105 -8716872 998558710 -1756428132 1057791154 728858552 + 759925180 1240457188 -1343503809 + + -4.3214339017868042e-01 3.7407690286636353e-01 + <_> + + 0 -1 0 713953058 -8196014 1102445911 1462899456 1750480879 + -70002273 -414456321 1164398675 + + -5.9928321838378906e-01 2.5606244802474976e-01 + <_> + + 0 -1 101 -1023423481 16438302 -782775987 1414519651 + 2036583974 1082048647 -815796477 1149231087 + + -5.1024430990219116e-01 3.2512000203132629e-01 + + <_> + 9 + -1.2098878622055054e+00 + + <_> + + 0 -1 114 828129277 1073495037 902635005 996941529 2139963389 + -1074492688 1073420797 -1073992196 + + -6.4098429679870605e-01 1.5371209383010864e-01 + <_> + + 0 -1 98 -2001852559 -1652591777 -549464097 -1075370221 + -1176567876 -4218881 -678250309 -1474166784 + + -4.6904677152633667e-01 3.1162470579147339e-01 + <_> + + 0 -1 96 -285506 -72338470 -118159272 1063598842 2062815416 + 755249356 1843205868 2139093755 + + -4.9528336524963379e-01 3.1505000591278076e-01 + <_> + + 0 -1 12 -428880145 -298400305 -433412890 -288428378 + 1406629637 -210544355 -178239196 -134875321 + + -4.5534977316856384e-01 3.5366109013557434e-01 + <_> + + 0 -1 50 1076150005 1576308661 1966390781 -41992271 + -1327710548 -1177534499 938254335 -1358152824 + + -3.7629887461662292e-01 4.3275776505470276e-01 + <_> + + 0 -1 72 -1073233406 -212621525 -753454245 -172756878 + -970474517 -1919709 1777054207 -467142777 + + -4.2834660410881042e-01 3.8237318396568298e-01 + <_> + + 0 -1 70 -1104674384 159417284 -1370474053 -1852796492 + -106169864 -39286580 683543517 -1449321120 + + -5.5155992507934570e-01 2.6364430785179138e-01 + <_> + + 0 -1 67 -873476314 1531589507 -233729446 1455866726 + -879542507 1427460699 -310958112 -175972465 + + -4.8735302686691284e-01 3.2814309000968933e-01 + <_> + + 0 -1 17 -167824420 529301702 286274006 -1076292717 + -731073828 361802184 -1513342056 1776401834 + + -5.2737045288085938e-01 3.0997651815414429e-01 + + <_> + 10 + -1.0168726444244385e+00 + + <_> + + 0 -1 2 -282105049 1854882015 -763108866 -24315162 1077237623 + 1593999391 -68683819 -134745097 + + -5.6601125001907349e-01 1.8559999763965607e-01 + <_> + + 0 -1 25 -1069658863 -143960619 -100935745 -2107053 -82945 + -72356689 -909156629 -2002059260 + + -3.8864189386367798e-01 3.7928998470306396e-01 + <_> + + 0 -1 113 -1051844 403755324 2090136700 1026881812 -112328761 + 479507756 -272697132 941618814 + + -5.2156370878219604e-01 2.7425044775009155e-01 + <_> + + 0 -1 64 -788467504 -112184752 1460884859 1964298582 + -249060385 -542400312 729492155 -611888945 + + -3.9346575736999512e-01 3.6530187726020813e-01 + <_> + + 0 -1 52 -163664932 931703872 1797007512 1066180504 929829884 + 493879645 432876188 2113797612 + + -6.3601201772689819e-01 2.0326934754848480e-01 + <_> + + 0 -1 89 -269490390 1153909735 -1056250586 1924030858 + 323638037 1083437111 -148909884 -744489985 + + -4.4436231255531311e-01 3.1746098399162292e-01 + <_> + + 0 -1 80 -1030758398 -1619279070 -1793862817 2008020225 + -85087332 -627838142 -243282482 1661989193 + + -4.6089529991149902e-01 2.8434169292449951e-01 + <_> + + 0 -1 47 1813451020 1060316989 -1281298465 1060333234 + -859403330 -22027843 -1790755684 -1474402292 + + -4.9409744143486023e-01 2.9811391234397888e-01 + <_> + + 0 -1 29 -620290014 -1682231266 21373082 429728255 + -1723004230 254347419 1539383790 -539496021 + + -4.2372927069664001e-01 3.5059794783592224e-01 + <_> + + 0 -1 91 1104627504 -202913830 567135735 1804026111 403270335 + -1417494079 4516271 1500482 + + -5.0134706497192383e-01 2.9041498899459839e-01 + + <_> + 10 + -1.3292946815490723e+00 + + <_> + + 0 -1 126 -8397891 1059715566 -284174916 -1089735083 + -71309331 993783025 -1615692 -1119882243 + + -5.1894056797027588e-01 2.5559258460998535e-01 + <_> + + 0 -1 117 268406023 16512863 -883751412 -1937838283 + 1508900115 211490699 -2254104 -1730678849 + + -4.8317861557006836e-01 2.8284984827041626e-01 + <_> + + 0 -1 41 996949948 -1074251786 -1927935233 -1076350024 + 2075671804 -1142087780 1073228796 419182780 + + -6.7560386657714844e-01 1.9361333549022675e-01 + <_> + + 0 -1 34 285159670 1574877663 812781612 902735252 1349253615 + 501624074 1660812238 837135102 + + -5.5906951427459717e-01 2.4412161111831665e-01 + <_> + + 0 -1 87 -401964542 -604282584 -238832173 -201336817 + 407841743 -1952461093 537912235 1076871682 + + -4.8815396428108215e-01 2.8597831726074219e-01 + <_> + + 0 -1 13 -319827993 -336706129 -464467252 -355808542 + 1880126391 2065129619 1637237623 -170393849 + + -4.3116512894630432e-01 3.1429883837699890e-01 + <_> + + 0 -1 58 -1082594312 2001260664 -1714419576 462032044 + 504642296 940394525 238295292 2140786687 + + -5.9527158737182617e-01 2.5428023934364319e-01 + <_> + + 0 -1 107 -536875509 -290797713 -836580346 1937303703 + -1143021729 1079230851 -241961232 -201326593 + + -3.8347908854484558e-01 3.8469237089157104e-01 + <_> + + 0 -1 76 24948468 1439961976 117965308 394148762 824443848 + 355535340 1576580607 2147105023 + + -5.8180338144302368e-01 2.6587575674057007e-01 + <_> + + 0 -1 79 -97412 705625872 -1928719736 991174672 1991513048 + 340794318 132452816 2074410985 + + -6.4737218618392944e-01 2.0131689310073853e-01 + + <_> + 11 + -1.3363951444625854e+00 + + <_> + + 0 -1 119 -1210061049 239064501 1036498688 48232447 -91241205 + 1571810027 -274466048 -234881025 + + -5.9806686639785767e-01 1.1762652546167374e-01 + <_> + + 0 -1 116 -2072027132 1499457101 1561809911 -608176129 + -823149942 -811340033 15530634 14679843 + + -4.8367556929588318e-01 2.6368728280067444e-01 + <_> + + 0 -1 26 -947640228 -5240282 1397146301 -1817464 -113215576 + -276022548 -1142185266 721682944 + + -5.6088328361511230e-01 2.3705115914344788e-01 + <_> + + 0 -1 83 -939475964 -1630259847 -1163364107 896693461 + -1129545224 -1163337043 -1131627606 -1476257760 + + -4.8416930437088013e-01 2.7785962820053101e-01 + <_> + + 0 -1 1 -286265553 -2097172889 -1360007282 -810752504 + 1945337719 931133431 2138466151 -134752411 + + -3.6276057362556458e-01 3.7057456374168396e-01 + <_> + + 0 -1 104 -1140933122 -1720043790 -1426186882 968100792 + -1159066977 -273792711 -1077401116 -1111601445 + + -3.9298012852668762e-01 3.4287840127944946e-01 + <_> + + 0 -1 95 -495991805 1207847255 553108994 2004613963 + 1695536911 125740555 1655957479 -204484689 + + -3.9670366048812866e-01 3.5816499590873718e-01 + <_> + + 0 -1 69 268572211 -1157685379 228540413 -1246281740 + 1073065723 -1073840132 -318914561 -104598310 + + -3.4255403280258179e-01 4.1352078318595886e-01 + <_> + + 0 -1 78 -401350528 -682952605 -767638169 -771264442 + -1383950964 -1412695392 -404776225 1345840138 + + -4.8042595386505127e-01 2.6937142014503479e-01 + <_> + + 0 -1 32 558182944 -1644488209 -774811301 -134234981 + -1848805381 -1147803949 -991183874 17965568 + + -6.9411402940750122e-01 1.7676301300525665e-01 + <_> + + 0 -1 54 -840311459 1472786284 329555577 494149437 851195388 + 788428412 937691550 -67110145 + + -3.3304396271705627e-01 4.0314087271690369e-01 + + <_> + 11 + -1.5702900886535645e+00 + + <_> + + 0 -1 111 -1056985201 1089403863 -1396998657 -665986049 + 1266152190 -860122726 -372251416 -1059062785 + + -5.3219449520111084e-01 2.0486110448837280e-01 + <_> + + 0 -1 4 782905099 -345065493 -1364269618 -353961782 + 1393776415 1458043255 -174820621 -142609545 + + -4.9447891116142273e-01 2.7227249741554260e-01 + <_> + + 0 -1 16 -606089254 925910490 -1241603426 -1076735762 + 1561271512 1304250681 497303692 1862229930 + + -4.5629221200942993e-01 2.8698876500129700e-01 + <_> + + 0 -1 66 421704177 -1683952655 2147336159 -1082880013 + -1073823745 -6570049 -17024338 -84729486 + + -3.2299223542213440e-01 3.8597777485847473e-01 + <_> + + 0 -1 115 -1749207555 998775349 -1256514305 -35727521 + 696089085 -1432598289 2043218169 -1610843912 + + -3.7526792287826538e-01 3.2450702786445618e-01 + <_> + + 0 -1 125 -545263849 -2146697218 -273416304 -50790418 + -1073746041 518324222 -33554784 -658514289 + + -3.2420372962951660e-01 4.0965199470520020e-01 + <_> + + 0 -1 100 -487594453 2026893416 387177215 1354210610 + -1031015122 -17247836 1084028367 1074782986 + + -3.6092731356620789e-01 3.6433976888656616e-01 + <_> + + 0 -1 35 2146353144 903648761 -712436227 1031184125 + 1368192253 1252083485 836306618 981267180 + + -5.4808396100997925e-01 2.0727691054344177e-01 + <_> + + 0 -1 39 -233848605 -142739610 -1384235451 2012741235 + -169505029 -361580766 1374625758 -531115930 + + -2.8540691733360291e-01 4.3118214607238770e-01 + <_> + + 0 -1 59 -270344964 2008479958 -822079716 727650460 -45194244 + 356275355 1602885720 2112421631 + + -4.5606413483619690e-01 2.6479825377464294e-01 + <_> + + 0 -1 62 -1419078664 2077620909 -845475078 -206014116 + 999095034 1532497066 749717756 -5257234 + + -3.8393145799636841e-01 3.2824596762657166e-01 + + <_> + 12 + -1.1136264801025391e+00 + + <_> + + 0 -1 14 -9 -69633 -68485377 -68485446 -172801 -536912001 + -9151014 -8193 + + -3.9596679806709290e-01 4.5984113216400146e-01 + <_> + + 0 -1 9 -269524181 1315914701 -858338706 -361857298 + 2002220407 1699956499 -674278799 -143199305 + + -5.1690953969955444e-01 2.3336707055568695e-01 + <_> + + 0 -1 85 -71849286 -14009750 2131167292 458173998 653927322 + 786276819 1088952524 2131229663 + + -4.5092019438743591e-01 2.6860737800598145e-01 + <_> + + 0 -1 18 -1566387536 958398750 -581927144 -1256505505 + -1781490504 -1456848172 -109045320 -304088401 + + -3.4108951687812805e-01 3.7153768539428711e-01 + <_> + + 0 -1 103 -16386 905453757 -216293961 -1073858611 2079972863 + 532715673 -24035334 2079799231 + + -3.9286559820175171e-01 3.1003287434577942e-01 + <_> + + 0 -1 82 -405325320 1939030768 -1882900744 931836032 + 2052894872 1029559721 1202524362 2134900734 + + -4.3752536177635193e-01 3.0235156416893005e-01 + <_> + + 0 -1 65 136184849 -42524502 1633233907 1608993695 182426623 + -1095793527 -1241308989 -1439178069 + + -4.3183043599128723e-01 3.1226927042007446e-01 + <_> + + 0 -1 46 -745823581 -214436743 -49470563 -542269649 + -295853061 -859714261 -876231724 -1073233150 + + -3.1300574541091919e-01 3.9668017625808716e-01 + <_> + + 0 -1 109 -1129675012 1066538866 -1387787522 1062903642 + -208723715 -1680193895 -1143404520 -1597050436 + + -4.1765916347503662e-01 3.0734732747077942e-01 + <_> + + 0 -1 40 -890510834 1831795832 -236676776 12903158 649249679 + 1074421867 -1339824180 -481820721 + + -4.0710410475730896e-01 3.0864900350570679e-01 + <_> + + 0 -1 21 -46191328 1971318713 -1 -100697553 -295768067 + -16777345 -1107521650 -391648248 + + -3.1451034545898438e-01 4.0722864866256714e-01 + <_> + + 0 -1 38 -128975033 -785907845 -1979322433 464517451 + -1665863986 -18882562 539409374 -937178073 + + -2.8620451688766479e-01 4.5744663476943970e-01 + + <_> + + 0 0 1 1 + <_> + + 0 1 1 1 + <_> + + 0 2 1 2 + <_> + + 0 2 1 3 + <_> + + 0 4 1 1 + <_> + + 0 5 1 1 + <_> + + 0 5 1 2 + <_> + + 0 6 1 1 + <_> + + 0 6 2 2 + <_> + + 0 7 1 1 + <_> + + 0 8 1 2 + <_> + + 0 10 1 2 + <_> + + 0 12 1 1 + <_> + + 0 13 1 1 + <_> + + 0 15 1 1 + <_> + + 0 15 2 1 + <_> + + 0 15 3 1 + <_> + + 0 15 12 1 + <_> + + 1 1 4 2 + <_> + + 1 2 6 1 + <_> + + 1 2 8 1 + <_> + + 2 0 2 1 + <_> + + 2 0 8 1 + <_> + + 2 12 9 1 + <_> + + 2 15 6 1 + <_> + + 3 0 2 1 + <_> + + 3 4 8 1 + <_> + + 3 13 7 1 + <_> + + 4 0 3 1 + <_> + + 4 9 9 2 + <_> + + 5 2 8 1 + <_> + + 5 14 7 1 + <_> + + 6 0 1 1 + <_> + + 6 0 4 1 + <_> + + 6 1 8 1 + <_> + + 6 15 1 1 + <_> + + 6 15 6 1 + <_> + + 7 0 8 2 + <_> + + 7 3 4 2 + <_> + + 7 4 7 2 + <_> + + 9 9 1 1 + <_> + + 9 15 2 1 + <_> + + 10 0 1 4 + <_> + + 10 0 8 1 + <_> + + 10 15 3 1 + <_> + + 10 15 6 1 + <_> + + 11 0 1 3 + <_> + + 11 0 8 1 + <_> + + 11 2 6 2 + <_> + + 11 3 8 1 + <_> + + 11 13 6 1 + <_> + + 12 3 6 1 + <_> + + 12 6 1 4 + <_> + + 12 9 2 1 + <_> + + 12 11 1 2 + <_> + + 13 0 1 3 + <_> + + 13 0 1 4 + <_> + + 13 0 4 1 + <_> + + 13 8 2 3 + <_> + + 13 9 1 2 + <_> + + 14 0 4 1 + <_> + + 14 4 5 1 + <_> + + 14 8 2 3 + <_> + + 14 15 4 1 + <_> + + 15 2 5 2 + <_> + + 15 3 1 1 + <_> + + 15 3 4 1 + <_> + + 15 10 2 1 + <_> + + 16 3 4 1 + <_> + + 17 3 1 1 + <_> + + 18 3 4 1 + <_> + + 18 15 3 1 + <_> + + 19 0 1 4 + <_> + + 19 5 1 3 + <_> + + 19 6 1 4 + <_> + + 19 9 1 1 + <_> + + 19 11 2 2 + <_> + + 20 1 1 3 + <_> + + 20 2 1 3 + <_> + + 20 9 1 2 + <_> + + 21 1 1 4 + <_> + + 21 6 1 4 + <_> + + 21 7 1 3 + <_> + + 21 13 3 1 + <_> + + 22 1 1 4 + <_> + + 22 7 1 3 + <_> + + 23 0 1 4 + <_> + + 23 3 1 2 + <_> + + 23 7 1 3 + <_> + + 23 9 1 1 + <_> + + 24 0 1 4 + <_> + + 24 0 4 1 + <_> + + 24 6 1 4 + <_> + + 24 9 1 1 + <_> + + 24 9 1 3 + <_> + + 25 6 1 1 + <_> + + 25 6 1 4 + <_> + + 25 8 1 3 + <_> + + 27 0 2 1 + <_> + + 27 1 1 3 + <_> + + 27 2 1 3 + <_> + + 27 5 3 2 + <_> + + 27 9 1 3 + <_> + + 27 15 1 1 + <_> + + 27 15 2 1 + <_> + + 27 15 3 1 + <_> + + 28 0 2 1 + <_> + + 28 7 1 1 + <_> + + 28 9 1 1 + <_> + + 28 15 1 1 + <_> + + 30 0 2 3 + <_> + + 30 1 2 3 + <_> + + 30 1 2 4 + <_> + + 30 12 2 2 + <_> + + 31 15 1 1 + <_> + + 32 15 1 1 + <_> + + 33 0 1 1 + <_> + + 33 1 1 2 + <_> + + 33 2 1 2 + <_> + + 33 5 1 1 + <_> + + 33 6 1 1 + <_> + + 33 6 1 2 + <_> + + 33 8 1 2 + <_> + + 33 9 1 1 + <_> + + 33 11 1 1 + <_> + + 33 11 1 2 + <_> + + 33 15 1 1 + From e1ab423d4df846cb4835ca1f10ea7a4932b6630c Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 2 Jan 2014 16:19:20 -0600 Subject: [PATCH 08/14] Added CLA --- cla.txt | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 cla.txt diff --git a/cla.txt b/cla.txt new file mode 100644 index 0000000..d5e9a2d --- /dev/null +++ b/cla.txt @@ -0,0 +1,23 @@ +In order to clarify the intellectual property license granted with Contributions from any person or entity, New Designs Unlimited, LLC. ("NDU") must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of NDU; it does not change your rights to use your own Contributions for any other purpose. + +You accept and agree to the following terms and conditions for Your present and future Contributions submitted to NDU. Except for the license granted herein to NDU and recipients of software distributed by NDU, You reserve all right, title, and interest in and to Your Contributions. + +Definitions. + +"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with NDU. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to NDU for inclusion in, or documentation of, any of the products owned or managed by NDU (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to NDU or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, NDU for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + +Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to NDU and to recipients of software distributed by NDU a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. + +Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to NDU and to recipients of software distributed by NDU a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. + +You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to NDU, or that your employer has executed a separate Corporate CLA with NDU. + +You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. + +You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +Should You wish to submit work that is not Your original creation, You may submit it to NDU separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [[]named here]". + +You agree to notify NDU of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. From 17746ef6b342c4c46ad5702736f777bd981eabf6 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 2 Jan 2014 16:27:19 -0600 Subject: [PATCH 09/14] Added gitignore --- .gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2916c99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*~ +build/ +scratch/ +/libraries/ +CMakeFiles/ +CMakeCache.txt +.o +benchmarks/ From 773d6e123fcca3c3a2bd9aef6686cb28fcec673a Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 2 Jan 2014 16:27:43 -0600 Subject: [PATCH 10/14] Added openalpr utility programs --- src/misc_utilities/CMakeLists.txt | 38 +++ src/misc_utilities/benchmark.cpp | 361 ++++++++++++++++++++ src/misc_utilities/classifychars.cpp | 355 +++++++++++++++++++ src/misc_utilities/prepcharsfortraining.cpp | 173 ++++++++++ src/misc_utilities/sortstate.cpp | 117 +++++++ 5 files changed, 1044 insertions(+) create mode 100644 src/misc_utilities/CMakeLists.txt create mode 100644 src/misc_utilities/benchmark.cpp create mode 100644 src/misc_utilities/classifychars.cpp create mode 100644 src/misc_utilities/prepcharsfortraining.cpp create mode 100644 src/misc_utilities/sortstate.cpp diff --git a/src/misc_utilities/CMakeLists.txt b/src/misc_utilities/CMakeLists.txt new file mode 100644 index 0000000..f2786b9 --- /dev/null +++ b/src/misc_utilities/CMakeLists.txt @@ -0,0 +1,38 @@ + +target_link_libraries(openalpr) + + + +ADD_EXECUTABLE( sortstate sortstate.cpp ) +TARGET_LINK_LIBRARIES(sortstate + openalpr + support + ${OpenCV_LIBS} + tesseract + ) + +ADD_EXECUTABLE( classifychars classifychars.cpp ) +TARGET_LINK_LIBRARIES(classifychars + openalpr + support + ${OpenCV_LIBS} + tesseract + ) + + +ADD_EXECUTABLE( benchmark benchmark.cpp ) +TARGET_LINK_LIBRARIES(benchmark + openalpr + support + ${OpenCV_LIBS} + tesseract + ) + + +ADD_EXECUTABLE( prepcharsfortraining prepcharsfortraining.cpp ) +TARGET_LINK_LIBRARIES(prepcharsfortraining + support + ${OpenCV_LIBS} + ) + + \ No newline at end of file diff --git a/src/misc_utilities/benchmark.cpp b/src/misc_utilities/benchmark.cpp new file mode 100644 index 0000000..364dbfc --- /dev/null +++ b/src/misc_utilities/benchmark.cpp @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + #include "opencv2/highgui/highgui.hpp" + #include "opencv2/imgproc/imgproc.hpp" + + #include + #include + #include +#include +#include // std::accumulate + +#include "alpr_impl.h" + +//#include "stage1.h" +//#include "stage2.h" +//#include "stateidentifier.h" +//#include "utility.h" +#include "support/filesystem.h" + + using namespace std; + using namespace cv; + +// Given a directory full of lp images (named [statecode]#.png) crop out the alphanumeric characters. +// These will be used to train the OCR + +void outputStats(vector datapoints); + +int main( int argc, const char** argv ) +{ + string country; + string benchmarkName; + string inDir; + string outDir; + Mat frame; + + + //Check if user specify image to process + if(argc == 5) + { + country = argv[1]; + benchmarkName = argv[2]; + inDir = argv[3]; + outDir = argv[4]; + + + }else{ + printf("Use:\n\t%s [country] [benchmark name] [img input dir] [results output dir]\n",argv[0]); + printf("\tex: %s us speed ./speed/usimages ./speed\n",argv[0]); + printf("\n"); + printf("\ttest names are: speed, segocr, detection\n\n" ); + return 0; + } + + + if (DirectoryExists(inDir.c_str()) == false) + { + printf("Input dir does not exist\n"); + return 0; + } + if (DirectoryExists(outDir.c_str()) == false) + { + printf("Output dir does not exist\n"); + return 0; + } + + + vector files = getFilesInDir(inDir.c_str()); + sort( files.begin(), files.end(), stringCompare ); + + + if (benchmarkName.compare("segocr") == 0) + { + Config* config = new Config(country); + config->debugOff(); + + OCR* ocr = new OCR(config); + + + for (int i = 0; i< files.size(); i++) + { + if (hasEnding(files[i], ".png")) + { + string fullpath = inDir + "/" + files[i]; + + frame = imread( fullpath.c_str() ); + resize(frame, frame, Size(config->ocrImageWidthPx, config->ocrImageHeightPx)); + + Rect plateCoords; + plateCoords.x = 0; + plateCoords.y = 0; + plateCoords.width = frame.cols; + plateCoords.height = frame.rows; + + char statecode[3]; + statecode[0] = files[i][0]; + statecode[1] = files[i][1]; + statecode[2] = '\0'; + string statecodestr(statecode); + + CharacterRegion charRegion(frame, config); + + if (abs(charRegion.getTopLine().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 ); + warpAffine( frame, rotated, rot_mat, frame.size() ); + + rotated.copyTo(frame); + } + + + + CharacterSegmenter charSegmenter(frame, charRegion.thresholdsInverted(), config); + ocr->performOCR(charSegmenter.getThresholds(), charSegmenter.characters); + ocr->postProcessor->analyze(statecode, 25); + + cout << files[i] << "," << statecode << "," << ocr->postProcessor->bestChars << endl; + + + imshow("Current LP", frame); + waitKey(5); + + + } + + } + + delete config; + delete ocr; + } + else if (benchmarkName.compare("detection") == 0) + { + Config config(country); + RegionDetector plateDetector(&config); + + for (int i = 0; i< files.size(); i++) + { + if (hasEnding(files[i], ".png")) + { + string fullpath = inDir + "/" + files[i]; + frame = imread( fullpath.c_str() ); + + vector regions = plateDetector.detect(frame); + + imshow("Current LP", frame); + waitKey(5); + + + } + + } + } + else if (benchmarkName.compare("speed") == 0) + { + // Benchmarks speed of region detection, plate analysis, and OCR + + timespec startTime; + timespec endTime; + + Config config(country); + config.debugOff(); + + AlprImpl alpr(country); + alpr.config->debugOff(); + alpr.setDetectRegion(true); + + RegionDetector plateDetector(&config); + StateIdentifier stateIdentifier(&config); + OCR ocr(&config); + + vector endToEndTimes; + vector regionDetectionTimes; + vector stateIdTimes; + vector lpAnalysisPositiveTimes; + vector lpAnalysisNegativeTimes; + vector ocrTimes; + vector postProcessTimes; + + + for (int i = 0; i< files.size(); i++) + { + if (hasEnding(files[i], ".png")) + { + cout << "Image: " << files[i] << endl; + + string fullpath = inDir + "/" + files[i]; + frame = imread( fullpath.c_str() ); + + + getTime(&startTime); + alpr.recognize(frame); + getTime(&endTime); + double endToEndTime = diffclock(startTime, endTime); + cout << " -- End to End recognition time: " << endToEndTime << "ms." << endl; + endToEndTimes.push_back(endToEndTime); + + getTime(&startTime); + vector regions = plateDetector.detect(frame); + getTime(&endTime); + + double regionDetectionTime = diffclock(startTime, endTime); + cout << " -- Region detection time: " << regionDetectionTime << "ms." << endl; + regionDetectionTimes.push_back(regionDetectionTime); + + for (int z = 0; z < regions.size(); z++) + { + getTime(&startTime); + char temp[5]; + stateIdentifier.recognize(frame, regions[z], temp); + getTime(&endTime); + double stateidTime = diffclock(startTime, endTime); + cout << "\tRegion " << z << ": State ID time: " << stateidTime << "ms." << endl; + stateIdTimes.push_back(stateidTime); + + getTime(&startTime); + LicensePlateCandidate lp(frame, regions[z], &config); + lp.recognize(); + getTime(&endTime); + double analysisTime = diffclock(startTime, endTime); + cout << "\tRegion " << z << ": Analysis time: " << analysisTime << "ms." << endl; + + if (lp.confidence > 10) + { + lpAnalysisPositiveTimes.push_back(analysisTime); + + + getTime(&startTime); + ocr.performOCR(lp.charSegmenter->getThresholds(), lp.charSegmenter->characters); + getTime(&endTime); + double ocrTime = diffclock(startTime, endTime); + cout << "\tRegion " << z << ": OCR time: " << ocrTime << "ms." << endl; + ocrTimes.push_back(ocrTime); + + getTime(&startTime); + ocr.postProcessor->analyze("", 25); + getTime(&endTime); + double postProcessTime = diffclock(startTime, endTime); + cout << "\tRegion " << z << ": PostProcess time: " << postProcessTime << "ms." << endl; + postProcessTimes.push_back(postProcessTime); + } + else + { + lpAnalysisNegativeTimes.push_back(analysisTime); + } + } + + waitKey(5); + + } + + } + + cout << endl << "---------------------" << endl; + + + cout << "End to End Time Statistics:" << endl; + outputStats(endToEndTimes); + cout << endl; + + cout << "Region Detection Time Statistics:" << endl; + outputStats(regionDetectionTimes); + cout << endl; + + cout << "State ID Time Statistics:" << endl; + outputStats(stateIdTimes); + cout << endl; + + cout << "Positive Region Analysis Time Statistics:" << endl; + outputStats(lpAnalysisPositiveTimes); + cout << endl; + + cout << "Negative Region Analysis Time Statistics:" << endl; + outputStats(lpAnalysisNegativeTimes); + cout << endl; + + cout << "OCR Time Statistics:" << endl; + outputStats(ocrTimes); + cout << endl; + + cout << "Post Processing Time Statistics:" << endl; + outputStats(postProcessTimes); + cout << endl; + } + else if (benchmarkName.compare("endtoend") == 0) + { + Alpr alpr(country); + alpr.setDetectRegion(true); + + ofstream outputdatafile; + + outputdatafile.open("results.txt"); + + for (int i = 0; i< files.size(); i++) + { + if (hasEnding(files[i], ".png")) + { + string fullpath = inDir + "/" + files[i]; + frame = imread( fullpath.c_str() ); + + vector buffer; + imencode(".bmp", frame, buffer ); + + vector results = alpr.recognize(buffer); + + outputdatafile << files[i] << ": "; + for (int z = 0; z < results.size(); z++) + { + outputdatafile << results[z].bestPlate.characters << ", "; + } + outputdatafile << endl; + + imshow("Current LP", frame); + waitKey(5); + + + } + + } + + outputdatafile.close(); + } + +} + +void outputStats(vector datapoints) +{ + double sum = std::accumulate(datapoints.begin(), datapoints.end(), 0.0); + double mean = sum / datapoints.size(); + + std::vector diff(datapoints.size()); + std::transform(datapoints.begin(), datapoints.end(), diff.begin(), + std::bind2nd(std::minus(), mean)); + double sq_sum = std::inner_product(diff.begin(), diff.end(), diff.begin(), 0.0); + double stdev = std::sqrt(sq_sum / datapoints.size()); + + cout << "\t" << datapoints.size() << " samples, avg: " << mean << "ms, stdev: " << stdev << endl; + +} + + diff --git a/src/misc_utilities/classifychars.cpp b/src/misc_utilities/classifychars.cpp new file mode 100644 index 0000000..ad7e22e --- /dev/null +++ b/src/misc_utilities/classifychars.cpp @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + #include "opencv2/highgui/highgui.hpp" + #include "opencv2/imgproc/imgproc.hpp" + + #include + #include +#include + +#include "regiondetector.h" +#include "licenseplatecandidate.h" +#include "stateidentifier.h" +#include "utility.h" +#include "support/filesystem.h" +#include "ocr.h" + + using namespace std; + using namespace cv; + +// Given a directory full of lp images (named [statecode]#.png) crop out the alphanumeric characters. +// These will be used to train the OCR + +const int LEFT_ARROW_KEY = 81; +const int RIGHT_ARROW_KEY = 83; +const int SPACE_KEY = 32; +const int ENTER_KEY = 10; +const int ESCAPE_KEY = 27; + +const int DOWN_ARROW_KEY = 84; +const int UP_ARROW_KEY= 82; +const int DASHBOARD_COLUMNS = 3; + +void showDashboard(vector images, vector selectedImages, int selectedIndex); +vector showCharSelection(Mat image, vector charRegions, string state); + +int main( int argc, const char** argv ) +{ + + string inDir; + string outDir; + Mat frame; + + + //Check if user specify image to process + if(argc == 3) + { + inDir = argv[1]; + outDir = argv[2]; + + + }else{ + printf("Use:\n\t%s indirectory outdirectory\n",argv[0]); + printf("Ex: \n\t%s ./pics/ ./out \n",argv[0]); + return 0; + } + + + if (DirectoryExists(outDir.c_str()) == false) + { + printf("Output dir does not exist\n"); + return 0; + } + + cout << "Usage: " << endl; + cout << "\tn -- Next plate" << endl; + cout << "\tp -- Previous plate" << endl; + cout << "\ts -- Save characters" << endl; + cout << "\t<- and -> -- Cycle between images" << endl; + cout << "\tEnt/space -- Select plate" << endl; + cout << endl; + cout << "Within a plate" << endl; + cout << "\t<- and -> -- Cycle between characters" << endl; + cout << "\t[0-9A-Z] -- Identify a character (saves the image)" << endl; + cout << "\tESC/Ent/Space -- Back to plate selection" << endl; + + Config* config = new Config("eu"); + OCR ocr(config); + + if (DirectoryExists(inDir.c_str())) + { + vector files = getFilesInDir(inDir.c_str()); + + sort( files.begin(), files.end(), stringCompare ); + + for (int i = 0; i< files.size(); i++) + { + if (hasEnding(files[i], ".png") || hasEnding(files[i], ".jpg")) + { + string fullpath = inDir + "/" + files[i]; + cout << fullpath << endl; + frame = imread( fullpath.c_str() ); + resize(frame, frame, Size(config->ocrImageWidthPx, config->ocrImageHeightPx)); + + imshow ("Original", frame); + + + char statecode[3]; + statecode[0] = files[i][0]; + statecode[1] = files[i][1]; + statecode[2] = '\0'; + string statecodestr(statecode); + + CharacterRegion regionizer(frame, config); + + if (abs(regionizer.getTopLine().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 ); + warpAffine( frame, rotated, rot_mat, frame.size() ); + + rotated.copyTo(frame); + } + + CharacterSegmenter charSegmenter(frame, regionizer.thresholdsInverted(), config); + + //ocr.cleanCharRegions(charSegmenter.thresholds, charSegmenter.characters); + + ocr.performOCR(charSegmenter.getThresholds(), charSegmenter.characters); + ocr.postProcessor->analyze(statecodestr, 25); + cout << "OCR results: " << ocr.postProcessor->bestChars << endl; + + + vector selectedBoxes(charSegmenter.getThresholds().size()); + for (int z = 0; z < charSegmenter.getThresholds().size(); z++) + selectedBoxes[z] = false; + + int curDashboardSelection = 0; + + vector humanInputs(charSegmenter.characters.size()); + + for (int z = 0; z < charSegmenter.characters.size(); z++) + humanInputs[z] = ' '; + + showDashboard(charSegmenter.getThresholds(), selectedBoxes, 0); + + + + char waitkey = (char) waitKey(50); + + while (waitkey != 'n' && waitkey != 'p') // Next image + { + if (waitkey == LEFT_ARROW_KEY) // left arrow key + { + if (curDashboardSelection > 0) + curDashboardSelection--; + showDashboard(charSegmenter.getThresholds(), selectedBoxes, curDashboardSelection); + } + else if (waitkey == RIGHT_ARROW_KEY) // right arrow key + { + if (curDashboardSelection < charSegmenter.getThresholds().size() - 1) + curDashboardSelection++; + showDashboard(charSegmenter.getThresholds(), selectedBoxes, curDashboardSelection); + } + else if (waitkey == DOWN_ARROW_KEY) + { + if (curDashboardSelection + DASHBOARD_COLUMNS <= charSegmenter.getThresholds().size() - 1) + curDashboardSelection += DASHBOARD_COLUMNS; + showDashboard(charSegmenter.getThresholds(), selectedBoxes, curDashboardSelection); + } + else if (waitkey == UP_ARROW_KEY) + { + if (curDashboardSelection - DASHBOARD_COLUMNS >= 0) + curDashboardSelection -= DASHBOARD_COLUMNS; + showDashboard(charSegmenter.getThresholds(), selectedBoxes, curDashboardSelection); + } + else if (waitkey == ENTER_KEY) + { + vector tempdata = showCharSelection(charSegmenter.getThresholds()[curDashboardSelection], charSegmenter.characters, statecodestr); + for (int c = 0; c < charSegmenter.characters.size(); c++) + humanInputs[c] = tempdata[c]; + } + else if (waitkey == SPACE_KEY) + { + selectedBoxes[curDashboardSelection] = !selectedBoxes[curDashboardSelection]; + showDashboard(charSegmenter.getThresholds(), selectedBoxes, curDashboardSelection); + } + else if (waitkey == 's' || waitkey == 'S') + { + bool somethingSelected = false; + bool chardataTagged = false; + for (int c = 0; c < charSegmenter.getThresholds().size(); c++) + { + if (selectedBoxes[c]) + { + somethingSelected = true; + break; + } + } + for (int c = 0; c < charSegmenter.characters.size(); c++) + { + if (humanInputs[c] != ' ') + { + chardataTagged = true; + break; + } + } + // Save + if (somethingSelected && chardataTagged) + { + + for (int c = 0; c < charSegmenter.characters.size(); c++) + { + if (humanInputs[c] == ' ') + continue; + + for (int t = 0; t < charSegmenter.getThresholds().size(); t++) + { + if (selectedBoxes[t] == false) + continue; + + stringstream filename; + Mat cropped = charSegmenter.getThresholds()[t](charSegmenter.characters[c]); + filename << outDir << "/" << humanInputs[c] << "-" << t << "-" << files[i]; + imwrite(filename.str(), cropped); + cout << "Writing char image: " << filename.str() << endl; + } + } + } + else if (somethingSelected == false) + cout << "Did not select any boxes" << endl; + else if (chardataTagged == false) + cout << "You have not tagged any characters" << endl; + } + + waitkey = (char) waitKey(50); + + } + + if (waitkey == 'p') + i = i - 2; + if (i < -1) + i = -1; + + + } + + } + } + +} + + +void showDashboard(vector images, vector selectedImages, int selectedIndex) +{ + vector vecCopy; + + if (selectedIndex < 0) + selectedIndex = 0; + if (selectedIndex >= images.size()) + selectedIndex = images.size() -1; + + for (int i = 0; i < images.size(); i++) + { + Mat imgCopy(images[i].size(), images[i].type()); + images[i].copyTo(imgCopy); + cvtColor(imgCopy, imgCopy, CV_GRAY2BGR); + if (i == selectedIndex) + { + rectangle(imgCopy, Point(1,1), Point(imgCopy.size().width - 1, imgCopy.size().height -1), Scalar(0, 255, 0), 1); + } + if (selectedImages[i] == true) + { + rectangle(imgCopy, Point(2,2), Point(imgCopy.size().width - 2, imgCopy.size().height -2), Scalar(255, 0, 0), 1); + } + + vecCopy.push_back(imgCopy); + } + + Mat dashboard = drawImageDashboard(vecCopy, vecCopy[0].type(), DASHBOARD_COLUMNS); + + imshow("Selection dashboard", dashboard); +} + +vector showCharSelection(Mat image, vector charRegions, string state) +{ + int curCharIdx = 0; + + vector humanInputs(charRegions.size()); + for (int i = 0; i < charRegions.size(); i++) + humanInputs[i] = (char) SPACE_KEY; + + + char waitkey = (char) waitKey(50); + while (waitkey != ENTER_KEY && waitkey != ESCAPE_KEY) + { + Mat imgCopy(image.size(), image.type()); + image.copyTo(imgCopy); + cvtColor(imgCopy, imgCopy, CV_GRAY2BGR); + + rectangle(imgCopy, charRegions[curCharIdx], Scalar(0, 255, 0), 1); + + imshow("Character selector", imgCopy); + + if (waitkey == LEFT_ARROW_KEY) + curCharIdx--; + else if (waitkey == RIGHT_ARROW_KEY ) + curCharIdx++; + else if ((waitkey >= '0' && waitkey <= '9') || (waitkey >= 'a' && waitkey <= 'z') || waitkey == SPACE_KEY) + { + // Save the character to disk + humanInputs[curCharIdx] = toupper((char) waitkey); + curCharIdx++; + + if (curCharIdx >= charRegions.size()) + { + waitkey = (char) ENTER_KEY; + break; + } + } + + if (curCharIdx < 0) + curCharIdx = 0; + if (curCharIdx >= charRegions.size()) + curCharIdx = charRegions.size() -1; + + waitkey = (char) waitKey(50); + } + + if (waitkey == ENTER_KEY) + { + // Save all the inputs + for (int i = 0; i < charRegions.size(); i++) + { + if (humanInputs[i] != (char) SPACE_KEY) + cout << "Tagged " << state << " char code: '" << humanInputs[i] << "' at char position: " << i << endl; + } + + } + + destroyWindow("Character selector"); + + + return humanInputs; +} \ No newline at end of file diff --git a/src/misc_utilities/prepcharsfortraining.cpp b/src/misc_utilities/prepcharsfortraining.cpp new file mode 100644 index 0000000..2ed31fd --- /dev/null +++ b/src/misc_utilities/prepcharsfortraining.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + #include "opencv2/highgui/highgui.hpp" + #include "opencv2/imgproc/imgproc.hpp" + + #include + #include +#include +#include +#include "support/filesystem.h" + + using namespace std; + using namespace cv; + + + +// Takes a directory full of single char images, and plops them on a big tif files +// Also creates a box file so Tesseract can recognize it +int main( int argc, const char** argv ) +{ + + string inDir; + + + //Check if user specify image to process + if(argc == 2) + { + inDir = argv[1]; + + }else{ + printf("Use:\n\t%s input dir \n",argv[0]); + return 0; + } + + + if (DirectoryExists(inDir.c_str()) == false) + { + printf("Output dir does not exist\n"); + return 0; + } + + cout << "Usage: " << endl; + cout << "\tinputdir -- input dir for benchmark data" << endl; + + + if (DirectoryExists(inDir.c_str())) + { + const int X_OFFSET = 10; + const int Y_OFFSET = 10; + + const int PAGE_MARGIN_X = 70; + const int PAGE_MARGIN_Y = 70; + const int HORIZONTAL_RESOLUTION = 3500; + + const int TILE_WIDTH = 55; + const int CHAR_HORIZ_OFFSET = 40; + const int TILE_HEIGHT = 70; + const int CHAR_VERT_OFFSET = 48; + + vector files = getFilesInDir(inDir.c_str()); + + sort( files.begin(), files.end(), stringCompare ); + + + int tiles_per_row = ((float) (HORIZONTAL_RESOLUTION - (PAGE_MARGIN_X * 2))) / ((float) TILE_WIDTH); + int lines = files.size() / (tiles_per_row); + int vertical_resolution = (lines * TILE_HEIGHT) + (PAGE_MARGIN_Y * 2) ; + cout << tiles_per_row << " : " << vertical_resolution << endl; + + Mat bigTif = Mat::zeros(Size(HORIZONTAL_RESOLUTION, vertical_resolution), CV_8U); + bitwise_not(bigTif, bigTif); + + stringstream boxFileOut; + + for (int i = 0; i< files.size(); i++) + { + int col = i % tiles_per_row; + int line = i / tiles_per_row; + + int xPos = (col * TILE_WIDTH) + PAGE_MARGIN_X; + int yPos = (line * TILE_HEIGHT) + PAGE_MARGIN_Y; + + if (hasEnding(files[i], ".png") || hasEnding(files[i], ".jpg")) + { + string fullpath = inDir + "/" + files[i]; + + cout << "Processing file: " << (i + 1) << " of " << files.size() << endl; + + char charcode = files[i][0]; + + Mat characterImg = imread(fullpath); + Mat charImgCopy = Mat::zeros(Size(150, 150), characterImg.type()); + bitwise_not(charImgCopy, charImgCopy); + + characterImg.copyTo(charImgCopy(Rect(X_OFFSET, Y_OFFSET, characterImg.cols, characterImg.rows))); + cvtColor(charImgCopy, charImgCopy, CV_BGR2GRAY); + bitwise_not(charImgCopy, charImgCopy); + + vector > contours; + + //imshow("copy", charImgCopy); + findContours(charImgCopy, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE); + + Rect tallestRect(0, 0, 0, 0); + for (int c = 0; c < contours.size(); c++) + { + Rect tmpRect = boundingRect(contours[c]); + if (tmpRect.height > tallestRect.height) + tallestRect = tmpRect; + } + + //cout << tallestRect.x << ":" << tallestRect.y << " -- " << tallestRect.width << ":" << tallestRect.height << endl; + + Rect cropRect(0, tallestRect.y - Y_OFFSET, tallestRect.width, tallestRect.height); + + + //cout << "Cropped: " << cropRect.x << ":" << cropRect.y << " -- " << cropRect.width << ":" << cropRect.height << endl; + Mat cropped(characterImg, cropRect); + cvtColor(cropped, cropped, CV_BGR2GRAY); + + Rect destinationRect(xPos + (CHAR_HORIZ_OFFSET - tallestRect.width), yPos + (CHAR_VERT_OFFSET - tallestRect.height), tallestRect.width, tallestRect.height); + + //cout << "1" << endl; + + cropped.copyTo(bigTif(destinationRect)); + + + int x1= destinationRect.x - 2; + int y1 = (vertical_resolution - destinationRect.y - destinationRect.height) - 2; + int x2 = (destinationRect.x + destinationRect.width) + 2; + int y2 = (vertical_resolution - destinationRect.y) + 2; + //0 70 5602 85 5636 0 + boxFileOut << charcode << " " << x1 << " " << y1 << " "; + boxFileOut << x2 << " " << y2 ; + boxFileOut << " 0" << endl; + + //rectangle(characterImg, tallestRect, Scalar(0, 255, 0)); + //imshow("characterImg", cropped); + + waitKey(2); + + + } + + } + + + imwrite("combined.tif", bigTif); + ofstream boxFile("combined.box", std::ios::out); + boxFile << boxFileOut.str(); + + } + +} + + diff --git a/src/misc_utilities/sortstate.cpp b/src/misc_utilities/sortstate.cpp new file mode 100644 index 0000000..d71dab3 --- /dev/null +++ b/src/misc_utilities/sortstate.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2013 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + + #include "opencv2/highgui/highgui.hpp" + #include "opencv2/imgproc/imgproc.hpp" + + #include + #include +#include + +#include "regiondetector.h" +#include "licenseplatecandidate.h" +#include "stateidentifier.h" +#include "utility.h" +#include "support/filesystem.h" + + using namespace std; + using namespace cv; + +// Given a directory full of pre-cropped images, identify the state that each image belongs to. +// This is used to sort our own positive image database as a first step before grabbing characters to use to train the OCR. + + bool detectPlate( StateIdentifier* identifier, Mat frame); + +int main( int argc, const char** argv ) +{ + + string inDir; + string outDir; + Mat frame; + + + //Check if user specify image to process + if(argc == 3 ) + { + inDir = argv[1]; + outDir = argv[2]; + outDir = outDir + "/"; + + + }else{ + printf("Use:\n\t%s directory \n",argv[0]); + printf("Ex: \n\t%s ./pics/ \n",argv[0]); + return 0; + } + + Config config("us"); + StateIdentifier identifier(&config); + + if (DirectoryExists(outDir.c_str()) == false) + { + printf("Output dir does not exist\n"); + return 0; + } + + if (DirectoryExists(inDir.c_str())) + { + vector files = getFilesInDir(inDir.c_str()); + + for (int i = 0; i< files.size(); i++) + { + if (hasEnding(files[i], ".png")) + { + string fullpath = inDir + "/" + files[i]; + cout << fullpath << endl; + frame = imread( fullpath.c_str() ); + + char code[4]; + int confidence = identifier.recognize(frame, code); + + if (confidence <= 20) + { + code[0] = 'z'; + code[1] = 'z'; + confidence = 100; + } + + //imshow("Plate", frame); + if (confidence > 20) + { + cout << confidence << " : " << code; + + ostringstream convert; // stream used for the conversion + convert << i; // insert the textual representation of 'Number' in the characters in the stream + + + string copyCommand = "cp \"" + fullpath + "\" " + outDir + code + convert.str() + ".png"; + system( copyCommand.c_str() ); + waitKey(50); + //while ((char) waitKey(50) != 'c') { } + } + else + waitKey(50); + } + + } + } + +} + + bool detectPlate( StateIdentifier* identifier, Mat frame); \ No newline at end of file From a866831eb5bac0f863378fad56939874a6ba77b3 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Sat, 4 Jan 2014 01:10:11 -0600 Subject: [PATCH 11/14] Added blank keypoints file for EU plates to prevent region detection from crashing --- runtime_data/keypoints/eu/fr.jpg | Bin 0 -> 27329 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 runtime_data/keypoints/eu/fr.jpg diff --git a/runtime_data/keypoints/eu/fr.jpg b/runtime_data/keypoints/eu/fr.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8db1aeb1cf2b669b7be47437d534656bd198cb28 GIT binary patch literal 27329 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 Date: Wed, 8 Jan 2014 17:54:34 -0600 Subject: [PATCH 12/14] Slightly improved EU plate region detection (LBP using increased # of samples) --- runtime_data/region/eu.xml | 664 +++++++++++++++++++------------------ 1 file changed, 335 insertions(+), 329 deletions(-) diff --git a/runtime_data/region/eu.xml b/runtime_data/region/eu.xml index b9463de..1d0c0a3 100644 --- a/runtime_data/region/eu.xml +++ b/runtime_data/region/eu.xml @@ -20,543 +20,549 @@ <_> 5 - -1.2342801094055176e+00 + -1.0891276597976685e+00 <_> - 0 -1 30 352328156 -192015 -15754753 -173569 -1164334081 + 0 -1 28 352329180 -175631 -15754753 -173569 -1164334081 -5526021 -1649426433 -1073862520 - -8.3821654319763184e-01 4.5727136731147766e-01 + -8.2864367961883545e-01 4.8592057824134827e-01 <_> - 0 -1 4 -286263569 -2102402322 -534582034 -353374577 -2098177 + 0 -1 4 -286263569 -2102402321 -534598418 -353374577 -2098177 2011675391 -1473 -150996993 - -7.1907609701156616e-01 5.3620684146881104e-01 + -6.9584220647811890e-01 5.5770379304885864e-01 <_> - 0 -1 17 -1679491076 -36390424 -1764698371 1071385316 - -139804689 -4539205 -1078198529 -1882715412 + 0 -1 21 -1658523684 -3753748 -1638885633 -1073860924 + -148705297 -4219205 -1078266113 -1356330260 - -6.6871184110641479e-01 5.8701753616333008e-01 + -6.7338258028030396e-01 5.8551847934722900e-01 <_> - 0 -1 42 806360401 -8441479 -41 2145992703 -1077740829 - -69494529 -1382096961 1035868412 + 0 -1 43 823137617 -8441479 -41 -1228801 -1077740829 + -69494529 -1348542529 -1111619332 - -7.2897046804428101e-01 4.8680499196052551e-01 + -6.1313426494598389e-01 5.7877427339553833e-01 <_> - 0 -1 58 -1050130 181039615 1040326952 61194898 1532960676 - 589570476 2004968781 2145386463 + 0 -1 57 -1050130 181039615 1040326952 61194906 1532956580 + 640426412 1996563789 2145386463 - -7.2125834226608276e-01 5.0491940975189209e-01 + -7.2357940673828125e-01 5.3356456756591797e-01 <_> - 3 - -1.1206305027008057e+00 + 5 + -9.8854637145996094e-01 <_> - 0 -1 75 -3325 -300158306 -1055923968 -1051789814 -15487472 - 26714273 -356277760 -134218769 + 0 -1 74 -253 -359664761 -1022376704 -942685554 -77862893 + 1391706287 -203955712 -134217729 - -8.3067792654037476e-01 4.7307375073432922e-01 + -7.9230999946594238e-01 5.3734940290451050e-01 <_> - 0 -1 22 -2144729862 -1107754033 -2146876419 -7702187 - -1144850757 -878974541 -1555824641 -1414656342 + 0 -1 18 -2103041794 -2420585 -292465441 -8957641 -1147163905 + -71444327 -1123811329 -1950445889 - -7.7843278646469116e-01 4.9317824840545654e-01 + -6.7109984159469604e-01 5.2774083614349365e-01 <_> - 0 -1 25 -403181842 653246531 -522927960 -1466965298 - -162827985 1387782497 -31897673 -201331721 + 0 -1 26 -958403858 537524426 -1073003830 -1563432246 + 275475263 169722019 -739266557 -204472333 - -7.6949650049209595e-01 4.8848018050193787e-01 + -6.2689256668090820e-01 5.7461816072463989e-01 + <_> + + 0 -1 48 -286267614 -1567719296 -1045512160 37978761 + -72244469 575078921 38732035 -469763093 + + -5.4586493968963623e-01 5.7905143499374390e-01 + <_> + + 0 -1 52 -1627890276 -539224611 424991221 -35959907 -41645316 + -1154983528 1058863805 -1097069384 + + -6.5484923124313354e-01 5.2270460128784180e-01 <_> - 5 - -1.1834602355957031e+00 + 4 + -1.8370269536972046e+00 <_> - 0 -1 34 1969028604 -174603 -1760004131 -2419235 -660035078 - -345927 -1685546577 -1081209844 + 0 -1 37 1431346684 -537013775 -1123001891 -2162347 + -1702327825 -4347461 -1953956866 -1089984500 - -8.2657516002655029e-01 4.6755406260490417e-01 + -8.2538139820098877e-01 4.0558964014053345e-01 <_> - 0 -1 13 -1815816452 -539327252 -972892705 -652156 - -1424303361 -36085509 -83694673 -1102173014 + 0 -1 14 -1832854276 -544078180 -1811917825 -141162300 + -1954358273 -41221669 -1426021649 -1163646968 - -7.0588159561157227e-01 5.1438063383102417e-01 + -7.3796498775482178e-01 4.7394013404846191e-01 <_> - 0 -1 57 67119184 -103071403 -576989217 1967067647 - -1147610369 -1141124982 -1184727105 -1681315656 + 0 -1 1 -2102754562 -67680845 -112394849 -141565556 + -1592870465 -4200277 -540738561 -1351728664 - -6.7077225446701050e-01 4.9133408069610596e-01 + -6.2538594007492065e-01 5.4038310050964355e-01 <_> - 0 -1 33 525342096 1604130064 1427685243 -6342179 -1713620348 - -277141107 -1895198281 -1952499204 + 0 -1 51 268443088 2144609117 -1754192003 1904871869 + 312118475 -72677336 -1987240737 780406832 - -6.2518852949142456e-01 5.1328247785568237e-01 - <_> - - 0 -1 55 -421271550 -1071591260 -788168569 1657793455 - -497851773 671337482 -475700442 1904211543 - - -6.5562003850936890e-01 5.0863337516784668e-01 + -8.2318174839019775e-01 3.5170540213584900e-01 <_> 5 - -1.4445747137069702e+00 + -1.3525897264480591e+00 <_> - 0 -1 12 -1534405121 -7603749 1997535231 -545009153 - -640697349 -5256817 -272638977 -1427382849 + 0 -1 13 -1508977665 -1057447 -34121761 -540319749 -569640001 + -1339237 -342098945 -1364206081 - -7.8040641546249390e-01 4.3654364347457886e-01 + -7.5512629747390747e-01 5.0126796960830688e-01 <_> - 0 -1 32 1573951736 -732840 482392825 -93088771 -1162303268 - -1074128191 -1618162693 1065880648 + 0 -1 33 520099280 1597862321 898108883 -18035 1485454798 + -273143652 -14050881 -1613809444 - -8.0440253019332886e-01 3.4074461460113525e-01 + -6.4212208986282349e-01 4.7490003705024719e-01 <_> - 0 -1 50 -196868 -1081042020 -652693028 993800656 -1098090051 - 465070273 197966809 -1089 + 0 -1 56 547364912 -2395823 -114049065 1964708223 -1171563521 + -5256977 -1115117137 -1678169866 - -6.0474485158920288e-01 4.6314033865928650e-01 + -6.5468692779541016e-01 4.2123568058013916e-01 <_> - 0 -1 35 806409680 -537310891 33015157 1507693981 -1832731928 - -14137270 -1129420100 409740064 + 0 -1 63 -21056708 1040462754 926941452 390376382 1332548548 + 492073200 1597633341 2138045439 - -7.3604851961135864e-01 4.1595551371574402e-01 + -7.5756329298019409e-01 3.3693149685859680e-01 <_> - 0 -1 23 -957878746 -1999965426 -2045851578 -1773288254 - -92571682 1243334953 -1858941089 1204805279 + 0 -1 6 -286610690 7889566 -923630852 -281375265 -643215128 + 397426828 1607180287 -554238081 - -7.1337687969207764e-01 4.1314238309860229e-01 + -6.8040394783020020e-01 3.8098624348640442e-01 <_> - 6 - -1.7404717206954956e+00 + 5 + -1.1867469549179077e+00 <_> - 0 -1 1 -290783233 -8662063 -583139329 -534081 -1446248449 - -6302979 -81007617 -286262273 + 0 -1 0 -823459841 -8924257 -619054113 -4194819 -1466958849 + -2367267 -75760641 -823207425 - -7.2589415311813354e-01 4.9178531765937805e-01 + -7.2308593988418579e-01 4.5390692353248596e-01 <_> - 0 -1 74 -245 -281018657 -145631488 -79243094 -67380702 - 1139491271 -254548224 -134742081 + 0 -1 72 -608178429 -874522369 -62262784 -486606033 -20972793 + 293317947 -234882267 -134221857 - -5.6722396612167358e-01 5.1794242858886719e-01 + -6.1693340539932251e-01 4.7333467006683350e-01 <_> - 0 -1 16 -1410585858 -11625066 -1592084291 1071133834 - -75633666 -1156933509 -1650494993 -1965380226 + 0 -1 24 -282542852 -14125448 -607988225 985927440 + -1076299265 -71640899 2145434619 -1968559064 - -5.7949805259704590e-01 4.6442687511444092e-01 + -6.7338615655899048e-01 4.0413773059844971e-01 <_> - 0 -1 41 276829264 -6467111 -547684897 -538763841 -1700545041 - -69220102 -1380516933 -1122040423 + 0 -1 55 -1071390686 -54536730 -2119197217 -171443501 + -307238675 -334529970 1733814651 1110426695 - -6.5663957595825195e-01 3.7706291675567627e-01 + -5.9707373380661011e-01 4.2942461371421814e-01 <_> - 0 -1 45 -822084950 549227283 -1023008128 263662540 907280548 - 357251361 794240313 1988100095 + 0 -1 35 1900082676 -552307788 622089147 2073940261 + -1113802728 -2520758 933810168 283651232 - -6.9282585382461548e-01 4.0284207463264465e-01 - <_> - - 0 -1 63 -7608534 1707979879 1950662688 1930289631 927414213 - 1145794859 -206058507 -207093825 - - -6.0658127069473267e-01 3.8594201207160950e-01 + -6.8028992414474487e-01 3.9772990345954895e-01 <_> 6 - -1.7376002073287964e+00 + -1.5750128030776978e+00 <_> - 0 -1 15 -67113473 -50397705 -671624993 -33621601 -36703233 - -33825569 -536871937 -1 + 0 -1 16 -33556993 -52634121 -545 -33554497 -1572865 + -35925285 -1 -1 - -6.9109666347503662e-01 6.3259667158126831e-01 + -6.4789474010467529e-01 6.8716579675674438e-01 <_> - 0 -1 73 -67375357 -438349313 -82707456 -495457425 -142611689 - 1652538223 -104080572 -138412593 + 0 -1 45 276824529 -1073785419 -44370465 -12895747 + -1159143426 -1078200165 -561136897 -1080355410 - -6.1666935682296753e-01 4.3335196375846863e-01 + -5.9955614805221558e-01 4.4649451971054077e-01 <_> - 0 -1 43 402657488 -44578 -648282633 -134785097 -1410444545 - -4211781 -1648690977 -1682372168 + 0 -1 2 -353383921 1651439343 -875639586 -892410273 + 1191004023 1369560507 -1749073306 -176171433 - -6.8725508451461792e-01 4.1583669185638428e-01 + -5.0599515438079834e-01 4.9024388194084167e-01 <_> - 0 -1 28 -279164164 -1074989448 -1655775393 -1159327304 - 995687420 -1410851 2070855679 -1970787324 + 0 -1 47 530060796 -1653009667 448302035 521999321 2016688604 + -1074226724 -1618575621 -1073853763 - -7.3297709226608276e-01 3.7756869196891785e-01 + -5.9446120262145996e-01 4.2087084054946899e-01 <_> - 0 -1 5 -403189969 -1361908067 1116678830 -386513329 - 1742103151 1414519535 1946120914 -148111428 + 0 -1 38 -1071136254 1223685519 1471152063 -134250673 + 1994376079 -345059705 -744244737 1109902082 - -5.0710958242416382e-01 4.9361771345138550e-01 + -6.2733697891235352e-01 3.7706780433654785e-01 <_> - 0 -1 47 489953432 1071731312 -1554270735 2134729717 - 1527790300 2078153336 -1157828613 -549552899 + 0 -1 66 -289474716 492493535 1633723308 663684984 -706944683 + 469863266 1912419244 1862268927 - -6.5112805366516113e-01 3.9441567659378052e-01 + -5.9589976072311401e-01 3.8489931821823120e-01 <_> - 6 - -1.4720557928085327e+00 + 7 + -1.4239969253540039e+00 <_> - 0 -1 71 -249 2073550167 -1086088193 -9729 -135266717 - -2387277 -89130546 -67109889 + 0 -1 70 -2097401 2147480859 -1686906241 -537929249 + -208950013 -70520649 -2360434 -67109889 - -7.1864211559295654e-01 4.3991416692733765e-01 + -6.9891244173049927e-01 4.6869069337844849e-01 <_> - 0 -1 21 -65537 -1073816065 788447231 -1078805068 -65537 -2 - -16844801 -1073807905 + 0 -1 19 -293603474 181316838 -609486917 -28311826 -111673345 + -31469125 -5506049 -19922945 - -4.9692794680595398e-01 5.7111859321594238e-01 + -5.4466742277145386e-01 4.3645247817039490e-01 <_> - 0 -1 6 -286265361 -522198801 -300226866 -824457913 - 1999336254 1165174031 -75536539 -134217733 + 0 -1 32 2108988376 -1073981975 923187955 -29909155 -72553316 + -1683061819 -1683427877 529006664 - -5.0260621309280396e-01 4.8911646008491516e-01 + -6.8817603588104248e-01 3.2912179827690125e-01 <_> - 0 -1 60 134224976 -42115 503045631 -104909380 -1423213585 - -1093682593 -1346775649 503843200 + 0 -1 49 -270538870 -389823518 55248416 -1031340886 + -643591296 4219461 -405545198 1395127295 - -8.1539380550384521e-01 2.9674032330513000e-01 + -6.8365395069122314e-01 3.0457839369773865e-01 <_> - 0 -1 27 -606607953 147439780 -123759474 148890575 - -1222134481 -265879523 -738331785 -100663425 + 0 -1 27 -1998585712 -3221643 -2121639969 -809041 -346030083 + -23343465 -1078461833 -1415313396 - -4.9730581045150757e-01 4.8398590087890625e-01 + -6.5273976325988770e-01 3.6138573288917542e-01 <_> - 0 -1 59 -939031293 -134221841 -5 -268436481 -67108929 -1 -1 - -1071651065 + 0 -1 64 -2113681918 -1294213469 -132286017 -712527217 + -916456545 -119827030 -913585586 1075835683 - -4.1419434547424316e-01 5.8424508571624756e-01 + -5.8287250995635986e-01 3.8705006241798401e-01 + <_> + + 0 -1 30 -276836593 -1965109297 -765473682 -453055127 + -628622583 406486027 -683059631 -245368065 + + -5.3347849845886230e-01 4.2466723918914795e-01 <_> - 7 - -1.9503176212310791e+00 + 6 + -1.9118229150772095e+00 <_> - 0 -1 77 -67108933 -541065249 -610542320 -1049601 -289407299 - -572527621 -75498256 -1 + 0 -1 77 -209 -273681601 -1215703776 -69208333 -67109083 + 398458087 -71304188 -167773185 - -6.8429845571517944e-01 4.5343136787414551e-01 + -7.7420461177825928e-01 1.9384615123271942e-01 <_> - 0 -1 18 -1431832585 -9502721 2147483647 -34603011 -840958017 - -268435457 -1094713345 -1397818113 + 0 -1 11 -16844833 -1610869765 -1139346433 531641518 + -1698705665 -72641841 -270861569 -18153985 - -4.7899335622787476e-01 5.1089739799499512e-01 + -5.1794987916946411e-01 5.1850950717926025e-01 <_> - 0 -1 54 -1067203069 -536870985 -4097 -1 -268437505 -1 - -38019153 -1039416409 + 0 -1 8 -890320154 1616805846 -569442322 -51972884 1983217523 + 1882690923 -149225483 -134743041 - -3.8713064789772034e-01 5.8315634727478027e-01 + -6.4354902505874634e-01 3.4861961007118225e-01 <_> - 0 -1 44 1043073500 -1619165736 -1765999621 -11021831 - -1678714630 -229827 869072861 957881548 + 0 -1 53 -545997892 2146712560 1027094448 1072698174 + 987312028 1072971401 2141460220 1073741823 - -7.2454583644866943e-01 3.4410527348518372e-01 + -7.3423033952713013e-01 2.7811017632484436e-01 <_> - 0 -1 62 -268507652 871379856 -1983639004 1038103444 - -1082884100 260856189 458107068 2146402047 + 0 -1 61 -839913558 1383071418 -1031696629 177087465 + -2124516560 406474775 -750558174 57146367 - -6.6823595762252808e-01 3.6522203683853149e-01 + -6.6036331653594971e-01 3.1190189719200134e-01 <_> - 0 -1 2 -285227361 -1565854002 -1366888194 -2033032641 - 1475044343 1702916555 -208944208 -143661409 + 0 -1 62 -1084269292 -15240231 -655132905 -8584425 + -1433421644 -1610766851 501946608 -1665981136 - -4.8592305183410645e-01 4.9365350604057312e-01 - <_> - - 0 -1 49 -3154 674558130 -909431384 1740634054 1667371440 - 873989589 1381178774 1610071935 - - -6.2329936027526855e-01 3.7792292237281799e-01 + -5.6968742609024048e-01 4.0613368153572083e-01 <_> 7 - -1.3005143404006958e+00 + -1.9141877889633179e+00 <_> - 0 -1 8 -150997249 -186651393 -51909121 -264193 -70542337 - -138413329 -3277825 -1310801 + 0 -1 69 -249 -671092433 -207620097 -1058825 -204472321 + -268437547 -3407873 -1048593 - -6.7938089370727539e-01 4.6835443377494812e-01 + -6.5279299020767212e-01 4.8660713434219360e-01 <_> - 0 -1 29 -1595932528 -10275 -371240481 -2902153 -352329729 - -89138473 -1684616193 -1142150132 + 0 -1 23 -65537 -1073840649 -1090600999 -1112359043 + -268509457 -196609 -1090666497 -1075118081 - -6.8929862976074219e-01 2.8295361995697021e-01 + -4.4178628921508789e-01 5.2866697311401367e-01 <_> - 0 -1 76 -75501649 -1478560481 -78783200 -669057349 - -285343931 115340787 -136381440 -170655777 + 0 -1 54 -1610382333 -536871001 -4097 -134221825 -268437505 + -1 -272900165 -2110004729 - -5.4028475284576416e-01 4.0457248687744141e-01 + -4.3439030647277832e-01 4.9104723334312439e-01 <_> - 0 -1 66 -135604068 -1616889908 176728724 1050470380 - -410239956 1860975396 986186492 2146959103 + 0 -1 29 -645621284 -74591240 1734207223 -241348156 + -1428227122 -73901090 -1222624529 339740672 - -6.4993071556091309e-01 3.1024640798568726e-01 + -6.9722545146942139e-01 3.1018492579460144e-01 <_> - 0 -1 53 -37815298 -1756071972 932584732 502929916 - -1620288551 714890421 1005624314 -83203 + 0 -1 7 -957363474 1145487462 -840710418 -20520813 1933828693 + 1729360047 -1162608945 -705762305 - -4.8649874329566956e-01 4.4701680541038513e-01 + -6.5354657173156738e-01 3.5359036922454834e-01 <_> - 0 -1 68 -1071175933 -1 -129 -4101 -390074625 -268468225 - -1025 -2111092861 + 0 -1 59 -1118220 1072477084 -716423692 900504924 407658456 + 713846217 1067723772 -537921537 - -4.5232096314430237e-01 4.7892215847969055e-01 + -5.8904892206192017e-01 3.6263024806976318e-01 <_> - 0 -1 3 -1075665154 -12306554 151832703 -720964926 - -2003879185 -71727650 -742296849 715269738 + 0 -1 73 -16781553 -504110849 -137626336 2020002273 + -1549809821 1108234023 -176708078 -439034884 - -6.1241555213928223e-01 3.7678867578506470e-01 + -5.1023495197296143e-01 4.1906473040580750e-01 <_> 7 - -1.3320474624633789e+00 + -1.3252449035644531e+00 <_> - 0 -1 70 -2297 -2201 -606217217 -536871013 -605029633 - -605045341 -84149761 -1 + 0 -1 17 -1364197385 -8365 -8388609 -36725793 -308288513 + -2169185 -1323009 -1364198401 - -6.7704600095748901e-01 4.7368422150611877e-01 + -6.9964027404785156e-01 3.1848186254501343e-01 <_> - 0 -1 20 -268437585 715893674 -980686082 -1162892626 - 2147438079 -604766342 -2411555 -33554657 + 0 -1 10 235841278 -11560137 -1781388161 -137386726 + 1574787071 -1615148845 -1610863873 -9460806 - -5.0731199979782104e-01 4.4156819581985474e-01 + -5.4633712768554688e-01 3.4596458077430725e-01 <_> - 0 -1 10 185116406 -1615877553 -1345158081 -1115716262 - 2044668927 2141941247 -873464833 -415058 + 0 -1 68 -1066417145 -2049 -536870913 -33033 -142608385 -17 + -67125761 -1036794873 - -5.0882929563522339e-01 3.9274227619171143e-01 + -3.7736514210700989e-01 5.1756191253662109e-01 <_> - 0 -1 40 -416551126 541063119 1720361486 1941921098 928217927 - 1077374554 -470371167 -169607201 + 0 -1 46 -8736836 925633468 -1649269292 330746758 1061227216 + 1604410751 958224828 2113929211 - -5.6418907642364502e-01 3.5969066619873047e-01 + -6.0834312438964844e-01 3.2764193415641785e-01 <_> - 0 -1 39 -2144965630 -1786853385 983290681 -193990065 - -1435593777 -874870781 -148652293 1076348426 + 0 -1 58 134217761 -145820769 -918561281 -32565 -342885637 + -1104166501 -276054129 1059268039 - -6.1384546756744385e-01 3.4418886899948120e-01 + -6.9328433275222778e-01 2.9834014177322388e-01 <_> - 0 -1 14 1324412412 -35863564 -535364629 -1764835842 - 2115366735 -1212035965 -8000562 -33887267 + 0 -1 25 -491526289 13550039 -471015962 -520687409 1891889001 + 1848636827 -195366252 -67117217 - -5.4024517536163330e-01 3.6199000477790833e-01 + -5.0798887014389038e-01 4.2228734493255615e-01 <_> - 0 -1 37 -120393908 971551622 -1026612532 804784283 701349152 - 787773517 723802396 2147483639 + 0 -1 67 -63500032 2073776991 -648816647 -681346729 + -1731736049 -895557187 -661139524 403862288 - -6.4826697111129761e-01 3.3292853832244873e-01 + -6.5347433090209961e-01 3.0001255869865417e-01 <_> 7 - -1.6739253997802734e+00 + -1.4286619424819946e+00 <_> - 0 -1 24 -33557249 83864537 -543172129 -536871533 -16793857 - -321942533 -18096129 -1061 + 0 -1 15 -1537 341630967 -644612737 -41945765 -67904561 + -67114549 -71570945 -33554433 - -6.8337130546569824e-01 3.4821429848670959e-01 + -6.7523366212844849e-01 3.1159418821334839e-01 <_> - 0 -1 64 -8429772 -1080077475 -217843395 -1078101315 - -73680708 -6723397 -1661387332 -1648672976 + 0 -1 50 -81924 1073219548 -1656450052 531635190 797730296 + 529286869 453778140 -16465 - -5.9955561161041260e-01 3.2578879594802856e-01 + -6.2270468473434448e-01 2.5747936964035034e-01 <_> - 0 -1 48 -127905580 -79939080 -1108012101 -537627339 - -1298089842 -76845170 -1912940051 85983264 + 0 -1 3 -6095106 -540174978 -880374563 -147402106 -1426218753 + -72770389 -102766085 -1431346578 - -7.3458421230316162e-01 2.5306507945060730e-01 + -4.5853179693222046e-01 4.0388166904449463e-01 <_> - 0 -1 52 17 -2246729 -777270789 -539231201 -1081169169 - -76884579 -1748256325 683018597 + 0 -1 65 458754 -1277427969 -115352577 -2124241 -72647681 + -607148621 -38831637 1073971266 - -7.9745399951934814e-01 2.6082354784011841e-01 + -7.1188706159591675e-01 2.6471686363220215e-01 <_> - 0 -1 9 -890241298 -463670186 -795681442 -51970621 1909811571 - -114822853 -24184323 -50869249 + 0 -1 75 -274727637 -1962155238 -172493568 -292301350 + -74516140 -689320765 -257446400 -134423309 - -5.2764928340911865e-01 3.5589152574539185e-01 + -4.9807366728782654e-01 3.6395463347434998e-01 <_> - 0 -1 51 -433068542 -991182329 44441604 306607153 1449382759 - 125030915 1409898947 -671101193 + 0 -1 60 -84740 468980664 454041488 534654744 464272364 + 176965560 1983490748 1073428479 - -5.8401101827621460e-01 3.5362774133682251e-01 + -6.9375485181808472e-01 2.5259119272232056e-01 <_> - 0 -1 38 2111600636 1066940316 863786494 2140674937 978850808 - 455878812 1503801855 1002372991 + 0 -1 39 -451948633 -878710325 -89990998 1928771826 -70590373 + 106065995 1971450036 -185073665 - -7.2070401906967163e-01 2.7193057537078857e-01 + -4.4236001372337341e-01 4.2276427149772644e-01 <_> 7 - -1.4490258693695068e+00 + -1.3514715433120728e+00 <_> - 0 -1 11 -68097 -1080405289 -1367935265 533863645 -721445889 - -2391185 -278987041 -1049089 + 0 -1 22 -1 830472175 -4194305 -100665153 -275461 -67110917 + -100663297 -67109889 - -7.1471744775772095e-01 2.5204733014106750e-01 + -6.0122072696685791e-01 6.6442954540252686e-01 <_> - 0 -1 72 -6555889 1097593319 -17379062 -369165825 1604055815 - 1634328719 -274734270 -470810629 + 0 -1 9 -536871186 -319887182 -572858202 -387529530 + -263783649 1936881186 -208273929 -145498425 - -5.6740534305572510e-01 3.2793164253234863e-01 + -5.4182785749435425e-01 3.4675773978233337e-01 <_> - 0 -1 31 -134320292 2029845341 938917884 855571322 1238654844 - 1507329893 2147228636 -2564101 + 0 -1 34 429658036 366774229 -1757103107 -1074039435 + 756829181 1055017305 -70350401 -1074360147 - -6.1741048097610474e-01 2.8779673576354980e-01 + -5.7372617721557617e-01 3.2286378741264343e-01 <_> - 0 -1 46 -1073495038 -545262305 -80218625 -538970417 - -604251650 -6629485 -83904001 -1072987226 + 0 -1 40 -33636948 991968120 -103904560 1002483790 + -1179373060 255358432 1071392492 2113848255 - -4.6788156032562256e-01 3.9040920138359070e-01 + -6.6383731365203857e-01 2.5130525231361389e-01 <_> - 0 -1 61 -2013248189 -740827685 -222339873 -1051777 - -337648385 -23390301 -274923557 -2102290957 + 0 -1 44 -2147318784 -2925 -784346277 -536882289 -747914266 + -352392513 -337117441 -2146721146 - -4.4455590844154358e-01 4.3781617283821106e-01 + -6.1697775125503540e-01 2.7669304609298706e-01 <_> - 0 -1 67 -425532380 -1948260198 -771398400 1078643656 - -610280144 1107493733 1455380011 1207686574 + 0 -1 12 -815355970 2035791128 -1130456197 1048879784 + -1685378358 -880031607 635238911 -1982913778 - -6.3499057292938232e-01 3.1427818536758423e-01 + -4.7567644715309143e-01 3.8744848966598511e-01 <_> - 0 -1 0 -1465474633 -1346333225 -1384647689 -439632648 - 555739383 1639682123 -18940001 615339959 + 0 -1 71 -100668145 185059827 -197985022 -1561928745 + 1593365762 827494795 -940055742 -140640293 - -6.4793455600738525e-01 2.6171669363975525e-01 + -5.3159093856811523e-01 3.6455613374710083e-01 <_> - 7 - -1.8950796127319336e+00 + 8 + -1.7042466402053833e+00 <_> - 0 -1 69 -67117281 -2629289 -188747521 -2107429 -5248769 - -624954197 -606086145 -71565729 + 0 -1 0 -286590977 -1059105 -67956225 -143142401 -306186497 + -67394918 -1074790401 -810549761 - -6.4841037988662720e-01 5.0977444648742676e-01 + -6.9610232114791870e-01 2.5562700629234314e-01 <_> - 0 -1 26 -277092053 1219489222 -272144530 -419430680 - -1034030083 -1092103687 -7605451 -603981825 + 0 -1 20 -52441857 -723714835 -2110241 -637534465 -34181129 + -729097 -16779299 -71368705 - -5.3364491462707520e-01 3.6466205120086670e-01 + -4.0139526128768921e-01 5.0971853733062744e-01 <_> - 0 -1 65 465055548 1006376861 1063125433 -1078365731 - 2023522188 1553474557 1872486879 2079284637 + 0 -1 5 -268439697 -1407736901 1323822782 -388012338 + 1206856575 2103180175 1979148022 -416350275 - -6.4313161373138428e-01 2.6651522517204285e-01 + -4.3721720576286316e-01 3.8813734054565430e-01 <_> - 0 -1 7 -54854582 1761013467 2121979482 -88981490 1394046423 - -147368984 -674043102 -42668425 + 0 -1 36 -1757391624 -1619958841 1569675345 1026588057 + -1709392434 -9961349 -616780808 182717056 - -6.1165940761566162e-01 2.7585926651954651e-01 + -6.2012439966201782e-01 2.8761184215545654e-01 <_> - 0 -1 56 -71748 1037336260 -1983891980 -1078316142 1056466172 - 704383733 871119804 1073643373 + 0 -1 41 201327621 -11320879 -950796801 -59561 -1416692737 + -124038232 -1409552433 579429813 - -7.3266768455505371e-01 2.3998503386974335e-01 + -7.2984564304351807e-01 2.4998624622821808e-01 <_> - 0 -1 19 -1968540329 2012184063 -16777217 -134234115 - -1342177281 -8388737 -402661386 -840250051 + 0 -1 42 -870064096 285238103 1820599346 -1621180257 + 285589700 1678906683 838856958 -270533921 - -3.9631426334381104e-01 4.8900303244590759e-01 + -5.2406799793243408e-01 3.4712812304496765e-01 <_> - 0 -1 36 -102240505 -579600401 -402662694 -152829955 - -671612979 224878431 -1153443866 -487325705 + 0 -1 76 1329575926 244245492 -951086856 -1126403084 + -877660755 25837750 -119410448 -717432460 - -3.8574314117431641e-01 5.1558119058609009e-01 + -5.7642680406570435e-01 2.9808703064918518e-01 + <_> + + 0 -1 31 -411331404 899449501 471203871 1061499199 421532152 + -1951856520 238168271 268370447 + + -4.8114898800849915e-01 3.8295701146125793e-01 <_> - 0 0 3 1 + 0 0 4 1 <_> - 0 0 4 1 + 0 0 5 1 <_> 0 1 1 3 @@ -571,22 +577,25 @@ 0 3 1 2 <_> - 0 6 1 1 + 0 4 4 3 <_> - 0 6 2 2 - <_> - - 0 6 3 2 + 0 5 3 2 <_> 0 7 2 1 <_> - 0 9 16 1 + 0 8 1 1 + <_> + + 0 9 15 1 <_> 0 10 4 1 + <_> + + 0 10 17 1 <_> 1 0 3 1 @@ -595,49 +604,40 @@ 1 1 14 1 <_> - 1 6 1 2 + 1 4 2 3 <_> 1 6 2 2 - <_> - - 1 10 7 1 - <_> - - 1 10 13 1 <_> 2 0 2 1 <_> - 2 0 3 1 + 2 0 10 1 <_> - 2 1 2 3 + 2 1 2 2 <_> - 2 10 3 1 + 2 6 2 2 <_> - 3 0 8 1 + 2 10 12 1 <_> - 3 1 2 3 + 3 1 1 3 <_> - 3 2 1 1 + 3 10 2 1 <_> - 3 5 2 1 + 3 10 7 1 <_> - 4 2 1 1 + 4 3 1 1 <_> - 4 4 1 1 - <_> - - 4 10 7 1 + 4 3 1 2 <_> 5 0 4 1 @@ -646,7 +646,13 @@ 5 0 14 1 <_> - 5 7 1 2 + 6 1 12 1 + <_> + + 6 5 1 1 + <_> + + 7 2 13 1 <_> 7 10 12 1 @@ -655,73 +661,70 @@ 8 9 14 1 <_> - 9 10 14 1 + 9 9 4 1 <_> - 10 1 13 1 + 10 1 12 1 <_> - 10 4 2 1 + 10 1 14 1 <_> - 12 3 1 3 + 11 10 13 1 <_> - 13 7 2 2 + 12 0 2 3 <_> - 14 0 1 3 + 13 4 1 1 <_> - 16 6 2 1 + 13 4 1 3 <_> - 17 0 4 1 + 14 0 1 1 + <_> + + 14 6 9 1 <_> 18 0 4 1 - <_> - - 18 0 5 1 - <_> - - 18 10 7 1 - <_> - - 19 3 2 3 <_> 20 0 1 2 <_> - 20 9 6 1 + 20 0 5 1 <_> - 21 1 8 1 + 21 4 1 3 <_> - 21 3 2 3 + 21 9 7 1 + <_> + + 22 4 1 2 + <_> + + 22 4 2 2 <_> 22 7 1 2 <_> - 24 3 2 1 + 23 0 7 1 <_> - 25 0 1 1 + 23 10 4 1 <_> - 25 7 1 2 + 25 7 2 2 <_> 27 0 1 2 <_> - 27 1 1 3 - <_> - - 27 7 1 2 + 28 0 1 3 <_> 28 0 7 1 @@ -730,37 +733,37 @@ 28 4 2 3 <_> - 31 0 1 2 + 29 0 1 1 <_> - 31 0 4 1 + 32 7 1 2 <_> - 31 1 1 1 + 32 7 2 2 <_> - 33 7 1 2 - <_> - - 34 6 2 1 + 33 2 2 3 <_> 34 10 6 1 <_> - 36 9 3 1 + 35 4 2 3 <_> - 37 4 1 3 + 36 0 1 3 <_> - 39 2 1 3 + 36 0 2 2 <_> - 40 0 1 2 + 39 1 1 4 <_> - 49 0 1 1 + 40 0 4 1 + <_> + + 41 0 1 2 <_> 49 0 1 2 @@ -781,8 +784,11 @@ 49 4 1 2 <_> - 49 8 1 1 + 49 7 1 1 <_> - 49 9 1 1 + 49 7 1 2 + <_> + + 49 8 1 1 From 3eefb08d12c378f284d8c516be60667340e20183 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Wed, 8 Jan 2014 22:43:00 -0600 Subject: [PATCH 13/14] Updated readme --- README.md | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0c9d8f2..0e9135c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,123 @@ openalpr ======== -Automated License Plate Recognition library +OpenALPR is an open source *Automated License Plate Recognition* library written in C++. The library analyzes images and identifies license plates. The output is the text representation of any license plate characters found in the image. + +Check out a live online demo here: http://www.openalpr.com/demo.html + +User Guide +----------- + +OpenALPR includes a command line utility. Simply typing "alpr [image file path]" is enough to get started recognizing license plate images. + +``` +user@linux:~/openalpr$ alpr --help + +USAGE: + + alpr [-t ] [-r ] [-n ] + [--seek ] [-c ] + [--clock] [-d] [-j] [--] [--version] [-h] + + + +Where: + + -t , --template_region + Attempt to match the plate number against a region template (e.g., md + for Maryland, ca for California) + + -r , --runtime_dir + Path to the OpenAlpr runtime data directory + + -n , --topn + Max number of possible plate numbers to return. Default=10 + + --seek + Seek to the specied millisecond in a video file. Default=0 + + -c , --country + Country code to identify (either us for USA or eu for Europe). + Default=us + + --clock + Measure/print the total time to process image and all plates. + Default=off + + -d, --detect_region + Attempt to detect the region of the plate image. Default=off + + -j, --json + Output recognition results in JSON format. Default=off + + --, --ignore_rest + Ignores the rest of the labeled arguments following this flag. + + --version + Displays version information and exits. + + -h, --help + Displays usage information and exits. + + + (required) Image containing license plates + + + OpenAlpr Command Line Utility +``` + +For example, the following output is created by analyzing the plate image at this URL: http://www.openalpr.com/images/demoscreenshots/plate3.png + +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/ + +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 + +``` + +Compiling +----------- + +OpenALPR has been compiled successfully on Linux, however other operating systems should also work. + +OpenALPR requires the following additional libraries: + + - Tesseract OCR v3.x (https://code.google.com/p/tesseract-ocr/) + - OpenCV v2.4.x (http://opencv.org/) + +After cloning this GitHub repository, you should download and extract Tesseract and OpenCV source code into their own directories. Compile both libraries. + +Update the src/CMakeLists.txt file in the OpenALPR project. Update the following lines to match the directories of your libraries on your system: + - SET(OpenCV_DIR "../libraries/opencv/") + - SET(Tesseract_DIR "/home/mhill/projects/alpr/libraries/tesseract-ocr") + +Finally, in the src directory, execute the following commands: + - cmake ./ + - make + +If all went well, there should be an executable named *alpr* along with *libopenalpr.a* that can be linked into your project. + + +Questions +--------- +Please post questions or comments to the Google group list: https://groups.google.com/forum/#!forum/openalpr + +License +------- + +Affero GPLv3 +http://www.gnu.org/licenses/agpl-3.0.html + + From b759db971790ba3e11d76168adf9e30146089324 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Wed, 8 Jan 2014 22:45:20 -0600 Subject: [PATCH 14/14] Updated readme --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0e9135c..4f2d1dc 100644 --- a/README.md +++ b/README.md @@ -100,12 +100,14 @@ OpenALPR requires the following additional libraries: After cloning this GitHub repository, you should download and extract Tesseract and OpenCV source code into their own directories. Compile both libraries. Update the src/CMakeLists.txt file in the OpenALPR project. Update the following lines to match the directories of your libraries on your system: - - SET(OpenCV_DIR "../libraries/opencv/") - - SET(Tesseract_DIR "/home/mhill/projects/alpr/libraries/tesseract-ocr") + + * SET(OpenCV_DIR "../libraries/opencv/") + * SET(Tesseract_DIR "/home/mhill/projects/alpr/libraries/tesseract-ocr") Finally, in the src directory, execute the following commands: - - cmake ./ - - make + + * cmake ./ + * make If all went well, there should be an executable named *alpr* along with *libopenalpr.a* that can be linked into your project.