{"id":16119486,"url":"https://github.com/downgoon/video-motion-detection","last_synced_at":"2025-03-18T10:31:31.819Z","repository":{"id":77273985,"uuid":"189621873","full_name":"downgoon/video-motion-detection","owner":"downgoon","description":"视频AI科普教程——视频运动检测","archived":false,"fork":false,"pushed_at":"2020-10-13T13:36:08.000Z","size":2450,"stargazers_count":15,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-02-28T09:11:31.188Z","etag":null,"topics":["camera","camera-capture","javacv","motion-detection","opencv","video-ai"],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/downgoon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-05-31T15:59:40.000Z","updated_at":"2024-11-15T11:58:45.000Z","dependencies_parsed_at":null,"dependency_job_id":"cbb30a17-208b-4669-bcf5-3ccf3505b1d4","html_url":"https://github.com/downgoon/video-motion-detection","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/downgoon%2Fvideo-motion-detection","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/downgoon%2Fvideo-motion-detection/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/downgoon%2Fvideo-motion-detection/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/downgoon%2Fvideo-motion-detection/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/downgoon","download_url":"https://codeload.github.com/downgoon/video-motion-detection/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243922056,"owners_count":20369338,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["camera","camera-capture","javacv","motion-detection","opencv","video-ai"],"created_at":"2024-10-09T20:54:17.150Z","updated_at":"2025-03-18T10:31:31.814Z","avatar_url":"https://github.com/downgoon.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 视频运动检测\n\n\u003c!-- MDTOC maxdepth:6 firsth1:1 numbering:0 flatten:0 bullets:1 updateOnSave:1 --\u003e\n\n- [视频运动检测](#视频运动检测)   \n   - [程序设计原理](#程序设计原理)   \n   - [运动检测算法](#运动检测算法)   \n      - [像素划分区块](#像素划分区块)   \n      - [区块运动像素](#区块运动像素)   \n      - [判定运动区块](#判定运动区块)   \n      - [合并邻居区块](#合并邻居区块)   \n      - [标记合并区块](#标记合并区块)   \n\n\u003c!-- /MDTOC --\u003e\n\n\n这是一篇视频AI的科普教程。视频AI有什么用呢？\n\n举个例子，比如用摄像头来看门，通常情况下无人打扰时，摄像头画面是相对静止的，如果突然出现运动的人或物，就会在画面中标记出来或者短信通知工作人员。如下图所示：\n\n![image-20190530232600655](assets/image-20190530232600655.png)\n\n\n\n本文将用一个Java程序来展示这个功能，并详述其原理，让每个小伙伴们都能入门视频AI。\n\n\n\n## 下载并体验AI\n\n\n\n``` bash\n$ wget https://github.com/downgoon/video-motion-detection/releases/download/0.1.0/video-motion-detection-0.1.0.tar.gz\n$ tar zxvf video-motion-detection-0.1.0.tar.gz\n$ cd video-motion-detection-0.1.0\n$ bin/video-motion-detection console\n\n```\n\n\n\n此时电脑的摄像头会被开启，对着摄像头摇摇头？AI会标记出运动区域。\n\n顺便说一下，如果想自己编译，请执行：\n\n``` bash\n$ git checkout 0.1.0\n$ mvn clean package\n$ file target/video-motion-detection-0.1.0.tar.gz\n```\n\n\n\n\n## 程序设计原理\n\n![image-20190531090416424](assets/image-20190531090416424.png)\n\n\n\n整个程序分3个环节：\n\n1. **抓帧**： 通过``JavaCV``（OpenCV的Java接口）连接电脑本地的摄像头，并快速\u0026不断地抓帧（可以理解为拍照，这里的``帧``是图片的别名）。\n2. **检测**： 前后两帧（也就是两幅图片）进行比较，找出它们中的不同，结合一定算法，判断这种不同是否属于运动，并对运动的区块用方框标记出来。\n3. **推图**： 无论图片有没有运动区块，都往``ImagePanel``上贴图，要求快并持续，快到超过人类的视觉暂留，比如每秒贴图24张，人眼看到的就是视频了（``moving pictures``）。\n\n\n\n顺便说一下，如果只是要从摄像头录制视频，上述第2步是一个可选项，运动检测可以理解为图片数据流的一个``过滤器``。\n\n\n\n## 运动检测算法\n\n\n\n上述流程中，第2环节的运动检测，介绍个朴素算法以便大家入门。这个算法简单讲就是比对前后两帧的每个像素，像素差值大于一定程度，就标记为``运动像素``。\n用于演示这个算法过程的Excel动画，请点击下载 [video-motion-detection.xlsx](assets/video-motion-detection.xlsx)。\n\n\n### 像素划分区块\n\n![image-20190531120821302](assets/image-20190531120821302.png)\n\n一个图片是由横纵的点阵构成的，每个点被称作为``像素``，每个像素通常是4个字节的整形数，每个字节被称作``通道``。其中第1个字节描述``alpha``通道，第2个是RGB三色体系的红色，第3是绿色，第4是蓝色。\n\n上述示意图中，灰色小格子就是``像素``。为了后面的AI识别，我们按10x10划分区块``Block``（图中是5x5，绿色标线标注的）。\n\n\n\n### 区块运动像素\n\n接着就要计算前后两张图片（帧）的差异，MxN点阵中所有像素的``αRGB``的取值差，差值大于一定程度，就被认为是``运动像素``。并对每个区块，统计运动像素的个数。\n\n\n\n代码``VideoMotionDetector#doBlockMotionCount.java``描述了这段逻辑：\n\n``` java\n\n/**\n* 色差阈值：相邻的两帧，在同一像素点位置，如果色差大于阈值，则判定为运动像素。\n*/\nprivate int colorDiffThreshold = 30;\n\nprivate void doBlockMotionCount(MatrixImage currImage, MatrixImage diffImage) {\n        for (int y = 0; y \u003c currImage.getHeight(); y++) {\n            for (int x = 0; x \u003c currImage.getWidth(); x++) {\n\n                // 当前图在(x,y)点的RGB值\n                int cR = currImage.rgbR(x, y);\n                int cG = currImage.rgbG(x, y);\n                int cB = currImage.rgbB(x, y);\n\n                // 对比图在相同点的RGB值\n                int dR = diffImage.rgbR(x, y);\n                int dG = diffImage.rgbG(x, y);\n                int dB = diffImage.rgbB(x, y);\n\n                // 两个点在RGB的任一通道色差大于色差阈值，则判定为\"运动像素\"\n                if (Math.abs(cR - dR) \u003e colorDiffThreshold\n                        || Math.abs(cG - dG) \u003e colorDiffThreshold\n                        || Math.abs(cB - dB) \u003e colorDiffThreshold) {\n\n                    // 区块内\"运动像素\"计数器累加1\n                    int blockX = x / blockSizeThreshold;\n                    int blockY = y / blockSizeThreshold;\n                    blockMotionCount[blockX][blockY]++;\n                }\n\n            }\n        }\n    }\n```\n\n\n\n不妨假设运动像素的分布是下面的样子：\n\n![image-20190531122730567](assets/image-20190531122730567.png)\n\n图中黄色（黄色充满运动感）荧光笔填涂的就是运动像素。\n\n\n\n### 判定运动区块\n\n统计完每个区块的``运动像素``的数量后，接下来就要判定这个区块是否是``运动区块``。判别的标准就是一个区块内超过半数是运动像素，它就是运动区块。\n\n\n\n代码``VideoMotionDetector#isMotionBlock.java``描述了这段逻辑：\n\n``` java\n\n\n/**\n* 区块粒度：将一张图片，切成很多个区块。每个区块大小为10像素*10像素。\n*/\nprivate int blockSizeThreshold = 10;\n\n\n/**\n* 判断一个区块是否是运动区块\n*/\nprivate boolean isMotionBlock(int blockX, int blockY) {\n   int halfBlockPixels = (blockSizeThreshold * blockSizeThreshold) / 2;\n   // 如果区块内有超过半数是运动像素，则区块判别为\"运动区块\"\n   return blockMotionCount[blockX][blockY] \u003e halfBlockPixels;\n}\n\n```\n\n\n\n![image-20190531134418114](assets/image-20190531134418114.png)\n\n\n\n上述图例中，每个区块如果有大于5个的运动像素，就被定义为``运动区块``（左上角的那个区域虽然有运动像素，但是不够5个，判定为非运动区块）。\n\n\n\n算法首先找到第一个``运动区块``，返回一个``Rect``，如图用``LT``标记的left-top左顶点，用``RB``标记的right-bottom右底点。\n\n\n\n### 合并邻居区块\n\n\n\n对于一个运动区块，合并周围5格近邻的运动区块。从左到右，从上而下，找到临近的运动区块，且尚未标记为运动的，把它纳入进来。\n\n\n\n代码``VideoMotionDetector#mergeNeighborBlocks.java``描述了这段逻辑：\n\n``` java\n\n\t/**\n     * 合并周围5格近邻的运动区块\n     */\n    private void mergeNeighborBlocks(int blockX, int blockY, Rect pixelRect) {\n        for (int nbx = blockX - 5; nbx \u003c blockX + 5; nbx++) {\n            for (int nby = blockY - 5; nby \u003c blockY + 5; nby++) {\n\n                // 邻居区块：某个指定区块周围5个区块\n                boolean isNear5 = (nbx \u003e 0 \u0026\u0026 nbx \u003c pixelWidth / blockSizeThreshold)\n                        \u0026\u0026 (nby \u003e 0 \u0026\u0026 nby \u003c pixelHeight / blockSizeThreshold);\n\n                if (isNear5) {\n\n                    // 对于邻居区块是运动的，且尚未被标记的\n                    if (isMotionBlock(nbx, nby) \u0026\u0026 !blockMotionJudge[nbx][nby]) {\n\n                        // 跟邻居对比，左顶点往左靠\n                        if (nbx * blockSizeThreshold \u003c pixelRect.getX1()) {\n                            pixelRect.setX1(nbx * blockSizeThreshold);\n                        }\n                        if (nby * blockSizeThreshold \u003c pixelRect.getY1()) {\n                            pixelRect.setY1(nby * blockSizeThreshold);\n                        }\n\n                        // 跟邻居对比，右底点往右靠\n                        if (nbx * blockSizeThreshold \u003e pixelRect.getX2()) {\n                            pixelRect.setX2(nbx * blockSizeThreshold);\n                        }\n                        if (nby * blockSizeThreshold \u003e pixelRect.getY2()) {\n                            pixelRect.setY2(nby * blockSizeThreshold);\n                        }\n\n                        // 把\"邻居区块\"也标记为\"运动区块\"\n                        blockMotionJudge[nbx][nby] = true;\n\n                        // 每当标记一个运动区域时，立即找出它的邻居区域也是运动的，但是尚未标记的\n                        mergeNeighborBlocks(nbx, nby, pixelRect);\n                    }\n                }\n            }\n        }\n    }\n\n```\n\n\n\n代码图示：两个运动区块，合并成一个大的区块，``LT``和``RB``分别标记合并后的区块坐标。\n\n![image-20190531141344522](assets/image-20190531141344522.png)\n\n\n\n继续拓展，会把下面那个区块也合并进来，如下图：\n\n![image-20190531142136773](assets/image-20190531142136773.png)\n\n\n\n### 标记合并区块\n\n\n\n合并后，整个大区块都是运动区域，我们要在这个大区域上画个方框标记出来。代码``MontionMarker#startCamera2PanelContinuously.java``：\n\n\n\n``` java\n\nMatrixImage showFrame = currFrame;\n\nif (lastFrame != null) {\n\t// motion detection\n\tList\u003cRect\u003e motionRegions = motionDetecor.detect(currFrame, lastFrame);\n\n\tfor (int i = 0; i \u003c motionRegions.size(); i++) {\n\t\tRect rect = motionRegions.get(i);\n\n\t\tMatrixImage markFrame = new MatrixImage(imageWidth, imageHeight);\n\t\t// deep copy and motion mark\n\t\tMatrixImage.copyRgbArray(currFrame, markFrame);\n\n\t\t// mark motion block with a tiffany blue rectangle\n\t\t// NICE-RED: 0xF01D39   NICE-BLUE: 0x0F76C1  TIFFANY-BLUE: 0x81d8cf\n\t\tmarkFrame.drawRect(rect.getX1(), rect.getY1(),\n                           rect.getWidth(), rect.getHeight(), 2,\n                           new Color(0x81d8cf));\n\n\t\tshowFrame = markFrame;\n\t } // end for\n}\n\nlastFrame = currFrame;\n\n// push original current image or marked image into the panel\nimagePanel.setMatrixImage(showFrame);\n\n```\n\n\n\n标记效果如下图所示：\n\n![image-20190531142838996](assets/image-20190531142838996.png)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdowngoon%2Fvideo-motion-detection","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdowngoon%2Fvideo-motion-detection","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdowngoon%2Fvideo-motion-detection/lists"}