Fork me on GitHub

将24位BMP图像转换为8位BMP图像的实现方法

项目说明
周末想起一个项目需要使用agg与8bits的BMP进行绘制,因agg不支持8bits BMP进行绘制,故自己实现了一个基于agg+rgb88进行绘制,绘制结果转为8bits的BMP.
从24位BMP图像到8位BMP图像的转换过程。转换后的图像文件将只使用256种颜色,但仍保留了原始图像的基本信息。

主要原理:

  1. 读取24位BMP图像数据:首先从给定的24位BMP图像文件中读取像素数据。在BMP文件中,像素数据紧跟在文件头之后。
  2. 统计颜色及其数量:对于读取到的每个像素,统计其RGB颜色值及其出现的次数。这样就得到了所有不同颜色的数量统计。
  3. 创建8位颜色表:根据统计结果,选择数量最多的256种颜色作为8位BMP的颜色表。对颜色表按颜色数量进行排序,然后选择前256种颜色作为颜色表。
  4. 转换为8位索引:遍历原始图像的每个像素,将其RGB颜色值映射到颜色表中,得到对应的索引值。这样就将24位的颜色值转换为8位的索引值。
  5. 写入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代替.

agg_output_image8bits.bmp

简单将代码与效果展示.
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;
}

posted @ 2024-04-06 18:56  yzhu798  阅读(112)  评论(0编辑  收藏  举报