
数字图像在外存储器设备中的存储形式是图像文件,图像必须按照某个已知的、公认的数据存储顺序和结构进行存储,才能使不同的程序对图像文件顺利进行打开或存盘操作,实现数据共享。图像数据在文件中的存储顺序和结构称为图像文件格式。目前广为流传的图像文件格式有许多种,常见的格式包括BMP、GIF、JPEG、TIFF、PSD、DICOM、MPEG等。在各种图像文件格式中,一部分是由某个软硬件厂商提出并被广泛接受和采用的格式,例如BMP、GIF和PSD格式;另一部分是由各种国际标准组织提出的格式,例如JPEG、TIFF和DICOM,其中JPEG是国际静止图像压缩标准组织提出的格式,TIFF是由部分厂商组织提出的格式,DICOM是医学图像国际标准组织提出的医学图像专用格式。
BMP文件是Windows操作系统所推荐和支持的图像文件格式,是一种将内存或显示器的图像数据不经过压缩而直接按位存盘的文件格式,所以称为位图(bitmap)文件,因其文件扩展名为BMP,故称为BMP文件格式,简称BMP文件。本书对图像的算法编程都是针对BMP图像文件的,因此在本章中我们详细介绍BMP文件结构及其读写操作,以加深对图像数据的理解。
1.2.1 BMP文件结构
如图1-7所示,BMP图像文件被分成4个部分:位图文件头(Bitmap File Header)、位图信息头(Bitmap Info Header)、颜色表(Color Map)和位图数据(即图像数据,Data Bits或Data Body)。
第1部分为位图文件头BITMAPFILEHEADER,是一个结构体类型,该结构的长度是固定的,为14个字节。其定义如下:
typedef struct tagBITMAPFILEHEADER
{
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER, FAR *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;
BITMAPFILEHEADER结构的各个域详细说明如下:
— bfType:位图文件类型,必须是0x424D,即字符串“BM”,也就是说,所有的文件的头两个字节都是“BM”。
— bfSize:位图文件大小,包括这14个字节。
— bfReserved1, bfReserved2:Windows保留字,暂不用。
— bfOffBits:从文件头到实际的位图数据的偏移字节数,图1-7中前3个部分的长度之和。
图1-7 BMP文件结构示意图
第2部分为位图信息头BITMAPINFOHEADER,也是一个结构体类型的数据结构,该结构的长度也是固定的,为40个字节(WORD为无符号16位整数,DWORD为无符号32位整数,LONG为32位整数)。其定义如下:
typedef struct tagBITMAPINFOHEADER
{
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;
BITMAPINFOHEADER结构的各个域的详细说明如下:
— biSize:本结构的长度,为40个字节。
— biWidth:位图的宽度,以像素为单位。
— biHeight:位图的高度,以像素为单位。
— biPlanes:目标设备的级别,必须是1。
— biBitCount:每个像素所占的位数(bit),其值必须为1(黑白图像)、4(16色图)、8(256色)、24(真彩色图),新的BMP格式支持32位色。
— biCompresssion:位图压缩类型,有效的值为BI_RGB(未经压缩)、BI_RLE8、BI_RLE4、BI_BITFILEDS(均为Windows定义常量)。这里只讨论未经压缩的情况,即biCompression=BI_RGB。
— biSizeImage:实际的位图数据占用的字节数,该值的大小在第4部分位图数据中有具体解释。
— biXPelsPerMeter:指定目标设备的水平分辨率,单位是像素/米。
— biYPelsPerMeter:指定目标设备的垂直分辨率,单位是像素/米。
— biClrUsed:位图实际用到的颜色数,如果该值为零,则用到的颜色数为2的biBitCount次幂。
— biClrImportant:位图显示过程中重要的颜色数,如果该值为零,则认为所有的颜色都是重要的。
第3部分为颜色表。颜色表实际上是一个RGBQUAD结构的数组,数组的长度由biClrUsed指定(如果该值为零,则由biBitCount指定,即2的biBitCount次幂个元素)。RGBQUAD结构是一个结构体类型,占4个字节,其定义如下:
typedef struct tagRGBQUAD
{
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
}RGBQUAD;
RGBQUAD结构的各个域的详细说明如下:
— rgbBlue:该颜色的蓝色分量;
— rgbGreen:该颜色的绿色分量;
— rgbRed:该颜色的红色分量;
— rgbReserved:保留字节,暂不用。
有些位图需要颜色表;有些位图(如真彩色图)则不需要颜色表,颜色表的长度由BITMAPINFOHEADER结构中biBitCount分量决定。对于biBitCount值为1的二值图像,每像素占1bit,图像中只有两种(如黑白)颜色,颜色表也就有21=2个表项,整个颜色表的大小为个字节;对于biBitCount值为8的灰度图像,每像素占8bit,图像中有28=256种颜色,颜色表也就有256个表项,且每个表项的R、G、B分量相等,整个颜色表的大小为个字节;而对于biBitCount=24的真彩色图像,由于每像素3个字节中分别代表了R、G、B三分量的值,此时不需要颜色表,因此真彩色图的BITMAPINFOHEADER结构后面直接就是位图数据。
第4部分是位图数据,即图像数据,其紧跟在位图文件头、位图信息头和颜色表(如果有颜色表的话)之后,记录了图像的每一个像素值。对于有颜色表的位图,位图数据就是该像素颜色在调色板中的索引值;对于真彩色图,位图数据就是实际的R、G、B值(三个分量的存储顺序是B、G、R)。下面分别就2色、16色、256色和真彩色位图的位图数据进行说明:
— 对于2色位图,用1位就可以表示该像素的颜色,所以1个字节能存储8个像素的颜色值。
— 对于16色位图,用4位可以表示一个像素的颜色。所以一个字节可以存储2个像素的颜色值。
— 对于256色位图,1个字节刚好存储1个像素的颜色值。
— 对于真彩色位图,3个字节才能表示1个像素的颜色值。
需要注意两点:
第一,Windows规定一个扫描行所占的字节数必须是4的倍数,不足4的倍数则要对其进行扩充。假设图像的宽为biWidth个像素、每像素biBitCount个比特,其一个扫描行所占的真实字节数的计算公式如下:
DataSizePerLine = (biWidth * biBitCount /8+ 3) / 4*4
那么,不压缩情况下位图数据的大小(BITMAPINFOHEADER结构中的biSizeImage成员)计算如下:
biSizeImage = DataSizePerLine * biHeight
第二,一般来说,BMP文件的数据是从图像的左下角开始逐行扫描图像的,即从下到上、从左到右,将图像的像素值一一记录下来,因此图像坐标零点在图像左下角。
1.2.2 BMP图像文件的读写
分析了BMP文件结构后,让我们用简单的C程序实现一个给定BMP位图文件的读写操作,来进一步巩固对图像数据的理解,这也是我们后续图像可视化编程的基础。此部分的代码以及后面两节所讲述的代码在工程chap1-1中的bmpReadWrite.cpp文件中,读者可以查阅。
1.BMP文件的读入
BMP文件分为4个组成部分,那么BMP文件的读入也要按照4个组成部分依次进行处理,即先处理BITMAPFILEHEADER结构,然后是BITMAPINFOHEADER结构、颜色表,最后是位图数据。
首先,有关BITMAPFILEHEADER、BITMAPINFOHEADER、RGBQUAD等结构的定义包含在头文件“Windows.h”中,应把其包含进来。
#include "Windows.h"
其次,为了后面对图像进行修改及存盘方便,我们定义了几个全局变量,用来存放读入图像的位图数据、宽、高、颜色表及每像素位数等信息。所定义的全局变量如下:
unsigned char *pBmpBuf;//读入图像数据的指针
int bmpWidth;//图像的宽
int bmpHeight;//图像的高
RGBQUAD *pColorTable;//颜色表指针
int biBitCount;//图像类型,每像素位数
根据BMP文件结构,BMP文件读入操作的基本流程如图1-8所示。
图1-8 BMP文件读入操作流程图
readBmp()函数实现了BMP文件的读取操作,下面的代码是对readBmp()函数的说明和实现。
/***********************************************************************
* 函数名称:
* readBmp()
*
*函数参数:
* char *bmpName -文件名字及路径
*
*返回值:
* 0为失败,1为成功
*
*说明:给定一个图像文件名及其路径,读图像的位图数据、宽、高、颜色表及每像素
* 位数等数据进内存,存放在相应的全局变量中
***********************************************************************/
bool readBmp(char *bmpName)
{
//二进制读方式打开指定的图像文件
FILE *fp=fopen(bmpName,"rb");
if(fp==0) return 0;
//跳过位图文件头结构BITMAPFILEHEADER
fseek(fp, sizeof(BITMAPFILEHEADER),0);
//定义位图信息头结构变量,读取位图信息头进内存,存放在变量head中
BITMAPINFOHEADER head;
fread(&head, sizeof(BITMAPINFOHEADER), 1,fp);
//获取图像宽、高、每像素所占位数等信息
bmpWidth = head.biWidth;
bmpHeight = head.biHeight;
biBitCount = head.biBitCount;
//定义变量,计算图像每行像素所占的字节数(必须是4的倍数)
int lineByte=(bmpWidth * biBitCount/8+3)/4*4;
//灰度图像有颜色表,且颜色表表项为256
if(biBitCount==8){
//申请颜色表所需要的空间,读颜色表进内存
pColorTable=new RGBQUAD[256];
fread(pColorTable,sizeof(RGBQUAD),256,fp);
}
//申请位图数据所需要的空间,读位图数据进内存
pBmpBuf=new unsigned char[lineByte * bmpHeight];
fread(pBmpBuf,1,lineByte * bmpHeight,fp);
//关闭文件
fclose(fp);
return 1;
}
2.BMP文件的存盘
给定图像路径名以及图像的数据,对图像的写操作也是按照BMP文件4个组成部分进行分别处理的。其基本流程如图1-9所示。
saveBmp()函数实现了BMP文件的写操作,该函数的说明及代码实现如下。
/*****************************************
* 函数名称:
* saveBmp()
*
*函数参数:
* char *bmpName-文件名字及路径
* unsigned char *imgBuf-待存盘的位图数据
* int width-以像素为单位待存盘位图的宽
* int height-以像素为单位待存盘位图高
* int biBitCount-每像素所占位数
* RGBQUAD *pColorTable-颜色表指针
*返回值:
* 0为失败,1为成功
*
*说明:给定一个图像位图数据、宽、高、颜色表指针及每像素所占的位数等信息,
* 将其写到指定文件中
***********************************************************************/
bool saveBmp(char *bmpName, unsigned char *imgBuf, int width, int height,
int biBitCount, RGBQUAD *pColorTable)
{
//如果位图数据指针为0,则没有数据传入,函数返回
if(!imgBuf)
return 0;
//颜色表大小,以字节为单位,灰度图像颜色表为1024字节,彩色图像颜色表大小为0
int colorTablesize=0;
if(biBitCount==8)
colorTablesize=1024;
//待存储图像数据每行字节数为4的倍数
int lineByte=(width * biBitCount/8+3)/4*4;
//以二进制写的方式打开文件
FILE *fp=fopen(bmpName,"wb");
if(fp==0) return 0;
//申请位图文件头结构变量,填写文件头信息
BITMAPFILEHEADER fileHead;
fileHead.bfType = 0x4D42;//bmp类型
//bfSize是图像文件4个组成部分之和
fileHead.bfSize= sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)
+ colorTablesize + lineByte*height;
fileHead.bfReserved1 = 0;
fileHead.bfReserved2 = 0;
//bfOffBits是图像文件前3个部分所需空间之和
fileHead.bfOffBits=54+colorTablesize;
//写文件头进文件
fwrite(&fileHead, sizeof(BITMAPFILEHEADER),1, fp);
//申请位图信息头结构变量,填写信息头信息
BITMAPINFOHEADER head;
head.biBitCount=biBitCount;
head.biClrImportant=0;
head.biClrUsed=0;
head.biCompression=0;
head.biHeight=height;
head.biPlanes=1;
head.biSize=40;
head.biSizeImage=lineByte*height;
head.biWidth=width;
head.biXPelsPerMeter=0;
head.biYPelsPerMeter=0;
//写位图信息头进内存
fwrite(&head, sizeof(BITMAPINFOHEADER),1, fp);
//如果灰度图像,有颜色表,写入文件
if(biBitCount==8)
fwrite(pColorTable, sizeof(RGBQUAD),256, fp);
//写位图数据进文件
fwrite(imgBuf, height*lineByte, 1, fp);
//关闭文件
fclose(fp);
return 1;
}
对于readBmp()和saveBmp()函数的简单调用如下:
void main()
{
//读入指定BMP文件进内存
char readPath[]="dog.BMP";
readBmp(readPath);
//输出图像的信息
printf("width=%d,height=%d, biBitCount=%d\\n",bmpWidth,bmpHeight, biBitCount);
//将图像数据存盘
char writePath[]="dogcpy.BMP";
saveBmp(writePath, pBmpBuf, bmpWidth, bmpHeight, biBitCount, pColorTable);
//清除缓冲区,pBmpBuf和pColorTable是全局变量,在文件读入时申请的空间
delete []pBmpBuf;
if(biBitCount==8)
delete []pColorTable;
}
该main()函数将指定BMP文件读入内存,将图像信息打印输出,最后又原样存入指定文件中。读者可以打开程序当前目录下的“”和“”两个文件进行对比。
以上对于BMP文件的读写函数仅针对灰度图像(biBitCount=8)和彩色图像(biBitCount=24)两种格式,对于其他如biBitCount=1的图像类型,读者可以根据需要,自己对程序作简单的修改即可实现。本书中后续的代码实现也都是围绕灰度和彩色两种格式进行的,希望读者予以注意。
1.2.3 BMP图像位图数据的访问
上面main()函数将图像文件读入内存,又写到文件里去,那么在读入图像数据后、写入文件前的一段时间里,图像的数据是在内存中存在的,这也是我们可以修改(访问)图像数据的时机所在。
假设内存中位图数据的指针为pBmpBuf,一行像素所占的字节数为lineByte(4的倍数)那么,对于灰度图像,第i行第j列的像素指针(所在的存储空间位置)为pBmpBuf+i*lineByte+j,*(pBmpBuf+i*lineByte+j)是该像素的灰度值,如果想让该像素变成指定颜色,只需要给*(pBmpBuf+i*lineByte+j)赋指定的值即可;对于彩色图像,每像素占3个字节,那么pBmpBuf+i*lineByte+j*3+0、pBmpBuf+i*lineByte+j*3+1、pBmpBuf+i*lineByte+j*3+2分别代表了第i行第j列像素B、G、R三个分量的指针,若想给该点指定一种颜色,则需要给三个分量分别赋值。
下面的main()函数中,将读入的图像数据左下角1/4部分置成黑色并存盘,图1-10所示是程序运行前后图像的变化情况。
void main()
{
//读入指定BMP文件进内存
char readPath[]="dog.BMP";
readBmp(readPath);
//输出图像的信息
printf("width=%d,height=%d,biBitCount=%d\\n",bmpWidth,bmpHeight,biBitCount);
//循环变量,图像的坐标
int i,j;
//每行字节数
int lineByte=(bmpWidth*biBitCount/8+3)/4*4;
//循环变量,针对彩色图像,遍历每像素的三个分量
int k;
//将图像左下角1/4部分置成黑色
if(biBitCount==8){//对于灰度图像
for(i=0;i } } } else if(biBitCount==24){//彩色图像 for(i=0;i *(pBmpBuf+i*lineByte+j*3+k)=0; } } } //将图像数据存盘 char writePath[]="dogcpy.BMP"; saveBmp(writePath, pBmpBuf, bmpWidth, bmpHeight, biBitCount, pColorTable); //清除缓冲区,pBmpBuf和pColorTable是全局变量,在文件读入时申请的空间 delete []pBmpBuf; if(biBitCount==8) delete []pColorTable; } 图1-10 数据修改后图像对比 1.2.4 灰度图像的颜色表 如表1-2所示,灰度图像的颜色表是一个256个表项的RGBQUAD结构体数组(具体参看BMP文件结构中对颜色表的描述),而每个RGBQUAD中的R、G、B分量的值是相等的。随着颜色表数组下标从0到255变化,颜色表数组元素中R、G、B分量也从0到255依次变化。灰度图像的位图数据每像素一个字节,其值为0到255之间的一个,当显示一幅灰度图像时,系统根据像素值,到颜色表数组下标与之对应的表项(数组元素)中查看颜色,根据该表项中的颜色显示像素。由于灰度图像颜色表每个表项中R、G、B分量相等,因此只有图像亮度信息,没有颜色信息,因而显示出的灰度图像也就没有颜色了。 表1-2 灰度图像的颜色表 void main() { //读入指定BMP文件进内存 char readPath[]="dog.BMP"; readBmp(readPath); //输出图像的信息 printf("width=%d,height=%d,biBitCount=%d\\n",bmpWidth,bmpHeight,biBitCount); //改变灰度图像的颜色表蓝色分量的值,查看前后变化 if(biBitCount==8){ for(int i=0; i<256;i++){ pColorTable[i].rgbBlue = 255-pColorTable[i].rgbBlue; } } //将图像数据存盘 char writePath[]="dogcpy.BMP"; saveBmp(writePath, pBmpBuf, bmpWidth, bmpHeight, biBitCount, pColorTable); //清除缓冲区,pBmpBuf和pColorTable是全局变量,在文件读入时申请的空间 delete []pBmpBuf; if(biBitCount==8) delete []pColorTable; } 改变后灰度图像颜色表变为表1-3的形式(蓝色分量是结构体的第一个分量),此时颜色表R、G、B三个分量的值已经不再相等,因此图像也就具有了颜色信息,只不过此时的颜色并不是物体真实的颜色,读者可以运行程序,查看颜色表改变前后图像颜色的变化。 表1-3 改变后的颜色表 本节内容用简单的C语言、采用结构化的程序设计思想实现了BMP文件的读写及访问操作,这只是我们后面可视化图像编程的前奏,有了这个基础后,我们来学习MFC库中的GDI位图,并采用面向对象的思想自己设计实现一个图像类(ImgCenterDIB类)。 1.3 GDI对象及GDI位图 GDI是图形设备接口(Graphics Device Interface)的缩写,Windows的GDI对象类型是通过Microsoft基础类库(MFC)中的类来表示的,而CGdiObject正是所有GDI对象类的抽象基类,即Windows的GDI对象是通过CGdiObject派生类的C++对象来表示的。下面我们给出了GDI派生类的列表。 — CBitmap——位图是一种位矩阵,每一个显示像素都对应于其中的一个或多个位。 — CBrush——刷子定义了一种位图形式的像素,利用它可以对区域内部填充颜色。 — CFont——字体是一种具有某种风格和尺寸的所有字符的完整集合,它常被当做资源存于磁盘中,其中有一些还依赖于某种设备。 — CPallete——调色板是一种颜色映射接口,它允许应用程序在不干扰其他应用程序的前提下,可以充分利用输出设备的颜色描绘能力。 — CPen——笔是一种用来画线及绘制有形边框的工具,可以指定它的颜色及厚度。 — CRgn——区域是由多边形、椭圆或者二者组合形成的一种范围,可利用它进行填充、裁剪以及鼠标点中测试。 CBitmap类封装了Windows GDI位图,同时提供了一些操作位图的成员函数。像笔和字体一样,CBitmap对象是GDI对象的一种,在使用CBitmap对象时必须创建一个CBitmap对象,然后把它选进设备环境中,再调用该类中的成员对位图进行操作,当我们对它使用完后,还必须将它从设备环境中选出来并删除掉。 1.3.1 从资源中装入GDI位图 使用位图的最简单的方法就是从资源中直接获取位图。以chap1_2工程文件为例,如果我们看一下Workspace窗口里的ResourceView,就会发现一个工程位图资源列表。其中IDB_building是一个位图资源的ID,打开IDB_building位图资源的属性对话框,可以看到其对应的位图文件位于工程文件res目录下的,它是一个湖边楼阁的照片,因资源位图不支持24位真彩色,因此该图像应该是256色的索引位图。 图1-11 位图资源IDB_building的属性对话框 CBitmap类中的LoadBitmap() 函数成员能够将基于资源的DIB转换成GDI位图。用如下简单的OnDraw()函数就可以显示building图像了。 void CChap1_2View::OnDraw(CDC* pDC) { //CBitmap对象 CBitmap bitmap; //设备环境类对象 CDC dcMemory; //加载资源位图 bitmap.LoadBitmap(IDB_building); //创建内存设备环境 dcMemory.CreateCompatibleDC(pDC); //把位图选进内存设备环境,并保存旧的GDI位图对象 CBitmap *oldbitmap=dcMemory.SelectObject(&bitmap); //显示 pDC->BitBlt(0,0,400,300, &dcMemory,0,0,SRCCOPY); //释放bitmap,恢复原GDI位图 dcMemory.SelectObject(oldbitmap); } 程序中BitBlt()函数将building的像素从内存显示环境复制到显示器(或打印机)设备环境中。building位图的尺寸是300´400,在显示器上,它占据了逻辑单位为300´400的一个矩形区域,并且该区域的左上角位于Windows客户区域的(0,0)点处。下面是BitBlt()函数的原型。 Bool BitBlt(int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, DWORD dwRop); 其中: — x和y表示绘制区域的左上角坐标,跟显示映射模式有关。 — nWidth表示绘制区域的宽度。 — nHeight表示绘制区域的高度。 — pSrcDC表示要复制位图所在的CDC对象指针。 — xSrc和ySrc表示原位图要绘制区域的左上角坐标(逻辑单位)。 — dwRop表示绘制方式,此处有多个选项,读者可以查阅相关书籍或MSDN,此处不赘述。 chap1_2的运行结果如图1-12所示。 图1-12 运行结果 1.3.2 对位图进行伸缩处理 为了显示需要,可以对位图进行伸缩处理,这可以由StretchBlt()函数来完成。StretchBlt()函数的原型如下: BOOL StretchBlt(int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, int nSrcWidth, int nSrcHeight, DWORD dwRop); 其中,nSrcWidth和nSrcHeight用来指定要复制原图像的宽度和高度,其他参数与BitBlt()函数相同。 以下是我们在工程chap1_3中实现的位图伸缩显示,读者可以运行程序查验显示结果。 void CChap1_3View::OnDraw(CDC* pDC) { //CBitmap对象 CBitmap bitmap; //设备环境类对象 CDC dcMemory; //加载资源位图 bitmap.LoadBitmap(IDB_building); //创建内存设备环境 dcMemory.CreateCompatibleDC(pDC); //把位图选进内存设备环境,并保存旧的GDI位图对象 CBitmap *oldbitmap=dcMemory.SelectObject(&bitmap); //将300*400的图像伸缩显示在以(0,0)点开始的250*450的一个区域内 pDC->StretchBlt(0,0,450,250, &dcMemory,0,0,400,300,SRCCOPY); //释放bitmap,恢复原GDI位图 dcMemory.SelectObject(oldbitmap); } 程序将building伸缩显示在250´450的矩形区域,因与图像原长宽比不同,因此景物出现了变形,图1-13所示是伸缩显示的结果。 图1-13 伸缩显示的结果
从以上的解释中我们知道,图像颜色表决定了图像的颜色,如果对灰度图像的颜色表数据进行改变,图像颜色自然也就被改变了。下面的main()函数,把灰度图像颜色表的蓝色分量进行了改变。B G R 保 留 位 0 0 0 不确定 1 1 1 不确定 2 2 2 不确定 3 3 3 不确定 M M M M 254 254 254 不确定 255 255 255 不确定
这里需要说明的是,为了说明方便,我们在1.2.3和1.2.4两小节中都重新写了main()函数,这些main()函数均在chap1-1中的bmpReadWrite.cpp文件内。而C语言中,一个程序只能有一个main()函数,读者在调用其中一个main()函数时,需要将其他两个屏蔽掉。B G R 保留位 255 0 0 不确定 254 1 1 不确定 253 2 2 不确定 252 3 3 不确定 M M M M 1 254 254 不确定 0 255 255 不确定
