基于OpenCV和Tensorflow的深蹲检测器
本期我们将介绍如和使用OpenCV以及Tensorflow实现深蹲检测
在检疫期间,我们的体育活动非常有限,这样并不好。在进行一些居家运动时,我们必须时刻保持高度的注意力集中,以便记录自己每天的运动量。因此我们希望建立一个自动化的系统来实现运动量计算。考虑到我们在深蹲时,有明确阶段和大幅度变化的基本运动,实现对深蹲的计数会相对比较简单。
下面我们就一起尝试实现它吧!
数据采集
使用带相机的Raspberry Pi来获取图片是非常方便的,完成图像的拍摄后再利用OpenCV即可将获取的图像写入文件系统。
运动识别
最初,我们打算使用图像分割完成人物的提取工作。但是我们都知道图像分割是一项非常繁琐的操作,尤其是在Raspberry资源有限的情况下。
除此之外,图像分割忽略了一个事实。当前我们所拥有的是一系列图像帧,而不是单个图片。该图像序列具有明显功能,并且我们后续将要使用到它。
因此,我们从OpenCV 着手进行背景去除,以提供了可靠的结果。
背景扣除
首先,创建一个背景减法器:
backSub = cv.createBackgroundSubtractorMOG2()
向其中添加图像帧:
mask = backSub.apply(frame)
最后我们可以得到一张带有身体轮廓的图片:
然后扩大图像以突出轮廓。
mask = cv.dilate(mask, None, 3)
将此算法应用于所有图像帧可以得出每一幅图像中的姿势。之后,我们将它们分类为站立,下蹲以及无三种情况。
接下来我们要把图像中的人提取出来,OpenCV可以帮助我们找到相应的找到轮廓:
cnts, _ = cv.findContours(img, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE)
这种方法或多或少适用于人物的最大轮廓的提取,但不幸的是,这样处理的结果并不稳定。例如,检测得到最大的轮廓只能包括人的身体,而不包括他的脚。
但不管怎么说,拥有一系列图像对我很有帮助。通常情况下我们做深蹲运动都发生在同一地点,因此我们可以假设所有动作都在某个区域内进行并且该区域是稳定的。为此我们可以迭代构建边界矩形,如果需要,可以以最大轮廓增加边界矩形。
有一个例子:
• 最大的轮廓是红色
• 轮廓边界矩形为蓝色
• 图边界矩形为绿色
通过以上的边缘提取以及轮廓绘制,可以为进一步处理做好充足准备。
分类
接下来我们将从图像中提取出边界矩形,并将其转化为按尺寸64x64正方形。
以下Mask用作分类器输入:
站立姿势:
下蹲姿势:
接下来我们将使用Keras 与Tensorflow进行分类。
最初,我们使用了经典的Lenet-5模型,运行结果良好。随后由于阅读了一些有关Lenet-5变体的文章后,我们决定尝试简化架构。
事实证明,简化后的CNN在当前示例中的精度几乎相同:
model = Sequential([
Convolution2D(8,(5,5), activation='relu', input_shape=input_shape),
MaxPooling2D(),
Flatten(),
Dense(512, activation='relu'),
Dense(3, activation='softmax')
])
model.compile(loss="categorical_crossentropy", optimizer=SGD(lr=0.01), metrics=["accuracy"])
10个纪元的准确度为86%,20个的准确度为94%,而30个的准确度为96%。训练如果在增加的话可能会导致过拟合引起准确度的下降,因此接下来我们将把这个模型运用到生活中去。
模型运用
我们将在Raspberry上运行。
加载模型:
with open(MODEL_JSON, 'r') as f:
model_data = f.read()
model = tf.keras.models.model_from_json(model_data)
model.load_weights(MODEL_H5)
graph = tf.get_default_graph()
并以此对下蹲Mask进行分类:
img = cv.imread(path + f, cv.IMREAD_GRAYSCALE)
img = np.reshape(img,[1,64,64,1])
with graph.as_default():
c = model.predict_classes(img)
return c[0] if c else None
在Raspberry上,输入为64x64的分类调用大约需要60-70毫秒,几乎接近实时。
最后让我们将以上所有部分整合到一个应用程序中:
• GET / —一个应用页面(下面有更多信息)
• GET / status-获取当前状态,下蹲次数和帧数
• POST / start —开始练习
• POST / stop —完成练习
• GET / stream —来自摄像机的视频流
- 我的职业是前端工程师【十】客户端存储艺术:数据存储与模型
- 【开源】2md:将复制的内容、网页转成 markdown
- React Native 持续部署实践— push 代码构建出新版的 Growth
- 技巧 - 如何好一个 Git 提交信息及几种不同的规范
- React、Vue、Ember 及其他前端开发者,请暂缓更新到 Chrome 59 浏览器
- 微软开源全新的文档生成工具DocFX
- 使用 MimeKit 和 MailKit 发送邮件
- 使用 React Native 重写大型 Ionic 应用后,我们想分享一下这八个经验
- 基于OWin的Web服务器Katana发布版本3
- 【工具推荐】图像界的魔术师 ImageMagick
- 使用Metrics.NET 构建 ASP.NET MVC 应用程序的性能指标
- 如何设计完善的构建系统,为日常开发提速一倍
- 两年 100 期技术周报后,我收获了这四点
- 如何为技术博客设计一个推荐系统(中):基于 Google 搜索的半自动推荐
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法