2_opencv2计算机视觉学习_操作像素
2015-06-07 23:04
369 查看
本次学习主要醋和操作图像的基本元素,如何遍历一张图像且处理其像素,并且在编程过程中,要考虑程序执行效率的问题!
像素是由8位无符号数来表示,其中0表示黑色,255代表白色。对于彩色图来说,每个像素需要三个这样的8位无符号数来表示三个颜色通道(红绿蓝),所以矩阵的元素是一个三元数。保存不同像素类型有整形(CV_8U)浮点型(CV_32F)
1、存取像素值
为了存取矩阵元素,需要访问矩阵的行和列。如果图像是单通道,返回值是单个值;如果图像是多通道,返回值是一组向量(Vector)。此例在图像中加入校验噪点,椒盐噪点就是随机的把部分像素设为白色或者黑色。在传输过程中,如果部分像素丢失,就会出现噪点。我们随机在图片中随机挑出若干像素,将其设置为白色。首先我们创建一个salt函数,它的形参为一张图image和白噪点的个数n。我们用随机函数rand()结合图像的宽高随机选取一个点设置为噪点,用at访问该点,设置为白色。我们要设置n个噪点,那可以使用循环循环n次。另外,由于图像不知道是灰度图还是彩色图,我们要对其判断,程序如下:
[cpp] view
plaincopy
<span style="font-size:10px;"><span style="font-size:10px;">#include <iostream>
#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;
int main()
{
void salt(Mat &image,int);//函数声明
Mat image=imread("bridge.jpg");
if(!image.data){
cout<<"imread error";
return 0;
}
namedWindow("bridge");
//调用函数
salt(image,3000);
imshow("bridge",image);
waitKey(0);
return 1;
}
void salt(Mat &image,int n){//n为白噪点个数
for(int k=0;k<n;k++){
int j=rand()%image.rows;//行
int i=rand()%image.cols;//列
if(image.channels()==1){
image.at<uchar>(j,i)=255;//at可以存取图像元素
}else if(image.channels()==3){
//设置三通道噪点颜色
image.at<Vec3b>(j,i)[0]=255;
image.at<Vec3b>(j,i)[1]=255;
image.at<Vec3b>(j,i)[2]=255;
}
}
}
</span></span>
注:
1、intj=rand()%image.rows;inti=rand()%image.cols;
由于随机数生成的数范围不确定,我们使用上述两条语句(取余)可以使得j,i不会超出image.rows;image.cols范围内
2、单通道元素访问image.at<uchar>(j,i)=255
多通道元素访问image.at<Vec3b>(j,i)[channel]=value;
3、cv::Mat_的使用重载操作符()
cv::Mat_<uchar>im2=image;//im2指向image
im2(50,100)=0;//存取50行100列元素
2、指针遍历图像
在图像中,我们经常要遍历图像进行操作,考虑像素有可能非常多,所以高效的遍历图像是很必要的。首先使用的是指针算术。例子:减少图像中的颜色数目
算法:对图像中像素每一个通道,将其值除以N(整数出发,舍去余数),在乘以N,得到不大于原始图像的N的最大倍数。对每个8位通道的值进行上述操作,可得到共计256/N*256/N*256/N个颜色值。程序如下:
方法一:指针运算
[cpp] viewplaincopy
void colorReduce1(Mat&image, int div=64){
int nl=image.rows;//行数
int nc=image.cols*image.channels();//列数:彩色图channel为3
for(int j=0;j<nl;j++){
//ptr访问j行地址
uchar *data=image.ptr<uchar>(j);
for(int i=0;i<nc;i++){
data[i]=data[i]/div*div+div/2;//算法
//*data++=*data/div*div+div/2;
//data[i]=data[i]-data[i]%div+div/2 计算速度变慢存取每个像素两次
}
}
}
方法二:位运算
[cpp] viewplaincopy
void colorReduce2(Mat&image, int div=64){
int nl=image.rows;
int nc=image.cols*image.channels();
//位运算
intn=static_cast<int>(log(static_cast<double>(div))/log(2.0));
for(int j=0;j<nl;j++){
uchar *data=image.ptr<uchar>(j);
for(int i=0;i<nc;i++){
//用来对像素取整的掩膜
uchar mask=0xFF<<n;
data[i]=(data[i]&mask)+div/2;
}
}
}
方法三:高效遍历连续图像
图像在行尾不进行填补时,图像可认为是一个长为W*H的一位数组,我们通过cv::Mat成员函数isContinue来判断,如果图像连续,整个处理使用一次循环完成。程序如下[cpp] view
plaincopy
void colorReduce3(Mat&image, int div=64){
int nl=image.rows;
int nc=image.cols*image.channels();
//判断图像是否连续
if(image.isContinuous()){
nc=nc*nl;
nl=1;//一维数组
}
for(int j=0;j<nl;j++){
uchar *data=image.ptr<uchar>(j);
for(int i=0;i<nc;i++){
//处理每个像素
data[i]=data[i]/div*div+div/2;
}
}
}
方法四:底层指针运算 (不建议容易出错)
[cpp] viewplaincopy
void colorReduce4(Mat&image, int div=64){
int nl=image.rows;
int nc=image.cols*image.channels();
//判断图像是否连续
if(image.isContinuous()){
nc=nc*nl;
nl=1;//一维数组
}
for(int j=0;j<nl;j++){
uchar *data=image.data;
for(int i=0;i<nc;i++){
data=image.data+i*image.step+i*image.elemSize();
//data+=image.step;
}
}
}
方法五:使用迭代器遍历图像
[cpp] viewplaincopy
void colorReduce5(Mat&image,int div=64){
//迭代器的初始化 常量迭代器cv::Mat_<cv::Vec3b>::const_iterator it
Mat_<Vec3b>::iteratorit=image.begin<Vec3b>();
Mat_<Vec3b>::iteratoritend=image.end<Vec3b>();
for(;it!=itend;it++){
//三通道图像
(*it)[0]=(*it)[0]/div*div+div/2;
(*it)[1]=(*it)[1]/div*div+div/2;
(*it)[2]=(*it)[2]/div*div+div/2;
}
}
方法六:at访问
[cpp] viewplaincopy
void colorReduce6(cv::Mat&image, int div=64) {
int nl= image.rows;// number of lines
int nc= image.cols;// number of columns
for (int j=0; j<nl; j++) {
for (int i=0; i<nc; i++) {
// process each pixel---------------------
image.at<cv::Vec3b>(j,i)[0]=image.at<cv::Vec3b>(j,i)[0]/div*div+ div/2;
image.at<cv::Vec3b>(j,i)[1]=image.at<cv::Vec3b>(j,i)[1]/div*div+ div/2;
image.at<cv::Vec3b>(j,i)[2]=image.at<cv::Vec3b>(j,i)[2]/div*div + div/2;
}
}
}
方法七
[cpp] viewplaincopy
void colorReduce7(constcv::Mat &image, cv::Mat &result, int div=64) {
int nl= image.rows;// number of lines
int nc= image.cols ;// number of columns
// allocate outputimage if necessary
result.create(image.rows,image.cols,image.type());
// created imageshave no padded pixels
nc= nc*nl;
nl= 1; // it is now a 1D array
int n=static_cast<int>(log(static_cast<double>(div))/log(2.0));
// mask used to roundthe pixel value
uchar mask=0xFF<<n; // e.g. for div=16, mask= 0xF0
for (int j=0; j<nl; j++) {
uchar* data= result.ptr<uchar>(j);
constuchar* idata= image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
// process each pixel--------------------
*data++= (*idata++)&mask + div/2;
*data++= (*idata++)&mask +div/2;
*data++= (*idata++)&mask +div/2;
}
}
}
结果如图,右图颜色明显减少
注:
迭代器是一种特殊的类,他专门用来遍历集合中的各个元素,同时隐藏了在给定集合上元素迭代的具体实现方式。
一个迭代器的声明如下:
cv::MatIterator_<cv::Vec3b> it;
另外一种使用定义在Mat_内部的迭代器类型:
cv::Mat_<cv::Vec3b>::Iterator it;
这样就可以用常规的begin和end这两个迭代器方法遍历所以图像。如果你想从图像的第二行开始,可以用image.begin<cv::Vec3b>()+image.rows来初始化迭代器。
常量迭代器的声明:
cv::MatConstIterator_<cv::Vec3b>it;
cv::Mat_<cv::Vec3b>::const_iteratorit;
3、高效遍历循环
cv::getTickCount()测量一段代码的运行时间,这个函数返回从上次开始算起的时钟周期!我们测量的是代码运行的毫秒数,还需要另一个函数cv::getTickFrequency(),此函数返回每秒钟内的时钟周期数。统计代码运行时间如下:
[cpp] view
plaincopy
double duration;
duration=static_cast<double>(getTickCount());
colorReduce1(imageclone);
duration=(static_cast<double>(getTickCount())duration/getTickFrequency();
前三小结总的程序如下
[cpp] view
plaincopy
#include<opencv2/core/core.hpp>
#include<iostream>
#include<opencv2/highgui/highgui.hpp>
usingnamespacecv;
usingnamespacestd;
intmain()
{
//函数声明
voidcolorReduce1(Mat&image,intdiv=64);
voidcolorReduce2(Mat&image,intdiv=64);
voidcolorReduce3(Mat&image,intdiv=64);
voidcolorReduce4(Mat&image,intdiv=64);
voidcolorReduce5(Mat&image,intdiv=64);
voidcolorReduce6(Mat&image,intdiv=64);
voidcolorReduce7(Mat&image,intdiv=64);
Matimage=imread("bridge.jpg");
//判断是否读入图片成功
if(!image.data){
cout<<"dataerror!";
}
Matimageclone=image.clone();
//定义一个数组存放记录不同程序运行时间
doubleduration[8]={0};
//6种方式运行时间程序
duration[1]=static_cast<double>(getTickCount());
colorReduce1(imageclone);
duration[1]=(static_cast<double>(getTickCount())-duration[1])/getTickFrequency();
duration[2]=static_cast<double>(getTickCount());
colorReduce2(imageclone);
duration[2]=(static_cast<double>(getTickCount())-duration[2])/getTickFrequency();
duration[3]=static_cast<double>(getTickCount());
colorReduce3(imageclone);
duration[3]=(static_cast<double>(getTickCount())-duration[3])/getTickFrequency();
duration[4]=static_cast<double>(getTickCount());
colorReduce4(imageclone);
duration[4]=(static_cast<double>(getTickCount())-duration[4])/getTickFrequency();
duration[5]=static_cast<double>(getTickCount());
colorReduce5(imageclone);
duration[5]=(static_cast<double>(getTickCount())-duration[5])/getTickFrequency();
duration[6]=static_cast<double>(getTickCount());
colorReduce6(imageclone);
duration[6]=(static_cast<double>(getTickCount())-duration[6])/getTickFrequency();
duration[7]=static_cast<double>(getTickCount());
colorReduce6(imageclone);
duration[7]=(static_cast<double>(getTickCount())-duration[7])/getTickFrequency();
//循环输出数组
for(inti=1;i<=7;i++){
cout<<"colorReduce"<<i<<"timeis:"<<duration[i]<<"(s)"<<endl;
}
namedWindow("Image");
imshow("Image",image);
namedWindow("result");
imshow("result",imageclone);
waitKey(0);
return0;
}
//******************************************
voidcolorReduce1(Mat&image,intdiv=64){
intnl=image.rows;//行数
intnc=image.cols*image.channels();//列数:彩色图channel为3
for(intj=0;j<nl;j++){
//ptr访问j行地址
uchar*data=image.ptr<uchar>(j);
for(inti=0;i<nc;i++){
data[i]=data[i]/div*div+div/2;//算法
//*data++=*data/div*div+div/2;
//data[i]=data[i]-data[i]%div+div/2计算速度变慢存取每个像素两次
}
}
}
//*****************************************
voidcolorReduce2(Mat&image,intdiv=64){
intnl=image.rows;
intnc=image.cols*image.channels();
//位运算
intn=static_cast<int>(log(static_cast<double>(div))/log(2.0));
for(intj=0;j<nl;j++){
uchar*data=image.ptr<uchar>(j);
for(inti=0;i<nc;i++){
//用来对像素取整的掩膜
ucharmask=0xFF<<n;
data[i]=(data[i]&mask)+div/2;
}
}
}
//***********************************
voidcolorReduce3(Mat&image,intdiv=64){
intnl=image.rows;
intnc=image.cols*image.channels();
//判断图像是否连续
if(image.isContinuous()){
nc=nc*nl;
nl=1;//一维数组
}
for(intj=0;j<nl;j++){
uchar*data=image.ptr<uchar>(j);
for(inti=0;i<nc;i++){
//处理每个像素
data[i]=data[i]/div*div+div/2;
}
}
}
//********************************************
voidcolorReduce4(Mat&image,intdiv=64){
intnl=image.rows;
intnc=image.cols*image.channels();
//判断图像是否连续
if(image.isContinuous()){
nc=nc*nl;
nl=1;//一维数组
}
for(intj=0;j<nl;j++){
uchar*data=image.data;
for(inti=0;i<nc;i++){
data=image.data+i*image.step+i*image.elemSize();
//data+=image.step;
}
}
}
//********************************************
voidcolorReduce5(Mat&image,intdiv=64){
//迭代器的初始化 常量迭代器cv::Mat_<cv::Vec3b>::const_iteratorit
Mat_<Vec3b>::iteratorit=image.begin<Vec3b>();
Mat_<Vec3b>::iteratoritend=image.end<Vec3b>();
for(;it!=itend;it++){
//三通道图像
(*it)[0]=(*it)[0]/div*div+div/2;
(*it)[1]=(*it)[1]/div*div+div/2;
(*it)[2]=(*it)[2]/div*div+div/2;
}
}
//********************************************
voidcolorReduce6(cv::Mat&image,intdiv=64){
intnl=image.rows;//numberoflines
intnc=image.cols;//numberofcolumns
for(intj=0;j<nl;j++){
for(inti=0;i<nc;i++){
//processeachpixel---------------------
image.at<cv::Vec3b>(j,i)[0]=image.at<cv::Vec3b>(j,i)[0]/div*div+div/2;
image.at<cv::Vec3b>(j,i)[1]=image.at<cv::Vec3b>(j,i)[1]/div*div+div/2;
image.at<cv::Vec3b>(j,i)[2]=image.at<cv::Vec3b>(j,i)[2]/div*div+div/2;
}
}
}
//********************************************
voidcolorReduce7(constcv::Mat&image,cv::Mat&result,intdiv=64){
intnl=image.rows;//numberoflines
intnc=image.cols;//numberofcolumns
//allocateoutputimageifnecessary
if(image.isContinuous()){
//createdimageshavenopaddedpixels
nc=nc*nl;
nl=1; //itisnowa1Darray
}
intn=static_cast<int>(log(static_cast<double>(div))/log(2.0));
//maskusedtoroundthepixelvalue
ucharmask=0xFF<<n;//e.g.fordiv=16,mask=0xF0
for(intj=0;j<nl;j++){
uchar*data=result.ptr<uchar>(j);
constuchar*idata=image.ptr<uchar>(j);
for(inti=0;i<nc;i++){
//processeachpixel--------------------
*data++=(*idata++)&mask+div/2;
*data++=(*idata++)&mask+div/2;
*data++=(*idata++)&mask+div/2;
}
}
}
4、遍历图像和邻域操作
图像处理中,当邻域包含图像的前几行和下几行时,需要同时扫描图像的若干行!例子:对图像进行锐化
它基于拉普拉斯算子,将一幅图像减去他经过拉普拉斯滤波后的图像,这幅图的边缘部分将得到放大,即细节更加锐利。图像遍历使用三个指针:一个指向当前行,一个指向上一行,一个下一行。由于每个像素值的计算都需要它的上下左右四个邻居像素,所以不可能对图像的第一行、最后一行、第一列、最后一列进行计算。锐化算子的计算方式如下:
sharpened_pixel=5*current-left-right-up-dowm;
程序如下:
[cpp] view
plaincopy
<span style="font-weight: normal;">#include<iostream>
#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
voidsharpen(constcv::Mat&image,cv::Mat&result){
result.create(image.size(),image.type());//分配大小
for(intj=1;j<image.rows-1;j++){//除了第一行和最后一行进行遍历
constuchar*previous=image.ptr<constuchar>(j-1);//上一行
constuchar*current=image.ptr<constuchar>(j); //当前行
constuchar*next=image.ptr<constuchar>(j+1); //下一行
uchar*output=result.ptr<uchar>(j);
for(inti=1;i<image.cols-1;i++){
*output++=cv::saturate_cast<uchar>(5*current[i]-current[i-1]-current[i+1]-previous[i]-next[i]);
// output[i]=cv::saturate_cast<uchar>(5*current[i]-current[i-1]-current[i+1]-previous[i]-next[i]);
}
}
//未处理像素设置为0
result.row(0).setTo(cv::Scalar(0));
result.row(result.rows-1).setTo(cv::Scalar(0));
result.col(0).setTo(cv::Scalar(0));
result.col(result.cols-1).setTo(cv::Scalar(0));
}
voidsharpen2(constcv::Mat&image,cv::Mat&result){
result.create(image.size(),image.type());//allocateifnecessary
intstep=image.step1();
constuchar*previous=image.data;
constuchar*current= image.data+step;
constuchar*next=image.data+2*step;
uchar*output=result.data+step;
for(intj=1;j<image.rows-1;j++){//foreachrow(exceptfirstandlast)
for(inti=1;i<image.cols-1;i++){//foreachcolumn(exceptfirstandlast)
output[i]=cv::saturate_cast<uchar>(5*current[i]-current[i-1]-current[i+1]-previous[i]-next[i]);
}
previous+=step;
current+=step;
next+=step;
output+=step;
}
result.row(0).setTo(cv::Scalar(0));
result.row(result.rows-1).setTo(cv::Scalar(0));
result.col(0).setTo(cv::Scalar(0));
result.col(result.cols-1).setTo(cv::Scalar(0));
}
voidsharpen3(constcv::Mat&image,cv::Mat&result){
cv::Mat_<uchar>::const_iteratorit=image.begin<uchar>()+image.step;
cv::Mat_<uchar>::const_iteratoritend=image.end<uchar>()-image.step;
cv::Mat_<uchar>::const_iteratoritup=image.begin<uchar>();
cv::Mat_<uchar>::const_iteratoritdown=image.begin<uchar>()+2*image.step;
result.create(image.size(),image.type());
cv::Mat_<uchar>::iteratoritout=result.begin<uchar>()+result.step;
for(;it!=itend;++it,++itup,++itdown){
*itout=cv::saturate_cast<uchar>(*it*5-*(it-1)-*(it+1)-*itup-*itdown);
}
}
intmain()
{
cv::Matimage=cv::imread("bridge.jpg",0);
if(!image.data)
return0;
cv::Matresult;
result.create(image.size(),image.type());
doubletime=static_cast<double>(cv::getTickCount());
sharpen(image,result);
time=(static_cast<double>(cv::getTickCount())-time)/cv::getTickFrequency();
std::cout<<"time="<<time<<std::endl;
cv::namedWindow("Image");
cv::imshow("Image",result);
image=cv::imread("bridge.jpg",0);
time=static_cast<double>(cv::getTickCount());
sharpen3(image,result);
time=(static_cast<double>(cv::getTickCount())-time)/cv::getTickFrequency();
std::cout<<"time3="<<time<<std::endl;
cv::namedWindow("Image3");
cv::imshow("Image3",result);
cv::waitKey();
return0;
}</span>
5、简单图像算术
图像就是一些矩阵,我们对矩阵进行运算从而就改变了图像的性质。在此次学习中,我们对两幅图进行相加,可以分为两种情况:①相加的的两张图为类型和大小相同的
②相加的的两张图不一样
类型和大小相同的两张图如下:
调用函数
cv::addWeighted()用法如下:(具体含义见Reference Manual)
void addWeighted(InputArray src1, double alpha,InputArray src2, double beta, double gamma, OutputArray dst, int dtype=-1)
可以使用dst = src1*alpha + src2*beta + gamma;
例如c[i]=a[i]+b[i]等价于cv::add(imageA,imageB,resultC)
c[i]=a[i]+k等价于cv::add(imageA,cv::Scalar(k),resultC)
c[i]=k1*a[i]+k2*b[i]+k3
等价于cv::addWeighted(imageA,k1,imageB,k2,k3,imageC)
我们也可以重载操作符:
Result=0.7*imageA+0.9*imageB+0.8;
此次程序为:
[cpp] view
plaincopy
#include<iostream>
#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
usingnamespacecv;
usingnamespacestd;
intmain(){
//读取相加的两张图片
cv::Matimage1=imread("bridge.jpg");;
cv::Matimage2=imread("sun.jpg");;
//判断是否读取成功
if(!image1.data)
return0;
if(!image2.data)
return0;
//显示两张图
cv::namedWindow("Image1");
cv::imshow("Image1",image1);
cv::namedWindow("Image2");
cv::imshow("Image2",image2);
//方法一
cv::Matresult;
//addweighted两张图需要大小类型相同
cv::addWeighted(image1,0.7,image2,0.9,0.,result);
//显示相加的结果
cv::namedWindow("result");
cv::imshow("result",result);
//方法二
//重载运算符“+”,“*”
result=0.7*image1+0.9*image2;
//显示
cv::namedWindow("resultwithoperators");
cv::imshow("resultwithoperators",result);
image2=cv::imread("rain.jpg",0);
cv::waitKey();
return0;
}
叠加结果为:
类型不同的两张图
Logo.bmp bridge.jpg
我们要把大小和类型不同两张图加到一起,由于cv::add要求输出图像要相同的尺寸,所以不能用。我们首先要定义感兴趣区域(ROI),只要感兴趣区域和logo大小相同,cv::add就能工作了!
//定义感兴趣区域
cv::MatimageROI;
imageROI=image(cv::Rect(300,300,logo.cols,logo.rows));
//插入logo
cv::addWeighted(imageROI,1.0,logo,0.5,0.,imageROI);
直接相加得到图像
得到的图像不是很令人满意,可以将插入处的像素设置为logo图像的像素效果会更好,可以通过一个图像掩码完成:
imageROI= image(cv::Rect(300,300,logo.cols,logo.rows));
// 加载掩膜(必须为灰度图)
cv::Matmask= cv::imread("logo.bmp",0);
// 拷贝拷贝ROI
logo.copyTo(imageROI,mask);
得到
程序为
[cpp] view
plaincopy
<span style="font-size:10px;">#include<iostream>
#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
usingnamespacecv;
usingnamespacestd;
intmain(){
//感兴趣区域
cv::Matimage=cv::imread("bridge.jpg");
cv::Matlogo=cv::imread("logo.bmp");
//定义感兴趣区域
cv::MatimageROI;
imageROI=image(cv::Rect(300,300,logo.cols,logo.rows));
//插入logo
cv::addWeighted(imageROI,1.0,logo,0.5,0.,imageROI);
cv::namedWindow("withlogo");
cv::imshow("withlogo",image);
//用掩膜
logo=cv::imread("logo.bmp");
imageROI=image(cv::Rect(300,300,logo.cols,logo.rows));
//加载掩膜(必须为灰度图)
cv::Matmask=cv::imread("logo.bmp",0);
//拷贝拷贝ROI
logo.copyTo(imageROI,mask);
cv::namedWindow("withlogo2");
cv::imshow("withlogo2",image);
logo=cv::imread("logo.bmp",0);
image=cv::imread("bridge.jpg");
cv::waitKey();
return0;
}
</span>
网址:http://blog.csdn.net/yale524/article/details/44837465
相关文章推荐
- 实现HTTP协议Get、Post和文件上传功能——使用libcurl接口实现
- HttpContext.Session==null 解决办法
- 数据结构:查找
- poj 3281 Dining 【拆点网络流】【最大流经典问题 关键建图】
- 网络抖动
- 网络加密
- UNP---套接字简介
- 从多层感知器到卷积网络(二)
- java网络编程笔记
- UNP学习记录---三次握手和四次挥手
- Android 优化电池使用时间——确定和检测网络状态
- Socket 通信原理(Android客户端和服务器以TCP&&UDP方式互通)
- iOS开发网络—13使用ASI框架进行文件下载
- iOS开发网络—11发送json数据给服务器以及多值参数
- iOS开发网络—12数据缓存
- iOS开发网络—10监测网络状态
- iOS开发网络—09简单介绍ASI框架的使用
- iOS开发网络—08文件的上传
- iOS开发网络—07大文件的多线程断点下载
- iOS开发网络—06NSURLConnection基本使用