一步一步实现多尺度多角度的形状匹配算法(C++版本)

前言

用过halcon形状匹配的都知道,这个算子贼好用,随便截一个ROI做模板就可以在搜索图像中匹配到相似的区域,并且能输出搜索图像的位置,匹配尺度,匹配角度。现在我们就要利用opencv在C++的环境下复现这个效果。

我们先看下复现的效果图,提升下学习的欲望(要在搜索图像中找到所有的K字母)。

下图是模板图像,为一个"K"字母。

一步一步实现多尺度多角度的形状匹配算法(C++版本)_第1张图片

下图是待搜索的图像,其中的K字符存在旋转,缩放,残缺遮挡,要利用上面的"K"字母模板在下图中找到所以的K字母。并输出它的位置,旋转角度,尺度,相似度。

一步一步实现多尺度多角度的形状匹配算法(C++版本)_第2张图片

下图是经过形状匹配后的结果图像,可以看到匹配的结果,用蓝色画出来,输出相似度,匹配角度,尺度。匹配时间为0.201040s。

一步一步实现多尺度多角度的形状匹配算法(C++版本)_第3张图片

原理简介

不具备多角度,多尺度的形状匹配原理

1.对模板图像进行特征提取,并存储特征信息

2.对搜索图像进行特征提取

3.将模板图像的特征信息在搜索图像上进行特征相似度比对,然后滑动窗口继续比对

4.直到比对完所有的搜索图像区域,则生成相似度矩阵

5.根据具体需求对相似度矩阵进行操作

具备多角度,多尺度的形状匹配原理

1.对模板图像进行特征提取,并存储特征信息,然后对模板图像进行多角度变换(旋转)后进行特征提前,然后存储,再对各个多角度变换后的模板图像进行多尺度变换(缩放),再存储。则一个旋转范围为0~360°,旋转步长为1°,缩放范围为0.9~1.1,缩放步长为0.1的模板个数为((360-0)/1) * ((1.1-0.9)/0.1) = 360 * 20 = 7200个模板,制作模板就是存储这7200个模板特征

2.对搜索图像进行特征提取

3.将所有的模板图像(比如这边的7200个)的特征信息依次在搜索图像上进行特征相似度比对,然后滑动窗口继续比对

4.直到所有的模板比对完所有的搜索图像区域,则生成相似度矩阵(7200个模板就是7200个相似度矩阵)

5.根据具体需求对所有的相似度矩阵进行操作

流程简介

主要流程就是两步

1.制作模板

2.开始匹配(使用模板在搜索图像上进行匹配)

制作模板

对所有的模板进行特征提取,这边使用类似于canny边缘提取的算法来提取特征。

开始匹配

对搜索图像提取特征,将所有的模板在搜索图像上进行相似度计算,然后滑动窗口直到匹配完成。

代码实现与分析

我们将实现三个主要的函数功能:制作模板(MakingTemplates),加载模板(LoadModel),匹配(Matching)。

/*
@model: 输入图像
@angle_range: 角度范围
@scale_range: 尺度范围
@num_features: 特征数
@weak_thresh:弱阈值
@strong_thresh: 强阈值
@mask: 掩码
*/
void MakingTemplates(Mat model, AngleRange angle_range, ScaleRange scale_range,
	int num_features, float weak_thresh = 30.0f, float strong_thresh = 60.0f,
	Mat mask = Mat());
/*
加载模型
*/
void LoadModel();
/*
@source: 输入图像
@score_thresh: 匹配分数阈值
@overlap: 重叠阈值
@mag_thresh: 最小梯度阈值
@greediness: 贪婪度,越小匹配越快,但是可能无法匹配到目标
@pyrd_level: 金字塔层数,越大匹配越快,但是可能无法匹配到目标
@T: T参数
@top_k: 最多匹配多少个
@strategy: 精确匹配(0), 普通匹配(1), 粗略匹配(2)
@mask: 匹配掩码
*/
vector Matching(Mat source, float score_thresh = 0.9f, float overlap = 0.4f,
	float mag_thresh = 30.f, float greediness = 0.8f, PyramidLevel pyrd_level = PyramidLevel_3,
	int T = 2, int top_k = 0, MatchingStrategy strategy = Strategy_Accurate, const Mat mask = Mat());

制作模板

先来看两个结构体AngleRange(角度范围(起始角度,终止角度,角度步长))和ScaleRange(尺度范围(起始尺度,终止尺度,角度尺度))。

struct MatchRange
{
	float begin;
	float end;
	float step;

	MatchRange() : begin(0.f), end(0.f), step(0.f) {}
	MatchRange(float b, float e, float s);
};
inline MatchRange::MatchRange(float b, float e, float s) : begin(b), end(e), step(s) {}
typedef struct MatchRange AngleRange; // 角度范围(起始角度,终止角度,角度步长)
typedef struct MatchRange ScaleRange; // 尺度范围(起始尺度,终止尺度,角度尺度)

制作模板的代码

void KcgMatch::MakingTemplates(Mat model, AngleRange angle_range, ScaleRange scale_range,
	int num_features,float weak_thresh, float strong_thresh, Mat mask) {

	ClearModel();
	// padding模板和模板掩码
	// 为什么要padding呢?因为在制作旋转模板的时候可能丢失有效区域
	PaddingModelAndMask(model, mask, scale_range.end);
	// 初始化角度,尺度范围
	angle_range_ = angle_range;
	scale_range_ = scale_range;
	// 生成所有的模板信息
	vector shape_infos = ProduceShapeInfos(angle_range, scale_range);
	vector l0_mdls; l0_mdls.clear();
	vector l0_msks; l0_msks.clear();
	// 生成所有模板的底层金字塔图像
	for (int s = 0; s < shape_infos.size(); s++) {

		l0_mdls.push_back(MdlOf(model, shape_infos[s]));
		l0_msks.push_back(MskOf(mask, shape_infos[s]));
	}
	// 对所有层的金字塔图像进行特征提取(这边最多8层,只制作8层,足够用)
	for (int p = 0; p <= PyramidLevel_7; p++) {

		// 某层金字塔的所有角度,尺度图像
		for (int s = 0; s < shape_infos.size(); s++) {

			Mat mdl_pyrd = l0_mdls[s];
			Mat msk_pyrd = l0_msks[s];
			if (p > 0) {

				Size sz = Size(l0_mdls[s].cols >> 1, l0_mdls[s].rows >> 1);
				pyrDown(l0_mdls[s], mdl_pyrd, sz);
				pyrDown(l0_msks[s], msk_pyrd, sz);
			}
			// 为什么要erode?因为有效特征信息可能在边缘
			erode(msk_pyrd, msk_pyrd, Mat(), Point(-1, -1), 1, BORDER_REPLICATE);
			l0_mdls[s] = mdl_pyrd;
			l0_msks[s] = msk_pyrd;

			// 计算某层金字塔需要的特征数
			int features_pyrd = (int)((num_features >> p) * shape_infos[s].scale);

			Mat mag8, angle8, quantized_angle8;
			// 量化边缘特征(8个方向)
			QuantifyEdge(mdl_pyrd, angle8, quantized_angle8, mag8, weak_thresh, false);
			// 提取模板信息(8个方向)
			Template templ = ExtractTemplate(	angle8, quantized_angle8, mag8, 
												shape_infos[s], PyramidLevel(p),
												weak_thresh, strong_thresh,
												features_pyrd, msk_pyrd);
			templ_all_[p].push_back(templ);

			Mat mag180, angle180, quantized_angle180;
			// 量化边缘特征(180个方向)
			QuantifyEdge(mdl_pyrd, angle180, quantized_angle180, mag180, weak_thresh, true);
			// 提取模板信息(180个方向)
			templ = ExtractTemplate(	angle180, quantized_angle180, mag180,
										shape_infos[s], PyramidLevel(p),
										weak_thresh, strong_thresh,
										features_pyrd, msk_pyrd);
			templ_all_[p + 8].push_back(templ);

			// 画出提取过程
			Mat draw_mask;
			msk_pyrd.copyTo(draw_mask);
			DrawTemplate(draw_mask, templ, Scalar(0));
			imshow("draw_mask", draw_mask);
			waitKey(1);
		}
		cout << "train pyramid level " << p << " complete." << endl;
	}
	// 保存模板
	SaveModel();
}

加载模板

void KcgMatch::LoadModel() {

	// 制作好的模板为一个yaml文件,里面存取了特征信息,这边就是读取该文件
	// 将所有的模板特征读到内存
	ClearModel();
	string model_name = model_root_ + class_name_ + KCG_MODEL_SUFFUX;
	FileStorage fs(model_name, FileStorage::READ);
	assert(fs.isOpened() && "load model failed.");
	FileNode fn = fs.root();
	angle_range_.begin = fn["angle_range_bgin"];
	angle_range_.end = fn["angle_range_end"];
	angle_range_.step = fn["angle_range_step"];
	scale_range_.begin = fn["scale_range_bgin"];
	scale_range_.end = fn["scale_range_end"];
	scale_range_.step = fn["scale_range_step"];
	FileNode tps_fn = fn["templates"];
	FileNodeIterator tps_it = tps_fn.begin(), tps_it_end = tps_fn.end();
	for (; tps_it != tps_it_end; ++tps_it)
	{
		int template_id = (*tps_it)["template_id"];
		FileNode pyrds_fn = (*tps_it)["template_pyrds"];
		FileNodeIterator pyrd_it = pyrds_fn.begin(), pyrd_it_end = pyrds_fn.end();
		int pl = 0;
		for (; pyrd_it != pyrd_it_end; ++pyrd_it)
		{
			FileNode pyrd_fn = (*pyrd_it);
			Template templ;
			templ.id = pyrd_fn["id"];
			templ.pyramid_level = pyrd_fn["pyramid_level"];
			templ.is_valid = pyrd_fn["is_valid"];
			templ.x = pyrd_fn["x"];
			templ.y = pyrd_fn["y"];
			templ.w = pyrd_fn["w"];
			templ.h = pyrd_fn["h"];
			templ.shape_info.scale = pyrd_fn["shape_scale"];
			templ.shape_info.angle = pyrd_fn["shape_angle"];
			FileNode features_fn = pyrd_fn["features"];
			FileNodeIterator feature_it = features_fn.begin(), feature_it_end = features_fn.end();
			for (; feature_it != feature_it_end; ++feature_it)
			{
				FileNode feature_fn = (*feature_it);
				FileNodeIterator feature_info = feature_fn.begin();
				Feature feat;
				feature_info >> feat.x >> feat.y >> feat.lbl;
				templ.features.push_back(feat);
			}
			templ_all_[pl].push_back(templ);
			pl++;
		}
	}

	LoadRegion8Idxes();
}

匹配

vector KcgMatch::Matching(Mat source, float score_thresh, float overlap,
	float mag_thresh, float greediness, PyramidLevel pyrd_level, int T, int top_k,
	MatchingStrategy strategy, const Mat mask) {

	// 初始化匹配参数
	InitMatchParameter(score_thresh, overlap, mag_thresh, greediness, T, top_k, strategy);
	// 获取搜索图像进行所有的有效金字塔层图像
	GetAllPyramidLevelValidSource(source, pyrd_level);

	vector matches; 
	// 从最高层金字塔开始匹配量化为8方向的图像相似度矩阵
	matches = MatchingPyrd8(sources_[pyrd_level], pyrd_level, region8_idxes_);
	// 获取前K个最相似match
	matches = GetTopKMatches(matches);

	// 再次确认
	matches = ReconfirmMatches(matches, pyrd_level);
	matches = GetTopKMatches(matches);

	// 最后匹配,从金字塔顶层还原到底层ROI开始匹配
	matches = MatchingFinal(matches, pyrd_level);
	matches = GetTopKMatches(matches);

	// 返回指定的匹配
	return matches;
}

完整代码

KcgMatch.h

/*M///
//
// Author	: KayChan
// Explain	: Shape matching
//
//M*/

#ifndef _KCG_MATCH_H_
#define _KCG_MATCH_H_

#include 
#include 
 
#ifndef ATTR_ALIGN
#  if defined(__GNUC__)
#    define ATTR_ALIGN(n)	__attribute__((aligned(n)))
#  else
#    define ATTR_ALIGN(n)	__declspec(align(n))
#  endif
#endif // #ifndef ATTR_ALIGN

using namespace cv;
using namespace std;

namespace kcg{

struct MatchRange
{
	float begin;
	float end;
	float step;

	MatchRange() : begin(0.f), end(0.f), step(0.f) {}
	MatchRange(float b, float e, float s);
};
inline MatchRange::MatchRange(float b, float e, float s) : begin(b), end(e), step(s) {}
typedef struct MatchRange AngleRange;
typedef struct MatchRange ScaleRange;

typedef struct ShapeInfo_S
{
	float angle;
	float scale;
}ShapeInfo;

typedef struct Feature_S
{
	int x;
	int y;
	int lbl;
}Feature;

typedef struct Candidate_S
{
	/// Sort candidates with high score to the front
	bool operator<(const struct Candidate_S &rhs) const
	{
		return score > rhs.score;
	}
	float score;
	Feature feature;
}Candidate;

typedef struct Template_S
{
	int id = 0;
	int pyramid_level = 0;
	int is_valid = 0;
	int x = 0;
	int y = 0;
	int w = 0;
	int h = 0;
	ShapeInfo shape_info;
	vector features;
}Template;

typedef struct Match_S
{
	/// Sort matches with high similarity to the front
	bool operator<(const struct Match_S &rhs) const
	{
		// Secondarily sort on template_id for the sake of duplicate removal
		if (similarity != rhs.similarity)
			return similarity > rhs.similarity;
		else
			return template_id < rhs.template_id;
	}

	bool operator==(const struct Match_S &rhs) const
	{
		return x == rhs.x && y == rhs.y && similarity == rhs.similarity;
	}

	int x;
	int y;
	float similarity;
	int template_id;
}Match;

typedef enum PyramidLevel_E
{
	PyramidLevel_0 = 0,
	PyramidLevel_1 = 1,
	PyramidLevel_2 = 2,
	PyramidLevel_3 = 3,
	PyramidLevel_4 = 4,
	PyramidLevel_5 = 5,
	PyramidLevel_6 = 6,
	PyramidLevel_7 = 7,
	PyramidLevel_TabooUse = 16,
}PyramidLevel;

typedef enum MatchingStrategy_E
{
	Strategy_Accurate = 0,
	Strategy_Middling = 1,
	Strategy_Rough = 2,
}MatchingStrategy;

class KcgMatch
{
public:

	KcgMatch(string model_root, string class_name);
	~KcgMatch();
	/*
	@model: 输入图像
	@angle_range: 角度范围
	@scale_range: 尺度范围
	@num_features: 特征数
	@weak_thresh:弱阈值
	@strong_thresh: 强阈值
	@mask: 掩码
	*/
	void MakingTemplates(Mat model, AngleRange angle_range, ScaleRange scale_range,
		int num_features, float weak_thresh = 30.0f, float strong_thresh = 60.0f,
		Mat mask = Mat());
	/*
	加载模型
	*/
	void LoadModel();
	/*
	@source: 输入图像
	@score_thresh: 匹配分数阈值
	@overlap: 重叠阈值
	@mag_thresh: 最小梯度阈值
	@greediness: 贪婪度,越小匹配越快,但是可能无法匹配到目标
	@pyrd_level: 金字塔层数,越大匹配越快,但是可能无法匹配到目标
	@T: T参数
	@top_k: 最多匹配多少个
	@strategy: 精确匹配(0), 普通匹配(1), 粗略匹配(2)
	@mask: 匹配掩码
	*/
	vector Matching(Mat source, float score_thresh = 0.9f, float overlap = 0.4f,
		float mag_thresh = 30.f, float greediness = 0.8f, PyramidLevel pyrd_level = PyramidLevel_3,
		int T = 2, int top_k = 0, MatchingStrategy strategy = Strategy_Accurate, const Mat mask = Mat());
	void DrawMatches(Mat &image, vector matches, Scalar color);

protected:
	void PaddingModelAndMask(Mat &model, Mat &mask, float max_scale);
	vector ProduceShapeInfos(AngleRange angle_range, ScaleRange scale_range);
	Mat Transform(Mat src, float angle, float scale);
	Mat MdlOf(Mat model, ShapeInfo info);
	Mat MskOf(Mat mask, ShapeInfo info);
	void DrawTemplate(Mat &image, Template templ, Scalar color);
	void QuantifyEdge(Mat image, Mat &angle, Mat &quantized_angle, Mat &mag, float mag_thresh, bool calc_180 = true);
	void Quantify8(Mat angle, Mat &quantized_angle, Mat mag, float mag_thresh);
	void Quantify180(Mat angle, Mat &quantized_angle, Mat mag, float mag_thresh);
	Template ExtractTemplate(Mat angle, Mat quantized_angle, Mat mag, ShapeInfo shape_info, 
		PyramidLevel pl, float weak_thresh, float strong_thresh, int num_features, Mat mask);
	Template SelectScatteredFeatures(vector candidates, int num_features, float distance);
	Rect CropTemplate(Template &templ);
	void LoadRegion8Idxes();
	void ClearModel();
	void SaveModel();
	void InitMatchParameter(float score_thresh, float overlap, float mag_thresh, float greediness, int T, int top_k, MatchingStrategy strategy);
	void GetAllPyramidLevelValidSource(Mat &source, PyramidLevel pyrd_level);
	vector GetTopKMatches(vector matches);
	vector DoNmsMatches(vector matches, PyramidLevel pl, float overlap);
	vector MatchingPyrd180(Mat src, PyramidLevel pl, vector region_idxes = vector());
	vector MatchingPyrd8(Mat src, PyramidLevel pl, vector region_idxes = vector());
	void Spread(const Mat quantized_angle, Mat &spread_angle, int T);
	void ComputeResponseMaps(const Mat spread_angle, vector &response_maps);
	bool CalcPyUpRoiAndStartPoint(PyramidLevel cur_pl, PyramidLevel obj_pl, Match match,
		Mat &r, Point &p, bool is_padding = false);
	void CalcRegionIndexes(vector ®ion_idxes, Match match, MatchingStrategy strategy);
	vector ReconfirmMatches(vector matches, PyramidLevel pl);
	vector MatchingFinal(vector matches, PyramidLevel pl);

private:
	typedef vector