examples.py: -------------- """ Call the satellite image segmentation REST API Berkeley Image Seg is a region merging image object segmentation algorithm Give it multiband satellite imagery and it returns an int image labeled with object IDs www.imageseg.com jscar@berkenviro.com This code is at https://github.com/jscarbor/bis >>> # endpoint = 'http://localhost:8080/v1/segment' >>> endpoint = 'https://api.imageseg.com/v1/segment' Send a small image as an array/list in 'array' parameter in URL of a GET request >>> r = requests.get(endpoint + '?array=[[[1]],[[2]],[[1]]]') # 3 band, 1 pix >>> regions = np.array(r.json()) >>> regions.shape (1, 1) >>> regions array([[0]]) Send an image as a JSON array/list in the body of a POST request >>> array = rasterio.open('img/agé.bmp').read() >>> array.shape, array.dtype # A plain RGB image ((3, 127, 212), dtype('uint8')) >>> r = requests.post(endpoint, json=array.tolist()) >>> regions = np.array(r.json()) >>> regions.shape (127, 212) >>> regions.max() + 1 # Total number of image objects / labels / IDs 338 >>> regions # doctest:+ELLIPSIS array([[310, 310, 310, ..., 328, 328, 336], [334, 334, 334, ..., 328, 328, 336], [293, 293, 293, ..., 328, 328, 328], ..., [ 0, 0, 0, ..., 20, 20, 20], [ 0, 0, 0, ..., 20, 20, 20], [ 0, 0, 0, ..., 20, 20, 20]]) Send an image as a rasterio compatible bytestream in the file of a POST request You'll get a GeoTIFF back with the same georeferencing >>> r = requests.post(endpoint, files={'file': open('img/agé.bmp', 'rb')}) >>> regions = rasterio.io.MemoryFile(r.content).open().read(1) >>> regions.shape (127, 212) >>> regions.max() + 1 338 Send the link to a file on the internet as 'url' parameter of a GET request >>> r = requests.get(endpoint + '?url=https://api.imageseg.com/agé.bmp') >>> regions = rasterio.io.MemoryFile(r.content).open().read(1) >>> regions.shape (127, 212) >>> regions.max() + 1 338 Your multi-spectral sensor, computed bands, or EO fused with LiDAR are cool too >>> rasterio.open('img/11-band.tif').profile #doctest:+NORMALIZE_WHITESPACE {'driver': 'GTiff', 'dtype': 'int16', 'nodata': None, 'width': 200, 'height': 200, 'count': 11, 'crs': CRS.from_epsg(32613), 'transform': Affine(30.0, 0.0, 399255.0, 0.0, -30.0, 2605275.0), 'tiled': False, 'interleave': 'pixel'} >>> r = requests.post(endpoint, files={'file': open('img/11-band.tif', 'rb')}) >>> dataset = rasterio.io.MemoryFile(r.content).open() >>> dataset.profile #doctest:+NORMALIZE_WHITESPACE {'driver': 'GTiff', 'dtype': 'int32', 'nodata': 0.0, 'width': 200, 'height': 200, 'count': 1, 'crs': CRS.from_epsg(32613), 'transform': Affine(30.0, 0.0, 399255.0, 0.0, -30.0, 2605275.0), 'blockxsize': 256, 'blockysize': 256, 'tiled': True, 'compress': 'lzw', 'interleave': 'band'} >>> dataset.read(1).max() + 1 187 RapidEye from https://developers.planet.com/planetschool/downloading-imagery-with-data-api/ >>> rasterio.open('img/redding1.tiff').profile #doctest:+NORMALIZE_WHITESPACE {'driver': 'GTiff', 'dtype': 'uint8', 'nodata': None, 'width': 5000, 'height': 5000, 'count': 4, 'crs': CRS.from_epsg(32610), 'transform': Affine(5.0, 0.0, 523500.0, 0.0, -5.0, 4536500.0), 'tiled': False, 'compress': 'lzw', 'interleave': 'pixel'} >>> r = requests.post(endpoint, files={'file': open('img/redding1.tiff', 'rb')}) ... #doctest:+SKIP >>> rasterio.io.MemoryFile(r.content).open().read(1).max() + 1 ... #doctest:+SKIP 172859 """ import numpy as np import rasterio import requests import doctest doctest.testmod() app.py: -------------- """ Google Cloud Run serverless deployment of BIS API by James Berkeley Image Seg is a region merging image object segmentation algorithm Give it multiband satellite imagery and it returns an int image labeled with object IDs Using Cloud Run because the core algorithm is Cython compiled to an so/pyd file www.imageseg.com jscar@berkenviro.com The Google Cloud Run tutorial https://cloud.google.com/run/docs/quickstarts/build-and-deploy Admin page https://console.cloud.google.com/run?authuser=1&project=graphite-proton-825 Mapped api.imageseg.com subdomain managed by wix.com to Cloud Run https://cloud.google.com/run/docs/mapping-custom-domains https://www.google.com/webmasters/verification/home?hl=en&authuser=1 On Wix console set CNAME from api.imageseg.com to ghs.googlehosted.com """ import os, json from flask import Flask, Response, request, jsonify, send_file from tempfile import NamedTemporaryFile import numpy as np import rasterio from rasterio.profiles import DefaultGTiffProfile CLOUD = bool(os.environ.get('K_SERVICE', None)) if CLOUD: "Google Cloud Run per reserved environment variable" from cregionmerge_ubuntu18_04_64bit_py36_20190309 import cregionmerge else: "Local Mac development and corresponding binary" from cregionmerge_macos10_15_64bit_py37_20191025 import cregionmerge app = Flask(__name__) @app.route('/v1/segment', methods=['GET', 'POST']) def entry(): """Web wrapper for segmentation call into BIS.""" wasfile = False if 'file' in request.files: wasfile = True f = request.files['file'] filename = f.name tmpname = NamedTemporaryFile().name f.save(tmpname) dataset = rasterio.open(tmpname) array = dataset.read() os.remove(tmpname) elif 'url' in request.args: wasfile = True filename = request.args.get('url') dataset = rasterio.open(filename) array = dataset.read() elif request.json: array = request.json else: array = request.args.get('array') if not array: raise ValueError("Send image as file data, json data, " "url parameter, or array parameter") array = json.loads(array) regions = segment(np.asarray(array)) if wasfile: new = DefaultGTiffProfile(count=1) old = dataset.profile new['height'], new['width'] = dataset.shape new['crs'] = old['crs'] new['transform'] = old['transform'] new['dtype'] = regions.dtype memfile = rasterio.io.MemoryFile() memfile.open(**new).write(regions, 1) return send_file(memfile, download_name=filename + '_10_05_05.tif', mimetype='image/tiff') else: return jsonify(regions.tolist()) def segment(array, t=10, s=0.5, c=0.5, nd=False, ndv=0): """BIS API segmentation call. >>> segment(np.array([[[1]], [[1]], [[1]]])) # One pixel, three band array([[0]], dtype=int32) >>> array = rasterio.open('img/agé.bmp').read() # Plain RGB image >>> array.shape # nbands, height, width (3, 127, 212) >>> array # doctest:+ELLIPSIS array([[[154, 151, 160, ..., 152, 152, 255], ... [ 57, 61, 64, ..., 55, 61, 56]]], dtype=uint8) >>> regions = segment(array) >>> regions.shape (127, 212) >>> regions # doctest:+ELLIPSIS array([[310, 310, 310, ..., 328, 328, 336], ... [ 0, 0, 0, ..., 20, 20, 20]], dtype=int32) >>> regions.max() + 1 # Total number of image objects / labels / IDs 338 """ array = array.transpose(1, 2, 0) # BIS wants pixel interleaved height, width, nbands = array.shape size = height * width merger = cregionmerge.cmerger(array, size, width, height, nbands, s, c, nodata=nd, nd_val=ndv) regions = np.zeros((height, width), dtype=np.int32) merger.merge(t, regions) return regions @app.route('/v1/test') def test(): """Trivial handling of a request with parameters / first function.""" array = request.args.get('array', '[[0,0,0]]') add = request.args.get('add', '0') answer = np.array(json.loads(array)) + float(add) return _debug(answer) @app.route('/') def root(): """Lay bare the source code as the help!""" files = ['examples.py', 'app.py', 'Dockerfile', '.dockerignore', 'deploy.zsh'] s = '\n\n'.join([f + ':\n--------------\n' + open(f).read() for f in files]) s += '\n\nTest image available from https://api.imageseg.com/agé.bmp\n' return _debug(s) @app.route('/agé.bmp') def ag(): """Serve up this one file for user testing""" return send_file('img/agé.bmp') def _debug(answer): body = f"{answer}\n\nWeb args: {list(request.args.items())}\nOn cloud: {CLOUD}" return Response(body, mimetype='text/plain') if __name__ == '__main__': import doctest doctest.testmod() port = int(os.environ.get('PORT', 8080)) app.run(debug=True, host='0.0.0.0', port=port) Dockerfile: -------------- # Template from Google Cloud Run tutorial -js # Use the official lightweight Python image. # https://hub.docker.com/_/python FROM python:3.7-slim # Copy local code to the container image. ENV APP_HOME /app WORKDIR $APP_HOME COPY . ./ # Install production dependencies. RUN pip install Flask gunicorn numpy rasterio # Run the web service on container startup. Here we use the gunicorn # webserver, with one worker process and 8 threads. # For environments with multiple CPU cores, increase the number of workers # to be equal to the cores available. CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 app:app .dockerignore: -------------- # Dockerfile README.md *.pyc *.pyo *.pyd venv # *.zsh __pycache__ deploy.zsh: -------------- #!/bin/zsh ~/Downloads/google-cloud-sdk/bin/gcloud config set run/region us-central1 ~/Downloads/google-cloud-sdk/bin/gcloud builds submit --tag gcr.io/graphite-proton-825/app ~/Downloads/google-cloud-sdk/bin/gcloud beta run deploy app --image gcr.io/graphite-proton-825/app --platform managed Test image available from https://api.imageseg.com/agé.bmp Web args: [] On cloud: True