Borner le comblement du MNT à l'enveloppe des points, ajouter la couche précision et un affichage relief/précision, encoder les sous-tuiles une seule fois en q75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Antoine Jacquin
2026-09-27 15:07:04 +02:00
parent c5097e21a0
commit 90dbe61f3b
17 changed files with 994 additions and 593 deletions

View File

@ -140,6 +140,105 @@ class TestInterpolateHoles:
assert np.isnan(filled).all()
class TestFillSmallGaps:
"""Comblement borné à l'enveloppe des points (plus de pastilles ni de liseré)."""
@staticmethod
def _grid(n=200, step=2, value=10.0):
"""Semis régulier de points (1 pixel sur `step`) sur n × n pixels."""
dtm = np.full((n, n), np.nan)
dtm[::step, ::step] = value
return dtm
def test_isolated_point_is_removed_not_grown(self):
from lidar_pipeline.dtm import _fill_small_gaps
dtm = np.full((100, 100), np.nan)
dtm[50, 50] = 5.0
out, filled, removed = _fill_small_gaps(dtm, 0.2)
assert np.isnan(out).all() # ni pastille, ni point seul
assert (filled, removed) == (0, 1)
def test_gaps_between_points_filled_without_edge_band(self):
from lidar_pipeline.dtm import _fill_small_gaps
dtm = self._grid()
dtm[:, 100:] = np.nan # grand trou à l'est
out, filled, _ = _fill_small_gaps(dtm, 0.2)
assert filled > 0
assert not np.isnan(out[10:190, 10:99]).any() # vides entre points comblés
assert np.isnan(out[:, 99:]).all() # rien d'extrapolé dans le trou
def test_radius_follows_local_density(self):
"""Semis clairsemé (1 point / 1,8 m, vides de 2,5 m en diagonale)
comblé ; trou de 3 m en zone dense conservé ; trou de 1,6 m comblé."""
from lidar_pipeline.dtm import _fill_small_gaps
sparse = self._grid(step=9)
out, _, _ = _fill_small_gaps(sparse, 0.2)
assert not np.isnan(out[30:170, 30:170]).any()
dense = self._grid(step=1)
dense[90:105, 90:105] = np.nan # trou de 3 m dans un semis plein
dense[40:48, 40:48] = np.nan # trou de 1,6 m (voiture)
out, _, _ = _fill_small_gaps(dense, 0.2)
assert np.isnan(out[95:100, 95:100]).all()
assert not np.isnan(out[40:48, 40:48]).any()
def test_morph_disk_matches_scipy(self):
from scipy import ndimage as nd
from lidar_pipeline.dtm import _morph_disk
rng = np.random.default_rng(0)
mask = rng.random((60, 70)) < 0.05
square = np.ones((3, 3), dtype=bool)
cross = nd.generate_binary_structure(2, 1)
ref = mask
for i in range(5):
ref = nd.binary_dilation(ref, structure=square if i % 2 == 0 else cross)
assert (_morph_disk(mask, 5) == ref).all()
ref_e = ref
for i in range(5):
ref_e = nd.binary_erosion(ref_e, structure=square if i % 2 == 0 else cross,
border_value=1)
assert (_morph_disk(ref, 5, erode=True) == ref_e).all()
def test_dtm_records_gap_fill_version(self, tmp_output_dir):
import laspy
import rasterio
from lidar_pipeline.dtm import (create_dtm_fast, read_dtm_gap_fill,
GAP_FILL_VERSION)
hdr = laspy.LasHeader(version='1.2', point_format=0)
las = laspy.LasData(hdr)
las.x, las.y, las.z = [0.5, 1.5, 0.5, 1.5], [0.5, 0.5, 1.5, 1.5], [10.0] * 4
las.write(str(tmp_output_dir / "g.las"))
out = create_dtm_fast(tmp_output_dir / "g.las", "g", tmp_output_dir, 1.0,
force=True, strip_align=False)
assert read_dtm_gap_fill(out) == GAP_FILL_VERSION
with rasterio.open(str(out)) as src:
src.tags() # lisible
legacy = tmp_output_dir / "legacy.tif"
with rasterio.open(str(legacy), "w", driver="GTiff", width=2, height=2,
count=1, dtype="float32") as dst:
dst.write(np.zeros((1, 2, 2), dtype="float32"))
assert read_dtm_gap_fill(legacy) == 1
class TestDensitySidecar:
def test_dtm_writes_ground_density(self, tmp_output_dir):
"""4 points par m² sur 10 × 10 m : densité 4 au cœur, grille de 1 m."""
import laspy
import rasterio
from lidar_pipeline.dtm import create_dtm_fast, density_path
g = (np.arange(20) + 0.25) / 2.0 # pas de 0,5 m
xx, yy = np.meshgrid(g, g)
hdr = laspy.LasHeader(version='1.2', point_format=0)
las = laspy.LasData(hdr)
las.x, las.y, las.z = xx.ravel(), yy.ravel(), np.full(xx.size, 10.0)
las.write(str(tmp_output_dir / "d.las"))
out = create_dtm_fast(tmp_output_dir / "d.las", "d", tmp_output_dir, 0.5,
force=True, strip_align=False)
with rasterio.open(density_path(out)) as src:
dens = src.read(1)
assert abs(src.transform.a - 1.0) < 1e-9
assert np.allclose(dens[2:-2, 2:-2], 4.0)
class TestDetectGroundMethod:
def _make_mock_las(self, num_returns, z_values):
"""Create a mock laspy object with specified NumberOfReturns and z."""