Correction orientation nord + filtrage ReturnNumber=0 pour fichiers corrompus
- Inversion axe Y du DTM pour orientation nord correcte - Fallback filters.range pour fichiers LAZ avec ReturnNumber=0 - Flèche nord vectorielle noire au-dessus de la légende - 9/9 fichiers traités avec succès Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
99
analyze_and_fix.py
Executable file
99
analyze_and_fix.py
Executable file
@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script pour analyser et corriger les fichiers LAZ avec ReturnNumber=0
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def analyze_return_numbers(laz_file):
|
||||||
|
"""Analyse les valeurs ReturnNumber dans un fichier LAZ"""
|
||||||
|
print(f"Analyse de {laz_file}...")
|
||||||
|
|
||||||
|
# Pipeline PDAL pour extraire les statistiques ReturnNumber
|
||||||
|
stats_pipeline = [
|
||||||
|
{
|
||||||
|
"type": "readers.las",
|
||||||
|
"filename": str(laz_file)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "filters.stats",
|
||||||
|
"dimensions": "ReturnNumber,NumberOfReturns"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
stats_json = json.dumps(stats_pipeline)
|
||||||
|
stats_file = Path("/tmp/analyze_stats.json")
|
||||||
|
with open(stats_file, 'w') as f:
|
||||||
|
f.write(stats_json)
|
||||||
|
|
||||||
|
result = subprocess.run(['pdal', 'pipeline', str(stats_file)],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
stats = data.get('stats', {}).get('statistic', [])
|
||||||
|
print(f" Statistiques ReturnNumber:")
|
||||||
|
for stat in stats:
|
||||||
|
if stat.get('name') == 'ReturnNumber':
|
||||||
|
counts = stat.get('counts', {})
|
||||||
|
for val, count in counts.items():
|
||||||
|
if count > 0:
|
||||||
|
pct = count / stat.get('total', 1) * 100
|
||||||
|
print(f" ReturnNumber={val}: {count} points ({pct:.1f}%)")
|
||||||
|
else:
|
||||||
|
print(f" Erreur: {result.stderr}")
|
||||||
|
|
||||||
|
def fix_return_numbers(laz_file, output_file):
|
||||||
|
"""Corrige les valeurs ReturnNumber=0 en utilisant une approche différente"""
|
||||||
|
print(f"Tentative de correction de {laz_file}...")
|
||||||
|
|
||||||
|
# Approche: utiliser filters.python pour modifier les valeurs
|
||||||
|
fix_pipeline = [
|
||||||
|
{
|
||||||
|
"type": "readers.las",
|
||||||
|
"filename": str(laz_file)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "filters.python",
|
||||||
|
"script": """
|
||||||
|
def filter(ins, args):
|
||||||
|
for point in ins:
|
||||||
|
if point.ReturnNumber == 0:
|
||||||
|
point.ReturnNumber = 1
|
||||||
|
if point.NumberOfReturns == 0:
|
||||||
|
point.NumberOfReturns = 1
|
||||||
|
return True
|
||||||
|
"""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "writers.las",
|
||||||
|
"filename": str(output_file),
|
||||||
|
"extra_dims": "all"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
fix_json = json.dumps(fix_pipeline, indent=2)
|
||||||
|
fix_file = Path("/tmp/fix_pipeline.json")
|
||||||
|
with open(fix_file, 'w') as f:
|
||||||
|
f.write(fix_json)
|
||||||
|
|
||||||
|
result = subprocess.run(['pdal', 'pipeline', str(fix_file)],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f" ✓ Correction réussie: {output_file}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f" ✗ Erreur correction: {result.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
laz_file = sys.argv[1] if len(sys.argv) > 1 else "/data/input/LHD_FXX_1000_6882_PTS_LAMB93_IGN69.copc.laz"
|
||||||
|
|
||||||
|
analyze_return_numbers(laz_file)
|
||||||
|
|
||||||
|
output_file = laz_file.replace(".laz", "_fixed.laz").replace("/input/", "/temp/")
|
||||||
|
fix_return_numbers(laz_file, output_file)
|
||||||
116
process_lidar.py
116
process_lidar.py
@ -109,53 +109,7 @@ class LidarArchaeoPipeline:
|
|||||||
print(f" ✓ Classification déjà effectuée")
|
print(f" ✓ Classification déjà effectuée")
|
||||||
return output_las
|
return output_las
|
||||||
|
|
||||||
# First, try to fix ReturnNumber values if needed
|
pipeline_json = self.create_pipeline_json(laz_file, output_las)
|
||||||
fixed_las = self.temp_dir / f"{laz_file.stem}_fixed.las"
|
|
||||||
|
|
||||||
# Create preprocessing pipeline to fix ReturnNumber=0 issue using expression
|
|
||||||
fix_pipeline = [
|
|
||||||
{
|
|
||||||
"type": "readers.las",
|
|
||||||
"filename": str(laz_file)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "filters.expression",
|
|
||||||
"expression": "ReturnNumber = MAX(ReturnNumber, 1)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "filters.expression",
|
|
||||||
"expression": "NumberOfReturns = MAX(NumberOfReturns, 1)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "writers.las",
|
|
||||||
"filename": str(fixed_las),
|
|
||||||
"extra_dims": "all"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Try with fixed data first
|
|
||||||
fix_json = json.dumps(fix_pipeline)
|
|
||||||
fix_pipe_file = self.temp_dir / "fix_pipeline.json"
|
|
||||||
with open(fix_pipe_file, 'w') as f:
|
|
||||||
f.write(fix_json)
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
result = subprocess.run(['pdal', 'pipeline', str(fix_pipe_file)],
|
|
||||||
capture_output=True, text=True)
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f" → Correction ReturnNumber effectuée")
|
|
||||||
input_for_smrf = fixed_las
|
|
||||||
else:
|
|
||||||
print(f" → Erreur correction, utilisation fichier original")
|
|
||||||
input_for_smrf = laz_file
|
|
||||||
if fixed_las.exists():
|
|
||||||
fixed_las.unlink()
|
|
||||||
except Exception as e:
|
|
||||||
print(f" → Erreur prétraitement: {e}")
|
|
||||||
input_for_smrf = laz_file
|
|
||||||
|
|
||||||
pipeline_json = self.create_pipeline_json(input_for_smrf, output_las)
|
|
||||||
pipeline_file = self.temp_dir / "pipeline.json"
|
pipeline_file = self.temp_dir / "pipeline.json"
|
||||||
|
|
||||||
with open(pipeline_file, 'w') as f:
|
with open(pipeline_file, 'w') as f:
|
||||||
@ -169,6 +123,71 @@ class LidarArchaeoPipeline:
|
|||||||
)
|
)
|
||||||
print(f" ✓ Classification sol terminée")
|
print(f" ✓ Classification sol terminée")
|
||||||
return output_las
|
return output_las
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
error_msg = e.stderr.decode()
|
||||||
|
print(f" ✗ Erreur PDAL: {error_msg}")
|
||||||
|
|
||||||
|
# If error is about ReturnNumber=0, try filtering those points
|
||||||
|
if "ReturnNumber" in error_msg and "NumberOfReturns" in error_msg:
|
||||||
|
print(f" → Tentative de filtrage des points ReturnNumber=0...")
|
||||||
|
|
||||||
|
# Use filters.range to keep only valid points (ReturnNumber >= 1)
|
||||||
|
filtered_pipeline = [
|
||||||
|
{
|
||||||
|
"type": "readers.las",
|
||||||
|
"filename": str(laz_file)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "filters.range",
|
||||||
|
"limits": "ReturnNumber[1:],NumberOfReturns[1:]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "filters.smrf",
|
||||||
|
"scalar": 1.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "filters.range",
|
||||||
|
"limits": "Classification[2:2]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "writers.las",
|
||||||
|
"filename": str(output_las),
|
||||||
|
"extra_dims": "all"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
filtered_json = json.dumps(filtered_pipeline)
|
||||||
|
with open(pipeline_file, 'w') as f:
|
||||||
|
f.write(filtered_json)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["pdal", "pipeline", str(pipeline_file)],
|
||||||
|
capture_output=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
print(f" ✓ Classification sol terminée (points filtrés)")
|
||||||
|
return output_las
|
||||||
|
except subprocess.CalledProcessError as e2:
|
||||||
|
print(f" ✗ Erreur même avec filtrage: {e2.stderr.decode()}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run_pdal_pipeline(self, pipeline_json, output_file):
|
||||||
|
"""Exécute un pipeline PDAL"""
|
||||||
|
pipeline_file = self.temp_dir / "temp_pipeline.json"
|
||||||
|
with open(pipeline_file, 'w') as f:
|
||||||
|
f.write(pipeline_json)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["pdal", "pipeline", str(pipeline_file)],
|
||||||
|
capture_output=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
print(f" ✓ Pipeline terminé")
|
||||||
|
return output_file
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print(f" ✗ Erreur PDAL: {e.stderr.decode()}")
|
print(f" ✗ Erreur PDAL: {e.stderr.decode()}")
|
||||||
return None
|
return None
|
||||||
@ -209,6 +228,9 @@ class LidarArchaeoPipeline:
|
|||||||
|
|
||||||
dtm = stat.statistic.T
|
dtm = stat.statistic.T
|
||||||
|
|
||||||
|
# Flip Y axis so north is at the top (Y decreases from top to bottom in image)
|
||||||
|
dtm = dtm[::-1, :]
|
||||||
|
|
||||||
# Fill gaps using interpolation
|
# Fill gaps using interpolation
|
||||||
print(f" Remplissage des zones vides...")
|
print(f" Remplissage des zones vides...")
|
||||||
from scipy.ndimage import distance_transform_edt
|
from scipy.ndimage import distance_transform_edt
|
||||||
|
|||||||
Reference in New Issue
Block a user