hoàn thành chức năng phân lô trên ảnh predict

This commit is contained in:
2026-02-18 16:22:18 +07:00
committed by Victor Phan
parent eacc6f9b96
commit ae4d8cbbc9
5 changed files with 785 additions and 2 deletions
+291 -1
View File
@@ -224,6 +224,9 @@ class PredictionConfig(BaseModel):
# Cloud removal strategy
cloud_removal_method: str = "classic"
cloud_removal_model: Optional[str] = None # Optional: .pth model filename for deep learning cloud removal
# Shapefile overlay for visualization
shapefile_overlay: Optional[str] = None # Path to shapefile for overlaying boundaries
class TrainingStatus(BaseModel):
@@ -286,6 +289,7 @@ class PredictionWithNDVIConfig(BaseModel):
export_classification: bool = True # Export classification raster
cloud_removal_method: str = "classic"
cloud_removal_model: Optional[str] = None # Optional: .pth model filename for deep learning cloud removal
shapefile_overlay: Optional[str] = None # Path to shapefile for overlaying boundaries
class CloudRemovalTrainingConfig(BaseModel):
@@ -1228,6 +1232,87 @@ async def list_training_files():
}
@app.get("/api/overlay/shapefiles")
async def list_overlay_shapefiles():
"""Liệt kê các shapefile có sẵn cho overlay trên prediction"""
overlay_dirs = ["region", "ChauThanh", "ThuanHoa"]
shapefiles = []
for overlay_dir in overlay_dirs:
dir_path = Path(overlay_dir)
if not dir_path.exists():
continue
# Find all .shp files in this directory and subdirectories
for shp_file in dir_path.rglob("*.shp"):
try:
file_size = shp_file.stat().st_size
file_modified = datetime.fromtimestamp(shp_file.stat().st_mtime).isoformat()
# Try to read shapefile to get feature count and bbox
import geopandas as gpd
gdf = gpd.read_file(str(shp_file))
feature_count = len(gdf)
# Calculate bbox (always in EPSG:4326 for consistency)
bbox = None
if not gdf.empty and gdf.crs:
try:
# Reproject to EPSG:4326 if needed
if gdf.crs != "EPSG:4326":
gdf_4326 = gdf.to_crs("EPSG:4326")
else:
gdf_4326 = gdf
# Get total bounds [minx, miny, maxx, maxy]
bounds = gdf_4326.total_bounds
if len(bounds) == 4:
bbox = [
float(bounds[0]), # min_lon
float(bounds[1]), # min_lat
float(bounds[2]), # max_lon
float(bounds[3]) # max_lat
]
except Exception as bbox_error:
print(f"[WARNING] Cannot calculate bbox for {shp_file}: {bbox_error}")
# Get relative path from workspace root
relative_path = str(shp_file)
shapefiles.append({
"filename": shp_file.name,
"path": relative_path,
"directory": overlay_dir,
"size_bytes": file_size,
"size_mb": round(file_size / 1024 / 1024, 2),
"modified": file_modified,
"feature_count": feature_count,
"crs": str(gdf.crs) if gdf.crs else "Unknown",
"bbox": bbox # [min_lon, min_lat, max_lon, max_lat] in EPSG:4326
})
except Exception as e:
# If cannot read shapefile, just add basic info
file_size = shp_file.stat().st_size
file_modified = datetime.fromtimestamp(shp_file.stat().st_mtime).isoformat()
relative_path = str(shp_file)
shapefiles.append({
"filename": shp_file.name,
"path": relative_path,
"directory": overlay_dir,
"size_bytes": file_size,
"size_mb": round(file_size / 1024 / 1024, 2),
"modified": file_modified,
"error": f"Cannot read shapefile: {str(e)}"
})
return {
"shapefiles": shapefiles,
"count": len(shapefiles),
"directories": overlay_dirs
}
@app.get("/api/training/shapefile/{filename}/labels")
async def get_shapefile_labels(filename: str):
"""Lấy các label từ một shapefile cụ thể"""
@@ -1729,6 +1814,85 @@ def update_progress(message: str):
print(f"[PROGRESS] {message}")
def rasterize_shapefile_overlay(shapefile_path, reference_raster, boundary_value=255):
"""
Rasterize shapefile boundaries to overlay on prediction result.
Args:
shapefile_path: Path to shapefile
reference_raster: xarray DataArray to match dimensions and CRS
boundary_value: Value to use for boundaries (default 255 for white)
Returns:
numpy array with boundaries, same shape as reference_raster
"""
try:
import geopandas as gpd
from rasterio.features import rasterize
import numpy as np
# Read shapefile
gdf = gpd.read_file(shapefile_path)
print(f"[OVERLAY] Loaded shapefile with {len(gdf)} features, CRS: {gdf.crs}")
# Ensure CRS matches
target_crs = reference_raster.rio.crs
if gdf.crs != target_crs:
print(f"[OVERLAY] Reprojecting from {gdf.crs} to {target_crs}")
gdf = gdf.to_crs(target_crs)
# Get raster dimensions and transform
height, width = reference_raster.shape
transform = reference_raster.rio.transform()
print(f"[OVERLAY] Raster dimensions: {height}x{width}")
print(f"[OVERLAY] Transform: {transform}")
# Calculate appropriate buffer size based on pixel resolution
# Get pixel size from transform (transform[0] is x resolution)
pixel_size = abs(transform[0]) # in CRS units
# Very thin boundary - only 0.2 pixels wide for 1px line
buffer_distance = pixel_size * 0.2
print(f"[OVERLAY] Pixel size: {pixel_size}, Buffer distance: {buffer_distance} (thin 1px line)")
# Create boundary geometries with minimal buffering
boundary_geoms = []
for idx, geom in enumerate(gdf.geometry):
if geom is not None and geom.is_valid:
# Get boundary of each polygon
boundary = geom.boundary
if boundary is not None:
# Minimal buffer for 1-pixel thin line
buffered = boundary.buffer(buffer_distance)
boundary_geoms.append((buffered, boundary_value))
if not boundary_geoms:
print(f"[WARNING] No valid boundary geometries found in {shapefile_path}")
return np.zeros((height, width), dtype=np.uint8)
print(f"[OVERLAY] Rasterizing {len(boundary_geoms)} boundaries...")
# Rasterize boundaries
boundary_mask = rasterize(
shapes=boundary_geoms,
out_shape=(height, width),
transform=transform,
fill=0, # Background
dtype=np.uint8
)
boundary_count = np.count_nonzero(boundary_mask)
print(f"[OVERLAY] Boundary pixels: {boundary_count} / {height*width} ({boundary_count/(height*width)*100:.2f}%)")
if boundary_count == 0:
print(f"[OVERLAY WARNING] No boundary pixels were rasterized! Check CRS and geometry overlap.")
return boundary_mask
except Exception as e:
print(f"[ERROR] Failed to rasterize shapefile {shapefile_path}: {e}")
return None
def update_prediction_progress(message: str):
"""Cập nhật prediction progress message"""
global prediction_status
@@ -2083,6 +2247,33 @@ async def run_prediction(config: PredictionConfig):
prediction_da.rio.to_raster(str(output_file), driver="GTiff")
# ============ SHAPEFILE OVERLAY ============
overlay_mask = None
if config.shapefile_overlay:
prediction_status["progress"] = f"Đang overlay shapefile: {config.shapefile_overlay}..."
print(f"[OVERLAY] Shapefile overlay requested: {config.shapefile_overlay}")
# Validate shapefile path exists
shapefile_path = Path(config.shapefile_overlay)
if not shapefile_path.exists():
print(f"[OVERLAY ERROR] Shapefile not found: {shapefile_path}")
print(f"[OVERLAY ERROR] Absolute path: {shapefile_path.absolute()}")
print(f"[OVERLAY ERROR] Current working directory: {Path.cwd()}")
else:
print(f"[OVERLAY] Shapefile exists: {shapefile_path.absolute()}")
try:
overlay_mask = rasterize_shapefile_overlay(str(shapefile_path), prediction_da)
if overlay_mask is not None and np.count_nonzero(overlay_mask) > 0:
print(f"[OVERLAY] Successfully rasterized shapefile boundaries ({np.count_nonzero(overlay_mask)} pixels)")
else:
print(f"[OVERLAY WARNING] Shapefile rasterized but no boundary pixels found")
except Exception as overlay_error:
print(f"[OVERLAY ERROR] Exception: {overlay_error}")
import traceback
traceback.print_exc()
else:
print(f"[OVERLAY] No shapefile overlay requested")
# Generate PNG preview for web display
prediction_status["progress"] = "Đang tạo PNG preview..."
png_file = output_dir / f"prediction_{timestamp}.png"
@@ -2097,6 +2288,42 @@ async def run_prediction(config: PredictionConfig):
# Plot prediction with colormap
im = ax.imshow(predictions_2d, cmap='tab20', interpolation='nearest')
# Overlay shapefile boundaries if available
if overlay_mask is not None and np.count_nonzero(overlay_mask) > 0:
print(f"[PNG OVERLAY] Overlaying {np.count_nonzero(overlay_mask)} boundary pixels")
# Create a mask for boundaries (where overlay_mask > 0)
boundary_mask = overlay_mask > 0
# Method: Direct overlay with high-contrast colors
# Create RGBA overlay image
overlay_rgba = np.zeros((*predictions_2d.shape, 4))
overlay_rgba[boundary_mask, 0] = 1.0 # Red = 1.0 (white)
overlay_rgba[boundary_mask, 1] = 1.0 # Green = 1.0 (white)
overlay_rgba[boundary_mask, 2] = 1.0 # Blue = 1.0 (white)
overlay_rgba[boundary_mask, 3] = 1.0 # Alpha = 1.0 (fully opaque)
# Overlay on top of prediction
ax.imshow(overlay_rgba, interpolation='nearest')
# Also add a black outline for better contrast
from scipy import ndimage
boundary_dilated = ndimage.binary_dilation(boundary_mask, iterations=1)
boundary_outline = boundary_dilated & ~boundary_mask
outline_rgba = np.zeros((*predictions_2d.shape, 4))
outline_rgba[boundary_outline, 0] = 0.0 # Black outline
outline_rgba[boundary_outline, 1] = 0.0
outline_rgba[boundary_outline, 2] = 0.0
outline_rgba[boundary_outline, 3] = 0.8
ax.imshow(outline_rgba, interpolation='nearest')
print(f"[PNG OVERLAY] Added shapefile boundaries to visualization (direct overlay method)")
else:
print(f"[PNG OVERLAY] No overlay mask or empty mask (pixels: {np.count_nonzero(overlay_mask) if overlay_mask is not None else 0})")
ax.set_title(f'Land Classification - {timestamp}', fontsize=16, fontweight='bold', pad=20)
ax.set_xlabel('X (pixels)', fontsize=11)
ax.set_ylabel('Y (pixels)', fontsize=11)
@@ -2176,7 +2403,9 @@ async def run_prediction(config: PredictionConfig):
"n_features": features.shape[1],
"feature_mode": feature_mode,
"used_radar": use_radar,
"model_used": config.model_filename
"model_used": config.model_filename,
"shapefile_overlay": config.shapefile_overlay,
"overlay_applied": overlay_mask is not None
}
# Auto generate prediction report
@@ -4355,8 +4584,69 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
# ============ SHAPEFILE OVERLAY ============
overlay_mask = None
if config.shapefile_overlay:
print(f"[OVERLAY] Shapefile overlay requested: {config.shapefile_overlay}")
# Validate shapefile path exists
shapefile_path = Path(config.shapefile_overlay)
if not shapefile_path.exists():
print(f"[OVERLAY ERROR] Shapefile not found: {shapefile_path}")
print(f"[OVERLAY ERROR] Absolute path: {shapefile_path.absolute()}")
else:
print(f"[OVERLAY] Shapefile exists: {shapefile_path.absolute()}")
try:
# Import rioxarray for rio accessor
import rioxarray
# Create temporary DataArray for rasterization
temp_da = xr.DataArray(
prediction_raster,
coords={
"y": np.linspace(bbox[3], bbox[1], height),
"x": np.linspace(bbox[0], bbox[2], width)
},
dims=["y", "x"]
)
temp_da.rio.write_crs("EPSG:4326", inplace=True)
temp_da.rio.write_transform(transform, inplace=True)
overlay_mask = rasterize_shapefile_overlay(str(shapefile_path), temp_da)
if overlay_mask is not None and np.count_nonzero(overlay_mask) > 0:
print(f"[OVERLAY] Successfully rasterized shapefile boundaries ({np.count_nonzero(overlay_mask)} pixels)")
else:
print(f"[OVERLAY WARNING] Shapefile rasterized but no boundary pixels found")
except Exception as overlay_error:
print(f"[OVERLAY ERROR] Exception: {overlay_error}")
import traceback
traceback.print_exc()
else:
print(f"[OVERLAY] No shapefile overlay requested")
fig, ax = plt.subplots(figsize=(14, 10), dpi=150)
im = ax.imshow(prediction_raster, cmap='tab20', interpolation='nearest')
# Overlay shapefile boundaries if available
if overlay_mask is not None and np.count_nonzero(overlay_mask) > 0:
print(f"[PNG OVERLAY] Overlaying {np.count_nonzero(overlay_mask)} boundary pixels (1px thin line)")
# Create a mask for boundaries
boundary_mask = overlay_mask > 0
# Create RGBA overlay image - thin 1px white line only
overlay_rgba = np.zeros((*prediction_raster.shape, 4))
overlay_rgba[boundary_mask, 0] = 1.0 # White (R=1)
overlay_rgba[boundary_mask, 1] = 1.0 # White (G=1)
overlay_rgba[boundary_mask, 2] = 1.0 # White (B=1)
overlay_rgba[boundary_mask, 3] = 1.0 # Fully opaque
ax.imshow(overlay_rgba, interpolation='nearest')
print(f"[PNG OVERLAY] Added thin 1px shapefile boundaries to visualization")
else:
print(f"[PNG OVERLAY] No overlay mask or empty mask")
ax.set_title(f'Land Classification - {timestamp}', fontsize=16, fontweight='bold', pad=20)
ax.set_xlabel('X (pixels)', fontsize=11)
ax.set_ylabel('Y (pixels)', fontsize=11)