Author: Pieter Philippaerts
3.
Processor Intensive function
In
part 3, we're going to write a more complex
function than we did in Part 2. This is where
you can see the real benifits of using C++
instead of Visual Basic.
We're going to write a function that converts
a picture to grayscale.
Before
I continue the DLL tutorial, I'm first going
to tell you how pointers work in C/C++,
because we'll definitely need them in our new
grayscale function.
In C/C++, the star ('*') can be interpreted in
several ways. As you probably know, it's the
multiplication operator, but it's also used to
define and use pointers. I'm going to be
*very* short with this explanation, so if you
have never used pointers, I suggest you take a
look at an excellent tutorial
about pointers and arrays in C.
int
*p;
The
above code creates a new variable where we can
store a pointer to an integer. We can change
the location where the pointer is pointing to
by simply assigning the new location like
this:
p
= [NEW LOCATION];
Where
[NEW LOCATION] is the memory location where
you want it to point to. This is often a very
large (or very small) number.
To access the value of the integer where our
pointer points to, we can use the following
code:
*p
= 5;
Where
*p means we want to access the integer where
the pointer points to, not the pointer itself.
int
val = 5;
int
*p;
int
q;
p = &value;
q = value;
*p = 7;
//
value is now equal to 7
q = 10;
//
value is still equal to 7
In
the above code, we first define an integer and
assign the value '5' to it, then a pointer p
and then an integer q.
Then we assign the address of val to our
pointer (the '&' means 'address of') and
copy the value of the integer val to q.
If we modify *p we also modify val because the
pointer p points to the address of val. If we
modify q, val doesn't change because q is a
copy of val, not a pointer to val.
We're
now ready to start programming our grayscale
function.
Our fucntions needs the following parameters:
a pointer to the 24bit bitmap data, the width
of the bitmap and the height of a bitmap.
Hence, the function declaration looks like
this:
int _stdcall ConvertToGray(BYTE *picData,
int width,
int height) { |
In
Visual Basic this translates to either
Private Declare Function ConvertToGray
Lib
"MyDll.dll" (ByRef picData
As Byte, ByVal width
As Long, ByVal height
As Long) As Long |
or
Private Declare Function ConvertToGray
Lib
"MyDll.dll" (ByVal picData
As Long, ByVal width
As Long, ByVal height
As Long) As Long |
The
difference between the two is that the first
declaration requires a byte array to be passed
to the function and the second fucntion wants
a pointer to a byte array.
In the end, it's both the same because the
pointer is passed ByVal and the byte array is
passed ByRef.
We're going to use the second version because
the CreateDIBSection function returns a
pointer to the byte array, instead of the byte
array itself.
In
a moment, we're now going to start writing our
Visual Basic code, but I'll explain what it's
going to do first.
We're going to write code that first creates a
200x200 pixels 24bit Device Independant
Bitmap, then copies a part of the desktop to
that memory bitmap, then converts it to
grayscale (with our VC++ function) and finally
displays the result.
Ok,
here we go:
Private Sub Form_Load()
Dim bi24BitInfo
As BITMAPINFO, Cnt
As Long
Dim hPointer
As Long, iBitmap
As Long, iDC
As Long
Me.AutoRedraw = True |
First
we declare the variables we're going to use.
Nothing special to see here, except perhaps
the bi24BitInfo declaration. The BITMAPINFO is
a structure that we're going to need to create
our Device Independant Bitmap.
We set the AutoRedraw property of the form to
True, to make sure our grayscale picture
doesn't get erased when the form redraws.
With bi24BitInfo.bmiHeader
.biBitCount = 24
.biCompression = BI_RGB
.biPlanes = 1
.biSize = Len(bi24BitInfo.bmiHeader)
.biWidth = 200
.biHeight = 200
End With |
Then,
we initialize the BITMAPINFO structure. Note
that we're going to create a 24bit bitmap with
width and height set to 200 pixels.
iDC = CreateCompatibleDC(0)
iBitmap = CreateDIBSection(iDC, bi24BitInfo,
_
DIB_RGB_COLORS, hPointer,
ByVal 0&,
ByVal 0&)
SelectObject iDC, iBitmap |
We
create our Device Independant Bitmap and
select it into a newly created device context.
Also note that the variable hPointer now
contains the value of a pointer to the bitmap
bits.
BitBlt iDC, 0, 0, bi24BitInfo.bmiHeader.biWidth,
_
bi24BitInfo.bmiHeader.biHeight, GetDC(0), 0,
_
0, vbSrcCopy
ConvertToGray hPointer,
_
bi24BitInfo.bmiHeader.biWidth,
_
bi24BitInfo.bmiHeader.biHeight
BitBlt Me.hdc, 0, 0, bi24BitInfo.bmiHeader.biWidth,
_
bi24BitInfo.bmiHeader.biHeight, iDC, 0, 0, vbSrcCopy |
The
first BitBlt copies the pixels from the screen
into our memory bitmap.
Then we call our VC++ function
'ConvertToGray'.
The second BitBlt copies the result to the
form.
DeleteDC iDC
DeleteObject iBitmap
End Sub |
These
final lines are cleanup code to delete our
memory bitmap from the memory.
Now
that we have our Visual Basic code, we can
finally start writing our ConvertToGray
function. What this function basically does is
looping trough the image, calculating the
grayscale values of each pixel.
int _stdcall ConvertToGray(BYTE *picData,
int width,
int height) {
int BytesPerScanLine = ((width * 3 + 3) & 0xfffffffc) - 3;
BYTE *r, *g, *b;
int index;
for
(int cy=0; cy<height; cy++) {
index = cy * BytesPerScanLine;
for (int cx=0; cx < BytesPerScanLine; cx+=3) {
b = &picData[index];
g = &picData[index + 1];
r = &picData[index + 2];
*r = (BYTE)(0.299 * *r + 0.587* *g + 0.114* *b);
*g = *r;
*b = *r;
index += 3;
}
}
return 1;
} |
A bitmap is
divided in several horizontal lines, called
Scan Lines. A bitmap with a height of y pixels
has y Scan Lines. The number of bytes in a
scan line must be divisible by 4. If the
number of bytes required by a bitmap to store
one horizontal line of pixels is not divisable
by 4, the line is padded with empty bytes
until it is divisable by 4.
For instance, lets say we have a 3x3 24bit
bitmap.This bitmap has 3 scan lines (because
its height is 3). The number of bytes required
to store one horizontal line of pixels is 9 (3
pixels x 3 bytes/pixel). Nine is not divisable
by 4, so each scan line is padded with 3 empty
bytes, because 9+3 = 12 = divisable by 4.
On the first line of
code in our function, we calculate the number
of bytes per scan line, and we store it in the
integer 'BytesPerScanLine'.
We
define three pointers and an integer. In this
integer, we're going to store the current
offset in the byte array.
We
enter our first loop that counts from 0 to
'the height of the bitmap-1'.
We
adjust the index, to make sure we start from
the right offset in the array.
We
enter our second loop which moves trough all
the bytes of a scan line.
We
assign values to aour pointers r, g & b
(note that the pixels are stored in BGR format
in the bitmap data).
We
calculate the corresponding gray value and we
assign this gray value to our r, g and b
components.
We
move our offset variable.
This
is a very processor intensive function,
because the code loops trough the entire
bitmap, pixel per pixel. The code is quite
fast (it takes only 130ms on my machine to
convert a 1600x1200 picture to grayscale). A
similar function written in VB would be
noticeable slower (also when it's compiled to
Native code).
You
can download the VC++ source code and the VB example project
over
here.
Part
1 - Part 2 - Part 3
- Part 4 isn't available
yet
|