最近一直在思考存储BMP图像的问题,在网上看的文章对于原理的讲述都很明确,但具体的实现代码方面有所欠缺。这次用Java语言来给大家说一下BMP的存储方式。
在讲解具体格式之前,给大家提一个小建议,因为对于图片的写入也是一个不小的工程,容易出现很多的细节问题,所以我们可以先利用WinHex对我们的目标图像进行转码,之后将我们得到的图像与其再转码后两方对照,更易分析问题所在。
BMP图像存储
学习前提:
1. writeInt
OutputStream out;
out.write(int a);该方法将该整数写入文件,是存在数据缺少的。它只写入八位数据,a的低八位,24个高位被省略。
所以对于超过255的整数,这样的写法存在问题。在BMP存储的时候,有的时候是四个字节为一个单位,对于int型,我们要将它的四个八位分别拆出来,再依次写入数据。
注意!BMP在存储时,高位放高字节,低位放低字节,所以我们要先写int型的低八位,最后写高八位。
private void writeInt(BufferedOutputStream ops, int t) throws IOException {
int a = (t >> 24) & 0xff;
int b = (t >> 16) & 0xff;
int c = (t >> 8) & 0xff;
int d = t & 0xff;
ops.write(d);
ops.write(c);
ops.write(b);
ops.write(a);
}
2. writeShort
short型也占用两个字节,同理进行处理。
private void writeShort(BufferedOutputStream ops, short t) throws IOException {
int c = (t >> 8) & 0xff;
int d = t & 0xff;
ops.write(d);
ops.write(c);
}
2. writeColor
这个方法是针对于24位BMP图像,位图数据存储的是实际的像素值。24/8 = 3,所以每一个像素占用三个字节,分别是r,g,b。
我们利用getRGB()方法得到的整数,8(a) 8(b) 8 (g)8(r)。
private void writeColor(BufferedOutputStream ops, int t) throws IOException {
int blue = (t>>16)&0xff;//此处写入三个颜色
int green = (t>>8)&0xff;
int red = t&0xff;
ops.write(red);
ops.write(green);
ops.write(blue);
}
一、 24色位图的存储
(1)存储结构
- BMP文件头
- 位图信息头
- 颜色索引表(对于24色位图,无颜色索引表)
- 位图数据
(2)代码实现
- BMP文件头代码及部分代码语句详解
public void saveBMP(BufferedOutputStream ops) throws IOException{
ops.write('B');//0x42 = B
ops.write('M');//0x4D = M
int size = 14 + 40 +height*width*3+(4-width*3%4)*height;
writeInt(ops,size);
writeShort(ops,(short)0);
writeShort(ops,(short) 0);
writeInt(ops,54);
}
int size = 14 + 40 +heightwidth3+(4-width3%4)height;
其为位图文件大小的计算公式:
14:代表BMP文件头的大小
40 :代表位图信息头的大小
heightwidth3:因为一个像素对应3个8位的数据存储(r,g,b三色)所以我们需要×3
(4-width*3%4)*height:位图进行存储的时候每行的数据的长度必须是4的倍数,所以对于不是4的倍数的可以用比特补充(可以用0填充);
附:若有颜色索引表,此处也要加上颜色索引表这部分的大小
witeInt(ops,54);
54代表偏移量: 即从BMP文件头到实际的图像数据所对应的偏移量(BMP文件头+位图信息头+颜色索引表大小)
- 位图信息头代码及部分代码语句详解
public void savebmpInfo(BufferedOutputStream ops) throws IOException{
writeInt(ops,40);//位图的信息头所占有的字节数
writeInt(ops,width);
writeInt(ops,height);
writeShort(ops,(short)1);//颜色平面数
witeShort(ops,(short) 24);//说明比特数/像素数,值有1、2、4、8、16、24、32
writeInt(ops,0);//BI_RGB, 说明本图像不压缩
writeInt(ops,size - 54);
writeInt(ops,0);//水平分辨率,缺省
writeInt(ops,0);//垂直分辨率,缺省
writeInt(ops,2);//说明本位图实际使用的颜色索引数,单色位图有两个颜色
writeInt(ops,2);//说明本位图实际使用的颜色索引数
}
writeInt(ops,size - 54);
位图图像数据大小,原先我们求的size为位图图像数据大小+偏移量,这里是在原来的值上减去偏移量.
- 位图图像数据代码及部分代码语句详解
public void savebmpData(BufferedOutputStream ops) throws IOException{
int m = 0;
if(width*3%4>0){
m = 4 - width*3%4;//m代表补充的字节数,对于这些字节是没有意义的跳过
}
int[][] imgData = new int[width][height];
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
imgData[i][j] = image.getRGB(i, j);
}
}
int count = 0,gray = 0,sum = 0;
byte c = 0;
int[] pix =new int[8];
for(int i = height - 1;i >= 0 ;i--){
for(int j = 0;j < width;j++){
int t = imgData[j][i];
writeColor(ops,t);
for(int k = 0;k<m;k++){
ops.write(0);
}
}
}
与上文的求size相呼应,这里我们再重申一下。
BMP图像要求每行的数据的长度必须是4的倍数,如果不够需要进行比特填充(以0填充),这样可以达到按行的快速存取。
位图数据区的大小不一定是图片宽×图片高×每像素字节数,需要加上比特填充 :
m = 4 - width3%4;//m代表补充的字节数,对于这些字节是没有意义的跳过*
当我们获取到图像的颜色后,需注意:由于位图信息头中的图像高度是正数,所以位图数据在文件中的排列顺序是从左下角到右上角,以行为主序排列的。我们写入的时候要分外注意这一点。
(3)问题及解决
当时在学习这一部分的时候,真的头大,还好有大佬们的文章辅助,理解了大致原理,但是动手写代码起来不知从何下手,这么多数据真的有点恐怖。
后来搜索了一些代码类的文章,看到了大家的思路,主要是根据大类分别创建方法,在方法里进行相应的小部分写入,逻辑清晰,易实现。
我当时主要在求文件的大小,以及图像颜色的写入部分报错严重。文件大小要注意补充的部分以及它具体的公式是怎样的,不能漏掉任意一个部分。颜色问题时写入的顺序从左到右,从下到上,当时没有考虑,总出现数组越界。后来我就把颜色存入数组,在获取数组数据,其实现在想想这一步其实也是没有必要的。
二、 单色位图的存储
(1)存储结构
相比于原先的存储,单色多了一项索引表,相应的也会引起偏移量的变化。
- BMP文件头
- 位图信息头
- 颜色索引表
- 位图数据
单色位图只有黑白两色,一个颜色占用四个字节存储,所以颜色索引表的大小为8个字节。
我现在遇到的问题,当我利用电脑自带的画图软件存储的时候,单色位图的颜色为
00 00 00 00 FF FF FF 00
(2)代码实现
public void saveBMP(BufferedOutputStream ops) throws IOException{
ops.write('B');//0x42 = B
ops.write('M');//0x4D = M
size = 14 + 40 + 8 + (height*width) / 8;
writeInt(ops,size);
writeShort(ops,(short)0);
writeShort(ops,(short) 0);
writeInt(ops,62);
}
public void savebmpInfo(BufferedOutputStream ops) throws IOException{
writeInt(ops,40);
writeInt(ops,width);
writeInt(ops,height);
writeShort(ops,(short)1);
writeShort(ops,(short) 1);//对于单色位图,所做的修改
writeInt(ops,0);
writeInt(ops,size - 62);//单色图像实际数据的大小
writeInt(ops,0);
writeInt(ops,0);
writeInt(ops,2);//说明本位图实际使用的颜色索引数,单色位图有两个颜色
writeInt(ops,2);//说明本位图实际使用的颜色索引数
}
public void saveColor(BufferedOutputStream ops) throws IOException {
//单色位图只存在黑白两个颜色
writeShort(ops,(short) 65535);//白色
writeShort(ops,(short) 65535);//白色
writeInt(ops,0);//黑色
}
/*
* 保存BMP位图信息数据部分的方法
*/
public void savebmpData(BufferedOutputStream ops) throws IOException{
int m = 0;
if(width*3%4>0){
m = 4 - width*3%4;//m代表补充的字节数,对于这些字节是没有意义的跳过
System.out.println("m "+m);
}
int[][] imgData = new int[width][height];
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
imgData[i][j] = image.getRGB(i, j);
}
}
int count = 0,gray = 0,sum = 0;
byte c = 0;
int[] pix =new int[8];
for(int i = height - 1;i >= 0 ;i--){
for(int j = 0;j < width;j++){
//对于单色位图,一个像素只写一个字节的一位
int t = imgData[j][i];
gray = toGray(t);
if(gray < 255/2){
pix[count++] = 1;
}else{
pix[count++] = 0;
}
while(count == 8){
for (int k = 0; k < pix.length; k++) {
pix[k] = (int) (pix[k]*Math.pow(2.0, (double)(k)));//k+1改为k,没有白条出现
c = (byte) (c + pix[k]&0xff);
}
ops.write(c);
++sum;
count = 0;
c = 0;
}
}
for(int k = 0;k<m;k++){
ops.write(0);
}
}
}
private void writeShort(BufferedOutputStream ops, short t) throws IOException {
int c = (t >> 8) & 0xff;
int d = t & 0xff;
ops.write(d);
ops.write(c);
}
private void writeInt(BufferedOutputStream ops, int t) throws IOException {
int a = (t >> 24) & 0xff;
int b = (t >> 16) & 0xff;
int c = (t >> 8) & 0xff;
int d = t & 0xff;
ops.write(d);
ops.write(c);
ops.write(b);
ops.write(a);
}
public byte[] divide(int num){
byte[] bytes = new byte[4];
bytes[0] = (byte) ((num)&0xff);//整数的低八位
bytes[1] = (byte) ((num>>8)&0xff);
bytes[2] = (byte) ((num>>16)&0xff);
bytes[3] = (byte) ((num>>24)&0xff);
return bytes;
}
public int toGray(int t){
int r = (t >> 16) & 0xff;
int g = (t >> 8) & 0xff;
int b = 0xff & t;
int gray = (int)(r*0.3+g*0.59+b*0.11);
return gray;
}
public void saveColor(BufferedOutputStream ops) throws IOException {
//单色位图只存在黑白两个颜色
writeShort(ops,(short) 65535);//白色
writeShort(ops,(short) 65535);//白色
writeInt(ops,0);//黑色
}
保存颜色索引,遇到的问题我不知道我这样对不对
gray = toGray(t);
单色图像只有黑白两色,直接对彩色图像处理是不易确定黑白值的,所以我们先将其转为灰度图像,是像素点的灰度值在0-255之间这个时候确定一直,转为黑白就比较方便。
pix[k] = (int) (pix[k]Math.pow(2.0, (double)(k)));
c = (byte) (c + pix[k]&0xff);
这里因为用1/8个字节存储一个像素,而我们写入的时候是以一个字节为单位的,所以我们可以每次读八个数据,再将这些数据存在一个大小为8的数组里,再分别把每一位放到字节中。(我在这里有一个思路,有待实现,就是构造一个方法,例如byte.charat[i]就可以把整数i放到字节相应的位上。)
我现在的图像出现的形式还是比较奇怪的,一开始有白条,是因为把k写成了k+1,后来就是转出来的黑白图像奇奇怪怪。
(3)问题及解决
单色图像我会努力的。
二、 其他格式图像的思考
现在主要存在的问题是颜色索引表的问题,对于颜色索引表的创建,以及位图数据里在相应的位置添加对应的颜色索引,还有点模糊,加油加油!!!
其他需要更改的地方:主要就是索引表的变化引起偏移量的变化!!!