forked from RimoChan/Vtuber_Tutorial
-
Notifications
You must be signed in to change notification settings - Fork 0
/
readme.md
222 lines (154 loc) · 8.76 KB
/
readme.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# 从零开始的自制Vtuber: 1.面部捕捉初级
这是一个自制Vtuber的教程,这个教程包含从面部捕捉到图形渲染的一条龙服务。
这个教程是迭代式的,因此每当你看完一节时都可以做出一个Vtuber,它会随着你的学习而越发强大。
也许你能从中学到很多知识,但请不要跟着这个教程做,因为我有一些操作过于神奇以至于成了反面教材。
在第一节里,我们将会简单地体验一下面部捕捉技术,然后用一些数学计算追踪头部的角度,最后用一个简单的角色来展示今天的成果。
## 目录
1. 面部捕捉初级 (就是这个文件所以没有链接)
2. [画图和绘图](2.md)
3. [进入虚空](3.md) (从这里开始命名开始放飞自我了)
4. [合成进化](4.md)
5. [一时休战](5.md)
6. [与神之假身的接触](6.md) (现在还是草稿)
7. [还没写!](7.md) (你以为有链接所以这真是个章节但实际上不是)
8. [真的还没写!](8.md) (没想到还没写居然会多出一个吧!)
## 警告
这个章节还没有完成校订,因此可能有和谐内容。
请您收好鸡儿,文明观球。
## 准备
在这个章节,你需要准备:
+ 电脑
+ 摄像头
+ 基本的图形图像概念
+ 高中的计算几何知识
+ Python3
+ Dlib
+ OpenCV
+ NumPy
明明是从零开始却用了好多库!
事情是这样的,根据皮亚诺公理,凡是你认为是0的东西就是0。
## 读取视频流
首先我们使用OpenCV从摄像头读取视频流,并尝试着把它播放在窗口上。
```python
cap = cv2.VideoCapture(0)
while True:
ret, img = cap.read()
img = cv2.flip(img, 1)
cv2.imshow('self', img)
cv2.waitKey(1)
```
> OpenCV 4.4.0.40 会出现 [Segment fault when call imshow](https://github.com/opencv/opencv/issues/18079) 的错误,最好使用其他版本。
`cv2.VideoCapture(0)`表示默认摄像头,如果用外置摄像头可能得把它改成1。
`cv2.flip`的作用是把帧左右翻转,使你看起来像在照镜子一样。
![./图/1-1.jpg](./图/1-1.jpg)
完成之后,你应当会获得一个这样的窗口。
如果你的窗口里的人和长得和上面的不一样,这是正常的。
## 提取面部关键点
接下来我们将使用Dlib提取面部关键点。因为Dlib挺好用的,所以我们暂时不用去想它是怎么做到的。
在这之前,我们得先确保`img`中至少有一张人脸,并找到最大的那张脸。
之所以要这么做并不是为了防止萝莉在旁边捣乱<sub>(她的脸比较小)</sub>,而是防止有时把环境里某些看起来像人头的小物件识别成脸而引起面部捕捉的错误。
```python
detector = dlib.get_frontal_face_detector()
def 人脸定位(img):
dets = detector(img, 0)
if not dets:
return None
return max(dets, key=lambda det: (det.right() - det.left()) * (det.bottom() - det.top()))
```
<sub>(代码逐渐缺德2333)</sub>
接下来,我们就可以对刚才找到的脸位置提取关键点了。为了让接下来的向量运算方便一些,你可以考虑把他们通通转换成`np.array`。
如果你没有`shape_predictor_68_face_landmarks.dat`,可以去 [GitHub](https://github.com/AKSHAYUBHAT/TensorFace/blob/master/openface/models/dlib/shape_predictor_68_face_landmarks.dat) 或 [dlib](http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2) 下载。
```python
predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
def 提取关键点(img, 脸位置):
landmark_shape = predictor(img, 脸位置)
关键点 = []
for i in range(68):
pos = landmark_shape.part(i)
关键点.append(np.array([pos.x, pos.y], dtype=np.float32))
return 关键点
```
接下来试着把这些关键点在你的摄像头画面上绘制出来——
```python
for i, (px, py) in enumerate(关键点):
cv2.putText(img, str(i), (int(px), int(py)), cv2.FONT_HERSHEY_COMPLEX, 0.25, (255, 255, 255))
```
![./图/1-2.jpg](./图/1-2.jpg)
如果一切正确的话,你的窗口将会像上面这样,有种戴上了奇怪的面具的感觉。
## 计算面部特征
第一天我们先来试着提取一点简单的特征吧,就做「头的左右旋转」好了。
这个原理非常简单以至于我们可以用手来实现它。
我们知道,越远离旋转轴的点,受到旋转的影响就越大,也就是说,我把头向左转的时候,尽管我的眉毛和鼻子都向左移动了,但是鼻子一定在比眉毛还左的左边。
接下来我们根据关键点计算一下它们的座标,就可以计算出旋转角度了。
```python
def 生成构造点(关键点):
def 中心(索引数组):
return sum([关键点[i] for i in 索引数组]) / len(索引数组)
左眉 = [18, 19, 20, 21]
右眉 = [22, 23, 24, 25]
下巴 = [6, 7, 8, 9, 10]
鼻子 = [29, 30]
return 中心(左眉 + 右眉), 中心(下巴), 中心(鼻子)
```
你可以模仿上面画关键点的代码在图上把它们画出来,它们应该长这样——
![./图/1-3.jpg](./图/1-3.jpg)
可以看出`眉中心` `下巴中心` `鼻子中心`组成了一个三角形,我们希望能从这个三角形里提取出一些有用的特征。而且因为各种不同的摄像头的画面大小都不太一样,最好这些特征都具有缩放不变性。
这个三角形的形状是和脸部的角度有关的。三角形越是瘦长,脸部就越是中正。因此你只要用一点向量知识来计算它的胖瘦就可以得到脸部的旋转量。
任意两条边做叉乘就能得到三角形的有向面积,除以底边长就能得到对应底边上的有向高,再除一次底边得到的就是高与底的比值了。
```python
def 生成特征(构造点):
眉中心, 下巴中心, 鼻子中心 = 构造点
中线 = 眉中心 - 下巴中心
斜边 = 眉中心 - 鼻子中心
横旋转量 = np.cross(中线, 斜边) / np.linalg.norm(中线)**2
return 横旋转量
```
我没有用到三角函数,这是因为我们的检测器没法检测太大的角度,而sin(x)在原点附近的导数几乎是1,所以这样就够了。
把旋转量画在鼻子上,它看起来是这样——
![./图/1-4-1.jpg](./图/1-4-1.jpg)
![./图/1-4-2.jpg](./图/1-4-2.jpg)
如果你觉得只有这样的左右旋转看起来有点无聊,我们也可以再来添加一个检测一个上下旋转的特征。
它的数学表达式和刚才的横旋转量很接近,你们可能已经猜出来了,这个特征只需要求斜边在底边上的投影长度与底边长的比值——
```
横旋转量 = np.cross(中线, 斜边) / np.linalg.norm(中线)**2
竖旋转量 = 中线 @ 斜边 / np.linalg.norm(中线)**2
```
如果你用起来的效果很差,说明你的鼻子太扁了,你可以考虑整容来解决这个问题。
## 添加标准脸
你肯定发现了这样一个问题,我们刚才检测了`横旋转量`和`竖旋转量`,可是因为普通人的脸是左右对称的,不是上下对称的,所以正对镜头时的`竖旋转量`并不是0。
为了解决这个问题,我们可以给竖旋转量减去固定一个数值,不过更好的方法是添加一个标准脸图片。
你只要正对镜头的时候拍个照,把它的特征保存下来作为一个相对的原点,之后把每次新检测到的特征减去原点就行了。
```Python
原点特征组 = 提取图片特征(cv2.imread('std_face.jpg'))
while True:
ret, img = cap.read()
新特征组 = 提取图片特征(img)
if 新特征组 is not None:
特征组 = 新特征组 - 原点特征组
```
## 绘图
现在我们马上就要做出第一个会动的Vtuber了,因为画画很麻烦所以先用圆圈凑合一下。
只要画上几个圈圈,让它们的座标取决于脸部的旋转量,这张圆圈脸看起来就好像跟着人脸在旋转一样——
```python
def 画图(横旋转量, 竖旋转量):
img = np.ones([512, 512], dtype=np.float32)
脸长 = 200
中心 = 256, 256
左眼 = int(220 + 横旋转量 * 脸长), int(249 + 竖旋转量 * 脸长)
右眼 = int(292 + 横旋转量 * 脸长), int(249 + 竖旋转量 * 脸长)
嘴 = int(256 + 横旋转量 * 脸长 / 2), int(310 + 竖旋转量 * 脸长 / 2)
cv2.circle(img, 中心, 100, 0, 1)
cv2.circle(img, 左眼, 15, 0, 1)
cv2.circle(img, 右眼, 15, 0, 1)
cv2.circle(img, 嘴, 5, 0, 1)
return img
```
![./图/1-5.jpg](./图/1-5.jpg)
这一节就到此为止了……这东西真的能叫Vtuber吗<sub>(笑)</sub>。
这样吧,下一节我们先来画一张立绘。
只要让真正的立绘动起来,就能做出一个可爱的Vtuber。
## 结束
如果我的某些操作让你非常迷惑,你也可以去这个项目的GitHub仓库查看源代码。
最后祝各位鸡儿放假。
下一节:
+ [从零开始的自制Vtuber: 2.画图和绘图](2.md)