Divide an image into blocks using GDI+ in C++#


In the previous blog, we used OpenCV to divide an image into multiple blocks of a certain height and width, which is useful when we need to apply a certain image transformation block-wise. This blog will provide an alternative implementation using the Windows API, specifically the GDI+ library.

Approach#

Similarly to the approach introduced in the previous blog (Divide an image into blocks using OpenCV in C++) using OpenCV, this alternative aims to crop the blocks one by one after computing their coordinates and dimensions. At the same time, we will make sure to crop the side blocks and lower blocks correctly even if their dimensions are different than other blocks.

From Divide an image into blocks using OpenCV in C++:

To elaborate on this a bit more: if the image width = 512 px and the block width = 128 px -> we will get 512 / 128 = 4 blocks. However if the image width = 576 px and the block width = 128 px -> we should get 4 blocks with 128 px and 1 block with width 64 px. Therefore on each loop we need to check/ compute the block dimensions.

Similarly, we also use while loops in this alternative to keep the code fast and optimal. The previously described code is implemented in the following snippet:

  1#include <Windows.h>
  2#include <minmax.h>
  3#include <gdiplus.h>
  4#include <SDKDDKVer.h>
  5#pragma comment(lib,"gdiplus.lib")
  6#include "atlimage.h"
  7
  8
  9bool saveToMemory(Gdiplus::Bitmap* tile, std::vector<BYTE>& data, std::string dataFormat)
 10{
 11  // write to IStream
 12  IStream* istream = nullptr;
 13  CreateStreamOnHGlobal(NULL, TRUE, &istream);
 14
 15  // define encoding
 16  CLSID clsid;
 17  if (dataFormat.compare("bmp") == 0)
 18  {
 19    CLSIDFromString(L"{557cf400-1a04-11d3-9a73-0000f81ef32e}", &clsid);
 20    Gdiplus::Status status = tile->Save(istream, &clsid, NULL);
 21    if (status != Gdiplus::Status::Ok)
 22      return false;
 23  }
 24  else if (dataFormat.compare("jpg") == 0)
 25  {
 26    CLSIDFromString(L"{557cf401-1a04-11d3-9a73-0000f81ef32e}", &clsid);
 27    // Before we call Image::Save, we must initialize an EncoderParameters object.
 28    // The EncoderParameters object has an array of EncoderParameter objects.
 29    // In this case, there is only one EncoderParameter object in the array.
 30    // The one EncoderParameter object has an array of values.
 31    // In this case, there is only one value (of type ULONG)
 32    Gdiplus::EncoderParameters encoderParameters;
 33    encoderParameters.Count = 1;
 34    encoderParameters.Parameter[0].Guid = Gdiplus::EncoderQuality;
 35    encoderParameters.Parameter[0].Type = Gdiplus::EncoderParameterValueTypeLong;
 36    encoderParameters.Parameter[0].NumberOfValues = 1;
 37
 38    // Save the image as a JPEG with quality level 0.
 39    int quality = 70;
 40    encoderParameters.Parameter[0].Value = &quality;
 41
 42    Gdiplus::Status status = tile->Save(istream, &clsid, &encoderParameters);
 43    if (status != Gdiplus::Status::Ok)
 44      return false;
 45  }
 46  else if (dataFormat.compare("png") == 0)
 47  {
 48    CLSIDFromString(L"{557cf406-1a04-11d3-9a73-0000f81ef32e}", &clsid);
 49    Gdiplus::Status status = tile->Save(istream, &clsid, NULL);
 50    if (status != Gdiplus::Status::Ok)
 51      return false;
 52  }
 53
 54  // get memory handle associated with istream
 55  HGLOBAL hg = NULL;
 56  GetHGlobalFromStream(istream, &hg);
 57
 58  // copy IStream to buffer
 59  int bufsize = GlobalSize(hg);
 60  data.resize(bufsize);
 61
 62  // lock & unlock memory
 63  LPVOID pimage = GlobalLock(hg);
 64  if (pimage != 0)
 65    memcpy(&data[0], pimage, bufsize);
 66
 67  GlobalUnlock(hg);
 68  istream->Release();
 69  return true;
 70}
 71
 72int gdiplusDivideImage(Gdiplus::Bitmap* img, const int blockWidth, const int blockHeight, std::vector<std::vector<BYTE>>& blocks)
 73{
 74  int imgWidth = img->GetWidth();
 75  int imgHeight = img->GetHeight();
 76
 77  int bhSize = 0;
 78  int bwSize = 0;
 79  int blockId = 0;
 80  int y0 = 0;
 81
 82  while (y0 < imgHeight)
 83  {
 84    bhSize = ((y0 + blockHeight) > imgHeight) * (blockHeight - (y0 + blockHeight - imgHeight)) + ((y0 + blockHeight) <= imgHeight) * blockHeight;
 85
 86    int x0 = 0;
 87    while (x0 < imgWidth)
 88    {
 89      bwSize = ((x0 + blockWidth) > imgWidth) * (blockWidth - (x0 + blockWidth - imgWidth)) + ((x0 + blockWidth) <= imgWidth) * blockWidth;
 90      blockId += 1;
 91
 92      Gdiplus::Bitmap* bmp = img->Clone(x0, y0, bwSize, bhSize, PixelFormat24bppRGB);
 93
 94      // encode block
 95      std::vector<BYTE> pngbytes;
 96
 97      // encode block
 98      if (!(saveToMemory(bmp, pngbytes, "png")))
 99      {
100        std::cout << "Cannot save block_" << std::to_string(x0) << "_" << std::to_string(y0) << "to png" << std::endl;
101        return EXIT_FAILURE;
102      }
103      // update starting coordinates
104      x0 = x0 + blockWidth;
105
106      // append to vec
107      blocks.push_back(pngbytes);
108    }
109    // update starting coordinates
110    y0 = y0 + blockHeight;
111  }
112  return EXIT_SUCCESS;
113}

The previous snippet includes two main functions:

  • saveToMemory() : used to save the image bytes to the memory using a specific encoding.

  • gdiplusDivideImage() : divides an input image into blocks with an integer output reflecting the run status of the function.

The main call#

Let's call the previous function in a main function and save the resulting blocks in a defined directory to visualize the results and verify, that the code is doing what it is supposed to. For the purpose of this test I chose to use the famous Lenna picture, that can downloaded from here. In code this can be done as follows:

 1int main()
 2{
 3  // initilialize GDI+
 4  CoInitialize(NULL);
 5  ULONG_PTR token;
 6  Gdiplus::GdiplusStartupInput tmp;
 7  Gdiplus::GdiplusStartup(&token, &tmp, NULL);
 8
 9  // init vars
10  std::vector<std::vector<BYTE>> imgblocks;
11
12  // load img
13  Gdiplus::Bitmap* bitmap = Gdiplus::Bitmap::FromFile(L"Lenna.png");
14
15  // divide img
16  int divstatus = gdiplusDivideImage(bitmap, blockw, blockh, imgblocks);
17
18  // create repository for blocks
19  if (!CreateDirectory(L"blocksFolder", NULL))
20  {
21    std::wcout << "Directory Error: Cannot create directory for blocks." << std::endl;
22    return 1;
23  }
24
25  // save blocks
26  for (int j = 0; j < imgblocks.size(); j++)
27  {
28    std::string blockId = std::to_string(j);
29    std::string blockImgName = "blocksFolder/block#" + blockId + ".png";
30
31    // write from memory to file for testing:
32    std::ofstream fout(blockImgName, std::ios::binary);
33    fout.write((char*)imgblocks[j].data(), imgblocks[j].size());
34  }
35  return 0;
36}

The full code can be found in this gist: DivideImageUsingGdiplus.cpp.

Result#

The resulting blocks should look something like this:

../../../../_images/divided_lenna.png

Figure 19: divided image into multiple blocks#

Limitations#

  • This code is practical and simple to use on Windows but unfortunately, on the contrary to the OpenCV variant, it does not support different operating systems like Linux for example.

  • In some cases the user might want to have equally sized blocks; in that case the dimensions of the blocks should be pre-computed if the user wants to use this snippet.

Conclusion#

This post provided an alternative C++ implementation to the previous OpenCV code used to divide an image into multiple blocks with predefined height and width. Similarly, we also saved the resulting blocks to the hard drive in order to verify that the code is functional. The code is fairly simple and supports various image encoding types(PNG, JPEG etc.) but unlike the OpenCV implementation, it only supports Windows since it is based in part on the Win32 API, specifically the GDI+ library.

Share this blog#

Updated on 08 April 2022

👨‍💻 edited and review were on 08.04.2022

References and Further readings#