快速遍历OpenCV Mat图像数据的多种方法和性能分析

- Series

Part 1: compile opencv on ubuntu 16.04
Part 2: compile opencv with CUDA support on windows 10
Part 3: opencv mat for loop
Part 4: speed up opencv image processing with openmp

- Guide

Mat

 - for gray image, use type <uchar>
 - for RGB color image,use type <Vec3b>

gray format storage
在这里插入图片描述
color format storage: BGR
在这里插入图片描述
we can use method isContinuous() to judge whether the memory buffer is continuous or not.

color space reduction

uchar color_space_reduction(uchar pixel)
{
    /*
    0-9 ===>0
    10-19===>10
    20-29===>20
    ...
    240-249===>24
    250-255===>25

    map from 256*256*256===>26*26*26
    */

    int divideWith = 10;
    uchar new_pixel = (pixel / divideWith)*divideWith;
    return new_pixel;
}

color table

void get_color_table()
{
    // cache color value in table[256]
    int divideWith = 10;
    uchar table[256];
    for (int i = 0; i < 256; ++i)
        table[i] = divideWith* (i / divideWith);
}

- C++

ptr []

// C ptr []: faster but not safe
Mat& ScanImageAndReduce_Cptr(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() != sizeof(uchar));
    int channels = I.channels();
    int nRows = I.rows;
    int nCols = I.cols* channels;
    if (I.isContinuous())
    {
        nCols *= nRows;
        nRows = 1;
    }
    int i, j;
    uchar* p;
    for (i = 0; i < nRows; ++i)
    {
        p = I.ptr<uchar>(i);
        for (j = 0; j < nCols; ++j)
        {
            p[j] = table[p[j]];
        }
    }
    return I;
}

ptr ++

// C ptr ++: faster but not safe
Mat& ScanImageAndReduce_Cptr2(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() != sizeof(uchar));
    int channels = I.channels();
    int nRows = I.rows;
    int nCols = I.cols* channels;
    if (I.isContinuous())
    {
        nCols *= nRows;
        nRows = 1;
    }
    uchar* start = I.ptr<uchar>(0); // same as I.ptr<uchar>(0,0)
    uchar* end = start + nRows * nCols;
    for (uchar* p=start; p < end; ++p)
    {
        *p = table[*p];
    }
    return I;
}

at(i,j)

// at<uchar>(i,j): random access, slow
Mat& ScanImageAndReduce_atRandomAccess(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() != sizeof(uchar));
    const int channels = I.channels();
    switch (channels)
    {
    case 1:
    {
        for (int i = 0; i < I.rows; ++i)
            for (int j = 0; j < I.cols; ++j)
                I.at<uchar>(i, j) = table[I.at<uchar>(i, j)];
        break;
    }
    case 3:
    {
        Mat_<Vec3b> _I = I;

        for (int i = 0; i < I.rows; ++i)
            for (int j = 0; j < I.cols; ++j)
            {
                _I(i, j)[0] = table[_I(i, j)[0]];
                _I(i, j)[1] = table[_I(i, j)[1]];
                _I(i, j)[2] = table[_I(i, j)[2]];
            }
        I = _I;
        break;
    }
    }
    return I;
}

Iterator

 // MatIterator_<uchar>: safe but slow
Mat& ScanImageAndReduce_Iterator(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() != sizeof(uchar));
    const int channels = I.channels();
    switch (channels)
    {
    case 1:
    {
        MatIterator_<uchar> it, end;
        for (it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
            *it = table[*it];
        break;
    }
    case 3:
    {
        MatIterator_<Vec3b> it, end;
        for (it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
        {
            (*it)[0] = table[(*it)[0]];
            (*it)[1] = table[(*it)[1]];
            (*it)[2] = table[(*it)[2]];
        }
    }
    }
    return I;
}

opencv LUT

// LUT
Mat& ScanImageAndReduce_LUT(Mat& I, const uchar* const table)
{
    Mat lookUpTable(1, 256, CV_8U);
    uchar* p = lookUpTable.data;
    for (int i = 0; i < 256; ++i)
        p[i] = table[i];

    cv::LUT(I, lookUpTable, I);
    return I;
}

forEach
forEach method of the Mat class that utilizes all the cores on your machine to apply any function at every pixel.

// Parallel execution with function object.
struct ForEachOperator
{
    uchar m_table[256];
    ForEachOperator(const uchar* const table)
    {
        for (size_t i = 0; i < 256; i++)
        {
            m_table[i] = table[i];
        }
    }

    void operator ()(uchar& p, const int * position) const
    {
        // Perform a simple operation
        p = m_table[p];
    }
};

// forEach use multiple processors, very fast
Mat& ScanImageAndReduce_forEach(Mat& I, const uchar* const table)
{
    I.forEach<uchar>(ForEachOperator(table));
    return I;
}

forEach with lambda

// forEach lambda use multiple processors, very fast (lambda slower than ForEachOperator)
Mat& ScanImageAndReduce_forEach_with_lambda(Mat& I, const uchar* const table)
{
    I.forEach<uchar>
    (
        [=](uchar &p, const int * position) -> void
        {
            p = table[p];
        }
    );
    return I;
}

time cost
no foreach

[1 Cptr   ] times=5000, total_cost=988 ms, avg_cost=0.1976 ms
[1 Cptr2  ] times=5000, total_cost=1704 ms, avg_cost=0.3408 ms
[2 atRandom] times=5000, total_cost=9611 ms, avg_cost=1.9222 ms
[3 Iterator] times=5000, total_cost=20195 ms, avg_cost=4.039 ms
[4 LUT    ] times=5000, total_cost=899 ms, avg_cost=0.1798 ms

[1 Cptr   ] times=10000, total_cost=2425 ms, avg_cost=0.2425 ms
[1 Cptr2  ] times=10000, total_cost=3391 ms, avg_cost=0.3391 ms
[2 atRandom] times=10000, total_cost=20024 ms, avg_cost=2.0024 ms
[3 Iterator] times=10000, total_cost=39980 ms, avg_cost=3.998 ms
[4 LUT    ] times=10000, total_cost=103 ms, avg_cost=0.0103 ms

foreach

[5 forEach     ] times=200000, total_cost=199 ms, avg_cost=0.000995 ms
[5 forEach lambda] times=200000, total_cost=521 ms, avg_cost=0.002605 ms

[5 forEach     ] times=20000, total_cost=17 ms, avg_cost=0.00085 ms
[5 forEach lambda] times=20000, total_cost=23 ms, avg_cost=0.00115 ms

results

Loop TypeTime Cost (us)
ptr []242
ptr ++339
at2002
iterator3998
LUT10
forEach0.85
forEach lambda1.15

forEach is 10x times faster than LUT, 240~340x times faster than ptr [] and ptr ++, and 2000~4000x times faster than at and iterator.

code

#include <stdio.h>
#include <iostream>

#pragma warning( disable: 4819 ) 
#pragma warning( disable: 4244 ) 
#pragma warning( disable: 4267 ) 

#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/features2d.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/calib3d.hpp"

#include <boost/date_time/posix_time/posix_time.hpp>  

using namespace std;
using namespace cv;

//#define SAVE_IMAGE

uchar color_space_reduction(uchar pixel)
{
	/*
	0-9 ===>0
	10-19===>10
	20-29===>20
	...
	240-249===>24
	250-255===>25

	map from 256*256*256===>26*26*26
	*/

	int divideWith = 10;
	uchar new_pixel = (pixel / divideWith)*divideWith;
	return new_pixel;
}

void get_color_table()
{
	// cache color value in table[256]
	int divideWith = 10;
	uchar table[256];
	for (int i = 0; i < 256; ++i)
		table[i] = divideWith* (i / divideWith);
}

// C ptr []: faster but not safe
Mat& ScanImageAndReduce_Cptr(Mat& I, const uchar* const table)
{
	// accept only char type matrices
	CV_Assert(I.depth() != sizeof(uchar));
	int channels = I.channels();
	int nRows = I.rows;
	int nCols = I.cols* channels;
	if (I.isContinuous())
	{
		nCols *= nRows;
		nRows = 1;
	}
	int i, j;
	uchar* p;
	for (i = 0; i < nRows; ++i)
	{
		p = I.ptr<uchar>(i);
		for (j = 0; j < nCols; ++j)
		{
			p[j] = table[p[j]];
		}
	}
	return I;
}

// C ptr ++: faster but not safe
Mat& ScanImageAndReduce_Cptr2(Mat& I, const uchar* const table)
{
	// accept only char type matrices
	CV_Assert(I.depth() != sizeof(uchar));
	int channels = I.channels();
	int nRows = I.rows;
	int nCols = I.cols* channels;
	if (I.isContinuous())
	{
		nCols *= nRows;
		nRows = 1;
	}
	uchar* start = I.ptr<uchar>(0); // same as I.ptr<uchar>(0,0)
	uchar* end = start + nRows * nCols;
	for (uchar* p=start; p < end; ++p)
	{
		*p = table[*p];
	}
	return I;
}

// at<uchar>(i,j): random access, slow
Mat& ScanImageAndReduce_atRandomAccess(Mat& I, const uchar* const table)
{
	// accept only char type matrices
	CV_Assert(I.depth() != sizeof(uchar));
	const int channels = I.channels();
	switch (channels)
	{
	case 1:
	{
		for (int i = 0; i < I.rows; ++i)
			for (int j = 0; j < I.cols; ++j)
				I.at<uchar>(i, j) = table[I.at<uchar>(i, j)];
		break;
	}
	case 3:
	{
		Mat_<Vec3b> _I = I;

		for (int i = 0; i < I.rows; ++i)
			for (int j = 0; j < I.cols; ++j)
			{
				_I(i, j)[0] = table[_I(i, j)[0]];
				_I(i, j)[1] = table[_I(i, j)[1]];
				_I(i, j)[2] = table[_I(i, j)[2]];
			}
		I = _I;
		break;
	}
	}
	return I;
}

// MatIterator_<uchar>: safe but slow
Mat& ScanImageAndReduce_Iterator(Mat& I, const uchar* const table)
{
	// accept only char type matrices
	CV_Assert(I.depth() != sizeof(uchar));
	const int channels = I.channels();
	switch (channels)
	{
	case 1:
	{
		MatIterator_<uchar> it, end;
		for (it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
			*it = table[*it];
		break;
	}
	case 3:
	{
		MatIterator_<Vec3b> it, end;
		for (it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
		{
			(*it)[0] = table[(*it)[0]];
			(*it)[1] = table[(*it)[1]];
			(*it)[2] = table[(*it)[2]];
		}
	}
	}
	return I;
}

// LUT
Mat& ScanImageAndReduce_LUT(Mat& I, const uchar* const table)
{
	Mat lookUpTable(1, 256, CV_8U);
	uchar* p = lookUpTable.data;
	for (int i = 0; i < 256; ++i)
		p[i] = table[i];

	cv::LUT(I, lookUpTable, I);
	return I;
}


#pragma region forEach 

// Parallel execution with function object.
struct ForEachOperator
{
	uchar m_table[256];
	ForEachOperator(const uchar* const table)
	{
		for (size_t i = 0; i < 256; i++)
		{
			m_table[i] = table[i];
		}
	}

	void operator ()(uchar& p, const int * position) const
	{
		// Perform a simple operation
		p = m_table[p];
	}
};

// forEach use multiple processors, very fast
Mat& ScanImageAndReduce_forEach(Mat& I, const uchar* const table)
{
	I.forEach<uchar>(ForEachOperator(table));
	return I;
}

// forEach lambda use multiple processors, very fast (lambda slower than ForEachOperator)
Mat& ScanImageAndReduce_forEach_with_lambda(Mat& I, const uchar* const table)
{
	I.forEach<uchar>
	(
		[=](uchar &p, const int * position) -> void
		{
			p = table[p];
		}
	);
	return I;
}

#pragma endregion


void test_cptr(Mat& image, const uchar* const table, int times)
{
	int64_t total_cost = 0;
	for (size_t i = 0; i < times; i++)
	{
		boost::posix_time::ptime pt1 = boost::posix_time::microsec_clock::local_time();

		image = ScanImageAndReduce_Cptr(image, table);

		boost::posix_time::ptime pt2 = boost::posix_time::microsec_clock::local_time();

		int64_t cost = (pt2 - pt1).total_milliseconds();
		total_cost += cost;

#ifdef SAVE_IMAGE
		cv::imwrite("./1_cptr.jpg", image);
#endif // SAVE_IMAGE
	}
	std::cout << "[1 Cptr] times=" << times << ", total_cost=" << total_cost << " ms, avg_cost=" << total_cost / (times*1.0) << " ms \n";
}

void test_cptr2(Mat& image, const uchar* const table, int times)
{
	int64_t total_cost = 0;
	for (size_t i = 0; i < times; i++)
	{
		boost::posix_time::ptime pt1 = boost::posix_time::microsec_clock::local_time();

		image = ScanImageAndReduce_Cptr2(image, table);

		boost::posix_time::ptime pt2 = boost::posix_time::microsec_clock::local_time();

		int64_t cost = (pt2 - pt1).total_milliseconds();
		total_cost += cost;

#ifdef SAVE_IMAGE
		cv::imwrite("./1_cptr2.jpg", image);
#endif // SAVE_IMAGE
	}
	std::cout << "[1 Cptr2] times=" << times << ", total_cost=" << total_cost << " ms, avg_cost=" << total_cost / (times*1.0) << " ms \n";
}

void test_at_random_access(Mat& image, const uchar* const table, int times)
{
	int64_t total_cost = 0;
	for (size_t i = 0; i < times; i++)
	{
		boost::posix_time::ptime pt1 = boost::posix_time::microsec_clock::local_time();

		image = ScanImageAndReduce_atRandomAccess(image, table);

		boost::posix_time::ptime pt2 = boost::posix_time::microsec_clock::local_time();

		int64_t cost = (pt2 - pt1).total_milliseconds();
		total_cost += cost;

#ifdef SAVE_IMAGE
		cv::imwrite("./1_at_random.jpg", image);
#endif // SAVE_IMAGE
	}
	std::cout << "[2 atRandom] times=" << times << ", total_cost=" << total_cost << " ms, avg_cost=" << total_cost / (times*1.0) << " ms \n";
}

void test_iterator(Mat& image, const uchar* const table, int times)
{
	int64_t total_cost = 0;
	for (size_t i = 0; i < times; i++)
	{
		boost::posix_time::ptime pt1 = boost::posix_time::microsec_clock::local_time();

		image = ScanImageAndReduce_Iterator(image, table);

		boost::posix_time::ptime pt2 = boost::posix_time::microsec_clock::local_time();

		int64_t cost = (pt2 - pt1).total_milliseconds();
		total_cost += cost;

#ifdef SAVE_IMAGE
		cv::imwrite("./1_iterator.jpg", image);
#endif // SAVE_IMAGE
	}
	std::cout << "[3 Iterator] times=" << times << ", total_cost=" << total_cost << " ms, avg_cost=" << total_cost / (times*1.0) << " ms \n";
}

void test_lut(Mat& image, const uchar* const table, int times)
{
	Mat lookUpTable(1, 256, CV_8U);
	uchar* p = lookUpTable.data;
	for (int i = 0; i < 256; ++i)
		p[i] = table[i];

	int64_t total_cost = 0;
	for (size_t i = 0; i < times; i++)
	{
		boost::posix_time::ptime pt1 = boost::posix_time::microsec_clock::local_time();

		cv::LUT(image, lookUpTable, image); // LUT

		boost::posix_time::ptime pt2 = boost::posix_time::microsec_clock::local_time();

		int64_t cost = (pt2 - pt1).total_milliseconds();
		total_cost += cost;

#ifdef SAVE_IMAGE
		cv::imwrite("./1_lut.jpg", image);
#endif // SAVE_IMAGE
	}
	std::cout << "[4 LUT] times=" << times << ", total_cost=" << total_cost << " ms, avg_cost=" << total_cost / (times*1.0) << " ms \n";
}

void test_for_each(Mat& image, const uchar* const table, int times)
{
	int64_t total_cost = 0;
	for (size_t i = 0; i < times; i++)
	{
		boost::posix_time::ptime pt1 = boost::posix_time::microsec_clock::local_time();

		image = ScanImageAndReduce_forEach(image, table);

		boost::posix_time::ptime pt2 = boost::posix_time::microsec_clock::local_time();

		int64_t cost = (pt2 - pt1).total_milliseconds();
		total_cost += cost;

#ifdef SAVE_IMAGE
		cv::imwrite("./1_foreach.jpg", image);
#endif // SAVE_IMAGE

	}
	std::cout << "[5 forEach] times=" << times << ", total_cost=" << total_cost << " ms, avg_cost=" << total_cost / (times*1.0) << " ms \n";
}

void test_for_each_with_lambda(Mat& image, const uchar* const table, int times)
{
	int64_t total_cost = 0;
	for (size_t i = 0; i < times; i++)
	{
		boost::posix_time::ptime pt1 = boost::posix_time::microsec_clock::local_time();

		image = ScanImageAndReduce_forEach_with_lambda(image, table);

		boost::posix_time::ptime pt2 = boost::posix_time::microsec_clock::local_time();

		int64_t cost = (pt2 - pt1).total_milliseconds();
		total_cost += cost;

#ifdef SAVE_IMAGE
		cv::imwrite("./1_foreach_with_lambda.jpg", image);
#endif // SAVE_IMAGE

	}
	std::cout << "[5 forEach lambda] times=" << times << ", total_cost=" << total_cost << " ms, avg_cost=" << total_cost / (times*1.0) << " ms \n";
}


int main(int argc, char const *argv[])
{
	// table
	int divideWith = 52;
	uchar table[256];
	for (int i = 0; i < 256; ++i)
		table[i] = divideWith* (i / divideWith);

	// image
	Mat image = cv::imread("./1.jpg", 0);
	//cv::imshow("image", image);
	//cv::waitKey(0);
	std::cout << image.size() << std::endl;
	if (image.isContinuous())
	{
		std::cout << " image is continuous. \n";
	}

	bool no_foreach = true;
	if (no_foreach)
	{
		int times = 5000;
		test_cptr(image, table, times); // ptr[i]
		test_cptr2(image, table, times); // ptr++
		test_at_random_access(image, table, times);
		test_iterator(image, table, times);
		test_lut(image, table, times);
	}
	else {
		int times = 20000;
		test_for_each(image, table, times);
		test_for_each_with_lambda(image, table, times);
	}
	
	std::cout << "Press enter to exit \n";
	char c;
	cin >> c;
	return 0;
}


/*
[2048 x 1024]
image is continuous.

[1 Cptr] times=5000, total_cost=988 ms, avg_cost=0.1976 ms
[1 Cptr2] times=5000, total_cost=1704 ms, avg_cost=0.3408 ms
[2 atRandom] times=5000, total_cost=9611 ms, avg_cost=1.9222 ms
[3 Iterator] times=5000, total_cost=20195 ms, avg_cost=4.039 ms
[4 LUT] times=5000, total_cost=899 ms, avg_cost=0.1798 ms

[1 Cptr] times=10000, total_cost=2425 ms, avg_cost=0.2425 ms
[1 Cptr2] times=10000, total_cost=3391 ms, avg_cost=0.3391 ms
[2 atRandom] times=10000, total_cost=20024 ms, avg_cost=2.0024 ms
[3 Iterator] times=10000, total_cost=39980 ms, avg_cost=3.998 ms
[4 LUT] times=10000, total_cost=103 ms, avg_cost=0.0103 ms


[5 forEach] times=200000, total_cost=199 ms, avg_cost=0.000995 ms
[5 forEach lambda] times=200000, total_cost=521 ms, avg_cost=0.002605 ms

[5 forEach] times=20000, total_cost=17 ms, avg_cost=0.00085 ms
[5 forEach lambda] times=20000, total_cost=23 ms, avg_cost=0.00115 ms
*/

- Python

pure python

# import the necessary packages
import matplotlib.pyplot as plt
import cv2
print(cv2.__version__)

%matplotlib inline
3.4.2
# load the original image, convert it to grayscale, and display
# it inline
image = cv2.imread("cat.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
print(image.shape)
#plt.imshow(image, cmap="gray")
(360, 480)
%load_ext cython
The cython extension is already loaded. To reload it, use:
  %reload_ext cython
%%cython -a

def threshold_python(T, image):
    # grab the image dimensions
    h = image.shape[0]
    w = image.shape[1]

    # loop over the image, pixel by pixel
    for y in range(0, h):
        for x in range(0, w):
            # threshold the pixel
            image[y, x] = 255 if image[y, x] >= T else 0

    # return the thresholded image
    return image
%timeit threshold_python(5, image)
263 ms ± 20.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

cython

%%cython -a

import cython

@cython.boundscheck(False)
cpdef unsigned char[:, :] threshold_cython(int T, unsigned char [:, :] image):
    # set the variable extension types
    cdef int x, y, w, h

    # grab the image dimensions
    h = image.shape[0]
    w = image.shape[1]

    # loop over the image
    for y in range(0, h):
        for x in range(0, w):
            # threshold the pixel
            image[y, x] = 255 if image[y, x] >= T else 0

    # return the thresholded image
    return image

numba

%timeit threshold_cython(5, image)
150 µs ± 7.14 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
from numba import njit

@njit
def threshold_njit(T, image):
    # grab the image dimensions
    h = image.shape[0]
    w = image.shape[1]

    # loop over the image, pixel by pixel
    for y in range(0, h):
        for x in range(0, w):
            # threshold the pixel
            image[y, x] = 255 if image[y, x] >= T else 0

    # return the thresholded image
    return image
%timeit threshold_njit(5, image)
43.5 µs ± 142 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

numpy

def threshold_numpy(T, image):
    image[image > T] = 255
    return image
%timeit threshold_numpy(5, image)
111 µs ± 334 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

conclusions

image = cv2.imread("cat.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
print(image.shape)

%timeit threshold_python(5, image)
%timeit threshold_cython(5, image)
%timeit threshold_njit(5, image)
%timeit threshold_numpy(5, image)
(360, 480)
251 ms ± 6.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
143 µs ± 1.19 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
43.8 µs ± 284 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
113 µs ± 957 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
image = cv2.imread("big.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
print(image.shape)

%timeit threshold_python(5, image)
%timeit threshold_cython(5, image)
%timeit threshold_njit(5, image)
%timeit threshold_numpy(5, image)
(2880, 5120)
21.8 s ± 460 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
12.3 ms ± 231 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.91 ms ± 66.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
10.3 ms ± 179 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

60,480

  • python: 251 ms
  • cython: 143 us
  • numba: 43 us
  • numpy: 113 us

2880, 5120

  • python: 21 s
  • cython: 12 ms
  • numba: 4 ms
  • numpy: 10 ms

- Reference

parallel-pixel-access-in-opencv-using-foreach
fast-optimized-for-pixel-loops-with-opencv-and-python
python performance tips

History

  • 20180823: created.

Author: kezunlin
Link: https://kezunlin.me/post/61d55ab4/
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source kezunlin !

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值