HOWTO: 在Macbook上DIY穷人的multi-touch (Part 3 – END)
Saturday, April 19th, 2008 | 浏览:27893人次这篇文章将接续这个系列Part 1及Part 2的说明向大家说明multi-touch背后软体运作的原理。这篇文章会尽量用简单的方式说明原理和实做,希望能让懂得基本程式设计的读者都能看懂。
这篇文章会说明怎么从摄影机中撷取图片,并取得手指所反射出的光点位置,并对每个点做追踪。下图是同时追踪四个手指的范例:
在上一篇已经介绍过我们所需要的硬体,包括红外线投射灯、红外线摄影机、及反光贴片。这些硬体很重要的功能是让我们双手的手指头能反射大量红外线,透过红外线滤光片的协助,我们的摄影机(iSight)就只会看到手指头产生的亮点,而不会看到其余和手指无关的背景。
上图为我们的摄影机所看到的画面,画面中除了手指上两块方形反光贴片外,其它什么都看不到。这样子的画面,对于电脑来说远比一般含有整个手掌、人、背景等图片还来得容易处理,而我们用红外线来替代原本的可见光的用意就在这里。
接下来,我们将开始解说怎么利用这些影像来取得手指的位置,进而追踪每只手指的移动过程。我们将利用C++写个程式出来分析这些影像,并将结果变成上层应用程式可以接受的event。整个程式需要做的事,简单说来可以分为以下这些步骤:
- 从摄影机撷取影像
- 从影像中找出每个亮点中心的位置
- 对于之后撷取到的每张画面追踪每个亮点位置的变化
- 产生对应的event,发送给上层的应用程式做处理
从摄影机撷取影像
在这边我推荐用Intel的OpenCV来做处理。OpenCV是非常有名的电脑视觉函式库,提供了一大票的电脑视觉及影像处理相关函式,甚至连从摄影机撷取影像都能帮你解决。OpenCV还有个好处是它是跨平台的函式库,在各大平台都能运作,所以我们接下来写的程式除了在Mac上外,也能在Linux及Windows上运作。
要开始找出亮点位置前,我先简单交代一下怎么用OpenCV从摄影机撷取影像出来(以下程式码都用C++填写)。
-
-
#include <cv.h>
-
#include <highgui.h>
-
-
int main ( int argc, char** argv )
-
{
-
CvCapture* capture;
-
IplImage *img;
-
capture = cvCaptureFromCAM ( 0 );
-
cvNamedWindow ( "mainWin", CV_WINDOW_AUTOSIZE );
-
cvMoveWindow ( "mainWin", 0, 100 );
-
while (cvGrabFrame (capture ) ) {
-
img=cvRetrieveFrame (capture );
-
// process img here …
-
cvShowImage ( "mainWin", img );
-
int key=cvWaitKey ( 10 );
-
if (key == 27 ) // 27=ESC
-
break;
-
}
-
-
cvReleaseCapture (&capture );
-
return 0;
-
}
上面是OpenCV从摄影机撷取影像并画在一个视窗内最基本的程式码,我们之后对于影像所有的处理都插在第14行那里就可以了。
侦测亮点位置
观察我们取得的影像可以发现,我们要找的亮点是实心的方形,且没有特定颜色和图样。所以要找出这些亮点中心的位置其实很简单。第一步,我们可以先去除颜色,将全彩(RGB 24bits)转为只有两种颜色(黑白)的影像。而剩下来白色的部份就是我们感兴趣的区块。
在OpenCV中,我们需要先将含有3个channel (RGB)的输入转为单一channel的灰阶图。灰阶图的意义是它只包含了亮度的资讯,最亮的地方会是白色(255),最暗就是黑色(0)。得到灰阶图后,我们就可以设定一个threshold,将一定亮度以上的pixel全变成白色,剩余的地方全设成黑色。透过这样子得到的图片将只剩下黑白两色,对于我们之后的处理会简单很多。这部份的程式码很简单,我把它写成了一个称为convertToBinary的函式,其中设定threshold为40,也就是说值超过40的pixel都会变成255,剩余都变0。
-
-
#define BINARY_THRESHOLD 40
-
-
IplImage* convertToBinary (IplImage *in, IplImage *out ) {
-
cvCvtColor (in, out, CV_BGR2GRAY );
-
cvThreshold (out, out, BINARY_THRESHOLD, 255, CV_THRESH_BINARY );
-
}
处理后如下面两张图所示,左边为灰阶,右边为黑白两色。
下一步我们要扫描整个画面,找到这些白色区块的位置和形状,接着就能计算出中心位置。OpenCV提供了一个函式称为cvFindContours
,这个函式能在画面中找到白色区块的轮廓(contour)。有了轮廓后,就能透过cvBoundingRect
找到这个轮廓的bounding box,也就是我们所要的白色区块位置。
下面的函式findBlobs
需要一个只含黑白两色的图作为输入,它可以找出图中所有白色区块的位置,但目前还不会输出任何东西。找出轮廓后,可以用14~18行的程式码把轮廓和bounding box画在debug_img上。(通常我们会开另一个视窗专门画这种debug用的画面)
-
-
void findBlobs (IplImage *bin_img, IplImage *debug_img ) {
-
static CvMemStorage *storage = cvCreateMemStorage ( 0 );
-
CvSeq * contour;
-
-
cvFindContours ( bin_img, storage, &contour, sizeof (CvContour ),
-
CV_RETR_CCOMP, CV_CHAIN_APPROX_NONE );
-
-
for (; contour; contour = contour->h_next ) {
-
CvRect rect = cvBoundingRect (contour, 1 );
-
CvPoint pt1 = cvPoint (rect. x, rect. y ),
-
pt2 = cvPoint (rect. x+rect. width, rect. y+rect. height );
-
// The center of rect is (rect.x+rect.width/2, rect.y+rect.height/2)
-
if (debug_img ) {
-
cvRectangle (debug_img, pt1, pt2, CV_RGB ( 255, 0, 0 ), 2 );
-
cvDrawContours (debug_img, contour,
-
CV_RGB ( 255, 0, 0 ), CV_RGB ( 255, 0, 0 ), 0, 2, 8 );
-
}
-
-
}
-
}
下面两张图即是OpenCV找到的轮廓和bounding box。
光点追踪
经过上面两个函式的处理,我们已经能在摄影机撷取出的每个frame标定出光点的位置。但我们要做的可是multi-touch的介面,也就是说每个frame会有不定个数的亮点,可能只有1个,也可能有2个,甚至有10个。我们必须持续追踪这些亮点的变化,包括每个点是什么时候出现、中间移动的轨迹、以及什么时候消失的。在OpenCV中有提供现成的tracker CAMSHIFT,但它实在太复杂,我还没有时间把它搞懂,所以我决定自己写一个简易版的tracker。(所谓的简易版就是没那么完美,可是大部分情况是work的)
如果我们的tracker只面对一个点,那么问题很简单。在下图中,圆形为前一个frame光点的位置,方形为目前这个frame的位置。在单一个点的情况,可以直接断定圆形和方形是同一个光点,并且移动路径是A。
但如果是多点,情况就没这么单纯了。下图展示了两个点的情况,我们要怎么判断两个光点是走了路径A或路径B呢?也就是说这两个方形,到底谁是光点1?谁是光点2?
其实上图还有更复杂的可能,像是光点2其实已经消失,而某个方形是新出现的光点3。在每个光点都是相同的情况下,其实我们是没办法区别这种奇怪的可能性,所以为了简化我们先暂时忽略这种可能。在这个问题上,我用一个简单的策略来解决:在两个frame间,相对位移距离越短就越可能是相同的一个点。也就是说在上图中,上方的方形应该是点1,下方是点2,因为这样子两个点移动的距离都比较短。
这个问题如果要求全域的最佳解(让每个点移动的距离和最短),可以转化成图论中的Bipartite matching问题来解。但在这篇以简单易懂为前提的教学中,我们还是用简单的方法吧。我的配对方法如下:
- 假设前一个frame的所有光点集合为A,目前frame的则为B
- 对于A中的所有点a,计算出到B中所有点b的edge长度,并放进一个阵列E中
- 把阵列E中距离太远的edge剔除掉
- 将E按edge长度排序,从小排到大
- 从E的开头开始,每取出一条edge前先看看edge的两端点是否已经配对成功过,若两端没有则标记两端点为同一光点
- 重复上一步直到取出所有edge为止
跑完上面的演算法,我们就可以标记出两个frame中所有对应的光点,并且可以产生出对应的FINGER_MOVE event。而A和B中剩下来没被配对到的点,就代表这些点只存在其中一个frame,要不是刚消失就是刚出现,所以我们就能把它们分别对应到FINGER_UP及FINGER_DOWN event。
採用这个简单的策略,就能很快解决这个配对问题,还能顺便产生出对应的event出来。详细的程式码太长就不列在这边,有兴趣的可以看下载整个原始码回去看BlobTracker.cpp。
与multi-touch应用程式的结合
到此我们已经能顺利辨识并追踪画面上的多个光点,并产生出上层应用程式所需的event。但要怎么把这些event送出去呢?这部份目前仍是一片迷雾,因为主流作业系统都还不支援multi-touch API。在缺乏有力的标准下,目前许多multi-touch应用程式是使用TUIO protocol。但我比较想直接和作业系统结合,所以我还在研究这部份要怎么解决。
下载
这篇文章所用的完整原始码:BlobTracker (有人需要执行档吗?)
2008-07-27更新: 释出支援TUIO的BlobTracker(这个版本需要oscpack,我已经附在里面,但注意非OS X平台必须自己重新make过oscpack内的lib。)
基本上在各个平台只要有g++和opencv,打make
就能编译完成。
在Mac OS X下需要另外安装xcode和opencv。opencv可以用darwin ports安装。
相关讨论
这篇文章所叙述的演算法并不是完美的,已知有下列问题:
- 当两个光点相黏在一起时会变成一个。这是因为我们直接找connected component的轮廓当作光点的关系,比较好的作法进一步是比对光点的形状。
- 光点追踪的演算法算出来的配对不是全域最佳解。改用CAMSHIFT可能(?)会比较好。
FAQ
Q: 这三篇文章介绍的东西能在Macbook以外的notebook上用吗?
A: 当然可以。硬体跟作业系统无关,软体是跨平台的,只要有OpenCV就能跑。唯一要注意的是有的摄影机内建了IR-block filter,会把红外线挡掉,碰到这种就不能用了。实验方法是随便拿个遥控器对着摄影机按,看会不会发光就知道了。
Q: 这样做出来的multi-touch能操作Mac里支援multi-touch的程式吗?
A: 目前还不可以。因为我不知道怎么伪装成OS X的multi-touch event长什么样,也不知道怎么塞到OS的event queue中。说不定Mac OS X根本就还没有multi-touch event这种东西。
Q: 那做出来的东西可以干么?
A: 只要把我自订的event转成TUIO message送出,就可以驱动所有支援TUIO的multi-touch应用程式。但如果没写这段,就…….只能给自己娱乐用。
Q: 之后会释出支援TUIO的程式吗? A: 如果我没研究出来怎么送Mac OS X的event应该就会改用TUIO。
A: 已经释出。(2008-07-27更新)