|
|
|
|
|
""" |
|
|
NWSD API - Simple Python API for water surface detection |
|
|
This module provides a simple interface for water surface segmentation. |
|
|
""" |
|
|
|
|
|
import os |
|
|
import cv2 |
|
|
import numpy as np |
|
|
from typing import Optional, Tuple, Dict, Union |
|
|
from pathlib import Path |
|
|
from ultralytics import YOLO |
|
|
|
|
|
|
|
|
class WaterSurfaceDetector: |
|
|
"""Water Surface Detection API using YOLOv11n.""" |
|
|
|
|
|
def __init__(self, weights_path: str = "model/nwsd-v2.pt", device: str = "cpu"): |
|
|
""" |
|
|
Initialize the water surface detector. |
|
|
|
|
|
Args: |
|
|
weights_path: Path to model weights |
|
|
device: Device to use for inference (cpu, cuda, mps) |
|
|
""" |
|
|
self.weights_path = weights_path |
|
|
self.device = device |
|
|
self.model = None |
|
|
self._load_model() |
|
|
|
|
|
def _load_model(self): |
|
|
"""Load the YOLO model.""" |
|
|
if not os.path.exists(self.weights_path): |
|
|
raise FileNotFoundError(f"Model weights not found: {self.weights_path}") |
|
|
|
|
|
self.model = YOLO(self.weights_path) |
|
|
self.model.to(self.device) |
|
|
|
|
|
def detect(self, |
|
|
image: Union[str, np.ndarray], |
|
|
conf: float = 0.25, |
|
|
iou: float = 0.45) -> Dict: |
|
|
""" |
|
|
Detect water surfaces in an image. |
|
|
|
|
|
Args: |
|
|
image: Path to image file or numpy array |
|
|
conf: Confidence threshold |
|
|
iou: IoU threshold for NMS |
|
|
|
|
|
Returns: |
|
|
Dictionary containing detection results |
|
|
""" |
|
|
if isinstance(image, str): |
|
|
img_array = cv2.imread(image) |
|
|
if img_array is None: |
|
|
raise ValueError(f"Could not load image: {image}") |
|
|
image_path = image |
|
|
else: |
|
|
img_array = image |
|
|
image_path = None |
|
|
|
|
|
results = self.model(image_path if image_path else img_array, |
|
|
conf=conf, iou=iou, verbose=False) |
|
|
|
|
|
return self._process_results(results, img_array) |
|
|
|
|
|
def _process_results(self, results, original_image: np.ndarray) -> Dict: |
|
|
"""Process YOLO results into structured output.""" |
|
|
h, w = original_image.shape[:2] |
|
|
|
|
|
output = { |
|
|
"detected": False, |
|
|
"binary_mask": None, |
|
|
"overlay": None, |
|
|
"water_percentage": 0.0, |
|
|
"water_pixels": 0, |
|
|
"total_pixels": h * w, |
|
|
"bounding_boxes": [], |
|
|
"confidence_scores": [] |
|
|
} |
|
|
|
|
|
if len(results) == 0 or results[0].masks is None: |
|
|
return output |
|
|
|
|
|
result = results[0] |
|
|
|
|
|
masks = result.masks.data.cpu().numpy() |
|
|
|
|
|
if len(masks) == 0: |
|
|
return output |
|
|
|
|
|
combined_mask = np.zeros((h, w), dtype=np.uint8) |
|
|
|
|
|
for mask in masks: |
|
|
resized_mask = cv2.resize(mask, (w, h)) |
|
|
combined_mask = np.maximum(combined_mask, (resized_mask > 0.5).astype(np.uint8)) |
|
|
|
|
|
binary_mask = combined_mask * 255 |
|
|
|
|
|
overlay = original_image.copy() |
|
|
colored_mask = np.zeros_like(original_image) |
|
|
colored_mask[binary_mask > 0] = [0, 0, 255] |
|
|
overlay = cv2.addWeighted(overlay, 0.7, colored_mask, 0.3, 0) |
|
|
|
|
|
water_pixels = np.sum(binary_mask > 0) |
|
|
water_percentage = (water_pixels / (h * w)) * 100 |
|
|
|
|
|
if result.boxes is not None: |
|
|
boxes = result.boxes.xyxy.cpu().numpy() |
|
|
scores = result.boxes.conf.cpu().numpy() |
|
|
|
|
|
output["bounding_boxes"] = boxes.tolist() |
|
|
output["confidence_scores"] = scores.tolist() |
|
|
|
|
|
output.update({ |
|
|
"detected": True, |
|
|
"binary_mask": binary_mask, |
|
|
"overlay": overlay, |
|
|
"water_percentage": water_percentage, |
|
|
"water_pixels": int(water_pixels) |
|
|
}) |
|
|
|
|
|
return output |
|
|
|
|
|
def detect_batch(self, |
|
|
image_paths: list, |
|
|
conf: float = 0.25, |
|
|
iou: float = 0.45) -> Dict: |
|
|
""" |
|
|
Detect water surfaces in multiple images. |
|
|
|
|
|
Args: |
|
|
image_paths: List of paths to image files |
|
|
conf: Confidence threshold |
|
|
iou: IoU threshold for NMS |
|
|
|
|
|
Returns: |
|
|
Dictionary with results for each image |
|
|
""" |
|
|
results = {} |
|
|
|
|
|
for image_path in image_paths: |
|
|
try: |
|
|
result = self.detect(image_path, conf, iou) |
|
|
results[image_path] = result |
|
|
except Exception as e: |
|
|
results[image_path] = {"error": str(e)} |
|
|
|
|
|
return results |
|
|
|
|
|
def save_results(self, |
|
|
results: Dict, |
|
|
output_dir: str, |
|
|
base_name: str, |
|
|
save_mask: bool = True, |
|
|
save_overlay: bool = True) -> Dict[str, str]: |
|
|
""" |
|
|
Save detection results to files. |
|
|
|
|
|
Args: |
|
|
results: Results from detect() method |
|
|
output_dir: Directory to save results |
|
|
base_name: Base name for output files |
|
|
save_mask: Whether to save binary mask |
|
|
save_overlay: Whether to save overlay |
|
|
|
|
|
Returns: |
|
|
Dictionary with saved file paths |
|
|
""" |
|
|
os.makedirs(output_dir, exist_ok=True) |
|
|
saved_files = {} |
|
|
|
|
|
if save_mask and results["binary_mask"] is not None: |
|
|
mask_path = os.path.join(output_dir, f"{base_name}_mask.png") |
|
|
cv2.imwrite(mask_path, results["binary_mask"]) |
|
|
saved_files["mask"] = mask_path |
|
|
|
|
|
if save_overlay and results["overlay"] is not None: |
|
|
overlay_path = os.path.join(output_dir, f"{base_name}_overlay.png") |
|
|
cv2.imwrite(overlay_path, results["overlay"]) |
|
|
saved_files["overlay"] = overlay_path |
|
|
|
|
|
return saved_files |
|
|
|
|
|
def get_water_classification(self, percentage: float) -> str: |
|
|
"""Classify water coverage level.""" |
|
|
if percentage < 10: |
|
|
return "minimal" |
|
|
elif percentage < 30: |
|
|
return "low" |
|
|
elif percentage < 50: |
|
|
return "moderate" |
|
|
elif percentage < 70: |
|
|
return "high" |
|
|
else: |
|
|
return "very_high" |
|
|
|
|
|
|
|
|
|
|
|
def main(): |
|
|
"""Example usage of the WaterSurfaceDetector API.""" |
|
|
print("🌊 NWSD API Example") |
|
|
print("=" * 30) |
|
|
|
|
|
detector = WaterSurfaceDetector() |
|
|
|
|
|
|
|
|
test_images = list(Path("..").glob("*.jpg")) |
|
|
|
|
|
if not test_images: |
|
|
print("No test images found") |
|
|
return |
|
|
|
|
|
test_image = str(test_images[0]) |
|
|
print(f"Processing: {test_image}") |
|
|
|
|
|
results = detector.detect(test_image) |
|
|
|
|
|
print(f"Water detected: {results['detected']}") |
|
|
print(f"Water coverage: {results['water_percentage']:.2f}%") |
|
|
print(f"Classification: {detector.get_water_classification(results['water_percentage'])}") |
|
|
|
|
|
|
|
|
if results['detected']: |
|
|
output_dir = "api_results" |
|
|
base_name = Path(test_image).stem |
|
|
saved_files = detector.save_results(results, output_dir, base_name) |
|
|
print(f"Results saved to: {saved_files}") |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |
|
|
|