基于Dlib+TensorFlow+PyQt5的智能口红色号检测推荐系统

发布时间:2026/10/14 22:40:42
基于Dlib+TensorFlow+PyQt5的智能口红色号检测推荐系统
简介这套系统是基于Dlib人脸关键点检测、PyQt5界面库与TensorFlow深度学习框架共同实现的智能口红色号检测与推荐应用完整提供Python工程源码、训练好的模型和配套数据集专门面向计算机视觉学习者、美妆电商开发者以及需要快速搭建推荐原型的算法工程师。系统借助人脸68点关键点定位唇部区域再对唇色进行提取与色彩空间转换并与色号库比对后输出最接近的口红推荐结果整体思路清晰便于二次开发。压缩包采用rar格式共84个文件主要以Python脚本、图像样本、配置文件、文本文档和参考论文等形式存在其中脚本负责人脸识别、蒙版生成与界面逻辑图像用于测试与展示配置和数据文件记录色号库信息全部内容仅3.03MB轻量便携。目前已有467人学习下载。资源完整覆盖从数据预处理、人脸检测、唇部轮廓提取、唇色匹配到程序界面搭建的流程同时附带训练模型与数据集可直接在配置好依赖环境后运行体验还能结合项目博客进一步理解算法细节可作为购物推荐、虚拟试妆等场景的产品原型进行扩展。1. 智能口红检测推荐把“试色”需求落地成可复现的深度学习工程如果你做过美妆类的工具或者给电商项目搭过推荐服务一定听过这种需求用户上传一张自拍系统告诉她“这支口红是什么色号”“这个颜色适不适合你”。听起来像调色板加个分类器但真正接数据就会发现口红色号检测牵扯人脸关键点、嘴唇区域提取、颜色空间转换、模型推理和推荐匹配任何一个环节都容易翻车。基于DlibPyQt5TensorFlow的智能口红色号检测推荐系统就是把这套完整链路打包成可复现代码的工程自带Python源码、训练模型和数据集。拆完这份资源后我觉得它最值钱的地方不是某个模型有多准而是把“如何从一张人脸图走到最终推荐结果”的工程路径讲清楚了。适合正在做深度学习落地、想上手桌面端AI应用开发的初学者也适合需要一套可二次开发的检测推荐基座的从业者。2. 系统设计Dlib做关键点、TensorFlow做识别、PyQt5做壳的职责划分在动手改代码之前我建议先理解这个项目为什么是“三件套”而不是一个端到端的大模型。口红色号检测的第一难点是模型得知道“该看脸上哪个位置”。直接用整张图分类背景、头发、肤色都会干扰模型训练数据量不够的话模型学到的是“图里有没有嘴唇”而不是“嘴唇是什么色”。所以合理分工是Dlib负责定位和裁剪把高概率的嘴唇区域喂给TensorFlow模型模型只负责做色号分类PyQt5负责把推理结果、推荐列表和交互逻辑串起来。这个分离思路会在后面每一步省下大量调试时间。2.1 整体流程从人脸关键点到色号推荐整个系统的推理链路可以拆成六个环节。第一步通过本地图片或摄像头拿到原始图像第二步用Dlib的人脸检测器找到人脸框第三步对人脸区域提取68个关键点取其中属于嘴唇的4867号点围出嘴部ROI第四步对ROI做预处理包括尺寸缩放、颜色空间转换和去高光第五步把处理好的嘴唇图像送入TensorFlow分类模型输出各色号类别的概率第六步结合用户肤色特征对概率做加权排序生成推荐列表最后渲染到PyQt5界面。这里有一个容易被忽略的细节Dlib给出的68点坐标是相对于人脸框的不同版本detector输出的坐标系并不完全一致。我一般会在拿到人脸框后先对坐标做一次相对位置的归一化再映射回原图坐标避免后续ROI裁剪时出现偏移。资源里如果看到dlib.full_object_detection的相关代码先确认坐标基准。推荐部分不是简单取概率最大值而是把肤色作为一个偏置项。常见做法是检测脸颊或额头区域的肤色均值映射到Lab空间计算它与每个色号的色差距离再把分类概率和色差距离做加权融合。这样某个色号就算分类概率不是第一只要肤色匹配度高也可能排到推荐列表前面。2.2 技术选型理由为什么是这个组合而不是其他方案模块技术职责选型理由人脸与关键点Dlib人脸检测、嘴唇关键点提取CPU推理快68点模型成熟离线可用色号识别TensorFlow嘴唇图像分类生态完善模型导出SavedModel方便部署界面与交互PyQt5图片预览、结果展示、摄像头采集Python桌面端最稳的选择信号槽机制适合异步刷新推荐逻辑Python规则融合色号排序与匹配不依赖额外框架逻辑透明可调有人可能会问为什么不用OpenCV的级联分类器做人脸检测我试过正脸效果尚可但侧脸和低头场景召回率明显下降而且OpenCV自带的人脸检测器不输出关键点还需要再额外接一个关键点模型。Dlib的HOG线性分类器检测器加上shape predictor在普通笔记本CPU上单帧推理大约3060毫秒放在桌面应用里足够实时。关键点定位和嘴唇裁剪一次完成代码路径更短。也有人问色号识别为什么不用目标检测模型比如YOLO因为唇部在整张图中的尺寸很小目标框很难标得准而且口红检测本质是“裁剪后分类”问题不是“多个目标定位”问题。除非你要同时检测口红管体、试色卡和嘴唇否则引入检测模型只会增加标注成本和推理开销。这个项目的粒度选择是合理的。2.3 工程目录与资源组织方式拆开资源包以后第一件事是看目录结构。典型的组织方式应该是这样的lipstick-recommend/ ├── data/ │ ├── train/ │ │ ├── 00_classic_red/ │ │ ├── 01_coral/ │ │ └── ... │ ├── val/ │ └── test/ ├── models/ │ ├── shape_predictor_68_face_landmarks.dat │ ├── mobilenetv2_lipstick.h5 │ └── label_map.json ├── src/ │ ├── preprocess.py │ ├── train.py │ ├── inference.py │ └── ui_main.py ├── requirements.txt └── README.mddata/train下面按色号文件夹组织数据每个文件夹名就是类别标签models里同时放了Dlib关键点模型和TensorFlow训练好的权重文件src下四个脚本分别对应数据处理、训练、推理和界面启动。如果缺少某个文件夹多半是压缩包解压层数不对检查外层嵌套目录。requirements.txt里通常会固定几个关键库的版本我自己装环境时会格外注意Dlib的安装方式。Dlib在Windows下用pip install dlib很容易遇到CMake编译报错建议直接用预编译的wheel包或者换到Linux环境跑。PyQt5体积较大安装时建议用清华源加速避免网络超时。3. 数据集准备与预处理从图片到“嘴唇样本”的5个步骤很多人在这个项目上翻车不是模型结构写错而是数据准备阶段太草率。口红检测的特殊性在于模型学习的不是嘴唇的纹理或形状而是颜色分布。同样的色号在不同光照、不同肤色、不同唇色下面拍出来差异极大。所以数据清洗和增强的优先级比调网络结构高得多。3.1 数据目录与标签格式拿到资源后先检查数据集的标签体系。如果train目录下每个子文件夹是一个色号那模型训练时可以直接用ImageDataGenerator的flow_from_directory读入文件夹名自动成为类别名。还有一个常见格式是CSV标注两列第一列图片路径第二列色号名称或编号。我偏好目录式组织因为它不用单独维护标签文件删图加图都方便也不会出现路径写错导致标签错位。无论哪种格式第一步都是建数据清单。以下脚本可以帮你把原始图片整理成统一的训练目录并打印类别分布防止某几个色号图片数量严重失衡import os import shutil from collections import Counter src_root raw_images dst_root data/train for root, _, files in os.walk(src_root): for name in files: if not name.lower().endswith((.jpg, .jpeg, .png)): continue label os.path.basename(root) os.makedirs(os.path.join(dst_root, label), exist_okTrue) shutil.copy(os.path.join(root, name), os.path.join(dst_root, label, name)) counts Counter( os.path.basename(os.path.dirname(f)) for f in [_ for _, _, fs in os.walk(dst_root) for _ in fs] ) print(counts)这段逻辑的核心就是“把当前目录名当作标签把图片文件按标签归位”。它只做移动和复制不修改图片像素因此不对数据质量负责。如果raw_images里有大量网图建议先人工抽检每个类别至少20张图确认没有放错文件夹。Counter打印的分布信息很关键某个类别如果少于50张后续训练就要给它更高的增强权重否则模型会直接忽略这个类。3.2 嘴唇区域裁剪从Dlib关键点到ROI标签就绪之后下一步是把每张人脸图变成“只有嘴唇”的小图。这里用Dlib的68点模型嘴唇区域由点48到点67围成。裁剪逻辑不能直接取最小包围盒因为嘴唇边缘的皮肤会混进来也不能只取内唇线因为外沿颜色过渡也是判断色号的重要依据。我一般会在最小包围盒基础上外扩2030像素把下嘴唇阴影和嘴角边缘都包含进来。import dlib import cv2 import numpy as np detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(models/shape_predictor_68_face_landmarks.dat) def crop_lip_roi(image_path, expand_ratio0.25): img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) if len(faces) 0: return None shape predictor(gray, faces[0]) lip_points [] for i in range(48, 68): pt shape.part(i) lip_points.append((pt.x, pt.y)) xs [p[0] for p in lip_points] ys [p[1] for p in lip_points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) w x_max - x_min h y_max - y_min pad_x int(w * expand_ratio) pad_y int(h * expand_ratio) x_min max(0, x_min - pad_x) y_min max(0, y_min - pad_y) x_max min(img.shape[1], x_max pad_x) y_max min(img.shape[0], y_max pad_y) roi img[y_min:y_max, x_min:x_max] return roi roi crop_lip_roi(sample.jpg) if roi is not None: cv2.imwrite(lip_roi.jpg, roi)代码里有几个参数值得调。expand_ratio控制外扩比例0.20.35之间比较合理外扩太小会让模型学到僵硬的边缘外扩太大则会混入下巴阴影。detector(gray, 0)里的0表示不执行图像上采样检测想提高侧脸召回可以改成1但推理时间会翻倍。最关键的是faces[0]只取检测到的第一张脸处理合影时会把不该出现的人脸当成目标。我处理多人图时会先按人脸框面积排序取最大的那张因为自拍场景里用户面部通常占据最大区域。3.3 数据增强与颜色扰动光照变化是口红识别的最大敌人。同一个色号在室内暖光和室外冷光下拍出来的RGB值可能相差30%以上。数据增强至少要覆盖三方面水平翻转嘴唇不对称发生率低但存在、亮度和饱和度扰动、以及轻微的颜色偏移。以下增强策略是常见方案import tensorflow as tf def augment(image): image tf.image.random_flip_left_right(image) image tf.image.random_brightness(image, max_delta0.15) image tf.image.random_saturation(image, lower0.6, upper1.4) image tf.image.random_hue(image, max_delta0.02) image tf.clip_by_value(image, 0.0, 1.0) return image增强操作应该在GPU上执行可以放在tf.data.Dataset.map里这样不会拖慢CPU端的图片解码。random_hue的max_delta一定要设得很小因为色调偏移0.05以上就会让口红颜色从正红偏到玫红。random_brightness的max_delta0.15是经验值更大值会让暗光样本直接糊掉。做这套增强的目的不是让模型见过所有光线而是逼着模型学会忽略光照差异、专注色调本身。4. 模型训练与调参TensorFlow分类模型训练与结果评估图片裁剪好之后模型部分就可以开始了。我拆过的资源里做法最稳的并不是从零训练一个几十层的CNN而是用预训练模型做迁移学习。原因很简单色号分类本质上是对颜色特征做精细划分底层卷积核提取的边缘、纹理、颜色渐变特征在ImageNet上已经学得足够好我们只需要替换分类头在嘴唇数据集上微调即可。4.1 模型结构MobileNetV2作为骨干网络MobileNetV2在CPU和GPU上都能跑模型体积小非常适合嵌入到PyQt5桌面应用。这里把预训练模型分成两步先冻结全部卷积层只训练分类头待分类头收敛后再解冻部分高层卷积做微调。这个策略可以避免一开始就用大学习率把预训练权重冲坏。import tensorflow as tf from tensorflow.keras import layers, Model base_model tf.keras.applications.MobileNetV2( input_shape(224, 224, 3), include_topFalse, weightsimagenet ) base_model.trainable False inputs tf.keras.Input(shape(224, 224, 3)) x tf.keras.applications.mobilenet_v2.preprocess_input(inputs) x base_model(x, trainingFalse) x layers.GlobalAveragePooling2D()(x) x layers.Dropout(0.3)(x) outputs layers.Dense(num_classes, activationsoftmax)(x) model Model(inputs, outputs) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy] ) model.summary().preprocess_input这一步很多人会漏MobileNetV2的预训练权重需要输入在[-1,1]区间的RGB图如果不做这个归一化收敛速度会明显变慢。Dropout(0.3)设在分类头之前对防止色号类别间的过拟合很有用尤其是当某些色号只有百来张图片时。base_model.trainable False保证第一阶段只更新Dense层。注意base_model(x, trainingFalse)里必须显式传trainingFalse否则BatchNorm层在冻结状态下也可能被错误更新。4.2 训练配置数据管道与回调训练阶段我通常用tf.data而不是ImageDataGenerator因为前者能利用并行解码也让增强逻辑更透明。图片统一缩放到224x224batch size设成32。如果CPU核数多num_parallel_calls可以调大如果是GPU训练batch size64也常见但必须保证显存不溢出。def parse_function(path, label): img tf.io.read_file(path) img tf.image.decode_jpeg(img, channels3) img tf.image.resize(img, (224, 224)) img tf.cast(img, tf.float32) / 255.0 return img, label train_ds tf.data.Dataset.from_tensor_slices((train_paths, train_labels)) train_ds train_ds.map(parse_function, num_parallel_callstf.data.AUTOTUNE) train_ds train_ds.map(augment, num_parallel_callstf.data.AUTOTUNE) train_ds train_ds.batch(32).prefetch(tf.data.AUTOTUNE) callbacks [ tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience3 ), tf.keras.callbacks.EarlyStopping( monitorval_loss, patience6, restore_best_weightsTrue ) ] history model.fit( train_ds, validation_dataval_ds, epochs30, callbackscallbacks )ReduceLROnPlateau的factor0.5表示验证损失不再下降时学习率减半patience3给模型3个epoch的机会寻找更优方向。EarlyStopping的patience6是总训练轮数的延伸避免在验证集上反复震荡浪费时间。训练完第一阶段的冻结模型后我会解冻base_model的最后50层用初始学习率1e-5继续训练10个epoch。这个微调阶段非常敏感如果学习率超过1e-4预训练权重会被迅速破坏准确率不升反降。4.3 评估与导出的关键操作训练结束后不能只看accuracy一个指标。口红色号类别之间相似度高比如正红和深红模型可能把80%的“正红”样本判成“深红”但平均准确率看起来仍然很高。这时要按类别看混淆矩阵确认每个色号的召回率差异。from sklearn.metrics import classification_report, confusion_matrix import numpy as np y_true [] y_pred [] for images, labels in test_ds: probs model.predict(images) y_pred.extend(np.argmax(probs, axis1)) y_true.extend(np.argmax(labels.numpy(), axis1)) print(classification_report(y_true, y_pred, target_namesclass_names)) model.save(models/lipstick_model.h5)classification_report会输出每个色号的precision、recall和f1-score我一般会重点看recall最低的三个类别它们往往就是训练图片最少或颜色最接近其他类的样本。保存模型用.h5或SavedModel都行PyQt5端加载时用tf.keras.models.load_model注意重载后要执行一次predict做热身否则第一帧推理会慢到几百毫秒。5. 避坑指南训练和部署中的常见问题排查这一章纯粹是血泪经验汇总。我把拆这个项目时碰到过的四个高频问题按“现象→原因→解决”的方式列出来希望你能少踩几个坑。5.1 Dlib检测不到侧脸或关键点错位现象用户上传一张侧脸或低头自拍Dlib返回的人脸框完全偏到耳朵位置或者关键点中嘴唇坐标落在下巴上导致裁剪出的ROI不是嘴唇而是皮肤。原因Dlib的HOG人脸检测器对正面和轻微左右转脸效果稳定但超过30度侧转时特征减弱检测框容易漂移shape predictor在检测框不准的情况下会输出看似合理但实际错误的关键点。解决在调用predictor之前做人脸框的置信度校验检测框宽高比超出合理范围就判定为失败。更稳妥的是做两次检测第一次原图检测若失败把图像旋转15度再检测一次取两次中面积最大的人脸框。对关键点结果增加几何约束——嘴唇点集的重心必须落在检测框下半部分否则认定为错位并丢弃样本。5.2 验证集准确率很高但真实图片识别稀烂现象训练时验证准确率超过90%把一个在训练集中从没见过的实拍图丢进去预测结果完全跑偏甚至输出概率不超过0.3。原因数据预处理流程不一致。很可能是训练阶段做了亮度归一化或增强但推理阶段只做了简单的resize或者训练时用的是裁剪后的嘴唇ROI而推理时直接喂了整张人脸图。模型见过的是224x224的唇部特写输入分布变了它的特征提取器完全无法对齐。解决把crop_lip_roi和预处理的逻辑封装成同一个函数训练脚本和推理脚本共用。我的习惯是导出一个preprocess_pipeline.py里面只暴露preprocess_for_model(image_path)一个入口这样训练和推理永远走同一条路径。每次部署前挑10张真实场景图片跑一遍端到端流程图对比中间裁剪结果和训练样本是否视觉一致。5.3 PyQt5界面点按钮后无响应窗口转圈现象点击“开始识别”按钮界面卡住标题栏显示“未响应”几秒后才恢复但摄像头画面已经变成黑屏。原因PyQt5的UI线程被推理任务阻塞。TensorFlow模型加载和预测都是耗时操作如果在主线程执行UI消息循环无法处理重绘事件就会出现假死。尤其是首次加载模型需要几十秒相当于整个窗口冻结。解决把模型加载、人脸检测、推理全部放到QThread工作线程主线程只负责显示结果。用一个信号槽把推理结果传回UI。示例逻辑是把run_inference()放在线程的run()方法里用pyqtSignal(dict)发回色号、置信度和推荐列表。摄像头画面读取也应该独立成线程否则画面帧率会被推理拖垮。5.4 同一张口红在不同灯光下预测结果不一致现象同一张嘴唇区域图片在暖光下预测为“珊瑚色”在冷光下预测为“豆沙色”模型的softmax概率在两个类别之间摇摆。原因模型没有真正学到口红本身的颜色属性而是学到了“暖光下RGB值偏高”这类环境特征。根本原因是训练数据中没有做足够的颜色空间规范化或者光照归一化策略不够激进。解决在预处理阶段引入颜色校正把嘴唇区域转成CIELAB色彩空间对L通道做直方图拉伸或标准化让亮度变化不再主导模型判断。我当时采用了另一个更直接的方案对所有训练图片同步生成多份“光照扰动版”每份分别提高和降低色温然后一起送入训练相当于逼模型在不同色温下输出同样标签。推荐系统侧也可以加一个软化逻辑如果前两个类别概率差值小于0.1就把两个都放进推荐列表让用户自行选择比硬压一个结果更实用。6. 进阶用PyQt5把模型包装成人人敢用的桌面工具模型训练完只是起点真正让这个项目产生价值的是可靠、可交互的落地界面。PyQt5部分我建议至少做到三件事独立工作线程跑推理、图片与摄像头双输入、推荐结果带置信度展示。摄像头实时检测看起来酷但用户体验关键在“响应速度”和“结果稳定性”。我曾用QTimer每30毫秒抓一帧画面但发现推理线程还没算完上一帧新帧又来了导致界面结果跳变很厉害。后来改成当检测线程空闲时才抓取最新帧保证显示的结果是“此时此刻”的推断。验证一个桌面端AI应用是否成熟我会强制走一遍“冷启动测试”第一次打开软件不加载任何缓存记录从点击按钮到看到首帧结果的时间。很多demo在这儿就会露馅因为模型初始化、Dlib关键点模型加载、PyQt5控件渲染全部堆在启动流程里。我的习惯是把模型加载做成懒加载首次点击识别时才创建推理线程界面先用一张默认占位图告诉用户“正在准备引擎”。这听起来是小事但体验差距巨大。推荐结果展示上别只给一行文字“正红色”加上色号名称、参考色卡、匹配度百分比和几条配色建议。原项目里推荐列表的权重参数通常写在配置文件中我一般会把weight_color_distance0.4, weight_probability0.6开放到界面的“高级设置”里方便测试不同参数组合下的推荐效果。颜色距离采用CIEDE2000色差公式比欧氏距离更接近人眼感知这也是我踩过雷之后换过去的做法。最后我想说这个项目给我的最大教训是不要把“模型训练准确率”当作项目成功的唯一指标。训练阶段花一周优化网络结构不如花一晚上梳理预处理、统一训练推理路径、把界面线程理清楚。从那以后我每次拿到类似的检测推荐类资源都强制走一遍端到端验证流程——从原始图片到最终UI展示逐个环节看中间产物是否符合预期再决定要不要优化模型。希望这个拆解过程和避坑记录能帮你在复现时省下几个晚上少经历几次“玄学报错”。本文还有配套的精品资源点击获取