Ehlum-Lucas
commited on
Commit
·
4109acb
0
Parent(s):
Initial commit
Browse files- .gitattributes +1 -0
- Readme.md +129 -0
- decode_mask.py +4 -0
- evaluate.py +165 -0
- model_card_template.yaml +87 -0
- nwsd-v2.pt +3 -0
- nwsd_api.py +233 -0
- predict.py +298 -0
- train.py +153 -0
.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
Readme.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language: "en"
|
| 3 |
+
license: "gpl-3.0"
|
| 4 |
+
tags:
|
| 5 |
+
- segmentation
|
| 6 |
+
- computer-vision
|
| 7 |
+
- yolo
|
| 8 |
+
- beach
|
| 9 |
+
- water
|
| 10 |
+
- open-source
|
| 11 |
+
task_categories:
|
| 12 |
+
- image-segmentation
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# 🌊 Water Surface Segmentation on Beach Images
|
| 16 |
+
|
| 17 |
+
## Model Overview
|
| 18 |
+
This model performs **semantic segmentation of water surfaces** in beach or coastal images.
|
| 19 |
+
It’s a fine-tuned version of **YOLOv11n**, adapted for **binary segmentation** with a single class: **`water`**.
|
| 20 |
+
|
| 21 |
+
Built for lightweight, real-time deployment, the model achieves strong accuracy while remaining small and efficient.
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## 🧠 Model Details
|
| 26 |
+
- **Architecture**: YOLOv11n segmentation head (binary)
|
| 27 |
+
- **Base framework**: PyTorch / Ultralytics YOLOv11
|
| 28 |
+
- **Input size**: 640×640 RGB images
|
| 29 |
+
- **Output**: Binary segmentation mask (1 class — `water`)
|
| 30 |
+
- **Model file**: `nwsd-v2.pt` (≈6 MB)
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## 🚀 Key Features
|
| 35 |
+
- ⚡ **Real-time inference** on CPU/GPU
|
| 36 |
+
- 🖼 **Outputs**: Binary masks, overlays, and coverage statistics
|
| 37 |
+
- 📊 **Evaluation tools** included for metrics & visualization
|
| 38 |
+
- 🐍 **Easy Python integration** via a simple API (`nwsd_api.py`)
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## 📈 Performance
|
| 43 |
+
| Metric | Value | Notes |
|
| 44 |
+
|:--|:--|:--|
|
| 45 |
+
| **mAP50** | > 0.85 | On validation set |
|
| 46 |
+
| **Inference speed** | ~50 ms/image | On CPU |
|
| 47 |
+
| **GPU memory** | < 2 GB | For 640×640 input |
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## 🗂 Dataset
|
| 52 |
+
- **Type**: Binary segmentation
|
| 53 |
+
- **Classes**: `water`
|
| 54 |
+
- **Annotations**: PNG masks
|
| 55 |
+
- **Source**: Custom-labeled beach dataset
|
| 56 |
+
|
| 57 |
+
🔗 [Dataset on Roboflow](https://universe.roboflow.com/neptune-uxxqf/neptune-water-surface-detection)
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
## 🧩 Intended Uses
|
| 62 |
+
**Use cases:**
|
| 63 |
+
- Coastal or maritime monitoring
|
| 64 |
+
- Beach safety & drowning prevention systems
|
| 65 |
+
- Environmental analysis (e.g., water coverage estimation)
|
| 66 |
+
|
| 67 |
+
**Limitations:**
|
| 68 |
+
- Designed for daylight, clear beach imagery
|
| 69 |
+
- May underperform in low-visibility or night-time scenes
|
| 70 |
+
|
| 71 |
+
---
|
| 72 |
+
|
| 73 |
+
## 🧪 How to Use
|
| 74 |
+
|
| 75 |
+
### Load model from Hub
|
| 76 |
+
```python
|
| 77 |
+
from huggingface_hub import hf_hub_download
|
| 78 |
+
import torch
|
| 79 |
+
|
| 80 |
+
model_path = hf_hub_download(repo_id="Ehlum-Lucas/NWSD", filename="nwsd-v2.pt")
|
| 81 |
+
model = torch.load(model_path, map_location="cpu")
|
| 82 |
+
model.eval()
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### Inference example
|
| 86 |
+
```python
|
| 87 |
+
from PIL import Image
|
| 88 |
+
import torch
|
| 89 |
+
from torchvision import transforms
|
| 90 |
+
|
| 91 |
+
img = Image.open("beachTest.jpg").convert("RGB")
|
| 92 |
+
input_tensor = transforms.ToTensor()(img).unsqueeze(0)
|
| 93 |
+
|
| 94 |
+
with torch.no_grad():
|
| 95 |
+
pred = model(input_tensor)
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
### ⚙️ Training
|
| 99 |
+
You can fine-tune or retrain the model using YOLOv11 tools:
|
| 100 |
+
```bash
|
| 101 |
+
python train.py --data data.yaml --weights <path_to_weights> --img 640 --batch 16 --epochs 50
|
| 102 |
+
```
|
| 103 |
+
Example configuration (data.yaml) defines paths to your datasets and class names.
|
| 104 |
+
|
| 105 |
+
### 🧭 Evaluation
|
| 106 |
+
```bash
|
| 107 |
+
python evaluate.py --data data.yaml --weights model/nwsd-v2.pt
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
Generates:
|
| 111 |
+
|
| 112 |
+
- Binary mask
|
| 113 |
+
- Overlay visualization
|
| 114 |
+
- Water coverage stats
|
| 115 |
+
|
| 116 |
+
## License
|
| 117 |
+
This model is released under the **GPL-3.0 License**. See the [LICENSE](LICENSE) file for details.
|
| 118 |
+
|
| 119 |
+
## Citation
|
| 120 |
+
If you use this model in your work, please consider citing:
|
| 121 |
+
```latex
|
| 122 |
+
@misc{nwsd2025,
|
| 123 |
+
title={Water Surface Segmentation on Beach Images},
|
| 124 |
+
author={Lucas Iglesia},
|
| 125 |
+
year={2025},
|
| 126 |
+
howpublished={\url{https://huggingface.co/Ehlum-Lucas/NWSD}}
|
| 127 |
+
}
|
| 128 |
+
```
|
| 129 |
+
|
decode_mask.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
|
| 3 |
+
with open("mask.png", "wb") as f:
|
| 4 |
+
f.write(base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAAAAAAQuoM4AAAFHElEQVR4Ae3BUbITBBQFwZn9L/pafqCUUvrgBU5CpluSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGJBmSZEiSIUmGPF6Z5LV5vDLJa/N4ZZLX5vHKJK/N4zcmeXIeb0X+dvyT/K/ja5LP8ciDSL6bRx5O8kEe+Zkk/8UjC5I/eeS5yDvxyKuT1+WR35U8P4+8DXk6Hnl38sXxEfI4HsnnyY/xSH4x+YtHsuOR7HgkOx7Jjkey45HseCQ7HsmOR7Ljkex4JDseyY5HsuOR7HgkOx7Jjkey45HseCQ7HsmOR7Ljkex4JDseyY5HsuOR7HgkOx7Jjkey45HseCQ7HsmOR7Ljkex4JDseyY5HsuOR7HgkOx7Jjkey45HseCQ7HsmOR7Ljkex4JDseyY5HsuOR7HgkOx7Jjkey45HseCQ7HsmOR7Ljkex4JDseyY5HsuOR7HgkOx7Jjkey45HseCQ7HsmOR7Ljkex4JDseyY5HsuOR7HgkOx7Jjkey45HseCQ7HsmOR7Ljkex4JDseyY5HsuOR7HgkOx7Jjkey45HseCQ7HsmOR7Ljkex4JDseyY5HsuOR7HgkOx7Jjkey45HseCQ7HsmOR7Ljkex4JDseyY7AkYzIhx3Jo8knHMnnyOMcyXeSX+NIvkGmjrw3eSbHA8k3HXkikg858jNIfsSRh5A82pEPk+wdb0vybI43Inkxx+9E8sKOr8g3Hc9M8j6OpyN5Q8ezkLyp4xlI3tixJnl7x44kXxy/nCT/dvwiknzE8VNI8j2Oh5LkBxyPIcknHJ8jyQMcP0aSBzq+jyRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgxJMiTJkCRDkgz9AV/Pnlo/ledrAAAAAElFTkSuQmCC"))
|
evaluate.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Water Surface Segmentation Evaluation Script
|
| 4 |
+
Evaluate the trained model on a validation dataset.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from ultralytics import YOLO
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def parse_arguments() -> argparse.Namespace:
|
| 15 |
+
"""Parse command line arguments."""
|
| 16 |
+
parser = argparse.ArgumentParser(
|
| 17 |
+
description="Evaluate water surface segmentation model",
|
| 18 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
parser.add_argument(
|
| 22 |
+
"--data",
|
| 23 |
+
type=str,
|
| 24 |
+
required=True,
|
| 25 |
+
help="Path to validation dataset or data.yaml file"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
parser.add_argument(
|
| 29 |
+
"--weights",
|
| 30 |
+
type=str,
|
| 31 |
+
default="model/nwsd-v2.pt",
|
| 32 |
+
help="Path to model weights file"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
parser.add_argument(
|
| 36 |
+
"--img",
|
| 37 |
+
type=int,
|
| 38 |
+
default=640,
|
| 39 |
+
help="Image size for evaluation"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
parser.add_argument(
|
| 43 |
+
"--batch",
|
| 44 |
+
type=int,
|
| 45 |
+
default=16,
|
| 46 |
+
help="Batch size for evaluation"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
parser.add_argument(
|
| 50 |
+
"--conf",
|
| 51 |
+
type=float,
|
| 52 |
+
default=0.25,
|
| 53 |
+
help="Confidence threshold"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
parser.add_argument(
|
| 57 |
+
"--iou",
|
| 58 |
+
type=float,
|
| 59 |
+
default=0.45,
|
| 60 |
+
help="IoU threshold for NMS"
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
parser.add_argument(
|
| 64 |
+
"--device",
|
| 65 |
+
type=str,
|
| 66 |
+
default="",
|
| 67 |
+
help="Device to use for evaluation (cpu, cuda, mps)"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
parser.add_argument(
|
| 71 |
+
"--project",
|
| 72 |
+
type=str,
|
| 73 |
+
default="runs/segment",
|
| 74 |
+
help="Project directory for results"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
parser.add_argument(
|
| 78 |
+
"--name",
|
| 79 |
+
type=str,
|
| 80 |
+
default="nwsd_eval",
|
| 81 |
+
help="Experiment name"
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
parser.add_argument(
|
| 85 |
+
"--save-json",
|
| 86 |
+
action="store_true",
|
| 87 |
+
help="Save results in JSON format"
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
parser.add_argument(
|
| 91 |
+
"--save-txt",
|
| 92 |
+
action="store_true",
|
| 93 |
+
help="Save results in TXT format"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
parser.add_argument(
|
| 97 |
+
"--plots",
|
| 98 |
+
action="store_true",
|
| 99 |
+
help="Generate evaluation plots"
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
return parser.parse_args()
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def validate_inputs(args: argparse.Namespace) -> None:
|
| 106 |
+
"""Validate input arguments."""
|
| 107 |
+
if not os.path.exists(args.data):
|
| 108 |
+
raise FileNotFoundError(f"Data path not found: {args.data}")
|
| 109 |
+
|
| 110 |
+
if not os.path.exists(args.weights):
|
| 111 |
+
raise FileNotFoundError(f"Model weights not found: {args.weights}")
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def main():
|
| 115 |
+
"""Main evaluation function."""
|
| 116 |
+
args = parse_arguments()
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
validate_inputs(args)
|
| 120 |
+
|
| 121 |
+
print(f"Loading model: {args.weights}")
|
| 122 |
+
model = YOLO(args.weights)
|
| 123 |
+
|
| 124 |
+
eval_params = {
|
| 125 |
+
'data': args.data,
|
| 126 |
+
'imgsz': args.img,
|
| 127 |
+
'batch': args.batch,
|
| 128 |
+
'conf': args.conf,
|
| 129 |
+
'iou': args.iou,
|
| 130 |
+
'device': args.device,
|
| 131 |
+
'project': args.project,
|
| 132 |
+
'name': args.name,
|
| 133 |
+
'save_json': args.save_json,
|
| 134 |
+
'save_txt': args.save_txt,
|
| 135 |
+
'plots': args.plots,
|
| 136 |
+
'verbose': True,
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
print("Starting evaluation with parameters:")
|
| 140 |
+
for key, value in eval_params.items():
|
| 141 |
+
print(f" {key}: {value}")
|
| 142 |
+
|
| 143 |
+
results = model.val(**eval_params)
|
| 144 |
+
|
| 145 |
+
print("\n" + "="*50)
|
| 146 |
+
print("EVALUATION RESULTS SUMMARY")
|
| 147 |
+
print("="*50)
|
| 148 |
+
|
| 149 |
+
if hasattr(results, 'box') and results.box is not None:
|
| 150 |
+
print(f"mAP50: {results.box.map50:.4f}")
|
| 151 |
+
print(f"mAP50-95: {results.box.map:.4f}")
|
| 152 |
+
|
| 153 |
+
if hasattr(results, 'seg') and results.seg is not None:
|
| 154 |
+
print(f"Segmentation mAP50: {results.seg.map50:.4f}")
|
| 155 |
+
print(f"Segmentation mAP50-95: {results.seg.map:.4f}")
|
| 156 |
+
|
| 157 |
+
print("\nEvaluation completed successfully!")
|
| 158 |
+
|
| 159 |
+
except Exception as e:
|
| 160 |
+
print(f"Error: {str(e)}", file=sys.stderr)
|
| 161 |
+
sys.exit(1)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
if __name__ == "__main__":
|
| 165 |
+
main()
|
model_card_template.yaml
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# model_card_template.yaml
|
| 2 |
+
|
| 3 |
+
# =====================================================
|
| 4 |
+
# 🌊 Water Surface Segmentation on Beach Images
|
| 5 |
+
# =====================================================
|
| 6 |
+
# Hugging Face model metadata file
|
| 7 |
+
# =====================================================
|
| 8 |
+
|
| 9 |
+
language:
|
| 10 |
+
- en
|
| 11 |
+
|
| 12 |
+
license: gpl-3.0
|
| 13 |
+
|
| 14 |
+
library_name: pytorch
|
| 15 |
+
|
| 16 |
+
tags:
|
| 17 |
+
- segmentation
|
| 18 |
+
- computer-vision
|
| 19 |
+
- yolo
|
| 20 |
+
- beach
|
| 21 |
+
- water
|
| 22 |
+
- open-source
|
| 23 |
+
|
| 24 |
+
task_categories:
|
| 25 |
+
- image-segmentation
|
| 26 |
+
|
| 27 |
+
model-index:
|
| 28 |
+
- name: Water Surface Segmentation (NWSD)
|
| 29 |
+
results:
|
| 30 |
+
- task:
|
| 31 |
+
type: image-segmentation
|
| 32 |
+
name: Image Segmentation
|
| 33 |
+
metrics:
|
| 34 |
+
- type: mAP50
|
| 35 |
+
name: Mean Average Precision @ 0.5
|
| 36 |
+
value: 0.85
|
| 37 |
+
- type: inference_speed
|
| 38 |
+
name: Inference Speed (CPU)
|
| 39 |
+
value: 50
|
| 40 |
+
unit: "ms/image"
|
| 41 |
+
|
| 42 |
+
model_details:
|
| 43 |
+
description: >
|
| 44 |
+
A YOLOv11n-based segmentation model fine-tuned for detecting and segmenting
|
| 45 |
+
water surfaces in coastal or beach images. Trained on a custom-labeled dataset
|
| 46 |
+
containing a single class: "water".
|
| 47 |
+
developed_by: Lucas Iglesia
|
| 48 |
+
repo: https://huggingface.co/Lucas-Iglesia/NWSD
|
| 49 |
+
license: GPL-3.0
|
| 50 |
+
framework: PyTorch
|
| 51 |
+
model_size: 6.07 MB
|
| 52 |
+
input_size: "640x640"
|
| 53 |
+
num_classes: 1
|
| 54 |
+
class_labels: ["water"]
|
| 55 |
+
release_date: "2025-11-07"
|
| 56 |
+
|
| 57 |
+
inference:
|
| 58 |
+
parameters:
|
| 59 |
+
device: "cpu or cuda"
|
| 60 |
+
conf_threshold: 0.5
|
| 61 |
+
example_inputs:
|
| 62 |
+
- beachTest.jpg
|
| 63 |
+
example_outputs:
|
| 64 |
+
- binary_mask.png
|
| 65 |
+
- overlay.png
|
| 66 |
+
usage_snippet: |
|
| 67 |
+
from huggingface_hub import hf_hub_download
|
| 68 |
+
import torch
|
| 69 |
+
model_path = hf_hub_download(repo_id="Ehlum-Lucas/NWSD", filename="nwsd-v2.pt")
|
| 70 |
+
model = torch.load(model_path, map_location="cpu")
|
| 71 |
+
model.eval()
|
| 72 |
+
|
| 73 |
+
recommended_use:
|
| 74 |
+
- Coastal monitoring
|
| 75 |
+
- Beach safety and drowning prevention
|
| 76 |
+
- Environmental water coverage analysis
|
| 77 |
+
|
| 78 |
+
limitations:
|
| 79 |
+
- Optimized for daylight beach scenes
|
| 80 |
+
- May underperform in low-visibility or night images
|
| 81 |
+
|
| 82 |
+
citation:
|
| 83 |
+
- type: misc
|
| 84 |
+
title: "Water Surface Segmentation on Beach Images"
|
| 85 |
+
author: "Lucas Iglesia"
|
| 86 |
+
year: 2025
|
| 87 |
+
url: "https://huggingface.co/Ehlum-Lucas/NWSD"
|
nwsd-v2.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:643363b33e702713dc69f38146bdeb6f6a47b1c7c32d7593a58b0a5c7f9b4722
|
| 3 |
+
size 6360093
|
nwsd_api.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
NWSD API - Simple Python API for water surface detection
|
| 4 |
+
This module provides a simple interface for water surface segmentation.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import cv2
|
| 9 |
+
import numpy as np
|
| 10 |
+
from typing import Optional, Tuple, Dict, Union
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from ultralytics import YOLO
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class WaterSurfaceDetector:
|
| 16 |
+
"""Water Surface Detection API using YOLOv11n."""
|
| 17 |
+
|
| 18 |
+
def __init__(self, weights_path: str = "model/nwsd-v2.pt", device: str = "cpu"):
|
| 19 |
+
"""
|
| 20 |
+
Initialize the water surface detector.
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
weights_path: Path to model weights
|
| 24 |
+
device: Device to use for inference (cpu, cuda, mps)
|
| 25 |
+
"""
|
| 26 |
+
self.weights_path = weights_path
|
| 27 |
+
self.device = device
|
| 28 |
+
self.model = None
|
| 29 |
+
self._load_model()
|
| 30 |
+
|
| 31 |
+
def _load_model(self):
|
| 32 |
+
"""Load the YOLO model."""
|
| 33 |
+
if not os.path.exists(self.weights_path):
|
| 34 |
+
raise FileNotFoundError(f"Model weights not found: {self.weights_path}")
|
| 35 |
+
|
| 36 |
+
self.model = YOLO(self.weights_path)
|
| 37 |
+
self.model.to(self.device)
|
| 38 |
+
|
| 39 |
+
def detect(self,
|
| 40 |
+
image: Union[str, np.ndarray],
|
| 41 |
+
conf: float = 0.25,
|
| 42 |
+
iou: float = 0.45) -> Dict:
|
| 43 |
+
"""
|
| 44 |
+
Detect water surfaces in an image.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
image: Path to image file or numpy array
|
| 48 |
+
conf: Confidence threshold
|
| 49 |
+
iou: IoU threshold for NMS
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
Dictionary containing detection results
|
| 53 |
+
"""
|
| 54 |
+
if isinstance(image, str):
|
| 55 |
+
img_array = cv2.imread(image)
|
| 56 |
+
if img_array is None:
|
| 57 |
+
raise ValueError(f"Could not load image: {image}")
|
| 58 |
+
image_path = image
|
| 59 |
+
else:
|
| 60 |
+
img_array = image
|
| 61 |
+
image_path = None
|
| 62 |
+
|
| 63 |
+
results = self.model(image_path if image_path else img_array,
|
| 64 |
+
conf=conf, iou=iou, verbose=False)
|
| 65 |
+
|
| 66 |
+
return self._process_results(results, img_array)
|
| 67 |
+
|
| 68 |
+
def _process_results(self, results, original_image: np.ndarray) -> Dict:
|
| 69 |
+
"""Process YOLO results into structured output."""
|
| 70 |
+
h, w = original_image.shape[:2]
|
| 71 |
+
|
| 72 |
+
output = {
|
| 73 |
+
"detected": False,
|
| 74 |
+
"binary_mask": None,
|
| 75 |
+
"overlay": None,
|
| 76 |
+
"water_percentage": 0.0,
|
| 77 |
+
"water_pixels": 0,
|
| 78 |
+
"total_pixels": h * w,
|
| 79 |
+
"bounding_boxes": [],
|
| 80 |
+
"confidence_scores": []
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
if len(results) == 0 or results[0].masks is None:
|
| 84 |
+
return output
|
| 85 |
+
|
| 86 |
+
result = results[0]
|
| 87 |
+
|
| 88 |
+
masks = result.masks.data.cpu().numpy()
|
| 89 |
+
|
| 90 |
+
if len(masks) == 0:
|
| 91 |
+
return output
|
| 92 |
+
|
| 93 |
+
combined_mask = np.zeros((h, w), dtype=np.uint8)
|
| 94 |
+
|
| 95 |
+
for mask in masks:
|
| 96 |
+
resized_mask = cv2.resize(mask, (w, h))
|
| 97 |
+
combined_mask = np.maximum(combined_mask, (resized_mask > 0.5).astype(np.uint8))
|
| 98 |
+
|
| 99 |
+
binary_mask = combined_mask * 255
|
| 100 |
+
|
| 101 |
+
overlay = original_image.copy()
|
| 102 |
+
colored_mask = np.zeros_like(original_image)
|
| 103 |
+
colored_mask[binary_mask > 0] = [0, 0, 255]
|
| 104 |
+
overlay = cv2.addWeighted(overlay, 0.7, colored_mask, 0.3, 0)
|
| 105 |
+
|
| 106 |
+
water_pixels = np.sum(binary_mask > 0)
|
| 107 |
+
water_percentage = (water_pixels / (h * w)) * 100
|
| 108 |
+
|
| 109 |
+
if result.boxes is not None:
|
| 110 |
+
boxes = result.boxes.xyxy.cpu().numpy()
|
| 111 |
+
scores = result.boxes.conf.cpu().numpy()
|
| 112 |
+
|
| 113 |
+
output["bounding_boxes"] = boxes.tolist()
|
| 114 |
+
output["confidence_scores"] = scores.tolist()
|
| 115 |
+
|
| 116 |
+
output.update({
|
| 117 |
+
"detected": True,
|
| 118 |
+
"binary_mask": binary_mask,
|
| 119 |
+
"overlay": overlay,
|
| 120 |
+
"water_percentage": water_percentage,
|
| 121 |
+
"water_pixels": int(water_pixels)
|
| 122 |
+
})
|
| 123 |
+
|
| 124 |
+
return output
|
| 125 |
+
|
| 126 |
+
def detect_batch(self,
|
| 127 |
+
image_paths: list,
|
| 128 |
+
conf: float = 0.25,
|
| 129 |
+
iou: float = 0.45) -> Dict:
|
| 130 |
+
"""
|
| 131 |
+
Detect water surfaces in multiple images.
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
image_paths: List of paths to image files
|
| 135 |
+
conf: Confidence threshold
|
| 136 |
+
iou: IoU threshold for NMS
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
Dictionary with results for each image
|
| 140 |
+
"""
|
| 141 |
+
results = {}
|
| 142 |
+
|
| 143 |
+
for image_path in image_paths:
|
| 144 |
+
try:
|
| 145 |
+
result = self.detect(image_path, conf, iou)
|
| 146 |
+
results[image_path] = result
|
| 147 |
+
except Exception as e:
|
| 148 |
+
results[image_path] = {"error": str(e)}
|
| 149 |
+
|
| 150 |
+
return results
|
| 151 |
+
|
| 152 |
+
def save_results(self,
|
| 153 |
+
results: Dict,
|
| 154 |
+
output_dir: str,
|
| 155 |
+
base_name: str,
|
| 156 |
+
save_mask: bool = True,
|
| 157 |
+
save_overlay: bool = True) -> Dict[str, str]:
|
| 158 |
+
"""
|
| 159 |
+
Save detection results to files.
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
results: Results from detect() method
|
| 163 |
+
output_dir: Directory to save results
|
| 164 |
+
base_name: Base name for output files
|
| 165 |
+
save_mask: Whether to save binary mask
|
| 166 |
+
save_overlay: Whether to save overlay
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
Dictionary with saved file paths
|
| 170 |
+
"""
|
| 171 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 172 |
+
saved_files = {}
|
| 173 |
+
|
| 174 |
+
if save_mask and results["binary_mask"] is not None:
|
| 175 |
+
mask_path = os.path.join(output_dir, f"{base_name}_mask.png")
|
| 176 |
+
cv2.imwrite(mask_path, results["binary_mask"])
|
| 177 |
+
saved_files["mask"] = mask_path
|
| 178 |
+
|
| 179 |
+
if save_overlay and results["overlay"] is not None:
|
| 180 |
+
overlay_path = os.path.join(output_dir, f"{base_name}_overlay.png")
|
| 181 |
+
cv2.imwrite(overlay_path, results["overlay"])
|
| 182 |
+
saved_files["overlay"] = overlay_path
|
| 183 |
+
|
| 184 |
+
return saved_files
|
| 185 |
+
|
| 186 |
+
def get_water_classification(self, percentage: float) -> str:
|
| 187 |
+
"""Classify water coverage level."""
|
| 188 |
+
if percentage < 10:
|
| 189 |
+
return "minimal"
|
| 190 |
+
elif percentage < 30:
|
| 191 |
+
return "low"
|
| 192 |
+
elif percentage < 50:
|
| 193 |
+
return "moderate"
|
| 194 |
+
elif percentage < 70:
|
| 195 |
+
return "high"
|
| 196 |
+
else:
|
| 197 |
+
return "very_high"
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
# Example usage
|
| 201 |
+
def main():
|
| 202 |
+
"""Example usage of the WaterSurfaceDetector API."""
|
| 203 |
+
print("🌊 NWSD API Example")
|
| 204 |
+
print("=" * 30)
|
| 205 |
+
|
| 206 |
+
detector = WaterSurfaceDetector()
|
| 207 |
+
|
| 208 |
+
# Look for test images
|
| 209 |
+
test_images = list(Path("..").glob("*.jpg"))
|
| 210 |
+
|
| 211 |
+
if not test_images:
|
| 212 |
+
print("No test images found")
|
| 213 |
+
return
|
| 214 |
+
|
| 215 |
+
test_image = str(test_images[0])
|
| 216 |
+
print(f"Processing: {test_image}")
|
| 217 |
+
|
| 218 |
+
results = detector.detect(test_image)
|
| 219 |
+
|
| 220 |
+
print(f"Water detected: {results['detected']}")
|
| 221 |
+
print(f"Water coverage: {results['water_percentage']:.2f}%")
|
| 222 |
+
print(f"Classification: {detector.get_water_classification(results['water_percentage'])}")
|
| 223 |
+
|
| 224 |
+
# Save results
|
| 225 |
+
if results['detected']:
|
| 226 |
+
output_dir = "api_results"
|
| 227 |
+
base_name = Path(test_image).stem
|
| 228 |
+
saved_files = detector.save_results(results, output_dir, base_name)
|
| 229 |
+
print(f"Results saved to: {saved_files}")
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
if __name__ == "__main__":
|
| 233 |
+
main()
|
predict.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Water Surface Segmentation Inference Script
|
| 4 |
+
This script performs inference on beach images to segment water surfaces using YOLOv11n.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import cv2
|
| 12 |
+
import numpy as np
|
| 13 |
+
from ultralytics import YOLO
|
| 14 |
+
import matplotlib
|
| 15 |
+
matplotlib.use('Agg') # Use non-interactive backend
|
| 16 |
+
import matplotlib.pyplot as plt
|
| 17 |
+
from typing import Optional, Tuple, List
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def parse_arguments() -> argparse.Namespace:
|
| 21 |
+
"""Parse command line arguments."""
|
| 22 |
+
parser = argparse.ArgumentParser(
|
| 23 |
+
description="Perform water surface segmentation on beach images",
|
| 24 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
parser.add_argument(
|
| 28 |
+
"--image",
|
| 29 |
+
type=str,
|
| 30 |
+
required=True,
|
| 31 |
+
help="Path to input image file"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
parser.add_argument(
|
| 35 |
+
"--weights",
|
| 36 |
+
type=str,
|
| 37 |
+
default="model/nwsd-v2.pt",
|
| 38 |
+
help="Path to model weights file"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
parser.add_argument(
|
| 42 |
+
"--output",
|
| 43 |
+
type=str,
|
| 44 |
+
default=None,
|
| 45 |
+
help="Output directory for results (default: same as input image)"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
parser.add_argument(
|
| 49 |
+
"--conf",
|
| 50 |
+
type=float,
|
| 51 |
+
default=0.25,
|
| 52 |
+
help="Confidence threshold for segmentation"
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
parser.add_argument(
|
| 56 |
+
"--iou",
|
| 57 |
+
type=float,
|
| 58 |
+
default=0.45,
|
| 59 |
+
help="IoU threshold for NMS"
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
parser.add_argument(
|
| 63 |
+
"--save-overlay",
|
| 64 |
+
action="store_true",
|
| 65 |
+
help="Save overlay visualization"
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
parser.add_argument(
|
| 69 |
+
"--save-mask",
|
| 70 |
+
action="store_true",
|
| 71 |
+
help="Save binary mask"
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--save-results",
|
| 76 |
+
action="store_true",
|
| 77 |
+
help="Save results visualization plot"
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
parser.add_argument(
|
| 81 |
+
"--device",
|
| 82 |
+
type=str,
|
| 83 |
+
default="cpu",
|
| 84 |
+
help="Device to use for inference (cpu, cuda, mps)"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
return parser.parse_args()
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def validate_inputs(args: argparse.Namespace) -> None:
|
| 91 |
+
"""Validate input arguments."""
|
| 92 |
+
if not os.path.exists(args.image):
|
| 93 |
+
raise FileNotFoundError(f"Input image not found: {args.image}")
|
| 94 |
+
|
| 95 |
+
if not os.path.exists(args.weights):
|
| 96 |
+
raise FileNotFoundError(f"Model weights not found: {args.weights}")
|
| 97 |
+
|
| 98 |
+
valid_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif'}
|
| 99 |
+
image_ext = Path(args.image).suffix.lower()
|
| 100 |
+
if image_ext not in valid_extensions:
|
| 101 |
+
raise ValueError(f"Unsupported image format: {image_ext}")
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def load_model(weights_path: str, device: str = "cpu") -> YOLO:
|
| 105 |
+
"""Load YOLO model."""
|
| 106 |
+
try:
|
| 107 |
+
model = YOLO(weights_path)
|
| 108 |
+
model.to(device)
|
| 109 |
+
print(f"Model loaded successfully from: {weights_path}")
|
| 110 |
+
print(f"Using device: {device}")
|
| 111 |
+
return model
|
| 112 |
+
except Exception as e:
|
| 113 |
+
raise RuntimeError(f"Failed to load model: {str(e)}")
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def preprocess_image(image_path: str) -> Tuple[np.ndarray, Tuple[int, int]]:
|
| 117 |
+
"""Load and preprocess image."""
|
| 118 |
+
image = cv2.imread(image_path)
|
| 119 |
+
if image is None:
|
| 120 |
+
raise ValueError(f"Could not read image: {image_path}")
|
| 121 |
+
|
| 122 |
+
original_shape = image.shape[:2] # (height, width)
|
| 123 |
+
return image, original_shape
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def postprocess_results(results, original_shape: Tuple[int, int]) -> Tuple[np.ndarray, np.ndarray]:
|
| 127 |
+
"""Extract masks and create binary mask."""
|
| 128 |
+
if len(results) == 0 or results[0].masks is None:
|
| 129 |
+
print("No water surface detected in the image")
|
| 130 |
+
return None, None
|
| 131 |
+
|
| 132 |
+
result = results[0]
|
| 133 |
+
|
| 134 |
+
masks = result.masks.data.cpu().numpy() # Shape: (N, H, W)
|
| 135 |
+
|
| 136 |
+
binary_mask = np.zeros(original_shape, dtype=np.uint8)
|
| 137 |
+
|
| 138 |
+
if len(masks) > 0:
|
| 139 |
+
resized_masks = []
|
| 140 |
+
for mask in masks:
|
| 141 |
+
resized_mask = cv2.resize(mask, (original_shape[1], original_shape[0]))
|
| 142 |
+
resized_masks.append(resized_mask)
|
| 143 |
+
|
| 144 |
+
combined_mask = np.max(resized_masks, axis=0)
|
| 145 |
+
binary_mask = (combined_mask > 0.5).astype(np.uint8) * 255
|
| 146 |
+
|
| 147 |
+
return binary_mask, masks
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def create_overlay(image: np.ndarray, binary_mask: np.ndarray, alpha: float = 0.3) -> np.ndarray:
|
| 151 |
+
"""Create overlay visualization."""
|
| 152 |
+
overlay = image.copy()
|
| 153 |
+
|
| 154 |
+
colored_mask = np.zeros_like(image)
|
| 155 |
+
colored_mask[binary_mask > 0] = [255, 0, 0]
|
| 156 |
+
|
| 157 |
+
overlay = cv2.addWeighted(overlay, 1 - alpha, colored_mask, alpha, 0)
|
| 158 |
+
|
| 159 |
+
return overlay
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def save_results(
|
| 163 |
+
image: np.ndarray,
|
| 164 |
+
binary_mask: Optional[np.ndarray],
|
| 165 |
+
overlay: Optional[np.ndarray],
|
| 166 |
+
output_dir: str,
|
| 167 |
+
base_name: str,
|
| 168 |
+
save_mask: bool = False,
|
| 169 |
+
save_overlay: bool = False
|
| 170 |
+
) -> None:
|
| 171 |
+
"""Save results to output directory."""
|
| 172 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 173 |
+
|
| 174 |
+
if save_mask and binary_mask is not None:
|
| 175 |
+
mask_path = os.path.join(output_dir, f"{base_name}_mask.png")
|
| 176 |
+
cv2.imwrite(mask_path, binary_mask)
|
| 177 |
+
print(f"Binary mask saved to: {mask_path}")
|
| 178 |
+
|
| 179 |
+
if save_overlay and overlay is not None:
|
| 180 |
+
overlay_path = os.path.join(output_dir, f"{base_name}_overlay.png")
|
| 181 |
+
cv2.imwrite(overlay_path, overlay)
|
| 182 |
+
print(f"Overlay visualization saved to: {overlay_path}")
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def display_results(
|
| 186 |
+
image: np.ndarray,
|
| 187 |
+
binary_mask: Optional[np.ndarray],
|
| 188 |
+
overlay: Optional[np.ndarray],
|
| 189 |
+
output_dir: str = ".",
|
| 190 |
+
base_name: str = "result"
|
| 191 |
+
) -> None:
|
| 192 |
+
"""Display results using matplotlib."""
|
| 193 |
+
num_plots = 1 + (binary_mask is not None) + (overlay is not None)
|
| 194 |
+
|
| 195 |
+
plt.figure(figsize=(5 * num_plots, 5))
|
| 196 |
+
|
| 197 |
+
plot_idx = 1
|
| 198 |
+
|
| 199 |
+
plt.subplot(1, num_plots, plot_idx)
|
| 200 |
+
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
| 201 |
+
plt.title("Original Image")
|
| 202 |
+
plt.axis('off')
|
| 203 |
+
plot_idx += 1
|
| 204 |
+
|
| 205 |
+
if binary_mask is not None:
|
| 206 |
+
plt.subplot(1, num_plots, plot_idx)
|
| 207 |
+
plt.imshow(binary_mask, cmap='gray')
|
| 208 |
+
plt.title("Water Surface Mask")
|
| 209 |
+
plt.axis('off')
|
| 210 |
+
plot_idx += 1
|
| 211 |
+
|
| 212 |
+
if overlay is not None:
|
| 213 |
+
plt.subplot(1, num_plots, plot_idx)
|
| 214 |
+
plt.imshow(cv2.cvtColor(overlay, cv2.COLOR_BGR2RGB))
|
| 215 |
+
plt.title("Overlay Visualization")
|
| 216 |
+
plt.axis('off')
|
| 217 |
+
|
| 218 |
+
plt.tight_layout()
|
| 219 |
+
|
| 220 |
+
plot_path = os.path.join(output_dir, f"{base_name}_results.png")
|
| 221 |
+
plt.savefig(plot_path, dpi=150, bbox_inches='tight')
|
| 222 |
+
print(f"Results visualization saved to: {plot_path}")
|
| 223 |
+
plt.close()
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def calculate_water_percentage(binary_mask: np.ndarray) -> float:
|
| 227 |
+
"""Calculate percentage of water surface in the image."""
|
| 228 |
+
if binary_mask is None:
|
| 229 |
+
return 0.0
|
| 230 |
+
|
| 231 |
+
total_pixels = binary_mask.shape[0] * binary_mask.shape[1]
|
| 232 |
+
water_pixels = np.sum(binary_mask > 0)
|
| 233 |
+
|
| 234 |
+
return (water_pixels / total_pixels) * 100
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def main():
|
| 238 |
+
"""Main inference function."""
|
| 239 |
+
args = parse_arguments()
|
| 240 |
+
|
| 241 |
+
try:
|
| 242 |
+
validate_inputs(args)
|
| 243 |
+
|
| 244 |
+
if args.output is None:
|
| 245 |
+
output_dir = os.path.dirname(args.image)
|
| 246 |
+
|
| 247 |
+
if not output_dir:
|
| 248 |
+
output_dir = "."
|
| 249 |
+
else:
|
| 250 |
+
output_dir = args.output
|
| 251 |
+
|
| 252 |
+
base_name = Path(args.image).stem
|
| 253 |
+
|
| 254 |
+
model = load_model(args.weights, args.device)
|
| 255 |
+
|
| 256 |
+
image, original_shape = preprocess_image(args.image)
|
| 257 |
+
|
| 258 |
+
print(f"Processing image: {args.image}")
|
| 259 |
+
print(f"Image shape: {image.shape}")
|
| 260 |
+
|
| 261 |
+
results = model(
|
| 262 |
+
args.image,
|
| 263 |
+
conf=args.conf,
|
| 264 |
+
iou=args.iou,
|
| 265 |
+
verbose=False
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
binary_mask, masks = postprocess_results(results, original_shape)
|
| 269 |
+
|
| 270 |
+
overlay = None
|
| 271 |
+
if binary_mask is not None:
|
| 272 |
+
overlay = create_overlay(image, binary_mask)
|
| 273 |
+
|
| 274 |
+
water_percentage = calculate_water_percentage(binary_mask)
|
| 275 |
+
print(f"Water surface coverage: {water_percentage:.2f}%")
|
| 276 |
+
|
| 277 |
+
save_results(
|
| 278 |
+
image,
|
| 279 |
+
binary_mask,
|
| 280 |
+
overlay,
|
| 281 |
+
output_dir,
|
| 282 |
+
base_name,
|
| 283 |
+
save_mask=args.save_mask,
|
| 284 |
+
save_overlay=args.save_overlay
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
if args.save_results:
|
| 288 |
+
display_results(image, binary_mask, overlay, output_dir, base_name)
|
| 289 |
+
|
| 290 |
+
print("Inference completed successfully!")
|
| 291 |
+
|
| 292 |
+
except Exception as e:
|
| 293 |
+
print(f"Error: {str(e)}", file=sys.stderr)
|
| 294 |
+
sys.exit(1)
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
if __name__ == "__main__":
|
| 298 |
+
main()
|
train.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Water Surface Segmentation Training Script
|
| 4 |
+
Train YOLOv11n model for water surface segmentation on beach images.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from ultralytics import YOLO
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def parse_arguments() -> argparse.Namespace:
|
| 15 |
+
"""Parse command line arguments."""
|
| 16 |
+
parser = argparse.ArgumentParser(
|
| 17 |
+
description="Train YOLOv11n model for water surface segmentation",
|
| 18 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
parser.add_argument(
|
| 22 |
+
"--data",
|
| 23 |
+
type=str,
|
| 24 |
+
required=True,
|
| 25 |
+
help="Path to data.yaml file"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
parser.add_argument(
|
| 29 |
+
"--weights",
|
| 30 |
+
type=str,
|
| 31 |
+
default="yolov11n-seg.pt",
|
| 32 |
+
help="Path to pretrained weights"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
parser.add_argument(
|
| 36 |
+
"--img",
|
| 37 |
+
type=int,
|
| 38 |
+
default=640,
|
| 39 |
+
help="Image size for training"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
parser.add_argument(
|
| 43 |
+
"--batch",
|
| 44 |
+
type=int,
|
| 45 |
+
default=16,
|
| 46 |
+
help="Batch size"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
parser.add_argument(
|
| 50 |
+
"--epochs",
|
| 51 |
+
type=int,
|
| 52 |
+
default=50,
|
| 53 |
+
help="Number of training epochs"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
parser.add_argument(
|
| 57 |
+
"--device",
|
| 58 |
+
type=str,
|
| 59 |
+
default="",
|
| 60 |
+
help="Device to use for training (cpu, cuda, mps)"
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
parser.add_argument(
|
| 64 |
+
"--project",
|
| 65 |
+
type=str,
|
| 66 |
+
default="runs/segment",
|
| 67 |
+
help="Project directory"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
parser.add_argument(
|
| 71 |
+
"--name",
|
| 72 |
+
type=str,
|
| 73 |
+
default="nwsd_train",
|
| 74 |
+
help="Experiment name"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
parser.add_argument(
|
| 78 |
+
"--patience",
|
| 79 |
+
type=int,
|
| 80 |
+
default=10,
|
| 81 |
+
help="Early stopping patience"
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
parser.add_argument(
|
| 85 |
+
"--save-period",
|
| 86 |
+
type=int,
|
| 87 |
+
default=5,
|
| 88 |
+
help="Save model every n epochs"
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
return parser.parse_args()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def validate_inputs(args: argparse.Namespace) -> None:
|
| 95 |
+
"""Validate input arguments."""
|
| 96 |
+
if not os.path.exists(args.data):
|
| 97 |
+
raise FileNotFoundError(f"Data configuration file not found: {args.data}")
|
| 98 |
+
|
| 99 |
+
if not args.weights.startswith("yolov11") and not os.path.exists(args.weights):
|
| 100 |
+
raise FileNotFoundError(f"Weights file not found: {args.weights}")
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def main():
|
| 104 |
+
"""Main training function."""
|
| 105 |
+
args = parse_arguments()
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
validate_inputs(args)
|
| 109 |
+
|
| 110 |
+
print(f"Loading model: {args.weights}")
|
| 111 |
+
model = YOLO(args.weights)
|
| 112 |
+
|
| 113 |
+
train_params = {
|
| 114 |
+
'data': args.data,
|
| 115 |
+
'imgsz': args.img,
|
| 116 |
+
'batch': args.batch,
|
| 117 |
+
'epochs': args.epochs,
|
| 118 |
+
'device': args.device,
|
| 119 |
+
'project': args.project,
|
| 120 |
+
'name': args.name,
|
| 121 |
+
'patience': args.patience,
|
| 122 |
+
'save_period': args.save_period,
|
| 123 |
+
'save': True,
|
| 124 |
+
'verbose': True,
|
| 125 |
+
'plots': True,
|
| 126 |
+
'val': True,
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
print("Starting training with parameters:")
|
| 130 |
+
for key, value in train_params.items():
|
| 131 |
+
print(f" {key}: {value}")
|
| 132 |
+
|
| 133 |
+
results = model.train(**train_params)
|
| 134 |
+
|
| 135 |
+
model_save_path = os.path.join(args.project, args.name, "weights", "best.pt")
|
| 136 |
+
final_model_path = os.path.join("model", "nwsd-v2.pt")
|
| 137 |
+
|
| 138 |
+
os.makedirs("model", exist_ok=True)
|
| 139 |
+
|
| 140 |
+
if os.path.exists(model_save_path):
|
| 141 |
+
import shutil
|
| 142 |
+
shutil.copy2(model_save_path, final_model_path)
|
| 143 |
+
print(f"Best model saved to: {final_model_path}")
|
| 144 |
+
|
| 145 |
+
print("Training completed successfully!")
|
| 146 |
+
|
| 147 |
+
except Exception as e:
|
| 148 |
+
print(f"Error: {str(e)}", file=sys.stderr)
|
| 149 |
+
sys.exit(1)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
if __name__ == "__main__":
|
| 153 |
+
main()
|