本文主要是介绍OpenCV3 和 Qt5 计算机视觉 学习笔记 - 特征和描述符,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
特征和描述符
所有算法的基础 – Algorithm
类
class CV_EXPORTS_W Algorithm
{
public:Algorithm();virtual ~Algorithm();/** @brief Clears the algorithm state*/CV_WRAP virtual void clear() {}/** @brief Stores algorithm parameters in a file storage*/virtual void write(FileStorage& fs) const { CV_UNUSED(fs); }/** @brief simplified API for language bindings* @overload*/CV_WRAP void write(const Ptr<FileStorage>& fs, const String& name = String()) const;/** @brief Reads algorithm parameters from a file storage*/CV_WRAP virtual void read(const FileNode& fn) { CV_UNUSED(fn); }/** @brief Returns true if the Algorithm is empty (e.g. in the very beginning or after unsuccessful read*/CV_WRAP virtual bool empty() const { return false; }/** @brief Reads algorithm from the file nodeThis is static template method of Algorithm. It's usage is following (in the case of SVM):@codecv::FileStorage fsRead("example.xml", FileStorage::READ);Ptr<SVM> svm = Algorithm::read<SVM>(fsRead.root());@endcodeIn order to make this method work, the derived class must overwrite Algorithm::read(constFileNode& fn) and also have static create() method without parameters(or with all the optional parameters)*/template<typename _Tp> static Ptr<_Tp> read(const FileNode& fn){}/** @brief Loads algorithm from the file@param filename Name of the file to read.@param objname The optional name of the node to read (if empty, the first top-level node will be used)This is static template method of Algorithm. It's usage is following (in the case of SVM):@codePtr<SVM> svm = Algorithm::load<SVM>("my_svm_model.xml");@endcodeIn order to make this method work, the derived class must overwrite Algorithm::read(constFileNode& fn).*/template<typename _Tp> static Ptr<_Tp> load(const String& filename, const String& objname=String()){FileStorage fs(filename, FileStorage::READ);CV_Assert(fs.isOpened());FileNode fn = objname.empty() ? fs.getFirstTopLevelNode() : fs[objname];if (fn.empty()) return Ptr<_Tp>();Ptr<_Tp> obj = _Tp::create();obj->read(fn);return !obj->empty() ? obj : Ptr<_Tp>();}/** @brief Loads algorithm from a String@param strModel The string variable containing the model you want to load.@param objname The optional name of the node to read (if empty, the first top-level node will be used)This is static template method of Algorithm. It's usage is following (in the case of SVM):@codePtr<SVM> svm = Algorithm::loadFromString<SVM>(myStringModel);@endcode*/template<typename _Tp> static Ptr<_Tp> loadFromString(const String& strModel, const String& objname=String()){FileStorage fs(strModel, FileStorage::READ + FileStorage::MEMORY);FileNode fn = objname.empty() ? fs.getFirstTopLevelNode() : fs[objname];Ptr<_Tp> obj = _Tp::create();obj->read(fn);return !obj->empty() ? obj : Ptr<_Tp>();}/** Saves the algorithm to a file.In order to make this method work, the derived class must implement Algorithm::write(FileStorage& fs). */CV_WRAP virtual void save(const String& filename) const;/** Returns the algorithm string identifier.This string is used as top level xml/yml node tag when the object is saved to a file or string. */CV_WRAP virtual String getDefaultName() const;protected:void writeFormat(FileStorage& fs) const;
};
OpenCV 中的所有算法或更好的算法,至少不是不太简短的算法,都被创建为cv::Algorithm
类的子类。 与通常所期望的相反,该类不是抽象类,这意味着您可以创建它的实例,而该实例只是不执行任何操作。 即使将来可能会更改它,也不会真正影响我们访问和使用它的方式。 在 OpenCV 中使用cv::Algorithm
类的方式,以及如果要创建自己的算法的推荐方法,是首先创建cv::Algorithm
的子类,其中包含用于特定目的或目标的所有必需成员函数。 。 然后,可以再次对该新创建的子类进行子类化,以创建同一算法的不同实现。 为了更好地理解这一点,让我们首先详细了解cv::Algorithm
类。 大致了解一下 OpenCV 源代码的外观
首先,让我们看看cv::Algorithm
类中使用的FileStorage
和FileNode
类是什么(以及许多其他 OpenCV 类),然后介绍cv::Algorithm
类中的方法:
/** @brief XML/YAML/JSON file storage class that encapsulates all the information necessary for writing or
reading data to/from a file.*/
class CV_EXPORTS_W FileStorage
{
public://! file storage modeenum Mode{READ = 0, //!< value, open the file for readingWRITE = 1, //!< value, open the file for writingAPPEND = 2, //!< value, open the file for appendingMEMORY = 4, //!< flag, read data from source or write data to the internal buffer (which is//!< returned by FileStorage::release)FORMAT_MASK = (7<<3), //!< mask for format flagsFORMAT_AUTO = 0, //!< flag, auto formatFORMAT_XML = (1<<3), //!< flag, XML formatFORMAT_YAML = (2<<3), //!< flag, YAML formatFORMAT_JSON = (3<<3), //!< flag, JSON formatBASE64 = 64, //!< flag, write rawdata in Base64 by default. (consider using WRITE_BASE64)WRITE_BASE64 = BASE64 | WRITE, //!< flag, enable both WRITE and BASE64};enum State{UNDEFINED = 0,VALUE_EXPECTED = 1,NAME_EXPECTED = 2,INSIDE_MAP = 4};/** @brief The constructors.The full constructor opens the file. Alternatively you can use the default constructor and thencall FileStorage::open.*/CV_WRAP FileStorage();/** @overload@copydoc open()*/CV_WRAP FileStorage(const String& filename, int flags, const String& encoding=String());//! the destructor. calls release()virtual ~FileStorage();/** @brief Opens a file.See description of parameters in FileStorage::FileStorage. The method calls FileStorage::releasebefore opening the file.@param filename Name of the file to open or the text string to read the data from.Extension of the file (.xml, .yml/.yaml or .json) determines its format (XML, YAML or JSONrespectively). Also you can append .gz to work with compressed files, for example myHugeMatrix.xml.gz. If bothFileStorage::WRITE and FileStorage::MEMORY flags are specified, source is used just to specifythe output file format (e.g. mydata.xml, .yml etc.). A file name can also contain parameters.You can use this format, "*?base64" (e.g. "file.json?base64" (case sensitive)), as an alternative toFileStorage::BASE64 flag.@param flags Mode of operation. One of FileStorage::Mode@param encoding Encoding of the file. Note that UTF-16 XML encoding is not supported currently andyou should use 8-bit encoding instead of it.*/CV_WRAP virtual bool open(const String& filename, int flags, const String& encoding=String());/** @brief Checks whether the file is opened.@returns true if the object is associated with the current file and false otherwise. It is agood practice to call this method after you tried to open a file.*/CV_WRAP virtual bool isOpened() const;/** @brief Closes the file and releases all the memory buffers.Call this method after all I/O operations with the storage are finished.*/CV_WRAP virtual void release();/** @brief Closes the file and releases all the memory buffers.Call this method after all I/O operations with the storage are finished. If the storage wasopened for writing data and FileStorage::WRITE was specified*/CV_WRAP virtual String releaseAndGetString();/** @brief Returns the first element of the top-level mapping.@returns The first element of the top-level mapping.*/CV_WRAP FileNode getFirstTopLevelNode() const;/** @brief Returns the top-level mapping@param streamidx Zero-based index of the stream. In most cases there is only one stream in the file.However, YAML supports multiple streams and so there can be several.@returns The top-level mapping.*/CV_WRAP FileNode root(int streamidx=0) const;/** @brief Returns the specified element of the top-level mapping.@param nodename Name of the file node.@returns Node with the given name.*/FileNode operator[](const String& nodename) const;/** @overload */CV_WRAP_AS(getNode) FileNode operator[](const char* nodename) const;/*** @brief Simplified writing API to use with bindings.* @param name Name of the written object* @param val Value of the written object*/CV_WRAP void write(const String& name, int val);/// @overloadCV_WRAP void write(const String& name, double val);/// @overloadCV_WRAP void write(const String& name, const String& val);/// @overloadCV_WRAP void write(const String& name, const Mat& val);/// @overloadCV_WRAP void write(const String& name, const std::vector<String>& val);/** @brief Writes multiple numbers.Writes one or more numbers of the specified format to the currently written structure. Usually it ismore convenient to use operator `<<` instead of this method.@param fmt Specification of each array element, see @ref format_spec "format specification"@param vec Pointer to the written array.@param len Number of the uchar elements to write.*/void writeRaw( const String& fmt, const void* vec, size_t len );/** @brief Writes a comment.The function writes a comment into file storage. The comments are skipped when the storage is read.@param comment The written comment, single-line or multi-line@param append If true, the function tries to put the comment at the end of current line.Else if the comment is multi-line, or if it does not fit at the end of the currentline, the comment starts a new line.*/CV_WRAP void writeComment(const String& comment, bool append = false);void startWriteStruct(const String& name, int flags, const String& typeName);void endWriteStruct();/** @brief Returns the normalized object name for the specified name of a file.@param filename Name of a file@returns The normalized object name.*/static String getDefaultObjectName(const String& filename);/** @brief Returns the current format.* @returns The current format, see FileStorage::Mode*/CV_WRAP int getFormat() const;int state;std::string elname;class Impl;Ptr<Impl> p;
};
-
FileStorage
类可用于轻松地读写 XML,YAML 和 JSON 文件。 此类在 OpenCV 中广泛使用,以存储许多算法产生或需要的各种类型的信息。 此类几乎与任何其他文件读取器/写入器类一样工作,不同之处在于它可以与所提到的文件类型一起工作。/** @brief File Storage Node class.The node is used to store each and every element of the file storage opened for reading. When XML/YAML file is read, it is first parsed and stored in the memory as a hierarchical collection of nodes. Each node can be a "leaf" that is contain a single number or a string, or be a collection of other nodes. There can be named collections (mappings) where each element has a name and it is accessed by a name, and ordered collections (sequences) where elements do not have names but rather accessed by index. Type of the file node can be determined using FileNode::type method.Note that file nodes are only used for navigating file storages opened for reading. When a file storage is opened for writing, no data is stored in memory after it is written.*/ class CV_EXPORTS_W_SIMPLE FileNode { public://! type of the file storage nodeenum{NONE = 0, //!< empty nodeINT = 1, //!< an integerREAL = 2, //!< floating-point numberFLOAT = REAL, //!< synonym or REALSTR = 3, //!< text string in UTF-8 encodingSTRING = STR, //!< synonym for STRSEQ = 4, //!< sequenceMAP = 5, //!< mappingTYPE_MASK = 7,FLOW = 8, //!< compact representation of a sequence or mapping. Used only by YAML writerUNIFORM = 8, //!< if set, means that all the collection elements are numbers of the same type (real's or int's).//!< UNIFORM is used only when reading FileStorage; FLOW is used only when writing. So they share the same bitEMPTY = 16, //!< empty structure (sequence or mapping)NAMED = 32 //!< the node has a name (i.e. it is element of a mapping).};/** @brief The constructors.These constructors are used to create a default file node, construct it from obsolete structures orfrom the another file node.*/CV_WRAP FileNode();/** @overload@param fs Pointer to the file storage structure.@param blockIdx Index of the memory block where the file node is stored@param ofs Offset in bytes from the beginning of the serialized storage*/FileNode(const FileStorage* fs, size_t blockIdx, size_t ofs);/** @overload@param node File node to be used as initialization for the created file node.*/FileNode(const FileNode& node);FileNode& operator=(const FileNode& node);/** @brief Returns element of a mapping node or a sequence node.@param nodename Name of an element in the mapping node.@returns Returns the element with the given identifier.*/FileNode operator[](const String& nodename) const;/** @overload@param nodename Name of an element in the mapping node.*/CV_WRAP_AS(getNode) FileNode operator[](const char* nodename) const;/** @overload@param i Index of an element in the sequence node.*/CV_WRAP_AS(at) FileNode operator[](int i) const;/** @brief Returns keys of a mapping node.@returns Keys of a mapping node.*/CV_WRAP std::vector<String> keys() const;/** @brief Returns type of the node.@returns Type of the node. See FileNode::Type*/CV_WRAP int type() const;//! returns true if the node is emptyCV_WRAP bool empty() const;//! returns true if the node is a "none" objectCV_WRAP bool isNone() const;//! returns true if the node is a sequenceCV_WRAP bool isSeq() const;//! returns true if the node is a mappingCV_WRAP bool isMap() const;//! returns true if the node is an integerCV_WRAP bool isInt() const;//! returns true if the node is a floating-point numberCV_WRAP bool isReal() const;//! returns true if the node is a text stringCV_WRAP bool isString() const;//! returns true if the node has a nameCV_WRAP bool isNamed() const;//! returns the node name or an empty string if the node is namelessCV_WRAP std::string name() const;//! returns the number of elements in the node, if it is a sequence or mapping, or 1 otherwise.CV_WRAP size_t size() const;//! returns raw size of the FileNode in bytesCV_WRAP size_t rawSize() const;//! returns the node content as an integer. If the node stores floating-point number, it is rounded.operator int() const;//! returns the node content as floatoperator float() const;//! returns the node content as doubleoperator double() const;//! returns the node content as text stringinline operator std::string() const { return this->string(); }static bool isMap(int flags);static bool isSeq(int flags);static bool isCollection(int flags);static bool isEmptyCollection(int flags);static bool isFlow(int flags);uchar* ptr();const uchar* ptr() const;//! returns iterator pointing to the first node elementFileNodeIterator begin() const;//! returns iterator pointing to the element following the last node elementFileNodeIterator end() const;/** @brief Reads node elements to the buffer with the specified format.Usually it is more convenient to use operator `>>` instead of this method.@param fmt Specification of each array element. See @ref format_spec "format specification"@param vec Pointer to the destination array.@param len Number of bytes to read (buffer size limit). If it is greater than number ofremaining elements then all of them will be read.*/void readRaw( const String& fmt, void* vec, size_t len ) const;/** Internal method used when reading FileStorage.Sets the type (int, real or string) and value of the previously created node.*/void setValue( int type, const void* value, int len=-1 );//! Simplified reading API to use with bindings.CV_WRAP double real() const;//! Simplified reading API to use with bindings.CV_WRAP std::string string() const;//! Simplified reading API to use with bindings.CV_WRAP Mat mat() const;//protected:const FileStorage* fs;size_t blockIdx;size_t ofs; };
-
FileNode
类本身是Node
类的子类,用于表示FileStorage
类中的单个元素。FileNode
类可以是FileNode
元素集合中的单个叶子,也可以是其他FileNode
元素的容器。2D 特征框架
正如本章前面提到的,OpenCV 提供了类来执行由世界各地的计算机视觉研究人员创建的各种特征检测和描述符提取算法。 与 OpenCV 中实现的任何其他复杂算法一样,特征检测器和描述符提取器也通过将
cv::Algorithm
类子类化而创建。 该子类称为Feature2D
,它包含所有特征检测和描述符提取类共有的各种函数。 基本上,任何可用于检测特征和提取描述符的类都应该是Featured2D
的子类。 为此,OpenCV 使用以下两种类类型:FeatureDetector
DescriptorExtractor
-
/** Feature detectors in OpenCV have wrappers with a common interface that enables you to easily switch
between different algorithms solving the same problem. All objects that implement keypoint detectors
inherit the FeatureDetector interface. */
typedef Feature2D FeatureDetector;
/** Extractors of keypoint descriptors in OpenCV have wrappers with a common interface that enables you
to easily switch between different algorithms solving the same problem. This section is devoted to
computing descriptors represented as vectors in a multidimensional space. All objects that implement
the vector descriptor extractors inherit the DescriptorExtractor interface.*/
typedef Feature2D DescriptorExtractor;
重要的是要注意,实际上这两个类都是Feature2D
的不同名称,因为它们是使用以下typedef
语句在 OpenCV 中创建的(我们将在本节后面讨论其原因) ):
看到Feature2D
类的声明也是一个好主意:
/** @brief Abstract base class for 2D image feature detectors and descriptor extractors
*/
#ifdef __EMSCRIPTEN__
class CV_EXPORTS_W Feature2D : public Algorithm
#else
class CV_EXPORTS_W Feature2D : public virtual Algorithm
#endif
{
public:virtual ~Feature2D();/** @brief Detects keypoints in an image (first variant) or image set (second variant).@param image Image.@param keypoints The detected keypoints. In the second variant of the method keypoints[i] is a setof keypoints detected in images[i] .@param mask Mask specifying where to look for keypoints (optional). It must be a 8-bit integermatrix with non-zero values in the region of interest.*/CV_WRAP virtual void detect( InputArray image,CV_OUT std::vector<KeyPoint>& keypoints,InputArray mask=noArray() );/** @overload@param images Image set.@param keypoints The detected keypoints. In the second variant of the method keypoints[i] is a setof keypoints detected in images[i] .@param masks Masks for each input image specifying where to look for keypoints (optional).masks[i] is a mask for images[i].*/CV_WRAP virtual void detect( InputArrayOfArrays images,CV_OUT std::vector<std::vector<KeyPoint> >& keypoints,InputArrayOfArrays masks=noArray() );/** @brief Computes the descriptors for a set of keypoints detected in an image (first variant) or image set(second variant).@param image Image.@param keypoints Input collection of keypoints. Keypoints for which a descriptor cannot becomputed are removed. Sometimes new keypoints can be added, for example: SIFT duplicates keypointwith several dominant orientations (for each orientation).@param descriptors Computed descriptors. In the second variant of the method descriptors[i] aredescriptors computed for a keypoints[i]. Row j is the keypoints (or keypoints[i]) is thedescriptor for keypoint j-th keypoint.*/CV_WRAP virtual void compute( InputArray image,CV_OUT CV_IN_OUT std::vector<KeyPoint>& keypoints,OutputArray descriptors );/** @overload@param images Image set.@param keypoints Input collection of keypoints. Keypoints for which a descriptor cannot becomputed are removed. Sometimes new keypoints can be added, for example: SIFT duplicates keypointwith several dominant orientations (for each orientation).@param descriptors Computed descriptors. In the second variant of the method descriptors[i] aredescriptors computed for a keypoints[i]. Row j is the keypoints (or keypoints[i]) is thedescriptor for keypoint j-th keypoint.*/CV_WRAP virtual void compute( InputArrayOfArrays images,CV_OUT CV_IN_OUT std::vector<std::vector<KeyPoint> >& keypoints,OutputArrayOfArrays descriptors );/** Detects keypoints and computes the descriptors */CV_WRAP virtual void detectAndCompute( InputArray image, InputArray mask,CV_OUT std::vector<KeyPoint>& keypoints,OutputArray descriptors,bool useProvidedKeypoints=false );CV_WRAP virtual int descriptorSize() const;CV_WRAP virtual int descriptorType() const;CV_WRAP virtual int defaultNorm() const;CV_WRAP void write( const String& fileName ) const;CV_WRAP void read( const String& fileName );virtual void write( FileStorage&) const CV_OVERRIDE;// see corresponding cv::Algorithm methodCV_WRAP virtual void read( const FileNode&) CV_OVERRIDE;//! Return true if detector object is emptyCV_WRAP virtual bool empty() const CV_OVERRIDE;CV_WRAP virtual String getDefaultName() const CV_OVERRIDE;// see corresponding cv::Algorithm methodCV_WRAP inline void write(const Ptr<FileStorage>& fs, const String& name = String()) const { Algorithm::write(fs, name); }
};
让我们快速回顾一下Feature2D
类的声明中的内容。 首先,它是cv::Algorithm
的子类,正如我们之前所学。 读,写和空函数只是cv::Algorithm
中存在的简单重新实现的函数。 但是,以下函数是cv::Algorithm
中的新增函数,不存在,它们基本上是特征检测器和描述符提取器所需的其他函数:
detect
函数可用于从一个图像或一组图像中检测特征(或关键点)。compute
函数可用于从关键点提取(或计算)描述符。detectAndCompute
函数可用于执行单个特征的检测和计算。descriptorSize
,descriptorType
和defaultNorm
是算法相关的值,它们在每个能够提取描述符的Feature2D
子类中重新实现。
看起来似乎很奇怪,但是有充分的理由以这种方式对特征检测器和描述符进行分类,并且只有一个类,这是因为某些算法(并非全部)都提供了特征检测和描述符提取函数。 随着我们继续为此目的创建许多算法,这将变得更加清晰。 因此,让我们从 OpenCV 2D 特征框架中现有的Feature2D
类和算法开始。
检测特征
OpenCV 提供了许多类来处理图像中的特征(关键点)检测。 每个类都有自己的实现,具体取决于它实现的特定算法,并且可能需要一组不同的参数才能正确执行或具有最佳表现。 但是,它们所有的共同点就是前面提到的detect
函数(因为它们都是Feature2D
的子类),可用于检测图像中的一组关键点。 OpenCV 中的关键点或特征是KeyPoint
类实例,其中包含为正确的关键点需要存储的大多数信息(这些术语,即关键点和特征,可以互换使用,并且很多,因此,请尝试习惯它)。 以下是KeyPoint
类的成员及其定义:
/** @brief Data structure for salient point detectors.The class instance stores a keypoint, i.e. a point feature found by one of many available keypoint
detectors, such as Harris corner detector, #FAST, %StarDetector, %SURF, %SIFT etc.The keypoint is characterized by the 2D position, scale (proportional to the diameter of the
neighborhood that needs to be taken into account), orientation and some other parameters. The
keypoint neighborhood is then analyzed by another algorithm that builds a descriptor (usually
represented as a feature vector). The keypoints representing the same object in different images
can then be matched using %KDTree or another method.
*/
class CV_EXPORTS_W_SIMPLE KeyPoint
{
public://! the default constructorCV_WRAP KeyPoint();/**@param _pt x & y coordinates of the keypoint@param _size keypoint diameter@param _angle keypoint orientation@param _response keypoint detector response on the keypoint (that is, strength of the keypoint)@param _octave pyramid octave in which the keypoint has been detected@param _class_id object id*/KeyPoint(Point2f _pt, float _size, float _angle=-1, float _response=0, int _octave=0, int _class_id=-1);/**@param x x-coordinate of the keypoint@param y y-coordinate of the keypoint@param _size keypoint diameter@param _angle keypoint orientation@param _response keypoint detector response on the keypoint (that is, strength of the keypoint)@param _octave pyramid octave in which the keypoint has been detected@param _class_id object id*/CV_WRAP KeyPoint(float x, float y, float _size, float _angle=-1, float _response=0, int _octave=0, int _class_id=-1);size_t hash() const;/**This method converts vector of keypoints to vector of points or the reverse, where each keypoint isassigned the same size and the same orientation.@param keypoints Keypoints obtained from any feature detection algorithm like SIFT/SURF/ORB@param points2f Array of (x,y) coordinates of each keypoint@param keypointIndexes Array of indexes of keypoints to be converted to points. (Acts like a mask toconvert only specified keypoints)*/CV_WRAP static void convert(const std::vector<KeyPoint>& keypoints,CV_OUT std::vector<Point2f>& points2f,const std::vector<int>& keypointIndexes=std::vector<int>());/** @overload@param points2f Array of (x,y) coordinates of each keypoint@param keypoints Keypoints obtained from any feature detection algorithm like SIFT/SURF/ORB@param size keypoint diameter@param response keypoint detector response on the keypoint (that is, strength of the keypoint)@param octave pyramid octave in which the keypoint has been detected@param class_id object id*/CV_WRAP static void convert(const std::vector<Point2f>& points2f,CV_OUT std::vector<KeyPoint>& keypoints,float size=1, float response=1, int octave=0, int class_id=-1);/**This method computes overlap for pair of keypoints. Overlap is the ratio between area of keypointregions' intersection and area of keypoint regions' union (considering keypoint region as circle).If they don't overlap, we get zero. If they coincide at same location with same size, we get 1.@param kp1 First keypoint@param kp2 Second keypoint*/CV_WRAP static float overlap(const KeyPoint& kp1, const KeyPoint& kp2);CV_PROP_RW Point2f pt; //!< coordinates of the keypointsCV_PROP_RW float size; //!< diameter of the meaningful keypoint neighborhoodCV_PROP_RW float angle; //!< computed orientation of the keypoint (-1 if not applicable);//!< it's in [0,360) degrees and measured relative to//!< image coordinate system, ie in clockwise.CV_PROP_RW float response; //!< the response by which the most strong keypoints have been selected. Can be used for the further sorting or subsamplingCV_PROP_RW int octave; //!< octave (pyramid layer) from which the keypoint has been extractedCV_PROP_RW int class_id; //!< object class (if the keypoints need to be clustered by an object they belong to)
};
pt
或简单地指向:这包含关键点(X
和Y
)在图像中的位置。angle
:这是指关键点的顺时针旋转(0 到 360 度),即,检测到关键点的算法是否能够找到它; 否则,它将被设置为-1
。response
:这是关键点的强度,可用于排序或过滤弱关键点,依此类推。size
:这是指指定可用于进一步处理的关键点邻域的直径。octave
:这是图像的八度(或金字塔音阶),从中可以检测到该特定关键点。 这是一个非常强大且实用的概念,在检测关键点或使用它们进一步检测图像上可能具有不同大小的对象时,已广泛用于实现与比例尺(比例尺不变)的独立性。 为此,可以使用相同的算法处理同一图像的不同缩放版本(仅较小版本)。 每个刻度基本上称为octave
或金字塔中的一个等级。
为了方便起见,KeyPoint
类提供了其他成员和方法,以便您可以自己检查一下,但是为了进一步使用它们,我们肯定经历了我们需要熟悉的所有重要属性。 现在,让我们看一下现有的 OpenCV 特征检测器类的列表,以及它们的简要说明以及如何使用它们的示例:
- 可以使用
AgastFeatureDetector
(包括 AGAST(自适应和通用加速分段测试)算法的实现)来检测图像中的角。 它需要三个参数(可以省略所有参数以使用默认值)来配置其行为。 这是一个例子:
Ptr<AgastFeatureDetector> agast = AgastFeatureDetector::create(); vector<KeyPoint> keypoints; agast->detect(inputImage, keypoints);
如此简单,我们仅将AgastFeatureDetector
与默认参数集一起使用。 在深入研究上述操作的结果之前,让我们首先看一下代码本身,因为其中使用了 OpenCV 中最重要和最实用的类之一(称为Ptr
)。 如前面的代码所示,我们使用了Ptr
类,它是 OpenCV 共享指针(也称为智能指针)的实现。 使用智能指针的优点之一是,您不必担心在使用完该类后释放为该类分配的内存。 另一个优点以及被称为共享指针的原因是,多个Ptr
类可以使用(共享)单个指针,并且该指针(分配的内存)仅保留到Ptr
指向的最后一个实例被摧毁为止。 在复杂的代码中,这可能意味着极大的简化。
接下来,请务必注意,您需要使用静态create
函数来创建AgastFeatureDetector
类的共享指针实例。 您将无法创建此类的实例,因为它是抽象类。 其余代码并不是新内容。 我们只需创建KeyPoint
的std::vector
,然后使用 AGAST 的基础算法检测输入Mat
图像中的关键点。
在前面的示例中,我们仅使用了默认参数(省略了默认参数),但是为了更好地控制 AGAST 算法的行为,我们需要注意以下参数:
-
默认情况下设置为
10
的threshold
值用于基于像素与围绕它的圆上的像素之间的强度差传递特征。 阈值越高意味着检测到的特征数量越少,反之亦然。 -
NonmaxSuppression
可用于对检测到的关键点应用非最大抑制。 默认情况下,此参数设置为true
,可用于进一步过滤掉不需要的关键点。 -
可以将
type
参数设置为以下值之一,并确定 AGAST 算法的类型:
AGAST_5_8
AGAST_7_12d
AGAST_7_12s
OAST_9_16
(默认值)
您可以使用适当的 Qt 小部件从用户界面获取参数值。 这是 AGAST 算法的示例用户界面以及其底层代码。 另外,您可以下载本节末尾提供的完整keypoint_plugin
源代码,其中包含该源代码以及以下特征检测示例,它们全部集成在一个插件中,与我们全面的computer_vision
项目兼容:
KAZE 和 AKAZE
KAZE
和AKAZE
(加速 KAZE)类可用于使用 KAZE 算法(其加速版本)检测特征。 有关 KAZE 和 AKAZE 算法的详细信息,请参考以下参考文献列表中提到的文件。 类似于我们在 AGAST 中看到的那样,我们可以使用默认参数集并简单地调用detect
函数,或者我们可以使用适当的 Qt 小部件获取所需的参数并进一步控制算法的行为。 这是一个例子:
AKAZE 和 KAZE 中的主要参数如下:
nOctaves
或八度的数量(默认为 4)可用于定义图像的最大八度。nOctaveLayers
或八度级别的数量(默认为 4)是每个八度(或每个比例级别)的子级别数。- 扩散率可以采用下列项之一,它是 KAZE 和 AKAZE 算法使用的非线性扩散方法(如稍后在此算法的参考文献中所述):
DIFF_PM_G1
DIFF_PM_G2
DIFF_WEICKERT
DIFF_CHARBONNIER
- 阈值是接受关键点的响应值(默认为 0.001000)。 阈值越低,检测到的(和接受的)关键点数量越多,反之亦然。
- 描述符类型参数可以是以下值之一。 请注意,此参数仅存在于 AKAZE 类中:
DESCRIPTOR_KAZE_UPRIGHT
DESCRIPTOR_KAZE
DESCRIPTOR_MLDB_UPRIGHT
descriptor_size
用于定义描述符的大小。 零值(也是默认值)表示完整尺寸的描述符。descriptor_channels
可用于设置描述符中的通道数。 默认情况下,此值设置为 3。
现在,不要理会与描述符相关的参数,例如描述符类型和大小以及通道数,我们将在后面看到。 这些相同的类也用于从特征中提取描述符,并且这些参数将在其中起作用,而不必检测关键点,尤其是detect
函数。
这是前面示例用户界面的源代码,其中,根据我们前面示例用户界面中Accelerated
复选框的状态,选择了 KAZE(未选中)或 AKAZE(加速):
BRISK
类
BRISK
类可用于使用 BRISK(二进制鲁棒不变可缩放关键点)算法检测图像中的特征。 请确保参考以下文章,以获取有关其工作方式以及 OpenCV 中基础实现的详细信息。 不过,用法与我们在 AGAST 和 KAZE 中看到的用法非常相似,其中使用create
函数创建了类,然后设置了参数(如果我们不使用默认值),最后是detect
函数被调用。 这是一个简单的例子:
GFTT
GFTT(需要跟踪的良好特征)仅是特征检测器。 GFTTDetector
可用于使用 Harris(以创建者命名)和 GFTT 角检测算法检测特征。 因此,是的,该类别实际上是将两种特征检测方法组合在一起的一个类别,原因是 GFTT 实际上是哈里斯算法的一种修改版本,使用的哪一种将由输入参数决定。 因此,让我们看看如何在示例案例中使用它,然后简要介绍一下参数:
以下是GFTTDetector
类的参数及其定义:
- 如果将
useHarrisDetector
设置为true
,则将使用 Harris 算法,否则将使用 GFTT。 默认情况下,此参数设置为false
。 blockSize
可用于设置块大小,该块大小将用于计算像素附近的导数协方差矩阵。 默认为 3。K
是 Harris 算法使用的常数参数值。- 可以设置
maxFeatures
或maxCorners
来限制检测到的关键点数量。 默认情况下,它设置为 1000,但是如果关键点的数量超过该数量,则仅返回最强的响应。 minDistance
是关键点之间的最小可接受值。 默认情况下,此值设置为 1,它不是像素距离,而是欧几里得距离。qualityLevel
是阈值级别的值,用于过滤质量指标低于特定级别的关键点。 请注意,实际阈值是通过将该值与图像中检测到的最佳关键点质量相乘得出的。
ORB
最后,ORB 算法,这是我们将在本节中介绍的最后一个特征检测算法。
ORB
类可用于使用 ORB(二进制鲁棒独立基本特征)算法检测图像中的关键点。 此类封装了我们已经看到的一些方法(例如 FAST 或 Harris)来检测关键点。 因此,在类构造器中设置或使用设置器函数设置的某些参数与描述符提取有关,我们将在后面学习; 但是,ORB 类可用于检测关键点,如以下示例所示:
该序列与到目前为止我们看到的其他算法完全相同。 让我们看看设置了哪些参数:
-
MaxFeatures
参数只是应该检索的最大关键点数。 请注意,检测到的关键点数量可能比此数量少很多,但永远不会更高。 -
ScaleFactor
或金字塔抽取比率,与我们先前算法中看到的八度参数有些相似,用于确定金字塔每个级别的尺度值,这些尺度将用于检测关键点并从不同尺度提取同一张图片的描述符。 这就是在 ORB 中实现尺度不变性的方式。 -
NLevels
是金字塔的等级数。 -
PatchSize
是 ORB 算法使用的补丁的大小。 有关此内容的详细信息,请确保参考以下参考文献,但是对于简短说明,补丁大小决定了要提取描述的关键点周围的区域。 请注意,PatchSize
和EdgeThreshold
参数需要大约相同的值,在前面的示例中也将其设置为相同的值。 -
EdgeThreshold
是在关键点检测期间将忽略的以像素为单位的边框。 -
WTA_K
或 ORB 算法内部使用的 WTA 哈希的 K 值是一个参数,用于确定将用于在 ORB 描述符中创建每个元素的点数。 我们将在本章稍后看到更多有关此的内容。 -
可以设置为以下值之一的
ScoreType
决定 ORB 算法使用的关键点检测方法:
ORB::HARRIS_SCORE
用于哈里斯角点检测算法ORB::FAST_SCORE
用于 FAST 关键点检测算法
-
FastThreshold
只是 ORB 在关键点检测算法中使用的阈值。
提取和匹配描述符
计算机视觉中的描述符是一种描述关键点的方式,该关键点完全依赖于用于提取关键点的特定算法,并且与关键点(在KeyPoint
类中定义的)不同,描述符没有共同的结构 ,除了每个描述符都代表一个关键点这一事实外。 OpenCV 中的描述符存储在Mat
类中,其中生成的描述符Mat
类中的每一行都引用关键点的描述符。 正如我们在上一节中了解到的,我们可以使用任何FeatureDetector
子类的detect
函数从图像上基本上检测出一组关键点。 同样,我们可以使用任何DescriptorExtractor
子类的compute
函数从关键点提取描述符。
由于特征检测器和描述符提取器在 OpenCV 中的组织方式(这都是Feature2D
子类,正如我们在本章前面了解的那样),令人惊讶的是,将它们结合使用非常容易。 在本节中看到,我们将使用完全相同的类(或更确切地说,也提供描述符提取方法的类)从我们在上一节中使用各种类发现的关键点中提取特征描述符,以在场景图像中找到对象。 重要的是要注意,并非所有提取的关键点都与所有描述符兼容,并且并非所有算法(在这种情况下为Feature2D
子类)都提供detect
函数和compute
函数。 不过,这样做的人还提供了detectAndCompute
函数,可以一次性完成关键点检测和特征提取,并且比分别调用这两个函数要快。 让我们从第一个示例案例开始,以便使所有这些变得更加清晰。 这也是匹配两个单独图像的特征所需的所有步骤的示例,这些图像可用于检测,比较等:
- 首先,我们将使用 AKAZE 算法(使用上一节中学习的
AKAZE
类)从以下图像中检测关键点:
我们可以使用以下代码从两个图像中提取关键点:
using namespace cv; using namespace std; Mat image1 = imread("image1.jpg"); Mat image2 = imread("image2.jpg"); Ptr<AKAZE> akaze = AKAZE::create(); // set AKAZE params ... vector<KeyPoint> keypoints1, keypoints2; akaze->detect(image1, keypoints1); akaze->detect(image2, keypoints2);
- 现在我们有了两个图像的特征(或关键点),我们可以使用相同的
AKAZE
类实例从这些关键点提取描述符。 这是完成的过程:
Mat descriptor1, descriptor2; akaze->compute(image1, keypoints1, descriptor1); akaze->compute(image2, keypoints2, descriptor2);
- 现在,我们需要匹配两个图像,这是两个图像中关键点的描述符。 为了能够执行描述符匹配操作,我们需要在 OpenCV 中使用一个名为
DescriptorMatcher
(非常方便)的类。 需要特别注意的是,此匹配器类需要设置为正确的类型,否则,将不会得到任何结果,或者甚至可能在运行时在应用中遇到错误。 如果在本示例中使用 AKAZE 算法来检测关键点并提取描述符,则可以在DescriptorMatcher
中使用FLANNBASED
类型。 这是完成的过程:
descMather = DescriptorMatcher::create( DescriptorMatcher::FLANNBASED);
请注意,您可以将以下值之一传递给DescriptorMatcher
的创建函数,并且这完全取决于您用来提取描述符的算法,显然,因为将对描述符执行匹配。 您始终可以参考每种算法的文档,以了解可用于任何特定描述符类型的算法,例如AKAZE
和KAZE
之类的算法具有浮点类型描述符,因此可以将FLANNBASED
与他们一起使用; 但是,具有String
类型的描述符(例如ORB
)将需要与描述符的汉明距离匹配的匹配方法。 以下是可用于匹配的现有方法:
FLANNBASED
BRUTEFORCE
BRUTEFORCE_L1
BRUTEFORCE_HAMMING
BRUTEFORCE_HAMMINGLUT
BRUTEFORCE_SL2
当然,最坏的情况是,当您不确定不确定时,尝试为每种特定的描述符类型找到正确的匹配算法时,只需简单地尝试每个。
- 现在,我们需要调用
DescriptorMatcher
的match
函数,以尝试将第一张图像(或需要检测的对象)中找到的关键点与第二张图像(或可能包含我们的对象的场景)中的关键点进行匹配。match
函数将需要一个DMatch
向量,并将所有匹配结果填充其中。 这是完成的过程:
vector<DMatch> matches; descMather->match(descriptor1, descriptor2, matches);
DMatch
类是简单类,仅用作保存匹配结果数据的结构:
- 在深入研究如何解释匹配操作的结果之前,我们将学习如何使用
drawMatches
函数。 与drawKeypoints
函数相似,drawMatches
可用于自动创建适合显示的输出结果。 这是如何做:
drawMatches(inputImage,keypoints1,secondImage,keypoints2,matches,outputImage,Scalar(0, 255, 0), // green for matchedScalar::all(-1), // unmatched color (default)vector<char>(), // empty maskDrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
在前面的代码中,dispImg
显然是可以显示的Mat
类。 这是结果图像:
现在,颜色更适合我们在这里使用的颜色。 还应注意一些很不正常的不正确匹配,可以通过修改 KAZE 算法的参数甚至使用其他算法来解决。 现在让我们看看如何解释匹配结果。
-
解释匹配结果完全取决于用例。 例如,如果我们要匹配两个具有相同大小和相同内容类型的图像(例如人脸,相同类型的对象,指纹等),则我们可能需要考虑距离值高于某个阈值的,匹配的关键点数量。 或者,就像在当前示例中一样,我们可能希望使用匹配来检测场景中的对象。 这样做的一种常见方法是尝试找出匹配关键点之间的单应性变化。 为此,我们需要执行以下三个操作:
- 首先,我们需要过滤出匹配结果,以消除较弱的匹配,换句话说,仅保留良好的匹配; 同样,这完全取决于您的场景和对象,但是通常,通过几次尝试和错误,您可以找到最佳阈值
- 接下来,我们需要使用
findHomography
函数来获得好关键点之间的单应性变化 - 最后,我们需要使用
perspectiveTransform
将对象边界框(矩形)转换为场景
这篇关于OpenCV3 和 Qt5 计算机视觉 学习笔记 - 特征和描述符的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!