简介这是一份面向海洋科学研究者、数据分析与深度学习应用开发者的PDF学术文献聚焦中尺度涡的自动识别与可视化分析。传统涡旋检测高度依赖专家预设参数或逐点扫描精度与效率难以兼顾文中提出的基于深度学习目标检测的新算法能以较高精确率与查全率识别中尺度涡免去阈值调整环节显著加快检测进程。围绕分析需求文章还设计了一套中尺度涡时空特征及海洋信息协同可视化系统支持涡旋统计信息、特征分布与属性关联的交互式展示和相关性分析为海洋大数据挖掘提供了实用工具。资源全文为1个PDF文件大小1.85MB完整收录了摘要、关键词、引言、算法原理、系统设计与结论等章节可作为参考文献或专业指导材料使用。目前已有335人学习下载尤其适合关注海洋智能感知与可视化分析的读者深度阅读。1. 海洋中尺度涡识别为什么值得用深度学习做中尺度涡是海洋里直径从几十公里到几百公里的旋转水体卫星高度计观测到的海面高度异常SLA场里它们表现为一个个正负交替的极值区域对航运、渔业、海洋碳循环都有直接影响。过去识别涡旋主要靠 Okubo-Weiss 参数、绕流角这类物理判别法人工调阈值的成分很重同一个算法换个海域往往失灵。基于深度学习的海洋中尺度涡识别与可视化本质上是把 SLA 场当作图像、把涡旋当作分割目标训练一个语义分割模型再把模型输出的掩膜转成业务能用的标注图和清单。这条链路在近几年的论文里被反复验证效果普遍优于传统阈值法工程上真正的门槛集中在数据预处理和可视化交付而不是模型本身。这篇笔记面向想把这个方向落地的研究人员和算法工程师按「数据 → 模型 → 可视化 → 验收」的顺序讲清楚每一步怎么做、参数怎么调、坑在哪里。2. 从 NetCDF 到训练样本数据源选择与标签生成的三个关键步骤2.1 数据源选型为什么默认用卫星 SLA 而不是流场深度学习识别涡旋的第一步不是模型而是数据。中尺度涡最可靠的可观测信号就是海面高度异常反气旋涡暖涡北半球顺时针对应 SLA 正异常气旋涡冷涡北半球逆时针对应 SLA 负异常。流场数据地转流 U/V也能用但地转流是 SLA 的梯度派生量梯度运算会放大噪声同样的涡旋在 SLA 场里是一个干净的圆形极值在流场里可能被背景环流切成碎片。所以绝大多数可复现的方案都把 SLA 场作为模型输入流场作为辅助验证。数据源选择上我一般优先用 CMEMS 或 AVISO 提供的延迟处理delayed-timeSLA 产品这类产品经过多卫星融合和交叉校正空间分辨率在 0.25 度左右时间分辨率是逐日对中尺度涡识别足够了。近实时near-real-time产品在边缘海域经常有缺测做训练集容易引入坏样本。如果做敏感性实验也可以用 HYCOM 或 OFES 的模式输出但模式数据有系统性偏差训练出来的模型迁移到真实卫星资料上往往要重新微调不建议直接当主力训练数据。读取 NetCDF 的过程绝大多数情况用 xarray 一行就解决但要注意不同产品的变量名和坐标名不统一。import xarray as xr # dt_global_allsat_msla_h.nc 只是示例文件名以你实际下载的产品为准 ds xr.open_dataset(dt_global_allsat_msla_h.nc) print(ds) # 先看变量名和维度名很多产品把 SLA 叫 sla、adt 或 ssh sla ds[sla] # shape 一般是 (time, lat, lon) lat ds[lat].values lon ds[lon].values # 裁剪到目标海域比如西北太平洋 20N-40N, 120E-160E sla_sub sla.sel(latslice(20, 40), lonslice(120, 160))这里有个容易翻车的地方CMEMS 的 lat 维度默认降序排列从 90 到 -90如果后续用 NumPy 手动做数组运算先打印lat[:5]确认方向否则画图时纬度会上下颠倒。xarray 的sel切片不受影响但一旦sla_sub.values转成裸数组方向问题就完全暴露了。另一个注意点是lon可能落在 0~360 或 -180~180 两种区间跨 180 度经线的区域切片前统一转换到 -180~180不然涡旋会被生切成两半。2.2 标签生成的三种路线伪标签、公开目录、人工修正训练分割模型需要逐像素的标签这是整个链路里最耗人工的部分。中尺度涡没有像自动驾驶那样现成的开源标注集常见做法是三种路线混用。第一条路是用传统物理方法生成伪标签。用 Okubo-Weiss 参数依据涡度与形变率的相对大小识别涡旋或绕流角方法对 SLA 场做一次识别把识别出的闭合区域直接当作 mask。这个方法快但边界粗糙阈值敏感伪标签错标率不低。第二条路是用公开涡旋目录比如 Chelton 团队发布的全球中尺度涡轨迹数据集里面记录了每个涡旋的中心经纬度、等效半径、极性用中心点和半径生成圆形 mask。第三条路是在前两条的基础上人工修正抽几百帧画出来的结果把明显错误的区域改掉。我一般先跑伪标签做一版 baseline再抽 500 帧人工修正把修正后的数据混进训练集重训一版。纯公开目录生成的圆形 mask 和真实涡旋边界差距较大只能用来做预训练。下面的代码把涡旋目录转成三分类 mask背景为 0气旋涡为 1反气旋涡为 2。import numpy as np from scipy.spatial import cKDTree def build_label_mask(lat_grid, lon_grid, eddy_list): eddy_list: [(lon_c, lat_c, radius_km, polar)]polar0 气旋, 1 反气旋 lat_flat lat_grid.ravel() lon_flat lon_grid.ravel() coords np.stack([lon_flat, lat_flat], axis-1) mask np.zeros(lat_grid.shape, dtypenp.uint8) tree cKDTree(coords) for lon_c, lat_c, r_km, polar in eddy_list: r_deg r_km / 111.0 # 1 纬度约 111 km这里把半径换算成度数 idx tree.query_ball_point([lon_c, lat_c], rr_deg) for i in idx: row, col np.unravel_index(i, lat_grid.shape) mask[row, col] 1 if polar 0 else 2 return mask代码逻辑不复杂把经纬度网格拍平成点集用 KDTree 找每个涡旋中心半径范围内的所有格点再按极性写入对应类别。r_deg r_km / 111.0是近似换算在几百公里尺度内误差可接受如果你的区域在高纬度比如 60 度以上经线距离会明显缩短需要按纬度修正系数。跨 180 度经线时先统一经度区间再做索引否则中心点落在东经 179 度、半径覆盖到西经 -179 度的涡旋会被漏掉大半。2.3 切片与归一化决定模型泛化能力的两个细节SLA 场是全球网格直接整图喂进 U-Net 既不现实也没必要。中尺度涡的直径从几十到几百公里在 0.25 度网格上大约是 10~40 个像素一个 128×128 的窗口足够装下两三个涡旋。我一般用滑动窗口切片窗口 128×128步长 64重叠 50%避免涡旋正好被切在边缘。切片时记录每片的经纬度范围后面可视化和导出清单都要靠这个对应关系。归一化是这里最容易被忽略的坑。SLA 的幅值随季节和海域变化很大同一帧里黑潮延伸体的涡旋振幅可能超过 50cm而东太平洋的弱涡只有 10cm 左右。如果做全局标准化所有帧用同一个均值和方差弱涡在模型眼里基本就是背景噪声。正确做法是按帧标准化每一帧减去自己的均值除以自己的标准差。这样模型学到的是 SLA 场的空间结构而不是绝对振幅。def normalize_frame(frame): frame: 单帧 2D SLA 数组返回标准化后的数组 f frame.copy() f f - f.mean() std f.std() if std 1e-6: # 全零帧保护训练时遇到这种样本直接跳过 return None return f / std切片和标准化做完把样本按 8:1:1 分成训练、验证、测试集。这个划分必须按时间块切不能随机打乱单帧原因在第五章展开讲。这一步产出的数据是四维数组(样本数, 1, 128, 128)和对应的标签直接存成.npz或.h5就行不需要工程化的数据管道先跑通再优化。3. 用 U-Net 做涡旋分割模型选型、最小可跑通代码与三个必调参数3.1 为什么是分割而不是检测涡旋没有「框」很多第一次做这个方向的人会问为什么不用 YOLO 或 Faster R-CNN 检测因为涡旋不是刚性目标没有明显的「框」。同一片海域里涡旋可以是圆形、椭圆形甚至被背景流场拉成条带状。检测框会框住大量背景水体后期算等效半径、面积、涡度通量这些物理量时全都失真。语义分割输出的是逐像素类别mask 可以直接算面积、质心、等效半径、极值中心位置业务上要的涡旋清单就是从 mask 派生的这是一条最短路径。模型选型上U-Net 在这个任务里是默认起点。原因有三第一它参数量适中在几千张样本的小数据集上不容易过拟合第二跳跃连接保留了空间细节涡旋边界这种强边缘信息能传到最后输出层第三工程生态成熟PyTorch 实现随手就能写。你不需要一开始就上 Attention U-Net 或 Swin Transformer中尺度涡识别的瓶颈从来不在模型容量而在标签质量和数据分布。3.2 最小可跑通的 U-NetPyTorch 代码与结构说明下面是能直接跑的最小 U-Net 实现输入单通道 SLA输出三通道分割图背景、气旋、反气旋模型层面没有花活但每一行都经得起检查。import torch import torch.nn as nn class SimpleUNet(nn.Module): def __init__(self, in_ch1, num_cls3): super().__init__() self.enc1 self._block(in_ch, 32) # 1 - 32 self.pool1 nn.MaxPool2d(2) self.enc2 self._block(32, 64) # 32 - 64 self.pool2 nn.MaxPool2d(2) self.bottle self._block(64, 128) # 最底层特征 self.up2 nn.ConvTranspose2d(128, 64, kernel_size2, stride2) self.dec2 self._block(128, 64) # cat(up2, enc2) 后通道数 128 self.up1 nn.ConvTranspose2d(64, 32, kernel_size2, stride2) self.dec1 self._block(64, 32) # cat(up1, enc1) 后通道数 64 self.out nn.Conv2d(32, num_cls, kernel_size1) def _block(self, in_ch, out_ch): return nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.ReLU(inplaceTrue), ) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool1(e1)) b self.bottle(self.pool2(e2)) d2 self.dec2(torch.cat([self.up2(b), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return self.out(d1)结构说明就两点值得讲。第一上采样用了ConvTranspose2d而不是插值加卷积因为转置卷积带可学习参数上采样过程能顺带修正边界锯齿kernel_size2、stride2 的组合保证特征图尺寸恰好翻倍不需要额外 padding 对齐。第二dec2和dec1输入通道数分别是 128 和 64因为跳跃连接把同尺寸的编码器特征直接拼在解码器特征后面通道数翻倍这个数字对不上就会报维度错是初学者最常见的报错点。跳连的作用对涡旋识别尤其明显SLA 场里涡旋和背景的对比度差异不算大真正的边界信息存在于编码器前几层的浅层特征里如果没有跳连深层的下采样特征会把边界模糊掉输出 mask 的边缘会外扩好几像素后处理阶段很难修。3.3 三个必调参数类别权重、学习率、归一化策略模型的骨架是死的真正影响结果的是训练配置。三个参数我每次做新数据集都要重新调一遍默认值只作为起点。第一个是损失函数。背景像素占了整个网格的 80% 以上直接交叉熵会让模型把所有像素都预测成背景损失值还挺好看。解决方式是给交叉熵加类别权重weight torch.tensor([0.1, 1.0, 1.0])背景权重压低。我一般再叠加 Dice LossDice 直接优化类别区域的交并比对前景区域的不平衡更敏感。组合方式是loss 0.5 * ce dice比例 1:1 起步如果前景召回率低就调大 dice 的权重。def dice_loss(probs, target_onehot, eps1e-6): probs: softmax 输出 (N, C, H, W)target_onehot: (N, C, H, W) intersection (probs * target_onehot).sum(dim(2, 3)) denominator probs.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) eps return 1 - (2 * intersection eps) / denominator第二个是学习率和调度策略。SLA 场是单通道灰度图特征分布和自然图像差别很大ImageNet 预训练权重基本用不上从头训练就需要更保守的学习率。Adam 优化器配合 1e-4 初始学习率起步配合余弦退火调度器在 60~80 轮内把学习率降到 1e-6。不要用固定学习率从头跑到尾涡旋边界收敛需要后期的小步长微调固定学习率很容易在某个局部解附近震荡。第三个是归一化策略和第二章绑定训练、验证、推理阶段的输入必须用同一套归一化逻辑。按帧标准化虽然能解决幅值差异但也带来一个问题——同一帧内不同涡旋的相对强度被压缩了模型只能靠形状和梯度结构区分目标这对标签质量的要求更高。如果你的标签是用 Chelton 目录圆近似生成的我建议改用每帧除以绝对中位数而不是均值方差这样对极端 SLA 值更鲁棒。4. 识别结果可视化从静态标注图到可交互大屏4.1 把 mask 叠回海图Cartopy 绘制与极值中心标注模型输出的 mask 是在 128×128 切片坐标系里的像素值可视化要做的第一件事是把这些像素坐标映射回经纬度。Python 做数据分析与可视化在这个环节最顺手Matplotlib 负责绘图Cartopy 负责投影和海岸线。这一步看起来简单但「mask 叠上去对不齐」的翻车现场我见过太多核心原因只有一个绘图时经纬度数组的来源不一致。下面这段代码强制从同一个ds对象取坐标并在画图前检查 shape。import matplotlib.pyplot as plt import cartopy.crs as ccrs from skimage.measure import find_contours def plot_eddy_result(sla_frame, pred_mask, lat, lon, save_path): sla_frame 和 pred_mask 必须 shape 完全一致lat/lon 是同一网格 fig plt.figure(figsize(10, 8)) ax fig.add_subplot(111, projectionccrs.PlateCarree()) ax.set_extent([lon.min(), lon.max(), lat.min(), lat.max()], crsccrs.PlateCarree()) # SLA 场填色RdBu_r 让正异常偏红、负异常偏蓝符合物理直觉 im ax.contourf(lon, lat, sla_frame, levels30, cmapRdBu_r, transformccrs.PlateCarree()) plt.colorbar(im, axax, shrink0.8) # 从 mask 里抽边界线不要直接用 plt.contour(mask)会带出锯齿 for cls, color in [(1, black), (2, red)]: cls_mask (pred_mask cls).astype(int) for contour in find_contours(cls_mask, 0.5): rows, cols contour[:, 0], contour[:, 1] contour_lat lat[0] rows * (lat[-1] - lat[0]) / (lat.shape[0] - 1) contour_lon lon[0] cols * (lon[-1] - lon[0]) / (lon.shape[1] - 1) ax.plot(contour_lon, contour_lat, colorcolor, linewidth1.2, transformccrs.PlateCarree()) ax.coastlines(resolution10m) plt.savefig(save_path, dpi150, bbox_inchestight) plt.close(fig)这段代码的关键在边界抽取find_contours直接作用于二值 mask输出的轮廓坐标是像素索引row, col再用线性映射换算到纬度、经度。注意lat[0] rows * (lat[-1] - lat[0]) / (lat.shape[0] - 1)这个公式在 lat 降序时也成立因为lat[-1] - lat[0]会自动取负值轮廓方向会跟着翻转。最容易出错的反而是contourf和ax.plot的transform参数——SLA 场数据本身是经纬度坐标必须指定ccrs.PlateCarree()不写这个参数 Cartopy 默认用投影坐标系图像会整体偏移。4.2 从 mask 到涡旋清单CSV 导出与字段设计出图只是中间产物业务和验收要的是「清单」每一帧有哪些涡旋、中心在哪、半径多大、极性是什么、强度多少。可视化大屏也好论文统计也好真正接入下游的都是这张表。用regionprops遍历连通域是最省事的路径它能同时给出面积、包围盒、强度分布直接在强度图上找极值中心还省掉了质心偏移的麻烦。import pandas as pd import numpy as np from skimage.measure import label, regionprops def mask_to_eddy_csv(pred_mask, prob_map, lat, lon, save_path): prob_map: softmax 概率图shape (3, H, W)用于定位极值中心 rows [] for polar, class_id in [(0, 1), (1, 2)]: labels label((pred_mask class_id).astype(np.uint8), connectivity2) for prop in regionprops(labels, intensity_imageprob_map[class_id]): # 极值中心取概率最大值位置比质心更接近涡旋实际中心 max_idx np.unravel_index(prop.intensity_image.argmax(), prop.intensity_image.shape) y prop.bbox[0] max_idx[0] x prop.bbox[1] max_idx[1] rows.append({ lon_center: lon[x], lat_center: lat[y], equiv_radius_km: np.sqrt(prop.area / np.pi) * 111.0, max_prob: float(prop.intensity_image.max()), polar: polar, }) pd.DataFrame(rows).to_csv(save_path, indexFalse)字段说明lon_center和lat_center是涡旋中心经纬度equiv_radius_km是把 mask 面积换算成等效圆半径max_prob表示模型对这个涡旋的置信度。polar列 0 代表气旋涡负 SLA 异常、1 代表反气旋涡正 SLA 异常。注意regionprops的intensity_image是包围盒裁剪后的局部数组argmax得到的索引必须加上bbox[0]和bbox[1]才能映射回全图坐标漏加的话中心点会整体偏移到包围盒左上角。equiv_radius_km用 111.0 换算在高纬度会偏大更精确的做法是按当前纬度的 1 度经向距离折算。4.3 从静态图到可视化大屏低成本路线怎么选很多团队拿到模型结果就想直接上可视化大屏第一反应是做 Web 三维渲染很容易把工期拖垮。我的建议是分两步走先用静态图交付结果再考虑交互。静态图就是 4.1 给出的 PNG 系列图每帧一张观测周期多长就出多少张配合 4.2 导出的 CSV已经能支撑业务看板、论文插图、PPT 汇报。前端如果要用 echarts 数据可视化CSV 转成标准 GeoJSON 后直接丢给 echarts 的 geo 组件就能画边界如果要用 Leaflet 或 OpenLayers也是基于同一个 GeoJSON。# 用 Python 直接把 CSV 转 GeoJSON避免手写 JSON python -c import pandas as pd, json df pd.read_csv(eddy_list.csv) features [] for _, r in df.iterrows(): features.append({ type: Feature, properties: {radius_km: r[equiv_radius_km], polar: int(r[polar]), prob: r[max_prob]}, geometry: {type: Point, coordinates: [r[lon_center], r[lat_center]]} }) json.dump({type: FeatureCollection, features: features}, open(eddy_list.geojson, w)) 这一步把识别结果和前端彻底解耦后端不碰 WebSocket、不碰关系型数据库前端拿到 GeoJSON 想接什么框架都行。如果后续要做时序动画在前端按帧加载多个 GeoJSON 文件即可。至于 Three.js 那种真实地理渲染等业务确认了「需要动态展示涡旋内部流场」这种硬需求再上否则前期投入很容易变成自嗨。5. 涡旋识别与可视化避坑指南五个让模型翻车的现场5.1 分割 mask 边缘碎裂小连通域像芝麻一样散落现象可视化时 mask 边界不连续画面里散布大量十几像素的小连通域看起来像撒了一层芝麻涡旋主体也经常被切成几块。原因SLA 场本身含噪模型把局部极值当成了弱涡旋另一个常见原因是训练标签用圆形近似生成边界区域模型始终学不踏实推理时概率在 0.5 附近震荡。解决先做面积阈值过滤小于 150 像素的连通域直接丢弃再对 mask 做一次膨胀腐蚀操作把裂缝补上。给regionprops加一道判断就行if prop.area area_threshold: mask[labels prop.label] 0。面积阈值按你的网格分辨率定0.25 度网格下 150 像素大约对应一个直径 30 公里的迷你涡旋物理上本来就不该保留。5.2 气旋涡和反气旋涡类别被系统性地预测反现象模型精度曲线很漂亮但可视化结果里所有涡旋的极性都是反的黑潮延伸体的暖涡被标成气旋涡一切都乱了。原因Gulf Stream 方向的 SLA 数据文件里变量名和符号约定和 CMEMS 不一致有的产品把海面高度异常定义成负值读进来之后没有按数据说明反转符号。另一个原因是你自己的标签生成代码里 polar 编码和可视化的颜色约定冲突同类变量在代码里被赋值了两次。解决在数据加载器里做一次符号定向用一个物理事实做校验北半球的暖涡反气旋对应 SLA 正异常。写一个冒烟测试取已知的黑潮延伸体区域某帧打印网格最大值出现在哪确认和卫星估值一致再进入训练。这种错不会自己暴露准确率不会掉只有可视化出来才看得出端倪。5.3 mask 和 SLA 填色图错位边界整体偏移半格现象涡旋边界线明明是从同一帧 mask 里抽的叠到 SLA 图上却整体往某个方向偏移了半个格点看起来像套歪了的眼镜。原因mask 是在切片、归一化后的数组上生成的那份数组经过滑动窗口裁剪和边界填充和原始网格不再一一对应或者find_contours输出的像素坐标在反算经纬度时用了错误的起点。解决统一数据路径。要画图的那一帧从同一个ds对象切片出来用同一个数组同时喂给模型推理和可视化脚本不要推理用 A 数组、绘图用 B 数组。反算坐标时在代码里加一行断言assert sla_frame.shape pred_mask.shapeshape 对不上立刻抛异常比画完图人工对位置快得多。5.4 同一涡旋在连续帧里编号跳变时序轨迹断裂现象做可视化大屏时发现同一涡旋在相邻两帧的标注 ID 变了轨迹线在中间断开业务方追问是不是漏检了。原因模型逐帧独立推理帧与帧之间没有做关联跟踪。同一涡旋的质心在相邻帧会漂移如果漂移距离超过了你临时设定的阈值就会被认为是新的涡旋。解决识别之后加一个轻量关联步骤hasattr(prev_frame, centroids)时用 KDTree 找当前帧每个涡旋质心在上一帧的最近邻距离小于 100 公里的视为同一涡旋并继承 ID否则分配新 ID。这个逻辑在 50 行以内能写完不需要引入卡尔曼滤波或多目标跟踪框架处理逐日 SLA 数据够用了。5.5 训练集和验证集按单帧随机划分F1 虚高到不真实现象训练和验证 F1 都超过 0.9一换时间段的真实场景精度掉到 0.6模型跟中邪似的。原因中尺度涡的生命周期从几周到几个月逐日 SLA 场里相邻两帧几乎一样。随机按帧划分训练验证集时同一个涡旋的「不同照片」同时出现在两边模型相当于开卷考试测出来的精度当然虚高。解决按时间块划分数据集。比如 2015-2019 年数据前 80% 的时间段做训练后 20% 做验证这样验证集里的涡旋是模型完全没见过的。这个坑的杀伤力极强因为它不报错只会让所有人的付出都建立在虚假的指标上。养成好习惯划分数据的代码里注释一行# 按时间切分禁止随机打乱单帧。6. 用阈值扫描给识别结果一个可验收的 F1 曲线训练完模型面对一摞概率图和 mask很多人直接选 0.5 作为置信度阈值就开始出图这是一件相当碰运气的事。U-Net 输出的 softmax 概率分布并不是理想校准的0.5 阈值大概率不是最优解。我习惯在交付可视化之前做一次阈值扫描把 0.3 到 0.7 之间的所有候选阈值跑一遍每个阈值算一次 IoU 或 F1画出一条曲线选曲线峰值对应的阈值作为最终生产参数。这比凭经验拍脑袋快得多也能在验收报告里给出一张有说服力的图。import numpy as np def threshold_scan(prob_map, mask_true, thresholdsnp.arange(0.30, 0.76, 0.05)): prob_map: (3, H, W) 或 (2, H, W) 的概率图mask_true: 二值标签 results [] for t in thresholds: pred (prob_map t).astype(np.uint8) inter (pred mask_true).sum() union (pred | mask_true).sum() iou inter / max(union, 1) # F1 等价形式避免单独算 precision/recall 时的除零麻烦 f1 2 * inter / max(2 * inter (pred.sum() - inter) (mask_true.sum() - inter), 1) results.append((t, iou, f1)) return results # 对气旋涡类别扫描mask_true 是类内二值标签 scan threshold_scan(prob_map[:, 1], (label_mask 1).astype(np.uint8)) best_t, best_iou, best_f1 max(scan, keylambda x: x[2]) print(fbest threshold: {best_t:.2f}, IoU: {best_iou:.3f}, F1: {best_f1:.3f})threshold_scan的逻辑很直白对每个候选阈值把概率图转成二值 mask和真值标签做交集并集运算。F1 的公式在极端情况预测为空或标签为空下做了分母保护不会出现除以零的崩溃。扫描结果要画成曲线横轴是阈值纵轴是 F1一个类别一条线气旋和反气旋各扫一张。如果两条曲线峰值对应的阈值差距明显说明模型对两种极性的置信度分布不一致这时候要检查训练数据里两类样本的比例而不是强行统一阈值。选完阈值、出完图、导完 CSV这批识别结果才算真正可验收。我个人的习惯是每次换数据集都先跑一遍这个脚本五分钟能确认「这次模型有没有在贴标签」。阈值扫描的结果也会留给业务方他们如果想调灵敏度把阈值往低调一档就能让召回率上升不用重新训练这个「后悔药」在生产环境里特别管用。希望这篇笔记能帮你把中尺度涡识别与可视化这条链路少踩几个坑跑通一次之后后续换海域、换数据源就都是体力活了。本文还有配套的精品资源点击获取