析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可以分別獲取到兩組重要信息:
先看請求包:
# 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
其中有幾個關鍵參數:
為了一次性獲取所有照片,可以將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的時候了。
確定爬取方案
創建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))
此處需要注意的是:
獲取 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
寫在最后
理論聯系實際,記錄下讀《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
特征提取網絡可分為backbone和assembled兩類
主干網絡(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 指定的內容還沒有生成好,應該是異步生成的,然后才渲染出來的。所以并沒有很好的辦法來對比他們的性能。
如果想要學習更多關于圖片預覽,可以閱讀以下兩篇文章:
三、裁剪圖片
關于圖片的裁剪,很自然的會想到使用 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 瘋狂的程序員丶 獲取更多前端教程!
*請認真填寫需求信息,我們會在24小時內與您取得聯系。