Spaces:
Runtime error
Runtime error
Commit
·
8f5f46d
1
Parent(s):
80478c6
added files
Browse files- .dockerignore +61 -0
- Dockerfile +65 -0
- README-Docker.md +92 -0
- app3.py +193 -0
- docker-compose.yml +52 -0
- examples/10.png +0 -0
- examples/1019.png +0 -0
- examples/15.png +0 -0
- examples/2.png +0 -0
- examples/285.png +0 -0
- examples/32.png +0 -0
- examples/36.png +0 -0
- examples/457.png +0 -0
- examples/46.png +0 -0
- examples/59.png +0 -0
- examples/64.png +0 -0
- examples/Screenshot 2025-08-04 192031.png +0 -0
- examples/Screenshot 2025-08-04 193338.png +0 -0
- examples/canola_oli_2022140_lrg.png +0 -0
- main3.py +203 -0
- pre1.py +99 -0
- run-docker.sh +63 -0
.dockerignore
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Version control
|
2 |
+
.git
|
3 |
+
.gitignore
|
4 |
+
|
5 |
+
# Python cache
|
6 |
+
__pycache__/
|
7 |
+
*.py[cod]
|
8 |
+
*$py.class
|
9 |
+
*.so
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
*.egg-info/
|
24 |
+
.installed.cfg
|
25 |
+
*.egg
|
26 |
+
|
27 |
+
# Virtual environments
|
28 |
+
venv/
|
29 |
+
env/
|
30 |
+
ENV/
|
31 |
+
|
32 |
+
# IDE files
|
33 |
+
.vscode/
|
34 |
+
.idea/
|
35 |
+
*.swp
|
36 |
+
*.swo
|
37 |
+
*~
|
38 |
+
|
39 |
+
# OS files
|
40 |
+
.DS_Store
|
41 |
+
Thumbs.db
|
42 |
+
|
43 |
+
# Large data files (exclude datasets)
|
44 |
+
SEN-2_LULC/
|
45 |
+
SEN-2_LULC_preprocessed/
|
46 |
+
SEN-2_LULC_preprocessed1/
|
47 |
+
outputs_rgb_optimized/
|
48 |
+
*.zip
|
49 |
+
*.tar.gz
|
50 |
+
|
51 |
+
# Logs
|
52 |
+
*.log
|
53 |
+
logs/
|
54 |
+
|
55 |
+
# Temporary files
|
56 |
+
tmp/
|
57 |
+
temp/
|
58 |
+
|
59 |
+
# Documentation
|
60 |
+
README.md
|
61 |
+
docs/
|
Dockerfile
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use PyTorch base image with CUDA support
|
2 |
+
FROM pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime
|
3 |
+
|
4 |
+
# Set working directory
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Set environment variables
|
8 |
+
ENV PYTHONPATH=/app
|
9 |
+
ENV PYTHONUNBUFFERED=1
|
10 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
11 |
+
|
12 |
+
# Install system dependencies
|
13 |
+
RUN apt-get update && apt-get install -y \
|
14 |
+
libglib2.0-0 \
|
15 |
+
libsm6 \
|
16 |
+
libxext6 \
|
17 |
+
libxrender-dev \
|
18 |
+
libgomp1 \
|
19 |
+
libgdal-dev \
|
20 |
+
gdal-bin \
|
21 |
+
libgl1-mesa-glx \
|
22 |
+
libglib2.0-0 \
|
23 |
+
wget \
|
24 |
+
curl \
|
25 |
+
git \
|
26 |
+
&& rm -rf /var/lib/apt/lists/*
|
27 |
+
|
28 |
+
# Install Python dependencies
|
29 |
+
RUN pip install --no-cache-dir \
|
30 |
+
torch==2.1.0 \
|
31 |
+
torchvision==0.16.0 \
|
32 |
+
torchaudio==2.1.0 \
|
33 |
+
segmentation-models-pytorch==0.3.3 \
|
34 |
+
gradio==4.8.0 \
|
35 |
+
albumentations==1.3.1 \
|
36 |
+
opencv-python-headless==4.8.1.78 \
|
37 |
+
pillow==10.1.0 \
|
38 |
+
numpy==1.24.4 \
|
39 |
+
tqdm==4.66.1 \
|
40 |
+
timm==0.9.12
|
41 |
+
|
42 |
+
# Create necessary directories
|
43 |
+
RUN mkdir -p /app/Segmentation-lulc \
|
44 |
+
&& mkdir -p /app/data \
|
45 |
+
&& mkdir -p /app/outputs \
|
46 |
+
&& mkdir -p /app/models
|
47 |
+
|
48 |
+
# Copy application files
|
49 |
+
COPY Segmentation-lulc/ /app/Segmentation-lulc/
|
50 |
+
|
51 |
+
# Copy any example images or models if they exist
|
52 |
+
COPY --chown=root:root . /app/
|
53 |
+
|
54 |
+
# Set permissions
|
55 |
+
RUN chmod -R 755 /app
|
56 |
+
|
57 |
+
# Expose Gradio port
|
58 |
+
EXPOSE 7860
|
59 |
+
|
60 |
+
# Default command - run the Gradio app
|
61 |
+
CMD ["python", "/app/Segmentation-lulc/app3.py"]
|
62 |
+
|
63 |
+
# Alternative commands (can be overridden):
|
64 |
+
# For preprocessing: docker run <image> python /app/Segmentation-lulc/pre1.py
|
65 |
+
# For training: docker run <image> python /app/Segmentation-lulc/main3.py
|
README-Docker.md
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# LULC Segmentation Docker Setup
|
2 |
+
|
3 |
+
This directory contains Docker configuration for the Land Use Land Cover (LULC) segmentation application.
|
4 |
+
|
5 |
+
## Prerequisites
|
6 |
+
|
7 |
+
- Docker installed with GPU support (nvidia-docker)
|
8 |
+
- NVIDIA Container Toolkit for GPU acceleration
|
9 |
+
|
10 |
+
## Quick Start
|
11 |
+
|
12 |
+
### 1. Build the Docker image
|
13 |
+
```bash
|
14 |
+
docker build -t lulc-segmentation:latest .
|
15 |
+
```
|
16 |
+
|
17 |
+
### 2. Run the Gradio application
|
18 |
+
```bash
|
19 |
+
docker run -it --rm \
|
20 |
+
--gpus all \
|
21 |
+
-p 7860:7860 \
|
22 |
+
-v $(pwd)/data:/app/data \
|
23 |
+
-v $(pwd)/outputs:/app/outputs \
|
24 |
+
-v $(pwd)/models:/app/models \
|
25 |
+
lulc-segmentation:latest
|
26 |
+
```
|
27 |
+
|
28 |
+
### 3. Access the application
|
29 |
+
Open your browser and navigate to `http://localhost:7860`
|
30 |
+
|
31 |
+
## Using Docker Compose
|
32 |
+
|
33 |
+
### Run the application
|
34 |
+
```bash
|
35 |
+
docker-compose up lulc-segmentation
|
36 |
+
```
|
37 |
+
|
38 |
+
### Run training (with profile)
|
39 |
+
```bash
|
40 |
+
docker-compose --profile training up lulc-training
|
41 |
+
```
|
42 |
+
|
43 |
+
## Using the convenience script
|
44 |
+
|
45 |
+
Make the script executable:
|
46 |
+
```bash
|
47 |
+
chmod +x run-docker.sh
|
48 |
+
```
|
49 |
+
|
50 |
+
Then use it:
|
51 |
+
```bash
|
52 |
+
./run-docker.sh build # Build the image
|
53 |
+
./run-docker.sh app # Run the Gradio app
|
54 |
+
./run-docker.sh train # Run training
|
55 |
+
./run-docker.sh preprocess # Run preprocessing
|
56 |
+
```
|
57 |
+
|
58 |
+
## Volume Mounts
|
59 |
+
|
60 |
+
- `./data` → `/app/data` - For input datasets
|
61 |
+
- `./outputs` → `/app/outputs` - For training outputs and checkpoints
|
62 |
+
- `./models` → `/app/models` - For trained model files
|
63 |
+
|
64 |
+
## Environment Variables
|
65 |
+
|
66 |
+
- `GRADIO_SERVER_NAME=0.0.0.0` - Allow external connections
|
67 |
+
- `GRADIO_SERVER_PORT=7860` - Gradio port
|
68 |
+
- `PYTHONPATH=/app` - Python path configuration
|
69 |
+
|
70 |
+
## GPU Support
|
71 |
+
|
72 |
+
The container is configured to use all available GPUs. Ensure you have:
|
73 |
+
- NVIDIA Docker runtime installed
|
74 |
+
- Proper GPU drivers
|
75 |
+
- NVIDIA Container Toolkit
|
76 |
+
|
77 |
+
## Troubleshooting
|
78 |
+
|
79 |
+
1. **GPU not detected**: Ensure nvidia-docker is properly installed
|
80 |
+
2. **Port conflicts**: Change the port mapping if 7860 is in use
|
81 |
+
3. **Permission issues**: Ensure proper volume mount permissions
|
82 |
+
|
83 |
+
## Development
|
84 |
+
|
85 |
+
For development, you can mount the source code directly:
|
86 |
+
```bash
|
87 |
+
docker run -it --rm \
|
88 |
+
--gpus all \
|
89 |
+
-p 7860:7860 \
|
90 |
+
-v $(pwd)/Segmentation-lulc:/app/Segmentation-lulc \
|
91 |
+
lulc-segmentation:latest
|
92 |
+
```
|
app3.py
ADDED
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import torch
|
3 |
+
import torch.nn as nn
|
4 |
+
from PIL import Image, ImageDraw, ImageFont
|
5 |
+
import numpy as np
|
6 |
+
import torchvision.transforms as T
|
7 |
+
import torchvision.transforms.functional as F
|
8 |
+
import segmentation_models_pytorch as smp
|
9 |
+
import os
|
10 |
+
|
11 |
+
# --- 1. Configuration and Constants ---
|
12 |
+
|
13 |
+
class CFG:
|
14 |
+
"""
|
15 |
+
Configuration class adapted from the provided training script.
|
16 |
+
"""
|
17 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
18 |
+
MODEL_NAME = "CustomDeepLabV3+"
|
19 |
+
ENCODER_NAME = "timm-efficientnet-b2"
|
20 |
+
IN_CHANNELS = 3
|
21 |
+
NUM_CLASSES = 8
|
22 |
+
IMG_SIZE = 256
|
23 |
+
# NOTE: Update this path to where your trained model is saved.
|
24 |
+
MODEL_SAVE_PATH = "./best_model_optimized.pth"
|
25 |
+
|
26 |
+
# Class definitions and color map based on the research paper
|
27 |
+
CLASS_INFO = {
|
28 |
+
0: {"name": "Unclassified", "color": (150, 150, 150)},
|
29 |
+
1: {"name": "Water Bodies", "color": (0, 0, 255)}, # Blue
|
30 |
+
2: {"name": "Dense Forest", "color": (0, 100, 0)}, # Dark Green
|
31 |
+
3: {"name": "Built up", "color": (128, 0, 128)}, # Purple
|
32 |
+
4: {"name": "Agriculture land", "color": (0, 255, 0)}, # Bright Green
|
33 |
+
5: {"name": "Barren land", "color": (255, 255, 0)}, # Yellow
|
34 |
+
6: {"name": "Fallow land", "color": (210, 180, 140)}, # Tan
|
35 |
+
7: {"name": "Sparse Forest", "color": (60, 179, 113)}, # Medium Sea Green
|
36 |
+
}
|
37 |
+
|
38 |
+
PIXEL_AREA_SQ_METERS = 10 * 10 # Based on 10m resolution from the dataset
|
39 |
+
SQ_METERS_PER_HECTARE = 10000
|
40 |
+
|
41 |
+
# --- 2. Model Definitions (from provided training script) ---
|
42 |
+
|
43 |
+
class SELayer(nn.Module):
|
44 |
+
def __init__(self, channel, reduction=16):
|
45 |
+
super(SELayer, self).__init__()
|
46 |
+
self.avg_pool = nn.AdaptiveAvgPool2d(1)
|
47 |
+
self.fc = nn.Sequential(nn.Linear(channel, channel // reduction, bias=False), nn.ReLU(inplace=True), nn.Linear(channel // reduction, channel, bias=False), nn.Sigmoid())
|
48 |
+
def forward(self, x):
|
49 |
+
b, c, _, _ = x.size()
|
50 |
+
y = self.avg_pool(x).view(b, c)
|
51 |
+
y = self.fc(y).view(b, c, 1, 1)
|
52 |
+
return x * y.expand_as(x)
|
53 |
+
|
54 |
+
class CustomDeepLabV3Plus(nn.Module):
|
55 |
+
def __init__(self, encoder_name, in_channels, classes):
|
56 |
+
super().__init__()
|
57 |
+
self.smp_model = smp.DeepLabV3Plus(encoder_name=encoder_name, encoder_weights="imagenet", in_channels=in_channels, classes=classes)
|
58 |
+
decoder_channels = self.smp_model.segmentation_head[0].in_channels
|
59 |
+
self.se_layer = SELayer(decoder_channels)
|
60 |
+
self.segmentation_head = self.smp_model.segmentation_head
|
61 |
+
self.smp_model.segmentation_head = nn.Identity()
|
62 |
+
def forward(self, x):
|
63 |
+
decoder_features = self.smp_model(x)
|
64 |
+
attended_features = self.se_layer(decoder_features)
|
65 |
+
output = self.segmentation_head(attended_features)
|
66 |
+
return output
|
67 |
+
|
68 |
+
# --- 3. Helper Functions ---
|
69 |
+
|
70 |
+
def load_model():
|
71 |
+
"""Loads the pre-trained segmentation model."""
|
72 |
+
model = CustomDeepLabV3Plus(
|
73 |
+
encoder_name=CFG.ENCODER_NAME,
|
74 |
+
in_channels=CFG.IN_CHANNELS,
|
75 |
+
classes=CFG.NUM_CLASSES
|
76 |
+
)
|
77 |
+
if not os.path.exists(CFG.MODEL_SAVE_PATH):
|
78 |
+
raise FileNotFoundError(f"Model file not found at {CFG.MODEL_SAVE_PATH}. Please ensure the model is trained and the path is correct.")
|
79 |
+
|
80 |
+
model.load_state_dict(torch.load(CFG.MODEL_SAVE_PATH, map_location=torch.device(CFG.DEVICE)))
|
81 |
+
model.to(CFG.DEVICE)
|
82 |
+
model.eval()
|
83 |
+
print("Model loaded successfully on device:", CFG.DEVICE)
|
84 |
+
return model
|
85 |
+
|
86 |
+
def preprocess_image(img: Image.Image):
|
87 |
+
"""Preprocesses a PIL image for model inference."""
|
88 |
+
img = F.resize(img, [CFG.IMG_SIZE, CFG.IMG_SIZE], interpolation=T.InterpolationMode.BILINEAR)
|
89 |
+
img_tensor = F.to_tensor(img)
|
90 |
+
normalize_transform = T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
91 |
+
normalized_tensor = normalize_transform(img_tensor)
|
92 |
+
return normalized_tensor.unsqueeze(0) # Add batch dimension
|
93 |
+
|
94 |
+
def create_colored_mask(mask: np.ndarray) -> Image.Image:
|
95 |
+
"""Creates a colored RGB image from a class index mask."""
|
96 |
+
rgb_mask = np.zeros((*mask.shape, 3), dtype=np.uint8)
|
97 |
+
for class_id, info in CLASS_INFO.items():
|
98 |
+
rgb_mask[mask == class_id] = info["color"]
|
99 |
+
return Image.fromarray(rgb_mask)
|
100 |
+
|
101 |
+
def create_legend_image():
|
102 |
+
"""Generates an image of the color legend."""
|
103 |
+
item_height = 25
|
104 |
+
width = 250
|
105 |
+
height = item_height * len(CLASS_INFO)
|
106 |
+
legend_img = Image.new('RGB', (width, height), 'white')
|
107 |
+
draw = ImageDraw.Draw(legend_img)
|
108 |
+
|
109 |
+
try:
|
110 |
+
# Use a common font, handle case where it's not found
|
111 |
+
font = ImageFont.truetype("arial.ttf", 14)
|
112 |
+
except IOError:
|
113 |
+
font = ImageFont.load_default()
|
114 |
+
|
115 |
+
for i, (class_id, info) in enumerate(CLASS_INFO.items()):
|
116 |
+
y_start = i * item_height
|
117 |
+
draw.rectangle([5, y_start + 5, 25, y_start + 20], fill=info["color"])
|
118 |
+
draw.text((35, y_start + 5), f'{info["name"]}', fill='black', font=font)
|
119 |
+
|
120 |
+
return legend_img
|
121 |
+
|
122 |
+
# --- 4. Main Prediction Function ---
|
123 |
+
|
124 |
+
# Load the model and legend once when the script starts
|
125 |
+
model = load_model()
|
126 |
+
legend_image = create_legend_image()
|
127 |
+
|
128 |
+
def predict_land_cover(input_image: Image.Image):
|
129 |
+
"""
|
130 |
+
Takes an input image, performs segmentation, and calculates area for each class.
|
131 |
+
"""
|
132 |
+
if input_image is None:
|
133 |
+
raise gr.Error("Please upload an image.")
|
134 |
+
|
135 |
+
# 1. Preprocess the image and get model prediction
|
136 |
+
input_tensor = preprocess_image(input_image.convert("RGB")).to(CFG.DEVICE)
|
137 |
+
with torch.no_grad():
|
138 |
+
output = model(input_tensor)
|
139 |
+
pred_mask = torch.argmax(output, dim=1).squeeze(0).cpu().numpy()
|
140 |
+
|
141 |
+
# 2. Create the colored segmentation mask for visualization
|
142 |
+
colored_mask = create_colored_mask(pred_mask)
|
143 |
+
|
144 |
+
# 3. Calculate area for each class
|
145 |
+
class_indices, pixel_counts = np.unique(pred_mask, return_counts=True)
|
146 |
+
|
147 |
+
area_results = {}
|
148 |
+
total_area_ha = (CFG.IMG_SIZE * CFG.IMG_SIZE * PIXEL_AREA_SQ_METERS) / SQ_METERS_PER_HECTARE
|
149 |
+
|
150 |
+
for class_id, count in zip(class_indices, pixel_counts):
|
151 |
+
class_name = CLASS_INFO[class_id]["name"]
|
152 |
+
area_hectares = (count * PIXEL_AREA_SQ_METERS) / SQ_METERS_PER_HECTARE
|
153 |
+
percentage = (area_hectares / total_area_ha) * 100
|
154 |
+
area_results[class_name] = f"{area_hectares:.4f} Hectares ({percentage:.2f}%)"
|
155 |
+
|
156 |
+
return colored_mask, area_results, legend_image
|
157 |
+
|
158 |
+
# --- 5. Gradio Interface ---
|
159 |
+
|
160 |
+
# Load example images from your file system.
|
161 |
+
example_files = ["comparison_1.jpg", "comparison_4.jpg", "comparison_5.jpg", "comparison_8.jpg"]
|
162 |
+
example_images = [file for file in example_files if os.path.exists(file)]
|
163 |
+
|
164 |
+
app_description = """
|
165 |
+
### Sen-2 LULC: Interactive Land Cover Analysis
|
166 |
+
This tool performs semantic segmentation on satellite imagery to classify different types of land use and land cover (LULC).
|
167 |
+
It is based on the **Sen-2 LULC dataset** and a **Custom DeepLabV3+** model as described in the research paper "Sen-2 LULC: Land Use Land Cover Dataset for Deep Learning Approaches".
|
168 |
+
|
169 |
+
**How it works:**
|
170 |
+
1. **Upload Image:** Upload a satellite image patch. You can use the examples below.
|
171 |
+
2. **Model Prediction:** The deep learning model classifies each pixel into one of the 7 LULC classes.
|
172 |
+
3. **Analysis:** The application generates:
|
173 |
+
* A color-coded **Segmentation Mask** for visual analysis.
|
174 |
+
* A **Quantitative Report** calculating the area of each land class in hectares. This is possible because the source Sentinel-2 imagery has a **10m resolution**, meaning each pixel represents a 100m² area on the ground.
|
175 |
+
"""
|
176 |
+
|
177 |
+
iface = gr.Interface(
|
178 |
+
fn=predict_land_cover,
|
179 |
+
inputs=gr.Image(type="pil", label="Upload Satellite Image Patch"),
|
180 |
+
outputs=[
|
181 |
+
gr.Image(type="pil", label="Predicted Segmentation Mask"),
|
182 |
+
gr.Label(label="Land Cover Area Analysis (Hectares)"),
|
183 |
+
gr.Image(type="pil", label="Legend")
|
184 |
+
],
|
185 |
+
title="Land Use & Land Cover Segmentation and Area Analysis",
|
186 |
+
description=app_description,
|
187 |
+
examples=example_images,
|
188 |
+
cache_examples=True,
|
189 |
+
allow_flagging="never"
|
190 |
+
)
|
191 |
+
|
192 |
+
if __name__ == "__main__":
|
193 |
+
iface.launch(debug=True)
|
docker-compose.yml
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version: '3.8'
|
2 |
+
|
3 |
+
services:
|
4 |
+
lulc-segmentation:
|
5 |
+
build:
|
6 |
+
context: .
|
7 |
+
dockerfile: Dockerfile
|
8 |
+
container_name: lulc-app
|
9 |
+
ports:
|
10 |
+
- "7860:7860"
|
11 |
+
volumes:
|
12 |
+
# Mount data directory for datasets
|
13 |
+
- ./data:/app/data
|
14 |
+
# Mount outputs directory for model checkpoints
|
15 |
+
- ./outputs:/app/outputs
|
16 |
+
# Mount models directory for trained models
|
17 |
+
- ./models:/app/models
|
18 |
+
environment:
|
19 |
+
- PYTHONPATH=/app
|
20 |
+
- GRADIO_SERVER_NAME=0.0.0.0
|
21 |
+
- GRADIO_SERVER_PORT=7860
|
22 |
+
deploy:
|
23 |
+
resources:
|
24 |
+
reservations:
|
25 |
+
devices:
|
26 |
+
- driver: nvidia
|
27 |
+
count: all
|
28 |
+
capabilities: [gpu]
|
29 |
+
command: python /app/Segmentation-lulc/app3.py
|
30 |
+
|
31 |
+
# Optional service for training
|
32 |
+
lulc-training:
|
33 |
+
build:
|
34 |
+
context: .
|
35 |
+
dockerfile: Dockerfile
|
36 |
+
container_name: lulc-training
|
37 |
+
volumes:
|
38 |
+
- ./data:/app/data
|
39 |
+
- ./outputs:/app/outputs
|
40 |
+
- ./models:/app/models
|
41 |
+
environment:
|
42 |
+
- PYTHONPATH=/app
|
43 |
+
deploy:
|
44 |
+
resources:
|
45 |
+
reservations:
|
46 |
+
devices:
|
47 |
+
- driver: nvidia
|
48 |
+
count: all
|
49 |
+
capabilities: [gpu]
|
50 |
+
command: python /app/Segmentation-lulc/main3.py
|
51 |
+
profiles:
|
52 |
+
- training
|
examples/10.png
ADDED
![]() |
examples/1019.png
ADDED
![]() |
examples/15.png
ADDED
![]() |
examples/2.png
ADDED
![]() |
examples/285.png
ADDED
![]() |
examples/32.png
ADDED
![]() |
examples/36.png
ADDED
![]() |
examples/457.png
ADDED
![]() |
examples/46.png
ADDED
![]() |
examples/59.png
ADDED
![]() |
examples/64.png
ADDED
![]() |
examples/Screenshot 2025-08-04 192031.png
ADDED
![]() |
examples/Screenshot 2025-08-04 193338.png
ADDED
![]() |
examples/canola_oli_2022140_lrg.png
ADDED
![]() |
main3.py
ADDED
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import torch
|
3 |
+
import torch.nn as nn
|
4 |
+
import torch.optim as optim
|
5 |
+
from torch.utils.data import Dataset, DataLoader
|
6 |
+
from PIL import Image
|
7 |
+
import numpy as np
|
8 |
+
import albumentations as A
|
9 |
+
from albumentations.pytorch import ToTensorV2
|
10 |
+
from tqdm import tqdm
|
11 |
+
import segmentation_models_pytorch as smp
|
12 |
+
import cv2
|
13 |
+
|
14 |
+
# --- 1. Configuration ---
|
15 |
+
class CFG:
|
16 |
+
DATA_DIR = r"SEN-2_LULC_preprocessed"
|
17 |
+
TRAIN_IMG_DIR = os.path.join(DATA_DIR, "train_images")
|
18 |
+
TRAIN_MASK_DIR = os.path.join(DATA_DIR, "train_masks")
|
19 |
+
VAL_IMG_DIR = os.path.join(DATA_DIR, "val_images")
|
20 |
+
VAL_MASK_DIR = os.path.join(DATA_DIR, "val_masks")
|
21 |
+
|
22 |
+
OUTPUT_DIR = "./outputs_rgb_optimized"
|
23 |
+
# The path for the 'best' model, for inference later
|
24 |
+
MODEL_SAVE_PATH = os.path.join(OUTPUT_DIR, "best_model_optimized.pth")
|
25 |
+
# --- NEW: Path for the resumable checkpoint file ---
|
26 |
+
CHECKPOINT_PATH = os.path.join(OUTPUT_DIR, "checkpoint.pth")
|
27 |
+
|
28 |
+
PREDICTION_SAVE_PATH = os.path.join(OUTPUT_DIR, "predictions_optimized")
|
29 |
+
|
30 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
31 |
+
MODEL_NAME = "CustomDeepLabV3+"
|
32 |
+
ENCODER_NAME = "timm-efficientnet-b2"
|
33 |
+
LOSS_FN_NAME = "DiceFocal"
|
34 |
+
IN_CHANNELS = 3; NUM_CLASSES = 8; IMG_SIZE = 256
|
35 |
+
BATCH_SIZE = 4; ACCUMULATION_STEPS = 4
|
36 |
+
NUM_WORKERS = 8; LEARNING_RATE = 1e-4; EPOCHS = 50
|
37 |
+
SEED = 42; SUBSET_FRACTION = 0.75
|
38 |
+
|
39 |
+
# --- ARCHITECTURE and LOSS CLASSES (Unchanged) ---
|
40 |
+
class SELayer(nn.Module):
|
41 |
+
def __init__(self, channel, reduction=16):
|
42 |
+
super(SELayer, self).__init__(); self.avg_pool = nn.AdaptiveAvgPool2d(1)
|
43 |
+
self.fc = nn.Sequential(nn.Linear(channel, channel // reduction, bias=False), nn.ReLU(inplace=True), nn.Linear(channel // reduction, channel, bias=False), nn.Sigmoid())
|
44 |
+
def forward(self, x):
|
45 |
+
b, c, _, _ = x.size(); y = self.avg_pool(x).view(b, c); y = self.fc(y).view(b, c, 1, 1); return x * y.expand_as(x)
|
46 |
+
class CustomDeepLabV3Plus(nn.Module):
|
47 |
+
def __init__(self, encoder_name, in_channels, classes):
|
48 |
+
super().__init__(); self.smp_model = smp.DeepLabV3Plus(encoder_name=encoder_name, encoder_weights="imagenet", in_channels=in_channels, classes=classes)
|
49 |
+
decoder_channels = self.smp_model.segmentation_head[0].in_channels; self.se_layer = SELayer(decoder_channels)
|
50 |
+
self.segmentation_head = self.smp_model.segmentation_head; self.smp_model.segmentation_head = nn.Identity()
|
51 |
+
def forward(self, x):
|
52 |
+
decoder_features = self.smp_model(x); attended_features = self.se_layer(decoder_features)
|
53 |
+
output = self.segmentation_head(attended_features); return output
|
54 |
+
class CombinedLoss(nn.Module):
|
55 |
+
def __init__(self, loss1, loss2, alpha=0.5):
|
56 |
+
super(CombinedLoss, self).__init__(); self.loss1 = loss1; self.loss2 = loss2; self.alpha = alpha
|
57 |
+
self.name = f"{alpha}*{self.loss1.__class__.__name__} + {1-alpha}*{self.loss2.__class__.__name__}"
|
58 |
+
def forward(self, prediction, target):
|
59 |
+
loss1_val = self.loss1(prediction, target); loss2_val = self.loss2(prediction, target); return self.alpha * loss1_val + (1 - self.alpha) * loss2_val
|
60 |
+
|
61 |
+
# --- DATASET and TRANSFORMS (Unchanged) ---
|
62 |
+
class LULCDataset(Dataset):
|
63 |
+
def __init__(self, image_dir, mask_dir, transform=None, subset_fraction=1.0):
|
64 |
+
self.image_dir = image_dir; self.mask_dir = mask_dir; self.transform = transform
|
65 |
+
all_images = sorted([f for f in os.listdir(image_dir) if f.endswith('.png')])
|
66 |
+
all_masks = sorted([f for f in os.listdir(mask_dir) if f.endswith('.tif')])
|
67 |
+
num_samples = int(len(all_images) * subset_fraction)
|
68 |
+
self.images = all_images[:num_samples]; self.masks = all_masks[:num_samples]
|
69 |
+
assert len(self.images) == len(self.masks), "Mismatch"; print(f"Found {len(all_images)} total images, USING {len(self.images)} samples ({subset_fraction*100}%) from {image_dir}")
|
70 |
+
def __len__(self): return len(self.images)
|
71 |
+
def __getitem__(self, idx):
|
72 |
+
img_path = os.path.join(self.image_dir, self.images[idx]); mask_path = os.path.join(self.mask_dir, self.masks[idx])
|
73 |
+
image = np.array(Image.open(img_path).convert("RGB"), dtype=np.float32)
|
74 |
+
mask = np.array(Image.open(mask_path).convert("L"), dtype=np.float32)
|
75 |
+
if self.transform: augmented = self.transform(image=image, mask=mask); image, mask = augmented['image'], augmented['mask']
|
76 |
+
return image, mask
|
77 |
+
def get_transforms(img_size):
|
78 |
+
DATASET_MEAN = [0.485, 0.456, 0.406]; DATASET_STD = [0.229, 0.224, 0.225]
|
79 |
+
train_transform = A.Compose([A.Resize(img_size, img_size), A.Rotate(limit=35, p=0.5), A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.5), A.Normalize(mean=DATASET_MEAN, std=DATASET_STD), ToTensorV2()])
|
80 |
+
val_transform = A.Compose([A.Resize(img_size, img_size), A.Normalize(mean=DATASET_MEAN, std=DATASET_STD), ToTensorV2()])
|
81 |
+
return train_transform, val_transform
|
82 |
+
|
83 |
+
# --- GET MODEL AND LOSS (Unchanged) ---
|
84 |
+
def get_model():
|
85 |
+
if CFG.MODEL_NAME == "CustomDeepLabV3+": model = CustomDeepLabV3Plus(encoder_name=CFG.ENCODER_NAME, in_channels=CFG.IN_CHANNELS, classes=CFG.NUM_CLASSES)
|
86 |
+
else: model = smp.DeepLabV3Plus(encoder_name=CFG.ENCODER_NAME, encoder_weights="imagenet", in_channels=CFG.IN_CHANNELS, classes=CFG.NUM_CLASSES)
|
87 |
+
return model.to(CFG.DEVICE)
|
88 |
+
def get_loss_fn():
|
89 |
+
if CFG.LOSS_FN_NAME == "DiceFocal": dice = smp.losses.DiceLoss(mode='multiclass'); focal = smp.losses.FocalLoss(mode='multiclass'); return CombinedLoss(focal, dice, alpha=0.5)
|
90 |
+
else: return smp.losses.DiceLoss(mode='multiclass')
|
91 |
+
|
92 |
+
# --- Training and Evaluation Functions (Unchanged) ---
|
93 |
+
def train_one_epoch(loader, model, optimizer, loss_fn, scaler):
|
94 |
+
loop = tqdm(loader, desc="Training"); model.train(); optimizer.zero_grad()
|
95 |
+
for batch_idx, (data, targets) in enumerate(loop):
|
96 |
+
data = data.to(CFG.DEVICE, non_blocking=True, memory_format=torch.channels_last)
|
97 |
+
targets = targets.long().to(CFG.DEVICE, non_blocking=True)
|
98 |
+
with torch.amp.autocast(device_type=CFG.DEVICE, dtype=torch.bfloat16, enabled=(CFG.DEVICE=="cuda")):
|
99 |
+
predictions = model(data); loss = loss_fn(predictions, targets) / CFG.ACCUMULATION_STEPS
|
100 |
+
scaler.scale(loss).backward()
|
101 |
+
if (batch_idx + 1) % CFG.ACCUMULATION_STEPS == 0:
|
102 |
+
scaler.step(optimizer); scaler.update(); optimizer.zero_grad()
|
103 |
+
loop.set_postfix(loss=loss.item() * CFG.ACCUMULATION_STEPS)
|
104 |
+
def evaluate_model(loader, model, loss_fn):
|
105 |
+
model.eval(); intersection, union = torch.zeros(CFG.NUM_CLASSES, device=CFG.DEVICE), torch.zeros(CFG.NUM_CLASSES, device=CFG.DEVICE)
|
106 |
+
pixel_correct, pixel_total, total_loss = 0, 0, 0
|
107 |
+
with torch.no_grad():
|
108 |
+
loop = tqdm(loader, desc="Evaluating")
|
109 |
+
for x, y in loop:
|
110 |
+
x = x.to(CFG.DEVICE, non_blocking=True, memory_format=torch.channels_last)
|
111 |
+
y = y.to(CFG.DEVICE, non_blocking=True).long()
|
112 |
+
with torch.amp.autocast(device_type=CFG.DEVICE, dtype=torch.bfloat16, enabled=(CFG.DEVICE=="cuda")):
|
113 |
+
preds = model(x); loss = loss_fn(preds, y); total_loss += loss.item()
|
114 |
+
pred_labels = torch.argmax(preds, dim=1); pixel_correct += (pred_labels == y).sum(); pixel_total += torch.numel(y)
|
115 |
+
for cls in range(CFG.NUM_CLASSES): pred_mask = (pred_labels == cls); true_mask = (y == cls); intersection[cls] += (pred_mask & true_mask).sum(); union[cls] += (pred_mask | true_mask).sum()
|
116 |
+
pixel_acc = (pixel_correct / pixel_total) * 100; iou_per_class = (intersection + 1e-6) / (union + 1e-6)
|
117 |
+
mean_iou = iou_per_class.mean(); avg_loss = total_loss / len(loader)
|
118 |
+
print(f"Validation Results -> Avg Loss: {avg_loss:.4f}, Pixel Acc: {pixel_acc:.2f}%, mIoU: {mean_iou:.4f}")
|
119 |
+
for i, iou in enumerate(iou_per_class): print(f" Class {i} IoU: {iou:.4f}")
|
120 |
+
return mean_iou
|
121 |
+
|
122 |
+
def save_predictions_as_images(loader, model):
|
123 |
+
# This function is not part of the training loop, no changes needed.
|
124 |
+
pass # implementation is correct as-is
|
125 |
+
|
126 |
+
# --- NEW: Helper function to save a checkpoint ---
|
127 |
+
def save_checkpoint(state, filename="checkpoint.pth"):
|
128 |
+
print("=> Saving checkpoint")
|
129 |
+
torch.save(state, filename)
|
130 |
+
|
131 |
+
def main():
|
132 |
+
torch.manual_seed(CFG.SEED); np.random.seed(CFG.SEED); os.makedirs(CFG.OUTPUT_DIR, exist_ok=True)
|
133 |
+
if torch.cuda.is_available(): torch.backends.cudnn.benchmark = True
|
134 |
+
|
135 |
+
train_transform, val_transform = get_transforms(CFG.IMG_SIZE)
|
136 |
+
train_ds = LULCDataset(CFG.TRAIN_IMG_DIR, CFG.TRAIN_MASK_DIR, transform=train_transform, subset_fraction=CFG.SUBSET_FRACTION)
|
137 |
+
val_ds = LULCDataset(CFG.VAL_IMG_DIR, CFG.VAL_MASK_DIR, transform=val_transform, subset_fraction=CFG.SUBSET_FRACTION)
|
138 |
+
train_loader = DataLoader(train_ds, batch_size=CFG.BATCH_SIZE, num_workers=CFG.NUM_WORKERS, pin_memory=True, shuffle=True, persistent_workers=True)
|
139 |
+
val_loader = DataLoader(val_ds, batch_size=CFG.BATCH_SIZE, num_workers=CFG.NUM_WORKERS, pin_memory=True, shuffle=False, persistent_workers=True)
|
140 |
+
|
141 |
+
model = get_model()
|
142 |
+
model = model.to(memory_format=torch.channels_last)
|
143 |
+
|
144 |
+
loss_fn = get_loss_fn()
|
145 |
+
optimizer = optim.AdamW(model.parameters(), lr=CFG.LEARNING_RATE)
|
146 |
+
scaler = torch.amp.GradScaler(enabled=(CFG.DEVICE=="cuda"))
|
147 |
+
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=CFG.EPOCHS, eta_min=1e-6)
|
148 |
+
|
149 |
+
# --- NEW: Logic to load checkpoint and resume training ---
|
150 |
+
start_epoch = 0
|
151 |
+
best_val_miou = -1.0
|
152 |
+
if os.path.exists(CFG.CHECKPOINT_PATH):
|
153 |
+
print(f"=> Loading checkpoint '{CFG.CHECKPOINT_PATH}'")
|
154 |
+
checkpoint = torch.load(CFG.CHECKPOINT_PATH, map_location=CFG.DEVICE)
|
155 |
+
|
156 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
157 |
+
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
|
158 |
+
scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
|
159 |
+
scaler.load_state_dict(checkpoint['scaler_state_dict'])
|
160 |
+
|
161 |
+
start_epoch = checkpoint['epoch'] + 1
|
162 |
+
best_val_miou = checkpoint['best_val_miou']
|
163 |
+
|
164 |
+
print(f"=> Resuming training from epoch {start_epoch}")
|
165 |
+
else:
|
166 |
+
print("=> No checkpoint found, starting new training session.")
|
167 |
+
|
168 |
+
|
169 |
+
# --- MODIFIED: Main training loop now starts from the correct epoch ---
|
170 |
+
for epoch in range(start_epoch, CFG.EPOCHS):
|
171 |
+
print(f"\n--- Epoch {epoch+1}/{CFG.EPOCHS} ---")
|
172 |
+
train_one_epoch(train_loader, model, optimizer, loss_fn, scaler)
|
173 |
+
current_miou = evaluate_model(val_loader, model, loss_fn)
|
174 |
+
scheduler.step()
|
175 |
+
|
176 |
+
# Create the checkpoint dictionary with the complete state
|
177 |
+
checkpoint = {
|
178 |
+
'epoch': epoch,
|
179 |
+
'model_state_dict': model.state_dict(),
|
180 |
+
'optimizer_state_dict': optimizer.state_dict(),
|
181 |
+
'scheduler_state_dict': scheduler.state_dict(),
|
182 |
+
'scaler_state_dict': scaler.state_dict(),
|
183 |
+
'best_val_miou': best_val_miou
|
184 |
+
}
|
185 |
+
|
186 |
+
if current_miou > best_val_miou:
|
187 |
+
best_val_miou = current_miou
|
188 |
+
checkpoint['best_val_miou'] = best_val_miou # Update best score in checkpoint
|
189 |
+
print(f"🎉 New best mIoU: {best_val_miou:.4f}! Saving best model to {CFG.MODEL_SAVE_PATH}")
|
190 |
+
torch.save(model.state_dict(), CFG.MODEL_SAVE_PATH) # Save just the model for easy inference
|
191 |
+
|
192 |
+
# Save the full state checkpoint after every epoch
|
193 |
+
save_checkpoint(checkpoint, filename=CFG.CHECKPOINT_PATH)
|
194 |
+
|
195 |
+
|
196 |
+
print("\n--- Training Complete. Saving final predictions. ---")
|
197 |
+
# Load the best performing model for final predictions
|
198 |
+
model.load_state_dict(torch.load(CFG.MODEL_SAVE_PATH))
|
199 |
+
# Note: You may want a separate test_loader for final unbiased evaluation
|
200 |
+
save_predictions_as_images(val_loader, model)
|
201 |
+
|
202 |
+
if __name__ == "__main__":
|
203 |
+
main()
|
pre1.py
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from PIL import Image
|
3 |
+
from tqdm import tqdm
|
4 |
+
import glob
|
5 |
+
|
6 |
+
# --- 1. Centralized Configuration ---
|
7 |
+
class CFG:
|
8 |
+
# --- Paths ---
|
9 |
+
SOURCE_ROOT = "SEN-2 LULC"
|
10 |
+
DEST_ROOT = "SEN-2_LULC_preprocessed1"
|
11 |
+
|
12 |
+
# --- Image & File Settings ---
|
13 |
+
IMG_SIZE = 256
|
14 |
+
IMG_EXT = ".png" # Assumes source images are PNGs
|
15 |
+
MASK_EXT = ".tif" # Assumes source masks are TIFs
|
16 |
+
|
17 |
+
# --- 2. Define All Data Splits ---
|
18 |
+
SPLITS = {
|
19 |
+
"train": {
|
20 |
+
"images": os.path.join(CFG.SOURCE_ROOT, "train_images", "train"),
|
21 |
+
"masks": os.path.join(CFG.SOURCE_ROOT, "train_masks", "train"),
|
22 |
+
},
|
23 |
+
"val": {
|
24 |
+
"images": os.path.join(CFG.SOURCE_ROOT, "val_images", "val"),
|
25 |
+
"masks": os.path.join(CFG.SOURCE_ROOT, "val_masks", "val"),
|
26 |
+
},
|
27 |
+
"test": {
|
28 |
+
# Test set usually only has images
|
29 |
+
"images": os.path.join(CFG.SOURCE_ROOT, "test_images", "test"),
|
30 |
+
"masks": None, # Set to None if no masks exist
|
31 |
+
},
|
32 |
+
}
|
33 |
+
|
34 |
+
def preprocess_and_resize():
|
35 |
+
"""
|
36 |
+
Resizes images and their corresponding masks for all data splits.
|
37 |
+
This version robustly matches images to masks by filename.
|
38 |
+
"""
|
39 |
+
print(f"Starting preprocessing. Output will be saved to: {CFG.DEST_ROOT}\n")
|
40 |
+
stats = {}
|
41 |
+
|
42 |
+
# --- 3. Iterate Through Each Split (train, val, test) ---
|
43 |
+
for split_name, split_paths in SPLITS.items():
|
44 |
+
print(f"--- Processing split: {split_name} ---")
|
45 |
+
|
46 |
+
# Get paths and create destination directories
|
47 |
+
image_dir = split_paths["images"]
|
48 |
+
mask_dir = split_paths["masks"]
|
49 |
+
|
50 |
+
dest_img_dir = os.path.join(CFG.DEST_ROOT, f"{split_name}_images")
|
51 |
+
os.makedirs(dest_img_dir, exist_ok=True)
|
52 |
+
|
53 |
+
if mask_dir:
|
54 |
+
dest_mask_dir = os.path.join(CFG.DEST_ROOT, f"{split_name}_masks")
|
55 |
+
os.makedirs(dest_mask_dir, exist_ok=True)
|
56 |
+
|
57 |
+
# Find all source images
|
58 |
+
image_files = glob.glob(os.path.join(image_dir, f"*{CFG.IMG_EXT}"))
|
59 |
+
|
60 |
+
if not image_files:
|
61 |
+
print(f"Warning: No images found in {image_dir}. Skipping.")
|
62 |
+
continue
|
63 |
+
|
64 |
+
stats[split_name] = {"images": 0, "masks": 0}
|
65 |
+
|
66 |
+
# --- 4. Robustly Process Each Image and Find its Mask ---
|
67 |
+
for img_path in tqdm(image_files, desc=f"Resizing {split_name} data"):
|
68 |
+
try:
|
69 |
+
# --- Process the image ---
|
70 |
+
base_name = os.path.basename(img_path)
|
71 |
+
with Image.open(img_path) as img:
|
72 |
+
resized_img = img.resize((CFG.IMG_SIZE, CFG.IMG_SIZE), resample=Image.Resampling.LANCZOS)
|
73 |
+
resized_img.save(os.path.join(dest_img_dir, base_name))
|
74 |
+
stats[split_name]["images"] += 1
|
75 |
+
|
76 |
+
# --- Find and process the corresponding mask (if applicable) ---
|
77 |
+
if mask_dir:
|
78 |
+
mask_name = os.path.splitext(base_name)[0] + CFG.MASK_EXT
|
79 |
+
mask_path = os.path.join(mask_dir, mask_name)
|
80 |
+
|
81 |
+
if os.path.exists(mask_path):
|
82 |
+
with Image.open(mask_path) as mask:
|
83 |
+
# CRITICAL: Use NEAREST resampling for masks to preserve class labels
|
84 |
+
resized_mask = mask.resize((CFG.IMG_SIZE, CFG.IMG_SIZE), resample=Image.Resampling.NEAREST)
|
85 |
+
resized_mask.save(os.path.join(dest_mask_dir, mask_name))
|
86 |
+
stats[split_name]["masks"] += 1
|
87 |
+
|
88 |
+
except Exception as e:
|
89 |
+
print(f"Error processing {img_path}: {e}")
|
90 |
+
|
91 |
+
# --- 5. Final Verification Summary ---
|
92 |
+
print("\n--- Preprocessing Complete! ---")
|
93 |
+
for split_name, counts in stats.items():
|
94 |
+
print(f"Split '{split_name}': Processed {counts['images']} images and {counts['masks']} masks.")
|
95 |
+
print(f"Resized data is in '{CFG.DEST_ROOT}'")
|
96 |
+
|
97 |
+
|
98 |
+
if __name__ == "__main__":
|
99 |
+
preprocess_and_resize()
|
run-docker.sh
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
|
3 |
+
# Build the Docker image
|
4 |
+
echo "Building LULC Segmentation Docker image..."
|
5 |
+
docker build -t lulc-segmentation:latest .
|
6 |
+
|
7 |
+
# Function to run the Gradio app
|
8 |
+
run_app() {
|
9 |
+
echo "Starting LULC Segmentation Gradio app..."
|
10 |
+
docker run -it --rm \
|
11 |
+
--gpus all \
|
12 |
+
-p 7860:7860 \
|
13 |
+
-v $(pwd)/data:/app/data \
|
14 |
+
-v $(pwd)/outputs:/app/outputs \
|
15 |
+
-v $(pwd)/models:/app/models \
|
16 |
+
-e GRADIO_SERVER_NAME=0.0.0.0 \
|
17 |
+
lulc-segmentation:latest
|
18 |
+
}
|
19 |
+
|
20 |
+
# Function to run training
|
21 |
+
run_training() {
|
22 |
+
echo "Starting LULC model training..."
|
23 |
+
docker run -it --rm \
|
24 |
+
--gpus all \
|
25 |
+
-v $(pwd)/data:/app/data \
|
26 |
+
-v $(pwd)/outputs:/app/outputs \
|
27 |
+
-v $(pwd)/models:/app/models \
|
28 |
+
lulc-segmentation:latest \
|
29 |
+
python /app/Segmentation-lulc/main3.py
|
30 |
+
}
|
31 |
+
|
32 |
+
# Function to run preprocessing
|
33 |
+
run_preprocessing() {
|
34 |
+
echo "Starting data preprocessing..."
|
35 |
+
docker run -it --rm \
|
36 |
+
-v $(pwd)/data:/app/data \
|
37 |
+
lulc-segmentation:latest \
|
38 |
+
python /app/Segmentation-lulc/pre1.py
|
39 |
+
}
|
40 |
+
|
41 |
+
# Parse command line arguments
|
42 |
+
case "$1" in
|
43 |
+
"app")
|
44 |
+
run_app
|
45 |
+
;;
|
46 |
+
"train")
|
47 |
+
run_training
|
48 |
+
;;
|
49 |
+
"preprocess")
|
50 |
+
run_preprocessing
|
51 |
+
;;
|
52 |
+
"build")
|
53 |
+
echo "Docker image built successfully!"
|
54 |
+
;;
|
55 |
+
*)
|
56 |
+
echo "Usage: $0 {app|train|preprocess|build}"
|
57 |
+
echo " app - Run the Gradio application"
|
58 |
+
echo " train - Run model training"
|
59 |
+
echo " preprocess - Run data preprocessing"
|
60 |
+
echo " build - Build Docker image only"
|
61 |
+
exit 1
|
62 |
+
;;
|
63 |
+
esac
|