跳转至

创建马歇尔火灾的GOES-16 ABI气溶胶检测可视化

原文链接: https://www.nv5geospatialsoftware.com/Learn/Blogs/Blog-Details/creating-goes-16-abi-aerosol-detection-visualizations-of-the-marshall-fire

9287 为本文评分:

3.9

创建马歇尔火灾的GOES-16 ABI气溶胶检测可视化

大卫·斯塔巴克 2022年5月2日,星期一

2021年秋季的科罗拉多州极度干旱,12月30日,当二级飓风级风力袭击该地区时,一场城市野火爆发,导致了科罗拉多州历史上最大的财产损失。马歇尔火灾摧毁了苏必利尔市、路易斯维尔市以及博尔德县未建制地区的一千多座建筑。该地区的居民往往只有几分钟的时间撤离,许多人只能穿着身上的衣服逃离。

除了损失的一千多座建筑外,许多其他房屋和企业也遭受了严重的烟雾损害。虽然极端大风和长期干旱为火灾失控创造了理想条件,但居民们现在面临一个现实:这种情况可能并非罕见的风暴叠加事件。在马歇尔火灾之后,科罗拉多州前岭地区(该地区人口众多,靠近火灾顶级生态系统)正在接受地方性野火和全年皆是的火灾季节这一真实可能性。

利用影像研究野火

地理空间影像可用于研究火灾前的条件、监测火灾期间的情况以及评估灾后损失。来自高级基线成像仪(ABI)的数据(ABI是安装在L3Harris建造的GOES-16平台上的传感器),使我们能够以高时间、空间和光谱分辨率研究所有类型的天气现象。某些波段会突出烟雾,另一些会突出不同大小的气溶胶,还有其他波段则突出火灾本身。能够访问、分析和可视化ABI数据的人越多,我们就能从这类事件中学到更多东西,了解它们如何影响人们的生活、健康、航空安全等。

ABI数据集有直达NOAA以及威斯康星大学等机构的设施用于预报业务,但也通过AWS S3存储桶公开提供。这篇博文提供了代码示例,并展示了任何人如何使用IDL®编程语言从AWS S3中拉取该日期的GOES-16 ABI数据,创建显示烟雾动态和范围的RGB图像,并创建气溶胶检测数据产品。

访问ABI数据的逐步指南

访问ABI数据的第一步是使用IDL中的IDLnetURL对象从AWS S3拉取数据。我使用AWS Amazon S3 API指南来了解如何填写IDLnetURL参数 - Amazon S3 - Amazon Simple Storage Service。根据这个指南,我编写了以下例程来查询Goes-16的S3数据。



    function dj_goes_16_s3_query, prefix=prefix, print=print

  compile_opt idl2



  s3UrlObject = obj_new('idlneturl')

  bucket = 'noaa-goes16'

  region = 'us-east-1'

  s3UrlObject.setProperty, url_host = bucket + '.s3.' + region + '.amazonaws.com'



  urlQuery = 'list-type=2&delimiter=/'



  if keyword_set(prefix) then begin

    urlQuery = urlQuery + '&prefix='+prefix

  endif 







  keyList=list()

  dateList = list()

  sizeList = list()



  gotAll = 0 

  startAfter = ''

  initUrlQuery = urlQuery 

  while (gotAll eq 0) do begin

    urlQuery = initUrlQuery

    if startAfter ne '' then begin

      urlQuery = urlQuery + '&start-after=' + startAfter

      startAfter = ''

    endif



    s3urlObject.setproperty, url_query= urlQuery

    result = s3urlObject.get()



    parsedData = xml_parse(result)



    if ((parsedData['ListBucketResult']).haskey('Contents')) then begin

      contents = ((parsedData['ListBucketResult'])['Contents'])

      for  awsKeyIndex=0L, n_elements(contents)-1 do begin

        keyList.add, (contents[awsKeyIndex])['Key']

        dateList.add, (contents[awsKeyIndex])['LastModified']

        sizeList.add, (contents[awsKeyIndex])['Size']

      endfor

      if keyword_set(print) then begin

        outArray = strarr(3,n_elements(keyList))

        outArray[0,*] = transpose(keyList.toArray())

        outArray[1,*] = transpose(dateList.toArray())

        outArray[2,*] = transpose(sizeList.toArray())

        print, '---------------------------------'

        print, 'Key, Date, Size

        print, '---------------------------------'

        print, outArray

      endif

    endif



    prefixList= list()

    if ((parsedData['ListBucketResult']).haskey('CommonPrefixes')) then begin

      commonPrefixes = ((parsedData['ListBucketResult'])['CommonPrefixes'])

      if n_elements(commonPrefixes) eq 1 then begin

        prefixList.add, commonPrefixes['Prefix']

      endif else begin

        for prefixKeyIndex=0L, n_elements(commonPrefixes)-1 do begin

          prefixList.add, (commonPrefixes[prefixKeyIndex])['Prefix']

        endfor

      endelse

      if keyword_set(print) then begin

        print, '---------------------------------'

        print, 'Prefixes'

        print, '---------------------------------'

        print, transpose(prefixList.toArray());objListStructor['prefixlist']

      endif

    endif



    if ((parsedData['ListBucketResult'])['IsTruncated'] eq 'true') then begin

      startAfter = keyList[-1]

    endif else begin

      gotAll = 1

    endelse



  endwhile



  returnHash = hash()

  returnhash['keylist'] = keyList

  returnhash['datelist'] = dateList

  returnhash['sizelist'] = sizeList

  returnhash['prefixlist'] = prefixList  



  obj_destroy, s3UrlObject



  return, returnhash

end



在此例程中,我使用 "noaa-goes16" 作为存储桶,"us-east-1" 作为区域。





         s3UrlObject = obj_new('idlneturl')

  bucket = 'noaa-goes16'

  region = 'us-east-1'

  s3UrlObject.setProperty, url_host = bucket + '.s3.' + region + '.amazonaws.com'

我使用 "list-type=2" 和 "delimiter=/" 作为 "url_query" 属性的参数。运行查询将返回存储桶顶层可用的键(文件)和前缀(子目录)列表。如果没有分隔符,响应会列出存储桶中的所有文件,这将创建一个非常长的列表。





          s3urlObject.setproperty, url_query='list-type=2&delimiter=/'

  result = s3urlObject.get()



响应将是一个XML对象,可以使用xml_parse例程进行解析。之后,可以提取键和前缀。





    parsedData = xml_parse(result)

在没有指定任何前缀的情况下运行 "dj_goes_16_s3_query",将产生以下结果:





        IDL> topLevelQuery = dj_goes_16_s3_query(/print)

---------------------------------

Key, Date, Size

---------------------------------

Beginners_Guide_to_GOES-R_Series_Data.pdf 2021-04-02T17:12:02.000Z 5887019

Version1.1_Beginners_Guide_to_GOES-R_Series_Data.pdf 2021-03-23T15:49:55.000Z 3578396

index.html 2021-09-27T19:48:14.000Z 32357

---------------------------------

Prefixes

---------------------------------

ABI-L1b-RadC/

ABI-L1b-RadF/

ABI-L1b-RadM/

ABI-L2-ACHAC/

ABI-L2-ACHAF/

ABI-L2-ACHAM/

ABI-L2-ACHTF/

ABI-L2-ACHTM/

ABI-L2-ACMC/

ABI-L2-ACMF/

ABI-L2-ACMM/

…

SUVI-L1b-Fe284/

然后,我使用 "prefix" 参数来查询特定前缀下的键和前缀。例如,我使用以下调用来查看 "ABI-L1b-RadC/" 下可用的前缀:



IDL> productTypeLevelQuery = dj_goes_16_s3_query(/print, prefix='ABI-L1b-RadC/')

---------------------------------

Prefixes

---------------------------------

ABI-L1b-RadC/2000/

ABI-L1b-RadC/2017/

ABI-L1b-RadC/2018/

ABI-L1b-RadC/2019/

ABI-L1b-RadC/2020/

ABI-L1b-RadC/2021/

ABI-L1b-RadC/2022/

我继续对这个过程使用进一步的子目录,并为每个键找到了以下结构:



<Product Type>/<year>/<day>/<hour>/<filename.nc>

我参考了GOES-16: True Color Recipe — Unidata Python Gallery教程来了解如何从GOES-16数据生成RGB图像。根据该教程,应使用二级多通道格式化数据产品(ABI-L3-MCIMP)。因此,我使用以下查询来确定2022年12月30日该类型有哪些可用文件:



IDL> hourlevelQuery = dj_goes_16_s3_query(/print, prefix='ABI-L2-MCMIPC/2021/364/17/')

我打印了结果,发现第一个可用的键是:



ABI-L2-MCMIPC/2021/364/17/OR_ABI-L2-MCMIPC-M6_G16_s20213641701172_e20213641703545_c20213641704048.nc

我编写了以下例程来从存储桶下载文件。



function dj_goes_16_s3_download, key, outputdirectory=outputdirectory

  compile_opt idl2



  s3UrlObject = obj_new('idlneturl')

  bucket = 'noaa-goes16'

  region = 'us-east-1'

  s3UrlObject.setProperty, url_host = bucket + '.s3.' + region + '.amazonaws.com'



  fileBaseSplitString = strsplit(key, '/', /EXTRACT)

  fileBaseName = fileBaseSplitString[-1]

  s3urlObject.setProperty, url_path=Key



  if (keyword_set(outputdirectory)) then begin

    outputFilename = filepath(filebasename, ROOT_DIR=outputDirectory)

  endif else begin

    outputFilename = fileBaseName

  endelse



  ;if file already exist, don't re-download it

  if (file_test(outputFilename)) then begin

    objectFile = outputfilename

  endif else begin

    objectFile = s3urlObject.get(filename=outputFileName)

  endelse



  return, objectFile

end

要下载文件,我使用键 "ABI-L2-MCMIPC/2021/364/17/OR_ABI-L2-MCMIPC-M6_G16_s20213641701172_e20213641703545_c20213641704048.nc" 运行了 "dj_goes_16_s3_download":



IDL>objectFile = dj_goes_16_s3_download('ABI-L2-MCMIPC/2021/364/17/OR_ABI-L2-MCMIPC-M6_G16_s20213641701172_e20213641703545_c20213641704048.nc')

GOES-16数据是NetCDF格式,可以使用ncdf_parse进行解析。



data = ncdf_parse(objectFile, /read_data) 



center_lon = data['geospatial_lat_lon_extent', $

        'geospatial_lon_nadir', '_DATA']

xscale = data['x', 'scale_factor', '_DATA']

xoffset = data['x', 'add_offset', '_DATA']

x_radians = data['x', '_DATA']*DOUBLE(xscale) + xoffset

yscale = data['y', 'scale_factor', '_DATA']

yoffset = data['y', 'add_offset', '_DATA']

y_radians = data['y', '_DATA']*DOUBLE(yscale) + yoffset

我使用以下命令从数据中获取红色、蓝色和真绿色值。在缩放时,我将最大值设置为2,000,因为我查看了直方图,发现大部分数据都低于该值。



redBand = bytscl(data['CMI_C02', '_DATA'], max=2000)

greenBand = bytscl(data['CMI_C03', '_DATA'], max=2000)

blueBand = bytscl(data['CMI_C01', '_DATA'], max=2000)



trueGreen = bytscl(0.45*redBand + 0.1*greenBand + 0.45*blueBand)



bandDims = size(redBand, /DIMENSIONS)

rgbArray = fltarr(3,bandDims[0],bandDims[1])

rgbArray[0,*,*] = redBand

rgbArray[1,*,*] = trueGreen

rgbArray[2,*,*] = blueBand

然后,我使用image函数来显示数据:



windowDimX = 800

windowDimY = 450

margin = [.2,.2,.1,.1]

i = IMAGE(bytscl(rgbArray), x_radians, y_radians, $

 MAP_PROJECTION='GOES-R', $

 margin=margin,    $

 GRID_UNITS='meters', $

 CENTER_LONGITUDE=center_lon, $

 DIMENSIONS=[windowDimX,windowDimY], $

 TITLE='GOES-16 '+ data['time_coverage_start', '_DATA'])

mg = i.MapGrid

mg.label_position = 0

mg.clip = 0 

mc = MAPCONTINENTS(/COUNTRIES, COLOR='red')

mc = MAPCONTINENTS(/US, COLOR='red')

然后,我遵循类似的程序来显示气溶胶检测。气溶胶数据是一个包含三个值(0, 1, 255)的字节数组。我添加了一个颜色表,将0设置为黑色,1设置为红色,255设置为白色。



objectFile = dj_goes_16_s3_download('ABI-L2-ADPC/2021/364/17/OR_ABI-L2-ADPC-M6_G16_s20213641701172_e20213641703545_c20213641705442.nc')

data = ncdf_parse(objectFile, /read_data)

center_lon = data['geospatial_lat_lon_extent', $

   'geospatial_lon_nadir', '_DATA']

xscale = data['x', 'scale_factor', '_DATA']

xoffset = data['x', 'add_offset', '_DATA']

x_radians = data['x', '_DATA']*DOUBLE(xscale) + xoffset

yscale = data['y', 'scale_factor', '_DATA']

yoffset = data['y', 'add_offset', '_DATA']

y_radians = data['y', '_DATA']*DOUBLE(yscale) + yoffset

aerosol = data['Aerosol', '_DATA']

rgbTable = bytarr(3,255)

rgbTable[*,0] = 255

rgbTable[0,1] = 255

rgbTable[1,1] = 0

rgbTable[2,1] = 0

windowDimX = 800

windowDimY = 450

margin = [.2,.2,.1,.1]

i = IMAGE(aerosol, x_radians, y_radians, $

  MAP_PROJECTION='GOES-R', GRID_UNITS='meters', $

  margin=margin, $

  CENTER_LONGITUDE=center_lon, $

  RGB_TABLE=rgbTable, $

  DIMENSIONS=[windowDimX,windowDimY], $

  TITLE='GOES-16 Aerosol '+ data['time_coverage_start', '_DATA'])

以下程序是一个示例程序,它使用 "dj_goes_16_s3_query" 和 "dj_goes_16_s3_download" 来生成马歇尔火灾期间(UTC时间17:00至22:00)使用GOES-16数据的真彩色图像动画。如果使用气溶胶关键字运行此程序,它将使用气溶胶检测GOES-16数据产品生成动画。





    pro dj_example_plot_areosol_goes_aws, aerosol=aerosol



  compile_opt idl2



  windowDimX = 800

  windowDimY = 450

  margin = [.2,.2,.1,.1]



  topLevelQuery = dj_goes_16_s3_query(/print)

  prefixes = (topLevelQuery['prefixlist']).toArray()

  multiChannelIndex = where(strmatch(prefixes,'*ABI-L2-MCMIPC*'))

  aerosolIndex = where(strmatch(prefixes, '*ABI-L2-ADPC*'))

  if keyword_set(aerosol) then begin

    productTypeLevelQuery = dj_goes_16_s3_query(prefix=prefixes[aerosolIndex[0]], /print)

  endif else begin

    productTypeLevelQuery = dj_goes_16_s3_query(prefix=prefixes[multiChannelIndex[0]], /print)

  endelse



  prefixes = (productTypeLevelQuery['prefixlist']).toArray()

  yearIndex = where(strmatch(prefixes,'*2021*'))

  yearLevelQuery = dj_goes_16_s3_query(prefix=prefixes[yearIndex[0]], /print)



  prefixes = (yearLevelQuery['prefixlist']).toArray()

  dayIndex = where(strmatch(prefixes,'*364*'))

  dayLevelQuery = dj_goes_16_s3_query(prefix=prefixes[dayIndex[0]], /print)



  hoursPrefixes = dayLevelQuery['prefixlist']



  ;Create video object for projected video

  if keyword_set(aerosol) then begin

    oVid = IDLffVideoWrite('aerosol_projected_output.mp4',FORMAT='mp4')

    vidstream = oVid.AddVideoStream(windowdimx,windowdimy,10)

  endif else begin

    oVid = IDLffVideoWrite('projected_output.mp4',FORMAT='mp4')

    vidstream = oVid.AddVideoStream(windowdimx,windowdimy,10)

  endelse





  ;Store a list of the hours prefixes for

  ;hoursPrefixes = objListStructor['prefixlist']



  for hoursIndex = 17L, 21 do begin



    hourLevelQuery = dj_goes_16_s3_query(prefix=hoursPrefixes[hoursIndex], /print)

    objectKey = (hourLevelQuery['keylist'])



    for objectKeyIndex = 0L, n_elements(objectKey)-1 do begin



      objectFile = dj_goes_16_s3_download(objectKey[objectKeyIndex])



      data = ncdf_parse(objectFile, /read_data)



      print, 'Time start = ', data['time_coverage_start', '_DATA']



      ; Extract the center longitude and radiance data

      center_lon = data['geospatial_lat_lon_extent', $

        'geospatial_lon_nadir', '_DATA']

      ; radiance = data['Rad','_DATA']



      ; Compute the X/Y pixel locations in radians

      xscale = data['x', 'scale_factor', '_DATA']

      xoffset = data['x', 'add_offset', '_DATA']

      x_radians = data['x', '_DATA']*DOUBLE(xscale) + xoffset

      yscale = data['y', 'scale_factor', '_DATA']

      yoffset = data['y', 'add_offset', '_DATA']

      y_radians = data['y', '_DATA']*DOUBLE(yscale) + yoffset



      if (keyword_set(aerosol)) then begin

        aerosol = data['Aerosol', '_DATA']

        smoke = data['Smoke', '_DATA']



        rgbTable = bytarr(3,255)

        rgbTable[*,0] = 255

        rgbTable[0,1] = 255

        rgbTable[1,1] = 0

        rgbTable[2,1] = 0



        if (obj_valid(i)) then begin

          i.setdata, aerosol, x_radians, y_radians

          i.title = 'GOES-16 Aerosol '+ data['time_coverage_start', '_DATA']

        endif else begin

          ; Use the GOES-R (GOES-16) map projection

          i = IMAGE(aerosol, x_radians, y_radians, $

            MAP_PROJECTION='GOES-R', GRID_UNITS='meters', $

            margin=margin, $

            CENTER_LONGITUDE=center_lon, $

            RGB_TABLE=rgbTable, $

            DIMENSIONS=[windowDimX,windowDimY], $

            TITLE='GOES-16 Aerosol '+ data['time_coverage_start', '_DATA'])



          i.limit = [38,-106, 41, -101]

          mg = i.MapGrid

          mg.label_position = 0

          mg.clip = 0

          mg.linestyle = 'none'

          mg.box_axes = 1



          mc = MAPCONTINENTS(/COUNTRIES, COLOR='teal')

          mc = MAPCONTINENTS(/US, COLOR='teal')



          annotationArrow = arrow([-105,-105.1],[40.5,40.05], /DATA,$

            COLOR='blue', $

            /CURRENT)



          t2 = text(-104.995, 40.51,'Approx. Smoke Origin', color='blue', /DATA, font_style=1)

        endelse



        time = ovid.put(vidStream, i.copywindow())

        print, time



      endif else begin

        redBand = bytscl(data['CMI_C02', '_DATA'], max=2000)

        greenBand = bytscl(data['CMI_C03', '_DATA'], max=2000)

        blueBand = bytscl(data['CMI_C01', '_DATA'], max=2000)



        trueGreen = bytscl(0.45*redBand + 0.1*greenBand + 0.45*blueBand)



        bandDims = size(redBand, /DIMENSIONS)

        rgbArray = fltarr(3,bandDims[0],bandDims[1])

        rgbArray[0,*,*] = redBand

        rgbArray[1,*,*] = trueGreen

        rgbArray[2,*,*] = blueBand



        ;Apply squareroot stetch to improve image clarity. 

        rgbArray = sqrt(rgbArray/255)*255



        ; Use the GOES-R (GOES-16) map projection

        if obj_valid(i) then begin

          ;skip frame at 2021-12-30T19:31:17.2Z

          if (data['time_coverage_start', '_DATA'] ne '2021-12-30T19:31:17.2Z') then begin

            i.setData, rgbArray, x_radians, y_radians

            i.title = 'GOES-16 '+ data['time_coverage_start', '_DATA']

          endif         

        endif else begin

          i = IMAGE(bytscl(rgbArray), x_radians, y_radians, $

            MAP_PROJECTION='GOES-R', $

            margin=margin, $

            GRID_UNITS='meters', $

            CENTER_LONGITUDE=center_lon, $

            DIMENSIONS=[windowDimX,windowDimY], $

            TITLE='GOES-16 '+ data['time_coverage_start', '_DATA'])



          i.limit = [38,-106, 41, -101]

          mg = i.MapGrid

          mg.label_position = 0

          mg.clip = 0

          mg.linestyle = 'none'

          mg.box_axes = 1



          mc = MAPCONTINENTS(/COUNTRIES, COLOR='red')

          mc = MAPCONTINENTS(/US, COLOR='red')



          annotationArrow = arrow([-105,-105.1],[40.5,40.05], /DATA,$

             COLOR='red', $

             /CURRENT)



          t2 = text(-104.995, 40.51,'Approx. Smoke Origin', color='red', /DATA, font_style=1)

        endelse

        ;skip frame at 2021-12-30T19:31:17.2Z

        if (data['time_coverage_start', '_DATA'] ne '2021-12-30T19:31:17.2Z') then begin

          time = ovid.put(vidStream, i.copywindow())

          print, time

        endif

      endelse

    endfor

  endfor

  ovid = 0

end

感谢您的关注。请期待更多关于使用GOES、EUMETSAT和其他仪器访问和创建可视化的博客。IDL提供了对复杂数据和代码的灵活访问,并且可以通过IDL虚拟机(一种免费运行IDL代码的方式!)与同事共享。我的团队擅长使用ENVI®和IDL解决图像相关的难题,并连接其他编程语言。如果您有我们可能能够协助的项目,请发送电子邮件至GeospatialInfo@NV5.com

探索更多

录制的网络研讨会

SAR Webinar Series

SAR三部分系列

学习合成孔径雷达(SAR)的应用和优势。

立即观看

真实案例研究

SAR网络研讨会

使用SAR干涉测量技术进行地表运动监测

立即观看

灾害管理

录制的网络研讨会

在灾害管理周期中有效使用地理空间数据

立即观看

2022年春季学生聚焦 世界各地的冲击波