当前位置: 首页 > news >正文

用 t-SNE 把 KSC 高光谱“变成可转动的 3D 影像”——从零到会,逐段读懂代码并导出旋转 GIF

最近有很多私信要求出一期用于PPT汇报或者日常展示的内容,这篇我们不做训练、不做分类,用 t-SNE 3DKSC标注像素降到 3 维,并导出自动旋转的 GIF,适合教学、汇报或文章配图。

一、数据与结果预览

  • 数据:KSC.matKSC_gt.mat(0 为未标注,>0 为类别)

  • 输出:

    • PNG:tSNE3_Labeled_cover.png
      在这里插入图片描述

    • GIF:tSNE3_Labeled_rotate.gif
      见保存文(太大无法上传)

  • 只对有标签的像素做降维与上色(颜色=真实类别),图像不做训练

二、运行环境

  • Python ≥ 3.8

  • 必需库:numpyscipyscikit-learnmatplotlibpillow

  • 安装(示例):

    pip install numpy scipy scikit-learn matplotlib pillow
    

三、代码结构鸟瞰

  1. 路径与可视化参数:数据位置、导出目录、t-SNE/动画参数

  2. 工具函数

    • load_hsi:自动识别 .mat 主键、读入数据
    • make_class_cmap:生成足够多、区分度高的离散类别色表
  3. main 主流程

    • 读取与筛选有标注像素
    • 标准化 + PCA 预降维(加速/去噪)
    • 子采样(避免 t-SNE 爆内存/过慢)
    • 自适应 perplexity(满足 t-SNE 约束)
    • t-SNE 3D 拟合
    • Matplotlib 3D 点云绘制(按类上色)
    • FuncAnimation 旋转相机并用 PillowWriter 导出 GIF

下面按模块逐段拆解要点与为什么这么写。

四、逐段解析与“为什么”

1)全局参数与样式

DATA_DIR = r"...\DATASETS"
X_FILE = "KSC.mat"
Y_FILE = "KSC_gt.mat"OUT_DIR = os.path.join(DATA_DIR, "VIS3D_tSNE_GIF")
PNG_PATH = os.path.join(OUT_DIR, "tSNE3_Labeled_cover.png")
GIF_PATH = os.path.join(OUT_DIR, "tSNE3_Labeled_rotate.gif")SEED = 42
PCA_FOR_TSNE = 30          # t-SNE 前置 PCA 维度
TSNE_SUBSAMPLE = 20000     # t-SNE 点数上限(仅标注像素)
TSNE_ITER = 1000NUM_FRAMES = 120           # 动画帧数
FPS = 20                   # 帧率
ELEV = 20                  # 俯仰角
AZIM_START = -60           # 起始方位角
AZIM_SWEEP = 360           # 旋转总角度
  • PCA_FOR_TSNE:t-SNE 前用 PCA 降到 30 维,常见做法,减噪且加速(高光谱往往 >100 维,无用噪声多)。
  • TSNE_SUBSAMPLE:上限 2 万点。t-SNE 的复杂度高,子采样能把时间和内存控制住,还能保持结构观感。
  • 动画参数NUM_FRAMES × (1/FPS) ≈ 动画时长(此处 120 帧 @20fps 约 6 秒)。
  • Matplotlib 字体设为中文(SimHei);如报字体警告可移除,不影响功能。

2)读取 .mat:键名自动识别

def load_hsi(x_path, y_path):Xm = sio.loadmat(x_path)Ym = sio.loadmat(y_path)x_key = [k for k in Xm if not k.startswith("__")][0]y_key = [k for k in Ym if not k.startswith("__")][0]return Xm[x_key], Ym[y_key]
  • 许多 .mat 文件的主键并非固定(可能叫 KSCindian_pines_corrected 等),用“不是 __ 开头”的第一个键作为主键,免手改

3)类别色表:够多、够分散

def make_class_cmap(n_cls):base = []for name in ["tab20", "tab20b", "tab20c"]:base.append(matplotlib.colormaps[name].colors)colors = np.vstack(base)if n_cls > len(colors):rep = int(np.ceil(n_cls / len(colors)))colors = np.tile(colors, (rep, 1))return ListedColormap(colors[:n_cls])
  • 组合 tab20tab20btab20c 这三套 20 色,共 60 色。KSC 的类别数远小于 60,一般足够且区分度高
  • 类别多时自动重复,但高光谱场景里基本用不到这一步。

4)主流程:从读数据到导出 GIF

(a) 只取有标注像素
X, Y = load_hsi(...)
X_flat = X.reshape(-1, B)
y_flat = Y.reshape(-1).astype(np.int32)
mask = y_flat != 0
X_lab = X_flat[mask]
y_lab = (y_flat[mask] - 1).astype(np.int64)
  • 标签 0 表示未标注,不进入可视化;其他标签转为 0 基1..K → 0..K-1),便于索引/上色。
  • 只画标注像素的好处:颜色==真实类别,用于直观看类间分布可分性
(b) 标准化 + PCA 预降维(t-SNE 的“热身”)
X_lab_std = StandardScaler().fit_transform(X_lab)
pca_dim = min(PCA_FOR_TSNE, X_lab_std.shape[1])
X_lab_pca = PCA(n_components=pca_dim, random_state=SEED).fit_transform(X_lab_std)
  • StandardScaler 把每个波段零均值/单位方差,避免量纲不一致破坏距离度量。

  • t-SNE 前置 PCA 是经典套路:

    • 提取主要结构(去噪)
    • 降低维度减少 t-SNE 的计算量
    • 实践中通常用 30–50 维即可。
© 子采样策略(两行代码省大麻烦)
if N > TSNE_SUBSAMPLE:idx = np.random.RandomState(SEED).choice(N, TSNE_SUBSAMPLE, replace=False)X_tsne_in = X_lab_pca[idx]; y_tsne = y_lab[idx]
else:X_tsne_in = X_lab_pca; y_tsne = y_lab
  • t-SNE 的时间/显存可随样本数迅速上升(近似 O(N log N) 或更高)。
  • 随机均匀子采样是稳健、直观的“第一步降压阀”。
  • 如果你有更强硬件,直接把 TSNE_SUBSAMPLE 开大即可。
(d) 自适应 perplexity(合法且稳健)
n_in = X_tsne_in.shape[0]
perplexity = max(5, min(30, n_in // 100))
perplexity = min(perplexity, max(5, n_in // 3))
  • t-SNE 约束:perplexity < n_samples - 1,常用范围 5–50
  • 这里用样本规模自适应:≈ N/100 并夹在 [5, 30] 内,再确保 < N/3不出错也比较稳。
(e) t-SNE 拟合
tsne = TSNE(n_components=3, perplexity=perplexity, learning_rate='auto',n_iter=TSNE_ITER, init='pca', random_state=SEED, verbose=1)
X_tsne = tsne.fit_transform(X_tsne_in)
  • n_components=3:直接在 3D 上可视化,无需再投影。
  • init='pca':一般比随机初始化更稳。
  • learning_rate='auto':sklearn 新版推荐;你也可显式设 200–1000。
  • n_iter=1000 足够出形;不收敛可适当加大。
(f) 3D 散点上色与静态封面
for cls in range(n_cls):sel = (y_tsne == cls)ax.scatter(X_tsne[sel, 0], X_tsne[sel, 1], X_tsne[sel, 2],s=6, alpha=0.8, color=cmap_cls(cls), label=f"类{cls+1}")
...
plt.savefig(PNG_PATH, bbox_inches='tight')
  • 逐类绘制,legend 只画一次,保证图例可读。
  • alpha=0.8 让重叠处略透,密度可见。
(g) 旋转动画:转相机,不动点
azims = AZIM_START + np.linspace(0, AZIM_SWEEP, NUM_FRAMES, endpoint=False)def update(frame_idx):ax.view_init(elev=ELEV, azim=azims[frame_idx])return ax,anim = FuncAnimation(fig, update, frames=NUM_FRAMES, interval=1000/FPS)
writer = PillowWriter(fps=FPS)
anim.save(GIF_PATH, writer=writer, dpi=120)
  • view_init 连续改变 azim,实现“围绕 z 轴”相机环绕
  • AZIM_SWEEP=360 → 转一整圈;想要“来回摆动”可改为 sin/triangle 轨迹。
  • PillowWriter 直出 GIF;若想导出 MP4,把 writer 换成 FFMpegWriter(需系统装 ffmpeg)。

五、参数怎么调更稳更美?

  • 点数(TSNE_SUBSAMPLE):2–3 万点是“观感/性能”的折中;更清晰→加;更快→减。

  • perplexity:默认自适应足够稳;如果类簇特别多、密度差异大,可以尝试 20–50 之间微调。

  • PCA_FOR_TSNE:20–50 都常见;波段很高时(>200)推荐 30–50。

  • 画面美观

    • s=点大小alpha 可按密度调;
    • 调整 ELEV(俯仰)和 AZIM_START(起始角)找“最有层次感”的视角。

六、常见报错与排查

  • No module named ‘PIL’
    安装 Pillow:pip install pillow
  • UserWarning: perplexity must be less than n_samples
    样本太少时自动计算可能越界;手动把 perplexity 设小点(如 5–10)。
  • GIF 很大/导出慢
    降低 NUM_FRAMESFPS;或减小 dpi;或减少点数(TSNE_SUBSAMPLE)。

七、可选扩展(两三行就能上手)

  • 导出 MP4

    from matplotlib.animation import FFMpegWriter
    writer = FFMpegWriter(fps=FPS, bitrate=2000)
    anim.save("rotate.mp4", writer=writer, dpi=120)
    
  • 颜色改为“预测强度/PC1 值”:用连续 colormap(viridis/plasma)而非类别色表。

  • 换 UMAP:体验速度与结构稳定性差异(需要 umap-learn)。

  • 双向来回旋转:把 azims 换成拼接的“去 + 回”序列。

八、完整代码(可直接运行)

与你提供的版本一致,仅为文章“拷贝即用”留档。

# -*- coding: utf-8 -*-
"""
KSC HSI 3D 可视化 - 自动旋转 GIF(仅 t-SNE 3D,按真实类别上色)
"""import os
import numpy as np
import scipy.io as sio
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.manifold import TSNEimport matplotlib
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from mpl_toolkits.mplot3d import Axes3D  # noqa: F401
from matplotlib.animation import FuncAnimation, PillowWriter# ============== 路径与参数 ==============
DATA_DIR = r"F:\WORK_SPACE\20240812\pythonProject\中汇\BASE_TOOL\DATASETS"
X_FILE = "KSC.mat"
Y_FILE = "KSC_gt.mat"OUT_DIR = os.path.join(DATA_DIR, "VIS3D_tSNE_GIF")
os.makedirs(OUT_DIR, exist_ok=True)
PNG_PATH = os.path.join(OUT_DIR, "tSNE3_Labeled_cover.png")
GIF_PATH = os.path.join(OUT_DIR, "tSNE3_Labeled_rotate.gif")SEED = 42
PCA_FOR_TSNE = 30          # t-SNE 前置 PCA 维度
TSNE_SUBSAMPLE = 20000     # t-SNE 点数上限(仅在标注像素中采样)
TSNE_ITER = 1000# 动画参数
NUM_FRAMES = 120           # 帧数(越大越顺滑,文件也更大)
FPS = 20                   # 帧率
ELEV = 20                  # 俯仰角
AZIM_START = -60           # 起始方位角
AZIM_SWEEP = 360           # 旋转总角度# Matplotlib 样式
matplotlib.rcParams['font.family'] = 'SimHei'
matplotlib.rcParams['axes.unicode_minus'] = False
plt.rcParams['figure.dpi'] = 120def load_hsi(x_path, y_path):Xm = sio.loadmat(x_path)Ym = sio.loadmat(y_path)x_key = [k for k in Xm if not k.startswith("__")][0]y_key = [k for k in Ym if not k.startswith("__")][0]return Xm[x_key], Ym[y_key]def make_class_cmap(n_cls):base = []for name in ["tab20", "tab20b", "tab20c"]:base.append(matplotlib.colormaps[name].colors)colors = np.vstack(base)if n_cls > len(colors):rep = int(np.ceil(n_cls / len(colors)))colors = np.tile(colors, (rep, 1))return ListedColormap(colors[:n_cls])def main():np.random.seed(SEED)# 1) 读数据,仅保留标注像素X, Y = load_hsi(os.path.join(DATA_DIR, X_FILE), os.path.join(DATA_DIR, Y_FILE))H, W, B = X.shapeX_flat = X.reshape(-1, B)y_flat = Y.reshape(-1).astype(np.int32)mask = y_flat != 0X_lab = X_flat[mask]y_lab = (y_flat[mask] - 1).astype(np.int64)n_cls = int(np.unique(y_lab).size)print(f"图像: {H}x{W}x{B} | 标注像素: {X_lab.shape[0]} | 类别: {n_cls}")# 2) 标准化 + PCA 预降维X_lab_std = StandardScaler().fit_transform(X_lab)pca_dim = min(PCA_FOR_TSNE, X_lab_std.shape[1])X_lab_pca = PCA(n_components=pca_dim, random_state=SEED).fit_transform(X_lab_std)# 3) 子采样(防 t-SNE 过慢/占内存)N = X_lab_pca.shape[0]if N > TSNE_SUBSAMPLE:idx = np.random.RandomState(SEED).choice(N, TSNE_SUBSAMPLE, replace=False)X_tsne_in = X_lab_pca[idx]y_tsne = y_lab[idx]else:X_tsne_in = X_lab_pcay_tsne = y_lab# 4) 自适应 perplexity(必须 < 样本数)n_in = X_tsne_in.shape[0]perplexity = max(5, min(30, n_in // 100))perplexity = min(perplexity, max(5, n_in // 3))print(f"t-SNE 输入: {n_in} | perplexity={perplexity}")# 5) t-SNE 3Dtsne = TSNE(n_components=3, perplexity=perplexity, learning_rate='auto',n_iter=TSNE_ITER, init='pca', random_state=SEED, verbose=1)X_tsne = tsne.fit_transform(X_tsne_in)# 6) 初始绘图(静态)cmap_cls = make_class_cmap(n_cls)fig = plt.figure(figsize=(8, 8))ax = fig.add_subplot(111, projection='3d')# 分类别画,legend 只画一次for cls in range(n_cls):sel = (y_tsne == cls)if sel.any():ax.scatter(X_tsne[sel, 0], X_tsne[sel, 1], X_tsne[sel, 2],s=6, alpha=0.8, color=cmap_cls(cls), label=f"类{cls+1}")ax.set_title(f"t-SNE 3D(标注像素,N={n_in})", fontsize=12, weight='bold')ax.set_xlabel("tSNE-1")ax.set_ylabel("tSNE-2")ax.set_zlabel("tSNE-3")ax.view_init(elev=ELEV, azim=AZIM_START)ax.legend(loc='upper right', bbox_to_anchor=(1.20, 1.0), fontsize=8)plt.tight_layout()# 保存封面 PNGplt.savefig(PNG_PATH, bbox_inches='tight')print("已保存封面 PNG:", PNG_PATH)# 7) 动画:绕 z 轴旋转相机azims = AZIM_START + np.linspace(0, AZIM_SWEEP, NUM_FRAMES, endpoint=False)def update(frame_idx):ax.view_init(elev=ELEV, azim=azims[frame_idx])return ax,anim = FuncAnimation(fig, update, frames=NUM_FRAMES, interval=1000/FPS, blit=False)# 8) 导出 GIF(需要 pillow)writer = PillowWriter(fps=FPS)anim.save(GIF_PATH, writer=writer, dpi=120)print("已保存旋转 GIF:", GIF_PATH)plt.close(fig)if __name__ == "__main__":main()

九、结语

这套脚本做了一件事:把“高维光谱-类别结构”转成一颗可旋转的 3D 彩球
它非常适合在课上汇报里直观展示“类簇关系”“可分性”和“局部/全局结构”。
如果你想把这一页做成论文图视频,我也可以帮你配上更美观的配色、相机轨迹、标题与标注样式。

欢迎大家关注下方我的公众获取更多内容!

http://www.dtcms.com/a/326719.html

相关文章:

  • 二叉树进阶 之 【模拟实现二叉搜索树】(递归、非递归实现查找、插入、删除功能)
  • 跨平台RTMP推流SDK vs OBS:技术差异与行业落地解析
  • 01数据结构-十字链表和多重邻接表
  • Lwip深度阅读-网络架构
  • 【代码随想录day 17】 力扣 654.最大二叉树
  • 贪心----2.跳跃游戏
  • 区块链技术原理(5)-网络
  • Docker部署MySQL完整指南:从入门到实践
  • Leetcode-25.K个一组翻转链表
  • 【13-向量化-高效计算】
  • 第二十一天:统计数字
  • 嵌入式系统学习Day16(C语言中的位运算)
  • 绿巨人VS Code多开项目单独管理每个项目单独使用一个不限制的augment
  • 构建AI代理工作流的开源利器——Sim Studio
  • 文件编辑html
  • C语言命令行参数
  • 北京JAVA基础面试30天打卡07
  • 【C++竞赛】核桃CSP-J模拟赛题解
  • 提示词工程实战:用角色扮演让AI输出更专业、更精准的内容
  • vagrant和itamae怎么配合使用? (放弃)
  • 33Nginx模块的从配置与优化
  • 如何使用curl编程来下载文件
  • MacBook 本地化部署 Dify 指南
  • AIDL简单使用
  • 【接口自动化测试】---YAML、JSON Schema
  • 逐际动力开源运控 tron1-rl-isaacgym 解读与改进
  • VMD例程(Matlab 2021b可直接使用)
  • 从“目标烂尾”到“100%交付”:谷歌OKR追踪系统如何用“透明化+强问责”打造职场责任闭环
  • 小白入门指南:Edge SCDN 轻松上手
  • Dify 从入门到精通(第 28/100 篇):Dify 的多租户架构