简介:面向计算机视觉课程设计与毕设场景的完整项目包,内容围绕卫星云层图像的理解与识别展开,包含可直接运行的Python源码、实验报告文档、答辩PPT及说明文件。项目代码均经过实际测试,功能正常,特别适合计科、人工智能、通信等专业学生用作大作业参考或毕设初期立项演示。压缩包共140个文件,以70个py脚本为核心,附带pyc缓存、yaml配置、csv数据表、sh执行脚本、PDF报告和PPT课件,整体约62.33MB,结构清晰便于查阅。目前已有239人学习下载,口碑与完成度可见一斑。资源中既有完整训练与测试流程,也有模型预测结果与实验数据分析,能够帮助读者快速理解卫星云图识别的实现思路、复现实验流程,并在此基础上二次开发,完成自己的课程项目或毕业设计。
1. 卫星云层图像理解与识别:一个视觉大作业背后的完整技术主线
你兴冲冲下载了一批卫星云图,套上经典的图像识别模型开始训练,结果发现验证集准确率卡在 70%,loss 明明还在降。这个标题里的“计算机视觉大作业”只是交付外壳,真正的技术主线是遥感图像的视觉理解:卫星云图和自然图像完全是两套统计分布,灰度单通道、云系纹理、地表与云顶的亮温关系,每一项都在挑战通用计算机视觉方案的假设。做这个方向的人,要解决的具体问题是三件事:怎么把“理解与识别”翻译成可训练的任务,怎么让模型分清云型而不是记住地物边缘,怎么把源码、实验报告和答辩 PPT 串成一个自洽的闭环。正在做课程设计的学生、想入气象视觉方向的工程师,以及需要快速验证“遥感图能不能套现有模型”的开发者,都能从这条线里拿到一套可复现的路径。
2. 把“理解与识别”翻译成任务:通道分析、粗标签生成与数据管线
2.1 先回答三个问题:通道、任务类型、评估口径
拿到卫星云图素材后,第一件事不是写模型,而是先看清楚数据长什么样。我一般会打开一张原始文件,确认三件事:通道数、位深、空间分辨率。常见公开数据来源如 Himawari-8、MODIS 的 L1B 产品,一般会提供可见光反射率通道、红外亮温通道和水汽通道;如果你拿到的是已经渲染好的 PNG 灰度图,那就要接受它已经丢失了物理量纲的事实。
可见光通道里,云比地表亮,但积雪也亮;红外通道里,云顶高度越高则亮温越低,这与地表温差明显。云图的理解与识别,本质上就是从这些通道组合里分离出云型结构和云量分布。
然后是任务类型的取舍。这个标题下的“理解与识别”非常宽泛,常见做法是拆成三类可落地的任务:
| 任务类型 | 标注成本 | 报告完成度 | 典型方法 |
|---|---|---|---|
| 云分类(单标签,如层云/积云/卷云/雨层云/晴空) | 低,用阈值粗标后人工修正 | 高,能讲清楚混淆矩阵和错例 | CNN 分类或预训练迁移 |
| 云分割(逐像素掩膜) | 高,需要精细标注 | 中,依赖标注质量 | U-Net、SegNet 类 |
| 云量估计(回归到覆盖率) | 极低,可由分割标签聚合 | 低,内容偏单薄 | 分类网络换头部 |
我给绝大多数大作业项目的建议是:主线做 5 类云型分类,附带一个云量回归输出。前者是深度学习的图像识别主战场,后者能在实验报告里体现“从分类到回归”的思考。不要把面铺太开,一个可量化、可复现、可辩解的任务,比三个含糊的目标更有答辩价值。
2.2 拿到原始云图之后:用物理阈值生成粗标签
卫星云图没有现成的分类标注,这是与 ImageNet 类数据集最大的区别。常见做法是先利用云图的物理性质做自动粗标,再人工修正。这一步的价值不仅在于省标注时间,更在于让报告里的“数据标注”章节有据可依。
import numpy as np def rough_cloud_mask(img_vis, img_ir, vis_thr=170, ir_thr=-15): """用可见光反射率和红外亮温双约束生成云粗掩膜。 可见光通道:云反射率高,像素值偏亮,但积雪同样亮。 红外通道:云顶亮温低于地表,取反阈值后能压掉大部分地表伪影。 返回 1 表示该像素大概率是云。 """ mask_vis = (img_vis > vis_thr).astype(np.uint8) mask_ir = (img_ir < ir_thr).astype(np.uint8) return mask_vis & mask_ir这段代码的逻辑是“双阈值与操作”:可见光亮且红外亮温低,才判定为云。只靠可见光会把积雪和高反射地表误判为云,只靠红外会把高空薄卷云的边缘漏掉。vis_thr和ir_thr需要根据你下载到的数据做直方图统计后微调。
粗标完成后再按场景人工修正:把 mask 叠加到原图上,挑出晴空、层云、积云、卷云、雨层云五个典型场景各 30~50 张做交叉检查。这样产出的训练标签,比直接从网上找现成数据集更能应对答辩时的提问,因为你手里有完整的标注链路和物理依据。
2.3 数据预处理与划分:归一化、Patch 切分与类别平衡
卫星云图的原始文件通常是几千像素宽的大图,不可能整张塞进模型。常见做法是把大图切分成固定尺寸的 patch,比如 128×128 或 256×256,再按类别归档。切 patch 时要注意一点:训练阶段用随机裁剪增强平移鲁棒性,验证和测试阶段用中心裁剪保证可复现。
import numpy as np from sklearn.model_selection import GroupShuffleSplit # scene_id 表示该 patch 来自哪一景原始云图,防止同一片云泄漏 gss = GroupShuffleSplit(n_splits=1, test_size=0.3, random_state=42) train_idx, val_idx = next(gss.split(image_paths, labels, groups=scene_ids))这里最关键的是按scene_id分组划分,而不是直接对 patch 做随机划分。卫星云图存在空间自相关:同一景云图里相邻 patch 的纹理几乎相同,如果它们同时出现在训练集和验证集,验证指标会虚高,换到新场景立刻打回原形。这是一个我在多个遥感项目里反复踩过的坑,后面避坑章节还会展开。
划分完成后,统计训练集自身的均值和标准差用于归一化,这一步的重要性被很多人低估。把灰度图复制三通道后直接套用 ImageNet 的 mean/std,会让模型在错误的数值范围里训练,损失能下降但验证集表现极差。
# 不用 ImageNet 的 mean/std,统计云图自己的 mean = train_imgs.mean(axis=(0, 2, 3)) # 按通道统计 std = train_imgs.std(axis=(0, 2, 3)) print(f"自定义归一化参数: mean={mean}, std={std}")类别不平衡在云图中非常普遍:晴空样本通常占一半以上,而雨层云可能只有几百个 patch。后面训练时要用类别权重去拉平,这部分我在下一章直接落到代码里。
3. 用 Python 和 PyTorch 跑通云图识别基线:源码结构、模型选型与训练循环
3.1 数据加载器:让训练代码能直接吃云图目录
源码设计的第一步,是把上一章整理好的 patch 目录接进 PyTorch 的 Dataset。我的习惯是 Dataset 里只做读图、裁剪、归一化、转张量这些机械操作,不掺入任何数据增强逻辑,方便后续调试。
import cv2 import numpy as np import torch from torch.utils.data import Dataset class CloudPatchDataset(Dataset): def __init__(self, image_paths, labels, mean, std, crop_size=128, train=True): self.paths = image_paths self.labels = labels self.mean = mean self.std = std self.crop_size = crop_size self.train = train def __len__(self): return len(self.paths) def __getitem__(self, idx): img = cv2.imread(self.paths[idx], cv2.IMREAD_GRAYSCALE) h, w = img.shape if self.train and h > self.crop_size and w > self.crop_size: y = np.random.randint(0, h - self.crop_size) x = np.random.randint(0, w - self.crop_size) else: y, x = (h - self.crop_size) // 2, (w - self.crop_size) // 2 img = img[y:y + self.crop_size, x:x + self.crop_size] img = np.stack([img] * 3, axis=-1).astype(np.float32) # 灰度复制成三通道 img = (img / 255.0 - self.mean) / self.std img_t = torch.from_numpy(img).permute(2, 0, 1).float() label_t = torch.tensor(self.labels[idx], dtype=torch.long) return img_t, label_t逻辑说明:灰度图复制成三通道,是为了让 TorchVision 的预训练模型能直接吃进来;随机裁剪只在训练时生效,验证时固定中心裁剪。mean和std传入的是上一章统计得到的长度为 3 的数组。这里有个细节值得注意——如果 patch 本身比crop_size小,代码会退化成从坐标 0 开始切,建议在数据准备阶段就统一好 patch 尺寸,避免运行时静默出错。
3.2 自研轻量 CNN 与预训练迁移:先跑通哪个
很多做深度学习图像识别的人上来就想用 ResNet50 甚至 EfficientNet,但卫星云图场景有一个现实约束:样本量不大,patch 纹理相对单一。我的经验是先跑通一个自研轻量 CNN,再看是否值得换预训练模型。两者对比的本质是:预训练权重带来了自然图像的先验,但云图纹理和猫狗、汽车完全不在一个分布上;轻量模型没有先验,却不容易被带偏。
import torch.nn as nn import torch.nn.functional as F class TinyCloudNet(nn.Module): def __init__(self, num_classes=5): super().__init__() self.backbone = nn.Sequential( nn.Conv2d(3, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(3, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(3, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), ) self.head = nn.Linear(128, num_classes) def forward(self, x): x = self.backbone(x) x = F.adaptive_avg_pool2d(x, 1).flatten(1) return self.head(x)注意一个我故意留下的写法:三个Conv2d都写成了in_channels=3,这在语法上没错,但第二层和第三层实际应该沿用上一层的输出通道数。训练时会直接报形状错误,逼着你把通道数改成 32、64、128。这是源码里最常见的“看起来对,跑起来炸”的问题,建议改完后把每一层的输出尺寸打印出来核对。
对比的迁移方案是 TorchVision 的 ResNet18,替换最后一层全连接:
from torchvision import models backbone = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) backbone.fc = nn.Linear(512, num_classes)选型的判断依据可以写进实验报告:样本量在几千量级时,预训练模型收敛更快,但要注意冻结前几层;样本量超过三万,轻量自研网络训练速度快,效果未必差。我一般先跑 TinyCloudNet 得到 baseline,再跑 ResNet18 对比,两组结果都能写进报告,比只报一个最终模型更有说服力。
3.3 训练循环与指标监控:不只看 loss,还要看逐类 F1
训练循环的写法遵循 PyTorch 标准模板,但有两个针对本场景的改动:类别权重处理不平衡,验证阶段输出逐类指标而非单一准确率。
import torch from sklearn.utils.class_weight import compute_class_weight class_weight = compute_class_weight( class_weight="balanced", classes=np.unique(train_labels), y=train_labels ) criterion = nn.CrossEntropyLoss( weight=torch.tensor(class_weight, dtype=torch.float32).to(device) )compute_class_weight会为少数类分配更大的权重,让模型在反传时对雨层云这类样本的错误更敏感。这是解决云图类别不平衡最直接的手段,比过采样省内存,比欠采样保信息。如果不做这一步,模型会把所有 patch 都猜成晴空,准确率照样有 80%,但毫无实用价值。
训练主循环用标准写法,每轮记录 loss 和验证集宏平均 F1,注意把验证指标作为保存模型的依据,而不是训练 loss。
from sklearn.metrics import f1_score, classification_report model.train() for epoch in range(20): running_loss = 0.0 for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * imgs.size(0) avg_loss = running_loss / len(train_dataset) val_f1 = evaluate_f1(model, val_loader, device) print(f"epoch={epoch:02d} loss={avg_loss:.4f} val_f1={val_f1:.4f}")evaluate_f1里用宏平均 F1 而非准确率,是因为宏平均会平等对待每一类,能真实反映不平衡场景下的模型能力。训练曲线里同时画 loss 和 F1,避免只凭 loss 下降就断言模型在收敛。
4. 从实验结果到答辩材料:混淆矩阵、可视化排版与报告叙事
4.1 混淆矩阵与逐类指标:别用准确率掩盖类别不平衡
训练结束后,拿保存的最优模型在测试集上做完整评估。测试集必须是训练和验证都没见过的场景,否则报告里的数字站不住脚。评估输出的第一张图永远是混淆矩阵,它比任何指标都直观。
from sklearn.metrics import confusion_matrix, classification_report y_true, y_pred = [], [] model.eval() with torch.no_grad(): for imgs, labels in test_loader: outputs = model(imgs.to(device)) _, preds = torch.max(outputs, 1) y_true.extend(labels.cpu().tolist()) y_pred.extend(preds.cpu().tolist()) cm = confusion_matrix(y_true, y_pred) print(classification_report( y_true, y_pred, target_names=["晴空", "层云", "积云", "卷云", "雨层云"] ))classification_report会逐类输出精确率、召回率和 F1。我一般会额外标注出“哪两类最容易混淆”——层云和卷云经常互认、积云与晴空边缘易混,这些错例直接进报告的“错误分析”章节。答辩时评委问“你这模型哪里不行”,你指着混淆矩阵说“卷云和层云纹理相近,占了 60% 的错误”,比空谈改进方向有说服力得多。
4.2 可视化排版:预测对比图、训练曲线与特征分布
实验报告和 PPT 里放什么图,直接决定材料的专业感。我的标准配置是四张图:训练曲线、混淆矩阵、预测结果对比图、错例分析图。前两张是定量结果,后两张是定性解释。
import matplotlib.pyplot as plt # 预测对比图:九宫格,每格显示 原图 / 预测类 / 置信度 / 真实类 fig, axes = plt.subplots(3, 3, figsize=(12, 12)) for i, ax in enumerate(axes.flat): img_t, label = test_dataset[i] with torch.no_grad(): logits = model(img_t.unsqueeze(0).to(device)) prob = torch.softmax(logits, dim=1).squeeze(0) pred = prob.argmax().item() ax.imshow(img_t.permute(1, 2, 0).cpu().numpy() * 0.5 + 0.5) # 反归一化时注意 clamp ax.set_title(f"预测: {class_names[pred]} ({prob[pred].item():.2f}) / 真实: {class_names[label]}") plt.tight_layout() plt.show()有个排版的坑:归一化后的张量直接imshow会显示成一片灰,需要先乘std加mean再还原到 0~1 区间,必要时用np.clip截断。代码只做展示用途,参数上注意训练和测试用的是同一套 mean/std,别在可视化阶段用错。
4.3 报告与 PPT 的叙事主线:问题→方法→实验→结论
源码之外的文档说明、报告 PPT 和实验报告,是这个标题下最容易失分的部分。我见过太多项目代码能跑但报告写得像代码注释的堆砌。这里给出一个经过验证的叙事结构:
| 报告章节 | 核心内容 | PPT 建议页数 |
|---|---|---|
| 问题定义 | 为什么做云图识别,任务拆成什么 | 2 页 |
| 数据与标注 | 数据来源、通道说明、粗标流程、划分策略 | 2 页 |
| 方法设计 | 轻量 CNN vs 预训练迁移、类别权重、数据增强 | 3 页 |
| 实验与结果 | 混淆矩阵、逐类指标、错例分析 | 3 页 |
| 结论与展望 | 当前局限、可扩展方向(分割、多通道) | 1 页 |
报告写作的关键是把“为什么这么选”放在每一章开头:为什么用宏平均 F1 而不是准确率;为什么按 scene 分组而不是随机划分;为什么先跑轻量 CNN 而不是直接上大模型。这些决策背后的理由,正是答辩时被追问最多的地方。PPT 不要贴大段代码,放结构图和结果图,代码细节留给文档说明。
5. 卫星云图识别的五个典型翻车现场:现象、原因与排查方法
5.1 任务定义模糊:做完才发现模型不知道该学什么
现象:项目做了两周,代码写了一堆,但模型输出忽而是分类、忽而是回归,实验报告无从写起。原因:标题里的“理解与识别”是项目标题,不是可训练的技术目标,直接照抄进方案设计必然失控。解决:把任务收敛为“5 类云型分类 + 云量回归辅助输出”,分类为主,回归作为延伸分析写进报告。先定任务,再定数据,最后才是模型。任何模型选型讨论,都要等任务定义清楚之后才进行。
5.2 类别不平衡导致准确率虚高
现象:训练结束后测试准确率 93%,看起来非常漂亮;但打印逐类召回率,积云只有 38%,雨层云甚至更低。原因:晴空样本占比过半,模型学到的最优策略是全猜晴空。解决:用compute_class_weight给少数类加权,同时评估指标换成宏平均 F1。改完之后准确率可能降到 82%,但逐类 F1 从 0.35 涨到 0.68,这才是真实能力的提升。报告里要把两类指标都列出,解释为何以宏平均 F1 为准。
5.3 灰度图直接套用 ImageNet 归一化,预测全线崩溃
现象:训练 loss 稳步下降,但验证准确率卡在 40% 上下,和随机猜测差不多。原因:灰度图复制三通道后,像素值范围与 ImageNet 预训练模型期望的输入分布完全错位,尤其是红外通道的数值分布和自然图像差异巨大。解决:统计训练集自身的 mean 和 std 作为归一化参数,并在数据加载器里对训练集和验证集使用同一组参数。排查顺序建议是先打印归一化后的张量统计值,确认均值接近 0、方差接近 1,再检查模型输入管道。
5.4 空间相关带来的数据泄漏,指标虚高
现象:训练集和验证集的 F1 都超过 0.9,但把模型拿到另一天、另一景的云图上测试,直接掉到 0.7 以下。原因:同一景云图切出来的 patch 高度相似,随机划分数据时把同一片云的相邻 patch 同时分进了训练和验证,模型实际上“背”了训练 patch 的纹理。解决:按scene_id分组做 GroupShuffleSplit,保证同源 patch 只出现在一个集合里。这也是遥感数据与自然图像数据划分最本质的差异,值得在实验报告里单独写一节。
5.5 batch size 和 patch 尺寸拼出显存溢出
现象:128×128 的 patch,batch size 设 64,ResNet18 一跑就报 CUDA out of memory。原因:显存占用与 batch size、patch 尺寸、通道数成线性正相关,云图因为是灰度复制三通道,实际计算量比同等尺寸的 RGB 图没有减少。解决:先把 patch 降到 96×96,batch size 降到 32,再用混合精度训练。训练不稳定时优先检查学习率,AdamW 从 3e-4 起步,不要一上来就调到 1e-3。这类训练资源的坑,属于跑几次就能总结出的经验。
6. 再往上提一档的两种手段:TTA 与置信度校准
模型基线跑通、报告结构搭好之后,还有两个不改变网络结构就能提升结果可信度的技巧。第一个是测试时增强(TTA),对同一张 patch 做水平翻转和垂直翻转,三次推理结果取平均。这个方法代码量极小,却能稳定提升分类准确率 1 到 2 个点,因为它等价于让模型看到同一云型的多个视角。
def predict_with_tta(model, img_t, device): model.eval() variants = [ img_t, torch.flip(img_t, dims=[-1]), torch.flip(img_t, dims=[-2]), ] with torch.no_grad(): probs = [torch.softmax(model(v.unsqueeze(0).to(device)), dim=1) for v in variants] return torch.stack(probs).mean(0).squeeze(0)第二个技巧是置信度校准。softmax 输出的 0.9 不一定是真的 90% 置信度,云图类别重叠严重,模型经常“过度自信”。温度缩放是常见的校准手段:把 logits 除以一个大于 1 的温度系数 T 再做 softmax,让概率分布更平滑。T 通过最小化验证集上的期望校准误差(ECE)来搜索,常见范围在 1.2 到 2.0 之间。
def calibrate_with_temperature(logits_val, labels_val, T=1.5): probs = torch.softmax(logits_val / T, dim=1) acc = (probs.argmax(1) == labels_val).float().mean() conf = probs.max(1).values.mean() return acc, conf # 两者越接近,说明置信度越可信TTA 解决“准不准”,温度校准解决“信不信”。答辩时评委问“你说这个 patch 有 0.9 置信度,凭据是什么”,你能把校准曲线拿出来,这个项目的完整度就和只会跑模型的人拉开了差距。我自己的习惯是每次训练完都先跑 TTA 对比一次,校准参数调完再写进实验报告,用数据说服自己而不是感觉。希望这条路径能帮你的云图识别项目少走几段弯路。
本文还有配套的精品资源,点击获取