暑假期间,导师布置了一个将24位BMP图像的翻转90°和180°的小任务,经过经典面向百度编程,学习了一些bmp格式的知识,主要参考以下几篇文章。
知乎一位大佬的详解:C++实现将24位真彩BMP文件顺时针旋转90度,补足字节部分是精华
bmp24位位图格式总结:BMP24位位图格式总结
主体代码部分还是知乎大佬那篇文章,我自己在vs2019上运行时debug了一遍,添加了旋转函数rotation1,使其能够旋转180°(这部分搞清楚bmp图像的存储格式,其实是可以旋转任意较好的角度,找到新指针和旧指针对应的像素点位置即可).
一、BMP文件的组成
一个BMP文件由四部分构成:文件头、信息头、颜色表和图像信息区。文件头和信息头储存一些关于图像的信息,颜色表用来生成调色板,图像信息区则存储像素信息。这些信息依次存储在BMP文件中,要读取他们时我们只需依次将其读取即可。而在信息头中,我们主要用到的就是
biWidth(位图的宽度,以像素为单位);
biHeight(位图的高度,以像素为单位);
biBitCount(每个像素所需位数),biBitCount是多少这个图就是多少位图,我们处理的24位图的biBitCount就等于24。
其他的信息在图片旋转操作中不会改变,我们也不去管他们,只用依次将其从原文件中读取,再依次存入新文件就好了。
而在24位真彩图中,没有颜色表信息,可以不加,但是如果对灰度图像进行处理,就需要添加颜色表信息,且灰度值用rgb三通道数据计算。
二、BMP文件存储格式
BMP文件在存储像素信息时,和平常jpeg之类的格式不同,不是从左上到右下存储,它可以认为是以一个一维数组存储,从图片的左下角开始,先填满最下面一行,再填倒数第二行,一直填到最上面一行。同时,每一行存储像素信息的字节数必须是4的倍数,不够就填0补上剩下的。
例如:宽度为957个像素,24位图每个像素占3个字节,一行所需的字节数应该是 957x3=2871 个字节,但是实际上一行的字节为2872个(4的倍数),多出来的这一个字节就填上0。
三、关键函数
若未补上0,可能导致旋转出错,为了处理这个问题,编写一个字节补足函数:
int getDiff(Info & info)
{
int DataSizePerline = (info.biWidth * info.biBitCount+31) / 8;
DataSizePerline -= DataSizePerline % 4;
return DataSizePerline - info.biWidth * info.biBitCount / 8;
}
然后读取像素信息:
DATA *imgdata=new DATA[size];
int diff = getDiff(srcInfo);
//读取原图片像素信息
for (int i=0;i<h;i++){
fread((char*)imgdata + i * w * 3, 3, w, p);
fseek(p, diff, SEEK_CUR);
}
其中用到了(char*)来转换指针类型,这样可以将指针移动特定的字节数(char*指针+1则移动一个字节)。读取完一行的像素之后,用fseek()函数来跳过这一行补零的那些字节,再给imgdata指针加上一行的“有内容字节数”,继续读下一行。
旋转函数:
(1)旋转90°函数
void rotation(const DATA* src){
int newH = w;
int newW = h;//图片旋转90度之后宽度、高度互换
int newSize = newH * newW;
FILE *p;
p=fopen("dest.bmp","wb");
Header newHead = srcHead;
Info newInfo = srcInfo;
//修改旋转后图片的尺寸、宽度和高度
newHead.bfSize = (DWORD)(newHead.bfSize);
newInfo.biHeight = (DWORD)newH;
newInfo.biWidth = (DWORD)newW;
int newdiff = getDiff(newInfo);
newInfo.biSizeImage = (DWORD)((newInfo.biWidth * 3 + newdiff) * newInfo.biHeight);
//将种类、文件头、信息头写入新bmp文件
fwrite(&bfType,1,sizeof(WORD),p);
fwrite(&newHead,1,sizeof(Header),p);
fwrite(&newInfo,1,sizeof(Info),p);
}
旋转90°时,长宽互换
用指针代替新像素数组,把原数组像素信息转移到新数组中
for (int i=0; i<newH; i++){
for (int j=0; j<newW; j++){
*(target + i * newW + j) = *(src + j * w + newH - i - 1)
}
}
原数组中(i,j)位置的像素换到了(j,newH-i-1)位置
现在将target所存储的像素信息写入dest.bmp文件中:
for (int i=0; i<newH; i++){
fwrite((char*)target + i * newW * 3, 3, newW, p);//写入“有内容”的字节
fseek(p, newdiff, SEEK_CUR);//跳过补零的字节
}
(2)旋转180°函数
旋转180°时,长宽不变
int newH = h;
int newW = w;//图片旋转180度之后宽高不变
int newSize = newH * newW;
用指针代替新像素数组,把原数组像素信息转移到新数组中
for (int i = 0; i < newH; i++) {
for (int j = 0; j < newW; j++) {
*(target + i * newW + j) = *(src + (h-i-1) * w +w-j-1);
}
}
原数组中(i,j)位置的像素换到了(newW-j-1,newH-i-1)位置
将target所存储的像素信息写入dest.bmp文件中:
for (int i = 0; i < newH; i++) {
fwrite((char*)target + i * newW * 3, 3, newW, p);
fseek(p, newdiff, SEEK_CUR);
}
最后释放内存。
四、完整代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned int DWORD;
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4996)
//位图文件头定义;
struct Header {
DWORD bfSize;//文件大小
WORD bfReserved1;//保留字
WORD bfReserved2;//保留字
DWORD bfOffBits;//从文件头到实际位图数据的偏移字节数
};
//位图信息头定义
struct Info {
DWORD biSize;//信息头大小
DWORD biWidth;//图像宽度
DWORD biHeight;//图像高度
WORD biPlanes;//位平面数,必须为1
WORD biBitCount;//每像素位数
DWORD biCompression; //压缩类型
DWORD biSizeImage; //压缩图像大小字节数
DWORD biXPelsPerMeter; //水平分辨率
DWORD biYPelsPerMeter; //垂直分辨率
DWORD biClrUsed; //位图实际用到的色彩数
DWORD biClrImportant; //本位图中重要的色彩数
};
//像素信息
struct DATA
{
BYTE blue;
BYTE green;
BYTE red;
};
WORD bfType;//文件类型
Header srcHead;//原文件文件头
Info srcInfo;//原文件信息头
int h, w, size1;//原图像的高度、宽度和尺寸
int getDiff(Info& info)
{
int DataSizePerline = (info.biWidth * info.biBitCount + 31) / 8;
DataSizePerline -= DataSizePerline % 4;
return DataSizePerline - info.biWidth * info.biBitCount / 8;
}
//旋转90°函数
void rotation(const DATA* src) {
int newH = w;
int newW = h;//图片旋转90度之后宽度、高度互换
int newSize = newH * newW;
FILE* p;
p = fopen("dest.bmp", "wb");
Header newHead = srcHead;
Info newInfo = srcInfo;
//修改旋转后图片的尺寸、宽度和高度
newHead.bfSize = (DWORD)(newHead.bfSize);
newInfo.biHeight = (DWORD)newH;
newInfo.biWidth = (DWORD)newW;
int newdiff = getDiff(newInfo);
newInfo.biSizeImage = (DWORD)((newInfo.biWidth * 3 + newdiff) * newInfo.biHeight);
//将种类、文件头、信息头写入新bmp文件
fwrite(&bfType, 1, sizeof(WORD), p);
fwrite(&newHead, 1, sizeof(Header), p);
fwrite(&newInfo, 1, sizeof(Info), p);
DATA* target = new DATA[newSize];
for (int i = 0; i < newH; i++) {
for (int j = 0; j < newW; j++) {
*(target + i * newW + j) = *(src + j * w + newH - i - 1);
}
}
for (int i = 0; i < newH; i++) {
fwrite((char*)target + i * newW * 3, 3, newW, p);
fseek(p, newdiff, SEEK_CUR);
}
fclose(p);
delete[]target;
}
//旋转180°函数
void rotation1(const DATA* src) {
int newH = h;
int newW = w;//图片旋转180度之后宽高不变
int newSize = newH * newW;
FILE* p;
p = fopen("dest.bmp", "wb");
Header newHead = srcHead;
Info newInfo = srcInfo;
//修改旋转后图片的尺寸、宽度和高度
newHead.bfSize = (DWORD)(newHead.bfSize);
newInfo.biHeight = (DWORD)newH;
newInfo.biWidth = (DWORD)newW;
int newdiff = getDiff(newInfo);
newInfo.biSizeImage = (DWORD)((newInfo.biWidth * 3 + newdiff) * newInfo.biHeight);
//将种类、文件头、信息头写入新bmp文件
fwrite(&bfType, 1, sizeof(WORD), p);
fwrite(&newHead, 1, sizeof(Header), p);
fwrite(&newInfo, 1, sizeof(Info), p);
DATA* target = new DATA[newSize];
for (int i = 0; i < newH; i++) {
for (int j = 0; j < newW; j++) {
*(target + i * newW + j) = *(src + (h-i-1) * w +w-j-1);
}
}
for (int i = 0; i < newH; i++) {
fwrite((char*)target + i * newW * 3, 3, newW, p);
fseek(p, newdiff, SEEK_CUR);
}
fclose(p);
delete[]target;
}
int main() {
FILE* p;
p = fopen("bmp_24.bmp", "rb");
if (p != NULL) {
//先读取文件类型
fread(&bfType, 1, sizeof(WORD), p);
//读取bmp文件的文件头和信息头
fread(&srcHead, 1, sizeof(Header), p);
fread(&srcInfo, 1, sizeof(Info), p);
h = srcInfo.biHeight;
w = srcInfo.biWidth;
size1 = w * h;
DATA* imgdata = new DATA[size1];
int diff = getDiff(srcInfo);
//读取原图片像素信息
for (int i = 0; i < h; i++) {
fread((char*)imgdata + i * w * 3, 3, w, p);
fseek(p, diff, SEEK_CUR);
}
fclose(p);
rotation1(imgdata);
delete[]imgdata;
}
else {
cout << "无法打开文件" << endl;
}
return 0;
}
五、一些debug过程
(1)报错errorC4996:添加代码
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4996)
(2)定义不明确
知乎大佬那篇文章有自己定义一个size,在vs运行时,会占用命名空间,所以我把size改成size1