Как работают маски в BMP изображениях?
Я пытаюсь написать примитивные алгоритмы для чтения изображений различных форматов в массив байтов. После того, как получилось разобрать PPM и TGA рисунки, решил приступить к формату BMP. На данный момент я научился преобразовывать BMP изображение в массив RGB последовательностей в следующих случаях:
- В рисунке информация о конкретном пикселе кодируется при помощи четырех или восьми бит, и при этом в файле присутствует таблица цветов (Color Table).
- В рисунке информация о конкретном пикселе кодируется при помощи двадцати четырех бит, и при этом файл не содержит таблицу цветов. Это наиболее простой вариант.
Проблемы возникли с 32-битными BMP изображениями. Я генерирую BMP файлы при помощи графического редактора Paint dot NET. В окне с параметрами сохранения можно выбрать значение битности рисунка. Мое 32-битное изображение имеет размер 2*2 px и выглядит следующим образом:
Пиксель №1 имеет цвет RGB(11, 12, 13), пиксель №2 – RGB(51, 52, 53), пиксель №3 – RGB(101, 102, 103), пиксель №4 – RGB(151, 152, 153).
С помощью класса std::ifstream и его метода get() я вывел содержимое файла в консоль (сначала идет номер байта, затем содержимое байта с этим номером):
Тот же самый вывод, но текстом:
1) 66 2) 77 3) 166 4) 0 5) 0 6) 0 7) 0 8) 0 9) 0 10) 0 11) 150 12) 0 13) 0 14) 0 15) 124 16) 0 17) 0 18) 0 19) 2 20) 0 21) 0 22) 0 23) 2 24) 0 25) 0 26) 0 27) 1 28) 0 29) 32 30) 0 31) 3 32) 0 33) 0 34) 0 35) 0 36) 0 37) 0 38) 0 39) 196 40) 14 41) 0 42) 0 43) 196 44) 14 45) 0 46) 0 47) 0 48) 0 49) 0 50) 0 51) 0 52) 0 53) 0 54) 0 55) 0 56) 0 57) 255 58) 0 59) 0 60) 255
61) 0 62) 0 63) 255 64) 0 65) 0 66) 0 67) 0 68) 0 69) 0 70) 255 71) 32 72) 110 73) 105 74) 87 75) 0 76) 0 77) 0 78) 0 79) 0 80) 0 81) 0 82) 0 83) 0 84) 0 85) 0 86) 0 87) 0 88) 0 89) 0 90) 0 91) 0 92) 0 93) 0 94) 0 95) 0 96) 0 97) 0 98) 0 99) 0 100) 0 101) 0 102) 0 103) 0 104) 0 105) 0 106) 0 107) 0 108) 0 109) 0 110) 0 111) 0 112) 0 113) 0 114) 0 115) 0 116) 0 117) 0 118) 0 119) 0 120) 0 121) 0 122) 0 123) 0 124) 0 125) 0 126) 0 127) 0 128) 0 129) 0 130) 0 131) 0 132) 0 133) 0 134) 0 135) 0 136) 0 137) 0 138) 0 139) 255 140) 0 141) 0 142) 0 143) 0 144) 255 145) 0 146) 0 147) 0 148) 0 149) 255 150) 0 151) 103 152) 102 153) 101 154) 255 155) 153 156) 152 157) 151 158) 255 159) 13 160) 12 161) 11 162) 255 163) 53 164) 52 165) 51 166) 255
Поле, которое занимает с 15-го по 18-й байт (в зависимости от версии BMP называется bcSize/biSize/bV4Size/bV5Size) имеет значение 124, что соответствует версии BMP 5. Это говорит о том, что рисунок может содержать «битовые маски для извлечения значений каналов: интенсивность красного, зелёного, синего и значение альфа-канала» (источник: https://ru.wikipedia.org/wiki/BMP#BITMAPINFO). На наличие масок также указывает значение поля Compression (с 31-го по 34-й байт), так как оно равно числу 3. Поля, содержащие маски, занимают 4 байта и идут друг за другом начиная с 55-го байта. Судя по консольному выводу, в моем изображении маски имеют следующие значения (десятичная система счисления):
Маска красного цвета: 0 0 255 0 (с 55-го по 58-й байт)
Маска зеленого цвета: 0 255 0 0 (с 59-го по 62-й байт)
Маска синего цвета: 255 0 0 0 (с 63-го по 66-й байт)
Маска альфа-канала: 0 0 0 255 (с 67-го по 70-й байт)
Я не могу понять, как интерпретировать эти значения и применять их к цветам пикселей, которые расположены в конце файла. Если я буду игнорировать эти значения и извлеку цвет напрямую (из блока данных, который иногда называют Image Data или Pixel Data), то получу верные цвета. Должно быть, так происходит из-за того, что в графическом редакторе я не корректировал маски, и программа записала для них значения по умолчанию, которые не искажают цвет пикселей.
Если поправить значения масок вручную и перезаписать файл, то цвет каждого пикселя изменится. Например, я заменил маску красного цвета на 0 0 60 0 (десятичная система счисления), открыл изображение в графическом редакторе и получил такой результат:
Пиксель №1 теперь имеет цвет RGB(34, 12, 13), пиксель №2 – RGB(204, 52, 53), пиксель №3 – RGB(153, 102, 103), пиксель №4 – RGB(85, 152, 153).
Действительно, поменялось значение красного цвета у каждого пикселя. Наверное, редактор выполняет какие-то операции с бинарными представлениями чисел, но я не знаю, какие именно. В статьях о BMP, которые я видел, маски почему-то упоминаются как-то вскользь. Вопросы вызывает также блок данных, расположенный до Image Data. Он занимает с 139-го по 150-й байт файла (в моем случае) и, согласно Википедии, имеет название GAP1 (https://commons.wikimedia.org/wiki/File:BMPfileFormat.svg?uselang=ru). GAP1 появился в моем BMP рисунке только после того, как я выставил значение битности на 32 в параметрах сохранения. Данные этого блока очень напоминают цветовые маски, но я не уверен, что это имеет какую-нибудь связь, так как я не смог найти информацию о GAP1. Наверное, я делаю что-то не так.
Пожалуйста, скажите, как понять принцип работы масок в BMP рисунках?
#include <iostream>
#include <fstream>
#include <string>
void ReadImage_BMP(const char* file_path_c_str);
int main()
{
ReadImage_BMP(".../image.bmp");
return 0;
}
void ReadImage_BMP(const char* file_path_c_str)
{
std::string file_path = file_path_c_str;
std::ifstream input_fs;
input_fs.open(file_path, std::ios_base::binary);
if (input_fs.is_open() == false)
{
perror(("Error while opening file " + file_path).c_str());
return;
}
char current_byte;
int count = 1;
while ((bool)input_fs.get(current_byte) == true)
{
std::cout << count << ") " << (int)(unsigned char)current_byte << std::endl;
count++;
}
if (input_fs.eof())
{
std::cout << "End of file reached." << std::endl;
}
else if (input_fs.fail())
{
std::cout << "Type error." << std::endl;
}
else
{
std::cout << "Unknown error" << std::endl;
}
input_fs.close();
}
Ответы (4 шт):
GAP1 - это просто дырка, которая появляется, если массив пикселей смещен на расстояние большее, чем размер заголовка (например для целей и выравнивания). Маски - это обычные битовые маски которые применяются через AND + смещение к цвету пикселя из массива для получения значения.
Это изображение, из wiki хорошо объясняет что такое битмаски, т.е. они определяют каким образом будут хранится биты цвета (там где 1 будет информация о соответствующем цвете) и таким образом весь файл и формируется.

Таким образом я предполагаю что поменяв маски цветов местами можно поменять сами цвета - это легко проверить.
Так же из документации можно подчерпнуть что:
bV5RedMask
Цветовая маска, указывающая красный компонент каждого пикселя, действительна только в том случае, если для параметра bV5Compression установлено значение BI_BITFIELDS.
bV5GreenMask
Цветовая маска, указывающая зеленый компонент каждого пикселя, действительна только в том случае, если для параметра bV5Compression установлено значение BI_BITFIELDS.
bV5BlueMask
Цветовая маска, определяющая синий компонент каждого пикселя, действительна только в том случае, если для параметра bV5Compression задано значение BI_BITFIELDS.
bV5AlphaMask
Цветовая маска, определяющая альфа-компонент каждого пикселя.
Демонстрация по смене цвета (меняю местами маску зеленого и красного)
Оригинальное изображение:
С измененной маской:
Как было произведена замена (оригинал)

Битовые операции AND
Как известно, эти операции очень хорошо позволяют извлекать части чисел, итак у нас есть массив пикселей по 3-4 байта на пискель в зависимости от битности изображения
[[255,255,255], [0,100,200] ... тут я обозначил группы доп скобками
В hex представлении это 0xFFFFFF и 0xС86400 соответсвенно (учитывая обратную запись байтов в файле)
А в хэдере битмапа лежат наши маски к примеру такие:
0xFF0000- red0x00FF00- green0x0000FF- blue
Таким образом байты 0xС86400 разложаться на:
0xС86400&0xFF0000->0xC80000->0xC8(R)0xС86400&0x00FF00->0x006400->0x64(G)0xС86400&0x0000FF->0x000000->0x00(B)
Как именно получаются новые цвета
И напоследок, я написал небольшую программку для экспериментов с битами, можно добиться очень интересных эффектов перемешивая их, своеобразный вариант шифровки изображений.
Модифицированная вами структура масок - нестандартная. Можно предположить, что разные программы будут по разному обрабатывать такой файл. Попробуем вникнуть, что происходит у вас.
По логике, если у нас 4-х битная маска (0x3C), то значение красного компонента не должно превышать 15 (в десятичной с/с).
Поэтому я решил проанализировать двоичное представление.
Пиксель №1
RGB(11, 12, 13) или 0x0D0C0BFF (+ альфа-канал)
Маска: 00 00 60 00 или 0x00003C00
В двоичном виде:
00001101000011000000101111111111 00000000000000000011110000000000
Биты под маской: 0010
Paint dot NET даёт нам: 00100010 (34)
Видно, что левые 4 бита идентичны правым и соответствуют битам под маской.
Можно предположить, что биты под маской были скопированы влево до заполнения 8 бит.
Пиксель №2 (0x353433FF)
00110101001101000011001111111111 00000000000000000011110000000000
Биты под маской: 1100
Заполняем до 8-бит: 11001100 (254)
Пиксель №3 (0x676665FF)
01100111011001100110010111111111 00000000000000000011110000000000
Биты под маской: 1001
Заполняем до 8-бит: 10011001 (153)
Пиксель №4 (0x999897FF)
10011001100110001001011111111111 00000000000000000011110000000000
Биты под маской: 0101
Заполняем до 8-бит: 01010101 (85)
Таким образом, предположение подтвердилось.
Почему именно так? Ума не приложу.
Скорее всего, единого стандарта здесь нет и каждая программа интерпретирует это по своему. В спецификации, которую я читал, есть только одно требование: биты в маске должны быть непрерывны и маски не должны перекрываться.
Учитывая, что 99.99% файлов будут со стандартными масками, можно не переживать на этот счет, хоть и любопытно очень...
UPD
Если предположение выше верно для любой маски, то могу предложить такой код для получения компоненты по маске:
#include <iostream>
unsigned long getPixelComponentByMask(unsigned long pixel, unsigned long mask);
int main()
{
unsigned long pixel = 0x0D0C0BFF;
unsigned long mask = 0x00003C00;
std::cout << getPixelComponentByMask(pixel, mask) << std::endl;
return 0;
}
unsigned long getPixelComponentByMask(unsigned long pixel, unsigned long mask)
{
if(pixel > 0 && mask > 0) {
unsigned long p, m, firstBit, n = 0, out = 0;
while(n < 8) {
p = pixel;
m = mask;
while(m > 0) {
if(m & 1) {
firstBit = p & 1;
out ^= (-firstBit ^ out) & (1UL << n);
n++;
}
p >>= 1;
m >>= 1;
}
}
return out;
}
return 0;
}
Если нужно, могу добавить комментарии к коду.
Вот программа с необходимыми функциями и тестами. Для работы нужны структуры и функции compute_mask_info, compute_pixel. Читая файл, посчитайте 4 mask_info для каждого цвета. Затем читайте пиксели и обрабатывайте их с помощью compute_pixel.
Программа не учитывает endianness компьютера, на котором она выполняется. Оставляю вам для развлечения.
Читайте также мои комментарии под вопросом, чтобы понять, для чего это нужно.
#include <iostream>
#include <iomanip>
using namespace std;
struct mask_info
{
uint32_t value; // mask
int placement_shift; // shift right to this value after applying the mask
// 8 bits normalization: > 0 - shift right to this value, < 0 = shift left to this value.
int normalization_shift;
};
struct pixel_info
{
uint32_t value; // actual value
uint32_t normalized_value; // normalized to 8 bit, for 8 bpp display
};
mask_info compute_mask_info(uint32_t mask);
void print_mask_info(const mask_info& info);
pixel_info compute_pixel(uint32_t value, const mask_info& info);
int main()
{
uint32_t masks[] =
{
// Standard masks
0xFF000000,
0x00FF0000,
0x0000FF00,
0x000000FF,
// Non-standard masks
0x000003FF, // normalization 2 expected
0x0000007F, // normalization -1 expected
0x00003FF0, // placement 4, normalization 2 expected
0x00007F00 // placement 8, normalization -1 expected
};
// Test masks
for(auto n: masks)
{
auto mi = compute_mask_info(n);
print_mask_info(mi);
}
cout << endl;
// Test pixels, standard masks
{
uint32_t pixel = 0xA1B2C3D4;
mask_info mi[] =
{
compute_mask_info(masks[0]), // blue
compute_mask_info(masks[1]), // green
compute_mask_info(masks[2]), // red
compute_mask_info(masks[3]) // alpha
};
for(const auto& minfo: mi)
{
auto p = compute_pixel(pixel, minfo);
cout << hex << setfill('0') << setw(2) << p.value << " " << setw(2) << p.normalized_value << endl;
}
}
cout << endl;
// Test pixels, non-standard masks
{
uint32_t pixel = 0x00003FF0; // expected output with masks[6]: 3FF FF
auto p = compute_pixel(pixel, compute_mask_info(masks[6]));
cout << hex << setfill('0') << setw(2) << p.value << " " << setw(2) << p.normalized_value << endl;
}
return 0;
}
pixel_info compute_pixel(uint32_t value, const mask_info& info)
{
pixel_info pi{};
pi.value = (value & info.value) >> info.placement_shift;
pi.normalized_value = pi.value;
if (info.normalization_shift > 0)
{
pi.normalized_value >>= info.normalization_shift;
}
else if (info.normalization_shift < 0)
{
pi.normalized_value <<= info.normalization_shift;
}
return pi;
}
void print_mask_info(const mask_info& info)
{
cout << "0x" << hex << setfill('0') << setw(8) << info.value << dec << " " << info.placement_shift << " " << info.normalization_shift << endl;
}
mask_info compute_mask_info(uint32_t mask)
{
mask_info mi{};
mi.value = mask;
int bit1_count{}; // number of set bits
for(int i = 0; i < 32; ++i)
{
if (mask & (1<<i))
{
++bit1_count;
}
else
{
if (bit1_count)
{
break; // this is 0 after mask, stop here
}
++mi.placement_shift;
}
}
mi.normalization_shift = bit1_count - 8;
return mi;
}
Для полноты картины, вывод программы:
0xff000000 24 0 0x00ff0000 16 0 0x0000ff00 8 0 0x000000ff 0 0 0x000003ff 0 2 0x0000007f 0 -1 0x00003ff0 4 2 0x00007f00 8 -1 a1 a1 b2 b2 c3 c3 d4 d4 3ff ff





