GMT三维绘图有BUG? 修复它!

前期一篇文章:Modern GMT Series:Slice in 3D View (三维切片图)中提到了在科研作图中会经常遇到三维作图的问题,而且GMT可以做三维图且导出的图片质量非常高!但是也提到了当前GMT版本中的三维绘图存在漏洞。主要两个问题:(1)切片位置错乱;(2)三维文字镜像。就这两个问题极大的限制了GMT三维作图的应用。因为最近论文里面要做三维立体图(地理坐标+笛卡尔坐标+海底地形+剖面切片等),找了找也没找到更好的解决方案,干脆自己动手丰衣足食——修改GMT的源代码。经过一天半的努力,已经比较完美的修复了这两个漏洞目前官方版本中并没有修复此bug。 下面就介绍一下修复漏洞的过程和最终效果!

在线视频讲解

视频讲解

先上一张BUG修复后的效果图鉴赏一下

三维地形图+剖面切片+线+文字+方向标+ModernGMTlogo

注: 由于我的论文正在审稿中,不便上传较为复杂的绘图结果,暂用一个相对简单的图来代替。

简单例子

问题描述:绘制一个三维立方体并在每个面上标注文字以及设置每个面具有不同颜色且半透明;水平旋转45度,然后x轴和y轴标注坐标标签。

GMT官方版本运行的结果

GMT6.0三维绘图结果

绘图代码

# Test 3D View of GMT: simple example
# Zhikui Guo, 2018-12-09, GEOMAR, Germany

function preset()
{
    figname=Test_3d
    angle_view=-45/25
    width_fig_x=10
    width_fig_y=10
    width_fig_z=7
    # 透明度
    alpha_profile=50
    # range
    xmin=0
    xmax=100
    ymin=0
    ymax=50
    zmin=-20
    zmax=0
    xc=`echo $xmin $xmax | awk '{print $1+($2-$1)/2}'`
    yc=`echo $ymin $ymax | awk '{print $1+($2-$1)/2}'`
    zc=`echo $zmin $zmax | awk '{print $1+($2-$1)/2}'`
    len_z=`echo $zmin $zmax | awk '{print ($2-$1)}'`
}
# 1. apply theme
# . styles.sh
# MonokaiTheme
# 常用代码头文件:绘制logo
# . stdafx.sh
# 范围和颜色等设置
preset

# plot 
gmt begin $figname pdf,png
    gmt basemap -JX$width_fig_x/$width_fig_y -JZ$width_fig_z -R$xmin/$xmax/$ymin/$ymax/$zmin/$zmax -Ba -Bza+l"Z(m)" -BwsenZ+gred  -pz$angle_view/$zmin
    echo "$xc $yc zmin plane" | gmt pstext -JZ -p -F+f20p,Helvetica-Bold,blue=thinner,white+jCM+a190 -Dj0c/0c
    gmt basemap -JX$width_fig_x/$width_fig_y -JZ$width_fig_z -R$xmin/$xmax/$ymin/$ymax/$zmin/$zmax -Bwsenz+ggray  -pz$angle_view/$zmax
    echo "$xc $yc zmax plane" | gmt pstext -JZ -p -F+f20p,Helvetica-Bold,blue=thinner,white+jCM+a190 -Dj0c/0c
    gmt basemap -JX$width_fig_y/$width_fig_z -JZ$width_fig_x -R$ymin/$ymax/$zmin/$zmax/$xmin/$xmax -Ba -BwSenz+glightblue@$alpha_profile -Bx+l"Y(m)" --MAP_FRAME_PEN=1,black@0 --MAP_ANNOT_OFFSET=-0.2   -px$angle_view/$xminn
    echo "$yc $zc ymin plane" | gmt pstext -JZ -p -F+f20p,Helvetica-Bold,blue=thinner,white+jCM+a45 -Dj0c/0c
    gmt basemap -JX$width_fig_x/$width_fig_z -JZ$width_fig_y -R$xmin/$xmax/$zmin/$zmax/$ymin/$ymax -Ba -BwSenz+glightgreen@$alpha_profile -Bx+l"X(m)" --MAP_FRAME_PEN=1,black@0 --MAP_ANNOT_OFFSET=-0.2  -py$angle_view/$ymax
    echo "$xc $zc xminn plane" | gmt pstext -JZ -p -F+f20p,Helvetica-Bold,blue=thinner,white+jCM+a-45 -Dj0c/0c
    # 使用logo
    # add_logo 0 0.5 moderngmt -grid=false 
gmt end
# open $figname.pdf
# rm tmp* gmt.conf gmt.history 

这段代码不涉及什么数据,直接可以复制运行的。这里我将主题logo注释了,这是我自己定义的,因为你的电脑上肯定是没有相关的文件和函数。直接复制上面的代码应该是可以直接出图的。注意这是Mac系统下的脚本,也可以在Linux系统下使用,但是windows系统下需要将变量引用从$xxx改为%xxx%

都有什么BUG?

从上面的第二个图中可以看出,官方版本的三维绘图结果存在一下几个问题:

  1. 切片位置错乱:需要向y轴正方向平移一个量

  2. 文字出现镜像:x轴和y轴的标注文字都变成了镜像且有一个翻转角度

  3. 普通文字镜像:除了坐标轴的标注文字外,剖面切片上的文字的角度也有错乱和镜像现象

如何修复BUG?

简单介绍以上三个BUG是如何修复的,具体见修复后的源代码(我的资源库中有介绍如何获取源代码和可用软件)。

切片位置错乱问题

GMT绘图是基于PostScript的,三维绘图的坐标旋转理论见GMT三维坐标旋转理论。经测试发现问题出现在gmt_plot.c源文件里面的gmt_plane_perspective函数

a = b = c = d = e = f = 0.0;
if (plane < 0)          /* Reset to original matrix */
    PSL_command (PSL, "PSL_GPP setmatrix\n");
else {  /* New perspective plane: compute all derivatives and use full matrix */
    if (plane >= GMT_ZW) level = gmt_z_to_zz (GMT, level);  /* First convert world z coordinate to projected z coordinate */
    switch (plane % 3) {
        case GMT_X: /* Constant x, Convert y,z to x',y' */
            a = GMT->current.proj.z_project.sin_az;
            b = -GMT->current.proj.z_project.cos_az * GMT->current.proj.z_project.sin_el;
            c = 0.0;
            d = GMT->current.proj.z_project.cos_el;
            e = GMT->current.proj.z_project.x_off - level * GMT->current.proj.z_project.cos_az;
            f = GMT->current.proj.z_project.y_off - level * GMT->current.proj.z_project.sin_az * GMT->current.proj.z_project.sin_el;
            break;
        case GMT_Y: /* Constant y. Convert x,z to x',y' */
            a = -GMT->current.proj.z_project.cos_az;
            b = -GMT->current.proj.z_project.sin_az * GMT->current.proj.z_project.sin_el;
            c = 0.0;
            d = GMT->current.proj.z_project.cos_el;
            e = GMT->current.proj.z_project.x_off + level * GMT->current.proj.z_project.sin_az;
            f = GMT->current.proj.z_project.y_off - level * GMT->current.proj.z_project.cos_az * GMT->current.proj.z_project.sin_el;
            break;
        case GMT_Z: /* Constant z. Convert x,y to x',y' */
            a = -GMT->current.proj.z_project.cos_az;
            b = -GMT->current.proj.z_project.sin_az * GMT->current.proj.z_project.sin_el;
            c = GMT->current.proj.z_project.sin_az;
            d = -GMT->current.proj.z_project.cos_az * GMT->current.proj.z_project.sin_el;
            e = GMT->current.proj.z_project.x_off;
            f = GMT->current.proj.z_project.y_off + level * GMT->current.proj.z_project.cos_el;
            break;
    }
    // printf("ix: %f iy: %f\n",PSL->internal.x2ix,PSL->internal.y2iy);
    /* First restore the old matrix or save the old one when that was not done before */
    PSL_command (PSL, "%s [%g %g %g %g %g %g] concat\n",
        (GMT->current.proj.z_project.plane >= 0) ? "PSL_GPP setmatrix" : "/PSL_GPP matrix currentmatrix def",
        a, b, c, d, e * PSL->internal.x2ix, f * PSL->internal.y2iy);
}

正如GMT三维坐标旋转理论所提到的公式,gmt_plane_perspective函数的这段代码目的就是根据MGT绘图命令中的-R, -J, -JZ, -p参数计算坐标旋转矩阵参数a,b,c,d,e,f,最后使用PSL_command将坐标旋转矩阵写入ps文件就能得到三维的效果。

** GMT_Z的情况都是OK的,正如上图所示三维坐标轴主框架没错。但是 GMT_X GMT_Y**的情况,也就是分别于X轴和Y轴垂直的切片的情况,出现了问题。经测试发现,问题在于e, f计算错误。三种情况的e, f值需要一致才不会发生未知平移错乱。

解决方案

这里有一个很容易的解决方案就是在case GMT_Z计算得到e,f值的时候,打开一个临时文件将这两个值保存到临时文件里面,然后如果执行到case GMT_X或者case GMT_Y的时候将这个临时文件里面的e,f值读入覆盖掉程序计算的结果。这虽然不是一种完美的解决方案,但是可以实实在在的修复这个bug。以后有时间了仔细研究GMT内部是如何计算这些参数然后找到计算错误的位置,再做完美修改方案。修改源代码后,重新编译得到更新的gmt程序,然后再运行这个例子,得到的结果如下:

修复剖面位置问题后的结果

坐标轴标注文字镜像问题

上面一步修复了剖面位置问题,剖面坐标轴标注文字依然存在镜像翻转的现象。PS的镜像翻转命令是-1 1 scale,如果相对某个文字进行镜像翻转,只需要在ps文件里面找到这个文字对应的代码,然后在前面加上这个命令即可实现镜像翻转。找到这个magic命令真是不容易,参见一个论坛

依照这个逻辑,在源代码中跟踪找到绘制坐标轴label的位置:gmt_plot.c文件中的gmt_xy_axis函数,关键代码如下:

/* Finally do axis label */
if (A->label[0] && annotate && !gmt_M_axis_is_geo_strict (GMT, axis)) {
    unsigned int far_ = !below;
    char *this_label = (far_ && A->secondary_label[0]) ? A->secondary_label : A->label; /* Get primary or secondary axis label */
    if (!MM_set) PSL_command (PSL, "/MM {%s%sM} def\n", neg ? "neg " : "", (axis != GMT_X) ? "exch " : "");
    form = gmt_setfont (GMT, &GMT->current.setting.font_label);
    PSL_command (PSL, "/PSL_LH ");
    PSL_deftextdim (PSL, "-h", GMT->current.setting.font_label.size, "M");
    PSL_command (PSL, "def\n");
    PSL_command (PSL, "/PSL_L_y PSL_A0_y PSL_A1_y mx %d add %sdef\n", PSL_IZ (PSL, GMT->current.setting.map_label_offset), (neg == horizontal) ? "PSL_LH add " : "");
    /* Move to new anchor point */
    PSL_command (PSL, "%d PSL_L_y MM\n", PSL_IZ (PSL, 0.5 * length));
    
    if (axis == GMT_Y && A->label_mode) {
        i = (below) ? PSL_MR : PSL_ML;
        PSL_plottext (PSL, 0.0, 0.0, -GMT->current.setting.font_label.size, this_label, 0.0, i, form);
    }
    else
        PSL_plottext (PSL, 0.0, 0.0, -GMT->current.setting.font_label.size, this_label, horizontal ? 0.0 : 90.0, PSL_BC, form);
    
}

代码段里面调用PSL_plottext绘制坐标轴label(也就是图中的X(m)Y(m)),为了方便管理和代码重用,我在gmt_plot.c里面自定义了两个函数: AxisLable_Flip_GMT_X_YAxisTickLabel_Flip_GMT_X_Y分别对axis label和axis tick label进行操作。注意:tick label除了镜像问题还存在一个180度的旋转角度问题,这些都需要根据-p参数进行重新计算和判断然后修复

加入自定义函数后的代码段如下:

/* Move to new anchor point */
PSL_command (PSL, "%d PSL_L_y MM\n", PSL_IZ (PSL, 0.5 * length));

AxisLable_Flip_GMT_X_Y(GMT);// X labbel: 官方版本中的对于x和y剖面切片的文字出现了镜像,所以这里只对x和y剖面进行翻转
if (axis == GMT_Y && A->label_mode) {
    i = (below) ? PSL_MR : PSL_ML;
    PSL_plottext (PSL, 0.0, 0.0, -GMT->current.setting.font_label.size, this_label, 0.0, i, form);
}
else
    PSL_plottext (PSL, 0.0, 0.0, -GMT->current.setting.font_label.size, this_label, horizontal ? 0.0 : 90.0, PSL_BC, form);

AxisLable_Flip_GMT_X_Y(GMT);//恢复翻转

ticklabel 和 axis label都在同一个函数里面,调用自定义函数AxisTickLabel_Flip_GMT_X_Y修复之后的代码片段如下:

for (i = 0; i < nx1; i++) {
    if (gmtlib_annot_pos (GMT, val0, val1, T, &knots[i], &t_use)) continue;         /* Outside range */
    if (axis == GMT_Z && fabs (knots[i] - GMT->current.proj.z_level) < GMT_CONV8_LIMIT) continue;   /* Skip z annotation coinciding with z-level plane */
    if (GMT->current.setting.map_frame_type & GMT_IS_INSIDE && (fabs (knots[i] - val0) < GMT_CONV8_LIMIT || fabs (knots[i] - val1) < GMT_CONV8_LIMIT)) continue;    /* Skip annotation on edges when MAP_FRAME_TYPE = inside */
    if (!is_interval && plot_skip_second_annot (k, knots[i], knots_p, np, primary)) continue;   /* Secondary annotation skipped when coinciding with primary annotation */
    x = (*xyz_fwd) (GMT, t_use);    /* Convert to inches on the page */
    /* Move to new anchor point */
    PSL_command (PSL, "%d PSL_A%d_y MM\n", PSL_IZ (PSL, x), annot_pos);
    if (label_c && label_c[i] && label_c[i][0])
        strncpy (string, label_c[i], GMT_LEN256-1);
    else
        gmtlib_get_coordinate_label (GMT, string, &GMT->current.plot.calclock, format, T, knots[i]);    /* Get annotation string */
    double angle_text2=AxisTickLabel_Flip_GMT_X_Y(GMT,text_angle); //坐标轴标注文字翻转
    PSL_plottext (PSL, 0.0, 0.0, -font.size, string, angle_text2, justify, form);
    AxisTickLabel_Flip_GMT_X_Y(GMT,text_angle); //恢复:坐标轴标注文字翻转
}
if (!faro) PSL_command (PSL, "/PSL_A%d_y PSL_A%d_y PSL_AH%d add def\n", annot_pos, annot_pos, annot_pos);

这里省略细节,感兴趣的可以直接看我的源代码

第二个BUG修复之后的结果

坐标轴label和tick label镜像问题修复后的效果

剖面文字的镜像和旋转角度问题

第二个BUG修复之后,坐标轴上的文字正确归为了。但是剖面上的文字还存在问题:文字镜像以及某些情况下存在一个角度偏转。找到绘制文字的代码位置:pstext.c文件中的GMT_pstext函数,代码太长,这里只贴关键代码附近的几行:

        c_txt[m] = strdup (curr_txt);
        c_x[m] = plot_x;
        c_y[m] = plot_y;
        c_just[m] = T.block_justify;
        c_font[m] = T.font;
        m++;
    }
    else {
        PSL_plottext (PSL, plot_x, plot_y, T.font.size, curr_txt, T.paragraph_angle, T.block_justify, fmode);
    }
    if (Ctrl->A.active) T.paragraph_angle = save_angle; /* Restore original angle */
}

就是在这里调用PSL_plottext函数进行文字绘制,而PSL_plottext函数定义在postscriptlight.h里面,函数实现在postscriptlight.c里面。进入这两个文件里面发现不太容易传递GMT_CTRL *GMT参数(GMT这个数据结构中包含了很多很多信息),为了兼容性。重新自定义一个int PSL_plottext_mirror (int isMirrow,struct PSL_CTRL *PSL, .....)的函数,在这个函数中增加一个参数isMirrow。用PSL_plottext_mirror替换上面代码段中的PSL_plottext。与前面的几个问题的修复方法类似,在pstext.c中新增函数int Text_Flip_GMT_X_Y(struct GMT_CTRL *GMT)判断何时需要对文字进行镜像翻转,然后将这个参数传递给新定义的文字绘制函数PSL_plottext_mirror去执行相应的操作。

c_txt[m] = strdup (curr_txt);
        c_x[m] = plot_x;
        c_y[m] = plot_y;
        c_just[m] = T.block_justify;
        c_font[m] = T.font;
        m++;
    }
    else {
        // PSL_command(PSL,"%%绘制文字主控函数\n");
        // Text_Flip_GMT_X_Y(GMT); //文字镜像翻转
        PSL_plottext_mirror (Text_Flip_GMT_X_Y(GMT),PSL, plot_x, plot_y, T.font.size, curr_txt, T.paragraph_angle, T.block_justify, fmode);
        // Text_Flip_GMT_X_Y(GMT); //恢复
    }
    if (Ctrl->A.active) T.paragraph_angle = save_angle; /* Restore original angle */
}

使用自定义的文字绘制函数PSL_plottext_mirror

至此,上面三个问题已全部修复完毕,修复bug之后的GMT姑且称之为ModernGMT(GMT私房菜)。运行结果如下。

ModernGMT运行结果

本人修复BUG之后的版本称之为ModernGMT,这是一个持续更新的私房版本,不仅修复各种bug还会添加一些有用的地球物理重磁位场相关的程序。而且我每个星期都会同步官方更新,所以ModernGMT总是比官方版本新一些且功能多一些。但是由于我时间有限不能暂时还没有把自己做的更新推送给官方团队,以后会考虑!

ModernGMT三维绘图结果

后记

经过上面的修复之后,目前绘制正常的三维图应该是没有什么大的问题了。如果有人尝试一些奇怪的想法,比如旋转角度很畸形或者其他的参数不和常理,不保证不出问题。

依然存在的问题

当使用-JM投影方式的时候,如果数据范围很大,比如100度X80度的跨度,由于投影计算过程中的误差等原因,可能会出现切片不贴合的现象。如下图所示:

大范围三维绘图切片长度计算误差

解决这个问题的技巧就是直接在GMT绘图代码里面调整一下切片的长度即可,比如增加一个小的倍数,稍微调整然后看着没问题就行。以后有空了会从代码里面直接解决

    # # profile along lat
    width_fig_y0=`echo $width_fig_y | awk '{print $1+0.3}'`
    gmt psbasemap -R$range_LatZLon -JX$width_fig_y/$width_fig_z -JZ$width_fig_x -BS -Ba --MAP_ANNOT_OFFSET=$offset_ticklabel_lon  -px$angle_view/$pos_profile_lon
    

小范围是没有问题的,比如下面这张图

西南印度洋脊龙旗热液区附近的ETOPO1水深数据

两个剖面上的数据是随便用程序生成的高斯分布数据,不代表任何意义,只是为了说明在剖面切片中绘制grdimage的问题。

GMT更新程序获取方法

由于时间和精力有限,暂不为伸手党直接开放源代码和程序。源代码以及编译之后的安装文件我会上传到云端,请到我的资源库中查看代码和程序获取方法。

你可能感兴趣的:(GMT三维绘图有BUG? 修复它!)