Чтение bmp в C++, версия 2

В предыдущей статье я совершенно проигнорировал тот факт, что формат BMP имеет множество версий (целых 7), что там присутствует компрессия, разное количество битов на пиксель и цветовые маски. Также, C++ там был только в названии статьи, код был написан на чистом C.

Здесь я постарался добавить поддержку большего количества форматов (конечно не все, но самые популярные) и использовать известные мне возможности C++.

Задача

Из bmp изображения записать в двумерный массив информацию о цвете каждого пикселя, с использованием только стандартных библиотек.

Немного о формате

Представленный ниже код несколько отличается от итогового. Сделано это для того, чтобы читателю было проще понять алгоритм работы программы.

Шапка

В первых 14 байтах располагается «шапка» файла. Перед любыми действиями, следует сверить формат открытого файла с необходимым нам, для этого нужно прочитать первые 2 байта и сравнить их с 0x4D42. Больше информация из «шапки» нам не понадобится.

Заголовок

После «шапки» идёт заголовок с информацией о bitmap файле. В первых 2 байтах находятся данные о длине этого заголовка, эту информацию также можно использовать для определения версии формата. Также нам понадобится информация о высоте и ширине изображения, количестве бит на один пиксель и цветовые маски.

Чтение файла

Читать файл нужно последовательно и побайтово. Для удобного осуществления этого подойдёт следующая шаблонная функция:

template <typename Type>
void read(std::ifstream &fp, Type &result, std::size_t size) {
    fp.read(reinterpret_cast<char*>(&result), size);
}
Отступ

Одной из особенностей bmp формата является то, что количество байт в строке обязательно должно быть кратно 4, недостающие пиксели добавляются в конец каждого пиксельного ряда. То есть, если наше изображение имеет 24 бита на пиксель и ширину 2 пикселя, то, так как 24 * 2 бита — это 6 байт (48 / 8 бит = 6 байт), полученный отступ будет равняться 2 байтам. Определить размер отступа можно следующим образом:

int padding = ((4 - (width * (bitCount / 8)) % 4) & 3;
Цветовая маска

Если открываемый файл имеет версию формата 2 или выше, то у него, возможно, установлена цветовая маска. Если это не так, то нужно установить маску цвета по умолчанию:

int colorsCount = bitCount >> 3;
if (colorsCount < 3) {
    colorsCount = 3;
}
 
int bitsOnColor = bitCount / colorsCount;
int maskValue = (1 << bitsOnColor) - 1;
 
if (redMask == 0 || greenMask == 0 || blueMask == 0) {
    redMask = maskValue << (bitsOnColor * 2);
    greenMask = maskValue << bitsOnColor;
    blueMask = maskValue;
}

Чтобы извлечь цвет по маске, нужно использовать логическое И (конъюнкция) и, после этого, сместить полученный результат вправо на количество бит равных количеству нулей справа от маски.

Функция извлечения битов будет выглядеть таким образом:

unsigned char bitextract(const unsigned int byte, const unsigned int mask) {
    if (mask == 0) {
        return 0;
    }
 
    // определение количества нулевых бит справа от маски
    int
        maskBufer = mask,
        maskPadding = 0;
 
    while (!(maskBufer & 1)) {
        maskBufer >>= 1;
        maskPadding++;
    }
 
    // применение маски и смещение
    return (byte & mask) >> maskPadding;
}

Код записи будет выглядеть таким образом:

unsigned int bufer;
read(fileStream, bufer, bitCount / 8);
 
rgbRed = bitextract(bufer, biRedMask);
rgbGreen = bitextract(bufer, biGreenMask);
rgbBlue = bitextract(bufer, biBlueMask);
rgbReserved = bitextract(bufer, biAlphaMask);

Код

Файл main.h
#ifndef MAIN_H_INCLUDED
#define MAIN_H_INCLUDED
 
// CIEXYZTRIPLE stuff
typedef int FXPT2DOT30;
 
typedef struct {
    FXPT2DOT30 ciexyzX;
    FXPT2DOT30 ciexyzY;
    FXPT2DOT30 ciexyzZ;
} CIEXYZ;
 
typedef struct {
    CIEXYZ  ciexyzRed; 
    CIEXYZ  ciexyzGreen; 
    CIEXYZ  ciexyzBlue; 
} CIEXYZTRIPLE;
 
// bitmap file header
typedef struct {
    unsigned short bfType;
    unsigned int   bfSize;
    unsigned short bfReserved1;
    unsigned short bfReserved2;
    unsigned int   bfOffBits;
} BITMAPFILEHEADER;
 
// bitmap info header
typedef struct {
    unsigned int   biSize;
    unsigned int   biWidth;
    unsigned int   biHeight;
    unsigned short biPlanes;
    unsigned short biBitCount;
    unsigned int   biCompression;
    unsigned int   biSizeImage;
    unsigned int   biXPelsPerMeter;
    unsigned int   biYPelsPerMeter;
    unsigned int   biClrUsed;
    unsigned int   biClrImportant;
    unsigned int   biRedMask;
    unsigned int   biGreenMask;
    unsigned int   biBlueMask;
    unsigned int   biAlphaMask;
    unsigned int   biCSType;
    CIEXYZTRIPLE   biEndpoints;
    unsigned int   biGammaRed;
    unsigned int   biGammaGreen;
    unsigned int   biGammaBlue;
    unsigned int   biIntent;
    unsigned int   biProfileData;
    unsigned int   biProfileSize;
    unsigned int   biReserved;
} BITMAPINFOHEADER;
 
// rgb quad
typedef struct {
    unsigned char  rgbBlue;
    unsigned char  rgbGreen;
    unsigned char  rgbRed;
    unsigned char  rgbReserved;
} RGBQUAD;
 
// read bytes
template <typename Type>
void read(std::ifstream &fp, Type &result, std::size_t size) {
    fp.read(reinterpret_cast<char*>(&result), size);
}
 
// bit extract
unsigned char bitextract(const unsigned int byte, const unsigned int mask);
 
#endif // MAIN_H_INCLUDEDs
Файл main.cpp
#include <iostream>
#include <fstream>
 
#include "main.h"
 
int main(int argc, char *argv[])
{
    if (argc < 2) {
        std::cout << "Usage: " << argv[0] << " file_name" << std::endl;
        return 0;
    }
 
    char *fileName = argv[1];
 
    // открываем файл
    std::ifstream fileStream(fileName, std::ifstream::binary);
    if (!fileStream) {
        std::cout << "Error opening file '" << fileName << "'." << std::endl;
        return 0;
    }
 
    // заголовк изображения
    BITMAPFILEHEADER fileHeader;
    read(fileStream, fileHeader.bfType, sizeof(fileHeader.bfType));
    read(fileStream, fileHeader.bfSize, sizeof(fileHeader.bfSize));
    read(fileStream, fileHeader.bfReserved1, sizeof(fileHeader.bfReserved1));
    read(fileStream, fileHeader.bfReserved2, sizeof(fileHeader.bfReserved2));
    read(fileStream, fileHeader.bfOffBits, sizeof(fileHeader.bfOffBits));
 
    if (fileHeader.bfType != 0x4D42) {
        std::cout << "Error: '" << fileName << "' is not BMP file." << std::endl;
        return 0;
    }
 
    // информация изображения
    BITMAPINFOHEADER fileInfoHeader;
    read(fileStream, fileInfoHeader.biSize, sizeof(fileInfoHeader.biSize));
 
    // bmp core
    if (fileInfoHeader.biSize >= 12) {
        read(fileStream, fileInfoHeader.biWidth, sizeof(fileInfoHeader.biWidth));
        read(fileStream, fileInfoHeader.biHeight, sizeof(fileInfoHeader.biHeight));
        read(fileStream, fileInfoHeader.biPlanes, sizeof(fileInfoHeader.biPlanes));
        read(fileStream, fileInfoHeader.biBitCount, sizeof(fileInfoHeader.biBitCount));
    }
 
    // получаем информацию о битности
    int colorsCount = fileInfoHeader.biBitCount >> 3;
    if (colorsCount < 3) {
        colorsCount = 3;
    }
 
    int bitsOnColor = fileInfoHeader.biBitCount / colorsCount;
    int maskValue = (1 << bitsOnColor) - 1;
 
    // bmp v1
    if (fileInfoHeader.biSize >= 40) {
        read(fileStream, fileInfoHeader.biCompression, sizeof(fileInfoHeader.biCompression));
        read(fileStream, fileInfoHeader.biSizeImage, sizeof(fileInfoHeader.biSizeImage));
        read(fileStream, fileInfoHeader.biXPelsPerMeter, sizeof(fileInfoHeader.biXPelsPerMeter));
        read(fileStream, fileInfoHeader.biYPelsPerMeter, sizeof(fileInfoHeader.biYPelsPerMeter));
        read(fileStream, fileInfoHeader.biClrUsed, sizeof(fileInfoHeader.biClrUsed));
        read(fileStream, fileInfoHeader.biClrImportant, sizeof(fileInfoHeader.biClrImportant));
    }
 
    // bmp v2
    fileInfoHeader.biRedMask = 0;
    fileInfoHeader.biGreenMask = 0;
    fileInfoHeader.biBlueMask = 0;
 
    if (fileInfoHeader.biSize >= 52) {
        read(fileStream, fileInfoHeader.biRedMask, sizeof(fileInfoHeader.biRedMask));
        read(fileStream, fileInfoHeader.biGreenMask, sizeof(fileInfoHeader.biGreenMask));
        read(fileStream, fileInfoHeader.biBlueMask, sizeof(fileInfoHeader.biBlueMask));
    }
 
    // если маска не задана, то ставим маску по умолчанию
    if (fileInfoHeader.biRedMask == 0 || fileInfoHeader.biGreenMask == 0 || fileInfoHeader.biBlueMask == 0) {
        fileInfoHeader.biRedMask = maskValue << (bitsOnColor * 2);
        fileInfoHeader.biGreenMask = maskValue << bitsOnColor;
        fileInfoHeader.biBlueMask = maskValue;
    }
 
    // bmp v3
    if (fileInfoHeader.biSize >= 56) {
        read(fileStream, fileInfoHeader.biAlphaMask, sizeof(fileInfoHeader.biAlphaMask));
    } else {
        fileInfoHeader.biAlphaMask = maskValue << (bitsOnColor * 3);
    }
 
    // bmp v4
    if (fileInfoHeader.biSize >= 108) {
        read(fileStream, fileInfoHeader.biCSType, sizeof(fileInfoHeader.biCSType));
        read(fileStream, fileInfoHeader.biEndpoints, sizeof(fileInfoHeader.biEndpoints));
        read(fileStream, fileInfoHeader.biGammaRed, sizeof(fileInfoHeader.biGammaRed));
        read(fileStream, fileInfoHeader.biGammaGreen, sizeof(fileInfoHeader.biGammaGreen));
        read(fileStream, fileInfoHeader.biGammaBlue, sizeof(fileInfoHeader.biGammaBlue));
    }
 
    // bmp v5
    if (fileInfoHeader.biSize >= 124) {
        read(fileStream, fileInfoHeader.biIntent, sizeof(fileInfoHeader.biIntent));
        read(fileStream, fileInfoHeader.biProfileData, sizeof(fileInfoHeader.biProfileData));
        read(fileStream, fileInfoHeader.biProfileSize, sizeof(fileInfoHeader.biProfileSize));
        read(fileStream, fileInfoHeader.biReserved, sizeof(fileInfoHeader.biReserved));
    }
 
    // проверка на поддерку этой версии формата
    if (fileInfoHeader.biSize != 12 && fileInfoHeader.biSize != 40 && fileInfoHeader.biSize != 52 &&
        fileInfoHeader.biSize != 56 && fileInfoHeader.biSize != 108 && fileInfoHeader.biSize != 124) {
        std::cout << "Error: Unsupported BMP format." << std::endl;
        return 0;
    }
 
    if (fileInfoHeader.biBitCount != 16 && fileInfoHeader.biBitCount != 24 && fileInfoHeader.biBitCount != 32) {
        std::cout << "Error: Unsupported BMP bit count." << std::endl;
        return 0;
    }
 
    if (fileInfoHeader.biCompression != 0 && fileInfoHeader.biCompression != 3) {
        std::cout << "Error: Unsupported BMP compression." << std::endl;
        return 0;
    }
 
    // rgb info
    RGBQUAD **rgbInfo = new RGBQUAD*[fileInfoHeader.biHeight];
 
    for (unsigned int i = 0; i < fileInfoHeader.biHeight; i++) {
        rgbInfo[i] = new RGBQUAD[fileInfoHeader.biWidth];
    }
 
    // определение размера отступа в конце каждой строки
    int linePadding = ((fileInfoHeader.biWidth * (fileInfoHeader.biBitCount / 8)) % 4) & 3;
 
    // чтение
    unsigned int bufer;
 
    for (unsigned int i = 0; i < fileInfoHeader.biHeight; i++) {
        for (unsigned int j = 0; j < fileInfoHeader.biWidth; j++) {
            read(fileStream, bufer, fileInfoHeader.biBitCount / 8);
 
            rgbInfo[i][j].rgbRed = bitextract(bufer, fileInfoHeader.biRedMask);
            rgbInfo[i][j].rgbGreen = bitextract(bufer, fileInfoHeader.biGreenMask);
            rgbInfo[i][j].rgbBlue = bitextract(bufer, fileInfoHeader.biBlueMask);
            rgbInfo[i][j].rgbReserved = bitextract(bufer, fileInfoHeader.biAlphaMask);
        }
        fileStream.seekg(linePadding, std::ios_base::cur);
    }
 
    // вывод
    for (unsigned int i = 0; i < fileInfoHeader.biHeight; i++) {
        for (unsigned int j = 0; j < fileInfoHeader.biWidth; j++) {
            std::cout << std::hex
                      << +rgbInfo[i][j].rgbRed << " "
                      << +rgbInfo[i][j].rgbGreen << " "
                      << +rgbInfo[i][j].rgbBlue << " "
                      << +rgbInfo[i][j].rgbReserved
                      << std::endl;
        }
        std::cout << std::endl;
    }
 
    return 1;
}
 
unsigned char bitextract(const unsigned int byte, const unsigned int mask) {
    if (mask == 0) {
        return 0;
    }
 
    // определение количества нулевых бит справа от маски
    int
        maskBufer = mask,
        maskPadding = 0;
 
    while (!(maskBufer & 1)) {
        maskBufer >>= 1;
        maskPadding++;
    }
 
    // применение маски и смещение
    return (byte & mask) >> maskPadding;
}

Полезные ссылки

  • Vit

    Быстро Вы отреагировали!!!!
    Спасибо за статью!

    В строке с » int linePadding » одна скобка забыта.

    И… программа выводит «Unsupported BMP bit count».
    Да, кстати, надо бы getchar() перед каждым return добавить, иначе не успеваешь читать эти сообщения об ошибках.

    Надо будет мне посерьёзней ей позаниматься.

    • Vit

      Ага,не потому-ли, что Ваша программа поддерживает только 16/24/32 бит. кодирование на пиксель, а я с 8-й пробую?…
      А что особенного в 8-битной версии bmp?

      Кстати, строки с пикселями заложенны в обратном порядке, т.е. 1-я строка в картинке соответствует нижней сторке в матрице.
      И последовательность пикселей идёт в порядке BGR. Это у всех bmp?

      • Ага,не потому-ли, что Ваша программа поддерживает только 16/24/32 бит. кодирование на пиксель, а я с 8-й пробую?…
        А что особенного в 8-битной версии bmp?

        Именно. 8 битная версия хранит таблицу используемых цветов, а в 8 битах кодирует его номер. Мне не хотелось с этим возиться, поэтому поддержку такого bmp (тем более довольно редкого) я не добавил.

        И последовательность пикселей идёт в порядке BGR. Это у всех bmp?

        У большинства. BGR может быть не всегда, всё зависит от маски.

    • В строке с » int linePadding » одна скобка забыта.

      Исправил, спасибо.

      И… программа выводит «Unsupported BMP bit count».

      Это значит, что этот файл не поддерживается. Я реализовал только 16, 24 и 32 битные изображения.

      Да, кстати, надо бы getchar() перед каждым return добавить, иначе не успеваешь читать эти сообщения об ошибках.

      Не испытываю подобных проблем, так как запускаю через терминал.

  • Vit

    Попробовал протестировать программку.
    Наделал в Paint 24-bit bmp картинок 4×4 пикселя.
    Пробую:
    все чистые цвета: красный/зелёный/синий, а так же белый и черный считывает и выдаёт без ошибок.
    смешанные цвета выдаёт с ошибками, н/р:
    в PAINT
    R / G / B
    голубой 0 / 162 / 232 выдаёт как 0 / 81 / 29
    зелёный 34 / 177 / 76 17 / 177 / 19
    красный 237/ 28 / 36 237 / 7 / 9

    где-то кажется ошибка…
    Или значения в Paint: цветной тон/насыщеность/светлость так же влияют на цифровые значения пикселей? Попробовал забить вывод программки в Paint, зелёный и красный еще как-то совпадают (может немного другой оттенок), но голубой превращается в зелённый…

    • Vit

      Страница сбросила формат текста, еще раз:
      ________в PAINT
      ________R / G / B
      голубой 0 / 162 / 232 выдаёт как 0 / 81 / 29
      зелёный 34 / 177 / 76 выдаёт как 17 / 177 / 19
      красный 237/ 28 / 36 выдаёт как 237 / 7 / 9

    • Спасибо! Я допустил ошибку, смещая цвет, который был извлечён по маске, до первого ненулевого бита, а это совершенно неверно. Ведь нужно смещать результат на столько бит вправо, сколько стоит нулей справа от маски. Код и статью обновил, теперь цвет определяется верно.

  • Russian Gamer
  • Russian Gamer
    • Используй компилятор C++, а не C. Например g++.

      • Russian Gamer

        Спасибо! удалость скомпилировать, теперь понять бы как с этим работать

  • Fabervox

    А как на счет записи?

    • read заменить на write :)
      Вот тут есть пример создания негатива из изображения: https://ziggi.org/bystryy-negativ-bmp-izobrazheniya-v-cpp/, нужно немного адаптировать под код из этой и будет готово.

      • Fabervox

        Заменить?? Это выглядит как реклама какая-то.
        «Вы сможете в 1 клик сделать свой [s]сайт[/s] декодер изображений, пройдите по ссылке»
        Вообще я хотел сделать с png, что-бы можно было налету, создавать на клиентской стороне изображения в MTA. При этом именно генерировать их, а не передавать по сети, т.к. если передавать то простых готовых вариантов уйма.

        • Смайлик в конце намекал на то, что это шутка, которая намекает на то, что запись почти ничем не отличается от чтения.
          Не вижу в своём сообщении ни намёка на рекламу, а ссылку, где создаётся негатив из bmp, я дал, этого достаточно для того, чтобы понять принципы.

  • Toxan Toxan

    «»То есть, если наше изображение имеет 24 бита на пиксель и ширину 2 пикселя, то, так как 24 * 3 бита — это 6 байт»»
    Тройка это опечатка?

    • Да, это опечатка. Спасибо, исправил.

Перейти к верхней панели