Spaces:
Paused
Paused
Delete script.py
Browse files
script.py
DELETED
@@ -1,362 +0,0 @@
|
|
1 |
-
import numpy as np
|
2 |
-
import cv2
|
3 |
-
import trimesh
|
4 |
-
import argparse
|
5 |
-
from PIL import Image
|
6 |
-
from sklearn.cluster import KMeans
|
7 |
-
|
8 |
-
class SatelliteModelGenerator:
|
9 |
-
def __init__(self, building_height=0.05):
|
10 |
-
self.building_height = building_height
|
11 |
-
|
12 |
-
# Reference colors for segmentation (RGB)
|
13 |
-
self.shadow_colors = np.array([
|
14 |
-
[31, 42, 76],
|
15 |
-
[58, 64, 92],
|
16 |
-
[15, 27, 56],
|
17 |
-
[21, 22, 50],
|
18 |
-
[76, 81, 99]
|
19 |
-
])
|
20 |
-
|
21 |
-
self.road_colors = np.array([
|
22 |
-
[187, 182, 175],
|
23 |
-
[138, 138, 138],
|
24 |
-
[142, 142, 129],
|
25 |
-
[202, 199, 189]
|
26 |
-
])
|
27 |
-
|
28 |
-
self.water_colors = np.array([
|
29 |
-
[167, 225, 217],
|
30 |
-
[67, 101, 97],
|
31 |
-
[53, 83, 84],
|
32 |
-
[47, 94, 100],
|
33 |
-
[73, 131, 135]
|
34 |
-
])
|
35 |
-
|
36 |
-
# Convert and normalize reference colors to HSV
|
37 |
-
self.shadow_colors_hsv = cv2.cvtColor(self.shadow_colors.reshape(-1, 1, 3).astype(np.uint8),
|
38 |
-
cv2.COLOR_RGB2HSV).reshape(-1, 3).astype(float)
|
39 |
-
self.road_colors_hsv = cv2.cvtColor(self.road_colors.reshape(-1, 1, 3).astype(np.uint8),
|
40 |
-
cv2.COLOR_RGB2HSV).reshape(-1, 3).astype(float)
|
41 |
-
self.water_colors_hsv = cv2.cvtColor(self.water_colors.reshape(-1, 1, 3).astype(np.uint8),
|
42 |
-
cv2.COLOR_RGB2HSV).reshape(-1, 3).astype(float)
|
43 |
-
|
44 |
-
# Normalize HSV values
|
45 |
-
for colors_hsv in [self.shadow_colors_hsv, self.road_colors_hsv, self.water_colors_hsv]:
|
46 |
-
colors_hsv[:, 0] = colors_hsv[:, 0] * 2
|
47 |
-
colors_hsv[:, 1:] = colors_hsv[:, 1:] / 255
|
48 |
-
|
49 |
-
# Color tolerances from original segmenter
|
50 |
-
self.shadow_tolerance = {'hue': 15, 'sat': 0.15, 'val': 0.12}
|
51 |
-
self.road_tolerance = {'hue': 10, 'sat': 0.12, 'val': 0.15}
|
52 |
-
self.water_tolerance = {'hue': 20, 'sat': 0.15, 'val': 0.20}
|
53 |
-
|
54 |
-
# Output colors (BGR for OpenCV)
|
55 |
-
self.colors = {
|
56 |
-
'black': np.array([0, 0, 0]), # Shadows
|
57 |
-
'blue': np.array([255, 0, 0]), # Water
|
58 |
-
'green': np.array([0, 255, 0]), # Vegetation
|
59 |
-
'gray': np.array([128, 128, 128]), # Roads
|
60 |
-
'brown': np.array([0, 140, 255]), # Terrain
|
61 |
-
'white': np.array([255, 255, 255]) # Buildings
|
62 |
-
}
|
63 |
-
|
64 |
-
# Constants for height estimation
|
65 |
-
self.shadow_search_distance = 5
|
66 |
-
self.min_area_for_clustering = 1000
|
67 |
-
self.residential_height_factor = 0.6
|
68 |
-
self.isolation_threshold = 0.6
|
69 |
-
|
70 |
-
def color_distance_hsv(self, pixel_hsv, reference_hsv, tolerance):
|
71 |
-
"""Calculate if a pixel is within tolerance of reference color in HSV space"""
|
72 |
-
pixel_h = float(pixel_hsv[0]) * 2
|
73 |
-
pixel_s = float(pixel_hsv[1]) / 255
|
74 |
-
pixel_v = float(pixel_hsv[2]) / 255
|
75 |
-
|
76 |
-
hue_diff = min(abs(pixel_h - reference_hsv[0]),
|
77 |
-
360 - abs(pixel_h - reference_hsv[0]))
|
78 |
-
sat_diff = abs(pixel_s - reference_hsv[1])
|
79 |
-
val_diff = abs(pixel_v - reference_hsv[2])
|
80 |
-
|
81 |
-
return (hue_diff <= tolerance['hue'] and
|
82 |
-
sat_diff <= tolerance['sat'] and
|
83 |
-
val_diff <= tolerance['val'])
|
84 |
-
|
85 |
-
def get_dominant_surrounding_color(self, output, y, x):
|
86 |
-
"""Determine dominant non-building color in neighborhood"""
|
87 |
-
height, width = output.shape[:2]
|
88 |
-
surroundings = []
|
89 |
-
|
90 |
-
for dy in [-1, 0, 1]:
|
91 |
-
for dx in [-1, 0, 1]:
|
92 |
-
if dx == 0 and dy == 0:
|
93 |
-
continue
|
94 |
-
|
95 |
-
ny, nx = y + dy, x + dx
|
96 |
-
if 0 <= ny < height and 0 <= nx < width:
|
97 |
-
pixel_color = tuple(output[ny, nx].tolist())
|
98 |
-
if not np.array_equal(output[ny, nx], self.colors['white']):
|
99 |
-
surroundings.append(pixel_color)
|
100 |
-
|
101 |
-
if not surroundings:
|
102 |
-
return None
|
103 |
-
|
104 |
-
surrounding_ratio = len(surroundings) / 8.0
|
105 |
-
|
106 |
-
if surrounding_ratio >= self.isolation_threshold:
|
107 |
-
color_counts = {}
|
108 |
-
for color in surroundings:
|
109 |
-
color_str = str(color)
|
110 |
-
color_counts[color_str] = color_counts.get(color_str, 0) + 1
|
111 |
-
|
112 |
-
most_common = max(color_counts.items(), key=lambda x: x[1])[0]
|
113 |
-
return np.array(eval(most_common))
|
114 |
-
|
115 |
-
return None
|
116 |
-
|
117 |
-
def segment_image(self, img, window_size=5):
|
118 |
-
"""Segment image using improved color detection"""
|
119 |
-
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
120 |
-
output = np.zeros_like(img)
|
121 |
-
|
122 |
-
pad = window_size // 2
|
123 |
-
hsv_pad = np.pad(hsv, ((pad, pad), (pad, pad), (0, 0)), mode='edge')
|
124 |
-
|
125 |
-
height, width = img.shape[:2]
|
126 |
-
|
127 |
-
# First pass: initial segmentation
|
128 |
-
for y in range(height):
|
129 |
-
for x in range(width):
|
130 |
-
window = hsv_pad[y:y+window_size, x:x+window_size]
|
131 |
-
center_hsv = window[pad, pad]
|
132 |
-
|
133 |
-
is_shadow = any(self.color_distance_hsv(center_hsv, ref_hsv, self.shadow_tolerance)
|
134 |
-
for ref_hsv in self.shadow_colors_hsv)
|
135 |
-
|
136 |
-
is_road = any(self.color_distance_hsv(center_hsv, ref_hsv, self.road_tolerance)
|
137 |
-
for ref_hsv in self.road_colors_hsv)
|
138 |
-
|
139 |
-
is_water = any(self.color_distance_hsv(center_hsv, ref_hsv, self.water_tolerance)
|
140 |
-
for ref_hsv in self.water_colors_hsv)
|
141 |
-
|
142 |
-
if is_shadow:
|
143 |
-
output[y, x] = self.colors['black']
|
144 |
-
elif is_water:
|
145 |
-
output[y, x] = self.colors['blue']
|
146 |
-
elif is_road:
|
147 |
-
output[y, x] = self.colors['gray']
|
148 |
-
else:
|
149 |
-
h, s, v = center_hsv
|
150 |
-
h = float(h) * 2 # Convert to 0-360 range
|
151 |
-
s = float(s) / 255
|
152 |
-
v = float(v) / 255
|
153 |
-
|
154 |
-
# Check for pinkish building tones (around red hue with specific saturation)
|
155 |
-
is_pinkish = (
|
156 |
-
((h >= 340 or h <= 15) and # Red-pink hue range
|
157 |
-
0.2 <= s <= 0.6 and # Moderate saturation
|
158 |
-
0.3 <= v <= 0.7) # Moderate brightness
|
159 |
-
)
|
160 |
-
|
161 |
-
# Vegetation detection (green)
|
162 |
-
is_vegetation = (
|
163 |
-
40 <= h <= 150 and
|
164 |
-
s >= 0.15
|
165 |
-
)
|
166 |
-
|
167 |
-
# Soil/dirt detection (yellow-brown, avoiding pinkish tones)
|
168 |
-
is_soil = (
|
169 |
-
15 <= h <= 45 and # Yellow-brown hue range
|
170 |
-
0.15 <= s <= 0.45 and # Lower saturation for dirt
|
171 |
-
not is_pinkish # Exclude pinkish tones
|
172 |
-
)
|
173 |
-
|
174 |
-
if is_pinkish:
|
175 |
-
output[y, x] = self.colors['white'] # Buildings
|
176 |
-
elif is_vegetation:
|
177 |
-
output[y, x] = self.colors['green'] # Vegetation
|
178 |
-
elif is_soil:
|
179 |
-
output[y, x] = self.colors['brown'] # Soil/dirt
|
180 |
-
else:
|
181 |
-
# Default to building for light-colored surfaces
|
182 |
-
output[y, x] = self.colors['white']
|
183 |
-
|
184 |
-
# Second pass: handle isolated building pixels
|
185 |
-
final_output = output.copy()
|
186 |
-
for y in range(height):
|
187 |
-
for x in range(width):
|
188 |
-
if np.array_equal(output[y, x], self.colors['white']):
|
189 |
-
dominant_color = self.get_dominant_surrounding_color(output, y, x)
|
190 |
-
if dominant_color is not None:
|
191 |
-
final_output[y, x] = dominant_color
|
192 |
-
|
193 |
-
return final_output
|
194 |
-
|
195 |
-
def estimate_heights(self, img, segmented):
|
196 |
-
"""Estimate building heights"""
|
197 |
-
buildings_mask = np.all(segmented == self.colors['white'], axis=2)
|
198 |
-
shadows_mask = np.all(segmented == self.colors['black'], axis=2)
|
199 |
-
|
200 |
-
num_buildings, labels = cv2.connectedComponents(buildings_mask.astype(np.uint8))
|
201 |
-
|
202 |
-
areas = np.bincount(labels.flatten())[1:] # Skip background
|
203 |
-
max_area = np.max(areas) if len(areas) > 0 else 1
|
204 |
-
|
205 |
-
height_map = np.zeros_like(labels, dtype=np.float32)
|
206 |
-
|
207 |
-
for label in range(1, num_buildings):
|
208 |
-
building_mask = (labels == label)
|
209 |
-
if not np.any(building_mask):
|
210 |
-
continue
|
211 |
-
|
212 |
-
area = areas[label-1]
|
213 |
-
size_factor = 0.3 + 0.7 * (area / max_area)
|
214 |
-
|
215 |
-
dilated = cv2.dilate(building_mask.astype(np.uint8), np.ones((5,5), np.uint8))
|
216 |
-
shadow_ratio = np.sum(dilated & shadows_mask) / np.sum(dilated)
|
217 |
-
shadow_factor = 0.2 + 0.8 * shadow_ratio
|
218 |
-
|
219 |
-
if area >= self.min_area_for_clustering:
|
220 |
-
building_intensities = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)[building_mask]
|
221 |
-
kmeans = KMeans(n_clusters=2, random_state=42)
|
222 |
-
clusters = kmeans.fit_predict(building_intensities.reshape(-1, 1))
|
223 |
-
cluster_means = [building_intensities[clusters == i].mean() for i in range(2)]
|
224 |
-
height_factor = self.residential_height_factor if cluster_means[0] > cluster_means[1] else 1.0
|
225 |
-
else:
|
226 |
-
height_factor = 1.0
|
227 |
-
|
228 |
-
final_height = size_factor * shadow_factor * height_factor
|
229 |
-
height_map[building_mask] = final_height
|
230 |
-
|
231 |
-
return height_map * 0.15
|
232 |
-
|
233 |
-
def generate_mesh(self, height_map, texture_img, add_walls=True):
|
234 |
-
"""Generate 3D mesh"""
|
235 |
-
height, width = height_map.shape
|
236 |
-
|
237 |
-
x, z = np.meshgrid(np.arange(width), np.arange(height))
|
238 |
-
vertices = np.stack([x, height_map * self.building_height, z], axis=-1)
|
239 |
-
vertices = vertices.reshape(-1, 3)
|
240 |
-
|
241 |
-
scale = max(width, height)
|
242 |
-
vertices[:, 0] = vertices[:, 0] / scale * 2 - (width / scale)
|
243 |
-
vertices[:, 2] = vertices[:, 2] / scale * 2 - (height / scale)
|
244 |
-
vertices[:, 1] = vertices[:, 1] * 2 - 1
|
245 |
-
|
246 |
-
i, j = np.meshgrid(np.arange(height-1), np.arange(width-1), indexing='ij')
|
247 |
-
v0 = (i * width + j).flatten()
|
248 |
-
v1 = v0 + 1
|
249 |
-
v2 = ((i + 1) * width + j).flatten()
|
250 |
-
v3 = v2 + 1
|
251 |
-
|
252 |
-
faces = np.vstack((
|
253 |
-
np.column_stack((v0, v2, v1)),
|
254 |
-
np.column_stack((v1, v2, v3))
|
255 |
-
))
|
256 |
-
|
257 |
-
uvs = np.zeros((vertices.shape[0], 2))
|
258 |
-
uvs[:, 0] = x.flatten() / (width - 1)
|
259 |
-
uvs[:, 1] = 1 - (z.flatten() / (height - 1))
|
260 |
-
|
261 |
-
if len(texture_img.shape) == 3:
|
262 |
-
if texture_img.shape[2] == 4:
|
263 |
-
texture_img = cv2.cvtColor(texture_img, cv2.COLOR_BGRA2RGB)
|
264 |
-
else:
|
265 |
-
texture_img = cv2.cvtColor(texture_img, cv2.COLOR_BGR2RGB)
|
266 |
-
|
267 |
-
mesh = trimesh.Trimesh(
|
268 |
-
vertices=vertices,
|
269 |
-
faces=faces,
|
270 |
-
visual=trimesh.visual.TextureVisuals(
|
271 |
-
uv=uvs,
|
272 |
-
image=Image.fromarray(texture_img)
|
273 |
-
)
|
274 |
-
)
|
275 |
-
|
276 |
-
if add_walls:
|
277 |
-
mesh = self._add_walls(mesh, height_map)
|
278 |
-
|
279 |
-
return mesh
|
280 |
-
|
281 |
-
def _add_walls(self, mesh, height_map):
|
282 |
-
"""Add vertical walls at building edges"""
|
283 |
-
edges = cv2.Canny(height_map.astype(np.uint8) * 255, 100, 200)
|
284 |
-
height, width = height_map.shape
|
285 |
-
scale = max(width, height)
|
286 |
-
|
287 |
-
edge_coords = np.column_stack(np.where(edges > 0))
|
288 |
-
if len(edge_coords) == 0:
|
289 |
-
return mesh
|
290 |
-
|
291 |
-
valid_mask = (edge_coords[:, 0] < height - 1) & (edge_coords[:, 1] < width - 1)
|
292 |
-
edge_coords = edge_coords[valid_mask]
|
293 |
-
|
294 |
-
if len(edge_coords) == 0:
|
295 |
-
return mesh
|
296 |
-
|
297 |
-
y, x = edge_coords.T
|
298 |
-
heights = height_map[y, x]
|
299 |
-
|
300 |
-
top_front = np.column_stack([x, heights * self.building_height, y])
|
301 |
-
top_back = np.column_stack([x + 1, heights * self.building_height, y])
|
302 |
-
bottom_front = np.column_stack([x, np.zeros_like(heights), y])
|
303 |
-
bottom_back = np.column_stack([x + 1, np.zeros_like(heights), y])
|
304 |
-
|
305 |
-
for vertices in [top_front, top_back, bottom_front, bottom_back]:
|
306 |
-
vertices[:, 0] = vertices[:, 0] / scale * 2 - (width / scale)
|
307 |
-
vertices[:, 2] = vertices[:, 2] / scale * 2 - (height / scale)
|
308 |
-
vertices[:, 1] = vertices[:, 1] * 2 - 1
|
309 |
-
|
310 |
-
new_vertices = np.vstack([top_front, top_back, bottom_front, bottom_back])
|
311 |
-
vertex_count = len(edge_coords)
|
312 |
-
|
313 |
-
indices = np.arange(4 * vertex_count).reshape(-1, 4)
|
314 |
-
new_faces = np.vstack([
|
315 |
-
np.column_stack([indices[:, 0], indices[:, 2], indices[:, 1]]),
|
316 |
-
np.column_stack([indices[:, 1], indices[:, 2], indices[:, 3]])
|
317 |
-
])
|
318 |
-
|
319 |
-
base_vertex_count = len(mesh.vertices)
|
320 |
-
mesh.vertices = np.vstack((mesh.vertices, new_vertices))
|
321 |
-
mesh.faces = np.vstack((mesh.faces, new_faces + base_vertex_count))
|
322 |
-
|
323 |
-
return mesh
|
324 |
-
|
325 |
-
def main():
|
326 |
-
parser = argparse.ArgumentParser(description='Generate 3D mesh from satellite image')
|
327 |
-
parser.add_argument('input_image', help='Path to satellite image')
|
328 |
-
parser.add_argument('output_mesh', help='Path for output GLB file')
|
329 |
-
parser.add_argument('--segmented_output', help='Optional path to save segmented image')
|
330 |
-
parser.add_argument('--height', type=float, default=0.09, help='Height of buildings (default: 0.09)')
|
331 |
-
parser.add_argument('--no_walls', action='store_true', help='Skip generating vertical walls')
|
332 |
-
parser.add_argument('--window_size', type=int, default=5, help='Window size for segmentation analysis')
|
333 |
-
|
334 |
-
args = parser.parse_args()
|
335 |
-
|
336 |
-
# Load image
|
337 |
-
img = cv2.imread(args.input_image)
|
338 |
-
if img is None:
|
339 |
-
raise ValueError(f"Could not read image at {args.input_image}")
|
340 |
-
|
341 |
-
generator = SatelliteModelGenerator(building_height=args.height)
|
342 |
-
|
343 |
-
# Process image
|
344 |
-
print("Segmenting image...")
|
345 |
-
segmented_img = generator.segment_image(img, args.window_size)
|
346 |
-
|
347 |
-
print("Estimating heights...")
|
348 |
-
height_map = generator.estimate_heights(img, segmented_img)
|
349 |
-
|
350 |
-
# Save segmented image if requested
|
351 |
-
if args.segmented_output:
|
352 |
-
cv2.imwrite(args.segmented_output, segmented_img)
|
353 |
-
print(f"Segmented image saved to {args.segmented_output}")
|
354 |
-
|
355 |
-
# Generate and save mesh
|
356 |
-
print("Generating mesh...")
|
357 |
-
mesh = generator.generate_mesh(height_map, img, add_walls=not args.no_walls)
|
358 |
-
mesh.export(args.output_mesh)
|
359 |
-
print(f"Mesh exported to {args.output_mesh}")
|
360 |
-
|
361 |
-
if __name__ == "__main__":
|
362 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|