hoàn thành chức năng phân lô trên ảnh predict
This commit is contained in:
+291
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user