[原创]摄像头娘来了

被标题吸引进来的各位,抱歉,这还是一篇技术文章crying
本文的主角是一个摄像头及其配套程序,我毫无创意地模仿网上的流行说法,称之为“摄像头娘”。她会自己发微博,记得follow她哦t.qq.com/cameragirl

前段时间浙大有个博士做了个“饮水机娘”,也就是他实验室里的饮水机没水或者水加热开了的时候,都会自动发一条微博。这个与“物联网”扯上了关系的东西,让我原来以为那位博士是改造了饮水机,加上了温度及水位传感器,配合单片机,将饮水机的状态采集到PC中去,再写一个PC上的软件将触发的事件发送到微博上,后来看了相关的介绍,才知道没那么复杂,其实他没有改造饮水机,而是用一个摄像头对准饮水机的状态指示灯,根据指示灯的情况,来判断饮水机的状态。

于是我也就萌生了做一个“摄像头娘”的念头。
我构想中的“摄像头娘”是这样的:当有物体在她面前运动的时候,她能感受得到,并且会为这个过客拍一张照片,然后发到微博上。
下面是实际效果截图

好吧,说得傻一点就是:一个有运动检测(motion detection)功能的程序,当检测到摄像头前有物体在运动时,就将画面拍一张照片,发到微博上。
确实,没啥创意,但是还是有一定实用性的。例如,你可以在家里放置一个“她”为你看家,假设你运气不好,有不速之客闯进了你家里,那么,只要他进入了“摄像头娘”的视角,就会马上被检测到,然后被拍上来,发到微博上,后面的事,哼哼,你就有第一手证据了。
文章来源:http://www.codelast.com/
我是用VC++ 2005开发的这个软件,运行于Win7上,其实它很简单,功能主要分为两部分:运动检测(motion detection)& 发微博,下面分别陈述。
【1】运动检测 / motion detection
从头自己写?那得多笨才能这样做啊!而且我也不是干视频处理这一行的,一时半会写不出来这玩意。
久闻OpenCV的大名,我知道它可以帮我实现想要的功能,所以毫不犹豫地选择了它。
我以前也从未使用过OpenCV,从下载它的开发包开始,到参照网上的demo写出一个可用的motion detection程序,只花了半天多时间,由此可见OpenCV的强大。
关于OpenCV的介绍,这里只摘取其官网的一句话:

OpenCV (Open Source Computer Vision) is a library of programming functions for real time computer vision.

没错,大名鼎鼎的机器视觉开发库。
我使用的版本是2.3.1,下载回来之后会看到,它里面已经没有VC++2005的lib了,只有2008及2010的(2005确实太老了),但是试验之后会发现,在VC++ 2005中用2008的lib完全没问题。
文章来源:http://www.codelast.com/
运动检测的基本原理是:获取摄像头的视频流→在很短的时间内连续抓取两帧图像→计算两幅图像的差值→计算直方图→判断某些指标是否超过了阈值→触发动作。我没有图像处理的编码经验,所以也只能解释到这个业余的水平了。
这种功能的代码在网上比比皆是,不过,很多都有陷阱:内存泄漏。基本上来说,都是错误使用cvCloneImage函数造成的。此函数如果放在一个循环中,会导致一次吃掉几M内存(对分辨率不高的图像来说),就算使用了cvReleaseImage函数来释放内存,似乎也没用——应该是对OpenCV的熟悉度不足造成的,用法不对。网上一搜索,就会发现无数人都遇到了这样的问题,那些一篇又一篇的转载文章几乎都是说用同一种方法:以cvCopy来代替cvCloneImage实现同样的功能。
我就是用这种方法解决了内存泄漏问题的,不过,在换成cvCopy之后还有许多其他的问题,一言难尽,看我下面的代码就知道正确的做法了。总之一句话:通过不断Google是可以搞定的这些问题的。
下面,就来看看motion detection的核心代码吧(使用的OpenCV库文件为 opencv_core231.libopencv_highgui231.libopencv_imgproc231.lib):

/**
 * Author: Darran Zhang @ codelast.com
 * Date: 2012-01-27
 */

CvCapture* pCapture = cvCreateCameraCapture(0);  // 初始化摄像头 

if(NULL == pCapture) {
  MessageBox(_T("检测不到摄像头!"), _T("错误"), MB_ICONERROR);
  return;
}

string strCameraWindowName = "摄像头";   // 窗口标题 
string strDiffWindowName = "图像差值";   // 窗口标题 
cvNamedWindow(strCameraWindowName.c_str(), CV_WINDOW_AUTOSIZE); // 创建一个窗口,第二个参数使得用户不能手动改变窗口大小 
cvNamedWindow(strDiffWindowName.c_str(), CV_WINDOW_AUTOSIZE);

IplImage *pFrame = cvQueryFrame(pCapture);      // 随意获取一帧,这是为了通过这一帧取到其宽、高、像素位深、通道数,否则就无法正确创建其他帧图像 
IplImage *pFrameA = NULL;                       // 其中一帧 
IplImage *pFrameB = cvCreateImage(cvSize(pFrame->width, pFrame->height), pFrame->depth, pFrame->nChannels);     // 其中一帧 
IplImage *pFrameSub = cvCloneImage(pFrameB);    // A、B帧相减之后的帧 

int nDims = 256;     // 划分HIST的个数,越高越精确 
float hRangesArr[] = {0, 255};
float* hRanges = hRangesArr;

IplImage *pGrayscaleImage = NULL;  // 灰度图 
CvHistogram *pHist = cvCreateHist(1, &nDims, CV_HIST_ARRAY, &hRanges, 1);   // 创建直方图 
float fMaxValue = 0.0;

time_t ts = 0;   // 记录时间戳,用于防止在1秒内多次触发运动检测事件 

m_bActive = true;
while(m_bActive)
{
  pFrameA = cvQueryFrame(pCapture);  // 注意:cvQueryFrame返回的指针总是指向同一块内存 
  if(!pFrameA) {
    m_stcSD.strLatestErrMsg = _T("无法抓取视频帧");
    SendStatDataUpdateMsg();

    break;
  }

  cvAbsDiff(pFrameB, pFrameA, pFrameSub); // 计算两幅图像之差 
  cvCopy(pFrameA, pFrameB);               // 拷贝图像,第一个参数为源,第二个参数为目标 

  /* 显示摄像头图像 */
  cvMoveWindow(strCameraWindowName.c_str(), 150, 50);   // 设定窗口位置(x,y坐标) 
  cvShowImage(strCameraWindowName.c_str(), pFrameB);    // 显示图像,第2个参数指定了要显示的图像 

  /* 显示差值图像 */
  cvMoveWindow(strDiffWindowName.c_str(), 150, 400);
  cvShowImage(strDiffWindowName.c_str(), pFrameSub);

  /* 转换图像并计算直方图 */
  pGrayscaleImage = cvCreateImage(cvGetSize(pFrameSub), IPL_DEPTH_8U, 1);   // 创建灰度图 
  cvCvtColor(pFrameSub, pGrayscaleImage, CV_BGR2GRAY);                      // 将彩色图像转换为灰阶图像 
  cvCalcHist(&pGrayscaleImage, pHist, 0, 0);                                // 计算直方图 

  /* 判断阈值是否超限,若超限则触发动作 */
  fMaxValue = 0.0;
  cvGetMinMaxHistValue(pHist, 0, &fMaxValue, 0, 0);  // 找最大值,保存到fMaxValue中,第2个参数是最小值,不过我们不用 
  cvConvertScale(pHist->bins, pHist->bins, (fMaxValue ? (255.0 / fMaxValue) : 0.0), 0); // 缩放 bin 到区间 [0, 255] 

  double dRealtimeVal = cvGetReal1D(pHist->bins, 10);
  if (dRealtimeVal > m_dDetectThreshold) {    // 判断是否大于预先设定的阈值 
    CTime ct = CTime::GetCurrentTime();
    time_t tsRef = ct.GetTime();
    if (tsRef - ts >= 1) {
	  //TODO: 在此触发动作 
      ts = tsRef;
    }
  }

  cvReleaseImage(&pGrayscaleImage);   // 释放内存 
  pGrayscaleImage = NULL;

  cvWaitKey(10);   // 等待若干毫秒 
}

cvReleaseCapture(&pCapture);   // 停止捕获并释放摄像头资源 
cvReleaseHist(&pHist);
cvReleaseImage(&pFrameB);
cvReleaseImage(&pFrameSub);

pCapture = NULL;
pHist = NULL;
pFrameB = NULL;
pFrameSub = NULL;
pFrame = NULL;
pFrameA = NULL;

cvDestroyWindow(strCameraWindowName.c_str());    // 销毁窗口 
cvDestroyWindow(strDiffWindowName.c_str());

文章来源:http://www.codelast.com/
其中有几处要特别注意的代码:
(1)IplImage *pFrameB = cvCreateImage(cvSize(pFrame->width, pFrame->height), pFrame->depth, pFrame->nChannels);
创建B帧图像的时候,后面的参数都是通过pFrame来获取的,如果这几个参数你随意填写,那么就会导致程序编译可通过,运行时崩溃。所以我前面先抓取了一帧,再通过这一帧来获取B帧图像的参数,这样就完全符合了。
(2)cvQueryFrame函数返回的指针总是指向同一块内存——如果你不知道这一点,并且B帧图像是通过cvQueryFrame获取的,那么可能你调试了半天,却发现A帧、B帧的内容始终相同,二者相减得到的结果永远不变,motion detection就不起作用了。所以我把B帧图像用cvCreateImage来创建,A帧用cvQueryFrame来获取,这样就没有问题了。
(3)由于两帧的检测时间较短,所以可能在多个瞬间触发多次动作,为此我根据时间戳做了一些简单的处理,来防止同一秒内触发多次动作。
(4)其他的代码,各位看看就明白了,也无需过多的解释。

【2】发微博
我选择的是腾讯微博。
不得不承认,腾讯微博对C++开发者的支持不足,下载回来的开发包里,仅仅同音的错别字就有一堆(可见开发API的同学用的是拼音输入法)。另外,还有各种各样的问题,例如,API Example是无法编译的——原因是ApiType.cpp 和 ApiType.h没有被添加进工程中,导致找不到 CApiType 类定义。再看看它的代码中,编写风格非常随意,让人看了非常难受,总之就一句话,这API你就凑合着用吧!
在动手写程序发微博之前,需要先在腾讯的网页上申请成为开发者,否则你的程序是无法用微博API来发微博的。申请的过程非常简单,当然你必须先有一个QQ号,登录后,在这个页面申请。申请之后,你需要在腾讯的网页上点击创建一个客户端应用——尽管这个时候,你还连程序都没有开始写,但是这是先决条件,不这样做的话,你就算写好了程序也用不了,所以我就把这个过程先全部做好了,再开始写程序的事。
文章来源:http://www.codelast.com/
创建客户端应用的页面如下图所示:

create a client app

点击之后只需要填写少许内容就可以创建一个客户端应用了。
创建应用之后,你可以看到你所创建的应用的App Key(一串数字)和App Secret(数字和字母的混合),这两个字符串在你写程序发微博的时候会用到:

文章来源:http://www.codelast.com/
在获取了以上所需的信息后,用API发微博的过程大致可以描述为:通过App Key和App Secret跳转到腾讯的页面去获取oauth_verifier→用户人工输入oauth_verifier→通过oauth_verifier获取Access Key和Access Secret→授权通过,可以发微博了
这个过程的demo,在腾讯微博的API Example中很详细地作了演示,不过,它的编码风格非常不规范,乱就一个字,会耽误开发者的时间。
正因为腾讯微博API Example已经有了例子,所以我在这里就不把发微博相关的代码全部放上来了,只是提醒大家注意以下几点:
(1)你的VC++工程中,只需要引入腾讯微博API中的一个库文件:QWBlogAPI.lib。
(2)你的VC++工程需要以下头文件:

JsonParser.h
UtilString.h
WeiboApi.h
WeiboParam.h
XmlParser.h
weibo.h

(3)你的程序运行时,需要以下dll:

curllib.dll
libeay32.dll
openldap.dll
QWBlogAPI.dll
ssleay32.dll

这些文件在腾讯微博开发包中都能找到。
(4)发普通微博,option是用TXWB_T_ADD;发带图片的微博,option是用TXWB_T_ADD_PIC。图片作为参数添加到CWeiboParam对象中时,是需要特殊处理的(详见API Example)。
(5)微博发不出去时,一定要检查发送内容中的“content”的字符串是什么编码的!别以为字符串里有内容就是对的,我在这上面折腾了不少时间,就是没注意这个问题。可以用API Example中的Unicode2Mbcs函数来转换一个CString到合适的字符串,很管用。
(6)oauth_verifier的值,是在腾讯网页的地址中获取到的,而不是在网页正文中。你跳转到类似于如下的网址后(其实就是腾讯首页):

http://www.qq.com/?oauth_token=XXX&oauth_verifier=XXX&openid=&openkey=

就可以肉眼识别出其值了。
文章来源:http://www.codelast.com/
【3】软件UI
UI很简单,但是调起来烦人。就截几幅图吧:


 

文章来源:http://www.codelast.com/
好了,先暂时到这吧,在折腾的道路上...

文章来源:https://www.codelast.com/
➤➤ 版权声明 ➤➤ 
转载需注明出处:codelast.com 
感谢关注我的微信公众号(微信扫一扫):

wechat qrcode of codelast

《[原创]摄像头娘来了》有8条评论

  1. 判断某些指标是否超过了阈值→触发动作
    double dRealtimeVal = cvGetReal1D(pHist->bins, 10);
    if (dRealtimeVal > detectThreshold)
    为什么就用了10指向的像素值来判断 ,我知道程序可以工作 但是为什么选了10,难道是随便选的?
    不好意思 ,好像有点挖坟贴

    回复

回复 learnhard 取消回复