将24位BMP图像转换为8位BMP图像的实现方法
项目说明
周末想起一个项目需要使用agg与8bits的BMP进行绘制,因agg不支持8bits BMP进行绘制,故自己实现了一个基于agg+rgb88进行绘制,绘制结果转为8bits的BMP.
从24位BMP图像到8位BMP图像的转换过程。转换后的图像文件将只使用256种颜色,但仍保留了原始图像的基本信息。
主要原理:
- 读取24位BMP图像数据:首先从给定的24位BMP图像文件中读取像素数据。在BMP文件中,像素数据紧跟在文件头之后。
- 统计颜色及其数量:对于读取到的每个像素,统计其RGB颜色值及其出现的次数。这样就得到了所有不同颜色的数量统计。
- 创建8位颜色表:根据统计结果,选择数量最多的256种颜色作为8位BMP的颜色表。对颜色表按颜色数量进行排序,然后选择前256种颜色作为颜色表。
- 转换为8位索引:遍历原始图像的每个像素,将其RGB颜色值映射到颜色表中,得到对应的索引值。这样就将24位的颜色值转换为8位的索引值。
- 写入8位BMP图像文件:根据8位索引值和颜色表,将转换后的像素数据写入到新的8位BMP图像文件中。首先写入BMP文件头信息,然后写入颜色表,最后写入像素数据。
效果展示
使用 AGG 绘制的图像,并将其转换为 8 位 BMP 格式。由于博客不支持BMP文件上传,使用 PNG 格式代替的展示图。
input_24bit.bmp 是直接使用Windows绘图随手画的一个,保存为24bits的BMP,这个默认是bgr排序,所以中间做了两次颜色bgr互转rgb,agg需要rgb排序,bmp需要bgr排序,读写bmp是简单实现的,也可以通过第三方库STB Image进行读写.
ps :STB Image 不支持8bits的BMP的读写,所以自己实现了8bits的BMP读写.
原理:统计每个像素点rgb值的个数,选取数量最多的256个颜色值,作为8bits的颜色表,并且将原像素数据转通过该颜色表,进行重新计算新的index值,并将结果写入24/8bits的BMP中.
cnblog不支持bmp上传,暂时使用png代替.
简单将代码与效果展示.
bitmapconverter.cpp
#include <fstream>
#include <iostream>
#include <algorithm>
#include <limits>
#include "bitmapconverter.h"
std::unordered_map<RGBAColor, size_t> BitmapConverterTo8bits::count_colors(const std::vector<RGBAColor> &image) {
std::unordered_map<RGBAColor, size_t> color_counts;
for (const auto& color : image) {
color_counts[color]++;
}
return color_counts;
}
std::vector<RGBAColor> BitmapConverterTo8bits::select_palette(const std::unordered_map<RGBAColor, size_t> &color_counts) {
std::vector<RGBAColor> palette;
palette.reserve(256);
// Sort colors by frequency
std::vector<std::pair<RGBAColor, size_t>> sorted_colors(color_counts.begin(), color_counts.end());
std::sort(sorted_colors.begin(), sorted_colors.end(), [](const auto& a, const auto& b) {
return a.second > b.second;
});
// Select top 256 colors as palette
for (size_t i = 0; i < std::min(256ULL, sorted_colors.size()); ++i) {
palette.push_back(sorted_colors[i].first);
}
return palette;
}
double BitmapConverterTo8bits::color_distance_squared(const RGBAColor &c1, const RGBAColor &c2) {
double dr = c1.r - c2.r;
double dg = c1.g - c2.g;
double db = c1.b - c2.b;
return dr * dr + dg * dg + db * db;
}
void BitmapConverterTo8bits::convert_24bits_to_8bits(std::vector<unsigned char> &imageData)
{
std::vector<RGBAColor> colors = convert_24bit_color(imageData);
// Count color frequencies
auto color_counts = count_colors(colors);
// Select top 256 colors as palette
m_palette = select_palette(color_counts);
// Convert 24-bit color image to 8-bit color image
m_indexed_image = convert_to_8bit(colors);
}
void BitmapConverterTo8bits::convert_16bits_to_8bits(std::vector<unsigned char> &imageData)
{
std::vector<RGBAColor> colors;
if(m_is_rgb1555){
colors = convert_1555_color(imageData);
}else{
colors = convert_565_color(imageData);
}
// Count color frequencies
auto color_counts = count_colors(colors);
// Select top 256 colors as palette
m_palette = select_palette(color_counts);
// Convert 24-bit color image to 8-bit color image
m_indexed_image = convert_to_8bit(colors);
}
void BitmapConverterTo8bits::convert_to_8bits(std::vector<unsigned char> &imageData)
{
if(m_channels == 3){
convert_24bits_to_8bits(imageData);
}
if(m_channels == 2){
convert_16bits_to_8bits(imageData);
}
}
std::vector<unsigned char> BitmapConverterTo8bits::load_bmp(const std::string &filename, bool is_rgb1555) {
std::vector<unsigned char> imageData;
bool ret = false;
do {
std::ifstream file(filename, std::ios::binary);
if (!file.is_open()) {
std::cerr << "Failed to open file." << std::endl;
break;
}
BitmapFileHeader header;
BitmapInfoHeader infoHeader;
// Read file header
file.read(reinterpret_cast<char*>(&header), sizeof(BitmapFileHeader));
if (header.bfType != 0x4D42) { // Check if it's a valid bitmap file
std::cerr << "File is not a valid bitmap." << std::endl;
break;
}
// Read info header
file.read(reinterpret_cast<char*>(&infoHeader), sizeof(BitmapInfoHeader));
m_width = infoHeader.biWidth;
m_height = infoHeader.biHeight;
m_channels = infoHeader.biBitCount / 8;
m_is_rgb1555 = is_rgb1555;
// Check if file size matches image data size
size_t expectedSize = m_width * m_height * m_channels;
if (header.bfSize - header.bfOffBits != expectedSize) {
std::cerr << "Invalid image data size." << std::endl;
std::cerr << header.bfSize<<":" << header.bfOffBits<<":" << expectedSize << std::endl;
std::cerr << header.bfSize<<":" << header.bfOffBits<<":" << expectedSize/3 << std::endl;
break;
}
// Skip bitmap file header and info header
file.seekg(header.bfOffBits, std::ios::beg);
// Read image data
imageData.resize(expectedSize);
file.read(reinterpret_cast<char*>(imageData.data()), expectedSize);
ret = true;
} while (false);
printf_info();
return imageData;
}
void BitmapConverterTo8bits::swap_bmp_for_agg(std::vector<unsigned char> &imageData)
{
size_t len = imageData.size();
for (size_t i = 0; i < len - 2; i += 3) {
std::swap(imageData[i], imageData[i + 2]);
}
}
void BitmapConverterTo8bits::swap_agg_for_bmp(std::vector<unsigned char> &imageData)
{
swap_bmp_for_agg(imageData);
}
bool BitmapConverterTo8bits::save_8bits_bmp(const std::string &filename) {
std::ofstream file(filename, std::ios::binary);
if (!file.is_open()) {
std::cerr << "Failed to open output file." << std::endl;
return false;
}
BitmapFileHeader fileHeader;
BitmapInfoHeader infoHeader;
// Fill file header information
fileHeader.bfSize = sizeof(BitmapFileHeader) + sizeof(BitmapInfoHeader) + m_palette.size() * sizeof(RGBAColor) + m_indexed_image.size();
fileHeader.bfOffBits = sizeof(BitmapFileHeader) + sizeof(BitmapInfoHeader) + m_palette.size() * sizeof(RGBAColor);
// Fill info header information
infoHeader.biSize = sizeof(BitmapInfoHeader);
infoHeader.biWidth = m_width;
infoHeader.biHeight = m_height;
infoHeader.biBitCount = 8; // 8-bit indexed color bitmap
infoHeader.biSizeImage = m_indexed_image.size();
infoHeader.biClrUsed = m_palette.size();
// Write file header and info header
file.write(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
file.write(reinterpret_cast<char*>(&infoHeader), sizeof(infoHeader));
for (const auto& color : m_palette) {
file.put(color.r);
file.put(color.g);
file.put(color.b);
file.put(0);
}
file.write(reinterpret_cast<const char*>(&m_indexed_image[0]), m_indexed_image.size());
std::cout << "Bitmap image saved successfully." << std::endl;
return true;
}
bool BitmapConverterTo8bits::save_24bits_bmp(const std::string &filename, std::vector<unsigned char> &imageData)
{
std::ofstream file(filename, std::ios::binary);
if (!file.is_open()) {
std::cerr << "Failed to open output file." << std::endl;
return false;
}
BitmapFileHeader fileHeader;
BitmapInfoHeader infoHeader;
// Fill file header information
fileHeader.bfSize = sizeof(BitmapFileHeader) + sizeof(BitmapInfoHeader) + imageData.size();
fileHeader.bfOffBits = sizeof(BitmapFileHeader) + sizeof(BitmapInfoHeader);
// Fill info header information
infoHeader.biSize = sizeof(BitmapInfoHeader);
infoHeader.biWidth = m_width;
infoHeader.biHeight = m_height;
// Write file header and info header
file.write(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
file.write(reinterpret_cast<char*>(&infoHeader), sizeof(infoHeader));
file.write(reinterpret_cast<const char*>(&imageData[0]), imageData.size());
std::cout << "Bitmap image saved successfully." << std::endl;
return true;
}
std::vector<unsigned char> BitmapConverterTo8bits::convert_to_8bit(const std::vector<RGBAColor> &image) {
std::vector<unsigned char> indexed_image(m_width * m_height);
for (uint32_t y = 0; y < m_height; ++y) {
for (uint32_t x = 0; x < m_width; ++x) {
const RGBAColor& color = image[y * m_width + x];
double min_distance = std::numeric_limits<double>::max();
uint8_t nearest_index = 0;
for (size_t i = 0; i < m_palette.size(); ++i) {
double dist = color_distance_squared(color, m_palette[i]);
if (dist < min_distance) {
min_distance = dist;
nearest_index = i;
}
}
indexed_image[y * m_width + x] = nearest_index;
}
}
return indexed_image;
}
std::vector<RGBAColor> BitmapConverterTo8bits::convert_24bit_color(std::vector<unsigned char> &imageData)
{
std::vector<RGBAColor> colors(m_width * m_height);
for (int i = 0; i < m_width * m_height; ++i) {
colors[i].r = imageData[i * 3];
colors[i].g = imageData[i * 3 + 1];
colors[i].b = imageData[i * 3 + 2];
}
return colors;
}
std::vector<RGBAColor> BitmapConverterTo8bits::convert_565_color(std::vector<unsigned char> &imageData)
{
std::vector<RGBAColor> colors(m_width * m_height);
for (int i = 0; i < m_width * m_height; i+=2) {
int16_t rgb565 = (imageData[i] << 8) | imageData[i + 1];
// Extract color channel values, using bitwise masks instead of multiplication operations.
colors[i / 2].r = (rgb565 >> 11) & 0x1F;
colors[i / 2].g = (rgb565 >> 5) & 0x3F;
colors[i / 2].b = rgb565 & 0x1F;
// Extend 5-bit color values to 8 bits.
colors[i / 2].r = (colors[i / 2].r << 3) | (colors[i / 2].r >> 2);
colors[i / 2].g = (colors[i / 2].g << 2) | (colors[i / 2].g >> 4);
colors[i / 2].b = (colors[i / 2].b << 3) | (colors[i / 2].b >> 2);
}
return colors;
}
std::vector<RGBAColor> BitmapConverterTo8bits::convert_1555_color(std::vector<unsigned char> &imageData)
{
std::vector<RGBAColor> colors(m_width * m_height);
for (int i = 0; i < m_width * m_height; i+=2) {
int16_t rgb1555 = (static_cast<int16_t>(imageData[i]) << 8) | imageData[i + 1];
colors[i].r = (rgb1555 >> 10) & 0x1F;
colors[i].g = (rgb1555 >> 5) & 0x1F;
colors[i].b = rgb1555 & 0x1F;
colors[i].a = (rgb1555 >> 15) ? 255 : 0;
}
return colors;
}
bitmapconverter.h
#ifndef BITMAPCONVERTER_H
#define BITMAPCONVERTER_H
#include <vector>
#include <unordered_map>
#include <string>
#include <iostream>
// Bitmap file header structure
#pragma pack(push, 1) // Ensure byte alignment
struct BitmapFileHeader {
uint16_t bfType = 0x4D42;
uint32_t bfSize = 0;
uint16_t bfReserved1 = 0;
uint16_t bfReserved2 = 0;
uint32_t bfOffBits = 0;
void printInfo() const {
std::cout << "Bitmap File Header:" << std::endl;
std::cout << " bfType: " << bfType << std::endl;
std::cout << " bfSize: " << bfSize << std::endl;
std::cout << " bfReserved1: " << bfReserved1 << std::endl;
std::cout << " bfReserved2: " << bfReserved2 << std::endl;
std::cout << " bfOffBits: " << bfOffBits << std::endl;
}
};
// Bitmap information header structure
struct BitmapInfoHeader {
uint32_t biSize = 40;
uint32_t biWidth = 0;
uint32_t biHeight = 0;
uint16_t biPlanes = 1;
uint16_t biBitCount = 24;
uint32_t biCompression = 0;
uint32_t biSizeImage = 0;
uint32_t biXPelsPerMeter = 0;
uint32_t biYPelsPerMeter = 0;
uint32_t biClrUsed = 0;
uint32_t biClrImportant = 0;
void printInfo() const {
std::cout << "Bitmap Info Header:" << std::endl;
std::cout << " biSize: " << biSize << std::endl;
std::cout << " biWidth: " << biWidth << std::endl;
std::cout << " biHeight: " << biHeight << std::endl;
std::cout << " biPlanes: " << biPlanes << std::endl;
std::cout << " biBitCount: " << biBitCount << std::endl;
std::cout << " biCompression: " << biCompression << std::endl;
std::cout << " biSizeImage: " << biSizeImage << std::endl;
std::cout << " biXPelsPerMeter: " << biXPelsPerMeter << std::endl;
std::cout << " biYPelsPerMeter: " << biYPelsPerMeter << std::endl;
std::cout << " biClrUsed: " << biClrUsed << std::endl;
std::cout << " biClrImportant: " << biClrImportant << std::endl;
}
};
#pragma pack(pop)
struct RGBAColor {
unsigned char r = 0;
unsigned char g = 0;
unsigned char b = 0;
unsigned char a = 255;
bool operator==(const RGBAColor& other) const {
return r == other.r && g == other.g && b == other.b && a == other.a;
}
};
namespace std {
template<> struct hash<RGBAColor> {
size_t operator()(const RGBAColor& color) const {
return hash<int>()(color.r) ^ hash<int>()(color.g) ^ hash<int>()(color.b) ^ hash<int>()(color.a);
}
};
}
class BitmapConverterTo8bits {
public:
uint32_t m_width,m_height,m_channels;
bool m_is_rgb1555 = false;
std::vector<RGBAColor> m_palette;
std::vector<unsigned char> m_indexed_image;
private:
std::unordered_map<RGBAColor, size_t> count_colors(const std::vector<RGBAColor>& image);
std::vector<RGBAColor> select_palette(const std::unordered_map<RGBAColor, size_t>& color_counts);
double color_distance_squared(const RGBAColor& c1, const RGBAColor& c2);
void convert_24bits_to_8bits(std::vector<unsigned char> &imageData);
void convert_16bits_to_8bits(std::vector<unsigned char> &imageData);
void printf_info(){
std::cout <<"m_width:" << m_width<<std::endl;
std::cout <<"m_height:"<< m_height<<std::endl;
std::cout <<"m_channels:"<< m_channels<<std::endl;
std::cout <<"m_is_rgb1555:"<< m_is_rgb1555<<std::endl;
}
public:
void convert_to_8bits(std::vector<unsigned char> &imageData);
bool save_8bits_bmp(const std::string& filename);
bool save_24bits_bmp(const std::string& filename,std::vector<unsigned char> &imageData);
std::vector<unsigned char> load_bmp(const std::string &filename, bool is_rgb1555 = false);
void swap_bmp_for_agg(std::vector<unsigned char> &imageData);
void swap_agg_for_bmp(std::vector<unsigned char> &imageData);
private:
std::vector<unsigned char> convert_to_8bit(const std::vector<RGBAColor>& image);
std::vector<RGBAColor> convert_24bit_color(std::vector<unsigned char> &imageData);
std::vector<RGBAColor> convert_565_color(std::vector<unsigned char> &imageData);
std::vector<RGBAColor> convert_1555_color(std::vector<unsigned char> &imageData);
};
#endif // BITMAPCONVERTER_H
main.cpp
#include <agg_pixfmt_rgb.h>
#include <agg_pixfmt_rgb_packed.h>
#include <agg_renderer_base.h>
#include <agg_renderer_scanline.h>
#include <agg_rasterizer_scanline_aa.h>
#include <agg_scanline_p.h>
#include <agg_scanline_u.h>
#include <agg_conv_stroke.h>
#include <agg_ellipse.h>
#include <agg_rendering_buffer.h>
#include "bitmapconverter.h"
int main() {
BitmapConverterTo8bits converter;
std::vector<unsigned char> imageData = converter.load_bmp("input_24bit.bmp") ;
// Load 24-bit color image data
if (imageData.empty()) {
return -1;
}
converter.swap_bmp_for_agg(imageData);
// 创建 AGG 渲染器和画布
using renderer_base = agg::renderer_base<agg::pixfmt_rgb24>;
agg::rendering_buffer rbuf;
rbuf.attach(imageData.data(), converter.m_width, converter.m_height, converter.m_width * converter.m_channels);
agg::pixfmt_rgb24 pixf(rbuf);
renderer_base rb(pixf);
// 设置绘制参数
agg::rasterizer_scanline_aa<> ras;
agg::scanline_p8 sl;
agg::renderer_scanline_aa_solid<renderer_base> ren(rb);
ras.auto_close(true);
// 绘制椭圆
agg::ellipse ellipse(converter.m_width / 2.0, converter.m_height / 2.0, converter.m_width / 3.0, converter.m_height / 4.0);
agg::conv_stroke<agg::ellipse> stroke(ellipse);
stroke.width(2.0);
ras.add_path(stroke);
ren.color(agg::rgba8(255, 0, 0)); // 设置椭圆颜色为红色
agg::render_scanlines(ras, sl, ren);
converter.swap_agg_for_bmp(imageData);
converter.convert_to_8bits(imageData); //=====>
// Save 8-bit color image as BMP file
if (!converter.save_8bits_bmp("agg_output_image8bits.bmp")) {
return -1;
}
// Save 24-bit color image as BMP file
if (!converter.save_24bits_bmp("agg_output_image24bits.bmp",imageData)) {
return -1;
}
return 0;
}