搜索
bottom↓
回复: 43

自制激光虚拟投影键盘(原创)

  [复制链接]

出0入0汤圆

发表于 2013-1-8 19:55:09 | 显示全部楼层 |阅读模式
本帖最后由 panwenjian 于 2013-1-8 19:53 编辑

之前看到有人发的虚拟键盘的帖子,但貌似一直没有发上来原理。这里发一个我做的键盘,原理代码都有。
很早之前就想做这个,只是找不到投射键盘图案的激光头。后来在淘宝找到了。
材料和时间所限,做的仅仅是demo。不过对于了解原理足够了。
先上一个小视频:

http://player.youku.com/player.php/sid/XNDQzMDI0MDY0/v.swf

再上一张实物图,够山寨把:


虚拟激光投射键盘在1992年就由IBM发明了。
我第一次看到这玩意儿就觉得特别新奇,后来看到淘宝上棒子的产品,要900多米,实在是宰人啊。于是就一直有想法做一个。
不久前在淘宝看到了投射键盘图案的激光模组,果断买了一个,开始筹划制作一个。

taobao上买的650nm虚拟键盘激光组件:


而且现在有了强大的opencv图像处理库,实现这样的虚拟激光投射键盘变得易如反掌。
先说说投影键盘的基本原理。键盘由三个主要部件组成:摄像头、键盘图案投射器、一字线性感应激光头。
见下图:


图上从上到下分别是键盘图案投射器、摄像头、一字线性感应激光头。
当然,摄像头放在键盘图案投射器上面也是可以的,比如我就是这么做的。
1. 键盘图案投射器在平坦的桌面投出清晰键盘图案
2. 最底下的一字线性激光(一般采用红外线的,这样眼睛不可见)发出一字型激光,平行于桌面射出,这样如果手指有按键活动,会在手指上形成激光光斑
3. 摄像头捕获激光光斑,对应于键盘图案映射的位置,就可以知道哪些键被按下
OK,原理很简单,是不是。有了这些模块,剩下的关键就是摄像头的图像处理算法了,而且现在有了opencv,实现也不是难事。
这里说一下我的实现方法。


由于人眼对激光的反应不一样,780nm-808nm的激光人眼不敏感,可看到微弱的一丝红光。850nm至1064nm波长人眼不可见,通过红外感光仪器等专业设备可以看到,其中808-850nm通过摄像头可以看到。980-1064nm通过倍频片可以看到。
所以我在网上买了一个808nm-810nm 红外一字线激光器。这样配上滤光片,可以滤去绝大多数其他波长的杂光,只剩下红外激光的光斑。
这样做的好处是减少干扰,增加键盘的可靠性,而且使算法处理更加简单有效。
加上前面的650nm虚拟键盘激光组件,总共也就花了100块钱左右。

25mw 808nm-810nm 红外一字线激光器 激光头


直径18mm可见光截止400-750nm滤光片,800-1000nm高透


在摄像头上看到的红外激光光斑投射到手指的图像如下图:


对于光斑的跟踪我找了个现成的opencv扩展库cvblob,具体可以参考它的文档和例子,google code上有这个项目的托管。
待会儿会奉上代码。cvblob可以跟踪多个光斑,所以很容易就可以实现ctrl+alt+delete之类的组合键。

再来两张键盘图:




顺便说一句,本文中的摄像头放的位置只能捕捉到部分键盘图像,所以demo只是演示了部分键盘的按键。
不过丝毫不影响原理介绍。如果要获得全部键盘图像,或者去买一个广角的摄像头,或者把这个摄像头位置提高,不是什么难事。
时间有限,不想折腾了。



代码:

#include <iostream>
#include <iomanip>

#include "opencv/cv.h"
#include "opencv/highgui.h"

#include "cvblob.h"
using namespace cvb;

typedef struct key
{
  char  c;
  int x0;
  int y0;
  int x1;
  int y1;
};

key g_keymap[] =
{
  {'4',525,350,588,419},
  {'5',442,345,504,414},
  {'6',360,339,422,408},
  {'7',277,332,342,404},
  {'8',198,327,259,399},
  {'9',121,320,174,389},
  {'0',41, 318,94, 383},
  {'E',528,274,590,337},
  {'R',443,267,507,332},
  {'T',359,263,428,327},
  {'Y',280,259,344,321},
  {'U',199,251,261,315},
  {'I',119,246,179,307},
  {'O',41, 240,96, 301},
  {'D',504,203,567,259},
  {'F',424,199,489,257},
  {'G',348,194,410,251},
  {'H',266,187,329,245},
  {'J',192,183,251,241},
  {'K',117,178,171,236},
  {'L',42 ,174,92, 229},
  {'X',543,144,605,197},
  {'C',467,139,530,191},
  {'V',392,135,457,190},
  {'B',316,128,377,181},
  {'N',242,124,299,176},
  {'M',171,118,225,172},
  {'<',98, 114,149,166},
  {'>',26, 108,73, 159},
  {'_',182,62, 531,127},
};

int g_key_num = sizeof(g_keymap)/sizeof(key);

int main()
{
CvTracks tracks;

cvNamedWindow("red_object_tracking", CV_WINDOW_AUTOSIZE);

CvCapture *capture = cvCaptureFromCAM(0);
cvGrabFrame(capture);
IplImage *img = cvRetrieveFrame(capture);

CvSize imgSize = cvGetSize(img);

IplImage *frame = cvCreateImage(imgSize, img->depth, img->nChannels);

IplConvKernel* morphKernel = cvCreateStructuringElementEx(5, 5, 1, 1, CV_SHAPE_RECT, NULL);

//unsigned int frameNumber = 0;
unsigned int blobNumber = 0;

bool quit = false;
while (!quit&&cvGrabFrame(capture))
{
   IplImage *img = cvRetrieveFrame(capture);

   cvConvertScale(img, frame, 1, 0);

   IplImage *segmentated = cvCreateImage(imgSize, 8, 1);

   // Detecting red pixels:
   // (This is very slow, use direct access better...)
   for (unsigned int j=0; j<imgSize.height; j++)
     for (unsigned int i=0; i<imgSize.width; i++)
     {
    CvScalar c = cvGet2D(frame, j, i);

    double b = ((double)c.val[0])/255.;
    double g = ((double)c.val[1])/255.;
    double r = ((double)c.val[2])/255.;
    // unsigned char f = 255*((r>0.2+g)&&(r>0.2+b));
   // cvSet2D(segmentated, j, i, CV_RGB(f, f, f));
   if(b>0.4 || g>0.4 || r>0.4)
      cvSet2D(segmentated, j, i, CV_RGB(255, 255, 255));
   else
      cvSet2D(segmentated, j, i, CV_RGB(0, 0, 0));
     }

   cvMorphologyEx(segmentated, segmentated, NULL, morphKernel, CV_MOP_OPEN, 1);

   cvShowImage("segmentated", segmentated);

   IplImage *labelImg = cvCreateImage(cvGetSize(frame), IPL_DEPTH_LABEL, 1);

   CvBlobs blobs;
   unsigned int result = cvLabel(segmentated, labelImg, blobs);
   cvFilterByArea(blobs, 500, 1000000);
   cvRenderBlobs(labelImg, blobs, frame, frame, CV_BLOB_RENDER_BOUNDING_BOX);
   cvUpdateTracks(blobs, tracks, 200., 5);
   cvRenderTracks(tracks, frame, frame, CV_TRACK_RENDER_ID|CV_TRACK_RENDER_BOUNDING_BOX);

   cvShowImage("red_object_tracking", frame);

   // print key
   for (CvTracks::const_iterator it=tracks.begin(); it!=tracks.end(); ++it)
   {
      int xx = (int)it->second->centroid.x;
      int yy = (int)it->second->centroid.y;
      //std::cout << xx << ',' << yy << std::endl;

     for(int i=0; i<g_key_num; i++)
     {
        if(xx > g_keymap.x0 &&
           xx < g_keymap.x1 &&
           yy > g_keymap.y0 &&
           yy < g_keymap.y1)
        {
            std::cout << g_keymap.c << std::endl;
            break;
        }
     }
   }

   cvReleaseImage(&labelImg);
   cvReleaseImage(&segmentated);

   char k = cvWaitKey(10)&0xff;
   switch (k)
   {
     case 27:
     case 'q':
     case 'Q':
       quit = true;
       break;
     case 's':
     case 'S':
       for (CvBlobs::const_iterator it=blobs.begin(); it!=blobs.end(); ++it)
       {
         std::stringstream filename;
         filename << "redobject_blob_" << std::setw(5) << std::setfill('0') << blobNumber << ".png";
         cvSaveImageBlob(filename.str().c_str(), img, it->second);
         blobNumber++;

         std::cout << filename.str() << " saved!" << std::endl;
       }
       break;
   }

   cvReleaseBlobs(blobs);

   //frameNumber++;
}

cvReleaseStructuringElement(&morphKernel);
cvReleaseImage(&frame);

cvDestroyWindow("red_object_tracking");

return 0;
}


我的博客:
http://blog.sina.com.cn/spalish

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

阿莫论坛20周年了!感谢大家的支持与爱护!!

曾经有一段真挚的爱情摆在我的面前,我没有珍惜,现在想起来,还好我没有珍惜……

出0入0汤圆

发表于 2013-1-8 20:29:51 | 显示全部楼层
Cool!!

出0入0汤圆

发表于 2013-1-8 21:04:26 | 显示全部楼层
似乎还是不太明白,属于遮挡还是图像识别?

出0入0汤圆

发表于 2013-1-8 21:24:20 | 显示全部楼层
不错啊 呵呵   动手能力强啊

出0入0汤圆

发表于 2013-1-8 21:28:03 | 显示全部楼层
貌似很牛的样子

出0入0汤圆

发表于 2013-1-8 21:47:27 | 显示全部楼层
厉害呀!        

出0入0汤圆

发表于 2013-1-8 21:52:11 | 显示全部楼层
mark,学习

出0入0汤圆

发表于 2013-1-8 22:44:53 | 显示全部楼层
牛x,没太看明白

出0入42汤圆

发表于 2013-1-8 23:04:48 | 显示全部楼层
灰常不错,了解了

出0入0汤圆

发表于 2013-1-8 23:10:10 | 显示全部楼层
虽然看不怎么懂,但是我知道很牛逼啊。哈哈

出0入34汤圆

发表于 2013-1-9 10:05:24 | 显示全部楼层
真是让人佩服楼主..

出0入0汤圆

发表于 2013-1-9 11:26:26 来自手机 | 显示全部楼层
谢谢分享!我正需要这个玩玩!

出0入0汤圆

 楼主| 发表于 2013-1-12 08:38:44 | 显示全部楼层
senzh01 发表于 2013-1-8 21:04
似乎还是不太明白,属于遮挡还是图像识别?

遮挡+图像识别
先遮挡产生光斑,根据这个光斑知道手指的位置。
还有没有更好的实现方法,大家可以多讨论讨论。

出0入0汤圆

发表于 2013-1-12 08:47:13 来自手机 | 显示全部楼层
厉害啊支持楼主!!!

出0入0汤圆

发表于 2013-1-12 09:36:14 | 显示全部楼层
帅飞了,楼主可以搞个套件

出0入85汤圆

发表于 2013-1-12 10:17:19 | 显示全部楼层
哈哈,有空研究一下,先mark!

出0入0汤圆

 楼主| 发表于 2013-1-12 11:22:22 | 显示全部楼层
bbssilverkey 发表于 2013-1-12 09:36
帅飞了,楼主可以搞个套件

纯粹只是业余玩而已,还没想过搞套件呢。。。
感兴趣的童鞋可以搞套件出来造福大家。

出0入0汤圆

发表于 2013-1-12 11:26:54 | 显示全部楼层
cool                                          

出0入0汤圆

发表于 2013-3-16 20:12:33 | 显示全部楼层
首先楼主的指针部分出现错误,应该用"->",你用"."
然后
编译环境ubuntu12.04  gcc4.6.3  blob0.10.4  opencv2.3.1 7  出现以下报错
jp.cpp:(.text+0x427): undefined reference to `cvLabel'
jp.cpp:(.text+0x448): undefined reference to `cvFilterByArea'
jp.cpp:(.text+0x479): undefined reference to `cvRenderBlobs'
jp.cpp:(.text+0x4ab): undefined reference to `cvUpdateTracks'
jp.cpp:(.text+0x4ef): undefined reference to `cvRenderTracks'
jp.cpp:(.text+0x7f5): undefined reference to `cvSaveImageBlob'
collect2: ld 返回 1
make: *** [jp] 错误 1
查了好久也没找出问题所在,请教楼主你的编译环境是啥?还有上面几个报错是啥情况,还请大神指教。

出0入0汤圆

发表于 2013-9-19 00:54:50 | 显示全部楼层
最近正在学习。

出0入0汤圆

发表于 2013-9-19 08:33:15 | 显示全部楼层
不错。学习学习!

出0入0汤圆

发表于 2013-10-16 23:43:11 | 显示全部楼层
好留比的样子,楼主的动手能力很强啊~

出0入0汤圆

发表于 2013-10-24 17:43:36 | 显示全部楼层

出0入0汤圆

发表于 2013-11-3 11:20:26 | 显示全部楼层
太酷了!!!

出0入0汤圆

发表于 2013-11-14 15:29:57 | 显示全部楼层
这个可以有,不错!

出0入0汤圆

发表于 2013-11-16 19:57:23 | 显示全部楼层
c o o l                                                                                       

出0入4汤圆

发表于 2013-11-16 20:13:18 | 显示全部楼层
coolest!  so must mark!

出0入0汤圆

发表于 2013-11-25 14:00:32 | 显示全部楼层
前几年也看琮累似资料,想市场应该也快有了,几年后这东西还是那么神秘.

出0入0汤圆

发表于 2014-6-30 17:37:27 | 显示全部楼层
轻微的赞一个
~

出0入0汤圆

发表于 2014-7-21 10:59:01 | 显示全部楼层

貌似很牛的样子

出0入0汤圆

发表于 2014-7-21 17:29:39 | 显示全部楼层
这个很有趣,学习了

出0入0汤圆

发表于 2014-7-21 18:35:33 | 显示全部楼层
提示一下楼主,可以研究下wii遥控器上的摄像头,跟你做的这个东西结合起来会很有趣

出0入0汤圆

发表于 2014-9-13 08:00:17 | 显示全部楼层
cool ! mark!

出0入0汤圆

发表于 2014-10-12 11:23:01 来自手机 | 显示全部楼层
nomady 发表于 2014-7-21 18:35
提示一下楼主,可以研究下wii遥控器上的摄像头,跟你做的这个东西结合起来会很有趣 ...

这个还没有理解到诶,
给详细介绍一下吧,我想自己做这个……

出0入0汤圆

发表于 2014-10-12 11:27:08 来自手机 | 显示全部楼层
panwenjian 发表于 2013-1-12 08:38
遮挡+图像识别
先遮挡产生光斑,根据这个光斑知道手指的位置。
还有没有更好的实现方法,大家可以多讨论 ...

一直在想一个问题,这样产生的光斑,会不会被前面的手指遮挡住啊?怎么实现组合控制啊

我曾经看见一个人实现的demo,他说理论上的可以实现的是n点触控

出0入0汤圆

发表于 2014-10-12 12:47:13 | 显示全部楼层
厉害啊,谢谢分享!

出0入0汤圆

发表于 2014-11-6 09:00:44 | 显示全部楼层
mark键盘学习

出0入0汤圆

发表于 2014-11-7 20:27:50 | 显示全部楼层
多谢楼主分享!

出0入0汤圆

发表于 2015-3-30 23:16:29 | 显示全部楼层
红外一字线激光器 激光头
这个淘宝哪里有卖?可否给个链接?

出0入0汤圆

发表于 2015-3-31 07:58:29 | 显示全部楼层
楼主厉害。本来有卖的,前边几天去问下架了。楼主还自己做出来了啊。呵呵。

出0入0汤圆

发表于 2015-10-29 20:34:29 | 显示全部楼层
收藏mark。。。。

出0入0汤圆

发表于 2015-12-16 11:13:57 | 显示全部楼层
楼主介绍的通俗易懂,赞。

出0入0汤圆

发表于 2015-12-16 11:36:42 | 显示全部楼层
高级货。
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片。注意:要连续压缩2次才能满足要求!!】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-8-15 11:30

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表