整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Python网络爬虫-爬取QQ空间相册

析QQ空间

登录QQ空间

爬取第一步,分析站点,首先需要知道如何登录QQ空间。最初想法是用requests库配置登录请求,模拟登录,但是不久便放弃了这一思路,请看下图↓

login

根据登录按钮绑定的监听事件可以追踪到该按钮的点击事件如下:

login function

账号加密是必然的,但这一堆堆的代码真心不好解析,有耐心的勇士尽情一试!

在排除这种登录方法后,选择selenium模拟用户登录不失为省时省力的方法,而且我们只是需要通过selenium完成登录,获取到Cookies和后面讲述的g_tk参数后,就可以停用了,所以效率并不太低。

分析空间相册

登录以后,页面会跳转至 [https://user.qzone.qq.com/{QQ_NUMBER}](javascript:;), 这时把鼠标移到导航栏你会发现,所有的导航栏链接都是javascript:; 。没错就是这么坑,一切都是暗箱操作。

当然这并不难处理,使用调试工具捕获点击后产生的请求,然后过滤出正确的请求包即可。因为网络包非常多,那么怎么过滤呢,猜想相册数据的API必然会返回个列表list,尝试过滤list然后逐个排除,最后定位到请求包。下面是通过fcg_list过滤后的数据包,列表信息以jsonp格式返回,稍作处理即可当做json格式来读取(后面有讲)。

album list

从Headers和Response可以分别获取到两组重要信息:

  1. request 获取相册列表所需的请求信息,包括请求链接和参数
  2. response 数据包包含的所有相册的信息,是每个相册所含照片对应的请求包参数的数据来源

先看请求包:

# url
https://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/fcg_list_album_v3

# args
g_tk: 477819917
callback: shine0_Callback
t: 691481346
hostUin: 123456789
uin: 123456789
appid: 4
inCharset: utf-8
outCharset: utf-8
source: qzone
plat: qzone
format: jsonp
notice: 0
filter: 1
handset: 4
pageNumModeSort: 40
pageNumModeClass: 15
needUserInfo: 1
idcNum: 4
callbackFun: shine0
_: 1551788226819

其中hostUin, uin都是QQ号,g_tk是必须的且每次重新登录都会更新(后面有讲如何获取),其它有些参数不是必须的,我尝试后整理出如下请求参数:

query = {
 'g_tk': self.g_tk,
 'hostUin': self.username,
 'uin': self.username,
 'appid': 4,
 'inCharset': 'utf-8',
 'outCharset': 'utf-8',
 'source': 'qzone',
 'plat': 'qzone',
 'format': 'jsonp'
}

接下来看jsonp格式的跨域响应包:

shine0_Callback({
 "code":0,
 "subcode":0,
 "message":"",
 "default":0,
 "data":
{
 "albumListModeSort" : [
 {
 "allowAccess" : 1,
 "anonymity" : 0,
 "bitmap" : "10000000",
 "classid" : 106,
 "comment" : 11,
 "createtime" : 1402661881,
 "desc" : "",
 "handset" : 0,
 "id" : "V13LmPKk0JLNRY",
 "lastuploadtime" : 1402662103,
 "modifytime" : 1408271987,
 "name" : "毕业季",
 "order" : 0,
 "pre" : "http:\/\/b171.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfGuwSk58K2rQY!\/a\/dIY29GUbJgAA",
 "priv" : 1,
 "pypriv" : 1,
 "total" : 4,
 "viewtype" : 0
 },

shine0_Callback是请求包的callbackFun参数决定的,如果没这个参数,响应包会以_Callback作为默认名,当然这都不重要。所有相册信息以json格式存入albumListModeSort中,上面仅截取了一个相册的信息。

相册信息中,name代表相册名称,id作为唯一标识可用于请求该相册内的照片信息,而pre仅仅是一个预览缩略图的链接,无关紧要。

分析单个相册

与获取相册信息类似,进入某一相册,使用cgi_list过滤数据包,找到该相册的照片信息

photo list

同样的道理,根据数据包可以获取照片列表信息的请求包和响应信息,先看请求:

# url
https://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/cgi_list_photo

# args
g_tk: 477819917
callback: shine0_Callback
t: 952444063
mode: 0
idcNum: 4
hostUin: 123456789
topicId: V13LmPKk0JLNRY
noTopic: 0
uin: 123456789
pageStart: 0
pageNum: 30
skipCmtCount: 0
singleurl: 1
batchId: 
notice: 0
appid: 4
inCharset: utf-8
outCharset: utf-8
source: qzone
plat: qzone
outstyle: json
format: jsonp
json_esc: 1
question: 
answer: 
callbackFun: shine0
_: 1551790719497

其中有几个关键参数:

  1. g_tk - 与相册列表参数一致
  2. topicId - 与相册列表参数中的id一致
  3. pageStart - 本次请求照片的起始编号
  4. pageNum - 本次请求的照片数量

为了一次性获取所有照片,可以将pageStart设为0,pageNum设为所有相册所含照片的最大值。

同样可以对上面的参数进行简化,在相册列表请求参数的基础上添加topicId,pageStart和pageNum三个参数即可。

下面来看返回的照片列表信息:

shine0_Callback({
 "code":0,
 "subcode":0,
 "message":"",
 "default":0,
 "data":
{
 "limit" : 0,
 "photoList" : [
 {
 "batchId" : "1402662093402000",
 "browser" : 0,
 "cameratype" : " ",
 "cp_flag" : false,
 "cp_x" : 455,
 "cp_y" : 388,
 "desc" : "",
 "exif" : {
 "exposureCompensation" : "",
 "exposureMode" : "",
 "exposureProgram" : "",
 "exposureTime" : "",
 "flash" : "",
 "fnumber" : "",
 "focalLength" : "",
 "iso" : "",
 "lensModel" : "",
 "make" : "",
 "meteringMode" : "",
 "model" : "",
 "originalTime" : ""
 },
 "forum" : 0,
 "frameno" : 0,
 "height" : 621,
 "id" : 0,
 "is_video" : false,
 "is_weixin_mode" : 0,
 "ismultiup" : 0,
 "lloc" : "NDN0sggyKs3smlOg6eYghjb0ZRsmAAA!",
 "modifytime" : 1402661792,
 "name" : "QQ图片20140612104616",
 "origin" : 0,
 "origin_upload" : 0,
 "origin_url" : "",
 "owner" : "123456789",
 "ownername" : "123456789",
 "photocubage" : 91602,
 "phototype" : 1,
 "picmark_flag" : 0,
 "picrefer" : 1,
 "platformId" : 0,
 "platformSubId" : 0,
 "poiName" : "",
 "pre" : "http:\/\/b171.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfSk58K2rQY!\/a\/dIY29GUbJgAA&bo=pANtAgAAAAABCeY!",
 "raw" : "http:\/\/r.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfSk58K2rQY!\/r\/dIY29GUbJgAA",
 "raw_upload" : 1,
 "rawshoottime" : 0,
 "shoottime" : 0,
 "shorturl" : "",
 "sloc" : "NDN0sggyKs3smlOg6eYghjb0ZRsmAAA!",
 "tag" : "",
 "uploadtime" : "2014-06-13 20:21:33",
 "url" : "http:\/\/b171.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfSk58K2rQY!\/b\/dIY29GUbJgAA&bo=pANtAgAAAAABCeY!",
 "width" : 932,
 "yurl" : 0
 },
 // ...
 ]
 "t" : "952444063",
 "topic" : {
 "bitmap" : "10000000",
 "browser" : 0,
 "classid" : 106,
 "comment" : 1,
 "cover_id" : "NDN0sggyKs3smlOg6eYghjb0ZRsmAAA!",
 "createtime" : 1402661881,
 "desc" : "",
 "handset" : 0,
 "id" : "V13LmPKk0JLNRY",
 "is_share_album" : 0,
 "lastuploadtime" : 1402662103,
 "modifytime" : 1408271987,
 "name" : "毕业季",
 "ownerName" : "707922098",
 "ownerUin" : "707922098",
 "pre" : "http:\/\/b171.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfGuwSk58K2rQY!\/a\/dIY29GUbJgAA",
 "priv" : 1,
 "pypriv" : 1,
 "share_album_owner" : 0,
 "total" : 4,
 "url" : "http:\/\/b171.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfGuwSk58K2rQY!\/b\/dIY29GUbJgAA",
 "viewtype" : 0
 },
 "totalInAlbum" : 4,
 "totalInPage" : 4
}

返回的照片信息都存于photoList, 上面同样只截取了一张照片的信息,后面一部分返回的是当前相册的一些基本信息。totalInAlbum, totalInPage存储了当前相册总共包含的照片数及本次返回的照片数。而我们需要下载的图片链接则是url!

OK, 到此,所有请求和响应数据都分析清楚了,接下来便是coding的时候了。

确定爬取方案

  1. 创建qqzone类,初始化用户信息
  2. 使用Selenium模拟登录
  3. 获取Cookies和g_tk
  4. 使用requests获取相册列表信息
  5. 遍历相册,获取照片列表信息并下载照片

创建qqzone类

class qqzone(object):
 """QQ空间相册爬虫"""
 def __init__(self, user):
 self.username = user['username']
 self.password = user['password']

模拟登录

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import WebDriverExceptio

# ...

def _login_and_get_args(self):
 """登录QQ,获取Cookies和g_tk"""
 opt = webdriver.ChromeOptions()
 opt.set_headless()

 driver = webdriver.Chrome(chrome_options=opt)
 driver.get('https://i.qq.com/')
 # time.sleep(2)

 logging.info('User {} login...'.format(self.username))
 driver.switch_to.frame('login_frame')
 driver.find_element_by_id('switcher_plogin').click()
 driver.find_element_by_id('u').clear()
 driver.find_element_by_id('u').send_keys(self.username)
 driver.find_element_by_id('p').clear()
 driver.find_element_by_id('p').send_keys(self.password)
 driver.find_element_by_id('login_button').click()

 time.sleep(1)
 driver.get('https://user.qzone.qq.com/{}'.format(self.username))

此处需要注意的是:

  1. 使用selenium需要安装对应的webdriver
  2. 可以通过webdriver.Chrome()指定浏览器位置,否则默认从环境变量定义的路径查找
  3. 如果电脑打开浏览器较慢,可能需要在driver.get后sleep几秒

获取 Cookies

使用selenium获取Cookies非常方便

self.cookies = driver.get_cookies()

获取 g_tk

获取g_tk最开始可以说是本爬虫最大的难点,因为从网页中根本找不到直接写明的数值,只有各种函数调用。为此我全局搜索,发现好多地方都有其获取方式。

g_tk

最后选择了其中一处,通过selenium执行脚本的功能成功获取到了g_tk!

self.g_tk = driver.execute_script('return QZONE.FP.getACSRFToken()')

到此,selenium的使命就完成了,剩下的将通过requests来完成。

初始化 request.Session

接下来需要逐步生成请求然后获取数据。但是为方便起见,这里使用会话的方式请求数据,配置好cookie和headers,省的每次请求都设置一遍。

def _init_session(self):
 self.session = requests.Session()
 for cookie in self.cookies:
 self.session.cookies.set(cookie['name'], cookie['value'])
 self.session.headers = {
 'Referer': 'https://qzs.qq.com/qzone/photo/v7/page/photo.html?init=photo.v7/module/albumList/index&navBar=1',
 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36'
 }

请求相册信息

获取相册信息,需要先封装好请求参数,然后通过session.get爬取数据,再通过正则匹配以json格式读取jsonp数据,最后解析所需的name和id。

def _get_ablum_list(self):
 """获取相册的列表信息"""
 album_url = '{}{}'.format(
 'https://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/fcg_list_album_v3?',
 self._get_query_for_request())

 logging.info('Getting ablum list id...')
 resp = self.session.get(album_url)
 data = self._load_callback_data(resp)

 album_list = {}
 for item in data['data']['albumListModeSort']:
 album_list[item['name']] = item['id']

 return album_list

其中的参数组合来自下面的函数_get_query_for_request函数。

def _get_query_for_request(self, topicId=None, pageStart=0, pageNum=100):
 """获取请求相册信息或照片信息所需的参数

 Args:
 topicId: 每个相册对应的唯一标识符
 pageStart: 请求某个相册的照片列表信息所需的起始页码
 pageNum: 单次请求某个相册的照片数量

 Returns:
 一个组合好所有请求参数的字符串
 """
 query = {
 'g_tk': self.g_tk,
 'hostUin': self.username,
 'uin': self.username,
 'appid': 4,
 'inCharset': 'utf-8',
 'outCharset': 'utf-8',
 'source': 'qzone',
 'plat': 'qzone',
 'format': 'jsonp'
 }
 if topicId:
 query['topicId'] = topicId
 query['pageStart'] = pageStart
 query['pageNum'] = pageNum
 return '&'.join('{}={}'.format(key, val) for key, val in query.items())

其中的jsonp解析函数如下,主体部分就是一个正则匹配,非常简单。

def _load_callback_data(self, resp):
 """以json格式解析返回的jsonp数据"""
 try:
 resp.encoding = 'utf-8'
 data = loads(re.search(r'.*?\(({.*}).*?\).*', resp.text, re.S)[1])
 return data
 except ValueError:
 logging.error('Invalid input')

解析并下载照片

获取相册列表后,逐个请求照片列表信息,进而逐一下载

def _get_photo(self, album_name, album_id):
 """获取单个相册的照片列表信息,并下载该相册所有照片"""
 photo_list_url = '{}{}'.format(
 'https://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/cgi_list_photo?',
 self._get_query_for_request(topicId=album_id))

 logging.info('Getting photo list for album {}...'.format(album_name))
 resp = self.session.get(photo_list_url)
 data = self._load_callback_data(resp)
 if data['data']['totalInPage'] == 0:
 return None

 file_dir = self.get_path(album_name)
 for item in data['data']['photoList']:
 path = '{}/{}.jpg'.format(file_dir, item['name'])
 logging.info('Downloading {}-{}'.format(album_name, item['name']))
 self._download_image(item['url'], path)

下载图片也是通过request,记得设置超时时间。

def _download_image(self, url, path):
 """下载单张照片"""
 try:
 resp = self.session.get(url, timeout=15)
 if resp.status_code == 200:
 open(path, 'wb').write(resp.content)
 except requests.exceptions.Timeout:
 logging.warning('get {} timeout'.format(url))
 except requests.exceptions.ConnectionError as e:
 logging.error(e.__str__)
 finally:
 pass

爬取测试

  • 爬取过程

capturing

  • 爬取结果

downloaded photos

写在最后

  1. 如果将请求参数中的format由jsonp改成json,则可以直接获取json数据
  2. 本用例并未使用多进程或多线程,所以速率不算快,还有待优化的地方

理论联系实际,记录下读《Deep Face Recognition: A Survey》的心得体会

一个完整的人脸识别流程应该包含以下几个模块:

1:人脸的检测: 定位图片中存在人脸的位置

2:人脸的对齐: 对齐人脸到正则坐标系的坐标

3:人脸的识别:

①:活体的检测
②:人脸的识别-面部姿态(处理姿态,表情,遮挡等),特征提取,人脸比对

上述流程中,第三步是整个系统的关键。

一:人脸识别的四个发展阶段

如图所示,回顾漫长的人脸识别的发展历程,大致可以划分为4个阶段

①:1964-1990:初步尝试

这个阶段是属于人脸识别的探索阶段,人们尝试使用一些简单的算法来初步尝试人脸的机器自动识别,人类最早的研究工作至少可追朔到二十世纪五十年代在心理学方面的研究和六十年代在工程学方面的研究。这一阶段主要是从感知和心理学角度探索人类识别人脸机理的,也有从视觉机理角度进行研究的。

②:1991~2000:快速发展

这一阶段研究的重点在人脸识别所需要的面部特征。研究者用计算机实现了较高质量的人脸灰度图模型。这一阶段工作的特点是识别过程全部依赖于操作人员,不是一种可以完成自动识别的系统,以至于这个阶段的人脸识别所需求的条件非常严苛,但是依然产生了一些极具影响力的算法和理论。

③:2000~2012:走向人机交互

这一阶段可以理解为是上一阶段的提升和改进,设计的系统可以对姿态,表情,光照,遮挡等环境条件进行处理,主要研究用几何特征参数来表示人脸正面图像。采用多维特征矢量表示人脸面部特征,并设计了基于这一特征表示法的识别系统。实质上这一阶段的算法(SVM,Boosting),实质上可以理解为带着一层隐藏节点的浅层学习,但是泛化能力依旧有限。这一阶段,人脸识别开始逐渐成熟,一些实用的系统开始诞生

④:2012~至今:快速发展

这一阶段,人脸识别的主流算法开始转为深度学习,深度学习的典型代表应用便是人脸识别,大计算、大数据、大模型则是深度神经网络的三大支柱与基础。第四阶段大量实用的系统与成功的应用案例出现,许多现象级别的网络结构开始出现,许多新兴的人脸识别公司也开始诞生。

二:人脸识别的算法流程

人脸的识别流程:面部姿态处理(处理姿态,亮度,表情,遮挡),特征提取,人脸比对。

1:面部处理face processing

这部分主要对姿态(主要)、亮度、表情、遮挡进行处理,可提升FR模型性能

两种方式:

one to many:从单个图像生成不同姿态的图像,使模型学习到不同的姿态

many to one:从多个不同姿态的图像中恢复正则坐标系视角下的图像,用于受限条件

2:特征提取 feature extraction

特征提取网络可分为backboneassembled两类

主干网络(Backbone network):一些通用的用于提取特征的网络

组装网络(Assembled network):用于拼接在主干网络前/后的用于特定训练目标的网络

Backbone Network

①:Mainstream architectures

主流的网络架构包括AlexNet,VGGNet,GoogleNet,ResNet,SENet等

​ AlexNet:引入ReLU,dropout,data augmentation等,第一次在图像上有效使用Conv

​ VGGNet:提出重复用简单网络块堆叠;滤波器3x3减少权重量,增强表示能力

GoogleNet:1x1跨通道整合信息,同时用于升降维减少参数;并行结构由网络自行挑选最好的路径;多个出口计算不同位置损失,综合考虑不同层次的信息

​ ResNet:引入残差块,削弱层间联系,提高模型容忍度;使得信息能跨层注入下游,恢复在信息蒸馏过程中的丢失的信息;残差块部分解决梯度消失

​SENet:在上述网络中嵌入Squeeze-and-Excitation块,通过1x1块显式地构建通道间相互关系,能自适应的校准通道间的特征响应。

Squeeze:全局平均池化得到1x1xC用于描述全局图像,使浅层也能获得全局感受野;
Excitation:使用FC-ReLU-FC-Sigmoid(类似门的作用)过程中得到各通道权重,然后rescale到WxHxC。从全局感受野和其它通道获得信息,SE块可自动根据每个通道的重要程度去提升有用的特征的权重,通过这个对原始特征进行重标定。

Special architectures

除了主流的最广泛使用的网络架构,还有一些特殊的模块和技巧,如max-feature-map activation,bilinear CNN,pairwise relational network等

Joint alignment-representation networks

这类模型将人脸检测、人脸对齐等融合到人脸识别的pipeline中进行端到端训练。比起分别训练各个部分的模型,这种端到端形式训练到的模型具有更强的鲁棒性


②:Assembled Network

组装网络用于拼接在主干网前或后方,用于多输入或多任务的场景中

Multi-input networks

在one-to-many这类会生成不同部位、姿态的多个图像时,这些图片会输入到一个multi-input的组装子网络,一个子网络处理其中一张图片。然后将各个输出进行联结、组合等,再送往后续网络。

如下图所示的多视点网络Multi-view Deep Network (MvDN)进行cross-view recognition(对不同视角下的样本进行分类)

multi-task networks

在某些情景中,人脸识别是主要任务,若需要同时完成姿态估计、表情估计、人脸对齐、笑容检测、年龄估计等其余任务时,可以使用multi-task组装网。

如下图Deep Residual EquivAriant Mapping (DREAM),用于特征层次的人脸对齐


3:损失函数 loss function

①:Euclidean-distance-based loss:(上图绿色)

基于欧几里得距离损失是一种度量学习方法,它通过对输入图像提取特征将其嵌入欧几里得空间,然后减小组内距离、增大组间距离,包括contrastive loss,triplet loss,center loss和它们的变种

contrastive loss:

损失计算需要image pair,增加负例(两张图不同脸)距离,减少正例(同脸)距离。它考虑的是正例、负例之间的绝对距离,表达式为:


其中yij=1表示xi,xj是正例pair,yij=0表示负例pair,f(.)表示特征嵌入函数

Triplet loss

该损失计算需要triplet pair,三张图,分别为anchor, negative, positive。最小化anchor和positve间距离,同时最大化anchor和negative间距离,表达式为

注意,数据集中大多数的人脸之间都很容易区分,容易区分的triplet pair算出来的L很小,导致收敛缓慢,因此triplet pair选择的时候需要选择难以区分的人脸图像

Center loss

该损失在原损失的基础上增加一个新的中心损失LC,及每个样本与它的类别中心之间的距离,通过惩罚样本与距离间的距离来降低组内距离


②:Angular/cosine-margin-based loss(黄色)

基于角度/余弦边缘损失,它使得FR网络学到的特征之间有更大的角度/余弦

Softmax

L-Softmax

令原始的Softmax loss中:

同时增大yi对应的项的权重可得到Large-margin softmax。该权重m引入了multiplicative angular/cosine margin

二分类的分类平面为

L-softmax存在问题:收敛比较困难,||W1||,||W2||通常也不等

A-softmax (SphereFace)

在L-softmax的基础上,将权重L2正则化得到||W||=1,因此正则化后的权重落在一个超球体上

二分类的分类超平面为:

CosFace / ArcFace

与A-softmax相同思想,但CosFace/ArcFace引入的是additive angular/cosine margin

各类损失函数对比:

4:面部匹配 face matching

对面部认证、面部识别任务,多数方法直接通过余弦距离或者L2距离直接计算两个特征图的相似性,再通过阈值对比threshold comparison或者最近邻NN判断是否为同一人。此外,也可以通过Metric learning或者稀疏表示分类器sparse-representation-based classifier进行后处理,再进行特征匹配

5:数据集

数据集的Depth、Breadth

Depth

不同人脸数较小,但每个人的图像数量很大。Depth大的数据集可以使模型能够更好的处理较大的组内变化intra-class variations,如光线、年龄、姿态。

VGGface2(3.3M,9K)

Breadth

不同人脸数较大,但每个人的图像数量较小。Breadth大的数据集可以使模型能够更好的处理更广范围的人群。

MS-Celeb-1M(10M,100K)、MegaFace(Challenge 2,4.7M,670K)

数据集的data noise

由于数据源和数据清洗策略的不同,各类数据集或多或少存在标签噪声label noise,这对模型的性能有较大的影响。

数据集的data bias

大多数数据集是从网上收集得来,因此主要为名人,并且大多大正式场合。因此这些数据集中的图像大多数是名人的微笑、带妆照片,年轻漂亮。这与从日常生活中获取的普通人的普通照片形成的数据集(Megaface)有很大的不同。

另外,人口群体分布不均也会产生data bias,如人种、性别、年龄。通常女性、黑人、年轻群体更难识别。


6:评估任务及性能指标

①:training protocols

subject-dependent protocol:所有用于测试的图像中的ID已在训练集中存在,FR即一个特征可分的分类问题(不同人脸视为不同标签,为测试图像预测标签)。这一protocol仅适用于早期FR研究和小数据集。

subject-independent protocol:测试图像中的ID可能未在训练集中存在。这一protocol的关键是模型需要学得有区分度的深度特征表示

②:Evaluation metric

Face verification:性能评价指标通常为受试者操作特性曲线(ROC - Receiver operating characteric),以及平均准确度(ACC)

Close-set face identification:rank-N,CMC (cumulative match characteristic)

Open-set face identification:

三:一些新的前景

①:Cross-Factor Face Recognition

Cross-Pose:正脸、侧脸,可使用one-to-many augmentation、many-to-one normalizations、multi-input networks、multi-task learning加以缓解

②:Heterogenous Face Recognition

NIS-VIS FR:低光照环境中NIR (near-infrared spectrum 近红外光谱)成像好,因此识别NIR图像也是一大热门话题。但大多数数据集都是VIS (visual ligtht spectrum可见光光谱)图像。-- 迁移学习

Low-Resolution FR:聚焦提高低分辨率图像的FR性能

Phote-Sketch FR:聚焦人脸图像、素描间的转换。 -- 迁移学习、image2image

③:Multiple (or single) media Face Recognition

Low-Shot FR:实际场景中,FR系统通常训练集样本很少(甚至单张)

Set/Template-based FR

Video FR:两个关键点,1. 各帧信息整合,2. 高模糊、高姿态变化、高遮挡

④:Face Recognition in Industry

3D FR

Partial FR:给定面部的任意子区域

Face Anti-attack:

FR for Mobile Device

四:参考资料

1:Deep Face Recognition: A Survey

https://arxiv.org/pdf/1804.06655.pdf

2:Deep Residual EquivAriant Mapping https://openaccess.thecvf.com/content_cvpr_2018/html/Cao_Pose-Robust_Face_Recognition_CVPR_2018_paper.html

3:Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks

https://arxiv.org/pdf/1703.1059

文转自:掘金 作者:chess

前言

本文讲的图片上传,主要是针对上传头像的。大家都知道,上传头像一般都会分成以下 4 个步骤:

选择图片 -> 预览图片 -> 裁剪图片 -> 上传图片

接下来,就详细的介绍每个步骤具体实现。

一、选择图片

选择图片有什么好讲的呢?不就一个 input[type=file] ,然后点击就可以了吗?确实是这样的,但是,我们想要做得更加的友好一些,比如需要过滤掉非图片文件, 或只允许从摄像头拍照获取图片等,还是需要进行一些简单配置的。

下面就先来看看最简单的选择图片:

这时候,点击这个 input , 在 iOS 手机的显示如下:

其中的 “浏览” 选项,可以查看到非图片类型的文件,这并不是我们想要的结果,毕竟我们只想要图片类型。可以通过 accept 属性来实现,如下:

这样就可以过滤掉非图片类型了。但是图片的类型可能也太多了, 有些可能服务器不支持,所以,如果想保守一些,只允许 jpg 和 png 类型,可以写成这样:

或:

OK, 过滤非图片的需求搞定了。但是有时候 ,产品还要求只能从摄像头采集图片,比如需要上传证件照,防止从网上随便找别人的证件上传,那capture 属性就可以派上用场了:

这时候,就不能从文件系统中选择照片了,只能从摄像头采集。到了这一步,可能觉得很完美了,但是还有个问题,可能有些变态产品要求默认打开前置摄像头采集图片,比如就是想要你的自拍照片。 capture 默认调用的是后置摄像头。默认启用前置摄像头可以设置 capture="user" ,如下:

好啦,关于选择图片的就讲么这么多了,有个注意的地方是,可能有些配置在兼容性上会有一些问题,所以需要在不同的机型上测试一下看看效果。

下面再来谈谈预览图片的实现。

二、预览图片

在远古时代,前端并没有预览图片的方法。当时的做法时,用户选择图片之后,立刻把图片上传到服务器,然后服务器返回远程图片的 url 给前端显示。这种方法略显麻烦,而且会浪费用户的流量,因为用户可能还没有确定要上传,你却已经上传了。幸好,远古时代已经离我们远去了,现代浏览器已经实现了前端预览图片的功能。常用的方法有两个,分别是 URL.createObjectURL() 和 FileReader 。虽然他们目前均处在 w3c 规范中的 Working Draft 阶段, 但是大多数的现代浏览器都已经良好的支持了。 下面就介绍一下如何使用这两个方法。

1. 使用 URL.createObjectURL 预览

URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的 URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。用法用下:

objectURL = URL.createObjectURL(object);

其中,object 参数指 用于创建 URL 的 File 对象、Blob 对象或者 MediaSource 对象。

对于我们的 input[type=file] 而言, input.files[0] 可以获取到当前选中文件的 File 对象。示例代码如下:

具体用法可以参考 MDN上的 URL.createObjectURL(),

2. 使用 FileReader 预览

FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。同理的,我们也可以通过 input.files[0] 获取到当前选中的图片的 File 对象。

特别注意,FileReader 和 是异步读取文件或数据的!

下面是使用 FileReader 预览图片的示例:

会发现, FileReader 会相对复杂一些.

更多关于 FileReader 的用法 ,可以参考 MDN 文档 FileReader

3.两种方法的对比

我个人更加倾向于使用 URL.createObjectURL() 。主要原先它的 API 简洁,同步读取,并且他返回的是一个 URL ,比 FileReaer 返回的base64 更加精简。兼容性上,两者都差不多,都是在 WD 的阶段。性能上的对比, 在 chrome 上, 选择了一张 2M 的图片, URL.createObjectURL() 用时是 0 , 而 FileReader 用时 20ms 左右。 0 感觉不太合理,虽然这个方法立刻就会返回一个 URL ,但是我猜测实际上这个 URL 指定的内容还没有生成好,应该是异步生成的,然后才渲染出来的。所以并没有很好的办法来对比他们的性能。

如果想要学习更多关于图片预览,可以阅读以下两篇文章:

  • 使用FileReader实现前端图片预览
  • js图片/视频预览 - URL.createObjectURL()
  • 三、裁剪图片

    关于图片的裁剪,很自然的会想到使用 canvas ,确实是要通过 canvas, 但是如果全部我们自己来实现,可能需要做比较多的工作,所以为了省力,我们可以站在巨人的肩膀上。比较优秀的图片裁剪库是 cropperjs , 该库可以对图片进行缩放、移动和旋转。

    cropperjs 的详细配置这里就不展开了 ,需要的可以自己去看文档就好。下面我们就以这个库为基础,实现一个裁剪人脸的例子:

    效果图如下:

    四、上传

    前面的操作已经完成了图片上传前的准备,包括选择图片、预览图片、编辑图片等,那接下来就可以上传图片了。上面的例子中,使用了 cropperInstance.getCroppedCanvas() 方法来获取到对应的 canvas 对象 。有了 canvas 对象就好办了,因为 canvas.toBlob() 方法可以取得相应的 Blob 对象,然后,我们就可以把这个 Blob 对象添加到 FromData 进行无刷新的提交了。大概的代码如下:

    这段代码并不能真正执行,因为我们还没有对应的后端服务器。如果想要尝试上传图片的朋友,可以参考一下这篇文章 写给新手前端的各种文件上传攻略,从小图片到大文件断点续传,由于篇幅原因,这里就不展开啦。

    五、最后

    关于图片上传的介绍,差不多不到些结束了。但是之前在 iPhone 和 小米 手机上,遇到一个奇怪的问题: 就是我使用前置摄像头自拍出来的照片,选择之后 ,会自逆时针旋转 90 度,比如像下图:

    拍照的时候明明就是正着拍的,为什么预览就会变成横着了呢?当时第一次遇到这个问题的时候,也觉得好奇怪。后来查了一下,得知这是因为拍照时,相机都会记录拍照的角度信息,可能 iPhone 前置摄像头记录的角度信息和其他的有点不一样,而 iPhone 自己的相册在浏览照片时,自动纠正了角度 ,而浏览器却没有纠正,所以才会出现这个旋转。

    为了解决这个问题,需要使用 EXIF 这个库来处理。

    我刚刚试了一下,发现我的 iPhone 现在竟然不会有这个问题了,大概是半年前,当时在做一个需求时,自拍的图片会发生这种旋转,有可能是 iOS 系统升级后, 已经修复了这个问题。而现在身边又没有小米手机, 所以也不好复现。还好,当时我保存了一张会自动旋转的图片。

    图片下载后,用电脑的图片查看器打开是正常的,但是,在浏览器中,选择这个图片后,使用 URL.createObjectURL() 或 FileReader 来预览就会发生旋转。甚至直接 img 标签引入也会逆时针旋转了 90 度,比如:

    效果如下:

    下面就以这张图片为例,介绍一下如何使用 EXIF 来检测图片角度。关于 EXIF 的详细用法大家可以到 github 的主页上查看 github.com/exif-js/exi…

    上面代码的输出 allMetaData.Orientation 的结果为 6 , 那 6 到底是什么意思呢? 可以参考这个篇文章 Exif Orientation Tag 里面有个表格:

    如果这个表格看不太懂,再参考一下这篇文章 JPEG Orientation,里有个图:

    可以看出,摄像头信息是逆时针旋转了 90 度。那要怎么纠正呢?可以使用 CSS 的 transfrom: rotate(-90deg) 顺时针旋转 90 度抵消掉这个角度就好。

    事实上, CropperJS 也会检测图片的 EXIF 信息,并且会自动纠正角度的,详情参考 github.com/fengyuanche… 这里也提到了,但只支持读取 jpg 图片的 EXIF 信息,而我们这张图片是 PNG 所以并不支持。

    有个 CSS 属性叫做 image-orientation , 它有个值叫做 from-image , 就是使用图片的 EXIF 数据来旋转的。可惜,目前 chrome 不支持该属性。有兴趣的可以了解一下。

    结语

    好啦,就先写到这里啦,有问题的欢迎在评论区交流哈~

    想了解更多前端知识欢迎评论区留言或私信我!

    欢迎关注公众号:fkdcxy 疯狂的程序员丶 获取更多前端教程!