PyTorch颜色识别实战:用HSV与轻量CNN让模型精度稳定95%
2026/9/14 2:49:41 网站建设 项目流程

简介:基于PyTorch的颜色识别项目代码,涵盖数据预处理、模型构建、训练、检测与调试等完整流程,适合作为课程设计、期末大作业或毕业设计参考,也适合刚接触深度学习的初学者对照学习。压缩包内共14个文件,包含9个Python脚本、1个已训练好的模型权重文件、环境依赖配置文件与说明文档;脚本覆盖训练、检测、图像转换、调试验证等模块,代码中附有详细注释,整体大小约6.93MB,部署轻便。主程序调用逻辑清晰,功能完整且界面友好,下载后简单配置环境即可运行,能快速获得颜色识别结果。目前已有210人学习下载,对于希望完成高分课设或入门PyTorch图像分类的读者,这套代码可帮助理解颜色识别网络的设计思路,并可在现有基础上进行二次开发与功能扩展,实用价值较高。

1. 颜色识别高分项目,问题从来不在模型层数

一张颜色识别模型通常坚持不到答辩的第三张测试图:前两张是训练时的固定光源,换到窗边自然光,红色和橙色开始混,深蓝和纯黑也开始混。这不是模型不够深,而是这类 PyTorch 大作业最常见的丢分点——把颜色当成通道数值去硬拟合,没有把颜色当成光照条件下的视觉分布。标题里的“高分项目”,落地时靠的是三条主线:用 HSV 把色相与亮度解耦、用 PyTorch 搭一个能学到色相分布的浅层分类网络、再补上评估闭环证明模型没有走捷径。按这个顺序做,模型参数不到一万,颜色识别精度也能稳定落在 95% 上下,答辩时拿得出手的是可复现的指标,不是“调了几百轮”。

2. 颜色识别的数据管线:先换掉 RGB,再用 HSV 定标签

2.1 为什么 RGB 通道不能直接当作网络输入

颜色识别和一般图像分类最大的差别在于:同一张红色桌布,在日光灯下是 (200, 50, 50),在白炽灯下变成 (230, 80, 70),在阴影里可能降到 (100, 30, 30)。这三个 RGB 向量的欧氏距离远大于“红色”和“橙色”之间的典型距离,网络要强行从这种光照扰动里学出稳定边界,只能在训练集里硬背光源特征。所以直接用原始 RGB 做输入,等价于把“识别颜色”这个任务改成了“识别光照环境”。

HSV 的动机就是把亮度 V 和色相 H 分离,算法上把“这是什么颜色”和“它有多亮”拆成两个变量。PyTorch 的torchvision.transforms不直接提供 RGB 到 HSV 的变换,但你不需要在数据加载里手动转,因为训练时给网络喂归一化 RGB 即可,模型自己能学到去光照的表示。真正需要 HSV 的场景是数据准备阶段:你要用它来快速确认每一类样本的区间分布,以及排查错误标签。

2.2 用 HSV 把颜色标签映射成可验证的区间

这里有一个非常实用的中期检查手段:用 OpenCV 读入所有训练图,转成 HSV,统计每个标签下 H 通道的直方图。正常情况下,每个类别的 H 值应该集中在一段区间内;如果直方图分散成两个峰,说明这批数据里混了别的颜色,或者标签本身就标错了。

以下是一段直接在 Jupyter 里跑的检查脚本,能在一分钟内定位标签噪声。

import cv2 import numpy as np import glob from collections import defaultdict hists = defaultdict(list) for path in glob.glob("data/train/*/*.jpg"): # 目录结构: train/类别/图片 label = path.split("/")[-2] img = cv2.imread(path) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) h_channel = hsv[:, :, 0] # OpenCV 里 H 范围是 0~180 hists[label].append(h_channel.flatten()) for label, h_list in hists.items(): all_h = np.concatenate(h_list) p10, p90 = np.percentile(all_h, [10, 90]) print(f"{label}: H 通道 10%~90% 分位 = {p10:.1f} ~ {p90:.1f}")

这段代码的逻辑是:把每张图的色相通道拉平,取每个类别的 10% 和 90% 分位数做成区间。分位数比均值更稳,因为均值会被少量异常像素拉偏。参数上唯一的注意点是 OpenCV 的 H 范围是 0 到 180,不是很多教程里写的 360,转成matplotlib画图时同样要按 0 到 180 处理,否则红色会被劈成两段。

2.3 数据集目录组织与针对性数据增强

颜色识别项目的数据量通常不大,大作业场景下每个类别 50 到 200 张是常态。目录直接按ImageFolder的约定组织,省掉手写 Dataset 的麻烦:

data/ train/ red/ img_001.jpg ... green/ img_001.jpg ... blue/ ... val/ red/ ...

数据增强是这类项目最容易翻车的地方。最常见的一个误用是给颜色识别任务加上torchvision.transforms.ColorJitter,随机改亮度、对比度、饱和度——你一边让它“学颜色不变性”,一边随手把颜色标签改掉了,等于给训练集注入错误标注。我一般的做法是:只保留对空间位置不敏感的增强,并且把分辨率控制在 32 或 48 像素,因为颜色是全局信息,不需要高分辨率。

from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf = transforms.Compose([ transforms.Resize((32, 32), interpolation=2), # 2 = 双线性,避免色块锯齿 transforms.RandomHorizontalFlip(p=0.5), # 颜色与翻转无关,可以加 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_tf = transforms.Compose([ transforms.Resize((32, 32), interpolation=2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds = datasets.ImageFolder("data/train", transform=train_tf) val_ds = datasets.ImageFolder("data/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4, pin_memory=True) print("类别:", train_ds.classes)

这段加载管线有四个参数值得细说。Resize里的interpolation=2指的是PIL.Image.BILINEAR,颜色识别样本大多是纯色块或大片色域,用最近邻插值会在边缘产生锯齿,给卷积核添加无意义的纹理;RandomHorizontalFlip是安全的,因为镜像不改变像素颜色分布;Normalize用的仍是 ImageNet 的均值和方差,这里不需要为了颜色任务重新统计,归一化只是为了数值稳定;num_workers=4在 Windows 下如果报多进程错误,直接降到 0,因为大作业数据量小,多进程带来的加速有限,反而可能引入排查成本。

3. 用 PyTorch 搭一个浅层颜色分类网络,把色相分布变成可训参数

3.1 为什么颜色识别不优先上 ResNet

现在很多模板代码都基于 pytorch 基础框架直接堆 ResNet18,但颜色识别是一个典型的“低语义、高分布”任务:判断依据是整张图的色调分布,而不是物体轮廓、纹理层级或局部部件。ResNet 的强项恰恰在后三者。用 ResNet18 在这种任务上只会得到两个结果:一是训练集上快速过拟合,因为模型的容量远超任务需求,开始去记忆每张训练图的光照细节;二是答辩时被问“为什么用这么深的网络”时,很难给出有说服力的选型理由。

颜色识别真正需要的是“感受野覆盖全图但不深”的浅层网络。一个可以被答辩接受的设计是:两层 3×3 卷积提取局部色块分布,中间接 BatchNorm 稳定训练,最后用全局平均池化把整张图的信息压缩成一个向量。全局平均池化在这里承担的是“把颜色统计量聚合起来”的角色,比直接 Flatten 再甩给全连接层更能体现设计意图。

3.2 可复现的 ColorNet 结构与参数量

下面这份模型定义,可以直接复制为一个model.py文件,训练时 import 即可。

import torch.nn as nn class ColorNet(nn.Module): def __init__(self, num_classes=7): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 16, kernel_size=3, padding=1, bias=False), nn.BatchNorm2d(16), nn.ReLU(inplace=True), nn.Conv2d(16, 32, kernel_size=3, padding=1, bias=False), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d(1) # 把 [B,32,H,W] 压成 [B,32,1,1] ) self.classifier = nn.Sequential( nn.Dropout(0.3), nn.Linear(32, num_classes) ) def forward(self, x): feat = self.features(x) # [B, 32, 1, 1] return self.classifier(feat.flatten(1))

这份结构里藏着两个容易忽略的选型决定。第一,输入分辨率只有 32×32,两层 3×3 卷积的等效感受野是 5×5,虽然单层感受野不大,但颜色是全局频段信息,经过全局平均池化后,每个通道已经聚合了整张图的色相响应,感受野不再是瓶颈。第二,bias=False配合BatchNorm2d,因为 BN 层自带偏置项,卷积层再加 bias 属于冗余参数,这也是 ResNet 系列在BasicBlock里的标准做法。

AdaptiveAvgPool2d(1)是对传统nn.Flatten()的替代。Flatten 会把 32 个通道的二维特征全部拉平,再接全连接层会产生明显的参数量跳变;平均池化先做通道内空间压缩,最后只有一个 32 维向量进入分类器。这个设计决定了整个模型的可训练参数数量,按每层展开可以精确估算:

输出形状参数量(约)
Conv2d(3,16,k=3)[B,16,32,32]432
BatchNorm2d(16)[B,16,32,32]32
Conv2d(16,32,k=3)[B,32,32,32]4608
BatchNorm2d(32)[B,32,32,32]64
AdaptiveAvgPool2d[B,32,1,1]0
Linear(32,7)[B,7]231
合计约 5367

五万参数量不到,即使在 CPU 上训练,一个 epoch 也就几秒。对大作业而言,这个规模意味着你可以大方地把完整训练日志和 loss 曲线放进报告里,展示“模型轻量但指标稳定”的对比实验,而不是只贴一张训练完成的截图。

3.3 训练前的环境与数据形状检查

在进入训练之前,有一个经常在答辩现场翻车的环节:环境版本不一致。不同 PyTorch 版本对torchvision.transforms的行为有细微差异,比如旧版本里Resizeinterpolation参数默认是最近邻,新版本已经改成双线性。为了避免现场演示时出现“在家里能跑、在台上不能跑”的问题,我一般会在训练脚本开头加一段设备与版本检查,同时把输入张量的形状验证一并做掉。

import torch print("PyTorch:", torch.__version__) print("CUDA 可用:", torch.cuda.is_available()) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 数据形状冒烟测试:跑一个 batch 的前向,确认网络能通 sample_batch, _ = next(iter(train_loader)) sample_batch = sample_batch.to(device) model = ColorNet(num_classes=len(train_ds.classes)).to(device) out = model(sample_batch) print("前向输出形状:", out.shape) # 预期 [batch_size, num_classes]

关于 PyTorch 安装这里多说一句:先到 pytorch 官网复核当前稳定版的安装命令,确认 CPU 版和 CUDA 版的差异,训练脚本里用torch.cuda.is_available()做自适应。大作业项目经常被拷到别的机器上跑,设备自适应代码能在演示时省掉“你装 CUDA 了吗”这类尴尬问答。输出形状检查的意义在于提前暴露类别数不匹配的问题——如果ImageFolder扫描目录时把某个空文件夹算成了一个类,num_classes会比预期多,前向不会报错,但训练后混淆矩阵会多出一行永远为零的类别。

4. 训练策略与控制变量:让大作业的颜色识别分数稳定在 95% 以上

4.1 固定随机种子与训练循环

颜色识别项目拿到高分的前提是“可复现”。可复现不是指固定一个随机种子就算完,而是让答辩老师在你的报告里能看到整套训练策略的可控性:数据划分固定、权重初始化固定、评估集固定。下面的训练循环代码带了两种控制:一是全局随机种子,二是最优模型保存条件。

import random import numpy as np import torch import torch.nn as nn import torch.optim as optim def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42) model = ColorNet(num_classes=len(train_ds.classes)).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5) best_val_acc = 0.0 for epoch in range(30): model.train() running_loss, correct, total = 0.0, 0, 0 for x, y in train_loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() loss = criterion(model(x), y) loss.backward() optimizer.step() running_loss += loss.item() * x.size(0) correct += (model(x).argmax(1) == y).sum().item() total += y.size(0) train_acc = correct / total val_acc = evaluate(model, val_loader, device) # 复用下文的评估函数 scheduler.step() if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), "best_color_net.pth") print(f"epoch {epoch+1:02d} | loss {running_loss/total:.4f} " f"| train_acc {train_acc:.4f} | val_acc {val_acc:.4f}")

几个参数的选择逻辑值得展开。weight_decay=1e-4是浅层小模型的安全默认值,它把权重的 L2 范数压住,防止颜色这类低频特征被少数异常像素主导;StepLR(step_size=10, gamma=0.5)表示每 10 个 epoch 学习率衰减一半,在 30 epoch 的训练里正好做两次衰减,适合数据量小、收敛快的场景,比 CosineAnnealing 更容易解释。CrossEntropyLoss这里没有手工加权,因为颜色分类数据通常是均衡的;如果某个类别样本明显少于其他类,优先考虑用WeightedRandomSampler做采样均衡,而不是调 loss 权重——前者保留原始数据分布,后者会改变梯度尺度,排查起来更麻烦。

4.2 超参数设定参考与调参边界

给出一份可以直接照抄的超参基线,并标注每项的可调范围和风险边界。

参数推荐值可调范围调大后果调小后果
输入分辨率32×3232~64训练变慢,增益极低低于 24 时色块细节丢失
batch_size3216~64需要同步调大 lr收敛变慢,BN 统计不稳定
学习率1e-35e-4~3e-3训练震荡,val_acc 跳变收敛慢,30 epoch 不够
优化器AdamSGD+动量Adam 足够,换 SGD 要调 lr
Dropout0.30~0.5模型欠拟合出现过拟合迹象时调大
训练轮数3020~50超过 30 基本无增益欠拟合,val_acc 大概率低于 90%

这里的核心边界是学习率。颜色识别任务的特征分布非常简单,梯度方向高度一致,学习率过大时 loss 曲线看起来在下降,但混淆矩阵会显示某些类别被整体吞掉,典型表现是红色和橙色互相混叠。如果训练曲线出现这种锯齿状 val_acc,第一反应不应该是加网络深度,而是把学习率降到 5e-4 重跑一次,同时保留旧日志做对比。

4.3 评估不是只看 accuracy:混淆矩阵与失败样本

单看 accuracy 最大的问题是无法回答“是哪两类在混”。颜色识别里最常见的混叠是相邻色相:红和橙、蓝和紫、深蓝和黑。这些错误分布如果不用混淆矩阵展示,只写一个 97% 的准确率,答辩时被问到“哪个颜色最容易出错”就会卡壳。因此评估代码至少要输出三样东西:总体准确率、每类 precision/recall/F1、混淆矩阵。

from sklearn.metrics import confusion_matrix, classification_report import numpy as np def evaluate(model, loader, device): model.eval() y_true, y_pred = [], [] with torch.no_grad(): for x, y in loader: x = x.to(device) out = model(x) y_pred += out.argmax(1).cpu().tolist() y_true += y.tolist() cm = confusion_matrix(y_true, y_pred) report = classification_report(y_true, y_pred, target_names=train_ds.classes, digits=3) val_acc = np.mean(np.array(y_true) == np.array(y_pred)) print(report) print("混淆矩阵:\n", cm) return val_acc # 保存失败样本,用于报告中的错误分析 val_acc = evaluate(model, val_loader, device)

这段代码的重点在错误分析。跑完评估后,把预测错误的样本单独拎出来,按“真实颜色/预测颜色/置信度”整理成表格,放进大作业报告里,能直接支撑答辩中的两个高频问题:错误集中在哪里(看混淆矩阵的非对角线元素)、为什么会错(看失败样本的光照情况)。参数上无需额外调整,classification_report里的digits=3记得要加,否则默认输出两位小数,精度变化很难在组间对比中看出来。

5. 一个答辩能直接用的收尾:推理函数、注意力验证与导出

5.1 封装 predict 函数并输出 Top-3 置信度

训练结束后,模型还只是一个.pth权重文件。答辩演示时最忌讳现场写加载代码,或者直接跑训练脚本。把推理逻辑封装成一个独立函数,是让项目从“能跑”变成“好用”的关键一步。这个函数返回 Top-3 预测结果,而不是只返回argmax,因为颜色识别本身就存在相邻色相的固有歧义,展示置信度分布比展示一个可能错误的单一结果更有说服力。

from PIL import Image def predict_color(image_path, topk=3): model.eval() img = Image.open(image_path).convert("RGB") x = val_tf(img).unsqueeze(0).to(device) with torch.no_grad(): prob = torch.softmax(model(x), dim=1)[0] idx = prob.argsort(descending=True)[:topk] return [(train_ds.classes[i], float(prob[i])) for i in idx] # 使用示例 for p in ["demo/red_1.jpg", "demo/orange_1.jpg"]: print(p, "->", predict_color(p))

5.2 用热力图验证模型看的是“颜色区域”不是纹理

这个验证技巧非常容易在答辩中加分。颜色识别模型如果学偏了,可能是靠背景纹理或图像边缘来分类的。用第一个卷积层的输出做激活可视化,能快速判断模型是否真的在响应彩色区域。具体做法:给第一层卷积注册一个 forward hook,取一个 batch 的输出特征图,在通道维度上做平均,得到一张空间热力图,再放大到原图尺寸。

def visualize_attention(image_path): model.eval() x = val_tf(Image.open(image_path).convert("RGB")).unsqueeze(0).to(device) act = {} def hook_fn(module, input, output): act["feat"] = output.detach() handle = model.features[0].register_forward_hook(hook_fn) with torch.no_grad(): model(x) handle.remove() heat = act["feat"][0].mean(0).cpu() # [32, 32],通道维取均值 heat = heat.unsqueeze(0).unsqueeze(0) heat = torch.nn.functional.interpolate( heat, size=(128, 128), mode="bilinear", align_corners=False ).squeeze() return heat.numpy()

5.3 导出 ONNX 让演示环境脱离训练脚本

最后一个小技巧是把模型导出成 ONNX 格式,答辩现场只需要onnxruntime就能加载推理,完全绕开 PyTorch 版本差异和训练代码依赖。导出代码只有几行,但dynamic_axes参数直接影响后续推理的灵活性。

dummy = torch.randn(1, 3, 32, 32).to(device) torch.onnx.export( model, dummy, "color_net.onnx", input_names=["image"], output_names=["logits"], dynamic_axes={"image": {0: "batch"}, "logits": {0: "batch"}} )

dynamic_axes把 batch 维度标记为动态,这样导出的模型既支持单张图片推理,也支持一次传入多张图做批量测试,不需要为演示场景单独维护两套推理代码。到这里,这个 PyTorch 颜色识别项目就具备了完整的高分要素:数据管线可复现、模型设计有依据、训练策略可解释、交付形态独立于训练环境。

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

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

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

立即咨询