|
24 | 24 | "We'll start by importing the Python libraries we'll be using.\n", |
25 | 25 | "Following common conventions, we'll abbreviate\n", |
26 | 26 | "[pandas](https://pandas.pydata.org) as `pd` and\n", |
27 | | - "[xarray](http://xarray.pydata.org) as `xr`." |
| 27 | + "[xarray](http://xarray.pydata.org) as `xr`.\n", |
| 28 | + "A little later into this tutorial,\n", |
| 29 | + "we'll also make use of\n", |
| 30 | + "[rioxarray](https://corteva.github.io/rioxarray) and\n", |
| 31 | + "[scikit-image](https://scikit-image.org)." |
28 | 32 | ] |
29 | 33 | }, |
30 | 34 | { |
|
35 | 39 | "source": [ |
36 | 40 | "import pandas as pd\n", |
37 | 41 | "import pygmt\n", |
| 42 | + "import rioxarray\n", |
| 43 | + "import skimage.exposure\n", |
38 | 44 | "import xarray as xr" |
39 | 45 | ] |
40 | 46 | }, |
|
301 | 307 | "cell_type": "markdown", |
302 | 308 | "metadata": {}, |
303 | 309 | "source": [ |
304 | | - "The next steps are just some additional preprocessing.\n", |
305 | | - "We'll remove the NaN values so that they don't get plotted,\n", |
306 | | - "and also sort our grid so that the x and y dimensions\n", |
307 | | - "go in ascending order (i.e. West to East, South to North)." |
| 310 | + "The next step is just some additional preprocessing.\n", |
| 311 | + "We'll remove the NaN values so that they don't get plotted." |
308 | 312 | ] |
309 | 313 | }, |
310 | 314 | { |
|
314 | 318 | "outputs": [], |
315 | 319 | "source": [ |
316 | 320 | "grid = grid.where(grid != grid.nodatavals, drop=True)\n", |
317 | | - "grid = grid.sortby(variables=list(grid.dims))\n", |
318 | 321 | "grid" |
319 | 322 | ] |
320 | 323 | }, |
|
359 | 362 | " " |
360 | 363 | ] |
361 | 364 | }, |
| 365 | + { |
| 366 | + "cell_type": "markdown", |
| 367 | + "metadata": {}, |
| 368 | + "source": [ |
| 369 | + "## **2.3 Looking at true colour bands**\n", |
| 370 | + "\n", |
| 371 | + "What if your raster grid is not a DEM,\n", |
| 372 | + "but a multi-band satellite image instead?\n", |
| 373 | + "Well, grdimage also accepts RGB bands,\n", |
| 374 | + "but there are a couple of tricks we'll need to learn for things to work properly.\n", |
| 375 | + "\n", |
| 376 | + "Let's have a go at plotting a Landsat 8 Image of good ol' Wellington (and Cook Strait) this time.\n", |
| 377 | + "To give you a quick sense of the place from space, we'll do a quick preview of the JPG image." |
| 378 | + ] |
| 379 | + }, |
| 380 | + { |
| 381 | + "cell_type": "code", |
| 382 | + "execution_count": null, |
| 383 | + "metadata": {}, |
| 384 | + "outputs": [], |
| 385 | + "source": [ |
| 386 | + "fig = pygmt.Figure()\n", |
| 387 | + "fig.grdimage(\n", |
| 388 | + " grid=\"https://landsat-pds.s3.amazonaws.com/c1/L8/073/089/LC08_L1TP_073089_20191021_20191030_01_T1/LC08_L1TP_073089_20191021_20191030_01_T1_thumb_large.jpg\",\n", |
| 389 | + " frame=True,\n", |
| 390 | + ")\n", |
| 391 | + "fig.show()" |
| 392 | + ] |
| 393 | + }, |
| 394 | + { |
| 395 | + "cell_type": "markdown", |
| 396 | + "metadata": {}, |
| 397 | + "source": [ |
| 398 | + "### **Getting some cloud optimized GeoTIFFs**\n", |
| 399 | + "\n", |
| 400 | + "Landsat 8 data is provided as [cloud optimized GeoTIFFs (COGs)](https://www.cogeo.org/)\n", |
| 401 | + "which makes it easy to pull down just what we need from the web.\n", |
| 402 | + "I recommend using [Remote Pixel's Satellite Search API](https://search.remotepixel.ca) to search for images.\n", |
| 403 | + "\n", |
| 404 | + "We'll switch from xarray to [rioxarray](https://corteva.github.io/rioxarray) this time,\n", |
| 405 | + "just because it's open_rasterio method works better with COGs.\n", |
| 406 | + "Instead of pulling down the full 30m resolution dataset,\n", |
| 407 | + "let's keep it easy on the servers and use overview_level 1." |
| 408 | + ] |
| 409 | + }, |
| 410 | + { |
| 411 | + "cell_type": "code", |
| 412 | + "execution_count": null, |
| 413 | + "metadata": {}, |
| 414 | + "outputs": [], |
| 415 | + "source": [ |
| 416 | + "band = {}\n", |
| 417 | + "for band_number in [4, 3, 2]: # Landsat 8's Red, Green and Blue bands\n", |
| 418 | + " band[band_number] = rioxarray.open_rasterio(\n", |
| 419 | + " f\"https://landsat-pds.s3.amazonaws.com/c1/L8/073/089/LC08_L1TP_073089_20191005_20191018_01_T1/LC08_L1TP_073089_20191005_20191018_01_T1_B{band_number}.TIF\",\n", |
| 420 | + " overview_level=1,\n", |
| 421 | + " masked=True,\n", |
| 422 | + " ).squeeze()\n", |
| 423 | + "band[3] # preview the green band" |
| 424 | + ] |
| 425 | + }, |
| 426 | + { |
| 427 | + "cell_type": "markdown", |
| 428 | + "metadata": {}, |
| 429 | + "source": [ |
| 430 | + "We'll concatenate all the bands into a grid to save us having to write for-loops later." |
| 431 | + ] |
| 432 | + }, |
| 433 | + { |
| 434 | + "cell_type": "code", |
| 435 | + "execution_count": null, |
| 436 | + "metadata": {}, |
| 437 | + "outputs": [], |
| 438 | + "source": [ |
| 439 | + "grid = xr.concat(\n", |
| 440 | + " objs=[band[4], band[3], band[2]],\n", |
| 441 | + " dim=pd.Index(data=[4, 3, 2], name=\"band\")\n", |
| 442 | + ")" |
| 443 | + ] |
| 444 | + }, |
| 445 | + { |
| 446 | + "cell_type": "markdown", |
| 447 | + "metadata": {}, |
| 448 | + "source": [ |
| 449 | + "We can try to plot an RGB image by passing a list of the bands, but it won't turn out so good." |
| 450 | + ] |
| 451 | + }, |
| 452 | + { |
| 453 | + "cell_type": "code", |
| 454 | + "execution_count": null, |
| 455 | + "metadata": {}, |
| 456 | + "outputs": [], |
| 457 | + "source": [ |
| 458 | + "fig = pygmt.Figure()\n", |
| 459 | + "fig.grdimage(grid=[grid.sel(band=4), grid.sel(band=3), grid.sel(band=2)], frame=True)\n", |
| 460 | + "fig.show()" |
| 461 | + ] |
| 462 | + }, |
| 463 | + { |
| 464 | + "cell_type": "markdown", |
| 465 | + "metadata": {}, |
| 466 | + "source": [ |
| 467 | + "### **Scaling to 8 bits (0-255)**\n", |
| 468 | + "\n", |
| 469 | + "Each band in a Landsat 8 GeoTIFF file has a dynamic range of 16 bits (0-65535).\n", |
| 470 | + "However, in order to plot an RGB (red, green, blue) image in GMT,\n", |
| 471 | + "each individual band needs to have their values scaled to between 0-255 (i.e. 8 bits).\n", |
| 472 | + "\n", |
| 473 | + "GMT does have a [grdhisteq](https://docs.generic-mapping-tools.org/latest/grdhisteq.html) function\n", |
| 474 | + "for histogram equalization,\n", |
| 475 | + "but it is not available in PyGMT yet.\n", |
| 476 | + "We'll use scikit-image's\n", |
| 477 | + "[equalize_hist](https://scikit-image.org/docs/dev/api/skimage.exposure#skimage.exposure.equalize_hist)\n", |
| 478 | + "function instead for now." |
| 479 | + ] |
| 480 | + }, |
| 481 | + { |
| 482 | + "cell_type": "code", |
| 483 | + "execution_count": null, |
| 484 | + "metadata": {}, |
| 485 | + "outputs": [], |
| 486 | + "source": [ |
| 487 | + "grid.values = 256 * skimage.exposure.equalize_hist(\n", |
| 488 | + " image=grid.values,\n", |
| 489 | + " nbins=2**16, # 16 bit of Landsat 8\n", |
| 490 | + " mask=(grid != 0).values, # don't include zero values in histogram stretch\n", |
| 491 | + ")" |
| 492 | + ] |
| 493 | + }, |
| 494 | + { |
| 495 | + "cell_type": "markdown", |
| 496 | + "metadata": {}, |
| 497 | + "source": [ |
| 498 | + "### **Reproject and Plot**" |
| 499 | + ] |
| 500 | + }, |
| 501 | + { |
| 502 | + "cell_type": "markdown", |
| 503 | + "metadata": {}, |
| 504 | + "source": [ |
| 505 | + "Let's reproject our grid so that we can more easily reference coordinates in longitude and latitude later." |
| 506 | + ] |
| 507 | + }, |
| 508 | + { |
| 509 | + "cell_type": "code", |
| 510 | + "execution_count": null, |
| 511 | + "metadata": {}, |
| 512 | + "outputs": [], |
| 513 | + "source": [ |
| 514 | + "grid = grid.rio.reproject(\n", |
| 515 | + " \"+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs\"\n", |
| 516 | + ")" |
| 517 | + ] |
| 518 | + }, |
| 519 | + { |
| 520 | + "cell_type": "markdown", |
| 521 | + "metadata": {}, |
| 522 | + "source": [ |
| 523 | + "Now the image should look much nicer." |
| 524 | + ] |
| 525 | + }, |
| 526 | + { |
| 527 | + "cell_type": "code", |
| 528 | + "execution_count": null, |
| 529 | + "metadata": {}, |
| 530 | + "outputs": [], |
| 531 | + "source": [ |
| 532 | + "fig = pygmt.Figure()\n", |
| 533 | + "fig.grdimage(grid=[grid.sel(band=4), grid.sel(band=3), grid.sel(band=2)], frame=True)\n", |
| 534 | + "fig.show()" |
| 535 | + ] |
| 536 | + }, |
| 537 | + { |
| 538 | + "cell_type": "markdown", |
| 539 | + "metadata": {}, |
| 540 | + "source": [ |
| 541 | + "We can also subset our map to that of Wellington using the region argument as usual." |
| 542 | + ] |
| 543 | + }, |
| 544 | + { |
| 545 | + "cell_type": "code", |
| 546 | + "execution_count": null, |
| 547 | + "metadata": {}, |
| 548 | + "outputs": [], |
| 549 | + "source": [ |
| 550 | + "fig = pygmt.Figure()\n", |
| 551 | + "fig.grdimage(\n", |
| 552 | + " grid=[grid.sel(band=4), grid.sel(band=3), grid.sel(band=2)],\n", |
| 553 | + " region=[174.6, 174.9, -41.4, -41.1],\n", |
| 554 | + " frame=True\n", |
| 555 | + ")\n", |
| 556 | + "fig.show()" |
| 557 | + ] |
| 558 | + }, |
| 559 | + { |
| 560 | + "cell_type": "markdown", |
| 561 | + "metadata": {}, |
| 562 | + "source": [ |
| 563 | + "## **2.4 Exploration time - Points or Grids, your pick**\n", |
| 564 | + "\n", |
| 565 | + "It's time to make a map on your own again!\n", |
| 566 | + "Depending on what you're interested in,\n", |
| 567 | + "use one of the Search APIs linked above\n", |
| 568 | + "(either the [GeoNet Quake Search](https://quakesearch.geonet.org.nz)\n", |
| 569 | + "or [Remote Pixel's Satellite Search API](https://search.remotepixel.ca))\n", |
| 570 | + "to pull in a good chunk of data for your city, town or region.\n", |
| 571 | + "\n", |
| 572 | + "If you're plotting earthquake points,\n", |
| 573 | + "try to play with different colormaps or change the size of the symbols.\n", |
| 574 | + "If you're plotting Landsat 8 grids,\n", |
| 575 | + "try to plot a false colour map instead or calculate\n", |
| 576 | + "[NDVI](https://en.wikipedia.org/wiki/Normalized_difference_vegetation_index) values\n", |
| 577 | + "and plot it with a colorbar.\n", |
| 578 | + "\n", |
| 579 | + "Go!" |
| 580 | + ] |
| 581 | + }, |
| 582 | + { |
| 583 | + "cell_type": "code", |
| 584 | + "execution_count": null, |
| 585 | + "metadata": {}, |
| 586 | + "outputs": [], |
| 587 | + "source": [] |
| 588 | + }, |
| 589 | + { |
| 590 | + "cell_type": "code", |
| 591 | + "execution_count": null, |
| 592 | + "metadata": {}, |
| 593 | + "outputs": [], |
| 594 | + "source": [] |
| 595 | + }, |
| 596 | + { |
| 597 | + "cell_type": "code", |
| 598 | + "execution_count": null, |
| 599 | + "metadata": {}, |
| 600 | + "outputs": [], |
| 601 | + "source": [] |
| 602 | + }, |
362 | 603 | { |
363 | 604 | "cell_type": "markdown", |
364 | 605 | "metadata": {}, |
|
0 commit comments