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:
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.
References and Further readings#
Image class (gdiplusheaders.h), Microsoft, https://docs.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nl-gdiplusheaders-image
GDI+, Microsoft, https://docs.microsoft.com/en-us/windows/win32/api/_gdiplus/
Crop a big picture into several small size pictures, Graphic design, https://graphicdesign.stackexchange.com/questions/30008/crop-a-big-picture-into-several-small-size-pictures
Divide 256*256 image into 4*4 blocks, Matlab, Stackoverflow, https://www.mathworks.com/matlabcentral/answers/33103-divide-256-256-image-into-4-4-blocks