你所能用到的BMP格式介绍(二)

一、可能你忽视的基础

        在正式开始之前,我不得不从最基本的地方开始,因为这些地方大多数人会忽视的一干二净,如果不在开始进行说明,那么在后面一定会有很多困惑的地方。

        最开始我们回到数字在计算机里的表示,回到最开始的问题上-2&-255是多少,那么我们首先得回顾一下-2在计算机里面的表示是什么样子的。

        最开始,希望你还记得什么叫无符号整数和有符号整数,实际上,你得庆幸在一般的图像中没有浮点数,不然你需要复习或者重新学习的基础比这个还多。

        先说有符号整数,从计算机组成原理中,有符号整数是以2的补码表示的,有很多办法计算这个2的补码,但是我最推荐的可能不是你最常见的那个方法,我推荐的是权值法,因为我觉得这个方法最符合逻辑也最符合2的补码的数学表达式。

        什么是权值法呢?我们从最简单的开始,比如说二进制101,我们之所以能转换成十进制5是因为我们对每一位都赋予了一个权值,从最低位开始权值是2的0次 方,权值依次向最高位提高一个幂值,所以我们能够计算为2^2*1+2^1*0+2^0*1,得出的值是5,而每一个位都有一个权值。对于2的补码也可以 按照这样的思路,但是第一位的权值略有不同,如果同样是101,如果表示的有符号的整数,我们赋予最高位的权值是2的-2次方,于是这个二进制补码转换成 十进制就是2^-2*1+2^1*0+2^0*1,这样就是-3,你可以用传说中反码加一的方式验证。

       为什么我说权值法最符合逻辑呢?我来举个例子,就说3这个数,无论是作为有符号整数还是无符号整数,其表示方法都是11,我同样可以表示成为 0000000011,前面的1只要你喜欢,无论写多少都不会改变3这个值。那么如果是-3呢?我们前面已经验证了-3的二进制补码表示为101,现在我 们试试看将符号位扩展,也就是前面添加1,得到11101(为了我后面的演算方便,就不添加很多1 了),那么这个值是多少呢?按照权值法,最高位是2的-4次方(这个-4就是最高位的位数),后面权值不变,我们来计算这个值2^- 4*1+2^3*1+2^2*1+2^1*0+2^0*1,计算一下吧,还是等于-3,这就是符号扩展的意义。

        有时候,看似简单的东西往往蕴含了很多的学问。

        下面,就要联系c++里面的基本数据类型来说明上面这个问题了,在c++中表示无符号整数用unsigned关键字,根据图像的像素的取值范围是[0,255],那么最适合表示这个值的c++数据类型是什么?

         在回答这个问题之前,再来看一下更基本的一个问题,一个int值在我们的计算机中(32位)是由几个字节表示的?你可能很快的可以回答出是4个字节,那 int的表示范围-2147483648 ~ +2147483647,这个值对于图像的像素的表示范围太大了,[0,255]这个值需要8位就能表示出来,而在32位系统中char是唯一的1个字节 的数据类型,但是还要注意的是char的范围是-128~+127,也就是有符号的,所以我们表示一个像素值应该用的是unsigned char。

        也许你觉得太罗嗦了,那么这是指多媒体编程的冰山一角,下面我开始以实际的例子演示,每一步你都有可能出错。

二、我们从显示图像数据开始

        图像文件再怎么特殊,实际上它还是一个文件,所以要读取一个图像,自然要用文件流(如果忘了或者不知道的,那我这里只能介绍最基本的,只能靠你自己了)。     

        我们用文件流读入一个图片。

        我很想解释为什么后面要用ios::binary,但是篇幅有限,就先这样认为它是必要的好了。

        第一个演示的目的是为了能够制作一个类似UE效果的功能,程序运行结果如下图:

       看起来很无趣的黑色,实现这个功能的核心代码如下:

 while(getline(fin,str))  
  {  
int length=str.length();  
for(int i=0;i<length;i++, count++)  
{       if(count>0&&count%16==0)  
        cout<<endl;  
 if((int)((unsigned char)str[i])<0x10)  
    {  
        cout<<"0";  
    }  
    cout<<hex<<(int)((unsigned char)str[i])<<" ";  
}  
 
int num=10;  
total_str+=str+(unsigned char)num;  
}  
   cout<<endl;  

       代码虽然很少看上去比程序还混乱,我会一行一行解释的,首先第一行一行一行的读取文件中的数值,有没有思考过,文件流读bmp中怎么样算是一 行?这问题下面再进行说明,但是可以明确告诉你的是,bmp中绝对不是按一个像素行为一行的。

       接下来是取得读入字符串的长度,再接下来进入循环,循环的一开始的一个判断是为了做每行显示16个字符的,可以不用管它,然后下面的这些看似简单的包含了这个程序的核心部分。

       让我们回到字节0-1:42 4d这上面来,按照程序我们读入这个第一个字节,str[0]储存的是’B’这个字符,也是42这个值(至于为什么是42,参见ASCII码表),按照我 们第一部分说的,对于一字节的数我们应该用unsigned char来表示,所以我们进行了将str[i]的值转换成unsigned char,至于再转换成int是为了能够显示出十六进制的数字。

        一个让人疑惑的地方出现了,循环内为什么要有最后两句,这要回到getline这个函数的原理上面了,前面说过getline是读取文件的一行,那么怎样判断文件的一行呢?我们来看一下getline的定义:

      After the function extracts an element that compares equal to delim,in which case the element is neither put back nor appended to the controlledsequence.

     msdn上写的是如果读到一个终止符,那么这个函数结束并且这个终止符不会加到这个字符串中,哪些字符是终止符呢? 一般来说换行(0x0A)和回车(0x0D)都会被选为终止符,括号是他们的ASCII码。

     如此便得到了一个图像文件的全部数据,在我们的程序中是存储在total_str之中的。这里面包括了文件信息头,位图信息头,调色板(当然如果有的 话),位图数据区,下面需要进行的就是依次取出每个信息,然后保存起来,按照前面的字节顺序,注意小端法或者大段法。

     这里我想提到的一个问题就是,为什么在第一部分说了一下看似没有关系进制转换问题呢?如果你在存储各个位图部分的数据的时候,如果发现输出的数据不对,请你返回去仔细阅读一下第一部分以及回忆下基本数据的长度。

三、把数据存储组织起来

        下面说明怎么将上面取出的数据字符串按照bmp的四部分存储起来,存储的目的一个是为了标示,还有一个作用就是在读取的时候方面取出来。       

        首先,bmp最先的一个部分是文件信息头,我们定义一个结构如下:

       在开始下面介绍的内容之前,先要说明一下这些UINT16以及DWORD的数据类型:

        在32位计算机中short由两个字节表示的,int是四字节表示的,所以UINT16,WORD表示的是两个字节,DWORD,LONG都表示四字节数。

结构中各个字段的意思如注释所示,和前面说明的文件信息头的字段是一一对应的,下面来展示怎么样将读出来的字符串(total_str)赋值到某一个字段上。

       根据我们读取的字符串,total_str[0]和total_str[1]分别是字符’b’和’m’,这是两个字节,那么我们需要将BITMAPFILEHEADER里面的bfType赋值为这两个值。采用的赋值方法如下所示:

         <<是不常见的左移操作,这样的话方便的就能将两个一字节的数扩展为双字节的数,这样bh.bfType里面存的值就是0x424d,你可以输出来进行验证。    

         这个看起来不起眼的操作似乎很容易扩展成将四个四字节的数扩展为一个四字节的数,但是当你操作的时候就会发现并不是这样:

        如果你是这样操作的话,那么你可以做一下输出,你会发现结果出乎你意料的错误,为什么?在第一部分我提过的符号扩展,后面所有的都没有进行过类型转 换,total_str[i]进行移位并且或操作默认的是带符号操作的,举个例子,如果total_str 5-2依次是00 00 00 f3,那么你这样做之后bh.bfSize就会变成ff ff ff f3,虽然bh.bfSize是无符号数,但是右边的是有符号数,所以你得到的一定是错误的结果。

        那正确的做法应该是怎么样的?一种做法如下:

        至于为什么这样是对的,我在这里不进行说明了。

        按照上面的方法,依次对文件信息头内的字段赋值,一样的对其他三个部分进行赋值,

        位图信息头结构:

         数据区结构:

         调色板结构:

        也许,你会问我,数据区和调色板为什么不用unsigned char, 这里完全可以用unsigned char 我用UINT16 的目的是因为我懒得再重新定义一个数据类型了。

        赋值的时候要注意的我在上面说过了,其他的要注意的我想说的就是不要忘记有数据对齐,如果你忘了,请你回去再看一下前面的说明,当你能够把bmp的每一 部分保存下来之后,那么你就可以对bmp做各种操作了,从另一种角度来说,你可以自己构造自己的bmp位图。

四、如果你会MFC

      如果你熟悉windows编程,那么你一定知道上面的结构在MFC以及windows编程里都是已经有了的结构,那你需要做的只是将图片加载到内存,在控件上显示。

     如果你想学这一部分的MFC构成,那么搜索的关键词是”MFC GDI”,在进行这个部分的MFC编程时,但是MFC的位图编程这一块所提供的类CBitmap并不好用,包括CPalette,BITMAP结构等等都 不是很好用,所以在实际开发中,有很多人都是选择自己包装一下再进行使用。

     这个部分如果你都可以会MFC的话,那么你就不是初级入门选手了,主要的学习方法应该是查阅MSDN和资料,如果你不会MFC的话,这一点篇幅也是说明不了任何问题的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏数据小魔方

左手用R右手Python系列——面向对象编程基础

面向对象编程是程序设计中一种重要且高效的编程规范,它区别于常见的面向过程编程。在R语言以及Python的程序包开发过程中,大量使用了面向对象的编程范式。 百度...

51112
来自专栏阿凯的Excel

文本数字拆分技巧(第二弹!)

上期刚刚分享了简单的通过智能填充和Len与LenB函数实现的文本数字拆分! 感兴趣可以点我先看上一期的! 本期难度较上期略有提高,和您分享新的技巧。 ? 没...

3007
来自专栏行者悟空

我眼中的并发编程——Fork/Join模型

1845
来自专栏灯塔大数据

技术 | Python从零开始系列连载(十九)

但它的特点就是下次使用next(a)时,接着上次的断点继续运行,直到下一个yield

1083
来自专栏老马说编程

计算机程序的思维逻辑 (9) - 强大的循环

循环 上节我们介绍了流程控制中的条件执行,根据具体条件不同执行不同操作。本节我们介绍流程控制中的循环,所谓循环就是多次重复执行某些类似的操作,这个操作一般不是...

2058
来自专栏java达人

哈希表

哈希表是种数据结构,它可以提供快速的插入操作和查找操作。第一次接触哈希表时,它的优点多得让人难以置信。不论哈希表中有多少数据,插入和删除(有时包括侧除)只需要接...

1827
来自专栏Spark学习技巧

Flink DataStream编程指南

Flink程序是执行分布式集合转换(例如,filtering, mapping, updating state, joining, grouping, defi...

1.7K7
来自专栏java一日一条

成为优秀Swift开发者的10条建议

在这里给大家分享一些帮助大家成为更优秀的Swift开发者的建议,让你的代码,写的更少,性能更优 。

1112
来自专栏

shell之sort命令

1 sort的工作原理 sort将文件的每一行作为一个单位,相互比较,比较原则是从首字符向后,依次按ASCII码值进行比较,最后将他们按升序输出。 [rocro...

2097
来自专栏云飞学编程

python学习,数据分析系列工具,初识numpy

其实,数据分析看着很高大上,也很实用,但是真的很枯燥啊。。。。但是它又不得不学,毕竟数据分析对很多工作是很有帮助的,比如爬虫,抓到的数据,不论是保存到文件还是数...

862

扫码关注云+社区

领取腾讯云代金券