问题:现有一张bmp图片,要求将它读取到程序中并进行灰度化、水平翻转、模糊、茶色滤镜四种效果的一种,并输出新图片,如下所示:

命令行输入:

其中:

参数1:-b/g/s/r,先后表示blur(模糊),grey(灰度化),sepia(褐色),row reverse(水平翻转)

参数2:源文件名

参数3:新文件名

当我第一次接触到这个问题时,是无从下手的。但在查阅了不少资料之后,整整一天,我成功地只用C++实现了打开、修饰、保存bmp文件的功能!

目录

1.bmp文件的基本信息

(1).bmp文件的种类

(2).bmp文件结构(重点)

1>文件头

2>信息头

3>调色板(不作讨论)

4>图像颜色信息

2.实现思路

3.定义相关类、结构体

文件头BmpFileHeader

内存对齐和#pragma pack(n)

信息头BmpFileInfoHeader

颜色结构体RGBTriple

图片类bmp

4.读文件

(1).基础知识

(2).读文件的准备工作

(3).读文件头和数据头

(4).读取图像颜色信息

5.写文件

6.修饰图片

(1).灰度化

(2).棕色滤镜效果

(3).水平翻转

(4).模糊

7.main函数

命令行传参

具体实现


1.bmp文件的基本信息

(1).bmp文件的种类

打开Windows自带的画图软件,发现bmp的存储格式有好几种。

  • 单色位图:只有黑白两种颜色,每个像素占1位(1/8字节)
  • 16色位图:每个像素占4位(1/2字节)
  • 256色位图:每个像素占8位(1字节)
  • 24位位图(真彩色):每个像素占24位(3字节),每个字节存储R/G/B三种中的一种颜色数值(0~255)

每个像素占的位数被称为位深度(biBitCount,在后面会用到),可以在图片的属性->详细信息中查看。

(2).bmp文件结构(重点)

bmp文件数据由4部分组成:

  1. 文件头
  2. 文件信息头
  3. 调色板(24位位图无)
  4. 图像颜色信息

在此只讨论24位位图即真彩色的问题,至于其他的bmp文件种类不做讨论。

先放出图片:

1>文件头

bfType 如果是bmp文件,值为“BM”,对应十进制为19778
bfSize 文件总大小
bfReserved1 保留字1,一般为0
bfReserved2 保留字2,一般为0
bfOffBits 文件起始位置距真正的图像信息的距离

2>信息头

biSize 信息头大小,24位图中为40
biWidth 图像宽度(px),即水平方向的像素个数
biHeight 图像高度(px),即垂直方向的像素个数
biPlanes 一般为1
biBitCount 位深度,重要,决定了bmp的类型
biCompression 是否压缩,一般为0
biSizeImages 图像颜色信息占用的实际字节数,包括了对齐所需的0
biXPelsPerMeter 水平分辨率
biYPelsPerMeter 垂直分辨率
biClrUsed 一般为0
biClrImportant 一般为0

注意:我们可能会发现 biWidth*3*biHeight与biSizeImages并不一样,这是为什么呢?接下来会解释。

3>调色板(不作讨论)

4>图像颜色信息

  1. 像素的存储顺序是从下到上,从左到右,在文件中以类似一维数组的方法线性存储。
  2. 每个像素的颜色信息每3个字节一组,按BGR的顺序存放。
  3. 其中每个字节只存一个颜色值。颜色值范围是0~255,用无符号char型存储。

三个图就能说明问题:

但是这些数据真的如此紧密地排列吗?

对于宽为4的倍数的图片(如:1024px),确实如此。每一行的像素数据存完后,紧挨着存储下一行像素的数据,行与行的数据之间没有空隙。

但对于宽度不是4的倍数的图片(如:474px),每一行的像素数据存储完后,会自动空出几个字节,直到这一行的字节数为4的倍数为止。

直接呈上图片:

biWidth=4时:很好,不用补任何0,因为4*3=12已经是4的倍数

biWidth=5时:糟糕,5*3=15不是4的倍数,要补一个0才能是4的倍数16

所以,在读取宽度不是4的倍数的图片时,一行的数据读完后,要跳过几个字节才能读到下一行的数据。跳过字节的个数,我取名为offset。它的计算方法如下:

  1. offset = (fileInFoHeader.biWidth * 3) % 4;
  2. if (offset != 0) {
  3. offset = 4 - offset;
  4. }

现在我可以解释2>中末尾提到的问题了。

例子:现在有一张宽度为474px,高度为842px的图片。

不考虑offset时:

474*3*842=1197324(Byte)

考虑时:

(474*3)%4=2

offset=4-2=2(Byte)

每行字节数:474*3+2=1424(Byte)

图像数据总字节数:1424*842=1199008(Byte)

谁对谁错?看看图就知道了。

它们的差值:1199008-1197324= 1684(Byte),而1684=842*2。因为每一行末尾有2字节的空隙,那么,842行的空隙积累起来,正好就是1684字节。

debug的结果说明,图像数据占用的实际字节数是考虑了偏移的,这些数据在存的时候就已经有空隙,因此我们写文件的时候也要刻意的写入空隙,不然系统无法读取我们生成的新图片。这一点在后面很关键!

2.实现思路

首先要把bmp文件读进来。由以上的分析,应该把bmp的文件头、信息头、图像数据分开读取。

然后要生成新bmp文件。应该要依次写入文件头、信息头、图像数据。

最后实现图像处理功能。这些用于图像处理的函数封装在一个单独的头文件中,使用时传入函数指针即可。

3.定义相关类、结构体

定义文件头、信息头结构体(因为它们不需要任何函数),里面存放与文件相关的属性。各个属性的大小参考一开始时的bmp文件结构图,2字节一般定义成unsigned short,4字节一般定义成unsigned int.

定义bmp类(因为它需要定义函数),里面最重要的是一个存放”颜色“结构体对象的数组,用于接收读出的图像颜色数据。还有一个int型的offset,一个文件头结构体对象,一个数据头结构体对象。定义读文件和写文件两个函数。

很自然地,需要一个”颜色“结构体对象,它有三个属性B/G/R。

注意:结构体中,所有属性的定义顺序必须和文件存储的信息顺序一致,否则在读文件时得到的数据会混乱!也就是:必须根据bmp文件结构来!

文件头BmpFileHeader

  1. #pragma pack(2)//注意这里
  2. struct BmpFileHeader {
  3. unsigned short bfType;
  4. unsigned int bfSize;
  5. unsigned short bfReserved1;
  6. unsigned short bfReserved2;
  7. unsigned int bfOffBits;
  8. };

尤其要注意#pragma pack(2),没有这一行,这个结构体占用的空间大小就不是所有属性大小之和,换句话说,它清除了属性与属性之间内存的”空洞“。明白这个,对读文件操作极其重要!

内存对齐和#pragma pack(n)

在此简短地说一下内存对齐问题,用于测试的代码如下:

  1. #include <iostream>
  2. using namespace std;
  3. struct BmpFileHeader {
  4. unsigned short bfType;
  5. unsigned int bfSize;
  6. unsigned short bfReserved1;
  7. unsigned short bfReserved2;
  8. unsigned int bfOffBits;
  9. };
  10. int main()
  11. {
  12. cout << sizeof(BmpFileHeader)<< endl;//16
  13. return 0;
  14. }

但是,这些数据占用的空间按理来说是2+4+2+2+4=14(Byte)才对呀,为什么会输出16呢?

首先放上原理:

结构体的属性是按定义的顺序来存放的。

结构体一般有很多属性,取其中占内存最大的一个,它所占的字节数为默认对齐模数

假定第一个属性的存放地址为0,后来的数据在存放时,取自身数据大小和对齐模数二者的最小值min,寻找离自己最近的而且是min整数倍的地址,把数据存到那里。

字有点多,不好理解,对不对?还是老规矩,画图:

 

给这个结构体指定#pragma pack(2)会怎样呢?

  1. #include <iostream>
  2. using namespace std;
  3. #pragma pack(2)
  4. struct BmpFileHeader {
  5. unsigned short bfType;
  6. unsigned int bfSize;
  7. unsigned short bfReserved1;
  8. unsigned short bfReserved2;
  9. unsigned int bfOffBits;
  10. };
  11. int main()
  12. {
  13. cout << sizeof(BmpFileHeader)<< endl;//14
  14. return 0;
  15. }

输出14,与我们先前预想的相符。

#pragma pack(n)的作用:为当前结构体指定新的对齐模数n。

 

信息头BmpFileInfoHeader

  1. struct BmpFileInFoHeader {
  2. unsigned int biSize;
  3. int biWidth = 0, biHeight = 0;
  4. unsigned short biPlanes;
  5. unsigned short biBitCount;
  6. unsigned int biCompression, biSizeImages;
  7. int biXPelsPerMeter, biYPelsPerMeter;
  8. unsigned int biClrUsed, biClrImportant;
  9. };

这里不用加#pragma pack()的原因是:这些属性按默认对齐模数4正好可以按顺序无空隙地存储,请大家自行验证。

颜色结构体RGBTriple

它放在一个单独的头文件(RGBTriple.h)中,注意里面的#pragma once,防止头文件重复包含。

也要注意,属性的定义顺序与习惯的不同,它只能是BGR,这是为了使读取时数据存放的顺序正确。

  1. #pragma once
  2. struct RGBTriple {
  3. unsigned char blue;
  4. unsigned char green;
  5. unsigned char red;
  6. };

图片类bmp

  1. class bmp {
  2. private:
  3. int offset;//行尾的空隙
  4. RGBTriple* surface;//存图片颜色数据的数组
  5. BmpFileHeader fileHeader;//文件头
  6. BmpFileInFoHeader fileInFoHeader;//数据头
  7. public:
  8. void readPic(const char* fileName);//读文件
  9. void writePic(void (*myMethod)(int,int,RGBTriple*), const char* outFileName);//写文件
  10. };

surface是一个指针,指向堆中的一个存放着RGBTriple结构体对象的数组。那个数组将在读文件时被创建,并一直保留到程序结束。

读文件和写文件的两个方法可以接收字符串作为文件名。写文件的方法还可以接收一个函数指针,指定在写文件之前,对图片进行的修饰操作。

文件头结构体、信息头结构体、图片类定义在同一个头文件(bmpFile.h)中,完整代码如下:

  1. #pragma once
  2. #include<fstream>
  3. #include<iostream>
  4. #include"RGBTriple.h"
  5. #define BMPTYPE 19778
  6. using namespace std;
  7. #pragma pack(2)
  8. struct BmpFileHeader {
  9. unsigned short bfType;
  10. unsigned int bfSize;
  11. unsigned short bfReserved1;
  12. unsigned short bfReserved2;
  13. unsigned int bfOffBits;
  14. };
  15. struct BmpFileInFoHeader {
  16. unsigned int biSize;
  17. int biWidth = 0, biHeight = 0;
  18. unsigned short biPlanes;
  19. unsigned short biBitCount;
  20. unsigned int biCompression, biSizeImages;
  21. int biXPelsPerMeter, biYPelsPerMeter;
  22. unsigned int biClrUsed, biClrImportant;
  23. };
  24. class bmp {
  25. private:
  26. int offset;
  27. RGBTriple* surface;
  28. BmpFileHeader fileHeader;
  29. BmpFileInFoHeader fileInFoHeader;
  30. public:
  31. void readPic(const char* fileName);
  32. void writePic(void (*myMethod)(int,int,RGBTriple*), const char* outFileName);
  33. };

4.读文件

(1).基础知识

C++的fstream头文件提供了文件输入流ifstream和文件输出流ofstream。

两种流对象在使用时有一些相同的步骤:

  • 创建流对象
  • 打开文件:open(“文件路径”,打开方式)
  • (可选)检测文件是否打开:is_open()
  • 使用后关闭流:close()

常见的打开方式:

ios::in 读文件
ios::out 写文件
ios::app 打开文件时,光标在文件末尾
ios::trunc 如果存在同名文件,就删除它创建新文件
ios::binary 以二进制的方式读/写文件

几个不同的打开方式可以用 |(单竖线)连接,表示这种打开方式同时具有两种含义。

一个完整的使用ifstream的例子:

  1. #include<fstream>
  2. #include<iostream>
  3. using namespace std;
  4. int main(){
  5. ifstream ifs;
  6. ifs.open("aaa.bmp",ios::in | ios::binary);
  7. if(!ifs.is_open()){
  8. cout<<"文件未打开!"<<endl;
  9. }
  10. //读取...
  11. ifs.close();
  12. }

ifstream的特有方法:read();ofstream的特有方法:write();它们的函数原型如下:

basic_istream& __CLR_OR_THIS_CALL read(_Elem* _Str, streamsize _Count);

参数1:char*类型,表示待读入的数据的“去路”

参数2:一次读入数据的字节总数

basic_ostream& __CLR_OR_THIS_CALL write(const _Elem* _Str, streamsize _Count);

参数1:char*类型,表示待写入的数据的来源

参数2:一次写入数据的字节总数

(2).读文件的准备工作

新建bmpFile.cpp文件,书写bmp类两个函数的空实现。

  1. #include"bmpFile.h"
  2. void bmp::readPic(const char* fileName) {
  3. }
  4. void bmp::writePic(void (*myMethod)(int,int,RGBTriple*),const char* outFileName) {
  5. }

注意到bmpFile.h中已经引入了头文件fsteram,故直接使用其中的结构即可。

  1. #include"bmpFile.h"
  2. void bmp::readPic(const char* fileName) {
  3. ifstream ifs;
  4. ifs.open(fileName, ios::in|ios::binary);
  5. if (!ifs.is_open()) {
  6. cout << "Can't open the file." << endl;
  7. return;
  8. }
  9. //do something
  10. ifs.close();
  11. }
  12. void bmp::writePic(void (*myMethod)(int,int,RGBTriple*),const char* outFileName) {
  13. }

注意ios::binary,它指定以二进制的方式读文件,如果少了它,程序虽不出错,但输出的图片却是一团黑(亲测)。

(3).读文件头和数据头

按先前的思路来,先读文件头,再读数据头,剩下的就是图像信息了。

ifs.read((char*)&fileHeader,sizeof(BmpFileHeader));

注意里面的强制类型转换。之所以把fileHeader结构体对象的地址转换成char*型,就是因为read()函数只接收char*型的地址

读完文件头之后,有个问题:要是读进来的文件根本不是bmp类型怎么办?那么后面的操作不就失去意义了吗?

所以我们紧接着添加一个if语句来判断读的是不是bmp类型,如果不是,就结束整个函数。由bmp文件头结构可知,其中的bfType如果不是19778,文件就不是bmp类型。19778已经在bmpFile.h中被定义为宏常量BMPTYPE。

  1. if(fileHeader.bfType!=BMPTYPE){
  2. cout<<"文件类型不正确!"<<endl;
  3. return;
  4. }

接着才读取数据头。还是一样的问题,读的bmp文件不是24位的怎么办?非24位的bmp,我们是不能处理的,只能再加上一个判断,如果不是24位就结束整个函数。

  1. ifs.read((char*)&fileInFoHeader, sizeof(BmpFileInFoHeader));
  2. if (fileInFoHeader.biBitCount != 24) {
  3. cout << "invalid!" << endl;
  4. return;
  5. }

(4).读取图像颜色信息

现在终于可以开始读取图像颜色信息啦!但在此之前,我们要解决空隙的问题。在读取数据头之后,我们获得了图像宽度biWidth,这时才可以计算offset的大小。

  1. offset = (fileInFoHeader.biWidth * 3) % 4;
  2. if (offset != 0) {
  3. offset = 4 - offset;
  4. }

还要考虑一个问题:我们必须使用一个存放RGBTriple对象的数组来存整张图片的颜色信息。

这个问题可以分解成两个小问题:

Q1:存在栈里还是存在堆里?

答案显而易见,一般图片的长宽在1000px以上的不在少数,如果存在栈里,栈很可能会溢出。

Q2:定义一维数组还是二维数组?

我们很自然地会想用二维数组,因为图片就是按行、列存储的。但实际上不行,因为尽管new是动态分配内存,二维数组的第二维仍然必须是一个常量,否则new不知道应该返回什么类型的行指针(有关这方面的知识,可以参考其他文章),编译会报错。我们可以用一种“降维”的方法解决这个问题,就是new一个超长的一维数组,它的长度是图片的(长*宽)。

用于存放图像颜色数据的数组surface是一个RGBTriple型的指针。我们new一个新的一维数组,将数组首地址赋给surface。

surface = new RGBTriple[fileInFoHeader.biHeight * fileInFoHeader.biWidth];

到目前为止,我们终于可以开始读bmp文件中最重要的信息啦!

还记得吗?bmp文件的像素存储顺序:从下到上,从左到右。因此我们如果要想在surface存入正常顺序的像素数据(从上到下,从左到右),在向surface中存数据时就有讲究,先读出的像素数据要靠后存储。

一次读入3个字节(BGR),存入surface的某一元素(即:一个RGBTriple对象)中。


这里说句题外话,我遇到过这样一个问题:我一开始为RGBTriple定义了无参、有参构造函数,可以用B、G、R三个参数创建新的RGBTriple对象。接着写了这些代码:

  1. for (int i = fileInFoHeader.biHeight-1;i >=0;i--) {
  2. int ured=0,ublue=0,ugreen=0;
  3. for (int j = 0;j < fileInFoHeader.biWidth;j++) {
  4. ifs.read((char*)(&ured), sizeof(char));   
  5. ifs.read((char*)(&ugreen), sizeof(char));   
  6. ifs.read((char*)(&ublue), sizeof(char));   
  7. RGBTriple rgb(ublue,ugreen,ured);
  8. *(surface+(fileInFoHeader.biWidth * i + j))=rgb;
  9. }
  10. if (offset != 0) {
  11. char ign;
  12. for (int k = 0;k < offset;k++) {
  13. ifs.read(&ign,sizeof(char));
  14. }
  15. }
  16. }

这是一个不太好发现的错误,细心的小伙伴可能已经看出,红色与蓝色的读取顺序反了。这导致输出图片的颜色整体有些偏差,就像这样(我其实也可以把它叫做艺术品?):

题外话结束...


我们直接一次性地从文件中读出3个字节,存入surface数组的某个元素中。

  1. for (int i = fileInFoHeader.biHeight-1;i >=0;i--) {
  2. for (int j = 0;j < fileInFoHeader.biWidth;j++) {
  3. ifs.read((char*)(surface+(fileInFoHeader.biWidth * i + j)), sizeof(RGBTriple));
  4. }
  5. if (offset != 0) {
  6. char ign;
  7. for (int k = 0;k < offset;k++) {
  8. ifs.read(&ign,sizeof(char));
  9. }
  10. }
  11. }

内层for循环结束代表一行像素读取完毕,此时就要注意空隙问题了。有空隙时,必须跳过空隙。如何跳过空隙呢?定义一个临时变量ign(ignore的简写),把offset个字符循环读入ign,最终ign被丢弃。这样就把用于补齐的0读走了,再读取下一行时,读取文件的指针就已经指到了真正的数据上。

完整代码如下:

  1. void bmp::readPic(const char* fileName) {
  2. ifstream ifs;
  3. ifs.open(fileName, ios::in|ios::binary);
  4. if (!ifs.is_open()) {
  5. cout << "Can't open the file." << endl;
  6. return;
  7. }
  8. ifs.read((char*)&fileHeader, sizeof(BmpFileHeader));
  9. if (fileHeader.bfType != BMPTYPE) {
  10. cout << "type error!" << endl;
  11. return;
  12. }
  13. ifs.read((char*)&fileInFoHeader, sizeof(BmpFileInFoHeader));
  14. if (fileInFoHeader.biBitCount != 24) {
  15. cout << "invalid!" << endl;
  16. return;
  17. }
  18. offset = (fileInFoHeader.biWidth * 3) % 4;
  19. if (offset != 0) {
  20. offset = 4 - offset;
  21. }
  22. surface = new RGBTriple[fileInFoHeader.biHeight * fileInFoHeader.biWidth];
  23. for (int i = fileInFoHeader.biHeight-1;i >=0;i--) {
  24. for (int j = 0;j < fileInFoHeader.biWidth;j++) {
  25. ifs.read((char*)(surface+(fileInFoHeader.biWidth * i + j)), sizeof(RGBTriple));
  26. }
  27. if (offset != 0) {
  28. char ign;
  29. for (int k = 0;k < offset;k++) {
  30. ifs.read(&ign,sizeof(char));
  31. }
  32. }
  33. }
  34. ifs.close();
  35. }

5.写文件

写文件比读文件容易,需要使用ofstream对象的write()函数。

用于图像修饰的函数(myMethod)是由函数指针传入的,这个函数指针的类型是:返回值void;参数列表:int height,int width,RGBTriple* (实质上是数组的首地址)。

接下来的操作和读文件大致相同,只是有几个注意点:

  • open()时必须写ios::binary,否则也会产生错误。这种错误与读文件时又不同了,并不是输出乌黑的图片,而是有一种别样的颜色滤镜效果。摆出图片(不得不说,还挺有艺术感?):
  • 从surface的height-1索引开始取出数据用于写入。因为我们读文件时,height-1这里存放的是左下角的像素,我们也应该从左下角的像素开始写才能输出正向的图片。
  • 写完文件之后,surface数组完成了它的使命,应该被delete掉。因为new的是一个数组,所以应该使用delete[],以完全释放内存。

完整代码如下:

  1. void bmp::writePic(void (*myMethod)(int,int,RGBTriple*),const char* outFileName) {
  2. //modify
  3. myMethod(fileInFoHeader.biHeight, fileInFoHeader.biWidth, surface);
  4. //create a new bmp
  5. ofstream ofs;
  6. ofs.open(outFileName, ios::out|ios::binary);
  7. ofs.write((char*)&fileHeader, sizeof(BmpFileHeader));
  8. ofs.write((char*)&fileInFoHeader, sizeof(BmpFileInFoHeader));
  9. //rewrite
  10. for (int i = fileInFoHeader.biHeight - 1;i >= 0;i--) {
  11. for (int j = 0;j < fileInFoHeader.biWidth;j++) {
  12. ofs.write((char*)(surface + (i*fileInFoHeader.biWidth+j)), sizeof(RGBTriple));
  13. }
  14. if (offset != 0) {
  15. char ign=0;
  16. for (int k = 0;k < offset;k++) {
  17. ofs.write(&ign, sizeof(char));
  18. }
  19. }
  20. }
  21. delete[] surface;
  22. ofs.close();
  23. }

6.修饰图片

本质上是对当前对象里的surface数组进行原地修改。

新建一个头文件helpers.h,给出了四个函数的声明。新建源程序文件helpers.cpp提供函数实现。

helpers.h的结构:

  1. #pragma once
  2. #include<iostream>
  3. #include"RGBTriple.h"
  4. using namespace std;
  5. void makeGray(int height , int width , RGBTriple* image);
  6. void makeSpeia(int height, int width, RGBTriple* image);
  7. void rowReverse(int height, int width, RGBTriple* image);
  8. void makeBlur(int height, int width, RGBTriple* image);

helpers.cpp的结构:

  1. #include"helpers.h"
  2. using namespace std;
  3. void makeGray(int height, int width, RGBTriple* image) {
  4. }
  5. void makeSpeia(int height, int width, RGBTriple* image) {
  6. }
  7. void rowReverse(int height, int width, RGBTriple* image) {
  8. }
  9. void makeBlur(int height, int width, RGBTriple* image) {
  10. }

(1).灰度化

原理:取出每个像素的RGB值,三个值求平均数,再将平均数分别赋值给RGB。

  1. void makeGray(int height, int width, RGBTriple* image) {
  2. for (int i = 0;i < height;i++) {
  3. for (int j = 0;j < width;j++) {
  4. int aver = ((image+(i * width + j))->blue+ (image + (i * width + j))->green+ (image + (i * width + j))->red)/3;
  5. (image + (i * width + j))->blue = (image + (i * width + j))->green = (image + (i * width + j))->red = aver;
  6. }
  7. }
  8. }

这个函数的实现相对容易。

(2).棕色滤镜效果

原理:公式

新Red=原Red*0.393+原Green*0.769+原Blue*0.189;

新Green=原Red*0.349+原Green*0.686+原Blue*0.168;

新Blue=原Red*0.272+原Green*0.534+原Blue*0.131;

这里,隐藏着一个很大的bug

大家发现,如果对一个白色像素进行操作(255,255,255),会得到(344,306,238),三个颜色值有两个都已经溢出!

因此需要这么一个逻辑:当检测到算出的颜色值溢出时,将它重新设置成255.

  1. void makeSpeia(int height, int width, RGBTriple* image) {
  2. int ured = 0 , ugreen=0 , ublue=0;
  3. for (int i = 0;i < height;i++) {
  4. for (int j = 0;j < width;j++) {
  5. ured = ((image + (i * width + j))->red) * 0.393 + ((image + (i * width + j))->green) * 0.769 + ((image + (i * width + j))->blue) * 0.189;
  6. ugreen = ((image + (i * width + j))->red) * 0.349 + ((image + (i * width + j))->green) * 0.686 + ((image + (i * width + j))->blue) * 0.168;
  7. ublue = ((image + (i * width + j))->red) * 0.272 + ((image + (i * width + j))->green) * 0.534 + ((image + (i * width + j))->blue) * 0.131;
  8. if (ured > 255) {
  9. ured = 255;
  10. }
  11. if (ugreen > 255) {
  12. ugreen = 255;
  13. }
  14. if (ublue > 255) {
  15. ublue = 255;
  16. }
  17. (image + (i * width + j))->red = ured;
  18. (image + (i * width + j))->green = ugreen;
  19. (image + (i * width + j))->blue = ublue;
  20. ured = ugreen = ublue = 0;
  21. }
  22. }
  23. }

(3).水平翻转

原理:外层for遍历每一行,内层for在行的开头和结尾定义两个计数变量,这两个变量同时向行中心移动,直至它们相等或“错过”,在每次移动时,交换两个变量对应像素的R、G、B三个值。

  1. void rowReverse(int height, int width, RGBTriple* image) {
  2. int ured = 0, ugreen = 0, ublue = 0;
  3. for (int i = 0;i < height;i++) {
  4. for (int j = 0,k=width-1;j < k;j++,k--) {
  5. ured = (image + (i * width + j))->red;
  6. (image + (i * width + j))->red = (image + (i * width + k))->red;
  7. (image + (i * width + k))->red = ured;
  8. ugreen = (image + (i * width + j))->green;
  9. (image + (i * width + j))->green = (image + (i * width + k))->green;
  10. (image + (i * width + k))->green = ugreen;
  11. ublue = (image + (i * width + j))->blue;
  12. (image + (i * width + j))->blue = (image + (i * width + k))->blue;
  13. (image + (i * width + k))->blue = ublue;
  14. }
  15. }
  16. }

(4).模糊

原理:

 我们对每一个像素进行如此操作之后,每一个像素都对应了自己的一份崭新的RGB颜色值。得到所有新颜色值之后,用它们依次覆盖掉原来的图片数据。

最终处理后的图片如下:

实现上的几个难点:

  • 必须用一个临时数组(肯定也在堆中,不然放不下)把新的颜色值存起来,等所有的像素都运算完了,再把临时数组中的值统一赋给原数组。否则,新的颜色值的输入会影响临近几个像素的运算,产生“污染”。统一赋值结束后,记得把临时数组delete掉。
  • 如何确定3*3网格到底盖住了几个像素? 先将目光放在中心格子的左上角那个格子,即image[i-1][j-1],然后看看 i-1 和 j-1 是否越界。本质上说,这两层for循环扫描了包括了中心格子在内的9个格子,并统计了有效(在图片范围内)的格子数。

以下为完整代码:

  1. void makeBlur(int height, int width, RGBTriple* image) {
  2. RGBTriple* temp = new RGBTriple[height * width]();
  3. int ured = 0, ugreen=0, ublue = 0;
  4. for (int i = 0;i < height;i++) {
  5. for (int j = 0;j < width;j++) {
  6. //计算实际覆盖的像素数量
  7. int total = 0;
  8. for (int y = i - 1, myCount = 0;myCount < 3;y++,myCount++) {
  9. if (y >= 0 && y < height) {
  10. for (int x = j - 1, myCount1 = 0;myCount1 < 3;x++, myCount1++) {
  11. if (x >= 0 && x < width) {
  12. ured+= (image + (y * width + x))->red;
  13. ugreen += (image + (y * width + x))->green;
  14. ublue += (image + (y * width + x))->blue;
  15. total++;
  16. }
  17. }
  18. }
  19. }
  20. (temp + (i * width + j))->red=(ured/total);
  21. (temp + (i * width + j))->green = (ugreen / total);
  22. (temp + (i * width + j))->blue = (ublue / total);
  23. ured = ugreen = ublue = 0;
  24. }
  25. }
  26. for (int i = 0;i < height;i++) {
  27. for (int j = 0;j < width;j++) {
  28. (image + (i * width + j))->blue = (temp + (i * width + j))->blue;
  29. (image + (i * width + j))->green = (temp + (i * width + j))->green;
  30. (image + (i * width + j))->red = (temp + (i * width + j))->red;
  31. }
  32. }
  33. delete[] temp;
  34. }

7.main函数

我们最后回到一开始的命令行窗口这里:

可以看出,main函数接收了三个参数,分别为:对图片的修饰方法、原文件名、新文件名

命令行传参

  1. int main(int argc,char* argv[]){
  2. //do something
  3. }

argc:参数总个数

argv:字符串数组,各个元素之间用任意个空白字符隔开

argv[0]是不能被直接读取的,真正的参数从argv[1]开始依次向后存放。

具体实现

  1. #include <iostream>
  2. #include<cstring>
  3. #include"bmpFile.h"
  4. #include"helpers.h"
  5. using namespace std;
  6. int main(int argc,const char* argv[])
  7. {
  8. for (int i = 1;i < argc;i++) {
  9. cout << argv[i]<<" ";
  10. }
  11. bmp mybmp;
  12. mybmp.readPic(argv[2]);
  13. if (!strcmp(argv[1],"-g")) {
  14. mybmp.writePic(makeGray,argv[3]);
  15. }
  16. else if (!strcmp(argv[1], "-s")) {
  17. mybmp.writePic(makeSpeia, argv[3]);
  18. }
  19. else if (!strcmp(argv[1], "-b")) {
  20. mybmp.writePic(makeBlur, argv[3]);
  21. }
  22. else if (!strcmp(argv[1], "-r")) {
  23. mybmp.writePic(rowReverse, argv[3]);
  24. }
  25. else {
  26. cout << "failed to write!" << endl;
  27. }
  28. return 0;
  29. }

几个注意点:

  • 因为main.cpp中同时包含了helpers.h和bmpFile.h,而它们俩都包含了RGBTriple.h,所以,必须在RGBTriple.h中加上#pragma once,否则链接会出错;
  • 不要用==去判断两个字符串是否相等,因为这只会比较地址值,这里必须用strcmp()函数。

小结:

哈,终于写完了!这个案例可以帮我们回顾不少学过的知识点,比如指针的运算、文件流的操作、函数指针、命令行传参等,也帮助我了解了不少新知识,如bmp文件的格式和内部存储方式、内存对齐、fstream的read()和write()方法等。

加油,代码人!

部分参考自:

#Pragma Pack(n)与内存分配 pragma pack(push,1)与#pragma pack(1)的区别_Wanda && Aidem -CSDN博客

BMP格式详解_Tut-CSDN博客_bmp格式

BMP文件格式详解(BMP file format)_mjiansun的专栏-CSDN博客_bmp文件格式

c++ 24位bmp格式分析相关推荐

  1. 纯C++实现24位bmp格式图片的读取和修饰

    问题:现有一张bmp图片,要求将它读取到程序中并进行灰度化.水平翻转.模糊.茶色滤镜四种效果的一种,并输出新图片,如下所示: 命令行输入: 其中: 参数1:-b/g/s/r,先后表示blur(模糊), ...

  2. Java_最不重要位替换(LSB)基于24位BMP图片

    隐写术的一个简单示例 向BMP图片中隐藏一段文字并保存,从保存的图片中提取文字. 原理:把需要隐藏的文本信息转换成二进制字符流,再将其拆分成一个个的0和1,隐藏在像素数据(RGB字节)中,因对RGB的 ...

  3. c语言读取24位BMP文件并实现翻转90度、180度、270度

    BMP图片格式 BMP图片,是Bitmap(位图)的简称,它是windows显示图片的基本格式.在windows下,任何格式的图片文件(包括视频播放)都要转化为位图才能显示出来.各种格式的图片文件也都 ...

  4. c语言读取24位bmp图像,[原创]在TC下显示24位真彩色BMP位图

    [原创]在TC下显示24位真彩色BMP位图 在TC下显示24位BMP 虽然在TC显示24位图像上的速度远远比不上256色的速度快,但是真彩色色彩带给我们的视觉上的冲击是256色远远不能达到的.我们今天 ...

  5. 【学习笔记】简易的24位BMP图片转换成灰度图片

    简易的24位BMP图片转换成灰度图片的C语言实现 使用C语言实现的一个简易的24位BMP图片转换成灰度图片的程序.需要先准备一张24位的BMP图片. 说明 RGB图片转换成灰度图片主要是使用这个公式: ...

  6. opengl读取24位BMP文件为纹理并处理黑色背景为透明

    原理: 直接用BITMAP数据而未用到AUX库和windows  LoadImage API 24位BItMap文件格式不细说,见度娘 格式要求:24位无压缩位图,必须是2^n*2^n大小 代码(C/ ...

  7. c++读取8位和24位BMP位图数据 俺的作业

    家人萌 我因为这个作业爆炸了好多天...所以我想发一下 菜鸡一个 别骂别骂 欢迎指正  关于这个作业要先了解一下这些登西... 1)BMP 位图的结构 1.BMP文件头(14字节) ,文件的第0字节到 ...

  8. axure怎样24位bmp输出_【白皮书】使用24位设备进行基础应变测量

    使用24位设备进行基础应变测量 简介 应变片是一种广泛应用于物理特性测试测量的传感设备,其受到拉伸或压缩时,会改变输出端的电阻值.由于这种特性,将应变片固定在固体材料表面,对该材料施加压力或张力时,可 ...

  9. axure怎样24位bmp输出_平衡(非平衡)输入输出的无源变压器前级放大器

    做这台前级,是因为手中有朋友的一对全新三角唛的OPT W 5866信号变压器,而他又从日本拍回了多套音频系统,琢磨着一台高素质又简单的前级放大器便放在了朋友的心头. W 5866变压器,严格来讲,属于 ...

最新文章

  1. 模型神器组合,yyds!
  2. Spring Boot 中的 @EnableAutoConfiguration 是如何处理的?
  3. 1.arm的linux系统搭建
  4. Java1.5语言新特性简单总结
  5. eclipse如何导出WAR包
  6. 解决高并发的问题python_python ---解决高并发超卖问题
  7. 为什么java容器能在for each中遍历(Map除外)
  8. 漫画:什么是Base64算法
  9. 服务器小程序servlet的使用
  10. 对于以太坊的Solidity语言介绍
  11. 信息学奥赛一本通(1028:字符菱形)
  12. 微软删除最大的公开人脸识别数据集,只因员工离职?!
  13. 用ClusterSSH管理多台Linux服务器(2)
  14. Github/github 初始化教程
  15. Facebook更名Meta,扎克伯格押注元宇宙
  16. python课程结课感悟_关于python课程的感想
  17. 微淘客推广技巧,教你如何用微信公众号淘客引流技巧
  18. python爬取百度百科获取中国高校信息
  19. 腾讯多媒体实验室:基于三维卷积神经网络的全参考视频质量评估算法
  20. 你的身份证,到底绑定了多少微信账号?

热门文章

  1. Conv2Former: A Simple Transformer-Style ConvNet for Visual Recognition
  2. 基于神经网络的目标检测论文之目标检测方法:改进的SSD目标检测算法
  3. oracle创建表空间及位置错误
  4. springboot mongodb 脱敏数据的明文查询
  5. excle 拆分合并的单元格并填充数据
  6. 微博爬虫 python
  7. pytest文档46-关于https请求警告问题(InsecureRequestWarning: Unverified HTTPS request is being made)
  8. catboost和xgboost_Catboost:超越Lightgbm和XGBoost的又一个boost算法神器
  9. 【网络】叶脊(Spine-Leaf)网络拓扑下全三层网络设计与实践(一) - 叶脊网络架构简介
  10. Linux内存管理(四十六):内核OOM机制详解