VC+DirectShow对视频进行图片处理之四

来源:互联网 发布:淘宝充值平台怎么弄 编辑:程序博客网 时间:2024/06/02 08:05
图像处理

  在我的程序中图像处理函数是作为 DirectShow 封装类一部分的,我认为这样便于移动和使用。没有连着上面的 DirectShow 类而另外写标题是因为我觉得有必要把它提到重要位置。在程序完善阶段您的工作基本就在这里了,除了花心思构造D3D环境外几乎所有效果都要在这里实现,水平高下也体现于此。

  在网上可以找到很多图像特效的代码和解说,我结合编程过程再说说。

  1. 访问缓冲区的麻烦。

  这是最麻烦的,二维图像在这里以一个连续的一维缓冲区呈现,您要靠一个指针去访问它,怎么办呢?先弄懂 Pitch,例如 32bit ARGB 图像,每个象素就占用 4 Byte 内存空间(1 Byte = 8 bit),对于宽度为 20 像素的图像,它的 Pitch 就是 80 Byte,即每一行占用的内存。按 x 、y 坐标就有如下公式(按 Byte 计算):

B:y * Pitch + x * 4
G:y * Pitch + x * 4 + 1
R:y * Pitch + x * 4 + 2
A:y * Pitch + x * 4 + 3
  可以看出在内存中是按 BGRA 存储的,我不明白为什么这样,可能可以从计算机的内存存储方式找到答案。上述公式计算多,效率较低,在实际使用中应适时作有益的改变。

  2. 浮雕。

  到目前为止我在网上找到的几篇文章都说把一点的值减去其右下角点的值再加上128就行了。为什么要减去右下角的点呢?为什么要加上128呢?原来浮雕是要把图像的变化部分突出显示出来,而把相同部分淡化,所以用一点减去其邻域任意点都可以达到这个目的,倒也不一定是右下角的点,包围着它的八个点都可以,甚至可以选择减去更远的点,只要规则明确、效果好就行。在相减后点的 RGB 值都减小了,大多接近黑色,黑乎乎一片的看不出什么来,一点也不像浮雕,所以要给它们都增加一个相同的亮度,通常加上128,其它的值,例如64、100,当然也行,一切都以实际效果为准。说到效果,上面所说的RGB相减会造成浮雕有一些色点,解决方法是计算两点的亮度之差,RGB都赋值为亮度差,画面就没色点了,因为已经变成灰度图了。亮度公式是 Brightness = 0.3 * R + 0.6 * G + 0.1 * B,其中G 的比重最大,可以近似的用 G 作为亮度,在RGB各自的分量图中也可以明显看出 G 分量的图最亮,简单的把 G 的值赋给 R 和 B 就得到灰度图了,这可以减少计算,提高速度。后来我还看到这样的话句,"用3 * 3 的小块做的浮雕效果更好",不过我不知道怎么用,可能这样就可以实现 PhotoShop 那样更好的浮雕效果。

  原理是这样了,到了编程却是另外一回事:能够把规则、数学公式转换为程序也是能力的一种体现。如果要减去右下角的点,那么最右一列和最后一行是要特殊处理的,否则肯定会发生内存访问错误,想一想就知道为什么;如果要减去左边的点,第一列也要特殊处理,请问第一列的点到哪里找它左边的点呢?不要小视此问题,它会令你访问内存时遇到一些问题。

  3. 铅笔画

  铅笔画原理和浮雕差不多,也是亮度相减,认为变化大的是边缘,然后设置一个阀值,例如差值大于8,则把该点设为黑色(0,0,0),要不设为白色(255,255,255)。阀值、色彩都可任意设置,没人要您拘束就不要忸忸怩怩的不敢改动。

  按照此方法得到的效果实在不怎么样,可惜我不是研究图像的料,对数据的处理能力很差,同样一幅在专家手中可以玩出很多花样的图片,沦落到我手上也只能饮恨屈膝投降无奈了。这是我看了一些图像处理方面资料和书籍所发的呆叹,图像处理实在太精深了,既要数学、物理知识雄厚,又要脑子灵活能东移西就把各种知识综合运用,不然就只好望洋兴叹。

  4. 加亮、对比度等

  首先悲痛的说明,我曾努力的要实现色度、饱和度的调整,知道是要把 RGB 转换成 HLS 之类的颜色空间才能实现,也找到了一些它们之间转换的说明和转换函数,可惜看不明白,或者说那些材料根本不打算让我明白!这不单是气话,而且事实,我真的十分气愤:怎么能够在前面铺了一大堆"效果图"说了一大堆废话然后给个只有几行无大用的注释的代码就可以呢?!尽管如此愤概,我还是乖乖的抄了程序,希冀能发挥作用,结果却是失望:不仅效率低下,而且在调整了饱和度的同时使图像出现不协调的彩色方块。由于不知道原理,无法改动,于是我放弃了它。

  调整亮度很简单,例如要加亮 10,把 RGB 都加上 10 就可以了,减亮就减10。

  对比度调整也不难,书上说是要令亮点更亮、暗点更暗,好像是要找出亮点来增亮、找出暗点加暗,其实不然,把所有点都乘以一个数,把亮暗点的差距拉大或减小就能调整对比度了。把图像原来的对比度定为 1,要增大对比度就调整为 1.3 、2 等大于1 的数,把每个点的 RGB 都乘以它,就行了,要降低就把数值设为 0 至 1 的数。只要注意保持 RGB 的值在 0 ~ 255 中即可。
5. 马赛克

  马赛克效果就是把图像分解成 m * n 个小块 或长宽为 x 、y 的小块,用小块内的某点颜色作为整块的颜色,通常用左上角的颜色。

  动起手编程会很麻烦,要定位到每小块的左上角,才能改变块内的颜色,因此要用很多循环,我在代码中就用了12个循环!除此,还有逻辑麻烦,拿分成宽高为 x、y 的小块这种情况为例,您不能保证图像的长宽刚好都是 x、y的倍数,很多时会余出一些"边角料",这就是麻烦,不可能舍弃它们不进行处理,因为很影响效果。因此如图所示,要先处理蓝色的倍数部分,再处理绿色的宽度上余下部分,处理红色的高度上余下部分,还有黄色的宽高夹缝的小块。


  除了这种长方形的处理,还可以试验上面右图菱形等形状,当然,您要付出很大的劳动,而我没能做到这些。

  6. MMX

  记得在上面我说过会在文章中涉及一点MMX,不妨在这里涉及。在 VC 中可以镶入汇编使汇编变得很容易,完全不是纯汇编代码所能相比的,所以不用怕汇编,可以先用 C 语言写出实现代码,再用汇编"翻译"过来。如果译不出来,更加可以把代码中断一下,让 VC 反汇编,看 VC 的汇编代码,再行改进,为什么不行呢,有人用枪指着您么?记住哦,如果没办法改进就放弃汇编,不要做多余的事。其实要用 MMX 也不一定非用汇编不可,VC 也提供了 MMX 的 C++ 封装,学习后可用它,我则懒于学习。

  MMX 最大的好处是可以自动保证处理的值范围为0 ~ 255,节省判断,而且MMX寄存器是64位的,一次可处理32bit图像的两个点。其它的我也不太懂,您可参考相关资料。

  下面列出浮雕效果代码,它是减去右边点的,由于不进行行列判断,每行最后一点减去的是下一行的首点。

__int64 Mask = 0x8080808080808080; // 0x80 = 128,就是亮度的增加值
UINT a = bmpBufferLen >> 3; // 缓冲区长度(按 BYTE 计算)除以 8(两个点的大小),计算要循环的次数
_asm{
mov esi,pIn; // 要处理的缓冲区指针
mov edi,pOut; // 峁撼迩刚?BR>mov eax,a; // 循环次数
dec eax; // 循环次数减一,因为最后两点没法减,可以在后面特殊处理,这里不作处理
movq mm1,Mask; // 增加值,movq 是 MMX 的专用汇编指令,请找资料看
_loop: // 循环
mov ecx,esi; // ecx 存储右边点的指针
add ecx,4; // 只加 4 就跳过一点到右边点了
movq mm0,[esi]; // 移动要处理的两点的值到 MMX 寄存器
movq mm2,[ecx]; // 移右边两点的值
psubusb mm0,mm2; // 相减
paddusb mm0,mm1; // 加上增加值
movq [edi],mm0; // 移到结果缓冲区
add esi,8; // 移动到下两点
add edi,8; // 同上
dec eax; // 循环计数减一
jnz _loop; // 不为零就继续循环
emms; // 结束 MMX 使用
}  7. 来点高级的,用摄像头控制鼠标!

  看着这个有点神奇吧,其实比什么都要简单。先做好"硬件准备":把摄像头如图摆放,镜头下方放张白纸以使图像中物体界限分明。


  软件方面,把图片作阀值处理-- B 值大于 128 的设为黑色,其它的设为白色。因为白纸的作用,您的手或其他物体会在阀值图中显示为白色,如图,再找出图中红点,即第一点白点的在图中位置(x,y),再把图的坐标影射到屏幕坐标就行了。

 

  下面要讨论具体做法。先解决图像坐标问题。我获取第一点白点的程序如下:

void CDSControl::GetMousePos(BYTE* pb,int *xPos,int* yPos)
{
 int x,y;
 BOOL mouseFound = FALSE;
 for(y = 0; y < m_bmpHei; y ++){
  for(x = 0; x < m_bmpWid; x ++){
   if(pb[0] == 255){
    // 因为白色为(255,255,255),判断一个255 即可
    pb[2] = 255; // 设为红色,别忘了 BGRA 的内存排列方式
    pb[1] = pb[0] = 0; //
    // 计算坐标
    *xPos = ScreenWid - x * ScreenWid / m_bmpWid;
    *yPos = y * ScreenHei / m_bmpHei;
    mouseFound = TRUE;
    break;
   }
   pb += 4;
  }
  if(mouseFound){
   break;
  }
 }
}
  可以看出我是从所得的图像缓冲区的第一点开始检索的,这也是摄像头扫描 CCD 的顺序,看上面右边的图,那是右手的图像,可以推想出摄像头的扫描顺序如下图所示:

 
(摄像头扫描坐标) (与屏幕坐标(蓝)相对的摄像头坐标(红))

  而相对于屏幕坐标很容易得到上面右图,说明屏幕坐标以左上角为原点,x的正方向为右,而摄像头坐标以右上角为原点,x的正方向为左,也就是说我按顺序寻找所得到的坐标值是这样的:y 与屏幕坐标 y 相符,x 则与屏幕坐标 x 刚好相反。因此推算出鼠标位置应该是:

*xPos = ScreenWid - x * ScreenWid / m_bmpWid;
*yPos = y * ScreenHei / m_bmpHei;
  其中 ScreenWid 、ScreenHei 分别是屏幕的宽和高,用 ScreenWid = GetSystemMetrics(SM_CXSCREEN) 和 ScreenHei = GetSystemMetrics(SM_CYSCREEN) 得到,而m_bmpWid、m_bmpHei 当然是图片的宽和高了。得到鼠标坐标后再用 SetCursorPos 设置即可,只不过鼠标晃动会比较厉害,这与图像噪音有关,可能先做个柔化处理会好一点,但鉴于摄像头的摄像质量,我不想作无谓的挣扎,您不会真的想用摄像头代替鼠标吧?!

  在使用前应该调整二值图的阀值,使整个图都变成黑色,保证能正确滤除干扰,不然在按下"鼠标控制"按钮后您的鼠标就不会听话,您会无法控制好它。请问没有鼠标的帮忙您将如何关闭程序?对了,"Alt + F4",别忘了,否则您得硬着动手把摄像头拔掉!!

  可以说控制鼠标真的很容易实现,不过效果出奇的不错,这种好像无影无踪的控制方式相当令人惊奇,记得我的大哥看程序时对我前面的图像处理没有一丝反应,看到这个却大大的惊奇!呵呵。如果您有兴趣的话可以在此方面做更多的试验,例如可以把手裁剪出来,让它参加拨动一个小球等游戏,只要您的几何过关、有毅力就可以实现。

  8. 更实用的数字减影技术

  请看上面两幅图,左边的是先存储一幅背景图然后把手放到摄像头前摄像,用摄到的图片减去背景图得到的图;中图是根据左图把手的颜色设为原来颜色得到的;右图是不断用新图片减去上一幅图得到的手移动痕迹图。这充分显示了数字减影的功用:能够从背景分解物体和侦测到物体的运动。


  如此说来此技术在安保方面的应用会很突出,像上面右图那样不断减去上一幅图片,当减影后得到的图片差别大于某一程度的点多到一定数值时就说明有情况发生,这时候就提醒保安工作,弥补保安的人为失误,也可以在此时启动录像录取有价值的情况。请看程序:

void CDSControl::DNS(BYTE* pIn,BYTE* pReduce,BYTE* pOut)
{
 // pIn 新图的数据区指针,pReduce 背景指针,pOut 存储区指针
 if(!pReduce) return; // 没有背景图就不处理
 int differentPoint = 0;
 for(int i = 0; i < m_bmpBufferLen; i ++){
  // m_bmpBufferLen 为数据区长度
  pOut[i] = abs(pIn[i] - pReduce[i]); // 相减,取差值的绝对值
  if(pOut[i] > 32){
   // 相差大于 32 就认为是不同的点,此值因摄像头而异,与噪音有关,请自行试验
   differentPoint ++; // 不同点增加
   pOut[i] = pIn[i]; // 把不同点赋回它的颜色
  }
  if(differentPoint > 200){
   // 不同点大于 200 就认为有情况,应适当改变
   // 调用警报等……
  }
 }
}
  当然了,我可舍不得整天整夜开着电脑守着我睡觉,只是试验这项技术获取了却罢了。
9. 广阔的图像处理天地

  因为写程序的需要,也因为浓厚的兴趣,我在此段期间找了不少图像处理的资料,不过正如前面说过的,图像处理需要数学,大多资料都有一大堆公式,看不看得懂就得看您的造诣了。虽然我看不懂那些公式,但我也得到了很多有益的启示,它们是我从更多更新的角度去看待图像,改变了我的思维方式,例如图像是平面的,但可以把其RGB 分量看作是高度,使图形呈现"立体模式",从而可以对它应用立体几何的方法。

  看看我理解的线性放缩吧,这也是下面 D3D 应用的铺垫。

 

  如上图,很容易写出此直线段的方程 y = 2 * x (0<= x <= 10)。我不知道您是怎么理解此方程的,我自己认为以前一点也不理解"映射"这个概念,现在从线性放缩中明白到:x 被映射到 y 上,长度被拉长了 2 倍。这和单单知道方程是两回事,我认为这比原来理解要好。利用这个映射就可以把 11 个像素点(下标为 0 ~ 10)的图像放大为 21 个像素点大小,使用下面程序:

for(y = 0; y < 21; y ++){
x = y / 2;
newPicture[y] = oldPicture[x];
}
  就这样把图像的宽度扩大后再扩大高度就可完成整个图像的放大,缩小也一样,把直线段的斜率减小就行。直线段代表线性放大,那么抛物线等曲线呢,分段曲线呢?它们都可以实现放缩,因为都是 x 到 y 的映射,曲线代表的是映射规则。呵呵,都怪中学没学好!

  在实际放缩时不可能都除得整数,这时就有两种解决方法,一是插值,上面的右图就是线性插值的示意图,二是取整数部分,也就是最近点法,这两种方法的效果和运算量大家都明瞭,自不必我说。

  此放缩原理还可以应用于灰度拉伸等方面,对比度增减就是一个例子。

  现在摄像头热门的人脸跟踪也是应用图像处理的,其它高级的物体识别、运动分析等都是基于图像处理,其应用前景十分广阔,您足可投身畅游其中,只要您有足够的兴趣和知识。使用图像处理时您是否有这样一个想法