缘由:作为一枚还没出学校的实习僧,进入公司接手的第一个项目,没人带,没用过CVI,而且公司还没人做过,幸好作为一个现代人还有互联网。从一无所知接手到写完这个测试程序,接近花了我3周左右的时间。我认为很有意义,实际项目要比这复杂,这只是其中一个功能而已。
其中两人的博客的资料对我来说较为重要,影响着整个编程。
1. 小信:基于LabWindows/CVI的摄像头控制技术--实现屏幕截图、录像功能 博客源地址:http://blog.sina.com.cn/s/blog_4b677075010006da.html
2. 醉囧里囧梦囧回:LabWindows/CVI入门之第六章:综合实例:远程监控系统 博客源地址:http://blog.sina.com.cn/s/blog_6373e9e60101cetk.html
(我有部分是借鉴醉囧里囧梦囧回的编程思路,但有不一样,他所用的是通过网页实现图像显示,而我是通过TCP服务端和客户端实现图像显示)
一、任务描述:
1.图像采集(在CVI摄像头控制技术里有较为详细的介绍)
2.图像传输(将图像数据通过Base64转码,将Base64编码通过TCP有客户端发送给服务端)
3.图像接收并显示(将接收数据,通过Base64解码,转换为图像数据保存显示)
注:因为我在查询图片转发时,最开始查询到的方法是直接将图片转换为二进制数据发送就行,但实际操作中数据转发和接收到的数据转成图片时,图片不能正常生成。所以最好选择base64转码解决这个问题。
界面效果:
二、系统详细设计:
(1)图像采集
我所用USB摄像头为免驱摄像头,“免驱”的意思并不是说这种摄像头不需要驱动即可运行,而是因为目前市面上主流的操作系统中已经包含了该摄像头所需要的驱动程序,当摄像头连接计算机后无需额外安装驱动程序。
考虑到普通摄像头加载驱动时均加载avicap32.dll,而且使用AVICAP32开发复杂度较低,所以就才用AVICAP32进行开发。需要在程序中引入avicap32的动态链接库。
UI设计:
1)摄像头初始化打开:
int Display_width;
int Display_height;
int Display_top;
int Display_left;
int i;
//获得面板句柄,用于在调用摄像头函数时,需要使用的句柄handle
GetPanelAttribute (panelHandle, ATTR_SYSTEM_WINDOW_HANDLE, &handle);
//获得屏幕中图像要显示的位置
GetCtrlAttribute (panelHandle, PANEL_CANVAS_DISPLAY, ATTR_LEFT, &Display_left);
GetCtrlAttribute (panelHandle, PANEL_CANVAS_DISPLAY, ATTR_TOP, &Display_top);
GetCtrlAttribute (panelHandle, PANEL_CANVAS_DISPLAY, ATTR_HEIGHT, &Display_height);
GetCtrlAttribute (panelHandle, PANEL_CANVAS_DISPLAY, ATTR_WIDTH, &Display_width);
//打开摄像头
result = capCreateCaptureWindowA("",WS_CHILD | WS_VISIBLE ,Display_left,Display_top,Display_width,Display_height,handle,0);
//设置摄像头函数
SendMessage((HWND)result, WM_CAP_SET_CALLBACK_VIDEOSTREAM, 0, 0);
SendMessage((HWND)result, WM_CAP_SET_CALLBACK_ERROR, 0, 0);
SendMessage((HWND)result, WM_CAP_SET_CALLBACK_STATUSA, 0, 0);
for(i=0;i<3;i++)
{
SendMessage((HWND)result, WM_CAP_DRIVER_CONNECT, 0, 0);
}
SendMessage((HWND)result, WM_CAP_SET_SCALE, 1, 0);
SendMessage((HWND)result, WM_CAP_SET_PREVIEWRATE, 66, 0);
SendMessage((HWND)result, WM_CAP_SET_OVERLAY, 1, 0);
SendMessage((HWND)result, WM_CAP_SET_PREVIEW, 1, 0);
注:在开启摄像头中出现过摄像头指示灯亮,但界面上一片漆黑,没有图像。原因为驱动没有调用。多调用几次图像就会出现。也就是源代码中的:SendMessage((HWND)result, WM_CAP_DRIVER_CONNECT, 0, 0);
2)截图操作
int CVICALLBACK CapOneSnap (int handle, int result, int message, unsigned int* wParam, unsigned int* lParam, void* callbackData)
{
char bmpFilePath[MAX_PATHNAME_LEN ]="";
char jpgFilePath[MAX_PATHNAME_LEN ]="";
int bitmap_ID;
switch(message)
{
case (WM_USER+1009):
//do some things.
GetDir(bmpFilePath);
GetDir(jpgFilePath);
sprintf(bmpFilePath,"%s\\%u.bmp",bmpFilePath,(unsigned long)(*wParam));
sprintf(jpgFilePath,"%s\\%u.jpg",jpgFilePath,(unsigned long)(*wParam));
SendMessage((HWND)result,WM_CAP_SAVEDIB,0,(LPARAM)bmpFilePath);
GetBitmapFromFile ( bmpFilePath , &bitmap_ID );
SaveBitmapToJPEGFile(bitmap_ID,jpgFilePath,JPEG_DCTFAST,90);
DiscardBitmap(bitmap_ID);
DeleteFile(bmpFilePath);
break;
case EVENT_NEWHANDLE :
handle=*wParam;
break;
}
return 0;
}
注:功能是保存屏幕图像数据为一张位图,这是CVI自带的函数,然后将位图保存为jpg图片(jpg是所知压缩率最大的图片格式,大约为一张1M左右的位图转换为50K左右的jpg图片)并删除位图。返回图片编号wParam。
3)将jpg图片转换为base64编码
int GetCap(int panel,int result, float* eclipsTime,unsigned char* base64Content)
{
double startTime;
char jpgFIlePath[MAX_PATHNAME_LEN]="";
unsigned int randNum=GetRandNumber();
unsigned char* fileContent;
int fileLength;
FILE* fp;
startTime=Timer();
CapOneSnap (panel,result, WM_USER+1009, &randNum , 0,NULL);
GetDir(jpgFIlePath);
sprintf(jpgFIlePath,"%s\\%u.jpg",jpgFIlePath,(unsigned int)(randNum));
if((fp = fopen(jpgFIlePath,"rb")) == NULL)
{
*eclipsTime=1000*(Timer()-startTime);
return -1;
}
fileContent=(unsigned char*)malloc(921654);
fileLength=fread(fileContent,1,921654,fp);
fclose(fp);
DeleteFile(jpgFIlePath);
if(fileLength==0)
{
*eclipsTime=1000*(Timer()-startTime);
return -2;
}
fnBase64Encode(fileContent,base64Content,fileLength);
free(fileContent);
*eclipsTime=1000*(Timer()-startTime);
return 0;
}
//图片进行base64编码
//返回base64编码值
const char BASE_CODE[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
int fnBase64Encode(unsigned char *lpString,unsigned char *lpBuffer, int sLen)
{
int vLen = 0;
while(sLen > 0)
{
*lpBuffer++ = BASE_CODE[(lpString[0] >> 2 ) & 0x3F];
if(sLen > 2)
{
*lpBuffer++ = BASE_CODE[((lpString[0] & 3) << 4) | (lpString[1] >> 4)];
*lpBuffer++ = BASE_CODE[((lpString[1] & 0xF) << 2) | (lpString[2] >> 6)];
*lpBuffer++ = BASE_CODE[lpString[2] & 0x3F];
}
else
{
switch(sLen)
{
case 1:
*lpBuffer ++ = BASE_CODE[(lpString[0] & 3) << 4 ];
*lpBuffer ++ = '=';
*lpBuffer ++ = '=';
break;
case 2:
*lpBuffer ++ = BASE_CODE[((lpString[0] & 3) << 4) | (lpString[1] >> 4)];
*lpBuffer ++ = BASE_CODE[((lpString[1] & 0x0F) << 2) | (lpString[2] >> 6)];
*lpBuffer ++ = '=';
break;
}
}
lpString += 3;
sLen -= 3;
vLen +=4;
}
//*lpBuffer = 0;
*lpBuffer = '\0';
return vLen;
}
注:图片与base64编码之间的相互转换,在网上有很多实例。我这里就不具体介绍,自己去了解。代码中还有一个算延迟时间的(eclipsTime),我这里没用,但保留了这个。
(2)图像传输:
我这里是图像采集这边是客户端,图像接收端是服务器端。
客户端:
int TcpFilishedOk=0;
int CVICALLBACK ClientCallback(unsigned int handle,int event,int error,void* callbackData)
{
char ss[ONE_POCKET_SIZE+1];
int rdSize=0;
unsigned char* base64Content;
float eclipsTime=0;
switch(event)
{
case TCP_DATAREADY: //收到数据
rdSize=ClientTCPRead(conversationHandle,ss,ONE_POCKET_SIZE,2000);
ss[rdSize]=0;
if(strcmp(ss,"CAPTURE") == 0)
{
ClientTCPWrite(handle,"image=",7,2000);
TcpFilishedOk=0;
}
if(strcmp(ss,"askok") == 0)
{
base64Content=(unsigned char*)malloc(999999);
if(GetCap(panelHandle,result,&eclipsTime,base64Content)>=0)
{
//发送图片base64编码
if(ClientTCPWrite(handle,base64Content,strlen(base64Content),20000)<0)
{
MessagePopup("提示","Write imageInfo failed.");
}
else
{
TcpFilishedOk=1;
}
}
else
{
MessagePopup("提示","Open JPGFile failed.");
sprintf(ss,"({\n\"errorCode\":1,\n\"eclipsTime\":%4.2f,\n\"image\":null\n})",eclipsTime);
if(ClientTCPWrite(handle,ss,strlen(ss),2000)<0)
{
MessagePopup("提示","Write image NULL failed.");
}
}
//释放base64Content分配空间
free(base64Content);
//清空ClientTCPRead()中缓冲区buffer的数据防止出错
while(rdSize>=ONE_POCKET_SIZE)//把没读出来的数据都读出来防止再次DATAREADY事件
{
rdSize=ClientTCPRead(handle,ss,ONE_POCKET_SIZE,2000);
}
//DisconnectTCPClient (handle);
}
if(TcpFilishedOk)
{
//ClientTCPWrite(handle,"结束",5,2000);
TcpFilishedOk=0;
}
break;
case TCP_DISCONNECT: //断开连接
//ShowState("Connection disconnected!");
sprintf(ss,"Connection disconnected!");
MessagePopup("提示",ss);
break;
}
return 0;
}
服务器端:
int CVICALLBACK ServerCallback(unsigned int handle,int event, int error,void* callbackData)
{
char ss[ONE_POCKET_SIZE+1];
int rdSize=0;
unsigned char* base64Content;
float eclipsTime=0;
switch(event)
{
case TCP_CONNECT: //建立连接
//
convHandle=handle;
sprintf(ss,"Connection %d established!",handle);
MessagePopup("提示",ss);
break;
case TCP_DISCONNECT:
//
sprintf(ss,"Connection %d disconnected!",handle);
MessagePopup("提示",ss);
break;
case TCP_DATAREADY: //收到数据
if(camera_ok)
{
CAMERA();
}
if(monitor_ok)
{
MONITOR();
}
break;
}
return 0;
}
注:在我的程序中所谓的监控就是在有效的时间内多次采集图片数据并在远程端显示,远程监控在没有足够的网速和高效的图像压缩算法的条件下,实现起来是不现实的,我尝试过发送流媒体,无论是CVI自身保存的视频(CVI保存的流媒体太大,二三十秒数据,大约占据了我1G的存储位置)还是我们下载下来的“清晰”视频,效率太低。或者直接无法传输。我这边暂时还没有解决。
(3)图像接收并显示
//摄影
int CAMERA (void)
{
static char ss[ONE_POCKET_SIZE];
int rdSize=0;
if((rdSize=ServerTCPRead(convHandle,ss,ONE_POCKET_SIZE,2000))<0)
printf("Acquire data failed!\n");
else
{
if(strcmp(ss,"image=") == 0)
{
ServerTCPWrite(convHandle,"askok",6,2000);
}
else
{
ShowState("图片数据接收中。。");
if(Decode(ss,"base64.jpg"))
{
Delay(0.5);
//显示图片到PICTURE控件上
DisplayImageFile(panelHandle,PANEL_PICTURE,"base64.jpg");
//ShowState("图片正在显示。。。。。");
TM_OK_FLAG=1;
}
}
}
return 1;
}
//监控
int threadID[26];
int MONITOR(void)
{
static char ss[ONE_POCKET_SIZE];
int rdSize=0;
if(monitor_ok)
{
if((rdSize=ServerTCPRead(convHandle,ss,ONE_POCKET_SIZE,2000))<0)
printf("Acquire data failed!\n");
else
{
if(strcmp(ss,"image=") == 0)
{
ServerTCPWrite(convHandle,"askok",6,2000);
// 发送允许:发送base64编码
}
else
{
if(MonitorCount == 26)
{
MonitorCount = 0;
}
Picture[MonitorCount].PictureFileName = PFile[MonitorCount].filename;
Picture[MonitorCount].PicturcBase64Data = ss;
PFile[MonitorCount].pfid = MonitorCount;
//创建一个新程函数接收图片信息并转码保存
CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, MonitorCount, &threadID[MonitorCount]);
//发送监控信息
Delay(0.55);
ServerTCPWrite(convHandle,"CAPTURE",8,2000);
MonitorCount++;
}
}
return 1;
}
return 1;
}
//线程:解码图片数据并显示
int CVICALLBACK ThreadFunction (int filenum)
{
if(Decode(Picture[filenum].PicturcBase64Data,PFile[filenum].filename))
{
// ShowState("图片正在显示。。。。。");
Delay(0.55);
DisplayImageFile(panelHandle,PANEL_PICTURE,PFile[filenum].filename);
TM_OK_FLAG=1;
}
return 0;
}
base64编码解码成图像数据并保存:
int Decode(char *base64,char *filename)
{
char *bindata;
FILE *fp;
fp = fopen(filename,"wb");
if(fp == 0)
{
//file is not
}
else
{
//ShowState("图片数据接收结束");
bindata=(char*)malloc(999999);
int bytes;
bytes = base64_decode( base64, bindata );
//ShowState("图片数据base64转码结束");
fwrite(bindata,bytes,1,fp);
free(bindata);
fclose(fp);
}
return 1;
}
//base64编码字符
const char * base64char = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
int base64_decode( const char * base64,char * bindata )
{
int i, j;
unsigned char k;
unsigned char temp[4];
for ( i = 0, j = 0; base64[i] != '\0' ; i += 4 )
{
memset( temp, 0xFF, sizeof(temp) );
for ( k = 0 ; k < 64 ; k ++ )
{
if ( base64char[k] == base64[i] )
temp[0]= k;
}
for ( k = 0 ; k < 64 ; k ++ )
{
if ( base64char[k] == base64[i+1] )
temp[1]= k;
}
for ( k = 0 ; k < 64 ; k ++ )
{
if ( base64char[k] == base64[i+2] )
temp[2]= k;
}
for ( k = 0 ; k < 64 ; k ++ )
{
if ( base64char[k] == base64[i+3] )
temp[3]= k;
}
bindata[j++] = ((unsigned char)(((unsigned char)(temp[0] << 2))&0xFC)) |
((unsigned char)((unsigned char)(temp[1]>>4)&0x03));
if ( base64[i+2] == '=' )
break;
bindata[j++] = ((unsigned char)(((unsigned char)(temp[1] << 4))&0xF0)) |
((unsigned char)((unsigned char)(temp[2]>>2)&0x0F));
if ( base64[i+3] == '=' )
break;
bindata[j++] = ((unsigned char)(((unsigned char)(temp[2] << 6))&0xF0)) |
((unsigned char)(temp[3]&0x3F));
}
return j;
}
注:图片数据必须写入完成后,方能显示在屏幕上,否则图像显示将显示一半或者直接花屏,但文件写入速度远不如读取速度,我用程序大概计算过,一张图片从写入到能显示大约为0.45s左右。这也是我为什么要在监控回调函数中开线程的原因,希望能在短时间内尽量显示多的图片同时又要求图像保存完整。
源码下载地址:http://download.csdn.net/detail/cb_869145753_hp/9736896
注意:我在程序中好像将客户端和服务器端相互调换过。但不影响程序和阅读