BMP Show or how I did the test
Link to GitHub project
Introduction
The other day I came across an interesting test task: I needed to output an image from only black and white (bmp 24 or 32 bit). However, the search for a suitable image and a ready-made converter did not bring success. This gave me an idea: why not create my own multi-threaded BMP converter?
A samurai has no goal, only a path… (c) Confucius / Mao Zedong / Kim Jong In
or one of them. By the way, they didn’t answer the test, so I'm still looking for a job…
I'm still here!
I think this will be useful to someone, at least as part of the study of another area of application of C++ – image processing. There is also multithreading and so on. In general, you will find out everything further ->
Getting started
Based on the assignment, I decided to create a console application that implements:
Reading and displaying images in BMP format.
Convert color images to black and white.
Using multithreading to improve performance. (because it’s a sin not to use conversion)
Implementation
Read BMP
First I developed the header structure of BMP files. To do this I used the directive #pragma pack
to ensure correct alignment. Then added the function openBMP
which reads headers and pixels, handling possible errors.
#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<char *>(&fileHeader), sizeof(fileHeader));
if (file.gcount() != sizeof(fileHeader)) throw std::runtime_error("Ошибка чтения заголовка файла.");
file.read(reinterpret_cast<char *>(&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<char *>(pixelData.data()), pixelData.size());
if (file.gcount() != pixelData.size()) throw std::runtime_error("Ошибка чтения пикселей.");
}
Checking colors
The next step was a function that checked if the image had more than two colors. This is critical because if the image is just black and white, it doesn't need to be converted.
[[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;
}
Convert to black and white
For the conversion I developed a method convertToBlackAndWhite
which uses multithreading functionality to increase processing speed. I determined the maximum number of threads available on the system and divided the work into equal parts. Convenient, isn't it?
void convertToBlackAndWhite() {
auto convertRow = [this](int startRow, int endRow, std::vector<uint8_t> &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<uint8_t> newPixelData = pixelData;
// Получаем максимальное количество потоков
unsigned int numThreads = std::thread::hardware_concurrency();
if (numThreads == 0) numThreads = 1; // Если нет доступного количества потоков, то берем 1
int rowsPerThread = infoHeader.height / numThreads;
std::vector<std::future<void> > 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);
}
Also, as you can see, I defined some brightness_factor
what is this curiosity?
Based on RGB You can determine the pixel illumination using the formula 0.2126 * red + 0.7152 * green + 0.0722 * blue
and what the indicator gives larger 128I define it as whiteA less – black. This indicator can be changed, thereby making darker or lighter images.
Here's an idea – to make an implementation that will take into account the white balance and set the parameter from it brightness_factor
. For me, this is the standard middle, no correction, pure hardcore)
Don't be afraid, I'll explain multithreading now
Multithreading
Implementing multithreading is key because it allows you to speed up image processing, especially when working with large files. Let's take a closer look at how this was implemented.
1. Determining the number of threads
The first step is to determine the number of available threads, which can be obtained using the function std::thread::hardware_concurrency()
. This function returns the number of threads that the system supports. If the system cannot determine this value, then it returns 1. (so as not to break everything to hell)
unsigned int numThreads = std::thread::hardware_concurrency();
if (numThreads == 0) numThreads = 1; // Если нет доступного количества потоков, то берем 1
2. Dividing the work into parts
Then you need to break down the conversion work into parts. We can divide the height of the image by the number of threads, so that each thread processes its own portion of the image lines.
int rowsPerThread = infoHeader.height / numThreads;
3. Creating threads
To create threads I used std::async
which allows functions to run asynchronously. It automatically manages the lifecycle of threads and returns std::future
which allows you to wait for threads to complete.
std::vector<std::future<void> > 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. Waiting for threads to complete
After all threads are created and started, you must wait for them to complete. This can be done using the method get()
for each object std::future
which we saved in a vector futures
.
for (auto &future: futures) {
future.get();
}
6. Final pixel replacement
After all threads have completed, pixelData
is replaced by newPixelData
which now contains the converted pixels.
pixelData = std::move(newPixelData);
Well, that’s all, there’s nothing complicated here either, if you look into it)
Image display
To display the result I used symbols #
and spaces to render a black and white image in the console. Yes, there are other implementations of these colors, but I chose this simple and obvious one.
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;
}
}
It's simple) And as you can see, this is where the check for the content of more than two colors and conversion, if necessary, takes place.
Results
how would it be without them?
Conclusion
As a result, despite the difficulties in finding suitable images, I was able to create a functional and efficient BMP converter. This not only helped me complete the assignment, but also gave me the opportunity to learn multithreading and working with graphic formats.
Thank you for reading to the end) ❤️