Question: I have the below code to capture an image from a webcam.
My problem is this part:
SendMessage(hCaptureWnd, WM_CAP_COPY, 0, 0); // copy it to the clipboard
What it does is copy the image from the window to the clipboard, and then create a byte array out of it.
It works - as long as you don't use the clipboard while the program is running.
The problem is, this doesn't even work for myself, as I sometimes copy something while Visual Studio takes ages to start debug the web application, and then it crashes.
So here my question:
How can I get the image without using the clipboard ?
Or more specifically, how to transform hCaptureWnd to System.Drawing.Image ?
/// <summary>
/// Captures a frame from the webcam and returns the byte array associated
/// with the captured image
/// </summary>
/// <param name="connectDelay">number of milliseconds to wait between connect
/// and capture - necessary for some cameras that take a while to 'warm up'</param>
/// <returns>byte array representing a bitmp or null (if error or no webcam)</returns>
private static byte[] InternalCaptureToByteArray(int connectDelay = 500)
{
Clipboard.Clear(); // clear the clipboard
int hCaptureWnd = capCreateCaptureWindowA("ccWebCam", 0, 0, 0, // create the hidden capture window
350, 350, 0, 0);
SendMessage(hCaptureWnd, WM_CAP_CONNECT, 0, 0); // send the connect message to it
Thread.Sleep(connectDelay); // sleep the specified time
SendMessage(hCaptureWnd, WM_CAP_GET_FRAME, 0, 0); // capture the frame
SendMessage(hCaptureWnd, WM_CAP_COPY, 0, 0); // copy it to the clipboard
SendMessage(hCaptureWnd, WM_CAP_DISCONNECT, 0, 0); // disconnect from the camera
Bitmap bitmap = (Bitmap)Clipboard.GetDataObject().GetData(DataFormats.Bitmap); // copy into bitmap
if (bitmap == null)
return null;
using (MemoryStream stream = new MemoryStream())
{
bitmap.Save(stream, ImageFormat.Bmp); // get bitmap bytes
return stream.ToArray();
} // End Using stream
} // End Function InternalCaptureToByteArray
Note (http://msdn.microsoft.com/en-us/library/windows/desktop/dd756879(v=vs.85).aspx):
HWND VFWAPI capCreateCaptureWindow(
LPCTSTR lpszWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWnd,
int nID
);
#define VFWAPI WINAPI
typedef HANDLE HWND;
typedef PVOID HANDLE;
typedef void *PVOID;
Full code for reference
using System;
using System.IO;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using System.Drawing.Imaging;
using System.Collections.Generic;
using System.Runtime.InteropServices;
// http://www.creativecodedesign.com/node/66
// http://www.barebonescoder.com/2012/01/finding-your-web-cam-with-c-directshow-net/
// http://www.codeproject.com/Articles/15219/WebCam-Fast-Image-Capture-Service-using-WIA
// http://www.c-sharpcorner.com/uploadfile/yougerthen/integrate-the-web-webcam-functionality-using-C-Sharp-net-and-com-part-viii/
// http://forums.asp.net/t/1410057.aspx
namespace cc.Utility
{
// bool isCaptured = ccWebCam.CaptureSTA("capture.jpg"); // Access to path C:\Program Files (x86)\Common Files\Microsoft Shared\DevServer\10.0\capture.jpg" denied.
// byte[] captureBytes = ccWebCam.CaptureSTA();
/// <summary>
/// Timur Kovalev (http://www.creativecodedesign.com):
/// This class provides a method of capturing a webcam image via avicap32.dll api.
/// </summary>
public static class ccWebCam
{
#region *** PInvoke Stuff - methods to interact with capture window ***
[DllImport("user32", EntryPoint = "SendMessage")]
private static extern int SendMessage(int hWnd, uint Msg, int wParam, int lParam);
[DllImport("avicap32.dll", EntryPoint = "capCreateCaptureWindowA")]
private static extern int capCreateCaptureWindowA(string lpszWindowName, int dwStyle,
int X, int Y, int nWidth, int nHeight, int hwndParent, int nID);
private const int WM_CAP_CONNECT = 1034;
private const int WM_CAP_DISCONNECT = 1035;
private const int WM_CAP_COPY = 1054;
private const int WM_CAP_GET_FRAME = 1084;
#endregion
private static object objWebCamThreadLock = new object();
//CaptureToFile(@"D:\Stefan.Steiger\Documents\Visual Studio 2010\Projects\Post_Ipag\image3.jpg"):
public static bool Capture(string filePath, int connectDelay = 500)
{
lock (objWebCamThreadLock)
{
return cc.Utility.ccWebCam.InternalCaptureAsFileInThread(filePath, connectDelay);
}
} // End Treadsafe Function Capture
public static byte[] Capture(int connectDelay = 500)
{
lock (objWebCamThreadLock)
{
return InternalCaptureToByteArrayInThread(connectDelay);
}
} // End Treadsafe Function Capture
/// <summary>
/// Captures a frame from the webcam and returns the byte array associated
/// with the captured image. The image is also stored in a file
/// </summary>
/// <param name="filePath">path the file wher ethe image will be saved</param>
/// <param name="connectDelay">number of milliseconds to wait between connect
/// and capture - necessary for some cameras that take a while to 'warm up'</param>
/// <returns>true on success, false on failure</returns>
private static bool InternalCaptureAsFileInThread(string filePath, int connectDelay = 500)
{
bool success = false;
Thread catureThread = new Thread(() =>
{
success = InternalCaptureAsFile(filePath, connectDelay);
});
catureThread.SetApartmentState(ApartmentState.STA);
catureThread.Start();
catureThread.Join();
return success;
} // End Function InternalCaptureAsFileInThread
/// <summary>
/// Captures a frame from the webcam and returns the byte array associated
/// with the captured image. The image is also stored in a file
/// </summary>
/// <param name="filePath">path the file wher ethe image will be saved</param>
/// <param name="connectDelay">number of milliseconds to wait between connect
/// and capture - necessary for some cameras that take a while to 'warm up'</param>
/// <returns>true on success, false on failure</returns>
private static bool InternalCaptureAsFile(string filePath, int connectDelay = 500)
{
byte[] capture = ccWebCam.InternalCaptureToByteArray(connectDelay);
if (capture != null)
{
// Access to path C:\Program Files (x86)\Common Files\Microsoft Shared\DevServer\10.0\image1.jpg" denied.
File.WriteAllBytes(filePath, capture);
return true;
}
return false;
} // End Function InternalCaptureAsFile
/// <summary>
/// Captures a frame from the webcam and returns the byte array associated
/// with the captured image. Runs in a newly-created STA thread which is
/// required for this method of capture
/// </summary>
/// <param name="connectDelay">number of milliseconds to wait between connect
/// and capture - necessary for some cameras that take a while to 'warm up'</param>
/// <returns>byte array representing a bitmp or null (if error or no webcam)</returns>
private static byte[] InternalCaptureToByteArrayInThread(int connectDelay = 500)
{
byte[] bytes = null;
Thread catureThread = new Thread(() =>
{
bytes = InternalCaptureToByteArray(connectDelay);
});
catureThread.SetApartmentState(ApartmentState.STA);
catureThread.Start();
catureThread.Join();
return bytes;
} // End Function InternalCaptureToByteArrayInThread
/// <summary>
/// Captures a frame from the webcam and returns the byte array associated
/// with the captured image
/// </summary>
/// <param name="connectDelay">number of milliseconds to wait between connect
/// and capture - necessary for some cameras that take a while to 'warm up'</param>
/// <returns>byte array representing a bitmp or null (if error or no webcam)</returns>
private static byte[] InternalCaptureToByteArray(int connectDelay = 500)
{
Clipboard.Clear(); // clear the clipboard
int hCaptureWnd = capCreateCaptureWindowA("ccWebCam", 0, 0, 0, // create the hidden capture window
350, 350, 0, 0);
SendMessage(hCaptureWnd, WM_CAP_CONNECT, 0, 0); // send the connect message to it
Thread.Sleep(connectDelay); // sleep the specified time
SendMessage(hCaptureWnd, WM_CAP_GET_FRAME, 0, 0); // capture the frame
SendMessage(hCaptureWnd, WM_CAP_COPY, 0, 0); // copy it to the clipboard
SendMessage(hCaptureWnd, WM_CAP_DISCONNECT, 0, 0); // disconnect from the camera
Bitmap bitmap = (Bitmap)Clipboard.GetDataObject().GetData(DataFormats.Bitmap); // copy into bitmap
if (bitmap == null)
return null;
using (MemoryStream stream = new MemoryStream())
{
bitmap.Save(stream, ImageFormat.Bmp); // get bitmap bytes
return stream.ToArray();
} // End Using stream
} // End Function InternalCaptureToByteArray
}
}
I tried like this, but it only gets a black image...
[DllImport("user32.dll")]
static extern IntPtr GetWindowDC(IntPtr hWnd);
[DllImport("gdi32.dll", SetLastError = true)]
static extern IntPtr CreateCompatibleDC(IntPtr hdc);
enum TernaryRasterOperations : uint
{
/// <summary>dest = source</summary>
SRCCOPY = 0x00CC0020,
/// <summary>dest = source OR dest</summary>
SRCPAINT = 0x00EE0086,
/// <summary>dest = source AND dest</summary>
SRCAND = 0x008800C6,
/// <summary>dest = source XOR dest</summary>
SRCINVERT = 0x00660046,
/// <summary>dest = source AND (NOT dest)</summary>
SRCERASE = 0x00440328,
/// <summary>dest = (NOT source)</summary>
NOTSRCCOPY = 0x00330008,
/// <summary>dest = (NOT src) AND (NOT dest)</summary>
NOTSRCERASE = 0x001100A6,
/// <summary>dest = (source AND pattern)</summary>
MERGECOPY = 0x00C000CA,
/// <summary>dest = (NOT source) OR dest</summary>
MERGEPAINT = 0x00BB0226,
/// <summary>dest = pattern</summary>
PATCOPY = 0x00F00021,
/// <summary>dest = DPSnoo</summary>
PATPAINT = 0x00FB0A09,
/// <summary>dest = pattern XOR dest</summary>
PATINVERT = 0x005A0049,
/// <summary>dest = (NOT dest)</summary>
DSTINVERT = 0x00550009,
/// <summary>dest = BLACK</summary>
BLACKNESS = 0x00000042,
/// <summary>dest = WHITE</summary>
WHITENESS = 0x00FF0062,
/// <summary>
/// Capture window as seen on screen. This includes layered windows
/// such as WPF windows with AllowsTransparency="true"
/// </summary>
CAPTUREBLT = 0x40000000
}
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, TernaryRasterOperations dwRop);
[DllImport("gdi32.dll")]
static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
[DllImport("gdi32.dll", ExactSpelling = true, PreserveSig = true, SetLastError = true)]
static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("gdi32.dll")]
static extern bool DeleteDC(IntPtr hdc);
[DllImport("user32.dll")]
static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll")]
static extern bool DeleteObject(IntPtr hObject);
public static void ScreenshotWindow(IntPtr windowHandle)
{
Rect Rect = new Rect();
GetWindowRect(windowHandle, ref Rect);
int width = Rect.Right - Rect.Left;
int height = Rect.Bottom - Rect.Top;
IntPtr windowDeviceContext = GetWindowDC(windowHandle);
IntPtr destDeviceContext = CreateCompatibleDC(windowDeviceContext);
IntPtr bitmapHandle = CreateCompatibleBitmap(windowDeviceContext, width, height);
IntPtr oldObject = SelectObject(destDeviceContext, bitmapHandle);
BitBlt(destDeviceContext, 0, 0, width, height, windowDeviceContext, 0, 0, TernaryRasterOperations.CAPTUREBLT | TernaryRasterOperations.SRCCOPY);
SelectObject(destDeviceContext, oldObject);
DeleteDC(destDeviceContext);
ReleaseDC(windowHandle, destDeviceContext);
Image screenshot = Image.FromHbitmap(bitmapHandle);
DeleteObject(bitmapHandle);
screenshot.Save("d:\\temp\\mywebcamimage.png", System.Drawing.Imaging.ImageFormat.Png);
/*
// TODO - Remove above save when it works
using (MemoryStream stream = new MemoryStream())
{
screenshot.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
return stream.ToArray();
}
*/
}
And then this after SendMessage(hCaptureWnd, WM_CAP_GET_FRAME, 0, 0);
ScreenshotWindow(new IntPtr(hCaptureWnd));
There is no such thing as WM_CAP_GET_FRAME
. The correct name of the message is WM_CAP_GRAB_FRAME
and it's described on MSDN.
What it does is:
The WM_CAP_GRAB_FRAME message retrieves and displays a single frame from the capture driver. After capture, overlay and preview are disabled. You can send this message explicitly or by using the capGrabFrame macro.
To obtain the actual data you need to use frame callback as described further on MSDN. The callback gets you the picture bytes, which you can write to file or use for whatever processing without need to transfer through clipboard.
...is the callback function used with streaming capture to optionally process a frame of captured video. The name capVideoStreamCallback is a placeholder for the application-supplied function name.
[And you have there a] ... Pointer to a VIDEOHDR structure containing information about the captured frame.
Again, this API is unlucky choice for video capture. Too old, too limited.
You have to send a different message, specifically WM_CAP_FILE_SAVEDIB
, to save data in a file on disk. You then'll be able to load it in a Bitmap
object for further processing (I'm not aware of any builtin cam-to-byte[] functionality).
[DllImport("user32", EntryPoint = "SendMessage")]
private static extern int SendMessage(
int hWnd, uint Msg, int wParam, string strFileName);
private const int WM_USER = 0x0400;
private const int WM_CAP_START = WM_USER;
private const int WM_CAP_FILE_SAVEDIB = WM_CAP_START + 25;
//before
SendMessage(hCaptureWnd, WM_CAP_COPY, 0, 0);
//after
string tempFile = Server.MapPath("~/App_Data/tempCap.bmp");
SendMessage(hCaptureWnd, WM_CAP_FILE_SAVEDIB, 0, tempFile); //create tempfile
Bitmap bitmap = new Bitmap(tempFile); //read tempfile
using (MemoryStream stream = new MemoryStream())
{
bitmap.Save(stream, ImageFormat.Bmp);
return stream.ToArray();
}
Building on Roman R.'s answer:
The finer point of morality is that you need to register the callback frame, and then call grabframe, and that you cannot directly cast C-style char[] to byte[], and that you get raw bitmap data - not a bitmap, and that the image size is 640x480, irrespective of what is set in capCreateCaptureWindowA, and that lpData needs to be a IntPtr, not a UIntPtr, because Marshal.Copy has no overload for UIntPtr, and that using WriteBitmapFile, it's possible to write raw bitmap data into a bitmap WITHOUT using unsafe code or mapping the bitmap file headers, and that whoever wrote Marshal.Copy made it possible to copy a negative value, because length is int, not uint...
Additionally, there is a need to rotate the image 180 oldDegrees for whatever reason...
Also, I changed the WM constants to their correct names.
SendMessage(hCaptureWnd, WM_CAP_SET_CALLBACK_FRAME, 0, capVideoStreamCallback);
SendMessage(hCaptureWnd, WM_CAP_GRAB_FRAME, 0, 0); // capture the frame
With these additional things
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd757688(v=vs.85).aspx
[StructLayout(LayoutKind.Sequential)]
private struct VIDEOHDR
{
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx
// typedef unsigned char BYTE;
// typedef BYTE far *LPBYTE;
// unsigned char* lpData
//public byte[] lpData; // LPBYTE lpData; // Aaargh, invalid cast, not a .NET byte array...
public IntPtr lpData; // LPBYTE lpData;
public UInt32 dwBufferLength; // DWORD dwBufferLength;
public UInt32 dwBytesUsed; // DWORD dwBytesUsed;
public UInt32 dwTimeCaptured; // DWORD dwTimeCaptured;
// typedef ULONG_PTR DWORD_PTR;
// #if defined(_WIN64)
// typedef unsigned __int64 ULONG_PTR;
// #else
// typedef unsigned long ULONG_PTR;
// #endif
public IntPtr dwUser; // DWORD_PTR dwUser;
public UInt32 dwFlags; // DWORD dwFlags;
[System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst = 4)]
public System.UIntPtr[] dwReserved; // DWORD_PTR dwReserved[4];
// Does not make a difference
//public System.UIntPtr[] dwReserved = new System.UIntPtr[4]; // DWORD_PTR dwReserved[4];
}
private delegate System.IntPtr capVideoStreamCallback_t(System.UIntPtr hWnd, ref VIDEOHDR lpVHdr);
[DllImport("user32", EntryPoint = "SendMessage")]
private static extern int SendMessage(int hWnd, uint Msg, int wParam, capVideoStreamCallback_t routine);
// http://eris.liralab.it/yarpdoc/vfw__extra__from__wine_8h.html
private const int WM_USER = 0x0400; // 1024
private const int WM_CAP_START = WM_USER;
private const int WM_CAP_DRIVER_CONNECT = WM_CAP_START + 10;
private const int WM_CAP_DRIVER_DISCONNECT = WM_CAP_START + 11;
private const int WM_CAP_FILE_SAVEDIB = WM_CAP_START + 25;
private const int WM_CAP_SET_CALLBACK_FRAME = WM_CAP_START + 5;
private const int WM_CAP_GRAB_FRAME = WM_CAP_START + 60;
private const int WM_CAP_EDIT_COPY = WM_CAP_START + 30;
// http://lists.ximian.com/pipermail/mono-devel-list/2011-March/037272.html
private static byte[] baSplendidIsolation;
private static System.IntPtr capVideoStreamCallback(System.UIntPtr hWnd, ref VIDEOHDR lpVHdr)
{
//System.Windows.Forms.MessageBox.Show("hello");
//System.Windows.Forms.MessageBox.Show(lpVHdr.dwBufferLength.ToString() + " " + lpVHdr.dwBytesUsed.ToString());
byte[] _imageTemp = new byte[lpVHdr.dwBufferLength];
Marshal.Copy(lpVHdr.lpData, _imageTemp, 0, (int) lpVHdr.dwBufferLength);
//System.IO.File.WriteAllBytes(@"d:\temp\mycbfile.bmp", _imageTemp); // AAaaarg, it's raw bitmap data...
// http://stackoverflow.com/questions/742236/how-to-create-a-bmp-file-from-byte-in-c-sharp
// http://stackoverflow.com/questions/2654480/writing-bmp-image-in-pure-c-c-without-other-libraries
// Tsssss... 350 x 350 was the expected setting, but never mind...
// fortunately alex told me about WM_CAP_FILE_SAVEDIB, so I could compare to the direct output
int width = 640;
int height = 480;
int stride = width*3;
baSplendidIsolation = null;
baSplendidIsolation = WriteBitmapFile(@"d:\temp\mycbfilecc.bmp", width, height, _imageTemp);
/*
unsafe
{
fixed (byte* ptr = _imageTemp)
{
using (Bitmap image = new Bitmap(width, height, stride, PixelFormat.Format24bppRgb, new IntPtr(ptr)))
{
image.Save(@"d:\temp\mycbfile2.bmp");
}
}
}
*/
//var hdr = (Elf32_Phdr)Marshal.PtrToStructure(ptr, typeof(Elf32_Phdr));
return System.IntPtr.Zero;
}
private static byte[] WriteBitmapFile(string filename, int width, int height, byte[] imageData)
{
using (var stream = new MemoryStream(imageData))
using (var bmp = new Bitmap(width, height, PixelFormat.Format24bppRgb))
{
BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0,bmp.Width, bmp.Height)
,ImageLockMode.WriteOnly
,bmp.PixelFormat
);
Marshal.Copy(imageData, 0, bmpData.Scan0, imageData.Length);
bmp.UnlockBits(bmpData);
if (bmp == null)
return null;
bmp.RotateFlip(RotateFlipType.Rotate180FlipNone);
bmp.Save(filename); // For testing only
using (MemoryStream ms = new MemoryStream())
{
bmp.Save(ms, ImageFormat.Png); // get bitmap bytes
return ms.ToArray();
} // End Using stream
}
} // End Function WriteBitmapFile
/// <summary>
/// Captures a frame from the webcam and returns the byte array associated
/// with the captured image
/// </summary>
/// <param name="connectDelay">number of milliseconds to wait between connect
/// and capture - necessary for some cameras that take a while to 'warm up'</param>
/// <returns>byte array representing a bitmp or null (if error or no webcam)</returns>
private static byte[] InternalCaptureToByteArray(int connectDelay = 500)
{
Clipboard.Clear();
int hCaptureWnd = capCreateCaptureWindowA("ccWebCam", 0, 0, 0,
350, 350, 0, 0); // create the hidden capture window
SendMessage(hCaptureWnd, WM_CAP_DRIVER_CONNECT, 0, 0); // send the connect message to it
//SendMessage(hCaptureWnd, WM_CAP_DRIVER_CONNECT, i, 0); // i device number retval != 0 --> valid device_id
Thread.Sleep(connectDelay); // sleep the specified time
SendMessage(hCaptureWnd, WM_CAP_SET_CALLBACK_FRAME, 0, capVideoStreamCallback);
SendMessage(hCaptureWnd, WM_CAP_GRAB_FRAME, 0, 0); // capture the frame
//SendMessage(hCaptureWnd, WM_CAP_FILE_SAVEDIB, 0, "d:\\temp\\testmywebcamimage.bmp");
//ScreenshotWindow(new IntPtr(hCaptureWnd));
//SendMessage(hCaptureWnd, WM_CAP_EDIT_COPY, 0, 0); // copy it to the clipboard
// using (Graphics g2 = Graphics.FromHwnd(new IntPtr(hCaptureWnd)))
SendMessage(hCaptureWnd, WM_CAP_DRIVER_DISCONNECT, 0, 0); // disconnect from the camera
return baSplendidIsolation;
/*
Bitmap bitmap = (Bitmap)Clipboard.GetDataObject().GetData(DataFormats.Bitmap); // copy into bitmap
if (bitmap == null)
return null;
using (MemoryStream stream = new MemoryStream())
{
bitmap.Save(stream, ImageFormat.Bmp); // get bitmap bytes
return stream.ToArray();
} // End Using stream
*/
} // End Function InternalCaptureToByteArray
User contributions licensed under CC BY-SA 3.0