大家好我是j3llypunk
👋,最近摸🐟时突然想到了小时候看精灵宝可梦
时的经典场景👇
每到这个桥段的时候我都会兴高采烈地跟着喊出宝可梦的名字:皮卡丘!杰尼龟!小火龙!...
如今由于工作和生活的原因,再也没有时间和心境去重温那些千奇百怪、可爱又迷人的反派...宝可梦了
虽然童年一去不复返,但是我滴童心天地可鉴🧒,所以就有了这篇复刻宝可梦中我是谁
的文章
既然要复刻,普普通通写点页面样式可没什么意思,不如我们玩(折腾)点有意思的
利用机器学习
的技术将任意带有人体
的照片扣出其轮廓并输出 canvas
展示出来
效果预览
🔗链接:我是谁???--🐔
由于项目部署在Github Page
上,所以基于你的网络环境可能会出现无法访问的情况,参考解决方案
开始之前
要在web端使用机器学习相关的功能,目前比较知名和稳定的第三方库当属tensorflow-js了,官方为我们训练好了许多开箱即用模型,只需按需引入即可使用其相关的能力,部分模型如下所示
你可以访问官网来查看所有模型
本篇文章主要用到的模型是#Body Segmentation
一般图像识别模型
的图像源都可以是视频
和图片
两种形式,本篇将以图片的形式提供给模型使用,其实两者相差不大,视频也是一帧一帧的图片组合而成的
那么我们开始吧💪
安装依赖
js
npm install @mediapipe/selfie_segmentation @tensorflow-models/body-segmentation @tensorflow/tfjs-backend-webgl @tensorflow/tfjs-converter @tensorflow/tfjs-core
搭建基础页面
html部分
html
<div className={style.whoami}>
<div className={style.photoFrame}> //原始图片和处理后canvas容器
<img src={Wrapper} className={style.wrapper} />
<img
id="image"
className={style.sourceImage}
src={Ji}
alt="ji"
crossOrigin="anonymous"
/>
<canvas
id="myCanvas"
style={{ width: '360px', height: '384px' }}
></canvas>
</div>
<div className={style.tips}> //文本部分
<div className={style.question}>????</div>
<div className={style.answer}>🐔</div>
<div className={style.content}>我是誰</div>
</div>
<img src={Logo} className={style.logo} /> //宝可梦logo
</div>
css部分
css
.whoami {
width: 100%;
height: 100vh;
background-color: #172242;
position: relative;
.photoFrame {
width: 407px;
height: 443px;
position: absolute;
left: 10vw;
top: 20vh;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
.wrapper {
position: absolute;
left: 0;
top: 0;
width: 407px;
height: 443px;
z-index: 100;
}
.sourceImage {
position: absolute;
left: 23px;
top: 33px;
z-index: 50;
width: 360px;
height: 384px;
opacity: 0;
transition: 1s ease-in-out;
}
canvas {
transition: 1s ease-in-out;
}
&:hover {
.sourceImage {
opacity: 1;
}
canvas {
opacity: 0;
}
}
}
.logo {
width: 400px;
position: absolute;
right: 5vw;
bottom: 5vh;
}
.tips {
width: 400px;
position: absolute;
right: 20vw;
top: 20vh;
font-weight: bolder;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
transform: scale(1.5);
cursor: pointer;
.question {
color: #c0ac64;
font-size: 70px;
letter-spacing: 5px;
transition: 0.5s ease-in-out;
}
.answer {
position: absolute;
left: 50%;
top: 40px;
opacity: 0;
transform: scale(5);
transition: 0.5s ease-in-out;
}
&:hover {
.question {
opacity: 0;
}
.answer {
opacity: 1;
}
}
.content {
letter-spacing: 10px;
color: #c0ac64;
font-size: 60px;
}
}
}
编写核心代码
首先将我们用到的资源import
进来
js
...
import * as bodySegmentation from '@tensorflow-models/body-segmentation';
import '@tensorflow/tfjs-backend-webgl';
import '@tensorflow/tfjs-converter';
import '@tensorflow/tfjs-core';
...
然后加载我们用到的模型
js
const model = bodySegmentation.SupportedModels.BodyPix;
const segmenterConfig = {
runtime: 'tfjs',
modelType: 'general',
architecture: 'ResNet50',
outputStride: 16,
quantBytes: 4,
};
const segmenter = await bodySegmentation.createSegmenter(
model,
segmenterConfig,
);
这一步我们实例化了一个模型对象,并为其传入模型和所需配置后,调用 createSegmenter
方法创建了一个 detector
检测器
下面讲解一下关于检测器的配置项说明
-
architecture - 体系结构 - 可以是
MobileNetV1
或ResNet50
。它确定要加载的 BodyPix 体系结构。 -
outputStride - outputStride - 可以是
16
(32
Stride ,受 ResNet 体系结构和 stride16
8
支持,32
并且16
MobileNetV1 体系结构受支持8
)。它指定 BodyPix 模型的输出步幅。值越小,输出分辨率越大,以速度为代价使模型更准确。值越大,模型越小,预测时间越快,但准确性越低。 -
multiplier - 乘数 - 可以是
0.75
或0.50
中的1.0
一种(该值仅由 MobileNetV1 体系结构使用,而不由 ResNet 体系结构使用)。它是所有卷积操作的深度(通道数)的浮点乘数。值越大,层的大小越大,以速度为代价使模型更准确。值越小,模型越小,预测时间越快,但准确性越低。 -
quantBytes - quantBytes - 此参数控制用于权重量化的字节。可用选项包括:
-
4
. 4 bytes per float (no quantization). Leads to highest accuracy and original model size.
4
.每个浮点数 4 个字节(无量化)。实现最高的精度和原始模型尺寸。 -
2
. 2 bytes per float. Leads to slightly lower accuracy and 2x model size reduction.
2
.每个浮点数 2 个字节。导致精度略低,模型尺寸减小 2 倍。 -
1
. 1 byte per float. Leads to lower accuracy and 4x model size reduction.
1
.每个浮点数 1 个字节。导致精度降低和模型尺寸减小 4 倍。
下表包含使用不同量化字节时相应的 BodyPix 2.0 模型检查点大小(widthout gzip):
Architecture 建筑 quantBytes=4 定量字节=4 quantBytes=2 quantBytes=2 quantBytes=1 定量字节=1 ResNet50 ~90MB ~90MB ~45MB ~45兆字节 ~22MB ~22兆字节 MobileNetV1 (1.00) 移动网络V1 (1.00) ~13MB ~13兆字节 ~6MB ~6兆字节 ~3MB ~3兆字节 MobileNetV1 (0.75) 移动网络V1 (0.75) ~5MB ~5兆字节 ~2MB ~2兆字节 ~1MB ~1兆字节 MobileNetV1 (0.50) 移动网络V1 (0.50) ~2MB ~2兆字节 ~1MB ~1兆字节 ~0.6MB ~0.6兆字节 -
-
modelUrl - modelUrl - 指定模型的自定义 URL 的可选字符串。这对于本地开发或无法访问 GCP 上托管的模型的国家/地区非常有用。
接下来我们进行canvas的具体绘制操作,代码如下
js
const image = document.getElementById('image');
const segmentation = await segmenter.segmentPeople(image, {
multiSegmentation: false,
segmentBodyParts: true,
});
const foregroundColor = { r: 0, g: 0, b: 0, a: 255 };
const backgroundColor = { r: 107, g: 220, b: 154, a: 255 };
const backgroundDarkeningMask = await bodySegmentation.toBinaryMask(
segmentation,
foregroundColor,
backgroundColor,
);
const opacity = 1;
const maskBlurAmount = 0;
const flipHorizontal = false;
首先获取页面上原始图片的dom对象将其作为模型的数据源,然后调用实例方法对图片进行识别与分割,分割方法的参数说明如下
- multiSegmentation - 多分段 - 必需。如果设置为 true,则每个人都被分割在一个单独的输出中,否则所有人都在一个细分中被分割在一起。
- segmentBodyParts - 段身体部位 - 必需。如果设置为 true,则在输出中分割 24 个身体部位,否则仅执行前景/背景二进制分割。
- flipHorizontal - 翻转水平 - 默认为 false。如果分割和姿势应该水平翻转/镜像。对于视频默认水平翻转的视频(即网络摄像头),这应该设置为 true,并且您希望以正确的方向返回分割和姿势。
- internalResolution - 内部分辨率 - 默认为
medium
。在推理之前将输入调整为的内部分辨率百分比。模型越大越internalResolution
准确,但代价是预测时间较慢。可用值为low
、medium
high
full
或介于 0 和 1 之间的百分比值。值low
、 和 相应地映射到 0.25、0.5、medium
high
0.75 和full
1.0。 - segmentationThreshold - 分段阈值 - 默认为 0.7。必须介于 0 和 1 之间。对于每个像素,模型估计一个介于 0 和 1 之间的分数,该分数指示在该像素中显示人员部分的置信度。此分割阈值用于将这些值转换为二进制 0 或 1,方法是确定像素分数必须被视为人的一部分的最小值。从本质上讲,较高的值将在人员周围创建更紧密的裁剪,但可能导致某些像素是人的一部分被排除在返回的分割掩码之外。
- maxDetections - 最大检测 - 默认为 10。对于姿势估计,每个图像要检测的最大人物姿势数。
- scoreThreshold - 分数阈值 - 默认为 0.3。对于姿势估计,仅返回根部分分数大于或等于此值的单个人检测。
- nmsRadius - nms半径 - 默认为 20。对于位姿估计,非最大抑制部分距离(以像素为单位)。它需要严格肯定。如果两个部分的距离小于
nmsRadius
像素,则它们会相互抑制。
segmentBodyParts:true
的效果如下所示
在调用分割方法后我们定义了rgba格式
的foregroundColor
和backgroundColor
来对输出canvas的颜色进行控制
而toBinaryMask
的作用则是 模型输出的结果->处理后的图片数据
toBinaryMask
给定分割或分割数组,生成一个图像,每个像素处具有前景色和背景色,由输出中像素处的相应二进制分割值确定。换句话说,有人物的像素将用前景色着色,而没有人的像素将用背景色着色。这可以用作蒙版,在合成时裁剪人物或背景。
Inputs 输入
- segmentation 分段 单个分段或分段数组,例如分段人员的输出。
- foreground 前景色 前景色 (r、g、b、a),用于可视化属于人的像素。
- background 背景色 (r,g,b,a),用于可视化不属于人的像素。
- drawContour 绘制轮廓 是否在每个人的分割掩码周围绘制轮廓。
- foregroundThresholdProbability 将像素着色为前景而不是背景的最小概率。
- foregroundMaskValues 前景掩码值 表示前景的红色通道整数值
Returns 返回
具有与输入分割相同和宽度高度的 ImageData,每个像素处的颜色和不透明度由输出中像素处的相应二进制分割值确定
最后,在拿到处理后的图片数据后我们只需将其渲染至canvas中就大功告成啦🎉
js
let canvas = document.getElementById('myCanvas');
await bodySegmentation.drawMask(
canvas,
image,
backgroundDarkeningMask,
opacity,
maskBlurAmount,
flipHorizontal,
);
贴下完整代码
js
const WhoAmI = () => {
const main = async () => {
const model = bodySegmentation.SupportedModels.BodyPix;
const segmenterConfig = {
runtime: 'tfjs',
modelType: 'general',
architecture: 'ResNet50',
outputStride: 16,
quantBytes: 4,
};
const segmenter = await bodySegmentation.createSegmenter(
model,
segmenterConfig,
);
const image = document.getElementById('image');
const segmentation = await segmenter.segmentPeople(image, {
multiSegmentation: false,
segmentBodyParts: true,
});
const foregroundColor = { r: 0, g: 0, b: 0, a: 255 };
const backgroundColor = { r: 107, g: 220, b: 154, a: 255 };
const backgroundDarkeningMask = await bodySegmentation.toBinaryMask(
segmentation,
foregroundColor,
backgroundColor,
);
const opacity = 1;
const maskBlurAmount = 0;
const flipHorizontal = false;
let canvas = document.getElementById('myCanvas');
await bodySegmentation.drawMask(
canvas,
image,
backgroundDarkeningMask,
opacity,
maskBlurAmount,
flipHorizontal,
);
};
最后来看下我们的效果如何吧
结语
以上就是本篇文章的全部内容了,不知道看完后是否勾起了你某个暑假时的童年回忆呢🧒
最后,都看到这了,如果你觉得这期内容对你有所帮助的话不妨点赞👍
+收藏🌟
+关注👀
支持一下我哦,我们下篇文章再见,see ya~👋