活体检测:keras

日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)


1. 什么是活体检测?
--> 判断捕捉到的人脸是真实人脸,还是伪造的人脸攻击(如:彩色纸张打印人脸图,电子设备屏幕中的人脸数字图像 以及 面具 等)
2. 为什么需要活体检测?
--> 在金融支付,门禁等应用场景,活体检测一般是嵌套在人脸检测与人脸识别or验证中的模块,用来验证是否用户真实本人
3. 活体检测对应的计算机视觉问题:
--> 就是分类问题,可看成二分类(真 or 假);也可看成多分类(真人,纸张攻击,屏幕攻击,面具攻击)

据《人民日报》报道,嘉兴上外秀洲外国语学校402班科学小队向都市快报《好奇实验室》报料:他们在一次课外科学实验中发现,只要用一张打印照片就能代替真人刷脸,骗过小区里的快递智能柜,最终取出父母们的货件。随后,小朋友们还发来了几段视频佐证。

据《人民日报》报道,嘉兴上外秀洲外国语学校402班科学小队向都市快报《好奇实验室》报料:他们在一次课外科学实验中发现,只要用一张打印照片就能代替真人刷脸,骗过小区里的快递智能柜,最终取出父母们的货件。随后,小朋友们还发来了几段视频佐证。

生物识别技术在验证过程中出现的漏洞可能会让不法分子破解各种人脸识别应用,包括苹果的 Face ID。 在拉斯维加斯举办的 2019 世界黑帽(Black Hat)安全大会上,腾讯公司的研究人员演示了攻破苹果 Face ID 的法宝:一款特制眼镜。这幅眼镜镜片上贴有黑色胶带,黑色胶带中心还贴有白色胶带。

在演示中,研究人员只需要将这款眼镜戴在受害者的脸上即可解锁 Face ID,访问手机。但是,鉴于不法分子需要在不唤醒受害者的情况下把眼镜戴在他/她的脸上,所以这种攻击本身存在难度。 在攻破苹果 Face ID 的过程中,研究人员利用了生物识别技术背后的「活体检测」功能。活体检测是筛选人们「真假」特征的生物识别认证过程中的一部分,其原理是检测背景噪声、响应失真或聚焦模糊。苹果公司在 iPhone 和 iPad Pro 的 Face ID 人脸识别系统中使用的就是这种活体检测功能。

腾讯安全研究人员马卓(Zhuo Ma)认为,虽然以前的攻击主要聚焦于制作假数据来欺骗生物识别技术,但这种类型的音频或视频攻击由各种部分组成,包括窃取受害者设备的指纹信息、生成假音频或视频以及硬件层面的侵入破解等。 而在此次攻击实验中,研究人员决定聚焦于活体检测(允许用户扫一眼即可解锁手机),从而希望在受害者意识不清醒的时候通过面部来欺骗 Face ID。 但是,马卓还表示:「这种操作非常具有挑战性,我们不能吵醒正在熟睡的受害者,同时 3D 系统的伪造也存在诸多困难... 所以我们需要找到一种成本低但成功率高的解决方案。」

利用活体检测破解 FaceID 研究人员专门研究了活体检测如何扫描用户眼睛。他们发现,活体检测对人眼的抽象化处理是在黑色区域(人眼)上嵌上白点(虹膜),即以黑区+白点的形式来模拟眼睛和虹膜。此外,如果用户戴上眼镜,活体检测扫描眼睛的方式也会出现变化。 研究人员发现了苹果 Face ID 的漏洞,即用户戴着眼镜时也能解锁手机。当 Face ID 识别到用户戴着眼镜时,就会自动跳过对眼部区域 3D 信息的提取。

利用活体检测破解 FaceID 所以,结合以上两种因素,研究人员制作了一副眼镜原型——X 眼镜(X-glasses):眼镜镜片上贴有黑色胶带,黑色胶带又内嵌白色胶带,以模仿眼睛的构造。他们将这款贴有双重胶带的眼镜戴在熟睡的受害者脸上,欺骗 Face ID 和其他类似技术的注意力检测机制,从而可以解锁受害者的手机,并通过移动支付应用转走受害者的钱。 研究人员认为,针对苹果 Face ID 的攻击揭露了活体检测和生物识别认证在安全和设计上所存在的漏洞。

石膏「人脸」竟可以破解四种流行旗舰手机的 AI 人脸识别解锁功能,而 iPhone X 不为所动。测试中被「假头」破解的手机包括 LG G7 ThinQ、三星 S9、三星 Note 8 。 从商场到工作场所,人脸识别无处不在,好像我们的脸每天都在被扫描。但智能手机应该保护用户数据,使其免于泄露,而不是侵犯隐私。

用于测试的 3D 打印头部是由英国伯明翰的 Backface 公司制作的。这家公司利用 50 个摄像头同时拍照就能生成一幅完整的 3D 图像,然后将生成的图像导入电脑,用编辑软件进行处理,任何错误都能得到修复。 接下来,Backface 用一台以石膏粉为原料的 3D 打印机打出模型。然后进行面部修复和上色。利用这种方法几天之内就能做出一个真人头部大小的模型,总花费仅 300 英镑。 在这之后,你就得到了一个几乎完美的人头复制体。

安卓手机抵抗攻击的性能也存在差异。如,首次打开这部全新的 G7 时,LG 曾提醒用户不要打开人脸识别。「人脸识别为二级解锁方法,会降低您手机的安全性,」LG 手机播报道,提醒用户类似的人脸也可以解锁你的手机。难怪开始试验的时候,3D 打印头部轻松就解开了 G7。 但在拍摄期间,LG 似乎更新了人脸识别程序,大大增加了破解难度。一位 LG 发言人表示:「通过 LG 推荐的第二个识别步骤和高级识别,可以通过设置在设备上改进人脸识别功能。LG 试图通过不断加强设备稳定性和安全性来改进手机。」他们补充道,人脸识别被视为次于 PIN、指纹等其他方式的「二级解锁功能」。

三星 S9 在用户注册时也有类似提醒。「您的手机可能会被与您长相类似的人或物解锁,」该手机提醒道。「如果仅使用人脸识别,安全性会低于使用手势密码、PIN 及密码。」但奇怪的是,在设置该设备时,首先出现的解锁选项是人脸和虹膜识别。虹膜识别不会被「假头」模糊的眼睛欺骗,但人脸识别被欺骗了,尽管需要先调整角度和照明。

Note 8 具有「快速识别」功能,根据制造商的表述,「快速识别」的安全性比普通识别还要差。在此次实验中,这并不重要,因为无论怎么设置,「假头」都可以解锁手机,只是普通解锁需要花更多时间调整角度和照明。S9 和 LG 的慢速解锁功能也是如此,而且事实证明,后者更难攻破。三星发言人表示:「人脸识别是为了更方便地打开手机,类似于『滑动解锁』。我们提供最高级别的生物特征识别——指纹和虹膜,利用它们解锁手机,并为 Samsung Pay、Secure Folder 等功能提供验证。」

全球销量排名第二的华为手机安全性如何?或许是因为记者们的偏好,在福布斯的测试中并没有出现华为手机。不过更早些时候,德国媒体的同行们使用了另一种方式破解了 Mate 20 Pro 的 3D 结构光人脸识别解锁功能:蓄胡子。 这两位长相相似,都留着胡子的用户,其中一人录入了自己的面部信息,随后在手机息屏状态下,另一人接过手机之后直接解锁成功了。大胡子的存在阻碍了人脸识别的扫描?

不过,iPhone X 似乎不那么容易被破解:苹果公司在人脸识别方面投资很大,他们甚至和好莱坞电影工作室合作,制造仿真面具来测试 Face ID,他们的努力得到了回报,模型是无法解锁 iPhone X 的。 作为最贵的旗舰手机系列,苹果公司自 2017 年起在 iPhone X 中使用了「TrueDepth 摄像机系统」(隐藏于屏幕上方的「齐刘海」部分)。 在识别时,手机会使用其中的传感器、摄像头和点阵投影仪,投射出 3 万多个点,以形成一张完整的 3D「模型」来识别用户脸部。此外,iPhone X 还采用了定制化的 AI 芯片 Neural Engine 来处理工作负载。 对于 Face ID 的自信甚至让苹果抛弃了一直使用的指纹解锁功能。苹果称,同为生物识别技术,TouchID 的解锁错误率是五万分之一,而 FaceID 则是一百万分之一。

什么是活体检测,是指计算机判别检测的人脸是真实的人脸,还是伪造的人脸攻击,比如合法用户图片、提前拍摄的视频等。传统方法将其视为一个“活体”VS“假体”的二分类问题,当然也可看成多分类问题,如真人、图片攻击、视频回放攻击、面具攻击等) 目前主要研究机构:OULU大学(有多篇论文)、MSU(多篇论文Department of Computer Science and Engineering,Michigan State University)、HKBU(和OULU合作)、IDIAP(提供了很多数据集)、国内(旷视、百度、商汤、虹软等都有提供) 目前攻击方法有打印照片、视频回放、面具攻击,反攻击也是针对这几种方式,例如用户配合动作检测、语音校验、颜色纹理、光流、远程心率rPPG,还有一些借助外部设备,如红外摄像头、深度摄像头。 应该针对具体情境制定方案,嵌入式还是移动端,是否需要减少交互增强用户体验(静默活体检测)、是否允许额外设备支持等。

3D摄像头:拍摄人脸,得到相应的人脸区域的3D数据,并基于这些数据做进一步的分析, 最终判断出这个人脸是来自活体还是非活体。这里非活体的来源是比较广泛的,包括手机和Pad等介质的照片和视频、各种打印的不同材质的照片(包含各种情形的弯曲、折叠、剪裁、挖洞等情形)等。关键是,基于活体和非活体的3D人脸数据,如何选择最具有区分度的特征来训练分类器,利用训练好的分类器来区分活体和非活体。 光流法:利用图像序列中的像素强度数据的时域变化和相关性来确定各自像素位置的“运动”,从图像序列中得到各个像素点的运行信息,采用高斯差分滤波器、LBP特征和支持向量机进行数据统计分析。同时,光流场对物体运动比较敏感,利用光流场可以统一检测眼球移动和眨眼。这种活体检测方式可以在用户无配合的情况下实现盲测。

由左侧两张对比图可以看出,活体的光流特征,显示为不规则的向量特征,而照片的光流特征,则是规则有序的向量特征,以此即可区分活体和照片。

静默活体检测:相对于动态活体检测方法,静默活体检测是指,不需要用户做任何动作,自然面对摄像头3、4秒钟即可。由于真实人脸并不是绝对静止的, 存在微表情,如眼皮眼球的律动、眨眼、嘴唇及周边面颊的伸缩等,可通过此类特征反欺骗。

红外活体检测:利用额外设备红外摄像头不管是可见光还是红外光,其本质都是电磁波。我们最终看到的图像长什么样,与材质表面的反射特性有关。真实的人脸和纸片、屏幕、立体面具等攻击媒介的反射特性都是不同的,所以成像也不同,而这种差异在红外波反射方面会更加明显,比如说,一块屏幕在红外成像的画面里,就只有白花花的一片,连人脸都没了,攻击完全不可能得逞。

纹理分析(Texture analysis):计算面部区域的局部二值模式(Local Binary Patterns),使用SVM分类真脸和假脸。 频率分析(Frequency analysis):分析脸部的频谱。

可变聚焦分析(Variable focusing analysis):评估两个连续帧中的像素变化。 启发式算法(Heuristic-based algorithms):包括眼睛动作,嘴唇动作,眨眼检测。这些算法尝试跟踪眼睛的移动以及眨眼,保证用户不是举着别人的照片(因为静止的图片中的人不会眨眼也不会动嘴唇)。

光流算法:审查3D物体和2D平面的光流特性变化。

3D脸部形状:和苹果iPhone脸部识别系统类似,使得系统能区别真脸和打印出来的别人图片。 可变聚焦分析(Variable focusing analysis):评估两个连续帧中的像素变化。


# 命令行参数
# 	python gather_examples.py --input videos/real.mov --output dataset/real --detector face_detector --skip 1
# 	python gather_examples.py --input videos/fake.mp4 --output dataset/fake --detector face_detector --skip 4

import numpy as np
import argparse
import cv2
import os

"""
这个脚本从输入的视频文件中提取了面部 ROI,帮助我们创建了深度学习面部活体数据集
	1.从训练(视频)数据集中检测并提取面部 ROI,根据这些帧,我们后续将在这些图像上训练基于深度学习的活体检测器。
		"./videos/fake.mp4":伪造面部图像的视频
		"./videos/real.mov":真实面部图像
	
	2.dataset/:我们的数据集目录中包含两类图像:
		1."./dataset/fake":在播放我的面部视频时通过录制屏幕得到的伪造图像,即fake.mp4 中的面部 ROI;
		2."./dataset/real":手机直接拍摄我的面部视频得到的真实图像,即real.mov 中的面部 ROI。
	
	3.因为「真」视频比「假」视频长,因此我们得把跳过帧的值设置得更长,来平衡每一类输出的面部 ROI 数量。
	  在执行这个脚本之后,你的图像数量应该如下:
			伪造面部:150 张图片;
			真实面部:161 张图片;
			总数:311 张图片。
	4.构造参数解析并解析参数		
		1.--input:输入视频文件的路径
		2.--output:输出目录的路径,截取的每一张面部图像都存储在这个目录中。
		3.--detector:面部检测器的路径。我们将使用 OpenCV 的深度学习面部检测器。
				"./face_detector" 该路径下包含以下两个文件:
					deploy.prototxt:检测人脸的网络模型文件(Caffe中deploy.prototxt)
					res10_300x300_ssd_iter_140000.caffemodel:预训练的权重参数文件(300x300代表该检测人脸的模型的输入应为300x300)
		4.--confidence:过滤弱面部检测的最小概率,默认值为 50%。
		5.--skip:我们不需要检测和存储每一张图像,因为相邻的帧是相似的。因此我们在检测时会跳过 N 个帧。你可以使用这个参数并更改默认值16。
"""
# 构造参数解析并解析参数
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--input", type=str, required=False, default="./videos/real.mov", help="path to input video")
ap.add_argument("-o", "--output", type=str, required=False, default="./dataset/real",help="path to output directory of cropped faces")
# ap.add_argument("-i", "--input", type=str, required=False, default="./videos/fake.mp4", help="path to input video")
# ap.add_argument("-o", "--output", type=str, required=False, default="./dataset/fake", help="path to output directory of cropped faces")
ap.add_argument("-d", "--detector", type=str, required=False, default="./face_detector", help="path to OpenCV's deep learning face detector")
ap.add_argument("-c", "--confidence", type=float, default=0.5, help="minimum probability to filter weak detections")
ap.add_argument("-s", "--skip", type=int, default=16, help="# of frames to skip before applying face detection")
args = vars(ap.parse_args())

# 从磁盘加载序列化的面部检测器
print("[INFO] loading face detector...")
#deploy.prototxt:检测人脸的网络模型文件(Caffe中deploy.prototxt)
protoPath = os.path.sep.join([args["detector"], "deploy.prototxt"])
#res10_300x300_ssd_iter_140000.caffemodel:预训练的权重参数文件(300x300代表该检测人脸的模型的输入应为300x300)
modelPath = os.path.sep.join([args["detector"], "res10_300x300_ssd_iter_140000.caffemodel"])
""" 加载了 OpenCV 的深度学习面部检测器 """
# 所加载的预训练模型和权重参数文件用于人脸检测
net = cv2.dnn.readNetFromCaffe(protoPath, modelPath)

# 打开一个指向视频文件流的指针,并初始化到目前为止已读取和保存的帧总数
vs = cv2.VideoCapture(args["input"])
read = 0 #读取的帧的数量
saved = 0 #执行循环时保存的帧的数量

# 循环播放视频文件流中的帧
while True:
	# 从文件中抓取帧
	(grabbed, frame) = vs.read()
	# 如果没有抓取到帧,那么我们已经到达视频流的尽头
	if not grabbed:
		break
	# 增加到目前为止读取的总帧数
	read += 1
	# 检查我们是否应该处理此帧
	# 我们不需要检测和存储每一张图像,因为相邻的帧是相似的。因此我们在检测时会跳过 N 个帧。你可以使用这个参数并更改默认值16。
	if read % args["skip"] != 0:
		continue

	# 抓取框架尺寸
	(h, w) = frame.shape[:2]
	# blobFromImage主要是用来对图片进行预处理。包含两个主要过程:
	#	1,整体像素值减去平均值(mean)
	#	2,通过缩放系数(scalefactor)对图片像素值进行缩放
	# 预处理由OpenCV的blobFromImage函数:
	# 	1.将输入图片大小调整为300×300像素,并执行均值减去。
	#	  因为预训练的权重参数文件使用的为:res10_300x300_ssd_iter_140000.caffemodel(其中300x300代表该检测人脸的模型的输入应为300x300)
	# 	2.图片RGB三个通道分别减去(B=104.0, G=177.0, R=123.0),即每个通道减去指定的均值
	# 	3.缩放系数1.0 可以对图片像素值进行缩放,此处使用1.0代表了并没有做真正的缩放,像素值除以1.0结果值仍不变
	#为了进行面部检测,根据图像创建一个 blob。为了适应 Caffe 面部识别器,这个 blob 是 300*300 的,之后还要缩放边界框。
	blob = cv2.dnn.blobFromImage(cv2.resize(frame, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0))
	# 通过网络传递blob并获得检测和预测
	net.setInput(blob)
	# 执行面部检测以定位图像中所有面部的位置。通过深度学习面部识别器执行了 blob 的前向传输。
	detections = net.forward()

	"""
	这里判断代码假设了视频的每一帧中只有一张面部。这有助于减少假阳性。
	如果你要处理的视频中不止有一张面部,建议根据需要调整逻辑。
	"""
	# 确保至少发现一张脸
	if len(detections) > 0:
		"""
		idx = int(detections[0, 0, 0, 1])  #比如 1.0,提取第1个可疑人脸的目标标签
		confidence = detections[0, 0, 0, 2] #比如 0.9984427,提取第1个可疑人脸的置信度
		box = detections[0, 0, 0, 3:7] #比如 [0.5462329  0.12488028 0.6709176  0.3542412 ] 提取第1个可疑人脸的4个位置信息值
		"""
		# 表示可以一次性检测出200个可能为人脸目标,[0, 0, :, 2]取出的为该200个可能为人脸目标的置信度
		# print(detections[0, 0, :, 2].size) #200
		# 我们假设每个图像只有一张脸,所以找到概率最大的边界框,抓取了概率最高的面部检测索引。
		# 从包含200个置信度值的列表中通过argmax取出最大置信度值对应的下标索引值
		i = np.argmax(detections[0, 0, :, 2])
		# 通过最大置信度值对应的下标索引值 取出对应的 置信度。
		confidence = detections[0, 0, i, 2]

		#确保检测出来的人脸具有最大置信度,确保我们的面部检测 ROI 满足最小阈值,从而减少假阳性。
		if confidence > args["confidence"]:
			# 提取最大置信度的人脸的4个位置信息值,计算面部边界框的(x,y)坐标
			box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
			(startX, startY, endX, endY) = box.astype("int")
			# 提取仅面部位置的ROI(感兴趣区域)
			face = frame[startY:endY, startX:endX]
			# 面部位置图片 的输出路径
			p = os.path.sep.join([args["output"], "{}.png".format(saved)])
			# 将检测出来的人脸 写入磁盘
			cv2.imwrite(p, face)
			saved += 1
			print("[INFO] saved {} to disk".format(p))

# 做一些清理
vs.release()
cv2.destroyAllWindows()

"""
dnn.blobFromImage
	作用:根据输入图像,创建维度N(图片的个数),通道数C,高H和宽W次序的blobs
	原型:blobFromImage(image, scalefactor=None, size=None, mean=None, swapRB=None, crop=None, ddepth=None)
	参数:
		image:cv2.imread 读取的图片数据
		scalefactor: 缩放像素值,如 [0, 255] - [0, 1]
		size: 输出blob(图像)的尺寸,如 (netInWidth, netInHeight)
		mean: 从各通道减均值. 如果输入 image 为 BGR 次序,且swapRB=True,则通道次序为 (mean-R, mean-G, mean-B).
		swapRB: 交换 3 通道图片的第一个和最后一个通道,如 BGR - RGB
		crop: 图像尺寸 resize 后是否裁剪. 如果crop=True,则,输入图片的尺寸调整resize后,一个边对应与 size 的一个维度,
			   而另一个边的值大于等于 size 的另一个维度;然后从 resize 后的图片中心进行 crop. 
			   如果crop=False,则无需 crop,只需保持图片的长宽比
		ddepth: 输出 blob 的 Depth. 可选: CV_32F 或 CV_8U

blob = cv2.dnn.blobFromImage(image, scalefactor=1.0, size, mean, swapRB=True)
	在进行深度学习或者图片分类时,blobFromImage主要是用来对图片进行预处理。包含两个主要过程:
		1,整体像素值减去平均值(mean)
		2,通过缩放系数(scalefactor)对图片像素值进行缩放
	参数:	
		image:这个就是我们将要输入神经网络进行处理或者分类的图片。
		scalefactor:当我们将图片减去平均值之后,还可以对剩下的像素值进行一定的尺度缩放,它的默认值是1,如果希望减去平均像素之后的值,
					  全部缩小一半,那么可以将scalefactor设为1/2。
		size:这个参数是我们神经网络在训练的时候要求输入的图片尺寸。
		mean:需要将图片整体减去的平均值,如果我们需要对RGB图片的三个通道分别减去不同的值,那么可以使用3组平均值,
			   如果只使用一组例如(B=106.13, G=115.97, R=124.96),那么就默认对三个通道减去一样的值。
			   减去平均值(mean):为了消除同一场景下不同光照的图片,对我们最终的分类或者神经网络的影响,
			   我们常常对图片的R、G、B通道的像素求一个平均值,然后将每个像素值减去我们的平均值,这样就可以得到像素之间的相对值,
			   就可以排除光照的影响。
		swapRB:OpenCV中认为我们的图片通道顺序是BGR,但是我平均值假设的顺序是RGB,所以如果需要交换R和G,那么就要使swapRB=true
"""
from keras.models import Sequential
from keras.layers.normalization import BatchNormalization
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.layers.core import Activation
from keras.layers.core import Flatten
from keras.layers.core import Dropout
from keras.layers.core import Dense
from keras import backend as K

class LivenessNet:
	"""
	静态方法build接受 4 个参数:
		width:图片/体积的宽度;
		height:图片的高度;
		depth:图像的通道数量(处理的是 RGB 图像,通道数量为3);
		classes:类的数量。总共有两类:「真」和「假」。
	"""
	@staticmethod
	def build(width, height, depth, classes):
		#初始化模型和输入形状,以及通道尺寸 height和width,通道顺序默认为"channels last"
		model = Sequential()
		# height和width为通道尺寸,位于最后的depth为通道数,相当于"channels last"
		inputShape = (height, width, depth)
		chanDim = -1 #代表 "channels last"

		#如果使用通道顺序为"channels first",请更新输入形状和通道尺寸
		if K.image_data_format() == "channels_first":
			#位于首位置的depth为通道数,相当于"channels first",height和width为通道尺寸
			inputShape = (depth, height, width)
			chanDim = 1 #代表 "channels_first"

		# CONV => RELU => BN => CONV => RELU => BN => POOL => dropout
		model.add(Conv2D(16, (3, 3), padding="same", input_shape=inputShape))
		model.add(Activation("relu"))
		#BatchNormalization 可以不设置 axis
		model.add(BatchNormalization(axis=chanDim))
		model.add(Conv2D(16, (3, 3), padding="same"))
		model.add(Activation("relu"))
		model.add(BatchNormalization(axis=chanDim))
		model.add(MaxPooling2D(pool_size=(2, 2)))
		model.add(Dropout(0.25))

		# CONV => RELU => BN => CONV => RELU => BN => POOL => dropout
		model.add(Conv2D(32, (3, 3), padding="same"))
		model.add(Activation("relu"))
		# BatchNormalization 可以不设置 axis
		model.add(BatchNormalization(axis=chanDim))
		model.add(Conv2D(32, (3, 3), padding="same"))
		model.add(Activation("relu"))
		model.add(BatchNormalization(axis=chanDim))
		model.add(MaxPooling2D(pool_size=(2, 2)))
		model.add(Dropout(0.25))

		# Flatten => FC(Dense) => RELU => BN => dropout
		model.add(Flatten())
		model.add(Dense(64))
		model.add(Activation("relu"))
		# BatchNormalization 可以不设置 axis
		model.add(BatchNormalization())
		model.add(Dropout(0.5))

		# softmax分类器:FC(Dense(标签类型数量)) => softmax
		model.add(Dense(classes))
		model.add(Activation("softmax"))

		# 返回构建的网络架构
		return model
# 命令行参数:python train_liveness.py --dataset dataset --model liveness.model --le le.pickle

# 设置matplotlib后端,以便可以将图形保存在后台
import matplotlib
#在PyCharm中不显示绘图:use("Agg")
matplotlib.use("Agg")

from 项目二.day05.liveness_detection_opencv.pyimage.livenessnet import LivenessNet
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from keras.preprocessing.image import ImageDataGenerator
from keras.optimizers import Adam
from keras.utils import np_utils
from imutils import paths
import matplotlib.pyplot as plt
import numpy as np
import argparse
import pickle
import cv2
import os

# 构造参数解析并解析参数
ap = argparse.ArgumentParser()
#输入数据集的路径
ap.add_argument("-d", "--dataset", required=False, default="./dataset", help="path to input dataset")
#输出活体检测模型的文件
ap.add_argument("-m", "--model", type=str, required=False, default="./liveness.model", help="path to trained model")
#输出序列化标签编码器的文件
ap.add_argument("-l", "--le", type=str, required=False, default="./le.pickle", help="path to label encoder")
ap.add_argument("-p", "--plot", type=str, default="plot.png", help="path to output loss/accuracy plot")
args = vars(ap.parse_args())

INIT_LR = 1e-4 # 初始化初始学习率
BS = 8 #批处理大小
EPOCHS = 1

#在我们的数据集目录中获取图像列表,然后初始化数据列表(即图像)和类图像
print("[INFO] loading images...")
#./dataset/fake 和 ./dataset/real 两个目录下所有的图片数据
imagePaths = list(paths.list_images(args["dataset"]))
# print("imagePaths",imagePaths)
data = [] #存放数据
labels = [] #存放类别标签

# 遍历 ./dataset/fake 和 ./dataset/real 两个目录下所有的图片数据
for imagePath in imagePaths:
	# 从文件名中提取类标签
	label = imagePath.split(os.path.sep)[-2]
	# 加载图像
	image = cv2.imread(imagePath)
	# 并将其大小调整为固定的32x32像素,而忽略宽高比
	image = cv2.resize(image, (32, 32))
	# 分别更新数据和标签列表,循环用于建立数据和标签列表。
	data.append(image) #数据是由加载并将尺寸调整为 32*32 像素的图像组成的
	labels.append(label) #标签列表中存储了每张图相对应的标签。

# 将数据转换为NumPy数组,然后通过将所有像素强度缩放到[0,1]范围进行预处理。
# 将所有像素缩放到 [0,1] 之间,并将列表转换为 NumPy 数组。
data = np.array(data, dtype="float") / 255.0
#Label编码器类:将标签(当前为字符串)编码为整数,然后一键编码
le = LabelEncoder()
# print("labels",labels) #labels ['fake',。。。,'fake', 'real',。。。,'real']
#fit_transform拟合变换:把字符串的标签 转换为 整数值的标签
labels = le.fit_transform(labels)
# print("labels",labels) #[0 0 。。。 0 0 1 1 。。。1 1]
# print("classes=len(le.classes_)",len(le.classes_)) #2
# print("target_names=le.classes_)",le.classes_) #['fake' 'real']
#把每个整数值的标签 进行 one-hot编码化。将类向量(整数)转换为二进制类矩阵。num_classes=2 表示为2种类型的标签
labels = np_utils.to_categorical(labels, 2)
# print("labels",labels) #[[1. 0.] [1. 0.] 。。。[1. 0.] [1. 0.]   [0. 1.] [0. 1.] 。。。 [0. 1.] [0. 1.]]

# 使用75%的数据进行训练,其余25%进行测试,将数据划分为训练和测试分组
(trainX, testX, trainY, testY) = train_test_split(data, labels, test_size=0.25, random_state=42)


"""
stratify=labels 的作用:
	保持测试集与整个数据集里的labels中标签分类的比例一致。
	例子:
		整个数据集有1000条样本,也有1000个labels,并且labels分两类(即0和1),其中labels为0有300条样本,labels为1有700条样本,
		即labels分两类的比例为3:7。那么现在把整个数据集进行split切分,因为test_size = 0.2,所以训练集分到800条样本,测试集分到200条样本。
		因为stratify=labels,则训练集和测试集中的labels分两类的比例均为3:7,结果就是在训练集中labels有240个0和560个1,
		测试集中labels有60个0和140个1。
	同理,若将训练集进一步分出一个验证集:
		(trainX, valX, trainY, valY) = train_test_split(trainX, trainY, test_size=0.20, stratify=trainY, random_state=42)
		则训练集和验证集中的样本数分别为640和160,且由于stratify=trainY,验证集与训练集中的标签分类0和1的比例均为3:7,
		则验证集中将被分到48个0和112个1。
"""

# 构建用于数据增强的训练图像生成器
aug = ImageDataGenerator(rotation_range=20, zoom_range=0.15, width_shift_range=0.2, height_shift_range=0.2,
						 shear_range=0.15, horizontal_flip=True, fill_mode="nearest")
"""
ImageDataGenerator()
	keras.preprocessing.image模块中的图片生成器,同时也可以在batch中对数据进行增强,扩充数据集大小,增强模型的泛化能力。
	比如进行旋转,变形,归一化等等。
		rotation_range(): 旋转范围
		width_shift_range(): 水平平移范围
		height_shift_range(): 垂直平移范围
		zoom_range(): 缩放范围
		fill_mode: 填充模式, constant, nearest, reflect
		horizontal_flip(): 水平反转
		vertical_flip(): 垂直翻转
"""

# 初始化优化器和模型
print("[INFO] compiling model...")
# decay衰变率:初始化的学习率 / EPOCHS
opt = Adam(lr=INIT_LR, decay=INIT_LR / EPOCHS)
#len(le.classes_)=2 表示有2种类型的数据/标签
model = LivenessNet.build(width=32, height=32, depth=3, classes=len(le.classes_))
model.compile(loss="binary_crossentropy", optimizer=opt, metrics=["accuracy"])

# 训练网络
print("[INFO] training network for {} epochs...".format(EPOCHS))
H = model.fit_generator(aug.flow(trainX, trainY, batch_size=BS), validation_data=(testX, testY),
						steps_per_epoch=len(trainX) // BS, epochs=EPOCHS)

# 评估网络
print("[INFO] evaluating network...")
predictions = model.predict(testX, batch_size=BS)
# print("predictions",predictions) # 比如 [[0.52001303 0.479987  ] 。。。[0.5353358  0.46466425]]
#classification_report:建立文字报告,显示主要的分类指标
#target_names=le.classes_:['fake' 'real']
print(classification_report(testY.argmax(axis=1), predictions.argmax(axis=1), target_names=le.classes_))
print("testY.argmax(axis=1)",testY.argmax(axis=1))
print("predictions.argmax(axis=1)",predictions.argmax(axis=1))
"""
print(classification_report(testY.argmax(axis=1), predictions.argmax(axis=1), target_names=le.classes_))
警告:
	UndefinedMetricWarning: Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples. 
	'precision', 'predicted', average, warn_for)
  	UndefinedMetricWarning:精度和F分数定义不明确,在没有预测样本的标签中设置为0.0。
分析:
	该为警告,不是错误。
	比如真实标签的类型是0和1,但是预测的标签仅有其中一种(或仅有0或仅有1),那么就会报出该警告。
"""
# 将活体检测的模型网络保存到磁盘文件中
print("[INFO] serializing network to '{}'...".format(args["model"]))
model.save(args["model"])

# 将标签编码器保存到磁盘文件中
f = open(args["le"], "wb")
f.write(pickle.dumps(le))
f.close()

# 绘制训练损失和准确性
plt.style.use("ggplot")
plt.figure()
plt.plot(np.arange(0, EPOCHS), H.history["loss"], label="train_loss")
plt.plot(np.arange(0, EPOCHS), H.history["val_loss"], label="val_loss")
plt.plot(np.arange(0, EPOCHS), H.history["accuracy"], label="train_accuracy")
plt.plot(np.arange(0, EPOCHS), H.history["val_accuracy"], label="val_accuracy")
plt.title("Training Loss and Accuracy on Dataset")
plt.xlabel("Epoch #")
plt.ylabel("Loss/Accuracy")
plt.legend(loc="lower left")
plt.savefig(args["plot"])
# 命令行参数:python train_liveness.py --dataset dataset --model liveness.model --le le.pickle

# 设置matplotlib后端,以便可以将图形保存在后台
import matplotlib
matplotlib.use("Agg")

from 项目二.day05.liveness_detection_opencv.pyimage.livenessnet import LivenessNet
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from keras.preprocessing.image import ImageDataGenerator
from keras.optimizers import Adam
from keras.utils import np_utils
from imutils import paths
import matplotlib.pyplot as plt
import numpy as np
import argparse
import pickle
import cv2
import os

# 构造参数解析并解析参数
ap = argparse.ArgumentParser()
#输入数据集的路径
# ap.add_argument("-d", "--dataset", required=False, default="./NewData/Detectedface", help="path to input dataset")
ap.add_argument("-d", "--dataset", required=False, default="./NewData/raw", help="path to input dataset")
#输出活体检测模型的文件
ap.add_argument("-m", "--model", type=str, required=False, default="./liveness.model", help="path to trained model")
#输出序列化标签编码器的文件
ap.add_argument("-l", "--le", type=str, required=False, default="./le.pickle", help="path to label encoder")
ap.add_argument("-p", "--plot", type=str, default="plot.png", help="path to output loss/accuracy plot")
args = vars(ap.parse_args())

INIT_LR = 1e-4 # 初始化初始学习率
BS = 8 #批处理大小
EPOCHS = 20

#在我们的数据集目录中获取图像列表,然后初始化数据列表(即图像)和类图像
print("[INFO] loading images...")
#./dataset/fake 和 ./dataset/real 两个目录下所有的图片数据
imagePaths = list(paths.list_images(args["dataset"]))
# print("imagePaths",imagePaths)
data = [] #存放数据
labels = [] #存放类别标签

# 遍历 ./dataset/fake 和 ./dataset/real 两个目录下所有的图片数据
for imagePath in imagePaths:
	# 从文件名中提取类标签
	label = imagePath.split(os.path.sep)[-3]
	# 加载图像
	image = cv2.imread(imagePath)
	# 并将其大小调整为固定的32x32像素,而忽略宽高比
	image = cv2.resize(image, (32, 32))
	# 分别更新数据和标签列表,循环用于建立数据和标签列表。
	data.append(image) #数据是由加载并将尺寸调整为 32*32 像素的图像组成的
	labels.append(label) #标签列表中存储了每张图相对应的标签。

# 将数据转换为NumPy数组,然后通过将所有像素强度缩放到[0,1]范围进行预处理。
# 将所有像素缩放到 [0,1] 之间,并将列表转换为 NumPy 数组。
data = np.array(data, dtype="float") / 255.0
#Label编码器类:将标签(当前为字符串)编码为整数,然后一键编码
le = LabelEncoder()
# print("labels",labels) #labels ['fake',。。。,'fake', 'real',。。。,'real']
#fit_transform拟合变换:把字符串的标签 转换为 整数值的标签
labels = le.fit_transform(labels)
# print("labels",labels) #[0 0 。。。 0 0 1 1 。。。1 1]
print("classes=len(le.classes_)",len(le.classes_)) #2
print("target_names=le.classes_)",le.classes_) #['fake' 'real']
#把每个整数值的标签 进行 one-hot编码化。将类向量(整数)转换为二进制类矩阵。num_classes=2 表示为2种类型的标签
labels = np_utils.to_categorical(labels, 2)
print("labels",labels) #[[1. 0.] [1. 0.] 。。。[1. 0.] [1. 0.]   [0. 1.] [0. 1.] 。。。 [0. 1.] [0. 1.]]

# 使用75%的数据进行训练,其余25%进行测试,将数据划分为训练和测试分组
(trainX, testX, trainY, testY) = train_test_split(data, labels, test_size=0.25, random_state=42)
"""
stratify=labels 的作用:
	保持测试集与整个数据集里的labels中标签分类的比例一致。
	例子:
		整个数据集有1000条样本,也有1000个labels,并且labels分两类(即0和1),其中labels为0有300条样本,labels为1有700条样本,
		即labels分两类的比例为3:7。那么现在把整个数据集进行split切分,因为test_size = 0.2,所以训练集分到800条样本,测试集分到200条样本。
		因为stratify=labels,则训练集和测试集中的labels分两类的比例均为3:7,结果就是在训练集中labels有240个0和560个1,
		测试集中labels有60个0和140个1。
	同理,若将训练集进一步分出一个验证集:
		(trainX, valX, trainY, valY) = train_test_split(trainX, trainY, test_size=0.20, stratify=trainY, random_state=42)
		则训练集和验证集中的样本数分别为640和160,且由于stratify=trainY,验证集与训练集中的标签分类0和1的比例均为3:7,
		则验证集中将被分到48个0和112个1。
"""

# 构建用于数据增强的训练图像生成器
aug = ImageDataGenerator(rotation_range=20, zoom_range=0.15, width_shift_range=0.2, height_shift_range=0.2,
						 shear_range=0.15, horizontal_flip=True, fill_mode="nearest")
"""
ImageDataGenerator()
	keras.preprocessing.image模块中的图片生成器,同时也可以在batch中对数据进行增强,扩充数据集大小,增强模型的泛化能力。
	比如进行旋转,变形,归一化等等。
		rotation_range(): 旋转范围
		width_shift_range(): 水平平移范围
		height_shift_range(): 垂直平移范围
		zoom_range(): 缩放范围
		fill_mode: 填充模式, constant, nearest, reflect
		horizontal_flip(): 水平反转
		vertical_flip(): 垂直翻转
"""

# 初始化优化器和模型
print("[INFO] compiling model...")
# decay衰变率:初始化的学习率 / EPOCHS
opt = Adam(lr=INIT_LR, decay=INIT_LR / EPOCHS)
#len(le.classes_)=2 表示有2种类型的数据/标签
model = LivenessNet.build(width=32, height=32, depth=3, classes=len(le.classes_))
model.compile(loss="binary_crossentropy", optimizer=opt, metrics=["accuracy"])

# 训练网络
print("[INFO] training network for {} epochs...".format(EPOCHS))
H = model.fit_generator(aug.flow(trainX, trainY, batch_size=BS), validation_data=(testX, testY),
						steps_per_epoch=len(trainX) // BS, epochs=EPOCHS)

# 评估网络
print("[INFO] evaluating network...")
predictions = model.predict(testX, batch_size=BS)
# print("predictions",predictions) # 比如 [[0.52001303 0.479987  ] 。。。[0.5353358  0.46466425]]
#classification_report:建立文字报告,显示主要的分类指标
#target_names=le.classes_:['fake' 'real']
print(classification_report(testY.argmax(axis=1), predictions.argmax(axis=1), target_names=le.classes_))

# 将活体检测的模型网络保存到磁盘文件中
print("[INFO] serializing network to '{}'...".format(args["model"]))
model.save(args["model"])

# 将标签编码器保存到磁盘文件中
f = open(args["le"], "wb")
f.write(pickle.dumps(le))
f.close()

# 绘制训练损失和准确性
plt.style.use("ggplot")
plt.figure()
plt.plot(np.arange(0, EPOCHS), H.history["loss"], label="train_loss")
plt.plot(np.arange(0, EPOCHS), H.history["val_loss"], label="val_loss")
plt.plot(np.arange(0, EPOCHS), H.history["accuracy"], label="train_accuracy")
plt.plot(np.arange(0, EPOCHS), H.history["val_accuracy"], label="val_accuracy")
plt.title("Training Loss and Accuracy on Dataset")
plt.xlabel("Epoch #")
plt.ylabel("Loss/Accuracy")
plt.legend(loc="lower left")
plt.savefig(args["plot"])
# 命令行参数:python liveness_demo.py --model liveness.model --le le.pickle --detector face_detector

from imutils.video import VideoStream
from keras.preprocessing.image import img_to_array
from keras.models import load_model
import numpy as np
import argparse
import imutils
import pickle
import time
import cv2
import os

# 构造参数解析并解析参数
ap = argparse.ArgumentParser()
#用于活体检测的预训练 Keras 模型的文件
ap.add_argument("-m", "--model", type=str, required=False, default="./liveness.model", help="path to trained model")
#序列化的标签编码器的文件
ap.add_argument("-l", "--le", type=str, required=False, default="./le.pickle", help="path to label encoder")
#用来寻找面部 ROI 的 OpenCV 的深度学习面部检测器
"""
面部检测器的路径。我们将使用 OpenCV 的深度学习面部检测器。
"./face_detector" 该路径下包含以下两个文件:
	deploy.prototxt:检测人脸的网络模型文件(Caffe中deploy.prototxt)
	res10_300x300_ssd_iter_140000.caffemodel:预训练的权重参数文件(300x300代表该检测人脸的模型的输入应为300x300)
"""
ap.add_argument("-d", "--detector", type=str, required=False, default="./face_detector", help="path to OpenCV's deep learning face detector")
#过滤弱面部检测的最小概率,默认值为 50%。
ap.add_argument("-c", "--confidence", type=float, default=0.5, help="minimum probability to filter weak detections")
args = vars(ap.parse_args())

# 从磁盘加载序列化的面部检测器
print("[INFO] loading face detector...")
#deploy.prototxt:检测人脸的网络模型文件(Caffe中deploy.prototxt)
protoPath = os.path.sep.join([args["detector"], "deploy.prototxt"])
#res10_300x300_ssd_iter_140000.caffemodel:预训练的权重参数文件(300x300代表该检测人脸的模型的输入应为300x300)
modelPath = os.path.sep.join([args["detector"], "res10_300x300_ssd_iter_140000.caffemodel"])
# 所加载的预训练模型和权重参数文件用于人脸检测
net = cv2.dnn.readNetFromCaffe(protoPath, modelPath)

# 从磁盘加载活体检测器模型和标签编码器
print("[INFO] loading liveness detector...")
model = load_model(args["model"]) #用于活体检测的预训练 Keras 模型的文件 liveness.model
le = pickle.loads(open(args["le"], "rb").read()) #序列化的标签编码器的文件 le.pickle

# 初始化视频流并允许相机传感器预热
print("[INFO] starting video stream...")
vs = VideoStream(src=0).start()
time.sleep(2.0)

# 循环播放视频流中的帧
while True:
	# 视频流中抓取帧
	frame = vs.read()
	# 将其调整为最大宽度为600像素
	frame = imutils.resize(frame, width=600)

	# 抓取帧尺寸并将其转换为blob
	(h, w) = frame.shape[:2]
	# blobFromImage主要是用来对图片进行预处理。包含两个主要过程:
	#	1,整体像素值减去平均值(mean)
	#	2,通过缩放系数(scalefactor)对图片像素值进行缩放
	# 预处理由OpenCV的blobFromImage函数:
	# 	1.将输入图片大小调整为300×300像素,并执行均值减去。
	#	  因为预训练的权重参数文件使用的为:res10_300x300_ssd_iter_140000.caffemodel(其中300x300代表该检测人脸的模型的输入应为300x300)
	# 	2.图片RGB三个通道分别减去(B=104.0, G=177.0, R=123.0),即每个通道减去指定的均值
	# 	3.缩放系数1.0 可以对图片像素值进行缩放,此处使用1.0代表了并没有做真正的缩放,像素值除以1.0结果值仍不变
	#为了进行面部检测,根据图像创建一个 blob。为了适应 Caffe 面部识别器,这个 blob 是 300*300 的,之后还要缩放边界框。
	blob = cv2.dnn.blobFromImage(cv2.resize(frame, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0))
	# 通过网络传递blob并获得检测和预测
	net.setInput(blob)
	# 执行面部检测以定位图像中所有面部的位置。通过深度学习面部识别器执行了 blob 的前向传输。
	detections = net.forward()
	"""
	for i in range(0, detections.shape[2]) 循环遍历每个检测出来的可能目标的人脸
		detections.shape:(1, 1, 200, 7)
			detections.shape[2]:表示有200个可能为人脸的目标
			detections.shape[3]:每个人脸目标对应的7个值,第2个值为目标标签,第3个值为目标是否为人脸的置信度,
								   第4到第7个值一共4个值为人脸坐标位置信息
		idx = int(detections[0, 0, 0, 1])  #1.0 提取第1个可疑人脸的目标标签
		confidence = detections[0, 0, 0, 2] #0.9984427 提取第1个可疑人脸的置信度
		box = detections[0, 0, 0, 3:7] #[0.5462329  0.12488028 0.6709176  0.3542412 ] 提取第1个可疑人脸的4个位置信息值
	"""
	# 循环检测
	for i in range(0, detections.shape[2]):
		# 提取与预测相关的置信度(即概率)
		confidence = detections[0, 0, i, 2]
		#确保检测出来的人脸具有最大置信度,确保我们的面部检测 ROI 满足最小阈值,从而减少假阳性。
		if confidence > args["confidence"]:
			# 计算面部边界框的(x,y)坐标并提取面部ROI
			box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
			(startX, startY, endX, endY) = box.astype("int")
			#提取对应的面部边界框,确保它们没有超出帧;
			# 确保检测到的边界框不在框架的尺寸范围内
			startX = max(0, startX)
			startY = max(0, startY)
			endX = min(w, endX)
			endY = min(h, endY)
			# 提取面部ROI,然后以与训练数据完全相同的方式进行预处理
			face = frame[startY:endY, startX:endX]
			# 因为活体检测模型的input_shape在训练数据时设置的输入形状即为(32, 32),因此此处预测时也需要设置输入形状为(32, 32)
			face = cv2.resize(face, (32, 32))
			face = face.astype("float") / 255.0
			face = img_to_array(face)
			face = np.expand_dims(face, axis=0)
			# print("classes=len(le.classes_)",len(le.classes_)) #2
			# print("target_names=le.classes_)",le.classes_) # [b'fake' b'real']

			# 通过训练有素的活动检测器模型传递面部ROI
			# 活体检测模型预测的标签为“真实real”还是“伪造fake”,或者“ClientRaw原始”还是“ImposterRaw冒名顶替”
			# e-01 表示10的负1次方。
			preds = model.predict(face)[0]
			# print("preds",preds) #model.predict(face) 返回的为 二维类型的数据,比如 [[9.9996865e-01  3.1292140e-05]]
			# print("preds", preds)#model.predict(face) 取出第一维数据,比如 [9.999951e-01  4.936377e-06]
			# print("preds", preds) #比如 [1.0848045e-05 9.9998915e-01]
			# 取出预测最大概率值的标签的索引值
			j = np.argmax(preds) # print("j",j) # 比如 1
			# 根据 最大概率值的标签的索引值 取出 最大概率值的标签
			label = le.classes_[j] # print("label", label) #比如 b'real'

			# 在帧上绘制标签和边框
			label = "{}: {:.4f}".format(label, preds[j])
			cv2.putText(frame, label, (startX, startY - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
			cv2.rectangle(frame, (startX, startY), (endX, endY), (0, 0, 255), 2)

	# 显示输出帧并等待按键
	cv2.imshow("Frame", frame)
	key = cv2.waitKey(1) & 0xFF
	# 如果按下“ q”键,则退出循环
	if key == ord("q"):
		break

# 做一些清理
cv2.destroyAllWindows()
vs.stop()

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

あずにゃん

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值