Skip to content

downgoon/video-motion-detection

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

视频运动检测

这是一篇视频AI的科普教程。视频AI有什么用呢?

举个例子,比如用摄像头来看门,通常情况下无人打扰时,摄像头画面是相对静止的,如果突然出现运动的人或物,就会在画面中标记出来或者短信通知工作人员。如下图所示:

image-20190530232600655

本文将用一个Java程序来展示这个功能,并详述其原理,让每个小伙伴们都能入门视频AI。

下载并体验AI

$ wget https://github.com/downgoon/video-motion-detection/releases/download/0.1.0/video-motion-detection-0.1.0.tar.gz $ tar zxvf video-motion-detection-0.1.0.tar.gz $ cd video-motion-detection-0.1.0 $ bin/video-motion-detection console 

此时电脑的摄像头会被开启,对着摄像头摇摇头?AI会标记出运动区域。

顺便说一下,如果想自己编译,请执行:

$ git checkout 0.1.0 $ mvn clean package $ file target/video-motion-detection-0.1.0.tar.gz

程序设计原理

image-20190531090416424

整个程序分3个环节:

  1. 抓帧: 通过JavaCV(OpenCV的Java接口)连接电脑本地的摄像头,并快速&不断地抓帧(可以理解为拍照,这里的是图片的别名)。
  2. 检测: 前后两帧(也就是两幅图片)进行比较,找出它们中的不同,结合一定算法,判断这种不同是否属于运动,并对运动的区块用方框标记出来。
  3. 推图: 无论图片有没有运动区块,都往ImagePanel上贴图,要求快并持续,快到超过人类的视觉暂留,比如每秒贴图24张,人眼看到的就是视频了(moving pictures)。

顺便说一下,如果只是要从摄像头录制视频,上述第2步是一个可选项,运动检测可以理解为图片数据流的一个过滤器

运动检测算法

上述流程中,第2环节的运动检测,介绍个朴素算法以便大家入门。这个算法简单讲就是比对前后两帧的每个像素,像素差值大于一定程度,就标记为运动像素。 用于演示这个算法过程的Excel动画,请点击下载 video-motion-detection.xlsx

像素划分区块

image-20190531120821302

一个图片是由横纵的点阵构成的,每个点被称作为像素,每个像素通常是4个字节的整形数,每个字节被称作通道。其中第1个字节描述alpha通道,第2个是RGB三色体系的红色,第3是绿色,第4是蓝色。

上述示意图中,灰色小格子就是像素。为了后面的AI识别,我们按10x10划分区块Block(图中是5x5,绿色标线标注的)。

区块运动像素

接着就要计算前后两张图片(帧)的差异,MxN点阵中所有像素的αRGB的取值差,差值大于一定程度,就被认为是运动像素。并对每个区块,统计运动像素的个数。

代码VideoMotionDetector#doBlockMotionCount.java描述了这段逻辑:

/** * 色差阈值:相邻的两帧,在同一像素点位置,如果色差大于阈值,则判定为运动像素。 */ private int colorDiffThreshold = 30; private void doBlockMotionCount(MatrixImage currImage, MatrixImage diffImage) { for (int y = 0; y < currImage.getHeight(); y++) { for (int x = 0; x < currImage.getWidth(); x++) { // 当前图在(x,y)点的RGB值 int cR = currImage.rgbR(x, y); int cG = currImage.rgbG(x, y); int cB = currImage.rgbB(x, y); // 对比图在相同点的RGB值 int dR = diffImage.rgbR(x, y); int dG = diffImage.rgbG(x, y); int dB = diffImage.rgbB(x, y); // 两个点在RGB的任一通道色差大于色差阈值,则判定为"运动像素" if (Math.abs(cR - dR) > colorDiffThreshold || Math.abs(cG - dG) > colorDiffThreshold || Math.abs(cB - dB) > colorDiffThreshold) { // 区块内"运动像素"计数器累加1 int blockX = x / blockSizeThreshold; int blockY = y / blockSizeThreshold; blockMotionCount[blockX][blockY]++; } } } }

不妨假设运动像素的分布是下面的样子:

image-20190531122730567

图中黄色(黄色充满运动感)荧光笔填涂的就是运动像素。

判定运动区块

统计完每个区块的运动像素的数量后,接下来就要判定这个区块是否是运动区块。判别的标准就是一个区块内超过半数是运动像素,它就是运动区块。

代码VideoMotionDetector#isMotionBlock.java描述了这段逻辑:

/** * 区块粒度:将一张图片,切成很多个区块。每个区块大小为10像素*10像素。 */ private int blockSizeThreshold = 10; /** * 判断一个区块是否是运动区块 */ private boolean isMotionBlock(int blockX, int blockY) { int halfBlockPixels = (blockSizeThreshold * blockSizeThreshold) / 2; // 如果区块内有超过半数是运动像素,则区块判别为"运动区块" return blockMotionCount[blockX][blockY] > halfBlockPixels; }

image-20190531134418114

上述图例中,每个区块如果有大于5个的运动像素,就被定义为运动区块(左上角的那个区域虽然有运动像素,但是不够5个,判定为非运动区块)。

算法首先找到第一个运动区块,返回一个Rect,如图用LT标记的left-top左顶点,用RB标记的right-bottom右底点。

合并邻居区块

对于一个运动区块,合并周围5格近邻的运动区块。从左到右,从上而下,找到临近的运动区块,且尚未标记为运动的,把它纳入进来。

代码VideoMotionDetector#mergeNeighborBlocks.java描述了这段逻辑:

/**  * 合并周围5格近邻的运动区块  */ private void mergeNeighborBlocks(int blockX, int blockY, Rect pixelRect) { for (int nbx = blockX - 5; nbx < blockX + 5; nbx++) { for (int nby = blockY - 5; nby < blockY + 5; nby++) { // 邻居区块:某个指定区块周围5个区块 boolean isNear5 = (nbx > 0 && nbx < pixelWidth / blockSizeThreshold) && (nby > 0 && nby < pixelHeight / blockSizeThreshold); if (isNear5) { // 对于邻居区块是运动的,且尚未被标记的 if (isMotionBlock(nbx, nby) && !blockMotionJudge[nbx][nby]) { // 跟邻居对比,左顶点往左靠 if (nbx * blockSizeThreshold < pixelRect.getX1()) { pixelRect.setX1(nbx * blockSizeThreshold); } if (nby * blockSizeThreshold < pixelRect.getY1()) { pixelRect.setY1(nby * blockSizeThreshold); } // 跟邻居对比,右底点往右靠 if (nbx * blockSizeThreshold > pixelRect.getX2()) { pixelRect.setX2(nbx * blockSizeThreshold); } if (nby * blockSizeThreshold > pixelRect.getY2()) { pixelRect.setY2(nby * blockSizeThreshold); } // 把"邻居区块"也标记为"运动区块" blockMotionJudge[nbx][nby] = true; // 每当标记一个运动区域时,立即找出它的邻居区域也是运动的,但是尚未标记的 mergeNeighborBlocks(nbx, nby, pixelRect); } } } } }

代码图示:两个运动区块,合并成一个大的区块,LTRB分别标记合并后的区块坐标。

image-20190531141344522

继续拓展,会把下面那个区块也合并进来,如下图:

image-20190531142136773

标记合并区块

合并后,整个大区块都是运动区域,我们要在这个大区域上画个方框标记出来。代码MontionMarker#startCamera2PanelContinuously.java

MatrixImage showFrame = currFrame; if (lastFrame != null) { // motion detection List<Rect> motionRegions = motionDetecor.detect(currFrame, lastFrame); for (int i = 0; i < motionRegions.size(); i++) { Rect rect = motionRegions.get(i); MatrixImage markFrame = new MatrixImage(imageWidth, imageHeight); // deep copy and motion mark MatrixImage.copyRgbArray(currFrame, markFrame); // mark motion block with a tiffany blue rectangle // NICE-RED: 0xF01D39 NICE-BLUE: 0x0F76C1 TIFFANY-BLUE: 0x81d8cf markFrame.drawRect(rect.getX1(), rect.getY1(), rect.getWidth(), rect.getHeight(), 2, new Color(0x81d8cf)); showFrame = markFrame; } // end for } lastFrame = currFrame; // push original current image or marked image into the panel imagePanel.setMatrixImage(showFrame);

标记效果如下图所示:

image-20190531142838996

About

视频AI科普教程——视频运动检测

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors