简介:这是一份基于PyTorch实现的颜色识别项目代码,面向需要完成毕业设计、期末大作业或课程设计的计算机相关专业学生,也适合有一定Python基础、想通过实战入门深度学习的开发者。项目包含完整的数据处理、CNN模型定义、训练与检测流程,代码注释详细,新手也能读懂。压缩包共14个文件(约6.93MB),其中9个Python脚本覆盖主程序、图像转换、模型构建、训练与检测等核心模块;另含预训练权重文件pth、环境依赖配置文件yml与txt、说明文档md等,拿到后简单部署即可使用。目前已有210人学习下载。该高分项目功能完整、界面直观,不仅演示了从数据准备到模型训练再到颜色识别的全流程,还提供了可直接调用的检测脚本与权重,读者既能快速复现识别效果,也可参考其工程结构进行二次开发,完成自己的课程任务或拓展应用场景。
1. 用 PyTorch 做颜色识别,为什么是课程作业的高分路径
颜色识别听起来是入门级任务,很多人直接用 OpenCV 的 inRange 写死几个 HSV 阈值就交差了。但课程作业拿高分的关键,恰恰不在"识别得准",而在"方案能不能讲清楚"。硬编码阈值最大的问题是:你说不出阈值为什么是 25 而不是 30,模型没有任何可解释的学习过程。用 PyTorch 把颜色识别当作图像分类问题来处理,数据准备、模型结构、损失函数、评估指标每一环都能正向解释,报告里可以写满三页真实分析与实验。无论大作业要求识别固定色块、桌面物体还是简单场景,这条路线都适用,而且对 GPU 没有硬性要求,CPU 也能跑完。
接下来按数据准备、模型搭建、训练评估、提交前验证四个环节,把关键代码、参数和常见坑位一次说透。
2. 在 PyTorch 里构建颜色识别数据集,先把归一化想清楚
颜色识别大作业的常见做法是手工拍几百张照片标类别。这个方案在小型数据集上不是不行,但样本数量少、光照不统一,后面模型出现误判时很难定位是数据问题还是模型问题。我一般先做合成数据,再混合少量真实样本。
2.1 类别体系怎么定:8 类比 20 类更适合作业
先确定分类体系。常见颜色类别定在 8 类左右:红、绿、蓝、黄、橙、紫、青、灰。类别太少显得工作量不够,太多会出现"粉红算不算红""深蓝算不算青"这类边界争议,答辩时要花大量篇幅解释,反而扣分。8 类是一个文档友好、演示稳定的范围。
有一个容易踩的坑:在 RGB 空间里直接做分类,模型需要额外学习亮度解耦,因为同样一种红色,在暗光下 RGB 值可能是 (80, 0, 0),在强光下是 (220, 50, 50)。人眼识别颜色的核心是色调,而不是 RGB 数值本身。所以数据准备阶段就转 HSV 空间更合理,H 通道承载主色调,S 和 V 分别承载饱和度和明暗,模型学起来轻松,报告里也能给出"为什么 HSV 比 RGB 更适合颜色识别"的分析。
2.2 用脚本批量生成颜色样本,避免手工标注
合成数据的核心思路:在 HSV 空间里按每个类别的中心色调生成色块,然后叠加明度、饱和度抖动和噪声。生成脚本如下。
import os import cv2 import numpy as np from tqdm import tqdm # 这里的 H 值范围是 0~179,对应 OpenCV 的 HSV 表示 CATEGORY_HUE = { 'red': 0, # 红在 OpenCV 中围绕 0 和 179 两个端点 'orange': 15, 'yellow': 30, 'green': 60, 'cyan': 90, 'blue': 120, 'purple': 140, } def generate_patch(hue, size=64): # 色调抖动 ±8,模拟同类颜色的深浅差异 h = int(np.clip(hue + np.random.uniform(-8, 8), 0, 179)) s = np.random.randint(80, 256) # 饱和度:80~255 v = np.random.randint(100, 256) # 明度:100~255,避免过暗 img = np.full((size, size, 3), (h, s, v), dtype=np.uint8) rgb = cv2.cvtColor(img, cv2.COLOR_HSV2RGB) # 加高斯噪声,模拟传感器噪声 noise = np.random.normal(0, 6, rgb.shape).astype(np.float32) return np.clip(rgb + noise, 0, 255).astype(np.uint8) def build_dataset(root, per_class=2000): for cls, hue in CATEGORY_HUE.items(): os.makedirs(os.path.join(root, cls), exist_ok=True) for i in tqdm(range(per_class), desc=cls): cv2.imwrite(os.path.join(root, cls, f"{i:05d}.png"), generate_patch(hue)) build_dataset("./data/color_train")参数说明:CATEGORY_HUE中的 H 值用的是 OpenCV 范围,不是数学常用的 0~360,写错这个会直接导致所有蓝色样本变成绿色。色调抖动幅度 ±8 是我测试后比较稳的选择,改成 ±15 会提高模型对偏色的容忍度,但训练也会更难收敛。每类 2000 张、8 类共 16000 张的训练集,在 CPU 上训练一个小型 CNN 只需几分钟。
提示:合成数据训练的模型直接放到真实照片上会有分布差异,建议留出 10% 的真实拍照样本做混合微调,答辩时这是"数据增强策略"的加分项。
2.3 Dataset 定义与 Normalize 参数的两种选择
PyTorch 的torchvision.datasets.ImageFolder可以直接读取文件夹结构的数据集,前提是目录格式为label/图片.jpg。归一化参数这里有两个流派:
- 用 ImageNet 的均值 (0.485, 0.456, 0.406) 和方差 (0.229, 0.224, 0.225)
- 用自计算的均值方差,或用对称的 0.5 归一化
颜色识别场景中,如果模型是从零训练的小型 CNN,用 0.5 归一化更合理。ImageNet 的统计量来自自然图像,对纯色块反而是一种偏差。如果后面走迁移学习加载 ResNet 预训练权重,那必须换回 ImageNet 参数,否则第一层输入分布与预训练假设不匹配。
from torch.utils.data import DataLoader from torchvision import datasets, transforms train_tf = transforms.Compose([ transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.3, contrast=0.2), transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), ]) val_tf = transforms.Compose([ transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), ]) train_ds = datasets.ImageFolder("./data/color_train", transform=train_tf) val_ds = datasets.ImageFolder("./data/color_val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=64, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=64, shuffle=False, num_workers=4) print("类别映射表:", train_ds.class_to_idx)逻辑说明:ColorJitter在训练时随机改亮度与对比度,等价于扰动 HSV 里的 V 通道,让模型不依赖固定亮度;Resize((64, 64))统一输入尺寸,64×64 对颜色识别足够,太小会丢失颜色过渡信息,太大则增加计算量。验证集的shuffle=False保证评估时样本顺序固定,指标可复现。
| 归一化方案 | mean / std | 适用场景 |
|---|---|---|
| ImageNet 统计量 | 0.485 / 0.229 等三组 | 迁移学习必须沿用 |
| 对称归一化 | 0.5 / 0.5 | 从零训练小型 CNN |
| 按数据集计算 | 自行统计 | 真实照片混合训练 |
3. 模型搭建:轻量 CNN 与 ResNet 迁移学习的选型依据
颜色识别属于低层视觉任务,核心特征都在前几层卷积里。这意味着不需要堆叠很深的网络,几十层模型在颜色数据上反而容易过拟合。
3.1 为什么 3 层卷积就够用
一个颜色块的主要特征表现为"某个区域的主色调",这本质上是通道维度的统计特征,不需要复杂的空间纹理抽象。3 层卷积足以提取颜色分布信息,因为第一层卷积核就会学到 RGB 通道间的加权组合,等价于在通道层面做颜色挑选。网络再深,特征图里主要保留的是边缘和形状结构,与颜色任务的相关性反而变弱。
3.2 用 PyTorch 定义一个轻量 ColorNet
import torch import torch.nn as nn class ColorNet(nn.Module): """3通道输入,输出颜色类别数""" def __init__(self, num_classes=8): super().__init__() self.features = nn.Sequential( # 第一层:提取局部颜色组合,输出 32 个特征图 nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 32×32 # 第二层:加深通道,压缩空间 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 16×16 # 第三层:全局平均池化替代 Flatten nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d(1), ) self.classifier = nn.Linear(128, num_classes) def forward(self, x): x = self.features(x) x = torch.flatten(x, 1) return self.classifier(x) model = ColorNet(num_classes=8) total_params = sum(p.numel() for p in model.parameters()) print(f"参数量:{total_params / 1e3:.1f}K")代码说明:BatchNorm2d在颜色数据上尤其重要,因为颜色输入的值域波动比自然图像更大,批归一化能稳定每层输出的分布。AdaptiveAvgPool2d(1)把 16×16×128 的特征压成 128 维向量,相比直接Flatten能保留更多全局信息,也避免了全连接层参数量爆炸。这套结构参数量约 0.2M,CPU 上训练很快,报告中写出这个数字能体现你关注模型复杂度。
3.3 迁移学习对比实验:ResNet18 的替换分类头
大作业如果想体现"模型对比",可以加一个 ResNet18 微调的实验。做法是加载预训练权重,冻结所有卷积层,只训练最后的全连接分类头。
import torchvision.models as models def build_resnet18(num_classes=8): model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) for param in model.parameters(): param.requires_grad = False # 冻结特征提取层 in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) return model resnet = build_resnet18(8) trainable_params = sum(p.numel() for p in resnet.parameters() if p.requires_grad) print(f"可训练参数量:{trainable_params / 1e3:.1f}K")参数说明:weights=models.ResNet18_Weights.IMAGENET1K_V1是 torchvision 较新版本的写法,旧版pretrained=True会报 DeprecationWarning。requires_grad=False含义是不参与反向传播更新,训练时只优化新加的全连接层。这里有个典型案例:如果坚持用 0.5 归一化喂给 ResNet,预训练权重对输入分布的假设被破坏,最终准确率会比小 CNN 还低。迁移学习必须配合 ImageNet 归一化。
| 模型 | 参数量 | 训练时间(CPU) | 精度特点 |
|---|---|---|---|
| ColorNet(3层卷积) | 约 0.2M | 3-5 分钟 | 纯色块上 99%+ |
| ResNet18 微调分类头 | 可训练约 0.4M | 15-20 分钟 | 真实照片更稳 |
4. 训练与评估:损失函数、学习率调度和混淆矩阵
训练环节的大作业价值在于"能解释每个超参数为什么这么设"。很多同学的报告写"loss 降低了",但没有说明用的是什么优化器、做了哪些调度、指标为什么这么选。
4.1 损失函数与类别不平衡处理
颜色识别默认用nn.CrossEntropyLoss。如果某些类别的样本数偏少,比如紫色在合成数据里生成得少,就要用类别加权。常见做法是从训练集统计类别数量,计算权重。
class_counts = torch.tensor([1600, 1550, 1500, 1650, 1400, 1200, 1450, 1580], dtype=torch.float32) weights = class_counts.max() / class_counts criterion = nn.CrossEntropyLoss(weight=weights) print("各类别损失权重:", weights.numpy())逻辑说明:max / class_counts的计算方式让样本少的类别获得更大权重,反向传播时少数类的梯度被放大,模型不会为了总体准确率而忽视稀有类别。如果各类别数量均匀,权重可以直接不传,但把统计过程留在代码里会让答辩更有说服力。
4.2 训练主循环模板:零梯度、反向传播与模型保存
import torch.optim as optim device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = ColorNet(num_classes=8).to(device) optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5) def train_one_epoch(model, loader, criterion, optimizer): model.train() total_loss, correct, total = 0.0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() # 清空上一轮梯度 outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) correct += (outputs.argmax(1) == labels).sum().item() total += labels.size(0) return total_loss / total, correct / total for epoch in range(30): loss, acc = train_one_epoch(model, train_loader, criterion, optimizer) scheduler.step() print(f"Epoch {epoch+1:02d} | loss {loss:.4f} | acc {acc:.4f}") torch.save(model.state_dict(), f"./checkpoints/colornet_{epoch+1:02d}.pth")参数说明:optimizer.zero_grad()不可省略,PyTorch 的梯度是累积的,不清空会让多个 batch 的梯度叠加,训练曲线剧烈震荡。lr=1e-3是 Adam 对中小型网络的常用起点,如果 loss 下不去,改 3e-4 更稳。StepLR(step_size=10, gamma=0.5)表示每 10 个 epoch 学习率减半,让后期在损失面平坦区域做更精细的收敛。每轮保存权重是为了可以在验证集指标回退时回滚,这个细节在演示时能体现工程能力。
4.3 评估指标:准确率之外必须有混淆矩阵
颜色识别很容易做到 98% 以上准确率,但这种指标掩盖了关键信息:哪些颜色对在混淆?用混淆矩阵一眼就能看出橙色和黄色是否边界重叠。
from sklearn.metrics import confusion_matrix def evaluate(model, loader, class_names): model.eval() preds, labels = [], [] with torch.no_grad(): for imgs, targets in loader: imgs = imgs.to(device) out = model(imgs).argmax(1).cpu().numpy() preds.extend(out.tolist()) labels.extend(targets.numpy().tolist()) cm = confusion_matrix(labels, preds) for i, name in enumerate(class_names): wrong = [(class_names[j], cm[i][j]) for j in range(len(class_names)) if i != j and cm[i][j] > 0] if wrong: print(f"{name} 误判为:{wrong}") return cm cm = evaluate(model, val_loader, val_ds.classes)这段代码核心在最后一层判断逻辑:它把每一类被误判成哪些类别打印出来。如果橙色→黄色出现大量样本,说明训练集的色调抖动跨度太大,或者归一化后两种颜色的距离太近,解决办法是减小色相抖动到 ±6 或增加橙色类别的样本数。这个"指标驱动数据调整"的闭环,是大作业拿高分的核心方法论。
| 超参数 | 推荐值 | 说明 |
|---|---|---|
| batch_size | 64 | CPU 训练时 32 也可以 |
| 初始学习率 | 1e-3 | Adam 默认规模下最稳 |
| weight_decay | 1e-4 | L2 正则,防过拟合 |
| step_size | 10 | 每 10 轮减半 |
| gamma | 0.5 | 学习率衰减系数 |
5. 提交前把"能跑"变成"高分"的三个关键验证
课程作业评分看的不只是代码能不能跑通,更是方案是否经得起质疑。以下是提交前必须做的三个验证。
5.1 保存混淆矩阵图并挑出错误样本
把混淆矩阵用 matplotlib 画出来,保存成图片放进报告。再从验证集里挑出 2 到 3 个误判样本,把原图、预测类别、预测置信度打印到一张图上。这个操作能让答辩老师直观看到"模型在哪些地方有限度",而不是只会报 99% 准确率。
import matplotlib.pyplot as plt def visualize_error(model, loader, class_names, num_samples=3): model.eval() shown = 0 with torch.no_grad(): for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) probs = torch.softmax(model(imgs), dim=1) preds = probs.argmax(1) for i in range(len(labels)): if preds[i] != labels[i] and shown < num_samples: top_p, top_c = probs[i].max(0) plt.figure() plt.imshow(imgs[i].permute(1, 2, 0).cpu().numpy() * 0.5 + 0.5) plt.title(f"真实: {class_names[labels[i]]} | " f"预测: {class_names[preds[i]]} | " f"置信度: {top_p:.3f}") plt.axis(False) plt.savefig(f"./error_{shown}.png") shown += 1逻辑说明:显示图像前要做* 0.5 + 0.5的反归一化,否则训练时的归一化会让图像显示成一片灰色。置信度从 softmax 输出里取最大值,保留这个数字可以用于分析模型对边界色的处理方式,是"确定性错误"还是"低置信度可疑判断"。
5.2 额外跑一组边界色测试
在训练集之外单独生成一组边界色样本,比如放在红色和橙色中间的 25 度色调,测试模型是否会对这类样本给出分散的概率分布而不是强行判定。合理的模型应当输出"橙色 0.55,红色 0.30,黄色 0.15"这样的软分布,这代表它"知道"这个颜色接近边界,而不是把边界色当独立类别。将结果做成一个三个小图横向排列的对比图,报告中放一张,是加分的常见做法。
5.3 训练曲线与 README 的完整链条
最后把每个 epoch 的训练 loss、验证准确率画在同一条图上,加上第三节的混淆矩阵图,整个大作业的代码和数据就构成了完整链路。README 中写明环境依赖、数据生成命令、训练入口和评估入口,让助教按步骤走一遍就能复现结果。颜色识别虽然任务简单,但整套方案体现的是工程完整性,这才是高分项目真正的得分点。
本文还有配套的精品资源,点击获取