
简介面向MATLAB初学者、课程设计者与图像处理入门实践者的人脸识别GUI示例包围绕人脸检测、特征提取、分类识别与界面交互搭建完整演示流程可解决缺少可运行小型案例、难以把算法与图形界面串联起来的问题。压缩包共14个文件约60KB主要由4个.m脚本与函数文件、9张jpg人脸样本及测试图像、1个.p文件组成脚本承担主流程控制与识别逻辑图片用于训练库和测试库的人脸样本对照.p文件便于封装调用。已有71人学习下载适合按模块理解GUI按钮回调、样本组织与识别结果展示。整体目录分为训练样本、测试样本和算法脚本几部分结构紧凑便于在MATLAB环境中对照运行、修改参数并观察识别效果也能作为课程设计或小型项目原型参考帮助读者快速把握从人脸库准备到界面化识别的基本实现思路。1. 双击 .fig 就报错这套 MATLAB 人脸识别 GUI 到底卡在哪某实验室的旧工作站上一位同学拿到一份课程设计压缩包双击里面的 .fig 文件MATLAB 弹出一串红字界面出来了但按钮全是死的。这个场景几乎是每届新人复用 MATLAB 人脸识别项目的固定开场——代码是能跑的只是没人告诉你 fig 和 m 必须同名、工具箱要装齐、训练集路径要按相对路径来找。这份基于 MATLAB 的人脸识别系统 GUI 面板干的事情其实很朴素载入人脸库、训练特征空间、在界面上点几下就能识别一张人脸并给出相似度适合课程设计、实验课验收、以及需要把算法演示成“能点的软件”的场合。它不追求识别率打榜追求的是从图片到界面的一条完整闭环新手照着能跑通熟手能看懂 PCA 那一套是怎么被塞进回调函数里的。2. 项目骨架与技术选型PCA 特征脸加 GUIDE 面板的组合动手之前先把这份资源的结构想明白比直接双击文件靠谱得多。整套系统由三层组成最底下是图像与样本层每个子文件夹代表一个人里面放若干张人脸照片中间是算法层负责把图片变成向量、训练出特征空间、计算距离最上面是界面层用 GUIDE 做的 .fig 面板加上一堆 Callback 回调。三层之间靠一个 model 结构体传递数据训练完存成 .mat界面加载后放进 handles 里后续识别直接取用。这个分层不复杂但它是你能不能改动这套代码的关键——很多人翻车就翻在分不清“这是算法问题还是界面问题”。2.1 为什么选 PCA 而不是卷神经网络在这类项目里PCA主成分分析也叫特征脸 Eigenfaces几乎是默认答案原因不在算法先进而在它“可解释、可复现、不依赖额外工具箱”。整个训练过程就是一次协方差矩阵的特征分解MATLAB 的eig和svd直接能算学生能在报告里把每一步写清楚换成深度学习光是环境配置和预训练权重就能耗掉一半时间答辩时还容易被追问“这个网络你为什么这么设计”。特征脸的核心假设是所有人脸在像素空间里分布在一个低维子流形上用前 k 个主成分就能把这部分结构抓住剩下的当成噪声扔掉。代价也要说清楚。PCA 对光照、姿态、对齐极其敏感样本里一个人戴眼镜、另一个人不戴投影后的距离可能就翻倍它本质是全局线性降维遇到侧脸基本无解。所以这套系统在受控环境下正面、光照均匀、背景干净能跑到不错的准确率一挪到真实场景就露馅。我一般会跟人说这份代码的价值是把“人脸识别”拆成了看得见的矩阵运算而不是给你一个能上生产的人脸引擎。想提升效果后面的章节会讲 LBP、LDA 这些替换路线。下面这张表是这套项目里各技术点的实际定位方便你判断哪部分值得深挖、哪部分够用就行技术点在本项目中的角色替换难度备注PCA / 特征脸核心降维与匹配中算法层主战场欧氏距离 / 余弦相似度判定“像不像”低改一行即可切换GUIDE 面板交互与结果展示中也可迁到 App DesignerImage Processing Toolbox灰度化、缩放、直方图均衡不可替换rgb2gray、imresize依赖它摄像头采集实时识别输入源高依赖硬件与适配器2.2 目录结构与文件职责解压之后的目录大致是这样一套布局虽然具体文件名可能略有差异但职责划分基本一致。养成先看目录再看代码的习惯能省掉大量“函数找不到”的排查时间faceRecognitionGUI/ ├── main.m % 启动入口负责 addpath 和打开界面 ├── faceRecognition.fig % GUIDE 界面布局文件 ├── faceRecognition.m % 界面回调集合必须与 fig 同名 ├── core/ │ ├── trainPCA.m % 训练读样本、算均值、求特征向量 │ ├── recognizeFace.m % 识别投影 距离匹配 │ └── preprocessImg.m % 预处理灰度化、尺寸统一、直方图均衡 ├── dataset/ │ ├── person01/ % 一个人的所有样本 │ ├── person02/ │ └── ... ├── models/ │ └── pcaModel.mat % 训练产物界面启动时尝试加载 └── test/ └── sample01.jpg % 单张测试图几个容易忽略的点。main.m里通常有一句addpath(genpath(pwd))作用是把自己和所有子目录都加进搜索路径少了这句core里的函数在回调里就找不到。faceRecognition.fig和faceRecognition.m必须严格同名GUIDE 靠文件名把界面和回调绑在一起改名会让界面变成一堆死控件——这是新人最常见的血泪经验之一。dataset下的子文件夹名会被直接当作类别标签所以文件夹名就是识别结果里显示的文字想改显示名就改文件夹名不用动代码。models/pcaModel.mat是缓存删了不影响运行重新点一次训练就回来了。提示如果解压后的目录名里带空格或中文先把整个文件夹改成一个纯英文短名再操作。MATLAB 对中文路径的兼容性时好时坏尤其是uigetfile和dir组合使用时玄学问题多半出在这。2.3 环境准备与第一次启动环境这块不复杂但有三个硬条件必须满足缺一个就会在某个具体按钮上炸掉。第一步确认 MATLAB 版本。这类 GUIDE 项目通常是在 R2016b 到 R2020a 之间写的太老的版本R2014 之前可能用不了某些语法太新R2023 之后GUIDE 虽然还能打开但界面控件的默认样式变了个别属性名被弃用会有警告。R2018b 到 R2021b 是比较稳的区间。第二步确认工具箱。Image Processing Toolbox 是必需项rgb2gray、imresize、histeq、imshow全在里面Computer Vision Toolbox 只有在用摄像头实时采集时才需要纯图片识别可以不装。查工具箱的方法很简单在命令行敲% 查看已安装的工具箱确认这两个在列表里 ver % 单独检查某个函数是否可用返回 2 表示存在 exist(rgb2gray, file) exist(imresize, file)第三步是启动。把 MATLAB 的“当前文件夹”切到项目根目录不要用“打开 .fig 后按 F5”的方式因为那样工作目录可能不对dataset的相对路径就会失效。正确做法是在命令行执行% 切到项目根目录路径按你的实际位置改 cd(D:\work\faceRecognitionGUI); % 把所有子目录加入搜索路径 addpath(genpath(pwd)); % 运行入口脚本它会负责打开界面 main;genpath(pwd)会递归返回当前目录及所有子目录一次加完比手动 addpath 一堆文件夹省事。启动后界面上一般有“载入图像”“训练模型”“开始识别”“打开摄像头”几个按钮第一次用先别急着点识别进dataset看一眼样本够不够——每个人至少五张少于三张基本没法训练出有意义的特征空间。3. 核心算法拆解一张人脸从像素走到特征空间的完整链路算法层是这份代码真正值得读的部分它把 PCA 拆成了几个可以单独调试的函数而不是一大坨塞在回调里。整条链路是这样的读图 → 灰度化 → 统一尺寸 → 拉成列向量 → 减去均值 → 求协方差矩阵的特征向量 → 取前 k 个组成投影矩阵 → 把所有训练样本投影到低维空间存起来。识别时对新图做同样的预处理和投影再在低维空间里算距离最近的训练样本所属的类别就是结果。3.1 训练阶段从图片矩阵到特征脸空间训练函数是整个系统的地基它做的事情可以浓缩成一段代码。这段代码我在多个类似项目里见过写法大同小异关键在于矩阵的方向和维度要对齐——行是样本、列是特征还是反过来一旦搞混后面全是维度不匹配的报错function model trainPCA(imgDir, imgSize, k) % imgDir 训练集根目录每个子文件夹是一个人的名字 % imgSize 统一尺寸例如 [112 92]行在前列在后 % k 保留的主成分个数常用 20~80 persons dir(imgDir); persons persons([persons.isdir] ~startsWith({persons.name}, .)); X []; % 每列是一个拉直后的样本 labels {}; % 与列一一对应的类别标签 for i 1:numel(persons) name persons(i).name; files dir(fullfile(imgDir, name, *.jpg)); for j 1:numel(files) img imread(fullfile(imgDir, name, files(j).name)); % 三通道才转灰度已经是单通道就跳过避免报错 if size(img, 3) 3 img rgb2gray(img); end img imresize(img, imgSize); % 尺寸必须严格统一 img histeq(img); % 直方图均衡削弱光照差异 X [X, double(img(:))]; % 拉成列向量并转 double labels{end1} name; end end mu mean(X, 2); % 每个像素位置的平均值 Xc X - repmat(mu, 1, size(X,2)); % 中心化这一步不能省 % 样本数远小于维度时用 economy SVD比直接算协方差矩阵快得多 [U, S, ~] svd(Xc, econ); U U(:, 1:k); % 取前 k 个主成分也就是特征脸 proj U * Xc; % 所有训练样本的低维表示 model.U U; model.mu mu; model.proj proj; model.labels labels; model.imgSize imgSize; model.k k; end逐段拆开看。dir拿到子文件夹列表后用[persons.isdir]过滤出目录再用startsWith去掉.和..这一步在 Windows 上不写会多遍历两个无效目录。读图时先判断通道数因为有些样本可能已经被预处理成灰度图直接rgb2gray会报“输入必须是三通道”。imresize的第二个参数传一个[高 宽]向量所有样本必须严格一致差一个像素后面矩阵拼接就会失败。double(img(:))把图像拉成一列并转成双精度img(:)的展开顺序是列优先只要训练和识别用同一套就不影响结果。中心化那两行是 PCA 的核心不减均值的话第一主成分会变成“平均脸的亮度”信息量大但对方差贡献没意义。svd(Xc, econ)的 economy 模式只算需要的部分当样本数比如 200远小于像素维度比如 112×9210304时直接算Xc*Xc的 10304 阶协方差矩阵会卡到怀疑人生用 SVD 就快得多这是必须掌握的一个技巧。取前k列得到的就是所谓的特征脸矩阵U * Xc把所有训练样本压缩成 k 维坐标存在model.proj里识别时只需要在这个小矩阵里比距离。3.2 投影与匹配新图怎么被认出来识别函数短得多但坑一点不少尤其是预处理必须和训练阶段完全一致差一个histeq就会让距离分布整体偏移function [label, dist, score] recognizeFace(model, imgPath) % 返回识别出的类别、最近距离、归一化后的相似度 img imread(imgPath); if size(img, 3) 3 img rgb2gray(img); end img imresize(img, model.imgSize); % 与训练时同一套预处理 img histeq(img); x double(img(:)); xc x - model.mu; % 用训练集的均值中心化 p model.U * xc; % 投影到 k 维特征空间 % 计算与每个训练样本的欧氏距离 diff model.proj - repmat(p, 1, size(model.proj, 2)); dists sqrt(sum(diff.^2, 1)); [dist, idx] min(dists); label model.labels{idx}; % 相似度映射到 0~1仅用于界面展示不参与判定 score 1 / (1 dist); endmodel.U * xc这一行是投影矩阵乘法把 10304 维的图压成 k 维坐标。距离计算用repmat把新样本的坐标复制成和训练集一样多的列再逐元素相减求平方和开根号这是最直白的欧氏距离写法。min返回最小值和索引索引去labels里取类别名。相似度那行是给人看的1/(1dist)只是个单调映射别拿它当概率用——真正的置信度得靠距离阈值或者后面要讲的拒识机制。有个细节值得单独说model.mu必须是训练集的均值不能用新图自己的均值。有些实现图省事在识别时对新图单独中心化结果新图的坐标和训练集的坐标系根本不在一个原点距离全乱。这是 PCA 类项目最隐蔽的一类 bug因为代码不报错只是准确率莫名其妙地低。3.3 关键参数k 值和距离度量怎么调k是唯一一个真正影响效果的参数。太小k10特征空间表达能力不够不同人的脸挤在一起识别率低太大k150把噪声和光照变化也当成特征保留下来反而过拟合测试集上掉点。经验区间是样本总数的 30%~50%比如你有 200 张训练图k 取 60~100 比较合适。调的时候不要凭感觉写个小循环扫一遍画出准确率曲线% 扫描不同 k 值下的识别率找出拐点 kList [10 20 40 60 80 100 120]; acc zeros(size(kList)); for i 1:numel(kList) model trainPCA(dataset, [112 92], kList(i)); correct 0; for t 1:numel(testFiles) pred recognizeFace(model, testFiles{t}); correct correct strcmp(pred, testLabels{t}); end acc(i) correct / numel(testFiles); end plot(kList, acc, -o);距离度量也可以换。欧氏距离对整体亮度敏感余弦相似度只看方向不看长度在人脸这种“长得像不像”的问题上有时更稳。改法很简单把sqrt(sum(diff.^2,1))换成1 - (model.proj*p)./(vecnorm(model.proj)*norm(p))即可注意分母的维度对齐。两种度量没有绝对优劣样本光照差异大就试余弦样本质量均匀就欧氏跑一次对比实验比看十篇博客管用。4. GUI 回调层按钮点下去之后数据是怎么流动的界面层是这份资源区别于“一堆 .m 脚本”的地方也是二次开发时最容易改出问题的地方。GUIDE 生成的界面本质上是一个 fig 文件加一个同名 m 文件控件通过Tag属性被回调函数识别共享数据靠handles结构体传递。理解这套机制你才能在界面上加按钮、加显示框、加进度条而不是每次改动都靠猜。4.1 handles 结构与回调注册规则handles是一个贯穿整个界面生命周期的结构体里面装着所有控件的句柄也常被当作全局变量用——训练好的model、当前载入的图片路径、摄像头对象都可以挂上去。关键规则是在回调里修改了handles的字段后必须调用guidata(hObject, handles)写回否则下一个回调读到的还是旧值。这条规则不遵守的表现是“第一次点有用第二次点就没反应”非常典型的翻车现场。function btnLoad_Callback(hObject, eventdata, handles) % 载入图像按钮选一张图显示在坐标轴上路径存进 handles [file, path] uigetfile({*.jpg;*.png;*.bmp, 图像文件}); if isequal(file, 0) return; % 用户取消直接返回别往下走 end imgPath fullfile(path, file); img imread(imgPath); axes(handles.axesInput); % 把当前绘图目标切到指定坐标轴 imshow(img); title(待识别图像); handles.currentImgPath imgPath; % 挂到 handles 上 guidata(hObject, handles); % 必须写回否则下一个回调读不到 enduigetfile取消时返回 0不判断就会拿 0 去拼路径报错信息还很难懂。axes(handles.axesInput)指定绘图目标是 GUIDE 里显示图片的标准姿势少了这句imshow会画到当前活动 figure 上可能是另一个窗口。guidata那一行是全部回调的地基我见过太多人在这里栽跟头调试半天以为是识别算法坏了其实是数据根本没存进去。4.2 训练与识别两个主回调的写法训练回调的逻辑是“读路径 → 调算法 → 存模型 → 给反馈”看起来简单但训练耗时可能几十秒不加提示用户会以为卡死function btnTrain_Callback(hObject, eventdata, handles) k str2double(get(handles.editK, String)); % 从编辑框读 k if isnan(k) || k 5 errordlg(主成分个数请填 5 以上的整数, 参数错误); return; end set(handles.textStatus, String, 正在训练请稍候...); drawnow; % 强制刷新界面 tic; model trainPCA(dataset, [112 92], k); elapsed toc; handles.model model; guidata(hObject, handles); set(handles.textStatus, String, sprintf(训练完成%d 类 %d 样本耗时 %.1fs, ... numel(unique(model.labels)), numel(model.labels), elapsed)); endget(handles.editK, String)拿到的是字符串必须str2double转数值编辑框里填了字母就会返回 NaN这个判断不能省。drawnow是让界面在长任务开始前先刷新一次否则状态文字要等训练结束才显示用户体感就是“点了没反应”。numel(unique(model.labels))统计类别数比numel(model.labels)更能说明训练集规模。识别回调则要处理“模型没训练就点识别”和“图片没载入就点识别”两种异常状态这是界面健壮性的基本要求function btnRecognize_Callback(hObject, eventdata, handles) if ~isfield(handles, model) || isempty(handles.model) errordlg(请先点击训练模型, 提示); return; end if ~isfield(handles, currentImgPath) errordlg(请先载入一张待识别图像, 提示); return; end [label, dist, score] recognizeFace(handles.model, handles.currentImgPath); set(handles.textResult, String, sprintf(识别结果%s, label)); set(handles.textScore, String, sprintf(距离 %.2f 相似度 %.3f, dist, score)); endisfield那两行是防御性编程比try-catch更清晰能给出明确的用户提示。识别结果同时展示距离和相似度是因为单独一个“张三”没有说服力距离数值能让人判断这次匹配靠不靠谱——距离明显偏大时八成是输入图和训练集差异太大这时候显示的名字反而会误导。4.3 摄像头实时识别链路实时识别是这类项目里最有演示效果的部分原理也不难开摄像头 → 抓一帧 → 预处理 → 调识别函数 → 显示。麻烦的地方在硬件适配和帧循环的控制写不好会卡死界面function btnCamera_Callback(hObject, eventdata, handles) % 先探测可用的摄像头设备 info imaqhwinfo; if isempty(info.InstalledAdaptors) errordlg(未检测到图像采集适配器, 提示); return; end vid videoinput(winvideo, 1, RGB24_640x480); set(vid, FramesPerTrigger, 1); start(vid); handles.vid vid; guidata(hObject, handles); while isfield(handles, cameraRunning) handles.cameraRunning frame getsnapshot(vid); axes(handles.axesInput); imshow(frame); % 把当前帧临时写到磁盘或用内存变量传给识别函数 tmpPath fullfile(tempdir, frame.jpg); imwrite(frame, tmpPath); [label, dist] recognizeFace(handles.model, tmpPath); set(handles.textResult, String, sprintf(%s (%.1f), label, dist)); drawnow; end endimaqhwinfo先查适配器没有就直接提示避免在videoinput那行抛出难懂的底层错误。RGB24_640x480是分辨率格式串不同摄像头支持的格式不一样写死可能报错稳妥做法是先imaqhwinfo(winvideo)看SupportedFormats再决定。帧循环里每帧都imwrite到临时文件再读回来效率不高但胜在简单可靠因为recognizeFace现在只接受路径参数——想优化就把识别函数改成能直接接收矩阵省掉一次磁盘往返帧率能提升不少。注意while循环里没有退出条件的话界面会彻底失去响应。常见做法是在循环里检查一个标志位用户点“停止”时把它置 false或者干脆改成timer对象定时触发把控制权交还界面。5. 避坑与排查跑不起来时按这个顺序查这类项目的报错信息普遍不友好Matrix dimensions must agree能出现在十个不同的地方。下面这几条是复现过程中出现频率最高的按“现象 → 原因 → 解决”记下来能省掉大半调试时间。5.1 现象训练时报维度不一致或者直接内存溢出训练刚开始就抛出Dimensions of arrays being concatenated are not consistent或者跑到一半 MATLAB 卡死、内存条飙红。原因基本是两个。维度不一致是样本尺寸没统一dataset里混了不同分辨率的照片imresize如果写在某条分支里被跳过拼接时矩阵宽度就对不上也可能是混进了灰度图和彩色图三通道的imresize结果和单通道维度不同。内存溢出则是样本量不大但傻算了大协方差矩阵10304×10304 的double矩阵接近 800MB几轮循环就撑不住。解决上训练前先加一段尺寸与通道的强制归一% 训练前扫描样本把不规范的文件挑出来 badFiles {}; files dir(fullfile(imgDir, **, *.jpg)); for i 1:numel(files) info imfinfo(fullfile(files(i).folder, files(i).name)); if info.Width 60 || info.Height 60 badFiles{end1} files(i).name; end end if ~isempty(badFiles) warning(以下文件过小建议剔除%s, strjoin(badFiles, , )); end内存问题则把svd(Xc, econ)用上不要写[V, D] eig(Xc * Xc)去硬算万阶矩阵。样本数小于维度时那条路径的复杂度完全是浪费。5.2 现象特征脸显示出来一片漆黑或者纯白想把model.U里的前几个特征向量还原成图片看一眼效果imshow出来要么全黑要么一片惨白什么都看不出来。原因是数据范围。imshow对double类型的输入默认按[0, 1]解释而特征向量的值有正有负通常在 -0.05 到 0.05 之间全落在 0 以下就被截断成黑色。这不是算法错了是显示方式错了。解决办法是先线性拉伸到[0,1]再显示或者干脆用mat2gray自动归一化eigenface reshape(model.U(:, 1), model.imgSize); % 还原成二维 eigenface mat2gray(eigenface); % 自动映射到 0~1 figure; imshow(eigenface); title(第 1 主成分特征脸);mat2gray按最小最大值线性映射是最省事的一种。想看得更清楚可以叠一次histeq但要注意这只是为了肉眼观察别把这个结果拿回去参与计算否则坐标系又乱了。5.3 现象换了台电脑界面能打开但按钮全报错在自己的机器上跑得好好的拷给别人或者换台机器双击 .fig 界面正常显示一点按钮就是Undefined function trainPCA或者Unrecognized function or variable。原因有两个层面。浅层是搜索路径没设置core目录不在path里回调里调用的函数自然找不到。深层是 fig 和 m 文件的绑定关系断了——如果有人把faceRecognition.m改成了faceRecognition_v2.mfig 打开时找的还是原来的名字回调就全部失效。解决方式是启动时统一走入口脚本把路径一次性加全% main.m 里应该有的两句 rootDir fileparts(mfilename(fullpath)); % 拿到本文件所在目录不依赖当前路径 addpath(genpath(rootDir));mfilename(fullpath)比pwd可靠因为用户未必会把当前文件夹切到项目根目录。至于文件名问题动手改名前先确认 fig 和 m 是否同名改就两个一起改别只改一个。5.4 现象识别率低得离谱十张错八张模型能训练界面也能出结果但准确率明显不正常随便换张图就认错人或者无论输入什么永远返回同一个名字。最常见的原因出在识别阶段的预处理和训练阶段不一致。训练时做了histeq识别时漏了训练时用[112 92]识别时用了[92 112]imresize的行列顺序搞反或者训练用rgb2gray转灰度识别时忘了判断通道数直接把三通道矩阵拉成一列维度直接对不上。另一种情况是数据本身有问题每个人只有两三张样本且光照差异极大PCA 学到的主成分全是光照方向。排查办法是先做自检拿训练集里的图去识别如果连训练样本都认不出来说明是代码链路问题不是泛化问题。自检通过后再看测试集准确率还是低就考虑加样本、做直方图均衡、或者把k调小避免过拟合。另外提醒一句min(dists)永远会返回一个最近邻哪怕输入的是个苹果它也会告诉你这是某个人——这是后文要加拒识机制的原因。5.5 现象摄像头按钮一点就报适配器错误点“打开摄像头”报The ADAPTORNAME specified is not a valid adaptor或者Unable to find any adaptors。原因是 Image Acquisition Toolbox 没装或者摄像头驱动不被识别。MATLAB 在 Windows 上主要靠winvideo适配器某些 USB 摄像头、虚拟摄像头不支持笔记本内置摄像头有时也会因为驱动版本问题识别不到。先做诊断再动手改代码% 查看系统里有哪些适配器以及每个适配器下的设备 info imaqhwinfo; disp(info.InstalledAdaptors); if ~isempty(info.InstalledAdaptors) dev imaqhwinfo(winvideo); disp(dev.DeviceInfo); % 设备 ID、支持的格式都在这 end如果InstalledAdaptors是空的先确认工具箱装了没ver里找 Image Acquisition Toolbox有适配器但设备列表为空多半是驱动问题换一个 USB 口或者用系统相机软件先验证摄像头本身是否正常。格式串也要按SupportedFormats里列出来的写RGB24_640x480只是最常见的一种不是通用值。6. 二次开发给这套系统加一个“不认识”的选项原始代码有个绕不开的缺陷它永远会给出一个答案哪怕站在镜头前的是个完全陌生的人。这在演示时很尴尬也是最值得动手改的地方。我的做法是加一层距离阈值拒识——先算出一个合理的阈值超过就输出“未知”。阈值不能拍脑袋用训练集内部的类间距离分布来估把每个训练样本轮流留出算它与同类的距离和与异类的距离取两者分界处作为参考。% 用训练集自测估算一个可用的拒识阈值 n size(model.proj, 2); intra []; % 同类距离 inter []; % 异类距离 for i 1:n d sqrt(sum((model.proj - model.proj(:, i)).^2, 1)); same strcmp(model.labels, model.labels{i}); intra [intra, d(same d 0)]; inter [inter, d(~same)]; end threshold 0.5 * (max(intra) min(inter)); % 取两类分布的中间值拿到阈值后在识别回调里做一次判断dist threshold就把label改成“未知”界面上用不同颜色标出来。这一步改动不到二十行但系统从“玩具”变成了“有判断力的玩具”答辩时也是个能讲的点。继续往下走的话特征提取那层是最值得换的。LBP 对光照的鲁棒性明显好于原始像素把preprocessImg换成 LBP 特征再走同一套 PCA 流程受控环境下准确率能提一截PCALDA 的组合则通过引入类标签信息让同类样本在投影后更聚集。再往上是把 GUI 从 GUIDE 迁到 App Designer控件更现代、布局自适应但迁移成本不低handles那一套要改成面向对象的属性和回调图省事的话不建议动。改造方向改动位置工作量收益距离阈值拒识识别回调小避免误认陌生人换 LBP 特征preprocessImg中抗光照提升明显PCA LDAtrainPCA中同类更聚集多人脸检测识别前加检测大支持画面多人迁到 App Designer整个界面层大界面现代化顺带说一个验证习惯每次改完算法别只看界面上的结果对不对把混淆矩阵打出来看一眼哪两类容易混、混的方向是不是单向的比一个总准确率数字有用得多。confusionmat两行就能出能省掉很多“为什么这个人和那个人总分不清”的猜测。从那以后我每次接手这类 MATLAB GUI 项目第一件事都是把 fig 和 m 的文件名对齐、把所有路径改成基于mfilename(fullpath)的相对路径、然后拿训练集里的图跑一遍自检三步走完再谈效果优化。这套流程看着笨但能挡掉八成的“跑不起来”。希望帮到你。本文还有配套的精品资源点击获取