在 OpenCV 中,直接使用 imwrite
保存二值图像为 BMP 时,默认会将其存储为 8 位灰度图像(每个像素占用 1 字节)。要实现 1 位(位深度为1)的 BMP 二值图像,需要手动处理二进制数据并直接构造符合 BMP 文件格式规范的二进制文件。以下是分步解决方案:
步骤 1:将 OpenCV Mat 转换为二值数据
确保输入图像为单通道(CV_8UC1
),且像素值仅为 0(黑) 或 255(白):
cv::Mat binaryMat = ...; // 输入的二值矩阵(0和255)
// 转换为 0和1 的二值数据(按位存储)
cv::Mat bitMat;
binaryMat.convertTo(bitMat, CV_8UC1); // 确保数据类型为单通道
bitMat = (bitMat == 255); // 将255映射为1,0保持为0
步骤 2:构造 BMP 文件结构
BMP 文件格式包含以下关键部分(按顺序写入):
- BMP文件头(BITMAPFILEHEADER)
- BMP信息头(BITMAPINFOHEADER)
- 调色板(Palette,适用于1位图像)
- 位图数据(每像素1bit,行对齐到4字节)
(1) 定义 BMP 头结构
#pragma pack(push, 1) // 禁用内存对齐
struct BITMAPFILEHEADER {
uint16_t bfType; // 文件类型,必须为 "BM" (0x4D42)
uint32_t bfSize; // 文件总大小
uint16_t bfReserved1; // 保留字段,设为0
uint16_t bfReserved2; // 保留字段,设为0
uint32_t bfOffBits; // 数据偏移量(头+调色板)
};
struct BITMAPINFOHEADER {
uint32_t biSize; // 信息头大小(40字节)
int32_t biWidth; // 图像宽度(像素)
int32_t biHeight; // 图像高度(像素)
uint16_t biPlanes; // 颜色平面数,必须为1
uint16_t biBitCount; // 每像素位数(1表示1位)
uint32_t biCompression; // 压缩方式(0表示不压缩)
uint32_t biSizeImage; // 图像数据大小(行对齐后的字节数)
int32_t biXPelsPerMeter;
int32_t biYPelsPerMeter;
uint32_t biClrUsed;
uint32_t biClrImportant;
};
#pragma pack(pop)
(2) 调色板(Palette)
1位 BMP 需要定义 2 个调色板项(对应黑、白):
uint8_t palette[8] = {
0,0,0,0, // 颜色0:BGR黑 (00 00 00)
255,255,255,0 // 颜色1:BGR白 (FF FF FF)
};
步骤 3:计算行对齐和图像数据大小
BMP 要求每行数据对齐到 4 字节:
int width = binaryMat.cols;
int height = binaryMat.rows;
// 每行像素所需的字节数(按1位存储)
int rowSize = (width + 31) / 32 * 4; // (位宽对齐到32bits,再转为字节数并4字节对齐)
int dataSize = rowSize * height;
步骤 4:生成 BMP 二进制数据
将二值数据按行打包为每像素1位:
std::vector<uint8_t> bitmapData(dataSize, 0); // 初始化全0
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// 获取二值数据(0或1)
uint8_t bit = bitMat.at<uint8_t>(height - y - 1, x); // BMP行存储是自下而上的
// 计算目标位置的字节和位偏移
int byteIdx = y * rowSize + x / 8;
int bitIdx = 7 - (x % 8); // BMP位顺序是高位在前
// 设置对应位
if (bit) {
bitmapData[byteIdx] |= (1 << bitIdx);
}
}
}
步骤 5:填充 BMP 头信息
BITMAPFILEHEADER bmfh = {
0x4D42, // bfType: 'BM'
sizeof(BITMAPFILEHEADER) +
sizeof(BITMAPINFOHEADER) +
sizeof(palette) + dataSize, // bfSize: 总文件大小
0, // 保留字段
0,
sizeof(BITMAPFILEHEADER) +
sizeof(BITMAPINFOHEADER) +
sizeof(palette) // bfOffBits: 数据偏移量
};
BITMAPINFOHEADER bmih = {
sizeof(BITMAPINFOHEADER), // biSize
width, // biWidth
height, // biHeight
1, // biPlanes
1, // biBitCount(1位)
0, // biCompression(不压缩)
dataSize, // biSizeImage(数据大小)
0, // biXPelsPerMeter
0, // biYPelsPerMeter
0, // biClrUsed
0
};
步骤 6:写入文件
将头、调色板、位图数据写入文件:
std::ofstream outFile("output_1bit.bmp", std::ios::binary");
outFile.write((char*)&bmfh, sizeof(BITMAPFILEHEADER));
outFile.write((char*)&bmih, sizeof(BITMAPINFOHEADER));
outFile.write((char*)palette, sizeof(palette));
outFile.write((char*)bitmapData.data(), bitmapData.size());
outFile.close();
完整代码示例
#include <opencv2/opencv.hpp>
#include <fstream>
#include <vector>
#pragma pack(push, 1)
struct BITMAPFILEHEADER { /* 同上 */ };
struct BITMAPINFOHEADER { /* 同上 */ };
#pragma pack(pop)
void save1bitBMP(const cv::Mat& binaryMat, const std::string& filename) {
int width = binaryMat.cols;
int height = binaryMat.rows;
// ---- 生成调色板 ----
uint8_t palette[8] = {0,0,0,0, 255,255,255,0};
// ---- 计算行对齐和数据大小 ----
int rowSize = ((width + 31) / 32) * 4;
int dataSize = rowSize * height;
// ---- 将 OpenCV Mat 转换为 1bit 数据 ----
cv::Mat bitMat;
binaryMat.convertTo(bitMat, CV_8UC1);
bitMat = (bitMat == 255);
std::vector<uint8_t> bitmapData(dataSize, 0);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
uint8_t bit = bitMat.at<uint8_t>(height - y - 1, x); // BMP存储行倒序
int byteIdx = y * rowSize + x / 8;
int bitIdx = 7 - (x % 8);
if (bit) {
bitmapData[byteIdx] |= (1 << bitIdx);
}
}
}
// ---- 填充 BMP 头信息 ----
BITMAPFILEHEADER bmfh = {/* 同步骤5 */};
BITMAPINFOHEADER bmih = {/* 同步骤5 */};
// ---- 写入文件 ----
std::ofstream outFile(filename, std::ios::binary");
outFile.write((char*)&bmfh, sizeof(BITMAPFILEHEADER));
outFile.write((char*)&bmih, sizeof(BITMAPINFOHEADER));
outFile.write((char*)palette, sizeof(palette));
outFile.write((char*)bitmapData.data(), bitmapData.size());
}
// 使用示例
int main() {
cv::Mat mat = cv::imread("input.png", cv::IMREAD_GRAYSCALE);
cv::threshold(mat, mat, 128, 255, cv::THRESH_BINARY);
save1bitBMP(mat, "output_1bit.bmp");
return 0;
}
关键注意事项
- BMP 行顺序:BMP 默认存储从下到上的行数据,需翻转行索引。
- 位操作:每个字节中,像素的位存储顺序是高位在前(左到右的第1个像素对应字节的最高位
bit7
)。 - 调色板定义:1位的 BMP 必须包含 2 种颜色,分别对应像素值的 0 和 1。
- 内存对齐:使用
#pragma pack(1)
确保结构中无填充字节。
通过此方法,可以将 OpenCV 的二值 Mat 保存为真正的 1 位 BMP 图像。