Introduction to PyWPS
Introduction to PyWPS
What is PyWPS?
PyWPS (Python Web Processing Service) is a free and open-source implementation of the OGC WPS (Web Processing Service) standard, which allows spatial processing tasks (Python scripts, geospatial tools, models) to be published as web services.
How it works
- A client (e.g., QGIS, browser) sends a WPS request (
GetCapabilities
/Execute
). - PyWPS receives the HTTP request and executes a defined Python process.
- The result (value, file, image, GeoJSON, etc.) is returned to the client.
Project structure
A typical PyWPS project looks like this:
my-wps/
├── wps_raster_analyser.py # the process
├── pywps.cfg # WPS service configuration
└── wps.py # service entry point
Simple process example: GetRasterProcess
wps_raster_analyser.py
Here’s an example that applies a filter to an image retrieved from a WMS server. The advantage of WMS is that data can be requested via web services.
The example below illustrates a Sobel or Prewitt filter applied to an image from a WMS service.
from pywps import Process, LiteralInput, ComplexOutput
from pywps.inout.formats import Format
from pywps.app.Common import Metadata
import requests
from PIL import Image
from io import BytesIO
import os
import numpy as np
from PIL import ImageFilter
# sobel filter x and y coefficients
sobel_x_coeffs = [
-1, 0, 1,
-2, 0, 2,
-1, 0, 1
]
sobel_y_coeffs = [
-1, -2, -1,
0, 0, 0,
1, 2, 1
]
# prewitt filter x and y coefficients
prewitt_x_coeffs = [
-1, 0, 1,
-1, 0, 1,
-1, 0, 1
]
prewitt_y_coeffs = [
-1, -1, -1,
0, 0, 0,
1, 1, 1
]
class GetRasterProcess(Process):
"""Process to get a raster image from a WMS layer and apply a filter."""
def __init__(self):
"""Initialize the process with inputs and outputs."""
inputs = [
# Input for WMS layer, latitude, longitude, and method
LiteralInput('wms', 'WMS Layer', data_type='string'),
LiteralInput('lat', 'Latitude', data_type='float'),
LiteralInput('lon', 'Longitude', data_type='float'),
LiteralInput('method', 'Method', data_type='string', default='sobel_x', allowed_values=['sobel_x', 'sobel_y', 'prewitt_x', 'prewitt_y']),
]
# Output for the raster image
outputs = [
ComplexOutput('image', 'WMS Result Image',
as_reference=True,
supported_formats=[Format('image/png')])
]
super().__init__(
self._handler,
identifier='get_raster_process',
title='Get Raster Image',
abstract='Get a 512x512 raster image centered at the given lat/lon.',
metadata=[Metadata('Raster Image')],
inputs=inputs,
outputs=outputs
)
# Handler function to process the request
def _handler(self, request, response):
lat = request.inputs['lat'][0].data
lon = request.inputs['lon'][0].data
wms_layer = request.inputs['wms'][0].data
method = request.inputs.get('method', ['slic'])[0].data
output_type = request.outputs['image']['mimetype']
# Geoserver Config
wms_url = "http://geoserver:8080/geoserver/wms"
layer = wms_layer
width, height = 512, 512
bbox_size_deg = 0.0005 # taille du carré autour du point (à adapter selon l'échelle)
# Estimate bbox around the point
minx = lon - bbox_size_deg / 2
maxx = lon + bbox_size_deg / 2
miny = lat - bbox_size_deg / 2
maxy = lat + bbox_size_deg / 2
bbox = f"{minx},{miny},{maxx},{maxy}"
# Get the WMS image
params = {
"service": "WMS",
"version": "1.1.1",
"request": "GetMap",
"layers": layer,
"bbox": bbox,
"width": width,
"height": height,
"srs": "EPSG:4326",
"format": "image/png"
}
# send the request to the WMS server
r = requests.get(wms_url, params=params)
r.raise_for_status()
# Save the image to a BytesIO object
img = Image.open(BytesIO(r.content))
# according to the method, apply the corresponding filter
if method == 'sobel_x':
# apply the Sobel filter in X direction
kernel = np.array(sobel_x_coeffs).reshape((3, 3))
elif method == 'sobel_y':
# apply the Sobel filter in Y direction
kernel = np.array(sobel_y_coeffs).reshape((3, 3))
elif method == 'prewitt_x':
# apply the Prewitt filter in X direction
kernel = np.array(prewitt_x_coeffs).reshape((3, 3))
elif method == 'prewitt_y':
# apply the Prewitt filter in Y direction
kernel = np.array(prewitt_y_coeffs).reshape((3, 3))
else:
raise ValueError("Invalid method specified. Use 'sobel_x', 'sobel_y', 'prewitt_x', or 'prewitt_y'.")
# Apply the filter
# Convert the image to grayscale if it is not already
img = img.convert('L')
# Apply the kernel filter
img = img.filter(ImageFilter.Kernel((3, 3), kernel.flatten(), scale=1, offset=0))
# keep the result in the /app/images directory
os.makedirs("/app/images", exist_ok=True)
# Save the image to a file
image_path = f"/app/images/tile_{method}_{lat}_{lon}.png"
img.save(image_path)
# response type
response.outputs['image'].output_format = Format(output_type)
response.outputs['image'].file = image_path
return response
Running the service
wps.py
from pywps import Service
from wps_raster_analyser import GetRasterProcess
processes = [GetRasterProcess()]
application = Service(processes, config_file='pywps.cfg')
Then run with:
gunicorn wps:application
Configuration: pywps.cfg
[server]
url = http://localhost:5000/wps
maxprocesses = 10
processes = 5
outputpath = outputs
logfile = pywps.log
loglevel = DEBUG
Docker integration
You can use docker-compose
to deploy the service:
pywps:
build:
context: ./pywps
ports:
- "5001:5000"
volumes:
- ./pywps:/app
If you want to use advanced libraries (like rasterio, gdal…), it’s better to define a Dockerfile and a requirements.txt
file.
Dockerfile:
FROM python:3.11-slim
RUN apt-get update && apt-get install -y \
libgdal-dev \
gdal-bin \
libexpat1 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "wps:application", "--bind", "0.0.0.0:5000"]
requirements.txt
pywps
gunicorn
requests
pillow
numpy
rasterio
Testing with curl
curl "http://localhost:5000/wps?service=WPS&request=GetCapabilities"
Testing in QGIS
- Go to Processing > Toolbox > Add WPS Server.
- Add the URL:
http://localhost:5000/wps
. - The
hello
process should appear and be ready to execute.
Execute the process
curl --location 'http://localhost:5000/wps/?service=WPS' \
--header 'Content-Type: text/xml' \
--data '<wps:Execute
xmlns:wps="http://www.opengis.net/wps/1.0.0"
xmlns:ows="http://www.opengis.net/ows/1.1"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/wps/1.0.0
http://schemas.opengis.net/wps/1.0.0/wpsExecute_request.xsd"
service="WPS"
version="1.0.0"
storeExecuteResponse="false"
status="false">
<ows:Identifier>get_raster_process</ows:Identifier>
<wps:DataInputs>
<wps:Input>
<ows:Identifier>wms</ows:Identifier>
<wps:Data>
<wps:LiteralData>geoimage:lon_bing_mosaic_tiled</wps:LiteralData>
</wps:Data>
</wps:Input>
<wps:Input>
<ows:Identifier>method</ows:Identifier>
<wps:Data>
<wps:LiteralData>sobel_y</wps:LiteralData>
</wps:Data>
</wps:Input>
<wps:Input>
<ows:Identifier>lat</ows:Identifier>
<wps:Data>
<wps:LiteralData>45.55425883140662</wps:LiteralData>
</wps:Data>
</wps:Input>
<wps:Input>
<ows:Identifier>lon</ows:Identifier>
<wps:Data>
<wps:LiteralData>-73.49350601434709</wps:LiteralData>
</wps:Data>
</wps:Input>
</wps:DataInputs>
<wps:ResponseForm>
<wps:RawDataOutput mimeType="image/png">
<ows:Identifier>image</ows:Identifier>
</wps:RawDataOutput>
</wps:ResponseForm>
</wps:Execute>'
The result will be a PNG image.
📚 Resources
- Official documentation: https://pywps.readthedocs.io/
- OGC WPS standard: https://www.ogc.org/standards/wps