¿Cómo escribir geometrías bien formadas en archivos shape?

26

¿Alguien puede demostrar una manera simple de escribir estructuras de datos de geometría de bien formados a archivos de formas? Estoy particularmente interesado en polígonos con agujeros y cadenas lineales. También sería beneficioso mantenerse alejado de arcpy (por lo que osgeo, pyshp, etc., todo sería mejor).

terra_matics
fuente

Respuestas:

44

El binario conocido es un buen formato de intercambio binario que se puede intercambiar con un montón de software SIG, incluidos Shapely y GDAL / OGR.

Este es un pequeño ejemplo del flujo de trabajo con osgeo.ogr:

from osgeo import ogr
from shapely.geometry import Polygon

# Here's an example Shapely geometry
poly = Polygon([(0, 0), (0, 1), (1, 1), (0, 0)])

# Now convert it to a shapefile with OGR    
driver = ogr.GetDriverByName('Esri Shapefile')
ds = driver.CreateDataSource('my.shp')
layer = ds.CreateLayer('', None, ogr.wkbPolygon)
# Add one attribute
layer.CreateField(ogr.FieldDefn('id', ogr.OFTInteger))
defn = layer.GetLayerDefn()

## If there are multiple geometries, put the "for" loop here

# Create a new feature (attribute and geometry)
feat = ogr.Feature(defn)
feat.SetField('id', 123)

# Make a geometry, from Shapely object
geom = ogr.CreateGeometryFromWkb(poly.wkb)
feat.SetGeometry(geom)

layer.CreateFeature(feat)
feat = geom = None  # destroy these

# Save and close everything
ds = layer = feat = geom = None

Actualización : aunque el póster ha aceptado la respuesta GDAL / OGR, aquí hay un equivalente de Fiona :

from shapely.geometry import mapping, Polygon
import fiona

# Here's an example Shapely geometry
poly = Polygon([(0, 0), (0, 1), (1, 1), (0, 0)])

# Define a polygon feature geometry with one attribute
schema = {
    'geometry': 'Polygon',
    'properties': {'id': 'int'},
}

# Write a new Shapefile
with fiona.open('my_shp2.shp', 'w', 'ESRI Shapefile', schema) as c:
    ## If there are multiple geometries, put the "for" loop here
    c.write({
        'geometry': mapping(poly),
        'properties': {'id': 123},
    })

(Nota usuarios de Windows: no tienes excusa )

Mike T
fuente
Me interesa saber por qué elegiste este método en lugar de la biblioteca Fiona.
Nathan W
1
Bueno, el póster buscaba un ejemplo de osgeo.ogr, y la comparación es interesante.
sgillies
Comparación explícita de @sgillies agregada
Mike T
3
Bueno, para ser honesto, fue sobre todo pragmática. Aprecié el esfuerzo de demostrar el código en respuesta a mi pregunta y ya estaba bromeando con Osgeo. Desde entonces he probado ambos métodos y ambos son respuestas suficientes. Aprecio que el esfuerzo de los respondedores sea preciso y rápido.
terra_matics
@ Mike T Con respecto al enfoque osgeo.ogr, lo estoy usando en un complemento de Python para QGIS. El archivo de forma considerado que se escribirá es una Línea (LineString en Shapely). Donde ha definido la variable "poli", he definido una variable de "línea" con coordenadas de un Qgs.Rectangle. He usado el código exacto, sin errores, pero no agrega una característica y me da un archivo de forma sin características.
Akhil
28

He diseñado a Fiona para que funcione bien con Shapely. Aquí hay un ejemplo muy simple de usarlos juntos para "limpiar" las características del archivo de forma:

import logging
import sys

from shapely.geometry import mapping, shape

import fiona

logging.basicConfig(stream=sys.stderr, level=logging.INFO)

with fiona.open('docs/data/test_uk.shp', 'r') as source:

    # **source.meta is a shortcut to get the crs, driver, and schema
    # keyword arguments from the source Collection.
    with fiona.open(
            'with-shapely.shp', 'w',
            **source.meta) as sink:

        for f in source:

            try:
                geom = shape(f['geometry'])
                if not geom.is_valid:
                    clean = geom.buffer(0.0)
                    assert clean.is_valid
                    assert clean.geom_type == 'Polygon'
                    geom = clean
                f['geometry'] = mapping(geom)
                sink.write(f)

            except Exception, e:
                # Writing uncleanable features to a different shapefile
                # is another option.
                logging.exception("Error cleaning feature %s:", f['id'])

Desde https://github.com/Toblerity/Fiona/blob/master/examples/with-shapely.py .

sgillies
fuente
6

También puede escribir geometrías Shapely usando PyShp (ya que el póster original también preguntó sobre PyShp).

Una forma sería convertir su geometría bien formada a geojson (con el método shapely.geometry.mapping) y luego usar mi bifurcación modificada de PyShp que proporciona un método de Escritor que acepta diccionarios de geometría geojson cuando escribe en un shapefile.

Si prefiere confiar en la versión principal de PyShp, también he proporcionado una función de conversión a continuación:

# THIS FUNCTION CONVERTS A GEOJSON GEOMETRY DICTIONARY TO A PYSHP SHAPE OBJECT
def shapely_to_pyshp(shapelygeom):
    # first convert shapely to geojson
    try:
        shapelytogeojson = shapely.geometry.mapping
    except:
        import shapely.geometry
        shapelytogeojson = shapely.geometry.mapping
    geoj = shapelytogeojson(shapelygeom)
    # create empty pyshp shape
    record = shapefile._Shape()
    # set shapetype
    if geoj["type"] == "Null":
        pyshptype = 0
    elif geoj["type"] == "Point":
        pyshptype = 1
    elif geoj["type"] == "LineString":
        pyshptype = 3
    elif geoj["type"] == "Polygon":
        pyshptype = 5
    elif geoj["type"] == "MultiPoint":
        pyshptype = 8
    elif geoj["type"] == "MultiLineString":
        pyshptype = 3
    elif geoj["type"] == "MultiPolygon":
        pyshptype = 5
    record.shapeType = pyshptype
    # set points and parts
    if geoj["type"] == "Point":
        record.points = geoj["coordinates"]
        record.parts = [0]
    elif geoj["type"] in ("MultiPoint","Linestring"):
        record.points = geoj["coordinates"]
        record.parts = [0]
    elif geoj["type"] in ("Polygon"):
        record.points = geoj["coordinates"][0]
        record.parts = [0]
    elif geoj["type"] in ("MultiPolygon","MultiLineString"):
        index = 0
        points = []
        parts = []
        for eachmulti in geoj["coordinates"]:
            points.extend(eachmulti[0])
            parts.append(index)
            index += len(eachmulti[0])
        record.points = points
        record.parts = parts
    return record

Simplemente copie y pegue la función en su propia secuencia de comandos y solicítela para convertir cualquiera de sus geometrías bien formadas a una forma compatible con pyshp. Para guardarlos, simplemente agregue cada forma de pyshp resultante a la lista ._shapes de la instancia de shapefile.Writer (para ver un ejemplo, vea el script de prueba al final de esta publicación).

Sin embargo, tenga en cuenta que la función NO manejará ningún orificio interior de polígono si lo hay, simplemente los ignora. Ciertamente es posible agregar esa funcionalidad a la función, pero simplemente no me he molestado todavía. Sugerencias o ediciones para mejorar la función son bienvenidas :)

Aquí hay un script de prueba independiente completo:

### HOW TO SAVE SHAPEFILE FROM SHAPELY GEOMETRY USING PYSHP

# IMPORT STUFF
import shapefile
import shapely, shapely.geometry

# CREATE YOUR SHAPELY TEST INPUT
TEST_SHAPELYSHAPE = shapely.geometry.Polygon([(133,822),(422,644),(223,445),(921,154)])

#########################################################
################## END OF USER INPUT ####################
#########################################################

# DEFINE/COPY-PASTE THE SHAPELY-PYSHP CONVERSION FUNCTION
def shapely_to_pyshp(shapelygeom):
    # first convert shapely to geojson
    try:
        shapelytogeojson = shapely.geometry.mapping
    except:
        import shapely.geometry
        shapelytogeojson = shapely.geometry.mapping
    geoj = shapelytogeojson(shapelygeom)
    # create empty pyshp shape
    record = shapefile._Shape()
    # set shapetype
    if geoj["type"] == "Null":
        pyshptype = 0
    elif geoj["type"] == "Point":
        pyshptype = 1
    elif geoj["type"] == "LineString":
        pyshptype = 3
    elif geoj["type"] == "Polygon":
        pyshptype = 5
    elif geoj["type"] == "MultiPoint":
        pyshptype = 8
    elif geoj["type"] == "MultiLineString":
        pyshptype = 3
    elif geoj["type"] == "MultiPolygon":
        pyshptype = 5
    record.shapeType = pyshptype
    # set points and parts
    if geoj["type"] == "Point":
        record.points = geoj["coordinates"]
        record.parts = [0]
    elif geoj["type"] in ("MultiPoint","Linestring"):
        record.points = geoj["coordinates"]
        record.parts = [0]
    elif geoj["type"] in ("Polygon"):
        record.points = geoj["coordinates"][0]
        record.parts = [0]
    elif geoj["type"] in ("MultiPolygon","MultiLineString"):
        index = 0
        points = []
        parts = []
        for eachmulti in geoj["coordinates"]:
            points.extend(eachmulti[0])
            parts.append(index)
            index += len(eachmulti[0])
        record.points = points
        record.parts = parts
    return record

# WRITE TO SHAPEFILE USING PYSHP
shapewriter = shapefile.Writer()
shapewriter.field("field1")
# step1: convert shapely to pyshp using the function above
converted_shape = shapely_to_pyshp(TEST_SHAPELYSHAPE)
# step2: tell the writer to add the converted shape
shapewriter._shapes.append(converted_shape)
# add a list of attributes to go along with the shape
shapewriter.record(["empty record"])
# save it
shapewriter.save("test_shapelytopyshp.shp")
Karim Bahgat
fuente
5

La respuesta de Karim es bastante antigua, pero he usado su código y quería agradecerle por ello. Una cosa menor que descubrí usando su código: si el tipo de forma es Polígono o Multipolígono, aún podría tener múltiples partes (agujeros en el interior). Por lo tanto, parte de su código debe cambiarse a

elif geoj["type"] == "Polygon":
    index = 0
    points = []
    parts = []
    for eachmulti in geoj["coordinates"]:
        points.extend(eachmulti)
        parts.append(index)
        index += len(eachmulti)
    record.points = points
    record.parts = parts
elif geoj["type"] in ("MultiPolygon", "MultiLineString"):
    index = 0
    points = []
    parts = []
    for polygon in geoj["coordinates"]:
        for part in polygon:
            points.extend(part)
            parts.append(index)
            index += len(part)
    record.points = points
    record.parts = parts
lebenlechzer
fuente