Click to See Complete Forum and Search --> : Image Tiling with GDI+


thehumph
March 24th, 2008, 04:37 PM
Hi folks,

I'm trying to take individual images and tile them together to form one larger image. The images are ordered, as they are pieces of a larger view, and currently I am trying to do this with 9 images in a grid of 3 by 3. Does anyone have any experience of this kind of thing using native C++ and GDI+?

I need to add one tile at at time to the overall image, and then process the overall image with the new tile included. Then I can add the next tile, and process the new extended image, and so on. I'm finding that when I create a new bitmap with the following code, and run a quick test to check that I can write to the pixels, I get an image that is mostly black, but has a white diagonal line running from the top left corner across the image (but not directly to the opposite corner I don't think).


Bitmap bmp(1000,1000,PixelFormat24bppRGB); // new mosaic

INT iWidth = bmp.GetWidth();
INT iHeight = bmp.GetHeight();

Rect rect(0,0,iWidth,iHeight);

BitmapData bmData;

bmp.LockBits(&rect,ImageLockModeRead |ImageLockModeWrite,
PixelFormat24bppRGB,&bmData);

int stride = bmData.Stride;
int offset = stride - iWidth * 3;

byte * p = (byte *)(void *)bmData.Scan0;

for (int y = 0; y < iHeight3; y++)
{
for (int x = 0; x < iWidth3; x++)
{
p3[0] = 0;
p3[1] = 0;
p3[2] = 0;

p3 += 3;
}
p3 += offset;
}

bmp.UnlockBits(&bmData);


I've noticed that the offset here is 0 because the Stride is 3000 and the width is 1000 (and obviously the image is 24-bit). This is unusual, I'm used to working with images that have an offset of 2, that have not been generated by GDI+. Has anyone had this problem before?

I'd appreciate quick answers on this as it is slowing my project down!

Cheers.

CBasicNet
March 24th, 2008, 11:44 PM
What is p3? Why don't you post the actual code?

thehumph
March 25th, 2008, 05:37 AM
Sorry I changed the variable names to bmp and p from im3 and p3 so that they'd have more meaning here.


Bitmap bmp(1000,1000,PixelFormat24bppRGB); // new mosaic

INT iWidth = bmp.GetWidth();
INT iHeight = bmp.GetHeight();

Rect rect(0,0,iWidth,iHeight);

BitmapData bmData;

bmp.LockBits(&rect,ImageLockModeRead |ImageLockModeWrite,
PixelFormat24bppRGB,&bmData);

int stride = bmData.Stride;
int offset = stride - iWidth * 3;

byte * p = (byte *)(void *)bmData.Scan0;

for (int y = 0; y < iHeight3; y++)
{
for (int x = 0; x < iWidth3; x++)
{
p3[0] = 0;
p3[1] = 0;
p3[2] = 0;

p += 3;
}
p += offset;
}

bmp.UnlockBits(&bmData);


I've found in another instance if I create a bitmap and try and edit any pixels, then draw it to a graphics object there are no changes. It just appears as a white space, I'm not sure if thats the image or the background of the canvas. I don't have the code for that on this machine though.

Is it normal for a newly created bitmap to have an offset of 0?

CBasicNet
March 26th, 2008, 03:09 AM
I see no tiling in your code. And why is the for loop counter index, x and y is not used in the looping operation?


p3[0] = 0;
p3[1] = 0;
p3[2] = 0;

p += 3;


??

No actual relevant code == No help

I do not want to read code which cannot be compiled.

thehumph
March 28th, 2008, 05:03 AM
My apologies CBasicNet, thanks for taking a look. I've got the FULL code here and I'll explain it after I've posted it. (This compiles, you'll need to change the image file loaded obviously). Right now I'm just trying to add one tile to another horizontally. After I get that working I can think about multiple tiling in both axes. All the images are 24-bit and this is unmanaged C++.


Bitmap b(L"blobtest.jpg");
Bitmap* b2;

int iWidth = b.GetWidth();
int iHeight = b.GetHeight();

Rect rect(0,0,iWidth,iHeight);
b2 = b.Clone(rect,PixelFormat24bppRGB);


BitmapData bmData;
BitmapData bmData2;

b.LockBits(&rect,ImageLockModeRead | ImageLockModeWrite,
PixelFormat24bppRGB,&bmData);
b2->LockBits(&rect,ImageLockModeRead |ImageLockModeWrite,
PixelFormat24bppRGB,&bmData2);

int stride = bmData.Stride;
int offset = stride - iWidth*3;

byte * p1 = (byte *)(void *)bmData.Scan0;
byte * p2 = (byte *)(void *)bmData2.Scan0;

Bitmap b3(iWidth*2,iHeight,PixelFormat24bppRGB);

int iWidth3 = b3.GetWidth();
int iHeight3 = b3.GetHeight();

Rect rect3(0,0,iWidth3,iHeight3);

BitmapData bmData3;

b3.LockBits(&rect3,ImageLockModeRead |ImageLockModeWrite,
PixelFormat24bppRGB,&bmData3);

int stride3 = bmData3.Stride;
int offset3 = stride3 - iWidth3*3;

byte * p3 = (byte *)(void *)bmData3.Scan0;

for (int y = 0; y < iHeight3; y++)
{
for (int x = 0; x < iWidth3; x++)
{
if (x < iWidth)
{
p3[0] = p3[1] = p3[2] = p1[0];
p1 += 3;
}
else if (x >= iWidth)
{
// COMMENTED OUT UNTIL PROBLEM RESOLVED
//p3[0] = p3[1] = p3[2] = p2[0];
//p2 += 3;
}
p3 += 3;
}
p1 += offset;
p2 += offset;
p3 += offset;
}


Hopefully a lot of it will be self-explanatory. p1, p2 and p3 are the pointers to the Scan0 attributes of bmData, bmData2 and bmData3. The reason I didn't use x and y in the previous example (and also not using it in this, except to check if the iteration is as wide as the tile) is because I am using that pointer to Scan0 for each image. So rather than using a multi-dimensional array I'm keeping the image data where it is and iterating through it's one-dimensional array.

Now here's the problem. When I run that code above I get the first tile b stretched into a parallellogram across the new bitmap b3. Thats why I commented out adding the second tile, because it tries to write over the bounds of the new image.

The first two images both have an offset of 2 which is pretty standard for the kind of images I'm working with. The third has an offset of 0 which might be causing the problem. I'm not sure why this is. Am I creating the new bitmap incorrectly?! Have you seen something like this before? Am I even using a good method for adding the tiles together (regardless of the current issue)?

Hopefully I've given you enough to go off this time. I appreciate any help! Cheers.

CBasicNet
March 28th, 2008, 05:52 AM
Doing pixel by pixel is primitive and low level way of doing. You can do it by DrawImage(0 to draw the tiles, first we must create a graphics with the DestBmp selected, so whatever we draw will be in graphics, will be in DestBmp


bool TileImage(
Bitmap &SrcBmp,
const std::wstring& szDestFile,
const std::wstring& szEncoderString )
{
Bitmap DestBmp(
SrcBmp.GetWidth()*3,
SrcBmp.GetHeight()*3,
PixelFormat32bppARGB );

Graphics graphics(&DestBmp);

for( int x=0; x<3; ++x )
{
for( int y=0; y<3; ++y )
{
graphics.DrawImage(
&SrcBmp,
(REAL)x*SrcBmp.GetWidth(),
(REAL)y*SrcBmp.GetHeight(),
(REAL)SrcBmp.GetWidth(),
(REAL)SrcBmp.GetHeight() );
}
}

// After you got the tiling DestBmp, do what you want, eg displaying or saving to a file

CLSID Clsid;
int result = GetEncoderClsid(szEncoderString.c_str(), &Clsid);

if( result < 0 )
return false;

Status status = DestBmp.Save( szDestFile.c_str(), &Clsid );

return status == Ok;
}

int GetEncoderClsid(const WCHAR* format, CLSID* pClsid)
{
UINT num = 0; // number of image encoders
UINT size = 0; // size of the image encoder array in bytes

ImageCodecInfo* pImageCodecInfo = NULL;

GetImageEncodersSize(&num, &size);
if(size == 0)
return -1; // Failure

pImageCodecInfo = (ImageCodecInfo*)(malloc(size));
if(pImageCodecInfo == NULL)
return -1; // Failure

GetImageEncoders(num, size, pImageCodecInfo);

for(UINT j = 0; j < num; ++j)
{
if( wcscmp(pImageCodecInfo[j].MimeType, format) == 0 )
{
*pClsid = pImageCodecInfo[j].Clsid;
free(pImageCodecInfo);
return j; // Success
}
}

free(pImageCodecInfo);
return -1; // Failure
}



Here is how to call the function.


Bitmap SrcBmp(L"E:\\hh\\GreenRect.bmp", TRUE);

bool bRet = TileImage(
SrcBmp,
L"E:\\GreenRect.bmp",
L"image/bmp" );

if( bRet )
MessageBox(NULL, L"Successful", L"Title", MB_OK);
else
MessageBox(NULL, L"Failed", L"Title", MB_OK);


Hopefully the code is self-explanatory.

thehumph
March 29th, 2008, 09:21 AM
Thanks for that CBasicNet, that works great! Much faster than per pixel too which is excellent because I'll be running various per pixel algorithms over the tiled images.

I also have a need to sub-sample the image if necessary (if the user chooses to do so) to a maximum sub-sample of 3 pixels (so the destination image is formed only of every 3rd pixel of the source image). I'm guessing this can only be done per pixel with GDI+ so it gets back to my original problem with the code above... when I use the Bitmap constructor to create an image of (width, height, PixelFormat24bppRGB) it has an offset of 0. So when I DO write pixels to that image it creates a paralellogram again (unless it tries to write out of bounds in which case it fails).

So am I creating the image incorrectly? Is there anything I have to do with the Bitmap object between its creation and iterating per pixel through it and changing the data?

CBasicNet
March 31st, 2008, 02:54 AM
Here is the example code to assess the x and y, the x is col and y is row, substitute your x and y into the col and row to get your 3x3.

Basically the below code just assign the pixel to itself. You just modify it to your needs as I do not know how you want to hold your 3x3 pic data.


DestPixels[row * nStride1 / 4 + col] = DestPixels[row * nStride1 / 4 + col];
[code]

[code]
bool SelfAssignedImage(
Bitmap &SrcBmp,
int x,
int y,
int nWidth,
int nHeight,
const std::wstring& szDestFile,
const std::wstring& szEncoderString )
{
// Create the dest image
Bitmap DestBmp(nWidth,nHeight,PixelFormat32bppARGB);

Rect rect1(0, 0, nWidth, nHeight);

BitmapData bitmapData;
memset( &bitmapData, 0, sizeof(bitmapData));
DestBmp.LockBits(
&rect1,
ImageLockModeRead,
PixelFormat32bppARGB,
&bitmapData );

int nStride1 = bitmapData.Stride;
if( nStride1 < 0 )
nStride1 = -nStride1;

UINT* DestPixels = (UINT*)bitmapData.Scan0;

if( !DestPixels )
return false;

for(UINT row = 0; row < bitmapData.Height; ++row)
{
for(UINT col = 0; col < bitmapData.Width; ++col)
{
DestPixels[row * nStride1 / 4 + col] = DestPixels[row * nStride1 / 4 + col];
}
}

DestBmp.UnlockBits(
&bitmapData );

CLSID Clsid;
int result = GetEncoderClsid(szEncoderString.c_str(), &Clsid);

if( result < 0 )
return false;

Status status = DestBmp.Save( szDestFile.c_str(), &Clsid );

return status == Ok;
}


Hope it helps! :)