From 490fe2506912f18d87d10c9c444fb2e24ede55cb Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Fri, 4 Nov 2016 14:06:18 -0400 Subject: [PATCH 1/3] - Kernel Density Process - Refactored inputs for Simple Grid Density Process (uses same method to generate bounds, image size as KernelDensityProcess) - Updated documentation --- docs/conf.py | 2 +- docs/examples/gaia_processes.html | 202 +++++++++++- docs/examples/gaia_processes.ipynb | 186 +++++++++-- docs/index.rst | 32 +- gaia_densitycomputations/processes.py | 288 +++++++++++++++--- requirements.txt | 3 +- tests/cases/test_parser.py | 31 +- tests/cases/test_processors.py | 38 ++- tests/data/densitycomputations.json | 2 +- .../densitycomputations_process_results.tif | Bin 25983 -> 26183 bytes tests/data/iraq_hospitals.geojson | 1 + tests/data/kerneldensity.json | 10 + tests/data/kerneldensity_process_results.tif | Bin 0 -> 1271226 bytes 13 files changed, 699 insertions(+), 96 deletions(-) create mode 100644 tests/data/iraq_hospitals.geojson create mode 100644 tests/data/kerneldensity.json create mode 100644 tests/data/kerneldensity_process_results.tif diff --git a/docs/conf.py b/docs/conf.py index ce1156e..ea7a4cd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,7 @@ def __getattr__(cls, name): MOCK_MODULES = ['pysal', 'scipy', 'gdal', 'gdalconst', 'osgeo', 'ogr', 'osr', 'osgeo.gdal_array', 'numpy', 'pandas', 'geopandas', 'psycopg2', 'scikit-image', 'skimage', 'skimage.graph', 'numpy.ma', - 'matplotlib', 'matplotlib.pyplot'] + 'matplotlib', 'matplotlib.pyplot', 'scipy.stats', 'gdalnumeric'] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) # -- General configuration ------------------------------------------------ diff --git a/docs/examples/gaia_processes.html b/docs/examples/gaia_processes.html index d204568..c589ed1 100644 --- a/docs/examples/gaia_processes.html +++ b/docs/examples/gaia_processes.html @@ -11766,7 +11766,7 @@

Gaia Density Computations Exampleimport folium import numpy as np from folium.plugins import ImageOverlay -from gaia_densitycomputations.processes import SimpleGridDensityProcess +from gaia_densitycomputations.processes import SimpleGridDensityProcess, KernelDensityProcess from gaia.geo.geo_inputs import VectorFileIO @@ -11774,6 +11774,15 @@

Gaia Density Computations Example + + @@ -11820,16 +11826,31 @@

Gaia Density Computations Example
-
In [3]:
+
In [7]:
sdp.compute()
+print sdp.output.uri
 
+
+
+ + +
+
+
/Users/mbertrand/dev/kitware/gaia/tests/data/output/c5fff097-ad3b-48e8-89c7-e641d6240e63/c5fff097-ad3b-48e8-89c7-e641d6240e63.tif
+
+
+
+ +
+
+
@@ -11843,7 +11864,7 @@

Gaia Density Computations Example
-
In [5]:
+
In [4]:
+ +
+
+
In [5]:
+
+
+
# Calculate kernel density with default bandwidth estimator ('scott')
+inputio = VectorFileIO(uri='/Users/mbertrand/dev/kitware/gaia-densitycomputations-plugin/tests/data/iraq_hospitals.geojson')
+kdp = KernelDensityProcess(inputs=[inputio], width=200)
+kdp.compute()
+print kdp.output.uri
+
+
+
+
+ +
+
+ + +
+
+
/Users/mbertrand/dev/kitware/gaia/tests/data/output/4b84bd47-12c2-4e96-9340-7b7cfb1a2ecc/4b84bd47-12c2-4e96-9340-7b7cfb1a2ecc.tif
+
+
@@ -11882,16 +11946,126 @@

Gaia Density Computations Example
-
In [ ]:
+
In [6]:
-
 
+
# Display kernel density on map
+from folium.plugins import MarkerCluster
+
+world_map = folium.Map([34.00,43.00],
+                  zoom_start=6,
+                  tiles='cartodbpositron')
+
+hospitals = folium.features.GeoJson(inputio.read().to_json(), name='Hospitals')
+temparray = np.array(kdp.output.read().GetRasterBand(1).ReadAsArray())
+overlay = ImageOverlay(temparray, [[27.0466603,38.9358608],[40.3888093,50.8226656]], 
+                       colormap=lambda x: (1, 0, 0, x),
+                       mercator_project=True, control=True)
+overlay.layer_name = 'Kernel Density'
+
+world_map.add_children(overlay)
+world_map.add_children(hospitals)
+
+folium.LayerControl().add_to(world_map)
+world_map
 
+
+
+ + +
Out[6]:
+ +
+
+
+ +
+ +
+
+ +
+
+
+
In [8]:
+
+
+
# Calculate kernel density with a custom bandwidth value
+inputio = VectorFileIO(uri='/Users/mbertrand/dev/kitware/gaia-densitycomputations-plugin/tests/data/iraq_hospitals.geojson')
+kdp = KernelDensityProcess(inputs=[inputio], width=200, bandwidth=0.8)
+kdp.compute()
+print kdp.output.uri
+
+ +
+
+
+ +
+
+ + +
+
+
/Users/mbertrand/dev/kitware/gaia/tests/data/output/09c8a41b-71c7-4805-9f18-e084c6774933/09c8a41b-71c7-4805-9f18-e084c6774933.tif
+
+
+
+ +
+
+ +
+
+
+
In [9]:
+
+
+
# Display kernel density on map
+from folium.plugins import MarkerCluster
+
+world_map = folium.Map([34.00,43.00],
+                  zoom_start=6,
+                  tiles='cartodbpositron')
+
+hospitals = folium.features.GeoJson(inputio.read().to_json(), name='Hospitals')
+temparray = np.array(kdp.output.read().GetRasterBand(1).ReadAsArray())
+overlay = ImageOverlay(temparray, [[27.0466603,38.9358608],[40.3888093,50.8226656]], 
+                       colormap=lambda x: (1, 0, 0, x),
+                       mercator_project=True, control=True)
+overlay.layer_name = 'Kernel Density'
+
+world_map.add_children(overlay)
+world_map.add_children(hospitals)
+
+folium.LayerControl().add_to(world_map)
+world_map
+
+ +
+
+
+ +
+
+ + +
Out[9]:
+ +
+
+
+ +
+ +
+
+

diff --git a/docs/examples/gaia_processes.ipynb b/docs/examples/gaia_processes.ipynb index 317116c..1246742 100644 --- a/docs/examples/gaia_processes.ipynb +++ b/docs/examples/gaia_processes.ipynb @@ -25,10 +25,17 @@ "import folium\n", "import numpy as np\n", "from folium.plugins import ImageOverlay\n", - "from gaia_densitycomputations.processes import SimpleGridDensityProcess\n", + "from gaia_densitycomputations.processes import SimpleGridDensityProcess, KernelDensityProcess\n", "from gaia.geo.geo_inputs import VectorFileIO" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Simple Grid Density Process" + ] + }, { "cell_type": "markdown", "metadata": { @@ -49,14 +56,11 @@ "# Point dataset to calculate density of\n", "ports = VectorFileIO(uri='../../tests/data/ports_and_harbours.geojson')\n", "\n", - "# Grid resolution to use for density calculation\n", - "resolution = {\n", - " \"nCol\": 200,\n", - " \"nRow\": 100\n", - "}\n", + "# Grid width to use for density calculation\n", + "width=200\n", "\n", "# Create an instance of the Simple Density Process\n", - "sdp = SimpleGridDensityProcess(inputs = [ports], resolution=resolution)" + "sdp = SimpleGridDensityProcess(inputs = [ports], width=width)" ] }, { @@ -70,13 +74,22 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 7, "metadata": { "collapsed": false }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/Users/mbertrand/dev/kitware/gaia/tests/data/output/c5fff097-ad3b-48e8-89c7-e641d6240e63/c5fff097-ad3b-48e8-89c7-e641d6240e63.tif\n" + ] + } + ], "source": [ - "sdp.compute()" + "sdp.compute()\n", + "print sdp.output.uri" ] }, { @@ -88,7 +101,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": { "collapsed": false }, @@ -96,13 +109,13 @@ { "data": { "text/html": [ - "
" + "
" ], "text/plain": [ - "" + "" ] }, - "execution_count": 5, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -113,7 +126,10 @@ " tiles='cartodbpositron')\n", "\n", "temparray = np.array(sdp.output.read().GetRasterBand(1).ReadAsArray())\n", - "overlay = ImageOverlay(temparray, [[-90.0,-180.0],[90.0,180.0]], mercator_project=True, control=True)\n", + "overlay = ImageOverlay(temparray, \n", + " [[-90.0,-180.0],[90.0,180.0]], \n", + " colormap=lambda x: (1, 0, 0, x),\n", + " mercator_project=True, control=True)\n", "overlay.layer_name = 'Port Density'\n", "\n", "world_map.add_children(overlay)\n", @@ -123,13 +139,145 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": { "collapsed": true }, - "outputs": [], - "source": [] + "source": [ + "## Kernel Density Process" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/Users/mbertrand/dev/kitware/gaia/tests/data/output/4b84bd47-12c2-4e96-9340-7b7cfb1a2ecc/4b84bd47-12c2-4e96-9340-7b7cfb1a2ecc.tif\n" + ] + } + ], + "source": [ + "# Calculate kernel density with default bandwidth estimator ('scott')\n", + "inputio = VectorFileIO(uri='/Users/mbertrand/dev/kitware/gaia-densitycomputations-plugin/tests/data/iraq_hospitals.geojson')\n", + "kdp = KernelDensityProcess(inputs=[inputio], width=200)\n", + "kdp.compute()\n", + "print kdp.output.uri" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Display kernel density on map\n", + "from folium.plugins import MarkerCluster\n", + "\n", + "world_map = folium.Map([34.00,43.00],\n", + " zoom_start=6,\n", + " tiles='cartodbpositron')\n", + "\n", + "hospitals = folium.features.GeoJson(inputio.read().to_json(), name='Hospitals')\n", + "temparray = np.array(kdp.output.read().GetRasterBand(1).ReadAsArray())\n", + "overlay = ImageOverlay(temparray, [[27.0466603,38.9358608],[40.3888093,50.8226656]], \n", + " colormap=lambda x: (1, 0, 0, x),\n", + " mercator_project=True, control=True)\n", + "overlay.layer_name = 'Kernel Density'\n", + "\n", + "world_map.add_children(overlay)\n", + "world_map.add_children(hospitals)\n", + "\n", + "folium.LayerControl().add_to(world_map)\n", + "world_map" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/Users/mbertrand/dev/kitware/gaia/tests/data/output/09c8a41b-71c7-4805-9f18-e084c6774933/09c8a41b-71c7-4805-9f18-e084c6774933.tif\n" + ] + } + ], + "source": [ + "# Calculate kernel density with a custom bandwidth value\n", + "inputio = VectorFileIO(uri='/Users/mbertrand/dev/kitware/gaia-densitycomputations-plugin/tests/data/iraq_hospitals.geojson')\n", + "kdp = KernelDensityProcess(inputs=[inputio], width=200, bandwidth=0.8)\n", + "kdp.compute()\n", + "print kdp.output.uri" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Display kernel density on map\n", + "from folium.plugins import MarkerCluster\n", + "\n", + "world_map = folium.Map([34.00,43.00],\n", + " zoom_start=6,\n", + " tiles='cartodbpositron')\n", + "\n", + "hospitals = folium.features.GeoJson(inputio.read().to_json(), name='Hospitals')\n", + "temparray = np.array(kdp.output.read().GetRasterBand(1).ReadAsArray())\n", + "overlay = ImageOverlay(temparray, [[27.0466603,38.9358608],[40.3888093,50.8226656]], \n", + " colormap=lambda x: (1, 0, 0, x),\n", + " mercator_project=True, control=True)\n", + "overlay.layer_name = 'Kernel Density'\n", + "\n", + "world_map.add_children(overlay)\n", + "world_map.add_children(hospitals)\n", + "\n", + "folium.LayerControl().add_to(world_map)\n", + "world_map" + ] } ], "metadata": { diff --git a/docs/index.rst b/docs/index.rst index 85edd57..8bb8622 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,11 +2,37 @@ Gaia Density Computations Plugin ================================ This is a plugin for Gaia (https://github.com/OpenDataAnalytics/gaia) that -calculates the point density of a dataset given a raster grid of a specified size (in columns and rows). -The current implementation performs a simple calculation of adding the number of points within each grid cell. +calculates the point density of a dataset given a raster grid of a specified size (in columns and rows), +using one of the following processes: + * `SimpleDensityProcess `__ + + * Calculate point density by adding the number of points within each grid cell. + + * Required input: a vector dataset containing points + + * Optional arguments: + + * width: Width of output image in pixels (default=1024) + + * `Example `__ + + * `KernelDensityProcess `__ + + * Calculates point density using a Gaussian kernel density function. + + * Required input: a vector dataset containing points + + * Optional arguments: + + * width: Width of output image in pixels (default=1024) + + * weight: Column of vector dataset, with integer values. Points will be multiplied by this value. + + * bandwidth: The method used to calculate the kernel density estimator bandwidth. Valid values are 'scott' (default), 'silverman', or a numerical value. + + * `Example `__ -An example of how to use this plugin can be found `here `__. Installation ----------------- diff --git a/gaia_densitycomputations/processes.py b/gaia_densitycomputations/processes.py index df77b96..be73d4f 100644 --- a/gaia_densitycomputations/processes.py +++ b/gaia_densitycomputations/processes.py @@ -16,7 +16,6 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## -from gaia import types import gaia.formats as formats import gdal @@ -24,17 +23,12 @@ import osr import ogr import numpy as np -import itertools -import geopandas import sys - -from gaia.inputs import GaiaIO +from scipy.stats import gaussian_kde +from gaia import types +from gaia.core import GaiaException from gaia.gaia_process import GaiaProcess -from gaia_densitycomputations import config -from gaia.geo.geo_inputs import RasterFileIO -from skimage.graph import route_through_array -import matplotlib.pyplot as plt -from math import sqrt, ceil +from gaia.geo.geo_inputs import RasterFileIO, VectorFileIO class SimpleGridDensityProcess(GaiaProcess): @@ -52,24 +46,24 @@ class SimpleGridDensityProcess(GaiaProcess): ] #: Required arguments, data types as dict - required_args = [ + optional_args = [ { - 'name': 'resolution', - 'title': 'Output image size', - 'description': 'Output image size (cols,rows), ex: "200,100"', - 'type': str + 'name': 'width', + 'title': 'Output image width', + 'description': 'Output image width in pixels', + 'type': int } ] default_output = formats.RASTER + width = 1024 - def __init__(self, resolution, **kwargs): + def __init__(self, **kwargs): """ Create an instance of the DensityComputationsProcess class :param resolution: grid cell resolution of the output :param kwargs: Optional keyword arguments """ - self.resolution = [int(n) for n in resolution.split(',')] super(SimpleGridDensityProcess, self).__init__(**kwargs) if not self.output: @@ -81,9 +75,11 @@ def calculate_density(self): within each grid cell. """ - shpDriver = ogr.GetDriverByName('GeoJSON') - strjson = self.inputs[0].read().to_json() - dataSource = shpDriver.Open(strjson, 0) + vec_driver = ogr.GetDriverByName('GeoJSON') + input = self.inputs[0] + strjson = input.read(format=formats.JSON) + crs = input.get_epsg() + dataSource = vec_driver.Open(strjson, 0) # Open the source file, and exit if doesn't exist if dataSource is None: @@ -91,36 +87,24 @@ def calculate_density(self): sys.exit(1) if os.path.exists(self.output.uri): - shpDriver.DeleteDataSource(self.output.uri) + vec_driver.DeleteDataSource(self.output.uri) else: self.output.create_output_dir(self.output.uri) # Get the layer layer = dataSource.GetLayer() - # Get the layer extent - extent = layer.GetExtent() - - # Open the layer - # Set the bounding box - xmin = extent[0] - ymin = extent[2] - xmax = extent[1] - ymax = extent[3] - - # Number of columns and rows - nbrColumns = self.resolution[0] - nbrRows = self.resolution[1] + gb = GridBounds(layer, width=self.width, crs=crs) # Caculate the cell size in x and y direction - csx = (xmax - xmin) / nbrColumns - csy = (ymax - ymin) / nbrRows + csx = (gb.xmax - gb.xmin) / self.width + csy = (gb.ymax - gb.ymin) / gb.height rows = [] - i = ymax - while i > ymin: - j = xmin + i = gb.ymax + while i > gb.ymin: + j = gb.xmin cols = [] - while j < xmax: + while j < gb.xmax: # Set a spatial filter layer.SetSpatialFilterRect(j, (i-csy), (j+csx), i) # And count the features @@ -133,8 +117,8 @@ def calculate_density(self): ncols = array.shape[1] nrows = array.shape[0] - originX = extent[0] - originY = extent[3] + originX = gb.xmin + originY = gb.ymin # Convert the results to geoTiff raster driver = gdal.GetDriverByName('GTiff') @@ -147,10 +131,7 @@ def calculate_density(self): outband = outRaster.GetRasterBand(1) # Add colors to the raster image outband.SetRasterColorInterpretation(gdal.GCI_PaletteIndex) - ct = gdal.ColorTable() - ct.CreateColorRamp(0, (0, 0, 255), 14, (0, 255, 0)) - ct.CreateColorRamp(15, (0, 255, 0), 30, (127, 127, 0)) - ct.CreateColorRamp(30, (127, 127, 0), 50, (255, 0, 0)) + ct = create_color_table(array.max()) outband.SetColorTable(ct) outband.SetNoDataValue(0) outband.FlushCache() @@ -170,6 +151,221 @@ def compute(self): """ self.calculate_density() + +class KernelDensityProcess(GaiaProcess): + + #: Tuple of required inputs; name, type , max # of each; None = no max + required_inputs = [ + {'description': 'Point dataset', + 'type': types.VECTOR, + 'max': 1 + } + ] + + optional_args = [ + { + 'description': + 'Weight attribute (attribute should have integer values)', + 'name': 'weight', + 'title': 'Weight', + 'type': str + }, + { + 'description': + 'Bandwidth estimator (scott, silverman, or a numeric value)', + 'name': 'bandwidth', + 'title': 'Bandwidth Estimator', + 'type': str + }, + { + 'description': + 'Width of output in pixels', + 'name': 'resolution', + 'title': 'Resolutionr', + 'type': int + } + ] + + bandwidth = 'scott' + width = 1024 + + default_output = formats.RASTER + + def __init__(self, **kwargs): + """ + Create an instance of the KernelDensityProcess class + """ + super(KernelDensityProcess, self).__init__(**kwargs) + + try: + self.bandwidth = float(self.bandwidth) + except ValueError: + if self.bandwidth not in ('scott', 'silverman'): + raise GaiaException('Invalid bandwidth') + + if not self.output: + self.output = RasterFileIO(name='result', uri=self.get_outpath()) + + def compute(self): + vec_driver = ogr.GetDriverByName('GeoJSON') + input = self.inputs[0] + strjson = input.read(format=formats.JSON) + crs = input.get_epsg() + data_source = vec_driver.Open(strjson, 0) + layer = data_source.GetLayer() + + # Buffer result to avoid edge effects + points = [] + for i in range(layer.GetFeatureCount()): + feature = layer.GetNextFeature() + geom = feature.GetGeometryRef() + weight = (1 if not hasattr(self, 'weight') else + feature.GetField(self.weight)) + for j in range(weight): + points.append((geom.GetX(), geom.GetY(), weight)) + + xtrain = np.vstack([[c[1] for c in points], + [c[0] for c in points]]).T + + # Set up the data grid for the contour plot + gb = GridBounds(layer, self.width, crs) + + xgrid, ygrid = construct_grids(gb.xres, gb.yres, + self.width, gb.height, + gb.xmin, gb.ymin) + x, y = np.meshgrid(xgrid, ygrid) + xy = np.vstack([y.ravel(), x.ravel()]).T + + kde = gaussian_kde(xtrain.T, bw_method=self.bandwidth) + density = kde(xy.T) + density = density.reshape(x.shape) + density = (density/(density.max()/255.0)).astype(np.int32) + + self.output.create_output_dir(self.output.uri) + driver = gdal.GetDriverByName('GTiff') + outDs = driver.Create(self.output.uri, density.shape[1], + density.shape[0], 1, gdal.GDT_UInt16) + outband = outDs.GetRasterBand(1) + # write the data + outband.WriteArray(density[::-1], 0, 0) + ct = create_color_table(int(density.max())) + outband.SetColorTable(ct) + outband.SetNoDataValue(0) + # # flush data to disk, set the NoData value and calculate stats + outband.FlushCache() + + gt = [gb.xmin, gb.xres, 0, gb.ymax, 0, -gb.yres] + outDs.SetGeoTransform(gt) + outDs.SetProjection(gb.projection.ExportToWkt()) + outDs.FlushCache() + del outDs + + +class GridBounds(object): + """ + Helper class to determine bounds and pixel height of a raster image, + based on the extent of a vector layer, specified pixel width, and + projection CRS. + """ + + xmin = -180.0 + xmax = 180.0 + ymin = -90.0 + ymax = 90.0 + xres = 1024 + yres = 1024 + projection = None + + def __init__(self, layer, width=1024, crs=4326): + """ + Initialize the object based on input parameters + :param layer: Vector layer + :param width: Desired width of output raster in pixels + :param crs: EPSG code of the layer + """ + self.projection = osr.SpatialReference() + self.projection.ImportFromEPSG(int(crs)) + unit = self.projection.GetAttrValue('Unit') + maxextent = [-180.0, 180.0, -90.0, 90.0] + + if unit != 'degree': + wg84source = osr.SpatialReference() + wg84source.ImportFromEPSG(4326) + + epsg = self.projection.GetAttrValue("AUTHORITY", 1) + if epsg in ('900913', '3857', '102100', '102113', '54004', '41001'): + maxextent = [-20037508.34, 20037508.34, + -20037508.34, 20037508.34] + else: + transform = osr.CoordinateTransformation(wg84source, + self.projection) + ul = ogr.CreateGeometryFromWkt("POINT (-180.0 90.0)") + lr = ogr.CreateGeometryFromWkt("POINT (180.0 -90.0)") + for point in (ul, lr): + point.Transform(transform) + maxextent = [ul.GetX(), lr.GetX(), ul.GetY(), lr.GetY()] + + extent = layer.GetExtent() + x_buffer = (extent[1]-extent[0])/2 + y_buffer = (extent[3]-extent[2])/2 + + # Set the bounding box + self.xmin = max(extent[0]-x_buffer, maxextent[0]) + self.ymin = max(extent[2]-y_buffer, maxextent[2]) + self.xmax = min(extent[1]+x_buffer, maxextent[1]) + self.ymax = min(extent[3]+y_buffer, maxextent[3]) + + self.xres = (self.xmax-self.xmin)/float(width) + self.height = int(round((self.ymax-self.ymin)/self.xres)) + self.yres = (self.ymax-self.ymin)/float(self.height) + + +def create_color_table(max, min=0, intervals=None): + """ + Create and return a color table. + :param max_value: Maximum value of raster grid + :param min_value: Optional minimum value (default=0) + :param intervals: Optional list of ColorRamp values as lists + :return: gdal.ColorTable + """ + ct = gdal.ColorTable() + if not intervals: + intvl = int(max/4) + ct.CreateColorRamp(min, (0, 0, 255), intvl-1, (0, 255, 0)) + ct.CreateColorRamp(intvl, (0, 255, 0), intvl*2-1, (127, 127, 0)) + ct.CreateColorRamp(intvl*2, (127, 127, 0), max, (255, 0, 0)) + else: + for i in intervals: + ct.CreateColorRamp(i[0], i[1], i[2], i[3]) + return ct + + +def construct_grids(xres, yres, columns, rows, minx, miny): + """ + Create a grid of coordinates + :param xres: Resolution of longitude columns + :param yres: Resolution of latitude rows + :param columns: Number of columns + :param rows: Number of rows + :param minx: Minimum X coordinate + :param miny: Minimum Y coordinate + :return: Grids of X and Y coordinate values + """ + # x,y coordinates for corner cells + xmin = minx + xres + xmax = xmin + (columns * xres) + ymin = miny + yres + ymax = ymin + (rows * yres) + + # x coordinates of the grid cells + xgrid = np.arange(xmin, xmax, xres) + # y coordinates of the grid cells + ygrid = np.arange(ymin, ymax, yres) + + return xgrid, ygrid + + PLUGIN_CLASS_EXPORTS = [ SimpleGridDensityProcess, + KernelDensityProcess ] diff --git a/requirements.txt b/requirements.txt index fe2b2e9..e9154af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -matplotlib==1.5.1 -scikit-image==0.12.3 +scipy==0.18.1 \ No newline at end of file diff --git a/tests/cases/test_parser.py b/tests/cases/test_parser.py index a41c171..c8c27e3 100644 --- a/tests/cases/test_parser.py +++ b/tests/cases/test_parser.py @@ -21,12 +21,7 @@ import gdal import unittest import numpy as np - -import pysal - -from gaia import formats from gaia.parser import deserialize -from gaia.core import config testfile_path = os.path.join(os.path.dirname( os.path.realpath(__file__)), '../data') @@ -60,3 +55,29 @@ def test_process_densitycomputations(self): finally: if process: process.purge() + + def test_process_kerneldensity(self): + """Test Density Computations Process""" + with open(os.path.join(testfile_path, + 'kerneldensity.json')) as inf: + body_text = inf.read().replace('{basepath}', testfile_path) + process = json.loads(body_text, object_hook=deserialize) + try: + process.compute() + expected_layer = process.output.read() + # Get layer stats + expected_results = \ + expected_layer.GetRasterBand(1).GetStatistics(0, 1) + + actual_layer = gdal.Open(os.path.join( + testfile_path, + 'kerneldensity_process_results.tif'), gdal.GA_Update) + actual_results = actual_layer.GetRasterBand(1).GetStatistics(0, 1) + + expected_results_rounded = np.around(expected_results, decimals=2) + actual_results_rounded = np.around(actual_results, decimals=2) + self.assertEquals(np.all(expected_results_rounded), + np.all(actual_results_rounded)) + finally: + if process: + process.purge() diff --git a/tests/cases/test_processors.py b/tests/cases/test_processors.py index 807b972..00bbb67 100644 --- a/tests/cases/test_processors.py +++ b/tests/cases/test_processors.py @@ -16,15 +16,14 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################### -import json + import os import unittest -import pysal import gdal import numpy as np -from gaia import formats -from gaia.geo.geo_inputs import RasterFileIO, VectorFileIO -from gaia_densitycomputations.processes import SimpleGridDensityProcess +from gaia.geo.geo_inputs import VectorFileIO +from gaia_densitycomputations.processes import SimpleGridDensityProcess, \ + KernelDensityProcess testfile_path = os.path.join(os.path.dirname( os.path.realpath(__file__)), '../data') @@ -64,3 +63,32 @@ def test_process_densitycomputations(self): finally: if process: process.purge() + + def test_process_kerneldensity(self): + """ + Test KernelDensityProcess for raster inputs + """ + + uri = os.path.join(testfile_path, 'iraq_hospitals.geojson') + points = VectorFileIO(uri) + process = KernelDensityProcess(inputs=[points]) + try: + process.compute() + expected_layer = process.output.read() + # Get layer stats + expected_results = \ + expected_layer.GetRasterBand(1).GetStatistics(0, 1) + + actual_layer = gdal.Open(os.path.join( + testfile_path, + 'kerneldensity_process_results.tif'), + gdal.GA_Update) + actual_results = actual_layer.GetRasterBand(1).GetStatistics(0, 1) + + expected_results_rounded = np.around(expected_results, decimals=2) + actual_results_rounded = np.around(actual_results, decimals=2) + self.assertEquals(np.all(expected_results_rounded), + np.all(actual_results_rounded)) + finally: + if process: + process.purge() diff --git a/tests/data/densitycomputations.json b/tests/data/densitycomputations.json index 6cc1aa2..5c30c1d 100644 --- a/tests/data/densitycomputations.json +++ b/tests/data/densitycomputations.json @@ -6,5 +6,5 @@ "uri": "tests/data/ports_and_harbours.geojson" } ], - "resolution": "200,100" + "width": 200 } diff --git a/tests/data/densitycomputations_process_results.tif b/tests/data/densitycomputations_process_results.tif index 20f02f4e254dd1f65eb55e24d278191607522d28..883349a49310e9c312a03ee56777967488126023 100644 GIT binary patch literal 26183 zcmeHPOK2a*8Q-0K{Ga+;wo=KI*i}_%+6ENWmK{5|u|*|aJT z2(4QeZ0x3oP?Sq9Ho=#W);Xl0Tw;y|c4F^8WS<&+B=fA9%j!dAIQA3wRMQ5pWZ5AYe)t3pfD0(+@p* z{s4d9(rAVS`~%=e1kFDIKdR9j15BAe27XS%_bzmq%#UNw`_rGh9!}m}`qS6Z>5Pnw zjg5~VK0Gt??6dRpFTAk4{0AHtE?m0w>Z`B4_WJ8@ym9sF_3LlFb>qgn@4olm`|p4F z;jLS@Z-4U1r=Q-r^Vw&2?|%OI7hin+^*7&aZ1iP7&2 FfuYeJ~J~vzr1|q%C&2& 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 agf^|STeWDLlbw^*wypn&cFmIog8v6IuHJ(H literal 0 HcmV?d00001 From acbe292ab659842095b3f511121f0abaced96027 Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Fri, 4 Nov 2016 14:16:07 -0400 Subject: [PATCH 2/3] Rename travis file --- travis.yml => .travis.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename travis.yml => .travis.yml (100%) diff --git a/travis.yml b/.travis.yml similarity index 100% rename from travis.yml rename to .travis.yml From e8f053b1a64c4da40bcbd6b6e0f47f98dc062c14 Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Fri, 4 Nov 2016 14:22:03 -0400 Subject: [PATCH 3/3] Use master branch of gaia for travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1e43c1e..fbcf75f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,7 @@ before_install: install: - export CPLUS_INCLUDE_PATH=/usr/include/gdal - export C_INCLUDE_PATH=/usr/include/gdal - - pip install git+https://github.com/OpenDataAnalytics/gaia.git@basegaia#egg=gaia + - pip install git+https://github.com/OpenDataAnalytics/gaia.git#egg=gaia - pip install -r requirements.txt - pip install -r requirements-dev.txt - pip install -e .