相位相关匹配法的opencv C++实现

这篇具有很好参考价值的文章主要介绍了相位相关匹配法的opencv C++实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言:一个图像拼接的小项目,用途场景,显微图像的拼接,或者只包含x,y平移的图像拼接。本来是显微镜拼接工具,MIST的核心拼接代码,matlab版的,已经开源。下面是地址,

GitHub - usnistgov/MIST at mist-matlab

 源码是一个显微镜的拼接工具,序列扫描,2D扫描的图像拼接,下面实现的只是两张图像的拼接,并且需要先验知识,输入图像1与输入图像2的相对位置,1在2的北边,1在2的西边,两种固定方向。

内容:

1.相位相关匹配法

相位相关图像匹配(Phase Correlation Image Matching)是一种用于在图像中寻找相似区域的方法,特别适用于图像的平移、旋转和缩放等变换。其原理基于图像的频域表示和互相关运算。

以下是相位相关图像匹配的基本流程:

预处理:首先,对待匹配的两幅图像进行预处理。这包括灰度化、归一化、去噪或滤波等操作,以提高匹配的准确性。

傅里叶变换:对两幅预处理后的图像应用傅里叶变换,将它们转换到频域。傅里叶变换将图像从空域表示转换为频域表示,其中频率较高的部分对应图像中的边缘和纹理信息。

计算互相关谱:通过计算参考图像和待匹配图像的频域表示之间的互相关谱,得到一个复数矩阵。互相关谱反映了两幅图像之间的相似度信息。

计算幅度谱:从互相关谱中提取幅度谱,即取复数矩阵的模。幅度谱表示图像中的结构信息,可以用来检测图像的平移、旋转和缩放等变换。

计算逆傅里叶变换:对幅度谱进行逆傅里叶变换,将其转换回空域表示。得到的结果是一个实数矩阵,称为相位相关图像。

寻找峰值:在相位相关图像中寻找峰值点,即响应最强的位置。峰值点的位置表示了两幅图像之间的最佳匹配位置。

精细化匹配:在峰值点附近进行精细化匹配,可以使用插值等方法来获取更精确的匹配结果。

相位相关图像匹配的原理是基于相位信息的保持不变性,相位谱的峰值对应于最佳匹配位置。通过将图像转换到频域,利用频域的特性进行匹配,可以提高匹配的准确性和鲁棒性。

需要注意的是,相位相关图像匹配方法在处理包含大量变形的图像时可能存在一些局限性。在这种情况下,可能需要使用更高级的图像匹配方法来获得更好的结果。

详细的物理原理参考这篇博客,图像配准之相位配准-CSDN博客 ,当然目前相关的论文已经发展到可以计算图像的场景旋转,但是在显微镜扫描的场景中默认是没有旋转,尺度变化,畸变变形等情况,仅存在xy平移,虽然目前的载物台精度已达到很高的精度,但也很难直接反馈得到准确的像素偏移。具体原因可以稍微分析一下,像与物体的大小可以认为是一一对应的关系,像的尺度表示就是相机的像元尺寸,一般为2~7微米,显微镜系统的放大倍率,高倍的话就是10,20,40倍,那么可以计算大概计算一下,一个像元,也就是一个像素代表多少。以7微米,40倍为例,7/40 = 0.175 um = 175 nm,当然只能大概这么算,事实上与波长尺度相关的计算需要考虑的东西很多,比如显微镜的口径,还有NA数值孔径。emmm,有点扯远了,总之就是大概介绍一下。意思就是目前的电动载物台可以达到100nm但是逐像素的精准移动,很难,所以还是需要计算。

好了接下来看代码(github感觉有点麻烦,后期会试试):

配置:VS2022 opencv 4.6.0 

文件树:

相位相关匹配法的opencv C++实现,opencv,计算机视觉

 头文件,以及各类函数,还有主函数,第一次独立的写这种分文件类型的项目,写的拉跨还请包涵,以及当然可以转成类的形式,但感觉有点小麻烦,后期再修改吧。

1.PCIAM.h

#ifndef PCIAM_H
#define PCIAM_H

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/core/cuda.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <limits>


using namespace std;
using namespace cv;
using namespace cuda;

struct PCC_type {

	int x;
	int y;
	double value;
};
PCC_type initial(int x, int y, double value);


//图像显示
void showImg(Mat& Img, string windowname);
//图像预处理,数据转换,单通道
Mat Img_process(Mat& Img);
//计算PCM
Mat computePCM(Mat& I1, Mat& I2);
//峰值搜索
void peaksearch(Mat& PCM, int& num_peaks, vector<double>& max_value, vector<Point>& max_Loc);
//计算PCC
PCC_type computePCC(Mat& I1, Mat& I2, string& direction, Point& Loc);
//提取ROI
Mat grap_roi(Mat& Img, int x, int y);
//计算归一化互相关值
double computeNCC(Mat I1, Mat I2);
//加权融合
Mat awblending(const Mat& I1, const Mat& I2, int shiftx, int shifty);
//羽化融合
Mat featherblending(Mat& I1, Mat& I2, int& shiftx, int& shifty);

#endif // !PCIAM_H

 头文件,一些头文件,一些功能函数的声明,注释都有,所以就不详解了,所有函数,基本上都是使用opencv是实现的。

2.Img_process.cpp

#include "PCIAM.h"


Mat Img_process(Mat& Img) {

	Mat Img_p;
	extractChannel(Img, Img_p, 1); //提取绿色通道
	Img_p.convertTo(Img_p, CV_64FC1);//转换数据类型 双精度浮点
	Img_p = Img_p / sum(Img_p)[0];
	//cout << Img_p.row(0) << endl;
	//cout << "second" << endl;
	//图像归一化
	//double sum_Img = sum(Img_p)[0];
	//sum(Img_p) 返回一个 Scalar 对象,这是一个包含四个元素的数组,
	//对于单通道图像,只有第一个元素是有意义的,它代表所有像素值的总和。
	//Img_p = Img_p / sum_Img;
	return Img_p;
}

这里是完全按照matlab代码来的,提取通道,一般绿色通道值更准确,然后转成double类型,再进行归一化,它这个归一化是除以所有像素点之和,而不是除以255之类的,需要注意。

3.computePCM函数

#include "PCIAM.h"


Mat computePCM(Mat& I1, Mat& I2) {

	//傅里叶变换
	Mat Img1_DFT, Img2_DFT;
	dft(I1, Img1_DFT, DFT_COMPLEX_OUTPUT);
	dft(I2, Img2_DFT, DFT_COMPLEX_OUTPUT);
	//cout << Img1_DFT.row(0) << endl;
	//Img2_DFT共轭变换后并相乘,
	Mat FFT1;
	mulSpectrums(Img1_DFT, Img2_DFT, FFT1, 0, true);

	//替换零值
	FFT1.forEach<Vec2d>([](Vec2d& value, const int* position) {
		if (value[0] == 0 && value[1] == 0) {
			value[0] = numeric_limits<double>::epsilon();  //当实部虚部都为0,只需要换实部即可
		}
		});

	//计算模值
	Mat channels[2];
	split(FFT1, channels);

	Mat abs;
	magnitude(channels[0], channels[1], abs);

	//计算互功率谱
	Mat CPS;
	Mat CPS_real = channels[0] / abs;
	Mat CPS_imag = channels[1] / abs;

	vector<Mat> channels2 = { CPS_real ,CPS_imag };
	merge(channels2, CPS);

	//	逆傅里叶变换
	Mat PCM;
	dft(CPS, PCM, DFT_INVERSE | DFT_REAL_OUTPUT | DFT_SCALE);

	//先验经验,重叠部分不会在边缘,而PCIAM算法往往在边缘处的峰值高
	//所以进行峰值过滤
	PCM.row(0).setTo(Scalar(0.0));
	PCM.row(PCM.rows - 1).setTo(Scalar(0.0));

	PCM.col(0).setTo(Scalar(0));
	PCM.col(PCM.cols - 1).setTo(Scalar(0.0));

	return PCM;
}

这一段就是PCIAM算法的核心了,计算两幅图像的峰值相关矩阵,也是完全按照matlab移植过来的,但是后面几行,是为了筛选一些不可能存在的情况,比如边缘处的完全重合,或者只有一行,或者一列重合等等情况,由于PCIAM算法在边缘处的响应值高,所以在一些场景下可能是一种干扰,所以进行了剔除,当然可以不剔除,就是对后期的峰值选取会有影响。

相位相关匹配法的opencv C++实现,opencv,计算机视觉

4.peaksearch

#include "PCIAM.h"


void peaksearch(Mat& PCM, int& num_peaks, vector<double>& max_value, vector<Point>& max_Loc) {
	for (int i = 0; i < num_peaks; i++) {
		double minValue, maxValue;
		Point  minLoc, maxLoc;
		minMaxLoc(PCM, &minValue, &maxValue, &minLoc, &maxLoc);
		max_value.push_back(maxValue);
		max_Loc.push_back(maxLoc);

		// 将找到的最大值置为一个较小的值,以便继续查找下一个最大值
		PCM.at<double>(maxLoc) = -numeric_limits<double>::max();
	}

}

这步就是从峰值相关矩阵中,选择峰值点,由于存在噪声点,所以选取多个峰值点位置,这个看自己情况,一般10个。

5. compute_PCC

#include "PCIAM.h"


PCC_type computePCC(Mat& I1, Mat& I2, string& direction, Point& Loc) {
	int h = I1.rows;
	int w = I1.cols;

	int x = Loc.x;
	int y = Loc.y;

	vector<int>m;
	vector<int>n;
	PCC_type PCC;
	PCC = initial(0, 0, 0.0);

	//8种可能的平移解释
	if (direction == "north") {
		m = { y,y,h - y,h - y,y,y,h - y,h - y };
		n = { x,w - x,x,w - x,-x,x - w,-x,x - w };
	}
	else if (direction == "west") {
		m = { y,y,h - y,h - y,-y,-y,y - h,y - h };
		n = { x,w - x,x,w - x,x,w - x,x,w - x };
	}
	else {
		cerr << "direction is wrong,please retry follow the ture direction" << endl;

		return PCC;
	}

	vector<double>value;//ncc值
	Point position;//最佳位置

	for (int i = 0; i < m.size(); i++) {
		Mat I1_roi = grap_roi(I1, n[i], m[i]);
		Mat I2_roi = grap_roi(I2, -n[i], -m[i]);

		//roi大小不一样,roi没有长或者框,直接返回n[i],m[i],-1
		if (I1_roi.size() != I2_roi.size()) {
			//cerr << "Error: Image sizes do not match." << endl;
			PCC = initial(n[i], m[i], -1);
			value.push_back(PCC.value);
		}
		else if (I1_roi.rows == 0 || I1_roi.cols == 0 || I2_roi.rows == 0 || I2_roi.cols == 0) {
			//cerr << "roi is empty " << endl;
			PCC = initial(n[i], m[i], -1);
			value.push_back(PCC.value);
		}
		else {
			value.push_back(computeNCC(I1_roi, I2_roi));
		}
	}

	auto max_iter = max_element(value.begin(), value.end()); //max_element,返回的是最大位置处的迭代器
	int idx = distance(value.begin(), max_iter); //得到最大值坐标
	PCC = initial(n[idx], m[idx], value[idx]); //返回PCC内容

	return PCC;
}

由于选出来多个峰值点位置,都有可能是对的匹配位置,又因为傅里叶变换的周期性,每个峰值点位置,又存在多种变化(这部分解释可以看mist的说明文件),所以需要在原图上截取重叠部分计算NCC归一化互相关矩阵,来确定最优的位置。

6.featherblending 羽化融合

#include "PCIAM.h"

Mat featherblending(Mat& I1, Mat& I2, int& shiftx, int& shifty) {

	//转成双精度浮点
	I1.convertTo(I1, CV_64FC3);
	I2.convertTo(I2, CV_64FC3);

	//拼接后图像尺寸
	int fusionW = abs(shiftx) + I1.cols;
	int fusionH = abs(shifty) + I1.rows;
	//创建一个画布
	Mat fusionImg = Mat::zeros(fusionH, fusionW, I1.type());

	//I1,I2起始位置的确定
	int x1_st, y1_st, x2_st, y2_st;
	if (shiftx >= 0) {
		x1_st = 0;
		x2_st = shiftx;
	}
	else {
		x1_st = abs(shiftx);
		x2_st = 0;
	}
	if (shifty >= 0) {
		y1_st = 0;
		y2_st = shifty;
	}
	else {
		y1_st = abs(shifty);
		y2_st = 0;
	}

	//先把I1放上去
	I1.copyTo(fusionImg(Rect(x1_st, y1_st, I1.cols, I1.rows)));
	//分别提取I1,I2的重叠区域
	Mat roi1 = grap_roi(I1, shiftx, shifty);
	Mat roi2 = grap_roi(I2, -shiftx, -shifty);

	//创建掩码矩阵,这里是生成I1大小的羽化矩阵采用
	float featherAmount = 1.5;//羽化量
	Mat mask = Mat::zeros(I1.size(), CV_64FC3);
	Mat hang = Mat::zeros(1, I1.cols, CV_64F);
	Mat lie = Mat::zeros(I1.rows, 1, CV_64F);

	for (int i = 1; i < hang.cols + 1; i++) {
		hang.at<double>(0, i - 1) = min(i, hang.cols - i + 1);
	}

	for (int j = 1; j < lie.rows + 1; j++) {
		lie.at<double>(j - 1, 0) = min(j, lie.rows - j + 1);
	}

	Mat channels[3];
	channels[0] = lie * hang;
	channels[1] = lie * hang;
	channels[2] = lie * hang;
	merge(channels, 3, mask);

	//截取I1,I2重叠区域mask
	Mat I1mask = grap_roi(mask, shiftx, shifty);
	Mat I2mask = grap_roi(mask, -shiftx, -shifty);
	I1mask.convertTo(I1mask, roi1.depth());

	//融合计算

	multiply(roi1, I1mask, roi1);
	multiply(roi2, I2mask, roi2);

	Mat blending = (roi1 + roi2) / (I1mask + I2mask);

	cout << blending.size() << " " << blending.channels() << endl;

	//计算I2中重叠部分的位置
	// 计算子区域的起始和结束坐标
	int x_st = max(0, -shiftx);
	int y_st = max(0, -shifty);
	int x_end = min(I2.cols, I2.cols + -shiftx);
	int y_end = min(I2.rows, I2.rows + -shifty);

	//将blending放入I2中
	blending.copyTo(I2(Rect(x_st, y_st, x_end - x_st, y_end - y_st)));

	//再将I2放入fusionImg中。
	I2.copyTo(fusionImg(Rect(x2_st, y2_st, I2.cols, I2.rows)));

	//将fusion转为CV_8UC3
	fusionImg.convertTo(fusionImg, CV_8UC3);

	return fusionImg;
}

在确定出最佳位置后,根据二者位置,进行融合,融合方法采用羽化融合,可以自行百度,这里不详细解释。原理就是生成随中心距离变化的mask矩阵,对重叠部分进行羽化融合。

其余的工具文件。

1.grap_roi 

功能,给定一个图像,一个坐标点,从图像中提取出区域,x,y是起点。如果为负,则相反。

#include "PCIAM.h"

Mat grap_roi(Mat& Img, int x, int y) {

	// 计算图像的宽度和高度
	int w = Img.cols;
	int h = Img.rows;

	// 计算子区域的起始和结束坐标
	int x_st = max(0, x);
	int y_st = max(0, y);
	int x_end = min(w, w + x);
	int y_end = min(h, h + y);

	// 计算宽度和高度
	int width = x_end - x_st;
	int height = y_end - y_st;

	// 如果计算出的宽度或高度小于1,则返回空Mat
	if (width < 1 || height < 1) {
		return cv::Mat();
	}
	// 使用cv::Rect来裁剪子区域
	cv::Rect region_of_interest(x_st, y_st, width, height);
	return Img(region_of_interest).clone();  // 返回子区域的深拷贝
}

PCC_type

一个结构体,含义最后每个点计算出的结果,一个该点的坐标。

#include "PCIAM.h"

PCC_type initial(int x, int y, double value) {
	PCC_type p;
	p.x = x;
	p.y = y;
	p.value = value;
	return p;
}

3.showImg

显示图片,自定义大小窗口

#include "PCIAM.h"

void showImg(Mat& Img, string windowname) {

	namedWindow(windowname, WINDOW_NORMAL); //启动窗口
	resizeWindow(windowname, 800, 600); //调整窗口大小
	imshow(windowname, Img); //show图
}

最后看下主函数:

main.cpp

#include "PCIAM.h"

int main() {

	string Img1_fliepath = "D:/ManualWSI/C14_Liver fatty Degeneration/Liver_15.tif";
	string Img2_fliepath = "D:/ManualWSI/C14_Liver fatty Degeneration/Liver_16.tif";
	string direction = "north";  //west 或者north  I1 在 I2的上面或者左边

	//初始化opencv的cuda支持
	cuda::setDevice(0);

	//读图
	Mat Img1 = imread(Img1_fliepath);
	Mat Img2 = imread(Img2_fliepath);

	double start = static_cast<double>(getTickCount()); //计时

	//创建Img1,Img2的gpu副本
	//GpuMat d_I1,d_I2;
	//d_I1.upload(Img1);
	//d_I2.upload(Img2);

	//图像预处理,提取绿色通道,并归一化
	Mat I1p = Img_process(Img1);
	Mat I2p = Img_process(Img2);

	//计算峰值相关矩阵
	Mat PCM = computePCM(I1p, I2p);

	//搜索峰值及其位置
	int numPeaks = 1; //峰值搜索个数
	vector<double> max_value;
	vector<Point> max_Loc;
	peaksearch(PCM, numPeaks, max_value, max_Loc);

	//互相关筛选
	int idx = 0;
	int x = 0;
	int y = 0;
	double ncc_value = numeric_limits<double>::lowest();//最小值
	for (int i = 0; i < numPeaks; i++) {
		PCC_type ePCC = computePCC(I1p, I2p, direction, max_Loc[i]);
		if (ePCC.value > ncc_value) {
			x = ePCC.x;
			y = ePCC.y;
			ncc_value = ePCC.value;
		}
	}

	cout << "匹配位置 x:" << x << " y:" << y << " value:" << ncc_value << endl;

	Mat fusionImg1 = featherblending(Img1, Img2, x, y);

	double end = static_cast<double>(getTickCount());
	double totalTime = (end - start) / getTickFrequency();
	cout << "Code executed in " << totalTime << " seconds." << endl;  //计时结束

	showImg(fusionImg1, "fusion_img1");

	//	Mat fusionImg2 = awblending(Img1, Img2, x, y);
	//	showImg(fusionImg2, "fusion_img2");
	imwrite("liver.tif", fusionImg1);

	waitKey();
	return 0;
}

以上就是全部文件,流程与步骤大致与mist,PCIAM算法相同。后续结果,

原图:

 结果:

相位相关匹配法的opencv C++实现,opencv,计算机视觉

还有一个竖直方向的拼接结果:

相位相关匹配法的opencv C++实现,opencv,计算机视觉

 效果还不错的。在剔除后,峰值搜索数只选一个也能实现拼接。当然实际工程中还是要将numPeaks设置为合理值才行。文章来源地址https://www.toymoban.com/news/detail-762425.html

到了这里,关于相位相关匹配法的opencv C++实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++计算机视觉库OpenCV在Visual Studio 2022的配置方法

      本文介绍在 Visual Studio 2022 中配置、编译 C++ 计算机视觉库 OpenCV 的方法。   首先,我们进行 OpenCV 库的下载与安装。作为一个开源的库,我们直接在其官方下载网站(https://opencv.org/releases/)中进行下载即可;如下图所示,我们首先选择需要下载的操作系统。   随后

    2024年02月16日
    浏览(60)
  • 用于计算机视觉的 OpenCV(C++ 与 Python)与 MATLAB之间的优缺点比较

    我们经常混淆我们的工具和我们的手艺。工具可以帮助你练习手艺,但它们并不能使你成为一名优秀的工匠。一个好工匠的口袋里有许多不同的工具,她会明智地使用适合工作的工具。她没有嫁给这些工具。她嫁给了她的手艺。 我经常被关于编程语言的战争逗乐。人们对哪一

    2024年02月13日
    浏览(50)
  • 【计算机视觉】---OpenCV实现物体追踪

    OpenCV中的物体追踪算法基于视觉目标跟踪的原理。物体追踪的目标是在连续的图像序列中定位和跟踪特定物体的位置。 在物体追踪中,我们需要对目标对象进行表示。通常使用边界框(bounding box)来表示目标的位置和大小。边界框是一个矩形区域,由左上角的坐标(x,y)和

    2024年02月08日
    浏览(51)
  • 计算机视觉:使用opencv实现车牌识别

    汽车车牌识别(License Plate Recognition)是一个日常生活中的普遍应用,特别是在智能交通系统中,汽车牌照识别发挥了巨大的作用。汽车牌照的自动识别技术是把处理图像的方法与计算机的软件技术相连接在一起,以准确识别出车牌牌照的字符为目的,将识别出的数据传送至交

    2024年02月04日
    浏览(51)
  • 计算机视觉:使用opencv实现银行卡号识别

    OpenCV是Open Source Computer Vision Library(开源计算机视觉库)的简称,由Intel公司在1999年提出建立,现在由Willow Garage提供运行支持,它是一个高度开源发行的计算机视觉库,可以实现Windows、Linux、Mac等多平台的跨平台操作。opencv是一个用于图像处理、分析、机器视觉方面的开源函

    2024年02月05日
    浏览(47)
  • 【计算机视觉】OpenCV实现单目相机标定

    文章目录 单目相机标定(基于Python OpenCV) 1.上期填坑 2.单目相机标定 2.1 数据采集 2.2 角点提取 2.3 参数求解 2.4 参数评估(重投影误差) 2.5 相机位姿(棋盘位姿)可视化 2.6 同Matlab标定结果比较 在开始本篇博客之前,先填一下上一篇博客【计算机视觉】基于ORB角点+RANSAC算法实现图像

    2023年04月18日
    浏览(62)
  • 【计算机视觉】基于OpenCV计算机视觉的摄像头测距技术设计与实现

    在当今技术日益进步的时代,计算机视觉已成为我们生活中不可或缺的一部分。从智能监控到虚拟现实,计算机视觉技术的应用范围日益广泛。在这篇博客中,我们将探索一个特别实用的计算机视觉案例:使用OpenCV实现摄像头测距。这一技术不仅对专业人士有用,也为编程爱

    2024年02月04日
    浏览(55)
  • 【计算机视觉·OpenCV】使用Haar+Cascade实现人脸检测

    人脸检测的目标是找出图像中所有的人脸对应的位置,算法的输出是人脸的外接矩形在图像中的坐标。使用 haar 特征和 cascade 检测器进行人脸检测是一种传统的方式,下面将给出利用 OpenCV 中的 haarcascade 进行人脸检测的代码。 可选的人脸检测模型(区别是检测速度和精度不同

    2023年04月12日
    浏览(64)
  • 计算机竞赛 python+opencv+深度学习实现二维码识别

    🔥 优质竞赛项目系列,今天要分享的是 🚩 python+opencv+深度学习实现二维码识别 🥇学长这里给一个题目综合评分(每项满分5分) 难度系数:3分 工作量:3分 创新点:3分 该项目较为新颖,适合作为竞赛课题方向,学长非常推荐! 🧿 更多资料, 项目分享: https://gitee.com/danch

    2024年02月12日
    浏览(62)
  • 计算机竞赛 python+深度学习+opencv实现植物识别算法系统

    🔥 优质竞赛项目系列,今天要分享的是 🚩 基于深度学习的植物识别算法研究与实现 🥇学长这里给一个题目综合评分(每项满分5分) 难度系数:4分 工作量:4分 创新点:4分 🧿 更多资料, 项目分享: https://gitee.com/dancheng-senior/postgraduate Google DeepMind公司研究员与牛津大学计算

    2024年02月12日
    浏览(76)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包