Skip to content

Smooth node #431

@koettert

Description

@koettert

The node should work similar like the https://help.alteryx.com/current/en/designer/tools/spatial/smooth-tool.html#smooth-tool from Alteryx. This could be done by implementing an algorithm using the Shapely library.

`import geopandas as gpd
from shapely.geometry import (
LineString,
MultiLineString,
Polygon,
MultiPolygon,
LinearRing,
GeometryCollection,
)

def _chaikin_coords(coords, iterations=3, closed=False):
"""
Smooth a coordinate sequence using Chaikin's corner-cutting algorithm.
"""
coords = list(coords)

if closed and coords[0] == coords[-1]:
    coords = coords[:-1]

for _ in range(iterations):
    new_coords = []

    pairs = (
        zip(coords, coords[1:] + coords[:1])
        if closed
        else zip(coords[:-1], coords[1:])
    )

    if not closed:
        new_coords.append(coords[0])

    for p0, p1 in pairs:
        x0, y0 = p0[:2]
        x1, y1 = p1[:2]

        q = (0.75 * x0 + 0.25 * x1, 0.75 * y0 + 0.25 * y1)
        r = (0.25 * x0 + 0.75 * x1, 0.25 * y0 + 0.75 * y1)

        new_coords.extend([q, r])

    if not closed:
        new_coords.append(coords[-1])

    coords = new_coords

if closed:
    coords.append(coords[0])

return coords

def smooth_linestring(line, iterations=3):
if len(line.coords) < 3:
return line

return LineString(
    _chaikin_coords(line.coords, iterations=iterations, closed=False)
)

def smooth_polygon(poly, iterations=3):
if poly.is_empty:
return poly

exterior = LinearRing(
    _chaikin_coords(poly.exterior.coords, iterations=iterations, closed=True)
)

interiors = [
    LinearRing(
        _chaikin_coords(ring.coords, iterations=iterations, closed=True)
    )
    for ring in poly.interiors
]

smoothed = Polygon(exterior, interiors)

# Optional validity repair
if not smoothed.is_valid:
    smoothed = smoothed.buffer(0)

return smoothed

def smooth_geometry(geom, iterations=3):
"""
Smooth Shapely geometries while preserving GeoPandas compatibility.
Supports LineString, MultiLineString, Polygon, MultiPolygon,
and GeometryCollection.
"""
if geom is None or geom.is_empty:
return geom

geom_type = geom.geom_type

if geom_type == "LineString":
    return smooth_linestring(geom, iterations)

if geom_type == "MultiLineString":
    return MultiLineString(
        [smooth_linestring(part, iterations) for part in geom.geoms]
    )

if geom_type == "Polygon":
    return smooth_polygon(geom, iterations)

if geom_type == "MultiPolygon":
    return MultiPolygon(
        [smooth_polygon(part, iterations) for part in geom.geoms]
    )

if geom_type == "GeometryCollection":
    return GeometryCollection(
        [smooth_geometry(part, iterations) for part in geom.geoms]
    )

# Points and unsupported geometries are returned unchanged
return geom

def smooth_geodataframe(gdf, iterations=3, inplace=False):
"""
Smooth geometries in a GeoDataFrame.

Parameters
----------
gdf : geopandas.GeoDataFrame
iterations : int
    More iterations means smoother geometry, but more vertices.
inplace : bool
    If True, modifies the input GeoDataFrame.

Returns
-------
geopandas.GeoDataFrame
"""
result = gdf if inplace else gdf.copy()

result.geometry = result.geometry.apply(
    lambda geom: smooth_geometry(geom, iterations=iterations)
)

return result`

Usage
`# Recommended: use a projected CRS, not latitude/longitude
gdf_projected = gdf.to_crs("EPSG:3857")

gdf_smooth = smooth_geodataframe(
gdf_projected,
iterations=3
)

Convert back if needed

gdf_smooth = gdf_smooth.to_crs(gdf.crs)`

Details: https://chatgpt.com/share/e/69f359d5-02e4-800c-bb84-5bcb74882ae6

Metadata

Metadata

Assignees

No one assigned

    Labels

    new nodeSpecial enhancement which has a new KNIME node as outcome

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions