
写海底地形图这件事我算是被m_map“救”过一次的人。早几年刚接触海洋数据处理的时候手里拿到一份南海的水深网格数据第一反应就是用Matlab自带的pcolor直接画。图倒是出来了但整个南海看起来被横向拉长了一大截等深线歪歪扭扭海岸线根本对不上经纬度比例完全是乱的。后来才知道问题出在没做地图投影变换——纬度每度的实际距离和经度不一样直接拿经纬度当平面坐标画高纬度地区必然变形。也就是从那次之后我开始认真用m_map这个工具箱一用就是好几年。这篇东西就围绕“Matlab m_map画地形水深图”这个主题把从安装、数据准备到最终出图的完整链路讲清楚顺便把那些真正会卡住人的坑都提前给你排掉。1. 为什么海底地形图绕不开m_map先搞懂它在解决什么问题m_map的全称叫Mapping Toolbox是UBC英属哥伦比亚大学的Rich Pawlowicz开发的一套开源Matlab地图投影工具箱。它不是专门为画海底地形设计的但海洋、大气、地质领域的人几乎人手一套原因很简单它把“地图投影”“海岸线叠加”“经纬度格网绘制”这三件最烦的事一次性全部解决了。1.1 核心痛点一经纬度坐标不能直接当平面坐标画很多刚从陆地数据转到海洋数据的同学会犯一个认知错误觉得pcolor(lon, lat, depth)没问题。实际上地球是个椭球体1度经度的实际长度是随纬度变化的赤道约111km北纬60度只有约55km而1度纬度的长度基本恒定在111km左右。你直接把lon当x轴、lat当y轴相当于假设地球是个平面纬度越高变形越严重。m_map的核心就是维护了一套完整的地图投影变换体系。它会根据你指定的投影方式Mercator、Lambert Conformal、Polar Stereographic等等把经纬度坐标实时换算成平面坐标再绘制。你不需要关心具体的换算公式它内部全处理好了。这里最关键的几个函数是m_proj初始化投影设定投影类型和中心经纬度m_pcolor画二维网格数据水深/温度/盐度等m_contour/m_contourf画等值线/填色等值线m_coast/m_gshhs叠加海岸线m_grid绘制经纬度网格和边框标注m_plot/m_line在图上叠加站点、航迹等这一套下来你不需要像传统做法那样先手动做投影换算、再慢慢加海岸线数据全部在统一坐标系里完成。1.2 核心痛点二海岸线和地形数据格式五花八门Matlab自带的Mapping Toolbox注意这是商业工具箱和开源的m_map不是一回事也能画地图但它对数据格式要求较高而且很多海洋数据的坐标参考系它处理起来不方便。m_map则轻量得多它直接支持常见的netCDF网格数据、ESRI shapefile海岸线数据以及它自己内置的GSHHS全球海岸线数据库分分辨率档位从crude到full都有。举个例子你要画中国近海的地形图用m_gshhs(intermediate)就能调出中等分辨率的海岸线叠加在地形填色图上面效果干净利落。同级别的效果如果自己找海岸线数据再手动拼接没有一两个小时下不来。1.3 适合谁来用不管你是做物理海洋、海洋地质、渔业资源还是研究气候变化的海气相互作用只要你的研究对象和海洋/湖泊/极地有关需要展示水深地形背景m_map就是绕不开的工具。它唯一的门槛是你要有一点Matlab基础至少看得懂脚本、会加载数据。接下来这篇东西的读者我默认是“已经能跑通Matlab基础绘图、但还没系统用过m_map”的人。2. m_map安装与环境配置分清楚三个版本再动手m_map的安装不复杂但网上能搜到的版本很混乱装错了后面各种报错这是我见过最多的“第一个坑”。所以这里单独拿出一节讲版本选择和安装细节。2.1 三个常见来源的区别目前网上流传的m_map主要有三个来源来源特点推荐度MathWorks File Exchange官方发布页Rich Pawlowicz本人维护更新较稳定功能完整最推荐优先用这个GitHub上的维护分支有人做了小修小补偶尔加一些新功能但质量参差不齐需要甄别不保证稳定部分Matlab教学资源包里的旧版网盘资料、学校课程附件等通常是2010年前后的老版本不推荐新版matlab可能兼容性差判断你下载的是不是最新版最简单的方法是看文件夹里有没有README.md或者m_map.m文件中的版本号。我目前用的是2023年之后的版本在Matlab R2021b和R2023a上都跑过m_proj、m_pcolor、m_gshhs这些核心函数没有任何问题。2.2 安装步骤和路径设置安装本质上就是把解压后的文件夹路径加进Matlab搜索路径。具体操作% 解压后把m_map文件夹放在任意目录例如 % D:\toolbox\m_map addpath(genpath(D:\toolbox\m_map)); savepath; % 保存路径设置避免下次启动失效注意不要只addpath顶层目录要加genpath因为m_map内部还按功能分了子文件夹比如private目录里的辅助函数。少加了路径调用m_proj时会报“Undefined function or variable”。装完后验证一下能不能正常工作m_proj(mercator, long, [105 125], lat, [15 30]); m_coast(patch, [0.8 0.8 0.8]); m_grid(box, fancy);如果这几行能画出一张带海岸线的地图底图说明m_map的基础环境没问题。2.3 老版本Matlab的兼容性问题如果你还在用R2016a或者更早的版本有些较新的m_map版本可能跑不起来最常见的问题出在contains、string这类新语法上。我个人建议是尽量升级Matlab到R2020a以上如果实在升不了就去GitHub找2016年左右的旧版m_map功能上画水深图完全够用。还有一个容易忽略的点m_map读取netCDF格式的地形数据时依赖Matlab的NetCDF工具箱。R2019a以上版本已经把netCDF读取函数ncread、ncdisp等内置化了不用额外装。但如果你用的是老版本且没装相关工具箱读数据时会报错这时候需要先解决NetCDF读写的问题。3. 从原始水深数据到可出版底图核心绘图流程拆解安装只是热身真正的核心在于理解m_map绘图的数据流程。很多人画出来的图丑、变形、或者数据错位往往是因为没有按正确的顺序组织代码或者没理解m_map对数据格式的隐含要求。3.1 m_pcolor的输入格式要求m_pcolor和Matlab自带的pcolor一样对输入数据的速度有要求你不能给它一堆散点必须给规则网格化的矩阵。具体来说经度是二维网格的X坐标大小m×n的矩阵纬度也是同样大小的二维矩阵水深数据也是同样尺寸的矩阵。这里有个常见的误区有人觉得网格数据只要传一维lon和lat向量就行m_pcolor会自动meshgrid。实测下来不行你必须自己先处理好。[lon2d, lat2d] meshgrid(lon1d, lat1d); m_pcolor(lon2d, lat2d, depth2d); shading flat; % 去掉网格线填色更平滑如果你的原始数据是像ncread读出来的三维数组比如(lon, lat, time)记得先squeeze去掉时间维或者只取第一个时刻。3.2 投影方式选不对图就废了一半m_map支持的投影方式很多但画海底地形图常用的就三种Mercator墨卡托适用于低纬度和中纬度区域如南海、热带西太平洋。它的特点是等角经纬网垂直相交方向准确但高纬度面积会被夸大。Lambert Conformal兰伯特正形圆锥适用于中纬度东西向跨度大的区域如北太平洋、北大西洋形变控制比Mercator好。Polar Stereographic极地方位投影适用于极地地区如南北极海域。以南海为例最稳妥的选择是Mercatorm_proj(mercator, long, [105 122], lat, [5 23]);选好中心投影后后续所有绘图函数都在这个投影下工作。有一点要特别提醒m_proj一旦设定好你不能在同一个figure里反复切换投影。想画两个不同区域的图要么用figure开新图要么用clf清掉重画。3.3 海岸线叠加的两种方式m_map加载海岸线有两种主要方式效果差别很大。第一种是m_coast它调用的是内置的GSHHS数据库速度较快但分辨率有限。对于区域尺度比如整个南海的地形图足够了。m_coast(patch, [0.7 0.7 0.7], edgecolor, none);第二种是m_gshhs它调用的也是GSHHS数据但可以指定更高分辨率m_gshhs(intermediate, patch, [0.8 0.8 0.8]);这两者最明显的区别是m_gshhs能识别并“镂空”被大陆遮挡的海域而且高分辨率下岛屿细节更丰富。但代价是绘制速度变慢尤其是全图范围很大时。我的建议是初稿用m_coast快速迭代定稿时换m_gshhs(high)提高质感。3.4 m_grid让坐标轴真正变成“地图”画完数据后要用m_grid加上经纬度网格线和边框标注。这里参数比较多说几个最实用的m_grid(box, fancy, tickdir, out, ... fontsize, 12, linewidth, 1.2, ... xtick, [105:5:122], ytick, [5:5:23]);box边框样式fancy会画一个带刻度的装饰性边框适合论文图。tickdir刻度方向out是向外默认in有时会和海岸线重叠。xtick/ytick手动指定刻度位置避免自动刻度出现太多小数。xticklabels/yticklabels如果数据范围和地理位置有偏移可以强制指定标签。m_grid的位置很关键一定放在所有绘图命令之后否则网格会覆盖在地形数据上面影响视觉效果。4. 地形数据的获取与预处理GEBCO和ETOPO到底怎么选没有地形数据m_map画得再漂亮也是空架子。这里单独说数据源的选择因为很多人在这块走了弯路下了几十GB的数据结果一半用不上。4.1 主流的全球地形水深数据源对比我实际用过的有三套各有优缺点数据源分辨率覆盖范围特色适合场景ETOPO 202215弧秒约450m全球NOAA发布陆海统一高程区域尺度地形图常规模拟底图GEBCO 202315弧秒约450m全球融合了最新测深航次数据海洋精度更好科研论文、精细海洋地形SRTM1515弧秒全球重点海洋优化融合卫星测高和船测数据深海地形平滑深海盆、洋中脊相关研究如果你是画大区域比如整个中国近海ETOPO 2022是性价比最高的选择下载快格式友好。如果你的研究区在南海深海盆或者其他测深数据覆盖密度高的区域GEBCO的海洋部分更可靠。4.2 netCDF格式的读取与裁剪这三套数据基本都是netCDF格式。下载下来之后先用ncdisp看一下变量结构ncdisp(ETOPO_2022_v1_15s_N30S120W.nc);一般会看到z海拔单位米变量以及lon和lat。需要注意ETOPO的海洋区域数值是负的海平面以下为负陆地是正的。而有些数据集比如GEBCO在某些处理方式下会把水深正负号反过来画图前一定要先检查数据范围min(z(:)), max(z(:))如果min是负的、max是正的说明这是“海拔”定义海洋水深需要取绝对值再画填色图或者直接用负值配蓝色系色标。如果min和max都是正的说明是“水深”定义这时候要把陆地剔除或者统一处理。读取和裁剪的完整代码可以参考% 读取变量 lon ncread(filename, lon); lat ncread(filename, lat); z ncread(filename, z); % 按研究区域裁剪 region_lon [105 122]; region_lat [5 23]; lon_idx find(lon region_lon(1) lon region_lon(2)); lat_idx find(lat region_lat(1) lat region_lat(2)); lon_sub lon(lon_idx); lat_sub lat(lat_idx); z_sub z(lon_idx, lat_idx); % 转置/翻转维度确保与meshgrid一致 z_sub z_sub;这里有个容易出错的点netCDF的维度顺序不同有的数据是(lat, lon)有的是(lon, lat)。读出来后用size(z_sub)确认一下再用meshgrid(lon_sub, lat_sub)生成坐标矩阵时务必和数组维度对齐。很多人的水深图和海岸线叠不齐十有八九是这一步维度反了。4.3 数据稀疏区浅水区的处理画水深图时浅水区比如近岸、岛礁周边往往是问题重灾区。因为很多船测数据更新不及时浅水区会出现“洞”或者异常的尖锐地形。我通常用两步处理第一步将超出合理范围的值置为NaNz_sub(z_sub 10000 | z_sub -10000) NaN;第二步对浅水区做一次中值滤波或高斯平滑消除单点噪声。Matlab自带的imgaussfilt就够用z_smooth imgaussfilt(z_sub, 1);注意这种平滑只适用于制图展示不适用于定量分析。如果你要做数值模拟的底地形平滑会改变水深分布绝对不能这么干。5. 实战案例画一张带等深线的南海地形水深图光说不练假把式。这一节给出一个从零到一画南海地形图的完整示例。这个区域既有超过5000米的深海盆又有一两百米的浅水陆架能很好地展示色标、等深线、海岸线各项细节的处理。5.1 准备数据和初始化假设你已经下载好了ETOPO 2022南海区域数据文件名设为etopo_south_china_sea.nc完整的绘图脚本如下clear; close all; clc; % 读取地形数据 filename etopo_south_china_sea.nc; lon ncread(filename, lon); lat ncread(filename, lat); z ncread(filename, z); % 裁剪到南海区域 lon_lim [105 122]; lat_lim [5 23]; lon_idx find(lon lon_lim(1) lon lon_lim(2)); lat_idx find(lat lat_lim(1) lat lat_lim(2)); lon_sub lon(lon_idx); lat_sub lat(lat_idx); z_sub z(lon_idx, lat_idx); % 数据修复剔除异常值 z_sub(z_sub 8000 | z_sub -8000) NaN; % 生成经纬度网格 [lon2d, lat2d] meshgrid(lon_sub, lat_sub);5.2 设置投影并绘制填色图% 新建图形窗口 figure(Color, white, Position, [100 100 1000 800]); % 初始化Mercator投影 m_proj(mercator, long, lon_lim, lat, lat_lim); % 绘制水深填色图 m_pcolor(lon2d, lat2d, z_sub); shading flat; % 设置色标蓝色系适合水深表达 colormap(flipud(m_colmap(blues))); % m_colmap是m_map自带色标比默认jet好看 caxis([-5000 0]); % 只看海面以下陆地为白色 % 叠加等深线 [cs, h] m_contour(lon2d, lat2d, z_sub, [-100 -200 -500 -1000 -2000 -3000 -4000 -5000], k, LineWidth, 0.6); clabel(cs, h, FontSize, 8, LabelSpacing, 400); % 叠加海岸线中分辨率 m_gshhs(intermediate, patch, [0.8 0.8 0.8]); % 叠加网格 m_grid(box, fancy, tickdir, out, fontsize, 12, xtick, 105:5:122, ytick, 5:5:23); % 添加色标 c colorbar; c.Label.String Elevation (m); c.Label.FontSize 12; % 保存 print(-dpng, -r300, south_china_sea_bathymetry.png);5.3 针对浅水区域的优化调整上面的代码是基础版能画出大概效果。但如果你照这个画完会发现华南近岸和台湾海峡一带的水深细节全是一片深色根本看不出陆架结构——因为色标范围拉到了-5000到0浅水区-200m以内的动态范围被压缩得太厉害。这时候有两个优化方向方向一色标非线性化先用caxis限制范围把5000米的动态范围压缩到两段显示或者用pcolor的Log变换自定义色标。实际中我更常用的是分段色标——计算一个对数值映射矩阵把浅水区拉开% 思路对|z|做对数变换后映射到色标 depth -z_sub; depth(depth 1) 1; depth_log log10(depth); imagesc(lon_sub, lat_sub, depth_log); % 注意这里用imagesc配合axis xy不过这属于进阶玩法需要自己控制坐标轴和色标标签工作量不小。如果只是画一张展示图我建议用第二条路。方向二画两张图拼接第一张画全区域的宏观水深第二张用axis限定到陆架区域专门用caxis([-200 0])突出浅水结构。论文里如果需要深浅都兼顾可以考虑“等深线分区域标注”的方式浅水区加细等深线深海区加粗等深线。5.4 岛屿和海域名称标注地形图如果没有地名标注信息量会打折扣。m_map本身不自带地名标注但可以用m_text叠加m_text(111.5, 18.5, Hainan, FontSize, 11, FontWeight, bold); m_text(114.5, 21.5, Northern South China Sea, FontSize, 10, Rotation, -20);注意m_text的前两个参数仍然是经纬度坐标m_map会自动换算到投影平面上不用你自己算像素位置。6. 论文出图前必须处理好的四个细节色标、标注、分辨率与地形数据正负号这部分算是我个人“交过学费”的经验汇总。画面上的小问题往往是在投稿或者做汇报时被发现然后返工重画特别浪费时间。提前处理这四个细节能让你的图直接达到出版级别。6.1 色标选择不要用默认jet改用m_colmap或cmoceanMatlab默认的jet色标色彩跨度大到离谱红色代表深海、绿色代表浅海视觉上会严重误导读者对水深梯度的判断。更致命的是jet没有亮度的一致性打印成黑白稿后几乎什么也分不清。m_map自带的m_colmap提供了几种海洋常用色标colormap(m_colmap(blues)); % 蓝色系适合水深 colormap(m_colmap(topog)); % 地形色标陆地棕绿色海洋蓝色 colormap(m_colmap(sealand)); % 海陆两色我个人最常用的是flipud(m_colmap(blues))让最深处用深蓝、近岸浅水用浅蓝符合大多数人的视觉直觉。如果追求更专业的海洋色标还可以从File Exchange下载cmocean工具箱注意这不是m_map自带的其中的deep、topo、haline色标都是海洋论文的高频选择。6.2 标注与字体中文乱码的规避办法m_map的m_grid标注默认使用Matlab的字体设置。很多中文字体在这个工具箱里会直接变成方块或者乱码尤其你把Matlab系统字体设成中文字体时经纬度数字有时都会受影响。稳妥的做法是图片内所有标注一律用英文。地理名称用拼音或者英文名比如Hainan Island、South China Sea不要在图上直接放中文。如果论文要求中文标注后期用AI或Inkscape二次加工把字体问题彻底绕开。另外m_grid的fontsize参数要足够大默认的10号字在图片缩放到两栏宽度后基本看不清。我一般设置到12或14。6.3 高清输出导出PNG和EPS两条路不同投稿系统对图片格式要求不一样最保险的方式是导出双版本% PNG版本用于网页预览或有些系统要求位图 print(-dpng, -r600, bathymetry.png); % EPS版本用于LaTeX投稿和排版矢量图 print(-depsc, -r300, bathymetry.eps);这里有个细节-depsc是彩色EPS-deps是黑白EPS。如果你不确定系统接受哪一个两个都导出来。EPS的好处是在LaTeX里能保持矢量特性放大后依然清晰踩点线不会糊。还有一点导出前先用set(gcf, PaperPositionMode, auto)这样导出的图片尺寸会和屏幕显示一致不会出现莫名其妙的白边或者裁切。6.4 最容易被忽略的地形数据正负号与等深线方向刚才提过正负号问题这里再展开说。ETOPO 2022的海洋部分是负值陆地是正值。而GEBCO的原始数据同样是“海拔”定义但很多二次处理的数据集直接把水深转成了正值同时把陆地设为零或者NaN。如果你混用两套数据没检查正负号就画结果可能是海陆颠倒或者等深线全部镜像。我的习惯是画图前先做三次检查1. min(z_sub(:)), max(z_sub(:)) % 检查整体范围 2. z_sub(1, 1) % 检查角落数据 3. sum(z_sub(:) 0) / numel(z_sub) % 统计陆地占比验证数据区域是否正确第三步特别有用。比如你裁剪的是南海区域如果陆地占比高达60%说明裁剪范围可能偏了或者正负号反了。正常南海陆架加岛屿区域陆地占比应该在10%-20%之间。等深线的方向也值得注意。m_contour画等深线时默认的标注方向是沿着线方向排出但如果你传的是海拔数据负值表示海洋等深线标注时容易把“-100”和“100”搞混。建议画之前先z_sub abs(z_sub)把数据转为水深正值同时把陆地部分设为NaN这样等深线数值更直观也方便后续标注。% 水深正值化 depth -z_sub; % 把海洋负值转正 depth(z_sub 0) NaN; % 陆地设为NaN不参与画图6.5 大范围地形图的内存和速度优化如果你要画的是全球或者半个太平洋这种大范围地形图数据量轻松超过几个GB直接用ncread全部读进去会非常卡甚至直接内存溢出。这时候必须做“分块读取”或者“先裁剪再读取”。netCDF支持按索引范围读取不需要先读全部lon_start find(lon 100, 1, first); lon_end find(lon 150, 1, last); lat_start find(lat -10, 1, first); lat_end find(lat 30, 1, last); % 只读取目标区间避免内存爆炸 lon_sub lon(lon_start:lon_end); lat_sub lat(lat_start:lat_end); z_sub ncread(filename, z, [lon_start lat_start], [lon_end-lon_start1 lat_end-lat_start1]);同样重要的一点是m_pcolor的绘制速度最吃数据密度。如果数据分辨率过高450m整张图绘制一次可能要几十秒。可以先用downsample把数据抽稀到合适分辨率比如2km画完看效果定稿前再用全分辨率出图。写在最后的个人体会m_map这套工具箱用了几年最大的感受是“前人把路铺得很好了你要做的只是别走偏”。它的数据接口、投影体系、海岸线模块都非常稳定真正容易出问题的反而是地形数据的预处理和正负号处理这些“看起来很简单”的小事。每次画图前多花两分钟检查数据范围、确认维度顺序、选对色标比画完再返工节省的时间多得多。如果你第一次画出来的地形图有点丑别急着怀疑工具耐心调整一两次基本就能达到论文配图的水平了。