Pipeline LiDAR optimisé: suppression classification sémantique, ajout flèche nord vectorielle
- Suppression module classification sémantique (non fonctionnel) - Suppression section rapports (vue synthétique) - Ajout flèche du nord vectorielle noire (coin supérieur droit, au-dessus légende) - Pipeline simplifié à 3 étapes: classification sol, génération DTM, visualisations - Prétraitement ReturnNumber pour fichiers LAZ corrompus - Orientation nord garantie sur toutes les cartes
This commit is contained in:
@ -30,8 +30,7 @@ RUN pip3 install --no-cache-dir \
|
|||||||
|
|
||||||
# Copy scripts
|
# Copy scripts
|
||||||
COPY process_lidar.py /usr/local/bin/
|
COPY process_lidar.py /usr/local/bin/
|
||||||
COPY semantic_classifier.py /usr/local/bin/
|
RUN chmod +x /usr/local/bin/process_lidar.py
|
||||||
RUN chmod +x /usr/local/bin/process_lidar.py /usr/local/bin/semantic_classifier.py
|
|
||||||
|
|
||||||
# Create directories with correct permissions
|
# Create directories with correct permissions
|
||||||
RUN mkdir -p /data/output /data/input && \
|
RUN mkdir -p /data/output /data/input && \
|
||||||
|
|||||||
200
process_lidar.py
200
process_lidar.py
@ -48,9 +48,8 @@ class LidarArchaeoPipeline:
|
|||||||
|
|
||||||
self.dtm_dir = self.output_dir / "DTM"
|
self.dtm_dir = self.output_dir / "DTM"
|
||||||
self.vis_dir = self.output_dir / "visualisations"
|
self.vis_dir = self.output_dir / "visualisations"
|
||||||
self.report_dir = self.output_dir / "rapports"
|
|
||||||
|
|
||||||
for d in [self.dtm_dir, self.vis_dir, self.report_dir]:
|
for d in [self.dtm_dir, self.vis_dir]:
|
||||||
d.mkdir(exist_ok=True)
|
d.mkdir(exist_ok=True)
|
||||||
|
|
||||||
print(f"✓ Pipeline initialisé (Python pur)")
|
print(f"✓ Pipeline initialisé (Python pur)")
|
||||||
@ -110,7 +109,53 @@ class LidarArchaeoPipeline:
|
|||||||
print(f" ✓ Classification déjà effectuée")
|
print(f" ✓ Classification déjà effectuée")
|
||||||
return output_las
|
return output_las
|
||||||
|
|
||||||
pipeline_json = self.create_pipeline_json(laz_file, output_las)
|
# First, try to fix ReturnNumber values if needed
|
||||||
|
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:
|
||||||
@ -670,8 +715,8 @@ class LidarArchaeoPipeline:
|
|||||||
# Create figure with white background for JPEG
|
# Create figure with white background for JPEG
|
||||||
fig, ax = plt.subplots(figsize=(18, 12), facecolor='white')
|
fig, ax = plt.subplots(figsize=(18, 12), facecolor='white')
|
||||||
|
|
||||||
# Display data
|
# Display data with north at the top
|
||||||
im = ax.imshow(data, cmap=cmap, aspect='equal')
|
im = ax.imshow(data, cmap=cmap, aspect='equal', origin='upper')
|
||||||
|
|
||||||
# Enhanced colorbar with explicit values
|
# Enhanced colorbar with explicit values
|
||||||
cbar = plt.colorbar(im, ax=ax, pad=0.03, shrink=0.75, aspect=30)
|
cbar = plt.colorbar(im, ax=ax, pad=0.03, shrink=0.75, aspect=30)
|
||||||
@ -691,11 +736,42 @@ class LidarArchaeoPipeline:
|
|||||||
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
|
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
|
||||||
|
|
||||||
ax.axis('off')
|
ax.axis('off')
|
||||||
|
|
||||||
|
# Add vector north arrow above the colorbar on the right side (black lines)
|
||||||
|
from matplotlib.patches import FancyArrow, Polygon
|
||||||
|
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
|
||||||
|
|
||||||
|
# Create inset axes positioned above the colorbar on the right
|
||||||
|
# The colorbar is typically at the right, so we place north arrow above it
|
||||||
|
north_ax = inset_axes(ax, width="5%", height="8%", loc='upper right',
|
||||||
|
bbox_to_anchor=(-0.02, 0.15, 1, 1), bbox_transform=ax.transAxes)
|
||||||
|
|
||||||
|
north_ax.set_xlim(0, 1)
|
||||||
|
north_ax.set_ylim(0, 1)
|
||||||
|
north_ax.axis('off')
|
||||||
|
|
||||||
|
# Draw vector arrow pointing north (black lines)
|
||||||
|
# Main arrow shaft
|
||||||
|
north_ax.plot([0.5, 0.5], [0.15, 0.65], color='black', linewidth=2.5, zorder=10)
|
||||||
|
# Arrow head (triangle outline)
|
||||||
|
arrow_head_outline = [[0.5, 0.25], [0.35, 0.45], [0.5, 0.65], [0.65, 0.45]]
|
||||||
|
for i in range(len(arrow_head_outline) - 1):
|
||||||
|
north_ax.plot([arrow_head_outline[i][0], arrow_head_outline[i+1][0]],
|
||||||
|
[arrow_head_outline[i][1], arrow_head_outline[i+1][1]],
|
||||||
|
color='black', linewidth=2.5, zorder=10)
|
||||||
|
# Fill the arrow head
|
||||||
|
north_ax.add_patch(Polygon([[0.5, 0.25], [0.35, 0.45], [0.5, 0.65], [0.65, 0.45]],
|
||||||
|
closed=True, facecolor='black', edgecolor='black', zorder=9))
|
||||||
|
|
||||||
|
# Add "N" label above the arrow
|
||||||
|
north_ax.text(0.5, 0.92, 'N', ha='center', va='top',
|
||||||
|
fontsize=14, fontweight='bold', color='black', zorder=11)
|
||||||
|
|
||||||
fig.patch.set_facecolor('white')
|
fig.patch.set_facecolor('white')
|
||||||
|
|
||||||
plt.tight_layout()
|
plt.tight_layout()
|
||||||
# Save as JPEG
|
# Save as JPEG
|
||||||
plt.savefig(jpg_file, dpi=150, bbox_inches='tight', facecolor='white', format='jpg')
|
plt.savefig(jpg_file, dpi=150, bbox_inches='tight', pad_inches=0.1, facecolor='white', format='jpg')
|
||||||
plt.close()
|
plt.close()
|
||||||
|
|
||||||
# Delete the source TIFF file to save space
|
# Delete the source TIFF file to save space
|
||||||
@ -735,94 +811,8 @@ class LidarArchaeoPipeline:
|
|||||||
|
|
||||||
return vis_results
|
return vis_results
|
||||||
|
|
||||||
def create_overview(self, basename):
|
|
||||||
"""Create overview image with all visualizations (JPEG)"""
|
|
||||||
patterns = {
|
|
||||||
'Hillshade': '*_hillshade_multi.jpg',
|
|
||||||
'Pente': '*_slope.jpg',
|
|
||||||
'Aspect': '*_aspect.jpg',
|
|
||||||
'Courbure': '*_curvature.jpg',
|
|
||||||
'Éclairage': '*_solar.jpg',
|
|
||||||
'Sky-View Factor': '*_svf.jpg',
|
|
||||||
'Local Relief': '*_lrm.jpg',
|
|
||||||
'Pos. Openness': '*_positive_openness.jpg',
|
|
||||||
'Neg. Openness': '*_negative_openness.jpg'
|
|
||||||
}
|
|
||||||
|
|
||||||
images = {}
|
|
||||||
for name, pattern in patterns.items():
|
|
||||||
files = list(self.vis_dir.glob(pattern))
|
|
||||||
if files:
|
|
||||||
images[name] = str(files[0])
|
|
||||||
|
|
||||||
if len(images) < 2:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 3x3 grid for 9 visualizations
|
|
||||||
fig, axes = plt.subplots(3, 3, figsize=(24, 18), facecolor='white')
|
|
||||||
axes = axes.flatten()
|
|
||||||
|
|
||||||
for idx, (name, img_path) in enumerate(images.items()):
|
|
||||||
if idx >= 9:
|
|
||||||
break
|
|
||||||
img = plt.imread(img_path)
|
|
||||||
axes[idx].imshow(img)
|
|
||||||
axes[idx].set_title(name, fontsize=11, fontweight='bold')
|
|
||||||
axes[idx].axis('off')
|
|
||||||
|
|
||||||
# Hide unused subplots
|
|
||||||
for idx in range(len(images), 9):
|
|
||||||
axes[idx].axis('off')
|
|
||||||
|
|
||||||
# Title
|
|
||||||
fig.suptitle(
|
|
||||||
f"Analyse Archéologique LiDAR - {basename}",
|
|
||||||
fontsize=18,
|
|
||||||
fontweight='bold',
|
|
||||||
y=0.98
|
|
||||||
)
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
output = self.report_dir / f"{basename}_overview.jpg"
|
|
||||||
plt.savefig(output, dpi=120, bbox_inches='tight', facecolor='white', format='jpg')
|
|
||||||
plt.close()
|
|
||||||
|
|
||||||
return output
|
|
||||||
|
|
||||||
# ============ Complete Pipeline ============
|
# ============ Complete Pipeline ============
|
||||||
|
|
||||||
def run_semantic_classification(self, dtm_file, basename):
|
|
||||||
"""Run semantic classification on DTM"""
|
|
||||||
print(f"\n[4/4] Classification Sémantique Automatique...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Import semantic classifier
|
|
||||||
from semantic_classifier import ArchaeoSemanticClassifier
|
|
||||||
|
|
||||||
# Create output subdirectory for semantic results
|
|
||||||
semantic_dir = self.output_dir / "semantic"
|
|
||||||
semantic_dir.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
# Run classification
|
|
||||||
classifier = ArchaeoSemanticClassifier(dtm_file, semantic_dir)
|
|
||||||
results = classifier.process(basename)
|
|
||||||
|
|
||||||
print(f" ✓ Classification sémantique terminée")
|
|
||||||
print(f" → Carte: {results['tif'].name}")
|
|
||||||
print(f" → Visualisation: {results['jpg'].name}")
|
|
||||||
stats_file = Path(results['tif']).parent / f"{Path(results['tif']).stem.replace('_semantic', '')}_statistics.json"
|
|
||||||
print(f" → Statistiques: {stats_file.name}")
|
|
||||||
|
|
||||||
return results
|
|
||||||
except ImportError as e:
|
|
||||||
print(f" ✗ Module non disponible: {e}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Erreur classification: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return None
|
|
||||||
|
|
||||||
def process_file(self, laz_file):
|
def process_file(self, laz_file):
|
||||||
"""Process a single LAZ file"""
|
"""Process a single LAZ file"""
|
||||||
basename = laz_file.stem
|
basename = laz_file.stem
|
||||||
@ -831,42 +821,23 @@ class LidarArchaeoPipeline:
|
|||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
|
|
||||||
# Step 1: Ground classification
|
# Step 1: Ground classification
|
||||||
print(f"\n[1/4] Classification du sol...")
|
print(f"\n[1/3] Classification du sol...")
|
||||||
las_file = self.classify_ground(laz_file)
|
las_file = self.classify_ground(laz_file)
|
||||||
if not las_file:
|
if not las_file:
|
||||||
print(f" ✗ Échec classification")
|
print(f" ✗ Échec classification")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Step 2: Generate DTM
|
# Step 2: Generate DTM
|
||||||
print(f"\n[2/4] Génération DTM...")
|
print(f"\n[2/3] Génération DTM...")
|
||||||
dtm_file = self.create_dtm_fast(las_file, basename)
|
dtm_file = self.create_dtm_fast(las_file, basename)
|
||||||
if not dtm_file:
|
if not dtm_file:
|
||||||
print(f" ✗ Échec DTM")
|
print(f" ✗ Échec DTM")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Step 3: Visualizations
|
# Step 3: Visualizations
|
||||||
print(f"\n[3/4] Visualisations archéologiques...")
|
print(f"\n[3/3] Visualisations archéologiques...")
|
||||||
vis = self.generate_all_visualizations(dtm_file, basename)
|
vis = self.generate_all_visualizations(dtm_file, basename)
|
||||||
|
|
||||||
# Overview
|
|
||||||
overview = self.create_overview(basename)
|
|
||||||
if overview:
|
|
||||||
print(f"\n ✓ Vue synthétique: {overview}")
|
|
||||||
|
|
||||||
# Step 4: Semantic Classification (NEW!)
|
|
||||||
semantic_results = self.run_semantic_classification(dtm_file, basename)
|
|
||||||
|
|
||||||
print(f"\n✓ {basename} traité avec succès !")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Overview
|
|
||||||
overview = self.create_overview(basename)
|
|
||||||
if overview:
|
|
||||||
print(f"\n ✓ Vue synthétique: {overview}")
|
|
||||||
|
|
||||||
# Step 4: Semantic Classification (NEW!)
|
|
||||||
semantic_results = self.run_semantic_classification(dtm_file, basename)
|
|
||||||
|
|
||||||
print(f"\n✓ {basename} traité avec succès !")
|
print(f"\n✓ {basename} traité avec succès !")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -908,7 +879,6 @@ class LidarArchaeoPipeline:
|
|||||||
print(f"\nRésultats dans: {self.output_dir}")
|
print(f"\nRésultats dans: {self.output_dir}")
|
||||||
print(f" • DTM: {self.dtm_dir}")
|
print(f" • DTM: {self.dtm_dir}")
|
||||||
print(f" • Visualisations JPEG: {self.vis_dir}")
|
print(f" • Visualisations JPEG: {self.vis_dir}")
|
||||||
print(f" • Rapports: {self.report_dir}")
|
|
||||||
|
|
||||||
# Clean up temporary files to save space
|
# Clean up temporary files to save space
|
||||||
print(f"\nNettoyage des fichiers temporaires...")
|
print(f"\nNettoyage des fichiers temporaires...")
|
||||||
|
|||||||
Reference in New Issue
Block a user