台风吧 关注:227,318贴子:6,076,805

回复:【资源共享】MODIS云图渲染基础教程&伪VIS代码实现

只看楼主收藏回复





IP属地:江西35楼2024-04-22 22:31
收起回复
    简单提几个绘制红外云图可能会用到的杂项功能,暂时不作特别详细的展开,以后可能会在本楼慢慢补充/勘误:
    1. 云图裁剪
    MODIS的1KM云图有2030×1354像素,分辨率更高的VIIRS则有超过6000×6000像素,像这种分辨率很高的极轨云图如果不经过裁剪,渲染时间会较长,而且出图后也很难上传到论坛/贴吧等平台。裁剪数据会用到python的“切片”功能,注意这里需要用到多维(二维)数组的切片,而不是普通的一维切片。
    以bt31为例,二维数组的切片方法如下:
    bt31[a:b, c:d]
    a,b代表第一个方向上的起止序号;c,d代表第二个方向,这样能裁剪出以TC为中心的近似平行四边形的图像,我现在画的大部分云图也是这种。
    如果需要得到长方形图像可以直接通过经纬度裁剪,未使用Cartopy的话可以直接用plt.xlim,plt.ylim输入经纬度进行裁剪,如果使用了Cartopy可以使用set_extent,若直接通过经纬度裁剪figsize也需要相应地调整。
    2. 眼温读取
    这个没什么技术难度,通过适当的切片使TC眼区为图像中亮温最高的部分,随后使用np.max读取即可。
    3. 海岸线/不同投影
    一般会选择Cartopy实现海岸线绘制和等经纬以外的投影。
    import cartopy.crs as ccrs
    import cartopy.feature as cfeature
    fig = plt.figure(figsize=(londiff, latdiff), dpi=150) #给创建的画布一个名字(fig)
    ax = fig.subplots(1, 1, subplot_kw={'projection': ccrs.Mercator()}) #设置子图为墨卡托投影,使高纬TC形态不被“压扁”
    ax.add_feature(cfeature.COASTLINE.with_scale('10m'), lw=1) #添加海岸线
    ax.pcolormesh(newlon, newlat, bt31, vmin=-100, vmax=50, cmap=LinearSegmentedColormap('1', bddata), transform=ccrs.PlateCarree()) #加上transform参数,明确数据原先是在等经纬网格上定义的

    同一张Patricia,使用了墨卡托投影并加了墨西哥海岸线(不过画非高纬TC我平时还是最习惯等经纬)


    IP属地:美国36楼2024-04-23 14:47
    收起回复
      4. 伪VIS云图渲染
      《伪VIS算法原理与教程》理论篇(https://www.tyboard.net/forum.php?mod=viewthread&tid=374)对伪VIS算法从原理和公式出发进行了较为详细的讲解,但代码层面没有过多的展开,对于部分读者来说自己复现的难度较大。这里我们会提供更详细的指引和代码示例,对代码背后的原理感兴趣的读者们也可以再回去看看理论篇。
      a. 伪VIS基础算法实现
      先前写的理论篇第三章分为7个小节,所谓“基础算法”指的是其中除了高云修正和海温反演修正以外的其他5项,其特点是比较通用,能应用于每个有合适波段与分辨率的卫星仪器。

      (伪VIS教程目录截图)
      首先把伪VIS需要的Band20,Band32亮温读取出来:
      radiance = (Emissive[0] - radiance_offsets[0]) * radiance_scales[0]
      bt20 = C1/3.750 / (np.log(1 + C2/3.750**5/radiance)) - 273.15
      radiance = (Emissive[11] - radiance_offsets[11]) * radiance_scales[11]
      bt32 = C1/12.020 / (np.log(1 + C2/12.020**5/radiance)) - 273.15
      合成高温区模拟反射率:
      ref_warm = (bt20[:, 1:]-26)/(bt31[:, 1:]-29)
      这里已经可以尝试用pcolormesh画出来了,注意渲染范围要改成0~1,cmap可以改成matplotlib内置的'gray'色阶。
      plt.pcolormesh(newlon, newlat, ref_warm, vmin=0, vmax=1, cmap='gray', transform=ccrs.PlateCarree())
      结果如下,其实已经和可见光云图有几分相像了


      IP属地:美国37楼2024-04-25 10:33
      回复
        合成低亮温区模拟反射率:
        先进行亮温差分
        dif = bt31 - bt32
        我们希望把差分亮温通过一定的映射转化为模拟反射率。3.a介绍的色阶本质上就是一类映射,而这里我们所需的映射也可以通过色阶达成。pcolormesh+色阶的办法只能给出色阶映射后的图像,而我们想要的是映射后的二维数组以方便后续计算,这就需要matplotlib.colors.Normalize和matplotlib.cm.ScalarMappable这两个功能。
        from matplotlib.colors import LinearSegmentedColormap, Normalize
        from matplotlib.cm import ScalarMappable
        定义映射方式。色阶范围为-15~15度的亮温差分值,色阶名为colddata。
        norm1 = Normalize(vmin=-15, vmax=15, clip=True)
        map1 = ScalarMappable(norm1, LinearSegmentedColormap('1', colddata))
        colddata是用于强调高反射率云盖与低反射率卷云的色阶,在映射过程中拉高了两者的对比度,有助于分辨CDO边缘。色阶的具体数值是经验性的,各位也可以按照个人喜好微调。做从亮温差分到模拟反射率的映射中,并不需要多种颜色,因此这里rgb三色中只有red起作用。
        colddata={'green': [(0, 0, 0),
        (1, 1, 1)],
        'red': [(0, 1, 1),
        (0.37, 1, 1),
        (0.438, 0.98, 0.98),
        (0.468, 0.94, 0.94),
        (0.498, 0.85, 0.85),
        (0.508, 0.81, 0.81),
        (0.538, 0.75, 0.75),
        (0.638, 0.61, 0.61),
        (0.758, 0.51, 0.51),
        (1, 0.5, 0.5)],
        'blue': [(0, 0, 0),
        (1, 1, 1)]}
        映射后会得到rgba形式的低温区模拟反射率,是2030×1354×4的三维数组,其中第三个维度分别是r, g, b, a,用索引取出r就得到了低温区的模拟反射率。
        ref_cold = map1.to_rgba(dif)[:, :, 0]


        IP属地:美国38楼2024-04-25 12:45
        回复
          对高温区与低温区的模拟反射率做加权合成:
          先对高温区模拟反射率进行一下预处理,去除Band20低温区信噪比不够造成的nan值,并将小于0的值全部设为0,大于1的值全部设为1。
          ref_warm = np.nan_to_num(ref_warm, nan=1)
          ref_warm = np.clip(ref_warm, 0, 1)
          低温区反射率由于是用ScalarMappable映射的,已经处在0~1的区间了,因此无需进一步处理。
          通过Band31亮温设置两份反射率的权重并合成。这里本质上也是一个从Band31亮温到权重参数的映射,但由于是线性的,不需要ScalarMappable创建映射,用np.clip也可以达到相同效果。
          weight = (np.clip(bt31, -70, -20) + 70)/50
          ref = ref_warm * weight + ref_cold * (1 - weight)


          IP属地:美国39楼2024-04-26 04:54
          回复
            合成光影效果:
            用数据切片将Band31亮温最左侧/最右侧一列数据去除并相减,达到“错位差分”的效果。[:, 0:-1]与[:, 1:]可以互换以达到模拟相反方向光照。
            shade = bt31[:, 0:-1] - bt31[:, 1:]
            随后将错位差分的亮温值限制在-4~4的范围内并乘上系数0.08,这意味着光影效果最多改变±0.32的反射率。这些数值可以按照个人喜好与不同卫星的分辨率调整,对于分辨率较高的卫星,错位差分的范围可以设置的小一些,系数则可以设置的大一些。
            shade = np.clip(shade, -4, 4) * 0.08

            (上图为shade的图像,渲染范围改到了-0.5~0.5,并把经纬度也去除了一列以对齐数据)
            图像中不是所有区域都应该叠加上如图所示的光影效果,本身反射率偏低的区域应有的光影效果会弱一些,卷云区域和其他非深对流云顶呈现的光影也应稍弱。这里我们定义一个“光影参数”,由先前合成的模拟反射率,Band31-32差分做的卷云识别,以及Band31亮温对深对流的识别共同决定。
            cirrus = (np.clip(dif, 1, 3) - 1) / 2
            deep_conv = (np.clip(bt31, -70, -50) + 70) / 20
            coeff = np.clip(ref - cirrus, 0, 1) * (1 - 0.5 * deep_conv)
            将模拟反射率叠上光影效果,注意光影效果的数组少一列,因此反射率和光影系数也要裁掉一列才能运算。
            ref = ref[:, 1:] + shade * coeff[:, 1:]

            注入灵魂.png


            IP属地:美国41楼2024-04-26 11:19
            收起回复
              陆地叠图部分原来的代码乱的有点看不下去容我改一改再发这部分的教程


              IP属地:美国42楼2024-04-27 12:39
              回复
                陆地叠图:
                伪VIS的陆地叠图有两种方式:Blue Marble和Black Marble。这里我们主要讲解流程更复杂的Blue Marble叠图。
                Blue Marble:
                https://visibleearth.nasa.gov/images/57752/blue-marble-land-surface-shallow-water-and-shaded-topography
                下面是个人预处理的Blue Marble底图,海面弄蓝了一点:
                archive.dapiya.top/AI-VIS_Demo/Worldv3.png
                (感谢@小谦.WX 帮忙生成图片链接)
                读取图片可以使用第三方库opencv:
                import cv2
                image = cv2.imread(r'底图文件路径', cv2.IMREAD_COLOR)
                Blue Marble是覆盖全球的等经纬投影地图。我们希望将其裁到和卫星亮温、经纬度等数据相同的网格上,即2030×1354的扫描网格,以便后续运算。我们将每个像素的经纬度转化为该经纬度在全球等经纬地图中的行列号,并把对应行列号的Blue Marble像素填回扫描网格。
                indexlon = np.round(lon*60).astype(int)
                indexlat = np.rint((90-lat)*60).astype(int)
                BM = image[indexlat, indexlon, :]/255
                这时候如果我们直接把Blue Marble画出来:
                plt.pcolormesh(newlon, newlat, BM)

                陆地的裁取没什么问题,且底图确实和亮温,经纬度等数据处于同一网格。唯一的问题是颜色不对,这是由于Matplotlib默认按照r, g, b的顺序,而opencv读取的顺序是b, g, r,将BM的第三个维度取倒序就能得到颜色正确的底图了。
                plt.pcolormesh(newlon, newlat, BM[:, :, ::-1])


                IP属地:美国43楼2024-04-28 09:16
                回复
                  接下来就是将模拟反射率和底图通过合成算法叠起来。此处许多步骤相当经验性,并且色调与代码可能还有不小的优化空间。有能力的读者可以自己尝试优化这部分代码,不过直接用我现在这版应该也大差不差。
                  将底图分解为rgb三个通道,并去除一列与合成好的模拟反射率对齐
                  b1, g1, r1 = cv2.split(BM[:, 1:])
                  提亮模拟反射率以免合成后海面过暗
                  b = 0.96 * ref + 25/255
                  初步合成模拟反射率与底图
                  blue = b * b1 + np.power(b, 1.6) * (1 - b1)
                  green = b * g1 + np.power(b, 1.6) * (1 - g1)
                  red = b * r1 + np.power(b, 1.6) * (1 - r1)
                  land_brightness = np.clip((g1 + r1)/2, 0, 0.4)
                  后续调色需要海洋和陆地分开调,通过映射底图的r通道做一个海陆分离,对于陆地像素ocean=0,海面像素ocean=1。
                  norm2 = Normalize(vmin=0, vmax=1, clip=True)
                  map2 = ScalarMappable(norm2, LinearSegmentedColormap('1', oceandata))
                  ocean = map2.to_rgba(r1)[:, :, 0]
                  用于映射的色阶oceandata:
                  oceandata={'green': [(0, 0, 0),
                  (1, 1, 1)],
                  'red': [(0, 255/255, 255/255),
                  (15/255, 255/255, 255/255),
                  (30/255, 0/255, 0/255),
                  (1, 0, 0)],
                  'blue': [(0, 0, 0),
                  (1, 1, 1)]}
                  根据亮温,模拟反射率调整图像饱和度。反射率越高/亮温越低,饱和度越低,陆地的饱和度也设置的比海面更高一些。这里饱和度的定义并不正式,和HSV空间中的有些区别。
                  Saturation = 1.5 * (1 - np.clip(ref - 0.4, 0, 0.5)/0.5) + (1 - ocean) * 3 * (np.clip(bt31[:, 1:], -60, 10) + 60)/70
                  avg = (blue + green + red) / 3
                  blue = (avg + (blue - avg) * Saturation) * 0.98
                  green = avg + (green - avg) * Saturation
                  red = avg + (red - avg) * Saturation
                  调整一下洋面颜色(这段两年多前写的有点过于繁琐,大概率是可以继续优化的):
                  recolor = np.clip(green, 0.1, 0.3) - 0.1
                  red = red * (0.2 + recolor * 4) * ocean + (1-ocean) * red
                  green = green * (0.8 + recolor) * ocean + (1-ocean) * green
                  norm3 = Normalize(vmin=0, vmax=1, clip=True)
                  map3 = ScalarMappable(norm3, LinearSegmentedColormap('1', recolordata))
                  recolor2 = map3.to_rgba(green)[:, :, 0]
                  green = green + (- 10/255 + recolor2) * ocean
                  用于调整洋面颜色的色阶:
                  recolordata={'green': [(0, 0, 0),
                  (1, 1, 1)],
                  'red': [(0, 0/255, 0/255),
                  (8/255, 4/255, 4/255),
                  (20/255, 2/255, 2/255),
                  (37/255, 10/255, 10/255),
                  (57/255, 10/255, 10/255),
                  (74/255, 16/255, 16/255),
                  (140/255, 15/255, 15/255),
                  (160/255, 10/255, 10/255),
                  (1, 10/255, 10/255)],
                  'blue': [(0, 0, 0),
                  (1, 1, 1)]}
                  将三个颜色通道合并,限制到0~1的值以便pcolormesh画图:
                  rgb = np.stack([red, green, blue], axis=2)
                  rgb = np.clip(rgb, 0, 1)
                  出图效果如下:


                  IP属地:美国44楼2024-04-28 15:03
                  回复
                    直接就交作业


                    IP属地:江西来自Android客户端47楼2024-04-28 17:43
                    收起回复
                      教程还剩最后一节没写,这个月也只剩一天半了
                      怎么我做啥都得赶着ddl交啊


                      IP属地:美国来自Android客户端49楼2024-04-29 13:26
                      回复
                        b. 海温反演修正与高云修正
                        相对于前面介绍的伪VIS基础算法,海温反演修正和高云修正更像是锦上添花的步骤,并且更具实验性也更不成熟。但既然目录中列都列出来了,还是把相关代码贴一下供参考。
                        目前版本的两项修正互相不兼容,一颗卫星只能应用一项修正。MODIS和H8伪VIS应用的是海温反演修正以改善高纬度的成像效果,而VIIRS,SLSTR等卫星一般绘制的范围较小,因此应用的是高云修正以改善TC卷云下方的细节。
                        海温反演修正调整了高温区模拟反射率(ref_warm)算法,伪VIS的其他部分则保持不变。海温反演公式中除了两个劈窗算法用到的波段Band31和Band32,还需要输入一个“SST参考值” Tsfc。我们这里不作精确的海温反演,仅用于修正伪VIS,这个参考值用季节+纬度估算一下即可。

                        文件名中包含的一年中的日期可以用于估算季节,例如MOD021KM.A2015296.0515.061.2017322234443.hdf这个文件就是2015年第296天拍的。我们稍微修改一下读取HDF时路径的输入方式即可用第三方库os将文件名单独提取出来。
                        import os
                        path = r'E:\Data\MOD021KM.A2015296.0515.061.2017322234443.hdf'
                        dir, filename = os.path.split(path)
                        file = SD(path, SDC.READ)
                        使用第三方库datetime将一年中的日期转化为UTC时间,这里相比实际的时间减了50天以体现季节/气候带相对于太阳赤纬的滞后
                        import datetime
                        day = int(filename[14:17])
                        time = datetime.datetime(2023, 1, 1) - datetime.timedelta(days=50) + datetime.timedelta(days=day)
                        使用第三方库pyorbital计算此时的太阳赤纬。求季节导致的气候带偏移时,考虑实际的偏移幅度比太阳直射点的偏移幅度小,且主要洋区北半球高海温区域更广阔,这里将太阳赤纬±23.5°的变化范围缩放到了±10度,并向北半球略微偏移了一点。
                        (顺带一提pyorbital.astronomy还是挺常用的,尤其是不方便直接从数据集取角度信息时,比如AI-VIS的太阳/卫星天顶角/方位角都是用这个读的)
                        from pyorbital.astronomy import sun_ra_dec
                        lat_shift = np.rad2deg(sun_ra_dec(time))[1] / 23.5 * 9 + 2
                        根据气候带偏移计算纬度参数,该参数越大对应越冷的区域,注意这里带入的纬度需要是已插值的。
                        lat_factor = np.clip(np.absolute(newlat - lat_shift) - 15, 0, 45)/50 * 1.22 * (1.06 - lat_shift/100) + 0.01 - 9/250
                        给出(非常)粗略的SST参考值:
                        SST_guess = 28 - lat_factor * 28
                        用简化后的劈窗算法反演SST。官方文件给出的反演系数对于TERRA与AQUA略有差异,因此需要根据文件名写一个卫星判定。
                        Terra_or_Aqua = (filename[1])
                        if Terra_or_Aqua == 'O':
                        SST = 5.77141142 + 0.777202129*bt31 + 0.116606250 *(bt31 - bt32) * SST_guess + 2 #TERRA
                        else:
                        SST = 4.98995447 + 0.806239545*bt31 + 0.106239545 * (bt31 - bt32) * SST_guess + 2 #AQUA
                        劈窗算法的SST对于有云遮盖的区域严重偏低,直接带入高温区模拟反射率的计算公式会导致部分云系明显偏暗。这里我们用Band32和Band20的差分将这些云识别出来,化为一个权重(SST_weight),对于有云的区域该权重偏低,倾向于使用原本的公式(bt20 - 28) / (bt31 - 30),而对于无云区域该权重接近1,会采用带入反演SST的方式计算反射率。correction这个参数同样也是为了防止热带云系的反射率偏暗设置的。
                        dif2 = bt32 - bt20
                        SST_weight = np.clip(dif2 + 13, 0, 13) / 13 * (0.6 + 1.1 * lat_factor) * (0.3 + np.clip(SST + 20, 0, 20)/20 * 0.7)
                        correction = 0.9 + np.clip(lat_factor, 0, 0.5) * 0.2
                        ref_warm = (bt20 - (SST-30) * correction - 28)/(bt31 - (SST - 30) * correction - 30) * SST_weight + (bt20 - 28) / (bt31 - 30) * (1 - SST_weight)
                        (当时公式写这么长不分段也不写模块化真的是很糟糕的习惯,大家不要学我)
                        这里得到的ref_warm即可替代原有的高温区反射率。海温修正前与修正后效果对比如下:


                        看上去效果差不多,这是因为Patricia本身海温就很高,不需要什么海温修正,甚至原本因为高海温过暗的洋面还稍微变亮了一点。如果画一下5分钟后的下一片MODIS数据对比就很明显了:


                        海温修正前的海陆颜色均有明显偏白的现象,修正后基本正常了。部分陆地云系和冷流云的模拟还是不够准确,不过伪VIS作为主打热带气旋的算法,高纬区域效果做到大致能看即可。


                        IP属地:美国50楼2024-04-30 14:16
                        回复
                          高云修正同样是调整了高温区模拟反射率(ref_warm)的算法。
                          首先将卷云识别出来,这里的系数对于不同的长波波段组合会不同,以下dif代表VIIRS M15 M16的亮温差,如果替换到其他波段系数需要作一定调整。
                          cirrus2 = np.clip(dif - 2.2, 0, 2.4)/2.4
                          通过幂运算提高卷云覆盖区域的对比度。由于幂运算会让图像变暗,再乘一个系数线性提亮一下即可。
                          ref_corrected = np.power(ref_warm, 1 + cirrus2) * np.power((1 + cirrus2), 1.35)
                          通过cirrus_weight对高云修正的区域和未经修正的区域做一个过渡。
                          cirrus_weight = np.clip(ref_corrected - 0.735, 0, 0.21)/0.21
                          ref_warm = ref_corrected * (1 - cirrus_weight) + ref_warm * cirrus_weight
                          例图就不放了,高云修正在这张Patricia上效果并不明显,主要是渲染VIIRS那种风眼/CDO特写时比较好用。


                          IP属地:美国52楼2024-04-30 21:43
                          回复
                            总算在四月最后几个小时把教程写完了!不过最后一小段看起来还在被吞
                            明天可能会再来写个后记之类的


                            IP属地:美国来自Android客户端54楼2024-04-30 21:46
                            回复
                              HDFSD和SDC import不了,用你的代码是他显示没有模块,去你给的网站看了好像要安装HDF4 吗?


                              IP属地:广东55楼2024-05-01 22:31
                              收起回复