Ehlum-Lucas commited on
Commit
4109acb
·
0 Parent(s):

Initial commit

Browse files
Files changed (9) hide show
  1. .gitattributes +1 -0
  2. Readme.md +129 -0
  3. decode_mask.py +4 -0
  4. evaluate.py +165 -0
  5. model_card_template.yaml +87 -0
  6. nwsd-v2.pt +3 -0
  7. nwsd_api.py +233 -0
  8. predict.py +298 -0
  9. 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()