Source code for shapelib.tools.lines.dedup.core
"""Drop near-coincident (stacked) lines, keeping the longest of each group.
Drone-mapped rows sometimes carry a near-duplicate: two input lines trace the
same physical row a few millimetres apart. That twin poisons downstream tools —
in :mod:`shapelib.tools.lines.merge` it becomes the "rail" of its own sibling and
the merge follows a phantom row. This tool collapses each cluster of stacked
lines into a single representative (the longest one).
A line is dropped when it lies entirely inside the tolerance tube of a longer,
kept line — tested exactly with a buffered ``covers`` predicate evaluated inside
the spatial index, so no point sampling is involved.
The public entry point is :func:`deduplicate_lines`.
"""
import geopandas as gpd
import numpy as np
import shapely
from shapelib.utils.utils import multiline_to_single_line
def _keep_mask(geoms: np.ndarray, tolerance: float) -> np.ndarray:
"""Boolean mask of the lines to keep after collapsing stacked duplicates.
Lines are visited longest-first; each kept line drops every not-yet-dropped
line of equal-or-shorter length that its tolerance buffer fully covers. The
``covers`` predicate is applied by the STRtree in a single GEOS call per kept
line, so the covered-by test is exact rather than sampled.
"""
lengths = shapely.length(geoms)
buffers = shapely.buffer(geoms, tolerance)
tree = shapely.STRtree(geoms)
dropped = np.zeros(len(geoms), dtype=bool)
for i in np.argsort(lengths)[::-1]: # longest first
if dropped[i]:
continue
for j in tree.query(buffers[i], predicate="covers"):
if j != i and not dropped[j] and lengths[j] <= lengths[i]:
dropped[j] = True
return ~dropped
[docs]
def deduplicate_lines(
lines: gpd.GeoDataFrame,
tolerance: float = 0.05,
) -> gpd.GeoDataFrame:
"""
Drop near-coincident (stacked) lines, keeping the longest of each cluster.
Two input lines that trace the same physical row a few millimetres apart are
"stacked" duplicates: the shorter one lies entirely within the ``tolerance``
tube of the longer. This tool collapses every such cluster to its longest
line and passes all other lines through unchanged, so the output is the same
line set with the phantom twins removed. Filtering by talhão is the caller's
responsibility — pass only the lines of a single talhão.
Parameters
----------
lines : geopandas.GeoDataFrame
Line geometries. Must be in a projected CRS (metres), since ``tolerance``
is expressed in CRS units. MultiLineStrings are flattened to single
LineStrings before the coverage test.
tolerance : float, default 0.05
Maximum perpendicular distance (CRS units) between two lines for one to
count as a stacked duplicate of the other. A line is dropped only when it
lies entirely within this distance of a longer, kept line.
Returns
-------
geopandas.GeoDataFrame
A subset of ``lines`` (original attribute columns and CRS preserved) with
the stacked duplicates removed. When two covering lines have exactly equal
length, the first in the length ordering is kept.
Examples
--------
>>> import shapelib
>>> lines = shapelib.read_file("lines.shp")
>>> lines.to_utm()
>>> cleaned = deduplicate_lines(lines, tolerance=0.05)
>>> len(cleaned) <= len(lines)
True
"""
if lines is None or len(lines) == 0:
return lines
geoms = np.array(
[multiline_to_single_line(g) for g in lines.geometry.values], dtype=object
)
keep = _keep_mask(geoms, tolerance)
return lines.iloc[keep].copy()