Pipeline LiDAR complet: 9 visualisations + classification sémantique automatique
- Correction bug geojson dans process_lidar.py - Semantic classifier fonctionnel avec K-Means - 9 visualisations JPEG selon état de l'art 2024-2025 - Statistiques de classification sémantique exportées en JSON - Nettoyage automatique des fichiers temporaires Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
293
semantic_classifier.py
Normal file
293
semantic_classifier.py
Normal file
@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Module de Classification Sémantique Simplifié pour LiDAR Archéologique
|
||||
Approche robuste avec K-Means pour classification automatique
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import rasterio
|
||||
from rasterio.transform import from_bounds
|
||||
from sklearn.cluster import KMeans
|
||||
from scipy import ndimage
|
||||
import json
|
||||
from pathlib import Path
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.colors as mcolors
|
||||
|
||||
|
||||
class ArchaeoSemanticClassifier:
|
||||
"""Classification sémantique automatique robuste"""
|
||||
|
||||
def __init__(self, dtm_file, output_dir):
|
||||
self.dtm_file = Path(dtm_file)
|
||||
self.output_dir = Path(output_dir)
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Load DTM
|
||||
with rasterio.open(self.dtm_file) as src:
|
||||
self.dem = src.read(1)
|
||||
self.transform = src.transform
|
||||
self.crs = src.crs
|
||||
self.height, self.width = self.dem.shape
|
||||
|
||||
print(f"✓ Classifieur initialisé (DTM: {self.width}x{self.height})")
|
||||
|
||||
def extract_features_simple(self):
|
||||
"""Extraction simplifiée des caractéristiques"""
|
||||
print(" → Extraction caractéristiques...")
|
||||
|
||||
# Calculate gradients
|
||||
dy, dx = np.gradient(self.dem)
|
||||
|
||||
# Slope
|
||||
slope = np.arctan(np.sqrt(dx**2 + dy**2)) * 180 / np.pi
|
||||
|
||||
# Aspect
|
||||
aspect = np.arctan2(-dy, dx) * 180 / np.pi
|
||||
aspect = np.mod(aspect, 360)
|
||||
|
||||
# Curvature
|
||||
dz_dx = np.gradient(dx, axis=1)
|
||||
dz_dy = np.gradient(dy, axis=0)
|
||||
curvature = (dz_dx + dz_dy) / 2
|
||||
|
||||
# Local Relief
|
||||
from scipy.ndimage import uniform_filter
|
||||
local_mean = uniform_filter(self.dem, size=int(15/0.5))
|
||||
local_relief = self.dem - local_mean
|
||||
|
||||
return {
|
||||
'elevation': self.dem,
|
||||
'slope': slope,
|
||||
'aspect': aspect,
|
||||
'curvature': curvature,
|
||||
'local_relief': local_relief
|
||||
}
|
||||
|
||||
def classify_kmeans(self, n_clusters=6):
|
||||
"""Classification K-Means robuste"""
|
||||
print(" → Classification K-Means...")
|
||||
|
||||
features = self.extract_features_simple()
|
||||
|
||||
# Normalize each feature to 0-1
|
||||
normalized_features = {}
|
||||
for name, data in features.items():
|
||||
min_val = np.percentile(data, 2)
|
||||
max_val = np.percentile(data, 98)
|
||||
normalized = np.clip((data - min_val) / (max_val - min_val + 1e-6), 0, 1)
|
||||
normalized_features[name] = normalized
|
||||
|
||||
# Stack features for clustering
|
||||
feature_stack = np.stack([
|
||||
normalized_features['elevation'].flatten(),
|
||||
normalized_features['slope'].flatten(),
|
||||
normalized_features['curvature'].flatten(),
|
||||
normalized_features['local_relief'].flatten()
|
||||
], axis=1)
|
||||
|
||||
# Remove NaN values
|
||||
valid_mask = ~np.isnan(feature_stack).any(axis=1)
|
||||
feature_stack = feature_stack[valid_mask]
|
||||
|
||||
# K-Means clustering with random_state for reproducibility
|
||||
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10, max_iter=300)
|
||||
labels_flat = kmeans.fit_predict(feature_stack)
|
||||
|
||||
# Create full resolution labels
|
||||
full_labels = np.zeros(self.height * self.width, dtype=int)
|
||||
full_indices = np.where(valid_mask)[0]
|
||||
full_labels[full_indices] = labels_flat + 1 # +1 to shift from 0-based
|
||||
full_labels = full_labels.reshape(self.height, self.width)
|
||||
|
||||
# Interpret clusters
|
||||
self._interpret_clusters(kmeans.cluster_centers_, features)
|
||||
|
||||
return full_labels
|
||||
|
||||
def _interpret_clusters(self, centers, features):
|
||||
"""Interprète les clusters selon les centroïdes"""
|
||||
interpretations = {}
|
||||
|
||||
# Centers are in normalized feature space
|
||||
# Features order: elevation, slope, curvature, local_relief
|
||||
for i, center in enumerate(centers):
|
||||
elev = center[0]
|
||||
slope = center[1]
|
||||
curve = center[2]
|
||||
relief = center[3]
|
||||
|
||||
# Interpret based on feature values
|
||||
if relief > 0.7:
|
||||
category = "ÉLEVÉE (Tumulus possible)"
|
||||
elif relief < -0.3:
|
||||
category = "ENFONCÉ (Fossé, cavité)"
|
||||
elif abs(curve) > 0.5:
|
||||
if curve > 0:
|
||||
category = "CONVEX (Bosse, monticule)"
|
||||
else:
|
||||
category = "CONCAVE (Creux, dépression)"
|
||||
elif slope > 0.6:
|
||||
category = "PENTE FORTE (Talus, mur)"
|
||||
elif slope < 0.2 and elev < 0.3:
|
||||
category = "PLAT (Zone plane)"
|
||||
else:
|
||||
category = "TOPOGRAPHIE MIXTE"
|
||||
|
||||
interpretations[i + 1] = category # +1 because labels are 1-indexed
|
||||
|
||||
print(f" Interprétation des {len(centers)} clusters :")
|
||||
for i, interp in interpretations.items():
|
||||
print(f" Classe {i}: {interp}")
|
||||
|
||||
def generate_semantic_map(self, labels):
|
||||
"""Génère une carte sémantique colorée"""
|
||||
print(" → Génération carte sémantique...")
|
||||
|
||||
# Create color map for semantic classes
|
||||
# 0: Background, 1-6: Different semantic classes
|
||||
colors = {
|
||||
0: [200, 200, 200], # Gray: Background/Unknown
|
||||
1: [139, 69, 19], # Brown: Linear/Walls
|
||||
2: [128, 0, 128], # Purple: Circular/Mounds
|
||||
3: [255, 140, 0], # Orange: Elevated
|
||||
4: [0, 200, 255], # Cyan: Depressed
|
||||
5: [220, 220, 0], # Yellow: Slope
|
||||
6: [0, 128, 0] # Green: Vegetation/Natural
|
||||
}
|
||||
|
||||
# Create RGB image
|
||||
rgb = np.zeros((self.height, self.width, 3), dtype=np.uint8)
|
||||
|
||||
for class_id, color in colors.items():
|
||||
mask = labels == class_id
|
||||
for c in range(3):
|
||||
rgb[:, :, c][mask] = color[c]
|
||||
|
||||
return rgb
|
||||
|
||||
def process(self, basename):
|
||||
"""Pipeline complet de classification sémantique"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" CLASSIFICATION SÉMANTIQUE - {basename}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Run K-Means classification
|
||||
labels = self.classify_kmeans(n_clusters=6)
|
||||
|
||||
# Generate semantic map
|
||||
rgb = self.generate_semantic_map(labels)
|
||||
|
||||
# Save semantic classification
|
||||
output_tif = self.output_dir / f"{basename}_semantic.tif"
|
||||
with rasterio.open(
|
||||
output_tif,
|
||||
'w',
|
||||
driver='GTiff',
|
||||
height=self.height,
|
||||
width=self.width,
|
||||
count=1,
|
||||
dtype='uint8',
|
||||
crs=self.crs,
|
||||
transform=self.transform,
|
||||
compress='lzw'
|
||||
) as dst:
|
||||
dst.write(labels, 1)
|
||||
|
||||
# Save visualization
|
||||
output_jpg = self.output_dir / f"{basename}_semantic.jpg"
|
||||
plt.figure(figsize=(16, 12), facecolor='white')
|
||||
plt.imshow(rgb)
|
||||
|
||||
# Create legend
|
||||
legend_elements = [
|
||||
plt.Rectangle((0, 0), 1, 1, facecolor=np.array(c)/255, edgecolor='black', label=label)
|
||||
for label, c in [
|
||||
("Inconnu/Fond", [200, 200, 200]),
|
||||
("Linéaire (murs)", [139, 69, 19]),
|
||||
("Circulaire (tumulus)", [128, 0, 128]),
|
||||
("Élevé (monticules)", [255, 140, 0]),
|
||||
("Enfoncé (fossés)", [0, 200, 255]),
|
||||
("Pente forte (talus)", [220, 220, 0]),
|
||||
("Naturel", [0, 128, 0])
|
||||
]
|
||||
]
|
||||
|
||||
plt.legend(handles=legend_elements, loc='upper right', fontsize=11)
|
||||
plt.title(f"Classification Sémantique LiDAR - {basename}\n",
|
||||
fontsize=14, fontweight='bold', pad=15)
|
||||
plt.axis('off')
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_jpg, dpi=150, bbox_inches='tight', format='jpg')
|
||||
plt.close()
|
||||
|
||||
# Generate statistics
|
||||
stats = self._generate_stats(labels, basename)
|
||||
|
||||
print(f"\n✓ Classification terminée !")
|
||||
print(f" • Carte sémantique: {output_tif.name}")
|
||||
print(f" • Visualisation: {output_jpg.name}")
|
||||
print(f" • Statistiques: {self.output_dir / f'{basename}_statistics.json'}")
|
||||
|
||||
return {
|
||||
'labels': labels,
|
||||
'tif': output_tif,
|
||||
'jpg': output_jpg,
|
||||
'stats': stats
|
||||
}
|
||||
|
||||
def _generate_stats(self, labels, basename):
|
||||
"""Génère les statistiques de classification"""
|
||||
print(" → Génération statistiques...")
|
||||
|
||||
total_pixels = labels.size
|
||||
stats = {}
|
||||
class_names = {
|
||||
0: "Inconnu/Fond",
|
||||
1: "Linéaire",
|
||||
2: "Circulaire",
|
||||
3: "Élevée",
|
||||
4: "Enfoncée",
|
||||
5: "Pente forte",
|
||||
6: "Naturel"
|
||||
}
|
||||
|
||||
for class_id in range(7):
|
||||
count = np.sum(labels == class_id)
|
||||
percentage = (count / total_pixels) * 100
|
||||
if count > 0:
|
||||
stats[class_id] = {
|
||||
'name': class_names[class_id],
|
||||
'count': int(count),
|
||||
'percentage': float(percentage)
|
||||
}
|
||||
|
||||
# Save as JSON
|
||||
stats_file = self.output_dir / f"{basename}_statistics.json"
|
||||
with open(stats_file, 'w') as f:
|
||||
json.dump(stats, f, indent=2, default=lambda x: float(x) if isinstance(x, (np.floating, np.integer)) else x)
|
||||
|
||||
# Print summary
|
||||
print(f"\n 📊 Statistiques :")
|
||||
for class_id, info in stats.items():
|
||||
print(f" {info['name']}: {info['count']:.0f} px ({info['percentage']:.1f}%)")
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python semantic_classifier.py <dtm_file.tif> [output_dir]")
|
||||
sys.exit(1)
|
||||
|
||||
dtm_file = sys.argv[1]
|
||||
output_dir = sys.argv[2] if len(sys.argv) > 2 else "semantic_output"
|
||||
|
||||
classifier = ArchaeoSemanticClassifier(dtm_file, output_dir)
|
||||
classifier.process(Path(dtm_file).stem)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user