daniel-crawford-dunedain commited on
Commit
bfee33f
·
verified ·
1 Parent(s): 484ffb5

Initial Space upload

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/example.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,12 +1,18 @@
1
- ---
2
- title: Road Detection Model
3
- emoji: 🌖
4
- colorFrom: purple
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 5.43.1
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
1
+ # PyTorch `.pth` Model – Hugging Face Space
2
+
3
+ This Space hosts a custom PyTorch model loaded from `weights/road_detection_model.pth`.
4
+
5
+ ## Inputs
6
+ - Satellite Image
7
+
8
+ ## Programmatic Usage
9
+
10
+ ```python
11
+ from gradio_client import Client
12
+
13
+ client = Client("https://<ORG_OR_USER>-<SPACE_NAME>.hf.space")
14
+ res = client.predict(
15
+ {x: '<IMAGE>'},
16
+ api_name="/predict"
17
+ )
18
+ print(res) # {"pred": int, "probs": [ ... ]}
__pycache__/push_to_hf.cpython-312.pyc ADDED
Binary file (1.75 kB). View file
 
app.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import torch
4
+ import gradio as gr
5
+ import cv2
6
+ import numpy as np
7
+ from PIL import Image
8
+ from torchvision import transforms
9
+ import base64
10
+ from io import BytesIO
11
+
12
+ from model_def import build_model
13
+
14
+ # ---------- Config ----------
15
+ WEIGHTS_PATH = os.environ.get("WEIGHTS_PATH", "weights/road_detection_model.pth")
16
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
17
+ MODEL = None
18
+ MODEL_EVAL = True # turn off dropout/bn if True
19
+
20
+ THRESHOLD = 0.9
21
+ MIN_AREA = 25
22
+ KERNEL_SIZE = 3
23
+ ASPECT_RATIO = 2
24
+ PERIMETER = 1
25
+ CONNECTIVITY = 8
26
+
27
+ # If your model expects different input (e.g., images, text), adapt preprocess/postprocess accordingly.
28
+ def load_model():
29
+ global MODEL
30
+ MODEL = build_model()
31
+ # If you saved state_dict:
32
+ state = torch.load(WEIGHTS_PATH, map_location="cpu")
33
+ MODEL.load_state_dict(state)
34
+ MODEL.to(DEVICE)
35
+ if MODEL_EVAL:
36
+ MODEL.eval()
37
+
38
+ def preprocess(raw_input):
39
+ """
40
+ Preprocess the input image for road detection.
41
+
42
+ Args:
43
+ raw_input: Can be base64 string, file path, or PIL Image
44
+
45
+ Returns:
46
+ torch.Tensor: Preprocessed image tensor ready for model inference
47
+ """
48
+ # Handle different input types
49
+ if isinstance(raw_input, str):
50
+ # Check if it's a base64 string
51
+ if raw_input.startswith('data:image'):
52
+ # Extract base64 data
53
+ base64_data = raw_input.split(',')[1]
54
+ image_data = base64.b64decode(base64_data)
55
+ image = Image.open(BytesIO(image_data)).convert('RGB')
56
+ else:
57
+ # Assume it's a file path
58
+ image = Image.open(raw_input).convert('RGB')
59
+ elif isinstance(raw_input, dict) and 'image' in raw_input:
60
+ # Handle Gradio image input
61
+ image = Image.fromarray(raw_input['image']).convert('RGB')
62
+ else:
63
+ # Assume it's already a PIL Image
64
+ image = raw_input.convert('RGB')
65
+
66
+ # Resize image to model input size (256x256)
67
+ input_size = 256
68
+ image = image.resize((input_size, input_size), Image.LANCZOS)
69
+
70
+ # Convert to tensor and normalize
71
+ transform = transforms.Compose([
72
+ transforms.ToTensor(),
73
+ ])
74
+
75
+ # Add batch dimension
76
+ tensor = transform(image).unsqueeze(0)
77
+
78
+ return tensor.to(DEVICE)
79
+
80
+ def _clean_road_mask(mask: np.ndarray, min_area: int = MIN_AREA, kernel_size: int = KERNEL_SIZE) -> np.ndarray:
81
+ """
82
+ Clean the road mask by removing small disconnected segments and improving connectivity.
83
+ Preserves long, thin roads by considering aspect ratio and perimeter.
84
+
85
+ Args:
86
+ mask (np.ndarray): Binary road mask (0-255)
87
+ min_area (int): Minimum area in pixels for a road segment to be kept
88
+ kernel_size (int): Size of morphological operation kernel
89
+
90
+ Returns:
91
+ np.ndarray: Cleaned binary road mask
92
+ """
93
+ # Convert to binary (0 or 1)
94
+ binary_mask = (mask > 127).astype(np.uint8)
95
+
96
+ # Create kernel for morphological operations
97
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
98
+
99
+ # 1. Remove small noise with opening (erosion followed by dilation)
100
+ cleaned = cv2.morphologyEx(binary_mask, cv2.MORPH_OPEN, kernel)
101
+
102
+ # 2. Fill small holes with closing (dilation followed by erosion)
103
+ cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, kernel)
104
+
105
+ # 3. Remove small connected components (islands) with better handling of thin roads
106
+ num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(cleaned, connectivity=CONNECTIVITY)
107
+
108
+ # Create output mask
109
+ cleaned_mask = np.zeros_like(cleaned)
110
+
111
+ # Keep components based on area AND shape characteristics
112
+ for i in range(1, num_labels): # Start from 1 to skip background
113
+ area = stats[i, cv2.CC_STAT_AREA]
114
+ width = stats[i, cv2.CC_STAT_WIDTH]
115
+ height = stats[i, cv2.CC_STAT_HEIGHT]
116
+
117
+ # Calculate aspect ratio (length/width)
118
+ aspect_ratio = max(width, height) / max(min(width, height), 1)
119
+
120
+ # Calculate perimeter (approximate)
121
+ perimeter = 2 * (width + height)
122
+
123
+ # Keep if:
124
+ # 1. Area is large enough, OR
125
+ # 2. It's a long, thin structure (high aspect ratio and reasonable perimeter)
126
+ if (area >= min_area or
127
+ (aspect_ratio >= 3 and perimeter >= 20 and area >= 5)):
128
+ cleaned_mask[labels == i] = 1
129
+
130
+ # 4. Apply additional dilation to connect nearby road segments
131
+ if kernel_size > 1:
132
+ cleaned_mask = cv2.dilate(cleaned_mask, kernel, iterations=1)
133
+ # Clean up again after dilation
134
+ cleaned_mask = cv2.morphologyEx(cleaned_mask, cv2.MORPH_CLOSE, kernel)
135
+
136
+ return cleaned_mask.astype(np.uint8) * 255
137
+
138
+ def postprocess(logits):
139
+ """
140
+ Postprocess the model output to create a clean road mask.
141
+
142
+ Args:
143
+ logits: Model output tensor
144
+
145
+ Returns:
146
+ dict: Contains the processed mask and metadata
147
+ """
148
+ # Convert logits to probabilities
149
+ if isinstance(logits, torch.Tensor):
150
+ # Apply sigmoid if not already applied
151
+ if logits.max() > 1.0:
152
+ probabilities = torch.sigmoid(logits)
153
+ else:
154
+ probabilities = logits
155
+
156
+ # Convert to numpy
157
+ mask_np = probabilities.squeeze().cpu().numpy()
158
+ else:
159
+ mask_np = logits
160
+
161
+ # Threshold the mask
162
+ binary_mask = (mask_np > THRESHOLD).astype(np.uint8) * 255
163
+
164
+ # Clean the mask using morphological operations
165
+ cleaned_mask = _clean_road_mask(binary_mask)
166
+
167
+ # Convert to PIL Image for easier handling
168
+ mask_image = Image.fromarray(cleaned_mask)
169
+
170
+ # Calculate statistics
171
+ road_pixels = np.sum(cleaned_mask > 0)
172
+ total_pixels = cleaned_mask.size
173
+ road_percentage = (road_pixels / total_pixels) * 100
174
+
175
+ # Create result dictionary
176
+ result = {
177
+ "mask": mask_image,
178
+ "road_percentage": round(road_percentage, 2),
179
+ "road_pixels": int(road_pixels),
180
+ "total_pixels": int(total_pixels),
181
+ "threshold_used": THRESHOLD,
182
+ "mask_shape": cleaned_mask.shape
183
+ }
184
+
185
+ return result
186
+
187
+ @torch.inference_mode()
188
+ def predict(raw_input):
189
+ """
190
+ Main prediction function that processes input and returns road detection results.
191
+
192
+ Args:
193
+ raw_input: Input image (base64, file path, or PIL Image)
194
+
195
+ Returns:
196
+ dict: Road detection results with mask and metadata
197
+ """
198
+ try:
199
+ # Preprocess input
200
+ x = preprocess(raw_input)
201
+
202
+ # Run model inference
203
+ logits = MODEL(x)
204
+
205
+ # Postprocess results
206
+ result = postprocess(logits)
207
+
208
+ return result
209
+
210
+ except Exception as e:
211
+ return {
212
+ "error": str(e),
213
+ "status": "failed"
214
+ }
215
+
216
+ def gradio_ui():
217
+ """Create Gradio interface for road detection."""
218
+ with gr.Blocks(title="Road Detection Model") as demo:
219
+ gr.Markdown("# 🛣️ Road Detection Model")
220
+ gr.Markdown("Upload a satellite image to detect roads.")
221
+
222
+ with gr.Row():
223
+ with gr.Column():
224
+ # Input
225
+ input_image = gr.Image(
226
+ label="Upload Satellite Image",
227
+ type="pil",
228
+ height=400
229
+ )
230
+
231
+ # Parameters
232
+ with gr.Accordion("Advanced Parameters", open=False):
233
+ threshold = gr.Slider(
234
+ minimum=0.1,
235
+ maximum=0.99,
236
+ value=THRESHOLD,
237
+ step=0.01,
238
+ label="Detection Threshold"
239
+ )
240
+ min_area = gr.Slider(
241
+ minimum=10,
242
+ maximum=100,
243
+ value=MIN_AREA,
244
+ step=5,
245
+ label="Minimum Road Area (pixels)"
246
+ )
247
+
248
+ # Run button
249
+ run_btn = gr.Button("🚀 Detect Roads", variant="primary")
250
+
251
+ with gr.Column():
252
+ # Output
253
+ output_image = gr.Image(
254
+ label="Detected Roads",
255
+ height=400
256
+ )
257
+
258
+ # Statistics
259
+ with gr.Accordion("Detection Statistics", open=True):
260
+ road_percentage = gr.Number(
261
+ label="Road Coverage (%)",
262
+ precision=2
263
+ )
264
+ road_pixels = gr.Number(
265
+ label="Road Pixels",
266
+ precision=0
267
+ )
268
+ total_pixels = gr.Number(
269
+ label="Total Pixels",
270
+ precision=0
271
+ )
272
+
273
+ # Example images
274
+ gr.Examples(
275
+ examples=[
276
+ ["examples/example.jpg"]
277
+ ],
278
+ inputs=input_image,
279
+ label="Example Image"
280
+ )
281
+
282
+ # Define prediction function
283
+ def predict_with_params(image, thresh, area):
284
+ global THRESHOLD, MIN_AREA
285
+ THRESHOLD = thresh
286
+ MIN_AREA = int(area)
287
+
288
+ if image is None:
289
+ return None, 0, 0, 0
290
+
291
+ result = predict(image)
292
+
293
+ if "error" in result:
294
+ return None, 0, 0, 0
295
+
296
+ return (
297
+ result["mask"],
298
+ result["road_percentage"],
299
+ result["road_pixels"],
300
+ result["total_pixels"]
301
+ )
302
+
303
+ # Connect components
304
+ run_btn.click(
305
+ fn=predict_with_params,
306
+ inputs=[input_image, threshold, min_area],
307
+ outputs=[output_image, road_percentage, road_pixels, total_pixels]
308
+ )
309
+
310
+ return demo
311
+
312
+ if __name__ == "__main__":
313
+ load_model()
314
+ ui = gradio_ui()
315
+ ui.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))
316
+ else:
317
+ # For Spaces
318
+ load_model()
319
+ demo = gradio_ui()
examples/example.png ADDED

Git LFS Details

  • SHA256: e8b0ed8beda59aacd665584f09bcc9d610dd6cdd1381947fb710c998d49af5fd
  • Pointer size: 131 Bytes
  • Size of remote file: 285 kB
model_def.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+
5
+ # --- UNet Model Definition ---
6
+ class UNet(nn.Module):
7
+ def __init__(self, in_channels: int = 3, out_channels: int = 1, features: List[int] = [64, 128, 256, 512]):
8
+ super(UNet, self).__init__()
9
+ self.encoder = nn.ModuleList()
10
+ self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
11
+ self.decoder = nn.ModuleList()
12
+
13
+ # Encoder
14
+ for feature in features:
15
+ self.encoder.append(self._conv_block(in_channels, feature))
16
+ in_channels = feature
17
+
18
+ # Bottleneck
19
+ self.bottleneck = self._conv_block(features[-1], features[-1]*2)
20
+
21
+ # Decoder
22
+ for feature in reversed(features):
23
+ self.decoder.append(
24
+ nn.ConvTranspose2d(feature*2, feature, kernel_size=2, stride=2)
25
+ )
26
+ self.decoder.append(self._conv_block(feature*2, feature))
27
+
28
+ self.final_conv = nn.Conv2d(features[0], out_channels, kernel_size=1)
29
+
30
+ def forward(self, x):
31
+ skip_connections = []
32
+ for down in self.encoder:
33
+ x = down(x)
34
+ skip_connections.append(x)
35
+ x = self.pool(x)
36
+ x = self.bottleneck(x)
37
+ skip_connections = skip_connections[::-1]
38
+ for idx in range(0, len(self.decoder), 2):
39
+ x = self.decoder[idx](x)
40
+ skip_connection = skip_connections[idx//2]
41
+ if x.shape != skip_connection.shape:
42
+ x = F.interpolate(x, size=skip_connection.shape[2:])
43
+ x = torch.cat((skip_connection, x), dim=1)
44
+ x = self.decoder[idx+1](x)
45
+ return torch.sigmoid(self.final_conv(x))
46
+
47
+ @staticmethod
48
+ def _conv_block(in_channels, out_channels):
49
+ return nn.Sequential(
50
+ nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, bias=False),
51
+ nn.BatchNorm2d(out_channels),
52
+ nn.ReLU(inplace=True),
53
+ nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, bias=False),
54
+ nn.BatchNorm2d(out_channels),
55
+ nn.ReLU(inplace=True),
56
+ )
57
+
58
+
59
+ def build_model():
60
+ # If you need custom args (e.g., from a config.json), read & pass them here.
61
+ return UNet()
push_to_hf.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+
3
+ load_dotenv()
4
+
5
+
6
+
7
+ import os
8
+ import shutil
9
+ from huggingface_hub import HfApi, create_repo, upload_folder
10
+
11
+ # ---- Setup ----
12
+ # 1) Put your token in env: export HUGGING_FACE_INFERENCE_TOKEN=hf_************************
13
+ # 2) Choose target: either your username or an organization you belong to.
14
+ HF_TOKEN = os.environ.get("HUGGING_FACE_INFERENCE_TOKEN")
15
+ assert HF_TOKEN, "Set HUGGINGFACE_TOKEN env var."
16
+
17
+ # Change these:
18
+ SPACE_OWNER = "dunedain-ai"
19
+ SPACE_NAME = "road-detection-model"
20
+ SPACE_SDK = "gradio" # Space SDK: gradio | streamlit | static | ...
21
+ REPO_ID = f"{SPACE_OWNER}/{SPACE_NAME}"
22
+
23
+ # Folder containing your app.py etc.
24
+ LOCAL_DIR = os.path.dirname(os.path.abspath(__file__))
25
+
26
+ def main():
27
+ print('Accessing Hugging Face...')
28
+ api = HfApi(token=HF_TOKEN)
29
+ print('Hugging Face Accessed!')
30
+
31
+ # Create Space repo (idempotent; set exist_ok=True)
32
+ print('Creating Repo...')
33
+ create_repo(
34
+ repo_id=REPO_ID,
35
+ repo_type="space",
36
+ space_sdk=SPACE_SDK,
37
+ private=False, # set True for private
38
+ exist_ok=True
39
+ )
40
+ print('Repo Created!')
41
+
42
+
43
+ # Upload everything in this directory
44
+ upload_folder(
45
+ repo_id=REPO_ID,
46
+ repo_type="space",
47
+ folder_path=LOCAL_DIR,
48
+ commit_message="Initial Space upload"
49
+ )
50
+
51
+ print(f"✅ Uploaded. Space: https://huggingface.co/spaces/{REPO_ID}")
52
+
53
+ if __name__ == "__main__":
54
+ main()
requirements.txt ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core ML and Deep Learning
2
+ torch>=2.0.0
3
+ torchvision>=0.15.0
4
+ numpy>=1.21.0
5
+
6
+ # Computer Vision
7
+ opencv-python>=4.8.0
8
+ Pillow>=9.0.0
9
+
10
+ # Web Interface
11
+ gradio>=4.0.0
12
+
13
+ # Image Processing
14
+ scikit-image>=0.20.0
15
+
16
+ # Utilities
17
+ requests>=2.28.0
18
+ python-dotenv>=1.0.0
19
+
20
+ # Optional: For Hugging Face integration
21
+ transformers>=4.30.0
22
+ huggingface-hub>=0.16.0
23
+
24
+ # Optional: For advanced image processing
25
+ scipy>=1.10.0
26
+
27
+ # Optional: For better performance
28
+ accelerate>=0.20.0
29
+
30
+ # Development and testing (optional)
31
+ pytest>=7.0.0
32
+ black>=23.0.0
33
+ flake8>=6.0.0
runtime.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python-3.12
weights/road_detection_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4f2ccb110c1c5b7eaca81b1ee136737cc8c1a0feccde2b0b9721a9a7fd5a09e5
3
+ size 124236775