:從特征臉到門禁系統(tǒng))
簡介基于Python的PCA人臉識別算法原理與實現(xiàn)資源包面向課程設計與期末大作業(yè)場景適合希望快速掌握人臉識別核心流程的Python初學者。資源圍繞PCA特征臉方法展開涵蓋樣本均值計算、協(xié)方差矩陣構建、特征值分解、降維投影及最近鄰匹配等關鍵環(huán)節(jié)配合可運行的Python主程序和說明文檔讓讀者既能理解算法推導又能直接看到實際運行效果。包內還提供10維、20維、30維、40維等不同主成分維度下的結果對比圖直觀反映維度選擇對識別精度和重建質量的影響。壓縮包共8個文件以py程序、txt說明和png圖像為主整體容量僅257KB結構緊湊無冗余。目前已有449人學習下載適合零基礎學生作為高分開課設、人臉識別入門或期末項目答辯的參考模板。1. 為什么人臉識別入門選了PCA從特征臉說起如果你找過人臉識別算法相關資料大概率會撞見PCA和特征臉Eigenface這個組合。哪怕現(xiàn)在門禁機里跑的已經(jīng)是深度學習模型PCA在人臉識別領域仍然是繞不開的第一課。原因很現(xiàn)實它不需要GPU不需要海量標注數(shù)據(jù)幾十張灰度圖丟進去numpy就能算出人臉主成分然后做識別。對于畢設、課程設計、轉行練手這是最容易跑通、也最容易解釋清楚的方案。這個標題里的PCA人臉識別解決的是已知一批標注好的人臉照片判斷一張新照片是誰的問題。它把每一張人臉當成高維空間的一個點用主成分分析找出人臉分布的主方向再把所有人臉投影到低維空間最后用最近鄰判斷身份。整個過程邏輯清晰參數(shù)少代碼量小尤其適合第一次動手做圖像識別的人。如果你剛接觸Python圖像處理想用一份能跑的代碼理解降維和分類是怎么配合的這個方向非常合適。2. PCA人臉識別的核心原理高維空間里找人臉主方向2.1 人臉圖像的本質一個像素就是一個維度要理解PCA先得接受一個抽象一張灰度圖就是一個高維向量。假設人臉圖統(tǒng)一縮放到64×64像素那么它就有4096個像素。把每個像素的亮度值按行拉平就得到一個4096維的向量。如果有100張人臉那就是100個點分布在4096維空間里。問題在于4096維空間太稀疏了。這些點不可能是均勻撒開的它們被人臉的結構約束著眼睛、鼻子、嘴的相對位置大致固定光照、表情帶來的變化集中在某些方向上。PCA要做的就是在這4096維空間里找到一組新的坐標軸讓數(shù)據(jù)在這些軸上的方差依次遞減——第一個軸方向信息量最大第二個其次最后我們只用前幾十個軸就能近似還原人臉。這就是主成分分析這個名字的來歷。2.2 主成分分析與特征臉降維后的人臉基函數(shù)把一個包含N張人臉、每張M維的數(shù)據(jù)矩陣XN行M列行是人臉列是像素做中心化每列減去均值得到X_c。然后計算協(xié)方差矩陣 X_c^T * X_c / (N-1)對它做特征值分解。較大的特征值對應的特征向量就是主成分。用人臉圖像算出來的主成分如果還原成圖片看起來就像一張模糊的鬼臉所以被稱為特征臉Eigenface。所有訓練人臉都可以近似表示為均值臉 若干個特征臉的線性組合。組合系數(shù)就是人臉在低維空間里的坐標。關鍵點這里用的協(xié)方差矩陣維度是M×M也就是像素數(shù)×像素數(shù)。64×64的圖是4096×4096還可以接受如果原圖是200×200那就是40000×40000內存直接爆掉。所以工程上常用SVD奇異值分解繞開這個大矩陣或者先把圖像縮小。后面實現(xiàn)代碼里會講具體做法。2.3 識別流程訓練與匹配兩段式PCA人臉識別的完整流程分兩步。訓練階段讀入所有標注好的人臉圖縮放、灰度化、拉成向量計算均值臉和特征臉確定保留K個主成分把每張訓練人臉投影到K維子空間得到模板向量。識別階段對一張新圖片做同樣的預處理投影到同一個子空間得到查詢向量計算查詢向量與所有模板向量之間的距離取最近的那個如果距離小于預設閾值就判定為對應身份否則認為無法識別。這里有一個容易被忽略的前提投影矩陣只能由訓練集確定。新來的圖片不能拿來重新算特征臉否則就泄漏了未知信息對單張照片做門禁識別時也不現(xiàn)實。所以先固化特征臉參數(shù)再對每張測試圖只做矩陣乘法。3. 用Python從零實現(xiàn)PCA人臉識別可復現(xiàn)的最小代碼3.1 數(shù)據(jù)準備用現(xiàn)有數(shù)據(jù)集還是一個文件夾最常見的數(shù)據(jù)集是ATT原ORL人臉庫包含40個人每人10張112×92灰度圖。如果你手頭沒有也可以自己建文件夾每個子文件夾放一個人的照片要求人臉基本居中、尺度近似。下面這段代碼負責加載數(shù)據(jù)返回一個二維數(shù)組和對應的標簽import os import cv2 import numpy as np def load_faces(data_dir, img_size(64, 64)): X [] y [] for person_id, person_name in enumerate(sorted(os.listdir(data_dir))): person_dir os.path.join(data_dir, person_name) if not os.path.isdir(person_dir): continue for img_name in sorted(os.listdir(person_dir)): if not img_name.lower().endswith((.jpg, .png)): continue img_path os.path.join(person_dir, img_name) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, img_size) X.append(img.flatten()) # 拉成一維 y.append(person_id) return np.array(X), np.array(y)邏輯說明person_id從0開始按文件夾名字母序編號cv2.imread第二個參數(shù)傳cv2.IMREAD_GRAYSCALE避免讀成三通道后再手動轉換flatten()默認按行拉平所以同一個人不同圖片要保持相同尺寸。參數(shù)里img_size(64,64)是經(jīng)驗值太小丟細節(jié)太大會拖慢特征分解。如果數(shù)據(jù)量小32×32也夠用。3.2 核心函數(shù)中心化、特征臉、投影數(shù)據(jù)準備好了就進入PCA核心。這里我采用SVD方式實現(xiàn)原因后面詳述。主要函數(shù)有三部分計算均值臉、計算主成分、將數(shù)據(jù)投影到低維空間。def pca_fit(X, n_components): # X: shape (n_samples, n_features) mean_face X.mean(axis0) X_centered X - mean_face # 用SVD而不是直接算協(xié)方差矩陣的特征分解 U, S, Vt np.linalg.svd(X_centered, full_matricesFalse) # 前n_components個右奇異向量就是主成分方向 eigenvectors Vt[:n_components] # 投影將中心化后的數(shù)據(jù)轉到低維空間 X_projected X_centered.dot(eigenvectors.T) return mean_face, eigenvectors, X_projected def pca_transform(X_new, mean_face, eigenvectors): X_centered X_new - mean_face return X_centered.dot(eigenvectors.T)邏輯說明np.linalg.svd(X_centered, full_matricesFalse)對數(shù)據(jù)矩陣直接做奇異值分解返回的Vt行向量是協(xié)方差矩陣的特征向量對應的奇異值平方與特征值成正比。相比np.linalg.eigh去分解M×M的協(xié)方差矩陣SVD在樣本數(shù)遠小于像素數(shù)時更省內存、數(shù)值更穩(wěn)定。eigenvectors的每一行是特征臉與公式里的特征向量方向一致。X_projected是訓練集在低維空間的坐標每行對應一個人。參數(shù)說明n_components就是保留的主成分個數(shù)。它不能超過樣本數(shù)N和特征維度M的最小值。SVD模式下Vt最多有min(N,M)行所以n_components必須小于等于訓練集圖片張數(shù)。例如用40人×10張400張圖訓練最多只能取399個主成分而實際我們通常只取幾十個。3.3 識別新圖片最近鄰與閾值拒絕有了投影坐標識別就是距離計算。常見做法是歐氏距離也可以用余弦相似度。這里演示一個簡單的最近鄰分類器外加陌生人拒識的初步思路。def predict(X_query, X_projected, labels, mean_face, eigenvectors, thresholdNone): q pca_transform(X_query.reshape(1, -1), mean_face, eigenvectors) distances np.linalg.norm(X_projected - q, axis1) idx np.argmin(distances) min_dist distances[idx] if threshold is not None and min_dist threshold: return -1, min_dist # -1 表示不在庫中 return labels[idx], min_dist邏輯說明X_query是經(jīng)過同樣預處理的新圖片向量pca_transform把它投影到訓練好的子空間np.linalg.norm(..., axis1)計算查詢點與所有訓練點之間的歐氏距離。閾值的作用是拒絕庫里沒有的人門禁系統(tǒng)里如果來了一個陌生人他的投影距離必然較遠直接返回-1。閾值需要實測校準沒有萬能值我是拿一部分已知人臉求距離分布后取分位數(shù)定的。需要特別說明識別時不能重新做PCA必須復用訓練好的mean_face和eigenvectors。一旦訓練集變動就要重新訓練并更新投影矩陣否則新舊坐標系不一致距離毫無意義。3.4 參數(shù)選擇保留多少主成分主成分數(shù)n_components直接決定信息保留比例和最終準確率。一般先畫出方差貢獻率曲線再決定保留多少個主成分。def explained_variance_ratio(s, n_total): # s是SVD返回的奇異值奇異值平方除以總方差就是每個主成分的貢獻率 variance s ** 2 ratio variance / variance.sum() cumulative np.cumsum(ratio) return cumulative # 使用示例 _, s, _ np.linalg.svd(X_centered, full_matricesFalse) cum explained_variance_ratio(s, X.shape[0]) k np.searchsorted(cum, 0.95) 1 # 保留95%方差的第一個位置邏輯說明searchsorted(cum, 0.95)返回cum中第一個大于等于0.95的索引1是因為索引從0開始。這樣選出來的k是保留95%總方差所需的最小主成分數(shù)量。實際經(jīng)驗ORL數(shù)據(jù)集上保留前30~50個主成分通常就能達到95%以上的識別率保留太多反而把噪聲和單人特有的細節(jié)裝進來泛化能力變差。4. 把準確率做上去參數(shù)調優(yōu)與數(shù)據(jù)預處理4.1 圖像尺寸與灰度歸一化決定了特征臉的分辨率PCA的輸入是像素灰度值像素值范圍、圖像尺寸直接改變主成分分布。最常見的做法是把所有人臉縮放到相同尺寸比如64×64。尺寸太小眼睛和嘴巴的信息丟失特征臉會糊成一片尺寸過大比如256×256特征維度65536維SVD速度和內存都會上升而識別率提升有限?;叶葰w一化也不能省。不同照片的全局亮度差別很大原圖可能是0~255的灰度但整體偏暗或偏亮。若不處理第一個主成分往往不是人臉結構而是整體亮度——像一張白板區(qū)分亮臉和暗臉。所以我會對每張圖做歸一化def normalize_face(img): img img.astype(np.float32) img (img - img.min()) / (img.max() - img.min() 1e-6) return img邏輯說明把每張圖獨立拉伸到[0,1]區(qū)間消除全局亮度差異。注意這里有個坑逐圖歸一化適合單張圖亮度不均的場景但如果同一人照片集里本來就含有不同光照逐圖歸一化會抹平光照差異反而有利于PCA。如果所有圖來自同一相機也可以做全局歸一化用所有圖的同一均值方差兩種都要對比實測后選擇。1e-6防止除以0。4.2 光照、對齊與直方圖均衡是翻車重災區(qū)人臉識別最經(jīng)典的敵人是光照和對齊。PCA對像素位置極其敏感眼睛坐標偏移幾個像素拉成向量后就是一次全局抖動距離計算立刻變大。所以數(shù)據(jù)預處理階段必須做兩件額外工作人臉對齊和光照歸一化。對齊可以使用OpenCV的cv2.detectMultiScale檢測人臉框然后用兩只眼睛的坐標做仿射變換把兩眼連線旋轉到水平并且統(tǒng)一兩眼間距。如果不想引入額外的人臉檢測器至少要做到裁剪時以人臉中心為基準保證鼻子在中心區(qū)域。實操里我習慣先用一個簡單的Haar級聯(lián)定位眼睛# 假設已檢測到左眼(left_x, left_y)和右眼(right_x, right_y) dx right_x - left_x dy right_y - left_y angle np.degrees(np.arctan2(dy, dx)) center ((left_x right_x) / 2.0, (left_y right_y) / 2.0) M cv2.getRotationMatrix2D(center, angle, scale1.0) aligned cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))邏輯說明先算出兩眼連線與水平線的夾角再以兩眼中點為旋轉中心將整張圖旋轉該角度。這樣人臉的傾斜姿態(tài)被粗略糾正。旋轉后還要再裁剪固定區(qū)域取包含額頭到下巴的矩形。對齊是所有傳統(tǒng)人臉識別方案的命脈不做對齊再好的降維算法也白搭。光照歸一化除了上面提到的線性拉伸還可以使用直方圖均衡化。cv2.equalizeHist可以把人臉的亮度分布拉平減少側光造成的半邊臉陰影。img cv2.equalizeHist(img)參數(shù)說明equalizeHist只接受8位單通道圖如果前面已經(jīng)轉成float歸一化需要先轉回uint8。我在實驗中直方圖均衡通常能把ORL上的識別率提升1~3個百分點尤其當訓練集和測試集拍攝環(huán)境不一致時效果明顯。4.3 主成分數(shù)K的選取策略不是越多越好很多初學者以為保留的主成分越多信息越全準確率越高。實際在PCA人臉識別里K太小會丟掉身份特征K太大會引入噪聲。比如某人的照片可能有眼鏡反光、表情擠壓這些細節(jié)被編碼進高編號的主成分后同一個人不同照片之間的距離反而被拉大識別率下降。我的經(jīng)驗做法是畫一條K-準確率曲線從5開始每隔5取一個K訓練并測試找到曲線平臺期然后在平臺區(qū)間的下限取K。這樣選出的K既穩(wěn)定又帶泛化能力。對ORL數(shù)據(jù)集K在20~40之間基本就是平臺如果你只拿10個人做演示K10左右就夠了。識別正確率的驗證方式很關鍵必須保證測試集的人沒有出現(xiàn)在訓練集里。具體做法是按人分組比如每人10張圖中取8張訓練、2張測試。如果隨機把圖片打進訓練集同一人的照片可能同時出現(xiàn)在兩側等于開卷考試準確率虛高。4.4 距離度量歐氏距離、曼哈頓距離與余弦相似度投影到低維空間后如何定義兩張臉長得像直接決定識別結果。三種常見度量各有脾氣歐氏距離對每個維度的權重平等看待是最常用的默認選擇。但它受主成分尺度影響大如果前幾個主成分方差很大歐氏距離會被它們主導。曼哈頓距離L1對離群點更不敏感在光照變化明顯的場景下有時比歐氏穩(wěn)。余弦相似度只關心方向、不關心長度適合特征向量整體縮放不改變身份的場景。我之前在自建數(shù)據(jù)集上對比過歐氏和余弦差異不大但曼哈頓略好一點??赡芤驗镻CA子空間里不同主成分的方差量級不同L1距離在投影后更接近模板匹配的直覺。建議你把三種距離都實現(xiàn)交叉驗證時選小的。代碼如下def compute_distances(q, X_projected, metriceuclidean): if metric euclidean: return np.linalg.norm(X_projected - q, axis1) elif metric manhattan: return np.sum(np.abs(X_projected - q), axis1) elif metric cosine: q_norm np.linalg.norm(q) proj_norm np.linalg.norm(X_projected, axis1) return 1.0 - (X_projected q.ravel()) / (proj_norm * q_norm 1e-6)邏輯說明余弦相似度轉成距離時用1 - 相似度數(shù)值越小代表越相似。注意X_projected q.ravel()計算的是每個訓練樣本與查詢向量的點積結果是一維數(shù)組。1e-6是為了防止某個向量模長為0。實際使用時所有模板向量和查詢向量都要先做同樣的投影然后compute_distances返回一維距離數(shù)組。5. PCA人臉識別避坑指南5個容易翻車的細節(jié)5.1 讀圖時通道混亂導致特征臉花掉現(xiàn)象訓練集加載后顯示特征臉變成紅一塊藍一塊或者明明都是灰度圖flatten()出來卻有三倍長度。原因cv2.imread(path)默認按BGR三通道讀取即使原圖是灰度圖也會被擴成(M, N, 3)。有人圖省事直接用img.flatten()把三個通道串在一起每個像素被當成三個獨立維度。解決讀圖時顯式指定cv2.IMREAD_GRAYSCALE如果圖像已經(jīng)讀成了彩色先用cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)轉灰度。另外訓練前打印X.shape確認列數(shù)是寬×高而不是寬×高×3這是最便宜的檢查手段。5.2 中心化用錯方差方向按行還是按列現(xiàn)象計算協(xié)方差時有人寫成np.cov(X, rowvarTrue)默認把每一行當成一個變量、每一列當成一個樣本結果特征臉完全混亂投影距離毫無規(guī)律。原因數(shù)據(jù)矩陣X是一行一張人臉行是樣本、列是特征。PCA需要按列中心化即每個像素位置減去該像素在所有圖片上的平均值。協(xié)方差應該描述像素與像素之間的相關性而不是圖片與圖片之間的相關性。解決直接用X - X.mean(axis0)不要用np.cov或者使用np.cov(X, rowvarFalse)。我一般用SVD替代協(xié)方差因為np.linalg.svd(X_centered, full_matricesFalse)自動把行當樣本、列當特征避免混淆。代碼里已經(jīng)這么寫但你要明白為什么。5.3 特征向量符號翻轉和排序問題現(xiàn)象同樣的數(shù)據(jù)兩次訓練得到的特征臉視覺方向相反比如一個向右的黑白漸變另一次向左。特征值排序不穩(wěn)定導致主成分順序改變。原因特征向量和奇異向量的符號是任意的np.linalg.svd和np.linalg.eigh都可能輸出符號相反的向量。這本身不影響投影后的距離因為投影系數(shù)也會變號但如果你手動排序時把特征值排序和特征向量排序解耦了就會張冠李戴。解決不要手動按特征值排序直接用Vt的順序SVD返回的奇異值本來就是降序。如果你用np.linalg.eigh注意它返回的特征值升序要反過來用eigenvalues, eigenvectors np.linalg.eigh(cov) idx np.argsort(eigenvalues)[::-1] eigenvalues eigenvalues[idx] eigenvectors eigenvectors[:, idx]邏輯說明eigh返回的是升序特征值argsort()[::-1]得到降序索引。這里eigenvectors是列向量索引時對第二維操作。如果忘記這一步取前K列相當于取了方差最小的K個方向識別率會差一截。5.4 數(shù)據(jù)量小于維度時協(xié)方差矩陣爆內存現(xiàn)象圖像尺寸128×128特征維度16384訓練集只有500張圖直接算協(xié)方差矩陣需要16384×16384×8字節(jié)約2GB內存機器直接卡死。原因協(xié)方差矩陣大小是特征維度×特征維度與樣本數(shù)無關。人臉圖像維度遠超樣本數(shù)直接分解協(xié)方差矩陣是典型的內存陷阱。解決使用SVD它只需要存儲(N×M)的數(shù)據(jù)矩陣(500×16384約65MB)和參與分解的矩陣。full_matricesFalse限制輸出尺寸避免生成M×M的完整U矩陣。如果樣本數(shù)N比特征維度M小很多還記得PCA的本質是N-1個子空間所以n_components最大只能取到N-1不需要去惦記那個巨大的協(xié)方差空間。5.5 先中心化再標準化的順序陷阱現(xiàn)象數(shù)據(jù)預處理時如果先對每個特征做標準化減去均值除以標準差再做PCA發(fā)現(xiàn)特征臉外觀完全不同識別率也曾好曾壞。原因PCA本身依賴方差先標準化會把所有像素方差強行拉到同一尺度。有人認為這樣可以避免大亮度區(qū)域主導但如果某個像素位置方差很小標準化后會把它放大成重要特征實際上是放大了噪聲。解決常規(guī)PCA人臉識別只做中心化不做按列標準化?;叶葰w一化是對每張圖做全局拉伸不是對每個像素做標準化。如果你想消除像素量綱影響可以測試標準化版本但要意識到它改變了主成分的意義——用標準化后的PCA和原始PCA識別結果需要重新調K和閾值。我一般保持中心化只在光照差異大的數(shù)據(jù)集上試驗標準化。6. 讓PCA人臉識別更實用自建門禁數(shù)據(jù)集的進階玩法6.1 訓練集擴充鏡像、平移與亮度擾動原始數(shù)據(jù)每人只有幾張照片識別率很容易波動。簡單有效的擴充方式是把每張訓練圖做左右翻轉、平移一到兩個像素、加一點高斯噪聲。這樣能把人臉姿態(tài)和微小對齊誤差的魯棒性喂給模型。但要注意測試集不要用同一張圖擴充后的樣本否則就是變相泄漏。6.2 識別閾值如何校準門禁類應用里陌生人拒識比熟人誤識更重要。我會先用訓練集自身的投影距離分布定一個初值統(tǒng)計每個訓練樣本與它同身份最近鄰的距離取95%分位數(shù)作為閾值下限。然后采集一批不在庫中的人臉圖片計算它們到最近鄰居的距離觀察兩者分布的重疊區(qū)域閾值取在重疊區(qū)靠陌生人一側壓低誤識率。6.3 和深度學習方案對比什么時候PCA夠用純PCA在光照劇烈變化、姿態(tài)大角度偏轉、遮擋面前準確率會被CNN方案甩開。但如果你的場景滿足三個條件——人臉正對、光線可控、庫內人員數(shù)量不大PCA依然是性價比最高的選擇訓練秒級完成模型只有幾個矩陣和均值臉內存占用不到1MB非常適合嵌入式門禁機、樹莓派這類資源緊張的環(huán)境。我自己的習慣是先拿PCA跑通全流程再根據(jù)瓶頸決定是否上深度學習。這條路徑能幫你快速理解特征提取、降維、分類和閾值怎么配合而這些經(jīng)驗在后來調任何識別模型都通用。實際部署時把均值臉、特征向量、模板投影存成.npy文件識別端只加載這些參數(shù)做矩陣乘法和距離計算。這樣訓練和識別分離代碼結構更清晰也避免每次啟動都要重新算PCA。希望這套實現(xiàn)可以成為你手里第一個能跑通的人臉識別系統(tǒng)也讓你在后續(xù)換用深度學習方案時知道自己在跟什么做對比。本文還有配套的精品資源點擊獲取