☰
Matlab+m_map绘制海底地形图:投影、等深线及数据预处理实战
2026/10/5 1:36:58 网站建设 项目流程

写海底地形图这件事,我算是被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(极地方位投影):适用于极地地区,如南北极海域。

以南海为例,最稳妥的选择是Mercator:

m_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)全球融合了最新测深航次数据,海洋精度更好科研论文、精细海洋地形
SRTM15+15弧秒全球(重点海洋优化)融合卫星测高和船测数据,深海地形平滑深海盆、洋中脊相关研究

如果你是画大区域(比如整个中国近海),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 数据稀疏区(浅水区)的处理

画水深图时,浅水区(比如近岸、岛礁周边)往往是问题重灾区。因为很多船测数据更新不及时,浅水区会出现“洞”或者异常的尖锐地形。我通常用两步处理:

第一步,将超出合理范围的值置为NaN:

z_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或cmocean

Matlab默认的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_start+1 lat_end-lat_start+1]);

同样重要的一点是,m_pcolor的绘制速度最吃数据密度。如果数据分辨率过高(450m),整张图绘制一次可能要几十秒。可以先用downsample把数据抽稀到合适分辨率(比如2km),画完看效果,定稿前再用全分辨率出图。

写在最后的个人体会

m_map这套工具箱用了几年,最大的感受是“前人把路铺得很好了,你要做的只是别走偏”。它的数据接口、投影体系、海岸线模块都非常稳定,真正容易出问题的反而是地形数据的预处理和正负号处理这些“看起来很简单”的小事。每次画图前多花两分钟检查数据范围、确认维度顺序、选对色标,比画完再返工节省的时间多得多。如果你第一次画出来的地形图有点丑,别急着怀疑工具,耐心调整一两次,基本就能达到论文配图的水平了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询