转自:http://blog.csdn.net/xizero00/article/details/6631209
数字图像基本处理算法
xizero00
常熟理工学院(CIT) 计算机科学与工程学院 下一代互联网实验室(NGIL Lab)
Email:xizero00@163.com
由于SIFT算法需要用到很多算法,所以这段时间研究了一下一些最基本的图像处理算法,
好了,废话不多说,先看结果,接下来上图说话:
1.二值化:
图1 二值化(阈值:140)处理效果
所谓二值化简单一点讲,就是将图像划分成黑和白,通过设定一个标准如果大于这个标准就设为白,如果小于这个标准,就设为黑,而这个标准,就叫做阈值。
具体定义如下所示:
下面给出实现的代码:
//二值化 //函数的参数iTR为阈值 void CBMPSampleDlg::ThresholdProcess(int iTR) { //读取BMP文件 m_Dib.AttachMapFile("1.bmp", TRUE); m_Dib.CopyToMapFile("二值化.bmp");
//将像素指针定位到图像数据的开始 RGBTRIPLE *rgbtri = (RGBTRIPLE *)m_Dib.m_lpImage; //获得图像的大小 int iSize = m_Dib.GetSizeImage(); //BMP文件头指针 BITMAPINFOHEADER * pBmiHeader = (BITMAPINFOHEADER *)m_Dib.m_lpBMIH; //遍历每一个像素,并判断每一个像素的分量(RGB),将其与阈值比较,然后进行赋值 for(int i = 0; i < iSize/( pBmiHeader->biBitCount / 8); i++) { if ( (rgbtri[i].rgbtRed < iTR )| (rgbtri[i].rgbtGreen < iTR) | (rgbtri[i].rgbtBlue < iTR) ) { rgbtri[i].rgbtRed = (BYTE) 0; rgbtri[i].rgbtGreen = (BYTE) 0; rgbtri[i].rgbtBlue = (BYTE) 0; } else { rgbtri[i].rgbtRed = (BYTE) 255; rgbtri[i].rgbtGreen = (BYTE) 255; rgbtri[i].rgbtBlue = (BYTE) 255;
} } //显示图像 DrawPic(); }
|
代码中,首先会读取原始图像文件,文件的格式为BMP的,关于BMP图像的存储结构,在接下来的文章中会讲到。
在读取图像之后,会将指针定位到图像像素数据的开始位置,然后获得图像的大小,然后通过BMP文件头获得图像的一个像素所占据的二进制的位数,这样就知道一个像素由几个字节组成的了,需要注意的是,一个像素不一定是由三个字节组成的,比如是灰度图像其只需要一个字节来存储一个像素究竟是灰到什么程度其范围在0-255 之间,而彩色图像却是由三种颜色组成的也就是所说的三原色RGB分别为Red、Green、Blue三种颜色组成,这三种颜色每个分量各占一个字节,所以这里需要三个字节,另外在BMP图像中还一个结构为RGBQUAD的结构体,这里一个像素占据的是4个字节,其实,这里就涉及到了8位图像24位图像以及32位图像的问题了,所谓的8位图像其实,每一个像素占一个字节,24位图像,每一个像素占据3个字节、而32位图像每一个像素占据4个字节就是这么来的。
2.海报化
图2 海报化处理效果
所谓的海报化其实就是将每一个像素的分量与224进行与运算,而244的16进制表示可以表示成0xe0,前面介绍了一个像素的分量的范围在0-255范围内,所以只需要将这两个数值的二进制位相与即可完成海报化的处理效果。
下面为实现的具体代码:
//海报化 void CBMPSampleDlg::Posterize() { m_Dib.AttachMapFile("1.bmp", TRUE); m_Dib.CopyToMapFile("海报化.bmp"); RGBTRIPLE *rgbtri = (RGBTRIPLE *)m_Dib.m_lpImage; int iSize = m_Dib.GetSizeImage(); BITMAPINFOHEADER * pBmiHeader = (BITMAPINFOHEADER *)m_Dib.m_lpBMIH; for(int i = 0; i < iSize/( pBmiHeader->biBitCount / 8); i++) { rgbtri[i].rgbtRed = (BYTE) (rgbtri[i].rgbtRed & 0xe0); rgbtri[i].rgbtGreen = (BYTE) (rgbtri[i].rgbtGreen & 0xe0); rgbtri[i].rgbtBlue = (BYTE) (rgbtri[i].rgbtBlue & 0xe0); } DrawPic(); } |
上面的这段代码是我参考DirectShow里面的ezrgb24滤镜这个例子改写的,另外下面的灰度化也是采用里面的改写的。
3.灰度化
图3 灰度化处理效果
灰度化有很多种处理方法,有分量法、最大值法、平均值法以及加权平均值法。
1)分量法
将彩色图像中的三分量的亮度作为三个灰度图像的灰度值,可根据应用需要选取一种灰度图像。
f1(i,j)=R(i,j) f2(i,j)=G(i,j)f3(i,j)=B(i,j)
其中fk(i,j)(k=1,2,3)为转换后的灰度图像在(i,j)处的灰度值。
2)最大值法
将彩色图像中的三分量亮度的最大值作为灰度图的灰度值。
f(i,j)=max(R(i,j),G(i,j),B(i,j))
3) 平均值法
将彩色图像中的三分量亮度求平均得到一个灰度图。
f(i,j)=(R(i,j)+G(i,j)+B(i,j)) /3
4) 加权平均法
根据重要性及其它指标,将三个分量以不同的权值进行加权平均。由于人眼对绿色的敏感最高,对蓝色敏感最低,因此,按下式对RGB三分量进行加权平均能得到较合理的灰度图像。
f(i,j)=0.30R(i,j)+0.59G(i,j)+0.11B(i,j))
在我们的程序中,我们采用的是加权平均法进行灰度化。
下面为实现的代码:
//灰度化 void CBMPSampleDlg::ConvertToGray() { m_Dib.AttachMapFile("1.bmp", TRUE); m_Dib.CopyToMapFile("灰度化.bmp"); RGBTRIPLE *rgbtri = (RGBTRIPLE *)m_Dib.m_lpImage; int iSize = m_Dib.GetSizeImage(); BITMAPINFOHEADER * pBmiHeader = (BITMAPINFOHEADER *)m_Dib.m_lpBMIH; int iGrayvalue = 0; //遍历每一个像素 for(int i = 0; i < iSize/( pBmiHeader->biBitCount / 8); i++) { iGrayvalue = int( rgbtri[i].rgbtBlue * 0.11 + rgbtri[i].rgbtGreen * 0.59 + rgbtri[i].rgbtRed * 0.3 ); rgbtri[i].rgbtRed = (BYTE) iGrayvalue; rgbtri[i].rgbtGreen = (BYTE) iGrayvalue; rgbtri[i].rgbtBlue = (BYTE) iGrayvalue; } DrawPic();
} |
在上述代码中,通过遍历每一个像素,然后计算该像素的三个分量的加权平均值,将三个分量设置成同一个值,这样就实现了对图像的灰度化处理。
4.模糊化
图4 模糊化处理效果
其实所谓的模糊化,就是将各个像素的相邻的像素的各个分量的值相加,然后除以2就可以实现对图像的模糊处理。
下面给出代码:
//模糊化 void CBMPSampleDlg::Blur() { m_Dib.AttachMapFile("1.bmp", TRUE); m_Dib.CopyToMapFile("模糊化.bmp"); RGBTRIPLE *rgbtri = (RGBTRIPLE *)m_Dib.m_lpImage; int iSize = m_Dib.GetSizeImage(); BITMAPINFOHEADER * pBmiHeader = (BITMAPINFOHEADER *)m_Dib.m_lpBMIH; LONG lHeight = pBmiHeader->biHeight; LONG lWidth = pBmiHeader->biWidth; for (int y = 0 ; y < lHeight; y++) { for (int x = 2 ; x < lWidth; x++, rgbtri ++) { rgbtri->rgbtRed = (BYTE) ((rgbtri->rgbtRed + rgbtri[2].rgbtRed) >> 1); rgbtri->rgbtGreen = (BYTE) ((rgbtri->rgbtGreen + rgbtri[2].rgbtGreen) >> 1); rgbtri->rgbtBlue = (BYTE) ((rgbtri->rgbtBlue + rgbtri[2].rgbtBlue) >> 1); } rgbtri +=2; } DrawPic(); } |
上面的代码同样是遍历每一个像素将前一个像素和后一个像素相加,然后将获得的值右移一位,这样就能实现除以2的效果,之所以做位运算,是因为位运算的速度比除法运算要快很多。
在介绍缩放算法之前,先介绍一些基本的概念
图像放大算法
图像放大有许多算法,其关键在于对未知像素使用何种插值方式。以下我们将具体分析几种常见的算法,然后从放大后的图像是否存在色彩失真,图像的细节是否得到较好的保存,放大过程所需时间是否分配合理等多方面来比较它们的优劣。
当把一个小图像放大的时候,比如放大400%,我们可以首先依据原来的相邻4个像素点的色彩值,按照放大倍数找到新的ABCD像素点的位置并进行对应的填充,但是它们之间存在的大量的像素点,比如p点的色彩值却是不可知的,需要进行估算。
图5原始图像的相邻4个像素点分布图
图6 图像放大4倍后已知像素分布图
1)最临近点插值算法(Nearest Neighbor)
最邻近点插值算法是最简单也是速度最快的一种算法,其做法是將放大后未知的像素点P,將其位置换算到原始影像上,与原始的邻近的4周像素点A,B,C,D做比较,令P点的像素值等于最靠近的邻近点像素值即可。如上图中的P点,由于最接近D点,所以就直接取P=D。
这种方法会带来明显的失真。在A,B中点处的像素值会突然出现一个跳跃,这就是出现马赛克和锯齿等明显走样的原因。最临近插值法唯一的优点就是速度快。
2)双线性插值算法(Bilinear Interpolation)
其做法是將放大后未知的像素点P,將其位置换算到原始影像上,计算的四個像素点A,B,C,D对P点的影响(越靠近P点取值越大,表明影响也越大),其示意图如下。
图7 双线性插值算法示意图
其具体的算法分三步:
第一步插值计算出AB两点对P点的影响得到e点的值。
图8 线性插值算法求值示意图
对线性插值的理解是这样的,对于AB两像素点之间的其它像素点的色彩值,认定为直线变化的,要求e点处的值,只需要找到对应位置直线上的点即可。换句话说,A,B间任意一点的值只跟A,B有关。
第二步,插值计算出CD两点对P点的影响得到f点的值。
第三步,插值计算出ef两点对P点的影响值。
双线性插值算法由于插值的结果是连续的,所以视觉上会比最邻近点插值算法要好一些,不过运算速度稍微要慢一点,如果讲究速度,是一个不错的折衷。
3)双立方插值算法(Bicubic Interpolation)
双立方插值算法与双线性插值算法类似,对于放大后未知的像素点P,将对其影响的范围扩大到邻近的16个像素点,依据对P点的远近影响进行插值计算,因P点的像素值信息来自16个邻近点,所以可得到较细致的影像,不过速度比较慢。
图 9双立方插值的附近4个临近点
好了,在介绍完了这些基础知识后,我们接下来讲解如何实现这些算法。
5.最临近点插值缩放
图10 最邻近点插值放大(2倍)处理效果
(由于Word版面原因,您看到的图像被word自动缩放成合适的宽度了)
最临近插值的的思想很简单。对于通过反向变换得到的的一个浮点坐标,对其进行简单的取整,得到一个整数型坐标,这个整数型坐标对应的像素值就是目的像素的像素值,也就是说,取浮点坐标最邻近的左上角点(对于DIB是右上角,因为它的扫描行是逆序存储的)对应的像素值。可见,最邻近插值简单且直观,但得到的图像质量不高。
下面给出算法的实现:
void CBMPSampleDlg::NearestNeighbourInterpolation(float fWRatio, float fHRatio) { //打开旧图像 CDib srcDib; srcDib.AttachMapFile("1.bmp", TRUE);
//获取旧的高和宽 LONG lOldHeight = srcDib.m_lpBMIH->biHeight; LONG lOldWidth = srcDib.m_lpBMIH->biWidth;
//保存旧的图像的字节 WORD wOldBitCount = srcDib.m_lpBMIH->biBitCount;
//计算新的高和宽 //四舍五入 LONG lNewHeight = LONG ( lOldHeight * fHRatio + 0.5 ); LONG lNewWidth = LONG ( lOldWidth * fWRatio + 0.5 );
CSize size; size.cx = lNewWidth; size.cy = lNewHeight;
//创建图像 m_Dib.CreatDib( size, wOldBitCount );
//获取旧图像每行字节数 LONG lOldLineBytes = WIDTHBYTES( lOldWidth * wOldBitCount ); //获取新图像每行的字节数 LONG lNewLineBytes = WIDTHBYTES( lNewWidth * wOldBitCount );
//计算填充的字节 int iNewFillBytes = lNewLineBytes - lNewWidth * ( wOldBitCount / 8 ); int iOldFillBytes = lOldLineBytes - lOldWidth * ( wOldBitCount / 8 );
//指定图像大小,新图像的大小有不同 m_Dib.m_dwSizeImage = lNewLineBytes * lNewHeight; //分配空间 m_Dib.AllocImage();
//定位到数据的开头,也就是图像最后一行的行首 LPRGBTRIPLE DRgb = ( LPRGBTRIPLE ) m_Dib.m_lpImage; LPRGBTRIPLE SRgb = ( LPRGBTRIPLE ) srcDib.m_lpImage;
//将每行的头指针存储起来 PRGBTRIPLE *pNewRow = (PRGBTRIPLE *)malloc(sizeof(PRGBTRIPLE) * lNewHeight); PRGBTRIPLE * pOldRow = (PRGBTRIPLE *)malloc(sizeof(PRGBTRIPLE) * lOldHeight);
LONG srcCol = 0,srcRow = 0; for (int row = 0 ; row < lNewHeight ; row ++) { pNewRow[row] = PRGBTRIPLE( (PBYTE)DRgb + row * lNewLineBytes ); }
for (int row = 0 ; row < lOldHeight ; row ++) { pOldRow[row] = PRGBTRIPLE ( (PBYTE)SRgb + row * lOldLineBytes ); }
for ( LONG row = 0 ; row < lNewHeight ; row ++ ) { //对每一行进行处理 for ( LONG col = 0 ; col < lNewWidth ; col ++ ) { //计算在源图像中的坐标srcCol 和srcRow //四舍五入 srcCol = (LONG) floor(col / fWRatio); srcRow = (LONG) floor(row / fHRatio);
//判断计算出来的坐标是否在源图像中 if ( ( ( srcCol >= 0 ) && ( srcCol < lOldWidth ) ) && ( ( srcRow >= 0 ) && ( srcRow < lOldHeight ) ) ) { //定位指针到源图像的位置 //复制像素的值 *(pNewRow[row] + col) = *(pOldRow[srcRow] + srcCol); } } } //释放申请的内存 free( pNewRow ); free( pOldRow );
DrawPic(); m_Dib.CopyToMapFile("最邻近法插值.bmp"); } |
图中的fWRatio, fHRatio分别为水平方向和垂直方向的缩放系数,如果大于1则为放大,如果大于0小于1则为缩小。
6.双线性插值缩放
图11 双线性插值放大(2倍)处理效果
(由于Word版面原因,您看到的图像被word自动缩放成合适的宽度了)
下面给出双线性插值的具体实现:
//双线性插值法 void CBMPSampleDlg::BiLinearInterpolation(float fWRatio, float fHRatio) {
//打开旧图像 CDib srcDib; srcDib.AttachMapFile("1.bmp", TRUE);
//获取旧的高和宽 LONG lOldHeight = srcDib.m_lpBMIH->biHeight; LONG lOldWidth = srcDib.m_lpBMIH->biWidth;
//保存旧的图像的字节 WORD wOldBitCount = srcDib.m_lpBMIH->biBitCount;
//计算新的高和宽 //四舍五入 LONG lNewHeight = LONG ( lOldHeight * fHRatio + 0.5 ); LONG lNewWidth = LONG ( lOldWidth * fWRatio + 0.5 );
CSize size; size.cx = lNewWidth; size.cy = lNewHeight;
//创建图像 m_Dib.CreatDib( size, wOldBitCount );
//获取旧图像每行字节数 LONG lOldLineBytes = WIDTHBYTES( lOldWidth * wOldBitCount ); //获取新图像每行的字节数 LONG lNewLineBytes = WIDTHBYTES( lNewWidth * wOldBitCount );
//计算填充的字节 int iNewFillBytes = lNewLineBytes - lNewWidth * ( wOldBitCount / 8 ); int iOldFillBytes = lOldLineBytes - lOldWidth * ( wOldBitCount / 8 );
//指定图像大小,新图像的大小有不同 m_Dib.m_dwSizeImage = lNewLineBytes * lNewHeight; //分配空间 m_Dib.AllocImage();
//定位到数据的开头,也就是图像最后一行的行首 LPRGBTRIPLE DRgb = ( LPRGBTRIPLE ) m_Dib.m_lpImage; LPRGBTRIPLE SRgb = ( LPRGBTRIPLE ) srcDib.m_lpImage;
//将每行的头指针存储起来
PRGBTRIPLE *pNewRow = (PRGBTRIPLE *)malloc( sizeof(PRGBTRIPLE) * lNewHeight ); PRGBTRIPLE * pOldRow = (PRGBTRIPLE *)malloc( sizeof(PRGBTRIPLE) * lOldHeight );
for (int row = 0 ; row < lNewHeight ; row ++) { pNewRow[row] = PRGBTRIPLE( (PBYTE)DRgb + row * lNewLineBytes ); }
for (int row = 0 ; row < lOldHeight ; row ++) { pOldRow[row] = PRGBTRIPLE ( (PBYTE)SRgb + row * lOldLineBytes ); }
float p = 0 , q = 0; LONG PACol = 0,PARow = 0; PRGBTRIPLE PA = NULL ,PB = NULL , PC = NULL , PD = NULL , PDst = NULL; for ( LONG row = 0 ; row < lNewHeight ; row ++ ) { //对每一行进行处理 for ( LONG col = 0 ; col < lNewWidth ; col ++ ) { //计算在源图像中的坐标
PACol = (LONG) floor( col / fWRatio ); PARow = (LONG) floor( row / fHRatio );
//计算P和Q p = col / fWRatio - PACol; q = row / fHRatio - PARow;
//判断计算出来的坐标是否在源图像中 if ( ( ( PACol >= 0 ) && ( PACol < lOldWidth -1 ) ) && ( ( PARow >= 0 ) && ( PARow < lOldHeight -1 ) ) ) { //获得四周的像素指针
//PA即 指向A点的指针,如下图为各个点的分布 // A | C // q // -p Dst // // B D // PA = pOldRow[PARow] + PACol; PB = pOldRow[PARow + 1] + PACol; PC = pOldRow[PARow] + PACol + 1; PD = pOldRow[PARow + 1] + PACol + 1;
//需要确定的像素的指针,在目标图像中 PDst = pNewRow[row] + col;
//对目标像素的分量进行计算
//Blue PDst->rgbtBlue = BYTE( ( 1 - p ) * ( 1 - q ) * PA->rgbtBlue + ( 1 - p ) * q * PB->rgbtBlue + p * ( 1- q ) * PC->rgbtBlue + p * q * PD->rgbtBlue ); if(PDst->rgbtBlue < 0) { PDst->rgbtBlue = 0; } if (PDst->rgbtBlue > 255) { PDst->rgbtBlue = 255; }
//Green PDst->rgbtGreen = BYTE( ( 1 - p ) * ( 1 - q ) * PA->rgbtGreen + ( 1 - p ) * q * PB->rgbtGreen + p * ( 1- q ) * PC->rgbtGreen + p * q * PD->rgbtGreen ); if(PDst->rgbtGreen < 0) { PDst->rgbtGreen = 0; } if (PDst->rgbtGreen > 255) { PDst->rgbtGreen = 255; }
//Red PDst->rgbtRed = BYTE( ( 1 - p ) * ( 1 - q ) * PA->rgbtRed + ( 1 - p ) * q * PB->rgbtRed + p * ( 1- q ) * PC->rgbtRed + p * q * PD->rgbtRed ); if(PDst->rgbtRed < 0) { PDst->rgbtRed = 0; } if (PDst->rgbtRed > 255) { PDst->rgbtRed = 255; }
} } }
free( pNewRow ); free( pOldRow );
DrawPic(); m_Dib.CopyToMapFile("双线性插值法.bmp"); } |
7.高斯模糊
图12高斯模糊处理效果
这里的高斯平滑(模糊)或者说滤波器就是这样一种带权的平均滤波器. 那么这些权重如何分布呢? 我们先来看几个经典的模板例子:
尝试了使用这些滤波器对我们原来的图进行操作, 得到了这样的一组结果:
图13 原图
图14 3x3 高斯模板处理效果
图15 5x5 高斯模板处理效果
单纯从效果来看, 两个模板都起到了平滑的作用, 只是程度有深浅的区分. 那么从理论上来说为什么能起到平滑的作用呢? 很显然, 像素的颜色不仅由自身决定了, 同时有其周围的像素加权决定, 客观上减小了和周围像素的差异. 同时这些权重的设定满足了越近权重越大的规律. 从理论来讲, 这些权重的分布满足了著名的所谓高斯分布:
这就是1维的计算公式
这就是2维的计算公式
x, y表示的就是当前点到对应点的距离, 而那些具体的模板就是由这里公式中的一些特例计算而来. 需要说明的是不只有这么一些特例, 从wikipedia可以方便地找到那些复杂的模板。
下面给出实现的代码:
void CBMPSampleDlg::GaussianSmooth() { CDib srcDib; srcDib.AttachMapFile("1.bmp", TRUE); srcDib.CopyToMapFile("高斯模糊.bmp");
//获取旧的高和宽 LONG lOldHeight = srcDib.m_lpBMIH->biHeight; LONG lOldWidth = srcDib.m_lpBMIH->biWidth;
//保存旧的图像的字节 WORD wOldBitCount = srcDib.m_lpBMIH->biBitCount;
//获取旧图像每行字节数 LONG lOldLineBytes = WIDTHBYTES( lOldWidth * wOldBitCount );
PRGBTRIPLE * pOldRow = (PRGBTRIPLE *)malloc( sizeof(PRGBTRIPLE) * lOldHeight );
LPRGBTRIPLE SRgb = ( LPRGBTRIPLE ) srcDib.m_lpImage; //计算每一行的起点指针 for (int row = 0 ; row < lOldHeight ; row ++) { pOldRow[row] = PRGBTRIPLE ( (PBYTE)SRgb + row * lOldLineBytes ); }
//高斯模板 5 * 5 int templates[25] = { 1, 4, 7, 4, 1, 4, 16, 26, 16, 4, 7, 26, 41, 26, 7, 4, 16, 26, 16, 4, 1, 4, 7, 4, 1 };
//各个分量的和 int Bsum = 0 , GSum = 0 , RSum = 0;
//在高斯模板中的索引 int index = 0;
//用到的临时指针 PRGBTRIPLE PTemp = NULL , PDst = NULL;
//在模板中的值 int s = 0;
//计算在5*5的高斯模板中的目标像素RGB的值 for ( int row =2 ; row <lOldHeight - 2 ; row++ ) { for ( int col =2 ; col < lOldWidth - 2 ; col++ ) { Bsum = GSum = RSum = 0; index = 0;
for ( int m = row - 2 ; m < row + 3 ; m++) { for (int n=col - 2 ; n < col + 3 ; n++) { PTemp = pOldRow[m] + n; s = templates[index ++];
//Blue Bsum += PTemp->rgbtBlue * s;
//Green GSum += PTemp->rgbtGreen * s;
//Red RSum += PTemp->rgbtRed * s;
} }
Bsum /= 273; GSum /= 273; RSum /= 273;
//判断计算出来的值是否合法 //如果超过了255则设成255 //依次对BGR进行检查 if (Bsum > 255) { Bsum = 255; }
if (GSum > 255) { GSum = 255; }
if (RSum > 255) { RSum = 255; }
PDst = pOldRow[row] + col; PDst->rgbtBlue = Bsum; PDst->rgbtGreen = GSum; PDst->rgbtRed = RSum; } }
DrawPic();
} |
代码中给出的是5*5的高斯模板实现的高斯模糊。由于时间原因,就先粗略地讲到这里。
好了一下子给出这么多算法,需要一点时间消化一下,我会把实现的工程放在我的CSDN上,方便大家下载。敬请期待正在酝酿的《SIFT算法原理与实现》,此篇算是对SIFT算法的一些关键的算法进行介绍吧。
如果您有什么看不懂的,可以提出您的意见。
Email:xizero00#163.com
将#换成@就是我的邮箱,广交八方朋友!
另外在写这篇文章的时候也参阅了不少文献和博客,在此一并感谢他们的无私的付出,只有每一个人的分享才能让大家有所收获!谢谢你们!
这里把工程的文件放到了CSDN上供感兴趣的童鞋们下载