机器视觉_图像算法(五)——分水岭

分水岭算法

建议

背景:OTSU、形态学得到背景

前景:距离变换公式得到前景

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

void waterSegment(InputArray& _src, OutputArray& _dst, int& noOfSegment);

int main(int argc, char** argv) {
    
    Mat inputImage = imread("coins.jpg");
    assert(!inputImage.data);
    Mat graImage, outputImage;
    int offSegment;
    waterSegment(inputImage, outputImage, offSegment);

    waitKey(0);
    return 0;
}

void waterSegment(InputArray& _src,OutputArray& _dst,int& noOfSegment)
{
    Mat src = _src.getMat();//dst = _dst.getMat();
    Mat grayImage;
    cvtColor(src, grayImage,CV_BGR2GRAY);
    threshold(grayImage, grayImage, 0, 255, THRESH_BINARY | THRESH_OTSU);
    Mat kernel = getStructuringElement(MORPH_RECT, Size(9, 9), Point(-1, -1));
    morphologyEx(grayImage, grayImage, MORPH_CLOSE, kernel);
    distanceTransform(grayImage, grayImage, DIST_L2, DIST_MASK_3, 5);
    normalize(grayImage, grayImage,0,1, NORM_MINMAX);
    grayImage.convertTo(grayImage, CV_8UC1);
    threshold(grayImage, grayImage,0,255, THRESH_BINARY | THRESH_OTSU);
    morphologyEx(grayImage, grayImage, MORPH_CLOSE, kernel);
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    Mat showImage = Mat::zeros(grayImage.size(), CV_32SC1);
    findContours(grayImage, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(-1, -1));
    for (size_t i = 0; i < contours.size(); i++)
    {
        //这里static_cast<int>(i+1)是为了分水岭的标记不同,区域1、2、3。。。。这样才能分割
        drawContours(showImage, contours, static_cast<int>(i), Scalar::all(static_cast<int>(i+1)), 2);
    }
    Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
    morphologyEx(src, src, MORPH_ERODE, k);
    watershed(src, showImage);

    //随机分配颜色
    vector<Vec3b> colors;
    for (size_t i = 0; i < contours.size(); i++) {
        int r = theRNG().uniform(0, 255);
        int g = theRNG().uniform(0, 255);
        int b = theRNG().uniform(0, 255);
        colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
    }

    // 显示
    Mat dst = Mat::zeros(showImage.size(), CV_8UC3);
    int index = 0;
    for (int row = 0; row < showImage.rows; row++) {
        for (int col = 0; col < showImage.cols; col++) {
            index = showImage.at<int>(row, col);
            if (index > 0 && index <= contours.size()) {
                dst.at<Vec3b>(row, col) = colors[index - 1];
            }
            else if (index == -1)
            {
                dst.at<Vec3b>(row, col) = Vec3b(255, 255, 255);
            }
            else {
                dst.at<Vec3b>(row, col) = Vec3b(0, 0, 0);
            }
        }
    }
}

封装子函数:

void segMerge(Mat& image, Mat& segments, int& numSeg)
{
    vector<Mat> samples;
    int newNumSeg = numSeg;
    //初始化变量长度的Vector
    for (size_t i = 0; i < newNumSeg; i++)
    {
        Mat sample;
        samples.push_back(sample);
    }
    for (size_t i = 0; i < segments.rows; i++)
    {
        for (size_t j = 0; j < segments.cols; j++)
        {
            int index = segments.at<uchar>(i, j);
            if (index >= 0 && index <= newNumSeg)//把同一个区域的点合并到一个Mat中
            {
                if (!samples[index].data)//数据为空不能合并,否则报错
                {
                    samples[index] = image(Rect(j, i, 1, 1));
                }
                else//按行合并
                {
                    vconcat(samples[index], image(Rect(j, i, 2, 1)), samples[index]);
                }
            }
            //if (index >= 0 && index <= newNumSeg)
            //    samples[index].push_back(image(Rect(j, i, 1, 1)));
        }
    }
    vector<Mat> hist_bases;
    Mat hsv_base;
    int h_bins = 35;
    int s_bins = 30;
    int histSize[2] = { h_bins , s_bins };
    float h_range[2] = { 0,256 };
    float s_range[2] = { 0,180 };
    const float* range[2] = { h_range,s_range };
    int channels[2] = { 0,1 };
    Mat hist_base;
    for (size_t i = 1; i < numSeg; i++)
    {
        if (samples[i].dims > 0)
        {
            cvtColor(samples[i], hsv_base, CV_BGR2HSV);
            calcHist(&hsv_base, 1, channels, Mat(), hist_base, 2, histSize, range);
            normalize(hist_base, hist_base, 0, 1, NORM_MINMAX);
            hist_bases.push_back(hist_base);
        }
        else
        {
            hist_bases.push_back(Mat());
        }
    }
    double similarity = 0;
    vector<bool> merged;//是否合并的标志位
    for (size_t i = 0; i < hist_bases.size(); i++)
    {
        for (size_t j = i+1; j < hist_bases.size(); j++)
        {
            if (!merged[j])//未合并的区域进行相似性判断
            {
                if (hist_bases[i].dims > 0 && hist_bases[j].dims > 0)//这里维数判断没必要,直接用个data就可以了
                {
                    similarity = compareHist(hist_bases[i], hist_bases[j], HISTCMP_BHATTACHARYYA);
                    if (similarity > 0.8)
                    {
                        merged[j] = true;//被合并的区域标志位true
                        if (i != j)//这里没必要,i不可能等于j
                        {
                            newNumSeg --;//分割部分减少
                            for (size_t p = 0; p < segments.rows; p++)
                            {
                                for (size_t k = 0; k < segments.cols; k++)
                                {
                                    int index = segments.at<uchar>(p, k);
                                    if (index == j) segments.at<uchar>(p, k) = i;
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    numSeg = newNumSeg;//返回合并之后的区域数量
}

验证通过的子程序:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;
// 分水岭
void waterSegment(InputArray& _src, OutputArray& _dst,int& noOfSegment)
{
	Mat src  ;// =_src.getMat();//dst = _dst.getMat();
	_src.copyTo(src);
	resize(src, src, Size(0.9*src.cols, 0.9*src.rows));//不这么做就会出错
	Mat grayImage;
	cvtColor(src, grayImage, CV_BGR2GRAY);
	threshold(grayImage, grayImage, 0, 255, THRESH_BINARY | THRESH_OTSU);
	Mat kernel = getStructuringElement(MORPH_RECT, Size(9, 9), Point(-1, -1));
	morphologyEx(grayImage, grayImage, MORPH_CLOSE, kernel);
	distanceTransform(grayImage, grayImage, DIST_L2, DIST_MASK_3, 5);
	normalize(grayImage, grayImage, 0, 1, NORM_MINMAX);
	grayImage.convertTo(grayImage, CV_8UC1);
	threshold(grayImage, grayImage, 0, 255, THRESH_BINARY | THRESH_OTSU);
	morphologyEx(grayImage, grayImage, MORPH_CLOSE, kernel);
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	Mat showImage = Mat::zeros(grayImage.size(), CV_32SC1);
	findContours(grayImage, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(-1, -1));
	for (size_t i = 0; i < contours.size(); i++)
	{
		//这里static_cast<int>(i+1)是为了分水岭的标记不同,区域1、2、3。。。。这样才能分割
		drawContours(showImage, contours, static_cast<int>(i), Scalar::all(static_cast<int>(i + 1)), 2);
	}
	Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
	morphologyEx(src, src, MORPH_ERODE, k);//形态变换
	watershed(src, showImage);


	//随机分配颜色
	vector<Vec3b> colors;
	for (size_t i = 0; i < contours.size(); i++) {
		int r = theRNG().uniform(0, 255);
		int g = theRNG().uniform(0, 255);
		int b = theRNG().uniform(0, 255);
		colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
	}

	// 显示
	Mat dst = Mat::zeros(showImage.size(), CV_8UC3);
	int index = 0;
	for (int row = 0; row < showImage.rows; row++) {
		for (int col = 0; col < showImage.cols; col++) {
			index = showImage.at<int>(row, col);
			if (index > 0 && index <= contours.size()) {
				dst.at<Vec3b>(row, col) = colors[index - 1];
			}
			else if (index == -1)
			{
				dst.at<Vec3b>(row, col) = Vec3b(255, 255, 255);
			}
			else {
				dst.at<Vec3b>(row, col) = Vec3b(0, 0, 0);
			}
		}
	}		
	resize(dst, dst, Size(10/9*dst.cols, 10/9*dst.rows));//不这么做就会出错
	dst.copyTo(_dst);
}

鼠标事件——分水岭

#include "stdafx.h"
#include <opencv2/opencv.hpp>
#include <opencv2\core\core.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <iostream>

using namespace cv;
using namespace std;

Mat srcImage, srcImage_, maskImage;
Mat maskWaterShed;  // watershed()函数的参数
Point clickPoint;   // 鼠标点下去的位置

void on_Mouse(int event, int x, int y, int flags, void*)
{
	// 如果鼠标不在窗口中则返回
	if (x < 0 || x >= srcImage.cols || y < 0 || y >= srcImage.rows)
		return;

	// 如果鼠标左键被按下,获取鼠标当前位置;当鼠标左键按下并且移动时,绘制白线;
	if (event == EVENT_LBUTTONDOWN)
	{
		clickPoint = Point(x, y);
	}
	else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON))
	{
		Point point(x, y);
		line(maskImage, clickPoint, point, Scalar::all(255), 5, 8, 0);
		line(srcImage, clickPoint, point, Scalar::all(255), 5, 8, 0);
		clickPoint = point;
		namedWindow("在图像中做标记", 0);
		imshow("在图像中做标记", srcImage);
	}
}

void helpText()
{
	cout << "先用鼠标在图片窗口中标记出大致的区域" << endl;
	cout << "如果想把图片分割为N个区域,就要做N个标记" << endl;
	cout << "键盘按键【1】  - 运行的分水岭分割算法" << endl;
	cout << "键盘按键【2】  - 恢复原始图片" << endl;
	cout << "键盘按键【0】  - 依次分割每个区域(必须先按【1】)" << endl;
	cout << "键盘按键【ESC】    - 退出程序" << endl << endl;
}

int main()
{	
	/* 操作提示 */
	helpText();
	srcImage = imread("C:\\Users\\Administrator\\Desktop\\样品\\大恒样品\\空纹理.bmp");
	resize(srcImage, srcImage, Size(srcImage.cols / 2, srcImage.rows / 2));
	//srcImage = imread("fly.jpg");
	srcImage_ = srcImage.clone();  // 程序中srcImage会被改变,所以这里做备份
	maskImage = Mat(srcImage.size(), CV_8UC1);  // 掩模,在上面做标记,然后传给findContours
	maskImage = Scalar::all(0);

	int areaCount = 1;  // 计数,在按【0】时绘制每个区域
	namedWindow("在图像中做标记", 0);
	imshow("在图像中做标记", srcImage);

	setMouseCallback("在图像中做标记", on_Mouse, 0);

	while (true)
	{
		int c = waitKey(0);

		if ((char)c == 27)  // 按【ESC】键退出
			break;

		if ((char)c == '2')  // 按【2】恢复原图
		{
			maskImage = Scalar::all(0);
			srcImage = srcImage_.clone();
			namedWindow("在图像中做标记", 0);
			imshow("在图像中做标记", srcImage);
		}

		if ((char)c == '1')  // 按【1】处理图片
		{
			vector<vector<Point>> contours;
			vector<Vec4i> hierarchy;

			findContours(maskImage, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);

			if (contours.size() == 0)  // 如果没有做标记,即没有轮廓,则退出该if语句
				break;
			cout << contours.size() << "个轮廓" << endl;

			maskWaterShed = Mat(maskImage.size(), CV_32S);
			maskWaterShed = Scalar::all(0);

			/* 在maskWaterShed上绘制轮廓 */
			for (int index = 0; index < contours.size(); index++)
				drawContours(maskWaterShed, contours, index, Scalar::all(index + 1), -1, 8, hierarchy, INT_MAX);

			/* 如果imshow这个maskWaterShed,我们会发现它是一片黑,原因是在上面我们只给它赋了1,2,3这样的值,通过代码80行的处理我们才能清楚的看出结果 */
			watershed(srcImage_, maskWaterShed);  // 注释一

			vector<Vec3b> colorTab;  // 随机生成几种颜色
			for (int i = 0; i < contours.size(); i++)
			{
				int b = theRNG().uniform(0, 255);
				int g = theRNG().uniform(0, 255);
				int r = theRNG().uniform(0, 255);

				colorTab.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
			}

			Mat resImage = Mat(srcImage.size(), CV_8UC3);  // 声明一个最后要显示的图像
			for (int i = 0; i < maskImage.rows; i++)
			{
				for (int j = 0; j < maskImage.cols; j++)
				{   // 根据经过watershed处理过的maskWaterShed来绘制每个区域的颜色
					int index = maskWaterShed.at<int>(i, j);  // 这里的maskWaterShed是经过watershed处理的
					if (index == -1)  // 区域间的值被置为-1(边界)
						resImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
					else if (index <= 0 || index > contours.size())  // 没有标记清楚的区域被置为0
						resImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
					else  // 其他每个区域的值保持不变:1,2,...,contours.size()
						resImage.at<Vec3b>(i, j) = colorTab[index - 1];  // 然后把这些区域绘制成不同颜色
				}
			}
			namedWindow("resImage", 0);
			imshow("resImage", resImage);
			addWeighted(resImage, 0.3, srcImage_, 0.7, 0, resImage);
			namedWindow("分水岭结果", 0);
			imshow("分水岭结果", resImage);
		}

		if ((char)c == '0')  // 多次点按【0】依次显示每个被分割的区域,需要先按【1】处理图像
		{
			Mat resImage = Mat(srcImage.size(), CV_8UC3);  // 声明一个最后要显示的图像
			for (int i = 0; i < maskImage.rows; i++)
			{
				for (int j = 0; j < maskImage.cols; j++)
				{
					int index = maskWaterShed.at<int>(i, j);
					if (index == areaCount)
						resImage.at<Vec3b>(i, j) = srcImage_.at<Vec3b>(i, j);
					else
						resImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
				}
			}
			namedWindow("分水岭结果", 0);
			imshow("分水岭结果", resImage);
			areaCount++;
			if (areaCount == 4)
				areaCount = 1;
		}
	}

	cv::waitKey();
	system("pause");
	return 0;
}

 

Mat srcImage;
//srcImage = imread("C:\\Users\\Administrator\\Desktop\\样品\\瓷砖\\方格.bmp");
//srcImage = imread("C:\\Users\\Administrator\\Desktop\\样品\\瓷砖\\条纹3.bmp"); 
srcImage = imread("C:\\Users\\Administrator\\Desktop\\样品\\瓷砖\\空纹理.bmp"); 
//srcImage = imread("C:\\Users\\Administrator\\Desktop\\样品\\瓷砖\\花纹.bmp");
//srcImage = imread("C:\\Users\\Administrator\\Desktop\\样品\\大恒样品\\方格.bmp");//C:\Users\Administrator\Desktop\样品\大恒样品
//srcImage = imread("C:\\Users\\Administrator\\Desktop\\样品\\大恒样品\\条纹3.bmp");
//srcImage = imread("C:\\Users\\Administrator\\Desktop\\样品\\大恒样品\\空纹理.bmp");
//srcImage= imread("C:\\Users\\Administrator\\Desktop\\样品\\大恒样品\\花纹.bmp");
cv::resize(srcImage, srcImage, Size(srcImage.cols / 2, srcImage.rows / 2));
namedWindow("resImage", 0);
imshow("resImage", srcImage);
//waitKey();
// 【mask两点】	
//mask的第一点 maskImage
Mat maskImage;	
maskImage = Mat(srcImage.size(), CV_8UC1);  // 掩模,在上面做标记,然后传给findContours
maskImage = Scalar::all(0);	
Point point1(0, 0), point2(10, 10);  	
line(maskImage, point1, point2, Scalar::all(255), 5, 8, 0);
	
//mask的第二点 maskImage
Point point3(srcImage.cols / 2, srcImage.rows / 2), point4(srcImage.cols / 2, srcImage.rows / 2);
line(maskImage, point3, point4, Scalar::all(255), 5, 8, 0);

// 【轮廓】
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(maskImage, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);

// 【分水岭】
// 参数二:maskWaterShed(CV_32S)
Mat maskWaterShed;  // watershed()函数的参数
maskWaterShed = Mat(maskImage.size(), CV_32S);//空白掩码	maskWaterShed
maskWaterShed = Scalar::all(0);

/* 在maskWaterShed上绘制轮廓 */
for (int index = 0; index < contours.size(); index++)
	drawContours(maskWaterShed, contours, index, Scalar::all(index + 1), -1, 8, hierarchy, INT_MAX);

/* 如果imshow这个maskWaterShed,我们会发现它是一片黑,原因是在上面我们只给它赋了1,2,3这样的值,通过代码80行的处理我们才能清楚的看出结果 */
// 参数一:srcImage(CV_8UC3)
watershed(srcImage, maskWaterShed);  //int index = maskWaterShed.at<int>(row, col);操作

// 【随机生成几种颜色】
vector<Vec3b> colorTab;
for (int i = 0; i < contours.size(); i++)
{
	int b = theRNG().uniform(0, 255);
	int g = theRNG().uniform(0, 255);
	int r = theRNG().uniform(0, 255);

	colorTab.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
}
Mat dst = Mat::zeros(maskWaterShed.size(), CV_8UC3);
Mat dst_edge = Mat::zeros(maskWaterShed.size(), CV_8UC3);
int index = maskWaterShed.at<int>(maskWaterShed.rows / 2, maskWaterShed.cols / 2);
int index_temp = 0;
for (int i = 0; i < maskWaterShed.rows; i++)
{
	for (int j = 0; j < maskWaterShed.cols; j++)
	{
		index_temp = maskWaterShed.at<int>(i, j);
		//cout << index_temp << endl;
		if (index_temp == index)//取中心的标签区域
		{
			dst_edge.at<Vec3b>(i, j) = colorTab[index - 1];
			dst.at<Vec3b>(i, j) = srcImage.at<Vec3b>(i, j);
		}
	}
}
namedWindow("分割结果", 0);
imshow("分割结果", dst);

Mat dst_add;
addWeighted(dst_edge, 0.3, srcImage, 0.7, 0, dst_add);
namedWindow("加权结果", 0);
imshow("加权结果", dst_add);

namedWindow("边缘区域", 0);
imshow("边缘区域", dst_edge);
imwrite("a.bmp", dst_edge);

 

 

建议缩小图像尺寸加快分割速度 

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

void Water_Cut(InputArray& src, OutputArray& dst, OutputArray& edge)
{
	Mat srcImage;
	src.copyTo(srcImage);	
	//cv::resize(srcImage, srcImage, Size(srcImage.cols / 2, srcImage.rows / 2));
	namedWindow("resImage", 0);
	imshow("resImage", srcImage);
	//waitKey();
	// 【mask两点】	
	//mask的第一点 maskImage
	Mat maskImage;
	maskImage = Mat(srcImage.size(), CV_8UC1);  // 掩模,在上面做标记,然后传给findContours
	maskImage = Scalar::all(0);
	Point point1(0, 0), point2(10, 10);
	line(maskImage, point1, point2, Scalar::all(255), 5, 8, 0);

	//mask的第二点 maskImage
	Point point3(srcImage.cols / 2, srcImage.rows / 2), point4(srcImage.cols / 2, srcImage.rows / 2);
	line(maskImage, point3, point4, Scalar::all(255), 5, 8, 0);

	// 【轮廓】
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	findContours(maskImage, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);

	// 【分水岭】
	// 参数二:maskWaterShed(CV_32S)
	Mat maskWaterShed;  // watershed()函数的参数
	maskWaterShed = Mat(maskImage.size(), CV_32S);//空白掩码	maskWaterShed
	maskWaterShed = Scalar::all(0);

	/* 在maskWaterShed上绘制轮廓 */
	for (int index = 0; index < contours.size(); index++)
		drawContours(maskWaterShed, contours, index, Scalar::all(index + 1), -1, 8, hierarchy, INT_MAX);

	/* 如果imshow这个maskWaterShed,我们会发现它是一片黑,原因是在上面我们只给它赋了1,2,3这样的值,通过代码80行的处理我们才能清楚的看出结果 */
	// 参数一:srcImage(CV_8UC3)
	watershed(srcImage, maskWaterShed);  //int index = maskWaterShed.at<int>(row, col);操作

	// 【随机生成几种颜色】
	vector<Vec3b> colorTab;
	for (int i = 0; i < contours.size(); i++)
	{
		int b = theRNG().uniform(0, 255);
		int g = theRNG().uniform(0, 255);
		int r = theRNG().uniform(0, 255);

		colorTab.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
	}
	Mat dst_ = Mat::zeros(maskWaterShed.size(), CV_8UC3);
	Mat dst_edge = Mat::zeros(maskWaterShed.size(), CV_8UC3);
	int index = maskWaterShed.at<int>(maskWaterShed.rows / 2, maskWaterShed.cols / 2);
	int index_temp = 0;
	for (int i = 0; i < maskWaterShed.rows; i++)
	{
		for (int j = 0; j < maskWaterShed.cols; j++)
		{
			index_temp = maskWaterShed.at<int>(i, j);
			//cout << index_temp << endl;
			if (index_temp == index)//取中心的标签区域
			{
				dst_edge.at<Vec3b>(i, j) = Vec3b((uchar)255, (uchar)255, (uchar)255); //colorTab[index - 1];
				dst_.at<Vec3b>(i, j) = srcImage.at<Vec3b>(i, j);
			}
		}
	}
	namedWindow("分割结果", 0);
	imshow("分割结果", dst_);

	/*Mat dst_add;
	addWeighted(dst_edge, 0.3, srcImage, 0.7, 0, dst_add);
	namedWindow("加权结果", 0);
	imshow("加权结果", dst_add);*/

	namedWindow("边缘区域", 0);
	imshow("边缘区域", dst_edge);
	imwrite("方格.bmp", dst_edge);
	dst_.copyTo(dst);
	dst_edge.copyTo(edge);
}

参考:

https://blog.csdn.net/Dangkie/article/details/77806211 

// 参考外接框:https://www.cnblogs.com/little-monkey/p/7429579.html
// 参考填充区:https://blog.csdn.net/klamen/article/details/56016865
// 动态分水岭(重要):https://www.2cto.com/kf/201611/565921.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智能之心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值