☰
基于Dlib+TensorFlow+PyQt5的智能口红色号检测推荐系统
2026/10/14 22:40:33 网站建设 项目流程

简介:这套系统是基于Dlib人脸关键点检测、PyQt5界面库与TensorFlow深度学习框架共同实现的智能口红色号检测与推荐应用,完整提供Python工程源码、训练好的模型和配套数据集,专门面向计算机视觉学习者、美妆电商开发者以及需要快速搭建推荐原型的算法工程师。系统借助人脸68点关键点定位唇部区域,再对唇色进行提取与色彩空间转换,并与色号库比对后输出最接近的口红推荐结果,整体思路清晰,便于二次开发。压缩包采用rar格式,共84个文件,主要以Python脚本、图像样本、配置文件、文本文档和参考论文等形式存在,其中脚本负责人脸识别、蒙版生成与界面逻辑,图像用于测试与展示,配置和数据文件记录色号库信息,全部内容仅3.03MB,轻量便携。目前已有467人学习下载。资源完整覆盖从数据预处理、人脸检测、唇部轮廓提取、唇色匹配到程序界面搭建的流程,同时附带训练模型与数据集,可直接在配置好依赖环境后运行体验,还能结合项目博客进一步理解算法细节,可作为购物推荐、虚拟试妆等场景的产品原型进行扩展。

1. 智能口红检测推荐:把“试色”需求落地成可复现的深度学习工程

如果你做过美妆类的工具或者给电商项目搭过推荐服务,一定听过这种需求:用户上传一张自拍,系统告诉她“这支口红是什么色号”“这个颜色适不适合你”。听起来像调色板加个分类器,但真正接数据就会发现,口红色号检测牵扯人脸关键点、嘴唇区域提取、颜色空间转换、模型推理和推荐匹配,任何一个环节都容易翻车。基于Dlib+PyQt5+TensorFlow的智能口红色号检测推荐系统,就是把这套完整链路打包成可复现代码的工程,自带Python源码、训练模型和数据集。拆完这份资源后,我觉得它最值钱的地方不是某个模型有多准,而是把“如何从一张人脸图走到最终推荐结果”的工程路径讲清楚了。适合正在做深度学习落地、想上手桌面端AI应用开发的初学者,也适合需要一套可二次开发的检测推荐基座的从业者。

2. 系统设计:Dlib做关键点、TensorFlow做识别、PyQt5做壳的职责划分

在动手改代码之前,我建议先理解这个项目为什么是“三件套”而不是一个端到端的大模型。口红色号检测的第一难点,是模型得知道“该看脸上哪个位置”。直接用整张图分类,背景、头发、肤色都会干扰模型,训练数据量不够的话,模型学到的是“图里有没有嘴唇”而不是“嘴唇是什么色”。所以合理分工是:Dlib负责定位和裁剪,把高概率的嘴唇区域喂给TensorFlow模型;模型只负责做色号分类;PyQt5负责把推理结果、推荐列表和交互逻辑串起来。这个分离思路会在后面每一步省下大量调试时间。

2.1 整体流程:从人脸关键点到色号推荐

整个系统的推理链路可以拆成六个环节。第一步,通过本地图片或摄像头拿到原始图像;第二步,用Dlib的人脸检测器找到人脸框;第三步,对人脸区域提取68个关键点,取其中属于嘴唇的48~67号点,围出嘴部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上单帧推理大约30~60毫秒,放在桌面应用里足够实时。关键点定位和嘴唇裁剪一次完成,代码路径更短。

也有人问:色号识别为什么不用目标检测模型,比如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.md

data/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_ok=True) 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围成。裁剪逻辑不能直接取最小包围盒,因为嘴唇边缘的皮肤会混进来;也不能只取内唇线,因为外沿颜色过渡也是判断色号的重要依据。我一般会在最小包围盒基础上外扩20~30像素,把下嘴唇阴影和嘴角边缘都包含进来。

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_ratio=0.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.2~0.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_delta=0.15) image = tf.image.random_saturation(image, lower=0.6, upper=1.4) image = tf.image.random_hue(image, max_delta=0.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_top=False, weights="imagenet" ) 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, training=False) x = layers.GlobalAveragePooling2D()(x) x = layers.Dropout(0.3)(x) outputs = layers.Dense(num_classes, activation="softmax")(x) model = Model(inputs, outputs) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss="categorical_crossentropy", metrics=["accuracy"] ) model.summary()

.preprocess_input这一步很多人会漏,MobileNetV2的预训练权重需要输入在[-1,1]区间的RGB图,如果不做这个归一化,收敛速度会明显变慢。Dropout(0.3)设在分类头之前,对防止色号类别间的过拟合很有用,尤其是当某些色号只有百来张图片时。base_model.trainable = False保证第一阶段只更新Dense层。注意base_model(x, training=False)里必须显式传training=False,否则BatchNorm层在冻结状态下也可能被错误更新。

4.2 训练配置:数据管道与回调

训练阶段我通常用tf.data而不是ImageDataGenerator,因为前者能利用并行解码,也让增强逻辑更透明。图片统一缩放到224x224,batch size设成32。如果CPU核数多,num_parallel_calls可以调大;如果是GPU训练,batch size=64也常见,但必须保证显存不溢出。

def parse_function(path, label): img = tf.io.read_file(path) img = tf.image.decode_jpeg(img, channels=3) 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_calls=tf.data.AUTOTUNE) train_ds = train_ds.map(augment, num_parallel_calls=tf.data.AUTOTUNE) train_ds = train_ds.batch(32).prefetch(tf.data.AUTOTUNE) callbacks = [ tf.keras.callbacks.ReduceLROnPlateau( monitor="val_loss", factor=0.5, patience=3 ), tf.keras.callbacks.EarlyStopping( monitor="val_loss", patience=6, restore_best_weights=True ) ] history = model.fit( train_ds, validation_data=val_ds, epochs=30, callbacks=callbacks )

ReduceLROnPlateau的factor=0.5表示验证损失不再下降时学习率减半,patience=3给模型3个epoch的机会寻找更优方向。EarlyStopping的patience=6是总训练轮数的延伸,避免在验证集上反复震荡浪费时间。训练完第一阶段的冻结模型后,我会解冻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, axis=1)) y_true.extend(np.argmax(labels.numpy(), axis=1)) print(classification_report(y_true, y_pred, target_names=class_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_distance=0.4, weight_probability=0.6开放到界面的“高级设置”里,方便测试不同参数组合下的推荐效果。颜色距离采用CIEDE2000色差公式,比欧氏距离更接近人眼感知,这也是我踩过雷之后换过去的做法。

最后我想说,这个项目给我的最大教训是:不要把“模型训练准确率”当作项目成功的唯一指标。训练阶段花一周优化网络结构,不如花一晚上梳理预处理、统一训练推理路径、把界面线程理清楚。从那以后我每次拿到类似的检测推荐类资源,都强制走一遍端到端验证流程——从原始图片到最终UI展示,逐个环节看中间产物是否符合预期,再决定要不要优化模型。希望这个拆解过程和避坑记录能帮你在复现时省下几个晚上,少经历几次“玄学报错”。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询