常见的图片格式:bmp,jpeg,png,gif,tiff
搜索并下载一张滑稽,用 windows 自带的画图软件打开,然后另存为某种 bmp 格式(如下图),这里代码实现了 24位 bmp 和 16色 bmp 格式图片的读取。
#ifndef BMPFORMAT_H
#define BMPFORMAT_H
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned int DWORD;
typedef long LONG;
/* 1. 位图文件头 */
typedef struct tagBITMAPFILEHEADER
{
//WORD bfType; // windows中为 BM (0x4d42)
DWORD bfSize; // 文件大小
WORD bfReserved1; // 保留,设置为 0
WORD bfReserved2; // 保留,设置为 0
DWORD bfOffBits; // 实际图像数据的偏移量
}BITMAPFILEHEADER;
/* 2. 位图信息头 */
typedef struct tagBITMAPINFOHEADER
{
DWORD biSize; // 该结构所占字节数,windows下为 40
LONG biWidth; // 图像宽度
LONG biHeight; // 图像高度
WORD biPlanes; // 位面数,1
WORD biBitCount; // 单像素位数
DWORD biCompression; // 压缩说明
DWORD biSizeImage; // 图像大小,如无压缩,可设置为 0
LONG biXPelsPerMerer;// 水平分辨率
LONG biYPelsPerMerer;// 垂直分辨率
DWORD biClrUsed; // 位图使用的颜色数, 0 表示使用了全部颜色
DWORD biClrImportant; // 重要颜色数目
}BITMAPINFOHEADER;
/* 3. 调色板 */
typedef struct tagRGBQUAD
{
BYTE rgbBlue; // 指定蓝色强度
BYTE rgbGreen; // 指定绿色强度
BYTE rgbRed; // 指定红色强度
BYTE rgbReserved; // 保留,设为 0
}RGBQUAD;
/* 4. 位图数据
* 扫描行由低向上存储,第一字节为左下角像素,最后一个字节为右上角像素
*/
#endif
这里注意将第一部分图片文件头结构体中 bfType 单独拎出来,是处于结构体需要对齐的考虑,并且注意读取时是按字节读取,且 pc 采用小端头存储,所以 bfType 读取出来后是 MB,而在一个字节内部,读取出来的顺序就是存储时的顺序,即高位对应前面的数据,低位对应后面的数据,这里在读取 16色 bmp 图片时有体现。
#include
#include
#include
#include
#include
#include "BmpFormat.h"
using namespace std;
using namespace cv;
/* 输出 bmp 图片头部信息 */
void bmpFileInfo(ifstream &fpbmp, int &Offset, int &rows, int &cols);
/* 将 bmp24bit 图片读入 Mat 中 */
void bmp24bitToMat(ifstream& fpbmp, Mat& bmp, int Offset);
/* 将 bmp16色 图片读入 Mat 中 */
void bmp16ToMat(ifstream& fpbmp, Mat& bmp, int Offset);
int main(int argc, char* argv[]) {
/* 打开 bmp 文件*/
ifstream fpbmp(".\\pic\\ppx16.bmp", ifstream::in | ifstream::binary);
fpbmp.seekg(0, fpbmp.end);
int length = fpbmp.tellg();
fpbmp.seekg(0, fpbmp.beg);
int Offset, rows, cols;
bmpFileInfo(fpbmp, Offset, rows, cols);
Mat bmp(rows, cols, CV_8UC3);
/* 24 位 bmp 图片 */
//bmp24bitToMat(fpbmp, bmp, Offset);
/* 16 色 bmp 图片 */
bmp16ToMat(fpbmp, bmp, Offset);
namedWindow("ppx", WINDOW_AUTOSIZE);
imshow("ppx", bmp);
waitKey(0);
destroyWindow("ppx");
return 0;
}
/* 输出 bmp 图片头部信息 */
void bmpFileInfo(ifstream &fpbmp, int& Offset, int& rows, int& cols) {
BITMAPFILEHEADER fh;
WORD bfType;
fpbmp.read((char *)&bfType, sizeof(WORD));
fpbmp.read((char *)&fh, sizeof(BITMAPFILEHEADER));
if (bfType != 'MB') {
cout << "It's not a bmp file." << endl;
exit(1);
}
cout << "****** tagBITMAPFILEHEADER info *******" << endl;
cout << " bfType :" << " BM" << endl;
cout << " bfSize : " << (fh.bfSize) << endl;
cout << " bfReserved1 : " << (fh.bfReserved1) << endl;
cout << " bfReserved2 : " << (fh.bfReserved2) << endl;
cout << " bfOffBits : " << (fh.bfOffBits) << endl;
cout << " 总字节偏移 :" << fpbmp.tellg() << endl << endl;
BITMAPINFOHEADER fih;
fpbmp.read((char*)&fih, sizeof(BITMAPINFOHEADER));
cout << "****** tagBITMAPINFOHEADER info *******" << endl;
cout << " 位图信息头字节数 : " << (fih.biSize) << endl;
cout << " 图像像素宽度 : " << (fih.biWidth) << endl;
cout << " 图像像素高度 : " << (fih.biHeight) << endl;
cout << " 位面数 : " << (fih.biPlanes) << endl;
cout << " 单像素位数 : " << (fih.biBitCount) << endl;
cout << " 压缩说明 : " << (fih.biCompression) << endl;
cout << " 图像大小 : " << (fih.biSizeImage) << endl;
cout << " 水平分辨率 : " << (fih.biXPelsPerMerer) << endl;
cout << " 垂直分辨率 : " << (fih.biYPelsPerMerer) << endl;
cout << " 位图使用的颜色数 : " << (fih.biClrUsed) << endl;
cout << " 重要颜色数目 : " << (fih.biClrImportant) << endl;
cout << " 总字节偏移 :" << fpbmp.tellg() << endl << endl;
Offset = fh.bfOffBits;
rows = fih.biHeight;
cols = fih.biWidth;
if (Offset != 54) {
RGBQUAD rgbPalette[16];
fpbmp.read((char*)&rgbPalette, 16 * sizeof(RGBQUAD));
cout << "****** tagRGBQUAD info *******" << endl;
cout << " (R, G, B, Alpha)" << endl;
for (int i = 0; i < 16; i++) {
cout << " No." << i << " " << "(" \
<< (rgbPalette[i].rgbRed + 0) << ", " \
<< (rgbPalette[i].rgbGreen + 0) << ", " \
<< (rgbPalette[i].rgbBlue + 0) << ", " \
<< (rgbPalette[i].rgbReserved + 0) << ")" << endl;
//if (i != 0 && i % 4 == 0) cout << endl;
}
cout << " 总字节偏移 :" << fpbmp.tellg() << endl;
}
}
/* 将 bmp24bit 图片读入 Mat 中 */
void bmp24bitToMat(ifstream& fpbmp, Mat& bmp, int Offset) {
fpbmp.seekg(Offset, fpbmp.beg);
for (int i = (bmp.rows-1); i >=0 ; i--) {
for (int j = 0; j < bmp.cols; j++) {
fpbmp.read((char*)&bmp.at<Vec3b>(i, j)[0], 1);
fpbmp.read((char*)&bmp.at<Vec3b>(i, j)[1], 1);
fpbmp.read((char*)&bmp.at<Vec3b>(i, j)[2], 1);
}
}
}
/* 将 bmp16色 图片读入 Mat 中 */
void bmp16ToMat(ifstream& fpbmp, Mat& bmp, int Offset) {
RGBQUAD rgbPalette[16];
fpbmp.seekg(54, fpbmp.beg);
fpbmp.read((char*)&rgbPalette, 16 * sizeof(RGBQUAD));
fpbmp.seekg(Offset, fpbmp.beg);
char tmp;
for (int i = (bmp.rows - 1); i >= 0; i--) {
for (int j = 0; j < bmp.cols; j+=2) {
fpbmp.read((char*)&tmp, 1);
bmp.at<Vec3b>(i, j)[0] = rgbPalette[tmp >> 4 & 0x0f].rgbBlue;
bmp.at<Vec3b>(i, j)[1] = rgbPalette[tmp >> 4 & 0x0f].rgbGreen;
bmp.at<Vec3b>(i, j)[2] = rgbPalette[tmp >> 4 & 0x0f].rgbRed;
bmp.at<Vec3b>(i, j+1)[0] = rgbPalette[tmp & 0x0f].rgbBlue;
bmp.at<Vec3b>(i, j+1)[1] = rgbPalette[tmp & 0x0f].rgbGreen;
bmp.at<Vec3b>(i, j+1)[2] = rgbPalette[tmp & 0x0f].rgbRed;
}
}
}
注意,opencv 和 bmp 图片的通道顺序都是反序,即 BGR。
因为是真彩色图片,这里文件头偏移固定为 54 字节,因为没有调色板,两个保留字为 0,总大小的计算为: 50 4 2 × 3 + 54 = 762102 504^{2}\times 3 + 54=762102 5042×3+54=762102,位面数固定为 1,压缩说明为 0,指未压缩,图像大小在无压缩情况下也为 0,位图使用的颜色数为 0,指所有颜色都有用到,共 2 24 2^{24} 224 种,重要颜色数为 0,表示都重要。
Alpha 是保留字节,设置为 0,总头部偏移: 54 + 4 × 16 = 118 54+4\times 16=118 54+4×16=118,图像大小: 252 × 504 × = 127008 252\times 504 \times =127008 252×504×=127008,这里因为只有 16 色,故一个字节刚好可以存储两个索引,如果是 256 色,则一个字节只能存储一个索引。
关于使用调色板的图片压缩,可以参考bmp图像压缩算法详细解析4。
BMP文件结构 ↩︎
BMP位图与调色板分析 ↩︎
BMP图像结构 ↩︎
bmp图像压缩算法详细解析 ↩︎