From bd1d4144280ca187f14a97d804015921e40436a9 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Wed, 5 Aug 2020 23:03:28 -0400 Subject: [PATCH] Add image props and stats --- README.rst | 14 + examples/README.md | 2 + .../notebooks/00_geemap_key_features.ipynb | 30 ++ examples/notebooks/30_image_props_stats.ipynb | 271 ++++++++++++++++++ geemap/geemap.py | 75 ++++- geemap/utils.py | 72 ++++- 6 files changed, 454 insertions(+), 10 deletions(-) create mode 100644 examples/notebooks/30_image_props_stats.ipynb diff --git a/README.rst b/README.rst index 09ddd13208..2648594e33 100644 --- a/README.rst +++ b/README.rst @@ -444,6 +444,20 @@ To add a local raster dataset to the map: Map.add_raster(image, bands, colormap, layer_name) +To get image basic properties: + +.. code:: python + + geemap.image_props(image).getInfo() + + +To get image descriptive statistics: + +.. code:: python + + geemap.image_stats(image, region, scale) + + Examples -------- diff --git a/examples/README.md b/examples/README.md index 1d50e8d757..ebee86bb3e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -47,6 +47,8 @@ More video tutorials for geemap and Earth Engine are available on my [YouTube ch 26. How to create and deploy Earth Engine Apps using Python? ([video](https://youtu.be/nsIjfD83ggA) | [gif](https://i.imgur.com/Hpfzazk.gif) | [notebook](https://github.com/giswqs/geemap/blob/master/examples/notebooks/26_heroku.ipynb)) 27. How to create an interactive Earth Engine App for creating Landsat timelapse? ([video](https://youtu.be/whIXudC6r_s) | [gif](https://i.imgur.com/doHfnKp.gif) | [notebook](https://github.com/giswqs/geemap/blob/master/examples/notebooks/27_timelapse_app.ipynb)) 28. How to use your local computer as a web server for hosting Earth Engine Apps? ([video](https://youtu.be/eRDZBVJcNCk) | [gif](https://i.imgur.com/q0sJSyi.gif) | [notebook](https://github.com/giswqs/geemap/blob/master/examples/notebooks/28_voila.ipynb)) +29. How to use pydeck for rendering Earth Engine data ([video](https://youtu.be/EIkEH4okFF4) | [gif](https://i.imgur.com/HjFB95l.gif) | [notebook](https://github.com/giswqs/geemap/blob/master/examples/notebooks/29_pydeck.ipynb)) +30. How to get image basic properties and descriptive statistics (video | [gif](https://i.imgur.com/3B6YhkI.gif) | [notebook](https://github.com/giswqs/geemap/blob/master/examples/notebooks/30_image_props_stats.ipynb)) ### 1. Introducing the geemap Python package for interactive mapping with Google Earth Engine diff --git a/examples/notebooks/00_geemap_key_features.ipynb b/examples/notebooks/00_geemap_key_features.ipynb index 465be69169..73651496d3 100644 --- a/examples/notebooks/00_geemap_key_features.ipynb +++ b/examples/notebooks/00_geemap_key_features.ipynb @@ -976,6 +976,7 @@ } ], "metadata": { + "hide_input": false, "kernelspec": { "display_name": "Python 3", "language": "python", @@ -1010,6 +1011,35 @@ }, "toc_section_display": true, "toc_window_display": true + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false } }, "nbformat": 4, diff --git a/examples/notebooks/30_image_props_stats.ipynb b/examples/notebooks/30_image_props_stats.ipynb new file mode 100644 index 0000000000..92fde7e693 --- /dev/null +++ b/examples/notebooks/30_image_props_stats.ipynb @@ -0,0 +1,271 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to get image basic properties and descriptive statistics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ee\n", + "import geemap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create an interactive map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Map = geemap.Map()\n", + "Map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Add images to the map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "centroid = ee.Geometry.Point([-122.4439, 37.7538])\n", + "\n", + "landsat = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR') \\\n", + " .filterBounds(centroid) \\\n", + " .first()\n", + "\n", + "landsat_vis = {\n", + " 'min': 0,\n", + " 'max': 3000,\n", + " 'bands': ['B5', 'B4', 'B3']\n", + "}\n", + "\n", + "Map.centerObject(centroid, 8)\n", + "Map.addLayer(landsat, landsat_vis, \"Landsat-8\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "naip = ee.ImageCollection('USDA/NAIP/DOQQ') \\\n", + " .filterBounds(centroid) \\\n", + " .first()\n", + "\n", + "naip_vis = {\n", + " 'bands': ['N', 'R', 'G']\n", + "}\n", + "\n", + "Map.addLayer(naip, naip_vis, 'NAIP')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Get image property names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "landsat.propertyNames().getInfo()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "landsat.get('CLOUD_COVER').getInfo()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# The number of milliseconds since 1970-01-01T00:00:00Z.\n", + "landsat.get('system:time_start').getInfo()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ee.Date(landsat.get('system:time_start')).format('YYYY-MM-dd').getInfo()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Get image properties all at once" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "landsat_props = geemap.image_props(landsat)\n", + "landsat_props.getInfo()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "landsat_props.get('IMAGE_DATE').getInfo()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "naip_props = geemap.image_props(naip)\n", + "naip_props.getInfo()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "naip_props.get('NOMINAL_SCALE').getInfo()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Get image descriptive statistics\n", + "\n", + "Including minimum, maximum, mean, standard deviation, and sum." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "landsat_stats = geemap.image_stats(landsat, scale=90)\n", + "landsat_stats.getInfo()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "naip_stats = geemap.image_stats(naip, scale=10)\n", + "naip_stats.getInfo()" + ] + } + ], + "metadata": { + "hide_input": false, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.2" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": true, + "title_cell": "Table of Contents", + "title_sidebar": "Table of Contents", + "toc_cell": false, + "toc_position": { + "height": "calc(100% - 180px)", + "left": "10px", + "top": "150px", + "width": "384px" + }, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/geemap/geemap.py b/geemap/geemap.py index 141a72d8be..71b8d002f3 100644 --- a/geemap/geemap.py +++ b/geemap/geemap.py @@ -2890,7 +2890,8 @@ def ee_export_vector(ee_object, filename, selectors=None): if selectors is None: selectors = ee_object.first().propertyNames().getInfo() if filetype == 'csv': - ee_object = ee_object.select([".*"], None, False) # remove .geo coordinate field + # remove .geo coordinate field + ee_object = ee_object.select([".*"], None, False) elif not isinstance(selectors, list): print("selectors must be a list, such as ['attribute1', 'attribute2']") @@ -2970,7 +2971,8 @@ def ee_export_vector_to_drive(ee_object, description, folder, file_format='shp', if selectors is not None: task_config['selectors'] = selectors elif (selectors is None) and (file_format.lower() == 'csv'): - ee_object = ee_object.select([".*"], None, False) # remove .geo coordinate field + # remove .geo coordinate field + ee_object = ee_object.select([".*"], None, False) print('Exporting {}...'.format(description)) task = ee.batch.Export.table.toDrive(ee_object, description, **task_config) @@ -5634,3 +5636,72 @@ def is_tool(name): from shutil import which return which(name) is not None + + +def image_props(img, date_format='YYYY-MM-dd'): + """Gets image properties. + + Args: + img (ee.Image): The input image. + date_format (str, optional): The output date format. Defaults to 'YYYY-MM-dd HH:mm:ss'. + + Returns: + dd.Dictionary: The dictionary containing image properties. + """ + if not isinstance(img, ee.Image): + print('The input object must be an ee.Image') + return + + keys = img.propertyNames().remove('system:footprint').remove('system:bands') + values = keys.map(lambda p: img.get(p)) + + bands = img.bandNames() + scales = bands.map(lambda b: img.select([b]).projection().nominalScale()) + scale = ee.Algorithms.If(scales.distinct().size().gt( + 1), ee.Dictionary.fromLists(bands.getInfo(), scales), scales.get(0)) + image_date = ee.Date(img.get('system:time_start')).format(date_format) + time_start = ee.Date(img.get('system:time_start')).format('YYYY-MM-dd HH:mm:ss') + time_end = ee.Date(img.get('system:time_end')).format('YYYY-MM-dd HH:mm:ss') + asset_size = ee.Number(img.get('system:asset_size')).divide( + 1e6).format().cat(ee.String(' MB')) + + props = ee.Dictionary.fromLists(keys, values) + props = props.set('system:time_start', time_start) + props = props.set('system:time_end', time_end) + props = props.set('system:asset_size', asset_size) + props = props.set('NOMINAL_SCALE', scale) + props = props.set('IMAGE_DATE', image_date) + + return props + + +def image_stats(img, region=None, scale=None): + """Gets image descriptive statistics. + + Args: + img (ee.Image): The input image to calculate descriptive statistics. + region (object, optional): The region over which to reduce data. Defaults to the footprint of the image's first band. + scale (float, optional): A nominal scale in meters of the projection to work in. Defaults to None. + + Returns: + ee.Dictionary: A dictionary containing the description statistics of the input image. + """ + import geemap.utils as utils + + if not isinstance(img, ee.Image): + print('The input object must be an ee.Image') + return + + stat_types = ['min', 'max', 'mean', 'std', 'sum'] + + image_min = utils.image_min_value(img, region, scale) + image_max = utils.image_max_value(img, region, scale) + image_mean = utils.image_mean_value(img, region, scale) + image_std = utils.image_std_value(img, region, scale) + image_sum = utils.image_sum_value(img, region, scale) + + stat_results = ee.List([image_min, image_max, image_mean, image_std, image_sum]) + + stats = ee.Dictionary.fromLists(stat_types, stat_results) + + return stats \ No newline at end of file diff --git a/geemap/utils.py b/geemap/utils.py index aeed623eb4..a02fa2aafa 100644 --- a/geemap/utils.py +++ b/geemap/utils.py @@ -93,7 +93,37 @@ def image_cell_size(img): Returns: float: The nominal scale in meters. """ - return img.projection().nominalScale().getInfo() + bands = img.bandNames() + scales = bands.map(lambda b: img.select([b]).projection().nominalScale()) + scale = ee.Algorithms.If(scales.distinct().size().gt(1), ee.Dictionary.fromLists(bands.getInfo(), scales), scales.get(0)) + return scale + + +def image_scale(img): + """Retrieves the image cell size (e.g., spatial resolution) + + Args: + img (object): ee.Image + + Returns: + float: The nominal scale in meters. + """ + # bands = img.bandNames() + # scales = bands.map(lambda b: img.select([b]).projection().nominalScale()) + # scale = ee.Algorithms.If(scales.distinct().size().gt(1), ee.Dictionary.fromLists(bands.getInfo(), scales), scales.get(0)) + return img.select(0).projection().nominalScale() + + +def image_band_names(img): + """Gets image band names. + + Args: + img (ee.Image): The input image. + + Returns: + ee.List: The returned list of image band names. + """ + return img.bandNames() def image_date(img, date_format='YYYY-MM-dd'): @@ -106,7 +136,7 @@ def image_date(img, date_format='YYYY-MM-dd'): Returns: str: A string representing the acquisition of the image. """ - return ee.Date(img.get('system:time_start')).format(date_format).getInfo() + return ee.Date(img.get('system:time_start')).format(date_format) def image_area(img, region=None, scale=None, denominator=1.0): @@ -125,7 +155,7 @@ def image_area(img, region=None, scale=None, denominator=1.0): region = img.geometry() if scale is None: - scale = img.projection().nominalScale() + scale = image_scale(img) pixel_area = img.unmask().neq(ee.Image(0)).multiply( ee.Image.pixelArea()).divide(denominator) @@ -153,7 +183,7 @@ def image_max_value(img, region=None, scale=None): region = img.geometry() if scale is None: - scale = img.projection().nominalScale() + scale = image_scale(img) max_value = img.reduceRegion(**{ 'reducer': ee.Reducer.max(), @@ -179,7 +209,7 @@ def image_min_value(img, region=None, scale=None): region = img.geometry() if scale is None: - scale = img.projection().nominalScale() + scale = image_scale(img) min_value = img.reduceRegion(**{ 'reducer': ee.Reducer.min(), @@ -205,7 +235,7 @@ def image_mean_value(img, region=None, scale=None): region = img.geometry() if scale is None: - scale = img.projection().nominalScale() + scale = image_scale(img) mean_value = img.reduceRegion(**{ 'reducer': ee.Reducer.mean(), @@ -231,7 +261,7 @@ def image_std_value(img, region=None, scale=None): region = img.geometry() if scale is None: - scale = img.projection().nominalScale() + scale = image_scale(img) std_value = img.reduceRegion(**{ 'reducer': ee.Reducer.stdDev(), @@ -242,6 +272,32 @@ def image_std_value(img, region=None, scale=None): return std_value +def image_sum_value(img, region=None, scale=None): + """Retrieves the sum of an image. + + Args: + img (object): The image to calculate the standard deviation. + region (object, optional): The region over which to reduce data. Defaults to the footprint of the image's first band. + scale (float, optional): A nominal scale in meters of the projection to work in. Defaults to None. + + Returns: + object: ee.Number + """ + if region is None: + region = img.geometry() + + if scale is None: + scale = image_scale(img) + + sum_value = img.reduceRegion(**{ + 'reducer': ee.Reducer.sum(), + 'geometry': region, + 'scale': scale, + 'maxPixels': 1e12 + }) + return sum_value + + def extract_values_to_points(in_points, img, label, scale=None): """Extracts image values to points. @@ -255,7 +311,7 @@ def extract_values_to_points(in_points, img, label, scale=None): object: ee.FeatureCollection """ if scale is None: - scale = img.projection().nominalScale() + scale = image_scale(img) out_fc = img.sampleRegions(**{ 'collection': in_points,