Deduplicating intersections: block_size vs min_spacing#
When lines cross each other repeatedly in a small area (e.g. at very low angles, or with proximity_dist), create_intersection_table can return many near-duplicate crossovers. Two parameters thin them, keeping the best (lowest max_dist, preferring true intersections over proximity ones) intersection of each line pair per neighborhood:
``block_size``: 2D spatial window β drops an intersection if a better one of the same line pair is within this straight-line distance.
``min_spacing``: 1D along-line window β drops an intersection if a better one of the same line pair is within this distance along both lines.
The difference matters when a line doubles back (a βhairpinβ) and re-crosses another line: the two crossings are spatially close, but far apart along the hairpin line, so they constrain different parts of that line and are both worth keeping for levelling. block_size drops one of them; min_spacing keeps both.
If both parameters are passed, they act as one combined filter in a single pass: an intersection is dropped if it fails either test against an already-kept intersection.
[1]:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import airbornegeo
Synthetic survey#
Three lines:
Line 1: straight horizontal line.
Line 2: a hairpin which crosses line 1 twice, only 400 m apart spatially, but ~6.4 km apart along line 2.
Line 3: a wiggly low-angle line which grazes line 1, crossing it several times within a couple of kilometres β true duplicates we want to thin.
[ ]:
# line 1: straight
xa = np.arange(0, 14000, 50.0)
line1 = pd.DataFrame({"easting": xa, "northing": 0.0, "line": 1})
# line 2: hairpin, down 6 km at x=3000, across 400 m, back up at x=3400
d = np.arange(0, 12400, 50.0)
line2 = pd.DataFrame(
{
"easting": np.where(
d < 6000, 3000.0, np.where(d < 6400, 3000 + (d - 6000), 3400.0)
),
"northing": np.where(
d < 6000, 3000 - d, np.where(d < 6400, -3000.0, -3000 + (d - 6400))
),
"line": 2,
}
)
# line 3: low-angle graze of line 1 with a wiggle -> several crossings
xc = np.arange(6000, 13000, 50.0)
line3 = pd.DataFrame(
{
"easting": xc,
"northing": 100 * np.sin(xc / 300) + 0.04 * (xc - 9500),
"line": 3,
}
)
data = pd.concat([line1, line2, line3], ignore_index=True)
_fig, ax = plt.subplots(figsize=(8, 5))
for name, df in data.groupby("line"):
ax.plot(df.easting, df.northing, label=f"line {name}")
ax.legend()
ax.set_aspect("equal")
[ ]:
survey = airbornegeo.Survey(data, line_column="line")
No deduplication#
Without either parameter we get all the crossings: 2 hairpin crossings (lines 1/2) and several graze crossings (lines 1/3).
[ ]:
def plot_inters(inters, title):
_fig, ax = plt.subplots(figsize=(8, 5))
for name, df in data.groupby("line"):
ax.plot(df.easting, df.northing, lw=0.8, label=f"line {name}")
ax.scatter(
inters.easting,
inters.northing,
c="black",
zorder=3,
label="intersections",
)
ax.legend()
ax.set_aspect("equal")
ax.set_title(title)
[ ]:
survey.create_intersection_table(method="network", progressbar=False)
inters_none = survey.intersections
plot_inters(inters_none, f"no deduplication: {len(inters_none)} intersections")
inters_none
block_size only (2D)#
A 1 km spatial window correctly thins the graze crossings, but also wrongly collapses the two hairpin crossings into one, since they are only 400 m apart spatially.
[ ]:
survey.create_intersection_table(method="network", block_size=1000, progressbar=False)
inters_block_size = survey.intersections
plot_inters(
inters_block_size, f"block_size=1000: {len(inters_block_size)} intersections"
)
inters_block_size
min_spacing only (1D)#
A 2 km along-line window thins the graze crossings (they are within 2 km of each other along both lines), but keeps both hairpin crossings, since they are ~6.4 km apart along line 2. The line1_along_dist / line2_along_dist columns show the distance along each line used for this test.
[ ]:
survey.create_intersection_table(method="network", min_spacing=2000, progressbar=False)
inters_min_spacing = survey.intersections
plot_inters(
inters_min_spacing, f"min_spacing=2000: {len(inters_min_spacing)} intersections"
)
inters_min_spacing
Both together#
The two act as a single combined filter: an intersection is dropped if it is within block_size spatially or within min_spacing along both lines of a better intersection. A small block_size (e.g. to catch numerically-duplicate points) combines naturally with a larger min_spacing. Note a min_spacing larger than block_size is typical, since along-line distance is always at least the straight-line distance.
[ ]:
survey.create_intersection_table(
method="network",
block_size=200,
min_spacing=2000,
progressbar=False,
)
inters_both = survey.intersections
plot_inters(
inters_both, f"block_size=200, min_spacing=2000: {len(inters_both)} intersections"
)
inters_both
Summary#
Use
block_sizewhen you want at most one crossover of a line pair per spatial neighborhood, regardless of line topology.Use
min_spacingwhen crossovers should be thinned by how much of each line they constrain β it keeps spatially-close re-crossings (hairpins, loops) which are far apart along one of the lines.Passing both applies both criteria in one pass; an intersection must clear both to be kept.
Note that since distance along a line is always at least the straight-line distance, min_spacing=s only ever drops a subset of what block_size=s would drop β its value is that you can use a large min_spacing without collapsing genuinely distinct re-crossings.