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 packto ensure correct alignment. Then added the function openBMPwhich 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 convertToBlackAndWhitewhich 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 lessblack. 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::asyncwhich allows functions to run asynchronously. It automatically manages the lifecycle of threads and returns std::futurewhich 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::futurewhich 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 newPixelDatawhich 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?

gorgeous

gorgeous

bring back the wall!

bring back the wall!

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) ❤️

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *