BMP Show или о том, как я делал тестовое
Ссылка на GitHub проект
Вступление
На днях я столкнулся с интересным тестовым заданием: нужно было вывести изображение только из черного и белого (bmp 24 или 32 бита). Однако поиски подходящего изображения и готового конвертера не принесли успеха. Это натолкнуло меня на идею: почему бы не создать свой собственный многопоточный конвертер BMP?
У самурая нет цели, только путь… © Конфуций / Мао Дзе Дун / Ким Чен Ин
ну или кто-то из них. кстати, на тестовое так и не ответили, так что я все еще ищу работу…
я еще здесь!
Думаю, это кому-то пригодится, по крайней мере в рамках изучения еще одной области применения C++ — обработке изображений. Еще тут есть многопоточность и так далее. В общем все узнаете дальше →
Начало работы
Исходя из задания, я решил создать консольное приложение, которое реализует:
Чтение и отображение изображений в формате BMP.
Конвертация цветных изображений в черно-белые.
Использование многопоточности для повышения производительности. (потому что с конвертацией грех не использовать)
Реализация
Чтение BMP
Сначала я разработал структуру заголовков BMP-файлов. Для этого использовал директиву #pragma pack
, чтобы гарантировать правильное выравнивание. Затем добавил функцию openBMP
, которая читает заголовки и пиксели, обрабатывая возможные ошибки.
#pragma pack(push, 1)
struct BMPFileHeader {
uint16_t fileType{};
uint32_t fileSize{};
uint16_t reserved1{};
uint16_t reserved2{};
uint32_t offsetData{};
};
struct BMPInfoHeader {
uint32_t size;
int32_t width;
int32_t height;
uint16_t planes;
uint16_t bitCount;
uint32_t compression{};
uint32_t imageSize{};
int32_t xPixelsPerMeter{};
int32_t yPixelsPerMeter{};
uint32_t colorsUsed{};
uint32_t colorsImportant{};
};
#pragma pack(pop)
void openBMP(const std::string &fileName) {
std::ifstream file(fileName, std::ios::binary);
if (!file) {
throw std::runtime_error("Ошибка открытия файла: " + fileName);
}
// Чтение заголовков
file.read(reinterpret_cast(&fileHeader), sizeof(fileHeader));
if (file.gcount() != sizeof(fileHeader)) throw std::runtime_error("Ошибка чтения заголовка файла.");
file.read(reinterpret_cast(&infoHeader), sizeof(infoHeader));
if (file.gcount() != sizeof(infoHeader)) throw std::runtime_error("Ошибка чтения заголовка информации.");
if (infoHeader.bitCount != 24 && infoHeader.bitCount != 32) {
throw std::runtime_error("Неподдерживаемый формат BMP! Ожидалось 24 или 32 бита.");
}
file.seekg(fileHeader.offsetData, std::ios::beg);
rowStride = (infoHeader.width * (infoHeader.bitCount / 8) + 3) & ~3;
pixelData.resize(rowStride * infoHeader.height);
file.read(reinterpret_cast(pixelData.data()), pixelData.size());
if (file.gcount() != pixelData.size()) throw std::runtime_error("Ошибка чтения пикселей.");
}
Проверка цветов
Следующим шагом была функция, проверяющая, есть ли у изображения более двух цветов. Это критически важно, так как если изображение состоит только из черного и белого, его не нужно конвертировать.
[[nodiscard]] bool hasMoreThanTwoColors() const {
for (int y = 0; y < infoHeader.height; ++y) {
for (int x = 0; x < infoHeader.width; ++x) {
int index = getPixelIndex(x, y);
uint8_t blue = pixelData[index];
uint8_t green = pixelData[index + 1];
uint8_t red = pixelData[index + 2];
if (!(red == 255 && green == 255 && blue == 255) && !(red == 0 && green == 0 && blue == 0))
return true;
}
}
return false;
}
Конвертация в черно-белый
Для конвертации я разработал метод convertToBlackAndWhite
, использующий функционал многопоточности для увеличения скорости обработки. Я определял максимальное количество потоков, доступных на системе, и разбивал работу на равные части. Удобно, не правда ли?
void convertToBlackAndWhite() {
auto convertRow = [this](int startRow, int endRow, std::vector &newPixelData) {
for (int y = startRow; y < endRow; ++y) {
for (int x = 0; x < infoHeader.width; ++x) {
int index = (y * rowStride) + (x * (infoHeader.bitCount / 8));
uint8_t blue = pixelData[index];
uint8_t green = pixelData[index + 1];
uint8_t red = pixelData[index + 2];
double brightness = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
if (brightness < brightness_factor) {
newPixelData[index] = 0;
newPixelData[index + 1] = 0;
newPixelData[index + 2] = 0;
} else {
newPixelData[index] = 255;
newPixelData[index + 1] = 255;
newPixelData[index + 2] = 255;
}
}
}
};
std::vector newPixelData = pixelData;
// Получаем максимальное количество потоков
unsigned int numThreads = std::thread::hardware_concurrency();
if (numThreads == 0) numThreads = 1; // Если нет доступного количества потоков, то берем 1
int rowsPerThread = infoHeader.height / numThreads;
std::vector > futures;
for (unsigned int i = 0; i < numThreads; ++i) {
int startRow = i * rowsPerThread;
int endRow = (i == numThreads - 1) ? infoHeader.height : startRow + rowsPerThread;
// Последний поток берет оставшиеся строки
futures.push_back(std::async(std::launch::async, convertRow, startRow, endRow, std::ref(newPixelData)));
}
for (auto &future: futures) {
future.get();
}
pixelData = std::move(newPixelData);
}
Также, как вы видите, я определил некий brightness_factor
, что же это да диковина?
Исходя из RGB можно определить освещенность пикселя по формуле 0.2126 * red + 0.7152 * green + 0.0722 * blue
, и то, что дает показатель больший 128, я определяю как белый, а меньший — черный. Этот показатель можно изменять, тем самым делая более темные или светлые изображения.
Вот вам идея — сделать реализацию, которая будет учитывать баланс белого и из него устанавливать параметр brightness_factor
. У меня же это стандартная середина, никакой коррекции, чистый хардкор)
Не бойтесь, сейчас объясню многопоточнось
Многопоточность
Реализация многопоточности является ключевым моментом, так как она позволяет ускорить процесс обработки изображений, особенно при работе с большими файлами. Давайте подробнее рассмотрим, как это было реализовано.
1. Определение количества потоков
Первым шагом является определение количества доступных потоков, которое можно получить с помощью функции std::thread::hardware_concurrency()
. Эта функция возвращает количество потоков, которые поддерживает система. Если система не может определить это значение, то она возвращает 1. (чтобы все не сломать к чертям)
unsigned int numThreads = std::thread::hardware_concurrency();
if (numThreads == 0) numThreads = 1; // Если нет доступного количества потоков, то берем 1
2. Разделение работы на части
Затем необходимо разбить работу по конвертации на части. Мы можем разделить высоту изображения на количество потоков, чтобы каждый поток обрабатывал свой участок строк изображения.
int rowsPerThread = infoHeader.height / numThreads;
3. Создание потоков
Для создания потоков я использовал std::async
, который позволяет запускать функции в асинхронном режиме. Он автоматически управляет жизненным циклом потоков и возвращает std::future
, который позволяет ожидать завершения работы потоков.
std::vector > futures;
for (unsigned int i = 0; i < numThreads; ++i) {
int startRow = i * rowsPerThread;
int endRow = (i == numThreads - 1) ? infoHeader.height : startRow + rowsPerThread;
// Последний поток берет оставшиеся строки
futures.push_back(std::async(std::launch::async, convertRow, startRow, endRow, std::ref(newPixelData)));
}
5. Ожидание завершения потоков
После создания и запуска всех потоков необходимо дождаться их завершения. Это можно сделать с помощью метода get()
для каждого объекта std::future
, который мы сохранили в векторе futures
.
for (auto &future: futures) {
future.get();
}
6. Итоговая замена пикселей
После завершения всех потоков, pixelData
заменяется на newPixelData
, которая теперь содержит конвертированные пиксели.
pixelData = std::move(newPixelData);
ну и все, тут тоже ничего сложного, если разобраться)
Отображение изображения
Для отображения результата использовал символы #
и пробелы, чтобы визуализировать черно-белое изображение в консоли. Да, есть и другие реализации этих цветов, но я выбрал такой простой и наглядный.
void displayBMP() {
if (hasMoreThanTwoColors()) {
std::cout << "Изображение содержит более двух цветов, конвертируем в черно-белое..." << std::endl;
convertToBlackAndWhite();
}
for (int y = infoHeader.height - 1; y >= 0; y -= 2) {
for (int x = 0; x < infoHeader.width; ++x) {
int index = getPixelIndex(x, y);
uint8_t blue = pixelData[index];
uint8_t green = pixelData[index + 1];
uint8_t red = pixelData[index + 2];
std::cout << ((red == 255 && green == 255 && blue == 255) ? WHITE : BLACK);
}
std::cout << std::endl;
}
}
Все просто) И как видите, как раз тут и происходит проверка на содержание более двух цветов и конвертация, если это необходимо.
Результаты
как без них-то
красавица
верните стену!
Заключение
В итоге, несмотря на трудности с поиском подходящих изображений, мне удалось создать функциональный и эффективный конвертер BMP. Это не только помогло мне выполнить задание, но и дало возможность изучить многопоточность и работу с графическими форматами.
Спасибо, что дочитал до конца) ❤️